@abloatai/transaction 0.51.0 → 0.53.0

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 (216) hide show
  1. package/dist/ablo.d.ts +1 -1
  2. package/dist/ablo.js +1 -1
  3. package/dist/ai-sdk/updateTool.d.ts +1 -1
  4. package/dist/ai-sdk/updateTool.js +1 -1
  5. package/dist/auth/apiKey.d.ts +0 -6
  6. package/dist/auth/apiKey.d.ts.map +1 -1
  7. package/dist/auth/apiKey.js +13 -60
  8. package/dist/auth/apiKey.js.map +1 -1
  9. package/dist/auth/baseUrl.d.ts +38 -0
  10. package/dist/auth/baseUrl.d.ts.map +1 -0
  11. package/dist/auth/baseUrl.js +126 -0
  12. package/dist/auth/baseUrl.js.map +1 -0
  13. package/dist/auth/capability.d.ts +2 -2
  14. package/dist/auth/capability.js +1 -1
  15. package/dist/auth/index.d.ts +15 -2
  16. package/dist/auth/index.d.ts.map +1 -1
  17. package/dist/auth/index.js +35 -21
  18. package/dist/auth/index.js.map +1 -1
  19. package/dist/auth/schemas.d.ts +24 -1
  20. package/dist/auth/schemas.d.ts.map +1 -1
  21. package/dist/auth/schemas.js +8 -2
  22. package/dist/auth/schemas.js.map +1 -1
  23. package/dist/auth/sessionIssuanceProtocol.d.ts +41 -0
  24. package/dist/auth/sessionIssuanceProtocol.d.ts.map +1 -0
  25. package/dist/auth/sessionIssuanceProtocol.js +48 -0
  26. package/dist/auth/sessionIssuanceProtocol.js.map +1 -0
  27. package/dist/auth/sessionMint.js +2 -2
  28. package/dist/branches.d.ts +34 -33
  29. package/dist/branches.d.ts.map +1 -1
  30. package/dist/branches.js +2 -4
  31. package/dist/branches.js.map +1 -1
  32. package/dist/coordination/schema.d.ts +3 -2
  33. package/dist/coordination/schema.d.ts.map +1 -1
  34. package/dist/coordination/schema.js +7 -2
  35. package/dist/coordination/schema.js.map +1 -1
  36. package/dist/errorCodes.d.ts +7 -3
  37. package/dist/errorCodes.d.ts.map +1 -1
  38. package/dist/errorCodes.js +8 -4
  39. package/dist/errorCodes.js.map +1 -1
  40. package/dist/errors.d.ts +1 -1
  41. package/dist/errors.js +1 -1
  42. package/dist/index.d.ts +2 -0
  43. package/dist/index.d.ts.map +1 -1
  44. package/dist/index.js.map +1 -1
  45. package/dist/resources/httpResources.d.ts +35 -2
  46. package/dist/resources/httpResources.d.ts.map +1 -1
  47. package/dist/resources/httpResources.js +10 -1
  48. package/dist/resources/httpResources.js.map +1 -1
  49. package/dist/resources/modelOperations.d.ts +18 -6
  50. package/dist/resources/modelOperations.d.ts.map +1 -1
  51. package/dist/resources/modelOperations.js.map +1 -1
  52. package/dist/resources/where.d.ts +67 -2
  53. package/dist/resources/where.d.ts.map +1 -1
  54. package/dist/resources/where.js +73 -0
  55. package/dist/resources/where.js.map +1 -1
  56. package/dist/schema/ddl.d.ts.map +1 -1
  57. package/dist/schema/ddl.js +9 -12
  58. package/dist/schema/ddl.js.map +1 -1
  59. package/dist/schema/field.d.ts +3 -3
  60. package/dist/schema/field.js +2 -2
  61. package/dist/schema/fieldRef.d.ts +1 -1
  62. package/dist/schema/generate.d.ts +2 -3
  63. package/dist/schema/generate.d.ts.map +1 -1
  64. package/dist/schema/generate.js +2 -8
  65. package/dist/schema/generate.js.map +1 -1
  66. package/dist/schema/index.d.ts +2 -2
  67. package/dist/schema/index.js +3 -3
  68. package/dist/schema/model.d.ts +7 -7
  69. package/dist/schema/model.js +2 -2
  70. package/dist/schema/openapi.d.ts.map +1 -1
  71. package/dist/schema/openapi.js +63 -21
  72. package/dist/schema/openapi.js.map +1 -1
  73. package/dist/schema/queries.d.ts +1 -1
  74. package/dist/schema/queries.js +1 -1
  75. package/dist/schema/relation.d.ts +8 -8
  76. package/dist/schema/relation.js +5 -5
  77. package/dist/schema/schema.d.ts +24 -37
  78. package/dist/schema/schema.d.ts.map +1 -1
  79. package/dist/schema/schema.js +15 -28
  80. package/dist/schema/schema.js.map +1 -1
  81. package/dist/schema/sugar.d.ts +7 -7
  82. package/dist/schema/sugar.js +5 -5
  83. package/dist/server/adapter.d.ts +2 -0
  84. package/dist/server/adapter.d.ts.map +1 -1
  85. package/dist/server/commit.d.ts +5 -1
  86. package/dist/server/commit.d.ts.map +1 -1
  87. package/dist/server/index.d.ts +3 -2
  88. package/dist/server/index.d.ts.map +1 -1
  89. package/dist/server/index.js.map +1 -1
  90. package/dist/server/readConfig.d.ts +109 -57
  91. package/dist/server/readConfig.d.ts.map +1 -1
  92. package/dist/server/readConfig.js +19 -5
  93. package/dist/server/readConfig.js.map +1 -1
  94. package/dist/source/adapters/drizzle.d.ts.map +1 -1
  95. package/dist/source/adapters/drizzle.js +5 -0
  96. package/dist/source/adapters/drizzle.js.map +1 -1
  97. package/dist/source/adapters/kysely.d.ts.map +1 -1
  98. package/dist/source/adapters/kysely.js +33 -8
  99. package/dist/source/adapters/kysely.js.map +1 -1
  100. package/dist/source/adapters/kyselyMutationCore.d.ts +1 -1
  101. package/dist/source/adapters/kyselyMutationCore.d.ts.map +1 -1
  102. package/dist/source/adapters/kyselyMutationCore.js +58 -20
  103. package/dist/source/adapters/kyselyMutationCore.js.map +1 -1
  104. package/dist/source/adapters/memory.d.ts.map +1 -1
  105. package/dist/source/adapters/memory.js +5 -0
  106. package/dist/source/adapters/memory.js.map +1 -1
  107. package/dist/source/adapters/prisma.d.ts.map +1 -1
  108. package/dist/source/adapters/prisma.js +6 -1
  109. package/dist/source/adapters/prisma.js.map +1 -1
  110. package/dist/source/conformance.js +21 -21
  111. package/dist/source/contract.d.ts +17 -0
  112. package/dist/source/contract.d.ts.map +1 -1
  113. package/dist/source/contract.js +31 -0
  114. package/dist/source/contract.js.map +1 -1
  115. package/dist/source/index.d.ts +1 -1
  116. package/dist/source/index.d.ts.map +1 -1
  117. package/dist/source/index.js +1 -1
  118. package/dist/source/index.js.map +1 -1
  119. package/dist/source/types.d.ts +1 -0
  120. package/dist/source/types.d.ts.map +1 -1
  121. package/dist/source/types.js.map +1 -1
  122. package/dist/transactionLayer.d.ts +3 -3
  123. package/dist/transactionLayer.js +3 -3
  124. package/dist/transport/httpClient.d.ts +13 -4
  125. package/dist/transport/httpClient.d.ts.map +1 -1
  126. package/dist/transport/httpClient.js +4 -2
  127. package/dist/transport/httpClient.js.map +1 -1
  128. package/dist/transport/httpTransport.d.ts +1 -0
  129. package/dist/transport/httpTransport.d.ts.map +1 -1
  130. package/dist/transport/httpTransport.js +79 -11
  131. package/dist/transport/httpTransport.js.map +1 -1
  132. package/dist/transport/wsFrameHandlers.d.ts.map +1 -1
  133. package/dist/transport/wsFrameHandlers.js +3 -0
  134. package/dist/transport/wsFrameHandlers.js.map +1 -1
  135. package/dist/types/streams.d.ts +2 -2
  136. package/dist/wire/accountResponses.d.ts +18 -13
  137. package/dist/wire/accountResponses.d.ts.map +1 -1
  138. package/dist/wire/accountResponses.js +5 -11
  139. package/dist/wire/accountResponses.js.map +1 -1
  140. package/dist/wire/claims.d.ts +1 -0
  141. package/dist/wire/claims.d.ts.map +1 -1
  142. package/dist/wire/claims.js +7 -1
  143. package/dist/wire/claims.js.map +1 -1
  144. package/dist/wire/commit.d.ts +144 -0
  145. package/dist/wire/commit.d.ts.map +1 -1
  146. package/dist/wire/commit.js +22 -1
  147. package/dist/wire/commit.js.map +1 -1
  148. package/dist/wire/frames.d.ts +4 -0
  149. package/dist/wire/frames.d.ts.map +1 -1
  150. package/dist/wire/inboundFrames.d.ts +22 -0
  151. package/dist/wire/inboundFrames.d.ts.map +1 -1
  152. package/dist/wire/index.d.ts +3 -3
  153. package/dist/wire/index.d.ts.map +1 -1
  154. package/dist/wire/index.js +2 -2
  155. package/dist/wire/index.js.map +1 -1
  156. package/dist/wire/listEnvelope.d.ts +20 -0
  157. package/dist/wire/listEnvelope.d.ts.map +1 -1
  158. package/dist/wire/listEnvelope.js +20 -0
  159. package/dist/wire/listEnvelope.js.map +1 -1
  160. package/dist/wire/protocol.d.ts +1 -1
  161. package/dist/wire/protocol.js +1 -1
  162. package/package.json +1 -1
  163. package/src/ablo.ts +1 -1
  164. package/src/ai-sdk/updateTool.ts +1 -1
  165. package/src/auth/apiKey.ts +13 -61
  166. package/src/auth/baseUrl.ts +144 -0
  167. package/src/auth/capability.ts +3 -3
  168. package/src/auth/index.ts +62 -21
  169. package/src/auth/schemas.ts +16 -2
  170. package/src/auth/sessionIssuanceProtocol.ts +57 -0
  171. package/src/auth/sessionMint.ts +2 -2
  172. package/src/branches.ts +2 -4
  173. package/src/coordination/schema.ts +7 -2
  174. package/src/errorCodes.ts +29 -5
  175. package/src/errors.ts +1 -1
  176. package/src/index.ts +2 -0
  177. package/src/resources/httpResources.ts +45 -2
  178. package/src/resources/modelOperations.ts +18 -6
  179. package/src/resources/where.ts +70 -4
  180. package/src/schema/ddl.ts +9 -13
  181. package/src/schema/field.ts +3 -3
  182. package/src/schema/fieldRef.ts +1 -1
  183. package/src/schema/generate.ts +2 -8
  184. package/src/schema/index.ts +3 -3
  185. package/src/schema/model.ts +7 -7
  186. package/src/schema/openapi.ts +64 -21
  187. package/src/schema/queries.ts +1 -1
  188. package/src/schema/relation.ts +8 -8
  189. package/src/schema/residency.ts +0 -1
  190. package/src/schema/schema.ts +33 -58
  191. package/src/schema/sugar.ts +7 -7
  192. package/src/server/adapter.ts +2 -0
  193. package/src/server/commit.ts +5 -1
  194. package/src/server/index.ts +8 -2
  195. package/src/server/readConfig.ts +111 -57
  196. package/src/source/adapters/drizzle.ts +5 -0
  197. package/src/source/adapters/kysely.ts +48 -10
  198. package/src/source/adapters/kyselyMutationCore.ts +69 -22
  199. package/src/source/adapters/memory.ts +5 -0
  200. package/src/source/adapters/prisma.ts +6 -1
  201. package/src/source/conformance.ts +21 -21
  202. package/src/source/contract.ts +34 -0
  203. package/src/source/index.ts +2 -0
  204. package/src/source/types.ts +1 -0
  205. package/src/transactionLayer.ts +3 -3
  206. package/src/transport/httpClient.ts +23 -6
  207. package/src/transport/httpTransport.ts +88 -11
  208. package/src/transport/wsFrameHandlers.ts +3 -0
  209. package/src/types/global.ts +1 -1
  210. package/src/types/streams.ts +2 -2
  211. package/src/wire/accountResponses.ts +5 -11
  212. package/src/wire/claims.ts +7 -1
  213. package/src/wire/commit.ts +31 -1
  214. package/src/wire/index.ts +10 -1
  215. package/src/wire/listEnvelope.ts +23 -0
  216. package/src/wire/protocol.ts +1 -1
