@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
|
@@ -4,7 +4,6 @@ import type { Plugin, PluginContext } from '@objectstack/core';
|
|
|
4
4
|
import { registerMetadataTypeActions } from '@objectstack/spec/kernel';
|
|
5
5
|
import type {
|
|
6
6
|
IDatasourceDriverFactory,
|
|
7
|
-
DatasourceConnectionSpec,
|
|
8
7
|
TestConnectionResult,
|
|
9
8
|
} from './contracts/index.js';
|
|
10
9
|
import {
|
|
@@ -13,6 +12,11 @@ import {
|
|
|
13
12
|
type StoredDatasource,
|
|
14
13
|
type ProbeInput,
|
|
15
14
|
} from './datasource-admin-service.js';
|
|
15
|
+
import {
|
|
16
|
+
DatasourceConnectionService,
|
|
17
|
+
type ConnectionEngineLike,
|
|
18
|
+
} from './datasource-connection-service.js';
|
|
19
|
+
import type { DatasourceConnectPolicy } from './contracts/connect-policy.js';
|
|
16
20
|
import type { Logger } from './logger.js';
|
|
17
21
|
|
|
18
22
|
/**
|
|
@@ -33,6 +37,82 @@ interface DataEngineLike {
|
|
|
33
37
|
registerDriver?: (driver: unknown, isDefault?: boolean) => void;
|
|
34
38
|
registerDatasourceDef?: (def: { name: string; schemaMode?: string; external?: { allowWrites?: boolean } }) => void;
|
|
35
39
|
getDriverByName?: (name: string) => unknown;
|
|
40
|
+
// sys_metadata CRUD used to persist runtime datasource records durably (same
|
|
41
|
+
// table runtime objects use). Optional — absent on lightweight kernels, in
|
|
42
|
+
// which case persistence degrades to in-memory (pre-existing behavior).
|
|
43
|
+
findOne?: (object: string, query: { where?: Record<string, unknown> }) => Promise<Record<string, unknown> | undefined | null>;
|
|
44
|
+
find?: (object: string, query: { where?: Record<string, unknown> }) => Promise<Record<string, unknown>[]>;
|
|
45
|
+
insert?: (object: string, row: Record<string, unknown>) => Promise<unknown>;
|
|
46
|
+
update?: (object: string, row: Record<string, unknown>, opts: { where: Record<string, unknown> }) => Promise<unknown>;
|
|
47
|
+
delete?: (object: string, opts: { where: Record<string, unknown> }) => Promise<unknown>;
|
|
48
|
+
}
|
|
49
|
+
|
|
50
|
+
/**
|
|
51
|
+
* Durable persistence for runtime datasource records via the `sys_metadata`
|
|
52
|
+
* table — the same store runtime objects use (the protocol writes objects there
|
|
53
|
+
* directly). `MetadataManager.register()` alone is in-memory unless a writable
|
|
54
|
+
* `datasource:` loader is wired, which standalone `serve` does not do; so a
|
|
55
|
+
* UI-created datasource vanished on restart. These helpers persist on write and
|
|
56
|
+
* the plugin restores them into the registry on boot before rehydrating pools.
|
|
57
|
+
* Credential cleartext is never stored — only the opaque `external.credentialsRef`.
|
|
58
|
+
*/
|
|
59
|
+
const DS_META_TYPE = 'datasource';
|
|
60
|
+
const SYS_METADATA = 'sys_metadata';
|
|
61
|
+
|
|
62
|
+
function newMetaId(): string {
|
|
63
|
+
return typeof crypto !== 'undefined' && typeof crypto.randomUUID === 'function'
|
|
64
|
+
? crypto.randomUUID()
|
|
65
|
+
: `meta_${Date.now()}_${Math.random().toString(36).slice(2)}`;
|
|
66
|
+
}
|
|
67
|
+
|
|
68
|
+
async function persistDatasourceRow(engine: DataEngineLike | undefined, record: { name: string }): Promise<void> {
|
|
69
|
+
if (!engine?.insert || !engine.findOne) return; // no durable store — in-memory only
|
|
70
|
+
const now = new Date().toISOString();
|
|
71
|
+
const existing = await engine.findOne(SYS_METADATA, {
|
|
72
|
+
where: { type: DS_META_TYPE, name: record.name, state: 'active' },
|
|
73
|
+
});
|
|
74
|
+
if (existing) {
|
|
75
|
+
await engine.update?.(
|
|
76
|
+
SYS_METADATA,
|
|
77
|
+
{ metadata: JSON.stringify(record), updated_at: now, version: ((existing.version as number) || 0) + 1, state: 'active' },
|
|
78
|
+
{ where: { id: existing.id } },
|
|
79
|
+
);
|
|
80
|
+
} else {
|
|
81
|
+
await engine.insert(SYS_METADATA, {
|
|
82
|
+
id: newMetaId(),
|
|
83
|
+
name: record.name,
|
|
84
|
+
type: DS_META_TYPE,
|
|
85
|
+
scope: 'platform',
|
|
86
|
+
metadata: JSON.stringify(record),
|
|
87
|
+
state: 'active',
|
|
88
|
+
version: 1,
|
|
89
|
+
created_at: now,
|
|
90
|
+
updated_at: now,
|
|
91
|
+
});
|
|
92
|
+
}
|
|
93
|
+
}
|
|
94
|
+
|
|
95
|
+
async function deleteDatasourceRow(engine: DataEngineLike | undefined, name: string): Promise<void> {
|
|
96
|
+
if (!engine?.findOne) return;
|
|
97
|
+
const existing = await engine.findOne(SYS_METADATA, { where: { type: DS_META_TYPE, name, state: 'active' } });
|
|
98
|
+
if (!existing) return;
|
|
99
|
+
if (engine.delete) await engine.delete(SYS_METADATA, { where: { id: existing.id } });
|
|
100
|
+
else await engine.update?.(SYS_METADATA, { state: 'inactive' }, { where: { id: existing.id } });
|
|
101
|
+
}
|
|
102
|
+
|
|
103
|
+
async function loadDatasourceRows(engine: DataEngineLike | undefined): Promise<Array<Record<string, unknown>>> {
|
|
104
|
+
if (!engine?.find) return [];
|
|
105
|
+
const rows = await engine.find(SYS_METADATA, { where: { type: DS_META_TYPE, state: 'active' } });
|
|
106
|
+
const out: Array<Record<string, unknown>> = [];
|
|
107
|
+
for (const r of rows ?? []) {
|
|
108
|
+
const raw = (r as { metadata?: unknown }).metadata;
|
|
109
|
+
try {
|
|
110
|
+
out.push(typeof raw === 'string' ? JSON.parse(raw) : (raw as Record<string, unknown>));
|
|
111
|
+
} catch {
|
|
112
|
+
/* skip corrupt row */
|
|
113
|
+
}
|
|
114
|
+
}
|
|
115
|
+
return out;
|
|
36
116
|
}
|
|
37
117
|
|
|
38
118
|
/**
|
|
@@ -59,6 +139,13 @@ export interface DatasourceAdminServicePluginOptions {
|
|
|
59
139
|
secrets?: SecretBinder;
|
|
60
140
|
/** Override the driver factory (defaults to the `'datasource-driver-factory'` service). */
|
|
61
141
|
driverFactory?: IDatasourceDriverFactory;
|
|
142
|
+
/**
|
|
143
|
+
* Host-injectable connect policy consulted before opening any datasource
|
|
144
|
+
* connection (ADR-0062 D5 / epic #2163 seam). Open-core default is permissive
|
|
145
|
+
* (allow); a multi-tenant host binds a stricter, fail-closed policy. Shared by
|
|
146
|
+
* both code-defined auto-connect and runtime-admin pool registration.
|
|
147
|
+
*/
|
|
148
|
+
connectPolicy?: DatasourceConnectPolicy;
|
|
62
149
|
logger?: Logger;
|
|
63
150
|
}
|
|
64
151
|
|
|
@@ -86,6 +173,8 @@ export class DatasourceAdminServicePlugin implements Plugin {
|
|
|
86
173
|
|
|
87
174
|
private service?: DatasourceAdminService;
|
|
88
175
|
private config?: DatasourceAdminServiceConfig;
|
|
176
|
+
/** Shared "definition → live driver" path (ADR-0062 D1); also exposed as the `'datasource-connection'` service. */
|
|
177
|
+
private connection?: DatasourceConnectionService;
|
|
89
178
|
private readonly options: DatasourceAdminServicePluginOptions;
|
|
90
179
|
|
|
91
180
|
constructor(options: DatasourceAdminServicePluginOptions = {}) {
|
|
@@ -128,6 +217,19 @@ export class DatasourceAdminServicePlugin implements Plugin {
|
|
|
128
217
|
const factory = (): IDatasourceDriverFactory | undefined =>
|
|
129
218
|
this.options.driverFactory ?? safeGetService<IDatasourceDriverFactory>(ctx, 'datasource-driver-factory');
|
|
130
219
|
|
|
220
|
+
// The single "definition → live driver" path (ADR-0062 D1). Built here so
|
|
221
|
+
// the admin pool registration (runtime origin) and the app-plugin
|
|
222
|
+
// auto-connect (code origin) share one connect + lifecycle + policy path.
|
|
223
|
+
// Registered as a kernel service so `AppPlugin.start()` can resolve it.
|
|
224
|
+
this.connection = new DatasourceConnectionService({
|
|
225
|
+
factory,
|
|
226
|
+
engine: () => engineOf() as ConnectionEngineLike | undefined,
|
|
227
|
+
secrets: { resolve: (ref) => this.options.secrets?.resolve?.(ref) ?? Promise.resolve(undefined) },
|
|
228
|
+
policy: this.options.connectPolicy,
|
|
229
|
+
logger: this.options.logger,
|
|
230
|
+
});
|
|
231
|
+
ctx.registerService('datasource-connection', this.connection);
|
|
232
|
+
|
|
131
233
|
const config: DatasourceAdminServiceConfig = {
|
|
132
234
|
probe: (input) => this.probe(factory(), input),
|
|
133
235
|
|
|
@@ -147,7 +249,10 @@ export class DatasourceAdminServicePlugin implements Plugin {
|
|
|
147
249
|
if (!metadata?.register) {
|
|
148
250
|
throw new Error('Metadata service is unavailable; cannot persist datasource.');
|
|
149
251
|
}
|
|
252
|
+
// In-memory registry (immediate visibility) + durable sys_metadata row
|
|
253
|
+
// (survives restart; restored on boot by restoreRuntimeDatasources).
|
|
150
254
|
await metadata.register('datasource', record.name, record);
|
|
255
|
+
await persistDatasourceRow(engineOf(), record);
|
|
151
256
|
},
|
|
152
257
|
|
|
153
258
|
deleteDatasourceRecord: async (name) => {
|
|
@@ -156,6 +261,7 @@ export class DatasourceAdminServicePlugin implements Plugin {
|
|
|
156
261
|
throw new Error('Metadata service is unavailable; cannot remove datasource.');
|
|
157
262
|
}
|
|
158
263
|
await metadata.unregister('datasource', name);
|
|
264
|
+
await deleteDatasourceRow(engineOf(), name);
|
|
159
265
|
},
|
|
160
266
|
|
|
161
267
|
writeSecret: async (input, hint) => {
|
|
@@ -181,40 +287,21 @@ export class DatasourceAdminServicePlugin implements Plugin {
|
|
|
181
287
|
return objects.filter((o) => o?.datasource === datasource).length;
|
|
182
288
|
},
|
|
183
289
|
|
|
290
|
+
// Hot pool (de)registration converges on the shared
|
|
291
|
+
// DatasourceConnectionService (ADR-0062 D1) — one connect path for code-
|
|
292
|
+
// and runtime-origin datasources. `connect()` builds the driver via the
|
|
293
|
+
// factory, dereferences `external.credentialsRef` through the SecretBinder,
|
|
294
|
+
// opens the connection, and registers the live driver + datasource def.
|
|
295
|
+
// Runtime-admin connects always degrade-with-warning on failure (never
|
|
296
|
+
// fail-fast), preserving the pre-ADR-0062 admin behavior.
|
|
184
297
|
registerPool: async (record) => {
|
|
185
|
-
|
|
186
|
-
|
|
187
|
-
if (!f || !engine?.registerDriver || !f.supports(record.driver)) return;
|
|
188
|
-
// Recover the cleartext credential from `sys_secret` so the pool opens
|
|
189
|
-
// with the real password. The cleartext is never persisted on the
|
|
190
|
-
// record (only `credentialsRef`), so it must be dereferenced here —
|
|
191
|
-
// both on create/update and on boot rehydration. Credential-less
|
|
192
|
-
// drivers (sqlite/memory) simply have no ref and skip this.
|
|
193
|
-
const credentialsRef = record.external?.credentialsRef;
|
|
194
|
-
const secret = credentialsRef ? await this.options.secrets?.resolve?.(credentialsRef) : undefined;
|
|
195
|
-
const handle = await f.create({ ...this.toSpec(record), ...(secret ? { secret } : {}) });
|
|
196
|
-
if (typeof handle?.connect === 'function') await handle.connect();
|
|
197
|
-
// The engine routes a datasource to a driver by `driver.name === <datasource name>`
|
|
198
|
-
// (see ObjectQL engine.getDriver). Prefer the factory's underlying engine
|
|
199
|
-
// driver (the `driver` escape hatch); fall back to the handle itself. Stamp
|
|
200
|
-
// the name so routing resolves to this pool.
|
|
201
|
-
const engineDriver = (handle.driver ?? handle) as { name?: string };
|
|
202
|
-
try {
|
|
203
|
-
engineDriver.name = record.name;
|
|
204
|
-
} catch {
|
|
205
|
-
/* frozen driver — registration may still work if name already matches */
|
|
206
|
-
}
|
|
207
|
-
engine.registerDriver(engineDriver);
|
|
208
|
-
engine.registerDatasourceDef?.({
|
|
209
|
-
name: record.name,
|
|
210
|
-
schemaMode: record.schemaMode,
|
|
211
|
-
external: record.external as { allowWrites?: boolean } | undefined,
|
|
298
|
+
await this.connection?.connect(record, {
|
|
299
|
+
context: { origin: record.origin ?? 'runtime', trigger: 'runtime-admin' },
|
|
212
300
|
});
|
|
213
301
|
},
|
|
214
302
|
|
|
215
303
|
unregisterPool: async (name) => {
|
|
216
|
-
|
|
217
|
-
if (typeof driver?.disconnect === 'function') await driver.disconnect();
|
|
304
|
+
await this.connection?.disconnect(name);
|
|
218
305
|
},
|
|
219
306
|
|
|
220
307
|
logger,
|
|
@@ -223,16 +310,85 @@ export class DatasourceAdminServicePlugin implements Plugin {
|
|
|
223
310
|
this.config = config;
|
|
224
311
|
this.service = new DatasourceAdminService(config);
|
|
225
312
|
ctx.registerService('datasource-admin', this.service);
|
|
313
|
+
|
|
314
|
+
// Setup-app nav (ADR-0029 D7): datasources are a *capability* this plugin
|
|
315
|
+
// owns, so it contributes its own entry into the `group_integrations` slot
|
|
316
|
+
// (core setup-nav must not fill capability-owned slots). datasource is a
|
|
317
|
+
// metadata type, so the entry opens the generic metadata-admin engine route
|
|
318
|
+
// rather than a bespoke page or an object view.
|
|
319
|
+
try {
|
|
320
|
+
const manifest = ctx.getService<{ register(m: any): void }>('manifest');
|
|
321
|
+
if (manifest && typeof manifest.register === 'function') {
|
|
322
|
+
manifest.register({
|
|
323
|
+
id: 'com.objectstack.service-datasource.nav',
|
|
324
|
+
namespace: 'sys',
|
|
325
|
+
version: this.version,
|
|
326
|
+
type: 'plugin',
|
|
327
|
+
scope: 'system',
|
|
328
|
+
name: 'Datasource Navigation',
|
|
329
|
+
description: 'Contributes the Datasources entry to the Setup app Integrations group.',
|
|
330
|
+
navigationContributions: [
|
|
331
|
+
{
|
|
332
|
+
app: 'setup',
|
|
333
|
+
group: 'group_integrations',
|
|
334
|
+
priority: 100,
|
|
335
|
+
items: [
|
|
336
|
+
{
|
|
337
|
+
id: 'nav_datasources',
|
|
338
|
+
type: 'url',
|
|
339
|
+
label: 'Datasources',
|
|
340
|
+
url: '/apps/setup/component/metadata/resource?type=datasource',
|
|
341
|
+
icon: 'database',
|
|
342
|
+
requiredPermissions: ['manage_platform_settings'],
|
|
343
|
+
},
|
|
344
|
+
],
|
|
345
|
+
},
|
|
346
|
+
],
|
|
347
|
+
});
|
|
348
|
+
}
|
|
349
|
+
} catch (err) {
|
|
350
|
+
this.options.logger?.warn?.('datasource nav contribution skipped', err);
|
|
351
|
+
}
|
|
226
352
|
}
|
|
227
353
|
|
|
228
354
|
async start(ctx: PluginContext): Promise<void> {
|
|
229
|
-
//
|
|
230
|
-
//
|
|
231
|
-
//
|
|
355
|
+
// Restore UI-created (runtime) datasources from the durable sys_metadata
|
|
356
|
+
// store back into the in-memory registry, THEN rebuild their live pools.
|
|
357
|
+
// `register()` is in-memory only in standalone serve (no writable
|
|
358
|
+
// `datasource:` loader), so without this a node restart drops every
|
|
359
|
+
// UI-created datasource. Code-defined datasources come from the artifact and
|
|
360
|
+
// are unaffected.
|
|
361
|
+
await this.restoreRuntimeDatasources(ctx);
|
|
232
362
|
await this.rehydratePools();
|
|
233
363
|
if (this.service) await ctx.trigger('datasource-admin:ready', this.service);
|
|
234
364
|
}
|
|
235
365
|
|
|
366
|
+
/** Reload persisted runtime datasource rows (sys_metadata) into the registry. */
|
|
367
|
+
private async restoreRuntimeDatasources(ctx: PluginContext): Promise<void> {
|
|
368
|
+
const engine = safeGetService<DataEngineLike>(ctx, 'data');
|
|
369
|
+
const metadata = safeGetService<MetadataServiceLike>(ctx, 'metadata');
|
|
370
|
+
if (!engine?.find || !metadata?.register) return;
|
|
371
|
+
let rows: Array<Record<string, unknown>>;
|
|
372
|
+
try {
|
|
373
|
+
rows = await loadDatasourceRows(engine);
|
|
374
|
+
} catch (err) {
|
|
375
|
+
this.options.logger?.warn?.('datasource restore: reading sys_metadata failed', err);
|
|
376
|
+
return;
|
|
377
|
+
}
|
|
378
|
+
let restored = 0;
|
|
379
|
+
for (const rec of rows) {
|
|
380
|
+
const name = (rec as { name?: string }).name;
|
|
381
|
+
if (!name) continue;
|
|
382
|
+
try {
|
|
383
|
+
await metadata.register('datasource', name, rec);
|
|
384
|
+
restored += 1;
|
|
385
|
+
} catch (err) {
|
|
386
|
+
this.options.logger?.warn?.(`datasource restore: register '${name}' failed`, err);
|
|
387
|
+
}
|
|
388
|
+
}
|
|
389
|
+
if (restored > 0) this.options.logger?.info?.(`datasource: restored ${restored} runtime record(s) from sys_metadata`);
|
|
390
|
+
}
|
|
391
|
+
|
|
236
392
|
/**
|
|
237
393
|
* Boot-time rehydration: list persisted runtime datasources and re-register
|
|
238
394
|
* each one's connection pool (driver build → connect → registerDriver),
|
|
@@ -277,16 +433,6 @@ export class DatasourceAdminServicePlugin implements Plugin {
|
|
|
277
433
|
|
|
278
434
|
// --- internals -----------------------------------------------------------
|
|
279
435
|
|
|
280
|
-
private toSpec(record: StoredDatasource): DatasourceConnectionSpec {
|
|
281
|
-
return {
|
|
282
|
-
name: record.name,
|
|
283
|
-
driver: record.driver,
|
|
284
|
-
config: record.config ?? {},
|
|
285
|
-
external: record.external,
|
|
286
|
-
pool: record.pool,
|
|
287
|
-
};
|
|
288
|
-
}
|
|
289
|
-
|
|
290
436
|
/** Probe a connection via the driver factory: build → connect → ping → close. */
|
|
291
437
|
private async probe(
|
|
292
438
|
factory: IDatasourceDriverFactory | undefined,
|
|
@@ -47,6 +47,8 @@ export interface StoredDatasource {
|
|
|
47
47
|
external?: (Record<string, unknown> & { credentialsRef?: string }) | undefined;
|
|
48
48
|
pool?: Record<string, unknown>;
|
|
49
49
|
active?: boolean;
|
|
50
|
+
/** Force a live connection at boot even when managed + unrouted (ADR-0062 D2(c)). */
|
|
51
|
+
autoConnect?: boolean;
|
|
50
52
|
origin?: 'code' | 'runtime';
|
|
51
53
|
/** Package that defines a code-origin datasource, when known. */
|
|
52
54
|
definedIn?: string;
|
|
@@ -130,6 +132,36 @@ export class DatasourceAdminService implements IDatasourceAdminService {
|
|
|
130
132
|
return summaries;
|
|
131
133
|
}
|
|
132
134
|
|
|
135
|
+
/**
|
|
136
|
+
* Read one datasource's full detail for editing, with the credential stripped.
|
|
137
|
+
* Returns `config` (non-sensitive — credentials live in `sys_secret`, never in
|
|
138
|
+
* config), `origin`, and a `hasSecret` flag so the UI can show "leave blank to
|
|
139
|
+
* keep" without ever receiving the `credentialsRef` or any cleartext. Returns
|
|
140
|
+
* `undefined` when the name is unknown.
|
|
141
|
+
*/
|
|
142
|
+
async getDatasource(name: string): Promise<
|
|
143
|
+
| (Pick<StoredDatasource, 'name' | 'label' | 'driver' | 'schemaMode' | 'config' | 'active' | 'definedIn'> & {
|
|
144
|
+
origin: 'code' | 'runtime';
|
|
145
|
+
hasSecret: boolean;
|
|
146
|
+
})
|
|
147
|
+
| undefined
|
|
148
|
+
> {
|
|
149
|
+
const rec = await this.config.getDatasourceRecord(name);
|
|
150
|
+
if (!rec) return undefined;
|
|
151
|
+
const hasSecret = Boolean(rec.external?.credentialsRef);
|
|
152
|
+
return {
|
|
153
|
+
name: rec.name,
|
|
154
|
+
label: rec.label,
|
|
155
|
+
driver: rec.driver,
|
|
156
|
+
schemaMode: rec.schemaMode ?? 'managed',
|
|
157
|
+
config: rec.config ?? {},
|
|
158
|
+
active: rec.active ?? true,
|
|
159
|
+
origin: rec.origin === 'runtime' ? 'runtime' : 'code',
|
|
160
|
+
hasSecret,
|
|
161
|
+
...(rec.definedIn ? { definedIn: rec.definedIn } : {}),
|
|
162
|
+
};
|
|
163
|
+
}
|
|
164
|
+
|
|
133
165
|
async testConnection(input: DatasourceDraft, secret?: SecretInput): Promise<TestConnectionResult> {
|
|
134
166
|
if (!input?.driver) {
|
|
135
167
|
return { ok: false, error: 'A driver is required to test a connection.' };
|