@synthigy/sdk 0.1.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/LICENSE +21 -0
- package/README.md +102 -0
- package/codegen/codegen.mjs +499 -0
- package/codegen/lib.mjs +30 -0
- package/codegen/pull.mjs +47 -0
- package/package.json +63 -0
- package/src/index.d.ts +1108 -0
- package/src/index.js +2535 -0
- package/src/watch-query.js +533 -0
- package/src/watch.js +915 -0
package/src/index.d.ts
ADDED
|
@@ -0,0 +1,1108 @@
|
|
|
1
|
+
// ============================================================================
|
|
2
|
+
// Selection
|
|
3
|
+
// ============================================================================
|
|
4
|
+
|
|
5
|
+
/** A rel() config object — use directly or in an array for multiple occurrences */
|
|
6
|
+
export interface RelationConfig {
|
|
7
|
+
selections?: Selection
|
|
8
|
+
args?: Args
|
|
9
|
+
alias?: string
|
|
10
|
+
}
|
|
11
|
+
|
|
12
|
+
/**
|
|
13
|
+
* Selection for a /data operation.
|
|
14
|
+
*
|
|
15
|
+
* Shorthand forms (all equivalent at nesting level):
|
|
16
|
+
* ['name', 'email'] — scalar field list
|
|
17
|
+
* { name: null, email: null } — scalar object
|
|
18
|
+
* { roles: { name: null } } — relation (auto-wrapped)
|
|
19
|
+
* { roles: rel({ name: null }, { _limit: 5 }) } — rel() with args
|
|
20
|
+
* { roles: [rel({ name: null }, w1, 'a'), rel({ name: null }, w2, 'b')] } — multiple
|
|
21
|
+
*/
|
|
22
|
+
export type Selection =
|
|
23
|
+
| string[]
|
|
24
|
+
| { [field: string]: null | true | RelationConfig | RelationConfig[] | Selection }
|
|
25
|
+
|
|
26
|
+
// ============================================================================
|
|
27
|
+
// Filter / Args
|
|
28
|
+
// ============================================================================
|
|
29
|
+
|
|
30
|
+
export type Condition =
|
|
31
|
+
| { _eq: unknown }
|
|
32
|
+
| { _neq: unknown }
|
|
33
|
+
| { _gt: unknown }
|
|
34
|
+
| { _ge: unknown }
|
|
35
|
+
| { _lt: unknown }
|
|
36
|
+
| { _le: unknown }
|
|
37
|
+
| { _in: unknown[] }
|
|
38
|
+
| { _nin: unknown[] }
|
|
39
|
+
| { _like: string }
|
|
40
|
+
| { _ilike: string }
|
|
41
|
+
| { _is_null: boolean }
|
|
42
|
+
|
|
43
|
+
export interface WhereClause {
|
|
44
|
+
_and?: WhereClause[]
|
|
45
|
+
_or?: WhereClause[]
|
|
46
|
+
_not?: WhereClause
|
|
47
|
+
[field: string]: Condition | WhereClause | WhereClause[] | undefined
|
|
48
|
+
}
|
|
49
|
+
|
|
50
|
+
export interface Args {
|
|
51
|
+
_where?: WhereClause
|
|
52
|
+
_limit?: number
|
|
53
|
+
_offset?: number
|
|
54
|
+
_order_by?: Array<[string, 'asc' | 'desc']>
|
|
55
|
+
_distinct?: string[]
|
|
56
|
+
[field: string]: unknown
|
|
57
|
+
}
|
|
58
|
+
|
|
59
|
+
// ============================================================================
|
|
60
|
+
// Operations
|
|
61
|
+
// ============================================================================
|
|
62
|
+
|
|
63
|
+
export type KeyFormat = 'kebab' | 'snake' | 'camel'
|
|
64
|
+
|
|
65
|
+
export interface ExecOpts {
|
|
66
|
+
actingAs?: string
|
|
67
|
+
keyFormat?: KeyFormat
|
|
68
|
+
}
|
|
69
|
+
|
|
70
|
+
export interface WriteOpts extends ExecOpts {
|
|
71
|
+
/** Echo the written records instead of the default silent `{ count }`. */
|
|
72
|
+
returning?: boolean
|
|
73
|
+
}
|
|
74
|
+
|
|
75
|
+
export interface TreeExecOpts extends ExecOpts {
|
|
76
|
+
/** Return the raw flat array instead of composing into a tree/forest. */
|
|
77
|
+
raw?: boolean
|
|
78
|
+
/** Key under which children are nested (default: "_children"). */
|
|
79
|
+
childrenKey?: string
|
|
80
|
+
}
|
|
81
|
+
|
|
82
|
+
export type OperationResult<T = unknown> =
|
|
83
|
+
| { ok: true; data: T }
|
|
84
|
+
| { ok: false; error: { message: string; code: string } }
|
|
85
|
+
|
|
86
|
+
export type Operation = Record<string, unknown>
|
|
87
|
+
|
|
88
|
+
export type FetchFn = (body: object) => Promise<{ results: OperationResult[] }>
|
|
89
|
+
|
|
90
|
+
// ============================================================================
|
|
91
|
+
// Subscriptions — records-only
|
|
92
|
+
// ============================================================================
|
|
93
|
+
|
|
94
|
+
export interface SubscribeEvent {
|
|
95
|
+
/**
|
|
96
|
+
* The delta kind from the envelope payload: 'record/insert' | 'record/update'
|
|
97
|
+
* | 'record/delete' | 'relation/link' | 'relation/unlink' |
|
|
98
|
+
* 'entity/touched' | 'relation/touched' (plus the SDK's 'sse/open' sentinel).
|
|
99
|
+
* NOT the SSE `event:` field — that is always the literal 'data'. This is the
|
|
100
|
+
* raw envelope; the typed shape is ObserveEvent (see observe()).
|
|
101
|
+
*/
|
|
102
|
+
type: string
|
|
103
|
+
[key: string]: unknown
|
|
104
|
+
}
|
|
105
|
+
|
|
106
|
+
/**
|
|
107
|
+
* A subscription descriptor declares the records a consumer is watching.
|
|
108
|
+
* Most callers pass a bare array of xids; the object form lets you also
|
|
109
|
+
* narrow by operation.
|
|
110
|
+
*
|
|
111
|
+
* subscribe(['u-abc', 'u-def'])
|
|
112
|
+
* subscribe({ records: ['u-abc'], operations: ['update', 'link'] })
|
|
113
|
+
*/
|
|
114
|
+
export type Descriptor =
|
|
115
|
+
| string[]
|
|
116
|
+
| { records: string[]; operations?: string[] }
|
|
117
|
+
|
|
118
|
+
export interface SubscribeOpts {
|
|
119
|
+
/** Stable handle for later unsubscribe(); defaults to a hash of the descriptor. */
|
|
120
|
+
key?: string
|
|
121
|
+
}
|
|
122
|
+
|
|
123
|
+
export interface ListenOpts {
|
|
124
|
+
signal?: AbortSignal
|
|
125
|
+
}
|
|
126
|
+
|
|
127
|
+
export interface SubscriptionInfo {
|
|
128
|
+
/** Subscription type. 'data' for record-scoped events, 'deployed-model' / 'runtime-model' for model deploys. */
|
|
129
|
+
type?: 'data' | 'deployed-model' | 'runtime-model'
|
|
130
|
+
records?: string[]
|
|
131
|
+
operations?: string[]
|
|
132
|
+
}
|
|
133
|
+
|
|
134
|
+
export interface SubscriptionItem {
|
|
135
|
+
type?: 'data' | 'deployed-model' | 'runtime-model'
|
|
136
|
+
/** Required for `type: "data"`. Non-empty array of xid strings. */
|
|
137
|
+
records?: string[]
|
|
138
|
+
operations?: string[]
|
|
139
|
+
/** Optional stable handle for later unsubscribe(). */
|
|
140
|
+
key?: string
|
|
141
|
+
}
|
|
142
|
+
|
|
143
|
+
// ============================================================================
|
|
144
|
+
// History — temporal query surface
|
|
145
|
+
// ============================================================================
|
|
146
|
+
|
|
147
|
+
export interface HistoryEvent {
|
|
148
|
+
ts: string
|
|
149
|
+
recordXid?: string
|
|
150
|
+
entityXid?: string
|
|
151
|
+
attributeXid?: string
|
|
152
|
+
value?: unknown
|
|
153
|
+
op?: 'insert' | 'update' | 'delete' | 'link' | 'unlink' | string
|
|
154
|
+
actorXid?: string
|
|
155
|
+
requestId?: string
|
|
156
|
+
scopeXid?: string
|
|
157
|
+
txid?: number | string
|
|
158
|
+
// Relation events
|
|
159
|
+
relationXid?: string
|
|
160
|
+
fromXid?: string
|
|
161
|
+
toXid?: string
|
|
162
|
+
}
|
|
163
|
+
|
|
164
|
+
export interface HistoryEventsOpts {
|
|
165
|
+
recordXid?: string
|
|
166
|
+
between?: [string, string]
|
|
167
|
+
tenant?: string
|
|
168
|
+
limit?: number
|
|
169
|
+
/** 'entity' (default) or 'relation' */
|
|
170
|
+
track?: 'entity' | 'relation'
|
|
171
|
+
}
|
|
172
|
+
|
|
173
|
+
export interface HistoryDiffResult<T = Record<string, unknown>> {
|
|
174
|
+
before: T | null
|
|
175
|
+
after: T | null
|
|
176
|
+
changed: string[]
|
|
177
|
+
}
|
|
178
|
+
|
|
179
|
+
export interface HistoryTimelineOpts {
|
|
180
|
+
between?: [string, string]
|
|
181
|
+
groupBy?: 'request' | 'actor' | 'scope'
|
|
182
|
+
tenant?: string
|
|
183
|
+
limit?: number
|
|
184
|
+
}
|
|
185
|
+
|
|
186
|
+
export interface HistorySinceOpts {
|
|
187
|
+
cursor?: string
|
|
188
|
+
tenant?: string
|
|
189
|
+
limit?: number
|
|
190
|
+
track?: 'entity' | 'relation'
|
|
191
|
+
}
|
|
192
|
+
|
|
193
|
+
// ============================================================================
|
|
194
|
+
// Lint
|
|
195
|
+
// ============================================================================
|
|
196
|
+
|
|
197
|
+
export interface LintDiagnostic {
|
|
198
|
+
severity: 'error' | 'warning'
|
|
199
|
+
message: string
|
|
200
|
+
/** Byte offset (inclusive) into the source. */
|
|
201
|
+
from: number
|
|
202
|
+
/** Byte offset (exclusive) into the source. */
|
|
203
|
+
to: number
|
|
204
|
+
/** 1-based line/column at `from`. */
|
|
205
|
+
start: { line: number; col: number }
|
|
206
|
+
/** 1-based line/column at `to`. */
|
|
207
|
+
end: { line: number; col: number }
|
|
208
|
+
}
|
|
209
|
+
|
|
210
|
+
// ============================================================================
|
|
211
|
+
// Schema
|
|
212
|
+
// ============================================================================
|
|
213
|
+
|
|
214
|
+
export interface SchemaRelation {
|
|
215
|
+
to: string
|
|
216
|
+
cardinality: 'one' | 'many'
|
|
217
|
+
}
|
|
218
|
+
|
|
219
|
+
export interface SchemaAttribute {
|
|
220
|
+
/** Scalar or reference type: 'string' | 'boolean' | 'int' | 'float' | 'timestamp' | 'json' | 'uuid' | 'enum' | 'hashed' | 'encrypted' | 'transit' | <referenced-entity> */
|
|
221
|
+
type: string
|
|
222
|
+
/** Present (and always `false`) only for mandatory fields; absent ⇒ nullable.
|
|
223
|
+
* The terse-by-exception form: most fields are optional and omit this key. */
|
|
224
|
+
nullable?: boolean
|
|
225
|
+
/** Enum member values, verbatim (not re-cased). Present only when `type === 'enum'`. */
|
|
226
|
+
enum?: string[]
|
|
227
|
+
/** Input-only — written but never read back (no plaintext). Present only for
|
|
228
|
+
* `hashed` fields. (`encrypted` reads back as ciphertext, so it is NOT write-only.) */
|
|
229
|
+
'write-only'?: boolean
|
|
230
|
+
}
|
|
231
|
+
|
|
232
|
+
export interface SchemaEntity {
|
|
233
|
+
name: string
|
|
234
|
+
/** Field name (kebab-case) → attribute schema. Keys are canonical kebab-case
|
|
235
|
+
* regardless of any `key_format` used on `/data` responses. */
|
|
236
|
+
attributes: Record<string, SchemaAttribute>
|
|
237
|
+
relations: Record<string, SchemaRelation>
|
|
238
|
+
constraints: { unique: string[][] }
|
|
239
|
+
}
|
|
240
|
+
|
|
241
|
+
export interface Schema {
|
|
242
|
+
'id-key': 'xid' | 'euuid'
|
|
243
|
+
/** Entity name (kebab-case) → entity schema */
|
|
244
|
+
entities: Record<string, SchemaEntity>
|
|
245
|
+
}
|
|
246
|
+
|
|
247
|
+
// ============================================================================
|
|
248
|
+
// Error
|
|
249
|
+
// ============================================================================
|
|
250
|
+
|
|
251
|
+
/**
|
|
252
|
+
* Wire-level error code emitted by the Synthigy server. Union of the
|
|
253
|
+
* stable typed codes; falls through to `string` so future codes don't
|
|
254
|
+
* become a breaking change here.
|
|
255
|
+
*
|
|
256
|
+
* Discriminate on these instead of regex-matching `error.message`:
|
|
257
|
+
*
|
|
258
|
+
* try { await client.search(...) } catch (e) {
|
|
259
|
+
* if (e instanceof SynthigyError && e.code === 'UNKNOWN_ATTRIBUTE') {
|
|
260
|
+
* // e.details may carry { entity, attribute, hint }
|
|
261
|
+
* }
|
|
262
|
+
* }
|
|
263
|
+
*
|
|
264
|
+
* Auth / impersonation:
|
|
265
|
+
* UNAUTHORIZED, CLIENT_NOT_FOUND, CLIENT_INACTIVE,
|
|
266
|
+
* PUBLIC_CLIENT_FORBIDDEN, NOT_TRUSTED, USER_NOT_FOUND, FORBIDDEN
|
|
267
|
+
*
|
|
268
|
+
* Request shape:
|
|
269
|
+
* INVALID_BODY, NO_OPERATIONS, BAD_OP, MISSING_ENTITY,
|
|
270
|
+
* BAD_ARGS_SHAPE
|
|
271
|
+
*
|
|
272
|
+
* Schema resolution (typed throws ride along the engine's existing
|
|
273
|
+
* walks; ex-`INTERNAL_ERROR`):
|
|
274
|
+
* UNKNOWN_OP, UNKNOWN_ENTITY, UNKNOWN_ATTRIBUTE, UNKNOWN_OPERATOR,
|
|
275
|
+
* MISSING_ON, MISSING_ROOT, XSQL_PARSE_ERROR,
|
|
276
|
+
* PARAM_MISSING, PARAM_TYPE_MISMATCH
|
|
277
|
+
*
|
|
278
|
+
* DB:
|
|
279
|
+
* FK_VIOLATION, UNIQUE_VIOLATION, NOT_NULL_VIOLATION,
|
|
280
|
+
* CHECK_VIOLATION, TYPE_CAST_FAILURE, TIMEOUT
|
|
281
|
+
*
|
|
282
|
+
* Transport / catch-all:
|
|
283
|
+
* HTTP_ERROR, OPERATION_ERROR, INTERNAL_ERROR
|
|
284
|
+
*
|
|
285
|
+
* Client-side (no server round trip):
|
|
286
|
+
* NO_TOKEN — no token source configured; see ClientConfig.
|
|
287
|
+
*/
|
|
288
|
+
export type SynthigyErrorCode =
|
|
289
|
+
| 'UNAUTHORIZED' | 'CLIENT_NOT_FOUND' | 'CLIENT_INACTIVE'
|
|
290
|
+
| 'PUBLIC_CLIENT_FORBIDDEN' | 'NOT_TRUSTED' | 'USER_NOT_FOUND'
|
|
291
|
+
| 'FORBIDDEN' | 'NO_TOKEN'
|
|
292
|
+
| 'INVALID_BODY' | 'NO_OPERATIONS' | 'BAD_OP' | 'MISSING_ENTITY'
|
|
293
|
+
| 'BAD_ARGS_SHAPE'
|
|
294
|
+
| 'UNKNOWN_OP' | 'UNKNOWN_ENTITY' | 'UNKNOWN_ATTRIBUTE' | 'UNKNOWN_OPERATOR'
|
|
295
|
+
| 'MISSING_ON' | 'MISSING_ROOT' | 'XSQL_PARSE_ERROR'
|
|
296
|
+
| 'PARAM_MISSING' | 'PARAM_TYPE_MISMATCH'
|
|
297
|
+
| 'FK_VIOLATION' | 'UNIQUE_VIOLATION' | 'NOT_NULL_VIOLATION'
|
|
298
|
+
| 'CHECK_VIOLATION' | 'TYPE_CAST_FAILURE' | 'TIMEOUT'
|
|
299
|
+
| 'HTTP_ERROR' | 'OPERATION_ERROR' | 'INTERNAL_ERROR'
|
|
300
|
+
| (string & {})
|
|
301
|
+
|
|
302
|
+
export class SynthigyError extends Error {
|
|
303
|
+
name: 'SynthigyError'
|
|
304
|
+
code: SynthigyErrorCode
|
|
305
|
+
details?: unknown
|
|
306
|
+
constructor(message: string, code: string, details?: unknown)
|
|
307
|
+
}
|
|
308
|
+
|
|
309
|
+
// ============================================================================
|
|
310
|
+
// Client config
|
|
311
|
+
// ============================================================================
|
|
312
|
+
|
|
313
|
+
export interface ClientConfig {
|
|
314
|
+
endpoint: string
|
|
315
|
+
/** OAuth client ID — required with clientSecret for client credentials */
|
|
316
|
+
clientId?: string
|
|
317
|
+
clientSecret?: string
|
|
318
|
+
/**
|
|
319
|
+
* Static bearer token — alternative to client credentials. On Node,
|
|
320
|
+
* with none of clientId+clientSecret/token given, the client also falls
|
|
321
|
+
* back to SYNTHIGY_TOKEN env, then a SYNTHIGY_SUPERVISED=1 stdio ask;
|
|
322
|
+
* with no source at all it throws SynthigyError{code: 'NO_TOKEN'}.
|
|
323
|
+
*/
|
|
324
|
+
token?: string
|
|
325
|
+
scope?: string
|
|
326
|
+
/**
|
|
327
|
+
* Default client_credentials audience, applied to every mint this client
|
|
328
|
+
* makes (falls back to SYNTHIGY_AUDIENCE on Node). The platform's audience
|
|
329
|
+
* model is opt-in: a mint naming no audience resolves to the identity-only
|
|
330
|
+
* OIDC audience, which /data rejects. The server publishes its /data
|
|
331
|
+
* audience at /.well-known/synthigy as `auth.oidc.audience`.
|
|
332
|
+
*/
|
|
333
|
+
audience?: string
|
|
334
|
+
/** Default acting_as applied to every request unless overridden per-call */
|
|
335
|
+
actingAs?: string
|
|
336
|
+
keyFormat?: KeyFormat
|
|
337
|
+
/**
|
|
338
|
+
* Hold the upstream SSE connection open across windows where no user
|
|
339
|
+
* watches are active. Default: false (SSE closes when the last watch
|
|
340
|
+
* closes — correct for browser SPAs).
|
|
341
|
+
*
|
|
342
|
+
* Set true for long-lived BFFs / Node services: the server-side
|
|
343
|
+
* plug session is keyed by [sub, client_id] and is torn down when
|
|
344
|
+
* the SSE drops, so a tab refresh that briefly closes every watch can
|
|
345
|
+
* lose published events. With keepAlive on, the SSE stays open from
|
|
346
|
+
* `createClient()` until `client.close()`.
|
|
347
|
+
*/
|
|
348
|
+
keepAlive?: boolean
|
|
349
|
+
/**
|
|
350
|
+
* Request timeout in milliseconds. Applied to all one-shot requests (exec,
|
|
351
|
+
* subscribe, etc.) via `AbortSignal.timeout`. Not applied when the call
|
|
352
|
+
* already carries an AbortSignal (e.g. listen() with opts.signal).
|
|
353
|
+
* Requires Node ≥ 17.3 / modern browsers.
|
|
354
|
+
*/
|
|
355
|
+
timeout?: number
|
|
356
|
+
/**
|
|
357
|
+
* Optional fetch implementation. Defaults to `globalThis.fetch`.
|
|
358
|
+
* Pass `undici.fetch` with a shared `Agent` for pooled connections in Node.
|
|
359
|
+
*/
|
|
360
|
+
fetch?: typeof globalThis.fetch
|
|
361
|
+
}
|
|
362
|
+
|
|
363
|
+
// ============================================================================
|
|
364
|
+
// Client
|
|
365
|
+
// ============================================================================
|
|
366
|
+
|
|
367
|
+
export class SynthigyClient {
|
|
368
|
+
/**
|
|
369
|
+
* Tear the client down — close every live watch, release the keepAlive
|
|
370
|
+
* hold, close the SSE. Idempotent. Use this on shutdown so the server
|
|
371
|
+
* drops the plug session promptly instead of waiting for the SSE
|
|
372
|
+
* to time out.
|
|
373
|
+
*/
|
|
374
|
+
close(): void
|
|
375
|
+
|
|
376
|
+
/**
|
|
377
|
+
* Returns a FetchFn for `@synthigy/buffer`'s `connect({ fetchFn })`.
|
|
378
|
+
* Shares this client's token cache, endpoint, and fetch implementation.
|
|
379
|
+
*/
|
|
380
|
+
fetchFn(): FetchFn
|
|
381
|
+
|
|
382
|
+
/** Get a bearer token, optionally for a specific audience (Synthigy as IdP). */
|
|
383
|
+
token(opts?: { audience?: string }): Promise<string>
|
|
384
|
+
|
|
385
|
+
/** Execute raw operations against /data. */
|
|
386
|
+
exec(operations: Operation[], opts?: ExecOpts): Promise<OperationResult[]>
|
|
387
|
+
|
|
388
|
+
// ── Read ──────────────────────────────────────────────────────────────────
|
|
389
|
+
|
|
390
|
+
search<T = Record<string, unknown>>(
|
|
391
|
+
entity: string,
|
|
392
|
+
args: Args | undefined,
|
|
393
|
+
selection: Selection,
|
|
394
|
+
opts?: ExecOpts,
|
|
395
|
+
): Promise<T[]>
|
|
396
|
+
|
|
397
|
+
/**
|
|
398
|
+
* Run an XSQL selection-DSL query in `get` mode. Returns a single
|
|
399
|
+
* record or `null`. The XSQL must express identity via unique-key
|
|
400
|
+
* predicates at the root (`xid = ?xid:string` etc.).
|
|
401
|
+
*/
|
|
402
|
+
query<T = Record<string, unknown>>(
|
|
403
|
+
xsql: string,
|
|
404
|
+
params: Record<string, unknown> | undefined,
|
|
405
|
+
opts: ExecOpts & { op: 'get'; entity?: string },
|
|
406
|
+
): Promise<T | null>
|
|
407
|
+
|
|
408
|
+
/**
|
|
409
|
+
* Run an XSQL selection-DSL query with optional named parameters.
|
|
410
|
+
* The wire envelope carries the XSQL string in `selections` plus a
|
|
411
|
+
* `params` map for `?name:type[]` placeholders. Default `op` is
|
|
412
|
+
* `'search'`; pass `opts.op` to use `'get'` / `'slice'` / `'purge'`.
|
|
413
|
+
*/
|
|
414
|
+
query<T = Record<string, unknown>>(
|
|
415
|
+
xsql: string,
|
|
416
|
+
params?: Record<string, unknown>,
|
|
417
|
+
opts?: ExecOpts & {
|
|
418
|
+
op?: 'search' | 'slice' | 'purge'
|
|
419
|
+
entity?: string
|
|
420
|
+
},
|
|
421
|
+
): Promise<T[]>
|
|
422
|
+
|
|
423
|
+
get<T = Record<string, unknown>>(
|
|
424
|
+
entity: string,
|
|
425
|
+
args: Record<string, unknown>,
|
|
426
|
+
selection: Selection,
|
|
427
|
+
opts?: ExecOpts,
|
|
428
|
+
): Promise<T | null>
|
|
429
|
+
|
|
430
|
+
sqlTemplate<T = Record<string, unknown>>(
|
|
431
|
+
template: string,
|
|
432
|
+
params?: Record<string, unknown> | unknown[],
|
|
433
|
+
opts?: ExecOpts & { cached?: boolean },
|
|
434
|
+
): Promise<T[]>
|
|
435
|
+
|
|
436
|
+
/** Returns a forest of composed trees. Pass `{ raw: true }` to get the flat array instead. */
|
|
437
|
+
searchTree<T = Record<string, unknown>>(
|
|
438
|
+
entity: string,
|
|
439
|
+
on: string,
|
|
440
|
+
args: Args | undefined,
|
|
441
|
+
selection: Selection,
|
|
442
|
+
opts?: TreeExecOpts,
|
|
443
|
+
): Promise<(T & { [key: string]: unknown })[]>
|
|
444
|
+
|
|
445
|
+
/** Returns a single composed tree rooted at `root`, or null if not found. Pass `{ raw: true }` to get the flat array instead. */
|
|
446
|
+
getTree<T = Record<string, unknown>>(
|
|
447
|
+
entity: string,
|
|
448
|
+
root: string,
|
|
449
|
+
on: string,
|
|
450
|
+
selection: Selection,
|
|
451
|
+
opts?: TreeExecOpts,
|
|
452
|
+
): Promise<(T & { [key: string]: unknown }) | null>
|
|
453
|
+
|
|
454
|
+
// ── Write ─────────────────────────────────────────────────────────────────
|
|
455
|
+
|
|
456
|
+
/**
|
|
457
|
+
* Writes are silent by default — the server answers `{ count }`. Pass
|
|
458
|
+
* `returning: true` for the written records, or mint ids up front with
|
|
459
|
+
* `newXid()`, which is the cheap way to know what you wrote.
|
|
460
|
+
*/
|
|
461
|
+
sync<T = Record<string, unknown>>(
|
|
462
|
+
entity: string,
|
|
463
|
+
data: Record<string, unknown> | Record<string, unknown>[],
|
|
464
|
+
opts?: WriteOpts,
|
|
465
|
+
): Promise<T>
|
|
466
|
+
|
|
467
|
+
/** Same `returning` contract as sync. */
|
|
468
|
+
stack<T = Record<string, unknown>>(
|
|
469
|
+
entity: string,
|
|
470
|
+
data: Record<string, unknown> | Record<string, unknown>[],
|
|
471
|
+
opts?: WriteOpts,
|
|
472
|
+
): Promise<T>
|
|
473
|
+
|
|
474
|
+
/** Returns a map of relation-name → boolean (true = sliced successfully). */
|
|
475
|
+
slice(
|
|
476
|
+
entity: string,
|
|
477
|
+
args: Record<string, unknown>,
|
|
478
|
+
selection?: Selection,
|
|
479
|
+
opts?: ExecOpts,
|
|
480
|
+
): Promise<Record<string, boolean>>
|
|
481
|
+
|
|
482
|
+
delete(
|
|
483
|
+
entity: string,
|
|
484
|
+
data: Record<string, unknown>,
|
|
485
|
+
opts?: ExecOpts,
|
|
486
|
+
): Promise<boolean>
|
|
487
|
+
|
|
488
|
+
purge<T = Record<string, unknown>>(
|
|
489
|
+
entity: string,
|
|
490
|
+
args: Args | undefined,
|
|
491
|
+
selection?: Selection,
|
|
492
|
+
opts?: ExecOpts,
|
|
493
|
+
): Promise<T>
|
|
494
|
+
|
|
495
|
+
// ── Subscriptions ─────────────────────────────────────────────────────────
|
|
496
|
+
//
|
|
497
|
+
// The SDK tracks the active subscription set locally; each call below
|
|
498
|
+
// mutates that local set and POSTs the full set to the server's
|
|
499
|
+
// /data/subscription/set endpoint (full-set replacement semantics).
|
|
500
|
+
|
|
501
|
+
/**
|
|
502
|
+
* Add a record-scoped data subscription. Re-subscribing with the same
|
|
503
|
+
* descriptor (or same `opts.key`) replaces the prior entry.
|
|
504
|
+
*
|
|
505
|
+
* subscribe(['u-abc', 'u-def'])
|
|
506
|
+
* subscribe({ records: ['u-abc'], operations: ['update'] }, { key: 'page-detail' })
|
|
507
|
+
*/
|
|
508
|
+
subscribe(descriptor: Descriptor, opts?: SubscribeOpts): Promise<{ ok: boolean }>
|
|
509
|
+
|
|
510
|
+
/**
|
|
511
|
+
* Remove a record-scoped data subscription. `handle` is either the
|
|
512
|
+
* `key` you used at subscribe-time or an equivalent descriptor.
|
|
513
|
+
*/
|
|
514
|
+
unsubscribe(handle: string | Descriptor): Promise<{ ok: boolean }>
|
|
515
|
+
|
|
516
|
+
/** Subscribe to model deploy events. raw=true uses 'deployed-model'; otherwise 'runtime-model'. */
|
|
517
|
+
subscribeModel(opts?: { raw?: boolean }): Promise<{ ok: boolean }>
|
|
518
|
+
|
|
519
|
+
/** Unsubscribe from model deploy events. */
|
|
520
|
+
unsubscribeModel(opts?: { raw?: boolean }): Promise<{ ok: boolean }>
|
|
521
|
+
|
|
522
|
+
/** Replace the entire subscription set in one call. */
|
|
523
|
+
setSubscriptions(items: SubscriptionItem[]): Promise<{ ok: boolean }>
|
|
524
|
+
|
|
525
|
+
/** Clear all subscriptions for this session. */
|
|
526
|
+
clearSubscriptions(): Promise<{ ok: boolean }>
|
|
527
|
+
|
|
528
|
+
/** Fetch current subscription state from the server. */
|
|
529
|
+
subscriptions(): Promise<{ subscriptions: SubscriptionInfo[] }>
|
|
530
|
+
|
|
531
|
+
// ── History — temporal query surface ─────────────────────────────────────
|
|
532
|
+
|
|
533
|
+
history: {
|
|
534
|
+
/** State of a record at timestamp T. */
|
|
535
|
+
getAt<T = Record<string, unknown>>(
|
|
536
|
+
recordXid: string,
|
|
537
|
+
at: string,
|
|
538
|
+
opts?: { tenant?: string; includeDeleted?: boolean },
|
|
539
|
+
): Promise<T | null>
|
|
540
|
+
|
|
541
|
+
/** Events for a record (or any record) over a time range. */
|
|
542
|
+
events(opts?: HistoryEventsOpts): Promise<HistoryEvent[]>
|
|
543
|
+
|
|
544
|
+
/** Diff a record between two timestamps. */
|
|
545
|
+
diff<T = Record<string, unknown>>(
|
|
546
|
+
recordXid: string,
|
|
547
|
+
fromTs: string,
|
|
548
|
+
toTs: string,
|
|
549
|
+
opts?: { tenant?: string },
|
|
550
|
+
): Promise<HistoryDiffResult<T>>
|
|
551
|
+
|
|
552
|
+
/** Events grouped by request, actor, or scope. */
|
|
553
|
+
timeline(opts: HistoryTimelineOpts): Promise<Record<string, HistoryEvent[]>>
|
|
554
|
+
|
|
555
|
+
/** Events strictly after cursor timestamp T, oldest-first. */
|
|
556
|
+
since(opts?: HistorySinceOpts): Promise<HistoryEvent[]>
|
|
557
|
+
}
|
|
558
|
+
|
|
559
|
+
/** Fetch the IAM-filtered model schema via `GET /schema`. Pass entity names to filter. */
|
|
560
|
+
schema(entities?: string[]): Promise<Schema>
|
|
561
|
+
|
|
562
|
+
/**
|
|
563
|
+
* Lint an XSQL source string against the IAM-projected schema.
|
|
564
|
+
* `entity` enables schema-aware checks; omit for syntax-only lint.
|
|
565
|
+
*/
|
|
566
|
+
lint(source: string, opts?: {
|
|
567
|
+
entity?: string
|
|
568
|
+
op?: 'search' | 'get' | 'slice' | 'purge'
|
|
569
|
+
}): Promise<LintDiagnostic[]>
|
|
570
|
+
|
|
571
|
+
/**
|
|
572
|
+
* Mint a one-time account-claim link via `POST /oauth/onboard`.
|
|
573
|
+
* Confidential client whose principal administers the account — RBAC
|
|
574
|
+
* update on User plus the row inside its owner-group write scope, which
|
|
575
|
+
* the shipped User Provisioner role grants — else `PROVISION_FORBIDDEN`;
|
|
576
|
+
* this client's own client_credentials identity IS that principal. `xid`
|
|
577
|
+
* addresses an EXISTING account (create it first via `sync('iam/user', ...)`);
|
|
578
|
+
* a blank xid fails with `XID_REQUIRED`, one that doesn't resolve with
|
|
579
|
+
* `USER_NOT_FOUND`. `reset` soft-recycles the account first (strips its
|
|
580
|
+
* federated identities, nulls its password, revokes its live
|
|
581
|
+
* sessions/tokens — leaves `active` untouched); `methods` restricts the
|
|
582
|
+
* claim page (e.g. `['password']`); omitted allows every active provider
|
|
583
|
+
* plus password. `returnUrl`, if set, must match one of THIS client's
|
|
584
|
+
* registered redirections (or be a loopback URI); a successful DIRECT
|
|
585
|
+
* claim (browser) then redirects there instead of the generic status page.
|
|
586
|
+
*/
|
|
587
|
+
onboard(xid: string, opts?: {
|
|
588
|
+
reset?: boolean
|
|
589
|
+
methods?: string[]
|
|
590
|
+
ttlSeconds?: number
|
|
591
|
+
returnUrl?: string
|
|
592
|
+
}): Promise<{ onboard_url: string; expires_at: number; user: { xid: string } }>
|
|
593
|
+
|
|
594
|
+
/**
|
|
595
|
+
* Redeem an onboarding ticket without a browser via
|
|
596
|
+
* `POST /oauth/onboard/complete` — the indirect face of the SAME ticket
|
|
597
|
+
* `onboard()` mints. Must be called by the SAME client that minted the
|
|
598
|
+
* ticket. Never sets a credential — credentials are subject-only; the
|
|
599
|
+
* account activates with none. Give it one via the claim page (password
|
|
600
|
+
* or a federated identity) or a later ticket.
|
|
601
|
+
*/
|
|
602
|
+
onboardComplete(ticket: string): Promise<{ user: { xid: string }; active: boolean }>
|
|
603
|
+
|
|
604
|
+
/**
|
|
605
|
+
* Fetch the full ERD model via `op: "deployed-model"`.
|
|
606
|
+
* Unlike `schema()`, this returns the raw model including internal
|
|
607
|
+
* metadata and un-filtered entities. Requires `dataset:load` scope.
|
|
608
|
+
*/
|
|
609
|
+
deployedModel(opts?: ExecOpts): Promise<Record<string, unknown>>
|
|
610
|
+
|
|
611
|
+
/**
|
|
612
|
+
* Fetch the runtime (enhanced) ERD model via `op: "runtime-model"` —
|
|
613
|
+
* the model as the running system actually emits it, with audit
|
|
614
|
+
* timestamps, computed defaults, and reference-typed attrs surfaced
|
|
615
|
+
* as first-class relations. Prefer this over `deployedModel()` when
|
|
616
|
+
* you want to see what the live system emits. The server currently
|
|
617
|
+
* returns this transit-encoded, so the result may be a string.
|
|
618
|
+
* Requires `dataset:load` scope.
|
|
619
|
+
*/
|
|
620
|
+
runtimeModel(opts?: ExecOpts): Promise<Record<string, unknown> | string>
|
|
621
|
+
|
|
622
|
+
/**
|
|
623
|
+
* Open a persistent SSE connection to `/data/events`.
|
|
624
|
+
* Reconnects automatically with exponential backoff.
|
|
625
|
+
* Sends `Last-Event-ID` on reconnect to avoid missing events.
|
|
626
|
+
* Cancel via `AbortSignal`.
|
|
627
|
+
*/
|
|
628
|
+
listen(opts?: ListenOpts): AsyncGenerator<SubscribeEvent>
|
|
629
|
+
|
|
630
|
+
/**
|
|
631
|
+
* One-call live primitive: subscribe + SSE + auto-reconnect (+ optional
|
|
632
|
+
* history backfill). Yields a typed event stream filtered to the
|
|
633
|
+
* records named by the descriptor. Cleanup on iterator close:
|
|
634
|
+
* unsubscribes locally.
|
|
635
|
+
*
|
|
636
|
+
* Server tears down subscription state on each SSE close, so observe()
|
|
637
|
+
* re-subscribes before every new SSE session. The plug's default
|
|
638
|
+
* best-effort delivery is normally enough; pass `backfill: true` to
|
|
639
|
+
* also replay `/history` events missed during a disconnect.
|
|
640
|
+
*
|
|
641
|
+
* for await (const ev of client.observe(['u-abc'])) { ... }
|
|
642
|
+
* for await (const ev of client.observe({
|
|
643
|
+
* records: [userXid, teamXid],
|
|
644
|
+
* operations: ['update', 'link'],
|
|
645
|
+
* })) { ... }
|
|
646
|
+
*/
|
|
647
|
+
observe(descriptor: Descriptor, opts?: ObserveOpts): AsyncGenerator<ObserveEvent>
|
|
648
|
+
|
|
649
|
+
/**
|
|
650
|
+
* Open a live, multiplexed subscription. Many `watch()` calls on the
|
|
651
|
+
* same client fuse onto ONE SSE connection and ONE consolidated
|
|
652
|
+
* subscription set on the server. Events are coalesced per-record
|
|
653
|
+
* and attribute-name keyed.
|
|
654
|
+
*
|
|
655
|
+
* Each `for await (const ev of w.events)` gets a fresh iterator with
|
|
656
|
+
* its own buffer — siblings in a component tree can consume the same
|
|
657
|
+
* Watch independently.
|
|
658
|
+
*/
|
|
659
|
+
watch(interest: WatchInterest, opts?: WatchOpts): Watch
|
|
660
|
+
|
|
661
|
+
/** Open a separate stream of schema-deploy events. */
|
|
662
|
+
watchSchema(): SchemaWatch
|
|
663
|
+
|
|
664
|
+
/**
|
|
665
|
+
* Live result-set for a query. Built on `watch()`. Snapshots via
|
|
666
|
+
* /data search, watches the result xids + the entity's relations,
|
|
667
|
+
* coalesced re-runs the query on each event window, diffs against
|
|
668
|
+
* last result, emits query/added | query/changed | query/removed.
|
|
669
|
+
*/
|
|
670
|
+
watchQuery: WatchQueryFn
|
|
671
|
+
|
|
672
|
+
/**
|
|
673
|
+
* Live raw-SQL result. Composes `sqlTemplate()` snapshot with
|
|
674
|
+
* `watch()` subscription on the entities named in `opts.entities`.
|
|
675
|
+
*
|
|
676
|
+
* SQL is opaque to the SDK, so the caller declares which entities
|
|
677
|
+
* the query reads from — those entities' relation xids become the
|
|
678
|
+
* watch interest. Add specific record xids via `opts.records` if
|
|
679
|
+
* you also need per-row update events to trigger refresh.
|
|
680
|
+
*/
|
|
681
|
+
watchSqlTemplate<T = Record<string, unknown>>(
|
|
682
|
+
template: string,
|
|
683
|
+
params?: Record<string, unknown> | unknown[],
|
|
684
|
+
opts?: SqlTemplateWatchOpts,
|
|
685
|
+
): SqlTemplateWatch<T>
|
|
686
|
+
}
|
|
687
|
+
|
|
688
|
+
export interface SqlTemplateWatchOpts extends WatchOpts {
|
|
689
|
+
/** Entity names the SQL reads from. Required — used to compute the
|
|
690
|
+
* relation-xid watch interest. */
|
|
691
|
+
entities: string[]
|
|
692
|
+
/** Extra record xids to include in the watch interest. Use when the
|
|
693
|
+
* SQL aggregates over fields that can change without inserts/deletes
|
|
694
|
+
* (e.g. avg(value) where existing rows' values are mutable). */
|
|
695
|
+
records?: string[]
|
|
696
|
+
/** Forwarded to sqlTemplate() — actingAs, abort signal, etc. */
|
|
697
|
+
searchOpts?: ExecOpts
|
|
698
|
+
}
|
|
699
|
+
|
|
700
|
+
export interface SqlTemplateWatch<T = Record<string, unknown>> {
|
|
701
|
+
/** Current result: array of rows. */
|
|
702
|
+
readonly value: T[] | null
|
|
703
|
+
/** Convenience: first row, or null. */
|
|
704
|
+
readonly first: T | null
|
|
705
|
+
/** Resolves once the initial snapshot + watch are wired. */
|
|
706
|
+
ready(): Promise<void>
|
|
707
|
+
/** Force a fresh re-run of the SQL. */
|
|
708
|
+
refresh(): Promise<void>
|
|
709
|
+
/** Async iterable of events. */
|
|
710
|
+
readonly events: AsyncIterable<SqlTemplateWatchEvent<T> | WatchSentinel>
|
|
711
|
+
/** Tear down the watch. */
|
|
712
|
+
close(): void
|
|
713
|
+
}
|
|
714
|
+
|
|
715
|
+
export type SqlTemplateWatchEvent<T = Record<string, unknown>> =
|
|
716
|
+
| { type: 'result/changed'; before: T[] | null; after: T[]; ts?: string; txid?: string; actor?: string; request?: string }
|
|
717
|
+
|
|
718
|
+
// ============================================================================
|
|
719
|
+
// Live query / aggregate types
|
|
720
|
+
// ============================================================================
|
|
721
|
+
|
|
722
|
+
export interface WatchQueryOpts extends WatchOpts {
|
|
723
|
+
/** Forwarded to search()/query() as the request's options bag. */
|
|
724
|
+
searchOpts?: ExecOpts
|
|
725
|
+
}
|
|
726
|
+
|
|
727
|
+
export interface XsqlWatchOpts extends WatchOpts {
|
|
728
|
+
/** Kebab-case root entity name — required so the SDK knows which
|
|
729
|
+
* relations to watch for new-member events. */
|
|
730
|
+
entity: string
|
|
731
|
+
searchOpts?: ExecOpts
|
|
732
|
+
/** Op override for the underlying client.query() call (default 'search'). */
|
|
733
|
+
op?: 'search' | 'get' | 'slice' | 'purge'
|
|
734
|
+
}
|
|
735
|
+
|
|
736
|
+
export interface WatchQueryFn {
|
|
737
|
+
<T = Record<string, unknown>>(
|
|
738
|
+
entity: string,
|
|
739
|
+
args: Args,
|
|
740
|
+
selection: Selection,
|
|
741
|
+
opts?: WatchQueryOpts,
|
|
742
|
+
): QueryWatch<T>
|
|
743
|
+
xsql<T = Record<string, unknown>>(
|
|
744
|
+
xsql: string,
|
|
745
|
+
params?: Record<string, unknown>,
|
|
746
|
+
opts?: XsqlWatchOpts,
|
|
747
|
+
): QueryWatch<T>
|
|
748
|
+
}
|
|
749
|
+
|
|
750
|
+
export type QueryEvent<T> =
|
|
751
|
+
| { type: 'query/added'; record: T;
|
|
752
|
+
ts?: string; txid?: number; actor?: string; request?: string }
|
|
753
|
+
| { type: 'query/removed'; recordXid: string;
|
|
754
|
+
ts?: string; txid?: number; actor?: string; request?: string }
|
|
755
|
+
| { type: 'query/changed'; record: T;
|
|
756
|
+
before: Partial<T>; after: Partial<T>; changed: (keyof T & string)[];
|
|
757
|
+
ts?: string; txid?: number; actor?: string; request?: string }
|
|
758
|
+
| WatchSentinel
|
|
759
|
+
|
|
760
|
+
export interface QueryWatch<T = Record<string, unknown>> {
|
|
761
|
+
readonly initial: readonly T[] | null
|
|
762
|
+
readonly records: ReadonlyMap<string, T>
|
|
763
|
+
list(): T[]
|
|
764
|
+
ready(): Promise<void>
|
|
765
|
+
refresh(): Promise<void>
|
|
766
|
+
readonly events: AsyncIterable<QueryEvent<T>>
|
|
767
|
+
close(): void
|
|
768
|
+
}
|
|
769
|
+
|
|
770
|
+
export interface AggregateSelection {
|
|
771
|
+
count?: null | true
|
|
772
|
+
avg?: Record<string, null | true>
|
|
773
|
+
sum?: Record<string, null | true>
|
|
774
|
+
min?: Record<string, null | true>
|
|
775
|
+
max?: Record<string, null | true>
|
|
776
|
+
}
|
|
777
|
+
|
|
778
|
+
export interface AggregateResult {
|
|
779
|
+
count?: number
|
|
780
|
+
avg?: Record<string, number>
|
|
781
|
+
sum?: Record<string, number>
|
|
782
|
+
min?: Record<string, unknown>
|
|
783
|
+
max?: Record<string, unknown>
|
|
784
|
+
}
|
|
785
|
+
|
|
786
|
+
// ============================================================================
|
|
787
|
+
// Watch types
|
|
788
|
+
// ============================================================================
|
|
789
|
+
|
|
790
|
+
export interface WatchInterest {
|
|
791
|
+
/** Record xids to watch. Receives entity events for these records, and
|
|
792
|
+
* relation events where either side of the link is one of these records. */
|
|
793
|
+
records?: string[]
|
|
794
|
+
/** Relation-type xids to watch. Receives link/unlink events for those types. */
|
|
795
|
+
relations?: string[]
|
|
796
|
+
/** Ops to include; omit for all. */
|
|
797
|
+
ops?: ('insert' | 'update' | 'delete' | 'link' | 'unlink')[]
|
|
798
|
+
}
|
|
799
|
+
|
|
800
|
+
export interface WatchOpts {
|
|
801
|
+
/**
|
|
802
|
+
* 'coalesce' (default) — collapse repeated entity events on the same record
|
|
803
|
+
* into the latest state. 'lossless' — preserve every event; emit a 'paused'
|
|
804
|
+
* sentinel on overflow. 'sliding' — drop oldest on overflow (legacy).
|
|
805
|
+
*/
|
|
806
|
+
backpressure?: 'coalesce' | 'lossless' | 'sliding'
|
|
807
|
+
/** Per-iterator buffer size (default 100). */
|
|
808
|
+
bufferSize?: number
|
|
809
|
+
signal?: AbortSignal
|
|
810
|
+
}
|
|
811
|
+
|
|
812
|
+
/** Common provenance carried on every event. */
|
|
813
|
+
export interface WatchProvenance {
|
|
814
|
+
ts: string
|
|
815
|
+
txid?: number
|
|
816
|
+
actor?: string
|
|
817
|
+
request?: string
|
|
818
|
+
tenant?: string
|
|
819
|
+
scope?: string
|
|
820
|
+
}
|
|
821
|
+
|
|
822
|
+
export interface RecordEvent extends WatchProvenance {
|
|
823
|
+
type: 'record/insert' | 'record/update' | 'record/delete'
|
|
824
|
+
record: string
|
|
825
|
+
/**
|
|
826
|
+
* Keyed by the plug's raw attribute names — **snake_case**, NOT
|
|
827
|
+
* transformed to the client's keyFormat (unlike `/data` responses, which the
|
|
828
|
+
* server cases per `key_format`). So `after['release_year']`, even when the
|
|
829
|
+
* client is configured `keyFormat: 'kebab'` and `/data` returns `release-year`.
|
|
830
|
+
*/
|
|
831
|
+
before?: Record<string, unknown>
|
|
832
|
+
after?: Record<string, unknown>
|
|
833
|
+
/** Attribute-name list; computed by the SDK from before/after. */
|
|
834
|
+
changed?: string[]
|
|
835
|
+
}
|
|
836
|
+
|
|
837
|
+
export interface RelationEvent extends WatchProvenance {
|
|
838
|
+
type: 'relation/link' | 'relation/unlink'
|
|
839
|
+
/** [subscribed-xid, other-xid] — position 0 is always your subscribed endpoint. */
|
|
840
|
+
data: [string, string]
|
|
841
|
+
}
|
|
842
|
+
|
|
843
|
+
export type WatchSentinel =
|
|
844
|
+
| { type: 'connection/resumed'; gap: [string | null, string | null] }
|
|
845
|
+
| { type: 'schema/changed' }
|
|
846
|
+
| { type: 'subscription/rejected'; reason: string; records: string[] }
|
|
847
|
+
| { type: 'paused' }
|
|
848
|
+
|
|
849
|
+
export type WatchEvent = RecordEvent | RelationEvent | WatchSentinel
|
|
850
|
+
|
|
851
|
+
export interface Watch {
|
|
852
|
+
readonly interest: WatchInterest
|
|
853
|
+
readonly closed: boolean
|
|
854
|
+
readonly events: AsyncIterable<WatchEvent>
|
|
855
|
+
|
|
856
|
+
add(xids: string[]): void
|
|
857
|
+
remove(xids: string[]): void
|
|
858
|
+
setInterest(interest: WatchInterest): void
|
|
859
|
+
|
|
860
|
+
/** Suppress the next event carrying this request-id. Auto-expires after ttlMs. */
|
|
861
|
+
muteRequest(requestId: string, opts?: { ttlMs?: number }): void
|
|
862
|
+
|
|
863
|
+
close(): void
|
|
864
|
+
}
|
|
865
|
+
|
|
866
|
+
export interface SchemaWatch {
|
|
867
|
+
readonly events: AsyncIterable<{ type: 'schema/changed' }>
|
|
868
|
+
close(): void
|
|
869
|
+
}
|
|
870
|
+
|
|
871
|
+
export class WatchError extends Error {
|
|
872
|
+
code: string
|
|
873
|
+
details?: unknown
|
|
874
|
+
constructor(message: string, code: string, details?: unknown)
|
|
875
|
+
}
|
|
876
|
+
|
|
877
|
+
// ============================================================================
|
|
878
|
+
// Observe types — record-shaped envelope
|
|
879
|
+
// ============================================================================
|
|
880
|
+
|
|
881
|
+
export interface ObserveOpts {
|
|
882
|
+
signal?: AbortSignal
|
|
883
|
+
/** Stable subscription handle; defaults to a hash of the descriptor. */
|
|
884
|
+
key?: string
|
|
885
|
+
/** Replay /history events missed during disconnect (default: false). */
|
|
886
|
+
backfill?: boolean
|
|
887
|
+
/** Max rows per backfill pass (default: 1000). */
|
|
888
|
+
backfillLimit?: number
|
|
889
|
+
}
|
|
890
|
+
|
|
891
|
+
/**
|
|
892
|
+
* Common provenance fields carried on every delta envelope.
|
|
893
|
+
*/
|
|
894
|
+
export interface DeltaProvenance {
|
|
895
|
+
/** ISO-8601 timestamp captured by the trigger when the underlying change happened. */
|
|
896
|
+
ts: string
|
|
897
|
+
/** Tenant scope xid (install-level boundary). */
|
|
898
|
+
tenant?: string
|
|
899
|
+
/** Scope xid (per-record access scope, if set). */
|
|
900
|
+
scope?: string
|
|
901
|
+
/** Actor xid (the user who triggered the change). */
|
|
902
|
+
actor?: string
|
|
903
|
+
/** Request-id from the actor's request, if propagated through the trigger. */
|
|
904
|
+
request?: string
|
|
905
|
+
/** DB transaction id — same across all envelopes from one multi-row tx. */
|
|
906
|
+
txid?: number
|
|
907
|
+
/** Set to `true` on events replayed from /history during reconnect. */
|
|
908
|
+
fromBackfill?: boolean
|
|
909
|
+
}
|
|
910
|
+
|
|
911
|
+
/**
|
|
912
|
+
* Record-track delta event. `before` is absent on `record/insert`;
|
|
913
|
+
* `after` is absent on `record/delete`. The `before`/`after` maps are keyed by
|
|
914
|
+
* the plug's raw attribute names — **snake_case**, NOT transformed to the
|
|
915
|
+
* client's keyFormat (unlike `/data`, which cases keys per `key_format`). So
|
|
916
|
+
* `after['release_year']` even under `keyFormat: 'kebab'`. Encrypted columns
|
|
917
|
+
* carry their encrypted form; resolve via /data if you need decrypted values.
|
|
918
|
+
*/
|
|
919
|
+
export interface RecordDeltaEvent extends DeltaProvenance {
|
|
920
|
+
type: 'record/insert' | 'record/update' | 'record/delete'
|
|
921
|
+
'record-xid': string
|
|
922
|
+
before?: Record<string, unknown>
|
|
923
|
+
after?: Record<string, unknown>
|
|
924
|
+
}
|
|
925
|
+
|
|
926
|
+
/**
|
|
927
|
+
* Relation-track delta event for a link/unlink between two entities.
|
|
928
|
+
* `data` is a `[subscribed-xid, other-xid]` tuple — server has rotated
|
|
929
|
+
* so position 0 is always the endpoint in your watched record set.
|
|
930
|
+
* When the identity's union covers BOTH endpoints, the server emits
|
|
931
|
+
* two events per link with rotated tuples (same `ts`/`txid`/provenance);
|
|
932
|
+
* dedupe by `txid` + sorted-endpoints if you want one per underlying link.
|
|
933
|
+
*/
|
|
934
|
+
export interface RelationDeltaEvent extends DeltaProvenance {
|
|
935
|
+
type: 'relation/link' | 'relation/unlink'
|
|
936
|
+
data: [string, string]
|
|
937
|
+
}
|
|
938
|
+
|
|
939
|
+
/**
|
|
940
|
+
* Coalesced cache-invalidation poke for an entity track subscription.
|
|
941
|
+
* `entity` is the client's verbatim original string (server echoes back
|
|
942
|
+
* exactly what was subscribed to, before any case/whitespace/separator
|
|
943
|
+
* normalization).
|
|
944
|
+
*/
|
|
945
|
+
export interface EntityTouchedEvent {
|
|
946
|
+
type: 'entity/touched'
|
|
947
|
+
entity: string
|
|
948
|
+
ts: string
|
|
949
|
+
}
|
|
950
|
+
|
|
951
|
+
/**
|
|
952
|
+
* Coalesced cache-invalidation poke for a relation track subscription.
|
|
953
|
+
* `relation` is the client's verbatim original string.
|
|
954
|
+
*/
|
|
955
|
+
export interface RelationTouchedEvent {
|
|
956
|
+
type: 'relation/touched'
|
|
957
|
+
relation: string
|
|
958
|
+
ts: string
|
|
959
|
+
}
|
|
960
|
+
|
|
961
|
+
export type ObserveEvent =
|
|
962
|
+
| RecordDeltaEvent
|
|
963
|
+
| RelationDeltaEvent
|
|
964
|
+
| EntityTouchedEvent
|
|
965
|
+
| RelationTouchedEvent
|
|
966
|
+
|
|
967
|
+
// ============================================================================
|
|
968
|
+
// Operator helpers
|
|
969
|
+
// ============================================================================
|
|
970
|
+
|
|
971
|
+
export function eq(v: unknown): { _eq: unknown }
|
|
972
|
+
export function neq(v: unknown): { _neq: unknown }
|
|
973
|
+
export function gt(v: unknown): { _gt: unknown }
|
|
974
|
+
export function gte(v: unknown): { _ge: unknown }
|
|
975
|
+
export function lt(v: unknown): { _lt: unknown }
|
|
976
|
+
export function lte(v: unknown): { _le: unknown }
|
|
977
|
+
export function in_(...v: unknown[]): { _in: unknown[] }
|
|
978
|
+
export function nin(...v: unknown[]): { _nin: unknown[] }
|
|
979
|
+
export function like(v: string): { _like: string }
|
|
980
|
+
export function ilike(v: string): { _ilike: string }
|
|
981
|
+
export function isNull(): { _is_null: true }
|
|
982
|
+
export function isNotNull(): { _is_null: false }
|
|
983
|
+
|
|
984
|
+
export function and(...clauses: WhereClause[]): { _and: WhereClause[] }
|
|
985
|
+
export function or(...clauses: WhereClause[]): { _or: WhereClause[] }
|
|
986
|
+
export function not(clause: WhereClause): { _not: WhereClause }
|
|
987
|
+
|
|
988
|
+
// ============================================================================
|
|
989
|
+
// Selection helpers
|
|
990
|
+
// ============================================================================
|
|
991
|
+
|
|
992
|
+
/**
|
|
993
|
+
* Build a relation config for use in a selection.
|
|
994
|
+
*
|
|
995
|
+
* Single: { roles: rel({ name: null }, { _limit: 5 }) }
|
|
996
|
+
* Multiple: { roles: [rel({ name: null }, w1, 'active'), rel({ name: null }, w2, 'archived')] }
|
|
997
|
+
*/
|
|
998
|
+
export function rel(selections: Selection, args?: Args, alias?: string): RelationConfig
|
|
999
|
+
|
|
1000
|
+
// ============================================================================
|
|
1001
|
+
// Operation builders
|
|
1002
|
+
// ============================================================================
|
|
1003
|
+
|
|
1004
|
+
export const op: {
|
|
1005
|
+
search(entity: string, args: Args | undefined, selection: Selection): Operation
|
|
1006
|
+
get(entity: string, args: Record<string, unknown>, selection: Selection): Operation
|
|
1007
|
+
sync(entity: string, data: Record<string, unknown>, returning?: boolean): Operation
|
|
1008
|
+
stack(entity: string, data: Record<string, unknown>, returning?: boolean): Operation
|
|
1009
|
+
slice(entity: string, args: Record<string, unknown>, selection?: Selection): Operation
|
|
1010
|
+
delete(entity: string, data: Record<string, unknown>): Operation
|
|
1011
|
+
purge(entity: string, args: Args | undefined, selection?: Selection): Operation
|
|
1012
|
+
searchTree(entity: string, on: string, args: Args | undefined, selection: Selection): Operation
|
|
1013
|
+
getTree(entity: string, root: string, on: string, selection: Selection): Operation
|
|
1014
|
+
sqlTemplate(template: string, params?: Record<string, unknown> | unknown[], opts?: { cached?: boolean }): Operation
|
|
1015
|
+
deployedModel(): Operation
|
|
1016
|
+
runtimeModel(): Operation
|
|
1017
|
+
}
|
|
1018
|
+
|
|
1019
|
+
// ============================================================================
|
|
1020
|
+
// Tree composition
|
|
1021
|
+
// ============================================================================
|
|
1022
|
+
|
|
1023
|
+
export interface ComposeTreeOpts {
|
|
1024
|
+
/** Name of the tree (self-FK) relation, e.g. "father", "parent" */
|
|
1025
|
+
on: string
|
|
1026
|
+
/** Root record id; defaults to the first record */
|
|
1027
|
+
rootId?: string
|
|
1028
|
+
/** Key under which children are nested (default: "_children") */
|
|
1029
|
+
childrenKey?: string
|
|
1030
|
+
}
|
|
1031
|
+
|
|
1032
|
+
export interface ComposeForestOpts {
|
|
1033
|
+
on: string
|
|
1034
|
+
/** Key under which children are nested (default: "_children") */
|
|
1035
|
+
childrenKey?: string
|
|
1036
|
+
}
|
|
1037
|
+
|
|
1038
|
+
export function composeTree<T extends Record<string, unknown>>(
|
|
1039
|
+
records: T[],
|
|
1040
|
+
opts: ComposeTreeOpts,
|
|
1041
|
+
): (T & { [key: string]: unknown }) | null
|
|
1042
|
+
|
|
1043
|
+
export function composeForest<T extends Record<string, unknown>>(
|
|
1044
|
+
records: T[],
|
|
1045
|
+
opts: ComposeForestOpts,
|
|
1046
|
+
): (T & { [key: string]: unknown })[]
|
|
1047
|
+
|
|
1048
|
+
// ============================================================================
|
|
1049
|
+
// Factory
|
|
1050
|
+
// ============================================================================
|
|
1051
|
+
|
|
1052
|
+
export function createClient(config: ClientConfig): SynthigyClient
|
|
1053
|
+
|
|
1054
|
+
// ============================================================================
|
|
1055
|
+
// Single-client layer — connect once, call the bare verbs, no client threading.
|
|
1056
|
+
// One process, one client; multiplex identity per-call with opts.actingAs.
|
|
1057
|
+
// Verb types are derived from SynthigyClient so they never drift.
|
|
1058
|
+
// ============================================================================
|
|
1059
|
+
|
|
1060
|
+
/** Install a module-wide default client (destroys any previous one). */
|
|
1061
|
+
export function connect(config: ClientConfig): SynthigyClient
|
|
1062
|
+
/** Destroy and uninstall the default client. No-op when not connected. */
|
|
1063
|
+
export function disconnect(): void
|
|
1064
|
+
/** The default client installed by `connect`, or null. */
|
|
1065
|
+
export function getClient(): SynthigyClient | null
|
|
1066
|
+
|
|
1067
|
+
/**
|
|
1068
|
+
* A fresh 22-char Base58 xid — client-minted identity for sync/stack, the same
|
|
1069
|
+
* derivation the server uses. Mint one before a write to know a record's id up
|
|
1070
|
+
* front, or to make a retried write idempotent.
|
|
1071
|
+
*/
|
|
1072
|
+
export function newXid(): string
|
|
1073
|
+
|
|
1074
|
+
export const search: SynthigyClient['search']
|
|
1075
|
+
export const get: SynthigyClient['get']
|
|
1076
|
+
export const sync: SynthigyClient['sync']
|
|
1077
|
+
export const stack: SynthigyClient['stack']
|
|
1078
|
+
export const slice: SynthigyClient['slice']
|
|
1079
|
+
export const purge: SynthigyClient['purge']
|
|
1080
|
+
export const sqlTemplate: SynthigyClient['sqlTemplate']
|
|
1081
|
+
export const query: SynthigyClient['query']
|
|
1082
|
+
export const searchTree: SynthigyClient['searchTree']
|
|
1083
|
+
export const getTree: SynthigyClient['getTree']
|
|
1084
|
+
export const exec: SynthigyClient['exec']
|
|
1085
|
+
declare const _delete: SynthigyClient['delete']
|
|
1086
|
+
export { _delete as delete }
|
|
1087
|
+
|
|
1088
|
+
export const schema: SynthigyClient['schema']
|
|
1089
|
+
export const lint: SynthigyClient['lint']
|
|
1090
|
+
export const deployedModel: SynthigyClient['deployedModel']
|
|
1091
|
+
export const runtimeModel: SynthigyClient['runtimeModel']
|
|
1092
|
+
export const token: SynthigyClient['token']
|
|
1093
|
+
export function history(): SynthigyClient['history']
|
|
1094
|
+
|
|
1095
|
+
export const listen: SynthigyClient['listen']
|
|
1096
|
+
export const observe: SynthigyClient['observe']
|
|
1097
|
+
export const watch: SynthigyClient['watch']
|
|
1098
|
+
export const watchSchema: SynthigyClient['watchSchema']
|
|
1099
|
+
export const watchQuery: SynthigyClient['watchQuery']
|
|
1100
|
+
export const watchSqlTemplate: SynthigyClient['watchSqlTemplate']
|
|
1101
|
+
|
|
1102
|
+
export const subscribe: SynthigyClient['subscribe']
|
|
1103
|
+
export const unsubscribe: SynthigyClient['unsubscribe']
|
|
1104
|
+
export const subscribeModel: SynthigyClient['subscribeModel']
|
|
1105
|
+
export const unsubscribeModel: SynthigyClient['unsubscribeModel']
|
|
1106
|
+
export const setSubscriptions: SynthigyClient['setSubscriptions']
|
|
1107
|
+
export const clearSubscriptions: SynthigyClient['clearSubscriptions']
|
|
1108
|
+
export const subscriptions: SynthigyClient['subscriptions']
|