@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/dist/index.d.cts CHANGED
@@ -1,8 +1,8 @@
1
1
  import { IExternalDatasourceService, IntrospectedSchema, RemoteTable, GenerateDraftOpts, ObjectDraft, ImportObjectOpts, ImportObjectResult, SchemaValidationResult, SchemaValidationReport, ICryptoProvider, IHttpServer } from '@objectstack/spec/contracts';
2
2
  import { ExternalCatalog } from '@objectstack/spec/data';
3
3
  import { Plugin, PluginContext } from '@objectstack/core';
4
- import { IDatasourceAdminService, TestConnectionResult, SecretInput, DatasourceSummary, DatasourceDraft, IDatasourceDriverFactory } from './contracts/index.cjs';
5
- export { DatasourceConnectionSpec, DatasourceDriverHandle, DatasourceOrigin } from './contracts/index.cjs';
4
+ import { IDatasourceDriverFactory, DatasourceConnectPolicy, DatasourceConnectContext, IDatasourceAdminService, TestConnectionResult, SecretInput, DatasourceSummary, DatasourceDraft } from './contracts/index.cjs';
5
+ export { DatasourceConnectDecision, DatasourceConnectSubject, DatasourceConnectionSpec, DatasourceDriverHandle, DatasourceOrigin, allowAllConnectPolicy } from './contracts/index.cjs';
6
6
 
7
7
  /**
8
8
  * ExternalDatasourceService — implements {@link IExternalDatasourceService}
@@ -81,6 +81,21 @@ declare class ExternalDatasourceService implements IExternalDatasourceService {
81
81
  listRemoteTables(datasource: string, opts?: {
82
82
  schema?: string;
83
83
  }): Promise<RemoteTable[]>;
84
+ /**
85
+ * Probe a *saved* datasource by name with a live round-trip. Reuses the
86
+ * introspection path (driver connect + schema read) as a cheap connectivity
87
+ * check, so the secret is resolved through the same wired pool as the rest of
88
+ * the introspection surface — the caller never handles cleartext. Returns a
89
+ * structured result rather than throwing so the route can render ok/error
90
+ * uniformly. This backs the `datasource` `test_connection` action
91
+ * (`POST /datasources/:name/test`).
92
+ */
93
+ testConnection(datasource: string): Promise<{
94
+ ok: boolean;
95
+ latencyMs?: number;
96
+ tableCount?: number;
97
+ error?: string;
98
+ }>;
84
99
  generateObjectDraft(datasource: string, remoteName: string, opts?: GenerateDraftOpts): Promise<ObjectDraft>;
85
100
  importObject(datasource: string, remoteName: string, opts?: ImportObjectOpts): Promise<ImportObjectResult>;
86
101
  refreshCatalog(datasource: string): Promise<ExternalCatalog>;
