@abloatai/ablo 0.9.0 → 0.9.2
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/AGENTS.md +84 -0
- package/CHANGELOG.md +40 -0
- package/README.md +15 -7
- package/dist/BaseSyncedStore.d.ts +10 -0
- package/dist/BaseSyncedStore.js +26 -0
- package/dist/SyncClient.d.ts +12 -0
- package/dist/SyncClient.js +15 -0
- package/dist/agent/index.js +1 -1
- package/dist/api/index.d.ts +1 -1
- package/dist/client/Ablo.d.ts +9 -51
- package/dist/client/Ablo.js +2 -104
- package/dist/client/ApiClient.d.ts +3 -115
- package/dist/client/ApiClient.js +0 -232
- package/dist/client/auth.js +32 -2
- package/dist/client/httpClient.d.ts +5 -6
- package/dist/client/httpClient.js +2 -3
- package/dist/client/index.d.ts +1 -1
- package/dist/errorCodes.js +3 -3
- package/dist/index.js +1 -1
- package/dist/interfaces/index.d.ts +4 -4
- package/dist/mutators/UndoManager.d.ts +100 -11
- package/dist/mutators/UndoManager.js +282 -13
- package/dist/react/AbloProvider.d.ts +18 -8
- package/dist/react/context.d.ts +31 -0
- package/dist/react/index.d.ts +1 -1
- package/dist/react/index.js +1 -1
- package/dist/react/useUndoScope.js +7 -0
- package/dist/schema/ddl.d.ts +8 -0
- package/dist/schema/ddl.js +10 -0
- package/dist/schema/index.d.ts +1 -1
- package/dist/schema/index.js +1 -1
- package/dist/server/commit.d.ts +4 -5
- package/dist/source/adapter.d.ts +18 -12
- package/dist/source/adapter.js +8 -7
- package/dist/source/adapters/drizzle.d.ts +15 -6
- package/dist/source/adapters/drizzle.js +87 -49
- package/dist/source/adapters/memory.d.ts +1 -1
- package/dist/source/adapters/memory.js +2 -2
- package/dist/source/adapters/prisma.d.ts +3 -3
- package/dist/source/adapters/prisma.js +6 -29
- package/dist/source/conformance.d.ts +1 -1
- package/dist/source/conformance.js +2 -2
- package/dist/source/contract.d.ts +3 -2
- package/dist/source/contract.js +3 -2
- package/dist/source/index.d.ts +1 -0
- package/dist/source/index.js +3 -2
- package/dist/source/migrations.d.ts +14 -0
- package/dist/source/migrations.js +39 -0
- package/dist/types/streams.d.ts +2 -1
- package/dist/wire/frames.d.ts +6 -8
- package/docs/api.md +1 -1
- package/docs/cli.md +18 -5
- package/docs/data-sources.md +68 -83
- package/docs/examples/ai-sdk-tool.md +11 -5
- package/docs/examples/existing-python-backend.md +26 -4
- package/docs/examples/nextjs.md +3 -2
- package/docs/examples/scoped-agent.md +38 -11
- package/docs/identity.md +86 -59
- package/docs/index.md +1 -1
- package/docs/integration-guide.md +85 -54
- package/docs/react.md +39 -28
- package/llms.txt +18 -11
- package/package.json +2 -2
package/dist/schema/ddl.d.ts
CHANGED
|
@@ -54,6 +54,14 @@ export interface MigrationPlan {
|
|
|
54
54
|
/** Per-app schema name for an app (organization) id. */
|
|
55
55
|
export declare function appSchemaName(organizationId: string): string;
|
|
56
56
|
export declare function camelToSnake(identifier: string): string;
|
|
57
|
+
/**
|
|
58
|
+
* Pure snake_case → camelCase — the inverse of {@link camelToSnake}, matching
|
|
59
|
+
* `postgres.toCamel` semantics. Read-side translation: a column read back from a
|
|
60
|
+
* BYO database (e.g. via `drizzleDataSource`) maps to the same JS field the SDK
|
|
61
|
+
* wrote, so `camelToSnake('operatorId') === 'operator_id'` and
|
|
62
|
+
* `snakeToCamel('operator_id') === 'operatorId'` round-trip.
|
|
63
|
+
*/
|
|
64
|
+
export declare function snakeToCamel(identifier: string): string;
|
|
57
65
|
/** Quote an identifier (defense-in-depth; inputs are already slug/snake). */
|
|
58
66
|
export declare function q(identifier: string): string;
|
|
59
67
|
export declare function sqlType(fieldType: ModelJSON['fields'][string]['type']): string;
|
package/dist/schema/ddl.js
CHANGED
|
@@ -31,6 +31,16 @@ export function appSchemaName(organizationId) {
|
|
|
31
31
|
export function camelToSnake(identifier) {
|
|
32
32
|
return identifier.replace(/[A-Z]/g, (ch) => `_${ch.toLowerCase()}`);
|
|
33
33
|
}
|
|
34
|
+
/**
|
|
35
|
+
* Pure snake_case → camelCase — the inverse of {@link camelToSnake}, matching
|
|
36
|
+
* `postgres.toCamel` semantics. Read-side translation: a column read back from a
|
|
37
|
+
* BYO database (e.g. via `drizzleDataSource`) maps to the same JS field the SDK
|
|
38
|
+
* wrote, so `camelToSnake('operatorId') === 'operator_id'` and
|
|
39
|
+
* `snakeToCamel('operator_id') === 'operatorId'` round-trip.
|
|
40
|
+
*/
|
|
41
|
+
export function snakeToCamel(identifier) {
|
|
42
|
+
return identifier.replace(/_+([a-z0-9])/g, (_, ch) => ch.toUpperCase());
|
|
43
|
+
}
|
|
34
44
|
/** Quote an identifier (defense-in-depth; inputs are already slug/snake). */
|
|
35
45
|
export function q(identifier) {
|
|
36
46
|
return `"${identifier.replace(/"/g, '""')}"`;
|
package/dist/schema/index.d.ts
CHANGED
|
@@ -32,7 +32,7 @@ export { mutable, readOnly, type SugarOptions } from './sugar.js';
|
|
|
32
32
|
export { defineSchema, composeIdentitySyncGroups, type Schema, type SchemaRecord, type InferModel, type InferCreate, type InferModelNames, type BaseModelFields, type InsertValue, type UpsertValue, type UpdateValue, type DeleteId, type DefineSchemaOptions, type Casing, type CasingConvention, type CasingFn, composeEntitySyncGroups, type IdentityRole, type IdentityContext, type IdentityRoleSource, type EntityRole, type EntityContext, type EntityRoleSource, type RoleSource, type RoleContext, type SyncGroup, identityRole, entityRole, extractIdentityIds, extractEntityIds, syncGroup, syncGroupSchema, identityRoleSchema, entityRoleSchema, roleSchema, roleSourceSchema, scopeSchema, grantsRefSchema, } from './schema.js';
|
|
33
33
|
export { serializeSchema, parseSchema, toSchemaJSON, fromSchemaJSON, schemaHash, type SchemaJSON, type ModelJSON, type RelationJSON, } from './serialize.js';
|
|
34
34
|
export { selectModels } from './select.js';
|
|
35
|
-
export { generateProvisionPlan, generateMigrationPlan, appSchemaName, camelToSnake, q, sqlType, type ProvisionPlan, type MigrationPlan, } from './ddl.js';
|
|
35
|
+
export { generateProvisionPlan, generateMigrationPlan, appSchemaName, camelToSnake, snakeToCamel, q, sqlType, type ProvisionPlan, type MigrationPlan, } from './ddl.js';
|
|
36
36
|
export { diffSchema, classifyMigration, classifyCast, isAutoApplicable, isBlockerResolved, unresolvedBlockers, type BackfillValue, type MigrationStep, type FieldChanges, type FieldColumnChange, type FieldTypeChange, type NullabilityChange, type EnumValuesChange, type IndexChange, type CastSafety, type FieldType, type RenameHints, type MigrationSignal, type MigrationClassification, type WarningCode, type BlockerCode, } from './diff.js';
|
|
37
37
|
export { generateTypes } from './generate.js';
|
|
38
38
|
export { query, defineQueries, type QueryDef, type QueryRecord, type Queries, type InferQueryInput, type InferQueryResult, } from './queries.js';
|
package/dist/schema/index.js
CHANGED
|
@@ -52,7 +52,7 @@ export { serializeSchema, parseSchema, toSchemaJSON, fromSchemaJSON, schemaHash,
|
|
|
52
52
|
// Schema projection — derive an app's subset from one canonical schema.
|
|
53
53
|
export { selectModels } from './select.js';
|
|
54
54
|
// Schema → Postgres DDL (pure; shared by the hosted server and the CLI)
|
|
55
|
-
export { generateProvisionPlan, generateMigrationPlan, appSchemaName, camelToSnake, q, sqlType, } from './ddl.js';
|
|
55
|
+
export { generateProvisionPlan, generateMigrationPlan, appSchemaName, camelToSnake, snakeToCamel, q, sqlType, } from './ddl.js';
|
|
56
56
|
// Schema diff + migration planning (pure; SQL emission lowered by ddl.ts)
|
|
57
57
|
export { diffSchema, classifyMigration, classifyCast, isAutoApplicable, isBlockerResolved, unresolvedBlockers, } from './diff.js';
|
|
58
58
|
// Schema → TypeScript type emission (the `generate` half; pure)
|
package/dist/server/commit.d.ts
CHANGED
|
@@ -61,11 +61,10 @@ export interface CommitContext {
|
|
|
61
61
|
*/
|
|
62
62
|
confirmationState?: ConfirmationState;
|
|
63
63
|
/**
|
|
64
|
-
* FK to
|
|
65
|
-
*
|
|
66
|
-
*
|
|
67
|
-
*
|
|
68
|
-
* predate the turn protocol (→ `caused_by_task_id = NULL`).
|
|
64
|
+
* Dormant FK to the agent-task id (`agent_tasks.id`). The SDK no longer
|
|
65
|
+
* sets it (turns/tasks removed; attribution rides on the claim/intent id
|
|
66
|
+
* + server-stamped actor/capability). Still validated + written onto
|
|
67
|
+
* `caused_by_task_id` when present, but client writes leave it `null`.
|
|
69
68
|
*/
|
|
70
69
|
causedByTaskId?: string | null;
|
|
71
70
|
}
|
package/dist/source/adapter.d.ts
CHANGED
|
@@ -1,24 +1,30 @@
|
|
|
1
1
|
/**
|
|
2
|
-
* The Data Source adapter
|
|
3
|
-
*
|
|
2
|
+
* The Data Source adapter — ONE interface every ORM backend implements, and the
|
|
3
|
+
* bridge that wires it into the core `dataSource()` handler.
|
|
4
4
|
*
|
|
5
5
|
* Pattern (Auth.js / Better Auth): one core interface, one package per ORM
|
|
6
6
|
* (`prismaDataSource`, `drizzleDataSource`, `kyselyDataSource`), each provably
|
|
7
|
-
* correct via the shared conformance suite. The adapter owns
|
|
8
|
-
* the transactional outbox
|
|
7
|
+
* correct via the shared conformance suite. The adapter owns reading and writing
|
|
8
|
+
* the database, plus the transactional outbox and idempotency, so a customer never
|
|
9
|
+
* hand-writes them:
|
|
9
10
|
*
|
|
10
11
|
* export const POST = dataSource({
|
|
11
12
|
* schema, apiKey: process.env.ABLO_API_KEY!,
|
|
12
13
|
* ...sourceHandlersFromAdapter(prismaDataSource(prisma, schema), schema),
|
|
13
14
|
* });
|
|
14
15
|
*
|
|
15
|
-
* The bridge below
|
|
16
|
-
* handler's `commit` / `events` / per-model `load`+`list` — no per-ORM
|
|
17
|
-
* anywhere above the adapter.
|
|
16
|
+
* The bridge below connects the adapter to the core: it turns ONE adapter into the
|
|
17
|
+
* core handler's `commit` / `events` / per-model `load`+`list` — no per-ORM
|
|
18
|
+
* branching anywhere above the adapter.
|
|
18
19
|
*/
|
|
19
20
|
import type { SourceListQuery, SourceRequestContext } from './index.js';
|
|
20
21
|
import type { AdapterCapabilities, ChangeSet, EventsPage, Migration } from './contract.js';
|
|
21
|
-
/**
|
|
22
|
+
/**
|
|
23
|
+
* A canonical row keyed by schema FIELD name (e.g. `operatorId`) — the SDK shape,
|
|
24
|
+
* NOT physical column names. Each adapter is the translation boundary: Prisma maps
|
|
25
|
+
* via its `@map`, Drizzle via the schema's `camelToSnake`/`column` rule. `unknown`
|
|
26
|
+
* leaf is narrowed by codegen later.
|
|
27
|
+
*/
|
|
22
28
|
export type Row = Record<string, unknown>;
|
|
23
29
|
/** A read against the canonical store — a single-row load or a filtered list. */
|
|
24
30
|
export type AdapterReadRequest = {
|
|
@@ -37,13 +43,13 @@ export interface AdapterCommitResult {
|
|
|
37
43
|
readonly rows: readonly Row[];
|
|
38
44
|
}
|
|
39
45
|
/**
|
|
40
|
-
* The
|
|
41
|
-
*
|
|
42
|
-
* `ablo_idempotency` + `ablo_outbox`
|
|
46
|
+
* The adapter interface. An ORM adapter implements exactly these. `read`/`commit`
|
|
47
|
+
* read and write the database; `events` reads the outbox; `migrations` ships the
|
|
48
|
+
* `ablo_idempotency` + `ablo_outbox` table-creation SQL so the customer never writes it.
|
|
43
49
|
*/
|
|
44
50
|
export interface DataSourceAdapter {
|
|
45
51
|
readonly capabilities: AdapterCapabilities;
|
|
46
|
-
/**
|
|
52
|
+
/** The table-creation SQL the adapter ships for its own tables (`ablo_idempotency` + `ablo_outbox`). */
|
|
47
53
|
migrations(): readonly Migration[];
|
|
48
54
|
/** Canonical rows for a load/list. */
|
|
49
55
|
read(req: AdapterReadRequest): Promise<readonly Row[]>;
|
package/dist/source/adapter.js
CHANGED
|
@@ -1,19 +1,20 @@
|
|
|
1
1
|
/**
|
|
2
|
-
* The Data Source adapter
|
|
3
|
-
*
|
|
2
|
+
* The Data Source adapter — ONE interface every ORM backend implements, and the
|
|
3
|
+
* bridge that wires it into the core `dataSource()` handler.
|
|
4
4
|
*
|
|
5
5
|
* Pattern (Auth.js / Better Auth): one core interface, one package per ORM
|
|
6
6
|
* (`prismaDataSource`, `drizzleDataSource`, `kyselyDataSource`), each provably
|
|
7
|
-
* correct via the shared conformance suite. The adapter owns
|
|
8
|
-
* the transactional outbox
|
|
7
|
+
* correct via the shared conformance suite. The adapter owns reading and writing
|
|
8
|
+
* the database, plus the transactional outbox and idempotency, so a customer never
|
|
9
|
+
* hand-writes them:
|
|
9
10
|
*
|
|
10
11
|
* export const POST = dataSource({
|
|
11
12
|
* schema, apiKey: process.env.ABLO_API_KEY!,
|
|
12
13
|
* ...sourceHandlersFromAdapter(prismaDataSource(prisma, schema), schema),
|
|
13
14
|
* });
|
|
14
15
|
*
|
|
15
|
-
* The bridge below
|
|
16
|
-
* handler's `commit` / `events` / per-model `load`+`list` — no per-ORM
|
|
17
|
-
* anywhere above the adapter.
|
|
16
|
+
* The bridge below connects the adapter to the core: it turns ONE adapter into the
|
|
17
|
+
* core handler's `commit` / `events` / per-model `load`+`list` — no per-ORM
|
|
18
|
+
* branching anywhere above the adapter.
|
|
18
19
|
*/
|
|
19
20
|
export {};
|
|
@@ -1,12 +1,21 @@
|
|
|
1
1
|
/**
|
|
2
|
-
* Drizzle Data Source adapter. Same
|
|
2
|
+
* Drizzle Data Source adapter. Same adapter interface + conformance as `prismaDataSource`,
|
|
3
3
|
* built against Drizzle's REAL API (read from drizzle-orm's own source/docs):
|
|
4
4
|
* - `db.transaction(async (tx) => …)` — interactive transaction (commit/rollback).
|
|
5
5
|
* - `db.execute(sql`…`)` — parametrized raw SQL; `sql.identifier()` safely quotes
|
|
6
6
|
* dynamic table/column names, `sql`${value}`` parametrizes values.
|
|
7
|
-
*
|
|
8
|
-
*
|
|
9
|
-
*
|
|
7
|
+
*
|
|
8
|
+
* SCHEMA-DRIVEN COLUMNS. Unlike Prisma — whose delegate applies the model's
|
|
9
|
+
* `@map` for free — this adapter writes raw SQL, so it would otherwise bypass any
|
|
10
|
+
* field→column translation. It therefore derives every table + column name from
|
|
11
|
+
* the SAME rule the provisioner uses (`generateProvisionPlan`):
|
|
12
|
+
* table = `model.tableName ?? key`
|
|
13
|
+
* column = `fieldMeta.column ?? camelToSnake(field)` (+ the model's tenancy column)
|
|
14
|
+
* so `ablo migrate` (which emits `operator_id`) and this adapter (which now writes
|
|
15
|
+
* `operator_id`) COMPOSE. Define the schema once, point Ablo at your Postgres —
|
|
16
|
+
* no hand-written parallel Drizzle table. The adapter is the translation boundary:
|
|
17
|
+
* its public surface (rows in/out, outbox `data`) is field-keyed (the SDK shape);
|
|
18
|
+
* the physical columns it reads/writes are snake_case.
|
|
10
19
|
*
|
|
11
20
|
* IMPORTANT GOTCHAS (from drizzle-orm docs):
|
|
12
21
|
* 1. Interactive `db.transaction` requires a driver that supports it. Neon's
|
|
@@ -20,8 +29,8 @@
|
|
|
20
29
|
* adapter is one small, fully-typed unit with no per-driver builder generics.
|
|
21
30
|
*/
|
|
22
31
|
import { type SQL } from 'drizzle-orm';
|
|
23
|
-
import type { PgTable } from 'drizzle-orm/pg-core';
|
|
24
32
|
import type { DataSourceAdapter, Row } from '../adapter.js';
|
|
33
|
+
import type { Schema, SchemaRecord } from '../../schema/schema.js';
|
|
25
34
|
/** The subset of a Drizzle database/transaction handle the adapter calls. */
|
|
26
35
|
export interface DrizzleLike {
|
|
27
36
|
execute(query: SQL): Promise<DrizzleExecuteResult>;
|
|
@@ -31,4 +40,4 @@ export interface DrizzleLike {
|
|
|
31
40
|
export type DrizzleExecuteResult = readonly Row[] | {
|
|
32
41
|
readonly rows: readonly Row[];
|
|
33
42
|
};
|
|
34
|
-
export declare function drizzleDataSource(db: DrizzleLike,
|
|
43
|
+
export declare function drizzleDataSource<S extends SchemaRecord>(db: DrizzleLike, schema: Schema<S>): DataSourceAdapter;
|
|
@@ -1,12 +1,21 @@
|
|
|
1
1
|
/**
|
|
2
|
-
* Drizzle Data Source adapter. Same
|
|
2
|
+
* Drizzle Data Source adapter. Same adapter interface + conformance as `prismaDataSource`,
|
|
3
3
|
* built against Drizzle's REAL API (read from drizzle-orm's own source/docs):
|
|
4
4
|
* - `db.transaction(async (tx) => …)` — interactive transaction (commit/rollback).
|
|
5
5
|
* - `db.execute(sql`…`)` — parametrized raw SQL; `sql.identifier()` safely quotes
|
|
6
6
|
* dynamic table/column names, `sql`${value}`` parametrizes values.
|
|
7
|
-
*
|
|
8
|
-
*
|
|
9
|
-
*
|
|
7
|
+
*
|
|
8
|
+
* SCHEMA-DRIVEN COLUMNS. Unlike Prisma — whose delegate applies the model's
|
|
9
|
+
* `@map` for free — this adapter writes raw SQL, so it would otherwise bypass any
|
|
10
|
+
* field→column translation. It therefore derives every table + column name from
|
|
11
|
+
* the SAME rule the provisioner uses (`generateProvisionPlan`):
|
|
12
|
+
* table = `model.tableName ?? key`
|
|
13
|
+
* column = `fieldMeta.column ?? camelToSnake(field)` (+ the model's tenancy column)
|
|
14
|
+
* so `ablo migrate` (which emits `operator_id`) and this adapter (which now writes
|
|
15
|
+
* `operator_id`) COMPOSE. Define the schema once, point Ablo at your Postgres —
|
|
16
|
+
* no hand-written parallel Drizzle table. The adapter is the translation boundary:
|
|
17
|
+
* its public surface (rows in/out, outbox `data`) is field-keyed (the SDK shape);
|
|
18
|
+
* the physical columns it reads/writes are snake_case.
|
|
10
19
|
*
|
|
11
20
|
* IMPORTANT GOTCHAS (from drizzle-orm docs):
|
|
12
21
|
* 1. Interactive `db.transaction` requires a driver that supports it. Neon's
|
|
@@ -19,8 +28,12 @@
|
|
|
19
28
|
* We use `sql` + `db.execute` for ALL writes (not the fluent builder) so the
|
|
20
29
|
* adapter is one small, fully-typed unit with no per-driver builder generics.
|
|
21
30
|
*/
|
|
22
|
-
import { sql
|
|
31
|
+
import { sql } from 'drizzle-orm';
|
|
23
32
|
import { outboxEventSchema } from '../contract.js';
|
|
33
|
+
import { adapterTableMigrations } from '../migrations.js';
|
|
34
|
+
import { toSchemaJSON } from '../../schema/serialize.js';
|
|
35
|
+
import { camelToSnake, snakeToCamel } from '../../schema/ddl.js';
|
|
36
|
+
import { tenancyColumn } from '../../schema/tenancy.js';
|
|
24
37
|
function rowsOf(result) {
|
|
25
38
|
return Array.isArray(result) ? result : result.rows;
|
|
26
39
|
}
|
|
@@ -33,74 +46,99 @@ function rowId(op) {
|
|
|
33
46
|
}
|
|
34
47
|
/** `col1, col2` as a safely-quoted identifier list. */
|
|
35
48
|
const identList = (cols) => sql.join(cols.map((c) => sql.identifier(c)), sql `, `);
|
|
36
|
-
|
|
37
|
-
const
|
|
38
|
-
|
|
39
|
-
|
|
40
|
-
|
|
41
|
-
|
|
49
|
+
function buildColumnMaps(schema) {
|
|
50
|
+
const json = toSchemaJSON(schema);
|
|
51
|
+
const out = new Map();
|
|
52
|
+
for (const [key, model] of Object.entries(json.models)) {
|
|
53
|
+
const fieldToColumn = new Map();
|
|
54
|
+
const columnToField = new Map();
|
|
55
|
+
const register = (field, column) => {
|
|
56
|
+
// The default rule already covers `camelToSnake(field)`; only record real
|
|
57
|
+
// divergences so the reverse map never shadows a clean round-trip.
|
|
58
|
+
if (column === camelToSnake(field))
|
|
59
|
+
return;
|
|
60
|
+
fieldToColumn.set(field, column);
|
|
61
|
+
columnToField.set(column, field);
|
|
62
|
+
};
|
|
63
|
+
for (const [field, meta] of Object.entries(model.fields)) {
|
|
64
|
+
if (meta.column)
|
|
65
|
+
register(field, meta.column);
|
|
66
|
+
}
|
|
67
|
+
const orgColumn = tenancyColumn(model.tenancy);
|
|
68
|
+
if (orgColumn)
|
|
69
|
+
register('organizationId', orgColumn);
|
|
70
|
+
out.set(key, { table: model.tableName ?? key, fieldToColumn, columnToField });
|
|
71
|
+
}
|
|
72
|
+
return out;
|
|
73
|
+
}
|
|
74
|
+
export function drizzleDataSource(db, schema) {
|
|
75
|
+
const maps = buildColumnMaps(schema);
|
|
76
|
+
const modelColumns = (model) => {
|
|
77
|
+
const mc = maps.get(model);
|
|
78
|
+
if (!mc)
|
|
79
|
+
throw new Error(`drizzleDataSource: no model "${model}" in schema`);
|
|
80
|
+
return mc;
|
|
81
|
+
};
|
|
82
|
+
const columnFor = (mc, field) => mc.fieldToColumn.get(field) ?? camelToSnake(field);
|
|
83
|
+
const fieldFor = (mc, column) => mc.columnToField.get(column) ?? snakeToCamel(column);
|
|
84
|
+
/** Field-keyed (SDK shape) → column-keyed (physical), for INSERT/UPDATE. */
|
|
85
|
+
const toColumns = (mc, row) => {
|
|
86
|
+
const out = {};
|
|
87
|
+
for (const k of Object.keys(row))
|
|
88
|
+
out[columnFor(mc, k)] = row[k];
|
|
89
|
+
return out;
|
|
90
|
+
};
|
|
91
|
+
/** Column-keyed (RETURNING * / SELECT *) → field-keyed (SDK shape), for reads + results. */
|
|
92
|
+
const toFields = (mc, row) => {
|
|
93
|
+
const out = {};
|
|
94
|
+
for (const k of Object.keys(row))
|
|
95
|
+
out[fieldFor(mc, k)] = row[k];
|
|
96
|
+
return out;
|
|
42
97
|
};
|
|
43
98
|
const applyOperation = async (tx, op) => {
|
|
44
|
-
const
|
|
99
|
+
const mc = modelColumns(op.model);
|
|
100
|
+
const table = sql.identifier(mc.table);
|
|
45
101
|
const id = rowId(op);
|
|
46
102
|
const input = op.input ?? {};
|
|
47
103
|
if (op.type === 'DELETE') {
|
|
48
104
|
const deleted = rowsOf(await tx.execute(sql `DELETE FROM ${table} WHERE id = ${id} RETURNING *`));
|
|
49
|
-
return deleted[0]
|
|
105
|
+
return deleted[0] ? toFields(mc, deleted[0]) : { id };
|
|
50
106
|
}
|
|
51
107
|
if (op.type === 'CREATE') {
|
|
52
|
-
const data = { id, ...input };
|
|
108
|
+
const data = toColumns(mc, { id, ...input });
|
|
53
109
|
const cols = Object.keys(data);
|
|
54
110
|
const values = sql.join(cols.map((c) => sql `${data[c]}`), sql `, `);
|
|
55
111
|
const inserted = rowsOf(await tx.execute(sql `INSERT INTO ${table} (${identList(cols)}) VALUES (${values}) RETURNING *`));
|
|
56
|
-
return inserted[0]
|
|
112
|
+
return inserted[0] ? toFields(mc, inserted[0]) : { id, ...input };
|
|
57
113
|
}
|
|
58
|
-
// UPDATE / ARCHIVE / UNARCHIVE — a SET clause + the lifecycle
|
|
59
|
-
|
|
114
|
+
// UPDATE / ARCHIVE / UNARCHIVE — a SET clause + the lifecycle field. The
|
|
115
|
+
// lifecycle field is `archivedAt` (camelCase) and goes through `toColumns`
|
|
116
|
+
// like any other, so it lands in `archived_at` — same column the provisioner
|
|
117
|
+
// emits and the Prisma adapter writes (no per-adapter casing divergence).
|
|
118
|
+
const patch = toColumns(mc, {
|
|
60
119
|
...input,
|
|
61
|
-
...(op.type === 'ARCHIVE' ? {
|
|
62
|
-
...(op.type === 'UNARCHIVE' ? {
|
|
63
|
-
};
|
|
120
|
+
...(op.type === 'ARCHIVE' ? { archivedAt: new Date() } : {}),
|
|
121
|
+
...(op.type === 'UNARCHIVE' ? { archivedAt: null } : {}),
|
|
122
|
+
});
|
|
64
123
|
const assignments = sql.join(Object.keys(patch).map((c) => sql `${sql.identifier(c)} = ${patch[c]}`), sql `, `);
|
|
65
124
|
const updated = rowsOf(await tx.execute(sql `UPDATE ${table} SET ${assignments} WHERE id = ${id} RETURNING *`));
|
|
66
|
-
return updated[0]
|
|
125
|
+
return updated[0] ? toFields(mc, updated[0]) : { id, ...input };
|
|
67
126
|
};
|
|
68
127
|
return {
|
|
69
128
|
capabilities: { transactions: true, propose: false, schemaIntrospection: true },
|
|
70
129
|
migrations() {
|
|
71
|
-
return
|
|
72
|
-
{
|
|
73
|
-
name: 'ablo_idempotency',
|
|
74
|
-
up: `CREATE TABLE IF NOT EXISTS ablo_idempotency (
|
|
75
|
-
client_tx_id TEXT PRIMARY KEY,
|
|
76
|
-
response JSONB NOT NULL,
|
|
77
|
-
created_at TIMESTAMPTZ NOT NULL DEFAULT now()
|
|
78
|
-
);`,
|
|
79
|
-
},
|
|
80
|
-
{
|
|
81
|
-
name: 'ablo_outbox',
|
|
82
|
-
up: `CREATE TABLE IF NOT EXISTS ablo_outbox (
|
|
83
|
-
cursor BIGSERIAL PRIMARY KEY,
|
|
84
|
-
id TEXT NOT NULL UNIQUE,
|
|
85
|
-
model TEXT NOT NULL,
|
|
86
|
-
entity_id TEXT NOT NULL,
|
|
87
|
-
type TEXT NOT NULL,
|
|
88
|
-
data JSONB,
|
|
89
|
-
organization_id TEXT,
|
|
90
|
-
client_tx_id TEXT,
|
|
91
|
-
occurred_at BIGINT,
|
|
92
|
-
created_at TIMESTAMPTZ NOT NULL DEFAULT now()
|
|
93
|
-
);`,
|
|
94
|
-
},
|
|
95
|
-
];
|
|
130
|
+
return adapterTableMigrations();
|
|
96
131
|
},
|
|
97
132
|
async read(req) {
|
|
98
|
-
const
|
|
133
|
+
const mc = modelColumns(req.model);
|
|
134
|
+
const table = sql.identifier(mc.table);
|
|
99
135
|
if (req.kind === 'load') {
|
|
100
|
-
|
|
136
|
+
const rows = rowsOf(await db.execute(sql `SELECT * FROM ${table} WHERE id = ${req.id} LIMIT 1`));
|
|
137
|
+
return rows.map((r) => toFields(mc, r));
|
|
101
138
|
}
|
|
102
139
|
const limit = req.query?.limit ?? 1000;
|
|
103
|
-
|
|
140
|
+
const rows = rowsOf(await db.execute(sql `SELECT * FROM ${table} LIMIT ${limit}`));
|
|
141
|
+
return rows.map((r) => toFields(mc, r));
|
|
104
142
|
},
|
|
105
143
|
async commit(change) {
|
|
106
144
|
return db.transaction(async (tx) => {
|
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
/**
|
|
2
2
|
* In-memory reference Data Source adapter — the canonical correct implementation
|
|
3
|
-
* of the
|
|
3
|
+
* of the adapter interface. It is the test double for the bridge/handler AND the thing the
|
|
4
4
|
* conformance suite runs against to prove the suite itself is real (same role as
|
|
5
5
|
* the server's `memoryTenantDirectory`). A new ORM adapter is "done" when it
|
|
6
6
|
* passes the same suite this one passes.
|
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
/**
|
|
2
2
|
* In-memory reference Data Source adapter — the canonical correct implementation
|
|
3
|
-
* of the
|
|
3
|
+
* of the adapter interface. It is the test double for the bridge/handler AND the thing the
|
|
4
4
|
* conformance suite runs against to prove the suite itself is real (same role as
|
|
5
5
|
* the server's `memoryTenantDirectory`). A new ORM adapter is "done" when it
|
|
6
6
|
* passes the same suite this one passes.
|
|
@@ -62,7 +62,7 @@ export function memoryDataSource() {
|
|
|
62
62
|
return {
|
|
63
63
|
capabilities: { transactions: true, propose: false, schemaIntrospection: false },
|
|
64
64
|
migrations() {
|
|
65
|
-
// In-memory: no
|
|
65
|
+
// In-memory: no table-creation SQL. A real ORM adapter ships ablo_idempotency + ablo_outbox here.
|
|
66
66
|
return [];
|
|
67
67
|
},
|
|
68
68
|
async read(req) {
|
|
@@ -1,12 +1,12 @@
|
|
|
1
1
|
/**
|
|
2
2
|
* Prisma Data Source adapter. The first real ORM adapter (Auth.js pattern: one
|
|
3
|
-
* package per ORM, all behind the `DataSourceAdapter`
|
|
3
|
+
* package per ORM, all behind the `DataSourceAdapter` interface, all proven by the
|
|
4
4
|
* same conformance suite the in-memory reference passes).
|
|
5
5
|
*
|
|
6
6
|
* It owns the transactional outbox + idempotency so the customer never writes
|
|
7
7
|
* them: `commit` runs the app-row mutations, the `ablo_outbox` append, and the
|
|
8
8
|
* `ablo_idempotency` record in ONE `prisma.$transaction`. `migrations()` ships
|
|
9
|
-
* the
|
|
9
|
+
* the table-creation SQL for those two tables.
|
|
10
10
|
*
|
|
11
11
|
* No `@prisma/client` dependency: the client is accepted structurally
|
|
12
12
|
* (`PrismaLike`), so this compiles in the SDK package and is unit-testable with
|
|
@@ -46,7 +46,7 @@ export interface PrismaRaw {
|
|
|
46
46
|
$executeRawUnsafe(query: string, ...values: unknown[]): Promise<number>;
|
|
47
47
|
$queryRawUnsafe<T = unknown>(query: string, ...values: unknown[]): Promise<T>;
|
|
48
48
|
}
|
|
49
|
-
/** A Prisma client (or interactive-
|
|
49
|
+
/** A Prisma client (or interactive-transaction client) — structural, no SDK dependency. */
|
|
50
50
|
export interface PrismaLike extends PrismaRaw {
|
|
51
51
|
$transaction<T>(fn: (tx: PrismaLike & PrismaRaw) => Promise<T>): Promise<T>;
|
|
52
52
|
}
|
|
@@ -1,18 +1,19 @@
|
|
|
1
1
|
/**
|
|
2
2
|
* Prisma Data Source adapter. The first real ORM adapter (Auth.js pattern: one
|
|
3
|
-
* package per ORM, all behind the `DataSourceAdapter`
|
|
3
|
+
* package per ORM, all behind the `DataSourceAdapter` interface, all proven by the
|
|
4
4
|
* same conformance suite the in-memory reference passes).
|
|
5
5
|
*
|
|
6
6
|
* It owns the transactional outbox + idempotency so the customer never writes
|
|
7
7
|
* them: `commit` runs the app-row mutations, the `ablo_outbox` append, and the
|
|
8
8
|
* `ablo_idempotency` record in ONE `prisma.$transaction`. `migrations()` ships
|
|
9
|
-
* the
|
|
9
|
+
* the table-creation SQL for those two tables.
|
|
10
10
|
*
|
|
11
11
|
* No `@prisma/client` dependency: the client is accepted structurally
|
|
12
12
|
* (`PrismaLike`), so this compiles in the SDK package and is unit-testable with
|
|
13
13
|
* a fake, while a real `PrismaClient` satisfies it at the call site.
|
|
14
14
|
*/
|
|
15
15
|
import { outboxEventSchema } from '../contract.js';
|
|
16
|
+
import { adapterTableMigrations } from '../migrations.js';
|
|
16
17
|
const lowerFirst = (s) => (s ? s[0].toLowerCase() + s.slice(1) : s);
|
|
17
18
|
/**
|
|
18
19
|
* Resolve a model's Prisma delegate by name. This is the ONE irreducible cast in
|
|
@@ -20,7 +21,7 @@ const lowerFirst = (s) => (s ? s[0].toLowerCase() + s.slice(1) : s);
|
|
|
20
21
|
*
|
|
21
22
|
* - Inside `prisma.$transaction(tx => …)` the writes MUST go through the
|
|
22
23
|
* transactional client `tx`, and the model is only known as a runtime string.
|
|
23
|
-
* - Prisma's client
|
|
24
|
+
* - Prisma's client (and transaction handle) is NOMINALLY keyed (`{ task: TaskDelegate; … }`), so a
|
|
24
25
|
* dynamic `tx[name]` is `unknown` to the compiler — there is no key to infer.
|
|
25
26
|
*
|
|
26
27
|
* Dynamic property access on a statically-keyed type cannot be typed without an
|
|
@@ -122,31 +123,7 @@ export function prismaDataSource(prisma, schema, options = {}) {
|
|
|
122
123
|
return {
|
|
123
124
|
capabilities: { transactions: true, propose: false, schemaIntrospection: true },
|
|
124
125
|
migrations() {
|
|
125
|
-
return
|
|
126
|
-
{
|
|
127
|
-
name: 'ablo_idempotency',
|
|
128
|
-
up: `CREATE TABLE IF NOT EXISTS ablo_idempotency (
|
|
129
|
-
client_tx_id TEXT PRIMARY KEY,
|
|
130
|
-
response JSONB NOT NULL,
|
|
131
|
-
created_at TIMESTAMPTZ NOT NULL DEFAULT now()
|
|
132
|
-
);`,
|
|
133
|
-
},
|
|
134
|
-
{
|
|
135
|
-
name: 'ablo_outbox',
|
|
136
|
-
up: `CREATE TABLE IF NOT EXISTS ablo_outbox (
|
|
137
|
-
cursor BIGSERIAL PRIMARY KEY,
|
|
138
|
-
id TEXT NOT NULL UNIQUE,
|
|
139
|
-
model TEXT NOT NULL,
|
|
140
|
-
entity_id TEXT NOT NULL,
|
|
141
|
-
type TEXT NOT NULL,
|
|
142
|
-
data JSONB,
|
|
143
|
-
organization_id TEXT,
|
|
144
|
-
client_tx_id TEXT,
|
|
145
|
-
occurred_at BIGINT,
|
|
146
|
-
created_at TIMESTAMPTZ NOT NULL DEFAULT now()
|
|
147
|
-
);`,
|
|
148
|
-
},
|
|
149
|
-
];
|
|
126
|
+
return adapterTableMigrations();
|
|
150
127
|
},
|
|
151
128
|
async read(req) {
|
|
152
129
|
const delegate = delegateFor(prisma, delegateName(req.model));
|
|
@@ -167,7 +144,7 @@ export function prismaDataSource(prisma, schema, options = {}) {
|
|
|
167
144
|
const row = await applyOperation(tx, op);
|
|
168
145
|
rows.push(row);
|
|
169
146
|
const entityId = String(row.id ?? rowId(op));
|
|
170
|
-
// Transactional outbox: one event per op, written in THIS
|
|
147
|
+
// Transactional outbox: one event per op, written in THIS transaction.
|
|
171
148
|
await tx.$executeRawUnsafe(`INSERT INTO ablo_outbox (id, model, entity_id, type, data, client_tx_id, occurred_at)
|
|
172
149
|
VALUES ($1, $2, $3, $4, $5::jsonb, $6, $7)`, `${change.clientTxId}:${index}`, op.model, entityId, op.type, JSON.stringify(op.type === 'DELETE' ? null : row), change.clientTxId, Date.now());
|
|
173
150
|
}
|
|
@@ -2,7 +2,7 @@
|
|
|
2
2
|
* Data Source adapter conformance suite — the shared "is this adapter correct?"
|
|
3
3
|
* test set, in the Auth.js `@auth/adapter-test` mould. Every ORM adapter
|
|
4
4
|
* (Prisma/Drizzle/Kysely) and any hand-written handler runs THIS to prove,
|
|
5
|
-
* before production, the guarantees the
|
|
5
|
+
* before production, the guarantees the adapter interface promises. A new adapter is "done"
|
|
6
6
|
* when it passes — not when it compiles.
|
|
7
7
|
*
|
|
8
8
|
* Runner-agnostic: checks are plain async functions that throw (node:assert) on
|
|
@@ -2,7 +2,7 @@
|
|
|
2
2
|
* Data Source adapter conformance suite — the shared "is this adapter correct?"
|
|
3
3
|
* test set, in the Auth.js `@auth/adapter-test` mould. Every ORM adapter
|
|
4
4
|
* (Prisma/Drizzle/Kysely) and any hand-written handler runs THIS to prove,
|
|
5
|
-
* before production, the guarantees the
|
|
5
|
+
* before production, the guarantees the adapter interface promises. A new adapter is "done"
|
|
6
6
|
* when it passes — not when it compiles.
|
|
7
7
|
*
|
|
8
8
|
* Runner-agnostic: checks are plain async functions that throw (node:assert) on
|
|
@@ -109,7 +109,7 @@ export function dataSourceConformanceChecks(make) {
|
|
|
109
109
|
},
|
|
110
110
|
},
|
|
111
111
|
{
|
|
112
|
-
name: 'a later UPDATE under a new clientTxId is applied (idempotency is per-
|
|
112
|
+
name: 'a later UPDATE under a new clientTxId is applied (idempotency is per-transaction)',
|
|
113
113
|
run: async () => {
|
|
114
114
|
const adapter = await make();
|
|
115
115
|
await adapter.commit(change('tx_c1', [{ type: 'CREATE', model: 'task', id: 't1', input: { title: 'A' } }]));
|
|
@@ -126,8 +126,9 @@ export declare const eventsPageSchema: z.ZodObject<{
|
|
|
126
126
|
}, z.core.$strip>;
|
|
127
127
|
export type EventsPage = z.infer<typeof eventsPageSchema>;
|
|
128
128
|
/**
|
|
129
|
-
* A
|
|
130
|
-
* `ablo_idempotency` / `ablo_outbox` tables —
|
|
129
|
+
* A table-creation migration an adapter ships so a customer never hand-writes the
|
|
130
|
+
* `ablo_idempotency` / `ablo_outbox` tables — the adapter returns them from
|
|
131
|
+
* `migrations()`.
|
|
131
132
|
*/
|
|
132
133
|
export declare const migrationSchema: z.ZodObject<{
|
|
133
134
|
name: z.ZodString;
|
package/dist/source/contract.js
CHANGED
|
@@ -73,8 +73,9 @@ export const eventsPageSchema = z.object({
|
|
|
73
73
|
nextCursor: z.string().nullable(),
|
|
74
74
|
});
|
|
75
75
|
/**
|
|
76
|
-
* A
|
|
77
|
-
* `ablo_idempotency` / `ablo_outbox` tables —
|
|
76
|
+
* A table-creation migration an adapter ships so a customer never hand-writes the
|
|
77
|
+
* `ablo_idempotency` / `ablo_outbox` tables — the adapter returns them from
|
|
78
|
+
* `migrations()`.
|
|
78
79
|
*/
|
|
79
80
|
export const migrationSchema = z.object({
|
|
80
81
|
/** Stable name, used as the migration filename + applied-ledger key. */
|
package/dist/source/index.d.ts
CHANGED
|
@@ -466,3 +466,4 @@ export { createPushQueue, InMemoryPushQueueStorage, STANDARD_WEBHOOKS_RETRY_SCHE
|
|
|
466
466
|
export { type DataSourceAdapter, type AdapterReadRequest, type AdapterCommitResult, type Row as AdapterRow, } from './adapter.js';
|
|
467
467
|
export { operationSchema, operationTypeSchema, changeSetSchema, outboxEventSchema, eventsPageSchema, migrationSchema, adapterCapabilitiesSchema, type Operation, type ChangeSet, type OutboxEvent, type EventsPage, type Migration, type AdapterCapabilities, } from './contract.js';
|
|
468
468
|
export { prismaDataSource, type PrismaLike, type PrismaDataSourceOptions } from './adapters/prisma.js';
|
|
469
|
+
export { adapterTableMigrations } from './migrations.js';
|
package/dist/source/index.js
CHANGED
|
@@ -59,8 +59,8 @@ function json(data, status = 200) {
|
|
|
59
59
|
});
|
|
60
60
|
}
|
|
61
61
|
/**
|
|
62
|
-
* Serve a request from an ORM `adapter`. Routes the four operations to the
|
|
63
|
-
* (`read`/`commit`/`events`) and shapes the wire response. The adapter is the
|
|
62
|
+
* Serve a request from an ORM `adapter`. Routes the four operations to the adapter
|
|
63
|
+
* interface (`read`/`commit`/`events`) and shapes the wire response. The adapter is the
|
|
64
64
|
* single point of dispatch — no per-model branching here.
|
|
65
65
|
*/
|
|
66
66
|
async function handleViaAdapter(adapter, body, scope) {
|
|
@@ -419,3 +419,4 @@ export function dataSource(options) {
|
|
|
419
419
|
export { createPushQueue, InMemoryPushQueueStorage, STANDARD_WEBHOOKS_RETRY_SCHEDULE, } from './pushQueue.js';
|
|
420
420
|
export { operationSchema, operationTypeSchema, changeSetSchema, outboxEventSchema, eventsPageSchema, migrationSchema, adapterCapabilitiesSchema, } from './contract.js';
|
|
421
421
|
export { prismaDataSource } from './adapters/prisma.js';
|
|
422
|
+
export { adapterTableMigrations } from './migrations.js';
|
|
@@ -0,0 +1,14 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* The table-creation SQL every ORM adapter ships for its OWN infrastructure tables —
|
|
3
|
+
* `ablo_idempotency` (dedupe by clientTxId) and `ablo_outbox` (transactional
|
|
4
|
+
* outbox the `events()` feed reads). Defined ONCE here so the Prisma adapter, the
|
|
5
|
+
* Drizzle adapter, and `ablo migrate` can never disagree on the shape (they used
|
|
6
|
+
* to inline their own copies, which had already drifted in whitespace).
|
|
7
|
+
*
|
|
8
|
+
* These are NOT model tables and are NOT emitted by the hosted provisioner
|
|
9
|
+
* (`generateProvisionPlan`) — the hosted path uses `sync_deltas` directly. They
|
|
10
|
+
* exist only on a customer's own database in Data Source mode.
|
|
11
|
+
*/
|
|
12
|
+
import type { Migration } from './contract.js';
|
|
13
|
+
/** Canonical adapter-owned table-creation SQL. Idempotent (`IF NOT EXISTS`). */
|
|
14
|
+
export declare function adapterTableMigrations(): readonly Migration[];
|