@objectstack/rest 5.1.0 → 5.2.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
@@ -343,6 +343,31 @@ declare class RestServer {
343
343
  * Register discovery endpoints
344
344
  */
345
345
  private registerDiscoveryEndpoints;
346
+ /**
347
+ * Register OpenAPI 3.1 spec + interactive docs viewer.
348
+ *
349
+ * GET <basePath>/openapi.json → enriched OpenAPI document
350
+ * GET <basePath>/docs → Scalar-rendered HTML (CDN, no dep)
351
+ *
352
+ * Enrichment at request time:
353
+ * - servers[0].url — derived from the request's Host header
354
+ * - paths — `{object}` placeholders expanded into
355
+ * one concrete path per registered object
356
+ * from the protocol's discovery metadata
357
+ *
358
+ * The base spec is loaded lazily from @objectstack/spec/openapi.json
359
+ * (shipped pre-generated by spec's build pipeline) so we don't pay
360
+ * the cost of regenerating on every request, and a missing or
361
+ * malformed file degrades to a stub instead of crashing.
362
+ */
363
+ private registerOpenApiEndpoints;
364
+ /**
365
+ * Lazily load the OpenAPI spec JSON shipped by @objectstack/spec.
366
+ * Cached after first read. Resilient to missing files / parse errors
367
+ * so a degraded environment still boots.
368
+ */
369
+ private _openApiSpecCache;
370
+ private loadOpenApiSpec;
346
371
  /**
347
372
  * Register metadata endpoints
348
373
  */
package/dist/index.d.ts CHANGED
@@ -343,6 +343,31 @@ declare class RestServer {
343
343
  * Register discovery endpoints
344
344
  */
345
345
  private registerDiscoveryEndpoints;
346
+ /**
347
+ * Register OpenAPI 3.1 spec + interactive docs viewer.
348
+ *
349
+ * GET <basePath>/openapi.json → enriched OpenAPI document
350
+ * GET <basePath>/docs → Scalar-rendered HTML (CDN, no dep)
351
+ *
352
+ * Enrichment at request time:
353
+ * - servers[0].url — derived from the request's Host header
354
+ * - paths — `{object}` placeholders expanded into
355
+ * one concrete path per registered object
356
+ * from the protocol's discovery metadata
357
+ *
358
+ * The base spec is loaded lazily from @objectstack/spec/openapi.json
359
+ * (shipped pre-generated by spec's build pipeline) so we don't pay
360
+ * the cost of regenerating on every request, and a missing or
361
+ * malformed file degrades to a stub instead of crashing.
362
+ */
363
+ private registerOpenApiEndpoints;
364
+ /**
365
+ * Lazily load the OpenAPI spec JSON shipped by @objectstack/spec.
366
+ * Cached after first read. Resilient to missing files / parse errors
367
+ * so a degraded environment still boots.
368
+ */
369
+ private _openApiSpecCache;
370
+ private loadOpenApiSpec;
346
371
  /**
347
372
  * Register metadata endpoints
348
373
  */
package/dist/index.js CHANGED
@@ -404,6 +404,12 @@ function rowsToCsv(fields, rows, includeHeader) {
404
404
  }
405
405
  var RestServer = class {
406
406
  constructor(server, protocol, config = {}, kernelManager, envRegistry, defaultProjectIdProvider, authServiceProvider, objectQLProvider, emailServiceProvider, sharingServiceProvider, reportsServiceProvider, approvalsServiceProvider, sharingRulesServiceProvider, i18nServiceProvider) {
407
+ /**
408
+ * Lazily load the OpenAPI spec JSON shipped by @objectstack/spec.
409
+ * Cached after first read. Resilient to missing files / parse errors
410
+ * so a degraded environment still boots.
411
+ */
412
+ this._openApiSpecCache = void 0;
407
413
  this.protocol = protocol;
408
414
  this.config = this.normalizeConfig(config);
409
415
  this.routeManager = new RouteManager(server);
@@ -670,12 +676,40 @@ var RestServer = class {
670
676
  }
671
677
  } catch {
672
678
  }
679
+ let org_user_ids = [userId];
680
+ if (tenantId) {
681
+ try {
682
+ let ql;
683
+ if (kernel) {
684
+ ql = await kernel.getServiceAsync("objectql").catch(() => void 0);
685
+ }
686
+ if (!ql && this.objectQLProvider) {
687
+ ql = await this.objectQLProvider(projectId).catch(() => void 0);
688
+ }
689
+ if (ql && typeof ql.find === "function") {
690
+ const sysOpts = { context: { isSystem: true } };
691
+ const memberRows = await ql.find("sys_member", {
692
+ where: { organization_id: tenantId },
693
+ limit: 1e3,
694
+ ...sysOpts
695
+ }).catch(() => []);
696
+ const ids = /* @__PURE__ */ new Set([userId]);
697
+ for (const m of memberRows ?? []) {
698
+ const uid = m.user_id ?? m.userId;
699
+ if (typeof uid === "string" && uid.length > 0) ids.add(uid);
700
+ }
701
+ org_user_ids = Array.from(ids);
702
+ }
703
+ } catch {
704
+ }
705
+ }
673
706
  return {
674
707
  userId,
675
708
  tenantId,
676
709
  roles,
677
710
  permissions,
678
- isSystem: false
711
+ isSystem: false,
712
+ org_user_ids
679
713
  };
680
714
  } catch {
681
715
  return void 0;
@@ -817,6 +851,7 @@ var RestServer = class {
817
851
  enableUi: api.enableUi ?? true,
818
852
  enableBatch: api.enableBatch ?? true,
819
853
  enableDiscovery: api.enableDiscovery ?? true,
854
+ enableOpenApi: api.enableOpenApi ?? true,
820
855
  enableSearch: api.enableSearch ?? true,
821
856
  enableProjectScoping: api.enableProjectScoping ?? false,
822
857
  projectResolution: api.projectResolution ?? "auto",
@@ -896,6 +931,9 @@ var RestServer = class {
896
931
  if (this.config.api.enableDiscovery) {
897
932
  this.registerDiscoveryEndpoints(bp);
898
933
  }
934
+ if (this.config.api.enableOpenApi ?? true) {
935
+ this.registerOpenApiEndpoints(bp);
936
+ }
899
937
  if (this.config.api.enableMetadata) {
900
938
  this.registerMetadataEndpoints(bp);
901
939
  }
@@ -987,6 +1025,135 @@ var RestServer = class {
987
1025
  }
988
1026
  });
989
1027
  }
1028
+ /**
1029
+ * Register OpenAPI 3.1 spec + interactive docs viewer.
1030
+ *
1031
+ * GET <basePath>/openapi.json → enriched OpenAPI document
1032
+ * GET <basePath>/docs → Scalar-rendered HTML (CDN, no dep)
1033
+ *
1034
+ * Enrichment at request time:
1035
+ * - servers[0].url — derived from the request's Host header
1036
+ * - paths — `{object}` placeholders expanded into
1037
+ * one concrete path per registered object
1038
+ * from the protocol's discovery metadata
1039
+ *
1040
+ * The base spec is loaded lazily from @objectstack/spec/openapi.json
1041
+ * (shipped pre-generated by spec's build pipeline) so we don't pay
1042
+ * the cost of regenerating on every request, and a missing or
1043
+ * malformed file degrades to a stub instead of crashing.
1044
+ */
1045
+ registerOpenApiEndpoints(basePath) {
1046
+ const isScoped = basePath.includes("/projects/:projectId");
1047
+ const openApiHandler = async (req, res) => {
1048
+ try {
1049
+ const spec = await this.loadOpenApiSpec();
1050
+ if (!spec) {
1051
+ res.status?.(503);
1052
+ res.json({
1053
+ error: "openapi_unavailable",
1054
+ message: "OpenAPI spec is not bundled with this runtime."
1055
+ });
1056
+ return;
1057
+ }
1058
+ const enriched = { ...spec, servers: [...spec.servers ?? []] };
1059
+ const host = req.headers?.host ?? req.headers?.["host"];
1060
+ const proto = req.headers?.["x-forwarded-proto"] || req.protocol || "http";
1061
+ if (host) {
1062
+ enriched.servers = [
1063
+ { url: `${proto}://${host}`, description: "Current server" },
1064
+ ...spec.servers ?? []
1065
+ ];
1066
+ }
1067
+ try {
1068
+ const projectId = isScoped ? req.params?.projectId : void 0;
1069
+ const protocol = await this.resolveProtocol(projectId, req);
1070
+ const items = await protocol?.getMetaItems?.({ type: "object" }).catch(() => null);
1071
+ const objects = Array.isArray(items?.items) ? items.items.map((i) => i?.name).filter(Boolean) : Array.isArray(items) ? items.map((i) => i?.name).filter(Boolean) : [];
1072
+ if (objects.length > 0 && enriched.paths) {
1073
+ const expanded = {};
1074
+ for (const [p, def] of Object.entries(enriched.paths)) {
1075
+ if (p.includes("{object}")) {
1076
+ expanded[p] = { ...def, "x-template": true };
1077
+ for (const obj of objects) {
1078
+ expanded[p.replace("{object}", obj)] = def;
1079
+ }
1080
+ } else {
1081
+ expanded[p] = def;
1082
+ }
1083
+ }
1084
+ enriched.paths = expanded;
1085
+ }
1086
+ } catch {
1087
+ }
1088
+ if (enriched.info) {
1089
+ enriched.info = {
1090
+ ...enriched.info,
1091
+ version: this.config.api.version || enriched.info.version
1092
+ };
1093
+ }
1094
+ res.json(enriched);
1095
+ } catch (error) {
1096
+ logError("[REST] openapi.json error:", error);
1097
+ sendError(res, error);
1098
+ }
1099
+ };
1100
+ this.routeManager.register({
1101
+ method: "GET",
1102
+ path: `${basePath}/openapi.json`,
1103
+ handler: openApiHandler,
1104
+ metadata: {
1105
+ summary: "OpenAPI 3.1 specification (machine-readable)",
1106
+ tags: ["openapi"]
1107
+ }
1108
+ });
1109
+ this.routeManager.register({
1110
+ method: "GET",
1111
+ path: `${basePath}/docs`,
1112
+ handler: async (req, res) => {
1113
+ const reqPath = req.path || req.url || `${basePath}/docs`;
1114
+ const apiBase = reqPath.replace(/\/docs\/?$/, "");
1115
+ const specUrl = `${apiBase}/openapi.json`;
1116
+ const html = `<!doctype html>
1117
+ <html>
1118
+ <head>
1119
+ <meta charset="utf-8" />
1120
+ <meta name="viewport" content="width=device-width, initial-scale=1" />
1121
+ <title>ObjectStack API Docs</title>
1122
+ </head>
1123
+ <body>
1124
+ <script id="api-reference" data-url="${specUrl}"></script>
1125
+ <script src="https://cdn.jsdelivr.net/npm/@scalar/api-reference"></script>
1126
+ </body>
1127
+ </html>`;
1128
+ if (res.setHeader) res.setHeader("content-type", "text/html; charset=utf-8");
1129
+ if (res.send) res.send(html);
1130
+ else if (res.body) res.body = html;
1131
+ else res.json?.(html);
1132
+ },
1133
+ metadata: {
1134
+ summary: "Interactive API docs (Scalar viewer)",
1135
+ tags: ["openapi"]
1136
+ }
1137
+ });
1138
+ }
1139
+ async loadOpenApiSpec() {
1140
+ if (this._openApiSpecCache !== void 0) return this._openApiSpecCache;
1141
+ try {
1142
+ const mod = await import("module");
1143
+ const requireFn = mod.createRequire(import.meta.url);
1144
+ const pkgJsonPath = requireFn.resolve("@objectstack/spec/package.json");
1145
+ const pathMod = await import("path");
1146
+ const fsMod = await import("fs");
1147
+ const specPath = pathMod.join(pathMod.dirname(pkgJsonPath), "json-schema", "openapi.json");
1148
+ const raw = await fsMod.promises.readFile(specPath, "utf-8");
1149
+ this._openApiSpecCache = JSON.parse(raw);
1150
+ return this._openApiSpecCache;
1151
+ } catch (err) {
1152
+ logError("[REST] Failed to load OpenAPI spec:", err?.message ?? err);
1153
+ this._openApiSpecCache = null;
1154
+ return null;
1155
+ }
1156
+ }
990
1157
  /**
991
1158
  * Register metadata endpoints
992
1159
  */