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

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 (92) hide show
  1. package/.turbo/turbo-lint.log +1 -1
  2. package/CHANGELOG.md +7 -0
  3. package/README.md +126 -61
  4. package/dist/index.d.ts +2 -2
  5. package/dist/index.d.ts.map +1 -1
  6. package/dist/index.js +1 -1
  7. package/dist/index.js.map +1 -1
  8. package/dist/internal/signal-identity.d.ts +17 -0
  9. package/dist/internal/signal-identity.d.ts.map +1 -0
  10. package/dist/internal/signal-identity.js +81 -0
  11. package/dist/internal/signal-identity.js.map +1 -0
  12. package/dist/jsonfile/index.d.ts +3 -0
  13. package/dist/jsonfile/index.d.ts.map +1 -0
  14. package/dist/jsonfile/index.js +2 -0
  15. package/dist/jsonfile/index.js.map +1 -0
  16. package/dist/jsonfile/runtime.d.ts +19 -0
  17. package/dist/jsonfile/runtime.d.ts.map +1 -0
  18. package/dist/jsonfile/runtime.js +403 -0
  19. package/dist/jsonfile/runtime.js.map +1 -0
  20. package/dist/jsonfile/types.d.ts +40 -0
  21. package/dist/jsonfile/types.d.ts.map +1 -0
  22. package/dist/{drizzle → jsonfile}/types.js.map +1 -1
  23. package/dist/store.d.ts +8 -18
  24. package/dist/store.d.ts.map +1 -1
  25. package/dist/store.js +152 -94
  26. package/dist/store.js.map +1 -1
  27. package/dist/testing.d.ts +28 -0
  28. package/dist/testing.d.ts.map +1 -0
  29. package/dist/testing.js +90 -0
  30. package/dist/testing.js.map +1 -0
  31. package/dist/trails/crud.d.ts +38 -0
  32. package/dist/trails/crud.d.ts.map +1 -0
  33. package/dist/trails/crud.js +112 -0
  34. package/dist/trails/crud.js.map +1 -0
  35. package/dist/trails/index.d.ts +7 -0
  36. package/dist/trails/index.d.ts.map +1 -0
  37. package/dist/trails/index.js +4 -0
  38. package/dist/trails/index.js.map +1 -0
  39. package/dist/trails/reconcile.d.ts +36 -0
  40. package/dist/trails/reconcile.d.ts.map +1 -0
  41. package/dist/trails/reconcile.js +142 -0
  42. package/dist/trails/reconcile.js.map +1 -0
  43. package/dist/trails/sync.d.ts +25 -0
  44. package/dist/trails/sync.d.ts.map +1 -0
  45. package/dist/trails/sync.js +96 -0
  46. package/dist/trails/sync.js.map +1 -0
  47. package/dist/trails/utils.d.ts +51 -0
  48. package/dist/trails/utils.d.ts.map +1 -0
  49. package/dist/trails/utils.js +65 -0
  50. package/dist/trails/utils.js.map +1 -0
  51. package/dist/types.d.ts +205 -35
  52. package/dist/types.d.ts.map +1 -1
  53. package/package.json +6 -9
  54. package/src/__tests__/crud.test.ts +346 -0
  55. package/src/__tests__/jsonfile.test.ts +502 -0
  56. package/src/__tests__/store.test.ts +239 -33
  57. package/src/__tests__/sync-reconcile.test.ts +646 -0
  58. package/src/index.ts +12 -8
  59. package/src/internal/signal-identity.ts +162 -0
  60. package/src/jsonfile/index.ts +6 -0
  61. package/src/jsonfile/runtime.ts +702 -0
  62. package/src/jsonfile/types.ts +50 -0
  63. package/src/store.ts +296 -134
  64. package/src/testing.ts +175 -0
  65. package/src/trails/crud.ts +355 -0
  66. package/src/trails/index.ts +10 -0
  67. package/src/trails/reconcile.ts +281 -0
  68. package/src/trails/sync.ts +251 -0
  69. package/src/trails/utils.ts +96 -0
  70. package/src/types.ts +355 -63
  71. package/tsconfig.tests.json +10 -0
  72. package/tsconfig.tsbuildinfo +1 -1
  73. package/dist/drizzle/index.d.ts +0 -3
  74. package/dist/drizzle/index.d.ts.map +0 -1
  75. package/dist/drizzle/index.js +0 -2
  76. package/dist/drizzle/index.js.map +0 -1
  77. package/dist/drizzle/runtime.d.ts +0 -21
  78. package/dist/drizzle/runtime.d.ts.map +0 -1
  79. package/dist/drizzle/runtime.js +0 -458
  80. package/dist/drizzle/runtime.js.map +0 -1
  81. package/dist/drizzle/schema.d.ts +0 -15
  82. package/dist/drizzle/schema.d.ts.map +0 -1
  83. package/dist/drizzle/schema.js +0 -322
  84. package/dist/drizzle/schema.js.map +0 -1
  85. package/dist/drizzle/types.d.ts +0 -40
  86. package/dist/drizzle/types.d.ts.map +0 -1
  87. package/src/drizzle/__tests__/drizzle.test.ts +0 -469
  88. package/src/drizzle/index.ts +0 -17
  89. package/src/drizzle/runtime.ts +0 -853
  90. package/src/drizzle/schema.ts +0 -577
  91. package/src/drizzle/types.ts +0 -70
  92. /package/dist/{drizzle → jsonfile}/types.js +0 -0
