diff --git a/.vscode/import_map.json b/.vscode/import_map.json index 460bfb3..0e3a3f3 100644 --- a/.vscode/import_map.json +++ b/.vscode/import_map.json @@ -1,13 +1,13 @@ { "imports": { - "$fresh/": "https://deno.land/x/fresh@1.5.4/", - "preact": "https://esm.sh/preact@10.18.1", - "preact/": "https://esm.sh/preact@10.18.1/", - "preact-render-to-string": "https://esm.sh/*preact-render-to-string@6.2.2", + "$fresh/": "https://deno.land/x/fresh@1.6.1/", + "preact": "https://esm.sh/preact@10.19.2", + "preact/": "https://esm.sh/preact@10.19.2/", "@preact/signals": "https://esm.sh/*@preact/signals@1.2.1", "@preact/signals-core": "https://esm.sh/*@preact/signals-core@1.5.0", "twind": "https://esm.sh/twind@0.16.19", "twind/": "https://esm.sh/twind@0.16.19/", + "$doc_components/": "https://deno.land/x/deno_doc_components@0.4.14/", "$std/": "https://deno.land/std@0.207.0/", diff --git a/examples/basic/main.ts b/examples/basic/main.ts index 5c33675..63e4b3a 100644 --- a/examples/basic/main.ts +++ b/examples/basic/main.ts @@ -21,7 +21,7 @@ const PersonSchema = { // Create a resource using the data schema and context above const Persons = createLens(PersonSchema, options); -// List all persons +// List some persons const persons = await Persons.find({ take: 10 }); for (const person of persons) { console.log(person.name); // string diff --git a/library/decoder.ts b/library/decoder.ts index cc3b4db..dcaa03e 100644 --- a/library/decoder.ts +++ b/library/decoder.ts @@ -1,8 +1,8 @@ import type { Options } from "./options.ts"; import { fromRdf, Graph, Iri, Node, type RDF } from "./rdf.ts"; import type { ExpandedProperty, ExpandedSchema } from "./schema/mod.ts"; -import ldkit from "./namespaces/ldkit.ts"; -import rdf from "./namespaces/rdf.ts"; +import { ldkit } from "../namespaces/ldkit.ts"; +import { rdf } from "../namespaces/rdf.ts"; export const decode = ( graph: Graph, diff --git a/library/encoder.ts b/library/encoder.ts index 02da5b9..080f036 100644 --- a/library/encoder.ts +++ b/library/encoder.ts @@ -1,8 +1,8 @@ import type { Options } from "./options.ts"; import { DataFactory, type Iri, type RDF, toRdf } from "./rdf.ts"; import type { ExpandedProperty, ExpandedSchema } from "./schema/mod.ts"; -import xsd from "./namespaces/xsd.ts"; -import rdf from "./namespaces/rdf.ts"; +import { xsd } from "../namespaces/xsd.ts"; +import { rdf } from "../namespaces/rdf.ts"; type DecodedNode = Record; diff --git a/library/engine/mod.ts b/library/engine/mod.ts index e7e1fbf..21ebf56 100644 --- a/library/engine/mod.ts +++ b/library/engine/mod.ts @@ -1,2 +1,2 @@ -export { QueryEngine } from "./query_engine.ts"; -export { QueryEngineProxy } from "./query_engine_proxy.ts"; +export * from "./query_engine.ts"; +export * from "./types.ts"; diff --git a/library/engine/query_engine.ts b/library/engine/query_engine.ts index 31eab76..87749d8 100644 --- a/library/engine/query_engine.ts +++ b/library/engine/query_engine.ts @@ -1,12 +1,26 @@ -import { type IQueryEngine, type QueryContext, type RDF } from "../rdf.ts"; +import { type RDF } from "../rdf.ts"; + +import { type IQueryEngine, type QueryContext } from "./types.ts"; import { getResponseTypes, resolve, type ResolverType, } from "./query_resolvers.ts"; +/** + * A query engine that can query a SPARQL endpoint. + * + * Implements {@link IQueryEngine} interface. + * + * This engine is used by default if no other engine is configured. + * See {@link Options} for more details. + * + * If you need to query other data sources, or multiple SPARQL endpoints, + * you can use [Comunica](https://comunica.dev) instead, extend this engine, + * or implement your own. + */ export class QueryEngine implements IQueryEngine { - protected getSparqlEndpoint(context?: QueryContext) { + protected getSparqlEndpoint(context?: QueryContext): string { if (!context) { throw new Error( "No context supplied to QueryEngine. You need to create a default context or pass one to a resource.", @@ -40,11 +54,15 @@ export class QueryEngine implements IQueryEngine { ); } - protected getFetch(context?: QueryContext) { + protected getFetch(context?: QueryContext): typeof fetch { return context && context.fetch ? context.fetch : fetch; } - query(query: string, responseType: string, context?: QueryContext) { + protected query( + query: string, + responseType: string, + context?: QueryContext, + ): Promise { const endpoint = this.getSparqlEndpoint(context); const fetchFn = this.getFetch(context); return fetchFn(endpoint, { @@ -59,7 +77,7 @@ export class QueryEngine implements IQueryEngine { }); } - async queryAndResolve( + protected async queryAndResolve( type: T, query: string, context?: QueryContext, @@ -81,20 +99,41 @@ export class QueryEngine implements IQueryEngine { return resolve(type, response); } - queryBindings( + /** + * Executes a SPARQL SELECT query and returns a stream of bindings. + * + * @param query SPARQL query string + * @param context Engine context + * @returns Stream of bindings + */ + public queryBindings( query: string, context?: QueryContext, ): Promise> { return this.queryAndResolve("bindings", query, context); } - queryBoolean( + /** + * Executes a SPARQL ASK query and returns a boolean result. + * + * @param query SPARQL query string + * @param context Engine context + * @returns Boolean result + */ + public queryBoolean( query: string, context?: QueryContext, ): Promise { return this.queryAndResolve("boolean", query, context); } + /** + * Executes a SPARQL CONSTRUCT query and returns a stream of quads. + * + * @param query SPARQL query string + * @param context Engine context + * @returns Stream of quads + */ queryQuads( query: string, context?: QueryContext, @@ -102,6 +141,13 @@ export class QueryEngine implements IQueryEngine { return this.queryAndResolve("quads", query, context); } + /** + * Executes a SPARQL UPDATE query and returns nothing. + * + * @param query SPARQL query string + * @param context Engine context + * @returns Nothing + */ async queryVoid( query: string, context?: QueryContext, diff --git a/library/engine/query_engine_proxy.ts b/library/engine/query_engine_proxy.ts index e636d60..b9575fa 100644 --- a/library/engine/query_engine_proxy.ts +++ b/library/engine/query_engine_proxy.ts @@ -1,12 +1,8 @@ -import { - IQueryEngine, - N3, - quadsToGraph, - type QueryContext, - type RDF, -} from "../rdf.ts"; +import { N3, quadsToGraph, type RDF } from "../rdf.ts"; import { type AsyncIterator } from "../asynciterator.ts"; +import type { IQueryEngine, QueryContext } from "./types.ts"; + export class QueryEngineProxy { private readonly engine: IQueryEngine; private readonly context: QueryContext; diff --git a/library/engine/types.ts b/library/engine/types.ts new file mode 100644 index 0000000..3440e3b --- /dev/null +++ b/library/engine/types.ts @@ -0,0 +1,35 @@ +import type { + IDataSource, + IQueryContextCommon, +} from "npm:@comunica/types@2.6.8"; + +import { RDF } from "../rdf.ts"; + +/** + * A set of context entries that can be passed to a query engine, + * such as data sources, fetch configuration, etc. + * + * @example + * ```typescript + * import { QueryContext, QueryEngine } from "ldkit"; + * + * const context: QueryContext = { + * sources: ["https://dbpedia.org/sparql"], + * }; + * + * const engine = new QueryEngine(); + * await engine.queryBoolean("ASK { ?s ?p ?o }", context); + * ``` + */ +export type QueryContext = + & RDF.QueryStringContext + & RDF.QuerySourceContext + & IQueryContextCommon; + +/** + * Interface of a query engine compatible with LDkit + */ +export type IQueryEngine = RDF.StringSparqlQueryable< + RDF.SparqlResultSupport, + QueryContext +>; diff --git a/library/lens/lens.ts b/library/lens/lens.ts index 71d3583..4e115ca 100644 --- a/library/lens/lens.ts +++ b/library/lens/lens.ts @@ -20,25 +20,71 @@ import type { Entity, Unite } from "./types.ts"; import { QueryEngineProxy } from "../engine/query_engine_proxy.ts"; /** - * Lens lets you query and update RDF data via data schema using TypeScript native data types. + * Creates an instance of Lens that lets you query and update RDF data + * via data schema using TypeScript native data types. * - * https://ldkit.io/docs/components/lens + * In order to create a Lens instance, you need to provide a data schema + * that describes the data model which serves to translate data between + * Linked Data and TypeScript native types (see {@link Schema} for details). * - * @param schema data schema which extends `SchemaPrototype` - * @param options optional `Options` - contains LDkit and query engine configuration - * @returns Lens instance + * You can also pass a set of options for LDkit and a query engine that + * specify the data source, preferred language, etc. (see {@link Options} for details). + * + * @example + * ```typescript + * import { createLens, type Options } from "ldkit"; + * import { dbo, rdfs, xsd } from "ldkit/namespaces"; + * + * // Create options for query engine + * const options: Options = { + * sources: ["https://dbpedia.org/sparql"], // SPARQL endpoint + * language: "en", // Preferred language + * }; + * + * // Create a schema + * const PersonSchema = { + * "@type": dbo.Person, + * name: rdfs.label, + * abstract: dbo.abstract, + * birthDate: { + * "@id": dbo.birthDate, + * "@type": xsd.date, + * }, + * } as const; + * + * // Create a resource using the data schema and options above + * const Persons = createLens(PersonSchema, options); + * + * // List some persons + * const persons = await Persons.find({ take: 10 }); + * for (const person of persons) { + * console.log(person.name); // string + * console.log(person.birthDate); // Date + * } + * + * // Get a particular person identified by IRI + * const ada = await Persons.findByIri("http://dbpedia.org/resource/Ada_Lovelace"); + * console.log(ada?.name); // string "Ada Lovelace" + * console.log(ada?.birthDate); // Date object of 1815-12-10 + * ``` + * + * @param schema data schema which extends {@link Schema} + * @param options optional {@link Options} - contains LDkit and query engine configuration + * @returns Lens instance that provides interface to Linked Data based on the schema */ -export const createLens = ( +export function createLens( schema: T, - options: Options = {}, -) => new Lens(schema, options); - -export class Lens< - T extends Schema, - I = SchemaInterface, - U = SchemaUpdateInterface, - S = SchemaSearchInterface, -> { + options?: Options, +): Lens { + return new Lens(schema, options); +} + +/** + * Lens provides an interface to Linked Data based on the data schema. + * + * For the best developer experience, use the {@link createLens} function to create the instance. + */ +export class Lens { private readonly schema: ExpandedSchema; private readonly options: Options; private readonly engine: QueryEngineProxy; @@ -53,30 +99,126 @@ export class Lens< } private decode(graph: Graph) { - return decode(graph, this.schema, this.options) as unknown as Unite[]; + return decode(graph, this.schema, this.options) as unknown as Unite< + SchemaInterface + >[]; } private log(query: string) { this.options.logQuery!(query); } - async count() { + /** + * Returns the total number of entities corresponding to the data schema. + * + * @example + * ```typescript + * import { createLens } from "ldkit"; + * import { schema } from "ldkit/namespaces"; + * + * // Create a schema + * const PersonSchema = { + * "@type": schema.Person, + * name: schema.name, + * } as const; + * + * // Create a resource using the data schema above + * const Persons = createLens(PersonSchema); + * + * // Count all persons + * const count = await Persons.count(); // number + * ``` + * + * @returns total number of entities corresponding to the data schema + */ + async count(): Promise { const q = this.queryBuilder.countQuery(); this.log(q); const bindings = await this.engine.queryBindings(q); return parseInt(bindings[0].get("count")!.value); } - async query(sparqlConstructQuery: string) { + /** + * Find entities with a custom SPARQL query. + * + * The query must be a CONSTRUCT query, and the root nodes must be of type `ldkit:Resource`. + * So that the decoder can decode the results, the query must also return all properties + * according to the data schema. + * + * @example + * ```typescript + * import { createLens } from "ldkit"; + * import { ldkit, schema } from "ldkit/namespaces"; + * import { CONSTRUCT } from "ldkit/sparql"; + * + * // Create a schema + * const PersonSchema = { + * "@type": schema.Person, + * name: schema.name, + * } as const; + * + * // Create a resource using the data schema above + * const Persons = createLens(PersonSchema); + * + * // Query to find all persons named "Doe" + * const query = CONSTRUCT`?s a <${ldkit.Resource}>; <${schema.name}> ?name` + * .WHERE`?s <${schema.name}> ?name; <${schema.familyName}> "Doe"`.build(); + * + * // Find all persons that match the custom query + * const doePersons = await Persons.query(query); + * ``` + * + * @param sparqlConstructQuery CONSTRUCT SPARQL query + * @returns Found entities + */ + async query( + sparqlConstructQuery: string, + ): Promise>[]> { this.log(sparqlConstructQuery); const graph = await this.engine.queryGraph(sparqlConstructQuery); return this.decode(graph); } + /** + * Find entities that match the given search criteria. + * + * The search criteria is a JSON object that may contain properties from the data schema. + * In addition you can specify how many results to return and how many to skip + * for pagination purposes. + * + * @example + * ```typescript + * import { createLens } from "ldkit"; + * import { schema } from "ldkit/namespaces"; + * + * // Create a schema + * const PersonSchema = { + * "@type": schema.Person, + * name: schema.name, + * } as const; + * + * // Create a resource using the data schema above + * const Persons = createLens(PersonSchema); + * + * // Find 100 persons with name that starts with "Ada" + * const persons = await Persons.find({ + * where: { + * name: { $strStarts: "Ada" }, + * }, + * take: 100, + * }); + * ``` + * + * @param options Search criteria and pagination options + * @returns entities that match the given search criteria + */ async find( - options: { where?: S | string | RDF.Quad[]; take?: number; skip?: number } = - {}, - ) { + options: { + where?: SchemaSearchInterface | string | RDF.Quad[]; + take?: number; + skip?: number; + } = {}, + ): Promise>[]> { const { where, take, skip } = { take: this.options.take!, skip: 0, @@ -93,12 +235,66 @@ export class Lens< return this.decode(graph); } - async findByIri(iri: Iri) { + /** + * Find a single entity that matches the given IRI. + * + * @example + * ```typescript + * import { createLens } from "ldkit"; + * import { schema } from "ldkit/namespaces"; + * + * // Create a schema + * const PersonSchema = { + * "@type": schema.Person, + * name: schema.name, + * } as const; + * + * // Create a resource using the data schema above + * const Persons = createLens(PersonSchema); + * + * // Get a particular person identified by IRI + * const ada = await Persons.findByIri("http://dbpedia.org/resource/Ada_Lovelace"); + * console.log(ada?.name); // string "Ada Lovelace" + * ``` + * + * @param iri IRI of the entity to find + * @returns Entity if found, null otherwise + */ + async findByIri(iri: Iri): Promise> | null> { const results = await this.findByIris([iri]); return results.length > 0 ? results[0] : null; } - async findByIris(iris: Iri[]) { + /** + * Find entities that match the given IRIs. + * + * @example + * ```typescript + * import { createLens } from "ldkit"; + * import { schema } from "ldkit/namespaces"; + * + * // Create a schema + * const PersonSchema = { + * "@type": schema.Person, + * name: schema.name, + * } as const; + * + * // Create a resource using the data schema above + * const Persons = createLens(PersonSchema); + * + * // Get specific persons identified by IRIs + * const matches = await Persons.findByIris([ + * "http://dbpedia.org/resource/Ada_Lovelace", + * "http://dbpedia.org/resource/Alan_Turing" + * ]); + * console.log(matches[0].name); // string "Ada Lovelace" + * console.log(matches[1].name); // string "Alan Turing" + * ``` + * + * @param iris IRIs of the entities to find + * @returns Array of found entities, empty array if there are no matches + */ + async findByIris(iris: Iri[]): Promise>[]> { const q = this.queryBuilder.getByIrisQuery(iris); this.log(q); const graph = await this.engine.queryGraph(q); @@ -110,22 +306,143 @@ export class Lens< return this.engine.queryVoid(query); } - insert(...entities: Entity[]) { + /** + * Inserts one or more entities to the data store. + * + * @example + * ```typescript + * import { createLens } from "ldkit"; + * import { schema } from "ldkit/namespaces"; + * + * // Create a schema + * const PersonSchema = { + * "@type": schema.Person, + * name: schema.name, + * } as const; + * + * // Create a resource using the data schema above + * const Persons = createLens(PersonSchema); + * + * // Insert a new person + * await Persons.insert({ + * $id: "http://example.org/Alan_Turing", + * name: "Alan Turing", + * }); + * ``` + * + * @param entities Entities to insert + * @returns Nothing + */ + insert(...entities: Entity>[]): Promise { const q = this.queryBuilder.insertQuery(entities); return this.updateQuery(q); } - insertData(...quads: RDF.Quad[]) { + /** + * Inserts raw RDF quads to the data store. + * + * This method is useful when you need to insert data that is not covered by the data schema. + * + * @example + * ```typescript + * import { createLens } from "ldkit"; + * import { schema } from "ldkit/namespaces"; + * import { DataFactory } from "ldkit/rdf"; + * + * // Create a schema + * const PersonSchema = { + * "@type": schema.Person, + * name: schema.name, + * } as const; + * + * // Create a resource using the data schema above + * const Persons = createLens(PersonSchema); + * + * // Create a custom quad to insert + * const df = new DataFactory(); + * const quad = df.quad( + * df.namedNode("http://example.org/Alan_Turing"), + * df.namedNode("http://schema.org/name"), + * df.literal("Alan Turing"), + * ); + * + * // Insert the quad + * await Persons.insertData(quad); + * ``` + * + * @param quads Quads to insert to the data store + * @returns Nothing + */ + insertData(...quads: RDF.Quad[]): Promise { const q = this.queryBuilder.insertDataQuery(quads); return this.updateQuery(q); } - update(...entities: U[]) { + /** + * Updates one or more entities in the data store. + * + * @example + * ```typescript + * import { createLens } from "ldkit"; + * import { schema } from "ldkit/namespaces"; + * + * // Create a schema + * const PersonSchema = { + * "@type": schema.Person, + * name: schema.name, + * } as const; + * + * // Create a resource using the data schema above + * const Persons = createLens(PersonSchema); + * + * // Update Alan Turing's name + * await Persons.update({ + * $id: "http://example.org/Alan_Turing", + * name: "Not Alan Turing", + * }); + * ``` + * + * @param entities Partial entities to update + * @returns Nothing + */ + update(...entities: SchemaUpdateInterface[]): Promise { const q = this.queryBuilder.updateQuery(entities as Entity[]); return this.updateQuery(q); } - delete(...identities: Identity[] | Iri[]) { + /** + * Deletes one or more entities from the data store. + * + * This method accepts IRIs of the entities to delete and attemps + * to delete all triples from the database that corresponds to + * the data schema. Other triples that are not covered by the data + * schema will not be deleted. + * + * If you need to have more control of what triples to delete, + * use {@link deleteData} instead. + * + * @example + * ```typescript + * import { createLens } from "ldkit"; + * import { schema } from "ldkit/namespaces"; + * + * // Create a schema + * const PersonSchema = { + * "@type": schema.Person, + * name: schema.name, + * } as const; + * + * // Create a resource using the data schema above + * const Persons = createLens(PersonSchema); + * + * // Delete a person + * await Persons.delete("http://example.org/Alan_Turing"); + * ``` + * + * @param identities Identities or IRIs of the entities to delete + * @returns Nothing + */ + delete(...identities: Identity[] | Iri[]): Promise { const iris = identities.map((identity) => { return typeof identity === "string" ? identity : identity.$id; }); @@ -134,7 +451,42 @@ export class Lens< return this.updateQuery(q); } - deleteData(...quads: RDF.Quad[]) { + /** + * Deletes raw RDF quads from the data store. + * + * This method is useful when you need to delete data that is not covered by the data schema. + * + * @example + * ```typescript + * import { createLens } from "ldkit"; + * import { schema } from "ldkit/namespaces"; + * import { DataFactory } from "ldkit/rdf"; + * + * // Create a schema + * const PersonSchema = { + * "@type": schema.Person, + * name: schema.name, + * } as const; + * + * // Create a resource using the data schema and context above + * const Persons = createLens(PersonSchema); + * + * // Create a custom quad to insert + * const df = new DataFactory(); + * const quad = df.quad( + * df.namedNode("http://example.org/Alan_Turing"), + * df.namedNode("http://schema.org/name"), + * df.literal("Alan Turing"), + * ); + * + * // Delete the quad + * await Persons.deleteData(quad); + * ``` + * + * @param quads Quads to delete from the data store + * @returns Nothing + */ + deleteData(...quads: RDF.Quad[]): Promise { const q = this.queryBuilder.deleteDataQuery(quads); return this.updateQuery(q); } diff --git a/library/lens/mod.ts b/library/lens/mod.ts index bee4798..931f4f3 100644 --- a/library/lens/mod.ts +++ b/library/lens/mod.ts @@ -1 +1 @@ -export { createLens, type Lens } from "./lens.ts"; +export * from "./lens.ts"; diff --git a/library/lens/query_builder.ts b/library/lens/query_builder.ts index da4ad7e..25a97f5 100644 --- a/library/lens/query_builder.ts +++ b/library/lens/query_builder.ts @@ -14,8 +14,8 @@ import { type SparqlValue, } from "../sparql/mod.ts"; import { DataFactory, type Iri, type RDF } from "../rdf.ts"; -import ldkit from "../namespaces/ldkit.ts"; -import rdf from "../namespaces/rdf.ts"; +import { ldkit } from "../../namespaces/ldkit.ts"; +import { rdf } from "../../namespaces/rdf.ts"; import { encode } from "../encoder.ts"; diff --git a/library/lens/search_helper.ts b/library/lens/search_helper.ts index 53b05b3..2f8573b 100644 --- a/library/lens/search_helper.ts +++ b/library/lens/search_helper.ts @@ -1,7 +1,7 @@ import { DataFactory, toRdf } from "../rdf.ts"; import { sparql as $, type SparqlValue } from "../sparql/mod.ts"; import { type ExpandedProperty, type SearchSchema } from "../schema/mod.ts"; -import xsd from "../namespaces/xsd.ts"; +import { xsd } from "../../namespaces/xsd.ts"; export class SearchHelper { private readonly property: ExpandedProperty; diff --git a/library/namespace.ts b/library/namespace.ts new file mode 100644 index 0000000..ee2adbc --- /dev/null +++ b/library/namespace.ts @@ -0,0 +1,59 @@ +/** Original type of namespace specification */ +export type Namespace = { + iri: string; + prefix: string; + terms: readonly string[]; +}; + +/** Resulting type of namespace providing access to all terms, prefix and IRI */ +export type NamespaceInterface = + & { + [Term in NamespaceSpec["terms"][number]]: + `${NamespaceSpec["prefix"]}${Term}`; + } + & { + $prefix: NamespaceSpec["prefix"]; + $iri: NamespaceSpec["iri"]; + }; + +/** + * Creates a strongly typed container for Linked Data vocabulary to provide + * type safe access to all vocabulary terms as well as IDE autocompletion. + * + * @example + * ```typescript + * import { createNamespace } from "ldkit"; + * + * const onto = createNamespace( + * { + * iri: "http://www.example.com/ontology#", + * prefix: "onto:", + * terms: [ + * "object", + * "predicate", + * "subject", + * ], + * } as const, + * ); + * + * console.log(onto.subject); // prints http://www.example.com/ontology#subject + * console.log(onto.unknown); // TypeScript error! This term does not exist + * ``` + * + * @param namespaceSpec Specification of the namespace + * @returns + */ +export function createNamespace( + namespaceSpec: N, +): NamespaceInterface { + return Object.assign( + namespaceSpec.terms.reduce((acc, term) => { + acc[term] = `${namespaceSpec.iri}${term}`; + return acc; + }, {} as Record), + { + $prefix: namespaceSpec["prefix"], + $iri: namespaceSpec["iri"], + }, + ) as unknown as NamespaceInterface; +} diff --git a/library/namespaces/ldkit.ts b/library/namespaces/ldkit.ts deleted file mode 100644 index 4484664..0000000 --- a/library/namespaces/ldkit.ts +++ /dev/null @@ -1,9 +0,0 @@ -import { createNamespace } from "./namespace.ts"; - -export default createNamespace( - { - iri: "http://ldkit/", - prefix: "ldkit:", - terms: ["Resource"], - } as const, -); diff --git a/library/namespaces/mod.ts b/library/namespaces/mod.ts deleted file mode 100644 index b710df2..0000000 --- a/library/namespaces/mod.ts +++ /dev/null @@ -1,15 +0,0 @@ -export { default as dbo } from "./dbo.ts"; -export { default as dc } from "./dc.ts"; -export { default as dcterms } from "./dcterms.ts"; -export { default as foaf } from "./foaf.ts"; -export { default as gr } from "./gr.ts"; -export { default as ldkit } from "./ldkit.ts"; -export { default as owl } from "./owl.ts"; -export { default as rdf } from "./rdf.ts"; -export { default as rdfs } from "./rdfs.ts"; -export { default as schema } from "./schema.ts"; -export { default as sioc } from "./sioc.ts"; -export { default as skos } from "./skos.ts"; -export { default as xsd } from "./xsd.ts"; - -export { createNamespace } from "./namespace.ts"; diff --git a/library/namespaces/namespace.ts b/library/namespaces/namespace.ts deleted file mode 100644 index a26324e..0000000 --- a/library/namespaces/namespace.ts +++ /dev/null @@ -1,39 +0,0 @@ -type NamespacePrototype = { - iri: string; - prefix: string; - terms: readonly string[]; -}; - -type NamespacePrefix = - Namespace["prefix"]; - -type NamespaceIri = Namespace["iri"]; - -type NamespaceObject = { - [Term in Namespace["terms"][number]]: `${NamespacePrefix}${Term}`; -}; - -export const createNamespace = < - N extends NamespacePrototype, - I = NamespaceIri, - P = NamespacePrefix, - O = NamespaceObject, ->( - namespaceSpec: N, -) => - Object.assign( - //(f: [X]) => - // `${namespaceSpec.prefix}:${f}` as `${string & P}${string & X}`, - namespaceSpec.terms.reduce((acc, term) => { - //acc[term] = `${namespaceSpec.prefix}${term}` - acc[term] = `${namespaceSpec.iri}${term}`; - return acc; - }, {} as Record), - { - $prefix: namespaceSpec["prefix"], - $iri: namespaceSpec["iri"], - }, - ) as unknown as O & { - $prefix: P; - $iri: I; - }; diff --git a/library/options.ts b/library/options.ts index a2b0ca8..7c453c7 100644 --- a/library/options.ts +++ b/library/options.ts @@ -1,14 +1,24 @@ -import type { IQueryEngine, QueryContext } from "./rdf.ts"; -import { QueryEngine } from "./engine/mod.ts"; - -type LDkitOptions = { - engine: IQueryEngine; - language: string; - take: number; - logQuery: (query: string) => void; -}; +import { + type IQueryEngine, + type QueryContext, + QueryEngine, +} from "./engine/mod.ts"; -export type Options = Partial & Partial; +/** + * LDkit options and query engine context + * + * LDkit-specific options are: + * - `engine` - a query engine to use for querying data sources + * - `language` - a preferred language for literals + * - `take` - a default number of results to take (limit of SELECT queries) + * - `logQuery` - a function that will be called for each SPARQL query + */ +export type Options = { + engine?: IQueryEngine; + language?: string; + take?: number; + logQuery?: (query: string) => void; +} & Partial; const defaultOptions = { engine: new QueryEngine(), @@ -18,19 +28,41 @@ const defaultOptions = { let globalOptions: Options = {}; -export const setGlobalOptions = (options: Options) => { +/** + * Sets global configuration {@link Options} for LDkit that will be used + * by default in all queries, unless overridden in {@link Lens}. + * + * LDkit-specific options are: + * - `engine` - a query engine to use for querying data sources + * - `language` - a preferred language for literals + * - `take` - a default number of results to take (limit of SELECT queries) + * - `logQuery` - a function that will be called for each SPARQL query + * + * Default values for these options are: + * ```typescript + * const defaultOptions = { + * engine: new QueryEngine(), + * take: 1000, + * logQuery: () => {}, + * }; + * ``` + * The default configuration uses built-in {@link QueryEngine}. Language is not set by default. + * + * @param options LDkit options and query engine context + */ +export function setGlobalOptions(options: Options): void { globalOptions = options; -}; +} -export const resolveOptions = (options: Options = {}) => { +export function resolveOptions(options: Options = {}) { return { ...defaultOptions, ...globalOptions, ...options, }; -}; +} -export const resolveQueryContext = (options: Options): QueryContext => { +export function resolveQueryContext(options: Options): QueryContext { const { engine: _engine, language: _language, take: _take, ...context } = options; @@ -44,4 +76,4 @@ export const resolveQueryContext = (options: Options): QueryContext => { } return context as QueryContext; -}; +} diff --git a/library/rdf.ts b/library/rdf.ts index deb837f..dd75a1b 100644 --- a/library/rdf.ts +++ b/library/rdf.ts @@ -1,4 +1,4 @@ -import type * as RDF from "npm:rdf-js@4.0.2"; +import type * as RDF from "npm:@rdfjs/types"; export type { RDF }; @@ -10,27 +10,6 @@ export { DataFactory, DefaultGraph }; // @deno-types="npm:@types/n3" export * as N3 from "npm:n3@1.17.2"; -import type { - IDataSource, - IQueryContextCommon, -} from "npm:@comunica/types@2.6.8"; - -export type LDkitContext = { - graph?: string; - language?: string; - take?: number; -}; - -export type QueryContext = - & RDF.QueryStringContext - & RDF.QuerySourceContext - & IQueryContextCommon; - -export type IQueryEngine = RDF.StringSparqlQueryable< - RDF.SparqlResultSupport, - QueryContext ->; - export type Iri = string; export type Node = Map; diff --git a/library/schema/data_types.ts b/library/schema/data_types.ts index 6e98244..ede5edc 100644 --- a/library/schema/data_types.ts +++ b/library/schema/data_types.ts @@ -1,5 +1,5 @@ -import xsd from "../namespaces/xsd.ts"; -import rdf from "../namespaces/rdf.ts"; +import { xsd } from "../../namespaces/xsd.ts"; +import { rdf } from "../../namespaces/rdf.ts"; const SupportedDataTypesPrototype = { [xsd.dateTime]: new Date(), @@ -40,6 +40,8 @@ const SupportedDataTypesPrototype = { [xsd.duration]: "", }; +/** Map of supported RDF data types and their JavaScript native counterparts */ export type SupportedDataTypes = typeof SupportedDataTypesPrototype; +/** List of supported native JavaScript types */ export type SupportedNativeTypes = SupportedDataTypes[keyof SupportedDataTypes]; diff --git a/library/schema/interface.ts b/library/schema/interface.ts index 3476490..43da55a 100644 --- a/library/schema/interface.ts +++ b/library/schema/interface.ts @@ -59,10 +59,17 @@ type ConvertProperty = T extends Property ? ConvertPropertyObject : string; +/** Object that contains IRI of an entity */ export type Identity = { $id: string; }; +/** + * Describes a data model of a data entity according to its schema,. as resolved + * by LDkit, i.e. the shape of data that LDkit returns when querying for entities. + * + * See {@link Lens.prototype.find} for usage example. + */ export type SchemaInterface = & Identity & { @@ -100,6 +107,11 @@ type ConvertUpdatePropertyObject = type ConvertUpdateProperty = T extends Property ? ConvertUpdatePropertyObject : string; +/** + * Describes a shape of data for updating an entity, according to its data schema. + * + * See {@link Lens.prototype.update} for usage example. + */ export type SchemaUpdateInterface = & Identity & { @@ -125,6 +137,11 @@ type InverseProperties = InversePropertiesMap< T >[keyof InversePropertiesMap]; +/** + * Describes a shape of data for updating an entity, according to its data schema. + * + * See {@link Lens.prototype.find} for usage example. + */ export type SchemaSearchInterface = { [X in Exclude>]?: T[X] extends ValidPropertyDefinition ? ConvertSearchProperty diff --git a/library/schema/schema.ts b/library/schema/schema.ts index 96a0bc1..3c84535 100644 --- a/library/schema/schema.ts +++ b/library/schema/schema.ts @@ -1,10 +1,13 @@ import type { SupportedDataTypes } from "./data_types.ts"; -type PropertyType = keyof SupportedDataTypes; - +/** + * Data property prototype that describes RDF predicate of a data entity. + * Includes specification of other metadata, such as whether the property + * is optional, array, inverse, or whether it is a nested data entity, etc. + */ export type Property = { "@id": string; - "@type"?: PropertyType; + "@type"?: keyof SupportedDataTypes; "@schema"?: Schema; "@optional"?: true; "@array"?: true; @@ -12,6 +15,10 @@ export type Property = { "@inverse"?: true; }; +/** + * Data schema prototype that describes a data entity. Includes an optional + * specification of RDF type and a map of RDF properties. + */ export type Schema = { "@type"?: string | readonly string[]; } & { @@ -20,7 +27,7 @@ export type Schema = { export type ExpandedProperty = { "@id": string; - "@type"?: PropertyType; + "@type"?: keyof SupportedDataTypes; "@schema"?: ExpandedSchema; "@optional"?: true; "@array"?: true; diff --git a/library/schema/utils.ts b/library/schema/utils.ts index 9226473..0dfdf4c 100644 --- a/library/schema/utils.ts +++ b/library/schema/utils.ts @@ -1,5 +1,5 @@ -import rdf from "../namespaces/rdf.ts"; -import xsd from "../namespaces/xsd.ts"; +import { rdf } from "../../namespaces/rdf.ts"; +import { xsd } from "../../namespaces/xsd.ts"; import type { ExpandedProperty, diff --git a/library/sparql/mod.ts b/library/sparql/mod.ts index 408d4cc..cb5720c 100644 --- a/library/sparql/mod.ts +++ b/library/sparql/mod.ts @@ -1,4 +1,4 @@ -export { sparql, type SparqlValue } from "./sparql_tag.ts"; +export * from "./sparql_tag.ts"; export { ASK, CONSTRUCT, DESCRIBE, SELECT } from "./sparql_query_builders.ts"; export { DELETE, INSERT, WITH } from "./sparql_update_builders.ts"; -export { OPTIONAL } from "./sparql_expression_builders.ts"; +export * from "./sparql_expression_builders.ts"; diff --git a/library/sparql/sparql_expression_builders.ts b/library/sparql/sparql_expression_builders.ts index bab1080..0c869ef 100644 --- a/library/sparql/sparql_expression_builders.ts +++ b/library/sparql/sparql_expression_builders.ts @@ -16,6 +16,17 @@ class SparqlExpressionBuilder extends SparqlBuilder { } } +/** + * SPARQL OPTIONAL expression fluent interface + * + * @example + * ```typescript + * import { OPTIONAL } from "ldkit/sparql"; + * + * const query = OPTIONAL`?s ?p ?o`.build(); + * console.log(query); // OPTIONAL { ?s ?p ?o } + * ``` + */ export const OPTIONAL = ( strings: TemplateStringsArray, ...values: SparqlValue[] diff --git a/library/sparql/sparql_query_builders.ts b/library/sparql/sparql_query_builders.ts index c5b2128..146b2c1 100644 --- a/library/sparql/sparql_query_builders.ts +++ b/library/sparql/sparql_query_builders.ts @@ -134,6 +134,27 @@ class SparqlQueryBuilder extends SparqlBuilder { } } +/** + * SPARQL SELECT query fluent interface + * + * @example + * ```typescript + * import { SELECT } from "ldkit/sparql"; + * + * const query = SELECT`?s`.WHERE`?s ?p ?o`.ORDER_BY`?s`.LIMIT(100).build(); + * console.log(query); + * // SELECT ?s WHERE { ?s ?p ?o } ORDER BY ?s LIMIT 100 + * ``` + * + * @example + * ```typescript + * import { SELECT } from "ldkit/sparql"; + * + * const query = SELECT.DISTINCT`?s`.WHERE`?s ?p ?o`.build(); + * console.log(query); + * // SELECT DISTINCT ?s WHERE { ?s ?p ?o } + * ``` + */ export const SELECT = Object.assign(( strings: TemplateStringsArray, ...values: SparqlValue[] @@ -151,6 +172,24 @@ export const SELECT = Object.assign(( }, }); +/** + * SPARQL CONSTRUCT query fluent interface + * + * @example + * ```typescript + * import { CONSTRUCT } from "ldkit/sparql"; + * import { DataFactory } from "ldkit/rdf"; + * + * const df = new DataFactory(); + * const sNode = df.namedNode("http://example.org/datasource"); + * const pNode = df.namedNode("http://example.org/hasSubject"); + * + * const query = CONSTRUCT`${sNode} ${pNode} ?s`.WHERE`?s ?p ?o`.build(); + * console.log(query); + * // CONSTRUCT { ?s } + * // WHERE { ?s ?p ?o } + * ``` + */ export const CONSTRUCT = Object.assign(( strings: TemplateStringsArray, ...values: SparqlValue[] @@ -161,6 +200,17 @@ export const CONSTRUCT = Object.assign(( ) => new SparqlQueryBuilder().CONSTRUCT_WHERE(strings, ...values), }); +/** + * SPARQL ASK query fluent interface + * + * @example + * ```typescript + * import { ASK } from "ldkit/sparql"; + * + * const query = ASK`?s ?p ?o`.build(); + * console.log(query); // ASK { ?s ?p ?o } + * ``` + */ export const ASK = Object.assign(( strings: TemplateStringsArray, ...values: SparqlValue[] @@ -177,6 +227,21 @@ export const ASK = Object.assign(( ) => new SparqlQueryBuilder().ASK_WHERE(strings, ...values), }); +/** + * SPARQL DESCRIBE query fluent interface + * + * @example + * ```typescript + * import { DESCRIBE } from "ldkit/sparql"; + * import { DataFactory } from "ldkit/rdf"; + * + * const df = new DataFactory(); + * const node = df.namedNode("http://example.org/resource"); + * + * const query = DESCRIBE`${node}`.build(); + * console.log(query); // DESCRIBE + * ``` + */ export const DESCRIBE = ( strings: TemplateStringsArray, ...values: SparqlValue[] diff --git a/library/sparql/sparql_tag.ts b/library/sparql/sparql_tag.ts index a6a1ad4..9e34dae 100644 --- a/library/sparql/sparql_tag.ts +++ b/library/sparql/sparql_tag.ts @@ -1,8 +1,11 @@ import { DataFactory, type RDF } from "../rdf.ts"; -import xsd from "../namespaces/xsd.ts"; +import { xsd } from "../../namespaces/xsd.ts"; import { stringify } from "./stringify.ts"; +/** + * Any value that can be used in LDkit SPARQL builders + */ export type SparqlValue = | RDF.Term | string @@ -14,10 +17,33 @@ export type SparqlValue = | null | undefined; +/** + * A template tag for SPARQL queries or its parts. Automatically converts + * values to SPARQL literals and escapes strings as needed. + * + * @example + * ```typescript + * import { sparql } from "ldkit/sparql"; + * import { DataFactory } from "ldkit/rdf"; + * + * const df = new DataFactory(); + * const quad = df.quad( + * df.namedNode("http://example.org/s"), + * df.namedNode("http://example.org/p"), + * df.literal("o"), + * ); + * const query = sparql`SELECT * WHERE { ${quad} }`; + * console.log(query); // SELECT * WHERE { "o" . } + * ``` + * + * @param strings {TemplateStringsArray} template strings + * @param values {SparqlValue[]} + * @returns {string} SPARQL query or its part + */ export const sparql = ( strings: TemplateStringsArray, ...values: SparqlValue[] -) => { +): string => { let counter = 0; let result = ""; diff --git a/library/sparql/sparql_update_builders.ts b/library/sparql/sparql_update_builders.ts index 19c823b..90b1768 100644 --- a/library/sparql/sparql_update_builders.ts +++ b/library/sparql/sparql_update_builders.ts @@ -70,6 +70,26 @@ class SparqlUpdateBuilder extends SparqlBuilder { } } +/** + * SPARQL INSERT query fluent interface + * + * @example + * ```typescript + * import { INSERT } from "ldkit/sparql"; + * import { foaf } from "ldkit/namespaces"; + * import { DataFactory } from "ldkit/rdf"; + * + * const df = new DataFactory(); + * const firstName = df.namedNode(foaf.firstName); + * + * const query = INSERT`?person ${firstName} "Paul"` + * .WHERE`?person ${firstName} "Jean"` + * .build(); + * console.log(query); + * // INSERT { ?person "Paul" } + * // WHERE { ?person "Jean" } + * ``` + */ export const INSERT = Object.assign(( strings: TemplateStringsArray, ...values: SparqlValue[] @@ -80,6 +100,28 @@ export const INSERT = Object.assign(( ) => new SparqlUpdateBuilder().INSERT_DATA(strings, ...values), }); +/** + * SPARQL DELETE query fluent interface + * + * @example + * ```typescript + * import { DELETE } from "ldkit/sparql"; + * import { foaf } from "ldkit/namespaces"; + * import { DataFactory } from "ldkit/rdf"; + * + * const df = new DataFactory(); + * const firstName = df.namedNode(foaf.firstName); + * + * const query = DELETE`?person ${firstName} "Jean"` + * .INSERT`?person ${firstName} "Paul"` + * .WHERE`?person ${firstName} "Jean"` + * .build(); + * console.log(query); + * // DELETE { ?person "Jean" } + * // INSERT { ?person "Paul" } + * // WHERE { ?person "Jean" } + * ``` + */ export const DELETE = Object.assign(( strings: TemplateStringsArray, ...values: SparqlValue[] @@ -94,6 +136,30 @@ export const DELETE = Object.assign(( ) => new SparqlUpdateBuilder().DELETE_WHERE(strings, ...values), }); +/** + * SPARQL WITH query fluent interface + * + * @example + * ```typescript + * import { DELETE } from "ldkit/sparql"; + * import { foaf } from "ldkit/namespaces"; + * import { DataFactory } from "ldkit/rdf"; + * + * const df = new DataFactory(); + * const firstName = df.namedNode(foaf.firstName); + * const graph = df.namedNode("http://example.org/graph"); + * + * const query = WITH(graph).DELETE`?person ${firstName} "Jean"` + * .INSERT`?person ${firstName} "Paul"` + * .WHERE`?person ${firstName} "Jean"` + * .build(); + * console.log(query); + * // WITH + * // DELETE { ?person "Jean" } + * // INSERT { ?person "Paul" } + * // WHERE { ?person "Jean" } + * ``` + */ export const WITH = ( stringOrNamedNode: string | RDF.NamedNode, ) => new SparqlUpdateBuilder().WITH(stringOrNamedNode); diff --git a/library/sparql/stringify.ts b/library/sparql/stringify.ts index 0041bba..af06d33 100644 --- a/library/sparql/stringify.ts +++ b/library/sparql/stringify.ts @@ -1,5 +1,5 @@ import { DefaultGraph, type RDF } from "../rdf.ts"; -import xsd from "../namespaces/xsd.ts"; +import { xsd } from "../../namespaces/xsd.ts"; import { escape } from "./escape.ts"; export const blankNode = (term: RDF.BlankNode) => { diff --git a/mod.ts b/mod.ts index d1ac4a5..8714f22 100644 --- a/mod.ts +++ b/mod.ts @@ -1,11 +1,17 @@ -export { type IQueryEngine, type QueryContext } from "./library/rdf.ts"; - export { type Options, setGlobalOptions } from "./library/options.ts"; -export * from "./library/schema/mod.ts"; +export type { + Identity, + Property, + Schema, + SchemaInterface, + SchemaSearchInterface, + SchemaUpdateInterface, + SupportedDataTypes, +} from "./library/schema/mod.ts"; export * from "./library/lens/mod.ts"; -export { createNamespace } from "./library/namespaces/namespace.ts"; +export * from "./library/namespace.ts"; -export { QueryEngine } from "./library/engine/mod.ts"; +export * from "./library/engine/mod.ts"; diff --git a/namespaces.ts b/namespaces.ts index 29c3d82..baaee84 100644 --- a/namespaces.ts +++ b/namespaces.ts @@ -1 +1,31 @@ -export * from "./library/namespaces/mod.ts"; +/** + * Popular namespaces used in Linked Data, fully compatible with LDkit, + * offering autocompletion and type checking in IDE. + * + * Create your own namespaces using {@link createNamespace} helper. + * + * @example + * ```typescript + * import { rdf, schema } from "ldkit/namespaces"; + * + * console.log(rdf.type); // "http://www.w3.org/1999/02/22-rdf-syntax-ns#type" + * console.log(schema.Person); // "http://schema.org/Person" + * ``` + * + * @module + */ +export { dbo } from "./namespaces/dbo.ts"; +export { dc } from "./namespaces/dc.ts"; +export { dcterms } from "./namespaces/dcterms.ts"; +export { foaf } from "./namespaces/foaf.ts"; +export { gr } from "./namespaces/gr.ts"; +export { ldkit } from "./namespaces/ldkit.ts"; +export { owl } from "./namespaces/owl.ts"; +export { rdf } from "./namespaces/rdf.ts"; +export { rdfs } from "./namespaces/rdfs.ts"; +export { schema } from "./namespaces/schema.ts"; +export { sioc } from "./namespaces/sioc.ts"; +export { skos } from "./namespaces/skos.ts"; +export { xsd } from "./namespaces/xsd.ts"; + +export * from "./library/namespace.ts"; diff --git a/library/namespaces/dbo.ts b/namespaces/dbo.ts similarity index 99% rename from library/namespaces/dbo.ts rename to namespaces/dbo.ts index 2ddb917..cf8b361 100644 --- a/library/namespaces/dbo.ts +++ b/namespaces/dbo.ts @@ -1,6 +1,11 @@ -import { createNamespace } from "./namespace.ts"; +import { createNamespace } from "../library/namespace.ts"; -export default createNamespace( +/** + * DBpedia Ontology + * + * `@dbo: ` + */ +export const dbo = createNamespace( { iri: "http://dbpedia.org/ontology/", prefix: "dbo:", diff --git a/library/namespaces/dc.ts b/namespaces/dc.ts similarity index 64% rename from library/namespaces/dc.ts rename to namespaces/dc.ts index 226f782..dd7b0da 100644 --- a/library/namespaces/dc.ts +++ b/namespaces/dc.ts @@ -1,6 +1,11 @@ -import { createNamespace } from "./namespace.ts"; +import { createNamespace } from "../library/namespace.ts"; -export default createNamespace( +/** + * Dublin Core Metadata Element Set, Version 1.1 + * + * `@dc: `, + */ +export const dc = createNamespace( { iri: "http://purl.org/dc/elements/1.1/", prefix: "dc:", diff --git a/library/namespaces/dcterms.ts b/namespaces/dcterms.ts similarity index 91% rename from library/namespaces/dcterms.ts rename to namespaces/dcterms.ts index c70c8a6..e3937d6 100644 --- a/library/namespaces/dcterms.ts +++ b/namespaces/dcterms.ts @@ -1,6 +1,11 @@ -import { createNamespace } from "./namespace.ts"; +import { createNamespace } from "../library/namespace.ts"; -export default createNamespace( +/** + * DCMI Metadata Terms + * + * `@dcterms: `, + */ +export const dcterms = createNamespace( { iri: "http://purl.org/dc/terms/", prefix: "dcterms:", diff --git a/library/namespaces/foaf.ts b/namespaces/foaf.ts similarity index 83% rename from library/namespaces/foaf.ts rename to namespaces/foaf.ts index a3e1c5f..10c174c 100644 --- a/library/namespaces/foaf.ts +++ b/namespaces/foaf.ts @@ -1,6 +1,11 @@ -import { createNamespace } from "./namespace.ts"; +import { createNamespace } from "../library/namespace.ts"; -export default createNamespace( +/** + * The Friend of a Friend (FOAF) vocabulary, described using W3C RDF Schema and the Web Ontology Language. + * + * `@foaf: `, + */ +export const foaf = createNamespace( { iri: "http://xmlns.com/foaf/0.1/", prefix: "foaf:", diff --git a/library/namespaces/gr.ts b/namespaces/gr.ts similarity index 95% rename from library/namespaces/gr.ts rename to namespaces/gr.ts index 76dbbb6..830b563 100644 --- a/library/namespaces/gr.ts +++ b/namespaces/gr.ts @@ -1,6 +1,11 @@ -import { createNamespace } from "./namespace.ts"; +import { createNamespace } from "../library/namespace.ts"; -export default createNamespace( +/** + * GoodRelations Ontology + * + * `@gr: `, + */ +export const gr = createNamespace( { iri: "http://purl.org/goodrelations/v1#", prefix: "gr:", diff --git a/namespaces/ldkit.ts b/namespaces/ldkit.ts new file mode 100644 index 0000000..ef0c0b5 --- /dev/null +++ b/namespaces/ldkit.ts @@ -0,0 +1,14 @@ +import { createNamespace } from "../library/namespace.ts"; + +/** + * LDkit Ontology + * + * `@ldkit: `, + */ +export const ldkit = createNamespace( + { + iri: "https://ldkit.io/ontology/", + prefix: "ldkit:", + terms: ["Resource"], + } as const, +); diff --git a/library/namespaces/owl.ts b/namespaces/owl.ts similarity index 92% rename from library/namespaces/owl.ts rename to namespaces/owl.ts index 8e1f2d8..2f35bca 100644 --- a/library/namespaces/owl.ts +++ b/namespaces/owl.ts @@ -1,6 +1,11 @@ -import { createNamespace } from "./namespace.ts"; +import { createNamespace } from "../library/namespace.ts"; -export default createNamespace( +/** + * OWL Web Ontology Language + * + * `@owl: `, + */ +export const owl = createNamespace( { iri: "http://www.w3.org/2002/07/owl#", prefix: "owl:", diff --git a/library/namespaces/rdf.ts b/namespaces/rdf.ts similarity index 70% rename from library/namespaces/rdf.ts rename to namespaces/rdf.ts index 8921c4e..e06150b 100644 --- a/library/namespaces/rdf.ts +++ b/namespaces/rdf.ts @@ -1,6 +1,11 @@ -import { createNamespace } from "./namespace.ts"; +import { createNamespace } from "../library/namespace.ts"; -export default createNamespace( +/** + * The RDF Concepts Vocabulary (RDF) + * + * `@rdf: `, + */ +export const rdf = createNamespace( { iri: "http://www.w3.org/1999/02/22-rdf-syntax-ns#", prefix: "rdf:", diff --git a/library/namespaces/rdfs.ts b/namespaces/rdfs.ts similarity index 66% rename from library/namespaces/rdfs.ts rename to namespaces/rdfs.ts index 4e2b40b..d09bee8 100644 --- a/library/namespaces/rdfs.ts +++ b/namespaces/rdfs.ts @@ -1,6 +1,11 @@ -import { createNamespace } from "./namespace.ts"; +import { createNamespace } from "../library/namespace.ts"; -export default createNamespace( +/** + * The RDF Schema vocabulary (RDFS) + * + * `@rdfs: `, + */ +export const rdfs = createNamespace( { iri: "http://www.w3.org/2000/01/rdf-schema#", prefix: "rdfs:", diff --git a/library/namespaces/schema.ts b/namespaces/schema.ts similarity index 99% rename from library/namespaces/schema.ts rename to namespaces/schema.ts index 1a74942..0ba28f1 100644 --- a/library/namespaces/schema.ts +++ b/namespaces/schema.ts @@ -1,6 +1,11 @@ -import { createNamespace } from "./namespace.ts"; +import { createNamespace } from "../library/namespace.ts"; -export default createNamespace( +/** + * Schema.org vocabulary + * + * `@schema: `, + */ +export const schema = createNamespace( { iri: "http://schema.org/", prefix: "schema:", diff --git a/library/namespaces/sioc.ts b/namespaces/sioc.ts similarity index 91% rename from library/namespaces/sioc.ts rename to namespaces/sioc.ts index 8f2c356..5ea0b85 100644 --- a/library/namespaces/sioc.ts +++ b/namespaces/sioc.ts @@ -1,6 +1,11 @@ -import { createNamespace } from "./namespace.ts"; +import { createNamespace } from "../library/namespace.ts"; -export default createNamespace( +/** + * SIOC Core Ontology Namespace + * + * `@sioc: `, + */ +export const sioc = createNamespace( { iri: "http://rdfs.org/sioc/ns#", prefix: "sioc:", diff --git a/library/namespaces/skos.ts b/namespaces/skos.ts similarity index 78% rename from library/namespaces/skos.ts rename to namespaces/skos.ts index 5992a7d..af51ce7 100644 --- a/library/namespaces/skos.ts +++ b/namespaces/skos.ts @@ -1,6 +1,11 @@ -import { createNamespace } from "./namespace.ts"; +import { createNamespace } from "../library/namespace.ts"; -export default createNamespace( +/** + * SKOS - Simple Knowledge Organization System + * + * `@skos: `, + */ +export const skos = createNamespace( { iri: "http://www.w3.org/2004/02/skos/core#", prefix: "skos:", diff --git a/library/namespaces/xsd.ts b/namespaces/xsd.ts similarity index 84% rename from library/namespaces/xsd.ts rename to namespaces/xsd.ts index 89de133..0226dcb 100644 --- a/library/namespaces/xsd.ts +++ b/namespaces/xsd.ts @@ -1,6 +1,11 @@ -import { createNamespace } from "./namespace.ts"; +import { createNamespace } from "../library/namespace.ts"; -export default createNamespace( +/** + * XML Schema Definition Language (XSD) + * + * `@xsd: `, + */ +export const xsd = createNamespace( { iri: "http://www.w3.org/2001/XMLSchema#", prefix: "xsd:", diff --git a/rdf.ts b/rdf.ts index 6c3ba22..988bc9c 100644 --- a/rdf.ts +++ b/rdf.ts @@ -1 +1,21 @@ -export * from "./library/rdf.ts"; +/** + * RDF utilities + * + * This module contains a re-export of external RDF libraries that are used + * in LDkit and may be used in tandem with LDkit in Linked Data applications as well. + * + * Included packages: + * - [@rdfjs/types](https://github.com/rdfjs/types) [RDF/JS](https://rdf.js.org/) authoritative TypeScript typings + * - [n3](https://rdf.js.org/N3.js/) RDF parser and serializer + * - [rdf-data-factory](https://github.com/rubensworks/rdf-data-factory.js) A TypeScript/JavaScript implementation of the RDF/JS data factory + * - [rdf-literal](https://github.com/rubensworks/rdf-literal.js) Translates between RDF literals and JavaScript primitives + * @module + */ +export { + DataFactory, + DefaultGraph, + fromRdf, + N3, + type RDF, + toRdf, +} from "./library/rdf.ts"; diff --git a/sparql.ts b/sparql.ts index 8415ef1..c20fd18 100644 --- a/sparql.ts +++ b/sparql.ts @@ -1 +1,14 @@ +/** + * SPARQL builders that provide a fluent interface for building SPARQL queries + * + * @example + * ```typescript + * import { SELECT } from "ldkit/sparql"; + * + * const query = SELECT`?s ?p ?o`.WHERE`?s ?p ?o`.LIMIT(10).build(); + * console.log(query); // SELECT ?s ?p ?o WHERE { ?s ?p ?o } LIMIT 10; + * ``` + * + * @module + */ export * from "./library/sparql/mod.ts"; diff --git a/tests/decoder.test.ts b/tests/decoder.test.ts index eccf779..59afefb 100644 --- a/tests/decoder.test.ts +++ b/tests/decoder.test.ts @@ -1,10 +1,11 @@ import { assertEquals, assertThrows } from "./test_deps.ts"; import { createGraph, x } from "./test_utils.ts"; -import type { ExpandedSchema, Options } from "ldkit"; +import type { Options } from "ldkit"; import { rdf, xsd } from "ldkit/namespaces"; import { decode } from "../library/decoder.ts"; +import type { ExpandedSchema } from "../library/schema/mod.ts"; const decodeGraph = ( turtle: string, diff --git a/tests/encoder.test.ts b/tests/encoder.test.ts index b9bcaeb..d52383d 100644 --- a/tests/encoder.test.ts +++ b/tests/encoder.test.ts @@ -1,10 +1,11 @@ import { assertEquals } from "./test_deps.ts"; import { ttl, x } from "./test_utils.ts"; -import type { ExpandedSchema, Options } from "ldkit"; +import type { Options } from "ldkit"; import { xsd } from "ldkit/namespaces"; import { encode } from "../library/encoder.ts"; +import type { ExpandedSchema } from "../library/schema/mod.ts"; const evaluate = ( node: Record, diff --git a/tests/engine.test.ts b/tests/engine.test.ts index 3c9b33c..1abe140 100644 --- a/tests/engine.test.ts +++ b/tests/engine.test.ts @@ -1,7 +1,6 @@ import { assertEquals, assertRejects } from "./test_deps.ts"; -import { QueryEngine } from "../library/engine/query_engine.ts"; -import { type QueryContext } from "../library/rdf.ts"; +import { type QueryContext, QueryEngine } from "ldkit"; const engine = new QueryEngine(); const context: QueryContext = { sources: ["https://dbpedia.org/sparql"] }; diff --git a/tests/rdf.test.ts b/tests/rdf.test.ts index f30e397..4b1c16f 100644 --- a/tests/rdf.test.ts +++ b/tests/rdf.test.ts @@ -1,12 +1,8 @@ import { assert, assertEquals } from "./test_deps.ts"; -import { - BindingsFactory, - DataFactory, - QuadFactory, - type RDF, - type RDFJSON, -} from "ldkit/rdf"; +import { DataFactory, type RDF } from "ldkit/rdf"; + +import { BindingsFactory, QuadFactory, RDFJSON } from "../library/rdf.ts"; Deno.test("RDF / Quad Factory", () => { const df = new DataFactory(); diff --git a/tests/test_utils.ts b/tests/test_utils.ts index a0f7782..068f21a 100644 --- a/tests/test_utils.ts +++ b/tests/test_utils.ts @@ -3,15 +3,11 @@ import { assertEquals, Comunica } from "./test_deps.ts"; const NOLOG = Deno.args.includes("--nolog"); export const logQuery = NOLOG ? () => {} : console.log; -import { - DataFactory, - N3, - quadsToGraph, - type QueryContext, - type RDF, -} from "ldkit/rdf"; +import { DataFactory, N3, type RDF } from "ldkit/rdf"; import { ldkit, schema, xsd } from "ldkit/namespaces"; -import { Options } from "ldkit"; +import { Options, type QueryContext } from "ldkit"; + +import { quadsToGraph } from "../library/rdf.ts"; export type Equals = A extends B ? (B extends A ? true : false) : false; diff --git a/www/components/App.tsx b/www/components/App.tsx index 2f144af..f7cb263 100644 --- a/www/components/App.tsx +++ b/www/components/App.tsx @@ -2,7 +2,7 @@ import type { ComponentChildren } from "preact"; import { asset, Head } from "$fresh/runtime.ts"; import { Footer } from "./Footer.tsx"; -import { type ActiveLink, Header } from "./Header.tsx"; +import { Header } from "./Header.tsx"; import { Title } from "./Title.tsx"; type AppProps = { diff --git a/www/components/Header.tsx b/www/components/Header.tsx index 7ca4dc6..97c8f0c 100644 --- a/www/components/Header.tsx +++ b/www/components/Header.tsx @@ -27,34 +27,12 @@ function Logo() { ); } -const menuItems = [ - { - title: "Home", - url: "/", - }, - { - title: "Documentation", - url: "/docs", - }, - //{ - // title: "Showcase", - // url: "/showcase", - //}, - { - title: "GitHub", - url: "https://github.com/karelklima/ldkit", - }, -] as const; - -type Writeable = { -readonly [P in keyof T]: T[P] }; -type Unpacked = T extends (infer U)[] ? U : T; -export type ActiveLink = Unpacked>["url"]; - function Menu() { return (
    Home Documentation + API Reference diff --git a/www/data/api.ts b/www/data/api.ts new file mode 100644 index 0000000..8c2c0cb --- /dev/null +++ b/www/data/api.ts @@ -0,0 +1,23 @@ +import { doc, load } from "../utils/doc.ts"; + +const modules = ["mod", "namespaces", "rdf", "sparql"]; + +async function loadDoc(module: string) { + const file = import.meta.resolve(`../../${module}.ts`); + return await doc(file, { printImportMapDiagnostics: true, load }); +} + +export async function getApi() { + const promises = modules.map(async (module) => { + const x = await loadDoc(module); + return { + kind: "module", + path: `/${module}.ts`, + items: x, + }; + }); + + return await Promise.all(promises); +} + +export const api = await getApi(); diff --git a/www/deno.json b/www/deno.json index 284cae6..a6001d9 100644 --- a/www/deno.json +++ b/www/deno.json @@ -19,15 +19,15 @@ "**/_fresh/*" ], "imports": { - "$fresh/": "https://deno.land/x/fresh@1.5.4/", - "preact": "https://esm.sh/preact@10.18.1", - "preact/": "https://esm.sh/preact@10.18.1/", - "preact-render-to-string": "https://esm.sh/*preact-render-to-string@6.2.2", + "$fresh/": "https://deno.land/x/fresh@1.6.1/", + "preact": "https://esm.sh/preact@10.19.2", + "preact/": "https://esm.sh/preact@10.19.2/", "@preact/signals": "https://esm.sh/*@preact/signals@1.2.1", "@preact/signals-core": "https://esm.sh/*@preact/signals-core@1.5.0", "twind": "https://esm.sh/twind@0.16.19", "twind/": "https://esm.sh/twind@0.16.19/", - "$std/": "https://deno.land/std@0.193.0/" + "$std/": "https://deno.land/std@0.193.0/", + "$doc_components/": "https://deno.land/x/deno_doc_components@0.4.14/" }, "compilerOptions": { "jsx": "react-jsx", diff --git a/www/dev.ts b/www/dev.ts index ae73946..2d85d6c 100644 --- a/www/dev.ts +++ b/www/dev.ts @@ -1,8 +1,5 @@ #!/usr/bin/env -S deno run -A --watch=static/,routes/ import dev from "$fresh/dev.ts"; -import config from "./fresh.config.ts"; -import "$std/dotenv/load.ts"; - -await dev(import.meta.url, "./main.ts", config); +await dev(import.meta.url, "./main.ts"); diff --git a/www/doc.config.ts b/www/doc.config.ts new file mode 100644 index 0000000..4de114b --- /dev/null +++ b/www/doc.config.ts @@ -0,0 +1,52 @@ +import { Configuration } from "$doc_components/services.ts"; + +const toApiPath = (url: URL) => { + const chunks = url.pathname.split("/"); + const module = chunks[chunks.length - 1]; + return `/api/${module}`; +}; + +const toRemoteSourceUrl = (target: string, line?: number) => { + const prefix = "https://deno.land/x/ldkit/library/"; + const lineSuffix = line ? `#L${line}` : ""; + const suffix = `?source=${lineSuffix}`; + const chunks = target.split("/library/"); + if (chunks.length > 1) { + return `${prefix}${chunks[1]}${suffix}`; + } + const smallChunks = target.split("/"); + const rootModule = smallChunks[smallChunks.length - 1]; + return `${prefix}${rootModule}${suffix}`; +}; + +const ignoredSymbols = [ + "Date", + "Iterable", + "Partial", + "Promise", + "Response", + "Unite", + "T", + "QueryEngineProxy", + "QueryBuilder", + "ExpandedSchema", +]; + +export default { + resolveHref(current, symbol, _namespace, property) { + const path = toApiPath(current); + return symbol + ? (property ? `${path}~${symbol}.${property}` : `${path}~${symbol}`) + : `${path}`; + }, + lookupHref(current, namespace, symbol) { + if (ignoredSymbols.includes(symbol)) { + return undefined; + } + const path = toApiPath(current); + return namespace ? `${path}~${namespace}.${symbol}` : `${path}~${symbol}`; + }, + resolveSourceHref(target, line) { + return toRemoteSourceUrl(target, line); + }, +} satisfies Configuration; diff --git a/www/fresh.gen.ts b/www/fresh.gen.ts index 9dbb7f5..9d6794a 100644 --- a/www/fresh.gen.ts +++ b/www/fresh.gen.ts @@ -2,22 +2,26 @@ // This file SHOULD be checked into source version control. // This file is automatically updated during development when running `dev.ts`. -import * as $0 from "./routes/[name].tsx"; -import * as $1 from "./routes/_404.tsx"; -import * as $2 from "./routes/docs/[...slug].tsx"; -import * as $3 from "./routes/gfm.css.ts"; -import * as $4 from "./routes/index.tsx"; +import * as $_name_ from "./routes/[name].tsx"; +import * as $_404 from "./routes/_404.tsx"; +import * as $api_slug_ from "./routes/api/[...slug].tsx"; +import * as $docs_slug_ from "./routes/docs/[...slug].tsx"; +import * as $gfm_css from "./routes/gfm.css.ts"; +import * as $index from "./routes/index.tsx"; + +import { type Manifest } from "$fresh/server.ts"; const manifest = { routes: { - "./routes/[name].tsx": $0, - "./routes/_404.tsx": $1, - "./routes/docs/[...slug].tsx": $2, - "./routes/gfm.css.ts": $3, - "./routes/index.tsx": $4, + "./routes/[name].tsx": $_name_, + "./routes/_404.tsx": $_404, + "./routes/api/[...slug].tsx": $api_slug_, + "./routes/docs/[...slug].tsx": $docs_slug_, + "./routes/gfm.css.ts": $gfm_css, + "./routes/index.tsx": $index, }, islands: {}, baseUrl: import.meta.url, -}; +} satisfies Manifest; export default manifest; diff --git a/www/main.ts b/www/main.ts index 675f529..a507b3b 100644 --- a/www/main.ts +++ b/www/main.ts @@ -10,4 +10,9 @@ import { start } from "$fresh/server.ts"; import manifest from "./fresh.gen.ts"; import config from "./fresh.config.ts"; +import { setup } from "$doc_components/services.ts"; +import docConfig from "./doc.config.ts"; + +await setup(docConfig); + await start(manifest, config); diff --git a/www/routes/api/[...slug].tsx b/www/routes/api/[...slug].tsx new file mode 100644 index 0000000..12a1d05 --- /dev/null +++ b/www/routes/api/[...slug].tsx @@ -0,0 +1,127 @@ +import { Handlers, PageProps } from "$fresh/server.ts"; +import { ModuleDoc } from "https://deno.land/x/deno_doc_components@0.4.14/doc/module_doc.tsx"; +import { + DocPageNavItem, + ModuleIndexPanel, +} from "$doc_components/doc/module_index_panel.tsx"; +import { SymbolDoc } from "$doc_components/doc/symbol_doc.tsx"; + +import { api } from "../../data/api.ts"; +import { App } from "../../components/App.tsx"; + +interface Data { + base: URL; + modules: typeof api; + current?: string; + currentSymbol?: string; + currentProperty?: string; +} + +export const handler: Handlers = { + GET(_req, ctx) { + const base = new URL(_req.url); + base.pathname = "/api"; + const slug = ctx.params.slug; + + const match = slug.match( + /^([a-zA-Z_\/.]+)?(~[a-zA-Z0-9_]+)?(\.[a-zA-Z_.]+)?$/, + ); + + if (match === null) { + return ctx.renderNotFound(); + } + + const current = match[1] ? `/${match[1]}` : undefined; + const currentSymbol = match[2] ? match[2].substring(1) : undefined; + const currentProperty = match[3] ? match[3].substring(1) : undefined; + + const resp = ctx.render({ + base, + current, + currentSymbol, + currentProperty, + modules: api, + }); + return resp; + }, +}; + +const createTitle = (props: PageProps) => { + const { current, currentSymbol, currentProperty } = props.data; + + const chunks = ["API"]; + + if (current) { + chunks.push(` ยท ${current}`); + } + + if (currentSymbol) { + chunks.push(`~${currentSymbol}`); + } + + if (currentProperty) { + chunks.push(`.${currentProperty}`); + } + return chunks.join(""); +}; + +export default function ApiPage(props: PageProps) { + return ( + +
    + + +
    +
    + ); +} + +function Sidebar(props: PageProps) { + const url = new URL("https://deno.land/x/ldkit"); + return ( + + {props.data.modules as DocPageNavItem[]} + + ); +} + +function Content(props: PageProps) { + const { base, current, currentSymbol, currentProperty } = props.data; + + const url = new URL("https://deno.land/x/ldkit"); + + let module = api.find((item) => item.path === current); + if (!module) { + module = api[0]; + } + + url.pathname = `${url.pathname}${module.path}`; + + if (!currentSymbol) { + return ( +
    + + {module.items} + +
    + ); + } + + let symbol = module.items.find((item) => item.name === currentSymbol); + if (!symbol) { + symbol = module.items[0]; + } + + return ( +
    + + {[symbol]} + +
    + ); +} diff --git a/www/twind.config.ts b/www/twind.config.ts index 8f2633f..242f72d 100644 --- a/www/twind.config.ts +++ b/www/twind.config.ts @@ -1,8 +1,16 @@ import { Options } from "$fresh/plugins/twind.ts"; +import { plugins } from "$doc_components/twind.config.ts"; +import * as twColors from "twind/colors"; export default { selfURL: import.meta.url, + plugins, theme: { + colors: { + transparent: "transparent", + current: "currentColor", + ...twColors, + }, screens: { "sm": "640px", "md": "768px", diff --git a/www/utils/doc.ts b/www/utils/doc.ts new file mode 100644 index 0000000..61c4350 --- /dev/null +++ b/www/utils/doc.ts @@ -0,0 +1,14 @@ +export { doc } from "https://deno.land/x/deno_doc@0.86.0/mod.ts"; +export type { DocNode } from "https://deno.land/x/deno_doc@0.86.0/types.d.ts"; +import { load as defaultLoad } from "https://deno.land/x/deno_graph@0.53.0/loader.ts"; + +export function load(specifier: string) { + if (specifier.startsWith("npm:")) { + specifier = `https://esm.sh/${specifier.slice(4)}`; + /*return Promise.resolve({ + "kind": "external" as const, + "specifier": specifier, + });*/ + } + return defaultLoad(specifier); +}