@objectstack/rest 4.0.3 → 4.0.5
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/README.md +94 -17
- package/dist/index.cjs +722 -60
- package/dist/index.cjs.map +1 -1
- package/dist/index.d.cts +133 -2
- package/dist/index.d.ts +133 -2
- package/dist/index.js +712 -60
- package/dist/index.js.map +1 -1
- package/package.json +32 -6
- package/.turbo/turbo-build.log +0 -22
- package/CHANGELOG.md +0 -473
- package/src/index.ts +0 -12
- package/src/rest-api-plugin.ts +0 -72
- package/src/rest-server.ts +0 -691
- package/src/rest.test.ts +0 -672
- package/src/route-manager.ts +0 -308
- package/tsconfig.json +0 -9
- package/vitest.config.ts +0 -10
package/dist/index.cjs
CHANGED
|
@@ -1,7 +1,9 @@
|
|
|
1
1
|
"use strict";
|
|
2
|
+
var __create = Object.create;
|
|
2
3
|
var __defProp = Object.defineProperty;
|
|
3
4
|
var __getOwnPropDesc = Object.getOwnPropertyDescriptor;
|
|
4
5
|
var __getOwnPropNames = Object.getOwnPropertyNames;
|
|
6
|
+
var __getProtoOf = Object.getPrototypeOf;
|
|
5
7
|
var __hasOwnProp = Object.prototype.hasOwnProperty;
|
|
6
8
|
var __export = (target, all) => {
|
|
7
9
|
for (var name in all)
|
|
@@ -15,6 +17,14 @@ var __copyProps = (to, from, except, desc) => {
|
|
|
15
17
|
}
|
|
16
18
|
return to;
|
|
17
19
|
};
|
|
20
|
+
var __toESM = (mod, isNodeMode, target) => (target = mod != null ? __create(__getProtoOf(mod)) : {}, __copyProps(
|
|
21
|
+
// If the importer is in node compatibility mode or this is not an ESM
|
|
22
|
+
// file that has been converted to a CommonJS file using a Babel-
|
|
23
|
+
// compatible transform (i.e. "__esModule" has not been set), then set
|
|
24
|
+
// "default" to the CommonJS "module.exports" for node compatibility.
|
|
25
|
+
isNodeMode || !mod || !mod.__esModule ? __defProp(target, "default", { value: mod, enumerable: true }) : target,
|
|
26
|
+
mod
|
|
27
|
+
));
|
|
18
28
|
var __toCommonJS = (mod) => __copyProps(__defProp({}, "__esModule", { value: true }), mod);
|
|
19
29
|
|
|
20
30
|
// src/index.ts
|
|
@@ -238,11 +248,360 @@ var RouteGroupBuilder = class {
|
|
|
238
248
|
};
|
|
239
249
|
|
|
240
250
|
// src/rest-server.ts
|
|
251
|
+
var logError = (...args) => globalThis.console?.error(...args);
|
|
252
|
+
function mapDataError(error, object) {
|
|
253
|
+
if (error?.code === "PERMISSION_DENIED" || error?.name === "PermissionDeniedError" || typeof error?.message === "string" && error.message.startsWith("[Security] Access denied")) {
|
|
254
|
+
return {
|
|
255
|
+
status: 403,
|
|
256
|
+
body: {
|
|
257
|
+
error: error?.message ?? "Permission denied",
|
|
258
|
+
code: "PERMISSION_DENIED",
|
|
259
|
+
...object ? { object } : {}
|
|
260
|
+
}
|
|
261
|
+
};
|
|
262
|
+
}
|
|
263
|
+
const raw = String(error?.message ?? error ?? "");
|
|
264
|
+
const lower = raw.toLowerCase();
|
|
265
|
+
if (raw.includes("[ProjectKernelFactory]") && (lower.includes("missing database_url") || lower.includes("not found"))) {
|
|
266
|
+
const isProvisioning = lower.includes("status='provisioning'") || lower.includes("status='pending'");
|
|
267
|
+
const isFailed = lower.includes("status='failed'");
|
|
268
|
+
return {
|
|
269
|
+
status: isProvisioning ? 503 : isFailed ? 502 : 404,
|
|
270
|
+
body: {
|
|
271
|
+
error: raw,
|
|
272
|
+
code: isProvisioning ? "PROJECT_PROVISIONING" : isFailed ? "PROJECT_PROVISIONING_FAILED" : "PROJECT_NOT_FOUND"
|
|
273
|
+
}
|
|
274
|
+
};
|
|
275
|
+
}
|
|
276
|
+
const looksLikeUnknownObject = lower.includes("no such table") || lower.includes("relation") && lower.includes("does not exist") || lower.includes("table not found") || lower.includes("unknown object") || lower.includes("object not found") || lower.includes("no driver available") || object !== void 0 && lower.includes(`'${object.toLowerCase()}'`) && lower.includes("not");
|
|
277
|
+
if (looksLikeUnknownObject) {
|
|
278
|
+
return {
|
|
279
|
+
status: 404,
|
|
280
|
+
body: {
|
|
281
|
+
error: object ? `Object '${object}' is not registered` : "Object not found",
|
|
282
|
+
code: "object_not_found",
|
|
283
|
+
object
|
|
284
|
+
}
|
|
285
|
+
};
|
|
286
|
+
}
|
|
287
|
+
return { status: 400, body: { error: raw || "Bad request" } };
|
|
288
|
+
}
|
|
289
|
+
function isExpectedDataStatus(status) {
|
|
290
|
+
return status === 403 || status === 404 || status === 502 || status === 503;
|
|
291
|
+
}
|
|
241
292
|
var RestServer = class {
|
|
242
|
-
constructor(server, protocol, config = {}) {
|
|
293
|
+
constructor(server, protocol, config = {}, kernelManager, envRegistry, defaultProjectIdProvider, authServiceProvider, objectQLProvider) {
|
|
243
294
|
this.protocol = protocol;
|
|
244
295
|
this.config = this.normalizeConfig(config);
|
|
245
296
|
this.routeManager = new RouteManager(server);
|
|
297
|
+
this.kernelManager = kernelManager;
|
|
298
|
+
this.envRegistry = envRegistry;
|
|
299
|
+
this.defaultProjectIdProvider = defaultProjectIdProvider;
|
|
300
|
+
this.authServiceProvider = authServiceProvider;
|
|
301
|
+
this.objectQLProvider = objectQLProvider;
|
|
302
|
+
}
|
|
303
|
+
/**
|
|
304
|
+
* Resolve the protocol for a given request. When `projectId` is present
|
|
305
|
+
* and a KernelManager is wired, fetch the per-project kernel's
|
|
306
|
+
* `protocol` service so metadata / data / UI reads hit the project's
|
|
307
|
+
* own registry and datastore.
|
|
308
|
+
*
|
|
309
|
+
* When `projectId` is absent on an unscoped route and an `envRegistry`
|
|
310
|
+
* is wired (runtime mode), the resolution chain is:
|
|
311
|
+
* 1. Hostname → projectId (`envRegistry.resolveByHostname`)
|
|
312
|
+
* 2. `X-Project-Id` header → projectId (`envRegistry.resolveById`)
|
|
313
|
+
* 3. Default-project fallback (`defaultProjectIdProvider`, set by
|
|
314
|
+
* `createSingleProjectPlugin`)
|
|
315
|
+
* 4. Control-plane protocol captured at boot.
|
|
316
|
+
*
|
|
317
|
+
* Special case: `projectId === 'platform'` is a reserved virtual id used
|
|
318
|
+
* by Studio to address the control plane through the regular project
|
|
319
|
+
* URL shape (`/projects/platform/...`). It is NOT a row in the projects
|
|
320
|
+
* table, so we must never call `KernelManager.getOrCreate('platform')`.
|
|
321
|
+
* Instead, return the control-plane protocol directly. This lets Studio
|
|
322
|
+
* (and any other client) speak a single, uniform URL family without
|
|
323
|
+
* duplicating route logic for the platform surface.
|
|
324
|
+
*/
|
|
325
|
+
async resolveProtocol(projectId, req) {
|
|
326
|
+
if (projectId === "platform") return this.protocol;
|
|
327
|
+
if (!projectId && req && this.envRegistry && this.kernelManager) {
|
|
328
|
+
const host = this.extractHostname(req);
|
|
329
|
+
if (host) {
|
|
330
|
+
try {
|
|
331
|
+
const result = await this.envRegistry.resolveByHostname(host);
|
|
332
|
+
if (result?.projectId) projectId = result.projectId;
|
|
333
|
+
} catch {
|
|
334
|
+
}
|
|
335
|
+
}
|
|
336
|
+
if (!projectId && typeof this.envRegistry.resolveById === "function") {
|
|
337
|
+
const headerVal = this.extractProjectIdHeader(req);
|
|
338
|
+
if (headerVal) {
|
|
339
|
+
try {
|
|
340
|
+
const driver = await this.envRegistry.resolveById(headerVal);
|
|
341
|
+
if (driver) projectId = headerVal;
|
|
342
|
+
} catch {
|
|
343
|
+
}
|
|
344
|
+
}
|
|
345
|
+
}
|
|
346
|
+
}
|
|
347
|
+
if (!projectId && this.defaultProjectIdProvider) {
|
|
348
|
+
try {
|
|
349
|
+
const def = this.defaultProjectIdProvider();
|
|
350
|
+
if (def) projectId = def;
|
|
351
|
+
} catch {
|
|
352
|
+
}
|
|
353
|
+
}
|
|
354
|
+
if (!projectId || !this.kernelManager) return this.protocol;
|
|
355
|
+
const kernel = await this.kernelManager.getOrCreate(projectId);
|
|
356
|
+
return kernel.getServiceAsync("protocol");
|
|
357
|
+
}
|
|
358
|
+
/**
|
|
359
|
+
* Resolve the i18n service for the request's project (or control plane
|
|
360
|
+
* when no project id is in scope). Returns `undefined` when no service is
|
|
361
|
+
* registered, so callers can short-circuit and skip translation rather
|
|
362
|
+
* than failing.
|
|
363
|
+
*
|
|
364
|
+
* Mirrors `resolveProtocol`'s lookup chain: explicit `projectId` from the
|
|
365
|
+
* route → kernel-managed `i18n` service. Control-plane / unscoped
|
|
366
|
+
* requests intentionally return `undefined` because the platform kernel
|
|
367
|
+
* does not own per-app translation bundles.
|
|
368
|
+
*/
|
|
369
|
+
async resolveI18nService(projectId) {
|
|
370
|
+
if (!projectId || projectId === "platform" || !this.kernelManager) return void 0;
|
|
371
|
+
try {
|
|
372
|
+
const kernel = await this.kernelManager.getOrCreate(projectId);
|
|
373
|
+
return await kernel.getServiceAsync("i18n");
|
|
374
|
+
} catch {
|
|
375
|
+
return void 0;
|
|
376
|
+
}
|
|
377
|
+
}
|
|
378
|
+
/**
|
|
379
|
+
* Resolve the request's execution context (RBAC/RLS/FLS) by looking up
|
|
380
|
+
* the better-auth session via the project's `auth` service. Returns
|
|
381
|
+
* `undefined` for anonymous requests so callers can pass `context` as-is
|
|
382
|
+
* to the protocol layer (the SecurityPlugin treats undefined as anon).
|
|
383
|
+
*/
|
|
384
|
+
async resolveExecCtx(projectId, req) {
|
|
385
|
+
try {
|
|
386
|
+
let authService;
|
|
387
|
+
let kernel;
|
|
388
|
+
if (projectId && projectId !== "platform" && this.kernelManager) {
|
|
389
|
+
kernel = await this.kernelManager.getOrCreate(projectId);
|
|
390
|
+
authService = await kernel.getServiceAsync("auth").catch(() => void 0);
|
|
391
|
+
}
|
|
392
|
+
if (!authService && this.defaultProjectIdProvider && this.kernelManager) {
|
|
393
|
+
try {
|
|
394
|
+
const def = this.defaultProjectIdProvider();
|
|
395
|
+
if (def) {
|
|
396
|
+
kernel = await this.kernelManager.getOrCreate(def);
|
|
397
|
+
authService = await kernel.getServiceAsync("auth").catch(() => void 0);
|
|
398
|
+
}
|
|
399
|
+
} catch {
|
|
400
|
+
}
|
|
401
|
+
}
|
|
402
|
+
if (!authService && this.authServiceProvider) {
|
|
403
|
+
authService = await this.authServiceProvider(projectId).catch(() => void 0);
|
|
404
|
+
}
|
|
405
|
+
if (!authService) return void 0;
|
|
406
|
+
let api = authService.api;
|
|
407
|
+
if (!api && typeof authService.getApi === "function") {
|
|
408
|
+
api = await authService.getApi();
|
|
409
|
+
}
|
|
410
|
+
if (!api?.getSession) return void 0;
|
|
411
|
+
const rawHeaders = req?.headers;
|
|
412
|
+
let headers;
|
|
413
|
+
if (rawHeaders && typeof rawHeaders.get === "function") {
|
|
414
|
+
headers = rawHeaders;
|
|
415
|
+
} else if (rawHeaders && typeof rawHeaders === "object") {
|
|
416
|
+
headers = new globalThis.Headers();
|
|
417
|
+
for (const [k, v] of Object.entries(rawHeaders)) {
|
|
418
|
+
if (Array.isArray(v)) v.forEach((x) => headers.append(k, String(x)));
|
|
419
|
+
else if (v != null) headers.set(k, String(v));
|
|
420
|
+
}
|
|
421
|
+
} else {
|
|
422
|
+
return void 0;
|
|
423
|
+
}
|
|
424
|
+
const session = await api.getSession({ headers });
|
|
425
|
+
if (!session?.user?.id) return void 0;
|
|
426
|
+
const userId = session.user.id;
|
|
427
|
+
const tenantId = session.session?.activeOrganizationId ?? void 0;
|
|
428
|
+
const permissions = [];
|
|
429
|
+
const roles = [];
|
|
430
|
+
try {
|
|
431
|
+
let ql;
|
|
432
|
+
if (kernel) {
|
|
433
|
+
ql = await kernel.getServiceAsync("objectql").catch(() => void 0);
|
|
434
|
+
}
|
|
435
|
+
if (!ql && this.objectQLProvider) {
|
|
436
|
+
ql = await this.objectQLProvider(projectId).catch(() => void 0);
|
|
437
|
+
}
|
|
438
|
+
if (ql && typeof ql.find === "function") {
|
|
439
|
+
const sysOpts = { context: { isSystem: true } };
|
|
440
|
+
const memberRows = await ql.find("sys_member", {
|
|
441
|
+
where: tenantId ? { user_id: userId, organization_id: tenantId } : { user_id: userId },
|
|
442
|
+
limit: 50,
|
|
443
|
+
...sysOpts
|
|
444
|
+
}).catch(() => []);
|
|
445
|
+
for (const m of memberRows ?? []) {
|
|
446
|
+
if (typeof m.role === "string") {
|
|
447
|
+
for (const r of m.role.split(",").map((s) => s.trim()).filter(Boolean)) {
|
|
448
|
+
if (!roles.includes(r)) roles.push(r);
|
|
449
|
+
}
|
|
450
|
+
}
|
|
451
|
+
}
|
|
452
|
+
const upsRows = await ql.find("sys_user_permission_set", {
|
|
453
|
+
where: { user_id: userId },
|
|
454
|
+
limit: 100,
|
|
455
|
+
...sysOpts
|
|
456
|
+
}).catch(() => []);
|
|
457
|
+
const psIds = /* @__PURE__ */ new Set();
|
|
458
|
+
for (const r of upsRows ?? []) {
|
|
459
|
+
const orgScope = r.organization_id ?? null;
|
|
460
|
+
if (!orgScope || tenantId && orgScope === tenantId) {
|
|
461
|
+
const pid = r.permission_set_id ?? r.permissionSetId;
|
|
462
|
+
if (pid) psIds.add(pid);
|
|
463
|
+
}
|
|
464
|
+
}
|
|
465
|
+
if (psIds.size > 0) {
|
|
466
|
+
const psRows = await ql.find("sys_permission_set", {
|
|
467
|
+
where: { id: { $in: Array.from(psIds) } },
|
|
468
|
+
limit: 500,
|
|
469
|
+
...sysOpts
|
|
470
|
+
}).catch(() => []);
|
|
471
|
+
for (const ps of psRows ?? []) {
|
|
472
|
+
if (ps.name && !permissions.includes(ps.name)) permissions.push(ps.name);
|
|
473
|
+
}
|
|
474
|
+
}
|
|
475
|
+
}
|
|
476
|
+
} catch {
|
|
477
|
+
}
|
|
478
|
+
return {
|
|
479
|
+
userId,
|
|
480
|
+
tenantId,
|
|
481
|
+
roles,
|
|
482
|
+
permissions,
|
|
483
|
+
isSystem: false
|
|
484
|
+
};
|
|
485
|
+
} catch {
|
|
486
|
+
return void 0;
|
|
487
|
+
}
|
|
488
|
+
}
|
|
489
|
+
/**
|
|
490
|
+
* Build a `TranslationBundle` (`Record<locale, TranslationData>`) from an
|
|
491
|
+
* `II18nService` instance. Returns `undefined` when no locales are
|
|
492
|
+
* registered so callers can avoid translation work.
|
|
493
|
+
*/
|
|
494
|
+
buildTranslationBundle(i18n) {
|
|
495
|
+
if (!i18n || typeof i18n.getLocales !== "function" || typeof i18n.getTranslations !== "function") {
|
|
496
|
+
return void 0;
|
|
497
|
+
}
|
|
498
|
+
const locales = i18n.getLocales();
|
|
499
|
+
if (!locales.length) return void 0;
|
|
500
|
+
const bundle = {};
|
|
501
|
+
for (const locale of locales) {
|
|
502
|
+
const data = i18n.getTranslations(locale);
|
|
503
|
+
if (data && typeof data === "object") bundle[locale] = data;
|
|
504
|
+
}
|
|
505
|
+
return Object.keys(bundle).length ? bundle : void 0;
|
|
506
|
+
}
|
|
507
|
+
/**
|
|
508
|
+
* Parse the highest-priority locale from an `Accept-Language` header.
|
|
509
|
+
* Falls back to a `?locale=` query parameter, then to the i18n service's
|
|
510
|
+
* default locale. Returns `undefined` when no preference is expressed
|
|
511
|
+
* (callers will then return untranslated metadata).
|
|
512
|
+
*/
|
|
513
|
+
extractLocale(req, i18n) {
|
|
514
|
+
const headers = req?.headers;
|
|
515
|
+
let header;
|
|
516
|
+
if (headers) {
|
|
517
|
+
header = typeof headers.get === "function" ? headers.get("accept-language") ?? void 0 : headers["accept-language"] ?? headers["Accept-Language"];
|
|
518
|
+
}
|
|
519
|
+
if (typeof header === "string" && header.length > 0) {
|
|
520
|
+
const top = header.split(",")[0]?.split(";")[0]?.trim();
|
|
521
|
+
if (top) return top;
|
|
522
|
+
}
|
|
523
|
+
const queryLocale = req?.query?.locale;
|
|
524
|
+
if (typeof queryLocale === "string" && queryLocale.length > 0) return queryLocale;
|
|
525
|
+
if (i18n && typeof i18n.getDefaultLocale === "function") {
|
|
526
|
+
const def = i18n.getDefaultLocale();
|
|
527
|
+
if (typeof def === "string" && def.length > 0) return def;
|
|
528
|
+
}
|
|
529
|
+
return void 0;
|
|
530
|
+
}
|
|
531
|
+
/**
|
|
532
|
+
* Translate a single metadata document (view or action) when an i18n
|
|
533
|
+
* service is registered for the request's project and the requested
|
|
534
|
+
* locale yields a match. Falls through unchanged for unsupported types
|
|
535
|
+
* or missing translations.
|
|
536
|
+
*/
|
|
537
|
+
async translateMetaItem(req, type, projectId, item) {
|
|
538
|
+
if (!item || typeof item !== "object") return item;
|
|
539
|
+
if (type !== "view" && type !== "action") return item;
|
|
540
|
+
const i18n = await this.resolveI18nService(projectId);
|
|
541
|
+
const bundle = this.buildTranslationBundle(i18n);
|
|
542
|
+
if (!bundle) return item;
|
|
543
|
+
const locale = this.extractLocale(req, i18n);
|
|
544
|
+
if (!locale) return item;
|
|
545
|
+
const { translateMetadataDocument } = await import("@objectstack/spec/system");
|
|
546
|
+
return translateMetadataDocument(type, item, bundle, { locale });
|
|
547
|
+
}
|
|
548
|
+
/**
|
|
549
|
+
* Translate a list of metadata documents using `translateMetaItem`.
|
|
550
|
+
*/
|
|
551
|
+
async translateMetaItems(req, type, projectId, items) {
|
|
552
|
+
if (!Array.isArray(items)) return items;
|
|
553
|
+
if (type !== "view" && type !== "action") return items;
|
|
554
|
+
const i18n = await this.resolveI18nService(projectId);
|
|
555
|
+
const bundle = this.buildTranslationBundle(i18n);
|
|
556
|
+
if (!bundle) return items;
|
|
557
|
+
const locale = this.extractLocale(req, i18n);
|
|
558
|
+
if (!locale) return items;
|
|
559
|
+
const { translateMetadataDocument } = await import("@objectstack/spec/system");
|
|
560
|
+
return items.map((item) => translateMetadataDocument(type, item, bundle, { locale }));
|
|
561
|
+
}
|
|
562
|
+
/**
|
|
563
|
+
* Pull the request hostname (without port) from a Node-style `req` or
|
|
564
|
+
* a Fetch-style request wrapper. Returns undefined when no Host header
|
|
565
|
+
* is available.
|
|
566
|
+
*/
|
|
567
|
+
extractHostname(req) {
|
|
568
|
+
const headers = req?.headers;
|
|
569
|
+
let host;
|
|
570
|
+
if (headers) {
|
|
571
|
+
if (typeof headers.get === "function") {
|
|
572
|
+
host = headers.get("host") ?? void 0;
|
|
573
|
+
} else {
|
|
574
|
+
host = headers.host ?? headers.Host;
|
|
575
|
+
}
|
|
576
|
+
}
|
|
577
|
+
if (!host && typeof req?.hostname === "string") host = req.hostname;
|
|
578
|
+
if (!host && typeof req?.url === "string") {
|
|
579
|
+
try {
|
|
580
|
+
host = new globalThis.URL(req.url).host;
|
|
581
|
+
} catch {
|
|
582
|
+
}
|
|
583
|
+
}
|
|
584
|
+
if (!host) return void 0;
|
|
585
|
+
return String(host).split(":")[0].toLowerCase();
|
|
586
|
+
}
|
|
587
|
+
/**
|
|
588
|
+
* Pull the `X-Project-Id` header from a Node- or Fetch-style request.
|
|
589
|
+
* Header names are case-insensitive; we probe both casings to cover
|
|
590
|
+
* adapters that don't normalize headers (e.g. raw Node http).
|
|
591
|
+
*/
|
|
592
|
+
extractProjectIdHeader(req) {
|
|
593
|
+
const headers = req?.headers;
|
|
594
|
+
if (!headers) return void 0;
|
|
595
|
+
let val;
|
|
596
|
+
if (typeof headers.get === "function") {
|
|
597
|
+
val = headers.get("x-project-id") ?? headers.get("X-Project-Id");
|
|
598
|
+
} else {
|
|
599
|
+
val = headers["x-project-id"] ?? headers["X-Project-Id"];
|
|
600
|
+
}
|
|
601
|
+
if (Array.isArray(val)) val = val[0];
|
|
602
|
+
if (typeof val !== "string") return void 0;
|
|
603
|
+
const trimmed = val.trim();
|
|
604
|
+
return trimmed.length > 0 ? trimmed : void 0;
|
|
246
605
|
}
|
|
247
606
|
/**
|
|
248
607
|
* Normalize configuration with defaults
|
|
@@ -263,6 +622,8 @@ var RestServer = class {
|
|
|
263
622
|
enableUi: api.enableUi ?? true,
|
|
264
623
|
enableBatch: api.enableBatch ?? true,
|
|
265
624
|
enableDiscovery: api.enableDiscovery ?? true,
|
|
625
|
+
enableProjectScoping: api.enableProjectScoping ?? false,
|
|
626
|
+
projectResolution: api.projectResolution ?? "auto",
|
|
266
627
|
documentation: api.documentation,
|
|
267
628
|
responseFormat: api.responseFormat
|
|
268
629
|
},
|
|
@@ -315,51 +676,88 @@ var RestServer = class {
|
|
|
315
676
|
const { api } = this.config;
|
|
316
677
|
return api.apiPath ?? `${api.basePath}/${api.version}`;
|
|
317
678
|
}
|
|
679
|
+
/**
|
|
680
|
+
* Get the project-scoped base path for a given unscoped base.
|
|
681
|
+
* Example: `/api/v1` → `/api/v1/projects/:projectId`.
|
|
682
|
+
*/
|
|
683
|
+
getScopedBasePath(basePath) {
|
|
684
|
+
return `${basePath}/projects/:projectId`;
|
|
685
|
+
}
|
|
318
686
|
/**
|
|
319
687
|
* Register all REST API routes
|
|
688
|
+
*
|
|
689
|
+
* When `enableProjectScoping` is true, routes are registered under
|
|
690
|
+
* `/api/v1/projects/:projectId/...`. The `projectResolution` strategy
|
|
691
|
+
* controls whether unscoped legacy routes remain available:
|
|
692
|
+
* - `required` → only scoped routes registered.
|
|
693
|
+
* - `optional` / `auto` → both scoped and unscoped routes registered.
|
|
320
694
|
*/
|
|
321
695
|
registerRoutes() {
|
|
322
696
|
const basePath = this.getApiBasePath();
|
|
323
|
-
|
|
324
|
-
|
|
325
|
-
|
|
326
|
-
|
|
327
|
-
|
|
328
|
-
|
|
329
|
-
|
|
330
|
-
|
|
331
|
-
|
|
332
|
-
|
|
333
|
-
|
|
334
|
-
|
|
335
|
-
|
|
336
|
-
|
|
697
|
+
const { enableProjectScoping, projectResolution } = this.config.api;
|
|
698
|
+
const registerForBase = (bp) => {
|
|
699
|
+
if (this.config.api.enableDiscovery) {
|
|
700
|
+
this.registerDiscoveryEndpoints(bp);
|
|
701
|
+
}
|
|
702
|
+
if (this.config.api.enableMetadata) {
|
|
703
|
+
this.registerMetadataEndpoints(bp);
|
|
704
|
+
}
|
|
705
|
+
if (this.config.api.enableUi) {
|
|
706
|
+
this.registerUiEndpoints(bp);
|
|
707
|
+
}
|
|
708
|
+
if (this.config.api.enableCrud) {
|
|
709
|
+
this.registerCrudEndpoints(bp);
|
|
710
|
+
}
|
|
711
|
+
if (this.config.api.enableBatch) {
|
|
712
|
+
this.registerBatchEndpoints(bp);
|
|
713
|
+
}
|
|
714
|
+
};
|
|
715
|
+
if (enableProjectScoping) {
|
|
716
|
+
const scopedBase = this.getScopedBasePath(basePath);
|
|
717
|
+
if (projectResolution === "required") {
|
|
718
|
+
registerForBase(scopedBase);
|
|
719
|
+
} else {
|
|
720
|
+
registerForBase(basePath);
|
|
721
|
+
registerForBase(scopedBase);
|
|
722
|
+
}
|
|
723
|
+
} else {
|
|
724
|
+
registerForBase(basePath);
|
|
337
725
|
}
|
|
338
726
|
}
|
|
339
727
|
/**
|
|
340
728
|
* Register discovery endpoints
|
|
341
729
|
*/
|
|
342
730
|
registerDiscoveryEndpoints(basePath) {
|
|
343
|
-
const
|
|
731
|
+
const isScoped = basePath.includes("/projects/:projectId");
|
|
732
|
+
const discoveryHandler = async (req, res) => {
|
|
344
733
|
try {
|
|
345
734
|
const discovery = await this.protocol.getDiscovery();
|
|
346
735
|
discovery.version = this.config.api.version;
|
|
736
|
+
const realBase = isScoped ? basePath.replace(":projectId", req.params?.projectId ?? ":projectId") : basePath;
|
|
347
737
|
if (discovery.routes) {
|
|
348
738
|
if (this.config.api.enableCrud) {
|
|
349
|
-
discovery.routes.data = `${
|
|
739
|
+
discovery.routes.data = `${realBase}${this.config.crud.dataPrefix}`;
|
|
350
740
|
}
|
|
351
741
|
if (this.config.api.enableMetadata) {
|
|
352
|
-
discovery.routes.metadata = `${
|
|
742
|
+
discovery.routes.metadata = `${realBase}${this.config.metadata.prefix}`;
|
|
353
743
|
}
|
|
354
744
|
if (this.config.api.enableUi) {
|
|
355
|
-
discovery.routes.ui = `${
|
|
745
|
+
discovery.routes.ui = `${realBase}/ui`;
|
|
356
746
|
}
|
|
357
747
|
if (discovery.routes.auth) {
|
|
358
|
-
|
|
748
|
+
const unscopedBase = isScoped ? basePath.replace(/\/projects\/:projectId$/, "") : basePath;
|
|
749
|
+
discovery.routes.auth = `${unscopedBase}/auth`;
|
|
359
750
|
}
|
|
360
751
|
}
|
|
752
|
+
discovery.scoping = {
|
|
753
|
+
enabled: this.config.api.enableProjectScoping,
|
|
754
|
+
resolution: this.config.api.projectResolution,
|
|
755
|
+
scoped: isScoped,
|
|
756
|
+
projectId: isScoped ? req.params?.projectId : void 0
|
|
757
|
+
};
|
|
361
758
|
res.json(discovery);
|
|
362
759
|
} catch (error) {
|
|
760
|
+
logError("[REST] Unhandled error:", error);
|
|
363
761
|
res.status(500).json({ error: error.message });
|
|
364
762
|
}
|
|
365
763
|
};
|
|
@@ -388,15 +786,19 @@ var RestServer = class {
|
|
|
388
786
|
registerMetadataEndpoints(basePath) {
|
|
389
787
|
const { metadata } = this.config;
|
|
390
788
|
const metaPath = `${basePath}${metadata.prefix}`;
|
|
789
|
+
const isScoped = basePath.includes("/projects/:projectId");
|
|
391
790
|
if (metadata.endpoints.types !== false) {
|
|
392
791
|
this.routeManager.register({
|
|
393
792
|
method: "GET",
|
|
394
793
|
path: metaPath,
|
|
395
|
-
handler: async (
|
|
794
|
+
handler: async (req, res) => {
|
|
396
795
|
try {
|
|
397
|
-
const
|
|
796
|
+
const projectId = isScoped ? req.params?.projectId : void 0;
|
|
797
|
+
const p = await this.resolveProtocol(projectId, req);
|
|
798
|
+
const types = await p.getMetaTypes();
|
|
398
799
|
res.json(types);
|
|
399
800
|
} catch (error) {
|
|
801
|
+
logError("[REST] Unhandled error:", error);
|
|
400
802
|
res.status(500).json({ error: error.message });
|
|
401
803
|
}
|
|
402
804
|
},
|
|
@@ -413,9 +815,18 @@ var RestServer = class {
|
|
|
413
815
|
handler: async (req, res) => {
|
|
414
816
|
try {
|
|
415
817
|
const packageId = req.query?.package || void 0;
|
|
416
|
-
const
|
|
417
|
-
|
|
818
|
+
const projectId = isScoped ? req.params?.projectId : void 0;
|
|
819
|
+
const p = await this.resolveProtocol(projectId, req);
|
|
820
|
+
const items = await p.getMetaItems({
|
|
821
|
+
type: req.params.type,
|
|
822
|
+
packageId,
|
|
823
|
+
...projectId ? { projectId } : {}
|
|
824
|
+
});
|
|
825
|
+
const translated = await this.translateMetaItems(req, req.params.type, projectId, items);
|
|
826
|
+
res.header("Vary", "Accept-Language");
|
|
827
|
+
res.json(translated);
|
|
418
828
|
} catch (error) {
|
|
829
|
+
logError("[REST] Unhandled error:", error);
|
|
419
830
|
res.status(404).json({ error: error.message });
|
|
420
831
|
}
|
|
421
832
|
},
|
|
@@ -431,15 +842,18 @@ var RestServer = class {
|
|
|
431
842
|
path: `${metaPath}/:type/:name`,
|
|
432
843
|
handler: async (req, res) => {
|
|
433
844
|
try {
|
|
434
|
-
|
|
845
|
+
const projectId = isScoped ? req.params?.projectId : void 0;
|
|
846
|
+
const p = await this.resolveProtocol(projectId, req);
|
|
847
|
+
if (metadata.enableCache && p.getMetaItemCached) {
|
|
435
848
|
const cacheRequest = {
|
|
436
849
|
ifNoneMatch: req.headers["if-none-match"],
|
|
437
850
|
ifModifiedSince: req.headers["if-modified-since"]
|
|
438
851
|
};
|
|
439
|
-
const result = await
|
|
852
|
+
const result = await p.getMetaItemCached({
|
|
440
853
|
type: req.params.type,
|
|
441
854
|
name: req.params.name,
|
|
442
|
-
cacheRequest
|
|
855
|
+
cacheRequest,
|
|
856
|
+
...projectId ? { projectId } : {}
|
|
443
857
|
});
|
|
444
858
|
if (result.notModified) {
|
|
445
859
|
res.status(304).send();
|
|
@@ -457,13 +871,20 @@ var RestServer = class {
|
|
|
457
871
|
const maxAge = result.cacheControl.maxAge ? `, max-age=${result.cacheControl.maxAge}` : "";
|
|
458
872
|
res.header("Cache-Control", directives + maxAge);
|
|
459
873
|
}
|
|
460
|
-
res.
|
|
874
|
+
res.header("Vary", "Accept-Language");
|
|
875
|
+
res.json(await this.translateMetaItem(req, req.params.type, projectId, result.data));
|
|
461
876
|
} else {
|
|
462
877
|
const packageId = req.query?.package || void 0;
|
|
463
|
-
const item = await
|
|
464
|
-
|
|
878
|
+
const item = await p.getMetaItem({
|
|
879
|
+
type: req.params.type,
|
|
880
|
+
name: req.params.name,
|
|
881
|
+
packageId
|
|
882
|
+
});
|
|
883
|
+
res.header("Vary", "Accept-Language");
|
|
884
|
+
res.json(await this.translateMetaItem(req, req.params.type, projectId, item));
|
|
465
885
|
}
|
|
466
886
|
} catch (error) {
|
|
887
|
+
logError("[REST] Unhandled error:", error);
|
|
467
888
|
res.status(404).json({ error: error.message });
|
|
468
889
|
}
|
|
469
890
|
},
|
|
@@ -478,17 +899,21 @@ var RestServer = class {
|
|
|
478
899
|
path: `${metaPath}/:type/:name`,
|
|
479
900
|
handler: async (req, res) => {
|
|
480
901
|
try {
|
|
481
|
-
|
|
902
|
+
const projectId = isScoped ? req.params?.projectId : void 0;
|
|
903
|
+
const p = await this.resolveProtocol(projectId, req);
|
|
904
|
+
if (!p.saveMetaItem) {
|
|
482
905
|
res.status(501).json({ error: "Save operation not supported by protocol implementation" });
|
|
483
906
|
return;
|
|
484
907
|
}
|
|
485
|
-
const result = await
|
|
908
|
+
const result = await p.saveMetaItem({
|
|
486
909
|
type: req.params.type,
|
|
487
910
|
name: req.params.name,
|
|
488
|
-
item: req.body
|
|
911
|
+
item: req.body,
|
|
912
|
+
...projectId ? { projectId } : {}
|
|
489
913
|
});
|
|
490
914
|
res.json(result);
|
|
491
915
|
} catch (error) {
|
|
916
|
+
logError("[REST] Unhandled error:", error);
|
|
492
917
|
res.status(400).json({ error: error.message });
|
|
493
918
|
}
|
|
494
919
|
},
|
|
@@ -503,21 +928,26 @@ var RestServer = class {
|
|
|
503
928
|
*/
|
|
504
929
|
registerUiEndpoints(basePath) {
|
|
505
930
|
const uiPath = `${basePath}/ui`;
|
|
931
|
+
const isScoped = basePath.includes("/projects/:projectId");
|
|
506
932
|
this.routeManager.register({
|
|
507
933
|
method: "GET",
|
|
508
934
|
path: `${uiPath}/view/:object/:type`,
|
|
509
935
|
handler: async (req, res) => {
|
|
510
936
|
try {
|
|
511
|
-
|
|
512
|
-
|
|
937
|
+
const projectId = isScoped ? req.params?.projectId : void 0;
|
|
938
|
+
const p = await this.resolveProtocol(projectId, req);
|
|
939
|
+
if (p.getUiView) {
|
|
940
|
+
const view = await p.getUiView({
|
|
513
941
|
object: req.params.object,
|
|
514
|
-
type: req.params.type
|
|
942
|
+
type: req.params.type,
|
|
943
|
+
...projectId ? { projectId } : {}
|
|
515
944
|
});
|
|
516
945
|
res.json(view);
|
|
517
946
|
} else {
|
|
518
947
|
res.status(501).json({ error: "UI View resolution not supported by protocol implementation" });
|
|
519
948
|
}
|
|
520
949
|
} catch (error) {
|
|
950
|
+
logError("[REST] Unhandled error:", error);
|
|
521
951
|
res.status(404).json({ error: error.message });
|
|
522
952
|
}
|
|
523
953
|
},
|
|
@@ -533,6 +963,7 @@ var RestServer = class {
|
|
|
533
963
|
registerCrudEndpoints(basePath) {
|
|
534
964
|
const { crud } = this.config;
|
|
535
965
|
const dataPath = `${basePath}${crud.dataPrefix}`;
|
|
966
|
+
const isScoped = basePath.includes("/projects/:projectId");
|
|
536
967
|
const operations = crud.operations;
|
|
537
968
|
if (operations.list) {
|
|
538
969
|
this.routeManager.register({
|
|
@@ -540,13 +971,24 @@ var RestServer = class {
|
|
|
540
971
|
path: `${dataPath}/:object`,
|
|
541
972
|
handler: async (req, res) => {
|
|
542
973
|
try {
|
|
543
|
-
const
|
|
974
|
+
const projectId = isScoped ? req.params?.projectId : void 0;
|
|
975
|
+
const p = await this.resolveProtocol(projectId, req);
|
|
976
|
+
const context = await this.resolveExecCtx(projectId, req);
|
|
977
|
+
const result = await p.findData({
|
|
544
978
|
object: req.params.object,
|
|
545
|
-
query: req.query
|
|
979
|
+
query: req.query,
|
|
980
|
+
...projectId ? { projectId } : {},
|
|
981
|
+
...context ? { context } : {}
|
|
546
982
|
});
|
|
547
983
|
res.json(result);
|
|
548
984
|
} catch (error) {
|
|
549
|
-
|
|
985
|
+
const mapped = mapDataError(error, req.params?.object);
|
|
986
|
+
if (mapped.status === 404 || mapped.status === 503 || mapped.status === 502) {
|
|
987
|
+
res.status(mapped.status).json(mapped.body);
|
|
988
|
+
} else {
|
|
989
|
+
logError("[REST] Unhandled error:", error);
|
|
990
|
+
res.status(mapped.status).json(mapped.body);
|
|
991
|
+
}
|
|
550
992
|
}
|
|
551
993
|
},
|
|
552
994
|
metadata: {
|
|
@@ -561,16 +1003,23 @@ var RestServer = class {
|
|
|
561
1003
|
path: `${dataPath}/:object/:id`,
|
|
562
1004
|
handler: async (req, res) => {
|
|
563
1005
|
try {
|
|
1006
|
+
const projectId = isScoped ? req.params?.projectId : void 0;
|
|
1007
|
+
const p = await this.resolveProtocol(projectId, req);
|
|
564
1008
|
const { select, expand } = req.query || {};
|
|
565
|
-
const
|
|
1009
|
+
const context = await this.resolveExecCtx(projectId, req);
|
|
1010
|
+
const result = await p.getData({
|
|
566
1011
|
object: req.params.object,
|
|
567
1012
|
id: req.params.id,
|
|
568
1013
|
...select != null ? { select } : {},
|
|
569
|
-
...expand != null ? { expand } : {}
|
|
1014
|
+
...expand != null ? { expand } : {},
|
|
1015
|
+
...projectId ? { projectId } : {},
|
|
1016
|
+
...context ? { context } : {}
|
|
570
1017
|
});
|
|
571
1018
|
res.json(result);
|
|
572
1019
|
} catch (error) {
|
|
573
|
-
|
|
1020
|
+
const mapped = mapDataError(error, req.params?.object);
|
|
1021
|
+
if (!isExpectedDataStatus(mapped.status)) logError("[REST] Unhandled error:", error);
|
|
1022
|
+
res.status(mapped.status === 400 ? 404 : mapped.status).json(mapped.body);
|
|
574
1023
|
}
|
|
575
1024
|
},
|
|
576
1025
|
metadata: {
|
|
@@ -585,13 +1034,20 @@ var RestServer = class {
|
|
|
585
1034
|
path: `${dataPath}/:object`,
|
|
586
1035
|
handler: async (req, res) => {
|
|
587
1036
|
try {
|
|
588
|
-
const
|
|
1037
|
+
const projectId = isScoped ? req.params?.projectId : void 0;
|
|
1038
|
+
const p = await this.resolveProtocol(projectId, req);
|
|
1039
|
+
const context = await this.resolveExecCtx(projectId, req);
|
|
1040
|
+
const result = await p.createData({
|
|
589
1041
|
object: req.params.object,
|
|
590
|
-
data: req.body
|
|
1042
|
+
data: req.body,
|
|
1043
|
+
...projectId ? { projectId } : {},
|
|
1044
|
+
...context ? { context } : {}
|
|
591
1045
|
});
|
|
592
1046
|
res.status(201).json(result);
|
|
593
1047
|
} catch (error) {
|
|
594
|
-
|
|
1048
|
+
const mapped = mapDataError(error, req.params?.object);
|
|
1049
|
+
if (!isExpectedDataStatus(mapped.status)) logError("[REST] Unhandled error:", error);
|
|
1050
|
+
res.status(mapped.status).json(mapped.body);
|
|
595
1051
|
}
|
|
596
1052
|
},
|
|
597
1053
|
metadata: {
|
|
@@ -606,14 +1062,21 @@ var RestServer = class {
|
|
|
606
1062
|
path: `${dataPath}/:object/:id`,
|
|
607
1063
|
handler: async (req, res) => {
|
|
608
1064
|
try {
|
|
609
|
-
const
|
|
1065
|
+
const projectId = isScoped ? req.params?.projectId : void 0;
|
|
1066
|
+
const p = await this.resolveProtocol(projectId, req);
|
|
1067
|
+
const context = await this.resolveExecCtx(projectId, req);
|
|
1068
|
+
const result = await p.updateData({
|
|
610
1069
|
object: req.params.object,
|
|
611
1070
|
id: req.params.id,
|
|
612
|
-
data: req.body
|
|
1071
|
+
data: req.body,
|
|
1072
|
+
...projectId ? { projectId } : {},
|
|
1073
|
+
...context ? { context } : {}
|
|
613
1074
|
});
|
|
614
1075
|
res.json(result);
|
|
615
1076
|
} catch (error) {
|
|
616
|
-
|
|
1077
|
+
const mapped = mapDataError(error, req.params?.object);
|
|
1078
|
+
if (!isExpectedDataStatus(mapped.status)) logError("[REST] Unhandled error:", error);
|
|
1079
|
+
res.status(mapped.status).json(mapped.body);
|
|
617
1080
|
}
|
|
618
1081
|
},
|
|
619
1082
|
metadata: {
|
|
@@ -628,13 +1091,20 @@ var RestServer = class {
|
|
|
628
1091
|
path: `${dataPath}/:object/:id`,
|
|
629
1092
|
handler: async (req, res) => {
|
|
630
1093
|
try {
|
|
631
|
-
const
|
|
1094
|
+
const projectId = isScoped ? req.params?.projectId : void 0;
|
|
1095
|
+
const p = await this.resolveProtocol(projectId, req);
|
|
1096
|
+
const context = await this.resolveExecCtx(projectId, req);
|
|
1097
|
+
const result = await p.deleteData({
|
|
632
1098
|
object: req.params.object,
|
|
633
|
-
id: req.params.id
|
|
1099
|
+
id: req.params.id,
|
|
1100
|
+
...projectId ? { projectId } : {},
|
|
1101
|
+
...context ? { context } : {}
|
|
634
1102
|
});
|
|
635
1103
|
res.json(result);
|
|
636
1104
|
} catch (error) {
|
|
637
|
-
|
|
1105
|
+
const mapped = mapDataError(error, req.params?.object);
|
|
1106
|
+
if (!isExpectedDataStatus(mapped.status)) logError("[REST] Unhandled error:", error);
|
|
1107
|
+
res.status(mapped.status).json(mapped.body);
|
|
638
1108
|
}
|
|
639
1109
|
},
|
|
640
1110
|
metadata: {
|
|
@@ -650,6 +1120,7 @@ var RestServer = class {
|
|
|
650
1120
|
registerBatchEndpoints(basePath) {
|
|
651
1121
|
const { crud, batch } = this.config;
|
|
652
1122
|
const dataPath = `${basePath}${crud.dataPrefix}`;
|
|
1123
|
+
const isScoped = basePath.includes("/projects/:projectId");
|
|
653
1124
|
const operations = batch.operations;
|
|
654
1125
|
if (batch.enableBatchEndpoint && this.protocol.batchData) {
|
|
655
1126
|
this.routeManager.register({
|
|
@@ -657,12 +1128,16 @@ var RestServer = class {
|
|
|
657
1128
|
path: `${dataPath}/:object/batch`,
|
|
658
1129
|
handler: async (req, res) => {
|
|
659
1130
|
try {
|
|
660
|
-
const
|
|
1131
|
+
const projectId = isScoped ? req.params?.projectId : void 0;
|
|
1132
|
+
const p = await this.resolveProtocol(projectId, req);
|
|
1133
|
+
const result = await p.batchData({
|
|
661
1134
|
object: req.params.object,
|
|
662
|
-
request: req.body
|
|
1135
|
+
request: req.body,
|
|
1136
|
+
...projectId ? { projectId } : {}
|
|
663
1137
|
});
|
|
664
1138
|
res.json(result);
|
|
665
1139
|
} catch (error) {
|
|
1140
|
+
logError("[REST] Unhandled error:", error);
|
|
666
1141
|
res.status(400).json({ error: error.message });
|
|
667
1142
|
}
|
|
668
1143
|
},
|
|
@@ -678,12 +1153,16 @@ var RestServer = class {
|
|
|
678
1153
|
path: `${dataPath}/:object/createMany`,
|
|
679
1154
|
handler: async (req, res) => {
|
|
680
1155
|
try {
|
|
681
|
-
const
|
|
1156
|
+
const projectId = isScoped ? req.params?.projectId : void 0;
|
|
1157
|
+
const p = await this.resolveProtocol(projectId, req);
|
|
1158
|
+
const result = await p.createManyData({
|
|
682
1159
|
object: req.params.object,
|
|
683
|
-
records: req.body || []
|
|
1160
|
+
records: req.body || [],
|
|
1161
|
+
...projectId ? { projectId } : {}
|
|
684
1162
|
});
|
|
685
1163
|
res.status(201).json(result);
|
|
686
1164
|
} catch (error) {
|
|
1165
|
+
logError("[REST] Unhandled error:", error);
|
|
687
1166
|
res.status(400).json({ error: error.message });
|
|
688
1167
|
}
|
|
689
1168
|
},
|
|
@@ -699,12 +1178,16 @@ var RestServer = class {
|
|
|
699
1178
|
path: `${dataPath}/:object/updateMany`,
|
|
700
1179
|
handler: async (req, res) => {
|
|
701
1180
|
try {
|
|
702
|
-
const
|
|
1181
|
+
const projectId = isScoped ? req.params?.projectId : void 0;
|
|
1182
|
+
const p = await this.resolveProtocol(projectId, req);
|
|
1183
|
+
const result = await p.updateManyData({
|
|
703
1184
|
object: req.params.object,
|
|
704
|
-
...req.body
|
|
1185
|
+
...req.body,
|
|
1186
|
+
...projectId ? { projectId } : {}
|
|
705
1187
|
});
|
|
706
1188
|
res.json(result);
|
|
707
1189
|
} catch (error) {
|
|
1190
|
+
logError("[REST] Unhandled error:", error);
|
|
708
1191
|
res.status(400).json({ error: error.message });
|
|
709
1192
|
}
|
|
710
1193
|
},
|
|
@@ -720,12 +1203,16 @@ var RestServer = class {
|
|
|
720
1203
|
path: `${dataPath}/:object/deleteMany`,
|
|
721
1204
|
handler: async (req, res) => {
|
|
722
1205
|
try {
|
|
723
|
-
const
|
|
1206
|
+
const projectId = isScoped ? req.params?.projectId : void 0;
|
|
1207
|
+
const p = await this.resolveProtocol(projectId, req);
|
|
1208
|
+
const result = await p.deleteManyData({
|
|
724
1209
|
object: req.params.object,
|
|
725
|
-
...req.body
|
|
1210
|
+
...req.body,
|
|
1211
|
+
...projectId ? { projectId } : {}
|
|
726
1212
|
});
|
|
727
1213
|
res.json(result);
|
|
728
1214
|
} catch (error) {
|
|
1215
|
+
logError("[REST] Unhandled error:", error);
|
|
729
1216
|
res.status(400).json({ error: error.message });
|
|
730
1217
|
}
|
|
731
1218
|
},
|
|
@@ -750,6 +1237,123 @@ var RestServer = class {
|
|
|
750
1237
|
}
|
|
751
1238
|
};
|
|
752
1239
|
|
|
1240
|
+
// src/package-routes.ts
|
|
1241
|
+
function registerPackageRoutes(server, packageService, basePath = "/api/v1", options = {}) {
|
|
1242
|
+
const packagesPath = `${basePath}/packages`;
|
|
1243
|
+
server.post(packagesPath, async (req, res) => {
|
|
1244
|
+
try {
|
|
1245
|
+
const { manifest, metadata } = req.body || {};
|
|
1246
|
+
if (!manifest || !metadata) {
|
|
1247
|
+
res.status(400).json({ error: "Missing required fields: manifest, metadata" });
|
|
1248
|
+
return;
|
|
1249
|
+
}
|
|
1250
|
+
if (!manifest.id || !manifest.version) {
|
|
1251
|
+
res.status(400).json({ error: "Invalid manifest: id and version are required" });
|
|
1252
|
+
return;
|
|
1253
|
+
}
|
|
1254
|
+
const result = await packageService.publish({ manifest, metadata });
|
|
1255
|
+
if (result.success) {
|
|
1256
|
+
res.json({
|
|
1257
|
+
success: true,
|
|
1258
|
+
message: `Published ${manifest.id}@${manifest.version}`,
|
|
1259
|
+
package: {
|
|
1260
|
+
id: manifest.id,
|
|
1261
|
+
version: manifest.version
|
|
1262
|
+
}
|
|
1263
|
+
});
|
|
1264
|
+
return;
|
|
1265
|
+
}
|
|
1266
|
+
res.status(400).json({ success: false, error: result.error });
|
|
1267
|
+
} catch (error) {
|
|
1268
|
+
res.status(500).json({ error: error.message });
|
|
1269
|
+
}
|
|
1270
|
+
});
|
|
1271
|
+
server.get(packagesPath, async (_req, res) => {
|
|
1272
|
+
try {
|
|
1273
|
+
const packagesMap = /* @__PURE__ */ new Map();
|
|
1274
|
+
if (options.protocol && typeof options.protocol.getMetaItems === "function") {
|
|
1275
|
+
try {
|
|
1276
|
+
const result = await options.protocol.getMetaItems({ type: "package" });
|
|
1277
|
+
if (result?.items) {
|
|
1278
|
+
for (const item of result.items) {
|
|
1279
|
+
const id = item.manifest?.id || item.id;
|
|
1280
|
+
if (id) {
|
|
1281
|
+
packagesMap.set(id, {
|
|
1282
|
+
...item,
|
|
1283
|
+
source: "registry"
|
|
1284
|
+
});
|
|
1285
|
+
}
|
|
1286
|
+
}
|
|
1287
|
+
}
|
|
1288
|
+
} catch {
|
|
1289
|
+
}
|
|
1290
|
+
}
|
|
1291
|
+
try {
|
|
1292
|
+
const dbPackages = await packageService.list();
|
|
1293
|
+
for (const pkg of dbPackages) {
|
|
1294
|
+
const id = pkg.manifest?.id || pkg.id;
|
|
1295
|
+
if (id) {
|
|
1296
|
+
packagesMap.set(id, {
|
|
1297
|
+
...packagesMap.get(id),
|
|
1298
|
+
...pkg,
|
|
1299
|
+
source: packagesMap.has(id) ? "both" : "database"
|
|
1300
|
+
});
|
|
1301
|
+
}
|
|
1302
|
+
}
|
|
1303
|
+
} catch {
|
|
1304
|
+
}
|
|
1305
|
+
const packages = Array.from(packagesMap.values());
|
|
1306
|
+
res.json({ packages, total: packages.length });
|
|
1307
|
+
} catch (error) {
|
|
1308
|
+
res.status(500).json({ error: error.message });
|
|
1309
|
+
}
|
|
1310
|
+
});
|
|
1311
|
+
server.get(`${packagesPath}/:id`, async (req, res) => {
|
|
1312
|
+
try {
|
|
1313
|
+
const packageId = req.params.id;
|
|
1314
|
+
const version = req.query?.version || "latest";
|
|
1315
|
+
const pkg = await packageService.get(packageId, version);
|
|
1316
|
+
if (pkg) {
|
|
1317
|
+
res.json({ package: { ...pkg, source: "database" } });
|
|
1318
|
+
return;
|
|
1319
|
+
}
|
|
1320
|
+
if (options.protocol && typeof options.protocol.getMetaItems === "function") {
|
|
1321
|
+
try {
|
|
1322
|
+
const result = await options.protocol.getMetaItems({ type: "package" });
|
|
1323
|
+
const match = result?.items?.find(
|
|
1324
|
+
(item) => (item.manifest?.id || item.id) === packageId
|
|
1325
|
+
);
|
|
1326
|
+
if (match) {
|
|
1327
|
+
res.json({ package: { ...match, source: "registry" } });
|
|
1328
|
+
return;
|
|
1329
|
+
}
|
|
1330
|
+
} catch {
|
|
1331
|
+
}
|
|
1332
|
+
}
|
|
1333
|
+
res.status(404).json({ error: "Package not found" });
|
|
1334
|
+
} catch (error) {
|
|
1335
|
+
res.status(500).json({ error: error.message });
|
|
1336
|
+
}
|
|
1337
|
+
});
|
|
1338
|
+
server.delete(`${packagesPath}/:id`, async (req, res) => {
|
|
1339
|
+
try {
|
|
1340
|
+
const packageId = req.params.id;
|
|
1341
|
+
const version = req.query?.version;
|
|
1342
|
+
const result = await packageService.delete(packageId, version);
|
|
1343
|
+
if (result.success) {
|
|
1344
|
+
res.json({
|
|
1345
|
+
success: true,
|
|
1346
|
+
message: `Deleted ${packageId}${version ? `@${version}` : ""}`
|
|
1347
|
+
});
|
|
1348
|
+
return;
|
|
1349
|
+
}
|
|
1350
|
+
res.status(400).json({ success: false });
|
|
1351
|
+
} catch (error) {
|
|
1352
|
+
res.status(500).json({ error: error.message });
|
|
1353
|
+
}
|
|
1354
|
+
});
|
|
1355
|
+
}
|
|
1356
|
+
|
|
753
1357
|
// src/rest-api-plugin.ts
|
|
754
1358
|
function createRestApiPlugin(config = {}) {
|
|
755
1359
|
return {
|
|
@@ -770,6 +1374,39 @@ function createRestApiPlugin(config = {}) {
|
|
|
770
1374
|
protocol = ctx.getService(protocolService);
|
|
771
1375
|
} catch (e) {
|
|
772
1376
|
}
|
|
1377
|
+
let kernelManager;
|
|
1378
|
+
const kernelManagerService = config.kernelManagerServiceName || "kernel-manager";
|
|
1379
|
+
try {
|
|
1380
|
+
kernelManager = ctx.getService(kernelManagerService);
|
|
1381
|
+
} catch (e) {
|
|
1382
|
+
}
|
|
1383
|
+
let envRegistry;
|
|
1384
|
+
try {
|
|
1385
|
+
envRegistry = ctx.getService("env-registry");
|
|
1386
|
+
} catch (e) {
|
|
1387
|
+
}
|
|
1388
|
+
const defaultProjectIdProvider = () => {
|
|
1389
|
+
try {
|
|
1390
|
+
const dp = ctx.getService("default-project");
|
|
1391
|
+
return dp?.projectId;
|
|
1392
|
+
} catch {
|
|
1393
|
+
return void 0;
|
|
1394
|
+
}
|
|
1395
|
+
};
|
|
1396
|
+
const authServiceProvider = async (_projectId) => {
|
|
1397
|
+
try {
|
|
1398
|
+
return ctx.getService("auth");
|
|
1399
|
+
} catch {
|
|
1400
|
+
return void 0;
|
|
1401
|
+
}
|
|
1402
|
+
};
|
|
1403
|
+
const objectQLProvider = async (_projectId) => {
|
|
1404
|
+
try {
|
|
1405
|
+
return ctx.getService("objectql");
|
|
1406
|
+
} catch {
|
|
1407
|
+
return void 0;
|
|
1408
|
+
}
|
|
1409
|
+
};
|
|
773
1410
|
if (!server) {
|
|
774
1411
|
ctx.logger.warn(`RestApiPlugin: HTTP Server service '${serverService}' not found. REST routes skipped.`);
|
|
775
1412
|
return;
|
|
@@ -780,13 +1417,38 @@ function createRestApiPlugin(config = {}) {
|
|
|
780
1417
|
}
|
|
781
1418
|
ctx.logger.info("Hydrating REST API from Protocol...");
|
|
782
1419
|
try {
|
|
783
|
-
const restServer = new RestServer(server, protocol, config.api);
|
|
1420
|
+
const restServer = new RestServer(server, protocol, config.api, kernelManager, envRegistry, defaultProjectIdProvider, authServiceProvider, objectQLProvider);
|
|
784
1421
|
restServer.registerRoutes();
|
|
785
1422
|
ctx.logger.info("REST API successfully registered");
|
|
786
1423
|
} catch (err) {
|
|
787
1424
|
ctx.logger.error("Failed to register REST API routes", { error: err.message });
|
|
788
1425
|
throw err;
|
|
789
1426
|
}
|
|
1427
|
+
try {
|
|
1428
|
+
const packageService = ctx.getService("package");
|
|
1429
|
+
if (packageService) {
|
|
1430
|
+
const basePath = config.api?.api?.basePath || "/api";
|
|
1431
|
+
const version = config.api?.api?.version || "v1";
|
|
1432
|
+
const versionedBase = `${basePath}/${version}`;
|
|
1433
|
+
const enableProjectScoping = config.api?.api?.enableProjectScoping ?? false;
|
|
1434
|
+
const projectResolution = config.api?.api?.projectResolution ?? "auto";
|
|
1435
|
+
if (enableProjectScoping && projectResolution === "required") {
|
|
1436
|
+
registerPackageRoutes(server, packageService, `${versionedBase}/projects/:projectId`, {
|
|
1437
|
+
protocol
|
|
1438
|
+
});
|
|
1439
|
+
} else {
|
|
1440
|
+
registerPackageRoutes(server, packageService, versionedBase, { protocol });
|
|
1441
|
+
if (enableProjectScoping) {
|
|
1442
|
+
registerPackageRoutes(server, packageService, `${versionedBase}/projects/:projectId`, {
|
|
1443
|
+
protocol
|
|
1444
|
+
});
|
|
1445
|
+
}
|
|
1446
|
+
}
|
|
1447
|
+
ctx.logger.info("Package management routes registered");
|
|
1448
|
+
}
|
|
1449
|
+
} catch (e) {
|
|
1450
|
+
ctx.logger.debug("Package service not available, package routes skipped");
|
|
1451
|
+
}
|
|
790
1452
|
}
|
|
791
1453
|
};
|
|
792
1454
|
}
|