8000 Added support for filtering results by karelklima · Pull Request #81 · karelklima/ldkit · GitHub
[go: up one dir, main page]
More Web Proxy on the site http://driver.im/
Skip to content

Added support for filtering results #81

New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Merged
merged 5 commits into from
Dec 19, 2023
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
1 change: 1 addition & 0 deletions docs/table-of-contents.json
Original file line number Diff line number Diff line change
Expand Up @@ -23,6 +23,7 @@
"v2": {
"title": "Version 2 (not yet released)",
"pages": [
["filtering", "Filtering"],
["pagination", "Pagination"],
["working-with-arrays", "Working with arrays"],
["inverse-properties", "Inverse properties"]
Expand Down
88 changes: 88 additions & 0 deletions docs/v2/filtering.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,88 @@
# Filtering

LDkit comes with powerful search and filtering capabilities, enabling users to
narrow data results and explore large datasets. The feature integrates
seamlessly with the existing [Lens](../components/lens) `find` method, allowing
for controlled data retrieval.

LDkit allows various search and filtering operations like `$equals`, `$not`,
`$contains`, `$strStarts`, `$strEnds`, `$gt`, `$lt`, `$gte`, `$lte`, `$regex`,
`$langMatches`, and `$filter`. Each is illustrated below with examples.

### Simple example

```ts
import { createLens } from "ldkit";
import { schema, xsd } from "ldkit/namespaces";

// Create a schema
const PersonSchema = {
"@type": schema.Person,
name: schema.name,
birthDate: {
"@id": schema.birthDate,
"@type": xsd.date,
},
} as const;

// Create a lens instance
const Persons = createLens(PersonSchema);

await Persons.find({
where: {
name: "Ada Lovelace",
},
}); // Returns list of all persons named "Ada Lovelace"
```

### Comparison operators

```typescript
await Persons.find({
where: {
name: {
$equals: "Ada Lovelace", // FILTER (?value = "Ada Lovelace")
$not: "Alan Turing", // FILTER (?value != "Alan Turing")
},
birthDate: {
$lt: new Date("01-01-1900"), // FILTER (?value < "01-01-1900"@xsd:date)
$lte: new Date("01-01-1900"), // FILTER (?value <= "01-01-1900"@xsd:date)
$gt: new Date("01-01-1900"), // FILTER (?value > "01-01-1900"@xsd:date)
$gte: new Date("01-01-1900"), // FILTER (?value >= "01-01-1900"@xsd:date)
},
},
});
```

### String functions

```typescript
await Persons.find({
where: {
name: {
$contains: "Ada", // FILTER CONTAINS(?value, "Ada")
$strStarts: "Ada", // FILTER STRSTARTS(?value, "Ada")
$strEnds: "Lovelace", // FILTER STRENDS(?value, "Lovelace")
$langMatches: "fr", // FILTER LANGMATCHES(LANG(?value), "fr")
$regex: "^A(.*)e$", // FILTER REGEX(?value, "^A(.*)e$")
},
},
});
```

### Custom filtering

On top of the above, it is possible to specify a custom filter function using
the SPARQL filtering syntax. There is a special placeholder `?value` used for
the value of the property to be filtered. This placeholder is replaced by actual
variable name during runtime.

```typescript
await Persons.find({
where: {
name: {
$filter: "STRLEN(?value) > 10", // FILTER (STRLEN(?value) > 10)
},
},
});
```
11 changes: 9 additions & 2 deletions library/engine/query_engine_proxy.ts
Original file line number Diff line number Diff line change
@@ -1,4 +1,10 @@
import { type Context, IQueryEngine, quadsToGraph, type RDF } from "../rdf.ts";
import {
type Context,
IQueryEngine,
N3,
quadsToGraph,
type RDF,
} from "../rdf.ts";
import { resolveContext, resolveEngine } from "../global.ts";
import { type AsyncIterator } from "../asynciterator.ts";

Expand Down Expand Up @@ -29,7 +35,8 @@ export class QueryEngineProxy {
this.context,
) as unknown as AsyncIterator<RDF.Quad>;
const quads = await (quadStream.toArray());
return quadsToGraph(quads);
const store = new N3.Store(quads);
return quadsToGraph(store);
}

queryVoid(query: string) {
Expand Down
14 changes: 11 additions & 3 deletions library/lens/lens.ts
EDBE
Original file line number Diff line number Diff line change
Expand Up @@ -6,6 +6,7 @@ import {
type SchemaInterface,
type SchemaInterfaceIdentity,
type SchemaPrototype,
type SchemaSearchInterface,
type SchemaUpdateInterface,
} from "../schema/mod.ts";
import { decode } from "../decoder.ts";
Expand Down Expand Up @@ -44,6 +45,7 @@ export class Lens<
S extends SchemaPrototype,
I = SchemaInterface<S>,
U = SchemaUpdateInterface<S>,
X = SchemaSearchInterface<S>,
> {
private readonly schema: Schema;
private readonly context: Context;
Expand Down Expand Up @@ -72,19 +74,25 @@ export class Lens<

async query(sparqlConstructQuery: string) {
const graph = await this.engine.queryGraph(sparqlConstructQuery);
console.log("GRAPH", graph);
return this.decode(graph);
}

async find(
options: { where?: string | RDF.Quad[]; take?: number; skip?: number } = {},
options: { where?: X | string | RDF.Quad[]; take?: number; skip?: number } =
{},
) {
const { where, take, skip } = {
take: 1000,
skip: 0,
...options,
};
const q = this.queryBuilder.getQuery(where, take, skip);
// TODO: console.log(q);
const isRegularQuery = typeof where === "string" ||
typeof where === "undefined" || Array.isArray(where);

const q = isRegularQuery
? this.queryBuilder.getQuery(where, take, skip)
: this.queryBuilder.getSearchQuery(where ?? {}, take, skip);
const graph = await this.engine.queryGraph(q);
return this.decode(graph);
}
Expand Down
64 changes: 57 additions & 7 deletions library/lens/query_builder.ts
Original file line number Diff line number Diff line change
@@ -1,11 +1,16 @@
import { type Schema } from "../schema/mod.ts";
import { getSchemaProperties } from "../schema/mod.ts";
import {
getSchemaProperties,
type Property,
type Schema,
type SearchSchema,
} from "../schema/mod.ts";
import {
CONSTRUCT,
DELETE,
INSERT,
SELECT,
sparql as $,
type SparqlValue,
} from "../sparql/mod.ts";
import { type Context, DataFactory, type Iri, type RDF } from "../rdf.ts";
import ldkit from "../namespaces/ldkit.ts";
Expand All @@ -15,6 +20,7 @@ import { encode } from "../encoder.ts";

import { type Entity } from "./types.ts";
import { UpdateHelper } from "./update_helpe 1E0A r.ts";
import { SearchHelper } from "./search_helper.ts";

enum Flags {
None = 0,
Expand Down Expand Up @@ -52,16 +58,33 @@ export class QueryBuilder {
return ([] as RDF.Quad[]).concat(...quadArrays);
}

private getShape(flags: Flags) {
private getShape(flags: Flags, searchSchema?: SearchSchema) {
const includeOptional = (flags & Flags.ExcludeOptional) === 0;
const wrapOptional = (flags & Flags.UnwrapOptional) === 0;
const omitRootTypes = (flags & Flags.IncludeTypes) === 0;
const ignoreInverse = (flags & Flags.IgnoreInverse) === Flags.IgnoreInverse;

const mainVar = "iri";
const conditions: (RDF.Quad | ReturnType<typeof $>)[] = [];
const conditions: SparqlValue[] = [];

const populateSearchConditions = (
property: Property,
varName: string,
search?: SearchSchema,
) => {
if (search === undefined) {
return;
}
const helper = new SearchHelper(property, varName, search);
helper.process();
conditions.push(helper.sparqlValues);
};

const populateConditionsRecursive = (s: Schema, varPrefix: string) => {
const populateConditionsRecursive = (
s: Schema,
varPrefix: string,
search?: SearchSchema,
) => {
const rdfType = s["@type"];
const properties = getSchemaProperties(s);

Expand All @@ -80,7 +103,8 @@ export class QueryBuilder {
Object.keys(properties).forEach((prop, index) => {
const property = properties[prop];
const isOptional = property["@optional"];
if (!includeOptional && isOptional) {
const propertySchema = search?.[prop] as SearchSchema | undefined;
if (!includeOptional && isOptional && propertySchema === undefined) {
return;
}
if (wrapOptional && isOptional) {
Expand All @@ -95,6 +119,11 @@ export class QueryBuilder {
this.df.variable!(`${varPrefix}_${index}`),
),
);
populateSearchConditions(
property,
`${varPrefix}_${index}`,
propertySchema,
);
} else {
conditions.push(
this.df.quad(
Expand All @@ -108,6 +137,7 @@ export class QueryBuilder {
populateConditionsRecursive(
property["@context"] as Schema,
`${varPrefix}_${index}`,
propertySchema,
);
}
if (wrapOptional && isOptional) {
Expand All @@ -116,7 +146,7 @@ export class QueryBuilder {
});
};

populateConditionsRecursive(this.schema, mainVar);
populateConditionsRecursive(this.schema, mainVar, searchSchema);
return conditions;
}

Expand Down Expand Up @@ -146,6 +176,26 @@ export class QueryBuilder {
return query;
}

getSearchQuery(where: SearchSchema, limit: number, offset: number) {
const selectSubQuery = SELECT.DISTINCT`
${this.df.variable!("iri")}
`.WHERE`
${this.getShape(Flags.ExcludeOptional | Flags.IncludeTypes, where)}
`.LIMIT(limit).OFFSET(offset).build();

const query = CONSTRUCT`
${this.getResourceSignature()}
${this.getShape(Flags.UnwrapOptional | Flags.IgnoreInverse)}
`.WHERE`
{
${selectSubQuery}
}
${this.getShape(Flags.None, where)}
`.build();

return query;
}

getByIrisQuery(iris: Iri[]) {
const query = CONSTRUCT`
${this.getResourceSignature()}
Expand Down
Loading
0