Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
116 commits
Select commit Hold shift + click to select a range
15b2355
Expand 003 dynamic IR construction with five detailed proposals
claude Mar 4, 2026
90c26f2
Add FieldSet composability design and CMS surface examples to 003
claude Mar 4, 2026
5b197a8
Add scoped filters, query derivation, shape remapping, and end-to-end…
claude Mar 4, 2026
9e8d74e
Add immutability, filter/select interaction, variable reuse, and adap…
claude Mar 5, 2026
fd39dde
Expand variable reuse with DSL examples, FieldSet binding composition…
claude Mar 5, 2026
36e1dc7
Rewrite variable reuse/binding section with clearer explanations and …
claude Mar 5, 2026
560e9be
Extract shared variable bindings into dedicated 008 doc, trim 003
claude Mar 5, 2026
040866c
Extract shape remapping into 009 doc, further trim 003
claude Mar 5, 2026
66bb7ad
Resolve serialization & prefix decisions in 003
claude Mar 5, 2026
fa269db
Add CMS surface examples, nested selection, method naming ideation
claude Mar 5, 2026
3273b9f
Major architecture update: DSL and QueryBuilder are the same system
claude Mar 5, 2026
951b518
Update execution model: PromiseLike replaces nextTick hack
claude Mar 5, 2026
ab1dbf2
Fix consistency in 003: .select() for creation, .setFields() for upda…
claude Mar 6, 2026
d6bcdd2
Add plan doc for dynamic queries (FieldSet + QueryBuilder + DSL align…
claude Mar 6, 2026
9720d0f
Update plan with accurate file sizes, paths, and test references
claude Mar 6, 2026
1a8e8fa
Address review feedback on dynamic queries plan
claude Mar 6, 2026
ad8b229
Add mutation builders to plan scope
claude Mar 6, 2026
d1e1837
Complete plan with missing items from ideation doc
claude Mar 6, 2026
85c3ae7
Resolve scoped filter merging, update plan skill for completeness
claude Mar 6, 2026
54f7783
Add implementation phases (top-down approach) to dynamic queries plan
claude Mar 6, 2026
b451e8c
Phase 1: Extract ProxiedPathBuilder from SelectQuery.ts
claude Mar 6, 2026
2c78bf4
Tasks mode: add concrete task breakdown with validation criteria
claude Mar 6, 2026
ee6001b
Add global test invariants section to plan 001
claude Mar 6, 2026
72e7d89
Phase 2: Add QueryBuilder + walkPropertyPath for dynamic queries
claude Mar 6, 2026
22ead99
Phase 3a+3b: Add FieldSet, CreateBuilder, UpdateBuilder, DeleteBuilder
claude Mar 6, 2026
9564903
Phase 4: Add serialization (toJSON/fromJSON) for FieldSet and QueryBu…
claude Mar 6, 2026
3bffd30
Mark all phases complete in plan 001-dynamic-queries
claude Mar 6, 2026
a5b66fa
Add detailed Phase 4.4 breakdown (type threading + DSL rewire + dead …
claude Mar 6, 2026
7ba98c9
Update Phase 4.4 plan with type invariant rules and Shape.query() rem…
claude Mar 6, 2026
3d60ad5
Add type probe and incremental 6-step approach for Phase 4.4a
claude Mar 6, 2026
a9390d5
Phase 4.4a Step 1: Add Result generic parameter to QueryBuilder
claude Mar 6, 2026
6f21781
Phase 4.4a Step 2: Wire then/catch/finally/exec to use Result generic
claude Mar 6, 2026
337df8c
Phase 4.4a Step 3: Wire select() to compute Result via QueryResponseT…
claude Mar 6, 2026
0653e55
Phase 4.4a Step 4: Update fluent methods to preserve Result generic
claude Mar 6, 2026
5613b7b
Phase 4.4a Step 5: Wire one() to unwrap array Result type
claude Mar 6, 2026
744956d
Phase 4.4a Step 6: Wire selectAll() result type via SelectAllQueryRes…
claude Mar 6, 2026
a863de2
Phase 4.4b: Rewire Shape.select()/selectAll() to return QueryBuilder
claude Mar 6, 2026
f622496
Phase 4.4c: Rewire Shape.create()/update()/delete() to return builders
claude Mar 6, 2026
8b2ac4b
Phase 4.4d: Thread result types through mutation builders
claude Mar 6, 2026
5233f8c
Phase 4.4e: Dead code removal
claude Mar 6, 2026
a87001d
Add Phase 5: preloadFor + Component Query Integration plan
claude Mar 6, 2026
146ce20
Add strictNullChecks idea doc + update Phase 5 plan
claude Mar 6, 2026
cb11d95
Implement Phase 5: preloadFor + component query integration
claude Mar 6, 2026
8107c64
Remove Shape.query() and clean up dead code
claude Mar 6, 2026
b5638d8
Use Person.select() instead of QueryBuilder.from() in test migrations
claude Mar 6, 2026
51837f2
Add documentation comment for _preloads serialization and build-time …
claude Mar 7, 2026
c049b29
Add Phase 6 (forAll multi-ID filtering) and Phase 7 (unified callback…
claude Mar 7, 2026
e62272a
Add Phases 8-12: typed FieldSets, direct IR, sub-queries, factory rem…
claude Mar 7, 2026
e8740c3
Reorder phases: merge typed FieldSet into Phase 7, renumber 8-11
claude Mar 7, 2026
8f9fad6
Break Phase 7 into subphases 7a-7e for incremental implementation
claude Mar 7, 2026
7980aff
Add task breakdown with detailed validation criteria for Phases 6–11
claude Mar 7, 2026
4ef5aff
Phase 6: implement forAll(ids) with VALUES clause for multi-ID filtering
claude Mar 8, 2026
f8fd22e
Phase 7a: extend FieldSetEntry with subSelect, aggregation, customKey
claude Mar 8, 2026
97e06da
Phase 7b: FieldSet.for() and .all() accept ShapeClass (e.g. Person)
claude Mar 8, 2026
cbffe70
Phase 7c: replace traceFieldsFromCallback with ProxiedPathBuilder
claude Mar 8, 2026
a0b896b
Phase 7d: toJSON for callback-based selections + orderDirection fix
claude Mar 8, 2026
66c33a6
Phase 7e: typed FieldSet<R> — carry callback return type
claude Mar 8, 2026
07caf77
Phase 8: QueryBuilder generates IR directly, bypassing SelectQueryFac…
claude Mar 8, 2026
c475046
Update plan: mark Phase 8 complete
claude Mar 8, 2026
0be32ef
Phase 9: Sub-queries through FieldSet — FieldSet extracts sub-selects…
claude Mar 8, 2026
9b14a41
Update plan: mark Phase 9 complete
claude Mar 8, 2026
f702b85
Update plan: Phase 10 blocked, document remaining dependencies
claude Mar 8, 2026
37d939a
Update plan: split Phase 10 into sub-phases 10a–10g for SelectQueryFa…
claude Mar 9, 2026
f0d57b3
Add detailed task breakdowns for phases 10a–10g (tasks mode)
claude Mar 9, 2026
f5cfab2
Phase 10a: Evaluation support in FieldSetEntry
claude Mar 9, 2026
9520eb0
Phase 10b: BoundComponent (preload) support in FieldSetEntry
claude Mar 9, 2026
65b8c11
Phase 10c: Replace LinkedWhereQuery with standalone where evaluation
claude Mar 9, 2026
2ba5c41
Phase 10d: Lightweight sub-select wrapper in proxy handlers
claude Mar 9, 2026
047ac31
Phase 10e: Remove _buildFactory() and remaining SelectQueryFactory ru…
claude Mar 9, 2026
b5d8d97
Phase 10f: Migrate type utilities away from SelectQueryFactory
claude Mar 9, 2026
d4e0d34
Phase 10g: Delete SelectQueryFactory class, retain type-only interfac…
claude Mar 9, 2026
58a0822
Mark all Phase 10 sub-phases (10a-10g) complete in plan document
claude Mar 9, 2026
f1fa8a4
Phase 10 cleanup: remove dead code and simplify QueryBuildFn
claude Mar 9, 2026
b95b19e
Rename SelectQueryFactory to SubSelectResult for clarity
claude Mar 9, 2026
bb38475
Extract SubSelectResult interface into its own file
claude Mar 9, 2026
d44252c
Add desugar TODO and Phase 12 plan for typed FieldSet
claude Mar 9, 2026
3d325f5
Phase 12: Make FieldSet carry generic types, replace SubSelectResult
claude Mar 9, 2026
8105aee
Add deep nesting type inference boundary tests for FieldSet<R, Source>
claude Mar 9, 2026
1292158
Fix deep property chain type resolution (Test 17) and cleanup
claude Mar 9, 2026
51e8ca1
Phase 11 hardening: simplify duck-types, add guards, depth support, t…
claude Mar 9, 2026
c8eecdd
11.9: Make PropertyPath.segments readonly at the type level
claude Mar 9, 2026
3b03516
11.4 + 11.7: JSDoc on FieldSet.set(), reduce `as any` casts in produc…
claude Mar 9, 2026
6d36518
Code review fix-up: bugs, design issues, type safety, and test quality
claude Mar 9, 2026
2727b25
Fix review findings B3, D7, D10, T3 and add D8 cleanup note
claude Mar 9, 2026
aeb0721
Add type system review and proposed phases 13-16 to plan document
claude Mar 9, 2026
1672554
Expand phases 13-19 with full task breakdown, validation, and open qu…
claude Mar 9, 2026
82f92fa
Update phase statuses in 001-dynamic-queries.md to reflect actual cod…
claude Mar 9, 2026
22e24cc
Mark Phase 11 items 4 and 9 as complete
claude Mar 10, 2026
ddbbbaa
Phase 13: Remove dead code — commented blocks, debug console.log, sta…
claude Mar 10, 2026
ff1d51f
Mark Phase 13 as complete in plan document
claude Mar 10, 2026
15a4387
Phase 14: Type safety quick wins — typed RawSelectInput.shape, brande…
claude Mar 10, 2026
ce7df68
Mark Phase 14 as complete in plan document
claude Mar 10, 2026
5b681df
Phase 15: Consolidate QueryString/QueryNumber/QueryBoolean/QueryDate …
claude Mar 10, 2026
e3c295b
Mark Phase 15 as complete in plan document
claude Mar 10, 2026
47744ac
Defer Phase 16 (CreateQResult simplification) to idea doc 011
claude Mar 10, 2026
d2cad53
Phase 17: Remove dead getQueryPaths monkey-patch from FieldSet
claude Mar 10, 2026
fee9eb5
Phase 18: Rewrite plan — full old SelectPath IR removal in 4 sub-phases
claude Mar 10, 2026
ca928c8
Phase 18: Remove old SelectPath IR — FieldSet entries go directly to …
claude Mar 10, 2026
f9ce088
Eliminate parentQueryPath duck-typing and sort QueryStep[] dependency
claude Mar 10, 2026
2fc857c
Add type-safe boundary for where clause paths in IRDesugar
claude Mar 10, 2026
770cc21
Phase 19: Add ShapeConstructor<S> type, eliminate ~30 as any casts
claude Mar 10, 2026
58cace3
Remove unused ShapeType type alias
claude Mar 10, 2026
8173aad
Clean up plan doc: condense completed phases, remove resolved questions
claude Mar 10, 2026
14ec261
Remove traceFieldsFromCallback + type getShapeClass as ShapeConstructor
claude Mar 10, 2026
fdffe0f
Add clarifying comments for 2.0 release readability
claude Mar 10, 2026
9dc784f
Add Dynamic Query Building section to README for 2.0 release
claude Mar 10, 2026
b2dca8d
Breaking: remove id argument from Shape.select() and Shape.update()
claude Mar 10, 2026
d2d1eca
Add changeset for 2.0 major release
claude Mar 10, 2026
be72d7f
Add wrapup report and REVIEW section for dynamic queries
claude Mar 10, 2026
e47fdc2
Update FieldSet use case comment to remove CMS reference
claude Mar 10, 2026
e3d5a67
Refactor DeleteBuilder: add .for() method, move validation to .build()
claude Mar 10, 2026
6caee9d
Update docs for DeleteBuilder .for() API
claude Mar 10, 2026
55fab20
Merge pull request #23 - Linked 2.0
flyon Mar 10, 2026
00c7e4a
Update idea docs: result typing in 011, callback mutations in 006
claude Mar 10, 2026
b13467e
Remove idea 003, plan 001; clean up report 008 references
claude Mar 10, 2026
62f1bf8
Merge pull request #24 from Semantu/claude/linked-2-cleanup-K01bt
flyon Mar 10, 2026
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
105 changes: 105 additions & 0 deletions .changeset/dynamic-queries-2.0.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,105 @@
---
'@_linked/core': major
---

## Breaking Changes

### `Shape.select()` and `Shape.update()` no longer accept an ID as the first argument

Use `.for(id)` to target a specific entity instead.

**Select:**
```typescript
// Before
const result = await Person.select({id: '...'}, p => p.name);

// After
const result = await Person.select(p => p.name).for({id: '...'});
```

`.for(id)` unwraps the result type from array to single object, matching the old single-subject overload behavior.

**Update:**
```typescript
// Before
const result = await Person.update({id: '...'}, {name: 'Alice'});

// After
const result = await Person.update({name: 'Alice'}).for({id: '...'});
```

`Shape.selectAll(id)` also no longer accepts an id — use `Person.selectAll().for(id)`.

### `ShapeType` renamed to `ShapeConstructor`

The type alias for concrete Shape subclass constructors has been renamed. Update any imports or references:

```typescript
// Before
import type {ShapeType} from '@_linked/core/shapes/Shape';

// After
import type {ShapeConstructor} from '@_linked/core/shapes/Shape';
```

### `QueryString`, `QueryNumber`, `QueryBoolean`, `QueryDate` classes removed

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.

### Internal IR types removed

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`):

- Types: `SelectPath`, `QueryPath`, `CustomQueryObject`, `SubQueryPaths`, `ComponentQueryPath`
- Functions: `fieldSetToSelectPath()`, `entryToQueryPath()`
- Methods: `QueryBuilder.getQueryPaths()`, `BoundComponent.getComponentQueryPaths()`
- `RawSelectInput.select` field renamed to `RawSelectInput.entries` (type changed from `SelectPath` to `FieldSetEntry[]`)

### `getPackageShape()` return type is now nullable

Returns `ShapeConstructor | undefined` instead of `typeof Shape`. Code that didn't null-check the return value will now get TypeScript errors.

## New Features

### `.for(id)` and `.forAll(ids)` chaining

Consistent API for targeting entities across select and update operations:

```typescript
// Single entity (result is unwrapped, not an array)
await Person.select(p => p.name).for({id: '...'});
await Person.select(p => p.name).for('https://...');

// Multiple specific entities
await QueryBuilder.from(Person).select(p => p.name).forAll([{id: '...'}, {id: '...'}]);

// All instances (default — no .for() needed)
await Person.select(p => p.name);
```

### Dynamic Query Building with `QueryBuilder` and `FieldSet`

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.

Key capabilities:
- `QueryBuilder.from(Person)` or `QueryBuilder.from('https://schema.org/Person')` — fluent, chainable, immutable query construction
- `FieldSet.for(Person, ['name', 'knows'])` — composable field selections with `.add()`, `.remove()`, `.pick()`, `FieldSet.merge()`
- `FieldSet.all(Person, {depth: 2})` — select all decorated properties with optional depth
- JSON serialization: `query.toJSON()` / `QueryBuilder.fromJSON(json)` and `fieldSet.toJSON()` / `FieldSet.fromJSON(json)`
- All builders are `PromiseLike` — `await` them directly or call `.build()` to inspect the IR

### Mutation Builders

`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.

### `PropertyPath` exported

The `PropertyPath` value object is now a public export — a type-safe representation of a sequence of property traversals through a shape graph.

```typescript
import {PropertyPath, walkPropertyPath} from '@_linked/core';
```

### `ShapeConstructor<S>` type

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).
223 changes: 209 additions & 14 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -7,6 +7,7 @@ Linked core gives you a type-safe, schema-parameterized query language and SHACL

- **Schema-Parameterized Query DSL**: TypeScript-embedded queries driven by your Shape definitions.
- **Fully Inferred Result Types**: The TypeScript return type of every query is automatically inferred from the selected paths — no manual type annotations needed. Select `p.name` and get `{id: string; name: string}[]`. Select `p.friends.name` and get nested result types. This works for all operations: select, create, update, and delete.
- **Dynamic Query Building**: Build queries programmatically with `QueryBuilder`, compose field selections with `FieldSet`, and serialize/deserialize queries as JSON — for CMS dashboards, dynamic forms, and API-driven query construction.
- **Shape Classes (SHACL)**: TypeScript classes that generate SHACL shape metadata.
- **Object-Oriented Data Operations**: Query, create, update, and delete data using the same Shape-based API.
- **Storage Routing**: `LinkedStorage` routes query objects to your configured store(s) that implement `IQuadStore`.
Expand Down Expand Up @@ -245,10 +246,9 @@ const allFriends = await Person.select((p) => p.knows.selectAll());

**3) Apply a simple mutation**
```typescript
const myNode = {id: 'https://my.app/node1'};
const updated = await Person.update(myNode, {
const updated = await Person.update({
name: 'Alicia',
});
}).for({id: 'https://my.app/node1'});
/* updated: {id: string} & UpdatePartial<Person> */
```

Expand Down Expand Up @@ -295,6 +295,10 @@ The query DSL is schema-parameterized: you define your own SHACL shapes, and Lin
- Query context variables
- Preloading (`preloadFor`) for component-like queries
- Create / Update / Delete mutations
- Dynamic query building with `QueryBuilder`
- Composable field sets with `FieldSet`
- Mutation builders (`CreateBuilder`, `UpdateBuilder`, `DeleteBuilder`)
- Query and FieldSet JSON serialization / deserialization

### Query examples

Expand All @@ -317,10 +321,9 @@ const flags = await Person.select((p) => p.isRealPerson);

#### Target a specific subject
```typescript
const myNode = {id: 'https://my.app/node1'};
/* Result: {id: string; name: string} | null */
const one = await Person.select(myNode, (p) => p.name);
const missing = await Person.select({id: 'https://my.app/missing'}, (p) => p.name); // null
/* Result: {id: string; name: string} */
const one = await Person.select((p) => p.name).for({id: 'https://my.app/node1'});
const missing = await Person.select((p) => p.name).for({id: 'https://my.app/missing'}); // null
```

#### Multiple paths + nested paths
Expand Down Expand Up @@ -444,10 +447,10 @@ Where UpdatePartial<Shape> reflects the created properties.

#### Update

Update will patch any property that you send as payload and leave the rest untouched.
Update will patch any property that you send as payload and leave the rest untouched. Chain `.for(id)` to target the entity:
```typescript
/* Result: {id: string} & UpdatePartial<Person> */
const updated = await Person.update({id: 'https://my.app/node1'}, {name: 'Alicia'});
const updated = await Person.update({name: 'Alicia'}).for({id: 'https://my.app/node1'});
```
Returns:
```json
Expand All @@ -463,9 +466,9 @@ When updating a property that holds multiple values (one that returns an array i
To overwrite all values:
```typescript
// Overwrite the full set of "knows" values.
const overwriteFriends = await Person.update({id: 'https://my.app/person1'}, {
const overwriteFriends = await Person.update({
knows: [{id: 'https://my.app/person2'}],
});
}).for({id: 'https://my.app/person1'});
```
The result will contain an object with `updatedTo`, to indicate that previous values were overwritten to this new set of values:
```json
Expand All @@ -475,17 +478,17 @@ The result will contain an object with `updatedTo`, to indicate that previous va
updatedTo: [{id:"https://my.app/person2"}],
}
}
```
```

