@objectstack/rest 14.7.0 → 15.0.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 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,12 +1094,27 @@ 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) : [];
1092
- const runAutomations = body?.runAutomations === true;
1117
+ const runAutomations = body?.runAutomations !== false;
1093
1118
  const trimWhitespace = body?.trimWhitespace !== false;
1094
1119
  const nullValues = Array.isArray(body?.nullValues) ? body.nullValues.filter((v) => typeof v === "string") : void 0;
1095
1120
  const createMissingOptions = body?.createMissingOptions === true;
@@ -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, { p, objectName, environmentId, maxRows: 5e3 });
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 ?? {}, { p, objectName, environmentId, maxRows: IMPORT_JOB_MAX_ROWS });
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");
@@ -4839,12 +4906,15 @@ var RestServer = class _RestServer {
4839
4906
  const parsed = ExplainRequestSchema.safeParse({
4840
4907
  object: src.object,
4841
4908
  operation: src.operation ?? "read",
4842
- ...src.userId != null && src.userId !== "" ? { userId: src.userId } : {}
4909
+ ...src.userId != null && src.userId !== "" ? { userId: src.userId } : {},
4910
+ // [C2 / ADR-0095] Optional record id — explains ONE concrete
4911
+ // row at record granularity; omitted stays object-level.
4912
+ ...src.recordId != null && src.recordId !== "" ? { recordId: src.recordId } : {}
4843
4913
  });
4844
4914
  if (!parsed.success) {
4845
4915
  return res.status(400).json({
4846
4916
  code: "VALIDATION_FAILED",
4847
- message: "Invalid explain request \u2014 expected { object: string, operation: read|create|update|delete|transfer|restore|purge, userId?: string }.",
4917
+ message: "Invalid explain request \u2014 expected { object: string, operation: read|create|update|delete|transfer|restore|purge, userId?: string, recordId?: string }.",
4848
4918
  detail: String(parsed.error?.message ?? "").slice(0, 1e3)
4849
4919
  });
4850
4920
  }