@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.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, { p, objectName, environmentId, maxRows: 5e3 });
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 ?? {}, { p, objectName, environmentId, maxRows: IMPORT_JOB_MAX_ROWS });
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");