@objectstack/service-external-datasource 7.4.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 +22 -0
- package/CHANGELOG.md +44 -0
- package/LICENSE +202 -0
- package/LICENSE.apache +202 -0
- package/dist/index.cjs +376 -0
- package/dist/index.cjs.map +1 -0
- package/dist/index.d.cts +115 -0
- package/dist/index.d.ts +115 -0
- package/dist/index.js +352 -0
- package/dist/index.js.map +1 -0
- package/package.json +47 -0
- package/src/__tests__/external-datasource-service.test.ts +360 -0
- package/src/external-datasource-service.ts +456 -0
- package/src/index.ts +19 -0
- package/src/plugin.ts +119 -0
- package/tsconfig.json +17 -0
package/dist/index.js
ADDED
|
@@ -0,0 +1,352 @@
|
|
|
1
|
+
// src/external-datasource-service.ts
|
|
2
|
+
import {
|
|
3
|
+
suggestFieldType,
|
|
4
|
+
isCompatible,
|
|
5
|
+
ExternalCatalogSchema
|
|
6
|
+
} from "@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 = ds?.external?.allowedSchemas;
|
|
41
|
+
const tables = [];
|
|
42
|
+
for (const table of Object.values(schema.tables)) {
|
|
43
|
+
const { schema: tableSchema, name } = parseQualified(table.name);
|
|
44
|
+
if (opts?.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 = 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 = opts.rename?.[col.name] ?? col.name;
|
|
71
|
+
const suggested = suggestFieldType(col.type, dialect);
|
|
72
|
+
const fieldType = 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 (isCompatible(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 = 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
|
+
this.logger?.info?.(`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 = 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: suggestFieldType(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
|
+
this.logger?.warn?.(`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 = 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 = obj.external?.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: obj.external?.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(obj.external?.ignoreColumns ?? []);
|
|
190
|
+
const fieldToRemote = /* @__PURE__ */ new Map();
|
|
191
|
+
for (const [remoteCol, fieldName] of Object.entries(obj.external?.columnMap ?? {})) {
|
|
192
|
+
fieldToRemote.set(fieldName, remoteCol);
|
|
193
|
+
}
|
|
194
|
+
for (const [fieldName, field] of Object.entries(obj.fields ?? {})) {
|
|
195
|
+
if (BUILTIN_COLUMNS.has(fieldName)) continue;
|
|
196
|
+
const remoteCol = 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 = field.type ?? "text";
|
|
209
|
+
const compat = isCompatible(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
|
+
this.logger?.warn(`validateObject('${o.name}') failed`, err);
|
|
242
|
+
return {
|
|
243
|
+
ok: false,
|
|
244
|
+
datasource: o.datasource ?? "default",
|
|
245
|
+
object: o.name,
|
|
246
|
+
diffs: [
|
|
247
|
+
{
|
|
248
|
+
kind: "missing_table",
|
|
249
|
+
remoteName: o.external?.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 = this.options.introspect ?? (async (datasource) => {
|
|
304
|
+
if (engine?.introspectDatasource) return engine.introspectDatasource(datasource);
|
|
305
|
+
const driver = engine?.getDatasourceDriver?.(datasource);
|
|
306
|
+
if (driver?.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 metadata?.get("datasource", n),
|
|
314
|
+
getObject: async (n) => metadata?.getObject ? await metadata.getObject(n) : await metadata?.get("object", n),
|
|
315
|
+
listObjects: async () => (metadata?.listObjects ? await metadata.listObjects() : await metadata?.list?.("object")) ?? [],
|
|
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
|
+
...metadata?.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 {
|
|
345
|
+
return void 0;
|
|
346
|
+
}
|
|
347
|
+
}
|
|
348
|
+
export {
|
|
349
|
+
ExternalDatasourceService,
|
|
350
|
+
ExternalDatasourceServicePlugin
|
|
351
|
+
};
|
|
352
|
+
//# sourceMappingURL=index.js.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"sources":["../src/external-datasource-service.ts","../src/plugin.ts"],"sourcesContent":["// Copyright (c) 2025 ObjectStack. Licensed under the Apache-2.0 license.\n\n/**\n * ExternalDatasourceService — implements {@link IExternalDatasourceService}\n * (ADR-0015 §6) on top of driver introspection.\n *\n * The service is intentionally decoupled from the kernel: all I/O\n * (introspection, metadata reads) is injected via\n * {@link ExternalDatasourceServiceConfig}, so the introspection/draft/validate\n * logic is pure and unit-testable. The kernel plugin wires the real\n * `IDataEngine` + `IMetadataService` callbacks in.\n */\n\nimport type {\n IExternalDatasourceService,\n RemoteTable,\n GenerateDraftOpts,\n ObjectDraft,\n ImportObjectOpts,\n ImportObjectResult,\n SchemaValidationResult,\n SchemaValidationReport,\n IntrospectedSchema,\n IntrospectedTable,\n} from '@objectstack/spec/contracts';\nimport type { SchemaDiffEntry } from '@objectstack/spec/shared';\nimport {\n suggestFieldType,\n isCompatible,\n ExternalCatalogSchema,\n type ExternalCatalog,\n type SqlDialect,\n type FieldType,\n} from '@objectstack/spec/data';\n\n/** Minimal datasource shape the service reads (subset of `Datasource`). */\nexport interface DatasourceLike {\n name: string;\n schemaMode?: 'managed' | 'external' | 'validate-only';\n external?: {\n allowedSchemas?: string[];\n validation?: { onMismatch?: 'fail' | 'warn' | 'ignore' };\n };\n}\n\n/** Minimal object shape the service reads (subset of `ServiceObject`). */\nexport interface ObjectLike {\n name: string;\n label?: string;\n datasource?: string;\n external?: {\n remoteName?: string;\n remoteSchema?: string;\n columnMap?: Record<string, string>;\n ignoreColumns?: string[];\n };\n fields?: Record<string, { type?: string; required?: boolean }>;\n}\n\nexport interface Logger {\n warn: (message: string, meta?: unknown) => void;\n info?: (message: string, meta?: unknown) => void;\n}\n\n/**\n * Injected dependencies. The plugin supplies real implementations backed by\n * the driver registry and `IMetadataService`; tests supply fakes.\n */\nexport interface ExternalDatasourceServiceConfig {\n /** Introspect a datasource's live schema via its driver. */\n introspect: (datasource: string) => Promise<IntrospectedSchema>;\n /** Resolve a datasource definition by name. */\n getDatasource: (name: string) => Promise<DatasourceLike | undefined>;\n /** Resolve one object definition by name. */\n getObject: (name: string) => Promise<ObjectLike | undefined>;\n /** List all object definitions (for `validateAll`). */\n listObjects: () => Promise<ObjectLike[]>;\n /**\n * Persist a refreshed catalog snapshot as an `external_catalog` metadata\n * record. Optional: when absent, `refreshCatalog` still returns the snapshot\n * but does not cache it (e.g. dev runs without a writable metadata store).\n */\n persistCatalog?: (catalog: ExternalCatalog) => Promise<void>;\n /**\n * Persist an imported object definition as a live (runtime-origin) `object`\n * metadata record. Optional: when absent, {@link ExternalDatasourceService.importObject}\n * throws (the deployment is GitOps-only / has no writable metadata store).\n */\n persistObject?: (name: string, definition: Record<string, unknown>) => Promise<void>;\n logger?: Logger;\n}\n\n/** Columns ObjectStack manages itself — never validated against the remote. */\nconst BUILTIN_COLUMNS = new Set(['id', 'created_at', 'updated_at']);\n\n/** Split a possibly schema-qualified name (`mart.fact_orders`). */\nfunction parseQualified(raw: string): { schema?: string; name: string } {\n const idx = raw.indexOf('.');\n if (idx === -1) return { name: raw };\n return { schema: raw.slice(0, idx), name: raw.slice(idx + 1) };\n}\n\n/** Normalise a remote table name into a snake_case object name. */\nfunction toObjectName(remoteName: string): string {\n const { name } = parseQualified(remoteName);\n return name\n .replace(/[^a-zA-Z0-9_]/g, '_')\n .replace(/^[^a-z_]/, (c) => `_${c.toLowerCase()}`)\n .toLowerCase();\n}\n\n/** snake_case → Title Case label. */\nfunction toLabel(name: string): string {\n return name\n .split('_')\n .filter(Boolean)\n .map((w) => w.charAt(0).toUpperCase() + w.slice(1))\n .join(' ');\n}\n\nexport class ExternalDatasourceService implements IExternalDatasourceService {\n constructor(private readonly config: ExternalDatasourceServiceConfig) {}\n\n private get logger(): Logger | undefined {\n return this.config.logger;\n }\n\n private findTable(schema: IntrospectedSchema, remoteName: string): IntrospectedTable | undefined {\n const want = parseQualified(remoteName).name;\n for (const table of Object.values(schema.tables)) {\n if (table.name === remoteName) return table;\n if (parseQualified(table.name).name === want) return table;\n }\n return undefined;\n }\n\n async listRemoteTables(\n datasource: string,\n opts?: { schema?: string },\n ): Promise<RemoteTable[]> {\n const [schema, ds] = await Promise.all([\n this.config.introspect(datasource),\n this.config.getDatasource(datasource),\n ]);\n const allowed = ds?.external?.allowedSchemas;\n\n const tables: RemoteTable[] = [];\n for (const table of Object.values(schema.tables)) {\n const { schema: tableSchema, name } = parseQualified(table.name);\n if (opts?.schema && tableSchema && tableSchema !== opts.schema) continue;\n // allowedSchemas only filters tables we can attribute to a schema.\n if (allowed && tableSchema && !allowed.includes(tableSchema)) continue;\n tables.push({ schema: tableSchema, name, columnCount: table.columns.length });\n }\n return tables;\n }\n\n async generateObjectDraft(\n datasource: string,\n remoteName: string,\n opts: GenerateDraftOpts = {},\n ): Promise<ObjectDraft> {\n const schema = await this.config.introspect(datasource);\n const table = this.findTable(schema, remoteName);\n if (!table) {\n throw new Error(\n `Remote table '${remoteName}' not found on datasource '${datasource}'.`,\n );\n }\n const dialect = schema.dialect as SqlDialect | undefined;\n // Derive the remote schema from the matched table's qualified name (the\n // caller may pass an unqualified `remoteName`).\n const matched = parseQualified(table.name);\n const remoteSchema = opts.remoteSchema ?? matched.schema;\n const resolvedRemoteName = matched.name;\n\n const include = opts.includeColumns ? new Set(opts.includeColumns) : undefined;\n const exclude = opts.excludeColumns ? new Set(opts.excludeColumns) : new Set<string>();\n const pkOverride = opts.primaryKey ? new Set(opts.primaryKey) : undefined;\n\n const fields: Record<string, { type: FieldType; primaryKey?: boolean }> = {};\n const review: ObjectDraft['review'] = [];\n\n for (const col of table.columns) {\n if (include && !include.has(col.name)) continue;\n if (exclude.has(col.name)) continue;\n\n const fieldName = opts.rename?.[col.name] ?? col.name;\n const suggested = suggestFieldType(col.type, dialect);\n const fieldType: FieldType = suggested ?? 'text';\n if (!suggested) {\n review.push({\n column: col.name,\n remoteType: col.type,\n note: `unrecognised remote type — defaulted to 'text', verify`,\n });\n } else if (isCompatible(col.type, fieldType, dialect) === 'lossy') {\n review.push({\n column: col.name,\n remoteType: col.type,\n note: `mapped lossy to '${fieldType}'`,\n });\n }\n\n const isPk = pkOverride ? pkOverride.has(col.name) : col.primaryKey;\n fields[fieldName] = isPk ? { type: fieldType, primaryKey: true } : { type: fieldType };\n }\n\n const name = toObjectName(resolvedRemoteName);\n const definition: Record<string, unknown> = {\n name,\n label: toLabel(name),\n datasource,\n external: {\n ...(remoteSchema ? { remoteSchema } : {}),\n remoteName: resolvedRemoteName,\n },\n fields,\n };\n\n return {\n name,\n datasource,\n definition,\n source: renderObjectSource(definition, fields, review),\n review,\n };\n }\n\n async importObject(\n datasource: string,\n remoteName: string,\n opts: ImportObjectOpts = {},\n ): Promise<ImportObjectResult> {\n if (!this.config.persistObject) {\n throw new Error(\n `importObject requires a writable metadata store, but none is wired ` +\n `(datasource '${datasource}'). This deployment may be GitOps-only — ` +\n `use 'os datasource introspect' and commit the generated *.object.ts instead.`,\n );\n }\n\n // Reuse the draft pipeline (type mapping, review notes, external binding).\n const draft = await this.generateObjectDraft(datasource, remoteName, opts);\n\n // Apply the runtime-persona overrides on top of the draft definition.\n const name = opts.name ?? draft.name;\n const external = {\n ...(draft.definition.external as Record<string, unknown>),\n ...(opts.writable ? { writable: true } : {}),\n };\n const definition: Record<string, unknown> = {\n ...draft.definition,\n name,\n label: toLabel(name),\n external,\n };\n\n await this.config.persistObject(name, definition);\n this.logger?.info?.(`importObject: persisted '${name}' from ${datasource}.${remoteName}`, {\n writable: opts.writable === true,\n review: draft.review.length,\n });\n\n return { name, definition, review: draft.review };\n }\n\n async refreshCatalog(datasource: string): Promise<ExternalCatalog> {\n const schema = await this.config.introspect(datasource);\n // Parse through the Zod schema so the persisted record is canonical\n // (defaults applied, shape validated) and matches the `external_catalog`\n // metadata type the boot gate + Studio read back.\n const catalog = ExternalCatalogSchema.parse({\n name: `${datasource}_catalog`,\n datasource,\n snapshotAt: new Date().toISOString(),\n dialect: schema.dialect,\n tables: Object.values(schema.tables).map((t) => {\n const { schema: s, name } = parseQualified(t.name);\n return {\n remoteSchema: s,\n remoteName: name,\n columns: t.columns.map((c) => ({\n name: c.name,\n sqlType: c.type,\n nullable: c.nullable,\n primaryKey: c.primaryKey,\n suggestedFieldType: suggestFieldType(c.type, schema.dialect as SqlDialect),\n })),\n };\n }),\n }) as ExternalCatalog;\n\n // Best-effort cache: a failure to persist must not fail the refresh — the\n // caller still gets the live snapshot back.\n if (this.config.persistCatalog) {\n try {\n await this.config.persistCatalog(catalog);\n } catch (err) {\n this.logger?.warn?.(`refreshCatalog: failed to persist '${catalog.name}'`, err);\n }\n }\n\n return catalog;\n }\n\n async validateObject(objectName: string): Promise<SchemaValidationResult> {\n const obj = await this.config.getObject(objectName);\n if (!obj) {\n throw new Error(`Object '${objectName}' not found.`);\n }\n const datasource = obj.datasource ?? 'default';\n const ds = await this.config.getDatasource(datasource);\n\n // Not a federated object → nothing to validate.\n if (!ds || !ds.schemaMode || ds.schemaMode === 'managed') {\n return { ok: true, datasource, object: objectName, diffs: [] };\n }\n\n const schema = await this.config.introspect(datasource);\n const dialect = schema.dialect as SqlDialect | undefined;\n const remoteName = obj.external?.remoteName ?? obj.name;\n const table = this.findTable(schema, remoteName);\n\n const diffs: SchemaDiffEntry[] = [];\n\n if (!table) {\n diffs.push({\n kind: 'missing_table',\n remoteSchema: obj.external?.remoteSchema,\n remoteName,\n severity: 'error',\n });\n return { ok: false, datasource, object: objectName, diffs };\n }\n\n const columnsByName = new Map(table.columns.map((c) => [c.name, c]));\n const ignore = new Set(obj.external?.ignoreColumns ?? []);\n // columnMap is remoteColumn → fieldName; invert for field → remoteColumn.\n const fieldToRemote = new Map<string, string>();\n for (const [remoteCol, fieldName] of Object.entries(obj.external?.columnMap ?? {})) {\n fieldToRemote.set(fieldName, remoteCol);\n }\n\n for (const [fieldName, field] of Object.entries(obj.fields ?? {})) {\n if (BUILTIN_COLUMNS.has(fieldName)) continue;\n const remoteCol = fieldToRemote.get(fieldName) ?? fieldName;\n if (ignore.has(remoteCol)) continue;\n\n const col = columnsByName.get(remoteCol);\n if (!col) {\n diffs.push({\n kind: 'missing_column',\n remoteName,\n column: remoteCol,\n severity: 'error',\n });\n continue;\n }\n const fieldType = (field.type ?? 'text') as FieldType;\n const compat = isCompatible(col.type, fieldType, dialect);\n if (compat === false) {\n diffs.push({\n kind: 'type_mismatch',\n remoteName,\n column: remoteCol,\n expected: fieldType,\n actual: col.type,\n severity: 'error',\n });\n } else if (compat === 'lossy') {\n diffs.push({\n kind: 'type_mismatch',\n remoteName,\n column: remoteCol,\n expected: fieldType,\n actual: col.type,\n severity: 'warning',\n });\n }\n }\n\n const ok = !diffs.some((d) => d.severity === 'error');\n return { ok, datasource, object: objectName, diffs };\n }\n\n async validateAll(): Promise<SchemaValidationReport> {\n const objects = await this.config.listObjects();\n const federated = objects.filter(\n (o) => o.external !== undefined || (o.datasource && o.datasource !== 'default'),\n );\n\n const results = await Promise.all(\n federated.map((o) =>\n this.validateObject(o.name).catch((err): SchemaValidationResult => {\n this.logger?.warn(`validateObject('${o.name}') failed`, err);\n return {\n ok: false,\n datasource: o.datasource ?? 'default',\n object: o.name,\n diffs: [\n {\n kind: 'missing_table',\n remoteName: o.external?.remoteName ?? o.name,\n actual: err instanceof Error ? err.message : String(err),\n severity: 'error',\n },\n ],\n };\n }),\n ),\n );\n\n const ok = results.every((r) => r.ok);\n return { ok, results };\n }\n}\n\n/** Render a reviewable `*.object.ts` source string for an object draft. */\nfunction renderObjectSource(\n definition: Record<string, unknown>,\n fields: Record<string, { type: FieldType; primaryKey?: boolean }>,\n review: ObjectDraft['review'],\n): string {\n const reviewByColumn = new Map(review.map((r) => [r.column, r.note]));\n const external = definition.external as { remoteSchema?: string; remoteName?: string };\n\n const fieldLines = Object.entries(fields).map(([fieldName, f]) => {\n const note = reviewByColumn.get(fieldName);\n const pk = f.primaryKey ? ', primaryKey: true' : '';\n const comment = note ? ` // REVIEW: ${note}` : '';\n return ` ${fieldName}: { type: '${f.type}'${pk} },${comment}`;\n });\n\n const externalLine = external.remoteSchema\n ? ` external: { remoteSchema: '${external.remoteSchema}', remoteName: '${external.remoteName}' },`\n : ` external: { remoteName: '${external.remoteName}' },`;\n\n return [\n `// Generated by \\`os datasource introspect\\` (ADR-0015). Review before committing.`,\n `import type { ServiceObjectInput } from '@objectstack/spec/data';`,\n ``,\n `const ${definition.name as string}: ServiceObjectInput = {`,\n ` name: '${definition.name as string}',`,\n ` label: '${definition.label as string}',`,\n ` datasource: '${definition.datasource as string}',`,\n externalLine,\n ` fields: {`,\n ...fieldLines,\n ` },`,\n `};`,\n ``,\n `export default ${definition.name as string};`,\n ``,\n ].join('\\n');\n}\n","// Copyright (c) 2025 ObjectStack. Licensed under the Apache-2.0 license.\n\nimport type { Plugin, PluginContext } from '@objectstack/core';\nimport type { IntrospectedSchema } from '@objectstack/spec/contracts';\nimport {\n ExternalDatasourceService,\n type ExternalDatasourceServiceConfig,\n type DatasourceLike,\n type ObjectLike,\n type Logger,\n} from './external-datasource-service.js';\n\n/**\n * Minimal surfaces the plugin needs from the data engine + metadata service.\n * Kept structural so the plugin doesn't hard-depend on concrete classes.\n */\ninterface DataEngineLike {\n /** Resolve a driver by datasource name and introspect its live schema. */\n introspectDatasource?: (datasource: string) => Promise<IntrospectedSchema>;\n getDatasourceDriver?: (datasource: string) => { introspectSchema?: () => Promise<IntrospectedSchema> } | undefined;\n}\n\ninterface MetadataServiceLike {\n get: (type: string, name: string) => Promise<unknown>;\n getObject?: (name: string) => Promise<unknown>;\n listObjects?: () => Promise<unknown[]>;\n list?: (type: string) => Promise<unknown[]>;\n register?: (type: string, name: string, data: unknown) => Promise<void> | void;\n}\n\nexport interface ExternalDatasourceServicePluginOptions {\n /** Override the introspection function (mainly for tests). */\n introspect?: (datasource: string) => Promise<IntrospectedSchema>;\n logger?: Logger;\n}\n\n/**\n * ExternalDatasourceServicePlugin — registers `IExternalDatasourceService`\n * into the kernel as the `'external-datasource'` service (ADR-0015 §6.1).\n *\n * It bridges the decoupled {@link ExternalDatasourceService} to the live\n * `IDataEngine` (for driver introspection) and `IMetadataService` (for object\n * + datasource reads).\n */\nexport class ExternalDatasourceServicePlugin implements Plugin {\n name = 'com.objectstack.service-external-datasource';\n version = '1.0.0';\n type = 'standard' as const;\n dependencies: string[] = [];\n\n private service?: ExternalDatasourceService;\n private readonly options: ExternalDatasourceServicePluginOptions;\n\n constructor(options: ExternalDatasourceServicePluginOptions = {}) {\n this.options = options;\n }\n\n async init(ctx: PluginContext): Promise<void> {\n const engine = safeGetService<DataEngineLike>(ctx, 'data');\n const metadata = safeGetService<MetadataServiceLike>(ctx, 'metadata');\n\n const introspect: ExternalDatasourceServiceConfig['introspect'] =\n this.options.introspect ??\n (async (datasource: string) => {\n if (engine?.introspectDatasource) return engine.introspectDatasource(datasource);\n const driver = engine?.getDatasourceDriver?.(datasource);\n if (driver?.introspectSchema) return driver.introspectSchema();\n throw new Error(\n `Cannot introspect datasource '${datasource}': no driver introspection available.`,\n );\n });\n\n const config: ExternalDatasourceServiceConfig = {\n introspect,\n getDatasource: async (n) => (await metadata?.get('datasource', n)) as DatasourceLike | undefined,\n getObject: async (n) =>\n (metadata?.getObject ? await metadata.getObject(n) : await metadata?.get('object', n)) as ObjectLike | undefined,\n listObjects: async () =>\n ((metadata?.listObjects\n ? await metadata.listObjects()\n : await metadata?.list?.('object')) ?? []) as ObjectLike[],\n // Persist the refreshed snapshot as an `external_catalog` metadata record\n // so the boot gate + Studio's schema browser can read it without\n // re-introspecting. No-op when the metadata service can't write.\n ...(metadata?.register\n ? {\n persistCatalog: async (catalog) => {\n await metadata.register!('external_catalog', catalog.name, catalog);\n },\n // Runtime \"Import as Object\": persist a federated object so it's\n // immediately queryable, no git commit required (ADR-0015 Addendum).\n persistObject: async (name, definition) => {\n await metadata.register!('object', name, definition);\n },\n }\n : {}),\n logger: this.options.logger,\n };\n\n this.service = new ExternalDatasourceService(config);\n ctx.registerService('external-datasource', this.service);\n }\n\n async start(ctx: PluginContext): Promise<void> {\n if (this.service) await ctx.trigger('external-datasource:ready', this.service);\n }\n\n async destroy(): Promise<void> {\n this.service = undefined;\n }\n}\n\nfunction safeGetService<T>(ctx: PluginContext, name: string): T | undefined {\n try {\n return ctx.getService<T>(name);\n } catch {\n return undefined;\n }\n}\n"],"mappings":"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","names":[]}
|
package/package.json
ADDED
|
@@ -0,0 +1,47 @@
|
|
|
1
|
+
{
|
|
2
|
+
"name": "@objectstack/service-external-datasource",
|
|
3
|
+
"version": "7.4.0",
|
|
4
|
+
"license": "Apache-2.0",
|
|
5
|
+
"description": "External Datasource Federation service for ObjectStack — implements IExternalDatasourceService (introspect, draft, validate) per ADR-0015",
|
|
6
|
+
"type": "module",
|
|
7
|
+
"main": "dist/index.js",
|
|
8
|
+
"types": "dist/index.d.ts",
|
|
9
|
+
"exports": {
|
|
10
|
+
".": {
|
|
11
|
+
"types": "./dist/index.d.ts",
|
|
12
|
+
"import": "./dist/index.js",
|
|
13
|
+
"require": "./dist/index.cjs"
|
|
14
|
+
}
|
|
15
|
+
},
|
|
16
|
+
"dependencies": {
|
|
17
|
+
"@objectstack/core": "7.4.0",
|
|
18
|
+
"@objectstack/spec": "7.4.0"
|
|
19
|
+
},
|
|
20
|
+
"devDependencies": {
|
|
21
|
+
"@types/node": "^25.9.1",
|
|
22
|
+
"typescript": "^6.0.3",
|
|
23
|
+
"vitest": "^4.1.7"
|
|
24
|
+
},
|
|
25
|
+
"keywords": [
|
|
26
|
+
"objectstack",
|
|
27
|
+
"service",
|
|
28
|
+
"external-datasource",
|
|
29
|
+
"federation",
|
|
30
|
+
"introspection"
|
|
31
|
+
],
|
|
32
|
+
"author": "ObjectStack",
|
|
33
|
+
"repository": {
|
|
34
|
+
"type": "git",
|
|
35
|
+
"url": "https://github.com/objectstack-ai/framework.git",
|
|
36
|
+
"directory": "packages/services/service-external-datasource"
|
|
37
|
+
},
|
|
38
|
+
"homepage": "https://objectstack.ai/docs",
|
|
39
|
+
"bugs": "https://github.com/objectstack-ai/framework/issues",
|
|
40
|
+
"publishConfig": {
|
|
41
|
+
"access": "public"
|
|
42
|
+
},
|
|
43
|
+
"scripts": {
|
|
44
|
+
"build": "tsup --config ../../../tsup.config.ts",
|
|
45
|
+
"test": "vitest run"
|
|
46
|
+
}
|
|
47
|
+
}
|