2025-08-29 02:00:10 +08:00
|
|
|
import { existsSync } from 'node:fs';
|
|
|
|
import { readFile } from 'node:fs/promises';
|
|
|
|
import { join, resolve } from 'node:path';
|
|
|
|
import yaml from 'js-yaml';
|
|
|
|
import {
|
|
|
|
type BusterConfig,
|
|
|
|
BusterConfigSchema,
|
|
|
|
type DeployOptions,
|
|
|
|
type ResolvedConfig,
|
|
|
|
ResolvedConfigSchema,
|
|
|
|
} from '../schemas';
|
|
|
|
|
|
|
|
/**
|
|
|
|
* Find and load buster.yml configuration file
|
|
|
|
* Searches in the given path and parent directories
|
2025-09-04 04:55:40 +08:00
|
|
|
* @throws Error if no buster.yml is found
|
2025-08-29 02:00:10 +08:00
|
|
|
*/
|
2025-09-04 04:55:40 +08:00
|
|
|
export async function loadBusterConfig(searchPath = '.'): Promise<BusterConfig> {
|
2025-08-29 02:00:10 +08:00
|
|
|
const absolutePath = resolve(searchPath);
|
|
|
|
let currentPath = absolutePath;
|
|
|
|
|
|
|
|
// Search for buster.yml in current and parent directories
|
|
|
|
while (currentPath !== '/') {
|
|
|
|
const configPath = join(currentPath, 'buster.yml');
|
|
|
|
|
|
|
|
if (existsSync(configPath)) {
|
2025-09-04 04:55:40 +08:00
|
|
|
const content = await readFile(configPath, 'utf-8');
|
|
|
|
const rawConfig = yaml.load(content) as unknown;
|
2025-08-29 02:00:10 +08:00
|
|
|
|
2025-09-04 04:55:40 +08:00
|
|
|
// Validate and parse with Zod schema
|
|
|
|
const result = BusterConfigSchema.safeParse(rawConfig);
|
2025-08-29 02:00:10 +08:00
|
|
|
|
2025-09-04 04:55:40 +08:00
|
|
|
if (result.success) {
|
|
|
|
return result.data;
|
2025-08-29 02:00:10 +08:00
|
|
|
}
|
2025-09-04 04:55:40 +08:00
|
|
|
throw new Error(`Invalid buster.yml at ${configPath}`);
|
2025-08-29 02:00:10 +08:00
|
|
|
}
|
|
|
|
|
|
|
|
// Move up one directory
|
|
|
|
const parentPath = join(currentPath, '..');
|
|
|
|
if (parentPath === currentPath) break; // Reached root
|
|
|
|
currentPath = parentPath;
|
|
|
|
}
|
|
|
|
|
2025-09-04 04:55:40 +08:00
|
|
|
throw new Error('No buster.yml found');
|
2025-08-29 02:00:10 +08:00
|
|
|
}
|
|
|
|
|
|
|
|
/**
|
|
|
|
* Resolve configuration hierarchy: CLI options > config file > defaults
|
|
|
|
* Returns a fully resolved configuration object
|
|
|
|
*/
|
|
|
|
export function resolveConfiguration(
|
2025-09-04 04:55:40 +08:00
|
|
|
config: BusterConfig,
|
|
|
|
_options: DeployOptions,
|
|
|
|
projectName?: string
|
2025-08-29 02:00:10 +08:00
|
|
|
): ResolvedConfig {
|
2025-09-04 04:55:40 +08:00
|
|
|
// Select project to use
|
|
|
|
const project = projectName
|
|
|
|
? config.projects.find((p) => p.name === projectName)
|
|
|
|
: config.projects[0];
|
2025-08-29 02:00:10 +08:00
|
|
|
|
2025-09-04 04:55:40 +08:00
|
|
|
if (!project) {
|
|
|
|
throw new Error(
|
|
|
|
projectName
|
|
|
|
? `Project '${projectName}' not found in buster.yml`
|
|
|
|
: 'No projects defined in buster.yml'
|
|
|
|
);
|
2025-08-29 02:00:10 +08:00
|
|
|
}
|
|
|
|
|
2025-09-04 04:55:40 +08:00
|
|
|
// Build resolved config from project
|
|
|
|
const resolved: ResolvedConfig = {
|
|
|
|
data_source_name: project.data_source,
|
|
|
|
database: project.database,
|
|
|
|
schema: project.schema,
|
|
|
|
include: project.include,
|
|
|
|
exclude: project.exclude,
|
|
|
|
};
|
2025-08-29 02:00:10 +08:00
|
|
|
|
|
|
|
// Validate resolved config
|
|
|
|
const result = ResolvedConfigSchema.parse(resolved);
|
|
|
|
return result;
|
|
|
|
}
|
|
|
|
|
|
|
|
/**
|
|
|
|
* Get the base directory for a buster.yml file
|
|
|
|
* Used for resolving relative paths in the config
|
|
|
|
*/
|
|
|
|
export function getConfigBaseDir(configPath: string): string {
|
|
|
|
// If configPath is a directory, use it directly
|
|
|
|
// Otherwise, use its parent directory
|
2025-08-29 02:00:20 +08:00
|
|
|
if (existsSync(configPath) && require('node:fs').statSync(configPath).isDirectory()) {
|
2025-08-29 02:00:10 +08:00
|
|
|
return resolve(configPath);
|
|
|
|
}
|
|
|
|
return resolve(join(configPath, '..'));
|
|
|
|
}
|
|
|
|
|
|
|
|
/**
|
|
|
|
* Resolve model paths relative to config base directory
|
|
|
|
*/
|
|
|
|
export function resolveModelPaths(modelPaths: string[], baseDir: string): string[] {
|
|
|
|
return modelPaths.map((path) => {
|
|
|
|
// If path is absolute, use it directly
|
|
|
|
if (path.startsWith('/')) {
|
|
|
|
return path;
|
|
|
|
}
|
|
|
|
// Otherwise, resolve relative to base directory
|
|
|
|
return resolve(baseDir, path);
|
|
|
|
});
|
|
|
|
}
|