@objectstack/rest 4.0.4 → 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 -481
- 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.js
CHANGED
|
@@ -209,11 +209,360 @@ var RouteGroupBuilder = class {
|
|
|
209
209
|
};
|
|
210
210
|
|
|
211
211
|
// src/rest-server.ts
|
|
212
|
+
var logError = (...args) => globalThis.console?.error(...args);
|
|
213
|
+
function mapDataError(error, object) {
|
|
214
|
+
if (error?.code === "PERMISSION_DENIED" || error?.name === "PermissionDeniedError" || typeof error?.message === "string" && error.message.startsWith("[Security] Access denied")) {
|
|
215
|
+
return {
|
|
216
|
+
status: 403,
|
|
217
|
+
body: {
|
|
218
|
+
error: error?.message ?? "Permission denied",
|
|
219
|
+
code: "PERMISSION_DENIED",
|
|
220
|
+
...object ? { object } : {}
|
|
221
|
+
}
|
|
222
|
+
};
|
|
223
|
+
}
|
|
224
|
+
const raw = String(error?.message ?? error ?? "");
|
|
225
|
+
const lower = raw.toLowerCase();
|
|
226
|
+
if (raw.includes("[ProjectKernelFactory]") && (lower.includes("missing database_url") || lower.includes("not found"))) {
|
|
227
|
+
const isProvisioning = lower.includes("status='provisioning'") || lower.includes("status='pending'");
|
|
228
|
+
const isFailed = lower.includes("status='failed'");
|
|
229
|
+
return {
|
|
230
|
+
status: isProvisioning ? 503 : isFailed ? 502 : 404,
|
|
231
|
+
body: {
|
|
232
|
+
error: raw,
|
|
233
|
+
code: isProvisioning ? "PROJECT_PROVISIONING" : isFailed ? "PROJECT_PROVISIONING_FAILED" : "PROJECT_NOT_FOUND"
|
|
234
|
+
}
|
|
235
|
+
};
|
|
236
|
+
}
|
|
237
|
+
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");
|
|
238
|
+
if (looksLikeUnknownObject) {
|
|
239
|
+
return {
|
|
240
|
+
status: 404,
|
|
241
|
+
body: {
|
|
242
|
+
error: object ? `Object '${object}' is not registered` : "Object not found",
|
|
243
|
+
code: "object_not_found",
|
|
244
|
+
object
|
|
245
|
+
}
|
|
246
|
+
};
|
|
247
|
+
}
|
|
248
|
+
return { status: 400, body: { error: raw || "Bad request" } };
|
|
249
|
+
}
|
|
250
|
+
function isExpectedDataStatus(status) {
|
|
251
|
+
return status === 403 || status === 404 || status === 502 || status === 503;
|
|
252
|
+
}
|
|
212
253
|
var RestServer = class {
|
|
213
|
-
constructor(server, protocol, config = {}) {
|
|
254
|
+
constructor(server, protocol, config = {}, kernelManager, envRegistry, defaultProjectIdProvider, authServiceProvider, objectQLProvider) {
|
|
214
255
|
this.protocol = protocol;
|
|
215
256
|
this.config = this.normalizeConfig(config);
|
|
216
257
|
this.routeManager = new RouteManager(server);
|
|
258
|
+
this.kernelManager = kernelManager;
|
|
259
|
+
this.envRegistry = envRegistry;
|
|
260
|
+
this.defaultProjectIdProvider = defaultProjectIdProvider;
|
|
261
|
+
this.authServiceProvider = authServiceProvider;
|
|
262
|
+
this.objectQLProvider = objectQLProvider;
|
|
263
|
+
}
|
|
264
|
+
/**
|
|
265
|
+
* Resolve the protocol for a given request. When `projectId` is present
|
|
266
|
+
* and a KernelManager is wired, fetch the per-project kernel's
|
|
267
|
+
* `protocol` service so metadata / data / UI reads hit the project's
|
|
268
|
+
* own registry and datastore.
|
|
269
|
+
*
|
|
270
|
+
* When `projectId` is absent on an unscoped route and an `envRegistry`
|
|
271
|
+
* is wired (runtime mode), the resolution chain is:
|
|
272
|
+
* 1. Hostname → projectId (`envRegistry.resolveByHostname`)
|
|
273
|
+
* 2. `X-Project-Id` header → projectId (`envRegistry.resolveById`)
|
|
274
|
+
* 3. Default-project fallback (`defaultProjectIdProvider`, set by
|
|
275
|
+
* `createSingleProjectPlugin`)
|
|
276
|
+
* 4. Control-plane protocol captured at boot.
|
|
277
|
+
*
|
|
278
|
+
* Special case: `projectId === 'platform'` is a reserved virtual id used
|
|
279
|
+
* by Studio to address the control plane through the regular project
|
|
280
|
+
* URL shape (`/projects/platform/...`). It is NOT a row in the projects
|
|
281
|
+
* table, so we must never call `KernelManager.getOrCreate('platform')`.
|
|
282
|
+
* Instead, return the control-plane protocol directly. This lets Studio
|
|
283
|
+
* (and any other client) speak a single, uniform URL family without
|
|
284
|
+
* duplicating route logic for the platform surface.
|
|
285
|
+
*/
|
|
286
|
+
async resolveProtocol(projectId, req) {
|
|
287
|
+
if (projectId === "platform") return this.protocol;
|
|
288
|
+
if (!projectId && req && this.envRegistry && this.kernelManager) {
|
|
289
|
+
const host = this.extractHostname(req);
|
|
290
|
+
if (host) {
|
|
291
|
+
try {
|
|
292
|
+
const result = await this.envRegistry.resolveByHostname(host);
|
|
293
|
+
if (result?.projectId) projectId = result.projectId;
|
|
294
|
+
} catch {
|
|
295
|
+
}
|
|
296
|
+
}
|
|
297
|
+
if (!projectId && typeof this.envRegistry.resolveById === "function") {
|
|
298
|
+
const headerVal = this.extractProjectIdHeader(req);
|
|
299
|
+
if (headerVal) {
|
|
300
|
+
try {
|
|
301
|
+
const driver = await this.envRegistry.resolveById(headerVal);
|
|
302
|
+
if (driver) projectId = headerVal;
|
|
303
|
+
} catch {
|
|
304
|
+
}
|
|
305
|
+
}
|
|
306
|
+
}
|
|
307
|
+
}
|
|
308
|
+
if (!projectId && this.defaultProjectIdProvider) {
|
|
309
|
+
try {
|
|
310
|
+
const def = this.defaultProjectIdProvider();
|
|
311
|
+
if (def) projectId = def;
|
|
312
|
+
} catch {
|
|
313
|
+
}
|
|
314
|
+
}
|
|
315
|
+
if (!projectId || !this.kernelManager) return this.protocol;
|
|
316
|
+
const kernel = await this.kernelManager.getOrCreate(projectId);
|
|
317
|
+
return kernel.getServiceAsync("protocol");
|
|
318
|
+
}
|
|
319
|
+
/**
|
|
320
|
+
* Resolve the i18n service for the request's project (or control plane
|
|
321
|
+
* when no project id is in scope). Returns `undefined` when no service is
|
|
322
|
+
* registered, so callers can short-circuit and skip translation rather
|
|
323
|
+
* than failing.
|
|
324
|
+
*
|
|
325
|
+
* Mirrors `resolveProtocol`'s lookup chain: explicit `projectId` from the
|
|
326
|
+
* route → kernel-managed `i18n` service. Control-plane / unscoped
|
|
327
|
+
* requests intentionally return `undefined` because the platform kernel
|
|
328
|
+
* does not own per-app translation bundles.
|
|
329
|
+
*/
|
|
330
|
+
async resolveI18nService(projectId) {
|
|
331
|
+
if (!projectId || projectId === "platform" || !this.kernelManager) return void 0;
|
|
332
|
+
try {
|
|
333
|
+
const kernel = await this.kernelManager.getOrCreate(projectId);
|
|
334
|
+
return await kernel.getServiceAsync("i18n");
|
|
335
|
+
} catch {
|
|
336
|
+
return void 0;
|
|
337
|
+
}
|
|
338
|
+
}
|
|
339
|
+
/**
|
|
340
|
+
* Resolve the request's execution context (RBAC/RLS/FLS) by looking up
|
|
341
|
+
* the better-auth session via the project's `auth` service. Returns
|
|
342
|
+
* `undefined` for anonymous requests so callers can pass `context` as-is
|
|
343
|
+
* to the protocol layer (the SecurityPlugin treats undefined as anon).
|
|
344
|
+
*/
|
|
345
|
+
async resolveExecCtx(projectId, req) {
|
|
346
|
+
try {
|
|
347
|
+
let authService;
|
|
348
|
+
let kernel;
|
|
349
|
+
if (projectId && projectId !== "platform" && this.kernelManager) {
|
|
350
|
+
kernel = await this.kernelManager.getOrCreate(projectId);
|
|
351
|
+
authService = await kernel.getServiceAsync("auth").catch(() => void 0);
|
|
352
|
+
}
|
|
353
|
+
if (!authService && this.defaultProjectIdProvider && this.kernelManager) {
|
|
354
|
+
try {
|
|
355
|
+
const def = this.defaultProjectIdProvider();
|
|
356
|
+
if (def) {
|
|
357
|
+
kernel = await this.kernelManager.getOrCreate(def);
|
|
358
|
+
authService = await kernel.getServiceAsync("auth").catch(() => void 0);
|
|
359
|
+
}
|
|
360
|
+
} catch {
|
|
361
|
+
}
|
|
362
|
+
}
|
|
363
|
+
if (!authService && this.authServiceProvider) {
|
|
364
|
+
authService = await this.authServiceProvider(projectId).catch(() => void 0);
|
|
365
|
+
}
|
|
366
|
+
if (!authService) return void 0;
|
|
367
|
+
let api = authService.api;
|
|
368
|
+
if (!api && typeof authService.getApi === "function") {
|
|
369
|
+
api = await authService.getApi();
|
|
370
|
+
}
|
|
371
|
+
if (!api?.getSession) return void 0;
|
|
372
|
+
const rawHeaders = req?.headers;
|
|
373
|
+
let headers;
|
|
374
|
+
if (rawHeaders && typeof rawHeaders.get === "function") {
|
|
375
|
+
headers = rawHeaders;
|
|
376
|
+
} else if (rawHeaders && typeof rawHeaders === "object") {
|
|
377
|
+
headers = new globalThis.Headers();
|
|
378
|
+
for (const [k, v] of Object.entries(rawHeaders)) {
|
|
379
|
+
if (Array.isArray(v)) v.forEach((x) => headers.append(k, String(x)));
|
|
380
|
+
else if (v != null) headers.set(k, String(v));
|
|
381
|
+
}
|
|
382
|
+
} else {
|
|
383
|
+
return void 0;
|
|
384
|
+
}
|
|
385
|
+
const session = await api.getSession({ headers });
|
|
386
|
+
if (!session?.user?.id) return void 0;
|
|
387
|
+
const userId = session.user.id;
|
|
388
|
+
const tenantId = session.session?.activeOrganizationId ?? void 0;
|
|
389
|
+
const permissions = [];
|
|
390
|
+
const roles = [];
|
|
391
|
+
try {
|
|
392
|
+
let ql;
|
|
393
|
+
if (kernel) {
|
|
394
|
+
ql = await kernel.getServiceAsync("objectql").catch(() => void 0);
|
|
395
|
+
}
|
|
396
|
+
if (!ql && this.objectQLProvider) {
|
|
397
|
+
ql = await this.objectQLProvider(projectId).catch(() => void 0);
|
|
398
|
+
}
|
|
399
|
+
if (ql && typeof ql.find === "function") {
|
|
400
|
+
const sysOpts = { context: { isSystem: true } };
|
|
401
|
+
const memberRows = await ql.find("sys_member", {
|
|
402
|
+
where: tenantId ? { user_id: userId, organization_id: tenantId } : { user_id: userId },
|
|
403
|
+
limit: 50,
|
|
404
|
+
...sysOpts
|
|
405
|
+
}).catch(() => []);
|
|
406
|
+
for (const m of memberRows ?? []) {
|
|
407
|
+
if (typeof m.role === "string") {
|
|
408
|
+
for (const r of m.role.split(",").map((s) => s.trim()).filter(Boolean)) {
|
|
409
|
+
if (!roles.includes(r)) roles.push(r);
|
|
410
|
+
}
|
|
411
|
+
}
|
|
412
|
+
}
|
|
413
|
+
const upsRows = await ql.find("sys_user_permission_set", {
|
|
414
|
+
where: { user_id: userId },
|
|
415
|
+
limit: 100,
|
|
416
|
+
...sysOpts
|
|
417
|
+
}).catch(() => []);
|
|
418
|
+
const psIds = /* @__PURE__ */ new Set();
|
|
419
|
+
for (const r of upsRows ?? []) {
|
|
420
|
+
const orgScope = r.organization_id ?? null;
|
|
421
|
+
if (!orgScope || tenantId && orgScope === tenantId) {
|
|
422
|
+
const pid = r.permission_set_id ?? r.permissionSetId;
|
|
423
|
+
if (pid) psIds.add(pid);
|
|
424
|
+
}
|
|
425
|
+
}
|
|
426
|
+
if (psIds.size > 0) {
|
|
427
|
+
const psRows = await ql.find("sys_permission_set", {
|
|
428
|
+
where: { id: { $in: Array.from(psIds) } },
|
|
429
|
+
limit: 500,
|
|
430
|
+
...sysOpts
|
|
431
|
+
}).catch(() => []);
|
|
432
|
+
for (const ps of psRows ?? []) {
|
|
433
|
+
if (ps.name && !permissions.includes(ps.name)) permissions.push(ps.name);
|
|
434
|
+
}
|
|
435
|
+
}
|
|
436
|
+
}
|
|
437
|
+
} catch {
|
|
438
|
+
}
|
|
439
|
+
return {
|
|
440
|
+
userId,
|
|
441
|
+
tenantId,
|
|
442
|
+
roles,
|
|
443
|
+
permissions,
|
|
444
|
+
isSystem: false
|
|
445
|
+
};
|
|
446
|
+
} catch {
|
|
447
|
+
return void 0;
|
|
448
|
+
}
|
|
449
|
+
}
|
|
450
|
+
/**
|
|
451
|
+
* Build a `TranslationBundle` (`Record<locale, TranslationData>`) from an
|
|
452
|
+
* `II18nService` instance. Returns `undefined` when no locales are
|
|
453
|
+
* registered so callers can avoid translation work.
|
|
454
|
+
*/
|
|
455
|
+
buildTranslationBundle(i18n) {
|
|
456
|
+
if (!i18n || typeof i18n.getLocales !== "function" || typeof i18n.getTranslations !== "function") {
|
|
457
|
+
return void 0;
|
|
458
|
+
}
|
|
459
|
+
const locales = i18n.getLocales();
|
|
460
|
+
if (!locales.length) return void 0;
|
|
461
|
+
const bundle = {};
|
|
462
|
+
for (const locale of locales) {
|
|
463
|
+
const data = i18n.getTranslations(locale);
|
|
464
|
+
if (data && typeof data === "object") bundle[locale] = data;
|
|
465
|
+
}
|
|
466
|
+
return Object.keys(bundle).length ? bundle : void 0;
|
|
467
|
+
}
|
|
468
|
+
/**
|
|
469
|
+
* Parse the highest-priority locale from an `Accept-Language` header.
|
|
470
|
+
* Falls back to a `?locale=` query parameter, then to the i18n service's
|
|
471
|
+
* default locale. Returns `undefined` when no preference is expressed
|
|
472
|
+
* (callers will then return untranslated metadata).
|
|
473
|
+
*/
|
|
474
|
+
extractLocale(req, i18n) {
|
|
475
|
+
const headers = req?.headers;
|
|
476
|
+
let header;
|
|
477
|
+
if (headers) {
|
|
478
|
+
header = typeof headers.get === "function" ? headers.get("accept-language") ?? void 0 : headers["accept-language"] ?? headers["Accept-Language"];
|
|
479
|
+
}
|
|
480
|
+
if (typeof header === "string" && header.length > 0) {
|
|
481
|
+
const top = header.split(",")[0]?.split(";")[0]?.trim();
|
|
482
|
+
if (top) return top;
|
|
483
|
+
}
|
|
484
|
+
const queryLocale = req?.query?.locale;
|
|
485
|
+
if (typeof queryLocale === "string" && queryLocale.length > 0) return queryLocale;
|
|
486
|
+
if (i18n && typeof i18n.getDefaultLocale === "function") {
|
|
487
|
+
const def = i18n.getDefaultLocale();
|
|
488
|
+
if (typeof def === "string" && def.length > 0) return def;
|
|
489
|
+
}
|
|
490
|
+
return void 0;
|
|
491
|
+
}
|
|
492
|
+
/**
|
|
493
|
+
* Translate a single metadata document (view or action) when an i18n
|
|
494
|
+
* service is registered for the request's project and the requested
|
|
495
|
+
* locale yields a match. Falls through unchanged for unsupported types
|
|
496
|
+
* or missing translations.
|
|
497
|
+
*/
|
|
498
|
+
async translateMetaItem(req, type, projectId, item) {
|
|
499
|
+
if (!item || typeof item !== "object") return item;
|
|
500
|
+
if (type !== "view" && type !== "action") return item;
|
|
501
|
+
const i18n = await this.resolveI18nService(projectId);
|
|
502
|
+
const bundle = this.buildTranslationBundle(i18n);
|
|
503
|
+
if (!bundle) return item;
|
|
504
|
+
const locale = this.extractLocale(req, i18n);
|
|
505
|
+
if (!locale) return item;
|
|
506
|
+
const { translateMetadataDocument } = await import("@objectstack/spec/system");
|
|
507
|
+
return translateMetadataDocument(type, item, bundle, { locale });
|
|
508
|
+
}
|
|
509
|
+
/**
|
|
510
|
+
* Translate a list of metadata documents using `translateMetaItem`.
|
|
511
|
+
*/
|
|
512
|
+
async translateMetaItems(req, type, projectId, items) {
|
|
513
|
+
if (!Array.isArray(items)) return items;
|
|
514
|
+
if (type !== "view" && type !== "action") return items;
|
|
515
|
+
const i18n = await this.resolveI18nService(projectId);
|
|
516
|
+
const bundle = this.buildTranslationBundle(i18n);
|
|
517
|
+
if (!bundle) return items;
|
|
518
|
+
const locale = this.extractLocale(req, i18n);
|
|
519
|
+
if (!locale) return items;
|
|
520
|
+
const { translateMetadataDocument } = await import("@objectstack/spec/system");
|
|
521
|
+
return items.map((item) => translateMetadataDocument(type, item, bundle, { locale }));
|
|
522
|
+
}
|
|
523
|
+
/**
|
|
524
|
+
* Pull the request hostname (without port) from a Node-style `req` or
|
|
525
|
+
* a Fetch-style request wrapper. Returns undefined when no Host header
|
|
526
|
+
* is available.
|
|
527
|
+
*/
|
|
528
|
+
extractHostname(req) {
|
|
529
|
+
const headers = req?.headers;
|
|
530
|
+
let host;
|
|
531
|
+
if (headers) {
|
|
532
|
+
if (typeof headers.get === "function") {
|
|
533
|
+
host = headers.get("host") ?? void 0;
|
|
534
|
+
} else {
|
|
535
|
+
host = headers.host ?? headers.Host;
|
|
536
|
+
}
|
|
537
|
+
}
|
|
538
|
+
if (!host && typeof req?.hostname === "string") host = req.hostname;
|
|
539
|
+
if (!host && typeof req?.url === "string") {
|
|
540
|
+
try {
|
|
541
|
+
host = new globalThis.URL(req.url).host;
|
|
542
|
+
} catch {
|
|
543
|
+
}
|
|
544
|
+
}
|
|
545
|
+
if (!host) return void 0;
|
|
546
|
+
return String(host).split(":")[0].toLowerCase();
|
|
547
|
+
}
|
|
548
|
+
/**
|
|
549
|
+
* Pull the `X-Project-Id` header from a Node- or Fetch-style request.
|
|
550
|
+
* Header names are case-insensitive; we probe both casings to cover
|
|
551
|
+
* adapters that don't normalize headers (e.g. raw Node http).
|
|
552
|
+
*/
|
|
553
|
+
extractProjectIdHeader(req) {
|
|
554
|
+
const headers = req?.headers;
|
|
555
|
+
if (!headers) return void 0;
|
|
556
|
+
let val;
|
|
557
|
+
if (typeof headers.get === "function") {
|
|
558
|
+
val = headers.get("x-project-id") ?? headers.get("X-Project-Id");
|
|
559
|
+
} else {
|
|
560
|
+
val = headers["x-project-id"] ?? headers["X-Project-Id"];
|
|
561
|
+
}
|
|
562
|
+
if (Array.isArray(val)) val = val[0];
|
|
563
|
+
if (typeof val !== "string") return void 0;
|
|
564
|
+
const trimmed = val.trim();
|
|
565
|
+
return trimmed.length > 0 ? trimmed : void 0;
|
|
217
566
|
}
|
|
218
567
|
/**
|
|
219
568
|
* Normalize configuration with defaults
|
|
@@ -234,6 +583,8 @@ var RestServer = class {
|
|
|
234
583
|
enableUi: api.enableUi ?? true,
|
|
235
584
|
enableBatch: api.enableBatch ?? true,
|
|
236
585
|
enableDiscovery: api.enableDiscovery ?? true,
|
|
586
|
+
enableProjectScoping: api.enableProjectScoping ?? false,
|
|
587
|
+
projectResolution: api.projectResolution ?? "auto",
|
|
237
588
|
documentation: api.documentation,
|
|
238
589
|
responseFormat: api.responseFormat
|
|
239
590
|
},
|
|
@@ -286,51 +637,88 @@ var RestServer = class {
|
|
|
286
637
|
const { api } = this.config;
|
|
287
638
|
return api.apiPath ?? `${api.basePath}/${api.version}`;
|
|
288
639
|
}
|
|
640
|
+
/**
|
|
641
|
+
* Get the project-scoped base path for a given unscoped base.
|
|
642
|
+
* Example: `/api/v1` → `/api/v1/projects/:projectId`.
|
|
643
|
+
*/
|
|
644
|
+
getScopedBasePath(basePath) {
|
|
645
|
+
return `${basePath}/projects/:projectId`;
|
|
646
|
+
}
|
|
289
647
|
/**
|
|
290
648
|
* Register all REST API routes
|
|
649
|
+
*
|
|
650
|
+
* When `enableProjectScoping` is true, routes are registered under
|
|
651
|
+
* `/api/v1/projects/:projectId/...`. The `projectResolution` strategy
|
|
652
|
+
* controls whether unscoped legacy routes remain available:
|
|
653
|
+
* - `required` → only scoped routes registered.
|
|
654
|
+
* - `optional` / `auto` → both scoped and unscoped routes registered.
|
|
291
655
|
*/
|
|
292
656
|
registerRoutes() {
|
|
293
657
|
const basePath = this.getApiBasePath();
|
|
294
|
-
|
|
295
|
-
|
|
296
|
-
|
|
297
|
-
|
|
298
|
-
|
|
299
|
-
|
|
300
|
-
|
|
301
|
-
|
|
302
|
-
|
|
303
|
-
|
|
304
|
-
|
|
305
|
-
|
|
306
|
-
|
|
307
|
-
|
|
658
|
+
const { enableProjectScoping, projectResolution } = this.config.api;
|
|
659
|
+
const registerForBase = (bp) => {
|
|
660
|
+
if (this.config.api.enableDiscovery) {
|
|
661
|
+
this.registerDiscoveryEndpoints(bp);
|
|
662
|
+
}
|
|
663
|
+
if (this.config.api.enableMetadata) {
|
|
664
|
+
this.registerMetadataEndpoints(bp);
|
|
665
|
+
}
|
|
666
|
+
if (this.config.api.enableUi) {
|
|
667
|
+
this.registerUiEndpoints(bp);
|
|
668
|
+
}
|
|
669
|
+
if (this.config.api.enableCrud) {
|
|
670
|
+
this.registerCrudEndpoints(bp);
|
|
671
|
+
}
|
|
672
|
+
if (this.config.api.enableBatch) {
|
|
673
|
+
this.registerBatchEndpoints(bp);
|
|
674
|
+
}
|
|
675
|
+
};
|
|
676
|
+
if (enableProjectScoping) {
|
|
677
|
+
const scopedBase = this.getScopedBasePath(basePath);
|
|
678
|
+
if (projectResolution === "required") {
|
|
679
|
+
registerForBase(scopedBase);
|
|
680
|
+
} else {
|
|
681
|
+
registerForBase(basePath);
|
|
682
|
+
registerForBase(scopedBase);
|
|
683
|
+
}
|
|
684
|
+
} else {
|
|
685
|
+
registerForBase(basePath);
|
|
308
686
|
}
|
|
309
687
|
}
|
|
310
688
|
/**
|
|
311
689
|
* Register discovery endpoints
|
|
312
690
|
*/
|
|
313
691
|
registerDiscoveryEndpoints(basePath) {
|
|
314
|
-
const
|
|
692
|
+
const isScoped = basePath.includes("/projects/:projectId");
|
|
693
|
+
const discoveryHandler = async (req, res) => {
|
|
315
694
|
try {
|
|
316
695
|
const discovery = await this.protocol.getDiscovery();
|
|
317
696
|
discovery.version = this.config.api.version;
|
|
697
|
+
const realBase = isScoped ? basePath.replace(":projectId", req.params?.projectId ?? ":projectId") : basePath;
|
|
318
698
|
if (discovery.routes) {
|
|
319
699
|
if (this.config.api.enableCrud) {
|
|
320
|
-
discovery.routes.data = `${
|
|
700
|
+
discovery.routes.data = `${realBase}${this.config.crud.dataPrefix}`;
|
|
321
701
|
}
|
|
322
702
|
if (this.config.api.enableMetadata) {
|
|
323
|
-
discovery.routes.metadata = `${
|
|
703
|
+
discovery.routes.metadata = `${realBase}${this.config.metadata.prefix}`;
|
|
324
704
|
}
|
|
325
705
|
if (this.config.api.enableUi) {
|
|
326
|
-
discovery.routes.ui = `${
|
|
706
|
+
discovery.routes.ui = `${realBase}/ui`;
|
|
327
707
|
}
|
|
328
708
|
if (discovery.routes.auth) {
|
|
329
|
-
|
|
709
|
+
const unscopedBase = isScoped ? basePath.replace(/\/projects\/:projectId$/, "") : basePath;
|
|
710
|
+
discovery.routes.auth = `${unscopedBase}/auth`;
|
|
330
711
|
}
|
|
331
712
|
}
|
|
713
|
+
discovery.scoping = {
|
|
714
|
+
enabled: this.config.api.enableProjectScoping,
|
|
715
|
+
resolution: this.config.api.projectResolution,
|
|
716
|
+
scoped: isScoped,
|
|
717
|
+
projectId: isScoped ? req.params?.projectId : void 0
|
|
718
|
+
};
|
|
332
719
|
res.json(discovery);
|
|
333
720
|
} catch (error) {
|
|
721
|
+
logError("[REST] Unhandled error:", error);
|
|
334
722
|
res.status(500).json({ error: error.message });
|
|
335
723
|
}
|
|
336
724
|
};
|
|
@@ -359,15 +747,19 @@ var RestServer = class {
|
|
|
359
747
|
registerMetadataEndpoints(basePath) {
|
|
360
748
|
const { metadata } = this.config;
|
|
361
749
|
const metaPath = `${basePath}${metadata.prefix}`;
|
|
750
|
+
const isScoped = basePath.includes("/projects/:projectId");
|
|
362
751
|
if (metadata.endpoints.types !== false) {
|
|
363
752
|
this.routeManager.register({
|
|
364
753
|
method: "GET",
|
|
365
754
|
path: metaPath,
|
|
366
|
-
handler: async (
|
|
755
|
+
handler: async (req, res) => {
|
|
367
756
|
try {
|
|
368
|
-
const
|
|
757
|
+
const projectId = isScoped ? req.params?.projectId : void 0;
|
|
758
|
+
const p = await this.resolveProtocol(projectId, req);
|
|
759
|
+
const types = await p.getMetaTypes();
|
|
369
760
|
res.json(types);
|
|
370
761
|
} catch (error) {
|
|
762
|
+
logError("[REST] Unhandled error:", error);
|
|
371
763
|
res.status(500).json({ error: error.message });
|
|
372
764
|
}
|
|
373
765
|
},
|
|
@@ -384,9 +776,18 @@ var RestServer = class {
|
|
|
384
776
|
handler: async (req, res) => {
|
|
385
777
|
try {
|
|
386
778
|
const packageId = req.query?.package || void 0;
|
|
387
|
-
const
|
|
388
|
-
|
|
779
|
+
const projectId = isScoped ? req.params?.projectId : void 0;
|
|
780
|
+
const p = await this.resolveProtocol(projectId, req);
|
|
781
|
+
const items = await p.getMetaItems({
|
|
782
|
+
type: req.params.type,
|
|
783
|
+
packageId,
|
|
784
|
+
...projectId ? { projectId } : {}
|
|
785
|
+
});
|
|
786
|
+
const translated = await this.translateMetaItems(req, req.params.type, projectId, items);
|
|
787
|
+
res.header("Vary", "Accept-Language");
|
|
788
|
+
res.json(translated);
|
|
389
789
|
} catch (error) {
|
|
790
|
+
logError("[REST] Unhandled error:", error);
|
|
390
791
|
res.status(404).json({ error: error.message });
|
|
391
792
|
}
|
|
392
793
|
},
|
|
@@ -402,15 +803,18 @@ var RestServer = class {
|
|
|
402
803
|
path: `${metaPath}/:type/:name`,
|
|
403
804
|
handler: async (req, res) => {
|
|
404
805
|
try {
|
|
405
|
-
|
|
806
|
+
const projectId = isScoped ? req.params?.projectId : void 0;
|
|
807
|
+
const p = await this.resolveProtocol(projectId, req);
|
|
808
|
+
if (metadata.enableCache && p.getMetaItemCached) {
|
|
406
809
|
const cacheRequest = {
|
|
407
810
|
ifNoneMatch: req.headers["if-none-match"],
|
|
408
811
|
ifModifiedSince: req.headers["if-modified-since"]
|
|
409
812
|
};
|
|
410
|
-
const result = await
|
|
813
|
+
const result = await p.getMetaItemCached({
|
|
411
814
|
type: req.params.type,
|
|
412
815
|
name: req.params.name,
|
|
413
|
-
cacheRequest
|
|
816
|
+
cacheRequest,
|
|
817
|
+
...projectId ? { projectId } : {}
|
|
414
818
|
});
|
|
415
819
|
if (result.notModified) {
|
|
416
820
|
res.status(304).send();
|
|
@@ -428,13 +832,20 @@ var RestServer = class {
|
|
|
428
832
|
const maxAge = result.cacheControl.maxAge ? `, max-age=${result.cacheControl.maxAge}` : "";
|
|
429
833
|
res.header("Cache-Control", directives + maxAge);
|
|
430
834
|
}
|
|
431
|
-
res.
|
|
835
|
+
res.header("Vary", "Accept-Language");
|
|
836
|
+
res.json(await this.translateMetaItem(req, req.params.type, projectId, result.data));
|
|
432
837
|
} else {
|
|
433
838
|
const packageId = req.query?.package || void 0;
|
|
434
|
-
const item = await
|
|
435
|
-
|
|
839
|
+
const item = await p.getMetaItem({
|
|
840
|
+
type: req.params.type,
|
|
841
|
+
name: req.params.name,
|
|
842
|
+
packageId
|
|
843
|
+
});
|
|
844
|
+
res.header("Vary", "Accept-Language");
|
|
845
|
+
res.json(await this.translateMetaItem(req, req.params.type, projectId, item));
|
|
436
846
|
}
|
|
437
847
|
} catch (error) {
|
|
848
|
+
logError("[REST] Unhandled error:", error);
|
|
438
849
|
res.status(404).json({ error: error.message });
|
|
439
850
|
}
|
|
440
851
|
},
|
|
@@ -449,17 +860,21 @@ var RestServer = class {
|
|
|
449
860
|
path: `${metaPath}/:type/:name`,
|
|
450
861
|
handler: async (req, res) => {
|
|
451
862
|
try {
|
|
452
|
-
|
|
863
|
+
const projectId = isScoped ? req.params?.projectId : void 0;
|
|
864
|
+
const p = await this.resolveProtocol(projectId, req);
|
|
865
|
+
if (!p.saveMetaItem) {
|
|
453
866
|
res.status(501).json({ error: "Save operation not supported by protocol implementation" });
|
|
454
867
|
return;
|
|
455
868
|
}
|
|
456
|
-
const result = await
|
|
869
|
+
const result = await p.saveMetaItem({
|
|
457
870
|
type: req.params.type,
|
|
458
871
|
name: req.params.name,
|
|
459
|
-
item: req.body
|
|
872
|
+
item: req.body,
|
|
873
|
+
...projectId ? { projectId } : {}
|
|
460
874
|
});
|
|
461
875
|
res.json(result);
|
|
462
876
|
} catch (error) {
|
|
877
|
+
logError("[REST] Unhandled error:", error);
|
|
463
878
|
res.status(400).json({ error: error.message });
|
|
464
879
|
}
|
|
465
880
|
},
|
|
@@ -474,21 +889,26 @@ var RestServer = class {
|
|
|
474
889
|
*/
|
|
475
890
|
registerUiEndpoints(basePath) {
|
|
476
891
|
const uiPath = `${basePath}/ui`;
|
|
892
|
+
const isScoped = basePath.includes("/projects/:projectId");
|
|
477
893
|
this.routeManager.register({
|
|
478
894
|
method: "GET",
|
|
479
895
|
path: `${uiPath}/view/:object/:type`,
|
|
480
896
|
handler: async (req, res) => {
|
|
481
897
|
try {
|
|
482
|
-
|
|
483
|
-
|
|
898
|
+
const projectId = isScoped ? req.params?.projectId : void 0;
|
|
899
|
+
const p = await this.resolveProtocol(projectId, req);
|
|
900
|
+
if (p.getUiView) {
|
|
901
|
+
const view = await p.getUiView({
|
|
484
902
|
object: req.params.object,
|
|
485
|
-
type: req.params.type
|
|
903
|
+
type: req.params.type,
|
|
904
|
+
...projectId ? { projectId } : {}
|
|
486
905
|
});
|
|
487
906
|
res.json(view);
|
|
488
907
|
} else {
|
|
489
908
|
res.status(501).json({ error: "UI View resolution not supported by protocol implementation" });
|
|
490
909
|
}
|
|
491
910
|
} catch (error) {
|
|
911
|
+
logError("[REST] Unhandled error:", error);
|
|
492
912
|
res.status(404).json({ error: error.message });
|
|
493
913
|
}
|
|
494
914
|
},
|
|
@@ -504,6 +924,7 @@ var RestServer = class {
|
|
|
504
924
|
registerCrudEndpoints(basePath) {
|
|
505
925
|
const { crud } = this.config;
|
|
506
926
|
const dataPath = `${basePath}${crud.dataPrefix}`;
|
|
927
|
+
const isScoped = basePath.includes("/projects/:projectId");
|
|
507
928
|
const operations = crud.operations;
|
|
508
929
|
if (operations.list) {
|
|
509
930
|
this.routeManager.register({
|
|
@@ -511,13 +932,24 @@ var RestServer = class {
|
|
|
511
932
|
path: `${dataPath}/:object`,
|
|
512
933
|
handler: async (req, res) => {
|
|
513
934
|
try {
|
|
514
|
-
const
|
|
935
|
+
const projectId = isScoped ? req.params?.projectId : void 0;
|
|
936
|
+
const p = await this.resolveProtocol(projectId, req);
|
|
937
|
+
const context = await this.resolveExecCtx(projectId, req);
|
|
938
|
+
const result = await p.findData({
|
|
515
939
|
object: req.params.object,
|
|
516
|
-
query: req.query
|
|
940
|
+
query: req.query,
|
|
941
|
+
...projectId ? { projectId } : {},
|
|
942
|
+
...context ? { context } : {}
|
|
517
943
|
});
|
|
518
944
|
res.json(result);
|
|
519
945
|
} catch (error) {
|
|
520
|
-
|
|
946
|
+
const mapped = mapDataError(error, req.params?.object);
|
|
947
|
+
if (mapped.status === 404 || mapped.status === 503 || mapped.status === 502) {
|
|
948
|
+
res.status(mapped.status).json(mapped.body);
|
|
949
|
+
} else {
|
|
950
|
+
logError("[REST] Unhandled error:", error);
|
|
951
|
+
res.status(mapped.status).json(mapped.body);
|
|
952
|
+
}
|
|
521
953
|
}
|
|
522
954
|
},
|
|
523
955
|
metadata: {
|
|
@@ -532,16 +964,23 @@ var RestServer = class {
|
|
|
532
964
|
path: `${dataPath}/:object/:id`,
|
|
533
965
|
handler: async (req, res) => {
|
|
534
966
|
try {
|
|
967
|
+
const projectId = isScoped ? req.params?.projectId : void 0;
|
|
968
|
+
const p = await this.resolveProtocol(projectId, req);
|
|
535
969
|
const { select, expand } = req.query || {};
|
|
536
|
-
const
|
|
970
|
+
const context = await this.resolveExecCtx(projectId, req);
|
|
971
|
+
const result = await p.getData({
|
|
537
972
|
object: req.params.object,
|
|
538
973
|
id: req.params.id,
|
|
539
974
|
...select != null ? { select } : {},
|
|
540
|
-
...expand != null ? { expand } : {}
|
|
975
|
+
...expand != null ? { expand } : {},
|
|
976
|
+
...projectId ? { projectId } : {},
|
|
977
|
+
...context ? { context } : {}
|
|
541
978
|
});
|
|
542
979
|
res.json(result);
|
|
543
980
|
} catch (error) {
|
|
544
|
-
|
|
981
|
+
const mapped = mapDataError(error, req.params?.object);
|
|
982
|
+
if (!isExpectedDataStatus(mapped.status)) logError("[REST] Unhandled error:", error);
|
|
983
|
+
res.status(mapped.status === 400 ? 404 : mapped.status).json(mapped.body);
|
|
545
984
|
}
|
|
546
985
|
},
|
|
547
986
|
metadata: {
|
|
@@ -556,13 +995,20 @@ var RestServer = class {
|
|
|
556
995
|
path: `${dataPath}/:object`,
|
|
557
996
|
handler: async (req, res) => {
|
|
558
997
|
try {
|
|
559
|
-
const
|
|
998
|
+
const projectId = isScoped ? req.params?.projectId : void 0;
|
|
999
|
+
const p = await this.resolveProtocol(projectId, req);
|
|
1000
|
+
const context = await this.resolveExecCtx(projectId, req);
|
|
1001
|
+
const result = await p.createData({
|
|
560
1002
|
object: req.params.object,
|
|
561
|
-
data: req.body
|
|
1003
|
+
data: req.body,
|
|
1004
|
+
...projectId ? { projectId } : {},
|
|
1005
|
+
...context ? { context } : {}
|
|
562
1006
|
});
|
|
563
1007
|
res.status(201).json(result);
|
|
564
1008
|
} catch (error) {
|
|
565
|
-
|
|
1009
|
+
const mapped = mapDataError(error, req.params?.object);
|
|
1010
|
+
if (!isExpectedDataStatus(mapped.status)) logError("[REST] Unhandled error:", error);
|
|
1011
|
+
res.status(mapped.status).json(mapped.body);
|
|
566
1012
|
}
|
|
567
1013
|
},
|
|
568
1014
|
metadata: {
|
|
@@ -577,14 +1023,21 @@ var RestServer = class {
|
|
|
577
1023
|
path: `${dataPath}/:object/:id`,
|
|
578
1024
|
handler: async (req, res) => {
|
|
579
1025
|
try {
|
|
580
|
-
const
|
|
1026
|
+
const projectId = isScoped ? req.params?.projectId : void 0;
|
|
1027
|
+
const p = await this.resolveProtocol(projectId, req);
|
|
1028
|
+
const context = await this.resolveExecCtx(projectId, req);
|
|
1029
|
+
const result = await p.updateData({
|
|
581
1030
|
object: req.params.object,
|
|
582
1031
|
id: req.params.id,
|
|
583
|
-
data: req.body
|
|
1032
|
+
data: req.body,
|
|
1033
|
+
...projectId ? { projectId } : {},
|
|
1034
|
+
...context ? { context } : {}
|
|
584
1035
|
});
|
|
585
1036
|
res.json(result);
|
|
586
1037
|
} catch (error) {
|
|
587
|
-
|
|
1038
|
+
const mapped = mapDataError(error, req.params?.object);
|
|
1039
|
+
if (!isExpectedDataStatus(mapped.status)) logError("[REST] Unhandled error:", error);
|
|
1040
|
+
res.status(mapped.status).json(mapped.body);
|
|
588
1041
|
}
|
|
589
1042
|
},
|
|
590
1043
|
metadata: {
|
|
@@ -599,13 +1052,20 @@ var RestServer = class {
|
|
|
599
1052
|
path: `${dataPath}/:object/:id`,
|
|
600
1053
|
handler: async (req, res) => {
|
|
601
1054
|
try {
|
|
602
|
-
const
|
|
1055
|
+
const projectId = isScoped ? req.params?.projectId : void 0;
|
|
1056
|
+
const p = await this.resolveProtocol(projectId, req);
|
|
1057
|
+
const context = await this.resolveExecCtx(projectId, req);
|
|
1058
|
+
const result = await p.deleteData({
|
|
603
1059
|
object: req.params.object,
|
|
604
|
-
id: req.params.id
|
|
1060
|
+
id: req.params.id,
|
|
1061
|
+
...projectId ? { projectId } : {},
|
|
1062
|
+
...context ? { context } : {}
|
|
605
1063
|
});
|
|
606
1064
|
res.json(result);
|
|
607
1065
|
} catch (error) {
|
|
608
|
-
|
|
1066
|
+
const mapped = mapDataError(error, req.params?.object);
|
|
1067
|
+
if (!isExpectedDataStatus(mapped.status)) logError("[REST] Unhandled error:", error);
|
|
1068
|
+
res.status(mapped.status).json(mapped.body);
|
|
609
1069
|
}
|
|
610
1070
|
},
|
|
611
1071
|
metadata: {
|
|
@@ -621,6 +1081,7 @@ var RestServer = class {
|
|
|
621
1081
|
registerBatchEndpoints(basePath) {
|
|
622
1082
|
const { crud, batch } = this.config;
|
|
623
1083
|
const dataPath = `${basePath}${crud.dataPrefix}`;
|
|
1084
|
+
const isScoped = basePath.includes("/projects/:projectId");
|
|
624
1085
|
const operations = batch.operations;
|
|
625
1086
|
if (batch.enableBatchEndpoint && this.protocol.batchData) {
|
|
626
1087
|
this.routeManager.register({
|
|
@@ -628,12 +1089,16 @@ var RestServer = class {
|
|
|
628
1089
|
path: `${dataPath}/:object/batch`,
|
|
629
1090
|
handler: async (req, res) => {
|
|
630
1091
|
try {
|
|
631
|
-
const
|
|
1092
|
+
const projectId = isScoped ? req.params?.projectId : void 0;
|
|
1093
|
+
const p = await this.resolveProtocol(projectId, req);
|
|
1094
|
+
const result = await p.batchData({
|
|
632
1095
|
object: req.params.object,
|
|
633
|
-
request: req.body
|
|
1096
|
+
request: req.body,
|
|
1097
|
+
...projectId ? { projectId } : {}
|
|
634
1098
|
});
|
|
635
1099
|
res.json(result);
|
|
636
1100
|
} catch (error) {
|
|
1101
|
+
logError("[REST] Unhandled error:", error);
|
|
637
1102
|
res.status(400).json({ error: error.message });
|
|
638
1103
|
}
|
|
639
1104
|
},
|
|
@@ -649,12 +1114,16 @@ var RestServer = class {
|
|
|
649
1114
|
path: `${dataPath}/:object/createMany`,
|
|
650
1115
|
handler: async (req, res) => {
|
|
651
1116
|
try {
|
|
652
|
-
const
|
|
1117
|
+
const projectId = isScoped ? req.params?.projectId : void 0;
|
|
1118
|
+
const p = await this.resolveProtocol(projectId, req);
|
|
1119
|
+
const result = await p.createManyData({
|
|
653
1120
|
object: req.params.object,
|
|
654
|
-
records: req.body || []
|
|
1121
|
+
records: req.body || [],
|
|
1122
|
+
...projectId ? { projectId } : {}
|
|
655
1123
|
});
|
|
656
1124
|
res.status(201).json(result);
|
|
657
1125
|
} catch (error) {
|
|
1126
|
+
logError("[REST] Unhandled error:", error);
|
|
658
1127
|
res.status(400).json({ error: error.message });
|
|
659
1128
|
}
|
|
660
1129
|
},
|
|
@@ -670,12 +1139,16 @@ var RestServer = class {
|
|
|
670
1139
|
path: `${dataPath}/:object/updateMany`,
|
|
671
1140
|
handler: async (req, res) => {
|
|
672
1141
|
try {
|
|
673
|
-
const
|
|
1142
|
+
const projectId = isScoped ? req.params?.projectId : void 0;
|
|
1143
|
+
const p = await this.resolveProtocol(projectId, req);
|
|
1144
|
+
const result = await p.updateManyData({
|
|
674
1145
|
object: req.params.object,
|
|
675
|
-
...req.body
|
|
1146
|
+
...req.body,
|
|
1147
|
+
...projectId ? { projectId } : {}
|
|
676
1148
|
});
|
|
677
1149
|
res.json(result);
|
|
678
1150
|
} catch (error) {
|
|
1151
|
+
logError("[REST] Unhandled error:", error);
|
|
679
1152
|
res.status(400).json({ error: error.message });
|
|
680
1153
|
}
|
|
681
1154
|
},
|
|
@@ -691,12 +1164,16 @@ var RestServer = class {
|
|
|
691
1164
|
path: `${dataPath}/:object/deleteMany`,
|
|
692
1165
|
handler: async (req, res) => {
|
|
693
1166
|
try {
|
|
694
|
-
const
|
|
1167
|
+
const projectId = isScoped ? req.params?.projectId : void 0;
|
|
1168
|
+
const p = await this.resolveProtocol(projectId, req);
|
|
1169
|
+
const result = await p.deleteManyData({
|
|
695
1170
|
object: req.params.object,
|
|
696
|
-
...req.body
|
|
1171
|
+
...req.body,
|
|
1172
|
+
...projectId ? { projectId } : {}
|
|
697
1173
|
});
|
|
698
1174
|
res.json(result);
|
|
699
1175
|
} catch (error) {
|
|
1176
|
+
logError("[REST] Unhandled error:", error);
|
|
700
1177
|
res.status(400).json({ error: error.message });
|
|
701
1178
|
}
|
|
702
1179
|
},
|
|
@@ -721,6 +1198,123 @@ var RestServer = class {
|
|
|
721
1198
|
}
|
|
722
1199
|
};
|
|
723
1200
|
|
|
1201
|
+
// src/package-routes.ts
|
|
1202
|
+
function registerPackageRoutes(server, packageService, basePath = "/api/v1", options = {}) {
|
|
1203
|
+
const packagesPath = `${basePath}/packages`;
|
|
1204
|
+
server.post(packagesPath, async (req, res) => {
|
|
1205
|
+
try {
|
|
1206
|
+
const { manifest, metadata } = req.body || {};
|
|
1207
|
+
if (!manifest || !metadata) {
|
|
1208
|
+
res.status(400).json({ error: "Missing required fields: manifest, metadata" });
|
|
1209
|
+
return;
|
|
1210
|
+
}
|
|
1211
|
+
if (!manifest.id || !manifest.version) {
|
|
1212
|
+
res.status(400).json({ error: "Invalid manifest: id and version are required" });
|
|
1213
|
+
return;
|
|
1214
|
+
}
|
|
1215
|
+
const result = await packageService.publish({ manifest, metadata });
|
|
1216
|
+
if (result.success) {
|
|
1217
|
+
res.json({
|
|
1218
|
+
success: true,
|
|
1219
|
+
message: `Published ${manifest.id}@${manifest.version}`,
|
|
1220
|
+
package: {
|
|
1221
|
+
id: manifest.id,
|
|
1222
|
+
version: manifest.version
|
|
1223
|
+
}
|
|
1224
|
+
});
|
|
1225
|
+
return;
|
|
1226
|
+
}
|
|
1227
|
+
res.status(400).json({ success: false, error: result.error });
|
|
1228
|
+
} catch (error) {
|
|
1229
|
+
res.status(500).json({ error: error.message });
|
|
1230
|
+
}
|
|
1231
|
+
});
|
|
1232
|
+
server.get(packagesPath, async (_req, res) => {
|
|
1233
|
+
try {
|
|
1234
|
+
const packagesMap = /* @__PURE__ */ new Map();
|
|
1235
|
+
if (options.protocol && typeof options.protocol.getMetaItems === "function") {
|
|
1236
|
+
try {
|
|
1237
|
+
const result = await options.protocol.getMetaItems({ type: "package" });
|
|
1238
|
+
if (result?.items) {
|
|
1239
|
+
for (const item of result.items) {
|
|
1240
|
+
const id = item.manifest?.id || item.id;
|
|
1241
|
+
if (id) {
|
|
1242
|
+
packagesMap.set(id, {
|
|
1243
|
+
...item,
|
|
1244
|
+
source: "registry"
|
|
1245
|
+
});
|
|
1246
|
+
}
|
|
1247
|
+
}
|
|
1248
|
+
}
|
|
1249
|
+
} catch {
|
|
1250
|
+
}
|
|
1251
|
+
}
|
|
1252
|
+
try {
|
|
1253
|
+
const dbPackages = await packageService.list();
|
|
1254
|
+
for (const pkg of dbPackages) {
|
|
1255
|
+
const id = pkg.manifest?.id || pkg.id;
|
|
1256
|
+
if (id) {
|
|
1257
|
+
packagesMap.set(id, {
|
|
1258
|
+
...packagesMap.get(id),
|
|
1259
|
+
...pkg,
|
|
1260
|
+
source: packagesMap.has(id) ? "both" : "database"
|
|
1261
|
+
});
|
|
1262
|
+
}
|
|
1263
|
+
}
|
|
1264
|
+
} catch {
|
|
1265
|
+
}
|
|
1266
|
+
const packages = Array.from(packagesMap.values());
|
|
1267
|
+
res.json({ packages, total: packages.length });
|
|
1268
|
+
} catch (error) {
|
|
1269
|
+
res.status(500).json({ error: error.message });
|
|
1270
|
+
}
|
|
1271
|
+
});
|
|
1272
|
+
server.get(`${packagesPath}/:id`, async (req, res) => {
|
|
1273
|
+
try {
|
|
1274
|
+
const packageId = req.params.id;
|
|
1275
|
+
const version = req.query?.version || "latest";
|
|
1276
|
+
const pkg = await packageService.get(packageId, version);
|
|
1277
|
+
if (pkg) {
|
|
1278
|
+
res.json({ package: { ...pkg, source: "database" } });
|
|
1279
|
+
return;
|
|
1280
|
+
}
|
|
1281
|
+
if (options.protocol && typeof options.protocol.getMetaItems === "function") {
|
|
1282
|
+
try {
|
|
1283
|
+
const result = await options.protocol.getMetaItems({ type: "package" });
|
|
1284
|
+
const match = result?.items?.find(
|
|
1285
|
+
(item) => (item.manifest?.id || item.id) === packageId
|
|
1286
|
+
);
|
|
1287
|
+
if (match) {
|
|
1288
|
+
res.json({ package: { ...match, source: "registry" } });
|
|
1289
|
+
return;
|
|
1290
|
+
}
|
|
1291
|
+
} catch {
|
|
1292
|
+
}
|
|
1293
|
+
}
|
|
1294
|
+
res.status(404).json({ error: "Package not found" });
|
|
1295
|
+
} catch (error) {
|
|
1296
|
+
res.status(500).json({ error: error.message });
|
|
1297
|
+
}
|
|
1298
|
+
});
|
|
1299
|
+
server.delete(`${packagesPath}/:id`, async (req, res) => {
|
|
1300
|
+
try {
|
|
1301
|
+
const packageId = req.params.id;
|
|
1302
|
+
const version = req.query?.version;
|
|
1303
|
+
const result = await packageService.delete(packageId, version);
|
|
1304
|
+
if (result.success) {
|
|
1305
|
+
res.json({
|
|
1306
|
+
success: true,
|
|
1307
|
+
message: `Deleted ${packageId}${version ? `@${version}` : ""}`
|
|
1308
|
+
});
|
|
1309
|
+
return;
|
|
1310
|
+
}
|
|
1311
|
+
res.status(400).json({ success: false });
|
|
1312
|
+
} catch (error) {
|
|
1313
|
+
res.status(500).json({ error: error.message });
|
|
1314
|
+
}
|
|
1315
|
+
});
|
|
1316
|
+
}
|
|
1317
|
+
|
|
724
1318
|
// src/rest-api-plugin.ts
|
|
725
1319
|
function createRestApiPlugin(config = {}) {
|
|
726
1320
|
return {
|
|
@@ -741,6 +1335,39 @@ function createRestApiPlugin(config = {}) {
|
|
|
741
1335
|
protocol = ctx.getService(protocolService);
|
|
742
1336
|
} catch (e) {
|
|
743
1337
|
}
|
|
1338
|
+
let kernelManager;
|
|
1339
|
+
const kernelManagerService = config.kernelManagerServiceName || "kernel-manager";
|
|
1340
|
+
try {
|
|
1341
|
+
kernelManager = ctx.getService(kernelManagerService);
|
|
1342
|
+
} catch (e) {
|
|
1343
|
+
}
|
|
1344
|
+
let envRegistry;
|
|
1345
|
+
try {
|
|
1346
|
+
envRegistry = ctx.getService("env-registry");
|
|
1347
|
+
} catch (e) {
|
|
1348
|
+
}
|
|
1349
|
+
const defaultProjectIdProvider = () => {
|
|
1350
|
+
try {
|
|
1351
|
+
const dp = ctx.getService("default-project");
|
|
1352
|
+
return dp?.projectId;
|
|
1353
|
+
} catch {
|
|
1354
|
+
return void 0;
|
|
1355
|
+
}
|
|
1356
|
+
};
|
|
1357
|
+
const authServiceProvider = async (_projectId) => {
|
|
1358
|
+
try {
|
|
1359
|
+
return ctx.getService("auth");
|
|
1360
|
+
} catch {
|
|
1361
|
+
return void 0;
|
|
1362
|
+
}
|
|
1363
|
+
};
|
|
1364
|
+
const objectQLProvider = async (_projectId) => {
|
|
1365
|
+
try {
|
|
1366
|
+
return ctx.getService("objectql");
|
|
1367
|
+
} catch {
|
|
1368
|
+
return void 0;
|
|
1369
|
+
}
|
|
1370
|
+
};
|
|
744
1371
|
if (!server) {
|
|
745
1372
|
ctx.logger.warn(`RestApiPlugin: HTTP Server service '${serverService}' not found. REST routes skipped.`);
|
|
746
1373
|
return;
|
|
@@ -751,13 +1378,38 @@ function createRestApiPlugin(config = {}) {
|
|
|
751
1378
|
}
|
|
752
1379
|
ctx.logger.info("Hydrating REST API from Protocol...");
|
|
753
1380
|
try {
|
|
754
|
-
const restServer = new RestServer(server, protocol, config.api);
|
|
1381
|
+
const restServer = new RestServer(server, protocol, config.api, kernelManager, envRegistry, defaultProjectIdProvider, authServiceProvider, objectQLProvider);
|
|
755
1382
|
restServer.registerRoutes();
|
|
756
1383
|
ctx.logger.info("REST API successfully registered");
|
|
757
1384
|
} catch (err) {
|
|
758
1385
|
ctx.logger.error("Failed to register REST API routes", { error: err.message });
|
|
759
1386
|
throw err;
|
|
760
1387
|
}
|
|
1388
|
+
try {
|
|
1389
|
+
const packageService = ctx.getService("package");
|
|
1390
|
+
if (packageService) {
|
|
1391
|
+
const basePath = config.api?.api?.basePath || "/api";
|
|
1392
|
+
const version = config.api?.api?.version || "v1";
|
|
1393
|
+
const versionedBase = `${basePath}/${version}`;
|
|
1394
|
+
const enableProjectScoping = config.api?.api?.enableProjectScoping ?? false;
|
|
1395
|
+
const projectResolution = config.api?.api?.projectResolution ?? "auto";
|
|
1396
|
+
if (enableProjectScoping && projectResolution === "required") {
|
|
1397
|
+
registerPackageRoutes(server, packageService, `${versionedBase}/projects/:projectId`, {
|
|
1398
|
+
protocol
|
|
1399
|
+
});
|
|
1400
|
+
} else {
|
|
1401
|
+
registerPackageRoutes(server, packageService, versionedBase, { protocol });
|
|
1402
|
+
if (enableProjectScoping) {
|
|
1403
|
+
registerPackageRoutes(server, packageService, `${versionedBase}/projects/:projectId`, {
|
|
1404
|
+
protocol
|
|
1405
|
+
});
|
|
1406
|
+
}
|
|
1407
|
+
}
|
|
1408
|
+
ctx.logger.info("Package management routes registered");
|
|
1409
|
+
}
|
|
1410
|
+
} catch (e) {
|
|
1411
|
+
ctx.logger.debug("Package service not available, package routes skipped");
|
|
1412
|
+
}
|
|
761
1413
|
}
|
|
762
1414
|
};
|
|
763
1415
|
}
|