@_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.
Files changed (95) hide show
  1. package/CHANGELOG.md +33 -198
  2. package/README.md +147 -21
  3. package/lib/cjs/expressions/Expr.d.ts +58 -0
  4. package/lib/cjs/expressions/Expr.js +217 -0
  5. package/lib/cjs/expressions/Expr.js.map +1 -0
  6. package/lib/cjs/expressions/ExpressionMethods.d.ts +81 -0
  7. package/lib/cjs/expressions/ExpressionMethods.js +3 -0
  8. package/lib/cjs/expressions/ExpressionMethods.js.map +1 -0
  9. package/lib/cjs/expressions/ExpressionNode.d.ts +95 -0
  10. package/lib/cjs/expressions/ExpressionNode.js +349 -0
  11. package/lib/cjs/expressions/ExpressionNode.js.map +1 -0
  12. package/lib/cjs/index.d.ts +4 -0
  13. package/lib/cjs/index.js +6 -1
  14. package/lib/cjs/index.js.map +1 -1
  15. package/lib/cjs/queries/FieldSet.d.ts +3 -0
  16. package/lib/cjs/queries/FieldSet.js +16 -0
  17. package/lib/cjs/queries/FieldSet.js.map +1 -1
  18. package/lib/cjs/queries/IRCanonicalize.d.ts +2 -2
  19. package/lib/cjs/queries/IRCanonicalize.js +4 -0
  20. package/lib/cjs/queries/IRCanonicalize.js.map +1 -1
  21. package/lib/cjs/queries/IRDesugar.d.ts +11 -2
  22. package/lib/cjs/queries/IRDesugar.js +14 -0
  23. package/lib/cjs/queries/IRDesugar.js.map +1 -1
  24. package/lib/cjs/queries/IRLower.d.ts +9 -0
  25. package/lib/cjs/queries/IRLower.js +42 -60
  26. package/lib/cjs/queries/IRLower.js.map +1 -1
  27. package/lib/cjs/queries/IRMutation.d.ts +11 -1
  28. package/lib/cjs/queries/IRMutation.js +45 -21
  29. package/lib/cjs/queries/IRMutation.js.map +1 -1
  30. package/lib/cjs/queries/IntermediateRepresentation.d.ts +9 -2
  31. package/lib/cjs/queries/MutationQuery.js +16 -2
  32. package/lib/cjs/queries/MutationQuery.js.map +1 -1
  33. package/lib/cjs/queries/QueryFactory.d.ts +2 -1
  34. package/lib/cjs/queries/QueryFactory.js.map +1 -1
  35. package/lib/cjs/queries/SelectQuery.d.ts +6 -2
  36. package/lib/cjs/queries/SelectQuery.js +51 -7
  37. package/lib/cjs/queries/SelectQuery.js.map +1 -1
  38. package/lib/cjs/queries/UpdateBuilder.d.ts +2 -0
  39. package/lib/cjs/queries/UpdateBuilder.js +0 -1
  40. package/lib/cjs/queries/UpdateBuilder.js.map +1 -1
  41. package/lib/cjs/shapes/Shape.d.ts +2 -0
  42. package/lib/cjs/shapes/Shape.js +0 -8
  43. package/lib/cjs/shapes/Shape.js.map +1 -1
  44. package/lib/cjs/sparql/irToAlgebra.js +99 -9
  45. package/lib/cjs/sparql/irToAlgebra.js.map +1 -1
  46. package/lib/cjs/test-helpers/query-fixtures.d.ts +64 -1624
  47. package/lib/cjs/test-helpers/query-fixtures.js +65 -17
  48. package/lib/cjs/test-helpers/query-fixtures.js.map +1 -1
  49. package/lib/esm/expressions/Expr.d.ts +58 -0
  50. package/lib/esm/expressions/Expr.js +214 -0
  51. package/lib/esm/expressions/Expr.js.map +1 -0
  52. package/lib/esm/expressions/ExpressionMethods.d.ts +81 -0
  53. package/lib/esm/expressions/ExpressionMethods.js +2 -0
  54. package/lib/esm/expressions/ExpressionMethods.js.map +1 -0
  55. package/lib/esm/expressions/ExpressionNode.d.ts +95 -0
  56. package/lib/esm/expressions/ExpressionNode.js +341 -0
  57. package/lib/esm/expressions/ExpressionNode.js.map +1 -0
  58. package/lib/esm/index.d.ts +4 -0
  59. package/lib/esm/index.js +3 -0
  60. package/lib/esm/index.js.map +1 -1
  61. package/lib/esm/queries/FieldSet.d.ts +3 -0
  62. package/lib/esm/queries/FieldSet.js +16 -0
  63. package/lib/esm/queries/FieldSet.js.map +1 -1
  64. package/lib/esm/queries/IRCanonicalize.d.ts +2 -2
  65. package/lib/esm/queries/IRCanonicalize.js +4 -0
  66. package/lib/esm/queries/IRCanonicalize.js.map +1 -1
  67. package/lib/esm/queries/IRDesugar.d.ts +11 -2
  68. package/lib/esm/queries/IRDesugar.js +14 -0
  69. package/lib/esm/queries/IRDesugar.js.map +1 -1
  70. package/lib/esm/queries/IRLower.d.ts +9 -0
  71. package/lib/esm/queries/IRLower.js +41 -60
  72. package/lib/esm/queries/IRLower.js.map +1 -1
  73. package/lib/esm/queries/IRMutation.d.ts +11 -1
  74. package/lib/esm/queries/IRMutation.js +44 -21
  75. package/lib/esm/queries/IRMutation.js.map +1 -1
  76. package/lib/esm/queries/IntermediateRepresentation.d.ts +9 -2
  77. package/lib/esm/queries/MutationQuery.js +16 -2
  78. package/lib/esm/queries/MutationQuery.js.map +1 -1
  79. package/lib/esm/queries/QueryFactory.d.ts +2 -1
  80. package/lib/esm/queries/QueryFactory.js.map +1 -1
  81. package/lib/esm/queries/SelectQuery.d.ts +6 -2
  82. package/lib/esm/queries/SelectQuery.js +51 -7
  83. package/lib/esm/queries/SelectQuery.js.map +1 -1
  84. package/lib/esm/queries/UpdateBuilder.d.ts +2 -0
  85. package/lib/esm/queries/UpdateBuilder.js +0 -1
  86. package/lib/esm/queries/UpdateBuilder.js.map +1 -1
  87. package/lib/esm/shapes/Shape.d.ts +2 -0
  88. package/lib/esm/shapes/Shape.js +0 -8
  89. package/lib/esm/shapes/Shape.js.map +1 -1
  90. package/lib/esm/sparql/irToAlgebra.js +99 -9
  91. package/lib/esm/sparql/irToAlgebra.js.map +1 -1
  92. package/lib/esm/test-helpers/query-fixtures.d.ts +64 -1624
  93. package/lib/esm/test-helpers/query-fixtures.js +65 -17
  94. package/lib/esm/test-helpers/query-fixtures.js.map +1 -1
  95. package/package.json +3 -3
