banatie-service/packages/database/src/schema/images.ts

138 lines
4.4 KiB
TypeScript

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<FocalPoint>(),
// 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<Record<string, unknown>>().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;