Skip to content

Commit

Permalink
Capture response (#41)
Browse files Browse the repository at this point in the history
* Added circleCI pipeline

* Added circleCI pipeline

* Added the capture responses functionalities

* Fixed npm vulnerabilities

* Updated examples and docs

* Removed double copyright header

* 2.5.0

* CaptureResponse.js
- Added check for null headers and null location in captureResponse constructor
- Simplified location set in constructor
captureResponse.spec.js
- Added tests for constructor's conditions
- Moved hardcoded values to variables (local and from responses.js)

* - Fixed tests
- Updated documentation
- The capture job information are now fetched by appending the capture job ID to 'API_URL/capture/'
- Added more tests for the capture job polling
- renamed the capture job polling function
- Applied linter

* Fixed example with full possibility

* Updated mock time to push `pollForTheCaptureToFinish` to its limit (it resolves just before the last try)

* Updated version

* Update test/helper/responses.js

Co-authored-by: larizgoitia <iker@evrythng.com>

Co-authored-by: larizgoitia <iker@evrythng.com>
  • Loading branch information
clementh59 and larizgoitia authored Oct 26, 2022
1 parent 885e4c2 commit f528631
Show file tree
Hide file tree
Showing 13 changed files with 337 additions and 18 deletions.
19 changes: 19 additions & 0 deletions .circleci/config.yml
Original file line number Diff line number Diff line change
@@ -0,0 +1,19 @@
version: 2.1
jobs:
build:
docker:
- image: node:14-alpine
steps:
- checkout
- run:
name: 'Install dependencies'
command: 'npm ci'
- run:
name: 'Build SDK'
command: 'npm run build'
- run:
name: 'Audit dependencies'
command: 'npm audit'
- run:
name: 'Run tests'
command: 'npm test'
21 changes: 20 additions & 1 deletion README.md
Original file line number Diff line number Diff line change
Expand Up @@ -56,7 +56,7 @@ import * as epcis from 'epcis2.js';

Or use a simple script tag to load it from the CDN:
```html
<script src="https://cdn.jsdelivr.net/npm/epcis2.js@2.4.1/dist/epcis2.browser.js"></script>
<script src="https://cdn.jsdelivr.net/npm/epcis2.js@2.5.0/dist/epcis2.browser.js"></script>
```

Then use in a browser `script` tag using the `epcis2` global variable:
Expand Down Expand Up @@ -548,6 +548,25 @@ You can override all the parameters defined in the previous section in the secon
If the `documentValidation` field of the settings is set to `true`, and the EPCISDocument hasn't a valid syntax, the
function throws an error.

### Handling the capture response

To check if the EPCIS document has been digested correctly, you can use the CaptureResponse class.
```js
let res = await capture(epcisDocument);
const cr = new CaptureResponse(res);
// this function polls the capture endpoint until the EPCIS document has been handled by the API. The first param is the
// number of retry, the second is the delay between each poll and here, we override the timeout of requests to be
// 2s. In this example, it will poll 5 times, with 2s between each poll.
await cr.pollForTheCaptureToFinish(5, 2000, { timeout: 2000 });
console.log(`Running status: ${cr.getRunningStatus()}`);
console.log(`Success status: ${cr.getSuccessStatus()}`);
console.log(`errors: ${cr.getErrors()}`);
console.log(`location: ${cr.getLocation()}`);
```

As for the capture function, you can override all the parameters defined in the settings in the 3rd parameters of the
`pollForTheCaptureToFinish` function.

## Contributing

### Build
Expand Down
12 changes: 10 additions & 2 deletions example/node_example/example_with_full_possibility.js
Original file line number Diff line number Diff line change
Expand Up @@ -23,7 +23,8 @@ const {
setup,
capture,
cbv,
vtype
vtype,
CaptureResponse
} = require('epcis2.js');

// you can override the global parameters with the setup function
Expand Down Expand Up @@ -486,7 +487,8 @@ const sendACaptureRequestExample = async () => {
// epcisDocument
epcisDocument.setContext(['https://ref.gs1.org/standards/epcis/2.0.0/epcis-context.jsonld',{
example: 'http://ns.example.com/epcis/',
ext1: 'http://example.com/ext1/'
ext1: 'http://example.com/ext1/',
evt: 'https://evrythng.com/context'
}])
.setCreationDate('2013-06-04T14:59:02.099+02:00')
.setSchemaVersion('2.0')
Expand All @@ -510,6 +512,12 @@ const sendACaptureRequestExample = async () => {
// capture
console.log('Capture:')
let res = await capture(epcisDocument);
const cr = new CaptureResponse(res);
await cr.pollForTheCaptureToFinish();
console.log(`Running status: ${cr.getRunningStatus()}`);
console.log(`Success status: ${cr.getSuccessStatus()}`);
console.log(`errors: ${cr.getErrors()}`);
console.log(`location: ${cr.getLocation()}`);
let text = await res.text();
console.log(`Request status: ${res.status}`);
console.log(`Request response: ${text}`);
Expand Down
2 changes: 1 addition & 1 deletion example/web-example/index.html
Original file line number Diff line number Diff line change
Expand Up @@ -3,7 +3,7 @@
<head>
<meta charset="UTF-8">
<title>EPCIS2.js demo</title>
<script src="https://cdn.jsdelivr.net/npm/epcis2.js@2.4.1/dist/epcis2.browser.js" defer></script>
<script src="https://cdn.jsdelivr.net/npm/epcis2.js@2.5.0/dist/epcis2.browser.js" defer></script>
<script>
window.onload = () => {
const doc = new epcis2.EPCISDocument();
Expand Down
2 changes: 1 addition & 1 deletion package-lock.json

Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.

2 changes: 1 addition & 1 deletion package.json
Original file line number Diff line number Diff line change
@@ -1,6 +1,6 @@
{
"name": "epcis2.js",
"version": "2.4.1",
"version": "2.5.0",
"description": "Javascript SDK for the EPCIS 2.0 standard",
"main": "./dist/epcis2.node.js",
"browser": "./dist/epcis2.browser.js",
Expand Down
112 changes: 112 additions & 0 deletions src/entity/epcis/CaptureResponse.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,112 @@
/**
* (c) Copyright Reserved EVRYTHNG Limited 2021. All rights reserved.
* Use of this material is subject to license.
* Copying and unauthorised use of this material strictly prohibited.
*/

import request from '../../request/request';
import { timer } from '../../utils/utils';

export default class CaptureResponse {
/**
* You can only provide an already existing CaptureResponse
* via Object
* @param {Object} [captureResponse] - The object that will be used to create the epcisHeader
* entity
*/
constructor(captureResponse) {
if (!captureResponse) {
throw new Error('A capture response must be provided to the constructor');
}
if (!captureResponse.headers) {
throw new Error('A capture response must have headers');
}
if (!captureResponse.headers.get('location')) {
throw new Error('A capture response must have a location property in the headers');
}
this.location = captureResponse.headers.get('location');
this.fetched = null;
}

/**
* Getter for the location
* @return {string}
*/
getLocation() {
return this.location;
}

/**
* Getter for the running property
* @return {boolean}
*/
getRunningStatus() {
if (!this.fetched) throw new Error('The capture job needs to be fetched to get the running status');
return this.running;
}

/**
* Getter for the success property
* @return {boolean}
*/
getSuccessStatus() {
if (!this.fetched) throw new Error('The capture job needs to be fetched to get the success status');
return this.success;
}

/**
* Getter for the errors property
* @return {Array<string>}
*/
getErrors() {
if (!this.fetched) throw new Error('The capture job needs to be fetched to get the errors');
return this.errors;
}

/**
* Getter for the error file property
* @return {Object}
*/
getErrorFile() {
if (!this.fetched) throw new Error('The capture job needs to be fetched to get the error file');
return this.errorFile;
}

async getCaptureJob(options = {}) {
const captureId = this.location.split('/').pop();
const endpoint = `capture/${captureId}`;
const res = await request(endpoint, options);
const json = await res.json();

this.success = json.success;
this.errors = json.errors;
this.running = json.running;
this.errorFile = json.errorFile;
this.fetched = true;

return json;
}

/**
* Fetch the capture job information until the running field is equal to false. Stop trying after
* [nbRetry] tries.
* @param {number} nbRetry - how much time should it fetch the capture job until aborted
* @param {number} delay - the delay between each call, in ms (2000 by default)
* @param {Object} options - the request options
* @returns {Promise<void>}
*/
async pollForTheCaptureToFinish(nbRetry = 5, delay = 2000, options = {}) {
let tries = 0;

/* eslint-disable no-await-in-loop */
do {
if (tries !== 0) {
await timer(delay);
}

await this.getCaptureJob(options);
tries += 1;
} while (tries < nbRetry && this.running);
/* eslint-enable no-await-in-loop */
}
}
1 change: 1 addition & 0 deletions src/index.js
Original file line number Diff line number Diff line change
Expand Up @@ -7,6 +7,7 @@
export { default as EPCISDocument } from './entity/epcis/EPCISDocument';
export { default as EPCISMasterData } from './entity/epcis/EPCISMasterData';
export { default as EPCISHeader } from './entity/epcis/EPCISHeader';
export { default as CaptureResponse } from './entity/epcis/CaptureResponse';
export { default as ObjectEvent } from './entity/events/ObjectEvent';
export { default as ExtendedEvent } from './entity/events/ExtendedEvent';
export { default as TransformationEvent } from './entity/events/TransformationEvent';
Expand Down
7 changes: 7 additions & 0 deletions src/utils/utils.js
Original file line number Diff line number Diff line change
Expand Up @@ -238,3 +238,10 @@ export function isJsonObject(data) {
}
return true;
}

/**
* Wait for [ms] ms to resolve.
* @param {number} ms - the time to wait in milliseconds.
* @returns {Promise<unknown>}
*/
export const timer = (ms) => new Promise((res) => setTimeout(res, ms));
119 changes: 119 additions & 0 deletions test/capture/captureResponse.spec.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,119 @@
/**
* (c) Copyright Reserved EVRYTHNG Limited 2021. All rights reserved.
* Use of this material is subject to license.
* Copying and unauthorised use of this material strictly prohibited.
*/

import { expect } from 'chai';
import { CaptureResponse } from '../../src';
import * as sdk from '../../src';
import {
mockCaptureJobIsFinished, mockCaptureJobIsNotFinished, prepare, tearDown,
} from '../helper/apiMock';
import setup from '../../src/setup';
import settings from '../../src/settings';
import responses from '../helper/responses';

const initialSettings = { ...settings };

let req;

const locationTestValue = 'capture/CAPTURE_JOB_ID';
const captureResponse = { headers: new Map() };
captureResponse.headers.set('location', locationTestValue);

describe('Unit tests: Capture response', () => {
beforeEach((done) => {
prepare();
done();
});

afterEach((done) => {
setup(initialSettings);
if (req) {
req.then(() => tearDown(done)).catch(done.fail);
} else {
done();
}
});

it('constructor should create a valid capture reponse', async () => {
const cr = new sdk.CaptureResponse(captureResponse);
expect(cr.getLocation()).to.be.equal(locationTestValue);
});

it('constructor should throw when passing a null object', async () => {
const error = 'A capture response must be provided to the constructor';
expect(() => new CaptureResponse()).to.throw(error);
});

it('constructor should throw when passing an object without headers', async () => {
const error = 'A capture response must have headers';
expect(() => new CaptureResponse({})).to.throw(error);
});

it('constructor should throw when passing an object without location', async () => {
const error = 'A capture response must have a location property';
const wrongCaptureResponse = { headers: new Map() };
wrongCaptureResponse.headers.set('test', 'test');
expect(() => new CaptureResponse(wrongCaptureResponse)).to.throw(error);
});

it('should create a valid capture reponse', async () => {
const cr = new sdk.CaptureResponse(captureResponse);
expect(cr.getLocation()).to.be.equal(locationTestValue);
});

it('should throw when passing a wrong object', async () => {
const error = 'A capture response must be provided to the constructor';
expect(() => new CaptureResponse()).to.throw(error);
});

it('should not be able to access capture job field before fetching them', async () => {
const cr = new sdk.CaptureResponse(captureResponse);
expect(() => cr.getErrors()).to.throw('The capture job needs to be fetched to get the errors');
expect(() => cr.getErrorFile()).to.throw('The capture job needs to be fetched to get the error file');
expect(() => cr.getSuccessStatus()).to.throw('The capture job needs to be fetched to get the success status');
expect(() => cr.getRunningStatus()).to.throw('The capture job needs to be fetched to get the running status');
});

it('should fetch the capture job information', async () => {
const cr = new sdk.CaptureResponse(captureResponse);
const res = await cr.getCaptureJob();
expect(res).to.be.deep.equal(responses.captureJob);
});

it('should automatically update the information', async () => {
const cr = new sdk.CaptureResponse(captureResponse);
await cr.getCaptureJob();
expect(cr.getErrors()).to.be.deep.equal([]);
expect(cr.getSuccessStatus()).to.be.deep.equal(true);
expect(cr.getRunningStatus()).to.be.deep.equal(false);
});

it('should wait for the capture job to be complete (when result is available on the first try)', async () => {
const cr = new sdk.CaptureResponse(captureResponse);
await cr.pollForTheCaptureToFinish(5, 2000);
expect(cr.getRunningStatus()).to.be.equal(false);
});

it("should wait for the capture job to be complete (when result isn't available on the first try)", async () => {
const cr = new sdk.CaptureResponse(captureResponse);
// we mock the capture result to 'running'
mockCaptureJobIsNotFinished();
// we mock the capture result to 'success' in 250ms
mockCaptureJobIsFinished(350);
await cr.pollForTheCaptureToFinish(5, 100);
expect(cr.getRunningStatus()).to.be.equal(false);
});

it("should wait for the capture job to be complete (when result isn't available after all tries)", async () => {
const cr = new sdk.CaptureResponse(captureResponse);
// we mock the capture result to 'running'
mockCaptureJobIsNotFinished();
// we mock the capture result to 'success' in 250ms
mockCaptureJobIsFinished(600);
await cr.pollForTheCaptureToFinish(5, 100);
expect(cr.getRunningStatus()).to.be.equal(true);
});
});
6 changes: 6 additions & 0 deletions test/exports.spec.js
Original file line number Diff line number Diff line change
Expand Up @@ -125,6 +125,12 @@ describe('All the functions should be well exported', () => {
sourceElement.setSource('urn:epc:id:sgln:4012345.00225.0');
expect(sourceElement.getSource()).to.be.equal('urn:epc:id:sgln:4012345.00225.0');
});
it('CaptureResponse', async () => {
const captureResponse = { headers: new Map() };
captureResponse.headers.set('location', 'capture/CAPTURE_JOB_ID');
const cr = new sdk.CaptureResponse(captureResponse);
expect(cr.getLocation()).to.be.equal('capture/CAPTURE_JOB_ID');
});
it('Vocabulary', async () => {
const vocabulary = new sdk.Vocabulary();
vocabulary.setType('test');
Expand Down
Loading

0 comments on commit f528631

Please sign in to comment.