@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.
- package/.turbo/turbo-lint.log +1 -1
- package/CHANGELOG.md +7 -0
- package/README.md +126 -61
- package/dist/index.d.ts +2 -2
- package/dist/index.d.ts.map +1 -1
- package/dist/index.js +1 -1
- package/dist/index.js.map +1 -1
- package/dist/internal/signal-identity.d.ts +17 -0
- package/dist/internal/signal-identity.d.ts.map +1 -0
- package/dist/internal/signal-identity.js +81 -0
- package/dist/internal/signal-identity.js.map +1 -0
- package/dist/jsonfile/index.d.ts +3 -0
- package/dist/jsonfile/index.d.ts.map +1 -0
- package/dist/jsonfile/index.js +2 -0
- package/dist/jsonfile/index.js.map +1 -0
- package/dist/jsonfile/runtime.d.ts +19 -0
- package/dist/jsonfile/runtime.d.ts.map +1 -0
- package/dist/jsonfile/runtime.js +403 -0
- package/dist/jsonfile/runtime.js.map +1 -0
- package/dist/jsonfile/types.d.ts +40 -0
- package/dist/jsonfile/types.d.ts.map +1 -0
- package/dist/{drizzle → jsonfile}/types.js.map +1 -1
- package/dist/store.d.ts +8 -18
- package/dist/store.d.ts.map +1 -1
- package/dist/store.js +152 -94
- package/dist/store.js.map +1 -1
- package/dist/testing.d.ts +28 -0
- package/dist/testing.d.ts.map +1 -0
- package/dist/testing.js +90 -0
- package/dist/testing.js.map +1 -0
- package/dist/trails/crud.d.ts +38 -0
- package/dist/trails/crud.d.ts.map +1 -0
- package/dist/trails/crud.js +112 -0
- package/dist/trails/crud.js.map +1 -0
- package/dist/trails/index.d.ts +7 -0
- package/dist/trails/index.d.ts.map +1 -0
- package/dist/trails/index.js +4 -0
- package/dist/trails/index.js.map +1 -0
- package/dist/trails/reconcile.d.ts +36 -0
- package/dist/trails/reconcile.d.ts.map +1 -0
- package/dist/trails/reconcile.js +142 -0
- package/dist/trails/reconcile.js.map +1 -0
- package/dist/trails/sync.d.ts +25 -0
- package/dist/trails/sync.d.ts.map +1 -0
- package/dist/trails/sync.js +96 -0
- package/dist/trails/sync.js.map +1 -0
- package/dist/trails/utils.d.ts +51 -0
- package/dist/trails/utils.d.ts.map +1 -0
- package/dist/trails/utils.js +65 -0
- package/dist/trails/utils.js.map +1 -0
- package/dist/types.d.ts +205 -35
- package/dist/types.d.ts.map +1 -1
- package/package.json +6 -9
- package/src/__tests__/crud.test.ts +346 -0
- package/src/__tests__/jsonfile.test.ts +502 -0
- package/src/__tests__/store.test.ts +239 -33
- package/src/__tests__/sync-reconcile.test.ts +646 -0
- package/src/index.ts +12 -8
- package/src/internal/signal-identity.ts +162 -0
- package/src/jsonfile/index.ts +6 -0
- package/src/jsonfile/runtime.ts +702 -0
- package/src/jsonfile/types.ts +50 -0
- package/src/store.ts +296 -134
- package/src/testing.ts +175 -0
- package/src/trails/crud.ts +355 -0
- package/src/trails/index.ts +10 -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 +355 -63
- package/tsconfig.tests.json +10 -0
- package/tsconfig.tsbuildinfo +1 -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/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/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 {
|
|
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
|
-
|
|
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
|
|
149
|
-
return hasField(partial,
|
|
150
|
-
? omitFields(partial, [
|
|
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
|
-
|
|
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[
|
|
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
|
-
|
|
179
|
+
identity: string
|
|
198
180
|
): void => {
|
|
199
|
-
if (hasField(schema,
|
|
181
|
+
if (hasField(schema, identity)) {
|
|
200
182
|
return;
|
|
201
183
|
}
|
|
202
184
|
|
|
203
185
|
throw new ValidationError(
|
|
204
|
-
`Store table "${tableName}" declares
|
|
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
|
-
|
|
245
|
+
identity: string,
|
|
212
246
|
generated: readonly string[],
|
|
213
|
-
|
|
247
|
+
indexed: readonly string[],
|
|
214
248
|
references: Readonly<Partial<Record<string, string>>>,
|
|
215
249
|
tableNames: readonly string[]
|
|
216
250
|
): void => {
|
|
217
|
-
validatePrimaryKey(tableName, schema,
|
|
251
|
+
validatePrimaryKey(tableName, schema, identity);
|
|
218
252
|
validateFieldList(tableName, schema, generated, 'generated');
|
|
219
|
-
validateFieldList(tableName, schema,
|
|
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
|
-
|
|
283
|
+
identity: string,
|
|
230
284
|
fixtureSchema: StoreObjectSchema,
|
|
231
285
|
fixtures: TInput['fixtures']
|
|
232
286
|
): StoreTable<TInput>['fixtures'] => {
|
|
233
|
-
|
|
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
|
|
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));
|
|
295
|
+
for (const [index, fixture] of fixtureList.entries()) {
|
|
296
|
+
normalized.push(parseFixture(tableName, fixtureSchema, fixture, index));
|
|
248
297
|
}
|
|
249
298
|
|
|
250
|
-
validateFixturePrimaryKeys(tableName,
|
|
299
|
+
validateFixturePrimaryKeys(tableName, identity, normalized);
|
|
251
300
|
return Object.freeze(normalized) as StoreTable<TInput>['fixtures'];
|
|
252
301
|
};
|
|
253
302
|
|
|
254
|
-
const
|
|
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
|
-
|
|
261
|
-
|
|
262
|
-
|
|
263
|
-
|
|
264
|
-
|
|
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
|
-
|
|
269
|
-
|
|
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
|
-
|
|
274
|
-
|
|
391
|
+
schema,
|
|
392
|
+
identity,
|
|
275
393
|
generated,
|
|
276
|
-
|
|
394
|
+
indexed,
|
|
277
395
|
references,
|
|
278
396
|
tableNames
|
|
279
397
|
);
|
|
280
398
|
|
|
281
|
-
|
|
282
|
-
|
|
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
|
-
|
|
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
|
|
443
|
+
return {
|
|
291
444
|
fixtureSchema,
|
|
292
445
|
fixtures,
|
|
293
|
-
generated,
|
|
294
|
-
|
|
446
|
+
generated: resolved.generated,
|
|
447
|
+
identity: resolved.identity,
|
|
448
|
+
indexed: resolved.indexed,
|
|
295
449
|
insertSchema,
|
|
296
|
-
|
|
297
|
-
|
|
298
|
-
|
|
299
|
-
|
|
300
|
-
|
|
301
|
-
|
|
302
|
-
|
|
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
|
|
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;
|