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,24 @@
{
"name": "@image-platform/storage",
"version": "0.1.0",
"private": true,
"type": "module",
"exports": {
".": {
"types": "./src/index.ts",
"default": "./dist/index.js"
}
},
"types": "./src/index.ts",
"scripts": {
"build": "tsc -p tsconfig.build.json",
"typecheck": "tsc --noEmit -p tsconfig.json"
},
"dependencies": {
"@aws-sdk/client-s3": "^3.1042.0"
},
"devDependencies": {
"@types/node": "^25.6.0",
"typescript": "^6.0.3"
}
}

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
}

View File

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

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"]
}