diff --git a/builder.ts b/builder.ts index 44a3b53..a7bdb09 100644 --- a/builder.ts +++ b/builder.ts @@ -54,7 +54,7 @@ import { filter as filterExpr, optional as optionalExpr, } from './utils.ts' -import { createExecutor, type ExecutorConfig, type SparqlResult } from './executor.ts' +import { createExecutor, type BindingMap, type ExecutionConfig, type QueryResult } from './executor.ts' // ============================================================================ // Core Query Types @@ -985,9 +985,9 @@ export class QueryBuilder { * } * ``` */ - execute(config: ExecutorConfig): Promise { + execute(config: ExecutionConfig): Promise> { const executor = createExecutor(config) - return executor(this.build()) + return executor.execute(this.build()) } } @@ -1045,9 +1045,9 @@ export function subquery(builder: QueryBuilder): SparqlValue { * ) * ``` */ -export function execute( +export function execute( builder: QueryBuilder, - config: ExecutorConfig -): Promise { - return builder.execute(config) + config: ExecutionConfig +): Promise> { + return builder.execute(config) } \ No newline at end of file diff --git a/executor.ts b/executor.ts index 269355c..c05c0d0 100644 --- a/executor.ts +++ b/executor.ts @@ -1,1071 +1,1009 @@ -/** - * SPARQL query execution with type-safe error handling. - * - * Executing SPARQL queries involves network requests that can fail in many ways - timeouts, - * bad syntax, server errors, or network issues. This module provides discriminated error - * types so you can handle each failure mode appropriately. - * - * The result is always a discriminated union: either success with data, or failure with - * a specific error type. This forces you to handle errors explicitly rather than letting - * exceptions bubble up unexpectedly. - * - * Think of this as a type-safe fetch for SPARQL. It handles the HTTP details, parses - * responses, and gives you structured errors when things go wrong. - * - * @module - */ - -import type { SparqlValue } from './sparql.ts' - -// ============================================================================ -// Configuration -// ============================================================================ +// executor.ts + +import sparql, { + raw, + uri, + type SparqlValue, +} from './sparql.ts' +import { + XSD, + RDF, + RDFS, + FOAF, + SCHEMA, +} from './namespaces.ts' /** - * Configuration for SPARQL endpoint connections. - * - * At minimum you need the endpoint URL. Optionally specify timeout and custom - * headers for authentication or other purposes. + * Raw SPARQL JSON binding value. + * + * Mirrors the SPARQL 1.1 JSON Results format. */ -export interface ExecutorConfig { - /** SPARQL endpoint URL (e.g., http://localhost:9999/sparql) */ - readonly endpoint: string - /** Query timeout in milliseconds (default: 30000) */ - readonly timeout?: number - /** Additional headers for requests (auth, etc.) */ - readonly headers?: Record +export interface BindingValue { + type: 'uri' | 'literal' | 'bnode' + value: string + 'xml:lang'?: string + datatype?: string } -// ============================================================================ -// Error Types -// ============================================================================ - /** - * Specific error type discriminants. - * - * Each error type represents a different failure mode: - * - syntax: Your SPARQL query has invalid syntax (400 response) - * - timeout: Query took too long and was aborted - * - unavailable: Can't connect to endpoint (network error or 503) - * - database: Server had an internal error (5xx responses) - * - unknown: Something unexpected happened + * Map of variable name → binding. + * + * Keys are variable names **without** the leading `?`. */ -export type SparqlErrorType = 'syntax' | 'timeout' | 'unavailable' | 'database' | 'unknown' +export type BindingMap = Record /** - * Structured error information. - * - * Provides context about what went wrong. The type discriminant tells you what - * kind of error it is, and the details provide additional context. + * Standard SPARQL JSON result shape. + * + * - `head.vars` lists variable names. + * - `results.bindings` holds rows. + * - `boolean` is present for ASK queries. */ -export interface SparqlError { - readonly type: SparqlErrorType - readonly message: string - readonly statusCode?: number - readonly details?: unknown +export interface QueryResult { + head: { + vars: string[] + } + results: { + bindings: TBind[] + } + /** + * Present for ASK queries. + */ + boolean?: boolean } -// ============================================================================ -// Result Types -// ============================================================================ +/* ============================================================================ + * Error types + * ========================================================================== */ -/** - * Raw binding value from SPARQL results. - * - * This is what the SPARQL endpoint returns for each bound variable. It includes - * type information so you know whether it's an IRI, literal, or blank node. - */ -export interface SparqlBinding { - readonly type: string - readonly value: string - readonly 'xml:lang'?: string - readonly datatype?: string -} +export type QueryErrorKind = + | 'http' // non-2xx HTTP status + | 'timeout' // request hit a client-side timeout + | 'abort' // caller-provided signal aborted the request + | 'network' // fetch never got a response (DNS, connection reset, etc.) + | 'protocol' // endpoint returned non-JSON / wrong shape when JSON expected + | 'unknown' // anything else unexpected /** - * Raw SPARQL JSON results format. - * - * This follows the SPARQL 1.1 Query Results JSON Format specification. Each - * result row is an object mapping variable names to bindings. + * Rich error type for SPARQL execution. + * + * Everything in here is designed to be safe to log and inspect. */ -export interface SparqlResponse { - readonly results: { - readonly bindings: ReadonlyArray> +export class QueryError extends Error { + readonly kind: QueryErrorKind + readonly status?: number + readonly responseBody?: unknown + readonly query: string + + constructor(init: { + message: string + kind: QueryErrorKind + query: string | SparqlValue + status?: number + responseBody?: unknown + cause?: unknown + }) { + super(init.message) + this.name = 'QueryError' + this.kind = init.kind + this.status = init.status + this.responseBody = init.responseBody + this.query = typeof init.query === 'string' ? init.query : init.query.value + this.cause = init.cause } } +/** Type guard for handling timeouts specifically. */ +export function isTimeoutError(error: unknown): error is QueryError & { kind: 'timeout' } { + return error instanceof QueryError && error.kind === 'timeout' +} + +/** Type guard for user aborts (external AbortSignal). */ +export function isAbortError(error: unknown): error is QueryError & { kind: 'abort' } { + return error instanceof QueryError && error.kind === 'abort' +} + +/* ============================================================================ + * Config + low-level execution + * ========================================================================== */ + /** - * Successful query result. - * - * When a query succeeds, you get this. The data is in standard SPARQL JSON - * format. You can transform it with the helper functions or process it directly. + * Shared configuration for an executor. */ -export interface SparqlSuccess { - readonly success: true - readonly data: SparqlResponse +export interface ExecutionConfig { + /** SPARQL endpoint URL. */ + endpoint: string + /** + * Optional fetch implementation (Deno, browser, node-fetch, etc.). + * Defaults to the global `fetch`. + */ + fetch?: typeof fetch + /** Extra headers for every request (auth, tenant, etc.). */ + headers?: HeadersInit + /** + * Default timeout in milliseconds for all requests from this executor. + * - `undefined` / `0` = no default timeout + */ + timeoutMs?: number } /** - * Failed query result. - * - * When a query fails, you get structured error information. Check the error - * type to determine how to handle it. + * Per-call options for executing a query. + * + * You can override the default timeout, and/or pass an `AbortSignal`. */ -export interface SparqlFailure { - readonly success: false - readonly error: SparqlError +export interface RequestOptions { + /** + * Timeout in milliseconds. + * - `undefined` → falls back to `ExecutionConfig.timeoutMs` + * - `0` or `null` → disable timeout for this call + */ + timeoutMs?: number | null + + /** + * Optional AbortSignal to cancel the request from the outside. + * + * Example: + * ```ts + * const controller = new AbortController() + * const promise = executor.query(query, { signal: controller.signal }) + * controller.abort() + * ``` + */ + signal?: AbortSignal } /** - * Discriminated union of query results. - * - * Every query execution returns this type. You check the `success` field to - * determine which variant you have, then TypeScript narrows the type appropriately. - * - * @example Handling results - * ```ts - * const result = await execute(query, config) - * - * if (result.success) { - * // result.data is available - * console.log(result.data.results.bindings) - * } else { - * // result.error is available - * console.error(result.error.type, result.error.message) - * } - * ``` + * Execute a SPARQL query (SELECT / CONSTRUCT / ASK / UPDATE) against an endpoint. + * + * This is the lowest-level building block. Higher-level helpers (builder, update, + * executor) all use this under the hood. + * + * @param config Endpoint + HTTP settings + * @param query Text SPARQL or a `SparqlValue` (tagged template / builder output) */ -export type SparqlResult = SparqlSuccess | SparqlFailure +export async function executeSparql( + config: ExecutionConfig, + query: string | SparqlValue, + options: RequestOptions = {}, +): Promise { + const { + endpoint, + fetch: fetchImpl = fetch, + headers, + timeoutMs: defaultTimeout, + } = config + + const queryText = typeof query === 'string' ? query : query.value + const effectiveTimeout = + options.timeoutMs === null + ? 0 + : options.timeoutMs ?? defaultTimeout ?? 0 + + let controller: AbortController | undefined + let signal: AbortSignal | undefined + let timeoutId: ReturnType | undefined + let timedOut = false + let abortedByUser = false + + // Combine timeout + user AbortSignal into a single signal for fetch. + if (effectiveTimeout > 0 || options.signal) { + controller = new AbortController() + signal = controller.signal + + if (effectiveTimeout > 0) { + timeoutId = setTimeout(() => { + timedOut = true + controller!.abort() + }, effectiveTimeout) + } -// ============================================================================ -// Core Execution -// ============================================================================ + if (options.signal) { + if (options.signal.aborted) { + abortedByUser = true + controller.abort(options.signal.reason) + } else { + options.signal.addEventListener( + 'abort', + () => { + abortedByUser = true + controller!.abort(options.signal?.reason) + }, + { once: true }, + ) + } + } + } else { + signal = options.signal + } -/** - * Execute a SPARQL query against an endpoint. - * - * Sends the query via HTTP POST and handles the response. Network errors, - * timeouts, and HTTP errors are all converted to structured error types. - * - * The function follows SPARQL 1.1 Protocol conventions: - * - POST request with Content-Type: application/sparql-query - * - Accept: application/sparql-results+json - * - 400 responses indicate syntax errors - * - 5xx responses indicate server errors - * - * @param config Endpoint configuration - * @param query Query to execute - * @param overrides Optional per-query config overrides - * @returns Discriminated result union - * - * @example Basic execution - * ```ts - * const result = await executeSparql( - * { endpoint: 'http://localhost:9999/sparql' }, - * sparql`SELECT * WHERE { ?s ?p ?o } LIMIT 10` - * ) - * - * if (result.success) { - * console.log(result.data) - * } else { - * console.error(result.error.type, result.error.message) - * } - * ``` - * - * @example With overrides - * ```ts - * const result = await executeSparql( - * { endpoint: 'http://localhost:9999/sparql', timeout: 30000 }, - * query, - * { timeout: 60000 } // Use longer timeout for this query - * ) - * ``` - */ -export async function executeSparql( - config: ExecutorConfig, - query: SparqlValue | string, - overrides?: Partial -): Promise { - const endpoint = overrides?.endpoint ?? config.endpoint - const timeout = overrides?.timeout ?? config.timeout ?? 30000 - const headers = { ...config.headers, ...overrides?.headers } - - // Extract query string - const queryString = typeof query === 'string' - ? query - : query.value - - // Setup timeout - const controller = new AbortController() - const timeoutId = setTimeout(() => controller.abort(), timeout) + let response: Response try { - const response = await fetch(endpoint, { + response = await fetchImpl(endpoint, { method: 'POST', headers: { - 'Content-Type': 'application/sparql-query', - 'Accept': 'application/sparql-results+json', + 'Content-Type': 'application/sparql-query; charset=utf-8', + Accept: 'application/sparql-results+json, application/json', ...headers, }, - body: queryString, - signal: controller.signal, + body: queryText, + signal, }) - - clearTimeout(timeoutId) - - // Handle HTTP errors - if (!response.ok) { - const text = await response.text() - - // 400 = Invalid SPARQL syntax (per SPARQL 1.1 Protocol) - if (response.status === 400) { - return { - success: false, - error: { - type: 'syntax', - message: `Invalid SPARQL syntax: ${text}`, - statusCode: response.status, - details: text, - }, - } - } - - // 503 = Service unavailable - if (response.status === 503) { - return { - success: false, - error: { - type: 'unavailable', - message: `SPARQL endpoint unavailable: ${text}`, - statusCode: response.status, - details: text, - }, - } + } catch (err) { + if (timeoutId) clearTimeout(timeoutId) + + // Abort / timeout cases – runtime-dependent, so we detect conservatively. + if ( + err instanceof DOMException || + (err instanceof Error && err.name === 'AbortError') + ) { + if (timedOut) { + throw new QueryError({ + kind: 'timeout', + message: `SPARQL request timed out after ${effectiveTimeout}ms`, + query, + cause: err, + }) } - - // 5xx = Database error - if (response.status >= 500) { - return { - success: false, - error: { - type: 'database', - message: `Database error: ${text}`, - statusCode: response.status, - details: text, - }, - } + if (abortedByUser) { + throw new QueryError({ + kind: 'abort', + message: 'SPARQL request was aborted by caller', + query, + cause: err, + }) } + throw new QueryError({ + kind: 'network', + message: 'SPARQL request was aborted or failed before a response was received', + query, + cause: err, + }) + } - // Other errors - return { - success: false, - error: { - type: 'unknown', - message: `HTTP error ${response.status}: ${text}`, - statusCode: response.status, - details: text, - }, - } + // Typical "cannot fetch" in many environments. + if (err instanceof TypeError) { + throw new QueryError({ + kind: 'network', + message: `Network error while calling SPARQL endpoint: ${err.message}`, + query, + cause: err, + }) } - // Parse successful response - const data = await response.json() as SparqlResponse + throw new QueryError({ + kind: 'unknown', + message: 'Unexpected error while calling SPARQL endpoint', + query, + cause: err, + }) + } finally { + if (timeoutId) clearTimeout(timeoutId) + } - return { - success: true, - data, - } - } catch (error) { - clearTimeout(timeoutId) - - // Timeout error (AbortError) - if (error instanceof Error && error.name === 'AbortError') { - return { - success: false, - error: { - type: 'timeout', - message: `Query timeout after ${timeout}ms`, - details: error, - }, - } - } + const text = await response.text() - // Network error (TypeError - cannot connect) - if (error instanceof TypeError) { - return { - success: false, - error: { - type: 'unavailable', - message: `Cannot connect to SPARQL endpoint: ${error.message}`, - details: error, - }, - } + if (!response.ok) { + let body: unknown = text + try { + body = text ? JSON.parse(text) : undefined + } catch { + // ignore – keep raw text } - // Unknown error - return { - success: false, - error: { - type: 'unknown', - message: error instanceof Error ? error.message : String(error), - details: error, - }, - } + // Treat all non-2xx as HTTP-level failures; the caller can inspect status. + throw new QueryError({ + kind: 'http', + message: `SPARQL HTTP ${response.status} ${response.statusText}`, + status: response.status, + responseBody: body, + query, + }) + } + + // Successful (2xx) with no body – quite common for UPDATEs. + if (!text) { + return undefined as T + } + + // Many endpoints return JSON for SELECT/ASK and raw RDF for CONSTRUCT/DESCRIBE. + // We try JSON first; if that fails, return raw text. + try { + return JSON.parse(text) as T + } catch (err) { + throw new QueryError({ + kind: 'protocol', + message: 'Expected SPARQL JSON results but endpoint returned non-JSON response', + status: response.status, + responseBody: text, + query, + cause: err, + }) } } +/* ============================================================================ + * High-level Executor + * ========================================================================== */ + /** - * Create an executor function with pre-configured settings. - * - * This is useful when you have a fixed endpoint and want to execute multiple - * queries without repeating the configuration. The returned function can still - * accept overrides for individual queries. - * - * @param config Default executor configuration - * @returns Executor function - * - * @example Create reusable executor - * ```ts - * const execute = createExecutor({ - * endpoint: 'http://localhost:9999/sparql', - * timeout: 30000, - * headers: { 'Authorization': 'Bearer token123' } - * }) - * - * // Use it for multiple queries - * const result1 = await execute(query1) - * const result2 = await execute(query2) - * const result3 = await execute(query3, { timeout: 60000 }) // Override for one query - * ``` + * High-level executor interface returned by {@link createExecutor}. + * + * This is the "one stop shop" you hand to your application code: + * + * - `execute` → raw JSON result + * - `query` → SELECT/DESCRIBE/CONSTRUCT with typed bindings + * - `ask` → boolean ASK queries + * - `update` → SPARQL UPDATE (INSERT / DELETE / LOAD / CLEAR / etc.) + * - `resolveLabels` → best-effort human label for URIs + * - `fetchProperties` → property map for URIs + * - `expand` → fuse labels + properties into a single enriched view */ -export function createExecutor(config: ExecutorConfig): ( - query: SparqlValue | string, - overrides?: Partial -) => Promise { - return ( - query: SparqlValue | string, - overrides?: Partial - ): Promise => { - return executeSparql(config, query, overrides) - } +export interface Executor { + /** Low-level "give me the raw SPARQL JSON result" call. */ + execute( + query: string | SparqlValue, + options?: RequestOptions, + ): Promise> + + /** SELECT / DESCRIBE / CONSTRUCT helper returning coerced rows. */ + query( + query: string | SparqlValue, + options?: RequestOptions, + ): Promise> + + /** ASK helper – returns the boolean. */ + ask( + query: string | SparqlValue, + options?: RequestOptions, + ): Promise + + /** SPARQL UPDATE – throws on error, otherwise resolves to void. */ + update( + query: string | SparqlValue, + options?: RequestOptions, + ): Promise + + /** Resolve human-readable labels for URIs. */ + resolveLabels( + uris: string[], + config?: LabelResolutionConfig, + ): Promise> + + /** Fetch selected properties for URIs. */ + fetchProperties( + uris: string[], + config: PropertyFetchConfig, + ): Promise>> + + /** Merge labels + properties into a richer representation. */ + expand( + uris: string[], + config?: ExpandConfig, + ): Promise } -// ============================================================================ -// Result Transformation -// ============================================================================ +/** Internal helper type for passing `execute` into helpers. */ +type ExecuteFn = ( + query: string | SparqlValue, + options?: RequestOptions, +) => Promise> /** - * Transform SPARQL bindings to simple key-value objects. - * - * The raw SPARQL response format includes type information for each value. - * Often you just want the values themselves. This helper strips the metadata - * and gives you plain objects. - * - * @param response SPARQL response data - * @returns Array of simple objects - * - * @example + * Create an {@link Executor} bound to a specific SPARQL endpoint. + * + * @example Simple usage * ```ts - * const result = await execute(query, config) - * if (result.success) { - * const rows = transformResults(result.data) - * // [{ name: 'Alice', age: '30' }, { name: 'Bob', age: '25' }] - * - * for (const row of rows) { - * console.log(row.name, row.age) - * } - * } + * const executor = createExecutor({ endpoint: 'https://dbpedia.org/sparql' }) + * + * const rows = await executor.query<{ name: BindingValue }>( + * `SELECT ?name WHERE { dbr:Toronto foaf:name ?name } LIMIT 10` + * ) + * + * console.log(rows[0].name) // "Toronto" (after coercion) * ``` */ -export function transformResults(response: SparqlResponse): Array> { - return response.results.bindings.map((binding) => { - const row: Record = {} - for (const [key, value] of Object.entries(binding)) { - row[key] = value.value +export function createExecutor(config: ExecutionConfig): Executor { + /** + * Core execution that always returns SPARQL JSON shape. This is the one + * everything else builds upon. + */ + async function execute( + query: string | SparqlValue, + options?: RequestOptions, + ): Promise> { + const json = await executeSparql>(config, query, options) + + // For ASK, some endpoints still embed results in this structure, others + // return { boolean }. We try to normalize here. + + // When the endpoint is properly configured, we should get JSON with + // { head, results } for SELECT and { boolean } for ASK. + if ( + !json || + typeof json !== 'object' || + !('results' in json) || + !('head' in json) + ) { + throw new QueryError({ + kind: 'protocol', + message: 'SPARQL endpoint returned JSON that does not match SPARQL Results format', + query, + responseBody: json, + }) } - return row - }) + + return json + } + + /** + * SELECT / DESCRIBE / CONSTRUCT helper that: + * - runs the query + * - coerces literals into JS types + * - returns an array of rows + */ + async function query( + q: string | SparqlValue, + options?: RequestOptions, + ): Promise> { + const result = await execute(q, options) + return transformResults(result) + } + + /** + * ASK helper – returns the boolean or `false` if the server didn't provide it. + */ + async function ask( + q: string | SparqlValue, + options?: RequestOptions, + ): Promise { + const result = await execute(q, options) + return Boolean(result.boolean) + } + + /** + * SPARQL UPDATE helper – we treat it as fire-and-forget. + * Any HTTP/protocol error will throw via `executeSparql`. + */ + async function update( + q: string | SparqlValue, + options?: RequestOptions, + ): Promise { + // For UPDATE, we just care that it doesn't throw. + await executeSparql(config, q, options) + } + + const execFn: ExecuteFn = execute + + return { + execute, + query, + ask, + update, + resolveLabels: (uris, cfg) => resolveLabels(uris, cfg, execFn), + fetchProperties: (uris, cfg) => fetchProperties(uris, cfg, execFn), + expand: (uris, cfg) => expand(uris, cfg, execFn), + } } +/* ============================================================================ + * Result transformation + coercion + * ========================================================================== */ + /** - * Extract all URIs from query results. - * - * Finds every URI value in the results and returns them as a deduplicated list. - * Useful when you need to do something with all the resources mentioned in - * your results. - * - * @param response SPARQL response data - * @returns Array of unique URIs - * - * @example - * ```ts - * const result = await execute(query, config) - * if (result.success) { - * const uris = extractUris(result.data) - * // ['http://example.org/person/1', 'http://example.org/person/2', ...] - * } - * ``` + * Transform SPARQL JSON into a more ergonomic JS representation. + * + * - Literals are coerced using {@link coerceValue} + * - URIs stay as strings + * - Blank nodes stay as strings (bnode IDs) */ -export function extractUris(response: SparqlResponse): string[] { - const uris = new Set() +export function transformResults( + result: QueryResult, +): Array<{ [K in keyof TBind]: unknown }> { + return result.results.bindings.map((binding) => parseBinding(binding)) +} - for (const binding of response.results.bindings) { - for (const value of Object.values(binding)) { - if (value.type === 'uri') { - uris.add(value.value) - } - } +/** + * Parse a single binding map into a JS row. + * + * Keys are variable names (without `?`) and values are coerced via + * {@link coerceValue}. + */ +export function parseBinding( + binding: TBind, +): { [K in keyof TBind]: unknown } { + const row: Record = {} + + for (const [key, value] of Object.entries(binding)) { + row[key] = coerceValue(value) } - return Array.from(uris) + return row as { [K in keyof TBind]: unknown } } -// ============================================================================ -// Label Resolution -// ============================================================================ +/* ============================================================================ + * Datatype Coercion + * ========================================================================== */ /** - * Configuration for resolving human-readable labels. - * - * Often you have URIs but want to display friendly names. Label resolution - * queries the graph for label properties and returns a map of URIs to labels. + * Expanded set of integer-like XSD datatypes. + * + * All of these are represented as `number` in JS. */ -export interface LabelResolutionConfig { - /** URIs to resolve labels for */ - readonly uris: string[] - /** Label predicates to query (defaults to common label properties) */ - readonly labelPredicates?: string[] - /** Maximum URIs per batch query (default: 50) */ - readonly maxBatchSize?: number -} +const INTEGER_DATATYPES = new Set([ + XSD.integer, + XSD.long, + XSD.int, + XSD.short, + XSD.byte, + XSD.nonNegativeInteger, + XSD.nonPositiveInteger, + XSD.positiveInteger, + XSD.negativeInteger, + XSD.unsignedLong, + XSD.unsignedInt, + XSD.unsignedShort, + XSD.unsignedByte, +]) /** - * Default label predicates in priority order. - * - * When resolving labels, we check these properties in order. This includes - * domain-specific labels followed by common vocabularies. + * Decimal / floating-point XSD datatypes. + * + * All of these are represented as `number` in JS. */ -const DEFAULT_LABEL_PREDICATES = [ - 'http://knowledge.graph/narrative#characterName', - 'http://knowledge.graph/narrative#seriesName', - 'http://knowledge.graph/narrative#productTitle', - 'http://www.w3.org/2000/01/rdf-schema#label', - 'http://schema.org/name', - 'http://xmlns.com/foaf/0.1/name', -] +const DECIMAL_DATATYPES = new Set([ + XSD.decimal, + XSD.float, + XSD.double, +]) /** - * Resolve human-readable labels for URIs. - * - * Queries the graph for label properties on the specified URIs. Returns a map - * where each URI gets an array of labels (there can be multiple if different - * properties have values). - * - * Processes URIs in batches to avoid overwhelming the endpoint with huge queries. - * - * @param config Executor configuration - * @param options Label resolution options - * @returns Map of URI to array of labels - * - * @example - * ```ts - * const uris = extractUris(queryResult.data) - * const labels = await resolveLabels(config, { uris }) - * - * for (const uri of uris) { - * const uriLabels = labels.get(uri) - * console.log(uri, uriLabels?.[0] ?? 'No label') - * } - * ``` + * Datetime-like XSD datatypes that should be parsed as JS `Date`. + * + * We attempt `Date.parse` and fall back to the original string on failure. */ -export async function resolveLabels( - config: ExecutorConfig, - options: LabelResolutionConfig -): Promise> { - const { uris, labelPredicates = DEFAULT_LABEL_PREDICATES, maxBatchSize = 50 } = options - const labelMap = new Map() - - // Process in batches - for (let i = 0; i < uris.length; i += maxBatchSize) { - const batch = uris.slice(i, i + maxBatchSize) - const uriValues = batch.map((uri) => `<${uri}>`).join(' ') - const predicateValues = labelPredicates.map((p) => `<${p}>`).join(' ') - - const query = ` - SELECT ?uri ?label WHERE { - VALUES ?uri { ${uriValues} } - VALUES ?predicate { ${predicateValues} } - ?uri ?predicate ?label . - } - ` +const DATETIME_DATATYPES = new Set([ + XSD.dateTime, + XSD.dateTimeStamp, +]) - const result = await executeSparql(config, query) +/** + * Date-only XSD datatypes. These are still parsed as full JS `Date` + * (midnight UTC or local, depending on environment). + */ +const DATE_DATATYPES = new Set([ + XSD.date, +]) - if (result.success) { - for (const binding of result.data.results.bindings) { - const uri = binding.uri.value - const label = binding.label.value +/** + * Time-only datatypes (stored as `Date` as well, for now). + */ +const TIME_DATATYPES = new Set([ + XSD.time, +]) - if (!labelMap.has(uri)) { - labelMap.set(uri, []) - } - labelMap.get(uri)!.push(label) +/** + * Coerce a SPARQL JSON binding value into a more natural JS type. + * + * - URIs → string + * - `xsd:boolean` → boolean + * - integer-like → number + * - decimal / float / double → number + * - date / dateTime / dateTimeStamp / time → Date + * - `rdf:JSON` → parsed JSON (with safe fallback) + * - everything else → string (original literal value) + * + * We deliberately stay conservative for exotic datatypes: if we don't have + * a clear, widely-understood JS representation, we keep the raw string. + */ +export function coerceValue(bindingValue: BindingValue): unknown { + const { type, value, datatype } = bindingValue + + // URIs are already natural strings. + if (type === 'uri') { + return value + } + + // Typed literals: try to interpret known datatypes. + if (datatype) { + // Integers + if (datatype === XSD.boolean) { + return value === 'true' || value === '1' + } + + // Integers + if (INTEGER_DATATYPES.has(datatype)) { + const n = Number.parseInt(value, 10) + return Number.isNaN(n) ? value : n + } + + // Decimals / floats / doubles + if (DECIMAL_DATATYPES.has(datatype)) { + const n = Number.parseFloat(value) + return Number.isNaN(n) ? value : n + } + + // Dates / times + if ( + DATETIME_DATATYPES.has(datatype) || + DATE_DATATYPES.has(datatype) || + TIME_DATATYPES.has(datatype) + ) { + const ts = Date.parse(value) + return Number.isNaN(ts) ? value : new Date(ts) + } + + // RDF JSON literal – use the constant if present in your RDF namespace. + if ((RDF as Record).JSON && datatype === (RDF as Record).JSON) { + try { + return JSON.parse(value) + } catch { + return value } } + + // AnyURI is already a string representation; keep as-is. + if (datatype === XSD.anyURI) { + return value + } + + // For everything else (HTML, XMLLiteral, gYear, durations, etc.) + // we fall through and return the raw string. + } - return labelMap + // Untyped literals or unsupported datatypes: keep as string. + return value } +/* ============================================================================ + * Convenience helpers + * ========================================================================== */ + /** - * Get the first label for a URI, with fallback. - * - * Returns the first label if available, otherwise extracts a reasonable name - * from the URI itself (fragment or last path segment). - * - * @param labels Label map from resolveLabels - * @param uri URI to get label for - * @returns First label or URI fragment - * - * @example + * Pluck a single column from result rows. + * + * @example Get list of names * ```ts - * const labels = await resolveLabels(config, { uris }) - * - * for (const uri of uris) { - * const label = getFirstLabel(labels, uri) ?? uri - * console.log(label) - * } + * const rows = await executor.query<{ name: BindingValue }>(...) + * const names = pluck(rows, 'name') // string[] * ``` */ -export function getFirstLabel(labels: Map, uri: string): string | undefined { - const uriLabels = labels.get(uri) - if (uriLabels && uriLabels.length > 0) { - return uriLabels[0] - } +export function pluck, K extends keyof T>( + rows: T[], + key: K, +): Array { + return rows.map((row) => row[key]) +} - // Fallback to URI fragment or last path segment - const fragment = uri.split('#').pop() || uri.split('/').pop() - return fragment && fragment !== uri ? fragment : undefined +/** + * Get the first row (or `undefined`). + * + * @example + * ```ts + * const rows = await executor.query(...) + * const firstRow = first(rows) + * ``` + */ +export function first(rows: T[]): T | undefined { + return rows[0] } -// ============================================================================ -// Property Fetching -// ============================================================================ +/* ============================================================================ + * Label resolution + * ========================================================================== */ /** - * Configuration for fetching all properties of resources. + * Default label predicates when resolving human-friendly names. + * + * Order matters – we stop at the first non-empty value: + * 1. `rdfs:label` + * 2. `foaf:name` + * 3. `schema:name` + * 4. `schema:alternateName` */ -export interface PropertyFetchConfig { - /** URIs to fetch properties for */ - readonly uris: string[] - /** Maximum URIs per batch query (default: 50) */ - readonly maxBatchSize?: number +const DEFAULT_LABEL_PREDICATES: readonly string[] = [ + RDFS.label, + FOAF.name, + SCHEMA.name, + SCHEMA.alternateName, +] + +/** + * Configuration for {@link resolveLabels}. + */ +export interface LabelResolutionConfig { + /** + * Predicates to try (in order of preference). + * + * Defaults to {@link DEFAULT_LABEL_PREDICATES}. + */ + labelPredicates?: string[] + + /** + * Batch size for VALUES clause when resolving many URIs. + * + * Defaults to 50. + */ + batchSize?: number } /** - * Fetch all properties for specified URIs. - * - * Queries the graph for all triples where these URIs are the subject. Returns - * a nested map structure: URI → predicate → array of values. - * - * This is useful when you need to inspect resources in detail or build entity - * detail views. - * - * @param config Executor configuration - * @param options Property fetch options - * @returns Map of URI to map of predicate to array of values - * - * @example - * ```ts - * const uris = ['http://example.org/person/1'] - * const properties = await fetchProperties(config, { uris }) - * - * const person = properties.get(uris[0]) - * const names = person?.get('http://xmlns.com/foaf/0.1/name') - * console.log(names?.[0]) // First name value - * ``` + * Resolve best-effort labels for a list of URIs. + * + * This is intentionally: + * - **best-effort** (if no label exists, URI won't be in the result) + * - **batch-friendly** (uses VALUES to resolve many URIs at once) + * + * Uses the `sparql\`...\`` tag internally for safer query construction. */ -export async function fetchProperties( - config: ExecutorConfig, - options: PropertyFetchConfig -): Promise>> { - const { uris, maxBatchSize = 50 } = options - const propertyMap = new Map>() - - // Process in batches - for (let i = 0; i < uris.length; i += maxBatchSize) { - const batch = uris.slice(i, i + maxBatchSize) - const uriValues = batch.map((uri) => `<${uri}>`).join(' ') - - const query = ` - SELECT ?uri ?predicate ?value WHERE { - VALUES ?uri { ${uriValues} } - ?uri ?predicate ?value . +export async function resolveLabels( + uris: string[], + config: LabelResolutionConfig | undefined, + execute: ExecuteFn, +): Promise> { + if (uris.length === 0) return {} + + const labelPredicates = + config?.labelPredicates && config.labelPredicates.length > 0 + ? config.labelPredicates + : [...DEFAULT_LABEL_PREDICATES] + + const batchSize = config?.batchSize ?? 50 + const result: Record = {} + + const labelVarNames = labelPredicates.map((_, index) => `label${index}`) + const labelVarList = labelVarNames.map((v) => `?${v}`).join(' ') + + for (let i = 0; i < uris.length; i += batchSize) { + const batch = uris.slice(i, i + batchSize) + + const optionalPatterns = labelPredicates + .map((predicate, index) => { + const varName = labelVarNames[index] + return `OPTIONAL { ?uri <${predicate}> ?${varName} }` + }) + .join('\n') + + const query = sparql` + SELECT ?uri ${raw(labelVarList)} WHERE { + VALUES ?uri { ${batch.map((u) => uri(u))} } + ${raw(optionalPatterns)} } ` - const result = await executeSparql(config, query) + const response = await execute(query) - if (result.success) { - for (const binding of result.data.results.bindings) { - const uri = binding.uri.value - const predicate = binding.predicate.value - const value = binding.value.value + for (const binding of response.results.bindings) { + const uriValue = binding.uri?.value + if (!uriValue) continue - if (!propertyMap.has(uri)) { - propertyMap.set(uri, new Map()) - } + const label = + labelVarNames + .map((v) => binding[v]?.value) + .find((v) => v !== undefined && v !== '') - const uriProps = propertyMap.get(uri)! - if (!uriProps.has(predicate)) { - uriProps.set(predicate, []) - } - - uriProps.get(predicate)!.push(value) + if (label) { + result[uriValue] = label } } } - return propertyMap + return result } -// ============================================================================ -// Enhanced Result Parsing -// ============================================================================ +/* ============================================================================ + * Property fetch + * ========================================================================== */ -/** - * Parsed SPARQL binding with type information preserved. - * - * **Common use case:** When you need to know not just the value, but also what - * *kind* of value it is - whether it's a URI, a typed literal, or has a language tag. - * - * **How it works:** SPARQL results include metadata about each value. This interface - * structures that metadata in an easy-to-use format while preserving all the - * type information the endpoint provided. - */ -export interface ParsedValue { - /** The actual value as a string */ - readonly raw: string - /** What kind of RDF term this is */ - readonly type: 'uri' | 'literal' | 'bnode' - /** Datatype IRI for typed literals (e.g., xsd:integer) */ - readonly datatype?: string - /** Language tag for language-tagged strings (e.g., "en", "fr") */ - readonly language?: string -} /** - * Parse a SPARQL binding while preserving all type metadata. - * - * **Common use case:** When you need to inspect the type information of a result, - * such as checking if a value is a URI vs a literal, or what datatype it has. - * - * **How it works:** Converts the raw SPARQL JSON binding format into a cleaner - * TypeScript interface. All the information is preserved, just in a more - * convenient structure. - * - * @param binding - Raw SPARQL binding from query results - * @returns Parsed value with type information - * - * @example Inspect value types - * ```ts - * const result = await query.execute(config) - * if (result.success) { - * for (const row of result.data.results.bindings) { - * const parsed = parseBinding(row.value) - * - * if (parsed.type === 'uri') { - * console.log('IRI:', parsed.raw) - * } else if (parsed.datatype === 'http://www.w3.org/2001/XMLSchema#integer') { - * console.log('Integer:', parsed.raw) - * } else if (parsed.language) { - * console.log(`Text in ${parsed.language}:`, parsed.raw) - * } - * } - * } - * ``` + * Configuration for {@link fetchProperties}. */ -export function parseBinding(binding: SparqlBinding): ParsedValue { - return { - raw: binding.value, - type: binding.type as 'uri' | 'literal' | 'bnode', - datatype: binding.datatype, - language: binding['xml:lang'], - } +export interface PropertyFetchConfig { + /** + * List of properties to fetch. + * + * `property` can be: + * - full IRI (`http://example.org/name`) + * - prefixed name (`schema:name`) + * - variable (`?property`) – in which case you control the pattern in your query + * + * `propertyName` controls the key used in the returned object. Defaults to + * the property string itself. + */ + properties: Array<{ + property: string + propertyName?: string + }> + + /** + * Batch size for VALUES clause when fetching many URIs. + * + * Defaults to 50. + */ + batchSize?: number } /** - * Convert SPARQL typed literals to native JavaScript types. - * - * **Common use case:** Working with numeric data, dates, or booleans where you want - * actual JavaScript types instead of strings. Makes it easier to do calculations, - * comparisons, and date manipulation. - * - * **How it works:** Reads the XSD datatype from the binding and converts the string - * value to the corresponding JavaScript type. Falls back to returning the string if - * the datatype isn't recognized. - * - * **Important:** This only works for bindings with XSD datatypes. Language-tagged - * strings and plain literals return as-is. URIs are never coerced. - * - * **Performance note:** Type conversion happens for every value. For large result - * sets where you don't need type conversion, use `transformResults()` instead. - * - * @param binding - SPARQL binding to convert - * @returns Native JavaScript value (number, boolean, Date, or string) - * - * @example Working with numeric data - * ```ts - * const result = await select(['?age', '?price']) - * .where(triple('?person', 'foaf:age', '?age')) - * .where(triple('?person', 'schema:price', '?price')) - * .execute(config) - * - * if (result.success) { - * for (const row of result.data.results.bindings) { - * const age = coerceValue(row.age) // number - * const price = coerceValue(row.price) // number - * - * if (typeof age === 'number') { - * console.log('Person is', age, 'years old') - * } - * } - * } - * ``` - * - * @example Date handling + * Fetch selected properties for each URI. + * + * Returns a nested map: + * * ```ts - * // Query returns xsd:dateTime literals - * const binding = row.timestamp - * const date = coerceValue(binding) // Date object - * - * if (date instanceof Date) { - * console.log('Event happened:', date.toLocaleDateString()) + * { + * "http://example.org/resource/1": { + * name: "Example", + * createdAt: Date, + * }, + * ... * } * ``` - * - * @example Supported type conversions - * ```ts - * // xsd:integer, xsd:int, xsd:long → number (parsed as integer) - * // xsd:decimal, xsd:float, xsd:double → number (parsed as float) - * // xsd:boolean → boolean (true/false) - * // xsd:date, xsd:dateTime → Date object - * // Anything else → string (unchanged) - * ``` */ -export function coerceValue(binding: SparqlBinding): string | number | boolean | Date { - const { value, datatype } = binding - - if (!datatype) return value - - // Integer types - if ( - datatype === 'http://www.w3.org/2001/XMLSchema#integer' || - datatype === 'http://www.w3.org/2001/XMLSchema#int' || - datatype === 'http://www.w3.org/2001/XMLSchema#long' || - datatype === 'http://www.w3.org/2001/XMLSchema#short' || - datatype === 'http://www.w3.org/2001/XMLSchema#byte' - ) { - return parseInt(value, 10) - } +export async function fetchProperties( + uris: string[], + config: PropertyFetchConfig, + execute: ExecuteFn, +): Promise>> { + if (uris.length === 0) return {} + const requestedProperties = config.properties ?? [] + if (requestedProperties.length === 0) return {} + + const batchSize = config.batchSize ?? 50 + const result: Record> = {} + + const propertyVarNames = requestedProperties.map((_, index) => `p${index}`) + const propertyVarList = propertyVarNames.map((v) => `?${v}`).join(' ') + + for (let i = 0; i < uris.length; i += batchSize) { + const batch = uris.slice(i, i + batchSize) + + const optionalPatterns = requestedProperties + .map((prop, index) => { + let propertyExpr = prop.property + + if (!propertyExpr.startsWith('?')) { + if (!propertyExpr.includes(':') && !propertyExpr.startsWith('<')) { + propertyExpr = `<${propertyExpr}>` + } + } - // Decimal/float types - if ( - datatype === 'http://www.w3.org/2001/XMLSchema#decimal' || - datatype === 'http://www.w3.org/2001/XMLSchema#float' || - datatype === 'http://www.w3.org/2001/XMLSchema#double' - ) { - return parseFloat(value) - } + const varName = propertyVarNames[index] + return `OPTIONAL { ?uri ${propertyExpr} ?${varName} }` + }) + .join('\n') - // Boolean - if (datatype === 'http://www.w3.org/2001/XMLSchema#boolean') { - return value === 'true' || value === '1' - } + const query = sparql` + SELECT ?uri ${raw(propertyVarList)} WHERE { + VALUES ?uri { ${batch.map((u) => uri(u))} } + ${raw(optionalPatterns)} + } + ` + + const response = await execute(query) - // Date/time types - if ( - datatype === 'http://www.w3.org/2001/XMLSchema#date' || - datatype === 'http://www.w3.org/2001/XMLSchema#dateTime' || - datatype === 'http://www.w3.org/2001/XMLSchema#time' - ) { - return new Date(value) + for (const binding of response.results.bindings) { + const uriValue = binding.uri?.value + if (!uriValue) continue + + const entry = (result[uriValue] ??= {}) + + requestedProperties.forEach((prop, index) => { + const varName = propertyVarNames[index] + const bindingValue = binding[varName] + if (!bindingValue) return + + const key = prop.propertyName ?? prop.property + entry[key] = coerceValue(bindingValue) + }) + } } - // Unknown datatype - return as string - return value + return result } +/* ============================================================================ + * Expand (labels + properties) + * ========================================================================== */ + /** - * Transform SPARQL results with automatic type coercion. - * - * **Common use case:** When you want to work with properly typed data instead of - * everything being strings. Particularly useful for numeric calculations, date - * comparisons, or boolean logic. - * - * **How it works:** Like `transformResults()`, but runs `coerceValue()` on every - * binding to convert typed literals to JavaScript types. Numbers become numbers, - * booleans become booleans, dates become Date objects. - * - * **Performance tradeoff:** Slightly slower than `transformResults()` due to type - * checking and conversion. For very large result sets, only use this if you actually - * need the type conversion. - * - * @param response - SPARQL response data - * @returns Array of objects with native JavaScript types - * - * @example Numeric calculations - * ```ts - * const result = await select(['?product', '?price', '?quantity']) - * .where(triple('?product', 'schema:price', '?price')) - * .where(triple('?product', 'schema:quantity', '?quantity')) - * .execute(config) - * - * if (result.success) { - * const rows = transformResultsTyped(result.data) - * - * for (const row of rows) { - * // price and quantity are numbers, not strings - * const total = row.price * row.quantity - * console.log(`Total value: $${total.toFixed(2)}`) - * } - * } - * ``` - * - * @example Date filtering - * ```ts - * const rows = transformResultsTyped(result.data) - * const recentEvents = rows.filter(row => { - * // timestamp is a Date object - * return row.timestamp instanceof Date && - * row.timestamp > new Date('2024-01-01') - * }) - * ``` - * - * @example Type checking - * ```ts - * const rows = transformResultsTyped(result.data) - * for (const row of rows) { - * if (typeof row.age === 'number') { - * console.log('Age:', row.age) - * } - * if (typeof row.active === 'boolean') { - * console.log('Active:', row.active) - * } - * if (row.created instanceof Date) { - * console.log('Created:', row.created.toISOString()) - * } - * } - * ``` + * Result of {@link expand}. + * + * Each expanded item includes: + * - `uri` – the original resource URI + * - `label` – resolved human label (if any) + * - `properties` – fetched properties keyed by name */ -export function transformResultsTyped( - response: SparqlResponse -): Array> { - return response.results.bindings.map((binding) => { - const row: Record = {} - for (const [key, value] of Object.entries(binding)) { - row[key] = coerceValue(value) - } - return row - }) +export interface ExpandResult { + uri: string + label?: string + properties: Record } /** - * Extract values for a specific variable from query results. - * - * **Common use case:** When you only care about one column from your results. - * Perfect for building lists, checking for existence, or collecting IDs. - * - * **How it works:** Walks through all result rows, extracts the specified variable, - * and returns just those values as an array. Optionally applies type coercion. - * - * **Filtering behavior:** Rows where the variable is unbound (undefined) are - * automatically filtered out. This is useful when using OPTIONAL patterns. - * - * @param response - SPARQL response data - * @param variable - Variable name to extract (without the ? prefix) - * @param coerce - Whether to apply type coercion (default: false) - * @returns Array of values for that variable - * - * @example Get list of names - * ```ts - * const result = await select(['?name', '?age']) - * .where(triple('?person', 'foaf:name', '?name')) - * .where(triple('?person', 'foaf:age', '?age')) - * .execute(config) - * - * if (result.success) { - * const names = pluck(result.data, 'name') - * // ['Alice', 'Bob', 'Charlie'] - * - * console.log('Found', names.length, 'people') - * names.forEach(name => console.log(name)) - * } - * ``` - * - * @example With type coercion - * ```ts - * const ages = pluck(result.data, 'age', true) - * // [25, 30, 42] as numbers, not strings - * - * const averageAge = ages.reduce((a, b) => a + b, 0) / ages.length - * console.log('Average age:', averageAge) - * ``` - * - * @example Collect URIs for further processing - * ```ts - * const productURIs = pluck(result.data, 'product') - * const labels = await resolveLabels(config, { uris: productURIs }) - * ``` - * - * @example With OPTIONAL patterns (undefined filtering) - * ```ts - * // Some people have emails, some don't - * select(['?name', '?email']) - * .where(triple('?person', 'foaf:name', '?name')) - * .optional(triple('?person', 'foaf:mbox', '?email')) - * - * const emails = pluck(result.data, 'email') - * // Only includes rows where email was bound - * ``` + * How to handle the base URI list when some URIs have no data. */ -export function pluck( - response: SparqlResponse, - variable: string, - coerce = false -): T[] { - return response.results.bindings - .filter((b) => variable in b) - .map((b) => (coerce ? coerceValue(b[variable]) : b[variable].value) as T) -} +export type ExpandMode = + | 'all' // include all URIs passed in + | 'withData' // only URIs that have label or properties /** - * Get the first result row, or undefined if no results. - * - * **Common use case:** Queries where you expect exactly one result (or zero) and - * don't want to write array access logic. Perfect for lookups, existence checks, - * or queries with LIMIT 1. - * - * **How it works:** Returns the first row transformed to a simple object, or - * undefined if the result set is empty. No type coercion is applied - use - * `transformResultsTyped()` first if you need that. - * - * **Safety note:** Returns `undefined` rather than throwing on empty results, - * so you can safely use optional chaining or nullish coalescing. - * - * @param response - SPARQL response data - * @returns First result row or undefined - * - * @example Lookup by unique identifier - * ```ts - * const result = await select(['?name', '?email']) - * .where(triple('?person', 'foaf:accountName', 'alice')) - * .where(triple('?person', 'foaf:name', '?name')) - * .where(triple('?person', 'foaf:mbox', '?email')) - * .limit(1) - * .execute(config) - * - * if (result.success) { - * const person = first(result.data) - * - * if (person) { - * console.log('Found:', person.name) - * console.log('Email:', person.email) - * } else { - * console.log('No person found with that username') - * } - * } - * ``` - * - * @example With optional chaining - * ```ts - * const person = first(result.data) - * const email = person?.email ?? 'No email' - * console.log(email) - * ``` - * - * @example Existence check - * ```ts - * const exists = first(result.data) !== undefined - * if (exists) { - * console.log('Record found') - * } - * ``` - * - * @example Safe destructuring - * ```ts - * const row = first(result.data) - * if (!row) { - * console.error('Query returned no results') - * return - * } - * - * // TypeScript knows row is defined here - * const { name, age } = row - * console.log(name, age) - * ``` + * Configuration for {@link expand}. */ -export function first(response: SparqlResponse): Record | undefined { - const bindings = response.results.bindings - return bindings.length > 0 ? transformResults(response)[0] : undefined +export interface ExpandConfig { + labels?: LabelResolutionConfig + properties?: PropertyFetchConfig + /** + * Whether to return entries for URIs that have no label and no properties. + * + * - `'all'` (default) – keep them with empty properties and no label + * - `'withData'` – filter out completely + */ + mode?: ExpandMode } /** - * Check if an ASK query returned true. - * - * **Common use case:** ASK queries return a different response format than SELECT - * queries. This helper makes it easy to extract the boolean result. - * - * **How it works:** ASK query responses have a `boolean` field instead of result - * bindings. This function safely extracts that boolean, defaulting to false if - * the format is unexpected. - * - * **Type safety note:** The response parameter is `unknown` because ASK and SELECT - * have different response formats. This function handles the type check internally. - * - * @param response - Raw response from ASK query - * @returns True if pattern exists, false otherwise - * - * @example Basic existence check + * Expand URIs into richer objects with: + * + * - a human-friendly `label` (using {@link resolveLabels}) + * - selected `properties` (using {@link fetchProperties}) + * + * @example Basic expansion * ```ts - * const result = await ask() - * .where(triple('?person', 'foaf:name', 'Alice')) - * .execute(config) - * - * if (result.success) { - * const exists = askResult(result.data) - * console.log('Alice exists:', exists) - * } - * ``` - * - * @example Conditional logic - * ```ts - * const hasAdults = askResult( - * await ask() - * .where(triple('?person', 'foaf:age', '?age')) - * .filter(v('age').gte(18)) - * .execute(config) - * .then(r => r.success ? r.data : { boolean: false }) + * const expanded = await executor.expand( + * [ + * 'http://knowledge.graph/narrative#Product/123', + * 'http://knowledge.graph/narrative#Product/456', + * ], + * { + * properties: { + * properties: [ + * { property: 'schema:releaseDate', propertyName: 'releaseDate' }, + * { property: 'schema:isbn', propertyName: 'isbn' }, + * ], + * }, + * }, * ) - * - * if (hasAdults) { - * console.log('Dataset contains adults') - * } - * ``` - * - * @example Validation check - * ```ts - * async function validatePerson(id: string): Promise { - * const result = await ask() - * .where(triple(`<${id}>`, RDF.type, uri(FOAF.Person))) - * .execute(config) - * - * return result.success && askResult(result.data) - * } - * - * const isValid = await validatePerson('http://example.org/person/123') + * + * // → [{ uri, label, properties: { releaseDate: Date, isbn: string } }, ...] * ``` */ -export function askResult(response: unknown): boolean { - return (response as { boolean?: boolean }).boolean ?? false -} \ No newline at end of file +export async function expand( + uris: string[], + config: ExpandConfig | undefined, + execute: ExecuteFn, +): Promise { + if (uris.length === 0) return [] + + const mode: ExpandMode = config?.mode ?? 'all' + + const [labels, props] = await Promise.all([ + resolveLabels(uris, config?.labels, execute), + config?.properties + ? fetchProperties(uris, config.properties, execute) + : Promise.resolve>>({}), + ]) + + const results: ExpandResult[] = [] + + for (const uriValue of uris) { + const label = labels[uriValue] + const properties = props[uriValue] ?? {} + + const hasData = + typeof label === 'string' || Object.keys(properties).length > 0 + + if (mode === 'withData' && !hasData) { + continue + } + + results.push({ + uri: uriValue, + label, + properties, + }) + } + + return results +} diff --git a/namespaces.ts b/namespaces.ts index 49ffc6f..d6473f1 100644 --- a/namespaces.ts +++ b/namespaces.ts @@ -1,90 +1,138 @@ /** - * Semantic Web / RDF namespace constants. + * Core RDF / SPARQL namespace constants with intent, datatypes, and usage examples. * - * This module exposes curated, strongly-typed constants for the most common RDF vocabularies: - * - {@link XSD} – XML Schema datatypes used for literals - * - {@link RDF} – Core RDF vocabulary - * - {@link RDFS} – RDF Schema (classes/properties, labels, comments) - * - {@link OWL} – Web Ontology Language (reasoning/ontology concepts) - * - {@link FOAF} – Friend of a Friend (people, agents, social graphs) - * - {@link SCHEMA} – Schema.org (web, products, content, places, events) + * Design goals: + * - Give you **strongly-typed, auto-complete friendly IRIs** for the vocabularies + * most relevant to SPARQL 1.1 / 1.2 and RDF 1.1 / RDF-star. + * - Focus on **datatypes** and high-value terms from: + * - XSD (XML Schema) + * - RDF / RDFS + * - OWL + * - FOAF + * - Schema.org + * - SPARQL Service Description (SD) + * - SHACL + * - SKOS + * - PROV, VoID, Dublin Core Terms + * - WGS84 (`geo:`) and GeoSPARQL (`geosparql:`, `geof:`) + * - SPARQL Results vocabulary * - * Each namespace object: - * - Has a `_namespace` field with the base IRI (for PREFIX declarations). - * - Exposes common classes and properties as string constants (full IRIs). + * All namespaces use the `http://` form of the IRI, which is still the most widely + * deployed and interoperable in RDF/SPARQL systems. * - * These constants are just strings – there is no runtime cost. They exist purely to give - * you autocomplete and avoid subtle typos in hand-written IRIs. - * - * @example Basic usage with a query builder - * ```ts - * import { RDF, FOAF, SCHEMA } from './namespaces.ts' - * - * const query = select(['?person', '?name', '?email']) - * .prefix('foaf', getNamespaceIRI(FOAF)) - * .prefix('schema', getNamespaceIRI(SCHEMA)) - * .where(triple('?person', RDF.type, uri(FOAF.Person))) - * .where(triple('?person', FOAF.name, '?name')) - * .optional(triple('?person', SCHEMA.email, '?email')) - * ``` - * - * @example Typed literals with XSD - * ```ts - * import { XSD } from './namespaces.ts' - * - * // Filter to adults: age > 18 (using xsd:integer semantics) - * query.filter(v('age').gt(typed('18', XSD.integer))) - * - * // Filter by date (using xsd:dateTime semantics) - * query.filter(v('createdAt').gt(typed('2024-01-01T00:00:00Z', XSD.dateTime))) - * ``` + * These are **just string constants** – zero runtime overhead. They help you avoid + * subtle typos and keep your query builder readable. */ /** - * Common shape shared by all namespace objects in this module. + * Common structural shape shared by all namespace objects. * - * You can use this to accept any of the known vocabularies generically. + * You rarely need this directly; it’s mainly here so helpers like `getNamespaceIRI` + * can accept any of the exported namespaces. */ export interface NamespaceLike { - /** Base namespace IRI, typically used in PREFIX declarations. */ + /** Base namespace IRI (typically used for PREFIX declarations). */ readonly _namespace: string; - /** - * All other keys are full IRIs for terms in this vocabulary. - * The concrete namespaces declare their own known properties explicitly; - * this index signature is mostly here for ergonomic generic helpers. - */ + /** Every other property is a full IRI for a class, datatype, or property. */ readonly [term: string]: string; } +/* ======================================================================= */ +/* XSD – XML Schema Datatypes */ +/* ======================================================================= */ + /** - * XML Schema Datatypes (XSD) namespace. + * XML Schema Datatypes (XSD) – the backbone of SPARQL literal typing. * - * These IRIs are used to type literals in RDF and SPARQL: - * `"42"^^xsd:integer`, `"2024-01-01"^^xsd:date`, etc. + * **Intent** + * XSD defines scalar types (numbers, dates, strings, URIs, etc.) used to type RDF + * literals like `"42"^^xsd:int` or `"2024-01-01"^^xsd:date`. SPARQL has explicit + * comparison and casting rules for these types. * - * SPARQL 1.1 has special comparison rules for many of these types - * (numeric promotion, date/time ordering, boolean logic, etc.). + * **Typical uses** + * - Creating typed literals in your DSL (e.g. `typed('18', XSD.int)`). + * - Writing filters that rely on numeric or date semantics. + * - Defining SHACL constraints or schema ranges for properties. + * + * @example Numeric filter + * ```ts + * import { XSD } from './namespaces.ts' * - * @see https://www.w3.org/TR/xmlschema11-2/ + * // ?age > 18 using xsd:int semantics + * query.filter(v('age').gt(typed('18', XSD.int))) + * ``` + * + * @example Date comparison + * ```ts + * query.filter( + * v('createdAt').gt(typed('2024-01-01T00:00:00Z', XSD.dateTime)) + * ) + * ``` */ export const XSD = { - /** Base namespace for all XSD types. */ + /** Base namespace for all XML Schema datatypes. */ _namespace: 'http://www.w3.org/2001/XMLSchema#', - // Core scalar types (very common in SPARQL) + // Core string & language /** Free-form Unicode string (default for plain literals). */ string: 'http://www.w3.org/2001/XMLSchema#string', - /** Boolean value: `"true"` / `"false"` / `"1"` / `"0"`. */ - boolean: 'http://www.w3.org/2001/XMLSchema#boolean', + /** Whitespace-normalized string. */ + normalizedString: 'http://www.w3.org/2001/XMLSchema#normalizedString', + + /** Tokenized string (no leading/trailing/extra internal spaces). */ + token: 'http://www.w3.org/2001/XMLSchema#token', + + /** Language tag (e.g. "en", "en-CA"). */ + language: 'http://www.w3.org/2001/XMLSchema#language', + + // Numeric hierarchy + + /** Arbitrary-precision decimal (great for currency/precise amounts). */ + decimal: 'http://www.w3.org/2001/XMLSchema#decimal', /** Arbitrary-precision integer. */ integer: 'http://www.w3.org/2001/XMLSchema#integer', - /** Arbitrary-precision decimal (often used for currency). */ - decimal: 'http://www.w3.org/2001/XMLSchema#decimal', + /** Integer ≤ 0. */ + nonPositiveInteger: 'http://www.w3.org/2001/XMLSchema#nonPositiveInteger', + + /** Integer < 0. */ + negativeInteger: 'http://www.w3.org/2001/XMLSchema#negativeInteger', + + /** Integer ≥ 0. */ + nonNegativeInteger: 'http://www.w3.org/2001/XMLSchema#nonNegativeInteger', + + /** Integer > 0. */ + positiveInteger: 'http://www.w3.org/2001/XMLSchema#positiveInteger', + + /** 64-bit signed integer. */ + long: 'http://www.w3.org/2001/XMLSchema#long', + + /** 32-bit signed integer. */ + int: 'http://www.w3.org/2001/XMLSchema#int', + + /** 16-bit signed integer. */ + short: 'http://www.w3.org/2001/XMLSchema#short', + + /** 8-bit signed integer. */ + byte: 'http://www.w3.org/2001/XMLSchema#byte', + + /** Unsigned 64-bit integer. */ + unsignedLong: 'http://www.w3.org/2001/XMLSchema#unsignedLong', + + /** Unsigned 32-bit integer. */ + unsignedInt: 'http://www.w3.org/2001/XMLSchema#unsignedInt', + + /** Unsigned 16-bit integer. */ + unsignedShort: 'http://www.w3.org/2001/XMLSchema#unsignedShort', + + /** Unsigned 8-bit integer. */ + unsignedByte: 'http://www.w3.org/2001/XMLSchema#unsignedByte', + + // Floating-point /** 32-bit IEEE 754 floating point. */ float: 'http://www.w3.org/2001/XMLSchema#float', @@ -92,20 +140,25 @@ export const XSD = { /** 64-bit IEEE 754 floating point. */ double: 'http://www.w3.org/2001/XMLSchema#double', - // Date / time types + // Boolean + + /** Boolean value: "true"/"false"/"1"/"0". */ + boolean: 'http://www.w3.org/2001/XMLSchema#boolean', + + // Date / time /** Calendar date without time (YYYY-MM-DD). */ date: 'http://www.w3.org/2001/XMLSchema#date', - /** Time without date (hh:mm:ss[.sss][timezone]). */ + /** Time without date (hh:mm:ss[.fraction][timezone]). */ time: 'http://www.w3.org/2001/XMLSchema#time', - /** Date and time (YYYY-MM-DDThh:mm:ss[.sss][timezone]). */ + /** Date and time (YYYY-MM-DDThh:mm:ss[.fraction][timezone]). */ dateTime: 'http://www.w3.org/2001/XMLSchema#dateTime', /** * Date and time with required timezone. - * Used in some RDF vocabularies for more precise timestamps. + * Useful when you need fully-qualified timestamps. */ dateTimeStamp: 'http://www.w3.org/2001/XMLSchema#dateTimeStamp', @@ -118,87 +171,84 @@ export const XSD = { /** Day and time duration (PnDTnHnMnS). */ dayTimeDuration: 'http://www.w3.org/2001/XMLSchema#dayTimeDuration', - // Integer subtypes (numeric constraints) + // Calendar fragments (useful in some vocabularies) - /** 32-bit signed integer (-2^31 to 2^31-1). */ - int: 'http://www.w3.org/2001/XMLSchema#int', + /** Gregorian year (YYYY). */ + gYear: 'http://www.w3.org/2001/XMLSchema#gYear', - /** 64-bit signed integer. */ - long: 'http://www.w3.org/2001/XMLSchema#long', - - /** 16-bit signed integer. */ - short: 'http://www.w3.org/2001/XMLSchema#short', + /** Gregorian year-month (YYYY-MM). */ + gYearMonth: 'http://www.w3.org/2001/XMLSchema#gYearMonth', - /** 8-bit signed integer. */ - byte: 'http://www.w3.org/2001/XMLSchema#byte', + /** Gregorian month (--MM). */ + gMonth: 'http://www.w3.org/2001/XMLSchema#gMonth', - /** Integer ≤ 0. */ - nonPositiveInteger: 'http://www.w3.org/2001/XMLSchema#nonPositiveInteger', + /** Gregorian month-day (--MM-DD). */ + gMonthDay: 'http://www.w3.org/2001/XMLSchema#gMonthDay', - /** Integer < 0. */ - negativeInteger: 'http://www.w3.org/2001/XMLSchema#negativeInteger', + /** Gregorian day of month (---DD). */ + gDay: 'http://www.w3.org/2001/XMLSchema#gDay', - /** Integer ≥ 0. */ - nonNegativeInteger: 'http://www.w3.org/2001/XMLSchema#nonNegativeInteger', + // Binary & misc - /** Integer > 0. */ - positiveInteger: 'http://www.w3.org/2001/XMLSchema#positiveInteger', - - /** Unsigned 64-bit integer. */ - unsignedLong: 'http://www.w3.org/2001/XMLSchema#unsignedLong', - - /** Unsigned 32-bit integer. */ - unsignedInt: 'http://www.w3.org/2001/XMLSchema#unsignedInt', - - /** Unsigned 16-bit integer. */ - unsignedShort: 'http://www.w3.org/2001/XMLSchema#unsignedShort', - - /** Unsigned 8-bit integer. */ - unsignedByte: 'http://www.w3.org/2001/XMLSchema#unsignedByte', - - // Other commonly used types in RDF land + /** Any type – root of the type hierarchy. */ + anyType: 'http://www.w3.org/2001/XMLSchema#anyType', /** URI/IRI represented as a string literal. */ anyURI: 'http://www.w3.org/2001/XMLSchema#anyURI', - /** RFC 3066 / BCP 47 language tags ("en", "en-GB", ...). */ - language: 'http://www.w3.org/2001/XMLSchema#language', - - /** Whitespace-normalized string. */ - normalizedString: 'http://www.w3.org/2001/XMLSchema#normalizedString', - - /** Tokenized string (no leading/trailing/extra internal spaces). */ - token: 'http://www.w3.org/2001/XMLSchema#token', + /** Base64-encoded binary data. */ + base64Binary: 'http://www.w3.org/2001/XMLSchema#base64Binary', - /** Hexadecimal binary data. */ + /** Hex-encoded binary data. */ hexBinary: 'http://www.w3.org/2001/XMLSchema#hexBinary', - /** Base64-encoded binary data. */ - base64Binary: 'http://www.w3.org/2001/XMLSchema#base64Binary', + /** Qualified name (prefix:local form). */ + QName: 'http://www.w3.org/2001/XMLSchema#QName', + + /** NOTATION type (legacy XML feature, rarely used in RDF). */ + NOTATION: 'http://www.w3.org/2001/XMLSchema#NOTATION', } as const satisfies NamespaceLike; +/* ======================================================================= */ +/* RDF – Core RDF vocabulary */ +/* ======================================================================= */ + /** - * RDF namespace – core building blocks of RDF graphs. + * RDF – Core RDF vocabulary. * - * RDF gives you the minimal vocabulary for describing triples, statements, - * lists, and some special literal types. SPARQL and most RDF tools assume - * these IRIs. + * **Intent** + * RDF defines the basic building blocks of RDF graphs: statements, lists, + * containers, and special literal datatypes. + * + * **Typical uses** + * - Using `RDF.type` to declare class membership. + * - Working with RDF lists via `RDF.first`, `RDF.rest`, `RDF.nil`. + * - Handling RDF-specific literal types like `RDF.HTML` or `RDF.JSON`. + * + * @example Declaring types + * ```ts + * import { RDF, RDFS } from './namespaces.ts' * - * @see https://www.w3.org/TR/rdf11-concepts/ + * where(triple('?c', RDF.type, RDFS.Class)) + * ``` + * + * @example RDF list traversal + * ```ts + * select(['?item']) + * .where(triple('?list', RDF.first, '?item')) + * .where(triple('?list', RDF.rest, RDF.nil)) + * ``` */ export const RDF = { - /** Base namespace. */ _namespace: 'http://www.w3.org/1999/02/22-rdf-syntax-ns#', - // Core structural terms - - /** The relationship that assigns a class to a resource (A rdf:type B). */ + /** Assigns a class to a resource: `?s rdf:type ?class`. */ type: 'http://www.w3.org/1999/02/22-rdf-syntax-ns#type', /** Class of RDF properties. */ Property: 'http://www.w3.org/1999/02/22-rdf-syntax-ns#Property', - /** Reified statement class (rarely used in modern data, but part of RDF). */ + /** Reified statement (rarely used in modern data). */ Statement: 'http://www.w3.org/1999/02/22-rdf-syntax-ns#Statement', /** Subject of a reified statement. */ @@ -210,217 +260,242 @@ export const RDF = { /** Object of a reified statement. */ object: 'http://www.w3.org/1999/02/22-rdf-syntax-ns#object', - // Containers & lists + /** Generic value property (used in some vocabularies). */ + value: 'http://www.w3.org/1999/02/22-rdf-syntax-ns#value', - /** First element of an RDF list. */ - first: 'http://www.w3.org/1999/02/22-rdf-syntax-ns#first', - - /** Rest of an RDF list (points to another list or rdf:nil). */ - rest: 'http://www.w3.org/1999/02/22-rdf-syntax-ns#rest', - - /** Marker for the empty RDF list. */ - nil: 'http://www.w3.org/1999/02/22-rdf-syntax-ns#nil', + // Collections & containers /** Class of RDF lists. */ List: 'http://www.w3.org/1999/02/22-rdf-syntax-ns#List', - /** Container type: ordered collection (1, 2, 3, …). */ + /** Ordered container. */ Seq: 'http://www.w3.org/1999/02/22-rdf-syntax-ns#Seq', - /** Container type: unordered bag (multi-set). */ + /** Unordered bag (multiset). */ Bag: 'http://www.w3.org/1999/02/22-rdf-syntax-ns#Bag', - /** Container type: alternatives (one of several options). */ + /** Container of alternatives. */ Alt: 'http://www.w3.org/1999/02/22-rdf-syntax-ns#Alt', - // Literal-related datatypes + /** First element in an RDF list. */ + first: 'http://www.w3.org/1999/02/22-rdf-syntax-ns#first', + + /** Rest of an RDF list (another list or rdf:nil). */ + rest: 'http://www.w3.org/1999/02/22-rdf-syntax-ns#rest', - /** Special datatype for XML literal content. */ + /** Marker for the empty RDF list. */ + nil: 'http://www.w3.org/1999/02/22-rdf-syntax-ns#nil', + + // Literal datatypes + + /** XML literal datatype. */ XMLLiteral: 'http://www.w3.org/1999/02/22-rdf-syntax-ns#XMLLiteral', - /** Special datatype for language-tagged strings. */ + /** HTML literal datatype. */ + HTML: 'http://www.w3.org/1999/02/22-rdf-syntax-ns#HTML', + + /** JSON literal datatype (RDF 1.1+ extension). */ + JSON: 'http://www.w3.org/1999/02/22-rdf-syntax-ns#JSON', + + /** Language-tagged string datatype. */ langString: 'http://www.w3.org/1999/02/22-rdf-syntax-ns#langString', - /** Special datatype for HTML literal content. */ - HTML: 'http://www.w3.org/1999/02/22-rdf-syntax-ns#HTML', + /** Directional language-tagged string datatype. */ + dirLangString: 'http://www.w3.org/1999/02/22-rdf-syntax-ns#dirLangString', } as const satisfies NamespaceLike; +/* ======================================================================= */ +/* RDFS – RDF Schema */ +/* ======================================================================= */ + /** - * RDF Schema (RDFS) – basic schema vocabulary. + * RDFS – RDF Schema. + * + * **Intent** + * RDFS gives you minimal schema language for RDF: + * - Class hierarchies (`rdfs:subClassOf`) + * - Property hierarchies (`rdfs:subPropertyOf`) + * - Domain and range constraints + * - Human-readable labels and comments + * + * **Typical uses** + * - Attaching human-friendly labels to resources. + * - Expressing lightweight type hierarchies. + * - Building small ontologies without full OWL. + * + * @example Labels for display + * ```ts + * import { RDF, RDFS } from './namespaces.ts' * - * Use this for: - * - Human-readable labels and descriptions. - * - Class hierarchies (`rdfs:subClassOf`). - * - Property hierarchies (`rdfs:subPropertyOf`). - * - Domain/range constraints. + * select(['?resource', '?label']) + * .where(triple('?resource', RDF.type, RDFS.Class)) + * .where(triple('?resource', RDFS.label, '?label')) + * ``` * - * @see https://www.w3.org/TR/rdf-schema/ + * @example Discovering subclasses + * ```ts + * select(['?sub']) + * .where(triple('?sub', RDFS.subClassOf, 'narrative:Product')) + * ``` */ export const RDFS = { - /** Base namespace. */ _namespace: 'http://www.w3.org/2000/01/rdf-schema#', - // Annotations - - /** Human-readable name for a resource. */ + /** Human-readable label. */ label: 'http://www.w3.org/2000/01/rdf-schema#label', - /** Human-readable description or documentation. */ + /** Human-readable description / documentation. */ comment: 'http://www.w3.org/2000/01/rdf-schema#comment', - /** See also: link to related resources. */ + /** Link to related resources. */ seeAlso: 'http://www.w3.org/2000/01/rdf-schema#seeAlso', - /** Link to the defining resource for this term. */ + /** Link to the defining resource of a term. */ isDefinedBy: 'http://www.w3.org/2000/01/rdf-schema#isDefinedBy', - // Core schema terms - /** Class of all RDFS classes. */ Class: 'http://www.w3.org/2000/01/rdf-schema#Class', - /** Class of all RDF resources that can be named by an IRI. */ + /** Class of all resources that can be named. */ Resource: 'http://www.w3.org/2000/01/rdf-schema#Resource', - /** Class of literal values (strings, numbers, dates, etc.). */ + /** Class of literal values. */ Literal: 'http://www.w3.org/2000/01/rdf-schema#Literal', - /** Class of data types (e.g., xsd:integer, xsd:string). */ + /** Class of datatypes. */ Datatype: 'http://www.w3.org/2000/01/rdf-schema#Datatype', - /** Class of container membership properties (rdf:_1, rdf:_2, …). */ - ContainerMembershipProperty: 'http://www.w3.org/2000/01/rdf-schema#ContainerMembershipProperty', - /** Class of containers (rdf:Bag, rdf:Seq, rdf:Alt). */ Container: 'http://www.w3.org/2000/01/rdf-schema#Container', - // Hierarchies & constraints + /** Class of container membership properties (rdf:_1, rdf:_2, …). */ + ContainerMembershipProperty: + 'http://www.w3.org/2000/01/rdf-schema#ContainerMembershipProperty', /** Relates a class to its superclass. */ subClassOf: 'http://www.w3.org/2000/01/rdf-schema#subClassOf', - /** Relates a property to a more general super-property. */ + /** Relates a property to its super-property. */ subPropertyOf: 'http://www.w3.org/2000/01/rdf-schema#subPropertyOf', - /** Domain constraint: types of subjects that can use this property. */ + /** Domain constraint for a property. */ domain: 'http://www.w3.org/2000/01/rdf-schema#domain', - /** Range constraint: types of objects this property can have. */ + /** Range constraint for a property. */ range: 'http://www.w3.org/2000/01/rdf-schema#range', - /** Membership relation between containers and their members. */ + /** Membership relation between containers and members. */ member: 'http://www.w3.org/2000/01/rdf-schema#member', } as const satisfies NamespaceLike; +/* ======================================================================= */ +/* OWL – Web Ontology Language */ +/* ======================================================================= */ + /** - * OWL namespace – Web Ontology Language. + * OWL – Web Ontology Language (OWL 2 core). * - * OWL extends RDFS with richer modeling constructs and is widely used for: - * - Expressive ontologies. - * - Reasoning (inference, classification). - * - Equivalence, disjointness, complex class expressions. + * **Intent** + * OWL lets you define more expressive ontologies than RDFS: + * - Equivalence and disjointness between classes and properties. + * - Property characteristics (functional, inverse functional, symmetric, etc.). + * - Complex class expressions (restrictions, intersections, unions). * - * @see https://www.w3.org/TR/owl2-overview/ + * **Typical uses** + * - Reasoner-backed knowledge graphs. + * - Entity resolution (owl:sameAs). + * - Complex domain models and validation. + * + * @example sameAs for entity resolution + * ```ts + * import { OWL } from './namespaces.ts' + * + * where(triple('?comic', OWL.sameAs, '?externalComic')) + * ``` */ export const OWL = { - /** Base namespace. */ _namespace: 'http://www.w3.org/2002/07/owl#', // Core classes - /** Class of OWL ontologies. */ + /** An ontology (document-level resource). */ Ontology: 'http://www.w3.org/2002/07/owl#Ontology', - /** Root of all individuals (top of the class hierarchy). */ + /** Class of OWL classes. */ + Class: 'http://www.w3.org/2002/07/owl#Class', + + /** Top of the class hierarchy (everything is an owl:Thing). */ Thing: 'http://www.w3.org/2002/07/owl#Thing', - /** Empty class (no individuals). */ + /** Bottom of the class hierarchy (no instances). */ Nothing: 'http://www.w3.org/2002/07/owl#Nothing', - /** Class of OWL classes. */ - Class: 'http://www.w3.org/2002/07/owl#Class', + // Properties - /** Class of object properties (link individuals to individuals). */ + /** Object property (links individuals to individuals). */ ObjectProperty: 'http://www.w3.org/2002/07/owl#ObjectProperty', - /** Class of datatype properties (link individuals to literals). */ + /** Datatype property (links individuals to literals). */ DatatypeProperty: 'http://www.w3.org/2002/07/owl#DatatypeProperty', - /** Class of annotation properties (labels, comments, etc. in OWL). */ + /** Annotation property (labels, comments, etc.). */ AnnotationProperty: 'http://www.w3.org/2002/07/owl#AnnotationProperty', - /** Class of ontology properties (describe ontologies themselves). */ - OntologyProperty: 'http://www.w3.org/2002/07/owl#OntologyProperty', - - // Property characteristics - - /** Functional property (at most one value per subject). */ + /** Functional property (at most one value). */ FunctionalProperty: 'http://www.w3.org/2002/07/owl#FunctionalProperty', - /** Inverse functional property (inverse is functional). */ - InverseFunctionalProperty: 'http://www.w3.org/2002/07/owl#InverseFunctionalProperty', + /** Inverse functional property (inverse has at most one value). */ + InverseFunctionalProperty: + 'http://www.w3.org/2002/07/owl#InverseFunctionalProperty', - /** Symmetric property (if A relates to B, then B relates to A). */ + /** Symmetric property (A R B ⇒ B R A). */ SymmetricProperty: 'http://www.w3.org/2002/07/owl#SymmetricProperty', - /** Asymmetric property (never holds in both directions). */ - AsymmetricProperty: 'http://www.w3.org/2002/07/owl#AsymmetricProperty', - - /** Transitive property (A→B and B→C implies A→C). */ + /** Transitive property (A R B ∧ B R C ⇒ A R C). */ TransitiveProperty: 'http://www.w3.org/2002/07/owl#TransitiveProperty', + /** Asymmetric property. */ + AsymmetricProperty: 'http://www.w3.org/2002/07/owl#AsymmetricProperty', + /** Reflexive property (every individual relates to itself). */ ReflexiveProperty: 'http://www.w3.org/2002/07/owl#ReflexiveProperty', /** Irreflexive property (no individual relates to itself). */ IrreflexiveProperty: 'http://www.w3.org/2002/07/owl#IrreflexiveProperty', - // Equivalence & difference + /** Declares two properties as inverses. */ + inverseOf: 'http://www.w3.org/2002/07/owl#inverseOf', + + // Equivalence & disjointness /** Two resources refer to the same real-world entity. */ sameAs: 'http://www.w3.org/2002/07/owl#sameAs', - /** Two classes have exactly the same instances. */ - equivalentClass: 'http://www.w3.org/2002/07/owl#equivalentClass', + /** Two individuals are explicitly different. */ + differentFrom: 'http://www.w3.org/2002/07/owl#differentFrom', - /** Two properties have the same extension. */ - equivalentProperty: 'http://www.w3.org/2002/07/owl#equivalentProperty', + /** Classes with identical instances. */ + equivalentClass: 'http://www.w3.org/2002/07/owl#equivalentClass', - /** Two individuals are distinct. */ - differentFrom: 'http://www.w3.org/2002/07/owl#differentFrom', + /** Properties with identical extension. */ + equivalentProperty: + 'http://www.w3.org/2002/07/owl#equivalentProperty', - /** Declares a class disjoint with another (no shared instances). */ + /** Disjoint classes (no shared instances). */ disjointWith: 'http://www.w3.org/2002/07/owl#disjointWith', - /** Declares a disjoint union of classes. */ - disjointUnionOf: 'http://www.w3.org/2002/07/owl#disjointUnionOf', - // Class constructors - /** Union of multiple classes. */ - unionOf: 'http://www.w3.org/2002/07/owl#unionOf', - - /** Intersection of multiple classes. */ - intersectionOf: 'http://www.w3.org/2002/07/owl#intersectionOf', - - /** Complement of a class. */ - complementOf: 'http://www.w3.org/2002/07/owl#complementOf', - - /** Enumerated class (explicit list of individuals). */ - oneOf: 'http://www.w3.org/2002/07/owl#oneOf', - - // Restrictions - - /** Class of property restrictions. */ + /** Class of restrictions. */ Restriction: 'http://www.w3.org/2002/07/owl#Restriction', /** Property being restricted. */ onProperty: 'http://www.w3.org/2002/07/owl#onProperty', - /** Restricted to values from a given class. */ + /** All values must be from this class. */ allValuesFrom: 'http://www.w3.org/2002/07/owl#allValuesFrom', - /** Restricted to some values from a given class. */ + /** At least one value must be from this class. */ someValuesFrom: 'http://www.w3.org/2002/07/owl#someValuesFrom', /** Property must have the given value. */ @@ -434,25 +509,59 @@ export const OWL = { /** Maximum cardinality restriction. */ maxCardinality: 'http://www.w3.org/2002/07/owl#maxCardinality', + + /** Intersection of multiple classes. */ + intersectionOf: 'http://www.w3.org/2002/07/owl#intersectionOf', + + /** Union of multiple classes. */ + unionOf: 'http://www.w3.org/2002/07/owl#unionOf', + + /** Complement of a class. */ + complementOf: 'http://www.w3.org/2002/07/owl#complementOf', + + /** Enumeration of individuals forming a class. */ + oneOf: 'http://www.w3.org/2002/07/owl#oneOf', + + // Individuals + + /** Named individual (explicitly named resource). */ + NamedIndividual: + 'http://www.w3.org/2002/07/owl#NamedIndividual', } as const satisfies NamespaceLike; +/* ======================================================================= */ +/* FOAF – Friend of a Friend */ +/* ======================================================================= */ + /** - * FOAF – Friend of a Friend vocabulary. + * FOAF – Friend of a Friend. * + * **Intent** * FOAF is a classic vocabulary for modeling: - * - People (names, accounts, profiles). - * - Organizations and groups. - * - Social relationships (knows, member, etc.). + * - People (names, accounts, profiles) + * - Organizations and groups + * - Social relationships (`foaf:knows`) + * + * **Typical uses** + * - Person profiles and social graphs. + * - Linking users to pages, images, and accounts. * - * @see http://xmlns.com/foaf/spec/ + * @example Basic person query + * ```ts + * import { RDF, FOAF } from './namespaces.ts' + * + * select(['?name', '?email']) + * .where(triple('?person', RDF.type, FOAF.Person)) + * .where(triple('?person', FOAF.name, '?name')) + * .optional(triple('?person', FOAF.mbox, '?email')) + * ``` */ export const FOAF = { - /** Base namespace. */ _namespace: 'http://xmlns.com/foaf/0.1/', // Core classes - /** Generic agent – person, organization, software, etc. */ + /** Generic agent (person, organization, software, etc.). */ Agent: 'http://xmlns.com/foaf/0.1/Agent', /** A person. */ @@ -461,7 +570,7 @@ export const FOAF = { /** An organization. */ Organization: 'http://xmlns.com/foaf/0.1/Organization', - /** A group of Agents. */ + /** A group of agents. */ Group: 'http://xmlns.com/foaf/0.1/Group', /** A document (web page, file, etc.). */ @@ -470,145 +579,146 @@ export const FOAF = { /** An image (photo, avatar, etc.). */ Image: 'http://xmlns.com/foaf/0.1/Image', - /** A project (endeavor, product, etc.). */ + /** A project. */ Project: 'http://xmlns.com/foaf/0.1/Project', /** An online account. */ OnlineAccount: 'http://xmlns.com/foaf/0.1/OnlineAccount', - // Person / agent properties + // Descriptive properties - /** Name of a thing (often full name). */ + /** Name of a person or thing (often full name). */ name: 'http://xmlns.com/foaf/0.1/name', - /** First/given name. */ + /** Given / first name. */ givenName: 'http://xmlns.com/foaf/0.1/givenName', - /** Last/family name. */ + /** Family / last name. */ familyName: 'http://xmlns.com/foaf/0.1/familyName', - /** Personal title (Mr, Ms, Dr, etc.). */ - title: 'http://xmlns.com/foaf/0.1/title', - /** Nickname or handle. */ nick: 'http://xmlns.com/foaf/0.1/nick', - /** Gender (often string values like "male", "female", ...). */ + /** Title (Mr, Ms, Dr, etc.). */ + title: 'http://xmlns.com/foaf/0.1/title', + + /** Gender string (not standardized, but commonly used). */ gender: 'http://xmlns.com/foaf/0.1/gender', /** Age in years. */ age: 'http://xmlns.com/foaf/0.1/age', - /** Birthday (often interpreted as xsd:date). */ + /** Birthday (often xsd:date). */ birthday: 'http://xmlns.com/foaf/0.1/birthday', - /** Home page of a person or thing. */ - homepage: 'http://xmlns.com/foaf/0.1/homepage', - - /** Weblog / blog of a person or thing. */ - weblog: 'http://xmlns.com/foaf/0.1/weblog', - - /** A generic page about something. */ - page: 'http://xmlns.com/foaf/0.1/page', - - /** Depiction (an image that shows this resource). */ - depiction: 'http://xmlns.com/foaf/0.1/depiction', - - /** Resource that is depicted in an image. */ - depicts: 'http://xmlns.com/foaf/0.1/depicts', - - /** Thumbnail image. */ - thumbnail: 'http://xmlns.com/foaf/0.1/thumbnail', - - /** Image (simpler alias often used instead of depiction). */ - img: 'http://xmlns.com/foaf/0.1/img', + // Contact & web presence - /** Interest of a person. */ - interest: 'http://xmlns.com/foaf/0.1/interest', + /** Email address (usually as mailto: IRI). */ + mbox: 'http://xmlns.com/foaf/0.1/mbox', - /** Topic a person is interested in. */ - topic_interest: 'http://xmlns.com/foaf/0.1/topic_interest', + /** SHA1 hash of email (privacy-friendly ID). */ + mbox_sha1sum: 'http://xmlns.com/foaf/0.1/mbox_sha1sum', - /** Topic of some document or thing. */ - topic: 'http://xmlns.com/foaf/0.1/topic', + /** Phone number. */ + phone: 'http://xmlns.com/foaf/0.1/phone', - /** Person's workplace homepage. */ - workplaceHomepage: 'http://xmlns.com/foaf/0.1/workplaceHomepage', + /** Homepage of a person or thing. */ + homepage: 'http://xmlns.com/foaf/0.1/homepage', - /** Person's school/university homepage. */ - schoolHomepage: 'http://xmlns.com/foaf/0.1/schoolHomepage', + /** Weblog/blog. */ + weblog: 'http://xmlns.com/foaf/0.1/weblog', - /** Address/location relation (broad). */ - based_near: 'http://xmlns.com/foaf/0.1/based_near', + /** Generic page about the thing. */ + page: 'http://xmlns.com/foaf/0.1/page', // Social graph - /** Social relationship - person knows another person. */ + /** Person knows another person. */ knows: 'http://xmlns.com/foaf/0.1/knows', - /** Membership relation: agent is a member of a group. */ + /** Membership of a group. */ member: 'http://xmlns.com/foaf/0.1/member', - /** Class of membership relations. */ - membershipClass: 'http://xmlns.com/foaf/0.1/membershipClass', + // Images / depictions - // Accounts & identifiers + /** An image representing the thing. */ + img: 'http://xmlns.com/foaf/0.1/img', - /** Email address (usually `mailto:` IRI). */ - mbox: 'http://xmlns.com/foaf/0.1/mbox', + /** An image that depicts the resource. */ + depiction: 'http://xmlns.com/foaf/0.1/depiction', - /** SHA1 hash of an email address (privacy-preserving identifier). */ - mbox_sha1sum: 'http://xmlns.com/foaf/0.1/mbox_sha1sum', + /** Resource depicted in an image. */ + depicts: 'http://xmlns.com/foaf/0.1/depicts', - /** An online account belonging to the agent. */ + // Accounts + + /** Online account belonging to the agent. */ account: 'http://xmlns.com/foaf/0.1/account', - /** Name (username) of an online account. */ + /** Username of an online account. */ accountName: 'http://xmlns.com/foaf/0.1/accountName', - /** Service homepage of an online account (e.g., twitter.com). */ - accountServiceHomepage: 'http://xmlns.com/foaf/0.1/accountServiceHomepage', + /** Service homepage of an online account (e.g., https://twitter.com). */ + accountServiceHomepage: + 'http://xmlns.com/foaf/0.1/accountServiceHomepage', } as const satisfies NamespaceLike; +/* ======================================================================= */ +/* Schema.org – curated subset */ +/* ======================================================================= */ + /** - * Schema.org vocabulary – structured data for the web. + * Schema.org – structured data for the web. + * + * **Intent** + * Schema.org is a large vocabulary used for SEO, rich snippets, and web-structured + * data (products, places, events, articles, organizations, etc.). * - * **Important note:** this module uses `http://schema.org/` IRIs, which are the - * most widely used and historically canonical forms. If your data uses - * `https://schema.org/` instead, you should either: - * - Normalize IRIs when ingesting, or - * - Provide a schema namespace variant that uses `https://`. + * **Typical uses** + * - Product catalogs, prices, availability. + * - Organizations and locations. + * - Articles, events, and creative works. * - * This is only a curated subset of Schema.org – enough for many common - * scenarios (products, organizations, places, events, content). + * This is a **curated subset**, not the full Schema.org universe. * - * @see http://schema.org/ + * @example Product query + * ```ts + * import { RDF, SCHEMA } from './namespaces.ts' + * + * select(['?name', '?price']) + * .where(triple('?p', RDF.type, SCHEMA.Product)) + * .where(triple('?p', SCHEMA.name, '?name')) + * .where(triple('?p', SCHEMA.price, '?price')) + * ``` */ export const SCHEMA = { - /** Base namespace (http, not https). */ + // Note: schema.org now often uses https:// in docs, but http:// IRIs are widely used. _namespace: 'http://schema.org/', - // Very common generic properties + // Generic properties /** Name of the thing. */ name: 'http://schema.org/name', + /** An alias for the item. */ + alternateName: 'http://schema.org/alternateName', + /** Description of the thing. */ description: 'http://schema.org/description', /** URL of the thing. */ url: 'http://schema.org/url', - /** Link to a representative image. */ + /** Representative image. */ image: 'http://schema.org/image', - /** Identifier for the thing (could be URI, SKU, etc.). */ + /** Identifier (could be SKU, ISBN, etc.). */ identifier: 'http://schema.org/identifier', - /** Link to a page that unambiguously indicates the item's identity. */ + /** Link to an unambiguous reference (e.g. Wikidata). */ sameAs: 'http://schema.org/sameAs', - // Core types + // Types /** A person. */ Person: 'http://schema.org/Person', @@ -616,66 +726,71 @@ export const SCHEMA = { /** An organization. */ Organization: 'http://schema.org/Organization', - /** A place (address, geo, etc.). */ + /** A product. */ + Product: 'http://schema.org/Product', + + /** A place. */ Place: 'http://schema.org/Place', - /** A postal address. */ - PostalAddress: 'http://schema.org/PostalAddress', + /** An event. */ + Event: 'http://schema.org/Event', - /** A creative work (article, book, movie, etc.). */ + /** A creative work. */ CreativeWork: 'http://schema.org/CreativeWork', - /** An article (news, blog post, etc.). */ + /** An article (blog post, news, etc.). */ Article: 'http://schema.org/Article', - /** A web page. */ - WebPage: 'http://schema.org/WebPage', - - /** A web site. */ - WebSite: 'http://schema.org/WebSite', - - /** A product. */ - Product: 'http://schema.org/Product', - /** An offer to sell or lease something. */ Offer: 'http://schema.org/Offer', - /** An event (concert, meetup, etc.). */ - Event: 'http://schema.org/Event', - - /** Rating (1–5 stars, etc.). */ - Rating: 'http://schema.org/Rating', + /** Aggregate offer (min/max prices, etc.). */ + AggregateOffer: 'http://schema.org/AggregateOffer', - /** Aggregate rating (average + count). */ - AggregateRating: 'http://schema.org/AggregateRating', + // Product / commerce - // Product / offer properties - - /** Price (numeric; often with a currency). */ + /** Price of an offer or product. */ price: 'http://schema.org/price', - /** Price currency (ISO 4217, e.g., "USD"). */ + /** Price currency (ISO 4217). */ priceCurrency: 'http://schema.org/priceCurrency', /** Availability status (InStock, OutOfStock, etc.). */ availability: 'http://schema.org/availability', + /** Stock keeping unit. */ + sku: 'http://schema.org/sku', + /** Brand associated with the product. */ brand: 'http://schema.org/brand', - /** Stock keeping unit (SKU). */ - sku: 'http://schema.org/sku', + /** Item condition (e.g., NewCondition). */ + itemCondition: 'http://schema.org/itemCondition', + + /** Offers associated with a product. */ + offers: 'http://schema.org/offers', - /** Global Trade Item Number (13-digit). */ - gtin13: 'http://schema.org/gtin13', + // Ratings & reviews - /** Global Trade Item Number (various lengths). */ - gtin: 'http://schema.org/gtin', + /** Aggregate rating node. */ + AggregateRating: 'http://schema.org/AggregateRating', - /** Link to the offer associated with a product. */ - offers: 'http://schema.org/offers', + /** Property linking a thing to its aggregate rating. */ + aggregateRating: 'http://schema.org/aggregateRating', - // Person / contact properties + /** Rating value (numeric). */ + ratingValue: 'http://schema.org/ratingValue', + + /** Count of reviews. */ + reviewCount: 'http://schema.org/reviewCount', + + /** Review type. */ + Review: 'http://schema.org/Review', + + /** Property linking a thing to its reviews. */ + review: 'http://schema.org/review', + + // Person / contact /** Email address. */ email: 'http://schema.org/email', @@ -689,10 +804,13 @@ export const SCHEMA = { /** Organization a person works for. */ worksFor: 'http://schema.org/worksFor', - /** Address of a person or organization. */ + /** Postal address. */ address: 'http://schema.org/address', - // Address properties + // Postal address fields + + /** Postal address type. */ + PostalAddress: 'http://schema.org/PostalAddress', /** Street address. */ streetAddress: 'http://schema.org/streetAddress', @@ -706,62 +824,559 @@ export const SCHEMA = { /** Postal code. */ postalCode: 'http://schema.org/postalCode', - /** Country (text or ISO code). */ + /** Country. */ addressCountry: 'http://schema.org/addressCountry', - // Organization relationships + // Authorship / publication + + /** Author of content. */ + author: 'http://schema.org/author', + + /** Creator (alias/related to author). */ + creator: 'http://schema.org/creator', /** Publisher of a creative work. */ publisher: 'http://schema.org/publisher', - /** Author of a creative work. */ - author: 'http://schema.org/author', - - /** Parent organization. */ - parentOrganization: 'http://schema.org/parentOrganization', + /** Publication date. */ + datePublished: 'http://schema.org/datePublished', - /** Sub-organization. */ - subOrganization: 'http://schema.org/subOrganization', + /** Last modification date. */ + dateModified: 'http://schema.org/dateModified', - // Temporal properties + // Events / temporal - /** Start date of an event or temporal thing. */ + /** Start date/time of an event. */ startDate: 'http://schema.org/startDate', - /** End date of an event or temporal thing. */ + /** End date/time of an event. */ endDate: 'http://schema.org/endDate', - /** Publication date. */ - datePublished: 'http://schema.org/datePublished', + /** Location of an event or organization. */ + location: 'http://schema.org/location', +} as const satisfies NamespaceLike; - /** Modification date. */ - dateModified: 'http://schema.org/dateModified', +/* ======================================================================= */ +/* SD – SPARQL Service Description */ +/* ======================================================================= */ + +/** + * SD – SPARQL Service Description vocabulary. + * + * **Intent** + * This vocabulary describes SPARQL endpoints and their capabilities, usually + * exposed at the service URL as RDF. It tells you: + * - What datasets and graphs exist. + * - Which features, result formats, and languages are supported. + * + * **Typical uses** + * - Inspecting an endpoint’s capabilities before deciding which features to use. + */ +export const SD = { + _namespace: 'http://www.w3.org/ns/sparql-service-description#', + + Service: 'http://www.w3.org/ns/sparql-service-description#Service', + Dataset: 'http://www.w3.org/ns/sparql-service-description#Dataset', + Graph: 'http://www.w3.org/ns/sparql-service-description#Graph', + + endpoint: 'http://www.w3.org/ns/sparql-service-description#endpoint', + url: 'http://www.w3.org/ns/sparql-service-description#url', + + defaultDataset: + 'http://www.w3.org/ns/sparql-service-description#defaultDataset', + namedGraph: + 'http://www.w3.org/ns/sparql-service-description#namedGraph', + name: 'http://www.w3.org/ns/sparql-service-description#name', + graph: 'http://www.w3.org/ns/sparql-service-description#graph', + + feature: 'http://www.w3.org/ns/sparql-service-description#feature', + supportedLanguage: + 'http://www.w3.org/ns/sparql-service-description#supportedLanguage', + languageExtension: + 'http://www.w3.org/ns/sparql-service-description#languageExtension', + + defaultEntailmentRegime: + 'http://www.w3.org/ns/sparql-service-description#defaultEntailmentRegime', + entailmentRegime: + 'http://www.w3.org/ns/sparql-service-description#entailmentRegime', + + extensionFunction: + 'http://www.w3.org/ns/sparql-service-description#extensionFunction', + extensionAggregate: + 'http://www.w3.org/ns/sparql-service-description#extensionAggregate', + + resultFormat: + 'http://www.w3.org/ns/sparql-service-description#resultFormat', +} as const satisfies NamespaceLike; + +/* ======================================================================= */ +/* SHACL – Shapes Constraint Language */ +/* ======================================================================= */ + +/** + * SHACL – Shapes Constraint Language. + * + * **Intent** + * SHACL lets you define validation rules ("shapes") for RDF graphs: + * - Cardinality constraints (`sh:minCount`, `sh:maxCount`) + * - Datatype and class constraints + * - Complex logical combinations of constraints + * + * **Typical uses** + * - Validating datasets before loading them into a KG. + * - Encoding business rules and invariants in RDF form. + */ +export const SHACL = { + _namespace: 'http://www.w3.org/ns/shacl#', + + // Core classes + Shape: 'http://www.w3.org/ns/shacl#Shape', + NodeShape: 'http://www.w3.org/ns/shacl#NodeShape', + PropertyShape: 'http://www.w3.org/ns/shacl#PropertyShape', + + // Targeting + targetClass: 'http://www.w3.org/ns/shacl#targetClass', + targetNode: 'http://www.w3.org/ns/shacl#targetNode', + targetSubjectsOf: 'http://www.w3.org/ns/shacl#targetSubjectsOf', + targetObjectsOf: 'http://www.w3.org/ns/shacl#targetObjectsOf', + + // Structure + path: 'http://www.w3.org/ns/shacl#path', + property: 'http://www.w3.org/ns/shacl#property', + node: 'http://www.w3.org/ns/shacl#node', + class: 'http://www.w3.org/ns/shacl#class', + datatype: 'http://www.w3.org/ns/shacl#datatype', + nodeKind: 'http://www.w3.org/ns/shacl#nodeKind', + + // Cardinality + minCount: 'http://www.w3.org/ns/shacl#minCount', + maxCount: 'http://www.w3.org/ns/shacl#maxCount', + + // Value ranges + minInclusive: 'http://www.w3.org/ns/shacl#minInclusive', + maxInclusive: 'http://www.w3.org/ns/shacl#maxInclusive', + minExclusive: 'http://www.w3.org/ns/shacl#minExclusive', + maxExclusive: 'http://www.w3.org/ns/shacl#maxExclusive', + + // Patterns and enumeration + pattern: 'http://www.w3.org/ns/shacl#pattern', + flags: 'http://www.w3.org/ns/shacl#flags', + in: 'http://www.w3.org/ns/shacl#in', + hasValue: 'http://www.w3.org/ns/shacl#hasValue', + + // Logical combinations + and: 'http://www.w3.org/ns/shacl#and', + or: 'http://www.w3.org/ns/shacl#or', + not: 'http://www.w3.org/ns/shacl#not', + xone: 'http://www.w3.org/ns/shacl#xone', + + // Qualified value shapes + qualifiedValueShape: + 'http://www.w3.org/ns/shacl#qualifiedValueShape', + qualifiedMinCount: + 'http://www.w3.org/ns/shacl#qualifiedMinCount', + qualifiedMaxCount: + 'http://www.w3.org/ns/shacl#qualifiedMaxCount', + + // Closed shapes + closed: 'http://www.w3.org/ns/shacl#closed', + ignoredProperties: + 'http://www.w3.org/ns/shacl#ignoredProperties', + + // Validation results + ValidationReport: + 'http://www.w3.org/ns/shacl#ValidationReport', + ValidationResult: + 'http://www.w3.org/ns/shacl#ValidationResult', + conforms: 'http://www.w3.org/ns/shacl#conforms', + result: 'http://www.w3.org/ns/shacl#result', + focusNode: 'http://www.w3.org/ns/shacl#focusNode', + resultPath: 'http://www.w3.org/ns/shacl#resultPath', + value: 'http://www.w3.org/ns/shacl#value', + resultMessage: + 'http://www.w3.org/ns/shacl#resultMessage', + resultSeverity: + 'http://www.w3.org/ns/shacl#resultSeverity', +} as const satisfies NamespaceLike; + +/* ======================================================================= */ +/* SKOS – Simple Knowledge Organization System */ +/* ======================================================================= */ + +/** + * SKOS – Simple Knowledge Organization System. + * + * **Intent** + * SKOS is used for thesauri, taxonomies, classification schemes, and controlled + * vocabularies (concepts, labels, broader/narrower relations). + * + * **Typical uses** + * - Modeling genres, subject headings, tag vocabularies. + * - Multi-level classification systems for products, story arcs, etc. + */ +export const SKOS = { + _namespace: 'http://www.w3.org/2004/02/skos/core#', + + // Core classes + Concept: 'http://www.w3.org/2004/02/skos/core#Concept', + ConceptScheme: + 'http://www.w3.org/2004/02/skos/core#ConceptScheme', + Collection: 'http://www.w3.org/2004/02/skos/core#Collection', + OrderedCollection: + 'http://www.w3.org/2004/02/skos/core#OrderedCollection', + + // Labelling + prefLabel: 'http://www.w3.org/2004/02/skos/core#prefLabel', + altLabel: 'http://www.w3.org/2004/02/skos/core#altLabel', + hiddenLabel: + 'http://www.w3.org/2004/02/skos/core#hiddenLabel', + notation: 'http://www.w3.org/2004/02/skos/core#notation', + + // Documentation + note: 'http://www.w3.org/2004/02/skos/core#note', + definition: + 'http://www.w3.org/2004/02/skos/core#definition', + scopeNote: 'http://www.w3.org/2004/02/skos/core#scopeNote', + example: 'http://www.w3.org/2004/02/skos/core#example', + + // Hierarchies + broader: 'http://www.w3.org/2004/02/skos/core#broader', + narrower: 'http://www.w3.org/2004/02/skos/core#narrower', + broaderTransitive: + 'http://www.w3.org/2004/02/skos/core#broaderTransitive', + narrowerTransitive: + 'http://www.w3.org/2004/02/skos/core#narrowerTransitive', + related: 'http://www.w3.org/2004/02/skos/core#related', + + // Schemes + inScheme: 'http://www.w3.org/2004/02/skos/core#inScheme', + hasTopConcept: + 'http://www.w3.org/2004/02/skos/core#hasTopConcept', + topConceptOf: + 'http://www.w3.org/2004/02/skos/core#topConceptOf', + + // Collections + member: 'http://www.w3.org/2004/02/skos/core#member', + memberList: + 'http://www.w3.org/2004/02/skos/core#memberList', + + // Mappings + semanticRelation: + 'http://www.w3.org/2004/02/skos/core#semanticRelation', + mappingRelation: + 'http://www.w3.org/2004/02/skos/core#mappingRelation', + exactMatch: 'http://www.w3.org/2004/02/skos/core#exactMatch', + closeMatch: 'http://www.w3.org/2004/02/skos/core#closeMatch', + broadMatch: 'http://www.w3.org/2004/02/skos/core#broadMatch', + narrowMatch: + 'http://www.w3.org/2004/02/skos/core#narrowMatch', + relatedMatch: + 'http://www.w3.org/2004/02/skos/core#relatedMatch', } as const satisfies NamespaceLike; +/* ======================================================================= */ +/* PROV-O – Provenance ontology */ +/* ======================================================================= */ + /** - * Helper to obtain the base namespace IRI for a vocabulary. + * PROV – W3C Provenance Ontology. + * + * **Intent** + * PROV describes how data was produced: + * - Which activities generated which entities. + * - Which agents were responsible. + * - When those activities happened. * - * This is handy when building PREFIX declarations programmatically. + * **Typical uses** + * - Tracking data lineage. + * - Recording who asserted which statements and when. + */ +export const PROV = { + _namespace: 'http://www.w3.org/ns/prov#', + + // Core classes + Entity: 'http://www.w3.org/ns/prov#Entity', + Activity: 'http://www.w3.org/ns/prov#Activity', + Agent: 'http://www.w3.org/ns/prov#Agent', + + // Core relations + wasGeneratedBy: + 'http://www.w3.org/ns/prov#wasGeneratedBy', + used: 'http://www.w3.org/ns/prov#used', + wasDerivedFrom: + 'http://www.w3.org/ns/prov#wasDerivedFrom', + wasAttributedTo: + 'http://www.w3.org/ns/prov#wasAttributedTo', + wasAssociatedWith: + 'http://www.w3.org/ns/prov#wasAssociatedWith', + actedOnBehalfOf: + 'http://www.w3.org/ns/prov#actedOnBehalfOf', + wasInformedBy: + 'http://www.w3.org/ns/prov#wasInformedBy', + wasInfluencedBy: + 'http://www.w3.org/ns/prov#wasInfluencedBy', + + // Qualifiers + startedAtTime: + 'http://www.w3.org/ns/prov#startedAtTime', + endedAtTime: 'http://www.w3.org/ns/prov#endedAtTime', + atLocation: 'http://www.w3.org/ns/prov#atLocation', + hadPrimarySource: + 'http://www.w3.org/ns/prov#hadPrimarySource', + specializationOf: + 'http://www.w3.org/ns/prov#specializationOf', + alternateOf: 'http://www.w3.org/ns/prov#alternateOf', +} as const satisfies NamespaceLike; + +/* ======================================================================= */ +/* VoID – Vocabulary of Interlinked Datasets */ +/* ======================================================================= */ + +/** + * VoID – Vocabulary of Interlinked Datasets. + * + * **Intent** + * VoID is used to describe dataset-level metadata: + * - Size, links, partitions, example resources. + * - SPARQL endpoints and data dumps. + */ +export const VOID = { + _namespace: 'http://rdfs.org/ns/void#', + + Dataset: 'http://rdfs.org/ns/void#Dataset', + Linkset: 'http://rdfs.org/ns/void#Linkset', + + subset: 'http://rdfs.org/ns/void#subset', + target: 'http://rdfs.org/ns/void#target', + linkPredicate: 'http://rdfs.org/ns/void#linkPredicate', + + triples: 'http://rdfs.org/ns/void#triples', + distinctSubjects: + 'http://rdfs.org/ns/void#distinctSubjects', + distinctObjects: + 'http://rdfs.org/ns/void#distinctObjects', + + classPartition: + 'http://rdfs.org/ns/void#classPartition', + propertyPartition: + 'http://rdfs.org/ns/void#propertyPartition', + + vocabulary: 'http://rdfs.org/ns/void#vocabulary', + dataDump: 'http://rdfs.org/ns/void#dataDump', + sparqlEndpoint: + 'http://rdfs.org/ns/void#sparqlEndpoint', + uriSpace: 'http://rdfs.org/ns/void#uriSpace', + exampleResource: + 'http://rdfs.org/ns/void#exampleResource', +} as const satisfies NamespaceLike; + +/* ======================================================================= */ +/* DCTERMS – Dublin Core Terms */ +/* ======================================================================= */ + +/** + * DCTERMS – Dublin Core Metadata Terms. + * + * **Intent** + * Dublin Core is a generic metadata vocabulary used all over the web + * and in many RDF datasets for titles, creators, dates, rights, etc. + */ +export const DCTERMS = { + _namespace: 'http://purl.org/dc/terms/', + + // Core DC elements (terms flavor) + title: 'http://purl.org/dc/terms/title', + creator: 'http://purl.org/dc/terms/creator', + subject: 'http://purl.org/dc/terms/subject', + description: 'http://purl.org/dc/terms/description', + publisher: 'http://purl.org/dc/terms/publisher', + contributor: 'http://purl.org/dc/terms/contributor', + date: 'http://purl.org/dc/terms/date', + type: 'http://purl.org/dc/terms/type', + format: 'http://purl.org/dc/terms/format', + identifier: 'http://purl.org/dc/terms/identifier', + source: 'http://purl.org/dc/terms/source', + language: 'http://purl.org/dc/terms/language', + relation: 'http://purl.org/dc/terms/relation', + coverage: 'http://purl.org/dc/terms/coverage', + rights: 'http://purl.org/dc/terms/rights', + + // Common refinements + created: 'http://purl.org/dc/terms/created', + modified: 'http://purl.org/dc/terms/modified', + issued: 'http://purl.org/dc/terms/issued', + license: 'http://purl.org/dc/terms/license', + rightsHolder: + 'http://purl.org/dc/terms/rightsHolder', + spatial: 'http://purl.org/dc/terms/spatial', + temporal: 'http://purl.org/dc/terms/temporal', +} as const satisfies NamespaceLike; + +/* ======================================================================= */ +/* GEO – WGS84 Geo Position */ +/* ======================================================================= */ + +/** + * GEO – WGS84 basic geo vocabulary. + * + * **Intent** + * Simple latitude/longitude/altitude vocabulary for expressing points + * on Earth (WGS84). + */ +export const GEO = { + _namespace: 'http://www.w3.org/2003/01/geo/wgs84_pos#', + + SpatialThing: + 'http://www.w3.org/2003/01/geo/wgs84_pos#SpatialThing', + Point: 'http://www.w3.org/2003/01/geo/wgs84_pos#Point', + + lat: 'http://www.w3.org/2003/01/geo/wgs84_pos#lat', + long: 'http://www.w3.org/2003/01/geo/wgs84_pos#long', + alt: 'http://www.w3.org/2003/01/geo/wgs84_pos#alt', +} as const satisfies NamespaceLike; + +/* ======================================================================= */ +/* GeoSPARQL – ontology and functions */ +/* ======================================================================= */ + +/** + * GEOSPARQL – ontology for geospatial data. + * + * **Intent** + * GeoSPARQL defines: + * - Feature / geometry classes + * - Datatypes for geometries (WKT, GML) + * - Topological relations (within, contains, etc.) + */ +export const GEOSPARQL = { + _namespace: 'http://www.opengis.net/ont/geosparql#', + + // Core classes + Feature: 'http://www.opengis.net/ont/geosparql#Feature', + Geometry: + 'http://www.opengis.net/ont/geosparql#Geometry', + + // Feature-geometry relations + hasGeometry: + 'http://www.opengis.net/ont/geosparql#hasGeometry', + hasDefaultGeometry: + 'http://www.opengis.net/ont/geosparql#hasDefaultGeometry', + + // Geometry literal datatypes + wktLiteral: + 'http://www.opengis.net/ont/geosparql#wktLiteral', + gmlLiteral: + 'http://www.opengis.net/ont/geosparql#gmlLiteral', + + // Common topological relations + sfWithin: + 'http://www.opengis.net/ont/geosparql#sfWithin', + sfContains: + 'http://www.opengis.net/ont/geosparql#sfContains', + sfOverlaps: + 'http://www.opengis.net/ont/geosparql#sfOverlaps', + sfIntersects: + 'http://www.opengis.net/ont/geosparql#sfIntersects', +} as const satisfies NamespaceLike; + +/** + * GEOF – GeoSPARQL functions namespace. + * + * **Intent** + * Defines IRI identifiers for spatial functions like distance, buffer, etc., + * used in SPARQL `FILTER` expressions. + */ +export const GEOF = { + _namespace: 'http://www.opengis.net/def/function/geosparql/', + + distance: + 'http://www.opengis.net/def/function/geosparql/distance', + buffer: + 'http://www.opengis.net/def/function/geosparql/buffer', + envelope: + 'http://www.opengis.net/def/function/geosparql/envelope', + intersection: + 'http://www.opengis.net/def/function/geosparql/intersection', + union: + 'http://www.opengis.net/def/function/geosparql/union', +} as const satisfies NamespaceLike; + +/* ======================================================================= */ +/* SPARQL Results vocabulary */ +/* ======================================================================= */ + +/** + * SPARQL_RESULTS – SPARQL Results vocabulary. + * + * **Intent** + * Used mostly in RDF encodings of SPARQL result sets (e.g., XML/JSON + * structured into RDF). You’ll see these IRIs if you round-trip results + * via RDF form. + */ +export const SPARQL_RESULTS = { + _namespace: 'http://www.w3.org/2005/sparql-results#', + + ResultSet: + 'http://www.w3.org/2005/sparql-results#ResultSet', + resultVariable: + 'http://www.w3.org/2005/sparql-results#resultVariable', + solution: + 'http://www.w3.org/2005/sparql-results#solution', + binding: 'http://www.w3.org/2005/sparql-results#binding', + variable: + 'http://www.w3.org/2005/sparql-results#variable', + value: 'http://www.w3.org/2005/sparql-results#value', + boolean: + 'http://www.w3.org/2005/sparql-results#boolean', +} as const satisfies NamespaceLike; + +/* ======================================================================= */ +/* Helper: getNamespaceIRI */ +/* ======================================================================= */ + +/** + * Union of all known namespace objects, for convenience. + */ +export type KnownNamespace = + | typeof XSD + | typeof RDF + | typeof RDFS + | typeof OWL + | typeof FOAF + | typeof SCHEMA + | typeof SD + | typeof SHACL + | typeof SKOS + | typeof PROV + | typeof VOID + | typeof DCTERMS + | typeof GEO + | typeof GEOSPARQL + | typeof GEOF + | typeof SPARQL_RESULTS; + +/** + * Get the base namespace IRI for a given vocabulary object. * - * @param ns - Any namespace object exported from this module. - * @returns The `_namespace` IRI. + * **Intent** + * Avoid hardcoding namespace IRIs when building `PREFIX` declarations or + * when you need to expose vocabulary metadata in your APIs. * - * @example Building PREFIX declarations + * @example Generate PREFIX declarations * ```ts - * import { RDF, RDFS, FOAF, SCHEMA, getNamespaceIRI } from './namespaces.ts' + * import { RDF, RDFS, SCHEMA, getNamespaceIRI } from './namespaces.ts' * * const prefixes = [ * ['rdf', getNamespaceIRI(RDF)], * ['rdfs', getNamespaceIRI(RDFS)], - * ['foaf', getNamespaceIRI(FOAF)], * ['schema', getNamespaceIRI(SCHEMA)], * ] * - * const query = buildQuery() + * const query = select(['?s']) * .prefixes(prefixes) - * .where(triple('?person', RDF.type, uri(FOAF.Person))) + * .where(triple('?s', RDF.type, SCHEMA.Product)) * ``` */ -export function getNamespaceIRI(ns: T): string { +export function getNamespaceIRI(ns: KnownNamespace): string { return ns._namespace; }