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

24
apps/worker/package.json Normal file
View File

@@ -0,0 +1,24 @@
{
"name": "@image-platform/worker",
"version": "0.1.0",
"private": true,
"type": "module",
"scripts": {
"build": "tsc -p tsconfig.build.json",
"dev": "tsx watch src/main.ts",
"start": "node dist/main.js",
"typecheck": "tsc --noEmit -p tsconfig.json"
},
"dependencies": {
"@image-platform/database": "workspace:*",
"@image-platform/queue": "workspace:*",
"@image-platform/storage": "workspace:*",
"amqplib": "^1.0.4"
},
"devDependencies": {
"@types/amqplib": "^0.10.8",
"@types/node": "^25.6.0",
"tsx": "^4.21.0",
"typescript": "^6.0.3"
}
}

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)
})

View File

@@ -0,0 +1,7 @@
{
"extends": "./tsconfig.json",
"compilerOptions": {
"noEmit": false
},
"exclude": ["dist", "node_modules", "**/*.spec.ts"]
}

21
apps/worker/tsconfig.json Normal file
View File

@@ -0,0 +1,21 @@
{
"compilerOptions": {
"allowSyntheticDefaultImports": true,
"declaration": true,
"esModuleInterop": true,
"forceConsistentCasingInFileNames": true,
"lib": ["ES2023"],
"module": "NodeNext",
"moduleResolution": "NodeNext",
"noUncheckedIndexedAccess": true,
"outDir": "./dist",
"rootDir": "./src",
"skipLibCheck": true,
"sourceMap": true,
"strict": true,
"target": "ES2023",
"types": ["node"]
},
"include": ["src/**/*.ts"],
"exclude": ["dist", "node_modules"]
}