@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
@@ -0,0 +1,50 @@
1
+ import type { Resource } from '@ontrails/core';
2
+ import type {
3
+ AnyStoreDefinition,
4
+ AnyStoreTable,
5
+ StoreAccessor,
6
+ StoreConnectorOptions,
7
+ } from '../types.js';
8
+
9
+ /** Connection shape: one StoreAccessor per table name. */
10
+ export type JsonFileConnection<TStore extends Record<string, AnyStoreTable>> =
11
+ Readonly<{ [K in keyof TStore]: StoreAccessor<TStore[K]> }>;
12
+
13
+ /** Options for creating a jsonfile store. */
14
+ export interface JsonFileStoreOptions<
15
+ TStore extends AnyStoreDefinition = AnyStoreDefinition,
16
+ > extends StoreConnectorOptions<TStore> {
17
+ /** Directory where JSON files are written. One `<tableName>.json` per table. */
18
+ readonly dir: string;
19
+ /**
20
+ * Optional identity generator. Defaults to `Bun.randomUUIDv7()`.
21
+ *
22
+ * @remarks
23
+ * When a jsonfile table is reused across multiple `connectJsonFile` call
24
+ * sites (same `dir` + table), the runtime enforces that every connection
25
+ * supplies a compatible `generateIdentity`. That compatibility check is
26
+ * intentional reference equality, not a structural or behavioral comparison:
27
+ * two distinct arrow functions that happen to be "logically identical"
28
+ * (e.g. `() => uuid()` written inline in two files) will still be treated
29
+ * as a conflict and surface as a `ConflictError`. Callers that want to
30
+ * share a table across call sites should hoist `generateIdentity` to a
31
+ * stable reference — a module-level `const` or shared utility — and pass
32
+ * that same reference in from every connection, rather than recreating
33
+ * the function inline at each site.
34
+ *
35
+ * The conflict check only fires for custom generators. When no custom
36
+ * generator is provided for an auto-generated-identity store,
37
+ * `deriveTableReuseConfig` normalizes `generateIdentity` to `undefined`
38
+ * regardless of what (if anything) was passed, so two connections that
39
+ * both rely on defaults never conflict on this field. The same
40
+ * normalization also applies when the identity field is not in
41
+ * `generatedFields` — the generator is irrelevant in that case, so the
42
+ * check is skipped.
43
+ */
44
+ readonly generateIdentity?: () => string;
45
+ }
46
+
47
+ /** Resource type for a jsonfile store. */
48
+ export type JsonFileStoreResource<
49
+ TStore extends Record<string, AnyStoreTable>,
50
+ > = Resource<JsonFileConnection<TStore>>;
package/src/store.ts CHANGED
@@ -1,8 +1,13 @@
1
1
  import { ValidationError } from '@ontrails/core';
2
- import type { z } from 'zod';
2
+ import type { AnySignal } from '@ontrails/core';
3
+ import { stripDefaultsFromShape } from '@ontrails/core/internal/zod-wrappers';
4
+ import { z } from 'zod';
3
5
 
