377 lines
13 KiB
TypeScript
377 lines
13 KiB
TypeScript
import {
|
|
pgTable,
|
|
pgEnum,
|
|
uuid,
|
|
text,
|
|
integer,
|
|
boolean,
|
|
timestamp,
|
|
jsonb,
|
|
real,
|
|
uniqueIndex,
|
|
index,
|
|
} from 'drizzle-orm/pg-core';
|
|
import { relations } from 'drizzle-orm';
|
|
|
|
// ── Enums ──────────────────────────────────────────────────────────────────────
|
|
|
|
export const userTierEnum = pgEnum('user_tier', [
|
|
'free',
|
|
'pro',
|
|
'team',
|
|
'enterprise',
|
|
]);
|
|
|
|
export const projectStatusEnum = pgEnum('project_status', [
|
|
'draft',
|
|
'analyzed',
|
|
'generated',
|
|
'tested',
|
|
'deployed',
|
|
]);
|
|
|
|
export const deploymentStatusEnum = pgEnum('deployment_status', [
|
|
'pending',
|
|
'building',
|
|
'live',
|
|
'failed',
|
|
'stopped',
|
|
]);
|
|
|
|
export const listingStatusEnum = pgEnum('listing_status', [
|
|
'review',
|
|
'published',
|
|
'rejected',
|
|
'archived',
|
|
]);
|
|
|
|
// ── Teams ──────────────────────────────────────────────────────────────────────
|
|
|
|
export const teams = pgTable(
|
|
'teams',
|
|
{
|
|
id: uuid('id').primaryKey().defaultRandom(),
|
|
name: text('name').notNull(),
|
|
slug: text('slug').notNull(),
|
|
ownerId: uuid('owner_id'), // FK to users.id (circular ref handled in relations)
|
|
tier: userTierEnum('tier').default('team').notNull(),
|
|
stripeSubscriptionId: text('stripe_subscription_id'),
|
|
maxSeats: integer('max_seats').default(5).notNull(),
|
|
createdAt: timestamp('created_at', { withTimezone: true })
|
|
.defaultNow()
|
|
.notNull(),
|
|
},
|
|
(t) => [
|
|
uniqueIndex('teams_slug_idx').on(t.slug),
|
|
],
|
|
);
|
|
|
|
// ── Users ──────────────────────────────────────────────────────────────────────
|
|
|
|
export const users = pgTable(
|
|
'users',
|
|
{
|
|
id: uuid('id').primaryKey().defaultRandom(),
|
|
clerkId: text('clerk_id').notNull(),
|
|
email: text('email').notNull(),
|
|
name: text('name'),
|
|
avatarUrl: text('avatar_url'),
|
|
tier: userTierEnum('tier').default('free').notNull(),
|
|
stripeCustomerId: text('stripe_customer_id'),
|
|
teamId: uuid('team_id').references(() => teams.id),
|
|
createdAt: timestamp('created_at', { withTimezone: true })
|
|
.defaultNow()
|
|
.notNull(),
|
|
updatedAt: timestamp('updated_at', { withTimezone: true })
|
|
.defaultNow()
|
|
.notNull(),
|
|
},
|
|
(t) => [
|
|
uniqueIndex('users_clerk_id_idx').on(t.clerkId),
|
|
uniqueIndex('users_email_idx').on(t.email),
|
|
index('users_team_id_idx').on(t.teamId),
|
|
],
|
|
);
|
|
|
|
// ── Projects ───────────────────────────────────────────────────────────────────
|
|
|
|
export const projects = pgTable(
|
|
'projects',
|
|
{
|
|
id: uuid('id').primaryKey().defaultRandom(),
|
|
userId: uuid('user_id')
|
|
.references(() => users.id)
|
|
.notNull(),
|
|
teamId: uuid('team_id').references(() => teams.id),
|
|
name: text('name').notNull(),
|
|
slug: text('slug').notNull(),
|
|
description: text('description'),
|
|
status: projectStatusEnum('status').default('draft').notNull(),
|
|
specUrl: text('spec_url'),
|
|
specRaw: jsonb('spec_raw'),
|
|
analysis: jsonb('analysis'),
|
|
toolConfig: jsonb('tool_config'),
|
|
appConfig: jsonb('app_config'),
|
|
authConfig: jsonb('auth_config'),
|
|
serverBundle: jsonb('server_bundle'),
|
|
templateId: uuid('template_id'), // FK to marketplace_listings (circular ref handled in relations)
|
|
createdAt: timestamp('created_at', { withTimezone: true })
|
|
.defaultNow()
|
|
.notNull(),
|
|
updatedAt: timestamp('updated_at', { withTimezone: true })
|
|
.defaultNow()
|
|
.notNull(),
|
|
},
|
|
(t) => [
|
|
uniqueIndex('projects_user_slug_idx').on(t.userId, t.slug),
|
|
index('projects_user_id_idx').on(t.userId),
|
|
index('projects_team_id_idx').on(t.teamId),
|
|
index('projects_status_idx').on(t.status),
|
|
index('projects_template_id_idx').on(t.templateId),
|
|
],
|
|
);
|
|
|
|
// ── Tools ──────────────────────────────────────────────────────────────────────
|
|
|
|
export const tools = pgTable(
|
|
'tools',
|
|
{
|
|
id: uuid('id').primaryKey().defaultRandom(),
|
|
projectId: uuid('project_id')
|
|
.references(() => projects.id, { onDelete: 'cascade' })
|
|
.notNull(),
|
|
name: text('name').notNull(),
|
|
description: text('description'),
|
|
groupName: text('group_name'),
|
|
inputSchema: jsonb('input_schema'),
|
|
outputSchema: jsonb('output_schema'),
|
|
annotations: jsonb('annotations'),
|
|
enabled: boolean('enabled').default(true).notNull(),
|
|
position: integer('position').default(0).notNull(),
|
|
canvasX: real('canvas_x'),
|
|
canvasY: real('canvas_y'),
|
|
createdAt: timestamp('created_at', { withTimezone: true })
|
|
.defaultNow()
|
|
.notNull(),
|
|
},
|
|
(t) => [
|
|
index('tools_project_id_idx').on(t.projectId),
|
|
index('tools_group_name_idx').on(t.groupName),
|
|
],
|
|
);
|
|
|
|
// ── Apps ────────────────────────────────────────────────────────────────────────
|
|
|
|
export const apps = pgTable(
|
|
'apps',
|
|
{
|
|
id: uuid('id').primaryKey().defaultRandom(),
|
|
projectId: uuid('project_id')
|
|
.references(() => projects.id, { onDelete: 'cascade' })
|
|
.notNull(),
|
|
name: text('name').notNull(),
|
|
pattern: text('pattern'),
|
|
layoutConfig: jsonb('layout_config'),
|
|
htmlBundle: text('html_bundle'),
|
|
toolBindings: jsonb('tool_bindings'),
|
|
createdAt: timestamp('created_at', { withTimezone: true })
|
|
.defaultNow()
|
|
.notNull(),
|
|
},
|
|
(t) => [
|
|
index('apps_project_id_idx').on(t.projectId),
|
|
],
|
|
);
|
|
|
|
// ── Deployments ────────────────────────────────────────────────────────────────
|
|
|
|
export const deployments = pgTable(
|
|
'deployments',
|
|
{
|
|
id: uuid('id').primaryKey().defaultRandom(),
|
|
projectId: uuid('project_id')
|
|
.references(() => projects.id)
|
|
.notNull(),
|
|
userId: uuid('user_id')
|
|
.references(() => users.id)
|
|
.notNull(),
|
|
target: text('target').notNull(),
|
|
status: deploymentStatusEnum('status').default('pending').notNull(),
|
|
url: text('url'),
|
|
endpoint: text('endpoint'),
|
|
workerId: text('worker_id'),
|
|
version: text('version'),
|
|
logs: jsonb('logs'),
|
|
createdAt: timestamp('created_at', { withTimezone: true })
|
|
.defaultNow()
|
|
.notNull(),
|
|
stoppedAt: timestamp('stopped_at', { withTimezone: true }),
|
|
},
|
|
(t) => [
|
|
index('deployments_project_id_idx').on(t.projectId),
|
|
index('deployments_user_id_idx').on(t.userId),
|
|
index('deployments_status_idx').on(t.status),
|
|
],
|
|
);
|
|
|
|
// ── Marketplace Listings ───────────────────────────────────────────────────────
|
|
|
|
export const marketplaceListings = pgTable(
|
|
'marketplace_listings',
|
|
{
|
|
id: uuid('id').primaryKey().defaultRandom(),
|
|
projectId: uuid('project_id').references(() => projects.id),
|
|
authorId: uuid('author_id').references(() => users.id),
|
|
name: text('name').notNull(),
|
|
slug: text('slug').notNull(),
|
|
description: text('description'),
|
|
category: text('category'),
|
|
tags: text('tags').array(),
|
|
iconUrl: text('icon_url'),
|
|
previewUrl: text('preview_url'),
|
|
toolCount: integer('tool_count').default(0).notNull(),
|
|
appCount: integer('app_count').default(0).notNull(),
|
|
forkCount: integer('fork_count').default(0).notNull(),
|
|
isOfficial: boolean('is_official').default(false).notNull(),
|
|
isFeatured: boolean('is_featured').default(false).notNull(),
|
|
priceCents: integer('price_cents').default(0).notNull(),
|
|
status: listingStatusEnum('status').default('review').notNull(),
|
|
createdAt: timestamp('created_at', { withTimezone: true })
|
|
.defaultNow()
|
|
.notNull(),
|
|
publishedAt: timestamp('published_at', { withTimezone: true }),
|
|
},
|
|
(t) => [
|
|
uniqueIndex('marketplace_slug_idx').on(t.slug),
|
|
index('marketplace_category_idx').on(t.category),
|
|
index('marketplace_status_idx').on(t.status),
|
|
index('marketplace_is_official_idx').on(t.isOfficial),
|
|
index('marketplace_is_featured_idx').on(t.isFeatured),
|
|
index('marketplace_author_id_idx').on(t.authorId),
|
|
],
|
|
);
|
|
|
|
// ── Usage Logs ─────────────────────────────────────────────────────────────────
|
|
|
|
export const usageLogs = pgTable(
|
|
'usage_logs',
|
|
{
|
|
id: uuid('id').primaryKey().defaultRandom(),
|
|
userId: uuid('user_id')
|
|
.references(() => users.id)
|
|
.notNull(),
|
|
action: text('action').notNull(),
|
|
projectId: uuid('project_id').references(() => projects.id),
|
|
tokensUsed: integer('tokens_used').default(0),
|
|
durationMs: integer('duration_ms').default(0),
|
|
createdAt: timestamp('created_at', { withTimezone: true })
|
|
.defaultNow()
|
|
.notNull(),
|
|
},
|
|
(t) => [
|
|
index('usage_logs_user_id_idx').on(t.userId),
|
|
index('usage_logs_project_id_idx').on(t.projectId),
|
|
index('usage_logs_action_idx').on(t.action),
|
|
index('usage_logs_created_at_idx').on(t.createdAt),
|
|
],
|
|
);
|
|
|
|
// ── API Keys ───────────────────────────────────────────────────────────────────
|
|
|
|
export const apiKeys = pgTable(
|
|
'api_keys',
|
|
{
|
|
id: uuid('id').primaryKey().defaultRandom(),
|
|
projectId: uuid('project_id')
|
|
.references(() => projects.id, { onDelete: 'cascade' })
|
|
.notNull(),
|
|
userId: uuid('user_id')
|
|
.references(() => users.id)
|
|
.notNull(),
|
|
keyName: text('key_name').notNull(),
|
|
encryptedValue: text('encrypted_value').notNull(),
|
|
createdAt: timestamp('created_at', { withTimezone: true })
|
|
.defaultNow()
|
|
.notNull(),
|
|
},
|
|
(t) => [
|
|
index('api_keys_project_id_idx').on(t.projectId),
|
|
index('api_keys_user_id_idx').on(t.userId),
|
|
],
|
|
);
|
|
|
|
// ── Relations ──────────────────────────────────────────────────────────────────
|
|
|
|
export const usersRelations = relations(users, ({ one, many }) => ({
|
|
team: one(teams, { fields: [users.teamId], references: [teams.id] }),
|
|
projects: many(projects),
|
|
deployments: many(deployments),
|
|
usageLogs: many(usageLogs),
|
|
apiKeys: many(apiKeys),
|
|
}));
|
|
|
|
export const teamsRelations = relations(teams, ({ one, many }) => ({
|
|
owner: one(users, { fields: [teams.ownerId], references: [users.id] }),
|
|
members: many(users),
|
|
projects: many(projects),
|
|
}));
|
|
|
|
export const projectsRelations = relations(projects, ({ one, many }) => ({
|
|
user: one(users, { fields: [projects.userId], references: [users.id] }),
|
|
team: one(teams, { fields: [projects.teamId], references: [teams.id] }),
|
|
template: one(marketplaceListings, {
|
|
fields: [projects.templateId],
|
|
references: [marketplaceListings.id],
|
|
}),
|
|
tools: many(tools),
|
|
apps: many(apps),
|
|
deployments: many(deployments),
|
|
apiKeys: many(apiKeys),
|
|
}));
|
|
|
|
export const toolsRelations = relations(tools, ({ one }) => ({
|
|
project: one(projects, { fields: [tools.projectId], references: [projects.id] }),
|
|
}));
|
|
|
|
export const appsRelations = relations(apps, ({ one }) => ({
|
|
project: one(projects, { fields: [apps.projectId], references: [projects.id] }),
|
|
}));
|
|
|
|
export const deploymentsRelations = relations(deployments, ({ one }) => ({
|
|
project: one(projects, {
|
|
fields: [deployments.projectId],
|
|
references: [projects.id],
|
|
}),
|
|
user: one(users, { fields: [deployments.userId], references: [users.id] }),
|
|
}));
|
|
|
|
export const marketplaceListingsRelations = relations(
|
|
marketplaceListings,
|
|
({ one, many }) => ({
|
|
project: one(projects, {
|
|
fields: [marketplaceListings.projectId],
|
|
references: [projects.id],
|
|
}),
|
|
author: one(users, {
|
|
fields: [marketplaceListings.authorId],
|
|
references: [users.id],
|
|
}),
|
|
}),
|
|
);
|
|
|
|
export const usageLogsRelations = relations(usageLogs, ({ one }) => ({
|
|
user: one(users, { fields: [usageLogs.userId], references: [users.id] }),
|
|
project: one(projects, {
|
|
fields: [usageLogs.projectId],
|
|
references: [projects.id],
|
|
}),
|
|
}));
|
|
|
|
export const apiKeysRelations = relations(apiKeys, ({ one }) => ({
|
|
project: one(projects, {
|
|
fields: [apiKeys.projectId],
|
|
references: [projects.id],
|
|
}),
|
|
user: one(users, { fields: [apiKeys.userId], references: [users.id] }),
|
|
}));
|