diff --git a/README.md b/README.md index 36b0b52..9874843 100644 --- a/README.md +++ b/README.md @@ -12,21 +12,31 @@ The packages are library-first. Importing a package describes or creates values. │ └── @okikio/comunica │ ├── @okikio/vocab - └── @okikio/triplestore + ├── @okikio/triplestore + ├── @okikio/rdf/jsonld + ├── @okikio/rdf/canon + ├── @okikio/rdf/xml + ├── @okikio/rdf/rdfa + └── @okikio/rdf/microdata ``` -The repository targets current RDF 1.2 semantics while keeping draft-dependent behavior explicit. SPARQL and SHACL 1.2 features are versioned because those specifications are still evolving. +The repository targets the RDF 1.2 syntax profiles listed in `support.json` while keeping draft-dependent behavior explicit. A profile is not a release claim until its pinned conformance suite passes without failures or skips. SPARQL and SHACL 1.2 features remain separately scoped because those specifications are still evolving. ## Packages -| Package | Responsibility | Runtime dependency posture | -| --- | --- | --- | -| `@okikio/rdf` | RDF terms, datasets, parsers, ontology and shape models | root module imports no third-party processor; processor subpaths are isolated | -| `@okikio/sparql` | SPARQL construction, lexical inspection, result contracts, HTTP protocol client | core depends only on `@okikio/rdf` | -| `@okikio/vocab` | Ontology-to-TypeScript compiler and generated vocabulary runtime | depends on `@okikio/rdf` | -| `@okikio/triplestore` | Crash-recoverable persistent RDF dataset | depends on `@okikio/rdf`; borrows a structural filesystem | -| `@okikio/oxigraph` | Adapter from a caller-owned Oxigraph `Store` to the SPARQL query contract | Oxigraph remains caller-owned | -| `@okikio/comunica` | Adapter from a caller-owned Comunica `QueryEngine` to the SPARQL query contract | Comunica remains caller-owned | +| Package | Responsibility | Runtime dependency posture | +| ----------------------- | ------------------------------------------------------------------------------- | -------------------------------------------------------------- | +| `@okikio/rdf` | RDF terms, datasets, native parsers, ontology and shape models | no third-party runtime implementation | +| `@okikio/sparql` | SPARQL construction, lexical inspection, result contracts, HTTP protocol client | depends only on `@okikio/rdf` | +| `@okikio/vocab` | Ontology-to-TypeScript compiler and generated vocabulary runtime | depends only on `@okikio/rdf` | +| `@okikio/triplestore` | Crash-recoverable persistent RDF dataset | depends only on `@okikio/rdf`; borrows a structural filesystem | +| `@okikio/rdf/jsonld` | Native JSON-LD 1.1 processing | no third-party runtime implementation | +| `@okikio/rdf/canon` | Native RDFC-1.0 canonicalization | no third-party runtime implementation | +| `@okikio/rdf/xml` | Native RDF/XML 1.1/1.2 parsing | no third-party runtime implementation | +| `@okikio/rdf/rdfa` | Native RDFa 1.1 extraction | no third-party runtime implementation | +| `@okikio/rdf/microdata` | Native Microdata-to-RDF extraction | no third-party runtime implementation | +| `@okikio/oxigraph` | Adapter from a caller-owned Oxigraph `Store` to the SPARQL query contract | external engine is explicit and caller-owned | +| `@okikio/comunica` | Adapter from a caller-owned Comunica `QueryEngine` to the SPARQL query contract | external engine is explicit and caller-owned | ## RDF @@ -47,31 +57,43 @@ for (const quad of data.match(product)) { } ``` -The root exports the semantic model only. Syntax-specific code is opt-in: +The root exports the semantic model only. Project-owned syntax and semantic capabilities use explicit RDF subpaths: ```ts import * as nquads from '@okikio/rdf/nquads' import * as turtle from '@okikio/rdf/turtle' +import * as ontology from '@okikio/rdf/ontology' +import * as shape from '@okikio/rdf/shape' +``` + +Additional RDF standards are native `@okikio/rdf` subpaths. Competitor implementations are used only by conformance, differential tests, and benchmarks: + +```ts import * as jsonld from '@okikio/rdf/jsonld' -import * as canon from '@okikio/rdf/canon' +import * as rdfc from '@okikio/rdf/canon' +import * as rdfxml from '@okikio/rdf/xml' +import * as rdfa from '@okikio/rdf/rdfa' +import * as microdata from '@okikio/rdf/microdata' ``` -Implemented format and semantic subpaths currently include: +Current format and semantic surfaces include: ```text @okikio/rdf/ntriples N-Triples 1.2 parsing and serialization @okikio/rdf/nquads N-Quads 1.2 parsing and serialization @okikio/rdf/turtle Turtle 1.2 streaming parser and conservative serializer @okikio/rdf/trig TriG 1.2 streaming parser and conservative serializer -@okikio/rdf/jsonld JSON-LD processor adapter with bounded document loading -@okikio/rdf/xml RDF/XML streaming adapter -@okikio/rdf/rdfa RDFa 1.1 streaming adapter -@okikio/rdf/microdata Microdata-to-RDF streaming adapter -@okikio/rdf/canon RDFC-1.0 canonicalization @okikio/rdf/ontology RDFS/OWL ontology interpretation model @okikio/rdf/shape loss-preserving SHACL shape model +@okikio/rdf/jsonld native JSON-LD 1.1 processor with bounded document loading +@okikio/rdf/canon native RDFC-1.0 canonicalization +@okikio/rdf/xml native RDF/XML parser +@okikio/rdf/rdfa native RDFa 1.1 extractor +@okikio/rdf/microdata native Microdata-to-RDF extractor ``` +The four core packages (`@okikio/rdf`, `@okikio/sparql`, `@okikio/vocab`, and `@okikio/triplestore`) cannot import third-party runtime implementations. External RDF/SPARQL implementations are allowed only in explicit interoperability packages such as `@okikio/oxigraph` and `@okikio/comunica`, or in tests, conformance suites, and benchmarks used as independent comparison oracles. + `@okikio/rdf` uses `Iterable`, `AsyncIterable`, `ReadableStream`, `AbortSignal`, and explicit disposal where those shapes match the workload. RDF/JS is an interoperability target, not a required core dependency. ## SPARQL @@ -93,9 +115,9 @@ console.log(query.build().value) Execution is explicit: ```ts -import { createClient } from '@okikio/sparql/http' +import * as http from '@okikio/sparql/http' -const client = createClient({ endpoint: 'https://example.com/sparql' }) +const client = http.create({ endpoint: 'https://example.com/sparql' }) for await (const row of await client.queryBindings(query)) { console.log(row.get('name')?.value) @@ -106,9 +128,9 @@ The result modes stay separate: ```ts client.queryBindings(query) // SELECT -client.queryQuads(query) // CONSTRUCT / DESCRIBE -client.queryBoolean(query) // ASK -client.update(update) // UPDATE +client.queryQuads(query) // CONSTRUCT / DESCRIBE +client.queryBoolean(query) // ASK +client.update(update) // UPDATE ``` This prevents graph results from being coerced into binding rows and keeps RDF values as RDF terms instead of silently converting datatypes to JavaScript primitives. @@ -120,13 +142,7 @@ This prevents graph results from being coerced into binding rows and keeps RDF v Generated vocabulary data uses direct, tree-shakeable imports. Runtime values, schemas, and types use PascalCase when the vocabulary term is PascalCase. ```ts -import { - Product, - ProductSchema, - type ProductType, - name, - offers, -} from '@okikio/vocab/schema' +import { name, offers, Product, ProductSchema, type ProductType } from '@okikio/vocab/schema' ``` This avoids awkward APIs such as `schema.ProductSchema` while still allowing operation-oriented namespaces elsewhere. @@ -171,18 +187,18 @@ The engine integration packages wrap resources that the caller creates and owns. ```ts import { Store } from 'oxigraph' -import { createClient } from '@okikio/oxigraph' +import * as oxigraph from '@okikio/oxigraph' const store = new Store() -const client = createClient(store) +const client = oxigraph.create(store) ``` ```ts import { QueryEngine } from '@comunica/query-sparql' -import { createClient } from '@okikio/comunica' +import * as comunica from '@okikio/comunica' const engine = new QueryEngine() -const client = createClient(engine, { +const client = comunica.create(engine, { context: () => ({ sources: [/* caller-selected sources */] }), }) ``` @@ -250,30 +266,19 @@ deno task vocab:schema ## Current implementation status -Implemented and locally validated without optional upstream packages: - -- RDF 1.2-native core terms, triple terms, directional literals, Dataset indexes -- streaming N-Triples/N-Quads -- streaming Turtle/TriG semantic parser -- RDF ontology and SHACL shape IRs -- SPARQL builder migration and explicit result-mode contracts -- SPARQL HTTP protocol client -- source-ranged SPARQL syntax inspection -- vocabulary compiler/runtime/bootstrap Schema.org surface -- crash-recoverable triplestore -- caller-owned Oxigraph and Comunica adapters -- lifecycle adapters for JSON-LD, RDF/XML, RDFa, Microdata, and RDFC-1.0 - -Still requiring canonical upstream validation before release claims: - -- Deno formatting, linting, type checks, and package-local tests on a Deno/JSR-capable host -- W3C RDF/Turtle/TriG/SPARQL/SHACL conformance corpora -- real `jsonld`, `rdf-canonize`, RDF/XML, RDFa, and Microdata dependency integration -- real Oxigraph Wasm and Comunica engine integration -- complete generated Schema.org vocabulary -- package builds and JSR/npm publication artifacts - -See [`docs/standard-schema.md`](./docs/standard-schema.md) for generated validation, [`docs/vocabulary-generation.md`](./docs/vocabulary-generation.md) for the `ttl-to-ts` replacement, [`docs/testing.md`](./docs/testing.md) for permanent test ownership, [`docs/migration.md`](./docs/migration.md) for intentional 0.0.2 API changes, [`docs/benchmarks.md`](./docs/benchmarks.md) for benchmark-derived decisions, and [`VALIDATION.md`](./VALIDATION.md) for the exact passed and blocked release gates. +The repository now contains the release evidence infrastructure, not merely a plan for it: + +- pinned official RDF 1.2/1.1, JSON-LD 1.1, JSON-LD Framing, RDFC-1.0, RDFa 1.1, and Microdata-to-RDF runners; +- fast-check properties for parser, Dataset, SPARQL, vocabulary, and persistent-store invariants; +- real Oxigraph/Comunica engine integration plus Testcontainers protocol/fault tests; +- SPARQL GET/form/direct POST modes, protocol dataset parameters, and a separate Graph Store API; +- competitive Mitata parser, Dataset, and adapter baselines with semantic oracles and JSON evidence output; +- clean package/consumer and browser-bundle isolation gates; +- a machine-readable `support.json` that prevents interpretation-only or unsupported features from being advertised as conformance. + +The checked-in Schema.org module remains a bootstrap surface until `deno task vocab:schema` is run in the canonical release environment. A committed Deno lockfile is also required before publication. The current sandbox cannot resolve the registry graph, so the lockfile is not fabricated. + +See [`docs/conformance.md`](./docs/conformance.md) for standards evidence, [`docs/standard-schema.md`](./docs/standard-schema.md) for generated validation, [`docs/vocabulary-generation.md`](./docs/vocabulary-generation.md) for vocabulary compilation, [`docs/testing.md`](./docs/testing.md) for test ownership, [`docs/migration.md`](./docs/migration.md) for intentional API changes, [`docs/benchmarks.md`](./docs/benchmarks.md) for benchmark design, and [`VALIDATION.md`](./VALIDATION.md) for executed versus pending release gates. ## License diff --git a/docs/architecture.md b/docs/architecture.md index 6a48764..a0f0422 100644 --- a/docs/architecture.md +++ b/docs/architecture.md @@ -4,45 +4,60 @@ **Repository:** `okikio/sparql-client` **Runtime direction:** Deno 2 first, strict TypeScript, ESM, browser/Node/Bun compatible where the capability exists -This document describes the architecture that the current source is intended to implement. Historical design exploration is retained under `docs/research/architecture-design-20260814.md`. +This document describes the architecture that the current source is intended to implement. Historical design exploration is retained under `docs/research/architecture-design.md`. ## Goals -The repository provides a complete RDF programming model, SPARQL construction and execution contracts, generated vocabulary tooling, optional query-engine adapters, and persistent RDF storage without turning them into one monolithic runtime. +The repository provides a dependency-free RDF core, native RDF syntax and JSON-LD processors, SPARQL construction and execution contracts, generated vocabulary tooling, optional query-engine adapters, and persistent RDF storage without turning them into one monolithic runtime. Conformance claims are limited to the evidence-backed profiles in `support.json`. -The design has six primary rules: +The design has seven primary rules: 1. RDF semantics do not depend on a query engine. -2. SPARQL construction does not own network or engine execution. -3. Generated vocabularies depend on RDF terms and ontology data, not on SPARQL. -4. Engine integrations depend on the generic SPARQL contract, not the reverse. -5. Persistent RDF storage borrows a filesystem capability instead of naming one runtime backend in the package identity. -6. Format parsers and draft standards can evolve behind focused seams without forcing unrelated public APIs to change. +2. Core packages do not import third-party runtime implementations. +3. An external RDF/SPARQL implementation is allowed only in an explicit interoperability package such as Oxigraph or Comunica, or in tests, conformance suites, and benchmarks. +4. SPARQL construction does not own network or engine execution. +5. Generated vocabularies depend on RDF terms and ontology data, not on SPARQL. +6. Engine integrations depend on the generic SPARQL contract, not the reverse. +7. Persistent RDF storage borrows a filesystem capability instead of naming one runtime backend in the package identity. ## Package graph ```text - @okikio/rdf - / | \ - / | \ - v v v - @okikio/sparql @okikio/vocab @okikio/triplestore - |\ - | \ - v v - @okikio/oxigraph @okikio/comunica + @okikio/rdf + +--------------------+--------------------+ + | | | + v v v + @okikio/sparql @okikio/vocab @okikio/triplestore + / \ + v v +@okikio/oxigraph @okikio/comunica + +Native standards processors are subpaths of @okikio/rdf: + +@okikio/rdf/jsonld +@okikio/rdf/canon +@okikio/rdf/xml +@okikio/rdf/rdfa +@okikio/rdf/microdata ``` Allowed direct package dependencies: -| Package | Depends on | -| --- | --- | -| `@okikio/rdf` | no project package | -| `@okikio/sparql` | `@okikio/rdf` | -| `@okikio/vocab` | `@okikio/rdf` | -| `@okikio/triplestore` | `@okikio/rdf` | -| `@okikio/oxigraph` | `@okikio/rdf`, `@okikio/sparql` | -| `@okikio/comunica` | `@okikio/rdf`, `@okikio/sparql` | +| Package | Depends on | +| ----------------------- | ----------------------------------------------------------------- | +| `@okikio/rdf` | no runtime package | +| `@okikio/sparql` | `@okikio/rdf` | +| `@okikio/vocab` | `@okikio/rdf` | +| `@okikio/triplestore` | `@okikio/rdf` | +| `@okikio/rdf/jsonld` | native subpath of `@okikio/rdf` | +| `@okikio/rdf/canon` | native subpath of `@okikio/rdf` | +| `@okikio/rdf/xml` | native subpath of `@okikio/rdf` | +| `@okikio/rdf/rdfa` | native subpath of `@okikio/rdf` | +| `@okikio/rdf/microdata` | native subpath of `@okikio/rdf` | +| `@okikio/oxigraph` | `@okikio/rdf`, `@okikio/sparql`; caller supplies the engine store | +| `@okikio/comunica` | `@okikio/rdf`, `@okikio/sparql`; caller supplies the query engine | + +The core set is `@okikio/rdf`, `@okikio/sparql`, `@okikio/vocab`, and `@okikio/triplestore`. These packages may depend on each other only in the directions shown above. They cannot import an npm, JSR, or other third-party runtime implementation. Tests, conformance runners, and benchmarks can import competitors because those imports do not become production implementation dependencies. `@okikio/sparql` must not depend on a generated vocabulary or a concrete engine. That would make a generic syntax library depend on one ontology/compiler or one execution implementation. @@ -70,25 +85,33 @@ Allowed direct package dependencies: The root module does not import a syntax processor just because the processor exists in the npm package. -### Format subpaths +### Native subpaths + +Project-owned RDF capabilities remain inside `@okikio/rdf`: ```text @okikio/rdf/ntriples @okikio/rdf/nquads @okikio/rdf/turtle @okikio/rdf/trig +@okikio/rdf/ontology +@okikio/rdf/shape +@okikio/rdf/stream +``` + +N-Triples, N-Quads, Turtle, and TriG use project-owned parsers. The package manifest contains no third-party runtime implementation dependency. + +The remaining standards processors are also project-owned subpaths: + +```text @okikio/rdf/jsonld +@okikio/rdf/canon @okikio/rdf/xml @okikio/rdf/rdfa @okikio/rdf/microdata -@okikio/rdf/canon -@okikio/rdf/ontology -@okikio/rdf/shape ``` -N-Triples, N-Quads, Turtle, and TriG use project-owned parsers. JSON-LD, RDFC-1.0 canonicalization, RDF/XML, RDFa, and Microdata currently use focused upstream processors behind project-owned contracts. - -The npm package has one dependency manifest, so installing `@okikio/rdf` installs those processor dependencies today. The important import guarantee is narrower: importing the root module does not initialize or import those optional processor implementations. +These subpaths own their algorithms directly. They do not delegate through dynamic imports, generic processor injection, or hidden optional dependencies. External implementations remain test and benchmark references only. ### Parser lifecycle @@ -119,11 +142,11 @@ Generic RDFS/OWL interpretation belongs under `@okikio/rdf/ontology`. The ontology model distinguishes semantic relationships from validation rules. In particular, RDFS domain/range statements do not mean that a JSON property is required. -Unknown or not-yet-normalized ontology assertions are retained so future readers can add semantics without the older reader irreversibly discarding data. +Unknown or not-yet-normalized ontology assertions are retained so future inspectors can add semantics without the earlier inspector irreversibly discarding data. ### Shape model -`@okikio/rdf/shape` is version-aware and loss-preserving. It reads known SHACL structure while retaining unsupported, extension, and malformed-known assertions with diagnostics where appropriate. +`@okikio/rdf/shape` is version-aware and loss-preserving. It inspects known SHACL structure while retaining unsupported, extension, and malformed-known assertions with diagnostics where appropriate. SHACL 1.2 is evolving as a family of drafts. The IR therefore does not pretend that one current parser is a frozen universal validator. @@ -150,11 +173,11 @@ It does not own a concrete database or query engine. The public model keeps grammar roles distinct: ```text -SparqlTerm one RDF/SPARQL term or legal predicate path -SparqlExpr one expression -PatternValue one graph-pattern fragment -SparqlQuery one complete query document -SparqlUpdate one complete Update document +SparqlTermType one RDF/SPARQL term or legal predicate path +SparqlExprType one expression +PatternValueType one graph-pattern fragment +SparqlQueryType one complete query document +SparqlUpdateType one complete Update document ``` Complete documents are not embeddable fragments. A complete SELECT query cannot accidentally be interpolated where a term is legal. A complete update cannot be sent through `queryBindings()` by structural accident. @@ -170,7 +193,7 @@ Generated vocabulary terms therefore compose without a `@okikio/sparql -> @okiki ```ts import * as rdf from '@okikio/rdf' import * as sparql from '@okikio/sparql' -import { Product, name } from '@okikio/vocab/schema' +import { name, Product } from '@okikio/vocab/schema' const query = sparql.select(['?product', '?name']).where( sparql.triple('?product', rdf.namedNode(rdf.RDF.type), Product), @@ -250,7 +273,7 @@ The runtime is open-world and does not convert ontology domain/range metadata in ```text compile() reusable library orchestration -read() RDF ontology -> compiler model +inspect() RDF ontology -> compiler model name planning deterministic symbols/collisions emit() compiler model -> TypeScript + manifest .mise task files/network/process orchestration only @@ -320,17 +343,17 @@ Neither adapter disposes the caller-created engine/store unless a future public The repository uses several explicit seams so changing specifications do not force a monolithic rewrite. -| Evolving area | Stable seam | -| --- | --- | -| RDF serialization | RDF quad/term model | -| JSON-LD processing | project loader/result contract around focused processor | -| RDFS/OWL vocabulary semantics | loss-preserving ontology model | -| SHACL drafts | versioned, loss-preserving shape model | -| SPARQL 1.2 grammar | source-ranged syntax event/token layer | -| query engines | `Queryable` | -| vocabulary source formats | quad-source compiler input | -| schema libraries | Standard Schema structural protocol | -| persistent filesystems | structural filesystem capability | +| Evolving area | Stable seam | +| ----------------------------- | ------------------------------------------------------- | +| RDF serialization | RDF quad/term model | +| JSON-LD processing | project loader/result contract around focused processor | +| RDFS/OWL vocabulary semantics | loss-preserving ontology model | +| SHACL drafts | versioned, loss-preserving shape model | +| SPARQL 1.2 grammar | source-ranged syntax event/token layer | +| query engines | `Queryable` | +| vocabulary source formats | quad-source compiler input | +| schema libraries | Standard Schema structural protocol | +| persistent filesystems | structural filesystem capability | ## Cancellation and ownership flow @@ -340,7 +363,7 @@ caller AbortSignal +--> parser source read/cancel +--> HTTP request +--> Comunica stream destroy - +--> supported external processors + +--> native standards processors caller resource | @@ -413,7 +436,12 @@ The intended main entry points are: ```text @okikio/rdf -@okikio/rdf/{ntriples,nquads,turtle,trig,jsonld,xml,rdfa,microdata,canon,ontology,shape} +@okikio/rdf/{ntriples,nquads,turtle,trig,ontology,shape,stream} +@okikio/rdf/jsonld +@okikio/rdf/canon +@okikio/rdf/xml +@okikio/rdf/rdfa +@okikio/rdf/microdata @okikio/sparql @okikio/sparql/http @okikio/sparql/syntax diff --git a/docs/implementation.md b/docs/implementation.md index 1885136..5ae0e1a 100644 --- a/docs/implementation.md +++ b/docs/implementation.md @@ -16,7 +16,7 @@ It should be read together with: ## Package model -The repository has six independently usable packages: +The repository has six independently publishable packages. `@okikio/rdf` also exposes focused native standards subpaths: ```text @okikio/rdf @@ -25,11 +25,17 @@ The repository has six independently usable packages: | +--> @okikio/comunica +--> @okikio/vocab +--> @okikio/triplestore + +--> @okikio/rdf/jsonld + +--> @okikio/rdf/canon + +--> @okikio/rdf/xml + +--> @okikio/rdf/rdfa + +--> @okikio/rdf/microdata ``` This dependency direction is intentional: -- RDF defines terms, datasets, parsers, ontology/shape models, and interoperability. +- RDF defines terms, datasets, project-owned parsers, ontology/shape models, and interoperability. +- JSON-LD, RDFC, RDF/XML, RDFa, and Microdata are native `@okikio/rdf` subpaths. External processors are used only as conformance, differential-test, or benchmark references. - SPARQL depends on RDF terms but not on a vocabulary generator or query engine. - Vocab compiles RDF ontology models into developer-facing code. - Engine packages adapt external engines to SPARQL's generic query/update contract. @@ -37,6 +43,8 @@ This dependency direction is intentional: Generated vocabulary constants are RDF `NamedNode` values. They therefore compose with SPARQL directly without adding a `@okikio/vocab` dependency to `@okikio/sparql`. +The core production graph has a dependency firewall. `@okikio/rdf`, `@okikio/sparql`, `@okikio/vocab`, and `@okikio/triplestore` cannot import third-party runtime implementations. External competitors remain valid conformance and benchmark oracles, and explicit adapter packages may own the dependency they name. + ## Standard Schema Standard Schema is now a first-class generated vocabulary contract rather than an undocumented implementation detail. @@ -54,8 +62,8 @@ Example generated surface: ```ts import { Product, - ProductSchema, type ProductPropertiesType, + ProductSchema, type ProductType, } from '@okikio/vocab/schema' ``` @@ -89,7 +97,7 @@ RDF serialization / Dataset / store @okikio/rdf/ontology | v - @okikio/vocab.read + @okikio/vocab.inspect | v deterministic name planning @@ -236,7 +244,7 @@ New or materially expanded permanent coverage includes: - ontology and SHACL loss preservation - SPARQL grammar roles, query builders, update builders, Cypher/object helpers, results, HTTP, and package composition - Standard Schema and Standard JSON Schema -- vocabulary compile/read/name/runtime behavior +- vocabulary compile/inspect/name/runtime behavior - triplestore format/recovery contracts - engine adapter query/update and ownership behavior @@ -340,187 +348,8 @@ Still required on an appropriate host: SPARQL 1.2 and SHACL 1.2 are draft families as of this implementation date, so their supported feature profiles must remain versioned and testable rather than being baked into an unversioned claim of final conformance. -## Changed-file appendix - -The appendix is generated from the final Git diff so review can distinguish the tracked `.agents/` deletion from functional/package/documentation changes. - - - -### Deleted (61) - -- `.agents/benchmark.ts` -- `.agents/canon.test.ts` -- `.agents/compact.test.ts` -- `.agents/deno.d.ts` -- `.agents/engines.test.ts` -- `.agents/generate-bootstrap-schema.ts` -- `.agents/generate-real-vocab.ts` -- `.agents/generated-narrative.ts` -- `.agents/generated-tsconfig.json` -- `.agents/html-semantic.test.ts` -- `.agents/jsonld.test.ts` -- `.agents/jsonld.ts` -- `.agents/memory-fs.ts` -- `.agents/microdata-rdf-streaming-parser.ts` -- `.agents/mitata.ts` -- `.agents/node-test.ts` -- `.agents/ontology.test.ts` -- `.agents/packs/comunica.json` -- `.agents/packs/oxigraph.json` -- `.agents/packs/rdf.json` -- `.agents/packs/sparql.json` -- `.agents/packs/triplestore.json` -- `.agents/packs/vocab.json` -- `.agents/public.test.ts` -- `.agents/rdf-canonize.ts` -- `.agents/rdf.test.ts` -- `.agents/rdfa-streaming-parser.ts` -- `.agents/rdfxml-streaming-parser.ts` -- `.agents/recovery-profile.ts` -- `.agents/results/benchmark.json` -- `.agents/results/benchmark.stdout.json` -- `.agents/results/final-benchmark.json` -- `.agents/results/final-benchmark.stdout.json` -- `.agents/results/final-check.txt` -- `.agents/results/final-generate-real.txt` -- `.agents/results/final-generated-check.txt` -- `.agents/results/final-host-tests.tap` -- `.agents/results/final-pack.json` -- `.agents/results/final-package-tests.tap` -- `.agents/results/package-tests.tap` -- `.agents/results/release-check-after-ledger.txt` -- `.agents/results/release-check.txt` -- `.agents/results/release-generate-real.txt` -- `.agents/results/release-generated-check.txt` -- `.agents/results/release-host-tests.tap` -- `.agents/results/release-pack.json` -- `.agents/results/release-package-tests.tap` -- `.agents/results/release-stale-api.txt` -- `.agents/results/types-after.stdout.json` -- `.agents/results/types-before-schema-composition.json` -- `.agents/results/types.json` -- `.agents/results/types.stdout.json` -- `.agents/shape.test.ts` -- `.agents/sparql.test.ts` -- `.agents/std-expect.ts` -- `.agents/triplestore.test.ts` -- `.agents/tsconfig.json` -- `.agents/type-benchmark.ts` -- `.agents/vocab-real.test.ts` -- `.agents/vocab.test.ts` -- `.agents/xml.test.ts` - -### Modified (74) - -- `.gitignore` -- `.mise/tasks/schema.ts` -- `.mise/tasks/vocab.ts` -- `AGENTS.md` -- `README.md` -- `VALIDATION.md` -- `deno.json` -- `docs/architecture.md` -- `docs/benchmarks.md` -- `docs/implementation.md` -- `docs/quick-start.md` -- `docs/sparql-mapping.md` -- `package.json` -- `packages/comunica/README.md` -- `packages/comunica/mod.ts` -- `packages/comunica/mod_test.ts` -- `packages/oxigraph/README.md` -- `packages/oxigraph/mod.ts` -- `packages/oxigraph/mod_test.ts` -- `packages/rdf/README.md` -- `packages/rdf/canon/mod.ts` -- `packages/rdf/compact.ts` -- `packages/rdf/dataset.ts` -- `packages/rdf/dataset_test.ts` -- `packages/rdf/factory.ts` -- `packages/rdf/jsonld/loader.ts` -- `packages/rdf/jsonld/mod.ts` -- `packages/rdf/line.ts` -- `packages/rdf/microdata/mod.ts` -- `packages/rdf/microdata/mod_test.ts` -- `packages/rdf/ontology/index.ts` -- `packages/rdf/ontology/read.ts` -- `packages/rdf/rdfa/mod.ts` -- `packages/rdf/rdfa/mod_test.ts` -- `packages/rdf/shape/index.ts` -- `packages/rdf/shape/list.ts` -- `packages/rdf/shape/path.ts` -- `packages/rdf/shape/read.ts` -- `packages/rdf/term.ts` -- `packages/rdf/text.ts` -- `packages/rdf/transform.ts` -- `packages/rdf/write.ts` -- `packages/rdf/xml/mod.ts` -- `packages/rdf/xml/mod_test.ts` -- `packages/sparql/README.md` -- `packages/sparql/builder.ts` -- `packages/sparql/client.ts` -- `packages/sparql/http/error.ts` -- `packages/sparql/http/mod.ts` -- `packages/sparql/http/mod_test.ts` -- `packages/sparql/mod.ts` -- `packages/sparql/patterns/cypher.ts` -- `packages/sparql/patterns/objects.ts` -- `packages/sparql/patterns/triples.ts` -- `packages/sparql/result/json.ts` -- `packages/sparql/sparql.ts` -- `packages/sparql/syntax/scan.ts` -- `packages/sparql/syntax/scanner.ts` -- `packages/sparql/syntax/source.ts` -- `packages/sparql/update.ts` -- `packages/sparql/utils.ts` -- `packages/triplestore/README.md` -- `packages/triplestore/format.ts` -- `packages/triplestore/store.ts` -- `packages/triplestore/store_test.ts` -- `packages/vocab/README.md` -- `packages/vocab/deno.json` -- `packages/vocab/emit.ts` -- `packages/vocab/mod.ts` -- `packages/vocab/name.ts` -- `packages/vocab/package.json` -- `packages/vocab/read.ts` -- `packages/vocab/runtime.ts` -- `packages/vocab/schema/mod.ts` - -### Added (untracked in baseline) (33) - -- `.mise/tasks/bench.ts` -- `docs/research/architecture-design-20260814.md` -- `docs/standard-schema.md` -- `docs/testing.md` -- `docs/vocabulary-generation.md` -- `packages/rdf/namespace_test.ts` -- `packages/rdf/source_test.ts` -- `packages/rdf/term_test.ts` -- `packages/rdf/text_test.ts` -- `packages/rdf/write_test.ts` -- `packages/sparql/builder_bench.ts` -- `packages/sparql/builder_test.ts` -- `packages/sparql/client_test.ts` -- `packages/sparql/composition_test.ts` -- `packages/sparql/http/error_test.ts` -- `packages/sparql/patterns/cypher_test.ts` -- `packages/sparql/patterns/objects_test.ts` -- `packages/sparql/patterns/triples_test.ts` -- `packages/sparql/result/binding_test.ts` -- `packages/sparql/result/json_test.ts` -- `packages/sparql/sparql_test.ts` -- `packages/sparql/update_test.ts` -- `packages/sparql/utils_test.ts` -- `packages/triplestore/format_test.ts` -- `packages/vocab/compile.ts` -- `packages/vocab/compile_bench.ts` -- `packages/vocab/compile_test.ts` -- `packages/vocab/name_test.ts` -- `packages/vocab/read_test.ts` -- `packages/vocab/runtime_bench.ts` -- `packages/vocab/runtime_test.ts` -- `packages/vocab/standard.ts` -- `packages/vocab/standard_test.ts` - - +## Historical pass record + +The previous changed-file appendix described an earlier implementation pass. It became stale after the dependency, package, naming, and documentation corrections in the current source. It is intentionally removed from this living implementation guide. + +Use the repository source, the current package manifests, and artifact validation output for the current file inventory. A release or handoff must derive its changed-file list from the exact delivered tree instead of copying an older snapshot. diff --git a/docs/migration.md b/docs/migration.md index d556ddb..4dc1ba9 100644 --- a/docs/migration.md +++ b/docs/migration.md @@ -4,19 +4,19 @@ Version 0.1 reorganizes the repository around RDF, SPARQL, vocabulary generation ## Package changes -| 0.0.2 concept | 0.1 target | -| --- | --- | +| 0.0.2 concept | 0.1 target | +| ----------------------------------------------- | --------------------------------------------------------- | | RDF namespace constants inside `@okikio/sparql` | generated terms in `@okikio/vocab/*` or `rdf.namespace()` | -| `Executor` / `createExecutor()` | `@okikio/sparql/http.createClient()` or an engine adapter | -| builder `.execute()` | build first, then call `client.query*()` | -| `executeSparql()` | explicit HTTP client result-mode methods | -| `transformResults()` / datatype coercion | binding values remain RDF terms | -| `resolveLabels()` | application query recipe; no core replacement | -| `fetchProperties()` | application query recipe; no core replacement | -| `expand()` | application query recipe; no core replacement | -| `scripts/ttl-to-ts.ts` | `@okikio/vocab` compiler + `.mise/tasks/vocab.ts` | -| N3 dependency inside the generator | RDF parser chosen by the caller; compiler consumes quads | -| old `quotedTriple()` / SPARQL-star wording | RDF/SPARQL 1.2 `tripleTerm()` | +| `Executor` / `createExecutor()` | `@okikio/sparql/http.create()` or an engine adapter | +| builder `.execute()` | build first, then call `client.query*()` | +| `executeSparql()` | explicit HTTP client result-mode methods | +| `transformResults()` / datatype coercion | binding values remain RDF terms | +| `resolveLabels()` | application query recipe; no core replacement | +| `fetchProperties()` | application query recipe; no core replacement | +| `expand()` | application query recipe; no core replacement | +| `scripts/ttl-to-ts.ts` | `@okikio/vocab` compiler + `.mise/tasks/vocab.ts` | +| N3 dependency inside the generator | RDF parser chosen by the caller; compiler consumes quads | +| old `quotedTriple()` / SPARQL-star wording | RDF/SPARQL 1.2 `tripleTerm()` | ## Query execution @@ -32,12 +32,12 @@ After: ```ts import * as sparql from '@okikio/sparql' -import { createClient } from '@okikio/sparql/http' +import * as http from '@okikio/sparql/http' const query = sparql.select(['?name']) .where(sparql.triple('?person', 'foaf:name', '?name')) -const client = createClient({ endpoint }) +const client = http.create({ endpoint }) for await (const row of await client.queryBindings(query)) { console.log(row.get('name')?.value) } @@ -74,7 +74,7 @@ const name = schema('name') For generated vocabularies, prefer direct imports: ```ts -import { Product, ProductSchema, type ProductType, name } from '@okikio/vocab/schema' +import { name, Product, ProductSchema, type ProductType } from '@okikio/vocab/schema' ``` This keeps generated types and schemas tree-shakeable and avoids `lowercase.CamelCase` call sites. diff --git a/docs/quick-start.md b/docs/quick-start.md index 46846ab..3b4d2e0 100644 --- a/docs/quick-start.md +++ b/docs/quick-start.md @@ -21,10 +21,12 @@ Use explicit parser subpaths: ```ts import * as turtle from '@okikio/rdf/turtle' -for await (const quad of turtle.parse(` +for await ( + const quad of turtle.parse(` @prefix schema: . schema:name "Widget" . -`)) { +`) +) { console.log(quad) } ``` @@ -46,9 +48,9 @@ const query = sparql.select(['?product', '?name']) ## SPARQL endpoint ```ts -import { createClient } from '@okikio/sparql/http' +import * as http from '@okikio/sparql/http' -const client = createClient({ endpoint: 'https://example.com/sparql' }) +const client = http.create({ endpoint: 'https://example.com/sparql' }) for await (const row of await client.queryBindings(query)) { console.log(row.get('name')?.value) @@ -69,12 +71,7 @@ client.update(update) Generated vocabulary symbols are direct and tree-shakeable: ```ts -import { - Product, - ProductSchema, - type ProductType, - name, -} from '@okikio/vocab/schema' +import { name, Product, ProductSchema, type ProductType } from '@okikio/vocab/schema' const product: ProductType = { '@type': 'Product', diff --git a/docs/research/architecture-design.md b/docs/research/architecture-design.md index cde06fc..f99c6e3 100644 --- a/docs/research/architecture-design.md +++ b/docs/research/architecture-design.md @@ -1,13 +1,16 @@ +> **Historical research note:** This file records design exploration. Current package names and implementation authority are in `docs/architecture.md`, `README.md`, and the source tree. External RDF processors mentioned here are comparison references, not core runtime dependencies. + # `@okikio/rdf` + `@okikio/sparql` architecture and implementation handoff -## Complete RDF, SPARQL, vocabulary generation, local query engines, persistent triplestore, and Kaiju Crawl semantic-data integration +## Historical RDF, SPARQL, vocabulary, query-engine, and triplestore design record -**Status:** Canonical design and implementation handoff -**Date:** 2026-08-14 -**Primary repository:** `okikio/sparql-client` / `@okikio/sparql` -**Related libraries:** `@okikio/opfs`, Kaiju Platform, Kaiju Crawl, Wikitext parser work -**Runtime posture:** Deno 2 first, strict TypeScript, ESM, explicit file extensions, same production source across Deno, Node.js, Bun, browsers, and workers where the capability is available -**Compatibility posture:** Pre-launch architectural cleanup. Do not retain an obsolete public shape only because version `0.0.2` already exposes it. +**Status:** Historical design record retained for rationale. Current authority: `../architecture.md` and `../implementation.md`.\ +**Important:** API names, package names, file trees, and dependency choices in this record can describe superseded design stages. Do not use them as current implementation instructions.\ +**Date:** 2026-08-14\ +**Primary repository:** `okikio/sparql-client` / `@okikio/sparql`\ +**Related libraries:** `@okikio/opfs`, Kaiju Platform, Kaiju Crawl, Wikitext parser work\ +**Runtime posture:** Deno 2 first, strict TypeScript, ESM, explicit file extensions, same production source across Deno, Node.js, Bun, browsers, and workers where the capability is available\ +**Compatibility posture:** Pre-launch architectural cleanup. Do not retain an obsolete public shape only because version `0.0.2` already exposes it.\ **Performance posture:** Correctness first, then evidence-led data-oriented optimization. Benchmarks are a release and architecture tool, not decorative microbenchmarks. --- @@ -169,13 +172,13 @@ These names follow one rule: **the package name describes the capability or tech The storage package needs a name that remains true if its bytes live in browser OPFS, Node, Deno, Bun, memory, RxDB-backed files, a SQL-backed OPFS adapter, or another future `@okikio/opfs` backend. -| Candidate | Strength | Problem | Decision | -|---|---|---|---| -| `@okikio/rdf-opfs` | immediately says RDF + OPFS | encodes one backend into the package identity; becomes false on Node/Deno/Bun/DB adapters | reject | -| `@okikio/rdf-store` | recognizable | repeats `rdf`, less consistent with the explicit package family, ambiguous between in-memory dataset and database | reject | -| `@okikio/store` | short | too vague outside local context | reject | -| `@okikio/dataset` | RDF-domain term | sounds like an in-memory collection and does not communicate persistence/indexing | reject | -| `@okikio/triplestore` | standard RDF-domain concept; says persistent/queryable graph store | conventional name says triples even though RDF datasets contain quads | **use** | +| Candidate | Strength | Problem | Decision | +| --------------------- | ------------------------------------------------------------------ | ----------------------------------------------------------------------------------------------------------------- | -------- | +| `@okikio/rdf-opfs` | immediately says RDF + OPFS | encodes one backend into the package identity; becomes false on Node/Deno/Bun/DB adapters | reject | +| `@okikio/rdf-store` | recognizable | repeats `rdf`, less consistent with the explicit package family, ambiguous between in-memory dataset and database | reject | +| `@okikio/store` | short | too vague outside local context | reject | +| `@okikio/dataset` | RDF-domain term | sounds like an in-memory collection and does not communicate persistence/indexing | reject | +| `@okikio/triplestore` | standard RDF-domain concept; says persistent/queryable graph store | conventional name says triples even though RDF datasets contain quads | **use** | The package documentation must state that it stores RDF datasets and therefore supports named graphs/quads. “Triplestore” is the conventional product category, not a restriction to three-term records. @@ -184,20 +187,20 @@ The package documentation must state that it stores RDF datasets and therefore s The intended one-way graph is: ```text - @okikio/oxigraph - / \ - v v - @okikio/sparql -------> @okikio/rdf - ^ ^ - | | - | +--------- @okikio/vocab - | | - | +--------- @okikio/triplestore - | | - | v - | @okikio/opfs - | - @okikio/comunica + @okikio/oxigraph + / \ + v v +@okikio/sparql -------> @okikio/rdf + ^ ^ + | | + | +--------- @okikio/vocab + | | + | +--------- @okikio/triplestore + | | + | v + | @okikio/opfs + | + @okikio/comunica ``` More exactly: @@ -223,14 +226,14 @@ The API needs two different import styles because the use cases are different. Use namespace imports when the module is a coherent family of short operations: ```ts -import * as rdf from '@okikio/rdf'; -import * as sparql from '@okikio/sparql'; +import * as rdf from '@okikio/rdf' +import * as sparql from '@okikio/sparql' -const product = rdf.namedNode('https://example.com/products/1'); -const label = rdf.literal('Widget', 'en'); +const product = rdf.namedNode('https://example.com/products/1') +const label = rdf.literal('Widget', 'en') const query = sparql.select(['?product', '?name']) - .where(sparql.triple('?product', '?predicate', '?name')); + .where(sparql.triple('?product', '?predicate', '?name')) ``` The namespace supplies the missing context. `rdf.literal()` and `sparql.select()` are clearer than long direct names such as `createRdfLiteral()` or `createSparqlSelectQuery()`. @@ -240,14 +243,7 @@ The namespace supplies the missing context. `rdf.literal()` and `sparql.select() Generated vocabulary values should be directly importable: ```ts -import { - Product, - ProductSchema, - name, - offers, - price, - type ProductType, -} from '@okikio/vocab/schema'; +import { name, offers, price, Product, ProductSchema, type ProductType } from '@okikio/vocab/schema' ``` This is the preferred vocabulary style. @@ -255,10 +251,10 @@ This is the preferred vocabulary style. Avoid making this the normal form: ```ts -import * as schema from '@okikio/vocab/schema'; +import * as schema from '@okikio/vocab/schema' -schema.Product; -schema.ProductSchema; +schema.Product +schema.ProductSchema ``` The latter creates the `lowercase.CamelCase` appearance that the project deliberately avoids and makes the imported surface less explicit. @@ -266,10 +262,10 @@ The latter creates the `lowercase.CamelCase` appearance that the project deliber Namespace imports can still be valid for deliberate operation modules, for example: ```ts -import * as vocab from '@okikio/vocab/generate'; +import * as vocab from '@okikio/vocab/generate' -const model = await vocab.read(sources); -const output = vocab.emit(model, options); +const model = await vocab.inspect(sources) +const output = vocab.emit(model, options) ``` ## 5.3 Runtime class terms and generated types @@ -277,9 +273,9 @@ const output = vocab.emit(model, options); A generated class should normally expose: ```ts -Product // RDF NamedNode term for https://schema.org/Product -ProductType // TypeScript JSON-LD / vocabulary data type -ProductSchema // Standard Schema-compatible runtime validator +Product // RDF NamedNode term for https://schema.org/Product +ProductType // TypeScript JSON-LD / vocabulary data type +ProductSchema // Standard Schema-compatible runtime validator ``` A generated property should normally expose the source property name as an RDF NamedNode term: @@ -294,27 +290,27 @@ priceCurrency This makes RDF construction direct: ```ts -import * as rdf from '@okikio/rdf'; -import { Product, name } from '@okikio/vocab/schema'; +import * as rdf from '@okikio/rdf' +import { name, Product } from '@okikio/vocab/schema' const quad = rdf.quad( rdf.namedNode('https://example.com/product/1'), name, rdf.literal('Widget'), -); +) ``` The type/schema form remains equally direct: ```ts -import { ProductSchema, type ProductType } from '@okikio/vocab/schema'; +import { ProductSchema, type ProductType } from '@okikio/vocab/schema' const product: ProductType = { '@type': 'Product', name: 'Widget', -}; +} -const result = await ProductSchema['~standard'].validate(product); +const result = await ProductSchema['~standard'].validate(product) ``` ## 5.4 Collision policy @@ -466,8 +462,8 @@ Do not repeat the current executor behavior where RDF terms are casually flatten Useful explicit conversions can live behind operations such as: ```ts -rdf.toValue(literal, options); -rdf.fromValue(value, options); +rdf.toValue(literal, options) +rdf.fromValue(value, options) ``` These operations must document precision loss, timezone behavior, numeric range, and unsupported datatypes. @@ -479,8 +475,8 @@ The namespace capability should be generated or data-driven instead of maintaini A namespace operation can be: ```ts -const schema = rdf.namespace('https://schema.org/'); -const Product = schema('Product'); +const schema = rdf.namespace('https://schema.org/') +const Product = schema('Product') ``` Generated curated vocabularies belong in `@okikio/vocab`, not in a giant RDF root constants file. @@ -675,8 +671,8 @@ For string input: ```ts export interface RangeType { - readonly start: number; - readonly end: number; + readonly start: number + readonly end: number } ``` @@ -692,9 +688,9 @@ Start with stable small records: ```ts interface TokenType { - readonly kind: TokenKind; - readonly start: number; - readonly end: number; + readonly kind: TokenKind + readonly start: number + readonly end: number } ``` @@ -730,13 +726,13 @@ SPARQL queries are normally small enough that a findings-first lane can be usefu ```ts export interface ParseFindingsType { - readonly source: string; - readonly events: readonly SyntaxEventType[]; - readonly diagnostics: readonly DiagnosticType[]; + readonly source: string + readonly events: readonly SyntaxEventType[] + readonly diagnostics: readonly DiagnosticType[] } -export function analyze(source: string, options?: AnalyzeOptionsType): ParseFindingsType; -export function materialize(findings: ParseFindingsType): QuerySyntaxType; +export function analyze(source: string, options?: AnalyzeOptionsType): ParseFindingsType +export function materialize(findings: ParseFindingsType): QuerySyntaxType ``` This is **exploratory public API** until real tooling uses it. @@ -848,9 +844,9 @@ export const BatchPolicySchema = z.object({ maxRecords: z.number().int().positive(), maxBytes: z.number().int().positive(), maxDelayMs: z.number().nonnegative(), -}); +}) -export type BatchPolicyType = z.output; +export type BatchPolicyType = z.output ``` For a pure synchronous string parser, `maxDelayMs` may not apply. Do not force one batch policy onto every input model. @@ -871,7 +867,7 @@ Public: ```ts for await (const quad of parse(source)) { - console.log(quad.subject.value); + console.log(quad.subject.value) } ``` @@ -909,7 +905,6 @@ Every index must answer: Do not automatically build all subject/predicate/object/graph permutations. Quadstore is a useful baseline because it makes those permutations explicit and exposes the cost/coverage trade-off. - --- # 9. `@okikio/sparql`: language, query model, protocol, and execution seams @@ -939,10 +934,10 @@ The current executor is HTTP-endpoint-centric: ```ts interface ExecutionConfig { - endpoint: string; - fetch?: typeof fetch; - headers?: HeadersInit; - timeoutMs?: number; + endpoint: string + fetch?: typeof fetch + headers?: HeadersInit + timeoutMs?: number } ``` @@ -963,7 +958,7 @@ The most important correctness defect is that its `query` documentation and impl The supplied repository also has a material validation gap: - no `*_test.ts`, `*.test.ts`, `*_bench.ts`, or `*.bench.ts` files are present; -- `deno.jsonc` still defines `test` and `bench` tasks; +- Historical snapshot note: `deno.jsonc` defined the old flat-package tasks; the current repository removed that stale authority. - the `check` task runs `deno check src/**/*.ts`, but the package source is flat and there is no `src/` tree; - the current `./generate` export points directly at the starter file under root `scripts/`. @@ -980,22 +975,22 @@ export interface Queryable { queryBindings( query: QueryInputType, options?: QueryOptionsType, - ): Promise>; + ): Promise> queryQuads( query: QueryInputType, options?: QueryOptionsType, - ): Promise>; + ): Promise> queryBoolean( query: QueryInputType, options?: QueryOptionsType, - ): Promise; + ): Promise queryVoid( query: QueryInputType, options?: QueryOptionsType, - ): Promise; + ): Promise } ``` @@ -1010,7 +1005,7 @@ A binding is not `Record` after automatic coercion. Canonical shape: ```ts -export type BindingType = ReadonlyMap; +export type BindingType = ReadonlyMap ``` or another measured immutable lookup shape with equivalent semantics. @@ -1021,7 +1016,7 @@ Ergonomic projection is a separate operation: const rows = sparql.mapBindings(bindings, { name: rdf.toString, price: rdf.toNumber, -}); +}) ``` The package must not lose: @@ -1066,16 +1061,16 @@ Do not make the root package read global environment variables or configure cred SPARQL values should accept `@okikio/rdf` terms directly: ```ts -import * as rdf from '@okikio/rdf'; -import * as sparql from '@okikio/sparql'; -import { Product, name } from '@okikio/vocab/schema'; +import * as rdf from '@okikio/rdf' +import * as sparql from '@okikio/sparql' +import { name, Product } from '@okikio/vocab/schema' -const product = sparql.v('product'); -const value = rdf.literal('Widget'); +const product = sparql.v('product') +const value = rdf.literal('Widget') const query = sparql.select([product]) .where(sparql.triple(product, rdf.type, Product)) - .where(sparql.triple(product, name, value)); + .where(sparql.triple(product, name, value)) ``` The exact export for the standard RDF `type` term should be decided with the vocabulary naming/collision rules rather than adding a one-off alias to this example. @@ -1143,9 +1138,9 @@ export const CapabilitiesSchema = z.object({ update: z.boolean(), service: z.boolean(), extensions: z.array(z.string()), -}); +}) -export type CapabilitiesType = z.output; +export type CapabilitiesType = z.output ``` Do not infer complete support from the package name. @@ -1186,12 +1181,12 @@ The package should make Oxigraph feel native to the Okikio RDF/SPARQL model with Target operations might include: ```ts -import * as oxigraph from '@okikio/oxigraph'; +import * as oxigraph from '@okikio/oxigraph' -await using store = await oxigraph.open(); -await store.load(source, options); +await using store = await oxigraph.open() +await store.load(source, options) -const rows = await store.queryBindings(query); +const rows = await store.queryBindings(query) ``` The actual operation names should be chosen after inspecting the final Oxigraph wrapper contract. @@ -1239,8 +1234,8 @@ A caller should be able to depend on the query contract: ```ts async function getProducts(queryable: sparql.Queryable) { - const query = createProductQuery(); - return queryable.queryBindings(query); + const query = createProductQuery() + return queryable.queryBindings(query) } ``` @@ -1266,13 +1261,13 @@ The persistent store is the most benchmark-sensitive package in the design. The public store should feel like a persistent RDF Dataset/Source/Store: ```ts -import * as store from '@okikio/triplestore'; +import * as store from '@okikio/triplestore' await using db = await store.open(fileSystem, { path: '/knowledge', -}); +}) -await db.add(quad); +await db.add(quad) for await (const result of db.match(subject, predicate, null, graph)) { // ... } @@ -1439,9 +1434,9 @@ export const ManifestSchema = z.object({ dictionary: z.object({ version: z.number().int().positive() }), indexes: z.array(z.string()), segments: z.array(z.string()), -}); +}) -export type ManifestType = z.output; +export type ManifestType = z.output ``` The final fields should be derived from the actual storage design. @@ -1562,9 +1557,9 @@ export const ClassSchema = z.object({ superClasses: z.array(z.string()), equivalentClasses: z.array(z.string()), deprecated: z.boolean(), -}); +}) -export type ClassType = z.output; +export type ClassType = z.output ``` The full model should include: @@ -1603,11 +1598,11 @@ Do not flatten an OWL expression to a string merely because TypeScript emission Input API should support: ```ts -const model = await readOntology([ +const model = await inspect([ schemaOrgSource, gs1Source, owlSource, -], options); +], options) ``` The reader must define: @@ -1657,7 +1652,7 @@ Evaluate runtime bundle size **and TypeScript compiler/language-server cost**. The desired consumer API remains: ```ts -import { Product, ProductSchema, type ProductType } from '@okikio/vocab/schema'; +import { Product, ProductSchema, type ProductType } from '@okikio/vocab/schema' ``` Internal file layout exists to make that API cheap, not to force consumers into generated directory knowledge. @@ -1676,11 +1671,11 @@ A more scalable direction is a property-map/generic-node model: ```ts export interface ProductPropertiesType extends ThingPropertiesType { - name?: ValueType; - offers?: ValueType; + name?: ValueType + offers?: ValueType } -export type ProductType = NodeType<'Product', ProductPropertiesType>; +export type ProductType = NodeType<'Product', ProductPropertiesType> ``` Multi-type entities can use a generic composition: @@ -1689,7 +1684,7 @@ Multi-type entities can use a generic composition: export type ProductSoftwareType = MergeType<[ ProductType, SoftwareApplicationType, -]>; +]> ``` or a generated `NodeType<['Product', 'SoftwareApplication'], ...>` form. @@ -1854,9 +1849,9 @@ export const VocabularyManifestSchema = z.object({ properties: z.number().int().nonnegative(), datatypes: z.number().int().nonnegative(), diagnostics: z.number().int().nonnegative(), -}); +}) -export type VocabularyManifestType = z.output; +export type VocabularyManifestType = z.output ``` Each source should retain: @@ -2128,8 +2123,8 @@ Early iterator return must propagate cleanup to owned source readers/producers w Use Explicit Resource Management for live resources: ```ts -await using store = await triplestore.open(fileSystem, options); -await using engine = await oxigraph.open(options); +await using store = await triplestore.open(fileSystem, options) +await using engine = await oxigraph.open(options) ``` A parser that only consumes a caller-owned stream does not suddenly own the stream unless the API explicitly says so. @@ -2384,8 +2379,8 @@ Avoid broad `export *` unless the whole underlying module is deliberately public Prefer explicit export review: ```ts -export { dataset, literal, namedNode, quad } from './factory.ts'; -export type { Dataset, QuadType, TermType } from './types.ts'; +export { dataset, literal, namedNode, quad } from './factory.ts' +export type { Dataset, QuadType, TermType } from './types.ts' ``` The exact `types.ts` example should not create a permanent broad root `types` dumping ground. Keep related types with their owning modules or use a focused file only when the concept is genuinely cohesive. @@ -2486,7 +2481,6 @@ const ctx = ...; // only for a documented execution/parser context A public API should not export `getData()` when it actually gets an ontology class or index page. - --- # 19. TSDoc and comment standard @@ -2518,7 +2512,7 @@ A public module should explain its role before enumerating exports. Good: -```ts +````ts /** * Parses N-Quads into RDF quads without materializing the complete document. * @@ -2534,7 +2528,7 @@ Good: * } * ``` */ -``` +```` The exact cleanup wording must match the implementation. Do not promise cancellation propagation until it is tested. @@ -2726,15 +2720,15 @@ Use mature implementations as oracles, not as unquestioned truth. Examples: -| Capability | Differential baseline | -|---|---| -| N-Triples/N-Quads/Turtle/TriG | N3, Oxigraph | -| JSON-LD to RDF | jsonld.js, `jsonld-streaming-parser`, Oxigraph where applicable | -| RDF canonicalization | `rdf-canonize` | -| RDFJS dataset/source behavior | N3 Store / relevant RDFJS suites | -| SPARQL query evaluation | Oxigraph, Comunica, QLever/Fuseki fixtures where semantic feature matches | -| persistent RDF | Quadstore + BrowserLevel baseline | -| Schema.org generated typing | `schema-dts` consumer fixtures | +| Capability | Differential baseline | +| ----------------------------- | ------------------------------------------------------------------------- | +| N-Triples/N-Quads/Turtle/TriG | N3, Oxigraph | +| JSON-LD to RDF | jsonld.js, `jsonld-streaming-parser`, Oxigraph where applicable | +| RDF canonicalization | `rdf-canonize` | +| RDFJS dataset/source behavior | N3 Store / relevant RDFJS suites | +| SPARQL query evaluation | Oxigraph, Comunica, QLever/Fuseki fixtures where semantic feature matches | +| persistent RDF | Quadstore + BrowserLevel baseline | +| Schema.org generated typing | `schema-dts` consumer fixtures | Compare normalized semantic output, not implementation-specific ordering unless the standard defines the order. @@ -3255,15 +3249,15 @@ Calibrate thresholds from several baseline runs on the same machine. Suggested starting review triggers, not universal laws: -| Metric | Review trigger | -|---|---:| -| stable warm latency | >10% regression with meaningful absolute cost | -| large parse throughput | >10% regression | -| p95 async query/import | >15% regression | -| peak memory | >15% regression | -| persistent bytes | >15% growth without a documented feature/index reason | -| TypeScript compile memory | >15% regression | -| cold import/start | >10% and meaningful absolute change | +| Metric | Review trigger | +| ------------------------- | ----------------------------------------------------: | +| stable warm latency | >10% regression with meaningful absolute cost | +| large parse throughput | >10% regression | +| p95 async query/import | >15% regression | +| peak memory | >15% regression | +| persistent bytes | >15% growth without a documented feature/index reason | +| TypeScript compile memory | >15% regression | +| cold import/start | >10% and meaningful absolute change | A representation change that adds substantial complexity should normally show a stable end-to-end improvement above measurement noise, not a 2% microbenchmark win. @@ -3289,9 +3283,9 @@ export const BenchmarkRecordSchema = z.object({ samples: z.number().int().positive(), correctness: z.string(), notes: z.array(z.string()), -}); +}) -export type BenchmarkRecordType = z.output; +export type BenchmarkRecordType = z.output ``` The exact schema can expand to preserve runner-specific estimates/raw samples. @@ -3519,25 +3513,25 @@ This lets research evolve without rewriting the stable public programming model The exact patch should be planned after a fresh repository checkout, but the current `0.0.2` source implies these actions. -| Current path | Target action | Reason | -|---|---|---| -| `mod.ts` | replace with intentional `packages/sparql/mod.ts` exports | current broad `export *` surface mixes builder, HTTP execution, namespaces, and helpers | -| `sparql.ts` | split by values/expressions/serialization as code demands | 43 KB single file is carrying several concepts | -| `builder.ts` | migrate to structured query model | preserve fluent DX without string-first architecture | -| `update.ts` | migrate beside query model/update syntax | keep SPARQL mutation semantics explicit | -| `executor.ts` | split into query contract + HTTP adapter + optional resource helpers | current endpoint-only model and wrong graph result path | -| `namespaces.ts` | replace curated giant constants with generated vocab/namespace utilities | vocab data belongs in `@okikio/vocab` | -| `patterns/triples.ts` | retain/adapt | useful pattern capability | -| `patterns/objects.ts` | retain/adapt after RDF term/type review | strong ergonomic graph pattern | -| `patterns/cypher.ts` | retain if tests prove syntax remains safe/clear | useful visual DSL but must not accept unsafe relation text | -| `scripts/ttl-to-ts.ts` | delete after `@okikio/vocab` replacement | starter generator violates target parsing/model/codegen structure | -| `docs/*` | migrate and rewrite around package family | current docs describe one monolithic package | -| `examples/*` | split by package/use case | examples should prove public packages independently | -| `infra/qlever` | retain as endpoint integration fixture if maintained | valuable real-engine tests | -| `infra/blazegraph` | retain only if still an actively tested compatibility target | avoid stale infrastructure merely because it exists | -| `.github/workflows/publish.yml` | review against actual release policy | generated multi-package release will change publishing needs; do not preserve blindly | -| `deno.jsonc` | convert to workspace/export map | current exports refer to old flat files and root generator script | -| `mise.toml` | expand repository tasks through `.mise/tasks/` | complex test/bench/gen work should not accumulate shell strings in one TOML | +| Current path | Target action | Reason | +| ------------------------------- | ------------------------------------------------------------------------ | --------------------------------------------------------------------------------------- | +| `mod.ts` | replace with intentional `packages/sparql/mod.ts` exports | current broad `export *` surface mixes builder, HTTP execution, namespaces, and helpers | +| `sparql.ts` | split by values/expressions/serialization as code demands | 43 KB single file is carrying several concepts | +| `builder.ts` | migrate to structured query model | preserve fluent DX without string-first architecture | +| `update.ts` | migrate beside query model/update syntax | keep SPARQL mutation semantics explicit | +| `executor.ts` | split into query contract + HTTP adapter + optional resource helpers | current endpoint-only model and wrong graph result path | +| `namespaces.ts` | replace curated giant constants with generated vocab/namespace utilities | vocab data belongs in `@okikio/vocab` | +| `patterns/triples.ts` | retain/adapt | useful pattern capability | +| `patterns/objects.ts` | retain/adapt after RDF term/type review | strong ergonomic graph pattern | +| `patterns/cypher.ts` | retain if tests prove syntax remains safe/clear | useful visual DSL but must not accept unsafe relation text | +| `scripts/ttl-to-ts.ts` | delete after `@okikio/vocab` replacement | starter generator violates target parsing/model/codegen structure | +| `docs/*` | migrate and rewrite around package family | current docs describe one monolithic package | +| `examples/*` | split by package/use case | examples should prove public packages independently | +| historical `infra/qlever` | removed | endpoint interoperability now belongs to Testcontainers-owned service tests | +| historical `infra/blazegraph` | removed | no active compatibility target justified retaining fixed-port deployment files | +| `.github/workflows/publish.yml` | review against actual release policy | generated multi-package release will change publishing needs; do not preserve blindly | +| historical `deno.jsonc` | removed | `deno.json` is the single workspace authority | +| `mise.toml` | expand repository tasks through `.mise/tasks/` | complex test/bench/gen work should not accumulate shell strings in one TOML | Do not combine this functional migration with unrelated repository-wide formatting churn. @@ -3963,14 +3957,14 @@ The architecture is successful when these user stories are straightforward. ## 28.1 Small RDF use ```ts -import * as rdf from '@okikio/rdf'; -import { Product, name } from '@okikio/vocab/schema'; +import * as rdf from '@okikio/rdf' +import { name, Product } from '@okikio/vocab/schema' -const product = rdf.namedNode('https://example.com/product/1'); +const product = rdf.namedNode('https://example.com/product/1') const graph = rdf.dataset([ rdf.quad(product, rdf.type, Product), rdf.quad(product, name, rdf.literal('Widget')), -]); +]) ``` No JSON-LD or engine code enters the bundle. @@ -3978,10 +3972,10 @@ No JSON-LD or engine code enters the bundle. ## 28.2 Parse a large RDF stream ```ts -import { parse } from '@okikio/rdf/nquads'; +import { parse } from '@okikio/rdf/nquads' for await (const quad of parse(response.body!)) { - await sink.add(quad); + await sink.add(quad) } ``` @@ -3990,10 +3984,10 @@ Memory is bounded and returning early stops parser-owned reading. ## 28.3 Direct generated types ```ts -import { ProductSchema, type ProductType } from '@okikio/vocab/schema'; +import { ProductSchema, type ProductType } from '@okikio/vocab/schema' -const product: ProductType = input; -const result = await ProductSchema['~standard'].validate(product); +const product: ProductType = input +const result = await ProductSchema['~standard'].validate(product) ``` No namespace-qualified `schema.ProductType` is required. @@ -4001,11 +3995,11 @@ No namespace-qualified `schema.ProductType` is required. ## 28.4 Local Oxigraph query ```ts -import * as oxigraph from '@okikio/oxigraph'; -import * as sparql from '@okikio/sparql'; +import * as oxigraph from '@okikio/oxigraph' +import * as sparql from '@okikio/sparql' -await using store = await oxigraph.open(); -await store.load(source); +await using store = await oxigraph.open() +await store.load(source) for await (const row of await store.queryBindings(query)) { // RDF terms preserved @@ -4015,27 +4009,27 @@ for await (const row of await store.queryBindings(query)) { ## 28.5 Comunica over persistent store ```ts -import * as comunica from '@okikio/comunica'; -import * as triplestore from '@okikio/triplestore'; +import * as comunica from '@okikio/comunica' +import * as triplestore from '@okikio/triplestore' -await using graph = await triplestore.open(fileSystem, { path: '/graph' }); -await using engine = await comunica.open({ sources: [graph] }); +await using graph = await triplestore.open(fileSystem, { path: '/graph' }) +await using engine = await comunica.open({ sources: [graph] }) -const rows = await engine.queryBindings(query); +const rows = await engine.queryBindings(query) ``` ## 28.6 Generate a custom vocabulary ```ts -import * as vocab from '@okikio/vocab/generate'; +import * as vocab from '@okikio/vocab/generate' -const model = await vocab.read([ontologySource, extensionSource]); +const model = await vocab.inspect([ontologySource, extensionSource]) const output = vocab.emit(model, { name: 'example', context: 'https://example.com/vocab/', -}); +}) -await vocab.write(output, destination); +await vocab.write(output, destination) ``` The generated package exposes direct term/type/schema imports. @@ -4312,17 +4306,11 @@ The public developer experience follows two intentional patterns: ```ts // coherent operation families -import * as rdf from '@okikio/rdf'; -import * as sparql from '@okikio/sparql'; +import * as rdf from '@okikio/rdf' +import * as sparql from '@okikio/sparql' // exact generated terms/types/schemas -import { - Product, - ProductSchema, - name, - offers, - type ProductType, -} from '@okikio/vocab/schema'; +import { name, offers, Product, ProductSchema, type ProductType } from '@okikio/vocab/schema' ``` Parsing is designed from the data path: diff --git a/docs/research/legacy-quick-start.md b/docs/research/legacy-quick-start.md index 0c8c8de..012bf08 100644 --- a/docs/research/legacy-quick-start.md +++ b/docs/research/legacy-quick-start.md @@ -3,14 +3,20 @@ ## Namespace Constants **Import what you need:** + ```ts -import { RDF, RDFS, FOAF, SCHEMA, XSD, OWL } from '@okikio/sparql' +import { FOAF, OWL, RDF, RDFS, SCHEMA, XSD } from '@okikio/sparql' ``` **Use in queries:** + ```ts // Instead of full IRIs: -triple('?person', 'http://www.w3.org/1999/02/22-rdf-syntax-ns#type', '') +triple( + '?person', + 'http://www.w3.org/1999/02/22-rdf-syntax-ns#type', + '', +) // Use constants: triple('?person', RDF.type, uri(FOAF.Person)) @@ -19,6 +25,7 @@ triple('?person', SCHEMA.email, '?email') ``` **Available namespaces:** + - **XSD** - Datatypes (string, integer, decimal, boolean, date, dateTime, etc.) - **RDF** - Core (type, Property, Statement, first, rest, nil) - **RDFS** - Schema (label, comment, Class, subClassOf, domain, range) @@ -31,6 +38,7 @@ triple('?person', SCHEMA.email, '?email') ## Prefix Declarations **Add prefixes to queries:** + ```ts const query = select(['?name', '?email']) .prefix('foaf', 'http://xmlns.com/foaf/0.1/') @@ -40,6 +48,7 @@ const query = select(['?name', '?email']) ``` **Generated SPARQL:** + ```sparql PREFIX foaf: PREFIX schema: @@ -51,8 +60,9 @@ SELECT ?name ?email WHERE { ``` **Combine with namespace constants:** + ```ts -import { FOAF, SCHEMA, getNamespaceIRI } from '@okikio/sparql' +import { FOAF, getNamespaceIRI, SCHEMA } from '@okikio/sparql' select(['?name']) .prefix('foaf', getNamespaceIRI(FOAF)) @@ -67,6 +77,7 @@ select(['?name']) ### Type Coercion **Transform results with automatic type conversion:** + ```ts import { transformResultsTyped } from '@okikio/sparql' @@ -78,7 +89,7 @@ const result = await select(['?price', '?quantity', '?active']) if (result.success) { const rows = transformResultsTyped(result.data) - + for (const row of rows) { // price and quantity are numbers, active is boolean const total = row.price * row.quantity @@ -91,6 +102,7 @@ if (result.success) { ### Extract Specific Variable **Get array of values for one variable:** + ```ts import { pluck } from '@okikio/sparql' @@ -108,6 +120,7 @@ const averageAge = ages.reduce((a, b) => a + b, 0) / ages.length ### Get First Result **Perfect for lookups:** + ```ts import { first } from '@okikio/sparql' @@ -120,7 +133,7 @@ const result = await select(['?name', '?email']) if (result.success) { const person = first(result.data) - + if (person) { console.log('Found:', person.name) console.log('Email:', person.email) @@ -133,8 +146,9 @@ if (result.success) { ### ASK Query Results **Extract boolean from ASK queries:** + ```ts -import { askResult, ask } from '@okikio/sparql' +import { ask, askResult } from '@okikio/sparql' const result = await ask() .where(triple('?person', 'foaf:name', 'Alice')) @@ -150,12 +164,12 @@ if (result.success) { ```ts import { - parseBinding, // Get type metadata - coerceValue, // Convert single binding to JS type + askResult, // Get boolean from ASK query + coerceValue, // Convert single binding to JS type + first, // Get first result or undefined + parseBinding, // Get type metadata + pluck, // Extract one variable's values transformResultsTyped, // Transform all results with type coercion - pluck, // Extract one variable's values - first, // Get first result or undefined - askResult // Get boolean from ASK query } from '@okikio/sparql' ``` @@ -166,13 +180,13 @@ import { ### Reusable Patterns ```ts -import { RDF, FOAF, SCHEMA } from '@okikio/sparql' +import { FOAF, RDF, SCHEMA } from '@okikio/sparql' // Define pattern once function personPattern(personVar = 'person') { return node(personVar, FOAF.Person, { [FOAF.name]: v('name'), - [FOAF.age]: v('age') + [FOAF.age]: v('age'), }) } @@ -217,19 +231,19 @@ interface SearchFilters { function searchPeople(filters: SearchFilters) { let query = select(['?name', '?age', '?city']) .where(personPattern()) - + if (filters.minAge !== undefined) { query = query.filter(v('age').gte(filters.minAge)) } - + if (filters.maxAge !== undefined) { query = query.filter(v('age').lte(filters.maxAge)) } - + if (filters.city) { query = query.filter(v('city').eq(filters.city)) } - + return query } @@ -246,12 +260,18 @@ const londonResidents = searchPeople({ city: 'London' }) ```ts import { - select, triple, node, v, uri, - RDF, FOAF, SCHEMA, + first, + FOAF, getNamespaceIRI, - transformResultsTyped, + node, pluck, - first + RDF, + SCHEMA, + select, + transformResultsTyped, + triple, + uri, + v, } from '@okikio/sparql' // Define reusable pattern @@ -259,7 +279,7 @@ function personPattern(personVar = 'person') { return node(personVar, FOAF.Person, { [FOAF.name]: v('name'), [FOAF.age]: v('age'), - [SCHEMA.email]: v('email') + [SCHEMA.email]: v('email'), }) } @@ -275,24 +295,24 @@ const query = select(['?name', '?age', '?email']) // Execute and parse const result = await query.execute({ - endpoint: 'http://localhost:3030/dataset/sparql' + endpoint: 'http://localhost:3030/dataset/sparql', }) if (result.success) { // Get typed results const rows = transformResultsTyped(result.data) console.log('Total people:', rows.length) - + // Extract specific data const ages = pluck(result.data, 'age', true) console.log('Average age:', ages.reduce((a, b) => a + b) / ages.length) - + // Get first person const oldest = first(result.data) if (oldest) { console.log('Oldest person:', oldest.name, oldest.age) } - + // Process all rows for (const row of rows) { // age is a number, not a string @@ -315,18 +335,18 @@ const query = select(['?name']) .where(triple( '?person', 'http://www.w3.org/1999/02/22-rdf-syntax-ns#type', - '' + '', )) .where(triple( '?person', 'http://xmlns.com/foaf/0.1/name', - '?name' + '?name', )) // String results const rows = transformResults(result.data) for (const row of rows) { - const age = parseInt(row.age, 10) // Manual conversion + const age = parseInt(row.age, 10) // Manual conversion } ``` @@ -334,7 +354,7 @@ for (const row of rows) { ```ts // Import constants -import { RDF, FOAF, getNamespaceIRI } from '@okikio/sparql' +import { FOAF, getNamespaceIRI, RDF } from '@okikio/sparql' // Clean query with prefixes and constants const query = select(['?name']) @@ -357,7 +377,7 @@ for (const row of rows) { 1. **Add namespace constants to your imports:** ```ts - import { RDF, FOAF, SCHEMA } from '@okikio/sparql' + import { FOAF, RDF, SCHEMA } from '@okikio/sparql' ``` 2. **Use `.prefix()` in your queries:** @@ -377,9 +397,9 @@ for (const row of rows) { 4. **Extract reusable patterns:** ```ts function myPattern() { return node(...) } - + select([...]) .where(myPattern()) ``` -Everything is fully typed and documented. Your editor will guide you with autocomplete and inline documentation. \ No newline at end of file +Everything is fully typed and documented. Your editor will guide you with autocomplete and inline documentation. diff --git a/docs/research/legacy-readme.md b/docs/research/legacy-readme.md index c823170..d84089f 100644 --- a/docs/research/legacy-readme.md +++ b/docs/research/legacy-readme.md @@ -9,7 +9,7 @@ deno add @okikio/sparql ``` ```ts -import { select, triple, node, v, filter } from '@okikio/sparql' +import { filter, node, select, triple, v } from '@okikio/sparql' ``` ## Quick Start @@ -24,7 +24,7 @@ const adults = select(['?name', '?age']) const sparql = adults.build() const results = await adults.execute({ - endpoint: 'http://localhost:3030/dataset/sparql' + endpoint: 'http://localhost:3030/dataset/sparql', }) ``` @@ -35,13 +35,15 @@ That `v('age').gte(18)` is the fluent API - variables become values with chainab Every library feature maps directly to standard SPARQL 1.1. The library provides 100% spec coverage with enhanced developer experience through type safety, fluent chaining, and multiple pattern styles. **Key mappings:** + - `v('age').gte(18)` → `?age >= 18` - `select([v('price').mul(1.2).as('total')])` → `SELECT (?price * 1.2 AS ?total)` - `triple('?s', 'rdf:type', 'ex:Person')` → `?s rdf:type ex:Person .` - `md5(v('email'))` → `MD5(?email)` - `now()` → `NOW()` -**See [sparql-mapping.md](./docs/sparql-mapping.md) for:** +**See [sparql-mapping.md](../sparql-mapping.md) for:** + - Complete function reference (85+ functions) - Library → SPARQL examples for all features - SPARQL → Library migration guide @@ -73,10 +75,10 @@ select(['?title', '?publisherName', '?city']) 'schema:name': v('publisherName'), 'schema:location': node('location', 'schema:Place', { 'schema:city': v('city'), - 'schema:country': v('country') - }) - }) - }) + 'schema:country': v('country'), + }), + }), + }), ) ``` @@ -86,11 +88,11 @@ ASCII art syntax emphasizes visual clarity. The cypher template tag lets you dra ```ts const product = node('product', 'schema:Product', { - 'schema:name': v('title') + 'schema:name': v('title'), }) const publisher = node('publisher', 'schema:Organization', { - 'schema:name': v('pubName') + 'schema:name': v('pubName'), }) const query = select(['?title', '?pubName']) @@ -105,7 +107,7 @@ Combine patterns with `match()` when you want to build complex structures from s const pattern = match( node('person', 'foaf:Person', { 'foaf:name': v('personName') }), rel('person', 'foaf:knows', 'friend'), - node('friend', 'foaf:Person', { 'foaf:name': v('friendName') }) + node('friend', 'foaf:Person', { 'foaf:name': v('friendName') }), ) select(['?personName', '?friendName']).where(pattern) @@ -118,9 +120,12 @@ select(['?person', '?skill', '?friendName']) .where(triple('?person', 'ex:hasSkill', '?skill')) .where( node('person') - .prop('foaf:knows', node('friend', { - 'foaf:name': v('friendName') - })) + .prop( + 'foaf:knows', + node('friend', { + 'foaf:name': v('friendName'), + }), + ), ) ``` @@ -133,10 +138,10 @@ const pricing = select(['?product', '?total']) .where(triple('?product', 'schema:price', '?basePrice')) .bind( v('basePrice') - .mul(1.2) // Apply markup - .add(5) // Add shipping - .round() // Clean up decimals - .as('total') + .mul(1.2) // Apply markup + .add(5) // Add shipping + .round() // Clean up decimals + .as('total'), ) .filter(v('total').gte(20)) ``` @@ -153,7 +158,7 @@ select(['?displayName']) .concat(' ') .concat(v('last').ucase().substr(1, 1)) .concat('.') - .as('displayName') + .as('displayName'), ) ``` @@ -174,9 +179,9 @@ select(['?item', '?price', '?status']) ifElse( v('stock').gt(0), v('base').mul(0.95), - v('base').add(20) - ) - ).round().as('price') + v('base').add(20), + ), + ).round().as('price'), ) .bind( ifElse( @@ -185,9 +190,9 @@ select(['?item', '?price', '?status']) ifElse( v('stock').gt(0), 'Low Stock', - 'Out of Stock' - ) - ).as('status') + 'Out of Stock', + ), + ).as('status'), ) ``` @@ -203,7 +208,7 @@ const analytics = select([ count().as('users'), avg(v('age')).as('avgAge'), countDistinct(v('city')).as('cities'), - sum(v('purchases')).as('revenue') + sum(v('purchases')).as('revenue'), ]) .where(triple('?user', 'schema:country', '?country')) .where(triple('?user', 'foaf:age', '?age')) @@ -236,8 +241,8 @@ const enriched = select(['?product', '?name', '?price', '?sales']) .where( node('product', { 'schema:name': v('name'), - 'schema:price': v('price') - }) + 'schema:price': v('price'), + }), ) .orderBy('?sales', 'DESC') ``` @@ -282,10 +287,10 @@ const bosses = select(['?employee', '?boss']) '?employee', sequence( oneOrMore('org:reportsTo'), - alternative('org:manages', 'org:supervises') + alternative('org:manages', 'org:supervises'), ), - '?boss' - ) + '?boss', + ), ) ``` @@ -313,8 +318,8 @@ const markSeniors = modify() .insert( node('person', { 'ex:seniorCitizen': true, - 'ex:discount': 0.15 - }) + 'ex:discount': 0.15, + }), ) .where(triple('?person', 'foaf:age', '?age')) .where(filter(v('age').gte(65))) @@ -328,8 +333,8 @@ const cleanup = modify() .delete( node('account', { 'ex:status': v('status'), - 'ex:lastLogin': v('lastLogin') - }) + 'ex:lastLogin': v('lastLogin'), + }), ) .where(triple('?account', 'ex:status', 'inactive')) .where(triple('?account', 'ex:lastLogin', '?lastLogin')) @@ -347,7 +352,7 @@ const productSearch = select([ v('displayPrice'), v('stockStatus'), v('categoryName'), - v('averageRating') + v('averageRating'), ]) .where( node('product', 'schema:Product', { @@ -355,15 +360,18 @@ const productSearch = select([ 'schema:price': v('basePrice'), 'schema:inventory': v('stock'), 'schema:category': node('category', 'schema:Category', { - 'schema:name': v('categoryName') - }) - }) + 'schema:name': v('categoryName'), + }), + }), ) .optional( node('product') - .prop('schema:review', node('review', 'schema:Review', { - 'schema:ratingValue': v('rating') - })) + .prop( + 'schema:review', + node('review', 'schema:Review', { + 'schema:ratingValue': v('rating'), + }), + ), ) .bind( ifElse( @@ -372,9 +380,9 @@ const productSearch = select([ ifElse( v('stock').gt(0), v('basePrice').mul(0.95), - v('basePrice').mul(1.1) - ) - ).round().as('displayPrice') + v('basePrice').mul(1.1), + ), + ).round().as('displayPrice'), ) .bind( ifElse( @@ -383,9 +391,9 @@ const productSearch = select([ ifElse( v('stock').gt(0), v('stock').concat(' left'), - 'Out of Stock' - ) - ).as('stockStatus') + 'Out of Stock', + ), + ).as('stockStatus'), ) .filter(v('displayPrice').gte(10)) .groupBy('?product', '?title', '?displayPrice', '?stockStatus', '?categoryName') @@ -395,7 +403,7 @@ const productSearch = select([ .limit(50) const results = await productSearch.execute({ - endpoint: 'http://localhost:3030/catalog/sparql' + endpoint: 'http://localhost:3030/catalog/sparql', }) ``` @@ -420,8 +428,8 @@ const federated = select(['?person', '?name', '?birthPlace', '?abstract']) service( 'http://dbpedia.org/sparql', triple('?person', 'dbo:birthPlace', '?birthPlace'), - triple('?person', 'dbo:abstract', '?abstract') - ) + triple('?person', 'dbo:abstract', '?abstract'), + ), ) .filter(v('abstract').regex('scientist')) ``` @@ -435,10 +443,10 @@ Everything is fully typed. TypeScript catches errors at compile time: ```ts const age = v('age') -age.gte(18) // ✓ Returns SparqlValue for filters -age.add(5) // ✓ Returns FluentValue, can chain -age.add(5).mul(2) // ✓ Chains continue naturally -age.gte('not a number') // ✗ TypeScript error +age.gte(18) // ✓ Returns SparqlValueType for filters +age.add(5) // ✓ Returns FluentValue, can chain +age.add(5).mul(2) // ✓ Chains continue naturally +age.gte('not a number') // ✗ TypeScript error ``` You get autocomplete in your editor. The library guides you toward correct code. Generated SPARQL is safe from injection attacks because values are properly escaped automatically. @@ -452,22 +460,25 @@ Building complex queries from reusable pieces makes your code cleaner and more m Extract common triple patterns into functions. This reduces duplication and makes queries easier to understand: ```ts -import { RDF, FOAF, SCHEMA } from '@okikio/sparql' +import { FOAF, RDF, SCHEMA } from '@okikio/sparql' // Define reusable pattern fragments function personPattern(personVar = 'person') { return node(personVar, FOAF.Person, { [FOAF.name]: v('name'), - [FOAF.age]: v('age') + [FOAF.age]: v('age'), }) } function addressPattern(personVar = 'person') { return node(personVar) - .prop(SCHEMA.address, node('address', SCHEMA.PostalAddress, { - [SCHEMA.addressLocality]: v('city'), - [SCHEMA.addressCountry]: v('country') - })) + .prop( + SCHEMA.address, + node('address', SCHEMA.PostalAddress, { + [SCHEMA.addressLocality]: v('city'), + [SCHEMA.addressCountry]: v('country'), + }), + ) } // Use them in queries @@ -493,9 +504,7 @@ function olderThan(age: number) { } function nameMatches(pattern: string, caseInsensitive = true) { - return caseInsensitive - ? v('name').regex(pattern, 'i') - : v('name').regex(pattern) + return caseInsensitive ? v('name').regex(pattern, 'i') : v('name').regex(pattern) } function inCountry(country: string) { @@ -577,8 +586,8 @@ const baseProductQuery = select(['?product', '?name', '?price']) .where( node('product', SCHEMA.Product, { [SCHEMA.name]: v('name'), - [SCHEMA.price]: v('price') - }) + [SCHEMA.price]: v('price'), + }), ) // Extend for specific needs @@ -619,15 +628,15 @@ const enrichedProducts = select([ '?name', '?price', '?sales', - '?category' + '?category', ]) .where(subquery(topSellers)) .where( node('product', { [SCHEMA.name]: v('name'), [SCHEMA.price]: v('price'), - [SCHEMA.category]: v('category') - }) + [SCHEMA.category]: v('category'), + }), ) .orderBy('?sales', 'DESC') ``` @@ -645,7 +654,7 @@ const ecommerce = { return node(productVar, SCHEMA.Product, { [SCHEMA.name]: v('productName'), [SCHEMA.price]: v('price'), - [SCHEMA.sku]: v('sku') + [SCHEMA.sku]: v('sku'), }) }, @@ -653,14 +662,14 @@ const ecommerce = { return node(orderVar, SCHEMA.Order, { [SCHEMA.orderDate]: v('orderDate'), [SCHEMA.orderNumber]: v('orderNumber'), - [SCHEMA.customer]: v('customer') + [SCHEMA.customer]: v('customer'), }) }, customer(customerVar = 'customer') { return node(customerVar, SCHEMA.Person, { [SCHEMA.name]: v('customerName'), - [SCHEMA.email]: v('email') + [SCHEMA.email]: v('email'), }) }, @@ -671,7 +680,7 @@ const ecommerce = { orderBy(orderVar = 'order', customerVar = 'customer') { return triple(`?${orderVar}`, SCHEMA.customer, `?${customerVar}`) - } + }, } // Use pattern collection @@ -679,7 +688,7 @@ const orderAnalysis = select([ '?orderNumber', '?customerName', '?productName', - '?price' + '?price', ]) .where(ecommerce.order()) .where(ecommerce.orderBy()) @@ -698,8 +707,8 @@ Build queries programmatically from user input or configuration: ```ts interface FieldSelection { fields: string[] - filters: Array<{ field: string, operator: string, value: any }> - sort?: { field: string, direction: 'ASC' | 'DESC' } + filters: Array<{ field: string; operator: string; value: any }> + sort?: { field: string; direction: 'ASC' | 'DESC' } limit?: number } @@ -709,11 +718,11 @@ function buildDynamicQuery(config: FieldSelection) { name: FOAF.name, age: FOAF.age, email: SCHEMA.email, - city: SCHEMA.addressLocality + city: SCHEMA.addressLocality, } // Start with base pattern - let query = select(config.fields.map(f => `?${f}`)) + let query = select(config.fields.map((f) => `?${f}`)) .where(triple('?person', RDF.type, uri(FOAF.Person))) // Add triples for each requested field @@ -760,16 +769,16 @@ function buildDynamicQuery(config: FieldSelection) { const query1 = buildDynamicQuery({ fields: ['name', 'email'], filters: [{ field: 'name', operator: 'contains', value: 'John' }], - limit: 10 + limit: 10, }) const query2 = buildDynamicQuery({ fields: ['name', 'age', 'city'], filters: [ { field: 'age', operator: 'gt', value: 18 }, - { field: 'city', operator: 'eq', value: 'London' } + { field: 'city', operator: 'eq', value: 'London' }, ], - sort: { field: 'age', direction: 'DESC' } + sort: { field: 'age', direction: 'DESC' }, }) ``` @@ -839,4 +848,4 @@ Start simple with basic queries. Add complexity as you need it. The patterns sta ## License -MIT \ No newline at end of file +MIT diff --git a/docs/research/legacy-sparql-mapping.md b/docs/research/legacy-sparql-mapping.md index 1415420..c0d53eb 100644 --- a/docs/research/legacy-sparql-mapping.md +++ b/docs/research/legacy-sparql-mapping.md @@ -1,6 +1,7 @@ # SPARQL Mapping Guide This guide shows how every library feature maps to SPARQL 1.1, and vice versa. Use this to: + - Understand what SPARQL is generated - Migrate from raw SPARQL to type-safe code - Migrate from library code back to SPARQL @@ -681,74 +682,74 @@ WHERE { ### All 85+ Functions Mapped -| Category | Library Function | SPARQL | -|----------|-----------------|--------| -| **Comparison** | `eq(a, b)` | `a = b` | -| | `neq(a, b)` | `a != b` | -| | `lt(a, b)` | `a < b` | -| | `lte(a, b)` | `a <= b` | -| | `gt(a, b)` | `a > b` | -| | `gte(a, b)` | `a >= b` | -| **Arithmetic** | `add(a, b)` | `a + b` | -| | `sub(a, b)` | `a - b` | -| | `mul(a, b)` | `a * b` | -| | `div(a, b)` | `a / b` | -| | `mod(a, b)` | `(a % b)` | -| **Math** | `abs(x)` | `ABS(x)` | -| | `round(x)` | `ROUND(x)` | -| | `ceil(x)` | `CEIL(x)` | -| | `floor(x)` | `FLOOR(x)` | -| **String** | `concat(...args)` | `CONCAT(...)` | -| | `str(x)` | `STR(x)` | -| | `strlen(x)` | `STRLEN(x)` | -| | `ucase(x)` | `UCASE(x)` | -| | `lcase(x)` | `LCASE(x)` | -| | `substr(s, start, len?)` | `SUBSTR(s, start, len)` | -| | `startsWith(s, prefix)` | `STRSTARTS(s, prefix)` | -| | `endsWith(s, suffix)` | `STRENDS(s, suffix)` | -| | `contains(s, substr)` | `CONTAINS(s, substr)` | -| | `regex(s, pattern, flags?)` | `REGEX(s, pattern, flags)` | -| | `replaceStr(s, old, new)` | `REPLACE(s, old, new)` | -| | `encodeForUri(s)` | `ENCODE_FOR_URI(s)` | -| **Hash** | `md5(x)` | `MD5(x)` | -| | `sha1(x)` | `SHA1(x)` | -| | `sha256(x)` | `SHA256(x)` | -| | `sha384(x)` | `SHA384(x)` | -| | `sha512(x)` | `SHA512(x)` | -| **Random/Unique** | `now()` | `NOW()` | -| | `uuid()` | `UUID()` | -| | `struuid()` | `STRUUID()` | -| | `rand()` | `RAND()` | -| **Type Check** | `isIri(x)` | `isIRI(x)` | -| | `isBlank(x)` | `isBlank(x)` | -| | `isLiteral(x)` | `isLiteral(x)` | -| | `bound(x)` | `BOUND(x)` | -| | `isNull(x)` | `!BOUND(x)` | -| | `isNotNull(x)` | `BOUND(x)` | -| | `getlang(x)` | `LANG(x)` | -| | `datatype(x)` | `DATATYPE(x)` | -| | `langMatches(lang, range)` | `langMatches(lang, range)` | -| **Logical** | `and(...conds)` | `cond1 && cond2 && ...` | -| | `or(...conds)` | `cond1 \|\| cond2 \|\| ...` | -| | `not(cond)` | `!(cond)` | -| | `exists(pattern)` | `EXISTS { pattern }` | -| | `notExists(pattern)` | `NOT EXISTS { pattern }` | -| **Conditional** | `ifElse(cond, then, else)` | `IF(cond, then, else)` | -| | `coalesce(...vals)` | `COALESCE(...)` | -| **IRI** | `iri(str)` | `IRI(str)` | -| | `uri(iri)` | `` | -| **Blank Nodes** | `bnode()` | `BNODE()` | -| | `bnode(id)` | `_:id` | -| **Special** | `undef()` | `?UNDEF` | -| **Aggregates** | `count()` | `COUNT(*)` | -| | `count(x)` | `COUNT(x)` | -| | `countDistinct(x)` | `COUNT(DISTINCT x)` | -| | `sum(x)` | `SUM(x)` | -| | `avg(x)` | `AVG(x)` | -| | `min(x)` | `MIN(x)` | -| | `max(x)` | `MAX(x)` | -| | `sample(x)` | `SAMPLE(x)` | -| | `groupConcat(x, sep)` | `GROUP_CONCAT(x; separator=sep)` | +| Category | Library Function | SPARQL | +| ----------------- | --------------------------- | -------------------------------- | +| **Comparison** | `eq(a, b)` | `a = b` | +| | `neq(a, b)` | `a != b` | +| | `lt(a, b)` | `a < b` | +| | `lte(a, b)` | `a <= b` | +| | `gt(a, b)` | `a > b` | +| | `gte(a, b)` | `a >= b` | +| **Arithmetic** | `add(a, b)` | `a + b` | +| | `sub(a, b)` | `a - b` | +| | `mul(a, b)` | `a * b` | +| | `div(a, b)` | `a / b` | +| | `mod(a, b)` | `(a % b)` | +| **Math** | `abs(x)` | `ABS(x)` | +| | `round(x)` | `ROUND(x)` | +| | `ceil(x)` | `CEIL(x)` | +| | `floor(x)` | `FLOOR(x)` | +| **String** | `concat(...args)` | `CONCAT(...)` | +| | `str(x)` | `STR(x)` | +| | `strlen(x)` | `STRLEN(x)` | +| | `ucase(x)` | `UCASE(x)` | +| | `lcase(x)` | `LCASE(x)` | +| | `substr(s, start, len?)` | `SUBSTR(s, start, len)` | +| | `startsWith(s, prefix)` | `STRSTARTS(s, prefix)` | +| | `endsWith(s, suffix)` | `STRENDS(s, suffix)` | +| | `contains(s, substr)` | `CONTAINS(s, substr)` | +| | `regex(s, pattern, flags?)` | `REGEX(s, pattern, flags)` | +| | `replaceStr(s, old, new)` | `REPLACE(s, old, new)` | +| | `encodeForUri(s)` | `ENCODE_FOR_URI(s)` | +| **Hash** | `md5(x)` | `MD5(x)` | +| | `sha1(x)` | `SHA1(x)` | +| | `sha256(x)` | `SHA256(x)` | +| | `sha384(x)` | `SHA384(x)` | +| | `sha512(x)` | `SHA512(x)` | +| **Random/Unique** | `now()` | `NOW()` | +| | `uuid()` | `UUID()` | +| | `struuid()` | `STRUUID()` | +| | `rand()` | `RAND()` | +| **Type Check** | `isIri(x)` | `isIRI(x)` | +| | `isBlank(x)` | `isBlank(x)` | +| | `isLiteral(x)` | `isLiteral(x)` | +| | `bound(x)` | `BOUND(x)` | +| | `isNull(x)` | `!BOUND(x)` | +| | `isNotNull(x)` | `BOUND(x)` | +| | `getlang(x)` | `LANG(x)` | +| | `datatype(x)` | `DATATYPE(x)` | +| | `langMatches(lang, range)` | `langMatches(lang, range)` | +| **Logical** | `and(...conds)` | `cond1 && cond2 && ...` | +| | `or(...conds)` | `cond1 \|\| cond2 \|\| ...` | +| | `not(cond)` | `!(cond)` | +| | `exists(pattern)` | `EXISTS { pattern }` | +| | `notExists(pattern)` | `NOT EXISTS { pattern }` | +| **Conditional** | `ifElse(cond, then, else)` | `IF(cond, then, else)` | +| | `coalesce(...vals)` | `COALESCE(...)` | +| **IRI** | `iri(str)` | `IRI(str)` | +| | `uri(iri)` | `` | +| **Blank Nodes** | `bnode()` | `BNODE()` | +| | `bnode(id)` | `_:id` | +| **Special** | `undef()` | `?UNDEF` | +| **Aggregates** | `count()` | `COUNT(*)` | +| | `count(x)` | `COUNT(x)` | +| | `countDistinct(x)` | `COUNT(DISTINCT x)` | +| | `sum(x)` | `SUM(x)` | +| | `avg(x)` | `AVG(x)` | +| | `min(x)` | `MIN(x)` | +| | `max(x)` | `MAX(x)` | +| | `sample(x)` | `SAMPLE(x)` | +| | `groupConcat(x, sep)` | `GROUP_CONCAT(x; separator=sep)` | --- @@ -757,34 +758,39 @@ WHERE { ### 1. Fluent Chaining **Raw SPARQL:** + ```sparql FILTER(?age >= 18 && ?age < 65 && ?status = "active") ``` **Library (functional style):** + ```typescript filter(and( gte(v('age'), 18), lt(v('age'), 65), - eq(v('status'), 'active') + eq(v('status'), 'active'), )) ``` **Library (fluent style - DX enhancement):** + ```typescript filter( - v('age').gte(18).and(v('age').lt(65)).and(v('status').eq('active')) + v('age').gte(18).and(v('age').lt(65)).and(v('status').eq('active')), ) ``` ### 2. Chainable Arithmetic **Raw SPARQL:** + ```sparql BIND(((?price * 1.2) + 5) AS ?total) ``` **Library (fluent - reads left to right):** + ```typescript bind(v('price').mul(1.2).add(5), 'total') ``` @@ -792,6 +798,7 @@ bind(v('price').mul(1.2).add(5), 'total') ### 3. Pattern Composition **Raw SPARQL (repetitive):** + ```sparql ?product a schema:Product . ?product schema:name ?title . @@ -804,21 +811,23 @@ bind(v('price').mul(1.2).add(5), 'total') ``` **Library (DRY nested structure):** + ```typescript node('product', 'schema:Product', { 'schema:name': v('title'), 'schema:publisher': node('publisher', 'schema:Organization', { 'schema:name': v('pubName'), 'schema:location': node('location', 'schema:Place', { - 'schema:city': v('city') - }) - }) + 'schema:city': v('city'), + }), + }), }) ``` ### 4. Multiple Pattern Styles **SPARQL (one way):** + ```sparql ?product a schema:Product . ?product schema:name ?title . @@ -826,6 +835,7 @@ node('product', 'schema:Product', { ``` **Library (pick your style):** + ```typescript // Traditional triples triple('?product', 'rdf:type', 'schema:Product') @@ -836,20 +846,20 @@ triple('?product', 'schema:publisher', '?publisher') triples('?product', [ ['rdf:type', 'schema:Product'], ['schema:name', v('title')], - ['schema:publisher', v('publisher')] + ['schema:publisher', v('publisher')], ]) // Object syntax triples('?product', { 'rdf:type': 'schema:Product', 'schema:name': v('title'), - 'schema:publisher': v('publisher') + 'schema:publisher': v('publisher'), }) // Nested nodes node('product', 'schema:Product', { 'schema:name': v('title'), - 'schema:publisher': v('publisher') + 'schema:publisher': v('publisher'), }) // ASCII art (Cypher-inspired) @@ -859,37 +869,43 @@ cypher`${product}-[schema:publisher]->${publisher}` ### 5. Type Safety **SPARQL (no type checking):** + ```sparql FILTER(?age >= "eighteen") -- Runtime error! ``` **Library (caught at compile time):** + ```typescript -v('age').gte('eighteen') // TypeScript error: Type 'string' is not assignable -v('age').gte(18) // ✓ Correct +v('age').gte('eighteen') // TypeScript error: Type 'string' is not assignable +v('age').gte(18) // ✓ Correct ``` ### 6. Automatic Escaping **Raw SPARQL (manual escaping):** + ```sparql FILTER(?name = "O'Brien") -- Breaks! FILTER(?name = "O\\'Brien") -- Must escape manually ``` **Library (automatic):** + ```typescript -filter(v('name').eq("O'Brien")) // Escapes automatically +filter(v('name').eq("O'Brien")) // Escapes automatically ``` ### 7. Query Composition **SPARQL (copy-paste to reuse):** + ```sparql -- Can't easily compose queries ``` **Library (composable builders):** + ```typescript // Define base query const baseQuery = select(['?name', '?age']) @@ -905,6 +921,7 @@ const seniors = baseQuery.filter(v('age').gte(65)) ### 8. Fluent Aggregations **SPARQL:** + ```sparql SELECT ?city (COUNT(*) AS ?total) (AVG(?age) AS ?avgAge) WHERE { ... } @@ -913,6 +930,7 @@ HAVING(COUNT(*) >= 10) ``` **Library:** + ```typescript select([ v('city'), @@ -927,16 +945,18 @@ select([ ### 9. Reusable Patterns **SPARQL (copy-paste):** + ```sparql -- Person pattern used in multiple places - must copy ``` **Library (DRY):** + ```typescript // Define once const personWithEmail = node('person', 'foaf:Person', { 'foaf:name': v('name'), - 'foaf:mbox': v('email') + 'foaf:mbox': v('email'), }) // Reuse everywhere @@ -948,6 +968,7 @@ query3.where(personWithEmail) ### 10. Intuitive Variable Handling **SPARQL (must remember ? prefix):** + ```sparql SELECT ?name ?age WHERE { ?person foaf:name ?name . @@ -957,10 +978,11 @@ SELECT ?name ?age WHERE { ``` **Library (handles it):** + ```typescript -select(['?name', '?age']) // Accept with or without ? +select(['?name', '?age']) // Accept with or without ? .where(triple('?person', 'foaf:name', '?name')) - .filter(v('age').gte(18)) // v() function normalizes + .filter(v('age').gte(18)) // v() function normalizes ``` --- @@ -1005,15 +1027,15 @@ const query = select(['?product', '?finalPrice']) node('product', 'schema:Product', { 'schema:name': v('name'), 'schema:price': v('basePrice'), - 'schema:inStock': v('inStock') - }) + 'schema:inStock': v('inStock'), + }), ) .bind( ifElse( v('inStock').eq(true), v('basePrice').mul(0.9), - v('basePrice').add(10) - ).as('finalPrice') + v('basePrice').add(10), + ).as('finalPrice'), ) .filter(v('finalPrice').gte(10)) @@ -1039,12 +1061,14 @@ WHERE { ## Summary ### Complete Coverage + - ✅ **100%** of SPARQL 1.1 query language features - ✅ **100%** of SPARQL 1.1 update operations - ✅ **85+** built-in functions (all from spec) - ✅ **RDF-star** (quoted triples) support ### DX Enhancements + 1. **Fluent chaining** - Methods return chainable values 2. **Multiple pattern styles** - Triples, nested, ASCII art 3. **Type safety** - TypeScript catches errors at compile time @@ -1056,4 +1080,4 @@ WHERE { 9. **Progressive** - Start simple, add complexity as needed 10. **Standard compliant** - 1:1 mapping to SPARQL 1.1 -Every library feature maps directly to standard SPARQL 1.1 - you're never locked in. Call `.build().value` to get the raw SPARQL string anytime. \ No newline at end of file +Every library feature maps directly to standard SPARQL 1.1 - you're never locked in. Call `.build().value` to get the raw SPARQL string anytime. diff --git a/examples/aggregations.ts b/examples/aggregations.ts deleted file mode 100644 index d125287..0000000 --- a/examples/aggregations.ts +++ /dev/null @@ -1,307 +0,0 @@ -/** - * Aggregation Functions Example - * - * Demonstrates SPARQL aggregation functions: - * - COUNT, SUM, AVG, MIN, MAX - * - GROUP_CONCAT, SAMPLE - * - Using .as() for variable binding - * - GROUP BY patterns - * - * Updated to: - * - Use FOAF & narrative namespaces explicitly. - * - Work cleanly with Blazegraph + narrative.ttl. - */ - -import { - node, - rel, - select, - variable, - count, - countDistinct, - sum, - avg, - min, - max, - groupConcat, - sample, - transformResults, - type ExecutionConfig, -} from '../mod.ts' - -import { - FOAF, - RDFS, -} from '../namespaces.ts' - -const config: ExecutionConfig = { - endpoint: 'http://localhost:9999/blazegraph/sparql', - timeoutMs: 30000, -} - -// ============================================================================ -// Example 1: COUNT - Simple Counting (FOAF) -// ============================================================================ - -async function countPeople() { - console.log('\n=== Counting total people ===\n') - - const person = node('person', 'foaf:Person') - - try { - const result = await select([count().as('total')]) - .prefix('foaf', FOAF._namespace) - .where(person) - // 🔧 NEW: make the aggregation legal in Blazegraph - .groupBy('?total') - .execute(config) - - const rows = transformResults(result) - console.log('Total people:', rows) - } catch (e) { - console.error('Query failed:', e) - } -} - -// ============================================================================ -// Example 2: COUNT with GROUP BY (FOAF) -// ============================================================================ - -async function countByAge() { - console.log('\n=== Counting people by age group ===\n') - - const person = node('person', 'foaf:Person') - .with.prop('foaf:age', variable('age')) - - try { - const result = await select([ - '?age', - count(variable('person')).as('count'), - ]) - .prefix('foaf', FOAF._namespace) - .where(person) - // 🔧 NEW: make the aggregation legal in Blazegraph - .groupBy('?age') - .orderBy('?count', 'DESC') - .execute(config) - - const rows = transformResults(result) - console.log('Count by age:', rows) - } catch (e) { - console.error('Query failed:', e) - } -} - -// ============================================================================ -// Example 3: COUNT DISTINCT (FOAF) -// ============================================================================ - -async function countUniqueNames() { - console.log('\n=== Counting unique names ===\n') - - const person = node('person', 'foaf:Person') - .with.prop('foaf:name', variable('name')) - - try { - const result = await select([ - countDistinct(variable('name')).as('uniqueNames'), - ]) - .prefix('foaf', FOAF._namespace) - .where(person) - // 🔧 NEW: make the aggregation legal in Blazegraph - .groupBy('?uniqueNames') - .execute(config) - - const rows = transformResults(result) - console.log('Unique names:', rows) - } catch (e) { - console.error('Query failed:', e) - } -} - -// ============================================================================ -// Example 4: SUM, AVG, MIN, MAX - Numeric Aggregations (FOAF) -// ============================================================================ - -async function numericAggregations() { - console.log('\n=== Numeric aggregations on age ===\n') - - const person = node('person', 'foaf:Person') - .with.prop('foaf:age', variable('age')) - - try { - const result = await select([ - sum(variable('age')).as('totalAge'), - avg(variable('age')).as('avgAge'), - min(variable('age')).as('minAge'), - max(variable('age')).as('maxAge'), - ]) - .prefix('foaf', FOAF._namespace) - .where(person) - // 🔧 NEW: make the aggregation legal in Blazegraph - .groupBy('?age') - .execute(config) - - const rows = transformResults(result) - console.log('Age statistics:', rows) - } catch (e) { - console.error('Query failed:', e) - } -} - -// ============================================================================ -// Example 5: GROUP_CONCAT - String Aggregation (FOAF) -// ============================================================================ - -async function concatenateNames() { - console.log('\n=== Concatenating friend names ===\n') - - const person = node('person', 'foaf:Person') - .with.prop('foaf:name', variable('personName')) - - const friend = node('friend', 'foaf:Person') - .with.prop('foaf:name', variable('friendName')) - - const knows = rel('person', 'foaf:knows', 'friend') - - try { - const result = await select([ - '?personName', - groupConcat(variable('friendName'), ', ').as('allFriends'), - ]) - .prefix('foaf', FOAF._namespace) - .where(person) - .where(friend) - .where(knows) - // 🔧 NEW: make the aggregation legal in Blazegraph - .groupBy('?personName') - .orderBy('?personName') - .execute(config) - - const rows = transformResults(result) - console.log('Friends list:', rows) - } catch (e) { - console.error('Query failed:', e) - } -} - -// ============================================================================ -// Example 6: SAMPLE - Arbitrary Value Selection (FOAF) -// ============================================================================ - -async function sampleValues() { - console.log('\n=== Sampling one value per group ===\n') - - const person = node('person', 'foaf:Person') - .with.prop('foaf:age', variable('age')) - .and.prop('foaf:name', variable('name')) - - try { - const result = await select([ - '?age', - sample(variable('name')).as('exampleName'), - ]) - .prefix('foaf', FOAF._namespace) - .where(person) - // 🔧 NEW: make the aggregation legal in Blazegraph - .groupBy('?age') - .orderBy('?age') - .execute(config) - - const rows = transformResults(result) - console.log('Sample names by age:', rows) - } catch (e) { - console.error('Query failed:', e) - } -} - -// ============================================================================ -// Example 7: Multiple Aggregations with Filtering (FOAF) -// ============================================================================ - -async function complexAggregation() { - console.log('\n=== Complex aggregation with multiple metrics ===\n') - - const person = node('person', 'foaf:Person') - .with.prop('foaf:name', variable('name')) - .and.prop('foaf:age', variable('age')) - - const friend = node('friend', 'foaf:Person') - - const knows = rel('person', 'foaf:knows', 'friend') - - try { - const result = await select([ - '?name', - '?age', - count(variable('friend')).as('friendCount'), - avg(variable('age')).as('avgAge'), - ]) - .prefix('foaf', FOAF._namespace) - .where(person) - .where(friend) - .where(knows) - // 🔧 NEW: make the aggregation legal in Blazegraph - .groupBy('?name', '?age') - .orderBy('?friendCount', 'DESC') - .limit(10) - .execute(config) - - const rows = transformResults(result) - console.log('Complex aggregation:', rows) - } catch (e) { - console.error('Query failed:', e) - } -} - -// ============================================================================ -// Example 8: Pop Modern - Count Comics by Publisher (narrative.ttl) -// ============================================================================ - -async function countComicsByPublisher() { - console.log('\n=== Counting comics by publisher ===\n') - - const comic = node('comic', 'narrative:Product') - - const publisher = node('publisher', 'narrative:Organization') - .with.prop('rdfs:label', variable('publisherName')) - - const publishedBy = rel('comic', 'narrative:publishedBy', 'publisher') - - try { - const result = await select([ - '?publisherName', - count(variable('comic')).as('comicCount'), - ]) - .prefix('narrative', "http://knowledge.graph/ontology/narrative#") - .prefix('rdfs', RDFS._namespace) - .where(comic) - .where(publisher) - .where(publishedBy) - // 🔧 NEW: make the aggregation legal in Blazegraph - .groupBy('?publisherName') - .orderBy('?comicCount', 'DESC') - .limit(20) - .execute(config) - - const rows = transformResults(result) - console.log('Comics by publisher:', rows) - } catch (e) { - console.error('Query failed:', e) - } -} - -// ============================================================================ -// Run Examples -// ============================================================================ - -if (import.meta.main) { - await countPeople() - await countByAge() - await countUniqueNames() - await numericAggregations() - await concatenateNames() - await sampleValues() - await complexAggregation() - await countComicsByPublisher() -} diff --git a/examples/basic.ts b/examples/basic.ts deleted file mode 100644 index 9bcfa25..0000000 --- a/examples/basic.ts +++ /dev/null @@ -1,137 +0,0 @@ -/** - * Basic Query Example - Updated for narrative.ttl ontology - */ - -import { - node, - select, - variable, - type ExecutionConfig, -} from '../mod.ts' - -import { RDFS } from '../namespaces.ts' - -const NARRATIVE = 'http://knowledge.graph/ontology/narrative#' - -const config: ExecutionConfig = { - endpoint: 'http://localhost:9999/blazegraph/sparql', - timeoutMs: 30000, -} - -// ============================================================================ -// Example 1: Find Creators (People) -// ============================================================================ - -async function findCreators() { - console.log('\n=== Finding creators ===\n') - - const person = node('person', 'narrative:Person') - .with.prop('narrative:knownAs', variable('name')) - - const query = select(['?person', '?name']) - .prefix('narrative', NARRATIVE) - .where(person) - .orderBy('?name') - .limit(10) - - console.log(query.build().value) - - try { - const result = await query.execute(config) - console.log('Results:', result.results.bindings) - } catch (e) { - console.error('Query failed:', e) - } -} - -// ============================================================================ -// Example 2: Find Publishers (Organizations) -// ============================================================================ - -async function findPublishers() { - console.log('\n=== Finding publishers ===\n') - - const org = node('org', 'narrative:Org') - .with.prop('rdfs:label', variable('name')) - .and.prop('narrative:orgType', variable('type')) - - const query = select(['?name', '?type']) - .prefix('narrative', NARRATIVE) - .prefix('rdfs', RDFS._namespace) - .where(org) - .orderBy('?name') - .limit(10) - - console.log(query.build().value) - - try { - const result = await query.execute(config) - console.log('Results:', result.results.bindings) - } catch (e) { - console.error('Query failed:', e) - } -} - -// ============================================================================ -// Example 3: Find Comics (StoryExpressions) with Series info -// ============================================================================ - -async function findComics() { - console.log('\n=== Finding comics ===\n') - - const issue = node('issue', 'narrative:StoryExpression') - .with.prop('narrative:issueNumber', variable('number')) - .and.prop('narrative:coverDate', variable('date')) - - const query = select(['?issue', '?number', '?date']) - .prefix('narrative', NARRATIVE) - .where(issue) - .orderBy('?date', 'DESC') - .limit(10) - - console.log(query.build().value) - - try { - const result = await query.execute(config) - console.log('Results:', result.results.bindings) - } catch (e) { - console.error('Query failed:', e) - } -} - -// ============================================================================ -// Example 4: Find Characters -// ============================================================================ - -async function findCharacters() { - console.log('\n=== Finding characters ===\n') - - const character = node('char', 'narrative:Character') - .with.prop('narrative:characterName', variable('name')) - - const query = select(['?char', '?name']) - .prefix('narrative', NARRATIVE) - .where(character) - .orderBy('?name') - .limit(20) - - console.log(query.build().value) - - try { - const result = await query.execute(config) - console.log('Results:', result.results.bindings) - } catch (e) { - console.error('Query failed:', e) - } -} - -// ============================================================================ -// Run examples -// ============================================================================ - -if (import.meta.main) { - await findCreators() - await findPublishers() - await findComics() - await findCharacters() -} \ No newline at end of file diff --git a/examples/complex.ts b/examples/complex.ts deleted file mode 100644 index e30d388..0000000 --- a/examples/complex.ts +++ /dev/null @@ -1,229 +0,0 @@ -/** - * Complex Query Example - * - * Demonstrates advanced query patterns: - * - Relationships between nodes - * - Multi-hop graph traversal - * - Optional patterns - * - Aggregation and grouping - * - Union queries - * - * Updated to: - * - Explicitly declare FOAF and narrative prefixes. - * - Make the PopModern example (`findCreatorsAndPublishers`) line up with - * your narrative.ttl ontology. - */ - -import { - node, - rel, - select, - variable, - str, - gte, - count, - transformResults, - type ExecutionConfig, -} from '../mod.ts' - -import { - FOAF, - RDFS, - SCHEMA, - getNamespaceIRI -} from '../namespaces.ts' - -const config: ExecutionConfig = { - endpoint: 'http://localhost:9999/blazegraph/sparql', - timeoutMs: 30000, -} - -// ============================================================================ -// Example 1: Social Network Query (FOAF) -// ============================================================================ - -async function findFriendsOfFriends() { - console.log('\n=== Finding friends of friends ===\n') - - // Person A - const personA = node('personA', 'foaf:Person') - .with.prop('foaf:name', 'Alice') - - // Person B (direct friend) - const personB = node('personB', 'foaf:Person') - .with.prop('foaf:name', variable('friendName')) - - // Person C (friend of friend) - const personC = node('personC', 'foaf:Person') - .with.prop('foaf:name', variable('friendOfFriendName')) - - // Relationships - const knowsB = rel('personA', 'foaf:knows', 'personB') - const knowsC = rel('personB', 'foaf:knows', 'personC') - - try { - const result = await select(['?friendName', '?friendOfFriendName']) - .prefix('foaf', FOAF._namespace) - .prefix('ex', getNamespaceIRI(SCHEMA)) - .where(personA) - .where(personB) - .where(personC) - .where(knowsB) - .where(knowsC) - .execute(config) - - const rows = transformResults(result) - console.log('Friends of friends:', rows) - } catch (e) { - console.error('Query failed:', e) - } -} - -// ============================================================================ -// Example 2: Optional Properties (FOAF) -// ============================================================================ - -async function findPeopleWithOptionalEmail() { - console.log('\n=== Finding people (email optional) ===\n') - - const person = node('person', 'foaf:Person') - .with.prop('foaf:name', variable('name')) - - // Email is optional - const emailPattern = node('person') - .with.prop('foaf:email', variable('email')) - - try { - const result = await select(['?name', '?email']) - .prefix('foaf', FOAF._namespace) - .prefix('ex', getNamespaceIRI(SCHEMA)) - .where(person) - .optional(emailPattern) - .orderBy('?name') - .limit(20) - .execute(config) - - const rows = transformResults(result) - console.log('People (with/without email):', rows) - } catch (e) { - console.error('Query failed:', e) - } -} - -// ============================================================================ -// Example 3: Aggregation - Count Friends (FOAF) -// ============================================================================ - -async function countFriendsPerPerson() { - console.log('\n=== Counting friends per person ===\n') - - const person = node('person', 'foaf:Person') - .with.prop('foaf:name', variable('name')) - - const friend = node('friend', 'foaf:Person') - - const knows = rel('person', 'foaf:knows', 'friend') - - try { - const result = await select(['?name', count(variable('friend')).as('friendCount')]) - .prefix('foaf', FOAF._namespace) - .prefix('ex', getNamespaceIRI(SCHEMA)) - .where(person) - .where(friend) - .where(knows) - .groupBy("?name") - .orderBy('?friendCount', 'DESC') - .limit(10) - .execute(config) - - const rows = transformResults(result) - console.log('Friend counts:', rows) - } catch (e) { - console.error('Query failed:', e) - } -} - -// ============================================================================ -// Example 4: Union - Find Creators OR Publishers (narrative.ttl) -// ============================================================================ - -async function findCreatorsAndPublishers() { - console.log('\n=== Finding creators OR publishers ===\n') - - // Branch 1: Creators in your narrative graph - const creator = node('entity', 'narrative:Person') - .with.prop('foaf:name', variable('name')) - .and.prop('narrative:role', variable('role')) - - // Branch 2: Publishers in your narrative graph - const publisher = node('entity', 'narrative:Organization') - .with.prop('rdfs:label', variable('name')) - - try { - const result = await select(['?name', '?role']) - .prefix('narrative', "http://knowledge.graph/ontology/narrative#") - .prefix('foaf', FOAF._namespace) - .prefix('rdfs', RDFS._namespace) - .prefix('ex', getNamespaceIRI(SCHEMA)) - .union(creator, publisher) - .orderBy('?name') - .limit(20) - .execute(config) - - const rows = transformResults(result) - console.log('Creators and publishers:', rows) - } catch (e) { - console.error('Query failed:', e) - } -} - -// ============================================================================ -// Example 5: Relationship with Properties (Generic / Example namespace) -// ============================================================================ - -async function findHighConfidenceConnections() { - console.log('\n=== Finding high-confidence relationships ===\n') - - const personA = node('personA', 'foaf:Person') - .with.prop('foaf:name', variable('nameA')) - - const personB = node('personB', 'foaf:Person') - .with.prop('foaf:name', variable('nameB')) - - // Relationship with confidence score (using a made-up ex: namespace) - const connection = rel('personA', 'ex:relatedTo', 'personB') - .with.prop('ex:confidence', variable('confidence')) - .and.prop('ex:source', variable('source')) - - try { - const result = await select(['?nameA', '?nameB', '?confidence', '?source']) - // In your real code, you'd also wire `ex` via namespaces.ts; - // for now we'll assume Blazegraph has PREFIX ex: already configured. - .prefix('foaf', FOAF._namespace) - .prefix('ex', getNamespaceIRI(SCHEMA)) - .where(personA) - .where(personB) - .where(connection) - .filter(gte(variable('confidence'), 0.8)) - .orderBy('?confidence', 'DESC') - .limit(20) - .execute(config) - - const rows = transformResults(result) - console.log('High-confidence connections:', rows) - } catch (e) { - console.error('Query failed:', e) - } -} - -// ============================================================================ -// Run examples -// ============================================================================ - -if (import.meta.main) { - await findFriendsOfFriends() - await findPeopleWithOptionalEmail() - await countFriendsPerPerson() - await findCreatorsAndPublishers() - await findHighConfidenceConnections() -} diff --git a/examples/rdf.ts b/examples/rdf.ts new file mode 100644 index 0000000..9692d54 --- /dev/null +++ b/examples/rdf.ts @@ -0,0 +1,11 @@ +import * as rdf from '@okikio/rdf' +import * as nquads from '@okikio/rdf/nquads' + +const schema = rdf.namespace('https://schema.org/') +const product = rdf.namedNode('https://example.com/products/1') +const data = rdf.dataset([ + rdf.quad(product, rdf.namedNode(rdf.RDF.type), schema('Product')), + rdf.quad(product, schema('name'), rdf.literal('Widget')), +]) + +console.log(nquads.write(data)) diff --git a/examples/sparql.ts b/examples/sparql.ts new file mode 100644 index 0000000..f8fa884 --- /dev/null +++ b/examples/sparql.ts @@ -0,0 +1,13 @@ +import * as sparql from '@okikio/sparql' +import * as http from '@okikio/sparql/http' + +const query = sparql.select(['?product', '?name']) + .prefix('schema', 'https://schema.org/') + .where(sparql.triple('?product', 'schema:name', '?name')) + .orderBy('?name') + .limit(25) + +console.log(query.build().value) + +const client = http.create({ endpoint: 'https://example.com/sparql' }) +void client diff --git a/examples/store.ts b/examples/store.ts new file mode 100644 index 0000000..87683a2 --- /dev/null +++ b/examples/store.ts @@ -0,0 +1,11 @@ +import * as rdf from '@okikio/rdf' +import { type FileSystemType, open } from '@okikio/triplestore' + +export async function saveExample(fileSystem: FileSystemType): Promise { + await using store = await open(fileSystem, { path: '/knowledge' }) + await store.add(rdf.quad( + rdf.namedNode('https://example.com/products/1'), + rdf.namedNode('https://schema.org/name'), + rdf.literal('Widget'), + )) +} diff --git a/examples/vocab.ts b/examples/vocab.ts new file mode 100644 index 0000000..ed7b591 --- /dev/null +++ b/examples/vocab.ts @@ -0,0 +1,17 @@ +import * as rdf from '@okikio/rdf' +import { name, Product, ProductSchema, type ProductType } from '@okikio/vocab/schema' + +const value: ProductType = { + '@type': 'Product', + name: 'Widget', +} + +const result = ProductSchema['~standard'].validate(value) +if (result instanceof Promise) { + throw new TypeError('Generated bootstrap schema is expected to validate synchronously.') +} +if (result.issues) throw new TypeError(result.issues[0]?.message ?? 'Invalid product.') + +const subject = rdf.namedNode('https://example.com/products/1') +console.log(rdf.quad(subject, rdf.namedNode(rdf.RDF.type), Product)) +console.log(rdf.quad(subject, name, rdf.literal(value.name as string)))