@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 +166 -6
- package/dist/index.d.cts +70 -1
- package/dist/index.d.ts +70 -1
- package/dist/index.js +166 -6
- package/package.json +5 -5
- package/src/connection.test.ts +41 -0
- package/src/errors.test.ts +1 -1
- package/src/index.ts +217 -7
- package/src/upload.test.ts +112 -0
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
|
|
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
|
-
|
|
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
|
-
|
|
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.
|
|
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
|
|
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
|
-
|
|
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
|
-
|
|
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.
|
|
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.
|
|
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.
|
|
24
|
-
"@object-ui/core": "0.
|
|
25
|
-
"@object-ui/types": "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": "^
|
|
30
|
+
"vitest": "^4.0.18"
|
|
31
31
|
},
|
|
32
32
|
"publishConfig": {
|
|
33
33
|
"access": "public"
|
package/src/connection.test.ts
CHANGED
|
@@ -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
|
+
});
|
package/src/errors.test.ts
CHANGED
|
@@ -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
|
|
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
|
|
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
|
-
|
|
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
|
-
|
|
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.
|
|
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
|
+
});
|