@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.cjs +169 -1
- package/dist/index.cjs.map +1 -1
- package/dist/index.d.cts +25 -0
- package/dist/index.d.ts +25 -0
- package/dist/index.js +168 -1
- package/dist/index.js.map +1 -1
- package/package.json +4 -4
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
|
*/
|