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:
21
packages/database/src/client.ts
Normal file
21
packages/database/src/client.ts
Normal 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 })
|
||||
}
|
||||
2
packages/database/src/index.ts
Normal file
2
packages/database/src/index.ts
Normal file
@@ -0,0 +1,2 @@
|
||||
export { createDatabase, createDatabasePool } from "./client.js"
|
||||
export * from "./schema.js"
|
||||
113
packages/database/src/schema.ts
Normal file
113
packages/database/src/schema.ts
Normal 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),
|
||||
],
|
||||
)
|
||||
Reference in New Issue
Block a user