diff --git a/docs/table-of-contents.json b/docs/table-of-contents.json index 464fbdc..5469051 100644 --- a/docs/table-of-contents.json +++ b/docs/table-of-contents.json @@ -23,6 +23,7 @@ "v2": { "title": "Version 2 (not yet released)", "pages": [ + ["filtering", "Filtering"], ["pagination", "Pagination"], ["working-with-arrays", "Working with arrays"], ["inverse-properties", "Inverse properties"] diff --git a/docs/v2/filtering.md b/docs/v2/filtering.md new file mode 100644 index 0000000..5bf225c --- /dev/null +++ b/docs/v2/filtering.md @@ -0,0 +1,88 @@ +# Filtering + +LDkit comes with powerful search and filtering capabilities, enabling users to +narrow data results and explore large datasets. The feature integrates +seamlessly with the existing [Lens](../components/lens) `find` method, allowing +for controlled data retrieval. + +LDkit allows various search and filtering operations like `$equals`, `$not`, +`$contains`, `$strStarts`, `$strEnds`, `$gt`, `$lt`, `$gte`, `$lte`, `$regex`, +`$langMatches`, and `$filter`. Each is illustrated below with examples. + +### Simple example + +```ts +import { createLens } from "ldkit"; +import { schema, xsd } from "ldkit/namespaces"; + +// Create a schema +const PersonSchema = { + "@type": schema.Person, + name: schema.name, + birthDate: { + "@id": schema.birthDate, + "@type": xsd.date, + }, +} as const; + +// Create a lens instance +const Persons = createLens(PersonSchema); + +await Persons.find({ + where: { + name: "Ada Lovelace", + }, +}); // Returns list of all persons named "Ada Lovelace" +``` + +### Comparison operators + +```typescript +await Persons.find({ + where: { + name: { + $equals: "Ada Lovelace", // FILTER (?value = "Ada Lovelace") + $not: "Alan Turing", // FILTER (?value != "Alan Turing") + }, + birthDate: { + $lt: new Date("01-01-1900"), // FILTER (?value < "01-01-1900"@xsd:date) + $lte: new Date("01-01-1900"), // FILTER (?value <= "01-01-1900"@xsd:date) + $gt: new Date("01-01-1900"), // FILTER (?value > "01-01-1900"@xsd:date) + $gte: new Date("01-01-1900"), // FILTER (?value >= "01-01-1900"@xsd:date) + }, + }, +}); +``` + +### String functions + +```typescript +await Persons.find({ + where: { + name: { + $contains: "Ada", // FILTER CONTAINS(?value, "Ada") + $strStarts: "Ada", // FILTER STRSTARTS(?value, "Ada") + $strEnds: "Lovelace", // FILTER STRENDS(?value, "Lovelace") + $langMatches: "fr", // FILTER LANGMATCHES(LANG(?value), "fr") + $regex: "^A(.*)e$", // FILTER REGEX(?value, "^A(.*)e$") + }, + }, +}); +``` + +### Custom filtering + +On top of the above, it is possible to specify a custom filter function using +the SPARQL filtering syntax. There is a special placeholder `?value` used for +the value of the property to be filtered. This placeholder is replaced by actual +variable name during runtime. + +```typescript +await Persons.find({ + where: { + name: { + $filter: "STRLEN(?value) > 10", // FILTER (STRLEN(?value) > 10) + }, + }, +}); +``` diff --git a/library/engine/query_engine_proxy.ts b/library/engine/query_engine_proxy.ts index 8b410c7..9189b01 100644 --- a/library/engine/query_engine_proxy.ts +++ b/library/engine/query_engine_proxy.ts @@ -1,4 +1,10 @@ -import { type Context, IQueryEngine, quadsToGraph, type RDF } from "../rdf.ts"; +import { + type Context, + IQueryEngine, + N3, + quadsToGraph, + type RDF, +} from "../rdf.ts"; import { resolveContext, resolveEngine } from "../global.ts"; import { type AsyncIterator } from "../asynciterator.ts"; @@ -29,7 +35,8 @@ export class QueryEngineProxy { this.context, ) as unknown as AsyncIterator; const quads = await (quadStream.toArray()); - return quadsToGraph(quads); + const store = new N3.Store(quads); + return quadsToGraph(store); } queryVoid(query: string) { diff --git a/library/lens/lens.ts b/library/lens/lens.ts index cb7bebc..d4672ff 100644 --- a/library/lens/lens.ts +++ b/library/lens/lens.ts @@ -6,6 +6,7 @@ import { type SchemaInterface, type SchemaInterfaceIdentity, type SchemaPrototype, + type SchemaSearchInterface, type SchemaUpdateInterface, } from "../schema/mod.ts"; import { decode } from "../decoder.ts"; @@ -44,6 +45,7 @@ export class Lens< S extends SchemaPrototype, I = SchemaInterface, U = SchemaUpdateInterface, + X = SchemaSearchInterface, > { private readonly schema: Schema; private readonly context: Context; @@ -72,19 +74,25 @@ export class Lens< async query(sparqlConstructQuery: string) { const graph = await this.engine.queryGraph(sparqlConstructQuery); + console.log("GRAPH", graph); return this.decode(graph); } async find( - options: { where?: string | RDF.Quad[]; take?: number; skip?: number } = {}, + options: { where?: X | string | RDF.Quad[]; take?: number; skip?: number } = + {}, ) { const { where, take, skip } = { take: 1000, skip: 0, ...options, }; - const q = this.queryBuilder.getQuery(where, take, skip); - // TODO: console.log(q); + const isRegularQuery = typeof where === "string" || + typeof where === "undefined" || Array.isArray(where); + + const q = isRegularQuery + ? this.queryBuilder.getQuery(where, take, skip) + : this.queryBuilder.getSearchQuery(where ?? {}, take, skip); const graph = await this.engine.queryGraph(q); return this.decode(graph); } diff --git a/library/lens/query_builder.ts b/library/lens/query_builder.ts index ba1edf8..7104834 100644 --- a/library/lens/query_builder.ts +++ b/library/lens/query_builder.ts @@ -1,11 +1,16 @@ -import { type Schema } from "../schema/mod.ts"; -import { getSchemaProperties } from "../schema/mod.ts"; +import { + getSchemaProperties, + type Property, + type Schema, + type SearchSchema, +} from "../schema/mod.ts"; import { CONSTRUCT, DELETE, INSERT, SELECT, sparql as $, + type SparqlValue, } from "../sparql/mod.ts"; import { type Context, DataFactory, type Iri, type RDF } from "../rdf.ts"; import ldkit from "../namespaces/ldkit.ts"; @@ -15,6 +20,7 @@ import { encode } from "../encoder.ts"; import { type Entity } from "./types.ts"; import { UpdateHelper } from "./update_helper.ts"; +import { SearchHelper } from "./search_helper.ts"; enum Flags { None = 0, @@ -52,16 +58,33 @@ export class QueryBuilder { return ([] as RDF.Quad[]).concat(...quadArrays); } - private getShape(flags: Flags) { + private getShape(flags: Flags, searchSchema?: SearchSchema) { const includeOptional = (flags & Flags.ExcludeOptional) === 0; const wrapOptional = (flags & Flags.UnwrapOptional) === 0; const omitRootTypes = (flags & Flags.IncludeTypes) === 0; const ignoreInverse = (flags & Flags.IgnoreInverse) === Flags.IgnoreInverse; const mainVar = "iri"; - const conditions: (RDF.Quad | ReturnType)[] = []; + const conditions: SparqlValue[] = []; + + const populateSearchConditions = ( + property: Property, + varName: string, + search?: SearchSchema, + ) => { + if (search === undefined) { + return; + } + const helper = new SearchHelper(property, varName, search); + helper.process(); + conditions.push(helper.sparqlValues); + }; - const populateConditionsRecursive = (s: Schema, varPrefix: string) => { + const populateConditionsRecursive = ( + s: Schema, + varPrefix: string, + search?: SearchSchema, + ) => { const rdfType = s["@type"]; const properties = getSchemaProperties(s); @@ -80,7 +103,8 @@ export class QueryBuilder { Object.keys(properties).forEach((prop, index) => { const property = properties[prop]; const isOptional = property["@optional"]; - if (!includeOptional && isOptional) { + const propertySchema = search?.[prop] as SearchSchema | undefined; + if (!includeOptional && isOptional && propertySchema === undefined) { return; } if (wrapOptional && isOptional) { @@ -95,6 +119,11 @@ export class QueryBuilder { this.df.variable!(`${varPrefix}_${index}`), ), ); + populateSearchConditions( + property, + `${varPrefix}_${index}`, + propertySchema, + ); } else { conditions.push( this.df.quad( @@ -108,6 +137,7 @@ export class QueryBuilder { populateConditionsRecursive( property["@context"] as Schema, `${varPrefix}_${index}`, + propertySchema, ); } if (wrapOptional && isOptional) { @@ -116,7 +146,7 @@ export class QueryBuilder { }); }; - populateConditionsRecursive(this.schema, mainVar); + populateConditionsRecursive(this.schema, mainVar, searchSchema); return conditions; } @@ -146,6 +176,26 @@ export class QueryBuilder { return query; } + getSearchQuery(where: SearchSchema, limit: number, offset: number) { + const selectSubQuery = SELECT.DISTINCT` + ${this.df.variable!("iri")} + `.WHERE` + ${this.getShape(Flags.ExcludeOptional | Flags.IncludeTypes, where)} + `.LIMIT(limit).OFFSET(offset).build(); + + const query = CONSTRUCT` + ${this.getResourceSignature()} + ${this.getShape(Flags.UnwrapOptional | Flags.IgnoreInverse)} + `.WHERE` + { + ${selectSubQuery} + } + ${this.getShape(Flags.None, where)} + `.build(); + + return query; + } + getByIrisQuery(iris: Iri[]) { const query = CONSTRUCT` ${this.getResourceSignature()} diff --git a/library/lens/search_helper.ts b/library/lens/search_helper.ts new file mode 100644 index 0000000..b6879e7 --- /dev/null +++ b/library/lens/search_helper.ts @@ -0,0 +1,133 @@ +import { DataFactory, toRdf } from "../rdf.ts"; +import { sparql as $, type SparqlValue } from "../sparql/mod.ts"; +import { type Property, type SearchSchema } from "../schema/mod.ts"; +import xsd from "../namespaces/xsd.ts"; + +export class SearchHelper { + private readonly property: Property; + private readonly propertyType: string; + private readonly varName: string; + private readonly searchSchema: SearchSchema; + + private df: DataFactory = new DataFactory({ + blankNodePrefix: "b", + }); + + public readonly sparqlValues: SparqlValue[] = []; + + constructor( + property: Property, + varName: string, + searchSchema: SearchSchema, + ) { + this.property = property; + this.propertyType = property["@type"] ? property["@type"] : xsd.string; + this.varName = varName; + this.searchSchema = this.isPlainObject(searchSchema) + ? searchSchema + : { $equals: searchSchema }; + } + + public process() { + this.processOperators(); + this.processStringFunctions(); + this.processRegex(); + this.processLangMatches(); + this.processFilter(); + } + + private processOperators() { + const map = { + $equals: "=", + $not: "!=", + $gt: ">", + $gte: ">=", + $lt: "<", + $lte: "<=", + }; + + for (const [key, operator] of Object.entries(map)) { + const value = this.searchSchema[key]; + if (value === undefined) { + continue; + } + + this.addFilter( + $`${this.df.variable(this.varName)} ${operator} ${this.encode(value)}`, + ); + } + } + + private processStringFunctions() { + const map = { + $contains: "CONTAINS", + $strStarts: "STRSTARTS", + $strEnds: "STRENDS", + }; + + for (const [key, func] of Object.entries(map)) { + const value = this.searchSchema[key]; + if (value === undefined) { + continue; + } + + this.addFilter( + $`${func}(${this.df.variable(this.varName)}, ${this.encode(value)})`, + ); + } + } + + private processRegex() { + const value = this.searchSchema.$regex; + if (value === undefined) { + return; + } + + this.addFilter( + $`REGEX(${this.df.variable(this.varName)}, "${value as string}")`, + ); + } + + private processLangMatches() { + const value = this.searchSchema.$langMatches; + if (value === undefined) { + return; + } + + this.addFilter( + $`LANGMATCHES(LANG(${ + this.df.variable(this.varName) + }), "${value as string}")`, + ); + } + + private processFilter() { + const value = this.searchSchema.$filter; + if (value === undefined) { + return; + } + const stringified = $`${value as SparqlValue}`; + this.addFilter(stringified.replace("?value", `?${this.varName}`)); + } + + private addFilter(filter: SparqlValue) { + this.sparqlValues.push($`FILTER (${filter}) .`); + } + + private encode(value: unknown) { + return toRdf(value, { + datatype: this.df.namedNode(this.propertyType), + }); + } + + private isPlainObject(value: unknown) { + if (typeof value !== "object" || value === null) { + return false; + } + + const prototype = Object.getPrototypeOf(value); + return (prototype === null || prototype === Object.prototype || + Object.getPrototypeOf(prototype) === null) && + !(Symbol.toStringTag in value) && !(Symbol.iterator in value); + } +} diff --git a/library/rdf.ts b/library/rdf.ts index a90cf63..a7bd9b9 100644 --- a/library/rdf.ts +++ b/library/rdf.ts @@ -38,7 +38,7 @@ export type Node = Map; export type Graph = Map; -export const quadsToGraph = (quads: RDF.Quad[]) => { +export const quadsToGraph = (quads: Iterable) => { const graph: Graph = new Map(); for (const quad of quads) { const s = quad.subject.value; diff --git a/library/schema/interface.ts b/library/schema/interface.ts index e6277f3..f04ef07 100644 --- a/library/schema/interface.ts +++ b/library/schema/interface.ts @@ -1,5 +1,6 @@ import type { SupportedDataTypes } from "./data_types.ts"; import type { PropertyPrototype, SchemaPrototype } from "./schema.ts"; +import type { SearchFilters } from "./search.ts"; type Unite = T extends Record ? { [Key in keyof T]: T[Key] } : T; @@ -19,6 +20,11 @@ type IsMultilang = Property extends { } ? true : false; +type IsInverse = Property extends { + "@inverse": true; +} ? true + : false; + type ValidPropertyDefinition = PropertyPrototype | string; type ConvertPropertyType = T extends @@ -107,3 +113,26 @@ export type SchemaUpdateInterface = ? ConvertUpdateProperty : never; }; + +type ConvertSearchPropertyContext = T extends + { "@context": SchemaPrototype } ? Unite> + : IsInverse extends true ? never + : SearchFilters>; + +type ConvertSearchProperty = T extends + PropertyPrototype ? ConvertSearchPropertyContext + : SearchFilters; + +type InversePropertiesMap = { + [X in keyof T]: T[X] extends { "@inverse": true } ? X : never; +}; + +type InverseProperties = InversePropertiesMap< + T +>[keyof InversePropertiesMap]; + +export type SchemaSearchInterface = { + [X in Exclude>]?: T[X] extends + ValidPropertyDefinition ? ConvertSearchProperty + : never; +}; diff --git a/library/schema/mod.ts b/library/schema/mod.ts index 8385968..98bb914 100644 --- a/library/schema/mod.ts +++ b/library/schema/mod.ts @@ -4,6 +4,7 @@ export type { SchemaInterface, SchemaInterfaceIdentity, SchemaInterfaceType, + SchemaSearchInterface, SchemaUpdateInterface, } from "./interface.ts"; @@ -14,4 +15,6 @@ export type { SchemaPrototype, } from "./schema.ts"; +export type { SearchFilters, SearchSchema } from "./search.ts"; + export { expandSchema, getSchemaProperties } from "./utils.ts"; diff --git a/library/schema/search.ts b/library/schema/search.ts new file mode 100644 index 0000000..d66009b --- /dev/null +++ b/library/schema/search.ts @@ -0,0 +1,20 @@ +import { SparqlValue } from "../sparql/mod.ts"; + +export type SearchFilters = T | { + $equals?: T; + $not?: T; + $contains?: T; + $strStarts?: T; + $strEnds?: T; + $gt?: T; + $gte?: T; + $lt?: T; + $lte?: T; + $regex?: string; + $langMatches?: string; + $filter?: SparqlValue; +}; + +export type SearchSchema = { + [key: string]: SearchFilters | SearchSchema; +}; diff --git a/tests/e2e/search.test.ts b/tests/e2e/search.test.ts new file mode 100644 index 0000000..f4ae1f8 --- /dev/null +++ b/tests/e2e/search.test.ts @@ -0,0 +1,221 @@ +import { assertEquals, Comunica } from "../test_deps.ts"; + +import { initStore, ttl, x } from "../test_utils.ts"; + +import { createLens } from "ldkit"; +import { xsd } from "ldkit/namespaces"; + +const engine = new Comunica(); + +const Director = { + name: x.name, + birthDate: { + "@id": x.birthDate, + "@type": xsd.date, + }, + movies: { + "@id": x.movie, + "@array": true, + }, +} as const; + +const defaultStoreContent = ttl(` + x:QuentinTarantino + x:name "Quentin Tarantino" ; + x:birthDate "1963-03-27"^^xsd:date ; + x:movie "Pulp Fiction", "Reservoir Dogs" . + x:StanleyKubrick + x:name "Stanley Kubrick" ; + x:birthDate "1928-07-26"^^xsd:date ; + x:movie "The Shining", "A Clockwork Orange" . + x:StevenSpielberg + x:name "Steven Spielberg" ; + x:birthDate "1946-12-18"^^xsd:date ; + x:movie "Jurassic Park", "Jaws", "Schindler's List" . + `); + +const init = () => { + const { store, context, assertStore, empty } = initStore(); + store.addQuads(defaultStoreContent); + const Directors = createLens(Director, context, engine); + return { Directors, assertStore, empty, store }; +}; + +Deno.test("E2E / Search / $equals", async () => { + const { Directors } = init(); + + const results = await Directors.find({ + where: { name: "Quentin Tarantino" }, + }); + + const resultsFull = await Directors.find({ + where: { + name: { + $equals: "Quentin Tarantino", + }, + }, + }); + + assertEquals(results.length, 1); + assertEquals(results[0].name, "Quentin Tarantino"); + assertEquals(resultsFull.length, 1); + assertEquals(results[0], resultsFull[0]); +}); + +Deno.test("E2E / Search / $not", async () => { + const { Directors } = init(); + + const results = await Directors.find({ + where: { + name: { + $not: "Quentin Tarantino", + }, + }, + }); + + assertEquals(results.length, 2); + assertEquals(results[0].name, "Stanley Kubrick"); + assertEquals(results[1].name, "Steven Spielberg"); +}); + +Deno.test("E2E / Search / $contains", async () => { + const { Directors } = init(); + + const results = await Directors.find({ + where: { + name: { + $contains: "Quentin", + }, + }, + }); + + assertEquals(results.length, 1); + assertEquals(results[0].name, "Quentin Tarantino"); +}); + +Deno.test("E2E / Search / $strStarts", async () => { + const { Directors } = init(); + + const results = await Directors.find({ + where: { + name: { + $strStarts: "Quentin", + }, + }, + }); + + assertEquals(results.length, 1); + assertEquals(results[0].name, "Quentin Tarantino"); +}); + +Deno.test("E2E / Search / $strEnds", async () => { + const { Directors } = init(); + + const results = await Directors.find({ + where: { + name: { + $strEnds: "Tarantino", + }, + }, + }); + + assertEquals(results.length, 1); + assertEquals(results[0].name, "Quentin Tarantino"); +}); + +Deno.test("E2E / Search / $gt $lt", async () => { + const { Directors } = init(); + + const results = await Directors.find({ + where: { + birthDate: { + $gt: new Date("1928-07-26"), + $lt: new Date("1963-03-27"), + }, + }, + }); + + assertEquals(results.length, 1); + assertEquals(results[0].name, "Steven Spielberg"); +}); + +Deno.test("E2E / Search / $gte $lte", async () => { + const { Directors } = init(); + + const results = await Directors.find({ + where: { + birthDate: { + $gte: new Date("1928-07-26"), + $lte: new Date("1963-03-27"), + }, + }, + }); + + assertEquals(results.length, 3); +}); + +Deno.test("E2E / Search / $regex", async () => { + const { Directors } = init(); + + const results = await Directors.find({ + where: { + name: { + $regex: "^Q(.*)o$", + }, + }, + }); + + assertEquals(results.length, 1); + assertEquals(results[0].name, "Quentin Tarantino"); +}); + +Deno.test("E2E / Search / $langMatches", async () => { + const { Directors, store, empty } = init(); + + await empty(); + + const localizedStoreContent = ttl(` + x:QuentinTarantino + x:name "Quentin Tarantino"@en, "クエンティン・タランティーノ"@jp, "昆汀·塔伦蒂诺"@zh ; + x:birthDate "1963-03-27"^^xsd:date ; + x:movie "Pulp Fiction", "Reservoir Dogs" . + x:StanleyKubrick + x:name "Stanley Kubrick"@en, "スタンリー・キューブリック"@jp, "斯坦利·库布里克"@zh ; + x:birthDate "1928-07-26"^^xsd:date ; + x:movie "The Shining", "A Clockwork Orange" . + x:StevenSpielberg + x:name "Steven Spielberg"@en, "スティーヴン・スピルバーグ"@jp, "史蒂文·斯皮尔伯格"@zh ; + x:birthDate "1946-12-18"^^xsd:date ; + x:movie "Jurassic Park", "Jaws", "Schindler's List" . + `); + + store.addQuads(localizedStoreContent); + + const results = await Directors.find({ + where: { + name: { + $langMatches: "jp", + }, + }, + }); + + assertEquals(results.length, 3); + assertEquals(results[0].name, "クエンティン・タランティーノ"); + assertEquals(results[1].name, "スタンリー・キューブリック"); + assertEquals(results[2].name, "スティーヴン・スピルバーグ"); +}); + +Deno.test("E2E / Search / $filter", async () => { + const { Directors } = init(); + + const results = await Directors.find({ + where: { + name: { + $filter: "strlen(?value) = 17", + }, + }, + }); + + assertEquals(results.length, 1); + assertEquals(results[0].name, "Quentin Tarantino"); +}); diff --git a/tests/schema.test.ts b/tests/schema.test.ts index 390c6f0..d4a9c4b 100644 --- a/tests/schema.test.ts +++ b/tests/schema.test.ts @@ -12,9 +12,11 @@ import { type Schema, type SchemaInterface, type SchemaPrototype, + type SchemaSearchInterface, type SchemaUpdateInterface, } from "../library/schema/mod.ts"; import { rdf, xsd } from "../namespaces.ts"; +import { type SparqlValue } from "../library/sparql/mod.ts"; type ArrayUpdate = { $set?: T[]; @@ -22,6 +24,21 @@ type ArrayUpdate = { $remove?: T[]; } | T[]; +type PropertySearch = T | { + $equals?: T; + $not?: T; + $contains?: T; + $strStarts?: T; + $strEnds?: T; + $gt?: T; + $gte?: T; + $lt?: T; + $lte?: T; + $regex?: string; + $langMatches?: string; + $filter?: SparqlValue; +}; + Deno.test("Schema / Full schema", () => { type ThingType = { $id: string; @@ -55,6 +72,20 @@ Deno.test("Schema / Full schema", () => { }; }; + type ThingSearchType = { + required?: PropertySearch; + optional?: PropertySearch; + array?: PropertySearch; + multilang?: PropertySearch; + multilangArray?: PropertySearch; + number?: PropertySearch; + boolean?: PropertySearch; + date?: PropertySearch; + nested?: { + nestedValue?: PropertySearch; + }; + }; + const Thing = { "@type": x.X, required: x.required, @@ -150,9 +181,11 @@ Deno.test("Schema / Full schema", () => { type I = SchemaInterface; type U = SchemaUpdateInterface; + type S = SchemaSearchInterface; assertTypeSafe>(); assertTypeSafe>(); + assertTypeSafe>(); assertEquals(expandedSchema, ThingSchema); }); @@ -196,6 +229,14 @@ Deno.test("Schema / Basic datatypes", () => { date?: Date; }; + type PrototypeSearchInterface = { + default?: PropertySearch; + string?: PropertySearch; + number?: PropertySearch; + boolean?: PropertySearch; + date?: PropertySearch; + }; + const PrototypeSchema: Schema = { "@type": [], default: { @@ -222,9 +263,11 @@ Deno.test("Schema / Basic datatypes", () => { type I = SchemaInterface; type U = SchemaUpdateInterface; + type S = SchemaSearchInterface; assertTypeSafe>(); assertTypeSafe>(); + assertTypeSafe>(); assertEquals(expandSchema(Prototype), PrototypeSchema); }); @@ -247,6 +290,10 @@ Deno.test("Schema / Optional", () => { optional?: string | null; }; + type PrototypeSearchInterface = { + optional?: PropertySearch; + }; + const PrototypeSchema: Schema = { "@type": [], optional: { @@ -258,9 +305,11 @@ Deno.test("Schema / Optional", () => { type I = SchemaInterface; type U = SchemaUpdateInterface; + type S = SchemaSearchInterface; assertTypeSafe>(); assertTypeSafe>(); + assertTypeSafe>(); assertEquals(expandSchema(Prototype), PrototypeSchema); }); @@ -290,6 +339,11 @@ Deno.test("Schema / Array", () => { optionalArray?: ArrayUpdate; }; + type PrototypeSearchInterface = { + array?: PropertySearch; + optionalArray?: PropertySearch; + }; + const PrototypeSchema: Schema = { "@type": [], array: { @@ -307,9 +361,11 @@ Deno.test("Schema / Array", () => { type I = SchemaInterface; type U = SchemaUpdateInterface; + type S = SchemaSearchInterface; assertTypeSafe>(); assertTypeSafe>(); + assertTypeSafe>(); assertEquals(expandSchema(Prototype), PrototypeSchema); }); @@ -339,6 +395,11 @@ Deno.test("Schema / Multilang", () => { multilangArray?: Record>; }; + type PrototypeSearchInterface = { + multilang?: PropertySearch; + multilangArray?: PropertySearch; + }; + const PrototypeSchema: Schema = { "@type": [], multilang: { @@ -356,9 +417,11 @@ Deno.test("Schema / Multilang", () => { type I = SchemaInterface; type U = SchemaUpdateInterface; + type S = SchemaSearchInterface; assertTypeSafe>(); assertTypeSafe>(); + assertTypeSafe>(); assertEquals(expandSchema(Prototype), PrototypeSchema); }); @@ -381,6 +444,9 @@ Deno.test("Schema / Inverse", () => { isPropertyOf?: string; }; + // deno-lint-ignore ban-types + type PrototypeSearchInterface = {}; + const PrototypeSchema: Schema = { "@type": [], isPropertyOf: { @@ -392,9 +458,11 @@ Deno.test("Schema / Inverse", () => { type I = SchemaInterface; type U = SchemaUpdateInterface; + type S = SchemaSearchInterface; assertTypeSafe>(); assertTypeSafe>(); + assertTypeSafe>(); assertEquals(expandSchema(Prototype), PrototypeSchema); }); @@ -426,6 +494,12 @@ Deno.test("Schema / Nested schema", () => { }; }; + type PrototypeSearchInterface = { + nested?: { + nestedValue?: PropertySearch; + }; + }; + const PrototypeSchema: Schema = { "@type": [], nested: { @@ -442,9 +516,11 @@ Deno.test("Schema / Nested schema", () => { type I = SchemaInterface; type U = SchemaUpdateInterface; + type S = SchemaSearchInterface; assertTypeSafe>(); assertTypeSafe>(); + assertTypeSafe>(); assertEquals(expandSchema(Prototype), PrototypeSchema); });