diff --git a/package-lock.json b/package-lock.json index b2b87838..7a1d7de2 100644 --- a/package-lock.json +++ b/package-lock.json @@ -403,6 +403,11 @@ "resolved": "https://registry.npmjs.org/@types/tough-cookie/-/tough-cookie-2.3.4.tgz", "integrity": "sha512-Set5ZdrAaKI/qHdFlVMgm/GsAv/wkXhSTuZFkJ+JI7HK+wIkIlOaUXSXieIvJ0+OvGIqtREFoE+NHJtEq0gtEw==" }, + "@types/traverse": { + "version": "0.6.32", + "resolved": "https://registry.npmjs.org/@types/traverse/-/traverse-0.6.32.tgz", + "integrity": "sha512-RBz2uRZVCXuMg93WD//aTS5B120QlT4lR/gL+935QtGsKHLS6sCtZBaKfWjIfk7ZXv/r8mtGbwjVIee6/3XTow==" + }, "@types/uuid": { "version": "3.4.4", "resolved": "https://registry.npmjs.org/@types/uuid/-/uuid-3.4.4.tgz", @@ -5227,6 +5232,11 @@ } } }, + "traverse": { + "version": "0.6.6", + "resolved": "https://registry.npmjs.org/traverse/-/traverse-0.6.6.tgz", + "integrity": "sha1-y99WD9e5r2MlAv7UD5GMFX6pcTc=" + }, "trim-newlines": { "version": "2.0.0", "resolved": "https://registry.npmjs.org/trim-newlines/-/trim-newlines-2.0.0.tgz", diff --git a/package.json b/package.json index 67efb1fb..c74c7528 100644 --- a/package.json +++ b/package.json @@ -19,11 +19,13 @@ "@types/cli-progress": "1.8.1", "@types/node": "10.14.4", "@types/request": "2.48.1", + "@types/traverse": "0.6.32", "@types/uuid": "3.4.4", "cli-progress": "2.1.1", "commander": "2.19.0", "moment": "2.24.0", "request": "2.88.0", + "traverse": "0.6.6", "uuid": "3.3.2" }, "license": "GPL-3.0-only", diff --git a/src/connectorClient.ts b/src/connectorClient.ts index 0acc8475..83e33b62 100644 --- a/src/connectorClient.ts +++ b/src/connectorClient.ts @@ -14,6 +14,8 @@ */ import {asyncPool} from '@krlwlfrt/async-pool'; import { + isThing, + SCAssociatedThingWithoutReferences, SCBulkResponse, SCBulkRoute, SCLicensePlate, @@ -68,6 +70,76 @@ export class ConnectorClient extends Client { return v5(uid.toString(), SCNamespaces[namespaceId]); } + /** + * Remove fields from a thing that are references + * + * This effectively turns a thing into a thing without references, e.g. SCDish into SCDishWithoutReferences. + * + * @param thing Thing to remove references from + */ + static removeReferences(thing: THING): SCAssociatedThingWithoutReferences { + const thingWithoutReferences: any = { + ...{}, + ...thing, + }; + + // iterate over all properties + for (const key in thingWithoutReferences) { + /* istanbul ignore if */ + if (!thingWithoutReferences.hasOwnProperty(key)) { + continue; + } + + const property = thingWithoutReferences[key]; + + // check if property itself is a thing + if (isThing(property)) { + // delete said property + delete thingWithoutReferences[key]; + continue; + } + + // check if property is an array + if (Array.isArray(property)) { + if (property.every(isThing)) { + // delete property if every item in it is a thing + delete thingWithoutReferences[key]; + } else { + // check every item in array + for (const item of property) { + if (['boolean', 'number', 'string'].indexOf(typeof item) >= 0) { + // skip primitives + continue; + } + + // check every property + for (const itemKey in item) { + /* istanbul ignore if */ + if (!item.hasOwnProperty(itemKey)) { + continue; + } + + if (isThing(item[itemKey])) { + // delete properties that are things + delete item[itemKey]; + } + } + } + } + } else if (typeof property === 'object') { + // iterate over all properties in nested objects + for (const nestedKey in property) { + if (isThing(property[nestedKey])) { + // delete properties that are things + delete property[nestedKey]; + } + } + } + } + + return thingWithoutReferences as SCAssociatedThingWithoutReferences; + } + /** * Request a bulk transfer to the backend * diff --git a/test/connectorClient.spec.ts b/test/connectorClient.spec.ts index 2ff863e8..57dfbb4a 100644 --- a/test/connectorClient.spec.ts +++ b/test/connectorClient.spec.ts @@ -12,7 +12,9 @@ * You should have received a copy of the GNU General Public License along with * this program. If not, see . */ +import {asyncPool} from '@krlwlfrt/async-pool'; import { + isThing, SCBulkAddResponse, SCBulkAddRoute, SCBulkDoneResponse, @@ -20,17 +22,22 @@ import { SCBulkResponse, SCBulkRoute, SCMessage, + SCThing, + SCThingOriginType, + SCThingType, SCThingUpdateResponse, SCThingUpdateRoute, - SCThingType, - SCThingOriginType, } from '@openstapps/core'; import * as chai from 'chai'; import {expect} from 'chai'; import * as chaiAsPromised from 'chai-as-promised'; import * as chaiSpies from 'chai-spies'; +import {readdir, readFile} from 'fs'; import {suite, test} from 'mocha-typescript'; import * as moment from 'moment'; +import {join, resolve} from 'path'; +import * as traverse from 'traverse'; +import {promisify} from 'util'; import {ConnectorClient} from '../src/connectorClient'; import {EmptyBulkError, NamespaceNotDefinedError} from '../src/errors'; import {HttpClient} from '../src/httpClient'; @@ -47,8 +54,27 @@ const bulkDoneRoute = new SCBulkDoneRoute(); const bulkRoute = new SCBulkRoute(); const thingUpdateRoute = new SCThingUpdateRoute(); +const readdirPromisified = promisify(readdir); +const readFilePromisified = promisify(readFile); + const httpClient = new HttpClient(); +/** + * Check if something contains things + * + * @param thing Thing to check + */ +function doesContainThings(thing: T): boolean { + /* tslint:disable-next-line:only-arrow-functions */ + return traverse(thing).reduce(function(sum, item) { + if (this.isRoot) { + return false; + } + + return sum || (item === null) ? false : isThing(item); + }, false); +} + @suite() export class ConnectorClientSpec { async after() { @@ -305,6 +331,36 @@ export class ConnectorClientSpec { }).to.throw(NamespaceNotDefinedError); } + @test + async removeReferences() { + const pathToTestFiles = resolve( + __dirname, + '..', + 'node_modules', + '@openstapps', + 'core', + 'test', + 'resources', + ); + + const testFiles = await readdirPromisified(pathToTestFiles); + + const testInstances = await asyncPool(5, testFiles, async (testFile) => { + const buffer = await readFilePromisified(join(pathToTestFiles, testFile)); + const content = JSON.parse(buffer.toString()); + return content.instance; + }); + + for (const testInstance of testInstances) { + const testInstanceWithoutReferences = ConnectorClient.removeReferences(testInstance); + expect(doesContainThings(testInstanceWithoutReferences)).to.be.equal(false, JSON.stringify( + [testInstance, testInstanceWithoutReferences], + null, + 2, + )); + } + } + @test async update() { const message: SCMessage = {