To make incremental changes to the current set of values you can provide an object with `add` and/or `remove` keys:
```typescript
// Add one value and remove one value without replacing the whole set.
const addRemoveFriends = await Person.update({id: 'https://my.app/person1'}, {
const addRemoveFriends = await Person.update({
knows: {
add: [{id: 'https://my.app/person2'}],
remove: [{id: 'https://my.app/person3'}],
},
});
}).for({id: 'https://my.app/person1'});
```
This returns an object with the added and removed items
```json
Expand Down Expand Up @@ -559,6 +562,198 @@ Override behavior:
- If an override omits `minCount`, `maxCount`, or `nodeKind`, inherited values are kept.
- Current scope: compatibility checks for `datatype`, `class`, and `pattern` are not enforced yet.

## Dynamic Query Building

The DSL (`Person.select(...)`) is ideal when you know shapes at compile time. For apps that need to build queries at runtime — CMS dashboards, configurable reports, API endpoints that accept field selections — use `QueryBuilder` and `FieldSet`.

### QueryBuilder

`QueryBuilder` provides a fluent, chainable API for constructing queries programmatically. It accepts a Shape class or a shape IRI string.

```typescript
import {QueryBuilder} from '@_linked/core';

// From a Shape class
const query = QueryBuilder.from(Person)
.select(p => [p.name, p.knows])
.where(p => p.name.equals('Semmy'))
.limit(10);

