@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.
@@ -1,5 +1,5 @@
1
1
 
2
- > @objectstack/runtime@3.2.5 build /home/runner/work/spec/spec/packages/runtime
2
+ > @objectstack/runtime@3.2.7 build /home/runner/work/spec/spec/packages/runtime
3
3
  > tsup --config ../../tsup.config.ts
4
4
 
5
5
  CLI Building entry: src/index.ts
@@ -10,13 +10,13 @@
10
10
  CLI Cleaning output folder
11
11
  ESM Build start
12
12
  CJS Build start
13
- CJS dist/index.cjs 82.74 KB
14
- CJS dist/index.cjs.map 172.94 KB
15
- CJS ⚡️ Build success in 219ms
16
- ESM dist/index.js 80.18 KB
17
- ESM dist/index.js.map 172.87 KB
18
- ESM ⚡️ Build success in 221ms
13
+ CJS dist/index.cjs 88.26 KB
14
+ CJS dist/index.cjs.map 184.29 KB
15
+ CJS ⚡️ Build success in 192ms
16
+ ESM dist/index.js 85.68 KB
17
+ ESM dist/index.js.map 184.22 KB
18
+ ESM ⚡️ Build success in 226ms
19
19
  DTS Build start
20
- DTS ⚡️ Build success in 6647ms
21
- DTS dist/index.d.ts 23.61 KB
22
- DTS dist/index.d.cts 23.61 KB
20
+ DTS ⚡️ Build success in 7678ms
21
+ DTS dist/index.d.ts 24.35 KB
22
+ DTS dist/index.d.cts 24.35 KB
package/CHANGELOG.md CHANGED
@@ -1,5 +1,23 @@
1
1
  # @objectstack/runtime
2
2
 
3
+ ## 3.2.7
4
+
5
+ ### Patch Changes
6
+
7
+ - @objectstack/spec@3.2.7
8
+ - @objectstack/core@3.2.7
9
+ - @objectstack/types@3.2.7
10
+ - @objectstack/rest@3.2.7
11
+
12
+ ## 3.2.6
13
+
14
+ ### Patch Changes
15
+
16
+ - @objectstack/spec@3.2.6
17
+ - @objectstack/core@3.2.6
18
+ - @objectstack/types@3.2.6
19
+ - @objectstack/rest@3.2.6
20
+
3
21
  ## 3.2.5
4
22
 
5
23
  ### Patch Changes
package/dist/index.cjs CHANGED
@@ -123,6 +123,7 @@ var DriverPlugin = class {
123
123
  };
124
124
 
125
125
  // src/seed-loader.ts
126
+ var import_data = require("@objectstack/spec/data");
126
127
  var DEFAULT_EXTERNAL_ID_FIELD = "name";
