@_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.
Files changed (119) hide show
  1. package/CHANGELOG.md +33 -171
  2. package/README.md +197 -45
  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/DeleteBuilder.d.ts +17 -20
  16. package/lib/cjs/queries/DeleteBuilder.js +46 -19
  17. package/lib/cjs/queries/DeleteBuilder.js.map +1 -1
  18. package/lib/cjs/queries/DeleteQuery.d.ts +2 -2
  19. package/lib/cjs/queries/DeleteQuery.js.map +1 -1
  20. package/lib/cjs/queries/FieldSet.d.ts +3 -0
  21. package/lib/cjs/queries/FieldSet.js +16 -0
  22. package/lib/cjs/queries/FieldSet.js.map +1 -1
  23. package/lib/cjs/queries/IRCanonicalize.d.ts +10 -3
  24. package/lib/cjs/queries/IRCanonicalize.js +10 -1
  25. package/lib/cjs/queries/IRCanonicalize.js.map +1 -1
  26. package/lib/cjs/queries/IRDesugar.d.ts +30 -2
  27. package/lib/cjs/queries/IRDesugar.js +29 -9
  28. package/lib/cjs/queries/IRDesugar.js.map +1 -1
  29. package/lib/cjs/queries/IRLower.d.ts +19 -2
  30. package/lib/cjs/queries/IRLower.js +104 -20
  31. package/lib/cjs/queries/IRLower.js.map +1 -1
  32. package/lib/cjs/queries/IRMutation.d.ts +31 -1
  33. package/lib/cjs/queries/IRMutation.js +68 -15
  34. package/lib/cjs/queries/IRMutation.js.map +1 -1
  35. package/lib/cjs/queries/IntermediateRepresentation.d.ts +33 -4
  36. package/lib/cjs/queries/MutationQuery.js +16 -2
  37. package/lib/cjs/queries/MutationQuery.js.map +1 -1
  38. package/lib/cjs/queries/QueryBuilder.d.ts +14 -1
  39. package/lib/cjs/queries/QueryBuilder.js +59 -2
  40. package/lib/cjs/queries/QueryBuilder.js.map +1 -1
  41. package/lib/cjs/queries/QueryFactory.d.ts +2 -1
  42. package/lib/cjs/queries/QueryFactory.js.map +1 -1
  43. package/lib/cjs/queries/SelectQuery.d.ts +6 -2
  44. package/lib/cjs/queries/SelectQuery.js +51 -7
  45. package/lib/cjs/queries/SelectQuery.js.map +1 -1
  46. package/lib/cjs/queries/UpdateBuilder.d.ts +22 -13
  47. package/lib/cjs/queries/UpdateBuilder.js +60 -11
  48. package/lib/cjs/queries/UpdateBuilder.js.map +1 -1
  49. package/lib/cjs/queries/UpdateQuery.d.ts +2 -2
  50. package/lib/cjs/shapes/Shape.d.ts +8 -2
  51. package/lib/cjs/shapes/Shape.js +9 -13
  52. package/lib/cjs/shapes/Shape.js.map +1 -1
  53. package/lib/cjs/sparql/SparqlStore.js +15 -0
  54. package/lib/cjs/sparql/SparqlStore.js.map +1 -1
  55. package/lib/cjs/sparql/irToAlgebra.d.ts +34 -3
  56. package/lib/cjs/sparql/irToAlgebra.js +380 -31
  57. package/lib/cjs/sparql/irToAlgebra.js.map +1 -1
  58. package/lib/cjs/test-helpers/query-fixtures.d.ts +96 -208
  59. package/lib/cjs/test-helpers/query-fixtures.js +96 -19
  60. package/lib/cjs/test-helpers/query-fixtures.js.map +1 -1
  61. package/lib/esm/expressions/Expr.d.ts +58 -0
  62. package/lib/esm/expressions/Expr.js +214 -0
  63. package/lib/esm/expressions/Expr.js.map +1 -0
  64. package/lib/esm/expressions/ExpressionMethods.d.ts +81 -0
  65. package/lib/esm/expressions/ExpressionMethods.js +2 -0
  66. package/lib/esm/expressions/ExpressionMethods.js.map +1 -0
  67. package/lib/esm/expressions/ExpressionNode.d.ts +95 -0
  68. package/lib/esm/expressions/ExpressionNode.js +341 -0
  69. package/lib/esm/expressions/ExpressionNode.js.map +1 -0
  70. package/lib/esm/index.d.ts +4 -0
  71. package/lib/esm/index.js +3 -0
  72. package/lib/esm/index.js.map +1 -1
  73. package/lib/esm/queries/DeleteBuilder.d.ts +17 -20
  74. package/lib/esm/queries/DeleteBuilder.js +46 -19
  75. package/lib/esm/queries/DeleteBuilder.js.map +1 -1
  76. package/lib/esm/queries/DeleteQuery.d.ts +2 -2
  77. package/lib/esm/queries/DeleteQuery.js.map +1 -1
  78. package/lib/esm/queries/FieldSet.d.ts +3 -0
  79. package/lib/esm/queries/FieldSet.js +16 -0
  80. package/lib/esm/queries/FieldSet.js.map +1 -1
  81. package/lib/esm/queries/IRCanonicalize.d.ts +10 -3
  82. package/lib/esm/queries/IRCanonicalize.js +10 -1
  83. package/lib/esm/queries/IRCanonicalize.js.map +1 -1
  84. package/lib/esm/queries/IRDesugar.d.ts +30 -2
  85. package/lib/esm/queries/IRDesugar.js +21 -2
  86. package/lib/esm/queries/IRDesugar.js.map +1 -1
  87. package/lib/esm/queries/IRLower.d.ts +19 -2
  88. package/lib/esm/queries/IRLower.js +101 -19
  89. package/lib/esm/queries/IRLower.js.map +1 -1
  90. package/lib/esm/queries/IRMutation.d.ts +31 -1
  91. package/lib/esm/queries/IRMutation.js +63 -14
  92. package/lib/esm/queries/IRMutation.js.map +1 -1
  93. package/lib/esm/queries/IntermediateRepresentation.d.ts +33 -4
  94. package/lib/esm/queries/MutationQuery.js +16 -2
  95. package/lib/esm/queries/MutationQuery.js.map +1 -1
  96. package/lib/esm/queries/QueryBuilder.d.ts +14 -1
  97. package/lib/esm/queries/QueryBuilder.js +59 -2
  98. package/lib/esm/queries/QueryBuilder.js.map +1 -1
  99. package/lib/esm/queries/QueryFactory.d.ts +2 -1
  100. package/lib/esm/queries/QueryFactory.js.map +1 -1
  101. package/lib/esm/queries/SelectQuery.d.ts +6 -2
  102. package/lib/esm/queries/SelectQuery.js +51 -7
  103. package/lib/esm/queries/SelectQuery.js.map +1 -1
  104. package/lib/esm/queries/UpdateBuilder.d.ts +22 -13
  105. package/lib/esm/queries/UpdateBuilder.js +60 -11
  106. package/lib/esm/queries/UpdateBuilder.js.map +1 -1
  107. package/lib/esm/queries/UpdateQuery.d.ts +2 -2
  108. package/lib/esm/shapes/Shape.d.ts +8 -2
  109. package/lib/esm/shapes/Shape.js +9 -13
  110. package/lib/esm/shapes/Shape.js.map +1 -1
  111. package/lib/esm/sparql/SparqlStore.js +16 -1
  112. package/lib/esm/sparql/SparqlStore.js.map +1 -1
  113. package/lib/esm/sparql/irToAlgebra.d.ts +34 -3
  114. package/lib/esm/sparql/irToAlgebra.js +374 -31
  115. package/lib/esm/sparql/irToAlgebra.js.map +1 -1
  116. package/lib/esm/test-helpers/query-fixtures.d.ts +96 -208
  117. package/lib/esm/test-helpers/query-fixtures.js +96 -19
  118. package/lib/esm/test-helpers/query-fixtures.js.map +1 -1
  119. package/package.json +3 -3
