feat(config): add MosaicConfig schema + loader with tier auto-detection
Some checks failed
ci/woodpecker/push/ci Pipeline failed
Some checks failed
ci/woodpecker/push/ci Pipeline failed
Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
This commit is contained in:
35
packages/config/package.json
Normal file
35
packages/config/package.json
Normal file
@@ -0,0 +1,35 @@
|
||||
{
|
||||
"name": "@mosaic/config",
|
||||
"version": "0.0.1",
|
||||
"type": "module",
|
||||
"main": "dist/index.js",
|
||||
"types": "dist/index.d.ts",
|
||||
"exports": {
|
||||
".": {
|
||||
"types": "./dist/index.d.ts",
|
||||
"default": "./dist/index.js"
|
||||
}
|
||||
},
|
||||
"scripts": {
|
||||
"build": "tsc",
|
||||
"lint": "eslint src",
|
||||
"typecheck": "tsc --noEmit",
|
||||
"test": "vitest run --passWithNoTests"
|
||||
},
|
||||
"dependencies": {
|
||||
"@mosaic/memory": "workspace:^",
|
||||
"@mosaic/queue": "workspace:^",
|
||||
"@mosaic/storage": "workspace:^"
|
||||
},
|
||||
"devDependencies": {
|
||||
"typescript": "^5.8.0",
|
||||
"vitest": "^2.0.0"
|
||||
},
|
||||
"publishConfig": {
|
||||
"registry": "https://git.mosaicstack.dev/api/packages/mosaic/npm/",
|
||||
"access": "public"
|
||||
},
|
||||
"files": [
|
||||
"dist"
|
||||
]
|
||||
}
|
||||
7
packages/config/src/index.ts
Normal file
7
packages/config/src/index.ts
Normal file
@@ -0,0 +1,7 @@
|
||||
export type { MosaicConfig, StorageTier, MemoryConfigRef } from './mosaic-config.js';
|
||||
export {
|
||||
DEFAULT_LOCAL_CONFIG,
|
||||
DEFAULT_TEAM_CONFIG,
|
||||
loadConfig,
|
||||
validateConfig,
|
||||
} from './mosaic-config.js';
|
||||
140
packages/config/src/mosaic-config.ts
Normal file
140
packages/config/src/mosaic-config.ts
Normal file
@@ -0,0 +1,140 @@
|
||||
import { readFileSync, existsSync } from 'node:fs';
|
||||
import { resolve } from 'node:path';
|
||||
import type { StorageConfig } from '@mosaic/storage';
|
||||
import type { QueueAdapterConfig as QueueConfig } from '@mosaic/queue';
|
||||
|
||||
/* ------------------------------------------------------------------ */
|
||||
/* Types */
|
||||
/* ------------------------------------------------------------------ */
|
||||
|
||||
export type StorageTier = 'local' | 'team';
|
||||
|
||||
export interface MemoryConfigRef {
|
||||
type: 'pgvector' | 'sqlite-vec' | 'keyword';
|
||||
}
|
||||
|
||||
export interface MosaicConfig {
|
||||
tier: StorageTier;
|
||||
storage: StorageConfig;
|
||||
queue: QueueConfig;
|
||||
memory: MemoryConfigRef;
|
||||
}
|
||||
|
||||
/* ------------------------------------------------------------------ */
|
||||
/* Defaults */
|
||||
/* ------------------------------------------------------------------ */
|
||||
|
||||
export const DEFAULT_LOCAL_CONFIG: MosaicConfig = {
|
||||
tier: 'local',
|
||||
storage: { type: 'sqlite', path: '.mosaic/data.db' },
|
||||
queue: { type: 'local', dataDir: '.mosaic/queue' },
|
||||
memory: { type: 'keyword' },
|
||||
};
|
||||
|
||||
export const DEFAULT_TEAM_CONFIG: MosaicConfig = {
|
||||
tier: 'team',
|
||||
storage: { type: 'postgres', url: 'postgresql://mosaic:mosaic@localhost:5432/mosaic' },
|
||||
queue: { type: 'bullmq' },
|
||||
memory: { type: 'pgvector' },
|
||||
};
|
||||
|
||||
/* ------------------------------------------------------------------ */
|
||||
/* Validation */
|
||||
/* ------------------------------------------------------------------ */
|
||||
|
||||
const VALID_TIERS = new Set<string>(['local', 'team']);
|
||||
const VALID_STORAGE_TYPES = new Set<string>(['postgres', 'sqlite', 'files']);
|
||||
const VALID_QUEUE_TYPES = new Set<string>(['bullmq', 'local']);
|
||||
const VALID_MEMORY_TYPES = new Set<string>(['pgvector', 'sqlite-vec', 'keyword']);
|
||||
|
||||
export function validateConfig(raw: unknown): MosaicConfig {
|
||||
if (typeof raw !== 'object' || raw === null) {
|
||||
throw new Error('MosaicConfig must be a non-null object');
|
||||
}
|
||||
|
||||
const obj = raw as Record<string, unknown>;
|
||||
|
||||
// tier
|
||||
const tier = obj['tier'];
|
||||
if (typeof tier !== 'string' || !VALID_TIERS.has(tier)) {
|
||||
throw new Error(`Invalid tier "${String(tier)}" — expected "local" or "team"`);
|
||||
}
|
||||
|
||||
// storage
|
||||
const storage = obj['storage'];
|
||||
if (typeof storage !== 'object' || storage === null) {
|
||||
throw new Error('config.storage must be a non-null object');
|
||||
}
|
||||
const storageType = (storage as Record<string, unknown>)['type'];
|
||||
if (typeof storageType !== 'string' || !VALID_STORAGE_TYPES.has(storageType)) {
|
||||
throw new Error(`Invalid storage.type "${String(storageType)}"`);
|
||||
}
|
||||
|
||||
// queue
|
||||
const queue = obj['queue'];
|
||||
if (typeof queue !== 'object' || queue === null) {
|
||||
throw new Error('config.queue must be a non-null object');
|
||||
}
|
||||
const queueType = (queue as Record<string, unknown>)['type'];
|
||||
if (typeof queueType !== 'string' || !VALID_QUEUE_TYPES.has(queueType)) {
|
||||
throw new Error(`Invalid queue.type "${String(queueType)}"`);
|
||||
}
|
||||
|
||||
// memory
|
||||
const memory = obj['memory'];
|
||||
if (typeof memory !== 'object' || memory === null) {
|
||||
throw new Error('config.memory must be a non-null object');
|
||||
}
|
||||
const memoryType = (memory as Record<string, unknown>)['type'];
|
||||
if (typeof memoryType !== 'string' || !VALID_MEMORY_TYPES.has(memoryType)) {
|
||||
throw new Error(`Invalid memory.type "${String(memoryType)}"`);
|
||||
}
|
||||
|
||||
return {
|
||||
tier: tier as StorageTier,
|
||||
storage: storage as StorageConfig,
|
||||
queue: queue as QueueConfig,
|
||||
memory: memory as MemoryConfigRef,
|
||||
};
|
||||
}
|
||||
|
||||
/* ------------------------------------------------------------------ */
|
||||
/* Loader */
|
||||
/* ------------------------------------------------------------------ */
|
||||
|
||||
function detectFromEnv(): MosaicConfig {
|
||||
if (process.env['DATABASE_URL']) {
|
||||
return {
|
||||
...DEFAULT_TEAM_CONFIG,
|
||||
storage: {
|
||||
type: 'postgres',
|
||||
url: process.env['DATABASE_URL'],
|
||||
},
|
||||
queue: {
|
||||
type: 'bullmq',
|
||||
url: process.env['VALKEY_URL'],
|
||||
},
|
||||
};
|
||||
}
|
||||
return DEFAULT_LOCAL_CONFIG;
|
||||
}
|
||||
|
||||
export function loadConfig(configPath?: string): MosaicConfig {
|
||||
// 1. Explicit path or default location
|
||||
const paths = configPath
|
||||
? [resolve(configPath)]
|
||||
: [
|
||||
resolve(process.cwd(), 'mosaic.config.json'),
|
||||
resolve(process.cwd(), '../../mosaic.config.json'), // monorepo root when cwd is apps/gateway
|
||||
];
|
||||
|
||||
for (const p of paths) {
|
||||
if (existsSync(p)) {
|
||||
const raw: unknown = JSON.parse(readFileSync(p, 'utf-8'));
|
||||
return validateConfig(raw);
|
||||
}
|
||||
}
|
||||
|
||||
// 2. Fall back to env-var detection
|
||||
return detectFromEnv();
|
||||
}
|
||||
9
packages/config/tsconfig.json
Normal file
9
packages/config/tsconfig.json
Normal file
@@ -0,0 +1,9 @@
|
||||
{
|
||||
"extends": "../../tsconfig.base.json",
|
||||
"compilerOptions": {
|
||||
"outDir": "dist",
|
||||
"rootDir": "src"
|
||||
},
|
||||
"include": ["src/**/*"],
|
||||
"exclude": ["node_modules", "dist"]
|
||||
}
|
||||
Reference in New Issue
Block a user