@abloatai/ablo 0.24.0 → 0.26.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/AGENTS.md +5 -3
- package/CHANGELOG.md +33 -0
- package/README.md +45 -36
- package/dist/BaseSyncedStore.d.ts +72 -195
- package/dist/BaseSyncedStore.js +240 -625
- package/dist/Database.d.ts +8 -9
- package/dist/Database.js +9 -14
- package/dist/LazyReferenceCollection.js +5 -0
- package/dist/Model.js +6 -8
- package/dist/ModelRegistry.d.ts +25 -6
- package/dist/ModelRegistry.js +12 -6
- package/dist/NetworkMonitor.js +1 -1
- package/dist/ObjectPool.js +9 -3
- package/dist/SyncClient.d.ts +15 -5
- package/dist/SyncClient.js +152 -66
- package/dist/SyncEngineContext.d.ts +1 -3
- package/dist/SyncEngineContext.js +1 -2
- package/dist/agent/Agent.d.ts +21 -8
- package/dist/agent/Agent.js +26 -13
- package/dist/ai-sdk/coordination-context.js +9 -3
- package/dist/ai-sdk/wrap.d.ts +1 -1
- package/dist/ai-sdk/wrap.js +1 -1
- package/dist/auth/credentialPolicy.d.ts +10 -2
- package/dist/auth/credentialSource.d.ts +1 -13
- package/dist/auth/credentialSource.js +6 -13
- package/dist/auth/index.js +3 -3
- package/dist/cli.cjs +583 -287
- package/dist/client/Ablo.d.ts +35 -764
- package/dist/client/Ablo.js +64 -688
- package/dist/client/ApiClient.d.ts +14 -1
- package/dist/client/ApiClient.js +63 -9
- package/dist/client/auth.d.ts +8 -17
- package/dist/client/auth.js +42 -6
- package/dist/client/consoleLogger.d.ts +36 -0
- package/dist/client/consoleLogger.js +45 -0
- package/dist/client/createInternalComponents.js +1 -1
- package/dist/client/createModelProxy.js +10 -6
- package/dist/client/credentialEndpoint.d.ts +63 -0
- package/dist/client/credentialEndpoint.js +87 -0
- package/dist/client/hostedEndpoints.d.ts +24 -0
- package/dist/client/hostedEndpoints.js +24 -0
- package/dist/client/httpClient.d.ts +1 -1
- package/dist/client/identity.js +2 -1
- package/dist/client/modelRegistration.d.ts +14 -0
- package/dist/client/modelRegistration.js +321 -0
- package/dist/client/options.d.ts +396 -0
- package/dist/client/options.js +10 -0
- package/dist/client/resourceTypes.d.ts +344 -0
- package/dist/client/resourceTypes.js +13 -0
- package/dist/client/schemaConfig.d.ts +56 -0
- package/dist/client/schemaConfig.js +188 -0
- package/dist/client/sessionMint.d.ts +1 -1
- package/dist/client/validateAbloOptions.js +1 -1
- package/dist/client/wsMutationExecutor.d.ts +33 -0
- package/dist/client/wsMutationExecutor.js +77 -0
- package/dist/context.js +0 -3
- package/dist/coordination/index.d.ts +8 -1
- package/dist/coordination/index.js +24 -1
- package/dist/coordination/trace.d.ts +0 -1
- package/dist/coordination/trace.js +0 -1
- package/dist/core/DatabaseManager.d.ts +0 -1
- package/dist/core/DatabaseManager.js +26 -24
- package/dist/core/QueryProcessor.js +7 -8
- package/dist/core/QueryView.d.ts +14 -2
- package/dist/core/QueryView.js +4 -5
- package/dist/core/StoreManager.d.ts +2 -2
- package/dist/core/StoreManager.js +5 -1
- package/dist/core/index.d.ts +1 -1
- package/dist/core/openIDBWithTimeout.js +15 -11
- package/dist/core/query-utils.js +1 -1
- package/dist/core/storeContract.d.ts +143 -0
- package/dist/core/storeContract.js +16 -0
- package/dist/errorCodes.d.ts +11 -2
- package/dist/errorCodes.js +155 -137
- package/dist/errors.d.ts +18 -7
- package/dist/errors.js +14 -1
- package/dist/index.js +5 -0
- package/dist/interfaces/index.d.ts +6 -20
- package/dist/keys/index.js +18 -9
- package/dist/mutators/UndoManager.js +6 -1
- package/dist/mutators/defineMutators.d.ts +1 -3
- package/dist/policy/types.d.ts +1 -1
- package/dist/query/client.d.ts +11 -0
- package/dist/query/client.js +81 -52
- package/dist/react/AbloProvider.d.ts +6 -10
- package/dist/react/AbloProvider.js +22 -15
- package/dist/react/context.d.ts +2 -118
- package/dist/react/useAblo.js +1 -1
- package/dist/react/useErrorListener.js +1 -1
- package/dist/react/useMutationFailureListener.js +1 -1
- package/dist/react/useMutators.js +7 -1
- package/dist/react/useReactive.js +0 -1
- package/dist/react/useUndoScope.js +2 -2
- package/dist/schema/ddlLock.d.ts +39 -0
- package/dist/schema/ddlLock.js +51 -0
- package/dist/schema/field.js +1 -1
- package/dist/schema/index.d.ts +4 -3
- package/dist/schema/index.js +15 -5
- package/dist/schema/model.d.ts +3 -3
- package/dist/schema/model.js +2 -2
- package/dist/schema/openapi.js +5 -1
- package/dist/schema/queries.d.ts +1 -1
- package/dist/schema/queries.js +1 -0
- package/dist/schema/residency.d.ts +35 -0
- package/dist/schema/residency.js +27 -0
- package/dist/schema/roles.d.ts +25 -0
- package/dist/schema/roles.js +37 -0
- package/dist/schema/schema.d.ts +3 -3
- package/dist/schema/schema.js +11 -7
- package/dist/schema/serialize.d.ts +8 -8
- package/dist/schema/sync-delta-row.d.ts +2 -2
- package/dist/schema/sync-delta-row.js +2 -2
- package/dist/server/storage-mode.d.ts +8 -1
- package/dist/server/storage-mode.js +14 -4
- package/dist/source/adapter.d.ts +1 -1
- package/dist/source/adapters/drizzle.js +4 -3
- package/dist/source/adapters/kysely.js +4 -3
- package/dist/source/adapters/memory.js +1 -1
- package/dist/source/adapters/prisma.js +5 -4
- package/dist/source/conformance.js +10 -8
- package/dist/source/connector.js +4 -2
- package/dist/source/factory.d.ts +106 -0
- package/dist/source/factory.js +273 -0
- package/dist/source/index.d.ts +11 -461
- package/dist/source/index.js +14 -416
- package/dist/source/next.d.ts +1 -1
- package/dist/source/next.js +1 -1
- package/dist/source/pushQueue.d.ts +1 -1
- package/dist/source/pushQueue.js +6 -3
- package/dist/source/signing.d.ts +59 -0
- package/dist/source/signing.js +142 -0
- package/dist/source/types.d.ts +334 -0
- package/dist/source/types.js +44 -0
- package/dist/stores/ObjectStore.d.ts +1 -1
- package/dist/stores/ObjectStore.js +23 -23
- package/dist/stores/SyncActionStore.d.ts +1 -1
- package/dist/stores/SyncActionStore.js +70 -35
- package/dist/surface.d.ts +1 -1
- package/dist/surface.js +1 -0
- package/dist/sync/AreaOfInterestManager.d.ts +2 -2
- package/dist/sync/AreaOfInterestManager.js +1 -1
- package/dist/sync/BootstrapHelper.d.ts +1 -3
- package/dist/sync/BootstrapHelper.js +29 -8
- package/dist/sync/ConnectionManager.d.ts +8 -0
- package/dist/sync/ConnectionManager.js +19 -4
- package/dist/sync/HydrationCoordinator.d.ts +12 -0
- package/dist/sync/HydrationCoordinator.js +25 -5
- package/dist/sync/NetworkProbe.d.ts +3 -2
- package/dist/sync/NetworkProbe.js +7 -6
- package/dist/sync/SyncWebSocket.d.ts +71 -120
- package/dist/sync/SyncWebSocket.js +324 -630
- package/dist/sync/awaitClaimGrant.js +27 -19
- package/dist/sync/bootstrapApply.d.ts +60 -0
- package/dist/sync/bootstrapApply.js +65 -0
- package/dist/sync/commitFrames.d.ts +43 -0
- package/dist/sync/commitFrames.js +94 -0
- package/dist/sync/createClaimStream.js +3 -2
- package/dist/sync/createSnapshot.d.ts +1 -3
- package/dist/sync/credentialLifecycle.d.ts +175 -0
- package/dist/sync/credentialLifecycle.js +329 -0
- package/dist/sync/deltaPipeline.d.ts +111 -0
- package/dist/sync/deltaPipeline.js +262 -0
- package/dist/sync/groupChange.d.ts +111 -0
- package/dist/sync/groupChange.js +246 -0
- package/dist/sync/heartbeat.d.ts +62 -0
- package/dist/sync/heartbeat.js +91 -0
- package/dist/sync/participants.d.ts +8 -8
- package/dist/sync/syncCursor.d.ts +44 -0
- package/dist/sync/syncCursor.js +59 -0
- package/dist/sync/syncPlan.d.ts +62 -0
- package/dist/sync/syncPlan.js +51 -0
- package/dist/sync/wsFrameHandlers.d.ts +117 -0
- package/dist/sync/wsFrameHandlers.js +378 -0
- package/dist/testing/fixtures/bootstrap.d.ts +9 -11
- package/dist/testing/fixtures/deltas.d.ts +1 -1
- package/dist/testing/fixtures/models.js +1 -0
- package/dist/testing/helpers/sync-engine-harness.d.ts +0 -3
- package/dist/testing/helpers/sync-engine-harness.js +2 -1
- package/dist/testing/index.d.ts +1 -1
- package/dist/testing/index.js +1 -1
- package/dist/testing/mocks/MockSyncContext.d.ts +1 -17
- package/dist/testing/mocks/MockSyncContext.js +1 -32
- package/dist/testing/mocks/MockSyncStore.d.ts +4 -4
- package/dist/testing/mocks/MockWebSocket.d.ts +2 -2
- package/dist/transactions/TransactionQueue.d.ts +20 -56
- package/dist/transactions/TransactionQueue.js +101 -487
- package/dist/transactions/TransactionStore.d.ts +18 -0
- package/dist/transactions/TransactionStore.js +51 -0
- package/dist/transactions/coalesceRules.d.ts +34 -0
- package/dist/transactions/coalesceRules.js +117 -0
- package/dist/transactions/commitPayload.d.ts +134 -0
- package/dist/transactions/commitPayload.js +152 -0
- package/dist/transactions/deltaConfirmation.d.ts +60 -0
- package/dist/transactions/deltaConfirmation.js +223 -0
- package/dist/transactions/optimistic.d.ts +24 -0
- package/dist/transactions/optimistic.js +45 -0
- package/dist/transactions/persistedReplay.d.ts +93 -0
- package/dist/transactions/persistedReplay.js +105 -0
- package/dist/types/index.d.ts +3 -3
- package/dist/types/index.js +0 -5
- package/dist/types/modelData.d.ts +12 -0
- package/dist/types/modelData.js +11 -0
- package/dist/types/participant.d.ts +21 -0
- package/dist/types/participant.js +12 -0
- package/dist/types/streams.d.ts +8 -14
- package/dist/utils/mobx-setup.d.ts +1 -1
- package/dist/utils/mobx-setup.js +2 -2
- package/dist/wire/errorEnvelope.d.ts +11 -2
- package/dist/wire/errorEnvelope.js +14 -2
- package/dist/wire/frames.d.ts +90 -0
- package/dist/wire/frames.js +55 -1
- package/dist/wire/index.d.ts +4 -1
- package/dist/wire/index.js +20 -1
- package/dist/wire/protocol.d.ts +45 -0
- package/dist/wire/protocol.js +45 -0
- package/dist/wire/protocolVersion.d.ts +56 -0
- package/dist/wire/protocolVersion.js +63 -0
- package/docs/api-keys.md +4 -3
- package/docs/data-sources.md +12 -5
- package/docs/examples/existing-python-backend.md +3 -3
- package/docs/identity.md +4 -4
- package/docs/integration-guide.md +1 -1
- package/docs/react.md +1 -1
- package/docs/sessions.md +5 -7
- package/llms.txt +4 -2
- package/package.json +15 -13
- package/dist/client/index.d.ts +0 -36
- package/dist/client/index.js +0 -33
- package/dist/config/index.d.ts +0 -10
- package/dist/config/index.js +0 -12
- package/dist/interfaces/headless.d.ts +0 -95
- package/dist/interfaces/headless.js +0 -41
- package/dist/query/index.d.ts +0 -6
- package/dist/query/index.js +0 -5
- package/dist/realtime/index.d.ts +0 -10
- package/dist/realtime/index.js +0 -9
- package/dist/schema/plane.d.ts +0 -23
- package/dist/schema/plane.js +0 -19
- package/dist/server/next.d.ts +0 -51
- package/dist/server/next.js +0 -47
- package/dist/sync/OfflineFlush.d.ts +0 -9
- package/dist/sync/OfflineFlush.js +0 -22
- package/dist/sync/OfflineTransactionStore.d.ts +0 -37
- package/dist/sync/OfflineTransactionStore.js +0 -263
- package/dist/transactions/index.d.ts +0 -16
- package/dist/transactions/index.js +0 -7
package/dist/schema/schema.js
CHANGED
|
@@ -25,7 +25,7 @@ import { scopeSchema, grantsRefSchema } from './roles.js';
|
|
|
25
25
|
// Sync-group roles (identity + entity) live in `./roles.js`. Re-exported here
|
|
26
26
|
// so the long-standing `@ablo/schema` / `./schema.js` import paths keep working
|
|
27
27
|
// after the rehome — see roles.ts for the full vocabulary.
|
|
28
|
-
export { identityRole, entityRole, extractIdentityIds, extractEntityIds, composeIdentitySyncGroups, composeEntitySyncGroups, syncGroup, syncGroupSchema, syncGroupInputSchema, isSyncGroupInput, identityRoleSchema, entityRoleSchema, roleSchema, roleSourceSchema, scopeSchema, grantsRefSchema, groupsInputSchema, } from './roles.js';
|
|
28
|
+
export { identityRole, entityRole, extractIdentityIds, extractEntityIds, composeIdentitySyncGroups, composeEntitySyncGroups, intersectRequestedWithAllowed, syncGroup, syncGroupSchema, syncGroupInputSchema, isSyncGroupInput, identityRoleSchema, entityRoleSchema, roleSchema, roleSourceSchema, scopeSchema, grantsRefSchema, groupsInputSchema, } from './roles.js';
|
|
29
29
|
function resolveCasing(fn) {
|
|
30
30
|
if (fn === undefined)
|
|
31
31
|
return (x) => x;
|
|
@@ -108,17 +108,18 @@ function assertRoundTrippableCamelCase(modelName, fieldName) {
|
|
|
108
108
|
return;
|
|
109
109
|
// Leading-lowercase constraint: fields must be camelCase, not PascalCase.
|
|
110
110
|
// PascalCase is reserved for typenames.
|
|
111
|
-
|
|
111
|
+
const first = fieldName.charAt(0);
|
|
112
|
+
if (first >= 'A' && first <= 'Z') {
|
|
112
113
|
throw new AbloValidationError(`[defineSchema] ${modelName}.${fieldName}: field names must start lowercase ` +
|
|
113
|
-
`(camelCase). Use "${
|
|
114
|
+
`(camelCase). Use "${first.toLowerCase()}${fieldName.slice(1)}" instead.`, { code: 'schema_field_not_camelcase' });
|
|
114
115
|
}
|
|
115
116
|
// Two-consecutive-uppercase check. The classic failure mode is
|
|
116
117
|
// `contentJSON`, `contentHTML`, `myURLParam`, etc. These don't round-trip
|
|
117
118
|
// through `postgres.camel` — the snake_case intermediate would be
|
|
118
119
|
// `content_j_s_o_n`, which is not a column that exists.
|
|
119
120
|
for (let i = 0; i < fieldName.length - 1; i++) {
|
|
120
|
-
const a = fieldName
|
|
121
|
-
const b = fieldName
|
|
121
|
+
const a = fieldName.charAt(i);
|
|
122
|
+
const b = fieldName.charAt(i + 1);
|
|
122
123
|
const aUpper = a >= 'A' && a <= 'Z';
|
|
123
124
|
const bUpper = b >= 'A' && b <= 'Z';
|
|
124
125
|
if (aUpper && bUpper) {
|
|
@@ -184,8 +185,7 @@ export function defineSchema(models, options) {
|
|
|
184
185
|
// columns stay declared in the artifact instead of rediscovered by
|
|
185
186
|
// SQL compilers. Server-side SQL compilers read the resolved value
|
|
186
187
|
// directly.
|
|
187
|
-
for (const
|
|
188
|
-
const rel = def.relations[relName];
|
|
188
|
+
for (const rel of Object.values(def.relations)) {
|
|
189
189
|
const fieldColumn = def.fields[rel.foreignKey]?.column;
|
|
190
190
|
rel.foreignKeyColumn = fieldColumn ?? casing(rel.foreignKey);
|
|
191
191
|
}
|
|
@@ -245,6 +245,10 @@ function validateSyncGroupSchema(models) {
|
|
|
245
245
|
// The scope edge must target a model that is actually a scope root —
|
|
246
246
|
// otherwise the resolved `<kind>:<id>` group is one nothing fans into.
|
|
247
247
|
const scopeRel = relations[def.grants.scope];
|
|
248
|
+
// Unreachable: the role loop above already threw if the scope relation
|
|
249
|
+
// is missing — this narrows the indexed access for the checks below.
|
|
250
|
+
if (!scopeRel)
|
|
251
|
+
continue;
|
|
248
252
|
const target = models[scopeRel.target];
|
|
249
253
|
if (target && !target.scope) {
|
|
250
254
|
throw new AbloValidationError(`Model "${name}": grants.scope "${def.grants.scope}" targets "${scopeRel.target}", ` +
|
|
@@ -30,10 +30,10 @@
|
|
|
30
30
|
*/
|
|
31
31
|
import type { FieldMeta } from './field.js';
|
|
32
32
|
import type { Tenancy } from './tenancy.js';
|
|
33
|
-
import type {
|
|
33
|
+
import type { ModelResidency } from './residency.js';
|
|
34
34
|
import type { GrantsRef, LoadStrategy, PersistOptions, AutoFillRule, ConflictAxis } from './model.js';
|
|
35
35
|
import type { RelationType } from './relation.js';
|
|
36
|
-
import { type Schema, type
|
|
36
|
+
import { type Schema, type IdentityRole, type EntityRole } from './schema.js';
|
|
37
37
|
/** Current schema-JSON envelope version. Bump on a breaking change to the
|
|
38
38
|
* JSON shape itself (not the user's schema). v2 replaced the per-model
|
|
39
39
|
* `syncGroupFormat` template string with structured `scope`/`grants`/
|
|
@@ -58,7 +58,7 @@ export interface ModelJSON {
|
|
|
58
58
|
readonly tenancy: Tenancy;
|
|
59
59
|
/** Database plane. Optional for back-compat: absent in artifacts written before
|
|
60
60
|
* the plane axis → read as `tenant` (the default). See `./plane.ts`. */
|
|
61
|
-
readonly plane?:
|
|
61
|
+
readonly plane?: ModelResidency;
|
|
62
62
|
readonly scope?: boolean | string;
|
|
63
63
|
readonly grants?: GrantsRef;
|
|
64
64
|
readonly entityRoles?: readonly EntityRole[];
|
|
@@ -84,18 +84,18 @@ export interface SchemaJSON {
|
|
|
84
84
|
* (validators, `computed`); keeps everything the server and a faithful
|
|
85
85
|
* rebuild need. The result is plain data — `JSON.stringify`-safe.
|
|
86
86
|
*/
|
|
87
|
-
export declare function toSchemaJSON(schema: Schema
|
|
87
|
+
export declare function toSchemaJSON(schema: Schema): SchemaJSON;
|
|
88
88
|
/** Serialize a `Schema` to a JSON string (the `ablo push` payload). */
|
|
89
|
-
export declare function serializeSchema(schema: Schema
|
|
89
|
+
export declare function serializeSchema(schema: Schema): string;
|
|
90
90
|
/**
|
|
91
91
|
* Reconstruct a working `Schema` from its JSON form. Validators are rebuilt
|
|
92
92
|
* permissively from field metadata (the server never validates field shapes);
|
|
93
93
|
* `computed` getters are absent. Everything the server reads — routing,
|
|
94
94
|
* scoping, relations, identity roles — is restored exactly.
|
|
95
95
|
*/
|
|
96
|
-
export declare function fromSchemaJSON(json: SchemaJSON): Schema
|
|
96
|
+
export declare function fromSchemaJSON(json: SchemaJSON): Schema;
|
|
97
97
|
/** Parse a `Schema` from a JSON string (inverse of {@link serializeSchema}). */
|
|
98
|
-
export declare function parseSchema(json: string): Schema
|
|
98
|
+
export declare function parseSchema(json: string): Schema;
|
|
99
99
|
/**
|
|
100
100
|
* Stable content hash of a `Schema`'s JSON form. FNV-1a over a canonical
|
|
101
101
|
* (sorted-key) encoding — deterministic across runs and order-invariant, no
|
|
@@ -103,5 +103,5 @@ export declare function parseSchema(json: string): Schema<SchemaRecord>;
|
|
|
103
103
|
* the hash it was built against, the server compares it to the tenant's
|
|
104
104
|
* active schema hash. Not a security primitive.
|
|
105
105
|
*/
|
|
106
|
-
export declare function schemaHash(schema: Schema
|
|
106
|
+
export declare function schemaHash(schema: Schema): string;
|
|
107
107
|
export {};
|
|
@@ -17,7 +17,7 @@
|
|
|
17
17
|
* - {@link deltaProvenanceSchema} — which AI task caused it. `control` plane.
|
|
18
18
|
*
|
|
19
19
|
* {@link syncDeltaRowSchema} is the full stored row (core ∪ attribution ∪
|
|
20
|
-
* provenance). {@link
|
|
20
|
+
* provenance). {@link DELTA_RESIDENCY} declares each slice's plane so provisioning
|
|
21
21
|
* (P1) can derive "what a customer DB gets" from the schema, not hand-code it.
|
|
22
22
|
*
|
|
23
23
|
* Distinct from the WIRE `SyncDelta` (`sync/SyncWebSocket.ts`, client-facing) and
|
|
@@ -152,7 +152,7 @@ export type SyncDeltaRow = z.infer<typeof syncDeltaRowSchema>;
|
|
|
152
152
|
* hand-coding the boundary; the BYO outbox writes the `tenant` slice and the
|
|
153
153
|
* relay enriches the `control` slices in Ablo's own database.
|
|
154
154
|
*/
|
|
155
|
-
export declare const
|
|
155
|
+
export declare const DELTA_RESIDENCY: {
|
|
156
156
|
readonly core: "tenant";
|
|
157
157
|
readonly attribution: "control";
|
|
158
158
|
readonly provenance: "control";
|
|
@@ -17,7 +17,7 @@
|
|
|
17
17
|
* - {@link deltaProvenanceSchema} — which AI task caused it. `control` plane.
|
|
18
18
|
*
|
|
19
19
|
* {@link syncDeltaRowSchema} is the full stored row (core ∪ attribution ∪
|
|
20
|
-
* provenance). {@link
|
|
20
|
+
* provenance). {@link DELTA_RESIDENCY} declares each slice's plane so provisioning
|
|
21
21
|
* (P1) can derive "what a customer DB gets" from the schema, not hand-code it.
|
|
22
22
|
*
|
|
23
23
|
* Distinct from the WIRE `SyncDelta` (`sync/SyncWebSocket.ts`, client-facing) and
|
|
@@ -96,7 +96,7 @@ export const syncDeltaRowSchema = syncDeltaCoreSchema
|
|
|
96
96
|
* hand-coding the boundary; the BYO outbox writes the `tenant` slice and the
|
|
97
97
|
* relay enriches the `control` slices in Ablo's own database.
|
|
98
98
|
*/
|
|
99
|
-
export const
|
|
99
|
+
export const DELTA_RESIDENCY = {
|
|
100
100
|
core: 'tenant',
|
|
101
101
|
attribution: 'control',
|
|
102
102
|
provenance: 'control',
|
|
@@ -1 +1,8 @@
|
|
|
1
|
-
|
|
1
|
+
/** See module note — host-deployment vocabulary, never customer-facing. */
|
|
2
|
+
export declare const storageModeSchema: z.ZodEnum<{
|
|
3
|
+
source: "source";
|
|
4
|
+
hosted: "hosted";
|
|
5
|
+
selfHosted: "selfHosted";
|
|
6
|
+
}>;
|
|
7
|
+
/** See module note — host-deployment vocabulary, never customer-facing. */
|
|
8
|
+
export type StorageMode = z.infer<typeof storageModeSchema>;
|
|
@@ -8,11 +8,21 @@
|
|
|
8
8
|
* - `selfHosted` — the customer's database, same execution path as hosted.
|
|
9
9
|
* - `source` — a customer-owned endpoint (credentialless ingestion).
|
|
10
10
|
*
|
|
11
|
-
*
|
|
12
|
-
* "storage mode" — their story is `
|
|
13
|
-
*
|
|
11
|
+
* Deployment topology, not product vocabulary. Customers never see a
|
|
12
|
+
* "storage mode" — their story is `ablo connect` (WAL replication: reads tail
|
|
13
|
+
* their database's logical-replication stream, the data stays in their DB)
|
|
14
|
+
* with the signed `dataSource()` endpoint as the marked fallback (ADR 0002,
|
|
15
|
+
* docs/decisions/0002-read-path-logical-replication-vs-data-hosting.md).
|
|
16
|
+
* The `databaseUrl` dial-in that this enum's `selfHosted` arm serves is
|
|
17
|
+
* DEPRECATED pending the WAL read cutover — do not build on it.
|
|
14
18
|
* This export exists for the sync-server host only.
|
|
19
|
+
*
|
|
20
|
+
* NOT `@internal`-tagged: tsconfig.build.json sets `stripInternal`, and these
|
|
21
|
+
* symbols are re-exported by `server/index.ts` — tagging them strips them from
|
|
22
|
+
* the emitted d.ts, leaving an empty `export {}` module behind a live
|
|
23
|
+
* re-export. `skipLibCheck` masks the break and `StorageMode` silently
|
|
24
|
+
* degrades to an error-any for every dist consumer (the sync-server host).
|
|
15
25
|
*/
|
|
16
26
|
import { z } from 'zod';
|
|
17
|
-
/**
|
|
27
|
+
/** See module note — host-deployment vocabulary, never customer-facing. */
|
|
18
28
|
export const storageModeSchema = z.enum(['hosted', 'source', 'selfHosted']);
|
package/dist/source/adapter.d.ts
CHANGED
|
@@ -17,7 +17,7 @@
|
|
|
17
17
|
* core handler's `commit` / `events` / per-model `load`+`list` — no per-ORM
|
|
18
18
|
* branching anywhere above the adapter.
|
|
19
19
|
*/
|
|
20
|
-
import type { SourceListQuery, SourceRequestContext } from './
|
|
20
|
+
import type { SourceListQuery, SourceRequestContext } from './types.js';
|
|
21
21
|
import type { AdapterCapabilities, ChangeSet, EventsPage, Migration } from './contract.js';
|
|
22
22
|
/**
|
|
23
23
|
* A canonical row keyed by schema FIELD name (e.g. `operatorId`) — the SDK shape,
|
|
@@ -144,8 +144,9 @@ export function drizzleDataSource(db, schema) {
|
|
|
144
144
|
async commit(change) {
|
|
145
145
|
return db.transaction(async (tx) => {
|
|
146
146
|
const cached = rowsOf(await tx.execute(sql `SELECT response FROM ablo_idempotency WHERE client_tx_id = ${change.clientTxId} LIMIT 1`));
|
|
147
|
-
|
|
148
|
-
|
|
147
|
+
const cachedRow = cached[0];
|
|
148
|
+
if (cachedRow)
|
|
149
|
+
return { rows: cachedRow.response };
|
|
149
150
|
const rows = [];
|
|
150
151
|
for (const [index, op] of change.operations.entries()) {
|
|
151
152
|
const row = await applyOperation(tx, op);
|
|
@@ -180,7 +181,7 @@ export function drizzleDataSource(db, schema) {
|
|
|
180
181
|
occurredAt: r.occurred_at != null ? Number(r.occurred_at) : null,
|
|
181
182
|
cursor: String(r.cursor),
|
|
182
183
|
}));
|
|
183
|
-
return { events, nextCursor: events.
|
|
184
|
+
return { events, nextCursor: events.at(-1)?.cursor ?? null };
|
|
184
185
|
},
|
|
185
186
|
};
|
|
186
187
|
}
|
|
@@ -153,8 +153,9 @@ export function kyselyDataSource(db, schema) {
|
|
|
153
153
|
.where('client_tx_id', '=', change.clientTxId)
|
|
154
154
|
.limit(1)
|
|
155
155
|
.execute();
|
|
156
|
-
|
|
157
|
-
|
|
156
|
+
const cachedRow = cached[0];
|
|
157
|
+
if (cachedRow) {
|
|
158
|
+
const response = cachedRow.response;
|
|
158
159
|
return {
|
|
159
160
|
rows: (typeof response === 'string' ? JSON.parse(response) : response),
|
|
160
161
|
};
|
|
@@ -204,7 +205,7 @@ export function kyselyDataSource(db, schema) {
|
|
|
204
205
|
occurredAt: r.occurred_at != null ? Number(r.occurred_at) : null,
|
|
205
206
|
cursor: String(r.cursor),
|
|
206
207
|
}));
|
|
207
|
-
return { events, nextCursor: events.
|
|
208
|
+
return { events, nextCursor: events.at(-1)?.cursor ?? null };
|
|
208
209
|
},
|
|
209
210
|
};
|
|
210
211
|
}
|
|
@@ -108,7 +108,7 @@ export function memoryDataSource() {
|
|
|
108
108
|
const page = outbox.filter((e) => Number(e.cursor) > after).slice(0, limit);
|
|
109
109
|
return {
|
|
110
110
|
events: page,
|
|
111
|
-
nextCursor: page.
|
|
111
|
+
nextCursor: page.at(-1)?.cursor ?? null,
|
|
112
112
|
};
|
|
113
113
|
},
|
|
114
114
|
};
|
|
@@ -15,7 +15,7 @@
|
|
|
15
15
|
import { AbloValidationError } from '../../errors.js';
|
|
16
16
|
import { outboxEventSchema } from '../contract.js';
|
|
17
17
|
import { adapterTableMigrations } from '../migrations.js';
|
|
18
|
-
const lowerFirst = (s) => (s ? s
|
|
18
|
+
const lowerFirst = (s) => (s ? s.charAt(0).toLowerCase() + s.slice(1) : s);
|
|
19
19
|
/**
|
|
20
20
|
* Resolve a model's Prisma delegate by name. This is the ONE irreducible cast in
|
|
21
21
|
* the adapter layer, and it's a genuine type-system limit, not laziness:
|
|
@@ -138,8 +138,9 @@ export function prismaDataSource(prisma, schema, options = {}) {
|
|
|
138
138
|
return prisma.$transaction(async (tx) => {
|
|
139
139
|
// Idempotency: a duplicate clientTxId returns the original rows, no re-apply.
|
|
140
140
|
const cached = await tx.$queryRawUnsafe(`SELECT response FROM ablo_idempotency WHERE client_tx_id = $1 LIMIT 1`, change.clientTxId);
|
|
141
|
-
|
|
142
|
-
|
|
141
|
+
const cachedRow = cached[0];
|
|
142
|
+
if (cachedRow)
|
|
143
|
+
return { rows: cachedRow.response };
|
|
143
144
|
const rows = [];
|
|
144
145
|
for (const [index, op] of change.operations.entries()) {
|
|
145
146
|
const row = await applyOperation(tx, op);
|
|
@@ -170,7 +171,7 @@ export function prismaDataSource(prisma, schema, options = {}) {
|
|
|
170
171
|
}));
|
|
171
172
|
return {
|
|
172
173
|
events,
|
|
173
|
-
nextCursor: events.
|
|
174
|
+
nextCursor: events.at(-1)?.cursor ?? null,
|
|
174
175
|
};
|
|
175
176
|
},
|
|
176
177
|
};
|
|
@@ -29,8 +29,10 @@ export function dataSourceConformanceChecks(make) {
|
|
|
29
29
|
const adapter = await make();
|
|
30
30
|
const result = await adapter.commit(change('tx_create', [{ type: 'CREATE', model: 'task', id: 't1', input: { title: 'A' } }]));
|
|
31
31
|
assert.equal(result.rows.length, 1, 'one row returned');
|
|
32
|
-
|
|
33
|
-
assert.
|
|
32
|
+
const created = result.rows[0];
|
|
33
|
+
assert.ok(created, 'one row returned');
|
|
34
|
+
assert.equal(created.id, 't1');
|
|
35
|
+
assert.equal(created.title, 'A');
|
|
34
36
|
},
|
|
35
37
|
},
|
|
36
38
|
{
|
|
@@ -40,7 +42,7 @@ export function dataSourceConformanceChecks(make) {
|
|
|
40
42
|
await adapter.commit(change('tx1', [{ type: 'CREATE', model: 'task', id: 't1', input: { title: 'A' } }]));
|
|
41
43
|
const found = await adapter.read({ kind: 'load', model: 'task', id: 't1' });
|
|
42
44
|
assert.equal(found.length, 1);
|
|
43
|
-
assert.equal(found[0]
|
|
45
|
+
assert.equal(found[0]?.title, 'A');
|
|
44
46
|
const missing = await adapter.read({ kind: 'load', model: 'task', id: 'nope' });
|
|
45
47
|
assert.equal(missing.length, 0, 'unknown id reads empty');
|
|
46
48
|
},
|
|
@@ -54,7 +56,7 @@ export function dataSourceConformanceChecks(make) {
|
|
|
54
56
|
{ type: 'CREATE', model: 'task', id: 't2', input: { title: 'B' } },
|
|
55
57
|
]));
|
|
56
58
|
const rows = await adapter.read({ kind: 'list', model: 'task' });
|
|
57
|
-
const ids = rows.map((r) => r.id).sort();
|
|
59
|
+
const ids = rows.map((r) => (r).id).sort();
|
|
58
60
|
assert.deepEqual(ids, ['t1', 't2']);
|
|
59
61
|
},
|
|
60
62
|
},
|
|
@@ -83,9 +85,9 @@ export function dataSourceConformanceChecks(make) {
|
|
|
83
85
|
assert.ok(page.events.length >= 1, 'at least one event');
|
|
84
86
|
const evt = page.events.find((e) => e.entityId === 't1');
|
|
85
87
|
assert.ok(evt, 'event for the committed row');
|
|
86
|
-
assert.equal(evt
|
|
87
|
-
assert.equal(evt
|
|
88
|
-
assert.equal(evt
|
|
88
|
+
assert.equal(evt.model, 'task');
|
|
89
|
+
assert.equal(evt.type, 'CREATE');
|
|
90
|
+
assert.equal(evt.clientTxId, 'tx_evt');
|
|
89
91
|
},
|
|
90
92
|
},
|
|
91
93
|
{
|
|
@@ -115,7 +117,7 @@ export function dataSourceConformanceChecks(make) {
|
|
|
115
117
|
await adapter.commit(change('tx_c1', [{ type: 'CREATE', model: 'task', id: 't1', input: { title: 'A' } }]));
|
|
116
118
|
await adapter.commit(change('tx_u1', [{ type: 'UPDATE', model: 'task', id: 't1', input: { title: 'B' } }]));
|
|
117
119
|
const found = await adapter.read({ kind: 'load', model: 'task', id: 't1' });
|
|
118
|
-
assert.equal(found[0]
|
|
120
|
+
assert.equal(found[0]?.title, 'B', 'update applied');
|
|
119
121
|
},
|
|
120
122
|
},
|
|
121
123
|
];
|
package/dist/source/connector.js
CHANGED
|
@@ -26,8 +26,10 @@
|
|
|
26
26
|
* path. The transport changes; the trust model does not.
|
|
27
27
|
*/
|
|
28
28
|
import { SOURCE_CONNECTOR_PROTOCOL_VERSION, SOURCE_CONNECTOR_WS_PATH, sourceConnectorSubprotocols, encodeFrame, decodeFrame, ConnectorProtocolError, } from './connector-protocol.js';
|
|
29
|
-
|
|
30
|
-
|
|
29
|
+
import { ABLO_HOSTED_HTTP_BASE_URL } from '../client/hostedEndpoints.js';
|
|
30
|
+
/** Default Ablo Cloud base. The connector appends `SOURCE_CONNECTOR_WS_PATH`.
|
|
31
|
+
* Single-sourced from the dependency-free hosted-endpoints leaf. */
|
|
32
|
+
const DEFAULT_BASE_URL = ABLO_HOSTED_HTTP_BASE_URL;
|
|
31
33
|
/**
|
|
32
34
|
* Reconnect backoff, in ms, indexed by consecutive failed connect attempts.
|
|
33
35
|
* Unlike the (multi-day) Standard Webhooks delivery schedule, a long-lived
|
|
@@ -0,0 +1,106 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* The `abloSource()` / `dataSource()` endpoint factory — the customer-owned
|
|
3
|
+
* Data Source handler. Takes the options (schema, apiKey, handlers or ORM
|
|
4
|
+
* adapter), verifies the signed request, enforces per-key scopes, and
|
|
5
|
+
* dispatches the four wire operations (`load`/`list`/`commit`/`events`) to
|
|
6
|
+
* the configured handlers.
|
|
7
|
+
*
|
|
8
|
+
* `AbloSourceOptions` lives here (not `types.ts`) because it references the
|
|
9
|
+
* ORM `DataSourceAdapter` — keeping it with the factory keeps `types.ts` a
|
|
10
|
+
* dependency-free leaf for `adapter.ts`/`contract.ts` to import from.
|
|
11
|
+
*/
|
|
12
|
+
import type { Schema, SchemaRecord, InferCreate } from '../schema/schema.js';
|
|
13
|
+
import type { DataSourceAdapter } from './adapter.js';
|
|
14
|
+
import type { SourceApiKey, SourceAuthorizeContext, SourceCommitHandler, SourceEventsHandler, SourceModelHandlers, SourceRequest, SourceScope } from './types.js';
|
|
15
|
+
type SourceModels<S extends SchemaRecord, TAuth> = Partial<{
|
|
16
|
+
readonly [K in keyof S & string]: SourceModelHandlers<InferCreate<Schema<S>, K> & {
|
|
17
|
+
readonly id: string;
|
|
18
|
+
} & Record<string, unknown>, InferCreate<Schema<S>, K>, TAuth>;
|
|
19
|
+
}>;
|
|
20
|
+
export type AbloSourceOptions<S extends SchemaRecord, TAuth = unknown> = {
|
|
21
|
+
readonly schema: Schema<S>;
|
|
22
|
+
/**
|
|
23
|
+
* Customer-visible Ablo credential. In the API-key-only onboarding
|
|
24
|
+
* path, Ablo signs Data Source calls with the same project API key
|
|
25
|
+
* that the customer's server-side SDK uses. This keeps the customer
|
|
26
|
+
* env surface to one Ablo credential while preserving signed request
|
|
27
|
+
* verification before any handler runs.
|
|
28
|
+
*/
|
|
29
|
+
readonly apiKey: SourceApiKey;
|
|
30
|
+
/**
|
|
31
|
+
* Clock-skew window for signed source requests. Default: 5 minutes.
|
|
32
|
+
*/
|
|
33
|
+
readonly signatureToleranceMs?: number;
|
|
34
|
+
/**
|
|
35
|
+
* Verify the Ablo request and return customer-owned context such as
|
|
36
|
+
* a database handle, account scope, or current actor. Keep database
|
|
37
|
+
* credentials in this function's environment; never send them to Ablo.
|
|
38
|
+
*
|
|
39
|
+
* Signature verification is handled by `apiKey` before this function
|
|
40
|
+
* runs. `authorize` should only attach business context.
|
|
41
|
+
*/
|
|
42
|
+
readonly authorize?: (context: SourceAuthorizeContext) => Promise<TAuth> | TAuth;
|
|
43
|
+
/**
|
|
44
|
+
* Optional per-request scope resolver. When set, the helper checks
|
|
45
|
+
* the resolved scope set against the request's operation
|
|
46
|
+
* (`load`/`list`/`commit`/`events`) and returns 403
|
|
47
|
+
* `source_forbidden` if not allowed — before any model handler
|
|
48
|
+
* runs.
|
|
49
|
+
*
|
|
50
|
+
* Customers typically extract a key id from the request (e.g.
|
|
51
|
+
* `webhook-id` prefix, a custom header, or the API key itself) and
|
|
52
|
+
* look up the scopes for that key in their store.
|
|
53
|
+
*
|
|
54
|
+
* When omitted, all operations are allowed. Returning an empty set
|
|
55
|
+
* denies all operations.
|
|
56
|
+
*/
|
|
57
|
+
readonly resolveScopes?: (params: {
|
|
58
|
+
readonly auth: TAuth;
|
|
59
|
+
readonly request: Request;
|
|
60
|
+
readonly body: SourceRequest;
|
|
61
|
+
}) => Promise<ReadonlySet<SourceScope> | readonly SourceScope[]> | ReadonlySet<SourceScope> | readonly SourceScope[];
|
|
62
|
+
/**
|
|
63
|
+
* Top-level atomic commit handler. Prefer this for real applications:
|
|
64
|
+
* one UI/action commit can span several models and should run inside
|
|
65
|
+
* one customer-owned transaction.
|
|
66
|
+
*/
|
|
67
|
+
readonly commit?: SourceCommitHandler<TAuth>;
|
|
68
|
+
/**
|
|
69
|
+
* External-write feed. Ablo polls this to learn about changes that
|
|
70
|
+
* happened outside the SDK (cron jobs, dashboard edits, batch
|
|
71
|
+
* imports). Each returned event becomes a delta and fans out to
|
|
72
|
+
* connected clients.
|
|
73
|
+
*
|
|
74
|
+
* Handlers may return the raw outbox feed. Ablo dedupes stable
|
|
75
|
+
* `event.id` values and filters SDK-origin echoes when rows carry
|
|
76
|
+
* the originating `clientTxId`; customers should persist both fields
|
|
77
|
+
* in their outbox table.
|
|
78
|
+
*/
|
|
79
|
+
readonly events?: SourceEventsHandler<TAuth>;
|
|
80
|
+
/**
|
|
81
|
+
* Optional grouped form. The object-key form below is usually terser:
|
|
82
|
+
* `abloSource({ schema, files: { load, list, commit } })`.
|
|
83
|
+
*/
|
|
84
|
+
readonly models?: SourceModels<S, TAuth>;
|
|
85
|
+
/**
|
|
86
|
+
* An ORM adapter (`prismaDataSource(prisma, schema)`, …). When set, it serves
|
|
87
|
+
* ALL four operations — read (load/list), commit (idempotent + outbox), and
|
|
88
|
+
* events — so no hand-written `commit`/`events`/model handlers are needed. The
|
|
89
|
+
* adapter is consumed at the generic dispatch layer (rows are JSON on the wire),
|
|
90
|
+
* which is why it carries no per-model types and needs no cast at the call site.
|
|
91
|
+
* Mutually exclusive with hand-written handlers.
|
|
92
|
+
*/
|
|
93
|
+
readonly adapter?: DataSourceAdapter;
|
|
94
|
+
} & SourceModels<S, TAuth>;
|
|
95
|
+
/**
|
|
96
|
+
* Create a customer-owned data source endpoint.
|
|
97
|
+
*
|
|
98
|
+
* App code still talks to Ablo with `ablo.files.load/list/update`.
|
|
99
|
+
* This helper is only for customers who keep canonical rows in their own
|
|
100
|
+
* database and want Ablo Cloud to call a narrow, signed endpoint instead
|
|
101
|
+
* of receiving database credentials.
|
|
102
|
+
*/
|
|
103
|
+
export declare function abloSource<const S extends SchemaRecord, TAuth = unknown>(options: AbloSourceOptions<S, TAuth>): (request: Request) => Promise<Response>;
|
|
104
|
+
export type DataSourceOptions<S extends SchemaRecord, TAuth = unknown> = AbloSourceOptions<S, TAuth>;
|
|
105
|
+
export declare function dataSource<const S extends SchemaRecord, TAuth = unknown>(options: DataSourceOptions<S, TAuth>): (request: Request) => Promise<Response>;
|
|
106
|
+
export {};
|