// From a shape IRI string (when the Shape class isn't available at compile time)
const query = QueryBuilder.from('https://schema.org/Person')
.select(['name', 'knows'])
.where(p => p.name.equals('Semmy'));

// QueryBuilder is PromiseLike — await it directly
const results = await query;

// Or inspect the compiled IR without executing
const ir = query.build();
```

**Target specific entities:**
```typescript
// Single entity — result is unwrapped (not an array)
const person = await QueryBuilder.from(Person)
.for({id: 'https://my.app/person1'})
.select(p => p.name);

// Multiple entities
const people = await QueryBuilder.from(Person)
.forAll([{id: 'https://my.app/p1'}, {id: 'https://my.app/p2'}])
.select(p => p.name);
```

**Sorting, limiting, and single results:**
```typescript
const topFive = await QueryBuilder.from(Person)
.select(p => p.name)
.orderBy(p => p.name, 'ASC')
.limit(5);

const first = await QueryBuilder.from(Person)
.select(p => p.name)
.one();
```

**Select with a FieldSet:**
```typescript
const fields = FieldSet.for(Person, ['name', 'knows']);
const results = await QueryBuilder.from(Person).select(fields);
```

### FieldSet — composable field selections

`FieldSet` is an independent, reusable object that describes which fields to select from a shape. Create them, compose them, and feed them into queries.

**Creating a FieldSet:**
```typescript
import {FieldSet} from '@_linked/core';

