@objectstack/runtime 3.2.0 → 3.2.2
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/.turbo/turbo-build.log +10 -10
- package/CHANGELOG.md +20 -0
- package/dist/index.cjs +156 -26
- package/dist/index.cjs.map +1 -1
- package/dist/index.d.cts +25 -1
- package/dist/index.d.ts +25 -1
- package/dist/index.js +156 -26
- package/dist/index.js.map +1 -1
- package/package.json +5 -5
- package/src/http-dispatcher.test.ts +273 -3
- package/src/http-dispatcher.ts +193 -32
package/dist/index.d.cts
CHANGED
|
@@ -465,6 +465,12 @@ declare class HttpDispatcher {
|
|
|
465
465
|
* path: sub-path after /auth/
|
|
466
466
|
*/
|
|
467
467
|
handleAuth(path: string, method: string, body: any, context: HttpProtocolContext): Promise<HttpDispatcherResult>;
|
|
468
|
+
/**
|
|
469
|
+
* Provides mock auth responses for core better-auth endpoints when
|
|
470
|
+
* AuthPlugin is not loaded (e.g. MSW/browser-only environments).
|
|
471
|
+
* This ensures registration/sign-in flows do not 404 in mock mode.
|
|
472
|
+
*/
|
|
473
|
+
private mockAuthFallback;
|
|
468
474
|
/**
|
|
469
475
|
* Handles Metadata requests
|
|
470
476
|
* Standard: /metadata/:type/:name
|
|
@@ -480,7 +486,19 @@ declare class HttpDispatcher {
|
|
|
480
486
|
* Handles Analytics requests
|
|
481
487
|
* path: sub-path after /analytics/
|
|
482
488
|
*/
|
|
483
|
-
handleAnalytics(path: string, method: string, body: any,
|
|
489
|
+
handleAnalytics(path: string, method: string, body: any, _context: HttpProtocolContext): Promise<HttpDispatcherResult>;
|
|
490
|
+
/**
|
|
491
|
+
* Handles i18n requests
|
|
492
|
+
* path: sub-path after /i18n/
|
|
493
|
+
*
|
|
494
|
+
* Routes:
|
|
495
|
+
* GET /locales → getLocales
|
|
496
|
+
* GET /translations/:locale → getTranslations (locale from path)
|
|
497
|
+
* GET /translations?locale=xx → getTranslations (locale from query)
|
|
498
|
+
* GET /labels/:object/:locale → getFieldLabels (both from path)
|
|
499
|
+
* GET /labels/:object?locale=xx → getFieldLabels (locale from query)
|
|
500
|
+
*/
|
|
501
|
+
handleI18n(path: string, method: string, query: any, _context: HttpProtocolContext): Promise<HttpDispatcherResult>;
|
|
484
502
|
/**
|
|
485
503
|
* Handles Package Management requests
|
|
486
504
|
*
|
|
@@ -530,6 +548,12 @@ declare class HttpDispatcher {
|
|
|
530
548
|
handleAutomation(path: string, method: string, body: any, context: HttpProtocolContext, query?: any): Promise<HttpDispatcherResult>;
|
|
531
549
|
private getServicesMap;
|
|
532
550
|
private getService;
|
|
551
|
+
/**
|
|
552
|
+
* Resolve any service by name, supporting async factories.
|
|
553
|
+
* Fallback chain: getServiceAsync → getService (sync) → context.getService → services map.
|
|
554
|
+
* Only returns when a non-null service is found; otherwise falls through to the next step.
|
|
555
|
+
*/
|
|
556
|
+
private resolveService;
|
|
533
557
|
/**
|
|
534
558
|
* Get the ObjectQL service which provides access to SchemaRegistry.
|
|
535
559
|
* Tries multiple access patterns since kernel structure varies.
|
package/dist/index.d.ts
CHANGED
|
@@ -465,6 +465,12 @@ declare class HttpDispatcher {
|
|
|
465
465
|
* path: sub-path after /auth/
|
|
466
466
|
*/
|
|
467
467
|
handleAuth(path: string, method: string, body: any, context: HttpProtocolContext): Promise<HttpDispatcherResult>;
|
|
468
|
+
/**
|
|
469
|
+
* Provides mock auth responses for core better-auth endpoints when
|
|
470
|
+
* AuthPlugin is not loaded (e.g. MSW/browser-only environments).
|
|
471
|
+
* This ensures registration/sign-in flows do not 404 in mock mode.
|
|
472
|
+
*/
|
|
473
|
+
private mockAuthFallback;
|
|
468
474
|
/**
|
|
469
475
|
* Handles Metadata requests
|
|
470
476
|
* Standard: /metadata/:type/:name
|
|
@@ -480,7 +486,19 @@ declare class HttpDispatcher {
|
|
|
480
486
|
* Handles Analytics requests
|
|
481
487
|
* path: sub-path after /analytics/
|
|
482
488
|
*/
|
|
483
|
-
handleAnalytics(path: string, method: string, body: any,
|
|
489
|
+
handleAnalytics(path: string, method: string, body: any, _context: HttpProtocolContext): Promise<HttpDispatcherResult>;
|
|
490
|
+
/**
|
|
491
|
+
* Handles i18n requests
|
|
492
|
+
* path: sub-path after /i18n/
|
|
493
|
+
*
|
|
494
|
+
* Routes:
|
|
495
|
+
* GET /locales → getLocales
|
|
496
|
+
* GET /translations/:locale → getTranslations (locale from path)
|
|
497
|
+
* GET /translations?locale=xx → getTranslations (locale from query)
|
|
498
|
+
* GET /labels/:object/:locale → getFieldLabels (both from path)
|
|
499
|
+
* GET /labels/:object?locale=xx → getFieldLabels (locale from query)
|
|
500
|
+
*/
|
|
501
|
+
handleI18n(path: string, method: string, query: any, _context: HttpProtocolContext): Promise<HttpDispatcherResult>;
|
|
484
502
|
/**
|
|
485
503
|
* Handles Package Management requests
|
|
486
504
|
*
|
|
@@ -530,6 +548,12 @@ declare class HttpDispatcher {
|
|
|
530
548
|
handleAutomation(path: string, method: string, body: any, context: HttpProtocolContext, query?: any): Promise<HttpDispatcherResult>;
|
|
531
549
|
private getServicesMap;
|
|
532
550
|
private getService;
|
|
551
|
+
/**
|
|
552
|
+
* Resolve any service by name, supporting async factories.
|
|
553
|
+
* Fallback chain: getServiceAsync → getService (sync) → context.getService → services map.
|
|
554
|
+
* Only returns when a non-null service is found; otherwise falls through to the next step.
|
|
555
|
+
*/
|
|
556
|
+
private resolveService;
|
|
533
557
|
/**
|
|
534
558
|
* Get the ObjectQL service which provides access to SchemaRegistry.
|
|
535
559
|
* Tries multiple access patterns since kernel structure varies.
|
package/dist/index.js
CHANGED
|
@@ -171,6 +171,16 @@ var AppPlugin = class {
|
|
|
171
171
|
// src/http-dispatcher.ts
|
|
172
172
|
import { getEnv } from "@objectstack/core";
|
|
173
173
|
import { CoreServiceName } from "@objectstack/spec/system";
|
|
174
|
+
function randomUUID() {
|
|
175
|
+
if (globalThis.crypto && typeof globalThis.crypto.randomUUID === "function") {
|
|
176
|
+
return globalThis.crypto.randomUUID();
|
|
177
|
+
}
|
|
178
|
+
return "xxxxxxxx-xxxx-4xxx-yxxx-xxxxxxxxxxxx".replace(/[xy]/g, (c) => {
|
|
179
|
+
const r = Math.random() * 16 | 0;
|
|
180
|
+
const v = c === "x" ? r : r & 3 | 8;
|
|
181
|
+
return v.toString(16);
|
|
182
|
+
});
|
|
183
|
+
}
|
|
174
184
|
var HttpDispatcher = class {
|
|
175
185
|
// Casting to any to access dynamic props like broker, services, graphql
|
|
176
186
|
constructor(kernel) {
|
|
@@ -313,9 +323,64 @@ var HttpDispatcher = class {
|
|
|
313
323
|
}
|
|
314
324
|
const normalizedPath = path.replace(/^\/+/, "");
|
|
315
325
|
if (normalizedPath === "login" && method.toUpperCase() === "POST") {
|
|
316
|
-
|
|
317
|
-
|
|
318
|
-
|
|
326
|
+
try {
|
|
327
|
+
const broker = this.ensureBroker();
|
|
328
|
+
const data = await broker.call("auth.login", body, { request: context.request });
|
|
329
|
+
return { handled: true, response: { status: 200, body: data } };
|
|
330
|
+
} catch (error) {
|
|
331
|
+
const statusCode = error?.statusCode ?? error?.status;
|
|
332
|
+
if (statusCode !== 500 || !error?.message?.includes("Broker not available")) {
|
|
333
|
+
throw error;
|
|
334
|
+
}
|
|
335
|
+
}
|
|
336
|
+
}
|
|
337
|
+
return this.mockAuthFallback(normalizedPath, method, body);
|
|
338
|
+
}
|
|
339
|
+
/**
|
|
340
|
+
* Provides mock auth responses for core better-auth endpoints when
|
|
341
|
+
* AuthPlugin is not loaded (e.g. MSW/browser-only environments).
|
|
342
|
+
* This ensures registration/sign-in flows do not 404 in mock mode.
|
|
343
|
+
*/
|
|
344
|
+
mockAuthFallback(path, method, body) {
|
|
345
|
+
const m = method.toUpperCase();
|
|
346
|
+
const MOCK_SESSION_EXPIRY_MS = 864e5;
|
|
347
|
+
if ((path === "sign-up/email" || path === "register") && m === "POST") {
|
|
348
|
+
const id = `mock_${randomUUID()}`;
|
|
349
|
+
return {
|
|
350
|
+
handled: true,
|
|
351
|
+
response: {
|
|
352
|
+
status: 200,
|
|
353
|
+
body: {
|
|
354
|
+
user: { id, name: body?.name || "Mock User", email: body?.email || "mock@test.local", emailVerified: false, createdAt: (/* @__PURE__ */ new Date()).toISOString(), updatedAt: (/* @__PURE__ */ new Date()).toISOString() },
|
|
355
|
+
session: { id: `session_${id}`, userId: id, token: `mock_token_${id}`, expiresAt: new Date(Date.now() + MOCK_SESSION_EXPIRY_MS).toISOString() }
|
|
356
|
+
}
|
|
357
|
+
}
|
|
358
|
+
};
|
|
359
|
+
}
|
|
360
|
+
if ((path === "sign-in/email" || path === "login") && m === "POST") {
|
|
361
|
+
const id = `mock_${randomUUID()}`;
|
|
362
|
+
return {
|
|
363
|
+
handled: true,
|
|
364
|
+
response: {
|
|
365
|
+
status: 200,
|
|
366
|
+
body: {
|
|
367
|
+
user: { id, name: "Mock User", email: body?.email || "mock@test.local", emailVerified: true, createdAt: (/* @__PURE__ */ new Date()).toISOString(), updatedAt: (/* @__PURE__ */ new Date()).toISOString() },
|
|
368
|
+
session: { id: `session_${id}`, userId: id, token: `mock_token_${id}`, expiresAt: new Date(Date.now() + MOCK_SESSION_EXPIRY_MS).toISOString() }
|
|
369
|
+
}
|
|
370
|
+
}
|
|
371
|
+
};
|
|
372
|
+
}
|
|
373
|
+
if (path === "get-session" && m === "GET") {
|
|
374
|
+
return {
|
|
375
|
+
handled: true,
|
|
376
|
+
response: { status: 200, body: { session: null, user: null } }
|
|
377
|
+
};
|
|
378
|
+
}
|
|
379
|
+
if (path === "sign-out" && m === "POST") {
|
|
380
|
+
return {
|
|
381
|
+
handled: true,
|
|
382
|
+
response: { status: 200, body: { success: true } }
|
|
383
|
+
};
|
|
319
384
|
}
|
|
320
385
|
return { handled: false };
|
|
321
386
|
}
|
|
@@ -328,7 +393,7 @@ var HttpDispatcher = class {
|
|
|
328
393
|
const broker = this.ensureBroker();
|
|
329
394
|
const parts = path.replace(/^\/+/, "").split("/").filter(Boolean);
|
|
330
395
|
if (parts[0] === "types") {
|
|
331
|
-
const protocol =
|
|
396
|
+
const protocol = await this.resolveService("protocol");
|
|
332
397
|
if (protocol && typeof protocol.getMetaTypes === "function") {
|
|
333
398
|
const result = await protocol.getMetaTypes({});
|
|
334
399
|
return { handled: true, response: this.success(result) };
|
|
@@ -358,7 +423,7 @@ var HttpDispatcher = class {
|
|
|
358
423
|
if (parts.length === 2) {
|
|
359
424
|
const [type, name] = parts;
|
|
360
425
|
if (method === "PUT" && body) {
|
|
361
|
-
const protocol =
|
|
426
|
+
const protocol = await this.resolveService("protocol");
|
|
362
427
|
if (protocol && typeof protocol.saveMetaItem === "function") {
|
|
363
428
|
try {
|
|
364
429
|
const result = await protocol.saveMetaItem({ type, name, item: body });
|
|
@@ -380,7 +445,7 @@ var HttpDispatcher = class {
|
|
|
380
445
|
return { handled: true, response: this.success(data2) };
|
|
381
446
|
}
|
|
382
447
|
const singularType = type.endsWith("s") ? type.slice(0, -1) : type;
|
|
383
|
-
const protocol =
|
|
448
|
+
const protocol = await this.resolveService("protocol");
|
|
384
449
|
if (protocol && typeof protocol.getMetaItem === "function") {
|
|
385
450
|
try {
|
|
386
451
|
const data2 = await protocol.getMetaItem({ type: singularType, name });
|
|
@@ -398,7 +463,7 @@ var HttpDispatcher = class {
|
|
|
398
463
|
if (parts.length === 1) {
|
|
399
464
|
const typeOrName = parts[0];
|
|
400
465
|
const packageId = query?.package || void 0;
|
|
401
|
-
const protocol =
|
|
466
|
+
const protocol = await this.resolveService("protocol");
|
|
402
467
|
if (protocol && typeof protocol.getMetaItems === "function") {
|
|
403
468
|
try {
|
|
404
469
|
const data = await protocol.getMetaItems({ type: typeOrName, packageId });
|
|
@@ -427,7 +492,7 @@ var HttpDispatcher = class {
|
|
|
427
492
|
}
|
|
428
493
|
}
|
|
429
494
|
if (parts.length === 0) {
|
|
430
|
-
const protocol =
|
|
495
|
+
const protocol = await this.resolveService("protocol");
|
|
431
496
|
if (protocol && typeof protocol.getMetaTypes === "function") {
|
|
432
497
|
const result = await protocol.getMetaTypes({});
|
|
433
498
|
return { handled: true, response: this.success(result) };
|
|
@@ -500,25 +565,72 @@ var HttpDispatcher = class {
|
|
|
500
565
|
* Handles Analytics requests
|
|
501
566
|
* path: sub-path after /analytics/
|
|
502
567
|
*/
|
|
503
|
-
async handleAnalytics(path, method, body,
|
|
568
|
+
async handleAnalytics(path, method, body, _context) {
|
|
504
569
|
const analyticsService = await this.getService(CoreServiceName.enum.analytics);
|
|
505
570
|
if (!analyticsService) return { handled: false };
|
|
506
571
|
const m = method.toUpperCase();
|
|
507
572
|
const subPath = path.replace(/^\/+/, "");
|
|
508
573
|
if (subPath === "query" && m === "POST") {
|
|
509
|
-
const result = await analyticsService.query(body
|
|
574
|
+
const result = await analyticsService.query(body);
|
|
510
575
|
return { handled: true, response: this.success(result) };
|
|
511
576
|
}
|
|
512
577
|
if (subPath === "meta" && m === "GET") {
|
|
513
|
-
const result = await analyticsService.
|
|
578
|
+
const result = await analyticsService.getMeta();
|
|
514
579
|
return { handled: true, response: this.success(result) };
|
|
515
580
|
}
|
|
516
581
|
if (subPath === "sql" && m === "POST") {
|
|
517
|
-
const result = await analyticsService.generateSql(body
|
|
582
|
+
const result = await analyticsService.generateSql(body);
|
|
518
583
|
return { handled: true, response: this.success(result) };
|
|
519
584
|
}
|
|
520
585
|
return { handled: false };
|
|
521
586
|
}
|
|
587
|
+
/**
|
|
588
|
+
* Handles i18n requests
|
|
589
|
+
* path: sub-path after /i18n/
|
|
590
|
+
*
|
|
591
|
+
* Routes:
|
|
592
|
+
* GET /locales → getLocales
|
|
593
|
+
* GET /translations/:locale → getTranslations (locale from path)
|
|
594
|
+
* GET /translations?locale=xx → getTranslations (locale from query)
|
|
595
|
+
* GET /labels/:object/:locale → getFieldLabels (both from path)
|
|
596
|
+
* GET /labels/:object?locale=xx → getFieldLabels (locale from query)
|
|
597
|
+
*/
|
|
598
|
+
async handleI18n(path, method, query, _context) {
|
|
599
|
+
const i18nService = await this.getService(CoreServiceName.enum.i18n);
|
|
600
|
+
if (!i18nService) return { handled: true, response: this.error("i18n service not available", 501) };
|
|
601
|
+
const m = method.toUpperCase();
|
|
602
|
+
const parts = path.replace(/^\/+/, "").split("/").filter(Boolean);
|
|
603
|
+
if (m !== "GET") return { handled: false };
|
|
604
|
+
if (parts[0] === "locales" && parts.length === 1) {
|
|
605
|
+
const locales = i18nService.getLocales();
|
|
606
|
+
return { handled: true, response: this.success({ locales }) };
|
|
607
|
+
}
|
|
608
|
+
if (parts[0] === "translations") {
|
|
609
|
+
const locale = parts[1] ? decodeURIComponent(parts[1]) : query?.locale;
|
|
610
|
+
if (!locale) return { handled: true, response: this.error("Missing locale parameter", 400) };
|
|
611
|
+
const translations = i18nService.getTranslations(locale);
|
|
612
|
+
return { handled: true, response: this.success({ locale, translations }) };
|
|
613
|
+
}
|
|
614
|
+
if (parts[0] === "labels" && parts.length >= 2) {
|
|
615
|
+
const objectName = decodeURIComponent(parts[1]);
|
|
616
|
+
const locale = parts[2] ? decodeURIComponent(parts[2]) : query?.locale;
|
|
617
|
+
if (!locale) return { handled: true, response: this.error("Missing locale parameter", 400) };
|
|
618
|
+
if (typeof i18nService.getFieldLabels === "function") {
|
|
619
|
+
const labels2 = i18nService.getFieldLabels(objectName, locale);
|
|
620
|
+
return { handled: true, response: this.success({ object: objectName, locale, labels: labels2 }) };
|
|
621
|
+
}
|
|
622
|
+
const translations = i18nService.getTranslations(locale);
|
|
623
|
+
const prefix = `o.${objectName}.fields.`;
|
|
624
|
+
const labels = {};
|
|
625
|
+
for (const [key, value] of Object.entries(translations)) {
|
|
626
|
+
if (key.startsWith(prefix)) {
|
|
627
|
+
labels[key.substring(prefix.length)] = value;
|
|
628
|
+
}
|
|
629
|
+
}
|
|
630
|
+
return { handled: true, response: this.success({ object: objectName, locale, labels }) };
|
|
631
|
+
}
|
|
632
|
+
return { handled: false };
|
|
633
|
+
}
|
|
522
634
|
/**
|
|
523
635
|
* Handles Package Management requests
|
|
524
636
|
*
|
|
@@ -709,7 +821,7 @@ var HttpDispatcher = class {
|
|
|
709
821
|
if (parts[0] === "view" && parts[1]) {
|
|
710
822
|
const objectName = parts[1];
|
|
711
823
|
const type = parts[2] || query?.type || "list";
|
|
712
|
-
const protocol =
|
|
824
|
+
const protocol = await this.resolveService("protocol");
|
|
713
825
|
if (protocol && typeof protocol.getUiView === "function") {
|
|
714
826
|
try {
|
|
715
827
|
const result = await protocol.getUiView({ object: objectName, type });
|
|
@@ -823,33 +935,48 @@ var HttpDispatcher = class {
|
|
|
823
935
|
return this.kernel.services || {};
|
|
824
936
|
}
|
|
825
937
|
async getService(name) {
|
|
826
|
-
|
|
827
|
-
return await this.kernel.getService(name);
|
|
828
|
-
}
|
|
829
|
-
const services = this.getServicesMap();
|
|
830
|
-
return services[name];
|
|
938
|
+
return this.resolveService(name);
|
|
831
939
|
}
|
|
832
940
|
/**
|
|
833
|
-
*
|
|
834
|
-
*
|
|
941
|
+
* Resolve any service by name, supporting async factories.
|
|
942
|
+
* Fallback chain: getServiceAsync → getService (sync) → context.getService → services map.
|
|
943
|
+
* Only returns when a non-null service is found; otherwise falls through to the next step.
|
|
835
944
|
*/
|
|
836
|
-
async
|
|
945
|
+
async resolveService(name) {
|
|
946
|
+
if (typeof this.kernel.getServiceAsync === "function") {
|
|
947
|
+
try {
|
|
948
|
+
const svc = await this.kernel.getServiceAsync(name);
|
|
949
|
+
if (svc != null) return svc;
|
|
950
|
+
} catch {
|
|
951
|
+
}
|
|
952
|
+
}
|
|
837
953
|
if (typeof this.kernel.getService === "function") {
|
|
838
954
|
try {
|
|
839
|
-
const svc = await this.kernel.getService(
|
|
840
|
-
if (svc
|
|
955
|
+
const svc = await this.kernel.getService(name);
|
|
956
|
+
if (svc != null) return svc;
|
|
841
957
|
} catch {
|
|
842
958
|
}
|
|
843
959
|
}
|
|
844
960
|
if (this.kernel?.context?.getService) {
|
|
845
961
|
try {
|
|
846
|
-
const svc = await this.kernel.context.getService(
|
|
847
|
-
if (svc
|
|
962
|
+
const svc = await this.kernel.context.getService(name);
|
|
963
|
+
if (svc != null) return svc;
|
|
848
964
|
} catch {
|
|
849
965
|
}
|
|
850
966
|
}
|
|
851
967
|
const services = this.getServicesMap();
|
|
852
|
-
|
|
968
|
+
return services[name];
|
|
969
|
+
}
|
|
970
|
+
/**
|
|
971
|
+
* Get the ObjectQL service which provides access to SchemaRegistry.
|
|
972
|
+
* Tries multiple access patterns since kernel structure varies.
|
|
973
|
+
*/
|
|
974
|
+
async getObjectQLService() {
|
|
975
|
+
try {
|
|
976
|
+
const svc = await this.resolveService("objectql");
|
|
977
|
+
if (svc?.registry) return svc;
|
|
978
|
+
} catch {
|
|
979
|
+
}
|
|
853
980
|
return null;
|
|
854
981
|
}
|
|
855
982
|
capitalize(s) {
|
|
@@ -895,6 +1022,9 @@ var HttpDispatcher = class {
|
|
|
895
1022
|
if (cleanPath.startsWith("/packages")) {
|
|
896
1023
|
return this.handlePackages(cleanPath.substring(9), method, body, query, context);
|
|
897
1024
|
}
|
|
1025
|
+
if (cleanPath.startsWith("/i18n")) {
|
|
1026
|
+
return this.handleI18n(cleanPath.substring(5), method, query, context);
|
|
1027
|
+
}
|
|
898
1028
|
if (cleanPath === "/openapi.json" && method === "GET") {
|
|
899
1029
|
const broker = this.ensureBroker();
|
|
900
1030
|
try {
|