db.ts 5.9 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211
  1. /**
  2. * Relay 数据库封装 — 轻量版
  3. *
  4. * 与主项目 lib/db.ts 类似,但独立维护,避免跨服务依赖。
  5. */
  6. import Database from 'better-sqlite3';
  7. import type { Database as DatabaseType } from 'better-sqlite3';
  8. import path from 'path';
  9. import fs from 'fs';
  10. import { randomUUID } from 'crypto';
  11. export interface DbRow {
  12. objectId: string;
  13. createdAt: string;
  14. updatedAt: string;
  15. [key: string]: unknown;
  16. }
  17. export interface ColumnDef {
  18. name: string;
  19. type: 'TEXT' | 'INTEGER' | 'REAL' | 'BOOLEAN';
  20. required?: boolean;
  21. defaultValue?: unknown;
  22. }
  23. let db: DatabaseType;
  24. export function initDb(dbPath: string): void {
  25. fs.mkdirSync(path.dirname(dbPath), { recursive: true });
  26. db = new Database(dbPath);
  27. db.pragma('journal_mode = WAL');
  28. db.pragma('foreign_keys = ON');
  29. }
  30. export function getDb(): DatabaseType {
  31. if (!db) throw new Error('数据库未初始化,请先调用 initDb()');
  32. return db;
  33. }
  34. export function createTable(tableName: string, columns: ColumnDef[]): void {
  35. const systemColumns = `
  36. objectId TEXT PRIMARY KEY,
  37. createdAt TEXT NOT NULL DEFAULT (datetime('now')),
  38. updatedAt TEXT NOT NULL DEFAULT (datetime('now'))
  39. `;
  40. const businessColumns = columns
  41. .map((col) => {
  42. let sql = `"${col.name}" ${col.type}`;
  43. if (col.required) sql += ' NOT NULL';
  44. if (col.defaultValue !== undefined) {
  45. const dv =
  46. typeof col.defaultValue === 'string'
  47. ? `'${col.defaultValue}'`
  48. : col.defaultValue;
  49. sql += ` DEFAULT ${dv}`;
  50. }
  51. return sql;
  52. })
  53. .join(',\n ');
  54. const sql = `CREATE TABLE IF NOT EXISTS "${tableName}" (\n ${systemColumns}${businessColumns ? ',\n ' + businessColumns : ''}\n )`;
  55. db.exec(sql);
  56. }
  57. export class Query<T extends DbRow = DbRow> {
  58. private tableName: string;
  59. private conditions: string[] = [];
  60. private params: unknown[] = [];
  61. private _limit = 100;
  62. private _skip = 0;
  63. private _orderBy = 'createdAt';
  64. private _orderDirection: 'asc' | 'desc' = 'desc';
  65. constructor(tableName: string) {
  66. this.tableName = tableName;
  67. }
  68. equalTo(field: string, value: unknown): this {
  69. if (value === null || value === undefined) {
  70. this.conditions.push(`"${field}" IS NULL`);
  71. } else {
  72. this.conditions.push(`"${field}" = ?`);
  73. this.params.push(value);
  74. }
  75. return this;
  76. }
  77. notEqualTo(field: string, value: unknown): this {
  78. if (value === null || value === undefined) {
  79. this.conditions.push(`"${field}" IS NOT NULL`);
  80. } else {
  81. this.conditions.push(`"${field}" != ?`);
  82. this.params.push(value);
  83. }
  84. return this;
  85. }
  86. containedIn(field: string, values: unknown[]): this {
  87. if (values.length === 0) {
  88. this.conditions.push('1 = 0');
  89. return this;
  90. }
  91. const placeholders = values.map(() => '?').join(',');
  92. this.conditions.push(`"${field}" IN (${placeholders})`);
  93. this.params.push(...values);
  94. return this;
  95. }
  96. greaterThan(field: string, value: number | string): this {
  97. this.conditions.push(`"${field}" > ?`);
  98. this.params.push(value);
  99. return this;
  100. }
  101. lessThan(field: string, value: number | string): this {
  102. this.conditions.push(`"${field}" < ?`);
  103. this.params.push(value);
  104. return this;
  105. }
  106. descending(field: string): this {
  107. this._orderBy = field;
  108. this._orderDirection = 'desc';
  109. return this;
  110. }
  111. ascending(field: string): this {
  112. this._orderBy = field;
  113. this._orderDirection = 'asc';
  114. return this;
  115. }
  116. limit(n: number): this {
  117. this._limit = n;
  118. return this;
  119. }
  120. skip(n: number): this {
  121. this._skip = n;
  122. return this;
  123. }
  124. private buildWhere(): string {
  125. return this.conditions.length > 0
  126. ? `WHERE ${this.conditions.join(' AND ')}`
  127. : '';
  128. }
  129. find(): T[] {
  130. const where = this.buildWhere();
  131. const sql = `SELECT * FROM "${this.tableName}" ${where} ORDER BY "${this._orderBy}" ${this._orderDirection} LIMIT ? OFFSET ?`;
  132. return db.prepare(sql).all(...this.params, this._limit, this._skip) as T[];
  133. }
  134. first(): T | null {
  135. const where = this.buildWhere();
  136. const sql = `SELECT * FROM "${this.tableName}" ${where} ORDER BY "${this._orderBy}" ${this._orderDirection} LIMIT 1`;
  137. return (db.prepare(sql).get(...this.params) as T | undefined) ?? null;
  138. }
  139. count(): number {
  140. const where = this.buildWhere();
  141. const sql = `SELECT COUNT(*) as count FROM "${this.tableName}" ${where}`;
  142. const result = db.prepare(sql).get(...this.params) as { count: number };
  143. return result.count;
  144. }
  145. }
  146. export function createObject(tableName: string, data: Record<string, unknown>): DbRow {
  147. const now = new Date().toISOString();
  148. const objectId = randomUUID();
  149. const columns: string[] = ['objectId', 'createdAt', 'updatedAt'];
  150. const values: unknown[] = [objectId, now, now];
  151. const placeholders: string[] = ['?', '?', '?'];
  152. for (const [key, value] of Object.entries(data)) {
  153. columns.push(`"${key}"`);
  154. values.push(value);
  155. placeholders.push('?');
  156. }
  157. const sql = `INSERT INTO "${tableName}" (${columns.join(', ')}) VALUES (${placeholders.join(', ')})`;
  158. db.prepare(sql).run(...values);
  159. return { objectId, createdAt: now, updatedAt: now, ...data } as DbRow;
  160. }
  161. export function updateObject(tableName: string, objectId: string, data: Record<string, unknown>): boolean {
  162. const now = new Date().toISOString();
  163. const sets: string[] = ['updatedAt = ?'];
  164. const values: unknown[] = [now];
  165. for (const [key, value] of Object.entries(data)) {
  166. sets.push(`"${key}" = ?`);
  167. values.push(value);
  168. }
  169. values.push(objectId);
  170. const sql = `UPDATE "${tableName}" SET ${sets.join(', ')} WHERE objectId = ?`;
  171. const result = db.prepare(sql).run(...values);
  172. return result.changes > 0;
  173. }
  174. export function getObject<T extends DbRow = DbRow>(tableName: string, objectId: string): T | null {
  175. const sql = `SELECT * FROM "${tableName}" WHERE objectId = ?`;
  176. return (db.prepare(sql).get(objectId) as T) ?? null;
  177. }