@objectstack/rest 12.6.0 → 14.3.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 +580 -163
- package/dist/index.cjs.map +1 -1
- package/dist/index.d.cts +383 -2
- package/dist/index.d.ts +383 -2
- package/dist/index.js +571 -161
- package/dist/index.js.map +1 -1
- package/package.json +8 -7
package/dist/index.d.ts
CHANGED
|
@@ -251,6 +251,10 @@ declare class RestServer {
|
|
|
251
251
|
private i18nServiceProvider?;
|
|
252
252
|
private analyticsServiceProvider?;
|
|
253
253
|
private settingsServiceProvider?;
|
|
254
|
+
/** [ADR-0090] `security` service resolver — used by the
|
|
255
|
+
* /security/suggested-bindings (D5/D9) and /security/explain (D6)
|
|
256
|
+
* routes (plugin-security). */
|
|
257
|
+
private securityServiceProvider?;
|
|
254
258
|
/** Sync probe: is a kernel service registered? Single-env path for nav
|
|
255
259
|
* capability gates (ADR-0057 D10) — resolveExecCtx sets no kernel in
|
|
256
260
|
* single-kernel deployments, so this prevents the gate failing open. */
|
|
@@ -262,7 +266,7 @@ declare class RestServer {
|
|
|
262
266
|
* the durable source of truth a restarted/other node reads.
|
|
263
267
|
*/
|
|
264
268
|
private readonly cancelledImportJobs;
|
|
265
|
-
constructor(server: IHttpServer, protocol: ObjectStackProtocol, config?: RestServerConfig, kernelManager?: RestKernelManager, envRegistry?: RestEnvRegistry, defaultEnvironmentIdProvider?: () => string | undefined, authServiceProvider?: (environmentId?: string) => Promise<any | undefined>, objectQLProvider?: (environmentId?: string) => Promise<any | undefined>, emailServiceProvider?: (environmentId?: string) => Promise<any | undefined>, sharingServiceProvider?: (environmentId?: string) => Promise<any | undefined>, reportsServiceProvider?: (environmentId?: string) => Promise<any | undefined>, approvalsServiceProvider?: (environmentId?: string) => Promise<any | undefined>, sharingRulesServiceProvider?: (environmentId?: string) => Promise<any | undefined>, i18nServiceProvider?: (environmentId?: string) => Promise<any | undefined>, analyticsServiceProvider?: (environmentId?: string) => Promise<any | undefined>, settingsServiceProvider?: (environmentId?: string) => Promise<any | undefined>, serviceExistsProvider?: (name: string) => boolean);
|
|
269
|
+
constructor(server: IHttpServer, protocol: ObjectStackProtocol, config?: RestServerConfig, kernelManager?: RestKernelManager, envRegistry?: RestEnvRegistry, defaultEnvironmentIdProvider?: () => string | undefined, authServiceProvider?: (environmentId?: string) => Promise<any | undefined>, objectQLProvider?: (environmentId?: string) => Promise<any | undefined>, emailServiceProvider?: (environmentId?: string) => Promise<any | undefined>, sharingServiceProvider?: (environmentId?: string) => Promise<any | undefined>, reportsServiceProvider?: (environmentId?: string) => Promise<any | undefined>, approvalsServiceProvider?: (environmentId?: string) => Promise<any | undefined>, sharingRulesServiceProvider?: (environmentId?: string) => Promise<any | undefined>, i18nServiceProvider?: (environmentId?: string) => Promise<any | undefined>, analyticsServiceProvider?: (environmentId?: string) => Promise<any | undefined>, settingsServiceProvider?: (environmentId?: string) => Promise<any | undefined>, serviceExistsProvider?: (name: string) => boolean, securityServiceProvider?: (environmentId?: string) => Promise<any | undefined>);
|
|
266
270
|
/**
|
|
267
271
|
* Resolve the protocol for a given request. When `environmentId` is present
|
|
268
272
|
* and a KernelManager is wired, fetch the per-project kernel's
|
|
@@ -350,6 +354,24 @@ declare class RestServer {
|
|
|
350
354
|
* to the protocol layer (the SecurityPlugin treats undefined as anon).
|
|
351
355
|
*/
|
|
352
356
|
private resolveExecCtx;
|
|
357
|
+
/**
|
|
358
|
+
* [ADR-0046 §6.7] The audience-evaluation view of the caller for book/doc
|
|
359
|
+
* gating. `permissionSets` resolves through the security service's
|
|
360
|
+
* `resolvePermissionSetNames` — the SAME resolution as data-plane
|
|
361
|
+
* enforcement (positions expanded, additive baseline), so the docs gate
|
|
362
|
+
* can never drift from it. `permissionSets` stays undefined when the
|
|
363
|
+
* service is absent or resolution fails; `audienceAllows` then DENIES
|
|
364
|
+
* permission-set-gated audiences (fail closed, ADR-0049). Resolution is
|
|
365
|
+
* skipped unless `needPermissionSets` — callers pass true only when a
|
|
366
|
+
* `{ permissionSet }` audience is actually in play.
|
|
367
|
+
*/
|
|
368
|
+
private resolveAudienceCaller;
|
|
369
|
+
/** Whether any of these books carries a `{ permissionSet }` audience. */
|
|
370
|
+
private static anyPermissionSetAudience;
|
|
371
|
+
/** Coerce a getMetaItems result (array | {items}) into an array. */
|
|
372
|
+
private static metaItemsArray;
|
|
373
|
+
/** Fetch every book of the environment, shaped for the audience resolver. */
|
|
374
|
+
private fetchAudienceBooks;
|
|
353
375
|
/** Heavy path behind `resolveExecCtx` — resolve identity + RBAC/RLS + localization. */
|
|
354
376
|
private computeExecCtx;
|
|
355
377
|
/**
|
|
@@ -593,6 +615,26 @@ declare class RestServer {
|
|
|
593
615
|
* `@objectstack/service-analytics` fails cleanly.
|
|
594
616
|
*/
|
|
595
617
|
private registerAnalyticsEndpoints;
|
|
618
|
+
/**
|
|
619
|
+
* [ADR-0090 D6] Access-explanation endpoint — the REST face of the
|
|
620
|
+
* explain engine (framework#2696).
|
|
621
|
+
*
|
|
622
|
+
* GET {basePath}/security/explain?object=…&operation=…&userId=…
|
|
623
|
+
* POST {basePath}/security/explain body: { object, operation, userId? }
|
|
624
|
+
*
|
|
625
|
+
* Delegates to the security service's `explain(request, callerContext)`
|
|
626
|
+
* (`SecurityPlugin.explainAccessForCaller`) — the same code paths the
|
|
627
|
+
* enforcement middleware runs, so the report is explained by
|
|
628
|
+
* construction. Caller authorization lives in the SERVICE, not here:
|
|
629
|
+
* explaining ANOTHER user requires `manage_users` or a delegated
|
|
630
|
+
* `adminScope` covering that user (D12); the service's
|
|
631
|
+
* `PermissionDeniedError` maps to 403. The route itself only insists on
|
|
632
|
+
* an authenticated caller (an access report is sensitive even about
|
|
633
|
+
* oneself, and the anonymous `guest` posture is not this endpoint's
|
|
634
|
+
* business) and returns 501 when no security service exposing `explain`
|
|
635
|
+
* is mounted (a deployment without `@objectstack/plugin-security`).
|
|
636
|
+
*/
|
|
637
|
+
private registerSecurityExplainEndpoints;
|
|
596
638
|
private registerSharingEndpoints;
|
|
597
639
|
/**
|
|
598
640
|
* Register sharing-rule endpoints (M10.17). Mirrors the existing
|
|
@@ -607,6 +649,25 @@ declare class RestServer {
|
|
|
607
649
|
* Returns 501 when no sharing-rule service is configured.
|
|
608
650
|
*/
|
|
609
651
|
private registerSharingRuleEndpoints;
|
|
652
|
+
/**
|
|
653
|
+
* Register the security admin endpoints (ADR-0090 D5/D9) — suggested
|
|
654
|
+
* audience bindings. A package permission set declaring `isDefault: true`
|
|
655
|
+
* is an install-time SUGGESTION to bind it to the `everyone` position;
|
|
656
|
+
* these routes surface pending suggestions and let a tenant admin resolve
|
|
657
|
+
* them. The `security` service (plugin-security) does the real gating:
|
|
658
|
+
* tenant-admin pre-check on all three, and confirm writes the binding
|
|
659
|
+
* with the caller's execution context so the audience-anchor and
|
|
660
|
+
* delegated-admin gates enforce it — never auto-bound, never system.
|
|
661
|
+
*
|
|
662
|
+
* GET {basePath}/security/suggested-bindings?status=&packageId=
|
|
663
|
+
* POST {basePath}/security/suggested-bindings/:id/confirm
|
|
664
|
+
* POST {basePath}/security/suggested-bindings/:id/dismiss
|
|
665
|
+
*
|
|
666
|
+
* Routes return 501 when the `security` service is not registered
|
|
667
|
+
* (deployment without plugin-security). Typed service errors carry their
|
|
668
|
+
* HTTP status (403 permission / 404 not found / 409 state).
|
|
669
|
+
*/
|
|
670
|
+
private registerSecurityEndpoints;
|
|
610
671
|
/**
|
|
611
672
|
* Register saved-report + scheduled-digest endpoints (M11.C16).
|
|
612
673
|
*
|
|
@@ -682,4 +743,324 @@ interface RestApiPluginConfig {
|
|
|
682
743
|
*/
|
|
683
744
|
declare function createRestApiPlugin(config?: RestApiPluginConfig): Plugin;
|
|
684
745
|
|
|
685
|
-
|
|
746
|
+
/**
|
|
747
|
+
* Type-aware value formatting for the streaming data export route
|
|
748
|
+
* (`GET /data/:object/export`).
|
|
749
|
+
*
|
|
750
|
+
* The raw rows returned by `findData` carry *storage* values: lookup / user
|
|
751
|
+
* fields hold ids (or, when `$expand`-ed, nested records), select fields hold
|
|
752
|
+
* option codes, booleans hold true/false, dates hold ISO strings. None of those
|
|
753
|
+
* read well in a spreadsheet. These helpers turn each value into a human
|
|
754
|
+
* readable cell using the object's field metadata.
|
|
755
|
+
*
|
|
756
|
+
* Contract: when no field metadata is available (schema lookup failed or carried
|
|
757
|
+
* no fields) every helper is a pass-through, so the export stays byte-for-byte
|
|
758
|
+
* identical to the un-formatted behaviour.
|
|
759
|
+
*/
|
|
760
|
+
interface ExportFieldMeta {
|
|
761
|
+
name: string;
|
|
762
|
+
type?: string;
|
|
763
|
+
label?: string;
|
|
764
|
+
options?: Array<{
|
|
765
|
+
label?: string;
|
|
766
|
+
value?: unknown;
|
|
767
|
+
color?: string;
|
|
768
|
+
}>;
|
|
769
|
+
/** Target object for lookup / master_detail / user fields. */
|
|
770
|
+
reference?: string;
|
|
771
|
+
/** Field on the referenced record to show as its label. */
|
|
772
|
+
displayField?: string;
|
|
773
|
+
}
|
|
774
|
+
/**
|
|
775
|
+
* Build a field-name → metadata map from an object schema (best-effort).
|
|
776
|
+
*
|
|
777
|
+
* Accepts both shapes `fields` appears in across the stack: the runtime
|
|
778
|
+
* `ObjectSchema.fields` is a `Record<fieldName, FieldDefinition>` object map
|
|
779
|
+
* (the form served by the engine registry / `getMetaItem`), while some callers
|
|
780
|
+
* and fixtures hand back a plain `FieldDefinition[]` array. A field's name is
|
|
781
|
+
* taken from its own `name`, falling back to the map key.
|
|
782
|
+
*/
|
|
783
|
+
declare function buildFieldMetaMap(schema: unknown): Map<string, ExportFieldMeta>;
|
|
784
|
+
|
|
785
|
+
/**
|
|
786
|
+
* Bulk-import request parsing — extracted verbatim from rest-server.ts
|
|
787
|
+
* (#2766 V2) so consumers outside the generic `/data/:object/import` routes
|
|
788
|
+
* (the identity import endpoint in plugin-auth) can accept byte-identical
|
|
789
|
+
* payloads (rows[]/csv/xlsxBase64, mapping in either shape, writeMode +
|
|
790
|
+
* matchFields) without re-implementing the parsers.
|
|
791
|
+
*/
|
|
792
|
+
|
|
793
|
+
/**
|
|
794
|
+
* Detect the `getMetaItem` response envelope (`{ type, name, item, lock, … }`)
|
|
795
|
+
* whose translatable metadata document is nested at `.item`. The cached read
|
|
796
|
+
* path and `getMetaItems` element shape hand back the already-unwrapped
|
|
797
|
+
* document instead, so translation helpers must distinguish the two: an
|
|
798
|
+
* envelope carries a nested `item` object alongside its own `type`/`name`,
|
|
799
|
+
* which a bare metadata document never does.
|
|
800
|
+
*/
|
|
801
|
+
declare function isMetaEnvelope(value: any): boolean;
|
|
802
|
+
/**
|
|
803
|
+
* Minimal RFC-4180-style CSV parser used by the bulk-import endpoint
|
|
804
|
+
* (M10.9). Handles quoted fields (including embedded quotes via "" and
|
|
805
|
+
* embedded commas/newlines) and both CRLF and LF line endings.
|
|
806
|
+
*
|
|
807
|
+
* The first non-empty line is treated as the header row. Header names
|
|
808
|
+
* can be re-mapped to canonical field names via the optional `mapping`
|
|
809
|
+
* argument (e.g. `{ "First Name": "first_name" }`); unmapped headers
|
|
810
|
+
* pass through unchanged. Empty cells become empty strings.
|
|
811
|
+
*
|
|
812
|
+
* Kept dependency-free so REST stays runtime-portable (Hono / Express
|
|
813
|
+
* adapters both consume this without pulling a CSV lib transitively).
|
|
814
|
+
*/
|
|
815
|
+
declare function parseCsvToRows(csv: string, mapping?: Record<string, string>): Array<Record<string, any>>;
|
|
816
|
+
/**
|
|
817
|
+
* Parse an .xlsx workbook (raw bytes) into row objects, mirroring
|
|
818
|
+
* {@link parseCsvToRows}: first non-empty row is the header, each subsequent row
|
|
819
|
+
* becomes `{ header→cell }` with the optional `mapping` renaming columns. Reads
|
|
820
|
+
* the named/indexed `sheet` when given, else the first worksheet. Dynamically
|
|
821
|
+
* imports ExcelJS (already a dependency of the export path) so CSV/JSON imports
|
|
822
|
+
* don't pay for it.
|
|
823
|
+
*/
|
|
824
|
+
declare function parseXlsxToRows(buffer: Buffer | ArrayBuffer, mapping?: Record<string, string>, sheet?: string | number): Promise<Array<Record<string, any>>>;
|
|
825
|
+
/** Everything the import runner needs, parsed & validated from a request body. */
|
|
826
|
+
interface PreparedImport {
|
|
827
|
+
rows: Array<Record<string, any>>;
|
|
828
|
+
metaMap: Map<string, ExportFieldMeta>;
|
|
829
|
+
writeMode: 'insert' | 'update' | 'upsert';
|
|
830
|
+
matchFields: string[];
|
|
831
|
+
dryRun: boolean;
|
|
832
|
+
runAutomations: boolean;
|
|
833
|
+
trimWhitespace: boolean;
|
|
834
|
+
nullValues?: string[];
|
|
835
|
+
createMissingOptions: boolean;
|
|
836
|
+
skipBlankMatchKey: boolean;
|
|
837
|
+
}
|
|
838
|
+
type PrepareImportResult = {
|
|
839
|
+
ok: true;
|
|
840
|
+
prepared: PreparedImport;
|
|
841
|
+
} | {
|
|
842
|
+
ok: false;
|
|
843
|
+
status: number;
|
|
844
|
+
code: string;
|
|
845
|
+
error: string;
|
|
846
|
+
};
|
|
847
|
+
/**
|
|
848
|
+
* Parse & validate a bulk-import request body into a {@link PreparedImport}.
|
|
849
|
+
*
|
|
850
|
+
* Shared by the synchronous `POST /data/:object/import` route and the async
|
|
851
|
+
* import-job create route so both accept byte-identical payloads (writeMode +
|
|
852
|
+
* matchFields, mapping in either shape, rows[]/csv/xlsxBase64) and resolve the
|
|
853
|
+
* same field metadata. The only knob that differs is `maxRows` (5k sync vs
|
|
854
|
+
* 50k async). Returns a discriminated result; the caller maps `!ok` to an HTTP
|
|
855
|
+
* error using the returned status/code/error.
|
|
856
|
+
*/
|
|
857
|
+
declare function prepareImportRequest(body: any, opts: {
|
|
858
|
+
p: any;
|
|
859
|
+
objectName: string;
|
|
860
|
+
environmentId?: string;
|
|
861
|
+
maxRows: number;
|
|
862
|
+
}): Promise<PrepareImportResult>;
|
|
863
|
+
|
|
864
|
+
/**
|
|
865
|
+
* import-runner — the shared row-processing core for bulk import.
|
|
866
|
+
*
|
|
867
|
+
* Both the synchronous `POST /data/:object/import` route and the asynchronous
|
|
868
|
+
* import-job worker feed rows through {@link runImport}. Extracting the loop
|
|
869
|
+
* keeps the two paths byte-for-byte identical in coercion, upsert matching, and
|
|
870
|
+
* per-row reporting — the async worker only adds progress persistence and
|
|
871
|
+
* cancellation on top.
|
|
872
|
+
*
|
|
873
|
+
* Rows resolved to a CREATE are batched through `p.createManyData` (the
|
|
874
|
+
* engine's array-form `insert()` — one round-trip per batch, with transient
|
|
875
|
+
* retry and per-row degradation on a logical/validation failure) instead of
|
|
876
|
+
* one `p.createData` call per row — see framework#2678. A protocol that
|
|
877
|
+
* doesn't implement `createManyData` falls back to the original per-row
|
|
878
|
+
* `createData` path unchanged.
|
|
879
|
+
*/
|
|
880
|
+
type ImportAction = 'created' | 'updated' | 'skipped' | 'failed';
|
|
881
|
+
interface ImportRowResult {
|
|
882
|
+
row: number;
|
|
883
|
+
ok: boolean;
|
|
884
|
+
action: ImportAction;
|
|
885
|
+
id?: string;
|
|
886
|
+
field?: string;
|
|
887
|
+
error?: string;
|
|
888
|
+
code?: string;
|
|
889
|
+
}
|
|
890
|
+
/** Running tallies handed to {@link RunImportOptions.onProgress}. */
|
|
891
|
+
interface ImportProgress {
|
|
892
|
+
processed: number;
|
|
893
|
+
total: number;
|
|
894
|
+
created: number;
|
|
895
|
+
updated: number;
|
|
896
|
+
skipped: number;
|
|
897
|
+
errors: number;
|
|
898
|
+
}
|
|
899
|
+
/**
|
|
900
|
+
* Records exactly what a non-dry-run import changed, so the job can be undone:
|
|
901
|
+
* created records are deleted, and updated records have the touched fields
|
|
902
|
+
* restored to their pre-import values. Only the fields the import wrote are
|
|
903
|
+
* captured (keyed to `before`), keeping the log precise and bounded.
|
|
904
|
+
*/
|
|
905
|
+
interface ImportUndoLog {
|
|
906
|
+
/** Ids of records this import created (delete to undo). */
|
|
907
|
+
created: string[];
|
|
908
|
+
/** Per updated record: the touched fields' values *before* the import. */
|
|
909
|
+
updated: Array<{
|
|
910
|
+
id: string;
|
|
911
|
+
before: Record<string, any>;
|
|
912
|
+
}>;
|
|
913
|
+
}
|
|
914
|
+
interface ImportRunSummary extends ImportProgress {
|
|
915
|
+
ok: number;
|
|
916
|
+
results: ImportRowResult[];
|
|
917
|
+
cancelled: boolean;
|
|
918
|
+
/** Present only when `captureUndo` was set — the reversal instructions. */
|
|
919
|
+
undoLog?: ImportUndoLog;
|
|
920
|
+
}
|
|
921
|
+
/** Minimal protocol surface the runner needs (find / create / update). */
|
|
922
|
+
interface ImportProtocolLike {
|
|
923
|
+
findData(args: any): Promise<any>;
|
|
924
|
+
createData(args: any): Promise<any>;
|
|
925
|
+
updateData(args: any): Promise<any>;
|
|
926
|
+
/**
|
|
927
|
+
* Optional bulk-create primitive. When present, `runImport` batches
|
|
928
|
+
* CREATE-resolved rows through it instead of one `createData` call per
|
|
929
|
+
* row — see framework#2678. Must resolve to `{ records: any[] }` with one
|
|
930
|
+
* record per input row, in the same order.
|
|
931
|
+
*/
|
|
932
|
+
createManyData?(args: {
|
|
933
|
+
object: string;
|
|
934
|
+
records: any[];
|
|
935
|
+
context?: any;
|
|
936
|
+
environmentId?: string;
|
|
937
|
+
}): Promise<{
|
|
938
|
+
records: any[];
|
|
939
|
+
}>;
|
|
940
|
+
}
|
|
941
|
+
interface RunImportOptions {
|
|
942
|
+
/** Protocol/engine to read & write through. */
|
|
943
|
+
p: ImportProtocolLike;
|
|
944
|
+
objectName: string;
|
|
945
|
+
environmentId?: string;
|
|
946
|
+
/** Exec context threaded onto reads and (with automation toggle) writes. */
|
|
947
|
+
context?: any;
|
|
948
|
+
/** Already-mapped rows (source columns renamed to target fields). */
|
|
949
|
+
rows: Array<Record<string, any>>;
|
|
950
|
+
/** Field metadata for value coercion (name→id lookups, select codes, …). */
|
|
951
|
+
metaMap: Map<string, ExportFieldMeta>;
|
|
952
|
+
writeMode: 'insert' | 'update' | 'upsert';
|
|
953
|
+
matchFields: string[];
|
|
954
|
+
dryRun: boolean;
|
|
955
|
+
runAutomations: boolean;
|
|
956
|
+
trimWhitespace: boolean;
|
|
957
|
+
nullValues?: string[];
|
|
958
|
+
createMissingOptions: boolean;
|
|
959
|
+
skipBlankMatchKey: boolean;
|
|
960
|
+
/**
|
|
961
|
+
* Progress callback, invoked every {@link RunImportOptions.progressEvery}
|
|
962
|
+
* processed rows and once at the end. May be async; the runner awaits it so a
|
|
963
|
+
* DB write of progress completes before the next chunk.
|
|
964
|
+
*/
|
|
965
|
+
onProgress?: (p: ImportProgress) => void | Promise<void>;
|
|
966
|
+
/**
|
|
967
|
+
* Rows between onProgress calls (default 200). Also the flush boundary for
|
|
968
|
+
* buffered creates — a batch never grows past this before being written,
|
|
969
|
+
* so progress numbers stay accurate at every reported checkpoint.
|
|
970
|
+
*/
|
|
971
|
+
progressEvery?: number;
|
|
972
|
+
/**
|
|
973
|
+
* Cooperative cancellation. Checked at each progress boundary; when it returns
|
|
974
|
+
* truthy the runner stops and returns `cancelled: true` with partial results.
|
|
975
|
+
*/
|
|
976
|
+
shouldCancel?: () => boolean | Promise<boolean>;
|
|
977
|
+
/**
|
|
978
|
+
* When true (and not a dry run), accumulate an {@link ImportUndoLog} so the
|
|
979
|
+
* import can be reverted later. Callers gate this on row count to bound the
|
|
980
|
+
* stored snapshot size.
|
|
981
|
+
*/
|
|
982
|
+
captureUndo?: boolean;
|
|
983
|
+
}
|
|
984
|
+
declare function runImport(opts: RunImportOptions): Promise<ImportRunSummary>;
|
|
985
|
+
|
|
986
|
+
/**
|
|
987
|
+
* Type-aware value *coercion* for the bulk-import route
|
|
988
|
+
* (`POST /data/:object/import`).
|
|
989
|
+
*
|
|
990
|
+
* This is the inverse of `export-format.ts`. A spreadsheet / CSV cell arrives as
|
|
991
|
+
* a raw string (or, for JSON payloads, an arbitrary primitive); the storage
|
|
992
|
+
* layer, on the other hand, expects *storage* values — booleans as real
|
|
993
|
+
* booleans, numbers as numbers, dates as ISO strings, select fields as their
|
|
994
|
+
* option **code** (not the human label), and lookup / user fields as the
|
|
995
|
+
* referenced record **id** (not its name). The engine deliberately does not
|
|
996
|
+
* coerce for storage (see `record-validator.ts`, which coerces only to *check*
|
|
997
|
+
* a value and then discards the coerced form), so import has to do it here.
|
|
998
|
+
*
|
|
999
|
+
* The accepted storage shapes below are dictated by what
|
|
1000
|
+
* `validateFieldValue` in `packages/objectql` will accept:
|
|
1001
|
+
* - number / currency / percent / rating / slider → a finite `number`
|
|
1002
|
+
* - boolean / toggle → a real `boolean`
|
|
1003
|
+
* - date / datetime → an ISO-8601 string
|
|
1004
|
+
* - time → `HH:MM` / `HH:MM:SS`
|
|
1005
|
+
* - select / radio → an option *value*
|
|
1006
|
+
* - multiselect / checkboxes / tags → an array of option values
|
|
1007
|
+
* - lookup / master_detail / user / reference → a record id (resolved async)
|
|
1008
|
+
*
|
|
1009
|
+
* Contract: when a field carries no usable metadata the value passes through
|
|
1010
|
+
* untouched, so an import stays byte-identical to the pre-coercion behaviour.
|
|
1011
|
+
*/
|
|
1012
|
+
|
|
1013
|
+
/**
|
|
1014
|
+
* Structured outcome of a reference lookup. `id` set → a single record matched.
|
|
1015
|
+
* `ambiguous` → the display value matched more than one record, so linking any
|
|
1016
|
+
* one of them would be a guess the importer refuses to make. `matchedField`
|
|
1017
|
+
* names the field the match came from (for diagnostics). An empty object means
|
|
1018
|
+
* nothing matched. A bare `string | undefined` is still accepted from legacy
|
|
1019
|
+
* resolvers and normalised to this shape.
|
|
1020
|
+
*/
|
|
1021
|
+
interface RefMatch {
|
|
1022
|
+
id?: string;
|
|
1023
|
+
ambiguous?: boolean;
|
|
1024
|
+
matchedField?: string;
|
|
1025
|
+
}
|
|
1026
|
+
/**
|
|
1027
|
+
* Resolve a reference field's display value (a name / email / id typed by the
|
|
1028
|
+
* user) to the referenced record's id. Return `undefined` / `{}` when nothing
|
|
1029
|
+
* matches (caller surfaces "not found"), a bare id string / `{ id }` on a unique
|
|
1030
|
+
* hit, or `{ ambiguous: true }` when several records share the value. Legacy
|
|
1031
|
+
* resolvers that return `string | undefined` keep working. Implementations are
|
|
1032
|
+
* expected to cache — the same name shows up on many rows.
|
|
1033
|
+
*/
|
|
1034
|
+
type RefResolver = (referenceObject: string, displayValue: string, meta: ExportFieldMeta) => Promise<string | undefined | RefMatch>;
|
|
1035
|
+
interface CoerceContext {
|
|
1036
|
+
/** Trim leading/trailing whitespace from string-ish cells (default true). */
|
|
1037
|
+
trimWhitespace?: boolean;
|
|
1038
|
+
/** Extra strings (besides `''`) treated as null, e.g. `['N/A', 'null']`. */
|
|
1039
|
+
nullValues?: string[];
|
|
1040
|
+
/**
|
|
1041
|
+
* When a select/multiselect cell matches no known option, keep the raw value
|
|
1042
|
+
* instead of failing. Note: the engine still validates option membership, so
|
|
1043
|
+
* this only helps when the option is (or will be) present in the schema.
|
|
1044
|
+
*/
|
|
1045
|
+
createMissingOptions?: boolean;
|
|
1046
|
+
/** Async reference resolver (name/email/id → record id). Optional. */
|
|
1047
|
+
resolveRef?: RefResolver;
|
|
1048
|
+
}
|
|
1049
|
+
/** A per-field coercion failure, shaped like the engine's validation errors. */
|
|
1050
|
+
interface FieldCoerceError {
|
|
1051
|
+
field: string;
|
|
1052
|
+
code: string;
|
|
1053
|
+
message: string;
|
|
1054
|
+
}
|
|
1055
|
+
/**
|
|
1056
|
+
* Coerce a whole raw row into a storage-ready record. Unknown columns (no
|
|
1057
|
+
* matching field metadata) pass through untouched so ad-hoc / schemaless
|
|
1058
|
+
* objects still import. Collects every field error rather than stopping at the
|
|
1059
|
+
* first, so a UI can show all problems in a row at once.
|
|
1060
|
+
*/
|
|
1061
|
+
declare function coerceRow(rawRow: Record<string, unknown>, metaMap: Map<string, ExportFieldMeta>, ctx: CoerceContext): Promise<{
|
|
1062
|
+
data: Record<string, unknown>;
|
|
1063
|
+
errors: FieldCoerceError[];
|
|
1064
|
+
}>;
|
|
1065
|
+
|
|
1066
|
+
export { type CoerceContext, type ExportFieldMeta, type ImportAction, type ImportProgress, type ImportProtocolLike, type ImportRowResult, type ImportRunSummary, type ImportUndoLog, type PrepareImportResult, type PreparedImport, type RefResolver, type RestApiPluginConfig, RestServer, type RouteEntry, RouteGroupBuilder, RouteManager, type RunImportOptions, buildFieldMetaMap, coerceRow, createRestApiPlugin, isMetaEnvelope, parseCsvToRows, parseXlsxToRows, prepareImportRequest, runImport };
|