@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.d.cts CHANGED
@@ -98,6 +98,7 @@ declare const StandaloneStackConfigSchema: z.ZodObject<{
98
98
  environmentId: z.ZodOptional<z.ZodString>;
99
99
  artifactPath: z.ZodOptional<z.ZodString>;
100
100
  projectRoot: z.ZodOptional<z.ZodString>;
101
+ dev: z.ZodOptional<z.ZodBoolean>;
101
102
  }, z.core.$strip>;
102
103
  type StandaloneStackConfig = z.input<typeof StandaloneStackConfigSchema>;
103
104
  interface StandaloneStackResult {
package/dist/index.d.ts CHANGED
@@ -98,6 +98,7 @@ declare const StandaloneStackConfigSchema: z.ZodObject<{
98
98
  environmentId: z.ZodOptional<z.ZodString>;
99
99
  artifactPath: z.ZodOptional<z.ZodString>;
100
100
  projectRoot: z.ZodOptional<z.ZodString>;
101
+ dev: z.ZodOptional<z.ZodBoolean>;
101
102
  }, z.core.$strip>;
102
103
  type StandaloneStackConfig = z.input<typeof StandaloneStackConfigSchema>;
103
104
  interface StandaloneStackResult {
package/dist/index.js CHANGED
@@ -963,6 +963,36 @@ var init_app_plugin = __esm({
963
963
  error: err?.message ?? String(err)
964
964
  });
965
965
  }
966
+ try {
967
+ const dsDefs = this.bundle.datasources;
968
+ const dsList = Array.isArray(dsDefs) ? dsDefs : dsDefs && typeof dsDefs === "object" ? Object.entries(dsDefs).map(([name, def]) => ({ name, ...def })) : [];
969
+ if (dsList.length > 0) {
970
+ let connection;
971
+ try {
972
+ connection = ctx.getService("datasource-connection");
973
+ } catch {
974
+ connection = void 0;
975
+ }
976
+ if (typeof connection?.connectDeclared === "function") {
977
+ const objects = Array.isArray(this.bundle.objects) ? this.bundle.objects : [];
978
+ const results = await connection.connectDeclared({ datasources: dsList, objects });
979
+ const connected = results.filter((r) => r.status === "connected");
980
+ if (connected.length > 0) {
981
+ ctx.logger.info("Auto-connected declared datasources", {
982
+ appId,
983
+ connected: connected.map((r) => r.name)
984
+ });
985
+ }
986
+ } else {
987
+ ctx.logger.debug("No datasource-connection service \u2014 declared datasources stay metadata-only", { appId });
988
+ }
989
+ }
990
+ } catch (err) {
991
+ ctx.logger.error(
992
+ `[AppPlugin] declared-datasource auto-connect failed for app '${appId}': ${err?.message ?? String(err)}`
993
+ );
994
+ throw err;
995
+ }
966
996
  try {
967
997
  const metadata = ctx.getService("metadata");
968
998
  if (typeof metadata?.registerInMemory === "function") {
@@ -1530,55 +1560,56 @@ async function createStandaloneStack(config) {
1530
1560
  const explicitDriver = cfg.databaseDriver ?? process.env.OS_DATABASE_DRIVER?.trim();
1531
1561
  const dbDriver = explicitDriver ?? detectDriverFromUrl(dbUrl);
1532
1562
  let driverPlugin;
1533
- if (dbDriver === "memory") {
1534
- const { InMemoryDriver } = await import("@objectstack/driver-memory");
1535
- driverPlugin = new DriverPlugin2(new InMemoryDriver());
1536
- } else if (dbDriver === "postgres") {
1537
- const { SqlDriver } = await import("@objectstack/driver-sql");
1538
- driverPlugin = new DriverPlugin2(
1539
- new SqlDriver({
1540
- client: "pg",
1541
- connection: dbUrl,
1542
- pool: { min: 0, max: 5 }
1543
- })
1544
- );
1545
- } else if (dbDriver === "mongodb") {
1546
- let MongoDBDriver;
1547
- try {
1548
- ({ MongoDBDriver } = await import("@objectstack/driver-mongodb"));
1549
- } catch (err) {
1550
- throw new Error(
1551
- `[StandaloneStack] mongodb URL detected but @objectstack/driver-mongodb is not installed. Add it as a dependency or pass an explicit driverPlugin. (${err?.message ?? err})`
1552
- );
1553
- }
1554
- driverPlugin = new DriverPlugin2(new MongoDBDriver({ url: dbUrl }));
1555
- } else if (dbDriver === "sqlite-wasm") {
1563
+ if (dbDriver === "sqlite-wasm") {
1556
1564
  const { SqliteWasmDriver } = await import("@objectstack/driver-sqlite-wasm");
1557
- const filename = dbUrl.replace(/^wasm-sqlite:(\/\/)?/i, "").replace(/^file:(\/\/)?/i, "");
1558
- if (filename && filename !== ":memory:") {
1565
+ const filename = dbUrl.replace(/^wasm-sqlite:(\/\/)?/i, "").replace(/^file:(\/\/)?/i, "") || ":memory:";
1566
+ if (filename !== ":memory:") {
1559
1567
  mkdirSync2(resolvePath2(filename, ".."), { recursive: true });
1560
1568
  }
1561
1569
  driverPlugin = new DriverPlugin2(
1562
1570
  new SqliteWasmDriver({
1563
- filename: filename || ":memory:",
1564
- persist: filename && filename !== ":memory:" ? "on-write" : void 0
1571
+ filename,
1572
+ persist: filename !== ":memory:" ? "on-write" : void 0
1565
1573
  })
1566
1574
  );
1567
1575
  } else {
1568
- const { SqlDriver } = await import("@objectstack/driver-sql");
1569
- const filename = dbUrl.replace(/^file:(\/\/)?/, "");
1570
- if (!filename || /^[a-z][a-z0-9+.-]*:\/\//i.test(filename)) {
1571
- throw new Error(
1572
- `[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.`
1573
- );
1576
+ const { createDefaultDatasourceDriverFactory } = await import("@objectstack/service-datasource");
1577
+ const factoryDev = cfg.dev ?? process.env.NODE_ENV === "development";
1578
+ let driverId;
1579
+ let driverConfig;
1580
+ if (dbDriver === "memory") {
1581
+ driverId = "memory";
1582
+ driverConfig = {};
1583
+ } else if (dbDriver === "postgres") {
1584
+ driverId = "postgres";
1585
+ driverConfig = { url: dbUrl };
1586
+ } else if (dbDriver === "mongodb") {
1587
+ driverId = "mongodb";
1588
+ driverConfig = { url: dbUrl };
1589
+ } else {
1590
+ driverId = "sqlite";
1591
+ const filename = dbUrl.replace(/^file:(\/\/)?/, "");
1592
+ if (!filename || /^[a-z][a-z0-9+.-]*:\/\//i.test(filename)) {
1593
+ throw new Error(
1594
+ `[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.`
1595
+ );
1596
+ }
1597
+ mkdirSync2(resolvePath2(filename, ".."), { recursive: true });
1598
+ driverConfig = { filename };
1599
+ }
1600
+ let driverHandle;
1601
+ try {
1602
+ driverHandle = await createDefaultDatasourceDriverFactory({ dev: factoryDev }).create({ driver: driverId, config: driverConfig });
1603
+ } catch (err) {
1604
+ if (dbDriver === "mongodb") {
1605
+ throw new Error(
1606
+ `[StandaloneStack] mongodb URL detected but @objectstack/driver-mongodb is not installed. Add it as a dependency or pass an explicit driverPlugin. (${err?.message ?? err})`
1607
+ );
1608
+ }
1609
+ throw err;
1574
1610
  }
1575
- mkdirSync2(resolvePath2(filename, ".."), { recursive: true });
1576
1611
  driverPlugin = new DriverPlugin2(
1577
- new SqlDriver({
1578
- client: "better-sqlite3",
1579
- connection: { filename },
1580
- useNullAsDefault: true
1581
- })
1612
+ driverHandle?.driver ?? driverHandle
1582
1613
  );
1583
1614
  }
1584
1615
  const artifactBundle = await loadArtifactBundle(artifactPath, {
@@ -1653,7 +1684,14 @@ var init_standalone_stack = __esm({
1653
1684
  * Explicit `databaseUrl` / `OS_DATABASE_URL` / `OS_HOME` still take
1654
1685
  * precedence over this default.
1655
1686
  */
1656
- projectRoot: z.string().optional()
1687
+ projectRoot: z.string().optional(),
1688
+ /**
1689
+ * Dev gate for the sqlite driver factory's native-better-sqlite3 → wasm →
1690
+ * in-memory step-down (#2229). When omitted, defaults to
1691
+ * `process.env.NODE_ENV === 'development'`. In production a native load
1692
+ * failure is NOT silently swapped for wasm/mingo (fail-closed).
1693
+ */
1694
+ dev: z.boolean().optional()
1657
1695
  });
1658
1696
  }
1659
1697
  });
@@ -2135,14 +2173,12 @@ async function resolveExecutionContext(opts) {
2135
2173
  } else {
2136
2174
  ctx.org_user_ids = [userId];
2137
2175
  }
2138
- const upsRows = await tryFind(
2139
- ql,
2140
- "sys_user_permission_set",
2141
- tenantId ? { user_id: userId, organization_id: tenantId } : { user_id: userId },
2142
- 100
2143
- );
2176
+ const upsRows = await tryFind(ql, "sys_user_permission_set", { user_id: userId }, 100);
2144
2177
  const psIds = new Set(
2145
- upsRows.map((r) => r.permission_set_id ?? r.permissionSetId).filter(Boolean)
2178
+ upsRows.filter((r) => {
2179
+ const org = r.organization_id ?? r.organizationId ?? null;
2180
+ return !(org && tenantId && org !== tenantId);
2181
+ }).map((r) => r.permission_set_id ?? r.permissionSetId).filter(Boolean)
2146
2182
  );
2147
2183
  if (ctx.roles.length > 0) {
2148
2184
  const roleRows = await tryFind(ql, "sys_role", { name: { $in: ctx.roles } }, 100);
@@ -2702,7 +2738,7 @@ var _HttpDispatcher = class _HttpDispatcher {
2702
2738
  */
2703
2739
  async enforceProjectMembership(context, path) {
2704
2740
  if (!this.enforceMembership) return null;
2705
- const skipPaths = ["/auth", "/cloud", "/health", "/discovery"];
2741
+ const skipPaths = ["/auth", "/cloud", "/health", "/ready", "/discovery"];
2706
2742
  if (skipPaths.some((p) => path.startsWith(p))) return null;
2707
2743
  if (/(^|\/)share-links\/[^/]+\/(resolve|messages)$/.test(path)) return null;
2708
2744
  const environmentId = context.environmentId;
@@ -4148,6 +4184,28 @@ var _HttpDispatcher = class _HttpDispatcher {
4148
4184
  if (!ql || typeof ql.executeAction !== "function") {
4149
4185
  return { handled: true, response: this.error("Data engine not available", 503) };
4150
4186
  }
4187
+ try {
4188
+ const actionSchema = (typeof ql.getSchema === "function" ? ql.getSchema(objectName) : void 0) ?? ql.registry?.getObject?.(objectName);
4189
+ const actionDef = Array.isArray(actionSchema?.actions) ? actionSchema.actions.find((a) => a?.name === actionName) : void 0;
4190
+ const required = Array.isArray(actionDef?.requiredPermissions) ? actionDef.requiredPermissions : [];
4191
+ if (required.length > 0) {
4192
+ const execCtx = _context?.executionContext;
4193
+ if (!execCtx?.isSystem) {
4194
+ const held = new Set(execCtx?.systemPermissions ?? []);
4195
+ const missing = required.filter((perm) => !held.has(perm));
4196
+ if (missing.length > 0) {
4197
+ return {
4198
+ handled: true,
4199
+ response: this.error(
4200
+ `Action '${actionName}' on '${objectName}' requires capability [${required.join(", ")}] \u2014 caller is missing [${missing.join(", ")}]`,
4201
+ 403
4202
+ )
4203
+ };
4204
+ }
4205
+ }
4206
+ }
4207
+ } catch {
4208
+ }
4151
4209
  const tryExecute = async (obj) => {
4152
4210
  return ql.executeAction(obj, actionName, actionContext);
4153
4211
  };
@@ -4519,6 +4577,10 @@ var _HttpDispatcher = class _HttpDispatcher {
4519
4577
  })
4520
4578
  };
4521
4579
  }
4580
+ if (cleanPath === "/ready" && method === "GET") {
4581
+ const state = typeof this.kernel?.getState === "function" ? this.kernel.getState() : "running";
4582
+ return state === "running" ? { handled: true, response: this.success({ status: "ready", state }) } : { handled: true, response: this.error("Service not ready", 503, { state }) };
4583
+ }
4522
4584
  if (cleanPath.startsWith("/auth")) {
4523
4585
  return this.handleAuth(cleanPath.substring(5), method, body, context);
4524
4586
  }
@@ -5206,6 +5268,14 @@ function createDispatcherPlugin(config = {}) {
5206
5268
  errorResponse(err, res);
5207
5269
  }
5208
5270
  });
5271
+ server.get(`${prefix}/ready`, async (_req, res) => {
5272
+ try {
5273
+ const result = await dispatcher.dispatch("GET", "/ready", void 0, {}, { request: _req });
5274
+ sendResult(result, res);
5275
+ } catch (err) {
5276
+ errorResponse(err, res);
5277
+ }
5278
+ });
5209
5279
  server.post(`${prefix}/auth/login`, async (req, res) => {
5210
5280
  try {
5211
5281
  const result = await dispatcher.handleAuth("login", "POST", req.body, { request: req });