@objectstack/service-datasource 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.
Files changed (37) hide show
  1. package/.turbo/turbo-build.log +28 -16
  2. package/CHANGELOG.md +100 -0
  3. package/dist/chunk-76HQ74MX.cjs +82 -0
  4. package/dist/chunk-76HQ74MX.cjs.map +1 -0
  5. package/dist/chunk-BI2SYWLC.cjs +9 -0
  6. package/dist/chunk-BI2SYWLC.cjs.map +1 -0
  7. package/dist/chunk-JRBGOCRJ.js +82 -0
  8. package/dist/chunk-JRBGOCRJ.js.map +1 -0
  9. package/dist/chunk-XLS4RP7B.js +9 -0
  10. package/dist/chunk-XLS4RP7B.js.map +1 -0
  11. package/dist/contracts/index.cjs +7 -1
  12. package/dist/contracts/index.cjs.map +1 -1
  13. package/dist/contracts/index.d.cts +59 -1
  14. package/dist/contracts/index.d.ts +59 -1
  15. package/dist/contracts/index.js +6 -0
  16. package/dist/index.cjs +284 -106
  17. package/dist/index.cjs.map +1 -1
  18. package/dist/index.d.cts +270 -5
  19. package/dist/index.d.ts +270 -5
  20. package/dist/index.js +216 -38
  21. package/dist/index.js.map +1 -1
  22. package/dist/sqlite-driver-fallback-BPFQYLX7.js +11 -0
  23. package/dist/sqlite-driver-fallback-BPFQYLX7.js.map +1 -0
  24. package/dist/sqlite-driver-fallback-JX4XOICD.cjs +11 -0
  25. package/dist/sqlite-driver-fallback-JX4XOICD.cjs.map +1 -0
  26. package/package.json +8 -7
  27. package/src/__tests__/datasource-connection-service.test.ts +294 -0
  28. package/src/contracts/connect-policy.ts +69 -0
  29. package/src/contracts/index.ts +11 -0
  30. package/src/datasource-admin-plugin.ts +37 -40
  31. package/src/datasource-admin-service.ts +2 -0
  32. package/src/datasource-connection-service.ts +364 -0
  33. package/src/default-datasource-driver-factory.ts +26 -9
  34. package/src/index.ts +29 -0
  35. package/src/logger.ts +2 -0
  36. package/src/sqlite-driver-fallback.test.ts +184 -0
  37. package/src/sqlite-driver-fallback.ts +195 -0
package/dist/index.js CHANGED
@@ -1,3 +1,12 @@
1
+ import {
2
+ allowAllConnectPolicy
3
+ } from "./chunk-XLS4RP7B.js";
4
+ import {
5
+ NATIVE_SQLITE_MEMORY_FALLBACK_WARNING,
6
+ NATIVE_SQLITE_WASM_FALLBACK_WARNING,
7
+ resolveSqliteDriver
8
+ } from "./chunk-JRBGOCRJ.js";
9
+
1
10
  // src/external-datasource-service.ts
2
11
  import {
3
12
  suggestFieldType,
@@ -368,6 +377,179 @@ function safeGetService(ctx, name) {
368
377
  }
369
378
  }
370
379
 
