@objectstack/service-datasource 7.6.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.
Files changed (37) hide show
  1. package/.turbo/turbo-build.log +28 -0
  2. package/CHANGELOG.md +94 -0
  3. package/LICENSE +202 -0
  4. package/LICENSE.apache +202 -0
  5. package/README.md +50 -0
  6. package/dist/contracts/index.cjs +1 -0
  7. package/dist/contracts/index.cjs.map +1 -0
  8. package/dist/contracts/index.d.cts +178 -0
  9. package/dist/contracts/index.d.ts +178 -0
  10. package/dist/contracts/index.js +1 -0
  11. package/dist/contracts/index.js.map +1 -0
  12. package/dist/index.cjs +995 -0
  13. package/dist/index.cjs.map +1 -0
  14. package/dist/index.d.cts +414 -0
  15. package/dist/index.d.ts +414 -0
  16. package/dist/index.js +995 -0
  17. package/dist/index.js.map +1 -0
  18. package/package.json +61 -0
  19. package/src/__tests__/admin-routes.test.ts +106 -0
  20. package/src/__tests__/datasource-admin-plugin.test.ts +231 -0
  21. package/src/__tests__/datasource-admin-service.test.ts +288 -0
  22. package/src/__tests__/datasource-secret-binder.test.ts +101 -0
  23. package/src/__tests__/external-datasource-service.test.ts +360 -0
  24. package/src/admin-routes.ts +117 -0
  25. package/src/contracts/datasource-admin-service.ts +119 -0
  26. package/src/contracts/datasource-driver-factory.ts +77 -0
  27. package/src/contracts/index.ts +18 -0
  28. package/src/datasource-admin-plugin.ts +362 -0
  29. package/src/datasource-admin-service.ts +297 -0
  30. package/src/datasource-secret-binder.ts +144 -0
  31. package/src/default-datasource-driver-factory.ts +185 -0
  32. package/src/external-datasource-service.ts +456 -0
  33. package/src/index.ts +73 -0
  34. package/src/logger.ts +11 -0
  35. package/src/plugin.ts +119 -0
  36. package/tsconfig.json +17 -0
  37. package/tsup.config.ts +19 -0
