diff --git a/cayley.ts b/cayley.ts index 6dbce33..d4e9a44 100644 --- a/cayley.ts +++ b/cayley.ts @@ -2,14 +2,13 @@ import NamedNode = require("@rdfjs/data-model/lib/named-node"); import BlankNode = require("@rdfjs/data-model/lib/blank-node"); import Literal = require("@rdfjs/data-model/lib/literal"); import * as N3 from "./n3"; -import { Graph } from "./query-builder"; +import Path, { Identifier } from "./path"; import "isomorphic-fetch"; -export { NamedNode, BlankNode, Literal, Graph }; - -type Identifier = NamedNode | BlankNode; +export { NamedNode, BlankNode, Literal, Path, Identifier }; export enum Language { + LinkedQL = "linkedql", Gizmo = "gizmo", GraphQL = "graphql", MQL = "mql" @@ -27,17 +26,9 @@ export enum QueryResultFormat { } export default class Client { - /** This is the only special object in the environment, generates the query objects. - * Under the hood, they're simple objects that get compiled to a Go iterator tree when executed. */ - graph: Graph; - /** Alias of graph. This is the only special object in the environment, generates the query objects. - Under the hood, they're simple objects that get compiled to a Go iterator tree when executed. */ - g: Graph; url: string; constructor(url: string = "http://localhost:64210") { this.url = url; - this.graph = new Graph(this); - this.g = this.graph; } /** @@ -124,10 +115,10 @@ export default class Client { * @param limit Globally limit the number of results */ query( - query: string, - language: Language = Language.Gizmo, - format: QueryResultFormat = QueryResultFormat.JsonLD, - limit: number = 100 + query: string | Path, + limit: number = 100, + language: Language = Language.LinkedQL, + format: QueryResultFormat = QueryResultFormat.JsonLD ): Promise { return fetch( `${this.url}/api/v2/query?${new URLSearchParams({ @@ -139,7 +130,7 @@ export default class Client { headers: { Accept: format }, - body: query + body: String(query) } ); } diff --git a/generate/generate.ts b/generate/generate.ts new file mode 100644 index 0000000..c046573 --- /dev/null +++ b/generate/generate.ts @@ -0,0 +1,222 @@ +import path = require("path"); +import fs = require("fs"); +import ts = require("typescript"); +import prettier = require("prettier"); +import * as schema from "./schema.json"; + +const headerFile = path.join(path.dirname(__filename), "header.ts"); +const pathFile = "path.ts"; +const code = fs.readFileSync(headerFile, "utf-8"); + +const sourceFile = ts.createSourceFile( + pathFile, + code, + ts.ScriptTarget.Latest, + true +); + +const pathClass = sourceFile.statements[sourceFile.statements.length - 1]; +if ( + !(ts.isClassDeclaration(pathClass) && pathClass.name.escapedText === "Path") +) { + throw new Error(`Unexpected statement: ${pathClass}`); +} + +type SchemaObject = { + "@id": string; +}; + +type Restriction = SchemaObject & { + "@type": "owl:Restriction"; + "owl:cardinality"?: number; + "owl:maxCardinality"?: number; + "owl:onProperty"?: SchemaObject; +}; + +type BaseStep = SchemaObject & { + "@type": "rdfs:Class"; + "rdfs:subClassOf": Array; + "rdfs:comment": string; +}; + +type BaseProperty = SchemaObject & { + "@type": "owl:ObjectProperty" | "owl:DatatypeProperty"; + /** @todo this is invalid domain should receive { @id: string } */ + "rdfs:domain": + | string + | { + "@id": string; + "@type": "owl:Class"; + "owl:unionOf": { + "@id": string; + "@list": BaseStep[]; + }; + }; + /** @todo this is invalid range should receive { @id: string } */ + "rdfs:range": string; +}; + +const rangeToType = (range: string) => { + const rangeID = range; + if (rangeID === "linkedql:PathStep") { + return ts.createTypeReferenceNode("Path", []); + } + if (rangeID == "xsd:string") { + return ts.createKeywordTypeNode(ts.SyntaxKind.StringKeyword); + } + if (rangeID == "xsd:int" || rangeID == "xsd:float") { + return ts.createKeywordTypeNode(ts.SyntaxKind.NumberKeyword); + } + if (rangeID == "xsd:boolean") { + return ts.createKeywordTypeNode(ts.SyntaxKind.BooleanKeyword); + } + if (rangeID == "linkedql:Operator") { + return ts.createTypeReferenceNode("Operator", []); + } + if (rangeID == "rdfs:Resource") { + return ts.createTypeReferenceNode("Identifier", []); + } + throw Error(`Unexpected range: ${range}`); +}; + +function createMethodFromStep( + step: BaseStep, + pathClassName: ts.Identifier, + properties: BaseProperty[] +): ts.MethodDeclaration { + const stepTypeID = step["@id"]; + const stepTypeName = stepTypeID.replace("linkedql:", ""); + const stepName = stepTypeName[0].toLowerCase() + stepTypeName.slice(1); + const restrictions: Restriction[] = step["rdfs:subClassOf"].filter( + (subClass): subClass is Restriction => + subClass["@type"] === "owl:Restriction" + ); + const stepProperties = properties.filter(property => { + if (property["@id"] === "linkedql:from") { + return false; + } + const domain = property["rdfs:domain"]; + if (typeof domain === "string") { + return domain === stepTypeID; + } + return domain["owl:unionOf"]["@list"].some( + step => step["@id"] === stepTypeID + ); + }); + const parameterNames = stepProperties.map(property => + property["@id"].replace("linkedql:", "") + ); + const parameters = parameterNames.map((name, i) => { + const property = stepProperties[i]; + const propertyRestrictions = restrictions.filter(restriction => { + return restriction["owl:onProperty"]["@id"] === property["@id"]; + }); + const cardinality = + propertyRestrictions[0] && propertyRestrictions[0]["owl:cardinality"]; + const maxCardinality = + propertyRestrictions[0] && propertyRestrictions[0]["owl:maxCardinality"]; + const baseType = rangeToType(property["rdfs:range"]); + let type: ts.TypeNode = baseType; + if (maxCardinality === 1) { + type = ts.createUnionTypeNode([ + type, + ts.createKeywordTypeNode(ts.SyntaxKind.NullKeyword) + ]); + } else if (cardinality !== 1) { + type = ts.createArrayTypeNode(type); + } + return ts.createParameter( + [], + [], + undefined, + name, + undefined, + type, + undefined + ); + }); + const stepPropertyAssignments = [ + ts.createPropertyAssignment( + ts.createStringLiteral("@type"), + ts.createStringLiteral(stepTypeID) + ), + ...stepProperties.map((property, i) => { + const propertyName = parameterNames[i]; + return ts.createPropertyAssignment( + ts.createStringLiteral(property["@id"]), + ts.createIdentifier(propertyName) + ); + }) + ]; + return ts.createMethod( + [], + [], + undefined, + stepName, + undefined, + [], + parameters, + ts.createTypeReferenceNode(pathClassName, []), + ts.createBlock([ + ts.createStatement( + ts.createCall( + ts.createPropertyAccess(ts.createThis(), "addStep"), + [], + [ts.createObjectLiteral(stepPropertyAssignments)] + ) + ), + ts.createReturn(ts.createThis()) + ]) + ); +} + +// @ts-ignore +const steps: BaseStep[] = schema.filter( + object => object["@type"] === "rdfs:Class" +); + +// @ts-ignore +const properties: BaseProperty[] = schema.filter( + object => + object["@type"] === "owl:ObjectProperty" || + object["@type"] === "owl:DatatypeProperty" +); + +const newMembers = [ + ...pathClass.members, + ...steps.map((step: BaseStep) => + createMethodFromStep(step, pathClass.name, properties) + ) +]; +const newPathClass = ts.createClassDeclaration( + pathClass.decorators, + pathClass.modifiers, + pathClass.name, + pathClass.typeParameters, + pathClass.heritageClauses, + newMembers +); + +const newSourceFile = ts.createSourceFile( + sourceFile.fileName, + sourceFile.text, + ts.ScriptTarget.Latest, + /*setParentNodes*/ false, + ts.ScriptKind.TS +); +const printer = ts.createPrinter({ + newLine: ts.NewLineKind.LineFeed +}); + +const result = printer.printList( + ts.ListFormat.SourceFileStatements, + ts.createNodeArray( + [ + ...sourceFile.statements.slice(0, sourceFile.statements.length - 1), + newPathClass + ], + false + ), + newSourceFile +); +fs.writeFileSync(pathFile, prettier.format(result, { parser: "typescript" })); diff --git a/generate/header.ts b/generate/header.ts new file mode 100644 index 0000000..fb55192 --- /dev/null +++ b/generate/header.ts @@ -0,0 +1,24 @@ +import NamedNode = require("@rdfjs/data-model/lib/named-node"); +import BlankNode = require("@rdfjs/data-model/lib/blank-node"); +// @ts-ignore +import { QueryLanguage, QueryContentType } from "./client"; + +class Operator {} + +type Step = Object; + +export type Identifier = NamedNode | BlankNode; + +export default class Path { + private cursor: Step | null; + private addStep(step: Step): void { + const previousCursor = this.cursor; + this.cursor = step; + if (previousCursor) { + this.cursor = { ...this.cursor, "linkedql:from": previousCursor }; + } + } + toString(): string { + return JSON.stringify(this.cursor); + } +} diff --git a/generate/schema.json b/generate/schema.json new file mode 100644 index 0000000..691ab69 --- /dev/null +++ b/generate/schema.json @@ -0,0 +1,1012 @@ +[ + { + "@id": "linkedql:Has", + "@type": "rdfs:Class", + "rdfs:comment": "Filter all paths which are, at this point, on the subject for the given predicate and object, but do not follow the path, merely filter the possible paths. Usually useful for starting with all nodes, or limiting to a subset depending on some predicate/value pair.", + "rdfs:subClassOf": [ + { + "@id": "linkedql:PathStep" + }, + { + "@id": "_:n3281282586393929482", + "@type": "owl:Restriction", + "owl:cardinality": 1, + "owl:onProperty": { + "@id": "linkedql:from" + } + }, + { + "@id": "_:n5767825496682386093", + "@type": "owl:Restriction", + "owl:cardinality": 1, + "owl:onProperty": { + "@id": "linkedql:via" + } + } + ] + }, + { + "@id": "linkedql:Union", + "@type": "rdfs:Class", + "rdfs:comment": "Return the combined paths of the two queries. Notice that it's per-path, not per-node. Once again, if multiple paths reach the same destination, they might have had different ways of getting there (and different tags).", + "rdfs:subClassOf": [ + { + "@id": "linkedql:PathStep" + }, + { + "@id": "_:n7613140878198455968", + "@type": "owl:Restriction", + "owl:cardinality": 1, + "owl:onProperty": { + "@id": "linkedql:from" + } + } + ] + }, + { + "@id": "linkedql:Select", + "@type": "rdfs:Class", + "rdfs:comment": "Select returns flat records of tags matched in the query", + "rdfs:subClassOf": [ + { + "@id": "linkedql:PathStep" + }, + { + "@id": "_:n7528696665548517577", + "@type": "owl:Restriction", + "owl:cardinality": 1, + "owl:onProperty": { + "@id": "linkedql:from" + } + } + ] + }, + { + "@id": "linkedql:Is", + "@type": "rdfs:Class", + "rdfs:comment": "Is resolves to all the values resolved by the from step which are included in provided values.", + "rdfs:subClassOf": [ + { + "@id": "linkedql:PathStep" + }, + { + "@id": "_:n748581873419640785", + "@type": "owl:Restriction", + "owl:cardinality": 1, + "owl:onProperty": { + "@id": "linkedql:from" + } + } + ] + }, + { + "@id": "linkedql:Skip", + "@type": "rdfs:Class", + "rdfs:comment": "Skip a number of nodes for current path.", + "rdfs:subClassOf": [ + { + "@id": "linkedql:PathStep" + }, + { + "@id": "_:n1756700804757103070", + "@type": "owl:Restriction", + "owl:cardinality": 1, + "owl:onProperty": { + "@id": "linkedql:from" + } + }, + { + "@id": "_:n8802552163656569641", + "@type": "owl:Restriction", + "owl:cardinality": 1, + "owl:onProperty": { + "@id": "linkedql:offset" + } + } + ] + }, + { + "@id": "linkedql:HasReverse", + "@type": "rdfs:Class", + "rdfs:comment": "The same as Has, but sets constraint in reverse direction.", + "rdfs:subClassOf": [ + { + "@id": "linkedql:PathStep" + }, + { + "@id": "_:n8250308532637761452", + "@type": "owl:Restriction", + "owl:cardinality": 1, + "owl:onProperty": { + "@id": "linkedql:from" + } + }, + { + "@id": "_:n1423144716455332136", + "@type": "owl:Restriction", + "owl:cardinality": 1, + "owl:onProperty": { + "@id": "linkedql:via" + } + } + ] + }, + { + "@id": "linkedql:In", + "@type": "rdfs:Class", + "rdfs:comment": "Alias for ViewReverse", + "rdfs:subClassOf": [ + { + "@id": "linkedql:PathStep" + }, + { + "@id": "_:n6083711021014342408", + "@type": "owl:Restriction", + "owl:cardinality": 1, + "owl:onProperty": { + "@id": "linkedql:from" + } + }, + { + "@id": "_:n406026357180598357", + "@type": "owl:Restriction", + "owl:cardinality": 1, + "owl:onProperty": { + "@id": "linkedql:via" + } + }, + { + "@id": "_:n913971724280450346", + "@type": "owl:Restriction", + "owl:cardinality": 1, + "owl:onProperty": { + "@id": "linkedql:via" + } + } + ] + }, + { + "@id": "linkedql:ReversePropertyNames", + "@type": "rdfs:Class", + "rdfs:comment": "Get the list of predicates that are pointing in to a node.", + "rdfs:subClassOf": [ + { + "@id": "linkedql:PathStep" + }, + { + "@id": "_:n8921697013008895101", + "@type": "owl:Restriction", + "owl:cardinality": 1, + "owl:onProperty": { + "@id": "linkedql:from" + } + } + ] + }, + { + "@id": "linkedql:Labels", + "@type": "rdfs:Class", + "rdfs:comment": "Get the list of inbound and outbound quad labels", + "rdfs:subClassOf": [ + { + "@id": "linkedql:PathStep" + }, + { + "@id": "_:n2437805078815776687", + "@type": "owl:Restriction", + "owl:cardinality": 1, + "owl:onProperty": { + "@id": "linkedql:from" + } + } + ] + }, + { + "@id": "linkedql:Properties", + "@type": "rdfs:Class", + "rdfs:comment": "Adds tags for all properties of the current entity", + "rdfs:subClassOf": [ + { + "@id": "linkedql:PathStep" + }, + { + "@id": "_:n7235691634148515785", + "@type": "owl:Restriction", + "owl:cardinality": 1, + "owl:onProperty": { + "@id": "linkedql:from" + } + } + ] + }, + { + "@id": "Placeholder", + "@type": "rdfs:Class", + "rdfs:comment": "Placeholder is like Vertex but resolves to the values in the context it is placed in. It should only be used where a PathStep is expected and can't be resolved on its own.", + "rdfs:subClassOf": [ + { + "@id": "linkedql:PathStep" + } + ] + }, + { + "@id": "linkedql:Back", + "@type": "rdfs:Class", + "rdfs:comment": "Back resolves to the values of the previous the step or the values assigned to name in a former step.", + "rdfs:subClassOf": [ + { + "@id": "linkedql:PathStep" + }, + { + "@id": "_:n8836432323201849516", + "@type": "owl:Restriction", + "owl:cardinality": 1, + "owl:onProperty": { + "@id": "linkedql:from" + } + }, + { + "@id": "_:n6295579874799110692", + "@type": "owl:Restriction", + "owl:cardinality": 1, + "owl:onProperty": { + "@id": "linkedql:name" + } + } + ] + }, + { + "@id": "linkedql:View", + "@type": "rdfs:Class", + "rdfs:comment": "View resolves to the values of the given property or properties in via of the current objects. If via is a path it's resolved values will be used as properties.", + "rdfs:subClassOf": [ + { + "@id": "linkedql:PathStep" + }, + { + "@id": "_:n3263482253562069274", + "@type": "owl:Restriction", + "owl:cardinality": 1, + "owl:onProperty": { + "@id": "linkedql:from" + } + }, + { + "@id": "_:n3309902289644245372", + "@type": "owl:Restriction", + "owl:cardinality": 1, + "owl:onProperty": { + "@id": "linkedql:via" + } + } + ] + }, + { + "@id": "linkedql:Filter", + "@type": "rdfs:Class", + "rdfs:comment": "Apply constraints to a set of nodes. Can be used to filter values by range or match strings.", + "rdfs:subClassOf": [ + { + "@id": "linkedql:PathStep" + }, + { + "@id": "_:n2413229582488884726", + "@type": "owl:Restriction", + "owl:cardinality": 1, + "owl:onProperty": { + "@id": "linkedql:from" + } + }, + { + "@id": "_:n351055054558782253", + "@type": "owl:Restriction", + "owl:cardinality": 1, + "owl:onProperty": { + "@id": "linkedql:filter" + } + } + ] + }, + { + "@id": "linkedql:Limit", + "@type": "rdfs:Class", + "rdfs:comment": "Limit a number of nodes for current path.", + "rdfs:subClassOf": [ + { + "@id": "linkedql:PathStep" + }, + { + "@id": "_:n3181809130686958634", + "@type": "owl:Restriction", + "owl:cardinality": 1, + "owl:onProperty": { + "@id": "linkedql:from" + } + }, + { + "@id": "_:n3458502423730344281", + "@type": "owl:Restriction", + "owl:cardinality": 1, + "owl:onProperty": { + "@id": "linkedql:limit" + } + } + ] + }, + { + "@id": "linkedql:Order", + "@type": "rdfs:Class", + "rdfs:comment": "Order sorts the results in ascending order according to the current entity / value", + "rdfs:subClassOf": [ + { + "@id": "linkedql:PathStep" + }, + { + "@id": "_:n568169897973950937", + "@type": "owl:Restriction", + "owl:cardinality": 1, + "owl:onProperty": { + "@id": "linkedql:from" + } + } + ] + }, + { + "@id": "linkedql:Intersect", + "@type": "rdfs:Class", + "rdfs:comment": "Intersect resolves to all the same values resolved by the from step and the provided steps.", + "rdfs:subClassOf": [ + { + "@id": "linkedql:PathStep" + }, + { + "@id": "_:n742694044543440878", + "@type": "owl:Restriction", + "owl:cardinality": 1, + "owl:onProperty": { + "@id": "linkedql:from" + } + } + ] + }, + { + "@id": "linkedql:Difference", + "@type": "rdfs:Class", + "rdfs:comment": "Difference resolves to all the values resolved by the from step different then the values resolved by the provided steps. Caution: it might be slow to execute.", + "rdfs:subClassOf": [ + { + "@id": "linkedql:PathStep" + }, + { + "@id": "_:n3851183820736213523", + "@type": "owl:Restriction", + "owl:cardinality": 1, + "owl:onProperty": { + "@id": "linkedql:from" + } + } + ] + }, + { + "@id": "linkedql:ViewBoth", + "@type": "rdfs:Class", + "rdfs:comment": "ViewBoth is like View but resolves to both the object values and references to the values of the given properties in via. It is the equivalent for the Union of View and ViewReverse of the same property.", + "rdfs:subClassOf": [ + { + "@id": "linkedql:PathStep" + }, + { + "@id": "_:n4206374420556606332", + "@type": "owl:Restriction", + "owl:cardinality": 1, + "owl:onProperty": { + "@id": "linkedql:from" + } + }, + { + "@id": "_:n1911693820425613485", + "@type": "owl:Restriction", + "owl:cardinality": 1, + "owl:onProperty": { + "@id": "linkedql:via" + } + } + ] + }, + { + "@id": "linkedql:Vertex", + "@type": "rdfs:Class", + "rdfs:comment": "Vertex resolves to all the existing objects and primitive values in the graph. If provided with values resolves to a sublist of all the existing values in the graph.", + "rdfs:subClassOf": [ + { + "@id": "linkedql:PathStep" + } + ] + }, + { + "@id": "linkedql:As", + "@type": "rdfs:Class", + "rdfs:comment": "As assigns the resolved values of the from step to a given name. The name can be used with the Select and Documents steps to retreive the values or to return to the values in further steps with the Back step. It resolves to the values of the from step.", + "rdfs:subClassOf": [ + { + "@id": "linkedql:PathStep" + }, + { + "@id": "_:n4318921620817149629", + "@type": "owl:Restriction", + "owl:cardinality": 1, + "owl:onProperty": { + "@id": "linkedql:from" + } + }, + { + "@id": "_:n4739624378311519206", + "@type": "owl:Restriction", + "owl:cardinality": 1, + "owl:onProperty": { + "@id": "linkedql:name" + } + } + ] + }, + { + "@id": "linkedql:ReverseProperties", + "@type": "rdfs:Class", + "rdfs:comment": "Gets all the properties the current entity / value is referenced at", + "rdfs:subClassOf": [ + { + "@id": "linkedql:PathStep" + }, + { + "@id": "_:n957116371706553368", + "@type": "owl:Restriction", + "owl:cardinality": 1, + "owl:onProperty": { + "@id": "linkedql:from" + } + } + ] + }, + { + "@id": "linkedql:Out", + "@type": "rdfs:Class", + "rdfs:comment": "Alias for View", + "rdfs:subClassOf": [ + { + "@id": "linkedql:PathStep" + }, + { + "@id": "_:n7254966467980509775", + "@type": "owl:Restriction", + "owl:cardinality": 1, + "owl:onProperty": { + "@id": "linkedql:from" + } + }, + { + "@id": "_:n9012524456034048164", + "@type": "owl:Restriction", + "owl:cardinality": 1, + "owl:onProperty": { + "@id": "linkedql:via" + } + }, + { + "@id": "_:n8994892294760893711", + "@type": "owl:Restriction", + "owl:cardinality": 1, + "owl:onProperty": { + "@id": "linkedql:via" + } + } + ] + }, + { + "@id": "linkedql:ViewReverse", + "@type": "rdfs:Class", + "rdfs:comment": "The inverse of View. Starting with the nodes in `path` on the object, follow the quads with predicates defined by `predicatePath` to their subjects.", + "rdfs:subClassOf": [ + { + "@id": "linkedql:PathStep" + }, + { + "@id": "_:n6403769817032349786", + "@type": "owl:Restriction", + "owl:cardinality": 1, + "owl:onProperty": { + "@id": "linkedql:from" + } + }, + { + "@id": "_:n6503870082476525068", + "@type": "owl:Restriction", + "owl:cardinality": 1, + "owl:onProperty": { + "@id": "linkedql:via" + } + } + ] + }, + { + "@id": "linkedql:Follow", + "@type": "rdfs:Class", + "rdfs:comment": "The way to use a path prepared with Morphism. Applies the path chain on the morphism object to the current path. Starts as if at the g.M() and follows through the morphism path.", + "rdfs:subClassOf": [ + { + "@id": "linkedql:PathStep" + }, + { + "@id": "_:n800228667234005022", + "@type": "owl:Restriction", + "owl:cardinality": 1, + "owl:onProperty": { + "@id": "linkedql:from" + } + }, + { + "@id": "_:n1955124143226396273", + "@type": "owl:Restriction", + "owl:cardinality": 1, + "owl:onProperty": { + "@id": "linkedql:followed" + } + } + ] + }, + { + "@id": "linkedql:FollowReverse", + "@type": "rdfs:Class", + "rdfs:comment": "The same as follow but follows the chain in the reverse direction. Flips View and ViewReverse where appropriate, the net result being a virtual predicate followed in the reverse direction. Starts at the end of the morphism and follows it backwards (with appropriate flipped directions) to the g.M() location.", + "rdfs:subClassOf": [ + { + "@id": "linkedql:PathStep" + }, + { + "@id": "_:n4156010644263579435", + "@type": "owl:Restriction", + "owl:cardinality": 1, + "owl:onProperty": { + "@id": "linkedql:from" + } + }, + { + "@id": "_:n5258912160534539360", + "@type": "owl:Restriction", + "owl:cardinality": 1, + "owl:onProperty": { + "@id": "linkedql:followed" + } + } + ] + }, + { + "@id": "linkedql:PropertyNames", + "@type": "rdfs:Class", + "rdfs:comment": "Get the list of predicates that are pointing out from a node.", + "rdfs:subClassOf": [ + { + "@id": "linkedql:PathStep" + }, + { + "@id": "_:n5779672541087670915", + "@type": "owl:Restriction", + "owl:cardinality": 1, + "owl:onProperty": { + "@id": "linkedql:from" + } + } + ] + }, + { + "@id": "linkedql:ReversePropertyNamesAs", + "@type": "rdfs:Class", + "rdfs:comment": "Tag the list of predicates that are pointing in to a node.", + "rdfs:subClassOf": [ + { + "@id": "linkedql:PathStep" + }, + { + "@id": "_:n4541004253017523638", + "@type": "owl:Restriction", + "owl:cardinality": 1, + "owl:onProperty": { + "@id": "linkedql:from" + } + }, + { + "@id": "_:n3177372750360501506", + "@type": "owl:Restriction", + "owl:cardinality": 1, + "owl:onProperty": { + "@id": "linkedql:tag" + } + } + ] + }, + { + "@id": "linkedql:PropertyNamesAs", + "@type": "rdfs:Class", + "rdfs:comment": "Tag the list of predicates that are pointing out from a node.", + "rdfs:subClassOf": [ + { + "@id": "linkedql:PathStep" + }, + { + "@id": "_:n1833825190499616474", + "@type": "owl:Restriction", + "owl:cardinality": 1, + "owl:onProperty": { + "@id": "linkedql:from" + } + }, + { + "@id": "_:n5082752009521515069", + "@type": "owl:Restriction", + "owl:cardinality": 1, + "owl:onProperty": { + "@id": "linkedql:tag" + } + } + ] + }, + { + "@id": "linkedql:Unique", + "@type": "rdfs:Class", + "rdfs:comment": "Remove duplicate values from the path.", + "rdfs:subClassOf": [ + { + "@id": "linkedql:PathStep" + }, + { + "@id": "_:n1844255146637842289", + "@type": "owl:Restriction", + "owl:cardinality": 1, + "owl:onProperty": { + "@id": "linkedql:from" + } + } + ] + }, + { + "@id": "linkedql:SelectFirst", + "@type": "rdfs:Class", + "rdfs:comment": "Like Select but only returns the first result", + "rdfs:subClassOf": [ + { + "@id": "linkedql:PathStep" + }, + { + "@id": "_:n4961457092690359420", + "@type": "owl:Restriction", + "owl:cardinality": 1, + "owl:onProperty": { + "@id": "linkedql:from" + } + } + ] + }, + { + "@id": "linkedql:Count", + "@type": "rdfs:Class", + "rdfs:comment": "Count resolves to the number of the resolved values of the from step", + "rdfs:subClassOf": [ + { + "@id": "linkedql:PathStep" + }, + { + "@id": "_:n5910920919397543284", + "@type": "owl:Restriction", + "owl:cardinality": 1, + "owl:onProperty": { + "@id": "linkedql:from" + } + } + ] + }, + { + "@id": "linkedql:filter", + "@type": "owl:ObjectProperty", + "rdfs:domain": "linkedql:Filter", + "rdfs:range": "linkedql:Operator" + }, + { + "@id": "linkedql:limit", + "@type": "owl:DatatypeProperty", + "rdfs:domain": "linkedql:Limit", + "rdfs:range": "xsd:int" + }, + { + "@id": "linkedql:from", + "@type": "owl:ObjectProperty", + "rdfs:domain": { + "@id": "_:n3095208771940409930", + "@type": "owl:Class", + "owl:unionOf": { + "@id": "_:n2612155258665861441", + "@list": [ + { + "@id": "linkedql:Union" + }, + { + "@id": "linkedql:View" + }, + { + "@id": "linkedql:ReverseProperties" + }, + { + "@id": "linkedql:Properties" + }, + { + "@id": "linkedql:Out" + }, + { + "@id": "linkedql:Count" + }, + { + "@id": "linkedql:Select" + }, + { + "@id": "linkedql:Is" + }, + { + "@id": "linkedql:Skip" + }, + { + "@id": "linkedql:Has" + }, + { + "@id": "linkedql:FollowReverse" + }, + { + "@id": "linkedql:PropertyNamesAs" + }, + { + "@id": "linkedql:SelectFirst" + }, + { + "@id": "linkedql:Back" + }, + { + "@id": "linkedql:Difference" + }, + { + "@id": "linkedql:PropertyNames" + }, + { + "@id": "linkedql:Unique" + }, + { + "@id": "linkedql:HasReverse" + }, + { + "@id": "linkedql:In" + }, + { + "@id": "linkedql:ReversePropertyNamesAs" + }, + { + "@id": "linkedql:ViewBoth" + }, + { + "@id": "linkedql:Filter" + }, + { + "@id": "linkedql:Limit" + }, + { + "@id": "linkedql:Order" + }, + { + "@id": "linkedql:Follow" + }, + { + "@id": "linkedql:ReversePropertyNames" + }, + { + "@id": "linkedql:Intersect" + }, + { + "@id": "linkedql:ViewReverse" + }, + { + "@id": "linkedql:Labels" + }, + { + "@id": "linkedql:As" + } + ] + } + }, + "rdfs:range": "linkedql:PathStep" + }, + { + "@id": "linkedql:via", + "@type": "owl:ObjectProperty", + "rdfs:domain": { + "@id": "_:n8326157831843712131", + "@type": "owl:Class", + "owl:unionOf": { + "@id": "_:n8116261452075740094", + "@list": [ + { + "@id": "linkedql:HasReverse" + }, + { + "@id": "linkedql:In" + }, + { + "@id": "linkedql:View" + }, + { + "@id": "linkedql:ViewBoth" + }, + { + "@id": "linkedql:Out" + }, + { + "@id": "linkedql:ViewReverse" + }, + { + "@id": "linkedql:Has" + } + ] + } + }, + "rdfs:range": "linkedql:PathStep" + }, + { + "@id": "linkedql:steps", + "@type": "owl:ObjectProperty", + "rdfs:domain": { + "@id": "_:n2959933589732803374", + "@type": "owl:Class", + "owl:unionOf": { + "@id": "_:n3676022118864270616", + "@list": [ + { + "@id": "linkedql:Union" + }, + { + "@id": "linkedql:Intersect" + }, + { + "@id": "linkedql:Difference" + } + ] + } + }, + "rdfs:range": "linkedql:PathStep" + }, + { + "@id": "linkedql:tags", + "@type": "owl:ObjectProperty", + "rdfs:domain": { + "@id": "_:n4828850549405307926", + "@type": "owl:Class", + "owl:unionOf": { + "@id": "_:n6833669161190995312", + "@list": [ + { + "@id": "linkedql:Select" + }, + { + "@id": "linkedql:SelectFirst" + } + ] + } + }, + "rdfs:range": "xsd:string" + }, + { + "@id": "linkedql:names", + "@type": "owl:ObjectProperty", + "rdfs:domain": { + "@id": "_:n2436986831324928111", + "@type": "owl:Class", + "owl:unionOf": { + "@id": "_:n3476415256053746299", + "@list": [ + { + "@id": "linkedql:ReverseProperties" + }, + { + "@id": "linkedql:Properties" + } + ] + } + }, + "rdfs:range": "xsd:string" + }, + { + "@id": "linkedql:name", + "@type": "owl:DatatypeProperty", + "rdfs:domain": { + "@id": "_:n7861444275797553784", + "@type": "owl:Class", + "owl:unionOf": { + "@id": "_:n2752042139194774327", + "@list": [ + { + "@id": "linkedql:Back" + }, + { + "@id": "linkedql:As" + } + ] + } + }, + "rdfs:range": "xsd:string" + }, + { + "@id": "linkedql:followed", + "@type": "owl:ObjectProperty", + "rdfs:domain": { + "@id": "_:n7313884863834630534", + "@type": "owl:Class", + "owl:unionOf": { + "@id": "_:n6339048795730443522", + "@list": [ + { + "@id": "linkedql:Follow" + }, + { + "@id": "linkedql:FollowReverse" + } + ] + } + }, + "rdfs:range": "linkedql:PathStep" + }, + { + "@id": "linkedql:tag", + "@type": "owl:DatatypeProperty", + "rdfs:domain": { + "@id": "_:n8001739765455100469", + "@type": "owl:Class", + "owl:unionOf": { + "@id": "_:n2445305848985434236", + "@list": [ + { + "@id": "linkedql:ReversePropertyNamesAs" + }, + { + "@id": "linkedql:PropertyNamesAs" + } + ] + } + }, + "rdfs:range": "xsd:string" + }, + { + "@id": "linkedql:values", + "@type": "owl:ObjectProperty", + "rdfs:domain": { + "@id": "_:n7046653586348562073", + "@type": "owl:Class", + "owl:unionOf": { + "@id": "_:n5600151863454519448", + "@list": [ + { + "@id": "linkedql:Is" + }, + { + "@id": "linkedql:HasReverse" + }, + { + "@id": "linkedql:Vertex" + }, + { + "@id": "linkedql:Has" + } + ] + } + }, + "rdfs:range": "rdfs:Resource" + }, + { + "@id": "linkedql:offset", + "@type": "owl:DatatypeProperty", + "rdfs:domain": "linkedql:Skip", + "rdfs:range": "xsd:int" + } +] diff --git a/package-lock.json b/package-lock.json index 14b82c9..3ebb4bf 100644 --- a/package-lock.json +++ b/package-lock.json @@ -181,9 +181,9 @@ "dev": true }, "commander": { - "version": "2.20.0", - "resolved": "https://registry.npmjs.org/commander/-/commander-2.20.0.tgz", - "integrity": "sha512-7j2y+40w61zy6YC2iRNpUe/NwhNyoXrYpHMrSunaMG64nRnaf96zO/KMQR4OyN/UnE5KLyEBnKHd4aG3rskjpQ==", + "version": "2.20.3", + "resolved": "https://registry.npmjs.org/commander/-/commander-2.20.3.tgz", + "integrity": "sha512-GpVkmM8vF2vQUkj2LvZmD35JxeJOLCwJ9cUkugyk2nuhbv3+mJvpLYYt+0+USMxE+oj+ey/lJEnhZw75x/OMcQ==", "dev": true, "optional": true }, @@ -352,9 +352,9 @@ "dev": true }, "handlebars": { - "version": "4.4.3", - "resolved": "https://registry.npmjs.org/handlebars/-/handlebars-4.4.3.tgz", - "integrity": "sha512-B0W4A2U1ww3q7VVthTKfh+epHx+q4mCt6iK+zEAzbMBpWQAwxCeKxEGpj/1oQTpzPXDNSOG7hmG14TsISH50yw==", + "version": "4.5.2", + "resolved": "https://registry.npmjs.org/handlebars/-/handlebars-4.5.2.tgz", + "integrity": "sha512-29Zxv/cynYB7mkT1rVWQnV7mGX6v7H/miQ6dbEpYTKq5eJBN7PsRB+ViYJlcT6JINTSu4dVB9kOqEun78h6Exg==", "dev": true, "requires": { "neo-async": "^2.6.0", @@ -731,6 +731,11 @@ "integrity": "sha512-GSmOT2EbHrINBf9SR7CDELwlJ8AENk3Qn7OikK4nFYAu3Ote2+JYNVvkpAEQm3/TLNEJFD/xZJjzyxg3KBWOzw==", "dev": true }, + "prettier": { + "version": "1.19.1", + "resolved": "https://registry.npmjs.org/prettier/-/prettier-1.19.1.tgz", + "integrity": "sha512-s7PoyDv/II1ObgQunCbB9PdLmUcBZcnWOcxDh7O0N/UwDEsHyqkW+Qh28jW+mVuCdx7gLB0BotYI1Y6uI9iyew==" + }, "progress": { "version": "2.0.3", "resolved": "https://registry.npmjs.org/progress/-/progress-2.0.3.tgz", @@ -901,19 +906,19 @@ } }, "typescript": { - "version": "3.6.4", - "resolved": "https://registry.npmjs.org/typescript/-/typescript-3.6.4.tgz", - "integrity": "sha512-unoCll1+l+YK4i4F8f22TaNVPRHcD9PA3yCuZ8g5e0qGqlVlJ/8FSateOLLSagn+Yg5+ZwuPkL8LFUc0Jcvksg==", + "version": "3.7.2", + "resolved": "https://registry.npmjs.org/typescript/-/typescript-3.7.2.tgz", + "integrity": "sha512-ml7V7JfiN2Xwvcer+XAf2csGO1bPBdRbFCkYBczNZggrBZ9c7G3riSUeJmqEU5uOtXNPMhE3n+R4FA/3YOAWOQ==", "dev": true }, "uglify-js": { - "version": "3.6.1", - "resolved": "https://registry.npmjs.org/uglify-js/-/uglify-js-3.6.1.tgz", - "integrity": "sha512-+dSJLJpXBb6oMHP+Yvw8hUgElz4gLTh82XuX68QiJVTXaE5ibl6buzhNkQdYhBlIhozWOC9ge16wyRmjG4TwVQ==", + "version": "3.6.9", + "resolved": "https://registry.npmjs.org/uglify-js/-/uglify-js-3.6.9.tgz", + "integrity": "sha512-pcnnhaoG6RtrvHJ1dFncAe8Od6Nuy30oaJ82ts6//sGSXOP5UjBMEthiProjXmMNHOfd93sqlkztifFMcb+4yw==", "dev": true, "optional": true, "requires": { - "commander": "2.20.0", + "commander": "~2.20.3", "source-map": "~0.6.1" } }, diff --git a/package.json b/package.json index 4ac9f4a..760670b 100644 --- a/package.json +++ b/package.json @@ -7,7 +7,8 @@ "main": "cayley.js", "types": "cayley.d.ts", "scripts": { - "build": "tsc --lib ES2015 --lib dom", + "generate": "tsc --lib ES2015 --lib dom --resolveJsonModule generate/generate.ts; node generate/generate.js", + "build": "npm run generate; tsc --lib ES2015 --lib dom --resolveJsonModule", "test": "npm run build && mocha", "docs": "typedoc --module commonjs --mode file --excludeExternals --excludePrivate --exclude n3.ts,**/*.spec.ts --toc Format,Language,Client,Graph,Path", "prepack": "npm run build & npm run build -- --declaration" @@ -17,13 +18,14 @@ "dependencies": { "@rdfjs/data-model": "^1.1.2", "@types/node": "^12.7.12", - "isomorphic-fetch": "^2.2.1" + "isomorphic-fetch": "^2.2.1", + "prettier": "^1.19.1" }, "devDependencies": { "@types/mocha": "^5.2.7", "mocha": "^6.2.1", "typedoc": "^0.15.0", - "typescript": "^3.6.4" + "typescript": "^3.7.2" }, "directories": { "doc": "docs", diff --git a/path.ts b/path.ts new file mode 100644 index 0000000..8387b4d --- /dev/null +++ b/path.ts @@ -0,0 +1,165 @@ +import NamedNode = require("@rdfjs/data-model/lib/named-node"); +import BlankNode = require("@rdfjs/data-model/lib/blank-node"); +// @ts-ignore +import { QueryLanguage, QueryContentType } from "./client"; +class Operator {} +type Step = Object; +export type Identifier = NamedNode | BlankNode; +export default class Path { + private cursor: Step | null; + private addStep(step: Step): void { + const previousCursor = this.cursor; + this.cursor = step; + if (previousCursor) { + this.cursor = { ...this.cursor, "linkedql:from": previousCursor }; + } + } + toString(): string { + return JSON.stringify(this.cursor); + } + has(via: Path, values: Identifier[]): Path { + this.addStep({ + "@type": "linkedql:Has", + "linkedql:via": via, + "linkedql:values": values + }); + return this; + } + union(steps: Path[]): Path { + this.addStep({ "@type": "linkedql:Union", "linkedql:steps": steps }); + return this; + } + select(tags: string[]): Path { + this.addStep({ "@type": "linkedql:Select", "linkedql:tags": tags }); + return this; + } + is(values: Identifier[]): Path { + this.addStep({ "@type": "linkedql:Is", "linkedql:values": values }); + return this; + } + skip(offset: number): Path { + this.addStep({ "@type": "linkedql:Skip", "linkedql:offset": offset }); + return this; + } + hasReverse(via: Path, values: Identifier[]): Path { + this.addStep({ + "@type": "linkedql:HasReverse", + "linkedql:via": via, + "linkedql:values": values + }); + return this; + } + in(via: Path): Path { + this.addStep({ "@type": "linkedql:In", "linkedql:via": via }); + return this; + } + reversePropertyNames(): Path { + this.addStep({ "@type": "linkedql:ReversePropertyNames" }); + return this; + } + labels(): Path { + this.addStep({ "@type": "linkedql:Labels" }); + return this; + } + properties(names: string[]): Path { + this.addStep({ "@type": "linkedql:Properties", "linkedql:names": names }); + return this; + } + placeholder(): Path { + this.addStep({ "@type": "Placeholder" }); + return this; + } + back(name: string): Path { + this.addStep({ "@type": "linkedql:Back", "linkedql:name": name }); + return this; + } + view(via: Path): Path { + this.addStep({ "@type": "linkedql:View", "linkedql:via": via }); + return this; + } + filter(filter: Operator): Path { + this.addStep({ "@type": "linkedql:Filter", "linkedql:filter": filter }); + return this; + } + limit(limit: number): Path { + this.addStep({ "@type": "linkedql:Limit", "linkedql:limit": limit }); + return this; + } + order(): Path { + this.addStep({ "@type": "linkedql:Order" }); + return this; + } + intersect(steps: Path[]): Path { + this.addStep({ "@type": "linkedql:Intersect", "linkedql:steps": steps }); + return this; + } + difference(steps: Path[]): Path { + this.addStep({ "@type": "linkedql:Difference", "linkedql:steps": steps }); + return this; + } + viewBoth(via: Path): Path { + this.addStep({ "@type": "linkedql:ViewBoth", "linkedql:via": via }); + return this; + } + vertex(values: Identifier[]): Path { + this.addStep({ "@type": "linkedql:Vertex", "linkedql:values": values }); + return this; + } + as(name: string): Path { + this.addStep({ "@type": "linkedql:As", "linkedql:name": name }); + return this; + } + reverseProperties(names: string[]): Path { + this.addStep({ + "@type": "linkedql:ReverseProperties", + "linkedql:names": names + }); + return this; + } + out(via: Path): Path { + this.addStep({ "@type": "linkedql:Out", "linkedql:via": via }); + return this; + } + viewReverse(via: Path): Path { + this.addStep({ "@type": "linkedql:ViewReverse", "linkedql:via": via }); + return this; + } + follow(followed: Path): Path { + this.addStep({ "@type": "linkedql:Follow", "linkedql:followed": followed }); + return this; + } + followReverse(followed: Path): Path { + this.addStep({ + "@type": "linkedql:FollowReverse", + "linkedql:followed": followed + }); + return this; + } + propertyNames(): Path { + this.addStep({ "@type": "linkedql:PropertyNames" }); + return this; + } + reversePropertyNamesAs(tag: string): Path { + this.addStep({ + "@type": "linkedql:ReversePropertyNamesAs", + "linkedql:tag": tag + }); + return this; + } + propertyNamesAs(tag: string): Path { + this.addStep({ "@type": "linkedql:PropertyNamesAs", "linkedql:tag": tag }); + return this; + } + unique(): Path { + this.addStep({ "@type": "linkedql:Unique" }); + return this; + } + selectFirst(tags: string[]): Path { + this.addStep({ "@type": "linkedql:SelectFirst", "linkedql:tags": tags }); + return this; + } + count(): Path { + this.addStep({ "@type": "linkedql:Count" }); + return this; + } +} diff --git a/query-builder.ts b/query-builder.ts deleted file mode 100644 index 3fcca32..0000000 --- a/query-builder.ts +++ /dev/null @@ -1,424 +0,0 @@ -import Client from "./cayley"; - -class Value { - path: Path; - constructor(path: Path) { - this.path = path; - } -} - -type Step = { type: string; args?: any[] }; - -/** - * Both `.Morphism()` and `.Vertex()` create path objects, which provide the following traversal methods. - * Note that `.Vertex()` returns a query object, which is a subclass of path object. - * - * For these examples, suppose we have the following graph: - * - * ``` - * +-------+ +------+ - * | alice |----- ->| fred |<-- - * +-------+ \---->+-------+-/ +------+ \-+-------+ - * ----->| #bob# | | |*emily*| - * +---------+--/ --->+-------+ | +-------+ - * | charlie | / v - * +---------+ / +--------+ - * \--- +--------+ |*#greg#*| - * \-->| #dani# |------------>+--------+ - * +--------+ - * ``` - * - * Where every link is a `` relationship, and the nodes with an extra `#` in the name have an extra `` link. As in, - * - * ``` - * -- --> "cool_person" - * ``` - * - * Perhaps these are the influencers in our community. So too are extra `*`s in the name -- these are our smart people, - * according to the `` label, eg, the quad: - * - * ``` - * "smart_person" . - * ``` - */ -class Path { - /** @todo make path to accept type variable of fields */ - private graph: Graph; - steps: Step[]; - constructor(graph: Graph, steps: Step[]) { - this.graph = graph; - this.steps = steps; - } - private addStep(step: Step) { - this.steps.push(step); - } - private chainStep(step: S): Path { - this.addStep(step); - return this; - } - private execute() { - return this.graph.execute(this.steps); - } - /** Execute the query and adds the results, with all tags, as a string-to-string (tag to node) map in the output set, one for each path that a traversal could take. */ - all() { - this.addStep({ type: "all" }); - return this.execute(); - } - /** Alias for intersect. - */ - and(path: Path) { - return this.chainStep({ type: "and", args: [path] }); - } - /** Alias for tag. - */ - as(...tags: string[]) { - return this.tag(...tags); - } - /** Return current path to a set of nodes on a given tag, preserving all constraints. - * If still valid, a path will now consider their vertex to be the same one as the previously tagged one, with the added constraint that it was valid all the way here. Useful for traversing back in queries and taking another route for things that have matched so far. - */ - back(tag?: string) { - const args = tag !== undefined ? [tag] : []; - return this.chainStep({ - type: "back", - args - }); - } - /** Follow the predicate in either direction. Same as out or in. - */ - both(path: Path, ...tags: string[]) { - return this.chainStep({ - type: "both", - args: [path, ...tags] - }); - } - /** Return a number of results and returns it as a value. */ - count() { - this.addStep({ type: "count" }); - return new Value(this); - } - /** Alias for Except */ - difference(path: Path) { - return this.except(path); - } - /** Removes all paths which match query from current path. In a set-theoretic sense, this is (A - B). While `g.V().Except(path)` to achieve `U - B = !B` is supported, it's often very slow. */ - except(path: Path) { - return this.chainStep({ type: "except", args: [path] }); - } - /** Apply constraints to a set of nodes. Can be used to filter values by range or match strings. */ - filter(filter: Filter) { - return this.chainStep({ type: "filter", args: [filter] }); - } - /** The way to use a path prepared with Morphism. Applies the path chain on the morphism object to the current path. - * Starts as if at the g.M() and follows through the morphism path. */ - follow(path: Path) { - return this.chainStep({ type: "follow", args: [path] }); - } - /** The same as follow but follows the chain in the reverse direction. Flips "In" and "Out" where appropriate, - the net result being a virtual predicate followed in the reverse direction. Starts at the end of the morphism and follows it backwards (with appropriate flipped directions) to the g.M() location. */ - followR(path: Path) { - return this.chainStep({ type: "followR", args: [path] }); - return this; - } - /** The same as follow but follows the chain recursively. Starts as if at the g.M() and follows through the morphism path multiple times, returning all nodes encountered. */ - followRecursive(path: Path) { - return this.chainStep({ type: "followRecursive", args: [path] }); - } - /** Call callback(data) for each result, where data is the tag-to-string map as in All case. - */ - forEach( - limit: number, - callback: (data: { [key: string]: any }) => void - ): void; - forEach(callback: (data: { [key: string]: any }) => void): void; - forEach( - limitOrCallback: number | ((data: { [key: string]: any }) => void), - callback?: (data: { [key: string]: any }) => void - ): void { - if (typeof limitOrCallback === "number") { - const limit = limitOrCallback; - this.limit(limit); - } else { - callback = limitOrCallback; - } - this.execute().then(response => { - response.forEach(callback); - }); - } - /** The same as All, but limited to the first N unique nodes at the end of the path, and each of their possible traversals. */ - getLimit(limit: number) { - this.addStep({ type: "getLimit", args: [limit] }); - return this.execute(); - } - /** Filter all paths which are, at this point, on the subject for the given predicate and object, - but do not follow the path, merely filter the possible paths. Usually useful for starting with all nodes, or limiting to a subset depending on some predicate/value pair. -/ - */ - has(predicate: string, object: string) { - return this.chainStep({ type: "has", args: [predicate, object] }); - } - /** The same as Has, but sets constraint in reverse direction. */ - hasR(predicate: string, object: string) { - return this.chainStep({ type: "hasR", args: [predicate, object] }); - } - /** The inverse of out. Starting with the nodes in `path` on the object, follow the quads with predicates defined by `predicatePath` to their subjects. - * * null or undefined: All predicates pointing into this node - * * a string: The predicate name to follow into this node - * * a list of strings: The predicates to follow into this node - * * a query path object: The target of which is a set of predicates to follow. - * * null or undefined: No tags - * * a string: A single tag to add the predicate used to the output set. - * * a list of strings: Multiple tags to use as keys to save the predicate used to the output set. - */ - in(predicatePath?: Path, ...tags: string[]) { - const args = predicatePath !== undefined ? [predicatePath, ...tags] : tags; - return this.chainStep({ type: "in", args }); - } - /** Get the list of predicates that are pointing in to a node. */ - inPredicates() { - return this.chainStep({ type: "inPredicates" }); - } - /** Filter all paths by the result of another query path. This is essentially a join where, at the stage of each path, a node is shared. */ - intersect(path: Path) { - return this.chainStep({ type: "intersect", args: [path] }); - } - /** Filter all paths to ones which, at this point, are on the given node. - */ - is(node: string, ...nodes: string[]) { - return this.chainStep({ type: "is", args: [node, ...nodes] }); - } - /** Set (or remove) the subgraph context to consider in the following traversals. - * Affects all in(), out(), and both() calls that follow it. The default LabelContext is null (all subgraphs). - * * null or undefined: In future traversals, consider all edges, regardless of subgraph. - * * a string: The name of the subgraph to restrict traversals to. - * * a list of strings: A set of subgraphs to restrict traversals to. - * * a query path object: The target of which is a set of subgraphs. - * * null or undefined: No tags - * * a string: A single tag to add the last traversed label to the output set. - * * a list of strings: Multiple tags to use as keys to save the label used to the output set. - */ - labelContext(labelPath: Path, ...tags: string[]) { - return this.chainStep({ type: "labelContext", args: [labelPath, ...tags] }); - } - /** Get the list of inbound and outbound quad labels */ - labels() { - return this.chainStep({ type: "labels" }); - } - /** Limit a number of nodes for current path. */ - limit(limit: number) { - return this.chainStep({ type: "limit", args: [limit] }); - } - /** Alias for Union. */ - or(path: Path) { - return this.union(path); - } - /** The work-a-day way to get between nodes, in the forward direction. Starting with the nodes in `path` on the subject, follow the quads with predicates defined by `predicatePath` to their objects. - * * null or undefined: All predicates pointing out from this node - * * a string: The predicate name to follow out from this node - * * a list of strings: The predicates to follow out from this node - * * a query path object: The target of which is a set of predicates to follow. - * * null or undefined: No tags - * * a string: A single tag to add the predicate used to the output set. - * * a list of strings: Multiple tags to use as keys to save the predicate used to the output set. - */ - out(predicateOrPath?: Call | Call[] | Path, ...tags: string[]) { - const args = - predicateOrPath !== undefined ? [predicateOrPath, ...tags] : tags; - return this.chainStep({ type: "out", args }); - } - /** Get the list of predicates that are pointing out from a node. */ - outPredicates() { - return this.chainStep({ type: "outPredicates" }); - } - /** Save the object of all quads with predicate into tag, without traversal. - */ - save(predicate: string, tag: string) { - return this.chainStep({ type: "save", args: [predicate, tag] }); - } - /** The same as save, but returns empty tags if predicate does not exists. */ - saveOpt(predicate: string, tag: string) { - return this.chainStep({ type: "saveOpt", args: [predicate, tag] }); - } - /** The same as saveOpt, but tags values via reverse predicate. */ - saveOptR(predicate: string, tag: string) { - return this.chainStep({ type: "saveOptR", args: [predicate, tag] }); - } - /** The same as save, but tags values via reverse predicate. */ - saveR(predicate: string, tag: string) { - return this.chainStep({ type: "saveR", args: [predicate, tag] }); - } - /** Tag the list of predicates that are pointing in to a node. */ - saveInPredicates(tag: string) { - return this.chainStep({ type: "saveInPredicates", args: [tag] }); - } - /** Tag the list of predicates that are pointing out from a node. */ - saveOutPredicates(tag: string) { - return this.chainStep({ type: "saveOutPredicates", args: [tag] }); - } - /** Skip a number of nodes for current path. - */ - skip(offset: number) { - return this.chainStep({ type: "skip", args: [offset] }); - } - /** Save a list of nodes to a given tag. In order to save your work or learn more about how a path got to the end, we have tags. - The simplest thing to do is to add a tag anywhere you'd like to put each node in the result set. -/reached "Tag" */ - tag(...tags: string[]) { - return this.chainStep({ type: "tag", args: tags }); - } - /** - * The same as toArray, but instead of a list of top-level nodes, returns an Array of tag-to-string dictionaries, much as All would, except inside the JS environment. - */ - tagArray() { - this.addStep({ type: "tagArray" }); - return new Value(this); - } - /** The same as TagArray, but limited to one result node. Returns a tag-to-string map. */ - tagValue() { - this.addStep({ type: "tagValue" }); - return new Value(this); - } - /** Execute a query and returns the results at the end of the query path as an JS array. */ - toArray() { - this.addStep({ type: "toArray" }); - return new Value(this); - } - /** The same as ToArray, but limited to one result node. */ - toValue() { - this.addStep({ type: "toValue" }); - return new Value(this); - } - /** Return the combined paths of the two queries. Notice that it's per-path, not per-node. Once again, if multiple paths reach the same destination, they might have had different ways of getting there (and different tags). See also: `Path.prototype.tag()` */ - union(path: Path) { - return this.chainStep({ type: "union", args: [path] }); - } - /** Remove duplicate values from the path. */ - unique() { - return this.chainStep({ type: "unique" }); - } - /** Order returns values from the path in ascending order. */ - order() { - return this.chainStep({ type: "order" }); - } -} - -export class Graph { - client: Client; - private steps: Step[]; - constructor(client: Client) { - this.client = client; - this.steps = []; - } - private static createCallString(name: string, args): string { - return `${name}(${args ? deepMap(args, Graph.argToString).join() : ""})`; - } - private static argToString(arg): string { - if (arg.function) { - return Graph.createCallString(arg.function, arg.args); - } - if (arg instanceof Path) { - return Graph.createGraphCallChainString(arg.steps); - } - if (arg instanceof Value) { - return Graph.createGraphCallChainString(arg.path.steps); - } - return JSON.stringify(arg); - } - private static createMethodCallString( - expression: string, - step: Step - ): string { - return `${expression}.${Graph.createCallString(step.type, step.args)}`; - } - private static Expression = "graph"; - private static createGraphCallChainString(steps: Step[]): string { - return steps.reduce(Graph.createMethodCallString, Graph.Expression); - } - async execute(steps: Step[]): Promise { - const globalCalls = this.steps.map(step => - Graph.createMethodCallString(Graph.Expression, step) - ); - const query = [ - ...globalCalls, - Graph.createGraphCallChainString(steps) - ].join(";"); - const res = await this.client.query(query); - const { result, error } = await res.json(); - if (error) { - throw new Error(error); - } - return result; - } - /** A shorthand for Vertex. */ - V(...nodeIds: (string | Call)[]) { - return this.Vertex(...nodeIds); - } - /** A shorthand for Morphism */ - M() { - return this.Morphism(); - } - /** Start a query path at the given vertex/vertices. No ids means "all vertices". */ - Vertex(...nodeIds: (string | Call)[]) { - const step = { - type: "Vertex", - args: nodeIds - }; - return new Path(this, [step]); - } - /** Create a morphism path object. Unqueryable on it's own, defines one end of the path. - Saving these to variables with */ - Morphism() { - return new Path(this, [{ type: "Morphism" }]); - } - /** Load all namespaces saved to graph. */ - loadNamespaces() { - this.steps.push({ type: "loadNamespaces" }); - } - /** Register all default namespaces for automatic IRI resolution. */ - addDefaultNamespaces() { - this.steps.push({ type: "addDefaultNamespaces" }); - } - /** Associate prefix with a given IRI namespace. */ - addNamespace(pref: string, ns: string) { - this.steps.push({ type: "addNamespace", args: [pref, ns] }); - } - /** Add data programmatically to the JSON result list. Can be any JSON type. */ - emit(value: Value) { - const step = { type: "emit", args: [value] }; - return this.execute([step]); - } - /** Create an IRI values from a given string. */ - IRI(iri: string): Call { - return { function: "g.IRI", args: [iri] }; - } -} - -type Call = { function: string; args: any[] }; - -type Filter = Call; - -/** Filter by match a regular expression ([syntax](https://github.com/google/re2/wiki/Syntax)). By default works only on literals unless includeIRIs is set to `true`. */ -export function regex(expression: string, includeIRIs?: boolean): Filter { - const args = - includeIRIs === undefined ? [expression] : [expression, includeIRIs]; - return { function: "regex", args }; -} - -export function like(pattern: string) { - return { function: "like", args: [pattern] }; -} - -interface DeepArray extends Array> {} - -function deepMap( - array: DeepArray, - func: (item: T) => T2 -): DeepArray { - // @ts-ignore - return array.map((item: T): T2 | DeepArray => { - if (Array.isArray(item)) { - return deepMap(item, func); - } - return func(item); - }); -} diff --git a/test/client.spec.ts b/test/client.spec.ts index a223359..e7cdb65 100644 --- a/test/client.spec.ts +++ b/test/client.spec.ts @@ -1,5 +1,5 @@ import assert = require("assert"); -import Client, { NamedNode, Format, Graph } from "../cayley"; +import Client, { NamedNode, Format, Path } from "../cayley"; describe("Read", () => { it("simple", async () => { @@ -34,19 +34,14 @@ describe("Write", () => { type TestCase = { name: string; - query(path: Graph): Promise; + query: Path; validate(result: any[]): void; }; const testCases: TestCase[] = [ { - name: "g.V().all()", - query: g => g.V().all(), - validate: assert - }, - { - name: "g.V(g.IRI('bob'))", - query: g => g.V(g.IRI("bob")).all(), + name: "g.vertex(g.IRI('bob'))", + query: new Path().vertex([new NamedNode("bob")]), validate: result => { assert(result); assert(result.length === 1); @@ -55,8 +50,8 @@ const testCases: TestCase[] = [ } }, { - name: "g.V().getLimit(-1)", - query: g => g.V().getLimit(-1), + name: "g.vertex()", + query: new Path().vertex([]), validate: result => { assert(result); assert(result.length); @@ -70,12 +65,10 @@ const testCases: TestCase[] = [ } }, { - name: "g.V().out(g.IRI('follows')).getLimit(-1)", - query: g => - g - .V() - .out(g.IRI("follows")) - .getLimit(-1), + name: "g.vertex().view(g.IRI('follows'))", + query: new Path() + .vertex([]) + .view(new Path().vertex([new NamedNode("follows")])), validate: result => { assert(result); assert(result.length); @@ -85,23 +78,6 @@ const testCases: TestCase[] = [ assert(typeof item["id"]["@id"] === "string"); } } - }, - { - name: "g.emit(g.V().toArray())", - query: g => g.emit(g.V().toArray()), - validate: result => { - assert(result); - assert(result.length); - for (const item of result) { - assert(Array.isArray(item)); - for (const subItem of item) { - assert(typeof subItem === "object" || typeof subItem === "string"); - if (typeof subItem === "object") { - assert(typeof subItem["@id"] === "string"); - } - } - } - } } ]; @@ -109,8 +85,11 @@ describe("Query Builder", () => { for (const testCase of testCases) { it(testCase.name, async () => { const client = new Client(); - const { g } = client; - const result = await testCase.query(g); + const response = await client.query(testCase.query); + const { result, error } = await response.json(); + if (error) { + new Error(error); + } testCase.validate(result); }); } diff --git a/tsconfig.json b/tsconfig.json index 321944e..372ce1d 100644 --- a/tsconfig.json +++ b/tsconfig.json @@ -1,3 +1,4 @@ { - "lib": ["ES2015", "dom"] + "lib": ["ES2015", "dom"], + "resolveJsonModule": true }