Initial commit

This commit is contained in:
Zhongwei Li
2025-11-30 08:43:11 +08:00
commit 5cf0559508
28 changed files with 5938 additions and 0 deletions

View File

@@ -0,0 +1,6 @@
import { drizzle } from 'drizzle-orm/neon-http';
import { neon } from '@neondatabase/serverless';
const sql = neon(process.env.DATABASE_URL!);
export const db = drizzle(sql);

View File

@@ -0,0 +1,24 @@
import { drizzle } from 'drizzle-orm/neon-serverless';
import { Pool, neonConfig } from '@neondatabase/serverless';
import ws from 'ws';
neonConfig.webSocketConstructor = ws;
const pool = new Pool({
connectionString: process.env.DATABASE_URL!,
max: 10,
idleTimeoutMillis: 30000,
connectionTimeoutMillis: 5000,
});
export const db = drizzle(pool);
process.on('SIGTERM', async () => {
await pool.end();
process.exit(0);
});
process.on('SIGINT', async () => {
await pool.end();
process.exit(0);
});

View File

@@ -0,0 +1,96 @@
/**
* Drizzle Configuration
*
* This file configures Drizzle ORM for use with Neon.
* Place this in your project root or src/ directory.
*
* Usage: Reference this in your drizzle.config.ts
*/
import { config } from 'dotenv';
import type { Config } from 'drizzle-kit';
config({ path: '.env.local' });
/**
* Drizzle Configuration for Neon Postgres
*
* Supports both HTTP and WebSocket connections.
* Automatically detects which driver to use based on environment.
*/
const dbUrl = process.env.DATABASE_URL;
if (!dbUrl) {
throw new Error('DATABASE_URL environment variable is not set');
}
// Determine connection type based on environment
const isServerless = process.env.RUNTIME === 'edge' ||
process.env.VERCEL_ENV === 'production';
export default {
schema: './src/db/schema.ts', // Path to your schema file
out: './src/db/migrations', // Output directory for migrations
// Database connection
dbCredentials: {
url: dbUrl,
},
// Migration options
migrations: {
prefix: 'timestamp', // or 'none'
},
// Verbose logging for debugging
verbose: process.env.DEBUG === 'true',
// Strict mode ensures all migrations are applied
strict: true,
} satisfies Config;
/**
* HTTP Connection Configuration (for Vercel Edge, etc.)
*
* export default {
* schema: './src/db/schema.ts',
* out: './src/db/migrations',
* driver: 'postgres',
* dbCredentials: {
* url: process.env.DATABASE_URL!,
* },
* } satisfies Config;
*/
/**
* WebSocket Connection Configuration (for Node.js servers)
*
* export default {
* schema: './src/db/schema.ts',
* out: './src/db/migrations',
* driver: 'pg',
* dbCredentials: {
* url: process.env.DATABASE_URL!,
* },
* } satisfies Config;
*/
/**
* Migration Commands
*
* # Generate migration files from schema changes
* npx drizzle-kit generate
*
* # Apply migrations to database
* npx drizzle-kit migrate
*
* # Drop all tables (careful!)
* npx drizzle-kit drop
*
* # Introspect existing database
* npx drizzle-kit introspect
*
* # Push schema changes directly (development only)
* npx drizzle-kit push
*/

View File

