@objectstack/service-datasource 9.11.0 → 10.0.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 +10 -10
- package/CHANGELOG.md +19 -0
- package/README.md +34 -0
- package/dist/index.cjs +359 -63
- package/dist/index.cjs.map +1 -1
- package/dist/index.d.cts +28 -0
- package/dist/index.d.ts +28 -0
- package/dist/index.js +296 -0
- 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/admin-routes.ts +75 -0
- package/src/datasource-admin-plugin.ts +152 -3
- package/src/datasource-admin-service.ts +30 -0
- package/src/driver-catalog.ts +113 -0
- package/src/external-datasource-service.ts +25 -0
package/dist/index.cjs
CHANGED
|
@@ -47,6 +47,28 @@ var ExternalDatasourceService = class {
|
|
|
47
47
|
}
|
|
48
48
|
return tables;
|
|
49
49
|
}
|
|
50
|
+
/**
|
|
51
|
+
* Probe a *saved* datasource by name with a live round-trip. Reuses the
|
|
52
|
+
* introspection path (driver connect + schema read) as a cheap connectivity
|
|
53
|
+
* check, so the secret is resolved through the same wired pool as the rest of
|
|
54
|
+
* the introspection surface — the caller never handles cleartext. Returns a
|
|
55
|
+
* structured result rather than throwing so the route can render ok/error
|
|
56
|
+
* uniformly. This backs the `datasource` `test_connection` action
|
|
57
|
+
* (`POST /datasources/:name/test`).
|
|
58
|
+
*/
|
|
59
|
+
async testConnection(datasource) {
|
|
60
|
+
const started = Date.now();
|
|
61
|
+
try {
|
|
62
|
+
const schema = await this.config.introspect(datasource);
|
|
63
|
+
return {
|
|
64
|
+
ok: true,
|
|
65
|
+
latencyMs: Date.now() - started,
|
|
66
|
+
tableCount: Object.keys(schema.tables).length
|
|
67
|
+
};
|
|
68
|
+
} catch (err) {
|
|
69
|
+
return { ok: false, error: err instanceof Error ? err.message : String(err) };
|
|
70
|
+
}
|
|
71
|
+
}
|
|
50
72
|
async generateObjectDraft(datasource, remoteName, opts = {}) {
|
|
51
73
|
const schema = await this.config.introspect(datasource);
|
|
52
74
|
const table = this.findTable(schema, remoteName);
|
|
@@ -382,16 +404,39 @@ var DatasourceAdminService = class {
|
|
|
382
404
|
}
|
|
383
405
|
return summaries;
|
|
384
406
|
}
|
|
407
|
+
/**
|
|
408
|
+
* Read one datasource's full detail for editing, with the credential stripped.
|
|
409
|
+
* Returns `config` (non-sensitive — credentials live in `sys_secret`, never in
|
|
410
|
+
* config), `origin`, and a `hasSecret` flag so the UI can show "leave blank to
|
|
411
|
+
* keep" without ever receiving the `credentialsRef` or any cleartext. Returns
|
|
412
|
+
* `undefined` when the name is unknown.
|
|
413
|
+
*/
|
|
414
|
+
async getDatasource(name) {
|
|
415
|
+
const rec = await this.config.getDatasourceRecord(name);
|
|
416
|
+
if (!rec) return void 0;
|
|
417
|
+
const hasSecret = Boolean(_optionalChain([rec, 'access', _40 => _40.external, 'optionalAccess', _41 => _41.credentialsRef]));
|
|
418
|
+
return {
|
|
419
|
+
name: rec.name,
|
|
420
|
+
label: rec.label,
|
|
421
|
+
driver: rec.driver,
|
|
422
|
+
schemaMode: _nullishCoalesce(rec.schemaMode, () => ( "managed")),
|
|
423
|
+
config: _nullishCoalesce(rec.config, () => ( {})),
|
|
424
|
+
active: _nullishCoalesce(rec.active, () => ( true)),
|
|
425
|
+
origin: rec.origin === "runtime" ? "runtime" : "code",
|
|
426
|
+
hasSecret,
|
|
427
|
+
...rec.definedIn ? { definedIn: rec.definedIn } : {}
|
|
428
|
+
};
|
|
429
|
+
}
|
|
385
430
|
async testConnection(input, secret) {
|
|
386
|
-
if (!_optionalChain([input, 'optionalAccess',
|
|
431
|
+
if (!_optionalChain([input, 'optionalAccess', _42 => _42.driver])) {
|
|
387
432
|
return { ok: false, error: "A driver is required to test a connection." };
|
|
388
433
|
}
|
|
389
|
-
const queryTimeoutMs = _optionalChain([input, 'access',
|
|
434
|
+
const queryTimeoutMs = _optionalChain([input, 'access', _43 => _43.external, 'optionalAccess', _44 => _44.queryTimeoutMs]);
|
|
390
435
|
try {
|
|
391
436
|
return await this.config.probe({
|
|
392
437
|
driver: input.driver,
|
|
393
438
|
config: _nullishCoalesce(input.config, () => ( {})),
|
|
394
|
-
secret: _optionalChain([secret, 'optionalAccess',
|
|
439
|
+
secret: _optionalChain([secret, 'optionalAccess', _45 => _45.value]),
|
|
395
440
|
external: input.external,
|
|
396
441
|
...typeof queryTimeoutMs === "number" ? { timeoutMs: queryTimeoutMs } : {}
|
|
397
442
|
});
|
|
@@ -400,7 +445,7 @@ var DatasourceAdminService = class {
|
|
|
400
445
|
}
|
|
401
446
|
}
|
|
402
447
|
async createDatasource(input, secret) {
|
|
403
|
-
this.assertValidName(_optionalChain([input, 'optionalAccess',
|
|
448
|
+
this.assertValidName(_optionalChain([input, 'optionalAccess', _46 => _46.name]));
|
|
404
449
|
if (!input.driver) throw new Error("A driver is required to create a datasource.");
|
|
405
450
|
const existing = await this.config.getDatasourceRecord(input.name);
|
|
406
451
|
if (existing) {
|
|
@@ -441,10 +486,10 @@ var DatasourceAdminService = class {
|
|
|
441
486
|
origin: "runtime"
|
|
442
487
|
};
|
|
443
488
|
if (patch.external !== void 0) {
|
|
444
|
-
merged.external = { ...patch.external, credentialsRef: _optionalChain([existing, 'access',
|
|
489
|
+
merged.external = { ...patch.external, credentialsRef: _optionalChain([existing, 'access', _47 => _47.external, 'optionalAccess', _48 => _48.credentialsRef]) };
|
|
445
490
|
}
|
|
446
491
|
if (secret) {
|
|
447
|
-
const prevRef = _optionalChain([existing, 'access',
|
|
492
|
+
const prevRef = _optionalChain([existing, 'access', _49 => _49.external, 'optionalAccess', _50 => _50.credentialsRef]);
|
|
448
493
|
const credentialsRef = await this.config.writeSecret(secret, { name });
|
|
449
494
|
merged.external = { ..._nullishCoalesce(merged.external, () => ( {})), credentialsRef };
|
|
450
495
|
if (prevRef && prevRef !== credentialsRef) await this.tryRemoveSecret(prevRef);
|
|
@@ -466,7 +511,7 @@ var DatasourceAdminService = class {
|
|
|
466
511
|
);
|
|
467
512
|
}
|
|
468
513
|
await this.config.deleteDatasourceRecord(name);
|
|
469
|
-
if (_optionalChain([existing, 'access',
|
|
514
|
+
if (_optionalChain([existing, 'access', _51 => _51.external, 'optionalAccess', _52 => _52.credentialsRef])) await this.tryRemoveSecret(existing.external.credentialsRef);
|
|
470
515
|
await this.tryUnregisterPool(name);
|
|
471
516
|
}
|
|
472
517
|
// --- internals -----------------------------------------------------------
|
|
@@ -502,29 +547,80 @@ var DatasourceAdminService = class {
|
|
|
502
547
|
}
|
|
503
548
|
async tryRegisterPool(record) {
|
|
504
549
|
try {
|
|
505
|
-
await _optionalChain([this, 'access',
|
|
550
|
+
await _optionalChain([this, 'access', _53 => _53.config, 'access', _54 => _54.registerPool, 'optionalCall', _55 => _55(record)]);
|
|
506
551
|
} catch (err) {
|
|
507
|
-
_optionalChain([this, 'access',
|
|
552
|
+
_optionalChain([this, 'access', _56 => _56.logger, 'optionalAccess', _57 => _57.warn, 'call', _58 => _58(`registerPool('${record.name}') failed`, err)]);
|
|
508
553
|
}
|
|
509
554
|
}
|
|
510
555
|
async tryUnregisterPool(name) {
|
|
511
556
|
try {
|
|
512
|
-
await _optionalChain([this, 'access',
|
|
557
|
+
await _optionalChain([this, 'access', _59 => _59.config, 'access', _60 => _60.unregisterPool, 'optionalCall', _61 => _61(name)]);
|
|
513
558
|
} catch (err) {
|
|
514
|
-
_optionalChain([this, 'access',
|
|
559
|
+
_optionalChain([this, 'access', _62 => _62.logger, 'optionalAccess', _63 => _63.warn, 'call', _64 => _64(`unregisterPool('${name}') failed`, err)]);
|
|
515
560
|
}
|
|
516
561
|
}
|
|
517
562
|
async tryRemoveSecret(credentialsRef) {
|
|
518
563
|
try {
|
|
519
|
-
await _optionalChain([this, 'access',
|
|
564
|
+
await _optionalChain([this, 'access', _65 => _65.config, 'access', _66 => _66.removeSecret, 'optionalCall', _67 => _67(credentialsRef)]);
|
|
520
565
|
} catch (err) {
|
|
521
|
-
_optionalChain([this, 'access',
|
|
566
|
+
_optionalChain([this, 'access', _68 => _68.logger, 'optionalAccess', _69 => _69.warn, 'call', _70 => _70(`removeSecret('${credentialsRef}') failed`, err)]);
|
|
522
567
|
}
|
|
523
568
|
}
|
|
524
569
|
};
|
|
525
570
|
|
|
526
571
|
// src/datasource-admin-plugin.ts
|
|
527
572
|
var _kernel = require('@objectstack/spec/kernel');
|
|
573
|
+
var DS_META_TYPE = "datasource";
|
|
574
|
+
var SYS_METADATA = "sys_metadata";
|
|
575
|
+
function newMetaId() {
|
|
576
|
+
return typeof crypto !== "undefined" && typeof crypto.randomUUID === "function" ? crypto.randomUUID() : `meta_${Date.now()}_${Math.random().toString(36).slice(2)}`;
|
|
577
|
+
}
|
|
578
|
+
async function persistDatasourceRow(engine, record) {
|
|
579
|
+
if (!_optionalChain([engine, 'optionalAccess', _71 => _71.insert]) || !engine.findOne) return;
|
|
580
|
+
const now = (/* @__PURE__ */ new Date()).toISOString();
|
|
581
|
+
const existing = await engine.findOne(SYS_METADATA, {
|
|
582
|
+
where: { type: DS_META_TYPE, name: record.name, state: "active" }
|
|
583
|
+
});
|
|
584
|
+
if (existing) {
|
|
585
|
+
await _optionalChain([engine, 'access', _72 => _72.update, 'optionalCall', _73 => _73(
|
|
586
|
+
SYS_METADATA,
|
|
587
|
+
{ metadata: JSON.stringify(record), updated_at: now, version: (existing.version || 0) + 1, state: "active" },
|
|
588
|
+
{ where: { id: existing.id } }
|
|
589
|
+
)]);
|
|
590
|
+
} else {
|
|
591
|
+
await engine.insert(SYS_METADATA, {
|
|
592
|
+
id: newMetaId(),
|
|
593
|
+
name: record.name,
|
|
594
|
+
type: DS_META_TYPE,
|
|
595
|
+
scope: "platform",
|
|
596
|
+
metadata: JSON.stringify(record),
|
|
597
|
+
state: "active",
|
|
598
|
+
version: 1,
|
|
599
|
+
created_at: now,
|
|
600
|
+
updated_at: now
|
|
601
|
+
});
|
|
602
|
+
}
|
|
603
|
+
}
|
|
604
|
+
async function deleteDatasourceRow(engine, name) {
|
|
605
|
+
if (!_optionalChain([engine, 'optionalAccess', _74 => _74.findOne])) return;
|
|
606
|
+
const existing = await engine.findOne(SYS_METADATA, { where: { type: DS_META_TYPE, name, state: "active" } });
|
|
607
|
+
if (!existing) return;
|
|
608
|
+
if (engine.delete) await engine.delete(SYS_METADATA, { where: { id: existing.id } });
|
|
609
|
+
else await _optionalChain([engine, 'access', _75 => _75.update, 'optionalCall', _76 => _76(SYS_METADATA, { state: "inactive" }, { where: { id: existing.id } })]);
|
|
610
|
+
}
|
|
611
|
+
async function loadDatasourceRows(engine) {
|
|
612
|
+
if (!_optionalChain([engine, 'optionalAccess', _77 => _77.find])) return [];
|
|
613
|
+
const rows = await engine.find(SYS_METADATA, { where: { type: DS_META_TYPE, state: "active" } });
|
|
614
|
+
const out = [];
|
|
615
|
+
for (const r of _nullishCoalesce(rows, () => ( []))) {
|
|
616
|
+
const raw = r.metadata;
|
|
617
|
+
try {
|
|
618
|
+
out.push(typeof raw === "string" ? JSON.parse(raw) : raw);
|
|
619
|
+
} catch (e2) {
|
|
620
|
+
}
|
|
621
|
+
}
|
|
622
|
+
return out;
|
|
623
|
+
}
|
|
528
624
|
var DatasourceAdminServicePlugin = class {
|
|
529
625
|
constructor(options = {}) {
|
|
530
626
|
this.name = "com.objectstack.service-datasource-admin";
|
|
@@ -554,30 +650,32 @@ var DatasourceAdminServicePlugin = class {
|
|
|
554
650
|
const config = {
|
|
555
651
|
probe: (input) => this.probe(factory(), input),
|
|
556
652
|
listDatasourceRecords: async () => {
|
|
557
|
-
const rows = await _asyncNullishCoalesce(await _optionalChain([metadataOf, 'call',
|
|
653
|
+
const rows = await _asyncNullishCoalesce(await _optionalChain([metadataOf, 'call', _78 => _78(), 'optionalAccess', _79 => _79.list, 'call', _80 => _80("datasource")]), async () => ( []));
|
|
558
654
|
return rows.map((r) => ({ ...r, origin: _nullishCoalesce(r.origin, () => ( "code")) }));
|
|
559
655
|
},
|
|
560
656
|
getDatasourceRecord: async (name) => {
|
|
561
|
-
const row = await _optionalChain([metadataOf, 'call',
|
|
657
|
+
const row = await _optionalChain([metadataOf, 'call', _81 => _81(), 'optionalAccess', _82 => _82.get, 'call', _83 => _83("datasource", name)]);
|
|
562
658
|
return row ? { ...row, origin: _nullishCoalesce(row.origin, () => ( "code")) } : void 0;
|
|
563
659
|
},
|
|
564
660
|
putDatasourceRecord: async (record) => {
|
|
565
661
|
const metadata = metadataOf();
|
|
566
|
-
if (!_optionalChain([metadata, 'optionalAccess',
|
|
662
|
+
if (!_optionalChain([metadata, 'optionalAccess', _84 => _84.register])) {
|
|
567
663
|
throw new Error("Metadata service is unavailable; cannot persist datasource.");
|
|
568
664
|
}
|
|
569
665
|
await metadata.register("datasource", record.name, record);
|
|
666
|
+
await persistDatasourceRow(engineOf(), record);
|
|
570
667
|
},
|
|
571
668
|
deleteDatasourceRecord: async (name) => {
|
|
572
669
|
const metadata = metadataOf();
|
|
573
|
-
if (!_optionalChain([metadata, 'optionalAccess',
|
|
670
|
+
if (!_optionalChain([metadata, 'optionalAccess', _85 => _85.unregister])) {
|
|
574
671
|
throw new Error("Metadata service is unavailable; cannot remove datasource.");
|
|
575
672
|
}
|
|
576
673
|
await metadata.unregister("datasource", name);
|
|
674
|
+
await deleteDatasourceRow(engineOf(), name);
|
|
577
675
|
},
|
|
578
676
|
writeSecret: async (input, hint) => {
|
|
579
677
|
const binder = this.options.secrets;
|
|
580
|
-
if (!_optionalChain([binder, 'optionalAccess',
|
|
678
|
+
if (!_optionalChain([binder, 'optionalAccess', _86 => _86.bind])) {
|
|
581
679
|
throw new Error(
|
|
582
680
|
"No secret store configured: refusing to persist a datasource credential in cleartext. Wire a SecretBinder (CryptoProvider + sys_secret) into DatasourceAdminServicePlugin."
|
|
583
681
|
);
|
|
@@ -585,47 +683,106 @@ var DatasourceAdminServicePlugin = class {
|
|
|
585
683
|
return binder.bind(input, hint);
|
|
586
684
|
},
|
|
587
685
|
removeSecret: async (ref) => {
|
|
588
|
-
await _optionalChain([this, 'access',
|
|
686
|
+
await _optionalChain([this, 'access', _87 => _87.options, 'access', _88 => _88.secrets, 'optionalAccess', _89 => _89.unbind, 'optionalCall', _90 => _90(ref)]);
|
|
589
687
|
},
|
|
590
688
|
countBoundObjects: async (datasource) => {
|
|
591
689
|
const metadata = metadataOf();
|
|
592
|
-
const objects = await _asyncNullishCoalesce(await _asyncNullishCoalesce(await _optionalChain([metadata, 'optionalAccess',
|
|
593
|
-
return objects.filter((o) => _optionalChain([o, 'optionalAccess',
|
|
690
|
+
const objects = await _asyncNullishCoalesce(await _asyncNullishCoalesce(await _optionalChain([metadata, 'optionalAccess', _91 => _91.listObjects, 'optionalCall', _92 => _92()]), async () => ( await _optionalChain([metadata, 'optionalAccess', _93 => _93.list, 'call', _94 => _94("object")]))), async () => ( []));
|
|
691
|
+
return objects.filter((o) => _optionalChain([o, 'optionalAccess', _95 => _95.datasource]) === datasource).length;
|
|
594
692
|
},
|
|
595
693
|
registerPool: async (record) => {
|
|
596
694
|
const f = factory();
|
|
597
695
|
const engine = engineOf();
|
|
598
|
-
if (!f || !_optionalChain([engine, 'optionalAccess',
|
|
599
|
-
const credentialsRef = _optionalChain([record, 'access',
|
|
600
|
-
const secret = credentialsRef ? await _optionalChain([this, 'access',
|
|
696
|
+
if (!f || !_optionalChain([engine, 'optionalAccess', _96 => _96.registerDriver]) || !f.supports(record.driver)) return;
|
|
697
|
+
const credentialsRef = _optionalChain([record, 'access', _97 => _97.external, 'optionalAccess', _98 => _98.credentialsRef]);
|
|
698
|
+
const secret = credentialsRef ? await _optionalChain([this, 'access', _99 => _99.options, 'access', _100 => _100.secrets, 'optionalAccess', _101 => _101.resolve, 'optionalCall', _102 => _102(credentialsRef)]) : void 0;
|
|
601
699
|
const handle = await f.create({ ...this.toSpec(record), ...secret ? { secret } : {} });
|
|
602
|
-
if (typeof _optionalChain([handle, 'optionalAccess',
|
|
700
|
+
if (typeof _optionalChain([handle, 'optionalAccess', _103 => _103.connect]) === "function") await handle.connect();
|
|
603
701
|
const engineDriver = _nullishCoalesce(handle.driver, () => ( handle));
|
|
604
702
|
try {
|
|
605
703
|
engineDriver.name = record.name;
|
|
606
|
-
} catch (
|
|
704
|
+
} catch (e3) {
|
|
607
705
|
}
|
|
608
706
|
engine.registerDriver(engineDriver);
|
|
609
|
-
_optionalChain([engine, 'access',
|
|
707
|
+
_optionalChain([engine, 'access', _104 => _104.registerDatasourceDef, 'optionalCall', _105 => _105({
|
|
610
708
|
name: record.name,
|
|
611
709
|
schemaMode: record.schemaMode,
|
|
612
710
|
external: record.external
|
|
613
711
|
})]);
|
|
614
712
|
},
|
|
615
713
|
unregisterPool: async (name) => {
|
|
616
|
-
const driver = _optionalChain([engineOf, 'call',
|
|
617
|
-
if (typeof _optionalChain([driver, 'optionalAccess',
|
|
714
|
+
const driver = _optionalChain([engineOf, 'call', _106 => _106(), 'optionalAccess', _107 => _107.getDriverByName, 'optionalCall', _108 => _108(name)]);
|
|
715
|
+
if (typeof _optionalChain([driver, 'optionalAccess', _109 => _109.disconnect]) === "function") await driver.disconnect();
|
|
618
716
|
},
|
|
619
717
|
logger
|
|
620
718
|
};
|
|
621
719
|
this.config = config;
|
|
622
720
|
this.service = new DatasourceAdminService(config);
|
|
623
721
|
ctx.registerService("datasource-admin", this.service);
|
|
722
|
+
try {
|
|
723
|
+
const manifest = ctx.getService("manifest");
|
|
724
|
+
if (manifest && typeof manifest.register === "function") {
|
|
725
|
+
manifest.register({
|
|
726
|
+
id: "com.objectstack.service-datasource.nav",
|
|
727
|
+
namespace: "sys",
|
|
728
|
+
version: this.version,
|
|
729
|
+
type: "plugin",
|
|
730
|
+
scope: "system",
|
|
731
|
+
name: "Datasource Navigation",
|
|
732
|
+
description: "Contributes the Datasources entry to the Setup app Integrations group.",
|
|
733
|
+
navigationContributions: [
|
|
734
|
+
{
|
|
735
|
+
app: "setup",
|
|
736
|
+
group: "group_integrations",
|
|
737
|
+
priority: 100,
|
|
738
|
+
items: [
|
|
739
|
+
{
|
|
740
|
+
id: "nav_datasources",
|
|
741
|
+
type: "url",
|
|
742
|
+
label: "Datasources",
|
|
743
|
+
url: "/apps/setup/component/metadata/resource?type=datasource",
|
|
744
|
+
icon: "database",
|
|
745
|
+
requiredPermissions: ["manage_platform_settings"]
|
|
746
|
+
}
|
|
747
|
+
]
|
|
748
|
+
}
|
|
749
|
+
]
|
|
750
|
+
});
|
|
751
|
+
}
|
|
752
|
+
} catch (err) {
|
|
753
|
+
_optionalChain([this, 'access', _110 => _110.options, 'access', _111 => _111.logger, 'optionalAccess', _112 => _112.warn, 'optionalCall', _113 => _113("datasource nav contribution skipped", err)]);
|
|
754
|
+
}
|
|
624
755
|
}
|
|
625
756
|
async start(ctx) {
|
|
757
|
+
await this.restoreRuntimeDatasources(ctx);
|
|
626
758
|
await this.rehydratePools();
|
|
627
759
|
if (this.service) await ctx.trigger("datasource-admin:ready", this.service);
|
|
628
760
|
}
|
|
761
|
+
/** Reload persisted runtime datasource rows (sys_metadata) into the registry. */
|
|
762
|
+
async restoreRuntimeDatasources(ctx) {
|
|
763
|
+
const engine = safeGetService2(ctx, "data");
|
|
764
|
+
const metadata = safeGetService2(ctx, "metadata");
|
|
765
|
+
if (!_optionalChain([engine, 'optionalAccess', _114 => _114.find]) || !_optionalChain([metadata, 'optionalAccess', _115 => _115.register])) return;
|
|
766
|
+
let rows;
|
|
767
|
+
try {
|
|
768
|
+
rows = await loadDatasourceRows(engine);
|
|
769
|
+
} catch (err) {
|
|
770
|
+
_optionalChain([this, 'access', _116 => _116.options, 'access', _117 => _117.logger, 'optionalAccess', _118 => _118.warn, 'optionalCall', _119 => _119("datasource restore: reading sys_metadata failed", err)]);
|
|
771
|
+
return;
|
|
772
|
+
}
|
|
773
|
+
let restored = 0;
|
|
774
|
+
for (const rec of rows) {
|
|
775
|
+
const name = rec.name;
|
|
776
|
+
if (!name) continue;
|
|
777
|
+
try {
|
|
778
|
+
await metadata.register("datasource", name, rec);
|
|
779
|
+
restored += 1;
|
|
780
|
+
} catch (err) {
|
|
781
|
+
_optionalChain([this, 'access', _120 => _120.options, 'access', _121 => _121.logger, 'optionalAccess', _122 => _122.warn, 'optionalCall', _123 => _123(`datasource restore: register '${name}' failed`, err)]);
|
|
782
|
+
}
|
|
783
|
+
}
|
|
784
|
+
if (restored > 0) _optionalChain([this, 'access', _124 => _124.options, 'access', _125 => _125.logger, 'optionalAccess', _126 => _126.info, 'optionalCall', _127 => _127(`datasource: restored ${restored} runtime record(s) from sys_metadata`)]);
|
|
785
|
+
}
|
|
629
786
|
/**
|
|
630
787
|
* Boot-time rehydration: list persisted runtime datasources and re-register
|
|
631
788
|
* each one's connection pool (driver build → connect → registerDriver),
|
|
@@ -637,12 +794,12 @@ var DatasourceAdminServicePlugin = class {
|
|
|
637
794
|
*/
|
|
638
795
|
async rehydratePools() {
|
|
639
796
|
const cfg = this.config;
|
|
640
|
-
if (!_optionalChain([cfg, 'optionalAccess',
|
|
797
|
+
if (!_optionalChain([cfg, 'optionalAccess', _128 => _128.registerPool]) || !cfg.listDatasourceRecords) return;
|
|
641
798
|
let records;
|
|
642
799
|
try {
|
|
643
800
|
records = await cfg.listDatasourceRecords();
|
|
644
801
|
} catch (err) {
|
|
645
|
-
_optionalChain([this, 'access',
|
|
802
|
+
_optionalChain([this, 'access', _129 => _129.options, 'access', _130 => _130.logger, 'optionalAccess', _131 => _131.warn, 'optionalCall', _132 => _132("datasource rehydrate: listing records failed", err)]);
|
|
646
803
|
return;
|
|
647
804
|
}
|
|
648
805
|
const runtime = records.filter((r) => r.origin === "runtime" && (_nullishCoalesce(r.active, () => ( true))));
|
|
@@ -653,10 +810,10 @@ var DatasourceAdminServicePlugin = class {
|
|
|
653
810
|
await cfg.registerPool(record);
|
|
654
811
|
registered++;
|
|
655
812
|
} catch (err) {
|
|
656
|
-
_optionalChain([this, 'access',
|
|
813
|
+
_optionalChain([this, 'access', _133 => _133.options, 'access', _134 => _134.logger, 'optionalAccess', _135 => _135.warn, 'optionalCall', _136 => _136(`datasource rehydrate: pool '${record.name}' failed`, err)]);
|
|
657
814
|
}
|
|
658
815
|
}
|
|
659
|
-
_optionalChain([this, 'access',
|
|
816
|
+
_optionalChain([this, 'access', _137 => _137.options, 'access', _138 => _138.logger, 'optionalAccess', _139 => _139.info, 'optionalCall', _140 => _140(
|
|
660
817
|
`Rehydrated ${registered}/${runtime.length} runtime datasource pool(s) on boot`
|
|
661
818
|
)]);
|
|
662
819
|
}
|
|
@@ -694,23 +851,23 @@ var DatasourceAdminServicePlugin = class {
|
|
|
694
851
|
}
|
|
695
852
|
const startedAt = monotonicNow();
|
|
696
853
|
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',
|
|
854
|
+
if (typeof _optionalChain([driver, 'optionalAccess', _141 => _141.connect]) === "function") await driver.connect();
|
|
855
|
+
if (typeof _optionalChain([driver, 'optionalAccess', _142 => _142.ping]) === "function") await driver.ping();
|
|
856
|
+
else if (typeof _optionalChain([driver, 'optionalAccess', _143 => _143.checkHealth]) === "function") await driver.checkHealth();
|
|
857
|
+
else if (typeof _optionalChain([driver, 'optionalAccess', _144 => _144.introspectSchema]) === "function") await driver.introspectSchema();
|
|
701
858
|
const latencyMs = elapsedSince(startedAt);
|
|
702
859
|
let serverVersion;
|
|
703
860
|
try {
|
|
704
|
-
serverVersion = typeof _optionalChain([driver, 'optionalAccess',
|
|
705
|
-
} catch (
|
|
861
|
+
serverVersion = typeof _optionalChain([driver, 'optionalAccess', _145 => _145.serverVersion]) === "function" ? await driver.serverVersion() : void 0;
|
|
862
|
+
} catch (e4) {
|
|
706
863
|
}
|
|
707
864
|
return { ok: true, latencyMs, ...serverVersion ? { serverVersion } : {} };
|
|
708
865
|
} catch (err) {
|
|
709
866
|
return { ok: false, error: errMsg(err) };
|
|
710
867
|
} finally {
|
|
711
868
|
try {
|
|
712
|
-
if (typeof _optionalChain([driver, 'optionalAccess',
|
|
713
|
-
} catch (
|
|
869
|
+
if (typeof _optionalChain([driver, 'optionalAccess', _146 => _146.disconnect]) === "function") await driver.disconnect();
|
|
870
|
+
} catch (e5) {
|
|
714
871
|
}
|
|
715
872
|
}
|
|
716
873
|
}
|
|
@@ -718,7 +875,7 @@ var DatasourceAdminServicePlugin = class {
|
|
|
718
875
|
function safeGetService2(ctx, name) {
|
|
719
876
|
try {
|
|
720
877
|
return ctx.getService(name);
|
|
721
|
-
} catch (
|
|
878
|
+
} catch (e6) {
|
|
722
879
|
return void 0;
|
|
723
880
|
}
|
|
724
881
|
}
|
|
@@ -727,7 +884,7 @@ function errMsg(err) {
|
|
|
727
884
|
}
|
|
728
885
|
function monotonicNow() {
|
|
729
886
|
const perf = globalThis.performance;
|
|
730
|
-
return typeof _optionalChain([perf, 'optionalAccess',
|
|
887
|
+
return typeof _optionalChain([perf, 'optionalAccess', _147 => _147.now]) === "function" ? perf.now() : 0;
|
|
731
888
|
}
|
|
732
889
|
function elapsedSince(startedAt) {
|
|
733
890
|
return Math.max(0, Math.round(monotonicNow() - startedAt));
|
|
@@ -752,10 +909,10 @@ function resolveKind(driverId) {
|
|
|
752
909
|
}
|
|
753
910
|
function toHandle(driver, serverVersion) {
|
|
754
911
|
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',
|
|
912
|
+
connect: typeof _optionalChain([driver, 'optionalAccess', _148 => _148.connect]) === "function" ? () => driver.connect() : void 0,
|
|
913
|
+
disconnect: typeof _optionalChain([driver, 'optionalAccess', _149 => _149.disconnect]) === "function" ? () => driver.disconnect() : void 0,
|
|
914
|
+
checkHealth: typeof _optionalChain([driver, 'optionalAccess', _150 => _150.checkHealth]) === "function" ? () => driver.checkHealth() : void 0,
|
|
915
|
+
ping: typeof _optionalChain([driver, 'optionalAccess', _151 => _151.checkHealth]) === "function" ? () => driver.checkHealth() : void 0,
|
|
759
916
|
...serverVersion ? { serverVersion } : {},
|
|
760
917
|
driver
|
|
761
918
|
};
|
|
@@ -800,7 +957,7 @@ function createDefaultDatasourceDriverFactory() {
|
|
|
800
957
|
if (!kind) {
|
|
801
958
|
throw new Error(`Unsupported driver id '${spec.driver}'.`);
|
|
802
959
|
}
|
|
803
|
-
const schemaMode = _nullishCoalesce(_optionalChain([spec, 'access',
|
|
960
|
+
const schemaMode = _nullishCoalesce(_optionalChain([spec, 'access', _152 => _152.external, 'optionalAccess', _153 => _153.schemaMode]), () => ( _optionalChain([spec, 'access', _154 => _154.config, 'optionalAccess', _155 => _155.schemaMode])));
|
|
804
961
|
if (kind === "postgres") {
|
|
805
962
|
const { SqlDriver } = await Promise.resolve().then(() => _interopRequireWildcard(require("@objectstack/driver-sql")));
|
|
806
963
|
const driver = new SqlDriver({
|
|
@@ -827,7 +984,7 @@ function createDefaultDatasourceDriverFactory() {
|
|
|
827
984
|
({ MongoDBDriver } = await Promise.resolve().then(() => _interopRequireWildcard(require("@objectstack/driver-mongodb"))));
|
|
828
985
|
} catch (err) {
|
|
829
986
|
throw new Error(
|
|
830
|
-
`mongodb driver requested but @objectstack/driver-mongodb is not installed (${_nullishCoalesce(_optionalChain([err, 'optionalAccess',
|
|
987
|
+
`mongodb driver requested but @objectstack/driver-mongodb is not installed (${_nullishCoalesce(_optionalChain([err, 'optionalAccess', _156 => _156.message]), () => ( err))}).`
|
|
831
988
|
);
|
|
832
989
|
}
|
|
833
990
|
const driver = new MongoDBDriver({ url: buildMongoUrl(spec) });
|
|
@@ -839,14 +996,14 @@ function createDefaultDatasourceDriverFactory() {
|
|
|
839
996
|
};
|
|
840
997
|
}
|
|
841
998
|
async function sqlServerVersion(driver, client) {
|
|
842
|
-
if (typeof _optionalChain([driver, 'optionalAccess',
|
|
999
|
+
if (typeof _optionalChain([driver, 'optionalAccess', _157 => _157.execute]) !== "function") return void 0;
|
|
843
1000
|
try {
|
|
844
1001
|
const sql = client === "pg" ? "SELECT version() AS v" : "SELECT sqlite_version() AS v";
|
|
845
1002
|
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',
|
|
1003
|
+
const first = Array.isArray(rows) ? rows[0] : Array.isArray(_optionalChain([rows, 'optionalAccess', _158 => _158.rows])) ? rows.rows[0] : rows;
|
|
1004
|
+
const v = _nullishCoalesce(_nullishCoalesce(_optionalChain([first, 'optionalAccess', _159 => _159.v]), () => ( _optionalChain([first, 'optionalAccess', _160 => _160.version]))), () => ( _optionalChain([first, 'optionalAccess', _161 => _161["sqlite_version()"]])));
|
|
848
1005
|
return typeof v === "string" ? v : void 0;
|
|
849
|
-
} catch (
|
|
1006
|
+
} catch (e7) {
|
|
850
1007
|
return void 0;
|
|
851
1008
|
}
|
|
852
1009
|
}
|
|
@@ -857,7 +1014,7 @@ function toCredentialsRef(handleId) {
|
|
|
857
1014
|
return `${REF_PREFIX}${handleId}`;
|
|
858
1015
|
}
|
|
859
1016
|
function parseCredentialsRef(ref) {
|
|
860
|
-
return _optionalChain([ref, 'optionalAccess',
|
|
1017
|
+
return _optionalChain([ref, 'optionalAccess', _162 => _162.startsWith, 'call', _163 => _163(REF_PREFIX)]) ? ref.slice(REF_PREFIX.length) : void 0;
|
|
861
1018
|
}
|
|
862
1019
|
function createDatasourceSecretBinder(deps) {
|
|
863
1020
|
const { engine, cryptoProvider } = deps;
|
|
@@ -894,9 +1051,9 @@ function createDatasourceSecretBinder(deps) {
|
|
|
894
1051
|
// skip the tenant-audit warning (mirrors SettingsService's store).
|
|
895
1052
|
bypassTenantAudit: true
|
|
896
1053
|
});
|
|
897
|
-
const rows = _nullishCoalesce((Array.isArray(result) ? result : _optionalChain([result, 'optionalAccess',
|
|
1054
|
+
const rows = _nullishCoalesce((Array.isArray(result) ? result : _optionalChain([result, 'optionalAccess', _164 => _164.data])), () => ( []));
|
|
898
1055
|
const row = rows[0];
|
|
899
|
-
if (!_optionalChain([row, 'optionalAccess',
|
|
1056
|
+
if (!_optionalChain([row, 'optionalAccess', _165 => _165.ciphertext])) return void 0;
|
|
900
1057
|
return await cryptoProvider.decrypt(
|
|
901
1058
|
{
|
|
902
1059
|
id: row.id,
|
|
@@ -907,20 +1064,113 @@ function createDatasourceSecretBinder(deps) {
|
|
|
907
1064
|
},
|
|
908
1065
|
{ namespace: row.namespace, key: row.key }
|
|
909
1066
|
);
|
|
910
|
-
} catch (
|
|
1067
|
+
} catch (e8) {
|
|
911
1068
|
return void 0;
|
|
912
1069
|
}
|
|
913
1070
|
}
|
|
914
1071
|
};
|
|
915
1072
|
}
|
|
916
1073
|
|
|
1074
|
+
// src/driver-catalog.ts
|
|
1075
|
+
var SSL_PROP = {
|
|
1076
|
+
ssl: { type: "boolean", title: "Use SSL/TLS", default: false }
|
|
1077
|
+
};
|
|
1078
|
+
var DRIVER_CATALOG = [
|
|
1079
|
+
{
|
|
1080
|
+
id: "memory",
|
|
1081
|
+
label: "In-Memory",
|
|
1082
|
+
description: "Ephemeral in-memory driver for dev, tests, and prototyping. No connection settings.",
|
|
1083
|
+
icon: "memory-stick",
|
|
1084
|
+
configSchema: { type: "object", properties: {}, additionalProperties: false }
|
|
1085
|
+
},
|
|
1086
|
+
{
|
|
1087
|
+
id: "sqlite",
|
|
1088
|
+
label: "SQLite",
|
|
1089
|
+
description: "File-backed (or in-memory) SQL database. Great for local dev and small deployments.",
|
|
1090
|
+
icon: "database",
|
|
1091
|
+
configSchema: {
|
|
1092
|
+
type: "object",
|
|
1093
|
+
properties: {
|
|
1094
|
+
filename: {
|
|
1095
|
+
type: "string",
|
|
1096
|
+
title: "Filename",
|
|
1097
|
+
description: 'Database file path, or ":memory:" for an ephemeral in-memory database.',
|
|
1098
|
+
default: ":memory:"
|
|
1099
|
+
}
|
|
1100
|
+
},
|
|
1101
|
+
required: ["filename"],
|
|
1102
|
+
additionalProperties: false
|
|
1103
|
+
}
|
|
1104
|
+
},
|
|
1105
|
+
{
|
|
1106
|
+
id: "postgres",
|
|
1107
|
+
label: "PostgreSQL",
|
|
1108
|
+
description: "PostgreSQL connection. Supply host/port/database or a connection URL.",
|
|
1109
|
+
icon: "database",
|
|
1110
|
+
configSchema: {
|
|
1111
|
+
type: "object",
|
|
1112
|
+
properties: {
|
|
1113
|
+
url: { type: "string", title: "Connection URL", description: "postgres://user:pass@host:5432/db (overrides the fields below when set)." },
|
|
1114
|
+
host: { type: "string", title: "Host", default: "localhost" },
|
|
1115
|
+
port: { type: "number", title: "Port", default: 5432 },
|
|
1116
|
+
database: { type: "string", title: "Database" },
|
|
1117
|
+
username: { type: "string", title: "User" },
|
|
1118
|
+
password: { type: "string", title: "Password", format: "password" },
|
|
1119
|
+
schema: { type: "string", title: "Schema", default: "public" },
|
|
1120
|
+
...SSL_PROP
|
|
1121
|
+
},
|
|
1122
|
+
additionalProperties: true
|
|
1123
|
+
}
|
|
1124
|
+
},
|
|
1125
|
+
{
|
|
1126
|
+
id: "mysql",
|
|
1127
|
+
label: "MySQL / MariaDB",
|
|
1128
|
+
description: "MySQL or MariaDB connection.",
|
|
1129
|
+
icon: "database",
|
|
1130
|
+
configSchema: {
|
|
1131
|
+
type: "object",
|
|
1132
|
+
properties: {
|
|
1133
|
+
host: { type: "string", title: "Host", default: "localhost" },
|
|
1134
|
+
port: { type: "number", title: "Port", default: 3306 },
|
|
1135
|
+
database: { type: "string", title: "Database" },
|
|
1136
|
+
username: { type: "string", title: "User" },
|
|
1137
|
+
password: { type: "string", title: "Password", format: "password" },
|
|
1138
|
+
...SSL_PROP
|
|
1139
|
+
},
|
|
1140
|
+
additionalProperties: true
|
|
1141
|
+
}
|
|
1142
|
+
},
|
|
1143
|
+
{
|
|
1144
|
+
id: "mongo",
|
|
1145
|
+
label: "MongoDB",
|
|
1146
|
+
description: "MongoDB connection via a connection URI.",
|
|
1147
|
+
icon: "database",
|
|
1148
|
+
configSchema: {
|
|
1149
|
+
type: "object",
|
|
1150
|
+
properties: {
|
|
1151
|
+
url: { type: "string", title: "Connection URI", description: "mongodb://host:27017" },
|
|
1152
|
+
database: { type: "string", title: "Database" }
|
|
1153
|
+
},
|
|
1154
|
+
required: ["url"],
|
|
1155
|
+
additionalProperties: true
|
|
1156
|
+
}
|
|
1157
|
+
}
|
|
1158
|
+
];
|
|
1159
|
+
|
|
917
1160
|
// src/admin-routes.ts
|
|
918
1161
|
function registerDatasourceAdminRoutes(server, ctx, basePath = "/api/v1") {
|
|
919
1162
|
const root = `${basePath}/datasources`;
|
|
920
1163
|
const adminService = () => {
|
|
921
1164
|
try {
|
|
922
1165
|
return ctx.getService("datasource-admin");
|
|
923
|
-
} catch (
|
|
1166
|
+
} catch (e9) {
|
|
1167
|
+
return void 0;
|
|
1168
|
+
}
|
|
1169
|
+
};
|
|
1170
|
+
const externalService = () => {
|
|
1171
|
+
try {
|
|
1172
|
+
return ctx.getService("external-datasource");
|
|
1173
|
+
} catch (e10) {
|
|
924
1174
|
return void 0;
|
|
925
1175
|
}
|
|
926
1176
|
};
|
|
@@ -933,13 +1183,59 @@ function registerDatasourceAdminRoutes(server, ctx, basePath = "/api/v1") {
|
|
|
933
1183
|
};
|
|
934
1184
|
server.get(root, async (_req, res) => {
|
|
935
1185
|
const svc = adminService();
|
|
936
|
-
if (!_optionalChain([svc, 'optionalAccess',
|
|
1186
|
+
if (!_optionalChain([svc, 'optionalAccess', _166 => _166.listDatasources])) return unavailable(res);
|
|
937
1187
|
const datasources = await svc.listDatasources();
|
|
938
1188
|
res.json({ datasources });
|
|
939
1189
|
});
|
|
1190
|
+
server.get(`${root}/drivers`, async (_req, res) => {
|
|
1191
|
+
res.json({ drivers: DRIVER_CATALOG });
|
|
1192
|
+
});
|
|
1193
|
+
server.get(`${root}/:name/remote-tables`, async (req, res) => {
|
|
1194
|
+
const svc = externalService();
|
|
1195
|
+
if (!_optionalChain([svc, 'optionalAccess', _167 => _167.listRemoteTables])) return unavailable(res);
|
|
1196
|
+
try {
|
|
1197
|
+
const tables = await svc.listRemoteTables(req.params.name);
|
|
1198
|
+
res.json({ tables });
|
|
1199
|
+
} catch (err) {
|
|
1200
|
+
badRequest(res, err);
|
|
1201
|
+
}
|
|
1202
|
+
});
|
|
1203
|
+
server.get(`${root}/:name`, async (req, res) => {
|
|
1204
|
+
const svc = adminService();
|
|
1205
|
+
if (!_optionalChain([svc, 'optionalAccess', _168 => _168.getDatasource])) return unavailable(res);
|
|
1206
|
+
try {
|
|
1207
|
+
const datasource = await svc.getDatasource(req.params.name);
|
|
1208
|
+
if (!datasource) return res.status(404).json({ error: "not_found" });
|
|
1209
|
+
res.json({ datasource });
|
|
1210
|
+
} catch (err) {
|
|
1211
|
+
badRequest(res, err);
|
|
1212
|
+
}
|
|
1213
|
+
});
|
|
1214
|
+
server.post(`${root}/:name/test`, async (req, res) => {
|
|
1215
|
+
const svc = externalService();
|
|
1216
|
+
if (!_optionalChain([svc, 'optionalAccess', _169 => _169.testConnection])) return unavailable(res);
|
|
1217
|
+
try {
|
|
1218
|
+
const result = await svc.testConnection(req.params.name);
|
|
1219
|
+
res.json(result);
|
|
1220
|
+
} catch (err) {
|
|
1221
|
+
badRequest(res, err);
|
|
1222
|
+
}
|
|
1223
|
+
});
|
|
1224
|
+
server.post(`${root}/:name/object-draft`, async (req, res) => {
|
|
1225
|
+
const svc = externalService();
|
|
1226
|
+
if (!_optionalChain([svc, 'optionalAccess', _170 => _170.generateObjectDraft])) return unavailable(res);
|
|
1227
|
+
const { table, ...opts } = _nullishCoalesce(req.body, () => ( {}));
|
|
1228
|
+
if (!table) return badRequest(res, new Error('Body field "table" is required.'));
|
|
1229
|
+
try {
|
|
1230
|
+
const draft = await svc.generateObjectDraft(req.params.name, String(table), opts);
|
|
1231
|
+
res.json({ draft });
|
|
1232
|
+
} catch (err) {
|
|
1233
|
+
badRequest(res, err);
|
|
1234
|
+
}
|
|
1235
|
+
});
|
|
940
1236
|
server.post(`${root}/test`, async (req, res) => {
|
|
941
1237
|
const svc = adminService();
|
|
942
|
-
if (!_optionalChain([svc, 'optionalAccess',
|
|
1238
|
+
if (!_optionalChain([svc, 'optionalAccess', _171 => _171.testConnection])) return unavailable(res);
|
|
943
1239
|
const { draft, secret } = splitSecret(req.body);
|
|
944
1240
|
try {
|
|
945
1241
|
const result = await svc.testConnection(draft, secret);
|
|
@@ -950,7 +1246,7 @@ function registerDatasourceAdminRoutes(server, ctx, basePath = "/api/v1") {
|
|
|
950
1246
|
});
|
|
951
1247
|
server.post(root, async (req, res) => {
|
|
952
1248
|
const svc = adminService();
|
|
953
|
-
if (!_optionalChain([svc, 'optionalAccess',
|
|
1249
|
+
if (!_optionalChain([svc, 'optionalAccess', _172 => _172.createDatasource])) return unavailable(res);
|
|
954
1250
|
const { draft, secret } = splitSecret(req.body);
|
|
955
1251
|
try {
|
|
956
1252
|
const datasource = await svc.createDatasource(draft, secret);
|
|
@@ -961,7 +1257,7 @@ function registerDatasourceAdminRoutes(server, ctx, basePath = "/api/v1") {
|
|
|
961
1257
|
});
|
|
962
1258
|
server.patch(`${root}/:name`, async (req, res) => {
|
|
963
1259
|
const svc = adminService();
|
|
964
|
-
if (!_optionalChain([svc, 'optionalAccess',
|
|
1260
|
+
if (!_optionalChain([svc, 'optionalAccess', _173 => _173.updateDatasource])) return unavailable(res);
|
|
965
1261
|
const { draft, secret } = splitSecret(req.body);
|
|
966
1262
|
try {
|
|
967
1263
|
const datasource = await svc.updateDatasource(req.params.name, draft, secret);
|
|
@@ -972,7 +1268,7 @@ function registerDatasourceAdminRoutes(server, ctx, basePath = "/api/v1") {
|
|
|
972
1268
|
});
|
|
973
1269
|
server.delete(`${root}/:name`, async (req, res) => {
|
|
974
1270
|
const svc = adminService();
|
|
975
|
-
if (!_optionalChain([svc, 'optionalAccess',
|
|
1271
|
+
if (!_optionalChain([svc, 'optionalAccess', _174 => _174.removeDatasource])) return unavailable(res);
|
|
976
1272
|
try {
|
|
977
1273
|
await svc.removeDatasource(req.params.name);
|
|
978
1274
|
res.status(204).end();
|