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

View File

@@ -0,0 +1,20 @@
import { S3Client, type S3ClientConfig } from "@aws-sdk/client-s3"
import type { StorageConfig } from "./config.js"
export function createS3Client(config: StorageConfig) {
const clientConfig: S3ClientConfig = {
endpoint: config.endpoint,
forcePathStyle: config.forcePathStyle,
region: config.region,
}
if (config.accessKeyId && config.secretAccessKey) {
clientConfig.credentials = {
accessKeyId: config.accessKeyId,
secretAccessKey: config.secretAccessKey,
}
}
return new S3Client(clientConfig)
}

View File

@@ -0,0 +1,31 @@
export type StorageConfig = {
accessKeyId?: string
bucket: string
endpoint?: string
forcePathStyle: boolean
region: string
secretAccessKey?: string
}
export function loadStorageConfigFromEnv(env: NodeJS.ProcessEnv = process.env): StorageConfig {
return {
accessKeyId: normalizeOptionalString(env.S3_ACCESS_KEY_ID),
bucket: env.S3_BUCKET ?? "image-platform",
endpoint: normalizeOptionalString(env.S3_ENDPOINT),
forcePathStyle: parseBoolean(env.S3_FORCE_PATH_STYLE, true),
region: env.S3_REGION ?? "us-east-1",
secretAccessKey: normalizeOptionalString(env.S3_SECRET_ACCESS_KEY),
}
}
function normalizeOptionalString(value: string | undefined) {
return value && value.trim().length > 0 ? value : undefined
}
function parseBoolean(value: string | undefined, fallback: boolean) {
if (value === undefined) {
return fallback
}
return ["1", "true", "yes"].includes(value.toLowerCase())
}

View File

@@ -0,0 +1,3 @@
export * from "./client.js"
export * from "./config.js"
export * from "./keys.js"

View File

@@ -0,0 +1,40 @@
export type VariantFormat = "avif" | "jpg" | "png" | "webp"
export type OriginalImageKeyInput = {
assetId: string
version: number
}
export type VariantImageKeyInput = OriginalImageKeyInput & {
format: VariantFormat
variantHash: string
}
export function buildOriginalImageKey(input: OriginalImageKeyInput) {
return `originals/${safeSegment(input.assetId, "assetId")}/v${safeVersion(input.version)}/source`
}
export function buildVariantImageKey(input: VariantImageKeyInput) {
return [
"variants",
safeSegment(input.assetId, "assetId"),
`v${safeVersion(input.version)}`,
`${safeSegment(input.variantHash, "variantHash")}.${input.format}`,
].join("/")
}
function safeSegment(value: string, name: string) {
if (value.length === 0 || value.includes("/")) {
throw new Error(`${name} must be a non-empty S3 key segment`)
}
return value
}
function safeVersion(value: number) {
if (!Number.isSafeInteger(value) || value < 1) {
throw new Error("version must be a positive integer")
}
return value
}