380
+ // src/datasource-connection-service.ts
381
+ function isDatasourceAddressed(ds, ctx) {
382
+ if (ds.schemaMode && ds.schemaMode !== "managed") return true;
383
+ if (ds.autoConnect === true) return true;
384
+ if (ctx.objects?.some((o) => o?.datasource === ds.name)) return true;
385
+ return false;
386
+ }
387
+ var DatasourceConnectionService = class {
388
+ constructor(cfg) {
389
+ this.cfg = cfg;
390
+ this.policy = cfg.policy ?? allowAllConnectPolicy;
391
+ this.logger = cfg.logger;
392
+ }
393
+ /**
394
+ * Auto-connect the declared (code-defined) datasources that pass the D2 gate.
395
+ * Called from `AppPlugin.start()` with the app bundle's datasources + objects.
396
+ * Each connected external datasource also has its bound objects' read metadata
397
+ * synced so they are immediately queryable with zero app code.
398
+ */
399
+ async connectDeclared(input) {
400
+ const objects = input.objects ?? [];
401
+ const results = [];
402
+ for (const ds of input.datasources) {
403
+ if (!ds?.name) continue;
404
+ if (ds.active === false) continue;
405
+ if (!isDatasourceAddressed(ds, { objects })) continue;
406
+ const bound = objects.filter((o) => o?.datasource === ds.name && typeof o?.name === "string").map((o) => o.name);
407
+ results.push(
408
+ await this.connect(ds, { objects: bound, context: { origin: ds.origin ?? "code", trigger: "declared-auto" } })
409
+ );
410
+ }
411
+ return results;
412
+ }
413
+ /**
414
+ * Build + connect + register a single datasource's live driver. The shared
415
+ * core used by both auto-connect and the runtime-admin pool registration.
416
+ *
417
+ * Failure policy (ADR-0062 D5): an `external` datasource with
418
+ * `validation.onMismatch: 'fail'` fails fast (re-throws, bricking boot as
419
+ * intended); everything else degrades with a warning so an optional replica's
420
+ * connectivity blip never bricks boot.
421
+ */
422
+ async connect(record, opts = {}) {
423
+ const name = record.name;
424
+ const engine = this.cfg.engine();
425
+ const factory = this.cfg.factory();
426
+ if (engine?.getDriverByName?.(name)) {
427
+ return { name, status: "already-registered" };
428
+ }
429
+ let decision;
430
+ try {
431
+ decision = await this.policy.canConnect(
432
+ { name, driver: record.driver, schemaMode: record.schemaMode, external: record.external },
433
+ opts.context
434
+ );
435
+ } catch (err) {
436
+ decision = { allow: false, reason: `connect policy threw: ${errMsg(err)}` };
437
+ }
438
+ if (!decision.allow) {
439
+ this.logger?.info?.(`datasource '${name}': connect denied by policy${decision.reason ? ` (${decision.reason})` : ""}`);
440
+ return { name, status: "skipped-policy", reason: decision.reason };
441
+ }
442
+ if (!factory || !engine?.registerDriver) {
443
+ this.logger?.debug?.(`datasource '${name}': no driver factory / engine \u2014 left metadata-only`);
444
+ return { name, status: "skipped-no-infra" };
445
+ }
446
+ if (!factory.supports(record.driver)) {
447
+ return this.handleFailure(
448
+ record,
449
+ "skipped-unsupported",
450
+ `no driver factory supports driver '${record.driver}'`,
451
+ opts.context
452
+ );
453
+ }
454
+ let secret;
455
+ const credentialsRef = record.external?.credentialsRef;
456
+ if (credentialsRef) {
457
+ const resolver = this.cfg.secrets?.resolve;
458
+ if (!resolver) {
459
+ return this.handleFailure(
460
+ record,
461
+ "failed-credentials",
462
+ `requires credential '${credentialsRef}' but no secret store (SecretBinder/ICryptoProvider) is configured`,
463
+ opts.context
464
+ );
465
+ }
466
+ try {
467
+ secret = await resolver(credentialsRef);
468
+ } catch (err) {
469
+ return this.handleFailure(record, "failed-credentials", `resolving credential '${credentialsRef}' threw: ${errMsg(err)}`, opts.context);
470
+ }
471
+ if (secret == null || secret === "") {
472
+ return this.handleFailure(
473
+ record,
474
+ "failed-credentials",
475
+ `credential '${credentialsRef}' could not be resolved or decrypted (missing sys_secret row, or the encryption key changed)`,
476
+ opts.context
477
+ );
478
+ }
479
+ }
480
+ try {
481
+ const handle = await factory.create({ ...toSpec(record), ...secret ? { secret } : {} });
482
+ if (typeof handle?.connect === "function") await handle.connect();
483
+ const engineDriver = handle.driver ?? handle;
484
+ try {
485
+ engineDriver.name = name;
486
+ } catch {
487
+ }
488
+ engine.registerDriver(engineDriver);
489
+ engine.registerDatasourceDef?.({
490
+ name,
491
+ schemaMode: record.schemaMode,
492
+ external: record.external
493
+ });
494
+ for (const objectName of opts.objects ?? []) {
495
+ try {
496
+ await engine.syncObjectSchema?.(objectName);
497
+ } catch (err) {
498
+ this.logger?.warn?.(`datasource '${name}': syncObjectSchema('${objectName}') failed: ${errMsg(err)}`);
499
+ }
500
+ }
501
+ this.logger?.info?.(`datasource '${name}': connected (driver=${record.driver}, schemaMode=${record.schemaMode ?? "managed"})`);
502
+ return { name, status: "connected" };
503
+ } catch (err) {
504
+ return this.handleFailure(record, "failed-degraded", errMsg(err), opts.context);
505
+ }
506
+ }
507
+ /** Gracefully disconnect a previously-registered datasource pool. */
508
+ async disconnect(name) {
509
+ const driver = this.cfg.engine()?.getDriverByName?.(name);
510
+ if (typeof driver?.disconnect === "function") {
511
+ try {
512
+ await driver.disconnect();
513
+ } catch (err) {
514
+ this.logger?.warn?.(`datasource '${name}': disconnect failed: ${errMsg(err)}`);
515
+ }
516
+ }
517
+ }
518
+ /**
519
+ * Apply the D5 connect-failure policy (also covers D3 credential failures). A
520
+ * code-defined `external` datasource with `onMismatch:'fail'` auto-connected at
521
+ * boot re-throws (fail-fast, bricking boot as intended). Runtime-admin
522
+ * create/update + boot rehydration always degrade-with-warning — a UI action
523
+ * or a replica blip must never brick the running server (preserves the
524
+ * pre-ADR-0062 admin behavior). Either way the datasource is left unconnected
525
+ * with a clear message — never a silent skip.
526
+ */
527
+ handleFailure(record, status, reason, context) {
528
+ const isExternal = record.schemaMode && record.schemaMode !== "managed";
529
+ const failFast = context?.trigger === "declared-auto" && isExternal && record.external?.validation?.onMismatch === "fail";
530
+ const msg = `datasource '${record.name}': connect failed \u2014 ${reason}`;
531
+ if (failFast) {
532
+ throw new Error(
533
+ `${msg}. (schemaMode=${record.schemaMode}, validation.onMismatch='fail' \u21D2 fail-fast per ADR-0062 D5)`
534
+ );
535
+ }
536
+ this.logger?.warn?.(`${msg} \u2014 degrading (datasource left unconnected)`);
537
+ return { name: record.name, status, reason };
538
+ }
539
+ };
540
+ function toSpec(record) {
541
+ return {
542
+ name: record.name,
543
+ driver: record.driver,
544
+ config: record.config ?? {},
545
+ external: record.external,
546
+ pool: record.pool
547
+ };
548
+ }
549
+ function errMsg(err) {
550
+ return err instanceof Error ? err.message : String(err);
551
+ }
552
+
371
553
  // src/datasource-admin-service.ts
