@objectstack/runtime 3.2.5 → 3.2.7
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 +18 -0
- package/dist/index.cjs +175 -31
- package/dist/index.cjs.map +1 -1
- package/dist/index.d.cts +18 -4
- package/dist/index.d.ts +18 -4
- package/dist/index.js +175 -31
- package/dist/index.js.map +1 -1
- package/package.json +5 -5
- package/src/app-plugin.test.ts +167 -0
- package/src/app-plugin.ts +96 -4
- package/src/dispatcher-plugin.ts +33 -3
- package/src/http-dispatcher.test.ts +254 -0
- package/src/http-dispatcher.ts +85 -29
- package/src/seed-loader.ts +1 -1
package/src/http-dispatcher.ts
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
// Copyright (c) 2025 ObjectStack. Licensed under the Apache-2.0 license.
|
|
2
2
|
|
|
3
|
-
import { ObjectKernel, getEnv } from '@objectstack/core';
|
|
3
|
+
import { ObjectKernel, getEnv, resolveLocale } from '@objectstack/core';
|
|
4
4
|
import { CoreServiceName } from '@objectstack/spec/system';
|
|
5
5
|
|
|
6
6
|
/** Browser-safe UUID generator — prefers Web Crypto, falls back to RFC 4122 v4 */
|
|
@@ -67,27 +67,52 @@ export class HttpDispatcher {
|
|
|
67
67
|
}
|
|
68
68
|
|
|
69
69
|
/**
|
|
70
|
-
* Generates the discovery JSON response for the API root
|
|
70
|
+
* Generates the discovery JSON response for the API root.
|
|
71
|
+
*
|
|
72
|
+
* Uses the same async `resolveService()` fallback chain that request
|
|
73
|
+
* handlers use, so the reported service status is always consistent
|
|
74
|
+
* with the actual runtime availability.
|
|
71
75
|
*/
|
|
72
|
-
getDiscoveryInfo(prefix: string) {
|
|
73
|
-
|
|
74
|
-
|
|
75
|
-
|
|
76
|
-
|
|
77
|
-
|
|
78
|
-
|
|
79
|
-
|
|
80
|
-
|
|
81
|
-
|
|
82
|
-
|
|
83
|
-
|
|
84
|
-
|
|
85
|
-
|
|
86
|
-
|
|
87
|
-
|
|
88
|
-
|
|
89
|
-
|
|
90
|
-
|
|
76
|
+
async getDiscoveryInfo(prefix: string) {
|
|
77
|
+
// Resolve all services through the same async fallback chain
|
|
78
|
+
// that request handlers (handleI18n, handleAuth, …) use.
|
|
79
|
+
const [
|
|
80
|
+
authSvc, graphqlSvc, searchSvc, realtimeSvc, filesSvc,
|
|
81
|
+
analyticsSvc, workflowSvc, aiSvc, notificationSvc, i18nSvc,
|
|
82
|
+
uiSvc, automationSvc, cacheSvc, queueSvc, jobSvc,
|
|
83
|
+
] = await Promise.all([
|
|
84
|
+
this.resolveService(CoreServiceName.enum.auth),
|
|
85
|
+
this.resolveService(CoreServiceName.enum.graphql),
|
|
86
|
+
this.resolveService(CoreServiceName.enum.search),
|
|
87
|
+
this.resolveService(CoreServiceName.enum.realtime),
|
|
88
|
+
this.resolveService(CoreServiceName.enum['file-storage']),
|
|
89
|
+
this.resolveService(CoreServiceName.enum.analytics),
|
|
90
|
+
this.resolveService(CoreServiceName.enum.workflow),
|
|
91
|
+
this.resolveService(CoreServiceName.enum.ai),
|
|
92
|
+
this.resolveService(CoreServiceName.enum.notification),
|
|
93
|
+
this.resolveService(CoreServiceName.enum.i18n),
|
|
94
|
+
this.resolveService(CoreServiceName.enum.ui),
|
|
95
|
+
this.resolveService(CoreServiceName.enum.automation),
|
|
96
|
+
this.resolveService(CoreServiceName.enum.cache),
|
|
97
|
+
this.resolveService(CoreServiceName.enum.queue),
|
|
98
|
+
this.resolveService(CoreServiceName.enum.job),
|
|
99
|
+
]);
|
|
100
|
+
|
|
101
|
+
const hasAuth = !!authSvc;
|
|
102
|
+
const hasGraphQL = !!(graphqlSvc || this.kernel.graphql);
|
|
103
|
+
const hasSearch = !!searchSvc;
|
|
104
|
+
const hasWebSockets = !!realtimeSvc;
|
|
105
|
+
const hasFiles = !!filesSvc;
|
|
106
|
+
const hasAnalytics = !!analyticsSvc;
|
|
107
|
+
const hasWorkflow = !!workflowSvc;
|
|
108
|
+
const hasAi = !!aiSvc;
|
|
109
|
+
const hasNotification = !!notificationSvc;
|
|
110
|
+
const hasI18n = !!i18nSvc;
|
|
111
|
+
const hasUi = !!uiSvc;
|
|
112
|
+
const hasAutomation = !!automationSvc;
|
|
113
|
+
const hasCache = !!cacheSvc;
|
|
114
|
+
const hasQueue = !!queueSvc;
|
|
115
|
+
const hasJob = !!jobSvc;
|
|
91
116
|
|
|
92
117
|
// Routes are only exposed when a plugin provides the service
|
|
93
118
|
const routes = {
|
|
@@ -116,6 +141,20 @@ export class HttpDispatcher {
|
|
|
116
141
|
message: `Install a ${name} plugin to enable`,
|
|
117
142
|
});
|
|
118
143
|
|
|
144
|
+
// Derive locale info from actual i18n service when available
|
|
145
|
+
let locale = { default: 'en', supported: ['en'], timezone: 'UTC' };
|
|
146
|
+
if (hasI18n && i18nSvc) {
|
|
147
|
+
const defaultLocale = typeof i18nSvc.getDefaultLocale === 'function'
|
|
148
|
+
? i18nSvc.getDefaultLocale() : 'en';
|
|
149
|
+
const locales = typeof i18nSvc.getLocales === 'function'
|
|
150
|
+
? i18nSvc.getLocales() : [];
|
|
151
|
+
locale = {
|
|
152
|
+
default: defaultLocale,
|
|
153
|
+
supported: locales.length > 0 ? locales : [defaultLocale],
|
|
154
|
+
timezone: 'UTC',
|
|
155
|
+
};
|
|
156
|
+
}
|
|
157
|
+
|
|
119
158
|
return {
|
|
120
159
|
name: 'ObjectOS',
|
|
121
160
|
version: '1.0.0',
|
|
@@ -154,11 +193,7 @@ export class HttpDispatcher {
|
|
|
154
193
|
'file-storage': hasFiles ? svcAvailable(routes.storage) : svcUnavailable('file-storage'),
|
|
155
194
|
search: hasSearch ? svcAvailable() : svcUnavailable('search'),
|
|
156
195
|
},
|
|
157
|
-
locale
|
|
158
|
-
default: 'en',
|
|
159
|
-
supported: ['en', 'zh-CN'],
|
|
160
|
-
timezone: 'UTC'
|
|
161
|
-
}
|
|
196
|
+
locale,
|
|
162
197
|
};
|
|
163
198
|
}
|
|
164
199
|
|
|
@@ -585,15 +620,36 @@ export class HttpDispatcher {
|
|
|
585
620
|
if (parts[0] === 'translations') {
|
|
586
621
|
const locale = parts[1] ? decodeURIComponent(parts[1]) : query?.locale;
|
|
587
622
|
if (!locale) return { handled: true, response: this.error('Missing locale parameter', 400) };
|
|
588
|
-
|
|
623
|
+
|
|
624
|
+
let translations = i18nService.getTranslations(locale);
|
|
625
|
+
|
|
626
|
+
// Locale fallback: try resolving to an available locale when
|
|
627
|
+
// the exact code yields empty translations (e.g. zh → zh-CN).
|
|
628
|
+
if (Object.keys(translations).length === 0) {
|
|
629
|
+
const availableLocales = typeof i18nService.getLocales === 'function'
|
|
630
|
+
? i18nService.getLocales() : [];
|
|
631
|
+
const resolved = resolveLocale(locale, availableLocales);
|
|
632
|
+
if (resolved && resolved !== locale) {
|
|
633
|
+
translations = i18nService.getTranslations(resolved);
|
|
634
|
+
return { handled: true, response: this.success({ locale: resolved, requestedLocale: locale, translations }) };
|
|
635
|
+
}
|
|
636
|
+
}
|
|
637
|
+
|
|
589
638
|
return { handled: true, response: this.success({ locale, translations }) };
|
|
590
639
|
}
|
|
591
640
|
|
|
592
641
|
// GET /i18n/labels/:object/:locale OR /i18n/labels/:object?locale=xx
|
|
593
642
|
if (parts[0] === 'labels' && parts.length >= 2) {
|
|
594
643
|
const objectName = decodeURIComponent(parts[1]);
|
|
595
|
-
|
|
644
|
+
let locale = parts[2] ? decodeURIComponent(parts[2]) : query?.locale;
|
|
596
645
|
if (!locale) return { handled: true, response: this.error('Missing locale parameter', 400) };
|
|
646
|
+
|
|
647
|
+
// Locale fallback for labels endpoint
|
|
648
|
+
const availableLocales = typeof i18nService.getLocales === 'function'
|
|
649
|
+
? i18nService.getLocales() : [];
|
|
650
|
+
const resolved = resolveLocale(locale, availableLocales);
|
|
651
|
+
if (resolved) locale = resolved;
|
|
652
|
+
|
|
597
653
|
if (typeof i18nService.getFieldLabels === 'function') {
|
|
598
654
|
const labels = i18nService.getFieldLabels(objectName, locale);
|
|
599
655
|
return { handled: true, response: this.success({ object: objectName, locale, labels }) };
|
|
@@ -1056,7 +1112,7 @@ export class HttpDispatcher {
|
|
|
1056
1112
|
// Handles request to base URL (e.g. /api/v1) which MSW strips to empty string
|
|
1057
1113
|
if (cleanPath === '' && method === 'GET') {
|
|
1058
1114
|
// We use '' as prefix since we are internal dispatcher
|
|
1059
|
-
const info = this.getDiscoveryInfo('');
|
|
1115
|
+
const info = await this.getDiscoveryInfo('');
|
|
1060
1116
|
return {
|
|
1061
1117
|
handled: true,
|
|
1062
1118
|
response: this.success(info)
|
package/src/seed-loader.ts
CHANGED
|
@@ -13,6 +13,7 @@ import type {
|
|
|
13
13
|
DatasetLoadResult,
|
|
14
14
|
Dataset,
|
|
15
15
|
} from '@objectstack/spec/data';
|
|
16
|
+
import { SeedLoaderConfigSchema } from '@objectstack/spec/data';
|
|
16
17
|
|
|
17
18
|
interface Logger {
|
|
18
19
|
info(message: string, meta?: Record<string, any>): void;
|
|
@@ -152,7 +153,6 @@ export class SeedLoaderService implements ISeedLoaderService {
|
|
|
152
153
|
}
|
|
153
154
|
|
|
154
155
|
async validate(datasets: Dataset[], config?: SeedLoaderConfigInput): Promise<SeedLoaderResult> {
|
|
155
|
-
const { SeedLoaderConfigSchema } = await import('@objectstack/spec/data');
|
|
156
156
|
const parsedConfig = SeedLoaderConfigSchema.parse({ ...config, dryRun: true });
|
|
157
157
|
return this.load({ datasets, config: parsedConfig });
|
|
158
158
|
}
|