@ontrails/store 1.0.0-beta.14 → 1.0.0-beta.16

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 (57) hide show
  1. package/CHANGELOG.md +43 -0
  2. package/README.md +144 -63
  3. package/package.json +14 -9
  4. package/src/adapter-support.ts +163 -0
  5. package/src/crud-doctrine.ts +43 -0
  6. package/src/index.ts +17 -8
  7. package/src/jsonfile/index.ts +6 -0
  8. package/src/jsonfile/runtime.ts +700 -0
  9. package/src/jsonfile/types.ts +50 -0
  10. package/src/store.ts +298 -137
  11. package/src/testing.ts +175 -0
  12. package/src/trails/crud.ts +356 -0
  13. package/src/trails/index.ts +15 -0
  14. package/src/trails/reconcile.ts +281 -0
  15. package/src/trails/sync.ts +251 -0
  16. package/src/trails/utils.ts +96 -0
  17. package/src/types.ts +361 -68
  18. package/.agents/notes/2026-04-04/handoff-202604032309-9e85a104.md +0 -38
  19. package/.turbo/turbo-build.log +0 -1
  20. package/.turbo/turbo-lint.log +0 -3
  21. package/.turbo/turbo-typecheck.log +0 -1
  22. package/dist/drizzle/index.d.ts +0 -3
  23. package/dist/drizzle/index.d.ts.map +0 -1
  24. package/dist/drizzle/index.js +0 -2
  25. package/dist/drizzle/index.js.map +0 -1
  26. package/dist/drizzle/runtime.d.ts +0 -21
  27. package/dist/drizzle/runtime.d.ts.map +0 -1
  28. package/dist/drizzle/runtime.js +0 -458
  29. package/dist/drizzle/runtime.js.map +0 -1
  30. package/dist/drizzle/schema.d.ts +0 -15
  31. package/dist/drizzle/schema.d.ts.map +0 -1
  32. package/dist/drizzle/schema.js +0 -322
  33. package/dist/drizzle/schema.js.map +0 -1
  34. package/dist/drizzle/types.d.ts +0 -40
  35. package/dist/drizzle/types.d.ts.map +0 -1
  36. package/dist/drizzle/types.js +0 -2
  37. package/dist/drizzle/types.js.map +0 -1
  38. package/dist/index.d.ts +0 -3
  39. package/dist/index.d.ts.map +0 -1
  40. package/dist/index.js +0 -2
  41. package/dist/index.js.map +0 -1
  42. package/dist/store.d.ts +0 -26
  43. package/dist/store.d.ts.map +0 -1
  44. package/dist/store.js +0 -192
  45. package/dist/store.js.map +0 -1
  46. package/dist/types.d.ts +0 -224
  47. package/dist/types.d.ts.map +0 -1
  48. package/dist/types.js +0 -2
  49. package/dist/types.js.map +0 -1
  50. package/src/__tests__/store.test.ts +0 -333
  51. package/src/drizzle/__tests__/drizzle.test.ts +0 -469
  52. package/src/drizzle/index.ts +0 -17
  53. package/src/drizzle/runtime.ts +0 -853
  54. package/src/drizzle/schema.ts +0 -577
  55. package/src/drizzle/types.ts +0 -70
  56. package/tsconfig.json +0 -9
  57. package/tsconfig.tsbuildinfo +0 -1
package/CHANGELOG.md CHANGED
@@ -1,5 +1,48 @@
1
1
  # @ontrails/store
2
2
 
