@object-ui/data-objectstack 0.5.0 → 2.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/dist/index.cjs CHANGED
@@ -377,11 +377,15 @@ var ObjectStackAdapter = class {
377
377
  __publicField(this, "maxReconnectAttempts");
378
378
  __publicField(this, "reconnectDelay");
379
379
  __publicField(this, "reconnectAttempts", 0);
380
+ __publicField(this, "baseUrl");
381
+ __publicField(this, "token");
380
382
  this.client = new import_client.ObjectStackClient(config);
381
383
  this.metadataCache = new MetadataCache(config.cache);
382
384
  this.autoReconnect = config.autoReconnect ?? true;
383
385
  this.maxReconnectAttempts = config.maxReconnectAttempts ?? 3;
384
386
  this.reconnectDelay = config.reconnectDelay ?? 1e3;
387
+ this.baseUrl = config.baseUrl;
388
+ this.token = config.token;
385
389
  }
386
390
  /**
387
391
  * Ensure the client is connected to the server.
@@ -521,8 +525,8 @@ var ObjectStackAdapter = class {
521
525
  async findOne(resource, id, _params) {
522
526
  await this.connect();
523
527
  try {
524
- const record = await this.client.data.get(resource, String(id));
525
- return record;
528
+ const result = await this.client.data.get(resource, String(id));
529
+ return result.record;
526
530
  } catch (error) {
527
531
  if (error?.status === 404) {
528
532
  return null;
@@ -535,14 +539,16 @@ var ObjectStackAdapter = class {
535
539
  */
536
540
  async create(resource, data) {
537
541
  await this.connect();
538
- return this.client.data.create(resource, data);
542
+ const result = await this.client.data.create(resource, data);
543
+ return result.record;
539
544
  }
540
545
  /**
541
546
  * Update an existing record.
542
547
  */
543
548
  async update(resource, id, data) {
544
549
  await this.connect();
545
- return this.client.data.update(resource, String(id), data);
550
+ const result = await this.client.data.update(resource, String(id), data);
551
+ return result.record;
546
552
  }
547
553
  /**
548
554
  * Delete a record.
@@ -550,7 +556,7 @@ var ObjectStackAdapter = class {
550
556
  async delete(resource, id) {
551
557
  await this.connect();
552
558
  const result = await this.client.data.delete(resource, String(id));
553
- return result.success;
559
+ return result.deleted;
554
560
  }
555
561
  /**
556
562
  * Bulk operations with optimized batch processing and error handling.
@@ -632,7 +638,7 @@ var ObjectStackAdapter = class {
632
638
  }
633
639
  try {
634
640
  const result = await this.client.data.update(resource, String(id), item);
635
- results.push(result);
641
+ results.push(result.record);
636
642
  completed++;
637
643
  emitProgress();
638
644
  } catch (error) {
@@ -757,6 +763,68 @@ var ObjectStackAdapter = class {
757
763
  getClient() {
758
764
  return this.client;
759
765
  }
766
+ /**
767
+ * Get the discovery information from the connected server.
768
+ * Returns the capabilities and service status of the ObjectStack server.
769
+ *
770
+ * Note: This accesses an internal property of the ObjectStackClient.
771
+ * The discovery data is populated during client.connect() and cached.
772
+ *
773
+ * @returns Promise resolving to discovery data, or null if not connected
774
+ */
775
+ async getDiscovery() {
776
+ try {
777
+ await this.connect();
778
+ return this.client.discoveryInfo || null;
779
+ } catch {
780
+ return null;
781
+ }
782
+ }
783
+ /**
784
+ * Get a view definition for an object.
785
+ * Attempts to fetch from the server metadata API.
786
+ * Falls back to null if the server doesn't provide view definitions,
787
+ * allowing the consumer to use static config.
788
+ *
789
+ * @param objectName - Object name
790
+ * @param viewId - View identifier
791
+ * @returns Promise resolving to the view definition or null
792
+ */
793
+ async getView(objectName, viewId) {
794
+ await this.connect();
795
+ try {
796
+ const cacheKey = `view:${objectName}:${viewId}`;
797
+ return await this.metadataCache.get(cacheKey, async () => {
798
+ const result = await this.client.meta.getItem(objectName, `views/${viewId}`);
799
+ if (result && result.item) return result.item;
800
+ return result ?? null;
801
+ });
802
+ } catch {
803
+ return null;
804
+ }
805
+ }
806
+ /**
807
+ * Get an application definition by name or ID.
808
+ * Attempts to fetch from the server metadata API.
809
+ * Falls back to null if the server doesn't provide app definitions,
810
+ * allowing the consumer to use static config.
811
+ *
812
+ * @param appId - Application identifier
813
+ * @returns Promise resolving to the app definition or null
814
+ */
815
+ async getApp(appId) {
816
+ await this.connect();
817
+ try {
818
+ const cacheKey = `app:${appId}`;
819
+ return await this.metadataCache.get(cacheKey, async () => {
820
+ const result = await this.client.meta.getItem("apps", appId);
821
+ if (result && result.item) return result.item;
822
+ return result ?? null;
823
+ });
824
+ } catch {
825
+ return null;
826
+ }
827
+ }
760
828
  /**
761
829
  * Get cache statistics for monitoring performance.
762
830
  */
