@object-ui/data-objectstack 0.5.0 → 3.0.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
package/src/index.ts CHANGED
@@ -7,7 +7,7 @@
7
7
  */
8
8
 
9
9
  import { ObjectStackClient, type QueryOptions as ObjectStackQueryOptions } from '@objectstack/client';
10
- import type { DataSource, QueryParams, QueryResult } from '@object-ui/types';
10
+ import type { DataSource, QueryParams, QueryResult, FileUploadResult } from '@object-ui/types';
11
11
  import { convertFiltersToAST } from '@object-ui/core';
12
12
  import { MetadataCache } from './cache/MetadataCache';
13
13
  import {
@@ -53,6 +53,9 @@ export type ConnectionStateListener = (event: ConnectionStateEvent) => void;
53
53
  */
54
54
  export type BatchProgressListener = (event: BatchProgressEvent) => void;
55
55
 
56
+ // Re-export FileUploadResult from types for consumers
57
+ export type { FileUploadResult } from '@object-ui/types';
58
+
56
59
  /**
57
60
  * ObjectStack Data Source Adapter
58
61
  *
@@ -93,6 +96,8 @@ export class ObjectStackAdapter<T = unknown> implements DataSource<T> {
93
96
  private maxReconnectAttempts: number;
94
97
  private reconnectDelay: number;
95
98
  private reconnectAttempts: number = 0;
99
+ private baseUrl: string;
100
+ private token?: string;
96
101
 
97
102
  constructor(config: {
98
103
  baseUrl: string;
@@ -111,6 +116,8 @@ export class ObjectStackAdapter<T = unknown> implements DataSource<T> {
111
116
  this.autoReconnect = config.autoReconnect ?? true;
112
117
  this.maxReconnectAttempts = config.maxReconnectAttempts ?? 3;
113
118
  this.reconnectDelay = config.reconnectDelay ?? 1000;
119
+ this.baseUrl = config.baseUrl;
120
+ this.token = config.token;
114
121
  }
115
122
 
116
123
  /**
@@ -261,14 +268,16 @@ export class ObjectStackAdapter<T = unknown> implements DataSource<T> {
261
268
  };
262
269
  }
263
270
 
264
- const resultObj = result as { value?: T[]; count?: number };
271
+ const resultObj = result as { records?: T[]; total?: number; value?: T[]; count?: number };
272
+ const records = resultObj.records || resultObj.value || [];
273
+ const total = resultObj.total ?? resultObj.count ?? records.length;
265
274
  return {
266
- data: resultObj.value || [],
267
- total: resultObj.count || (resultObj.value ? resultObj.value.length : 0),
275
+ data: records,
276
+ total,
268
277
  // Calculate page number safely
269
278
  page: params?.$skip && params.$top ? Math.floor(params.$skip / params.$top) + 1 : 1,
270
279
  pageSize: params?.$top,
271
- hasMore: params?.$top ? (resultObj.value?.length || 0) === params.$top : false,
280
+ hasMore: params?.$top ? records.length === params.$top : false,
272
281
  };
273
282
  }
274
283
 
@@ -279,8 +288,8 @@ export class ObjectStackAdapter<T = unknown> implements DataSource<T> {
279
288
  await this.connect();
280
289
 
281
290
  try {
282
- const record = await this.client.data.get<T>(resource, String(id));
283
- return record;
291
+ const result = await this.client.data.get<T>(resource, String(id));
292
+ return result.record;
284
293
  } catch (error: unknown) {
285
294
  // If record not found, return null instead of throwing
286
295
  if ((error as Record<string, unknown>)?.status === 404) {
@@ -295,7 +304,8 @@ export class ObjectStackAdapter<T = unknown> implements DataSource<T> {
295
304
  */
296
305
  async create(resource: string, data: Partial<T>): Promise<T> {
297
306
  await this.connect();
298
- return this.client.data.create<T>(resource, data);
307
+ const result = await this.client.data.create<T>(resource, data);
308
+ return result.record;
299
309
  }
300
310
 
301
311
  /**
@@ -303,7 +313,8 @@ export class ObjectStackAdapter<T = unknown> implements DataSource<T> {
303
313
  */
304
314
  async update(resource: string, id: string | number, data: Partial<T>): Promise<T> {
305
315
  await this.connect();
306
- return this.client.data.update<T>(resource, String(id), data);
316
+ const result = await this.client.data.update<T>(resource, String(id), data);
317
+ return result.record;
307
318
  }
308
319
 
309
320
  /**
@@ -312,7 +323,7 @@ export class ObjectStackAdapter<T = unknown> implements DataSource<T> {
312
323
  async delete(resource: string, id: string | number): Promise<boolean> {
313
324
  await this.connect();
314
325
  const result = await this.client.data.delete(resource, String(id));
315
- return result.success;
326
+ return result.deleted;
316
327
  }
317
328
 
318
329
  /**
@@ -416,7 +427,7 @@ export class ObjectStackAdapter<T = unknown> implements DataSource<T> {
416
427
 
417
428
  try {
418
429
  const result = await this.client.data.update<T>(resource, String(id), item);
419
- results.push(result);
430
+ results.push(result.record);
420
431
  completed++;
421
432
  emitProgress();
422
433
  } catch (error: unknown) {
@@ -548,7 +559,7 @@ export class ObjectStackAdapter<T = unknown> implements DataSource<T> {
548
559
  try {
549
560
  // Use cache with automatic fetching
550
561
  const schema = await this.metadataCache.get(objectName, async () => {
551
- const result: any = await this.client.meta.getObject(objectName);
562
+ const result: any = await this.client.meta.getItem('object', objectName);
552
563
 
553
564
  // Unwrap 'item' property if present (common API response wrapper)
554
565
  if (result && result.item) {
@@ -582,6 +593,132 @@ export class ObjectStackAdapter<T = unknown> implements DataSource<T> {
582
593
  return this.client;
583
594
  }
584
595
 
596
+ /**
597
+ * Get the discovery information from the connected server.
598
+ * Returns the capabilities and service status of the ObjectStack server.
599
+ *
600
+ * Note: This accesses an internal property of the ObjectStackClient.
601
+ * The discovery data is populated during client.connect() and cached.
602
+ *
603
+ * @returns Promise resolving to discovery data, or null if not connected
604
+ */
605
+ async getDiscovery(): Promise<unknown | null> {
606
+ try {
607
+ // Ensure we're connected first
608
+ await this.connect();
609
+
610
+ // Access discovery data from the client
611
+ // The ObjectStackClient caches discovery during connect()
612
+ // This is an internal property, but documented for this use case
613
+ // @ts-expect-error - Accessing internal discoveryInfo property
614
+ return this.client.discoveryInfo || null;
615
+ } catch {
616
+ return null;
617
+ }
618
+ }
619
+
620
+ /**
621
+ * Get a view definition for an object.
622
+ * Attempts to fetch from the server metadata API.
623
+ * Falls back to null if the server doesn't provide view definitions,
624
+ * allowing the consumer to use static config.
625
+ *
626
+ * @param objectName - Object name
627
+ * @param viewId - View identifier
628
+ * @returns Promise resolving to the view definition or null
629
+ */
630
+ async getView(objectName: string, viewId: string): Promise<unknown | null> {
631
+ await this.connect();
632
+
633
+ try {
634
+ const cacheKey = `view:${objectName}:${viewId}`;
635
+ return await this.metadataCache.get(cacheKey, async () => {
636
+ // Try meta.getItem for view metadata
637
+ const result: any = await this.client.meta.getItem(objectName, `views/${viewId}`);
638
+ if (result && result.item) return result.item;
639
+ return result ?? null;
640
+ });
641
+ } catch {
642
+ // Server doesn't support view metadata — return null to fall back to static config
643
+ return null;
644
+ }
645
+ }
646
+
647
+ /**
648
+ * Get an application definition by name or ID.
649
+ * Attempts to fetch from the server metadata API.
650
+ * Falls back to null if the server doesn't provide app definitions,
651
+ * allowing the consumer to use static config.
652
+ *
653
+ * @param appId - Application identifier
654
+ * @returns Promise resolving to the app definition or null
655
+ */
656
+ async getApp(appId: string): Promise<unknown | null> {
657
+ await this.connect();
658
+
659
+ try {
660
+ const cacheKey = `app:${appId}`;
661
+ return await this.metadataCache.get(cacheKey, async () => {
662
+ const result: any = await this.client.meta.getItem('apps', appId);
663
+ if (result && result.item) return result.item;
664
+ return result ?? null;
665
+ });
666
+ } catch {
667
+ // Server doesn't support app metadata — return null to fall back to static config
668
+ return null;
669
+ }
670
+ }
671
+
672
+ /**
673
+ * Get a page definition from ObjectStack.
674
+ * Uses the metadata API to fetch page layouts.
675
+ * Returns null if the server doesn't support page metadata.
676
+ */
677
+ async getPage(pageId: string): Promise<unknown | null> {
678
+ await this.connect();
679
+
680
+ try {
681
+ const cacheKey = `page:${pageId}`;
682
+ return await this.metadataCache.get(cacheKey, async () => {
683
+ const result: any = await this.client.meta.getItem('pages', pageId);
684
+ if (result && result.item) return result.item;
685
+ return result ?? null;
686
+ });
687
+ } catch {
688
+ // Server doesn't support page metadata — return null to fall back to static config
689
+ return null;
690
+ }
691
+ }
692
+
693
+ /**
694
+ * Get multiple metadata items from ObjectStack.
695
+ * Uses v3.0.0 metadata API pattern: getItems for batch retrieval.
696
+ */
697
+ async getItems(category: string, names: string[]): Promise<unknown[]> {
698
+ await this.connect();
699
+
700
+ const results = await Promise.all(
701
+ names.map(async (name) => {
702
+ const cacheKey = `${category}:${name}`;
703
+ return this.metadataCache.get(cacheKey, async () => {
704
+ const result: any = await this.client.meta.getItem(category, name);
705
+ if (result && result.item) return result.item;
706
+ return result;
707
+ });
708
+ })
709
+ );
710
+
711
+ return results;
712
+ }
713
+
714
+ /**
715
+ * Get cached metadata if available, without triggering a fetch.
716
+ * Uses v3.0.0 metadata API pattern: getCached for synchronous cache access.
717
+ */
718
+ getCached(key: string): unknown | undefined {
719
+ return this.metadataCache.getCachedSync(key);
720
+ }
721
+
585
722
  /**
586
723
  * Get cache statistics for monitoring performance.
587
724
  */
@@ -604,6 +741,131 @@ export class ObjectStackAdapter<T = unknown> implements DataSource<T> {
604
741
  clearCache(): void {
605
742
  this.metadataCache.clear();
606
743
  }
744
+
745
+ /**
746
+ * Upload a single file to a resource.
747
+ * Posts the file as multipart/form-data to the ObjectStack server.
748
+ *
749
+ * @param resource - The resource/object name to attach the file to
750
+ * @param file - File object or Blob to upload
751
+ * @param options - Additional upload options (recordId, fieldName, metadata)
752
+ * @returns Promise resolving to the upload result (file URL, metadata)
753
+ */
754
+ async uploadFile(
755
+ resource: string,
756
+ file: File | Blob,
757
+ options?: {
758
+ recordId?: string;
759
+ fieldName?: string;
760
+ metadata?: Record<string, unknown>;
761
+ onProgress?: (percent: number) => void;
762
+ },
763
+ ): Promise<FileUploadResult> {
764
+ await this.connect();
765
+
766
+ const formData = new FormData();
767
+ formData.append('file', file);
768
+
769
+ if (options?.recordId) {
770
+ formData.append('recordId', options.recordId);
771
+ }
772
+ if (options?.fieldName) {
773
+ formData.append('fieldName', options.fieldName);
774
+ }
775
+ if (options?.metadata) {
776
+ formData.append('metadata', JSON.stringify(options.metadata));
777
+ }
778
+
779
+ const url = `${this.baseUrl}/api/data/${encodeURIComponent(resource)}/upload`;
780
+
781
+ const response = await fetch(url, {
782
+ method: 'POST',
783
+ body: formData,
784
+ headers: {
785
+ ...(this.getAuthHeaders()),
786
+ },
787
+ });
788
+
789
+ if (!response.ok) {
790
+ const error = await response.json().catch(() => ({ message: response.statusText }));
791
+ throw new ObjectStackError(
792
+ error.message || `Upload failed with status ${response.status}`,
793
+ 'UPLOAD_ERROR',
794
+ response.status,
795
+ );
796
+ }
797
+
798
+ return response.json();
799
+ }
800
+
801
+ /**
802
+ * Upload multiple files to a resource.
803
+ * Posts all files as a single multipart/form-data request.
804
+ *
805
+ * @param resource - The resource/object name to attach the files to
806
+ * @param files - Array of File objects or Blobs to upload
807
+ * @param options - Additional upload options
808
+ * @returns Promise resolving to array of upload results
809
+ */
810
+ async uploadFiles(
811
+ resource: string,
812
+ files: (File | Blob)[],
813
+ options?: {
814
+ recordId?: string;
815
+ fieldName?: string;
816
+ metadata?: Record<string, unknown>;
817
+ onProgress?: (percent: number) => void;
818
+ },
819
+ ): Promise<FileUploadResult[]> {
820
+ await this.connect();
821
+
822
+ const formData = new FormData();
823
+ files.forEach((file, idx) => {
824
+ formData.append(`files`, file, (file as File).name || `file-${idx}`);
825
+ });
826
+
827
+ if (options?.recordId) {
828
+ formData.append('recordId', options.recordId);
829
+ }
830
+ if (options?.fieldName) {
831
+ formData.append('fieldName', options.fieldName);
832
+ }
833
+ if (options?.metadata) {
834
+ formData.append('metadata', JSON.stringify(options.metadata));
835
+ }
836
+
837
+ const url = `${this.baseUrl}/api/data/${encodeURIComponent(resource)}/upload`;
838
+
839
+ const response = await fetch(url, {
840
+ method: 'POST',
841
+ body: formData,
842
+ headers: {
843
+ ...(this.getAuthHeaders()),
844
+ },
845
+ });
846
+
847
+ if (!response.ok) {
848
+ const error = await response.json().catch(() => ({ message: response.statusText }));
849
+ throw new ObjectStackError(
850
+ error.message || `Upload failed with status ${response.status}`,
851
+ 'UPLOAD_ERROR',
852
+ response.status,
853
+ );
854
+ }
855
+
856
+ return response.json();
857
+ }
858
+
859
+ /**
860
+ * Get authorization headers from the adapter config.
861
+ */
862
+ private getAuthHeaders(): Record<string, string> {
863
+ const headers: Record<string, string> = {};
864
+ if (this.token) {
865
+ headers['Authorization'] = `Bearer ${this.token}`;
866
+ }
867
+ return headers;
868
+ }
607
869
  }
608
870
 
609
871
  /**
@@ -650,3 +912,19 @@ export {
650
912
 
651
913
  // Export cache types
652
914
  export type { CacheStats } from './cache/MetadataCache';
915
+
916
+ // v3.0.0 Deep Integration modules
917
+ export { CloudOperations } from './cloud';
918
+ export type { CloudDeploymentConfig, CloudHostingConfig, CloudMarketplaceEntry } from './cloud';
919
+
920
+ export { validatePluginContract, generateContractManifest } from './contracts';
921
+ export type { PluginContract, PluginExport, PluginAPIContract, ContractValidationResult, ContractValidationError } from './contracts';
922
+
923
+ export { IntegrationManager } from './integration';
924
+ export type { IntegrationConfig, IntegrationTrigger, IntegrationProvider, SlackIntegrationConfig, EmailIntegrationConfig, WebhookIntegrationConfig } from './integration';
925
+
926
+ export { SecurityManager } from './security';
927
+ export type { SecurityPolicy, CSPConfig, AuditLogConfig, AuditEventType, DataMaskingConfig, DataMaskingRule, AuditLogEntry } from './security';
928
+
929
+ export { createDefaultCanvasConfig, snapToGrid, calculateAutoLayout } from './studio';
930
+ export type { StudioCanvasConfig, StudioPropertyEditor, StudioThemeBuilderConfig, StudioColorPalette, StudioTypographyPreset, StudioShadowPreset } from './studio';
@@ -0,0 +1,192 @@
1
+ /**
2
+ * ObjectUI
3
+ * Copyright (c) 2024-present ObjectStack Inc.
4
+ *
5
+ * This source code is licensed under the MIT license found in the
6
+ * LICENSE file in the root directory of this source tree.
7
+ */
8
+
9
+ /**
10
+ * Integration module for @objectstack/spec v3.0.0
11
+ * Provides third-party service connectors for Slack, email, webhooks.
12
+ */
13
+
14
+ export type IntegrationProvider = 'slack' | 'email' | 'webhook' | 'teams' | 'discord';
15
+
16
+ export interface IntegrationConfig {
17
+ /** Integration provider type */
18
+ provider: IntegrationProvider;
19
+ /** Whether this integration is enabled */
20
+ enabled: boolean;
21
+ /** Provider-specific configuration */
22
+ config: Record<string, unknown>;
23
+ /** Event triggers */
24
+ triggers?: IntegrationTrigger[];
25
+ }
26
+
27
+ export interface IntegrationTrigger {
28
+ /** Event name (e.g. 'record.created', 'record.updated') */
29
+ event: string;
30
+ /** Filter condition */
31
+ filter?: string;
32
+ /** Template for the message/payload */
33
+ template?: string;
34
+ }
35
+
36
+ export interface SlackIntegrationConfig extends IntegrationConfig {
37
+ provider: 'slack';
38
+ config: {
39
+ webhookUrl: string;
40
+ channel?: string;
41
+ username?: string;
42
+ iconEmoji?: string;
43
+ };
44
+ }
45
+
46
+ export interface EmailIntegrationConfig extends IntegrationConfig {
47
+ provider: 'email';
48
+ config: {
49
+ smtpHost: string;
50
+ smtpPort: number;
51
+ secure: boolean;
52
+ from: string;
53
+ to: string[];
54
+ subject?: string;
55
+ };
56
+ }
57
+
58
+ export interface WebhookIntegrationConfig extends IntegrationConfig {
59
+ provider: 'webhook';
60
+ config: {
61
+ url: string;
62
+ method: 'GET' | 'POST' | 'PUT' | 'PATCH';
63
+ headers?: Record<string, string>;
64
+ retryCount?: number;
65
+ retryDelay?: number;
66
+ };
67
+ }
68
+
69
+ /**
70
+ * Integration service manager.
71
+ * Manages third-party service connections and event dispatching.
72
+ */
73
+ export class IntegrationManager {
74
+ private integrations: Map<string, IntegrationConfig> = new Map();
75
+
76
+ /**
77
+ * Register a new integration.
78
+ */
79
+ register(id: string, config: IntegrationConfig): void {
80
+ this.integrations.set(id, config);
81
+ }
82
+
83
+ /**
84
+ * Remove an integration.
85
+ */
86
+ unregister(id: string): void {
87
+ this.integrations.delete(id);
88
+ }
89
+
90
+ /**
91
+ * Get all registered integrations.
92
+ */
93
+ getAll(): Map<string, IntegrationConfig> {
94
+ return new Map(this.integrations);
95
+ }
96
+
97
+ /**
98
+ * Get integrations that match a specific event.
99
+ */
100
+ getForEvent(event: string): IntegrationConfig[] {
101
+ return Array.from(this.integrations.values()).filter(
102
+ (integration) =>
103
+ integration.enabled &&
104
+ integration.triggers?.some((t) => t.event === event)
105
+ );
106
+ }
107
+
108
+ /**
109
+ * Dispatch an event to all matching integrations.
110
+ * Returns results for each integration.
111
+ */
112
+ async dispatch(event: string, payload: Record<string, unknown>): Promise<Array<{ id: string; success: boolean; error?: string }>> {
113
+ const matching = this.getForEvent(event);
114
+ const results: Array<{ id: string; success: boolean; error?: string }> = [];
115
+
116
+ for (const [id, integration] of this.integrations) {
117
+ if (!matching.includes(integration)) continue;
118
+
119
+ try {
120
+ await this.send(integration, payload);
121
+ results.push({ id, success: true });
122
+ } catch (err) {
123
+ results.push({ id, success: false, error: (err as Error).message });
124
+ }
125
+ }
126
+
127
+ return results;
128
+ }
129
+
130
+ /**
131
+ * Send payload to a specific integration.
132
+ */
133
+ private async send(integration: IntegrationConfig, payload: Record<string, unknown>): Promise<void> {
134
+ switch (integration.provider) {
135
+ case 'webhook': {
136
+ const cfg = integration.config as WebhookIntegrationConfig['config'];
137
+ const url = cfg.url;
138
+ // Validate URL - only allow http and https protocols
139
+ try {
140
+ const parsed = new URL(url);
141
+ if (parsed.protocol !== 'http:' && parsed.protocol !== 'https:') {
142
+ throw new Error(`Invalid URL protocol: ${parsed.protocol}`);
143
+ }
144
+ } catch (e) {
145
+ if (e instanceof TypeError) {
146
+ throw new Error(`Invalid webhook URL: ${url}`);
147
+ }
148
+ throw e;
149
+ }
150
+ await fetch(url, {
151
+ method: cfg.method,
152
+ headers: {
153
+ 'Content-Type': 'application/json',
154
+ ...cfg.headers,
155
+ },
156
+ body: JSON.stringify(payload),
157
+ });
158
+ break;
159
+ }
160
+ case 'slack': {
161
+ const cfg = integration.config as SlackIntegrationConfig['config'];
162
+ const url = cfg.webhookUrl;
163
+ // Validate URL - only allow https protocol for Slack webhooks
164
+ try {
165
+ const parsed = new URL(url);
166
+ if (parsed.protocol !== 'https:') {
167
+ throw new Error(`Invalid Slack webhook URL protocol: ${parsed.protocol}`);
168
+ }
169
+ } catch (e) {
170
+ if (e instanceof TypeError) {
171
+ throw new Error(`Invalid Slack webhook URL: ${url}`);
172
+ }
173
+ throw e;
174
+ }
175
+ await fetch(url, {
176
+ method: 'POST',
177
+ headers: { 'Content-Type': 'application/json' },
178
+ body: JSON.stringify({
179
+ channel: cfg.channel,
180
+ username: cfg.username,
181
+ icon_emoji: cfg.iconEmoji,
182
+ text: JSON.stringify(payload),
183
+ }),
184
+ });
185
+ break;
186
+ }
187
+ // Email and other providers would require server-side implementation
188
+ default:
189
+ break;
190
+ }
191
+ }
192
+ }