@objectstack/runtime 10.0.0 → 10.3.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,36 @@ 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
+ }
990
1020
  try {
991
1021
  const metadata = ctx.getService("metadata");
992
1022
  if (typeof metadata?.registerInMemory === "function") {
@@ -1549,55 +1579,56 @@ async function createStandaloneStack(config) {
1549
1579
  const explicitDriver = cfg.databaseDriver ?? process.env.OS_DATABASE_DRIVER?.trim();
1550
1580
  const dbDriver = explicitDriver ?? detectDriverFromUrl(dbUrl);
1551
1581
  let driverPlugin;
1552
- if (dbDriver === "memory") {
1553
- const { InMemoryDriver } = await import("@objectstack/driver-memory");
1554
- driverPlugin = new DriverPlugin2(new InMemoryDriver());
1555
- } else if (dbDriver === "postgres") {
1556
- const { SqlDriver } = await import("@objectstack/driver-sql");
1557
- driverPlugin = new DriverPlugin2(
1558
- new SqlDriver({
1559
- client: "pg",
1560
- connection: dbUrl,
1561
- pool: { min: 0, max: 5 }
1562
- })
1563
- );
1564
- } else if (dbDriver === "mongodb") {
1565
- let MongoDBDriver;
1566
- try {
1567
- ({ MongoDBDriver } = await import("@objectstack/driver-mongodb"));
1568
- } catch (err) {
1569
- throw new Error(
1570
- `[StandaloneStack] mongodb URL detected but @objectstack/driver-mongodb is not installed. Add it as a dependency or pass an explicit driverPlugin. (${err?.message ?? err})`
1571
- );
1572
- }
1573
- driverPlugin = new DriverPlugin2(new MongoDBDriver({ url: dbUrl }));
1574
- } else if (dbDriver === "sqlite-wasm") {
1582
+ if (dbDriver === "sqlite-wasm") {
1575
1583
  const { SqliteWasmDriver } = await import("@objectstack/driver-sqlite-wasm");
1576
- const filename = dbUrl.replace(/^wasm-sqlite:(\/\/)?/i, "").replace(/^file:(\/\/)?/i, "");
1577
- if (filename && filename !== ":memory:") {
1584
+ const filename = dbUrl.replace(/^wasm-sqlite:(\/\/)?/i, "").replace(/^file:(\/\/)?/i, "") || ":memory:";
1585
+ if (filename !== ":memory:") {
1578
1586
  (0, import_node_fs2.mkdirSync)((0, import_node_path3.resolve)(filename, ".."), { recursive: true });
1579
1587
  }
1580
1588
  driverPlugin = new DriverPlugin2(
1581
1589
  new SqliteWasmDriver({
1582
- filename: filename || ":memory:",
1583
- persist: filename && filename !== ":memory:" ? "on-write" : void 0
1590
+ filename,
1591
+ persist: filename !== ":memory:" ? "on-write" : void 0
1584
1592
  })
1585
1593
  );
1586
1594
  } else {
1587
- const { SqlDriver } = await import("@objectstack/driver-sql");
1588
- const filename = dbUrl.replace(/^file:(\/\/)?/, "");
1589
- if (!filename || /^[a-z][a-z0-9+.-]*:\/\//i.test(filename)) {
1590
- throw new Error(
1591
- `[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.`
1592
- );
1595
+ const { createDefaultDatasourceDriverFactory } = await import("@objectstack/service-datasource");
1596
+ const factoryDev = cfg.dev ?? process.env.NODE_ENV === "development";
1597
+ let driverId;
1598
+ let driverConfig;
1599
+ if (dbDriver === "memory") {
1600
+ driverId = "memory";
1601
+ driverConfig = {};
1602
+ } else if (dbDriver === "postgres") {
1603
+ driverId = "postgres";
1604
+ driverConfig = { url: dbUrl };
1605
+ } else if (dbDriver === "mongodb") {
1606
+ driverId = "mongodb";
1607
+ driverConfig = { url: dbUrl };
1608
+ } else {
1609
+ driverId = "sqlite";
1610
+ const filename = dbUrl.replace(/^file:(\/\/)?/, "");
1611
+ if (!filename || /^[a-z][a-z0-9+.-]*:\/\//i.test(filename)) {
1612
+ throw new Error(
1613
+ `[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.`
1614
+ );
1615
+ }
1616
+ (0, import_node_fs2.mkdirSync)((0, import_node_path3.resolve)(filename, ".."), { recursive: true });
1617
+ driverConfig = { filename };
1618
+ }
1619
+ let driverHandle;
1620
+ try {
1621
+ driverHandle = await createDefaultDatasourceDriverFactory({ dev: factoryDev }).create({ driver: driverId, config: driverConfig });
1622
+ } catch (err) {
1623
+ if (dbDriver === "mongodb") {
1624
+ throw new Error(
1625
+ `[StandaloneStack] mongodb URL detected but @objectstack/driver-mongodb is not installed. Add it as a dependency or pass an explicit driverPlugin. (${err?.message ?? err})`
1626
+ );
1627
+ }
1628
+ throw err;
1593
1629
  }
1594
- (0, import_node_fs2.mkdirSync)((0, import_node_path3.resolve)(filename, ".."), { recursive: true });
1595
1630
  driverPlugin = new DriverPlugin2(
1596
- new SqlDriver({
1597
- client: "better-sqlite3",
1598
- connection: { filename },
1599
- useNullAsDefault: true
1600
- })
1631
+ driverHandle?.driver ?? driverHandle
1601
1632
  );
1602
1633
  }
1603
1634
  const artifactBundle = await loadArtifactBundle(artifactPath, {
@@ -1677,7 +1708,14 @@ var init_standalone_stack = __esm({
1677
1708
  * Explicit `databaseUrl` / `OS_DATABASE_URL` / `OS_HOME` still take
1678
1709
  * precedence over this default.
1679
1710
  */
1680
- projectRoot: import_zod.z.string().optional()
1711
+ projectRoot: import_zod.z.string().optional(),
1712
+ /**
1713
+ * Dev gate for the sqlite driver factory's native-better-sqlite3 → wasm →
1714
+ * in-memory step-down (#2229). When omitted, defaults to
1715
+ * `process.env.NODE_ENV === 'development'`. In production a native load
1716
+ * failure is NOT silently swapped for wasm/mingo (fail-closed).
1717
+ */
1718
+ dev: import_zod.z.boolean().optional()
1681
1719
  });
1682
1720
  }
1683
1721
  });
@@ -2203,14 +2241,12 @@ async function resolveExecutionContext(opts) {
2203
2241
  } else {
2204
2242
  ctx.org_user_ids = [userId];
2205
2243
  }
2206
- const upsRows = await tryFind(
2207
- ql,
2208
- "sys_user_permission_set",
2209
- tenantId ? { user_id: userId, organization_id: tenantId } : { user_id: userId },
2210
- 100
2211
- );
2244
+ const upsRows = await tryFind(ql, "sys_user_permission_set", { user_id: userId }, 100);
2212
2245
  const psIds = new Set(
2213
- upsRows.map((r) => r.permission_set_id ?? r.permissionSetId).filter(Boolean)
2246
+ upsRows.filter((r) => {
2247
+ const org = r.organization_id ?? r.organizationId ?? null;
2248
+ return !(org && tenantId && org !== tenantId);
2249
+ }).map((r) => r.permission_set_id ?? r.permissionSetId).filter(Boolean)
2214
2250
  );
2215
2251
  if (ctx.roles.length > 0) {
2216
2252
  const roleRows = await tryFind(ql, "sys_role", { name: { $in: ctx.roles } }, 100);
@@ -2770,7 +2806,7 @@ var _HttpDispatcher = class _HttpDispatcher {
2770
2806
  */
2771
2807
  async enforceProjectMembership(context, path) {
2772
2808
  if (!this.enforceMembership) return null;
2773
- const skipPaths = ["/auth", "/cloud", "/health", "/discovery"];
2809
+ const skipPaths = ["/auth", "/cloud", "/health", "/ready", "/discovery"];
2774
2810
  if (skipPaths.some((p) => path.startsWith(p))) return null;
2775
2811
  if (/(^|\/)share-links\/[^/]+\/(resolve|messages)$/.test(path)) return null;
2776
2812
  const environmentId = context.environmentId;
@@ -4216,6 +4252,28 @@ var _HttpDispatcher = class _HttpDispatcher {
4216
4252
  if (!ql || typeof ql.executeAction !== "function") {
4217
4253
  return { handled: true, response: this.error("Data engine not available", 503) };
4218
4254
  }
4255
+ try {
4256
+ const actionSchema = (typeof ql.getSchema === "function" ? ql.getSchema(objectName) : void 0) ?? ql.registry?.getObject?.(objectName);
4257
+ const actionDef = Array.isArray(actionSchema?.actions) ? actionSchema.actions.find((a) => a?.name === actionName) : void 0;
4258
+ const required = Array.isArray(actionDef?.requiredPermissions) ? actionDef.requiredPermissions : [];
4259
+ if (required.length > 0) {
4260
+ const execCtx = _context?.executionContext;
4261
+ if (!execCtx?.isSystem) {
4262
+ const held = new Set(execCtx?.systemPermissions ?? []);
4263
+ const missing = required.filter((perm) => !held.has(perm));
4264
+ if (missing.length > 0) {
4265
+ return {
4266
+ handled: true,
4267
+ response: this.error(
4268
+ `Action '${actionName}' on '${objectName}' requires capability [${required.join(", ")}] \u2014 caller is missing [${missing.join(", ")}]`,
4269
+ 403
4270
+ )
4271
+ };
4272
+ }
4273
+ }
4274
+ }
4275
+ } catch {
4276
+ }
4219
4277
  const tryExecute = async (obj) => {
4220
4278
  return ql.executeAction(obj, actionName, actionContext);
4221
4279
  };
@@ -4587,6 +4645,10 @@ var _HttpDispatcher = class _HttpDispatcher {
4587
4645
  })
4588
4646
  };
4589
4647
  }
4648
+ if (cleanPath === "/ready" && method === "GET") {
4649
+ const state = typeof this.kernel?.getState === "function" ? this.kernel.getState() : "running";
4650
+ return state === "running" ? { handled: true, response: this.success({ status: "ready", state }) } : { handled: true, response: this.error("Service not ready", 503, { state }) };
4651
+ }
4590
4652
  if (cleanPath.startsWith("/auth")) {
4591
4653
  return this.handleAuth(cleanPath.substring(5), method, body, context);
4592
4654
  }
@@ -5264,6 +5326,14 @@ function createDispatcherPlugin(config = {}) {
5264
5326
  errorResponse(err, res);
5265
5327
  }
5266
5328
  });
5329
+ server.get(`${prefix}/ready`, async (_req, res) => {
5330
+ try {
5331
+ const result = await dispatcher.dispatch("GET", "/ready", void 0, {}, { request: _req });
5332
+ sendResult(result, res);
5333
+ } catch (err) {
5334
+ errorResponse(err, res);
5335
+ }
5336
+ });
5267
5337
  server.post(`${prefix}/auth/login`, async (req, res) => {
5268
5338
  try {
5269
5339
  const result = await dispatcher.handleAuth("login", "POST", req.body, { request: req });