@@ -777,6 +845,98 @@ var ObjectStackAdapter = class {
777
845
  clearCache() {
778
846
  this.metadataCache.clear();
779
847
  }
848
+ /**
849
+ * Upload a single file to a resource.
850
+ * Posts the file as multipart/form-data to the ObjectStack server.
851
+ *
852
+ * @param resource - The resource/object name to attach the file to
853
+ * @param file - File object or Blob to upload
854
+ * @param options - Additional upload options (recordId, fieldName, metadata)
855
+ * @returns Promise resolving to the upload result (file URL, metadata)
856
+ */
857
+ async uploadFile(resource, file, options) {
858
+ await this.connect();
859
+ const formData = new FormData();
860
+ formData.append("file", file);
861
+ if (options?.recordId) {
862
+ formData.append("recordId", options.recordId);
863
+ }
864
+ if (options?.fieldName) {
865
+ formData.append("fieldName", options.fieldName);
866
+ }
867
+ if (options?.metadata) {
868
+ formData.append("metadata", JSON.stringify(options.metadata));
869
+ }
870
+ const url = `${this.baseUrl}/api/data/${encodeURIComponent(resource)}/upload`;
871
+ const response = await fetch(url, {
872
+ method: "POST",
873
+ body: formData,
874
+ headers: {
875
+ ...this.getAuthHeaders()
876
+ }
877
+ });
878
+ if (!response.ok) {
879
+ const error = await response.json().catch(() => ({ message: response.statusText }));
880
+ throw new ObjectStackError(
881
+ error.message || `Upload failed with status ${response.status}`,
882
+ "UPLOAD_ERROR",
883
+ response.status
884
+ );
885
+ }
886
+ return response.json();
887
+ }
888
+ /**
889
+ * Upload multiple files to a resource.
890
+ * Posts all files as a single multipart/form-data request.
891
+ *
892
+ * @param resource - The resource/object name to attach the files to
893
+ * @param files - Array of File objects or Blobs to upload
894
+ * @param options - Additional upload options
895
+ * @returns Promise resolving to array of upload results
896
+ */
897
+ async uploadFiles(resource, files, options) {
898
+ await this.connect();
899
+ const formData = new FormData();
900
+ files.forEach((file, idx) => {
901
+ formData.append(`files`, file, file.name || `file-${idx}`);
902
+ });
903
+ if (options?.recordId) {
904
+ formData.append("recordId", options.recordId);
905
+ }
906
+ if (options?.fieldName) {
907
+ formData.append("fieldName", options.fieldName);
908
+ }
909
+ if (options?.metadata) {
910
+ formData.append("metadata", JSON.stringify(options.metadata));
911
+ }
912
+ const url = `${this.baseUrl}/api/data/${encodeURIComponent(resource)}/upload`;
913
+ const response = await fetch(url, {
914
+ method: "POST",
915
+ body: formData,
916
+ headers: {
917
+ ...this.getAuthHeaders()
918
+ }
919
+ });
920
+ if (!response.ok) {
921
+ const error = await response.json().catch(() => ({ message: response.statusText }));
922
+ throw new ObjectStackError(
923
+ error.message || `Upload failed with status ${response.status}`,
924
+ "UPLOAD_ERROR",
925
+ response.status
926
+ );
927
+ }
928
+ return response.json();
929
+ }
930
+ /**
931
+ * Get authorization headers from the adapter config.
932
+ */
933
+ getAuthHeaders() {
934
+ const headers = {};
935
+ if (this.token) {
936
+ headers["Authorization"] = `Bearer ${this.token}`;
937
+ }
938
+ return headers;
939
+ }
780
940
  };