@@ -8,20 +8,20 @@
8
8
  * import { defineSchema, model, relation } from '@abloatai/transaction/schema';
9
9
  *
10
10
  * const schema = defineSchema({
11
- * tasks: model({
11
+ * items: model({
12
12
  * title: z.string(),
13
13
  * status: z.enum(['todo', 'doing', 'done']).default('todo'),
14
- * projectId: z.string().optional(),
14
+ * workspaceId: z.string().optional(),
15
15
  * }, {
16
- * project: relation.belongsTo('projects', 'projectId'),
16
+ * workspace: relation.belongsTo('workspaces', 'workspaceId'),
17
17
  * }),
18
18
  * });
19
19
  *
20
- * type Task = InferModel<typeof schema, 'tasks'>;
20
+ * type Item = InferModel<typeof schema, 'items'>;
21
21
  */
22
22
 
23
23
  import { z } from 'zod';
24
- import type { ModelDef, RelationRecord } from './model.js';
24
+ import type { ComputedRecord, ModelDef, RelationRecord } from './model.js';
25
25
  import type { RelationDef } from './relation.js';
26
26
  import { AbloValidationError } from '../errors.js';
27
27
  import type { IdentityRole } from './roles.js';
@@ -210,7 +210,7 @@ export interface DefineSchemaOptions {
210
210
  export type SchemaRecord = Record<string, ModelDef>;
211
211
 
212
212
  /**
213
- * Base fields every synced model gets automatically.
213
+ * The logical identity every model gets automatically.
214
214
  *
215
215
  * Exported (internal) so `parseSchema` can rebuild a model's validator the
216
216
  * same way `defineSchema` does — `baseFieldsSchema.merge(modelSchema)` — when
@@ -218,26 +218,14 @@ export type SchemaRecord = Record<string, ModelDef>;
218
218
  */
219
219
  export const baseFieldsSchema = z.object({
220
220
  id: z.string(),
221
- createdAt: z.date(),
222
- updatedAt: z.date(),
223
- organizationId: z.string().optional(),
224
- createdBy: z.string().optional(),
225
221
  });
226
222
 
227
223
  /**
228
- * The base-column names every model carries automatically the keys of
229
- * {@link baseFieldsSchema}, kept here as the single source of truth. Code
230
- * generation reads it to avoid emitting a base column twice, and
231
- * {@link defineSchema} uses it to reject a model that redeclares one, since
232
- * merging the user's field over the base field would produce a `string & Date`
233
- * type and break the build.
224
+ * The universal model field names. Audit and application metadata is declared
225
+ * by the application or carried by the commit record, never injected here.
234
226
  */
235
227
  export const BASE_FIELDS = [
236
228
  'id',
237
- 'createdAt',
238
- 'updatedAt',
239
- 'organizationId',
240
- 'createdBy',
241
229
  ] as const;
242
230
 
243
231
  /** The base fields type — pure data columns. */
@@ -262,15 +250,15 @@ export interface Schema<S extends SchemaRecord = SchemaRecord> {
262
250
 
263
251
  /**
264
252
  * Every declared field as a {@link FieldRef} — the field as a value rather
265
- * than a quoted name: `schema.fields.tasks.status`.
253
+ * than a quoted name: `schema.fields.items.status`.
266
254
  *
267
255
  * A surface that names a field takes one of these instead of a string, so a
268
256
  * name that does not exist stops compiling and a rename is a compile error at
269
257
  * every use. Claims are the first caller; anything else that has to say
270
258
  * "which field" should take a reference for the same reason.
271
259
  *
272
- * Base fields (`id`, `createdAt`, …) are not here: the schema does not
273
- * declare them, and a caller naming one means the row, not a part of it.
260
+ * Automatically supplied `id` is not here. Every application-declared field
261
+ * does appear here.
274
262
  */
275
263
  readonly fields: {
276
264
  readonly [K in keyof S & string]: S[K] extends ModelDef<infer Shape>
@@ -281,7 +269,7 @@ export interface Schema<S extends SchemaRecord = SchemaRecord> {
281
269
  : never;
282
270
  };
283
271
 
284
- /** Zod schemas with base fields merged in */
272
+ /** Zod schemas with logical identity merged into each declared model shape. */
285
273
  readonly validators: {
286
274
  readonly [K in keyof S]: S[K] extends ModelDef<infer Shape>
287
275
  ? z.ZodObject<Shape & typeof baseFieldsSchema.shape>
@@ -339,7 +327,7 @@ export interface Schema<S extends SchemaRecord = SchemaRecord> {
339
327
  * Includes base fields (id, createdAt, updatedAt, etc.)
340
328
  *
341
329
  * ```ts
342
- * type Task = InferModel<typeof schema, 'tasks'>;
330
+ * type Item = InferModel<typeof schema, 'items'>;
343
331
  * ```
344
332
  */
345
333
  /** The schema bound via `declare module … interface Register { Schema: … }`
@@ -351,15 +339,15 @@ type RegisteredSchema = import('../types/global.js').Register extends {
351
339
  : never;
352
340
 
353
341
  /**
354
- * The primary model-type helper. Once your project's `ablo/register.ts`
342
+ * The primary model-type helper. Once your workspace's `ablo/register.ts`
355
343
  * registers the schema, a single argument is all it takes:
356
344
  *
357
345
  * ```ts
358
- * type Task = Model<'tasks'>;
346
+ * type Item = Model<'items'>;
359
347
  * ```
360
348
  *
361
349
  * Without that registration, or for a second schema, pass the schema
362
- * explicitly: `Model<typeof schema, 'tasks'>`.
350
+ * explicitly: `Model<typeof schema, 'items'>`.
363
351
  */
364
352
  export type Model<A, B = never> = [B] extends [never]
365
353
  ? A extends keyof RegisteredSchema['models']
@@ -371,21 +359,14 @@ export type Model<A, B = never> = [B] extends [never]
371
359
 
372
360
  /**
373
361
  * The row type {@link Model} resolves to. Internal: `Model<typeof schema,
374
- * 'tasks'>` is the published spelling, because it reads as the domain rather
362
+ * 'items'>` is the published spelling, because it reads as the domain rather
375
363
  * than the machinery. This one is no longer exported from any subpath — it
376
364
  * stays because `Model` is defined in terms of it, not as a second name for
377
365
  * the same idea.
378
366
  */
379
367
  export type InferModel<S extends Schema, ModelName extends keyof S['models']> =
380
368
  S['models'][ModelName] extends ModelDef<infer Shape, infer R, infer C>
381
- ? // `Omit<…, keyof BaseModelFields>` so a model that (wrongly) redeclares a
382
- // reserved field degrades to "framework field wins" (e.g. `createdAt: Date`)
383
- // instead of intersecting to `never` (`string & Date`, which then surfaces
384
- // as a baffling "missing field" error three layers away). defineSchema also
385
- // throws on such a redeclaration at runtime (code `schema_reserved_field`);
386
- // this is the type-level belt to that runtime suspenders. No-op for correct
387
- // schemas — they never carry a base-field key, so nothing is omitted.
388
- Omit<z.infer<z.ZodObject<Shape>>, keyof BaseModelFields>
369
+ ? Omit<z.infer<z.ZodObject<Shape>>, 'id'>
389
370
  & BaseModelFields
390
371
  & BaseModelMethods
391
372
  & InferComputed<C>
@@ -452,14 +433,13 @@ export type InferRelations<S extends Schema, R extends RelationRecord> =
452
433
  */
453
434
  export type InferRow<S extends Schema, ModelName extends keyof S['models']> =
454
435
  S['models'][ModelName] extends ModelDef<infer Shape, RelationRecord, infer C>
455
- ? // Same reserved-field guard as InferModel — see the comment there.
456
- Omit<z.infer<z.ZodObject<Shape>>, keyof BaseModelFields>
436
+ ? Omit<z.infer<z.ZodObject<Shape>>, 'id'>
457
437
  & BaseModelFields
458
438
  & InferComputed<C>
459
439
  : never;
460
440
 
461
441
  /**
462
- * The reactive-row companion to {@link Model}. Once your project's
442
+ * The reactive-row companion to {@link Model}. Once your workspace's
463
443
  * `ablo/register.ts` registers the schema, a single argument is all it takes:
464
444
  *
465
445
  * ```ts
@@ -500,26 +480,22 @@ export type InferComputed<C> =
500
480
  : { readonly [K in keyof C]: C[K] extends (...args: any[]) => infer R ? R : never };
501
481
 
502
482
  /**
503
- * Infer the create input type. Only schema-defined fields are accepted
504
- * base fields (id, createdAt, updatedAt) are auto-generated by the SDK
505
- * and cannot be passed by the consumer.
483
+ * Infer the create input type. Application fields retain their declared input
484
+ * requirements and types.
506
485
  *
507
486
  * The only exception is `id`: consumers can optionally provide one for
508
487
  * client-generated IDs (useful for optimistic UI that needs to reference
509
488
  * the entity before the server confirms).
510
489
  *
511
490
  * ```ts
512
- * type CreateTask = InferCreate<typeof schema, 'tasks'>;
491
+ * type CreateItem = InferCreate<typeof schema, 'items'>;
513
492
  * // { title: string; status?: 'todo' | 'doing' | 'done'; id?: string }
514
- * // createdAt, updatedAt are NOT accepted — they're auto-generated
515
493
  * ```
516
494
  */
517
495
  export type InferCreate<S extends Schema, ModelName extends keyof S['models']> =
518
496
  S['models'][ModelName] extends ModelDef<infer Shape>
519
- ? // Same reserved-field guard as InferModel: drop any (wrongly) redeclared
520
- // base field so the input degrades to "framework field wins" rather than
521
- // collapsing to `never`. No-op for correct schemas.
522
- Omit<z.input<z.ZodObject<Shape>>, keyof BaseModelFields> & Partial<BaseModelFields>
497
+ ? Omit<z.input<z.ZodObject<Shape>>, 'id'>
498
+ & Partial<Pick<BaseModelFields, 'id'>>
523
499
  : never;
524
500
 
525
501
  /**
@@ -570,8 +546,8 @@ export interface DeleteId<S extends Schema, ModelName extends keyof S['models']>
570
546
  *
571
547
  * ```ts
572
548
  * const schema = defineSchema({
573
- * tasks: model({ title: z.string(), status: z.string().default('todo') }),
574
- * projects: model({ name: z.string() }),
549
+ * items: model({ title: z.string(), status: z.string().default('todo') }),
550
+ * workspaces: model({ name: z.string() }),
575
551
  * });
576
552
  * ```
577
553
  */
@@ -682,16 +658,15 @@ export function defineSchema<const S extends SchemaRecord>(
682
658
  // failure immediate and unambiguous.
683
659
  for (const fieldName of Object.keys(def.shape)) {
684
660
  assertRoundTrippableCamelCase(name, fieldName);
685
- // Reserved base columns are merged in below via `baseFieldsSchema.merge`,
686
- // and Zod `.merge` silently OVERWRITES the base field with the user's —
687
- // e.g. a model declaring `createdAt: z.string()` ends up with a field
688
- // typed `string & Date`, which breaks the build. Reject the collision at
689
- // definition time so the author sees an unambiguous error instead.
690
- if ((BASE_FIELDS as readonly string[]).includes(fieldName)) {
661
+ // Logical identity is the only universal model field. Application
662
+ // timestamps, tenancy columns and attribution columns are ordinary
663
+ // fields when an application declares them.
664
+ const reservedFields = ['id'];
665
+ if ((reservedFields as readonly string[]).includes(fieldName)) {
691
666
  throw new AbloValidationError(
692
667
  `[defineSchema] ${name}.${fieldName}: field \`${fieldName}\` collides with a ` +
693
668
  `reserved field that the SDK provides automatically ` +
694
- `(${BASE_FIELDS.join(', ')}). Remove it from your model — redeclaring it ` +
669
+ `(${reservedFields.join(', ')}). Remove it from your model — redeclaring it ` +
695
670
  `produces a \`string & Date\` type and breaks the build.`,
696
671
  { code: 'schema_reserved_field', param: `${name}.${fieldName}` },
697
672
  );
@@ -15,10 +15,10 @@
15
15
  *
16
16
  * @example
17
17
  * ```ts
18
- * tasks: mutable.lazy({ title: z.string() }, {
19
- * typename: 'Task', tableName: 'tasks',
18
+ * items: mutable.lazy({ title: z.string() }, {
19
+ * typename: 'Item', tableName: 'items',
20
20
  * relations: { ... },
21
- * computed: tasksComputed,
21
+ * computed: itemsComputed,
22
22
  * }),
23
23
  * ```
24
24
  */
@@ -47,13 +47,13 @@ export interface SugarOptions<
47
47
  /** Computed getters installed on the model class prototype. */
48
48
  computed?: C;
49
49
  /**
50
- * Wire `__typename` (PascalCase, e.g. `'Task'`). Defaults to the schema
50
+ * Wire `__typename` (PascalCase, e.g. `'Item'`). Defaults to the schema
51
51
  * key via `defineSchema` — override when the wire shape differs from
52
52
  * the camelCase schema key.
53
53
  */
54
54
  typename?: string;
55
55
  /**
56
- * The physical table name. Override it when the table name differs from the
56
+ * The database table name. Override it when the table name differs from the
57
57
  * snake_case of the typename — for example, a `Member` type stored in a table
58
58
  * named `'member'` rather than `'members'`.
59
59
  */
@@ -112,9 +112,9 @@ function build<
112
112
  * `{ mutable: true, load: X }` on `model()`.
113
113
  *
114
114
  * Pick the load suffix by data-access pattern:
115
- * - `.instant` — small, always-needed (Theme, Layout, StatusGroup)
115
+ * - `.instant` — small, always-needed control rows
116
116
  * - `.lazy` — large collections fetched on first query
117
- * (Block, Message, Task)
117
+ * (Block, Message, Item)
118
118
  */
119
119
  export const mutable = {
120
120
  instant: <
@@ -61,6 +61,8 @@ export type ReadResult =
61
61
  | {
62
62
  readonly kind: 'query';
63
63
  readonly rows: readonly Row[];
64
+ /** Opaque upstream cursor when a source-backed list has another page. */
65
+ readonly nextCursor?: string;
64
66
  };
65
67
 
66
68
  // ── sync ─────────────────────────────────────────────────────────────────────
@@ -16,6 +16,7 @@
16
16
  import type { ParticipantKind, ConfirmationState } from '../log/syncDeltaRow.js';
17
17
  import type { ParticipantRef } from '../wire/delta.js';
18
18
  import type { CommitExecutionResultInput } from '../wire/commit.js';
19
+ import type { CommitOperationResult } from '../wire/commit.js';
19
20
  import type { ReadDependency, TrackDependency } from '../coordination/schema.js';
20
21
  import type { EffectiveAuthority } from '../auth/capability.js';
21
22
 
@@ -99,4 +100,7 @@ export interface CommitContext {
99
100
  * lives with the HTTP/WS confirmation contract so queued correlation cannot drift
100
101
  * between cache, transport, and client.
101
102
  */
102
- export type CommitExecutionResult = CommitExecutionResultInput;
103
+ export type CommitExecutionResult = CommitExecutionResultInput & {
104
+ /** Response-time projection from the exact writing transaction. Never persisted. */
105
+ readonly operationResults?: readonly CommitOperationResult[];
106
+ };
@@ -4,7 +4,8 @@
4
4
  * vocabulary ({@link Row}, {@link ReadRequest}, {@link ChangeSet}, and the rest),
5
5
  * the {@link CommitContext} and {@link CommitExecutionResult} commit types, the
6
6
  * {@link StorageMode} enumeration, and the per-model read configuration
7
- * ({@link BootstrapModel}, {@link ColumnOverride}). These are plain, driver-free
7
+ * ({@link ReadModelShape} and the {@link BootstrapModel} descriptor built on it,
8
+ * plus {@link ColumnOverride} and {@link ParentScope}). These are plain, driver-free
8
9
  * types; you supply the database code that fulfills them.
9
10
  */
10
11
  export type {
@@ -27,4 +28,9 @@ export {
27
28
  type CommitRecord,
28
29
  } from '../wire/commit.js';
29
30
  export { storageModeSchema, type StorageMode } from './storageMode.js';
30
- export type { ColumnOverride, BootstrapModel } from './readConfig.js';
31
+ export type {
32
+ ColumnOverride,
33
+ ParentScope,
34
+ ReadModelShape,
35
+ BootstrapModel,
36
+ } from './readConfig.js';
@@ -1,9 +1,23 @@
1
1
  /**
2
- * Canonical per-model read configuration consumed when a client bootstraps. Each
3
- * {@link BootstrapModel} maps a model to its physical table, its tenancy column, and
4
- * any parent scoping — plain data with no database driver. It feeds the read side of
5
- * the data adapter contract; your query builder reads it to load a model's initial
6
- * rows.
2
+ * Canonical per-model read configuration how one model's rows are located,
3
+ * scoped to a tenant, and decoded back into the types the schema declared.
4
+ *
5
+ * {@link ReadModelShape} is the definition site for those facts. Every read seam
6
+ * needs the same set, and each one that restated the set drifted from the
7
+ * others: `json` decoding reached three of them, `number` decoding reached none,
8
+ * and the where-clause `boolean` list reached exactly one — so a `bigint`-backed
9
+ * field left one seam as a decimal string and its `int4` neighbour left the next
10
+ * as a number. Adding a declared type is now one edit here rather than a
11
+ * remembered sweep.
12
+ *
13
+ * The descriptors below carry that shape plus the facts only they need. They
14
+ * disagree on one thing, deliberately: the model's own name. A bootstrap
15
+ * descriptor spells it `name`, the server's read-model descriptor spells it
16
+ * `typename`. `name` is part of the published data-adapter contract, so the two
17
+ * spellings stay and the shape they share owns everything else.
18
+ *
19
+ * Plain data, no database driver — it feeds the read side of the data adapter
20
+ * contract, and your query builder reads it to load a model's initial rows.
7
21
  */
8
22
 
9
23
  /** A mapping from a declared field to a physical column, with the alias to apply after a `SELECT *`. */
@@ -13,70 +27,110 @@ export interface ColumnOverride {
13
27
  readonly alias: string;
14
28
  }
15
29
 
16
- /** Read configuration for one model: how to locate its rows and scope them to a tenant. */
17
- export interface BootstrapModel {
18
- name: string;
30
+ /**
31
+ * Parent-table tenancy scoping for rows that have no `organization_id` column
32
+ * of their own, mirroring the schema's `scopedVia` option. A read carrying this
33
+ * adds:
34
+ *
35
+ * WHERE <table>.<localKey> IN
36
+ * (SELECT <parentKey> FROM <parentTable> WHERE <parentOrgColumn> = $1)
37
+ *
38
+ * It applies on top of whatever `orgScoped` dictates, so a table can carry its
39
+ * own `organization_id` and still narrow through a parent. The common case is
40
+ * `orgScoped: false` together with this on a table that lacks the column —
41
+ * without it, such a table reads every tenant's rows.
42
+ */
43
+ export interface ParentScope {
44
+ readonly localKey: string;
45
+ readonly parentTable: string;
46
+ readonly parentKey?: string;
47
+ readonly parentOrgColumn?: string;
48
+ }
49
+
50
+ /**
51
+ * Everything a read seam needs to know about one model except what it is called.
52
+ *
53
+ * Split from the descriptors that carry it because the two of them name the
54
+ * model differently and agree on all of this. A seam that only decodes, or only
55
+ * compiles SQL, projects the members it uses with `Pick` rather than declaring
56
+ * its own near-copy.
57
+ */
58
+ export interface ReadModelShape {
19
59
  /**
20
60
  * Extra names accepted when a request looks up or filters this model. When the
21
- * physical table name is canonical, `name` stays that table name and the aliases
22
- * cover generated compatibility names such as `WeatherReports` or `weatherReports`.
61
+ * physical table name is canonical, the descriptor's own name stays that table
62
+ * name and the aliases cover generated compatibility names such as
63
+ * `WeatherReports` or `weatherReports`.
23
64
  */
24
- aliases?: readonly string[];
65
+ readonly aliases?: readonly string[];
25
66
  /**
26
- * The schema key used by source endpoints. `name` stays the wire and result model
27
- * name (usually the typename), while source handlers are keyed by the developer's
28
- * schema key, such as `files` or `blocks`.
67
+ * The schema key used by source endpoints. The descriptor's own name stays the
68
+ * wire and result model name (usually the typename), while source handlers are
69
+ * keyed by the developer's schema key, such as `files` or `blocks`.
29
70
  */
30
- sourceModel?: string;
31
- table: string;
32
- syncGroups?: string[];
33
- enabled?: boolean;
34
- /** Max rows to return. Omit for unlimited. Maps to schema's bootstrapLimit. */
35
- limit?: number;
36
- /** SQL ORDER BY clause. Default: 'id'. Maps to schema's bootstrapOrderBy. */
37
- orderBy?: string;
71
+ readonly sourceModel?: string;
72
+ readonly table: string;
38
73
  /** Whether the table has organization_id. Default: true. */
39
- orgScoped?: boolean;
74
+ readonly orgScoped?: boolean;
40
75
  /**
41
76
  * The model derives its tenant from the connected data source (`policy:
42
- * { by: 'source' }`) rather than a row column. It carries no tenant predicate on a
43
- * log plane, so it is excluded from the bootstrap set entirely until the
44
- * write-through connect path can resolve the org from the source registration
45
- * serving it now would return every row to every tenant. Kept on the type as a
46
- * defense-in-depth marker so a source-scoped model that reaches `getBootstrapData`
47
- * fails closed rather than bootstrapping unscoped.
77
+ * { by: 'source' }`) rather than a row column the source registration is the
78
+ * tenant boundary. Distinct from `orgScoped: false` (a `none`/global model): a
79
+ * source-scoped model IS tenant-scoped, just not by a column. It carries no
80
+ * tenant predicate on a log plane, so every read and bootstrap site fails it
81
+ * closed with `source_tenancy_not_enforced` rather than treating it as global
82
+ * serving it now would return every row to every tenant. When the
83
+ * write-through connect path can resolve the org from the source registration,
84
+ * this same flag routes to that resolution.
48
85
  */
49
- sourceScoped?: boolean;
86
+ readonly sourceScoped?: boolean;
50
87
  /** Physical tenancy column (default `organization_id`, configurable per model). */
51
- orgColumn?: string;
52
- /**
53
- * Parent-table scoping for rows that have no `organization_id` column, mirroring
54
- * the schema's `scopedVia` option. When set, the bootstrap query adds:
55
- *
56
- * WHERE <table>.<localKey> IN
57
- * (SELECT <parentKey> FROM <parentTable> WHERE <parentOrgColumn> = $1)
58
- *
59
- * This applies on top of whatever `orgScoped` dictates, so a table can carry its
60
- * own `organization_id` and still narrow through a parent. The common case,
61
- * though, is `orgScoped: false` together with `scopedVia` on a table that lacks the
62
- * column.
63
- */
64
- scopedVia?: {
65
- localKey: string;
66
- parentTable: string;
67
- parentKey?: string;
68
- parentOrgColumn?: string;
69
- };
88
+ readonly orgColumn?: string;
89
+ /** Parent-table scoping for rows with no tenancy column of their own. */
90
+ readonly scopedVia?: ParentScope;
70
91
  /** Client-facing field name → physical DB column for declared fields. */
71
- fieldColumns?: Record<string, string>;
92
+ readonly fieldColumns?: Readonly<Record<string, string>>;
72
93
  /** Physical-column aliases needed after SELECT * for `.from(...)` fields. */
73
- columnOverrides?: readonly ColumnOverride[];
94
+ readonly columnOverrides?: readonly ColumnOverride[];
74
95
  /**
75
- * Physical columns the schema declares as JSON (via `field.json()`). A JSON field
76
- * stored in a text column comes back from `row_to_json` as a serialized string, so
77
- * the bootstrap reparses these columns to make the wire value the canonical object
78
- * regardless of the physical column type. A `jsonb` column already returns an
79
- * object, so reparsing it is a no-op.
96
+ * Physical columns the schema declares as JSON (via `field.json()`). A JSON
97
+ * field stored in a text column comes back serialized, so a read reparses these
98
+ * to make the wire value the canonical object regardless of the physical column
99
+ * type. A `jsonb` column already returns an object, so reparsing it is a no-op.
80
100
  */
81
- jsonColumns?: readonly string[];
101
+ readonly jsonColumns?: readonly string[];
102
+ /**
103
+ * Physical columns the schema declares as a boolean (via `field.boolean()`).
104
+ * Two seams need the list. A where-clause parameter is coerced to a real JS
105
+ * boolean before binding, because the driver's bool encoder is a strict
106
+ * `=== true` and a string `'t'` would silently bind as `'f'` — an INVERTED
107
+ * filter, not an error. A row read back from the log is coerced the same way,
108
+ * because a value echoed from a customer's database arrives as PG's text
109
+ * literal rather than as `true`.
110
+ */
111
+ readonly boolColumns?: readonly string[];
112
+ /**
113
+ * Physical columns the schema declares as a number (via `field.number()`).
114
+ * Postgres has four integer widths where JavaScript has one, so a column too
115
+ * wide to always fit a JS number comes back as decimal text. A read decodes
116
+ * these so a `bigint`-backed field and an `int4`-backed one reach the client as
117
+ * the same JS type.
118
+ */
119
+ readonly numberColumns?: readonly string[];
120
+ }
121
+
122
+ /**
123
+ * Read configuration for one model at bootstrap: {@link ReadModelShape} plus the
124
+ * facts only the initial payload needs — which models are in it, how much of
125
+ * each, and in what order.
126
+ */
127
+ export interface BootstrapModel extends ReadModelShape {
128
+ /** The wire and result model name. `ModelMeta.typename` is the same fact. */
129
+ readonly name: string;
130
+ readonly syncGroups?: readonly string[];
131
+ readonly enabled?: boolean;
132
+ /** Max rows to return. Omit for unlimited. Maps to schema's bootstrapLimit. */
133
+ readonly limit?: number;
134
+ /** SQL ORDER BY clause. Default: 'id'. Maps to schema's bootstrapOrderBy. */
135
+ readonly orderBy?: string;
82
136
  }
@@ -151,6 +151,11 @@ export function drizzleDataSource<S extends SchemaRecord>(
151
151
  };
152
152
 
153
153
  const applyOperation = async (tx: DrizzleLike, op: Operation): Promise<Row> => {
154
+ if (op.where) {
155
+ throw new AbloValidationError('The Drizzle adapter does not support conditional operations', {
156
+ code: 'source_adapter_misconfigured',
157
+ });
158
+ }
154
159
  const mc = modelColumns(op.model);
155
160
  const table = sql.identifier(mc.table);
156
161
  const id = rowId(op);
@@ -32,6 +32,8 @@ import {
32
32
  changeSetSchema,
33
33
  outboxEventSchema,
34
34
  sourceCommitEchoMarkerSchema,
35
+ sourceCommitEchoIntentSchema,
36
+ type SourceCommitEchoIntent,
35
37
  } from '../contract.js';
36
38
  import {
37
39
  assertSourceIdempotencyIntent,
@@ -135,19 +137,17 @@ function markerAction(type: ChangeSet['operations'][number]['type']): 'I' | 'U'
135
137
  return 'U';
136
138
  }
137
139
 
138
- function assertDirectLogicalMarker(
140
+ function parseEchoIntent(
139
141
  change: ChangeSet,
140
142
  markerModelFor: (operationModel: string) => string,
141
- ): void {
143
+ required: boolean,
144
+ ): SourceCommitEchoIntent | undefined {
142
145
  const payload = change.echo?.payload;
143
146
  if (!payload) {
144
- throw new AbloValidationError(
145
- 'A direct Kysely mutation requires a transactional Postgres logical marker',
146
- { code: 'source_adapter_misconfigured' },
147
- );
147
+ return undefined;
148
148
  }
149
149
  try {
150
- const marker = sourceCommitEchoMarkerSchema.parse(JSON.parse(payload) as unknown);
150
+ const marker = sourceCommitEchoIntentSchema.parse(JSON.parse(payload) as unknown);
151
151
  if (marker.correlationId !== change.correlationId) {
152
152
  throw new Error('marker correlation does not match the ledger key');
153
153
  }
@@ -163,15 +163,18 @@ function assertDirectLogicalMarker(
163
163
  if (
164
164
  !markerOperation ||
165
165
  markerOperation.model !== markerModelFor(operation.model) ||
166
- markerOperation.id !== kyselyOperationRowId(operation) ||
166
+ (markerOperation.id != null && markerOperation.id !== kyselyOperationRowId(operation)) ||
167
+ (markerOperation.id == null && operation.type !== 'CREATE') ||
167
168
  markerOperation.action !== markerAction(operation.type) ||
168
169
  markerOperation.transactionId !== operation.transactionId
169
170
  ) {
170
171
  throw new Error(`marker operation ${index} does not match the mutation`);
171
172
  }
172
173
  }
174
+ return marker;
173
175
  } catch (error) {
174
176
  if (error instanceof AbloValidationError) throw error;
177
+ if (!required) return undefined;
175
178
  throw new AbloValidationError(
176
179
  `The direct Postgres logical marker is invalid: ${error instanceof Error ? error.message : 'unknown marker error'}`,
177
180
  { code: 'source_adapter_misconfigured' },
@@ -179,6 +182,32 @@ function assertDirectLogicalMarker(
179
182
  }
180
183
  }
181
184
 
185
+ function resolveEchoMarker(
186
+ intent: SourceCommitEchoIntent,
187
+ rows: readonly Row[],
188
+ ) {
189
+ return sourceCommitEchoMarkerSchema.parse({
190
+ ...intent,
191
+ operations: intent.operations.map((operation, index) => {
192
+ const returnedId = rows[index]?.id;
193
+ const id = operation.id ?? returnedId;
194
+ if (typeof id !== 'string' || id.length === 0) {
195
+ throw new AbloValidationError(
196
+ `source operation ${index} did not return a canonical id for WAL correlation`,
197
+ { code: 'source_adapter_misconfigured' },
198
+ );
199
+ }
200
+ if (returnedId != null && String(returnedId) !== id) {
201
+ throw new AbloValidationError(
202
+ `source operation ${index} returned an id that does not match its WAL correlation`,
203
+ { code: 'source_adapter_misconfigured' },
204
+ );
205
+ }
206
+ return { ...operation, id };
207
+ }),
208
+ });
209
+ }
210
+
182
211
  /**
183
212
  * Build the shared transaction policy around one Kysely mutation core. Exported
184
213
  * for engine integrations that need to inject a prebuilt core while retaining
@@ -226,7 +255,13 @@ export function createKyselyMutationAdapter(
226
255
 
227
256
  async commit(change: ChangeSet): Promise<AdapterCommitResult> {
228
257
  const request = changeSetSchema.parse(change);
229
- if (mode === 'direct') assertDirectLogicalMarker(request, markerModelFor);
258
+ if (mode === 'direct' && !request.echo) {
259
+ throw new AbloValidationError(
260
+ 'A direct Kysely mutation requires a transactional Postgres logical marker',
261
+ { code: 'source_adapter_misconfigured' },
262
+ );
263
+ }
264
+ const echoIntent = parseEchoIntent(request, markerModelFor, mode === 'direct');
230
265
 
231
266
  const requestHash = sourceChangeIntentHash(request);
232
267
  return db.transaction().execute(async (transaction) => {
@@ -280,8 +315,11 @@ export function createKyselyMutationAdapter(
280
315
  completeLedgerQuery(request.correlationId, rows),
281
316
  );
282
317
  if (request.echo?.kind === 'postgres-wal') {
318
+ const payload = echoIntent
319
+ ? JSON.stringify(resolveEchoMarker(echoIntent, rows))
320
+ : request.echo.payload;
283
321
  await transaction.executeQuery(
284
- postgresLogicalMarkerQuery(request.echo.payload),
322
+ postgresLogicalMarkerQuery(payload),
285
323
  );
286
324
  }
287
325
  return { rows };