@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.
- package/CHANGELOG.md +43 -0
- package/README.md +144 -63
- package/package.json +14 -9
- package/src/adapter-support.ts +163 -0
- package/src/crud-doctrine.ts +43 -0
- package/src/index.ts +17 -8
- package/src/jsonfile/index.ts +6 -0
- package/src/jsonfile/runtime.ts +700 -0
- package/src/jsonfile/types.ts +50 -0
- package/src/store.ts +298 -137
- package/src/testing.ts +175 -0
- package/src/trails/crud.ts +356 -0
- package/src/trails/index.ts +15 -0
- package/src/trails/reconcile.ts +281 -0
- package/src/trails/sync.ts +251 -0
- package/src/trails/utils.ts +96 -0
- package/src/types.ts +361 -68
- package/.agents/notes/2026-04-04/handoff-202604032309-9e85a104.md +0 -38
- package/.turbo/turbo-build.log +0 -1
- package/.turbo/turbo-lint.log +0 -3
- package/.turbo/turbo-typecheck.log +0 -1
- package/dist/drizzle/index.d.ts +0 -3
- package/dist/drizzle/index.d.ts.map +0 -1
- package/dist/drizzle/index.js +0 -2
- package/dist/drizzle/index.js.map +0 -1
- package/dist/drizzle/runtime.d.ts +0 -21
- package/dist/drizzle/runtime.d.ts.map +0 -1
- package/dist/drizzle/runtime.js +0 -458
- package/dist/drizzle/runtime.js.map +0 -1
- package/dist/drizzle/schema.d.ts +0 -15
- package/dist/drizzle/schema.d.ts.map +0 -1
- package/dist/drizzle/schema.js +0 -322
- package/dist/drizzle/schema.js.map +0 -1
- package/dist/drizzle/types.d.ts +0 -40
- package/dist/drizzle/types.d.ts.map +0 -1
- package/dist/drizzle/types.js +0 -2
- package/dist/drizzle/types.js.map +0 -1
- package/dist/index.d.ts +0 -3
- package/dist/index.d.ts.map +0 -1
- package/dist/index.js +0 -2
- package/dist/index.js.map +0 -1
- package/dist/store.d.ts +0 -26
- package/dist/store.d.ts.map +0 -1
- package/dist/store.js +0 -192
- package/dist/store.js.map +0 -1
- package/dist/types.d.ts +0 -224
- package/dist/types.d.ts.map +0 -1
- package/dist/types.js +0 -2
- package/dist/types.js.map +0 -1
- package/src/__tests__/store.test.ts +0 -333
- package/src/drizzle/__tests__/drizzle.test.ts +0 -469
- package/src/drizzle/index.ts +0 -17
- package/src/drizzle/runtime.ts +0 -853
- package/src/drizzle/schema.ts +0 -577
- package/src/drizzle/types.ts +0 -70
- package/tsconfig.json +0 -9
- 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
|
|
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
|
-
|
|
17
|
+
identity: 'id',
|
|
18
18
|
generated: ['id', 'createdAt', 'updatedAt'],
|
|
19
|
-
|
|
19
|
+
indexed: ['owner', 'createdAt'],
|
|
20
|
+
versioned: true,
|
|
20
21
|
},
|
|
21
22
|
files: {
|
|
22
23
|
schema: fileSchema,
|
|
23
|
-
|
|
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
|
-
-
|
|
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/
|
|
50
|
+
import { connectDrizzle } from '@ontrails/drizzle';
|
|
47
51
|
|
|
48
52
|
const definition = store({
|
|
49
53
|
gists: {
|
|
50
54
|
schema: gistSchema,
|
|
51
|
-
|
|
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
|
|
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
|
-
|
|
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
|
|
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.
|
|
84
|
-
|
|
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({
|
|
90
|
-
const updated = await conn.gists.
|
|
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
|
-
- `
|
|
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
|
-
|
|
174
|
+
identity: 'id',
|
|
112
175
|
generated: ['id', 'createdAt', 'updatedAt'],
|
|
113
176
|
fixtures: [
|
|
114
|
-
{ id: 'g_1',
|
|
177
|
+
{ id: 'g_1', ownerId: 'matt', description: 'Seed gist' },
|
|
115
178
|
],
|
|
116
179
|
},
|
|
117
180
|
});
|
|
118
181
|
```
|
|
119
182
|
|
|
120
|
-
When
|
|
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
|
|
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
|
|
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
|
|
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
|
-
|
|
137
|
-
|
|
138
|
-
|
|
139
|
-
|
|
140
|
-
|
|
141
|
-
|
|
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
|
-
|
|
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
|
|
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
|
|
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
|
-
##
|
|
228
|
+
## Adapter binding
|
|
167
229
|
|
|
168
|
-
`@ontrails/
|
|
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 {
|
|
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
|
-
|
|
185
|
-
{
|
|
186
|
-
|
|
187
|
-
|
|
188
|
-
|
|
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
|
-
|
|
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
|
-
|
|
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`,
|
|
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 {
|
|
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
|
|
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.
|
|
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
|
-
"./
|
|
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.
|
|
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
|
-
|
|
3
|
-
|
|
4
|
-
|
|
5
|
-
|
|
6
|
-
|
|
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';
|