@centrali-io/centrali-sdk 5.5.1 → 6.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.
Files changed (49) hide show
  1. package/README.md +164 -14
  2. package/dist/index.d.ts +1815 -878
  3. package/dist/index.js +9154 -4076
  4. package/index.ts +61 -7152
  5. package/package.json +10 -3
  6. package/query-types.ts +91 -4
  7. package/scripts/smoke-types.ts +145 -5
  8. package/src/client.ts +1507 -0
  9. package/src/internal/auth.ts +35 -0
  10. package/src/internal/deprecation.ts +11 -0
  11. package/src/internal/error.ts +90 -0
  12. package/src/internal/paths.ts +456 -0
  13. package/src/internal/queryGuard.ts +21 -0
  14. package/src/managers/allowedDomains.ts +90 -0
  15. package/src/managers/anomalyInsights.ts +215 -0
  16. package/src/managers/auditLog.ts +105 -0
  17. package/src/managers/collections.ts +197 -0
  18. package/src/managers/files.ts +182 -0
  19. package/src/managers/functionRuns.ts +229 -0
  20. package/src/managers/functions.ts +171 -0
  21. package/src/managers/orchestrationRuns.ts +122 -0
  22. package/src/managers/orchestrations.ts +297 -0
  23. package/src/managers/query.ts +199 -0
  24. package/src/managers/records.ts +186 -0
  25. package/src/managers/smartQueries.ts +374 -0
  26. package/src/managers/structures.ts +205 -0
  27. package/src/managers/triggers.ts +349 -0
  28. package/src/managers/validation.ts +303 -0
  29. package/src/managers/webhookSubscriptions.ts +206 -0
  30. package/src/realtime/manager.ts +292 -0
  31. package/src/types/allowedDomains.ts +29 -0
  32. package/src/types/auth.ts +83 -0
  33. package/src/types/common.ts +57 -0
  34. package/src/types/compute.ts +145 -0
  35. package/src/types/insights.ts +113 -0
  36. package/src/types/orchestrations.ts +460 -0
  37. package/src/types/realtime.ts +403 -0
  38. package/src/types/records.ts +261 -0
  39. package/src/types/search.ts +44 -0
  40. package/src/types/smartQueries.ts +303 -0
  41. package/src/types/structures.ts +203 -0
  42. package/src/types/triggers.ts +122 -0
  43. package/src/types/validation.ts +167 -0
  44. package/src/types/webhooks.ts +114 -0
  45. package/src/urls.ts +33 -0
  46. package/dist/query-types.d.ts +0 -187
  47. package/dist/query-types.js +0 -137
  48. package/dist/scripts/smoke-types.d.ts +0 -12
  49. package/dist/scripts/smoke-types.js +0 -102
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@centrali-io/centrali-sdk",
3
- "version": "5.5.1",
3
+ "version": "6.1.0",
4
4
  "description": "Centrali Node SDK",
5
5
  "main": "dist/index.js",
6
6
  "types": "dist/index.d.ts",
@@ -12,7 +12,8 @@
12
12
  "scripts": {
13
13
  "sync:query-types": "node scripts/sync-query-types.mjs",
14
14
  "prebuild": "npm run sync:query-types",
15
- "build": "tsc --project tsconfig.json",
15
+ "build": "tsup",
16
+ "test": "jest",
16
17
  "prepublishOnly": "npm run build"
17
18
  },
18
19
  "keywords": [],
@@ -24,10 +25,16 @@
24
25
  "qs": "^6.14.2"
25
26
  },
26
27
  "devDependencies": {
28
+ "@centrali/query": "file:../../backend/shared/query",
27
29
  "@types/eventsource": "^1.1.15",
30
+ "@types/jest": "^29.5.12",
28
31
  "@types/q": "^1.5.8",
29
32
  "@types/qs": "^6.14.0",
30
- "typescript": "5.9.3"
33
+ "jest": "^29.7.0",
34
+ "ts-jest": "^29.1.2",
35
+ "tsup": "^8.3.5",
36
+ "typescript": "5.9.3",
37
+ "zod": "^3.24.1"
31
38
  },
