@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
@@ -0,0 +1 @@
1
+ {"version":3,"sources":["../src/external-datasource-service.ts","../src/plugin.ts","../src/datasource-admin-service.ts","../src/datasource-admin-plugin.ts","../src/default-datasource-driver-factory.ts","../src/datasource-secret-binder.ts","../src/admin-routes.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","// Copyright (c) 2025 ObjectStack. Licensed under the Apache-2.0 license.\n\n/**\n * DatasourceAdminService — implements {@link IDatasourceAdminService}\n * (ADR-0015 Addendum) on top of injected persistence + secret + driver probe\n * callbacks.\n *\n * Like its federation sibling `ExternalDatasourceService`, this service is\n * intentionally decoupled from the kernel: every side effect (connection probe,\n * metadata read/write, secret write, bound-object count, hot pool (de)register)\n * is injected via {@link DatasourceAdminServiceConfig}, so the lifecycle rules\n * (origin gating, secret indirection, removal safety) are pure and unit-testable.\n *\n * Invariants enforced here, independent of the wiring:\n * - Code-defined datasources (`origin: 'code'`) are read-only — update/remove\n * reject them, and create refuses a name a code datasource already owns.\n * - A runtime datasource never shadows a code one (code wins on collision).\n * - Credentials never persist in cleartext: the cleartext {@link SecretInput}\n * transits create/update/test only; create/update write it to the secret\n * store and persist only the returned `credentialsRef`.\n * - Removal is refused while objects are still bound to the datasource.\n */\n\nimport type {\n IDatasourceAdminService,\n DatasourceDraft,\n SecretInput,\n TestConnectionResult,\n DatasourceSummary,\n} from './contracts/index.js';\nimport type { Logger } from './logger.js';\n\n/** Datasource name rule (mirrors `DatasourceSchema.name`). */\nconst NAME_RE = /^[a-z_][a-z0-9_]*$/;\n\n/**\n * A persisted datasource record (subset of `Datasource`). `origin` distinguishes\n * code-defined from runtime; `external.credentialsRef` is the opaque secret\n * handle — never a cleartext credential.\n */\nexport interface StoredDatasource {\n name: string;\n label?: string;\n driver: string;\n schemaMode?: 'managed' | 'external' | 'validate-only';\n config?: Record<string, unknown>;\n external?: (Record<string, unknown> & { credentialsRef?: string }) | undefined;\n pool?: Record<string, unknown>;\n active?: boolean;\n origin?: 'code' | 'runtime';\n /** Package that defines a code-origin datasource, when known. */\n definedIn?: string;\n}\n\n/** What a connection probe needs (cleartext secret is transient, never stored). */\nexport interface ProbeInput {\n driver: string;\n config: Record<string, unknown>;\n /** Cleartext secret used for this probe only (e.g. password / DSN). */\n secret?: string;\n external?: Record<string, unknown>;\n timeoutMs?: number;\n}\n\n/**\n * Injected dependencies. The plugin supplies real implementations backed by the\n * driver registry, `IMetadataService` (runtime store), and the secret store;\n * tests supply fakes.\n */\nexport interface DatasourceAdminServiceConfig {\n /** Probe a connection live (driver connect + cheap round-trip). */\n probe: (input: ProbeInput) => Promise<TestConnectionResult>;\n /** Read every datasource record (code + runtime). */\n listDatasourceRecords: () => Promise<StoredDatasource[]>;\n /** Read one datasource record by name. */\n getDatasourceRecord: (name: string) => Promise<StoredDatasource | undefined>;\n /** Persist a runtime datasource record into the runtime metadata store. */\n putDatasourceRecord: (record: StoredDatasource) => Promise<void>;\n /** Remove a runtime datasource record from the runtime metadata store. */\n deleteDatasourceRecord: (name: string) => Promise<void>;\n /** Encrypt + store a secret, returning an opaque `credentialsRef`. */\n writeSecret: (input: SecretInput, hint: { name: string }) => Promise<string>;\n /** Best-effort delete of a stored secret by ref (cleanup on remove/rewrap). */\n removeSecret?: (credentialsRef: string) => Promise<void>;\n /** Count objects bound to a datasource (removal blocked while > 0). */\n countBoundObjects: (datasource: string) => Promise<number>;\n /** Hot-(re)register a runtime datasource's connection pool after write. */\n registerPool?: (record: StoredDatasource) => Promise<void> | void;\n /** Tear down a runtime datasource's pool on remove. */\n unregisterPool?: (name: string) => Promise<void> | void;\n logger?: Logger;\n}\n\nexport class DatasourceAdminService implements IDatasourceAdminService {\n constructor(private readonly config: DatasourceAdminServiceConfig) {}\n\n private get logger(): Logger | undefined {\n return this.config.logger;\n }\n\n async listDatasources(): Promise<DatasourceSummary[]> {\n const records = await this.config.listDatasourceRecords();\n\n // Group by name; code wins on collision, and a shadowed runtime row marks\n // the effective (code) entry as conflicting.\n const byName = new Map<string, { code?: StoredDatasource; runtime?: StoredDatasource }>();\n for (const rec of records) {\n const slot = byName.get(rec.name) ?? {};\n if (rec.origin === 'runtime') slot.runtime = rec;\n else slot.code = rec;\n byName.set(rec.name, slot);\n }\n\n const summaries: DatasourceSummary[] = [];\n for (const [name, slot] of byName) {\n const effective = slot.code ?? slot.runtime;\n if (!effective) continue;\n summaries.push({\n name,\n label: effective.label,\n driver: effective.driver,\n schemaMode: effective.schemaMode ?? 'managed',\n origin: slot.code ? 'code' : 'runtime',\n active: effective.active ?? true,\n status: 'unvalidated',\n ...(slot.code?.definedIn ? { definedIn: slot.code.definedIn } : {}),\n ...(slot.code && slot.runtime ? { conflictsWithCode: true } : {}),\n });\n }\n return summaries;\n }\n\n async testConnection(input: DatasourceDraft, secret?: SecretInput): Promise<TestConnectionResult> {\n if (!input?.driver) {\n return { ok: false, error: 'A driver is required to test a connection.' };\n }\n const queryTimeoutMs = (input.external as { queryTimeoutMs?: number } | undefined)?.queryTimeoutMs;\n try {\n return await this.config.probe({\n driver: input.driver,\n config: input.config ?? {},\n secret: secret?.value,\n external: input.external,\n ...(typeof queryTimeoutMs === 'number' ? { timeoutMs: queryTimeoutMs } : {}),\n });\n } catch (err) {\n return { ok: false, error: err instanceof Error ? err.message : String(err) };\n }\n }\n\n async createDatasource(input: DatasourceDraft, secret?: SecretInput): Promise<DatasourceSummary> {\n this.assertValidName(input?.name);\n if (!input.driver) throw new Error('A driver is required to create a datasource.');\n\n const existing = await this.config.getDatasourceRecord(input.name);\n if (existing) {\n if (existing.origin === 'code' || existing.origin === undefined) {\n throw new Error(\n `Cannot create datasource '${input.name}': a code-defined datasource owns this name (read-only).`,\n );\n }\n throw new Error(`Datasource '${input.name}' already exists.`);\n }\n\n const record: StoredDatasource = {\n ...this.toRecord(input),\n origin: 'runtime',\n };\n\n if (secret) {\n const credentialsRef = await this.config.writeSecret(secret, { name: input.name });\n record.external = { ...(record.external ?? {}), credentialsRef };\n }\n\n await this.config.putDatasourceRecord(record);\n await this.tryRegisterPool(record);\n return this.toSummary(record);\n }\n\n async updateDatasource(\n name: string,\n patch: Partial<DatasourceDraft>,\n secret?: SecretInput,\n ): Promise<DatasourceSummary> {\n const existing = await this.config.getDatasourceRecord(name);\n if (!existing) throw new Error(`Datasource '${name}' not found.`);\n if (existing.origin !== 'runtime') {\n throw new Error(`Datasource '${name}' is code-defined and cannot be edited at runtime.`);\n }\n\n // Merge patch over the existing record; `name`/`origin` are never patched.\n const merged: StoredDatasource = {\n ...existing,\n ...(patch.label !== undefined ? { label: patch.label } : {}),\n ...(patch.driver !== undefined ? { driver: patch.driver } : {}),\n ...(patch.schemaMode !== undefined ? { schemaMode: patch.schemaMode } : {}),\n ...(patch.config !== undefined ? { config: patch.config } : {}),\n ...(patch.pool !== undefined ? { pool: patch.pool } : {}),\n ...(patch.active !== undefined ? { active: patch.active } : {}),\n name: existing.name,\n origin: 'runtime',\n };\n if (patch.external !== undefined) {\n // Preserve the existing credentialsRef unless a new secret rewraps it.\n merged.external = { ...patch.external, credentialsRef: existing.external?.credentialsRef };\n }\n\n if (secret) {\n const prevRef = existing.external?.credentialsRef;\n const credentialsRef = await this.config.writeSecret(secret, { name });\n merged.external = { ...(merged.external ?? {}), credentialsRef };\n if (prevRef && prevRef !== credentialsRef) await this.tryRemoveSecret(prevRef);\n }\n\n await this.config.putDatasourceRecord(merged);\n await this.tryRegisterPool(merged);\n return this.toSummary(merged);\n }\n\n async removeDatasource(name: string): Promise<void> {\n const existing = await this.config.getDatasourceRecord(name);\n if (!existing) throw new Error(`Datasource '${name}' not found.`);\n if (existing.origin !== 'runtime') {\n throw new Error(`Datasource '${name}' is code-defined and cannot be removed at runtime.`);\n }\n\n const bound = await this.config.countBoundObjects(name);\n if (bound > 0) {\n throw new Error(\n `Cannot remove datasource '${name}': ${bound} object(s) are still bound to it.`,\n );\n }\n\n await this.config.deleteDatasourceRecord(name);\n if (existing.external?.credentialsRef) await this.tryRemoveSecret(existing.external.credentialsRef);\n await this.tryUnregisterPool(name);\n }\n\n // --- internals -----------------------------------------------------------\n\n private assertValidName(name: string | undefined): void {\n if (!name || !NAME_RE.test(name)) {\n throw new Error(\n `Invalid datasource name '${name ?? ''}': must match /^[a-z_][a-z0-9_]*$/.`,\n );\n }\n }\n\n private toRecord(input: DatasourceDraft): StoredDatasource {\n return {\n name: input.name,\n ...(input.label !== undefined ? { label: input.label } : {}),\n driver: input.driver,\n ...(input.schemaMode !== undefined ? { schemaMode: input.schemaMode } : {}),\n ...(input.config !== undefined ? { config: input.config } : {}),\n ...(input.external !== undefined ? { external: input.external } : {}),\n ...(input.pool !== undefined ? { pool: input.pool } : {}),\n ...(input.active !== undefined ? { active: input.active } : {}),\n };\n }\n\n private toSummary(record: StoredDatasource): DatasourceSummary {\n return {\n name: record.name,\n label: record.label,\n driver: record.driver,\n schemaMode: record.schemaMode ?? 'managed',\n origin: record.origin ?? 'runtime',\n active: record.active ?? true,\n status: 'unvalidated',\n };\n }\n\n private async tryRegisterPool(record: StoredDatasource): Promise<void> {\n try {\n await this.config.registerPool?.(record);\n } catch (err) {\n this.logger?.warn(`registerPool('${record.name}') failed`, err);\n }\n }\n\n private async tryUnregisterPool(name: string): Promise<void> {\n try {\n await this.config.unregisterPool?.(name);\n } catch (err) {\n this.logger?.warn(`unregisterPool('${name}') failed`, err);\n }\n }\n\n private async tryRemoveSecret(credentialsRef: string): Promise<void> {\n try {\n await this.config.removeSecret?.(credentialsRef);\n } catch (err) {\n this.logger?.warn(`removeSecret('${credentialsRef}') failed`, err);\n }\n }\n}\n","// Copyright (c) 2025 ObjectStack. Licensed under the Apache-2.0 license.\n\nimport type { Plugin, PluginContext } from '@objectstack/core';\nimport { registerMetadataTypeActions } from '@objectstack/spec/kernel';\nimport type {\n IDatasourceDriverFactory,\n DatasourceConnectionSpec,\n TestConnectionResult,\n} from './contracts/index.js';\nimport {\n DatasourceAdminService,\n type DatasourceAdminServiceConfig,\n type StoredDatasource,\n type ProbeInput,\n} from './datasource-admin-service.js';\nimport type { Logger } from './logger.js';\n\n/**\n * Minimal metadata-service surface used for datasource persistence + the\n * bound-object count. Kept structural so the plugin doesn't hard-depend on the\n * concrete `MetadataManager`.\n */\ninterface MetadataServiceLike {\n get: (type: string, name: string) => Promise<unknown>;\n list: (type: string) => Promise<unknown[]>;\n register: (type: string, name: string, data: unknown) => Promise<void>;\n unregister: (type: string, name: string) => Promise<void>;\n listObjects?: () => Promise<unknown[]>;\n}\n\n/** Engine surface used for hot pool (de)registration. */\ninterface DataEngineLike {\n registerDriver?: (driver: unknown, isDefault?: boolean) => void;\n registerDatasourceDef?: (def: { name: string; schemaMode?: string; external?: { allowWrites?: boolean } }) => void;\n getDriverByName?: (name: string) => unknown;\n}\n\n/**\n * Host-provided secret binding. Encrypts a cleartext credential into the secret\n * store and returns an opaque `credentialsRef`; `unbind` deletes it. Wired by\n * the stack that owns the `ICryptoProvider` + `sys_secret` store. When absent,\n * the plugin fails *closed*: creating/updating a datasource *with* a secret\n * throws rather than risk persisting cleartext.\n */\nexport interface SecretBinder {\n bind: (input: { value: string; namespace?: string; key?: string }, hint: { name: string }) => Promise<string>;\n unbind?: (credentialsRef: string) => Promise<void>;\n /**\n * Dereference a `credentialsRef` back to cleartext for opening a live\n * connection (boot rehydration + hot pool registration). Optional: when\n * absent, pools for secret-bearing datasources are built without the\n * credential (fine for credential-less drivers like sqlite/memory).\n */\n resolve?: (credentialsRef: string) => Promise<string | undefined>;\n}\n\nexport interface DatasourceAdminServicePluginOptions {\n /** Secret binding backed by the host's crypto provider + `sys_secret`. */\n secrets?: SecretBinder;\n /** Override the driver factory (defaults to the `'datasource-driver-factory'` service). */\n driverFactory?: IDatasourceDriverFactory;\n logger?: Logger;\n}\n\n/**\n * DatasourceAdminServicePlugin — registers `IDatasourceAdminService` into the\n * kernel as the `'datasource-admin'` service (ADR-0015 Addendum).\n *\n * Bridges the decoupled {@link DatasourceAdminService} to live infrastructure:\n * - persistence + bound-object count via the `'metadata'` service\n * (`register`/`unregister` write through to the runtime DB loader),\n * - connection probe + hot pool (de)registration via the\n * `'datasource-driver-factory'` capability and the `'data'` engine,\n * - secret encryption via a host-provided {@link SecretBinder} (fail-closed).\n *\n * Every dependency degrades gracefully: a missing driver factory turns\n * `testConnection` into a clear `{ ok: false }` and skips hot pool registration\n * (the driver is picked up at next boot); a missing secret binder makes\n * secret-bearing create/update fail loudly instead of leaking cleartext.\n */\nexport class DatasourceAdminServicePlugin implements Plugin {\n name = 'com.objectstack.service-datasource-admin';\n version = '1.0.0';\n type = 'standard' as const;\n dependencies: string[] = [];\n\n private service?: DatasourceAdminService;\n private config?: DatasourceAdminServiceConfig;\n private readonly options: DatasourceAdminServicePluginOptions;\n\n constructor(options: DatasourceAdminServicePluginOptions = {}) {\n this.options = options;\n }\n\n async init(ctx: PluginContext): Promise<void> {\n const logger = this.options.logger;\n\n // Contribute the metadata-admin \"Test connection\" type-level action,\n // co-located with the route handler that serves it\n // (`POST /api/v1/datasources/:name/test`, see admin-routes.ts). The\n // open-source framework deliberately ships no declarative datasource\n // action, so the button is emitted by `/api/v1/meta` only when this\n // backend plugin is installed — never advertising a route the host\n // can't serve. `${ctx.recordId}` resolves to the datasource's name.\n registerMetadataTypeActions('datasource', [\n {\n name: 'test_connection',\n label: 'Test connection',\n icon: 'plug-zap',\n type: 'api',\n target: '/api/v1/datasources/${ctx.recordId}/test',\n method: 'POST',\n variant: 'secondary',\n refreshAfter: false,\n locations: ['record_header', 'list_item'],\n },\n ] as any);\n\n // Resolve infra services lazily, per call — `init()` may run before the\n // `data` / `metadata` plugins have registered their services (plugin start\n // order is dependency- not registration-driven), and admin requests only\n // arrive long after the full boot completes.\n const metadataOf = (): MetadataServiceLike | undefined =>\n safeGetService<MetadataServiceLike>(ctx, 'metadata');\n const engineOf = (): DataEngineLike | undefined =>\n safeGetService<DataEngineLike>(ctx, 'data');\n\n const factory = (): IDatasourceDriverFactory | undefined =>\n this.options.driverFactory ?? safeGetService<IDatasourceDriverFactory>(ctx, 'datasource-driver-factory');\n\n const config: DatasourceAdminServiceConfig = {\n probe: (input) => this.probe(factory(), input),\n\n listDatasourceRecords: async () => {\n const rows = ((await metadataOf()?.list('datasource')) ?? []) as StoredDatasource[];\n // Artefact-loaded rows may omit `origin`; treat them as code-defined.\n return rows.map((r) => ({ ...r, origin: r.origin ?? 'code' }));\n },\n\n getDatasourceRecord: async (name) => {\n const row = (await metadataOf()?.get('datasource', name)) as StoredDatasource | undefined;\n return row ? { ...row, origin: row.origin ?? 'code' } : undefined;\n },\n\n putDatasourceRecord: async (record) => {\n const metadata = metadataOf();\n if (!metadata?.register) {\n throw new Error('Metadata service is unavailable; cannot persist datasource.');\n }\n await metadata.register('datasource', record.name, record);\n },\n\n deleteDatasourceRecord: async (name) => {\n const metadata = metadataOf();\n if (!metadata?.unregister) {\n throw new Error('Metadata service is unavailable; cannot remove datasource.');\n }\n await metadata.unregister('datasource', name);\n },\n\n writeSecret: async (input, hint) => {\n const binder = this.options.secrets;\n if (!binder?.bind) {\n throw new Error(\n 'No secret store configured: refusing to persist a datasource credential in cleartext. ' +\n 'Wire a SecretBinder (CryptoProvider + sys_secret) into DatasourceAdminServicePlugin.',\n );\n }\n return binder.bind(input, hint);\n },\n\n removeSecret: async (ref) => {\n await this.options.secrets?.unbind?.(ref);\n },\n\n countBoundObjects: async (datasource) => {\n const metadata = metadataOf();\n const objects = ((await metadata?.listObjects?.()) ??\n (await metadata?.list('object')) ??\n []) as Array<{ datasource?: string }>;\n return objects.filter((o) => o?.datasource === datasource).length;\n },\n\n registerPool: async (record) => {\n const f = factory();\n const engine = engineOf();\n if (!f || !engine?.registerDriver || !f.supports(record.driver)) return;\n // Recover the cleartext credential from `sys_secret` so the pool opens\n // with the real password. The cleartext is never persisted on the\n // record (only `credentialsRef`), so it must be dereferenced here —\n // both on create/update and on boot rehydration. Credential-less\n // drivers (sqlite/memory) simply have no ref and skip this.\n const credentialsRef = record.external?.credentialsRef;\n const secret = credentialsRef ? await this.options.secrets?.resolve?.(credentialsRef) : undefined;\n const handle = await f.create({ ...this.toSpec(record), ...(secret ? { secret } : {}) });\n if (typeof handle?.connect === 'function') await handle.connect();\n // The engine routes a datasource to a driver by `driver.name === <datasource name>`\n // (see ObjectQL engine.getDriver). Prefer the factory's underlying engine\n // driver (the `driver` escape hatch); fall back to the handle itself. Stamp\n // the name so routing resolves to this pool.\n const engineDriver = (handle.driver ?? handle) as { name?: string };\n try {\n engineDriver.name = record.name;\n } catch {\n /* frozen driver — registration may still work if name already matches */\n }\n engine.registerDriver(engineDriver);\n engine.registerDatasourceDef?.({\n name: record.name,\n schemaMode: record.schemaMode,\n external: record.external as { allowWrites?: boolean } | undefined,\n });\n },\n\n unregisterPool: async (name) => {\n const driver = engineOf()?.getDriverByName?.(name) as { disconnect?: () => Promise<void> } | undefined;\n if (typeof driver?.disconnect === 'function') await driver.disconnect();\n },\n\n logger,\n };\n\n this.config = config;\n this.service = new DatasourceAdminService(config);\n ctx.registerService('datasource-admin', this.service);\n }\n\n async start(ctx: PluginContext): Promise<void> {\n // Rebuild live connection pools for persisted runtime datasources before\n // announcing readiness — a node restart otherwise leaves UI-created\n // datasources with a record but no open pool until the next write.\n await this.rehydratePools();\n if (this.service) await ctx.trigger('datasource-admin:ready', this.service);\n }\n\n /**\n * Boot-time rehydration: list persisted runtime datasources and re-register\n * each one's connection pool (driver build → connect → registerDriver),\n * decrypting its `sys_secret` credential on the way via the configured\n * `registerPool` (which resolves `credentialsRef`). Code-defined datasources\n * are owned by the host stack's own boot path and skipped here. Entirely\n * best-effort: a missing factory/engine, an unpersisted dev store (nothing\n * to rehydrate), or a single failing pool never blocks boot.\n */\n private async rehydratePools(): Promise<void> {\n const cfg = this.config;\n if (!cfg?.registerPool || !cfg.listDatasourceRecords) return;\n\n let records: StoredDatasource[];\n try {\n records = await cfg.listDatasourceRecords();\n } catch (err) {\n this.options.logger?.warn?.('datasource rehydrate: listing records failed', err);\n return;\n }\n\n const runtime = records.filter((r) => r.origin === 'runtime' && (r.active ?? true));\n if (runtime.length === 0) return;\n\n let registered = 0;\n for (const record of runtime) {\n try {\n await cfg.registerPool(record);\n registered++;\n } catch (err) {\n this.options.logger?.warn?.(`datasource rehydrate: pool '${record.name}' failed`, err);\n }\n }\n this.options.logger?.info?.(\n `Rehydrated ${registered}/${runtime.length} runtime datasource pool(s) on boot`,\n );\n }\n\n async destroy(): Promise<void> {\n this.service = undefined;\n }\n\n // --- internals -----------------------------------------------------------\n\n private toSpec(record: StoredDatasource): DatasourceConnectionSpec {\n return {\n name: record.name,\n driver: record.driver,\n config: record.config ?? {},\n external: record.external,\n pool: record.pool,\n };\n }\n\n /** Probe a connection via the driver factory: build → connect → ping → close. */\n private async probe(\n factory: IDatasourceDriverFactory | undefined,\n input: ProbeInput,\n ): Promise<TestConnectionResult> {\n if (!factory) {\n return { ok: false, error: 'No driver factory is registered to test connections.' };\n }\n if (!factory.supports(input.driver)) {\n return { ok: false, error: `No driver factory supports driver '${input.driver}'.` };\n }\n\n let driver: any;\n try {\n driver = await factory.create({\n driver: input.driver,\n config: input.config,\n secret: input.secret,\n external: input.external,\n });\n } catch (err) {\n return { ok: false, error: `Failed to build driver: ${errMsg(err)}` };\n }\n\n const startedAt = monotonicNow();\n try {\n if (typeof driver?.connect === 'function') await driver.connect();\n // Prefer a cheap ping; fall back to the engine driver's health check, then\n // a schema introspection round-trip — whichever the handle exposes.\n if (typeof driver?.ping === 'function') await driver.ping();\n else if (typeof driver?.checkHealth === 'function') await driver.checkHealth();\n else if (typeof driver?.introspectSchema === 'function') await driver.introspectSchema();\n const latencyMs = elapsedSince(startedAt);\n let serverVersion: string | undefined;\n try {\n serverVersion = typeof driver?.serverVersion === 'function' ? await driver.serverVersion() : undefined;\n } catch {\n /* version is best-effort */\n }\n return { ok: true, latencyMs, ...(serverVersion ? { serverVersion } : {}) };\n } catch (err) {\n return { ok: false, error: errMsg(err) };\n } finally {\n try {\n if (typeof driver?.disconnect === 'function') await driver.disconnect();\n } catch {\n /* best-effort teardown */\n }\n }\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\nfunction errMsg(err: unknown): string {\n return err instanceof Error ? err.message : String(err);\n}\n\n/** Monotonic clock when available (avoids wall-clock skew); falls back to 0. */\nfunction monotonicNow(): number {\n const perf = (globalThis as { performance?: { now?: () => number } }).performance;\n return typeof perf?.now === 'function' ? perf.now() : 0;\n}\n\nfunction elapsedSince(startedAt: number): number {\n return Math.max(0, Math.round(monotonicNow() - startedAt));\n}\n","// Copyright (c) 2025 ObjectStack. Licensed under the Apache-2.0 license.\n\n/**\n * Default (dev/self-host) implementation of {@link IDatasourceDriverFactory}.\n *\n * The framework ships no universal \"driver-by-id\" registry — concrete drivers\n * are constructed by the host stack (ADR-0015 Addendum §3.5). This factory is\n * the host-side glue that lets the runtime-datasource lifecycle\n * (`IDatasourceAdminService`) build a live driver from an *unsaved* draft so it\n * can probe a connection before \"Save\" and hot-register a pool afterwards.\n *\n * Supported driver ids map onto the same open-core drivers the standalone\n * stack auto-detects:\n * - `postgres` / `pg` / `postgresql` → `@objectstack/driver-sql` (client `pg`)\n * - `sqlite` / `sqlite3` → `@objectstack/driver-sql` (better-sqlite3)\n * - `mongodb` / `mongo` → `@objectstack/driver-mongodb` (peer dep)\n * - `memory` / `inmemory` → `@objectstack/driver-memory`\n *\n * Anything else returns `supports() === false`, so the admin service degrades\n * gracefully (testConnection → `{ ok: false }`, create skips hot pool reg).\n *\n * SECURITY: the cleartext `spec.secret` is used only to open the connection and\n * is never persisted or logged here.\n */\n\nimport type {\n IDatasourceDriverFactory,\n DatasourceConnectionSpec,\n DatasourceDriverHandle,\n} from './contracts/index.js';\n\ntype ResolvedKind = 'postgres' | 'sqlite' | 'mongodb' | 'memory';\n\nconst DRIVER_ID_ALIASES: Record<string, ResolvedKind> = {\n postgres: 'postgres',\n postgresql: 'postgres',\n pg: 'postgres',\n sqlite: 'sqlite',\n sqlite3: 'sqlite',\n 'better-sqlite3': 'sqlite',\n mongodb: 'mongodb',\n mongo: 'mongodb',\n memory: 'memory',\n inmemory: 'memory',\n 'in-memory': 'memory',\n};\n\nfunction resolveKind(driverId: string): ResolvedKind | undefined {\n return DRIVER_ID_ALIASES[String(driverId ?? '').toLowerCase()];\n}\n\n/**\n * Wrap a concrete engine driver in a probe handle. `ping`/`checkHealth` reuse\n * the driver's own health check; `driver` is the escape hatch the admin service\n * hands to `registerDriver()`.\n */\nfunction toHandle(driver: any, serverVersion?: () => Promise<string | undefined>): DatasourceDriverHandle {\n return {\n connect: typeof driver?.connect === 'function' ? () => driver.connect() : undefined,\n disconnect: typeof driver?.disconnect === 'function' ? () => driver.disconnect() : undefined,\n checkHealth: typeof driver?.checkHealth === 'function' ? () => driver.checkHealth() : undefined,\n ping: typeof driver?.checkHealth === 'function' ? () => driver.checkHealth() : undefined,\n ...(serverVersion ? { serverVersion } : {}),\n driver,\n };\n}\n\n/** Build the Knex `connection` for a SQL driver from a spec's config + secret. */\nfunction buildSqlConnection(spec: DatasourceConnectionSpec, client: 'pg' | 'better-sqlite3'): unknown {\n const cfg = (spec.config ?? {}) as Record<string, unknown>;\n\n if (client === 'better-sqlite3') {\n const filename =\n (cfg.filename as string | undefined) ??\n (cfg.file as string | undefined) ??\n (cfg.database as string | undefined) ??\n ':memory:';\n return { filename };\n }\n\n // pg — accept either a connection string (`url`/`connectionString`) or\n // discrete fields. The secret is the password and is never part of `config`.\n const url = (cfg.url as string | undefined) ?? (cfg.connectionString as string | undefined);\n if (url) {\n // For a DSN, a separately-supplied secret overrides the embedded password.\n return spec.secret ? { connectionString: url, password: spec.secret } : { connectionString: url };\n }\n return {\n host: cfg.host,\n port: cfg.port,\n database: cfg.database,\n user: cfg.user ?? cfg.username,\n ...(spec.secret ? { password: spec.secret } : cfg.password ? { password: cfg.password } : {}),\n ...(cfg.ssl != null ? { ssl: cfg.ssl } : {}),\n };\n}\n\n/** Build a mongodb connection URL from a spec's config + secret. */\nfunction buildMongoUrl(spec: DatasourceConnectionSpec): string {\n const cfg = (spec.config ?? {}) as Record<string, unknown>;\n const explicit = (cfg.url as string | undefined) ?? (cfg.uri as string | undefined);\n if (explicit) return explicit;\n const host = (cfg.host as string | undefined) ?? 'localhost';\n const port = (cfg.port as number | string | undefined) ?? 27017;\n const db = (cfg.database as string | undefined) ?? '';\n const user = (cfg.user as string | undefined) ?? (cfg.username as string | undefined);\n const auth = user ? `${encodeURIComponent(user)}:${encodeURIComponent(spec.secret ?? '')}@` : '';\n return `mongodb://${auth}${host}:${port}/${db}`;\n}\n\n/**\n * Create the default datasource driver factory. Driver packages are imported\n * lazily so a host that never builds (e.g.) a mongo connection doesn't pay for\n * the mongo SDK.\n */\nexport function createDefaultDatasourceDriverFactory(): IDatasourceDriverFactory {\n return {\n supports(driverId: string): boolean {\n return resolveKind(driverId) !== undefined;\n },\n\n async create(spec: DatasourceConnectionSpec): Promise<DatasourceDriverHandle> {\n const kind = resolveKind(spec.driver);\n if (!kind) {\n throw new Error(`Unsupported driver id '${spec.driver}'.`);\n }\n\n const schemaMode = (spec.external as { schemaMode?: string } | undefined)?.schemaMode\n ?? ((spec.config as Record<string, unknown> | undefined)?.schemaMode as string | undefined);\n\n if (kind === 'postgres') {\n const { SqlDriver } = await import('@objectstack/driver-sql');\n const driver = new SqlDriver({\n client: 'pg',\n connection: buildSqlConnection(spec, 'pg') as any,\n pool: { min: 0, max: 5 },\n ...(schemaMode ? { schemaMode: schemaMode as any } : {}),\n } as any);\n return toHandle(driver, () => sqlServerVersion(driver, 'pg'));\n }\n\n if (kind === 'sqlite') {\n const { SqlDriver } = await import('@objectstack/driver-sql');\n const driver = new SqlDriver({\n client: 'better-sqlite3',\n connection: buildSqlConnection(spec, 'better-sqlite3') as any,\n useNullAsDefault: true,\n ...(schemaMode ? { schemaMode: schemaMode as any } : {}),\n } as any);\n return toHandle(driver, () => sqlServerVersion(driver, 'sqlite'));\n }\n\n if (kind === 'mongodb') {\n let MongoDBDriver: any;\n try {\n ({ MongoDBDriver } = await import('@objectstack/driver-mongodb' as any));\n } catch (err: any) {\n throw new Error(\n `mongodb driver requested but @objectstack/driver-mongodb is not installed (${err?.message ?? err}).`,\n );\n }\n const driver = new MongoDBDriver({ url: buildMongoUrl(spec) });\n return toHandle(driver);\n }\n\n // memory\n const { InMemoryDriver } = await import('@objectstack/driver-memory');\n return toHandle(new InMemoryDriver());\n },\n };\n}\n\n/** Best-effort server version via a raw query; swallows everything. */\nasync function sqlServerVersion(driver: any, client: 'pg' | 'sqlite'): Promise<string | undefined> {\n if (typeof driver?.execute !== 'function') return undefined;\n try {\n const sql = client === 'pg' ? 'SELECT version() AS v' : 'SELECT sqlite_version() AS v';\n const rows: any = await driver.execute(sql);\n const first = Array.isArray(rows) ? rows[0] : Array.isArray(rows?.rows) ? rows.rows[0] : rows;\n const v = first?.v ?? first?.version ?? first?.['sqlite_version()'];\n return typeof v === 'string' ? v : undefined;\n } catch {\n return undefined;\n }\n}\n","// Copyright (c) 2025 ObjectStack. Licensed under the Apache-2.0 license.\n\n/**\n * Default datasource SecretBinder — persists a runtime datasource's cleartext\n * credential into the `sys_secret` cipher store and returns an opaque\n * `credentialsRef` handle (ADR-0015 Addendum, security invariant).\n *\n * Mirrors the SettingsService Phase-3 split: the cleartext is wrapped by an\n * {@link ICryptoProvider} into a {@link CryptoHandle}, the ciphertext lands in a\n * `sys_secret` row keyed by `handle.id`, and only the handle id (wrapped as\n * `sys_secret:<id>`) is ever stored on the datasource artefact. Cleartext never\n * touches metadata.\n *\n * This is the dev/self-host wiring; production hosts swap the\n * `LocalCryptoProvider` for a KMS-backed `ICryptoProvider` and pass it here.\n */\n\nimport type { CryptoHandle, ICryptoProvider } from '@objectstack/spec/contracts';\n\n/** Prefix used to recognise a datasource credential handle. */\nconst REF_PREFIX = 'sys_secret:';\n\n/** A persisted `sys_secret` row (subset used to reconstruct a {@link CryptoHandle}). */\ninterface SecretRow {\n id: string;\n namespace: string;\n key: string;\n kms_key_id: string;\n alg: string;\n version: number;\n ciphertext: string;\n}\n\n/** Minimal data-engine surface used to read/write the `sys_secret` store. */\nexport interface SecretStoreEngineLike {\n insert(object: string, data: Record<string, unknown>, options?: unknown): Promise<unknown>;\n delete(object: string, options: { where: Record<string, unknown> }): Promise<unknown>;\n /**\n * Read `sys_secret` rows for the `resolve()` path. Optional so existing\n * callers that only bind/unbind keep working; `resolve()` no-ops when absent.\n * Mirrors `IDataEngine.find` — returns an array (or `{ data: [...] }`).\n */\n find?(object: string, query: Record<string, unknown>): Promise<unknown>;\n}\n\nexport interface DatasourceSecretBinderDeps {\n /** Data engine (ObjectQL) used to persist the `sys_secret` row. */\n engine: SecretStoreEngineLike;\n /** Crypto provider that wraps cleartext into a {@link CryptoHandle}. */\n cryptoProvider: ICryptoProvider;\n /** Settings namespace recorded on the secret row (default `'datasource'`). */\n namespace?: string;\n}\n\nexport interface DatasourceSecretBinder {\n bind(input: { value: string; namespace?: string; key?: string }, hint: { name: string }): Promise<string>;\n unbind(credentialsRef: string): Promise<void>;\n /**\n * Dereference a `credentialsRef` back to its cleartext credential by reading\n * the `sys_secret` row and decrypting it. Used at boot to rebuild a runtime\n * datasource's live connection pool (the cleartext is never persisted, so it\n * must be recovered from the cipher store). Returns `undefined` when the ref\n * isn't ours, the row is gone, the engine can't read, or decryption fails\n * (e.g. an ephemeral dev key changed across restarts) — callers degrade to\n * skipping that pool rather than crashing boot.\n */\n resolve(credentialsRef: string): Promise<string | undefined>;\n}\n\n/** Build a `credentialsRef` from a crypto handle id. */\nexport function toCredentialsRef(handleId: string): string {\n return `${REF_PREFIX}${handleId}`;\n}\n\n/** Extract the `sys_secret` handle id from a credentialsRef, if it is one. */\nexport function parseCredentialsRef(ref: string): string | undefined {\n return ref?.startsWith(REF_PREFIX) ? ref.slice(REF_PREFIX.length) : undefined;\n}\n\n/**\n * Create the default datasource secret binder. Persists into `sys_secret` via\n * the data engine and never returns or logs the cleartext.\n */\nexport function createDatasourceSecretBinder(deps: DatasourceSecretBinderDeps): DatasourceSecretBinder {\n const { engine, cryptoProvider } = deps;\n const defaultNamespace = deps.namespace ?? 'datasource';\n\n return {\n async bind(input, hint) {\n const namespace = input.namespace ?? defaultNamespace;\n const key = input.key ?? hint.name;\n const handle: CryptoHandle = await cryptoProvider.encrypt(input.value, { namespace, key });\n await engine.insert('sys_secret', {\n id: handle.id,\n namespace,\n key,\n kms_key_id: handle.kmsKeyId,\n alg: handle.alg,\n version: handle.version,\n ciphertext: handle.ciphertext,\n });\n return toCredentialsRef(handle.id);\n },\n\n async unbind(credentialsRef) {\n const id = parseCredentialsRef(credentialsRef);\n if (!id) return; // not ours (or already cleared) — nothing to do\n await engine.delete('sys_secret', { where: { id } });\n },\n\n async resolve(credentialsRef) {\n const id = parseCredentialsRef(credentialsRef);\n if (!id || typeof engine.find !== 'function') return undefined;\n try {\n const result = await engine.find('sys_secret', {\n where: { id },\n limit: 1,\n // Secrets are scoped through their owning datasource artefact, so\n // skip the tenant-audit warning (mirrors SettingsService's store).\n bypassTenantAudit: true,\n });\n const rows = (Array.isArray(result) ? result : (result as { data?: unknown[] })?.data) ?? [];\n const row = rows[0] as SecretRow | undefined;\n if (!row?.ciphertext) return undefined;\n // Reconstruct the handle and decrypt under the same (namespace,key)\n // AAD the row was sealed with — a mismatch fails authentication.\n return await cryptoProvider.decrypt(\n {\n id: row.id,\n kmsKeyId: row.kms_key_id,\n alg: row.alg,\n version: row.version,\n ciphertext: row.ciphertext,\n },\n { namespace: row.namespace, key: row.key },\n );\n } catch {\n // Missing row / unreadable engine / decrypt failure (e.g. rotated dev\n // key) — never block boot; the pool is simply not rehydrated.\n return undefined;\n }\n },\n };\n}\n","// Copyright (c) 2025 ObjectStack. Licensed under the Apache-2.0 license.\n\nimport type { PluginContext } from '@objectstack/core';\nimport type { IHttpServer } from '@objectstack/spec/contracts';\n\n/**\n * Datasource lifecycle REST routes (ADR-0015 Addendum §3.5).\n *\n * Mounted under `/api/v1/datasources` and served by the `datasource-admin`\n * service. Every route degrades gracefully\n * (`503 datasource_admin_unavailable`) when the service is not wired in, and\n * lifecycle/validation failures surface as `400` with the service's message.\n *\n * GET /datasources → listDatasources (provenance + health)\n * POST /datasources/test → testConnection (no persistence)\n * POST /datasources → createDatasource (origin: 'runtime')\n * PATCH /datasources/:name → updateDatasource (runtime only)\n * DELETE /datasources/:name → removeDatasource (runtime only)\n *\n * Request bodies carry the connection draft inline with an optional cleartext\n * `secret` field; the route splits `secret` out so it never reaches the draft\n * the service persists.\n */\nexport function registerDatasourceAdminRoutes(\n server: IHttpServer,\n ctx: PluginContext,\n basePath = '/api/v1',\n): void {\n const root = `${basePath}/datasources`;\n\n const adminService = (): any => {\n try {\n return ctx.getService<any>('datasource-admin');\n } catch {\n return undefined;\n }\n };\n\n const unavailable = (res: any) =>\n res.status(503).json({ error: 'datasource_admin_unavailable' });\n\n const badRequest = (res: any, err: unknown) =>\n res.status(400).json({ error: 'datasource_admin_error', message: err instanceof Error ? err.message : String(err) });\n\n /** Split an inline `{ secret, ...draft }` body into (draft, secret). */\n const splitSecret = (body: any): { draft: any; secret: any } => {\n const { secret, ...draft } = (body as Record<string, unknown>) ?? {};\n // Accept either a bare string or a `{ value, namespace?, key? }` object.\n const normalised =\n secret == null\n ? undefined\n : typeof secret === 'string'\n ? { value: secret }\n : secret;\n return { draft, secret: normalised };\n };\n\n // List all datasources with provenance + health.\n server.get(root, async (_req: any, res: any) => {\n const svc = adminService();\n if (!svc?.listDatasources) return unavailable(res);\n const datasources = await svc.listDatasources();\n res.json({ datasources });\n });\n\n // Probe a connection without persisting anything. Registered before the\n // `:name` routes so the literal `test` segment is never captured as a name.\n server.post(`${root}/test`, async (req: any, res: any) => {\n const svc = adminService();\n if (!svc?.testConnection) return unavailable(res);\n const { draft, secret } = splitSecret(req.body);\n try {\n const result = await svc.testConnection(draft, secret);\n res.json({ result });\n } catch (err) {\n badRequest(res, err);\n }\n });\n\n // Create a runtime datasource.\n server.post(root, async (req: any, res: any) => {\n const svc = adminService();\n if (!svc?.createDatasource) return unavailable(res);\n const { draft, secret } = splitSecret(req.body);\n try {\n const datasource = await svc.createDatasource(draft, secret);\n res.status(201).json({ datasource });\n } catch (err) {\n badRequest(res, err);\n }\n });\n\n // Patch a runtime datasource.\n server.patch(`${root}/:name`, async (req: any, res: any) => {\n const svc = adminService();\n if (!svc?.updateDatasource) return unavailable(res);\n const { draft, secret } = splitSecret(req.body);\n try {\n const datasource = await svc.updateDatasource(req.params.name, draft, secret);\n res.json({ datasource });\n } catch (err) {\n badRequest(res, err);\n }\n });\n\n // Remove a runtime datasource.\n server.delete(`${root}/:name`, async (req: any, res: any) => {\n const svc = adminService();\n if (!svc?.removeDatasource) return unavailable(res);\n try {\n await svc.removeDatasource(req.params.name);\n res.status(204).end();\n } catch (err) {\n badRequest(res, err);\n }\n });\n}\n"],"mappings":";AA0BA;AAAA,EACE;AAAA,EACA;AAAA,EACA;AAAA,OAIK;AA4DP,IAAM,kBAAkB,oBAAI,IAAI,CAAC,MAAM,cAAc,YAAY,CAAC;AAGlE,SAAS,eAAe,KAAgD;AACtE,QAAM,MAAM,IAAI,QAAQ,GAAG;AAC3B,MAAI,QAAQ,GAAI,QAAO,EAAE,MAAM,IAAI;AACnC,SAAO,EAAE,QAAQ,IAAI,MAAM,GAAG,GAAG,GAAG,MAAM,IAAI,MAAM,MAAM,CAAC,EAAE;AAC/D;AAGA,SAAS,aAAa,YAA4B;AAChD,QAAM,EAAE,KAAK,IAAI,eAAe,UAAU;AAC1C,SAAO,KACJ,QAAQ,kBAAkB,GAAG,EAC7B,QAAQ,YAAY,CAAC,MAAM,IAAI,EAAE,YAAY,CAAC,EAAE,EAChD,YAAY;AACjB;AAGA,SAAS,QAAQ,MAAsB;AACrC,SAAO,KACJ,MAAM,GAAG,EACT,OAAO,OAAO,EACd,IAAI,CAAC,MAAM,EAAE,OAAO,CAAC,EAAE,YAAY,IAAI,EAAE,MAAM,CAAC,CAAC,EACjD,KAAK,GAAG;AACb;AAEO,IAAM,4BAAN,MAAsE;AAAA,EAC3E,YAA6B,QAAyC;AAAzC;AAAA,EAA0C;AAAA,EAEvE,IAAY,SAA6B;AACvC,WAAO,KAAK,OAAO;AAAA,EACrB;AAAA,EAEQ,UAAU,QAA4B,YAAmD;AAC/F,UAAM,OAAO,eAAe,UAAU,EAAE;AACxC,eAAW,SAAS,OAAO,OAAO,OAAO,MAAM,GAAG;AAChD,UAAI,MAAM,SAAS,WAAY,QAAO;AACtC,UAAI,eAAe,MAAM,IAAI,EAAE,SAAS,KAAM,QAAO;AAAA,IACvD;AACA,WAAO;AAAA,EACT;AAAA,EAEA,MAAM,iBACJ,YACA,MACwB;AACxB,UAAM,CAAC,QAAQ,EAAE,IAAI,MAAM,QAAQ,IAAI;AAAA,MACrC,KAAK,OAAO,WAAW,UAAU;AAAA,MACjC,KAAK,OAAO,cAAc,UAAU;AAAA,IACtC,CAAC;AACD,UAAM,UAAU,IAAI,UAAU;AAE9B,UAAM,SAAwB,CAAC;AAC/B,eAAW,SAAS,OAAO,OAAO,OAAO,MAAM,GAAG;AAChD,YAAM,EAAE,QAAQ,aAAa,KAAK,IAAI,eAAe,MAAM,IAAI;AAC/D,UAAI,MAAM,UAAU,eAAe,gBAAgB,KAAK,OAAQ;AAEhE,UAAI,WAAW,eAAe,CAAC,QAAQ,SAAS,WAAW,EAAG;AAC9D,aAAO,KAAK,EAAE,QAAQ,aAAa,MAAM,aAAa,MAAM,QAAQ,OAAO,CAAC;AAAA,IAC9E;AACA,WAAO;AAAA,EACT;AAAA,EAEA,MAAM,oBACJ,YACA,YACA,OAA0B,CAAC,GACL;AACtB,UAAM,SAAS,MAAM,KAAK,OAAO,WAAW,UAAU;AACtD,UAAM,QAAQ,KAAK,UAAU,QAAQ,UAAU;AAC/C,QAAI,CAAC,OAAO;AACV,YAAM,IAAI;AAAA,QACR,iBAAiB,UAAU,8BAA8B,UAAU;AAAA,MACrE;AAAA,IACF;AACA,UAAM,UAAU,OAAO;AAGvB,UAAM,UAAU,eAAe,MAAM,IAAI;AACzC,UAAM,eAAe,KAAK,gBAAgB,QAAQ;AAClD,UAAM,qBAAqB,QAAQ;AAEnC,UAAM,UAAU,KAAK,iBAAiB,IAAI,IAAI,KAAK,cAAc,IAAI;AACrE,UAAM,UAAU,KAAK,iBAAiB,IAAI,IAAI,KAAK,cAAc,IAAI,oBAAI,IAAY;AACrF,UAAM,aAAa,KAAK,aAAa,IAAI,IAAI,KAAK,UAAU,IAAI;AAEhE,UAAM,SAAoE,CAAC;AAC3E,UAAM,SAAgC,CAAC;AAEvC,eAAW,OAAO,MAAM,SAAS;AAC/B,UAAI,WAAW,CAAC,QAAQ,IAAI,IAAI,IAAI,EAAG;AACvC,UAAI,QAAQ,IAAI,IAAI,IAAI,EAAG;AAE3B,YAAM,YAAY,KAAK,SAAS,IAAI,IAAI,KAAK,IAAI;AACjD,YAAM,YAAY,iBAAiB,IAAI,MAAM,OAAO;AACpD,YAAM,YAAuB,aAAa;AAC1C,UAAI,CAAC,WAAW;AACd,eAAO,KAAK;AAAA,UACV,QAAQ,IAAI;AAAA,UACZ,YAAY,IAAI;AAAA,UAChB,MAAM;AAAA,QACR,CAAC;AAAA,MACH,WAAW,aAAa,IAAI,MAAM,WAAW,OAAO,MAAM,SAAS;AACjE,eAAO,KAAK;AAAA,UACV,QAAQ,IAAI;AAAA,UACZ,YAAY,IAAI;AAAA,UAChB,MAAM,oBAAoB,SAAS;AAAA,QACrC,CAAC;AAAA,MACH;AAEA,YAAM,OAAO,aAAa,WAAW,IAAI,IAAI,IAAI,IAAI,IAAI;AACzD,aAAO,SAAS,IAAI,OAAO,EAAE,MAAM,WAAW,YAAY,KAAK,IAAI,EAAE,MAAM,UAAU;AAAA,IACvF;AAEA,UAAM,OAAO,aAAa,kBAAkB;AAC5C,UAAM,aAAsC;AAAA,MAC1C;AAAA,MACA,OAAO,QAAQ,IAAI;AAAA,MACnB;AAAA,MACA,UAAU;AAAA,QACR,GAAI,eAAe,EAAE,aAAa,IAAI,CAAC;AAAA,QACvC,YAAY;AAAA,MACd;AAAA,MACA;AAAA,IACF;AAEA,WAAO;AAAA,MACL;AAAA,MACA;AAAA,MACA;AAAA,MACA,QAAQ,mBAAmB,YAAY,QAAQ,MAAM;AAAA,MACrD;AAAA,IACF;AAAA,EACF;AAAA,EAEA,MAAM,aACJ,YACA,YACA,OAAyB,CAAC,GACG;AAC7B,QAAI,CAAC,KAAK,OAAO,eAAe;AAC9B,YAAM,IAAI;AAAA,QACR,mFACkB,UAAU;AAAA,MAE9B;AAAA,IACF;AAGA,UAAM,QAAQ,MAAM,KAAK,oBAAoB,YAAY,YAAY,IAAI;AAGzE,UAAM,OAAO,KAAK,QAAQ,MAAM;AAChC,UAAM,WAAW;AAAA,MACf,GAAI,MAAM,WAAW;AAAA,MACrB,GAAI,KAAK,WAAW,EAAE,UAAU,KAAK,IAAI,CAAC;AAAA,IAC5C;AACA,UAAM,aAAsC;AAAA,MAC1C,GAAG,MAAM;AAAA,MACT;AAAA,MACA,OAAO,QAAQ,IAAI;AAAA,MACnB;AAAA,IACF;AAEA,UAAM,KAAK,OAAO,cAAc,MAAM,UAAU;AAChD,SAAK,QAAQ,OAAO,4BAA4B,IAAI,UAAU,UAAU,IAAI,UAAU,IAAI;AAAA,MACxF,UAAU,KAAK,aAAa;AAAA,MAC5B,QAAQ,MAAM,OAAO;AAAA,IACvB,CAAC;AAED,WAAO,EAAE,MAAM,YAAY,QAAQ,MAAM,OAAO;AAAA,EAClD;AAAA,EAEA,MAAM,eAAe,YAA8C;AACjE,UAAM,SAAS,MAAM,KAAK,OAAO,WAAW,UAAU;AAItD,UAAM,UAAU,sBAAsB,MAAM;AAAA,MAC1C,MAAM,GAAG,UAAU;AAAA,MACnB;AAAA,MACA,aAAY,oBAAI,KAAK,GAAE,YAAY;AAAA,MACnC,SAAS,OAAO;AAAA,MAChB,QAAQ,OAAO,OAAO,OAAO,MAAM,EAAE,IAAI,CAAC,MAAM;AAC9C,cAAM,EAAE,QAAQ,GAAG,KAAK,IAAI,eAAe,EAAE,IAAI;AACjD,eAAO;AAAA,UACL,cAAc;AAAA,UACd,YAAY;AAAA,UACZ,SAAS,EAAE,QAAQ,IAAI,CAAC,OAAO;AAAA,YAC7B,MAAM,EAAE;AAAA,YACR,SAAS,EAAE;AAAA,YACX,UAAU,EAAE;AAAA,YACZ,YAAY,EAAE;AAAA,YACd,oBAAoB,iBAAiB,EAAE,MAAM,OAAO,OAAqB;AAAA,UAC3E,EAAE;AAAA,QACJ;AAAA,MACF,CAAC;AAAA,IACH,CAAC;AAID,QAAI,KAAK,OAAO,gBAAgB;AAC9B,UAAI;AACF,cAAM,KAAK,OAAO,eAAe,OAAO;AAAA,MAC1C,SAAS,KAAK;AACZ,aAAK,QAAQ,OAAO,sCAAsC,QAAQ,IAAI,KAAK,GAAG;AAAA,MAChF;AAAA,IACF;AAEA,WAAO;AAAA,EACT;AAAA,EAEA,MAAM,eAAe,YAAqD;AACxE,UAAM,MAAM,MAAM,KAAK,OAAO,UAAU,UAAU;AAClD,QAAI,CAAC,KAAK;AACR,YAAM,IAAI,MAAM,WAAW,UAAU,cAAc;AAAA,IACrD;AACA,UAAM,aAAa,IAAI,cAAc;AACrC,UAAM,KAAK,MAAM,KAAK,OAAO,cAAc,UAAU;AAGrD,QAAI,CAAC,MAAM,CAAC,GAAG,cAAc,GAAG,eAAe,WAAW;AACxD,aAAO,EAAE,IAAI,MAAM,YAAY,QAAQ,YAAY,OAAO,CAAC,EAAE;AAAA,IAC/D;AAEA,UAAM,SAAS,MAAM,KAAK,OAAO,WAAW,UAAU;AACtD,UAAM,UAAU,OAAO;AACvB,UAAM,aAAa,IAAI,UAAU,cAAc,IAAI;AACnD,UAAM,QAAQ,KAAK,UAAU,QAAQ,UAAU;AAE/C,UAAM,QAA2B,CAAC;AAElC,QAAI,CAAC,OAAO;AACV,YAAM,KAAK;AAAA,QACT,MAAM;AAAA,QACN,cAAc,IAAI,UAAU;AAAA,QAC5B;AAAA,QACA,UAAU;AAAA,MACZ,CAAC;AACD,aAAO,EAAE,IAAI,OAAO,YAAY,QAAQ,YAAY,MAAM;AAAA,IAC5D;AAEA,UAAM,gBAAgB,IAAI,IAAI,MAAM,QAAQ,IAAI,CAAC,MAAM,CAAC,EAAE,MAAM,CAAC,CAAC,CAAC;AACnE,UAAM,SAAS,IAAI,IAAI,IAAI,UAAU,iBAAiB,CAAC,CAAC;AAExD,UAAM,gBAAgB,oBAAI,IAAoB;AAC9C,eAAW,CAAC,WAAW,SAAS,KAAK,OAAO,QAAQ,IAAI,UAAU,aAAa,CAAC,CAAC,GAAG;AAClF,oBAAc,IAAI,WAAW,SAAS;AAAA,IACxC;AAEA,eAAW,CAAC,WAAW,KAAK,KAAK,OAAO,QAAQ,IAAI,UAAU,CAAC,CAAC,GAAG;AACjE,UAAI,gBAAgB,IAAI,SAAS,EAAG;AACpC,YAAM,YAAY,cAAc,IAAI,SAAS,KAAK;AAClD,UAAI,OAAO,IAAI,SAAS,EAAG;AAE3B,YAAM,MAAM,cAAc,IAAI,SAAS;AACvC,UAAI,CAAC,KAAK;AACR,cAAM,KAAK;AAAA,UACT,MAAM;AAAA,UACN;AAAA,UACA,QAAQ;AAAA,UACR,UAAU;AAAA,QACZ,CAAC;AACD;AAAA,MACF;AACA,YAAM,YAAa,MAAM,QAAQ;AACjC,YAAM,SAAS,aAAa,IAAI,MAAM,WAAW,OAAO;AACxD,UAAI,WAAW,OAAO;AACpB,cAAM,KAAK;AAAA,UACT,MAAM;AAAA,UACN;AAAA,UACA,QAAQ;AAAA,UACR,UAAU;AAAA,UACV,QAAQ,IAAI;AAAA,UACZ,UAAU;AAAA,QACZ,CAAC;AAAA,MACH,WAAW,WAAW,SAAS;AAC7B,cAAM,KAAK;AAAA,UACT,MAAM;AAAA,UACN;AAAA,UACA,QAAQ;AAAA,UACR,UAAU;AAAA,UACV,QAAQ,IAAI;AAAA,UACZ,UAAU;AAAA,QACZ,CAAC;AAAA,MACH;AAAA,IACF;AAEA,UAAM,KAAK,CAAC,MAAM,KAAK,CAAC,MAAM,EAAE,aAAa,OAAO;AACpD,WAAO,EAAE,IAAI,YAAY,QAAQ,YAAY,MAAM;AAAA,EACrD;AAAA,EAEA,MAAM,cAA+C;AACnD,UAAM,UAAU,MAAM,KAAK,OAAO,YAAY;AAC9C,UAAM,YAAY,QAAQ;AAAA,MACxB,CAAC,MAAM,EAAE,aAAa,UAAc,EAAE,cAAc,EAAE,eAAe;AAAA,IACvE;AAEA,UAAM,UAAU,MAAM,QAAQ;AAAA,MAC5B,UAAU;AAAA,QAAI,CAAC,MACb,KAAK,eAAe,EAAE,IAAI,EAAE,MAAM,CAAC,QAAgC;AACjE,eAAK,QAAQ,KAAK,mBAAmB,EAAE,IAAI,aAAa,GAAG;AAC3D,iBAAO;AAAA,YACL,IAAI;AAAA,YACJ,YAAY,EAAE,cAAc;AAAA,YAC5B,QAAQ,EAAE;AAAA,YACV,OAAO;AAAA,cACL;AAAA,gBACE,MAAM;AAAA,gBACN,YAAY,EAAE,UAAU,cAAc,EAAE;AAAA,gBACxC,QAAQ,eAAe,QAAQ,IAAI,UAAU,OAAO,GAAG;AAAA,gBACvD,UAAU;AAAA,cACZ;AAAA,YACF;AAAA,UACF;AAAA,QACF,CAAC;AAAA,MACH;AAAA,IACF;AAEA,UAAM,KAAK,QAAQ,MAAM,CAAC,MAAM,EAAE,EAAE;AACpC,WAAO,EAAE,IAAI,QAAQ;AAAA,EACvB;AACF;AAGA,SAAS,mBACP,YACA,QACA,QACQ;AACR,QAAM,iBAAiB,IAAI,IAAI,OAAO,IAAI,CAAC,MAAM,CAAC,EAAE,QAAQ,EAAE,IAAI,CAAC,CAAC;AACpE,QAAM,WAAW,WAAW;AAE5B,QAAM,aAAa,OAAO,QAAQ,MAAM,EAAE,IAAI,CAAC,CAAC,WAAW,CAAC,MAAM;AAChE,UAAM,OAAO,eAAe,IAAI,SAAS;AACzC,UAAM,KAAK,EAAE,aAAa,uBAAuB;AACjD,UAAM,UAAU,OAAO,eAAe,IAAI,KAAK;AAC/C,WAAO,OAAO,SAAS,cAAc,EAAE,IAAI,IAAI,EAAE,MAAM,OAAO;AAAA,EAChE,CAAC;AAED,QAAM,eAAe,SAAS,eAC1B,gCAAgC,SAAS,YAAY,mBAAmB,SAAS,UAAU,SAC3F,8BAA8B,SAAS,UAAU;AAErD,SAAO;AAAA,IACL;AAAA,IACA;AAAA,IACA;AAAA,IACA,SAAS,WAAW,IAAc;AAAA,IAClC,YAAY,WAAW,IAAc;AAAA,IACrC,aAAa,WAAW,KAAe;AAAA,IACvC,kBAAkB,WAAW,UAAoB;AAAA,IACjD;AAAA,IACA;AAAA,IACA,GAAG;AAAA,IACH;AAAA,IACA;AAAA,IACA;AAAA,IACA,kBAAkB,WAAW,IAAc;AAAA,IAC3C;AAAA,EACF,EAAE,KAAK,IAAI;AACb;;;AC3ZO,IAAM,kCAAN,MAAwD;AAAA,EAS7D,YAAY,UAAkD,CAAC,GAAG;AARlE,gBAAO;AACP,mBAAU;AACV,gBAAO;AACP,wBAAyB,CAAC;AAMxB,SAAK,UAAU;AAAA,EACjB;AAAA,EAEA,MAAM,KAAK,KAAmC;AAC5C,UAAM,SAAS,eAA+B,KAAK,MAAM;AACzD,UAAM,WAAW,eAAoC,KAAK,UAAU;AAEpE,UAAM,aACJ,KAAK,QAAQ,eACZ,OAAO,eAAuB;AAC7B,UAAI,QAAQ,qBAAsB,QAAO,OAAO,qBAAqB,UAAU;AAC/E,YAAM,SAAS,QAAQ,sBAAsB,UAAU;AACvD,UAAI,QAAQ,iBAAkB,QAAO,OAAO,iBAAiB;AAC7D,YAAM,IAAI;AAAA,QACR,iCAAiC,UAAU;AAAA,MAC7C;AAAA,IACF;AAEF,UAAM,SAA0C;AAAA,MAC9C;AAAA,MACA,eAAe,OAAO,MAAO,MAAM,UAAU,IAAI,cAAc,CAAC;AAAA,MAChE,WAAW,OAAO,MACf,UAAU,YAAY,MAAM,SAAS,UAAU,CAAC,IAAI,MAAM,UAAU,IAAI,UAAU,CAAC;AAAA,MACtF,aAAa,aACT,UAAU,cACR,MAAM,SAAS,YAAY,IAC3B,MAAM,UAAU,OAAO,QAAQ,MAAM,CAAC;AAAA;AAAA;AAAA;AAAA,MAI5C,GAAI,UAAU,WACV;AAAA,QACE,gBAAgB,OAAO,YAAY;AACjC,gBAAM,SAAS,SAAU,oBAAoB,QAAQ,MAAM,OAAO;AAAA,QACpE;AAAA;AAAA;AAAA,QAGA,eAAe,OAAO,MAAM,eAAe;AACzC,gBAAM,SAAS,SAAU,UAAU,MAAM,UAAU;AAAA,QACrD;AAAA,MACF,IACA,CAAC;AAAA,MACL,QAAQ,KAAK,QAAQ;AAAA,IACvB;AAEA,SAAK,UAAU,IAAI,0BAA0B,MAAM;AACnD,QAAI,gBAAgB,uBAAuB,KAAK,OAAO;AAAA,EACzD;AAAA,EAEA,MAAM,MAAM,KAAmC;AAC7C,QAAI,KAAK,QAAS,OAAM,IAAI,QAAQ,6BAA6B,KAAK,OAAO;AAAA,EAC/E;AAAA,EAEA,MAAM,UAAyB;AAC7B,SAAK,UAAU;AAAA,EACjB;AACF;AAEA,SAAS,eAAkB,KAAoB,MAA6B;AAC1E,MAAI;AACF,WAAO,IAAI,WAAc,IAAI;AAAA,EAC/B,QAAQ;AACN,WAAO;AAAA,EACT;AACF;;;ACrFA,IAAM,UAAU;AA4DT,IAAM,yBAAN,MAAgE;AAAA,EACrE,YAA6B,QAAsC;AAAtC;AAAA,EAAuC;AAAA,EAEpE,IAAY,SAA6B;AACvC,WAAO,KAAK,OAAO;AAAA,EACrB;AAAA,EAEA,MAAM,kBAAgD;AACpD,UAAM,UAAU,MAAM,KAAK,OAAO,sBAAsB;AAIxD,UAAM,SAAS,oBAAI,IAAqE;AACxF,eAAW,OAAO,SAAS;AACzB,YAAM,OAAO,OAAO,IAAI,IAAI,IAAI,KAAK,CAAC;AACtC,UAAI,IAAI,WAAW,UAAW,MAAK,UAAU;AAAA,UACxC,MAAK,OAAO;AACjB,aAAO,IAAI,IAAI,MAAM,IAAI;AAAA,IAC3B;AAEA,UAAM,YAAiC,CAAC;AACxC,eAAW,CAAC,MAAM,IAAI,KAAK,QAAQ;AACjC,YAAM,YAAY,KAAK,QAAQ,KAAK;AACpC,UAAI,CAAC,UAAW;AAChB,gBAAU,KAAK;AAAA,QACb;AAAA,QACA,OAAO,UAAU;AAAA,QACjB,QAAQ,UAAU;AAAA,QAClB,YAAY,UAAU,cAAc;AAAA,QACpC,QAAQ,KAAK,OAAO,SAAS;AAAA,QAC7B,QAAQ,UAAU,UAAU;AAAA,QAC5B,QAAQ;AAAA,QACR,GAAI,KAAK,MAAM,YAAY,EAAE,WAAW,KAAK,KAAK,UAAU,IAAI,CAAC;AAAA,QACjE,GAAI,KAAK,QAAQ,KAAK,UAAU,EAAE,mBAAmB,KAAK,IAAI,CAAC;AAAA,MACjE,CAAC;AAAA,IACH;AACA,WAAO;AAAA,EACT;AAAA,EAEA,MAAM,eAAe,OAAwB,QAAqD;AAChG,QAAI,CAAC,OAAO,QAAQ;AAClB,aAAO,EAAE,IAAI,OAAO,OAAO,6CAA6C;AAAA,IAC1E;AACA,UAAM,iBAAkB,MAAM,UAAsD;AACpF,QAAI;AACF,aAAO,MAAM,KAAK,OAAO,MAAM;AAAA,QAC7B,QAAQ,MAAM;AAAA,QACd,QAAQ,MAAM,UAAU,CAAC;AAAA,QACzB,QAAQ,QAAQ;AAAA,QAChB,UAAU,MAAM;AAAA,QAChB,GAAI,OAAO,mBAAmB,WAAW,EAAE,WAAW,eAAe,IAAI,CAAC;AAAA,MAC5E,CAAC;AAAA,IACH,SAAS,KAAK;AACZ,aAAO,EAAE,IAAI,OAAO,OAAO,eAAe,QAAQ,IAAI,UAAU,OAAO,GAAG,EAAE;AAAA,IAC9E;AAAA,EACF;AAAA,EAEA,MAAM,iBAAiB,OAAwB,QAAkD;AAC/F,SAAK,gBAAgB,OAAO,IAAI;AAChC,QAAI,CAAC,MAAM,OAAQ,OAAM,IAAI,MAAM,8CAA8C;AAEjF,UAAM,WAAW,MAAM,KAAK,OAAO,oBAAoB,MAAM,IAAI;AACjE,QAAI,UAAU;AACZ,UAAI,SAAS,WAAW,UAAU,SAAS,WAAW,QAAW;AAC/D,cAAM,IAAI;AAAA,UACR,6BAA6B,MAAM,IAAI;AAAA,QACzC;AAAA,MACF;AACA,YAAM,IAAI,MAAM,eAAe,MAAM,IAAI,mBAAmB;AAAA,IAC9D;AAEA,UAAM,SAA2B;AAAA,MAC/B,GAAG,KAAK,SAAS,KAAK;AAAA,MACtB,QAAQ;AAAA,IACV;AAEA,QAAI,QAAQ;AACV,YAAM,iBAAiB,MAAM,KAAK,OAAO,YAAY,QAAQ,EAAE,MAAM,MAAM,KAAK,CAAC;AACjF,aAAO,WAAW,EAAE,GAAI,OAAO,YAAY,CAAC,GAAI,eAAe;AAAA,IACjE;AAEA,UAAM,KAAK,OAAO,oBAAoB,MAAM;AAC5C,UAAM,KAAK,gBAAgB,MAAM;AACjC,WAAO,KAAK,UAAU,MAAM;AAAA,EAC9B;AAAA,EAEA,MAAM,iBACJ,MACA,OACA,QAC4B;AAC5B,UAAM,WAAW,MAAM,KAAK,OAAO,oBAAoB,IAAI;AAC3D,QAAI,CAAC,SAAU,OAAM,IAAI,MAAM,eAAe,IAAI,cAAc;AAChE,QAAI,SAAS,WAAW,WAAW;AACjC,YAAM,IAAI,MAAM,eAAe,IAAI,oDAAoD;AAAA,IACzF;AAGA,UAAM,SAA2B;AAAA,MAC/B,GAAG;AAAA,MACH,GAAI,MAAM,UAAU,SAAY,EAAE,OAAO,MAAM,MAAM,IAAI,CAAC;AAAA,MAC1D,GAAI,MAAM,WAAW,SAAY,EAAE,QAAQ,MAAM,OAAO,IAAI,CAAC;AAAA,MAC7D,GAAI,MAAM,eAAe,SAAY,EAAE,YAAY,MAAM,WAAW,IAAI,CAAC;AAAA,MACzE,GAAI,MAAM,WAAW,SAAY,EAAE,QAAQ,MAAM,OAAO,IAAI,CAAC;AAAA,MAC7D,GAAI,MAAM,SAAS,SAAY,EAAE,MAAM,MAAM,KAAK,IAAI,CAAC;AAAA,MACvD,GAAI,MAAM,WAAW,SAAY,EAAE,QAAQ,MAAM,OAAO,IAAI,CAAC;AAAA,MAC7D,MAAM,SAAS;AAAA,MACf,QAAQ;AAAA,IACV;AACA,QAAI,MAAM,aAAa,QAAW;AAEhC,aAAO,WAAW,EAAE,GAAG,MAAM,UAAU,gBAAgB,SAAS,UAAU,eAAe;AAAA,IAC3F;AAEA,QAAI,QAAQ;AACV,YAAM,UAAU,SAAS,UAAU;AACnC,YAAM,iBAAiB,MAAM,KAAK,OAAO,YAAY,QAAQ,EAAE,KAAK,CAAC;AACrE,aAAO,WAAW,EAAE,GAAI,OAAO,YAAY,CAAC,GAAI,eAAe;AAC/D,UAAI,WAAW,YAAY,eAAgB,OAAM,KAAK,gBAAgB,OAAO;AAAA,IAC/E;AAEA,UAAM,KAAK,OAAO,oBAAoB,MAAM;AAC5C,UAAM,KAAK,gBAAgB,MAAM;AACjC,WAAO,KAAK,UAAU,MAAM;AAAA,EAC9B;AAAA,EAEA,MAAM,iBAAiB,MAA6B;AAClD,UAAM,WAAW,MAAM,KAAK,OAAO,oBAAoB,IAAI;AAC3D,QAAI,CAAC,SAAU,OAAM,IAAI,MAAM,eAAe,IAAI,cAAc;AAChE,QAAI,SAAS,WAAW,WAAW;AACjC,YAAM,IAAI,MAAM,eAAe,IAAI,qDAAqD;AAAA,IAC1F;AAEA,UAAM,QAAQ,MAAM,KAAK,OAAO,kBAAkB,IAAI;AACtD,QAAI,QAAQ,GAAG;AACb,YAAM,IAAI;AAAA,QACR,6BAA6B,IAAI,MAAM,KAAK;AAAA,MAC9C;AAAA,IACF;AAEA,UAAM,KAAK,OAAO,uBAAuB,IAAI;AAC7C,QAAI,SAAS,UAAU,eAAgB,OAAM,KAAK,gBAAgB,SAAS,SAAS,cAAc;AAClG,UAAM,KAAK,kBAAkB,IAAI;AAAA,EACnC;AAAA;AAAA,EAIQ,gBAAgB,MAAgC;AACtD,QAAI,CAAC,QAAQ,CAAC,QAAQ,KAAK,IAAI,GAAG;AAChC,YAAM,IAAI;AAAA,QACR,4BAA4B,QAAQ,EAAE;AAAA,MACxC;AAAA,IACF;AAAA,EACF;AAAA,EAEQ,SAAS,OAA0C;AACzD,WAAO;AAAA,MACL,MAAM,MAAM;AAAA,MACZ,GAAI,MAAM,UAAU,SAAY,EAAE,OAAO,MAAM,MAAM,IAAI,CAAC;AAAA,MAC1D,QAAQ,MAAM;AAAA,MACd,GAAI,MAAM,eAAe,SAAY,EAAE,YAAY,MAAM,WAAW,IAAI,CAAC;AAAA,MACzE,GAAI,MAAM,WAAW,SAAY,EAAE,QAAQ,MAAM,OAAO,IAAI,CAAC;AAAA,MAC7D,GAAI,MAAM,aAAa,SAAY,EAAE,UAAU,MAAM,SAAS,IAAI,CAAC;AAAA,MACnE,GAAI,MAAM,SAAS,SAAY,EAAE,MAAM,MAAM,KAAK,IAAI,CAAC;AAAA,MACvD,GAAI,MAAM,WAAW,SAAY,EAAE,QAAQ,MAAM,OAAO,IAAI,CAAC;AAAA,IAC/D;AAAA,EACF;AAAA,EAEQ,UAAU,QAA6C;AAC7D,WAAO;AAAA,MACL,MAAM,OAAO;AAAA,MACb,OAAO,OAAO;AAAA,MACd,QAAQ,OAAO;AAAA,MACf,YAAY,OAAO,cAAc;AAAA,MACjC,QAAQ,OAAO,UAAU;AAAA,MACzB,QAAQ,OAAO,UAAU;AAAA,MACzB,QAAQ;AAAA,IACV;AAAA,EACF;AAAA,EAEA,MAAc,gBAAgB,QAAyC;AACrE,QAAI;AACF,YAAM,KAAK,OAAO,eAAe,MAAM;AAAA,IACzC,SAAS,KAAK;AACZ,WAAK,QAAQ,KAAK,iBAAiB,OAAO,IAAI,aAAa,GAAG;AAAA,IAChE;AAAA,EACF;AAAA,EAEA,MAAc,kBAAkB,MAA6B;AAC3D,QAAI;AACF,YAAM,KAAK,OAAO,iBAAiB,IAAI;AAAA,IACzC,SAAS,KAAK;AACZ,WAAK,QAAQ,KAAK,mBAAmB,IAAI,aAAa,GAAG;AAAA,IAC3D;AAAA,EACF;AAAA,EAEA,MAAc,gBAAgB,gBAAuC;AACnE,QAAI;AACF,YAAM,KAAK,OAAO,eAAe,cAAc;AAAA,IACjD,SAAS,KAAK;AACZ,WAAK,QAAQ,KAAK,iBAAiB,cAAc,aAAa,GAAG;AAAA,IACnE;AAAA,EACF;AACF;;;ACrSA,SAAS,mCAAmC;AA6ErC,IAAM,+BAAN,MAAqD;AAAA,EAU1D,YAAY,UAA+C,CAAC,GAAG;AAT/D,gBAAO;AACP,mBAAU;AACV,gBAAO;AACP,wBAAyB,CAAC;AAOxB,SAAK,UAAU;AAAA,EACjB;AAAA,EAEA,MAAM,KAAK,KAAmC;AAC5C,UAAM,SAAS,KAAK,QAAQ;AAS5B,gCAA4B,cAAc;AAAA,MACxC;AAAA,QACE,MAAM;AAAA,QACN,OAAO;AAAA,QACP,MAAM;AAAA,QACN,MAAM;AAAA,QACN,QAAQ;AAAA,QACR,QAAQ;AAAA,QACR,SAAS;AAAA,QACT,cAAc;AAAA,QACd,WAAW,CAAC,iBAAiB,WAAW;AAAA,MAC1C;AAAA,IACF,CAAQ;AAMR,UAAM,aAAa,MACjBA,gBAAoC,KAAK,UAAU;AACrD,UAAM,WAAW,MACfA,gBAA+B,KAAK,MAAM;AAE5C,UAAM,UAAU,MACd,KAAK,QAAQ,iBAAiBA,gBAAyC,KAAK,2BAA2B;AAEzG,UAAM,SAAuC;AAAA,MAC3C,OAAO,CAAC,UAAU,KAAK,MAAM,QAAQ,GAAG,KAAK;AAAA,MAE7C,uBAAuB,YAAY;AACjC,cAAM,OAAS,MAAM,WAAW,GAAG,KAAK,YAAY,KAAM,CAAC;AAE3D,eAAO,KAAK,IAAI,CAAC,OAAO,EAAE,GAAG,GAAG,QAAQ,EAAE,UAAU,OAAO,EAAE;AAAA,MAC/D;AAAA,MAEA,qBAAqB,OAAO,SAAS;AACnC,cAAM,MAAO,MAAM,WAAW,GAAG,IAAI,cAAc,IAAI;AACvD,eAAO,MAAM,EAAE,GAAG,KAAK,QAAQ,IAAI,UAAU,OAAO,IAAI;AAAA,MAC1D;AAAA,MAEA,qBAAqB,OAAO,WAAW;AACrC,cAAM,WAAW,WAAW;AAC5B,YAAI,CAAC,UAAU,UAAU;AACvB,gBAAM,IAAI,MAAM,6DAA6D;AAAA,QAC/E;AACA,cAAM,SAAS,SAAS,cAAc,OAAO,MAAM,MAAM;AAAA,MAC3D;AAAA,MAEA,wBAAwB,OAAO,SAAS;AACtC,cAAM,WAAW,WAAW;AAC5B,YAAI,CAAC,UAAU,YAAY;AACzB,gBAAM,IAAI,MAAM,4DAA4D;AAAA,QAC9E;AACA,cAAM,SAAS,WAAW,cAAc,IAAI;AAAA,MAC9C;AAAA,MAEA,aAAa,OAAO,OAAO,SAAS;AAClC,cAAM,SAAS,KAAK,QAAQ;AAC5B,YAAI,CAAC,QAAQ,MAAM;AACjB,gBAAM,IAAI;AAAA,YACR;AAAA,UAEF;AAAA,QACF;AACA,eAAO,OAAO,KAAK,OAAO,IAAI;AAAA,MAChC;AAAA,MAEA,cAAc,OAAO,QAAQ;AAC3B,cAAM,KAAK,QAAQ,SAAS,SAAS,GAAG;AAAA,MAC1C;AAAA,MAEA,mBAAmB,OAAO,eAAe;AACvC,cAAM,WAAW,WAAW;AAC5B,cAAM,UAAY,MAAM,UAAU,cAAc,KAC7C,MAAM,UAAU,KAAK,QAAQ,KAC9B,CAAC;AACH,eAAO,QAAQ,OAAO,CAAC,MAAM,GAAG,eAAe,UAAU,EAAE;AAAA,MAC7D;AAAA,MAEA,cAAc,OAAO,WAAW;AAC9B,cAAM,IAAI,QAAQ;AAClB,cAAM,SAAS,SAAS;AACxB,YAAI,CAAC,KAAK,CAAC,QAAQ,kBAAkB,CAAC,EAAE,SAAS,OAAO,MAAM,EAAG;AAMjE,cAAM,iBAAiB,OAAO,UAAU;AACxC,cAAM,SAAS,iBAAiB,MAAM,KAAK,QAAQ,SAAS,UAAU,cAAc,IAAI;AACxF,cAAM,SAAS,MAAM,EAAE,OAAO,EAAE,GAAG,KAAK,OAAO,MAAM,GAAG,GAAI,SAAS,EAAE,OAAO,IAAI,CAAC,EAAG,CAAC;AACvF,YAAI,OAAO,QAAQ,YAAY,WAAY,OAAM,OAAO,QAAQ;AAKhE,cAAM,eAAgB,OAAO,UAAU;AACvC,YAAI;AACF,uBAAa,OAAO,OAAO;AAAA,QAC7B,QAAQ;AAAA,QAER;AACA,eAAO,eAAe,YAAY;AAClC,eAAO,wBAAwB;AAAA,UAC7B,MAAM,OAAO;AAAA,UACb,YAAY,OAAO;AAAA,UACnB,UAAU,OAAO;AAAA,QACnB,CAAC;AAAA,MACH;AAAA,MAEA,gBAAgB,OAAO,SAAS;AAC9B,cAAM,SAAS,SAAS,GAAG,kBAAkB,IAAI;AACjD,YAAI,OAAO,QAAQ,eAAe,WAAY,OAAM,OAAO,WAAW;AAAA,MACxE;AAAA,MAEA;AAAA,IACF;AAEA,SAAK,SAAS;AACd,SAAK,UAAU,IAAI,uBAAuB,MAAM;AAChD,QAAI,gBAAgB,oBAAoB,KAAK,OAAO;AAAA,EACtD;AAAA,EAEA,MAAM,MAAM,KAAmC;AAI7C,UAAM,KAAK,eAAe;AAC1B,QAAI,KAAK,QAAS,OAAM,IAAI,QAAQ,0BAA0B,KAAK,OAAO;AAAA,EAC5E;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA,EAWA,MAAc,iBAAgC;AAC5C,UAAM,MAAM,KAAK;AACjB,QAAI,CAAC,KAAK,gBAAgB,CAAC,IAAI,sBAAuB;AAEtD,QAAI;AACJ,QAAI;AACF,gBAAU,MAAM,IAAI,sBAAsB;AAAA,IAC5C,SAAS,KAAK;AACZ,WAAK,QAAQ,QAAQ,OAAO,gDAAgD,GAAG;AAC/E;AAAA,IACF;AAEA,UAAM,UAAU,QAAQ,OAAO,CAAC,MAAM,EAAE,WAAW,cAAc,EAAE,UAAU,KAAK;AAClF,QAAI,QAAQ,WAAW,EAAG;AAE1B,QAAI,aAAa;AACjB,eAAW,UAAU,SAAS;AAC5B,UAAI;AACF,cAAM,IAAI,aAAa,MAAM;AAC7B;AAAA,MACF,SAAS,KAAK;AACZ,aAAK,QAAQ,QAAQ,OAAO,+BAA+B,OAAO,IAAI,YAAY,GAAG;AAAA,MACvF;AAAA,IACF;AACA,SAAK,QAAQ,QAAQ;AAAA,MACnB,cAAc,UAAU,IAAI,QAAQ,MAAM;AAAA,IAC5C;AAAA,EACF;AAAA,EAEA,MAAM,UAAyB;AAC7B,SAAK,UAAU;AAAA,EACjB;AAAA;AAAA,EAIQ,OAAO,QAAoD;AACjE,WAAO;AAAA,MACL,MAAM,OAAO;AAAA,MACb,QAAQ,OAAO;AAAA,MACf,QAAQ,OAAO,UAAU,CAAC;AAAA,MAC1B,UAAU,OAAO;AAAA,MACjB,MAAM,OAAO;AAAA,IACf;AAAA,EACF;AAAA;AAAA,EAGA,MAAc,MACZ,SACA,OAC+B;AAC/B,QAAI,CAAC,SAAS;AACZ,aAAO,EAAE,IAAI,OAAO,OAAO,uDAAuD;AAAA,IACpF;AACA,QAAI,CAAC,QAAQ,SAAS,MAAM,MAAM,GAAG;AACnC,aAAO,EAAE,IAAI,OAAO,OAAO,sCAAsC,MAAM,MAAM,KAAK;AAAA,IACpF;AAEA,QAAI;AACJ,QAAI;AACF,eAAS,MAAM,QAAQ,OAAO;AAAA,QAC5B,QAAQ,MAAM;AAAA,QACd,QAAQ,MAAM;AAAA,QACd,QAAQ,MAAM;AAAA,QACd,UAAU,MAAM;AAAA,MAClB,CAAC;AAAA,IACH,SAAS,KAAK;AACZ,aAAO,EAAE,IAAI,OAAO,OAAO,2BAA2B,OAAO,GAAG,CAAC,GAAG;AAAA,IACtE;AAEA,UAAM,YAAY,aAAa;AAC/B,QAAI;AACF,UAAI,OAAO,QAAQ,YAAY,WAAY,OAAM,OAAO,QAAQ;AAGhE,UAAI,OAAO,QAAQ,SAAS,WAAY,OAAM,OAAO,KAAK;AAAA,eACjD,OAAO,QAAQ,gBAAgB,WAAY,OAAM,OAAO,YAAY;AAAA,eACpE,OAAO,QAAQ,qBAAqB,WAAY,OAAM,OAAO,iBAAiB;AACvF,YAAM,YAAY,aAAa,SAAS;AACxC,UAAI;AACJ,UAAI;AACF,wBAAgB,OAAO,QAAQ,kBAAkB,aAAa,MAAM,OAAO,cAAc,IAAI;AAAA,MAC/F,QAAQ;AAAA,MAER;AACA,aAAO,EAAE,IAAI,MAAM,WAAW,GAAI,gBAAgB,EAAE,cAAc,IAAI,CAAC,EAAG;AAAA,IAC5E,SAAS,KAAK;AACZ,aAAO,EAAE,IAAI,OAAO,OAAO,OAAO,GAAG,EAAE;AAAA,IACzC,UAAE;AACA,UAAI;AACF,YAAI,OAAO,QAAQ,eAAe,WAAY,OAAM,OAAO,WAAW;AAAA,MACxE,QAAQ;AAAA,MAER;AAAA,IACF;AAAA,EACF;AACF;AAEA,SAASA,gBAAkB,KAAoB,MAA6B;AAC1E,MAAI;AACF,WAAO,IAAI,WAAc,IAAI;AAAA,EAC/B,QAAQ;AACN,WAAO;AAAA,EACT;AACF;AAEA,SAAS,OAAO,KAAsB;AACpC,SAAO,eAAe,QAAQ,IAAI,UAAU,OAAO,GAAG;AACxD;AAGA,SAAS,eAAuB;AAC9B,QAAM,OAAQ,WAAwD;AACtE,SAAO,OAAO,MAAM,QAAQ,aAAa,KAAK,IAAI,IAAI;AACxD;AAEA,SAAS,aAAa,WAA2B;AAC/C,SAAO,KAAK,IAAI,GAAG,KAAK,MAAM,aAAa,IAAI,SAAS,CAAC;AAC3D;;;ACxUA,IAAM,oBAAkD;AAAA,EACtD,UAAU;AAAA,EACV,YAAY;AAAA,EACZ,IAAI;AAAA,EACJ,QAAQ;AAAA,EACR,SAAS;AAAA,EACT,kBAAkB;AAAA,EAClB,SAAS;AAAA,EACT,OAAO;AAAA,EACP,QAAQ;AAAA,EACR,UAAU;AAAA,EACV,aAAa;AACf;AAEA,SAAS,YAAY,UAA4C;AAC/D,SAAO,kBAAkB,OAAO,YAAY,EAAE,EAAE,YAAY,CAAC;AAC/D;AAOA,SAAS,SAAS,QAAa,eAA2E;AACxG,SAAO;AAAA,IACL,SAAS,OAAO,QAAQ,YAAY,aAAa,MAAM,OAAO,QAAQ,IAAI;AAAA,IAC1E,YAAY,OAAO,QAAQ,eAAe,aAAa,MAAM,OAAO,WAAW,IAAI;AAAA,IACnF,aAAa,OAAO,QAAQ,gBAAgB,aAAa,MAAM,OAAO,YAAY,IAAI;AAAA,IACtF,MAAM,OAAO,QAAQ,gBAAgB,aAAa,MAAM,OAAO,YAAY,IAAI;AAAA,IAC/E,GAAI,gBAAgB,EAAE,cAAc,IAAI,CAAC;AAAA,IACzC;AAAA,EACF;AACF;AAGA,SAAS,mBAAmB,MAAgC,QAA0C;AACpG,QAAM,MAAO,KAAK,UAAU,CAAC;AAE7B,MAAI,WAAW,kBAAkB;AAC/B,UAAM,WACH,IAAI,YACJ,IAAI,QACJ,IAAI,YACL;AACF,WAAO,EAAE,SAAS;AAAA,EACpB;AAIA,QAAM,MAAO,IAAI,OAA+B,IAAI;AACpD,MAAI,KAAK;AAEP,WAAO,KAAK,SAAS,EAAE,kBAAkB,KAAK,UAAU,KAAK,OAAO,IAAI,EAAE,kBAAkB,IAAI;AAAA,EAClG;AACA,SAAO;AAAA,IACL,MAAM,IAAI;AAAA,IACV,MAAM,IAAI;AAAA,IACV,UAAU,IAAI;AAAA,IACd,MAAM,IAAI,QAAQ,IAAI;AAAA,IACtB,GAAI,KAAK,SAAS,EAAE,UAAU,KAAK,OAAO,IAAI,IAAI,WAAW,EAAE,UAAU,IAAI,SAAS,IAAI,CAAC;AAAA,IAC3F,GAAI,IAAI,OAAO,OAAO,EAAE,KAAK,IAAI,IAAI,IAAI,CAAC;AAAA,EAC5C;AACF;AAGA,SAAS,cAAc,MAAwC;AAC7D,QAAM,MAAO,KAAK,UAAU,CAAC;AAC7B,QAAM,WAAY,IAAI,OAA+B,IAAI;AACzD,MAAI,SAAU,QAAO;AACrB,QAAM,OAAQ,IAAI,QAA+B;AACjD,QAAM,OAAQ,IAAI,QAAwC;AAC1D,QAAM,KAAM,IAAI,YAAmC;AACnD,QAAM,OAAQ,IAAI,QAAgC,IAAI;AACtD,QAAM,OAAO,OAAO,GAAG,mBAAmB,IAAI,CAAC,IAAI,mBAAmB,KAAK,UAAU,EAAE,CAAC,MAAM;AAC9F,SAAO,aAAa,IAAI,GAAG,IAAI,IAAI,IAAI,IAAI,EAAE;AAC/C;AAOO,SAAS,uCAAiE;AAC/E,SAAO;AAAA,IACL,SAAS,UAA2B;AAClC,aAAO,YAAY,QAAQ,MAAM;AAAA,IACnC;AAAA,IAEA,MAAM,OAAO,MAAiE;AAC5E,YAAM,OAAO,YAAY,KAAK,MAAM;AACpC,UAAI,CAAC,MAAM;AACT,cAAM,IAAI,MAAM,0BAA0B,KAAK,MAAM,IAAI;AAAA,MAC3D;AAEA,YAAM,aAAc,KAAK,UAAkD,cACpE,KAAK,QAAgD;AAE5D,UAAI,SAAS,YAAY;AACvB,cAAM,EAAE,UAAU,IAAI,MAAM,OAAO,yBAAyB;AAC5D,cAAM,SAAS,IAAI,UAAU;AAAA,UAC3B,QAAQ;AAAA,UACR,YAAY,mBAAmB,MAAM,IAAI;AAAA,UACzC,MAAM,EAAE,KAAK,GAAG,KAAK,EAAE;AAAA,UACvB,GAAI,aAAa,EAAE,WAA8B,IAAI,CAAC;AAAA,QACxD,CAAQ;AACR,eAAO,SAAS,QAAQ,MAAM,iBAAiB,QAAQ,IAAI,CAAC;AAAA,MAC9D;AAEA,UAAI,SAAS,UAAU;AACrB,cAAM,EAAE,UAAU,IAAI,MAAM,OAAO,yBAAyB;AAC5D,cAAM,SAAS,IAAI,UAAU;AAAA,UAC3B,QAAQ;AAAA,UACR,YAAY,mBAAmB,MAAM,gBAAgB;AAAA,UACrD,kBAAkB;AAAA,UAClB,GAAI,aAAa,EAAE,WAA8B,IAAI,CAAC;AAAA,QACxD,CAAQ;AACR,eAAO,SAAS,QAAQ,MAAM,iBAAiB,QAAQ,QAAQ,CAAC;AAAA,MAClE;AAEA,UAAI,SAAS,WAAW;AACtB,YAAI;AACJ,YAAI;AACF,WAAC,EAAE,cAAc,IAAI,MAAM,OAAO,6BAAoC;AAAA,QACxE,SAAS,KAAU;AACjB,gBAAM,IAAI;AAAA,YACR,8EAA8E,KAAK,WAAW,GAAG;AAAA,UACnG;AAAA,QACF;AACA,cAAM,SAAS,IAAI,cAAc,EAAE,KAAK,cAAc,IAAI,EAAE,CAAC;AAC7D,eAAO,SAAS,MAAM;AAAA,MACxB;AAGA,YAAM,EAAE,eAAe,IAAI,MAAM,OAAO,4BAA4B;AACpE,aAAO,SAAS,IAAI,eAAe,CAAC;AAAA,IACtC;AAAA,EACF;AACF;AAGA,eAAe,iBAAiB,QAAa,QAAsD;AACjG,MAAI,OAAO,QAAQ,YAAY,WAAY,QAAO;AAClD,MAAI;AACF,UAAM,MAAM,WAAW,OAAO,0BAA0B;AACxD,UAAM,OAAY,MAAM,OAAO,QAAQ,GAAG;AAC1C,UAAM,QAAQ,MAAM,QAAQ,IAAI,IAAI,KAAK,CAAC,IAAI,MAAM,QAAQ,MAAM,IAAI,IAAI,KAAK,KAAK,CAAC,IAAI;AACzF,UAAM,IAAI,OAAO,KAAK,OAAO,WAAW,QAAQ,kBAAkB;AAClE,WAAO,OAAO,MAAM,WAAW,IAAI;AAAA,EACrC,QAAQ;AACN,WAAO;AAAA,EACT;AACF;;;ACpKA,IAAM,aAAa;AAkDZ,SAAS,iBAAiB,UAA0B;AACzD,SAAO,GAAG,UAAU,GAAG,QAAQ;AACjC;AAGO,SAAS,oBAAoB,KAAiC;AACnE,SAAO,KAAK,WAAW,UAAU,IAAI,IAAI,MAAM,WAAW,MAAM,IAAI;AACtE;AAMO,SAAS,6BAA6B,MAA0D;AACrG,QAAM,EAAE,QAAQ,eAAe,IAAI;AACnC,QAAM,mBAAmB,KAAK,aAAa;AAE3C,SAAO;AAAA,IACL,MAAM,KAAK,OAAO,MAAM;AACtB,YAAM,YAAY,MAAM,aAAa;AACrC,YAAM,MAAM,MAAM,OAAO,KAAK;AAC9B,YAAM,SAAuB,MAAM,eAAe,QAAQ,MAAM,OAAO,EAAE,WAAW,IAAI,CAAC;AACzF,YAAM,OAAO,OAAO,cAAc;AAAA,QAChC,IAAI,OAAO;AAAA,QACX;AAAA,QACA;AAAA,QACA,YAAY,OAAO;AAAA,QACnB,KAAK,OAAO;AAAA,QACZ,SAAS,OAAO;AAAA,QAChB,YAAY,OAAO;AAAA,MACrB,CAAC;AACD,aAAO,iBAAiB,OAAO,EAAE;AAAA,IACnC;AAAA,IAEA,MAAM,OAAO,gBAAgB;AAC3B,YAAM,KAAK,oBAAoB,cAAc;AAC7C,UAAI,CAAC,GAAI;AACT,YAAM,OAAO,OAAO,cAAc,EAAE,OAAO,EAAE,GAAG,EAAE,CAAC;AAAA,IACrD;AAAA,IAEA,MAAM,QAAQ,gBAAgB;AAC5B,YAAM,KAAK,oBAAoB,cAAc;AAC7C,UAAI,CAAC,MAAM,OAAO,OAAO,SAAS,WAAY,QAAO;AACrD,UAAI;AACF,cAAM,SAAS,MAAM,OAAO,KAAK,cAAc;AAAA,UAC7C,OAAO,EAAE,GAAG;AAAA,UACZ,OAAO;AAAA;AAAA;AAAA,UAGP,mBAAmB;AAAA,QACrB,CAAC;AACD,cAAM,QAAQ,MAAM,QAAQ,MAAM,IAAI,SAAU,QAAiC,SAAS,CAAC;AAC3F,cAAM,MAAM,KAAK,CAAC;AAClB,YAAI,CAAC,KAAK,WAAY,QAAO;AAG7B,eAAO,MAAM,eAAe;AAAA,UAC1B;AAAA,YACE,IAAI,IAAI;AAAA,YACR,UAAU,IAAI;AAAA,YACd,KAAK,IAAI;AAAA,YACT,SAAS,IAAI;AAAA,YACb,YAAY,IAAI;AAAA,UAClB;AAAA,UACA,EAAE,WAAW,IAAI,WAAW,KAAK,IAAI,IAAI;AAAA,QAC3C;AAAA,MACF,QAAQ;AAGN,eAAO;AAAA,MACT;AAAA,IACF;AAAA,EACF;AACF;;;ACxHO,SAAS,8BACd,QACA,KACA,WAAW,WACL;AACN,QAAM,OAAO,GAAG,QAAQ;AAExB,QAAM,eAAe,MAAW;AAC9B,QAAI;AACF,aAAO,IAAI,WAAgB,kBAAkB;AAAA,IAC/C,QAAQ;AACN,aAAO;AAAA,IACT;AAAA,EACF;AAEA,QAAM,cAAc,CAAC,QACnB,IAAI,OAAO,GAAG,EAAE,KAAK,EAAE,OAAO,+BAA+B,CAAC;AAEhE,QAAM,aAAa,CAAC,KAAU,QAC5B,IAAI,OAAO,GAAG,EAAE,KAAK,EAAE,OAAO,0BAA0B,SAAS,eAAe,QAAQ,IAAI,UAAU,OAAO,GAAG,EAAE,CAAC;AAGrH,QAAM,cAAc,CAAC,SAA2C;AAC9D,UAAM,EAAE,QAAQ,GAAG,MAAM,IAAK,QAAoC,CAAC;AAEnE,UAAM,aACJ,UAAU,OACN,SACA,OAAO,WAAW,WAChB,EAAE,OAAO,OAAO,IAChB;AACR,WAAO,EAAE,OAAO,QAAQ,WAAW;AAAA,EACrC;AAGA,SAAO,IAAI,MAAM,OAAO,MAAW,QAAa;AAC9C,UAAM,MAAM,aAAa;AACzB,QAAI,CAAC,KAAK,gBAAiB,QAAO,YAAY,GAAG;AACjD,UAAM,cAAc,MAAM,IAAI,gBAAgB;AAC9C,QAAI,KAAK,EAAE,YAAY,CAAC;AAAA,EAC1B,CAAC;AAID,SAAO,KAAK,GAAG,IAAI,SAAS,OAAO,KAAU,QAAa;AACxD,UAAM,MAAM,aAAa;AACzB,QAAI,CAAC,KAAK,eAAgB,QAAO,YAAY,GAAG;AAChD,UAAM,EAAE,OAAO,OAAO,IAAI,YAAY,IAAI,IAAI;AAC9C,QAAI;AACF,YAAM,SAAS,MAAM,IAAI,eAAe,OAAO,MAAM;AACrD,UAAI,KAAK,EAAE,OAAO,CAAC;AAAA,IACrB,SAAS,KAAK;AACZ,iBAAW,KAAK,GAAG;AAAA,IACrB;AAAA,EACF,CAAC;AAGD,SAAO,KAAK,MAAM,OAAO,KAAU,QAAa;AAC9C,UAAM,MAAM,aAAa;AACzB,QAAI,CAAC,KAAK,iBAAkB,QAAO,YAAY,GAAG;AAClD,UAAM,EAAE,OAAO,OAAO,IAAI,YAAY,IAAI,IAAI;AAC9C,QAAI;AACF,YAAM,aAAa,MAAM,IAAI,iBAAiB,OAAO,MAAM;AAC3D,UAAI,OAAO,GAAG,EAAE,KAAK,EAAE,WAAW,CAAC;AAAA,IACrC,SAAS,KAAK;AACZ,iBAAW,KAAK,GAAG;AAAA,IACrB;AAAA,EACF,CAAC;AAGD,SAAO,MAAM,GAAG,IAAI,UAAU,OAAO,KAAU,QAAa;AAC1D,UAAM,MAAM,aAAa;AACzB,QAAI,CAAC,KAAK,iBAAkB,QAAO,YAAY,GAAG;AAClD,UAAM,EAAE,OAAO,OAAO,IAAI,YAAY,IAAI,IAAI;AAC9C,QAAI;AACF,YAAM,aAAa,MAAM,IAAI,iBAAiB,IAAI,OAAO,MAAM,OAAO,MAAM;AAC5E,UAAI,KAAK,EAAE,WAAW,CAAC;AAAA,IACzB,SAAS,KAAK;AACZ,iBAAW,KAAK,GAAG;AAAA,IACrB;AAAA,EACF,CAAC;AAGD,SAAO,OAAO,GAAG,IAAI,UAAU,OAAO,KAAU,QAAa;AAC3D,UAAM,MAAM,aAAa;AACzB,QAAI,CAAC,KAAK,iBAAkB,QAAO,YAAY,GAAG;AAClD,QAAI;AACF,YAAM,IAAI,iBAAiB,IAAI,OAAO,IAAI;AAC1C,UAAI,OAAO,GAAG,EAAE,IAAI;AAAA,IACtB,SAAS,KAAK;AACZ,iBAAW,KAAK,GAAG;AAAA,IACrB;AAAA,EACF,CAAC;AACH;","names":["safeGetService"]}
package/package.json ADDED
@@ -0,0 +1,61 @@
1
+ {
2
+ "name": "@objectstack/service-datasource",
3
+ "version": "7.6.0",
4
+ "license": "Apache-2.0",
5
+ "description": "The datasource service (ADR-0015): external-table federation (introspect/draft/import/validate) + runtime UI datasource lifecycle (list/test/create/update/remove + REST routes). Open-source mechanism; the tier line falls on which ICryptoProvider / driver factory a host injects.",
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
+ "./contracts": {
16
+ "types": "./dist/contracts/index.d.ts",
17
+ "import": "./dist/contracts/index.js",
18
+ "require": "./dist/contracts/index.cjs"
19
+ }
20
+ },
21
+ "dependencies": {
22
+ "@objectstack/core": "7.6.0",
23
+ "@objectstack/spec": "7.6.0"
24
+ },
25
+ "devDependencies": {
26
+ "@types/node": "^25.9.1",
27
+ "tsup": "^8.5.1",
28
+ "typescript": "^6.0.3",
29
+ "vitest": "^4.1.8",
30
+ "@objectstack/driver-memory": "7.6.0",
31
+ "@objectstack/driver-sql": "7.6.0",
32
+ "@objectstack/plugin-hono-server": "7.6.0",
33
+ "@objectstack/driver-mongodb": "7.6.0"
34
+ },
35
+ "keywords": [
36
+ "objectstack",
37
+ "datasource",
38
+ "federation",
39
+ "introspection",
40
+ "datasource-admin",
41
+ "runtime-datasource"
42
+ ],
43
+ "author": "ObjectStack",
44
+ "repository": {
45
+ "type": "git",
46
+ "url": "https://github.com/objectstack-ai/framework.git",
47
+ "directory": "packages/services/service-datasource"
48
+ },
49
+ "homepage": "https://objectstack.ai/docs",
50
+ "bugs": "https://github.com/objectstack-ai/framework/issues",
51
+ "publishConfig": {
52
+ "access": "public"
53
+ },
54
+ "scripts": {
55
+ "build": "tsup",
56
+ "dev": "tsup --watch",
57
+ "test": "vitest run",
58
+ "test:watch": "vitest",
59
+ "typecheck": "tsc --noEmit"
60
+ }
61
+ }
@@ -0,0 +1,106 @@
1
+ // Copyright (c) 2026 ObjectStack. Licensed under the Apache-2.0 license.
2
+
3
+ import { describe, it, expect, vi } from 'vitest';
4
+ import { HonoHttpServer } from '@objectstack/plugin-hono-server';
5
+ import { registerDatasourceAdminRoutes } from '../admin-routes.js';
6
+
7
+ /**
8
+ * End-to-end routing test against the REAL `HonoHttpServer` adapter — the same
9
+ * IHttpServer implementation `os serve` mounts. We exercise the routes via
10
+ * `getRawApp().fetch(...)` (no socket bind needed), proving the wiring path the
11
+ * serve composition root relies on: routes mount on `IHttpServer` and dispatch
12
+ * to whatever object the plugin context resolves for the `datasource-admin`
13
+ * service.
14
+ */
15
+
16
+ const json = (path: string, init?: RequestInit) =>
17
+ new Request(`http://local${path}`, {
18
+ ...init,
19
+ headers: { 'content-type': 'application/json', ...(init?.headers ?? {}) },
20
+ });
21
+
22
+ function mount(svc: unknown) {
23
+ const server = new HonoHttpServer(0);
24
+ const ctx = { getService: vi.fn().mockReturnValue(svc) } as any;
25
+ registerDatasourceAdminRoutes(server, ctx, '/api/v1');
26
+ return server.getRawApp();
27
+ }
28
+
29
+ describe('registerDatasourceAdminRoutes (real HonoHttpServer)', () => {
30
+ it('GET /api/v1/datasources returns the service listing', async () => {
31
+ const listDatasources = vi.fn().mockResolvedValue([
32
+ { name: 'pg', origin: 'runtime', health: 'ok' },
33
+ ]);
34
+ const app = mount({ listDatasources });
35
+
36
+ const res = await app.fetch(json('/api/v1/datasources'));
37
+
38
+ expect(res.status).toBe(200);
39
+ expect(await res.json()).toEqual({
40
+ datasources: [{ name: 'pg', origin: 'runtime', health: 'ok' }],
41
+ });
42
+ expect(listDatasources).toHaveBeenCalledOnce();
43
+ });
44
+
45
+ it('POST /api/v1/datasources/test splits the inline secret out of the draft', async () => {
46
+ const testConnection = vi.fn().mockResolvedValue({ ok: true });
47
+ const app = mount({ testConnection });
48
+
49
+ const res = await app.fetch(
50
+ json('/api/v1/datasources/test', {
51
+ method: 'POST',
52
+ body: JSON.stringify({ name: 'pg', driver: 'postgres', secret: 's3cr3t' }),
53
+ }),
54
+ );
55
+
56
+ expect(res.status).toBe(200);
57
+ expect(await res.json()).toEqual({ result: { ok: true } });
58
+ // draft must NOT carry the secret; secret is normalised to { value }.
59
+ expect(testConnection).toHaveBeenCalledWith(
60
+ { name: 'pg', driver: 'postgres' },
61
+ { value: 's3cr3t' },
62
+ );
63
+ });
64
+
65
+ it('POST /api/v1/datasources creates a runtime datasource (201)', async () => {
66
+ const createDatasource = vi.fn().mockResolvedValue({ name: 'pg', origin: 'runtime' });
67
+ const app = mount({ createDatasource });
68
+
69
+ const res = await app.fetch(
70
+ json('/api/v1/datasources', {
71
+ method: 'POST',
72
+ body: JSON.stringify({ name: 'pg', driver: 'postgres' }),
73
+ }),
74
+ );
75
+
76
+ expect(res.status).toBe(201);
77
+ expect(await res.json()).toEqual({ datasource: { name: 'pg', origin: 'runtime' } });
78
+ });
79
+
80
+ it('degrades to 503 when the datasource-admin service is not wired', async () => {
81
+ const app = mount(undefined);
82
+
83
+ const res = await app.fetch(json('/api/v1/datasources'));
84
+
85
+ expect(res.status).toBe(503);
86
+ expect(await res.json()).toEqual({ error: 'datasource_admin_unavailable' });
87
+ });
88
+
89
+ it('surfaces lifecycle errors as 400 with the service message', async () => {
90
+ const createDatasource = vi.fn().mockRejectedValue(new Error('duplicate name'));
91
+ const app = mount({ createDatasource });
92
+
93
+ const res = await app.fetch(
94
+ json('/api/v1/datasources', {
95
+ method: 'POST',
96
+ body: JSON.stringify({ name: 'pg', driver: 'postgres' }),
97
+ }),
98
+ );
99
+
100
+ expect(res.status).toBe(400);
101
+ expect(await res.json()).toEqual({
102
+ error: 'datasource_admin_error',
103
+ message: 'duplicate name',
104
+ });
105
+ });
106
+ });
@@ -0,0 +1,231 @@
1
+ // Copyright (c) 2025 ObjectStack. Licensed under the Apache-2.0 license.
2
+
3
+ import { describe, it, expect } from 'vitest';
4
+ import type { IDatasourceAdminService, IDatasourceDriverFactory } from '../contracts/index.js';
5
+ import {
6
+ DatasourceAdminServicePlugin,
7
+ type DatasourceAdminServicePluginOptions,
8
+ } from '../datasource-admin-plugin.js';
9
+
10
+ /**
11
+ * Minimal PluginContext + in-memory metadata service. Boots the plugin and
12
+ * returns the registered `datasource-admin` service so we can exercise the
13
+ * plugin's glue (probe via factory, fail-closed secret) end to end.
14
+ */
15
+ async function boot(opts: DatasourceAdminServicePluginOptions & {
16
+ services?: Record<string, unknown>;
17
+ } = {}) {
18
+ const registry = new Map<string, Map<string, unknown>>();
19
+ const metadata = {
20
+ get: async (t: string, n: string) => registry.get(t)?.get(n),
21
+ list: async (t: string) => [...(registry.get(t)?.values() ?? [])],
22
+ register: async (t: string, n: string, d: unknown) => {
23
+ if (!registry.has(t)) registry.set(t, new Map());
24
+ registry.get(t)!.set(n, d);
25
+ },
26
+ unregister: async (t: string, n: string) => {
27
+ registry.get(t)?.delete(n);
28
+ },
29
+ listObjects: async () => [...(registry.get('object')?.values() ?? [])],
30
+ };
31
+
32
+ const services: Record<string, unknown> = { metadata, ...(opts.services ?? {}) };
33
+ let registered: IDatasourceAdminService | undefined;
34
+ const ctx: any = {
35
+ getService: (name: string) => {
36
+ if (name in services) return services[name];
37
+ throw new Error(`no service ${name}`);
38
+ },
39
+ registerService: (name: string, svc: unknown) => {
40
+ if (name === 'datasource-admin') registered = svc as IDatasourceAdminService;
41
+ },
42
+ trigger: async () => {},
43
+ logger: { warn() {}, info() {} },
44
+ };
45
+
46
+ const { services: _omit, ...pluginOpts } = opts;
47
+ const plugin = new DatasourceAdminServicePlugin(pluginOpts);
48
+ await plugin.init(ctx);
49
+ return { service: registered!, registry, metadata, plugin, ctx };
50
+ }
51
+
52
+ /** A driver factory whose handle records connect/ping/disconnect calls. */
53
+ function fakeFactory(over?: Partial<IDatasourceDriverFactory> & { onProbe?: () => void }): IDatasourceDriverFactory {
54
+ return {
55
+ supports: (id: string) => id === 'postgres',
56
+ create: async (spec) => ({
57
+ connect: async () => {},
58
+ ping: async () => {
59
+ over?.onProbe?.();
60
+ // expose the secret the factory received for assertions
61
+ (globalThis as any).__lastProbeSecret = spec.secret;
62
+ },
63
+ disconnect: async () => {},
64
+ serverVersion: async () => 'PostgreSQL 16.1',
65
+ }),
66
+ ...over,
67
+ };
68
+ }
69
+
70
+ describe('DatasourceAdminServicePlugin: probe', () => {
71
+ it('tests a connection through the driver factory (latency + version)', async () => {
72
+ const { service } = await boot({
73
+ driverFactory: fakeFactory(),
74
+ });
75
+ const res = await service.testConnection(
76
+ { name: 'reporting', driver: 'postgres', config: { host: 'db' } },
77
+ { value: 's3cret' },
78
+ );
79
+ expect(res.ok).toBe(true);
80
+ expect(res.serverVersion).toBe('PostgreSQL 16.1');
81
+ expect(typeof res.latencyMs).toBe('number');
82
+ expect((globalThis as any).__lastProbeSecret).toBe('s3cret');
83
+ });
84
+
85
+ it('returns ok:false when no factory supports the driver', async () => {
86
+ const { service } = await boot({ driverFactory: fakeFactory() });
87
+ const res = await service.testConnection({ name: 'x', driver: 'oracle', config: {} });
88
+ expect(res.ok).toBe(false);
89
+ expect(res.error).toMatch(/no driver factory supports/i);
90
+ });
91
+
92
+ it('returns ok:false when no factory is registered at all', async () => {
93
+ const { service } = await boot();
94
+ const res = await service.testConnection({ name: 'x', driver: 'postgres', config: {} });
95
+ expect(res.ok).toBe(false);
96
+ expect(res.error).toMatch(/no driver factory is registered/i);
97
+ });
98
+ });
99
+
100
+ describe('DatasourceAdminServicePlugin: secret fail-closed', () => {
101
+ it('refuses to create a secret-bearing datasource without a secret binder', async () => {
102
+ const { service, registry } = await boot({ driverFactory: fakeFactory() });
103
+ await expect(
104
+ service.createDatasource({ name: 'reporting', driver: 'postgres', config: {} }, { value: 'pw' }),
105
+ ).rejects.toThrow(/no secret store configured/i);
106
+ // nothing persisted
107
+ expect(registry.get('datasource')?.size ?? 0).toBe(0);
108
+ });
109
+
110
+ it('persists a credentialsRef (not cleartext) when a binder is wired', async () => {
111
+ const bound: string[] = [];
112
+ const { service, registry } = await boot({
113
+ driverFactory: fakeFactory(),
114
+ secrets: {
115
+ bind: async (input, hint) => {
116
+ bound.push(input.value);
117
+ return `sys_secret://datasource/${hint.name}#1`;
118
+ },
119
+ },
120
+ });
121
+ await service.createDatasource({ name: 'reporting', driver: 'postgres', config: {} }, { value: 'pw' });
122
+ const rec = registry.get('datasource')?.get('reporting') as any;
123
+ expect(rec.origin).toBe('runtime');
124
+ expect(rec.external?.credentialsRef).toBe('sys_secret://datasource/reporting#1');
125
+ expect(JSON.stringify(rec)).not.toContain('pw');
126
+ expect(bound).toEqual(['pw']);
127
+ });
128
+ });
129
+
130
+ describe('DatasourceAdminServicePlugin: boot rehydration', () => {
131
+ /** Fake engine ('data') that records hot-registered drivers. */
132
+ function fakeEngine() {
133
+ const drivers: any[] = [];
134
+ return {
135
+ drivers,
136
+ registerDriver: (d: any) => drivers.push(d),
137
+ registerDatasourceDef: () => {},
138
+ getDriverByName: (n: string) => drivers.find((d) => d.name === n),
139
+ };
140
+ }
141
+
142
+ /** Factory that records the spec (incl. resolved secret) of each create(). */
143
+ function recordingFactory() {
144
+ const specs: any[] = [];
145
+ const factory: IDatasourceDriverFactory = {
146
+ supports: (id: string) => id === 'postgres',
147
+ create: async (spec) => {
148
+ specs.push(spec);
149
+ return { connect: async () => {}, disconnect: async () => {} };
150
+ },
151
+ };
152
+ return { factory, specs };
153
+ }
154
+
155
+ it('rebuilds runtime pools at start(), decrypting the credentialsRef', async () => {
156
+ const engine = fakeEngine();
157
+ const { factory, specs } = recordingFactory();
158
+ const resolved: string[] = [];
159
+
160
+ const { plugin, ctx, registry } = await boot({
161
+ driverFactory: factory,
162
+ services: { data: engine },
163
+ secrets: {
164
+ bind: async () => 'sys_secret:abc',
165
+ resolve: async (ref) => {
166
+ resolved.push(ref);
167
+ return ref === 'sys_secret:abc' ? 'super-secret-pw' : undefined;
168
+ },
169
+ },
170
+ });
171
+
172
+ // Simulate a persisted (DB-backed) runtime datasource that survived a restart.
173
+ registry.set(
174
+ 'datasource',
175
+ new Map<string, unknown>([
176
+ ['crm_primary', { name: 'crm_primary', driver: 'sqlite', origin: 'code' }],
177
+ [
178
+ 'reporting',
179
+ {
180
+ name: 'reporting',
181
+ driver: 'postgres',
182
+ origin: 'runtime',
183
+ active: true,
184
+ config: { host: 'db' },
185
+ external: { credentialsRef: 'sys_secret:abc' },
186
+ },
187
+ ],
188
+ [
189
+ 'archived',
190
+ { name: 'archived', driver: 'postgres', origin: 'runtime', active: false },
191
+ ],
192
+ ]),
193
+ );
194
+
195
+ await plugin.start(ctx);
196
+
197
+ // Only the active runtime datasource is rehydrated — not the code one, not the inactive one.
198
+ expect(engine.drivers.map((d) => d.name)).toEqual(['reporting']);
199
+ // The credentialsRef was dereferenced and the cleartext handed to the factory.
200
+ expect(resolved).toEqual(['sys_secret:abc']);
201
+ expect(specs).toHaveLength(1);
202
+ expect(specs[0].secret).toBe('super-secret-pw');
203
+ expect(specs[0].name).toBe('reporting');
204
+ });
205
+
206
+ it('does not block boot when nothing is persisted (dev: in-memory store)', async () => {
207
+ const engine = fakeEngine();
208
+ const { factory } = recordingFactory();
209
+ const { plugin, ctx } = await boot({ driverFactory: factory, services: { data: engine } });
210
+ await expect(plugin.start(ctx)).resolves.toBeUndefined();
211
+ expect(engine.drivers).toHaveLength(0);
212
+ });
213
+ });
214
+
215
+ describe('DatasourceAdminServicePlugin: persistence + bound count', () => {
216
+ it('lists code (artefact) + runtime records with origin, blocks remove while bound', async () => {
217
+ const { service, registry } = await boot({ driverFactory: fakeFactory() });
218
+ // seed an artefact (code) datasource lacking explicit origin
219
+ registry.set('datasource', new Map([['crm_primary', { name: 'crm_primary', driver: 'sqlite' }]]));
220
+ // seed an object bound to a runtime datasource
221
+ registry.set('object', new Map([['lead', { name: 'lead', datasource: 'reporting' }]]));
222
+
223
+ await service.createDatasource({ name: 'reporting', driver: 'postgres', config: {} });
224
+
225
+ const list = await service.listDatasources();
226
+ expect(list.find((d) => d.name === 'crm_primary')?.origin).toBe('code');
227
+ expect(list.find((d) => d.name === 'reporting')?.origin).toBe('runtime');
228
+
229
+ await expect(service.removeDatasource('reporting')).rejects.toThrow(/1 object\(s\)/);
230
+ });
231
+ });