@ontrails/store 1.0.0-beta.14 → 1.0.0-beta.16
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/CHANGELOG.md +43 -0
- package/README.md +144 -63
- package/package.json +14 -9
- package/src/adapter-support.ts +163 -0
- package/src/crud-doctrine.ts +43 -0
- package/src/index.ts +17 -8
- package/src/jsonfile/index.ts +6 -0
- package/src/jsonfile/runtime.ts +700 -0
- package/src/jsonfile/types.ts +50 -0
- package/src/store.ts +298 -137
- package/src/testing.ts +175 -0
- package/src/trails/crud.ts +356 -0
- package/src/trails/index.ts +15 -0
- package/src/trails/reconcile.ts +281 -0
- package/src/trails/sync.ts +251 -0
- package/src/trails/utils.ts +96 -0
- package/src/types.ts +361 -68
- package/.agents/notes/2026-04-04/handoff-202604032309-9e85a104.md +0 -38
- package/.turbo/turbo-build.log +0 -1
- package/.turbo/turbo-lint.log +0 -3
- package/.turbo/turbo-typecheck.log +0 -1
- package/dist/drizzle/index.d.ts +0 -3
- package/dist/drizzle/index.d.ts.map +0 -1
- package/dist/drizzle/index.js +0 -2
- package/dist/drizzle/index.js.map +0 -1
- package/dist/drizzle/runtime.d.ts +0 -21
- package/dist/drizzle/runtime.d.ts.map +0 -1
- package/dist/drizzle/runtime.js +0 -458
- package/dist/drizzle/runtime.js.map +0 -1
- package/dist/drizzle/schema.d.ts +0 -15
- package/dist/drizzle/schema.d.ts.map +0 -1
- package/dist/drizzle/schema.js +0 -322
- package/dist/drizzle/schema.js.map +0 -1
- package/dist/drizzle/types.d.ts +0 -40
- package/dist/drizzle/types.d.ts.map +0 -1
- package/dist/drizzle/types.js +0 -2
- package/dist/drizzle/types.js.map +0 -1
- package/dist/index.d.ts +0 -3
- package/dist/index.d.ts.map +0 -1
- package/dist/index.js +0 -2
- package/dist/index.js.map +0 -1
- package/dist/store.d.ts +0 -26
- package/dist/store.d.ts.map +0 -1
- package/dist/store.js +0 -192
- package/dist/store.js.map +0 -1
- package/dist/types.d.ts +0 -224
- package/dist/types.d.ts.map +0 -1
- package/dist/types.js +0 -2
- package/dist/types.js.map +0 -1
- package/src/__tests__/store.test.ts +0 -333
- package/src/drizzle/__tests__/drizzle.test.ts +0 -469
- package/src/drizzle/index.ts +0 -17
- package/src/drizzle/runtime.ts +0 -853
- package/src/drizzle/schema.ts +0 -577
- package/src/drizzle/types.ts +0 -70
- package/tsconfig.json +0 -9
- package/tsconfig.tsbuildinfo +0 -1
package/src/types.ts
CHANGED
|
@@ -1,5 +1,19 @@
|
|
|
1
|
+
import type { Signal } from '@ontrails/core';
|
|
2
|
+
import type { StoreAccessorProtocol } from '@ontrails/core/store';
|
|
1
3
|
import type { z } from 'zod';
|
|
2
4
|
|
|
5
|
+
/**
|
|
6
|
+
* Backend-agnostic persistence shapes that an adapter can interpret.
|
|
7
|
+
*/
|
|
8
|
+
export type StoreKind = 'tabular' | 'document' | 'file' | 'kv' | 'cache';
|
|
9
|
+
|
|
10
|
+
/**
|
|
11
|
+
* Store-level options applied to the authored definition.
|
|
12
|
+
*/
|
|
13
|
+
export interface StoreOptions {
|
|
14
|
+
readonly kind?: StoreKind;
|
|
15
|
+
}
|
|
16
|
+
|
|
3
17
|
/**
|
|
4
18
|
* Object schema accepted by the store definition layer.
|
|
5
19
|
*/
|
|
@@ -13,83 +27,176 @@ export type StoreFieldKey<TSchema extends StoreObjectSchema> = Extract<
|
|
|
13
27
|
string
|
|
14
28
|
>;
|
|
15
29
|
|
|
30
|
+
type VersionedSchema<
|
|
31
|
+
TSchema extends StoreObjectSchema,
|
|
32
|
+
TVersioned extends boolean | undefined,
|
|
33
|
+
> = TVersioned extends true
|
|
34
|
+
? TSchema extends z.ZodObject<infer TShape>
|
|
35
|
+
? z.ZodObject<TShape & { version: z.ZodNumber }>
|
|
36
|
+
: never
|
|
37
|
+
: TSchema;
|
|
38
|
+
|
|
16
39
|
type GeneratedFieldNames<
|
|
17
40
|
TSchema extends StoreObjectSchema,
|
|
18
|
-
TGenerated extends readonly
|
|
41
|
+
TGenerated extends readonly string[] | undefined,
|
|
19
42
|
> = TGenerated extends readonly StoreFieldKey<TSchema>[]
|
|
20
43
|
? TGenerated[number]
|
|
21
44
|
: never;
|
|
22
45
|
|
|
46
|
+
// We intentionally use Zod v4's shape-routed object infer helpers here because
|
|
47
|
+
// the public z.input/z.output path widens away the generic equality we need at
|
|
48
|
+
// store/core trail boundaries. These $-prefixed helpers are semi-internal Zod
|
|
49
|
+
// v4 API; that maintenance tradeoff is acceptable here because the public path
|
|
50
|
+
// does not preserve the boundary-proof shape we need.
|
|
51
|
+
type ObjectInputOf<TShape extends z.ZodRawShape> = z.core.$InferObjectInput<
|
|
52
|
+
TShape,
|
|
53
|
+
Record<never, never>
|
|
54
|
+
>;
|
|
55
|
+
|
|
56
|
+
type ObjectOutputOf<TShape extends z.ZodRawShape> = z.core.$InferObjectOutput<
|
|
57
|
+
TShape,
|
|
58
|
+
Record<never, never>
|
|
59
|
+
>;
|
|
60
|
+
|
|
23
61
|
/**
|
|
24
62
|
* Seed row accepted for one table fixture.
|
|
25
63
|
*
|
|
26
64
|
* Generated fields may be supplied explicitly, but they are optional so test
|
|
27
65
|
* fixtures can omit timestamps and similar server-managed values when the mock
|
|
28
66
|
* store can synthesize them.
|
|
67
|
+
*
|
|
68
|
+
* Routes input inference through the object's `shape` via a conditional
|
|
69
|
+
* `infer` so fixture inputs stay structurally aligned with the derived
|
|
70
|
+
* create/upsert inputs they meet at store/core trail boundaries.
|
|
71
|
+
*
|
|
72
|
+
* This is not the same type-level path core's `deriveTrail()` uses — core
|
|
73
|
+
* still goes through `z.input<TContour>` / `z.output<TContour>` — but at
|
|
74
|
+
* concrete instantiations it collapses to the same structural fixture shape
|
|
75
|
+
* while preserving generic equality across the store/core seam.
|
|
29
76
|
*/
|
|
30
77
|
export type StoreFixtureInput<
|
|
31
78
|
TSchema extends StoreObjectSchema,
|
|
32
|
-
TGenerated extends readonly
|
|
33
|
-
| readonly
|
|
79
|
+
TGenerated extends readonly string[] | undefined =
|
|
80
|
+
| readonly string[]
|
|
34
81
|
| undefined,
|
|
35
|
-
> =
|
|
36
|
-
|
|
37
|
-
|
|
38
|
-
|
|
39
|
-
|
|
40
|
-
|
|
41
|
-
|
|
42
|
-
|
|
43
|
-
|
|
44
|
-
|
|
82
|
+
> = [TSchema] extends [z.ZodObject<infer TShape>]
|
|
83
|
+
? Omit<
|
|
84
|
+
ObjectInputOf<TShape>,
|
|
85
|
+
Extract<
|
|
86
|
+
GeneratedFieldNames<TSchema, TGenerated>,
|
|
87
|
+
keyof ObjectInputOf<TShape>
|
|
88
|
+
>
|
|
89
|
+
> &
|
|
90
|
+
Partial<
|
|
91
|
+
Pick<
|
|
92
|
+
ObjectInputOf<TShape>,
|
|
93
|
+
Extract<
|
|
94
|
+
GeneratedFieldNames<TSchema, TGenerated>,
|
|
95
|
+
keyof ObjectInputOf<TShape>
|
|
96
|
+
>
|
|
97
|
+
>
|
|
98
|
+
>
|
|
99
|
+
: never;
|
|
45
100
|
|
|
46
101
|
/**
|
|
47
102
|
* Normalized fixture row after schema validation and default application.
|
|
103
|
+
*
|
|
104
|
+
* Mirror of {@link StoreFixtureInput} on the output side — routed through
|
|
105
|
+
* `$InferObjectOutput<TShape, Record<never, never>>` so row types stay
|
|
106
|
+
* structurally aligned with the contour output shapes they compose against.
|
|
107
|
+
*
|
|
108
|
+
* As with {@link StoreFixtureInput}, this is a shape-routed equivalent rather
|
|
109
|
+
* than the identical `z.output<TSchema>` inference path core uses directly.
|
|
48
110
|
*/
|
|
49
111
|
export type StoreFixtureRow<
|
|
50
112
|
TSchema extends StoreObjectSchema,
|
|
51
|
-
TGenerated extends readonly
|
|
52
|
-
| readonly
|
|
113
|
+
TGenerated extends readonly string[] | undefined =
|
|
114
|
+
| readonly string[]
|
|
53
115
|
| undefined,
|
|
54
|
-
> =
|
|
55
|
-
|
|
56
|
-
|
|
57
|
-
|
|
58
|
-
|
|
59
|
-
|
|
60
|
-
|
|
61
|
-
|
|
62
|
-
|
|
63
|
-
|
|
116
|
+
> = [TSchema] extends [z.ZodObject<infer TShape>]
|
|
117
|
+
? Omit<
|
|
118
|
+
ObjectOutputOf<TShape>,
|
|
119
|
+
Extract<
|
|
120
|
+
GeneratedFieldNames<TSchema, TGenerated>,
|
|
121
|
+
keyof ObjectOutputOf<TShape>
|
|
122
|
+
>
|
|
123
|
+
> &
|
|
124
|
+
Partial<
|
|
125
|
+
Pick<
|
|
126
|
+
ObjectOutputOf<TShape>,
|
|
127
|
+
Extract<
|
|
128
|
+
GeneratedFieldNames<TSchema, TGenerated>,
|
|
129
|
+
keyof ObjectOutputOf<TShape>
|
|
130
|
+
>
|
|
131
|
+
>
|
|
132
|
+
>
|
|
133
|
+
: never;
|
|
64
134
|
|
|
65
135
|
/**
|
|
66
|
-
*
|
|
136
|
+
* Adapter-owned search metadata.
|
|
67
137
|
*
|
|
68
138
|
* The core package keeps this opaque on purpose. Search behavior is declared
|
|
69
|
-
* here and interpreted by a concrete
|
|
139
|
+
* here and interpreted by a concrete adapter later.
|
|
70
140
|
*/
|
|
71
141
|
export type StoreSearchDefinition = Readonly<Record<string, unknown>>;
|
|
72
142
|
|
|
73
143
|
/**
|
|
74
|
-
*
|
|
144
|
+
* Change signals projected from one store entity definition.
|
|
75
145
|
*/
|
|
76
|
-
export interface
|
|
146
|
+
export interface StoreTableSignals<TPayload> {
|
|
147
|
+
readonly created: Signal<TPayload>;
|
|
148
|
+
readonly updated: Signal<TPayload>;
|
|
149
|
+
readonly removed: Signal<TPayload>;
|
|
150
|
+
}
|
|
151
|
+
|
|
152
|
+
/**
|
|
153
|
+
* Shared fields for all store table input variants.
|
|
154
|
+
*/
|
|
155
|
+
interface StoreTableInputBase<
|
|
77
156
|
TSchema extends StoreObjectSchema = StoreObjectSchema,
|
|
78
157
|
TGenerated extends readonly StoreFieldKey<TSchema>[] | undefined =
|
|
79
158
|
| readonly StoreFieldKey<TSchema>[]
|
|
80
159
|
| undefined,
|
|
160
|
+
TVersioned extends boolean | undefined = boolean | undefined,
|
|
81
161
|
> {
|
|
82
|
-
readonly fixtures?: readonly StoreFixtureInput<
|
|
162
|
+
readonly fixtures?: readonly StoreFixtureInput<
|
|
163
|
+
VersionedSchema<TSchema, TVersioned>,
|
|
164
|
+
GeneratedFieldsOfShape<TSchema, TGenerated, TVersioned>
|
|
165
|
+
>[];
|
|
83
166
|
readonly generated?: TGenerated;
|
|
167
|
+
readonly indexed?: readonly StoreFieldKey<TSchema>[];
|
|
84
168
|
readonly indexes?: readonly StoreFieldKey<TSchema>[];
|
|
85
|
-
readonly primaryKey: StoreFieldKey<TSchema>;
|
|
86
169
|
readonly references?: Readonly<
|
|
87
170
|
Partial<Record<StoreFieldKey<TSchema>, string>>
|
|
88
171
|
>;
|
|
89
172
|
readonly schema: TSchema;
|
|
90
173
|
readonly search?: StoreSearchDefinition;
|
|
174
|
+
readonly versioned?: TVersioned;
|
|
91
175
|
}
|
|
92
176
|
|
|
177
|
+
/**
|
|
178
|
+
* Authored metadata for one store entity.
|
|
179
|
+
*
|
|
180
|
+
* At least one of `identity` or `primaryKey` must be provided. Omitting both
|
|
181
|
+
* is a compile-time error — `resolveIdentity` would throw at runtime without
|
|
182
|
+
* this guard.
|
|
183
|
+
*/
|
|
184
|
+
export type StoreTableInput<
|
|
185
|
+
TSchema extends StoreObjectSchema = StoreObjectSchema,
|
|
186
|
+
TGenerated extends readonly StoreFieldKey<TSchema>[] | undefined =
|
|
187
|
+
| readonly StoreFieldKey<TSchema>[]
|
|
188
|
+
| undefined,
|
|
189
|
+
TVersioned extends boolean | undefined = boolean | undefined,
|
|
190
|
+
> =
|
|
191
|
+
| (StoreTableInputBase<TSchema, TGenerated, TVersioned> & {
|
|
192
|
+
readonly identity: StoreFieldKey<TSchema>;
|
|
193
|
+
readonly primaryKey?: StoreFieldKey<TSchema>;
|
|
194
|
+
})
|
|
195
|
+
| (StoreTableInputBase<TSchema, TGenerated, TVersioned> & {
|
|
196
|
+
readonly identity?: StoreFieldKey<TSchema>;
|
|
197
|
+
readonly primaryKey: StoreFieldKey<TSchema>;
|
|
198
|
+
});
|
|
199
|
+
|
|
93
200
|
/**
|
|
94
201
|
* Record of authored tables passed to `store(...)`.
|
|
95
202
|
*/
|
|
@@ -97,25 +204,82 @@ export type StoreTablesInput = Record<
|
|
|
97
204
|
string,
|
|
98
205
|
StoreTableInput<
|
|
99
206
|
StoreObjectSchema,
|
|
100
|
-
readonly StoreFieldKey<StoreObjectSchema>[] | undefined
|
|
207
|
+
readonly StoreFieldKey<StoreObjectSchema>[] | undefined,
|
|
208
|
+
boolean | undefined
|
|
101
209
|
>
|
|
102
210
|
>;
|
|
103
211
|
|
|
212
|
+
type DeclaredGeneratedFieldsOfInput<TInput extends StoreTableInput> =
|
|
213
|
+
TInput['generated'] extends readonly StoreFieldKey<TInput['schema']>[]
|
|
214
|
+
? TInput['generated']
|
|
215
|
+
: readonly [];
|
|
216
|
+
|
|
217
|
+
type GeneratedFieldsOfShape<
|
|
218
|
+
TSchema extends StoreObjectSchema,
|
|
219
|
+
TGenerated extends readonly StoreFieldKey<TSchema>[] | undefined,
|
|
220
|
+
TVersioned extends boolean | undefined,
|
|
221
|
+
> = TVersioned extends true
|
|
222
|
+
? readonly [
|
|
223
|
+
...(TGenerated extends readonly StoreFieldKey<TSchema>[]
|
|
224
|
+
? TGenerated
|
|
225
|
+
: readonly []),
|
|
226
|
+
'version',
|
|
227
|
+
]
|
|
228
|
+
: TGenerated extends readonly StoreFieldKey<TSchema>[]
|
|
229
|
+
? TGenerated
|
|
230
|
+
: readonly [];
|
|
231
|
+
|
|
232
|
+
type VersionedFieldsOfInput<TInput extends StoreTableInput> =
|
|
233
|
+
TInput['versioned'] extends true ? true : false;
|
|
234
|
+
|
|
235
|
+
type SchemaOfInput<TInput extends StoreTableInput> = VersionedSchema<
|
|
236
|
+
TInput['schema'],
|
|
237
|
+
VersionedFieldsOfInput<TInput>
|
|
238
|
+
>;
|
|
239
|
+
|
|
104
240
|
/**
|
|
105
241
|
* Preserve generated fields when present, otherwise normalize to an empty tuple.
|
|
106
242
|
*/
|
|
107
243
|
export type GeneratedFieldsOfInput<TInput extends StoreTableInput> =
|
|
108
|
-
|
|
109
|
-
|
|
110
|
-
|
|
244
|
+
GeneratedFieldsOfShape<
|
|
245
|
+
TInput['schema'],
|
|
246
|
+
DeclaredGeneratedFieldsOfInput<TInput>,
|
|
247
|
+
VersionedFieldsOfInput<TInput>
|
|
248
|
+
>;
|
|
249
|
+
|
|
250
|
+
/**
|
|
251
|
+
* Preserve the authored identity field.
|
|
252
|
+
*/
|
|
253
|
+
export type IdentityFieldOfInput<TInput extends StoreTableInput> =
|
|
254
|
+
TInput['identity'] extends StoreFieldKey<TInput['schema']>
|
|
255
|
+
? TInput['identity']
|
|
256
|
+
: TInput['primaryKey'] extends StoreFieldKey<TInput['schema']>
|
|
257
|
+
? TInput['primaryKey']
|
|
258
|
+
: never;
|
|
111
259
|
|
|
112
260
|
/**
|
|
113
|
-
* Preserve
|
|
261
|
+
* Preserve indexed fields when present, otherwise normalize to an empty tuple.
|
|
262
|
+
*
|
|
263
|
+
* At runtime `resolveIndexed` merges both `indexed` and `indexes` arrays, so
|
|
264
|
+
* this type mirrors that behavior: when both are present, the result is the
|
|
265
|
+
* union of both tuples. When only one is provided, it is used directly.
|
|
266
|
+
*/
|
|
267
|
+
export type IndexedFieldsOfInput<TInput extends StoreTableInput> =
|
|
268
|
+
TInput['indexed'] extends readonly StoreFieldKey<TInput['schema']>[]
|
|
269
|
+
? TInput['indexes'] extends readonly StoreFieldKey<TInput['schema']>[]
|
|
270
|
+
? readonly [...TInput['indexed'], ...TInput['indexes']]
|
|
271
|
+
: TInput['indexed']
|
|
272
|
+
: TInput['indexes'] extends readonly StoreFieldKey<TInput['schema']>[]
|
|
273
|
+
? TInput['indexes']
|
|
274
|
+
: readonly [];
|
|
275
|
+
|
|
276
|
+
/**
|
|
277
|
+
* Backward-compatible alias for code that still uses the SQL-shaped name.
|
|
114
278
|
*/
|
|
115
279
|
export type IndexFieldsOfInput<TInput extends StoreTableInput> =
|
|
116
280
|
TInput['indexes'] extends readonly StoreFieldKey<TInput['schema']>[]
|
|
117
281
|
? TInput['indexes']
|
|
118
|
-
:
|
|
282
|
+
: IndexedFieldsOfInput<TInput>;
|
|
119
283
|
|
|
120
284
|
/**
|
|
121
285
|
* Preserve references when present, otherwise normalize to an empty object.
|
|
@@ -132,11 +296,11 @@ export type ReferencesOfInput<TInput extends StoreTableInput> =
|
|
|
132
296
|
*/
|
|
133
297
|
export type FixturesOfInput<TInput extends StoreTableInput> =
|
|
134
298
|
TInput['fixtures'] extends readonly StoreFixtureInput<
|
|
135
|
-
TInput
|
|
299
|
+
SchemaOfInput<TInput>,
|
|
136
300
|
GeneratedFieldsOfInput<TInput>
|
|
137
301
|
>[]
|
|
138
302
|
? readonly StoreFixtureRow<
|
|
139
|
-
TInput
|
|
303
|
+
SchemaOfInput<TInput>,
|
|
140
304
|
GeneratedFieldsOfInput<TInput>
|
|
141
305
|
>[]
|
|
142
306
|
: readonly [];
|
|
@@ -151,14 +315,18 @@ export interface StoreTable<
|
|
|
151
315
|
readonly fixtureSchema: StoreObjectSchema;
|
|
152
316
|
readonly fixtures: FixturesOfInput<TInput>;
|
|
153
317
|
readonly generated: GeneratedFieldsOfInput<TInput>;
|
|
154
|
-
readonly
|
|
318
|
+
readonly identity: IdentityFieldOfInput<TInput>;
|
|
319
|
+
readonly indexed: IndexedFieldsOfInput<TInput>;
|
|
320
|
+
readonly indexes: IndexedFieldsOfInput<TInput>;
|
|
155
321
|
readonly insertSchema: StoreObjectSchema;
|
|
156
322
|
readonly name: TName;
|
|
157
|
-
readonly primaryKey: TInput
|
|
323
|
+
readonly primaryKey: IdentityFieldOfInput<TInput>;
|
|
158
324
|
readonly references: ReferencesOfInput<TInput>;
|
|
159
|
-
readonly schema: TInput
|
|
160
|
-
readonly search?:
|
|
325
|
+
readonly schema: SchemaOfInput<TInput>;
|
|
326
|
+
readonly search?: StoreSearchDefinition | undefined;
|
|
327
|
+
readonly signals: StoreTableSignals<z.output<SchemaOfInput<TInput>>>;
|
|
161
328
|
readonly updateSchema: StoreObjectSchema;
|
|
329
|
+
readonly versioned: VersionedFieldsOfInput<TInput>;
|
|
162
330
|
}
|
|
163
331
|
|
|
164
332
|
/**
|
|
@@ -170,7 +338,8 @@ export interface StoreDefinition<
|
|
|
170
338
|
readonly get: <TName extends Extract<keyof TTables, string>>(
|
|
171
339
|
name: TName
|
|
172
340
|
) => StoreTable<TTables[TName], TName>;
|
|
173
|
-
readonly kind:
|
|
341
|
+
readonly kind: StoreKind;
|
|
342
|
+
readonly signals: readonly Signal<unknown>[];
|
|
174
343
|
readonly tableNames: readonly Extract<keyof TTables, string>[];
|
|
175
344
|
readonly tables: {
|
|
176
345
|
readonly [TName in keyof TTables]: StoreTable<
|
|
@@ -178,12 +347,13 @@ export interface StoreDefinition<
|
|
|
178
347
|
Extract<TName, string>
|
|
179
348
|
>;
|
|
180
349
|
};
|
|
350
|
+
readonly type: 'store';
|
|
181
351
|
}
|
|
182
352
|
|
|
183
353
|
/**
|
|
184
354
|
* Structural view of any normalized store table.
|
|
185
355
|
*
|
|
186
|
-
* This stays broad on purpose so
|
|
356
|
+
* This stays broad on purpose so adapter packages can accept concrete store
|
|
187
357
|
* definitions returned by `store(...)` without erasing their table-specific
|
|
188
358
|
* types back to one canonical generic instantiation.
|
|
189
359
|
*/
|
|
@@ -191,6 +361,8 @@ export interface AnyStoreTable {
|
|
|
191
361
|
readonly fixtureSchema: StoreObjectSchema;
|
|
192
362
|
readonly fixtures: readonly Record<string, unknown>[];
|
|
193
363
|
readonly generated: readonly string[];
|
|
364
|
+
readonly identity: string;
|
|
365
|
+
readonly indexed: readonly string[];
|
|
194
366
|
readonly indexes: readonly string[];
|
|
195
367
|
readonly insertSchema: StoreObjectSchema;
|
|
196
368
|
readonly name: string;
|
|
@@ -198,16 +370,20 @@ export interface AnyStoreTable {
|
|
|
198
370
|
readonly references: Readonly<Partial<Record<string, string>>>;
|
|
199
371
|
readonly schema: StoreObjectSchema;
|
|
200
372
|
readonly search?: StoreSearchDefinition | undefined;
|
|
373
|
+
readonly signals: StoreTableSignals<unknown>;
|
|
201
374
|
readonly updateSchema: StoreObjectSchema;
|
|
375
|
+
readonly versioned: boolean;
|
|
202
376
|
}
|
|
203
377
|
|
|
204
378
|
/**
|
|
205
379
|
* Structural view of any normalized store definition.
|
|
206
380
|
*/
|
|
207
381
|
export interface AnyStoreDefinition {
|
|
208
|
-
readonly kind:
|
|
382
|
+
readonly kind: StoreKind;
|
|
383
|
+
readonly signals: readonly Signal<unknown>[];
|
|
209
384
|
readonly tableNames: readonly string[];
|
|
210
385
|
readonly tables: Readonly<Record<string, AnyStoreTable>>;
|
|
386
|
+
readonly type: 'store';
|
|
211
387
|
}
|
|
212
388
|
|
|
213
389
|
type GeneratedFieldKeysOf<TTable extends AnyStoreTable> = readonly Extract<
|
|
@@ -237,9 +413,9 @@ export type FixtureOf<TTable extends AnyStoreTable> = StoreFixtureRow<
|
|
|
237
413
|
>;
|
|
238
414
|
|
|
239
415
|
/**
|
|
240
|
-
*
|
|
416
|
+
* Identity field name for one store entity.
|
|
241
417
|
*/
|
|
242
|
-
export type
|
|
418
|
+
export type IdentityOf<TTable extends AnyStoreTable> = TTable['identity'];
|
|
243
419
|
|
|
244
420
|
/**
|
|
245
421
|
* Server-managed fields for one store table.
|
|
@@ -252,21 +428,39 @@ export type GeneratedKeysOf<TTable extends AnyStoreTable> = Extract<
|
|
|
252
428
|
/**
|
|
253
429
|
* Insert shape: entity minus generated fields, with defaulted fields optional.
|
|
254
430
|
*
|
|
255
|
-
*
|
|
256
|
-
*
|
|
431
|
+
* Routes through `$InferObjectInput<TShape, Record<never, never>>` via a
|
|
432
|
+
* conditional `infer` on the object's `shape`. At concrete instantiations
|
|
433
|
+
* this collapses to the same structural shape as
|
|
434
|
+
* `Omit<z.input<TTable['schema']>, GeneratedKeysOf<TTable>>`, but the
|
|
435
|
+
* shape-routed form lets TypeScript prove structural equality with
|
|
436
|
+
* `CreateInputOf<Contour, ...>` at trail boundaries without widening
|
|
437
|
+
* generic call sites to `Record<string, unknown>`.
|
|
438
|
+
*
|
|
439
|
+
* Defaulted fields remain optional because `$InferObjectInput` honors
|
|
440
|
+
* `OptionalInSchema`, mirroring the runtime insert schema behavior.
|
|
257
441
|
*/
|
|
258
|
-
export type InsertOf<TTable extends AnyStoreTable> =
|
|
259
|
-
|
|
260
|
-
|
|
261
|
-
|
|
442
|
+
export type InsertOf<TTable extends AnyStoreTable> = [
|
|
443
|
+
TTable['schema'],
|
|
444
|
+
] extends [z.ZodObject<infer TShape>]
|
|
445
|
+
? Omit<ObjectInputOf<TShape>, GeneratedKeysOf<TTable>>
|
|
446
|
+
: never;
|
|
262
447
|
|
|
263
448
|
/**
|
|
264
449
|
* Update shape: partial insert minus the primary key (immutable identifier).
|
|
265
450
|
*/
|
|
266
451
|
export type UpdateOf<TTable extends AnyStoreTable> = Partial<
|
|
267
|
-
Omit<InsertOf<TTable>,
|
|
452
|
+
Omit<InsertOf<TTable>, IdentityOf<TTable>>
|
|
268
453
|
>;
|
|
269
454
|
|
|
455
|
+
/**
|
|
456
|
+
* Upsert shape: entity payload with generated fields remaining optional.
|
|
457
|
+
*
|
|
458
|
+
* This matches the backend-agnostic "create or replace" contract while
|
|
459
|
+
* still allowing adapters to synthesize generated values like IDs and
|
|
460
|
+
* timestamps when the caller omits them.
|
|
461
|
+
*/
|
|
462
|
+
export type UpsertOf<TTable extends AnyStoreTable> = FixtureInputOf<TTable>;
|
|
463
|
+
|
|
270
464
|
/**
|
|
271
465
|
* Typed filter shape for list operations.
|
|
272
466
|
*/
|
|
@@ -284,10 +478,10 @@ export interface StoreListOptions {
|
|
|
284
478
|
* Shared identifier type for read/write accessors.
|
|
285
479
|
*/
|
|
286
480
|
export type StoreIdentifierOf<TTable extends AnyStoreTable> =
|
|
287
|
-
EntityOf<TTable>[Extract<
|
|
481
|
+
EntityOf<TTable>[Extract<IdentityOf<TTable>, keyof EntityOf<TTable>>];
|
|
288
482
|
|
|
289
483
|
/**
|
|
290
|
-
* Access mode for a bound store connection or
|
|
484
|
+
* Access mode for a bound store connection or resource.
|
|
291
485
|
*/
|
|
292
486
|
export type StoreAccessMode = 'readonly' | 'readwrite';
|
|
293
487
|
|
|
@@ -295,7 +489,7 @@ export type StoreAccessMode = 'readonly' | 'readwrite';
|
|
|
295
489
|
* Read-only table operations that every bound store must expose.
|
|
296
490
|
*/
|
|
297
491
|
export interface ReadOnlyStoreTableAccessor<TTable extends AnyStoreTable> {
|
|
298
|
-
/** Retrieve a single entity by
|
|
492
|
+
/** Retrieve a single entity by identity. Returns `null` when not found. */
|
|
299
493
|
get(id: StoreIdentifierOf<TTable>): Promise<EntityOf<TTable> | null>;
|
|
300
494
|
/**
|
|
301
495
|
* List entities, optionally filtered. Returns all rows when no filters are
|
|
@@ -308,33 +502,91 @@ export interface ReadOnlyStoreTableAccessor<TTable extends AnyStoreTable> {
|
|
|
308
502
|
}
|
|
309
503
|
|
|
310
504
|
/**
|
|
311
|
-
*
|
|
505
|
+
* Backend-agnostic writable operations layered on top of the read contract.
|
|
312
506
|
*/
|
|
313
|
-
export interface
|
|
507
|
+
export interface StoreAccessor<
|
|
314
508
|
TTable extends AnyStoreTable,
|
|
315
509
|
> extends ReadOnlyStoreTableAccessor<TTable> {
|
|
316
510
|
/**
|
|
317
|
-
*
|
|
511
|
+
* Create or replace one entity using the store's identity field.
|
|
318
512
|
*
|
|
319
513
|
* @throws {AlreadyExistsError} On primary key or unique constraint violation.
|
|
320
514
|
*
|
|
321
515
|
* @remarks
|
|
322
|
-
* This is an intentional throw-based boundary: store
|
|
516
|
+
* This is an intentional throw-based boundary: store adapters throw typed
|
|
323
517
|
* errors (`AlreadyExistsError`) rather than returning `Result`. Trail
|
|
324
518
|
* implementations that call store accessors should catch and convert to
|
|
325
|
-
* `Result.err()` at their level. A future
|
|
326
|
-
* is planned but deferred to avoid cascading changes across all
|
|
519
|
+
* `Result.err()` at their level. A future safe variant returning `Result`
|
|
520
|
+
* is planned but deferred to avoid cascading changes across all adapters.
|
|
327
521
|
*/
|
|
328
|
-
|
|
522
|
+
upsert(input: UpsertOf<TTable>): Promise<EntityOf<TTable>>;
|
|
329
523
|
/**
|
|
330
|
-
* Remove an entity by
|
|
524
|
+
* Remove an entity by identity. Returns `{ deleted: true }` when the
|
|
331
525
|
* row was found and removed, `{ deleted: false }` when no matching row
|
|
332
526
|
* existed (not an error).
|
|
333
527
|
*/
|
|
334
528
|
remove(id: StoreIdentifierOf<TTable>): Promise<{ readonly deleted: boolean }>;
|
|
529
|
+
}
|
|
530
|
+
|
|
531
|
+
// ---------------------------------------------------------------------------
|
|
532
|
+
// Compile-time assertion: StoreAccessor satisfies the core accessor protocol.
|
|
533
|
+
//
|
|
534
|
+
// `@ontrails/core/store` declares a structural protocol that `deriveTrail()`
|
|
535
|
+
// uses to synthesize default blazes without importing `@ontrails/store`. We
|
|
536
|
+
// pin the relationship here rather than in core so that any drift between
|
|
537
|
+
// the two shapes fails the store build immediately. If this check fails, the
|
|
538
|
+
// protocol in core has diverged from the store accessor contract — fix the
|
|
539
|
+
// protocol shape, not this assertion.
|
|
540
|
+
// ---------------------------------------------------------------------------
|
|
541
|
+
|
|
542
|
+
type AssertExtends<TActual, TExpected> = TActual extends TExpected
|
|
543
|
+
? true
|
|
544
|
+
: never;
|
|
545
|
+
|
|
546
|
+
/**
|
|
547
|
+
* Pins the structural relationship between `StoreAccessor` and the core
|
|
548
|
+
* `StoreAccessorProtocol`. If this check resolves to `never`, the protocol in
|
|
549
|
+
* core has drifted from the store accessor contract — fix the protocol shape,
|
|
550
|
+
* not this assertion.
|
|
551
|
+
*/
|
|
552
|
+
const storeAccessorProtocolCheck: AssertExtends<
|
|
553
|
+
StoreAccessor<AnyStoreTable>,
|
|
554
|
+
StoreAccessorProtocol<
|
|
555
|
+
UpsertOf<AnyStoreTable>,
|
|
556
|
+
EntityOf<AnyStoreTable>,
|
|
557
|
+
StoreIdentifierOf<AnyStoreTable>,
|
|
558
|
+
FiltersOf<AnyStoreTable>
|
|
559
|
+
>
|
|
560
|
+
> = true;
|
|
561
|
+
|
|
562
|
+
void storeAccessorProtocolCheck;
|
|
563
|
+
|
|
564
|
+
/**
|
|
565
|
+
* Tabular writable operations layered on top of the backend-agnostic
|
|
566
|
+
* contract.
|
|
567
|
+
*/
|
|
568
|
+
export interface StoreTableAccessor<
|
|
569
|
+
TTable extends AnyStoreTable,
|
|
570
|
+
> extends StoreAccessor<TTable> {
|
|
571
|
+
/**
|
|
572
|
+
* Insert a new entity.
|
|
573
|
+
*
|
|
574
|
+
* Tabular adapters can expose this convenience when the backend has a
|
|
575
|
+
* native distinction between create and update.
|
|
576
|
+
*/
|
|
577
|
+
insert(input: InsertOf<TTable>): Promise<EntityOf<TTable>>;
|
|
335
578
|
/**
|
|
336
|
-
* Patch an entity by
|
|
579
|
+
* Patch an entity by identity with partial fields. Returns the updated
|
|
337
580
|
* entity, or `null` when no row with that ID exists.
|
|
581
|
+
*
|
|
582
|
+
* @remarks
|
|
583
|
+
* On versioned tables, `update` does **not** participate in optimistic
|
|
584
|
+
* concurrency control. The `UpdateOf<TTable>` shape is derived by omitting
|
|
585
|
+
* generated fields — including the framework-managed `version` column — so
|
|
586
|
+
* any `version` value is dropped before reaching the adapter and the
|
|
587
|
+
* adapter always auto-increments without comparing. Callers that need
|
|
588
|
+
* lost-update protection must use {@link StoreAccessor.upsert | `upsert`}
|
|
589
|
+
* instead and pass the expected `version` in the payload.
|
|
338
590
|
*/
|
|
339
591
|
update(
|
|
340
592
|
id: StoreIdentifierOf<TTable>,
|
|
@@ -352,10 +604,51 @@ export type ReadOnlyStoreConnection<TStore extends AnyStoreDefinition> = {
|
|
|
352
604
|
};
|
|
353
605
|
|
|
354
606
|
/**
|
|
355
|
-
*
|
|
607
|
+
* Backend-agnostic connection shape exposed by a writable bound store.
|
|
356
608
|
*/
|
|
357
609
|
export type StoreConnection<TStore extends AnyStoreDefinition> = {
|
|
610
|
+
readonly [TName in keyof TStore['tables']]: StoreAccessor<
|
|
611
|
+
TStore['tables'][TName]
|
|
612
|
+
>;
|
|
613
|
+
};
|
|
614
|
+
|
|
615
|
+
/**
|
|
616
|
+
* Tabular connection shape exposed by adapters that distinguish insert and
|
|
617
|
+
* patch operations from the generalized `upsert` contract.
|
|
618
|
+
*/
|
|
619
|
+
export type StoreTableConnection<TStore extends AnyStoreDefinition> = {
|
|
358
620
|
readonly [TName in keyof TStore['tables']]: StoreTableAccessor<
|
|
359
621
|
TStore['tables'][TName]
|
|
360
622
|
>;
|
|
361
623
|
};
|
|
624
|
+
|
|
625
|
+
/**
|
|
626
|
+
* Optional fixture overrides used when building a mock store connection.
|
|
627
|
+
*
|
|
628
|
+
* The shape is a partial map keyed by table name; each entry is a list of
|
|
629
|
+
* fixture inputs validated against the table's fixture schema. Adapters
|
|
630
|
+
* share this type so every store backend seeds mocks the same way.
|
|
631
|
+
*/
|
|
632
|
+
export type StoreMockSeed<TDef extends AnyStoreDefinition> = Partial<{
|
|
633
|
+
readonly [TName in keyof TDef['tables']]: readonly FixtureInputOf<
|
|
634
|
+
TDef['tables'][TName]
|
|
635
|
+
>[];
|
|
636
|
+
}>;
|
|
637
|
+
|
|
638
|
+
/**
|
|
639
|
+
* Shared adapter options every store backend accepts.
|
|
640
|
+
*
|
|
641
|
+
* Concrete adapters extend this shape with their backend-specific fields
|
|
642
|
+
* (e.g. `url`, `dir`). Aligning the authored surface here lets the framework
|
|
643
|
+
* reason about adapter options uniformly — one shape, many projections.
|
|
644
|
+
*/
|
|
645
|
+
export interface StoreAdapterOptions<TDef extends AnyStoreDefinition> {
|
|
646
|
+
/** Optional resource id override. Defaults to `"store"`. */
|
|
647
|
+
readonly id?: string;
|
|
648
|
+
/** Human-readable description surfaced on the resource definition. */
|
|
649
|
+
readonly description?: string;
|
|
650
|
+
/** Free-form metadata for downstream tooling and governance. */
|
|
651
|
+
readonly meta?: Record<string, unknown>;
|
|
652
|
+
/** Optional per-table fixture overrides used by the mock resource factory. */
|
|
653
|
+
readonly mockSeed?: StoreMockSeed<TDef>;
|
|
654
|
+
}
|
|
@@ -1,38 +0,0 @@
|
|
|
1
|
-
---
|
|
2
|
-
created: 2026-04-04T03:09:15.424Z
|
|
3
|
-
type: handoff
|
|
4
|
-
session: 9e85a104-864f-4273-b68d-a4a95eb2d5fc
|
|
5
|
-
---
|
|
6
|
-
|
|
7
|
-
# Handoff 2026-04-04 23:09
|
|
8
|
-
|
|
9
|
-
> Session `9e85a104` — PR feedback loop on 14-branch Graphite stack (#64-#77)
|
|
10
|
-
|
|
11
|
-
## Done
|
|
12
|
-
|
|
13
|
-
- **Round 1**: Fixed 11 P1s across the stack — `persistEstablishedTopoSave` throw→Result, hash pipeline unification in drift.ts, store type safety (nullable defaults, PK leak, InsertOf/UpdateOf precision), drizzle error mapping, legacy tracker DB cleanup, transitive cache TSDoc, duplicate const before compile error
|
|
14
|
-
- **Round 2**: Fixed absorb regressions (variable reference splits across branches), plus new P1s — Commander undefined opts, toTrackStore close() no-op, verifyCurrentTopo hash pipeline, duplicate verifyCurrentTopo removal
|
|
15
|
-
- **Round 3**: Fixed remaining P1s — ADR depends_on slug→integer normalization, draft-promote preflight rename validation (duplicates + existing targets), NOT NULL on PK columns in generated SQL, InternalError passthrough, warden README method names (.trailhead→.blaze), vocabulary verb fix
|
|
16
|
-
- **Thread resolution**: 128 threads resolved across 3 rounds, re-reviews requested each time
|
|
17
|
-
- **Key learning**: `gt absorb` splits changes across branches unpredictably when a function definition and its callers span different branches — always walk up and typecheck each branch after absorb
|
|
18
|
-
|
|
19
|
-
## State
|
|
20
|
-
|
|
21
|
-
- On branch `trl-129-topo-and-dev-surfaces` (#69) with an uncommitted fix (drift.ts resolveTrailsDir import + readTrailheadLock dir fix) — lint failed, needs investigation
|
|
22
|
-
- 4 new P1s surfaced in Round 4 (from `-p 2` run which includes P0-P2):
|
|
23
|
-
- **#69**: drift.ts passes workspace root instead of `.trails/` to readTrailheadLock (fix attempted, lint error pending)
|
|
24
|
-
- **#71**: v2→v3 migration gap in topo-saves.ts — crash on existing schema version 2 databases
|
|
25
|
-
- **#72**: createMockTopoStore returns data without seeded saves (diverges from real store)
|
|
26
|
-
- **#77**: Three broken API examples in warden README (wrong signatures, invalid lefthook key)
|
|
27
|
-
- P2s not yet addressed across #65, #68, #70, #72, #75
|
|
28
|
-
|
|
29
|
-
## Next
|
|
30
|
-
|
|
31
|
-
- [ ] Debug lint failure on #69 drift.ts fix and commit
|
|
32
|
-
- [ ] Fix #71: Add v2→v3 migration guard in `ensureTopoHistorySchema` to create `topo_schemas`/`topo_exports` tables
|
|
33
|
-
- [ ] Fix #72: Add empty-save guard to `createMockTopoStore` `trails.list()`/`trails.get()`
|
|
34
|
-
- [ ] Fix #77: Correct `runWarden` and `checkDrift` signatures + lefthook key in warden README
|
|
35
|
-
- [ ] Walk up all branches verifying typecheck after each fix
|
|
36
|
-
- [ ] Submit stack, resolve threads, write loop record
|
|
37
|
-
- [ ] Then address P2s: gitignore duplicate-append (#68), stale TSDoc (#70), test harness fallback (#65), readonlyStore mock factory (#75)
|
|
38
|
-
- [ ] Consider: the `isDraftMarkedFile` vs `stripDraftFileMarkers` inconsistency on #67 is still open (P2)
|
package/.turbo/turbo-build.log
DELETED
|
@@ -1 +0,0 @@
|
|
|
1
|
-
$ tsc -b
|