@@ -122,6 +137,168 @@ declare class ExternalDatasourceServicePlugin implements Plugin {
122
137
  interface Logger {
123
138
  warn: (message: string, meta?: unknown) => void;
124
139
  info?: (message: string, meta?: unknown) => void;
140
+ debug?: (message: string, meta?: unknown) => void;
141
+ error?: (message: string, meta?: unknown) => void;
142
+ }
143
+
144
+ /**
145
+ * DatasourceConnectionService — the single "definition → live driver" path
146
+ * (ADR-0062 D1).
147
+ *
148
+ * Given a datasource definition, it: consults the injectable connect policy
149
+ * (D5/epic seam), builds a driver via the host-provided driver factory,
150
+ * resolves any `external.credentialsRef` to a cleartext secret via the
151
+ * `SecretBinder` (D3, wired in Phase 2), opens the connection, and registers
152
+ * the live driver + the datasource *definition* into the ObjectQL engine under
153
+ * the datasource name (the engine routes by `driver.name === <datasource>`).
154
+ *
155
+ * Both origins converge here (D1):
156
+ * - **code-defined** datasources auto-connect at boot via
157
+ * {@link connectDeclared} (gated per D2 — see {@link isDatasourceAddressed}),
158
+ * called from `AppPlugin.start()`.
159
+ * - **runtime** (UI-created) datasources connect via {@link connect}, called
160
+ * from `DatasourceAdminServicePlugin`'s `registerPool` (create/update + boot
161
+ * rehydration).
162
+ *
163
+ * Idempotent: a datasource already registered as a live driver is skipped, so
164
+ * an app's legacy `onEnable` driver registration (the escape hatch, ADR-0062
165
+ * D8) and auto-connect never double-register.
166
+ */
167
+
168
+ /** A datasource definition this service can connect (code- or runtime-origin). */
169
+ interface ConnectableDatasource {
170
+ name: string;
171
+ label?: string;
172
+ driver: string;
173
+ schemaMode?: 'managed' | 'external' | 'validate-only';
174
+ config?: Record<string, unknown>;
175
+ external?: (Record<string, unknown> & {
176
+ credentialsRef?: string;
177
+ validation?: {
178
+ onMismatch?: 'fail' | 'warn' | 'ignore';
179
+ };
180
+ }) | undefined;
181
+ pool?: Record<string, unknown>;
182
+ active?: boolean;
183
+ origin?: 'code' | 'runtime';
184
+ /**
185
+ * ADR-0062 D2(c): explicit opt-in to auto-connect even for a managed,
186
+ * unrouted datasource. Defaults to false.
187
+ */
188
+ autoConnect?: boolean;
189
+ }
190
+ /** Minimal object shape used for the D2 routing gate + post-connect schema sync. */
191
+ interface DatasourceBoundObject {
192
+ name?: string;
193
+ /** The object's explicit `datasource` binding (ADR-0015 federation). */
194
+ datasource?: string;
195
+ }
196
+ /** Engine surface this service drives (the ObjectQL `'data'` engine). */
197
+ interface ConnectionEngineLike {
198
+ registerDriver?: (driver: unknown, isDefault?: boolean) => void;
199
+ registerDatasourceDef?: (def: {
200
+ name: string;
201
+ schemaMode?: string;
202
+ external?: {
203
+ allowWrites?: boolean;
204
+ };
205
+ }) => void;
206
+ getDriverByName?: (name: string) => unknown;
207
+ /**
208
+ * Register read metadata (DDL-free) for a federated object so its physical
209
+ * remote table/columns resolve for queries. Idempotent; called per bound
210
+ * external object after the driver is registered, because boot schema-sync
211
+ * ran before this driver existed (ADR-0015 §18; matches what the legacy
212
+ * `onEnable` bridge does manually).
213
+ */
214
+ syncObjectSchema?: (objectName: string) => Promise<void>;
215
+ }
216
+ /** Secret dereference surface (the `SecretBinder.resolve`, Phase 2 / D3). */
217
+ interface ConnectionSecretResolver {
218
+ resolve?: (credentialsRef: string) => Promise<string | undefined>;
219
+ }
220
+ interface DatasourceConnectionServiceConfig {
221
+ /** Resolve the host driver factory (lazy — may be registered after init). */
222
+ factory: () => IDatasourceDriverFactory | undefined;
223
+ /** Resolve the ObjectQL engine (lazy). */
224
+ engine: () => ConnectionEngineLike | undefined;
225
+ /** Dereference `credentialsRef` → cleartext (Phase 2). Optional in Phase 1. */
226
+ secrets?: ConnectionSecretResolver;
227
+ /** Injectable connect policy. Defaults to {@link allowAllConnectPolicy}. */
228
+ policy?: DatasourceConnectPolicy;
229
+ logger?: Logger;
230
+ }
231
+ /** Outcome of a single {@link DatasourceConnectionService.connect} attempt. */
232
+ type ConnectStatus = 'connected' | 'already-registered' | 'skipped-policy' | 'skipped-no-infra' | 'skipped-unsupported' | 'failed-credentials' | 'failed-degraded';
233
+ interface ConnectResult {
234
+ name: string;
235
+ status: ConnectStatus;
236
+ reason?: string;
237
+ }
238
+ /**
239
+ * ADR-0062 D2 — is this declared datasource "meaningfully addressed", such that
240
+ * auto-connecting it is safe and intended?
241
+ *
242
+ * Returns true when:
243
+ * - (a) it is external (`schemaMode !== 'managed'`), OR
244
+ * - (b) some object **explicitly** binds to it (`object.datasource === name`), OR
245
+ * - (c) it sets `autoConnect: true`.
246
+ *
247
+ * Deliberately NOT triggered by a `datasourceMapping` rule alone. A managed
248
+ * datasource that is only *mapped* (namespace/package/default) but has no live
249
+ * driver historically falls through to the `default` driver at query time
250
+ * (`engine.getDriver` step 4) — e.g. `examples/app-crm`'s `crm_primary`
251
+ * (`:memory:`, mapped + default-fallback, no `onEnable`). Connecting it would
252
+ * divert those objects to a fresh, empty connection and silently change app
253
+ * behavior. So mapping-only routing to a *managed* datasource is treated as
254
+ * decorative, keeping existing apps byte-for-byte unchanged (D2's load-bearing
255
+ * backward-compat guarantee). External datasources and explicit
256
+ * `object.datasource` bindings never resolved to `default` (they throw when
257
+ * unregistered), so auto-connecting them is a strict improvement, not a change.
258
+ */
259
+ declare function isDatasourceAddressed(ds: Pick<ConnectableDatasource, 'name' | 'schemaMode' | 'autoConnect'>, ctx: {
260
+ objects?: readonly DatasourceBoundObject[];
261
+ }): boolean;
262
+ declare class DatasourceConnectionService {
263
+ private readonly cfg;
264
+ private readonly policy;
265
+ private readonly logger?;
266
+ constructor(cfg: DatasourceConnectionServiceConfig);
267
+ /**
268
+ * Auto-connect the declared (code-defined) datasources that pass the D2 gate.
269
+ * Called from `AppPlugin.start()` with the app bundle's datasources + objects.
270
+ * Each connected external datasource also has its bound objects' read metadata
271
+ * synced so they are immediately queryable with zero app code.
272
+ */
273
+ connectDeclared(input: {
274
+ datasources: readonly ConnectableDatasource[];
275
+ objects?: readonly DatasourceBoundObject[];
276
+ }): Promise<ConnectResult[]>;
277
+ /**
278
+ * Build + connect + register a single datasource's live driver. The shared
279
+ * core used by both auto-connect and the runtime-admin pool registration.
280
+ *
281
+ * Failure policy (ADR-0062 D5): an `external` datasource with
282
+ * `validation.onMismatch: 'fail'` fails fast (re-throws, bricking boot as
283
+ * intended); everything else degrades with a warning so an optional replica's
284
+ * connectivity blip never bricks boot.
285
+ */
286
+ connect(record: ConnectableDatasource, opts?: {
287
+ objects?: readonly string[];
288
+ context?: DatasourceConnectContext;
289
+ }): Promise<ConnectResult>;
290
+ /** Gracefully disconnect a previously-registered datasource pool. */
291
+ disconnect(name: string): Promise<void>;
292
+ /**
293
+ * Apply the D5 connect-failure policy (also covers D3 credential failures). A
294
+ * code-defined `external` datasource with `onMismatch:'fail'` auto-connected at
295
+ * boot re-throws (fail-fast, bricking boot as intended). Runtime-admin
296
+ * create/update + boot rehydration always degrade-with-warning — a UI action
297
+ * or a replica blip must never brick the running server (preserves the
298
+ * pre-ADR-0062 admin behavior). Either way the datasource is left unconnected
299
+ * with a clear message — never a silent skip.
300
+ */
301
+ private handleFailure;
125
302
  }
126
303
 
127
304
  /**
@@ -161,6 +338,8 @@ interface StoredDatasource {
161
338
  }) | undefined;
162
339
  pool?: Record<string, unknown>;
163
340
  active?: boolean;
341
+ /** Force a live connection at boot even when managed + unrouted (ADR-0062 D2(c)). */
342
+ autoConnect?: boolean;
164
343
  origin?: 'code' | 'runtime';
165
344
  /** Package that defines a code-origin datasource, when known. */
166
345
  definedIn?: string;
@@ -209,6 +388,17 @@ declare class DatasourceAdminService implements IDatasourceAdminService {
209
388
  constructor(config: DatasourceAdminServiceConfig);
210
389
  private get logger();
211
390
  listDatasources(): Promise<DatasourceSummary[]>;
391
+ /**
392
+ * Read one datasource's full detail for editing, with the credential stripped.
393
+ * Returns `config` (non-sensitive — credentials live in `sys_secret`, never in
394
+ * config), `origin`, and a `hasSecret` flag so the UI can show "leave blank to
395
+ * keep" without ever receiving the `credentialsRef` or any cleartext. Returns
396
+ * `undefined` when the name is unknown.
397
+ */
398
+ getDatasource(name: string): Promise<(Pick<StoredDatasource, 'name' | 'label' | 'driver' | 'schemaMode' | 'config' | 'active' | 'definedIn'> & {
399
+ origin: 'code' | 'runtime';
400
+ hasSecret: boolean;
401
+ }) | undefined>;
212
402
  testConnection(input: DatasourceDraft, secret?: SecretInput): Promise<TestConnectionResult>;
213
403
  createDatasource(input: DatasourceDraft, secret?: SecretInput): Promise<DatasourceSummary>;
214
404
  updateDatasource(name: string, patch: Partial<DatasourceDraft>, secret?: SecretInput): Promise<DatasourceSummary>;
@@ -250,6 +440,13 @@ interface DatasourceAdminServicePluginOptions {
250
440
  secrets?: SecretBinder;
251
441
  /** Override the driver factory (defaults to the `'datasource-driver-factory'` service). */
252
442
  driverFactory?: IDatasourceDriverFactory;
443
+ /**
444
+ * Host-injectable connect policy consulted before opening any datasource
445
+ * connection (ADR-0062 D5 / epic #2163 seam). Open-core default is permissive
446
+ * (allow); a multi-tenant host binds a stricter, fail-closed policy. Shared by
447
+ * both code-defined auto-connect and runtime-admin pool registration.
448
+ */
449
+ connectPolicy?: DatasourceConnectPolicy;
253
450
  logger?: Logger;
254
451
  }
255
452
  /**
@@ -275,10 +472,14 @@ declare class DatasourceAdminServicePlugin implements Plugin {
275
472
  dependencies: string[];
276
473
  private service?;
277
474
  private config?;
475
+ /** Shared "definition → live driver" path (ADR-0062 D1); also exposed as the `'datasource-connection'` service. */
476
+ private connection?;
278
477
  private readonly options;
279
478
  constructor(options?: DatasourceAdminServicePluginOptions);
280
479
  init(ctx: PluginContext): Promise<void>;
281
480
  start(ctx: PluginContext): Promise<void>;
481
+ /** Reload persisted runtime datasource rows (sys_metadata) into the registry. */
482
+ private restoreRuntimeDatasources;
282
483
  /**
283
484
  * Boot-time rehydration: list persisted runtime datasources and re-register
284
485
  * each one's connection pool (driver build → connect → registerDriver),
@@ -290,7 +491,6 @@ declare class DatasourceAdminServicePlugin implements Plugin {
290
491
  */
291
492
  private rehydratePools;
292
493
  destroy(): Promise<void>;
293
- private toSpec;
294
494
  /** Probe a connection via the driver factory: build → connect → ping → close. */
295
495
  private probe;
296
496
  }
@@ -411,4 +611,4 @@ declare function createDatasourceSecretBinder(deps: DatasourceSecretBinderDeps):
411
611
  */
412
612
  declare function registerDatasourceAdminRoutes(server: IHttpServer, ctx: PluginContext, basePath?: string): void;
413
613
 
414
- export { DatasourceAdminService, type DatasourceAdminServiceConfig, DatasourceAdminServicePlugin, type DatasourceAdminServicePluginOptions, DatasourceDraft, type DatasourceLike, type DatasourceSecretBinder, type DatasourceSecretBinderDeps, DatasourceSummary, ExternalDatasourceService, type ExternalDatasourceServiceConfig, ExternalDatasourceServicePlugin, type ExternalDatasourceServicePluginOptions, IDatasourceAdminService, IDatasourceDriverFactory, type Logger$1 as Logger, type ObjectLike, type ProbeInput, type SecretBinder, SecretInput, type SecretStoreEngineLike, type StoredDatasource, TestConnectionResult, createDatasourceSecretBinder, createDefaultDatasourceDriverFactory, parseCredentialsRef, registerDatasourceAdminRoutes, toCredentialsRef };
614
+ export { type ConnectResult, type ConnectStatus, type ConnectableDatasource, type ConnectionEngineLike, type ConnectionSecretResolver, DatasourceAdminService, type DatasourceAdminServiceConfig, DatasourceAdminServicePlugin, type DatasourceAdminServicePluginOptions, type DatasourceBoundObject, DatasourceConnectContext, DatasourceConnectPolicy, DatasourceConnectionService, type DatasourceConnectionServiceConfig, DatasourceDraft, type DatasourceLike, type DatasourceSecretBinder, type DatasourceSecretBinderDeps, DatasourceSummary, ExternalDatasourceService, type ExternalDatasourceServiceConfig, ExternalDatasourceServicePlugin, type ExternalDatasourceServicePluginOptions, IDatasourceAdminService, IDatasourceDriverFactory, type Logger$1 as Logger, type ObjectLike, type ProbeInput, type SecretBinder, SecretInput, type SecretStoreEngineLike, type StoredDatasource, TestConnectionResult, createDatasourceSecretBinder, createDefaultDatasourceDriverFactory, isDatasourceAddressed, parseCredentialsRef, registerDatasourceAdminRoutes, toCredentialsRef };
package/dist/index.d.ts CHANGED
@@ -1,8 +1,8 @@
1
1
  import { IExternalDatasourceService, IntrospectedSchema, RemoteTable, GenerateDraftOpts, ObjectDraft, ImportObjectOpts, ImportObjectResult, SchemaValidationResult, SchemaValidationReport, ICryptoProvider, IHttpServer } from '@objectstack/spec/contracts';
2
2
  import { ExternalCatalog } from '@objectstack/spec/data';
3
3
  import { Plugin, PluginContext } from '@objectstack/core';
4
- import { IDatasourceAdminService, TestConnectionResult, SecretInput, DatasourceSummary, DatasourceDraft, IDatasourceDriverFactory } from './contracts/index.js';
5
- export { DatasourceConnectionSpec, DatasourceDriverHandle, DatasourceOrigin } from './contracts/index.js';
4
+ import { IDatasourceDriverFactory, DatasourceConnectPolicy, DatasourceConnectContext, IDatasourceAdminService, TestConnectionResult, SecretInput, DatasourceSummary, DatasourceDraft } from './contracts/index.js';
5
+ export { DatasourceConnectDecision, DatasourceConnectSubject, DatasourceConnectionSpec, DatasourceDriverHandle, DatasourceOrigin, allowAllConnectPolicy } from './contracts/index.js';
6
6
 
7
7
  /**
8
8
  * ExternalDatasourceService — implements {@link IExternalDatasourceService}
@@ -81,6 +81,21 @@ declare class ExternalDatasourceService implements IExternalDatasourceService {
81
81
  listRemoteTables(datasource: string, opts?: {
82
82
  schema?: string;
83
83
  }): Promise<RemoteTable[]>;
84
+ /**
85
+ * Probe a *saved* datasource by name with a live round-trip. Reuses the
86
+ * introspection path (driver connect + schema read) as a cheap connectivity
87
+ * check, so the secret is resolved through the same wired pool as the rest of
88
+ * the introspection surface — the caller never handles cleartext. Returns a
89
+ * structured result rather than throwing so the route can render ok/error
90
+ * uniformly. This backs the `datasource` `test_connection` action
91
+ * (`POST /datasources/:name/test`).
92
+ */
93
+ testConnection(datasource: string): Promise<{
94
+ ok: boolean;
95
+ latencyMs?: number;
96
+ tableCount?: number;
97
+ error?: string;
98
+ }>;
84
99
  generateObjectDraft(datasource: string, remoteName: string, opts?: GenerateDraftOpts): Promise<ObjectDraft>;
85
100
  importObject(datasource: string, remoteName: string, opts?: ImportObjectOpts): Promise<ImportObjectResult>;
86
101
  refreshCatalog(datasource: string): Promise<ExternalCatalog>;
@@ -122,6 +137,168 @@ declare class ExternalDatasourceServicePlugin implements Plugin {
122
137
  interface Logger {
123
138
  warn: (message: string, meta?: unknown) => void;
124
139
  info?: (message: string, meta?: unknown) => void;
140
+ debug?: (message: string, meta?: unknown) => void;
141
+ error?: (message: string, meta?: unknown) => void;
142
+ }
143
+
144
+ /**
145
+ * DatasourceConnectionService — the single "definition → live driver" path
146
+ * (ADR-0062 D1).
147
+ *
148
+ * Given a datasource definition, it: consults the injectable connect policy
149
+ * (D5/epic seam), builds a driver via the host-provided driver factory,
150
+ * resolves any `external.credentialsRef` to a cleartext secret via the
151
+ * `SecretBinder` (D3, wired in Phase 2), opens the connection, and registers
152
+ * the live driver + the datasource *definition* into the ObjectQL engine under
153
+ * the datasource name (the engine routes by `driver.name === <datasource>`).
154
+ *
155
+ * Both origins converge here (D1):
156
+ * - **code-defined** datasources auto-connect at boot via
157
+ * {@link connectDeclared} (gated per D2 — see {@link isDatasourceAddressed}),
158
+ * called from `AppPlugin.start()`.
159
+ * - **runtime** (UI-created) datasources connect via {@link connect}, called
160
+ * from `DatasourceAdminServicePlugin`'s `registerPool` (create/update + boot
161
+ * rehydration).
162
+ *
163
+ * Idempotent: a datasource already registered as a live driver is skipped, so
164
+ * an app's legacy `onEnable` driver registration (the escape hatch, ADR-0062
165
+ * D8) and auto-connect never double-register.
166
+ */
167
+
168
+ /** A datasource definition this service can connect (code- or runtime-origin). */
169
+ interface ConnectableDatasource {
170
+ name: string;
171
+ label?: string;
172
+ driver: string;
173
+ schemaMode?: 'managed' | 'external' | 'validate-only';
174
+ config?: Record<string, unknown>;
175
+ external?: (Record<string, unknown> & {
176
+ credentialsRef?: string;
177
+ validation?: {
178
+ onMismatch?: 'fail' | 'warn' | 'ignore';
179
+ };
180
+ }) | undefined;
181
+ pool?: Record<string, unknown>;
182
+ active?: boolean;
183
+ origin?: 'code' | 'runtime';
184
+ /**
185
+ * ADR-0062 D2(c): explicit opt-in to auto-connect even for a managed,
186
+ * unrouted datasource. Defaults to false.
187
+ */
188
+ autoConnect?: boolean;
189
+ }
190
+ /** Minimal object shape used for the D2 routing gate + post-connect schema sync. */
191
+ interface DatasourceBoundObject {
192
+ name?: string;
193
+ /** The object's explicit `datasource` binding (ADR-0015 federation). */
194
+ datasource?: string;
195
+ }
196
+ /** Engine surface this service drives (the ObjectQL `'data'` engine). */
197
+ interface ConnectionEngineLike {
198
+ registerDriver?: (driver: unknown, isDefault?: boolean) => void;
199
+ registerDatasourceDef?: (def: {
200
+ name: string;
201
+ schemaMode?: string;
202
+ external?: {
203
+ allowWrites?: boolean;
204
+ };
205
+ }) => void;
206
+ getDriverByName?: (name: string) => unknown;
207
+ /**
208
+ * Register read metadata (DDL-free) for a federated object so its physical
209
+ * remote table/columns resolve for queries. Idempotent; called per bound
210
+ * external object after the driver is registered, because boot schema-sync
211
+ * ran before this driver existed (ADR-0015 §18; matches what the legacy
212
+ * `onEnable` bridge does manually).
213
+ */
214
+ syncObjectSchema?: (objectName: string) => Promise<void>;
215
+ }
216
+ /** Secret dereference surface (the `SecretBinder.resolve`, Phase 2 / D3). */
217
+ interface ConnectionSecretResolver {
218
+ resolve?: (credentialsRef: string) => Promise<string | undefined>;
219
+ }
220
+ interface DatasourceConnectionServiceConfig {
221
+ /** Resolve the host driver factory (lazy — may be registered after init). */
222
+ factory: () => IDatasourceDriverFactory | undefined;
223
+ /** Resolve the ObjectQL engine (lazy). */
224
+ engine: () => ConnectionEngineLike | undefined;
225
+ /** Dereference `credentialsRef` → cleartext (Phase 2). Optional in Phase 1. */
226
+ secrets?: ConnectionSecretResolver;
227
+ /** Injectable connect policy. Defaults to {@link allowAllConnectPolicy}. */
228
+ policy?: DatasourceConnectPolicy;
229
+ logger?: Logger;
230
+ }
231
+ /** Outcome of a single {@link DatasourceConnectionService.connect} attempt. */
232
+ type ConnectStatus = 'connected' | 'already-registered' | 'skipped-policy' | 'skipped-no-infra' | 'skipped-unsupported' | 'failed-credentials' | 'failed-degraded';
233
+ interface ConnectResult {
234
+ name: string;
235
+ status: ConnectStatus;
236
+ reason?: string;
237
+ }
238
+ /**
239
+ * ADR-0062 D2 — is this declared datasource "meaningfully addressed", such that
240
+ * auto-connecting it is safe and intended?
241
+ *
242
+ * Returns true when:
243
+ * - (a) it is external (`schemaMode !== 'managed'`), OR
244
+ * - (b) some object **explicitly** binds to it (`object.datasource === name`), OR
245
+ * - (c) it sets `autoConnect: true`.
246
+ *
247
+ * Deliberately NOT triggered by a `datasourceMapping` rule alone. A managed
248
+ * datasource that is only *mapped* (namespace/package/default) but has no live
249
+ * driver historically falls through to the `default` driver at query time
250
+ * (`engine.getDriver` step 4) — e.g. `examples/app-crm`'s `crm_primary`
251
+ * (`:memory:`, mapped + default-fallback, no `onEnable`). Connecting it would
252
+ * divert those objects to a fresh, empty connection and silently change app
253
+ * behavior. So mapping-only routing to a *managed* datasource is treated as
254
+ * decorative, keeping existing apps byte-for-byte unchanged (D2's load-bearing
255
+ * backward-compat guarantee). External datasources and explicit
256
+ * `object.datasource` bindings never resolved to `default` (they throw when
257
+ * unregistered), so auto-connecting them is a strict improvement, not a change.
258
+ */
259
+ declare function isDatasourceAddressed(ds: Pick<ConnectableDatasource, 'name' | 'schemaMode' | 'autoConnect'>, ctx: {
260
+ objects?: readonly DatasourceBoundObject[];
261
+ }): boolean;
262
+ declare class DatasourceConnectionService {
263
+ private readonly cfg;
264
+ private readonly policy;
265
+ private readonly logger?;
266
+ constructor(cfg: DatasourceConnectionServiceConfig);
267
+ /**
268
+ * Auto-connect the declared (code-defined) datasources that pass the D2 gate.
269
+ * Called from `AppPlugin.start()` with the app bundle's datasources + objects.
270
+ * Each connected external datasource also has its bound objects' read metadata
271
+ * synced so they are immediately queryable with zero app code.
272
+ */
273
+ connectDeclared(input: {
274
+ datasources: readonly ConnectableDatasource[];
275
+ objects?: readonly DatasourceBoundObject[];
276
+ }): Promise<ConnectResult[]>;
277
+ /**
278
+ * Build + connect + register a single datasource's live driver. The shared
279
+ * core used by both auto-connect and the runtime-admin pool registration.
280
+ *
281
+ * Failure policy (ADR-0062 D5): an `external` datasource with
282
+ * `validation.onMismatch: 'fail'` fails fast (re-throws, bricking boot as
283
+ * intended); everything else degrades with a warning so an optional replica's
284
+ * connectivity blip never bricks boot.
285
+ */
286
+ connect(record: ConnectableDatasource, opts?: {
287
+ objects?: readonly string[];
288
+ context?: DatasourceConnectContext;
289
+ }): Promise<ConnectResult>;
290
+ /** Gracefully disconnect a previously-registered datasource pool. */
291
+ disconnect(name: string): Promise<void>;
292
+ /**
293
+ * Apply the D5 connect-failure policy (also covers D3 credential failures). A
294
+ * code-defined `external` datasource with `onMismatch:'fail'` auto-connected at
295
+ * boot re-throws (fail-fast, bricking boot as intended). Runtime-admin
296
+ * create/update + boot rehydration always degrade-with-warning — a UI action
297
+ * or a replica blip must never brick the running server (preserves the
298
+ * pre-ADR-0062 admin behavior). Either way the datasource is left unconnected
299
+ * with a clear message — never a silent skip.
300
+ */
301
+ private handleFailure;
125
302
  }
126
303
 
127
304
  /**
@@ -161,6 +338,8 @@ interface StoredDatasource {
161
338
  }) | undefined;
162
339
  pool?: Record<string, unknown>;
163
340
  active?: boolean;
341
+ /** Force a live connection at boot even when managed + unrouted (ADR-0062 D2(c)). */
342
+ autoConnect?: boolean;
164
343
  origin?: 'code' | 'runtime';
165
344
  /** Package that defines a code-origin datasource, when known. */
166
345
  definedIn?: string;
@@ -209,6 +388,17 @@ declare class DatasourceAdminService implements IDatasourceAdminService {
209
388
  constructor(config: DatasourceAdminServiceConfig);
210
389
  private get logger();
211
390
  listDatasources(): Promise<DatasourceSummary[]>;
391
+ /**
392
+ * Read one datasource's full detail for editing, with the credential stripped.
393
+ * Returns `config` (non-sensitive — credentials live in `sys_secret`, never in
394
+ * config), `origin`, and a `hasSecret` flag so the UI can show "leave blank to
395
+ * keep" without ever receiving the `credentialsRef` or any cleartext. Returns
396
+ * `undefined` when the name is unknown.
397
+ */
398
+ getDatasource(name: string): Promise<(Pick<StoredDatasource, 'name' | 'label' | 'driver' | 'schemaMode' | 'config' | 'active' | 'definedIn'> & {
399
+ origin: 'code' | 'runtime';
400
+ hasSecret: boolean;
401
+ }) | undefined>;
212
402
  testConnection(input: DatasourceDraft, secret?: SecretInput): Promise<TestConnectionResult>;
213
403
  createDatasource(input: DatasourceDraft, secret?: SecretInput): Promise<DatasourceSummary>;
214
404
  updateDatasource(name: string, patch: Partial<DatasourceDraft>, secret?: SecretInput): Promise<DatasourceSummary>;
@@ -250,6 +440,13 @@ interface DatasourceAdminServicePluginOptions {
250
440
  secrets?: SecretBinder;
251
441
  /** Override the driver factory (defaults to the `'datasource-driver-factory'` service). */
252
442
  driverFactory?: IDatasourceDriverFactory;
443
+ /**
444
+ * Host-injectable connect policy consulted before opening any datasource
445
+ * connection (ADR-0062 D5 / epic #2163 seam). Open-core default is permissive
446
+ * (allow); a multi-tenant host binds a stricter, fail-closed policy. Shared by
447
+ * both code-defined auto-connect and runtime-admin pool registration.
448
+ */
449
+ connectPolicy?: DatasourceConnectPolicy;
253
450
  logger?: Logger;
254
451
  }
255
452
  /**
@@ -275,10 +472,14 @@ declare class DatasourceAdminServicePlugin implements Plugin {
275
472
  dependencies: string[];
276
473
  private service?;
277
474
  private config?;
475
+ /** Shared "definition → live driver" path (ADR-0062 D1); also exposed as the `'datasource-connection'` service. */
476
+ private connection?;
278
477
  private readonly options;
279
478
  constructor(options?: DatasourceAdminServicePluginOptions);
280
479
  init(ctx: PluginContext): Promise<void>;
281
480
  start(ctx: PluginContext): Promise<void>;
481
+ /** Reload persisted runtime datasource rows (sys_metadata) into the registry. */
482
+ private restoreRuntimeDatasources;
282
483
  /**
283
484
  * Boot-time rehydration: list persisted runtime datasources and re-register
284
485
  * each one's connection pool (driver build → connect → registerDriver),
@@ -290,7 +491,6 @@ declare class DatasourceAdminServicePlugin implements Plugin {
290
491
  */
291
492
  private rehydratePools;
292
493
  destroy(): Promise<void>;
293
- private toSpec;
294
494
  /** Probe a connection via the driver factory: build → connect → ping → close. */
295
495
  private probe;
296
496
  }
@@ -411,4 +611,4 @@ declare function createDatasourceSecretBinder(deps: DatasourceSecretBinderDeps):
411
611
  */
412
612
  declare function registerDatasourceAdminRoutes(server: IHttpServer, ctx: PluginContext, basePath?: string): void;
413
613
 
414
- export { DatasourceAdminService, type DatasourceAdminServiceConfig, DatasourceAdminServicePlugin, type DatasourceAdminServicePluginOptions, DatasourceDraft, type DatasourceLike, type DatasourceSecretBinder, type DatasourceSecretBinderDeps, DatasourceSummary, ExternalDatasourceService, type ExternalDatasourceServiceConfig, ExternalDatasourceServicePlugin, type ExternalDatasourceServicePluginOptions, IDatasourceAdminService, IDatasourceDriverFactory, type Logger$1 as Logger, type ObjectLike, type ProbeInput, type SecretBinder, SecretInput, type SecretStoreEngineLike, type StoredDatasource, TestConnectionResult, createDatasourceSecretBinder, createDefaultDatasourceDriverFactory, parseCredentialsRef, registerDatasourceAdminRoutes, toCredentialsRef };
614
+ export { type ConnectResult, type ConnectStatus, type ConnectableDatasource, type ConnectionEngineLike, type ConnectionSecretResolver, DatasourceAdminService, type DatasourceAdminServiceConfig, DatasourceAdminServicePlugin, type DatasourceAdminServicePluginOptions, type DatasourceBoundObject, DatasourceConnectContext, DatasourceConnectPolicy, DatasourceConnectionService, type DatasourceConnectionServiceConfig, DatasourceDraft, type DatasourceLike, type DatasourceSecretBinder, type DatasourceSecretBinderDeps, DatasourceSummary, ExternalDatasourceService, type ExternalDatasourceServiceConfig, ExternalDatasourceServicePlugin, type ExternalDatasourceServicePluginOptions, IDatasourceAdminService, IDatasourceDriverFactory, type Logger$1 as Logger, type ObjectLike, type ProbeInput, type SecretBinder, SecretInput, type SecretStoreEngineLike, type StoredDatasource, TestConnectionResult, createDatasourceSecretBinder, createDefaultDatasourceDriverFactory, isDatasourceAddressed, parseCredentialsRef, registerDatasourceAdminRoutes, toCredentialsRef };