781
941
  function createObjectStackAdapter(config) {
782
942
  return new ObjectStackAdapter(config);
package/dist/index.d.cts CHANGED
@@ -1,5 +1,6 @@
1
1
  import { ObjectStackClient } from '@objectstack/client';
2
- import { DataSource, QueryParams, QueryResult } from '@object-ui/types';
2
+ import { DataSource, QueryParams, QueryResult, FileUploadResult } from '@object-ui/types';
3
+ export { FileUploadResult } from '@object-ui/types';
3
4
 
4
5
  /**
5
6
  * ObjectUI
@@ -188,6 +189,7 @@ type ConnectionStateListener = (event: ConnectionStateEvent) => void;
188
189
  * Event listener type for batch operation progress
189
190
  */
190
191
  type BatchProgressListener = (event: BatchProgressEvent) => void;
192
+
191
193
  /**
192
194
  * ObjectStack Data Source Adapter
193
195
  *
@@ -228,6 +230,8 @@ declare class ObjectStackAdapter<T = unknown> implements DataSource<T> {
228
230
  private maxReconnectAttempts;
229
231
  private reconnectDelay;
230
232
  private reconnectAttempts;
233
+ private baseUrl;
234
+ private token?;
231
235
  constructor(config: {
232
236
  baseUrl: string;
233
237
  token?: string;
@@ -321,6 +325,37 @@ declare class ObjectStackAdapter<T = unknown> implements DataSource<T> {
321
325
  * Get access to the underlying ObjectStack client for advanced operations.
322
326
  */
323
327
  getClient(): ObjectStackClient;
328
+ /**
329
+ * Get the discovery information from the connected server.
330
+ * Returns the capabilities and service status of the ObjectStack server.
331
+ *
332
+ * Note: This accesses an internal property of the ObjectStackClient.
333
+ * The discovery data is populated during client.connect() and cached.
334
+ *
335
+ * @returns Promise resolving to discovery data, or null if not connected
336
+ */
337
+ getDiscovery(): Promise<unknown | null>;
338
+ /**
339
+ * Get a view definition for an object.
340
+ * Attempts to fetch from the server metadata API.
341
+ * Falls back to null if the server doesn't provide view definitions,
342
+ * allowing the consumer to use static config.
343
+ *
344
+ * @param objectName - Object name
345
+ * @param viewId - View identifier
346
+ * @returns Promise resolving to the view definition or null
347
+ */
348
+ getView(objectName: string, viewId: string): Promise<unknown | null>;
349
+ /**
350
+ * Get an application definition by name or ID.
351
+ * Attempts to fetch from the server metadata API.
352
+ * Falls back to null if the server doesn't provide app definitions,
353
+ * allowing the consumer to use static config.
354
+ *
355
+ * @param appId - Application identifier
356
+ * @returns Promise resolving to the app definition or null
357
+ */
358
+ getApp(appId: string): Promise<unknown | null>;
324
359
  /**
325
360
  * Get cache statistics for monitoring performance.
326
361
  */
@@ -335,6 +370,40 @@ declare class ObjectStackAdapter<T = unknown> implements DataSource<T> {
335
370
  * Clear all cache entries and statistics.
336
371
  */
337
372
  clearCache(): void;
373
+ /**
374
+ * Upload a single file to a resource.
375
+ * Posts the file as multipart/form-data to the ObjectStack server.
376
+ *
377
+ * @param resource - The resource/object name to attach the file to
378
+ * @param file - File object or Blob to upload
379
+ * @param options - Additional upload options (recordId, fieldName, metadata)
380
+ * @returns Promise resolving to the upload result (file URL, metadata)
381
+ */
382
+ uploadFile(resource: string, file: File | Blob, options?: {
383
+ recordId?: string;
384
+ fieldName?: string;
385
+ metadata?: Record<string, unknown>;
386
+ onProgress?: (percent: number) => void;
387
+ }): Promise<FileUploadResult>;
388
+ /**
389
+ * Upload multiple files to a resource.
390
+ * Posts all files as a single multipart/form-data request.
391
+ *
392
+ * @param resource - The resource/object name to attach the files to
393
+ * @param files - Array of File objects or Blobs to upload
394
+ * @param options - Additional upload options
395
+ * @returns Promise resolving to array of upload results
396
+ */
397
+ uploadFiles(resource: string, files: (File | Blob)[], options?: {
398
+ recordId?: string;
399
+ fieldName?: string;
400
+ metadata?: Record<string, unknown>;
401
+ onProgress?: (percent: number) => void;
402
+ }): Promise<FileUploadResult[]>;
403
+ /**
404
+ * Get authorization headers from the adapter config.
405
+ */
406
+ private getAuthHeaders;
338
407
  }
339
408
  /**
340
409
  * Factory function to create an ObjectStack data source.
package/dist/index.d.ts CHANGED
@@ -1,5 +1,6 @@
1
1
  import { ObjectStackClient } from '@objectstack/client';
2
- import { DataSource, QueryParams, QueryResult } from '@object-ui/types';
2
+ import { DataSource, QueryParams, QueryResult, FileUploadResult } from '@object-ui/types';
3
+ export { FileUploadResult } from '@object-ui/types';
3
4
 
4
5
  /**
5
6
  * ObjectUI
@@ -188,6 +189,7 @@ type ConnectionStateListener = (event: ConnectionStateEvent) => void;
188
189
  * Event listener type for batch operation progress
189
190
  */
190
191
  type BatchProgressListener = (event: BatchProgressEvent) => void;
192
+
191
193
  /**
192
194
  * ObjectStack Data Source Adapter
193
195
  *
@@ -228,6 +230,8 @@ declare class ObjectStackAdapter<T = unknown> implements DataSource<T> {
228
230
  private maxReconnectAttempts;
229
231
  private reconnectDelay;
230
232
  private reconnectAttempts;
233
+ private baseUrl;
234
+ private token?;
231
235
  constructor(config: {
232
236
  baseUrl: string;
233
237
  token?: string;
@@ -321,6 +325,37 @@ declare class ObjectStackAdapter<T = unknown> implements DataSource<T> {
321
325
  * Get access to the underlying ObjectStack client for advanced operations.
322
326
  */
323
327
  getClient(): ObjectStackClient;
328
+ /**
329
+ * Get the discovery information from the connected server.
330
+ * Returns the capabilities and service status of the ObjectStack server.
331
+ *
332
+ * Note: This accesses an internal property of the ObjectStackClient.
333
+ * The discovery data is populated during client.connect() and cached.
334
+ *
335
+ * @returns Promise resolving to discovery data, or null if not connected
336
+ */
337
+ getDiscovery(): Promise<unknown | null>;
338
+ /**
339
+ * Get a view definition for an object.
340
+ * Attempts to fetch from the server metadata API.
341
+ * Falls back to null if the server doesn't provide view definitions,
342
+ * allowing the consumer to use static config.
343
+ *
344
+ * @param objectName - Object name
345
+ * @param viewId - View identifier
346
+ * @returns Promise resolving to the view definition or null
347
+ */
348
+ getView(objectName: string, viewId: string): Promise<unknown | null>;
349
+ /**
350
+ * Get an application definition by name or ID.
351
+ * Attempts to fetch from the server metadata API.
352
+ * Falls back to null if the server doesn't provide app definitions,
353
+ * allowing the consumer to use static config.
354
+ *
355
+ * @param appId - Application identifier
356
+ * @returns Promise resolving to the app definition or null
357
+ */
358
+ getApp(appId: string): Promise<unknown | null>;
324
359
  /**
325
360
  * Get cache statistics for monitoring performance.
326
361
  */
@@ -335,6 +370,40 @@ declare class ObjectStackAdapter<T = unknown> implements DataSource<T> {
335
370
  * Clear all cache entries and statistics.
336
371
  */
337
372
  clearCache(): void;
373
+ /**
374
+ * Upload a single file to a resource.
375
+ * Posts the file as multipart/form-data to the ObjectStack server.
376
+ *
377
+ * @param resource - The resource/object name to attach the file to
378
+ * @param file - File object or Blob to upload
379
+ * @param options - Additional upload options (recordId, fieldName, metadata)
380
+ * @returns Promise resolving to the upload result (file URL, metadata)
381
+ */
382
+ uploadFile(resource: string, file: File | Blob, options?: {
383
+ recordId?: string;
384
+ fieldName?: string;
385
+ metadata?: Record<string, unknown>;
386
+ onProgress?: (percent: number) => void;
387
+ }): Promise<FileUploadResult>;
388
+ /**
389
+ * Upload multiple files to a resource.
390
+ * Posts all files as a single multipart/form-data request.
391
+ *
392
+ * @param resource - The resource/object name to attach the files to
393
+ * @param files - Array of File objects or Blobs to upload
394
+ * @param options - Additional upload options
395
+ * @returns Promise resolving to array of upload results
396
+ */
397
+ uploadFiles(resource: string, files: (File | Blob)[], options?: {
398
+ recordId?: string;
399
+ fieldName?: string;
400
+ metadata?: Record<string, unknown>;
401
+ onProgress?: (percent: number) => void;
402
+ }): Promise<FileUploadResult[]>;
403
+ /**
404
+ * Get authorization headers from the adapter config.
405
+ */
406
+ private getAuthHeaders;
338
407
  }
339
408
  /**
340
409
  * Factory function to create an ObjectStack data source.
package/dist/index.js CHANGED
@@ -345,11 +345,15 @@ var ObjectStackAdapter = class {
345
345
  __publicField(this, "maxReconnectAttempts");
346
346
  __publicField(this, "reconnectDelay");
347
347
  __publicField(this, "reconnectAttempts", 0);
348
+ __publicField(this, "baseUrl");
349
+ __publicField(this, "token");
348
350
  this.client = new ObjectStackClient(config);
349
351
  this.metadataCache = new MetadataCache(config.cache);
350
352
  this.autoReconnect = config.autoReconnect ?? true;
351
353
  this.maxReconnectAttempts = config.maxReconnectAttempts ?? 3;
352
354
  this.reconnectDelay = config.reconnectDelay ?? 1e3;
355
+ this.baseUrl = config.baseUrl;
356
+ this.token = config.token;
353
357
  }
354
358
  /**
355
359
  * Ensure the client is connected to the server.
@@ -489,8 +493,8 @@ var ObjectStackAdapter = class {
489
493
  async findOne(resource, id, _params) {
490
494
  await this.connect();
491
495
  try {
492
- const record = await this.client.data.get(resource, String(id));
493
- return record;
496
+ const result = await this.client.data.get(resource, String(id));
497
+ return result.record;
494
498
  } catch (error) {
495
499
  if (error?.status === 404) {
496
500
  return null;
@@ -503,14 +507,16 @@ var ObjectStackAdapter = class {
503
507
  */
504
508
  async create(resource, data) {
505
509
  await this.connect();
506
- return this.client.data.create(resource, data);
510
+ const result = await this.client.data.create(resource, data);
511
+ return result.record;
507
512
  }
508
513
  /**
509
514
  * Update an existing record.
510
515
  */
511
516
  async update(resource, id, data) {
512
517
  await this.connect();
513
- return this.client.data.update(resource, String(id), data);
518
+ const result = await this.client.data.update(resource, String(id), data);
519
+ return result.record;
514
520
  }
515
521
  /**
516
522
  * Delete a record.
@@ -518,7 +524,7 @@ var ObjectStackAdapter = class {
518
524
  async delete(resource, id) {
519
525
  await this.connect();
520
526
  const result = await this.client.data.delete(resource, String(id));
521
- return result.success;
527
+ return result.deleted;
522
528
  }
523
529
  /**
524
530
  * Bulk operations with optimized batch processing and error handling.
@@ -600,7 +606,7 @@ var ObjectStackAdapter = class {
600
606
  }
601
607
  try {
602
608
  const result = await this.client.data.update(resource, String(id), item);
603
- results.push(result);
609
+ results.push(result.record);
604
610
  completed++;
605
611
  emitProgress();
606
612
  } catch (error) {
@@ -725,6 +731,68 @@ var ObjectStackAdapter = class {
725
731
  getClient() {
726
732
  return this.client;
727
733
  }
734
+ /**
735
+ * Get the discovery information from the connected server.
736
+ * Returns the capabilities and service status of the ObjectStack server.
737
+ *
738
+ * Note: This accesses an internal property of the ObjectStackClient.
739
+ * The discovery data is populated during client.connect() and cached.
740
+ *
741
+ * @returns Promise resolving to discovery data, or null if not connected
742
+ */
743
+ async getDiscovery() {
744
+ try {
745
+ await this.connect();
746
+ return this.client.discoveryInfo || null;
747
+ } catch {
748
+ return null;
749
+ }
750
+ }
751
+ /**
752
+ * Get a view definition for an object.
753
+ * Attempts to fetch from the server metadata API.
754
+ * Falls back to null if the server doesn't provide view definitions,
755
+ * allowing the consumer to use static config.
756
+ *
757
+ * @param objectName - Object name
758
+ * @param viewId - View identifier
759
+ * @returns Promise resolving to the view definition or null
760
+ */
761
+ async getView(objectName, viewId) {
762
+ await this.connect();
763
+ try {
764
+ const cacheKey = `view:${objectName}:${viewId}`;
765
+ return await this.metadataCache.get(cacheKey, async () => {
766
+ const result = await this.client.meta.getItem(objectName, `views/${viewId}`);
767
+ if (result && result.item) return result.item;
768
+ return result ?? null;
769
+ });
770
+ } catch {
771
+ return null;
772
+ }
773
+ }
774
+ /**
775
+ * Get an application definition by name or ID.
776
+ * Attempts to fetch from the server metadata API.
777
+ * Falls back to null if the server doesn't provide app definitions,
778
+ * allowing the consumer to use static config.
779
+ *
780
+ * @param appId - Application identifier
781
+ * @returns Promise resolving to the app definition or null
782
+ */
783
+ async getApp(appId) {
784
+ await this.connect();
785
+ try {
786
+ const cacheKey = `app:${appId}`;
787
+ return await this.metadataCache.get(cacheKey, async () => {
788
+ const result = await this.client.meta.getItem("apps", appId);
789
+ if (result && result.item) return result.item;
790
+ return result ?? null;
791
+ });
792
+ } catch {
793
+ return null;
794
+ }
795
+ }
728
796
  /**
729
797
  * Get cache statistics for monitoring performance.
730
798
  */
@@ -745,6 +813,98 @@ var ObjectStackAdapter = class {
745
813
  clearCache() {
746
814
  this.metadataCache.clear();
747
815
  }
816
+ /**
817
+ * Upload a single file to a resource.
818
+ * Posts the file as multipart/form-data to the ObjectStack server.
819
+ *
820
+ * @param resource - The resource/object name to attach the file to
821
+ * @param file - File object or Blob to upload
822
+ * @param options - Additional upload options (recordId, fieldName, metadata)
823
+ * @returns Promise resolving to the upload result (file URL, metadata)
824
+ */
825
+ async uploadFile(resource, file, options) {
826
+ await this.connect();
827
+ const formData = new FormData();
828
+ formData.append("file", file);
829
+ if (options?.recordId) {
830
+ formData.append("recordId", options.recordId);
831
+ }
832
+ if (options?.fieldName) {
833
+ formData.append("fieldName", options.fieldName);
834
+ }
835
+ if (options?.metadata) {
836
+ formData.append("metadata", JSON.stringify(options.metadata));
837
+ }
838
+ const url = `${this.baseUrl}/api/data/${encodeURIComponent(resource)}/upload`;
839
+ const response = await fetch(url, {
840
+ method: "POST",
841
+ body: formData,
842
+ headers: {
843
+ ...this.getAuthHeaders()
844
+ }
845
+ });
846
+ if (!response.ok) {
847
+ const error = await response.json().catch(() => ({ message: response.statusText }));
848
+ throw new ObjectStackError(
849
+ error.message || `Upload failed with status ${response.status}`,
850
+ "UPLOAD_ERROR",
851
+ response.status
852
+ );
853
+ }
854
+ return response.json();
855
+ }
856
+ /**
857
+ * Upload multiple files to a resource.
858
+ * Posts all files as a single multipart/form-data request.
859
+ *
860
+ * @param resource - The resource/object name to attach the files to
861
+ * @param files - Array of File objects or Blobs to upload
862
+ * @param options - Additional upload options
863
+ * @returns Promise resolving to array of upload results
864
+ */
865
+ async uploadFiles(resource, files, options) {
866
+ await this.connect();
867
+ const formData = new FormData();
868
+ files.forEach((file, idx) => {
869
+ formData.append(`files`, file, file.name || `file-${idx}`);
870
+ });
871
+ if (options?.recordId) {
872
+ formData.append("recordId", options.recordId);
873
+ }
874
+ if (options?.fieldName) {
875
+ formData.append("fieldName", options.fieldName);
876
+ }
877
+ if (options?.metadata) {
878
+ formData.append("metadata", JSON.stringify(options.metadata));
879
+ }
880
+ const url = `${this.baseUrl}/api/data/${encodeURIComponent(resource)}/upload`;
881
+ const response = await fetch(url, {
882
+ method: "POST",
883
+ body: formData,
884
+ headers: {
885
+ ...this.getAuthHeaders()
886
+ }
887
+ });
888
+ if (!response.ok) {
889
+ const error = await response.json().catch(() => ({ message: response.statusText }));
890
+ throw new ObjectStackError(
891
+ error.message || `Upload failed with status ${response.status}`,
892
+ "UPLOAD_ERROR",
893
+ response.status
894
+ );
895
+ }
896
+ return response.json();
897
+ }
898
+ /**
899
+ * Get authorization headers from the adapter config.
900
+ */
901
+ getAuthHeaders() {
902
+ const headers = {};
903
+ if (this.token) {
904
+ headers["Authorization"] = `Bearer ${this.token}`;
905
+ }
906
+ return headers;
907
+ }
748
908
  };
749
909
  function createObjectStackAdapter(config) {
750
910
  return new ObjectStackAdapter(config);
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@object-ui/data-objectstack",
3
- "version": "0.5.0",
3
+ "version": "2.0.0",
4
4
  "description": "ObjectStack Data Adapter for Object UI",
5
5
  "license": "MIT",
6
6
  "type": "module",
@@ -20,14 +20,14 @@
20
20
  "README.md"
21
21
  ],
22
22
  "dependencies": {
23
- "@objectstack/client": "^0.9.1",
24
- "@object-ui/core": "0.5.0",
25
- "@object-ui/types": "0.5.0"
23
+ "@objectstack/client": "^2.0.7",
24
+ "@object-ui/core": "2.0.0",
25
+ "@object-ui/types": "2.0.0"
26
26
  },
27
27
  "devDependencies": {
28
28
  "tsup": "^8.0.1",
29
29
  "typescript": "^5.3.3",
30
- "vitest": "^1.2.0"
30
+ "vitest": "^4.0.18"
31
31
  },
32
32
  "publishConfig": {
33
33
  "access": "public"
@@ -98,3 +98,44 @@ describe('Batch Progress Events', () => {
98
98
  unsubscribe();
99
99
  });
100
100
  });
