@an5/adapters 0.2.0 → 0.2.2
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/README.md +226 -226
- package/package.json +46 -38
- package/typescript/an5Adapter.d.ts +4 -2
- package/typescript/an5Adapter.js +58 -21
- package/typescript/browser.d.ts +5 -0
- package/typescript/browser.js +12 -0
- package/typescript/googlesheets/adapter.d.ts +3 -2
- package/typescript/googlesheets/adapter.js +77 -7
- package/typescript/googlesheets/an5SheetsAdapter.d.ts +1 -1
- package/typescript/googlesheets/config.d.ts +11 -0
- package/typescript/googlesheets/config.js +7 -0
- package/typescript/googlesheets/helpers.js +1 -1
- package/typescript/googlesheets/index.d.ts +2 -1
- package/typescript/googlesheets/retry.js +21 -8
- package/typescript/googlesheets/tableClient.js +4 -3
- package/typescript/index.d.ts +4 -2
- package/typescript/{an5Adapter.ts → src/an5Adapter.ts} +505 -474
- package/typescript/{base → src/base}/index.ts +3 -3
- package/typescript/{base → src/base}/metadata.ts +28 -28
- package/typescript/{base → src/base}/sql.ts +98 -98
- package/typescript/{base → src/base}/types.ts +24 -24
- package/typescript/src/browser.ts +6 -0
- package/typescript/{config.ts → src/config.ts} +95 -95
- package/typescript/{googlesheets → src/googlesheets}/adapter.ts +242 -167
- package/typescript/{googlesheets → src/googlesheets}/an5SheetsAdapter.ts +3 -3
- package/typescript/{googlesheets → src/googlesheets}/config.ts +44 -33
- package/typescript/{googlesheets → src/googlesheets}/helpers.ts +124 -124
- package/typescript/{googlesheets → src/googlesheets}/index.ts +3 -2
- package/typescript/{googlesheets → src/googlesheets}/parseConnectionString.ts +44 -44
- package/typescript/src/googlesheets/retry.ts +32 -0
- package/typescript/{googlesheets → src/googlesheets}/sqlExecutor.ts +301 -301
- package/typescript/{googlesheets → src/googlesheets}/tableClient.ts +435 -434
- package/typescript/{googlesheets → src/googlesheets}/types.ts +6 -6
- package/typescript/{index.ts → src/index.ts} +19 -15
- package/typescript/{mssql → src/mssql}/engine.ts +132 -132
- package/typescript/{mysql → src/mssql}/index.ts +1 -1
- package/typescript/{mysql → src/mysql}/engine.ts +80 -80
- package/typescript/{postgres → src/mysql}/index.ts +1 -1
- package/typescript/{postgres → src/postgres}/engine.ts +87 -87
- package/typescript/{sqlite → src/postgres}/index.ts +1 -1
- package/typescript/{sqlite → src/sqlite}/engine.ts +55 -55
- package/typescript/{mssql → src/sqlite}/index.ts +1 -1
- package/typescript/googlesheets/retry.ts +0 -23
|
@@ -1,474 +1,505 @@
|
|
|
1
|
-
import { randomUUID } from 'crypto';
|
|
2
|
-
|
|
3
|
-
import {
|
|
4
|
-
An5SheetsAdapter,
|
|
5
|
-
An5SheetsAdapterConfig,
|
|
6
|
-
SheetsTableClient,
|
|
7
|
-
} from './googlesheets';
|
|
8
|
-
import { parseSheetsConnectionString } from './googlesheets/parseConnectionString';
|
|
9
|
-
import type { An5AdapterConfig, Dialect, QueryEngine, TransactionHandle } from './base/types';
|
|
10
|
-
import { buildOrderBy, parseWhere, quote } from './base/sql';
|
|
11
|
-
import { getFieldsForModel, getModelToTable, setAdapterMetadata, type AdapterMetadata } from './base/metadata';
|
|
12
|
-
|
|
13
|
-
|
|
14
|
-
|
|
15
|
-
|
|
16
|
-
|
|
17
|
-
|
|
18
|
-
|
|
19
|
-
|
|
20
|
-
|
|
21
|
-
|
|
22
|
-
|
|
23
|
-
|
|
24
|
-
|
|
25
|
-
|
|
26
|
-
|
|
27
|
-
|
|
28
|
-
|
|
29
|
-
|
|
30
|
-
|
|
31
|
-
|
|
32
|
-
|
|
33
|
-
|
|
34
|
-
|
|
35
|
-
|
|
36
|
-
|
|
37
|
-
|
|
38
|
-
|
|
39
|
-
|
|
40
|
-
|
|
41
|
-
|
|
42
|
-
|
|
43
|
-
|
|
44
|
-
|
|
45
|
-
|
|
46
|
-
|
|
47
|
-
|
|
48
|
-
|
|
49
|
-
}
|
|
50
|
-
|
|
51
|
-
|
|
52
|
-
|
|
53
|
-
|
|
54
|
-
|
|
55
|
-
|
|
56
|
-
|
|
57
|
-
|
|
58
|
-
|
|
59
|
-
|
|
60
|
-
|
|
61
|
-
}
|
|
62
|
-
|
|
63
|
-
private
|
|
64
|
-
if (!this.
|
|
65
|
-
|
|
66
|
-
|
|
67
|
-
|
|
68
|
-
|
|
69
|
-
|
|
70
|
-
|
|
71
|
-
|
|
72
|
-
|
|
73
|
-
|
|
74
|
-
|
|
75
|
-
|
|
76
|
-
|
|
77
|
-
|
|
78
|
-
|
|
79
|
-
|
|
80
|
-
|
|
81
|
-
|
|
82
|
-
|
|
83
|
-
|
|
84
|
-
|
|
85
|
-
|
|
86
|
-
|
|
87
|
-
|
|
88
|
-
|
|
89
|
-
|
|
90
|
-
|
|
91
|
-
|
|
92
|
-
|
|
93
|
-
|
|
94
|
-
|
|
95
|
-
|
|
96
|
-
|
|
97
|
-
|
|
98
|
-
|
|
99
|
-
|
|
100
|
-
|
|
101
|
-
|
|
102
|
-
|
|
103
|
-
|
|
104
|
-
|
|
105
|
-
|
|
106
|
-
|
|
107
|
-
|
|
108
|
-
|
|
109
|
-
|
|
110
|
-
|
|
111
|
-
|
|
112
|
-
|
|
113
|
-
|
|
114
|
-
|
|
115
|
-
|
|
116
|
-
|
|
117
|
-
|
|
118
|
-
|
|
119
|
-
|
|
120
|
-
|
|
121
|
-
|
|
122
|
-
}
|
|
123
|
-
|
|
124
|
-
|
|
125
|
-
|
|
126
|
-
|
|
127
|
-
|
|
128
|
-
|
|
129
|
-
|
|
130
|
-
|
|
131
|
-
|
|
132
|
-
|
|
133
|
-
|
|
134
|
-
|
|
135
|
-
|
|
136
|
-
}
|
|
137
|
-
|
|
138
|
-
|
|
139
|
-
|
|
140
|
-
|
|
141
|
-
|
|
142
|
-
|
|
143
|
-
|
|
144
|
-
|
|
145
|
-
|
|
146
|
-
|
|
147
|
-
|
|
148
|
-
|
|
149
|
-
|
|
150
|
-
|
|
151
|
-
|
|
152
|
-
|
|
153
|
-
|
|
154
|
-
|
|
155
|
-
async
|
|
156
|
-
|
|
157
|
-
|
|
158
|
-
|
|
159
|
-
|
|
160
|
-
|
|
161
|
-
|
|
162
|
-
|
|
163
|
-
|
|
164
|
-
|
|
165
|
-
|
|
166
|
-
|
|
167
|
-
|
|
168
|
-
|
|
169
|
-
|
|
170
|
-
|
|
171
|
-
|
|
172
|
-
|
|
173
|
-
|
|
174
|
-
|
|
175
|
-
|
|
176
|
-
|
|
177
|
-
|
|
178
|
-
|
|
179
|
-
|
|
180
|
-
|
|
181
|
-
|
|
182
|
-
|
|
183
|
-
|
|
184
|
-
|
|
185
|
-
|
|
186
|
-
|
|
187
|
-
|
|
188
|
-
|
|
189
|
-
|
|
190
|
-
|
|
191
|
-
|
|
192
|
-
|
|
193
|
-
|
|
194
|
-
|
|
195
|
-
|
|
196
|
-
|
|
197
|
-
|
|
198
|
-
|
|
199
|
-
|
|
200
|
-
|
|
201
|
-
|
|
202
|
-
|
|
203
|
-
|
|
204
|
-
|
|
205
|
-
|
|
206
|
-
|
|
207
|
-
|
|
208
|
-
|
|
209
|
-
|
|
210
|
-
|
|
211
|
-
|
|
212
|
-
|
|
213
|
-
|
|
214
|
-
|
|
215
|
-
|
|
216
|
-
|
|
217
|
-
|
|
218
|
-
|
|
219
|
-
|
|
220
|
-
|
|
221
|
-
|
|
222
|
-
|
|
223
|
-
|
|
224
|
-
|
|
225
|
-
|
|
226
|
-
|
|
227
|
-
|
|
228
|
-
|
|
229
|
-
|
|
230
|
-
|
|
231
|
-
const
|
|
232
|
-
|
|
233
|
-
const
|
|
234
|
-
|
|
235
|
-
|
|
236
|
-
|
|
237
|
-
|
|
238
|
-
|
|
239
|
-
|
|
240
|
-
if (
|
|
241
|
-
|
|
242
|
-
|
|
243
|
-
|
|
244
|
-
|
|
245
|
-
|
|
246
|
-
|
|
247
|
-
|
|
248
|
-
|
|
249
|
-
|
|
250
|
-
|
|
251
|
-
|
|
252
|
-
|
|
253
|
-
|
|
254
|
-
|
|
255
|
-
|
|
256
|
-
|
|
257
|
-
|
|
258
|
-
|
|
259
|
-
|
|
260
|
-
|
|
261
|
-
|
|
262
|
-
|
|
263
|
-
|
|
264
|
-
|
|
265
|
-
|
|
266
|
-
|
|
267
|
-
|
|
268
|
-
|
|
269
|
-
|
|
270
|
-
|
|
271
|
-
|
|
272
|
-
|
|
273
|
-
|
|
274
|
-
|
|
275
|
-
|
|
276
|
-
|
|
277
|
-
|
|
278
|
-
|
|
279
|
-
|
|
280
|
-
|
|
281
|
-
|
|
282
|
-
|
|
283
|
-
|
|
284
|
-
|
|
285
|
-
|
|
286
|
-
|
|
287
|
-
|
|
288
|
-
|
|
289
|
-
|
|
290
|
-
}
|
|
291
|
-
|
|
292
|
-
|
|
293
|
-
|
|
294
|
-
|
|
295
|
-
|
|
296
|
-
|
|
297
|
-
|
|
298
|
-
|
|
299
|
-
|
|
300
|
-
|
|
301
|
-
|
|
302
|
-
|
|
303
|
-
|
|
304
|
-
|
|
305
|
-
|
|
306
|
-
|
|
307
|
-
|
|
308
|
-
|
|
309
|
-
|
|
310
|
-
const
|
|
311
|
-
|
|
312
|
-
|
|
313
|
-
|
|
314
|
-
|
|
315
|
-
|
|
316
|
-
|
|
317
|
-
|
|
318
|
-
|
|
319
|
-
|
|
320
|
-
|
|
321
|
-
|
|
322
|
-
|
|
323
|
-
|
|
324
|
-
|
|
325
|
-
|
|
326
|
-
|
|
327
|
-
|
|
328
|
-
|
|
329
|
-
|
|
330
|
-
|
|
331
|
-
|
|
332
|
-
|
|
333
|
-
|
|
334
|
-
|
|
335
|
-
|
|
336
|
-
|
|
337
|
-
|
|
338
|
-
|
|
339
|
-
|
|
340
|
-
|
|
341
|
-
|
|
342
|
-
|
|
343
|
-
|
|
344
|
-
|
|
345
|
-
|
|
346
|
-
|
|
347
|
-
|
|
348
|
-
|
|
349
|
-
|
|
350
|
-
|
|
351
|
-
|
|
352
|
-
|
|
353
|
-
|
|
354
|
-
|
|
355
|
-
|
|
356
|
-
|
|
357
|
-
|
|
358
|
-
|
|
359
|
-
|
|
360
|
-
|
|
361
|
-
|
|
362
|
-
|
|
363
|
-
|
|
364
|
-
|
|
365
|
-
|
|
366
|
-
|
|
367
|
-
|
|
368
|
-
|
|
369
|
-
|
|
370
|
-
|
|
371
|
-
|
|
372
|
-
}
|
|
373
|
-
|
|
374
|
-
|
|
375
|
-
|
|
376
|
-
|
|
377
|
-
|
|
378
|
-
|
|
379
|
-
|
|
380
|
-
const
|
|
381
|
-
|
|
382
|
-
|
|
383
|
-
|
|
384
|
-
|
|
385
|
-
|
|
386
|
-
|
|
387
|
-
|
|
388
|
-
const
|
|
389
|
-
|
|
390
|
-
|
|
391
|
-
|
|
392
|
-
|
|
393
|
-
}
|
|
394
|
-
|
|
395
|
-
|
|
396
|
-
const
|
|
397
|
-
|
|
398
|
-
|
|
399
|
-
|
|
400
|
-
|
|
401
|
-
|
|
402
|
-
|
|
403
|
-
|
|
404
|
-
|
|
405
|
-
const
|
|
406
|
-
|
|
407
|
-
|
|
408
|
-
|
|
409
|
-
|
|
410
|
-
|
|
411
|
-
|
|
412
|
-
|
|
413
|
-
const
|
|
414
|
-
|
|
415
|
-
return
|
|
416
|
-
}
|
|
417
|
-
|
|
418
|
-
async
|
|
419
|
-
const params: Record<string, any> = {};
|
|
420
|
-
const whereSql = parseWhere(this.modelName, args?.where, params, this.dialect);
|
|
421
|
-
const
|
|
422
|
-
const
|
|
423
|
-
return
|
|
424
|
-
}
|
|
425
|
-
|
|
426
|
-
async
|
|
427
|
-
|
|
428
|
-
|
|
429
|
-
|
|
430
|
-
|
|
431
|
-
|
|
432
|
-
|
|
433
|
-
|
|
434
|
-
const
|
|
435
|
-
const
|
|
436
|
-
|
|
437
|
-
|
|
438
|
-
|
|
439
|
-
|
|
440
|
-
|
|
441
|
-
|
|
442
|
-
|
|
443
|
-
|
|
444
|
-
|
|
445
|
-
|
|
446
|
-
|
|
447
|
-
|
|
448
|
-
|
|
449
|
-
|
|
450
|
-
|
|
451
|
-
|
|
452
|
-
|
|
453
|
-
|
|
454
|
-
|
|
455
|
-
|
|
456
|
-
|
|
457
|
-
|
|
458
|
-
|
|
459
|
-
|
|
460
|
-
|
|
461
|
-
|
|
462
|
-
|
|
463
|
-
|
|
464
|
-
|
|
465
|
-
|
|
466
|
-
|
|
467
|
-
|
|
468
|
-
|
|
469
|
-
|
|
470
|
-
|
|
471
|
-
|
|
472
|
-
|
|
473
|
-
|
|
474
|
-
|
|
1
|
+
import { randomUUID } from 'crypto';
|
|
2
|
+
|
|
3
|
+
import {
|
|
4
|
+
An5SheetsAdapter,
|
|
5
|
+
An5SheetsAdapterConfig,
|
|
6
|
+
SheetsTableClient,
|
|
7
|
+
} from './googlesheets';
|
|
8
|
+
import { parseSheetsConnectionString } from './googlesheets/parseConnectionString';
|
|
9
|
+
import type { An5AdapterConfig, Dialect, QueryEngine, TransactionHandle } from './base/types';
|
|
10
|
+
import { buildOrderBy, parseWhere, quote } from './base/sql';
|
|
11
|
+
import { getFieldsForModel, getModelToTable, setAdapterMetadata, type AdapterMetadata } from './base/metadata';
|
|
12
|
+
export type { An5AdapterConfig, Dialect, QueryEngine, TransactionHandle } from './base/types';
|
|
13
|
+
export { setAdapterMetadata, type AdapterMetadata } from './base/metadata';
|
|
14
|
+
|
|
15
|
+
export type AnyAdapter = An5Adapter | An5SheetsAdapter;
|
|
16
|
+
export type AnyAdapterConfig = An5AdapterConfig | An5SheetsAdapterConfig | { connectionString: string };
|
|
17
|
+
|
|
18
|
+
function isSheetsConfig(config: AnyAdapterConfig): config is An5SheetsAdapterConfig {
|
|
19
|
+
return (config as any).spreadsheetId !== undefined;
|
|
20
|
+
}
|
|
21
|
+
|
|
22
|
+
// ─── An5Adapter ────────────────────────────────────────────────────────────────────
|
|
23
|
+
|
|
24
|
+
export class An5Adapter {
|
|
25
|
+
private _engine: QueryEngine | null = null;
|
|
26
|
+
private _engineType: 'postgres' | 'mysql' | 'sqlite' | 'mssql' | null = null;
|
|
27
|
+
private _engineConfig: An5AdapterConfig | null = null;
|
|
28
|
+
sheetsAdapter: An5SheetsAdapter | null = null;
|
|
29
|
+
|
|
30
|
+
get dialect(): Dialect {
|
|
31
|
+
if (this.sheetsAdapter) return 'googlesheets';
|
|
32
|
+
if (this._engine) return this._engine.dialect;
|
|
33
|
+
if (this._engineType === 'postgres') return 'postgres';
|
|
34
|
+
if (this._engineType === 'mysql') return 'mysql';
|
|
35
|
+
if (this._engineType === 'sqlite') return 'sqlite';
|
|
36
|
+
return 'mssql';
|
|
37
|
+
}
|
|
38
|
+
|
|
39
|
+
constructor(adapterConfig: An5AdapterConfig | An5SheetsAdapterConfig) {
|
|
40
|
+
if (isSheetsConfig(adapterConfig)) {
|
|
41
|
+
this.sheetsAdapter = new An5SheetsAdapter(adapterConfig);
|
|
42
|
+
return;
|
|
43
|
+
}
|
|
44
|
+
|
|
45
|
+
const cs = adapterConfig.connectionString.trim();
|
|
46
|
+
if (cs.startsWith('googlesheets://')) {
|
|
47
|
+
this.sheetsAdapter = new An5SheetsAdapter(parseSheetsConnectionString(cs));
|
|
48
|
+
return;
|
|
49
|
+
}
|
|
50
|
+
|
|
51
|
+
this._engineConfig = adapterConfig;
|
|
52
|
+
if (cs.startsWith('postgres://') || cs.startsWith('postgresql://')) {
|
|
53
|
+
this._engineType = 'postgres';
|
|
54
|
+
} else if (cs.startsWith('mysql://') || cs.startsWith('mariadb://')) {
|
|
55
|
+
this._engineType = 'mysql';
|
|
56
|
+
} else if (cs.startsWith('sqlite://') || cs.endsWith('.sqlite') || cs.endsWith('.db')) {
|
|
57
|
+
this._engineType = 'sqlite';
|
|
58
|
+
} else {
|
|
59
|
+
this._engineType = 'mssql';
|
|
60
|
+
}
|
|
61
|
+
}
|
|
62
|
+
|
|
63
|
+
private async requireEngine(): Promise<QueryEngine> {
|
|
64
|
+
if (!this._engine && this._engineType) {
|
|
65
|
+
switch (this._engineType) {
|
|
66
|
+
case 'postgres': {
|
|
67
|
+
const { PostgresEngine } = require('./postgres.js');
|
|
68
|
+
this._engine = new PostgresEngine(this._engineConfig!);
|
|
69
|
+
break;
|
|
70
|
+
}
|
|
71
|
+
case 'mysql': {
|
|
72
|
+
const { MysqlEngine } = require('./mysql.js');
|
|
73
|
+
this._engine = new MysqlEngine(this._engineConfig!);
|
|
74
|
+
break;
|
|
75
|
+
}
|
|
76
|
+
case 'sqlite': {
|
|
77
|
+
const { SqliteEngine } = require('./sqlite.js');
|
|
78
|
+
this._engine = new SqliteEngine(this._engineConfig!);
|
|
79
|
+
break;
|
|
80
|
+
}
|
|
81
|
+
case 'mssql': {
|
|
82
|
+
const { MssqlEngine } = require('./mssql.js');
|
|
83
|
+
this._engine = new MssqlEngine(this._engineConfig!);
|
|
84
|
+
break;
|
|
85
|
+
}
|
|
86
|
+
}
|
|
87
|
+
}
|
|
88
|
+
if (!this._engine) throw new Error('SQL engine is not available for Google Sheets adapter');
|
|
89
|
+
return this._engine;
|
|
90
|
+
}
|
|
91
|
+
|
|
92
|
+
private requireSheetsAdapter(): An5SheetsAdapter {
|
|
93
|
+
if (!this.sheetsAdapter) throw new Error('Google Sheets methods require a googlesheets:// connection or Sheets config');
|
|
94
|
+
return this.sheetsAdapter;
|
|
95
|
+
}
|
|
96
|
+
|
|
97
|
+
async exec<T = any>(query: string, params?: Record<string, any>): Promise<T[]> {
|
|
98
|
+
if (this.sheetsAdapter) return this.sheetsAdapter.exec<T>(query, params);
|
|
99
|
+
return (await this.requireEngine()).exec<T>(query, params);
|
|
100
|
+
}
|
|
101
|
+
|
|
102
|
+
/** INTERNAL: used by AdapterTableClient for DML statements needing row count */
|
|
103
|
+
async _executeRaw(query: string, params?: Record<string, any>): Promise<number> {
|
|
104
|
+
return (await this.requireEngine()).executeRaw(query, params);
|
|
105
|
+
}
|
|
106
|
+
|
|
107
|
+
/** Execute a raw query with positional values → @p_0, @p_1, ... per dialect */
|
|
108
|
+
async $queryRawUnsafe<T = any>(query: string, ...values: any[]): Promise<T[]> {
|
|
109
|
+
if (this.sheetsAdapter) return this.sheetsAdapter.$queryRawUnsafe<T>(query, ...values);
|
|
110
|
+
const engine = await this.requireEngine();
|
|
111
|
+
const params: Record<string, any> = {};
|
|
112
|
+
values.forEach((v, i) => { params[`p_${i}`] = v; });
|
|
113
|
+
let q = query;
|
|
114
|
+
if (this.dialect === 'postgres') {
|
|
115
|
+
let idx = 0;
|
|
116
|
+
q = q.replace(/\$\d+/g, () => `@p_${idx++}`);
|
|
117
|
+
} else if (this.dialect === 'mysql') {
|
|
118
|
+
let idx = 0;
|
|
119
|
+
q = q.replace(/\?/g, () => `@p_${idx++}`);
|
|
120
|
+
}
|
|
121
|
+
return engine.exec<T>(q, params);
|
|
122
|
+
}
|
|
123
|
+
|
|
124
|
+
async $executeRaw(query: string, ...values: any[]): Promise<number> {
|
|
125
|
+
if (this.sheetsAdapter) return this.sheetsAdapter.$executeRaw(query, ...values);
|
|
126
|
+
const engine = await this.requireEngine();
|
|
127
|
+
const params: Record<string, any> = {};
|
|
128
|
+
values.forEach((v, i) => { params[`p_${i}`] = v; });
|
|
129
|
+
let q = query;
|
|
130
|
+
if (this.dialect === 'postgres') {
|
|
131
|
+
let idx = 0;
|
|
132
|
+
q = q.replace(/\$\d+/g, () => `@p_${idx++}`);
|
|
133
|
+
} else if (this.dialect === 'mysql') {
|
|
134
|
+
let idx = 0;
|
|
135
|
+
q = q.replace(/\?/g, () => `@p_${idx++}`);
|
|
136
|
+
}
|
|
137
|
+
return engine.executeRaw(q, params);
|
|
138
|
+
}
|
|
139
|
+
|
|
140
|
+
async $executeRawUnsafe(query: string, ...values: any[]): Promise<number> {
|
|
141
|
+
return this.$executeRaw(query, ...values);
|
|
142
|
+
}
|
|
143
|
+
|
|
144
|
+
async $connect(): Promise<void> {
|
|
145
|
+
if (this.sheetsAdapter) return this.sheetsAdapter.$connect();
|
|
146
|
+
await (await this.requireEngine()).connect();
|
|
147
|
+
}
|
|
148
|
+
|
|
149
|
+
async $disconnect(): Promise<void> {
|
|
150
|
+
if (this.sheetsAdapter) return this.sheetsAdapter.$disconnect();
|
|
151
|
+
await (await this.requireEngine()).disconnect();
|
|
152
|
+
}
|
|
153
|
+
|
|
154
|
+
/** Real transaction with BEGIN / COMMIT / ROLLBACK */
|
|
155
|
+
async $transaction<R>(fn: (tx: An5AdapterTx) => Promise<R>, options?: { timeout?: number }): Promise<R>;
|
|
156
|
+
async $transaction<R>(list: Promise<R>[]): Promise<R[]>;
|
|
157
|
+
async $transaction(fn: any, _options?: any): Promise<any> {
|
|
158
|
+
if (this.sheetsAdapter) return this.sheetsAdapter.$transaction(fn, _options);
|
|
159
|
+
if (Array.isArray(fn)) return Promise.all(fn);
|
|
160
|
+
const engine = await this.requireEngine();
|
|
161
|
+
const handle = await engine.beginTransaction();
|
|
162
|
+
const txAdapter = new An5AdapterTx(handle, this.dialect);
|
|
163
|
+
try {
|
|
164
|
+
const result = await fn(txAdapter);
|
|
165
|
+
await handle.commit();
|
|
166
|
+
return result;
|
|
167
|
+
} catch (err) {
|
|
168
|
+
await handle.rollback();
|
|
169
|
+
throw err;
|
|
170
|
+
}
|
|
171
|
+
}
|
|
172
|
+
|
|
173
|
+
table<T = any>(modelName: string): AdapterTableClient<T> | SheetsTableClient<T> {
|
|
174
|
+
if (this.sheetsAdapter) return this.sheetsAdapter.table<T>(modelName);
|
|
175
|
+
return new AdapterTableClient<T>(this, modelName);
|
|
176
|
+
}
|
|
177
|
+
|
|
178
|
+
async readRange<T = any>(range: string): Promise<T[][]> {
|
|
179
|
+
return this.requireSheetsAdapter().readRange<T>(range);
|
|
180
|
+
}
|
|
181
|
+
|
|
182
|
+
async writeRange(range: string, values: any[][]): Promise<void> {
|
|
183
|
+
await this.requireSheetsAdapter().writeRange(range, values);
|
|
184
|
+
}
|
|
185
|
+
|
|
186
|
+
async appendRange(range: string, values: any[][]): Promise<void> {
|
|
187
|
+
await this.requireSheetsAdapter().appendRange(range, values);
|
|
188
|
+
}
|
|
189
|
+
|
|
190
|
+
async listSheets(): Promise<string[]> {
|
|
191
|
+
return this.requireSheetsAdapter().listSheets();
|
|
192
|
+
}
|
|
193
|
+
|
|
194
|
+
async deleteSheet(name: string): Promise<void> {
|
|
195
|
+
await this.requireSheetsAdapter().deleteSheet(name);
|
|
196
|
+
}
|
|
197
|
+
}
|
|
198
|
+
|
|
199
|
+
// ─── Transaction-scoped adapter ────────────────────────────────────────────────────
|
|
200
|
+
|
|
201
|
+
export class An5AdapterTx {
|
|
202
|
+
constructor(
|
|
203
|
+
private readonly handle: TransactionHandle,
|
|
204
|
+
public readonly dialect: Dialect,
|
|
205
|
+
) { }
|
|
206
|
+
|
|
207
|
+
async exec<T = any>(query: string, params?: Record<string, any>): Promise<T[]> {
|
|
208
|
+
return this.handle.exec<T>(query, params);
|
|
209
|
+
}
|
|
210
|
+
|
|
211
|
+
async _executeRaw(query: string, params?: Record<string, any>): Promise<number> {
|
|
212
|
+
return this.handle.executeRaw(query, params);
|
|
213
|
+
}
|
|
214
|
+
|
|
215
|
+
table<T = any>(modelName: string): AdapterTableClient<T> {
|
|
216
|
+
return new AdapterTableClient<T>(this, modelName);
|
|
217
|
+
}
|
|
218
|
+
}
|
|
219
|
+
|
|
220
|
+
// ─── Table Client ──────────────────────────────────────────────────────────────────
|
|
221
|
+
|
|
222
|
+
export class AdapterTableClient<T = any> {
|
|
223
|
+
constructor(
|
|
224
|
+
private readonly adapter: An5Adapter | An5AdapterTx,
|
|
225
|
+
private readonly modelName: string,
|
|
226
|
+
) { }
|
|
227
|
+
|
|
228
|
+
private get dialect(): Dialect { return this.adapter.dialect; }
|
|
229
|
+
|
|
230
|
+
private get tableName(): string {
|
|
231
|
+
const name = this.modelName;
|
|
232
|
+
let t = name;
|
|
233
|
+
const modelToTable = getModelToTable();
|
|
234
|
+
if (modelToTable[name]) t = modelToTable[name];
|
|
235
|
+
else {
|
|
236
|
+
const camel = name.charAt(0).toLowerCase() + name.slice(1);
|
|
237
|
+
if (modelToTable[camel]) t = modelToTable[camel];
|
|
238
|
+
else {
|
|
239
|
+
const lower = name.toLowerCase();
|
|
240
|
+
if (modelToTable[lower]) t = modelToTable[lower];
|
|
241
|
+
}
|
|
242
|
+
}
|
|
243
|
+
if (t.startsWith('[') || t.startsWith('"') || t.startsWith('`')) return t;
|
|
244
|
+
if (t.includes('.')) return t.split('.').map(p => quote(p, this.dialect)).join('.');
|
|
245
|
+
return quote(t, this.dialect);
|
|
246
|
+
}
|
|
247
|
+
|
|
248
|
+
private get nolock(): string {
|
|
249
|
+
return this.dialect === 'mssql' ? ' WITH (NOLOCK)' : '';
|
|
250
|
+
}
|
|
251
|
+
|
|
252
|
+
private async doExec<U = any>(query: string, params?: Record<string, any>): Promise<U[]> {
|
|
253
|
+
return this.adapter.exec<U>(query, params);
|
|
254
|
+
}
|
|
255
|
+
|
|
256
|
+
private async doExecuteRaw(query: string, params?: Record<string, any>): Promise<number> {
|
|
257
|
+
return this.adapter._executeRaw(query, params);
|
|
258
|
+
}
|
|
259
|
+
|
|
260
|
+
async findMany(args?: { where?: any; orderBy?: any; skip?: number; take?: number; select?: any }): Promise<T[]> {
|
|
261
|
+
const params: Record<string, any> = {};
|
|
262
|
+
const whereSql = parseWhere(this.modelName, args?.where, params, this.dialect);
|
|
263
|
+
const orderSql = buildOrderBy(args?.orderBy, this.dialect);
|
|
264
|
+
const take = args?.take;
|
|
265
|
+
const skip = args?.skip ?? 0;
|
|
266
|
+
|
|
267
|
+
let query: string;
|
|
268
|
+
if (take !== undefined) {
|
|
269
|
+
if (this.dialect === 'postgres' || this.dialect === 'mysql' || this.dialect === 'sqlite') {
|
|
270
|
+
query = `SELECT * FROM ${this.tableName}`;
|
|
271
|
+
if (whereSql) query += ` WHERE ${whereSql}`;
|
|
272
|
+
if (orderSql) query += ` ${orderSql}`;
|
|
273
|
+
query += ` LIMIT ${take} OFFSET ${skip}`;
|
|
274
|
+
} else {
|
|
275
|
+
// mssql requires ORDER BY before OFFSET/FETCH
|
|
276
|
+
query = `SELECT * FROM ${this.tableName}${this.nolock}`;
|
|
277
|
+
if (whereSql) query += ` WHERE ${whereSql}`;
|
|
278
|
+
query += ` ${orderSql || 'ORDER BY (SELECT NULL)'}`;
|
|
279
|
+
query += ` OFFSET ${skip} ROWS FETCH NEXT ${take} ROWS ONLY`;
|
|
280
|
+
}
|
|
281
|
+
} else {
|
|
282
|
+
query = `SELECT * FROM ${this.tableName}${this.nolock}`;
|
|
283
|
+
if (whereSql) query += ` WHERE ${whereSql}`;
|
|
284
|
+
if (orderSql) query += ` ${orderSql}`;
|
|
285
|
+
}
|
|
286
|
+
return this.doExec<T>(query, params);
|
|
287
|
+
}
|
|
288
|
+
|
|
289
|
+
async findFirst(args?: { where?: any; orderBy?: any; select?: any }): Promise<T | null> {
|
|
290
|
+
const rows = await this.findMany({ ...args, take: 1 });
|
|
291
|
+
return rows[0] ?? null;
|
|
292
|
+
}
|
|
293
|
+
|
|
294
|
+
async findUnique(args: { where: any }): Promise<T | null> {
|
|
295
|
+
return this.findFirst({ where: args.where });
|
|
296
|
+
}
|
|
297
|
+
|
|
298
|
+
async count(args?: { where?: any }): Promise<number> {
|
|
299
|
+
const params: Record<string, any> = {};
|
|
300
|
+
const whereSql = parseWhere(this.modelName, args?.where, params, this.dialect);
|
|
301
|
+
// Add NOLOCK for mssql count too
|
|
302
|
+
let query = `SELECT COUNT(*) AS cnt FROM ${this.tableName}${this.nolock}`;
|
|
303
|
+
if (whereSql) query += ` WHERE ${whereSql}`;
|
|
304
|
+
const rows = await this.doExec<any>(query, params);
|
|
305
|
+
return Number(rows[0]?.cnt ?? rows[0]?.CNT ?? 0);
|
|
306
|
+
}
|
|
307
|
+
|
|
308
|
+
async create(args: { data: Partial<T> }): Promise<T> {
|
|
309
|
+
const fields = getFieldsForModel(this.modelName);
|
|
310
|
+
const idFieldName = Object.prototype.hasOwnProperty.call(fields, 'id')
|
|
311
|
+
? 'id'
|
|
312
|
+
: Object.keys(fields).find((name) => name.endsWith('_id') || name.endsWith('Id') || name.toLowerCase() === 'id');
|
|
313
|
+
|
|
314
|
+
const data: any = { ...args.data };
|
|
315
|
+
if (idFieldName) {
|
|
316
|
+
const fieldDef: any = fields[idFieldName];
|
|
317
|
+
const rawType = typeof fieldDef === 'string' ? fieldDef : (fieldDef?.ts || fieldDef?.sql || fieldDef?.type || '');
|
|
318
|
+
const normalizedType = String(rawType).toLowerCase();
|
|
319
|
+
const isStringType = ['string', 'uuid', 'uniqueidentifier', 'nvarchar', 'varchar', 'text'].includes(normalizedType);
|
|
320
|
+
if (isStringType && !data[idFieldName]) data[idFieldName] = randomUUID();
|
|
321
|
+
}
|
|
322
|
+
|
|
323
|
+
const cols = Object.keys(data).filter(k => data[k] !== undefined);
|
|
324
|
+
const params: Record<string, any> = {};
|
|
325
|
+
const vals: string[] = [];
|
|
326
|
+
|
|
327
|
+
for (const col of cols) {
|
|
328
|
+
const p = `c_${col}`;
|
|
329
|
+
params[p] = data[col];
|
|
330
|
+
vals.push(`@${p}`);
|
|
331
|
+
}
|
|
332
|
+
|
|
333
|
+
const query = `INSERT INTO ${this.tableName} (${cols.map(c => quote(c, this.dialect)).join(', ')}) VALUES (${vals.join(', ')})`;
|
|
334
|
+
await this.doExec(query, params);
|
|
335
|
+
return (await this.findFirst({ where: idFieldName ? { [idFieldName]: data[idFieldName] } : data })) as T;
|
|
336
|
+
}
|
|
337
|
+
|
|
338
|
+
async createMany(args: { data: Partial<T>[]; skipDuplicates?: boolean }): Promise<{ count: number }> {
|
|
339
|
+
if (args.data.length === 0) return { count: 0 };
|
|
340
|
+
|
|
341
|
+
const firstCols = Object.keys(args.data[0]).filter(k => (args.data[0] as any)[k] !== undefined);
|
|
342
|
+
|
|
343
|
+
// Bulk INSERT for the common case (same columns, no skipDuplicates)
|
|
344
|
+
if (firstCols.length > 0 && !args.skipDuplicates) {
|
|
345
|
+
const params: Record<string, any> = {};
|
|
346
|
+
const rowPlaceholders: string[] = [];
|
|
347
|
+
|
|
348
|
+
for (let r = 0; r < args.data.length; r++) {
|
|
349
|
+
const row = args.data[r] as any;
|
|
350
|
+
const vals = firstCols.map(col => {
|
|
351
|
+
const p = `r${r}_${col}`;
|
|
352
|
+
params[p] = row[col] ?? null;
|
|
353
|
+
return `@${p}`;
|
|
354
|
+
});
|
|
355
|
+
rowPlaceholders.push(`(${vals.join(', ')})`);
|
|
356
|
+
}
|
|
357
|
+
|
|
358
|
+
const query = `INSERT INTO ${this.tableName} (${firstCols.map(c => quote(c, this.dialect)).join(', ')}) VALUES ${rowPlaceholders.join(', ')}`;
|
|
359
|
+
try {
|
|
360
|
+
await this.doExecuteRaw(query, params);
|
|
361
|
+
return { count: args.data.length };
|
|
362
|
+
} catch {
|
|
363
|
+
// fallback below if bulk fails (e.g. mixed column sets)
|
|
364
|
+
}
|
|
365
|
+
}
|
|
366
|
+
|
|
367
|
+
// Row-by-row fallback
|
|
368
|
+
let count = 0;
|
|
369
|
+
for (const row of args.data) {
|
|
370
|
+
try { await this.create({ data: row }); count++; }
|
|
371
|
+
catch (e) { if (!args.skipDuplicates) throw e; }
|
|
372
|
+
}
|
|
373
|
+
return { count };
|
|
374
|
+
}
|
|
375
|
+
|
|
376
|
+
async update(args: { where: any; data: Partial<T> }): Promise<T> {
|
|
377
|
+
const params: Record<string, any> = {};
|
|
378
|
+
const whereSql = parseWhere(this.modelName, args.where, params, this.dialect, 'w_');
|
|
379
|
+
const setCols = Object.keys(args.data).filter(k => (args.data as any)[k] !== undefined);
|
|
380
|
+
const sets: string[] = [];
|
|
381
|
+
|
|
382
|
+
for (const col of setCols) {
|
|
383
|
+
const p = `s_${col}`;
|
|
384
|
+
params[p] = (args.data as any)[col];
|
|
385
|
+
sets.push(`${quote(col, this.dialect)} = @${p}`);
|
|
386
|
+
}
|
|
387
|
+
|
|
388
|
+
const query = `UPDATE ${this.tableName} SET ${sets.join(', ')}${whereSql ? ` WHERE ${whereSql}` : ''}`;
|
|
389
|
+
await this.doExecuteRaw(query, params);
|
|
390
|
+
return (await this.findFirst({ where: args.where })) as T;
|
|
391
|
+
}
|
|
392
|
+
|
|
393
|
+
async updateMany(args: { where?: any; data: Partial<T> }): Promise<{ count: number }> {
|
|
394
|
+
const params: Record<string, any> = {};
|
|
395
|
+
const whereSql = parseWhere(this.modelName, args.where, params, this.dialect, 'w_');
|
|
396
|
+
const setCols = Object.keys(args.data).filter(k => (args.data as any)[k] !== undefined);
|
|
397
|
+
const sets: string[] = [];
|
|
398
|
+
|
|
399
|
+
for (const col of setCols) {
|
|
400
|
+
const p = `s_${col}`;
|
|
401
|
+
params[p] = (args.data as any)[col];
|
|
402
|
+
sets.push(`${quote(col, this.dialect)} = @${p}`);
|
|
403
|
+
}
|
|
404
|
+
|
|
405
|
+
const query = `UPDATE ${this.tableName} SET ${sets.join(', ')}${whereSql ? ` WHERE ${whereSql}` : ''}`;
|
|
406
|
+
const count = await this.doExecuteRaw(query, params);
|
|
407
|
+
return { count };
|
|
408
|
+
}
|
|
409
|
+
|
|
410
|
+
async delete(args: { where: any }): Promise<T> {
|
|
411
|
+
const existing = await this.findFirst({ where: args.where });
|
|
412
|
+
const params: Record<string, any> = {};
|
|
413
|
+
const whereSql = parseWhere(this.modelName, args.where, params, this.dialect);
|
|
414
|
+
await this.doExecuteRaw(`DELETE FROM ${this.tableName} WHERE ${whereSql}`, params);
|
|
415
|
+
return existing as T;
|
|
416
|
+
}
|
|
417
|
+
|
|
418
|
+
async deleteMany(args?: { where?: any }): Promise<{ count: number }> {
|
|
419
|
+
const params: Record<string, any> = {};
|
|
420
|
+
const whereSql = parseWhere(this.modelName, args?.where, params, this.dialect);
|
|
421
|
+
const query = `DELETE FROM ${this.tableName}${whereSql ? ` WHERE ${whereSql}` : ''}`;
|
|
422
|
+
const count = await this.doExecuteRaw(query, params);
|
|
423
|
+
return { count };
|
|
424
|
+
}
|
|
425
|
+
|
|
426
|
+
async upsert(args: { where: any; create: Partial<T>; update: Partial<T> }): Promise<T> {
|
|
427
|
+
const existing = await this.findFirst({ where: args.where });
|
|
428
|
+
return existing
|
|
429
|
+
? this.update({ where: args.where, data: args.update })
|
|
430
|
+
: this.create({ data: args.create });
|
|
431
|
+
}
|
|
432
|
+
|
|
433
|
+
async aggregate(args: any): Promise<any> {
|
|
434
|
+
const params: Record<string, any> = {};
|
|
435
|
+
const whereSql = parseWhere(this.modelName, args?.where, params, this.dialect);
|
|
436
|
+
const aggs: string[] = [];
|
|
437
|
+
|
|
438
|
+
if (args._count) aggs.push('COUNT(*) AS _count');
|
|
439
|
+
if (args._sum) for (const f of Object.keys(args._sum)) aggs.push(`SUM(${quote(f, this.dialect)}) AS _sum_${f}`);
|
|
440
|
+
if (args._avg) for (const f of Object.keys(args._avg)) aggs.push(`AVG(${quote(f, this.dialect)}) AS _avg_${f}`);
|
|
441
|
+
if (args._min) for (const f of Object.keys(args._min)) aggs.push(`MIN(${quote(f, this.dialect)}) AS _min_${f}`);
|
|
442
|
+
if (args._max) for (const f of Object.keys(args._max)) aggs.push(`MAX(${quote(f, this.dialect)}) AS _max_${f}`);
|
|
443
|
+
|
|
444
|
+
const query = `SELECT ${aggs.join(', ')} FROM ${this.tableName}${whereSql ? ` WHERE ${whereSql}` : ''}`;
|
|
445
|
+
const rows = await this.doExec(query, params);
|
|
446
|
+
return rows[0] ?? {};
|
|
447
|
+
}
|
|
448
|
+
|
|
449
|
+
async groupBy(args: any): Promise<any[]> {
|
|
450
|
+
const params: Record<string, any> = {};
|
|
451
|
+
const whereSql = parseWhere(this.modelName, args?.where, params, this.dialect);
|
|
452
|
+
const byCols = (args.by || []).map((b: string) => quote(b, this.dialect)).join(', ');
|
|
453
|
+
const query = `SELECT ${byCols}, COUNT(*) AS _count FROM ${this.tableName}${whereSql ? ` WHERE ${whereSql}` : ''} GROUP BY ${byCols}`;
|
|
454
|
+
return this.doExec(query, params);
|
|
455
|
+
}
|
|
456
|
+
|
|
457
|
+
async vectorSearch(args: {
|
|
458
|
+
vector: number[];
|
|
459
|
+
take?: number;
|
|
460
|
+
where?: any;
|
|
461
|
+
vectorField?: string;
|
|
462
|
+
distanceMetric?: 'cosine' | 'euclidean' | 'dot';
|
|
463
|
+
}): Promise<(T & { distance: number })[]> {
|
|
464
|
+
const rows = await this.findMany({ where: args.where });
|
|
465
|
+
const vectorField = args.vectorField || 'embedding';
|
|
466
|
+
const metric = args.distanceMetric || 'cosine';
|
|
467
|
+
|
|
468
|
+
const scored: { row: T; dist: number }[] = [];
|
|
469
|
+
for (const row of rows) {
|
|
470
|
+
const raw = (row as any)[vectorField];
|
|
471
|
+
if (!raw) continue;
|
|
472
|
+
let vec: number[] = [];
|
|
473
|
+
try { vec = typeof raw === 'string' ? JSON.parse(raw) : raw; } catch { continue; }
|
|
474
|
+
if (!Array.isArray(vec) || vec.length !== args.vector.length) continue;
|
|
475
|
+
|
|
476
|
+
let dot = 0, m1 = 0, m2 = 0;
|
|
477
|
+
for (let i = 0; i < args.vector.length; i++) {
|
|
478
|
+
dot += args.vector[i] * vec[i];
|
|
479
|
+
m1 += args.vector[i] ** 2;
|
|
480
|
+
m2 += vec[i] ** 2;
|
|
481
|
+
}
|
|
482
|
+
const cosine = m1 && m2 ? dot / (Math.sqrt(m1) * Math.sqrt(m2)) : 0;
|
|
483
|
+
const dist = metric === 'cosine'
|
|
484
|
+
? 1 - cosine
|
|
485
|
+
: metric === 'dot'
|
|
486
|
+
? -dot
|
|
487
|
+
: Math.sqrt(args.vector.reduce((s, v, i) => s + (v - vec[i]) ** 2, 0));
|
|
488
|
+
scored.push({ row, dist });
|
|
489
|
+
}
|
|
490
|
+
scored.sort((a, b) => a.dist - b.dist);
|
|
491
|
+
return scored.slice(0, args.take ?? 10).map(s => ({ ...s.row as any, distance: s.dist }));
|
|
492
|
+
}
|
|
493
|
+
}
|
|
494
|
+
|
|
495
|
+
// ─── Factory ──────────────────────────────────────────────────────────────────────
|
|
496
|
+
|
|
497
|
+
export function createAn5Adapter(config: AnyAdapterConfig): AnyAdapter {
|
|
498
|
+
if (isSheetsConfig(config)) return new An5SheetsAdapter(config);
|
|
499
|
+
if (config.connectionString.trim().startsWith('googlesheets://')) {
|
|
500
|
+
return new An5SheetsAdapter(parseSheetsConnectionString(config.connectionString));
|
|
501
|
+
}
|
|
502
|
+
return new An5Adapter(config);
|
|
503
|
+
}
|
|
504
|
+
|
|
505
|
+
export const createAdapter = createAn5Adapter;
|