feat: initialize SQLite with versioned migrations and sample content
This commit is contained in:
2
.gitignore
vendored
2
.gitignore
vendored
@@ -47,3 +47,5 @@ Desktop.ini
|
||||
/.deploy.lock
|
||||
/current
|
||||
/releases/
|
||||
/data/
|
||||
/backups/
|
||||
|
||||
2
drizzle.config.ts
Normal file
2
drizzle.config.ts
Normal file
@@ -0,0 +1,2 @@
|
||||
import { defineConfig } from 'drizzle-kit';
|
||||
export default defineConfig({ dialect: 'sqlite', schema: './src/server/schema.ts', out: './migrations' });
|
||||
88
migrations/0000_clever_vision.sql
Normal file
88
migrations/0000_clever_vision.sql
Normal file
@@ -0,0 +1,88 @@
|
||||
CREATE TABLE `account` (
|
||||
`id` text PRIMARY KEY NOT NULL,
|
||||
`account_id` text NOT NULL,
|
||||
`provider_id` text NOT NULL,
|
||||
`user_id` text NOT NULL,
|
||||
`access_token` text,
|
||||
`refresh_token` text,
|
||||
`id_token` text,
|
||||
`access_token_expires_at` integer,
|
||||
`refresh_token_expires_at` integer,
|
||||
`scope` text,
|
||||
`password` text,
|
||||
`created_at` integer NOT NULL,
|
||||
`updated_at` integer NOT NULL,
|
||||
FOREIGN KEY (`user_id`) REFERENCES `user`(`id`) ON UPDATE no action ON DELETE cascade
|
||||
);
|
||||
--> statement-breakpoint
|
||||
CREATE TABLE `entries` (
|
||||
`id` text PRIMARY KEY NOT NULL,
|
||||
`kind` text NOT NULL,
|
||||
`position` integer NOT NULL,
|
||||
`visible` integer NOT NULL,
|
||||
`shared` text NOT NULL,
|
||||
`translations` text NOT NULL
|
||||
);
|
||||
--> statement-breakpoint
|
||||
CREATE TABLE `languages` (
|
||||
`code` text PRIMARY KEY NOT NULL,
|
||||
`data` text NOT NULL
|
||||
);
|
||||
--> statement-breakpoint
|
||||
CREATE TABLE `media` (
|
||||
`key` text PRIMARY KEY NOT NULL,
|
||||
`original_name` text NOT NULL,
|
||||
`mime` text NOT NULL,
|
||||
`width` integer,
|
||||
`height` integer,
|
||||
`size` integer NOT NULL,
|
||||
`created_at` integer NOT NULL
|
||||
);
|
||||
--> statement-breakpoint
|
||||
CREATE TABLE `sections` (
|
||||
`key` text PRIMARY KEY NOT NULL,
|
||||
`data` text NOT NULL
|
||||
);
|
||||
--> statement-breakpoint
|
||||
CREATE TABLE `session` (
|
||||
`id` text PRIMARY KEY NOT NULL,
|
||||
`expires_at` integer NOT NULL,
|
||||
`token` text NOT NULL,
|
||||
`created_at` integer NOT NULL,
|
||||
`updated_at` integer NOT NULL,
|
||||
`ip_address` text,
|
||||
`user_agent` text,
|
||||
`user_id` text NOT NULL,
|
||||
FOREIGN KEY (`user_id`) REFERENCES `user`(`id`) ON UPDATE no action ON DELETE cascade
|
||||
);
|
||||
--> statement-breakpoint
|
||||
CREATE UNIQUE INDEX `session_token_unique` ON `session` (`token`);--> statement-breakpoint
|
||||
CREATE TABLE `site_settings` (
|
||||
`id` integer PRIMARY KEY NOT NULL,
|
||||
`data` text NOT NULL
|
||||
);
|
||||
--> statement-breakpoint
|
||||
CREATE TABLE `app_state` (
|
||||
`key` text PRIMARY KEY NOT NULL,
|
||||
`value` text NOT NULL
|
||||
);
|
||||
--> statement-breakpoint
|
||||
CREATE TABLE `user` (
|
||||
`id` text PRIMARY KEY NOT NULL,
|
||||
`name` text NOT NULL,
|
||||
`email` text NOT NULL,
|
||||
`email_verified` integer NOT NULL,
|
||||
`image` text,
|
||||
`created_at` integer NOT NULL,
|
||||
`updated_at` integer NOT NULL
|
||||
);
|
||||
--> statement-breakpoint
|
||||
CREATE UNIQUE INDEX `user_email_unique` ON `user` (`email`);--> statement-breakpoint
|
||||
CREATE TABLE `verification` (
|
||||
`id` text PRIMARY KEY NOT NULL,
|
||||
`identifier` text NOT NULL,
|
||||
`value` text NOT NULL,
|
||||
`expires_at` integer NOT NULL,
|
||||
`created_at` integer NOT NULL,
|
||||
`updated_at` integer NOT NULL
|
||||
);
|
||||
547
migrations/meta/0000_snapshot.json
Normal file
547
migrations/meta/0000_snapshot.json
Normal file
@@ -0,0 +1,547 @@
|
||||
{
|
||||
"version": "6",
|
||||
"dialect": "sqlite",
|
||||
"id": "b9356621-932c-4541-a1ca-4ef8af30f9d3",
|
||||
"prevId": "00000000-0000-0000-0000-000000000000",
|
||||
"tables": {
|
||||
"account": {
|
||||
"name": "account",
|
||||
"columns": {
|
||||
"id": {
|
||||
"name": "id",
|
||||
"type": "text",
|
||||
"primaryKey": true,
|
||||
"notNull": true,
|
||||
"autoincrement": false
|
||||
},
|
||||
"account_id": {
|
||||
"name": "account_id",
|
||||
"type": "text",
|
||||
"primaryKey": false,
|
||||
"notNull": true,
|
||||
"autoincrement": false
|
||||
},
|
||||
"provider_id": {
|
||||
"name": "provider_id",
|
||||
"type": "text",
|
||||
"primaryKey": false,
|
||||
"notNull": true,
|
||||
"autoincrement": false
|
||||
},
|
||||
"user_id": {
|
||||
"name": "user_id",
|
||||
"type": "text",
|
||||
"primaryKey": false,
|
||||
"notNull": true,
|
||||
"autoincrement": false
|
||||
},
|
||||
"access_token": {
|
||||
"name": "access_token",
|
||||
"type": "text",
|
||||
"primaryKey": false,
|
||||
"notNull": false,
|
||||
"autoincrement": false
|
||||
},
|
||||
"refresh_token": {
|
||||
"name": "refresh_token",
|
||||
"type": "text",
|
||||
"primaryKey": false,
|
||||
"notNull": false,
|
||||
"autoincrement": false
|
||||
},
|
||||
"id_token": {
|
||||
"name": "id_token",
|
||||
"type": "text",
|
||||
"primaryKey": false,
|
||||
"notNull": false,
|
||||
"autoincrement": false
|
||||
},
|
||||
"access_token_expires_at": {
|
||||
"name": "access_token_expires_at",
|
||||
"type": "integer",
|
||||
"primaryKey": false,
|
||||
"notNull": false,
|
||||
"autoincrement": false
|
||||
},
|
||||
"refresh_token_expires_at": {
|
||||
"name": "refresh_token_expires_at",
|
||||
"type": "integer",
|
||||
"primaryKey": false,
|
||||
"notNull": false,
|
||||
"autoincrement": false
|
||||
},
|
||||
"scope": {
|
||||
"name": "scope",
|
||||
"type": "text",
|
||||
"primaryKey": false,
|
||||
"notNull": false,
|
||||
"autoincrement": false
|
||||
},
|
||||
"password": {
|
||||
"name": "password",
|
||||
"type": "text",
|
||||
"primaryKey": false,
|
||||
"notNull": false,
|
||||
"autoincrement": false
|
||||
},
|
||||
"created_at": {
|
||||
"name": "created_at",
|
||||
"type": "integer",
|
||||
"primaryKey": false,
|
||||
"notNull": true,
|
||||
"autoincrement": false
|
||||
},
|
||||
"updated_at": {
|
||||
"name": "updated_at",
|
||||
"type": "integer",
|
||||
"primaryKey": false,
|
||||
"notNull": true,
|
||||
"autoincrement": false
|
||||
}
|
||||
},
|
||||
"indexes": {},
|
||||
"foreignKeys": {
|
||||
"account_user_id_user_id_fk": {
|
||||
"name": "account_user_id_user_id_fk",
|
||||
"tableFrom": "account",
|
||||
"tableTo": "user",
|
||||
"columnsFrom": [
|
||||
"user_id"
|
||||
],
|
||||
"columnsTo": [
|
||||
"id"
|
||||
],
|
||||
"onDelete": "cascade",
|
||||
"onUpdate": "no action"
|
||||
}
|
||||
},
|
||||
"compositePrimaryKeys": {},
|
||||
"uniqueConstraints": {},
|
||||
"checkConstraints": {}
|
||||
},
|
||||
"entries": {
|
||||
"name": "entries",
|
||||
"columns": {
|
||||
"id": {
|
||||
"name": "id",
|
||||
"type": "text",
|
||||
"primaryKey": true,
|
||||
"notNull": true,
|
||||
"autoincrement": false
|
||||
},
|
||||
"kind": {
|
||||
"name": "kind",
|
||||
"type": "text",
|
||||
"primaryKey": false,
|
||||
"notNull": true,
|
||||
"autoincrement": false
|
||||
},
|
||||
"position": {
|
||||
"name": "position",
|
||||
"type": "integer",
|
||||
"primaryKey": false,
|
||||
"notNull": true,
|
||||
"autoincrement": false
|
||||
},
|
||||
"visible": {
|
||||
"name": "visible",
|
||||
"type": "integer",
|
||||
"primaryKey": false,
|
||||
"notNull": true,
|
||||
"autoincrement": false
|
||||
},
|
||||
"shared": {
|
||||
"name": "shared",
|
||||
"type": "text",
|
||||
"primaryKey": false,
|
||||
"notNull": true,
|
||||
"autoincrement": false
|
||||
},
|
||||
"translations": {
|
||||
"name": "translations",
|
||||
"type": "text",
|
||||
"primaryKey": false,
|
||||
"notNull": true,
|
||||
"autoincrement": false
|
||||
}
|
||||
},
|
||||
"indexes": {},
|
||||
"foreignKeys": {},
|
||||
"compositePrimaryKeys": {},
|
||||
"uniqueConstraints": {},
|
||||
"checkConstraints": {}
|
||||
},
|
||||
"languages": {
|
||||
"name": "languages",
|
||||
"columns": {
|
||||
"code": {
|
||||
"name": "code",
|
||||
"type": "text",
|
||||
"primaryKey": true,
|
||||
"notNull": true,
|
||||
"autoincrement": false
|
||||
},
|
||||
"data": {
|
||||
"name": "data",
|
||||
"type": "text",
|
||||
"primaryKey": false,
|
||||
"notNull": true,
|
||||
"autoincrement": false
|
||||
}
|
||||
},
|
||||
"indexes": {},
|
||||
"foreignKeys": {},
|
||||
"compositePrimaryKeys": {},
|
||||
"uniqueConstraints": {},
|
||||
"checkConstraints": {}
|
||||
},
|
||||
"media": {
|
||||
"name": "media",
|
||||
"columns": {
|
||||
"key": {
|
||||
"name": "key",
|
||||
"type": "text",
|
||||
"primaryKey": true,
|
||||
"notNull": true,
|
||||
"autoincrement": false
|
||||
},
|
||||
"original_name": {
|
||||
"name": "original_name",
|
||||
"type": "text",
|
||||
"primaryKey": false,
|
||||
"notNull": true,
|
||||
"autoincrement": false
|
||||
},
|
||||
"mime": {
|
||||
"name": "mime",
|
||||
"type": "text",
|
||||
"primaryKey": false,
|
||||
"notNull": true,
|
||||
"autoincrement": false
|
||||
},
|
||||
"width": {
|
||||
"name": "width",
|
||||
"type": "integer",
|
||||
"primaryKey": false,
|
||||
"notNull": false,
|
||||
"autoincrement": false
|
||||
},
|
||||
"height": {
|
||||
"name": "height",
|
||||
"type": "integer",
|
||||
"primaryKey": false,
|
||||
"notNull": false,
|
||||
"autoincrement": false
|
||||
},
|
||||
"size": {
|
||||
"name": "size",
|
||||
"type": "integer",
|
||||
"primaryKey": false,
|
||||
"notNull": true,
|
||||
"autoincrement": false
|
||||
},
|
||||
"created_at": {
|
||||
"name": "created_at",
|
||||
"type": "integer",
|
||||
"primaryKey": false,
|
||||
"notNull": true,
|
||||
"autoincrement": false
|
||||
}
|
||||
},
|
||||
"indexes": {},
|
||||
"foreignKeys": {},
|
||||
"compositePrimaryKeys": {},
|
||||
"uniqueConstraints": {},
|
||||
"checkConstraints": {}
|
||||
},
|
||||
"sections": {
|
||||
"name": "sections",
|
||||
"columns": {
|
||||
"key": {
|
||||
"name": "key",
|
||||
"type": "text",
|
||||
"primaryKey": true,
|
||||
"notNull": true,
|
||||
"autoincrement": false
|
||||
},
|
||||
"data": {
|
||||
"name": "data",
|
||||
"type": "text",
|
||||
"primaryKey": false,
|
||||
"notNull": true,
|
||||
"autoincrement": false
|
||||
}
|
||||
},
|
||||
"indexes": {},
|
||||
"foreignKeys": {},
|
||||
"compositePrimaryKeys": {},
|
||||
"uniqueConstraints": {},
|
||||
"checkConstraints": {}
|
||||
},
|
||||
"session": {
|
||||
"name": "session",
|
||||
"columns": {
|
||||
"id": {
|
||||
"name": "id",
|
||||
"type": "text",
|
||||
"primaryKey": true,
|
||||
"notNull": true,
|
||||
"autoincrement": false
|
||||
},
|
||||
"expires_at": {
|
||||
"name": "expires_at",
|
||||
"type": "integer",
|
||||
"primaryKey": false,
|
||||
"notNull": true,
|
||||
"autoincrement": false
|
||||
},
|
||||
"token": {
|
||||
"name": "token",
|
||||
"type": "text",
|
||||
"primaryKey": false,
|
||||
"notNull": true,
|
||||
"autoincrement": false
|
||||
},
|
||||
"created_at": {
|
||||
"name": "created_at",
|
||||
"type": "integer",
|
||||
"primaryKey": false,
|
||||
"notNull": true,
|
||||
"autoincrement": false
|
||||
},
|
||||
"updated_at": {
|
||||
"name": "updated_at",
|
||||
"type": "integer",
|
||||
"primaryKey": false,
|
||||
"notNull": true,
|
||||
"autoincrement": false
|
||||
},
|
||||
"ip_address": {
|
||||
"name": "ip_address",
|
||||
"type": "text",
|
||||
"primaryKey": false,
|
||||
"notNull": false,
|
||||
"autoincrement": false
|
||||
},
|
||||
"user_agent": {
|
||||
"name": "user_agent",
|
||||
"type": "text",
|
||||
"primaryKey": false,
|
||||
"notNull": false,
|
||||
"autoincrement": false
|
||||
},
|
||||
"user_id": {
|
||||
"name": "user_id",
|
||||
"type": "text",
|
||||
"primaryKey": false,
|
||||
"notNull": true,
|
||||
"autoincrement": false
|
||||
}
|
||||
},
|
||||
"indexes": {
|
||||
"session_token_unique": {
|
||||
"name": "session_token_unique",
|
||||
"columns": [
|
||||
"token"
|
||||
],
|
||||
"isUnique": true
|
||||
}
|
||||
},
|
||||
"foreignKeys": {
|
||||
"session_user_id_user_id_fk": {
|
||||
"name": "session_user_id_user_id_fk",
|
||||
"tableFrom": "session",
|
||||
"tableTo": "user",
|
||||
"columnsFrom": [
|
||||
"user_id"
|
||||
],
|
||||
"columnsTo": [
|
||||
"id"
|
||||
],
|
||||
"onDelete": "cascade",
|
||||
"onUpdate": "no action"
|
||||
}
|
||||
},
|
||||
"compositePrimaryKeys": {},
|
||||
"uniqueConstraints": {},
|
||||
"checkConstraints": {}
|
||||
},
|
||||
"site_settings": {
|
||||
"name": "site_settings",
|
||||
"columns": {
|
||||
"id": {
|
||||
"name": "id",
|
||||
"type": "integer",
|
||||
"primaryKey": true,
|
||||
"notNull": true,
|
||||
"autoincrement": false
|
||||
},
|
||||
"data": {
|
||||
"name": "data",
|
||||
"type": "text",
|
||||
"primaryKey": false,
|
||||
"notNull": true,
|
||||
"autoincrement": false
|
||||
}
|
||||
},
|
||||
"indexes": {},
|
||||
"foreignKeys": {},
|
||||
"compositePrimaryKeys": {},
|
||||
"uniqueConstraints": {},
|
||||
"checkConstraints": {}
|
||||
},
|
||||
"app_state": {
|
||||
"name": "app_state",
|
||||
"columns": {
|
||||
"key": {
|
||||
"name": "key",
|
||||
"type": "text",
|
||||
"primaryKey": true,
|
||||
"notNull": true,
|
||||
"autoincrement": false
|
||||
},
|
||||
"value": {
|
||||
"name": "value",
|
||||
"type": "text",
|
||||
"primaryKey": false,
|
||||
"notNull": true,
|
||||
"autoincrement": false
|
||||
}
|
||||
},
|
||||
"indexes": {},
|
||||
"foreignKeys": {},
|
||||
"compositePrimaryKeys": {},
|
||||
"uniqueConstraints": {},
|
||||
"checkConstraints": {}
|
||||
},
|
||||
"user": {
|
||||
"name": "user",
|
||||
"columns": {
|
||||
"id": {
|
||||
"name": "id",
|
||||
"type": "text",
|
||||
"primaryKey": true,
|
||||
"notNull": true,
|
||||
"autoincrement": false
|
||||
},
|
||||
"name": {
|
||||
"name": "name",
|
||||
"type": "text",
|
||||
"primaryKey": false,
|
||||
"notNull": true,
|
||||
"autoincrement": false
|
||||
},
|
||||
"email": {
|
||||
"name": "email",
|
||||
"type": "text",
|
||||
"primaryKey": false,
|
||||
"notNull": true,
|
||||
"autoincrement": false
|
||||
},
|
||||
"email_verified": {
|
||||
"name": "email_verified",
|
||||
"type": "integer",
|
||||
"primaryKey": false,
|
||||
"notNull": true,
|
||||
"autoincrement": false
|
||||
},
|
||||
"image": {
|
||||
"name": "image",
|
||||
"type": "text",
|
||||
"primaryKey": false,
|
||||
"notNull": false,
|
||||
"autoincrement": false
|
||||
},
|
||||
"created_at": {
|
||||
"name": "created_at",
|
||||
"type": "integer",
|
||||
"primaryKey": false,
|
||||
"notNull": true,
|
||||
"autoincrement": false
|
||||
},
|
||||
"updated_at": {
|
||||
"name": "updated_at",
|
||||
"type": "integer",
|
||||
"primaryKey": false,
|
||||
"notNull": true,
|
||||
"autoincrement": false
|
||||
}
|
||||
},
|
||||
"indexes": {
|
||||
"user_email_unique": {
|
||||
"name": "user_email_unique",
|
||||
"columns": [
|
||||
"email"
|
||||
],
|
||||
"isUnique": true
|
||||
}
|
||||
},
|
||||
"foreignKeys": {},
|
||||
"compositePrimaryKeys": {},
|
||||
"uniqueConstraints": {},
|
||||
"checkConstraints": {}
|
||||
},
|
||||
"verification": {
|
||||
"name": "verification",
|
||||
"columns": {
|
||||
"id": {
|
||||
"name": "id",
|
||||
"type": "text",
|
||||
"primaryKey": true,
|
||||
"notNull": true,
|
||||
"autoincrement": false
|
||||
},
|
||||
"identifier": {
|
||||
"name": "identifier",
|
||||
"type": "text",
|
||||
"primaryKey": false,
|
||||
"notNull": true,
|
||||
"autoincrement": false
|
||||
},
|
||||
"value": {
|
||||
"name": "value",
|
||||
"type": "text",
|
||||
"primaryKey": false,
|
||||
"notNull": true,
|
||||
"autoincrement": false
|
||||
},
|
||||
"expires_at": {
|
||||
"name": "expires_at",
|
||||
"type": "integer",
|
||||
"primaryKey": false,
|
||||
"notNull": true,
|
||||
"autoincrement": false
|
||||
},
|
||||
"created_at": {
|
||||
"name": "created_at",
|
||||
"type": "integer",
|
||||
"primaryKey": false,
|
||||
"notNull": true,
|
||||
"autoincrement": false
|
||||
},
|
||||
"updated_at": {
|
||||
"name": "updated_at",
|
||||
"type": "integer",
|
||||
"primaryKey": false,
|
||||
"notNull": true,
|
||||
"autoincrement": false
|
||||
}
|
||||
},
|
||||
"indexes": {},
|
||||
"foreignKeys": {},
|
||||
"compositePrimaryKeys": {},
|
||||
"uniqueConstraints": {},
|
||||
"checkConstraints": {}
|
||||
}
|
||||
},
|
||||
"views": {},
|
||||
"enums": {},
|
||||
"_meta": {
|
||||
"schemas": {},
|
||||
"tables": {},
|
||||
"columns": {}
|
||||
},
|
||||
"internal": {
|
||||
"indexes": {}
|
||||
}
|
||||
}
|
||||
13
migrations/meta/_journal.json
Normal file
13
migrations/meta/_journal.json
Normal file
@@ -0,0 +1,13 @@
|
||||
{
|
||||
"version": "7",
|
||||
"dialect": "sqlite",
|
||||
"entries": [
|
||||
{
|
||||
"idx": 0,
|
||||
"version": "6",
|
||||
"when": 1789474520735,
|
||||
"tag": "0000_clever_vision",
|
||||
"breakpoints": true
|
||||
}
|
||||
]
|
||||
}
|
||||
6
src/instrumentation.ts
Normal file
6
src/instrumentation.ts
Normal file
@@ -0,0 +1,6 @@
|
||||
export async function register() {
|
||||
if (process.env.NEXT_RUNTIME === 'nodejs' && process.env.NEXT_PHASE !== 'phase-production-build') {
|
||||
const { initialize } = await import('./server/initialize');
|
||||
initialize();
|
||||
}
|
||||
}
|
||||
34
src/server/db.ts
Normal file
34
src/server/db.ts
Normal file
@@ -0,0 +1,34 @@
|
||||
import Database from 'better-sqlite3';
|
||||
import { drizzle } from 'drizzle-orm/better-sqlite3';
|
||||
import { migrate } from 'drizzle-orm/better-sqlite3/migrator';
|
||||
import { readMigrationFiles } from 'drizzle-orm/migrator';
|
||||
import { mkdirSync } from 'node:fs';
|
||||
import path from 'node:path';
|
||||
import * as schema from './schema';
|
||||
|
||||
// Runtime storage must never be traced into the deployable application image.
|
||||
export const dataDirectory = () => path.resolve(/* turbopackIgnore: true */ process.env.DATA_DIR || './data');
|
||||
export function openDatabase(directory = dataDirectory()) {
|
||||
mkdirSync(directory, { recursive: true, mode: 0o700 });
|
||||
const sqlite = new Database(/* turbopackIgnore: true */ path.join(directory, 'portfolio.sqlite'));
|
||||
sqlite.pragma('journal_mode = WAL');
|
||||
sqlite.pragma('foreign_keys = ON');
|
||||
sqlite.pragma('busy_timeout = 5000');
|
||||
const db = drizzle(sqlite, { schema });
|
||||
return { db, sqlite };
|
||||
}
|
||||
export type DatabaseConnection = ReturnType<typeof openDatabase>;
|
||||
let connection: DatabaseConnection | undefined;
|
||||
export function getDatabase() { return connection ??= openDatabase(); }
|
||||
export function migrateDatabase(connection: DatabaseConnection) {
|
||||
// Only maintenance/startup invokes migrations. Importing this module has no disk side effects.
|
||||
const config = { migrationsFolder: path.join(process.cwd(), 'migrations') };
|
||||
const available = readMigrationFiles(config);
|
||||
if (connection.sqlite.prepare("SELECT name FROM sqlite_master WHERE type = 'table' AND name = '__drizzle_migrations'").get()) {
|
||||
const applied = connection.sqlite.prepare('SELECT hash, created_at FROM __drizzle_migrations').all() as { hash: string; created_at: number }[];
|
||||
if (applied.some(row => !available.some(migration => migration.hash === row.hash && migration.folderMillis === row.created_at))) {
|
||||
throw new Error('Database migrations do not match this application version. Use the matching image or restore its pre-upgrade backup.');
|
||||
}
|
||||
}
|
||||
migrate(connection.db, config);
|
||||
}
|
||||
16
src/server/initialize.ts
Normal file
16
src/server/initialize.ts
Normal file
@@ -0,0 +1,16 @@
|
||||
import { existsSync, writeFileSync } from 'node:fs';
|
||||
import { randomBytes } from 'node:crypto';
|
||||
import path from 'node:path';
|
||||
import { dataDirectory, getDatabase, migrateDatabase } from './db';
|
||||
import { seedDatabase } from './seed';
|
||||
|
||||
export function initialize() {
|
||||
const connection = getDatabase();
|
||||
migrateDatabase(connection);
|
||||
seedDatabase(connection);
|
||||
const secretPath = path.join(dataDirectory(), 'auth-secret');
|
||||
if (!process.env.BETTER_AUTH_SECRET && !existsSync(/* turbopackIgnore: true */ secretPath)) {
|
||||
writeFileSync(secretPath, randomBytes(48).toString('base64url'), { flag: 'wx', mode: 0o600 });
|
||||
}
|
||||
return connection;
|
||||
}
|
||||
39
src/server/schema.ts
Normal file
39
src/server/schema.ts
Normal file
@@ -0,0 +1,39 @@
|
||||
import { sqliteTable, text, integer } from 'drizzle-orm/sqlite-core';
|
||||
import type { Settings, Language, Section, Entry } from '../content/contracts';
|
||||
|
||||
export const siteSettings = sqliteTable('site_settings', { id: integer('id').primaryKey(), data: text('data', { mode: 'json' }).$type<Settings>().notNull() });
|
||||
export const languages = sqliteTable('languages', { code: text('code').primaryKey(), data: text('data', { mode: 'json' }).$type<Language>().notNull() });
|
||||
export const sections = sqliteTable('sections', { key: text('key').primaryKey(), data: text('data', { mode: 'json' }).$type<Section>().notNull() });
|
||||
export const entries = sqliteTable('entries', {
|
||||
id: text('id').primaryKey(), kind: text('kind').$type<Entry['kind']>().notNull(), position: integer('position').notNull(),
|
||||
visible: integer('visible', { mode: 'boolean' }).notNull(),
|
||||
shared: text('shared', { mode: 'json' }).$type<Entry['shared']>().notNull(),
|
||||
translations: text('translations', { mode: 'json' }).$type<Entry['translations']>().notNull(),
|
||||
});
|
||||
export const media = sqliteTable('media', {
|
||||
key: text('key').primaryKey(), originalName: text('original_name').notNull(), mime: text('mime').notNull(),
|
||||
width: integer('width'), height: integer('height'), size: integer('size').notNull(), createdAt: integer('created_at').notNull(),
|
||||
});
|
||||
export const state = sqliteTable('app_state', { key: text('key').primaryKey(), value: text('value').notNull() });
|
||||
|
||||
export const user = sqliteTable('user', {
|
||||
id: text('id').primaryKey(), name: text('name').notNull(), email: text('email').notNull().unique(),
|
||||
emailVerified: integer('email_verified', { mode: 'boolean' }).notNull(), image: text('image'),
|
||||
createdAt: integer('created_at', { mode: 'timestamp_ms' }).notNull(), updatedAt: integer('updated_at', { mode: 'timestamp_ms' }).notNull(),
|
||||
});
|
||||
export const session = sqliteTable('session', {
|
||||
id: text('id').primaryKey(), expiresAt: integer('expires_at', { mode: 'timestamp_ms' }).notNull(), token: text('token').notNull().unique(),
|
||||
createdAt: integer('created_at', { mode: 'timestamp_ms' }).notNull(), updatedAt: integer('updated_at', { mode: 'timestamp_ms' }).notNull(),
|
||||
ipAddress: text('ip_address'), userAgent: text('user_agent'), userId: text('user_id').notNull().references(() => user.id, { onDelete: 'cascade' }),
|
||||
});
|
||||
export const account = sqliteTable('account', {
|
||||
id: text('id').primaryKey(), accountId: text('account_id').notNull(), providerId: text('provider_id').notNull(),
|
||||
userId: text('user_id').notNull().references(() => user.id, { onDelete: 'cascade' }),
|
||||
accessToken: text('access_token'), refreshToken: text('refresh_token'), idToken: text('id_token'),
|
||||
accessTokenExpiresAt: integer('access_token_expires_at', { mode: 'timestamp_ms' }), refreshTokenExpiresAt: integer('refresh_token_expires_at', { mode: 'timestamp_ms' }),
|
||||
scope: text('scope'), password: text('password'), createdAt: integer('created_at', { mode: 'timestamp_ms' }).notNull(), updatedAt: integer('updated_at', { mode: 'timestamp_ms' }).notNull(),
|
||||
});
|
||||
export const verification = sqliteTable('verification', {
|
||||
id: text('id').primaryKey(), identifier: text('identifier').notNull(), value: text('value').notNull(),
|
||||
expiresAt: integer('expires_at', { mode: 'timestamp_ms' }).notNull(), createdAt: integer('created_at', { mode: 'timestamp_ms' }).notNull(), updatedAt: integer('updated_at', { mode: 'timestamp_ms' }).notNull(),
|
||||
});
|
||||
18
src/server/seed.ts
Normal file
18
src/server/seed.ts
Normal file
@@ -0,0 +1,18 @@
|
||||
import { eq } from 'drizzle-orm';
|
||||
import type { DatabaseConnection } from './db';
|
||||
import * as tables from './schema';
|
||||
import { sampleSettings, sampleEntries, sampleSections } from '../content/samples';
|
||||
|
||||
export function seedDatabase({ db }: DatabaseConnection) {
|
||||
db.transaction(tx => {
|
||||
if (tx.select().from(tables.siteSettings).get()) return;
|
||||
tx.insert(tables.siteSettings).values({ id: 1, data: sampleSettings }).run();
|
||||
tx.insert(tables.languages).values({ code: 'en', data: { code: 'en', name: 'English', direction: 'ltr', enabled: true, messages: {} } }).run();
|
||||
for (const section of sampleSections()) tx.insert(tables.sections).values({ key: section.key, data: section }).run();
|
||||
for (const entry of sampleEntries()) tx.insert(tables.entries).values(entry).run();
|
||||
tx.insert(tables.state).values({ key: 'content-status', value: 'sample' }).run();
|
||||
});
|
||||
}
|
||||
export function markContentEdited({ db }: DatabaseConnection) {
|
||||
db.update(tables.state).set({ value: 'edited' }).where(eq(tables.state.key, 'content-status')).run();
|
||||
}
|
||||
Reference in New Issue
Block a user