package/dist/index.cjs ADDED
@@ -0,0 +1,995 @@
1
+ "use strict";Object.defineProperty(exports, "__esModule", {value: true}); function _interopRequireWildcard(obj) { if (obj && obj.__esModule) { return obj; } else { var newObj = {}; if (obj != null) { for (var key in obj) { if (Object.prototype.hasOwnProperty.call(obj, key)) { newObj[key] = obj[key]; } } } newObj.default = obj; return newObj; } } function _nullishCoalesce(lhs, rhsFn) { if (lhs != null) { return lhs; } else { return rhsFn(); } } async function _asyncNullishCoalesce(lhs, rhsFn) { if (lhs != null) { return lhs; } else { return await rhsFn(); } } function _optionalChain(ops) { let lastAccessLHS = undefined; let value = ops[0]; let i = 1; while (i < ops.length) { const op = ops[i]; const fn = ops[i + 1]; i += 2; if ((op === 'optionalAccess' || op === 'optionalCall') && value == null) { return undefined; } if (op === 'access' || op === 'optionalAccess') { lastAccessLHS = value; value = fn(value); } else if (op === 'call' || op === 'optionalCall') { value = fn((...args) => value.call(lastAccessLHS, ...args)); lastAccessLHS = undefined; } } return value; }// src/external-datasource-service.ts
2
+
3
+
4
+
5
+
6
+ var _data = require('@objectstack/spec/data');
7
+ var BUILTIN_COLUMNS = /* @__PURE__ */ new Set(["id", "created_at", "updated_at"]);
8
+ function parseQualified(raw) {
9
+ const idx = raw.indexOf(".");
10
+ if (idx === -1) return { name: raw };
11
+ return { schema: raw.slice(0, idx), name: raw.slice(idx + 1) };
12
+ }
13
+ function toObjectName(remoteName) {
14
+ const { name } = parseQualified(remoteName);
15
+ return name.replace(/[^a-zA-Z0-9_]/g, "_").replace(/^[^a-z_]/, (c) => `_${c.toLowerCase()}`).toLowerCase();
16
+ }
17
+ function toLabel(name) {
18
+ return name.split("_").filter(Boolean).map((w) => w.charAt(0).toUpperCase() + w.slice(1)).join(" ");
19
+ }
20
+ var ExternalDatasourceService = class {
21
+ constructor(config) {
22
+ this.config = config;
23
+ }
24
+ get logger() {
25
+ return this.config.logger;
26
+ }
27
+ findTable(schema, remoteName) {
28
+ const want = parseQualified(remoteName).name;
29
+ for (const table of Object.values(schema.tables)) {
30
+ if (table.name === remoteName) return table;
31
+ if (parseQualified(table.name).name === want) return table;
32
+ }
33
+ return void 0;
34
+ }
35
+ async listRemoteTables(datasource, opts) {
36
+ const [schema, ds] = await Promise.all([
37
+ this.config.introspect(datasource),
38
+ this.config.getDatasource(datasource)
39
+ ]);
40
+ const allowed = _optionalChain([ds, 'optionalAccess', _ => _.external, 'optionalAccess', _2 => _2.allowedSchemas]);
41
+ const tables = [];
42
+ for (const table of Object.values(schema.tables)) {
43
+ const { schema: tableSchema, name } = parseQualified(table.name);
44
+ if (_optionalChain([opts, 'optionalAccess', _3 => _3.schema]) && tableSchema && tableSchema !== opts.schema) continue;
45
+ if (allowed && tableSchema && !allowed.includes(tableSchema)) continue;
46
+ tables.push({ schema: tableSchema, name, columnCount: table.columns.length });
47
+ }
48
+ return tables;
49
+ }
50
+ async generateObjectDraft(datasource, remoteName, opts = {}) {
51
+ const schema = await this.config.introspect(datasource);
52
+ const table = this.findTable(schema, remoteName);
53
+ if (!table) {
54
+ throw new Error(
55
+ `Remote table '${remoteName}' not found on datasource '${datasource}'.`
56
+ );
57
+ }
58
+ const dialect = schema.dialect;
59
+ const matched = parseQualified(table.name);
60
+ const remoteSchema = _nullishCoalesce(opts.remoteSchema, () => ( matched.schema));
61
+ const resolvedRemoteName = matched.name;
62
+ const include = opts.includeColumns ? new Set(opts.includeColumns) : void 0;
63
+ const exclude = opts.excludeColumns ? new Set(opts.excludeColumns) : /* @__PURE__ */ new Set();
64
+ const pkOverride = opts.primaryKey ? new Set(opts.primaryKey) : void 0;
65
+ const fields = {};
66
+ const review = [];
67
+ for (const col of table.columns) {
68
+ if (include && !include.has(col.name)) continue;
69
+ if (exclude.has(col.name)) continue;
70
+ const fieldName = _nullishCoalesce(_optionalChain([opts, 'access', _4 => _4.rename, 'optionalAccess', _5 => _5[col.name]]), () => ( col.name));
71
+ const suggested = _data.suggestFieldType.call(void 0, col.type, dialect);
72
+ const fieldType = _nullishCoalesce(suggested, () => ( "text"));
73
+ if (!suggested) {
74
+ review.push({
75
+ column: col.name,
76
+ remoteType: col.type,
77
+ note: `unrecognised remote type \u2014 defaulted to 'text', verify`
78
+ });
79
+ } else if (_data.isCompatible.call(void 0, col.type, fieldType, dialect) === "lossy") {
80
+ review.push({
81
+ column: col.name,
82
+ remoteType: col.type,
83
+ note: `mapped lossy to '${fieldType}'`
84
+ });
85
+ }
86
+ const isPk = pkOverride ? pkOverride.has(col.name) : col.primaryKey;
87
+ fields[fieldName] = isPk ? { type: fieldType, primaryKey: true } : { type: fieldType };
88
+ }
89
+ const name = toObjectName(resolvedRemoteName);
90
+ const definition = {
91
+ name,
92
+ label: toLabel(name),
93
+ datasource,
94
+ external: {
95
+ ...remoteSchema ? { remoteSchema } : {},
96
+ remoteName: resolvedRemoteName
97
+ },
98
+ fields
99
+ };
100
+ return {
101
+ name,
102
+ datasource,
103
+ definition,
104
+ source: renderObjectSource(definition, fields, review),
105
+ review
106
+ };
107
+ }
108
+ async importObject(datasource, remoteName, opts = {}) {
109
+ if (!this.config.persistObject) {
110
+ throw new Error(
111
+ `importObject requires a writable metadata store, but none is wired (datasource '${datasource}'). This deployment may be GitOps-only \u2014 use 'os datasource introspect' and commit the generated *.object.ts instead.`
112
+ );
113
+ }
114
+ const draft = await this.generateObjectDraft(datasource, remoteName, opts);
115
+ const name = _nullishCoalesce(opts.name, () => ( draft.name));
116
+ const external = {
117
+ ...draft.definition.external,
118
+ ...opts.writable ? { writable: true } : {}
119
+ };
120
+ const definition = {
121
+ ...draft.definition,
122
+ name,
123
+ label: toLabel(name),
124
+ external
125
+ };
126
+ await this.config.persistObject(name, definition);
127
+ _optionalChain([this, 'access', _6 => _6.logger, 'optionalAccess', _7 => _7.info, 'optionalCall', _8 => _8(`importObject: persisted '${name}' from ${datasource}.${remoteName}`, {
128
+ writable: opts.writable === true,
129
+ review: draft.review.length
130
+ })]);
131
+ return { name, definition, review: draft.review };
132
+ }
133
+ async refreshCatalog(datasource) {
134
+ const schema = await this.config.introspect(datasource);
135
+ const catalog = _data.ExternalCatalogSchema.parse({
136
+ name: `${datasource}_catalog`,
137
+ datasource,
138
+ snapshotAt: (/* @__PURE__ */ new Date()).toISOString(),
139
+ dialect: schema.dialect,
140
+ tables: Object.values(schema.tables).map((t) => {
141
+ const { schema: s, name } = parseQualified(t.name);
142
+ return {
143
+ remoteSchema: s,
144
+ remoteName: name,
145
+ columns: t.columns.map((c) => ({
146
+ name: c.name,
147
+ sqlType: c.type,
148
+ nullable: c.nullable,
149
+ primaryKey: c.primaryKey,
150
+ suggestedFieldType: _data.suggestFieldType.call(void 0, c.type, schema.dialect)
151
+ }))
152
+ };
153
+ })
154
+ });
155
+ if (this.config.persistCatalog) {
156
+ try {
157
+ await this.config.persistCatalog(catalog);
158
+ } catch (err) {
159
+ _optionalChain([this, 'access', _9 => _9.logger, 'optionalAccess', _10 => _10.warn, 'optionalCall', _11 => _11(`refreshCatalog: failed to persist '${catalog.name}'`, err)]);
160
+ }
161
+ }
162
+ return catalog;
163
+ }
164
+ async validateObject(objectName) {
165
+ const obj = await this.config.getObject(objectName);
166
+ if (!obj) {
167
+ throw new Error(`Object '${objectName}' not found.`);
168
+ }
169
+ const datasource = _nullishCoalesce(obj.datasource, () => ( "default"));
170
+ const ds = await this.config.getDatasource(datasource);
171
+ if (!ds || !ds.schemaMode || ds.schemaMode === "managed") {
172
+ return { ok: true, datasource, object: objectName, diffs: [] };
173
+ }
174
+ const schema = await this.config.introspect(datasource);
175
+ const dialect = schema.dialect;
176
+ const remoteName = _nullishCoalesce(_optionalChain([obj, 'access', _12 => _12.external, 'optionalAccess', _13 => _13.remoteName]), () => ( obj.name));
177
+ const table = this.findTable(schema, remoteName);
178
+ const diffs = [];
179
+ if (!table) {
180
+ diffs.push({
181
+ kind: "missing_table",
182
+ remoteSchema: _optionalChain([obj, 'access', _14 => _14.external, 'optionalAccess', _15 => _15.remoteSchema]),
183
+ remoteName,
184
+ severity: "error"
185
+ });
186
+ return { ok: false, datasource, object: objectName, diffs };
187
+ }
188
+ const columnsByName = new Map(table.columns.map((c) => [c.name, c]));
189
+ const ignore = new Set(_nullishCoalesce(_optionalChain([obj, 'access', _16 => _16.external, 'optionalAccess', _17 => _17.ignoreColumns]), () => ( [])));
190
+ const fieldToRemote = /* @__PURE__ */ new Map();
191
+ for (const [remoteCol, fieldName] of Object.entries(_nullishCoalesce(_optionalChain([obj, 'access', _18 => _18.external, 'optionalAccess', _19 => _19.columnMap]), () => ( {})))) {
192
+ fieldToRemote.set(fieldName, remoteCol);
193
+ }
194
+ for (const [fieldName, field] of Object.entries(_nullishCoalesce(obj.fields, () => ( {})))) {
195
+ if (BUILTIN_COLUMNS.has(fieldName)) continue;
196
+ const remoteCol = _nullishCoalesce(fieldToRemote.get(fieldName), () => ( fieldName));
197
+ if (ignore.has(remoteCol)) continue;
198
+ const col = columnsByName.get(remoteCol);
199
+ if (!col) {
200
+ diffs.push({
201
+ kind: "missing_column",
202
+ remoteName,
203
+ column: remoteCol,
204
+ severity: "error"
205
+ });
206
+ continue;
207
+ }
208
+ const fieldType = _nullishCoalesce(field.type, () => ( "text"));
209
+ const compat = _data.isCompatible.call(void 0, col.type, fieldType, dialect);
210
+ if (compat === false) {
211
+ diffs.push({
212
+ kind: "type_mismatch",
213
+ remoteName,
214
+ column: remoteCol,
215
+ expected: fieldType,
216
+ actual: col.type,
217
+ severity: "error"
218
+ });
219
+ } else if (compat === "lossy") {
220
+ diffs.push({
221
+ kind: "type_mismatch",
222
+ remoteName,
223
+ column: remoteCol,
224
+ expected: fieldType,
225
+ actual: col.type,
226
+ severity: "warning"
227
+ });
228
+ }
229
+ }
230
+ const ok = !diffs.some((d) => d.severity === "error");
231
+ return { ok, datasource, object: objectName, diffs };
232
+ }
233
+ async validateAll() {
234
+ const objects = await this.config.listObjects();
235
+ const federated = objects.filter(
236
+ (o) => o.external !== void 0 || o.datasource && o.datasource !== "default"
237
+ );
238
+ const results = await Promise.all(
239
+ federated.map(
240
+ (o) => this.validateObject(o.name).catch((err) => {
241
+ _optionalChain([this, 'access', _20 => _20.logger, 'optionalAccess', _21 => _21.warn, 'call', _22 => _22(`validateObject('${o.name}') failed`, err)]);
242
+ return {
243
+ ok: false,
244
+ datasource: _nullishCoalesce(o.datasource, () => ( "default")),
245
+ object: o.name,
246
+ diffs: [
247
+ {
248
+ kind: "missing_table",
249
+ remoteName: _nullishCoalesce(_optionalChain([o, 'access', _23 => _23.external, 'optionalAccess', _24 => _24.remoteName]), () => ( o.name)),
250
+ actual: err instanceof Error ? err.message : String(err),
251
+ severity: "error"
252
+ }
253
+ ]
254
+ };
255
+ })
256
+ )
257
+ );
258
+ const ok = results.every((r) => r.ok);
259
+ return { ok, results };
260
+ }
261
+ };
262
+ function renderObjectSource(definition, fields, review) {
263
+ const reviewByColumn = new Map(review.map((r) => [r.column, r.note]));
264
+ const external = definition.external;
265
+ const fieldLines = Object.entries(fields).map(([fieldName, f]) => {
266
+ const note = reviewByColumn.get(fieldName);
267
+ const pk = f.primaryKey ? ", primaryKey: true" : "";
268
+ const comment = note ? ` // REVIEW: ${note}` : "";
269
+ return ` ${fieldName}: { type: '${f.type}'${pk} },${comment}`;
270
+ });
271
+ const externalLine = external.remoteSchema ? ` external: { remoteSchema: '${external.remoteSchema}', remoteName: '${external.remoteName}' },` : ` external: { remoteName: '${external.remoteName}' },`;
272
+ return [
273
+ `// Generated by \`os datasource introspect\` (ADR-0015). Review before committing.`,
274
+ `import type { ServiceObjectInput } from '@objectstack/spec/data';`,
275
+ ``,
276
+ `const ${definition.name}: ServiceObjectInput = {`,
277
+ ` name: '${definition.name}',`,
278
+ ` label: '${definition.label}',`,
279
+ ` datasource: '${definition.datasource}',`,
280
+ externalLine,
281
+ ` fields: {`,
282
+ ...fieldLines,
283
+ ` },`,
284
+ `};`,
285
+ ``,
286
+ `export default ${definition.name};`,
287
+ ``
288
+ ].join("\n");
289
+ }
290
+
291
+ // src/plugin.ts
292
+ var ExternalDatasourceServicePlugin = class {
293
+ constructor(options = {}) {
294
+ this.name = "com.objectstack.service-external-datasource";
295
+ this.version = "1.0.0";
296
+ this.type = "standard";
297
+ this.dependencies = [];
298
+ this.options = options;
299
+ }
300
+ async init(ctx) {
301
+ const engine = safeGetService(ctx, "data");
302
+ const metadata = safeGetService(ctx, "metadata");
303
+ const introspect = _nullishCoalesce(this.options.introspect, () => ( (async (datasource) => {
304
+ if (_optionalChain([engine, 'optionalAccess', _25 => _25.introspectDatasource])) return engine.introspectDatasource(datasource);
305
+ const driver = _optionalChain([engine, 'optionalAccess', _26 => _26.getDatasourceDriver, 'optionalCall', _27 => _27(datasource)]);
306
+ if (_optionalChain([driver, 'optionalAccess', _28 => _28.introspectSchema])) return driver.introspectSchema();
307
+ throw new Error(
308
+ `Cannot introspect datasource '${datasource}': no driver introspection available.`
309
+ );
310
+ })));
311
+ const config = {
312
+ introspect,
313
+ getDatasource: async (n) => await _optionalChain([metadata, 'optionalAccess', _29 => _29.get, 'call', _30 => _30("datasource", n)]),
314
+ getObject: async (n) => _optionalChain([metadata, 'optionalAccess', _31 => _31.getObject]) ? await metadata.getObject(n) : await _optionalChain([metadata, 'optionalAccess', _32 => _32.get, 'call', _33 => _33("object", n)]),
315
+ listObjects: async () => await _asyncNullishCoalesce((_optionalChain([metadata, 'optionalAccess', _34 => _34.listObjects]) ? await metadata.listObjects() : await _optionalChain([metadata, 'optionalAccess', _35 => _35.list, 'optionalCall', _36 => _36("object")])), async () => ( [])),
316
+ // Persist the refreshed snapshot as an `external_catalog` metadata record
317
+ // so the boot gate + Studio's schema browser can read it without
318
+ // re-introspecting. No-op when the metadata service can't write.
319
+ ..._optionalChain([metadata, 'optionalAccess', _37 => _37.register]) ? {
320
+ persistCatalog: async (catalog) => {
321
+ await metadata.register("external_catalog", catalog.name, catalog);
322
+ },
323
+ // Runtime "Import as Object": persist a federated object so it's
324
+ // immediately queryable, no git commit required (ADR-0015 Addendum).
325
+ persistObject: async (name, definition) => {
326
+ await metadata.register("object", name, definition);
327
+ }
328
+ } : {},
329
+ logger: this.options.logger
330
+ };
331
+ this.service = new ExternalDatasourceService(config);
332
+ ctx.registerService("external-datasource", this.service);
333
+ }
334
+ async start(ctx) {
335
+ if (this.service) await ctx.trigger("external-datasource:ready", this.service);
336
+ }
337
+ async destroy() {
338
+ this.service = void 0;
339
+ }
340
+ };
341
+ function safeGetService(ctx, name) {
342
+ try {
343
+ return ctx.getService(name);
344
+ } catch (e) {
345
+ return void 0;
346
+ }
347
+ }
348
+
349
+ // src/datasource-admin-service.ts
350
+ var NAME_RE = /^[a-z_][a-z0-9_]*$/;
351
+ var DatasourceAdminService = class {
352
+ constructor(config) {
353
+ this.config = config;
354
+ }
355
+ get logger() {
356
+ return this.config.logger;
357
+ }
358
+ async listDatasources() {
359
+ const records = await this.config.listDatasourceRecords();
360
+ const byName = /* @__PURE__ */ new Map();
361
+ for (const rec of records) {
362
+ const slot = _nullishCoalesce(byName.get(rec.name), () => ( {}));
363
+ if (rec.origin === "runtime") slot.runtime = rec;
364
+ else slot.code = rec;
365
+ byName.set(rec.name, slot);
366
+ }
367
+ const summaries = [];
368
+ for (const [name, slot] of byName) {
369
+ const effective = _nullishCoalesce(slot.code, () => ( slot.runtime));
370
+ if (!effective) continue;
371
+ summaries.push({
372
+ name,
373
+ label: effective.label,
374
+ driver: effective.driver,
375
+ schemaMode: _nullishCoalesce(effective.schemaMode, () => ( "managed")),
376
+ origin: slot.code ? "code" : "runtime",
377
+ active: _nullishCoalesce(effective.active, () => ( true)),
378
+ status: "unvalidated",
379
+ ..._optionalChain([slot, 'access', _38 => _38.code, 'optionalAccess', _39 => _39.definedIn]) ? { definedIn: slot.code.definedIn } : {},
380
+ ...slot.code && slot.runtime ? { conflictsWithCode: true } : {}
381
+ });
382
+ }
383
+ return summaries;
384
+ }
385
+ async testConnection(input, secret) {
386
+ if (!_optionalChain([input, 'optionalAccess', _40 => _40.driver])) {
387
+ return { ok: false, error: "A driver is required to test a connection." };
388
+ }
389
+ const queryTimeoutMs = _optionalChain([input, 'access', _41 => _41.external, 'optionalAccess', _42 => _42.queryTimeoutMs]);
390
+ try {
391
+ return await this.config.probe({
392
+ driver: input.driver,
393
+ config: _nullishCoalesce(input.config, () => ( {})),
394
+ secret: _optionalChain([secret, 'optionalAccess', _43 => _43.value]),
395
+ external: input.external,
396
+ ...typeof queryTimeoutMs === "number" ? { timeoutMs: queryTimeoutMs } : {}
397
+ });
398
+ } catch (err) {
399
+ return { ok: false, error: err instanceof Error ? err.message : String(err) };
400
+ }
401
+ }
402
+ async createDatasource(input, secret) {
403
+ this.assertValidName(_optionalChain([input, 'optionalAccess', _44 => _44.name]));
404
+ if (!input.driver) throw new Error("A driver is required to create a datasource.");
405
+ const existing = await this.config.getDatasourceRecord(input.name);
406
+ if (existing) {
407
+ if (existing.origin === "code" || existing.origin === void 0) {
408
+ throw new Error(
409
+ `Cannot create datasource '${input.name}': a code-defined datasource owns this name (read-only).`
410
+ );
411
+ }
412
+ throw new Error(`Datasource '${input.name}' already exists.`);
413
+ }
414
+ const record = {
415
+ ...this.toRecord(input),
416
+ origin: "runtime"
417
+ };
418
+ if (secret) {
419
+ const credentialsRef = await this.config.writeSecret(secret, { name: input.name });
420
+ record.external = { ..._nullishCoalesce(record.external, () => ( {})), credentialsRef };
421
+ }
422
+ await this.config.putDatasourceRecord(record);
423
+ await this.tryRegisterPool(record);
424
+ return this.toSummary(record);
425
+ }
426
+ async updateDatasource(name, patch, secret) {
427
+ const existing = await this.config.getDatasourceRecord(name);
428
+ if (!existing) throw new Error(`Datasource '${name}' not found.`);
429
+ if (existing.origin !== "runtime") {
430
+ throw new Error(`Datasource '${name}' is code-defined and cannot be edited at runtime.`);
431
+ }
432
+ const merged = {
433
+ ...existing,
434
+ ...patch.label !== void 0 ? { label: patch.label } : {},
435
+ ...patch.driver !== void 0 ? { driver: patch.driver } : {},
436
+ ...patch.schemaMode !== void 0 ? { schemaMode: patch.schemaMode } : {},
437
+ ...patch.config !== void 0 ? { config: patch.config } : {},
438
+ ...patch.pool !== void 0 ? { pool: patch.pool } : {},
439
+ ...patch.active !== void 0 ? { active: patch.active } : {},
440
+ name: existing.name,
441
+ origin: "runtime"
442
+ };
443
+ if (patch.external !== void 0) {
444
+ merged.external = { ...patch.external, credentialsRef: _optionalChain([existing, 'access', _45 => _45.external, 'optionalAccess', _46 => _46.credentialsRef]) };
445
+ }
446
+ if (secret) {
447
+ const prevRef = _optionalChain([existing, 'access', _47 => _47.external, 'optionalAccess', _48 => _48.credentialsRef]);
448
+ const credentialsRef = await this.config.writeSecret(secret, { name });
449
+ merged.external = { ..._nullishCoalesce(merged.external, () => ( {})), credentialsRef };
450
+ if (prevRef && prevRef !== credentialsRef) await this.tryRemoveSecret(prevRef);
451
+ }
452
+ await this.config.putDatasourceRecord(merged);
453
+ await this.tryRegisterPool(merged);
454
+ return this.toSummary(merged);
455
+ }
456
+ async removeDatasource(name) {
457
+ const existing = await this.config.getDatasourceRecord(name);
458
+ if (!existing) throw new Error(`Datasource '${name}' not found.`);
459
+ if (existing.origin !== "runtime") {
460
+ throw new Error(`Datasource '${name}' is code-defined and cannot be removed at runtime.`);
461
+ }
462
+ const bound = await this.config.countBoundObjects(name);
463
+ if (bound > 0) {
464
+ throw new Error(
465
+ `Cannot remove datasource '${name}': ${bound} object(s) are still bound to it.`
466
+ );
467
+ }
468
+ await this.config.deleteDatasourceRecord(name);
469
+ if (_optionalChain([existing, 'access', _49 => _49.external, 'optionalAccess', _50 => _50.credentialsRef])) await this.tryRemoveSecret(existing.external.credentialsRef);
470
+ await this.tryUnregisterPool(name);
471
+ }
472
+ // --- internals -----------------------------------------------------------
473
+ assertValidName(name) {
474
+ if (!name || !NAME_RE.test(name)) {
475
+ throw new Error(
476
+ `Invalid datasource name '${_nullishCoalesce(name, () => ( ""))}': must match /^[a-z_][a-z0-9_]*$/.`
477
+ );
478
+ }
479
+ }
480
+ toRecord(input) {
481
+ return {
482
+ name: input.name,
483
+ ...input.label !== void 0 ? { label: input.label } : {},
484
+ driver: input.driver,
485
+ ...input.schemaMode !== void 0 ? { schemaMode: input.schemaMode } : {},
486
+ ...input.config !== void 0 ? { config: input.config } : {},
487
+ ...input.external !== void 0 ? { external: input.external } : {},
488
+ ...input.pool !== void 0 ? { pool: input.pool } : {},
489
+ ...input.active !== void 0 ? { active: input.active } : {}
490
+ };
491
+ }
492
+ toSummary(record) {
493
+ return {
494
+ name: record.name,
495
+ label: record.label,
496
+ driver: record.driver,
497
+ schemaMode: _nullishCoalesce(record.schemaMode, () => ( "managed")),
498
+ origin: _nullishCoalesce(record.origin, () => ( "runtime")),
499
+ active: _nullishCoalesce(record.active, () => ( true)),
500
+ status: "unvalidated"
501
+ };
502
+ }
503
+ async tryRegisterPool(record) {
504
+ try {
505
+ await _optionalChain([this, 'access', _51 => _51.config, 'access', _52 => _52.registerPool, 'optionalCall', _53 => _53(record)]);
506
+ } catch (err) {
507
+ _optionalChain([this, 'access', _54 => _54.logger, 'optionalAccess', _55 => _55.warn, 'call', _56 => _56(`registerPool('${record.name}') failed`, err)]);
508
+ }
509
+ }
510
+ async tryUnregisterPool(name) {
511
+ try {
512
+ await _optionalChain([this, 'access', _57 => _57.config, 'access', _58 => _58.unregisterPool, 'optionalCall', _59 => _59(name)]);
513
+ } catch (err) {
514
+ _optionalChain([this, 'access', _60 => _60.logger, 'optionalAccess', _61 => _61.warn, 'call', _62 => _62(`unregisterPool('${name}') failed`, err)]);
515
+ }
516
+ }
517
+ async tryRemoveSecret(credentialsRef) {
518
+ try {
519
+ await _optionalChain([this, 'access', _63 => _63.config, 'access', _64 => _64.removeSecret, 'optionalCall', _65 => _65(credentialsRef)]);
520
+ } catch (err) {
521
+ _optionalChain([this, 'access', _66 => _66.logger, 'optionalAccess', _67 => _67.warn, 'call', _68 => _68(`removeSecret('${credentialsRef}') failed`, err)]);
522
+ }
523
+ }
524
+ };
525
+
526
+ // src/datasource-admin-plugin.ts
527
+ var _kernel = require('@objectstack/spec/kernel');
528
+ var DatasourceAdminServicePlugin = class {
529
+ constructor(options = {}) {
530
+ this.name = "com.objectstack.service-datasource-admin";
531
+ this.version = "1.0.0";
532
+ this.type = "standard";
533
+ this.dependencies = [];
534
+ this.options = options;
535
+ }
536
+ async init(ctx) {
537
+ const logger = this.options.logger;
538
+ _kernel.registerMetadataTypeActions.call(void 0, "datasource", [
539
+ {
540
+ name: "test_connection",
541
+ label: "Test connection",
542
+ icon: "plug-zap",
543
+ type: "api",
544
+ target: "/api/v1/datasources/${ctx.recordId}/test",
545
+ method: "POST",
546
+ variant: "secondary",
547
+ refreshAfter: false,
548
+ locations: ["record_header", "list_item"]
549
+ }
550
+ ]);
551
+ const metadataOf = () => safeGetService2(ctx, "metadata");
552
+ const engineOf = () => safeGetService2(ctx, "data");
553
+ const factory = () => _nullishCoalesce(this.options.driverFactory, () => ( safeGetService2(ctx, "datasource-driver-factory")));
554
+ const config = {
555
+ probe: (input) => this.probe(factory(), input),
556
+ listDatasourceRecords: async () => {
557
+ const rows = await _asyncNullishCoalesce(await _optionalChain([metadataOf, 'call', _69 => _69(), 'optionalAccess', _70 => _70.list, 'call', _71 => _71("datasource")]), async () => ( []));
558
+ return rows.map((r) => ({ ...r, origin: _nullishCoalesce(r.origin, () => ( "code")) }));
559
+ },
560
+ getDatasourceRecord: async (name) => {
561
+ const row = await _optionalChain([metadataOf, 'call', _72 => _72(), 'optionalAccess', _73 => _73.get, 'call', _74 => _74("datasource", name)]);
562
+ return row ? { ...row, origin: _nullishCoalesce(row.origin, () => ( "code")) } : void 0;
563
+ },
564
+ putDatasourceRecord: async (record) => {
565
+ const metadata = metadataOf();
566
+ if (!_optionalChain([metadata, 'optionalAccess', _75 => _75.register])) {
567
+ throw new Error("Metadata service is unavailable; cannot persist datasource.");
568
+ }
569
+ await metadata.register("datasource", record.name, record);
570
+ },
571
+ deleteDatasourceRecord: async (name) => {
572
+ const metadata = metadataOf();
573
+ if (!_optionalChain([metadata, 'optionalAccess', _76 => _76.unregister])) {
574
+ throw new Error("Metadata service is unavailable; cannot remove datasource.");
575
+ }
576
+ await metadata.unregister("datasource", name);
577
+ },
578
+ writeSecret: async (input, hint) => {
579
+ const binder = this.options.secrets;
580
+ if (!_optionalChain([binder, 'optionalAccess', _77 => _77.bind])) {
581
+ throw new Error(
582
+ "No secret store configured: refusing to persist a datasource credential in cleartext. Wire a SecretBinder (CryptoProvider + sys_secret) into DatasourceAdminServicePlugin."
583
+ );
584
+ }
585
+ return binder.bind(input, hint);
586
+ },
587
+ removeSecret: async (ref) => {
588
+ await _optionalChain([this, 'access', _78 => _78.options, 'access', _79 => _79.secrets, 'optionalAccess', _80 => _80.unbind, 'optionalCall', _81 => _81(ref)]);
589
+ },
590
+ countBoundObjects: async (datasource) => {
591
+ const metadata = metadataOf();
592
+ const objects = await _asyncNullishCoalesce(await _asyncNullishCoalesce(await _optionalChain([metadata, 'optionalAccess', _82 => _82.listObjects, 'optionalCall', _83 => _83()]), async () => ( await _optionalChain([metadata, 'optionalAccess', _84 => _84.list, 'call', _85 => _85("object")]))), async () => ( []));
593
+ return objects.filter((o) => _optionalChain([o, 'optionalAccess', _86 => _86.datasource]) === datasource).length;
594
+ },
595
+ registerPool: async (record) => {
596
+ const f = factory();
597
+ const engine = engineOf();
598
+ if (!f || !_optionalChain([engine, 'optionalAccess', _87 => _87.registerDriver]) || !f.supports(record.driver)) return;
599
+ const credentialsRef = _optionalChain([record, 'access', _88 => _88.external, 'optionalAccess', _89 => _89.credentialsRef]);
600
+ const secret = credentialsRef ? await _optionalChain([this, 'access', _90 => _90.options, 'access', _91 => _91.secrets, 'optionalAccess', _92 => _92.resolve, 'optionalCall', _93 => _93(credentialsRef)]) : void 0;
601
+ const handle = await f.create({ ...this.toSpec(record), ...secret ? { secret } : {} });
602
+ if (typeof _optionalChain([handle, 'optionalAccess', _94 => _94.connect]) === "function") await handle.connect();
603
+ const engineDriver = _nullishCoalesce(handle.driver, () => ( handle));
604
+ try {
605
+ engineDriver.name = record.name;
606
+ } catch (e2) {
607
+ }
608
+ engine.registerDriver(engineDriver);
609
+ _optionalChain([engine, 'access', _95 => _95.registerDatasourceDef, 'optionalCall', _96 => _96({
610
+ name: record.name,
611
+ schemaMode: record.schemaMode,
612
+ external: record.external
613
+ })]);
614
+ },
615
+ unregisterPool: async (name) => {
616
+ const driver = _optionalChain([engineOf, 'call', _97 => _97(), 'optionalAccess', _98 => _98.getDriverByName, 'optionalCall', _99 => _99(name)]);
617
+ if (typeof _optionalChain([driver, 'optionalAccess', _100 => _100.disconnect]) === "function") await driver.disconnect();
618
+ },
619
+ logger
620
+ };
621
+ this.config = config;
622
+ this.service = new DatasourceAdminService(config);
623
+ ctx.registerService("datasource-admin", this.service);
624
+ }
625
+ async start(ctx) {
626
+ await this.rehydratePools();
627
+ if (this.service) await ctx.trigger("datasource-admin:ready", this.service);
628
+ }
629
+ /**
630
+ * Boot-time rehydration: list persisted runtime datasources and re-register
631
+ * each one's connection pool (driver build → connect → registerDriver),
632
+ * decrypting its `sys_secret` credential on the way via the configured
633
+ * `registerPool` (which resolves `credentialsRef`). Code-defined datasources
634
+ * are owned by the host stack's own boot path and skipped here. Entirely
635
+ * best-effort: a missing factory/engine, an unpersisted dev store (nothing
636
+ * to rehydrate), or a single failing pool never blocks boot.
637
+ */
638
+ async rehydratePools() {
639
+ const cfg = this.config;
640
+ if (!_optionalChain([cfg, 'optionalAccess', _101 => _101.registerPool]) || !cfg.listDatasourceRecords) return;
641
+ let records;
642
+ try {
643
+ records = await cfg.listDatasourceRecords();
644
+ } catch (err) {
645
+ _optionalChain([this, 'access', _102 => _102.options, 'access', _103 => _103.logger, 'optionalAccess', _104 => _104.warn, 'optionalCall', _105 => _105("datasource rehydrate: listing records failed", err)]);
646
+ return;
647
+ }
648
+ const runtime = records.filter((r) => r.origin === "runtime" && (_nullishCoalesce(r.active, () => ( true))));
649
+ if (runtime.length === 0) return;
650
+ let registered = 0;
651
+ for (const record of runtime) {
652
+ try {
653
+ await cfg.registerPool(record);
654
+ registered++;
655
+ } catch (err) {
656
+ _optionalChain([this, 'access', _106 => _106.options, 'access', _107 => _107.logger, 'optionalAccess', _108 => _108.warn, 'optionalCall', _109 => _109(`datasource rehydrate: pool '${record.name}' failed`, err)]);
657
+ }
658
+ }
659
+ _optionalChain([this, 'access', _110 => _110.options, 'access', _111 => _111.logger, 'optionalAccess', _112 => _112.info, 'optionalCall', _113 => _113(
660
+ `Rehydrated ${registered}/${runtime.length} runtime datasource pool(s) on boot`
661
+ )]);
662
+ }
663
+ async destroy() {
664
+ this.service = void 0;
665
+ }
666
+ // --- internals -----------------------------------------------------------
667
+ toSpec(record) {
668
+ return {
669
+ name: record.name,
670
+ driver: record.driver,
671
+ config: _nullishCoalesce(record.config, () => ( {})),
672
+ external: record.external,
673
+ pool: record.pool
674
+ };
675
+ }
676
+ /** Probe a connection via the driver factory: build → connect → ping → close. */
677
+ async probe(factory, input) {
678
+ if (!factory) {
679
+ return { ok: false, error: "No driver factory is registered to test connections." };
680
+ }
681
+ if (!factory.supports(input.driver)) {
682
+ return { ok: false, error: `No driver factory supports driver '${input.driver}'.` };
683
+ }
684
+ let driver;
685
+ try {
686
+ driver = await factory.create({
687
+ driver: input.driver,
688
+ config: input.config,
689
+ secret: input.secret,
690
+ external: input.external
691
+ });
692
+ } catch (err) {
693
+ return { ok: false, error: `Failed to build driver: ${errMsg(err)}` };
694
+ }
695
+ const startedAt = monotonicNow();
696
+ try {
697
+ if (typeof _optionalChain([driver, 'optionalAccess', _114 => _114.connect]) === "function") await driver.connect();
698
+ if (typeof _optionalChain([driver, 'optionalAccess', _115 => _115.ping]) === "function") await driver.ping();
699
+ else if (typeof _optionalChain([driver, 'optionalAccess', _116 => _116.checkHealth]) === "function") await driver.checkHealth();
700
+ else if (typeof _optionalChain([driver, 'optionalAccess', _117 => _117.introspectSchema]) === "function") await driver.introspectSchema();
701
+ const latencyMs = elapsedSince(startedAt);
702
+ let serverVersion;
703
+ try {
704
+ serverVersion = typeof _optionalChain([driver, 'optionalAccess', _118 => _118.serverVersion]) === "function" ? await driver.serverVersion() : void 0;
705
+ } catch (e3) {
706
+ }
707
+ return { ok: true, latencyMs, ...serverVersion ? { serverVersion } : {} };
708
+ } catch (err) {
709
+ return { ok: false, error: errMsg(err) };
710
+ } finally {
711
+ try {
712
+ if (typeof _optionalChain([driver, 'optionalAccess', _119 => _119.disconnect]) === "function") await driver.disconnect();
713
+ } catch (e4) {
714
+ }
715
+ }
716
+ }
717
+ };
718
+ function safeGetService2(ctx, name) {
719
+ try {
720
+ return ctx.getService(name);
721
+ } catch (e5) {
722
+ return void 0;
723
+ }
724
+ }
725
+ function errMsg(err) {
726
+ return err instanceof Error ? err.message : String(err);
727
+ }
728
+ function monotonicNow() {
729
+ const perf = globalThis.performance;
730
+ return typeof _optionalChain([perf, 'optionalAccess', _120 => _120.now]) === "function" ? perf.now() : 0;
731
+ }
732
+ function elapsedSince(startedAt) {
733
+ return Math.max(0, Math.round(monotonicNow() - startedAt));
734
+ }
735
+
736
+ // src/default-datasource-driver-factory.ts
737
+ var DRIVER_ID_ALIASES = {
738
+ postgres: "postgres",
739
+ postgresql: "postgres",
740
+ pg: "postgres",
741
+ sqlite: "sqlite",
742
+ sqlite3: "sqlite",
743
+ "better-sqlite3": "sqlite",
744
+ mongodb: "mongodb",
745
+ mongo: "mongodb",
746
+ memory: "memory",
747
+ inmemory: "memory",
748
+ "in-memory": "memory"
749
+ };
750
+ function resolveKind(driverId) {
751
+ return DRIVER_ID_ALIASES[String(_nullishCoalesce(driverId, () => ( ""))).toLowerCase()];
752
+ }
753
+ function toHandle(driver, serverVersion) {
754
+ return {
755
+ connect: typeof _optionalChain([driver, 'optionalAccess', _121 => _121.connect]) === "function" ? () => driver.connect() : void 0,
756
+ disconnect: typeof _optionalChain([driver, 'optionalAccess', _122 => _122.disconnect]) === "function" ? () => driver.disconnect() : void 0,
757
+ checkHealth: typeof _optionalChain([driver, 'optionalAccess', _123 => _123.checkHealth]) === "function" ? () => driver.checkHealth() : void 0,
758
+ ping: typeof _optionalChain([driver, 'optionalAccess', _124 => _124.checkHealth]) === "function" ? () => driver.checkHealth() : void 0,
759
+ ...serverVersion ? { serverVersion } : {},
760
+ driver
761
+ };
762
+ }
763
+ function buildSqlConnection(spec, client) {
764
+ const cfg = _nullishCoalesce(spec.config, () => ( {}));
765
+ if (client === "better-sqlite3") {
766
+ const filename = _nullishCoalesce(_nullishCoalesce(_nullishCoalesce(cfg.filename, () => ( cfg.file)), () => ( cfg.database)), () => ( ":memory:"));
767
+ return { filename };
768
+ }
769
+ const url = _nullishCoalesce(cfg.url, () => ( cfg.connectionString));
770
+ if (url) {
771
+ return spec.secret ? { connectionString: url, password: spec.secret } : { connectionString: url };
772
+ }
773
+ return {
774
+ host: cfg.host,
775
+ port: cfg.port,
776
+ database: cfg.database,
777
+ user: _nullishCoalesce(cfg.user, () => ( cfg.username)),
778
+ ...spec.secret ? { password: spec.secret } : cfg.password ? { password: cfg.password } : {},
779
+ ...cfg.ssl != null ? { ssl: cfg.ssl } : {}
780
+ };
781
+ }
782
+ function buildMongoUrl(spec) {
783
+ const cfg = _nullishCoalesce(spec.config, () => ( {}));
784
+ const explicit = _nullishCoalesce(cfg.url, () => ( cfg.uri));
785
+ if (explicit) return explicit;
786
+ const host = _nullishCoalesce(cfg.host, () => ( "localhost"));
787
+ const port = _nullishCoalesce(cfg.port, () => ( 27017));
788
+ const db = _nullishCoalesce(cfg.database, () => ( ""));
789
+ const user = _nullishCoalesce(cfg.user, () => ( cfg.username));
790
+ const auth = user ? `${encodeURIComponent(user)}:${encodeURIComponent(_nullishCoalesce(spec.secret, () => ( "")))}@` : "";
791
+ return `mongodb://${auth}${host}:${port}/${db}`;
792
+ }
793
+ function createDefaultDatasourceDriverFactory() {
794
+ return {
795
+ supports(driverId) {
796
+ return resolveKind(driverId) !== void 0;
797
+ },
798
+ async create(spec) {
799
+ const kind = resolveKind(spec.driver);
800
+ if (!kind) {
801
+ throw new Error(`Unsupported driver id '${spec.driver}'.`);
802
+ }
803
+ const schemaMode = _nullishCoalesce(_optionalChain([spec, 'access', _125 => _125.external, 'optionalAccess', _126 => _126.schemaMode]), () => ( _optionalChain([spec, 'access', _127 => _127.config, 'optionalAccess', _128 => _128.schemaMode])));
804
+ if (kind === "postgres") {
805
+ const { SqlDriver } = await Promise.resolve().then(() => _interopRequireWildcard(require("@objectstack/driver-sql")));
806
+ const driver = new SqlDriver({
807
+ client: "pg",
808
+ connection: buildSqlConnection(spec, "pg"),
809
+ pool: { min: 0, max: 5 },
810
+ ...schemaMode ? { schemaMode } : {}
811
+ });
812
+ return toHandle(driver, () => sqlServerVersion(driver, "pg"));
813
+ }
814
+ if (kind === "sqlite") {
815
+ const { SqlDriver } = await Promise.resolve().then(() => _interopRequireWildcard(require("@objectstack/driver-sql")));
816
+ const driver = new SqlDriver({
817
+ client: "better-sqlite3",
818
+ connection: buildSqlConnection(spec, "better-sqlite3"),
819
+ useNullAsDefault: true,
820
+ ...schemaMode ? { schemaMode } : {}
821
+ });
822
+ return toHandle(driver, () => sqlServerVersion(driver, "sqlite"));
823
+ }
824
+ if (kind === "mongodb") {
825
+ let MongoDBDriver;
826
+ try {
827
+ ({ MongoDBDriver } = await Promise.resolve().then(() => _interopRequireWildcard(require("@objectstack/driver-mongodb"))));
828
+ } catch (err) {
829
+ throw new Error(
830
+ `mongodb driver requested but @objectstack/driver-mongodb is not installed (${_nullishCoalesce(_optionalChain([err, 'optionalAccess', _129 => _129.message]), () => ( err))}).`
831
+ );
832
+ }
833
+ const driver = new MongoDBDriver({ url: buildMongoUrl(spec) });
834
+ return toHandle(driver);
835
+ }
836
+ const { InMemoryDriver } = await Promise.resolve().then(() => _interopRequireWildcard(require("@objectstack/driver-memory")));
837
+ return toHandle(new InMemoryDriver());
838
+ }
839
+ };
840
+ }
841
+ async function sqlServerVersion(driver, client) {
842
+ if (typeof _optionalChain([driver, 'optionalAccess', _130 => _130.execute]) !== "function") return void 0;
843
+ try {
844
+ const sql = client === "pg" ? "SELECT version() AS v" : "SELECT sqlite_version() AS v";
845
+ const rows = await driver.execute(sql);
846
+ const first = Array.isArray(rows) ? rows[0] : Array.isArray(_optionalChain([rows, 'optionalAccess', _131 => _131.rows])) ? rows.rows[0] : rows;
847
+ const v = _nullishCoalesce(_nullishCoalesce(_optionalChain([first, 'optionalAccess', _132 => _132.v]), () => ( _optionalChain([first, 'optionalAccess', _133 => _133.version]))), () => ( _optionalChain([first, 'optionalAccess', _134 => _134["sqlite_version()"]])));
848
+ return typeof v === "string" ? v : void 0;
849
+ } catch (e6) {
850
+ return void 0;
851
+ }
852
+ }
853
+
854
+ // src/datasource-secret-binder.ts
855
+ var REF_PREFIX = "sys_secret:";
856
+ function toCredentialsRef(handleId) {
857
+ return `${REF_PREFIX}${handleId}`;
858
+ }
859
+ function parseCredentialsRef(ref) {
860
+ return _optionalChain([ref, 'optionalAccess', _135 => _135.startsWith, 'call', _136 => _136(REF_PREFIX)]) ? ref.slice(REF_PREFIX.length) : void 0;
861
+ }
862
+ function createDatasourceSecretBinder(deps) {
863
+ const { engine, cryptoProvider } = deps;
864
+ const defaultNamespace = _nullishCoalesce(deps.namespace, () => ( "datasource"));
865
+ return {
866
+ async bind(input, hint) {
867
+ const namespace = _nullishCoalesce(input.namespace, () => ( defaultNamespace));
868
+ const key = _nullishCoalesce(input.key, () => ( hint.name));
869
+ const handle = await cryptoProvider.encrypt(input.value, { namespace, key });
870
+ await engine.insert("sys_secret", {
871
+ id: handle.id,
872
+ namespace,
873
+ key,
874
+ kms_key_id: handle.kmsKeyId,
875
+ alg: handle.alg,
876
+ version: handle.version,
877
+ ciphertext: handle.ciphertext
878
+ });
879
+ return toCredentialsRef(handle.id);
880
+ },
881
+ async unbind(credentialsRef) {
882
+ const id = parseCredentialsRef(credentialsRef);
883
+ if (!id) return;
884
+ await engine.delete("sys_secret", { where: { id } });
885
+ },
886
+ async resolve(credentialsRef) {
887
+ const id = parseCredentialsRef(credentialsRef);
888
+ if (!id || typeof engine.find !== "function") return void 0;
889
+ try {
890
+ const result = await engine.find("sys_secret", {
891
+ where: { id },
892
+ limit: 1,
893
+ // Secrets are scoped through their owning datasource artefact, so
894
+ // skip the tenant-audit warning (mirrors SettingsService's store).
895
+ bypassTenantAudit: true
896
+ });
897
+ const rows = _nullishCoalesce((Array.isArray(result) ? result : _optionalChain([result, 'optionalAccess', _137 => _137.data])), () => ( []));
898
+ const row = rows[0];
899
+ if (!_optionalChain([row, 'optionalAccess', _138 => _138.ciphertext])) return void 0;
900
+ return await cryptoProvider.decrypt(
901
+ {
902
+ id: row.id,
903
+ kmsKeyId: row.kms_key_id,
904
+ alg: row.alg,
905
+ version: row.version,
906
+ ciphertext: row.ciphertext
907
+ },
908
+ { namespace: row.namespace, key: row.key }
909
+ );
910
+ } catch (e7) {
911
+ return void 0;
912
+ }
913
+ }
914
+ };
915
+ }
916
+
917
+ // src/admin-routes.ts
918
+ function registerDatasourceAdminRoutes(server, ctx, basePath = "/api/v1") {
919
+ const root = `${basePath}/datasources`;
920
+ const adminService = () => {
921
+ try {
922
+ return ctx.getService("datasource-admin");
923
+ } catch (e8) {
924
+ return void 0;
925
+ }
926
+ };
927
+ const unavailable = (res) => res.status(503).json({ error: "datasource_admin_unavailable" });
928
+ const badRequest = (res, err) => res.status(400).json({ error: "datasource_admin_error", message: err instanceof Error ? err.message : String(err) });
929
+ const splitSecret = (body) => {
930
+ const { secret, ...draft } = _nullishCoalesce(body, () => ( {}));
931
+ const normalised = secret == null ? void 0 : typeof secret === "string" ? { value: secret } : secret;
932
+ return { draft, secret: normalised };
933
+ };
934
+ server.get(root, async (_req, res) => {
935
+ const svc = adminService();
936
+ if (!_optionalChain([svc, 'optionalAccess', _139 => _139.listDatasources])) return unavailable(res);
937
+ const datasources = await svc.listDatasources();
938
+ res.json({ datasources });
939
+ });
940
+ server.post(`${root}/test`, async (req, res) => {
941
+ const svc = adminService();
942
+ if (!_optionalChain([svc, 'optionalAccess', _140 => _140.testConnection])) return unavailable(res);
943
+ const { draft, secret } = splitSecret(req.body);
944
+ try {
945
+ const result = await svc.testConnection(draft, secret);
946
+ res.json({ result });
947
+ } catch (err) {
948
+ badRequest(res, err);
949
+ }
950
+ });
951
+ server.post(root, async (req, res) => {
952
+ const svc = adminService();
953
+ if (!_optionalChain([svc, 'optionalAccess', _141 => _141.createDatasource])) return unavailable(res);
954
+ const { draft, secret } = splitSecret(req.body);
955
+ try {
956
+ const datasource = await svc.createDatasource(draft, secret);
957
+ res.status(201).json({ datasource });
958
+ } catch (err) {
959
+ badRequest(res, err);
960
+ }
961
+ });
962
+ server.patch(`${root}/:name`, async (req, res) => {
963
+ const svc = adminService();
964
+ if (!_optionalChain([svc, 'optionalAccess', _142 => _142.updateDatasource])) return unavailable(res);
965
+ const { draft, secret } = splitSecret(req.body);
966
+ try {
967
+ const datasource = await svc.updateDatasource(req.params.name, draft, secret);
968
+ res.json({ datasource });
969
+ } catch (err) {
970
+ badRequest(res, err);
971
+ }
972
+ });
973
+ server.delete(`${root}/:name`, async (req, res) => {
974
+ const svc = adminService();
975
+ if (!_optionalChain([svc, 'optionalAccess', _143 => _143.removeDatasource])) return unavailable(res);
976
+ try {
977
+ await svc.removeDatasource(req.params.name);
978
+ res.status(204).end();
979
+ } catch (err) {
980
+ badRequest(res, err);
981
+ }
982
+ });
983
+ }
984
+
985
+
986
+
987
+
988
+
989
+
990
+
991
+
992
+
993
+
994
+ exports.DatasourceAdminService = DatasourceAdminService; exports.DatasourceAdminServicePlugin = DatasourceAdminServicePlugin; exports.ExternalDatasourceService = ExternalDatasourceService; exports.ExternalDatasourceServicePlugin = ExternalDatasourceServicePlugin; exports.createDatasourceSecretBinder = createDatasourceSecretBinder; exports.createDefaultDatasourceDriverFactory = createDefaultDatasourceDriverFactory; exports.parseCredentialsRef = parseCredentialsRef; exports.registerDatasourceAdminRoutes = registerDatasourceAdminRoutes; exports.toCredentialsRef = toCredentialsRef;
995
+ //# sourceMappingURL=index.cjs.map