// From a Shape class with string field names
const fs = FieldSet.for(Person, ['name', 'knows']);

// From a Shape class with a type-safe callback
const fs = FieldSet.for(Person, p => [p.name, p.knows]);

// From a shape IRI string (when you only have the shape's IRI)
const fs = FieldSet.for('https://schema.org/Person', ['name', 'knows']);

// Select all decorated properties
const allFields = FieldSet.all(Person);

// Select all properties with depth (includes nested shapes)
const deep = FieldSet.all(Person, {depth: 2});
```

**Nested fields:**
```typescript
// Dot-separated paths for nested properties
const fs = FieldSet.for(Person, ['name', 'knows.name']);

// Object form for nested sub-selections
const fs = FieldSet.for(Person, [{knows: ['name', 'hobby']}]);
```

**Composing FieldSets:**
```typescript
const base = FieldSet.for(Person, ['name']);

// Add fields
const extended = base.add(['knows', 'birthDate']);

// Remove fields
const minimal = extended.remove(['birthDate']);

// Pick specific fields
const picked = extended.pick(['name', 'knows']);

// Merge multiple FieldSets
const merged = FieldSet.merge([fieldSet1, fieldSet2]);
```

**Inspecting a FieldSet:**
```typescript
const fs = FieldSet.for(Person, ['name', 'knows']);
fs.labels(); // ['name', 'knows']
fs.paths(); // [PropertyPath, PropertyPath]
```

**Use cases:**

```typescript
// Dynamically selected fields from a UI
const fields = FieldSet.for(Person, userSelectedFields);
const results = await QueryBuilder.from(Person).select(fields);

