diff --git a/.changeset/long-files-look.md b/.changeset/long-files-look.md new file mode 100644 index 0000000..d5e9e6d --- /dev/null +++ b/.changeset/long-files-look.md @@ -0,0 +1,5 @@ +--- +"@rdfjs/types": patch +--- + +Make queryable metadata types configurable diff --git a/.changeset/pre.json b/.changeset/pre.json new file mode 100644 index 0000000..7b2891b --- /dev/null +++ b/.changeset/pre.json @@ -0,0 +1,13 @@ +{ + "mode": "pre", + "tag": "next", + "initialVersions": { + "@rdfjs/types": "1.0.1" + }, + "changesets": [ + "friendly-lies-suffer", + "long-files-look", + "seven-shrimps-build", + "tough-guests-flash" + ] +} diff --git a/.changeset/tough-guests-flash.md b/.changeset/tough-guests-flash.md new file mode 100644 index 0000000..ac39a32 --- /dev/null +++ b/.changeset/tough-guests-flash.md @@ -0,0 +1,5 @@ +--- +"@rdfjs/types": minor +--- + +Add queryable interfaces diff --git a/.github/workflows/release.yml b/.github/workflows/release.yml index cebc897..1e53d3b 100644 --- a/.github/workflows/release.yml +++ b/.github/workflows/release.yml @@ -4,6 +4,7 @@ on: push: branches: - master + - feature/query jobs: release: diff --git a/CHANGELOG.md b/CHANGELOG.md index 779f93c..8100a4d 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -1,5 +1,22 @@ # @rdfjs/types +## 1.1.0-next.1 + +### Patch Changes + +- Make queryable metadata types configurable + +## 1.1.0-next.0 + +### Minor Changes + +- 95f1e31: Dataset: Use correct type of `dataset` in methods with callbacks +- bc7163e: Add queryable interfaces + +### Patch Changes + +- 8164183: Documentation Fix: Update reference of Quad to BaseQuad in the definition of Term in order to align with the type declaration. + ## 1.0.1 ### Patch Changes diff --git a/index.d.ts b/index.d.ts index 9e2c7fb..b7a4b96 100644 --- a/index.d.ts +++ b/index.d.ts @@ -3,3 +3,4 @@ export * from './data-model'; export * from './stream'; export * from './dataset'; +export * from './query'; diff --git a/package.json b/package.json index edeb377..2c7bed2 100644 --- a/package.json +++ b/package.json @@ -1,6 +1,6 @@ { "name": "@rdfjs/types", - "version": "1.0.1", + "version": "1.1.0-next.1", "license": "MIT", "types": "index.d.ts", "author": { diff --git a/query.d.ts b/query.d.ts new file mode 100644 index 0000000..399bb4a --- /dev/null +++ b/query.d.ts @@ -0,0 +1,8 @@ +/* Query Interfaces */ +/* https://rdf.js.org/query-spec/ */ + +export * from './query/common'; +export * from './query/filterable'; +export * from './query/queryable'; + + diff --git a/query/common.d.ts b/query/common.d.ts new file mode 100644 index 0000000..dc13d3b --- /dev/null +++ b/query/common.d.ts @@ -0,0 +1,287 @@ +/* Query Interfaces - Common */ +/* https://rdf.js.org/query-spec/ */ + +import { EventEmitter } from "events"; +import * as RDF from '../data-model'; + +/** + * Helper union type for quad term names. + */ +export type QuadTermName = 'subject' | 'predicate' | 'object' | 'graph'; + +// TODO: merge this with Stream upon the next major change +/** + * Custom typings for the RDF/JS ResultStream interface as the current + * typings restrict the generic param Q to extensions of "BaseQuad", + * meaning it cannot be used for Bindings. + */ +export interface ResultStream extends EventEmitter { + read(): Q | null; +} + +/** + * QueryOperationCost represents the cost of a given query operation. + */ +export interface QueryOperationCost { + /** + * An estimation of how many iterations over items are executed. + * This is used to determine the CPU cost. + */ + iterations: number; + /** + * An estimation of how many items are stored in memory. + * This is used to determine the memory cost. + */ + persistedItems: number; + /** + * An estimation of how many items block the stream. + * This is used to determine the time the stream is not progressing anymore. + */ + blockingItems: number; + /** + * An estimation of the time to request items from sources. + * This is used to determine the I/O cost. + */ + requestTime: number; + /** + * Custom properties + */ + [key: string]: any; +} + +/** + * QueryOperationOrder represents an ordering of the results of a given query operation. + * + * These objects can represent orderings of both quad and bindings streams, + * respectively identified by quad term names and variables. + */ +export interface QueryOperationOrder { + cost: QueryOperationCost; + terms: { term: T, direction: 'asc' | 'desc' }[]; +} + +/** + * QueryResultCardinality represents the number of results, which can either be an estimate or exact value. + */ +export interface QueryResultCardinality { + /** + * indicates the type of counting that was done, and MUST either be "estimate" or "exact". + */ + type: 'estimate' | 'exact'; + + /** + * Indicates an estimated of the number of results in the stream if type = "estimate", + * or the exact number of results in the stream if type = "exact". + */ + value: number; +} + +/** + * BaseMetadataQuery is helper interface that provides a metadata callback. + */ +interface BaseMetadataQuery { + /** + * Asynchronously return metadata of the current result. + */ + metadata>(opts?: M): Promise>; +} + +export type AllMetadataSupport = CardinalityMetadataSupport & OrderMetadataSupport & AvailableOrdersMetadataSupport; +export type CardinalityMetadataSupport = { cardinality: true }; +export type OrderMetadataSupport = { order: true }; +export type AvailableOrdersMetadataSupport = { availableOrders: true }; + +export type MetadataOpts = + (SupportedMetadataType extends CardinalityMetadataSupport ? CardinalityMetadataOpts : unknown) | + (SupportedMetadataType extends OrderMetadataSupport ? OrderMetadataOpts : unknown) | + (SupportedMetadataType extends AvailableOrdersMetadataSupport ? AvailableOrdersMetadataOpts : unknown); +export interface CardinalityMetadataOpts { cardinality: 'estimate' | 'exact'; } +export interface OrderMetadataOpts { order: true; } +export interface AvailableOrdersMetadataOpts { availableOrders: true; } + +export type ConditionalMetadataType = AdditionalMetadataType + & (M extends CardinalityMetadataOpts ? { cardinality: QueryResultCardinality } : Record) + & (M extends OrderMetadataOpts ? { order: QueryOperationOrder['terms'] } : Record) + & (M extends AvailableOrdersMetadataOpts ? { availableOrders: QueryOperationOrder[] } : Record); + +/** + * Options that can be passed when executing a query. + */ +export interface QueryExecuteOptions { + /** + * The required order for the result stream. + */ + order?: QueryOperationOrder; + + /** + * Custom properties + */ + [key: string]: any; +} + +/** + * Generic interface that defines query objects following the Future pattern. + */ +export interface BaseQuery { + /** + * Identifier for the type of result of tis query. + */ + resultType: string; + + /** + * Returns either a stream containing all the items that match the given query, + * a boolean or void depending on the semantics of the given query. + */ + execute(opts?: any): Promise | boolean | void>; +} + +/** + * Query object that returns bindings. + */ +export interface QueryBindings extends BaseQuery, BaseMetadataQuery { + resultType: 'bindings'; + execute(opts?: QueryExecuteOptions): Promise>; +} + +/** + * Query object that returns quads. + */ +export interface QueryQuads extends BaseQuery, BaseMetadataQuery { + resultType: 'quads'; + execute(opts?: QueryExecuteOptions): Promise>; +} + +/** + * Query object that returns a boolean. + */ +export interface QueryBoolean extends BaseQuery { + resultType: 'boolean'; + execute(): Promise; +} + +/** + * Query object that returns void. + */ +export interface QueryVoid extends BaseQuery { + resultType: 'void'; + execute(): Promise; +} + +/** + * Union type for the different query types. + */ +export type Query = QueryBindings | QueryBoolean | QueryQuads | QueryVoid; + +/** + * Bindings represents the mapping of variables to RDF values using an immutable Map-like representation. + * This means that methods such as `set` and `delete` do not modify this instance, + * but they return a new Bindings instance that contains the modification. + * + * Bindings instances are created using a BindingsFactory. + * + * The internal order of variable-value entries is undefined. + */ +export interface Bindings extends Iterable<[RDF.Variable, RDF.Term]> { + type: 'bindings'; + /** + * Check if a binding exist for the given variable. + * @param key A variable + */ + has: (key: RDF.Variable) => boolean; + /** + * Obtain the binding value for the given variable. + * @param key A variable + */ + get: (key: RDF.Variable) => RDF.Term | undefined; + /** + * Create a new Bindings object by adding the given variable and value mapping. + * + * If the variable already exists in the binding, then the existing mapping is overwritten. + * + * @param key The variable key. + * @param value The value. + */ + set: (key: RDF.Variable, value: RDF.Term) => Bindings; + /** + * Create a new Bindings object by removing the given variable. + * + * If the variable does not exist in the binding, a copy of the Bindings object is returned. + * + * @param key The variable key. + */ + delete: (key: RDF.Variable) => Bindings; + /** + * Obtain all variables for which mappings exist. + */ + keys: () => Iterable; + /** + * Obtain all values that are mapped to. + */ + values: () => Iterable; + /** + * Iterate over all variable-value pairs. + * @param fn A callback that is called for each variable-value pair + * with value as first argument, and variable as second argument. + */ + forEach: (fn: (value: RDF.Term, key: RDF.Variable) => any) => void; + /** + * The number of variable-value pairs. + */ + size: number; + /** + * Iterator over all variable-value pairs. + */ + [Symbol.iterator]: () => Iterator<[RDF.Variable, RDF.Term]>; + /** + * Check if all entries contained in this Bindings object are equal to all entries in the other Bindings object. + * @param other A Bindings object. + */ + equals(other: Bindings | null | undefined): boolean; + /** + * Create a new Bindings object by filtering entries using a callback. + * @param fn A callback that is applied on each entry. + * Returning true indicates that this entry must be contained in the resulting Bindings object. + */ + filter: (fn: (value: RDF.Term, key: RDF.Variable) => boolean) => Bindings; + /** + * Create a new Bindings object by mapping entries using a callback. + * @param fn A callback that is applied on each entry, in which the original value is replaced by the returned value. + */ + map: (fn: (value: RDF.Term, key: RDF.Variable) => RDF.Term) => Bindings; + /** + * Merge this bindings with another. + * + * If a merge conflict occurs (this and other have an equal variable with unequal value), + * then undefined is returned. + * + * @param other A Bindings object. + */ + merge: (other: Bindings) => Bindings | undefined; + /** + * Merge this bindings with another, where merge conflicts can be resolved using a callback function. + * @param merger A function that is invoked when a merge conflict occurs, + * for which the returned value is considered the merged value. + * @param other A Bindings object. + */ + mergeWith: ( + merger: (self: RDF.Term, other: RDF.Term, key: RDF.Variable) => RDF.Term, + other: Bindings, + ) => Bindings; +} + +/** + * BindingsFactory can create new instances of Bindings. + */ +export interface BindingsFactory { + /** + * Create a new Bindings object from the given variable-value entries. + * @param entries An array of entries, where each entry is a tuple containing a variable and a term. + */ + bindings: (entries?: [RDF.Variable, RDF.Term][]) => Bindings; + + /** + * Create a copy of the given bindings object using this factory. + * @param bindings A Bindings object. + */ + fromBindings: (bindings: Bindings) => Bindings; +} diff --git a/query/filterable.d.ts b/query/filterable.d.ts new file mode 100644 index 0000000..f092dec --- /dev/null +++ b/query/filterable.d.ts @@ -0,0 +1,123 @@ +/* Query Interfaces - Filterable */ +/* https://rdf.js.org/query-spec/ */ + +import * as RDF from '../data-model'; +import { QueryQuads } from './common'; + +/** + * Expression is an abstract interface that represents a generic expression over a stream of quads. + */ +export interface Expression { + /** + * Value that identifies the concrete interface of the expression, + * since the Expression itself is not directly instantiated. + * Possible values include "operator" and "term". + */ + expressionType: string; +} + +/** + * An OperatorExpression is represents an expression that applies a given operator on given sub-expressions. + * + * The WebIDL definition of the Filterable spec contains a list of supported + * operators: https://rdf.js.org/query-spec/#expression-operators + */ +export interface OperatorExpression extends Expression { + + /** + * Contains the constant "operator". + */ + expressionType: 'operator'; + + /** + * Value that identifies an operator. Possible values can be found in the list of operators. + */ + operator: string; + + /** + * Array of Expression's on to which the given operator applies. + * The length of this array depends on the operator. + */ + args: Expression[]; +} + +/** + * A TermExpression is an expression that contains a Term. + */ +export interface TermExpression { + + /** + * The constant "term". + */ + expressionType: 'term'; + + /** + * an RDF Term. + */ + term: RDF.Term; +} + +/** + * ExpressionFactory enables expressions to be created in an idiomatic manner. + */ +export interface ExpressionFactory { + + /** + * Creates a new OperatorExpression instance for the given operator and array of arguments. + */ + operatorExpression(operator: string, args: Expression[]): OperatorExpression; + + /** + * Creates a new TermExpression instance for the given term. + */ + termExpression(term: RDF.Term): TermExpression; +} + +/** + * A FilterableSource is an object that emits quads based on a quad pattern and filter expression. + * + * The emitted quads can be directly contained in this object, or they can be generated on the fly. + * + * FilterableSource is not necessarily an extension of the RDF/JS Source + * interface, but implementers MAY decide to implement both at the same time. + * + * matchExpression() Returns a QueryQuads future that can produce a quad + * stream that contains all quads matching the quad pattern and the expression. + * + * When a Term parameter is defined, and is a NamedNode, Literal or BlankNode, + * it must match each produced quad, according to the Quad.equals semantics. + * When a Term parameter is a Variable, or it is undefined, it acts as a + * wildcard, and can match with any Term. + * + * NOTES: + * - When matching with graph set to undefined or null it MUST match all the + * graphs (sometimes called the union graph). To match only the default graph + * set graph to a DefaultGraph. + * - When an Expression parameter is defined, the complete quad stream is + * filtered according to this expression. When it is undefined, no filter is + * applied. + * + * If parameters of type Variable with an equal variable name are in place, + * then the corresponding quad components in the resulting quad stream MUST be + * equal. + * Expression's MAY contain Variable Term's. If their variable names are equal + * to Variable's in the given quad pattern, then the Expression MUST be + * instantiated for each variable's binding in the resulting quad stream when + * applying the Expression filter. + */ +export interface FilterableSource { + /** + * May reject given an unsupported expression. + */ + matchExpression( + subject?: RDF.Term, + predicate?: RDF.Term, + obj?: RDF.Term, + graph?: RDF.Term, + expression?: Expression, + opts?: { + length?: number; + start?: number; + }, + ): Promise>; +} diff --git a/query/queryable.d.ts b/query/queryable.d.ts new file mode 100644 index 0000000..7f32174 --- /dev/null +++ b/query/queryable.d.ts @@ -0,0 +1,146 @@ +/* Query Interfaces - Queryable */ +/* https://rdf.js.org/query-spec/ */ + +import * as RDF from '../data-model'; +import { Bindings, Query, ResultStream } from './common'; + +/** + * Context objects provide a way to pass additional bits information to the query engine when executing a query. + */ +export interface QueryContext { + /** + * An array of data sources the query engine must use. + */ + sources?: [SourceType, ...SourceType[]]; + /** + * The date that should be used by SPARQL operations such as NOW(). + */ + queryTimestamp?: Date; + /** + * Other options + */ + [key: string]: any; +} + +/** + * Context object in the case the passed query is a string. + */ +export interface QueryStringContext extends QueryContext { + /** + * The format in which the query string is defined. + * Defaults to { language: 'sparql', version: '1.1' } + */ + queryFormat?: QueryFormat; + /** + * The baseIRI for parsing the query. + */ + baseIRI?: string; +} + +/** + * Context object in the case the passed query is an algebra object. + */ +export type QueryAlgebraContext = QueryContext; + +/** + * Represents a specific query format + */ +export interface QueryFormat { + /** + * The query language, e.g. 'sparql'. + */ + language: string; + /** + * The version of the query language, e.g. '1.1'. + */ + version: string; + /** + * An optional array of extensions on the query language. + * The representation of these extensions is undefined. + */ + extensions?: string[]; +} + +/** + * Placeholder to represent SPARQL Algebra trees. + * Algebra typings are TBD. Reference implementations include: + * - https://www.npmjs.com/package/sparqlalgebrajs + */ +export type Algebra = any; + +/** + * Generic query engine interfaces. + * It allow engines to return any type of result object for any type of query. + * @param QueryFormatTypesAvailable The format of the query, either string or algebra object. + * @param SourceType The allowed sources over which queries can be executed. + * @param SupportedMetadataType The allowed metadata types. + * @param QueryType The allowed query types. + * @param QueryStringContextType Type of the string-based query context. + * @param QueryAlgebraContextType Type of the algebra-based query context. + */ +export interface Queryable< + QueryFormatTypesAvailable extends string | Algebra, + SourceType, + SupportedMetadataType, + QueryType extends Query, + QueryStringContextType extends QueryStringContext, + QueryAlgebraContextType extends QueryAlgebraContext, +> { + /** + * Initiate a given query. + * + * This will produce a future to a query result, which has to be executed to obtain the query results. + * + * This can reject given an unsupported or invalid query. + * + * @see Query + */ + query( + query: QueryFormatType, + context?: QueryFormatType extends string ? QueryStringContextType : QueryAlgebraContextType, + ): Promise; +} + +/** + * SPARQL-constrainted query interface. + * + * This interface guarantees that result objects are of the expected type as defined by the SPARQL spec. + */ +export type SparqlQueryable< + QueryFormatTypesAvailable extends string | Algebra, + SourceType, + QueryStringContextType extends QueryStringContext, + QueryAlgebraContextType extends QueryAlgebraContext, + SupportedResultType, +> = unknown + & (SupportedResultType extends BindingsResultSupport ? { + queryBindings( + query: QueryFormatType, + context?: QueryFormatType extends string ? QueryStringContextType : QueryAlgebraContextType, + ): Promise>; +} : unknown) + & (SupportedResultType extends BooleanResultSupport ? { + queryBoolean( + query: QueryFormatType, + context?: QueryFormatType extends string ? QueryStringContextType : QueryAlgebraContextType, + ): Promise; +} : unknown) + & (SupportedResultType extends QuadsResultSupport ? { + queryQuads( + query: QueryFormatType, + context?: QueryFormatType extends string ? QueryStringContextType : QueryAlgebraContextType, + ): Promise>; +} : unknown) + & (SupportedResultType extends VoidResultSupport ? { + queryVoid( + query: QueryFormatType, + context?: QueryFormatType extends string ? QueryStringContextType : QueryAlgebraContextType, + ): Promise; +} : unknown) + ; + +export type SparqlResultSupport = BindingsResultSupport & VoidResultSupport & QuadsResultSupport & BooleanResultSupport; +export type BindingsResultSupport = { bindings: true }; +export type VoidResultSupport = { void: true }; +export type QuadsResultSupport = { quads: true }; +export type BooleanResultSupport = { boolean: true }; diff --git a/stream.d.ts b/stream.d.ts index bc78df6..2217114 100644 --- a/stream.d.ts +++ b/stream.d.ts @@ -6,6 +6,7 @@ import { EventEmitter } from "events"; import { BaseQuad, Quad, Term } from './data-model'; +// TODO: merge this with ResultStream upon the next major change /** * A quad stream. * This stream is only readable, not writable.