@objectstack/rest 14.3.0 → 14.6.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
@@ -508,9 +508,26 @@ declare class RestServer {
508
508
  private _openApiSpecCache;
509
509
  private loadOpenApiSpec;
510
510
  /**
511
- * Register metadata endpoints
511
+ * Register the metadata routes behind the SAME `requireAuth` gate the
512
+ * `/data` routes use.
513
+ *
514
+ * `registerMetadataEndpoints` builds ~17 `/meta/*` routes but — unlike the
515
+ * `/data` handlers — never calls {@link enforceAuth}: its handlers assumed
516
+ * the `requireAuth` gate rejected anonymous callers "upstream", yet nothing
517
+ * upstream covers `/meta`, so on a `requireAuth` deployment an anonymous
518
+ * caller could read object / field schemas. On a tenant-less runtime host
519
+ * those are SYSTEM-object schemas and the host is publicly reachable — a
520
+ * real leak.
521
+ *
522
+ * Rather than add the gate to every handler (and have the next new route
523
+ * forget it — the exact failure mode that caused this), wrap the route
524
+ * registrar for the duration of registration so every meta route, present
525
+ * and future, inherits it. The check is a no-op when `requireAuth` is off
526
+ * (demo / single-tenant), so the previously-public metadata surface there
527
+ * is unchanged; an authenticated user passes exactly as on `/data`.
512
528
  */
513
529
  private registerMetadataEndpoints;
530
+ private registerMetadataEndpointsInner;
514
531
  /**
515
532
  * Register UI endpoints
516
533
  */
@@ -770,6 +787,14 @@ interface ExportFieldMeta {
770
787
  reference?: string;
771
788
  /** Field on the referenced record to show as its label. */
772
789
  displayField?: string;
790
+ /** Field is required — a value (or default) must exist on insert. */
791
+ required?: boolean;
792
+ /** Engine-owned column the client never supplies (never required of import). */
793
+ system?: boolean;
794
+ /** Read-only column the client never supplies (never required of import). */
795
+ readonly?: boolean;
796
+ /** Field declares a `defaultValue` the engine applies on insert (satisfies required). */
797
+ hasDefault?: boolean;
773
798
  }
774
799
  /**
775
800
  * Build a field-name → metadata map from an object schema (best-effort).
package/dist/index.d.ts CHANGED
@@ -508,9 +508,26 @@ declare class RestServer {
508
508
  private _openApiSpecCache;
509
509
  private loadOpenApiSpec;
510
510
  /**
511
- * Register metadata endpoints
511
+ * Register the metadata routes behind the SAME `requireAuth` gate the
512
+ * `/data` routes use.
513
+ *
514
+ * `registerMetadataEndpoints` builds ~17 `/meta/*` routes but — unlike the
515
+ * `/data` handlers — never calls {@link enforceAuth}: its handlers assumed
516
+ * the `requireAuth` gate rejected anonymous callers "upstream", yet nothing
517
+ * upstream covers `/meta`, so on a `requireAuth` deployment an anonymous
518
+ * caller could read object / field schemas. On a tenant-less runtime host
519
+ * those are SYSTEM-object schemas and the host is publicly reachable — a
520
+ * real leak.
521
+ *
522
+ * Rather than add the gate to every handler (and have the next new route
523
+ * forget it — the exact failure mode that caused this), wrap the route
524
+ * registrar for the duration of registration so every meta route, present
525
+ * and future, inherits it. The check is a no-op when `requireAuth` is off
526
+ * (demo / single-tenant), so the previously-public metadata surface there
527
+ * is unchanged; an authenticated user passes exactly as on `/data`.
512
528
  */
513
529
  private registerMetadataEndpoints;
530
+ private registerMetadataEndpointsInner;
514
531
  /**
515
532
  * Register UI endpoints
516
533
  */
@@ -770,6 +787,14 @@ interface ExportFieldMeta {
770
787
  reference?: string;
771
788
  /** Field on the referenced record to show as its label. */
772
789
  displayField?: string;
790
+ /** Field is required — a value (or default) must exist on insert. */
791
+ required?: boolean;
792
+ /** Engine-owned column the client never supplies (never required of import). */
793
+ system?: boolean;
794
+ /** Read-only column the client never supplies (never required of import). */
795
+ readonly?: boolean;
796
+ /** Field declares a `defaultValue` the engine applies on insert (satisfies required). */
797
+ hasDefault?: boolean;
773
798
  }
774
799
  /**
775
800
  * Build a field-name → metadata map from an object schema (best-effort).
package/dist/index.js CHANGED
@@ -250,7 +250,14 @@ function buildFieldMetaMap(schema) {
250
250
  label: typeof f.label === "string" ? f.label : void 0,
251
251
  options: Array.isArray(f.options) ? f.options : void 0,
252
252
  reference: typeof f.reference === "string" ? f.reference : void 0,
253
- displayField: typeof f.displayField === "string" ? f.displayField : void 0
253
+ displayField: typeof f.displayField === "string" ? f.displayField : void 0,
254
+ required: f.required === true,
255
+ system: f.system === true,
256
+ readonly: f.readonly === true,
257
+ // Mirror the engine's `applyFieldDefaults` gate (`f.defaultValue == null`
258
+ // ⇒ no default): any non-null default — literal, expression object, or the
259
+ // `current_user` token — counts as satisfying a required field.
260
+ hasDefault: f.defaultValue != null
254
261
  });
255
262
  }
256
263
  return map;
@@ -520,6 +527,27 @@ async function coerceFieldValue(raw, meta, ctx) {
520
527
  }
521
528
  return { value: trim && typeof raw === "string" ? raw.trim() : raw };
522
529
  }
530
+ var REQUIRED_CHECK_SKIP = /* @__PURE__ */ new Set([
531
+ "id",
532
+ "created_at",
533
+ "created_by",
534
+ "updated_at",
535
+ "updated_by"
536
+ ]);
537
+ function isBlankValue(v) {
538
+ return v === void 0 || v === null || typeof v === "string" && v.trim() === "";
539
+ }
540
+ function firstMissingRequiredField(data, metaMap) {
541
+ for (const meta of metaMap.values()) {
542
+ if (!meta.required) continue;
543
+ if (meta.system || meta.readonly) continue;
544
+ if (meta.hasDefault) continue;
545
+ if (meta.type === "autonumber") continue;
546
+ if (REQUIRED_CHECK_SKIP.has(meta.name)) continue;
547
+ if (isBlankValue(data[meta.name])) return meta.name;
548
+ }
549
+ return null;
550
+ }
523
551
  async function coerceRow(rawRow, metaMap, ctx) {
524
552
  const data = {};
525
553
  const errors = [];
@@ -718,7 +746,11 @@ function runImport(opts) {
718
746
  if (!handled) {
719
747
  const willUpdate = existing && typeof existing === "object";
720
748
  const willCreate = !willUpdate && (writeMode === "insert" || writeMode === "upsert");
721
- if (!willUpdate && !willCreate) {
749
+ const requiredMiss = willCreate && !runAutomations ? firstMissingRequiredField(data, metaMap) : null;
750
+ if (requiredMiss) {
751
+ errCount++;
752
+ results[i] = { row: rowNo, ok: false, action: "failed", field: requiredMiss, code: "required", error: `${requiredMiss} is required` };
753
+ } else if (!willUpdate && !willCreate) {
722
754
  skipped++;
723
755
  results[i] = { row: rowNo, ok: true, action: "skipped", code: "NO_MATCH" };
724
756
  } else if (dryRun) {
@@ -2463,9 +2495,49 @@ var RestServer = class _RestServer {
2463
2495
  }
2464
2496
  }
2465
2497
  /**
2466
- * Register metadata endpoints
2498
+ * Register the metadata routes behind the SAME `requireAuth` gate the
2499
+ * `/data` routes use.
2500
+ *
2501
+ * `registerMetadataEndpoints` builds ~17 `/meta/*` routes but — unlike the
2502
+ * `/data` handlers — never calls {@link enforceAuth}: its handlers assumed
2503
+ * the `requireAuth` gate rejected anonymous callers "upstream", yet nothing
2504
+ * upstream covers `/meta`, so on a `requireAuth` deployment an anonymous
2505
+ * caller could read object / field schemas. On a tenant-less runtime host
2506
+ * those are SYSTEM-object schemas and the host is publicly reachable — a
2507
+ * real leak.
2508
+ *
2509
+ * Rather than add the gate to every handler (and have the next new route
2510
+ * forget it — the exact failure mode that caused this), wrap the route
2511
+ * registrar for the duration of registration so every meta route, present
2512
+ * and future, inherits it. The check is a no-op when `requireAuth` is off
2513
+ * (demo / single-tenant), so the previously-public metadata surface there
2514
+ * is unchanged; an authenticated user passes exactly as on `/data`.
2467
2515
  */
2468
2516
  registerMetadataEndpoints(basePath) {
2517
+ const realRouteManager = this.routeManager;
2518
+ const guardedRouteManager = {
2519
+ register: (entry) => {
2520
+ const inner = entry.handler;
2521
+ if (typeof inner !== "function") return realRouteManager.register(entry);
2522
+ return realRouteManager.register({
2523
+ ...entry,
2524
+ handler: async (req, res) => {
2525
+ const environmentId = req?.params?.environmentId;
2526
+ const context = await this.resolveExecCtx(environmentId, req).catch(() => void 0);
2527
+ if (this.enforceAuth(req, res, context)) return;
2528
+ return inner(req, res);
2529
+ }
2530
+ });
2531
+ }
2532
+ };
2533
+ this.routeManager = guardedRouteManager;
2534
+ try {
2535
+ this.registerMetadataEndpointsInner(basePath);
2536
+ } finally {
2537
+ this.routeManager = realRouteManager;
2538
+ }
2539
+ }
2540
+ registerMetadataEndpointsInner(basePath) {
2469
2541
  const { metadata } = this.config;
2470
2542
  const metaPath = `${basePath}${metadata.prefix}`;
2471
2543
  const isScoped = basePath.includes("/environments/:environmentId");