@objectstack/rest 14.7.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.cjs
CHANGED
|
@@ -259,6 +259,16 @@ var RouteGroupBuilder = class {
|
|
|
259
259
|
};
|
|
260
260
|
|
|
261
261
|
// src/export-format.ts
|
|
262
|
+
function exportContentDisposition(objectName, label, ext, now = /* @__PURE__ */ new Date()) {
|
|
263
|
+
const pad = (n) => String(n).padStart(2, "0");
|
|
264
|
+
const stamp = `${now.getFullYear()}${pad(now.getMonth() + 1)}${pad(now.getDate())}-${pad(now.getHours())}${pad(now.getMinutes())}${pad(now.getSeconds())}`;
|
|
265
|
+
const asciiBase = objectName.replace(/[^A-Za-z0-9_.-]/g, "_") || "export";
|
|
266
|
+
const utf8Base = String(label ?? "").replace(/[\\/:*?"<>|\u0000-\u001f]+/g, "_").replace(/^[\s._-]+|[\s._-]+$/g, "").slice(0, 80) || asciiBase;
|
|
267
|
+
const asciiName = `${asciiBase}-${stamp}.${ext}`;
|
|
268
|
+
const utf8Name = `${utf8Base}-${stamp}.${ext}`;
|
|
269
|
+
const encoded = encodeURIComponent(utf8Name).replace(/['()*]/g, (c) => `%${c.charCodeAt(0).toString(16).toUpperCase()}`);
|
|
270
|
+
return `attachment; filename="${asciiName}"; filename*=UTF-8''${encoded}`;
|
|
271
|
+
}
|
|
262
272
|
var REFERENCE_TYPES = /* @__PURE__ */ new Set(["lookup", "master_detail", "user", "reference", "tree"]);
|
|
263
273
|
var OPTION_TYPES = /* @__PURE__ */ new Set(["select", "radio"]);
|
|
264
274
|
var MULTI_OPTION_TYPES = /* @__PURE__ */ new Set(["multiselect", "checkboxes", "tags"]);
|
|
@@ -1084,8 +1094,23 @@ async function parseXlsxToRows(buffer, mapping = {}, sheet) {
|
|
|
1084
1094
|
}
|
|
1085
1095
|
return out;
|
|
1086
1096
|
}
|
|
1097
|
+
function mergeLocalizedOptionSynonyms(metaMap, localized) {
|
|
1098
|
+
for (const [name, meta] of metaMap) {
|
|
1099
|
+
const loc = localized.get(name);
|
|
1100
|
+
if (!meta.options?.length || !loc?.options?.length) continue;
|
|
1101
|
+
const known = new Set(
|
|
1102
|
+
meta.options.map((o) => typeof o?.label === "string" ? o.label.trim().toLowerCase() : "").filter(Boolean)
|
|
1103
|
+
);
|
|
1104
|
+
const synonyms = loc.options.filter(
|
|
1105
|
+
(o) => o && typeof o.label === "string" && o.label.trim().length > 0 && o.value !== void 0 && !known.has(o.label.trim().toLowerCase())
|
|
1106
|
+
);
|
|
1107
|
+
if (synonyms.length > 0) {
|
|
1108
|
+
meta.options = [...meta.options, ...synonyms.map((o) => ({ label: o.label, value: o.value }))];
|
|
1109
|
+
}
|
|
1110
|
+
}
|
|
1111
|
+
}
|
|
1087
1112
|
async function prepareImportRequest(body, opts) {
|
|
1088
|
-
const { p, objectName, environmentId, maxRows } = opts;
|
|
1113
|
+
const { p, objectName, environmentId, maxRows, localizeSchema } = opts;
|
|
1089
1114
|
const dryRun = body?.dryRun === true;
|
|
1090
1115
|
let writeMode = body?.writeMode === "update" || body?.writeMode === "upsert" ? body.writeMode : "insert";
|
|
1091
1116
|
let matchFields = Array.isArray(body?.matchFields) ? body.matchFields.filter((f) => typeof f === "string" && f.length > 0) : [];
|
|
@@ -1172,6 +1197,15 @@ async function prepareImportRequest(body, opts) {
|
|
|
1172
1197
|
schema = await p.getObjectSchema(objectName, environmentId);
|
|
1173
1198
|
}
|
|
1174
1199
|
metaMap = buildFieldMetaMap(schema);
|
|
1200
|
+
if (schema && typeof localizeSchema === "function") {
|
|
1201
|
+
try {
|
|
1202
|
+
const localized = await localizeSchema(schema);
|
|
1203
|
+
if (localized && localized !== schema) {
|
|
1204
|
+
mergeLocalizedOptionSynonyms(metaMap, buildFieldMetaMap(localized));
|
|
1205
|
+
}
|
|
1206
|
+
} catch {
|
|
1207
|
+
}
|
|
1208
|
+
}
|
|
1175
1209
|
} catch {
|
|
1176
1210
|
}
|
|
1177
1211
|
return {
|
|
@@ -1251,8 +1285,28 @@ function mapDataError(error, object) {
|
|
|
1251
1285
|
}
|
|
1252
1286
|
};
|
|
1253
1287
|
}
|
|
1288
|
+
if (typeof error?.innerMessage === "string" && error.innerMessage) {
|
|
1289
|
+
return {
|
|
1290
|
+
status: 400,
|
|
1291
|
+
body: {
|
|
1292
|
+
error: error.innerMessage,
|
|
1293
|
+
...object ? { object } : {}
|
|
1294
|
+
}
|
|
1295
|
+
};
|
|
1296
|
+
}
|
|
1254
1297
|
const raw = String(error?.message ?? error ?? "");
|
|
1255
1298
|
const lower = raw.toLowerCase();
|
|
1299
|
+
const sandboxWrapper = /^(?:hook|action) '[^']*' threw:\s*(.+)$/s.exec(raw);
|
|
1300
|
+
if (sandboxWrapper) {
|
|
1301
|
+
const msg = sandboxWrapper[1].startsWith("Error: ") ? sandboxWrapper[1].slice("Error: ".length) : sandboxWrapper[1];
|
|
1302
|
+
return {
|
|
1303
|
+
status: 400,
|
|
1304
|
+
body: {
|
|
1305
|
+
error: msg,
|
|
1306
|
+
...object ? { object } : {}
|
|
1307
|
+
}
|
|
1308
|
+
};
|
|
1309
|
+
}
|
|
1256
1310
|
if (raw.includes("[EnvironmentKernelFactory]") && (lower.includes("missing database_url") || lower.includes("not found"))) {
|
|
1257
1311
|
const isProvisioning = lower.includes("status='provisioning'") || lower.includes("status='pending'");
|
|
1258
1312
|
const isFailed = lower.includes("status='failed'");
|
|
@@ -2103,7 +2157,6 @@ var RestServer = class _RestServer {
|
|
|
2103
2157
|
if (!TRANSLATABLE_META_TYPES.has(type)) return item;
|
|
2104
2158
|
const i18n = i18nService !== void 0 ? i18nService : await this.resolveI18nService(environmentId, req);
|
|
2105
2159
|
const bundle = this.buildTranslationBundle(i18n);
|
|
2106
|
-
if (!bundle) return item;
|
|
2107
2160
|
const locale = this.extractLocale(req, i18n);
|
|
2108
2161
|
if (!locale) return item;
|
|
2109
2162
|
const { translateMetadataDocument } = await import("@objectstack/spec/system");
|
|
@@ -2121,7 +2174,6 @@ var RestServer = class _RestServer {
|
|
|
2121
2174
|
if (!arr) return items;
|
|
2122
2175
|
const i18n = await this.resolveI18nService(environmentId, req);
|
|
2123
2176
|
const bundle = this.buildTranslationBundle(i18n);
|
|
2124
|
-
if (!bundle) return items;
|
|
2125
2177
|
const locale = this.extractLocale(req, i18n);
|
|
2126
2178
|
if (!locale) return items;
|
|
2127
2179
|
const { translateMetadataDocument } = await import("@objectstack/spec/system");
|
|
@@ -3693,7 +3745,15 @@ var RestServer = class _RestServer {
|
|
|
3693
3745
|
}
|
|
3694
3746
|
if (await this.enforceApiAccess(req, res, p, environmentId, "import")) return;
|
|
3695
3747
|
const body = req.body ?? {};
|
|
3696
|
-
const prep = await prepareImportRequest(body, {
|
|
3748
|
+
const prep = await prepareImportRequest(body, {
|
|
3749
|
+
p,
|
|
3750
|
+
objectName,
|
|
3751
|
+
environmentId,
|
|
3752
|
+
maxRows: 5e3,
|
|
3753
|
+
// Accept locale-translated option labels (what the localized
|
|
3754
|
+
// export / import template contain) as select-cell synonyms.
|
|
3755
|
+
localizeSchema: (schema) => this.translateMetaItem(req, "object", environmentId, schema)
|
|
3756
|
+
});
|
|
3697
3757
|
if (!prep.ok) {
|
|
3698
3758
|
if (prep.status === 413) prep.error += " Use an async import job for larger files.";
|
|
3699
3759
|
res.status(prep.status).json({ code: prep.code, error: prep.error });
|
|
@@ -3744,7 +3804,14 @@ var RestServer = class _RestServer {
|
|
|
3744
3804
|
return;
|
|
3745
3805
|
}
|
|
3746
3806
|
if (await this.enforceApiAccess(req, res, p, environmentId, "import")) return;
|
|
3747
|
-
const prep = await prepareImportRequest(req.body ?? {}, {
|
|
3807
|
+
const prep = await prepareImportRequest(req.body ?? {}, {
|
|
3808
|
+
p,
|
|
3809
|
+
objectName,
|
|
3810
|
+
environmentId,
|
|
3811
|
+
maxRows: IMPORT_JOB_MAX_ROWS,
|
|
3812
|
+
// Same round-trip i18n synonyms as the synchronous route.
|
|
3813
|
+
localizeSchema: (schema) => this.translateMetaItem(req, "object", environmentId, schema)
|
|
3814
|
+
});
|
|
3748
3815
|
if (!prep.ok) {
|
|
3749
3816
|
if (prep.status === 413) prep.error += ` This is the async import ceiling; split the file into batches of ${IMPORT_JOB_MAX_ROWS}.`;
|
|
3750
3817
|
res.status(prep.status).json({ code: prep.code, error: prep.error });
|
|
@@ -4074,6 +4141,7 @@ var RestServer = class _RestServer {
|
|
|
4074
4141
|
fields = q.fields.filter((s) => typeof s === "string" && s.length > 0);
|
|
4075
4142
|
}
|
|
4076
4143
|
let metaMap = /* @__PURE__ */ new Map();
|
|
4144
|
+
let objectLabel;
|
|
4077
4145
|
try {
|
|
4078
4146
|
let schema = void 0;
|
|
4079
4147
|
if (typeof p.getMetaItem === "function") {
|
|
@@ -4084,6 +4152,9 @@ var RestServer = class _RestServer {
|
|
|
4084
4152
|
schema = await p.getObjectSchema(objectName, environmentId);
|
|
4085
4153
|
}
|
|
4086
4154
|
schema = await this.translateMetaItem(req, "object", environmentId, schema);
|
|
4155
|
+
if (typeof schema?.label === "string" && schema.label.length > 0) {
|
|
4156
|
+
objectLabel = schema.label;
|
|
4157
|
+
}
|
|
4087
4158
|
metaMap = buildFieldMetaMap(schema);
|
|
4088
4159
|
if (!fields || fields.length === 0) {
|
|
4089
4160
|
const names = [...metaMap.keys()];
|
|
@@ -4092,18 +4163,14 @@ var RestServer = class _RestServer {
|
|
|
4092
4163
|
} catch {
|
|
4093
4164
|
}
|
|
4094
4165
|
const expandFields = referenceFieldNames(metaMap);
|
|
4095
|
-
const stamp = (/* @__PURE__ */ new Date()).toISOString().slice(0, 10);
|
|
4096
|
-
const safeObj = objectName.replace(/[^A-Za-z0-9_.-]/g, "_");
|
|
4097
4166
|
if (format === "csv") {
|
|
4098
4167
|
res.header("Content-Type", "text/csv; charset=utf-8");
|
|
4099
|
-
res.header("Content-Disposition", `attachment; filename="${safeObj}-${stamp}.csv"`);
|
|
4100
4168
|
} else if (format === "xlsx") {
|
|
4101
4169
|
res.header("Content-Type", "application/vnd.openxmlformats-officedocument.spreadsheetml.sheet");
|
|
4102
|
-
res.header("Content-Disposition", `attachment; filename="${safeObj}-${stamp}.xlsx"`);
|
|
4103
4170
|
} else {
|
|
4104
4171
|
res.header("Content-Type", "application/json; charset=utf-8");
|
|
4105
|
-
res.header("Content-Disposition", `attachment; filename="${safeObj}-${stamp}.json"`);
|
|
4106
4172
|
}
|
|
4173
|
+
res.header("Content-Disposition", exportContentDisposition(objectName, objectLabel, format));
|
|
4107
4174
|
res.header("X-Export-Format", format);
|
|
4108
4175
|
res.header("X-Export-Limit", String(limit));
|
|
4109
4176
|
if (format === "xlsx") res.header("X-Export-Styles", styled ? "applied" : "dropped");
|