32
39
  "publishConfig": {
33
40
  "registry": "https://registry.npmjs.org/",
package/query-types.ts CHANGED
@@ -34,6 +34,7 @@ export type QueryDefinition = {
34
34
  page?: PageClause;
35
35
  select?: SelectClause;
36
36
  include?: IncludeClause[];
37
+ joins?: JoinClause[];
37
38
  };
38
39
 
39
40
  export type SavedQueryDefinition = {
@@ -299,10 +300,16 @@ export type SortClause = {
299
300
  * Two pagination modes. Per contract §6 they are mutually exclusive — `cursor`
300
301
  * is forbidden in offset mode and `offset` is forbidden in cursor mode so a
301
302
  * caller cannot construct an ambiguous request.
303
+ *
304
+ * `withTotal` is opt-in (default false). When omitted, executors skip the
305
+ * accompanying `count(*)` query and return `meta.total` undefined; `hasMore`
306
+ * is still accurate (executors fetch `limit + 1` rows). Set `withTotal: true`
307
+ * when the caller actually renders a total — e.g. page-1 of a numbered
308
+ * paginator. CEN-1259.
302
309
  */
303
310
  export type PageClause =
304
- | { limit: number; offset?: number; cursor?: never }
305
- | { limit: number; cursor?: string; offset?: never };
311
+ | { limit: number; offset?: number; cursor?: never; withTotal?: boolean }
312
+ | { limit: number; cursor?: string; offset?: never; withTotal?: boolean };
306
313
 
307
314
  /**
308
315
  * Records Phase 1 page defaults (contract §6).
@@ -320,12 +327,81 @@ export type SelectClause = {
320
327
  };
321
328
 
322
329
  /**
323
- * Phase 1 reserves the shape but rejects execution with `unsupported_clause`.
330
+ * Recursive direct-relationship expansion. The executor walks each clause
331
+ * against reference-typed properties on the parent structure and substitutes
332
+ * the related row(s) inline under the relation name. Nested `include`
333
+ * children traverse the next hop on the just-fetched rows. Cycle detection
334
+ * and depth limits live in the executor (CEN-1192).
335
+ *
336
+ * `where` and `select` (CEN-1219) operate on the **target** structure's
337
+ * namespace — fields are resolved against the included relation's own
338
+ * properties (e.g. `data.active` on the related `customer`, not the parent's
339
+ * `data.customer.data.active`). The executor narrows the batched fetch with
340
+ * `where` and trims the attached row to `select.fields` post-fetch. Nested
341
+ * `include` children inherit nothing from this clause's `select` — they are
342
+ * always fetched in full and can carry their own `select`.
324
343
  */
325
344
  export type IncludeClause = {
326
345
  relation: string;
346
+ where?: WhereExpression;
347
+ select?: SelectClause;
348
+ include?: IncludeClause[];
349
+ };
350
+
351
+ // ---------------------------------------------------------------------------
352
+ // JoinClause (contract §7.5 — added 6.0, CEN-1058)
353
+ //
354
+ // Multi-join saved queries. Each entry compiles to one Knex `.join` /
355
+ // `.leftJoin` / `.rightJoin` / `.fullOuterJoin` against a per-side
356
+ // tenancy-filtered subquery (`SELECT * FROM records WHERE
357
+ // workspaceSlug=? AND recordSlug=? AND ...`). Equi-join predicates
358
+ // remain in the ON clause; tenancy lives in the FROM subquery so
359
+ // outer-join row preservation works correctly across all four join
360
+ // types — see PR 1b commit `dc4fb4fcb` for the SQL-correctness analysis.
361
+ //
362
+ // Identifier slots (`foreignSlug`, `localField`, `foreignField`,
363
+ // `alias`, `joinType`) are literal-only — `${var}` placeholders are
364
+ // rejected by the validator and skipped by every substitution layer.
365
+ // `joins.length` is capped at JOINS_MAX_LENGTH; `text + joins` and
366
+ // `include + joins` are both rejected with `unsupported_combination`.
367
+ // ---------------------------------------------------------------------------
368
+
369
+ export type JoinType = 'inner' | 'left' | 'right' | 'full';
370
+
371
+ export type JoinClause = {
372
+ /** Resource (collection slug) of the joined records table. */
373
+ foreignSlug: string;
374
+ /**
375
+ * Field on the primary or any prior join's alias. Bare names refer to the
376
+ * primary structure; dotted names (`<alias>.<field>`) reference a prior
377
+ * join's logical alias.
378
+ */
379
+ localField: string;
380
+ /** Field on the joined table. */
381
+ foreignField: string;
382
+ /** SQL join type. Required — no implicit default. */
383
+ joinType: JoinType;
384
+ /**
385
+ * Optional projection on the joined row. Defaults to all readable fields
386
+ * (subject to field-level auth).
387
+ */
388
+ select?: string[];
389
+ /**
390
+ * Logical alias used to namespace the joined row in the response under
391
+ * `_joined[alias]`, and for `localField` references in subsequent joins.
392
+ * Defaults to `foreignSlug`. Required when joining the same `foreignSlug`
393
+ * more than once in the same query.
394
+ */
395
+ alias?: string;
327
396
  };
328
397
 
398
+ /**
399
+ * Maximum number of `joins[]` entries allowed in a single QueryDefinition.
400
+ * Cap is conservative until we have telemetry on real-world chain lengths;
401
+ * raise if customers hit it for legitimate reporting use cases.
402
+ */
403
+ export const JOINS_MAX_LENGTH = 4;
404
+
329
405
  // ---------------------------------------------------------------------------
330
406
  // QueryVariableDefinition (contract §8) — Phase 4 (saved queries)
331
407
  // ---------------------------------------------------------------------------
@@ -378,7 +454,18 @@ export type QueryErrorCode =
378
454
  | 'unsupported_legacy_operator'
379
455
  | 'unreadable_field'
380
456
  | 'invalid_query'
381
- | 'legacy_write_unsupported';
457
+ | 'legacy_write_unsupported'
458
+ // Phase 4 — saved-query typed parameters (contract §8). Authoring and
459
+ // execute-time validation produce these codes; routes thread them through
460
+ // `queryErrorsToHttp` so the response shape matches the rest of the engine.
461
+ | 'unknown_variable'
462
+ | 'variable_type_mismatch'
463
+ | 'missing_required_variable'
464
+ | 'extra_variable'
465
+ // 6.0 — multi-join (CEN-1058).
466
+ | 'joins_length_exceeded'
467
+ | 'unsupported_combination'
468
+ | 'duplicate_join_alias';
382
469
 
383
470
  export type QueryError = {
384
471
  code: QueryErrorCode;
@@ -20,6 +20,9 @@ import {
20
20
  type SelectClause,
21
21
  type FieldCondition,
22
22
  type CanonicalOperator,
23
+ type ExecuteSavedQueryValues,
24
+ type QueryVariableDefinition,
25
+ type SmartQuery,
23
26
  CANONICAL_OPERATORS,
24
27
  RECORDS_PAGE_DEFAULT_LIMIT,
25
28
  RECORDS_PAGE_MAX_LIMIT,
@@ -103,11 +106,73 @@ async function exerciseSdk() {
103
106
  });
104
107
  console.log(r3.data, r3.meta.mode);
105
108
 
106
- // (Saved-query SDK surface `client.savedQueries` is deliberately not
107
- // shipped in 5.5.0. The /saved-queries/* HTTP routes are Phase 4 work
108
- // tracked in CEN-1198; the canonical SDK manager lands alongside them.
109
- // Today's `client.smartQueries` namespace is the supported way to call
110
- // saved queries from the SDK.)
109
+ // ---- Saved-query typed parameters (CEN-1208 / Phase 4 WS4) ----
110
+ //
111
+ // `get()` returns the typed `variables` declarations alongside the
112
+ // query body so callers can construct invocations programmatically.
113
+ const savedQuery: SmartQuery = (await client.savedQueries.get('orders', 'qry-uuid')).data;
114
+ const decls: Record<string, QueryVariableDefinition> | null | undefined = savedQuery.variables;
115
+ console.log(decls);
116
+
117
+ // Untyped execute — accepts ScalarValue | ScalarValue[] | Date | Date[].
118
+ await client.savedQueries.execute('orders', 'qry-uuid', {
119
+ variables: {
120
+ since: new Date('2026-01-01'),
121
+ statuses: ['open', 'paid'],
122
+ limit: 25,
123
+ // array<datetime> binding — matches the shared { array: 'datetime' } variable type.
124
+ window: [new Date('2026-01-01'), new Date('2026-04-01')],
125
+ },
126
+ });
127
+
128
+ // Typed execute — generic enforces the parameter shape at the call site.
129
+ // Plain TS interfaces (no string index signature) are accepted — the
130
+ // SDK does NOT require `extends ExecuteSavedQueryValues`.
131
+ interface MonthlyRevenueVars {
132
+ month: Date;
133
+ region: string;
134
+ }
135
+ await client.savedQueries.executeTyped<MonthlyRevenueVars>('orders', 'monthly-revenue', {
136
+ month: new Date('2026-04-01'),
137
+ region: 'us-east',
138
+ });
139
+
140
+ // Canonical authoring — `query: QueryDefinition` + typed `variables`.
141
+ await client.savedQueries.create('orders', {
142
+ name: 'Active Orders',
143
+ query: {
144
+ resource: 'orders',
145
+ where: { 'data.status': { eq: '${statusFilter}' } },
146
+ page: { limit: 100 },
147
+ },
148
+ variables: { statusFilter: { type: 'string', required: true } },
149
+ });
150
+ await client.savedQueries.update('orders', 'qry-uuid', {
151
+ query: {
152
+ resource: 'orders',
153
+ where: { 'data.amount': { gte: '${minTotal}' } },
154
+ page: { limit: 50 },
155
+ },
156
+ variables: { minTotal: { type: 'number', required: true } },
157
+ });
158
+ await client.savedQueries.test('orders', {
159
+ query: {
160
+ resource: 'orders',
161
+ where: { 'data.amount': { gte: '${minTotal}' } },
162
+ page: { limit: 5 },
163
+ },
164
+ variableDeclarations: { minTotal: { type: 'number', required: true } },
165
+ variables: { minTotal: 100 },
166
+ });
167
+
168
+ // The lines below MUST fail to compile (uncomment to verify):
169
+ // await client.savedQueries.executeTyped<MonthlyRevenueVars>('orders', 'monthly-revenue', {
170
+ // month: 'not-a-date', // wrong type
171
+ // region: 'us-east',
172
+ // });
173
+ // await client.savedQueries.executeTyped<MonthlyRevenueVars>('orders', 'monthly-revenue', {
174
+ // region: 'us-east', // missing required `month`
175
+ // });
111
176
 
112
177
  // Top-level canonical overload
113
178
  const r4: QueryResult<Order> = await client.queryRecords<Order>('orders', {
@@ -123,6 +188,81 @@ async function exerciseSdk() {
123
188
  sort: '-createdAt',
124
189
  });
125
190
  console.log(r5.data);
191
+
192
+ // ---- Function-runs canonical surface (CEN-1216, accessor renamed CEN-1227) ----
193
+ interface FRow { id: string; functionId: string; status: string; startedAt: string }
194
+
195
+ // Canonical accessor: client.functionRuns
196
+ // Caller can omit `resource` — manager forces 'function-runs'
197
+ const r6: QueryResult<FRow> = await client.functionRuns.query<FRow>({
198
+ where: {
199
+ and: [
200
+ { functionId: { eq: 'fn-123' } },
201
+ { status: { eq: 'failure' } },
202
+ ],
203
+ },
204
+ sort: [{ field: 'startedAt', direction: 'desc' }],
205
+ page: { limit: 50 },
206
+ });
207
+ console.log(r6.data, r6.meta);
208
+
209
+ // Authoring dry-run
210
+ const planResp = await client.functionRuns.test({
211
+ where: { status: { eq: 'completed' } },
212
+ page: { limit: 10 },
213
+ });
214
+ console.log(planResp.plan.executor, planResp.plan.mode);
215
+
216
+ // Deprecated alias still type-checks (emits runtime warning)
217
+ const r6b: QueryResult<FRow> = await client.runs.query<FRow>({
218
+ where: { status: { eq: 'failure' } },
219
+ page: { limit: 1 },
220
+ });
221
+ console.log(r6b.data.length);
222
+
223
+ // ---- Files canonical surface (CEN-1218 / Phase 3) ----
224
+ interface FilesRow { id: string; name?: string; contentType?: string; size?: number; createdAt?: string }
225
+
226
+ // Caller can omit `resource` — manager forces 'files'.
227
+ const r7: QueryResult<FilesRow> = await client.files.query<FilesRow>({
228
+ where: {
229
+ and: [
230
+ { fileType: { eq: 'image' } },
231
+ { tags: { hasAny: ['draft', 'review'] } },
232
+ ],
233
+ },
234
+ sort: [{ field: 'createdAt', direction: 'desc' }],
235
+ page: { limit: 25 },
236
+ select: { fields: ['name', 'contentType', 'size', 'createdAt'] },
237
+ });
238
+ console.log(r7.data, r7.meta);
239
+
240
+ // Default row shape (no generic) returns the canonical FileMetadata projection.
241
+ const r7b = await client.files.query({
242
+ where: { folderId: { eq: 'folder-uuid' } },
243
+ });
244
+ console.log(r7b.data.map((f) => f.name));
245
+
246
+ // renderId on the canonical row pairs with getFileRenderUrl /
247
+ // getFileDownloadUrl — so callers who discover files via query can
248
+ // build URLs without a separate uploadFile() round-trip.
249
+ const r7c = await client.files.query({
250
+ where: { fileType: { eq: 'image' } },
251
+ select: { fields: ['renderId', 'name', 'contentType'] },
252
+ });
253
+ for (const f of r7c.data) {
254
+ if (f.renderId) {
255
+ const url = client.getFileRenderUrl(f.renderId, { width: 200 });
256
+ console.log(f.name, url);
257
+ }
258
+ }
259
+
260
+ // Authoring dry-run
261
+ const filesPlan = await client.files.test({
262
+ where: { isPublic: { eq: true } },
263
+ page: { limit: 10 },
264
+ });
265
+ console.log(filesPlan.plan.executor, filesPlan.plan.resource);
126
266
  }
127
267
 
128
268
  void exerciseSdk;