@abloatai/transaction 0.51.0 → 0.53.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/dist/ablo.d.ts +1 -1
- package/dist/ablo.js +1 -1
- package/dist/ai-sdk/updateTool.d.ts +1 -1
- package/dist/ai-sdk/updateTool.js +1 -1
- package/dist/auth/apiKey.d.ts +0 -6
- package/dist/auth/apiKey.d.ts.map +1 -1
- package/dist/auth/apiKey.js +13 -60
- package/dist/auth/apiKey.js.map +1 -1
- package/dist/auth/baseUrl.d.ts +38 -0
- package/dist/auth/baseUrl.d.ts.map +1 -0
- package/dist/auth/baseUrl.js +126 -0
- package/dist/auth/baseUrl.js.map +1 -0
- package/dist/auth/capability.d.ts +2 -2
- package/dist/auth/capability.js +1 -1
- package/dist/auth/index.d.ts +15 -2
- package/dist/auth/index.d.ts.map +1 -1
- package/dist/auth/index.js +35 -21
- package/dist/auth/index.js.map +1 -1
- package/dist/auth/schemas.d.ts +24 -1
- package/dist/auth/schemas.d.ts.map +1 -1
- package/dist/auth/schemas.js +8 -2
- package/dist/auth/schemas.js.map +1 -1
- package/dist/auth/sessionIssuanceProtocol.d.ts +41 -0
- package/dist/auth/sessionIssuanceProtocol.d.ts.map +1 -0
- package/dist/auth/sessionIssuanceProtocol.js +48 -0
- package/dist/auth/sessionIssuanceProtocol.js.map +1 -0
- package/dist/auth/sessionMint.js +2 -2
- package/dist/branches.d.ts +34 -33
- package/dist/branches.d.ts.map +1 -1
- package/dist/branches.js +2 -4
- package/dist/branches.js.map +1 -1
- package/dist/coordination/schema.d.ts +3 -2
- package/dist/coordination/schema.d.ts.map +1 -1
- package/dist/coordination/schema.js +7 -2
- package/dist/coordination/schema.js.map +1 -1
- package/dist/errorCodes.d.ts +7 -3
- package/dist/errorCodes.d.ts.map +1 -1
- package/dist/errorCodes.js +8 -4
- package/dist/errorCodes.js.map +1 -1
- package/dist/errors.d.ts +1 -1
- package/dist/errors.js +1 -1
- package/dist/index.d.ts +2 -0
- package/dist/index.d.ts.map +1 -1
- package/dist/index.js.map +1 -1
- package/dist/resources/httpResources.d.ts +35 -2
- package/dist/resources/httpResources.d.ts.map +1 -1
- package/dist/resources/httpResources.js +10 -1
- package/dist/resources/httpResources.js.map +1 -1
- package/dist/resources/modelOperations.d.ts +18 -6
- package/dist/resources/modelOperations.d.ts.map +1 -1
- package/dist/resources/modelOperations.js.map +1 -1
- package/dist/resources/where.d.ts +67 -2
- package/dist/resources/where.d.ts.map +1 -1
- package/dist/resources/where.js +73 -0
- package/dist/resources/where.js.map +1 -1
- package/dist/schema/ddl.d.ts.map +1 -1
- package/dist/schema/ddl.js +9 -12
- package/dist/schema/ddl.js.map +1 -1
- package/dist/schema/field.d.ts +3 -3
- package/dist/schema/field.js +2 -2
- package/dist/schema/fieldRef.d.ts +1 -1
- package/dist/schema/generate.d.ts +2 -3
- package/dist/schema/generate.d.ts.map +1 -1
- package/dist/schema/generate.js +2 -8
- package/dist/schema/generate.js.map +1 -1
- package/dist/schema/index.d.ts +2 -2
- package/dist/schema/index.js +3 -3
- package/dist/schema/model.d.ts +7 -7
- package/dist/schema/model.js +2 -2
- package/dist/schema/openapi.d.ts.map +1 -1
- package/dist/schema/openapi.js +63 -21
- package/dist/schema/openapi.js.map +1 -1
- package/dist/schema/queries.d.ts +1 -1
- package/dist/schema/queries.js +1 -1
- package/dist/schema/relation.d.ts +8 -8
- package/dist/schema/relation.js +5 -5
- package/dist/schema/schema.d.ts +24 -37
- package/dist/schema/schema.d.ts.map +1 -1
- package/dist/schema/schema.js +15 -28
- package/dist/schema/schema.js.map +1 -1
- package/dist/schema/sugar.d.ts +7 -7
- package/dist/schema/sugar.js +5 -5
- package/dist/server/adapter.d.ts +2 -0
- package/dist/server/adapter.d.ts.map +1 -1
- package/dist/server/commit.d.ts +5 -1
- package/dist/server/commit.d.ts.map +1 -1
- package/dist/server/index.d.ts +3 -2
- package/dist/server/index.d.ts.map +1 -1
- package/dist/server/index.js.map +1 -1
- package/dist/server/readConfig.d.ts +109 -57
- package/dist/server/readConfig.d.ts.map +1 -1
- package/dist/server/readConfig.js +19 -5
- package/dist/server/readConfig.js.map +1 -1
- package/dist/source/adapters/drizzle.d.ts.map +1 -1
- package/dist/source/adapters/drizzle.js +5 -0
- package/dist/source/adapters/drizzle.js.map +1 -1
- package/dist/source/adapters/kysely.d.ts.map +1 -1
- package/dist/source/adapters/kysely.js +33 -8
- package/dist/source/adapters/kysely.js.map +1 -1
- package/dist/source/adapters/kyselyMutationCore.d.ts +1 -1
- package/dist/source/adapters/kyselyMutationCore.d.ts.map +1 -1
- package/dist/source/adapters/kyselyMutationCore.js +58 -20
- package/dist/source/adapters/kyselyMutationCore.js.map +1 -1
- package/dist/source/adapters/memory.d.ts.map +1 -1
- package/dist/source/adapters/memory.js +5 -0
- package/dist/source/adapters/memory.js.map +1 -1
- package/dist/source/adapters/prisma.d.ts.map +1 -1
- package/dist/source/adapters/prisma.js +6 -1
- package/dist/source/adapters/prisma.js.map +1 -1
- package/dist/source/conformance.js +21 -21
- package/dist/source/contract.d.ts +17 -0
- package/dist/source/contract.d.ts.map +1 -1
- package/dist/source/contract.js +31 -0
- package/dist/source/contract.js.map +1 -1
- package/dist/source/index.d.ts +1 -1
- package/dist/source/index.d.ts.map +1 -1
- package/dist/source/index.js +1 -1
- package/dist/source/index.js.map +1 -1
- package/dist/source/types.d.ts +1 -0
- package/dist/source/types.d.ts.map +1 -1
- package/dist/source/types.js.map +1 -1
- package/dist/transactionLayer.d.ts +3 -3
- package/dist/transactionLayer.js +3 -3
- package/dist/transport/httpClient.d.ts +13 -4
- package/dist/transport/httpClient.d.ts.map +1 -1
- package/dist/transport/httpClient.js +4 -2
- package/dist/transport/httpClient.js.map +1 -1
- package/dist/transport/httpTransport.d.ts +1 -0
- package/dist/transport/httpTransport.d.ts.map +1 -1
- package/dist/transport/httpTransport.js +79 -11
- package/dist/transport/httpTransport.js.map +1 -1
- package/dist/transport/wsFrameHandlers.d.ts.map +1 -1
- package/dist/transport/wsFrameHandlers.js +3 -0
- package/dist/transport/wsFrameHandlers.js.map +1 -1
- package/dist/types/streams.d.ts +2 -2
- package/dist/wire/accountResponses.d.ts +18 -13
- package/dist/wire/accountResponses.d.ts.map +1 -1
- package/dist/wire/accountResponses.js +5 -11
- package/dist/wire/accountResponses.js.map +1 -1
- package/dist/wire/claims.d.ts +1 -0
- package/dist/wire/claims.d.ts.map +1 -1
- package/dist/wire/claims.js +7 -1
- package/dist/wire/claims.js.map +1 -1
- package/dist/wire/commit.d.ts +144 -0
- package/dist/wire/commit.d.ts.map +1 -1
- package/dist/wire/commit.js +22 -1
- package/dist/wire/commit.js.map +1 -1
- package/dist/wire/frames.d.ts +4 -0
- package/dist/wire/frames.d.ts.map +1 -1
- package/dist/wire/inboundFrames.d.ts +22 -0
- package/dist/wire/inboundFrames.d.ts.map +1 -1
- package/dist/wire/index.d.ts +3 -3
- package/dist/wire/index.d.ts.map +1 -1
- package/dist/wire/index.js +2 -2
- package/dist/wire/index.js.map +1 -1
- package/dist/wire/listEnvelope.d.ts +20 -0
- package/dist/wire/listEnvelope.d.ts.map +1 -1
- package/dist/wire/listEnvelope.js +20 -0
- package/dist/wire/listEnvelope.js.map +1 -1
- package/dist/wire/protocol.d.ts +1 -1
- package/dist/wire/protocol.js +1 -1
- package/package.json +1 -1
- package/src/ablo.ts +1 -1
- package/src/ai-sdk/updateTool.ts +1 -1
- package/src/auth/apiKey.ts +13 -61
- package/src/auth/baseUrl.ts +144 -0
- package/src/auth/capability.ts +3 -3
- package/src/auth/index.ts +62 -21
- package/src/auth/schemas.ts +16 -2
- package/src/auth/sessionIssuanceProtocol.ts +57 -0
- package/src/auth/sessionMint.ts +2 -2
- package/src/branches.ts +2 -4
- package/src/coordination/schema.ts +7 -2
- package/src/errorCodes.ts +29 -5
- package/src/errors.ts +1 -1
- package/src/index.ts +2 -0
- package/src/resources/httpResources.ts +45 -2
- package/src/resources/modelOperations.ts +18 -6
- package/src/resources/where.ts +70 -4
- package/src/schema/ddl.ts +9 -13
- package/src/schema/field.ts +3 -3
- package/src/schema/fieldRef.ts +1 -1
- package/src/schema/generate.ts +2 -8
- package/src/schema/index.ts +3 -3
- package/src/schema/model.ts +7 -7
- package/src/schema/openapi.ts +64 -21
- package/src/schema/queries.ts +1 -1
- package/src/schema/relation.ts +8 -8
- package/src/schema/residency.ts +0 -1
- package/src/schema/schema.ts +33 -58
- package/src/schema/sugar.ts +7 -7
- package/src/server/adapter.ts +2 -0
- package/src/server/commit.ts +5 -1
- package/src/server/index.ts +8 -2
- package/src/server/readConfig.ts +111 -57
- package/src/source/adapters/drizzle.ts +5 -0
- package/src/source/adapters/kysely.ts +48 -10
- package/src/source/adapters/kyselyMutationCore.ts +69 -22
- package/src/source/adapters/memory.ts +5 -0
- package/src/source/adapters/prisma.ts +6 -1
- package/src/source/conformance.ts +21 -21
- package/src/source/contract.ts +34 -0
- package/src/source/index.ts +2 -0
- package/src/source/types.ts +1 -0
- package/src/transactionLayer.ts +3 -3
- package/src/transport/httpClient.ts +23 -6
- package/src/transport/httpTransport.ts +88 -11
- package/src/transport/wsFrameHandlers.ts +3 -0
- package/src/types/global.ts +1 -1
- package/src/types/streams.ts +2 -2
- package/src/wire/accountResponses.ts +5 -11
- package/src/wire/claims.ts +7 -1
- package/src/wire/commit.ts +31 -1
- package/src/wire/index.ts +10 -1
- package/src/wire/listEnvelope.ts +23 -0
- package/src/wire/protocol.ts +1 -1
package/src/auth/index.ts
CHANGED
|
@@ -12,6 +12,8 @@
|
|
|
12
12
|
import {
|
|
13
13
|
type EphemeralKeyResponse,
|
|
14
14
|
type IdentityResolveResponse,
|
|
15
|
+
type SessionCredentialMetadata,
|
|
16
|
+
type SessionCredentialRevocationRequest,
|
|
15
17
|
parseCapabilityMintResponse,
|
|
16
18
|
parseEphemeralKeyResponse,
|
|
17
19
|
parseIdentityResolveResponse,
|
|
@@ -27,7 +29,17 @@ export {
|
|
|
27
29
|
ABLO_HOSTED_API_DOMAIN,
|
|
28
30
|
ABLO_HOSTED_HTTP_BASE_URL,
|
|
29
31
|
} from './hostedEndpoints.js';
|
|
30
|
-
export {
|
|
32
|
+
export { normalizeAbloBaseUrl } from './baseUrl.js';
|
|
33
|
+
// A re-export does not bind the name in this module; the alias below needs it.
|
|
34
|
+
import { normalizeAbloBaseUrl } from './baseUrl.js';
|
|
35
|
+
|
|
36
|
+
/**
|
|
37
|
+
* @deprecated Renamed to {@link normalizeAbloBaseUrl}. Removed in 0.54.0.
|
|
38
|
+
* The "hosted" spelling named a rewrite of retired hosted hostnames that no
|
|
39
|
+
* longer exists; both spellings resolve through the one function, so a caller
|
|
40
|
+
* on the old name gets the same refusals of unsafe credential destinations.
|
|
41
|
+
*/
|
|
42
|
+
export const normalizeAbloHostedBaseUrl = normalizeAbloBaseUrl;
|
|
31
43
|
|
|
32
44
|
/**
|
|
33
45
|
* @deprecated Use {@link CapabilityMintResponse}. This is a type-only,
|
|
@@ -39,6 +51,26 @@ export type CapabilityExchangeResponse = CapabilityMintResponse;
|
|
|
39
51
|
export type {
|
|
40
52
|
EphemeralKeyResponse,
|
|
41
53
|
IdentityResolveResponse,
|
|
54
|
+
SessionCredentialMetadata,
|
|
55
|
+
SessionCredentialRevocationRequest,
|
|
56
|
+
} from './schemas.js';
|
|
57
|
+
export {
|
|
58
|
+
buildSessionIssuanceMacMessage,
|
|
59
|
+
canonicalizeSessionIssuanceBody,
|
|
60
|
+
opaqueSessionFingerprintSchema,
|
|
61
|
+
sha256HexSchema,
|
|
62
|
+
SESSION_ISSUANCE_HEADERS,
|
|
63
|
+
SESSION_ISSUANCE_MAC_VERSION,
|
|
64
|
+
SESSION_ISSUANCE_PATHS,
|
|
65
|
+
SESSION_ISSUANCE_SLOT_VERSION,
|
|
66
|
+
sessionIssuanceMacHeadersSchema,
|
|
67
|
+
sessionIssuanceMacMessageSchema,
|
|
68
|
+
type SessionIssuanceMacHeaders,
|
|
69
|
+
type SessionIssuanceMacMessage,
|
|
70
|
+
} from './sessionIssuanceProtocol.js';
|
|
71
|
+
export {
|
|
72
|
+
SessionCredentialMetadataSchema,
|
|
73
|
+
SessionCredentialRevocationRequestSchema,
|
|
42
74
|
} from './schemas.js';
|
|
43
75
|
export { parseCapabilityMintResponse } from './schemas.js';
|
|
44
76
|
export {
|
|
@@ -99,6 +131,10 @@ export type {
|
|
|
99
131
|
// A re-export does not bind the name in this module, and the mint request type
|
|
100
132
|
// below needs it.
|
|
101
133
|
import type { CapabilityOperation } from './capability.js';
|
|
134
|
+
import {
|
|
135
|
+
ephemeralKeyRequestSchema,
|
|
136
|
+
type EphemeralKeyRequest,
|
|
137
|
+
} from '../wire/auth.js';
|
|
102
138
|
|
|
103
139
|
export interface ExchangeApiKeyRequest {
|
|
104
140
|
readonly apiKey: string;
|
|
@@ -265,6 +301,30 @@ export type MintUserSessionRequest = MintUserSessionBase & {
|
|
|
265
301
|
readonly controlPlaneOnly?: true;
|
|
266
302
|
};
|
|
267
303
|
|
|
304
|
+
/** Canonical wire projection shared by the SDK and first-party issuer. */
|
|
305
|
+
export function buildMintUserSessionRequest(
|
|
306
|
+
options: MintUserSessionRequest,
|
|
307
|
+
): EphemeralKeyRequest {
|
|
308
|
+
return ephemeralKeyRequestSchema.parse({
|
|
309
|
+
user: { id: options.userId },
|
|
310
|
+
...(options.organizationId ? { organizationId: options.organizationId } : {}),
|
|
311
|
+
...(options.schemaProject
|
|
312
|
+
? {
|
|
313
|
+
schemaProjectId: options.schemaProject.projectId,
|
|
314
|
+
schemaOwnerOrgId: options.schemaProject.organizationId,
|
|
315
|
+
}
|
|
316
|
+
: {}),
|
|
317
|
+
...(options.syncGroups ? { syncGroups: options.syncGroups } : {}),
|
|
318
|
+
...(options.controlPlaneOnly
|
|
319
|
+
? { controlPlaneOnly: true }
|
|
320
|
+
: options.operations
|
|
321
|
+
? { operations: options.operations }
|
|
322
|
+
: { activeSchemaOperations: options.activeSchemaOperations }),
|
|
323
|
+
ttlSeconds: options.ttlSeconds,
|
|
324
|
+
...(options.label ? { label: options.label } : {}),
|
|
325
|
+
});
|
|
326
|
+
}
|
|
327
|
+
|
|
268
328
|
/**
|
|
269
329
|
* Mints an end-user session key (an `ek_` key) by calling
|
|
270
330
|
* `POST /v1/ephemeral_keys`, using your secret key as authorization. Your
|
|
@@ -322,26 +382,7 @@ export async function mintUserSessionKey(
|
|
|
322
382
|
'Content-Type': 'application/json',
|
|
323
383
|
Authorization: `Bearer ${options.apiKey}`,
|
|
324
384
|
},
|
|
325
|
-
body: JSON.stringify(
|
|
326
|
-
user: { id: options.userId },
|
|
327
|
-
...(options.organizationId ? { organizationId: options.organizationId } : {}),
|
|
328
|
-
// The public option is project-centric; map it to the flat wire keys the
|
|
329
|
-
// endpoint expects.
|
|
330
|
-
...(options.schemaProject
|
|
331
|
-
? {
|
|
332
|
-
schemaProjectId: options.schemaProject.projectId,
|
|
333
|
-
schemaOwnerOrgId: options.schemaProject.organizationId,
|
|
334
|
-
}
|
|
335
|
-
: {}),
|
|
336
|
-
...(options.syncGroups ? { syncGroups: options.syncGroups } : {}),
|
|
337
|
-
...(options.controlPlaneOnly
|
|
338
|
-
? { controlPlaneOnly: true }
|
|
339
|
-
: options.operations
|
|
340
|
-
? { operations: options.operations }
|
|
341
|
-
: { activeSchemaOperations: options.activeSchemaOperations }),
|
|
342
|
-
ttlSeconds: options.ttlSeconds,
|
|
343
|
-
...(options.label ? { label: options.label } : {}),
|
|
344
|
-
}),
|
|
385
|
+
body: JSON.stringify(buildMintUserSessionRequest(options)),
|
|
345
386
|
signal: controller.signal,
|
|
346
387
|
});
|
|
347
388
|
} catch (err) {
|
package/src/auth/schemas.ts
CHANGED
|
@@ -9,6 +9,7 @@ import {
|
|
|
9
9
|
import { AbloAuthenticationError } from '../errors.js';
|
|
10
10
|
import { authTokenSchema } from './token.js';
|
|
11
11
|
import { deliveryPartitionRouteSchema } from './deliveryPartition.js';
|
|
12
|
+
import { opaqueSessionFingerprintSchema } from './sessionIssuanceProtocol.js';
|
|
12
13
|
|
|
13
14
|
// Not a second enum. The auth responses carry the same participant vocabulary
|
|
14
15
|
// the coordination plane parses, so they validate against the same schema — a
|
|
@@ -40,10 +41,9 @@ export type IdentityResolveResponse = z.infer<typeof IdentityResolveResponseSche
|
|
|
40
41
|
* block. It still echoes the effective operations stored on the key so the
|
|
41
42
|
* client reports enforced authority rather than reconstructing it from input.
|
|
42
43
|
*/
|
|
43
|
-
export const
|
|
44
|
+
export const SessionCredentialMetadataSchema = z.object({
|
|
44
45
|
object: z.literal('ephemeral_key').optional(),
|
|
45
46
|
id: z.string().min(1),
|
|
46
|
-
token: authTokenSchema,
|
|
47
47
|
expiresAt: z.string().min(1),
|
|
48
48
|
organizationId: z.string().min(1),
|
|
49
49
|
participantId: z.string().min(1),
|
|
@@ -67,6 +67,20 @@ export const EphemeralKeyResponseSchema = z.object({
|
|
|
67
67
|
},
|
|
68
68
|
);
|
|
69
69
|
|
|
70
|
+
/** Persistable/replayable projection of the canonical mint response. */
|
|
71
|
+
export type SessionCredentialMetadata = z.infer<typeof SessionCredentialMetadataSchema>;
|
|
72
|
+
|
|
73
|
+
export const SessionCredentialRevocationRequestSchema = z.object({
|
|
74
|
+
parentSessionHash: opaqueSessionFingerprintSchema,
|
|
75
|
+
});
|
|
76
|
+
export type SessionCredentialRevocationRequest = z.infer<
|
|
77
|
+
typeof SessionCredentialRevocationRequestSchema
|
|
78
|
+
>;
|
|
79
|
+
|
|
80
|
+
export const EphemeralKeyResponseSchema = SessionCredentialMetadataSchema.safeExtend({
|
|
81
|
+
token: authTokenSchema,
|
|
82
|
+
});
|
|
83
|
+
|
|
70
84
|
export type EphemeralKeyResponse = z.infer<typeof EphemeralKeyResponseSchema>;
|
|
71
85
|
|
|
72
86
|
function formatIssues(error: z.ZodError): string {
|
|
@@ -0,0 +1,57 @@
|
|
|
1
|
+
import { z } from 'zod';
|
|
2
|
+
import { stableStringify } from '../utils/json.js';
|
|
3
|
+
|
|
4
|
+
/** Bump only with a coordinated MAC-message protocol rollout. */
|
|
5
|
+
export const SESSION_ISSUANCE_MAC_VERSION = 'v1' as const;
|
|
6
|
+
/** Bump whenever canonical mint semantics change incompatibly. */
|
|
7
|
+
export const SESSION_ISSUANCE_SLOT_VERSION = 'v1' as const;
|
|
8
|
+
|
|
9
|
+
export const SESSION_ISSUANCE_PATHS = {
|
|
10
|
+
mint: '/api/v1/ephemeral_keys',
|
|
11
|
+
revoke: '/api/v1/internal/session-credentials/revoke',
|
|
12
|
+
} as const;
|
|
13
|
+
|
|
14
|
+
export const SESSION_ISSUANCE_HEADERS = {
|
|
15
|
+
slotKey: 'ablo-session-slot',
|
|
16
|
+
parentSessionHash: 'ablo-parent-session',
|
|
17
|
+
timestamp: 'ablo-session-issuance-timestamp',
|
|
18
|
+
mac: 'ablo-session-issuance-mac',
|
|
19
|
+
} as const;
|
|
20
|
+
|
|
21
|
+
export const sha256HexSchema = z.string().regex(/^[a-f0-9]{64}$/);
|
|
22
|
+
export const opaqueSessionFingerprintSchema = sha256HexSchema;
|
|
23
|
+
|
|
24
|
+
export const sessionIssuanceMacHeadersSchema = z.object({
|
|
25
|
+
timestamp: z.string().regex(/^\d{13}$/),
|
|
26
|
+
mac: sha256HexSchema,
|
|
27
|
+
});
|
|
28
|
+
export type SessionIssuanceMacHeaders = z.infer<typeof sessionIssuanceMacHeadersSchema>;
|
|
29
|
+
|
|
30
|
+
export const sessionIssuanceMacMessageSchema = z.object({
|
|
31
|
+
method: z.enum(['POST']),
|
|
32
|
+
path: z.enum([SESSION_ISSUANCE_PATHS.mint, SESSION_ISSUANCE_PATHS.revoke]),
|
|
33
|
+
timestamp: z.string().regex(/^\d{13}$/),
|
|
34
|
+
bodySha256: sha256HexSchema,
|
|
35
|
+
slotKey: opaqueSessionFingerprintSchema.optional(),
|
|
36
|
+
parentSessionHash: opaqueSessionFingerprintSchema,
|
|
37
|
+
});
|
|
38
|
+
export type SessionIssuanceMacMessage = z.infer<typeof sessionIssuanceMacMessageSchema>;
|
|
39
|
+
|
|
40
|
+
/** One canonical JSON representation for slot derivation and request MACs. */
|
|
41
|
+
export function canonicalizeSessionIssuanceBody(body: unknown): string {
|
|
42
|
+
return stableStringify(body);
|
|
43
|
+
}
|
|
44
|
+
|
|
45
|
+
/** Canonical, bounded KMS GenerateMac/VerifyMac message (well below 4 KiB). */
|
|
46
|
+
export function buildSessionIssuanceMacMessage(input: SessionIssuanceMacMessage): Uint8Array {
|
|
47
|
+
const value = sessionIssuanceMacMessageSchema.parse(input);
|
|
48
|
+
return new TextEncoder().encode([
|
|
49
|
+
`ablo-session-issuance:${SESSION_ISSUANCE_MAC_VERSION}`,
|
|
50
|
+
value.method,
|
|
51
|
+
value.path,
|
|
52
|
+
value.timestamp,
|
|
53
|
+
value.bodySha256,
|
|
54
|
+
value.slotKey ?? '',
|
|
55
|
+
value.parentSessionHash,
|
|
56
|
+
].join('\n'));
|
|
57
|
+
}
|
package/src/auth/sessionMint.ts
CHANGED
|
@@ -13,8 +13,8 @@
|
|
|
13
13
|
* participant with `invalid_participant_kind`.
|
|
14
14
|
* `{ agent }` → POST /v1/capabilities, returning an `rk_` key scoped to exactly
|
|
15
15
|
* the operations named in `can`. For example
|
|
16
|
-
* `can: {
|
|
17
|
-
* `
|
|
16
|
+
* `can: { items: ['update'] }` becomes the wire allowlist entry
|
|
17
|
+
* `items.update`, which the server matches against the model's
|
|
18
18
|
* registered names.
|
|
19
19
|
*
|
|
20
20
|
* The caller supplies the already-resolved secret key and base URL in
|
package/src/branches.ts
CHANGED
|
@@ -1,4 +1,5 @@
|
|
|
1
1
|
import { z } from 'zod';
|
|
2
|
+
import { listEnvelopeSchema } from './wire/listEnvelope.js';
|
|
2
3
|
|
|
3
4
|
/**
|
|
4
5
|
* A branch handle is for people and URLs. Durable routing always uses the
|
|
@@ -47,10 +48,7 @@ export const branchResponseSchema = z.object({
|
|
|
47
48
|
});
|
|
48
49
|
export type BranchResponse = z.infer<typeof branchResponseSchema>;
|
|
49
50
|
|
|
50
|
-
export const branchListResponseSchema =
|
|
51
|
-
object: z.literal('list'),
|
|
52
|
-
data: z.array(branchResponseSchema).readonly(),
|
|
53
|
-
});
|
|
51
|
+
export const branchListResponseSchema = listEnvelopeSchema(branchResponseSchema);
|
|
54
52
|
export type BranchListResponse = z.infer<typeof branchListResponseSchema>;
|
|
55
53
|
|
|
56
54
|
export const createBranchRequestSchema = z.object({
|
|
@@ -61,7 +61,7 @@ export function part(name: string): ClaimPart {
|
|
|
61
61
|
* The wire spelling of a part name, from whichever spelling the caller used.
|
|
62
62
|
*
|
|
63
63
|
* Three, because they are three different promises. A {@link FieldRef} —
|
|
64
|
-
* `schema.fields.
|
|
64
|
+
* `schema.fields.items.status` — is a field the schema declares, so a name that
|
|
65
65
|
* does not exist never compiles. `part('B2')` is a name the schema does not
|
|
66
66
|
* know and says so. A bare string is neither, and survives only because the
|
|
67
67
|
* erased `SchemaRecord` view and untyped callers still need it.
|
|
@@ -330,7 +330,7 @@ const syncIdSchema = z.number().int().nonnegative();
|
|
|
330
330
|
*
|
|
331
331
|
* • `self` — the row that moved IS the group's scope root.
|
|
332
332
|
* • `parent` — it sits one declared containment edge below the root.
|
|
333
|
-
* • `transitive` — the root is ≥2 hops up (`comment →
|
|
333
|
+
* • `transitive` — the root is ≥2 hops up (`comment → item → project`).
|
|
334
334
|
*/
|
|
335
335
|
export const stalePropagationSchema = z
|
|
336
336
|
.object({
|
|
@@ -1417,6 +1417,11 @@ export const commitOperationSchema = writeGuardSchema.extend({
|
|
|
1417
1417
|
model: z.string(),
|
|
1418
1418
|
id: z.string().nullish(),
|
|
1419
1419
|
input: z.record(z.string(), z.unknown()).nullish(),
|
|
1420
|
+
/** Equality conditions evaluated atomically by the writing database. */
|
|
1421
|
+
where: z
|
|
1422
|
+
.record(z.string().min(1), z.unknown())
|
|
1423
|
+
.refine((value) => Object.keys(value).length > 0, 'where must name at least one field')
|
|
1424
|
+
.nullish(),
|
|
1420
1425
|
/** Per-op client tx id, echoed on the broadcast delta. */
|
|
1421
1426
|
transactionId: z.string().nullish(),
|
|
1422
1427
|
/**
|
package/src/errorCodes.ts
CHANGED
|
@@ -39,7 +39,7 @@ import { z } from 'zod';
|
|
|
39
39
|
* error documentation and returned on the `Ablo-Version` response header, so a
|
|
40
40
|
* consumer can detect when its expected contract has drifted from the server's.
|
|
41
41
|
*/
|
|
42
|
-
export const ERROR_CONTRACT_VERSION = '2026-08-
|
|
42
|
+
export const ERROR_CONTRACT_VERSION = '2026-08-15';
|
|
43
43
|
|
|
44
44
|
/** A coarse grouping of error codes, used to organize metrics and documentation. */
|
|
45
45
|
export type ErrorCategory =
|
|
@@ -604,6 +604,12 @@ export const ERROR_CODES = {
|
|
|
604
604
|
false,
|
|
605
605
|
'An earlier attempt under this idempotency key was pinned to a different source transport. Restore that route and retry the same key; Ablo will not switch a possibly committed write.'
|
|
606
606
|
),
|
|
607
|
+
precondition_failed: wire(
|
|
608
|
+
'conflict',
|
|
609
|
+
409,
|
|
610
|
+
false,
|
|
611
|
+
'A conditional operation did not match the current database row. The complete atomic commit was rejected and no operation in the batch was applied.'
|
|
612
|
+
),
|
|
607
613
|
idempotency_key_too_long: wire(
|
|
608
614
|
'validation',
|
|
609
615
|
400,
|
|
@@ -815,11 +821,11 @@ export const ERROR_CODES = {
|
|
|
815
821
|
false,
|
|
816
822
|
'The branch has no endpoint Data Source for this connector to serve. Run `ablo dev --local` with the current CLI, which registers the connector-only source before opening the socket.'
|
|
817
823
|
),
|
|
818
|
-
|
|
824
|
+
item_id_missing: wire(
|
|
819
825
|
'server',
|
|
820
826
|
502,
|
|
821
827
|
true,
|
|
822
|
-
'The
|
|
828
|
+
'The item-create response arrived without an item id, so the result cannot be used. Retry the request.'
|
|
823
829
|
),
|
|
824
830
|
|
|
825
831
|
// ── data integrity / database constraints ──────────────────────────
|
|
@@ -864,6 +870,12 @@ export const ERROR_CODES = {
|
|
|
864
870
|
false,
|
|
865
871
|
'A structured (JSON) value was written to a column whose database type cannot hold it. Ablo adapts a json field to either a jsonb column (native) or a text column (serialized) — but a scalar column (integer, boolean, uuid, timestamp, …) cannot store a JSON object or array. Use a jsonb or text column for this field. Ablo adapts to your column; it does not alter your schema.'
|
|
866
872
|
),
|
|
873
|
+
column_value_out_of_range: wire(
|
|
874
|
+
'validation',
|
|
875
|
+
400,
|
|
876
|
+
false,
|
|
877
|
+
'A stored value is outside the range the field was declared to hold. A number field reads back as a JavaScript number, which represents integers exactly only up to 9,007,199,254,740,991; a bigint column holding more than that would come back rounded. Declare the field as text to read those values digit for digit.'
|
|
878
|
+
),
|
|
867
879
|
|
|
868
880
|
// ── tenant / unknown model (400) ───────────────────────────────────
|
|
869
881
|
server_execute_unknown_model: wire(
|
|
@@ -918,7 +930,7 @@ export const ERROR_CODES = {
|
|
|
918
930
|
),
|
|
919
931
|
schema_reserved_field: client(
|
|
920
932
|
'schema',
|
|
921
|
-
'A model
|
|
933
|
+
'A model declared `id`, the one universal field Ablo supplies. Remove it from the declared application fields.'
|
|
922
934
|
),
|
|
923
935
|
schema_grants_shape_invalid: wire(
|
|
924
936
|
'schema',
|
|
@@ -1611,6 +1623,18 @@ export const ERROR_CODES = {
|
|
|
1611
1623
|
false,
|
|
1612
1624
|
'The query contained an invalid identifier.'
|
|
1613
1625
|
),
|
|
1626
|
+
query_relation_expansion_too_large: wire(
|
|
1627
|
+
'validation',
|
|
1628
|
+
400,
|
|
1629
|
+
false,
|
|
1630
|
+
'A requested relation expansion exceeds the bounded nested-row budget. Query the related model as its own paginated collection instead.'
|
|
1631
|
+
),
|
|
1632
|
+
organization_disabled: wire(
|
|
1633
|
+
'permission',
|
|
1634
|
+
403,
|
|
1635
|
+
false,
|
|
1636
|
+
'This organization has been disabled by an operator. Contact support before retrying.'
|
|
1637
|
+
),
|
|
1614
1638
|
org_id_required: wire(
|
|
1615
1639
|
'validation',
|
|
1616
1640
|
400,
|
|
@@ -1647,7 +1671,7 @@ export const ERROR_CODES = {
|
|
|
1647
1671
|
false,
|
|
1648
1672
|
'Uploads are not configured on this deployment: the upload storage bucket and CDN domain are unset.'
|
|
1649
1673
|
),
|
|
1650
|
-
|
|
1674
|
+
item_id_required: wire('validation', 400, false, 'An item id is required for this request.'),
|
|
1651
1675
|
claim_id_required: wire('validation', 400, false, 'A claim id is required for this request.'),
|
|
1652
1676
|
commit_operation_action_required: wire(
|
|
1653
1677
|
'validation',
|
package/src/errors.ts
CHANGED
|
@@ -67,7 +67,7 @@ export class AbloError extends Error {
|
|
|
67
67
|
/** Opaque id of the exact private observation associated with this error. */
|
|
68
68
|
readonly eventId?: string;
|
|
69
69
|
/** The specific input that caused the error, as a model or field path such as
|
|
70
|
-
* `'
|
|
70
|
+
* `'archiveMember.grants.subject'`, so tooling can point at the exact
|
|
71
71
|
* offending value. */
|
|
72
72
|
readonly param?: string;
|
|
73
73
|
/** A link to the documentation for this error's {@link code}. When not set
|
package/src/index.ts
CHANGED
|
@@ -35,6 +35,8 @@ export type {
|
|
|
35
35
|
HttpModelClient,
|
|
36
36
|
HttpModelMutationParams,
|
|
37
37
|
} from './transport/httpClient.js';
|
|
38
|
+
/** What `ablo.<model>.list()` resolves to: the rows, and where the page ends. */
|
|
39
|
+
export type { ModelList } from './resources/httpResources.js';
|
|
38
40
|
export type {
|
|
39
41
|
FunctionalUpdateOptions,
|
|
40
42
|
/** @deprecated Use `FunctionalUpdateOptions`. */
|
|
@@ -101,9 +101,51 @@ export interface HttpTransportRead<T = Record<string, unknown>> {
|
|
|
101
101
|
/** @internal Exact collection envelope retained until the typed facade captures evidence. */
|
|
102
102
|
export interface HttpTransportList<T = Record<string, unknown>> {
|
|
103
103
|
readonly data: readonly T[];
|
|
104
|
+
/**
|
|
105
|
+
* Whether the collection continues past this page. A list read is always a
|
|
106
|
+
* page — the server applies a default size and caps the largest one — so a
|
|
107
|
+
* caller that treats `data` as the whole set is right only while this is
|
|
108
|
+
* `false`. It used to be dropped on the floor here, which made a truncated
|
|
109
|
+
* read indistinguishable from a complete one.
|
|
110
|
+
*/
|
|
111
|
+
readonly hasMore: boolean;
|
|
112
|
+
/**
|
|
113
|
+
* The cursor to pass back as `cursor` for the next page, or `null` at the end
|
|
114
|
+
* of the collection.
|
|
115
|
+
*/
|
|
116
|
+
readonly nextCursor: string | null;
|
|
104
117
|
readonly evidence?: readonly ModelListEvidence[];
|
|
105
118
|
}
|
|
106
119
|
|
|
120
|
+
/**
|
|
121
|
+
* What a collection read hands back: the rows, and where the collection stands.
|
|
122
|
+
*
|
|
123
|
+
* It is an array, so it maps, filters, spreads, and iterates like the rows it
|
|
124
|
+
* always was. `hasMore` and `nextCursor` ride along as non-enumerable
|
|
125
|
+
* properties, which keeps `JSON.stringify` and a spread producing exactly the
|
|
126
|
+
* array they produced before.
|
|
127
|
+
*
|
|
128
|
+
* They ride along because a list read is a page: the server applies a default
|
|
129
|
+
* size and caps the largest one. Returning only the rows made a truncated read
|
|
130
|
+
* and a complete one the same value, so the caller with 500 matching rows got
|
|
131
|
+
* 20 and no way to find out.
|
|
132
|
+
*/
|
|
133
|
+
export type ModelList<T> = T[] & Pick<HttpTransportList<T>, 'hasMore' | 'nextCursor'>;
|
|
134
|
+
|
|
135
|
+
/**
|
|
136
|
+
* Attach the page state to the rows. Non-enumerable so the result stays
|
|
137
|
+
* indistinguishable from a plain array everywhere the properties aren't read.
|
|
138
|
+
*/
|
|
139
|
+
export function modelList<T>(
|
|
140
|
+
rows: readonly T[],
|
|
141
|
+
page: Pick<HttpTransportList<unknown>, 'hasMore' | 'nextCursor'>,
|
|
142
|
+
): ModelList<T> {
|
|
143
|
+
return Object.defineProperties([...rows], {
|
|
144
|
+
hasMore: { value: page.hasMore, enumerable: false },
|
|
145
|
+
nextCursor: { value: page.nextCursor, enumerable: false },
|
|
146
|
+
}) as ModelList<T>;
|
|
147
|
+
}
|
|
148
|
+
|
|
107
149
|
export type IfClaimedPolicy = 'return' | 'fail';
|
|
108
150
|
|
|
109
151
|
export interface ClaimedOptions {
|
|
@@ -170,6 +212,7 @@ export interface CommitOperationInput {
|
|
|
170
212
|
readonly model: string;
|
|
171
213
|
readonly id?: string | null;
|
|
172
214
|
readonly data?: Record<string, unknown> | null;
|
|
215
|
+
readonly where?: Record<string, unknown> | null;
|
|
173
216
|
readonly transactionId?: string | null;
|
|
174
217
|
/** Claim identity derived from a held claim; not an application id. */
|
|
175
218
|
readonly claimId?: string | null;
|
|
@@ -432,8 +475,8 @@ export interface CreateUserSessionParams<S extends SchemaRecord> {
|
|
|
432
475
|
|
|
433
476
|
/** Mint params for a scoped **agent** session — mints a restricted `rk_` token
|
|
434
477
|
* gated to exactly the operations named in `can`. `can` is typed off your
|
|
435
|
-
* schema (no magic `'
|
|
436
|
-
* — the SDK serializes each entry to the wire allowlist (`
|
|
478
|
+
* schema (no magic `'item.update'` strings): `{ Item: ['update'], Report: ['read'] }`
|
|
479
|
+
* — the SDK serializes each entry to the wire allowlist (`item.update`). */
|
|
437
480
|
export interface CreateAgentSessionParams<S extends SchemaRecord> {
|
|
438
481
|
/** Your agent. `id` becomes the token's `participantId`. */
|
|
439
482
|
agent: { id: string };
|
|
@@ -205,7 +205,19 @@ export interface ServerReadOptions<T> {
|
|
|
205
205
|
*/
|
|
206
206
|
where?: LoadWhere<T>;
|
|
207
207
|
orderBy?: { [K in keyof T]?: 'asc' | 'desc' };
|
|
208
|
+
/**
|
|
209
|
+
* Rows per page. The server has its own ceiling and applies a default when
|
|
210
|
+
* this is omitted, so a collection larger than one page always comes back as
|
|
211
|
+
* a page: read `hasMore` on the result before treating it as the whole set.
|
|
212
|
+
*/
|
|
208
213
|
limit?: number;
|
|
214
|
+
/**
|
|
215
|
+
* Where to resume: the `nextCursor` from the previous page. Pass it back with
|
|
216
|
+
* the same `where` and `orderBy` to walk a collection; the cursor encodes the
|
|
217
|
+
* sort position it was issued for, so it is an opaque token rather than a row
|
|
218
|
+
* id, and a read that changes either starts a new walk.
|
|
219
|
+
*/
|
|
220
|
+
cursor?: string;
|
|
209
221
|
/**
|
|
210
222
|
* `complete` waits for the server. `unknown` returns whatever is local
|
|
211
223
|
* immediately and refreshes in the background.
|
|
@@ -256,7 +268,7 @@ export interface ClaimTargetOptions<T = Record<string, unknown>> {
|
|
|
256
268
|
|
|
257
269
|
/**
|
|
258
270
|
* Narrow the claim to Zod-declared fields of the row. Return one field
|
|
259
|
-
* directly or an array for several: `fields: (
|
|
271
|
+
* directly or an array for several: `fields: (item) => item.status`.
|
|
260
272
|
*
|
|
261
273
|
* Exclusion follows the target: claims on the same row conflict only where
|
|
262
274
|
* their sets intersect, so a holder on `['title']` and a holder on
|
|
@@ -313,7 +325,7 @@ export interface ClaimTargetOptions<T = Record<string, unknown>> {
|
|
|
313
325
|
* @deprecated Prefer `contention: { signal }`.
|
|
314
326
|
*
|
|
315
327
|
* Abort a pending wait from outside — the same signal that cancels
|
|
316
|
-
* everything else in the program, so a cancelled agent
|
|
328
|
+
* everything else in the program, so a cancelled agent item or an unmounted
|
|
317
329
|
* component takes its queued claim with it. Rejects with
|
|
318
330
|
* {@link AbloClaimedError} (`claim_wait_aborted`); over HTTP the abort also
|
|
319
331
|
* leaves the line. Ignored once the grant has arrived — a held lease is
|
|
@@ -327,7 +339,7 @@ export interface ClaimTargetOptions<T = Record<string, unknown>> {
|
|
|
327
339
|
ttl?: Duration;
|
|
328
340
|
/**
|
|
329
341
|
* Keep the lease alive for the duration of real work by beating on a
|
|
330
|
-
* cadence — the pattern for background workers whose
|
|
342
|
+
* cadence — the pattern for background workers whose item outlives the
|
|
331
343
|
* crash-cleanup TTL. `true` beats every third of the TTL (so two beats can
|
|
332
344
|
* fail before the lease is at risk, and a crashed worker's lease still
|
|
333
345
|
* lapses within one beat window); a duration such as `'2m'` sets the
|
|
@@ -431,12 +443,12 @@ export type ClaimSkipOptions<T = Record<string, unknown>> =
|
|
|
431
443
|
* write and the SDK acquires/releases around that one mutation:
|
|
432
444
|
*
|
|
433
445
|
* ```ts
|
|
434
|
-
* await ablo.
|
|
446
|
+
* await ablo.items.update({
|
|
435
447
|
* id,
|
|
436
448
|
* data: { title },
|
|
437
449
|
* claim: {
|
|
438
|
-
* fields: (
|
|
439
|
-
* description: 'Renaming the
|
|
450
|
+
* fields: (item) => item.title,
|
|
451
|
+
* description: 'Renaming the item to match the project brief.',
|
|
440
452
|
* },
|
|
441
453
|
* });
|
|
442
454
|
* ```
|
package/src/resources/where.ts
CHANGED
|
@@ -18,7 +18,13 @@ import { z } from 'zod';
|
|
|
18
18
|
import { AbloValidationError } from '../errors.js';
|
|
19
19
|
|
|
20
20
|
/** Primitive operand types allowed in a where clause. */
|
|
21
|
-
export
|
|
21
|
+
export const wherePrimitiveSchema = z.union([
|
|
22
|
+
z.string(),
|
|
23
|
+
z.number(),
|
|
24
|
+
z.boolean(),
|
|
25
|
+
z.null(),
|
|
26
|
+
]);
|
|
27
|
+
export type WherePrimitive = z.infer<typeof wherePrimitiveSchema>;
|
|
22
28
|
|
|
23
29
|
/**
|
|
24
30
|
* The comparison operators a {@link WhereClause} may use: equality and
|
|
@@ -93,6 +99,22 @@ export const WHERE_LIKE_OPS: ReadonlySet<WhereOp> = new Set<WhereOp>([
|
|
|
93
99
|
* Returns the classification the caller needs next, so the check and the branch
|
|
94
100
|
* are the same statement rather than two that can disagree.
|
|
95
101
|
*/
|
|
102
|
+
/**
|
|
103
|
+
* Whether an operand is the array form the set operators take.
|
|
104
|
+
*
|
|
105
|
+
* `Array.isArray` answers this at runtime but not in the type system: its
|
|
106
|
+
* signature narrows to `any[]`, which drops both the `readonly` and the element
|
|
107
|
+
* type, so a compiler that reached for it lost the operand type it had just
|
|
108
|
+
* finished validating and handed `any` to everything downstream. Declared here
|
|
109
|
+
* because operand shape is this module's vocabulary — the same reason
|
|
110
|
+
* {@link classifyWhereOperand} lives here rather than in either plane.
|
|
111
|
+
*/
|
|
112
|
+
export function isWhereOperandList(
|
|
113
|
+
value: WherePrimitive | readonly WherePrimitive[] | undefined,
|
|
114
|
+
): value is readonly WherePrimitive[] {
|
|
115
|
+
return Array.isArray(value);
|
|
116
|
+
}
|
|
117
|
+
|
|
96
118
|
export function classifyWhereOperand(
|
|
97
119
|
op: WhereOp,
|
|
98
120
|
value: unknown,
|
|
@@ -134,10 +156,27 @@ export function classifyWhereOperand(
|
|
|
134
156
|
*
|
|
135
157
|
* The value is a single primitive for scalar operators and an array of
|
|
136
158
|
* primitives for IN/NOT IN.
|
|
159
|
+
*
|
|
160
|
+
* This is a schema rather than a bare type because the grammar crosses a
|
|
161
|
+
* boundary: a filtered read arrives at the collection route as text, and the
|
|
162
|
+
* route has to decide whether what it received is a clause before compiling it
|
|
163
|
+
* to SQL. Declaring the shape twice — once to check at the edge, once to type
|
|
164
|
+
* the interior — is how the two planes came to disagree about `IN`.
|
|
137
165
|
*/
|
|
138
|
-
export
|
|
139
|
-
|
|
140
|
-
|
|
166
|
+
export const whereClauseSchema = z.union([
|
|
167
|
+
z.tuple([z.string(), wherePrimitiveSchema]).readonly(),
|
|
168
|
+
z
|
|
169
|
+
.tuple([
|
|
170
|
+
z.string(),
|
|
171
|
+
whereOpSchema,
|
|
172
|
+
z.union([wherePrimitiveSchema, z.array(wherePrimitiveSchema).readonly()]),
|
|
173
|
+
])
|
|
174
|
+
.readonly(),
|
|
175
|
+
]);
|
|
176
|
+
export type WhereClause = z.infer<typeof whereClauseSchema>;
|
|
177
|
+
|
|
178
|
+
/** A whole filter as it travels: the AND-combined list of conditions. */
|
|
179
|
+
export const whereClausesSchema = z.array(whereClauseSchema).readonly();
|
|
141
180
|
|
|
142
181
|
/**
|
|
143
182
|
* Client-facing where shape for `load({where})` and `deleteMany({where})`.
|
|
@@ -158,3 +197,30 @@ export type LoadWhere<T> =
|
|
|
158
197
|
| Partial<T>
|
|
159
198
|
| { [K in keyof T]?: T[K] | readonly T[K][] }
|
|
160
199
|
| readonly WhereClause[];
|
|
200
|
+
|
|
201
|
+
/**
|
|
202
|
+
* Collapse either accepted {@link LoadWhere} form to the canonical clause list.
|
|
203
|
+
* Tuple input passes through; object input becomes one `['col', '=', val]` per
|
|
204
|
+
* key, or `['col', 'IN', vals]` where the value is an array.
|
|
205
|
+
*
|
|
206
|
+
* Detection: an array is tuple form, an object is object form.
|
|
207
|
+
*
|
|
208
|
+
* Every transport funnels through this. It used to live beside the WebSocket
|
|
209
|
+
* loader, which left the HTTP transport to walk `Object.entries` itself — and
|
|
210
|
+
* that copy dropped any value it found to be an object, so an `IN` filter and
|
|
211
|
+
* every tuple-form clause were discarded in silence and the read came back
|
|
212
|
+
* unfiltered.
|
|
213
|
+
*/
|
|
214
|
+
export function normalizeWhere(where: unknown): readonly WhereClause[] {
|
|
215
|
+
if (where == null) return [];
|
|
216
|
+
// Tuple form — assumed to already use server-side column names.
|
|
217
|
+
if (Array.isArray(where)) return where as readonly WhereClause[];
|
|
218
|
+
if (typeof where === 'object') {
|
|
219
|
+
return Object.entries(where as Record<string, unknown>).map(([key, value]) =>
|
|
220
|
+
Array.isArray(value)
|
|
221
|
+
? ([key, 'IN', value as readonly WherePrimitive[]] as WhereClause)
|
|
222
|
+
: ([key, value as WherePrimitive] as WhereClause),
|
|
223
|
+
);
|
|
224
|
+
}
|
|
225
|
+
return [];
|
|
226
|
+
}
|