@@ -3,19 +3,15 @@ import { ValidationError } from '@ontrails/core';
3
3
  import { z } from 'zod';
4
4
 
5
5
  import type {
6
+ AnyStoreTable,
6
7
  EntityOf,
7
8
  FixtureInputOf,
8
9
  FixtureOf,
9
10
  InsertOf,
10
11
  UpdateOf,
11
12
  } from '../index.js';
12
- import {
13
- entitySchemaOf,
14
- fixtureSchemaOf,
15
- insertSchemaOf,
16
- store,
17
- updateSchemaOf,
18
- } from '../index.js';
13
+ import { store } from '../index.js';
14
+ import { bindStoreDefinition } from '../internal/signal-identity.js';
19
15
 
20
16
  const userSchema = z.object({
21
17
  email: z.string().email(),
@@ -36,37 +32,64 @@ const createStoreDefinition = () =>
36
32
  store({
37
33
  gists: {
38
34
  generated: ['id', 'createdAt', 'updatedAt'],
39
- indexes: ['ownerId'],
40
- primaryKey: 'id',
35
+ identity: 'id',
36
+ indexed: ['ownerId'],
41
37
  references: { ownerId: 'users' },
42
38
  schema: gistSchema,
43
39
  search: { fts: true },
44
40
  },
45
41
  users: {
46
42
  generated: ['id'],
47
- primaryKey: 'id',
43
+ identity: 'id',
48
44
  schema: userSchema,
49
45
  },
50
46
  });
51
47
 
48
+ const expectGistSignals = (
49
+ table: ReturnType<typeof createStoreDefinition>['tables']['gists']
50
+ ) => {
51
+ expect(table.signals.created.id).toBe('gists.created');
52
+ expect(table.signals.updated.id).toBe('gists.updated');
53
+ expect(table.signals.removed.id).toBe('gists.removed');
54
+ expect(
55
+ table.signals.created.payload.parse({
56
+ createdAt: '2026-04-03T12:00:00.000Z',
57
+ id: 'gist-1',
58
+ ownerId: 'user-1',
59
+ updatedAt: '2026-04-03T12:00:00.000Z',
60
+ })
61
+ ).toEqual({
62
+ createdAt: '2026-04-03T12:00:00.000Z',
63
+ description: null,
64
+ id: 'gist-1',
65
+ isPublic: true,
66
+ ownerId: 'user-1',
67
+ tags: [],
68
+ updatedAt: '2026-04-03T12:00:00.000Z',
69
+ });
70
+ };
71
+
52
72
  const expectNormalizedGistTable = (
53
73
  table: ReturnType<typeof createStoreDefinition>['tables']['gists']
54
74
  ) => {
55
75
  expect(table.name).toBe('gists');
76
+ expect(table.identity).toBe('id');
56
77
  expect(table.primaryKey).toBe('id');
57
78
  expect(table.generated).toEqual(['id', 'createdAt', 'updatedAt']);
79
+ expect(table.indexed).toEqual(['ownerId']);
58
80
  expect(table.indexes).toEqual(['ownerId']);
59
81
  expect(table.references).toEqual({ ownerId: 'users' });
60
82
  expect(table.search).toEqual({ fts: true });
83
+ expectGistSignals(table);
61
84
  };
62
85
 
63
86
  const expectDerivedSchemas = (
64
87
  table: ReturnType<typeof createStoreDefinition>['tables']['gists']
65
88
  ) => {
66
- expect(entitySchemaOf(table)).toBe(table.schema);
67
- expect(fixtureSchemaOf(table)).toBe(table.fixtureSchema);
68
- expect(insertSchemaOf(table)).toBe(table.insertSchema);
69
- expect(updateSchemaOf(table)).toBe(table.updateSchema);
89
+ expect(table.schema).toBeDefined();
90
+ expect(table.fixtureSchema).toBeDefined();
91
+ expect(table.insertSchema).toBeDefined();
92
+ expect(table.updateSchema).toBeDefined();
70
93
 
71
94
  expect(
72
95
  table.insertSchema.parse({
@@ -92,14 +115,26 @@ const createTypeTestStore = () =>
92
115
  store({
93
116
  gists: {
94
117
  generated: ['id', 'createdAt', 'updatedAt'],
95
- primaryKey: 'id',
118
+ identity: 'id',
119
+ schema: gistSchema,
120
+ },
121
+ });
122
+
123
+ const createVersionedStoreDefinition = () =>
124
+ store({
125
+ gists: {
126
+ generated: ['id', 'createdAt', 'updatedAt'],
127
+ identity: 'id',
96
128
  schema: gistSchema,
129
+ versioned: true,
97
130
  },
98
131
  });
99
132
 
100
- const createGistEntity = <
101
- TTable extends Parameters<typeof entitySchemaOf>[0],
102
- >() =>
133
+ type VersionedGistTable = ReturnType<
134
+ typeof createVersionedStoreDefinition
135
+ >['tables']['gists'];
136
+
137
+ const createGistEntity = <TTable extends AnyStoreTable>() =>
103
138
  ({
104
139
  createdAt: '2026-04-03T12:00:00.000Z',
105
140
  description: null,
@@ -118,12 +153,66 @@ const requireFixture = <T>(fixture: T | undefined): T => {
118
153
  return fixture;
119
154
  };
120
155
 
156
+ const createVersionedGistTypeSamples = (): {
157
+ readonly entity: EntityOf<VersionedGistTable>;
158
+ readonly fixtureInput: FixtureInputOf<VersionedGistTable>;
159
+ readonly insertInput: InsertOf<VersionedGistTable>;
160
+ } => ({
161
+ entity: {
162
+ createdAt: '2026-04-03T12:00:00.000Z',
163
+ description: null,
164
+ id: 'gist-1',
165
+ isPublic: true,
166
+ ownerId: 'user-1',
167
+ tags: ['core'],
168
+ updatedAt: '2026-04-03T12:00:00.000Z',
169
+ version: 1,
170
+ },
171
+ fixtureInput: {
172
+ ownerId: 'user-2',
173
+ version: 2,
174
+ },
175
+ insertInput: {
176
+ ownerId: 'user-3',
177
+ },
178
+ });
179
+
180
+ const expectVersionedGistTypeSamples = ({
181
+ entity,
182
+ fixtureInput,
183
+ insertInput,
184
+ }: ReturnType<typeof createVersionedGistTypeSamples>): void => {
185
+ expect(entity.version).toBe(1);
186
+ expect(fixtureInput.version).toBe(2);
187
+ expect(insertInput.ownerId).toBe('user-3');
188
+ };
189
+
190
+ const expectVersionedFixtureDefaults = (
191
+ db: ReturnType<typeof createVersionedStoreDefinition>
192
+ ): void => {
193
+ expect(db.tables.gists.fixtureSchema.parse({ ownerId: 'user-4' })).toEqual({
194
+ description: null,
195
+ isPublic: true,
196
+ ownerId: 'user-4',
197
+ tags: [],
198
+ });
199
+ };
200
+
121
201
  describe('@ontrails/store', () => {
122
202
  test('normalizes tables and derives insert/update schemas', () => {
123
203
  const db = createStoreDefinition();
124
204
 
125
- expect(db.kind).toBe('store');
205
+ expect(db.kind).toBe('tabular');
206
+ expect(db.signals.map((candidate) => candidate.id)).toEqual([
207
+ 'gists.created',
208
+ 'gists.updated',
209
+ 'gists.removed',
210
+ 'users.created',
211
+ 'users.updated',
212
+ 'users.removed',
213
+ ]);
126
214
  expect(db.tableNames).toEqual(['gists', 'users']);
215
+ expect(db.type).toBe('store');
127
216
 
128
217
  const table = db.tables.gists;
129
218
  expectNormalizedGistTable(table);
@@ -131,6 +220,21 @@ describe('@ontrails/store', () => {
131
220
  expectDerivedSchemas(table);
132
221
  });
133
222
 
223
+ test('accepts an explicit backend-agnostic kind', () => {
224
+ const db = store(
225
+ {
226
+ gists: {
227
+ identity: 'id',
228
+ schema: gistSchema,
229
+ },
230
+ },
231
+ { kind: 'document' }
232
+ );
233
+
234
+ expect(db.kind).toBe('document');
235
+ expect(db.tables.gists.identity).toBe('id');
236
+ });
237
+
134
238
  test('normalizes fixtures through the derived fixture schema', () => {
135
239
  const db = store({
136
240
  gists: {
@@ -141,7 +245,7 @@ describe('@ontrails/store', () => {
141
245
  },
142
246
  ],
143
247
  generated: ['id', 'createdAt', 'updatedAt'],
144
- primaryKey: 'id',
248
+ identity: 'id',
145
249
  schema: gistSchema,
146
250
  },
147
251
  });
@@ -174,6 +278,42 @@ describe('@ontrails/store', () => {
174
278
  });
175
279
  });
176
280
 
281
+ test('normalizes versioned tables with a framework-managed version field', () => {
282
+ const db = createVersionedStoreDefinition();
283
+ const samples = createVersionedGistTypeSamples();
284
+
285
+ expect(db.tables.gists.versioned).toBe(true);
286
+ expect(db.tables.gists.generated).toEqual([
287
+ 'id',
288
+ 'createdAt',
289
+ 'updatedAt',
290
+ 'version',
291
+ ]);
292
+ expectVersionedGistTypeSamples(samples);
293
+ expect(db.tables.gists.schema.parse(samples.entity)).toEqual(
294
+ samples.entity
295
+ );
296
+ expect(
297
+ db.tables.gists.signals.updated.payload.parse({
298
+ createdAt: '2026-04-03T12:00:00.000Z',
299
+ id: 'gist-1',
300
+ ownerId: 'user-1',
301
+ updatedAt: '2026-04-03T12:00:00.000Z',
302
+ version: 2,
303
+ })
304
+ ).toEqual({
305
+ createdAt: '2026-04-03T12:00:00.000Z',
306
+ description: null,
307
+ id: 'gist-1',
308
+ isPublic: true,
309
+ ownerId: 'user-1',
310
+ tags: [],
311
+ updatedAt: '2026-04-03T12:00:00.000Z',
312
+ version: 2,
313
+ });
314
+ expectVersionedFixtureDefaults(db);
315
+ });
316
+
177
317
  test('rejects duplicate fixture primary keys when they are explicitly provided', () => {
178
318
  expect(() =>
179
319
  store({
@@ -189,7 +329,7 @@ describe('@ontrails/store', () => {
189
329
  },
190
330
  ],
191
331
  generated: ['id', 'createdAt', 'updatedAt'],
192
- primaryKey: 'id',
332
+ identity: 'id',
193
333
  schema: gistSchema,
194
334
  },
195
335
  })
@@ -204,7 +344,7 @@ describe('@ontrails/store', () => {
204
344
  expect(() =>
205
345
  store({
206
346
  broken: {
207
- primaryKey: 'id' as never,
347
+ identity: 'id' as never,
208
348
  schema: z.string() as never,
209
349
  },
210
350
  })
@@ -215,13 +355,13 @@ describe('@ontrails/store', () => {
215
355
  expect(() =>
216
356
  store({
217
357
  gists: {
218
- primaryKey: 'slug' as never,
358
+ identity: 'slug' as never,
219
359
  schema: gistSchema,
220
360
  },
221
361
  })
222
362
  ).toThrow(
223
363
  new ValidationError(
224
- 'Store table "gists" declares primaryKey "slug" that is not present on the schema'
364
+ 'Store table "gists" declares identity "slug" that is not present on the schema'
225
365
  )
226
366
  );
227
367
 
@@ -229,7 +369,7 @@ describe('@ontrails/store', () => {
229
369
  store({
230
370
  gists: {
231
371
  generated: ['missing'] as const as never,
232
- primaryKey: 'id',
372
+ identity: 'id',
233
373
  schema: gistSchema,
234
374
  },
235
375
  })
@@ -242,14 +382,28 @@ describe('@ontrails/store', () => {
242
382
  expect(() =>
243
383
  store({
244
384
  gists: {
245
- indexes: ['missing'] as const as never,
246
- primaryKey: 'id',
385
+ identity: 'id',
386
+ schema: gistSchema.extend({ version: z.number().int() }),
387
+ versioned: true,
388
+ },
389
+ })
390
+ ).toThrow(
391
+ new ValidationError(
392
+ 'Store table "gists" cannot declare a "version" field when versioned storage is enabled because the framework manages that field.'
393
+ )
394
+ );
395
+
396
+ expect(() =>
397
+ store({
398
+ gists: {
399
+ identity: 'id',
400
+ indexed: ['missing'] as const as never,
247
401
  schema: gistSchema,
248
402
  },
249
403
  })
250
404
  ).toThrow(
251
405
  new ValidationError(
252
- 'Store table "gists" declares index field "missing" that is not present on the schema'
406
+ 'Store table "gists" declares indexed field "missing" that is not present on the schema'
253
407
  )
254
408
  );
255
409
 
@@ -258,7 +412,7 @@ describe('@ontrails/store', () => {
258
412
  gists: {
259
413
  fixtures: [{ id: 'gist-seed' } as never],
260
414
  generated: ['id', 'createdAt', 'updatedAt'],
261
- primaryKey: 'id',
415
+ identity: 'id',
262
416
  schema: gistSchema,
263
417
  },
264
418
  })
@@ -273,12 +427,12 @@ describe('@ontrails/store', () => {
273
427
  expect(() =>
274
428
  store({
275
429
  gists: {
276
- primaryKey: 'id',
430
+ identity: 'id',
277
431
  references: { missing: 'users' } as never,
278
432
  schema: gistSchema,
279
433
  },
280
434
  users: {
281
- primaryKey: 'id',
435
+ identity: 'id',
282
436
  schema: userSchema,
283
437
  },
284
438
  })
@@ -291,12 +445,12 @@ describe('@ontrails/store', () => {
291
445
  expect(() =>
292
446
  store({
293
447
  gists: {
294
- primaryKey: 'id',
448
+ identity: 'id',
295
449
  references: { ownerId: 'accounts' },
296
450
  schema: gistSchema,
297
451
  },
298
452
  users: {
299
- primaryKey: 'id',
453
+ identity: 'id',
300
454
  schema: userSchema,
301
455
  },
302
456
  })
@@ -307,6 +461,23 @@ describe('@ontrails/store', () => {
307
461
  );
308
462
  });
309
463
 
464
+ test('keeps tabular aliases working for existing stores', () => {
465
+ const db = store({
466
+ gists: {
467
+ generated: ['id'],
468
+ indexes: ['ownerId'],
469
+ primaryKey: 'id',
470
+ schema: gistSchema,
471
+ },
472
+ });
473
+
474
+ expect(db.kind).toBe('tabular');
475
+ expect(db.tables.gists.identity).toBe('id');
476
+ expect(db.tables.gists.primaryKey).toBe('id');
477
+ expect(db.tables.gists.indexed).toEqual(['ownerId']);
478
+ expect(db.tables.gists.indexes).toEqual(['ownerId']);
479
+ });
480
+
310
481
  test('type-level helpers expose connector-facing contracts', () => {
311
482
  const db = createTypeTestStore();
312
483
 
@@ -331,3 +502,38 @@ describe('@ontrails/store', () => {
331
502
  expect(db.tables.gists.name).toBe('gists');
332
503
  });
333
504
  });
505
+
506
+ describe('bindStoreDefinition', () => {
507
+ const definition = store({
508
+ gists: {
509
+ generated: ['id'],
510
+ identity: 'id',
511
+ schema: gistSchema,
512
+ },
513
+ });
514
+
515
+ test('rejects scopes containing ":"', () => {
516
+ expect(() => bindStoreDefinition(definition, 'bad:scope')).toThrow(
517
+ new ValidationError(
518
+ 'Store resource id "bad:scope" is invalid: must be a non-empty string with no ":" characters and no whitespace.'
519
+ )
520
+ );
521
+ });
522
+
523
+ test('rejects scopes containing whitespace', () => {
524
+ expect(() => bindStoreDefinition(definition, 'bad scope')).toThrow(
525
+ ValidationError
526
+ );
527
+ });
528
+
529
+ test('rejects empty scopes', () => {
530
+ expect(() => bindStoreDefinition(definition, '')).toThrow(ValidationError);
531
+ });
532
+
533
+ test('accepts valid scopes and scopes store signal ids', () => {
534
+ const bound = bindStoreDefinition(definition, 'primary');
535
+ expect(bound.tables.gists.signals.created.id).toBe('primary:gists.created');
536
+ expect(bound.tables.gists.signals.updated.id).toBe('primary:gists.updated');
537
+ expect(bound.tables.gists.signals.removed.id).toBe('primary:gists.removed');
538
+ });
539
+ });