@centrali-io/centrali-sdk 5.5.0 → 6.0.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 +1807 -878
  3. package/dist/index.js +9153 -4076
  4. package/index.ts +61 -7152
  5. package/package.json +10 -3
  6. package/query-types.ts +83 -2
  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.0",
3
+ "version": "6.0.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 = {
@@ -320,12 +321,81 @@ export type SelectClause = {
320
321
  };
321
322
 
322
323
  /**
323
- * Phase 1 reserves the shape but rejects execution with `unsupported_clause`.
324
+ * Recursive direct-relationship expansion. The executor walks each clause
325
+ * against reference-typed properties on the parent structure and substitutes
326
+ * the related row(s) inline under the relation name. Nested `include`
327
+ * children traverse the next hop on the just-fetched rows. Cycle detection
328
+ * and depth limits live in the executor (CEN-1192).
329
+ *
330
+ * `where` and `select` (CEN-1219) operate on the **target** structure's
331
+ * namespace — fields are resolved against the included relation's own
332
+ * properties (e.g. `data.active` on the related `customer`, not the parent's
333
+ * `data.customer.data.active`). The executor narrows the batched fetch with
334
+ * `where` and trims the attached row to `select.fields` post-fetch. Nested
335
+ * `include` children inherit nothing from this clause's `select` — they are
336
+ * always fetched in full and can carry their own `select`.
324
337
  */
325
338
  export type IncludeClause = {
326
339
  relation: string;
340
+ where?: WhereExpression;
341
+ select?: SelectClause;
342
+ include?: IncludeClause[];
327
343
  };
328
344
 
345
+ // ---------------------------------------------------------------------------
346
+ // JoinClause (contract §7.5 — added 6.0, CEN-1058)
347
+ //
348
+ // Multi-join saved queries. Each entry compiles to one Knex `.join` /
349
+ // `.leftJoin` / `.rightJoin` / `.fullOuterJoin` against a per-side
350
+ // tenancy-filtered subquery (`SELECT * FROM records WHERE
351
+ // workspaceSlug=? AND recordSlug=? AND ...`). Equi-join predicates
352
+ // remain in the ON clause; tenancy lives in the FROM subquery so
353
+ // outer-join row preservation works correctly across all four join
354
+ // types — see PR 1b commit `dc4fb4fcb` for the SQL-correctness analysis.
355
+ //
356
+ // Identifier slots (`foreignSlug`, `localField`, `foreignField`,
357
+ // `alias`, `joinType`) are literal-only — `${var}` placeholders are
358
+ // rejected by the validator and skipped by every substitution layer.
359
+ // `joins.length` is capped at JOINS_MAX_LENGTH; `text + joins` and
360
+ // `include + joins` are both rejected with `unsupported_combination`.
361
+ // ---------------------------------------------------------------------------
362
+
363
+ export type JoinType = 'inner' | 'left' | 'right' | 'full';
364
+
365
+ export type JoinClause = {
366
+ /** Resource (collection slug) of the joined records table. */
367
+ foreignSlug: string;
368
+ /**
369
+ * Field on the primary or any prior join's alias. Bare names refer to the
370
+ * primary structure; dotted names (`<alias>.<field>`) reference a prior
371
+ * join's logical alias.
372
+ */
373
+ localField: string;
374
+ /** Field on the joined table. */
375
+ foreignField: string;
376
+ /** SQL join type. Required — no implicit default. */
377
+ joinType: JoinType;
378
+ /**
379
+ * Optional projection on the joined row. Defaults to all readable fields
380
+ * (subject to field-level auth).
381
+ */
382
+ select?: string[];
383
+ /**
384
+ * Logical alias used to namespace the joined row in the response under
385
+ * `_joined[alias]`, and for `localField` references in subsequent joins.
386
+ * Defaults to `foreignSlug`. Required when joining the same `foreignSlug`
387
+ * more than once in the same query.
388
+ */
389
+ alias?: string;
390
+ };
391
+
392
+ /**
393
+ * Maximum number of `joins[]` entries allowed in a single QueryDefinition.
394
+ * Cap is conservative until we have telemetry on real-world chain lengths;
395
+ * raise if customers hit it for legitimate reporting use cases.
396
+ */
397
+ export const JOINS_MAX_LENGTH = 4;
398
+
329
399
  // ---------------------------------------------------------------------------
330
400
  // QueryVariableDefinition (contract §8) — Phase 4 (saved queries)
331
401
  // ---------------------------------------------------------------------------
@@ -378,7 +448,18 @@ export type QueryErrorCode =
378
448
  | 'unsupported_legacy_operator'
379
449
  | 'unreadable_field'
380
450
  | 'invalid_query'
381
- | 'legacy_write_unsupported';
451
+ | 'legacy_write_unsupported'
452
+ // Phase 4 — saved-query typed parameters (contract §8). Authoring and
453
+ // execute-time validation produce these codes; routes thread them through
454
+ // `queryErrorsToHttp` so the response shape matches the rest of the engine.
455
+ | 'unknown_variable'
456
+ | 'variable_type_mismatch'
457
+ | 'missing_required_variable'
458
+ | 'extra_variable'
459
+ // 6.0 — multi-join (CEN-1058).
460
+ | 'joins_length_exceeded'
461
+ | 'unsupported_combination'
462
+ | 'duplicate_join_alias';
382
463
 
383
464
  export type QueryError = {
384
465
  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;