diff --git a/packages/semcom-components/components/profile.ts b/packages/semcom-components/components/profile.ts index e040c781..8b1fb20b 100644 --- a/packages/semcom-components/components/profile.ts +++ b/packages/semcom-components/components/profile.ts @@ -177,6 +177,7 @@ export default class ProfileComponent extends LitElement implements Component { ` : ''} +

Example Profile Component v0.1.0

`; } diff --git a/packages/semcom-components/components/profile1.ts b/packages/semcom-components/components/profile1.ts new file mode 100644 index 00000000..49c8a529 --- /dev/null +++ b/packages/semcom-components/components/profile1.ts @@ -0,0 +1,212 @@ +/* eslint-disable no-console -- is a web component */ +import * as N3 from 'n3'; +import { LitElement, css, html, property } from 'lit-element'; +import type { Component } from '@digita-ai/semcom-core'; + +export default class ProfileComponent extends LitElement implements Component { + + data ( + entry: string, + customFetch?: (input: RequestInfo, init?: RequestInit) => Promise, + ): Promise { + + const myFetch = customFetch ? customFetch : fetch; + const parser = new N3.Parser(); + const store = new N3.Store(); + + const foaf = 'http://xmlns.com/foaf/0.1/'; + const n = 'http://www.w3.org/2006/vcard/ns#'; + + return myFetch(entry) + .then((response) => response.text()) + .then((text) => { + store.addQuads(parser.parse(text)); + this.name = store.getQuads(null, new N3.NamedNode(`${foaf}name`), null, null)[0]?.object.value; + this.avatar = store.getQuads(null, new N3.NamedNode(`${n}hasPhoto`), null, null)[0]?.object.value; + this.job = store.getQuads(null, new N3.NamedNode(`${n}role`), null, null)[0]?.object.value; + this.company = store.getQuads(null, new N3.NamedNode(`${n}organization-name`), null, null)[0]?.object.value; + this.city = store.getQuads(null, new N3.NamedNode(`${n}locality`), null, null)[0]?.object.value; + this.country = store.getQuads(null, new N3.NamedNode(`${n}country-name`), null, null)[0]?.object.value; + this.about = store.getQuads(null, new N3.NamedNode(`${n}note`), null, null)[0]?.object.value; + store.getQuads(null, new N3.NamedNode(`${n}hasTelephone`), null, null).map((tele) => { + this.phones?.push(store.getQuads(new N3.NamedNode(tele.object.value), new N3.NamedNode(`${n}value`), null, null)[0]?.object.value.split(':')[1]); + }); + store.getQuads(null, new N3.NamedNode(`${n}hasEmail`), null, null).map((mail) => { + this.emails?.push(store.getQuads(new N3.NamedNode(mail.object.value), new N3.NamedNode(`${n}value`), null, null)[0]?.object.value.split(':')[1]); + }); + }); + + } + + @property() name?: string; + @property() avatar = 'https://upload.wikimedia.org/wikipedia/commons/7/7c/Profile_avatar_placeholder_large.png'; + @property() job?: string; + @property() company?: string; + @property() city?: string; + @property() country?: string; + @property() about?: string; + @property({type: Array}) phones?: string[] = []; + @property({type: Array}) emails?: string[] = []; + + static get styles() { + return [ + css` + :host { + font-family: 'Roboto', sans-serif; + font-weight: 300; + } + .container { + width: 100%; + text-align: center; + padding: 40px 0; + } + #avatar { + border-radius: 50%; + border: 1px solid black; + width: 200px; + height: 200px; + box-shadow: 0 0 20px rgba(0, 0, 0, 0.25); + } + .title { + font-size: 1.7rem; + } + .title, #info, #about, .contactContainerWrapper { + margin-top: 40px; + } + #about { + font-size: 0.8rem; + } + #info { + line-height: 1.5rem; + } + .contactContainer { + width: 80%; + margin-left: auto; + margin-right: auto; + color: #2F363C; + display: flex; + flex-wrap: wrap; + justify-content: space-around; + } + .contactContainer > div { + border: 2px solid #2F363C; + padding: 10px 25px 10px calc(25px + 1rem); + margin: 10px 0; + } + .contactContainer > div > div { + padding-left: 10px; + } + .email, .phone { + position: relative; + } + svg { + height: 1rem; + } + .email svg, .phone svg { + position: absolute; + top: 50%; + -webkit-transform: translateY(-50%); + -ms-transform: translateY(-50%); + transform: translateY(-50%); + left: 25px; + margin: auto; + text-align: center; + } + `, + ]; + } + + render() { + return html` +
+ avatar +
${this.name}
+
+ ${this.job || this.company ? html` +
${this.job ?? ''}${this.company && this.job ? ' at ' : this.company ? 'Works at ': ''}${this.company ?? ''}
+ ` : ''} + ${this.city || this.country ? html` +
+ + + + + ${this.city?.toUpperCase()}${this.city && this.country ? ', ': ''}${this.country?.toUpperCase()} +
+ ` : ''} +
+ + + ${this.about ? html` +
${this.about}
+ ` : ''} + + +
+ ${(this.phones && this.phones.length > 0) ? html` +
+ ${this.phones?.map((phone) => html` +
+ + + + + + +
${phone}
+
+ `)} +
+ ` : ''} + ${(this.emails && this.emails.length > 0) ? html` +
+ ${this.emails?.map((email) => html` + + `)} +
+ ` : ''} +
+

Example Profile Component v0.1.1

+
+ `; + } + + /* + * W3C Custom Element Specification (from MDN) + */ + + // Invoked each time the element is appended into a DOM (i.e. when node is added or moved). + connectedCallback() { + super.connectedCallback(); + console.info('[DGT-ProfileComponent] Element connected'); + } + + // Invoked each time the element is disconnected from a DOM. + disconnectedCallback() { + super.disconnectedCallback(); + console.info('[DGT-ProfileComponent] Element disconnected'); + } + // Invoked each time the custom element is moved to a new DOM. + adoptedCallback() { + // super.adoptedCallback(); + console.info('[DGT-ProfileComponent] Element moved to other DOM'); + } + + // Invoked each time one of the element's attributes specified in observedAttributes is changed. + attributeChangedCallback(name: string, oldValue: string, newValue: string) { + super.attributeChangedCallback(name, oldValue, newValue); + console.info(`[DGT-ProfileComponent] Changed ${name} attribute from "${oldValue}" to "${newValue}"`); + } + +} diff --git a/packages/semcom-components/components/profile2.ts b/packages/semcom-components/components/profile2.ts new file mode 100644 index 00000000..32bdec3e --- /dev/null +++ b/packages/semcom-components/components/profile2.ts @@ -0,0 +1,212 @@ +/* eslint-disable no-console -- is a web component */ +import * as N3 from 'n3'; +import { LitElement, css, html, property } from 'lit-element'; +import type { Component } from '@digita-ai/semcom-core'; + +export default class ProfileComponent extends LitElement implements Component { + + data ( + entry: string, + customFetch?: (input: RequestInfo, init?: RequestInit) => Promise, + ): Promise { + + const myFetch = customFetch ? customFetch : fetch; + const parser = new N3.Parser(); + const store = new N3.Store(); + + const foaf = 'http://xmlns.com/foaf/0.1/'; + const n = 'http://www.w3.org/2006/vcard/ns#'; + + return myFetch(entry) + .then((response) => response.text()) + .then((text) => { + store.addQuads(parser.parse(text)); + this.name = store.getQuads(null, new N3.NamedNode(`${foaf}name`), null, null)[0]?.object.value; + this.avatar = store.getQuads(null, new N3.NamedNode(`${n}hasPhoto`), null, null)[0]?.object.value; + this.job = store.getQuads(null, new N3.NamedNode(`${n}role`), null, null)[0]?.object.value; + this.company = store.getQuads(null, new N3.NamedNode(`${n}organization-name`), null, null)[0]?.object.value; + this.city = store.getQuads(null, new N3.NamedNode(`${n}locality`), null, null)[0]?.object.value; + this.country = store.getQuads(null, new N3.NamedNode(`${n}country-name`), null, null)[0]?.object.value; + this.about = store.getQuads(null, new N3.NamedNode(`${n}note`), null, null)[0]?.object.value; + store.getQuads(null, new N3.NamedNode(`${n}hasTelephone`), null, null).map((tele) => { + this.phones?.push(store.getQuads(new N3.NamedNode(tele.object.value), new N3.NamedNode(`${n}value`), null, null)[0]?.object.value.split(':')[1]); + }); + store.getQuads(null, new N3.NamedNode(`${n}hasEmail`), null, null).map((mail) => { + this.emails?.push(store.getQuads(new N3.NamedNode(mail.object.value), new N3.NamedNode(`${n}value`), null, null)[0]?.object.value.split(':')[1]); + }); + }); + + } + + @property() name?: string; + @property() avatar = 'https://upload.wikimedia.org/wikipedia/commons/7/7c/Profile_avatar_placeholder_large.png'; + @property() job?: string; + @property() company?: string; + @property() city?: string; + @property() country?: string; + @property() about?: string; + @property({type: Array}) phones?: string[] = []; + @property({type: Array}) emails?: string[] = []; + + static get styles() { + return [ + css` + :host { + font-family: 'Roboto', sans-serif; + font-weight: 300; + } + .container { + width: 100%; + text-align: center; + padding: 40px 0; + } + #avatar { + border-radius: 50%; + border: 1px solid black; + width: 200px; + height: 200px; + box-shadow: 0 0 20px rgba(0, 0, 0, 0.25); + } + .title { + font-size: 1.7rem; + } + .title, #info, #about, .contactContainerWrapper { + margin-top: 40px; + } + #about { + font-size: 0.8rem; + } + #info { + line-height: 1.5rem; + } + .contactContainer { + width: 80%; + margin-left: auto; + margin-right: auto; + color: #2F363C; + display: flex; + flex-wrap: wrap; + justify-content: space-around; + } + .contactContainer > div { + border: 2px solid #2F363C; + padding: 10px 25px 10px calc(25px + 1rem); + margin: 10px 0; + } + .contactContainer > div > div { + padding-left: 10px; + } + .email, .phone { + position: relative; + } + svg { + height: 1rem; + } + .email svg, .phone svg { + position: absolute; + top: 50%; + -webkit-transform: translateY(-50%); + -ms-transform: translateY(-50%); + transform: translateY(-50%); + left: 25px; + margin: auto; + text-align: center; + } + `, + ]; + } + + render() { + return html` +
+ avatar +
${this.name}
+
+ ${this.job || this.company ? html` +
${this.job ?? ''}${this.company && this.job ? ' at ' : this.company ? 'Works at ': ''}${this.company ?? ''}
+ ` : ''} + ${this.city || this.country ? html` +
+ + + + + ${this.city?.toUpperCase()}${this.city && this.country ? ', ': ''}${this.country?.toUpperCase()} +
+ ` : ''} +
+ + + ${this.about ? html` +
${this.about}
+ ` : ''} + + +
+ ${(this.phones && this.phones.length > 0) ? html` +
+ ${this.phones?.map((phone) => html` +
+ + + + + + +
${phone}
+
+ `)} +
+ ` : ''} + ${(this.emails && this.emails.length > 0) ? html` +
+ ${this.emails?.map((email) => html` + + `)} +
+ ` : ''} +
+

Example Profile Component v1.0.0

+
+ `; + } + + /* + * W3C Custom Element Specification (from MDN) + */ + + // Invoked each time the element is appended into a DOM (i.e. when node is added or moved). + connectedCallback() { + super.connectedCallback(); + console.info('[DGT-ProfileComponent] Element connected'); + } + + // Invoked each time the element is disconnected from a DOM. + disconnectedCallback() { + super.disconnectedCallback(); + console.info('[DGT-ProfileComponent] Element disconnected'); + } + // Invoked each time the custom element is moved to a new DOM. + adoptedCallback() { + // super.adoptedCallback(); + console.info('[DGT-ProfileComponent] Element moved to other DOM'); + } + + // Invoked each time one of the element's attributes specified in observedAttributes is changed. + attributeChangedCallback(name: string, oldValue: string, newValue: string) { + super.attributeChangedCallback(name, oldValue, newValue); + console.info(`[DGT-ProfileComponent] Changed ${name} attribute from "${oldValue}" to "${newValue}"`); + } + +} diff --git a/packages/semcom-demo-app/src/app/services/semcom.service.ts b/packages/semcom-demo-app/src/app/services/semcom.service.ts index a15fde86..f13508d5 100644 --- a/packages/semcom-demo-app/src/app/services/semcom.service.ts +++ b/packages/semcom-demo-app/src/app/services/semcom.service.ts @@ -17,7 +17,7 @@ export class SemComService { } queryComponents(shapeId: string): Observable { - const filter = { shapes: [ shapeId ] } as ComponentMetadata; + const filter = { shapes: [ shapeId ], version: '^0.1.0'} as ComponentMetadata; return from(this.repo.query(filter)); } diff --git a/packages/semcom-node/config/presets/store.json b/packages/semcom-node/config/presets/store.json index 7d21b0cd..3c3552db 100644 --- a/packages/semcom-node/config/presets/store.json +++ b/packages/semcom-node/config/presets/store.json @@ -17,6 +17,28 @@ "ComponentMetadata:_author": "https://digita.ai", "ComponentMetadata:_tag": "profile", "ComponentMetadata:_version": "0.1.0", + "ComponentMetadata:_latest": false + }, + { + "@type": "ComponentMetadata", + "ComponentMetadata:_description": "Digita SemCom component for profile information.", + "ComponentMetadata:_label": "SemCom Profile Component", + "ComponentMetadata:_uri": "https://components.semcom.digita.ai/components/profile1.js", + "ComponentMetadata:_shapes": "http://xmlns.com/foaf/0.1/PersonalProfileDocument", + "ComponentMetadata:_author": "https://digita.ai", + "ComponentMetadata:_tag": "profile", + "ComponentMetadata:_version": "0.1.1", + "ComponentMetadata:_latest": false + }, + { + "@type": "ComponentMetadata", + "ComponentMetadata:_description": "Digita SemCom component for profile information.", + "ComponentMetadata:_label": "SemCom Profile Component", + "ComponentMetadata:_uri": "https://components.semcom.digita.ai/components/profile2.js", + "ComponentMetadata:_shapes": "http://xmlns.com/foaf/0.1/PersonalProfileDocument", + "ComponentMetadata:_author": "https://digita.ai", + "ComponentMetadata:_tag": "profile", + "ComponentMetadata:_version": "1.0.0", "ComponentMetadata:_latest": true }, { diff --git a/packages/semcom-node/lib/mock/initial-components.ts b/packages/semcom-node/lib/mock/initial-components.ts index 1fa759ca..de81fa54 100644 --- a/packages/semcom-node/lib/mock/initial-components.ts +++ b/packages/semcom-node/lib/mock/initial-components.ts @@ -3,34 +3,42 @@ import { ComponentMetadata } from '@digita-ai/semcom-core'; export const initialComponents = [ { uri: 'foo1/bar', + shapes: [ 'http://xmlns.com/foaf/0.1/PersonalProfileDocument' ], description: 'test1', label: 'test1', author: 'test1', - version: 'test1', + tag: 'profile', + version: '0.1.1', latest: false, } as ComponentMetadata, { uri: 'foo2/bar', + shapes: [ 'http://xmlns.com/foaf/0.1/PersonalProfileDocument' ], description: 'test2', label: 'test2', author: 'test2', - version: 'test2', + version: '0.1.2', + tag: 'profile', latest: true, } as ComponentMetadata, { uri: 'foo3/bar', + shapes: [ 'http://xmlns.com/foaf/0.1/PersonalProfileDocument' ], description: 'test3', label: 'test3', author: 'test3', - version: 'test3', + tag: 'profile', + version: '0.1.3', latest: false, } as ComponentMetadata, { uri: 'foo4/bar', + shapes: [ 'http://xmlns.com/foaf/0.1/PersonalProfileDocument' ], description: 'test4', label: 'test4', author: 'test4', - version: 'test4', + tag: 'profile', + version: '1.1.2', latest: true, } as ComponentMetadata, ] as ComponentMetadata[]; diff --git a/packages/semcom-node/lib/store/services/component-in-memory-store.service.ts b/packages/semcom-node/lib/store/services/component-in-memory-store.service.ts index d9091bb1..ce55a96b 100644 --- a/packages/semcom-node/lib/store/services/component-in-memory-store.service.ts +++ b/packages/semcom-node/lib/store/services/component-in-memory-store.service.ts @@ -1,4 +1,5 @@ import { ComponentMetadata } from '@digita-ai/semcom-core'; +import * as semver from 'semver'; import { ComponentStore } from './component-store.service'; export class ComponentInMemoryStore extends ComponentStore { @@ -7,7 +8,7 @@ export class ComponentInMemoryStore extends ComponentStore { } async query(filter: Partial): Promise { - return this.components.filter((component) => + const filtered = this.components.filter((component) => Object.keys(filter).every((key) => { const componentValue = component[key]; const filterValue = filter[key]; @@ -15,8 +16,20 @@ export class ComponentInMemoryStore extends ComponentStore { Array.isArray(componentValue) && Array.isArray(filterValue) && filterValue.every((value) => componentValue.includes(value)) + || (key === 'version') ); })); + + // seperated version logic for readability + + let versioned = null; + if(filtered && filter.version) { + const versions = filtered.map((component) => component.version); + const maxVersion = semver.maxSatisfying(versions, filter.version); + versioned = filtered.filter((component) => component.version === maxVersion); + } + + return versioned ?? filtered; } async all(): Promise { return this.components; diff --git a/packages/semcom-node/package-lock.json b/packages/semcom-node/package-lock.json index 0c626b13..8c3a5fd4 100644 --- a/packages/semcom-node/package-lock.json +++ b/packages/semcom-node/package-lock.json @@ -657,26 +657,6 @@ "rxjs": "6.3.3" } }, - "@digita-ai/semcom-core": { - "version": "0.3.4", - "resolved": "https://npm.pkg.github.com/download/@digita-ai/semcom-core/0.3.4/4e4eb79b614fef361a15ba726c2a2fac7573ef6d44c941197e246ea1f6417d05", - "integrity": "sha512-lStrJsNAtkMoQ/OKZ90JxXefSHHmxM6W3gwozgNsqGmkjZUwLwOFqMnByWyjwW16tcSzplfdC2sU9oNdaAJ26Q==", - "requires": { - "@types/rdf-ext": "^1.3.8", - "@types/rdf-js": "^4.0.1", - "rdf-ext": "^1.3.0" - }, - "dependencies": { - "@types/rdf-js": { - "version": "4.0.1", - "resolved": "https://registry.npmjs.org/@types/rdf-js/-/rdf-js-4.0.1.tgz", - "integrity": "sha512-S+28+3RoFI+3arls7dS813gYnhb2HiyLX+gs00rgIvCzHU93DaYajhx4tyT+XEO8SjtzZw90OF4OVdYXBwbvkQ==", - "requires": { - "@types/node": "*" - } - } - } - }, "@eslint/eslintrc": { "version": "0.3.0", "resolved": "https://registry.npmjs.org/@eslint/eslintrc/-/eslintrc-0.3.0.tgz", @@ -985,19 +965,6 @@ "fastq": "^1.6.0" } }, - "@rdfjs/data-model": { - "version": "1.2.0", - "resolved": "https://registry.npmjs.org/@rdfjs/data-model/-/data-model-1.2.0.tgz", - "integrity": "sha512-6ITWcu2sr9zJqXUPDm1XJ8DRpea7PotWBIkTzuO1MCSruLOWH2ICoQOAtlJy30cT+GqH9oAQKPR+CHXejsdizA==", - "requires": { - "@types/rdf-js": "*" - } - }, - "@rdfjs/to-ntriples": { - "version": "1.0.2", - "resolved": "https://registry.npmjs.org/@rdfjs/to-ntriples/-/to-ntriples-1.0.2.tgz", - "integrity": "sha512-ngw5XAaGHjgGiwWWBPGlfdCclHftonmbje5lMys4G2j4NvfExraPIuRZgjSnd5lg4dnulRVUll8tRbgKO+7EDA==" - }, "@shopify/jest-koa-mocks": { "version": "2.3.0", "resolved": "https://registry.npmjs.org/@shopify/jest-koa-mocks/-/jest-koa-mocks-2.3.0.tgz", @@ -1301,23 +1268,6 @@ "resolved": "https://registry.npmjs.org/@types/range-parser/-/range-parser-1.2.3.tgz", "integrity": "sha512-ewFXqrQHlFsgc09MK5jP5iR7vumV/BYayNC6PgJO2LPe8vrnNFyjQjSppfEngITi0qvfKtzFvgKymGheFM9UOA==" }, - "@types/rdf-dataset-indexed": { - "version": "0.4.5", - "resolved": "https://registry.npmjs.org/@types/rdf-dataset-indexed/-/rdf-dataset-indexed-0.4.5.tgz", - "integrity": "sha512-Pw/jEG3cRyewlg0xD5cQ9Tl85DjzAfewzDGaXo2f+Fgv/WbMSWYq9qGlJgNELFGZH6P1iaNOMQVwlSoA64j1Fg==", - "requires": { - "@types/rdf-js": "*" - } - }, - "@types/rdf-ext": { - "version": "1.3.8", - "resolved": "https://registry.npmjs.org/@types/rdf-ext/-/rdf-ext-1.3.8.tgz", - "integrity": "sha512-pdaO+sGV006Xm2bam6qUl4M0jrirVhHrISS/KELZ0HiNz/F9xMqIGM99eN2a5PwHvVsNGnwO0kK3vwTXngJ2uw==", - "requires": { - "@types/rdf-dataset-indexed": "*", - "@types/rdf-js": "*" - } - }, "@types/rdf-js": { "version": "4.0.0", "resolved": "https://registry.npmjs.org/@types/rdf-js/-/rdf-js-4.0.0.tgz", @@ -5766,26 +5716,6 @@ "@types/rdf-js": "*" } }, - "rdf-dataset-indexed": { - "version": "0.4.0", - "resolved": "https://registry.npmjs.org/rdf-dataset-indexed/-/rdf-dataset-indexed-0.4.0.tgz", - "integrity": "sha512-xIZ3PGwBHh1DVax9FTq/zhJcrdJTkCgRT2xolF5ZhKj0EOFoT6wdITyfxKSmnD6YNyGyng5F5o2SR62TYask3Q==", - "requires": { - "@rdfjs/data-model": "^1.1.1", - "readable-stream": "^3.3.0" - } - }, - "rdf-ext": { - "version": "1.3.1", - "resolved": "https://registry.npmjs.org/rdf-ext/-/rdf-ext-1.3.1.tgz", - "integrity": "sha512-mdkmEULWtKV9t5EHfnAxM2zL4xiloiSrEZnDtsBsVEpR4kk7teUfhotaXBzW5J1EHZSq5KcZ5VmJsUFZljLMSw==", - "requires": { - "@rdfjs/data-model": "^1.1.0", - "@rdfjs/to-ntriples": "^1.0.1", - "rdf-dataset-indexed": "^0.4.0", - "rdf-normalize": "^1.0.0" - } - }, "rdf-literal": { "version": "1.2.0", "resolved": "https://registry.npmjs.org/rdf-literal/-/rdf-literal-1.2.0.tgz", @@ -5795,11 +5725,6 @@ "rdf-data-factory": "^1.0.1" } }, - "rdf-normalize": { - "version": "1.0.0", - "resolved": "https://registry.npmjs.org/rdf-normalize/-/rdf-normalize-1.0.0.tgz", - "integrity": "sha1-U0lrrzYszp2fyh8iFsbDAAf5nMo=" - }, "rdf-object": { "version": "1.8.0", "resolved": "https://registry.npmjs.org/rdf-object/-/rdf-object-1.8.0.tgz", @@ -6334,9 +6259,9 @@ } }, "semver": { - "version": "7.3.4", - "resolved": "https://registry.npmjs.org/semver/-/semver-7.3.4.tgz", - "integrity": "sha512-tCfb2WLjqFAtXn4KEdxIhalnRtoKFN7nAwj0B3ZXCbQloV2tq5eDbcTmT68JJD3nRJq24/XgxtQKFIpQdtvmVw==", + "version": "7.3.5", + "resolved": "https://registry.npmjs.org/semver/-/semver-7.3.5.tgz", + "integrity": "sha512-PoeGJYh8HK4BTO/a9Tf6ZG3veo/A7ZVsYrSA6J8ny9nb3B1VrpkuN+z9OE5wfE5p6H4LchYZsegiQgbJD94ZFQ==", "requires": { "lru-cache": "^6.0.0" } diff --git a/packages/semcom-node/package.json b/packages/semcom-node/package.json index 59225cd7..31132ed1 100644 --- a/packages/semcom-node/package.json +++ b/packages/semcom-node/package.json @@ -35,7 +35,7 @@ "lint:staged": "lint-staged", "prepare": "npm run build", "start": "node dist/main.js", - "test": "jest --silent", + "test": "jest", "test:ci": "jest --runInBand --silent" }, "eslintConfig": { @@ -72,6 +72,7 @@ "rdf-quad": "^1.5.0", "rdf-serialize": "^1.0.1", "rxjs": "6.3.3", + "semver": "^7.3.5", "streamify-array": "^1.0.1" }, "devDependencies": {