@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.
- package/.turbo/turbo-build.log +28 -16
- package/CHANGELOG.md +100 -0
- package/dist/chunk-76HQ74MX.cjs +82 -0
- package/dist/chunk-76HQ74MX.cjs.map +1 -0
- package/dist/chunk-BI2SYWLC.cjs +9 -0
- package/dist/chunk-BI2SYWLC.cjs.map +1 -0
- package/dist/chunk-JRBGOCRJ.js +82 -0
- package/dist/chunk-JRBGOCRJ.js.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 +284 -106
- package/dist/index.cjs.map +1 -1
- package/dist/index.d.cts +270 -5
- package/dist/index.d.ts +270 -5
- package/dist/index.js +216 -38
- package/dist/index.js.map +1 -1
- package/dist/sqlite-driver-fallback-BPFQYLX7.js +11 -0
- package/dist/sqlite-driver-fallback-BPFQYLX7.js.map +1 -0
- package/dist/sqlite-driver-fallback-JX4XOICD.cjs +11 -0
- package/dist/sqlite-driver-fallback-JX4XOICD.cjs.map +1 -0
- package/package.json +8 -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/default-datasource-driver-factory.ts +26 -9
- package/src/index.ts +29 -0
- package/src/logger.ts +2 -0
- package/src/sqlite-driver-fallback.test.ts +184 -0
- 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
|
-
|
|
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
|
|
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
|
-
|
|
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: ${
|
|
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:
|
|
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
|
|
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
|
|
973
|
-
const
|
|
974
|
-
|
|
975
|
-
|
|
976
|
-
|
|
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
|