@objectstack/runtime 9.11.0 → 10.2.0

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/dist/index.cjs CHANGED
@@ -987,6 +987,65 @@ var init_app_plugin = __esm({
987
987
  error: err?.message ?? String(err)
988
988
  });
989
989
  }
990
+ try {
991
+ const dsDefs = this.bundle.datasources;
992
+ const dsList = Array.isArray(dsDefs) ? dsDefs : dsDefs && typeof dsDefs === "object" ? Object.entries(dsDefs).map(([name, def]) => ({ name, ...def })) : [];
993
+ if (dsList.length > 0) {
994
+ let connection;
995
+ try {
996
+ connection = ctx.getService("datasource-connection");
997
+ } catch {
998
+ connection = void 0;
999
+ }
1000
+ if (typeof connection?.connectDeclared === "function") {
1001
+ const objects = Array.isArray(this.bundle.objects) ? this.bundle.objects : [];
1002
+ const results = await connection.connectDeclared({ datasources: dsList, objects });
1003
+ const connected = results.filter((r) => r.status === "connected");
1004
+ if (connected.length > 0) {
1005
+ ctx.logger.info("Auto-connected declared datasources", {
1006
+ appId,
1007
+ connected: connected.map((r) => r.name)
1008
+ });
1009
+ }
1010
+ } else {
1011
+ ctx.logger.debug("No datasource-connection service \u2014 declared datasources stay metadata-only", { appId });
1012
+ }
1013
+ }
1014
+ } catch (err) {
1015
+ ctx.logger.error(
1016
+ `[AppPlugin] declared-datasource auto-connect failed for app '${appId}': ${err?.message ?? String(err)}`
1017
+ );
1018
+ throw err;
1019
+ }
1020
+ try {
1021
+ const metadata = ctx.getService("metadata");
1022
+ if (typeof metadata?.registerInMemory === "function") {
1023
+ const securityBundle = this.bundle.manifest ? { ...this.bundle.manifest, ...this.bundle } : this.bundle;
1024
+ const SECURITY_FIELDS = [
1025
+ ["roles", "role"],
1026
+ ["permissions", "permission"],
1027
+ ["sharingRules", "sharing_rule"],
1028
+ ["policies", "policy"]
1029
+ ];
1030
+ let count = 0;
1031
+ for (const [field, type] of SECURITY_FIELDS) {
1032
+ const arr = securityBundle?.[field];
1033
+ if (!Array.isArray(arr)) continue;
1034
+ for (const item of arr) {
1035
+ if (!item?.name) continue;
1036
+ metadata.registerInMemory(type, item.name, item);
1037
+ count += 1;
1038
+ }
1039
+ }
1040
+ if (count > 0) {
1041
+ ctx.logger.info("Registered stack-declared security metadata", { appId, count });
1042
+ }
1043
+ }
1044
+ } catch (err) {
1045
+ ctx.logger.warn("[AppPlugin] failed to register security metadata", {
1046
+ error: err?.message ?? String(err)
1047
+ });
1048
+ }
990
1049
  const stackBundle = this.bundle.default || this.bundle;
991
1050
  const runtime = stackBundle && typeof stackBundle.onEnable === "function" ? stackBundle : this.bundle;
