Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Capture response #41

Merged
merged 14 commits into from
Oct 26, 2022
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 @@ -520,6 +520,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",
"scripts": {
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