diff --git a/.vscode/settings.json b/.vscode/settings.json index a29643e..254547e 100644 --- a/.vscode/settings.json +++ b/.vscode/settings.json @@ -2,5 +2,6 @@ "deno.enable": true, "deno.importMap": ".vscode/import_map.json", "deno.documentPreloadLimit": 0, - "editor.defaultFormatter": "denoland.vscode-deno" + "editor.defaultFormatter": "denoland.vscode-deno", + "editor.formatOnSave": true } diff --git a/library/decoder.ts b/library/decoder.ts index be36148..587c16d 100644 --- a/library/decoder.ts +++ b/library/decoder.ts @@ -75,8 +75,6 @@ class Decoder { throw new Error(`Error decoding graph, <${nodeIri}> node not found.`); } - output.$type = this.decodeNodeType(node); - Object.keys(schema).forEach((key) => { if (key === "@type") { return; @@ -97,26 +95,16 @@ class Decoder { return output; } - decodeNodeType(node: Node) { - const typeTerms = node.get(rdf.type); - if (!typeTerms) { - return []; - } - return typeTerms.reduce((acc, term) => { - if (term.value !== ldkit.Resource) { - acc.push(term.value); - } - return acc; - }, [] as Iri[]); - } - decodeNodeProperty( nodeIri: Iri, node: Node, propertyKey: string, property: Property, ) { - const terms = node.get(property["@id"]); + const allTerms = node.get(property["@id"]); + const terms = property["@id"] !== rdf.type + ? allTerms + : allTerms?.filter((term) => term.value !== ldkit.Resource); if (!terms) { if (!property["@optional"]) { diff --git a/library/lens/query_builder.ts b/library/lens/query_builder.ts index 5988010..5343d38 100644 --- a/library/lens/query_builder.ts +++ b/library/lens/query_builder.ts @@ -114,7 +114,7 @@ export class QueryBuilder { } getQuery(where?: string | RDF.Quad[], limit = 1000) { - const selectSubQuery = SELECT` + const selectSubQuery = SELECT.DISTINCT` ${this.df.variable!("iri")} `.WHERE` ${this.getShape(false, true)} @@ -123,10 +123,8 @@ export class QueryBuilder { const query = CONSTRUCT` ${this.getResourceSignature()} - ${this.getTypesSignature()} ${this.getShape(true, false, true)} `.WHERE` - ${this.getTypesSignature()} ${this.getShape(true, true, true)} { ${selectSubQuery} @@ -139,11 +137,9 @@ export class QueryBuilder { getByIrisQuery(iris: Iri[]) { const query = CONSTRUCT` ${this.getResourceSignature()} - ${this.getTypesSignature()} ${this.getShape(true, false, true)} `.WHERE` - ${this.getTypesSignature()} - ${this.getShape(true, true, true)} + ${this.getShape(true, true, false)} VALUES ?iri { ${iris.map(this.df.namedNode)} } diff --git a/library/schema/interface.ts b/library/schema/interface.ts index f435ee8..71be94e 100644 --- a/library/schema/interface.ts +++ b/library/schema/interface.ts @@ -66,5 +66,4 @@ export type SchemaInterface = ? ConvertProperty : never; } - & SchemaInterfaceIdentity - & SchemaInterfaceType; + & SchemaInterfaceIdentity; diff --git a/library/schema/schema.ts b/library/schema/schema.ts index 98df35b..7916326 100644 --- a/library/schema/schema.ts +++ b/library/schema/schema.ts @@ -16,7 +16,7 @@ export type SchemaPrototypeProperties = { }; export type SchemaPrototypeType = { - "@type": string | readonly string[]; + "@type"?: string | readonly string[]; }; export type SchemaPrototype = SchemaPrototypeProperties & SchemaPrototypeType; diff --git a/library/schema/utils.ts b/library/schema/utils.ts index 931291a..45d13d5 100644 --- a/library/schema/utils.ts +++ b/library/schema/utils.ts @@ -1,3 +1,4 @@ +import rdf from "../namespaces/rdf.ts"; import xsd from "../namespaces/xsd.ts"; import type { @@ -8,6 +9,20 @@ import type { } from "./schema.ts"; export const expandSchema = (schemaPrototype: SchemaPrototype) => { + if (typeof schemaPrototype !== "object") { + throw new Error(`Invalid schema, expected object`); + } + + if (Object.keys(schemaPrototype).length === 0) { + throw new Error( + `Invalid schema, empty object, expected "@type" key or property definition`, + ); + } + + const expandShortcut = (value: string) => { + return value === "@type" ? rdf.type : value; + }; + const expandArray = (stringOrStrings: T | readonly T[]) => { return Array.isArray(stringOrStrings) ? stringOrStrings : [stringOrStrings]; }; @@ -17,7 +32,7 @@ export const expandSchema = (schemaPrototype: SchemaPrototype) => { ) => { if (typeof stringOrProperty === "string") { return { - "@id": stringOrProperty, + "@id": expandShortcut(stringOrProperty), "@type": xsd.string, }; } @@ -44,6 +59,8 @@ export const expandSchema = (schemaPrototype: SchemaPrototype) => { const expandedProperty = Object.keys(property).reduce((acc, key) => { if (key === "@context") { acc[key] = expandSchema(property[key]!); + } else if (key === "@id") { + acc[key] = expandShortcut(property[key]); } else if (validKeys.includes(key as keyof PropertyPrototype)) { acc[key] = property[key as keyof PropertyPrototype] as unknown; } @@ -63,7 +80,7 @@ export const expandSchema = (schemaPrototype: SchemaPrototype) => { return Object.keys(schemaPrototype).reduce((acc, key) => { if (key === "@type") { - acc[key] = expandArray(schemaPrototype[key]); + acc[key] = expandArray(schemaPrototype[key]!); } else { acc[key] = expandSchemaProperty( schemaPrototype[key] as string | PropertyPrototype, diff --git a/tests/decoder.test.ts b/tests/decoder.test.ts index 497b597..4dc8773 100644 --- a/tests/decoder.test.ts +++ b/tests/decoder.test.ts @@ -4,7 +4,7 @@ import { createGraph, x } from "./test_utils.ts"; import type { Context } from "../library/rdf.ts"; import { decode } from "../library/decoder.ts"; import type { Schema } from "../library/schema/mod.ts"; -import { xsd } from "../library/namespaces/mod.ts"; +import { rdf, xsd } from "../library/namespaces/mod.ts"; const decodeGraph = ( turtle: string, @@ -21,15 +21,14 @@ const evaluate = ( Deno.test("Decoder / Minimal resource", () => { const input = ` - x:A a ldkit:Resource, x:Item . + x:A a ldkit:Resource . `; - const schema = { "@type": [x.Item] }; + const schema = { "@type": [] }; const output = [ { $id: x.A, - $type: [x.Item], }, ]; @@ -38,25 +37,91 @@ Deno.test("Decoder / Minimal resource", () => { Deno.test("Decoder / Multiple minimal resources", () => { const input = ` - x:A a ldkit:Resource, x:Item . - x:B a ldkit:Resource, x:Item . - x:C a ldkit:Resource, x:Item . + x:A a ldkit:Resource . + x:B a ldkit:Resource . + x:C a ldkit:Resource . `; - const schema = { "@type": [x.Item] }; + const schema = { "@type": [] }; const output = [ { $id: x.A, - $type: [x.Item], }, { $id: x.B, - $type: [x.Item], }, { $id: x.C, - $type: [x.Item], + }, + ]; + + evaluate(input, schema, output); +}); + +Deno.test("Decoder / Query without RDF type", () => { + const input = ` + x:A a ldkit:Resource; + x:property "value" . + `; + + const schema = { + "@type": [], + property: { + "@id": x.property, + }, + }; + + const output = [ + { + $id: x.A, + property: "value", + }, + ]; + + evaluate(input, schema, output); +}); + +Deno.test("Decoder / Query for RDF types", () => { + const input = ` + x:A a ldkit:Resource, x:TypeA, x:TypeB, x:TypeC . + `; + + const schema = { + "@type": [], + types: { + "@id": rdf.type, + "@array": true as const, + }, + }; + + const output = [ + { + $id: x.A, + types: [x.TypeA, x.TypeB, x.TypeC], + }, + ]; + + evaluate(input, schema, output); +}); + +Deno.test("Decoder / Query with multiple RDF types for RDF types", () => { + const input = ` + x:A a ldkit:Resource, x:TypeA, x:TypeB, x:TypeC . + `; + + const schema = { + "@type": [x.TypeA, x.TypeB], + types: { + "@id": rdf.type, + "@array": true as const, + }, + }; + + const output = [ + { + $id: x.A, + types: [x.TypeA, x.TypeB, x.TypeC], }, ]; @@ -106,7 +171,6 @@ Deno.test("Decoder / Basic types", () => { const output = [ { $id: x.A, - $type: [x.Item], string: "LDKit", integer: -5, decimal: -5.0, @@ -121,11 +185,11 @@ Deno.test("Decoder / Basic types", () => { Deno.test("Decoder / Required property missing", () => { const input = ` - x:A a ldkit:Resource, x:Item . + x:A a ldkit:Resource . `; const schema = { - "@type": [x.Item], + "@type": [], required: { "@id": x.required, }, @@ -136,11 +200,11 @@ Deno.test("Decoder / Required property missing", () => { Deno.test("Decoder / Optional property missing", () => { const input = ` - x:A a ldkit:Resource, x:Item . + x:A a ldkit:Resource . `; const schema = { - "@type": [x.Item], + "@type": [], optional: { "@id": x.optional, "@optional": true as const, @@ -150,7 +214,6 @@ Deno.test("Decoder / Optional property missing", () => { const output = [ { $id: x.A, - $type: [x.Item], }, ]; @@ -159,11 +222,11 @@ Deno.test("Decoder / Optional property missing", () => { Deno.test("Decoder / Optional array property missing", () => { const input = ` - x:A a ldkit:Resource, x:Item . + x:A a ldkit:Resource . `; const schema = { - "@type": [x.Item], + "@type": [], optional: { "@id": x.optional, "@optional": true as const, @@ -174,7 +237,6 @@ Deno.test("Decoder / Optional array property missing", () => { const output = [ { $id: x.A, - $type: [x.Item], optional: [], }, ]; @@ -185,12 +247,12 @@ Deno.test("Decoder / Optional array property missing", () => { Deno.test("Decoder / Array simple property", () => { const input = ` x:A - a ldkit:Resource, x:Item ; + a ldkit:Resource ; x:array 1, 2, 3 . `; const schema = { - "@type": [x.Item], + "@type": [], array: { "@id": x.array, "@array": true as const, @@ -200,7 +262,6 @@ Deno.test("Decoder / Array simple property", () => { const output = [ { $id: x.A, - $type: [x.Item], array: [1, 2, 3], }, ]; @@ -211,22 +272,19 @@ Deno.test("Decoder / Array simple property", () => { Deno.test("Decoder / Array subschema property", () => { const input = ` x:A - a ldkit:Resource, x:Item ; + a ldkit:Resource ; x:array x:B, x:C . - x:B - a x:SubItem ; - x:value "value B" . - x:C a x:SubItem ; - x:value "value C" . + x:B x:value "value B" . + x:C x:value "value C" . `; const schema = { - "@type": [x.Item], + "@type": [], array: { "@id": x.array, "@array": true as const, "@context": { - "@type": [x.SubItem], + "@type": [], value: { "@id": x.value, }, @@ -237,16 +295,13 @@ Deno.test("Decoder / Array subschema property", () => { const output = [ { $id: x.A, - $type: [x.Item], array: [ { $id: x.B, - $type: [x.SubItem], value: "value B", }, { $id: x.C, - $type: [x.SubItem], value: "value C", }, ], @@ -259,12 +314,12 @@ Deno.test("Decoder / Array subschema property", () => { Deno.test("Decoder / Multilang property", () => { const input = ` x:A - a ldkit:Resource, x:Item ; + a ldkit:Resource ; x:multilang "CS"@cs, "EN"@en, "Unknown" . `; const schema = { - "@type": [x.Item], + "@type": [], multilang: { "@id": x.multilang, "@multilang": true as const, @@ -274,7 +329,6 @@ Deno.test("Decoder / Multilang property", () => { const output = [ { $id: x.A, - $type: [x.Item], multilang: { cs: "CS", en: "EN", @@ -289,12 +343,12 @@ Deno.test("Decoder / Multilang property", () => { Deno.test("Decoder / Multilang array property", () => { const input = ` x:A - a ldkit:Resource, x:Item ; + a ldkit:Resource; x:multilang "CS 1"@cs, "CS 2"@cs, "CS 3"@cs, "EN"@en, "Unknown" . `; const schema = { - "@type": [x.Item], + "@type": [], multilang: { "@id": x.multilang, "@multilang": true as const, @@ -305,7 +359,6 @@ Deno.test("Decoder / Multilang array property", () => { const output = [ { $id: x.A, - $type: [x.Item], multilang: { cs: ["CS 1", "CS 2", "CS 3"], en: ["EN"], @@ -320,12 +373,12 @@ Deno.test("Decoder / Multilang array property", () => { Deno.test("Decoder / Preferred language property", () => { const input = ` x:A - a ldkit:Resource, x:Item ; + a ldkit:Resource ; x:preferredLanguage "DE"@de, "CS"@cs, "EN"@en . `; const schema = { - "@type": [x.Item], + "@type": [], preferredLanguage: { "@id": x.preferredLanguage, }, @@ -334,7 +387,6 @@ Deno.test("Decoder / Preferred language property", () => { const output = [ { $id: x.A, - $type: [x.Item], preferredLanguage: "CS", }, ]; @@ -345,12 +397,12 @@ Deno.test("Decoder / Preferred language property", () => { Deno.test("Decoder / Preferred first property", () => { const input = ` x:A - a ldkit:Resource, x:Item ; + a ldkit:Resource ; x:preferredFirst "DE"@de, "CS"@cs, "EN"@en . `; const schema = { - "@type": [x.Item], + "@type": [], preferredFirst: { "@id": x.preferredFirst, }, @@ -359,7 +411,6 @@ Deno.test("Decoder / Preferred first property", () => { const output = [ { $id: x.A, - $type: [x.Item], preferredFirst: "DE", }, ]; @@ -370,14 +421,14 @@ Deno.test("Decoder / Preferred first property", () => { Deno.test("Decoder / One resource multiple schemas", () => { const input = ` x:A - a ldkit:Resource, x:Item ; + a ldkit:Resource ; x:nested x:A ; x:rootProperty "Root property" ; x:nestedProperty "Nested property" . `; const schema = { - "@type": [x.Item], + "@type": [], rootProperty: { "@id": x.rootProperty, }, @@ -395,11 +446,9 @@ Deno.test("Decoder / One resource multiple schemas", () => { const output = [ { $id: x.A, - $type: [x.Item], rootProperty: "Root property", nested: { $id: x.A, - $type: [x.Item], nestedProperty: "Nested property", }, }, @@ -411,17 +460,17 @@ Deno.test("Decoder / One resource multiple schemas", () => { Deno.test("Decoder / Caching", () => { const input = ` x:A - a ldkit:Resource, x:Item ; + a ldkit:Resource ; x:nested x:C . x:B - a ldkit:Resource, x:Item ; + a ldkit:Resource ; x:nested x:C . x:C a x:Nested . `; const schema = { - "@type": [x.Item], + "@type": [], nested: { "@id": x.nested, "@context": { @@ -433,18 +482,14 @@ Deno.test("Decoder / Caching", () => { const output = [ { $id: x.A, - $type: [x.Item], nested: { $id: x.C, - $type: [x.Nested], }, }, { $id: x.B, - $type: [x.Item], nested: { $id: x.C, - $type: [x.Nested], }, }, ]; diff --git a/tests/lens.test.ts b/tests/lens.test.ts index cc84679..c0d68c9 100644 --- a/tests/lens.test.ts +++ b/tests/lens.test.ts @@ -73,7 +73,6 @@ const defaultStoreContent = ttl(` const createDirector = ($id: string, name: string) => ({ $id: x[$id], - $type: [x.Director], name, }); @@ -239,7 +238,6 @@ Deno.test("Resource / Insert data", async () => { const result = await directors.findByIri(x.ChristopherNolan); assertEquals(result, { $id: x.ChristopherNolan, - $type: [x.Director], name: "Christopher Nolan", }); }); @@ -248,7 +246,6 @@ Deno.test("Resource / Delete data", async () => { const { directors } = init(); await directors.insert({ $id: x.ChristopherNolan, - $type: [x.Director, x.CustomType], name: "Christopher Nolan", }); await directors.deleteData( @@ -261,21 +258,6 @@ Deno.test("Resource / Delete data", async () => { const result = await directors.findByIri(x.ChristopherNolan); assertEquals(result, { $id: x.ChristopherNolan, - $type: [x.Director], name: "Christopher Nolan", }); }); - -// TODO Review and fix this test -/*Deno.test("Resource / Support for custom types", async () => { - const { movies } = init(); - await movies.insert({ - $id: x.KillBill, - $type: [x.TarantinoMovie], - name: "Kill Bill", - director: { $id: x.QuentinTarantino }, - }); - const result = await movies.findByIri(x.KillBill); - - assertEquals(result?.$type, [x.Movie, x.TarantinoMovie]); -});*/ diff --git a/tests/schema.test.ts b/tests/schema.test.ts index 5e8d1e3..e6732f8 100644 --- a/tests/schema.test.ts +++ b/tests/schema.test.ts @@ -1,16 +1,18 @@ -import { assertEquals, assertTypeSafe } from "./test_deps.ts"; +import { assertEquals, assertThrows, assertTypeSafe } from "./test_deps.ts"; import { Equals, x } from "./test_utils.ts"; import { expandSchema, - Schema, + Property, + type Schema, type SchemaInterface, + type SchemaPrototype, } from "../library/schema/mod.ts"; import { xsd } from "../library/namespaces/mod.ts"; +import rdf from "../library/namespaces/rdf.ts"; type ThingType = { $id: string; - $type: string[]; required: string; optional: string | undefined; array: string[]; @@ -21,7 +23,6 @@ type ThingType = { date: Date; nested: { $id: string; - $type: string[]; nestedValue: string; }; }; @@ -127,3 +128,28 @@ Deno.test("Schema / accepts schema prototype as schema interface creates schema assertEquals(expandedSchema, ThingSchema); }); + +Deno.test("Schema / should have at least one property or @type restriction", () => { + assertThrows(() => { + expandSchema(undefined as unknown as SchemaPrototype); + }); + assertThrows(() => { + expandSchema({} as unknown as SchemaPrototype); + }); +}); + +Deno.test("Schema / should expand @type shortcut definition", () => { + const schema = { + "type": "@type", + }; + const expandedSchema = expandSchema(schema); + assertEquals((expandedSchema["type"] as Property)["@id"], rdf.type); +}); + +Deno.test("Schema / should expand @type property definition", () => { + const schema = { + "type": { "@id": "@type" }, + }; + const expandedSchema = expandSchema(schema); + assertEquals((expandedSchema["type"] as Property)["@id"], rdf.type); +});