@abloatai/transaction 0.50.0 → 0.52.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/capability.d.ts +2 -2
- package/dist/auth/capability.js +1 -1
- package/dist/auth/index.d.ts +9 -3
- package/dist/auth/index.d.ts.map +1 -1
- package/dist/auth/index.js +25 -20
- 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.d.ts.map +1 -1
- package/dist/auth/sessionMint.js +3 -2
- package/dist/auth/sessionMint.js.map +1 -1
- package/dist/branches.d.ts +4 -4
- 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 +3 -2
- package/dist/errorCodes.d.ts.map +1 -1
- package/dist/errorCodes.js +4 -3
- package/dist/errorCodes.js.map +1 -1
- package/dist/errors.d.ts +1 -1
- package/dist/errors.js +1 -1
- package/dist/resources/httpResources.d.ts +11 -2
- package/dist/resources/httpResources.d.ts.map +1 -1
- package/dist/resources/modelOperations.d.ts +6 -6
- 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 +4 -7
- 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/commit.d.ts +5 -1
- package/dist/server/commit.d.ts.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 +6 -2
- package/dist/transport/httpClient.d.ts.map +1 -1
- 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 +50 -4
- 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.js +2 -2
- package/dist/wire/accountResponses.js.map +1 -1
- package/dist/wire/auth.d.ts.map +1 -1
- package/dist/wire/auth.js +4 -3
- package/dist/wire/auth.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 +2 -2
- package/dist/wire/index.d.ts.map +1 -1
- package/dist/wire/index.js +1 -1
- package/dist/wire/index.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/capability.ts +3 -3
- package/src/auth/index.ts +54 -22
- package/src/auth/schemas.ts +16 -2
- package/src/auth/sessionIssuanceProtocol.ts +57 -0
- package/src/auth/sessionMint.ts +3 -2
- package/src/coordination/schema.ts +7 -2
- package/src/errorCodes.ts +10 -4
- package/src/errors.ts +1 -1
- package/src/resources/httpResources.ts +11 -2
- package/src/resources/modelOperations.ts +6 -6
- 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 +4 -7
- 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/commit.ts +5 -1
- 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 +6 -2
- package/src/transport/httpTransport.ts +59 -4
- 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 +2 -2
- package/src/wire/auth.ts +4 -3
- package/src/wire/commit.ts +31 -1
- package/src/wire/index.ts +3 -0
- package/src/wire/protocol.ts +1 -1
package/src/schema/ddl.ts
CHANGED
|
@@ -117,7 +117,7 @@ export function sqlType(fieldType: ModelJSON['fields'][string]['type']): string
|
|
|
117
117
|
}
|
|
118
118
|
}
|
|
119
119
|
|
|
120
|
-
const BASE_COLUMNS = new Set(['id'
|
|
120
|
+
const BASE_COLUMNS = new Set(['id']);
|
|
121
121
|
|
|
122
122
|
// ── Foreign keys (relation-driven, sync-safe) ────────────────────────────────
|
|
123
123
|
|
|
@@ -154,11 +154,11 @@ interface ForeignKeyDdl {
|
|
|
154
154
|
* in by setting `{ fk: true }`.
|
|
155
155
|
*
|
|
156
156
|
* The `fk` marker is deliberately separate from `parent`: `parent` controls
|
|
157
|
-
* sync-group fan-out and visibility, while `fk` requests
|
|
157
|
+
* sync-group fan-out and visibility, while `fk` requests database referential
|
|
158
158
|
* integrity in the database. A relation sets `fk` only when its target lives in
|
|
159
159
|
* the same database, is written in the same commit, and is a strong, contained
|
|
160
160
|
* entity. Soft references — provenance or template pointers such as
|
|
161
|
-
* `
|
|
161
|
+
* `sourceDocumentId` or `templateId` — stay plain columns; a hard foreign key there
|
|
162
162
|
* would reject a write that points across scopes or at an absent row and break
|
|
163
163
|
* sync.
|
|
164
164
|
*
|
|
@@ -204,7 +204,7 @@ function foreignKeyStatements(
|
|
|
204
204
|
// The model's provisioned column set — guard so a relation whose FK field
|
|
205
205
|
// isn't actually declared (no column) never produces a broken ALTER.
|
|
206
206
|
const orgCol = tenancyColumn(resolveTenancy(model));
|
|
207
|
-
const columns = new Set<string>(['id'
|
|
207
|
+
const columns = new Set<string>(['id']);
|
|
208
208
|
if (orgCol) columns.add(orgCol);
|
|
209
209
|
for (const [fieldName, meta] of Object.entries(model.fields)) {
|
|
210
210
|
columns.add(meta.column ?? camelToSnake(fieldName));
|
|
@@ -275,8 +275,7 @@ export function generateProvisionPlan(
|
|
|
275
275
|
// declared boundary is what makes the set of tables a customer's own
|
|
276
276
|
// database receives derivable instead of hand-coded.
|
|
277
277
|
if ((model.plane ?? 'tenant') === 'control') continue;
|
|
278
|
-
|
|
279
|
-
// Default the physical table to the model key when `tableName` is omitted —
|
|
278
|
+
// Default the database table to the model key when `tableName` is omitted —
|
|
280
279
|
// same fallback the migration path uses (`tableOfModel: m.tableName ?? key`).
|
|
281
280
|
// Without this, a schema that doesn't set `tableName` (e.g. the `ablo init`
|
|
282
281
|
// starter) provisions zero tables.
|
|
@@ -294,13 +293,10 @@ export function generateProvisionPlan(
|
|
|
294
293
|
// no tenancy column — they're scoped via a parent FK or not at all.
|
|
295
294
|
const orgCol = tenancyColumn(resolveTenancy(model));
|
|
296
295
|
const baseColumns = [
|
|
297
|
-
` ${q('id')} TEXT PRIMARY KEY
|
|
298
|
-
...(orgCol ? [` ${q(orgCol)} TEXT NOT NULL
|
|
299
|
-
` ${q('created_by')} TEXT,`,
|
|
300
|
-
` ${q('created_at')} TIMESTAMPTZ NOT NULL DEFAULT NOW(),`,
|
|
301
|
-
` ${q('updated_at')} TIMESTAMPTZ NOT NULL DEFAULT NOW()`,
|
|
296
|
+
` ${q('id')} TEXT PRIMARY KEY`,
|
|
297
|
+
...(orgCol ? [` ${q(orgCol)} TEXT NOT NULL`] : []),
|
|
302
298
|
];
|
|
303
|
-
statements.push(`CREATE TABLE IF NOT EXISTS ${qt} (\n${baseColumns.join('
|
|
299
|
+
statements.push(`CREATE TABLE IF NOT EXISTS ${qt} (\n${baseColumns.join(',\n')}\n);`);
|
|
304
300
|
|
|
305
301
|
for (const [fieldName, meta] of Object.entries(model.fields)) {
|
|
306
302
|
const col = meta.column ?? camelToSnake(fieldName);
|
|
@@ -458,7 +454,7 @@ export function generateMigrationPlan(
|
|
|
458
454
|
case 'rename_model': {
|
|
459
455
|
const fromTable = tableOfModel(prev, step.from);
|
|
460
456
|
const toTable = tableOfModel(next, step.to);
|
|
461
|
-
// A logical model rename only needs SQL when the
|
|
457
|
+
// A logical model rename only needs SQL when the database table name
|
|
462
458
|
// actually changes; if tableName is unchanged the rename is metadata.
|
|
463
459
|
if (fromTable && toTable && fromTable !== toTable) {
|
|
464
460
|
statements.push(`ALTER TABLE ${qtFor(fromTable)} RENAME TO ${q(toTable)};`);
|
package/src/schema/field.ts
CHANGED
|
@@ -9,7 +9,7 @@
|
|
|
9
9
|
* Usage:
|
|
10
10
|
* import { field } from '@abloatai/transaction/schema';
|
|
11
11
|
*
|
|
12
|
-
* const
|
|
12
|
+
* const items = model({
|
|
13
13
|
* title: field.string(),
|
|
14
14
|
* projectId: field.string().indexed(), // fluent chain
|
|
15
15
|
* priority: field.number().optional(),
|
|
@@ -19,7 +19,7 @@
|
|
|
19
19
|
* Or use Zod directly (no metadata, but still works):
|
|
20
20
|
* import { z } from 'zod';
|
|
21
21
|
*
|
|
22
|
-
* const
|
|
22
|
+
* const items = model({
|
|
23
23
|
* title: z.string(),
|
|
24
24
|
* });
|
|
25
25
|
*/
|
|
@@ -199,7 +199,7 @@ export function resolveFieldMeta(schema: z.ZodType): FieldMeta {
|
|
|
199
199
|
|
|
200
200
|
/** A Zod schema returned by a {@link field} builder — the underlying Zod type plus
|
|
201
201
|
* two chainable methods: `indexed()` marks the field for a database index, and
|
|
202
|
-
* `from(column)` overrides the
|
|
202
|
+
* `from(column)` overrides the database column name it maps to. */
|
|
203
203
|
export type FieldBuilder<T extends z.ZodType> = T & {
|
|
204
204
|
indexed(): FieldBuilder<T>;
|
|
205
205
|
from(column: string): FieldBuilder<T>;
|
package/src/schema/fieldRef.ts
CHANGED
|
@@ -24,7 +24,7 @@
|
|
|
24
24
|
* `string`.
|
|
25
25
|
*
|
|
26
26
|
* `model` rides along so a reference carries where it came from. Claiming
|
|
27
|
-
* `users.email` through `ablo.
|
|
27
|
+
* `users.email` through `ablo.items` is a mistake nothing can currently see.
|
|
28
28
|
*/
|
|
29
29
|
export interface FieldRef<
|
|
30
30
|
Model extends string = string,
|
package/src/schema/generate.ts
CHANGED
|
@@ -10,9 +10,8 @@
|
|
|
10
10
|
* that same schema, so the types you code against, the rows the database stores, and
|
|
11
11
|
* the entities the sync layer moves all describe the same thing.
|
|
12
12
|
*
|
|
13
|
-
* The generated interface covers
|
|
14
|
-
* enums emitted as string-literal unions. Relations are not expanded here
|
|
15
|
-
* runtime SDK resolves those through its typed accessors.
|
|
13
|
+
* The generated interface covers storage-mode fields plus each declared field,
|
|
14
|
+
* with enums emitted as string-literal unions. Relations are not expanded here.
|
|
16
15
|
*/
|
|
17
16
|
|
|
18
17
|
import type { FieldMeta } from './field.js';
|
|
@@ -70,12 +69,7 @@ export function generateTypes(schema: SchemaJSON): string {
|
|
|
70
69
|
for (const [key, model] of Object.entries(schema.models)) {
|
|
71
70
|
lines.push(`export interface ${nameByKey.get(key)!} {`);
|
|
72
71
|
lines.push(' id: string;');
|
|
73
|
-
lines.push(' createdAt: Date;');
|
|
74
|
-
lines.push(' updatedAt: Date;');
|
|
75
|
-
lines.push(' organizationId?: string;');
|
|
76
|
-
lines.push(' createdBy?: string;');
|
|
77
72
|
for (const [fieldName, meta] of Object.entries(model.fields)) {
|
|
78
|
-
// A model that redeclares a base column doesn't double-emit it.
|
|
79
73
|
if ((BASE_FIELDS as readonly string[]).includes(fieldName)) continue;
|
|
80
74
|
lines.push(` ${fieldName}${meta.isOptional ? '?' : ''}: ${tsType(meta)};`);
|
|
81
75
|
}
|
package/src/schema/index.ts
CHANGED
|
@@ -8,7 +8,7 @@
|
|
|
8
8
|
* import { defineSchema, model, relation } from '@abloatai/transaction/schema';
|
|
9
9
|
*
|
|
10
10
|
* export const schema = defineSchema({
|
|
11
|
-
*
|
|
11
|
+
* items: model({
|
|
12
12
|
* title: z.string(),
|
|
13
13
|
* status: z.enum(['todo', 'doing', 'done']).default('todo'),
|
|
14
14
|
* projectId: z.string().optional(),
|
|
@@ -17,7 +17,7 @@
|
|
|
17
17
|
* }),
|
|
18
18
|
* });
|
|
19
19
|
*
|
|
20
|
-
* type
|
|
20
|
+
* type Item = Model<typeof schema, 'items'>;
|
|
21
21
|
* ```
|
|
22
22
|
*/
|
|
23
23
|
|
|
@@ -27,7 +27,7 @@ export { z } from 'zod';
|
|
|
27
27
|
// Field helpers (optional convenience wrappers around Zod)
|
|
28
28
|
export { field, indexed, getFieldMeta, type FieldBuilder, type FieldMeta } from './field.js';
|
|
29
29
|
|
|
30
|
-
// A field as a value rather than a quoted name — `schema.fields.
|
|
30
|
+
// A field as a value rather than a quoted name — `schema.fields.items.status`.
|
|
31
31
|
// Surfaces that name a field (claims today) take one of these so a name that
|
|
32
32
|
// does not exist stops compiling.
|
|
33
33
|
export {
|
package/src/schema/model.ts
CHANGED
|
@@ -8,7 +8,7 @@
|
|
|
8
8
|
* import { z } from 'zod';
|
|
9
9
|
* import { model, relation } from '@abloatai/transaction/schema';
|
|
10
10
|
*
|
|
11
|
-
* const
|
|
11
|
+
* const items = model({
|
|
12
12
|
* title: z.string(),
|
|
13
13
|
* status: z.enum(['todo', 'doing', 'done']).default('todo'),
|
|
14
14
|
* projectId: z.string().optional(),
|
|
@@ -90,13 +90,13 @@ export interface ModelOptions {
|
|
|
90
90
|
* Edges from this model to others, keyed by the accessor name they create. The
|
|
91
91
|
* engine reads them to index foreign keys, to order inserts so a parent row lands
|
|
92
92
|
* before the rows referencing it, and to generate the accessors that let you read
|
|
93
|
-
* `
|
|
93
|
+
* `item.project` or `project.items` directly. Built with the {@link relation}
|
|
94
94
|
* factories.
|
|
95
95
|
*
|
|
96
96
|
* ```ts
|
|
97
97
|
* relations: {
|
|
98
98
|
* project: relation.belongsTo('projects', 'projectId'),
|
|
99
|
-
* comments: relation.hasMany('comments', '
|
|
99
|
+
* comments: relation.hasMany('comments', 'itemId'),
|
|
100
100
|
* }
|
|
101
101
|
* ```
|
|
102
102
|
*/
|
|
@@ -110,8 +110,8 @@ export interface ModelOptions {
|
|
|
110
110
|
/**
|
|
111
111
|
* The wire type name for this model — the value that identifies its rows on the
|
|
112
112
|
* wire (the `__typename`). The loader stamps it onto incoming rows and uses it to
|
|
113
|
-
* find the matching model class. It defaults to the schema key (`
|
|
114
|
-
* `'
|
|
113
|
+
* find the matching model class. It defaults to the schema key (`items` →
|
|
114
|
+
* `'items'`); set it explicitly when the wire shape uses different casing, such as
|
|
115
115
|
* schema key `block` mapping to typename `'Block'`.
|
|
116
116
|
*
|
|
117
117
|
* This is the one value that identifies the model on the wire; the client-side
|
|
@@ -178,7 +178,7 @@ export interface ModelOptions {
|
|
|
178
178
|
* inbox. Accepts one role or many.
|
|
179
179
|
*
|
|
180
180
|
* ```ts
|
|
181
|
-
* //
|
|
181
|
+
* // archiveMember: { userId, archiveId }
|
|
182
182
|
* groups: { grants: { subject: 'user', scope: 'workspace' } }
|
|
183
183
|
* // a message → its addressee's inbox, keyed on `toId`
|
|
184
184
|
* groups: { roles: [entityRole({ kind: 'inbox', source: 'toId' })] }
|
|
@@ -395,7 +395,7 @@ export interface ModelDef<
|
|
|
395
395
|
* const tags = model({ label: z.string() });
|
|
396
396
|
*
|
|
397
397
|
* // Loaded at bootstrap (the default), with an edge to its project
|
|
398
|
-
* const
|
|
398
|
+
* const items = model({
|
|
399
399
|
* title: z.string(),
|
|
400
400
|
* status: z.enum(['todo', 'doing', 'done']).default('todo'),
|
|
401
401
|
* projectId: z.string().optional(),
|
package/src/schema/openapi.ts
CHANGED
|
@@ -208,8 +208,9 @@ const namedResp = (description: string, name: string): Json =>
|
|
|
208
208
|
*
|
|
209
209
|
* - `$schema` belongs on a standalone JSON Schema document, not every embedded
|
|
210
210
|
* OpenAPI Schema Object;
|
|
211
|
-
* - `propertyNames
|
|
212
|
-
* unsupported by Stainless
|
|
211
|
+
* - `propertyNames` constraining keys to strings is redundant for JSON objects,
|
|
212
|
+
* whatever else it constrains, and is unsupported by Stainless. The key rule
|
|
213
|
+
* still binds at runtime, where the Zod schema parses;
|
|
213
214
|
* - `additionalProperties: {}` means "any JSON value", but Stainless requires
|
|
214
215
|
* the equivalent, explicit `true`.
|
|
215
216
|
*
|
|
@@ -228,7 +229,6 @@ function portableSchema(value: unknown): unknown {
|
|
|
228
229
|
key === 'propertyNames' &&
|
|
229
230
|
typeof child === 'object' &&
|
|
230
231
|
child !== null &&
|
|
231
|
-
Object.keys(child).length === 1 &&
|
|
232
232
|
(child as Record<string, unknown>).type === 'string'
|
|
233
233
|
) {
|
|
234
234
|
continue;
|
|
@@ -267,7 +267,7 @@ const modelParam = (): Json => ({
|
|
|
267
267
|
in: 'path',
|
|
268
268
|
required: true,
|
|
269
269
|
schema: { type: 'string' },
|
|
270
|
-
description: 'A model name from your pushed schema, e.g. `
|
|
270
|
+
description: 'A model name from your pushed schema, e.g. `item`.',
|
|
271
271
|
});
|
|
272
272
|
const genericRow = (): Json => ({ type: 'object', additionalProperties: true });
|
|
273
273
|
|
|
@@ -591,9 +591,6 @@ export function abloOpenApi(options: SchemaToOpenApiOptions = {}): Json {
|
|
|
591
591
|
description:
|
|
592
592
|
'Call this first: every other route needs the key it returns. Requires a ' +
|
|
593
593
|
'secret (`sk_`) key — a session cannot mint itself.',
|
|
594
|
-
parameters: [
|
|
595
|
-
{ name: 'Idempotency-Key', in: 'header', schema: { type: 'string' }, description: 'Replay-safe key; the server returns the cached credential on retry.' },
|
|
596
|
-
],
|
|
597
594
|
requestBody: jsonBody(derive(ephemeralKeyRequestSchema, 'input')),
|
|
598
595
|
responses: { '201': jsonResp('The minted credential', derive(EphemeralKeyResponseSchema, 'output')) },
|
|
599
596
|
},
|
package/src/schema/queries.ts
CHANGED
|
@@ -44,7 +44,7 @@
|
|
|
44
44
|
* the factory and defers the cross-reference check to schema assembly.
|
|
45
45
|
*
|
|
46
46
|
* - A query carries no `name` until `defineQueries()` assigns one from its
|
|
47
|
-
* record key — the same pattern `defineSchema({
|
|
47
|
+
* record key — the same pattern `defineSchema({ items: model(...) })`
|
|
48
48
|
* uses, where the model's name is the record key rather than a field on
|
|
49
49
|
* the factory.
|
|
50
50
|
*
|
package/src/schema/relation.ts
CHANGED
|
@@ -4,15 +4,15 @@
|
|
|
4
4
|
* {@link relation} factories; the engine reads them to index foreign keys for
|
|
5
5
|
* fast child lookups, to order inserts so a parent row lands before the rows
|
|
6
6
|
* that reference it, and to generate the accessor properties that let you read
|
|
7
|
-
* `
|
|
7
|
+
* `item.project` or `project.items` directly.
|
|
8
8
|
*
|
|
9
9
|
* Usage:
|
|
10
10
|
* import { relation } from '@abloatai/transaction/schema';
|
|
11
11
|
*
|
|
12
|
-
* const
|
|
12
|
+
* const itemRelations = {
|
|
13
13
|
* project: relation.belongsTo('projects', 'projectId'),
|
|
14
14
|
* assignee: relation.belongsTo('users', 'assigneeId'),
|
|
15
|
-
* comments: relation.hasMany('comments', '
|
|
15
|
+
* comments: relation.hasMany('comments', 'itemId'),
|
|
16
16
|
* };
|
|
17
17
|
*/
|
|
18
18
|
|
|
@@ -28,9 +28,9 @@
|
|
|
28
28
|
* way often, such as a block's `sectionId`.
|
|
29
29
|
*
|
|
30
30
|
* `enrich: true` auto-populates the parent reference on an incoming change
|
|
31
|
-
* before the child data lands. A change to `
|
|
31
|
+
* before the child data lands. A change to `Item { teamId: 't1' }` picks up the
|
|
32
32
|
* already-loaded `teams:t1` record and attaches it as `data.team`, so you can
|
|
33
|
-
* read `
|
|
33
|
+
* read `item.team` without a second lookup. Enrichment is best-effort: if the
|
|
34
34
|
* parent has not loaded yet it quietly does nothing, and the child data still
|
|
35
35
|
* applies.
|
|
36
36
|
*
|
|
@@ -71,7 +71,7 @@ export interface BelongsToOptions {
|
|
|
71
71
|
* provenance or as a template, such as `sourceSectionId` or `templateId`; doing
|
|
72
72
|
* so would leak the record into an unrelated scope. The engine also cannot
|
|
73
73
|
* infer the parent from whether a field is optional — many real parent keys
|
|
74
|
-
* are optional, like a root folder or an inbox
|
|
74
|
+
* are optional, like a root folder or an inbox item — so you must declare the
|
|
75
75
|
* parent edge explicitly.
|
|
76
76
|
*
|
|
77
77
|
* It reads naturally at the call site:
|
|
@@ -218,7 +218,7 @@ class RelationBuilder<
|
|
|
218
218
|
export const relation = {
|
|
219
219
|
/**
|
|
220
220
|
* This model belongs to another model via a foreign key.
|
|
221
|
-
* e.g.,
|
|
221
|
+
* e.g., Item belongs to Project via projectId
|
|
222
222
|
*
|
|
223
223
|
* ```ts
|
|
224
224
|
* // Simple reference (no options)
|
|
@@ -252,7 +252,7 @@ export const relation = {
|
|
|
252
252
|
|
|
253
253
|
/**
|
|
254
254
|
* This model has many of another model — for example, a project has many
|
|
255
|
-
*
|
|
255
|
+
* items via `Item.projectId`.
|
|
256
256
|
*
|
|
257
257
|
* At runtime the engine adds a getter to the parent model that returns every
|
|
258
258
|
* child whose foreign key matches, and registers the foreign-key index on the
|
package/src/schema/residency.ts
CHANGED
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/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
|
+
};
|
|
@@ -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);
|