@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.
- package/README.md +164 -14
- package/dist/index.d.ts +1807 -878
- package/dist/index.js +9153 -4076
- package/index.ts +61 -7152
- package/package.json +10 -3
- package/query-types.ts +83 -2
- package/scripts/smoke-types.ts +145 -5
- package/src/client.ts +1507 -0
- package/src/internal/auth.ts +35 -0
- package/src/internal/deprecation.ts +11 -0
- package/src/internal/error.ts +90 -0
- package/src/internal/paths.ts +456 -0
- package/src/internal/queryGuard.ts +21 -0
- package/src/managers/allowedDomains.ts +90 -0
- package/src/managers/anomalyInsights.ts +215 -0
- package/src/managers/auditLog.ts +105 -0
- package/src/managers/collections.ts +197 -0
- package/src/managers/files.ts +182 -0
- package/src/managers/functionRuns.ts +229 -0
- package/src/managers/functions.ts +171 -0
- package/src/managers/orchestrationRuns.ts +122 -0
- package/src/managers/orchestrations.ts +297 -0
- package/src/managers/query.ts +199 -0
- package/src/managers/records.ts +186 -0
- package/src/managers/smartQueries.ts +374 -0
- package/src/managers/structures.ts +205 -0
- package/src/managers/triggers.ts +349 -0
- package/src/managers/validation.ts +303 -0
- package/src/managers/webhookSubscriptions.ts +206 -0
- package/src/realtime/manager.ts +292 -0
- package/src/types/allowedDomains.ts +29 -0
- package/src/types/auth.ts +83 -0
- package/src/types/common.ts +57 -0
- package/src/types/compute.ts +145 -0
- package/src/types/insights.ts +113 -0
- package/src/types/orchestrations.ts +460 -0
- package/src/types/realtime.ts +403 -0
- package/src/types/records.ts +261 -0
- package/src/types/search.ts +44 -0
- package/src/types/smartQueries.ts +303 -0
- package/src/types/structures.ts +203 -0
- package/src/types/triggers.ts +122 -0
- package/src/types/validation.ts +167 -0
- package/src/types/webhooks.ts +114 -0
- package/src/urls.ts +33 -0
- package/dist/query-types.d.ts +0 -187
- package/dist/query-types.js +0 -137
- package/dist/scripts/smoke-types.d.ts +0 -12
- package/dist/scripts/smoke-types.js +0 -102
package/src/client.ts
ADDED
|
@@ -0,0 +1,1507 @@
|
|
|
1
|
+
import axios, { AxiosError, AxiosInstance, AxiosRequestConfig, AxiosRequestHeaders, AxiosResponse, Method } from 'axios';
|
|
2
|
+
import qs from 'qs';
|
|
3
|
+
|
|
4
|
+
import { CentraliError, isCentraliError, toCentraliError } from './internal/error';
|
|
5
|
+
import { emitDeprecationWarning } from './internal/deprecation';
|
|
6
|
+
import { fetchClientToken } from './internal/auth';
|
|
7
|
+
import { getApiUrl } from './urls';
|
|
8
|
+
import { isCanonicalQueryDefinition } from './internal/queryGuard';
|
|
9
|
+
|
|
10
|
+
import {
|
|
11
|
+
getRecordApiPath,
|
|
12
|
+
getFileUploadApiPath,
|
|
13
|
+
getSearchApiPath,
|
|
14
|
+
} from './internal/paths';
|
|
15
|
+
|
|
16
|
+
import type { CentraliSDKOptions, ApiResponse } from './types/common';
|
|
17
|
+
import type { CheckAuthorizationOptions, AuthorizationResult } from './types/auth';
|
|
18
|
+
import type { DeleteRecordOptions, RecordTtlOptions, GetRecordOptions, QueryRecordOptions } from './types/records';
|
|
19
|
+
import type { SearchOptions, SearchResponse } from './types/search';
|
|
20
|
+
import type { QueryDefinition, QueryResult } from '../query-types';
|
|
21
|
+
|
|
22
|
+
import { RealtimeManager } from './realtime/manager';
|
|
23
|
+
import { OrchestrationsManager } from './managers/orchestrations';
|
|
24
|
+
import { TriggersManager } from './managers/triggers';
|
|
25
|
+
import { RecordsManager } from './managers/records';
|
|
26
|
+
import { AuditLogManager } from './managers/auditLog';
|
|
27
|
+
import { SmartQueriesManager } from './managers/smartQueries';
|
|
28
|
+
import { AnomalyInsightsManager } from './managers/anomalyInsights';
|
|
29
|
+
import { ValidationManager } from './managers/validation';
|
|
30
|
+
import { AllowedDomainsManager } from './managers/allowedDomains';
|
|
31
|
+
import { StructuresManager } from './managers/structures';
|
|
32
|
+
import { CollectionsManager } from './managers/collections';
|
|
33
|
+
import { ComputeFunctionsManager } from './managers/functions';
|
|
34
|
+
import { FunctionRunsManager } from './managers/functionRuns';
|
|
35
|
+
import { OrchestrationRunsManager } from './managers/orchestrationRuns';
|
|
36
|
+
import { FilesManager } from './managers/files';
|
|
37
|
+
import { WebhookSubscriptionsManager } from './managers/webhookSubscriptions';
|
|
38
|
+
import { QueryManager } from './managers/query';
|
|
39
|
+
|
|
40
|
+
export class CentraliSDK {
|
|
41
|
+
private axios: AxiosInstance;
|
|
42
|
+
private token: string | null = null;
|
|
43
|
+
private options: CentraliSDKOptions;
|
|
44
|
+
private _realtime: RealtimeManager | null = null;
|
|
45
|
+
private _triggers: TriggersManager | null = null;
|
|
46
|
+
private _records: RecordsManager | null = null;
|
|
47
|
+
private _auditLog: AuditLogManager | null = null;
|
|
48
|
+
private _smartQueries: SmartQueriesManager | null = null;
|
|
49
|
+
private _queryRecordsLegacyWarned: boolean = false;
|
|
50
|
+
private _anomalyInsights: AnomalyInsightsManager | null = null;
|
|
51
|
+
private _validation: ValidationManager | null = null;
|
|
52
|
+
private _orchestrations: OrchestrationsManager | null = null;
|
|
53
|
+
private _allowedDomains: AllowedDomainsManager | null = null;
|
|
54
|
+
private _structures: StructuresManager | null = null;
|
|
55
|
+
private _collections: CollectionsManager | null = null;
|
|
56
|
+
private _functions: ComputeFunctionsManager | null = null;
|
|
57
|
+
private _runs: FunctionRunsManager | null = null;
|
|
58
|
+
private _orchestrationRuns: OrchestrationRunsManager | null = null;
|
|
59
|
+
private _files: FilesManager | null = null;
|
|
60
|
+
private _webhookSubscriptions: WebhookSubscriptionsManager | null = null;
|
|
61
|
+
private _query: QueryManager | null = null;
|
|
62
|
+
private isRefreshingToken: boolean = false;
|
|
63
|
+
private tokenRefreshPromise: Promise<string> | null = null;
|
|
64
|
+
|
|
65
|
+
constructor(options: CentraliSDKOptions) {
|
|
66
|
+
this.options = options;
|
|
67
|
+
this.token = options.token || null;
|
|
68
|
+
|
|
69
|
+
// Validate mutually exclusive auth options
|
|
70
|
+
const authPaths = [
|
|
71
|
+
options.publishableKey ? 'publishableKey' : null,
|
|
72
|
+
options.getToken ? 'getToken' : null,
|
|
73
|
+
(options.clientId || options.clientSecret) ? 'clientId/clientSecret' : null,
|
|
74
|
+
].filter(Boolean);
|
|
75
|
+
|
|
76
|
+
if (options.publishableKey && authPaths.length > 1) {
|
|
77
|
+
throw new Error(`Cannot use publishableKey with ${authPaths.filter(p => p !== 'publishableKey').join(', ')}. Use one auth method.`);
|
|
78
|
+
}
|
|
79
|
+
if (options.getToken && (options.clientId || options.clientSecret)) {
|
|
80
|
+
throw new Error('Cannot use getToken with clientId/clientSecret. Use one auth method.');
|
|
81
|
+
}
|
|
82
|
+
|
|
83
|
+
const apiUrl = getApiUrl(options.baseUrl);
|
|
84
|
+
this.axios = axios.create({
|
|
85
|
+
baseURL: apiUrl,
|
|
86
|
+
paramsSerializer: (params: Record<string, any>): string =>
|
|
87
|
+
qs.stringify(params, { arrayFormat: "repeat" }),
|
|
88
|
+
proxy: false,
|
|
89
|
+
...options.axiosConfig,
|
|
90
|
+
});
|
|
91
|
+
|
|
92
|
+
// Attach async interceptor to fetch token on first request if needed
|
|
93
|
+
this.axios.interceptors.request.use(
|
|
94
|
+
async (config) => {
|
|
95
|
+
// Auth path 1: Publishable key — send as x-api-key header, no token logic
|
|
96
|
+
if (this.options.publishableKey) {
|
|
97
|
+
config.headers['x-api-key'] = this.options.publishableKey;
|
|
98
|
+
return config;
|
|
99
|
+
}
|
|
100
|
+
|
|
101
|
+
// Auth path 2: Dynamic token callback (getToken) — call on each request
|
|
102
|
+
if (this.options.getToken) {
|
|
103
|
+
this.token = await this.options.getToken();
|
|
104
|
+
if (this.token) {
|
|
105
|
+
config.headers.Authorization = `Bearer ${this.token}`;
|
|
106
|
+
}
|
|
107
|
+
return config;
|
|
108
|
+
}
|
|
109
|
+
|
|
110
|
+
// Auth path 3: Client credentials — fetch token on first request
|
|
111
|
+
if (!this.token && this.options.clientId && this.options.clientSecret) {
|
|
112
|
+
this.token = await fetchClientToken(
|
|
113
|
+
this.options.clientId,
|
|
114
|
+
this.options.clientSecret,
|
|
115
|
+
this.options.baseUrl
|
|
116
|
+
);
|
|
117
|
+
}
|
|
118
|
+
if (this.token) {
|
|
119
|
+
config.headers.Authorization = `Bearer ${this.token}`;
|
|
120
|
+
}
|
|
121
|
+
return config;
|
|
122
|
+
},
|
|
123
|
+
(error) => Promise.reject(error)
|
|
124
|
+
);
|
|
125
|
+
|
|
126
|
+
// Response interceptor for automatic token refresh on 401/403
|
|
127
|
+
this.axios.interceptors.response.use(
|
|
128
|
+
(response) => response,
|
|
129
|
+
async (error) => {
|
|
130
|
+
const originalRequest = error.config;
|
|
131
|
+
|
|
132
|
+
// Publishable keys: no retry — scope errors are permanent
|
|
133
|
+
if (this.options.publishableKey) {
|
|
134
|
+
return Promise.reject(error);
|
|
135
|
+
}
|
|
136
|
+
|
|
137
|
+
const isAuthError = error.response?.status === 401 || error.response?.status === 403;
|
|
138
|
+
const hasNotRetried = !originalRequest._hasRetried;
|
|
139
|
+
|
|
140
|
+
// getToken path: retry once with a fresh token on 401
|
|
141
|
+
if (isAuthError && this.options.getToken && hasNotRetried) {
|
|
142
|
+
originalRequest._hasRetried = true;
|
|
143
|
+
try {
|
|
144
|
+
this.token = await this.options.getToken();
|
|
145
|
+
originalRequest.headers.Authorization = `Bearer ${this.token}`;
|
|
146
|
+
return this.axios(originalRequest);
|
|
147
|
+
} catch (refreshError) {
|
|
148
|
+
return Promise.reject(error);
|
|
149
|
+
}
|
|
150
|
+
}
|
|
151
|
+
|
|
152
|
+
// Client credentials path: refresh token and retry on 401/403
|
|
153
|
+
const hasClientCredentials = this.options.clientId && this.options.clientSecret;
|
|
154
|
+
|
|
155
|
+
if (isAuthError && hasClientCredentials && hasNotRetried) {
|
|
156
|
+
// Mark request as retried to prevent infinite loops
|
|
157
|
+
originalRequest._hasRetried = true;
|
|
158
|
+
|
|
159
|
+
try {
|
|
160
|
+
// If already refreshing, wait for the existing refresh to complete
|
|
161
|
+
if (this.isRefreshingToken && this.tokenRefreshPromise) {
|
|
162
|
+
await this.tokenRefreshPromise;
|
|
163
|
+
} else {
|
|
164
|
+
// Start a new token refresh
|
|
165
|
+
this.isRefreshingToken = true;
|
|
166
|
+
this.tokenRefreshPromise = fetchClientToken(
|
|
167
|
+
this.options.clientId!,
|
|
168
|
+
this.options.clientSecret!,
|
|
169
|
+
this.options.baseUrl
|
|
170
|
+
);
|
|
171
|
+
this.token = await this.tokenRefreshPromise;
|
|
172
|
+
this.isRefreshingToken = false;
|
|
173
|
+
this.tokenRefreshPromise = null;
|
|
174
|
+
}
|
|
175
|
+
|
|
176
|
+
// Retry the original request with the new token
|
|
177
|
+
originalRequest.headers.Authorization = `Bearer ${this.token}`;
|
|
178
|
+
return this.axios(originalRequest);
|
|
179
|
+
} catch (refreshError) {
|
|
180
|
+
// Token refresh failed, clear state and reject
|
|
181
|
+
this.isRefreshingToken = false;
|
|
182
|
+
this.tokenRefreshPromise = null;
|
|
183
|
+
this.token = null;
|
|
184
|
+
return Promise.reject(toCentraliError(refreshError));
|
|
185
|
+
}
|
|
186
|
+
}
|
|
187
|
+
|
|
188
|
+
return Promise.reject(toCentraliError(error));
|
|
189
|
+
}
|
|
190
|
+
);
|
|
191
|
+
}
|
|
192
|
+
|
|
193
|
+
/**
|
|
194
|
+
* Realtime namespace for subscribing to SSE events.
|
|
195
|
+
*
|
|
196
|
+
* Usage:
|
|
197
|
+
* ```ts
|
|
198
|
+
* const sub = client.realtime.subscribe({
|
|
199
|
+
* structures: ['order'],
|
|
200
|
+
* events: ['record_created', 'record_updated'],
|
|
201
|
+
* onEvent: (event) => console.log(event),
|
|
202
|
+
* });
|
|
203
|
+
* // Later: sub.unsubscribe();
|
|
204
|
+
* ```
|
|
205
|
+
*
|
|
206
|
+
* IMPORTANT: Initial Sync Pattern
|
|
207
|
+
* Realtime delivers only new events after connection. For dashboards and lists:
|
|
208
|
+
* 1. Fetch current records first
|
|
209
|
+
* 2. Subscribe to realtime
|
|
210
|
+
* 3. Apply diffs while UI shows the snapshot
|
|
211
|
+
*/
|
|
212
|
+
public get realtime(): RealtimeManager {
|
|
213
|
+
if (!this._realtime) {
|
|
214
|
+
this._realtime = new RealtimeManager(
|
|
215
|
+
this.options.baseUrl,
|
|
216
|
+
this.options.workspaceId,
|
|
217
|
+
() => this.getTokenOrFetch()
|
|
218
|
+
);
|
|
219
|
+
}
|
|
220
|
+
return this._realtime;
|
|
221
|
+
}
|
|
222
|
+
|
|
223
|
+
/**
|
|
224
|
+
* Get the current token, or fetch one using client credentials if available.
|
|
225
|
+
* This ensures realtime subscriptions work without needing a prior HTTP request.
|
|
226
|
+
*/
|
|
227
|
+
private async getTokenOrFetch(): Promise<string | null> {
|
|
228
|
+
// If token exists, return it
|
|
229
|
+
if (this.token) {
|
|
230
|
+
return this.token;
|
|
231
|
+
}
|
|
232
|
+
// For client-credentials flow, proactively fetch token if not available
|
|
233
|
+
if (this.options.clientId && this.options.clientSecret) {
|
|
234
|
+
this.token = await fetchClientToken(
|
|
235
|
+
this.options.clientId,
|
|
236
|
+
this.options.clientSecret,
|
|
237
|
+
this.options.baseUrl
|
|
238
|
+
);
|
|
239
|
+
return this.token;
|
|
240
|
+
}
|
|
241
|
+
// No token and no credentials
|
|
242
|
+
return null;
|
|
243
|
+
}
|
|
244
|
+
|
|
245
|
+
/**
|
|
246
|
+
* Triggers namespace for invoking and managing function triggers.
|
|
247
|
+
*
|
|
248
|
+
* Usage:
|
|
249
|
+
* ```ts
|
|
250
|
+
* // Invoke an on-demand trigger
|
|
251
|
+
* const job = await client.triggers.invoke('trigger-id');
|
|
252
|
+
*
|
|
253
|
+
* // Invoke with custom payload
|
|
254
|
+
* const job = await client.triggers.invoke('trigger-id', {
|
|
255
|
+
* payload: { orderId: '12345' }
|
|
256
|
+
* });
|
|
257
|
+
*
|
|
258
|
+
* // Get trigger details
|
|
259
|
+
* const trigger = await client.triggers.get('trigger-id');
|
|
260
|
+
*
|
|
261
|
+
* // List all triggers
|
|
262
|
+
* const triggers = await client.triggers.list();
|
|
263
|
+
* ```
|
|
264
|
+
*/
|
|
265
|
+
public get triggers(): TriggersManager {
|
|
266
|
+
if (!this._triggers) {
|
|
267
|
+
this._triggers = new TriggersManager(
|
|
268
|
+
this.options.workspaceId,
|
|
269
|
+
this.request.bind(this)
|
|
270
|
+
);
|
|
271
|
+
}
|
|
272
|
+
return this._triggers;
|
|
273
|
+
}
|
|
274
|
+
|
|
275
|
+
/**
|
|
276
|
+
* Records namespace — canonical query surface for records (CEN-1194).
|
|
277
|
+
*
|
|
278
|
+
* Three methods, one engine, one envelope:
|
|
279
|
+
* - `records.query(resource, definition)` — POST `/records/query` with a
|
|
280
|
+
* full `QueryDefinition` (boolean trees, `select`, `text`, `include`).
|
|
281
|
+
* - `records.list(resource, urlOpts?)` — GET adapter for simple URL-param
|
|
282
|
+
* queries. Bookmarkable; cannot express nested booleans.
|
|
283
|
+
* - `records.search(resource, text, opts?)` — sugar over `query({ text })`.
|
|
284
|
+
*
|
|
285
|
+
* @example
|
|
286
|
+
* ```ts
|
|
287
|
+
* // Boolean tree with sort + projection
|
|
288
|
+
* const orders = await client.records.query<Order>('orders', {
|
|
289
|
+
* resource: 'orders',
|
|
290
|
+
* where: {
|
|
291
|
+
* and: [
|
|
292
|
+
* { 'data.status': { eq: 'paid' } },
|
|
293
|
+
* { 'data.amount': { gte: 100 } }
|
|
294
|
+
* ]
|
|
295
|
+
* },
|
|
296
|
+
* sort: [{ field: 'createdAt', direction: 'desc' }],
|
|
297
|
+
* page: { limit: 50 },
|
|
298
|
+
* select: { fields: ['id', 'data.amount', 'data.customer'] }
|
|
299
|
+
* });
|
|
300
|
+
*
|
|
301
|
+
* // Simple GET
|
|
302
|
+
* const recent = await client.records.list<Order>('orders', {
|
|
303
|
+
* 'data.status': 'paid',
|
|
304
|
+
* sort: '-createdAt',
|
|
305
|
+
* });
|
|
306
|
+
*
|
|
307
|
+
* // Text search
|
|
308
|
+
* const matches = await client.records.search<Order>('orders', 'urgent');
|
|
309
|
+
* ```
|
|
310
|
+
*/
|
|
311
|
+
public get records(): RecordsManager {
|
|
312
|
+
if (!this._records) {
|
|
313
|
+
this._records = new RecordsManager(
|
|
314
|
+
this.options.workspaceId,
|
|
315
|
+
this.request.bind(this)
|
|
316
|
+
);
|
|
317
|
+
}
|
|
318
|
+
return this._records;
|
|
319
|
+
}
|
|
320
|
+
|
|
321
|
+
/**
|
|
322
|
+
* Query namespace — canonical query primitives bundled into the SDK
|
|
323
|
+
* (CEN-1202). Validate, translate-from-legacy, and parse URL-style
|
|
324
|
+
* queries locally without a server roundtrip. Same source as the
|
|
325
|
+
* server-side validator, so behavior matches byte-for-byte.
|
|
326
|
+
*
|
|
327
|
+
* @example
|
|
328
|
+
* ```ts
|
|
329
|
+
* const r = client.query.validate({ resource: 'orders', page: { limit: 50 } });
|
|
330
|
+
* if (!r.ok) console.error(r.errors);
|
|
331
|
+
* ```
|
|
332
|
+
*/
|
|
333
|
+
public get query(): QueryManager {
|
|
334
|
+
if (!this._query) {
|
|
335
|
+
this._query = new QueryManager();
|
|
336
|
+
}
|
|
337
|
+
return this._query;
|
|
338
|
+
}
|
|
339
|
+
|
|
340
|
+
/**
|
|
341
|
+
* Audit Log namespace — canonical query surface for the workspace audit
|
|
342
|
+
* log (CEN-1215, Phase 3 of the query foundation).
|
|
343
|
+
*
|
|
344
|
+
* Routes through `POST /workspace/<ws>/api/v1/audit/query` on the
|
|
345
|
+
* workspace service. Same `QueryDefinition` vocabulary as
|
|
346
|
+
* {@link CentraliSDK.records | records} so callers learn one shape and
|
|
347
|
+
* reuse it.
|
|
348
|
+
*
|
|
349
|
+
* @example
|
|
350
|
+
* ```ts
|
|
351
|
+
* // Per-resource history
|
|
352
|
+
* const history = await client.auditLog.query({
|
|
353
|
+
* resource: 'audit-log',
|
|
354
|
+
* where: {
|
|
355
|
+
* and: [
|
|
356
|
+
* { resourceType: { eq: 'structure' } },
|
|
357
|
+
* { resourceId: { eq: 'r-123' } },
|
|
358
|
+
* ],
|
|
359
|
+
* },
|
|
360
|
+
* sort: [{ field: 'createdAt', direction: 'desc' }],
|
|
361
|
+
* page: { limit: 50 },
|
|
362
|
+
* });
|
|
363
|
+
* ```
|
|
364
|
+
*/
|
|
365
|
+
public get auditLog(): AuditLogManager {
|
|
366
|
+
if (!this._auditLog) {
|
|
367
|
+
this._auditLog = new AuditLogManager(
|
|
368
|
+
this.options.workspaceId,
|
|
369
|
+
this.request.bind(this)
|
|
370
|
+
);
|
|
371
|
+
}
|
|
372
|
+
return this._auditLog;
|
|
373
|
+
}
|
|
374
|
+
|
|
375
|
+
/**
|
|
376
|
+
* Saved Queries namespace — list, execute, create, update, and delete
|
|
377
|
+
* saved (formerly "smart") queries. Routes through the canonical
|
|
378
|
+
* `/saved-queries/*` endpoints (Phase 4 of the query foundation,
|
|
379
|
+
* CEN-1198). The data service dual-mounts the deprecated `/smart-queries`
|
|
380
|
+
* alias for the deprecation window.
|
|
381
|
+
*
|
|
382
|
+
* Usage:
|
|
383
|
+
* ```ts
|
|
384
|
+
* // List all saved queries in workspace
|
|
385
|
+
* const allQueries = await client.savedQueries.listAll();
|
|
386
|
+
*
|
|
387
|
+
* // List saved queries for a structure
|
|
388
|
+
* const queries = await client.savedQueries.list('employee');
|
|
389
|
+
*
|
|
390
|
+
* // Get a saved query by name
|
|
391
|
+
* const query = await client.savedQueries.getByName('employee', 'Active Employees');
|
|
392
|
+
*
|
|
393
|
+
* // Execute a saved query
|
|
394
|
+
* const results = await client.savedQueries.execute('employee', query.data.id);
|
|
395
|
+
* console.log('Found:', results.data.length, 'records');
|
|
396
|
+
* ```
|
|
397
|
+
*/
|
|
398
|
+
public get savedQueries(): SmartQueriesManager {
|
|
399
|
+
if (!this._smartQueries) {
|
|
400
|
+
this._smartQueries = new SmartQueriesManager(
|
|
401
|
+
this.options.workspaceId,
|
|
402
|
+
this.request.bind(this)
|
|
403
|
+
);
|
|
404
|
+
}
|
|
405
|
+
return this._smartQueries;
|
|
406
|
+
}
|
|
407
|
+
|
|
408
|
+
/**
|
|
409
|
+
* @deprecated Use `client.savedQueries` instead. The "smart queries"
|
|
410
|
+
* surface was renamed to "saved queries" in Phase 4 of the query
|
|
411
|
+
* foundation (CEN-1198). This getter is a deprecated alias and will be
|
|
412
|
+
* removed in a future major SDK release.
|
|
413
|
+
*/
|
|
414
|
+
public get smartQueries(): SmartQueriesManager {
|
|
415
|
+
emitDeprecationWarning('client.smartQueries is deprecated. Use client.savedQueries instead.');
|
|
416
|
+
if (!this._smartQueriesAlias) {
|
|
417
|
+
this._smartQueriesAlias = new SmartQueriesManager(
|
|
418
|
+
this.options.workspaceId,
|
|
419
|
+
this.requestWithDeprecationHeader('smartQueries'),
|
|
420
|
+
);
|
|
421
|
+
}
|
|
422
|
+
return this._smartQueriesAlias;
|
|
423
|
+
}
|
|
424
|
+
private _smartQueriesAlias?: SmartQueriesManager;
|
|
425
|
+
|
|
426
|
+
/**
|
|
427
|
+
* Anomaly Insights namespace for querying and managing AI-generated insights.
|
|
428
|
+
*
|
|
429
|
+
* Usage:
|
|
430
|
+
* ```ts
|
|
431
|
+
* // List all active critical insights
|
|
432
|
+
* const insights = await client.anomalyInsights.list({
|
|
433
|
+
* status: 'active',
|
|
434
|
+
* severity: 'critical'
|
|
435
|
+
* });
|
|
436
|
+
*
|
|
437
|
+
* // Get insights for a specific structure
|
|
438
|
+
* const orderInsights = await client.anomalyInsights.listByStructure('orders');
|
|
439
|
+
*
|
|
440
|
+
* // Get insight summary
|
|
441
|
+
* const summary = await client.anomalyInsights.getSummary();
|
|
442
|
+
* console.log('Critical:', summary.data.bySeverity.critical);
|
|
443
|
+
*
|
|
444
|
+
* // Acknowledge an insight
|
|
445
|
+
* await client.anomalyInsights.acknowledge('insight-id');
|
|
446
|
+
*
|
|
447
|
+
* // Dismiss an insight
|
|
448
|
+
* await client.anomalyInsights.dismiss('insight-id');
|
|
449
|
+
* ```
|
|
450
|
+
*/
|
|
451
|
+
public get anomalyInsights(): AnomalyInsightsManager {
|
|
452
|
+
if (!this._anomalyInsights) {
|
|
453
|
+
this._anomalyInsights = new AnomalyInsightsManager(
|
|
454
|
+
this.options.workspaceId,
|
|
455
|
+
this.request.bind(this)
|
|
456
|
+
);
|
|
457
|
+
}
|
|
458
|
+
return this._anomalyInsights;
|
|
459
|
+
}
|
|
460
|
+
|
|
461
|
+
/**
|
|
462
|
+
* Validation namespace for AI-powered data quality validation.
|
|
463
|
+
*
|
|
464
|
+
* Features:
|
|
465
|
+
* - Trigger batch validation scans on structures
|
|
466
|
+
* - List and manage validation suggestions (typos, format issues, duplicates)
|
|
467
|
+
* - Accept or reject suggestions to fix data
|
|
468
|
+
*
|
|
469
|
+
* Usage:
|
|
470
|
+
* ```ts
|
|
471
|
+
* // Trigger a batch scan
|
|
472
|
+
* const batch = await client.validation.triggerScan('orders');
|
|
473
|
+
*
|
|
474
|
+
* // Wait for completion
|
|
475
|
+
* const result = await client.validation.waitForScan(batch.data.batchId);
|
|
476
|
+
*
|
|
477
|
+
* // List pending suggestions
|
|
478
|
+
* const suggestions = await client.validation.listSuggestions({ status: 'pending' });
|
|
479
|
+
*
|
|
480
|
+
* // Accept a suggestion (applies the fix)
|
|
481
|
+
* await client.validation.accept('suggestion-id');
|
|
482
|
+
* ```
|
|
483
|
+
*/
|
|
484
|
+
public get validation(): ValidationManager {
|
|
485
|
+
if (!this._validation) {
|
|
486
|
+
this._validation = new ValidationManager(
|
|
487
|
+
this.options.workspaceId,
|
|
488
|
+
this.request.bind(this)
|
|
489
|
+
);
|
|
490
|
+
}
|
|
491
|
+
return this._validation;
|
|
492
|
+
}
|
|
493
|
+
|
|
494
|
+
/**
|
|
495
|
+
* Orchestrations namespace for managing multi-step workflows.
|
|
496
|
+
*
|
|
497
|
+
* Orchestrations chain compute functions together with conditional logic,
|
|
498
|
+
* delays, and decision branches to automate complex business processes.
|
|
499
|
+
*
|
|
500
|
+
* Usage:
|
|
501
|
+
* ```ts
|
|
502
|
+
* // List all orchestrations
|
|
503
|
+
* const orchestrations = await client.orchestrations.list();
|
|
504
|
+
*
|
|
505
|
+
* // Trigger an on-demand orchestration
|
|
506
|
+
* const run = await client.orchestrations.trigger('orch-id', {
|
|
507
|
+
* input: { orderId: '12345' }
|
|
508
|
+
* });
|
|
509
|
+
*
|
|
510
|
+
* // Get run status
|
|
511
|
+
* const runStatus = await client.orchestrations.getRun('orch-id', run.data.id);
|
|
512
|
+
*
|
|
513
|
+
* // Create a new orchestration
|
|
514
|
+
* const orch = await client.orchestrations.create({
|
|
515
|
+
* slug: 'order-processing',
|
|
516
|
+
* name: 'Order Processing',
|
|
517
|
+
* trigger: { type: 'on-demand' },
|
|
518
|
+
* steps: [
|
|
519
|
+
* { id: 'validate', type: 'compute', functionId: 'func_validate', onSuccess: { nextStepId: 'process' } },
|
|
520
|
+
* { id: 'process', type: 'compute', functionId: 'func_process' }
|
|
521
|
+
* ]
|
|
522
|
+
* });
|
|
523
|
+
* ```
|
|
524
|
+
*/
|
|
525
|
+
public get orchestrations(): OrchestrationsManager {
|
|
526
|
+
if (!this._orchestrations) {
|
|
527
|
+
this._orchestrations = new OrchestrationsManager(
|
|
528
|
+
this.options.workspaceId,
|
|
529
|
+
this.request.bind(this)
|
|
530
|
+
);
|
|
531
|
+
}
|
|
532
|
+
return this._orchestrations;
|
|
533
|
+
}
|
|
534
|
+
|
|
535
|
+
/**
|
|
536
|
+
* Allowed Domains namespace for managing compute function external call domains.
|
|
537
|
+
*
|
|
538
|
+
* Usage:
|
|
539
|
+
* ```ts
|
|
540
|
+
* // List all allowed domains
|
|
541
|
+
* const domains = await client.allowedDomains.list();
|
|
542
|
+
*
|
|
543
|
+
* // Add a new domain
|
|
544
|
+
* const domain = await client.allowedDomains.add({ domain: 'api.stripe.com' });
|
|
545
|
+
*
|
|
546
|
+
* // Remove a domain
|
|
547
|
+
* await client.allowedDomains.remove('domain-id');
|
|
548
|
+
* ```
|
|
549
|
+
*/
|
|
550
|
+
public get allowedDomains(): AllowedDomainsManager {
|
|
551
|
+
if (!this._allowedDomains) {
|
|
552
|
+
this._allowedDomains = new AllowedDomainsManager(
|
|
553
|
+
this.options.workspaceId,
|
|
554
|
+
this.request.bind(this)
|
|
555
|
+
);
|
|
556
|
+
}
|
|
557
|
+
return this._allowedDomains;
|
|
558
|
+
}
|
|
559
|
+
|
|
560
|
+
/**
|
|
561
|
+
* Structures namespace for managing data structures (schemas).
|
|
562
|
+
* Provides CRUD operations and validation for structure definitions.
|
|
563
|
+
*
|
|
564
|
+
* Usage:
|
|
565
|
+
* ```ts
|
|
566
|
+
* // List all structures
|
|
567
|
+
* const structures = await client.structures.list();
|
|
568
|
+
*
|
|
569
|
+
* // Create a structure
|
|
570
|
+
* const structure = await client.structures.create({
|
|
571
|
+
* name: 'Orders',
|
|
572
|
+
* slug: 'orders',
|
|
573
|
+
* properties: [{ name: 'title', type: 'string', required: true }]
|
|
574
|
+
* });
|
|
575
|
+
*
|
|
576
|
+
* // Validate before creating
|
|
577
|
+
* const result = await client.structures.validate({ slug: 'orders' });
|
|
578
|
+
* ```
|
|
579
|
+
*/
|
|
580
|
+
public get structures(): StructuresManager {
|
|
581
|
+
emitDeprecationWarning('client.structures is deprecated. Use client.collections instead.');
|
|
582
|
+
if (!this._structures) {
|
|
583
|
+
this._structures = new StructuresManager(
|
|
584
|
+
this.options.workspaceId,
|
|
585
|
+
this.requestWithDeprecationHeader('structures'),
|
|
586
|
+
);
|
|
587
|
+
}
|
|
588
|
+
return this._structures;
|
|
589
|
+
}
|
|
590
|
+
|
|
591
|
+
/**
|
|
592
|
+
* Collections namespace for managing data collections (schemas).
|
|
593
|
+
* Provides CRUD operations and validation for collection definitions.
|
|
594
|
+
* This is the preferred API — use `client.collections` instead of `client.structures`.
|
|
595
|
+
*
|
|
596
|
+
* Usage:
|
|
597
|
+
* ```ts
|
|
598
|
+
* // List all collections
|
|
599
|
+
* const collections = await client.collections.list();
|
|
600
|
+
*
|
|
601
|
+
* // Create a collection
|
|
602
|
+
* const collection = await client.collections.create({
|
|
603
|
+
* name: 'Orders',
|
|
604
|
+
* slug: 'orders',
|
|
605
|
+
* properties: [{ name: 'title', type: 'string', required: true }]
|
|
606
|
+
* });
|
|
607
|
+
*
|
|
608
|
+
* // Validate before creating
|
|
609
|
+
* const result = await client.collections.validate({ slug: 'orders' });
|
|
610
|
+
* ```
|
|
611
|
+
*/
|
|
612
|
+
public get collections(): CollectionsManager {
|
|
613
|
+
if (!this._collections) {
|
|
614
|
+
this._collections = new CollectionsManager(
|
|
615
|
+
this.options.workspaceId,
|
|
616
|
+
this.request.bind(this)
|
|
617
|
+
);
|
|
618
|
+
}
|
|
619
|
+
return this._collections;
|
|
620
|
+
}
|
|
621
|
+
|
|
622
|
+
/**
|
|
623
|
+
* Functions namespace for managing compute functions.
|
|
624
|
+
* Provides CRUD operations and test execution for compute function code.
|
|
625
|
+
*
|
|
626
|
+
* Usage:
|
|
627
|
+
* ```ts
|
|
628
|
+
* // List all functions
|
|
629
|
+
* const fns = await client.functions.list();
|
|
630
|
+
*
|
|
631
|
+
* // Create a function
|
|
632
|
+
* const fn = await client.functions.create({
|
|
633
|
+
* name: 'Process Order',
|
|
634
|
+
* code: 'async function run() { return { ok: true }; }'
|
|
635
|
+
* });
|
|
636
|
+
*
|
|
637
|
+
* // Test execute without saving
|
|
638
|
+
* const result = await client.functions.testExecute({
|
|
639
|
+
* code: 'async function run() { return executionParams; }',
|
|
640
|
+
* params: { test: true }
|
|
641
|
+
* });
|
|
642
|
+
* ```
|
|
643
|
+
*/
|
|
644
|
+
public get functions(): ComputeFunctionsManager {
|
|
645
|
+
if (!this._functions) {
|
|
646
|
+
this._functions = new ComputeFunctionsManager(
|
|
647
|
+
this.options.workspaceId,
|
|
648
|
+
this.request.bind(this)
|
|
649
|
+
);
|
|
650
|
+
}
|
|
651
|
+
return this._functions;
|
|
652
|
+
}
|
|
653
|
+
|
|
654
|
+
/**
|
|
655
|
+
* Function Runs namespace — query function execution history. Canonical
|
|
656
|
+
* accessor (CEN-1227); pairs with `client.orchestrationRuns` for
|
|
657
|
+
* orchestration runs (CEN-1217).
|
|
658
|
+
*
|
|
659
|
+
* Usage:
|
|
660
|
+
* ```ts
|
|
661
|
+
* // Get a specific run
|
|
662
|
+
* const run = await client.functionRuns.get('run-id');
|
|
663
|
+
*
|
|
664
|
+
* // List runs for a trigger
|
|
665
|
+
* const runs = await client.functionRuns.listByTrigger('trigger-id');
|
|
666
|
+
*
|
|
667
|
+
* // List failed runs for a compute definition
|
|
668
|
+
* const failed = await client.functionRuns.listByFunction('fn-id', {
|
|
669
|
+
* status: 'failure'
|
|
670
|
+
* });
|
|
671
|
+
*
|
|
672
|
+
* // Canonical query surface
|
|
673
|
+
* const failures = await client.functionRuns.query({
|
|
674
|
+
* where: { and: [{ status: { eq: 'failure' } }] },
|
|
675
|
+
* limit: 50
|
|
676
|
+
* });
|
|
677
|
+
* ```
|
|
678
|
+
*/
|
|
679
|
+
public get functionRuns(): FunctionRunsManager {
|
|
680
|
+
if (!this._runs) {
|
|
681
|
+
this._runs = new FunctionRunsManager(
|
|
682
|
+
this.options.workspaceId,
|
|
683
|
+
this.request.bind(this)
|
|
684
|
+
);
|
|
685
|
+
}
|
|
686
|
+
return this._runs;
|
|
687
|
+
}
|
|
688
|
+
|
|
689
|
+
/**
|
|
690
|
+
* Orchestration Runs namespace — canonical query surface for orchestration
|
|
691
|
+
* run history (CEN-1217 / Phase 3).
|
|
692
|
+
*
|
|
693
|
+
* Per-orchestration trigger / list / get-by-id helpers stay on
|
|
694
|
+
* `client.orchestrations.{trigger, listRuns, getRun, getRunSteps}`. Use
|
|
695
|
+
* this namespace when you want the canonical `QueryDefinition` shape
|
|
696
|
+
* (nested boolean trees, `select`, paging) or workspace-wide queries.
|
|
697
|
+
*
|
|
698
|
+
* Usage:
|
|
699
|
+
* ```ts
|
|
700
|
+
* // Recent failed runs across the workspace
|
|
701
|
+
* const failures = await client.orchestrationRuns.query({
|
|
702
|
+
* resource: 'orchestration-runs',
|
|
703
|
+
* where: { and: [{ status: { eq: 'failed' } }] },
|
|
704
|
+
* sort: [{ field: 'startedAt', direction: 'desc' }],
|
|
705
|
+
* page: { limit: 50 },
|
|
706
|
+
* });
|
|
707
|
+
*
|
|
708
|
+
* // Authoring dry-run from a query builder UI
|
|
709
|
+
* const plan = await client.orchestrationRuns.test({
|
|
710
|
+
* resource: 'orchestration-runs',
|
|
711
|
+
* where: { and: [{ orchestrationId: { eq: 'orch-123' } }] },
|
|
712
|
+
* });
|
|
713
|
+
* ```
|
|
714
|
+
*/
|
|
715
|
+
public get orchestrationRuns(): OrchestrationRunsManager {
|
|
716
|
+
if (!this._orchestrationRuns) {
|
|
717
|
+
this._orchestrationRuns = new OrchestrationRunsManager(
|
|
718
|
+
this.options.workspaceId,
|
|
719
|
+
this.request.bind(this)
|
|
720
|
+
);
|
|
721
|
+
}
|
|
722
|
+
return this._orchestrationRuns;
|
|
723
|
+
}
|
|
724
|
+
|
|
725
|
+
/**
|
|
726
|
+
* Files namespace — canonical query surface for files (CEN-1218 /
|
|
727
|
+
* Phase 3). Pairs with the existing top-level helpers
|
|
728
|
+
* (`client.uploadFile`, `client.getFileRenderUrl`,
|
|
729
|
+
* `client.createFolder`, …) — those stay where they are. Use this
|
|
730
|
+
* namespace when you want the canonical `QueryDefinition` shape
|
|
731
|
+
* (nested boolean trees, projection, paging, range/array operators
|
|
732
|
+
* on `tags`).
|
|
733
|
+
*
|
|
734
|
+
* Usage:
|
|
735
|
+
* ```ts
|
|
736
|
+
* // Recent images
|
|
737
|
+
* const images = await client.files.query({
|
|
738
|
+
* resource: 'files',
|
|
739
|
+
* where: { fileType: { eq: 'image' } },
|
|
740
|
+
* sort: [{ field: 'createdAt', direction: 'desc' }],
|
|
741
|
+
* page: { limit: 50 },
|
|
742
|
+
* });
|
|
743
|
+
*
|
|
744
|
+
* // Authoring dry-run
|
|
745
|
+
* const plan = await client.files.test({
|
|
746
|
+
* resource: 'files',
|
|
747
|
+
* where: { folderId: { eq: 'folder-uuid' } },
|
|
748
|
+
* });
|
|
749
|
+
* ```
|
|
750
|
+
*/
|
|
751
|
+
public get files(): FilesManager {
|
|
752
|
+
if (!this._files) {
|
|
753
|
+
this._files = new FilesManager(
|
|
754
|
+
this.options.workspaceId,
|
|
755
|
+
this.request.bind(this)
|
|
756
|
+
);
|
|
757
|
+
}
|
|
758
|
+
return this._files;
|
|
759
|
+
}
|
|
760
|
+
|
|
761
|
+
/**
|
|
762
|
+
* @deprecated Use `client.functionRuns` instead. Renamed in CEN-1227 ahead
|
|
763
|
+
* of `client.orchestrationRuns` (CEN-1217), at which point `client.runs`
|
|
764
|
+
* would be ambiguous (function runs vs. orchestration runs). This getter
|
|
765
|
+
* is a deprecated alias and will be removed in a future major SDK release.
|
|
766
|
+
*/
|
|
767
|
+
public get runs(): FunctionRunsManager {
|
|
768
|
+
emitDeprecationWarning('client.runs is deprecated. Use client.functionRuns instead.');
|
|
769
|
+
if (!this._runsAlias) {
|
|
770
|
+
const Mgr = FunctionRunsManager;
|
|
771
|
+
this._runsAlias = new Mgr(
|
|
772
|
+
this.options.workspaceId,
|
|
773
|
+
this.requestWithDeprecationHeader('runs'),
|
|
774
|
+
);
|
|
775
|
+
}
|
|
776
|
+
return this._runsAlias;
|
|
777
|
+
}
|
|
778
|
+
private _runsAlias?: FunctionRunsManager;
|
|
779
|
+
|
|
780
|
+
/**
|
|
781
|
+
* Webhook subscriptions namespace for outbound webhooks — create, update,
|
|
782
|
+
* rotate signing secrets, inspect delivery history, and replay/cancel
|
|
783
|
+
* individual deliveries.
|
|
784
|
+
*
|
|
785
|
+
* Usage:
|
|
786
|
+
* ```ts
|
|
787
|
+
* // Create a subscription (capture secret immediately — not returned on reads)
|
|
788
|
+
* const sub = await centrali.webhookSubscriptions.create({
|
|
789
|
+
* name: 'Order notifications',
|
|
790
|
+
* url: 'https://api.example.com/hooks/centrali',
|
|
791
|
+
* events: [RecordEvents.CREATED, RecordEvents.UPDATED],
|
|
792
|
+
* recordSlugs: ['orders'],
|
|
793
|
+
* });
|
|
794
|
+
*
|
|
795
|
+
* // Rotate the signing secret (immediate cutover)
|
|
796
|
+
* const rotated = await centrali.webhookSubscriptions.rotateSecret(sub.data.id);
|
|
797
|
+
*
|
|
798
|
+
* // Inspect deliveries
|
|
799
|
+
* const deliveries = await centrali.webhookSubscriptions.deliveries.list(sub.data.id, {
|
|
800
|
+
* status: 'failed'
|
|
801
|
+
* });
|
|
802
|
+
*
|
|
803
|
+
* // Replay a failed delivery
|
|
804
|
+
* await centrali.webhookSubscriptions.deliveries.retry(deliveries.data[0].id);
|
|
805
|
+
* ```
|
|
806
|
+
*/
|
|
807
|
+
public get webhookSubscriptions(): WebhookSubscriptionsManager {
|
|
808
|
+
if (!this._webhookSubscriptions) {
|
|
809
|
+
this._webhookSubscriptions = new WebhookSubscriptionsManager(
|
|
810
|
+
this.options.workspaceId,
|
|
811
|
+
this.request.bind(this)
|
|
812
|
+
);
|
|
813
|
+
}
|
|
814
|
+
return this._webhookSubscriptions;
|
|
815
|
+
}
|
|
816
|
+
|
|
817
|
+
/**
|
|
818
|
+
* Manually set or update the bearer token for subsequent requests.
|
|
819
|
+
*/
|
|
820
|
+
public setToken(token: string): void {
|
|
821
|
+
this.token = token;
|
|
822
|
+
}
|
|
823
|
+
|
|
824
|
+
/**
|
|
825
|
+
* Fetch Service Account token using Client Credentials flow.
|
|
826
|
+
*/
|
|
827
|
+
public async fetchServiceAccountToken(): Promise<string> {
|
|
828
|
+
if (!this.options.clientId || !this.options.clientSecret) {
|
|
829
|
+
throw new Error('Client ID and Client Secret are required to fetch Service Account token.');
|
|
830
|
+
}
|
|
831
|
+
const token = await fetchClientToken(
|
|
832
|
+
this.options.clientId,
|
|
833
|
+
this.options.clientSecret,
|
|
834
|
+
this.options.baseUrl
|
|
835
|
+
);
|
|
836
|
+
return token;
|
|
837
|
+
}
|
|
838
|
+
|
|
839
|
+
/**
|
|
840
|
+
* Build a manager-shaped request callback that tags every outbound call
|
|
841
|
+
* with `X-Centrali-Deprecated-Method`. Used by `@deprecated` SDK aliases
|
|
842
|
+
* (`client.smartQueries`, `client.structures`, `client.runs`) so the
|
|
843
|
+
* existing per-route deprecation counter on the data/workspace services
|
|
844
|
+
* (CEN-1196) can attribute hits back to which SDK surface the caller used,
|
|
845
|
+
* even when the underlying HTTP route is canonical and doesn't fire its
|
|
846
|
+
* own deprecation telemetry.
|
|
847
|
+
*/
|
|
848
|
+
private requestWithDeprecationHeader(methodName: string) {
|
|
849
|
+
return <T>(
|
|
850
|
+
method: Method,
|
|
851
|
+
path: string,
|
|
852
|
+
data?: any,
|
|
853
|
+
queryParams?: Record<string, any>,
|
|
854
|
+
): Promise<ApiResponse<T>> => {
|
|
855
|
+
return this.request<T>(method, path, data, queryParams, {
|
|
856
|
+
headers: { 'X-Centrali-Deprecated-Method': methodName },
|
|
857
|
+
});
|
|
858
|
+
};
|
|
859
|
+
}
|
|
860
|
+
|
|
861
|
+
/**
|
|
862
|
+
* Perform an HTTP request.
|
|
863
|
+
*/
|
|
864
|
+
private async request<T>(
|
|
865
|
+
method: Method,
|
|
866
|
+
path: string,
|
|
867
|
+
data?: any,
|
|
868
|
+
queryParams?: Record<string, any>,
|
|
869
|
+
config?: AxiosRequestConfig
|
|
870
|
+
): Promise<ApiResponse<T>> {
|
|
871
|
+
const resp = await this.axios.request<ApiResponse<T>>({
|
|
872
|
+
method,
|
|
873
|
+
url: path,
|
|
874
|
+
data,
|
|
875
|
+
params: queryParams,
|
|
876
|
+
...config,
|
|
877
|
+
});
|
|
878
|
+
|
|
879
|
+
// 🔧 Normalize responses to always return { data: T, ... } format
|
|
880
|
+
// Handle primitives (strings, numbers, etc.)
|
|
881
|
+
if (typeof resp.data !== 'object' || resp.data === null) {
|
|
882
|
+
return { data: resp.data as T };
|
|
883
|
+
}
|
|
884
|
+
// Handle arrays (which are objects but should be wrapped in data property)
|
|
885
|
+
if (Array.isArray(resp.data)) {
|
|
886
|
+
return { data: resp.data as T };
|
|
887
|
+
}
|
|
888
|
+
// Handle { result } responses (smart queries and some other endpoints)
|
|
889
|
+
if ('result' in resp.data && !('data' in resp.data)) {
|
|
890
|
+
return { data: (resp.data as any).result as T };
|
|
891
|
+
}
|
|
892
|
+
// Handle objects that don't have a data property (legacy endpoints)
|
|
893
|
+
if (!('data' in resp.data)) {
|
|
894
|
+
return { data: resp.data as T };
|
|
895
|
+
}
|
|
896
|
+
return resp.data;
|
|
897
|
+
}
|
|
898
|
+
|
|
899
|
+
// ------------------ Data API Methods ------------------
|
|
900
|
+
|
|
901
|
+
/** Create a new record in a given recordSlug. */
|
|
902
|
+
public createRecord<T = any>(
|
|
903
|
+
recordSlug: string,
|
|
904
|
+
record: Record<string, any>,
|
|
905
|
+
options?: RecordTtlOptions
|
|
906
|
+
): Promise<ApiResponse<T>> {
|
|
907
|
+
const path = getRecordApiPath(this.options.workspaceId, recordSlug);
|
|
908
|
+
const queryParams: Record<string, string> = {};
|
|
909
|
+
if (options?.ttlSeconds) queryParams.ttlSeconds = String(options.ttlSeconds);
|
|
910
|
+
if (options?.expiresAt) queryParams.expiresAt = options.expiresAt;
|
|
911
|
+
return this.request('POST', path, { ...record }, Object.keys(queryParams).length > 0 ? queryParams : undefined);
|
|
912
|
+
}
|
|
913
|
+
|
|
914
|
+
/** Get the token used for authentication. */
|
|
915
|
+
public getToken(): string | null {
|
|
916
|
+
return this.token;
|
|
917
|
+
}
|
|
918
|
+
|
|
919
|
+
|
|
920
|
+
/**
|
|
921
|
+
* Retrieve a record by ID.
|
|
922
|
+
*
|
|
923
|
+
* @param recordSlug - The structure's record slug
|
|
924
|
+
* @param id - The record ID
|
|
925
|
+
* @param options - Optional parameters including expand for reference fields
|
|
926
|
+
*
|
|
927
|
+
* @example
|
|
928
|
+
* // Basic fetch
|
|
929
|
+
* const order = await centrali.getRecord('Order', 'order-123');
|
|
930
|
+
*
|
|
931
|
+
* // With expanded references
|
|
932
|
+
* const order = await centrali.getRecord('Order', 'order-123', {
|
|
933
|
+
* expand: 'customer,items'
|
|
934
|
+
* });
|
|
935
|
+
* // Access expanded data: order.data.data._expanded.customer
|
|
936
|
+
*/
|
|
937
|
+
public getRecord<T = any>(
|
|
938
|
+
recordSlug: string,
|
|
939
|
+
id: string,
|
|
940
|
+
options?: GetRecordOptions
|
|
941
|
+
): Promise<ApiResponse<T>> {
|
|
942
|
+
const path = getRecordApiPath(this.options.workspaceId, recordSlug, id);
|
|
943
|
+
return this.request('GET', path, null, options);
|
|
944
|
+
}
|
|
945
|
+
|
|
946
|
+
/**
|
|
947
|
+
* Query records with filters, pagination, sorting, and reference expansion.
|
|
948
|
+
*
|
|
949
|
+
* IMPORTANT: Filters are passed at the TOP LEVEL, not nested under 'filter'.
|
|
950
|
+
* Use 'data.' prefix for custom fields and bracket notation for operators.
|
|
951
|
+
*
|
|
952
|
+
* @param recordSlug - The structure's record slug
|
|
953
|
+
* @param queryParams - Query parameters (filters at top level, plus sort, pagination, expand)
|
|
954
|
+
*
|
|
955
|
+
* @example
|
|
956
|
+
* // Simple equality filter
|
|
957
|
+
* const activeProducts = await centrali.queryRecords('Product', {
|
|
958
|
+
* 'data.status': 'active',
|
|
959
|
+
* sort: '-createdAt',
|
|
960
|
+
* page: 1,
|
|
961
|
+
* pageSize: 10
|
|
962
|
+
* });
|
|
963
|
+
*
|
|
964
|
+
* // Filter with operators (bracket notation)
|
|
965
|
+
* const products = await centrali.queryRecords('Product', {
|
|
966
|
+
* 'data.inStock': true,
|
|
967
|
+
* 'data.price[lte]': 100,
|
|
968
|
+
* sort: '-createdAt',
|
|
969
|
+
* pageSize: 10
|
|
970
|
+
* });
|
|
971
|
+
*
|
|
972
|
+
* // Multiple values with 'in' operator (comma-separated string)
|
|
973
|
+
* const orders = await centrali.queryRecords('Order', {
|
|
974
|
+
* 'data.status[in]': 'pending,processing',
|
|
975
|
+
* expand: 'customer,items'
|
|
976
|
+
* });
|
|
977
|
+
* // Access expanded data: orders.data[0].data._expanded.customer
|
|
978
|
+
*
|
|
979
|
+
* // Range filters
|
|
980
|
+
* const customers = await centrali.queryRecords('Customer', {
|
|
981
|
+
* 'data.age[gte]': 18,
|
|
982
|
+
* 'data.age[lte]': 65,
|
|
983
|
+
* 'data.verified': true
|
|
984
|
+
* });
|
|
985
|
+
*
|
|
986
|
+
* // Filter with 'ne' (not equal)
|
|
987
|
+
* const availableItems = await centrali.queryRecords('Product', {
|
|
988
|
+
* 'data.status[ne]': 'discontinued',
|
|
989
|
+
* pageSize: 100
|
|
990
|
+
* });
|
|
991
|
+
*/
|
|
992
|
+
/**
|
|
993
|
+
* Canonical query (CEN-1194). When called with a `QueryDefinition` body,
|
|
994
|
+
* routes to `POST /records/query` and returns canonical `QueryResult<T>`.
|
|
995
|
+
*
|
|
996
|
+
* @example
|
|
997
|
+
* const result = await centrali.queryRecords<Order>('orders', {
|
|
998
|
+
* resource: 'orders',
|
|
999
|
+
* where: { 'data.status': { eq: 'paid' } },
|
|
1000
|
+
* page: { limit: 50 }
|
|
1001
|
+
* });
|
|
1002
|
+
* console.log(result.data, result.meta.hasMore);
|
|
1003
|
+
*/
|
|
1004
|
+
public queryRecords<T = any>(
|
|
1005
|
+
resource: string,
|
|
1006
|
+
definition: QueryDefinition
|
|
1007
|
+
): Promise<QueryResult<T>>;
|
|
1008
|
+
/**
|
|
1009
|
+
* Legacy GET-adapter form. Pass `QueryRecordOptions` (URL-param style with
|
|
1010
|
+
* `data.field[op]` keys, `sort: '-createdAt'`, `pageSize`, etc.) and the
|
|
1011
|
+
* call routes to `GET /records/slug/:rs`.
|
|
1012
|
+
*
|
|
1013
|
+
* @deprecated Since 5.5.0. Prefer the canonical overload above (pass a
|
|
1014
|
+
* `QueryDefinition`) or {@link CentraliSDK.records | client.records.list()}
|
|
1015
|
+
* for the GET adapter explicitly. The legacy form keeps working — server-side
|
|
1016
|
+
* it already routes through the canonical engine (CEN-1181 WS3) — but the
|
|
1017
|
+
* client-side type story diverges from the canonical surface. Emits a
|
|
1018
|
+
* one-shot `console.warn` per client.
|
|
1019
|
+
*/
|
|
1020
|
+
public queryRecords<T = any>(
|
|
1021
|
+
recordSlug: string,
|
|
1022
|
+
queryParams?: QueryRecordOptions
|
|
1023
|
+
): Promise<ApiResponse<T>>;
|
|
1024
|
+
public queryRecords<T = any>(
|
|
1025
|
+
resourceOrSlug: string,
|
|
1026
|
+
arg2?: QueryDefinition | QueryRecordOptions
|
|
1027
|
+
): Promise<QueryResult<T>> | Promise<ApiResponse<T>> {
|
|
1028
|
+
if (isCanonicalQueryDefinition(arg2)) {
|
|
1029
|
+
return this.records.query<T>(resourceOrSlug, arg2);
|
|
1030
|
+
}
|
|
1031
|
+
if (!this._queryRecordsLegacyWarned) {
|
|
1032
|
+
this._queryRecordsLegacyWarned = true;
|
|
1033
|
+
// eslint-disable-next-line no-console
|
|
1034
|
+
console.warn(
|
|
1035
|
+
'[centrali-sdk] `client.queryRecords(slug, urlOpts)` (legacy URL-param form) is deprecated — pass a canonical `QueryDefinition` (`{ resource, where, sort, page, … }`) for `POST /records/query`, or use `client.records.list(resource, urlOpts)` for the GET adapter explicitly.'
|
|
1036
|
+
);
|
|
1037
|
+
}
|
|
1038
|
+
const path = getRecordApiPath(this.options.workspaceId, resourceOrSlug);
|
|
1039
|
+
return this.request<T>('GET', path, null, arg2);
|
|
1040
|
+
}
|
|
1041
|
+
|
|
1042
|
+
/** Get records by Ids. */
|
|
1043
|
+
public getRecordsByIds<T = any>(
|
|
1044
|
+
recordSlug: string,
|
|
1045
|
+
ids: string[]
|
|
1046
|
+
): Promise<ApiResponse<T[]>> {
|
|
1047
|
+
const path = getRecordApiPath(this.options.workspaceId, recordSlug) + '/bulk/get';
|
|
1048
|
+
return this.request('POST', path, { ids });
|
|
1049
|
+
}
|
|
1050
|
+
|
|
1051
|
+
/** Update an existing record by ID. */
|
|
1052
|
+
public updateRecord<T = any>(
|
|
1053
|
+
recordSlug: string,
|
|
1054
|
+
id: string,
|
|
1055
|
+
updates: Record<string, any>,
|
|
1056
|
+
options?: RecordTtlOptions
|
|
1057
|
+
): Promise<ApiResponse<T>> {
|
|
1058
|
+
const path = getRecordApiPath(this.options.workspaceId, recordSlug, id);
|
|
1059
|
+
const queryParams: Record<string, string> = {};
|
|
1060
|
+
if (options?.ttlSeconds) queryParams.ttlSeconds = String(options.ttlSeconds);
|
|
1061
|
+
if (options?.expiresAt) queryParams.expiresAt = options.expiresAt;
|
|
1062
|
+
if (options?.clearTtl) queryParams.clearTtl = 'true';
|
|
1063
|
+
return this.request('PUT', path, { ...updates }, Object.keys(queryParams).length > 0 ? queryParams : undefined);
|
|
1064
|
+
}
|
|
1065
|
+
|
|
1066
|
+
/**
|
|
1067
|
+
* Upsert a record: find by match fields, update if exists, create if not.
|
|
1068
|
+
* Uses advisory locking for atomicity — safe for concurrent calls.
|
|
1069
|
+
*
|
|
1070
|
+
* @param recordSlug - The structure's record slug
|
|
1071
|
+
* @param options - { match: key-value pairs to find existing record, data: full record data }
|
|
1072
|
+
* @returns Response where result.data is the record and result.operation indicates create/update
|
|
1073
|
+
*
|
|
1074
|
+
* @example
|
|
1075
|
+
* const result = await client.upsertRecord('HourlyRollup', {
|
|
1076
|
+
* match: { metricKey: 'pageviews', bucketHour: '2025-01-01T10:00' },
|
|
1077
|
+
* data: { metricKey: 'pageviews', bucketHour: '2025-01-01T10:00', count: 42 },
|
|
1078
|
+
* });
|
|
1079
|
+
* // result.data → the record
|
|
1080
|
+
* // result.operation → 'created' or 'updated'
|
|
1081
|
+
*/
|
|
1082
|
+
public upsertRecord<T = any>(
|
|
1083
|
+
recordSlug: string,
|
|
1084
|
+
options: { match: Record<string, any>; data: Record<string, any> }
|
|
1085
|
+
): Promise<ApiResponse<T> & { operation: 'created' | 'updated' }> {
|
|
1086
|
+
const path = getRecordApiPath(this.options.workspaceId, recordSlug) + '/upsert';
|
|
1087
|
+
return this.request('POST', path, { match: options.match, data: options.data }) as Promise<ApiResponse<T> & { operation: 'created' | 'updated' }>;
|
|
1088
|
+
}
|
|
1089
|
+
|
|
1090
|
+
/** Delete a record by ID (soft delete by default, can be restored). */
|
|
1091
|
+
public deleteRecord(
|
|
1092
|
+
recordSlug: string,
|
|
1093
|
+
id: string,
|
|
1094
|
+
options?: DeleteRecordOptions
|
|
1095
|
+
): Promise<ApiResponse<null>> {
|
|
1096
|
+
const path = getRecordApiPath(this.options.workspaceId, recordSlug, id);
|
|
1097
|
+
const queryParams = options?.hard ? { hard: 'true' } : undefined;
|
|
1098
|
+
return this.request('DELETE', path, null, queryParams);
|
|
1099
|
+
}
|
|
1100
|
+
|
|
1101
|
+
/** Restore a soft-deleted record by ID. */
|
|
1102
|
+
public restoreRecord(
|
|
1103
|
+
recordSlug: string,
|
|
1104
|
+
id: string
|
|
1105
|
+
): Promise<ApiResponse<null>> {
|
|
1106
|
+
const path = getRecordApiPath(this.options.workspaceId, recordSlug, id) + '/restore';
|
|
1107
|
+
return this.request('POST', path);
|
|
1108
|
+
}
|
|
1109
|
+
|
|
1110
|
+
// ------------------ Secrets API Methods ------------------
|
|
1111
|
+
|
|
1112
|
+
/**
|
|
1113
|
+
* Reveal plaintext values of secret fields for a record.
|
|
1114
|
+
* Requires secrets:reveal permission.
|
|
1115
|
+
*
|
|
1116
|
+
* @param recordSlug - The structure's record slug
|
|
1117
|
+
* @param id - The record ID
|
|
1118
|
+
* @param fields - Optional array of specific secret field names to reveal
|
|
1119
|
+
* @returns Object with field names as keys and plaintext secret values
|
|
1120
|
+
*
|
|
1121
|
+
* @example
|
|
1122
|
+
* ```ts
|
|
1123
|
+
* // Reveal all secrets
|
|
1124
|
+
* const result = await client.revealSecrets('users', 'record-123');
|
|
1125
|
+
* console.log(result.data.revealed.apiKey); // "sk_live_..."
|
|
1126
|
+
*
|
|
1127
|
+
* // Reveal specific fields
|
|
1128
|
+
* const result = await client.revealSecrets('users', 'record-123', ['apiKey']);
|
|
1129
|
+
* ```
|
|
1130
|
+
*/
|
|
1131
|
+
public revealSecrets(
|
|
1132
|
+
recordSlug: string,
|
|
1133
|
+
id: string,
|
|
1134
|
+
fields?: string[]
|
|
1135
|
+
): Promise<ApiResponse<{ revealed: Record<string, any> }>> {
|
|
1136
|
+
const path = getRecordApiPath(this.options.workspaceId, recordSlug, id) + '/secrets/reveal';
|
|
1137
|
+
return this.request('POST', path, fields ? { fields } : {});
|
|
1138
|
+
}
|
|
1139
|
+
|
|
1140
|
+
/**
|
|
1141
|
+
* Compare a candidate value against a secret field without revealing the stored secret.
|
|
1142
|
+
* Requires secrets:compare permission.
|
|
1143
|
+
*
|
|
1144
|
+
* @param recordSlug - The structure's record slug
|
|
1145
|
+
* @param id - The record ID
|
|
1146
|
+
* @param field - The secret field name to compare against
|
|
1147
|
+
* @param value - The candidate value to compare
|
|
1148
|
+
* @returns Boolean indicating if the values match
|
|
1149
|
+
*
|
|
1150
|
+
* @example
|
|
1151
|
+
* ```ts
|
|
1152
|
+
* const result = await client.compareSecret('users', 'record-123', 'apiKey', 'sk_live_test');
|
|
1153
|
+
* if (result.data.matches) {
|
|
1154
|
+
* console.log('API key is valid');
|
|
1155
|
+
* }
|
|
1156
|
+
* ```
|
|
1157
|
+
*/
|
|
1158
|
+
public compareSecret(
|
|
1159
|
+
recordSlug: string,
|
|
1160
|
+
id: string,
|
|
1161
|
+
field: string,
|
|
1162
|
+
value: string
|
|
1163
|
+
): Promise<ApiResponse<{ matches: boolean }>> {
|
|
1164
|
+
const path = getRecordApiPath(this.options.workspaceId, recordSlug, id) + '/secrets/compare';
|
|
1165
|
+
return this.request('POST', path, { field, value });
|
|
1166
|
+
}
|
|
1167
|
+
|
|
1168
|
+
// ------------------ Storage API Methods ------------------
|
|
1169
|
+
|
|
1170
|
+
/**
|
|
1171
|
+
* Upload a file to the storage service.
|
|
1172
|
+
*
|
|
1173
|
+
* @param file - The file to upload
|
|
1174
|
+
* @param location - Target folder path (e.g., '/root/shared/images'). Defaults to '/root/shared' if not specified.
|
|
1175
|
+
* /root/shared always exists. For custom subfolders, create them first with createFolder().
|
|
1176
|
+
* @param isPublic - If true, the file will be publicly accessible without authentication. Defaults to false.
|
|
1177
|
+
* @returns The file URL or render ID
|
|
1178
|
+
*
|
|
1179
|
+
* @example
|
|
1180
|
+
* ```ts
|
|
1181
|
+
* // Upload to default location (/root/shared)
|
|
1182
|
+
* const result = await client.uploadFile(file);
|
|
1183
|
+
*
|
|
1184
|
+
* // Upload to specific folder
|
|
1185
|
+
* const result = await client.uploadFile(file, '/root/shared/images');
|
|
1186
|
+
*
|
|
1187
|
+
* // Upload as public file
|
|
1188
|
+
* const result = await client.uploadFile(file, '/root/shared/public', true);
|
|
1189
|
+
* ```
|
|
1190
|
+
*/
|
|
1191
|
+
public async uploadFile(
|
|
1192
|
+
file: File,
|
|
1193
|
+
location?: string,
|
|
1194
|
+
isPublic: boolean = false
|
|
1195
|
+
): Promise<ApiResponse<string>> {
|
|
1196
|
+
const path = getFileUploadApiPath(this.options.workspaceId);
|
|
1197
|
+
const formData = new FormData();
|
|
1198
|
+
const fileName = this.options.workspaceId + Date.now() + file.name;
|
|
1199
|
+
|
|
1200
|
+
formData.append('file', file);
|
|
1201
|
+
formData.append('fileName', fileName);
|
|
1202
|
+
formData.append('isPublic', isPublic ? 'true' : 'false');
|
|
1203
|
+
|
|
1204
|
+
// Only append location if specified; backend defaults to /root/shared
|
|
1205
|
+
if (location) {
|
|
1206
|
+
formData.append('location', location);
|
|
1207
|
+
}
|
|
1208
|
+
|
|
1209
|
+
return this.request<string>('POST', path, formData, undefined, {
|
|
1210
|
+
headers: {
|
|
1211
|
+
'Content-Type': 'multipart/form-data',
|
|
1212
|
+
},
|
|
1213
|
+
});
|
|
1214
|
+
}
|
|
1215
|
+
|
|
1216
|
+
// ------------------ Folder API Methods ------------------
|
|
1217
|
+
|
|
1218
|
+
/**
|
|
1219
|
+
* Create a folder in the storage service.
|
|
1220
|
+
* Use this to create subfolders under /root/shared (which always exists).
|
|
1221
|
+
*
|
|
1222
|
+
* @param name - The folder name (e.g., 'logos', 'avatars')
|
|
1223
|
+
* @param location - Parent folder path (e.g., '/root/shared'). Defaults to '/root/shared'.
|
|
1224
|
+
* @returns The created folder object
|
|
1225
|
+
*
|
|
1226
|
+
* @example
|
|
1227
|
+
* ```ts
|
|
1228
|
+
* // Create a folder under /root/shared
|
|
1229
|
+
* const folder = await client.createFolder('logos', '/root/shared');
|
|
1230
|
+
* // Result: folder at /root/shared/logos
|
|
1231
|
+
*
|
|
1232
|
+
* // Then upload to it
|
|
1233
|
+
* const { data: renderId } = await client.uploadFile(file, '/root/shared/logos', true);
|
|
1234
|
+
* ```
|
|
1235
|
+
*/
|
|
1236
|
+
public async createFolder(
|
|
1237
|
+
name: string,
|
|
1238
|
+
location: string = '/root/shared'
|
|
1239
|
+
): Promise<ApiResponse<any>> {
|
|
1240
|
+
const path = `storage/ws/${this.options.workspaceId}/api/v1/folders`;
|
|
1241
|
+
return this.request<any>('POST', path, { name, location });
|
|
1242
|
+
}
|
|
1243
|
+
|
|
1244
|
+
/**
|
|
1245
|
+
* List folders in the workspace, optionally filtered by parent location.
|
|
1246
|
+
*
|
|
1247
|
+
* @param location - Parent folder path to list contents of (e.g., '/root/shared'). If omitted, lists top-level folders.
|
|
1248
|
+
* @returns Array of folder objects
|
|
1249
|
+
*
|
|
1250
|
+
* @example
|
|
1251
|
+
* ```ts
|
|
1252
|
+
* // List all folders under /root/shared
|
|
1253
|
+
* const folders = await client.listFolders('/root/shared');
|
|
1254
|
+
*
|
|
1255
|
+
* // Check if a folder exists before uploading
|
|
1256
|
+
* const folders = await client.listFolders('/root/shared');
|
|
1257
|
+
* const hasLogos = folders.data.some(f => f.name === 'logos');
|
|
1258
|
+
* if (!hasLogos) {
|
|
1259
|
+
* await client.createFolder('logos', '/root/shared');
|
|
1260
|
+
* }
|
|
1261
|
+
* ```
|
|
1262
|
+
*/
|
|
1263
|
+
public async listFolders(location?: string): Promise<ApiResponse<any[]>> {
|
|
1264
|
+
const path = `storage/ws/${this.options.workspaceId}/api/v1/folders`;
|
|
1265
|
+
const params = location ? { location } : undefined;
|
|
1266
|
+
return this.request<any[]>('GET', path, null, params);
|
|
1267
|
+
}
|
|
1268
|
+
|
|
1269
|
+
/**
|
|
1270
|
+
* Get a specific folder by ID.
|
|
1271
|
+
*
|
|
1272
|
+
* @param folderId - The folder ID (UUID)
|
|
1273
|
+
* @returns The folder object
|
|
1274
|
+
*/
|
|
1275
|
+
public async getFolder(folderId: string): Promise<ApiResponse<any>> {
|
|
1276
|
+
const path = `storage/ws/${this.options.workspaceId}/api/v1/folders/${folderId}`;
|
|
1277
|
+
return this.request<any>('GET', path);
|
|
1278
|
+
}
|
|
1279
|
+
|
|
1280
|
+
/**
|
|
1281
|
+
* List sub-folders within a specific folder.
|
|
1282
|
+
*
|
|
1283
|
+
* @param folderId - The parent folder ID (UUID)
|
|
1284
|
+
* @returns Array of sub-folder objects
|
|
1285
|
+
*/
|
|
1286
|
+
public async listSubFolders(folderId: string): Promise<ApiResponse<any[]>> {
|
|
1287
|
+
const path = `storage/ws/${this.options.workspaceId}/api/v1/folders/${folderId}/sub-folders`;
|
|
1288
|
+
return this.request<any[]>('GET', path);
|
|
1289
|
+
}
|
|
1290
|
+
|
|
1291
|
+
/**
|
|
1292
|
+
* Delete a folder by ID. System folders (/root, /root/shared, /root/users) cannot be deleted.
|
|
1293
|
+
*
|
|
1294
|
+
* @param folderId - The folder ID (UUID) to delete
|
|
1295
|
+
*/
|
|
1296
|
+
public async deleteFolder(folderId: string): Promise<ApiResponse<void>> {
|
|
1297
|
+
const path = `storage/ws/${this.options.workspaceId}/api/v1/folders/${folderId}`;
|
|
1298
|
+
return this.request<void>('DELETE', path);
|
|
1299
|
+
}
|
|
1300
|
+
|
|
1301
|
+
// ------------------ File Render/Download URL Methods ------------------
|
|
1302
|
+
|
|
1303
|
+
/**
|
|
1304
|
+
* Get the render URL for a file. Use this URL to display files inline (e.g., images in img tags).
|
|
1305
|
+
* Supports optional image transformation parameters.
|
|
1306
|
+
*
|
|
1307
|
+
* @param renderId - The render ID returned from uploadFile()
|
|
1308
|
+
* @param options - Optional image transformation parameters
|
|
1309
|
+
* @returns The full render URL
|
|
1310
|
+
*
|
|
1311
|
+
* @example
|
|
1312
|
+
* ```ts
|
|
1313
|
+
* // Basic render URL
|
|
1314
|
+
* const url = client.getFileRenderUrl('abc123');
|
|
1315
|
+
* // => "https://api.centrali.io/storage/ws/my-workspace/api/v1/render/abc123"
|
|
1316
|
+
*
|
|
1317
|
+
* // With image transformations
|
|
1318
|
+
* const thumbUrl = client.getFileRenderUrl('abc123', { width: 200 });
|
|
1319
|
+
* const compressedUrl = client.getFileRenderUrl('abc123', { width: 800, quality: 60, format: 'webp' });
|
|
1320
|
+
* ```
|
|
1321
|
+
*/
|
|
1322
|
+
public getFileRenderUrl(
|
|
1323
|
+
renderId: string,
|
|
1324
|
+
options?: {
|
|
1325
|
+
width?: number;
|
|
1326
|
+
height?: number;
|
|
1327
|
+
quality?: number;
|
|
1328
|
+
format?: 'jpeg' | 'png' | 'webp';
|
|
1329
|
+
}
|
|
1330
|
+
): string {
|
|
1331
|
+
const apiUrl = getApiUrl(this.options.baseUrl);
|
|
1332
|
+
const baseUrl = `${apiUrl}/storage/ws/${this.options.workspaceId}/api/v1/render/${renderId}`;
|
|
1333
|
+
|
|
1334
|
+
if (!options) {
|
|
1335
|
+
return baseUrl;
|
|
1336
|
+
}
|
|
1337
|
+
|
|
1338
|
+
const params = new URLSearchParams();
|
|
1339
|
+
if (options.width) params.append('width', String(options.width));
|
|
1340
|
+
if (options.height) params.append('height', String(options.height));
|
|
1341
|
+
if (options.quality) params.append('quality', String(options.quality));
|
|
1342
|
+
if (options.format) params.append('format', options.format);
|
|
1343
|
+
|
|
1344
|
+
const queryString = params.toString();
|
|
1345
|
+
return queryString ? `${baseUrl}?${queryString}` : baseUrl;
|
|
1346
|
+
}
|
|
1347
|
+
|
|
1348
|
+
/**
|
|
1349
|
+
* Get the download URL for a file. Use this URL to download files as attachments.
|
|
1350
|
+
*
|
|
1351
|
+
* @param renderId - The render ID returned from uploadFile()
|
|
1352
|
+
* @returns The full download URL
|
|
1353
|
+
*
|
|
1354
|
+
* @example
|
|
1355
|
+
* ```ts
|
|
1356
|
+
* const downloadUrl = client.getFileDownloadUrl('abc123');
|
|
1357
|
+
* // => "https://api.centrali.io/storage/ws/my-workspace/api/v1/download/abc123"
|
|
1358
|
+
* ```
|
|
1359
|
+
*/
|
|
1360
|
+
public getFileDownloadUrl(renderId: string): string {
|
|
1361
|
+
const apiUrl = getApiUrl(this.options.baseUrl);
|
|
1362
|
+
return `${apiUrl}/storage/ws/${this.options.workspaceId}/api/v1/download/${renderId}`;
|
|
1363
|
+
}
|
|
1364
|
+
|
|
1365
|
+
// ------------------ Search API Methods ------------------
|
|
1366
|
+
|
|
1367
|
+
/**
|
|
1368
|
+
* Search records across the workspace using full-text search.
|
|
1369
|
+
*
|
|
1370
|
+
* @param query - The search query string
|
|
1371
|
+
* @param options - Optional search parameters
|
|
1372
|
+
* @returns Search results with hits and metadata
|
|
1373
|
+
*
|
|
1374
|
+
* @example
|
|
1375
|
+
* ```ts
|
|
1376
|
+
* // Basic search
|
|
1377
|
+
* const results = await client.search('customer email');
|
|
1378
|
+
* console.log('Found:', results.data.totalHits, 'results');
|
|
1379
|
+
* results.data.hits.forEach(hit => console.log(hit.id, hit.structureSlug));
|
|
1380
|
+
*
|
|
1381
|
+
* // Search with structure filter
|
|
1382
|
+
* const userResults = await client.search('john', { structures: 'users' });
|
|
1383
|
+
*
|
|
1384
|
+
* // Search multiple structures with limit
|
|
1385
|
+
* const results = await client.search('active', {
|
|
1386
|
+
* structures: ['users', 'orders'],
|
|
1387
|
+
* limit: 50
|
|
1388
|
+
* });
|
|
1389
|
+
* ```
|
|
1390
|
+
*/
|
|
1391
|
+
public async search(
|
|
1392
|
+
query: string,
|
|
1393
|
+
options?: SearchOptions
|
|
1394
|
+
): Promise<ApiResponse<SearchResponse>> {
|
|
1395
|
+
const path = getSearchApiPath(this.options.workspaceId);
|
|
1396
|
+
|
|
1397
|
+
const queryParams: Record<string, string> = { q: query };
|
|
1398
|
+
|
|
1399
|
+
if (options?.collections) {
|
|
1400
|
+
const collections = Array.isArray(options.collections)
|
|
1401
|
+
? options.collections.join(',')
|
|
1402
|
+
: options.collections;
|
|
1403
|
+
queryParams.collections = collections;
|
|
1404
|
+
} else if (options?.structures) {
|
|
1405
|
+
emitDeprecationWarning("The 'structures' search option is deprecated. Use 'collections' instead.");
|
|
1406
|
+
const structures = Array.isArray(options.structures)
|
|
1407
|
+
? options.structures.join(',')
|
|
1408
|
+
: options.structures;
|
|
1409
|
+
queryParams.collections = structures;
|
|
1410
|
+
}
|
|
1411
|
+
|
|
1412
|
+
if (options?.limit) {
|
|
1413
|
+
queryParams.limit = String(options.limit);
|
|
1414
|
+
}
|
|
1415
|
+
|
|
1416
|
+
return this.request<SearchResponse>('GET', path, null, queryParams);
|
|
1417
|
+
}
|
|
1418
|
+
|
|
1419
|
+
// ------------------ Authorization API Methods (BYOT) ------------------
|
|
1420
|
+
|
|
1421
|
+
/**
|
|
1422
|
+
* Check if an action is authorized for an external token.
|
|
1423
|
+
*
|
|
1424
|
+
* Use this method when you want to authorize access using tokens from your
|
|
1425
|
+
* own identity provider (Clerk, Auth0, Okta, etc.) instead of Centrali's
|
|
1426
|
+
* built-in authentication.
|
|
1427
|
+
*
|
|
1428
|
+
* **Use Cases:**
|
|
1429
|
+
* 1. **AuthZ-as-a-Service**: Define custom resources (orders, invoices) in Centrali
|
|
1430
|
+
* and use it purely for authorization decisions.
|
|
1431
|
+
* 2. **External IdP for Centrali resources**: Access Centrali data (records, files)
|
|
1432
|
+
* using your corporate IdP tokens.
|
|
1433
|
+
*
|
|
1434
|
+
* **Prerequisites:**
|
|
1435
|
+
* - Configure an External Auth Provider in Centrali Console (Settings → External Auth)
|
|
1436
|
+
* - Define claim mappings to extract attributes from your JWT
|
|
1437
|
+
* - Create policies that reference the extracted attributes (prefixed with `ext_`)
|
|
1438
|
+
*
|
|
1439
|
+
* @example
|
|
1440
|
+
* // Simple authorization check
|
|
1441
|
+
* const result = await client.checkAuthorization({
|
|
1442
|
+
* token: clerkJWT,
|
|
1443
|
+
* resource: 'orders',
|
|
1444
|
+
* action: 'read'
|
|
1445
|
+
* });
|
|
1446
|
+
*
|
|
1447
|
+
* if (result.data.allowed) {
|
|
1448
|
+
* // Proceed with the action
|
|
1449
|
+
* }
|
|
1450
|
+
*
|
|
1451
|
+
* @example
|
|
1452
|
+
* // Authorization with context for policy evaluation
|
|
1453
|
+
* const result = await client.checkAuthorization({
|
|
1454
|
+
* token: clerkJWT,
|
|
1455
|
+
* resource: 'orders',
|
|
1456
|
+
* action: 'approve',
|
|
1457
|
+
* context: {
|
|
1458
|
+
* orderId: 'order-123',
|
|
1459
|
+
* orderAmount: 50000,
|
|
1460
|
+
* department: 'sales'
|
|
1461
|
+
* }
|
|
1462
|
+
* });
|
|
1463
|
+
*
|
|
1464
|
+
* // Policy can check: ext_role == 'manager' AND request_metadata.orderAmount > 10000
|
|
1465
|
+
*
|
|
1466
|
+
* @param options - Authorization check options
|
|
1467
|
+
* @returns Promise resolving to the authorization result
|
|
1468
|
+
*/
|
|
1469
|
+
public async checkAuthorization(
|
|
1470
|
+
options: CheckAuthorizationOptions
|
|
1471
|
+
): Promise<ApiResponse<AuthorizationResult>> {
|
|
1472
|
+
const { token, resource, action, resourceCategory = 'custom', context } = options;
|
|
1473
|
+
|
|
1474
|
+
const path = `/workspace/${this.options.workspaceId}/api/v1/access/evaluate`;
|
|
1475
|
+
|
|
1476
|
+
const body = {
|
|
1477
|
+
action,
|
|
1478
|
+
resource_name: resource,
|
|
1479
|
+
resource_category: resourceCategory,
|
|
1480
|
+
request_data: context ? { request_metadata: context } : undefined
|
|
1481
|
+
};
|
|
1482
|
+
|
|
1483
|
+
// Make request with the external token
|
|
1484
|
+
const response = await this.axios.request<{
|
|
1485
|
+
status: string;
|
|
1486
|
+
response: 'allow' | 'deny' | 'not_applicable';
|
|
1487
|
+
message?: string;
|
|
1488
|
+
}>({
|
|
1489
|
+
method: 'POST',
|
|
1490
|
+
url: path,
|
|
1491
|
+
data: body,
|
|
1492
|
+
headers: {
|
|
1493
|
+
'Authorization': `Bearer ${token}`,
|
|
1494
|
+
'Content-Type': 'application/json'
|
|
1495
|
+
}
|
|
1496
|
+
});
|
|
1497
|
+
|
|
1498
|
+
return {
|
|
1499
|
+
data: {
|
|
1500
|
+
allowed: response.data.response === 'allow',
|
|
1501
|
+
decision: response.data.response,
|
|
1502
|
+
message: response.data.message
|
|
1503
|
+
}
|
|
1504
|
+
};
|
|
1505
|
+
}
|
|
1506
|
+
|
|
1507
|
+
}
|