feat: добавить базовые сервисы image-platform

- добавлены backend, admin, gateway и worker skeleton
- добавлены Drizzle schema, database package и initial migration
- добавлены shared packages для RabbitMQ topology и S3 helpers
- обновлены dev-инфраструктура, env example, scripts и dependencies
- обновлена документация под versioned image URLs и read-through flow
This commit is contained in:
2026-05-05 09:59:21 +03:00
parent 37592c8b81
commit bcadb85a83
66 changed files with 8698 additions and 213 deletions

25
apps/worker/src/config.ts Normal file
View File

@@ -0,0 +1,25 @@
import { loadQueueTopologyFromEnv, type QueueTopology } from "@image-platform/queue"
export type WorkerConfig = {
prefetch: number
queueTopology: QueueTopology
rabbitmqUrl: string
}
export function loadWorkerConfig(env: NodeJS.ProcessEnv = process.env): WorkerConfig {
return {
prefetch: parsePositiveInteger(env.WORKER_PREFETCH, 2),
queueTopology: loadQueueTopologyFromEnv(env),
rabbitmqUrl: env.RABBITMQ_URL ?? "amqp://image:image-password@localhost:5672/image_platform",
}
}
function parsePositiveInteger(value: string | undefined, fallback: number) {
if (!value) {
return fallback
}
const parsed = Number.parseInt(value, 10)
return Number.isSafeInteger(parsed) && parsed > 0 ? parsed : fallback
}

67
apps/worker/src/main.ts Normal file
View File

@@ -0,0 +1,67 @@
import amqp, { type Channel, type ConsumeMessage } from "amqplib"
import { parseGenerateVariantJobBuffer, type QueueTopology } from "@image-platform/queue"
import { loadWorkerConfig } from "./config.js"
async function bootstrap() {
const config = loadWorkerConfig()
const connection = await amqp.connect(config.rabbitmqUrl)
const channel = await connection.createChannel()
await assertQueueTopology(channel, config.queueTopology)
await channel.prefetch(config.prefetch)
await channel.consume(
config.queueTopology.generateVariantQueue,
(message) => void handleGenerateVariantMessage(channel, message),
{ noAck: false },
)
console.log(`worker consuming ${config.queueTopology.generateVariantQueue}`)
const shutdown = async () => {
console.log("worker shutting down")
await channel.close().catch((error: unknown) => console.error("failed to close RabbitMQ channel", error))
await connection.close().catch((error: unknown) => console.error("failed to close RabbitMQ connection", error))
process.exit(0)
}
process.once("SIGINT", () => void shutdown())
process.once("SIGTERM", () => void shutdown())
}
async function assertQueueTopology(channel: Channel, topology: QueueTopology) {
await channel.assertExchange(topology.jobsExchange, "direct", { durable: true })
await channel.assertExchange(topology.jobsDeadLetterExchange, "direct", { durable: true })
await channel.assertQueue(topology.generateVariantQueue, {
deadLetterExchange: topology.jobsDeadLetterExchange,
deadLetterRoutingKey: topology.generateVariantDeadLetterRoutingKey,
durable: true,
})
await channel.assertQueue(topology.generateVariantDeadLetterQueue, { durable: true })
await channel.bindQueue(topology.generateVariantQueue, topology.jobsExchange, topology.generateVariantRoutingKey)
await channel.bindQueue(
topology.generateVariantDeadLetterQueue,
topology.jobsDeadLetterExchange,
topology.generateVariantDeadLetterRoutingKey,
)
}
async function handleGenerateVariantMessage(channel: Channel, message: ConsumeMessage | null) {
if (message === null) {
return
}
try {
const job = parseGenerateVariantJobBuffer(message.content)
console.log("generate variant job received, handler not implemented yet", job)
channel.nack(message, false, false)
} catch (error) {
console.error("invalid generate variant job", error)
channel.nack(message, false, false)
}
}
void bootstrap().catch((error: unknown) => {
console.error("worker failed to start", error)
process.exit(1)
})