@@ -0,0 +1,231 @@
/**
* Drizzle Schema Example
*
* This file demonstrates how to define database tables and relationships
* using Drizzle ORM with Neon Postgres.
*
* Usage: Import these tables in your application code for type-safe queries
*/
import {
pgTable,
serial,
text,
varchar,
integer,
timestamp,
boolean,
decimal,
json,
index,
unique,
foreignKey,
} from 'drizzle-orm/pg-core';
import { relations } from 'drizzle-orm';
/**
* Users Table
*
* Stores basic user information. Can be extended with additional fields
* as needed by your application.
*/
export const users = pgTable(
'users',
{
id: serial('id').primaryKey(),
email: varchar('email', { length: 255 }).notNull().unique(),
name: varchar('name', { length: 255 }).notNull(),
password: text('password'), // If not using external auth
avatar: text('avatar'), // URL to avatar image
isActive: boolean('is_active').default(true),
createdAt: timestamp('created_at').defaultNow(),
updatedAt: timestamp('updated_at').defaultNow(),
},
(table) => ({
emailIdx: index('users_email_idx').on(table.email),
createdAtIdx: index('users_created_at_idx').on(table.createdAt),
})
);
/**
* Profiles Table
*
* Extended user information. Uses a foreign key to link with users.
*/
export const profiles = pgTable('profiles', {
id: serial('id').primaryKey(),
userId: integer('user_id')
.notNull()
.references(() => users.id, { onDelete: 'cascade' }),
bio: text('bio'),
location: varchar('location', { length: 255 }),
website: varchar('website', { length: 255 }),
phone: varchar('phone', { length: 20 }),
createdAt: timestamp('created_at').defaultNow(),
updatedAt: timestamp('updated_at').defaultNow(),
});
/**
* Posts Table
*
* Blog posts created by users.
*/
export const posts = pgTable(
'posts',
{
id: serial('id').primaryKey(),
userId: integer('user_id')
.notNull()
.references(() => users.id, { onDelete: 'cascade' }),
title: varchar('title', { length: 255 }).notNull(),
slug: varchar('slug', { length: 255 }).notNull().unique(),
content: text('content').notNull(),
excerpt: text('excerpt'),
published: boolean('published').default(false),
publishedAt: timestamp('published_at'),
createdAt: timestamp('created_at').defaultNow(),
updatedAt: timestamp('updated_at').defaultNow(),
},
(table) => ({
userIdIdx: index('posts_user_id_idx').on(table.userId),
publishedIdx: index('posts_published_idx').on(table.published),
slugIdx: index('posts_slug_idx').on(table.slug),
})
);
/**
* Comments Table
*
* Comments on blog posts. Supports nested comments via parent_id.
*/
export const comments = pgTable(
'comments',
{
id: serial('id').primaryKey(),
postId: integer('post_id')
.notNull()
.references(() => posts.id, { onDelete: 'cascade' }),
userId: integer('user_id')
.notNull()
.references(() => users.id, { onDelete: 'cascade' }),
parentId: integer('parent_id').references(() => comments.id, {
onDelete: 'cascade',
}),
content: text('content').notNull(),
approved: boolean('approved').default(false),
createdAt: timestamp('created_at').defaultNow(),
updatedAt: timestamp('updated_at').defaultNow(),
},
(table) => ({
postIdIdx: index('comments_post_id_idx').on(table.postId),
userIdIdx: index('comments_user_id_idx').on(table.userId),
parentIdIdx: index('comments_parent_id_idx').on(table.parentId),
})
);
/**
* Tags Table
*
* Tags for categorizing posts.
*/
export const tags = pgTable('tags', {
id: serial('id').primaryKey(),
name: varchar('name', { length: 100 }).notNull().unique(),
slug: varchar('slug', { length: 100 }).notNull().unique(),
createdAt: timestamp('created_at').defaultNow(),
});
/**
* PostTags Junction Table
*
* Many-to-many relationship between posts and tags.
*/
export const postTags = pgTable(
'post_tags',
{
postId: integer('post_id')
.notNull()
.references(() => posts.id, { onDelete: 'cascade' }),
tagId: integer('tag_id')
.notNull()
.references(() => tags.id, { onDelete: 'cascade' }),
},
(table) => ({
pk: { name: 'post_tags_pk', columns: [table.postId, table.tagId] },
postIdIdx: index('post_tags_post_id_idx').on(table.postId),
tagIdIdx: index('post_tags_tag_id_idx').on(table.tagId),
})
);
/**
* Settings Table
*
* Application-wide or user-specific settings stored as JSON.
*/
export const settings = pgTable('settings', {
id: serial('id').primaryKey(),
userId: integer('user_id').references(() => users.id, {
onDelete: 'cascade',
}), // null = global settings
key: varchar('key', { length: 255 }).notNull(),
value: json('value'),
createdAt: timestamp('created_at').defaultNow(),
updatedAt: timestamp('updated_at').defaultNow(),
});
// ============================================================================
// Relations (optional but recommended for better type safety)
// ============================================================================
export const usersRelations = relations(users, ({ many, one }) => ({
profile: one(profiles),
posts: many(posts),
comments: many(comments),
}));
export const profilesRelations = relations(profiles, ({ one }) => ({
user: one(users, {
fields: [profiles.userId],
references: [users.id],
}),
}));
export const postsRelations = relations(posts, ({ one, many }) => ({
author: one(users, {
fields: [posts.userId],
references: [users.id],
}),
comments: many(comments),
tags: many(postTags),
}));
export const commentsRelations = relations(comments, ({ one, many }) => ({
post: one(posts, {
fields: [comments.postId],
references: [posts.id],
}),
author: one(users, {
fields: [comments.userId],
references: [users.id],
}),
parent: one(comments, {
fields: [comments.parentId],
references: [comments.id],
}),
replies: many(comments),
}));
export const tagsRelations = relations(tags, ({ many }) => ({
posts: many(postTags),
}));
export const postTagsRelations = relations(postTags, ({ one }) => ({
post: one(posts, {
fields: [postTags.postId],
references: [posts.id],
}),
tag: one(tags, {
fields: [postTags.tagId],
references: [tags.id],
}),
}));