@objectstack/client 4.0.3 → 4.0.5

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/src/index.ts DELETED
@@ -1,1875 +0,0 @@
1
- // Copyright (c) 2025 ObjectStack. Licensed under the Apache-2.0 license.
2
-
3
- import { QueryAST, SortNode, AggregationNode, isFilterAST } from '@objectstack/spec/data';
4
- import {
5
- BatchUpdateRequest,
6
- BatchUpdateResponse,
7
- UpdateManyRequest,
8
- DeleteManyRequest,
9
- BatchOptions,
10
- MetadataCacheRequest,
11
- MetadataCacheResponse,
12
- StandardErrorCode,
13
- ErrorCategory,
14
- GetDiscoveryResponse,
15
- GetMetaTypesResponse,
16
- GetMetaItemsResponse,
17
- LoginRequest,
18
- SessionResponse,
19
- GetPresignedUrlRequest,
20
- PresignedUrlResponse,
21
- CompleteUploadRequest,
22
- FileUploadResponse,
23
- InitiateChunkedUploadRequest,
24
- InitiateChunkedUploadResponse,
25
- UploadChunkResponse,
26
- CompleteChunkedUploadRequest,
27
- CompleteChunkedUploadResponse,
28
- UploadProgress,
29
- CheckPermissionRequest,
30
- CheckPermissionResponse,
31
- GetObjectPermissionsResponse,
32
- GetEffectivePermissionsResponse,
33
- RealtimeConnectRequest,
34
- RealtimeConnectResponse,
35
- RealtimeSubscribeRequest,
36
- RealtimeSubscribeResponse,
37
- SetPresenceRequest,
38
- GetPresenceResponse,
39
- GetWorkflowConfigResponse,
40
- GetWorkflowStateResponse,
41
- WorkflowTransitionRequest,
42
- WorkflowTransitionResponse,
43
- WorkflowApproveRequest,
44
- WorkflowApproveResponse,
45
- WorkflowRejectRequest,
46
- WorkflowRejectResponse,
47
- ListViewsResponse,
48
- GetViewResponse,
49
- CreateViewRequest,
50
- CreateViewResponse,
51
- UpdateViewRequest,
52
- UpdateViewResponse,
53
- DeleteViewResponse,
54
- RegisterDeviceRequest,
55
- RegisterDeviceResponse,
56
- UnregisterDeviceResponse,
57
- GetNotificationPreferencesResponse,
58
- UpdateNotificationPreferencesRequest,
59
- UpdateNotificationPreferencesResponse,
60
- ListNotificationsResponse,
61
- MarkNotificationsReadResponse,
62
- MarkAllNotificationsReadResponse,
63
- AiNlqRequest,
64
- AiNlqResponse,
65
- AiSuggestRequest,
66
- AiSuggestResponse,
67
- AiInsightsRequest,
68
- AiInsightsResponse,
69
- GetLocalesResponse,
70
- GetTranslationsResponse,
71
- GetFieldLabelsResponse,
72
- RegisterRequest,
73
- GetFeedResponse,
74
- CreateFeedItemResponse,
75
- UpdateFeedItemResponse,
76
- DeleteFeedItemResponse,
77
- AddReactionResponse,
78
- RemoveReactionResponse,
79
- PinFeedItemResponse,
80
- UnpinFeedItemResponse,
81
- StarFeedItemResponse,
82
- UnstarFeedItemResponse,
83
- SearchFeedResponse,
84
- GetChangelogResponse,
85
- SubscribeResponse,
86
- UnsubscribeResponse,
87
- WellKnownCapabilities,
88
- ApiRoutes,
89
- } from '@objectstack/spec/api';
90
- import { Logger, createLogger } from '@objectstack/core';
91
- import { RealtimeAPI } from './realtime-api';
92
-
93
- /**
94
- * Route types that the client can resolve.
95
- * Covers all keys from `ApiRoutes` (the discovery schema) plus
96
- * client-specific virtual routes (`views`, `permissions`).
97
- */
98
- export type ApiRouteType = keyof ApiRoutes | 'views' | 'permissions';
99
-
100
- export interface ClientConfig {
101
- baseUrl: string;
102
- token?: string;
103
- /**
104
- * Custom fetch implementation (e.g. node-fetch or for Next.js caching)
105
- */
106
- fetch?: (input: RequestInfo | URL, init?: RequestInit) => Promise<Response>;
107
- /**
108
- * Logger instance for debugging
109
- */
110
- logger?: Logger;
111
- /**
112
- * Enable debug logging
113
- */
114
- debug?: boolean;
115
- }
116
-
117
- /**
118
- * Discovery Result
119
- * Re-export from @objectstack/spec/api for convenience
120
- */
121
- export type DiscoveryResult = GetDiscoveryResponse;
122
-
123
- /**
124
- * @deprecated Use `data.query()` with standard QueryAST parameters instead.
125
- * This interface uses legacy parameter names (filter/sort/top/skip) that
126
- * require translation to QueryAST. Prefer QueryAST fields directly:
127
- * - filter → where
128
- * - select → fields
129
- * - sort → orderBy
130
- * - skip → offset
131
- * - top → limit
132
- */
133
- export interface QueryOptions {
134
- select?: string[]; // Simplified Selection
135
- /** @canonical Preferred filter parameter (singular). */
136
- filter?: Record<string, any> | unknown[]; // Map or AST
137
- /** @deprecated Use `filter` (singular). Kept for backward compatibility. */
138
- filters?: Record<string, any> | unknown[]; // Map or AST
139
- sort?: string | string[] | SortNode[]; // 'name' or ['-created_at'] or AST
140
- top?: number;
141
- skip?: number;
142
- // Advanced features
143
- aggregations?: AggregationNode[];
144
- groupBy?: string[];
145
- }
146
-
147
- /**
148
- * Canonical query options using Spec protocol field names.
149
- * This is the recommended interface for `data.find()` queries.
150
- *
151
- * Canonical field mapping (QueryAST-aligned):
152
- * - `where` — filter conditions (replaces legacy `filter`/`filters`)
153
- * - `fields` — field selection (replaces legacy `select`)
154
- * - `orderBy` — sort definition (replaces legacy `sort`)
155
- * - `limit` — max records (replaces legacy `top`)
156
- * - `offset` — skip records (replaces legacy `skip`)
157
- * - `expand` — relation loading (replaces legacy `populate`)
158
- */
159
- export interface QueryOptionsV2 {
160
- /** Filter conditions (WHERE clause). Accepts MongoDB-style $op object or FilterCondition AST. */
161
- where?: Record<string, any> | unknown[];
162
- /** Fields to retrieve (SELECT clause). */
163
- fields?: string[];
164
- /** Sort definition (ORDER BY clause). */
165
- orderBy?: string | string[] | SortNode[];
166
- /** Maximum number of records to return (LIMIT). */
167
- limit?: number;
168
- /** Number of records to skip (OFFSET). */
169
- offset?: number;
170
- /** Relations to expand (JOIN / eager-load). */
171
- expand?: Record<string, any> | string[];
172
- /** Aggregation functions. */
173
- aggregations?: AggregationNode[];
174
- /** Group by fields. */
175
- groupBy?: string[];
176
- }
177
-
178
- export interface PaginatedResult<T = any> {
179
- /** Spec-compliant: array of matching records */
180
- records: T[];
181
- /** Total number of matching records (if requested) */
182
- total?: number;
183
- /** The object name */
184
- object?: string;
185
- /** Whether more records are available */
186
- hasMore?: boolean;
187
- }
188
-
189
- /** Spec: GetDataResponseSchema */
190
- export interface GetDataResult<T = any> {
191
- object: string;
192
- id: string;
193
- record: T;
194
- }
195
-
196
- /** Spec: CreateDataResponseSchema */
197
- export interface CreateDataResult<T = any> {
198
- object: string;
199
- id: string;
200
- record: T;
201
- }
202
-
203
- /** Spec: UpdateDataResponseSchema */
204
- export interface UpdateDataResult<T = any> {
205
- object: string;
206
- id: string;
207
- record: T;
208
- }
209
-
210
- /** Spec: DeleteDataResponseSchema */
211
- export interface DeleteDataResult {
212
- object: string;
213
- id: string;
214
- deleted: boolean;
215
- }
216
-
217
- export interface StandardError {
218
- code: StandardErrorCode;
219
- message: string;
220
- category: ErrorCategory;
221
- httpStatus: number;
222
- retryable: boolean;
223
- details?: Record<string, any>;
224
- }
225
-
226
- export class ObjectStackClient {
227
- private baseUrl: string;
228
- private token?: string;
229
- private fetchImpl: (input: RequestInfo | URL, init?: RequestInit) => Promise<Response>;
230
- private discoveryInfo?: DiscoveryResult;
231
- private logger: Logger;
232
- private realtimeAPI: RealtimeAPI;
233
-
234
- constructor(config: ClientConfig) {
235
- this.baseUrl = config.baseUrl.replace(/\/$/, ''); // Remove trailing slash
236
- this.token = config.token;
237
- this.fetchImpl = config.fetch || globalThis.fetch.bind(globalThis);
238
-
239
- // Initialize logger
240
- this.logger = config.logger || createLogger({
241
- level: config.debug ? 'debug' : 'info',
242
- format: 'pretty'
243
- });
244
-
245
- // Initialize realtime API
246
- this.realtimeAPI = new RealtimeAPI(this.baseUrl, this.token);
247
-
248
- this.logger.debug('ObjectStack client created', { baseUrl: this.baseUrl });
249
- }
250
-
251
- /**
252
- * Initialize the client by discovering server capabilities.
253
- */
254
- async connect() {
255
- this.logger.debug('Connecting to ObjectStack server', { baseUrl: this.baseUrl });
256
-
257
- try {
258
- let data: DiscoveryResult | undefined;
259
-
260
- // 1. Try Standard Discovery (.well-known)
261
- try {
262
- let wellKnownUrl: string;
263
- try {
264
- // If baseUrl is absolute, get origin
265
- const url = new URL(this.baseUrl);
266
- wellKnownUrl = `${url.origin}/.well-known/objectstack`;
267
- } catch {
268
- // If baseUrl is relative, use absolute path from root
269
- wellKnownUrl = '/.well-known/objectstack';
270
- }
271
-
272
- this.logger.debug('Probing .well-known discovery', { url: wellKnownUrl });
273
- const res = await this.fetchImpl(wellKnownUrl);
274
- if (res.ok) {
275
- const body = await res.json();
276
- data = body.data || body;
277
- this.logger.debug('Discovered via .well-known');
278
- }
279
- } catch (e) {
280
- this.logger.debug('Standard discovery probe failed', { error: (e as Error).message });
281
- }
282
-
283
- // 2. Fallback to Protocol-standard Discovery Path /api/v1/discovery
284
- if (!data) {
285
- const fallbackUrl = `${this.baseUrl}/api/v1/discovery`;
286
- this.logger.debug('Falling back to standard discovery endpoint', { url: fallbackUrl });
287
- const res = await this.fetchImpl(fallbackUrl);
288
- if (!res.ok) {
289
- throw new Error(`Failed to connect to ${fallbackUrl}: ${res.statusText}`);
290
- }
291
- const body = await res.json();
292
- data = body.data || body;
293
- }
294
-
295
- if (!data) {
296
- throw new Error('Connection failed: No discovery data returned');
297
- }
298
-
299
- this.discoveryInfo = data;
300
-
301
- this.logger.info('Connected to ObjectStack server', {
302
- version: data.version,
303
- apiName: data.apiName,
304
- services: data.services
305
- });
306
-
307
- return data as DiscoveryResult;
308
- } catch (e) {
309
- this.logger.error('Failed to connect to ObjectStack server', e as Error, { baseUrl: this.baseUrl });
310
- throw e;
311
- }
312
- }
313
-
314
- /**
315
- * Well-known capability flags discovered from the server.
316
- * Returns undefined if the client has not yet connected or the server
317
- * did not include capabilities in its discovery response.
318
- *
319
- * The server may return capabilities in hierarchical format
320
- * `{ key: { enabled: boolean } }` or flat boolean format `{ key: boolean }`.
321
- * This getter normalizes both to flat `WellKnownCapabilities`.
322
- */
323
- get capabilities(): WellKnownCapabilities | undefined {
324
- const raw = this.discoveryInfo?.capabilities;
325
- if (!raw) return undefined;
326
- // Normalize: hierarchical { enabled: boolean } → flat boolean
327
- const result: Record<string, boolean> = {};
328
- for (const [key, value] of Object.entries(raw)) {
329
- result[key] = typeof value === 'object' && value !== null ? !!(value as any).enabled : !!value;
330
- }
331
- return result as unknown as WellKnownCapabilities;
332
- }
333
-
334
- /**
335
- * Metadata Operations
336
- */
337
- meta = {
338
- /**
339
- * Get all available metadata types
340
- * Returns types like 'object', 'plugin', 'view', etc.
341
- */
342
- getTypes: async (): Promise<GetMetaTypesResponse> => {
343
- const route = this.getRoute('metadata');
344
- const res = await this.fetch(`${this.baseUrl}${route}`);
345
- return this.unwrapResponse<GetMetaTypesResponse>(res);
346
- },
347
-
348
- /**
349
- * Get all items of a specific metadata type
350
- * @param type - Metadata type name (e.g., 'object', 'plugin')
351
- * @param options - Optional filters (e.g., packageId to scope by package)
352
- */
353
- getItems: async (type: string, options?: { packageId?: string }): Promise<GetMetaItemsResponse> => {
354
- const route = this.getRoute('metadata');
355
- const params = new URLSearchParams();
356
- if (options?.packageId) params.set('package', options.packageId);
357
- const qs = params.toString();
358
- const url = `${this.baseUrl}${route}/${type}${qs ? `?${qs}` : ''}`;
359
- const res = await this.fetch(url);
360
- return this.unwrapResponse<GetMetaItemsResponse>(res);
361
- },
362
-
363
- /**
364
- * Get a specific metadata item by type and name
365
- * @param type - Metadata type (e.g., 'object', 'plugin')
366
- * @param name - Item name (snake_case identifier)
367
- * @param options - Optional filters (e.g., packageId to scope by package)
368
- */
369
- getItem: async (type: string, name: string, options?: { packageId?: string }) => {
370
- const route = this.getRoute('metadata');
371
- const params = new URLSearchParams();
372
- if (options?.packageId) params.set('package', options.packageId);
373
- const qs = params.toString();
374
- const url = `${this.baseUrl}${route}/${type}/${name}${qs ? `?${qs}` : ''}`;
375
- const res = await this.fetch(url);
376
- return this.unwrapResponse(res);
377
- },
378
-
379
- /**
380
- * Save a metadata item
381
- * @param type - Metadata type (e.g., 'object', 'plugin')
382
- * @param name - Item name
383
- * @param item - The metadata content to save
384
- */
385
- saveItem: async (type: string, name: string, item: any) => {
386
- const route = this.getRoute('metadata');
387
- const res = await this.fetch(`${this.baseUrl}${route}/${type}/${name}`, {
388
- method: 'PUT',
389
- body: JSON.stringify(item)
390
- });
391
- return this.unwrapResponse(res);
392
- },
393
-
394
- /**
395
- * Delete a metadata item
396
- * @param type - Metadata type (e.g., 'object', 'plugin')
397
- * @param name - Item name (snake_case identifier)
398
- */
399
- deleteItem: async (type: string, name: string): Promise<{ type: string; name: string; deleted: boolean }> => {
400
- const route = this.getRoute('metadata');
401
- const res = await this.fetch(`${this.baseUrl}${route}/${encodeURIComponent(type)}/${encodeURIComponent(name)}`, {
402
- method: 'DELETE',
403
- });
404
- return this.unwrapResponse(res);
405
- },
406
-
407
- /**
408
- * Get object metadata with cache support
409
- * Supports ETag-based conditional requests for efficient caching
410
- */
411
- getCached: async (name: string, cacheOptions?: MetadataCacheRequest): Promise<MetadataCacheResponse> => {
412
- const route = this.getRoute('metadata');
413
- const headers: Record<string, string> = {};
414
-
415
- if (cacheOptions?.ifNoneMatch) {
416
- headers['If-None-Match'] = cacheOptions.ifNoneMatch;
417
- }
418
- if (cacheOptions?.ifModifiedSince) {
419
- headers['If-Modified-Since'] = cacheOptions.ifModifiedSince;
420
- }
421
-
422
- const res = await this.fetch(`${this.baseUrl}${route}/object/${name}`, {
423
- headers
424
- });
425
-
426
- // Check for 304 Not Modified
427
- if (res.status === 304) {
428
- return {
429
- notModified: true,
430
- etag: cacheOptions?.ifNoneMatch ? {
431
- value: cacheOptions.ifNoneMatch.replace(/^W\/|"/g, ''),
432
- weak: cacheOptions.ifNoneMatch.startsWith('W/')
433
- } : undefined
434
- };
435
- }
436
-
437
- const data = await res.json();
438
- const etag = res.headers.get('ETag');
439
- const lastModified = res.headers.get('Last-Modified');
440
-
441
- return {
442
- data,
443
- etag: etag ? {
444
- value: etag.replace(/^W\/|"/g, ''),
445
- weak: etag.startsWith('W/')
446
- } : undefined,
447
- lastModified: lastModified || undefined,
448
- notModified: false
449
- };
450
- },
451
-
452
- getView: async (object: string, type: 'list' | 'form' = 'list') => {
453
- const route = this.getRoute('ui');
454
- const res = await this.fetch(`${this.baseUrl}${route}/view/${object}?type=${type}`);
455
- return this.unwrapResponse(res);
456
- }
457
- };
458
-
459
- /**
460
- * Analytics Services
461
- */
462
- analytics = {
463
- query: async (payload: any) => {
464
- const route = this.getRoute('analytics');
465
- const res = await this.fetch(`${this.baseUrl}${route}/query`, {
466
- method: 'POST',
467
- body: JSON.stringify(payload)
468
- });
469
- return res.json();
470
- },
471
- meta: async (cube: string) => {
472
- const route = this.getRoute('analytics');
473
- const res = await this.fetch(`${this.baseUrl}${route}/meta/${cube}`);
474
- return res.json();
475
- },
476
- explain: async (payload: any) => {
477
- const route = this.getRoute('analytics');
478
- const res = await this.fetch(`${this.baseUrl}${route}/explain`, {
479
- method: 'POST',
480
- body: JSON.stringify(payload)
481
- });
482
- return res.json();
483
- }
484
- };
485
-
486
- /**
487
- * Package Management Services
488
- *
489
- * Manages the lifecycle of installed packages.
490
- * A package (ManifestSchema) is the unit of installation.
491
- * An app (AppSchema) is a UI navigation definition within a package.
492
- * A package may contain 0, 1, or many apps, or be a pure functionality plugin.
493
- *
494
- * Endpoints:
495
- * - GET /packages → list installed packages
496
- * - GET /packages/:id → get package details
497
- * - POST /packages → install a package
498
- * - DELETE /packages/:id → uninstall a package
499
- * - PATCH /packages/:id/enable → enable a package
500
- * - PATCH /packages/:id/disable → disable a package
501
- */
502
- packages = {
503
- /**
504
- * List all installed packages with optional filters.
505
- */
506
- list: async (filters?: { status?: string; type?: string; enabled?: boolean }) => {
507
- const route = this.getRoute('packages');
508
- const params = new URLSearchParams();
509
- if (filters?.status) params.set('status', filters.status);
510
- if (filters?.type) params.set('type', filters.type);
511
- if (filters?.enabled !== undefined) params.set('enabled', String(filters.enabled));
512
- const qs = params.toString();
513
- const url = `${this.baseUrl}${route}${qs ? '?' + qs : ''}`;
514
- const res = await this.fetch(url);
515
- return this.unwrapResponse<{ packages: any[]; total: number }>(res);
516
- },
517
-
518
- /**
519
- * Get a specific installed package by its ID (reverse domain identifier).
520
- */
521
- get: async (id: string) => {
522
- const route = this.getRoute('packages');
523
- const res = await this.fetch(`${this.baseUrl}${route}/${encodeURIComponent(id)}`);
524
- return this.unwrapResponse<{ package: any }>(res);
525
- },
526
-
527
- /**
528
- * Install a new package from its manifest.
529
- */
530
- install: async (manifest: any, options?: { settings?: Record<string, any>; enableOnInstall?: boolean }) => {
531
- const route = this.getRoute('packages');
532
- const res = await this.fetch(`${this.baseUrl}${route}`, {
533
- method: 'POST',
534
- body: JSON.stringify({
535
- manifest,
536
- settings: options?.settings,
537
- enableOnInstall: options?.enableOnInstall,
538
- }),
539
- });
540
- return this.unwrapResponse<{ package: any; message?: string }>(res);
541
- },
542
-
543
- /**
544
- * Uninstall a package by its ID.
545
- */
546
- uninstall: async (id: string) => {
547
- const route = this.getRoute('packages');
548
- const res = await this.fetch(`${this.baseUrl}${route}/${encodeURIComponent(id)}`, {
549
- method: 'DELETE',
550
- });
551
- return this.unwrapResponse<{ id: string; success: boolean; message?: string }>(res);
552
- },
553
-
554
- /**
555
- * Enable a disabled package.
556
- */
557
- enable: async (id: string) => {
558
- const route = this.getRoute('packages');
559
- const res = await this.fetch(`${this.baseUrl}${route}/${encodeURIComponent(id)}/enable`, {
560
- method: 'PATCH',
561
- });
562
- return this.unwrapResponse<{ package: any; message?: string }>(res);
563
- },
564
-
565
- /**
566
- * Disable an installed package.
567
- */
568
- disable: async (id: string) => {
569
- const route = this.getRoute('packages');
570
- const res = await this.fetch(`${this.baseUrl}${route}/${encodeURIComponent(id)}/disable`, {
571
- method: 'PATCH',
572
- });
573
- return this.unwrapResponse<{ package: any; message?: string }>(res);
574
- },
575
- };
576
-
577
- /**
578
- * Authentication Services
579
- */
580
- auth = {
581
- /**
582
- * Login with email and password
583
- * Uses better-auth endpoint: POST /sign-in/email
584
- */
585
- login: async (request: LoginRequest): Promise<SessionResponse> => {
586
- const route = this.getRoute('auth');
587
- const res = await this.fetch(`${this.baseUrl}${route}/sign-in/email`, {
588
- method: 'POST',
589
- body: JSON.stringify(request)
590
- });
591
- const data = await res.json();
592
- // Auto-set token if present in response
593
- if (data.data?.token) {
594
- this.token = data.data.token;
595
- }
596
- return data;
597
- },
598
-
599
- /**
600
- * Logout current user
601
- * Uses better-auth endpoint: POST /sign-out
602
- */
603
- logout: async () => {
604
- const route = this.getRoute('auth');
605
- await this.fetch(`${this.baseUrl}${route}/sign-out`, { method: 'POST' });
606
- this.token = undefined;
607
- },
608
-
609
- /**
610
- * Get current user session
611
- * Uses better-auth endpoint: GET /get-session
612
- */
613
- me: async (): Promise<SessionResponse> => {
614
- const route = this.getRoute('auth');
615
- const res = await this.fetch(`${this.baseUrl}${route}/get-session`);
616
- return res.json();
617
- },
618
-
619
- /**
620
- * Register a new user account
621
- * Uses better-auth endpoint: POST /sign-up/email
622
- */
623
- register: async (request: RegisterRequest): Promise<SessionResponse> => {
624
- const route = this.getRoute('auth');
625
- const res = await this.fetch(`${this.baseUrl}${route}/sign-up/email`, {
626
- method: 'POST',
627
- body: JSON.stringify(request)
628
- });
629
- const data = await res.json();
630
- if (data.data?.token) {
631
- this.token = data.data.token;
632
- }
633
- return data;
634
- },
635
-
636
- /**
637
- * Refresh an authentication token
638
- * Note: better-auth handles token refresh automatically via /get-session
639
- * @param _refreshToken - Not used (better-auth handles refresh automatically)
640
- */
641
- refreshToken: async (_refreshToken: string): Promise<SessionResponse> => {
642
- const route = this.getRoute('auth');
643
- // better-auth doesn't have a separate refresh endpoint
644
- // Session refresh is handled automatically when calling /get-session
645
- const res = await this.fetch(`${this.baseUrl}${route}/get-session`, {
646
- method: 'GET'
647
- });
648
- const data = await res.json();
649
- if (data.data?.token) {
650
- this.token = data.data.token;
651
- }
652
- return data;
653
- }
654
- };
655
-
656
- /**
657
- * Storage Services
658
- */
659
- storage = {
660
- upload: async (file: any, scope: string = 'user'): Promise<FileUploadResponse> => {
661
- // 1. Get Presigned URL
662
- const presignedReq: GetPresignedUrlRequest = {
663
- filename: file.name,
664
- mimeType: file.type,
665
- size: file.size,
666
- scope
667
- };
668
-
669
- const route = this.getRoute('storage');
670
- const presignedRes = await this.fetch(`${this.baseUrl}${route}/upload/presigned`, {
671
- method: 'POST',
672
- body: JSON.stringify(presignedReq)
673
- });
674
- const { data: presigned } = await presignedRes.json() as { data: PresignedUrlResponse['data'] };
675
-
676
- // 2. Upload to Cloud directly (Bypass API Middleware to avoid Auth headers if using S3)
677
- // Use fetchImpl directly
678
- const uploadRes = await this.fetchImpl(presigned.uploadUrl, {
679
- method: presigned.method,
680
- headers: presigned.headers,
681
- body: file
682
- });
683
-
684
- if (!uploadRes.ok) {
685
- throw new Error(`Storage Upload Failed: ${uploadRes.statusText}`);
686
- }
687
-
688
- // 3. Complete Upload
689
- const completeReq: CompleteUploadRequest = {
690
- fileId: presigned.fileId
691
- };
692
- const completeRes = await this.fetch(`${this.baseUrl}${route}/upload/complete`, {
693
- method: 'POST',
694
- body: JSON.stringify(completeReq)
695
- });
696
-
697
- return completeRes.json();
698
- },
699
-
700
- getDownloadUrl: async (fileId: string): Promise<string> => {
701
- const route = this.getRoute('storage');
702
- const res = await this.fetch(`${this.baseUrl}${route}/files/${fileId}/url`);
703
- const data = await res.json();
704
- return data.url;
705
- },
706
-
707
- /**
708
- * Get a presigned URL for direct-to-cloud upload
709
- */
710
- getPresignedUrl: async (req: GetPresignedUrlRequest): Promise<PresignedUrlResponse> => {
711
- const route = this.getRoute('storage');
712
- const res = await this.fetch(`${this.baseUrl}${route}/upload/presigned`, {
713
- method: 'POST',
714
- body: JSON.stringify(req)
715
- });
716
- return res.json();
717
- },
718
-
719
- /**
720
- * Initiate a chunked (multipart) upload session
721
- */
722
- initChunkedUpload: async (req: InitiateChunkedUploadRequest): Promise<InitiateChunkedUploadResponse> => {
723
- const route = this.getRoute('storage');
724
- const res = await this.fetch(`${this.baseUrl}${route}/upload/chunked`, {
725
- method: 'POST',
726
- body: JSON.stringify(req)
727
- });
728
- return res.json();
729
- },
730
-
731
- /**
732
- * Upload a single chunk/part of a multipart upload
733
- */
734
- uploadPart: async (uploadId: string, chunkIndex: number, resumeToken: string, data: Blob | Buffer): Promise<UploadChunkResponse> => {
735
- const route = this.getRoute('storage');
736
- const res = await this.fetch(`${this.baseUrl}${route}/upload/chunked/${uploadId}/chunk/${chunkIndex}`, {
737
- method: 'PUT',
738
- headers: { 'x-resume-token': resumeToken },
739
- body: data as any
740
- });
741
- return res.json();
742
- },
743
-
744
- /**
745
- * Complete a chunked upload by assembling all parts
746
- */
747
- completeChunkedUpload: async (req: CompleteChunkedUploadRequest): Promise<CompleteChunkedUploadResponse> => {
748
- const route = this.getRoute('storage');
749
- const res = await this.fetch(`${this.baseUrl}${route}/upload/chunked/${req.uploadId}/complete`, {
750
- method: 'POST',
751
- body: JSON.stringify(req)
752
- });
753
- return res.json();
754
- },
755
-
756
- /**
757
- * Resume an interrupted chunked upload.
758
- * Fetches current progress, then uploads remaining chunks and completes.
759
- */
760
- resumeUpload: async (uploadId: string, file: Blob | ArrayBuffer, chunkSize: number, resumeToken: string): Promise<CompleteChunkedUploadResponse> => {
761
- const route = this.getRoute('storage');
762
-
763
- // 1. Get current progress
764
- const progressRes = await this.fetch(`${this.baseUrl}${route}/upload/chunked/${uploadId}/progress`);
765
- const progress = await progressRes.json() as UploadProgress;
766
-
767
- const { totalChunks, uploadedChunks } = progress.data;
768
- const parts: Array<{ chunkIndex: number; eTag: string }> = [];
769
-
770
- // 2. Upload remaining chunks
771
- const fileBuffer = file instanceof ArrayBuffer ? file : await file.arrayBuffer();
772
- for (let i = uploadedChunks; i < totalChunks; i++) {
773
- const start = i * chunkSize;
774
- const end = Math.min(start + chunkSize, fileBuffer.byteLength);
775
- const chunk = new Blob([fileBuffer.slice(start, end)]);
776
-
777
- const chunkRes = await this.storage.uploadPart(uploadId, i, resumeToken, chunk);
778
- parts.push({ chunkIndex: i, eTag: chunkRes.data.eTag });
779
- }
780
-
781
- // 3. Complete
782
- return this.storage.completeChunkedUpload({ uploadId, parts });
783
- },
784
- };
785
-
786
- /**
787
- * Automation Services
788
- */
789
- automation = {
790
- /**
791
- * Trigger a named automation flow (legacy endpoint)
792
- */
793
- trigger: async (triggerName: string, payload: any) => {
794
- const route = this.getRoute('automation');
795
- const res = await this.fetch(`${this.baseUrl}${route}/trigger/${triggerName}`, {
796
- method: 'POST',
797
- body: JSON.stringify(payload)
798
- });
799
- return res.json();
800
- },
801
-
802
- /**
803
- * List all registered automation flows
804
- */
805
- list: async (): Promise<{ flows: string[]; total: number; hasMore: boolean }> => {
806
- const route = this.getRoute('automation');
807
- const res = await this.fetch(`${this.baseUrl}${route}`);
808
- return this.unwrapResponse(res);
809
- },
810
-
811
- /**
812
- * Get a flow definition by name
813
- */
814
- get: async (name: string): Promise<any> => {
815
- const route = this.getRoute('automation');
816
- const res = await this.fetch(`${this.baseUrl}${route}/${name}`);
817
- return this.unwrapResponse(res);
818
- },
819
-
820
- /**
821
- * Create (register) a new flow
822
- */
823
- create: async (name: string, definition: any): Promise<any> => {
824
- const route = this.getRoute('automation');
825
- const res = await this.fetch(`${this.baseUrl}${route}`, {
826
- method: 'POST',
827
- body: JSON.stringify({ name, ...definition }),
828
- });
829
- return this.unwrapResponse(res);
830
- },
831
-
832
- /**
833
- * Update an existing flow
834
- */
835
- update: async (name: string, definition: any): Promise<any> => {
836
- const route = this.getRoute('automation');
837
- const res = await this.fetch(`${this.baseUrl}${route}/${name}`, {
838
- method: 'PUT',
839
- body: JSON.stringify({ definition }),
840
- });
841
- return this.unwrapResponse(res);
842
- },
843
-
844
- /**
845
- * Delete (unregister) a flow
846
- */
847
- delete: async (name: string): Promise<{ name: string; deleted: boolean }> => {
848
- const route = this.getRoute('automation');
849
- const res = await this.fetch(`${this.baseUrl}${route}/${name}`, {
850
- method: 'DELETE',
851
- });
852
- return this.unwrapResponse(res);
853
- },
854
-
855
- /**
856
- * Enable or disable a flow
857
- */
858
- toggle: async (name: string, enabled: boolean): Promise<{ name: string; enabled: boolean }> => {
859
- const route = this.getRoute('automation');
860
- const res = await this.fetch(`${this.baseUrl}${route}/${name}/toggle`, {
861
- method: 'POST',
862
- body: JSON.stringify({ enabled }),
863
- });
864
- return this.unwrapResponse(res);
865
- },
866
-
867
- /**
868
- * Execution run history
869
- */
870
- runs: {
871
- /**
872
- * List execution runs for a flow
873
- */
874
- list: async (flowName: string, options?: { limit?: number; cursor?: string }): Promise<{ runs: any[]; hasMore: boolean }> => {
875
- const route = this.getRoute('automation');
876
- const params = new URLSearchParams();
877
- if (options?.limit) params.set('limit', String(options.limit));
878
- if (options?.cursor) params.set('cursor', options.cursor);
879
- const qs = params.toString();
880
- const res = await this.fetch(`${this.baseUrl}${route}/${flowName}/runs${qs ? `?${qs}` : ''}`);
881
- return this.unwrapResponse(res);
882
- },
883
-
884
- /**
885
- * Get a single execution run
886
- */
887
- get: async (flowName: string, runId: string): Promise<any> => {
888
- const route = this.getRoute('automation');
889
- const res = await this.fetch(`${this.baseUrl}${route}/${flowName}/runs/${runId}`);
890
- return this.unwrapResponse(res);
891
- },
892
- },
893
- };
894
-
895
- /**
896
- * Event Subscription API
897
- * Provides real-time event subscriptions for metadata and data changes
898
- */
899
- get events() {
900
- return this.realtimeAPI;
901
- }
902
-
903
- /**
904
- * Permissions Services
905
- */
906
- permissions = {
907
- /**
908
- * Check if current user has permission for an action on an object
909
- */
910
- check: async (request: CheckPermissionRequest): Promise<CheckPermissionResponse> => {
911
- const route = this.getRoute('permissions');
912
- const params = new URLSearchParams({ object: request.object, action: request.action });
913
- if (request.recordId !== undefined) params.set('recordId', request.recordId);
914
- if (request.field !== undefined) params.set('field', request.field);
915
- const res = await this.fetch(`${this.baseUrl}${route}/check?${params.toString()}`);
916
- return this.unwrapResponse<CheckPermissionResponse>(res);
917
- },
918
-
919
- /**
920
- * Get all permissions for a specific object
921
- */
922
- getObjectPermissions: async (object: string): Promise<GetObjectPermissionsResponse> => {
923
- const route = this.getRoute('permissions');
924
- const res = await this.fetch(`${this.baseUrl}${route}/objects/${encodeURIComponent(object)}`);
925
- return this.unwrapResponse<GetObjectPermissionsResponse>(res);
926
- },
927
-
928
- /**
929
- * Get effective permissions for the current user
930
- */
931
- getEffectivePermissions: async (): Promise<GetEffectivePermissionsResponse> => {
932
- const route = this.getRoute('permissions');
933
- const res = await this.fetch(`${this.baseUrl}${route}/effective`);
934
- return this.unwrapResponse<GetEffectivePermissionsResponse>(res);
935
- }
936
- };
937
-
938
- /**
939
- * Realtime Services
940
- */
941
- realtime = {
942
- /**
943
- * Establish a realtime connection
944
- */
945
- connect: async (request?: RealtimeConnectRequest): Promise<RealtimeConnectResponse> => {
946
- const route = this.getRoute('realtime');
947
- const res = await this.fetch(`${this.baseUrl}${route}/connect`, {
948
- method: 'POST',
949
- body: JSON.stringify(request || {})
950
- });
951
- return this.unwrapResponse<RealtimeConnectResponse>(res);
952
- },
953
-
954
- /**
955
- * Disconnect from realtime services
956
- */
957
- disconnect: async (): Promise<void> => {
958
- const route = this.getRoute('realtime');
959
- await this.fetch(`${this.baseUrl}${route}/disconnect`, {
960
- method: 'POST'
961
- });
962
- },
963
-
964
- /**
965
- * Subscribe to a channel
966
- */
967
- subscribe: async (request: RealtimeSubscribeRequest): Promise<RealtimeSubscribeResponse> => {
968
- const route = this.getRoute('realtime');
969
- const res = await this.fetch(`${this.baseUrl}${route}/subscribe`, {
970
- method: 'POST',
971
- body: JSON.stringify(request)
972
- });
973
- return this.unwrapResponse<RealtimeSubscribeResponse>(res);
974
- },
975
-
976
- /**
977
- * Unsubscribe from a channel
978
- */
979
- unsubscribe: async (subscriptionId: string): Promise<void> => {
980
- const route = this.getRoute('realtime');
981
- await this.fetch(`${this.baseUrl}${route}/unsubscribe`, {
982
- method: 'POST',
983
- body: JSON.stringify({ subscriptionId })
984
- });
985
- },
986
-
987
- /**
988
- * Set presence state on a channel
989
- */
990
- setPresence: async (channel: string, state: SetPresenceRequest['state']): Promise<void> => {
991
- const route = this.getRoute('realtime');
992
- await this.fetch(`${this.baseUrl}${route}/presence`, {
993
- method: 'PUT',
994
- body: JSON.stringify({ channel, state })
995
- });
996
- },
997
-
998
- /**
999
- * Get presence information for a channel
1000
- */
1001
- getPresence: async (channel: string): Promise<GetPresenceResponse> => {
1002
- const route = this.getRoute('realtime');
1003
- const res = await this.fetch(`${this.baseUrl}${route}/presence/${encodeURIComponent(channel)}`);
1004
- return this.unwrapResponse<GetPresenceResponse>(res);
1005
- }
1006
- };
1007
-
1008
- /**
1009
- * Workflow Services
1010
- */
1011
- workflow = {
1012
- /**
1013
- * Get workflow configuration for an object
1014
- */
1015
- getConfig: async (object: string): Promise<GetWorkflowConfigResponse> => {
1016
- const route = this.getRoute('workflow');
1017
- const res = await this.fetch(`${this.baseUrl}${route}/${encodeURIComponent(object)}/config`);
1018
- return this.unwrapResponse<GetWorkflowConfigResponse>(res);
1019
- },
1020
-
1021
- /**
1022
- * Get current workflow state for a record
1023
- */
1024
- getState: async (object: string, recordId: string): Promise<GetWorkflowStateResponse> => {
1025
- const route = this.getRoute('workflow');
1026
- const res = await this.fetch(`${this.baseUrl}${route}/${encodeURIComponent(object)}/${encodeURIComponent(recordId)}/state`);
1027
- return this.unwrapResponse<GetWorkflowStateResponse>(res);
1028
- },
1029
-
1030
- /**
1031
- * Execute a workflow state transition
1032
- */
1033
- transition: async (request: WorkflowTransitionRequest): Promise<WorkflowTransitionResponse> => {
1034
- const route = this.getRoute('workflow');
1035
- const res = await this.fetch(`${this.baseUrl}${route}/${encodeURIComponent(request.object)}/${encodeURIComponent(request.recordId)}/transition`, {
1036
- method: 'POST',
1037
- body: JSON.stringify({
1038
- transition: request.transition,
1039
- comment: request.comment,
1040
- data: request.data
1041
- })
1042
- });
1043
- return this.unwrapResponse<WorkflowTransitionResponse>(res);
1044
- },
1045
-
1046
- /**
1047
- * Approve a workflow step
1048
- */
1049
- approve: async (request: WorkflowApproveRequest): Promise<WorkflowApproveResponse> => {
1050
- const route = this.getRoute('workflow');
1051
- const res = await this.fetch(`${this.baseUrl}${route}/${encodeURIComponent(request.object)}/${encodeURIComponent(request.recordId)}/approve`, {
1052
- method: 'POST',
1053
- body: JSON.stringify({
1054
- comment: request.comment,
1055
- data: request.data
1056
- })
1057
- });
1058
- return this.unwrapResponse<WorkflowApproveResponse>(res);
1059
- },
1060
-
1061
- /**
1062
- * Reject a workflow step
1063
- */
1064
- reject: async (request: WorkflowRejectRequest): Promise<WorkflowRejectResponse> => {
1065
- const route = this.getRoute('workflow');
1066
- const res = await this.fetch(`${this.baseUrl}${route}/${encodeURIComponent(request.object)}/${encodeURIComponent(request.recordId)}/reject`, {
1067
- method: 'POST',
1068
- body: JSON.stringify({
1069
- reason: request.reason,
1070
- comment: request.comment
1071
- })
1072
- });
1073
- return this.unwrapResponse<WorkflowRejectResponse>(res);
1074
- }
1075
- };
1076
-
1077
- /**
1078
- * Views CRUD Services
1079
- */
1080
- views = {
1081
- /**
1082
- * List views for an object
1083
- */
1084
- list: async (object: string, type?: 'list' | 'form'): Promise<ListViewsResponse> => {
1085
- const route = this.getRoute('views');
1086
- const params = new URLSearchParams();
1087
- if (type) params.set('type', type);
1088
- const qs = params.toString();
1089
- const res = await this.fetch(`${this.baseUrl}${route}/${encodeURIComponent(object)}${qs ? `?${qs}` : ''}`);
1090
- return this.unwrapResponse<ListViewsResponse>(res);
1091
- },
1092
-
1093
- /**
1094
- * Get a specific view
1095
- */
1096
- get: async (object: string, viewId: string): Promise<GetViewResponse> => {
1097
- const route = this.getRoute('views');
1098
- const res = await this.fetch(`${this.baseUrl}${route}/${encodeURIComponent(object)}/${encodeURIComponent(viewId)}`);
1099
- return this.unwrapResponse<GetViewResponse>(res);
1100
- },
1101
-
1102
- /**
1103
- * Create a new view
1104
- */
1105
- create: async (object: string, data: CreateViewRequest['data']): Promise<CreateViewResponse> => {
1106
- const route = this.getRoute('views');
1107
- const res = await this.fetch(`${this.baseUrl}${route}/${encodeURIComponent(object)}`, {
1108
- method: 'POST',
1109
- body: JSON.stringify({ object, data })
1110
- });
1111
- return this.unwrapResponse<CreateViewResponse>(res);
1112
- },
1113
-
1114
- /**
1115
- * Update an existing view
1116
- */
1117
- update: async (object: string, viewId: string, data: UpdateViewRequest['data']): Promise<UpdateViewResponse> => {
1118
- const route = this.getRoute('views');
1119
- const res = await this.fetch(`${this.baseUrl}${route}/${encodeURIComponent(object)}/${encodeURIComponent(viewId)}`, {
1120
- method: 'PUT',
1121
- body: JSON.stringify({ object, viewId, data })
1122
- });
1123
- return this.unwrapResponse<UpdateViewResponse>(res);
1124
- },
1125
-
1126
- /**
1127
- * Delete a view
1128
- */
1129
- delete: async (object: string, viewId: string): Promise<DeleteViewResponse> => {
1130
- const route = this.getRoute('views');
1131
- const res = await this.fetch(`${this.baseUrl}${route}/${encodeURIComponent(object)}/${encodeURIComponent(viewId)}`, {
1132
- method: 'DELETE'
1133
- });
1134
- return this.unwrapResponse<DeleteViewResponse>(res);
1135
- }
1136
- };
1137
-
1138
- /**
1139
- * Notification Services
1140
- */
1141
- notifications = {
1142
- /**
1143
- * Register a device for push notifications
1144
- */
1145
- registerDevice: async (request: RegisterDeviceRequest): Promise<RegisterDeviceResponse> => {
1146
- const route = this.getRoute('notifications');
1147
- const res = await this.fetch(`${this.baseUrl}${route}/devices`, {
1148
- method: 'POST',
1149
- body: JSON.stringify(request)
1150
- });
1151
- return this.unwrapResponse<RegisterDeviceResponse>(res);
1152
- },
1153
-
1154
- /**
1155
- * Unregister a device from push notifications
1156
- */
1157
- unregisterDevice: async (deviceId: string): Promise<UnregisterDeviceResponse> => {
1158
- const route = this.getRoute('notifications');
1159
- const res = await this.fetch(`${this.baseUrl}${route}/devices/${encodeURIComponent(deviceId)}`, {
1160
- method: 'DELETE'
1161
- });
1162
- return this.unwrapResponse<UnregisterDeviceResponse>(res);
1163
- },
1164
-
1165
- /**
1166
- * Get notification preferences for the current user
1167
- */
1168
- getPreferences: async (): Promise<GetNotificationPreferencesResponse> => {
1169
- const route = this.getRoute('notifications');
1170
- const res = await this.fetch(`${this.baseUrl}${route}/preferences`);
1171
- return this.unwrapResponse<GetNotificationPreferencesResponse>(res);
1172
- },
1173
-
1174
- /**
1175
- * Update notification preferences
1176
- */
1177
- updatePreferences: async (preferences: UpdateNotificationPreferencesRequest['preferences']): Promise<UpdateNotificationPreferencesResponse> => {
1178
- const route = this.getRoute('notifications');
1179
- const res = await this.fetch(`${this.baseUrl}${route}/preferences`, {
1180
- method: 'PUT',
1181
- body: JSON.stringify({ preferences })
1182
- });
1183
- return this.unwrapResponse<UpdateNotificationPreferencesResponse>(res);
1184
- },
1185
-
1186
- /**
1187
- * List notifications for the current user
1188
- */
1189
- list: async (options?: { read?: boolean; type?: string; limit?: number; cursor?: string }): Promise<ListNotificationsResponse> => {
1190
- const route = this.getRoute('notifications');
1191
- const params = new URLSearchParams();
1192
- if (options?.read !== undefined) params.set('read', String(options.read));
1193
- if (options?.type) params.set('type', options.type);
1194
- if (options?.limit) params.set('limit', String(options.limit));
1195
- if (options?.cursor) params.set('cursor', options.cursor);
1196
- const qs = params.toString();
1197
- const res = await this.fetch(`${this.baseUrl}${route}${qs ? `?${qs}` : ''}`);
1198
- return this.unwrapResponse<ListNotificationsResponse>(res);
1199
- },
1200
-
1201
- /**
1202
- * Mark specific notifications as read
1203
- */
1204
- markRead: async (ids: string[]): Promise<MarkNotificationsReadResponse> => {
1205
- const route = this.getRoute('notifications');
1206
- const res = await this.fetch(`${this.baseUrl}${route}/read`, {
1207
- method: 'POST',
1208
- body: JSON.stringify({ ids })
1209
- });
1210
- return this.unwrapResponse<MarkNotificationsReadResponse>(res);
1211
- },
1212
-
1213
- /**
1214
- * Mark all notifications as read
1215
- */
1216
- markAllRead: async (): Promise<MarkAllNotificationsReadResponse> => {
1217
- const route = this.getRoute('notifications');
1218
- const res = await this.fetch(`${this.baseUrl}${route}/read/all`, {
1219
- method: 'POST'
1220
- });
1221
- return this.unwrapResponse<MarkAllNotificationsReadResponse>(res);
1222
- }
1223
- };
1224
-
1225
- /**
1226
- * AI Services
1227
- */
1228
- ai = {
1229
- /**
1230
- * Natural language query — converts natural language to structured query
1231
- */
1232
- nlq: async (request: AiNlqRequest): Promise<AiNlqResponse> => {
1233
- const route = this.getRoute('ai');
1234
- const res = await this.fetch(`${this.baseUrl}${route}/nlq`, {
1235
- method: 'POST',
1236
- body: JSON.stringify(request)
1237
- });
1238
- return this.unwrapResponse<AiNlqResponse>(res);
1239
- },
1240
-
1241
- // AI chat method removed — use Vercel AI SDK `useChat()` / `@ai-sdk/react` directly.
1242
-
1243
- /**
1244
- * AI-powered field value suggestions
1245
- */
1246
- suggest: async (request: AiSuggestRequest): Promise<AiSuggestResponse> => {
1247
- const route = this.getRoute('ai');
1248
- const res = await this.fetch(`${this.baseUrl}${route}/suggest`, {
1249
- method: 'POST',
1250
- body: JSON.stringify(request)
1251
- });
1252
- return this.unwrapResponse<AiSuggestResponse>(res);
1253
- },
1254
-
1255
- /**
1256
- * AI-powered data insights
1257
- */
1258
- insights: async (request: AiInsightsRequest): Promise<AiInsightsResponse> => {
1259
- const route = this.getRoute('ai');
1260
- const res = await this.fetch(`${this.baseUrl}${route}/insights`, {
1261
- method: 'POST',
1262
- body: JSON.stringify(request)
1263
- });
1264
- return this.unwrapResponse<AiInsightsResponse>(res);
1265
- }
1266
- };
1267
-
1268
- /**
1269
- * Internationalization Services
1270
- */
1271
- i18n = {
1272
- /**
1273
- * Get available locales
1274
- */
1275
- getLocales: async (): Promise<GetLocalesResponse> => {
1276
- const route = this.getRoute('i18n');
1277
- const res = await this.fetch(`${this.baseUrl}${route}/locales`);
1278
- return this.unwrapResponse<GetLocalesResponse>(res);
1279
- },
1280
-
1281
- /**
1282
- * Get translations for a locale
1283
- */
1284
- getTranslations: async (locale: string, options?: { namespace?: string; keys?: string[] }): Promise<GetTranslationsResponse> => {
1285
- const route = this.getRoute('i18n');
1286
- const params = new URLSearchParams();
1287
- params.set('locale', locale);
1288
- if (options?.namespace) params.set('namespace', options.namespace);
1289
- if (options?.keys) params.set('keys', options.keys.join(','));
1290
- const res = await this.fetch(`${this.baseUrl}${route}/translations?${params.toString()}`);
1291
- return this.unwrapResponse<GetTranslationsResponse>(res);
1292
- },
1293
-
1294
- /**
1295
- * Get translated field labels for an object
1296
- */
1297
- getFieldLabels: async (object: string, locale: string): Promise<GetFieldLabelsResponse> => {
1298
- const route = this.getRoute('i18n');
1299
- const res = await this.fetch(`${this.baseUrl}${route}/labels/${encodeURIComponent(object)}?locale=${encodeURIComponent(locale)}`);
1300
- return this.unwrapResponse<GetFieldLabelsResponse>(res);
1301
- }
1302
- };
1303
-
1304
- /**
1305
- * Feed / Chatter Services
1306
- *
1307
- * Provides access to the activity timeline (comments, field changes, tasks),
1308
- * emoji reactions, pin/star, search, changelog, and record subscriptions.
1309
- * Base path: /api/data/{object}/{recordId}/feed
1310
- */
1311
- feed = {
1312
- /**
1313
- * List feed items for a record
1314
- */
1315
- list: async (object: string, recordId: string, options?: { type?: string; limit?: number; cursor?: string }): Promise<GetFeedResponse> => {
1316
- const route = this.getRoute('data');
1317
- const params = new URLSearchParams();
1318
- if (options?.type) params.set('type', options.type);
1319
- if (options?.limit) params.set('limit', String(options.limit));
1320
- if (options?.cursor) params.set('cursor', options.cursor);
1321
- const qs = params.toString();
1322
- const res = await this.fetch(`${this.baseUrl}${route}/${encodeURIComponent(object)}/${encodeURIComponent(recordId)}/feed${qs ? `?${qs}` : ''}`);
1323
- return this.unwrapResponse<GetFeedResponse>(res);
1324
- },
1325
-
1326
- /**
1327
- * Create a new feed item (comment, note, task, etc.)
1328
- */
1329
- create: async (object: string, recordId: string, data: { type: string; body?: string; mentions?: any[]; parentId?: string; visibility?: string }): Promise<CreateFeedItemResponse> => {
1330
- const route = this.getRoute('data');
1331
- const res = await this.fetch(`${this.baseUrl}${route}/${encodeURIComponent(object)}/${encodeURIComponent(recordId)}/feed`, {
1332
- method: 'POST',
1333
- body: JSON.stringify(data)
1334
- });
1335
- return this.unwrapResponse<CreateFeedItemResponse>(res);
1336
- },
1337
-
1338
- /**
1339
- * Update an existing feed item
1340
- */
1341
- update: async (object: string, recordId: string, feedId: string, data: { body?: string; mentions?: any[]; visibility?: string }): Promise<UpdateFeedItemResponse> => {
1342
- const route = this.getRoute('data');
1343
- const res = await this.fetch(`${this.baseUrl}${route}/${encodeURIComponent(object)}/${encodeURIComponent(recordId)}/feed/${encodeURIComponent(feedId)}`, {
1344
- method: 'PUT',
1345
- body: JSON.stringify(data)
1346
- });
1347
- return this.unwrapResponse<UpdateFeedItemResponse>(res);
1348
- },
1349
-
1350
- /**
1351
- * Delete a feed item
1352
- */
1353
- delete: async (object: string, recordId: string, feedId: string): Promise<DeleteFeedItemResponse> => {
1354
- const route = this.getRoute('data');
1355
- const res = await this.fetch(`${this.baseUrl}${route}/${encodeURIComponent(object)}/${encodeURIComponent(recordId)}/feed/${encodeURIComponent(feedId)}`, {
1356
- method: 'DELETE'
1357
- });
1358
- return this.unwrapResponse<DeleteFeedItemResponse>(res);
1359
- },
1360
-
1361
- /**
1362
- * Add an emoji reaction to a feed item
1363
- */
1364
- addReaction: async (object: string, recordId: string, feedId: string, emoji: string): Promise<AddReactionResponse> => {
1365
- const route = this.getRoute('data');
1366
- const res = await this.fetch(`${this.baseUrl}${route}/${encodeURIComponent(object)}/${encodeURIComponent(recordId)}/feed/${encodeURIComponent(feedId)}/reactions`, {
1367
- method: 'POST',
1368
- body: JSON.stringify({ emoji })
1369
- });
1370
- return this.unwrapResponse<AddReactionResponse>(res);
1371
- },
1372
-
1373
- /**
1374
- * Remove an emoji reaction from a feed item
1375
- */
1376
- removeReaction: async (object: string, recordId: string, feedId: string, emoji: string): Promise<RemoveReactionResponse> => {
1377
- const route = this.getRoute('data');
1378
- const res = await this.fetch(`${this.baseUrl}${route}/${encodeURIComponent(object)}/${encodeURIComponent(recordId)}/feed/${encodeURIComponent(feedId)}/reactions/${encodeURIComponent(emoji)}`, {
1379
- method: 'DELETE'
1380
- });
1381
- return this.unwrapResponse<RemoveReactionResponse>(res);
1382
- },
1383
-
1384
- /**
1385
- * Pin a feed item to the top of the timeline
1386
- */
1387
- pin: async (object: string, recordId: string, feedId: string): Promise<PinFeedItemResponse> => {
1388
- const route = this.getRoute('data');
1389
- const res = await this.fetch(`${this.baseUrl}${route}/${encodeURIComponent(object)}/${encodeURIComponent(recordId)}/feed/${encodeURIComponent(feedId)}/pin`, {
1390
- method: 'POST'
1391
- });
1392
- return this.unwrapResponse<PinFeedItemResponse>(res);
1393
- },
1394
-
1395
- /**
1396
- * Unpin a feed item
1397
- */
1398
- unpin: async (object: string, recordId: string, feedId: string): Promise<UnpinFeedItemResponse> => {
1399
- const route = this.getRoute('data');
1400
- const res = await this.fetch(`${this.baseUrl}${route}/${encodeURIComponent(object)}/${encodeURIComponent(recordId)}/feed/${encodeURIComponent(feedId)}/pin`, {
1401
- method: 'DELETE'
1402
- });
1403
- return this.unwrapResponse<UnpinFeedItemResponse>(res);
1404
- },
1405
-
1406
- /**
1407
- * Star (bookmark) a feed item
1408
- */
1409
- star: async (object: string, recordId: string, feedId: string): Promise<StarFeedItemResponse> => {
1410
- const route = this.getRoute('data');
1411
- const res = await this.fetch(`${this.baseUrl}${route}/${encodeURIComponent(object)}/${encodeURIComponent(recordId)}/feed/${encodeURIComponent(feedId)}/star`, {
1412
- method: 'POST'
1413
- });
1414
- return this.unwrapResponse<StarFeedItemResponse>(res);
1415
- },
1416
-
1417
- /**
1418
- * Unstar a feed item
1419
- */
1420
- unstar: async (object: string, recordId: string, feedId: string): Promise<UnstarFeedItemResponse> => {
1421
- const route = this.getRoute('data');
1422
- const res = await this.fetch(`${this.baseUrl}${route}/${encodeURIComponent(object)}/${encodeURIComponent(recordId)}/feed/${encodeURIComponent(feedId)}/star`, {
1423
- method: 'DELETE'
1424
- });
1425
- return this.unwrapResponse<UnstarFeedItemResponse>(res);
1426
- },
1427
-
1428
- /**
1429
- * Search feed items
1430
- */
1431
- search: async (object: string, recordId: string, query: string, options?: { type?: string; actorId?: string; dateFrom?: string; dateTo?: string; limit?: number; cursor?: string }): Promise<SearchFeedResponse> => {
1432
- const route = this.getRoute('data');
1433
- const params = new URLSearchParams();
1434
- params.set('query', query);
1435
- if (options?.type) params.set('type', options.type);
1436
- if (options?.actorId) params.set('actorId', options.actorId);
1437
- if (options?.dateFrom) params.set('dateFrom', options.dateFrom);
1438
- if (options?.dateTo) params.set('dateTo', options.dateTo);
1439
- if (options?.limit) params.set('limit', String(options.limit));
1440
- if (options?.cursor) params.set('cursor', options.cursor);
1441
- const res = await this.fetch(`${this.baseUrl}${route}/${encodeURIComponent(object)}/${encodeURIComponent(recordId)}/feed/search?${params.toString()}`);
1442
- return this.unwrapResponse<SearchFeedResponse>(res);
1443
- },
1444
-
1445
- /**
1446
- * Get field-level changelog for a record
1447
- */
1448
- getChangelog: async (object: string, recordId: string, options?: { field?: string; actorId?: string; dateFrom?: string; dateTo?: string; limit?: number; cursor?: string }): Promise<GetChangelogResponse> => {
1449
- const route = this.getRoute('data');
1450
- const params = new URLSearchParams();
1451
- if (options?.field) params.set('field', options.field);
1452
- if (options?.actorId) params.set('actorId', options.actorId);
1453
- if (options?.dateFrom) params.set('dateFrom', options.dateFrom);
1454
- if (options?.dateTo) params.set('dateTo', options.dateTo);
1455
- if (options?.limit) params.set('limit', String(options.limit));
1456
- if (options?.cursor) params.set('cursor', options.cursor);
1457
- const qs = params.toString();
1458
- const res = await this.fetch(`${this.baseUrl}${route}/${encodeURIComponent(object)}/${encodeURIComponent(recordId)}/changelog${qs ? `?${qs}` : ''}`);
1459
- return this.unwrapResponse<GetChangelogResponse>(res);
1460
- },
1461
-
1462
- /**
1463
- * Subscribe to record notifications
1464
- */
1465
- subscribe: async (object: string, recordId: string, options?: { events?: string[]; channels?: string[] }): Promise<SubscribeResponse> => {
1466
- const route = this.getRoute('data');
1467
- const res = await this.fetch(`${this.baseUrl}${route}/${encodeURIComponent(object)}/${encodeURIComponent(recordId)}/subscribe`, {
1468
- method: 'POST',
1469
- body: JSON.stringify(options || {})
1470
- });
1471
- return this.unwrapResponse<SubscribeResponse>(res);
1472
- },
1473
-
1474
- /**
1475
- * Unsubscribe from record notifications
1476
- */
1477
- unsubscribe: async (object: string, recordId: string): Promise<UnsubscribeResponse> => {
1478
- const route = this.getRoute('data');
1479
- const res = await this.fetch(`${this.baseUrl}${route}/${encodeURIComponent(object)}/${encodeURIComponent(recordId)}/subscribe`, {
1480
- method: 'DELETE'
1481
- });
1482
- return this.unwrapResponse<UnsubscribeResponse>(res);
1483
- },
1484
- };
1485
-
1486
- /**
1487
- * Data Operations
1488
- */
1489
- data = {
1490
- /**
1491
- * Advanced Query using ObjectStack Query Protocol
1492
- * Supports both simplified options and full AST
1493
- */
1494
- query: async <T = any>(object: string, query: Partial<QueryAST>): Promise<PaginatedResult<T>> => {
1495
- const route = this.getRoute('data');
1496
- // POST for complex query to avoid URL length limits and allow clean JSON AST
1497
- // Convention: POST /api/v1/data/:object/query
1498
- const res = await this.fetch(`${this.baseUrl}${route}/${object}/query`, {
1499
- method: 'POST',
1500
- body: JSON.stringify(query)
1501
- });
1502
- return this.unwrapResponse<PaginatedResult<T>>(res);
1503
- },
1504
-
1505
- /**
1506
- * @deprecated Use `data.query()` with standard QueryAST parameters instead.
1507
- * This method uses legacy parameter names. Internally adapts to HTTP GET params.
1508
- */
1509
- find: async <T = any>(object: string, options: QueryOptions | QueryOptionsV2 = {}): Promise<PaginatedResult<T>> => {
1510
- const route = this.getRoute('data');
1511
- const queryParams = new URLSearchParams();
1512
-
1513
- // ── Normalize V2 canonical options → HTTP transport params ───
1514
- // Detect V2 options by presence of canonical-only keys.
1515
- const v2 = options as QueryOptionsV2;
1516
- const normalizedOptions: QueryOptions = {} as QueryOptions;
1517
- if ('where' in options || 'fields' in options || 'orderBy' in options || 'offset' in options) {
1518
- // V2 canonical options detected — map to legacy HTTP transport keys
1519
- if (v2.where) normalizedOptions.filter = v2.where as any;
1520
- if (v2.fields) normalizedOptions.select = v2.fields;
1521
- if (v2.orderBy) normalizedOptions.sort = v2.orderBy as any;
1522
- if (v2.limit != null) normalizedOptions.top = v2.limit;
1523
- if (v2.offset != null) normalizedOptions.skip = v2.offset;
1524
- if (v2.aggregations) normalizedOptions.aggregations = v2.aggregations;
1525
- if (v2.groupBy) normalizedOptions.groupBy = v2.groupBy;
1526
- } else {
1527
- // Legacy QueryOptions — pass through as-is
1528
- Object.assign(normalizedOptions, options);
1529
- }
1530
-
1531
- // 1. Handle Pagination
1532
- if (normalizedOptions.top) queryParams.set('top', normalizedOptions.top.toString());
1533
- if (normalizedOptions.skip) queryParams.set('skip', normalizedOptions.skip.toString());
1534
-
1535
- // 2. Handle Sort
1536
- if (normalizedOptions.sort) {
1537
- // Check if it's AST
1538
- if (Array.isArray(normalizedOptions.sort) && typeof normalizedOptions.sort[0] === 'object') {
1539
- queryParams.set('sort', JSON.stringify(normalizedOptions.sort));
1540
- } else {
1541
- const sortVal = Array.isArray(normalizedOptions.sort) ? normalizedOptions.sort.join(',') : normalizedOptions.sort;
1542
- queryParams.set('sort', sortVal as string);
1543
- }
1544
- }
1545
-
1546
- // 3. Handle Select
1547
- if (normalizedOptions.select) {
1548
- queryParams.set('select', normalizedOptions.select.join(','));
1549
- }
1550
-
1551
- // 4. Handle Filters (Simple vs AST)
1552
- // Canonical HTTP param name: `filter` (singular). `filters` (plural) is accepted
1553
- // for backward compatibility but `filter` is the standard going forward.
1554
- const filterValue = normalizedOptions.filter ?? normalizedOptions.filters;
1555
- if (filterValue) {
1556
- // Detect AST filter format vs simple key-value map. AST filters use an array structure
1557
- // with [field, operator, value] or [logicOp, ...nodes] shape (see isFilterAST from spec).
1558
- // For complex filter expressions, use .query() which builds a proper QueryAST.
1559
- if (this.isFilterAST(filterValue) || Array.isArray(filterValue)) {
1560
- // AST or any array → serialize as JSON in `filter` param
1561
- queryParams.set('filter', JSON.stringify(filterValue));
1562
- } else if (typeof filterValue === 'object' && filterValue !== null) {
1563
- // Plain key-value map → append each as individual query params
1564
- Object.entries(filterValue as Record<string, unknown>).forEach(([k, v]) => {
1565
- if (v !== undefined && v !== null) {
1566
- queryParams.append(k, String(v));
1567
- }
1568
- });
1569
- }
1570
- }
1571
-
1572
- // 5. Handle Aggregations & GroupBy (Pass through as JSON if present)
1573
- if (normalizedOptions.aggregations) {
1574
- queryParams.set('aggregations', JSON.stringify(normalizedOptions.aggregations));
1575
- }
1576
- if (normalizedOptions.groupBy) {
1577
- queryParams.set('groupBy', normalizedOptions.groupBy.join(','));
1578
- }
1579
-
1580
- const res = await this.fetch(`${this.baseUrl}${route}/${object}?${queryParams.toString()}`);
1581
- return this.unwrapResponse<PaginatedResult<T>>(res);
1582
- },
1583
-
1584
- get: async <T = any>(object: string, id: string): Promise<GetDataResult<T>> => {
1585
- const route = this.getRoute('data');
1586
- const res = await this.fetch(`${this.baseUrl}${route}/${object}/${id}`);
1587
- return this.unwrapResponse<GetDataResult<T>>(res);
1588
- },
1589
-
1590
- create: async <T = any>(object: string, data: Partial<T>): Promise<CreateDataResult<T>> => {
1591
- const route = this.getRoute('data');
1592
- const res = await this.fetch(`${this.baseUrl}${route}/${object}`, {
1593
- method: 'POST',
1594
- body: JSON.stringify(data)
1595
- });
1596
- return this.unwrapResponse<CreateDataResult<T>>(res);
1597
- },
1598
-
1599
- createMany: async <T = any>(object: string, data: Partial<T>[]): Promise<T[]> => {
1600
- const route = this.getRoute('data');
1601
- const res = await this.fetch(`${this.baseUrl}${route}/${object}/createMany`, {
1602
- method: 'POST',
1603
- body: JSON.stringify(data)
1604
- });
1605
- return this.unwrapResponse<T[]>(res);
1606
- },
1607
-
1608
- update: async <T = any>(object: string, id: string, data: Partial<T>): Promise<UpdateDataResult<T>> => {
1609
- const route = this.getRoute('data');
1610
- const res = await this.fetch(`${this.baseUrl}${route}/${object}/${id}`, {
1611
- method: 'PATCH',
1612
- body: JSON.stringify(data)
1613
- });
1614
- return this.unwrapResponse<UpdateDataResult<T>>(res);
1615
- },
1616
-
1617
- /**
1618
- * Batch update multiple records
1619
- * Uses the new BatchUpdateRequest schema with full control over options
1620
- */
1621
- batch: async (object: string, request: BatchUpdateRequest): Promise<BatchUpdateResponse> => {
1622
- const route = this.getRoute('data');
1623
- const res = await this.fetch(`${this.baseUrl}${route}/${object}/batch`, {
1624
- method: 'POST',
1625
- body: JSON.stringify(request)
1626
- });
1627
- return this.unwrapResponse<BatchUpdateResponse>(res);
1628
- },
1629
-
1630
- /**
1631
- * Update multiple records (simplified batch update)
1632
- * Convenience method for batch updates without full BatchUpdateRequest
1633
- */
1634
- updateMany: async <T = any>(
1635
- object: string,
1636
- records: Array<{ id: string; data: Partial<T> }>,
1637
- options?: BatchOptions
1638
- ): Promise<BatchUpdateResponse> => {
1639
- const route = this.getRoute('data');
1640
- const request: UpdateManyRequest = {
1641
- records,
1642
- options
1643
- };
1644
- const res = await this.fetch(`${this.baseUrl}${route}/${object}/updateMany`, {
1645
- method: 'POST',
1646
- body: JSON.stringify(request)
1647
- });
1648
- return this.unwrapResponse<BatchUpdateResponse>(res);
1649
- },
1650
-
1651
- delete: async (object: string, id: string): Promise<DeleteDataResult> => {
1652
- const route = this.getRoute('data');
1653
- const res = await this.fetch(`${this.baseUrl}${route}/${object}/${id}`, {
1654
- method: 'DELETE'
1655
- });
1656
- return this.unwrapResponse<DeleteDataResult>(res);
1657
- },
1658
-
1659
- /**
1660
- * Delete multiple records by IDs
1661
- */
1662
- deleteMany: async(object: string, ids: string[], options?: BatchOptions): Promise<BatchUpdateResponse> => {
1663
- const route = this.getRoute('data');
1664
- const request: DeleteManyRequest = {
1665
- ids,
1666
- options
1667
- };
1668
- const res = await this.fetch(`${this.baseUrl}${route}/${object}/deleteMany`, {
1669
- method: 'POST',
1670
- body: JSON.stringify(request)
1671
- });
1672
- return this.unwrapResponse<BatchUpdateResponse>(res);
1673
- }
1674
- };
1675
-
1676
-
1677
-
1678
- /**
1679
- * Private Helpers
1680
- */
1681
-
1682
- private isFilterAST(filter: any): boolean {
1683
- // Delegate to the spec-exported structural validator instead of naive Array.isArray.
1684
- // This checks for valid AST shapes: [field, op, val], [logic, ...nodes], or [[cond], ...].
1685
- return isFilterAST(filter);
1686
- }
1687
-
1688
- /**
1689
- * Unwrap the standard REST API response envelope.
1690
- * The HTTP layer wraps responses as `{ success: boolean, data: T, meta? }`
1691
- * (see BaseResponseSchema in contract.zod.ts).
1692
- * This method strips the envelope and returns the inner `data` payload
1693
- * so callers receive the spec-level type (e.g. GetMetaTypesResponse).
1694
- */
1695
- private async unwrapResponse<T>(res: Response): Promise<T> {
1696
- const body = await res.json();
1697
- // If the body has a `success` flag it's a BaseResponse envelope
1698
- if (body && typeof body.success === 'boolean' && 'data' in body) {
1699
- return body.data as T;
1700
- }
1701
- // Already unwrapped or non-standard
1702
- return body as T;
1703
- }
1704
-
1705
- private async fetch(url: string, options: RequestInit = {}): Promise<Response> {
1706
- this.logger.debug('HTTP request', {
1707
- method: options.method || 'GET',
1708
- url,
1709
- hasBody: !!options.body
1710
- });
1711
-
1712
- const headers: Record<string, string> = {
1713
- 'Content-Type': 'application/json',
1714
- ...(options.headers as Record<string, string> || {}),
1715
- };
1716
-
1717
- if (this.token) {
1718
- headers['Authorization'] = `Bearer ${this.token}`;
1719
- }
1720
-
1721
- const res = await this.fetchImpl(url, { ...options, headers });
1722
-
1723
- this.logger.debug('HTTP response', {
1724
- method: options.method || 'GET',
1725
- url,
1726
- status: res.status,
1727
- ok: res.ok
1728
- });
1729
-
1730
- if (!res.ok) {
1731
- let errorBody: any;
1732
- try {
1733
- errorBody = await res.json();
1734
- } catch {
1735
- errorBody = { message: res.statusText };
1736
- }
1737
-
1738
- this.logger.error('HTTP request failed', undefined, {
1739
- method: options.method || 'GET',
1740
- url,
1741
- status: res.status,
1742
- error: errorBody
1743
- });
1744
-
1745
- // Create a standardized error if the response includes error details
1746
- const errorMessage = errorBody?.message || errorBody?.error?.message || res.statusText;
1747
- const errorCode = errorBody?.code || errorBody?.error?.code;
1748
- const error = new Error(`[ObjectStack] ${errorCode ? `${errorCode}: ` : ''}${errorMessage}`) as any;
1749
-
1750
- // Attach error details for programmatic access
1751
- error.code = errorCode;
1752
- error.category = errorBody?.category;
1753
- error.httpStatus = res.status;
1754
- error.retryable = errorBody?.retryable;
1755
- error.details = errorBody?.details || errorBody;
1756
-
1757
- throw error;
1758
- }
1759
-
1760
- return res;
1761
- }
1762
-
1763
- /**
1764
- * Get the conventional route path for a given API endpoint type
1765
- * ObjectStack uses standard conventions: /api/v1/data, /api/v1/meta, /api/v1/ui
1766
- */
1767
- private getRoute(type: ApiRouteType): string {
1768
- // 1. Use discovered routes if available (only for ApiRoutes keys, not client-specific keys)
1769
- const routes = this.discoveryInfo?.routes;
1770
- if (routes) {
1771
- const key = type as keyof ApiRoutes;
1772
- const discovered = routes[key];
1773
- if (discovered) return discovered;
1774
- }
1775
-
1776
- // 2. Fallback to conventions (covers all ApiRoutes keys + client-specific virtual routes)
1777
- const routeMap: Record<ApiRouteType, string> = {
1778
- data: '/api/v1/data',
1779
- metadata: '/api/v1/meta',
1780
- discovery: '/api/v1/discovery',
1781
- ui: '/api/v1/ui',
1782
- auth: '/api/v1/auth',
1783
- analytics: '/api/v1/analytics',
1784
- storage: '/api/v1/storage',
1785
- automation: '/api/v1/automation',
1786
- packages: '/api/v1/packages',
1787
- permissions: '/api/v1/permissions',
1788
- realtime: '/api/v1/realtime',
1789
- workflow: '/api/v1/workflow',
1790
- views: '/api/v1/ui/views',
1791
- notifications: '/api/v1/notifications',
1792
- ai: '/api/v1/ai',
1793
- i18n: '/api/v1/i18n',
1794
- feed: '/api/v1/feed',
1795
- graphql: '/graphql',
1796
- };
1797
-
1798
- return routeMap[type] || `/api/v1/${type}`;
1799
- }
1800
- }
1801
-
1802
- // Re-export type-safe query builder
1803
- export { QueryBuilder, FilterBuilder, createQuery, createFilter } from './query-builder';
1804
-
1805
- // Re-export realtime API types
1806
- export { RealtimeAPI, RealtimeSubscriptionFilter, RealtimeEventHandler } from './realtime-api';
1807
-
1808
- // Re-export commonly used types from @objectstack/spec/api for convenience
1809
- export type {
1810
- BatchUpdateRequest,
1811
- BatchUpdateResponse,
1812
- UpdateManyRequest,
1813
- DeleteManyRequest,
1814
- BatchOptions,
1815
- BatchRecord,
1816
- BatchOperationResult,
1817
- MetadataCacheRequest,
1818
- MetadataCacheResponse,
1819
- StandardErrorCode,
1820
- ErrorCategory,
1821
- GetDiscoveryResponse,
1822
- GetMetaTypesResponse,
1823
- GetMetaItemsResponse,
1824
- CheckPermissionRequest,
1825
- CheckPermissionResponse,
1826
- GetObjectPermissionsResponse,
1827
- GetEffectivePermissionsResponse,
1828
- RealtimeConnectRequest,
1829
- RealtimeConnectResponse,
1830
- RealtimeSubscribeRequest,
1831
- RealtimeSubscribeResponse,
1832
- GetPresenceResponse,
1833
- GetWorkflowConfigResponse,
1834
- GetWorkflowStateResponse,
1835
- WorkflowTransitionRequest,
1836
- WorkflowTransitionResponse,
1837
- WorkflowApproveRequest,
1838
- WorkflowApproveResponse,
1839
- WorkflowRejectRequest,
1840
- WorkflowRejectResponse,
1841
- ListViewsResponse,
1842
- GetViewResponse,
1843
- CreateViewResponse,
1844
- UpdateViewResponse,
1845
- DeleteViewResponse,
1846
- RegisterDeviceRequest,
1847
- RegisterDeviceResponse,
1848
- ListNotificationsResponse,
1849
- AiNlqRequest,
1850
- AiNlqResponse,
1851
- AiSuggestRequest,
1852
- AiSuggestResponse,
1853
- AiInsightsRequest,
1854
- AiInsightsResponse,
1855
- GetLocalesResponse,
1856
- GetTranslationsResponse,
1857
- GetFieldLabelsResponse,
1858
- RegisterRequest,
1859
- RefreshTokenRequest,
1860
- GetFeedResponse,
1861
- CreateFeedItemResponse,
1862
- UpdateFeedItemResponse,
1863
- DeleteFeedItemResponse,
1864
- AddReactionResponse,
1865
- RemoveReactionResponse,
1866
- PinFeedItemResponse,
1867
- UnpinFeedItemResponse,
1868
- StarFeedItemResponse,
1869
- UnstarFeedItemResponse,
1870
- SearchFeedResponse,
1871
- GetChangelogResponse,
1872
- SubscribeResponse,
1873
- UnsubscribeResponse,
1874
- WellKnownCapabilities,
1875
- } from '@objectstack/spec/api';