Initial commit: Meshkee CMS API

NestJS backend with Prisma, Docker Compose for Postgres/Redis, and deploy docs for the production VM.
This commit is contained in:
Ali Reza
2026-07-21 17:52:36 +03:30
commit bb59d5e9ba
254 changed files with 37031 additions and 0 deletions
+83
View File
@@ -0,0 +1,83 @@
import { ServiceUnavailableException } from '@nestjs/common';
import { ConfigService } from '@nestjs/config';
export type AiProviderConfig = {
apiKey: string;
baseUrl: string;
model: string;
};
export function resolveAiProvider(config: ConfigService): AiProviderConfig {
const configured = config.get<string>('AI_PROVIDER')?.trim().toLowerCase();
const groqKey = config.get<string>('GROQ_API_KEY')?.trim();
const openAiKey = config.get<string>('OPENAI_API_KEY')?.trim();
const useGroq =
configured === 'groq' || (!configured && !!groqKey) || (!openAiKey && !!groqKey);
if (useGroq) {
if (!groqKey) {
throw new ServiceUnavailableException(
'AI is not configured. Set GROQ_API_KEY on the server.',
);
}
return {
apiKey: groqKey,
baseUrl: 'https://api.groq.com/openai/v1',
model: config.get<string>('GROQ_MODEL')?.trim() || 'llama-3.3-70b-versatile',
};
}
if (!openAiKey) {
throw new ServiceUnavailableException(
'AI is not configured. Set GROQ_API_KEY or OPENAI_API_KEY on the server.',
);
}
return {
apiKey: openAiKey,
baseUrl: 'https://api.openai.com/v1',
model: config.get<string>('OPENAI_MODEL')?.trim() || 'gpt-4o-mini',
};
}
export async function requestAiJsonCompletion(
provider: AiProviderConfig,
systemPrompt: string,
userPrompt: string,
temperature = 0.4,
): Promise<string> {
const response = await fetch(`${provider.baseUrl}/chat/completions`, {
method: 'POST',
headers: {
Authorization: `Bearer ${provider.apiKey}`,
'Content-Type': 'application/json',
},
body: JSON.stringify({
model: provider.model,
temperature,
response_format: { type: 'json_object' },
messages: [
{ role: 'system', content: systemPrompt },
{ role: 'user', content: userPrompt },
],
}),
});
if (!response.ok) {
const detail = await response.text();
throw new Error(`AI provider error (${response.status}): ${detail.slice(0, 240)}`);
}
const payload = (await response.json()) as {
choices?: { message?: { content?: string } }[];
};
const content = payload.choices?.[0]?.message?.content;
if (!content) {
throw new Error('AI returned an empty response');
}
return content;
}
@@ -0,0 +1,36 @@
import {
CallHandler,
ExecutionContext,
Injectable,
NestInterceptor,
} from '@nestjs/common';
import { Observable, map } from 'rxjs';
function serializeBigInt(value: unknown): unknown {
if (typeof value === 'bigint') {
return Number(value);
}
if (value instanceof Date) {
return value.toISOString();
}
if (Array.isArray(value)) {
return value.map(serializeBigInt);
}
if (value !== null && typeof value === 'object') {
return Object.fromEntries(
Object.entries(value).map(([key, val]) => [key, serializeBigInt(val)]),
);
}
return value;
}
@Injectable()
export class BigIntSerializerInterceptor implements NestInterceptor {
intercept(_context: ExecutionContext, next: CallHandler): Observable<unknown> {
return next.handle().pipe(map((data) => serializeBigInt(data)));
}
}