diff --git a/drizzle.config.ts b/drizzle.config.ts new file mode 100644 index 0000000..b36e054 --- /dev/null +++ b/drizzle.config.ts @@ -0,0 +1,10 @@ +import type { Config } from 'drizzle-kit'; + +export default { + schema: './src/db/schema.ts', + out: './drizzle', + driver: 'libsql', + dbCredentials: { + url: process.env.DATABASE_URL || 'file:./data.db', + }, +} satisfies Config; diff --git a/package.json b/package.json index b81620f..ea61fbf 100644 --- a/package.json +++ b/package.json @@ -17,7 +17,12 @@ "audit:accessibility": "node scripts/accessibility-test.js", "audit:forms": "node scripts/form-validation.js", "audit:all": "./scripts/run-all-tests.sh", - "report:generate": "node scripts/generate-test-report.js" + "report:generate": "node scripts/generate-test-report.js", + "db:generate": "drizzle-kit generate", + "db:migrate": "drizzle-kit migrate", + "db:push": "drizzle-kit push", + "db:studio": "drizzle-kit studio", + "db:seed": "tsx src/db/seed.ts" }, "dependencies": { "@antv/g2": "^5.4.8", diff --git a/src/db/schema.ts b/src/db/schema.ts new file mode 100644 index 0000000..7c8ed15 --- /dev/null +++ b/src/db/schema.ts @@ -0,0 +1,86 @@ +import { sqliteTable, text, integer } from 'drizzle-orm/sqlite-core'; +import { relations } from 'drizzle-orm'; + +export const users = sqliteTable('users', { + id: text('id').primaryKey(), + email: text('email').notNull().unique(), + passwordHash: text('password_hash'), + name: text('name').notNull(), + role: text('role', { enum: ['admin', 'editor', 'viewer'] }).notNull().default('editor'), + avatar: text('avatar'), + createdAt: integer('created_at', { mode: 'timestamp' }).notNull(), + updatedAt: integer('updated_at', { mode: 'timestamp' }).notNull(), +}); + +export const content = sqliteTable('content', { + id: text('id').primaryKey(), + type: text('type', { enum: ['news', 'product', 'service', 'case'] }).notNull(), + title: text('title').notNull(), + slug: text('slug').notNull().unique(), + excerpt: text('excerpt'), + content: text('content').notNull(), + coverImage: text('cover_image'), + category: text('category'), + tags: text('tags', { mode: 'json' }).$type(), + status: text('status', { enum: ['draft', 'published', 'archived'] }).notNull().default('draft'), + publishedAt: integer('published_at', { mode: 'timestamp' }), + authorId: text('author_id').notNull().references(() => users.id), + sortOrder: integer('sort_order').default(0), + metadata: text('metadata', { mode: 'json' }).$type>(), + createdAt: integer('created_at', { mode: 'timestamp' }).notNull(), + updatedAt: integer('updated_at', { mode: 'timestamp' }).notNull(), +}); + +export const contentVersions = sqliteTable('content_versions', { + id: text('id').primaryKey(), + contentId: text('content_id').notNull().references(() => content.id), + version: integer('version').notNull(), + title: text('title').notNull(), + content: text('content').notNull(), + changes: text('changes', { mode: 'json' }).$type>(), + changedBy: text('changed_by').notNull().references(() => users.id), + changedAt: integer('changed_at', { mode: 'timestamp' }).notNull(), +}); + +export const siteConfig = sqliteTable('site_config', { + id: text('id').primaryKey(), + key: text('key').notNull().unique(), + value: text('value', { mode: 'json' }).notNull(), + category: text('category', { enum: ['feature', 'style', 'seo', 'general'] }).notNull(), + description: text('description'), + updatedAt: integer('updated_at', { mode: 'timestamp' }).notNull(), + updatedBy: text('updated_by').references(() => users.id), +}); + +export const auditLogs = sqliteTable('audit_logs', { + id: text('id').primaryKey(), + userId: text('user_id').references(() => users.id), + action: text('action', { enum: ['create', 'update', 'delete', 'publish', 'login'] }).notNull(), + resourceType: text('resource_type').notNull(), + resourceId: text('resource_id'), + details: text('details', { mode: 'json' }).$type>(), + ipAddress: text('ip_address'), + userAgent: text('user_agent'), + timestamp: integer('timestamp', { mode: 'timestamp' }).notNull(), +}); + +export const usersRelations = relations(users, ({ many }) => ({ + content: many(content), + versions: many(contentVersions), + logs: many(auditLogs), +})); + +export const contentRelations = relations(content, ({ one, many }) => ({ + author: one(users, { + fields: [content.authorId], + references: [users.id], + }), + versions: many(contentVersions), +})); + +export type User = typeof users.$inferSelect; +export type NewUser = typeof users.$inferInsert; +export type Content = typeof content.$inferSelect; +export type NewContent = typeof content.$inferInsert; +export type SiteConfig = typeof siteConfig.$inferSelect; +export type NewSiteConfig = typeof siteConfig.$inferInsert;