372
554
  var NAME_RE = /^[a-z_][a-z0-9_]*$/;
373
555
  var DatasourceAdminService = class {
@@ -647,6 +829,14 @@ var DatasourceAdminServicePlugin = class {
647
829
  const metadataOf = () => safeGetService2(ctx, "metadata");
648
830
  const engineOf = () => safeGetService2(ctx, "data");
649
831
  const factory = () => this.options.driverFactory ?? safeGetService2(ctx, "datasource-driver-factory");
832
+ this.connection = new DatasourceConnectionService({
833
+ factory,
834
+ engine: () => engineOf(),
835
+ secrets: { resolve: (ref) => this.options.secrets?.resolve?.(ref) ?? Promise.resolve(void 0) },
836
+ policy: this.options.connectPolicy,
837
+ logger: this.options.logger
838
+ });
839
+ ctx.registerService("datasource-connection", this.connection);
650
840
  const config = {
651
841
  probe: (input) => this.probe(factory(), input),
652
842
  listDatasourceRecords: async () => {
@@ -690,29 +880,20 @@ var DatasourceAdminServicePlugin = class {
690
880
  const objects = await metadata?.listObjects?.() ?? await metadata?.list("object") ?? [];
691
881
  return objects.filter((o) => o?.datasource === datasource).length;
692
882
  },
883
+ // Hot pool (de)registration converges on the shared
884
+ // DatasourceConnectionService (ADR-0062 D1) — one connect path for code-
885
+ // and runtime-origin datasources. `connect()` builds the driver via the
886
+ // factory, dereferences `external.credentialsRef` through the SecretBinder,
887
+ // opens the connection, and registers the live driver + datasource def.
888
+ // Runtime-admin connects always degrade-with-warning on failure (never
889
+ // fail-fast), preserving the pre-ADR-0062 admin behavior.
693
890
  registerPool: async (record) => {
694
- const f = factory();
695
- const engine = engineOf();
696
- if (!f || !engine?.registerDriver || !f.supports(record.driver)) return;
697
- const credentialsRef = record.external?.credentialsRef;
698
- const secret = credentialsRef ? await this.options.secrets?.resolve?.(credentialsRef) : void 0;
699
- const handle = await f.create({ ...this.toSpec(record), ...secret ? { secret } : {} });
700
- if (typeof handle?.connect === "function") await handle.connect();
701
- const engineDriver = handle.driver ?? handle;
702
- try {
703
- engineDriver.name = record.name;
704
- } catch {
705
- }
706
- engine.registerDriver(engineDriver);
707
- engine.registerDatasourceDef?.({
708
- name: record.name,
709
- schemaMode: record.schemaMode,
710
- external: record.external
891
+ await this.connection?.connect(record, {
892
+ context: { origin: record.origin ?? "runtime", trigger: "runtime-admin" }
711
893
  });
712
894
  },
713
895
  unregisterPool: async (name) => {
714
- const driver = engineOf()?.getDriverByName?.(name);
715
- if (typeof driver?.disconnect === "function") await driver.disconnect();
896
+ await this.connection?.disconnect(name);
716
897
  },
717
898
  logger
718
899
  };
@@ -821,15 +1002,6 @@ var DatasourceAdminServicePlugin = class {
821
1002
  this.service = void 0;
822
1003
  }
823
1004
  // --- internals -----------------------------------------------------------
824
- toSpec(record) {
825
- return {
826
- name: record.name,
827
- driver: record.driver,
828
- config: record.config ?? {},
829
- external: record.external,
830
- pool: record.pool
831
- };
832
- }
833
1005
  /** Probe a connection via the driver factory: build → connect → ping → close. */
834
1006
  async probe(factory, input) {
835
1007
  if (!factory) {
@@ -847,7 +1019,7 @@ var DatasourceAdminServicePlugin = class {
847
1019
  external: input.external
848
1020
  });
849
1021
  } catch (err) {
850
- return { ok: false, error: `Failed to build driver: ${errMsg(err)}` };
1022
+ return { ok: false, error: `Failed to build driver: ${errMsg2(err)}` };
851
1023
  }
852
1024
  const startedAt = monotonicNow();
853
1025
  try {
@@ -863,7 +1035,7 @@ var DatasourceAdminServicePlugin = class {
863
1035
  }
864
1036
  return { ok: true, latencyMs, ...serverVersion ? { serverVersion } : {} };
865
1037
  } catch (err) {
866
- return { ok: false, error: errMsg(err) };
1038
+ return { ok: false, error: errMsg2(err) };
867
1039
  } finally {
868
1040
  try {
869
1041
  if (typeof driver?.disconnect === "function") await driver.disconnect();
@@ -879,7 +1051,7 @@ function safeGetService2(ctx, name) {
879
1051
  return void 0;
880
1052
  }
881
1053
  }
882
- function errMsg(err) {
1054
+ function errMsg2(err) {
883
1055
  return err instanceof Error ? err.message : String(err);
884
1056
  }
885
1057
  function monotonicNow() {
@@ -947,7 +1119,7 @@ function buildMongoUrl(spec) {
947
1119
  const auth = user ? `${encodeURIComponent(user)}:${encodeURIComponent(spec.secret ?? "")}@` : "";
948
1120
  return `mongodb://${auth}${host}:${port}/${db}`;
949
1121
  }
950
- function createDefaultDatasourceDriverFactory() {
1122
+ function createDefaultDatasourceDriverFactory(options = {}) {
951
1123
  return {
952
1124
  supports(driverId) {
953
1125
  return resolveKind(driverId) !== void 0;
@@ -969,14 +1141,14 @@ function createDefaultDatasourceDriverFactory() {
969
1141
  return toHandle(driver, () => sqlServerVersion(driver, "pg"));
970
1142
  }
971
1143
  if (kind === "sqlite") {
972
- const { SqlDriver } = await import("@objectstack/driver-sql");
973
- const driver = new SqlDriver({
974
- client: "better-sqlite3",
975
- connection: buildSqlConnection(spec, "better-sqlite3"),
976
- useNullAsDefault: true,
1144
+ const conn = buildSqlConnection(spec, "better-sqlite3");
1145
+ const { resolveSqliteDriver: resolveSqliteDriver2 } = await import("./sqlite-driver-fallback-BPFQYLX7.js");
1146
+ const resolved = await resolveSqliteDriver2({
1147
+ filename: conn.filename ?? ":memory:",
1148
+ dev: options.dev,
977
1149
  ...schemaMode ? { schemaMode } : {}
978
1150
  });
979
- return toHandle(driver, () => sqlServerVersion(driver, "sqlite"));
1151
+ return toHandle(resolved.driver, () => sqlServerVersion(resolved.driver, "sqlite"));
980
1152
  }
981
1153
  if (kind === "mongodb") {
982
1154
  let MongoDBDriver;
@@ -1280,12 +1452,18 @@ function registerDatasourceAdminRoutes(server, ctx, basePath = "/api/v1") {
1280
1452
  export {
1281
1453
  DatasourceAdminService,
1282
1454
  DatasourceAdminServicePlugin,
1455
+ DatasourceConnectionService,
1283
1456
  ExternalDatasourceService,
1284
1457
  ExternalDatasourceServicePlugin,
1458
+ NATIVE_SQLITE_MEMORY_FALLBACK_WARNING,
1459
+ NATIVE_SQLITE_WASM_FALLBACK_WARNING,
1460
+ allowAllConnectPolicy,
1285
1461
  createDatasourceSecretBinder,
1286
1462
  createDefaultDatasourceDriverFactory,
1463
+ isDatasourceAddressed,
1287
1464
  parseCredentialsRef,
1288
1465
  registerDatasourceAdminRoutes,
1466
+ resolveSqliteDriver,
1289
1467
  toCredentialsRef
1290
1468
  };
1291
1469
  //# sourceMappingURL=index.js.map