@object-ui/data-objectstack 11.2.0 → 11.4.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/CHANGELOG.md +46 -0
- package/dist/index.cjs +299 -46
- package/dist/index.d.cts +76 -2
- package/dist/index.d.ts +76 -2
- package/dist/index.js +299 -46
- package/package.json +4 -4
- package/src/exportDownload.test.ts +93 -0
- package/src/getObjectSchema.test.ts +36 -0
- package/src/importJob.test.ts +71 -0
- package/src/index.ts +312 -9
- package/src/metadata-client.test.ts +30 -0
- package/src/metadata-client.ts +37 -7
- package/src/onMutation.test.ts +125 -0
- package/src/userState.test.ts +53 -0
- package/src/userState.ts +64 -37
package/dist/index.d.ts
CHANGED
|
@@ -1,5 +1,5 @@
|
|
|
1
1
|
import { ObjectStackClient } from '@objectstack/client';
|
|
2
|
-
import { DataSource, QueryParams, QueryResult, FileUploadResult } from '@object-ui/types';
|
|
2
|
+
import { DataSource, QueryParams, QueryResult, MutationEvent, ImportRequestOptions, ImportRecordsResult, CreateImportJobResult, ImportJobProgressInfo, ImportJobResultsInfo, ListImportJobsOptions, ImportJobSummaryInfo, ImportJobUndoResult, ExportDownloadRequest, FileUploadResult } from '@object-ui/types';
|
|
3
3
|
export { FileUploadResult } from '@object-ui/types';
|
|
4
4
|
|
|
5
5
|
/**
|
|
@@ -764,10 +764,19 @@ interface MetadataHistoryOptions {
|
|
|
764
764
|
/** Limit the number of events returned. */
|
|
765
765
|
limit?: number;
|
|
766
766
|
}
|
|
767
|
+
/** A single field-anchored spec-validation issue (server `error.details.issues`). */
|
|
768
|
+
interface MetadataValidationIssue {
|
|
769
|
+
/** Dot-path to the offending field, e.g. `fields.amount.type` (`''` = root). */
|
|
770
|
+
path: string;
|
|
771
|
+
message: string;
|
|
772
|
+
code?: string;
|
|
773
|
+
}
|
|
767
774
|
interface MetadataError extends Error {
|
|
768
775
|
status: number;
|
|
769
776
|
code?: string;
|
|
770
777
|
body?: unknown;
|
|
778
|
+
/** Structured spec-validation issues, when the failure was a validation error. */
|
|
779
|
+
issues?: MetadataValidationIssue[];
|
|
771
780
|
}
|
|
772
781
|
/**
|
|
773
782
|
* MetadataClient — read/write protocol metadata via the framework REST API.
|
|
@@ -1302,6 +1311,7 @@ declare class ObjectStackAdapter<T = unknown> implements DataSource<T> {
|
|
|
1302
1311
|
private fetchImpl;
|
|
1303
1312
|
private inflightFinds;
|
|
1304
1313
|
private missingResources;
|
|
1314
|
+
private mutationListeners;
|
|
1305
1315
|
constructor(config: {
|
|
1306
1316
|
baseUrl: string;
|
|
1307
1317
|
token?: string;
|
|
@@ -1359,6 +1369,18 @@ declare class ObjectStackAdapter<T = unknown> implements DataSource<T> {
|
|
|
1359
1369
|
/**
|
|
1360
1370
|
* Create a new record.
|
|
1361
1371
|
*/
|
|
1372
|
+
/**
|
|
1373
|
+
* Notify all mutation subscribers. A throwing listener must not break the
|
|
1374
|
+
* mutation or starve the other subscribers, so each is isolated.
|
|
1375
|
+
*/
|
|
1376
|
+
private emitMutation;
|
|
1377
|
+
/**
|
|
1378
|
+
* Subscribe to create/update/delete events on any resource. Returns an
|
|
1379
|
+
* unsubscribe function. Data-bound views use this to auto-refresh after a
|
|
1380
|
+
* mutation (e.g. inline-edit "Save All", which writes through `update` and
|
|
1381
|
+
* must repaint the list without a manual reload).
|
|
1382
|
+
*/
|
|
1383
|
+
onMutation(callback: (event: MutationEvent<T>) => void): () => void;
|
|
1362
1384
|
create(resource: string, data: Partial<T>): Promise<T>;
|
|
1363
1385
|
/**
|
|
1364
1386
|
* Update an existing record.
|
|
@@ -1432,6 +1454,47 @@ declare class ObjectStackAdapter<T = unknown> implements DataSource<T> {
|
|
|
1432
1454
|
results: any[];
|
|
1433
1455
|
}>;
|
|
1434
1456
|
bulk(resource: string, operation: 'create' | 'update' | 'delete', data: Partial<T>[]): Promise<T[]>;
|
|
1457
|
+
/**
|
|
1458
|
+
* Bulk-import raw spreadsheet rows in a single server round-trip via
|
|
1459
|
+
* `POST /api/v1/data/:object/import`. The server performs all value coercion
|
|
1460
|
+
* (booleans, numbers, dates→ISO, select label→code, lookup name→id) from the
|
|
1461
|
+
* object's field metadata, so this method forwards the request verbatim and
|
|
1462
|
+
* returns the aggregate + per-row result untouched.
|
|
1463
|
+
*
|
|
1464
|
+
* Requires `@objectstack/client` with `data.import` (server `/import` route).
|
|
1465
|
+
* Callers should feature-detect (`typeof dataSource.importRecords`) and fall
|
|
1466
|
+
* back to a per-row `create` loop when unavailable.
|
|
1467
|
+
*/
|
|
1468
|
+
importRecords(resource: string, request: ImportRequestOptions): Promise<ImportRecordsResult>;
|
|
1469
|
+
/**
|
|
1470
|
+
* Feature-detect the async import-job API on the connected client. Older
|
|
1471
|
+
* clients/servers lack these routes; callers fall back to {@link importRecords}.
|
|
1472
|
+
*/
|
|
1473
|
+
private importJobApi;
|
|
1474
|
+
/**
|
|
1475
|
+
* Start an asynchronous import job — the large-file counterpart to
|
|
1476
|
+
* {@link importRecords}. Posts the whole payload once; the server processes
|
|
1477
|
+
* rows in the background. Requires an `@objectstack/client` new enough to
|
|
1478
|
+
* expose `data.createImportJob` (server `/import/jobs` route). Callers should
|
|
1479
|
+
* feature-detect (`typeof dataSource.createImportJob`) and fall back to the
|
|
1480
|
+
* synchronous path when unavailable.
|
|
1481
|
+
*/
|
|
1482
|
+
createImportJob(resource: string, request: ImportRequestOptions): Promise<CreateImportJobResult>;
|
|
1483
|
+
/** Poll an import job's progress. Requires {@link createImportJob} support. */
|
|
1484
|
+
getImportJobProgress(jobId: string): Promise<ImportJobProgressInfo>;
|
|
1485
|
+
/** Fetch an import job's capped per-row results. */
|
|
1486
|
+
getImportJobResults(jobId: string): Promise<ImportJobResultsInfo>;
|
|
1487
|
+
/** List recent import jobs (history), newest first. */
|
|
1488
|
+
listImportJobs(options?: ListImportJobsOptions): Promise<ImportJobSummaryInfo[]>;
|
|
1489
|
+
/** Cancel a pending/running import job (cooperative). */
|
|
1490
|
+
cancelImportJob(jobId: string): Promise<void>;
|
|
1491
|
+
/**
|
|
1492
|
+
* Logically roll back a finished import job — delete the records it created
|
|
1493
|
+
* and restore the records it updated to their pre-import values. Requires an
|
|
1494
|
+
* `@objectstack/client` new enough to expose `data.undoImportJob`, and a job
|
|
1495
|
+
* the server captured an undo log for (see {@link ImportJobProgressInfo.undoable}).
|
|
1496
|
+
*/
|
|
1497
|
+
undoImportJob(jobId: string): Promise<ImportJobUndoResult>;
|
|
1435
1498
|
/**
|
|
1436
1499
|
* Normalize the result from data.find() or data.query() into a consistent QueryResult.
|
|
1437
1500
|
*/
|
|
@@ -1443,6 +1506,17 @@ declare class ObjectStackAdapter<T = unknown> implements DataSource<T> {
|
|
|
1443
1506
|
* to findData({ object, query: req.query }) which processes `populate`.
|
|
1444
1507
|
*/
|
|
1445
1508
|
private rawFindWithPopulate;
|
|
1509
|
+
/**
|
|
1510
|
+
* Synchronously download a server-streamed export (csv / json / xlsx).
|
|
1511
|
+
*
|
|
1512
|
+
* Hits `GET /api/v1/data/:object/export`, which streams matching rows in the
|
|
1513
|
+
* requested format, formats values for readability (lookup → name, select →
|
|
1514
|
+
* label, boolean → 是/否, dates formatted) and enforces permissions. The
|
|
1515
|
+
* filter / sort are translated the same way as `rawFindWithPopulate` so the
|
|
1516
|
+
* exported file mirrors the active list view. Returns the file as a Blob;
|
|
1517
|
+
* the caller triggers the browser download.
|
|
1518
|
+
*/
|
|
1519
|
+
exportDownload(resource: string, request?: ExportDownloadRequest): Promise<Blob>;
|
|
1446
1520
|
/**
|
|
1447
1521
|
* Convert ObjectUI QueryParams to ObjectStack QueryOptions.
|
|
1448
1522
|
* Maps OData-style conventions to ObjectStack conventions.
|
|
@@ -1770,4 +1844,4 @@ declare function createObjectStackAdapter<T = unknown>(config: {
|
|
|
1770
1844
|
reconnectDelay?: number;
|
|
1771
1845
|
}): DataSource<T>;
|
|
1772
1846
|
|
|
1773
|
-
export { type AuditEventType, type AuditLogConfig, type AuditLogEntry, AuthenticationError, type BatchProgressEvent, type BatchProgressListener, BulkOperationError, type CSPConfig, type CacheStats, type CloudDeploymentConfig, type CloudHostingConfig, type CloudMarketplaceEntry, CloudOperations, ConcurrentUpdateError, ConnectionError, type ConnectionState, type ConnectionStateEvent, type ConnectionStateListener, type ContractValidationError, type ContractValidationResult, type DataMaskingConfig, type DataMaskingRule, type EmailIntegrationConfig, type IntegrationConfig, IntegrationManager, type IntegrationProvider, type IntegrationTrigger, type MetadataAuditEntry, type MetadataAuditResponse, MetadataClient, type MetadataClientConfig, type MetadataDeleteOptions, type MetadataDiagnostics, type MetadataDiagnosticsEntry, type MetadataDiagnosticsOptions, type MetadataDiagnosticsSummary, type MetadataDraftHeader, type MetadataError, type MetadataGetOptions, type MetadataHistoryOptions, type MetadataLayered, type MetadataListOptions, MetadataNotFoundError, type MetadataReference, type MetadataSaveOptions, ObjectStackAdapter, ObjectStackError, type ObjectStackUserStateAdapterOptions, type PluginAPIContract, type PluginContract, type PluginExport, SecurityManager, type SecurityPolicy, type SlackIntegrationConfig, type StudioCanvasConfig, type StudioColorPalette, type StudioPropertyEditor, type StudioShadowPreset, type StudioThemeBuilderConfig, type StudioTypographyPreset, type UserDataAdapter, ValidationError, type WebhookIntegrationConfig, calculateAutoLayout, clearSharedDiscoveryCache, createDefaultCanvasConfig, createErrorFromResponse, createObjectStackAdapter, createObjectStackUserStateAdapter, generateContractManifest, getSharedDiscovery, is404Error, isConcurrentUpdateError, isErrorType, isObjectStackError, normaliseClientError, snapToGrid, validatePluginContract };
|
|
1847
|
+
export { type AuditEventType, type AuditLogConfig, type AuditLogEntry, AuthenticationError, type BatchProgressEvent, type BatchProgressListener, BulkOperationError, type CSPConfig, type CacheStats, type CloudDeploymentConfig, type CloudHostingConfig, type CloudMarketplaceEntry, CloudOperations, ConcurrentUpdateError, ConnectionError, type ConnectionState, type ConnectionStateEvent, type ConnectionStateListener, type ContractValidationError, type ContractValidationResult, type DataMaskingConfig, type DataMaskingRule, type EmailIntegrationConfig, type IntegrationConfig, IntegrationManager, type IntegrationProvider, type IntegrationTrigger, type MetadataAuditEntry, type MetadataAuditResponse, MetadataClient, type MetadataClientConfig, type MetadataDeleteOptions, type MetadataDiagnostics, type MetadataDiagnosticsEntry, type MetadataDiagnosticsOptions, type MetadataDiagnosticsSummary, type MetadataDraftHeader, type MetadataError, type MetadataGetOptions, type MetadataHistoryOptions, type MetadataLayered, type MetadataListOptions, MetadataNotFoundError, type MetadataReference, type MetadataSaveOptions, type MetadataValidationIssue, ObjectStackAdapter, ObjectStackError, type ObjectStackUserStateAdapterOptions, type PluginAPIContract, type PluginContract, type PluginExport, SecurityManager, type SecurityPolicy, type SlackIntegrationConfig, type StudioCanvasConfig, type StudioColorPalette, type StudioPropertyEditor, type StudioShadowPreset, type StudioThemeBuilderConfig, type StudioTypographyPreset, type UserDataAdapter, ValidationError, type WebhookIntegrationConfig, calculateAutoLayout, clearSharedDiscoveryCache, createDefaultCanvasConfig, createErrorFromResponse, createObjectStackAdapter, createObjectStackUserStateAdapter, generateContractManifest, getSharedDiscovery, is404Error, isConcurrentUpdateError, isErrorType, isObjectStackError, normaliseClientError, snapToGrid, validatePluginContract };
|
package/dist/index.js
CHANGED
|
@@ -537,6 +537,7 @@ function createObjectStackUserStateAdapter(options) {
|
|
|
537
537
|
}
|
|
538
538
|
} = options;
|
|
539
539
|
let cachedRowId = null;
|
|
540
|
+
let saveChain = Promise.resolve();
|
|
540
541
|
const findExisting = async () => {
|
|
541
542
|
const params = {
|
|
542
543
|
$filter: {
|
|
@@ -567,6 +568,42 @@ function createObjectStackUserStateAdapter(options) {
|
|
|
567
568
|
}
|
|
568
569
|
return [];
|
|
569
570
|
};
|
|
571
|
+
const upsert = async (items) => {
|
|
572
|
+
const now = (/* @__PURE__ */ new Date()).toISOString();
|
|
573
|
+
if (cachedRowId !== null) {
|
|
574
|
+
try {
|
|
575
|
+
await dataSource.update(resource, cachedRowId, { value: items, updated_at: now });
|
|
576
|
+
return;
|
|
577
|
+
} catch (updateError) {
|
|
578
|
+
cachedRowId = null;
|
|
579
|
+
onError("save", updateError);
|
|
580
|
+
}
|
|
581
|
+
}
|
|
582
|
+
const existing = await findExisting();
|
|
583
|
+
if (existing && existing.id !== void 0 && existing.id !== null) {
|
|
584
|
+
cachedRowId = existing.id;
|
|
585
|
+
await dataSource.update(resource, existing.id, { value: items, updated_at: now });
|
|
586
|
+
return;
|
|
587
|
+
}
|
|
588
|
+
try {
|
|
589
|
+
const created = await dataSource.create(resource, {
|
|
590
|
+
user_id: userId,
|
|
591
|
+
key,
|
|
592
|
+
value: items,
|
|
593
|
+
updated_at: now
|
|
594
|
+
});
|
|
595
|
+
const newId = created?.id;
|
|
596
|
+
if (newId !== void 0 && newId !== null) cachedRowId = newId;
|
|
597
|
+
} catch (createError) {
|
|
598
|
+
const recovered = await findExisting();
|
|
599
|
+
if (recovered && recovered.id !== void 0 && recovered.id !== null) {
|
|
600
|
+
cachedRowId = recovered.id;
|
|
601
|
+
await dataSource.update(resource, recovered.id, { value: items, updated_at: now });
|
|
602
|
+
return;
|
|
603
|
+
}
|
|
604
|
+
throw createError;
|
|
605
|
+
}
|
|
606
|
+
};
|
|
570
607
|
return {
|
|
571
608
|
async load() {
|
|
572
609
|
try {
|
|
@@ -583,40 +620,11 @@ function createObjectStackUserStateAdapter(options) {
|
|
|
583
620
|
}
|
|
584
621
|
},
|
|
585
622
|
async save(items) {
|
|
586
|
-
|
|
587
|
-
const now = (/* @__PURE__ */ new Date()).toISOString();
|
|
588
|
-
if (cachedRowId !== null) {
|
|
589
|
-
try {
|
|
590
|
-
await dataSource.update(resource, cachedRowId, {
|
|
591
|
-
value: items,
|
|
592
|
-
updated_at: now
|
|
593
|
-
});
|
|
594
|
-
return;
|
|
595
|
-
} catch (updateError) {
|
|
596
|
-
cachedRowId = null;
|
|
597
|
-
onError("save", updateError);
|
|
598
|
-
}
|
|
599
|
-
}
|
|
600
|
-
const existing = await findExisting();
|
|
601
|
-
if (existing && existing.id !== void 0 && existing.id !== null) {
|
|
602
|
-
cachedRowId = existing.id;
|
|
603
|
-
await dataSource.update(resource, existing.id, {
|
|
604
|
-
value: items,
|
|
605
|
-
updated_at: now
|
|
606
|
-
});
|
|
607
|
-
return;
|
|
608
|
-
}
|
|
609
|
-
const created = await dataSource.create(resource, {
|
|
610
|
-
user_id: userId,
|
|
611
|
-
key,
|
|
612
|
-
value: items,
|
|
613
|
-
updated_at: now
|
|
614
|
-
});
|
|
615
|
-
const newId = created?.id;
|
|
616
|
-
if (newId !== void 0 && newId !== null) cachedRowId = newId;
|
|
617
|
-
} catch (error) {
|
|
623
|
+
const run = saveChain.then(() => upsert(items)).catch((error) => {
|
|
618
624
|
onError("save", error);
|
|
619
|
-
}
|
|
625
|
+
});
|
|
626
|
+
saveChain = run;
|
|
627
|
+
return run;
|
|
620
628
|
}
|
|
621
629
|
};
|
|
622
630
|
}
|
|
@@ -752,13 +760,17 @@ async function parseError(res) {
|
|
|
752
760
|
} catch {
|
|
753
761
|
body = await res.text().catch(() => void 0);
|
|
754
762
|
}
|
|
755
|
-
const
|
|
756
|
-
const
|
|
763
|
+
const bodyObj = body && typeof body === "object" ? body : void 0;
|
|
764
|
+
const errNode = bodyObj?.error;
|
|
765
|
+
const message = (errNode && typeof errNode === "object" ? errNode.message : errNode) ?? bodyObj?.message ?? `Metadata request failed: ${res.status} ${res.statusText}`;
|
|
766
|
+
const err = new Error(String(message));
|
|
757
767
|
err.status = res.status;
|
|
758
768
|
err.body = body;
|
|
759
|
-
|
|
760
|
-
|
|
761
|
-
|
|
769
|
+
const details = errNode && typeof errNode === "object" ? errNode.details : void 0;
|
|
770
|
+
const issues = details?.issues ?? (errNode && typeof errNode === "object" ? errNode.issues : void 0) ?? bodyObj?.issues;
|
|
771
|
+
if (Array.isArray(issues)) err.issues = issues;
|
|
772
|
+
const code = details?.code ?? (errNode && typeof errNode === "object" ? errNode.code : void 0) ?? bodyObj?.code;
|
|
773
|
+
if (code != null) err.code = String(code);
|
|
762
774
|
return err;
|
|
763
775
|
}
|
|
764
776
|
var MetadataClient = class _MetadataClient {
|
|
@@ -1468,6 +1480,11 @@ var ObjectStackAdapter = class {
|
|
|
1468
1480
|
// so optional collections like sys_presence don't hammer the server with
|
|
1469
1481
|
// failing requests on every record open / panel render.
|
|
1470
1482
|
__publicField(this, "missingResources", /* @__PURE__ */ new Set());
|
|
1483
|
+
// Subscribers registered via onMutation(). Emitted after each successful
|
|
1484
|
+
// create/update/delete so data-bound views (ListView, ObjectView, kanban,
|
|
1485
|
+
// calendar) auto-refresh — the interface ListView relies on to reflect
|
|
1486
|
+
// inline-edit "Save All" writes without a manual reload.
|
|
1487
|
+
__publicField(this, "mutationListeners", /* @__PURE__ */ new Set());
|
|
1471
1488
|
this.client = new ObjectStackClient({ ...config, logger: createQuietHttpLogger() });
|
|
1472
1489
|
this.metadataCache = new MetadataCache(config.cache);
|
|
1473
1490
|
this.autoReconnect = config.autoReconnect ?? true;
|
|
@@ -1676,9 +1693,35 @@ var ObjectStackAdapter = class {
|
|
|
1676
1693
|
/**
|
|
1677
1694
|
* Create a new record.
|
|
1678
1695
|
*/
|
|
1696
|
+
/**
|
|
1697
|
+
* Notify all mutation subscribers. A throwing listener must not break the
|
|
1698
|
+
* mutation or starve the other subscribers, so each is isolated.
|
|
1699
|
+
*/
|
|
1700
|
+
emitMutation(event) {
|
|
1701
|
+
for (const listener of this.mutationListeners) {
|
|
1702
|
+
try {
|
|
1703
|
+
listener(event);
|
|
1704
|
+
} catch (err) {
|
|
1705
|
+
console.warn("ObjectStackAdapter: mutation listener error", err);
|
|
1706
|
+
}
|
|
1707
|
+
}
|
|
1708
|
+
}
|
|
1709
|
+
/**
|
|
1710
|
+
* Subscribe to create/update/delete events on any resource. Returns an
|
|
1711
|
+
* unsubscribe function. Data-bound views use this to auto-refresh after a
|
|
1712
|
+
* mutation (e.g. inline-edit "Save All", which writes through `update` and
|
|
1713
|
+
* must repaint the list without a manual reload).
|
|
1714
|
+
*/
|
|
1715
|
+
onMutation(callback) {
|
|
1716
|
+
this.mutationListeners.add(callback);
|
|
1717
|
+
return () => {
|
|
1718
|
+
this.mutationListeners.delete(callback);
|
|
1719
|
+
};
|
|
1720
|
+
}
|
|
1679
1721
|
async create(resource, data) {
|
|
1680
1722
|
await this.connect();
|
|
1681
1723
|
const result = await this.client.data.create(resource, data);
|
|
1724
|
+
this.emitMutation({ type: "create", resource, record: { ...result.record } });
|
|
1682
1725
|
return result.record;
|
|
1683
1726
|
}
|
|
1684
1727
|
/**
|
|
@@ -1701,6 +1744,7 @@ var ObjectStackAdapter = class {
|
|
|
1701
1744
|
data,
|
|
1702
1745
|
opts?.ifMatch ? { ifMatch: opts.ifMatch } : void 0
|
|
1703
1746
|
);
|
|
1747
|
+
this.emitMutation({ type: "update", resource, id, record: { ...result.record } });
|
|
1704
1748
|
return result.record;
|
|
1705
1749
|
} catch (err) {
|
|
1706
1750
|
throw normaliseClientError(err);
|
|
@@ -1721,6 +1765,9 @@ var ObjectStackAdapter = class {
|
|
|
1721
1765
|
String(id),
|
|
1722
1766
|
opts?.ifMatch ? { ifMatch: opts.ifMatch } : void 0
|
|
1723
1767
|
);
|
|
1768
|
+
if (result.deleted) {
|
|
1769
|
+
this.emitMutation({ type: "delete", resource, id });
|
|
1770
|
+
}
|
|
1724
1771
|
return result.deleted;
|
|
1725
1772
|
} catch (err) {
|
|
1726
1773
|
throw normaliseClientError(err);
|
|
@@ -1745,15 +1792,19 @@ var ObjectStackAdapter = class {
|
|
|
1745
1792
|
await this.connect();
|
|
1746
1793
|
if (!ids || ids.length === 0) return 0;
|
|
1747
1794
|
const records = ids.map((id) => ({ id: String(id), data: patch }));
|
|
1795
|
+
const emitBulk = (count) => {
|
|
1796
|
+
if (count > 0) this.emitMutation({ type: "update", resource });
|
|
1797
|
+
return count;
|
|
1798
|
+
};
|
|
1748
1799
|
const updateMany = this.client.data.updateMany;
|
|
1749
1800
|
if (typeof updateMany === "function") {
|
|
1750
1801
|
try {
|
|
1751
1802
|
const res = await updateMany(resource, records, { continueOnError: true });
|
|
1752
1803
|
if (res && typeof res === "object" && typeof res.succeeded === "number") {
|
|
1753
|
-
return res.succeeded;
|
|
1804
|
+
return emitBulk(res.succeeded);
|
|
1754
1805
|
}
|
|
1755
|
-
if (Array.isArray(res)) return res.length;
|
|
1756
|
-
return ids.length;
|
|
1806
|
+
if (Array.isArray(res)) return emitBulk(res.length);
|
|
1807
|
+
return emitBulk(ids.length);
|
|
1757
1808
|
} catch (err) {
|
|
1758
1809
|
throw normaliseClientError(err);
|
|
1759
1810
|
}
|
|
@@ -1766,7 +1817,7 @@ var ObjectStackAdapter = class {
|
|
|
1766
1817
|
} catch {
|
|
1767
1818
|
}
|
|
1768
1819
|
}
|
|
1769
|
-
return succeeded;
|
|
1820
|
+
return emitBulk(succeeded);
|
|
1770
1821
|
}
|
|
1771
1822
|
/**
|
|
1772
1823
|
* Single-call bulk delete. Mirrors the bulkUpdate contract: prefers
|
|
@@ -1779,15 +1830,19 @@ var ObjectStackAdapter = class {
|
|
|
1779
1830
|
await this.connect();
|
|
1780
1831
|
if (!ids || ids.length === 0) return 0;
|
|
1781
1832
|
const strIds = ids.map((id) => String(id));
|
|
1833
|
+
const emitBulk = (count) => {
|
|
1834
|
+
if (count > 0) this.emitMutation({ type: "delete", resource });
|
|
1835
|
+
return count;
|
|
1836
|
+
};
|
|
1782
1837
|
const deleteMany = this.client.data.deleteMany;
|
|
1783
1838
|
if (typeof deleteMany === "function") {
|
|
1784
1839
|
try {
|
|
1785
1840
|
const res = await deleteMany(resource, strIds, { continueOnError: true });
|
|
1786
1841
|
if (res && typeof res === "object" && typeof res.succeeded === "number") {
|
|
1787
|
-
return res.succeeded;
|
|
1842
|
+
return emitBulk(res.succeeded);
|
|
1788
1843
|
}
|
|
1789
|
-
if (Array.isArray(res)) return res.length;
|
|
1790
|
-
return strIds.length;
|
|
1844
|
+
if (Array.isArray(res)) return emitBulk(res.length);
|
|
1845
|
+
return emitBulk(strIds.length);
|
|
1791
1846
|
} catch (err) {
|
|
1792
1847
|
throw normaliseClientError(err);
|
|
1793
1848
|
}
|
|
@@ -1800,7 +1855,7 @@ var ObjectStackAdapter = class {
|
|
|
1800
1855
|
} catch {
|
|
1801
1856
|
}
|
|
1802
1857
|
}
|
|
1803
|
-
return succeeded;
|
|
1858
|
+
return emitBulk(succeeded);
|
|
1804
1859
|
}
|
|
1805
1860
|
/**
|
|
1806
1861
|
* Bulk operations with optimized batch processing and error handling.
|
|
@@ -1960,6 +2015,157 @@ var ObjectStackAdapter = class {
|
|
|
1960
2015
|
);
|
|
1961
2016
|
}
|
|
1962
2017
|
}
|
|
2018
|
+
/**
|
|
2019
|
+
* Bulk-import raw spreadsheet rows in a single server round-trip via
|
|
2020
|
+
* `POST /api/v1/data/:object/import`. The server performs all value coercion
|
|
2021
|
+
* (booleans, numbers, dates→ISO, select label→code, lookup name→id) from the
|
|
2022
|
+
* object's field metadata, so this method forwards the request verbatim and
|
|
2023
|
+
* returns the aggregate + per-row result untouched.
|
|
2024
|
+
*
|
|
2025
|
+
* Requires `@objectstack/client` with `data.import` (server `/import` route).
|
|
2026
|
+
* Callers should feature-detect (`typeof dataSource.importRecords`) and fall
|
|
2027
|
+
* back to a per-row `create` loop when unavailable.
|
|
2028
|
+
*/
|
|
2029
|
+
async importRecords(resource, request) {
|
|
2030
|
+
await this.connect();
|
|
2031
|
+
const importFn = this.client.data.import;
|
|
2032
|
+
if (typeof importFn !== "function") {
|
|
2033
|
+
throw new ObjectStackError(
|
|
2034
|
+
"The connected @objectstack/client does not support data.import(). Upgrade the client, or import via a per-row create fallback.",
|
|
2035
|
+
"UNSUPPORTED_OPERATION",
|
|
2036
|
+
400
|
|
2037
|
+
);
|
|
2038
|
+
}
|
|
2039
|
+
try {
|
|
2040
|
+
const result = await importFn.call(this.client.data, resource, request);
|
|
2041
|
+
return result;
|
|
2042
|
+
} catch (err) {
|
|
2043
|
+
throw normaliseClientError(err);
|
|
2044
|
+
}
|
|
2045
|
+
}
|
|
2046
|
+
/**
|
|
2047
|
+
* Feature-detect the async import-job API on the connected client. Older
|
|
2048
|
+
* clients/servers lack these routes; callers fall back to {@link importRecords}.
|
|
2049
|
+
*/
|
|
2050
|
+
importJobApi() {
|
|
2051
|
+
const d = this.client.data;
|
|
2052
|
+
if (typeof d.createImportJob !== "function") return void 0;
|
|
2053
|
+
return d;
|
|
2054
|
+
}
|
|
2055
|
+
/**
|
|
2056
|
+
* Start an asynchronous import job — the large-file counterpart to
|
|
2057
|
+
* {@link importRecords}. Posts the whole payload once; the server processes
|
|
2058
|
+
* rows in the background. Requires an `@objectstack/client` new enough to
|
|
2059
|
+
* expose `data.createImportJob` (server `/import/jobs` route). Callers should
|
|
2060
|
+
* feature-detect (`typeof dataSource.createImportJob`) and fall back to the
|
|
2061
|
+
* synchronous path when unavailable.
|
|
2062
|
+
*/
|
|
2063
|
+
async createImportJob(resource, request) {
|
|
2064
|
+
await this.connect();
|
|
2065
|
+
const api = this.importJobApi();
|
|
2066
|
+
if (!api) {
|
|
2067
|
+
throw new ObjectStackError(
|
|
2068
|
+
"The connected @objectstack/client does not support async import jobs (data.createImportJob). Upgrade the client, or use the synchronous importRecords() path.",
|
|
2069
|
+
"UNSUPPORTED_OPERATION",
|
|
2070
|
+
400
|
|
2071
|
+
);
|
|
2072
|
+
}
|
|
2073
|
+
try {
|
|
2074
|
+
return await api.createImportJob.call(this.client.data, resource, request);
|
|
2075
|
+
} catch (err) {
|
|
2076
|
+
throw normaliseClientError(err);
|
|
2077
|
+
}
|
|
2078
|
+
}
|
|
2079
|
+
/** Poll an import job's progress. Requires {@link createImportJob} support. */
|
|
2080
|
+
async getImportJobProgress(jobId) {
|
|
2081
|
+
await this.connect();
|
|
2082
|
+
const api = this.importJobApi();
|
|
2083
|
+
if (!api) {
|
|
2084
|
+
throw new ObjectStackError(
|
|
2085
|
+
"The connected @objectstack/client does not support async import jobs.",
|
|
2086
|
+
"UNSUPPORTED_OPERATION",
|
|
2087
|
+
400
|
|
2088
|
+
);
|
|
2089
|
+
}
|
|
2090
|
+
try {
|
|
2091
|
+
return await api.getImportJobProgress.call(this.client.data, jobId);
|
|
2092
|
+
} catch (err) {
|
|
2093
|
+
throw normaliseClientError(err);
|
|
2094
|
+
}
|
|
2095
|
+
}
|
|
2096
|
+
/** Fetch an import job's capped per-row results. */
|
|
2097
|
+
async getImportJobResults(jobId) {
|
|
2098
|
+
await this.connect();
|
|
2099
|
+
const api = this.importJobApi();
|
|
2100
|
+
if (!api) {
|
|
2101
|
+
throw new ObjectStackError(
|
|
2102
|
+
"The connected @objectstack/client does not support async import jobs.",
|
|
2103
|
+
"UNSUPPORTED_OPERATION",
|
|
2104
|
+
400
|
|
2105
|
+
);
|
|
2106
|
+
}
|
|
2107
|
+
try {
|
|
2108
|
+
return await api.getImportJobResults.call(this.client.data, jobId);
|
|
2109
|
+
} catch (err) {
|
|
2110
|
+
throw normaliseClientError(err);
|
|
2111
|
+
}
|
|
2112
|
+
}
|
|
2113
|
+
/** List recent import jobs (history), newest first. */
|
|
2114
|
+
async listImportJobs(options = {}) {
|
|
2115
|
+
await this.connect();
|
|
2116
|
+
const api = this.importJobApi();
|
|
2117
|
+
if (!api) {
|
|
2118
|
+
throw new ObjectStackError(
|
|
2119
|
+
"The connected @objectstack/client does not support async import jobs.",
|
|
2120
|
+
"UNSUPPORTED_OPERATION",
|
|
2121
|
+
400
|
|
2122
|
+
);
|
|
2123
|
+
}
|
|
2124
|
+
try {
|
|
2125
|
+
return await api.listImportJobs.call(this.client.data, options);
|
|
2126
|
+
} catch (err) {
|
|
2127
|
+
throw normaliseClientError(err);
|
|
2128
|
+
}
|
|
2129
|
+
}
|
|
2130
|
+
/** Cancel a pending/running import job (cooperative). */
|
|
2131
|
+
async cancelImportJob(jobId) {
|
|
2132
|
+
await this.connect();
|
|
2133
|
+
const api = this.importJobApi();
|
|
2134
|
+
if (!api) {
|
|
2135
|
+
throw new ObjectStackError(
|
|
2136
|
+
"The connected @objectstack/client does not support async import jobs.",
|
|
2137
|
+
"UNSUPPORTED_OPERATION",
|
|
2138
|
+
400
|
|
2139
|
+
);
|
|
2140
|
+
}
|
|
2141
|
+
try {
|
|
2142
|
+
await api.cancelImportJob.call(this.client.data, jobId);
|
|
2143
|
+
} catch (err) {
|
|
2144
|
+
throw normaliseClientError(err);
|
|
2145
|
+
}
|
|
2146
|
+
}
|
|
2147
|
+
/**
|
|
2148
|
+
* Logically roll back a finished import job — delete the records it created
|
|
2149
|
+
* and restore the records it updated to their pre-import values. Requires an
|
|
2150
|
+
* `@objectstack/client` new enough to expose `data.undoImportJob`, and a job
|
|
2151
|
+
* the server captured an undo log for (see {@link ImportJobProgressInfo.undoable}).
|
|
2152
|
+
*/
|
|
2153
|
+
async undoImportJob(jobId) {
|
|
2154
|
+
await this.connect();
|
|
2155
|
+
const api = this.importJobApi();
|
|
2156
|
+
if (!api || typeof api.undoImportJob !== "function") {
|
|
2157
|
+
throw new ObjectStackError(
|
|
2158
|
+
"The connected @objectstack/client does not support undoing import jobs (data.undoImportJob).",
|
|
2159
|
+
"UNSUPPORTED_OPERATION",
|
|
2160
|
+
400
|
|
2161
|
+
);
|
|
2162
|
+
}
|
|
2163
|
+
try {
|
|
2164
|
+
return await api.undoImportJob.call(this.client.data, jobId);
|
|
2165
|
+
} catch (err) {
|
|
2166
|
+
throw normaliseClientError(err);
|
|
2167
|
+
}
|
|
2168
|
+
}
|
|
1963
2169
|
/**
|
|
1964
2170
|
* Normalize the result from data.find() or data.query() into a consistent QueryResult.
|
|
1965
2171
|
*/
|
|
@@ -2057,6 +2263,53 @@ var ObjectStackAdapter = class {
|
|
|
2057
2263
|
}
|
|
2058
2264
|
return body;
|
|
2059
2265
|
}
|
|
2266
|
+
/**
|
|
2267
|
+
* Synchronously download a server-streamed export (csv / json / xlsx).
|
|
2268
|
+
*
|
|
2269
|
+
* Hits `GET /api/v1/data/:object/export`, which streams matching rows in the
|
|
2270
|
+
* requested format, formats values for readability (lookup → name, select →
|
|
2271
|
+
* label, boolean → 是/否, dates formatted) and enforces permissions. The
|
|
2272
|
+
* filter / sort are translated the same way as `rawFindWithPopulate` so the
|
|
2273
|
+
* exported file mirrors the active list view. Returns the file as a Blob;
|
|
2274
|
+
* the caller triggers the browser download.
|
|
2275
|
+
*/
|
|
2276
|
+
async exportDownload(resource, request = {}) {
|
|
2277
|
+
const queryParams = new URLSearchParams();
|
|
2278
|
+
const format = request.format === "xlsx" ? "xlsx" : request.format === "json" ? "json" : "csv";
|
|
2279
|
+
queryParams.set("format", format);
|
|
2280
|
+
if (request.fields && request.fields.length > 0) {
|
|
2281
|
+
queryParams.set("fields", request.fields.join(","));
|
|
2282
|
+
}
|
|
2283
|
+
if (request.limit && request.limit > 0) {
|
|
2284
|
+
queryParams.set("limit", String(request.limit));
|
|
2285
|
+
}
|
|
2286
|
+
if (request.includeHeaders === false) {
|
|
2287
|
+
queryParams.set("header", "false");
|
|
2288
|
+
}
|
|
2289
|
+
if (request.sort && request.sort.length > 0) {
|
|
2290
|
+
const orderby = request.sort.filter((s) => s && s.field).map((s) => `${s.field}:${s.direction === "desc" ? "desc" : "asc"}`).join(",");
|
|
2291
|
+
if (orderby) queryParams.set("orderby", orderby);
|
|
2292
|
+
}
|
|
2293
|
+
if (request.filter !== void 0 && request.filter !== null) {
|
|
2294
|
+
const translated = translateFilterToAST(request.filter);
|
|
2295
|
+
if (translated !== void 0) {
|
|
2296
|
+
queryParams.set("filter", JSON.stringify(translated));
|
|
2297
|
+
}
|
|
2298
|
+
}
|
|
2299
|
+
const baseUrl = this.baseUrl.replace(/\/$/, "");
|
|
2300
|
+
const hasApiVersionSuffix = /\/api\/v\d+$/i.test(baseUrl);
|
|
2301
|
+
const dataPath = hasApiVersionSuffix ? "/data" : "/api/v1/data";
|
|
2302
|
+
const url = `${baseUrl}${dataPath}/${encodeURIComponent(resource)}/export?${queryParams.toString()}`;
|
|
2303
|
+
const headers = { ...this.getAuthHeaders() };
|
|
2304
|
+
const res = await this.fetchImpl(url, { method: "GET", headers, credentials: "include" });
|
|
2305
|
+
if (!res.ok) {
|
|
2306
|
+
const errorBody = await res.json().catch(() => ({ message: res.statusText }));
|
|
2307
|
+
const err = new Error(errorBody?.error?.message || errorBody?.message || res.statusText);
|
|
2308
|
+
err.status = res.status;
|
|
2309
|
+
throw err;
|
|
2310
|
+
}
|
|
2311
|
+
return await res.blob();
|
|
2312
|
+
}
|
|
2060
2313
|
/**
|
|
2061
2314
|
* Convert ObjectUI QueryParams to ObjectStack QueryOptions.
|
|
2062
2315
|
* Maps OData-style conventions to ObjectStack conventions.
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@object-ui/data-objectstack",
|
|
3
|
-
"version": "11.
|
|
3
|
+
"version": "11.4.0",
|
|
4
4
|
"description": "ObjectStack Data Adapter for Object UI",
|
|
5
5
|
"license": "MIT",
|
|
6
6
|
"type": "module",
|
|
@@ -22,9 +22,9 @@
|
|
|
22
22
|
"LICENSE"
|
|
23
23
|
],
|
|
24
24
|
"dependencies": {
|
|
25
|
-
"@objectstack/client": "^11.
|
|
26
|
-
"@object-ui/core": "11.
|
|
27
|
-
"@object-ui/types": "11.
|
|
25
|
+
"@objectstack/client": "^11.5.0",
|
|
26
|
+
"@object-ui/core": "11.4.0",
|
|
27
|
+
"@object-ui/types": "11.4.0"
|
|
28
28
|
},
|
|
29
29
|
"devDependencies": {
|
|
30
30
|
"tsup": "^8.5.1",
|