@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
|
@@ -0,0 +1,50 @@
|
|
|
1
|
+
import type { Resource } from '@ontrails/core';
|
|
2
|
+
import type {
|
|
3
|
+
AnyStoreDefinition,
|
|
4
|
+
AnyStoreTable,
|
|
5
|
+
StoreAccessor,
|
|
6
|
+
StoreAdapterOptions,
|
|
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 StoreAdapterOptions<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,12 @@
|
|
|
1
|
-
import { ValidationError } from '@ontrails/core';
|
|
2
|
-
import type {
|
|
1
|
+
import { stripDefaultsFromShape, ValidationError } from '@ontrails/core';
|
|
2
|
+
import type { AnySignal } from '@ontrails/core';
|
|
3
|
+
import { z } from 'zod';
|
|
3
4
|
|
|
5
|
+
import { createStoreTableSignals } from './adapter-support.js';
|
|
4
6
|
import type {
|
|
5
7
|
StoreDefinition,
|
|
8
|
+
StoreKind,
|
|
9
|
+
StoreOptions,
|
|
6
10
|
StoreObjectSchema,
|
|
7
11
|
StoreTable,
|
|
8
12
|
StoreTableInput,
|
|
@@ -12,14 +16,31 @@ import type {
|
|
|
12
16
|
const isStoreObjectSchema = (schema: z.ZodType): schema is StoreObjectSchema =>
|
|
13
17
|
schema.def.type === 'object' && 'shape' in schema.def;
|
|
14
18
|
|
|
19
|
+
/**
|
|
20
|
+
* Name of the framework-managed version column used by versioned tables for
|
|
21
|
+
* optimistic concurrency control. Exported so adapters can gate their own
|
|
22
|
+
* version-handling logic without duplicating the literal.
|
|
23
|
+
*/
|
|
24
|
+
export const versionFieldName = 'version';
|
|
25
|
+
const versionFieldSchema = z.number().int().positive();
|
|
26
|
+
|
|
15
27
|
const uniqueStrings = <T extends string>(
|
|
16
28
|
values: readonly T[] | undefined
|
|
17
29
|
): readonly T[] =>
|
|
18
30
|
Object.freeze([...(values === undefined ? [] : new Set(values))]);
|
|
19
31
|
|
|
32
|
+
const uniqueMergedStrings = <T extends string>(
|
|
33
|
+
...groups: readonly (readonly T[] | undefined)[]
|
|
34
|
+
): readonly T[] => uniqueStrings(groups.flatMap((group) => [...(group ?? [])]));
|
|
35
|
+
|
|
20
36
|
const hasField = (schema: StoreObjectSchema, field: string): boolean =>
|
|
21
37
|
Object.hasOwn(schema.shape, field);
|
|
22
38
|
|
|
39
|
+
const versionedSchema = (schema: StoreObjectSchema): StoreObjectSchema =>
|
|
40
|
+
schema.extend({
|
|
41
|
+
[versionFieldName]: versionFieldSchema,
|
|
42
|
+
}) as StoreObjectSchema;
|
|
43
|
+
|
|
23
44
|
const validateFieldList = (
|
|
24
45
|
tableName: string,
|
|
25
46
|
schema: StoreObjectSchema,
|
|
@@ -98,56 +119,16 @@ const deriveFixtureSchema = <TSchema extends StoreObjectSchema>(
|
|
|
98
119
|
generated: readonly string[]
|
|
99
120
|
): StoreObjectSchema => partialFields(schema, generated);
|
|
100
121
|
|
|
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
122
|
const deriveUpdateSchema = (
|
|
142
123
|
schema: StoreObjectSchema,
|
|
143
|
-
|
|
124
|
+
identity: string
|
|
144
125
|
): StoreObjectSchema => {
|
|
145
126
|
const partial = schema
|
|
146
127
|
.extend(stripDefaultsFromShape(schema))
|
|
147
128
|
.partial() as StoreObjectSchema;
|
|
148
|
-
// Only omit the
|
|
149
|
-
return hasField(partial,
|
|
150
|
-
? omitFields(partial, [
|
|
129
|
+
// Only omit the identity field if it's still present (it may already be in `generated`)
|
|
130
|
+
return hasField(partial, identity)
|
|
131
|
+
? omitFields(partial, [identity])
|
|
151
132
|
: partial;
|
|
152
133
|
};
|
|
153
134
|
|
|
@@ -156,13 +137,13 @@ const formatFixtureIssues = (issues: readonly { readonly message: string }[]) =>
|
|
|
156
137
|
|
|
157
138
|
const validateFixturePrimaryKeys = (
|
|
158
139
|
tableName: string,
|
|
159
|
-
|
|
140
|
+
identity: string,
|
|
160
141
|
fixtures: readonly Record<string, unknown>[]
|
|
161
142
|
): void => {
|
|
162
143
|
const seen = new Set<unknown>();
|
|
163
144
|
|
|
164
145
|
for (const [index, fixture] of fixtures.entries()) {
|
|
165
|
-
const identifier = fixture[
|
|
146
|
+
const identifier = fixture[identity];
|
|
166
147
|
if (identifier === undefined) {
|
|
167
148
|
continue;
|
|
168
149
|
}
|
|
@@ -194,29 +175,81 @@ const normalizeReferences = (
|
|
|
194
175
|
const validatePrimaryKey = (
|
|
195
176
|
tableName: string,
|
|
196
177
|
schema: StoreObjectSchema,
|
|
197
|
-
|
|
178
|
+
identity: string
|
|
198
179
|
): void => {
|
|
199
|
-
if (hasField(schema,
|
|
180
|
+
if (hasField(schema, identity)) {
|
|
200
181
|
return;
|
|
201
182
|
}
|
|
202
183
|
|
|
203
184
|
throw new ValidationError(
|
|
204
|
-
`Store table "${tableName}" declares
|
|
185
|
+
`Store table "${tableName}" declares identity "${identity}" that is not present on the schema`
|
|
186
|
+
);
|
|
187
|
+
};
|
|
188
|
+
|
|
189
|
+
const validateVersioning = (
|
|
190
|
+
tableName: string,
|
|
191
|
+
schema: StoreObjectSchema,
|
|
192
|
+
versioned: boolean
|
|
193
|
+
): void => {
|
|
194
|
+
if (!versioned || !hasField(schema, versionFieldName)) {
|
|
195
|
+
return;
|
|
196
|
+
}
|
|
197
|
+
|
|
198
|
+
throw new ValidationError(
|
|
199
|
+
`Store table "${tableName}" cannot declare a "${versionFieldName}" field when versioned storage is enabled because the framework manages that field.`
|
|
200
|
+
);
|
|
201
|
+
};
|
|
202
|
+
|
|
203
|
+
const resolveStoreObjectSchema = (
|
|
204
|
+
tableName: string,
|
|
205
|
+
schema: StoreTableInput['schema']
|
|
206
|
+
): StoreObjectSchema => {
|
|
207
|
+
if (isStoreObjectSchema(schema)) {
|
|
208
|
+
return schema;
|
|
209
|
+
}
|
|
210
|
+
|
|
211
|
+
throw new ValidationError(
|
|
212
|
+
`Store table "${tableName}" must use a Zod object schema`
|
|
205
213
|
);
|
|
206
214
|
};
|
|
207
215
|
|
|
216
|
+
const resolveIdentity = (tableName: string, input: StoreTableInput): string => {
|
|
217
|
+
if (
|
|
218
|
+
input.identity !== undefined &&
|
|
219
|
+
input.primaryKey !== undefined &&
|
|
220
|
+
input.identity !== input.primaryKey
|
|
221
|
+
) {
|
|
222
|
+
throw new ValidationError(
|
|
223
|
+
`Store table "${tableName}" declares conflicting identity "${input.identity}" and primaryKey "${input.primaryKey}"`
|
|
224
|
+
);
|
|
225
|
+
}
|
|
226
|
+
|
|
227
|
+
const identity = input.identity ?? input.primaryKey;
|
|
228
|
+
if (identity !== undefined) {
|
|
229
|
+
return identity;
|
|
230
|
+
}
|
|
231
|
+
|
|
232
|
+
throw new ValidationError(`Store table "${tableName}" must declare identity`);
|
|
233
|
+
};
|
|
234
|
+
|
|
235
|
+
const resolveIndexed = (input: StoreTableInput): readonly string[] =>
|
|
236
|
+
uniqueMergedStrings(input.indexed, input.indexes);
|
|
237
|
+
|
|
238
|
+
const resolveVersioned = (input: StoreTableInput): boolean =>
|
|
239
|
+
input.versioned === true;
|
|
240
|
+
|
|
208
241
|
const validateTableInput = (
|
|
209
242
|
tableName: string,
|
|
210
243
|
schema: StoreObjectSchema,
|
|
211
|
-
|
|
244
|
+
identity: string,
|
|
212
245
|
generated: readonly string[],
|
|
213
|
-
|
|
246
|
+
indexed: readonly string[],
|
|
214
247
|
references: Readonly<Partial<Record<string, string>>>,
|
|
215
248
|
tableNames: readonly string[]
|
|
216
249
|
): void => {
|
|
217
|
-
validatePrimaryKey(tableName, schema,
|
|
250
|
+
validatePrimaryKey(tableName, schema, identity);
|
|
218
251
|
validateFieldList(tableName, schema, generated, 'generated');
|
|
219
|
-
validateFieldList(tableName, schema,
|
|
252
|
+
validateFieldList(tableName, schema, indexed, 'indexed');
|
|
220
253
|
validateReferences(tableName, schema, references, tableNames);
|
|
221
254
|
};
|
|
222
255
|
|
|
@@ -224,144 +257,272 @@ type MutableTables<TTables extends StoreTablesInput> = {
|
|
|
224
257
|
-readonly [TName in keyof TTables]: StoreDefinition<TTables>['tables'][TName];
|
|
225
258
|
};
|
|
226
259
|
|
|
260
|
+
const fixtureListFrom = (
|
|
261
|
+
fixtures: StoreTableInput['fixtures']
|
|
262
|
+
): readonly unknown[] => (Array.isArray(fixtures) ? fixtures : []);
|
|
263
|
+
|
|
264
|
+
const parseFixture = (
|
|
265
|
+
tableName: string,
|
|
266
|
+
fixtureSchema: StoreObjectSchema,
|
|
267
|
+
fixture: unknown,
|
|
268
|
+
index: number
|
|
269
|
+
): Readonly<Record<string, unknown>> => {
|
|
270
|
+
const parsed = fixtureSchema.safeParse(fixture);
|
|
271
|
+
if (!parsed.success) {
|
|
272
|
+
throw new ValidationError(
|
|
273
|
+
`Store table "${tableName}" fixture ${index + 1} is invalid: ${formatFixtureIssues(parsed.error.issues)}`
|
|
274
|
+
);
|
|
275
|
+
}
|
|
276
|
+
|
|
277
|
+
return Object.freeze(parsed.data);
|
|
278
|
+
};
|
|
279
|
+
|
|
227
280
|
const normalizeFixtures = <TInput extends StoreTableInput>(
|
|
228
281
|
tableName: string,
|
|
229
|
-
|
|
282
|
+
identity: string,
|
|
230
283
|
fixtureSchema: StoreObjectSchema,
|
|
231
284
|
fixtures: TInput['fixtures']
|
|
232
285
|
): StoreTable<TInput>['fixtures'] => {
|
|
233
|
-
|
|
286
|
+
const fixtureList = fixtureListFrom(fixtures);
|
|
287
|
+
|
|
288
|
+
if (fixtureList.length === 0) {
|
|
234
289
|
return Object.freeze([]) as StoreTable<TInput>['fixtures'];
|
|
235
290
|
}
|
|
236
291
|
|
|
237
|
-
const normalized = [];
|
|
292
|
+
const normalized: Record<string, unknown>[] = [];
|
|
238
293
|
|
|
239
|
-
for (const [index, fixture] of
|
|
240
|
-
|
|
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));
|
|
294
|
+
for (const [index, fixture] of fixtureList.entries()) {
|
|
295
|
+
normalized.push(parseFixture(tableName, fixtureSchema, fixture, index));
|
|
248
296
|
}
|
|
249
297
|
|
|
250
|
-
validateFixturePrimaryKeys(tableName,
|
|
298
|
+
validateFixturePrimaryKeys(tableName, identity, normalized);
|
|
251
299
|
return Object.freeze(normalized) as StoreTable<TInput>['fixtures'];
|
|
252
300
|
};
|
|
253
301
|
|
|
254
|
-
const
|
|
302
|
+
const resolveTableSchema = <TInput extends StoreTableInput<StoreObjectSchema>>(
|
|
303
|
+
tableName: string,
|
|
304
|
+
input: TInput
|
|
305
|
+
): {
|
|
306
|
+
readonly schema: StoreObjectSchema;
|
|
307
|
+
readonly versioned: boolean;
|
|
308
|
+
} => {
|
|
309
|
+
const authoredSchema = resolveStoreObjectSchema(tableName, input.schema);
|
|
310
|
+
const versioned = resolveVersioned(input);
|
|
311
|
+
validateVersioning(tableName, authoredSchema, versioned);
|
|
312
|
+
|
|
313
|
+
return {
|
|
314
|
+
schema: versioned ? versionedSchema(authoredSchema) : authoredSchema,
|
|
315
|
+
versioned,
|
|
316
|
+
};
|
|
317
|
+
};
|
|
318
|
+
|
|
319
|
+
const resolveGeneratedFields = <
|
|
320
|
+
TInput extends StoreTableInput<StoreObjectSchema>,
|
|
321
|
+
>(
|
|
322
|
+
input: TInput,
|
|
323
|
+
versioned: boolean
|
|
324
|
+
): readonly string[] =>
|
|
325
|
+
versioned
|
|
326
|
+
? uniqueMergedStrings(input.generated, [versionFieldName])
|
|
327
|
+
: uniqueStrings(input.generated);
|
|
328
|
+
|
|
329
|
+
const freezeNormalizedTable = <
|
|
255
330
|
TName extends string,
|
|
256
331
|
TInput extends StoreTableInput<StoreObjectSchema>,
|
|
257
332
|
>(
|
|
258
333
|
name: TName,
|
|
259
334
|
input: TInput,
|
|
260
|
-
|
|
261
|
-
|
|
262
|
-
|
|
263
|
-
|
|
264
|
-
|
|
265
|
-
|
|
335
|
+
resolved: {
|
|
336
|
+
readonly fixtureSchema: StoreObjectSchema;
|
|
337
|
+
readonly fixtures: StoreTable<TInput>['fixtures'];
|
|
338
|
+
readonly generated: readonly string[];
|
|
339
|
+
readonly identity: string;
|
|
340
|
+
readonly indexed: readonly string[];
|
|
341
|
+
readonly insertSchema: StoreObjectSchema;
|
|
342
|
+
readonly references: Readonly<Partial<Record<string, string>>>;
|
|
343
|
+
readonly schema: StoreObjectSchema;
|
|
344
|
+
readonly signals: StoreTable<TInput, TName>['signals'];
|
|
345
|
+
readonly versioned: boolean;
|
|
266
346
|
}
|
|
347
|
+
): StoreTable<TInput, TName> =>
|
|
348
|
+
Object.freeze({
|
|
349
|
+
fixtureSchema: resolved.fixtureSchema,
|
|
350
|
+
fixtures: resolved.fixtures,
|
|
351
|
+
generated: resolved.generated,
|
|
352
|
+
identity: resolved.identity,
|
|
353
|
+
indexed: resolved.indexed,
|
|
354
|
+
indexes: resolved.indexed,
|
|
355
|
+
insertSchema: resolved.insertSchema,
|
|
356
|
+
name,
|
|
357
|
+
primaryKey: resolved.identity,
|
|
358
|
+
references: resolved.references,
|
|
359
|
+
schema: resolved.schema,
|
|
360
|
+
...(input.search === undefined ? {} : { search: input.search }),
|
|
361
|
+
signals: resolved.signals,
|
|
362
|
+
updateSchema: deriveUpdateSchema(resolved.insertSchema, resolved.identity),
|
|
363
|
+
versioned: resolved.versioned,
|
|
364
|
+
}) as StoreTable<TInput, TName>;
|
|
267
365
|
|
|
268
|
-
|
|
269
|
-
|
|
366
|
+
interface NormalizedTableState {
|
|
367
|
+
readonly generated: readonly string[];
|
|
368
|
+
readonly identity: string;
|
|
369
|
+
readonly indexed: readonly string[];
|
|
370
|
+
readonly references: Readonly<Partial<Record<string, string>>>;
|
|
371
|
+
readonly schema: StoreObjectSchema;
|
|
372
|
+
readonly versioned: boolean;
|
|
373
|
+
}
|
|
374
|
+
|
|
375
|
+
const resolveNormalizedTableState = <
|
|
376
|
+
TInput extends StoreTableInput<StoreObjectSchema>,
|
|
377
|
+
>(
|
|
378
|
+
name: string,
|
|
379
|
+
input: TInput,
|
|
380
|
+
tableNames: readonly string[]
|
|
381
|
+
): NormalizedTableState => {
|
|
382
|
+
const { schema, versioned } = resolveTableSchema(name, input);
|
|
383
|
+
const identity = resolveIdentity(name, input);
|
|
384
|
+
const generated = resolveGeneratedFields(input, versioned);
|
|
385
|
+
const indexed = resolveIndexed(input);
|
|
270
386
|
const references = normalizeReferences(input.references);
|
|
387
|
+
|
|
271
388
|
validateTableInput(
|
|
272
389
|
name,
|
|
273
|
-
|
|
274
|
-
|
|
390
|
+
schema,
|
|
391
|
+
identity,
|
|
275
392
|
generated,
|
|
276
|
-
|
|
393
|
+
indexed,
|
|
277
394
|
references,
|
|
278
395
|
tableNames
|
|
279
396
|
);
|
|
280
397
|
|
|
281
|
-
|
|
282
|
-
|
|
398
|
+
return {
|
|
399
|
+
generated,
|
|
400
|
+
identity,
|
|
401
|
+
indexed,
|
|
402
|
+
references,
|
|
403
|
+
schema,
|
|
404
|
+
versioned,
|
|
405
|
+
};
|
|
406
|
+
};
|
|
407
|
+
|
|
408
|
+
const resolveNormalizedTableArtifacts = <
|
|
409
|
+
TName extends string,
|
|
410
|
+
TInput extends StoreTableInput<StoreObjectSchema>,
|
|
411
|
+
>(
|
|
412
|
+
name: TName,
|
|
413
|
+
input: TInput,
|
|
414
|
+
resolved: NormalizedTableState
|
|
415
|
+
): {
|
|
416
|
+
readonly fixtureSchema: StoreObjectSchema;
|
|
417
|
+
readonly fixtures: StoreTable<TInput>['fixtures'];
|
|
418
|
+
readonly generated: readonly string[];
|
|
419
|
+
readonly identity: string;
|
|
420
|
+
readonly indexed: readonly string[];
|
|
421
|
+
readonly insertSchema: StoreObjectSchema;
|
|
422
|
+
readonly references: Readonly<Partial<Record<string, string>>>;
|
|
423
|
+
readonly schema: StoreObjectSchema;
|
|
424
|
+
readonly signals: StoreTable<TInput, TName>['signals'];
|
|
425
|
+
} => {
|
|
426
|
+
const insertSchema = deriveInsertSchema(resolved.schema, resolved.generated);
|
|
427
|
+
const fixtureSchema = deriveFixtureSchema(
|
|
428
|
+
resolved.schema,
|
|
429
|
+
resolved.generated
|
|
430
|
+
);
|
|
283
431
|
const fixtures = normalizeFixtures(
|
|
284
432
|
name,
|
|
285
|
-
|
|
433
|
+
resolved.identity,
|
|
286
434
|
fixtureSchema,
|
|
287
435
|
input.fixtures
|
|
288
436
|
);
|
|
437
|
+
const signals = createStoreTableSignals(name, resolved.schema) as StoreTable<
|
|
438
|
+
TInput,
|
|
439
|
+
TName
|
|
440
|
+
>['signals'];
|
|
289
441
|
|
|
290
|
-
return
|
|
442
|
+
return {
|
|
291
443
|
fixtureSchema,
|
|
292
444
|
fixtures,
|
|
293
|
-
generated,
|
|
294
|
-
|
|
445
|
+
generated: resolved.generated,
|
|
446
|
+
identity: resolved.identity,
|
|
447
|
+
indexed: resolved.indexed,
|
|
295
448
|
insertSchema,
|
|
296
|
-
|
|
297
|
-
|
|
298
|
-
|
|
299
|
-
|
|
300
|
-
|
|
301
|
-
|
|
302
|
-
|
|
449
|
+
references: resolved.references,
|
|
450
|
+
schema: resolved.schema,
|
|
451
|
+
signals,
|
|
452
|
+
};
|
|
453
|
+
};
|
|
454
|
+
|
|
455
|
+
const normalizeTable = <
|
|
456
|
+
TName extends string,
|
|
457
|
+
TInput extends StoreTableInput<StoreObjectSchema>,
|
|
458
|
+
>(
|
|
459
|
+
name: TName,
|
|
460
|
+
input: TInput,
|
|
461
|
+
tableNames: readonly string[]
|
|
462
|
+
): StoreTable<TInput, TName> => {
|
|
463
|
+
const resolved = resolveNormalizedTableState(name, input, tableNames);
|
|
464
|
+
|
|
465
|
+
return freezeNormalizedTable(name, input, {
|
|
466
|
+
...resolveNormalizedTableArtifacts(name, input, resolved),
|
|
467
|
+
versioned: resolved.versioned,
|
|
468
|
+
});
|
|
469
|
+
};
|
|
470
|
+
|
|
471
|
+
const normalizeTables = <const TTables extends StoreTablesInput>(
|
|
472
|
+
tables: TTables,
|
|
473
|
+
tableNames: readonly Extract<keyof TTables, string>[]
|
|
474
|
+
): MutableTables<TTables> => {
|
|
475
|
+
const normalized = {} as MutableTables<TTables>;
|
|
476
|
+
|
|
477
|
+
for (const name of tableNames) {
|
|
478
|
+
const input = tables[name];
|
|
479
|
+
if (input !== undefined) {
|
|
480
|
+
normalized[name] = normalizeTable(name, input, tableNames);
|
|
481
|
+
}
|
|
482
|
+
}
|
|
483
|
+
|
|
484
|
+
return normalized;
|
|
303
485
|
};
|
|
304
486
|
|
|
487
|
+
const collectStoreSignals = <const TTables extends StoreTablesInput>(
|
|
488
|
+
normalized: MutableTables<TTables>,
|
|
489
|
+
tableNames: readonly Extract<keyof TTables, string>[]
|
|
490
|
+
): readonly AnySignal[] =>
|
|
491
|
+
Object.freeze(
|
|
492
|
+
tableNames.flatMap((name) => {
|
|
493
|
+
const table = normalized[name];
|
|
494
|
+
return table === undefined
|
|
495
|
+
? []
|
|
496
|
+
: [table.signals.created, table.signals.updated, table.signals.removed];
|
|
497
|
+
})
|
|
498
|
+
);
|
|
499
|
+
|
|
305
500
|
/**
|
|
306
|
-
* Declare a
|
|
501
|
+
* Declare a backend-agnostic store definition from entity schemas and
|
|
307
502
|
* persistence metadata.
|
|
308
503
|
*
|
|
309
|
-
* The returned value is a normalized, read-only contract that
|
|
504
|
+
* The returned value is a normalized, read-only contract that adapters can
|
|
310
505
|
* bind to a concrete runtime later.
|
|
311
506
|
*/
|
|
312
507
|
export const store = <const TTables extends StoreTablesInput>(
|
|
313
|
-
tables: TTables
|
|
508
|
+
tables: TTables,
|
|
509
|
+
options: StoreOptions = {}
|
|
314
510
|
): StoreDefinition<TTables> => {
|
|
511
|
+
const kind: StoreKind = options.kind ?? 'tabular';
|
|
315
512
|
const tableNames = Object.freeze(
|
|
316
513
|
Object.keys(tables).toSorted()
|
|
317
514
|
) 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
|
-
|
|
515
|
+
const normalized = normalizeTables(tables, tableNames);
|
|
330
516
|
const get = <TName extends Extract<keyof TTables, string>>(name: TName) =>
|
|
331
517
|
normalized[name];
|
|
518
|
+
const signals = collectStoreSignals(normalized, tableNames);
|
|
332
519
|
|
|
333
520
|
return Object.freeze({
|
|
334
521
|
get,
|
|
335
|
-
kind
|
|
522
|
+
kind,
|
|
523
|
+
signals,
|
|
336
524
|
tableNames,
|
|
337
525
|
tables: Object.freeze(normalized),
|
|
526
|
+
type: 'store' as const,
|
|
338
527
|
});
|
|
339
528
|
};
|
|
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;
|