@objectstack/rest 14.6.0 → 14.8.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 +77 -10
- package/dist/index.cjs.map +1 -1
- package/dist/index.d.cts +7 -10
- package/dist/index.d.ts +7 -10
- package/dist/index.js +77 -10
- package/dist/index.js.map +1 -1
- package/package.json +8 -8
package/dist/index.d.cts
CHANGED
|
@@ -869,21 +869,18 @@ type PrepareImportResult = {
|
|
|
869
869
|
code: string;
|
|
870
870
|
error: string;
|
|
871
871
|
};
|
|
872
|
-
/**
|
|
873
|
-
* Parse & validate a bulk-import request body into a {@link PreparedImport}.
|
|
874
|
-
*
|
|
875
|
-
* Shared by the synchronous `POST /data/:object/import` route and the async
|
|
876
|
-
* import-job create route so both accept byte-identical payloads (writeMode +
|
|
877
|
-
* matchFields, mapping in either shape, rows[]/csv/xlsxBase64) and resolve the
|
|
878
|
-
* same field metadata. The only knob that differs is `maxRows` (5k sync vs
|
|
879
|
-
* 50k async). Returns a discriminated result; the caller maps `!ok` to an HTTP
|
|
880
|
-
* error using the returned status/code/error.
|
|
881
|
-
*/
|
|
882
872
|
declare function prepareImportRequest(body: any, opts: {
|
|
883
873
|
p: any;
|
|
884
874
|
objectName: string;
|
|
885
875
|
environmentId?: string;
|
|
886
876
|
maxRows: number;
|
|
877
|
+
/**
|
|
878
|
+
* Optional hook applying the request locale to a schema document (the
|
|
879
|
+
* REST server passes `translateMetaItem` bound to the request). Used to
|
|
880
|
+
* accept locale-translated option labels — the strings the localized
|
|
881
|
+
* export / import template actually contain — as select-cell synonyms.
|
|
882
|
+
*/
|
|
883
|
+
localizeSchema?: (schema: any) => Promise<any> | any;
|
|
887
884
|
}): Promise<PrepareImportResult>;
|
|
888
885
|
|
|
889
886
|
/**
|
package/dist/index.d.ts
CHANGED
|
@@ -869,21 +869,18 @@ type PrepareImportResult = {
|
|
|
869
869
|
code: string;
|
|
870
870
|
error: string;
|
|
871
871
|
};
|
|
872
|
-
/**
|
|
873
|
-
* Parse & validate a bulk-import request body into a {@link PreparedImport}.
|
|
874
|
-
*
|
|
875
|
-
* Shared by the synchronous `POST /data/:object/import` route and the async
|
|
876
|
-
* import-job create route so both accept byte-identical payloads (writeMode +
|
|
877
|
-
* matchFields, mapping in either shape, rows[]/csv/xlsxBase64) and resolve the
|
|
878
|
-
* same field metadata. The only knob that differs is `maxRows` (5k sync vs
|
|
879
|
-
* 50k async). Returns a discriminated result; the caller maps `!ok` to an HTTP
|
|
880
|
-
* error using the returned status/code/error.
|
|
881
|
-
*/
|
|
882
872
|
declare function prepareImportRequest(body: any, opts: {
|
|
883
873
|
p: any;
|
|
884
874
|
objectName: string;
|
|
885
875
|
environmentId?: string;
|
|
886
876
|
maxRows: number;
|
|
877
|
+
/**
|
|
878
|
+
* Optional hook applying the request locale to a schema document (the
|
|
879
|
+
* REST server passes `translateMetaItem` bound to the request). Used to
|
|
880
|
+
* accept locale-translated option labels — the strings the localized
|
|
881
|
+
* export / import template actually contain — as select-cell synonyms.
|
|
882
|
+
*/
|
|
883
|
+
localizeSchema?: (schema: any) => Promise<any> | any;
|
|
887
884
|
}): Promise<PrepareImportResult>;
|
|
888
885
|
|
|
889
886
|
/**
|
package/dist/index.js
CHANGED
|
@@ -213,6 +213,16 @@ var RouteGroupBuilder = class {
|
|
|
213
213
|
};
|
|
214
214
|
|
|
215
215
|
// src/export-format.ts
|
|
216
|
+
function exportContentDisposition(objectName, label, ext, now = /* @__PURE__ */ new Date()) {
|
|
217
|
+
const pad = (n) => String(n).padStart(2, "0");
|
|
218
|
+
const stamp = `${now.getFullYear()}${pad(now.getMonth() + 1)}${pad(now.getDate())}-${pad(now.getHours())}${pad(now.getMinutes())}${pad(now.getSeconds())}`;
|
|
219
|
+
const asciiBase = objectName.replace(/[^A-Za-z0-9_.-]/g, "_") || "export";
|
|
220
|
+
const utf8Base = String(label ?? "").replace(/[\\/:*?"<>|\u0000-\u001f]+/g, "_").replace(/^[\s._-]+|[\s._-]+$/g, "").slice(0, 80) || asciiBase;
|
|
221
|
+
const asciiName = `${asciiBase}-${stamp}.${ext}`;
|
|
222
|
+
const utf8Name = `${utf8Base}-${stamp}.${ext}`;
|
|
223
|
+
const encoded = encodeURIComponent(utf8Name).replace(/['()*]/g, (c) => `%${c.charCodeAt(0).toString(16).toUpperCase()}`);
|
|
224
|
+
return `attachment; filename="${asciiName}"; filename*=UTF-8''${encoded}`;
|
|
225
|
+
}
|
|
216
226
|
var REFERENCE_TYPES = /* @__PURE__ */ new Set(["lookup", "master_detail", "user", "reference", "tree"]);
|
|
217
227
|
var OPTION_TYPES = /* @__PURE__ */ new Set(["select", "radio"]);
|
|
218
228
|
var MULTI_OPTION_TYPES = /* @__PURE__ */ new Set(["multiselect", "checkboxes", "tags"]);
|
|
@@ -1038,8 +1048,23 @@ async function parseXlsxToRows(buffer, mapping = {}, sheet) {
|
|
|
1038
1048
|
}
|
|
1039
1049
|
return out;
|
|
1040
1050
|
}
|
|
1051
|
+
function mergeLocalizedOptionSynonyms(metaMap, localized) {
|
|
1052
|
+
for (const [name, meta] of metaMap) {
|
|
1053
|
+
const loc = localized.get(name);
|
|
1054
|
+
if (!meta.options?.length || !loc?.options?.length) continue;
|
|
1055
|
+
const known = new Set(
|
|
1056
|
+
meta.options.map((o) => typeof o?.label === "string" ? o.label.trim().toLowerCase() : "").filter(Boolean)
|
|
1057
|
+
);
|
|
1058
|
+
const synonyms = loc.options.filter(
|
|
1059
|
+
(o) => o && typeof o.label === "string" && o.label.trim().length > 0 && o.value !== void 0 && !known.has(o.label.trim().toLowerCase())
|
|
1060
|
+
);
|
|
1061
|
+
if (synonyms.length > 0) {
|
|
1062
|
+
meta.options = [...meta.options, ...synonyms.map((o) => ({ label: o.label, value: o.value }))];
|
|
1063
|
+
}
|
|
1064
|
+
}
|
|
1065
|
+
}
|
|
1041
1066
|
async function prepareImportRequest(body, opts) {
|
|
1042
|
-
const { p, objectName, environmentId, maxRows } = opts;
|
|
1067
|
+
const { p, objectName, environmentId, maxRows, localizeSchema } = opts;
|
|
1043
1068
|
const dryRun = body?.dryRun === true;
|
|
1044
1069
|
let writeMode = body?.writeMode === "update" || body?.writeMode === "upsert" ? body.writeMode : "insert";
|
|
1045
1070
|
let matchFields = Array.isArray(body?.matchFields) ? body.matchFields.filter((f) => typeof f === "string" && f.length > 0) : [];
|
|
@@ -1126,6 +1151,15 @@ async function prepareImportRequest(body, opts) {
|
|
|
1126
1151
|
schema = await p.getObjectSchema(objectName, environmentId);
|
|
1127
1152
|
}
|
|
1128
1153
|
metaMap = buildFieldMetaMap(schema);
|
|
1154
|
+
if (schema && typeof localizeSchema === "function") {
|
|
1155
|
+
try {
|
|
1156
|
+
const localized = await localizeSchema(schema);
|
|
1157
|
+
if (localized && localized !== schema) {
|
|
1158
|
+
mergeLocalizedOptionSynonyms(metaMap, buildFieldMetaMap(localized));
|
|
1159
|
+
}
|
|
1160
|
+
} catch {
|
|
1161
|
+
}
|
|
1162
|
+
}
|
|
1129
1163
|
} catch {
|
|
1130
1164
|
}
|
|
1131
1165
|
return {
|
|
@@ -1204,8 +1238,28 @@ function mapDataError(error, object) {
|
|
|
1204
1238
|
}
|
|
1205
1239
|
};
|
|
1206
1240
|
}
|
|
1241
|
+
if (typeof error?.innerMessage === "string" && error.innerMessage) {
|
|
1242
|
+
return {
|
|
1243
|
+
status: 400,
|
|
1244
|
+
body: {
|
|
1245
|
+
error: error.innerMessage,
|
|
1246
|
+
...object ? { object } : {}
|
|
1247
|
+
}
|
|
1248
|
+
};
|
|
1249
|
+
}
|
|
1207
1250
|
const raw = String(error?.message ?? error ?? "");
|
|
1208
1251
|
const lower = raw.toLowerCase();
|
|
1252
|
+
const sandboxWrapper = /^(?:hook|action) '[^']*' threw:\s*(.+)$/s.exec(raw);
|
|
1253
|
+
if (sandboxWrapper) {
|
|
1254
|
+
const msg = sandboxWrapper[1].startsWith("Error: ") ? sandboxWrapper[1].slice("Error: ".length) : sandboxWrapper[1];
|
|
1255
|
+
return {
|
|
1256
|
+
status: 400,
|
|
1257
|
+
body: {
|
|
1258
|
+
error: msg,
|
|
1259
|
+
...object ? { object } : {}
|
|
1260
|
+
}
|
|
1261
|
+
};
|
|
1262
|
+
}
|
|
1209
1263
|
if (raw.includes("[EnvironmentKernelFactory]") && (lower.includes("missing database_url") || lower.includes("not found"))) {
|
|
1210
1264
|
const isProvisioning = lower.includes("status='provisioning'") || lower.includes("status='pending'");
|
|
1211
1265
|
const isFailed = lower.includes("status='failed'");
|
|
@@ -2056,7 +2110,6 @@ var RestServer = class _RestServer {
|
|
|
2056
2110
|
if (!TRANSLATABLE_META_TYPES.has(type)) return item;
|
|
2057
2111
|
const i18n = i18nService !== void 0 ? i18nService : await this.resolveI18nService(environmentId, req);
|
|
2058
2112
|
const bundle = this.buildTranslationBundle(i18n);
|
|
2059
|
-
if (!bundle) return item;
|
|
2060
2113
|
const locale = this.extractLocale(req, i18n);
|
|
2061
2114
|
if (!locale) return item;
|
|
2062
2115
|
const { translateMetadataDocument } = await import("@objectstack/spec/system");
|
|
@@ -2074,7 +2127,6 @@ var RestServer = class _RestServer {
|
|
|
2074
2127
|
if (!arr) return items;
|
|
2075
2128
|
const i18n = await this.resolveI18nService(environmentId, req);
|
|
2076
2129
|
const bundle = this.buildTranslationBundle(i18n);
|
|
2077
|
-
if (!bundle) return items;
|
|
2078
2130
|
const locale = this.extractLocale(req, i18n);
|
|
2079
2131
|
if (!locale) return items;
|
|
2080
2132
|
const { translateMetadataDocument } = await import("@objectstack/spec/system");
|
|
@@ -3646,7 +3698,15 @@ var RestServer = class _RestServer {
|
|
|
3646
3698
|
}
|
|
3647
3699
|
if (await this.enforceApiAccess(req, res, p, environmentId, "import")) return;
|
|
3648
3700
|
const body = req.body ?? {};
|
|
3649
|
-
const prep = await prepareImportRequest(body, {
|
|
3701
|
+
const prep = await prepareImportRequest(body, {
|
|
3702
|
+
p,
|
|
3703
|
+
objectName,
|
|
3704
|
+
environmentId,
|
|
3705
|
+
maxRows: 5e3,
|
|
3706
|
+
// Accept locale-translated option labels (what the localized
|
|
3707
|
+
// export / import template contain) as select-cell synonyms.
|
|
3708
|
+
localizeSchema: (schema) => this.translateMetaItem(req, "object", environmentId, schema)
|
|
3709
|
+
});
|
|
3650
3710
|
if (!prep.ok) {
|
|
3651
3711
|
if (prep.status === 413) prep.error += " Use an async import job for larger files.";
|
|
3652
3712
|
res.status(prep.status).json({ code: prep.code, error: prep.error });
|
|
@@ -3697,7 +3757,14 @@ var RestServer = class _RestServer {
|
|
|
3697
3757
|
return;
|
|
3698
3758
|
}
|
|
3699
3759
|
if (await this.enforceApiAccess(req, res, p, environmentId, "import")) return;
|
|
3700
|
-
const prep = await prepareImportRequest(req.body ?? {}, {
|
|
3760
|
+
const prep = await prepareImportRequest(req.body ?? {}, {
|
|
3761
|
+
p,
|
|
3762
|
+
objectName,
|
|
3763
|
+
environmentId,
|
|
3764
|
+
maxRows: IMPORT_JOB_MAX_ROWS,
|
|
3765
|
+
// Same round-trip i18n synonyms as the synchronous route.
|
|
3766
|
+
localizeSchema: (schema) => this.translateMetaItem(req, "object", environmentId, schema)
|
|
3767
|
+
});
|
|
3701
3768
|
if (!prep.ok) {
|
|
3702
3769
|
if (prep.status === 413) prep.error += ` This is the async import ceiling; split the file into batches of ${IMPORT_JOB_MAX_ROWS}.`;
|
|
3703
3770
|
res.status(prep.status).json({ code: prep.code, error: prep.error });
|
|
@@ -4027,6 +4094,7 @@ var RestServer = class _RestServer {
|
|
|
4027
4094
|
fields = q.fields.filter((s) => typeof s === "string" && s.length > 0);
|
|
4028
4095
|
}
|
|
4029
4096
|
let metaMap = /* @__PURE__ */ new Map();
|
|
4097
|
+
let objectLabel;
|
|
4030
4098
|
try {
|
|
4031
4099
|
let schema = void 0;
|
|
4032
4100
|
if (typeof p.getMetaItem === "function") {
|
|
@@ -4037,6 +4105,9 @@ var RestServer = class _RestServer {
|
|
|
4037
4105
|
schema = await p.getObjectSchema(objectName, environmentId);
|
|
4038
4106
|
}
|
|
4039
4107
|
schema = await this.translateMetaItem(req, "object", environmentId, schema);
|
|
4108
|
+
if (typeof schema?.label === "string" && schema.label.length > 0) {
|
|
4109
|
+
objectLabel = schema.label;
|
|
4110
|
+
}
|
|
4040
4111
|
metaMap = buildFieldMetaMap(schema);
|
|
4041
4112
|
if (!fields || fields.length === 0) {
|
|
4042
4113
|
const names = [...metaMap.keys()];
|
|
@@ -4045,18 +4116,14 @@ var RestServer = class _RestServer {
|
|
|
4045
4116
|
} catch {
|
|
4046
4117
|
}
|
|
4047
4118
|
const expandFields = referenceFieldNames(metaMap);
|
|
4048
|
-
const stamp = (/* @__PURE__ */ new Date()).toISOString().slice(0, 10);
|
|
4049
|
-
const safeObj = objectName.replace(/[^A-Za-z0-9_.-]/g, "_");
|
|
4050
4119
|
if (format === "csv") {
|
|
4051
4120
|
res.header("Content-Type", "text/csv; charset=utf-8");
|
|
4052
|
-
res.header("Content-Disposition", `attachment; filename="${safeObj}-${stamp}.csv"`);
|
|
4053
4121
|
} else if (format === "xlsx") {
|
|
4054
4122
|
res.header("Content-Type", "application/vnd.openxmlformats-officedocument.spreadsheetml.sheet");
|
|
4055
|
-
res.header("Content-Disposition", `attachment; filename="${safeObj}-${stamp}.xlsx"`);
|
|
4056
4123
|
} else {
|
|
4057
4124
|
res.header("Content-Type", "application/json; charset=utf-8");
|
|
4058
|
-
res.header("Content-Disposition", `attachment; filename="${safeObj}-${stamp}.json"`);
|
|
4059
4125
|
}
|
|
4126
|
+
res.header("Content-Disposition", exportContentDisposition(objectName, objectLabel, format));
|
|
4060
4127
|
res.header("X-Export-Format", format);
|
|
4061
4128
|
res.header("X-Export-Limit", String(limit));
|
|
4062
4129
|
if (format === "xlsx") res.header("X-Export-Styles", styled ? "applied" : "dropped");
|