@objectstack/rest 14.3.0 → 14.5.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
@@ -296,7 +296,14 @@ function buildFieldMetaMap(schema) {
296
296
  label: typeof f.label === "string" ? f.label : void 0,
297
297
  options: Array.isArray(f.options) ? f.options : void 0,
298
298
  reference: typeof f.reference === "string" ? f.reference : void 0,
299
- displayField: typeof f.displayField === "string" ? f.displayField : void 0
299
+ displayField: typeof f.displayField === "string" ? f.displayField : void 0,
300
+ required: f.required === true,
301
+ system: f.system === true,
302
+ readonly: f.readonly === true,
303
+ // Mirror the engine's `applyFieldDefaults` gate (`f.defaultValue == null`
304
+ // ⇒ no default): any non-null default — literal, expression object, or the
305
+ // `current_user` token — counts as satisfying a required field.
306
+ hasDefault: f.defaultValue != null
300
307
  });
301
308
  }
302
309
  return map;
@@ -566,6 +573,27 @@ async function coerceFieldValue(raw, meta, ctx) {
566
573
  }
567
574
  return { value: trim && typeof raw === "string" ? raw.trim() : raw };
568
575
  }
576
+ var REQUIRED_CHECK_SKIP = /* @__PURE__ */ new Set([
577
+ "id",
578
+ "created_at",
579
+ "created_by",
580
+ "updated_at",
581
+ "updated_by"
582
+ ]);
583
+ function isBlankValue(v) {
584
+ return v === void 0 || v === null || typeof v === "string" && v.trim() === "";
585
+ }
586
+ function firstMissingRequiredField(data, metaMap) {
587
+ for (const meta of metaMap.values()) {
588
+ if (!meta.required) continue;
589
+ if (meta.system || meta.readonly) continue;
590
+ if (meta.hasDefault) continue;
591
+ if (meta.type === "autonumber") continue;
592
+ if (REQUIRED_CHECK_SKIP.has(meta.name)) continue;
593
+ if (isBlankValue(data[meta.name])) return meta.name;
594
+ }
595
+ return null;
596
+ }
569
597
  async function coerceRow(rawRow, metaMap, ctx) {
570
598
  const data = {};
571
599
  const errors = [];
@@ -764,7 +792,11 @@ function runImport(opts) {
764
792
  if (!handled) {
765
793
  const willUpdate = existing && typeof existing === "object";
766
794
  const willCreate = !willUpdate && (writeMode === "insert" || writeMode === "upsert");
767
- if (!willUpdate && !willCreate) {
795
+ const requiredMiss = willCreate && !runAutomations ? firstMissingRequiredField(data, metaMap) : null;
796
+ if (requiredMiss) {
797
+ errCount++;
798
+ results[i] = { row: rowNo, ok: false, action: "failed", field: requiredMiss, code: "required", error: `${requiredMiss} is required` };
799
+ } else if (!willUpdate && !willCreate) {
768
800
  skipped++;
769
801
  results[i] = { row: rowNo, ok: true, action: "skipped", code: "NO_MATCH" };
770
802
  } else if (dryRun) {
@@ -2510,9 +2542,49 @@ var RestServer = class _RestServer {
2510
2542
  }
2511
2543
  }
2512
2544
  /**
2513
- * Register metadata endpoints
2545
+ * Register the metadata routes behind the SAME `requireAuth` gate the
2546
+ * `/data` routes use.
2547
+ *
2548
+ * `registerMetadataEndpoints` builds ~17 `/meta/*` routes but — unlike the
2549
+ * `/data` handlers — never calls {@link enforceAuth}: its handlers assumed
2550
+ * the `requireAuth` gate rejected anonymous callers "upstream", yet nothing
2551
+ * upstream covers `/meta`, so on a `requireAuth` deployment an anonymous
2552
+ * caller could read object / field schemas. On a tenant-less runtime host
2553
+ * those are SYSTEM-object schemas and the host is publicly reachable — a
2554
+ * real leak.
2555
+ *
2556
+ * Rather than add the gate to every handler (and have the next new route
2557
+ * forget it — the exact failure mode that caused this), wrap the route
2558
+ * registrar for the duration of registration so every meta route, present
2559
+ * and future, inherits it. The check is a no-op when `requireAuth` is off
2560
+ * (demo / single-tenant), so the previously-public metadata surface there
2561
+ * is unchanged; an authenticated user passes exactly as on `/data`.
2514
2562
  */
2515
2563
  registerMetadataEndpoints(basePath) {
2564
+ const realRouteManager = this.routeManager;
2565
+ const guardedRouteManager = {
2566
+ register: (entry) => {
2567
+ const inner = entry.handler;
2568
+ if (typeof inner !== "function") return realRouteManager.register(entry);
2569
+ return realRouteManager.register({
2570
+ ...entry,
2571
+ handler: async (req, res) => {
2572
+ const environmentId = req?.params?.environmentId;
2573
+ const context = await this.resolveExecCtx(environmentId, req).catch(() => void 0);
2574
+ if (this.enforceAuth(req, res, context)) return;
2575
+ return inner(req, res);
2576
+ }
2577
+ });
2578
+ }
2579
+ };
2580
+ this.routeManager = guardedRouteManager;
2581
+ try {
2582
+ this.registerMetadataEndpointsInner(basePath);
2583
+ } finally {
2584
+ this.routeManager = realRouteManager;
2585
+ }
2586
+ }
2587
+ registerMetadataEndpointsInner(basePath) {
2516
2588
  const { metadata } = this.config;
2517
2589
  const metaPath = `${basePath}${metadata.prefix}`;
2518
2590
  const isScoped = basePath.includes("/environments/:environmentId");