134 lines
7.4 KiB
TypeScript
134 lines
7.4 KiB
TypeScript
import { pgTable, text, integer, bigint, boolean, unique, index, primaryKey, AnyPgColumn } from 'drizzle-orm/pg-core';
|
|
import { sql } from 'drizzle-orm';
|
|
|
|
export const ENTITIES = ['categories', 'tasks', 'subtasks', 'repeatProfiles', 'friendships'] as const;
|
|
export type EntityName = (typeof ENTITIES)[number];
|
|
|
|
export const users = pgTable('users', {
|
|
id: text('id').primaryKey(),
|
|
username: text('username').notNull().unique(),
|
|
passwordHash: text('password_hash').notNull(),
|
|
resetToken: text('reset_token'),
|
|
resetTokenExpiry: bigint('reset_token_expiry', { mode: 'number' }),
|
|
createdAt: bigint('created_at', { mode: 'number' }).notNull().default(sql`EXTRACT(EPOCH FROM NOW()) * 1000`),
|
|
});
|
|
|
|
export const friendships = pgTable(
|
|
'friendships',
|
|
{
|
|
id: text('id').primaryKey(),
|
|
userId: text('user_id').notNull().references(() => users.id, { onDelete: 'cascade' }),
|
|
friendId: text('friend_id').notNull().references(() => users.id, { onDelete: 'cascade' }),
|
|
status: text('status', { enum: ['pending', 'accepted'] }).notNull().default('pending'),
|
|
createdAt: bigint('created_at', { mode: 'number' }).notNull().default(sql`EXTRACT(EPOCH FROM NOW()) * 1000`),
|
|
updatedAt: bigint('updated_at', { mode: 'number' }).notNull().default(sql`EXTRACT(EPOCH FROM NOW()) * 1000`),
|
|
},
|
|
(t) => ({
|
|
pairUnique: unique('friendships_pair').on(t.userId, t.friendId),
|
|
userIdx: index('friendships_user_idx').on(t.userId),
|
|
friendIdx: index('friendships_friend_idx').on(t.friendId),
|
|
})
|
|
);
|
|
|
|
export const categories = pgTable('categories', {
|
|
id: text('id').primaryKey(),
|
|
userId: text('user_id').notNull().references(() => users.id, { onDelete: 'cascade' }),
|
|
name: text('name').notNull(),
|
|
color: text('color').notNull(),
|
|
order: integer('order').notNull().default(0),
|
|
createdAt: bigint('created_at', { mode: 'number' }).notNull().default(sql`EXTRACT(EPOCH FROM NOW()) * 1000`),
|
|
updatedAt: bigint('updated_at', { mode: 'number' }).notNull().default(sql`EXTRACT(EPOCH FROM NOW()) * 1000`),
|
|
});
|
|
|
|
export const tasks = pgTable('tasks', {
|
|
id: text('id').primaryKey(),
|
|
userId: text('user_id').notNull().references(() => users.id, { onDelete: 'cascade' }),
|
|
categoryId: text('category_id').references(() => categories.id, { onDelete: 'cascade' }),
|
|
tags: text('tags').notNull().default(''),
|
|
title: text('title').notNull(),
|
|
description: text('description').notNull().default(''),
|
|
priority: text('priority', { enum: ['none', 'low', 'medium', 'high', 'critical'] }).notNull().default('none'),
|
|
completed: boolean('completed').notNull().default(false),
|
|
dueDate: bigint('due_date', { mode: 'number' }).notNull().default(0),
|
|
dueTime: text('due_time').notNull().default(''),
|
|
endTime: text('end_time').notNull().default(''),
|
|
allDay: boolean('all_day').notNull().default(false),
|
|
repeat: text('repeat', { enum: ['none', 'daily', 'weekly', 'monthly', 'custom'] }).notNull().default('none'),
|
|
repeatInterval: integer('repeat_interval').notNull().default(1),
|
|
repeatDays: text('repeat_days').notNull().default(''),
|
|
seriesId: text('series_id').notNull().default(''),
|
|
assigneeId: text('assignee_id').references(() => users.id, { onDelete: 'set null' }),
|
|
reminder: text('reminder', { enum: ['none', 'at_time', '15', '30', '60', '120', '1440'] }).notNull().default('none'),
|
|
reminders: text('reminders').notNull().default(''),
|
|
completedAt: bigint('completed_at', { mode: 'number' }),
|
|
createdAt: bigint('created_at', { mode: 'number' }).notNull().default(sql`EXTRACT(EPOCH FROM NOW()) * 1000`),
|
|
updatedAt: bigint('updated_at', { mode: 'number' }).notNull().default(sql`EXTRACT(EPOCH FROM NOW()) * 1000`),
|
|
});
|
|
|
|
export const tombstones = pgTable(
|
|
'tombstones',
|
|
{
|
|
entity: text('entity', { enum: ENTITIES }).notNull(),
|
|
entityId: text('entity_id').notNull(),
|
|
userId: text('user_id').notNull().references(() => users.id, { onDelete: 'cascade' }),
|
|
updatedAt: bigint('updated_at', { mode: 'number' }).notNull(),
|
|
},
|
|
(t) => ({
|
|
pk: primaryKey({ columns: [t.entity, t.entityId] }),
|
|
userIdx: index('tombstones_user_updated_idx').on(t.userId, t.updatedAt),
|
|
})
|
|
);
|
|
|
|
// Define subtasks with explicit type to avoid circular reference
|
|
export const subtasks = pgTable('subtasks', {
|
|
id: text('id').primaryKey(),
|
|
userId: text('user_id').notNull().references(() => users.id, { onDelete: 'cascade' }),
|
|
taskId: text('task_id').notNull().references(() => tasks.id, { onDelete: 'cascade' }),
|
|
parentSubtaskId: text('parent_subtask_id').references((): AnyPgColumn => subtasks.id, { onDelete: 'cascade' }),
|
|
categoryId: text('category_id').references(() => categories.id, { onDelete: 'set null' }),
|
|
title: text('title').notNull(),
|
|
description: text('description').notNull().default(''),
|
|
priority: text('priority', { enum: ['none', 'low', 'medium', 'high', 'critical'] }).notNull().default('none'),
|
|
completed: boolean('completed').notNull().default(false),
|
|
dueDate: bigint('due_date', { mode: 'number' }).notNull().default(0),
|
|
dueTime: text('due_time').notNull().default(''),
|
|
endTime: text('end_time').notNull().default(''),
|
|
allDay: boolean('all_day').notNull().default(false),
|
|
repeat: text('repeat', { enum: ['none', 'daily', 'weekly', 'monthly', 'custom'] }).notNull().default('none'),
|
|
repeatInterval: integer('repeat_interval').notNull().default(1),
|
|
repeatDays: text('repeat_days').notNull().default(''),
|
|
seriesId: text('series_id').notNull().default(''),
|
|
reminder: text('reminder', { enum: ['none', 'at_time', '15', '30', '60', '120', '1440'] }).notNull().default('none'),
|
|
reminders: text('reminders').notNull().default(''),
|
|
assigneeId: text('assignee_id').references(() => users.id, { onDelete: 'set null' }),
|
|
order: integer('order').notNull().default(0),
|
|
createdAt: bigint('created_at', { mode: 'number' }).notNull().default(sql`EXTRACT(EPOCH FROM NOW()) * 1000`),
|
|
updatedAt: bigint('updated_at', { mode: 'number' }).notNull().default(sql`EXTRACT(EPOCH FROM NOW()) * 1000`),
|
|
}, (t) => ({
|
|
parentIdx: index('subtasks_parent_idx').on(t.parentSubtaskId),
|
|
taskIdx: index('subtasks_task_idx').on(t.taskId),
|
|
userIdx: index('subtasks_user_idx').on(t.userId),
|
|
}));
|
|
|
|
export const repeatProfiles = pgTable('repeat_profiles', {
|
|
id: text('id').primaryKey(),
|
|
userId: text('user_id').notNull().references(() => users.id, { onDelete: 'cascade' }),
|
|
name: text('name').notNull(),
|
|
repeat: text('repeat', { enum: ['none', 'daily', 'weekly', 'monthly', 'custom'] }).notNull().default('none'),
|
|
repeatInterval: integer('repeat_interval').notNull().default(1),
|
|
repeatDays: text('repeat_days').notNull().default(''),
|
|
createdAt: bigint('created_at', { mode: 'number' }).notNull().default(sql`EXTRACT(EPOCH FROM NOW()) * 1000`),
|
|
updatedAt: bigint('updated_at', { mode: 'number' }).notNull().default(sql`EXTRACT(EPOCH FROM NOW()) * 1000`),
|
|
});
|
|
|
|
export const userSettings = pgTable('user_settings', {
|
|
userId: text('user_id').primaryKey().references(() => users.id, { onDelete: 'cascade' }),
|
|
darkMode: boolean('dark_mode').notNull().default(false),
|
|
notifications: boolean('notifications').notNull().default(true),
|
|
reminderTime: text('reminder_time').notNull().default('09:00'),
|
|
defaultCategory: text('default_category'),
|
|
sortBy: text('sort_by', { enum: ['dueDate', 'priority', 'title', 'createdAt'] }).notNull().default('dueDate'),
|
|
sortOrder: text('sort_order', { enum: ['asc', 'desc'] }).notNull().default('asc'),
|
|
updatedAt: bigint('updated_at', { mode: 'number' }).notNull().default(sql`EXTRACT(EPOCH FROM NOW()) * 1000`),
|
|
});
|