@objectstack/service-datasource 10.0.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.js CHANGED
@@ -1,3 +1,7 @@
1
+ import {
2
+ allowAllConnectPolicy
3
+ } from "./chunk-XLS4RP7B.js";
4
+
1
5
  // src/external-datasource-service.ts
2
6
  import {
3
7
  suggestFieldType,
@@ -368,6 +372,179 @@ function safeGetService(ctx, name) {
368
372
  }
369
373
  }
370
374
 
375
+ // src/datasource-connection-service.ts
376
+ function isDatasourceAddressed(ds, ctx) {
377
+ if (ds.schemaMode && ds.schemaMode !== "managed") return true;
378
+ if (ds.autoConnect === true) return true;
379
+ if (ctx.objects?.some((o) => o?.datasource === ds.name)) return true;
380
+ return false;
381
+ }
382
+ var DatasourceConnectionService = class {
383
+ constructor(cfg) {
384
+ this.cfg = cfg;
385
+ this.policy = cfg.policy ?? allowAllConnectPolicy;
386
+ this.logger = cfg.logger;
387
+ }
388
+ /**
389
+ * Auto-connect the declared (code-defined) datasources that pass the D2 gate.
390
+ * Called from `AppPlugin.start()` with the app bundle's datasources + objects.
391
+ * Each connected external datasource also has its bound objects' read metadata
392
+ * synced so they are immediately queryable with zero app code.
393
+ */
394
+ async connectDeclared(input) {
395
+ const objects = input.objects ?? [];
396
+ const results = [];
397
+ for (const ds of input.datasources) {
398
+ if (!ds?.name) continue;
399
+ if (ds.active === false) continue;
400
+ if (!isDatasourceAddressed(ds, { objects })) continue;
401
+ const bound = objects.filter((o) => o?.datasource === ds.name && typeof o?.name === "string").map((o) => o.name);
402
+ results.push(
403
+ await this.connect(ds, { objects: bound, context: { origin: ds.origin ?? "code", trigger: "declared-auto" } })
404
+ );
405
+ }
406
+ return results;
407
+ }
408
+ /**
409
+ * Build + connect + register a single datasource's live driver. The shared
410
+ * core used by both auto-connect and the runtime-admin pool registration.
411
+ *
412
+ * Failure policy (ADR-0062 D5): an `external` datasource with
413
+ * `validation.onMismatch: 'fail'` fails fast (re-throws, bricking boot as
414
+ * intended); everything else degrades with a warning so an optional replica's
415
+ * connectivity blip never bricks boot.
416
+ */
417
+ async connect(record, opts = {}) {
418
+ const name = record.name;
419
+ const engine = this.cfg.engine();
420
+ const factory = this.cfg.factory();
421
+ if (engine?.getDriverByName?.(name)) {
422
+ return { name, status: "already-registered" };
423
+ }
424
+ let decision;
425
+ try {
426
+ decision = await this.policy.canConnect(
427
+ { name, driver: record.driver, schemaMode: record.schemaMode, external: record.external },
428
+ opts.context
429
+ );
430
+ } catch (err) {
431
+ decision = { allow: false, reason: `connect policy threw: ${errMsg(err)}` };
432
+ }
433
+ if (!decision.allow) {
434
+ this.logger?.info?.(`datasource '${name}': connect denied by policy${decision.reason ? ` (${decision.reason})` : ""}`);
435
+ return { name, status: "skipped-policy", reason: decision.reason };
436
+ }
437
+ if (!factory || !engine?.registerDriver) {
438
+ this.logger?.debug?.(`datasource '${name}': no driver factory / engine \u2014 left metadata-only`);
439
+ return { name, status: "skipped-no-infra" };
440
+ }
441
+ if (!factory.supports(record.driver)) {
442
+ return this.handleFailure(
443
+ record,
444
+ "skipped-unsupported",
445
+ `no driver factory supports driver '${record.driver}'`,
446
+ opts.context
447
+ );
448
+ }
449
+ let secret;
450
+ const credentialsRef = record.external?.credentialsRef;
451
+ if (credentialsRef) {
452
+ const resolver = this.cfg.secrets?.resolve;
453
+ if (!resolver) {
454
+ return this.handleFailure(
455
+ record,
456
+ "failed-credentials",
457
+ `requires credential '${credentialsRef}' but no secret store (SecretBinder/ICryptoProvider) is configured`,
458
+ opts.context
459
+ );
460
+ }
461
+ try {
462
+ secret = await resolver(credentialsRef);
463
+ } catch (err) {
464
+ return this.handleFailure(record, "failed-credentials", `resolving credential '${credentialsRef}' threw: ${errMsg(err)}`, opts.context);
465
+ }
466
+ if (secret == null || secret === "") {
467
+ return this.handleFailure(
468
+ record,
469
+ "failed-credentials",
470
+ `credential '${credentialsRef}' could not be resolved or decrypted (missing sys_secret row, or the encryption key changed)`,
471
+ opts.context
472
+ );
473
+ }
474
+ }
475
+ try {
476
+ const handle = await factory.create({ ...toSpec(record), ...secret ? { secret } : {} });
477
+ if (typeof handle?.connect === "function") await handle.connect();
478
+ const engineDriver = handle.driver ?? handle;
479
+ try {
480
+ engineDriver.name = name;
481
+ } catch {
482
+ }
483
+ engine.registerDriver(engineDriver);
484
+ engine.registerDatasourceDef?.({
485
+ name,
486
+ schemaMode: record.schemaMode,
487
+ external: record.external
488
+ });
489
+ for (const objectName of opts.objects ?? []) {
490
+ try {
491
+ await engine.syncObjectSchema?.(objectName);
492
+ } catch (err) {
493
+ this.logger?.warn?.(`datasource '${name}': syncObjectSchema('${objectName}') failed: ${errMsg(err)}`);
494
+ }
495
+ }
496
+ this.logger?.info?.(`datasource '${name}': connected (driver=${record.driver}, schemaMode=${record.schemaMode ?? "managed"})`);
497
+ return { name, status: "connected" };
498
+ } catch (err) {
499
+ return this.handleFailure(record, "failed-degraded", errMsg(err), opts.context);
500
+ }
501
+ }
502
+ /** Gracefully disconnect a previously-registered datasource pool. */
503
+ async disconnect(name) {
504
+ const driver = this.cfg.engine()?.getDriverByName?.(name);
505
+ if (typeof driver?.disconnect === "function") {
506
+ try {
507
+ await driver.disconnect();
508
+ } catch (err) {
509
+ this.logger?.warn?.(`datasource '${name}': disconnect failed: ${errMsg(err)}`);
510
+ }
511
+ }
512
+ }
513
+ /**
514
+ * Apply the D5 connect-failure policy (also covers D3 credential failures). A
515
+ * code-defined `external` datasource with `onMismatch:'fail'` auto-connected at
516
+ * boot re-throws (fail-fast, bricking boot as intended). Runtime-admin
517
+ * create/update + boot rehydration always degrade-with-warning — a UI action
518
+ * or a replica blip must never brick the running server (preserves the
519
+ * pre-ADR-0062 admin behavior). Either way the datasource is left unconnected
520
+ * with a clear message — never a silent skip.
521
+ */
522
+ handleFailure(record, status, reason, context) {
523
+ const isExternal = record.schemaMode && record.schemaMode !== "managed";
524
+ const failFast = context?.trigger === "declared-auto" && isExternal && record.external?.validation?.onMismatch === "fail";
525
+ const msg = `datasource '${record.name}': connect failed \u2014 ${reason}`;
526
+ if (failFast) {
527
+ throw new Error(
528
+ `${msg}. (schemaMode=${record.schemaMode}, validation.onMismatch='fail' \u21D2 fail-fast per ADR-0062 D5)`
529
+ );
530
+ }
531
+ this.logger?.warn?.(`${msg} \u2014 degrading (datasource left unconnected)`);
532
+ return { name: record.name, status, reason };
533
+ }
534
+ };
535
+ function toSpec(record) {
536
+ return {
537
+ name: record.name,
538
+ driver: record.driver,
539
+ config: record.config ?? {},
540
+ external: record.external,
541
+ pool: record.pool
542
+ };
543
+ }
544
+ function errMsg(err) {
545
+ return err instanceof Error ? err.message : String(err);
546
+ }
547
+
371
548
  // src/datasource-admin-service.ts