101
+
102
+ describe('getDiscovery', () => {
103
+ it('should return discoveryInfo from the underlying client after connect', async () => {
104
+ const mockDiscovery = {
105
+ name: 'test-server',
106
+ version: '1.0.0',
107
+ services: {
108
+ auth: { enabled: false, status: 'unavailable' },
109
+ data: { enabled: true, status: 'available' },
110
+ },
111
+ };
112
+
113
+ const adapter = new ObjectStackAdapter({
114
+ baseUrl: 'http://localhost:3000',
115
+ autoReconnect: false,
116
+ });
117
+
118
+ // Mock the underlying client's connect method and discoveryInfo property
119
+ const client = adapter.getClient();
120
+ vi.spyOn(client, 'connect').mockResolvedValue(mockDiscovery as any);
121
+ // Simulate what connect() does: sets discoveryInfo
122
+ (client as any).discoveryInfo = mockDiscovery;
123
+
124
+ const discovery = await adapter.getDiscovery();
125
+ expect(discovery).toEqual(mockDiscovery);
126
+ expect((discovery as any)?.services?.auth?.enabled).toBe(false);
127
+ });
128
+
129
+ it('should return null when connection fails', async () => {
130
+ const adapter = new ObjectStackAdapter({
131
+ baseUrl: 'http://localhost:3000',
132
+ autoReconnect: false,
133
+ });
134
+
135
+ const client = adapter.getClient();
136
+ vi.spyOn(client, 'connect').mockRejectedValue(new Error('Connection failed'));
137
+
138
+ const discovery = await adapter.getDiscovery();
139
+ expect(discovery).toBeNull();
140
+ });
141
+ });
@@ -270,7 +270,7 @@ describe('Error Helpers', () => {
270
270
 
271
271
  expect(error).toBeInstanceOf(MetadataNotFoundError);
272
272
  expect(error.statusCode).toBe(404);
273
- expect((error as MetadataNotFoundError).details.objectName).toBe('users');
273
+ expect((error as MetadataNotFoundError).details?.objectName).toBe('users');
274
274
  });
