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,21 @@
import { drizzle } from "drizzle-orm/node-postgres"
import pg from "pg"
import * as schema from "./schema.js"
const { Pool } = pg
export type DatabasePool = pg.Pool
export type Database = ReturnType<typeof createDatabase>
export function createDatabasePool(databaseUrl = process.env.DATABASE_URL) {
if (!databaseUrl) {
throw new Error("DATABASE_URL is required")
}
return new Pool({ connectionString: databaseUrl })
}
export function createDatabase(pool: DatabasePool) {
return drizzle(pool, { schema })
}

View File

@@ -0,0 +1,2 @@
export { createDatabase, createDatabasePool } from "./client.js"
export * from "./schema.js"

View File

@@ -0,0 +1,113 @@
import {
bigint,
boolean,
index,
integer,
pgEnum,
pgTable,
text,
timestamp,
uniqueIndex,
uuid,
} from "drizzle-orm/pg-core"
export const assetStatusEnum = pgEnum("asset_status", ["active", "disabled", "deleted"])
export const variantFormatEnum = pgEnum("variant_format", ["avif", "webp", "jpg", "png"])
export const requestedFormatEnum = pgEnum("requested_format", ["auto", "avif", "webp", "jpg", "png"])
export const variantStatusEnum = pgEnum("variant_status", ["pending", "processing", "ready", "failed"])
const timestamps = {
createdAt: timestamp("created_at", { withTimezone: true }).notNull().defaultNow(),
updatedAt: timestamp("updated_at", { withTimezone: true }).notNull().defaultNow(),
}
export const allowedImageHosts = pgTable(
"allowed_image_hosts",
{
id: uuid("id").primaryKey().defaultRandom(),
hostname: text("hostname").notNull(),
enabled: boolean("enabled").notNull().default(true),
description: text("description"),
...timestamps,
},
(table) => [uniqueIndex("allowed_image_hosts_hostname_idx").on(table.hostname)],
)
export const imageAssets = pgTable(
"image_assets",
{
id: uuid("id").primaryKey().defaultRandom(),
publicId: text("public_id").notNull(),
currentVersion: integer("current_version").notNull().default(1),
status: assetStatusEnum("status").notNull().default("active"),
...timestamps,
},
(table) => [uniqueIndex("image_assets_public_id_idx").on(table.publicId)],
)
export const imageAssetVersions = pgTable(
"image_asset_versions",
{
id: uuid("id").primaryKey().defaultRandom(),
assetId: uuid("asset_id")
.notNull()
.references(() => imageAssets.id, { onDelete: "cascade" }),
version: integer("version").notNull(),
sourceUrl: text("source_url").notNull(),
sourceHost: text("source_host").notNull(),
sourceHash: text("source_hash").notNull(),
originalS3Key: text("original_s3_key"),
width: integer("width"),
height: integer("height"),
contentType: text("content_type"),
sizeBytes: bigint("size_bytes", { mode: "number" }),
createdAt: timestamp("created_at", { withTimezone: true }).notNull().defaultNow(),
},
(table) => [
uniqueIndex("image_asset_versions_asset_version_idx").on(table.assetId, table.version),
index("image_asset_versions_source_hash_idx").on(table.sourceHash),
],
)
export const imageVariants = pgTable(
"image_variants",
{
id: uuid("id").primaryKey().defaultRandom(),
assetId: uuid("asset_id")
.notNull()
.references(() => imageAssets.id, { onDelete: "cascade" }),
assetVersionId: uuid("asset_version_id")
.notNull()
.references(() => imageAssetVersions.id, { onDelete: "cascade" }),
assetVersion: integer("asset_version").notNull(),
preset: text("preset").notNull(),
variantHash: text("variant_hash").notNull(),
requestedFormat: requestedFormatEnum("requested_format").notNull().default("auto"),
format: variantFormatEnum("format").notNull(),
width: integer("width").notNull(),
height: integer("height"),
quality: integer("quality").notNull(),
s3Key: text("s3_key").notNull(),
contentType: text("content_type"),
etag: text("etag"),
status: variantStatusEnum("status").notNull().default("pending"),
sizeBytes: bigint("size_bytes", { mode: "number" }),
error: text("error"),
attemptCount: integer("attempt_count").notNull().default(0),
lastAccessedAt: timestamp("last_accessed_at", { withTimezone: true }),
...timestamps,
},
(table) => [
uniqueIndex("image_variants_lookup_idx").on(
table.assetId,
table.assetVersion,
table.preset,
table.width,
table.quality,
table.format,
),
uniqueIndex("image_variants_s3_key_idx").on(table.s3Key),
uniqueIndex("image_variants_variant_hash_idx").on(table.variantHash),
index("image_variants_status_idx").on(table.status),
],
)