372
549
  var NAME_RE = /^[a-z_][a-z0-9_]*$/;
373
550
  var DatasourceAdminService = class {
@@ -647,6 +824,14 @@ var DatasourceAdminServicePlugin = class {
647
824
  const metadataOf = () => safeGetService2(ctx, "metadata");
648
825
  const engineOf = () => safeGetService2(ctx, "data");
649
826
  const factory = () => this.options.driverFactory ?? safeGetService2(ctx, "datasource-driver-factory");
827
+ this.connection = new DatasourceConnectionService({
828
+ factory,
829
+ engine: () => engineOf(),
830
+ secrets: { resolve: (ref) => this.options.secrets?.resolve?.(ref) ?? Promise.resolve(void 0) },
831
+ policy: this.options.connectPolicy,
832
+ logger: this.options.logger
833
+ });
834
+ ctx.registerService("datasource-connection", this.connection);
650
835
  const config = {
651
836
  probe: (input) => this.probe(factory(), input),
652
837
  listDatasourceRecords: async () => {
@@ -690,29 +875,20 @@ var DatasourceAdminServicePlugin = class {
690
875
  const objects = await metadata?.listObjects?.() ?? await metadata?.list("object") ?? [];
691
876
  return objects.filter((o) => o?.datasource === datasource).length;
692
877
  },
878
+ // Hot pool (de)registration converges on the shared
879
+ // DatasourceConnectionService (ADR-0062 D1) — one connect path for code-
880
+ // and runtime-origin datasources. `connect()` builds the driver via the
881
+ // factory, dereferences `external.credentialsRef` through the SecretBinder,
882
+ // opens the connection, and registers the live driver + datasource def.
883
+ // Runtime-admin connects always degrade-with-warning on failure (never
884
+ // fail-fast), preserving the pre-ADR-0062 admin behavior.
693
885
  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
886
+ await this.connection?.connect(record, {
887
+ context: { origin: record.origin ?? "runtime", trigger: "runtime-admin" }
711
888
  });
712
889
  },
713
890
  unregisterPool: async (name) => {
714
- const driver = engineOf()?.getDriverByName?.(name);
715
- if (typeof driver?.disconnect === "function") await driver.disconnect();
891
+ await this.connection?.disconnect(name);
716
892
  },
717
893
  logger
718
894
  };
@@ -821,15 +997,6 @@ var DatasourceAdminServicePlugin = class {
821
997
  this.service = void 0;
822
998
  }
823
999
  // --- 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
1000
  /** Probe a connection via the driver factory: build → connect → ping → close. */
834
1001
  async probe(factory, input) {
835
1002
  if (!factory) {
@@ -847,7 +1014,7 @@ var DatasourceAdminServicePlugin = class {
847
1014
  external: input.external
848
1015
  });
849
1016
  } catch (err) {
850
- return { ok: false, error: `Failed to build driver: ${errMsg(err)}` };
1017
+ return { ok: false, error: `Failed to build driver: ${errMsg2(err)}` };
851
1018
  }
852
1019
  const startedAt = monotonicNow();
853
1020
  try {
@@ -863,7 +1030,7 @@ var DatasourceAdminServicePlugin = class {
863
1030
  }
864
1031
  return { ok: true, latencyMs, ...serverVersion ? { serverVersion } : {} };
865
1032
  } catch (err) {
866
- return { ok: false, error: errMsg(err) };
1033
+ return { ok: false, error: errMsg2(err) };
867
1034
  } finally {
868
1035
  try {
869
1036
  if (typeof driver?.disconnect === "function") await driver.disconnect();
@@ -879,7 +1046,7 @@ function safeGetService2(ctx, name) {
879
1046
  return void 0;
880
1047
  }
881
1048
  }
882
- function errMsg(err) {
1049
+ function errMsg2(err) {
883
1050
  return err instanceof Error ? err.message : String(err);
884
1051
  }
885
1052
  function monotonicNow() {
@@ -1280,10 +1447,13 @@ function registerDatasourceAdminRoutes(server, ctx, basePath = "/api/v1") {
1280
1447
  export {
1281
1448
  DatasourceAdminService,
1282
1449
  DatasourceAdminServicePlugin,
1450
+ DatasourceConnectionService,
1283
1451
  ExternalDatasourceService,
1284
1452
  ExternalDatasourceServicePlugin,
1453
+ allowAllConnectPolicy,
1285
1454
  createDatasourceSecretBinder,
1286
1455
  createDefaultDatasourceDriverFactory,
1456
+ isDatasourceAddressed,
1287
1457
  parseCredentialsRef,
1288
1458
  registerDatasourceAdminRoutes,
1289
1459
  toCredentialsRef