6
+ import { createStoreTableSignals } from './internal/signal-identity.js';
4
7
  import type {
5
8
  StoreDefinition,
9
+ StoreKind,
10
+ StoreOptions,
6
11
  StoreObjectSchema,
7
12
  StoreTable,
8
13
  StoreTableInput,
@@ -12,14 +17,31 @@ import type {
12
17
  const isStoreObjectSchema = (schema: z.ZodType): schema is StoreObjectSchema =>
13
18
  schema.def.type === 'object' && 'shape' in schema.def;
14
19
 
20
+ /**
21
+ * Name of the framework-managed version column used by versioned tables for
22
+ * optimistic concurrency control. Exported so connectors can gate their own
23
+ * version-handling logic without duplicating the literal.
24
+ */
25
+ export const versionFieldName = 'version';
26
+ const versionFieldSchema = z.number().int().positive();
27
+
15
28
  const uniqueStrings = <T extends string>(
16
29
  values: readonly T[] | undefined
17
30
  ): readonly T[] =>
18
31
  Object.freeze([...(values === undefined ? [] : new Set(values))]);
19
32
 
33
+ const uniqueMergedStrings = <T extends string>(
34
+ ...groups: readonly (readonly T[] | undefined)[]
35
+ ): readonly T[] => uniqueStrings(groups.flatMap((group) => [...(group ?? [])]));
36
+
20
37
  const hasField = (schema: StoreObjectSchema, field: string): boolean =>
21
38
  Object.hasOwn(schema.shape, field);
22
39
 
40
+ const versionedSchema = (schema: StoreObjectSchema): StoreObjectSchema =>
41
+ schema.extend({
42
+ [versionFieldName]: versionFieldSchema,
43
+ }) as StoreObjectSchema;
44
+
23
45
  const validateFieldList = (
24
46
  tableName: string,
25
47
  schema: StoreObjectSchema,
@@ -98,56 +120,16 @@ const deriveFixtureSchema = <TSchema extends StoreObjectSchema>(
98
120
  generated: readonly string[]
99
121
  ): StoreObjectSchema => partialFields(schema, generated);
100
122
 
101
- /**
102
- * Strip `default` wrappers from a Zod type so that partial update schemas
103
- * do not silently re-materialize defaults. Walks through all wrapper layers
104
- * (default, optional, nullable), strips defaults, and re-applies non-default
105
- * wrappers to preserve the nullable constraint.
106
- */
107
- const stripDefaultWrappers = (schema: z.ZodType): z.ZodType => {
108
- const wrappers: ('optional' | 'nullable')[] = [];
109
- let current = schema;
110
-
111
- while (
112
- current.def.type === 'default' ||
113
- current.def.type === 'optional' ||
114
- current.def.type === 'nullable'
115
- ) {
116
- const type = current.def.type as 'default' | 'optional' | 'nullable';
117
- if (type !== 'default') {
118
- wrappers.push(type);
119
- }
120
- current = (current.def as unknown as Record<string, unknown>)[
121
- 'innerType'
122
- ] as z.ZodType;
123
- }
124
-
125
- // Re-apply nullable wrappers (optional is dropped — .partial() re-adds it)
126
- return wrappers.includes('nullable') ? current.nullable() : current;
127
- };
128
-
129
- const stripDefaultsFromShape = (
130
- schema: StoreObjectSchema
131
- ): Record<string, z.ZodType> => {
132
- const stripped: Record<string, z.ZodType> = {};
133
-
134
- for (const [field, value] of Object.entries(schema.shape)) {
135
- stripped[field] = stripDefaultWrappers(value);
136
- }
137
-
138
- return stripped;
139
- };
140
-
141
123
  const deriveUpdateSchema = (
142
124
  schema: StoreObjectSchema,
143
- primaryKey: string
125
+ identity: string
144
126
  ): StoreObjectSchema => {
145
127
  const partial = schema
146
128
  .extend(stripDefaultsFromShape(schema))
147
129
  .partial() as StoreObjectSchema;
148
- // Only omit the primaryKey if it's still present (it may already be in `generated`)
149
- return hasField(partial, primaryKey)
150
- ? omitFields(partial, [primaryKey])
130
+ // Only omit the identity field if it's still present (it may already be in `generated`)
131
+ return hasField(partial, identity)
132
+ ? omitFields(partial, [identity])
151
133
  : partial;
152
134
  };
153
135
 
@@ -156,13 +138,13 @@ const formatFixtureIssues = (issues: readonly { readonly message: string }[]) =>
156
138
 
157
139
  const validateFixturePrimaryKeys = (
158
140
  tableName: string,
159
- primaryKey: string,
141
+ identity: string,
160
142
  fixtures: readonly Record<string, unknown>[]
161
143
  ): void => {
162
144
  const seen = new Set<unknown>();
163
145
 
164
146
  for (const [index, fixture] of fixtures.entries()) {
165
- const identifier = fixture[primaryKey];
147
+ const identifier = fixture[identity];
166
148
  if (identifier === undefined) {
167
149
  continue;
168
150
  }
@@ -194,29 +176,81 @@ const normalizeReferences = (
194
176
  const validatePrimaryKey = (
195
177
  tableName: string,
196
178
  schema: StoreObjectSchema,
197
- primaryKey: string
179
+ identity: string
198
180
  ): void => {
199
- if (hasField(schema, primaryKey)) {
181
+ if (hasField(schema, identity)) {
200
182
  return;
201
183
  }
202
184
 
203
185
  throw new ValidationError(
204
- `Store table "${tableName}" declares primaryKey "${primaryKey}" that is not present on the schema`
186
+ `Store table "${tableName}" declares identity "${identity}" that is not present on the schema`
187
+ );
188
+ };
189
+
190
+ const validateVersioning = (
191
+ tableName: string,
192
+ schema: StoreObjectSchema,
193
+ versioned: boolean
194
+ ): void => {
195
+ if (!versioned || !hasField(schema, versionFieldName)) {
196
+ return;
197
+ }
198
+
199
+ throw new ValidationError(
200
+ `Store table "${tableName}" cannot declare a "${versionFieldName}" field when versioned storage is enabled because the framework manages that field.`
201
+ );
202
+ };
203
+
204
+ const resolveStoreObjectSchema = (
205
+ tableName: string,
206
+ schema: StoreTableInput['schema']
207
+ ): StoreObjectSchema => {
208
+ if (isStoreObjectSchema(schema)) {
209
+ return schema;
210
+ }
211
+
212
+ throw new ValidationError(
213
+ `Store table "${tableName}" must use a Zod object schema`
205
214
  );
206
215
  };
207
216
 
217
+ const resolveIdentity = (tableName: string, input: StoreTableInput): string => {
218
+ if (
219
+ input.identity !== undefined &&
220
+ input.primaryKey !== undefined &&
221
+ input.identity !== input.primaryKey
222
+ ) {
223
+ throw new ValidationError(
224
+ `Store table "${tableName}" declares conflicting identity "${input.identity}" and primaryKey "${input.primaryKey}"`
225
+ );
226
+ }
227
+
228
+ const identity = input.identity ?? input.primaryKey;
229
+ if (identity !== undefined) {
230
+ return identity;
231
+ }
232
+
233
+ throw new ValidationError(`Store table "${tableName}" must declare identity`);
234
+ };
235
+
236
+ const resolveIndexed = (input: StoreTableInput): readonly string[] =>
237
+ uniqueMergedStrings(input.indexed, input.indexes);
238
+
239
+ const resolveVersioned = (input: StoreTableInput): boolean =>
240
+ input.versioned === true;
241
+
208
242
  const validateTableInput = (
209
243
  tableName: string,
210
244
  schema: StoreObjectSchema,
211
- primaryKey: string,
245
+ identity: string,
212
246
  generated: readonly string[],
213
- indexes: readonly string[],
247
+ indexed: readonly string[],
214
248
  references: Readonly<Partial<Record<string, string>>>,
215
249
  tableNames: readonly string[]
216
250
  ): void => {
217
- validatePrimaryKey(tableName, schema, primaryKey);
251
+ validatePrimaryKey(tableName, schema, identity);
218
252
  validateFieldList(tableName, schema, generated, 'generated');
219
- validateFieldList(tableName, schema, indexes, 'index');
253
+ validateFieldList(tableName, schema, indexed, 'indexed');
220
254
  validateReferences(tableName, schema, references, tableNames);
221
255
  };
222
256
 
@@ -224,84 +258,246 @@ type MutableTables<TTables extends StoreTablesInput> = {
224
258
  -readonly [TName in keyof TTables]: StoreDefinition<TTables>['tables'][TName];
225
259
  };
226
260
 
261
+ const fixtureListFrom = (
262
+ fixtures: StoreTableInput['fixtures']
263
+ ): readonly unknown[] => (Array.isArray(fixtures) ? fixtures : []);
264
+
265
+ const parseFixture = (
266
+ tableName: string,
267
+ fixtureSchema: StoreObjectSchema,
268
+ fixture: unknown,
269
+ index: number
270
+ ): Readonly<Record<string, unknown>> => {
271
+ const parsed = fixtureSchema.safeParse(fixture);
272
+ if (!parsed.success) {
273
+ throw new ValidationError(
274
+ `Store table "${tableName}" fixture ${index + 1} is invalid: ${formatFixtureIssues(parsed.error.issues)}`
275
+ );
276
+ }
277
+
278
+ return Object.freeze(parsed.data);
279
+ };
280
+
227
281
  const normalizeFixtures = <TInput extends StoreTableInput>(
228
282
  tableName: string,
229
- primaryKey: string,
283
+ identity: string,
230
284
  fixtureSchema: StoreObjectSchema,
231
285
  fixtures: TInput['fixtures']
232
286
  ): StoreTable<TInput>['fixtures'] => {
233
- if (fixtures === undefined || fixtures.length === 0) {
287
+ const fixtureList = fixtureListFrom(fixtures);
288
+
289
+ if (fixtureList.length === 0) {
234
290
  return Object.freeze([]) as StoreTable<TInput>['fixtures'];
235
291
  }
236
292
 
237
- const normalized = [];
293
+ const normalized: Record<string, unknown>[] = [];
238
294
 
239
- for (const [index, fixture] of fixtures.entries()) {
240
- const parsed = fixtureSchema.safeParse(fixture);
241
- if (!parsed.success) {
242
- throw new ValidationError(
243
- `Store table "${tableName}" fixture ${index + 1} is invalid: ${formatFixtureIssues(parsed.error.issues)}`
244
- );
245
- }
246
-
247
- normalized.push(Object.freeze(parsed.data));
295
+ for (const [index, fixture] of fixtureList.entries()) {
296
+ normalized.push(parseFixture(tableName, fixtureSchema, fixture, index));
248
297
  }
249
298
 
250
- validateFixturePrimaryKeys(tableName, primaryKey, normalized);
299
+ validateFixturePrimaryKeys(tableName, identity, normalized);
251
300
  return Object.freeze(normalized) as StoreTable<TInput>['fixtures'];
252
301
  };
253
302
 
254
- const normalizeTable = <
303
+ const resolveTableSchema = <TInput extends StoreTableInput<StoreObjectSchema>>(
304
+ tableName: string,
305
+ input: TInput
306
+ ): {
307
+ readonly schema: StoreObjectSchema;
308
+ readonly versioned: boolean;
309
+ } => {
310
+ const authoredSchema = resolveStoreObjectSchema(tableName, input.schema);
311
+ const versioned = resolveVersioned(input);
312
+ validateVersioning(tableName, authoredSchema, versioned);
313
+
314
+ return {
315
+ schema: versioned ? versionedSchema(authoredSchema) : authoredSchema,
316
+ versioned,
317
+ };
318
+ };
319
+
320
+ const resolveGeneratedFields = <
321
+ TInput extends StoreTableInput<StoreObjectSchema>,
322
+ >(
323
+ input: TInput,
324
+ versioned: boolean
325
+ ): readonly string[] =>
326
+ versioned
327
+ ? uniqueMergedStrings(input.generated, [versionFieldName])
328
+ : uniqueStrings(input.generated);
329
+
330
+ const freezeNormalizedTable = <
255
331
  TName extends string,
256
332
  TInput extends StoreTableInput<StoreObjectSchema>,
257
333
  >(
258
334
  name: TName,
259
335
  input: TInput,
260
- tableNames: readonly string[]
261
- ): StoreTable<TInput, TName> => {
262
- if (!isStoreObjectSchema(input.schema)) {
263
- throw new ValidationError(
264
- `Store table "${name}" must use a Zod object schema`
265
- );
336
+ resolved: {
337
+ readonly fixtureSchema: StoreObjectSchema;
338
+ readonly fixtures: StoreTable<TInput>['fixtures'];
339
+ readonly generated: readonly string[];
340
+ readonly identity: string;
341
+ readonly indexed: readonly string[];
342
+ readonly insertSchema: StoreObjectSchema;
343
+ readonly references: Readonly<Partial<Record<string, string>>>;
344
+ readonly schema: StoreObjectSchema;
345
+ readonly signals: StoreTable<TInput, TName>['signals'];
346
+ readonly versioned: boolean;
266
347
  }
348
+ ): StoreTable<TInput, TName> =>
349
+ Object.freeze({
350
+ fixtureSchema: resolved.fixtureSchema,
351
+ fixtures: resolved.fixtures,
352
+ generated: resolved.generated,
353
+ identity: resolved.identity,
354
+ indexed: resolved.indexed,
355
+ indexes: resolved.indexed,
356
+ insertSchema: resolved.insertSchema,
357
+ name,
358
+ primaryKey: resolved.identity,
359
+ references: resolved.references,
360
+ schema: resolved.schema,
361
+ ...(input.search === undefined ? {} : { search: input.search }),
362
+ signals: resolved.signals,
363
+ updateSchema: deriveUpdateSchema(resolved.insertSchema, resolved.identity),
364
+ versioned: resolved.versioned,
365
+ }) as StoreTable<TInput, TName>;
267
366
 
268
- const generated = uniqueStrings(input.generated);
269
- const indexes = uniqueStrings(input.indexes);
367
+ interface NormalizedTableState {
368
+ readonly generated: readonly string[];
369
+ readonly identity: string;
370
+ readonly indexed: readonly string[];
371
+ readonly references: Readonly<Partial<Record<string, string>>>;
372
+ readonly schema: StoreObjectSchema;
373
+ readonly versioned: boolean;
374
+ }
375
+
376
+ const resolveNormalizedTableState = <
377
+ TInput extends StoreTableInput<StoreObjectSchema>,
378
+ >(
379
+ name: string,
380
+ input: TInput,
381
+ tableNames: readonly string[]
382
+ ): NormalizedTableState => {
383
+ const { schema, versioned } = resolveTableSchema(name, input);
384
+ const identity = resolveIdentity(name, input);
385
+ const generated = resolveGeneratedFields(input, versioned);
386
+ const indexed = resolveIndexed(input);
270
387
  const references = normalizeReferences(input.references);
388
+
271
389
  validateTableInput(
272
390
  name,
273
- input.schema,
274
- input.primaryKey,
391
+ schema,
392
+ identity,
275
393
  generated,
276
- indexes,
394
+ indexed,
277
395
  references,
278
396
  tableNames
279
397
  );
280
398
 
281
- const insertSchema = deriveInsertSchema(input.schema, generated);
282
- const fixtureSchema = deriveFixtureSchema(input.schema, generated);
399
+ return {
400
+ generated,
401
+ identity,
402
+ indexed,
403
+ references,
404
+ schema,
405
+ versioned,
406
+ };
407
+ };
408
+
409
+ const resolveNormalizedTableArtifacts = <
410
+ TName extends string,
411
+ TInput extends StoreTableInput<StoreObjectSchema>,
412
+ >(
413
+ name: TName,
414
+ input: TInput,
415
+ resolved: NormalizedTableState
416
+ ): {
417
+ readonly fixtureSchema: StoreObjectSchema;
418
+ readonly fixtures: StoreTable<TInput>['fixtures'];
419
+ readonly generated: readonly string[];
420
+ readonly identity: string;
421
+ readonly indexed: readonly string[];
422
+ readonly insertSchema: StoreObjectSchema;
423
+ readonly references: Readonly<Partial<Record<string, string>>>;
424
+ readonly schema: StoreObjectSchema;
425
+ readonly signals: StoreTable<TInput, TName>['signals'];
426
+ } => {
427
+ const insertSchema = deriveInsertSchema(resolved.schema, resolved.generated);
428
+ const fixtureSchema = deriveFixtureSchema(
429
+ resolved.schema,
430
+ resolved.generated
431
+ );
283
432
  const fixtures = normalizeFixtures(
284
433
  name,
285
- input.primaryKey,
434
+ resolved.identity,
286
435
  fixtureSchema,
287
436
  input.fixtures
288
437
  );
438
+ const signals = createStoreTableSignals(name, resolved.schema) as StoreTable<
439
+ TInput,
440
+ TName
441
+ >['signals'];
289
442
 
290
- return Object.freeze({
443
+ return {
291
444
  fixtureSchema,
292
445
  fixtures,
293
- generated,
294
- indexes,
446
+ generated: resolved.generated,
447
+ identity: resolved.identity,
448
+ indexed: resolved.indexed,
295
449
  insertSchema,
296
- name,
297
- primaryKey: input.primaryKey,
298
- references,
299
- schema: input.schema,
300
- ...(input.search === undefined ? {} : { search: input.search }),
301
- updateSchema: deriveUpdateSchema(insertSchema, input.primaryKey),
302
- }) as StoreTable<TInput, TName>;
450
+ references: resolved.references,
451
+ schema: resolved.schema,
452
+ signals,
453
+ };
454
+ };
455
+
456
+ const normalizeTable = <
457
+ TName extends string,
458
+ TInput extends StoreTableInput<StoreObjectSchema>,
459
+ >(
460
+ name: TName,
461
+ input: TInput,
462
+ tableNames: readonly string[]
463
+ ): StoreTable<TInput, TName> => {
464
+ const resolved = resolveNormalizedTableState(name, input, tableNames);
465
+
466
+ return freezeNormalizedTable(name, input, {
467
+ ...resolveNormalizedTableArtifacts(name, input, resolved),
468
+ versioned: resolved.versioned,
469
+ });
470
+ };
471
+
472
+ const normalizeTables = <const TTables extends StoreTablesInput>(
473
+ tables: TTables,
474
+ tableNames: readonly Extract<keyof TTables, string>[]
475
+ ): MutableTables<TTables> => {
476
+ const normalized = {} as MutableTables<TTables>;
477
+
478
+ for (const name of tableNames) {
479
+ const input = tables[name];
480
+ if (input !== undefined) {
481
+ normalized[name] = normalizeTable(name, input, tableNames);
482
+ }
483
+ }
484
+
485
+ return normalized;
303
486
  };
304
487
 
488
+ const collectStoreSignals = <const TTables extends StoreTablesInput>(
489
+ normalized: MutableTables<TTables>,
490
+ tableNames: readonly Extract<keyof TTables, string>[]
491
+ ): readonly AnySignal[] =>
492
+ Object.freeze(
493
+ tableNames.flatMap((name) => {
494
+ const table = normalized[name];
495
+ return table === undefined
496
+ ? []
497
+ : [table.signals.created, table.signals.updated, table.signals.removed];
498
+ })
499
+ );
500
+
305
501
  /**
306
502
  * Declare a connector-agnostic store definition from entity schemas and
307
503
  * persistence metadata.
@@ -310,58 +506,24 @@ const normalizeTable = <
310
506
  * bind to a concrete runtime later.
311
507
  */
312
508
  export const store = <const TTables extends StoreTablesInput>(
313
- tables: TTables
509
+ tables: TTables,
510
+ options: StoreOptions = {}
314
511
  ): StoreDefinition<TTables> => {
512
+ const kind: StoreKind = options.kind ?? 'tabular';
315
513
  const tableNames = Object.freeze(
316
514
  Object.keys(tables).toSorted()
317
515
  ) as readonly Extract<keyof TTables, string>[];
318
-
319
- const normalized = {} as MutableTables<TTables>;
320
- for (const name of tableNames) {
321
- const input = tables[name];
322
-
323
- if (input === undefined) {
324
- continue;
325
- }
326
-
327
- normalized[name] = normalizeTable(name, input, tableNames);
328
- }
329
-
516
+ const normalized = normalizeTables(tables, tableNames);
330
517
  const get = <TName extends Extract<keyof TTables, string>>(name: TName) =>
331
518
  normalized[name];
519
+ const signals = collectStoreSignals(normalized, tableNames);
332
520
 
333
521
  return Object.freeze({
334
522
  get,
335
- kind: 'store' as const,
523
+ kind,
524
+ signals,
336
525
  tableNames,
337
526
  tables: Object.freeze(normalized),
527
+ type: 'store' as const,
338
528
  });
339
529
  };
340
-
341
- /**
342
- * Read the full entity schema from a normalized store table.
343
- */
344
- export const entitySchemaOf = <TTable extends StoreTable>(
345
- table: TTable
346
- ): TTable['schema'] => table.schema;
347
-
348
- /**
349
- * Read the fixture schema from a normalized store table.
350
- */
351
- export const fixtureSchemaOf = <TTable extends StoreTable>(
352
- table: TTable
353
- ): TTable['fixtureSchema'] => table.fixtureSchema;
354
-
355
- /**
356
- * Read the insert schema from a normalized store table.
357
- */
358
- export const insertSchemaOf = <TTable extends StoreTable>(
359
- table: TTable
360
- ): TTable['insertSchema'] => table.insertSchema;
361
-
362
- /**
363
- * Read the update schema from a normalized store table.
364
- */
365
- export const updateSchemaOf = <TTable extends StoreTable>(
366
- table: TTable
367
- ): TTable['updateSchema'] => table.updateSchema;