import { pgTable, uuid, varchar, text, integer, jsonb, timestamp, pgEnum, index, uniqueIndex, check, type AnyPgColumn, } from 'drizzle-orm/pg-core'; import { sql } from 'drizzle-orm'; import { projects } from './projects'; import { flows } from './flows'; import { apiKeys } from './apiKeys'; // Enum for image source export const imageSourceEnum = pgEnum('image_source', ['generated', 'uploaded']); // Type for focal point JSONB export type FocalPoint = { x: number; // 0.0 - 1.0 y: number; // 0.0 - 1.0 }; export const images = pgTable( 'images', { id: uuid('id').primaryKey().defaultRandom(), // Relations projectId: uuid('project_id') .notNull() .references(() => projects.id, { onDelete: 'cascade' }), generationId: uuid('generation_id').references( (): AnyPgColumn => { const { generations } = require('./generations'); return generations.id; }, { onDelete: 'set null' }, ), flowId: uuid('flow_id').references(() => flows.id, { onDelete: 'cascade' }), apiKeyId: uuid('api_key_id').references(() => apiKeys.id, { onDelete: 'set null' }), // Storage (MinIO path format: orgSlug/projectSlug/category/YYYY-MM/filename.ext) storageKey: varchar('storage_key', { length: 500 }).notNull().unique(), storageUrl: text('storage_url').notNull(), // File metadata mimeType: varchar('mime_type', { length: 100 }).notNull(), fileSize: integer('file_size').notNull(), fileHash: varchar('file_hash', { length: 64 }), // SHA-256 for deduplication // Dimensions width: integer('width'), height: integer('height'), aspectRatio: varchar('aspect_ratio', { length: 10 }), // Focal point for image transformations (imageflow) // Normalized coordinates: { "x": 0.5, "y": 0.3 } where 0.0-1.0 focalPoint: jsonb('focal_point').$type(), // Source source: imageSourceEnum('source').notNull(), // Project-level alias (global scope) // Flow-level aliases stored in flows.aliases alias: varchar('alias', { length: 100 }), // Metadata description: text('description'), tags: text('tags').array(), meta: jsonb('meta').$type>().notNull().default({}), // Audit createdAt: timestamp('created_at').notNull().defaultNow(), updatedAt: timestamp('updated_at') .notNull() .defaultNow() .$onUpdate(() => new Date()), deletedAt: timestamp('deleted_at'), // Soft delete }, (table) => ({ // CHECK constraints sourceGeneratedCheck: check( 'source_generation_check', sql`(${table.source} = 'uploaded' AND ${table.generationId} IS NULL) OR (${table.source} = 'generated' AND ${table.generationId} IS NOT NULL)`, ), aliasFormatCheck: check( 'alias_format_check', sql`${table.alias} IS NULL OR ${table.alias} ~ '^@[a-zA-Z0-9_-]+$'`, ), fileSizeCheck: check('file_size_check', sql`${table.fileSize} > 0`), widthCheck: check( 'width_check', sql`${table.width} IS NULL OR (${table.width} > 0 AND ${table.width} <= 8192)`, ), heightCheck: check( 'height_check', sql`${table.height} IS NULL OR (${table.height} > 0 AND ${table.height} <= 8192)`, ), // Indexes // Unique index for project-scoped aliases (partial index) projectAliasIdx: uniqueIndex('idx_images_project_alias') .on(table.projectId, table.alias) .where(sql`${table.alias} IS NOT NULL AND ${table.deletedAt} IS NULL AND ${table.flowId} IS NULL`), // Index for querying images by project and source (partial index) projectSourceIdx: index('idx_images_project_source') .on(table.projectId, table.source, table.createdAt.desc()) .where(sql`${table.deletedAt} IS NULL`), // Index for flow-scoped images (partial index) flowIdx: index('idx_images_flow') .on(table.flowId) .where(sql`${table.flowId} IS NOT NULL`), // Index for generation lookup generationIdx: index('idx_images_generation').on(table.generationId), // Index for storage key lookup storageKeyIdx: index('idx_images_storage_key').on(table.storageKey), // Index for file hash (deduplication) hashIdx: index('idx_images_hash').on(table.fileHash), // Index for API key audit trail apiKeyIdx: index('idx_images_api_key').on(table.apiKeyId), }), ); export type Image = typeof images.$inferSelect; export type NewImage = typeof images.$inferInsert;