@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.
- package/dist/ablo.d.ts +1 -1
- package/dist/ablo.js +1 -1
- package/dist/ai-sdk/updateTool.d.ts +1 -1
- package/dist/ai-sdk/updateTool.js +1 -1
- package/dist/auth/apiKey.d.ts +0 -6
- package/dist/auth/apiKey.d.ts.map +1 -1
- package/dist/auth/apiKey.js +13 -60
- package/dist/auth/apiKey.js.map +1 -1
- package/dist/auth/baseUrl.d.ts +38 -0
- package/dist/auth/baseUrl.d.ts.map +1 -0
- package/dist/auth/baseUrl.js +126 -0
- package/dist/auth/baseUrl.js.map +1 -0
- package/dist/auth/capability.d.ts +2 -2
- package/dist/auth/capability.js +1 -1
- package/dist/auth/index.d.ts +15 -2
- package/dist/auth/index.d.ts.map +1 -1
- package/dist/auth/index.js +35 -21
- package/dist/auth/index.js.map +1 -1
- package/dist/auth/schemas.d.ts +24 -1
- package/dist/auth/schemas.d.ts.map +1 -1
- package/dist/auth/schemas.js +8 -2
- package/dist/auth/schemas.js.map +1 -1
- package/dist/auth/sessionIssuanceProtocol.d.ts +41 -0
- package/dist/auth/sessionIssuanceProtocol.d.ts.map +1 -0
- package/dist/auth/sessionIssuanceProtocol.js +48 -0
- package/dist/auth/sessionIssuanceProtocol.js.map +1 -0
- package/dist/auth/sessionMint.js +2 -2
- package/dist/branches.d.ts +34 -33
- package/dist/branches.d.ts.map +1 -1
- package/dist/branches.js +2 -4
- package/dist/branches.js.map +1 -1
- package/dist/coordination/schema.d.ts +3 -2
- package/dist/coordination/schema.d.ts.map +1 -1
- package/dist/coordination/schema.js +7 -2
- package/dist/coordination/schema.js.map +1 -1
- package/dist/errorCodes.d.ts +7 -3
- package/dist/errorCodes.d.ts.map +1 -1
- package/dist/errorCodes.js +8 -4
- package/dist/errorCodes.js.map +1 -1
- package/dist/errors.d.ts +1 -1
- package/dist/errors.js +1 -1
- package/dist/index.d.ts +2 -0
- package/dist/index.d.ts.map +1 -1
- package/dist/index.js.map +1 -1
- package/dist/resources/httpResources.d.ts +35 -2
- package/dist/resources/httpResources.d.ts.map +1 -1
- package/dist/resources/httpResources.js +10 -1
- package/dist/resources/httpResources.js.map +1 -1
- package/dist/resources/modelOperations.d.ts +18 -6
- package/dist/resources/modelOperations.d.ts.map +1 -1
- package/dist/resources/modelOperations.js.map +1 -1
- package/dist/resources/where.d.ts +67 -2
- package/dist/resources/where.d.ts.map +1 -1
- package/dist/resources/where.js +73 -0
- package/dist/resources/where.js.map +1 -1
- package/dist/schema/ddl.d.ts.map +1 -1
- package/dist/schema/ddl.js +9 -12
- package/dist/schema/ddl.js.map +1 -1
- package/dist/schema/field.d.ts +3 -3
- package/dist/schema/field.js +2 -2
- package/dist/schema/fieldRef.d.ts +1 -1
- package/dist/schema/generate.d.ts +2 -3
- package/dist/schema/generate.d.ts.map +1 -1
- package/dist/schema/generate.js +2 -8
- package/dist/schema/generate.js.map +1 -1
- package/dist/schema/index.d.ts +2 -2
- package/dist/schema/index.js +3 -3
- package/dist/schema/model.d.ts +7 -7
- package/dist/schema/model.js +2 -2
- package/dist/schema/openapi.d.ts.map +1 -1
- package/dist/schema/openapi.js +63 -21
- package/dist/schema/openapi.js.map +1 -1
- package/dist/schema/queries.d.ts +1 -1
- package/dist/schema/queries.js +1 -1
- package/dist/schema/relation.d.ts +8 -8
- package/dist/schema/relation.js +5 -5
- package/dist/schema/schema.d.ts +24 -37
- package/dist/schema/schema.d.ts.map +1 -1
- package/dist/schema/schema.js +15 -28
- package/dist/schema/schema.js.map +1 -1
- package/dist/schema/sugar.d.ts +7 -7
- package/dist/schema/sugar.js +5 -5
- package/dist/server/adapter.d.ts +2 -0
- package/dist/server/adapter.d.ts.map +1 -1
- package/dist/server/commit.d.ts +5 -1
- package/dist/server/commit.d.ts.map +1 -1
- package/dist/server/index.d.ts +3 -2
- package/dist/server/index.d.ts.map +1 -1
- package/dist/server/index.js.map +1 -1
- package/dist/server/readConfig.d.ts +109 -57
- package/dist/server/readConfig.d.ts.map +1 -1
- package/dist/server/readConfig.js +19 -5
- package/dist/server/readConfig.js.map +1 -1
- package/dist/source/adapters/drizzle.d.ts.map +1 -1
- package/dist/source/adapters/drizzle.js +5 -0
- package/dist/source/adapters/drizzle.js.map +1 -1
- package/dist/source/adapters/kysely.d.ts.map +1 -1
- package/dist/source/adapters/kysely.js +33 -8
- package/dist/source/adapters/kysely.js.map +1 -1
- package/dist/source/adapters/kyselyMutationCore.d.ts +1 -1
- package/dist/source/adapters/kyselyMutationCore.d.ts.map +1 -1
- package/dist/source/adapters/kyselyMutationCore.js +58 -20
- package/dist/source/adapters/kyselyMutationCore.js.map +1 -1
- package/dist/source/adapters/memory.d.ts.map +1 -1
- package/dist/source/adapters/memory.js +5 -0
- package/dist/source/adapters/memory.js.map +1 -1
- package/dist/source/adapters/prisma.d.ts.map +1 -1
- package/dist/source/adapters/prisma.js +6 -1
- package/dist/source/adapters/prisma.js.map +1 -1
- package/dist/source/conformance.js +21 -21
- package/dist/source/contract.d.ts +17 -0
- package/dist/source/contract.d.ts.map +1 -1
- package/dist/source/contract.js +31 -0
- package/dist/source/contract.js.map +1 -1
- package/dist/source/index.d.ts +1 -1
- package/dist/source/index.d.ts.map +1 -1
- package/dist/source/index.js +1 -1
- package/dist/source/index.js.map +1 -1
- package/dist/source/types.d.ts +1 -0
- package/dist/source/types.d.ts.map +1 -1
- package/dist/source/types.js.map +1 -1
- package/dist/transactionLayer.d.ts +3 -3
- package/dist/transactionLayer.js +3 -3
- package/dist/transport/httpClient.d.ts +13 -4
- package/dist/transport/httpClient.d.ts.map +1 -1
- package/dist/transport/httpClient.js +4 -2
- package/dist/transport/httpClient.js.map +1 -1
- package/dist/transport/httpTransport.d.ts +1 -0
- package/dist/transport/httpTransport.d.ts.map +1 -1
- package/dist/transport/httpTransport.js +79 -11
- package/dist/transport/httpTransport.js.map +1 -1
- package/dist/transport/wsFrameHandlers.d.ts.map +1 -1
- package/dist/transport/wsFrameHandlers.js +3 -0
- package/dist/transport/wsFrameHandlers.js.map +1 -1
- package/dist/types/streams.d.ts +2 -2
- package/dist/wire/accountResponses.d.ts +18 -13
- package/dist/wire/accountResponses.d.ts.map +1 -1
- package/dist/wire/accountResponses.js +5 -11
- package/dist/wire/accountResponses.js.map +1 -1
- package/dist/wire/claims.d.ts +1 -0
- package/dist/wire/claims.d.ts.map +1 -1
- package/dist/wire/claims.js +7 -1
- package/dist/wire/claims.js.map +1 -1
- package/dist/wire/commit.d.ts +144 -0
- package/dist/wire/commit.d.ts.map +1 -1
- package/dist/wire/commit.js +22 -1
- package/dist/wire/commit.js.map +1 -1
- package/dist/wire/frames.d.ts +4 -0
- package/dist/wire/frames.d.ts.map +1 -1
- package/dist/wire/inboundFrames.d.ts +22 -0
- package/dist/wire/inboundFrames.d.ts.map +1 -1
- package/dist/wire/index.d.ts +3 -3
- package/dist/wire/index.d.ts.map +1 -1
- package/dist/wire/index.js +2 -2
- package/dist/wire/index.js.map +1 -1
- package/dist/wire/listEnvelope.d.ts +20 -0
- package/dist/wire/listEnvelope.d.ts.map +1 -1
- package/dist/wire/listEnvelope.js +20 -0
- package/dist/wire/listEnvelope.js.map +1 -1
- package/dist/wire/protocol.d.ts +1 -1
- package/dist/wire/protocol.js +1 -1
- package/package.json +1 -1
- package/src/ablo.ts +1 -1
- package/src/ai-sdk/updateTool.ts +1 -1
- package/src/auth/apiKey.ts +13 -61
- package/src/auth/baseUrl.ts +144 -0
- package/src/auth/capability.ts +3 -3
- package/src/auth/index.ts +62 -21
- package/src/auth/schemas.ts +16 -2
- package/src/auth/sessionIssuanceProtocol.ts +57 -0
- package/src/auth/sessionMint.ts +2 -2
- package/src/branches.ts +2 -4
- package/src/coordination/schema.ts +7 -2
- package/src/errorCodes.ts +29 -5
- package/src/errors.ts +1 -1
- package/src/index.ts +2 -0
- package/src/resources/httpResources.ts +45 -2
- package/src/resources/modelOperations.ts +18 -6
- package/src/resources/where.ts +70 -4
- package/src/schema/ddl.ts +9 -13
- package/src/schema/field.ts +3 -3
- package/src/schema/fieldRef.ts +1 -1
- package/src/schema/generate.ts +2 -8
- package/src/schema/index.ts +3 -3
- package/src/schema/model.ts +7 -7
- package/src/schema/openapi.ts +64 -21
- package/src/schema/queries.ts +1 -1
- package/src/schema/relation.ts +8 -8
- package/src/schema/residency.ts +0 -1
- package/src/schema/schema.ts +33 -58
- package/src/schema/sugar.ts +7 -7
- package/src/server/adapter.ts +2 -0
- package/src/server/commit.ts +5 -1
- package/src/server/index.ts +8 -2
- package/src/server/readConfig.ts +111 -57
- package/src/source/adapters/drizzle.ts +5 -0
- package/src/source/adapters/kysely.ts +48 -10
- package/src/source/adapters/kyselyMutationCore.ts +69 -22
- package/src/source/adapters/memory.ts +5 -0
- package/src/source/adapters/prisma.ts +6 -1
- package/src/source/conformance.ts +21 -21
- package/src/source/contract.ts +34 -0
- package/src/source/index.ts +2 -0
- package/src/source/types.ts +1 -0
- package/src/transactionLayer.ts +3 -3
- package/src/transport/httpClient.ts +23 -6
- package/src/transport/httpTransport.ts +88 -11
- package/src/transport/wsFrameHandlers.ts +3 -0
- package/src/types/global.ts +1 -1
- package/src/types/streams.ts +2 -2
- package/src/wire/accountResponses.ts +5 -11
- package/src/wire/claims.ts +7 -1
- package/src/wire/commit.ts +31 -1
- package/src/wire/index.ts +10 -1
- package/src/wire/listEnvelope.ts +23 -0
- package/src/wire/protocol.ts +1 -1
package/src/schema/schema.ts
CHANGED
|
@@ -8,20 +8,20 @@
|
|
|
8
8
|
* import { defineSchema, model, relation } from '@abloatai/transaction/schema';
|
|
9
9
|
*
|
|
10
10
|
* const schema = defineSchema({
|
|
11
|
-
*
|
|
11
|
+
* items: model({
|
|
12
12
|
* title: z.string(),
|
|
13
13
|
* status: z.enum(['todo', 'doing', 'done']).default('todo'),
|
|
14
|
-
*
|
|
14
|
+
* workspaceId: z.string().optional(),
|
|
15
15
|
* }, {
|
|
16
|
-
*
|
|
16
|
+
* workspace: relation.belongsTo('workspaces', 'workspaceId'),
|
|
17
17
|
* }),
|
|
18
18
|
* });
|
|
19
19
|
*
|
|
20
|
-
* type
|
|
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
|
-
*
|
|
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
|
|
229
|
-
*
|
|
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.
|
|
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
|
-
*
|
|
273
|
-
*
|
|
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
|
|
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
|
|
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
|
|
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
|
|
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, '
|
|
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
|
-
* '
|
|
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
|
-
?
|
|
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
|
-
?
|
|
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
|
|
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.
|
|
504
|
-
*
|
|
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
|
|
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
|
-
?
|
|
520
|
-
|
|
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
|
-
*
|
|
574
|
-
*
|
|
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
|
-
//
|
|
686
|
-
//
|
|
687
|
-
//
|
|
688
|
-
|
|
689
|
-
|
|
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
|
-
`(${
|
|
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
|
);
|
package/src/schema/sugar.ts
CHANGED
|
@@ -15,10 +15,10 @@
|
|
|
15
15
|
*
|
|
16
16
|
* @example
|
|
17
17
|
* ```ts
|
|
18
|
-
*
|
|
19
|
-
* typename: '
|
|
18
|
+
* items: mutable.lazy({ title: z.string() }, {
|
|
19
|
+
* typename: 'Item', tableName: 'items',
|
|
20
20
|
* relations: { ... },
|
|
21
|
-
* computed:
|
|
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. `'
|
|
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
|
|
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
|
|
115
|
+
* - `.instant` — small, always-needed control rows
|
|
116
116
|
* - `.lazy` — large collections fetched on first query
|
|
117
|
-
* (Block, Message,
|
|
117
|
+
* (Block, Message, Item)
|
|
118
118
|
*/
|
|
119
119
|
export const mutable = {
|
|
120
120
|
instant: <
|
package/src/server/adapter.ts
CHANGED
|
@@ -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 ─────────────────────────────────────────────────────────────────────
|
package/src/server/commit.ts
CHANGED
|
@@ -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
|
+
};
|
package/src/server/index.ts
CHANGED
|
@@ -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
|
|
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 {
|
|
31
|
+
export type {
|
|
32
|
+
ColumnOverride,
|
|
33
|
+
ParentScope,
|
|
34
|
+
ReadModelShape,
|
|
35
|
+
BootstrapModel,
|
|
36
|
+
} from './readConfig.js';
|
package/src/server/readConfig.ts
CHANGED
|
@@ -1,9 +1,23 @@
|
|
|
1
1
|
/**
|
|
2
|
-
* Canonical per-model read configuration
|
|
3
|
-
*
|
|
4
|
-
*
|
|
5
|
-
*
|
|
6
|
-
*
|
|
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
|
-
/**
|
|
17
|
-
|
|
18
|
-
|
|
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,
|
|
22
|
-
* cover generated compatibility names such as
|
|
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.
|
|
27
|
-
* name (usually the typename), while source handlers are
|
|
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
|
|
43
|
-
*
|
|
44
|
-
*
|
|
45
|
-
*
|
|
46
|
-
*
|
|
47
|
-
*
|
|
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
|
-
|
|
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
|
|
76
|
-
* stored in a text column comes back
|
|
77
|
-
*
|
|
78
|
-
*
|
|
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
|
|
140
|
+
function parseEchoIntent(
|
|
139
141
|
change: ChangeSet,
|
|
140
142
|
markerModelFor: (operationModel: string) => string,
|
|
141
|
-
|
|
143
|
+
required: boolean,
|
|
144
|
+
): SourceCommitEchoIntent | undefined {
|
|
142
145
|
const payload = change.echo?.payload;
|
|
143
146
|
if (!payload) {
|
|
144
|
-
|
|
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 =
|
|
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'
|
|
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(
|
|
322
|
+
postgresLogicalMarkerQuery(payload),
|
|
285
323
|
);
|
|
286
324
|
}
|
|
287
325
|
return { rows };
|