275
275
 
276
276
  it('should create generic error for 404 without metadata context', () => {
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
  /**
@@ -279,8 +286,8 @@ export class ObjectStackAdapter<T = unknown> implements DataSource<T> {
279
286
  await this.connect();
280
287
 
281
288
  try {
282
- const record = await this.client.data.get<T>(resource, String(id));
283
- return record;
289
+ const result = await this.client.data.get<T>(resource, String(id));
290
+ return result.record;
284
291
  } catch (error: unknown) {
285
292
  // If record not found, return null instead of throwing
286
293
  if ((error as Record<string, unknown>)?.status === 404) {
@@ -295,7 +302,8 @@ export class ObjectStackAdapter<T = unknown> implements DataSource<T> {
295
302
  */
296
303
  async create(resource: string, data: Partial<T>): Promise<T> {
297
304
  await this.connect();
298
- return this.client.data.create<T>(resource, data);
305
+ const result = await this.client.data.create<T>(resource, data);
306
+ return result.record;
299
307
  }
300
308
 
301
309
  /**
@@ -303,7 +311,8 @@ export class ObjectStackAdapter<T = unknown> implements DataSource<T> {
303
311
  */
304
312
  async update(resource: string, id: string | number, data: Partial<T>): Promise<T> {
305
313
  await this.connect();
306
- return this.client.data.update<T>(resource, String(id), data);
314
+ const result = await this.client.data.update<T>(resource, String(id), data);
315
+ return result.record;
307
316
  }
308
317
 
309
318
  /**
@@ -312,7 +321,7 @@ export class ObjectStackAdapter<T = unknown> implements DataSource<T> {
312
321
  async delete(resource: string, id: string | number): Promise<boolean> {
313
322
  await this.connect();
314
323
  const result = await this.client.data.delete(resource, String(id));
315
- return result.success;
324
+ return result.deleted;
316
325
  }
317
326
 
318
327
  /**
@@ -416,7 +425,7 @@ export class ObjectStackAdapter<T = unknown> implements DataSource<T> {
416
425
 
417
426
  try {
418
427
  const result = await this.client.data.update<T>(resource, String(id), item);
419
- results.push(result);
428
+ results.push(result.record);
420
429
  completed++;
421
430
  emitProgress();
422
431
  } catch (error: unknown) {
@@ -582,6 +591,82 @@ export class ObjectStackAdapter<T = unknown> implements DataSource<T> {
582
591
  return this.client;
583
592
  }
584
593
 
594
+ /**
595
+ * Get the discovery information from the connected server.
596
+ * Returns the capabilities and service status of the ObjectStack server.
597
+ *
598
+ * Note: This accesses an internal property of the ObjectStackClient.
599
+ * The discovery data is populated during client.connect() and cached.
600
+ *
601
+ * @returns Promise resolving to discovery data, or null if not connected
602
+ */
603
+ async getDiscovery(): Promise<unknown | null> {
604
+ try {
605
+ // Ensure we're connected first
606
+ await this.connect();
607
+
608
+ // Access discovery data from the client
609
+ // The ObjectStackClient caches discovery during connect()
610
+ // This is an internal property, but documented for this use case
611
+ // @ts-expect-error - Accessing internal discoveryInfo property
612
+ return this.client.discoveryInfo || null;
613
+ } catch {
614
+ return null;
615
+ }
616
+ }
617
+
618
+ /**
619
+ * Get a view definition for an object.
620
+ * Attempts to fetch from the server metadata API.
621
+ * Falls back to null if the server doesn't provide view definitions,
622
+ * allowing the consumer to use static config.
623
+ *
624
+ * @param objectName - Object name
625
+ * @param viewId - View identifier
626
+ * @returns Promise resolving to the view definition or null
627
+ */
628
+ async getView(objectName: string, viewId: string): Promise<unknown | null> {
629
+ await this.connect();
630
+
631
+ try {
632
+ const cacheKey = `view:${objectName}:${viewId}`;
633
+ return await this.metadataCache.get(cacheKey, async () => {
634
+ // Try meta.getItem for view metadata
635
+ const result: any = await this.client.meta.getItem(objectName, `views/${viewId}`);
636
+ if (result && result.item) return result.item;
637
+ return result ?? null;
638
+ });
639
+ } catch {
640
+ // Server doesn't support view metadata — return null to fall back to static config
641
+ return null;
642
+ }
643
+ }
644
+
645
+ /**
646
+ * Get an application definition by name or ID.
647
+ * Attempts to fetch from the server metadata API.
648
+ * Falls back to null if the server doesn't provide app definitions,
649
+ * allowing the consumer to use static config.
650
+ *
651
+ * @param appId - Application identifier
652
+ * @returns Promise resolving to the app definition or null
653
+ */
654
+ async getApp(appId: string): Promise<unknown | null> {
655
+ await this.connect();
656
+
657
+ try {
658
+ const cacheKey = `app:${appId}`;
659
+ return await this.metadataCache.get(cacheKey, async () => {
660
+ const result: any = await this.client.meta.getItem('apps', appId);
661
+ if (result && result.item) return result.item;
662
+ return result ?? null;
663
+ });
664
+ } catch {
665
+ // Server doesn't support app metadata — return null to fall back to static config
666
+ return null;
667
+ }
668
+ }
669
+
585
670
  /**
586
671
  * Get cache statistics for monitoring performance.
587
672
  */
@@ -604,6 +689,131 @@ export class ObjectStackAdapter<T = unknown> implements DataSource<T> {
604
689
  clearCache(): void {
605
690
  this.metadataCache.clear();
606
691
  }
692
+
693
+ /**
694
+ * Upload a single file to a resource.
695
+ * Posts the file as multipart/form-data to the ObjectStack server.
696
+ *
697
+ * @param resource - The resource/object name to attach the file to
698
+ * @param file - File object or Blob to upload
699
+ * @param options - Additional upload options (recordId, fieldName, metadata)
700
+ * @returns Promise resolving to the upload result (file URL, metadata)
701
+ */
702
+ async uploadFile(
703
+ resource: string,
704
+ file: File | Blob,
705
+ options?: {
706
+ recordId?: string;
707
+ fieldName?: string;
708
+ metadata?: Record<string, unknown>;
709
+ onProgress?: (percent: number) => void;
710
+ },
711
+ ): Promise<FileUploadResult> {
712
+ await this.connect();
713
+
714
+ const formData = new FormData();
715
+ formData.append('file', file);
716
+
717
+ if (options?.recordId) {
718
+ formData.append('recordId', options.recordId);
719
+ }
720
+ if (options?.fieldName) {
721
+ formData.append('fieldName', options.fieldName);
722
+ }
723
+ if (options?.metadata) {
724
+ formData.append('metadata', JSON.stringify(options.metadata));
725
+ }
726
+
727
+ const url = `${this.baseUrl}/api/data/${encodeURIComponent(resource)}/upload`;
728
+
729
+ const response = await fetch(url, {
730
+ method: 'POST',
731
+ body: formData,
732
+ headers: {
733
+ ...(this.getAuthHeaders()),
734
+ },
735
+ });
736
+
737
+ if (!response.ok) {
738
+ const error = await response.json().catch(() => ({ message: response.statusText }));
739
+ throw new ObjectStackError(
740
+ error.message || `Upload failed with status ${response.status}`,
741
+ 'UPLOAD_ERROR',
742
+ response.status,
743
+ );
744
+ }
745
+
746
+ return response.json();
747
+ }
748
+
749
+ /**
750
+ * Upload multiple files to a resource.
751
+ * Posts all files as a single multipart/form-data request.
752
+ *
753
+ * @param resource - The resource/object name to attach the files to
754
+ * @param files - Array of File objects or Blobs to upload
755
+ * @param options - Additional upload options
756
+ * @returns Promise resolving to array of upload results
757
+ */
758
+ async uploadFiles(
759
+ resource: string,
760
+ files: (File | Blob)[],
761
+ options?: {
762
+ recordId?: string;
763
+ fieldName?: string;
764
+ metadata?: Record<string, unknown>;
765
+ onProgress?: (percent: number) => void;
766
+ },
767
+ ): Promise<FileUploadResult[]> {
768
+ await this.connect();
769
+
770
+ const formData = new FormData();
771
+ files.forEach((file, idx) => {
772
+ formData.append(`files`, file, (file as File).name || `file-${idx}`);
773
+ });
774
+
775
+ if (options?.recordId) {
776
+ formData.append('recordId', options.recordId);
777
+ }
778
+ if (options?.fieldName) {
779
+ formData.append('fieldName', options.fieldName);
780
+ }
781
+ if (options?.metadata) {
782
+ formData.append('metadata', JSON.stringify(options.metadata));
783
+ }
784
+
785
+ const url = `${this.baseUrl}/api/data/${encodeURIComponent(resource)}/upload`;
786
+
787
+ const response = await fetch(url, {
788
+ method: 'POST',
789
+ body: formData,
790
+ headers: {
791
+ ...(this.getAuthHeaders()),
792
+ },
793
+ });
794
+
795
+ if (!response.ok) {
796
+ const error = await response.json().catch(() => ({ message: response.statusText }));
797
+ throw new ObjectStackError(
798
+ error.message || `Upload failed with status ${response.status}`,
799
+ 'UPLOAD_ERROR',
800
+ response.status,
801
+ );
802
+ }
803
+
804
+ return response.json();
805
+ }
806
+
807
+ /**
808
+ * Get authorization headers from the adapter config.
809
+ */
810
+ private getAuthHeaders(): Record<string, string> {
811
+ const headers: Record<string, string> = {};
812
+ if (this.token) {
813
+ headers['Authorization'] = `Bearer ${this.token}`;
814
+ }
815
+ return headers;
816
+ }
607
817
  }
608
818
 
609
819
  /**
@@ -0,0 +1,112 @@
1
+ /**
2
+ * Tests for ObjectStackAdapter file upload integration
3
+ */
4
+ import { describe, it, expect, vi, beforeEach } from 'vitest';
5
+ import { ObjectStackAdapter } from './index';
6
+
7
+ describe('ObjectStackAdapter File Upload', () => {
8
+ let adapter: ObjectStackAdapter;
9
+
10
+ beforeEach(() => {
11
+ adapter = new ObjectStackAdapter({
12
+ baseUrl: 'http://localhost:3000',
13
+ autoReconnect: false,
14
+ });
15
+ vi.clearAllMocks();
16
+ });
17
+
18
+ describe('uploadFile', () => {
19
+ it('should be a method on the adapter', () => {
20
+ expect(typeof adapter.uploadFile).toBe('function');
21
+ });
22
+
23
+ it('should call fetch with multipart form data when connected', async () => {
24
+ const mockResponse = {
25
+ ok: true,
26
+ json: vi.fn().mockResolvedValue({
27
+ id: 'file-1',
28
+ filename: 'test.pdf',
29
+ mimeType: 'application/pdf',
30
+ size: 1024,
31
+ url: 'http://localhost:3000/files/file-1',
32
+ }),
33
+ };
34
+
35
+ global.fetch = vi.fn().mockResolvedValue(mockResponse);
36
+
37
+ // Manually set connected state by accessing private field
38
+ (adapter as any).connected = true;
39
+ (adapter as any).connectionState = 'connected';
40
+
41
+ const file = new File(['test content'], 'test.pdf', { type: 'application/pdf' });
42
+
43
+ const result = await adapter.uploadFile('documents', file, {
44
+ recordId: 'rec-123',
45
+ fieldName: 'attachment',
46
+ });
47
+
48
+ expect(global.fetch).toHaveBeenCalledWith(
49
+ expect.stringContaining('/api/data/documents/upload'),
50
+ expect.objectContaining({
51
+ method: 'POST',
52
+ body: expect.any(FormData),
53
+ }),
54
+ );
55
+
56
+ expect(result.id).toBe('file-1');
57
+ expect(result.filename).toBe('test.pdf');
58
+ });
59
+
60
+ it('should throw on upload failure', async () => {
61
+ const mockResponse = {
62
+ ok: false,
63
+ status: 413,
64
+ statusText: 'Payload Too Large',
65
+ json: vi.fn().mockResolvedValue({ message: 'File too large' }),
66
+ };
67
+
68
+ global.fetch = vi.fn().mockResolvedValue(mockResponse);
69
+
70
+ // Manually set connected state
71
+ (adapter as any).connected = true;
72
+ (adapter as any).connectionState = 'connected';
73
+
74
+ const file = new File(['test'], 'large.bin', { type: 'application/octet-stream' });
75
+
76
+ await expect(adapter.uploadFile('documents', file)).rejects.toThrow('File too large');
77
+ });
78
+ });
79
+
80
+ describe('uploadFiles', () => {
81
+ it('should be a method on the adapter', () => {
82
+ expect(typeof adapter.uploadFiles).toBe('function');
83
+ });
84
+
85
+ it('should upload multiple files', async () => {
86
+ const mockResponse = {
87
+ ok: true,
88
+ json: vi.fn().mockResolvedValue([
89
+ { id: 'file-1', filename: 'a.pdf', mimeType: 'application/pdf', size: 100, url: '/files/1' },
90
+ { id: 'file-2', filename: 'b.pdf', mimeType: 'application/pdf', size: 200, url: '/files/2' },
91
+ ]),
92
+ };
93
+
94
+ global.fetch = vi.fn().mockResolvedValue(mockResponse);
95
+
96
+ // Manually set connected state
97
+ (adapter as any).connected = true;
98
+ (adapter as any).connectionState = 'connected';
99
+
100
+ const files = [
101
+ new File(['content1'], 'a.pdf', { type: 'application/pdf' }),
102
+ new File(['content2'], 'b.pdf', { type: 'application/pdf' }),
103
+ ];
104
+
105
+ const results = await adapter.uploadFiles('documents', files);
106
+
107
+ expect(results).toHaveLength(2);
108
+ expect(results[0].id).toBe('file-1');
109
+ expect(results[1].id).toBe('file-2');
110
+ });
111
+ });
112
+ });