@objectstack/client 4.0.4 → 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,1889 +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 Protocol-standard Discovery Path /api/v1/discovery (primary)
261
- try {
262
- const discoveryUrl = `${this.baseUrl}/api/v1/discovery`;
263
- this.logger.debug('Probing protocol-standard discovery endpoint', { url: discoveryUrl });
264
- const res = await this.fetchImpl(discoveryUrl);
265
- if (res.ok) {
266
- const body = await res.json();
267
- data = body.data || body;
268
- this.logger.debug('Discovered via /api/v1/discovery');
269
- }
270
- } catch (e) {
271
- this.logger.debug('Protocol-standard discovery probe failed', { error: (e as Error).message });
272
- }
273
-
274
- // 2. Fallback to Standard Discovery (.well-known)
275
- if (!data) {
276
- let wellKnownUrl: string;
277
- try {
278
- // If baseUrl is absolute, get origin
279
- const url = new URL(this.baseUrl);
280
- wellKnownUrl = `${url.origin}/.well-known/objectstack`;
281
- } catch {
282
- // If baseUrl is relative, use absolute path from root
283
- wellKnownUrl = '/.well-known/objectstack';
284
- }
285
-
286
- this.logger.debug('Falling back to .well-known discovery', { url: wellKnownUrl });
287
- const res = await this.fetchImpl(wellKnownUrl);
288
- if (!res.ok) {
289
- throw new Error(`Failed to connect to ${wellKnownUrl}: ${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
- * Get authentication configuration
583
- * Returns available auth providers and features
584
- */
585
- getConfig: async () => {
586
- const route = this.getRoute('auth');
587
- const res = await this.fetch(`${this.baseUrl}${route}/config`);
588
- return this.unwrapResponse(res);
589
- },
590
-
591
- /**
592
- * Login with email and password
593
- * Uses better-auth endpoint: POST /sign-in/email
594
- */
595
- login: async (request: LoginRequest): Promise<SessionResponse> => {
596
- const route = this.getRoute('auth');
597
- const res = await this.fetch(`${this.baseUrl}${route}/sign-in/email`, {
598
- method: 'POST',
599
- body: JSON.stringify(request)
600
- });
601
- const data = await res.json();
602
- // Auto-set token if present in response
603
- if (data.data?.token) {
604
- this.token = data.data.token;
605
- }
606
- return data;
607
- },
608
-
609
- /**
610
- * Logout current user
611
- * Uses better-auth endpoint: POST /sign-out
612
- */
613
- logout: async () => {
614
- const route = this.getRoute('auth');
615
- await this.fetch(`${this.baseUrl}${route}/sign-out`, { method: 'POST' });
616
- this.token = undefined;
617
- },
618
-
619
- /**
620
- * Get current user session
621
- * Uses better-auth endpoint: GET /get-session
622
- */
623
- me: async (): Promise<SessionResponse> => {
624
- const route = this.getRoute('auth');
625
- const res = await this.fetch(`${this.baseUrl}${route}/get-session`);
626
- return res.json();
627
- },
628
-
629
- /**
630
- * Register a new user account
631
- * Uses better-auth endpoint: POST /sign-up/email
632
- */
633
- register: async (request: RegisterRequest): Promise<SessionResponse> => {
634
- const route = this.getRoute('auth');
635
- const res = await this.fetch(`${this.baseUrl}${route}/sign-up/email`, {
636
- method: 'POST',
637
- body: JSON.stringify(request)
638
- });
639
- const data = await res.json();
640
- if (data.data?.token) {
641
- this.token = data.data.token;
642
- }
643
- return data;
644
- },
645
-
646
- /**
647
- * Refresh an authentication token
648
- * Note: better-auth handles token refresh automatically via /get-session
649
- * @param _refreshToken - Not used (better-auth handles refresh automatically)
650
- */
651
- refreshToken: async (_refreshToken: string): Promise<SessionResponse> => {
652
- const route = this.getRoute('auth');
653
- // better-auth doesn't have a separate refresh endpoint
654
- // Session refresh is handled automatically when calling /get-session
655
- const res = await this.fetch(`${this.baseUrl}${route}/get-session`, {
656
- method: 'GET'
657
- });
658
- const data = await res.json();
659
- if (data.data?.token) {
660
- this.token = data.data.token;
661
- }
662
- return data;
663
- }
664
- };
665
-
666
- /**
667
- * Storage Services
668
- */
669
- storage = {
670
- upload: async (file: any, scope: string = 'user'): Promise<FileUploadResponse> => {
671
- // 1. Get Presigned URL
672
- const presignedReq: GetPresignedUrlRequest = {
673
- filename: file.name,
674
- mimeType: file.type,
675
- size: file.size,
676
- scope
677
- };
678
-
679
- const route = this.getRoute('storage');
680
- const presignedRes = await this.fetch(`${this.baseUrl}${route}/upload/presigned`, {
681
- method: 'POST',
682
- body: JSON.stringify(presignedReq)
683
- });
684
- const { data: presigned } = await presignedRes.json() as { data: PresignedUrlResponse['data'] };
685
-
686
- // 2. Upload to Cloud directly (Bypass API Middleware to avoid Auth headers if using S3)
687
- // Use fetchImpl directly
688
- const uploadRes = await this.fetchImpl(presigned.uploadUrl, {
689
- method: presigned.method,
690
- headers: presigned.headers,
691
- body: file
692
- });
693
-
694
- if (!uploadRes.ok) {
695
- throw new Error(`Storage Upload Failed: ${uploadRes.statusText}`);
696
- }
697
-
698
- // 3. Complete Upload
699
- const completeReq: CompleteUploadRequest = {
700
- fileId: presigned.fileId
701
- };
702
- const completeRes = await this.fetch(`${this.baseUrl}${route}/upload/complete`, {
703
- method: 'POST',
704
- body: JSON.stringify(completeReq)
705
- });
706
-
707
- return completeRes.json();
708
- },
709
-
710
- getDownloadUrl: async (fileId: string): Promise<string> => {
711
- const route = this.getRoute('storage');
712
- const res = await this.fetch(`${this.baseUrl}${route}/files/${fileId}/url`);
713
- const data = await res.json();
714
- return data.url;
715
- },
716
-
717
- /**
718
- * Get a presigned URL for direct-to-cloud upload
719
- */
720
- getPresignedUrl: async (req: GetPresignedUrlRequest): Promise<PresignedUrlResponse> => {
721
- const route = this.getRoute('storage');
722
- const res = await this.fetch(`${this.baseUrl}${route}/upload/presigned`, {
723
- method: 'POST',
724
- body: JSON.stringify(req)
725
- });
726
- return res.json();
727
- },
728
-
729
- /**
730
- * Initiate a chunked (multipart) upload session
731
- */
732
- initChunkedUpload: async (req: InitiateChunkedUploadRequest): Promise<InitiateChunkedUploadResponse> => {
733
- const route = this.getRoute('storage');
734
- const res = await this.fetch(`${this.baseUrl}${route}/upload/chunked`, {
735
- method: 'POST',
736
- body: JSON.stringify(req)
737
- });
738
- return res.json();
739
- },
740
-
741
- /**
742
- * Upload a single chunk/part of a multipart upload
743
- */
744
- uploadPart: async (uploadId: string, chunkIndex: number, resumeToken: string, data: Blob | Buffer): Promise<UploadChunkResponse> => {
745
- const route = this.getRoute('storage');
746
- const res = await this.fetch(`${this.baseUrl}${route}/upload/chunked/${uploadId}/chunk/${chunkIndex}`, {
747
- method: 'PUT',
748
- headers: { 'x-resume-token': resumeToken },
749
- body: data as any
750
- });
751
- return res.json();
752
- },
753
-
754
- /**
755
- * Complete a chunked upload by assembling all parts
756
- */
757
- completeChunkedUpload: async (req: CompleteChunkedUploadRequest): Promise<CompleteChunkedUploadResponse> => {
758
- const route = this.getRoute('storage');
759
- const res = await this.fetch(`${this.baseUrl}${route}/upload/chunked/${req.uploadId}/complete`, {
760
- method: 'POST',
761
- body: JSON.stringify(req)
762
- });
763
- return res.json();
764
- },
765
-
766
- /**
767
- * Resume an interrupted chunked upload.
768
- * Fetches current progress, then uploads remaining chunks and completes.
769
- */
770
- resumeUpload: async (uploadId: string, file: Blob | ArrayBuffer, chunkSize: number, resumeToken: string): Promise<CompleteChunkedUploadResponse> => {
771
- const route = this.getRoute('storage');
772
-
773
- // 1. Get current progress
774
- const progressRes = await this.fetch(`${this.baseUrl}${route}/upload/chunked/${uploadId}/progress`);
775
- const progress = await progressRes.json() as UploadProgress;
776
-
777
- const { totalChunks, uploadedChunks } = progress.data;
778
- const parts: Array<{ chunkIndex: number; eTag: string }> = [];
779
-
780
- // 2. Upload remaining chunks
781
- const fileBuffer = file instanceof ArrayBuffer ? file : await file.arrayBuffer();
782
- for (let i = uploadedChunks; i < totalChunks; i++) {
783
- const start = i * chunkSize;
784
- const end = Math.min(start + chunkSize, fileBuffer.byteLength);
785
- const chunk = new Blob([fileBuffer.slice(start, end)]);
786
-
787
- const chunkRes = await this.storage.uploadPart(uploadId, i, resumeToken, chunk);
788
- parts.push({ chunkIndex: i, eTag: chunkRes.data.eTag });
789
- }
790
-
791
- // 3. Complete
792
- return this.storage.completeChunkedUpload({ uploadId, parts });
793
- },
794
- };
795
-
796
- /**
797
- * Automation Services
798
- */
799
- automation = {
800
- /**
801
- * Trigger a named automation flow (legacy endpoint)
802
- */
803
- trigger: async (triggerName: string, payload: any) => {
804
- const route = this.getRoute('automation');
805
- const res = await this.fetch(`${this.baseUrl}${route}/trigger/${triggerName}`, {
806
- method: 'POST',
807
- body: JSON.stringify(payload)
808
- });
809
- return res.json();
810
- },
811
-
812
- /**
813
- * List all registered automation flows
814
- */
815
- list: async (): Promise<{ flows: string[]; total: number; hasMore: boolean }> => {
816
- const route = this.getRoute('automation');
817
- const res = await this.fetch(`${this.baseUrl}${route}`);
818
- return this.unwrapResponse(res);
819
- },
820
-
821
- /**
822
- * Get a flow definition by name
823
- */
824
- get: async (name: string): Promise<any> => {
825
- const route = this.getRoute('automation');
826
- const res = await this.fetch(`${this.baseUrl}${route}/${name}`);
827
- return this.unwrapResponse(res);
828
- },
829
-
830
- /**
831
- * Create (register) a new flow
832
- */
833
- create: async (name: string, definition: any): Promise<any> => {
834
- const route = this.getRoute('automation');
835
- const res = await this.fetch(`${this.baseUrl}${route}`, {
836
- method: 'POST',
837
- body: JSON.stringify({ name, ...definition }),
838
- });
839
- return this.unwrapResponse(res);
840
- },
841
-
842
- /**
843
- * Update an existing flow
844
- */
845
- update: async (name: string, definition: any): Promise<any> => {
846
- const route = this.getRoute('automation');
847
- const res = await this.fetch(`${this.baseUrl}${route}/${name}`, {
848
- method: 'PUT',
849
- body: JSON.stringify({ definition }),
850
- });
851
- return this.unwrapResponse(res);
852
- },
853
-
854
- /**
855
- * Delete (unregister) a flow
856
- */
857
- delete: async (name: string): Promise<{ name: string; deleted: boolean }> => {
858
- const route = this.getRoute('automation');
859
- const res = await this.fetch(`${this.baseUrl}${route}/${name}`, {
860
- method: 'DELETE',
861
- });
862
- return this.unwrapResponse(res);
863
- },
864
-
865
- /**
866
- * Enable or disable a flow
867
- */
868
- toggle: async (name: string, enabled: boolean): Promise<{ name: string; enabled: boolean }> => {
869
- const route = this.getRoute('automation');
870
- const res = await this.fetch(`${this.baseUrl}${route}/${name}/toggle`, {
871
- method: 'POST',
872
- body: JSON.stringify({ enabled }),
873
- });
874
- return this.unwrapResponse(res);
875
- },
876
-
877
- /**
878
- * Execution run history
879
- */
880
- runs: {
881
- /**
882
- * List execution runs for a flow
883
- */
884
- list: async (flowName: string, options?: { limit?: number; cursor?: string }): Promise<{ runs: any[]; hasMore: boolean }> => {
885
- const route = this.getRoute('automation');
886
- const params = new URLSearchParams();
887
- if (options?.limit) params.set('limit', String(options.limit));
888
- if (options?.cursor) params.set('cursor', options.cursor);
889
- const qs = params.toString();
890
- const res = await this.fetch(`${this.baseUrl}${route}/${flowName}/runs${qs ? `?${qs}` : ''}`);
891
- return this.unwrapResponse(res);
892
- },
893
-
894
- /**
895
- * Get a single execution run
896
- */
897
- get: async (flowName: string, runId: string): Promise<any> => {
898
- const route = this.getRoute('automation');
899
- const res = await this.fetch(`${this.baseUrl}${route}/${flowName}/runs/${runId}`);
900
- return this.unwrapResponse(res);
901
- },
902
- },
903
- };
904
-
905
- /**
906
- * Event Subscription API
907
- * Provides real-time event subscriptions for metadata and data changes
908
- */
909
- get events() {
910
- return this.realtimeAPI;
911
- }
912
-
913
- /**
914
- * Permissions Services
915
- */
916
- permissions = {
917
- /**
918
- * Check if current user has permission for an action on an object
919
- */
920
- check: async (request: CheckPermissionRequest): Promise<CheckPermissionResponse> => {
921
- const route = this.getRoute('permissions');
922
- const params = new URLSearchParams({ object: request.object, action: request.action });
923
- if (request.recordId !== undefined) params.set('recordId', request.recordId);
924
- if (request.field !== undefined) params.set('field', request.field);
925
- const res = await this.fetch(`${this.baseUrl}${route}/check?${params.toString()}`);
926
- return this.unwrapResponse<CheckPermissionResponse>(res);
927
- },
928
-
929
- /**
930
- * Get all permissions for a specific object
931
- */
932
- getObjectPermissions: async (object: string): Promise<GetObjectPermissionsResponse> => {
933
- const route = this.getRoute('permissions');
934
- const res = await this.fetch(`${this.baseUrl}${route}/objects/${encodeURIComponent(object)}`);
935
- return this.unwrapResponse<GetObjectPermissionsResponse>(res);
936
- },
937
-
938
- /**
939
- * Get effective permissions for the current user
940
- */
941
- getEffectivePermissions: async (): Promise<GetEffectivePermissionsResponse> => {
942
- const route = this.getRoute('permissions');
943
- const res = await this.fetch(`${this.baseUrl}${route}/effective`);
944
- return this.unwrapResponse<GetEffectivePermissionsResponse>(res);
945
- }
946
- };
947
-
948
- /**
949
- * Realtime Services
950
- */
951
- realtime = {
952
- /**
953
- * Establish a realtime connection
954
- */
955
- connect: async (request?: RealtimeConnectRequest): Promise<RealtimeConnectResponse> => {
956
- const route = this.getRoute('realtime');
957
- const res = await this.fetch(`${this.baseUrl}${route}/connect`, {
958
- method: 'POST',
959
- body: JSON.stringify(request || {})
960
- });
961
- return this.unwrapResponse<RealtimeConnectResponse>(res);
962
- },
963
-
964
- /**
965
- * Disconnect from realtime services
966
- */
967
- disconnect: async (): Promise<void> => {
968
- const route = this.getRoute('realtime');
969
- await this.fetch(`${this.baseUrl}${route}/disconnect`, {
970
- method: 'POST'
971
- });
972
- },
973
-
974
- /**
975
- * Subscribe to a channel
976
- */
977
- subscribe: async (request: RealtimeSubscribeRequest): Promise<RealtimeSubscribeResponse> => {
978
- const route = this.getRoute('realtime');
979
- const res = await this.fetch(`${this.baseUrl}${route}/subscribe`, {
980
- method: 'POST',
981
- body: JSON.stringify(request)
982
- });
983
- return this.unwrapResponse<RealtimeSubscribeResponse>(res);
984
- },
985
-
986
- /**
987
- * Unsubscribe from a channel
988
- */
989
- unsubscribe: async (subscriptionId: string): Promise<void> => {
990
- const route = this.getRoute('realtime');
991
- await this.fetch(`${this.baseUrl}${route}/unsubscribe`, {
992
- method: 'POST',
993
- body: JSON.stringify({ subscriptionId })
994
- });
995
- },
996
-
997
- /**
998
- * Set presence state on a channel
999
- */
1000
- setPresence: async (channel: string, state: SetPresenceRequest['state']): Promise<void> => {
1001
- const route = this.getRoute('realtime');
1002
- await this.fetch(`${this.baseUrl}${route}/presence`, {
1003
- method: 'PUT',
1004
- body: JSON.stringify({ channel, state })
1005
- });
1006
- },
1007
-
1008
- /**
1009
- * Get presence information for a channel
1010
- */
1011
- getPresence: async (channel: string): Promise<GetPresenceResponse> => {
1012
- const route = this.getRoute('realtime');
1013
- const res = await this.fetch(`${this.baseUrl}${route}/presence/${encodeURIComponent(channel)}`);
1014
- return this.unwrapResponse<GetPresenceResponse>(res);
1015
- }
1016
- };
1017
-
1018
- /**
1019
- * Workflow Services
1020
- */
1021
- workflow = {
1022
- /**
1023
- * Get workflow configuration for an object
1024
- */
1025
- getConfig: async (object: string): Promise<GetWorkflowConfigResponse> => {
1026
- const route = this.getRoute('workflow');
1027
- const res = await this.fetch(`${this.baseUrl}${route}/${encodeURIComponent(object)}/config`);
1028
- return this.unwrapResponse<GetWorkflowConfigResponse>(res);
1029
- },
1030
-
1031
- /**
1032
- * Get current workflow state for a record
1033
- */
1034
- getState: async (object: string, recordId: string): Promise<GetWorkflowStateResponse> => {
1035
- const route = this.getRoute('workflow');
1036
- const res = await this.fetch(`${this.baseUrl}${route}/${encodeURIComponent(object)}/${encodeURIComponent(recordId)}/state`);
1037
- return this.unwrapResponse<GetWorkflowStateResponse>(res);
1038
- },
1039
-
1040
- /**
1041
- * Execute a workflow state transition
1042
- */
1043
- transition: async (request: WorkflowTransitionRequest): Promise<WorkflowTransitionResponse> => {
1044
- const route = this.getRoute('workflow');
1045
- const res = await this.fetch(`${this.baseUrl}${route}/${encodeURIComponent(request.object)}/${encodeURIComponent(request.recordId)}/transition`, {
1046
- method: 'POST',
1047
- body: JSON.stringify({
1048
- transition: request.transition,
1049
- comment: request.comment,
1050
- data: request.data
1051
- })
1052
- });
1053
- return this.unwrapResponse<WorkflowTransitionResponse>(res);
1054
- },
1055
-
1056
- /**
1057
- * Approve a workflow step
1058
- */
1059
- approve: async (request: WorkflowApproveRequest): Promise<WorkflowApproveResponse> => {
1060
- const route = this.getRoute('workflow');
1061
- const res = await this.fetch(`${this.baseUrl}${route}/${encodeURIComponent(request.object)}/${encodeURIComponent(request.recordId)}/approve`, {
1062
- method: 'POST',
1063
- body: JSON.stringify({
1064
- comment: request.comment,
1065
- data: request.data
1066
- })
1067
- });
1068
- return this.unwrapResponse<WorkflowApproveResponse>(res);
1069
- },
1070
-
1071
- /**
1072
- * Reject a workflow step
1073
- */
1074
- reject: async (request: WorkflowRejectRequest): Promise<WorkflowRejectResponse> => {
1075
- const route = this.getRoute('workflow');
1076
- const res = await this.fetch(`${this.baseUrl}${route}/${encodeURIComponent(request.object)}/${encodeURIComponent(request.recordId)}/reject`, {
1077
- method: 'POST',
1078
- body: JSON.stringify({
1079
- reason: request.reason,
1080
- comment: request.comment
1081
- })
1082
- });
1083
- return this.unwrapResponse<WorkflowRejectResponse>(res);
1084
- }
1085
- };
1086
-
1087
- /**
1088
- * Views CRUD Services
1089
- */
1090
- views = {
1091
- /**
1092
- * List views for an object
1093
- */
1094
- list: async (object: string, type?: 'list' | 'form'): Promise<ListViewsResponse> => {
1095
- const route = this.getRoute('views');
1096
- const params = new URLSearchParams();
1097
- if (type) params.set('type', type);
1098
- const qs = params.toString();
1099
- const res = await this.fetch(`${this.baseUrl}${route}/${encodeURIComponent(object)}${qs ? `?${qs}` : ''}`);
1100
- return this.unwrapResponse<ListViewsResponse>(res);
1101
- },
1102
-
1103
- /**
1104
- * Get a specific view
1105
- */
1106
- get: async (object: string, viewId: string): Promise<GetViewResponse> => {
1107
- const route = this.getRoute('views');
1108
- const res = await this.fetch(`${this.baseUrl}${route}/${encodeURIComponent(object)}/${encodeURIComponent(viewId)}`);
1109
- return this.unwrapResponse<GetViewResponse>(res);
1110
- },
1111
-
1112
- /**
1113
- * Create a new view
1114
- */
1115
- create: async (object: string, data: CreateViewRequest['data']): Promise<CreateViewResponse> => {
1116
- const route = this.getRoute('views');
1117
- const res = await this.fetch(`${this.baseUrl}${route}/${encodeURIComponent(object)}`, {
1118
- method: 'POST',
1119
- body: JSON.stringify({ object, data })
1120
- });
1121
- return this.unwrapResponse<CreateViewResponse>(res);
1122
- },
1123
-
1124
- /**
1125
- * Update an existing view
1126
- */
1127
- update: async (object: string, viewId: string, data: UpdateViewRequest['data']): Promise<UpdateViewResponse> => {
1128
- const route = this.getRoute('views');
1129
- const res = await this.fetch(`${this.baseUrl}${route}/${encodeURIComponent(object)}/${encodeURIComponent(viewId)}`, {
1130
- method: 'PUT',
1131
- body: JSON.stringify({ object, viewId, data })
1132
- });
1133
- return this.unwrapResponse<UpdateViewResponse>(res);
1134
- },
1135
-
1136
- /**
1137
- * Delete a view
1138
- */
1139
- delete: async (object: string, viewId: string): Promise<DeleteViewResponse> => {
1140
- const route = this.getRoute('views');
1141
- const res = await this.fetch(`${this.baseUrl}${route}/${encodeURIComponent(object)}/${encodeURIComponent(viewId)}`, {
1142
- method: 'DELETE'
1143
- });
1144
- return this.unwrapResponse<DeleteViewResponse>(res);
1145
- }
1146
- };
1147
-
1148
- /**
1149
- * Notification Services
1150
- */
1151
- notifications = {
1152
- /**
1153
- * Register a device for push notifications
1154
- */
1155
- registerDevice: async (request: RegisterDeviceRequest): Promise<RegisterDeviceResponse> => {
1156
- const route = this.getRoute('notifications');
1157
- const res = await this.fetch(`${this.baseUrl}${route}/devices`, {
1158
- method: 'POST',
1159
- body: JSON.stringify(request)
1160
- });
1161
- return this.unwrapResponse<RegisterDeviceResponse>(res);
1162
- },
1163
-
1164
- /**
1165
- * Unregister a device from push notifications
1166
- */
1167
- unregisterDevice: async (deviceId: string): Promise<UnregisterDeviceResponse> => {
1168
- const route = this.getRoute('notifications');
1169
- const res = await this.fetch(`${this.baseUrl}${route}/devices/${encodeURIComponent(deviceId)}`, {
1170
- method: 'DELETE'
1171
- });
1172
- return this.unwrapResponse<UnregisterDeviceResponse>(res);
1173
- },
1174
-
1175
- /**
1176
- * Get notification preferences for the current user
1177
- */
1178
- getPreferences: async (): Promise<GetNotificationPreferencesResponse> => {
1179
- const route = this.getRoute('notifications');
1180
- const res = await this.fetch(`${this.baseUrl}${route}/preferences`);
1181
- return this.unwrapResponse<GetNotificationPreferencesResponse>(res);
1182
- },
1183
-
1184
- /**
1185
- * Update notification preferences
1186
- */
1187
- updatePreferences: async (preferences: UpdateNotificationPreferencesRequest['preferences']): Promise<UpdateNotificationPreferencesResponse> => {
1188
- const route = this.getRoute('notifications');
1189
- const res = await this.fetch(`${this.baseUrl}${route}/preferences`, {
1190
- method: 'PUT',
1191
- body: JSON.stringify({ preferences })
1192
- });
1193
- return this.unwrapResponse<UpdateNotificationPreferencesResponse>(res);
1194
- },
1195
-
1196
- /**
1197
- * List notifications for the current user
1198
- */
1199
- list: async (options?: { read?: boolean; type?: string; limit?: number; cursor?: string }): Promise<ListNotificationsResponse> => {
1200
- const route = this.getRoute('notifications');
1201
- const params = new URLSearchParams();
1202
- if (options?.read !== undefined) params.set('read', String(options.read));
1203
- if (options?.type) params.set('type', options.type);
1204
- if (options?.limit) params.set('limit', String(options.limit));
1205
- if (options?.cursor) params.set('cursor', options.cursor);
1206
- const qs = params.toString();
1207
- const res = await this.fetch(`${this.baseUrl}${route}${qs ? `?${qs}` : ''}`);
1208
- return this.unwrapResponse<ListNotificationsResponse>(res);
1209
- },
1210
-
1211
- /**
1212
- * Mark specific notifications as read
1213
- */
1214
- markRead: async (ids: string[]): Promise<MarkNotificationsReadResponse> => {
1215
- const route = this.getRoute('notifications');
1216
- const res = await this.fetch(`${this.baseUrl}${route}/read`, {
1217
- method: 'POST',
1218
- body: JSON.stringify({ ids })
1219
- });
1220
- return this.unwrapResponse<MarkNotificationsReadResponse>(res);
1221
- },
1222
-
1223
- /**
1224
- * Mark all notifications as read
1225
- */
1226
- markAllRead: async (): Promise<MarkAllNotificationsReadResponse> => {
1227
- const route = this.getRoute('notifications');
1228
- const res = await this.fetch(`${this.baseUrl}${route}/read/all`, {
1229
- method: 'POST'
1230
- });
1231
- return this.unwrapResponse<MarkAllNotificationsReadResponse>(res);
1232
- }
1233
- };
1234
-
1235
- /**
1236
- * AI Services
1237
- */
1238
- ai = {
1239
- /**
1240
- * Natural language query — converts natural language to structured query
1241
- */
1242
- nlq: async (request: AiNlqRequest): Promise<AiNlqResponse> => {
1243
- const route = this.getRoute('ai');
1244
- const res = await this.fetch(`${this.baseUrl}${route}/nlq`, {
1245
- method: 'POST',
1246
- body: JSON.stringify(request)
1247
- });
1248
- return this.unwrapResponse<AiNlqResponse>(res);
1249
- },
1250
-
1251
- // AI chat method removed — use Vercel AI SDK `useChat()` / `@ai-sdk/react` directly.
1252
-
1253
- /**
1254
- * AI-powered field value suggestions
1255
- */
1256
- suggest: async (request: AiSuggestRequest): Promise<AiSuggestResponse> => {
1257
- const route = this.getRoute('ai');
1258
- const res = await this.fetch(`${this.baseUrl}${route}/suggest`, {
1259
- method: 'POST',
1260
- body: JSON.stringify(request)
1261
- });
1262
- return this.unwrapResponse<AiSuggestResponse>(res);
1263
- },
1264
-
1265
- /**
1266
- * AI-powered data insights
1267
- */
1268
- insights: async (request: AiInsightsRequest): Promise<AiInsightsResponse> => {
1269
- const route = this.getRoute('ai');
1270
- const res = await this.fetch(`${this.baseUrl}${route}/insights`, {
1271
- method: 'POST',
1272
- body: JSON.stringify(request)
1273
- });
1274
- return this.unwrapResponse<AiInsightsResponse>(res);
1275
- }
1276
- };
1277
-
1278
- /**
1279
- * Internationalization Services
1280
- */
1281
- i18n = {
1282
- /**
1283
- * Get available locales
1284
- */
1285
- getLocales: async (): Promise<GetLocalesResponse> => {
1286
- const route = this.getRoute('i18n');
1287
- const res = await this.fetch(`${this.baseUrl}${route}/locales`);
1288
- return this.unwrapResponse<GetLocalesResponse>(res);
1289
- },
1290
-
1291
- /**
1292
- * Get translations for a locale
1293
- */
1294
- getTranslations: async (locale: string, options?: { namespace?: string; keys?: string[] }): Promise<GetTranslationsResponse> => {
1295
- const route = this.getRoute('i18n');
1296
- const params = new URLSearchParams();
1297
- params.set('locale', locale);
1298
- if (options?.namespace) params.set('namespace', options.namespace);
1299
- if (options?.keys) params.set('keys', options.keys.join(','));
1300
- const res = await this.fetch(`${this.baseUrl}${route}/translations?${params.toString()}`);
1301
- return this.unwrapResponse<GetTranslationsResponse>(res);
1302
- },
1303
-
1304
- /**
1305
- * Get translated field labels for an object
1306
- */
1307
- getFieldLabels: async (object: string, locale: string): Promise<GetFieldLabelsResponse> => {
1308
- const route = this.getRoute('i18n');
1309
- const res = await this.fetch(`${this.baseUrl}${route}/labels/${encodeURIComponent(object)}?locale=${encodeURIComponent(locale)}`);
1310
- return this.unwrapResponse<GetFieldLabelsResponse>(res);
1311
- }
1312
- };
1313
-
1314
- /**
1315
- * Feed / Chatter Services
1316
- *
1317
- * Provides access to the activity timeline (comments, field changes, tasks),
1318
- * emoji reactions, pin/star, search, changelog, and record subscriptions.
1319
- * Base path: /api/data/{object}/{recordId}/feed
1320
- */
1321
- feed = {
1322
- /**
1323
- * List feed items for a record
1324
- */
1325
- list: async (object: string, recordId: string, options?: { type?: string; limit?: number; cursor?: string }): Promise<GetFeedResponse> => {
1326
- const route = this.getRoute('data');
1327
- const params = new URLSearchParams();
1328
- if (options?.type) params.set('type', options.type);
1329
- if (options?.limit) params.set('limit', String(options.limit));
1330
- if (options?.cursor) params.set('cursor', options.cursor);
1331
- const qs = params.toString();
1332
- const res = await this.fetch(`${this.baseUrl}${route}/${encodeURIComponent(object)}/${encodeURIComponent(recordId)}/feed${qs ? `?${qs}` : ''}`);
1333
- return this.unwrapResponse<GetFeedResponse>(res);
1334
- },
1335
-
1336
- /**
1337
- * Create a new feed item (comment, note, task, etc.)
1338
- */
1339
- create: async (object: string, recordId: string, data: { type: string; body?: string; mentions?: any[]; parentId?: string; visibility?: string }): Promise<CreateFeedItemResponse> => {
1340
- const route = this.getRoute('data');
1341
- const res = await this.fetch(`${this.baseUrl}${route}/${encodeURIComponent(object)}/${encodeURIComponent(recordId)}/feed`, {
1342
- method: 'POST',
1343
- body: JSON.stringify(data)
1344
- });
1345
- return this.unwrapResponse<CreateFeedItemResponse>(res);
1346
- },
1347
-
1348
- /**
1349
- * Update an existing feed item
1350
- */
1351
- update: async (object: string, recordId: string, feedId: string, data: { body?: string; mentions?: any[]; visibility?: string }): Promise<UpdateFeedItemResponse> => {
1352
- const route = this.getRoute('data');
1353
- const res = await this.fetch(`${this.baseUrl}${route}/${encodeURIComponent(object)}/${encodeURIComponent(recordId)}/feed/${encodeURIComponent(feedId)}`, {
1354
- method: 'PUT',
1355
- body: JSON.stringify(data)
1356
- });
1357
- return this.unwrapResponse<UpdateFeedItemResponse>(res);
1358
- },
1359
-
1360
- /**
1361
- * Delete a feed item
1362
- */
1363
- delete: async (object: string, recordId: string, feedId: string): Promise<DeleteFeedItemResponse> => {
1364
- const route = this.getRoute('data');
1365
- const res = await this.fetch(`${this.baseUrl}${route}/${encodeURIComponent(object)}/${encodeURIComponent(recordId)}/feed/${encodeURIComponent(feedId)}`, {
1366
- method: 'DELETE'
1367
- });
1368
- return this.unwrapResponse<DeleteFeedItemResponse>(res);
1369
- },
1370
-
1371
- /**
1372
- * Add an emoji reaction to a feed item
1373
- */
1374
- addReaction: async (object: string, recordId: string, feedId: string, emoji: string): Promise<AddReactionResponse> => {
1375
- const route = this.getRoute('data');
1376
- const res = await this.fetch(`${this.baseUrl}${route}/${encodeURIComponent(object)}/${encodeURIComponent(recordId)}/feed/${encodeURIComponent(feedId)}/reactions`, {
1377
- method: 'POST',
1378
- body: JSON.stringify({ emoji })
1379
- });
1380
- return this.unwrapResponse<AddReactionResponse>(res);
1381
- },
1382
-
1383
- /**
1384
- * Remove an emoji reaction from a feed item
1385
- */
1386
- removeReaction: async (object: string, recordId: string, feedId: string, emoji: string): Promise<RemoveReactionResponse> => {
1387
- const route = this.getRoute('data');
1388
- const res = await this.fetch(`${this.baseUrl}${route}/${encodeURIComponent(object)}/${encodeURIComponent(recordId)}/feed/${encodeURIComponent(feedId)}/reactions/${encodeURIComponent(emoji)}`, {
1389
- method: 'DELETE'
1390
- });
1391
- return this.unwrapResponse<RemoveReactionResponse>(res);
1392
- },
1393
-
1394
- /**
1395
- * Pin a feed item to the top of the timeline
1396
- */
1397
- pin: async (object: string, recordId: string, feedId: string): Promise<PinFeedItemResponse> => {
1398
- const route = this.getRoute('data');
1399
- const res = await this.fetch(`${this.baseUrl}${route}/${encodeURIComponent(object)}/${encodeURIComponent(recordId)}/feed/${encodeURIComponent(feedId)}/pin`, {
1400
- method: 'POST'
1401
- });
1402
- return this.unwrapResponse<PinFeedItemResponse>(res);
1403
- },
1404
-
1405
- /**
1406
- * Unpin a feed item
1407
- */
1408
- unpin: async (object: string, recordId: string, feedId: string): Promise<UnpinFeedItemResponse> => {
1409
- const route = this.getRoute('data');
1410
- const res = await this.fetch(`${this.baseUrl}${route}/${encodeURIComponent(object)}/${encodeURIComponent(recordId)}/feed/${encodeURIComponent(feedId)}/pin`, {
1411
- method: 'DELETE'
1412
- });
1413
- return this.unwrapResponse<UnpinFeedItemResponse>(res);
1414
- },
1415
-
1416
- /**
1417
- * Star (bookmark) a feed item
1418
- */
1419
- star: async (object: string, recordId: string, feedId: string): Promise<StarFeedItemResponse> => {
1420
- const route = this.getRoute('data');
1421
- const res = await this.fetch(`${this.baseUrl}${route}/${encodeURIComponent(object)}/${encodeURIComponent(recordId)}/feed/${encodeURIComponent(feedId)}/star`, {
1422
- method: 'POST'
1423
- });
1424
- return this.unwrapResponse<StarFeedItemResponse>(res);
1425
- },
1426
-
1427
- /**
1428
- * Unstar a feed item
1429
- */
1430
- unstar: async (object: string, recordId: string, feedId: string): Promise<UnstarFeedItemResponse> => {
1431
- const route = this.getRoute('data');
1432
- const res = await this.fetch(`${this.baseUrl}${route}/${encodeURIComponent(object)}/${encodeURIComponent(recordId)}/feed/${encodeURIComponent(feedId)}/star`, {
1433
- method: 'DELETE'
1434
- });
1435
- return this.unwrapResponse<UnstarFeedItemResponse>(res);
1436
- },
1437
-
1438
- /**
1439
- * Search feed items
1440
- */
1441
- search: async (object: string, recordId: string, query: string, options?: { type?: string; actorId?: string; dateFrom?: string; dateTo?: string; limit?: number; cursor?: string }): Promise<SearchFeedResponse> => {
1442
- const route = this.getRoute('data');
1443
- const params = new URLSearchParams();
1444
- params.set('query', query);
1445
- if (options?.type) params.set('type', options.type);
1446
- if (options?.actorId) params.set('actorId', options.actorId);
1447
- if (options?.dateFrom) params.set('dateFrom', options.dateFrom);
1448
- if (options?.dateTo) params.set('dateTo', options.dateTo);
1449
- if (options?.limit) params.set('limit', String(options.limit));
1450
- if (options?.cursor) params.set('cursor', options.cursor);
1451
- const res = await this.fetch(`${this.baseUrl}${route}/${encodeURIComponent(object)}/${encodeURIComponent(recordId)}/feed/search?${params.toString()}`);
1452
- return this.unwrapResponse<SearchFeedResponse>(res);
1453
- },
1454
-
1455
- /**
1456
- * Get field-level changelog for a record
1457
- */
1458
- getChangelog: async (object: string, recordId: string, options?: { field?: string; actorId?: string; dateFrom?: string; dateTo?: string; limit?: number; cursor?: string }): Promise<GetChangelogResponse> => {
1459
- const route = this.getRoute('data');
1460
- const params = new URLSearchParams();
1461
- if (options?.field) params.set('field', options.field);
1462
- if (options?.actorId) params.set('actorId', options.actorId);
1463
- if (options?.dateFrom) params.set('dateFrom', options.dateFrom);
1464
- if (options?.dateTo) params.set('dateTo', options.dateTo);
1465
- if (options?.limit) params.set('limit', String(options.limit));
1466
- if (options?.cursor) params.set('cursor', options.cursor);
1467
- const qs = params.toString();
1468
- const res = await this.fetch(`${this.baseUrl}${route}/${encodeURIComponent(object)}/${encodeURIComponent(recordId)}/changelog${qs ? `?${qs}` : ''}`);
1469
- return this.unwrapResponse<GetChangelogResponse>(res);
1470
- },
1471
-
1472
- /**
1473
- * Subscribe to record notifications
1474
- */
1475
- subscribe: async (object: string, recordId: string, options?: { events?: string[]; channels?: string[] }): Promise<SubscribeResponse> => {
1476
- const route = this.getRoute('data');
1477
- const res = await this.fetch(`${this.baseUrl}${route}/${encodeURIComponent(object)}/${encodeURIComponent(recordId)}/subscribe`, {
1478
- method: 'POST',
1479
- body: JSON.stringify(options || {})
1480
- });
1481
- return this.unwrapResponse<SubscribeResponse>(res);
1482
- },
1483
-
1484
- /**
1485
- * Unsubscribe from record notifications
1486
- */
1487
- unsubscribe: async (object: string, recordId: string): Promise<UnsubscribeResponse> => {
1488
- const route = this.getRoute('data');
1489
- const res = await this.fetch(`${this.baseUrl}${route}/${encodeURIComponent(object)}/${encodeURIComponent(recordId)}/subscribe`, {
1490
- method: 'DELETE'
1491
- });
1492
- return this.unwrapResponse<UnsubscribeResponse>(res);
1493
- },
1494
- };
1495
-
1496
- /**
1497
- * Data Operations
1498
- */
1499
- data = {
1500
- /**
1501
- * Advanced Query using ObjectStack Query Protocol
1502
- * Supports both simplified options and full AST
1503
- */
1504
- query: async <T = any>(object: string, query: Partial<QueryAST>): Promise<PaginatedResult<T>> => {
1505
- const route = this.getRoute('data');
1506
- // POST for complex query to avoid URL length limits and allow clean JSON AST
1507
- // Convention: POST /api/v1/data/:object/query
1508
- const res = await this.fetch(`${this.baseUrl}${route}/${object}/query`, {
1509
- method: 'POST',
1510
- body: JSON.stringify(query)
1511
- });
1512
- return this.unwrapResponse<PaginatedResult<T>>(res);
1513
- },
1514
-
1515
- /**
1516
- * @deprecated Use `data.query()` with standard QueryAST parameters instead.
1517
- * This method uses legacy parameter names. Internally adapts to HTTP GET params.
1518
- */
1519
- find: async <T = any>(object: string, options: QueryOptions | QueryOptionsV2 = {}): Promise<PaginatedResult<T>> => {
1520
- const route = this.getRoute('data');
1521
- const queryParams = new URLSearchParams();
1522
-
1523
- // ── Normalize V2 canonical options → HTTP transport params ───
1524
- // Detect V2 options by presence of canonical-only keys.
1525
- const v2 = options as QueryOptionsV2;
1526
- const normalizedOptions: QueryOptions = {} as QueryOptions;
1527
- if ('where' in options || 'fields' in options || 'orderBy' in options || 'offset' in options) {
1528
- // V2 canonical options detected — map to legacy HTTP transport keys
1529
- if (v2.where) normalizedOptions.filter = v2.where as any;
1530
- if (v2.fields) normalizedOptions.select = v2.fields;
1531
- if (v2.orderBy) normalizedOptions.sort = v2.orderBy as any;
1532
- if (v2.limit != null) normalizedOptions.top = v2.limit;
1533
- if (v2.offset != null) normalizedOptions.skip = v2.offset;
1534
- if (v2.aggregations) normalizedOptions.aggregations = v2.aggregations;
1535
- if (v2.groupBy) normalizedOptions.groupBy = v2.groupBy;
1536
- } else {
1537
- // Legacy QueryOptions — pass through as-is
1538
- Object.assign(normalizedOptions, options);
1539
- }
1540
-
1541
- // 1. Handle Pagination
1542
- if (normalizedOptions.top) queryParams.set('top', normalizedOptions.top.toString());
1543
- if (normalizedOptions.skip) queryParams.set('skip', normalizedOptions.skip.toString());
1544
-
1545
- // 2. Handle Sort
1546
- if (normalizedOptions.sort) {
1547
- // Check if it's AST
1548
- if (Array.isArray(normalizedOptions.sort) && typeof normalizedOptions.sort[0] === 'object') {
1549
- queryParams.set('sort', JSON.stringify(normalizedOptions.sort));
1550
- } else {
1551
- const sortVal = Array.isArray(normalizedOptions.sort) ? normalizedOptions.sort.join(',') : normalizedOptions.sort;
1552
- queryParams.set('sort', sortVal as string);
1553
- }
1554
- }
1555
-
1556
- // 3. Handle Select
1557
- if (normalizedOptions.select) {
1558
- queryParams.set('select', normalizedOptions.select.join(','));
1559
- }
1560
-
1561
- // 4. Handle Filters (Simple vs AST)
1562
- // Canonical HTTP param name: `filter` (singular). `filters` (plural) is accepted
1563
- // for backward compatibility but `filter` is the standard going forward.
1564
- const filterValue = normalizedOptions.filter ?? normalizedOptions.filters;
1565
- if (filterValue) {
1566
- // Detect AST filter format vs simple key-value map. AST filters use an array structure
1567
- // with [field, operator, value] or [logicOp, ...nodes] shape (see isFilterAST from spec).
1568
- // For complex filter expressions, use .query() which builds a proper QueryAST.
1569
- if (this.isFilterAST(filterValue) || Array.isArray(filterValue)) {
1570
- // AST or any array → serialize as JSON in `filter` param
1571
- queryParams.set('filter', JSON.stringify(filterValue));
1572
- } else if (typeof filterValue === 'object' && filterValue !== null) {
1573
- // Plain key-value map → append each as individual query params
1574
- Object.entries(filterValue as Record<string, unknown>).forEach(([k, v]) => {
1575
- if (v !== undefined && v !== null) {
1576
- queryParams.append(k, String(v));
1577
- }
1578
- });
1579
- }
1580
- }
1581
-
1582
- // 5. Handle Aggregations & GroupBy (Pass through as JSON if present)
1583
- if (normalizedOptions.aggregations) {
1584
- queryParams.set('aggregations', JSON.stringify(normalizedOptions.aggregations));
1585
- }
1586
- if (normalizedOptions.groupBy) {
1587
- queryParams.set('groupBy', normalizedOptions.groupBy.join(','));
1588
- }
1589
-
1590
- const res = await this.fetch(`${this.baseUrl}${route}/${object}?${queryParams.toString()}`);
1591
- return this.unwrapResponse<PaginatedResult<T>>(res);
1592
- },
1593
-
1594
- get: async <T = any>(object: string, id: string): Promise<GetDataResult<T>> => {
1595
- const route = this.getRoute('data');
1596
- const res = await this.fetch(`${this.baseUrl}${route}/${object}/${id}`);
1597
- return this.unwrapResponse<GetDataResult<T>>(res);
1598
- },
1599
-
1600
- create: async <T = any>(object: string, data: Partial<T>): Promise<CreateDataResult<T>> => {
1601
- const route = this.getRoute('data');
1602
- const res = await this.fetch(`${this.baseUrl}${route}/${object}`, {
1603
- method: 'POST',
1604
- body: JSON.stringify(data)
1605
- });
1606
- return this.unwrapResponse<CreateDataResult<T>>(res);
1607
- },
1608
-
1609
- createMany: async <T = any>(object: string, data: Partial<T>[]): Promise<T[]> => {
1610
- const route = this.getRoute('data');
1611
- const res = await this.fetch(`${this.baseUrl}${route}/${object}/createMany`, {
1612
- method: 'POST',
1613
- body: JSON.stringify(data)
1614
- });
1615
- return this.unwrapResponse<T[]>(res);
1616
- },
1617
-
1618
- update: async <T = any>(object: string, id: string, data: Partial<T>): Promise<UpdateDataResult<T>> => {
1619
- const route = this.getRoute('data');
1620
- const res = await this.fetch(`${this.baseUrl}${route}/${object}/${id}`, {
1621
- method: 'PATCH',
1622
- body: JSON.stringify(data)
1623
- });
1624
- return this.unwrapResponse<UpdateDataResult<T>>(res);
1625
- },
1626
-
1627
- /**
1628
- * Batch update multiple records
1629
- * Uses the new BatchUpdateRequest schema with full control over options
1630
- */
1631
- batch: async (object: string, request: BatchUpdateRequest): Promise<BatchUpdateResponse> => {
1632
- const route = this.getRoute('data');
1633
- const res = await this.fetch(`${this.baseUrl}${route}/${object}/batch`, {
1634
- method: 'POST',
1635
- body: JSON.stringify(request)
1636
- });
1637
- return this.unwrapResponse<BatchUpdateResponse>(res);
1638
- },
1639
-
1640
- /**
1641
- * Update multiple records (simplified batch update)
1642
- * Convenience method for batch updates without full BatchUpdateRequest
1643
- */
1644
- updateMany: async <T = any>(
1645
- object: string,
1646
- records: Array<{ id: string; data: Partial<T> }>,
1647
- options?: BatchOptions
1648
- ): Promise<BatchUpdateResponse> => {
1649
- const route = this.getRoute('data');
1650
- const request: UpdateManyRequest = {
1651
- records,
1652
- options
1653
- };
1654
- const res = await this.fetch(`${this.baseUrl}${route}/${object}/updateMany`, {
1655
- method: 'POST',
1656
- body: JSON.stringify(request)
1657
- });
1658
- return this.unwrapResponse<BatchUpdateResponse>(res);
1659
- },
1660
-
1661
- delete: async (object: string, id: string): Promise<DeleteDataResult> => {
1662
- const route = this.getRoute('data');
1663
- const res = await this.fetch(`${this.baseUrl}${route}/${object}/${id}`, {
1664
- method: 'DELETE'
1665
- });
1666
- return this.unwrapResponse<DeleteDataResult>(res);
1667
- },
1668
-
1669
- /**
1670
- * Delete multiple records by IDs
1671
- */
1672
- deleteMany: async(object: string, ids: string[], options?: BatchOptions): Promise<BatchUpdateResponse> => {
1673
- const route = this.getRoute('data');
1674
- const request: DeleteManyRequest = {
1675
- ids,
1676
- options
1677
- };
1678
- const res = await this.fetch(`${this.baseUrl}${route}/${object}/deleteMany`, {
1679
- method: 'POST',
1680
- body: JSON.stringify(request)
1681
- });
1682
- return this.unwrapResponse<BatchUpdateResponse>(res);
1683
- }
1684
- };
1685
-
1686
-
1687
-
1688
- /**
1689
- * Private Helpers
1690
- */
1691
-
1692
- private isFilterAST(filter: any): boolean {
1693
- // Delegate to the spec-exported structural validator instead of naive Array.isArray.
1694
- // This checks for valid AST shapes: [field, op, val], [logic, ...nodes], or [[cond], ...].
1695
- return isFilterAST(filter);
1696
- }
1697
-
1698
- /**
1699
- * Unwrap the standard REST API response envelope.
1700
- * The HTTP layer wraps responses as `{ success: boolean, data: T, meta? }`
1701
- * (see BaseResponseSchema in contract.zod.ts).
1702
- * This method strips the envelope and returns the inner `data` payload
1703
- * so callers receive the spec-level type (e.g. GetMetaTypesResponse).
1704
- */
1705
- private async unwrapResponse<T>(res: Response): Promise<T> {
1706
- const body = await res.json();
1707
- // If the body has a `success` flag it's a BaseResponse envelope
1708
- if (body && typeof body.success === 'boolean' && 'data' in body) {
1709
- return body.data as T;
1710
- }
1711
- // Already unwrapped or non-standard
1712
- return body as T;
1713
- }
1714
-
1715
- private async fetch(url: string, options: RequestInit = {}): Promise<Response> {
1716
- this.logger.debug('HTTP request', {
1717
- method: options.method || 'GET',
1718
- url,
1719
- hasBody: !!options.body
1720
- });
1721
-
1722
- const headers: Record<string, string> = {
1723
- 'Content-Type': 'application/json',
1724
- ...(options.headers as Record<string, string> || {}),
1725
- };
1726
-
1727
- if (this.token) {
1728
- headers['Authorization'] = `Bearer ${this.token}`;
1729
- }
1730
-
1731
- const res = await this.fetchImpl(url, { ...options, headers });
1732
-
1733
- this.logger.debug('HTTP response', {
1734
- method: options.method || 'GET',
1735
- url,
1736
- status: res.status,
1737
- ok: res.ok
1738
- });
1739
-
1740
- if (!res.ok) {
1741
- let errorBody: any;
1742
- try {
1743
- errorBody = await res.json();
1744
- } catch {
1745
- errorBody = { message: res.statusText };
1746
- }
1747
-
1748
- this.logger.error('HTTP request failed', undefined, {
1749
- method: options.method || 'GET',
1750
- url,
1751
- status: res.status,
1752
- error: errorBody
1753
- });
1754
-
1755
- // Create a standardized error if the response includes error details
1756
- const errorMessage = errorBody?.message || errorBody?.error?.message || res.statusText;
1757
- const errorCode = errorBody?.code || errorBody?.error?.code;
1758
- const error = new Error(`[ObjectStack] ${errorCode ? `${errorCode}: ` : ''}${errorMessage}`) as any;
1759
-
1760
- // Attach error details for programmatic access
1761
- error.code = errorCode;
1762
- error.category = errorBody?.category;
1763
- error.httpStatus = res.status;
1764
- error.retryable = errorBody?.retryable;
1765
- error.details = errorBody?.details || errorBody;
1766
-
1767
- throw error;
1768
- }
1769
-
1770
- return res;
1771
- }
1772
-
1773
- /**
1774
- * Get the conventional route path for a given API endpoint type
1775
- * ObjectStack uses standard conventions: /api/v1/data, /api/v1/meta, /api/v1/ui
1776
- */
1777
- private getRoute(type: ApiRouteType): string {
1778
- // 1. Use discovered routes if available (only for ApiRoutes keys, not client-specific keys)
1779
- const routes = this.discoveryInfo?.routes;
1780
- if (routes) {
1781
- const key = type as keyof ApiRoutes;
1782
- const discovered = routes[key];
1783
- if (discovered) return discovered;
1784
- }
1785
-
1786
- // 2. Fallback to conventions (covers all ApiRoutes keys + client-specific virtual routes)
1787
- const routeMap: Record<ApiRouteType, string> = {
1788
- data: '/api/v1/data',
1789
- metadata: '/api/v1/meta',
1790
- discovery: '/api/v1/discovery',
1791
- ui: '/api/v1/ui',
1792
- auth: '/api/v1/auth',
1793
- analytics: '/api/v1/analytics',
1794
- storage: '/api/v1/storage',
1795
- automation: '/api/v1/automation',
1796
- packages: '/api/v1/packages',
1797
- permissions: '/api/v1/permissions',
1798
- realtime: '/api/v1/realtime',
1799
- workflow: '/api/v1/workflow',
1800
- views: '/api/v1/ui/views',
1801
- notifications: '/api/v1/notifications',
1802
- ai: '/api/v1/ai',
1803
- i18n: '/api/v1/i18n',
1804
- feed: '/api/v1/feed',
1805
- graphql: '/graphql',
1806
- };
1807
-
1808
- return routeMap[type] || `/api/v1/${type}`;
1809
- }
1810
- }
1811
-
1812
- // Re-export type-safe query builder
1813
- export { QueryBuilder, FilterBuilder, createQuery, createFilter } from './query-builder';
1814
-
1815
- // Re-export realtime API types
1816
- export { RealtimeAPI, RealtimeSubscriptionFilter, RealtimeEventHandler } from './realtime-api';
1817
-
1818
- // Re-export commonly used types from @objectstack/spec/api for convenience
1819
- export type {
1820
- BatchUpdateRequest,
1821
- BatchUpdateResponse,
1822
- UpdateManyRequest,
1823
- DeleteManyRequest,
1824
- BatchOptions,
1825
- BatchRecord,
1826
- BatchOperationResult,
1827
- MetadataCacheRequest,
1828
- MetadataCacheResponse,
1829
- StandardErrorCode,
1830
- ErrorCategory,
1831
- GetDiscoveryResponse,
1832
- GetMetaTypesResponse,
1833
- GetMetaItemsResponse,
1834
- CheckPermissionRequest,
1835
- CheckPermissionResponse,
1836
- GetObjectPermissionsResponse,
1837
- GetEffectivePermissionsResponse,
1838
- RealtimeConnectRequest,
1839
- RealtimeConnectResponse,
1840
- RealtimeSubscribeRequest,
1841
- RealtimeSubscribeResponse,
1842
- GetPresenceResponse,
1843
- GetWorkflowConfigResponse,
1844
- GetWorkflowStateResponse,
1845
- WorkflowTransitionRequest,
1846
- WorkflowTransitionResponse,
1847
- WorkflowApproveRequest,
1848
- WorkflowApproveResponse,
1849
- WorkflowRejectRequest,
1850
- WorkflowRejectResponse,
1851
- ListViewsResponse,
1852
- GetViewResponse,
1853
- CreateViewResponse,
1854
- UpdateViewResponse,
1855
- DeleteViewResponse,
1856
- RegisterDeviceRequest,
1857
- RegisterDeviceResponse,
1858
- ListNotificationsResponse,
1859
- AiNlqRequest,
1860
- AiNlqResponse,
1861
- AiSuggestRequest,
1862
- AiSuggestResponse,
1863
- AiInsightsRequest,
1864
- AiInsightsResponse,
1865
- GetLocalesResponse,
1866
- GetTranslationsResponse,
1867
- GetFieldLabelsResponse,
1868
- RegisterRequest,
1869
- RefreshTokenRequest,
1870
- GetFeedResponse,
1871
- CreateFeedItemResponse,
1872
- UpdateFeedItemResponse,
1873
- DeleteFeedItemResponse,
1874
- AddReactionResponse,
1875
- RemoveReactionResponse,
1876
- PinFeedItemResponse,
1877
- UnpinFeedItemResponse,
1878
- StarFeedItemResponse,
1879
- UnstarFeedItemResponse,
1880
- SearchFeedResponse,
1881
- GetChangelogResponse,
1882
- SubscribeResponse,
1883
- UnsubscribeResponse,
1884
- WellKnownCapabilities,
1885
- GetAuthConfigResponse,
1886
- AuthProviderInfo,
1887
- EmailPasswordConfigPublic,
1888
- AuthFeaturesConfig,
1889
- } from '@objectstack/spec/api';