package/CHANGELOG.md CHANGED
@@ -1,193 +1,55 @@
1
1
  # Changelog
2
2
 
3
- ## 2.0.0
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
- - [#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.
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
- ### Patch Changes
9
+ ### What's new
125
10
 
126
- - [#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.
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
- 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).
129
-
130
- Also adds regression coverage for desugar preservation, IR lowering completeness, and updated SPARQL golden output for nested query fixtures.
131
-
132
- ## 1.2.0
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
- - **IR SPARQL string** convenience functions (full pipeline in one call):
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
- - **IR → SPARQL algebra** (for stores that want to inspect/optimize the algebra before serialization):
167
-
168
- - `selectToAlgebra(query, options?)` returns `SparqlSelectPlan`
169
- - `createToAlgebra(query, options?)` — returns `SparqlInsertDataPlan`
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
- - **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.
174
29
 
175
- - `selectPlanToSparql(plan, options?)`, `insertDataPlanToSparql(plan, options?)`, `deleteInsertPlanToSparql(plan, options?)`, `deleteWherePlanToSparql(plan, options?)`
176
- - `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
+ ```
177
36
 
178
- - **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.
179
38
 
180
- - `mapSparqlSelectResult(json, query)` — handles flat/nested/aggregated results with XSD type coercion
181
- - `mapSparqlCreateResult(uri, query)` — echoes created fields with generated URI
182
- - `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
+ ```
183
45
 
184
- - **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.
185
47
 
186
- **Bug fixes included:**
48
+ ### New exports
187
49
 
188
- - 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`).
189
51
 
190
- 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.
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 = 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
  ```
@@ -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
- If person.pets returns an array of Pets. And Dog extends Pet.
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).sortBy((p) => p.name, 'ASC');
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. Chain `.for(id)` to target the entity:
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
- /* Result: {deleted: Array<{id: string}>, count: number} */
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
- To delete multiple nodes pass an array:
671
+ // Delete multiple nodes
672
+ const deleted = await Person.delete([{id: 'https://my.app/node1'}, {id: 'https://my.app/node2'}]);
523
673
 
524
- ```typescript
525
- /* Result: {deleted: Array<{id: string}>, count: number} */
526
- const deleted = await Person.delete([{id: 'https://my.app/node1'},{id: 'https://my.app/node2'}]);
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 = EmployeeClass;
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).for({id: 'https://my.app/alice'});
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
+ };