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:
24
packages/storage/package.json
Normal file
24
packages/storage/package.json
Normal 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"
|
||||
}
|
||||
}
|
||||
20
packages/storage/src/client.ts
Normal file
20
packages/storage/src/client.ts
Normal 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)
|
||||
}
|
||||
31
packages/storage/src/config.ts
Normal file
31
packages/storage/src/config.ts
Normal 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())
|
||||
}
|
||||
3
packages/storage/src/index.ts
Normal file
3
packages/storage/src/index.ts
Normal file
@@ -0,0 +1,3 @@
|
||||
export * from "./client.js"
|
||||
export * from "./config.js"
|
||||
export * from "./keys.js"
|
||||
40
packages/storage/src/keys.ts
Normal file
40
packages/storage/src/keys.ts
Normal 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
|
||||
}
|
||||
7
packages/storage/tsconfig.build.json
Normal file
7
packages/storage/tsconfig.build.json
Normal file
@@ -0,0 +1,7 @@
|
||||
{
|
||||
"extends": "./tsconfig.json",
|
||||
"compilerOptions": {
|
||||
"noEmit": false
|
||||
},
|
||||
"exclude": ["dist", "node_modules", "**/*.spec.ts"]
|
||||
}
|
||||
21
packages/storage/tsconfig.json
Normal file
21
packages/storage/tsconfig.json
Normal 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"]
|
||||
}
|
||||
Reference in New Issue
Block a user