@objectstack/rest 5.0.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.cjs CHANGED
@@ -248,6 +248,7 @@ var RouteGroupBuilder = class {
248
248
  };
249
249
 
250
250
  // src/rest-server.ts
251
+ var import_meta = {};
251
252
  var logError = (...args) => globalThis.console?.error(...args);
252
253
  function mapDataError(error, object) {
253
254
  if (error?.code === "CONCURRENT_UPDATE" || error?.name === "ConcurrentUpdateError") {
@@ -443,6 +444,12 @@ function rowsToCsv(fields, rows, includeHeader) {
443
444
  }
444
445
  var RestServer = class {
445
446
  constructor(server, protocol, config = {}, kernelManager, envRegistry, defaultProjectIdProvider, authServiceProvider, objectQLProvider, emailServiceProvider, sharingServiceProvider, reportsServiceProvider, approvalsServiceProvider, sharingRulesServiceProvider, i18nServiceProvider) {
447
+ /**
448
+ * Lazily load the OpenAPI spec JSON shipped by @objectstack/spec.
449
+ * Cached after first read. Resilient to missing files / parse errors
450
+ * so a degraded environment still boots.
451
+ */
452
+ this._openApiSpecCache = void 0;
446
453
  this.protocol = protocol;
447
454
  this.config = this.normalizeConfig(config);
448
455
  this.routeManager = new RouteManager(server);
@@ -709,12 +716,40 @@ var RestServer = class {
709
716
  }
710
717
  } catch {
711
718
  }
719
+ let org_user_ids = [userId];
720
+ if (tenantId) {
721
+ try {
722
+ let ql;
723
+ if (kernel) {
724
+ ql = await kernel.getServiceAsync("objectql").catch(() => void 0);
725
+ }
726
+ if (!ql && this.objectQLProvider) {
727
+ ql = await this.objectQLProvider(projectId).catch(() => void 0);
728
+ }
729
+ if (ql && typeof ql.find === "function") {
730
+ const sysOpts = { context: { isSystem: true } };
731
+ const memberRows = await ql.find("sys_member", {
732
+ where: { organization_id: tenantId },
733
+ limit: 1e3,
734
+ ...sysOpts
735
+ }).catch(() => []);
736
+ const ids = /* @__PURE__ */ new Set([userId]);
737
+ for (const m of memberRows ?? []) {
738
+ const uid = m.user_id ?? m.userId;
739
+ if (typeof uid === "string" && uid.length > 0) ids.add(uid);
740
+ }
741
+ org_user_ids = Array.from(ids);
742
+ }
743
+ } catch {
744
+ }
745
+ }
712
746
  return {
713
747
  userId,
714
748
  tenantId,
715
749
  roles,
716
750
  permissions,
717
- isSystem: false
751
+ isSystem: false,
752
+ org_user_ids
718
753
  };
719
754
  } catch {
720
755
  return void 0;
@@ -856,6 +891,7 @@ var RestServer = class {
856
891
  enableUi: api.enableUi ?? true,
857
892
  enableBatch: api.enableBatch ?? true,
858
893
  enableDiscovery: api.enableDiscovery ?? true,
894
+ enableOpenApi: api.enableOpenApi ?? true,
859
895
  enableSearch: api.enableSearch ?? true,
860
896
  enableProjectScoping: api.enableProjectScoping ?? false,
861
897
  projectResolution: api.projectResolution ?? "auto",
@@ -935,6 +971,9 @@ var RestServer = class {
935
971
  if (this.config.api.enableDiscovery) {
936
972
  this.registerDiscoveryEndpoints(bp);
937
973
  }
974
+ if (this.config.api.enableOpenApi ?? true) {
975
+ this.registerOpenApiEndpoints(bp);
976
+ }
938
977
  if (this.config.api.enableMetadata) {
939
978
  this.registerMetadataEndpoints(bp);
940
979
  }
@@ -1026,6 +1065,135 @@ var RestServer = class {
1026
1065
  }
1027
1066
  });
1028
1067
  }
1068
+ /**
1069
+ * Register OpenAPI 3.1 spec + interactive docs viewer.
1070
+ *
1071
+ * GET <basePath>/openapi.json → enriched OpenAPI document
1072
+ * GET <basePath>/docs → Scalar-rendered HTML (CDN, no dep)
1073
+ *
1074
+ * Enrichment at request time:
1075
+ * - servers[0].url — derived from the request's Host header
1076
+ * - paths — `{object}` placeholders expanded into
1077
+ * one concrete path per registered object
1078
+ * from the protocol's discovery metadata
1079
+ *
1080
+ * The base spec is loaded lazily from @objectstack/spec/openapi.json
1081
+ * (shipped pre-generated by spec's build pipeline) so we don't pay
1082
+ * the cost of regenerating on every request, and a missing or
1083
+ * malformed file degrades to a stub instead of crashing.
1084
+ */
1085
+ registerOpenApiEndpoints(basePath) {
1086
+ const isScoped = basePath.includes("/projects/:projectId");
1087
+ const openApiHandler = async (req, res) => {
1088
+ try {
1089
+ const spec = await this.loadOpenApiSpec();
1090
+ if (!spec) {
1091
+ res.status?.(503);
1092
+ res.json({
1093
+ error: "openapi_unavailable",
1094
+ message: "OpenAPI spec is not bundled with this runtime."
1095
+ });
1096
+ return;
1097
+ }
1098
+ const enriched = { ...spec, servers: [...spec.servers ?? []] };
1099
+ const host = req.headers?.host ?? req.headers?.["host"];
1100
+ const proto = req.headers?.["x-forwarded-proto"] || req.protocol || "http";
1101
+ if (host) {
1102
+ enriched.servers = [
1103
+ { url: `${proto}://${host}`, description: "Current server" },
1104
+ ...spec.servers ?? []
1105
+ ];
1106
+ }
1107
+ try {
1108
+ const projectId = isScoped ? req.params?.projectId : void 0;
1109
+ const protocol = await this.resolveProtocol(projectId, req);
1110
+ const items = await protocol?.getMetaItems?.({ type: "object" }).catch(() => null);
1111
+ const objects = Array.isArray(items?.items) ? items.items.map((i) => i?.name).filter(Boolean) : Array.isArray(items) ? items.map((i) => i?.name).filter(Boolean) : [];
1112
+ if (objects.length > 0 && enriched.paths) {
1113
+ const expanded = {};
1114
+ for (const [p, def] of Object.entries(enriched.paths)) {
1115
+ if (p.includes("{object}")) {
1116
+ expanded[p] = { ...def, "x-template": true };
1117
+ for (const obj of objects) {
1118
+ expanded[p.replace("{object}", obj)] = def;
1119
+ }
1120
+ } else {
1121
+ expanded[p] = def;
1122
+ }
1123
+ }
1124
+ enriched.paths = expanded;
1125
+ }
1126
+ } catch {
1127
+ }
1128
+ if (enriched.info) {
1129
+ enriched.info = {
1130
+ ...enriched.info,
1131
+ version: this.config.api.version || enriched.info.version
1132
+ };
1133
+ }
1134
+ res.json(enriched);
1135
+ } catch (error) {
1136
+ logError("[REST] openapi.json error:", error);
1137
+ sendError(res, error);
1138
+ }
1139
+ };
1140
+ this.routeManager.register({
1141
+ method: "GET",
1142
+ path: `${basePath}/openapi.json`,
1143
+ handler: openApiHandler,
1144
+ metadata: {
1145
+ summary: "OpenAPI 3.1 specification (machine-readable)",
1146
+ tags: ["openapi"]
1147
+ }
1148
+ });
1149
+ this.routeManager.register({
1150
+ method: "GET",
1151
+ path: `${basePath}/docs`,
1152
+ handler: async (req, res) => {
1153
+ const reqPath = req.path || req.url || `${basePath}/docs`;
1154
+ const apiBase = reqPath.replace(/\/docs\/?$/, "");
1155
+ const specUrl = `${apiBase}/openapi.json`;
1156
+ const html = `<!doctype html>
1157
+ <html>
1158
+ <head>
1159
+ <meta charset="utf-8" />
1160
+ <meta name="viewport" content="width=device-width, initial-scale=1" />
1161
+ <title>ObjectStack API Docs</title>
1162
+ </head>
1163
+ <body>
1164
+ <script id="api-reference" data-url="${specUrl}"></script>
1165
+ <script src="https://cdn.jsdelivr.net/npm/@scalar/api-reference"></script>
1166
+ </body>
1167
+ </html>`;
1168
+ if (res.setHeader) res.setHeader("content-type", "text/html; charset=utf-8");
1169
+ if (res.send) res.send(html);
1170
+ else if (res.body) res.body = html;
1171
+ else res.json?.(html);
1172
+ },
1173
+ metadata: {
1174
+ summary: "Interactive API docs (Scalar viewer)",
1175
+ tags: ["openapi"]
1176
+ }
1177
+ });
1178
+ }
1179
+ async loadOpenApiSpec() {
1180
+ if (this._openApiSpecCache !== void 0) return this._openApiSpecCache;
1181
+ try {
1182
+ const mod = await import("module");
1183
+ const requireFn = mod.createRequire(import_meta.url);
1184
+ const pkgJsonPath = requireFn.resolve("@objectstack/spec/package.json");
1185
+ const pathMod = await import("path");
1186
+ const fsMod = await import("fs");
1187
+ const specPath = pathMod.join(pathMod.dirname(pkgJsonPath), "json-schema", "openapi.json");
1188
+ const raw = await fsMod.promises.readFile(specPath, "utf-8");
1189
+ this._openApiSpecCache = JSON.parse(raw);
1190
+ return this._openApiSpecCache;
1191
+ } catch (err) {
1192
+ logError("[REST] Failed to load OpenAPI spec:", err?.message ?? err);
1193
+ this._openApiSpecCache = null;
1194
+ return null;
1195
+ }
1196
+ }
1029
1197
  /**
1030
1198
  * Register metadata endpoints
1031
1199
  */