diff --git a/src/cli/auto_resolver_adapters.ts b/src/cli/auto_resolver_adapters.ts index abf28f84..e927259e 100644 --- a/src/cli/auto_resolver_adapters.ts +++ b/src/cli/auto_resolver_adapters.ts @@ -30,6 +30,7 @@ import { } from "../libswamp/mod.ts"; import { UserModelLoader } from "../domain/models/user_model_loader.ts"; import { UserVaultLoader } from "../domain/vaults/user_vault_loader.ts"; +import { UserDatastoreLoader } from "../domain/datastore/user_datastore_loader.ts"; import type { OutputMode } from "../presentation/output/output.ts"; import { renderAutoResolveInstalled, @@ -125,6 +126,16 @@ export function createAutoResolveInstallerAdapter( skipAlreadyRegistered: true, }); }, + + async hotLoadDatastores() { + const absoluteDatastoresDir = isAbsolute(datastoresDir) + ? datastoresDir + : resolve(repoDir, datastoresDir); + const loader = new UserDatastoreLoader(denoRuntime, repoDir); + await loader.loadDatastores(absoluteDatastoresDir, { + skipAlreadyRegistered: true, + }); + }, }; } diff --git a/src/cli/repo_context.ts b/src/cli/repo_context.ts index dd754f2a..b64e79bb 100644 --- a/src/cli/repo_context.ts +++ b/src/cli/repo_context.ts @@ -133,7 +133,7 @@ export async function resolveDatastoreForRepo( const markerRepo = new RepoMarkerRepository(); const marker = await markerRepo.read(repoPath); - const datastoreConfig = resolveDatastoreConfig( + const datastoreConfig = await resolveDatastoreConfig( marker, undefined, repoPath.value, diff --git a/src/cli/resolve_datastore.ts b/src/cli/resolve_datastore.ts index 5459eac4..a9915369 100644 --- a/src/cli/resolve_datastore.ts +++ b/src/cli/resolve_datastore.ts @@ -30,12 +30,25 @@ */ import { join } from "@std/path"; +import { getLogger } from "@logtape/logtape"; import type { RepoMarkerData } from "../infrastructure/persistence/repo_marker_repository.ts"; import type { DatastoreConfig } from "../domain/datastore/datastore_config.ts"; import { getSwampDataDir } from "../infrastructure/persistence/paths.ts"; import { expandEnvVars } from "../infrastructure/persistence/env_path.ts"; import { datastoreTypeRegistry } from "../domain/datastore/datastore_type_registry.ts"; import { UserError } from "../domain/errors.ts"; +import { resolveDatastoreType } from "../domain/extensions/extension_auto_resolver.ts"; +import { getAutoResolver } from "../domain/extensions/auto_resolver_context.ts"; + +const logger = getLogger(["swamp", "datastore", "resolve"]); + +/** + * Maps old built-in datastore type names to their extension replacements. + * Applied when loading datastore configs from .swamp.yaml or env vars. + */ +export const RENAMED_DATASTORE_TYPES: Record = { + "s3": "@swamp/s3-datastore", +}; /** S3 bucket naming rules: 3-63 chars, lowercase alphanumeric, hyphens, dots. */ const S3_BUCKET_NAME_RE = /^[a-z0-9][a-z0-9.\-]{1,61}[a-z0-9]$/; @@ -57,11 +70,11 @@ function validateBucketName(bucket: string): void { * @param repoDir - The repository root directory (for custom datastore path resolution) * @returns Parsed DatastoreConfig */ -export function parseDatastoreEnvVar( +export async function parseDatastoreEnvVar( envValue: string, repoId?: string, repoDir?: string, -): DatastoreConfig { +): Promise { const colonIdx = envValue.indexOf(":"); if (colonIdx === -1) { throw new Error( @@ -70,24 +83,63 @@ export function parseDatastoreEnvVar( ); } - const type = envValue.slice(0, colonIdx); + let type = envValue.slice(0, colonIdx); const value = envValue.slice(colonIdx + 1); if (type === "filesystem") { return { type: "filesystem", path: expandEnvVars(value) }; } - if (type === "s3") { - const slashIdx = value.indexOf("/"); - const bucket = slashIdx === -1 ? value : value.slice(0, slashIdx); - validateBucketName(bucket); - const prefix = slashIdx === -1 ? undefined : value.slice(slashIdx + 1); - const cachePath = join( - getSwampDataDir(), - "repos", - repoId ?? "unknown", + // Remap renamed types (e.g., "s3" → "@swamp/s3-datastore") + const renamedTo = RENAMED_DATASTORE_TYPES[type]; + if (renamedTo) { + logger.warn( + `Datastore type '${type}' has been renamed to '${renamedTo}'. ` + + `Update your SWAMP_DATASTORE env var to use the new name.`, ); - return { type: "s3", bucket, prefix, cachePath }; + + // Parse "s3:bucket/prefix" shorthand into config JSON + if (type === "s3") { + const slashIdx = value.indexOf("/"); + const bucket = slashIdx === -1 ? value : value.slice(0, slashIdx); + validateBucketName(bucket); + const prefix = slashIdx === -1 ? undefined : value.slice(slashIdx + 1); + + // Auto-resolve the extension if not already loaded + await resolveDatastoreType(renamedTo, getAutoResolver()); + + const typeInfo = datastoreTypeRegistry.get(renamedTo); + if (typeInfo?.createProvider) { + const config: Record = { bucket }; + if (prefix) config.prefix = prefix; + const provider = typeInfo.createProvider(config); + const resolvedRepoDir = repoDir ?? "."; + const datastorePath = provider.resolveDatastorePath(resolvedRepoDir); + const cachePath = provider.resolveCachePath?.(resolvedRepoDir) ?? + join(getSwampDataDir(), "repos", repoId ?? "unknown"); + return { + type: renamedTo, + config, + datastorePath, + cachePath, + }; + } + + // Fallback to built-in S3 config if extension not available + const cachePath = join( + getSwampDataDir(), + "repos", + repoId ?? "unknown", + ); + return { type: "s3", bucket, prefix, cachePath }; + } + + type = renamedTo; + } + + // Auto-resolve extension types + if (type.startsWith("@")) { + await resolveDatastoreType(type, getAutoResolver()); } // Custom datastore type: value is JSON config @@ -154,28 +206,81 @@ export function parseDatastoreEnvVar( * @param repoDir - The repository root directory * @returns Resolved DatastoreConfig */ -export function resolveDatastoreConfig( +export async function resolveDatastoreConfig( marker: RepoMarkerData | null, cliArg?: string, repoDir?: string, -): DatastoreConfig { +): Promise { const repoId = marker?.repoId; // 1. Environment variable takes highest priority const envDatastore = Deno.env.get("SWAMP_DATASTORE"); if (envDatastore) { - return parseDatastoreEnvVar(envDatastore, repoId, repoDir); + return await parseDatastoreEnvVar(envDatastore, repoId, repoDir); } // 2. CLI argument if (cliArg) { - return parseDatastoreEnvVar(cliArg, repoId, repoDir); + return await parseDatastoreEnvVar(cliArg, repoId, repoDir); } // 3. .swamp.yaml datastore config if (marker?.datastore) { const ds = marker.datastore; - if (ds.type === "s3") { + let dsType = ds.type; + + // Remap renamed types (e.g., "s3" → "@swamp/s3-datastore") + const renamedTo = RENAMED_DATASTORE_TYPES[dsType]; + if (renamedTo) { + logger.warn( + `Datastore type '${dsType}' has been renamed to '${renamedTo}'. ` + + `Update your .swamp.yaml to use the new name.`, + ); + + // Auto-resolve the extension if not already loaded + await resolveDatastoreType(renamedTo, getAutoResolver()); + + const typeInfo = datastoreTypeRegistry.get(renamedTo); + if (typeInfo?.createProvider) { + // Build config from the S3-specific YAML fields + const config: Record = {}; + if (ds.bucket) config.bucket = ds.bucket; + if (ds.prefix) config.prefix = ds.prefix; + if (ds.region) config.region = ds.region; + if (ds.endpoint) config.endpoint = ds.endpoint; + if (ds.forcePathStyle != null) { + config.forcePathStyle = ds.forcePathStyle; + } + + if (typeInfo.configSchema) { + const result = typeInfo.configSchema.safeParse(config); + if (!result.success) { + throw new UserError( + `Invalid config for datastore type "${renamedTo}": ${result.error.message}`, + ); + } + } + + const provider = typeInfo.createProvider(config); + const datastorePath = provider.resolveDatastorePath(repoDir ?? "."); + const cachePath = provider.resolveCachePath?.(repoDir ?? ".") ?? + join(getSwampDataDir(), "repos", repoId ?? "unknown"); + + return { + type: renamedTo, + config, + datastorePath, + cachePath, + directories: ds.directories, + exclude: ds.exclude, + }; + } + + // Fallback to built-in S3 handling if extension not available + dsType = ds.type; + } + + if (dsType === "s3") { if (!ds.bucket) { throw new Error( "S3 datastore config in .swamp.yaml requires a 'bucket' field.", @@ -200,7 +305,7 @@ export function resolveDatastoreConfig( }; } - if (ds.type === "filesystem") { + if (dsType === "filesystem") { if (!ds.path) { throw new Error( "Filesystem datastore config in .swamp.yaml requires a 'path' field.", @@ -214,19 +319,24 @@ export function resolveDatastoreConfig( }; } + // Auto-resolve extension types + if (dsType.startsWith("@")) { + await resolveDatastoreType(dsType, getAutoResolver()); + } + // Custom datastore type from YAML config - const typeInfo = datastoreTypeRegistry.get(ds.type); + const typeInfo = datastoreTypeRegistry.get(dsType); if (!typeInfo) { const available = datastoreTypeRegistry.getAll().map((t) => t.type).join( ", ", ); throw new UserError( - `Unknown datastore type "${ds.type}" in .swamp.yaml. Available types: ${available}`, + `Unknown datastore type "${dsType}" in .swamp.yaml. Available types: ${available}`, ); } if (!typeInfo.createProvider) { throw new UserError( - `Datastore type "${ds.type}" is registered but has no provider.`, + `Datastore type "${dsType}" is registered but has no provider.`, ); } @@ -236,7 +346,7 @@ export function resolveDatastoreConfig( const result = typeInfo.configSchema.safeParse(customConfig); if (!result.success) { throw new UserError( - `Invalid config for datastore type "${ds.type}": ${result.error.message}`, + `Invalid config for datastore type "${dsType}": ${result.error.message}`, ); } } @@ -246,7 +356,7 @@ export function resolveDatastoreConfig( const cachePath = provider.resolveCachePath?.(repoDir ?? "."); return { - type: ds.type, + type: dsType, config: customConfig, datastorePath, cachePath, diff --git a/src/cli/resolve_datastore_test.ts b/src/cli/resolve_datastore_test.ts index 38b7f3a3..4ca42e68 100644 --- a/src/cli/resolve_datastore_test.ts +++ b/src/cli/resolve_datastore_test.ts @@ -17,9 +17,10 @@ // You should have received a copy of the GNU Affero General Public License // along with Swamp. If not, see . -import { assertEquals, assertThrows } from "@std/assert"; +import { assertEquals, assertRejects, assertThrows } from "@std/assert"; import { parseDatastoreEnvVar, + RENAMED_DATASTORE_TYPES, resolveDatastoreConfig, } from "./resolve_datastore.ts"; import { @@ -77,16 +78,20 @@ function ensureTestType( } } -Deno.test("parseDatastoreEnvVar - parses filesystem path", () => { - const config = parseDatastoreEnvVar("filesystem:/data/my-project"); +Deno.test("parseDatastoreEnvVar: parses filesystem path", async () => { + const config = await parseDatastoreEnvVar("filesystem:/data/my-project"); assertEquals(config.type, "filesystem"); if (!isCustomDatastoreConfig(config) && config.type === "filesystem") { assertEquals(config.path, "/data/my-project"); } }); -Deno.test("parseDatastoreEnvVar - parses s3 bucket with prefix", () => { - const config = parseDatastoreEnvVar("s3:my-bucket/my-prefix", "test-repo"); +Deno.test("parseDatastoreEnvVar: parses s3 bucket with prefix", async () => { + const config = await parseDatastoreEnvVar( + "s3:my-bucket/my-prefix", + "test-repo", + ); + // Falls back to built-in S3 config since extension isn't installed in tests assertEquals(config.type, "s3"); if (!isCustomDatastoreConfig(config) && config.type === "s3") { assertEquals(config.bucket, "my-bucket"); @@ -94,8 +99,8 @@ Deno.test("parseDatastoreEnvVar - parses s3 bucket with prefix", () => { } }); -Deno.test("parseDatastoreEnvVar - parses s3 bucket without prefix", () => { - const config = parseDatastoreEnvVar("s3:my-bucket", "test-repo"); +Deno.test("parseDatastoreEnvVar: parses s3 bucket without prefix", async () => { + const config = await parseDatastoreEnvVar("s3:my-bucket", "test-repo"); assertEquals(config.type, "s3"); if (!isCustomDatastoreConfig(config) && config.type === "s3") { assertEquals(config.bucket, "my-bucket"); @@ -103,43 +108,43 @@ Deno.test("parseDatastoreEnvVar - parses s3 bucket without prefix", () => { } }); -Deno.test("parseDatastoreEnvVar - throws on invalid format", () => { - assertThrows( +Deno.test("parseDatastoreEnvVar: throws on invalid format", async () => { + await assertRejects( () => parseDatastoreEnvVar("invalid"), Error, "Invalid SWAMP_DATASTORE format", ); }); -Deno.test("parseDatastoreEnvVar - throws on unknown type", () => { - assertThrows( +Deno.test("parseDatastoreEnvVar: throws on unknown type", async () => { + await assertRejects( () => parseDatastoreEnvVar("gcs:bucket"), Error, "Unknown datastore type", ); }); -Deno.test("parseDatastoreEnvVar - throws on invalid S3 bucket name", () => { - assertThrows( +Deno.test("parseDatastoreEnvVar: throws on invalid S3 bucket name", async () => { + await assertRejects( () => parseDatastoreEnvVar("s3:INVALID_BUCKET"), Error, "Invalid S3 bucket name", ); }); -Deno.test("resolveDatastoreConfig - default is filesystem at .swamp/", () => { - const config = resolveDatastoreConfig(null, undefined, "/repo"); +Deno.test("resolveDatastoreConfig: default is filesystem at .swamp/", async () => { + const config = await resolveDatastoreConfig(null, undefined, "/repo"); assertEquals(config.type, "filesystem"); if (!isCustomDatastoreConfig(config) && config.type === "filesystem") { assertEquals(config.path, "/repo/.swamp"); } }); -Deno.test("resolveDatastoreConfig - env var takes priority", () => { +Deno.test("resolveDatastoreConfig: env var takes priority", async () => { const originalEnv = Deno.env.get("SWAMP_DATASTORE"); try { Deno.env.set("SWAMP_DATASTORE", "filesystem:/custom/path"); - const config = resolveDatastoreConfig(null, undefined, "/repo"); + const config = await resolveDatastoreConfig(null, undefined, "/repo"); assertEquals(config.type, "filesystem"); if (!isCustomDatastoreConfig(config) && config.type === "filesystem") { assertEquals(config.path, "/custom/path"); @@ -153,14 +158,14 @@ Deno.test("resolveDatastoreConfig - env var takes priority", () => { } }); -Deno.test("resolveDatastoreConfig - CLI arg overrides marker", () => { +Deno.test("resolveDatastoreConfig: CLI arg overrides marker", async () => { const marker: RepoMarkerData = { swampVersion: "0.1.0", initializedAt: "2024-01-01", repoId: "test-repo", datastore: { type: "filesystem", path: "/marker-path" }, }; - const config = resolveDatastoreConfig( + const config = await resolveDatastoreConfig( marker, "filesystem:/cli-path", "/repo", @@ -171,7 +176,7 @@ Deno.test("resolveDatastoreConfig - CLI arg overrides marker", () => { } }); -Deno.test("resolveDatastoreConfig - marker config used when no env/cli", () => { +Deno.test("resolveDatastoreConfig: marker config used when no env/cli", async () => { const marker: RepoMarkerData = { swampVersion: "0.1.0", initializedAt: "2024-01-01", @@ -182,7 +187,7 @@ Deno.test("resolveDatastoreConfig - marker config used when no env/cli", () => { directories: ["data", "outputs"], }, }; - const config = resolveDatastoreConfig(marker, undefined, "/repo"); + const config = await resolveDatastoreConfig(marker, undefined, "/repo"); assertEquals(config.type, "filesystem"); if (!isCustomDatastoreConfig(config) && config.type === "filesystem") { assertEquals(config.path, "/marker-path"); @@ -194,7 +199,7 @@ Deno.test("resolveDatastoreConfig - marker config used when no env/cli", () => { // S3 endpoint / forcePathStyle tests // ============================================================================ -Deno.test("resolveDatastoreConfig - S3 marker with endpoint and forcePathStyle", () => { +Deno.test("resolveDatastoreConfig: S3 marker with endpoint and forcePathStyle", async () => { const marker: RepoMarkerData = { swampVersion: "0.1.0", initializedAt: "2024-01-01", @@ -207,7 +212,8 @@ Deno.test("resolveDatastoreConfig - S3 marker with endpoint and forcePathStyle", forcePathStyle: false, }, }; - const config = resolveDatastoreConfig(marker, undefined, "/repo"); + const config = await resolveDatastoreConfig(marker, undefined, "/repo"); + // Falls back to built-in S3 since extension isn't installed in tests assertEquals(config.type, "s3"); if (!isCustomDatastoreConfig(config) && config.type === "s3") { assertEquals(config.bucket, "my-space"); @@ -217,7 +223,7 @@ Deno.test("resolveDatastoreConfig - S3 marker with endpoint and forcePathStyle", } }); -Deno.test("resolveDatastoreConfig - S3 marker without endpoint defaults to undefined", () => { +Deno.test("resolveDatastoreConfig: S3 marker without endpoint defaults to undefined", async () => { const marker: RepoMarkerData = { swampVersion: "0.1.0", initializedAt: "2024-01-01", @@ -228,7 +234,7 @@ Deno.test("resolveDatastoreConfig - S3 marker without endpoint defaults to undef region: "us-west-2", }, }; - const config = resolveDatastoreConfig(marker, undefined, "/repo"); + const config = await resolveDatastoreConfig(marker, undefined, "/repo"); assertEquals(config.type, "s3"); if (!isCustomDatastoreConfig(config) && config.type === "s3") { assertEquals(config.bucket, "my-bucket"); @@ -241,9 +247,9 @@ Deno.test("resolveDatastoreConfig - S3 marker without endpoint defaults to undef // Custom datastore type tests // ============================================================================ -Deno.test("parseDatastoreEnvVar - parses custom type with JSON config", () => { +Deno.test("parseDatastoreEnvVar: parses custom type with JSON config", async () => { ensureTestType("test-custom-env"); - const config = parseDatastoreEnvVar( + const config = await parseDatastoreEnvVar( 'test-custom-env:{"region":"us-east-1"}', "repo-1", "/my/repo", @@ -256,9 +262,9 @@ Deno.test("parseDatastoreEnvVar - parses custom type with JSON config", () => { assertEquals(custom.cachePath, "/my/repo/.custom-cache"); }); -Deno.test("parseDatastoreEnvVar - parses custom type with empty config", () => { +Deno.test("parseDatastoreEnvVar: parses custom type with empty config", async () => { ensureTestType("test-custom-empty"); - const config = parseDatastoreEnvVar( + const config = await parseDatastoreEnvVar( "test-custom-empty:", "repo-1", "/my/repo", @@ -269,9 +275,9 @@ Deno.test("parseDatastoreEnvVar - parses custom type with empty config", () => { assertEquals(custom.config, {}); }); -Deno.test("parseDatastoreEnvVar - custom type uses repoDir not repoId for path resolution", () => { +Deno.test("parseDatastoreEnvVar: custom type uses repoDir not repoId for path resolution", async () => { ensureTestType("test-custom-path"); - const config = parseDatastoreEnvVar( + const config = await parseDatastoreEnvVar( "test-custom-path:{}", "some-repo-id", "/actual/repo/dir", @@ -281,26 +287,26 @@ Deno.test("parseDatastoreEnvVar - custom type uses repoDir not repoId for path r assertEquals(custom.datastorePath, "/actual/repo/dir/.custom-store"); }); -Deno.test("parseDatastoreEnvVar - custom type throws on invalid JSON", () => { +Deno.test("parseDatastoreEnvVar: custom type throws on invalid JSON", async () => { ensureTestType("test-custom-badjson"); - assertThrows( + await assertRejects( () => parseDatastoreEnvVar("test-custom-badjson:not-json", "r", "/repo"), Error, "Invalid JSON config", ); }); -Deno.test("parseDatastoreEnvVar - custom type validates config schema", () => { +Deno.test("parseDatastoreEnvVar: custom type validates config schema", async () => { const schema = z.object({ endpoint: z.string() }); ensureTestType("test-custom-schema", { configSchema: schema }); - assertThrows( + await assertRejects( () => parseDatastoreEnvVar("test-custom-schema:{}", "r", "/repo"), Error, "Invalid config for datastore type", ); }); -Deno.test("resolveDatastoreConfig - YAML custom type produces CustomDatastoreConfig", () => { +Deno.test("resolveDatastoreConfig: YAML custom type produces CustomDatastoreConfig", async () => { ensureTestType("test-custom-yaml"); const marker: RepoMarkerData = { swampVersion: "0.1.0", @@ -312,7 +318,7 @@ Deno.test("resolveDatastoreConfig - YAML custom type produces CustomDatastoreCon directories: ["data"], }, }; - const config = resolveDatastoreConfig(marker, undefined, "/repo"); + const config = await resolveDatastoreConfig(marker, undefined, "/repo"); assertEquals(config.type, "test-custom-yaml"); assertEquals(isCustomDatastoreConfig(config), true); const custom = config as CustomDatastoreConfig; @@ -322,21 +328,21 @@ Deno.test("resolveDatastoreConfig - YAML custom type produces CustomDatastoreCon assertEquals(custom.directories, ["data"]); }); -Deno.test("resolveDatastoreConfig - YAML unknown type throws UserError", () => { +Deno.test("resolveDatastoreConfig: YAML unknown type throws UserError", async () => { const marker: RepoMarkerData = { swampVersion: "0.1.0", initializedAt: "2024-01-01", repoId: "test-repo", datastore: { type: "nonexistent-type" }, }; - assertThrows( + await assertRejects( () => resolveDatastoreConfig(marker, undefined, "/repo"), Error, "Unknown datastore type", ); }); -Deno.test("resolveDatastoreConfig - YAML custom type with no config defaults to empty object", () => { +Deno.test("resolveDatastoreConfig: YAML custom type with no config defaults to empty object", async () => { ensureTestType("test-custom-noconfig"); const marker: RepoMarkerData = { swampVersion: "0.1.0", @@ -344,19 +350,31 @@ Deno.test("resolveDatastoreConfig - YAML custom type with no config defaults to repoId: "test-repo", datastore: { type: "test-custom-noconfig" }, }; - const config = resolveDatastoreConfig(marker, undefined, "/repo"); + const config = await resolveDatastoreConfig(marker, undefined, "/repo"); const custom = config as CustomDatastoreConfig; assertEquals(custom.config, {}); }); -Deno.test("isCustomDatastoreConfig - returns false for filesystem", () => { +// ============================================================================ +// Renamed datastore type tests +// ============================================================================ + +Deno.test("RENAMED_DATASTORE_TYPES: maps s3 to @swamp/s3-datastore", () => { + assertEquals(RENAMED_DATASTORE_TYPES["s3"], "@swamp/s3-datastore"); +}); + +// ============================================================================ +// Type guard tests +// ============================================================================ + +Deno.test("isCustomDatastoreConfig: returns false for filesystem", () => { assertEquals( isCustomDatastoreConfig({ type: "filesystem", path: "/tmp" }), false, ); }); -Deno.test("isCustomDatastoreConfig - returns false for s3", () => { +Deno.test("isCustomDatastoreConfig: returns false for s3", () => { assertEquals( isCustomDatastoreConfig({ type: "s3", @@ -367,7 +385,7 @@ Deno.test("isCustomDatastoreConfig - returns false for s3", () => { ); }); -Deno.test("isCustomDatastoreConfig - returns true for custom type", () => { +Deno.test("isCustomDatastoreConfig: returns true for custom type", () => { assertEquals( isCustomDatastoreConfig({ type: "my-custom-store", diff --git a/src/domain/extensions/extension_auto_resolver.ts b/src/domain/extensions/extension_auto_resolver.ts index 0c74ca8c..db12d182 100644 --- a/src/domain/extensions/extension_auto_resolver.ts +++ b/src/domain/extensions/extension_auto_resolver.ts @@ -21,6 +21,7 @@ import { getLogger } from "@logtape/logtape"; import { ModelType } from "../models/model_type.ts"; import { type ModelDefinition, modelRegistry } from "../models/model.ts"; import { vaultTypeRegistry } from "../vaults/vault_type_registry.ts"; +import { datastoreTypeRegistry } from "../datastore/datastore_type_registry.ts"; const logger = getLogger(["swamp", "extensions", "auto-resolver"]); @@ -68,6 +69,7 @@ export interface ExtensionInstallerPort { install(extensionName: string): Promise; hotLoadModels(): Promise; hotLoadVaults(): Promise; + hotLoadDatastores(): Promise; } /** @@ -312,9 +314,10 @@ export class ExtensionAutoResolver { if (!installResult) return false; - // Hot-load newly installed models and vaults + // Hot-load newly installed models, vaults, and datastores const newModelsCount = await extensionInstaller.hotLoadModels(); await extensionInstaller.hotLoadVaults(); + await extensionInstaller.hotLoadDatastores(); output.installed(extensionName, installResult.version, newModelsCount); @@ -379,3 +382,20 @@ export async function resolveVaultType( return await resolver.resolve(type); } + +/** + * Standalone helper function for resolving datastore types at choke points. + * + * Checks the datastore type registry first (sync fast path), then falls back + * to auto-resolution if a resolver is available. + */ +export async function resolveDatastoreType( + type: string, + resolver: ExtensionAutoResolver | null, +): Promise { + if (datastoreTypeRegistry.has(type)) return true; + if (!resolver) return false; + if (!type.startsWith("@")) return false; + + return await resolver.resolve(type); +} diff --git a/src/domain/extensions/extension_auto_resolver_test.ts b/src/domain/extensions/extension_auto_resolver_test.ts index 80f675aa..5696da6a 100644 --- a/src/domain/extensions/extension_auto_resolver_test.ts +++ b/src/domain/extensions/extension_auto_resolver_test.ts @@ -99,6 +99,9 @@ function createMockInstaller( hotLoadVaults() { return Promise.resolve(); }, + hotLoadDatastores() { + return Promise.resolve(); + }, }; }