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:
@@ -19,6 +19,7 @@
|
|||||||
"@modelcontextprotocol/sdk": "^1.27.1",
|
"@modelcontextprotocol/sdk": "^1.27.1",
|
||||||
"@mosaic/auth": "workspace:^",
|
"@mosaic/auth": "workspace:^",
|
||||||
"@mosaic/brain": "workspace:^",
|
"@mosaic/brain": "workspace:^",
|
||||||
|
"@mosaic/config": "workspace:^",
|
||||||
"@mosaic/coord": "workspace:^",
|
"@mosaic/coord": "workspace:^",
|
||||||
"@mosaic/db": "workspace:^",
|
"@mosaic/db": "workspace:^",
|
||||||
"@mosaic/discord-plugin": "workspace:^",
|
"@mosaic/discord-plugin": "workspace:^",
|
||||||
|
|||||||
@@ -1,6 +1,7 @@
|
|||||||
import { Module } from '@nestjs/common';
|
import { Module } from '@nestjs/common';
|
||||||
import { APP_GUARD } from '@nestjs/core';
|
import { APP_GUARD } from '@nestjs/core';
|
||||||
import { HealthController } from './health/health.controller.js';
|
import { HealthController } from './health/health.controller.js';
|
||||||
|
import { ConfigModule } from './config/config.module.js';
|
||||||
import { DatabaseModule } from './database/database.module.js';
|
import { DatabaseModule } from './database/database.module.js';
|
||||||
import { AuthModule } from './auth/auth.module.js';
|
import { AuthModule } from './auth/auth.module.js';
|
||||||
import { BrainModule } from './brain/brain.module.js';
|
import { BrainModule } from './brain/brain.module.js';
|
||||||
@@ -28,6 +29,7 @@ import { ThrottlerGuard, ThrottlerModule } from '@nestjs/throttler';
|
|||||||
@Module({
|
@Module({
|
||||||
imports: [
|
imports: [
|
||||||
ThrottlerModule.forRoot([{ name: 'default', ttl: 60_000, limit: 60 }]),
|
ThrottlerModule.forRoot([{ name: 'default', ttl: 60_000, limit: 60 }]),
|
||||||
|
ConfigModule,
|
||||||
DatabaseModule,
|
DatabaseModule,
|
||||||
AuthModule,
|
AuthModule,
|
||||||
BrainModule,
|
BrainModule,
|
||||||
|
|||||||
16
apps/gateway/src/config/config.module.ts
Normal file
16
apps/gateway/src/config/config.module.ts
Normal file
@@ -0,0 +1,16 @@
|
|||||||
|
import { Global, Module } from '@nestjs/common';
|
||||||
|
import { loadConfig, type MosaicConfig } from '@mosaic/config';
|
||||||
|
|
||||||
|
export const MOSAIC_CONFIG = 'MOSAIC_CONFIG';
|
||||||
|
|
||||||
|
@Global()
|
||||||
|
@Module({
|
||||||
|
providers: [
|
||||||
|
{
|
||||||
|
provide: MOSAIC_CONFIG,
|
||||||
|
useFactory: (): MosaicConfig => loadConfig(),
|
||||||
|
},
|
||||||
|
],
|
||||||
|
exports: [MOSAIC_CONFIG],
|
||||||
|
})
|
||||||
|
export class ConfigModule {}
|
||||||
@@ -1,19 +1,21 @@
|
|||||||
import { Global, Inject, Module, type OnApplicationShutdown } from '@nestjs/common';
|
import { Global, Inject, Module, type OnApplicationShutdown } from '@nestjs/common';
|
||||||
import { createDb, type Db, type DbHandle } from '@mosaic/db';
|
import { createDb, type Db, type DbHandle } from '@mosaic/db';
|
||||||
import { createStorageAdapter, type StorageAdapter } from '@mosaic/storage';
|
import { createStorageAdapter, type StorageAdapter } from '@mosaic/storage';
|
||||||
|
import type { MosaicConfig } from '@mosaic/config';
|
||||||
|
import { MOSAIC_CONFIG } from '../config/config.module.js';
|
||||||
|
|
||||||
export const DB_HANDLE = 'DB_HANDLE';
|
export const DB_HANDLE = 'DB_HANDLE';
|
||||||
export const DB = 'DB';
|
export const DB = 'DB';
|
||||||
export const STORAGE_ADAPTER = 'STORAGE_ADAPTER';
|
export const STORAGE_ADAPTER = 'STORAGE_ADAPTER';
|
||||||
|
|
||||||
const DEFAULT_DATABASE_URL = 'postgresql://mosaic:mosaic@localhost:5432/mosaic';
|
|
||||||
|
|
||||||
@Global()
|
@Global()
|
||||||
@Module({
|
@Module({
|
||||||
providers: [
|
providers: [
|
||||||
{
|
{
|
||||||
provide: DB_HANDLE,
|
provide: DB_HANDLE,
|
||||||
useFactory: (): DbHandle => createDb(),
|
useFactory: (config: MosaicConfig): DbHandle =>
|
||||||
|
createDb(config.storage.type === 'postgres' ? config.storage.url : undefined),
|
||||||
|
inject: [MOSAIC_CONFIG],
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
provide: DB,
|
provide: DB,
|
||||||
@@ -22,11 +24,8 @@ const DEFAULT_DATABASE_URL = 'postgresql://mosaic:mosaic@localhost:5432/mosaic';
|
|||||||
},
|
},
|
||||||
{
|
{
|
||||||
provide: STORAGE_ADAPTER,
|
provide: STORAGE_ADAPTER,
|
||||||
useFactory: (): StorageAdapter =>
|
useFactory: (config: MosaicConfig): StorageAdapter => createStorageAdapter(config.storage),
|
||||||
createStorageAdapter({
|
inject: [MOSAIC_CONFIG],
|
||||||
type: 'postgres',
|
|
||||||
url: process.env['DATABASE_URL'] ?? DEFAULT_DATABASE_URL,
|
|
||||||
}),
|
|
||||||
},
|
},
|
||||||
],
|
],
|
||||||
exports: [DB, STORAGE_ADAPTER],
|
exports: [DB, STORAGE_ADAPTER],
|
||||||
|
|||||||
@@ -1,13 +1,29 @@
|
|||||||
import { Global, Module } from '@nestjs/common';
|
import { Global, Module } from '@nestjs/common';
|
||||||
import { createMemory, type Memory, createMemoryAdapter, type MemoryAdapter } from '@mosaic/memory';
|
import {
|
||||||
|
createMemory,
|
||||||
|
type Memory,
|
||||||
|
createMemoryAdapter,
|
||||||
|
type MemoryAdapter,
|
||||||
|
type MemoryConfig,
|
||||||
|
} from '@mosaic/memory';
|
||||||
import type { Db } from '@mosaic/db';
|
import type { Db } from '@mosaic/db';
|
||||||
import { DB } from '../database/database.module.js';
|
import type { StorageAdapter } from '@mosaic/storage';
|
||||||
|
import type { MosaicConfig } from '@mosaic/config';
|
||||||
|
import { MOSAIC_CONFIG } from '../config/config.module.js';
|
||||||
|
import { DB, STORAGE_ADAPTER } from '../database/database.module.js';
|
||||||
import { MEMORY } from './memory.tokens.js';
|
import { MEMORY } from './memory.tokens.js';
|
||||||
import { MemoryController } from './memory.controller.js';
|
import { MemoryController } from './memory.controller.js';
|
||||||
import { EmbeddingService } from './embedding.service.js';
|
import { EmbeddingService } from './embedding.service.js';
|
||||||
|
|
||||||
export const MEMORY_ADAPTER = 'MEMORY_ADAPTER';
|
export const MEMORY_ADAPTER = 'MEMORY_ADAPTER';
|
||||||
|
|
||||||
|
function buildMemoryConfig(config: MosaicConfig, storageAdapter: StorageAdapter): MemoryConfig {
|
||||||
|
if (config.memory.type === 'keyword') {
|
||||||
|
return { type: 'keyword', storage: storageAdapter };
|
||||||
|
}
|
||||||
|
return { type: config.memory.type };
|
||||||
|
}
|
||||||
|
|
||||||
@Global()
|
@Global()
|
||||||
@Module({
|
@Module({
|
||||||
providers: [
|
providers: [
|
||||||
@@ -18,7 +34,9 @@ export const MEMORY_ADAPTER = 'MEMORY_ADAPTER';
|
|||||||
},
|
},
|
||||||
{
|
{
|
||||||
provide: MEMORY_ADAPTER,
|
provide: MEMORY_ADAPTER,
|
||||||
useFactory: (): MemoryAdapter => createMemoryAdapter({ type: 'pgvector' }),
|
useFactory: (config: MosaicConfig, storageAdapter: StorageAdapter): MemoryAdapter =>
|
||||||
|
createMemoryAdapter(buildMemoryConfig(config, storageAdapter)),
|
||||||
|
inject: [MOSAIC_CONFIG, STORAGE_ADAPTER],
|
||||||
},
|
},
|
||||||
EmbeddingService,
|
EmbeddingService,
|
||||||
],
|
],
|
||||||
|
|||||||
@@ -1,5 +1,7 @@
|
|||||||
import { Global, Module } from '@nestjs/common';
|
import { Global, Module } from '@nestjs/common';
|
||||||
import { createQueueAdapter, type QueueAdapter } from '@mosaic/queue';
|
import { createQueueAdapter, type QueueAdapter } from '@mosaic/queue';
|
||||||
|
import type { MosaicConfig } from '@mosaic/config';
|
||||||
|
import { MOSAIC_CONFIG } from '../config/config.module.js';
|
||||||
import { QueueService } from './queue.service.js';
|
import { QueueService } from './queue.service.js';
|
||||||
|
|
||||||
export const QUEUE_ADAPTER = 'QUEUE_ADAPTER';
|
export const QUEUE_ADAPTER = 'QUEUE_ADAPTER';
|
||||||
@@ -10,11 +12,8 @@ export const QUEUE_ADAPTER = 'QUEUE_ADAPTER';
|
|||||||
QueueService,
|
QueueService,
|
||||||
{
|
{
|
||||||
provide: QUEUE_ADAPTER,
|
provide: QUEUE_ADAPTER,
|
||||||
useFactory: (): QueueAdapter =>
|
useFactory: (config: MosaicConfig): QueueAdapter => createQueueAdapter(config.queue),
|
||||||
createQueueAdapter({
|
inject: [MOSAIC_CONFIG],
|
||||||
type: 'bullmq',
|
|
||||||
url: process.env['VALKEY_URL'],
|
|
||||||
}),
|
|
||||||
},
|
},
|
||||||
],
|
],
|
||||||
exports: [QueueService, QUEUE_ADAPTER],
|
exports: [QueueService, QUEUE_ADAPTER],
|
||||||
|
|||||||
6
mosaic.config.json
Normal file
6
mosaic.config.json
Normal file
@@ -0,0 +1,6 @@
|
|||||||
|
{
|
||||||
|
"tier": "local",
|
||||||
|
"storage": { "type": "sqlite", "path": ".mosaic/data.db" },
|
||||||
|
"queue": { "type": "local", "dataDir": ".mosaic/queue" },
|
||||||
|
"memory": { "type": "keyword" }
|
||||||
|
}
|
||||||
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"]
|
||||||
|
}
|
||||||
22
pnpm-lock.yaml
generated
22
pnpm-lock.yaml
generated
@@ -62,6 +62,9 @@ importers:
|
|||||||
'@mosaic/brain':
|
'@mosaic/brain':
|
||||||
specifier: workspace:^
|
specifier: workspace:^
|
||||||
version: link:../../packages/brain
|
version: link:../../packages/brain
|
||||||
|
'@mosaic/config':
|
||||||
|
specifier: workspace:^
|
||||||
|
version: link:../../packages/config
|
||||||
'@mosaic/coord':
|
'@mosaic/coord':
|
||||||
specifier: workspace:^
|
specifier: workspace:^
|
||||||
version: link:../../packages/coord
|
version: link:../../packages/coord
|
||||||
@@ -351,6 +354,25 @@ importers:
|
|||||||
specifier: ^2.0.0
|
specifier: ^2.0.0
|
||||||
version: 2.1.9(@types/node@22.19.15)(jsdom@29.0.0(@noble/hashes@2.0.1))(lightningcss@1.31.1)
|
version: 2.1.9(@types/node@22.19.15)(jsdom@29.0.0(@noble/hashes@2.0.1))(lightningcss@1.31.1)
|
||||||
|
|
||||||
|
packages/config:
|
||||||
|
dependencies:
|
||||||
|
'@mosaic/memory':
|
||||||
|
specifier: workspace:^
|
||||||
|
version: link:../memory
|
||||||
|
'@mosaic/queue':
|
||||||
|
specifier: workspace:^
|
||||||
|
version: link:../queue
|
||||||
|
'@mosaic/storage':
|
||||||
|
specifier: workspace:^
|
||||||
|
version: link:../storage
|
||||||
|
devDependencies:
|
||||||
|
typescript:
|
||||||
|
specifier: ^5.8.0
|
||||||
|
version: 5.9.3
|
||||||
|
vitest:
|
||||||
|
specifier: ^2.0.0
|
||||||
|
version: 2.1.9(@types/node@24.12.0)(jsdom@29.0.0(@noble/hashes@2.0.1))(lightningcss@1.31.1)
|
||||||
|
|
||||||
packages/coord:
|
packages/coord:
|
||||||
dependencies:
|
dependencies:
|
||||||
'@mosaic/types':
|
'@mosaic/types':
|
||||||
|
|||||||
Reference in New Issue
Block a user