992
1051
  if (runtime && typeof runtime.onEnable === "function") {
@@ -1520,55 +1579,55 @@ async function createStandaloneStack(config) {
1520
1579
  const explicitDriver = cfg.databaseDriver ?? process.env.OS_DATABASE_DRIVER?.trim();
1521
1580
  const dbDriver = explicitDriver ?? detectDriverFromUrl(dbUrl);
1522
1581
  let driverPlugin;
1523
- if (dbDriver === "memory") {
1524
- const { InMemoryDriver } = await import("@objectstack/driver-memory");
1525
- driverPlugin = new DriverPlugin2(new InMemoryDriver());
1526
- } else if (dbDriver === "postgres") {
1527
- const { SqlDriver } = await import("@objectstack/driver-sql");
1528
- driverPlugin = new DriverPlugin2(
1529
- new SqlDriver({
1530
- client: "pg",
1531
- connection: dbUrl,
1532
- pool: { min: 0, max: 5 }
1533
- })
1534
- );
1535
- } else if (dbDriver === "mongodb") {
1536
- let MongoDBDriver;
1537
- try {
1538
- ({ MongoDBDriver } = await import("@objectstack/driver-mongodb"));
1539
- } catch (err) {
1540
- throw new Error(
1541
- `[StandaloneStack] mongodb URL detected but @objectstack/driver-mongodb is not installed. Add it as a dependency or pass an explicit driverPlugin. (${err?.message ?? err})`
1542
- );
1543
- }
1544
- driverPlugin = new DriverPlugin2(new MongoDBDriver({ url: dbUrl }));
1545
- } else if (dbDriver === "sqlite-wasm") {
1582
+ if (dbDriver === "sqlite-wasm") {
1546
1583
  const { SqliteWasmDriver } = await import("@objectstack/driver-sqlite-wasm");
1547
- const filename = dbUrl.replace(/^wasm-sqlite:(\/\/)?/i, "").replace(/^file:(\/\/)?/i, "");
1548
- if (filename && filename !== ":memory:") {
1584
+ const filename = dbUrl.replace(/^wasm-sqlite:(\/\/)?/i, "").replace(/^file:(\/\/)?/i, "") || ":memory:";
1585
+ if (filename !== ":memory:") {
1549
1586
  (0, import_node_fs2.mkdirSync)((0, import_node_path3.resolve)(filename, ".."), { recursive: true });
1550
1587
  }
1551
1588
  driverPlugin = new DriverPlugin2(
1552
1589
  new SqliteWasmDriver({
1553
- filename: filename || ":memory:",
1554
- persist: filename && filename !== ":memory:" ? "on-write" : void 0
1590
+ filename,
1591
+ persist: filename !== ":memory:" ? "on-write" : void 0
1555
1592
  })
1556
1593
  );
1557
1594
  } else {
1558
- const { SqlDriver } = await import("@objectstack/driver-sql");
1559
- const filename = dbUrl.replace(/^file:(\/\/)?/, "");
1560
- if (!filename || /^[a-z][a-z0-9+.-]*:\/\//i.test(filename)) {
1561
- throw new Error(
1562
- `[StandaloneStack] sqlite driver was selected but the URL does not look like a file path: "${dbUrl}". Use file:/path/to/db.sqlite, or set OS_DATABASE_DRIVER explicitly.`
1563
- );
1595
+ const { createDefaultDatasourceDriverFactory } = await import("@objectstack/service-datasource");
1596
+ let driverId;
1597
+ let driverConfig;
1598
+ if (dbDriver === "memory") {
1599
+ driverId = "memory";
1600
+ driverConfig = {};
1601
+ } else if (dbDriver === "postgres") {
1602
+ driverId = "postgres";
1603
+ driverConfig = { url: dbUrl };
1604
+ } else if (dbDriver === "mongodb") {
1605
+ driverId = "mongodb";
1606
+ driverConfig = { url: dbUrl };
1607
+ } else {
1608
+ driverId = "sqlite";
1609
+ const filename = dbUrl.replace(/^file:(\/\/)?/, "");
1610
+ if (!filename || /^[a-z][a-z0-9+.-]*:\/\//i.test(filename)) {
1611
+ throw new Error(
1612
+ `[StandaloneStack] sqlite driver was selected but the URL does not look like a file path: "${dbUrl}". Use file:/path/to/db.sqlite, or set OS_DATABASE_DRIVER explicitly.`
1613
+ );
1614
+ }
1615
+ (0, import_node_fs2.mkdirSync)((0, import_node_path3.resolve)(filename, ".."), { recursive: true });
1616
+ driverConfig = { filename };
1617
+ }
1618
+ let driverHandle;
1619
+ try {
1620
+ driverHandle = await createDefaultDatasourceDriverFactory().create({ driver: driverId, config: driverConfig });
1621
+ } catch (err) {
1622
+ if (dbDriver === "mongodb") {
1623
+ throw new Error(
1624
+ `[StandaloneStack] mongodb URL detected but @objectstack/driver-mongodb is not installed. Add it as a dependency or pass an explicit driverPlugin. (${err?.message ?? err})`
1625
+ );
1626
+ }
1627
+ throw err;
1564
1628
  }
1565
- (0, import_node_fs2.mkdirSync)((0, import_node_path3.resolve)(filename, ".."), { recursive: true });
1566
1629
  driverPlugin = new DriverPlugin2(
1567
- new SqlDriver({
1568
- client: "better-sqlite3",
1569
- connection: { filename },
1570
- useNullAsDefault: true
1571
- })
1630
+ driverHandle?.driver ?? driverHandle
1572
1631
  );
1573
1632
  }
1574
1633
  const artifactBundle = await loadArtifactBundle(artifactPath, {
@@ -1606,6 +1665,8 @@ async function createStandaloneStack(config) {
1606
1665
  const requires = Array.isArray(artifactBundle?.requires) ? artifactBundle.requires.filter((c) => typeof c === "string") : void 0;
1607
1666
  const objects = Array.isArray(artifactBundle?.objects) ? artifactBundle.objects : void 0;
1608
1667
  const manifest = artifactBundle?.manifest;
1668
+ const permissions = Array.isArray(artifactBundle?.permissions) ? artifactBundle.permissions : void 0;
1669
+ const roles = Array.isArray(artifactBundle?.roles) ? artifactBundle.roles : void 0;
1609
1670
  return {
1610
1671
  plugins,
1611
1672
  api: {
@@ -1614,7 +1675,9 @@ async function createStandaloneStack(config) {
1614
1675
  },
1615
1676
  ...requires ? { requires } : {},
1616
1677
  ...objects ? { objects } : {},
1617
- ...manifest ? { manifest } : {}
1678
+ ...manifest ? { manifest } : {},
1679
+ ...permissions ? { permissions } : {},
1680
+ ...roles ? { roles } : {}
1618
1681
  };
1619
1682
  }
1620
1683
  var import_node_path3, import_node_fs2, import_node_os, import_zod, import_types2, StandaloneStackConfigSchema;
@@ -2065,25 +2128,33 @@ function coerceLocale(value) {
2065
2128
  const s = typeof value === "string" ? value.trim() : value != null ? String(value).trim() : "";
2066
2129
  return s || void 0;
2067
2130
  }
2131
+ function coerceCurrency(value) {
2132
+ const s = typeof value === "string" ? value.trim().toUpperCase() : "";
2133
+ return /^[A-Z]{3}$/.test(s) ? s : void 0;
2134
+ }
2068
2135
  async function resolveLocalization(opts, ql, sctx) {
2069
2136
  try {
2070
2137
  const settings = await opts.getService("settings");
2071
2138
  if (settings && typeof settings.get === "function") {
2072
- const [tzRes, localeRes] = await Promise.all([
2139
+ const [tzRes, localeRes, currencyRes] = await Promise.all([
2073
2140
  settings.get("localization", "timezone", sctx).catch(() => void 0),
2074
- settings.get("localization", "locale", sctx).catch(() => void 0)
2141
+ settings.get("localization", "locale", sctx).catch(() => void 0),
2142
+ settings.get("localization", "currency", sctx).catch(() => void 0)
2075
2143
  ]);
2076
2144
  const tz = coerceTimeZone(tzRes?.value);
2077
2145
  const locale = coerceLocale(localeRes?.value);
2078
- if (tz || locale) return { timezone: tz ?? "UTC", locale: locale ?? "en-US" };
2146
+ const currency = coerceCurrency(currencyRes?.value);
2147
+ if (tz || locale || currency) return { timezone: tz ?? "UTC", locale: locale ?? "en-US", currency };
2079
2148
  }
2080
2149
  } catch {
2081
2150
  }
2082
2151
  const tzRows = await tryFind(ql, "sys_setting", { namespace: "localization", key: "timezone", scope: "tenant" }, 1);
2083
2152
  const localeRows = await tryFind(ql, "sys_setting", { namespace: "localization", key: "locale", scope: "tenant" }, 1);
2153
+ const currencyRows = await tryFind(ql, "sys_setting", { namespace: "localization", key: "currency", scope: "tenant" }, 1);
2084
2154
  return {
2085
2155
  timezone: coerceTimeZone(tzRows[0]?.value) ?? "UTC",
2086
- locale: coerceLocale(localeRows[0]?.value) ?? "en-US"
2156
+ locale: coerceLocale(localeRows[0]?.value) ?? "en-US",
2157
+ currency: coerceCurrency(currencyRows[0]?.value)
2087
2158
  };
2088
2159
  }
2089
2160
  async function resolveExecutionContext(opts) {
@@ -2138,6 +2209,13 @@ async function resolveExecutionContext(opts) {
2138
2209
  }
2139
2210
  }
2140
2211
  }
2212
+ const userRoleRows = await tryFind(ql, "sys_user_role", { user_id: userId }, 200);
2213
+ for (const ur of userRoleRows) {
2214
+ const org = ur.organization_id ?? null;
2215
+ if (org && tenantId && org !== tenantId) continue;
2216
+ const r = ur.role;
2217
+ if (typeof r === "string" && r && !ctx.roles.includes(r)) ctx.roles.push(r);
2218
+ }
2141
2219
  if (tenantId) {
2142
2220
  const orgMembers = await tryFind(
2143
2221
  ql,
@@ -2224,6 +2302,7 @@ async function resolveExecutionContext(opts) {
2224
2302
  const localization = await resolveLocalization(opts, ql, { tenantId, userId });
2225
2303
  ctx.timezone = localization.timezone;
2226
2304
  ctx.locale = localization.locale;
2305
+ if (localization.currency) ctx.currency = localization.currency;
2227
2306
  return ctx;
2228
2307
  }
2229
2308
  function isPermissionDeniedError(e) {
@@ -2721,7 +2800,7 @@ var _HttpDispatcher = class _HttpDispatcher {
2721
2800
  */
2722
2801
  async enforceProjectMembership(context, path) {
2723
2802
  if (!this.enforceMembership) return null;
2724
- const skipPaths = ["/auth", "/cloud", "/health", "/discovery"];
2803
+ const skipPaths = ["/auth", "/cloud", "/health", "/ready", "/discovery"];
2725
2804
  if (skipPaths.some((p) => path.startsWith(p))) return null;
2726
2805
  if (/(^|\/)share-links\/[^/]+\/(resolve|messages)$/.test(path)) return null;
2727
2806
  const environmentId = context.environmentId;
@@ -4538,6 +4617,10 @@ var _HttpDispatcher = class _HttpDispatcher {
4538
4617
  })
4539
4618
  };
4540
4619
  }
4620
+ if (cleanPath === "/ready" && method === "GET") {
4621
+ const state = typeof this.kernel?.getState === "function" ? this.kernel.getState() : "running";
4622
+ return state === "running" ? { handled: true, response: this.success({ status: "ready", state }) } : { handled: true, response: this.error("Service not ready", 503, { state }) };
4623
+ }
4541
4624
  if (cleanPath.startsWith("/auth")) {
4542
4625
  return this.handleAuth(cleanPath.substring(5), method, body, context);
4543
4626
  }