@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.cjs +75 -3
- package/dist/index.cjs.map +1 -1
- package/dist/index.d.cts +26 -1
- package/dist/index.d.ts +26 -1
- package/dist/index.js +75 -3
- package/dist/index.js.map +1 -1
- package/package.json +9 -9
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
|
|
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
|
|
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
|
-
|
|
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
|
|
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");
|