Skip to content
Draft
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
11 changes: 11 additions & 0 deletions src/cli/auto_resolver_adapters.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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,
Expand Down Expand Up @@ -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,
});
},
};
}

Expand Down
2 changes: 1 addition & 1 deletion src/cli/repo_context.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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,
Expand Down
158 changes: 134 additions & 24 deletions src/cli/resolve_datastore.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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<string, string> = {
"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]$/;
Expand All @@ -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<DatastoreConfig> {
const colonIdx = envValue.indexOf(":");
if (colonIdx === -1) {
throw new Error(
Expand All @@ -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<string, unknown> = { 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
Expand Down Expand Up @@ -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<DatastoreConfig> {
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<string, unknown> = {};
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.",
Expand All @@ -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.",
Expand All @@ -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.`,
);
}

Expand All @@ -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}`,
);
}
}
Expand All @@ -246,7 +356,7 @@ export function resolveDatastoreConfig(
const cachePath = provider.resolveCachePath?.(repoDir ?? ".");

return {
type: ds.type,
type: dsType,
config: customConfig,
datastorePath,
cachePath,
Expand Down
Loading
Loading