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
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