feat: add src/db/adapter/neo4j.ts
swadhinbiswas committed 8 months ago
7952467
1 parent bc6f2b2
commit 7952467
1 file changed
+190 -0
Showing 1 changed file+190-0
▼src/db/adapter/neo4j.ts
+190-0
| @@ -0,0 +1,190 @@ | ||
1 | +import { | |
2 | + BaseDatabaseAdapter, | |
3 | + ColumnType, | |
4 | + DatabaseConfig, | |
5 | + Migration, | |
6 | + MigrationStatus, | |
7 | + QueryOptions, | |
8 | + QueryResult, | |
9 | + TableAlterCommand, | |
10 | + TableSchema, | |
11 | + WhereClause, | |
12 | +} from "./types"; | |
13 | + | |
14 | +const dynamicImport = new Function("m", "return import(m)") as (m: string) => Promise<any>; | |
15 | + | |
16 | +function toWhereCypher(where?: WhereClause[]): { clause: string; params: Record<string, any> } { | |
17 | + if (!where?.length) return { clause: "", params: {} }; | |
18 | + const params: Record<string, any> = {}; | |
19 | + const parts: string[] = []; | |
20 | + where.forEach((w, i) => { | |
21 | + const key = `p${i}`; | |
22 | + params[key] = w.value; | |
23 | + const field = `n.${w.field}`; | |
24 | + switch (w.operator) { | |
25 | + case "=": | |
26 | + case "eq": | |
27 | + parts.push(`${field} = $${key}`); | |
28 | + break; | |
29 | + case "!=": | |
30 | + case "ne": | |
31 | + parts.push(`${field} <> $${key}`); | |
32 | + break; | |
33 | + case ">": | |
34 | + case "gt": | |
35 | + parts.push(`${field} > $${key}`); | |
36 | + break; | |
37 | + case ">=": | |
38 | + case "gte": | |
39 | + parts.push(`${field} >= $${key}`); | |
40 | + break; | |
41 | + case "<": | |
42 | + case "lt": | |
43 | + parts.push(`${field} < $${key}`); | |
44 | + break; | |
45 | + case "<=": | |
46 | + case "lte": | |
47 | + parts.push(`${field} <= $${key}`); | |
48 | + break; | |
49 | + default: | |
50 | + break; | |
51 | + } | |
52 | + }); | |
53 | + return { clause: parts.length ? `WHERE ${parts.join(" AND ")}` : "", params }; | |
54 | +} | |
55 | + | |
56 | +export class Neo4jAdapter extends BaseDatabaseAdapter { | |
57 | + private driver: any; | |
58 | + | |
59 | + async connect(): Promise<void> { | |
60 | + if (this.connected) return; | |
61 | + const neo4j = await dynamicImport("neo4j-driver"); | |
62 | + this.driver = neo4j.driver( | |
63 | + this.config.url, | |
64 | + neo4j.auth.basic(this.config.user || "neo4j", this.config.password || "") | |
65 | + ); | |
66 | + await this.driver.verifyConnectivity(); | |
67 | + this.connected = true; | |
68 | + } | |
69 | + | |
70 | + async disconnect(): Promise<void> { | |
71 | + if (!this.connected) return; | |
72 | + await this.driver?.close(); | |
73 | + this.connected = false; | |
74 | + } | |
75 | + | |
76 | + private async run(query: string, params?: Record<string, any>) { | |
77 | + const session = this.driver.session(); | |
78 | + try { | |
79 | + return await session.run(query, params || {}); | |
80 | + } finally { | |
81 | + await session.close(); | |
82 | + } | |
83 | + } | |
84 | + | |
85 | + async findOne<T>(table: string, options?: QueryOptions): Promise<T | null> { | |
86 | + const rows = await this.findMany<T>(table, { ...options, limit: 1 }); | |
87 | + return rows[0] || null; | |
88 | + } | |
89 | + | |
90 | + async findMany<T>(table: string, options?: QueryOptions): Promise<T[]> { | |
91 | + const where = toWhereCypher(options?.where); | |
92 | + const limit = options?.limit ? `LIMIT ${options.limit}` : ""; | |
93 | + const result = await this.run( | |
94 | + `MATCH (n:${table}) ${where.clause} RETURN n ${limit}`, | |
95 | + where.params | |
96 | + ); | |
97 | + return result.records.map((r: any) => r.get("n").properties) as T[]; | |
98 | + } | |
99 | + | |
100 | + async create<T>(table: string, data: Partial<T>): Promise<T> { | |
101 | + const props = { ...(data as any) }; | |
102 | + if (!props.id) props.id = `${Date.now()}_${Math.random().toString(36).slice(2)}`; | |
103 | + await this.run(`CREATE (n:${table} $props)`, { props }); | |
104 | + return props as T; | |
105 | + } | |
106 | + | |
107 | + async createMany<T>(table: string, data: Partial<T>[]): Promise<T[]> { | |
108 | + const out: T[] = []; | |
109 | + for (const item of data) out.push(await this.create<T>(table, item)); | |
110 | + return out; | |
111 | + } | |
112 | + | |
113 | + async update<T>(table: string, id: string | number, data: Partial<T>): Promise<T | null> { | |
114 | + const props = { ...(data as any) }; | |
115 | + const result = await this.run( | |
116 | + `MATCH (n:${table} {id: $id}) SET n += $props RETURN n`, | |
117 | + { id: String(id), props } | |
118 | + ); | |
119 | + if (!result.records.length) return null; | |
120 | + return result.records[0].get("n").properties as T; | |
121 | + } | |
122 | + | |
123 | + async updateMany<T>(table: string, options: QueryOptions, data: Partial<T>): Promise<number> { | |
124 | + const where = toWhereCypher(options?.where); | |
125 | + const result = await this.run( | |
126 | + `MATCH (n:${table}) ${where.clause} SET n += $props RETURN count(n) as c`, | |
127 | + { ...where.params, props: data } | |
128 | + ); | |
129 | + return Number(result.records[0]?.get("c") || 0); | |
130 | + } | |
131 | + | |
132 | + async delete(table: string, id: string | number): Promise<boolean> { | |
133 | + const result = await this.run( | |
134 | + `MATCH (n:${table} {id: $id}) DETACH DELETE n RETURN count(n) as c`, | |
135 | + { id: String(id) } | |
136 | + ); | |
137 | + return Number(result.records[0]?.get("c") || 0) > 0; | |
138 | + } | |
139 | + | |
140 | + async deleteMany(table: string, options?: QueryOptions): Promise<number> { | |
141 | + const where = toWhereCypher(options?.where); | |
142 | + const result = await this.run( | |
143 | + `MATCH (n:${table}) ${where.clause} WITH n, count(n) as c DETACH DELETE n RETURN c`, | |
144 | + where.params | |
145 | + ); | |
146 | + return Number(result.records[0]?.get("c") || 0); | |
147 | + } | |
148 | + | |
149 | + async count(table: string, options?: QueryOptions): Promise<number> { | |
150 | + const where = toWhereCypher(options?.where); | |
151 | + const result = await this.run(`MATCH (n:${table}) ${where.clause} RETURN count(n) as c`, where.params); | |
152 | + return Number(result.records[0]?.get("c") || 0); | |
153 | + } | |
154 | + | |
155 | + async rawQuery<T>(query: string, params?: unknown[]): Promise<QueryResult<T>> { | |
156 | + const result = await this.run(query, (params as any) || {}); | |
157 | + return { | |
158 | + rows: result.records.map((r: any) => r.toObject()) as T[], | |
159 | + rowCount: result.records.length, | |
160 | + }; | |
161 | + } | |
162 | + | |
163 | + async beginTransaction(): Promise<void> {} | |
164 | + async commitTransaction(): Promise<void> {} | |
165 | + async rollbackTransaction(): Promise<void> {} | |
166 | + | |
167 | + async tableExists(table: string): Promise<boolean> { | |
168 | + const result = await this.run("CALL db.labels() YIELD label RETURN collect(label) as labels"); | |
169 | + const labels = result.records[0]?.get("labels") || []; | |
170 | + return labels.includes(table); | |
171 | + } | |
172 | + | |
173 | + async createTable(table: string, schema: TableSchema): Promise<void> {} | |
174 | + async dropTable(table: string): Promise<void> { | |
175 | + await this.run(`MATCH (n:${table}) DETACH DELETE n`); | |
176 | + } | |
177 | + async alterTable(table: string, changes: TableAlterCommand[]): Promise<void> {} | |
178 | + | |
179 | + async runMigration(migration: Migration): Promise<void> { | |
180 | + await migration.up(this); | |
181 | + } | |
182 | + | |
183 | + async getMigrationStatus(): Promise<MigrationStatus[]> { | |
184 | + return []; | |
185 | + } | |
186 | + | |
187 | + protected mapColumnType(type: ColumnType): string { | |
188 | + return type; | |
189 | + } | |
190 | +} | |
0 | 191 | |