feat: update DB schemas
This commit is contained in:
parent
e88617b430
commit
dbf82d2801
|
|
@ -0,0 +1,37 @@
|
|||
import { pgTable, uuid, jsonb, timestamp, index } from 'drizzle-orm/pg-core';
|
||||
import { projects } from './projects';
|
||||
|
||||
export const flows = pgTable(
|
||||
'flows',
|
||||
{
|
||||
id: uuid('id').primaryKey().defaultRandom(),
|
||||
|
||||
// Relations
|
||||
projectId: uuid('project_id')
|
||||
.notNull()
|
||||
.references(() => projects.id, { onDelete: 'cascade' }),
|
||||
|
||||
// Flow-scoped named aliases (user-assigned only)
|
||||
// Technical aliases (@last, @first, @upload) computed programmatically
|
||||
// Format: { "@hero": "image-uuid", "@product": "image-uuid" }
|
||||
aliases: jsonb('aliases').$type<Record<string, string>>().notNull().default({}),
|
||||
|
||||
// Flexible metadata storage
|
||||
meta: jsonb('meta').$type<Record<string, unknown>>().notNull().default({}),
|
||||
|
||||
// Timestamps
|
||||
createdAt: timestamp('created_at').notNull().defaultNow(),
|
||||
// Updates on every generation/upload activity within this flow
|
||||
updatedAt: timestamp('updated_at')
|
||||
.notNull()
|
||||
.defaultNow()
|
||||
.$onUpdate(() => new Date()),
|
||||
},
|
||||
(table) => ({
|
||||
// Index for querying flows by project, ordered by most recent
|
||||
projectCreatedAtIdx: index('idx_flows_project').on(table.projectId, table.createdAt.desc()),
|
||||
}),
|
||||
);
|
||||
|
||||
export type Flow = typeof flows.$inferSelect;
|
||||
export type NewFlow = typeof flows.$inferInsert;
|
||||
|
|
@ -0,0 +1,137 @@
|
|||
import {
|
||||
pgTable,
|
||||
uuid,
|
||||
varchar,
|
||||
text,
|
||||
integer,
|
||||
jsonb,
|
||||
timestamp,
|
||||
pgEnum,
|
||||
index,
|
||||
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 generation status
|
||||
export const generationStatusEnum = pgEnum('generation_status', [
|
||||
'pending',
|
||||
'processing',
|
||||
'success',
|
||||
'failed',
|
||||
]);
|
||||
|
||||
// Type for referenced images JSONB
|
||||
export type ReferencedImage = {
|
||||
imageId: string;
|
||||
alias: string;
|
||||
};
|
||||
|
||||
export const generations = pgTable(
|
||||
'generations',
|
||||
{
|
||||
id: uuid('id').primaryKey().defaultRandom(),
|
||||
|
||||
// Relations
|
||||
projectId: uuid('project_id')
|
||||
.notNull()
|
||||
.references(() => projects.id, { onDelete: 'cascade' }),
|
||||
flowId: uuid('flow_id').references(() => flows.id, { onDelete: 'set null' }),
|
||||
apiKeyId: uuid('api_key_id').references(() => apiKeys.id, { onDelete: 'set null' }),
|
||||
|
||||
// Status
|
||||
status: generationStatusEnum('status').notNull().default('pending'),
|
||||
|
||||
// Prompts
|
||||
originalPrompt: text('original_prompt').notNull(),
|
||||
enhancedPrompt: text('enhanced_prompt'), // AI-enhanced version (if enabled)
|
||||
|
||||
// Generation parameters
|
||||
aspectRatio: varchar('aspect_ratio', { length: 10 }),
|
||||
width: integer('width'),
|
||||
height: integer('height'),
|
||||
|
||||
// AI Model
|
||||
modelName: varchar('model_name', { length: 100 }).notNull().default('gemini-flash-image-001'),
|
||||
modelVersion: varchar('model_version', { length: 50 }),
|
||||
|
||||
// Result
|
||||
outputImageId: uuid('output_image_id').references(
|
||||
(): AnyPgColumn => {
|
||||
const { images } = require('./images');
|
||||
return images.id;
|
||||
},
|
||||
{ onDelete: 'set null' },
|
||||
),
|
||||
|
||||
// Referenced images used in generation
|
||||
// Format: [{ "imageId": "uuid", "alias": "@product" }, ...]
|
||||
referencedImages: jsonb('referenced_images').$type<ReferencedImage[]>(),
|
||||
|
||||
// Error handling
|
||||
errorMessage: text('error_message'),
|
||||
errorCode: varchar('error_code', { length: 50 }),
|
||||
retryCount: integer('retry_count').notNull().default(0),
|
||||
|
||||
// Metrics
|
||||
processingTimeMs: integer('processing_time_ms'),
|
||||
cost: integer('cost'), // In cents (USD)
|
||||
|
||||
// Request context
|
||||
requestId: uuid('request_id'),
|
||||
userAgent: text('user_agent'),
|
||||
ipAddress: varchar('ip_address', { length: 45 }), // IPv6 max length
|
||||
|
||||
// Metadata
|
||||
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()),
|
||||
},
|
||||
(table) => ({
|
||||
// CHECK constraints
|
||||
statusSuccessCheck: check(
|
||||
'status_success_check',
|
||||
sql`(${table.status} = 'success' AND ${table.outputImageId} IS NOT NULL) OR (${table.status} != 'success')`,
|
||||
),
|
||||
statusFailedCheck: check(
|
||||
'status_failed_check',
|
||||
sql`(${table.status} = 'failed' AND ${table.errorMessage} IS NOT NULL) OR (${table.status} != 'failed')`,
|
||||
),
|
||||
retryCountCheck: check('retry_count_check', sql`${table.retryCount} >= 0`),
|
||||
processingTimeCheck: check(
|
||||
'processing_time_check',
|
||||
sql`${table.processingTimeMs} IS NULL OR ${table.processingTimeMs} >= 0`,
|
||||
),
|
||||
costCheck: check('cost_check', sql`${table.cost} IS NULL OR ${table.cost} >= 0`),
|
||||
|
||||
// Indexes
|
||||
// Index for querying generations by project and status
|
||||
projectStatusIdx: index('idx_generations_project_status').on(
|
||||
table.projectId,
|
||||
table.status,
|
||||
table.createdAt.desc(),
|
||||
),
|
||||
|
||||
// Index for flow-scoped generations (partial index)
|
||||
flowIdx: index('idx_generations_flow')
|
||||
.on(table.flowId, table.createdAt.desc())
|
||||
.where(sql`${table.flowId} IS NOT NULL`),
|
||||
|
||||
// Index for output image lookup
|
||||
outputIdx: index('idx_generations_output').on(table.outputImageId),
|
||||
|
||||
// Index for request correlation
|
||||
requestIdx: index('idx_generations_request').on(table.requestId),
|
||||
}),
|
||||
);
|
||||
|
||||
export type Generation = typeof generations.$inferSelect;
|
||||
export type NewGeneration = typeof generations.$inferInsert;
|
||||
|
|
@ -0,0 +1,134 @@
|
|||
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 }).notNull(), // 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),
|
||||
}),
|
||||
);
|
||||
|
||||
export type Image = typeof images.$inferSelect;
|
||||
export type NewImage = typeof images.$inferInsert;
|
||||
|
|
@ -2,11 +2,19 @@ import { relations } from 'drizzle-orm';
|
|||
import { organizations } from './organizations';
|
||||
import { projects } from './projects';
|
||||
import { apiKeys } from './apiKeys';
|
||||
import { flows } from './flows';
|
||||
import { images } from './images';
|
||||
import { generations } from './generations';
|
||||
import { promptUrlCache } from './promptUrlCache';
|
||||
|
||||
// Export all tables
|
||||
export * from './organizations';
|
||||
export * from './projects';
|
||||
export * from './apiKeys';
|
||||
export * from './flows';
|
||||
export * from './images';
|
||||
export * from './generations';
|
||||
export * from './promptUrlCache';
|
||||
|
||||
// Define relations
|
||||
export const organizationsRelations = relations(organizations, ({ many }) => ({
|
||||
|
|
@ -20,9 +28,13 @@ export const projectsRelations = relations(projects, ({ one, many }) => ({
|
|||
references: [organizations.id],
|
||||
}),
|
||||
apiKeys: many(apiKeys),
|
||||
flows: many(flows),
|
||||
images: many(images),
|
||||
generations: many(generations),
|
||||
promptUrlCache: many(promptUrlCache),
|
||||
}));
|
||||
|
||||
export const apiKeysRelations = relations(apiKeys, ({ one }) => ({
|
||||
export const apiKeysRelations = relations(apiKeys, ({ one, many }) => ({
|
||||
organization: one(organizations, {
|
||||
fields: [apiKeys.organizationId],
|
||||
references: [organizations.id],
|
||||
|
|
@ -31,4 +43,70 @@ export const apiKeysRelations = relations(apiKeys, ({ one }) => ({
|
|||
fields: [apiKeys.projectId],
|
||||
references: [projects.id],
|
||||
}),
|
||||
images: many(images),
|
||||
generations: many(generations),
|
||||
}));
|
||||
|
||||
export const flowsRelations = relations(flows, ({ one, many }) => ({
|
||||
project: one(projects, {
|
||||
fields: [flows.projectId],
|
||||
references: [projects.id],
|
||||
}),
|
||||
images: many(images),
|
||||
generations: many(generations),
|
||||
}));
|
||||
|
||||
export const imagesRelations = relations(images, ({ one, many }) => ({
|
||||
project: one(projects, {
|
||||
fields: [images.projectId],
|
||||
references: [projects.id],
|
||||
}),
|
||||
generation: one(generations, {
|
||||
fields: [images.generationId],
|
||||
references: [generations.id],
|
||||
}),
|
||||
flow: one(flows, {
|
||||
fields: [images.flowId],
|
||||
references: [flows.id],
|
||||
}),
|
||||
apiKey: one(apiKeys, {
|
||||
fields: [images.apiKeyId],
|
||||
references: [apiKeys.id],
|
||||
}),
|
||||
promptUrlCacheEntries: many(promptUrlCache),
|
||||
}));
|
||||
|
||||
export const generationsRelations = relations(generations, ({ one, many }) => ({
|
||||
project: one(projects, {
|
||||
fields: [generations.projectId],
|
||||
references: [projects.id],
|
||||
}),
|
||||
flow: one(flows, {
|
||||
fields: [generations.flowId],
|
||||
references: [flows.id],
|
||||
}),
|
||||
apiKey: one(apiKeys, {
|
||||
fields: [generations.apiKeyId],
|
||||
references: [apiKeys.id],
|
||||
}),
|
||||
outputImage: one(images, {
|
||||
fields: [generations.outputImageId],
|
||||
references: [images.id],
|
||||
}),
|
||||
promptUrlCacheEntries: many(promptUrlCache),
|
||||
}));
|
||||
|
||||
export const promptUrlCacheRelations = relations(promptUrlCache, ({ one }) => ({
|
||||
project: one(projects, {
|
||||
fields: [promptUrlCache.projectId],
|
||||
references: [projects.id],
|
||||
}),
|
||||
generation: one(generations, {
|
||||
fields: [promptUrlCache.generationId],
|
||||
references: [generations.id],
|
||||
}),
|
||||
image: one(images, {
|
||||
fields: [promptUrlCache.imageId],
|
||||
references: [images.id],
|
||||
}),
|
||||
}));
|
||||
|
|
|
|||
|
|
@ -0,0 +1,77 @@
|
|||
import {
|
||||
pgTable,
|
||||
uuid,
|
||||
varchar,
|
||||
text,
|
||||
integer,
|
||||
jsonb,
|
||||
timestamp,
|
||||
index,
|
||||
uniqueIndex,
|
||||
check,
|
||||
} from 'drizzle-orm/pg-core';
|
||||
import { sql } from 'drizzle-orm';
|
||||
import { projects } from './projects';
|
||||
import { generations } from './generations';
|
||||
import { images } from './images';
|
||||
|
||||
export const promptUrlCache = pgTable(
|
||||
'prompt_url_cache',
|
||||
{
|
||||
id: uuid('id').primaryKey().defaultRandom(),
|
||||
|
||||
// Relations
|
||||
projectId: uuid('project_id')
|
||||
.notNull()
|
||||
.references(() => projects.id, { onDelete: 'cascade' }),
|
||||
generationId: uuid('generation_id')
|
||||
.notNull()
|
||||
.references(() => generations.id, { onDelete: 'cascade' }),
|
||||
imageId: uuid('image_id')
|
||||
.notNull()
|
||||
.references(() => images.id, { onDelete: 'cascade' }),
|
||||
|
||||
// Cache keys (SHA-256 hashes)
|
||||
promptHash: varchar('prompt_hash', { length: 64 }).notNull(),
|
||||
queryParamsHash: varchar('query_params_hash', { length: 64 }).notNull(),
|
||||
|
||||
// Original request (for debugging/reconstruction)
|
||||
originalPrompt: text('original_prompt').notNull(),
|
||||
requestParams: jsonb('request_params').$type<Record<string, unknown>>().notNull(),
|
||||
|
||||
// Cache statistics
|
||||
hitCount: integer('hit_count').notNull().default(0),
|
||||
lastHitAt: timestamp('last_hit_at'),
|
||||
|
||||
// Audit
|
||||
createdAt: timestamp('created_at').notNull().defaultNow(),
|
||||
},
|
||||
(table) => ({
|
||||
// CHECK constraints
|
||||
hitCountCheck: check('hit_count_check', sql`${table.hitCount} >= 0`),
|
||||
|
||||
// Indexes
|
||||
// Unique composite index for cache lookup
|
||||
cacheKeyIdx: uniqueIndex('idx_cache_key').on(
|
||||
table.projectId,
|
||||
table.promptHash,
|
||||
table.queryParamsHash,
|
||||
),
|
||||
|
||||
// Index for generation lookup
|
||||
generationIdx: index('idx_cache_generation').on(table.generationId),
|
||||
|
||||
// Index for image lookup
|
||||
imageIdx: index('idx_cache_image').on(table.imageId),
|
||||
|
||||
// Index for cache hit analytics
|
||||
hitsIdx: index('idx_cache_hits').on(
|
||||
table.projectId,
|
||||
table.hitCount.desc(),
|
||||
table.createdAt.desc(),
|
||||
),
|
||||
}),
|
||||
);
|
||||
|
||||
export type PromptUrlCache = typeof promptUrlCache.$inferSelect;
|
||||
export type NewPromptUrlCache = typeof promptUrlCache.$inferInsert;
|
||||
Loading…
Reference in New Issue