@_linked/core 2.0.0 → 2.2.0-next.20260313111019
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/CHANGELOG.md +33 -171
- package/README.md +197 -45
- package/lib/cjs/expressions/Expr.d.ts +58 -0
- package/lib/cjs/expressions/Expr.js +217 -0
- package/lib/cjs/expressions/Expr.js.map +1 -0
- package/lib/cjs/expressions/ExpressionMethods.d.ts +81 -0
- package/lib/cjs/expressions/ExpressionMethods.js +3 -0
- package/lib/cjs/expressions/ExpressionMethods.js.map +1 -0
- package/lib/cjs/expressions/ExpressionNode.d.ts +95 -0
- package/lib/cjs/expressions/ExpressionNode.js +349 -0
- package/lib/cjs/expressions/ExpressionNode.js.map +1 -0
- package/lib/cjs/index.d.ts +4 -0
- package/lib/cjs/index.js +6 -1
- package/lib/cjs/index.js.map +1 -1
- package/lib/cjs/queries/DeleteBuilder.d.ts +17 -20
- package/lib/cjs/queries/DeleteBuilder.js +46 -19
- package/lib/cjs/queries/DeleteBuilder.js.map +1 -1
- package/lib/cjs/queries/DeleteQuery.d.ts +2 -2
- package/lib/cjs/queries/DeleteQuery.js.map +1 -1
- package/lib/cjs/queries/FieldSet.d.ts +3 -0
- package/lib/cjs/queries/FieldSet.js +16 -0
- package/lib/cjs/queries/FieldSet.js.map +1 -1
- package/lib/cjs/queries/IRCanonicalize.d.ts +10 -3
- package/lib/cjs/queries/IRCanonicalize.js +10 -1
- package/lib/cjs/queries/IRCanonicalize.js.map +1 -1
- package/lib/cjs/queries/IRDesugar.d.ts +30 -2
- package/lib/cjs/queries/IRDesugar.js +29 -9
- package/lib/cjs/queries/IRDesugar.js.map +1 -1
- package/lib/cjs/queries/IRLower.d.ts +19 -2
- package/lib/cjs/queries/IRLower.js +104 -20
- package/lib/cjs/queries/IRLower.js.map +1 -1
- package/lib/cjs/queries/IRMutation.d.ts +31 -1
- package/lib/cjs/queries/IRMutation.js +68 -15
- package/lib/cjs/queries/IRMutation.js.map +1 -1
- package/lib/cjs/queries/IntermediateRepresentation.d.ts +33 -4
- package/lib/cjs/queries/MutationQuery.js +16 -2
- package/lib/cjs/queries/MutationQuery.js.map +1 -1
- package/lib/cjs/queries/QueryBuilder.d.ts +14 -1
- package/lib/cjs/queries/QueryBuilder.js +59 -2
- package/lib/cjs/queries/QueryBuilder.js.map +1 -1
- package/lib/cjs/queries/QueryFactory.d.ts +2 -1
- package/lib/cjs/queries/QueryFactory.js.map +1 -1
- package/lib/cjs/queries/SelectQuery.d.ts +6 -2
- package/lib/cjs/queries/SelectQuery.js +51 -7
- package/lib/cjs/queries/SelectQuery.js.map +1 -1
- package/lib/cjs/queries/UpdateBuilder.d.ts +22 -13
- package/lib/cjs/queries/UpdateBuilder.js +60 -11
- package/lib/cjs/queries/UpdateBuilder.js.map +1 -1
- package/lib/cjs/queries/UpdateQuery.d.ts +2 -2
- package/lib/cjs/shapes/Shape.d.ts +8 -2
- package/lib/cjs/shapes/Shape.js +9 -13
- package/lib/cjs/shapes/Shape.js.map +1 -1
- package/lib/cjs/sparql/SparqlStore.js +15 -0
- package/lib/cjs/sparql/SparqlStore.js.map +1 -1
- package/lib/cjs/sparql/irToAlgebra.d.ts +34 -3
- package/lib/cjs/sparql/irToAlgebra.js +380 -31
- package/lib/cjs/sparql/irToAlgebra.js.map +1 -1
- package/lib/cjs/test-helpers/query-fixtures.d.ts +96 -208
- package/lib/cjs/test-helpers/query-fixtures.js +96 -19
- package/lib/cjs/test-helpers/query-fixtures.js.map +1 -1
- package/lib/esm/expressions/Expr.d.ts +58 -0
- package/lib/esm/expressions/Expr.js +214 -0
- package/lib/esm/expressions/Expr.js.map +1 -0
- package/lib/esm/expressions/ExpressionMethods.d.ts +81 -0
- package/lib/esm/expressions/ExpressionMethods.js +2 -0
- package/lib/esm/expressions/ExpressionMethods.js.map +1 -0
- package/lib/esm/expressions/ExpressionNode.d.ts +95 -0
- package/lib/esm/expressions/ExpressionNode.js +341 -0
- package/lib/esm/expressions/ExpressionNode.js.map +1 -0
- package/lib/esm/index.d.ts +4 -0
- package/lib/esm/index.js +3 -0
- package/lib/esm/index.js.map +1 -1
- package/lib/esm/queries/DeleteBuilder.d.ts +17 -20
- package/lib/esm/queries/DeleteBuilder.js +46 -19
- package/lib/esm/queries/DeleteBuilder.js.map +1 -1
- package/lib/esm/queries/DeleteQuery.d.ts +2 -2
- package/lib/esm/queries/DeleteQuery.js.map +1 -1
- package/lib/esm/queries/FieldSet.d.ts +3 -0
- package/lib/esm/queries/FieldSet.js +16 -0
- package/lib/esm/queries/FieldSet.js.map +1 -1
- package/lib/esm/queries/IRCanonicalize.d.ts +10 -3
- package/lib/esm/queries/IRCanonicalize.js +10 -1
- package/lib/esm/queries/IRCanonicalize.js.map +1 -1
- package/lib/esm/queries/IRDesugar.d.ts +30 -2
- package/lib/esm/queries/IRDesugar.js +21 -2
- package/lib/esm/queries/IRDesugar.js.map +1 -1
- package/lib/esm/queries/IRLower.d.ts +19 -2
- package/lib/esm/queries/IRLower.js +101 -19
- package/lib/esm/queries/IRLower.js.map +1 -1
- package/lib/esm/queries/IRMutation.d.ts +31 -1
- package/lib/esm/queries/IRMutation.js +63 -14
- package/lib/esm/queries/IRMutation.js.map +1 -1
- package/lib/esm/queries/IntermediateRepresentation.d.ts +33 -4
- package/lib/esm/queries/MutationQuery.js +16 -2
- package/lib/esm/queries/MutationQuery.js.map +1 -1
- package/lib/esm/queries/QueryBuilder.d.ts +14 -1
- package/lib/esm/queries/QueryBuilder.js +59 -2
- package/lib/esm/queries/QueryBuilder.js.map +1 -1
- package/lib/esm/queries/QueryFactory.d.ts +2 -1
- package/lib/esm/queries/QueryFactory.js.map +1 -1
- package/lib/esm/queries/SelectQuery.d.ts +6 -2
- package/lib/esm/queries/SelectQuery.js +51 -7
- package/lib/esm/queries/SelectQuery.js.map +1 -1
- package/lib/esm/queries/UpdateBuilder.d.ts +22 -13
- package/lib/esm/queries/UpdateBuilder.js +60 -11
- package/lib/esm/queries/UpdateBuilder.js.map +1 -1
- package/lib/esm/queries/UpdateQuery.d.ts +2 -2
- package/lib/esm/shapes/Shape.d.ts +8 -2
- package/lib/esm/shapes/Shape.js +9 -13
- package/lib/esm/shapes/Shape.js.map +1 -1
- package/lib/esm/sparql/SparqlStore.js +16 -1
- package/lib/esm/sparql/SparqlStore.js.map +1 -1
- package/lib/esm/sparql/irToAlgebra.d.ts +34 -3
- package/lib/esm/sparql/irToAlgebra.js +374 -31
- package/lib/esm/sparql/irToAlgebra.js.map +1 -1
- package/lib/esm/test-helpers/query-fixtures.d.ts +96 -208
- package/lib/esm/test-helpers/query-fixtures.js +96 -19
- package/lib/esm/test-helpers/query-fixtures.js.map +1 -1
- package/package.json +3 -3
package/CHANGELOG.md
CHANGED
|
@@ -1,193 +1,55 @@
|
|
|
1
1
|
# Changelog
|
|
2
2
|
|
|
3
|
-
## 2.
|
|
4
|
-
|
|
5
|
-
### Major Changes
|
|
6
|
-
|
|
7
|
-
- [#23](https://github.com/Semantu/linked/pull/23) [`d2d1eca`](https://github.com/Semantu/linked/commit/d2d1eca3517af11f39348dc83ba5e60703ef86d2) Thanks [@flyon](https://github.com/flyon)! - ## Breaking Changes
|
|
8
|
-
|
|
9
|
-
### `Shape.select()` and `Shape.update()` no longer accept an ID as the first argument
|
|
10
|
-
|
|
11
|
-
Use `.for(id)` to target a specific entity instead.
|
|
12
|
-
|
|
13
|
-
**Select:**
|
|
14
|
-
|
|
15
|
-
```typescript
|
|
16
|
-
// Before
|
|
17
|
-
const result = await Person.select({ id: "..." }, (p) => p.name);
|
|
18
|
-
|
|
19
|
-
// After
|
|
20
|
-
const result = await Person.select((p) => p.name).for({ id: "..." });
|
|
21
|
-
```
|
|
22
|
-
|
|
23
|
-
`.for(id)` unwraps the result type from array to single object, matching the old single-subject overload behavior.
|
|
24
|
-
|
|
25
|
-
**Update:**
|
|
26
|
-
|
|
27
|
-
```typescript
|
|
28
|
-
// Before
|
|
29
|
-
const result = await Person.update({ id: "..." }, { name: "Alice" });
|
|
30
|
-
|
|
31
|
-
// After
|
|
32
|
-
const result = await Person.update({ name: "Alice" }).for({ id: "..." });
|
|
33
|
-
```
|
|
34
|
-
|
|
35
|
-
`Shape.selectAll(id)` also no longer accepts an id — use `Person.selectAll().for(id)`.
|
|
36
|
-
|
|
37
|
-
### `ShapeType` renamed to `ShapeConstructor`
|
|
38
|
-
|
|
39
|
-
The type alias for concrete Shape subclass constructors has been renamed. Update any imports or references:
|
|
40
|
-
|
|
41
|
-
```typescript
|
|
42
|
-
// Before
|
|
43
|
-
import type { ShapeType } from "@_linked/core/shapes/Shape";
|
|
44
|
-
|
|
45
|
-
// After
|
|
46
|
-
import type { ShapeConstructor } from "@_linked/core/shapes/Shape";
|
|
47
|
-
```
|
|
48
|
-
|
|
49
|
-
### `QueryString`, `QueryNumber`, `QueryBoolean`, `QueryDate` classes removed
|
|
50
|
-
|
|
51
|
-
These have been consolidated into a single generic `QueryPrimitive<T>` class. If you were using `instanceof` checks against these classes, use `instanceof QueryPrimitive` instead and check the value's type.
|
|
52
|
-
|
|
53
|
-
### Internal IR types removed
|
|
54
|
-
|
|
55
|
-
The following types and functions have been removed from `SelectQuery`. These were internal pipeline types — if you were using them for custom store integrations, the replacement is `FieldSetEntry[]` (available from `FieldSet`):
|
|
56
|
-
|
|
57
|
-
- Types: `SelectPath`, `QueryPath`, `CustomQueryObject`, `SubQueryPaths`, `ComponentQueryPath`
|
|
58
|
-
- Functions: `fieldSetToSelectPath()`, `entryToQueryPath()`
|
|
59
|
-
- Methods: `QueryBuilder.getQueryPaths()`, `BoundComponent.getComponentQueryPaths()`
|
|
60
|
-
- `RawSelectInput.select` field renamed to `RawSelectInput.entries` (type changed from `SelectPath` to `FieldSetEntry[]`)
|
|
61
|
-
|
|
62
|
-
### `getPackageShape()` return type is now nullable
|
|
63
|
-
|
|
64
|
-
Returns `ShapeConstructor | undefined` instead of `typeof Shape`. Code that didn't null-check the return value will now get TypeScript errors.
|
|
65
|
-
|
|
66
|
-
## New Features
|
|
67
|
-
|
|
68
|
-
### `.for(id)` and `.forAll(ids)` chaining
|
|
69
|
-
|
|
70
|
-
Consistent API for targeting entities across select and update operations:
|
|
71
|
-
|
|
72
|
-
```typescript
|
|
73
|
-
// Single entity (result is unwrapped, not an array)
|
|
74
|
-
await Person.select((p) => p.name).for({ id: "..." });
|
|
75
|
-
await Person.select((p) => p.name).for("https://...");
|
|
76
|
-
|
|
77
|
-
// Multiple specific entities
|
|
78
|
-
await QueryBuilder.from(Person)
|
|
79
|
-
.select((p) => p.name)
|
|
80
|
-
.forAll([{ id: "..." }, { id: "..." }]);
|
|
81
|
-
|
|
82
|
-
// All instances (default — no .for() needed)
|
|
83
|
-
await Person.select((p) => p.name);
|
|
84
|
-
```
|
|
85
|
-
|
|
86
|
-
### Dynamic Query Building with `QueryBuilder` and `FieldSet`
|
|
87
|
-
|
|
88
|
-
Build queries programmatically at runtime — for CMS dashboards, API endpoints, configurable reports. See the [Dynamic Query Building](./README.md#dynamic-query-building) section in the README for full documentation and examples.
|
|
89
|
-
|
|
90
|
-
Key capabilities:
|
|
91
|
-
|
|
92
|
-
- `QueryBuilder.from(Person)` or `QueryBuilder.from('https://schema.org/Person')` — fluent, chainable, immutable query construction
|
|
93
|
-
- `FieldSet.for(Person, ['name', 'knows'])` — composable field selections with `.add()`, `.remove()`, `.pick()`, `FieldSet.merge()`
|
|
94
|
-
- `FieldSet.all(Person, {depth: 2})` — select all decorated properties with optional depth
|
|
95
|
-
- JSON serialization: `query.toJSON()` / `QueryBuilder.fromJSON(json)` and `fieldSet.toJSON()` / `FieldSet.fromJSON(json)`
|
|
96
|
-
- All builders are `PromiseLike` — `await` them directly or call `.build()` to inspect the IR
|
|
97
|
-
|
|
98
|
-
### Mutation Builders
|
|
99
|
-
|
|
100
|
-
`CreateBuilder`, `UpdateBuilder`, and `DeleteBuilder` provide the programmatic equivalent of `Person.create()`, `Person.update()`, and `Person.delete()`, accepting Shape classes or shape IRI strings. See the [Mutation Builders](./README.md#mutation-builders) section in the README.
|
|
101
|
-
|
|
102
|
-
### `PropertyPath` exported
|
|
103
|
-
|
|
104
|
-
The `PropertyPath` value object is now a public export — a type-safe representation of a sequence of property traversals through a shape graph.
|
|
105
|
-
|
|
106
|
-
```typescript
|
|
107
|
-
import { PropertyPath, walkPropertyPath } from "@_linked/core";
|
|
108
|
-
```
|
|
109
|
-
|
|
110
|
-
### `ShapeConstructor<S>` type
|
|
111
|
-
|
|
112
|
-
New concrete constructor type for Shape subclasses. Eliminates ~30 `as any` casts across the codebase and provides better type safety at runtime boundaries (builder `.from()` methods, Shape static methods).
|
|
113
|
-
|
|
114
|
-
## 1.3.0
|
|
3
|
+
## 2.2.0
|
|
115
4
|
|
|
116
5
|
### Minor Changes
|
|
117
6
|
|
|
118
|
-
- [#
|
|
119
|
-
|
|
120
|
-
**New:** `getQueryDispatch()` and `setQueryDispatch()` are now exported, allowing custom query dispatch implementations (e.g. for testing or alternative storage backends) without subclassing `LinkedStorage`.
|
|
121
|
-
|
|
122
|
-
## 1.2.1
|
|
7
|
+
- [#31](https://github.com/Semantu/linked/pull/31) [`eb88865`](https://github.com/Semantu/linked/commit/eb8886564f2c9663805c4308a834ca615f9a1dab) Thanks [@flyon](https://github.com/flyon)! - Properties in `select()` and `update()` now support expressions — you can compute values dynamically instead of just reading or writing raw fields.
|
|
123
8
|
|
|
124
|
-
###
|
|
9
|
+
### What's new
|
|
125
10
|
|
|
126
|
-
-
|
|
11
|
+
- **Computed fields in queries** — chain expression methods on properties to derive new values: string manipulation (`.strlen()`, `.ucase()`, `.concat()`), arithmetic (`.plus()`, `.times()`, `.abs()`), date extraction (`.year()`, `.month()`, `.hours()`), and comparisons (`.gt()`, `.eq()`, `.contains()`).
|
|
127
12
|
|
|
128
|
-
|
|
129
|
-
|
|
130
|
-
|
|
131
|
-
|
|
132
|
-
|
|
133
|
-
|
|
134
|
-
### Minor Changes
|
|
135
|
-
|
|
136
|
-
- [#9](https://github.com/Semantu/linked/pull/9) [`381067b`](https://github.com/Semantu/linked/commit/381067b0fbc25f4a0446c5f8cc0eec57ddded466) Thanks [@flyon](https://github.com/flyon)! - Replaced internal query representation with a canonical backend-agnostic IR AST. `SelectQuery`, `CreateQuery`, `UpdateQuery`, and `DeleteQuery` are now typed IR objects with `kind` discriminators, compact shape/property ID references, and expression trees — replacing the previous ad-hoc nested arrays. The public Shape DSL is unchanged; what changed is what `IQuadStore` implementations receive. Store result types (`ResultRow`, `SelectResult`, `CreateResult`, `UpdateResult`) are now exported. All factories expose `build()` as the primary method. See `documentation/intermediate-representation.md` for the full IR reference and migration guidance.
|
|
137
|
-
|
|
138
|
-
- [#14](https://github.com/Semantu/linked/pull/14) [`b65e156`](https://github.com/Semantu/linked/commit/b65e15688ac173478e58e1dbb9f26dbaf5fc5a37) Thanks [@flyon](https://github.com/flyon)! - Add SPARQL conversion layer — compiles Linked IR queries into executable SPARQL and maps results back to typed DSL objects.
|
|
139
|
-
|
|
140
|
-
**New exports from `@_linked/core/sparql`:**
|
|
141
|
-
|
|
142
|
-
- **`SparqlStore`** — abstract base class for SPARQL-backed stores. Extend it and implement two methods to connect any SPARQL 1.1 endpoint:
|
|
143
|
-
|
|
144
|
-
```ts
|
|
145
|
-
import { SparqlStore } from "@_linked/core/sparql";
|
|
146
|
-
|
|
147
|
-
class MyStore extends SparqlStore {
|
|
148
|
-
protected async executeSparqlSelect(
|
|
149
|
-
sparql: string
|
|
150
|
-
): Promise<SparqlJsonResults> {
|
|
151
|
-
/* ... */
|
|
152
|
-
}
|
|
153
|
-
protected async executeSparqlUpdate(sparql: string): Promise<void> {
|
|
154
|
-
/* ... */
|
|
155
|
-
}
|
|
156
|
-
}
|
|
13
|
+
```typescript
|
|
14
|
+
await Person.select((p) => ({
|
|
15
|
+
name: p.name,
|
|
16
|
+
nameLen: p.name.strlen(),
|
|
17
|
+
ageInMonths: p.age.times(12),
|
|
18
|
+
}));
|
|
157
19
|
```
|
|
158
20
|
|
|
159
|
-
- **
|
|
160
|
-
|
|
161
|
-
- `selectToSparql(query, options?)` — SelectQuery → SPARQL string
|
|
162
|
-
- `createToSparql(query, options?)` — CreateQuery → SPARQL string
|
|
163
|
-
- `updateToSparql(query, options?)` — UpdateQuery → SPARQL string
|
|
164
|
-
- `deleteToSparql(query, options?)` — DeleteQuery → SPARQL string
|
|
21
|
+
- **Expression-based WHERE filters** — filter using computed conditions, not just equality checks. Works on queries, updates, and deletes.
|
|
165
22
|
|
|
166
|
-
|
|
167
|
-
|
|
168
|
-
|
|
169
|
-
|
|
170
|
-
- `updateToAlgebra(query, options?)` — returns `SparqlDeleteInsertPlan`
|
|
171
|
-
- `deleteToAlgebra(query, options?)` — returns `SparqlDeleteInsertPlan`
|
|
23
|
+
```typescript
|
|
24
|
+
await Person.select((p) => p.name).where((p) => p.name.strlen().gt(5));
|
|
25
|
+
await Person.update({ verified: true }).where((p) => p.age.gte(18));
|
|
26
|
+
```
|
|
172
27
|
|
|
173
|
-
- **
|
|
28
|
+
- **Computed updates** — when updating data, calculate new values based on existing ones instead of providing static values. Pass a callback to `update()` to reference current field values.
|
|
174
29
|
|
|
175
|
-
|
|
176
|
-
|
|
30
|
+
```typescript
|
|
31
|
+
await Person.update((p) => ({ age: p.age.plus(1) })).for(entity);
|
|
32
|
+
await Person.update((p) => ({
|
|
33
|
+
label: p.firstName.concat(" ").concat(p.lastName),
|
|
34
|
+
})).for(entity);
|
|
35
|
+
```
|
|
177
36
|
|
|
178
|
-
-
|
|
37
|
+
- **`Expr` module** — for expressions that don't start from a property, like the current timestamp, conditional logic, or coalescing nulls.
|
|
179
38
|
|
|
180
|
-
|
|
181
|
-
|
|
182
|
-
|
|
39
|
+
```typescript
|
|
40
|
+
await Person.update({ lastSeen: Expr.now() }).for(entity);
|
|
41
|
+
await Person.select((p) => ({
|
|
42
|
+
displayName: Expr.firstDefined(p.nickname, p.name),
|
|
43
|
+
}));
|
|
44
|
+
```
|
|
183
45
|
|
|
184
|
-
|
|
46
|
+
Update expression callbacks are fully typed — `.plus()` only appears on number properties, `.strlen()` only on strings, etc.
|
|
185
47
|
|
|
186
|
-
|
|
48
|
+
### New exports
|
|
187
49
|
|
|
188
|
-
|
|
50
|
+
`ExpressionNode`, `Expr`, `ExpressionInput`, `PropertyRefMap`, `ExpressionUpdateProxy<S>`, `ExpressionUpdateResult<S>`, and per-type method interfaces (`NumericExpressionMethods`, `StringExpressionMethods`, `DateExpressionMethods`, `BooleanExpressionMethods`, `BaseExpressionMethods`).
|
|
189
51
|
|
|
190
|
-
See [
|
|
52
|
+
See the [README](./README.md#computed-expressions) for the full method reference and more examples.
|
|
191
53
|
|
|
192
54
|
## 1.1.0
|
|
193
55
|
|
package/README.md
CHANGED
|
@@ -56,14 +56,26 @@ Shape class → DSL query → IR (AST) → Target query language → Execute →
|
|
|
56
56
|
Shape classes use decorators to generate SHACL metadata. These shapes define the data model, drive the DSL's type safety, and can be synced to a store for runtime data validation.
|
|
57
57
|
|
|
58
58
|
```typescript
|
|
59
|
+
import {createNameSpace} from '@_linked/core/utils/NameSpace';
|
|
60
|
+
|
|
61
|
+
const ns = createNameSpace('https://example.org/');
|
|
62
|
+
|
|
63
|
+
// Example ontology references
|
|
64
|
+
const ex = {
|
|
65
|
+
Person: ns('Person'),
|
|
66
|
+
name: ns('name'),
|
|
67
|
+
knows: ns('knows'),
|
|
68
|
+
// ... rest of your ontology
|
|
69
|
+
};
|
|
70
|
+
|
|
59
71
|
@linkedShape
|
|
60
72
|
export class Person extends Shape {
|
|
61
|
-
static targetClass =
|
|
73
|
+
static targetClass = ex.Person;
|
|
62
74
|
|
|
63
|
-
@literalProperty({path:
|
|
75
|
+
@literalProperty({path: ex.name, maxCount: 1})
|
|
64
76
|
get name(): string { return ''; }
|
|
65
77
|
|
|
66
|
-
@objectProperty({path:
|
|
78
|
+
@objectProperty({path: ex.knows, shape: Person})
|
|
67
79
|
get friends(): ShapeSet<Person> { return null; }
|
|
68
80
|
}
|
|
69
81
|
```
|
|
@@ -202,19 +214,24 @@ import {literalProperty, objectProperty} from '@_linked/core/shapes/SHACL';
|
|
|
202
214
|
import {createNameSpace} from '@_linked/core/utils/NameSpace';
|
|
203
215
|
import {linkedShape} from './package';
|
|
204
216
|
|
|
205
|
-
const
|
|
206
|
-
|
|
207
|
-
|
|
208
|
-
const
|
|
217
|
+
const ns = createNameSpace('https://example.org/');
|
|
218
|
+
|
|
219
|
+
// Example ontology references
|
|
220
|
+
const ex = {
|
|
221
|
+
Person: ns('Person'),
|
|
222
|
+
name: ns('name'),
|
|
223
|
+
knows: ns('knows'),
|
|
224
|
+
// ... rest of your ontology
|
|
225
|
+
};
|
|
209
226
|
|
|
210
227
|
@linkedShape
|
|
211
228
|
export class Person extends Shape {
|
|
212
|
-
static targetClass =
|
|
229
|
+
static targetClass = ex.Person;
|
|
213
230
|
|
|
214
|
-
@literalProperty({path: name, required: true, maxCount: 1})
|
|
231
|
+
@literalProperty({path: ex.name, required: true, maxCount: 1})
|
|
215
232
|
declare name: string;
|
|
216
233
|
|
|
217
|
-
@objectProperty({path: knows, shape: Person})
|
|
234
|
+
@objectProperty({path: ex.knows, shape: Person})
|
|
218
235
|
declare knows: ShapeSet<Person>;
|
|
219
236
|
}
|
|
220
237
|
```
|
|
@@ -246,9 +263,7 @@ const allFriends = await Person.select((p) => p.knows.selectAll());
|
|
|
246
263
|
|
|
247
264
|
**3) Apply a simple mutation**
|
|
248
265
|
```typescript
|
|
249
|
-
const updated = await Person.update({
|
|
250
|
-
name: 'Alicia',
|
|
251
|
-
}).for({id: 'https://my.app/node1'});
|
|
266
|
+
const updated = await Person.update({name: 'Alicia'}).for({id: 'https://my.app/node1'});
|
|
252
267
|
/* updated: {id: string} & UpdatePartial<Person> */
|
|
253
268
|
```
|
|
254
269
|
|
|
@@ -290,11 +305,16 @@ The query DSL is schema-parameterized: you define your own SHACL shapes, and Lin
|
|
|
290
305
|
- Outer `where(...)` chaining
|
|
291
306
|
- Counting with `.size()`
|
|
292
307
|
- Custom result formats (object mapping)
|
|
308
|
+
- Computed values — derive new fields with arithmetic, string, date, and comparison methods
|
|
309
|
+
- Expression-based WHERE filters (`p.name.strlen().gt(5)`)
|
|
310
|
+
- Standalone expressions with `Expr` — timestamps, conditionals, null coalescing
|
|
293
311
|
- Type casting with `.as(Shape)`
|
|
312
|
+
- MINUS exclusion (by shape, property, condition, multi-property, nested path)
|
|
294
313
|
- Sorting, limiting, and `.one()`
|
|
295
314
|
- Query context variables
|
|
296
315
|
- Preloading (`preloadFor`) for component-like queries
|
|
297
|
-
- Create / Update / Delete mutations
|
|
316
|
+
- Create / Update / Delete mutations (including bulk and conditional)
|
|
317
|
+
- Expression-based updates (`p => ({age: p.age.plus(1)})`)
|
|
298
318
|
- Dynamic query building with `QueryBuilder`
|
|
299
319
|
- Composable field sets with `FieldSet`
|
|
300
320
|
- Mutation builders (`CreateBuilder`, `UpdateBuilder`, `DeleteBuilder`)
|
|
@@ -403,16 +423,122 @@ const custom = await Person.select((p) => ({
|
|
|
403
423
|
}));
|
|
404
424
|
```
|
|
405
425
|
|
|
426
|
+
#### Computed expressions
|
|
427
|
+
|
|
428
|
+
You can compute derived values directly in your queries — string manipulation, arithmetic, date extraction, and more. Expression methods chain naturally left-to-right.
|
|
429
|
+
|
|
430
|
+
```typescript
|
|
431
|
+
// String length as a computed field
|
|
432
|
+
const withLen = await Person.select((p) => ({
|
|
433
|
+
name: p.name,
|
|
434
|
+
nameLen: p.name.strlen(),
|
|
435
|
+
}));
|
|
436
|
+
|
|
437
|
+
// Arithmetic chaining (left-to-right, no hidden precedence)
|
|
438
|
+
const withAge = await Person.select((p) => ({
|
|
439
|
+
name: p.name,
|
|
440
|
+
ageInMonths: p.age.times(12),
|
|
441
|
+
agePlusTen: p.age.plus(10).times(2), // (age + 10) * 2
|
|
442
|
+
}));
|
|
443
|
+
|
|
444
|
+
// String manipulation
|
|
445
|
+
const upper = await Person.select((p) => ({
|
|
446
|
+
shout: p.name.ucase(),
|
|
447
|
+
greeting: p.name.concat(' says hello'),
|
|
448
|
+
}));
|
|
449
|
+
|
|
450
|
+
// Date extraction
|
|
451
|
+
const birthYear = await Person.select((p) => ({
|
|
452
|
+
year: p.birthDate.year(),
|
|
453
|
+
}));
|
|
454
|
+
```
|
|
455
|
+
|
|
456
|
+
**Expression methods by type:**
|
|
457
|
+
|
|
458
|
+
| Type | Methods |
|
|
459
|
+
|------|---------|
|
|
460
|
+
| **Numeric** | `plus`, `minus`, `times`, `divide`, `abs`, `round`, `ceil`, `floor`, `power` |
|
|
461
|
+
| **String** | `concat`, `contains`, `startsWith`, `endsWith`, `substr`, `before`, `after`, `replace`, `ucase`, `lcase`, `strlen`, `encodeForUri`, `matches` |
|
|
462
|
+
| **Date** | `year`, `month`, `day`, `hours`, `minutes`, `seconds`, `timezone`, `tz` |
|
|
463
|
+
| **Boolean** | `and`, `or`, `not` |
|
|
464
|
+
| **Comparison** | `eq`, `neq`, `gt`, `gte`, `lt`, `lte` |
|
|
465
|
+
| **Null-handling** | `isDefined`, `isNotDefined`, `defaultTo` |
|
|
466
|
+
| **Type** | `str`, `iri`, `isIri`, `isLiteral`, `isBlank`, `isNumeric`, `lang`, `datatype` |
|
|
467
|
+
| **Hash** | `md5`, `sha256`, `sha512` |
|
|
468
|
+
|
|
469
|
+
#### Expression-based WHERE filters
|
|
470
|
+
|
|
471
|
+
Expressions can be used in `where()` clauses for computed filtering:
|
|
472
|
+
|
|
473
|
+
```typescript
|
|
474
|
+
// Filter by string length
|
|
475
|
+
const longNames = await Person.select((p) => p.name)
|
|
476
|
+
.where((p) => p.name.strlen().gt(5));
|
|
477
|
+
|
|
478
|
+
// Filter by arithmetic
|
|
479
|
+
const young = await Person.select((p) => p.name)
|
|
480
|
+
.where((p) => p.age.plus(10).lt(100));
|
|
481
|
+
|
|
482
|
+
// Chain expressions with and/or
|
|
483
|
+
const filtered = await Person.select((p) => p.name)
|
|
484
|
+
.where((p) => p.name.strlen().gt(3).and(p.age.gt(18)));
|
|
485
|
+
|
|
486
|
+
// Expression WHERE on nested paths
|
|
487
|
+
const deep = await Person.select((p) => p.name)
|
|
488
|
+
.where((p) => p.bestFriend.name.strlen().gt(3));
|
|
489
|
+
|
|
490
|
+
// Expression WHERE on mutations
|
|
491
|
+
await Person.update({status: 'senior'}).where((p) => p.age.plus(10).gt(65));
|
|
492
|
+
```
|
|
493
|
+
|
|
494
|
+
#### `Expr` module
|
|
495
|
+
|
|
496
|
+
Some expressions don't belong to a specific property — like getting the current timestamp, picking the first non-null value, or conditional logic. Use the `Expr` module for these:
|
|
497
|
+
|
|
498
|
+
```typescript
|
|
499
|
+
import {Expr} from '@_linked/core';
|
|
500
|
+
|
|
501
|
+
// Current timestamp
|
|
502
|
+
const withTimestamp = await Person.update({lastSeen: Expr.now()}).for(entity);
|
|
503
|
+
|
|
504
|
+
// Conditional expressions
|
|
505
|
+
const labeled = await Person.select((p) => ({
|
|
506
|
+
label: Expr.ifThen(p.age.gt(18), 'adult', 'minor'),
|
|
507
|
+
}));
|
|
508
|
+
|
|
509
|
+
// First non-null value
|
|
510
|
+
const display = await Person.select((p) => ({
|
|
511
|
+
display: Expr.firstDefined(p.name, p.nickNames, Expr.str('Unknown')),
|
|
512
|
+
}));
|
|
513
|
+
```
|
|
514
|
+
|
|
406
515
|
#### Query As (type casting to a sub shape)
|
|
407
|
-
|
|
408
|
-
And you want to select properties of those pets that are dogs:
|
|
516
|
+
Cast to a subtype when you know the concrete shape — for example, selecting dog-specific properties from a pets collection:
|
|
409
517
|
```typescript
|
|
410
518
|
const guards = await Person.select((p) => p.pets.as(Dog).guardDogLevel);
|
|
411
519
|
```
|
|
412
520
|
|
|
521
|
+
#### MINUS (exclusion)
|
|
522
|
+
```typescript
|
|
523
|
+
// Exclude by shape — all Persons that are NOT also Employees
|
|
524
|
+
const nonEmployees = await Person.select((p) => p.name).minus(Employee);
|
|
525
|
+
|
|
526
|
+
// Exclude by property existence — Persons that do NOT have a hobby
|
|
527
|
+
const noHobby = await Person.select((p) => p.name).minus((p) => p.hobby);
|
|
528
|
+
|
|
529
|
+
// Exclude by multiple properties — Persons missing BOTH hobby AND nickNames
|
|
530
|
+
const sparse = await Person.select((p) => p.name).minus((p) => [p.hobby, p.nickNames]);
|
|
531
|
+
|
|
532
|
+
// Exclude by nested path — Persons whose bestFriend does NOT have a name
|
|
533
|
+
const unnamed = await Person.select((p) => p.name).minus((p) => [p.bestFriend.name]);
|
|
534
|
+
|
|
535
|
+
// Exclude by condition — Persons whose hobby is NOT 'Chess'
|
|
536
|
+
const noChess = await Person.select((p) => p.name).minus((p) => p.hobby.equals('Chess'));
|
|
537
|
+
```
|
|
538
|
+
|
|
413
539
|
#### Sorting, limiting, one
|
|
414
540
|
```typescript
|
|
415
|
-
const sorted = await Person.select((p) => p.name).
|
|
541
|
+
const sorted = await Person.select((p) => p.name).orderBy((p) => p.name, 'ASC');
|
|
416
542
|
const limited = await Person.select((p) => p.name).limit(1);
|
|
417
543
|
const single = await Person.select((p) => p.name).one();
|
|
418
544
|
```
|
|
@@ -447,8 +573,10 @@ Where UpdatePartial<Shape> reflects the created properties.
|
|
|
447
573
|
|
|
448
574
|
#### Update
|
|
449
575
|
|
|
450
|
-
Update will patch any property that you send as payload and leave the rest untouched.
|
|
576
|
+
Update will patch any property that you send as payload and leave the rest untouched. The data to update is required:
|
|
577
|
+
|
|
451
578
|
```typescript
|
|
579
|
+
// Target a specific entity with .for(id)
|
|
452
580
|
/* Result: {id: string} & UpdatePartial<Person> */
|
|
453
581
|
const updated = await Person.update({name: 'Alicia'}).for({id: 'https://my.app/node1'});
|
|
454
582
|
```
|
|
@@ -460,6 +588,38 @@ Returns:
|
|
|
460
588
|
}
|
|
461
589
|
```
|
|
462
590
|
|
|
591
|
+
**Expression-based updates:**
|
|
592
|
+
|
|
593
|
+
Instead of static values, you can compute new values from existing ones. Pass a callback to reference the entity's current properties:
|
|
594
|
+
|
|
595
|
+
```typescript
|
|
596
|
+
// Increment age by 1
|
|
597
|
+
await Person.update((p) => ({age: p.age.plus(1)})).for({id: 'https://my.app/node1'});
|
|
598
|
+
|
|
599
|
+
// Uppercase a name
|
|
600
|
+
await Person.update((p) => ({name: p.name.ucase()})).for({id: 'https://my.app/node1'});
|
|
601
|
+
|
|
602
|
+
// Reference related entity properties
|
|
603
|
+
await Person.update((p) => ({hobby: p.bestFriend.name.ucase()})).for({id: 'https://my.app/node1'});
|
|
604
|
+
|
|
605
|
+
// Mix literals and expressions
|
|
606
|
+
await Person.update((p) => ({name: 'Bob', age: p.age.plus(1)})).for({id: 'https://my.app/node1'});
|
|
607
|
+
|
|
608
|
+
// Use Expr module values directly in plain objects
|
|
609
|
+
await Person.update({lastSeen: Expr.now()}).for({id: 'https://my.app/node1'});
|
|
610
|
+
```
|
|
611
|
+
|
|
612
|
+
The callback is type-safe — `.plus()` only appears on number properties, `.ucase()` only on strings, etc.
|
|
613
|
+
|
|
614
|
+
**Conditional and bulk updates:**
|
|
615
|
+
```typescript
|
|
616
|
+
// Update all matching entities
|
|
617
|
+
const archived = await Person.update({status: 'archived'}).where(p => p.status.equals('inactive'));
|
|
618
|
+
|
|
619
|
+
// Update all instances of a shape
|
|
620
|
+
await Person.update({verified: true}).forAll();
|
|
621
|
+
```
|
|
622
|
+
|
|
463
623
|
**Updating multi-value properties**
|
|
464
624
|
When updating a property that holds multiple values (one that returns an array in the results), you can either overwrite all the values with a new explicit array of values, or delete from/add to the current values.
|
|
465
625
|
|
|
@@ -503,27 +663,19 @@ This returns an object with the added and removed items
|
|
|
503
663
|
|
|
504
664
|
|
|
505
665
|
#### Delete
|
|
506
|
-
To delete a node entirely:
|
|
507
666
|
|
|
508
667
|
```typescript
|
|
509
|
-
|
|
668
|
+
// Delete a single node
|
|
510
669
|
const deleted = await Person.delete({id: 'https://my.app/node1'});
|
|
511
|
-
```
|
|
512
|
-
Returns
|
|
513
|
-
```json
|
|
514
|
-
{
|
|
515
|
-
deleted:[
|
|
516
|
-
{id:"https://my.app/node1"}
|
|
517
|
-
],
|
|
518
|
-
count:1
|
|
519
|
-
}
|
|
520
|
-
```
|
|
521
670
|
|
|
522
|
-
|
|
671
|
+
// Delete multiple nodes
|
|
672
|
+
const deleted = await Person.delete([{id: 'https://my.app/node1'}, {id: 'https://my.app/node2'}]);
|
|
523
673
|
|
|
524
|
-
|
|
525
|
-
|
|
526
|
-
|
|
674
|
+
// Delete all instances of a shape (with blank node cleanup)
|
|
675
|
+
await Person.deleteAll();
|
|
676
|
+
|
|
677
|
+
// Conditional delete
|
|
678
|
+
await Person.deleteWhere(p => p.status.equals('inactive'));
|
|
527
679
|
```
|
|
528
680
|
|
|
529
681
|
|
|
@@ -534,23 +686,17 @@ This example assumes `Person` from the `Shapes` section above.
|
|
|
534
686
|
|
|
535
687
|
```typescript
|
|
536
688
|
import {literalProperty} from '@_linked/core/shapes/SHACL';
|
|
537
|
-
import {createNameSpace} from '@_linked/core/utils/NameSpace';
|
|
538
689
|
import {linkedShape} from './package';
|
|
539
690
|
|
|
540
|
-
const schema = createNameSpace('https://schema.org/');
|
|
541
|
-
const EmployeeClass = schema('Employee');
|
|
542
|
-
const name = schema('name');
|
|
543
|
-
const employeeId = schema('employeeId');
|
|
544
|
-
|
|
545
691
|
@linkedShape
|
|
546
692
|
export class Employee extends Person {
|
|
547
|
-
static targetClass =
|
|
693
|
+
static targetClass = ex.Employee;
|
|
548
694
|
|
|
549
695
|
// Override inherited "name" with stricter constraints (still maxCount: 1)
|
|
550
|
-
@literalProperty({path: name, required: true, minLength: 2, maxCount: 1})
|
|
696
|
+
@literalProperty({path: ex.name, required: true, minLength: 2, maxCount: 1})
|
|
551
697
|
declare name: string;
|
|
552
698
|
|
|
553
|
-
@literalProperty({path: employeeId, required: true, maxCount: 1})
|
|
699
|
+
@literalProperty({path: ex.employeeId, required: true, maxCount: 1})
|
|
554
700
|
declare employeeId: string;
|
|
555
701
|
}
|
|
556
702
|
```
|
|
@@ -716,8 +862,14 @@ const updated = await UpdateBuilder.from(Person)
|
|
|
716
862
|
.for({id: 'https://my.app/alice'})
|
|
717
863
|
.set({name: 'Alicia'});
|
|
718
864
|
|
|
719
|
-
// Delete — equivalent to Person.delete({id: '...'})
|
|
720
|
-
const deleted = await DeleteBuilder.from(Person
|
|
865
|
+
// Delete by ID — equivalent to Person.delete({id: '...'})
|
|
866
|
+
const deleted = await DeleteBuilder.from(Person, {id: 'https://my.app/alice'});
|
|
867
|
+
|
|
868
|
+
// Delete all — equivalent to Person.deleteAll()
|
|
869
|
+
await DeleteBuilder.from(Person).all();
|
|
870
|
+
|
|
871
|
+
// Conditional update — equivalent to Person.update({...}).where(fn)
|
|
872
|
+
await UpdateBuilder.from(Person).set({verified: true}).forAll();
|
|
721
873
|
|
|
722
874
|
// All builders are PromiseLike — await them or call .build() for the IR
|
|
723
875
|
const ir = CreateBuilder.from(Person).set({name: 'Alice'}).build();
|
|
@@ -0,0 +1,58 @@
|
|
|
1
|
+
import { ExpressionNode } from './ExpressionNode.js';
|
|
2
|
+
import type { ExpressionInput } from './ExpressionNode.js';
|
|
3
|
+
export declare const Expr: {
|
|
4
|
+
readonly plus: (a: ExpressionInput, b: ExpressionInput) => ExpressionNode;
|
|
5
|
+
readonly minus: (a: ExpressionInput, b: ExpressionInput) => ExpressionNode;
|
|
6
|
+
readonly times: (a: ExpressionInput, b: ExpressionInput) => ExpressionNode;
|
|
7
|
+
readonly divide: (a: ExpressionInput, b: ExpressionInput) => ExpressionNode;
|
|
8
|
+
readonly abs: (a: ExpressionInput) => ExpressionNode;
|
|
9
|
+
readonly round: (a: ExpressionInput) => ExpressionNode;
|
|
10
|
+
readonly ceil: (a: ExpressionInput) => ExpressionNode;
|
|
11
|
+
readonly floor: (a: ExpressionInput) => ExpressionNode;
|
|
12
|
+
readonly power: (a: ExpressionInput, b: number) => ExpressionNode;
|
|
13
|
+
readonly eq: (a: ExpressionInput, b: ExpressionInput) => ExpressionNode;
|
|
14
|
+
readonly neq: (a: ExpressionInput, b: ExpressionInput) => ExpressionNode;
|
|
15
|
+
readonly gt: (a: ExpressionInput, b: ExpressionInput) => ExpressionNode;
|
|
16
|
+
readonly gte: (a: ExpressionInput, b: ExpressionInput) => ExpressionNode;
|
|
17
|
+
readonly lt: (a: ExpressionInput, b: ExpressionInput) => ExpressionNode;
|
|
18
|
+
readonly lte: (a: ExpressionInput, b: ExpressionInput) => ExpressionNode;
|
|
19
|
+
readonly concat: (...parts: ExpressionInput[]) => ExpressionNode;
|
|
20
|
+
readonly contains: (a: ExpressionInput, b: ExpressionInput) => ExpressionNode;
|
|
21
|
+
readonly startsWith: (a: ExpressionInput, b: ExpressionInput) => ExpressionNode;
|
|
22
|
+
readonly endsWith: (a: ExpressionInput, b: ExpressionInput) => ExpressionNode;
|
|
23
|
+
readonly substr: (a: ExpressionInput, start: number, len?: number) => ExpressionNode;
|
|
24
|
+
readonly before: (a: ExpressionInput, b: ExpressionInput) => ExpressionNode;
|
|
25
|
+
readonly after: (a: ExpressionInput, b: ExpressionInput) => ExpressionNode;
|
|
26
|
+
readonly replace: (a: ExpressionInput, pat: string, rep: string, flags?: string) => ExpressionNode;
|
|
27
|
+
readonly ucase: (a: ExpressionInput) => ExpressionNode;
|
|
28
|
+
readonly lcase: (a: ExpressionInput) => ExpressionNode;
|
|
29
|
+
readonly strlen: (a: ExpressionInput) => ExpressionNode;
|
|
30
|
+
readonly encodeForUri: (a: ExpressionInput) => ExpressionNode;
|
|
31
|
+
readonly regex: (a: ExpressionInput, pat: string, flags?: string) => ExpressionNode;
|
|
32
|
+
readonly now: () => ExpressionNode;
|
|
33
|
+
readonly year: (a: ExpressionInput) => ExpressionNode;
|
|
34
|
+
readonly month: (a: ExpressionInput) => ExpressionNode;
|
|
35
|
+
readonly day: (a: ExpressionInput) => ExpressionNode;
|
|
36
|
+
readonly hours: (a: ExpressionInput) => ExpressionNode;
|
|
37
|
+
readonly minutes: (a: ExpressionInput) => ExpressionNode;
|
|
38
|
+
readonly seconds: (a: ExpressionInput) => ExpressionNode;
|
|
39
|
+
readonly timezone: (a: ExpressionInput) => ExpressionNode;
|
|
40
|
+
readonly tz: (a: ExpressionInput) => ExpressionNode;
|
|
41
|
+
readonly and: (a: ExpressionInput, b: ExpressionInput) => ExpressionNode;
|
|
42
|
+
readonly or: (a: ExpressionInput, b: ExpressionInput) => ExpressionNode;
|
|
43
|
+
readonly not: (a: ExpressionInput) => ExpressionNode;
|
|
44
|
+
readonly firstDefined: (...args: ExpressionInput[]) => ExpressionNode;
|
|
45
|
+
readonly ifThen: (cond: ExpressionInput, thenVal: ExpressionInput, elseVal: ExpressionInput) => ExpressionNode;
|
|
46
|
+
readonly bound: (a: ExpressionInput) => ExpressionNode;
|
|
47
|
+
readonly lang: (a: ExpressionInput) => ExpressionNode;
|
|
48
|
+
readonly datatype: (a: ExpressionInput) => ExpressionNode;
|
|
49
|
+
readonly str: (a: ExpressionInput) => ExpressionNode;
|
|
50
|
+
readonly iri: (a: ExpressionInput) => ExpressionNode;
|
|
51
|
+
readonly isIri: (a: ExpressionInput) => ExpressionNode;
|
|
52
|
+
readonly isLiteral: (a: ExpressionInput) => ExpressionNode;
|
|
53
|
+
readonly isBlank: (a: ExpressionInput) => ExpressionNode;
|
|
54
|
+
readonly isNumeric: (a: ExpressionInput) => ExpressionNode;
|
|
55
|
+
readonly md5: (a: ExpressionInput) => ExpressionNode;
|
|
56
|
+
readonly sha256: (a: ExpressionInput) => ExpressionNode;
|
|
57
|
+
readonly sha512: (a: ExpressionInput) => ExpressionNode;
|
|
58
|
+
};
|