@_linked/core 2.0.1 → 2.2.0-next.20260313111301
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 -198
- package/README.md +147 -21
- 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/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 +2 -2
- package/lib/cjs/queries/IRCanonicalize.js +4 -0
- package/lib/cjs/queries/IRCanonicalize.js.map +1 -1
- package/lib/cjs/queries/IRDesugar.d.ts +11 -2
- package/lib/cjs/queries/IRDesugar.js +14 -0
- package/lib/cjs/queries/IRDesugar.js.map +1 -1
- package/lib/cjs/queries/IRLower.d.ts +9 -0
- package/lib/cjs/queries/IRLower.js +42 -60
- package/lib/cjs/queries/IRLower.js.map +1 -1
- package/lib/cjs/queries/IRMutation.d.ts +11 -1
- package/lib/cjs/queries/IRMutation.js +45 -21
- package/lib/cjs/queries/IRMutation.js.map +1 -1
- package/lib/cjs/queries/IntermediateRepresentation.d.ts +9 -2
- package/lib/cjs/queries/MutationQuery.js +16 -2
- package/lib/cjs/queries/MutationQuery.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 +2 -0
- package/lib/cjs/queries/UpdateBuilder.js +0 -1
- package/lib/cjs/queries/UpdateBuilder.js.map +1 -1
- package/lib/cjs/shapes/Shape.d.ts +2 -0
- package/lib/cjs/shapes/Shape.js +0 -8
- package/lib/cjs/shapes/Shape.js.map +1 -1
- package/lib/cjs/sparql/irToAlgebra.js +99 -9
- package/lib/cjs/sparql/irToAlgebra.js.map +1 -1
- package/lib/cjs/test-helpers/query-fixtures.d.ts +64 -1624
- package/lib/cjs/test-helpers/query-fixtures.js +65 -17
- 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/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 +2 -2
- package/lib/esm/queries/IRCanonicalize.js +4 -0
- package/lib/esm/queries/IRCanonicalize.js.map +1 -1
- package/lib/esm/queries/IRDesugar.d.ts +11 -2
- package/lib/esm/queries/IRDesugar.js +14 -0
- package/lib/esm/queries/IRDesugar.js.map +1 -1
- package/lib/esm/queries/IRLower.d.ts +9 -0
- package/lib/esm/queries/IRLower.js +41 -60
- package/lib/esm/queries/IRLower.js.map +1 -1
- package/lib/esm/queries/IRMutation.d.ts +11 -1
- package/lib/esm/queries/IRMutation.js +44 -21
- package/lib/esm/queries/IRMutation.js.map +1 -1
- package/lib/esm/queries/IntermediateRepresentation.d.ts +9 -2
- package/lib/esm/queries/MutationQuery.js +16 -2
- package/lib/esm/queries/MutationQuery.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 +2 -0
- package/lib/esm/queries/UpdateBuilder.js +0 -1
- package/lib/esm/queries/UpdateBuilder.js.map +1 -1
- package/lib/esm/shapes/Shape.d.ts +2 -0
- package/lib/esm/shapes/Shape.js +0 -8
- package/lib/esm/shapes/Shape.js.map +1 -1
- package/lib/esm/sparql/irToAlgebra.js +99 -9
- package/lib/esm/sparql/irToAlgebra.js.map +1 -1
- package/lib/esm/test-helpers/query-fixtures.d.ts +64 -1624
- package/lib/esm/test-helpers/query-fixtures.js +65 -17
- package/lib/esm/test-helpers/query-fixtures.js.map +1 -1
- package/package.json +3 -3
package/CHANGELOG.md
CHANGED
|
@@ -1,220 +1,55 @@
|
|
|
1
1
|
# Changelog
|
|
2
2
|
|
|
3
|
-
## 2.0
|
|
4
|
-
|
|
5
|
-
### Patch Changes
|
|
6
|
-
|
|
7
|
-
- [#27](https://github.com/Semantu/linked/pull/27) [`d3c1e91`](https://github.com/Semantu/linked/commit/d3c1e918b2a63240ddbf3cb550ec43fa1e019c35) Thanks [@flyon](https://github.com/flyon)! - Add MINUS support on QueryBuilder with multiple call styles:
|
|
8
|
-
|
|
9
|
-
- `.minus(Shape)` — exclude by shape type
|
|
10
|
-
- `.minus(p => p.prop.equals(val))` — exclude by condition
|
|
11
|
-
- `.minus(p => p.prop)` — exclude by property existence
|
|
12
|
-
- `.minus(p => [p.prop1, p.nested.prop2])` — exclude by multi-property existence with nested path support
|
|
13
|
-
|
|
14
|
-
Add bulk delete operations:
|
|
15
|
-
|
|
16
|
-
- `Shape.deleteAll()` / `DeleteBuilder.from(Shape).all()` — delete all instances with schema-aware blank node cleanup
|
|
17
|
-
- `Shape.deleteWhere(fn)` / `DeleteBuilder.from(Shape).where(fn)` — conditional delete
|
|
18
|
-
|
|
19
|
-
Add conditional update operations:
|
|
20
|
-
|
|
21
|
-
- `.update(data).where(fn)` — update matching instances
|
|
22
|
-
- `.update(data).forAll()` — update all instances
|
|
23
|
-
|
|
24
|
-
API cleanup:
|
|
25
|
-
|
|
26
|
-
- Deprecate `sortBy()` in favor of `orderBy()`
|
|
27
|
-
- Remove `DeleteBuilder.for()` — use `DeleteBuilder.from(shape, ids)` instead
|
|
28
|
-
- Require `data` parameter in `Shape.update(data)`
|
|
29
|
-
|
|
30
|
-
## 2.0.0
|
|
31
|
-
|
|
32
|
-
### Major Changes
|
|
33
|
-
|
|
34
|
-
- [#23](https://github.com/Semantu/linked/pull/23) [`d2d1eca`](https://github.com/Semantu/linked/commit/d2d1eca3517af11f39348dc83ba5e60703ef86d2) Thanks [@flyon](https://github.com/flyon)! - ## Breaking Changes
|
|
35
|
-
|
|
36
|
-
### `Shape.select()` and `Shape.update()` no longer accept an ID as the first argument
|
|
37
|
-
|
|
38
|
-
Use `.for(id)` to target a specific entity instead.
|
|
39
|
-
|
|
40
|
-
**Select:**
|
|
41
|
-
|
|
42
|
-
```typescript
|
|
43
|
-
// Before
|
|
44
|
-
const result = await Person.select({ id: "..." }, (p) => p.name);
|
|
45
|
-
|
|
46
|
-
// After
|
|
47
|
-
const result = await Person.select((p) => p.name).for({ id: "..." });
|
|
48
|
-
```
|
|
49
|
-
|
|
50
|
-
`.for(id)` unwraps the result type from array to single object, matching the old single-subject overload behavior.
|
|
51
|
-
|
|
52
|
-
**Update:**
|
|
53
|
-
|
|
54
|
-
```typescript
|
|
55
|
-
// Before
|
|
56
|
-
const result = await Person.update({ id: "..." }, { name: "Alice" });
|
|
57
|
-
|
|
58
|
-
// After
|
|
59
|
-
const result = await Person.update({ name: "Alice" }).for({ id: "..." });
|
|
60
|
-
```
|
|
61
|
-
|
|
62
|
-
`Shape.selectAll(id)` also no longer accepts an id — use `Person.selectAll().for(id)`.
|
|
63
|
-
|
|
64
|
-
### `ShapeType` renamed to `ShapeConstructor`
|
|
65
|
-
|
|
66
|
-
The type alias for concrete Shape subclass constructors has been renamed. Update any imports or references:
|
|
67
|
-
|
|
68
|
-
```typescript
|
|
69
|
-
// Before
|
|
70
|
-
import type { ShapeType } from "@_linked/core/shapes/Shape";
|
|
71
|
-
|
|
72
|
-
// After
|
|
73
|
-
import type { ShapeConstructor } from "@_linked/core/shapes/Shape";
|
|
74
|
-
```
|
|
75
|
-
|
|
76
|
-
### `QueryString`, `QueryNumber`, `QueryBoolean`, `QueryDate` classes removed
|
|
77
|
-
|
|
78
|
-
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.
|
|
79
|
-
|
|
80
|
-
### Internal IR types removed
|
|
81
|
-
|
|
82
|
-
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`):
|
|
83
|
-
|
|
84
|
-
- Types: `SelectPath`, `QueryPath`, `CustomQueryObject`, `SubQueryPaths`, `ComponentQueryPath`
|
|
85
|
-
- Functions: `fieldSetToSelectPath()`, `entryToQueryPath()`
|
|
86
|
-
- Methods: `QueryBuilder.getQueryPaths()`, `BoundComponent.getComponentQueryPaths()`
|
|
87
|
-
- `RawSelectInput.select` field renamed to `RawSelectInput.entries` (type changed from `SelectPath` to `FieldSetEntry[]`)
|
|
88
|
-
|
|
89
|
-
### `getPackageShape()` return type is now nullable
|
|
90
|
-
|
|
91
|
-
Returns `ShapeConstructor | undefined` instead of `typeof Shape`. Code that didn't null-check the return value will now get TypeScript errors.
|
|
92
|
-
|
|
93
|
-
## New Features
|
|
94
|
-
|
|
95
|
-
### `.for(id)` and `.forAll(ids)` chaining
|
|
96
|
-
|
|
97
|
-
Consistent API for targeting entities across select and update operations:
|
|
98
|
-
|
|
99
|
-
```typescript
|
|
100
|
-
// Single entity (result is unwrapped, not an array)
|
|
101
|
-
await Person.select((p) => p.name).for({ id: "..." });
|
|
102
|
-
await Person.select((p) => p.name).for("https://...");
|
|
103
|
-
|
|
104
|
-
// Multiple specific entities
|
|
105
|
-
await QueryBuilder.from(Person)
|
|
106
|
-
.select((p) => p.name)
|
|
107
|
-
.forAll([{ id: "..." }, { id: "..." }]);
|
|
108
|
-
|
|
109
|
-
// All instances (default — no .for() needed)
|
|
110
|
-
await Person.select((p) => p.name);
|
|
111
|
-
```
|
|
112
|
-
|
|
113
|
-
### Dynamic Query Building with `QueryBuilder` and `FieldSet`
|
|
114
|
-
|
|
115
|
-
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.
|
|
116
|
-
|
|
117
|
-
Key capabilities:
|
|
118
|
-
|
|
119
|
-
- `QueryBuilder.from(Person)` or `QueryBuilder.from('https://schema.org/Person')` — fluent, chainable, immutable query construction
|
|
120
|
-
- `FieldSet.for(Person, ['name', 'knows'])` — composable field selections with `.add()`, `.remove()`, `.pick()`, `FieldSet.merge()`
|
|
121
|
-
- `FieldSet.all(Person, {depth: 2})` — select all decorated properties with optional depth
|
|
122
|
-
- JSON serialization: `query.toJSON()` / `QueryBuilder.fromJSON(json)` and `fieldSet.toJSON()` / `FieldSet.fromJSON(json)`
|
|
123
|
-
- All builders are `PromiseLike` — `await` them directly or call `.build()` to inspect the IR
|
|
124
|
-
|
|
125
|
-
### Mutation Builders
|
|
126
|
-
|
|
127
|
-
`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.
|
|
128
|
-
|
|
129
|
-
### `PropertyPath` exported
|
|
130
|
-
|
|
131
|
-
The `PropertyPath` value object is now a public export — a type-safe representation of a sequence of property traversals through a shape graph.
|
|
132
|
-
|
|
133
|
-
```typescript
|
|
134
|
-
import { PropertyPath, walkPropertyPath } from "@_linked/core";
|
|
135
|
-
```
|
|
136
|
-
|
|
137
|
-
### `ShapeConstructor<S>` type
|
|
138
|
-
|
|
139
|
-
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).
|
|
140
|
-
|
|
141
|
-
## 1.3.0
|
|
142
|
-
|
|
143
|
-
### Minor Changes
|
|
144
|
-
|
|
145
|
-
- [#20](https://github.com/Semantu/linked/pull/20) [`33e9fb0`](https://github.com/Semantu/linked/commit/33e9fb0205343eca8c84723cbabc3f3342e40be5) Thanks [@flyon](https://github.com/flyon)! - **Breaking:** `QueryParser` has been removed. If you imported `QueryParser` directly, replace with `getQueryDispatch()` from `@_linked/core/queries/queryDispatch`. The Shape DSL (`Shape.select()`, `.create()`, `.update()`, `.delete()`) and `SelectQuery.exec()` are unchanged.
|
|
146
|
-
|
|
147
|
-
**New:** `getQueryDispatch()` and `setQueryDispatch()` are now exported, allowing custom query dispatch implementations (e.g. for testing or alternative storage backends) without subclassing `LinkedStorage`.
|
|
148
|
-
|
|
149
|
-
## 1.2.1
|
|
150
|
-
|
|
151
|
-
### Patch Changes
|
|
152
|
-
|
|
153
|
-
- [#17](https://github.com/Semantu/linked/pull/17) [`0654780`](https://github.com/Semantu/linked/commit/06547807a7bae56e992eba73263f83e092b7788b) Thanks [@flyon](https://github.com/flyon)! - Preserve nested array sub-select branches in canonical IR so `build()` emits complete traversals, projection fields, and `resultMap` entries for nested selections.
|
|
154
|
-
|
|
155
|
-
This fixes cases where nested branches present in `toRawInput().select` were dropped during desugar/lowering (for example nested `friends.select([name, hobby])` branches under another sub-select).
|
|
156
|
-
|
|
157
|
-
Also adds regression coverage for desugar preservation, IR lowering completeness, and updated SPARQL golden output for nested query fixtures.
|
|
158
|
-
|
|
159
|
-
## 1.2.0
|
|
3
|
+
## 2.2.0
|
|
160
4
|
|
|
161
5
|
### Minor Changes
|
|
162
6
|
|
|
163
|
-
- [#
|
|
164
|
-
|
|
165
|
-
- [#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.
|
|
166
|
-
|
|
167
|
-
**New exports from `@_linked/core/sparql`:**
|
|
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.
|
|
168
8
|
|
|
169
|
-
|
|
9
|
+
### What's new
|
|
170
10
|
|
|
171
|
-
|
|
172
|
-
import { SparqlStore } from "@_linked/core/sparql";
|
|
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()`).
|
|
173
12
|
|
|
174
|
-
|
|
175
|
-
|
|
176
|
-
|
|
177
|
-
|
|
178
|
-
|
|
179
|
-
|
|
180
|
-
protected async executeSparqlUpdate(sparql: string): Promise<void> {
|
|
181
|
-
/* ... */
|
|
182
|
-
}
|
|
183
|
-
}
|
|
13
|
+
```typescript
|
|
14
|
+
await Person.select((p) => ({
|
|
15
|
+
name: p.name,
|
|
16
|
+
nameLen: p.name.strlen(),
|
|
17
|
+
ageInMonths: p.age.times(12),
|
|
18
|
+
}));
|
|
184
19
|
```
|
|
185
20
|
|
|
186
|
-
- **
|
|
187
|
-
|
|
188
|
-
- `selectToSparql(query, options?)` — SelectQuery → SPARQL string
|
|
189
|
-
- `createToSparql(query, options?)` — CreateQuery → SPARQL string
|
|
190
|
-
- `updateToSparql(query, options?)` — UpdateQuery → SPARQL string
|
|
191
|
-
- `deleteToSparql(query, options?)` — DeleteQuery → SPARQL string
|
|
192
|
-
|
|
193
|
-
- **IR → SPARQL algebra** (for stores that want to inspect/optimize the algebra before serialization):
|
|
21
|
+
- **Expression-based WHERE filters** — filter using computed conditions, not just equality checks. Works on queries, updates, and deletes.
|
|
194
22
|
|
|
195
|
-
|
|
196
|
-
|
|
197
|
-
|
|
198
|
-
|
|
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
|
+
```
|
|
199
27
|
|
|
200
|
-
- **
|
|
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.
|
|
201
29
|
|
|
202
|
-
|
|
203
|
-
|
|
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
|
+
```
|
|
204
36
|
|
|
205
|
-
-
|
|
37
|
+
- **`Expr` module** — for expressions that don't start from a property, like the current timestamp, conditional logic, or coalescing nulls.
|
|
206
38
|
|
|
207
|
-
|
|
208
|
-
|
|
209
|
-
|
|
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
|
+
```
|
|
210
45
|
|
|
211
|
-
|
|
46
|
+
Update expression callbacks are fully typed — `.plus()` only appears on number properties, `.strlen()` only on strings, etc.
|
|
212
47
|
|
|
213
|
-
|
|
48
|
+
### New exports
|
|
214
49
|
|
|
215
|
-
|
|
50
|
+
`ExpressionNode`, `Expr`, `ExpressionInput`, `PropertyRefMap`, `ExpressionUpdateProxy<S>`, `ExpressionUpdateResult<S>`, and per-type method interfaces (`NumericExpressionMethods`, `StringExpressionMethods`, `DateExpressionMethods`, `BooleanExpressionMethods`, `BaseExpressionMethods`).
|
|
216
51
|
|
|
217
|
-
See [
|
|
52
|
+
See the [README](./README.md#computed-expressions) for the full method reference and more examples.
|
|
218
53
|
|
|
219
54
|
## 1.1.0
|
|
220
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
|
```
|
|
@@ -288,12 +305,16 @@ The query DSL is schema-parameterized: you define your own SHACL shapes, and Lin
|
|
|
288
305
|
- Outer `where(...)` chaining
|
|
289
306
|
- Counting with `.size()`
|
|
290
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
|
|
291
311
|
- Type casting with `.as(Shape)`
|
|
292
312
|
- MINUS exclusion (by shape, property, condition, multi-property, nested path)
|
|
293
313
|
- Sorting, limiting, and `.one()`
|
|
294
314
|
- Query context variables
|
|
295
315
|
- Preloading (`preloadFor`) for component-like queries
|
|
296
316
|
- Create / Update / Delete mutations (including bulk and conditional)
|
|
317
|
+
- Expression-based updates (`p => ({age: p.age.plus(1)})`)
|
|
297
318
|
- Dynamic query building with `QueryBuilder`
|
|
298
319
|
- Composable field sets with `FieldSet`
|
|
299
320
|
- Mutation builders (`CreateBuilder`, `UpdateBuilder`, `DeleteBuilder`)
|
|
@@ -402,9 +423,97 @@ const custom = await Person.select((p) => ({
|
|
|
402
423
|
}));
|
|
403
424
|
```
|
|
404
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
|
+
|
|
405
515
|
#### Query As (type casting to a sub shape)
|
|
406
|
-
|
|
407
|
-
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:
|
|
408
517
|
```typescript
|
|
409
518
|
const guards = await Person.select((p) => p.pets.as(Dog).guardDogLevel);
|
|
410
519
|
```
|
|
@@ -479,6 +588,29 @@ Returns:
|
|
|
479
588
|
}
|
|
480
589
|
```
|
|
481
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
|
+
|
|
482
614
|
**Conditional and bulk updates:**
|
|
483
615
|
```typescript
|
|
484
616
|
// Update all matching entities
|
|
@@ -554,23 +686,17 @@ This example assumes `Person` from the `Shapes` section above.
|
|
|
554
686
|
|
|
555
687
|
```typescript
|
|
556
688
|
import {literalProperty} from '@_linked/core/shapes/SHACL';
|
|
557
|
-
import {createNameSpace} from '@_linked/core/utils/NameSpace';
|
|
558
689
|
import {linkedShape} from './package';
|
|
559
690
|
|
|
560
|
-
const schema = createNameSpace('https://schema.org/');
|
|
561
|
-
const EmployeeClass = schema('Employee');
|
|
562
|
-
const name = schema('name');
|
|
563
|
-
const employeeId = schema('employeeId');
|
|
564
|
-
|
|
565
691
|
@linkedShape
|
|
566
692
|
export class Employee extends Person {
|
|
567
|
-
static targetClass =
|
|
693
|
+
static targetClass = ex.Employee;
|
|
568
694
|
|
|
569
695
|
// Override inherited "name" with stricter constraints (still maxCount: 1)
|
|
570
|
-
@literalProperty({path: name, required: true, minLength: 2, maxCount: 1})
|
|
696
|
+
@literalProperty({path: ex.name, required: true, minLength: 2, maxCount: 1})
|
|
571
697
|
declare name: string;
|
|
572
698
|
|
|
573
|
-
@literalProperty({path: employeeId, required: true, maxCount: 1})
|
|
699
|
+
@literalProperty({path: ex.employeeId, required: true, maxCount: 1})
|
|
574
700
|
declare employeeId: string;
|
|
575
701
|
}
|
|
576
702
|
```
|
|
@@ -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
|
+
};
|