@abloatai/transaction 0.52.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/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/index.d.ts +9 -1
- package/dist/auth/index.d.ts.map +1 -1
- package/dist/auth/index.js +10 -1
- package/dist/auth/index.js.map +1 -1
- package/dist/branches.d.ts +31 -30
- package/dist/branches.d.ts.map +1 -1
- package/dist/branches.js +2 -4
- package/dist/branches.js.map +1 -1
- package/dist/errorCodes.d.ts +4 -1
- package/dist/errorCodes.d.ts.map +1 -1
- package/dist/errorCodes.js +4 -1
- package/dist/errorCodes.js.map +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 +32 -0
- 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 +12 -0
- 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/openapi.d.ts.map +1 -1
- package/dist/schema/openapi.js +59 -14
- package/dist/schema/openapi.js.map +1 -1
- package/dist/server/adapter.d.ts +2 -0
- package/dist/server/adapter.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/transport/httpClient.d.ts +7 -2
- 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.map +1 -1
- package/dist/transport/httpTransport.js +29 -7
- package/dist/transport/httpTransport.js.map +1 -1
- package/dist/wire/accountResponses.d.ts +18 -13
- package/dist/wire/accountResponses.d.ts.map +1 -1
- package/dist/wire/accountResponses.js +3 -9
- 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/index.d.ts +1 -1
- 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/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/package.json +1 -1
- package/src/auth/apiKey.ts +13 -61
- package/src/auth/baseUrl.ts +144 -0
- package/src/auth/index.ts +11 -1
- package/src/branches.ts +2 -4
- package/src/errorCodes.ts +19 -1
- package/src/index.ts +2 -0
- package/src/resources/httpResources.ts +42 -0
- package/src/resources/modelOperations.ts +12 -0
- package/src/resources/where.ts +70 -4
- package/src/schema/openapi.ts +60 -14
- package/src/server/adapter.ts +2 -0
- package/src/server/index.ts +8 -2
- package/src/server/readConfig.ts +111 -57
- package/src/transport/httpClient.ts +17 -4
- package/src/transport/httpTransport.ts +29 -7
- package/src/wire/accountResponses.ts +3 -9
- package/src/wire/claims.ts +7 -1
- package/src/wire/index.ts +7 -1
- package/src/wire/listEnvelope.ts +23 -0
package/src/schema/openapi.ts
CHANGED
|
@@ -36,6 +36,7 @@ import {
|
|
|
36
36
|
commitRecordListSchema,
|
|
37
37
|
commitRecordWhereSchema,
|
|
38
38
|
} from '../wire/commit.js';
|
|
39
|
+
import { CURSOR_PARAM, CURSOR_PARAM_ALIAS } from '../wire/listEnvelope.js';
|
|
39
40
|
import {
|
|
40
41
|
claimRequestSchema,
|
|
41
42
|
claimHeartbeatRequestSchema,
|
|
@@ -185,6 +186,43 @@ function fieldSchema(f: FieldMeta): Json {
|
|
|
185
186
|
|
|
186
187
|
const pascal = (s: string): string => s.charAt(0).toUpperCase() + s.slice(1);
|
|
187
188
|
const idParam = (): Json => ({ name: 'id', in: 'path', required: true, schema: { type: 'string' } });
|
|
189
|
+
const idempotencyKeyParam = (): Json => ({
|
|
190
|
+
name: 'Idempotency-Key',
|
|
191
|
+
in: 'header',
|
|
192
|
+
schema: { type: 'string', maxLength: 255 },
|
|
193
|
+
description: 'Replay identity. Reuse the same key only for an identical request.',
|
|
194
|
+
});
|
|
195
|
+
const collectionPageParams = (): Json[] => [
|
|
196
|
+
{
|
|
197
|
+
name: 'limit',
|
|
198
|
+
in: 'query',
|
|
199
|
+
schema: { type: 'integer', minimum: 1, maximum: 100, default: 20 },
|
|
200
|
+
},
|
|
201
|
+
cursorParam(),
|
|
202
|
+
retiredCursorParam(),
|
|
203
|
+
];
|
|
204
|
+
|
|
205
|
+
/** The cursor parameter, named from the wire contract that issues its value. */
|
|
206
|
+
const cursorParam = (): Json => ({
|
|
207
|
+
name: CURSOR_PARAM,
|
|
208
|
+
in: 'query',
|
|
209
|
+
schema: { type: 'string' },
|
|
210
|
+
description: 'The opaque next_cursor returned by the preceding page.',
|
|
211
|
+
});
|
|
212
|
+
|
|
213
|
+
/**
|
|
214
|
+
* The retired spelling, documented as deprecated so a caller still sending it
|
|
215
|
+
* can see it is going. Declared once: `queryParams` reaches for it too, and a
|
|
216
|
+
* second copy is how the model list came to document the same parameter without
|
|
217
|
+
* the deprecation the collection routes carried.
|
|
218
|
+
*/
|
|
219
|
+
const retiredCursorParam = (): Json => ({
|
|
220
|
+
name: CURSOR_PARAM_ALIAS,
|
|
221
|
+
in: 'query',
|
|
222
|
+
deprecated: true,
|
|
223
|
+
schema: { type: 'string' },
|
|
224
|
+
description: `Deprecated spelling of ${CURSOR_PARAM}, honoured until it is removed. Send ${CURSOR_PARAM}.`,
|
|
225
|
+
});
|
|
188
226
|
const jsonBody = (schema: Json): Json => ({
|
|
189
227
|
required: true,
|
|
190
228
|
content: { 'application/json': { schema } },
|
|
@@ -313,16 +351,22 @@ function withGenericRows(derived: Json): Json {
|
|
|
313
351
|
*/
|
|
314
352
|
function queryParams(schema: z.ZodType): Json[] {
|
|
315
353
|
const props = (derive(schema, 'input').properties ?? {}) as Record<string, Json>;
|
|
316
|
-
return Object.entries(props).map(([name, s]) =>
|
|
317
|
-
|
|
318
|
-
in
|
|
319
|
-
//
|
|
320
|
-
|
|
321
|
-
|
|
322
|
-
|
|
323
|
-
|
|
324
|
-
|
|
325
|
-
|
|
354
|
+
return Object.entries(props).map(([name, s]) => {
|
|
355
|
+
// The retired cursor spelling is documented the same way wherever it
|
|
356
|
+
// appears. Derived from the schema it still lives in, so the flag cannot be
|
|
357
|
+
// present on one route's copy of the parameter and absent on another's.
|
|
358
|
+
if (name === CURSOR_PARAM_ALIAS) return retiredCursorParam();
|
|
359
|
+
return {
|
|
360
|
+
name,
|
|
361
|
+
in: 'query',
|
|
362
|
+
// Query strings arrive at the server as text, but OpenAPI describes the
|
|
363
|
+
// caller-facing value before serialization. Generated clients should take
|
|
364
|
+
// an integer here and encode it, not expose a stringly typed page size.
|
|
365
|
+
schema: name === 'limit'
|
|
366
|
+
? { type: 'integer', minimum: 1 }
|
|
367
|
+
: s,
|
|
368
|
+
};
|
|
369
|
+
});
|
|
326
370
|
}
|
|
327
371
|
|
|
328
372
|
const commitRecordListQuerySchema = commitRecordWhereSchema.safeExtend({
|
|
@@ -492,8 +536,8 @@ export function abloOpenApi(options: SchemaToOpenApiOptions = {}): Json {
|
|
|
492
536
|
parameters: [modelParam(), ...queryParams(listQuerySchema)],
|
|
493
537
|
responses: {
|
|
494
538
|
'200': namedResp(
|
|
495
|
-
'A page of rows. `next_cursor` feeds `
|
|
496
|
-
'
|
|
539
|
+
'A page of rows. `next_cursor` feeds `cursor` on the next call; ' +
|
|
540
|
+
'`stamp` is the watermark the page was read at.',
|
|
497
541
|
'ModelPage',
|
|
498
542
|
),
|
|
499
543
|
},
|
|
@@ -599,6 +643,7 @@ export function abloOpenApi(options: SchemaToOpenApiOptions = {}): Json {
|
|
|
599
643
|
get: {
|
|
600
644
|
tags: ['branches'],
|
|
601
645
|
summary: 'List transaction branches for the credential project',
|
|
646
|
+
parameters: collectionPageParams(),
|
|
602
647
|
responses: {
|
|
603
648
|
'200': jsonResp(
|
|
604
649
|
'The root and every active child branch.',
|
|
@@ -611,6 +656,7 @@ export function abloOpenApi(options: SchemaToOpenApiOptions = {}): Json {
|
|
|
611
656
|
summary: 'Create an isolated child branch',
|
|
612
657
|
description:
|
|
613
658
|
'The returned id is immutable; retain it for automation. The slug is a project-scoped human handle.',
|
|
659
|
+
parameters: [idempotencyKeyParam()],
|
|
614
660
|
requestBody: jsonBody(derive(createBranchRequestSchema, 'input')),
|
|
615
661
|
responses: {
|
|
616
662
|
'201': jsonResp('The ready branch.', derive(branchResponseSchema, 'output')),
|
|
@@ -866,7 +912,7 @@ export function abloOpenApi(options: SchemaToOpenApiOptions = {}): Json {
|
|
|
866
912
|
post: {
|
|
867
913
|
tags: ['commits'],
|
|
868
914
|
summary: 'Commit a batch of operations atomically, and/or register durable premises',
|
|
869
|
-
parameters: [
|
|
915
|
+
parameters: [idempotencyKeyParam()],
|
|
870
916
|
requestBody: commitBody(),
|
|
871
917
|
responses: { '200': commitReceipt() },
|
|
872
918
|
},
|
|
@@ -993,7 +1039,7 @@ export function schemaToOpenApi<S extends SchemaRecord>(
|
|
|
993
1039
|
post: {
|
|
994
1040
|
tags: ['commits'],
|
|
995
1041
|
summary: 'Commit a batch of operations atomically, and/or register durable premises',
|
|
996
|
-
parameters: [
|
|
1042
|
+
parameters: [idempotencyKeyParam()],
|
|
997
1043
|
requestBody: commitBody(),
|
|
998
1044
|
responses: { '200': commitReceipt() },
|
|
999
1045
|
},
|
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/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
|
}
|
|
@@ -32,10 +32,12 @@ import type {
|
|
|
32
32
|
HttpTransportModel,
|
|
33
33
|
ModelReadOptions,
|
|
34
34
|
ModelMutationOptions,
|
|
35
|
+
ModelList,
|
|
35
36
|
CreateSessionParams,
|
|
36
37
|
AbloSession,
|
|
37
38
|
SessionResource,
|
|
38
39
|
} from '../resources/httpResources.js';
|
|
40
|
+
import { modelList } from '../resources/httpResources.js';
|
|
39
41
|
import type {
|
|
40
42
|
ModelCreateParams,
|
|
41
43
|
ModelDeleteParams,
|
|
@@ -128,7 +130,12 @@ export interface HttpModelClient<T, C = T> {
|
|
|
128
130
|
* Reads the rows matching a filter. Same resolution as `get`, in bulk,
|
|
129
131
|
* and deduplicated so concurrent identical calls share one request.
|
|
130
132
|
*/
|
|
131
|
-
|
|
133
|
+
/**
|
|
134
|
+
* The rows are an array as before; `hasMore` and `nextCursor` on the result
|
|
135
|
+
* say whether the collection continues past this page. Pass `nextCursor`
|
|
136
|
+
* back as `cursor`, with the same `where` and `orderBy`, to walk it.
|
|
137
|
+
*/
|
|
138
|
+
list(options?: ServerReadOptions<T>): Promise<ModelList<CapturedRow<T>>>;
|
|
132
139
|
/**
|
|
133
140
|
* Creates a row and returns it, including any framework-applied defaults.
|
|
134
141
|
* Passing an id that already exists is idempotent: the existing row is
|
|
@@ -427,10 +434,16 @@ function createHttpModelClient<T, C = T>(
|
|
|
427
434
|
return read.data as CapturedRow<T> | undefined;
|
|
428
435
|
};
|
|
429
436
|
|
|
430
|
-
const list = async (
|
|
437
|
+
const list = async (
|
|
438
|
+
options?: ServerReadOptions<T>,
|
|
439
|
+
): Promise<ModelList<CapturedRow<T>>> => {
|
|
431
440
|
const snapshot = await protocol.list(options);
|
|
441
|
+
const page = modelList<CapturedRow<T>>(
|
|
442
|
+
snapshot.data as readonly CapturedRow<T>[],
|
|
443
|
+
snapshot,
|
|
444
|
+
);
|
|
432
445
|
const registry = readSetContext?.getStore();
|
|
433
|
-
if (!registry) return
|
|
446
|
+
if (!registry) return page;
|
|
434
447
|
if (!snapshot.evidence) {
|
|
435
448
|
throw new AbloConnectionError(
|
|
436
449
|
`${modelName}.list did not return row evidence. Upgrade the Ablo server or use get({ id }).`,
|
|
@@ -451,7 +464,7 @@ function createHttpModelClient<T, C = T>(
|
|
|
451
464
|
}
|
|
452
465
|
capturePointRead(readSetContext, clientIdentity, modelName, id, row, stamp);
|
|
453
466
|
}
|
|
454
|
-
return
|
|
467
|
+
return page;
|
|
455
468
|
};
|
|
456
469
|
|
|
457
470
|
// Claim acquisition performs its authoritative read only after the grant.
|
|
@@ -84,6 +84,7 @@ import type {
|
|
|
84
84
|
ModelMutationOptions,
|
|
85
85
|
ModelReadOptions,
|
|
86
86
|
HttpTransportRead,
|
|
87
|
+
HttpTransportList,
|
|
87
88
|
HttpLogsResource,
|
|
88
89
|
ModelTarget,
|
|
89
90
|
CreateSessionParams,
|
|
@@ -151,6 +152,7 @@ import { declaredMeta, wireMeta } from '../coordination/claimMeta.js';
|
|
|
151
152
|
import type { Claim, ClaimHeartbeat, ClaimHeartbeatOptions, HeldClaim } from '../types/streams.js';
|
|
152
153
|
import type { CoordinationObservability } from '../observability.js';
|
|
153
154
|
import { assertWriteOptions } from '../resources/writeOptionsSchema.js';
|
|
155
|
+
import { normalizeWhere } from '../resources/where.js';
|
|
154
156
|
import {
|
|
155
157
|
createDurableHttpCommitEnvelope,
|
|
156
158
|
canonicalHttpCommitBody,
|
|
@@ -1493,9 +1495,12 @@ export function createHttpTransport(options: HttpTransportOptions): HttpTranspor
|
|
|
1493
1495
|
async function listModel<T>(
|
|
1494
1496
|
modelName: string,
|
|
1495
1497
|
options?: ServerReadOptions<T>,
|
|
1496
|
-
): Promise<
|
|
1498
|
+
): Promise<HttpTransportList<T>> {
|
|
1497
1499
|
const params = new URLSearchParams();
|
|
1498
1500
|
if (options?.limit !== undefined) params.set('limit', String(options.limit));
|
|
1501
|
+
if (options?.cursor !== undefined) {
|
|
1502
|
+
params.set('cursor', options.cursor);
|
|
1503
|
+
}
|
|
1499
1504
|
if (options?.orderBy) {
|
|
1500
1505
|
const [col, dir] = Object.entries(options.orderBy)[0] ?? [];
|
|
1501
1506
|
if (col) {
|
|
@@ -1503,12 +1508,27 @@ export function createHttpTransport(options: HttpTransportOptions): HttpTranspor
|
|
|
1503
1508
|
if (dir === 'desc') params.set('order', 'desc');
|
|
1504
1509
|
}
|
|
1505
1510
|
}
|
|
1506
|
-
// The
|
|
1507
|
-
//
|
|
1508
|
-
//
|
|
1509
|
-
|
|
1510
|
-
|
|
1511
|
-
|
|
1511
|
+
// The whole filter travels as the canonical clause list, so every operator
|
|
1512
|
+
// the grammar declares survives the trip. The previous encoding walked the
|
|
1513
|
+
// object's own entries and skipped any value that was an object, which
|
|
1514
|
+
// silently discarded `IN` filters and every tuple-form clause: the request
|
|
1515
|
+
// went out unfiltered and the caller read the result as a filtered one.
|
|
1516
|
+
const clauses = normalizeWhere(options?.where);
|
|
1517
|
+
if (clauses.length > 0) {
|
|
1518
|
+
params.set('where', JSON.stringify(clauses));
|
|
1519
|
+
// Plain equality also goes out in the older `?column=value` shorthand.
|
|
1520
|
+
// A server that predates the `where` parameter does not reserve the name
|
|
1521
|
+
// and does not declare it as a column, so it skips it — and a dropped
|
|
1522
|
+
// filter is not an error there, it is a wider answer than the caller
|
|
1523
|
+
// asked for. Sending both means a version skew loses only the operator
|
|
1524
|
+
// filters, which that server could not have honoured anyway. A current
|
|
1525
|
+
// server ignores a shorthand key the clause list already carries.
|
|
1526
|
+
for (const clause of clauses) {
|
|
1527
|
+
const [column] = clause;
|
|
1528
|
+
const value = clause.length === 2 ? clause[1] : clause[2];
|
|
1529
|
+
if (clause.length === 3 && clause[1] !== '=') continue;
|
|
1530
|
+
if (value === null || typeof value === 'object') continue;
|
|
1531
|
+
params.set(column, String(value));
|
|
1512
1532
|
}
|
|
1513
1533
|
}
|
|
1514
1534
|
const qs = params.toString();
|
|
@@ -1523,6 +1543,8 @@ export function createHttpTransport(options: HttpTransportOptions): HttpTranspor
|
|
|
1523
1543
|
// the typed facade above, which holds the model's schema.
|
|
1524
1544
|
return {
|
|
1525
1545
|
data: res.data as T[],
|
|
1546
|
+
hasMore: res.has_more,
|
|
1547
|
+
nextCursor: res.next_cursor,
|
|
1526
1548
|
...(res.evidence ? { evidence: res.evidence } : {}),
|
|
1527
1549
|
};
|
|
1528
1550
|
}
|
|
@@ -14,6 +14,7 @@
|
|
|
14
14
|
*/
|
|
15
15
|
|
|
16
16
|
import { z } from 'zod';
|
|
17
|
+
import { listEnvelopeSchema } from './listEnvelope.js';
|
|
17
18
|
// Composed, never restated: these are the artifact's own shapes, and a
|
|
18
19
|
// hand-written mirror here would be a second copy of the exact record this
|
|
19
20
|
// response exists to stop withholding.
|
|
@@ -45,16 +46,9 @@ export type ProjectResponse = z.infer<typeof projectResponseSchema>;
|
|
|
45
46
|
/**
|
|
46
47
|
* `GET /v1/projects`.
|
|
47
48
|
*
|
|
48
|
-
*
|
|
49
|
-
* are returned whole, so the route emits `data` with no `has_more`/`next_cursor`
|
|
50
|
-
* beside it. That divergence is real and this schema states it rather than
|
|
51
|
-
* describing a pagination the endpoint does not implement — adding the two
|
|
52
|
-
* fields is an API change, not a documentation fix.
|
|
49
|
+
* Uses the canonical paginated list envelope shared by every collection.
|
|
53
50
|
*/
|
|
54
|
-
export const projectListResponseSchema =
|
|
55
|
-
object: z.literal('list'),
|
|
56
|
-
data: z.array(projectResponseSchema).readonly(),
|
|
57
|
-
});
|
|
51
|
+
export const projectListResponseSchema = listEnvelopeSchema(projectResponseSchema);
|
|
58
52
|
export type ProjectListResponse = z.infer<typeof projectListResponseSchema>;
|
|
59
53
|
|
|
60
54
|
/** One minted key, returned exactly once — the plaintext is never readable again. */
|
package/src/wire/claims.ts
CHANGED
|
@@ -121,7 +121,13 @@ export const listQuerySchema = z.object({
|
|
|
121
121
|
limit: z.string().optional(),
|
|
122
122
|
order_by: z.string().optional(),
|
|
123
123
|
order: z.enum(['asc', 'desc']).optional(),
|
|
124
|
-
/**
|
|
124
|
+
/**
|
|
125
|
+
* Keyset cursor: the opaque `next_cursor` the previous page returned. It
|
|
126
|
+
* encodes the sort position it was issued for, so it is not a row id and is
|
|
127
|
+
* refused against a different `order_by`/`order`.
|
|
128
|
+
*/
|
|
129
|
+
cursor: z.string().optional(),
|
|
130
|
+
/** @deprecated The pre-0.53.0 spelling of `cursor`. Send `cursor`. */
|
|
125
131
|
starting_after: z.string().optional(),
|
|
126
132
|
});
|
|
127
133
|
export type ListQuery = z.infer<typeof listQuerySchema>;
|
package/src/wire/index.ts
CHANGED
|
@@ -22,7 +22,13 @@ export {
|
|
|
22
22
|
INTERNAL_ERROR_PUBLIC_MESSAGE,
|
|
23
23
|
} from './errorEnvelope.js';
|
|
24
24
|
export type { ErrorEnvelope } from './errorEnvelope.js';
|
|
25
|
-
export {
|
|
25
|
+
export {
|
|
26
|
+
listEnvelopeSchema,
|
|
27
|
+
listEnvelope,
|
|
28
|
+
CURSOR_PARAM,
|
|
29
|
+
CURSOR_PARAM_ALIAS,
|
|
30
|
+
CURSOR_PARAM_NAMES,
|
|
31
|
+
} from './listEnvelope.js';
|
|
26
32
|
export type { ListEnvelope } from './listEnvelope.js';
|
|
27
33
|
|
|
28
34
|
// The `GET /v1/logs` feed — the two arms, their envelope, and the cursor that
|
package/src/wire/listEnvelope.ts
CHANGED
|
@@ -10,6 +10,29 @@
|
|
|
10
10
|
*/
|
|
11
11
|
import { z } from 'zod';
|
|
12
12
|
|
|
13
|
+
/**
|
|
14
|
+
* The query parameter a caller resumes a collection with, and the single alias
|
|
15
|
+
* retained for it.
|
|
16
|
+
*
|
|
17
|
+
* Declared beside the envelope that issues `next_cursor`, because the parameter
|
|
18
|
+
* and the field it consumes are one contract and both planes read them: the
|
|
19
|
+
* server to know which names are taken, the spec generator to document them,
|
|
20
|
+
* the client to send one. When the name lived in each of those separately, this
|
|
21
|
+
* spec described `starting_after` for a model list and `cursor` for a commit
|
|
22
|
+
* record list — the same concept under two names, in one document.
|
|
23
|
+
*
|
|
24
|
+
* `starting_after` is the spelling used through 0.52.0. It borrowed a name whose
|
|
25
|
+
* established meaning elsewhere is a row id, while this value has always been an
|
|
26
|
+
* opaque token bound to the sort it was issued for.
|
|
27
|
+
*/
|
|
28
|
+
export const CURSOR_PARAM = 'cursor';
|
|
29
|
+
|
|
30
|
+
/** @deprecated The pre-0.53.0 spelling of {@link CURSOR_PARAM}. */
|
|
31
|
+
export const CURSOR_PARAM_ALIAS = 'starting_after';
|
|
32
|
+
|
|
33
|
+
/** Both names a cursor may arrive under, for reserving them in a query string. */
|
|
34
|
+
export const CURSOR_PARAM_NAMES: readonly string[] = [CURSOR_PARAM, CURSOR_PARAM_ALIAS];
|
|
35
|
+
|
|
13
36
|
/**
|
|
14
37
|
* Builds the authoritative list-envelope schema for a row schema. Keeping the
|
|
15
38
|
* item validator generic lets every endpoint share the same envelope contract
|