package/CHANGELOG.md CHANGED
@@ -1,220 +1,55 @@
1
1
  # Changelog
2
2
 
3
- ## 2.0.1
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
- - [#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.
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 expressionsyou can compute values dynamically instead of just reading or writing raw fields.
168
8
 
169
- - **`SparqlStore`** — abstract base class for SPARQL-backed stores. Extend it and implement two methods to connect any SPARQL 1.1 endpoint:
9
+ ### What's new
170
10
 
171
- ```ts
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
- class MyStore extends SparqlStore {
175
- protected async executeSparqlSelect(
176
- sparql: string
177
- ): Promise<SparqlJsonResults> {
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
- - **IR SPARQL string** convenience functions (full pipeline in one call):
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
- - `selectToAlgebra(query, options?)` — returns `SparqlSelectPlan`
196
- - `createToAlgebra(query, options?)` returns `SparqlInsertDataPlan`
197
- - `updateToAlgebra(query, options?)` returns `SparqlDeleteInsertPlan`
198
- - `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
+ ```
199
27
 
200
- - **Algebra SPARQL string** serialization:
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
- - `selectPlanToSparql(plan, options?)`, `insertDataPlanToSparql(plan, options?)`, `deleteInsertPlanToSparql(plan, options?)`, `deleteWherePlanToSparql(plan, options?)`
203
- - `serializeAlgebraNode(node)`, `serializeExpression(expr)`, `serializeTerm(term)`
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
- - **Result mapping** (SPARQL JSON results typed DSL objects):
37
+ - **`Expr` module** for expressions that don't start from a property, like the current timestamp, conditional logic, or coalescing nulls.
206
38
 
207
- - `mapSparqlSelectResult(json, query)` — handles flat/nested/aggregated results with XSD type coercion
208
- - `mapSparqlCreateResult(uri, query)` — echoes created fields with generated URI
209
- - `mapSparqlUpdateResult(query)` echoes updated fields
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
- - **All algebra types** re-exported: `SparqlTerm`, `SparqlTriple`, `SparqlAlgebraNode`, `SparqlExpression`, `SparqlSelectPlan`, `SparqlInsertDataPlan`, `SparqlDeleteInsertPlan`, `SparqlDeleteWherePlan`, `SparqlPlan`, `SparqlOptions`, etc.
46
+ Update expression callbacks are fully typed `.plus()` only appears on number properties, `.strlen()` only on strings, etc.
212
47
 
213
- **Bug fixes included:**
48
+ ### New exports
214
49
 
215
- - Fixed `isNodeReference()` in MutationQuery.ts nested creates with predefined IDs (e.g., `{id: '...', name: 'Bestie'}`) now correctly insert entity data instead of only creating the link.
50
+ `ExpressionNode`, `Expr`, `ExpressionInput`, `PropertyRefMap`, `ExpressionUpdateProxy<S>`, `ExpressionUpdateResult<S>`, and per-type method interfaces (`NumericExpressionMethods`, `StringExpressionMethods`, `DateExpressionMethods`, `BooleanExpressionMethods`, `BaseExpressionMethods`).
216
51
 
217
- See [SPARQL Algebra Layer docs](./documentation/sparql-algebra.md) for the full type reference, conversion rules, and store implementation guide.
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 = schema('Person');
73
+ static targetClass = ex.Person;
62
74
 
63
- @literalProperty({path: schema('name'), maxCount: 1})
75
+ @literalProperty({path: ex.name, maxCount: 1})
64
76
  get name(): string { return ''; }
65
77
 
66
- @objectProperty({path: schema('knows'), shape: Person})
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 schema = createNameSpace('https://schema.org/');
206
- const PersonClass = schema('Person');
207
- const name = schema('name');
208
- const knows = schema('knows');
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 = PersonClass;
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
- If person.pets returns an array of Pets. And Dog extends Pet.
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 = EmployeeClass;
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
+ };