@objectstack/service-datasource 9.11.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 +88 -0
- package/README.md +34 -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 +555 -89
- package/dist/index.cjs.map +1 -1
- package/dist/index.d.cts +204 -4
- package/dist/index.d.ts +204 -4
- package/dist/index.js +497 -31
- package/dist/index.js.map +1 -1
- package/package.json +8 -8
- package/src/__tests__/admin-routes.test.ts +58 -0
- package/src/__tests__/datasource-admin-plugin.test.ts +59 -0
- package/src/__tests__/datasource-admin-service.test.ts +30 -0
- package/src/__tests__/datasource-connection-service.test.ts +294 -0
- package/src/admin-routes.ts +75 -0
- package/src/contracts/connect-policy.ts +69 -0
- package/src/contracts/index.ts +11 -0
- package/src/datasource-admin-plugin.ts +189 -43
- package/src/datasource-admin-service.ts +32 -0
- package/src/datasource-connection-service.ts +364 -0
- package/src/driver-catalog.ts +113 -0
- package/src/external-datasource-service.ts +25 -0
- package/src/index.ts +18 -0
- package/src/logger.ts +2 -0
package/dist/index.cjs
CHANGED
|
@@ -1,4 +1,8 @@
|
|
|
1
|
-
"use strict";Object.defineProperty(exports, "__esModule", {value: true}); function _interopRequireWildcard(obj) { if (obj && obj.__esModule) { return obj; } else { var newObj = {}; if (obj != null) { for (var key in obj) { if (Object.prototype.hasOwnProperty.call(obj, key)) { newObj[key] = obj[key]; } } } newObj.default = obj; return newObj; } } function _nullishCoalesce(lhs, rhsFn) { if (lhs != null) { return lhs; } else { return rhsFn(); } } async function _asyncNullishCoalesce(lhs, rhsFn) { if (lhs != null) { return lhs; } else { return await rhsFn(); } } function _optionalChain(ops) { let lastAccessLHS = undefined; let value = ops[0]; let i = 1; while (i < ops.length) { const op = ops[i]; const fn = ops[i + 1]; i += 2; if ((op === 'optionalAccess' || op === 'optionalCall') && value == null) { return undefined; } if (op === 'access' || op === 'optionalAccess') { lastAccessLHS = value; value = fn(value); } else if (op === 'call' || op === 'optionalCall') { value = fn((...args) => value.call(lastAccessLHS, ...args)); lastAccessLHS = undefined; } } return value; }
|
|
1
|
+
"use strict";Object.defineProperty(exports, "__esModule", {value: true}); function _interopRequireWildcard(obj) { if (obj && obj.__esModule) { return obj; } else { var newObj = {}; if (obj != null) { for (var key in obj) { if (Object.prototype.hasOwnProperty.call(obj, key)) { newObj[key] = obj[key]; } } } newObj.default = obj; return newObj; } } function _nullishCoalesce(lhs, rhsFn) { if (lhs != null) { return lhs; } else { return rhsFn(); } } async function _asyncNullishCoalesce(lhs, rhsFn) { if (lhs != null) { return lhs; } else { return await rhsFn(); } } function _optionalChain(ops) { let lastAccessLHS = undefined; let value = ops[0]; let i = 1; while (i < ops.length) { const op = ops[i]; const fn = ops[i + 1]; i += 2; if ((op === 'optionalAccess' || op === 'optionalCall') && value == null) { return undefined; } if (op === 'access' || op === 'optionalAccess') { lastAccessLHS = value; value = fn(value); } else if (op === 'call' || op === 'optionalCall') { value = fn((...args) => value.call(lastAccessLHS, ...args)); lastAccessLHS = undefined; } } return value; }
|
|
2
|
+
|
|
3
|
+
var _chunkBI2SYWLCcjs = require('./chunk-BI2SYWLC.cjs');
|
|
4
|
+
|
|
5
|
+
// src/external-datasource-service.ts
|
|
2
6
|
|
|
3
7
|
|
|
4
8
|
|
|
@@ -47,6 +51,28 @@ var ExternalDatasourceService = class {
|
|
|
47
51
|
}
|
|
48
52
|
return tables;
|
|
49
53
|
}
|
|
54
|
+
/**
|
|
55
|
+
* Probe a *saved* datasource by name with a live round-trip. Reuses the
|
|
56
|
+
* introspection path (driver connect + schema read) as a cheap connectivity
|
|
57
|
+
* check, so the secret is resolved through the same wired pool as the rest of
|
|
58
|
+
* the introspection surface — the caller never handles cleartext. Returns a
|
|
59
|
+
* structured result rather than throwing so the route can render ok/error
|
|
60
|
+
* uniformly. This backs the `datasource` `test_connection` action
|
|
61
|
+
* (`POST /datasources/:name/test`).
|
|
62
|
+
*/
|
|
63
|
+
async testConnection(datasource) {
|
|
64
|
+
const started = Date.now();
|
|
65
|
+
try {
|
|
66
|
+
const schema = await this.config.introspect(datasource);
|
|
67
|
+
return {
|
|
68
|
+
ok: true,
|
|
69
|
+
latencyMs: Date.now() - started,
|
|
70
|
+
tableCount: Object.keys(schema.tables).length
|
|
71
|
+
};
|
|
72
|
+
} catch (err) {
|
|
73
|
+
return { ok: false, error: err instanceof Error ? err.message : String(err) };
|
|
74
|
+
}
|
|
75
|
+
}
|
|
50
76
|
async generateObjectDraft(datasource, remoteName, opts = {}) {
|
|
51
77
|
const schema = await this.config.introspect(datasource);
|
|
52
78
|
const table = this.findTable(schema, remoteName);
|
|
@@ -346,6 +372,179 @@ function safeGetService(ctx, name) {
|
|
|
346
372
|
}
|
|
347
373
|
}
|
|
348
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 (_optionalChain([ctx, 'access', _38 => _38.objects, 'optionalAccess', _39 => _39.some, 'call', _40 => _40((o) => _optionalChain([o, 'optionalAccess', _41 => _41.datasource]) === ds.name)])) return true;
|
|
380
|
+
return false;
|
|
381
|
+
}
|
|
382
|
+
var DatasourceConnectionService = class {
|
|
383
|
+
constructor(cfg) {
|
|
384
|
+
this.cfg = cfg;
|
|
385
|
+
this.policy = _nullishCoalesce(cfg.policy, () => ( _chunkBI2SYWLCcjs.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 = _nullishCoalesce(input.objects, () => ( []));
|
|
396
|
+
const results = [];
|
|
397
|
+
for (const ds of input.datasources) {
|
|
398
|
+
if (!_optionalChain([ds, 'optionalAccess', _42 => _42.name])) continue;
|
|
399
|
+
if (ds.active === false) continue;
|
|
400
|
+
if (!isDatasourceAddressed(ds, { objects })) continue;
|
|
401
|
+
const bound = objects.filter((o) => _optionalChain([o, 'optionalAccess', _43 => _43.datasource]) === ds.name && typeof _optionalChain([o, 'optionalAccess', _44 => _44.name]) === "string").map((o) => o.name);
|
|
402
|
+
results.push(
|
|
403
|
+
await this.connect(ds, { objects: bound, context: { origin: _nullishCoalesce(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 (_optionalChain([engine, 'optionalAccess', _45 => _45.getDriverByName, 'optionalCall', _46 => _46(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
|
+
_optionalChain([this, 'access', _47 => _47.logger, 'optionalAccess', _48 => _48.info, 'optionalCall', _49 => _49(`datasource '${name}': connect denied by policy${decision.reason ? ` (${decision.reason})` : ""}`)]);
|
|
435
|
+
return { name, status: "skipped-policy", reason: decision.reason };
|
|
436
|
+
}
|
|
437
|
+
if (!factory || !_optionalChain([engine, 'optionalAccess', _50 => _50.registerDriver])) {
|
|
438
|
+
_optionalChain([this, 'access', _51 => _51.logger, 'optionalAccess', _52 => _52.debug, 'optionalCall', _53 => _53(`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 = _optionalChain([record, 'access', _54 => _54.external, 'optionalAccess', _55 => _55.credentialsRef]);
|
|
451
|
+
if (credentialsRef) {
|
|
452
|
+
const resolver = _optionalChain([this, 'access', _56 => _56.cfg, 'access', _57 => _57.secrets, 'optionalAccess', _58 => _58.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 _optionalChain([handle, 'optionalAccess', _59 => _59.connect]) === "function") await handle.connect();
|
|
478
|
+
const engineDriver = _nullishCoalesce(handle.driver, () => ( handle));
|
|
479
|
+
try {
|
|
480
|
+
engineDriver.name = name;
|
|
481
|
+
} catch (e2) {
|
|
482
|
+
}
|
|
483
|
+
engine.registerDriver(engineDriver);
|
|
484
|
+
_optionalChain([engine, 'access', _60 => _60.registerDatasourceDef, 'optionalCall', _61 => _61({
|
|
485
|
+
name,
|
|
486
|
+
schemaMode: record.schemaMode,
|
|
487
|
+
external: record.external
|
|
488
|
+
})]);
|
|
489
|
+
for (const objectName of _nullishCoalesce(opts.objects, () => ( []))) {
|
|
490
|
+
try {
|
|
491
|
+
await _optionalChain([engine, 'access', _62 => _62.syncObjectSchema, 'optionalCall', _63 => _63(objectName)]);
|
|
492
|
+
} catch (err) {
|
|
493
|
+
_optionalChain([this, 'access', _64 => _64.logger, 'optionalAccess', _65 => _65.warn, 'optionalCall', _66 => _66(`datasource '${name}': syncObjectSchema('${objectName}') failed: ${errMsg(err)}`)]);
|
|
494
|
+
}
|
|
495
|
+
}
|
|
496
|
+
_optionalChain([this, 'access', _67 => _67.logger, 'optionalAccess', _68 => _68.info, 'optionalCall', _69 => _69(`datasource '${name}': connected (driver=${record.driver}, schemaMode=${_nullishCoalesce(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 = _optionalChain([this, 'access', _70 => _70.cfg, 'access', _71 => _71.engine, 'call', _72 => _72(), 'optionalAccess', _73 => _73.getDriverByName, 'optionalCall', _74 => _74(name)]);
|
|
505
|
+
if (typeof _optionalChain([driver, 'optionalAccess', _75 => _75.disconnect]) === "function") {
|
|
506
|
+
try {
|
|
507
|
+
await driver.disconnect();
|
|
508
|
+
} catch (err) {
|
|
509
|
+
_optionalChain([this, 'access', _76 => _76.logger, 'optionalAccess', _77 => _77.warn, 'optionalCall', _78 => _78(`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 = _optionalChain([context, 'optionalAccess', _79 => _79.trigger]) === "declared-auto" && isExternal && _optionalChain([record, 'access', _80 => _80.external, 'optionalAccess', _81 => _81.validation, 'optionalAccess', _82 => _82.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
|
+
_optionalChain([this, 'access', _83 => _83.logger, 'optionalAccess', _84 => _84.warn, 'optionalCall', _85 => _85(`${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: _nullishCoalesce(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
|
+
|
|
349
548
|
// src/datasource-admin-service.ts
|
|
350
549
|
var NAME_RE = /^[a-z_][a-z0-9_]*$/;
|
|
351
550
|
var DatasourceAdminService = class {
|
|
@@ -376,22 +575,45 @@ var DatasourceAdminService = class {
|
|
|
376
575
|
origin: slot.code ? "code" : "runtime",
|
|
377
576
|
active: _nullishCoalesce(effective.active, () => ( true)),
|
|
378
577
|
status: "unvalidated",
|
|
379
|
-
..._optionalChain([slot, 'access',
|
|
578
|
+
..._optionalChain([slot, 'access', _86 => _86.code, 'optionalAccess', _87 => _87.definedIn]) ? { definedIn: slot.code.definedIn } : {},
|
|
380
579
|
...slot.code && slot.runtime ? { conflictsWithCode: true } : {}
|
|
381
580
|
});
|
|
382
581
|
}
|
|
383
582
|
return summaries;
|
|
384
583
|
}
|
|
584
|
+
/**
|
|
585
|
+
* Read one datasource's full detail for editing, with the credential stripped.
|
|
586
|
+
* Returns `config` (non-sensitive — credentials live in `sys_secret`, never in
|
|
587
|
+
* config), `origin`, and a `hasSecret` flag so the UI can show "leave blank to
|
|
588
|
+
* keep" without ever receiving the `credentialsRef` or any cleartext. Returns
|
|
589
|
+
* `undefined` when the name is unknown.
|
|
590
|
+
*/
|
|
591
|
+
async getDatasource(name) {
|
|
592
|
+
const rec = await this.config.getDatasourceRecord(name);
|
|
593
|
+
if (!rec) return void 0;
|
|
594
|
+
const hasSecret = Boolean(_optionalChain([rec, 'access', _88 => _88.external, 'optionalAccess', _89 => _89.credentialsRef]));
|
|
595
|
+
return {
|
|
596
|
+
name: rec.name,
|
|
597
|
+
label: rec.label,
|
|
598
|
+
driver: rec.driver,
|
|
599
|
+
schemaMode: _nullishCoalesce(rec.schemaMode, () => ( "managed")),
|
|
600
|
+
config: _nullishCoalesce(rec.config, () => ( {})),
|
|
601
|
+
active: _nullishCoalesce(rec.active, () => ( true)),
|
|
602
|
+
origin: rec.origin === "runtime" ? "runtime" : "code",
|
|
603
|
+
hasSecret,
|
|
604
|
+
...rec.definedIn ? { definedIn: rec.definedIn } : {}
|
|
605
|
+
};
|
|
606
|
+
}
|
|
385
607
|
async testConnection(input, secret) {
|
|
386
|
-
if (!_optionalChain([input, 'optionalAccess',
|
|
608
|
+
if (!_optionalChain([input, 'optionalAccess', _90 => _90.driver])) {
|
|
387
609
|
return { ok: false, error: "A driver is required to test a connection." };
|
|
388
610
|
}
|
|
389
|
-
const queryTimeoutMs = _optionalChain([input, 'access',
|
|
611
|
+
const queryTimeoutMs = _optionalChain([input, 'access', _91 => _91.external, 'optionalAccess', _92 => _92.queryTimeoutMs]);
|
|
390
612
|
try {
|
|
391
613
|
return await this.config.probe({
|
|
392
614
|
driver: input.driver,
|
|
393
615
|
config: _nullishCoalesce(input.config, () => ( {})),
|
|
394
|
-
secret: _optionalChain([secret, 'optionalAccess',
|
|
616
|
+
secret: _optionalChain([secret, 'optionalAccess', _93 => _93.value]),
|
|
395
617
|
external: input.external,
|
|
396
618
|
...typeof queryTimeoutMs === "number" ? { timeoutMs: queryTimeoutMs } : {}
|
|
397
619
|
});
|
|
@@ -400,7 +622,7 @@ var DatasourceAdminService = class {
|
|
|
400
622
|
}
|
|
401
623
|
}
|
|
402
624
|
async createDatasource(input, secret) {
|
|
403
|
-
this.assertValidName(_optionalChain([input, 'optionalAccess',
|
|
625
|
+
this.assertValidName(_optionalChain([input, 'optionalAccess', _94 => _94.name]));
|
|
404
626
|
if (!input.driver) throw new Error("A driver is required to create a datasource.");
|
|
405
627
|
const existing = await this.config.getDatasourceRecord(input.name);
|
|
406
628
|
if (existing) {
|
|
@@ -441,10 +663,10 @@ var DatasourceAdminService = class {
|
|
|
441
663
|
origin: "runtime"
|
|
442
664
|
};
|
|
443
665
|
if (patch.external !== void 0) {
|
|
444
|
-
merged.external = { ...patch.external, credentialsRef: _optionalChain([existing, 'access',
|
|
666
|
+
merged.external = { ...patch.external, credentialsRef: _optionalChain([existing, 'access', _95 => _95.external, 'optionalAccess', _96 => _96.credentialsRef]) };
|
|
445
667
|
}
|
|
446
668
|
if (secret) {
|
|
447
|
-
const prevRef = _optionalChain([existing, 'access',
|
|
669
|
+
const prevRef = _optionalChain([existing, 'access', _97 => _97.external, 'optionalAccess', _98 => _98.credentialsRef]);
|
|
448
670
|
const credentialsRef = await this.config.writeSecret(secret, { name });
|
|
449
671
|
merged.external = { ..._nullishCoalesce(merged.external, () => ( {})), credentialsRef };
|
|
450
672
|
if (prevRef && prevRef !== credentialsRef) await this.tryRemoveSecret(prevRef);
|
|
@@ -466,7 +688,7 @@ var DatasourceAdminService = class {
|
|
|
466
688
|
);
|
|
467
689
|
}
|
|
468
690
|
await this.config.deleteDatasourceRecord(name);
|
|
469
|
-
if (_optionalChain([existing, 'access',
|
|
691
|
+
if (_optionalChain([existing, 'access', _99 => _99.external, 'optionalAccess', _100 => _100.credentialsRef])) await this.tryRemoveSecret(existing.external.credentialsRef);
|
|
470
692
|
await this.tryUnregisterPool(name);
|
|
471
693
|
}
|
|
472
694
|
// --- internals -----------------------------------------------------------
|
|
@@ -502,29 +724,80 @@ var DatasourceAdminService = class {
|
|
|
502
724
|
}
|
|
503
725
|
async tryRegisterPool(record) {
|
|
504
726
|
try {
|
|
505
|
-
await _optionalChain([this, 'access',
|
|
727
|
+
await _optionalChain([this, 'access', _101 => _101.config, 'access', _102 => _102.registerPool, 'optionalCall', _103 => _103(record)]);
|
|
506
728
|
} catch (err) {
|
|
507
|
-
_optionalChain([this, 'access',
|
|
729
|
+
_optionalChain([this, 'access', _104 => _104.logger, 'optionalAccess', _105 => _105.warn, 'call', _106 => _106(`registerPool('${record.name}') failed`, err)]);
|
|
508
730
|
}
|
|
509
731
|
}
|
|
510
732
|
async tryUnregisterPool(name) {
|
|
511
733
|
try {
|
|
512
|
-
await _optionalChain([this, 'access',
|
|
734
|
+
await _optionalChain([this, 'access', _107 => _107.config, 'access', _108 => _108.unregisterPool, 'optionalCall', _109 => _109(name)]);
|
|
513
735
|
} catch (err) {
|
|
514
|
-
_optionalChain([this, 'access',
|
|
736
|
+
_optionalChain([this, 'access', _110 => _110.logger, 'optionalAccess', _111 => _111.warn, 'call', _112 => _112(`unregisterPool('${name}') failed`, err)]);
|
|
515
737
|
}
|
|
516
738
|
}
|
|
517
739
|
async tryRemoveSecret(credentialsRef) {
|
|
518
740
|
try {
|
|
519
|
-
await _optionalChain([this, 'access',
|
|
741
|
+
await _optionalChain([this, 'access', _113 => _113.config, 'access', _114 => _114.removeSecret, 'optionalCall', _115 => _115(credentialsRef)]);
|
|
520
742
|
} catch (err) {
|
|
521
|
-
_optionalChain([this, 'access',
|
|
743
|
+
_optionalChain([this, 'access', _116 => _116.logger, 'optionalAccess', _117 => _117.warn, 'call', _118 => _118(`removeSecret('${credentialsRef}') failed`, err)]);
|
|
522
744
|
}
|
|
523
745
|
}
|
|
524
746
|
};
|
|
525
747
|
|
|
526
748
|
// src/datasource-admin-plugin.ts
|
|
527
749
|
var _kernel = require('@objectstack/spec/kernel');
|
|
750
|
+
var DS_META_TYPE = "datasource";
|
|
751
|
+
var SYS_METADATA = "sys_metadata";
|
|
752
|
+
function newMetaId() {
|
|
753
|
+
return typeof crypto !== "undefined" && typeof crypto.randomUUID === "function" ? crypto.randomUUID() : `meta_${Date.now()}_${Math.random().toString(36).slice(2)}`;
|
|
754
|
+
}
|
|
755
|
+
async function persistDatasourceRow(engine, record) {
|
|
756
|
+
if (!_optionalChain([engine, 'optionalAccess', _119 => _119.insert]) || !engine.findOne) return;
|
|
757
|
+
const now = (/* @__PURE__ */ new Date()).toISOString();
|
|
758
|
+
const existing = await engine.findOne(SYS_METADATA, {
|
|
759
|
+
where: { type: DS_META_TYPE, name: record.name, state: "active" }
|
|
760
|
+
});
|
|
761
|
+
if (existing) {
|
|
762
|
+
await _optionalChain([engine, 'access', _120 => _120.update, 'optionalCall', _121 => _121(
|
|
763
|
+
SYS_METADATA,
|
|
764
|
+
{ metadata: JSON.stringify(record), updated_at: now, version: (existing.version || 0) + 1, state: "active" },
|
|
765
|
+
{ where: { id: existing.id } }
|
|
766
|
+
)]);
|
|
767
|
+
} else {
|
|
768
|
+
await engine.insert(SYS_METADATA, {
|
|
769
|
+
id: newMetaId(),
|
|
770
|
+
name: record.name,
|
|
771
|
+
type: DS_META_TYPE,
|
|
772
|
+
scope: "platform",
|
|
773
|
+
metadata: JSON.stringify(record),
|
|
774
|
+
state: "active",
|
|
775
|
+
version: 1,
|
|
776
|
+
created_at: now,
|
|
777
|
+
updated_at: now
|
|
778
|
+
});
|
|
779
|
+
}
|
|
780
|
+
}
|
|
781
|
+
async function deleteDatasourceRow(engine, name) {
|
|
782
|
+
if (!_optionalChain([engine, 'optionalAccess', _122 => _122.findOne])) return;
|
|
783
|
+
const existing = await engine.findOne(SYS_METADATA, { where: { type: DS_META_TYPE, name, state: "active" } });
|
|
784
|
+
if (!existing) return;
|
|
785
|
+
if (engine.delete) await engine.delete(SYS_METADATA, { where: { id: existing.id } });
|
|
786
|
+
else await _optionalChain([engine, 'access', _123 => _123.update, 'optionalCall', _124 => _124(SYS_METADATA, { state: "inactive" }, { where: { id: existing.id } })]);
|
|
787
|
+
}
|
|
788
|
+
async function loadDatasourceRows(engine) {
|
|
789
|
+
if (!_optionalChain([engine, 'optionalAccess', _125 => _125.find])) return [];
|
|
790
|
+
const rows = await engine.find(SYS_METADATA, { where: { type: DS_META_TYPE, state: "active" } });
|
|
791
|
+
const out = [];
|
|
792
|
+
for (const r of _nullishCoalesce(rows, () => ( []))) {
|
|
793
|
+
const raw = r.metadata;
|
|
794
|
+
try {
|
|
795
|
+
out.push(typeof raw === "string" ? JSON.parse(raw) : raw);
|
|
796
|
+
} catch (e3) {
|
|
797
|
+
}
|
|
798
|
+
}
|
|
799
|
+
return out;
|
|
800
|
+
}
|
|
528
801
|
var DatasourceAdminServicePlugin = class {
|
|
529
802
|
constructor(options = {}) {
|
|
530
803
|
this.name = "com.objectstack.service-datasource-admin";
|
|
@@ -551,33 +824,43 @@ var DatasourceAdminServicePlugin = class {
|
|
|
551
824
|
const metadataOf = () => safeGetService2(ctx, "metadata");
|
|
552
825
|
const engineOf = () => safeGetService2(ctx, "data");
|
|
553
826
|
const factory = () => _nullishCoalesce(this.options.driverFactory, () => ( safeGetService2(ctx, "datasource-driver-factory")));
|
|
827
|
+
this.connection = new DatasourceConnectionService({
|
|
828
|
+
factory,
|
|
829
|
+
engine: () => engineOf(),
|
|
830
|
+
secrets: { resolve: (ref) => _nullishCoalesce(_optionalChain([this, 'access', _126 => _126.options, 'access', _127 => _127.secrets, 'optionalAccess', _128 => _128.resolve, 'optionalCall', _129 => _129(ref)]), () => ( Promise.resolve(void 0))) },
|
|
831
|
+
policy: this.options.connectPolicy,
|
|
832
|
+
logger: this.options.logger
|
|
833
|
+
});
|
|
834
|
+
ctx.registerService("datasource-connection", this.connection);
|
|
554
835
|
const config = {
|
|
555
836
|
probe: (input) => this.probe(factory(), input),
|
|
556
837
|
listDatasourceRecords: async () => {
|
|
557
|
-
const rows = await _asyncNullishCoalesce(await _optionalChain([metadataOf, 'call',
|
|
838
|
+
const rows = await _asyncNullishCoalesce(await _optionalChain([metadataOf, 'call', _130 => _130(), 'optionalAccess', _131 => _131.list, 'call', _132 => _132("datasource")]), async () => ( []));
|
|
558
839
|
return rows.map((r) => ({ ...r, origin: _nullishCoalesce(r.origin, () => ( "code")) }));
|
|
559
840
|
},
|
|
560
841
|
getDatasourceRecord: async (name) => {
|
|
561
|
-
const row = await _optionalChain([metadataOf, 'call',
|
|
842
|
+
const row = await _optionalChain([metadataOf, 'call', _133 => _133(), 'optionalAccess', _134 => _134.get, 'call', _135 => _135("datasource", name)]);
|
|
562
843
|
return row ? { ...row, origin: _nullishCoalesce(row.origin, () => ( "code")) } : void 0;
|
|
563
844
|
},
|
|
564
845
|
putDatasourceRecord: async (record) => {
|
|
565
846
|
const metadata = metadataOf();
|
|
566
|
-
if (!_optionalChain([metadata, 'optionalAccess',
|
|
847
|
+
if (!_optionalChain([metadata, 'optionalAccess', _136 => _136.register])) {
|
|
567
848
|
throw new Error("Metadata service is unavailable; cannot persist datasource.");
|
|
568
849
|
}
|
|
569
850
|
await metadata.register("datasource", record.name, record);
|
|
851
|
+
await persistDatasourceRow(engineOf(), record);
|
|
570
852
|
},
|
|
571
853
|
deleteDatasourceRecord: async (name) => {
|
|
572
854
|
const metadata = metadataOf();
|
|
573
|
-
if (!_optionalChain([metadata, 'optionalAccess',
|
|
855
|
+
if (!_optionalChain([metadata, 'optionalAccess', _137 => _137.unregister])) {
|
|
574
856
|
throw new Error("Metadata service is unavailable; cannot remove datasource.");
|
|
575
857
|
}
|
|
576
858
|
await metadata.unregister("datasource", name);
|
|
859
|
+
await deleteDatasourceRow(engineOf(), name);
|
|
577
860
|
},
|
|
578
861
|
writeSecret: async (input, hint) => {
|
|
579
862
|
const binder = this.options.secrets;
|
|
580
|
-
if (!_optionalChain([binder, 'optionalAccess',
|
|
863
|
+
if (!_optionalChain([binder, 'optionalAccess', _138 => _138.bind])) {
|
|
581
864
|
throw new Error(
|
|
582
865
|
"No secret store configured: refusing to persist a datasource credential in cleartext. Wire a SecretBinder (CryptoProvider + sys_secret) into DatasourceAdminServicePlugin."
|
|
583
866
|
);
|
|
@@ -585,47 +868,97 @@ var DatasourceAdminServicePlugin = class {
|
|
|
585
868
|
return binder.bind(input, hint);
|
|
586
869
|
},
|
|
587
870
|
removeSecret: async (ref) => {
|
|
588
|
-
await _optionalChain([this, 'access',
|
|
871
|
+
await _optionalChain([this, 'access', _139 => _139.options, 'access', _140 => _140.secrets, 'optionalAccess', _141 => _141.unbind, 'optionalCall', _142 => _142(ref)]);
|
|
589
872
|
},
|
|
590
873
|
countBoundObjects: async (datasource) => {
|
|
591
874
|
const metadata = metadataOf();
|
|
592
|
-
const objects = await _asyncNullishCoalesce(await _asyncNullishCoalesce(await _optionalChain([metadata, 'optionalAccess',
|
|
593
|
-
return objects.filter((o) => _optionalChain([o, 'optionalAccess',
|
|
875
|
+
const objects = await _asyncNullishCoalesce(await _asyncNullishCoalesce(await _optionalChain([metadata, 'optionalAccess', _143 => _143.listObjects, 'optionalCall', _144 => _144()]), async () => ( await _optionalChain([metadata, 'optionalAccess', _145 => _145.list, 'call', _146 => _146("object")]))), async () => ( []));
|
|
876
|
+
return objects.filter((o) => _optionalChain([o, 'optionalAccess', _147 => _147.datasource]) === datasource).length;
|
|
594
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.
|
|
595
885
|
registerPool: async (record) => {
|
|
596
|
-
|
|
597
|
-
|
|
598
|
-
if (!f || !_optionalChain([engine, 'optionalAccess', _87 => _87.registerDriver]) || !f.supports(record.driver)) return;
|
|
599
|
-
const credentialsRef = _optionalChain([record, 'access', _88 => _88.external, 'optionalAccess', _89 => _89.credentialsRef]);
|
|
600
|
-
const secret = credentialsRef ? await _optionalChain([this, 'access', _90 => _90.options, 'access', _91 => _91.secrets, 'optionalAccess', _92 => _92.resolve, 'optionalCall', _93 => _93(credentialsRef)]) : void 0;
|
|
601
|
-
const handle = await f.create({ ...this.toSpec(record), ...secret ? { secret } : {} });
|
|
602
|
-
if (typeof _optionalChain([handle, 'optionalAccess', _94 => _94.connect]) === "function") await handle.connect();
|
|
603
|
-
const engineDriver = _nullishCoalesce(handle.driver, () => ( handle));
|
|
604
|
-
try {
|
|
605
|
-
engineDriver.name = record.name;
|
|
606
|
-
} catch (e2) {
|
|
607
|
-
}
|
|
608
|
-
engine.registerDriver(engineDriver);
|
|
609
|
-
_optionalChain([engine, 'access', _95 => _95.registerDatasourceDef, 'optionalCall', _96 => _96({
|
|
610
|
-
name: record.name,
|
|
611
|
-
schemaMode: record.schemaMode,
|
|
612
|
-
external: record.external
|
|
886
|
+
await _optionalChain([this, 'access', _148 => _148.connection, 'optionalAccess', _149 => _149.connect, 'call', _150 => _150(record, {
|
|
887
|
+
context: { origin: _nullishCoalesce(record.origin, () => ( "runtime")), trigger: "runtime-admin" }
|
|
613
888
|
})]);
|
|
614
889
|
},
|
|
615
890
|
unregisterPool: async (name) => {
|
|
616
|
-
|
|
617
|
-
if (typeof _optionalChain([driver, 'optionalAccess', _100 => _100.disconnect]) === "function") await driver.disconnect();
|
|
891
|
+
await _optionalChain([this, 'access', _151 => _151.connection, 'optionalAccess', _152 => _152.disconnect, 'call', _153 => _153(name)]);
|
|
618
892
|
},
|
|
619
893
|
logger
|
|
620
894
|
};
|
|
621
895
|
this.config = config;
|
|
622
896
|
this.service = new DatasourceAdminService(config);
|
|
623
897
|
ctx.registerService("datasource-admin", this.service);
|
|
898
|
+
try {
|
|
899
|
+
const manifest = ctx.getService("manifest");
|
|
900
|
+
if (manifest && typeof manifest.register === "function") {
|
|
901
|
+
manifest.register({
|
|
902
|
+
id: "com.objectstack.service-datasource.nav",
|
|
903
|
+
namespace: "sys",
|
|
904
|
+
version: this.version,
|
|
905
|
+
type: "plugin",
|
|
906
|
+
scope: "system",
|
|
907
|
+
name: "Datasource Navigation",
|
|
908
|
+
description: "Contributes the Datasources entry to the Setup app Integrations group.",
|
|
909
|
+
navigationContributions: [
|
|
910
|
+
{
|
|
911
|
+
app: "setup",
|
|
912
|
+
group: "group_integrations",
|
|
913
|
+
priority: 100,
|
|
914
|
+
items: [
|
|
915
|
+
{
|
|
916
|
+
id: "nav_datasources",
|
|
917
|
+
type: "url",
|
|
918
|
+
label: "Datasources",
|
|
919
|
+
url: "/apps/setup/component/metadata/resource?type=datasource",
|
|
920
|
+
icon: "database",
|
|
921
|
+
requiredPermissions: ["manage_platform_settings"]
|
|
922
|
+
}
|
|
923
|
+
]
|
|
924
|
+
}
|
|
925
|
+
]
|
|
926
|
+
});
|
|
927
|
+
}
|
|
928
|
+
} catch (err) {
|
|
929
|
+
_optionalChain([this, 'access', _154 => _154.options, 'access', _155 => _155.logger, 'optionalAccess', _156 => _156.warn, 'optionalCall', _157 => _157("datasource nav contribution skipped", err)]);
|
|
930
|
+
}
|
|
624
931
|
}
|
|
625
932
|
async start(ctx) {
|
|
933
|
+
await this.restoreRuntimeDatasources(ctx);
|
|
626
934
|
await this.rehydratePools();
|
|
627
935
|
if (this.service) await ctx.trigger("datasource-admin:ready", this.service);
|
|
628
936
|
}
|
|
937
|
+
/** Reload persisted runtime datasource rows (sys_metadata) into the registry. */
|
|
938
|
+
async restoreRuntimeDatasources(ctx) {
|
|
939
|
+
const engine = safeGetService2(ctx, "data");
|
|
940
|
+
const metadata = safeGetService2(ctx, "metadata");
|
|
941
|
+
if (!_optionalChain([engine, 'optionalAccess', _158 => _158.find]) || !_optionalChain([metadata, 'optionalAccess', _159 => _159.register])) return;
|
|
942
|
+
let rows;
|
|
943
|
+
try {
|
|
944
|
+
rows = await loadDatasourceRows(engine);
|
|
945
|
+
} catch (err) {
|
|
946
|
+
_optionalChain([this, 'access', _160 => _160.options, 'access', _161 => _161.logger, 'optionalAccess', _162 => _162.warn, 'optionalCall', _163 => _163("datasource restore: reading sys_metadata failed", err)]);
|
|
947
|
+
return;
|
|
948
|
+
}
|
|
949
|
+
let restored = 0;
|
|
950
|
+
for (const rec of rows) {
|
|
951
|
+
const name = rec.name;
|
|
952
|
+
if (!name) continue;
|
|
953
|
+
try {
|
|
954
|
+
await metadata.register("datasource", name, rec);
|
|
955
|
+
restored += 1;
|
|
956
|
+
} catch (err) {
|
|
957
|
+
_optionalChain([this, 'access', _164 => _164.options, 'access', _165 => _165.logger, 'optionalAccess', _166 => _166.warn, 'optionalCall', _167 => _167(`datasource restore: register '${name}' failed`, err)]);
|
|
958
|
+
}
|
|
959
|
+
}
|
|
960
|
+
if (restored > 0) _optionalChain([this, 'access', _168 => _168.options, 'access', _169 => _169.logger, 'optionalAccess', _170 => _170.info, 'optionalCall', _171 => _171(`datasource: restored ${restored} runtime record(s) from sys_metadata`)]);
|
|
961
|
+
}
|
|
629
962
|
/**
|
|
630
963
|
* Boot-time rehydration: list persisted runtime datasources and re-register
|
|
631
964
|
* each one's connection pool (driver build → connect → registerDriver),
|
|
@@ -637,12 +970,12 @@ var DatasourceAdminServicePlugin = class {
|
|
|
637
970
|
*/
|
|
638
971
|
async rehydratePools() {
|
|
639
972
|
const cfg = this.config;
|
|
640
|
-
if (!_optionalChain([cfg, 'optionalAccess',
|
|
973
|
+
if (!_optionalChain([cfg, 'optionalAccess', _172 => _172.registerPool]) || !cfg.listDatasourceRecords) return;
|
|
641
974
|
let records;
|
|
642
975
|
try {
|
|
643
976
|
records = await cfg.listDatasourceRecords();
|
|
644
977
|
} catch (err) {
|
|
645
|
-
_optionalChain([this, 'access',
|
|
978
|
+
_optionalChain([this, 'access', _173 => _173.options, 'access', _174 => _174.logger, 'optionalAccess', _175 => _175.warn, 'optionalCall', _176 => _176("datasource rehydrate: listing records failed", err)]);
|
|
646
979
|
return;
|
|
647
980
|
}
|
|
648
981
|
const runtime = records.filter((r) => r.origin === "runtime" && (_nullishCoalesce(r.active, () => ( true))));
|
|
@@ -653,10 +986,10 @@ var DatasourceAdminServicePlugin = class {
|
|
|
653
986
|
await cfg.registerPool(record);
|
|
654
987
|
registered++;
|
|
655
988
|
} catch (err) {
|
|
656
|
-
_optionalChain([this, 'access',
|
|
989
|
+
_optionalChain([this, 'access', _177 => _177.options, 'access', _178 => _178.logger, 'optionalAccess', _179 => _179.warn, 'optionalCall', _180 => _180(`datasource rehydrate: pool '${record.name}' failed`, err)]);
|
|
657
990
|
}
|
|
658
991
|
}
|
|
659
|
-
_optionalChain([this, 'access',
|
|
992
|
+
_optionalChain([this, 'access', _181 => _181.options, 'access', _182 => _182.logger, 'optionalAccess', _183 => _183.info, 'optionalCall', _184 => _184(
|
|
660
993
|
`Rehydrated ${registered}/${runtime.length} runtime datasource pool(s) on boot`
|
|
661
994
|
)]);
|
|
662
995
|
}
|
|
@@ -664,15 +997,6 @@ var DatasourceAdminServicePlugin = class {
|
|
|
664
997
|
this.service = void 0;
|
|
665
998
|
}
|
|
666
999
|
// --- internals -----------------------------------------------------------
|
|
667
|
-
toSpec(record) {
|
|
668
|
-
return {
|
|
669
|
-
name: record.name,
|
|
670
|
-
driver: record.driver,
|
|
671
|
-
config: _nullishCoalesce(record.config, () => ( {})),
|
|
672
|
-
external: record.external,
|
|
673
|
-
pool: record.pool
|
|
674
|
-
};
|
|
675
|
-
}
|
|
676
1000
|
/** Probe a connection via the driver factory: build → connect → ping → close. */
|
|
677
1001
|
async probe(factory, input) {
|
|
678
1002
|
if (!factory) {
|
|
@@ -690,27 +1014,27 @@ var DatasourceAdminServicePlugin = class {
|
|
|
690
1014
|
external: input.external
|
|
691
1015
|
});
|
|
692
1016
|
} catch (err) {
|
|
693
|
-
return { ok: false, error: `Failed to build driver: ${
|
|
1017
|
+
return { ok: false, error: `Failed to build driver: ${errMsg2(err)}` };
|
|
694
1018
|
}
|
|
695
1019
|
const startedAt = monotonicNow();
|
|
696
1020
|
try {
|
|
697
|
-
if (typeof _optionalChain([driver, 'optionalAccess',
|
|
698
|
-
if (typeof _optionalChain([driver, 'optionalAccess',
|
|
699
|
-
else if (typeof _optionalChain([driver, 'optionalAccess',
|
|
700
|
-
else if (typeof _optionalChain([driver, 'optionalAccess',
|
|
1021
|
+
if (typeof _optionalChain([driver, 'optionalAccess', _185 => _185.connect]) === "function") await driver.connect();
|
|
1022
|
+
if (typeof _optionalChain([driver, 'optionalAccess', _186 => _186.ping]) === "function") await driver.ping();
|
|
1023
|
+
else if (typeof _optionalChain([driver, 'optionalAccess', _187 => _187.checkHealth]) === "function") await driver.checkHealth();
|
|
1024
|
+
else if (typeof _optionalChain([driver, 'optionalAccess', _188 => _188.introspectSchema]) === "function") await driver.introspectSchema();
|
|
701
1025
|
const latencyMs = elapsedSince(startedAt);
|
|
702
1026
|
let serverVersion;
|
|
703
1027
|
try {
|
|
704
|
-
serverVersion = typeof _optionalChain([driver, 'optionalAccess',
|
|
705
|
-
} catch (
|
|
1028
|
+
serverVersion = typeof _optionalChain([driver, 'optionalAccess', _189 => _189.serverVersion]) === "function" ? await driver.serverVersion() : void 0;
|
|
1029
|
+
} catch (e4) {
|
|
706
1030
|
}
|
|
707
1031
|
return { ok: true, latencyMs, ...serverVersion ? { serverVersion } : {} };
|
|
708
1032
|
} catch (err) {
|
|
709
|
-
return { ok: false, error:
|
|
1033
|
+
return { ok: false, error: errMsg2(err) };
|
|
710
1034
|
} finally {
|
|
711
1035
|
try {
|
|
712
|
-
if (typeof _optionalChain([driver, 'optionalAccess',
|
|
713
|
-
} catch (
|
|
1036
|
+
if (typeof _optionalChain([driver, 'optionalAccess', _190 => _190.disconnect]) === "function") await driver.disconnect();
|
|
1037
|
+
} catch (e5) {
|
|
714
1038
|
}
|
|
715
1039
|
}
|
|
716
1040
|
}
|
|
@@ -718,16 +1042,16 @@ var DatasourceAdminServicePlugin = class {
|
|
|
718
1042
|
function safeGetService2(ctx, name) {
|
|
719
1043
|
try {
|
|
720
1044
|
return ctx.getService(name);
|
|
721
|
-
} catch (
|
|
1045
|
+
} catch (e6) {
|
|
722
1046
|
return void 0;
|
|
723
1047
|
}
|
|
724
1048
|
}
|
|
725
|
-
function
|
|
1049
|
+
function errMsg2(err) {
|
|
726
1050
|
return err instanceof Error ? err.message : String(err);
|
|
727
1051
|
}
|
|
728
1052
|
function monotonicNow() {
|
|
729
1053
|
const perf = globalThis.performance;
|
|
730
|
-
return typeof _optionalChain([perf, 'optionalAccess',
|
|
1054
|
+
return typeof _optionalChain([perf, 'optionalAccess', _191 => _191.now]) === "function" ? perf.now() : 0;
|
|
731
1055
|
}
|
|
732
1056
|
function elapsedSince(startedAt) {
|
|
733
1057
|
return Math.max(0, Math.round(monotonicNow() - startedAt));
|
|
@@ -752,10 +1076,10 @@ function resolveKind(driverId) {
|
|
|
752
1076
|
}
|
|
753
1077
|
function toHandle(driver, serverVersion) {
|
|
754
1078
|
return {
|
|
755
|
-
connect: typeof _optionalChain([driver, 'optionalAccess',
|
|
756
|
-
disconnect: typeof _optionalChain([driver, 'optionalAccess',
|
|
757
|
-
checkHealth: typeof _optionalChain([driver, 'optionalAccess',
|
|
758
|
-
ping: typeof _optionalChain([driver, 'optionalAccess',
|
|
1079
|
+
connect: typeof _optionalChain([driver, 'optionalAccess', _192 => _192.connect]) === "function" ? () => driver.connect() : void 0,
|
|
1080
|
+
disconnect: typeof _optionalChain([driver, 'optionalAccess', _193 => _193.disconnect]) === "function" ? () => driver.disconnect() : void 0,
|
|
1081
|
+
checkHealth: typeof _optionalChain([driver, 'optionalAccess', _194 => _194.checkHealth]) === "function" ? () => driver.checkHealth() : void 0,
|
|
1082
|
+
ping: typeof _optionalChain([driver, 'optionalAccess', _195 => _195.checkHealth]) === "function" ? () => driver.checkHealth() : void 0,
|
|
759
1083
|
...serverVersion ? { serverVersion } : {},
|
|
760
1084
|
driver
|
|
761
1085
|
};
|
|
@@ -800,7 +1124,7 @@ function createDefaultDatasourceDriverFactory() {
|
|
|
800
1124
|
if (!kind) {
|
|
801
1125
|
throw new Error(`Unsupported driver id '${spec.driver}'.`);
|
|
802
1126
|
}
|
|
803
|
-
const schemaMode = _nullishCoalesce(_optionalChain([spec, 'access',
|
|
1127
|
+
const schemaMode = _nullishCoalesce(_optionalChain([spec, 'access', _196 => _196.external, 'optionalAccess', _197 => _197.schemaMode]), () => ( _optionalChain([spec, 'access', _198 => _198.config, 'optionalAccess', _199 => _199.schemaMode])));
|
|
804
1128
|
if (kind === "postgres") {
|
|
805
1129
|
const { SqlDriver } = await Promise.resolve().then(() => _interopRequireWildcard(require("@objectstack/driver-sql")));
|
|
806
1130
|
const driver = new SqlDriver({
|
|
@@ -827,7 +1151,7 @@ function createDefaultDatasourceDriverFactory() {
|
|
|
827
1151
|
({ MongoDBDriver } = await Promise.resolve().then(() => _interopRequireWildcard(require("@objectstack/driver-mongodb"))));
|
|
828
1152
|
} catch (err) {
|
|
829
1153
|
throw new Error(
|
|
830
|
-
`mongodb driver requested but @objectstack/driver-mongodb is not installed (${_nullishCoalesce(_optionalChain([err, 'optionalAccess',
|
|
1154
|
+
`mongodb driver requested but @objectstack/driver-mongodb is not installed (${_nullishCoalesce(_optionalChain([err, 'optionalAccess', _200 => _200.message]), () => ( err))}).`
|
|
831
1155
|
);
|
|
832
1156
|
}
|
|
833
1157
|
const driver = new MongoDBDriver({ url: buildMongoUrl(spec) });
|
|
@@ -839,14 +1163,14 @@ function createDefaultDatasourceDriverFactory() {
|
|
|
839
1163
|
};
|
|
840
1164
|
}
|
|
841
1165
|
async function sqlServerVersion(driver, client) {
|
|
842
|
-
if (typeof _optionalChain([driver, 'optionalAccess',
|
|
1166
|
+
if (typeof _optionalChain([driver, 'optionalAccess', _201 => _201.execute]) !== "function") return void 0;
|
|
843
1167
|
try {
|
|
844
1168
|
const sql = client === "pg" ? "SELECT version() AS v" : "SELECT sqlite_version() AS v";
|
|
845
1169
|
const rows = await driver.execute(sql);
|
|
846
|
-
const first = Array.isArray(rows) ? rows[0] : Array.isArray(_optionalChain([rows, 'optionalAccess',
|
|
847
|
-
const v = _nullishCoalesce(_nullishCoalesce(_optionalChain([first, 'optionalAccess',
|
|
1170
|
+
const first = Array.isArray(rows) ? rows[0] : Array.isArray(_optionalChain([rows, 'optionalAccess', _202 => _202.rows])) ? rows.rows[0] : rows;
|
|
1171
|
+
const v = _nullishCoalesce(_nullishCoalesce(_optionalChain([first, 'optionalAccess', _203 => _203.v]), () => ( _optionalChain([first, 'optionalAccess', _204 => _204.version]))), () => ( _optionalChain([first, 'optionalAccess', _205 => _205["sqlite_version()"]])));
|
|
848
1172
|
return typeof v === "string" ? v : void 0;
|
|
849
|
-
} catch (
|
|
1173
|
+
} catch (e7) {
|
|
850
1174
|
return void 0;
|
|
851
1175
|
}
|
|
852
1176
|
}
|
|
@@ -857,7 +1181,7 @@ function toCredentialsRef(handleId) {
|
|
|
857
1181
|
return `${REF_PREFIX}${handleId}`;
|
|
858
1182
|
}
|
|
859
1183
|
function parseCredentialsRef(ref) {
|
|
860
|
-
return _optionalChain([ref, 'optionalAccess',
|
|
1184
|
+
return _optionalChain([ref, 'optionalAccess', _206 => _206.startsWith, 'call', _207 => _207(REF_PREFIX)]) ? ref.slice(REF_PREFIX.length) : void 0;
|
|
861
1185
|
}
|
|
862
1186
|
function createDatasourceSecretBinder(deps) {
|
|
863
1187
|
const { engine, cryptoProvider } = deps;
|
|
@@ -894,9 +1218,9 @@ function createDatasourceSecretBinder(deps) {
|
|
|
894
1218
|
// skip the tenant-audit warning (mirrors SettingsService's store).
|
|
895
1219
|
bypassTenantAudit: true
|
|
896
1220
|
});
|
|
897
|
-
const rows = _nullishCoalesce((Array.isArray(result) ? result : _optionalChain([result, 'optionalAccess',
|
|
1221
|
+
const rows = _nullishCoalesce((Array.isArray(result) ? result : _optionalChain([result, 'optionalAccess', _208 => _208.data])), () => ( []));
|
|
898
1222
|
const row = rows[0];
|
|
899
|
-
if (!_optionalChain([row, 'optionalAccess',
|
|
1223
|
+
if (!_optionalChain([row, 'optionalAccess', _209 => _209.ciphertext])) return void 0;
|
|
900
1224
|
return await cryptoProvider.decrypt(
|
|
901
1225
|
{
|
|
902
1226
|
id: row.id,
|
|
@@ -907,20 +1231,113 @@ function createDatasourceSecretBinder(deps) {
|
|
|
907
1231
|
},
|
|
908
1232
|
{ namespace: row.namespace, key: row.key }
|
|
909
1233
|
);
|
|
910
|
-
} catch (
|
|
1234
|
+
} catch (e8) {
|
|
911
1235
|
return void 0;
|
|
912
1236
|
}
|
|
913
1237
|
}
|
|
914
1238
|
};
|
|
915
1239
|
}
|
|
916
1240
|
|
|
1241
|
+
// src/driver-catalog.ts
|
|
1242
|
+
var SSL_PROP = {
|
|
1243
|
+
ssl: { type: "boolean", title: "Use SSL/TLS", default: false }
|
|
1244
|
+
};
|
|
1245
|
+
var DRIVER_CATALOG = [
|
|
1246
|
+
{
|
|
1247
|
+
id: "memory",
|
|
1248
|
+
label: "In-Memory",
|
|
1249
|
+
description: "Ephemeral in-memory driver for dev, tests, and prototyping. No connection settings.",
|
|
1250
|
+
icon: "memory-stick",
|
|
1251
|
+
configSchema: { type: "object", properties: {}, additionalProperties: false }
|
|
1252
|
+
},
|
|
1253
|
+
{
|
|
1254
|
+
id: "sqlite",
|
|
1255
|
+
label: "SQLite",
|
|
1256
|
+
description: "File-backed (or in-memory) SQL database. Great for local dev and small deployments.",
|
|
1257
|
+
icon: "database",
|
|
1258
|
+
configSchema: {
|
|
1259
|
+
type: "object",
|
|
1260
|
+
properties: {
|
|
1261
|
+
filename: {
|
|
1262
|
+
type: "string",
|
|
1263
|
+
title: "Filename",
|
|
1264
|
+
description: 'Database file path, or ":memory:" for an ephemeral in-memory database.',
|
|
1265
|
+
default: ":memory:"
|
|
1266
|
+
}
|
|
1267
|
+
},
|
|
1268
|
+
required: ["filename"],
|
|
1269
|
+
additionalProperties: false
|
|
1270
|
+
}
|
|
1271
|
+
},
|
|
1272
|
+
{
|
|
1273
|
+
id: "postgres",
|
|
1274
|
+
label: "PostgreSQL",
|
|
1275
|
+
description: "PostgreSQL connection. Supply host/port/database or a connection URL.",
|
|
1276
|
+
icon: "database",
|
|
1277
|
+
configSchema: {
|
|
1278
|
+
type: "object",
|
|
1279
|
+
properties: {
|
|
1280
|
+
url: { type: "string", title: "Connection URL", description: "postgres://user:pass@host:5432/db (overrides the fields below when set)." },
|
|
1281
|
+
host: { type: "string", title: "Host", default: "localhost" },
|
|
1282
|
+
port: { type: "number", title: "Port", default: 5432 },
|
|
1283
|
+
database: { type: "string", title: "Database" },
|
|
1284
|
+
username: { type: "string", title: "User" },
|
|
1285
|
+
password: { type: "string", title: "Password", format: "password" },
|
|
1286
|
+
schema: { type: "string", title: "Schema", default: "public" },
|
|
1287
|
+
...SSL_PROP
|
|
1288
|
+
},
|
|
1289
|
+
additionalProperties: true
|
|
1290
|
+
}
|
|
1291
|
+
},
|
|
1292
|
+
{
|
|
1293
|
+
id: "mysql",
|
|
1294
|
+
label: "MySQL / MariaDB",
|
|
1295
|
+
description: "MySQL or MariaDB connection.",
|
|
1296
|
+
icon: "database",
|
|
1297
|
+
configSchema: {
|
|
1298
|
+
type: "object",
|
|
1299
|
+
properties: {
|
|
1300
|
+
host: { type: "string", title: "Host", default: "localhost" },
|
|
1301
|
+
port: { type: "number", title: "Port", default: 3306 },
|
|
1302
|
+
database: { type: "string", title: "Database" },
|
|
1303
|
+
username: { type: "string", title: "User" },
|
|
1304
|
+
password: { type: "string", title: "Password", format: "password" },
|
|
1305
|
+
...SSL_PROP
|
|
1306
|
+
},
|
|
1307
|
+
additionalProperties: true
|
|
1308
|
+
}
|
|
1309
|
+
},
|
|
1310
|
+
{
|
|
1311
|
+
id: "mongo",
|
|
1312
|
+
label: "MongoDB",
|
|
1313
|
+
description: "MongoDB connection via a connection URI.",
|
|
1314
|
+
icon: "database",
|
|
1315
|
+
configSchema: {
|
|
1316
|
+
type: "object",
|
|
1317
|
+
properties: {
|
|
1318
|
+
url: { type: "string", title: "Connection URI", description: "mongodb://host:27017" },
|
|
1319
|
+
database: { type: "string", title: "Database" }
|
|
1320
|
+
},
|
|
1321
|
+
required: ["url"],
|
|
1322
|
+
additionalProperties: true
|
|
1323
|
+
}
|
|
1324
|
+
}
|
|
1325
|
+
];
|
|
1326
|
+
|
|
917
1327
|
// src/admin-routes.ts
|
|
918
1328
|
function registerDatasourceAdminRoutes(server, ctx, basePath = "/api/v1") {
|
|
919
1329
|
const root = `${basePath}/datasources`;
|
|
920
1330
|
const adminService = () => {
|
|
921
1331
|
try {
|
|
922
1332
|
return ctx.getService("datasource-admin");
|
|
923
|
-
} catch (
|
|
1333
|
+
} catch (e9) {
|
|
1334
|
+
return void 0;
|
|
1335
|
+
}
|
|
1336
|
+
};
|
|
1337
|
+
const externalService = () => {
|
|
1338
|
+
try {
|
|
1339
|
+
return ctx.getService("external-datasource");
|
|
1340
|
+
} catch (e10) {
|
|
924
1341
|
return void 0;
|
|
925
1342
|
}
|
|
926
1343
|
};
|
|
@@ -933,13 +1350,59 @@ function registerDatasourceAdminRoutes(server, ctx, basePath = "/api/v1") {
|
|
|
933
1350
|
};
|
|
934
1351
|
server.get(root, async (_req, res) => {
|
|
935
1352
|
const svc = adminService();
|
|
936
|
-
if (!_optionalChain([svc, 'optionalAccess',
|
|
1353
|
+
if (!_optionalChain([svc, 'optionalAccess', _210 => _210.listDatasources])) return unavailable(res);
|
|
937
1354
|
const datasources = await svc.listDatasources();
|
|
938
1355
|
res.json({ datasources });
|
|
939
1356
|
});
|
|
1357
|
+
server.get(`${root}/drivers`, async (_req, res) => {
|
|
1358
|
+
res.json({ drivers: DRIVER_CATALOG });
|
|
1359
|
+
});
|
|
1360
|
+
server.get(`${root}/:name/remote-tables`, async (req, res) => {
|
|
1361
|
+
const svc = externalService();
|
|
1362
|
+
if (!_optionalChain([svc, 'optionalAccess', _211 => _211.listRemoteTables])) return unavailable(res);
|
|
1363
|
+
try {
|
|
1364
|
+
const tables = await svc.listRemoteTables(req.params.name);
|
|
1365
|
+
res.json({ tables });
|
|
1366
|
+
} catch (err) {
|
|
1367
|
+
badRequest(res, err);
|
|
1368
|
+
}
|
|
1369
|
+
});
|
|
1370
|
+
server.get(`${root}/:name`, async (req, res) => {
|
|
1371
|
+
const svc = adminService();
|
|
1372
|
+
if (!_optionalChain([svc, 'optionalAccess', _212 => _212.getDatasource])) return unavailable(res);
|
|
1373
|
+
try {
|
|
1374
|
+
const datasource = await svc.getDatasource(req.params.name);
|
|
1375
|
+
if (!datasource) return res.status(404).json({ error: "not_found" });
|
|
1376
|
+
res.json({ datasource });
|
|
1377
|
+
} catch (err) {
|
|
1378
|
+
badRequest(res, err);
|
|
1379
|
+
}
|
|
1380
|
+
});
|
|
1381
|
+
server.post(`${root}/:name/test`, async (req, res) => {
|
|
1382
|
+
const svc = externalService();
|
|
1383
|
+
if (!_optionalChain([svc, 'optionalAccess', _213 => _213.testConnection])) return unavailable(res);
|
|
1384
|
+
try {
|
|
1385
|
+
const result = await svc.testConnection(req.params.name);
|
|
1386
|
+
res.json(result);
|
|
1387
|
+
} catch (err) {
|
|
1388
|
+
badRequest(res, err);
|
|
1389
|
+
}
|
|
1390
|
+
});
|
|
1391
|
+
server.post(`${root}/:name/object-draft`, async (req, res) => {
|
|
1392
|
+
const svc = externalService();
|
|
1393
|
+
if (!_optionalChain([svc, 'optionalAccess', _214 => _214.generateObjectDraft])) return unavailable(res);
|
|
1394
|
+
const { table, ...opts } = _nullishCoalesce(req.body, () => ( {}));
|
|
1395
|
+
if (!table) return badRequest(res, new Error('Body field "table" is required.'));
|
|
1396
|
+
try {
|
|
1397
|
+
const draft = await svc.generateObjectDraft(req.params.name, String(table), opts);
|
|
1398
|
+
res.json({ draft });
|
|
1399
|
+
} catch (err) {
|
|
1400
|
+
badRequest(res, err);
|
|
1401
|
+
}
|
|
1402
|
+
});
|
|
940
1403
|
server.post(`${root}/test`, async (req, res) => {
|
|
941
1404
|
const svc = adminService();
|
|
942
|
-
if (!_optionalChain([svc, 'optionalAccess',
|
|
1405
|
+
if (!_optionalChain([svc, 'optionalAccess', _215 => _215.testConnection])) return unavailable(res);
|
|
943
1406
|
const { draft, secret } = splitSecret(req.body);
|
|
944
1407
|
try {
|
|
945
1408
|
const result = await svc.testConnection(draft, secret);
|
|
@@ -950,7 +1413,7 @@ function registerDatasourceAdminRoutes(server, ctx, basePath = "/api/v1") {
|
|
|
950
1413
|
});
|
|
951
1414
|
server.post(root, async (req, res) => {
|
|
952
1415
|
const svc = adminService();
|
|
953
|
-
if (!_optionalChain([svc, 'optionalAccess',
|
|
1416
|
+
if (!_optionalChain([svc, 'optionalAccess', _216 => _216.createDatasource])) return unavailable(res);
|
|
954
1417
|
const { draft, secret } = splitSecret(req.body);
|
|
955
1418
|
try {
|
|
956
1419
|
const datasource = await svc.createDatasource(draft, secret);
|
|
@@ -961,7 +1424,7 @@ function registerDatasourceAdminRoutes(server, ctx, basePath = "/api/v1") {
|
|
|
961
1424
|
});
|
|
962
1425
|
server.patch(`${root}/:name`, async (req, res) => {
|
|
963
1426
|
const svc = adminService();
|
|
964
|
-
if (!_optionalChain([svc, 'optionalAccess',
|
|
1427
|
+
if (!_optionalChain([svc, 'optionalAccess', _217 => _217.updateDatasource])) return unavailable(res);
|
|
965
1428
|
const { draft, secret } = splitSecret(req.body);
|
|
966
1429
|
try {
|
|
967
1430
|
const datasource = await svc.updateDatasource(req.params.name, draft, secret);
|
|
@@ -972,7 +1435,7 @@ function registerDatasourceAdminRoutes(server, ctx, basePath = "/api/v1") {
|
|
|
972
1435
|
});
|
|
973
1436
|
server.delete(`${root}/:name`, async (req, res) => {
|
|
974
1437
|
const svc = adminService();
|
|
975
|
-
if (!_optionalChain([svc, 'optionalAccess',
|
|
1438
|
+
if (!_optionalChain([svc, 'optionalAccess', _218 => _218.removeDatasource])) return unavailable(res);
|
|
976
1439
|
try {
|
|
977
1440
|
await svc.removeDatasource(req.params.name);
|
|
978
1441
|
res.status(204).end();
|
|
@@ -991,5 +1454,8 @@ function registerDatasourceAdminRoutes(server, ctx, basePath = "/api/v1") {
|
|
|
991
1454
|
|
|
992
1455
|
|
|
993
1456
|
|
|
994
|
-
|
|
1457
|
+
|
|
1458
|
+
|
|
1459
|
+
|
|
1460
|
+
exports.DatasourceAdminService = DatasourceAdminService; exports.DatasourceAdminServicePlugin = DatasourceAdminServicePlugin; exports.DatasourceConnectionService = DatasourceConnectionService; exports.ExternalDatasourceService = ExternalDatasourceService; exports.ExternalDatasourceServicePlugin = ExternalDatasourceServicePlugin; exports.allowAllConnectPolicy = _chunkBI2SYWLCcjs.allowAllConnectPolicy; exports.createDatasourceSecretBinder = createDatasourceSecretBinder; exports.createDefaultDatasourceDriverFactory = createDefaultDatasourceDriverFactory; exports.isDatasourceAddressed = isDatasourceAddressed; exports.parseCredentialsRef = parseCredentialsRef; exports.registerDatasourceAdminRoutes = registerDatasourceAdminRoutes; exports.toCredentialsRef = toCredentialsRef;
|
|
995
1461
|
//# sourceMappingURL=index.cjs.map
|