3
+ ## 1.0.0-beta.16
4
+
5
+ ### Major Changes
6
+
7
+ - 3395234: Move store adapter-binding helpers to `@ontrails/store/adapter-support` and topographer direct database/admin helpers to `@ontrails/topographer/backend-support`, keeping root exports focused on contract-level APIs.
8
+ - 6300f70: BREAKING: rename the shared store backend option type from `StoreConnectorOptions` to `StoreAdapterOptions`.
9
+
10
+ ### Patch Changes
11
+
12
+ - e898cc4: Add repo-level Knip dead-code detection and remove stale internal exports and unused package dependencies surfaced by the new check.
13
+ - 49c2e7d: Refresh published package README taxonomy to use adapter language instead of retired connector vocabulary.
14
+ - df9a7d0: Add project-aware public export-map governance for @ontrails workspace docs,
15
+ imports, root barrels, and bin-only package surfaces.
16
+ - Updated dependencies [73622ae]
17
+ - Updated dependencies [6300f70]
18
+ - Updated dependencies [d172013]
19
+ - Updated dependencies [c3fc5c3]
20
+ - Updated dependencies [20d7a5c]
21
+ - Updated dependencies [be5fb46]
22
+ - Updated dependencies [e898cc4]
23
+ - Updated dependencies [3395234]
24
+ - Updated dependencies [bcdc484]
25
+ - Updated dependencies [331e3a9]
26
+ - Updated dependencies [4399fdb]
27
+ - Updated dependencies [4b8d13b]
28
+ - Updated dependencies [112b9f2]
29
+ - Updated dependencies [893025e]
30
+ - Updated dependencies [eec5e9d]
31
+ - Updated dependencies [ebd4434]
32
+ - Updated dependencies [863d473]
33
+ - Updated dependencies [344f2f7]
34
+ - Updated dependencies [26f9ffd]
35
+ - Updated dependencies [10eae9a]
36
+ - Updated dependencies [22c6c06]
37
+ - @ontrails/core@1.0.0-beta.16
38
+
39
+ ## 1.0.0-beta.15
40
+
41
+ ### Patch Changes
42
+
43
+ - Updated dependencies [4ad6b25]
44
+ - @ontrails/core@1.0.0-beta.15
45
+
3
46
  ## 1.0.0-beta.14
4
47
 
5
48
  ### Minor Changes
package/README.md CHANGED
@@ -2,7 +2,7 @@
2
2
 
3
3
  Schema-derived persistence for Trails.
4
4
 
5
- The root package owns the connector-agnostic `store(...)` declaration. Connector packages such as `@ontrails/store/drizzle` bind that declaration to a concrete runtime, just like a trailhead connector binds a topo to CLI, MCP, or HTTP.
5
+ The root package owns the backend-agnostic `store(...)` declaration. External adapter packages such as `@ontrails/drizzle` bind that declaration to a concrete runtime, and first-party built-ins such as `@ontrails/store/jsonfile` live as opt-in subpaths on the same package.
6
6
 
7
7
  ## The two layers
8
8
 