127
128
  var SeedLoaderService = class {
128
129
  constructor(engine, metadata, logger) {
@@ -207,8 +208,7 @@ var SeedLoaderService = class {
207
208
  return { nodes, insertOrder, circularDependencies };
208
209
  }
209
210
  async validate(datasets, config) {
210
- const { SeedLoaderConfigSchema } = await import("@objectstack/spec/data");
211
- const parsedConfig = SeedLoaderConfigSchema.parse({ ...config, dryRun: true });
211
+ const parsedConfig = import_data.SeedLoaderConfigSchema.parse({ ...config, dryRun: true });
212
212
  return this.load({ datasets, config: parsedConfig });
213
213
  }
214
214
  // ==========================================================================
@@ -653,7 +653,11 @@ var AppPlugin = class {
653
653
  this.start = async (ctx) => {
654
654
  const sys = this.bundle.manifest || this.bundle;
655
655
  const appId = sys.id || sys.name;
656
- const ql = ctx.getService("objectql");
656
+ let ql;
657
+ try {
658
+ ql = ctx.getService("objectql");
659
+ } catch {
660
+ }
657
661
  if (!ql) {
658
662
  ctx.logger.warn("ObjectQL engine service not found", {
659
663
  appName: this.name,
@@ -687,6 +691,7 @@ var AppPlugin = class {
687
691
  } else {
688
692
  ctx.logger.debug("No runtime.onEnable function found", { appId });
689
693
  }
694
+ this.loadTranslations(ctx, appId);
690
695
  const seedDatasets = [];
691
696
  if (Array.isArray(this.bundle.data)) {
692
697
  seedDatasets.push(...this.bundle.data);
@@ -757,6 +762,68 @@ var AppPlugin = class {
757
762
  this.name = `plugin.app.${appId}`;
758
763
  this.version = sys.version;
759
764
  }
765
+ /**
766
+ * Auto-load i18n translation bundles from the app config into the
767
+ * kernel's i18n service. Handles both `translations` (array of
768
+ * TranslationBundle) and `i18n` config (default locale, etc.).
769
+ *
770
+ * Gracefully skips when the i18n service is not registered —
771
+ * this keeps AppPlugin resilient across server/dev/mock environments.
772
+ */
773
+ loadTranslations(ctx, appId) {
774
+ let i18nService;
775
+ try {
776
+ i18nService = ctx.getService("i18n");
777
+ } catch {
778
+ }
779
+ const bundles = [];
780
+ if (Array.isArray(this.bundle.translations)) {
781
+ bundles.push(...this.bundle.translations);
782
+ }
783
+ const manifest = this.bundle.manifest || this.bundle;
784
+ if (manifest && Array.isArray(manifest.translations) && manifest.translations !== this.bundle.translations) {
785
+ bundles.push(...manifest.translations);
786
+ }
787
+ if (!i18nService) {
788
+ if (bundles.length > 0) {
789
+ ctx.logger.warn(
790
+ `[i18n] App "${appId}" has ${bundles.length} translation bundle(s) but no i18n service is registered. Translations will not be served via REST API. Register I18nServicePlugin from @objectstack/service-i18n, or use DevPlugin which auto-detects translations and registers the i18n service automatically.`
791
+ );
792
+ } else {
793
+ ctx.logger.debug("[i18n] No i18n service registered; skipping translation loading", { appId });
794
+ }
795
+ return;
796
+ }
797
+ const i18nConfig = this.bundle.i18n || (this.bundle.manifest || this.bundle)?.i18n;
798
+ if (i18nConfig?.defaultLocale && typeof i18nService.setDefaultLocale === "function") {
799
+ i18nService.setDefaultLocale(i18nConfig.defaultLocale);
800
+ ctx.logger.debug("[i18n] Set default locale", { appId, locale: i18nConfig.defaultLocale });
801
+ }
802
+ if (bundles.length === 0) {
803
+ return;
804
+ }
805
+ let loadedLocales = 0;
806
+ for (const bundle of bundles) {
807
+ for (const [locale, data] of Object.entries(bundle)) {
808
+ if (data && typeof data === "object") {
809
+ try {
810
+ i18nService.loadTranslations(locale, data);
811
+ loadedLocales++;
812
+ } catch (err) {
813
+ ctx.logger.warn("[i18n] Failed to load translations", { appId, locale, error: err.message });
814
+ }
815
+ }
816
+ }
817
+ }
818
+ const svcAny = i18nService;
819
+ if (svcAny._fallback || svcAny._dev) {
820
+ ctx.logger.info(
821
+ `[i18n] Loaded ${loadedLocales} locale(s) into in-memory i18n fallback for "${appId}". For production, consider registering I18nServicePlugin from @objectstack/service-i18n.`
822
+ );
823
+ } else {
824
+ ctx.logger.info("[i18n] Loaded translation bundles", { appId, bundles: bundles.length, locales: loadedLocales });
825
+ }
826
+ }
760
827
  };
761
828
 
762
829
  // src/http-dispatcher.ts
@@ -796,25 +863,61 @@ var HttpDispatcher = class {
796
863
  return this.kernel.broker;
797
864
  }
798
865
  /**
799
- * Generates the discovery JSON response for the API root
866
+ * Generates the discovery JSON response for the API root.
867
+ *
868
+ * Uses the same async `resolveService()` fallback chain that request
869
+ * handlers use, so the reported service status is always consistent
870
+ * with the actual runtime availability.
800
871
  */
801
- getDiscoveryInfo(prefix) {
802
- const services = this.getServicesMap();
803
- const hasAuth = !!services[import_system.CoreServiceName.enum.auth];
804
- const hasGraphQL = !!(services[import_system.CoreServiceName.enum.graphql] || this.kernel.graphql);
805
- const hasSearch = !!services[import_system.CoreServiceName.enum.search];
806
- const hasWebSockets = !!services[import_system.CoreServiceName.enum.realtime];
807
- const hasFiles = !!(services[import_system.CoreServiceName.enum["file-storage"]] || services["storage"]?.supportsFiles);
808
- const hasAnalytics = !!services[import_system.CoreServiceName.enum.analytics];
809
- const hasWorkflow = !!services[import_system.CoreServiceName.enum.workflow];
810
- const hasAi = !!services[import_system.CoreServiceName.enum.ai];
811
- const hasNotification = !!services[import_system.CoreServiceName.enum.notification];
812
- const hasI18n = !!services[import_system.CoreServiceName.enum.i18n];
813
- const hasUi = !!services[import_system.CoreServiceName.enum.ui];
814
- const hasAutomation = !!services[import_system.CoreServiceName.enum.automation];
815
- const hasCache = !!services[import_system.CoreServiceName.enum.cache];
816
- const hasQueue = !!services[import_system.CoreServiceName.enum.queue];
817
- const hasJob = !!services[import_system.CoreServiceName.enum.job];
872
+ async getDiscoveryInfo(prefix) {
873
+ const [
874
+ authSvc,
875
+ graphqlSvc,
876
+ searchSvc,
877
+ realtimeSvc,
878
+ filesSvc,
879
+ analyticsSvc,
880
+ workflowSvc,
881
+ aiSvc,
882
+ notificationSvc,
883
+ i18nSvc,
884
+ uiSvc,
885
+ automationSvc,
886
+ cacheSvc,
887
+ queueSvc,
888
+ jobSvc
889
+ ] = await Promise.all([
890
+ this.resolveService(import_system.CoreServiceName.enum.auth),
891
+ this.resolveService(import_system.CoreServiceName.enum.graphql),
892
+ this.resolveService(import_system.CoreServiceName.enum.search),
893
+ this.resolveService(import_system.CoreServiceName.enum.realtime),
894
+ this.resolveService(import_system.CoreServiceName.enum["file-storage"]),
895
+ this.resolveService(import_system.CoreServiceName.enum.analytics),
896
+ this.resolveService(import_system.CoreServiceName.enum.workflow),
897
+ this.resolveService(import_system.CoreServiceName.enum.ai),
898
+ this.resolveService(import_system.CoreServiceName.enum.notification),
899
+ this.resolveService(import_system.CoreServiceName.enum.i18n),
900
+ this.resolveService(import_system.CoreServiceName.enum.ui),
901
+ this.resolveService(import_system.CoreServiceName.enum.automation),
902
+ this.resolveService(import_system.CoreServiceName.enum.cache),
903
+ this.resolveService(import_system.CoreServiceName.enum.queue),
904
+ this.resolveService(import_system.CoreServiceName.enum.job)
905
+ ]);
906
+ const hasAuth = !!authSvc;
907
+ const hasGraphQL = !!(graphqlSvc || this.kernel.graphql);
908
+ const hasSearch = !!searchSvc;
909
+ const hasWebSockets = !!realtimeSvc;
910
+ const hasFiles = !!filesSvc;
911
+ const hasAnalytics = !!analyticsSvc;
912
+ const hasWorkflow = !!workflowSvc;
913
+ const hasAi = !!aiSvc;
914
+ const hasNotification = !!notificationSvc;
915
+ const hasI18n = !!i18nSvc;
916
+ const hasUi = !!uiSvc;
917
+ const hasAutomation = !!automationSvc;
918
+ const hasCache = !!cacheSvc;
919
+ const hasQueue = !!queueSvc;
920
+ const hasJob = !!jobSvc;
818
921
  const routes = {
819
922
  data: `${prefix}/data`,
820
923
  metadata: `${prefix}/meta`,
@@ -842,6 +945,16 @@ var HttpDispatcher = class {
842
945
  status: "unavailable",
843
946
  message: `Install a ${name} plugin to enable`
844
947
  });
948
+ let locale = { default: "en", supported: ["en"], timezone: "UTC" };
949
+ if (hasI18n && i18nSvc) {
950
+ const defaultLocale = typeof i18nSvc.getDefaultLocale === "function" ? i18nSvc.getDefaultLocale() : "en";
951
+ const locales = typeof i18nSvc.getLocales === "function" ? i18nSvc.getLocales() : [];
952
+ locale = {
953
+ default: defaultLocale,
954
+ supported: locales.length > 0 ? locales : [defaultLocale],
955
+ timezone: "UTC"
956
+ };
957
+ }
845
958
  return {
846
959
  name: "ObjectOS",
847
960
  version: "1.0.0",
@@ -881,11 +994,7 @@ var HttpDispatcher = class {
881
994
  "file-storage": hasFiles ? svcAvailable(routes.storage) : svcUnavailable("file-storage"),
882
995
  search: hasSearch ? svcAvailable() : svcUnavailable("search")
883
996
  },
884
- locale: {
885
- default: "en",
886
- supported: ["en", "zh-CN"],
887
- timezone: "UTC"
888
- }
997
+ locale
889
998
  };
890
999
  }
891
1000
  /**
@@ -1199,13 +1308,24 @@ var HttpDispatcher = class {
1199
1308
  if (parts[0] === "translations") {
1200
1309
  const locale = parts[1] ? decodeURIComponent(parts[1]) : query?.locale;
1201
1310
  if (!locale) return { handled: true, response: this.error("Missing locale parameter", 400) };
1202
- const translations = i18nService.getTranslations(locale);
1311
+ let translations = i18nService.getTranslations(locale);
1312
+ if (Object.keys(translations).length === 0) {
1313
+ const availableLocales = typeof i18nService.getLocales === "function" ? i18nService.getLocales() : [];
1314
+ const resolved = (0, import_core2.resolveLocale)(locale, availableLocales);
1315
+ if (resolved && resolved !== locale) {
1316
+ translations = i18nService.getTranslations(resolved);
1317
+ return { handled: true, response: this.success({ locale: resolved, requestedLocale: locale, translations }) };
1318
+ }
1319
+ }
1203
1320
  return { handled: true, response: this.success({ locale, translations }) };
1204
1321
  }
1205
1322
  if (parts[0] === "labels" && parts.length >= 2) {
1206
1323
  const objectName = decodeURIComponent(parts[1]);
1207
- const locale = parts[2] ? decodeURIComponent(parts[2]) : query?.locale;
1324
+ let locale = parts[2] ? decodeURIComponent(parts[2]) : query?.locale;
1208
1325
  if (!locale) return { handled: true, response: this.error("Missing locale parameter", 400) };
1326
+ const availableLocales = typeof i18nService.getLocales === "function" ? i18nService.getLocales() : [];
1327
+ const resolved = (0, import_core2.resolveLocale)(locale, availableLocales);
1328
+ if (resolved) locale = resolved;
1209
1329
  if (typeof i18nService.getFieldLabels === "function") {
1210
1330
  const labels2 = i18nService.getFieldLabels(objectName, locale);
1211
1331
  return { handled: true, response: this.success({ object: objectName, locale, labels: labels2 }) };
@@ -1580,7 +1700,7 @@ var HttpDispatcher = class {
1580
1700
  async dispatch(method, path, body, query, context) {
1581
1701
  const cleanPath = path.replace(/\/$/, "");
1582
1702
  if (cleanPath === "" && method === "GET") {
1583
- const info = this.getDiscoveryInfo("");
1703
+ const info = await this.getDiscoveryInfo("");
1584
1704
  return {
1585
1705
  handled: true,
1586
1706
  response: this.success(info)
@@ -1728,10 +1848,10 @@ function createDispatcherPlugin(config = {}) {
1728
1848
  const dispatcher = new HttpDispatcher(kernel);
1729
1849
  const prefix = config.prefix || "/api/v1";
1730
1850
  server.get("/.well-known/objectstack", async (_req, res) => {
1731
- res.json({ data: dispatcher.getDiscoveryInfo(prefix) });
1851
+ res.json({ data: await dispatcher.getDiscoveryInfo(prefix) });
1732
1852
  });
1733
1853
  server.get(`${prefix}/discovery`, async (_req, res) => {
1734
- res.json({ data: dispatcher.getDiscoveryInfo(prefix) });
1854
+ res.json({ data: await dispatcher.getDiscoveryInfo(prefix) });
1735
1855
  });
1736
1856
  server.post(`${prefix}/auth/login`, async (req, res) => {
1737
1857
  try {
@@ -1853,6 +1973,30 @@ function createDispatcherPlugin(config = {}) {
1853
1973
  errorResponse(err, res);
1854
1974
  }
1855
1975
  });
1976
+ server.get(`${prefix}/i18n/locales`, async (req, res) => {
1977
+ try {
1978
+ const result = await dispatcher.handleI18n("/locales", "GET", req.query, { request: req });
1979
+ sendResult(result, res);
1980
+ } catch (err) {
1981
+ errorResponse(err, res);
1982
+ }
1983
+ });
1984
+ server.get(`${prefix}/i18n/translations/:locale`, async (req, res) => {
1985
+ try {
1986
+ const result = await dispatcher.handleI18n(`/translations/${req.params.locale}`, "GET", req.query, { request: req });
1987
+ sendResult(result, res);
1988
+ } catch (err) {
1989
+ errorResponse(err, res);
1990
+ }
1991
+ });
1992
+ server.get(`${prefix}/i18n/labels/:object/:locale`, async (req, res) => {
1993
+ try {
1994
+ const result = await dispatcher.handleI18n(`/labels/${req.params.object}/${req.params.locale}`, "GET", req.query, { request: req });
1995
+ sendResult(result, res);
1996
+ } catch (err) {
1997
+ errorResponse(err, res);
1998
+ }
1999
+ });
1856
2000
  server.get(`${prefix}/automation`, async (req, res) => {
1857
2001
  try {
1858
2002
  const result = await dispatcher.handleAutomation("", "GET", {}, { request: req });