138 lines
4.4 KiB
TypeScript
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;
|