From 713588c655c7516f008a1828104bd14fa9792e49 Mon Sep 17 00:00:00 2001 From: Adler Faulkner Date: Thu, 2 Nov 2023 19:14:44 -0700 Subject: [PATCH] feat(validation): add validation on update and save --- src/SklEngine.ts | 201 ++++++- src/storage/operator/OneOrMorePath.ts | 9 +- src/storage/operator/SequencePath.ts | 9 +- src/storage/operator/ZeroOrMorePath.ts | 9 +- src/storage/query-adapter/QueryAdapter.ts | 4 + .../InMemorySparqlQueryExecutor.ts | 12 +- test/assets/schemas/core.json | 59 +- .../series-verb-with-pre-processing.json | 1 + test/assets/schemas/series-verb.json | 1 + test/deploy/src/assets/core.json | 44 +- test/unit/SklEngine.test.ts | 552 ++++++++++++------ .../InMemorySparqlQueryExecutor.test.ts | 5 +- 12 files changed, 611 insertions(+), 295 deletions(-) diff --git a/src/SklEngine.ts b/src/SklEngine.ts index a63c12f..8c54c60 100644 --- a/src/SklEngine.ts +++ b/src/SklEngine.ts @@ -14,8 +14,12 @@ import SHACLValidator from 'rdf-validate-shacl'; import type ValidationReport from 'rdf-validate-shacl/src/validation-report'; import { Mapper } from './mapping/Mapper'; import type { SklEngineOptions } from './SklEngineOptions'; +import type { FindOperator } from './storage/FindOperator'; import type { FindAllOptions, FindOneOptions, FindOptionsWhere } from './storage/FindOptionsTypes'; +import { In } from './storage/operator/In'; import { InversePath } from './storage/operator/InversePath'; +import { OneOrMorePath } from './storage/operator/OneOrMorePath'; +import { SequencePath } from './storage/operator/SequencePath'; import { ZeroOrMorePath } from './storage/operator/ZeroOrMorePath'; import type { QueryAdapter, RawQueryResult } from './storage/query-adapter/QueryAdapter'; import { SparqlQueryAdapter } from './storage/query-adapter/sparql/SparqlQueryAdapter'; @@ -96,12 +100,21 @@ export class SKLEngine { throw new Error(`No schema found with fields matching ${JSON.stringify(options)}`); } - public async findBy(where: FindOptionsWhere): Promise { + public async findBy(where: FindOptionsWhere, notFoundErrorMessage?: string): Promise { const entity = await this.queryAdapter.findBy(where); if (entity) { return entity; } - throw new Error(`No schema found with fields matching ${JSON.stringify(where)}`); + throw new Error(notFoundErrorMessage ?? `No schema found with fields matching ${JSON.stringify(where)}`); + } + + public async findByIfExists(options: FindOptionsWhere): Promise { + try { + const entity = await this.findBy(options); + return entity; + } catch { + return undefined; + } } public async findAll(options?: FindAllOptions): Promise { @@ -124,8 +137,10 @@ export class SKLEngine { public async save(entities: Entity[]): Promise; public async save(entityOrEntities: Entity | Entity[]): Promise { if (Array.isArray(entityOrEntities)) { + await this.validateEntitiesConformToNounSchema(entityOrEntities); return await this.queryAdapter.save(entityOrEntities); } + await this.validateEntityConformsToNounSchema(entityOrEntities); return await this.queryAdapter.save(entityOrEntities); } @@ -133,11 +148,143 @@ export class SKLEngine { public async update(ids: string[], attributes: Partial): Promise; public async update(idOrIds: string | string[], attributes: Partial): Promise { if (Array.isArray(idOrIds)) { + await this.validateEntitiesWithIdsConformsToNounSchemaForAttributes(idOrIds, attributes); return await this.queryAdapter.update(idOrIds, attributes); } + await this.validateEntityWithIdConformsToNounSchemaForAttributes(idOrIds, attributes); return await this.queryAdapter.update(idOrIds, attributes); } + private async validateEntitiesConformToNounSchema( + entities: Entity[], + ): Promise { + const entitiesByType = this.groupEntitiesByType(entities); + for (const type of Object.keys(entitiesByType)) { + const noun = await this.findByIfExists({ id: type }); + if (noun) { + const parentNouns = await this.getSuperClassesOfNoun(type); + for (const currentNoun of [ noun, ...parentNouns ]) { + const entitiesOfType = entitiesByType[type]; + const nounSchemaWithTarget = { + ...currentNoun, + [SHACL.targetNode]: entitiesOfType.map((entity): ReferenceNodeObject => ({ '@id': entity['@id'] })), + }; + const report = await this.convertToQuadsAndValidateAgainstShape(entitiesOfType, nounSchemaWithTarget); + if (!report.conforms) { + throw new Error(`An entity does not conform to the ${currentNoun['@id']} schema.`); + } + } + } + } + } + + private groupEntitiesByType(entities: Entity[]): Record { + return entities.reduce((groupedEntities: Record, entity): Record => { + const entityTypes = Array.isArray(entity['@type']) ? entity['@type'] : [ entity['@type'] ]; + for (const type of entityTypes) { + if (!groupedEntities[type]) { + groupedEntities[type] = []; + } + groupedEntities[type].push(entity); + } + return groupedEntities; + }, {}); + } + + private async getSuperClassesOfNoun(noun: string): Promise { + return await this.getParentsOfSelector(noun); + } + + private async getSuperClassesOfNouns(nouns: string[]): Promise { + return await this.getParentsOfSelector(In(nouns)); + } + + private async getParentsOfSelector(selector: string | FindOperator): Promise { + return await this.findAll({ + where: { + id: InversePath({ + subPath: OneOrMorePath({ subPath: RDFS.subClassOf as string }), + value: selector, + }), + }, + }); + } + + private async validateEntityConformsToNounSchema( + entity: Entity, + ): Promise { + const nounIds = Array.isArray(entity['@type']) ? entity['@type'] : [ entity['@type'] ]; + const directNouns = await this.findAllBy({ id: In(nounIds) }); + const parentNouns = await this.getSuperClassesOfNouns(nounIds); + for (const currentNoun of [ ...directNouns, ...parentNouns ]) { + const nounSchemaWithTarget = { + ...currentNoun, + [SHACL.targetNode]: { '@id': entity['@id'] }, + }; + const report = await this.convertToQuadsAndValidateAgainstShape(entity, nounSchemaWithTarget); + if (!report.conforms) { + throw new Error(`Entity ${entity['@id']} does not conform to the ${currentNoun['@id']} schema.`); + } + } + } + + private async validateEntitiesWithIdsConformsToNounSchemaForAttributes( + ids: string[], + attributes: Partial, + ): Promise { + for (const id of ids) { + await this.validateEntityWithIdConformsToNounSchemaForAttributes(id, attributes); + } + } + + private async getNounsAndParentNounsOfEntity(id: string): Promise { + return await this.findAllBy({ + id: InversePath({ + subPath: SequencePath({ + subPath: [ + RDF.type, + ZeroOrMorePath({ subPath: RDFS.subClassOf as string }), + ], + }), + value: id, + }), + }); + } + + private async validateEntityWithIdConformsToNounSchemaForAttributes( + id: string, + attributes: Partial, + ): Promise { + const nouns = await this.getNounsAndParentNounsOfEntity(id); + for (const currentNoun of nouns) { + if (SHACL.property in currentNoun) { + const nounProperties = ensureArray(currentNoun[SHACL.property] as OrArray) + .filter((property): boolean => { + const path = property[SHACL.path]; + if (typeof path === 'string' && path in attributes) { + return true; + } + if (typeof path === 'object' && '@id' in path! && (path['@id'] as string) in attributes) { + return true; + } + return false; + }); + if (nounProperties.length > 0) { + const nounSchemaWithTarget = { + '@type': SHACL.NodeShape, + [SHACL.targetNode]: { '@id': id }, + [SHACL.property]: nounProperties, + }; + const attributesWithId = { ...attributes, '@id': id }; + const report = await this.convertToQuadsAndValidateAgainstShape(attributesWithId, nounSchemaWithTarget); + if (!report.conforms) { + throw new Error(`Entity ${id} does not conform to the ${currentNoun['@id']} schema.`); + } + } + } + } + } + public async delete(id: string): Promise; public async delete(ids: string[]): Promise; public async delete(idOrIds: string | string[]): Promise { @@ -193,14 +340,13 @@ export class SKLEngine { } private async findTriggerVerbMapping(integration: string): Promise { - try { - return (await this.findBy({ + return (await this.findBy( + { type: SKL.TriggerVerbMapping, [SKL.integration]: integration, - })) as TriggerVerbMapping; - } catch { - throw new Error(`Failed to find a Trigger Verb mapping for integration ${integration}`); - } + }, + `Failed to find a Trigger Verb mapping for integration ${integration}`, + )) as TriggerVerbMapping; } private async executeVerbByName( @@ -213,11 +359,10 @@ export class SKLEngine { } private async findVerbWithName(verbName: string): Promise { - try { - return (await this.findBy({ type: SKL.Verb, [RDFS.label]: verbName })) as Verb; - } catch { - throw new Error(`Failed to find the verb ${verbName} in the schema.`); - } + return (await this.findBy( + { type: SKL.Verb, [RDFS.label]: verbName }, + `Failed to find the verb ${verbName} in the schema.`, + )) as Verb; } private async executeVerb(verb: Verb, verbArgs: JSONObject, verbConfig?: VerbConfig): Promise> { @@ -366,10 +511,7 @@ export class SKLEngine { } private async updateEntityFromVerbArgs(args: Record): Promise { - if (args.id) { - await this.update(args.id, args.attributes); - } - await this.update(args.ids, args.attributes); + await this.update(args.id ?? args.ids, args.attributes); } private async saveEntityOrEntitiesFromVerbArgs(args: Record): Promise> { @@ -613,14 +755,10 @@ export class SKLEngine { } private async findSecurityCredentialsForAccountIfDefined(accountId: string): Promise { - try { - return await this.findBy({ - type: SKL.SecurityCredentials, - [SKL.account]: accountId, - }); - } catch { - return undefined; - } + return await this.findByIfExists({ + type: SKL.SecurityCredentials, + [SKL.account]: accountId, + }); } private async createOpenApiOperationExecutorWithSpec(openApiDescription: OpenApi): Promise { @@ -780,9 +918,16 @@ export class SKLEngine { await this.assertVerbReturnValueMatchesReturnTypeSchema(mappedReturnValue, getOauthTokenVerb); const bearerToken = getValueIfDefined(mappedReturnValue[SKL.bearerToken]); const accessToken = getValueIfDefined(mappedReturnValue[SKL.accessToken]); - securityCredentials[SKL.bearerToken] = bearerToken; - securityCredentials[SKL.accessToken] = accessToken; - securityCredentials[SKL.refreshToken] = getValueIfDefined(mappedReturnValue[SKL.refreshToken]); + const refreshToken = getValueIfDefined(mappedReturnValue[SKL.refreshToken]); + if (bearerToken) { + securityCredentials[SKL.bearerToken] = bearerToken; + } + if (accessToken) { + securityCredentials[SKL.accessToken] = accessToken; + } + if (refreshToken) { + securityCredentials[SKL.refreshToken] = refreshToken; + } await this.save(securityCredentials); return { accessToken, bearerToken }; } diff --git a/src/storage/operator/OneOrMorePath.ts b/src/storage/operator/OneOrMorePath.ts index d9fdd3c..3066cb5 100644 --- a/src/storage/operator/OneOrMorePath.ts +++ b/src/storage/operator/OneOrMorePath.ts @@ -1,14 +1,15 @@ import { FindOperator } from '../FindOperator'; -export interface OneOrMorePathValue { +export interface OneOrMorePathValue { subPath: string | FindOperator; - value?: string; + value?: string | FindOperator; } // eslint-disable-next-line @typescript-eslint/naming-convention export function OneOrMorePath< - T extends OneOrMorePathValue ->(value: T): FindOperator { + T, + TI extends OneOrMorePathValue +>(value: TI): FindOperator { return new FindOperator({ operator: 'oneOrMorePath', value, diff --git a/src/storage/operator/SequencePath.ts b/src/storage/operator/SequencePath.ts index d344305..5d60cfa 100644 --- a/src/storage/operator/SequencePath.ts +++ b/src/storage/operator/SequencePath.ts @@ -1,14 +1,15 @@ import { FindOperator } from '../FindOperator'; -export interface SequencePathValue { +export interface SequencePathValue { subPath: (string | FindOperator)[]; - value?: string; + value?: string | FindOperator; } // eslint-disable-next-line @typescript-eslint/naming-convention export function SequencePath< - T extends SequencePathValue ->(value: T): FindOperator { + T, + TI extends SequencePathValue +>(value: TI): FindOperator { return new FindOperator({ operator: 'sequencePath', value, diff --git a/src/storage/operator/ZeroOrMorePath.ts b/src/storage/operator/ZeroOrMorePath.ts index 59feb12..2ed5e5c 100644 --- a/src/storage/operator/ZeroOrMorePath.ts +++ b/src/storage/operator/ZeroOrMorePath.ts @@ -1,14 +1,15 @@ import { FindOperator } from '../FindOperator'; -export interface ZeroOrMorePathValue { +export interface ZeroOrMorePathValue { subPath: string | FindOperator; - value?: string; + value?: string | FindOperator; } // eslint-disable-next-line @typescript-eslint/naming-convention export function ZeroOrMorePath< - T extends ZeroOrMorePathValue ->(value: T): FindOperator { + T, + TI extends ZeroOrMorePathValue +>(value: TI): FindOperator { return new FindOperator({ operator: 'zeroOrMorePath', value, diff --git a/src/storage/query-adapter/QueryAdapter.ts b/src/storage/query-adapter/QueryAdapter.ts index 6013bbf..15321cb 100644 --- a/src/storage/query-adapter/QueryAdapter.ts +++ b/src/storage/query-adapter/QueryAdapter.ts @@ -12,6 +12,10 @@ import type { export type RawQueryResult = Record; +export interface UpdateOptions { + validate?: boolean; +} + /** * Adapts CRUD operations to a specific persistence layer. */ diff --git a/src/storage/query-adapter/sparql/query-executor/InMemorySparqlQueryExecutor.ts b/src/storage/query-adapter/sparql/query-executor/InMemorySparqlQueryExecutor.ts index fe80743..4dfa058 100644 --- a/src/storage/query-adapter/sparql/query-executor/InMemorySparqlQueryExecutor.ts +++ b/src/storage/query-adapter/sparql/query-executor/InMemorySparqlQueryExecutor.ts @@ -68,11 +68,13 @@ export class InMemorySparqlQueryExecutor implements QueryExecutor { } public async executeSparqlUpdate(query: Update): Promise { - const generatedQuery = this.sparqlGenerator.stringify(query); - await this.engine.queryVoid( - generatedQuery, - this.queryContext, - ); + if ((query?.updates?.length ?? 0) > 0) { + const generatedQuery = this.sparqlGenerator.stringify(query); + await this.engine.queryVoid( + generatedQuery, + this.queryContext, + ); + } } public async executeAskQueryAndGetResponse(query: AskQuery): Promise { diff --git a/test/assets/schemas/core.json b/test/assets/schemas/core.json index f8ca940..38392ce 100644 --- a/test/assets/schemas/core.json +++ b/test/assets/schemas/core.json @@ -23,18 +23,17 @@ "shacl:description": { "@type": "xsd:string" }, "shacl:closed": { "@type": "xsd:boolean" }, "shacl:property": { "@type": "@id" }, - "shacl:nodeKind": { "@type": "@id" } + "shacl:nodeKind": { "@type": "@id" }, + "skl": "https://standardknowledge.com/ontologies/core/" }, "@graph": [ { "@id": "https://standardknowledge.com/ontologies/core/Account", - "@type": ["owl:Class", "shacl:NodeShape"], + "@type": ["owl:Class", "shacl:NodeShape", "skl:Noun"], "rdfs:label": "Account", - "rdfs:subClassOf": "https://standardknowledge.com/ontologies/core/Noun", "shacl:closed": false, "shacl:property": [ { - "shacl:class": "https://standardknowledge.com/ontologies/core/Integration", "shacl:maxCount": 1, "shacl:nodeKind": "shacl:IRI", "shacl:path": "https://standardknowledge.com/ontologies/core/integration" @@ -44,9 +43,8 @@ }, { "@id": "https://standardknowledge.com/ontologies/core/File", - "@type": ["owl:Class", "shacl:NodeShape"], + "@type": ["owl:Class", "shacl:NodeShape", "skl:Noun"], "rdfs:label": "File", - "rdfs:subClassOf": "https://standardknowledge.com/ontologies/core/Noun", "skos:definition": "An electronic document.", "shacl:closed": false, "shacl:property": [ @@ -94,7 +92,7 @@ }, { "@id": "https://standardknowledge.com/ontologies/core/Folder", - "@type": ["owl:Class", "shacl:NodeShape"], + "@type": ["owl:Class", "shacl:NodeShape", "skl:Noun"], "rdfs:label": "Folder", "rdfs:subClassOf": "https://standardknowledge.com/ontologies/core/File", "skos:definition": "An electronic folder that stores electronic documents.", @@ -102,9 +100,8 @@ }, { "@id": "https://standardknowledge.com/ontologies/core/Integration", - "@type": ["owl:Class", "shacl:NodeShape"], + "@type": ["owl:Class", "shacl:NodeShape", "skl:Noun"], "rdfs:label": "Integration", - "rdfs:subClassOf": "https://standardknowledge.com/ontologies/core/Noun", "shacl:closed": false, "shacl:property": [ { @@ -116,7 +113,6 @@ "shacl:path": "http://purl.org/dc/elements/1.1/description" }, { - "shacl:class": "https://standardknowledge.com/ontologies/core/File", "shacl:nodeKind": "shacl:IRI", "shacl:path": "https://standardknowledge.com/ontologies/core/icons" } @@ -124,9 +120,8 @@ }, { "@id": "https://standardknowledge.com/ontologies/core/Mapping", - "@type": ["owl:Class", "shacl:NodeShape"], + "@type": ["owl:Class", "shacl:NodeShape", "skl:Noun"], "rdfs:label": "Mapping", - "rdfs:subClassOf": "https://standardknowledge.com/ontologies/core/Noun", "shacl:closed": false, "shacl:property": { "shacl:maxCount": 1, @@ -135,7 +130,7 @@ }, { "@id": "https://standardknowledge.com/ontologies/core/Noun", - "@type": ["owl:Class", "shacl:NodeShape"], + "@type": ["owl:Class", "shacl:NodeShape", "skl:Noun"], "rdfs:label": "Noun", "shacl:closed": false, "shacl:property": { @@ -145,9 +140,8 @@ }, { "@id": "https://standardknowledge.com/ontologies/core/SecurityCredentials", - "@type": ["owl:Class", "shacl:NodeShape"], + "@type": ["owl:Class", "shacl:NodeShape", "skl:Noun"], "rdfs:label": "Security Credentials", - "rdfs:subClassOf": "https://standardknowledge.com/ontologies/core/Noun", "shacl:closed": false, "shacl:property": [ { @@ -155,7 +149,6 @@ "shacl:path": "https://standardknowledge.com/ontologies/core/refreshToken" }, { - "shacl:class": "https://standardknowledge.com/ontologies/core/Account", "shacl:maxCount": 1, "shacl:nodeKind": "shacl:IRI", "shacl:path": "https://standardknowledge.com/ontologies/core/account" @@ -164,21 +157,31 @@ "shacl:maxCount": 1, "shacl:path": "https://standardknowledge.com/ontologies/core/accessToken" }, + { + "shacl:maxCount": 1, + "shacl:path": "https://standardknowledge.com/ontologies/core/bearerToken" + }, { "shacl:maxCount": 1, "shacl:path": "https://standardknowledge.com/ontologies/core/apiKey" + }, + { + "shacl:maxCount": 1, + "shacl:path": "https://standardknowledge.com/ontologies/core/clientId" + }, + { + "shacl:maxCount": 1, + "shacl:path": "https://standardknowledge.com/ontologies/core/clientSecret" } ] }, { "@id": "https://standardknowledge.com/ontologies/core/OpenApiDescription", - "@type": ["owl:Class", "shacl:NodeShape"], + "@type": ["owl:Class", "shacl:NodeShape", "skl:Noun"], "rdfs:label": "OpenApiDescription", - "rdfs:subClassOf": "https://standardknowledge.com/ontologies/core/Noun", "shacl:closed": false, "shacl:property": [ { - "shacl:class": "https://standardknowledge.com/ontologies/core/Integration", "shacl:maxCount": 1, "shacl:nodeKind": "shacl:IRI", "shacl:path": "https://standardknowledge.com/ontologies/core/integration" @@ -191,9 +194,8 @@ }, { "@id": "https://standardknowledge.com/ontologies/core/TokenPaginatedCollection", - "@type": ["owl:Class", "shacl:NodeShape"], + "@type": ["owl:Class", "shacl:NodeShape", "skl:Noun"], "rdfs:label": "Token Paginated Collection", - "rdfs:subClassOf": "https://standardknowledge.com/ontologies/core/Noun", "shacl:closed": false, "shacl:property": [ { @@ -208,8 +210,7 @@ }, { "@id": "https://standardknowledge.com/ontologies/core/Verb", - "@type": ["owl:Class", "shacl:NodeShape"], - "rdfs:subClassOf": "https://standardknowledge.com/ontologies/core/Noun", + "@type": ["owl:Class", "shacl:NodeShape", "skl:Noun"], "rdfs:label": "Verb", "shacl:closed": false, "property": [ @@ -380,17 +381,17 @@ }, { "@id": "https://standardknowledge.com/ontologies/core/NounMappedVerb", - "@type": ["owl:Class", "shacl:NodeShape"], + "@type": ["owl:Class", "shacl:NodeShape", "skl:Noun"], "rdfs:subClassOf": "https://standardknowledge.com/ontologies/core/Verb" }, { "@id": "https://standardknowledge.com/ontologies/core/OpenApiSecuritySchemeVerb", - "@type": ["owl:Class", "shacl:NodeShape"], + "@type": ["owl:Class", "shacl:NodeShape", "skl:Noun"], "rdfs:subClassOf": "https://standardknowledge.com/ontologies/core/Verb" }, { "@id": "https://standardknowledge.com/ontologies/core/VerbIntegrationMapping", - "@type": ["owl:Class", "shacl:NodeShape"], + "@type": ["owl:Class", "shacl:NodeShape", "skl:Noun"], "rdfs:label": "Verb to Integration Mapping", "rdfs:subClassOf": "https://standardknowledge.com/ontologies/core/Mapping", "shacl:closed": false, @@ -402,7 +403,6 @@ }, { "shacl:maxCount": 1, - "shacl:class": "https://standardknowledge.com/ontologies/core/Verb", "shacl:nodeKind": "shacl:IRI", "shacl:path": "https://standardknowledge.com/ontologies/core/verb" }, @@ -424,7 +424,6 @@ "shacl:path": "https://standardknowledge.com/ontologies/core/returnValueMapping" }, { - "shacl:class": "https://standardknowledge.com/ontologies/core/Integration", "shacl:maxCount": 1, "shacl:minCount": 1, "shacl:nodeKind": "shacl:IRI", @@ -434,20 +433,18 @@ }, { "@id": "https://standardknowledge.com/ontologies/core/VerbNounMapping", - "@type": ["owl:Class", "shacl:NodeShape"], + "@type": ["owl:Class", "shacl:NodeShape", "skl:Noun"], "rdfs:label": "Verb to Noun Mapping", "rdfs:subClassOf": "https://standardknowledge.com/ontologies/core/Mapping", "shacl:closed": false, "shacl:property": [ { - "shacl:class": "https://standardknowledge.com/ontologies/core/Verb", "shacl:maxCount": 1, "shacl:minCount": 1, "shacl:nodeKind": "shacl:IRI", "shacl:path": "https://standardknowledge.com/ontologies/core/verb" }, { - "shacl:class": "https://standardknowledge.com/ontologies/core/Noun", "shacl:maxCount": 1, "shacl:minCount": 1, "shacl:nodeKind": "shacl:IRI", diff --git a/test/assets/schemas/series-verb-with-pre-processing.json b/test/assets/schemas/series-verb-with-pre-processing.json index ebb7656..d4afd1f 100644 --- a/test/assets/schemas/series-verb-with-pre-processing.json +++ b/test/assets/schemas/series-verb-with-pre-processing.json @@ -232,6 +232,7 @@ "rr:predicate": "example:id", "rr:objectMap": { "@type": "rr:ObjectMap", + "rr:datatype": "xsd:string", "rml:reference": "originalVerbParameters.entity[`@id]" } }, diff --git a/test/assets/schemas/series-verb.json b/test/assets/schemas/series-verb.json index b371f3c..76d6ca9 100644 --- a/test/assets/schemas/series-verb.json +++ b/test/assets/schemas/series-verb.json @@ -201,6 +201,7 @@ "rr:predicate": "example:id", "rr:objectMap": { "@type": "rr:ObjectMap", + "rr:datatype": "xsd:string", "rml:reference": "originalVerbParameters.entity[`@id]" } }, diff --git a/test/deploy/src/assets/core.json b/test/deploy/src/assets/core.json index c6b9743..ea3335d 100644 --- a/test/deploy/src/assets/core.json +++ b/test/deploy/src/assets/core.json @@ -23,13 +23,11 @@ "@graph": [ { "@id": "https://standardknowledge.com/ontologies/core/Account", - "@type": ["owl:Class", "shacl:NodeShape"], + "@type": ["owl:Class", "shacl:NodeShape", "skl:Noun"], "rdfs:label": "Account", - "rdfs:subClassOf": "https://standardknowledge.com/ontologies/core/Noun", "shacl:closed": false, "shacl:property": [ { - "shacl:class": "https://standardknowledge.com/ontologies/core/Integration", "shacl:maxCount": 1, "shacl:nodeKind": { "@id": "shacl:IRI" }, "shacl:path": "https://standardknowledge.com/ontologies/core/integration" @@ -39,9 +37,8 @@ }, { "@id": "https://standardknowledge.com/ontologies/core/File", - "@type": ["owl:Class", "shacl:NodeShape"], + "@type": ["owl:Class", "shacl:NodeShape", "skl:Noun"], "rdfs:label": "File", - "rdfs:subClassOf": "https://standardknowledge.com/ontologies/core/Noun", "skos:definition": "An electronic document.", "shacl:closed": false, "shacl:property": [ @@ -89,7 +86,7 @@ }, { "@id": "https://standardknowledge.com/ontologies/core/Folder", - "@type": ["owl:Class", "shacl:NodeShape"], + "@type": ["owl:Class", "shacl:NodeShape", "skl:Noun"], "rdfs:label": "Folder", "rdfs:subClassOf": "https://standardknowledge.com/ontologies/core/File", "skos:definition": "An electronic folder that stores electronic documents.", @@ -97,9 +94,8 @@ }, { "@id": "https://standardknowledge.com/ontologies/core/Integration", - "@type": ["owl:Class", "shacl:NodeShape"], + "@type": ["owl:Class", "shacl:NodeShape", "skl:Noun"], "rdfs:label": "Integration", - "rdfs:subClassOf": "https://standardknowledge.com/ontologies/core/Noun", "shacl:closed": false, "shacl:property": [ { @@ -111,7 +107,6 @@ "shacl:path": "http://purl.org/dc/elements/1.1/description" }, { - "shacl:class": "https://standardknowledge.com/ontologies/core/File", "shacl:nodeKind": { "@id": "shacl:IRI" }, "shacl:path": "https://standardknowledge.com/ontologies/core/icons" } @@ -119,9 +114,8 @@ }, { "@id": "https://standardknowledge.com/ontologies/core/Mapping", - "@type": ["owl:Class", "shacl:NodeShape"], + "@type": ["owl:Class", "shacl:NodeShape", "skl:Noun"], "rdfs:label": "Mapping", - "rdfs:subClassOf": "https://standardknowledge.com/ontologies/core/Noun", "shacl:closed": false, "shacl:property": { "shacl:maxCount": 1, @@ -130,7 +124,7 @@ }, { "@id": "https://standardknowledge.com/ontologies/core/Noun", - "@type": ["owl:Class", "shacl:NodeShape"], + "@type": ["owl:Class", "shacl:NodeShape", "skl:Noun"], "rdfs:label": "Noun", "shacl:closed": false, "shacl:property": { @@ -140,9 +134,8 @@ }, { "@id": "https://standardknowledge.com/ontologies/core/SecurityCredentials", - "@type": ["owl:Class", "shacl:NodeShape"], + "@type": ["owl:Class", "shacl:NodeShape", "skl:Noun"], "rdfs:label": "Security Credentials", - "rdfs:subClassOf": "https://standardknowledge.com/ontologies/core/Noun", "shacl:closed": false, "shacl:property": [ { @@ -150,7 +143,6 @@ "shacl:path": "https://standardknowledge.com/ontologies/core/refreshToken" }, { - "shacl:class": "https://standardknowledge.com/ontologies/core/Account", "shacl:maxCount": 1, "shacl:nodeKind": { "@id": "shacl:IRI" }, "shacl:path": "https://standardknowledge.com/ontologies/core/account" @@ -167,13 +159,11 @@ }, { "@id": "https://standardknowledge.com/ontologies/core/OpenApiDescription", - "@type": ["owl:Class", "shacl:NodeShape"], + "@type": ["owl:Class", "shacl:NodeShape", "skl:Noun"], "rdfs:label": "OpenApiDescription", - "rdfs:subClassOf": "https://standardknowledge.com/ontologies/core/Noun", "shacl:closed": false, "shacl:property": [ { - "shacl:class": "https://standardknowledge.com/ontologies/core/Integration", "shacl:maxCount": 1, "shacl:nodeKind": { "@id": "shacl:IRI" }, "shacl:path": "https://standardknowledge.com/ontologies/core/integration" @@ -186,9 +176,8 @@ }, { "@id": "https://standardknowledge.com/ontologies/core/TokenPaginatedCollection", - "@type": ["owl:Class", "shacl:NodeShape"], + "@type": ["owl:Class", "shacl:NodeShape", "skl:Noun"], "rdfs:label": "Token Paginated Collection", - "rdfs:subClassOf": "https://standardknowledge.com/ontologies/core/Noun", "shacl:closed": false, "shacl:property": [ { @@ -203,8 +192,7 @@ }, { "@id": "https://standardknowledge.com/ontologies/core/Verb", - "@type": ["owl:Class", "shacl:NodeShape"], - "rdfs:subClassOf": "https://standardknowledge.com/ontologies/core/Noun", + "@type": ["owl:Class", "shacl:NodeShape", "skl:Noun"], "rdfs:label": "Verb", "shacl:closed": false, "shacl:property": [ @@ -235,17 +223,17 @@ }, { "@id": "https://standardknowledge.com/ontologies/core/NounMappedVerb", - "@type": ["owl:Class", "shacl:NodeShape"], + "@type": ["owl:Class", "shacl:NodeShape", "skl:Noun"], "rdfs:subClassOf": "https://standardknowledge.com/ontologies/core/Verb" }, { "@id": "https://standardknowledge.com/ontologies/core/OpenApiSecuritySchemeVerb", - "@type": ["owl:Class", "shacl:NodeShape"], + "@type": ["owl:Class", "shacl:NodeShape", "skl:Noun"], "rdfs:subClassOf": "https://standardknowledge.com/ontologies/core/Verb" }, { "@id": "https://standardknowledge.com/ontologies/core/VerbIntegrationMapping", - "@type": ["owl:Class", "shacl:NodeShape"], + "@type": ["owl:Class", "shacl:NodeShape", "skl:Noun"], "rdfs:label": "Verb to Integration Mapping", "rdfs:subClassOf": "https://standardknowledge.com/ontologies/core/Mapping", "shacl:closed": false, @@ -256,7 +244,6 @@ "shacl:path": "https://standardknowledge.com/ontologies/core/parameterMapping" }, { - "shacl:class": "https://standardknowledge.com/ontologies/core/Verb", "shacl:maxCount": 1, "shacl:nodeKind": { "@id": "shacl:IRI" }, "shacl:path": "https://standardknowledge.com/ontologies/core/verb" @@ -272,7 +259,6 @@ "shacl:path": "https://standardknowledge.com/ontologies/core/returnValueMapping" }, { - "shacl:class": "https://standardknowledge.com/ontologies/core/Integration", "shacl:maxCount": 1, "shacl:nodeKind": { "@id": "shacl:IRI" }, "shacl:path": "https://standardknowledge.com/ontologies/core/integration" @@ -281,20 +267,18 @@ }, { "@id": "https://standardknowledge.com/ontologies/core/VerbNounMapping", - "@type": ["owl:Class", "shacl:NodeShape"], + "@type": ["owl:Class", "shacl:NodeShape", "skl:Noun"], "rdfs:label": "Verb to Noun Mapping", "rdfs:subClassOf": "https://standardknowledge.com/ontologies/core/Mapping", "shacl:closed": false, "shacl:property": [ { - "shacl:class": "https://standardknowledge.com/ontologies/core/Verb", "shacl:maxCount": 1, "shacl:minCount": 1, "shacl:nodeKind": { "@id": "shacl:IRI" }, "shacl:path": "https://standardknowledge.com/ontologies/core/verb" }, { - "shacl:class": "https://standardknowledge.com/ontologies/core/Noun", "shacl:maxCount": 1, "shacl:minCount": 1, "shacl:nodeKind": { "@id": "shacl:IRI" }, diff --git a/test/unit/SklEngine.test.ts b/test/unit/SklEngine.test.ts index 86d53d0..31e1ac7 100644 --- a/test/unit/SklEngine.test.ts +++ b/test/unit/SklEngine.test.ts @@ -1,4 +1,3 @@ -/* eslint-disable jest/no-disabled-tests */ /* eslint-disable @typescript-eslint/naming-convention */ import { OpenApiOperationExecutor } from '@comake/openapi-operation-executor'; import { RR } from '@comake/rmlmapper-js'; @@ -120,228 +119,407 @@ describe('SKLEngine', (): void => { jest.restoreAllMocks(); }); - it('delegates calls to executeRawQuery to the query adapter.', async(): Promise => { - const executeRawQuerySpy = jest.spyOn(SparqlQueryAdapter.prototype, 'executeRawQuery'); - await expect(sklEngine.executeRawQuery('')).resolves.toEqual([]); - expect(executeRawQuerySpy).toHaveBeenCalledTimes(1); - expect(executeRawQuerySpy).toHaveBeenCalledWith(''); + describe('executeRawQuery', (): void => { + it('delegates calls to executeRawQuery to the query adapter.', async(): Promise => { + const executeRawQuerySpy = jest.spyOn(SparqlQueryAdapter.prototype, 'executeRawQuery'); + await expect(sklEngine.executeRawQuery('')).resolves.toEqual([]); + expect(executeRawQuerySpy).toHaveBeenCalledTimes(1); + expect(executeRawQuerySpy).toHaveBeenCalledWith(''); + }); }); - it('delegates calls to executeRawEntityQuery to the query adapter.', async(): Promise => { - const executeRawEntityQuerySpy = jest.spyOn(SparqlQueryAdapter.prototype, 'executeRawEntityQuery'); - await expect(sklEngine.executeRawEntityQuery('', {})).resolves.toEqual({ '@graph': []}); - expect(executeRawEntityQuerySpy).toHaveBeenCalledTimes(1); - expect(executeRawEntityQuerySpy).toHaveBeenCalledWith('', {}); + describe('executeRawEntityQuery', (): void => { + it('delegates calls to executeRawEntityQuery to the query adapter.', async(): Promise => { + const executeRawEntityQuerySpy = jest.spyOn(SparqlQueryAdapter.prototype, 'executeRawEntityQuery'); + await expect(sklEngine.executeRawEntityQuery('', {})).resolves.toEqual({ '@graph': []}); + expect(executeRawEntityQuerySpy).toHaveBeenCalledTimes(1); + expect(executeRawEntityQuerySpy).toHaveBeenCalledWith('', {}); + }); }); - it('delegates calls to find to the query adapter.', async(): Promise => { - const findSpy = jest.spyOn(SparqlQueryAdapter.prototype, 'find'); - await expect(sklEngine.find({ where: { id: 'https://standardknowledge.com/ontologies/core/Share' }})).resolves.toEqual(schemas[0]); - expect(findSpy).toHaveBeenCalledTimes(1); - expect(findSpy).toHaveBeenCalledWith({ where: { id: 'https://standardknowledge.com/ontologies/core/Share' }}); - }); + describe('find', (): void => { + it('delegates calls to find to the query adapter.', async(): Promise => { + const findSpy = jest.spyOn(SparqlQueryAdapter.prototype, 'find'); + await expect(sklEngine.find({ where: { id: 'https://standardknowledge.com/ontologies/core/Share' }})).resolves.toEqual(schemas[0]); + expect(findSpy).toHaveBeenCalledTimes(1); + expect(findSpy).toHaveBeenCalledWith({ where: { id: 'https://standardknowledge.com/ontologies/core/Share' }}); + }); - it('throws an error if there is no schema matching the query during find.', async(): Promise => { - const findSpy = jest.spyOn(SparqlQueryAdapter.prototype, 'find'); - await expect(sklEngine.find({ where: { id: 'https://standardknowledge.com/ontologies/core/Send' }})).rejects.toThrow( - 'No schema found with fields matching {"where":{"id":"https://standardknowledge.com/ontologies/core/Send"}}', - ); - expect(findSpy).toHaveBeenCalledTimes(1); - expect(findSpy).toHaveBeenCalledWith({ where: { id: 'https://standardknowledge.com/ontologies/core/Send' }}); + it('throws an error if there is no schema matching the query during find.', async(): Promise => { + const findSpy = jest.spyOn(SparqlQueryAdapter.prototype, 'find'); + await expect(sklEngine.find({ where: { id: 'https://standardknowledge.com/ontologies/core/Send' }})).rejects.toThrow( + 'No schema found with fields matching {"where":{"id":"https://standardknowledge.com/ontologies/core/Send"}}', + ); + expect(findSpy).toHaveBeenCalledTimes(1); + expect(findSpy).toHaveBeenCalledWith({ where: { id: 'https://standardknowledge.com/ontologies/core/Send' }}); + }); }); - it('delegates calls to findBy to the query adapter.', async(): Promise => { - const findBySpy = jest.spyOn(SparqlQueryAdapter.prototype, 'findBy'); - await expect(sklEngine.findBy({ id: 'https://standardknowledge.com/ontologies/core/Share' })).resolves.toEqual(schemas[0]); - expect(findBySpy).toHaveBeenCalledTimes(1); - expect(findBySpy).toHaveBeenCalledWith({ id: 'https://standardknowledge.com/ontologies/core/Share' }); - }); + describe('findBy', (): void => { + it('delegates calls to findBy to the query adapter.', async(): Promise => { + const findBySpy = jest.spyOn(SparqlQueryAdapter.prototype, 'findBy'); + await expect(sklEngine.findBy({ id: 'https://standardknowledge.com/ontologies/core/Share' })).resolves.toEqual(schemas[0]); + expect(findBySpy).toHaveBeenCalledTimes(1); + expect(findBySpy).toHaveBeenCalledWith({ id: 'https://standardknowledge.com/ontologies/core/Share' }); + }); - it('throws an error if there is no schema matching the query during findBy.', async(): Promise => { - const findBySpy = jest.spyOn(SparqlQueryAdapter.prototype, 'findBy'); - await expect(sklEngine.findBy({ id: 'https://standardknowledge.com/ontologies/core/Send' })).rejects.toThrow( - 'No schema found with fields matching {"id":"https://standardknowledge.com/ontologies/core/Send"}', - ); - expect(findBySpy).toHaveBeenCalledTimes(1); - expect(findBySpy).toHaveBeenCalledWith({ id: 'https://standardknowledge.com/ontologies/core/Send' }); + it('throws an error if there is no schema matching the query during findBy.', async(): Promise => { + const findBySpy = jest.spyOn(SparqlQueryAdapter.prototype, 'findBy'); + await expect(sklEngine.findBy({ id: 'https://standardknowledge.com/ontologies/core/Send' })).rejects.toThrow( + 'No schema found with fields matching {"id":"https://standardknowledge.com/ontologies/core/Send"}', + ); + expect(findBySpy).toHaveBeenCalledTimes(1); + expect(findBySpy).toHaveBeenCalledWith({ id: 'https://standardknowledge.com/ontologies/core/Send' }); + }); }); - it('delegates calls to findAll to the query adapter.', async(): Promise => { - const findAllSpy = jest.spyOn(SparqlQueryAdapter.prototype, 'findAll'); - await expect(sklEngine.findAll({ where: { id: 'https://standardknowledge.com/ontologies/core/Share' }})).resolves.toEqual([ schemas[0] ]); - expect(findAllSpy).toHaveBeenCalledTimes(1); - expect(findAllSpy).toHaveBeenCalledWith({ where: { id: 'https://standardknowledge.com/ontologies/core/Share' }}); + describe('findAll', (): void => { + it('delegates calls to findAll to the query adapter.', async(): Promise => { + const findAllSpy = jest.spyOn(SparqlQueryAdapter.prototype, 'findAll'); + await expect(sklEngine.findAll({ where: { id: 'https://standardknowledge.com/ontologies/core/Share' }})).resolves.toEqual([ schemas[0] ]); + expect(findAllSpy).toHaveBeenCalledTimes(1); + expect(findAllSpy).toHaveBeenCalledWith({ where: { id: 'https://standardknowledge.com/ontologies/core/Share' }}); + }); }); - it('delegates calls to findAllBy to the query adapter.', async(): Promise => { - const findAllBySpy = jest.spyOn(SparqlQueryAdapter.prototype, 'findAllBy'); - await expect(sklEngine.findAllBy({ id: 'https://standardknowledge.com/ontologies/core/Share' })).resolves.toEqual([ schemas[0] ]); - expect(findAllBySpy).toHaveBeenCalledTimes(1); - expect(findAllBySpy).toHaveBeenCalledWith({ id: 'https://standardknowledge.com/ontologies/core/Share' }); + describe('findAllBy', (): void => { + it('delegates calls to findAllBy to the query adapter.', async(): Promise => { + const findAllBySpy = jest.spyOn(SparqlQueryAdapter.prototype, 'findAllBy'); + await expect(sklEngine.findAllBy({ id: 'https://standardknowledge.com/ontologies/core/Share' })).resolves.toEqual([ schemas[0] ]); + expect(findAllBySpy).toHaveBeenCalledTimes(1); + expect(findAllBySpy).toHaveBeenCalledWith({ id: 'https://standardknowledge.com/ontologies/core/Share' }); + }); }); - it('delegates calls to exists to the query adapter.', async(): Promise => { - const existsSpy = jest.spyOn(SparqlQueryAdapter.prototype, 'exists'); - await expect(sklEngine.exists({ where: { id: 'https://standardknowledge.com/ontologies/core/Share' }})).resolves.toBe(true); - expect(existsSpy).toHaveBeenCalledTimes(1); - expect(existsSpy).toHaveBeenCalledWith({ where: { id: 'https://standardknowledge.com/ontologies/core/Share' }}); + describe('exists', (): void => { + it('delegates calls to exists to the query adapter.', async(): Promise => { + const existsSpy = jest.spyOn(SparqlQueryAdapter.prototype, 'exists'); + await expect(sklEngine.exists({ where: { id: 'https://standardknowledge.com/ontologies/core/Share' }})).resolves.toBe(true); + expect(existsSpy).toHaveBeenCalledTimes(1); + expect(existsSpy).toHaveBeenCalledWith({ where: { id: 'https://standardknowledge.com/ontologies/core/Share' }}); + }); }); - it('delegates calls to count to the query adapter.', async(): Promise => { - const countSpy = jest.spyOn(SparqlQueryAdapter.prototype, 'count'); - await expect(sklEngine.count({ where: { id: 'https://standardknowledge.com/ontologies/core/Share' }})).resolves.toBe(1); - expect(countSpy).toHaveBeenCalledTimes(1); - expect(countSpy).toHaveBeenCalledWith({ where: { id: 'https://standardknowledge.com/ontologies/core/Share' }}); + describe('count', (): void => { + it('delegates calls to count to the query adapter.', async(): Promise => { + const countSpy = jest.spyOn(SparqlQueryAdapter.prototype, 'count'); + await expect(sklEngine.count({ where: { id: 'https://standardknowledge.com/ontologies/core/Share' }})).resolves.toBe(1); + expect(countSpy).toHaveBeenCalledTimes(1); + expect(countSpy).toHaveBeenCalledWith({ where: { id: 'https://standardknowledge.com/ontologies/core/Share' }}); + }); }); - it('delegates calls to save a single entity to the query adapter.', async(): Promise => { - const saveSpy = jest.spyOn(SparqlQueryAdapter.prototype, 'save'); - const res = await sklEngine.save({ - '@id': 'https://standardknowledge.com/ontologies/core/Share', - '@type': 'https://standardknowledge.com/ontologies/core/Verb', - [RDFS.label]: 'Share', - }); - expect(res).toEqual({ - '@id': 'https://standardknowledge.com/ontologies/core/Share', - '@type': 'https://standardknowledge.com/ontologies/core/Verb', - [RDFS.label]: 'Share', - }); - expect(saveSpy).toHaveBeenCalledTimes(1); - expect(saveSpy).toHaveBeenCalledWith({ - '@id': 'https://standardknowledge.com/ontologies/core/Share', - '@type': 'https://standardknowledge.com/ontologies/core/Verb', - [RDFS.label]: 'Share', + describe('save', (): void => { + beforeEach(async(): Promise => { + schemas = await frameAndCombineSchemas([ './test/assets/schemas/core.json' ]); + await sklEngine.save(schemas); }); - }); - it('delegates calls to save multiple entities to the query adapter.', async(): Promise => { - const saveSpy = jest.spyOn(SparqlQueryAdapter.prototype, 'save'); - const entities = [ - { + it('delegates calls to save a single entity to the query adapter.', async(): Promise => { + const saveSpy = jest.spyOn(SparqlQueryAdapter.prototype, 'save'); + const res = await sklEngine.save({ '@id': 'https://standardknowledge.com/ontologies/core/Share', '@type': 'https://standardknowledge.com/ontologies/core/Verb', [RDFS.label]: 'Share', - }, - { - '@id': 'https://standardknowledge.com/ontologies/core/Send', + }); + expect(res).toEqual({ + '@id': 'https://standardknowledge.com/ontologies/core/Share', '@type': 'https://standardknowledge.com/ontologies/core/Verb', - [RDFS.label]: 'Send', - }, - ]; - const res = await sklEngine.save(entities); - expect(res).toEqual(entities); - expect(saveSpy).toHaveBeenCalledTimes(1); - expect(saveSpy).toHaveBeenCalledWith(entities); - }); + [RDFS.label]: 'Share', + }); + expect(saveSpy).toHaveBeenCalledTimes(1); + expect(saveSpy).toHaveBeenCalledWith({ + '@id': 'https://standardknowledge.com/ontologies/core/Share', + '@type': 'https://standardknowledge.com/ontologies/core/Verb', + [RDFS.label]: 'Share', + }); + }); - it('delegates calls to update a single entity to the query adapter.', async(): Promise => { - const updateSpy = jest.spyOn(SparqlQueryAdapter.prototype, 'update'); - const res = await sklEngine.update( - 'https://standardknowledge.com/ontologies/core/Share', - { [RDFS.label]: 'Share' }, - ); - expect(res).toBeUndefined(); - expect(updateSpy).toHaveBeenCalledTimes(1); - expect(updateSpy).toHaveBeenCalledWith( - 'https://standardknowledge.com/ontologies/core/Share', - { [RDFS.label]: 'Share' }, - ); - }); + it('delegates calls to save multiple entities to the query adapter.', async(): Promise => { + const saveSpy = jest.spyOn(SparqlQueryAdapter.prototype, 'save'); + const entities = [ + { + '@id': 'https://standardknowledge.com/ontologies/core/Share', + '@type': 'https://standardknowledge.com/ontologies/core/Verb', + [RDFS.label]: 'Share', + }, + { + '@id': 'https://standardknowledge.com/ontologies/core/Send', + '@type': 'https://standardknowledge.com/ontologies/core/Verb', + [RDFS.label]: 'Send', + }, + ]; + const res = await sklEngine.save(entities); + expect(res).toEqual(entities); + expect(saveSpy).toHaveBeenCalledTimes(1); + expect(saveSpy).toHaveBeenCalledWith(entities); + }); - it('delegates calls to update multiple entities to the query adapter.', async(): Promise => { - const updateSpy = jest.spyOn(SparqlQueryAdapter.prototype, 'update'); - const res = await sklEngine.update( - [ - 'https://standardknowledge.com/ontologies/core/Share', - 'https://standardknowledge.com/ontologies/core/Send', - ], - { [RDFS.label]: 'Share' }, - ); - expect(res).toBeUndefined(); - expect(updateSpy).toHaveBeenCalledTimes(1); - expect(updateSpy).toHaveBeenCalledWith( - [ - 'https://standardknowledge.com/ontologies/core/Share', - 'https://standardknowledge.com/ontologies/core/Send', - ], - { [RDFS.label]: 'Share' }, - ); - }); + it('throws an error if the entity does not conform to the schema.', async(): Promise => { + const saveSpy = jest.spyOn(SparqlQueryAdapter.prototype, 'save'); + const entity = { + '@id': 'https://example.com/data/1', + '@type': 'https://standardknowledge.com/ontologies/core/File', + }; + await expect(sklEngine.save(entity)).rejects.toThrow( + 'Entity https://example.com/data/1 does not conform to the https://standardknowledge.com/ontologies/core/File schema', + ); + expect(saveSpy).toHaveBeenCalledTimes(0); + }); - it('delegates calls to delete a single entity to the query adapter.', async(): Promise => { - const deleteSpy = jest.spyOn(SparqlQueryAdapter.prototype, 'delete'); - const entity = { - '@id': 'https://standardknowledge.com/ontologies/core/Share', - '@type': 'https://standardknowledge.com/ontologies/core/Verb', - }; - await sklEngine.save(entity); - const res = await sklEngine.delete(entity['@id']); - expect(res).toBeUndefined(); - expect(deleteSpy).toHaveBeenCalledTimes(1); - expect(deleteSpy).toHaveBeenCalledWith(entity['@id']); + it('throws an error if the entity does not conform to the parent schema.', async(): Promise => { + const saveSpy = jest.spyOn(SparqlQueryAdapter.prototype, 'save'); + const entity = { + '@id': 'https://example.com/data/1', + '@type': 'https://standardknowledge.com/ontologies/core/Folder', + }; + await expect(sklEngine.save(entity)).rejects.toThrow( + 'Entity https://example.com/data/1 does not conform to the https://standardknowledge.com/ontologies/core/File schema', + ); + expect(saveSpy).toHaveBeenCalledTimes(0); + }); + + it('throws an error if one of the entities does not conform to the schema.', async(): Promise => { + const saveSpy = jest.spyOn(SparqlQueryAdapter.prototype, 'save'); + const entities = [ + { + '@id': 'https://example.com/data/1', + '@type': 'https://standardknowledge.com/ontologies/core/File', + }, + { + '@id': 'https://example.com/data/2', + '@type': 'https://standardknowledge.com/ontologies/core/File', + }, + ]; + await expect(sklEngine.save(entities)).rejects.toThrow( + 'An entity does not conform to the https://standardknowledge.com/ontologies/core/File schema', + ); + expect(saveSpy).toHaveBeenCalledTimes(0); + }); + + it('throws an error if one of the entities does not conform to the parent schema.', async(): Promise => { + const saveSpy = jest.spyOn(SparqlQueryAdapter.prototype, 'save'); + const entities = [ + { + '@id': 'https://example.com/data/1', + '@type': 'https://standardknowledge.com/ontologies/core/Folder', + }, + { + '@id': 'https://example.com/data/2', + '@type': 'https://standardknowledge.com/ontologies/core/Folder', + }, + ]; + await expect(sklEngine.save(entities)).rejects.toThrow( + 'An entity does not conform to the https://standardknowledge.com/ontologies/core/File schema', + ); + expect(saveSpy).toHaveBeenCalledTimes(0); + }); }); - it('delegates calls to delete miltiple entities to the query adapter.', async(): Promise => { - const deleteSpy = jest.spyOn(SparqlQueryAdapter.prototype, 'delete'); - const entities = [ + describe('update', (): void => { + let entities = [ { - '@id': 'https://standardknowledge.com/ontologies/core/Share', - '@type': 'https://standardknowledge.com/ontologies/core/Verb', - [RDFS.label]: 'Share', + '@id': 'https://example.com/data/1', + '@type': 'https://standardknowledge.com/ontologies/core/File', + [RDFS.label]: { + '@type': XSD.string, + '@value': 'fileA', + }, + [SKL.sourceId]: { + '@type': XSD.string, + '@value': '12345', + }, + [SKL.integration]: { + '@id': 'https://example.com/integrations/Dropbox', + }, }, { - '@id': 'https://standardknowledge.com/ontologies/core/Send', - '@type': 'https://standardknowledge.com/ontologies/core/Verb', - [RDFS.label]: 'Send', + '@id': 'https://example.com/data/2', + '@type': 'https://standardknowledge.com/ontologies/core/File', + [RDFS.label]: { + '@type': XSD.string, + '@value': 'fileB', + }, + [SKL.sourceId]: { + '@type': XSD.string, + '@value': '12346', + }, + [SKL.integration]: { + '@id': 'https://example.com/integrations/Dropbox', + }, }, ]; const entityIds = entities.map((entity): string => entity['@id']); - await sklEngine.save(entities); - const res = await sklEngine.delete(entityIds); - expect(res).toBeUndefined(); - expect(deleteSpy).toHaveBeenCalledTimes(1); - expect(deleteSpy).toHaveBeenCalledWith(entityIds); - }); - - it('delegates calls to destroy a single entity to the query adapter.', async(): Promise => { - const destroySpy = jest.spyOn(SparqlQueryAdapter.prototype, 'destroy'); - const entity = { - '@id': 'https://standardknowledge.com/ontologies/core/Share', - '@type': 'https://standardknowledge.com/ontologies/core/Verb', + const badAttributes = { + [RDFS.label]: [ + 'file1', + 'file2', + ], }; - await sklEngine.save(entity); - const res = await sklEngine.destroy(entity); - expect(res).toEqual(entity); - expect(destroySpy).toHaveBeenCalledTimes(1); - expect(destroySpy).toHaveBeenCalledWith(entity); + + beforeEach(async(): Promise => { + schemas = await frameAndCombineSchemas([ './test/assets/schemas/core.json' ]); + await sklEngine.save(schemas); + await sklEngine.save(entities); + }); + + it('delegates calls to update a single entity to the query adapter.', async(): Promise => { + const updateSpy = jest.spyOn(SparqlQueryAdapter.prototype, 'update'); + const res = await sklEngine.update( + entities[0]['@id'], + { [RDFS.label]: 'file1' }, + ); + expect(res).toBeUndefined(); + expect(updateSpy).toHaveBeenCalledTimes(1); + expect(updateSpy).toHaveBeenCalledWith( + entities[0]['@id'], + { [RDFS.label]: 'file1' }, + ); + }); + + it('delegates calls to update multiple entities to the query adapter.', async(): Promise => { + const updateSpy = jest.spyOn(SparqlQueryAdapter.prototype, 'update'); + const res = await sklEngine.update( + entityIds, + { [RDFS.label]: 'file1' }, + ); + expect(res).toBeUndefined(); + expect(updateSpy).toHaveBeenCalledTimes(1); + expect(updateSpy).toHaveBeenCalledWith( + entityIds, + { [RDFS.label]: 'file1' }, + ); + }); + + it('throws an error if the entity does not conform to the schema.', async(): Promise => { + const updateSpy = jest.spyOn(SparqlQueryAdapter.prototype, 'update'); + await expect(sklEngine.update(entities[0]['@id'], badAttributes)).rejects.toThrow( + 'Entity https://example.com/data/1 does not conform to the https://standardknowledge.com/ontologies/core/File schema', + ); + expect(updateSpy).toHaveBeenCalledTimes(0); + }); + + it('throws an error if the entity does not conform to the parent schema.', async(): Promise => { + const updateSpy = jest.spyOn(SparqlQueryAdapter.prototype, 'update'); + const entity = { + ...entities[0], + '@type': 'https://standardknowledge.com/ontologies/core/Folder', + }; + await sklEngine.save(entity); + await expect(sklEngine.update(entity['@id'], badAttributes)).rejects.toThrow( + 'Entity https://example.com/data/1 does not conform to the https://standardknowledge.com/ontologies/core/File schema', + ); + expect(updateSpy).toHaveBeenCalledTimes(0); + }); + + it('throws an error if one of the entities does not conform to the schema.', async(): Promise => { + const updateSpy = jest.spyOn(SparqlQueryAdapter.prototype, 'update'); + await expect(sklEngine.update(entityIds, badAttributes)).rejects.toThrow( + 'Entity https://example.com/data/1 does not conform to the https://standardknowledge.com/ontologies/core/File schema', + ); + expect(updateSpy).toHaveBeenCalledTimes(0); + }); + + it('throws an error if one of the entities does not conform to the parent schema.', async(): Promise => { + const updateSpy = jest.spyOn(SparqlQueryAdapter.prototype, 'update'); + entities = [ + { + ...entities[0], + '@type': 'https://standardknowledge.com/ontologies/core/Folder', + }, + { + ...entities[0], + '@type': 'https://standardknowledge.com/ontologies/core/Folder', + }, + ]; + await sklEngine.save(entities); + await expect(sklEngine.update(entityIds, badAttributes)).rejects.toThrow( + 'Entity https://example.com/data/1 does not conform to the https://standardknowledge.com/ontologies/core/File schema', + ); + expect(updateSpy).toHaveBeenCalledTimes(0); + }); }); - it('delegates calls to destroy miltiple entities to the query adapter.', async(): Promise => { - const destroySpy = jest.spyOn(SparqlQueryAdapter.prototype, 'destroy'); - const entities = [ - { + describe('delete', (): void => { + it('delegates calls to delete a single entity to the query adapter.', async(): Promise => { + const deleteSpy = jest.spyOn(SparqlQueryAdapter.prototype, 'delete'); + const entity = { '@id': 'https://standardknowledge.com/ontologies/core/Share', '@type': 'https://standardknowledge.com/ontologies/core/Verb', - [RDFS.label]: 'Share', - }, - { - '@id': 'https://standardknowledge.com/ontologies/core/Send', + }; + await sklEngine.save(entity); + const res = await sklEngine.delete(entity['@id']); + expect(res).toBeUndefined(); + expect(deleteSpy).toHaveBeenCalledTimes(1); + expect(deleteSpy).toHaveBeenCalledWith(entity['@id']); + }); + + it('delegates calls to delete miltiple entities to the query adapter.', async(): Promise => { + const deleteSpy = jest.spyOn(SparqlQueryAdapter.prototype, 'delete'); + const entities = [ + { + '@id': 'https://standardknowledge.com/ontologies/core/Share', + '@type': 'https://standardknowledge.com/ontologies/core/Verb', + [RDFS.label]: 'Share', + }, + { + '@id': 'https://standardknowledge.com/ontologies/core/Send', + '@type': 'https://standardknowledge.com/ontologies/core/Verb', + [RDFS.label]: 'Send', + }, + ]; + const entityIds = entities.map((entity): string => entity['@id']); + await sklEngine.save(entities); + const res = await sklEngine.delete(entityIds); + expect(res).toBeUndefined(); + expect(deleteSpy).toHaveBeenCalledTimes(1); + expect(deleteSpy).toHaveBeenCalledWith(entityIds); + }); + }); + + describe('destroy', (): void => { + it('delegates calls to destroy a single entity to the query adapter.', async(): Promise => { + const destroySpy = jest.spyOn(SparqlQueryAdapter.prototype, 'destroy'); + const entity = { + '@id': 'https://standardknowledge.com/ontologies/core/Share', '@type': 'https://standardknowledge.com/ontologies/core/Verb', - [RDFS.label]: 'Send', - }, - ]; - await sklEngine.save(entities); - const res = await sklEngine.destroy(entities); - expect(res).toEqual(entities); - expect(destroySpy).toHaveBeenCalledTimes(1); - expect(destroySpy).toHaveBeenCalledWith(entities); + }; + await sklEngine.save(entity); + const res = await sklEngine.destroy(entity); + expect(res).toEqual(entity); + expect(destroySpy).toHaveBeenCalledTimes(1); + expect(destroySpy).toHaveBeenCalledWith(entity); + }); + + it('delegates calls to destroy miltiple entities to the query adapter.', async(): Promise => { + const destroySpy = jest.spyOn(SparqlQueryAdapter.prototype, 'destroy'); + const entities = [ + { + '@id': 'https://standardknowledge.com/ontologies/core/Share', + '@type': 'https://standardknowledge.com/ontologies/core/Verb', + [RDFS.label]: 'Share', + }, + { + '@id': 'https://standardknowledge.com/ontologies/core/Send', + '@type': 'https://standardknowledge.com/ontologies/core/Verb', + [RDFS.label]: 'Send', + }, + ]; + await sklEngine.save(entities); + const res = await sklEngine.destroy(entities); + expect(res).toEqual(entities); + expect(destroySpy).toHaveBeenCalledTimes(1); + expect(destroySpy).toHaveBeenCalledWith(entities); + }); }); - it('delegates calls to destroyAll to the query adapter.', async(): Promise => { - const destroyAllSpy = jest.spyOn(SparqlQueryAdapter.prototype, 'destroyAll'); - const res = await sklEngine.destroyAll(); - expect(res).toBeUndefined(); - expect(destroyAllSpy).toHaveBeenCalledTimes(1); + describe('destroyAll', (): void => { + it('delegates calls to destroyAll to the query adapter.', async(): Promise => { + const destroyAllSpy = jest.spyOn(SparqlQueryAdapter.prototype, 'destroyAll'); + const res = await sklEngine.destroyAll(); + expect(res).toBeUndefined(); + expect(destroyAllSpy).toHaveBeenCalledTimes(1); + }); }); }); @@ -826,7 +1004,7 @@ describe('SKLEngine', (): void => { (OpenApiOperationExecutor as jest.Mock).mockReturnValue({ executeOperation, setOpenapiSpec }); }); - it.skip('can execute a Noun mapped Verb defined via a verbMapping.', async(): Promise => { + it('can execute a Noun mapped Verb defined via a verbMapping.', async(): Promise => { const sklEngine = new SKLEngine({ type: 'memory' }); await sklEngine.save(schemas); const response = await sklEngine.verb.sync({ @@ -837,7 +1015,7 @@ describe('SKLEngine', (): void => { expect(response).toEqual(expectedGetFileResponse); }); - it.skip('can execute a Noun mapped Verb with only a mapping.', async(): Promise => { + it('can execute a Noun mapped Verb with only a mapping.', async(): Promise => { const sklEngine = new SKLEngine({ type: 'memory' }); await sklEngine.save(schemas); const response = await sklEngine.verb.getName({ @@ -849,7 +1027,7 @@ describe('SKLEngine', (): void => { }); }); - it.skip('can execute a Noun mapped Verb through a mapping that defines a constant verbId.', + it('can execute a Noun mapped Verb through a mapping that defines a constant verbId.', async(): Promise => { schemas = schemas.map((schemaItem: any): any => { if (schemaItem['@id'] === 'https://example.com/data/34') { @@ -965,7 +1143,7 @@ describe('SKLEngine', (): void => { }); describe('calling Verbs which specify a series sub Verb execution', (): void => { - it.skip('can execute multiple Verbs in series.', async(): Promise => { + it('can execute multiple Verbs in series.', async(): Promise => { schemas = await frameAndCombineSchemas([ './test/assets/schemas/core.json', './test/assets/schemas/series-verb.json', @@ -991,7 +1169,7 @@ describe('SKLEngine', (): void => { expect(response).toEqual({}); }); - it.skip('runs a preProcessingMapping and adds preProcessedParameters to the series verb arguments.', + it('runs a preProcessingMapping and adds preProcessedParameters to the series verb arguments.', async(): Promise => { schemas = await frameAndCombineSchemas([ './test/assets/schemas/core.json', @@ -1042,7 +1220,7 @@ describe('SKLEngine', (): void => { ]); }); - it.skip('can execute multiple Verbs in parallel.', async(): Promise => { + it('can execute multiple Verbs in parallel.', async(): Promise => { const functions = { 'https://example.com/functions/parseLinksFromText'(data: any): string[] { const text = data['https://example.com/functions/text']; @@ -1088,7 +1266,7 @@ describe('SKLEngine', (): void => { ]); }); - it.skip('can execute multiple Verbs in with return values that have ids.', async(): Promise => { + it('can execute multiple Verbs in with return values that have ids.', async(): Promise => { const functions = { 'https://example.com/functions/parseLinksFromText'(data: any): string[] { const text = data['https://example.com/functions/text']; diff --git a/test/unit/storage/query-adapter/sparql/query-executor/InMemorySparqlQueryExecutor.test.ts b/test/unit/storage/query-adapter/sparql/query-executor/InMemorySparqlQueryExecutor.test.ts index 07545f8..52fb445 100644 --- a/test/unit/storage/query-adapter/sparql/query-executor/InMemorySparqlQueryExecutor.test.ts +++ b/test/unit/storage/query-adapter/sparql/query-executor/InMemorySparqlQueryExecutor.test.ts @@ -146,11 +146,12 @@ describe('a MemoryQueryAdapter', (): void => { describe('executeSparqlUpdate', (): void => { it('executes a void query and returns undefined.', async(): Promise => { + const updateQuery = { type: 'update', updates: [{}]} as any; await expect( - executor.executeSparqlUpdate({} as any), + executor.executeSparqlUpdate(updateQuery), ).resolves.toBeUndefined(); expect(stringify).toHaveBeenCalledTimes(1); - expect(stringify).toHaveBeenCalledWith({}); + expect(stringify).toHaveBeenCalledWith(updateQuery); expect(queryVoid).toHaveBeenCalledTimes(1); expect(queryVoid).toHaveBeenCalledWith( 'query',