// API gateway: accept fields as query parameters
const fields = FieldSet.for(Person, req.query.fields.split(','));
const results = await QueryBuilder.from(Person).select(fields);

// Component composition: merge field sets from child components
const merged = FieldSet.merge([headerFields, sidebarFields, contentFields]);
const results = await QueryBuilder.from(Person).select(merged);

// Progressive loading: start minimal, add detail on demand
const summary = FieldSet.for(Person, ['name']);
const detail = summary.add(['email', 'knows', 'birthDate']);
```

### Mutation Builders

The mutation builders are the programmatic equivalent of `Person.create(...)`, `Person.update(...)`, and `Person.delete(...)`. They accept Shape classes or shape IRI strings.

```typescript
import {CreateBuilder, UpdateBuilder, DeleteBuilder} from '@_linked/core';

// Create — equivalent to Person.create({name: 'Alice'})
const created = await CreateBuilder.from(Person)
.set({name: 'Alice'})
.withId('https://my.app/alice');

// Update — equivalent to Person.update({name: 'Alicia'}).for({id: '...'})
const updated = await UpdateBuilder.from(Person)
.for({id: 'https://my.app/alice'})
.set({name: 'Alicia'});

// Delete — equivalent to Person.delete({id: '...'})
const deleted = await DeleteBuilder.from(Person).for({id: 'https://my.app/alice'});

// All builders are PromiseLike — await them or call .build() for the IR
const ir = CreateBuilder.from(Person).set({name: 'Alice'}).build();
```

### JSON Serialization

Queries and FieldSets can be serialized to JSON and reconstructed — useful for saving query configurations, sending them over the wire, or building query editor UIs.

```typescript
// Serialize a QueryBuilder
const query = QueryBuilder.from(Person)
.select(p => [p.name, p.knows])
.where(p => p.name.equals('Semmy'));

const json = query.toJSON();
// json is a plain object — store it, send it, etc.

// Reconstruct from JSON
const restored = QueryBuilder.fromJSON(json);
const results = await restored;

// FieldSet serialization works the same way
const fs = FieldSet.for(Person, ['name', 'knows']);
const fsJson = fs.toJSON();
const restoredFs = FieldSet.fromJSON(fsJson);
```

Example JSON output for `QueryBuilder.from(Person).select(p => p.name).toJSON()`:
```json
{
"shape": "https://schema.org/Person",
"fields": [{"path": "name"}]
}
```

## TODO

- Allow `preloadFor` to accept another query (not just a component).
Expand Down
Loading
Loading