@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/.turbo/turbo-build.log +19 -15
- package/CHANGELOG.md +69 -0
- package/dist/chunk-BI2SYWLC.cjs +9 -0
- package/dist/chunk-BI2SYWLC.cjs.map +1 -0
- package/dist/chunk-XLS4RP7B.js +9 -0
- package/dist/chunk-XLS4RP7B.js.map +1 -0
- package/dist/contracts/index.cjs +7 -1
- package/dist/contracts/index.cjs.map +1 -1
- package/dist/contracts/index.d.cts +59 -1
- package/dist/contracts/index.d.ts +59 -1
- package/dist/contracts/index.js +6 -0
- package/dist/index.cjs +269 -99
- package/dist/index.cjs.map +1 -1
- package/dist/index.d.cts +176 -4
- package/dist/index.d.ts +176 -4
- package/dist/index.js +201 -31
- package/dist/index.js.map +1 -1
- package/package.json +7 -7
- package/src/__tests__/datasource-connection-service.test.ts +294 -0
- package/src/contracts/connect-policy.ts +69 -0
- package/src/contracts/index.ts +11 -0
- package/src/datasource-admin-plugin.ts +37 -40
- package/src/datasource-admin-service.ts +2 -0
- package/src/datasource-connection-service.ts +364 -0
- package/src/index.ts +18 -0
- package/src/logger.ts +2 -0
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
|
-
|
|
695
|
-
|
|
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
|
-
|
|
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: ${
|
|
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:
|
|
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
|
|
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
|