@@ -14,13 +14,14 @@ import { store } from '@ontrails/store';
14
14
  export const db = store({
15
15
  gists: {
16
16
  schema: gistSchema,
17
- primaryKey: 'id',
17
+ identity: 'id',
18
18
  generated: ['id', 'createdAt', 'updatedAt'],
19
- indexes: ['owner', 'createdAt'],
19
+ indexed: ['owner', 'createdAt'],
20
+ versioned: true,
20
21
  },
21
22
  files: {
22
23
  schema: fileSchema,
23
- primaryKey: 'id',
24
+ identity: 'id',
24
25
  generated: ['id'],
25
26
  references: { gistId: 'gists' },
26
27
  },
@@ -33,8 +34,11 @@ This declaration is pure metadata:
33
34
  - insert schema
34
35
  - update schema
35
36
  - fixture schema
37
+ - derived change-signal handles (`table.signals.created|updated|removed`)
38
+ - identity field
36
39
  - generated-field metadata
37
- - indexes
40
+ - optional framework-managed version tracking
41
+ - indexed markers
38
42
  - references
39
43
 
40
44
  No database connection is opened here. The returned value is the durable authored source of truth.
@@ -43,12 +47,12 @@ No database connection is opened here. The returned value is the durable authore
43
47
 
44
48
  ```typescript
45
49
  import { store } from '@ontrails/store';
46
- import { connectDrizzle } from '@ontrails/store/drizzle';
50
+ import { connectDrizzle } from '@ontrails/drizzle';
47
51
 
48
52
  const definition = store({
49
53
  gists: {
50
54
  schema: gistSchema,
51
- primaryKey: 'id',
55
+ identity: 'id',
52
56
  generated: ['id', 'createdAt', 'updatedAt'],
53
57
  },
54
58
  });
@@ -59,11 +63,11 @@ export const db = connectDrizzle(definition, {
59
63
  });
60
64
  ```
61
65
 
62
- The bound store is a provision. Use it directly in trails:
66
+ The bound store is a resource. Use it directly in trails:
63
67
 
64
68
  ```typescript
65
69
  export const list = trail('gist.list', {
66
- provisions: [db],
70
+ resources: [db],
67
71
  intent: 'read',
68
72
  blaze: async (_input, ctx) => {
69
73
  const conn = db.from(ctx);
@@ -73,32 +77,91 @@ export const list = trail('gist.list', {
73
77
  });
74
78
  ```
75
79
 
80
+ ### Built-in local backend
81
+
82
+ For a zero-extra-package local backend, use the first-party JSON file binding:
83
+
84
+ ```typescript
85
+ import { store } from '@ontrails/store';
86
+ import { jsonFile } from '@ontrails/store/jsonfile';
87
+
88
+ const definition = store({
89
+ gists: {
90
+ schema: gistSchema,
91
+ identity: 'id',
92
+ generated: ['id', 'createdAt', 'updatedAt'],
93
+ },
94
+ });
95
+
96
+ export const db = jsonFile(definition, {
97
+ dir: './data',
98
+ });
99
+ ```
100
+
76
101
  ## Typed accessors
77
102
 
78
- Every table on a bound connection exposes typed CRUD accessors:
103
+ Every writable table on a bound connection exposes the backend-agnostic accessor contract:
79
104
 
80
105
  ```typescript
81
106
  const conn = db.from(ctx);
82
107
 
83
- const created = await conn.gists.insert({
84
- owner: 'matt',
108
+ const created = await conn.gists.upsert({
109
+ ownerId: 'matt',
85
110
  description: 'Hello, Trails',
86
111
  });
87
112
 
88
113
  const found = await conn.gists.get(created.id);
89
- const page = await conn.gists.list({ owner: 'matt' }, { limit: 20, offset: 0 });
90
- const updated = await conn.gists.update(created.id, {
114
+ const page = await conn.gists.list({ ownerId: 'matt' }, { limit: 20, offset: 0 });
115
+ const updated = await conn.gists.upsert({
91
116
  description: 'Updated description',
117
+ id: created.id,
118
+ ownerId: 'matt',
92
119
  });
93
120
  const removed = await conn.gists.remove(created.id);
94
121
  ```
95
122
 
96
123
  Types are derived from the Zod schema:
97
124
 
98
- - `insert()` uses the entity schema minus generated fields
99
- - `update()` uses the entity schema minus generated fields, then makes it partial
125
+ - `upsert()` uses the fixture/entity shape with generated fields optional
100
126
  - `get()` returns `Entity | null`
101
127
  - `list()` accepts typed partial filters and pagination options
128
+ - `versioned: true` adds a framework-managed `version` field to returned entities and lets `upsert()` accept an expected `version` for optimistic concurrency
129
+
130
+ Each normalized table also derives typed change signals from the same schema:
131
+
132
+ ```typescript
133
+ const createdHandle = definition.tables.gists.signals.created;
134
+ const updatedHandle = definition.tables.gists.signals.updated;
135
+ const removedHandle = definition.tables.gists.signals.removed;
136
+ ```
137
+
138
+ These pre-bind handles preserve payload shape, but the canonical signal id materializes only when an adapter binds the store to a resource. The bound form is always `resource:table.change`:
139
+
140
+ ```typescript
141
+ const created = db.store.tables.gists.signals.created;
142
+
143
+ created.id;
144
+ // "db.main:gists.created"
145
+ ```
146
+
147
+ ## Adapter Support Subpath
148
+
149
+ Adapter authors who bind a `store(...)` definition to a concrete backend should
150
+ import signal-binding helpers from `@ontrails/store/adapter-support`:
151
+
152
+ ```typescript
153
+ import { bindStoreDefinition } from '@ontrails/store/adapter-support';
154
+ ```
155
+
156
+ The subpath owns `bindStoreDefinition`, `createStoreTableSignals`,
157
+ `composeStoreSignalId`, `isValidResourceId`, and `StoreSignalChange`. The root
158
+ package stays focused on backend-agnostic store contracts.
159
+
160
+ Writable bindings fire those canonical scoped signals automatically when you access the resource through `db.from(ctx)` inside a trail context.
161
+
162
+ See [Store Signal Identity Migration](../../docs/store-signal-identity-migration.md) when updating existing `on:` clauses, surface-map fixtures, or custom resource wrappers from bare ids to scoped ids.
163
+
164
+ Tabular adapters such as `@ontrails/drizzle` also expose `insert()` and `update()` as convenience methods when the backend natively distinguishes create and patch operations.
102
165
 
103
166
  ## Fixtures and mocks
104
167
 
@@ -108,48 +171,47 @@ Fixtures belong on the root definition:
108
171
  export const db = store({
109
172
  gists: {
110
173
  schema: gistSchema,
111
- primaryKey: 'id',
174
+ identity: 'id',
112
175
  generated: ['id', 'createdAt', 'updatedAt'],
113
176
  fixtures: [
114
- { id: 'g_1', owner: 'matt', description: 'Seed gist' },
177
+ { id: 'g_1', ownerId: 'matt', description: 'Seed gist' },
115
178
  ],
116
179
  },
117
180
  });
118
181
  ```
119
182
 
120
- When a connector binds the store, those fixtures feed the provision mock automatically. Connector options can also add or override seed data for tests.
183
+ When an adapter binds the store, those fixtures feed the resource mock automatically. Adapter options can also add or override seed data for tests.
121
184
 
122
- That means `testAll(app)` can auto-resolve connector-bound store provisions without extra ceremony, as long as the provision is registered in the topo.
185
+ That means `testAll(app)` can auto-resolve adapter-bound store resources without extra ceremony, as long as the resource is registered in the topo.
123
186
 
124
187
  ## Read-only bindings
125
188
 
126
- Use the Drizzle connector's read-only helpers when a trail should inspect persisted state without exposing writes:
189
+ Use the Drizzle adapter's read-only binding when a trail should inspect persisted state without exposing writes:
127
190
 
128
191
  ```typescript
129
- import { connectReadOnlyDrizzle, readonlyStore } from '@ontrails/store/drizzle';
192
+ import { connectReadOnlyDrizzle } from '@ontrails/drizzle';
130
193
 
131
194
  const analytics = connectReadOnlyDrizzle(definition, {
132
195
  id: 'analytics.db',
133
196
  url: './data/analytics.sqlite',
134
197
  });
198
+ ```
135
199
 
136
- const auditLog = readonlyStore(
137
- {
138
- entries: {
139
- schema: auditEntrySchema,
140
- primaryKey: 'id',
141
- generated: ['id', 'createdAt'],
142
- },
143
- },
144
- { id: 'audit.db', url: './data/audit.sqlite' }
145
- );
200
+ Read-only bindings expose `get()`, `list()`, and `query()`, but not `upsert()`, `remove()`, `insert()`, or `update()`.
201
+
202
+ ## Accessor contract testing
203
+
204
+ Adapters can reuse the shared writable-accessor contract tests from `@ontrails/store/testing`:
205
+
206
+ ```typescript
207
+ import { createStoreAccessorContractCases } from '@ontrails/store/testing';
146
208
  ```
147
209
 
148
- Read-only bindings expose `get()`, `list()`, and `query()`, but not `insert()`, `update()`, or `remove()`.
210
+ That helper provides reusable cases for the baseline `get()`, `list()`, `upsert()`, and `remove()` behavior so adapter suites only need to wrap them with their normal `test(...)` calls and add backend-specific coverage on top.
149
211
 
150
212
  ## Drizzle escape hatch
151
213
 
152
- Complex queries use the connector-native query builder through `query()`:
214
+ Complex queries use the adapter-native query builder through `query()`:
153
215
 
154
216
  ```typescript
155
217
  const conn = db.from(ctx);
@@ -161,53 +223,72 @@ const rows = await conn.query(({ drizzle, tables }) =>
161
223
  );
162
224
  ```
163
225
 
164
- This keeps the default happy path derived and typed, while still giving you full access to the underlying connector when the CRUD accessors are not enough.
226
+ This keeps the default happy path derived and typed, while still giving you full access to the underlying adapter when the CRUD accessors are not enough.
165
227
 
166
- ## Connector conveniences
228
+ ## Adapter binding
167
229
 
168
- `@ontrails/store/drizzle` also exports one-line conveniences when you want declaration and binding together:
230
+ `@ontrails/drizzle` keeps the durable `store(...)` declaration in
231
+ `@ontrails/store` and binds it to a concrete runtime:
169
232
 
170
233
  ```typescript
171
- import { store, readonlyStore } from '@ontrails/store/drizzle';
172
-
173
- export const writable = store(
174
- {
175
- gists: {
176
- schema: gistSchema,
177
- primaryKey: 'id',
178
- generated: ['id', 'createdAt', 'updatedAt'],
179
- },
180
- },
181
- { url: ':memory:' }
182
- );
234
+ import { connectDrizzle, connectReadOnlyDrizzle } from '@ontrails/drizzle';
235
+ import { store } from '@ontrails/store';
183
236
 
184
- export const readonly = readonlyStore(
185
- {
186
- gists: {
187
- schema: gistSchema,
188
- primaryKey: 'id',
189
- generated: ['id', 'createdAt', 'updatedAt'],
190
- },
237
+ const definition = store({
238
+ gists: {
239
+ schema: gistSchema,
240
+ identity: 'id',
241
+ generated: ['id', 'createdAt', 'updatedAt'],
191
242
  },
192
- { url: './data/gists.sqlite' }
193
- );
243
+ });
244
+
245
+ export const writable = connectDrizzle(definition, { url: ':memory:' });
246
+
247
+ export const readonly = connectReadOnlyDrizzle(definition, {
248
+ url: './data/gists.sqlite',
249
+ });
194
250
  ```
195
251
 
196
- These are conveniences, not the architectural source of truth. The root package still owns the durable authored `store(...)` model.
252
+ The root package still owns the authored persistence model; adapter packages
253
+ project that model into runnable resources.
197
254
 
198
255
  ## Schema export for external tooling
199
256
 
200
- If you need the raw derived Drizzle tables for tooling such as `drizzle-kit`, use `getSchema()`:
257
+ If you need the raw derived Drizzle tables for tooling such as `drizzle-kit`,
258
+ read them from the bound resource's `tables` field:
201
259
 
202
260
  ```typescript
203
- import { getSchema } from '@ontrails/store/drizzle';
261
+ import { connectDrizzle } from '@ontrails/drizzle';
262
+ import { store } from '@ontrails/store';
263
+
264
+ const definition = store({
265
+ gists: {
266
+ schema: gistSchema,
267
+ identity: 'id',
268
+ generated: ['id', 'createdAt', 'updatedAt'],
269
+ },
270
+ });
204
271
 
205
- const schema = getSchema(db);
272
+ const db = connectDrizzle(definition, { url: ':memory:' });
273
+ const schema = db.tables;
206
274
  ```
207
275
 
208
276
  ## Installation
209
277
 
210
278
  ```bash
211
279
  bun add @ontrails/store zod
212
- bun add drizzle-orm
213
280
  ```
281
+
282
+ Add Drizzle only when you want the external SQLite/ORM adapter:
283
+
284
+ ```bash
285
+ bun add @ontrails/drizzle
286
+ ```
287
+
288
+ ## Migration
289
+
290
+ The Drizzle binding now lives in `@ontrails/drizzle`.
291
+
292
+ <!-- warden-ignore-next-line -->
293
+ - Replace `import { ... } from '@ontrails/store/drizzle'` with `import { ... } from '@ontrails/drizzle'`
294
+ - Keep backend-agnostic store declarations on `@ontrails/store`
package/package.json CHANGED
@@ -1,10 +1,21 @@
1
1
  {
2
2
  "name": "@ontrails/store",
3
- "version": "1.0.0-beta.14",
3
+ "version": "1.0.0-beta.16",
4
+ "files": [
5
+ "src/**/*.ts",
6
+ "!src/**/__tests__/**",
7
+ "!src/**/*.test.ts",
8
+ "!src/**/*.test-d.ts",
9
+ "README.md",
10
+ "CHANGELOG.md"
11
+ ],
4
12
  "type": "module",
5
13
  "exports": {
6
14
  ".": "./src/index.ts",
7
- "./drizzle": "./src/drizzle/index.ts",
15
+ "./adapter-support": "./src/adapter-support.ts",
16
+ "./jsonfile": "./src/jsonfile/index.ts",
17
+ "./trails": "./src/trails/index.ts",
18
+ "./testing": "./src/testing.ts",
8
19
  "./package.json": "./package.json"
9
20
  },
10
21
  "scripts": {
@@ -15,15 +26,9 @@
15
26
  "clean": "rm -rf dist *.tsbuildinfo"
16
27
  },
17
28
  "dependencies": {
18
- "@ontrails/core": "^1.0.0-beta.13"
29
+ "@ontrails/core": "^1.0.0-beta.15"
19
30
  },
20
31
  "peerDependencies": {
21
- "drizzle-orm": "^0.45.2",
22
32
  "zod": "^4.3.5"
23
- },
24
- "peerDependenciesMeta": {
25
- "drizzle-orm": {
26
- "optional": true
27
- }
28
33
  }
29
34
  }
@@ -0,0 +1,163 @@
1
+ import {
2
+ attachLateBoundSignalRef,
3
+ cloneSignalWithId,
4
+ signal,
5
+ ValidationError,
6
+ } from '@ontrails/core';
7
+ import type { Signal } from '@ontrails/core';
8
+ import type { z } from 'zod';
9
+
10
+ import type {
11
+ AnyStoreDefinition,
12
+ AnyStoreTable,
13
+ StoreTableSignals,
14
+ } from './types.js';
15
+
16
+ type MutableTables<TStore extends AnyStoreDefinition> = {
17
+ -readonly [TName in keyof TStore['tables']]: TStore['tables'][TName];
18
+ };
19
+
20
+ type StoreSignalChange = 'created' | 'removed' | 'updated';
21
+
22
+ const createStoreSignalDescription = (
23
+ tableName: string,
24
+ change: StoreSignalChange
25
+ ): string => {
26
+ switch (change) {
27
+ case 'created': {
28
+ return `Fired after a "${tableName}" entity is created.`;
29
+ }
30
+ case 'removed': {
31
+ return `Fired after a "${tableName}" entity is removed.`;
32
+ }
33
+ case 'updated': {
34
+ return `Fired after a "${tableName}" entity is updated.`;
35
+ }
36
+ default: {
37
+ throw new Error(`Unsupported store signal change: ${change as string}`);
38
+ }
39
+ }
40
+ };
41
+
42
+ const createStoreSignal = <TPayload>(
43
+ tableName: string,
44
+ change: StoreSignalChange,
45
+ payload: z.ZodType<TPayload>
46
+ ): Signal<TPayload> =>
47
+ attachLateBoundSignalRef(
48
+ signal(`${tableName}.${change}`, {
49
+ description: createStoreSignalDescription(tableName, change),
50
+ payload,
51
+ }),
52
+ {
53
+ kind: 'store-derived',
54
+ token: Bun.randomUUIDv7(),
55
+ }
56
+ );
57
+
58
+ export const createStoreTableSignals = <TPayload>(
59
+ tableName: string,
60
+ payload: z.ZodType<TPayload>
61
+ ): StoreTableSignals<TPayload> =>
62
+ Object.freeze({
63
+ created: createStoreSignal(tableName, 'created', payload),
64
+ removed: createStoreSignal(tableName, 'removed', payload),
65
+ updated: createStoreSignal(tableName, 'updated', payload),
66
+ });
67
+
68
+ export const composeStoreSignalId = (
69
+ scope: string,
70
+ tableName: string,
71
+ change: StoreSignalChange
72
+ ): string => `${scope}:${tableName}.${change}`;
73
+
74
+ const bindTableSignals = (
75
+ scope: string,
76
+ table: AnyStoreTable
77
+ ): StoreTableSignals<unknown> =>
78
+ Object.freeze({
79
+ created: cloneSignalWithId(
80
+ table.signals.created,
81
+ composeStoreSignalId(scope, table.name, 'created')
82
+ ),
83
+ removed: cloneSignalWithId(
84
+ table.signals.removed,
85
+ composeStoreSignalId(scope, table.name, 'removed')
86
+ ),
87
+ updated: cloneSignalWithId(
88
+ table.signals.updated,
89
+ composeStoreSignalId(scope, table.name, 'updated')
90
+ ),
91
+ });
92
+
93
+ const collectStoreSignals = <TStore extends AnyStoreDefinition>(
94
+ normalized: MutableTables<TStore>,
95
+ tableNames: readonly Extract<keyof TStore['tables'], string>[]
96
+ ) =>
97
+ Object.freeze(
98
+ tableNames.flatMap((name) => {
99
+ const table = normalized[name];
100
+ return table === undefined
101
+ ? []
102
+ : [table.signals.created, table.signals.updated, table.signals.removed];
103
+ })
104
+ );
105
+
106
+ /**
107
+ * Verifies that a resource id is safe to compose into a scoped signal id.
108
+ *
109
+ * Scoped signal ids are matched by the `SCOPED_SIGNAL_ID` pattern in
110
+ * `@ontrails/core` (`^[^:\s]+:[^:.\s]+(?:\.[^:.\s]+)+$`). A resource id used
111
+ * as the scope segment must therefore be a non-empty string that contains
112
+ * neither `":"` nor any whitespace.
113
+ */
114
+ export const isValidResourceId = (resourceId: string): boolean =>
115
+ resourceId.length > 0 && !resourceId.includes(':') && !/\s/.test(resourceId);
116
+
117
+ const assertValidScope = (scope: string): void => {
118
+ if (!isValidResourceId(scope)) {
119
+ throw new ValidationError(
120
+ `Store resource id "${scope}" is invalid: must be a non-empty string with no ":" characters and no whitespace.`
121
+ );
122
+ }
123
+ };
124
+
125
+ export const bindStoreDefinition = <TStore extends AnyStoreDefinition>(
126
+ definition: TStore,
127
+ scope: string
128
+ ): TStore => {
129
+ assertValidScope(scope);
130
+
131
+ const tableNames = definition.tableNames as readonly Extract<
132
+ keyof TStore['tables'],
133
+ string
134
+ >[];
135
+ const tables = {} as MutableTables<TStore>;
136
+
137
+ for (const tableName of tableNames) {
138
+ const table = definition.tables[tableName];
139
+ if (table === undefined) {
140
+ continue;
141
+ }
142
+
143
+ tables[tableName] = Object.freeze({
144
+ ...table,
145
+ signals: bindTableSignals(scope, table),
146
+ }) as MutableTables<TStore>[typeof tableName];
147
+ }
148
+
149
+ const get =
150
+ 'get' in definition && typeof definition.get === 'function'
151
+ ? <TName extends Extract<keyof TStore['tables'], string>>(name: TName) =>
152
+ tables[name]
153
+ : undefined;
154
+
155
+ return Object.freeze({
156
+ ...definition,
157
+ ...(get ? { get } : {}),
158
+ signals: collectStoreSignals(tables, tableNames),
159
+ tables: Object.freeze(tables),
160
+ }) as TStore;
161
+ };
162
+
163
+ export type { StoreSignalChange };
@@ -0,0 +1,43 @@
1
+ export const crudOperations = [
2
+ 'create',
3
+ 'read',
4
+ 'update',
5
+ 'delete',
6
+ 'list',
7
+ ] as const;
8
+
9
+ export type CrudOperation = (typeof crudOperations)[number];
10
+
11
+ export interface CrudAccessorExpectation {
12
+ readonly fallback?: string | undefined;
13
+ readonly preferred: string;
14
+ readonly severityWhenNoFallback: 'error';
15
+ readonly severityWhenPreferredMissingWithFallback?: 'warn' | undefined;
16
+ }
17
+
18
+ export const crudAccessorExpectations = {
19
+ create: {
20
+ fallback: 'upsert',
21
+ preferred: 'insert',
22
+ severityWhenNoFallback: 'error',
23
+ severityWhenPreferredMissingWithFallback: 'warn',
24
+ },
25
+ delete: {
26
+ preferred: 'remove',
27
+ severityWhenNoFallback: 'error',
28
+ },
29
+ list: {
30
+ preferred: 'list',
31
+ severityWhenNoFallback: 'error',
32
+ },
33
+ read: {
34
+ preferred: 'get',
35
+ severityWhenNoFallback: 'error',
36
+ },
37
+ update: {
38
+ fallback: 'upsert',
39
+ preferred: 'update',
40
+ severityWhenNoFallback: 'error',
41
+ severityWhenPreferredMissingWithFallback: 'warn',
42
+ },
43
+ } as const satisfies Record<CrudOperation, CrudAccessorExpectation>;
package/src/index.ts CHANGED
@@ -1,10 +1,9 @@
1
- export {
2
- entitySchemaOf,
3
- fixtureSchemaOf,
4
- insertSchemaOf,
5
- store,
6
- updateSchemaOf,
7
- } from './store.js';
1
+ export { crudAccessorExpectations, crudOperations } from './crud-doctrine.js';
2
+ export type {
3
+ CrudAccessorExpectation,
4
+ CrudOperation,
5
+ } from './crud-doctrine.js';
6
+ export { store, versionFieldName } from './store.js';
8
7
  export type {
9
8
  AnyStoreDefinition,
10
9
  AnyStoreTable,
@@ -15,25 +14,35 @@ export type {
15
14
  FixturesOfInput,
16
15
  GeneratedFieldsOfInput,
17
16
  GeneratedKeysOf,
17
+ IdentityFieldOfInput,
18
+ IdentityOf,
18
19
  IndexFieldsOfInput,
20
+ IndexedFieldsOfInput,
19
21
  InsertOf,
20
- PrimaryKeyOf,
21
22
  ReadOnlyStoreConnection,
23
+ StoreAccessor,
22
24
  ReadOnlyStoreTableAccessor,
23
25
  ReferencesOfInput,
24
26
  StoreAccessMode,
25
27
  StoreConnection,
28
+ StoreAdapterOptions,
26
29
  StoreDefinition,
27
30
  StoreFieldKey,
28
31
  StoreFixtureInput,
29
32
  StoreFixtureRow,
30
33
  StoreIdentifierOf,
34
+ StoreKind,
31
35
  StoreListOptions,
36
+ StoreMockSeed,
32
37
  StoreObjectSchema,
38
+ StoreOptions,
33
39
  StoreSearchDefinition,
34
40
  StoreTable,
41
+ StoreTableSignals,
35
42
  StoreTableAccessor,
43
+ StoreTableConnection,
36
44
  StoreTableInput,
37
45
  StoreTablesInput,
46
+ UpsertOf,
38
47
  UpdateOf,
39
48
  } from './types.js';
@@ -0,0 +1,6 @@
1
+ export { connectJsonFile, jsonFile, jsonFile as store } from './runtime.js';
2
+ export type {
3
+ JsonFileConnection,
4
+ JsonFileStoreOptions,
5
+ JsonFileStoreResource,
6
+ } from './types.js';