@an5/adapters 0.2.1 → 0.2.3
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 +283 -263
- package/dotnet/An5Adapter.cs +275 -275
- package/package.json +56 -53
- package/python/base/metadata.py +10 -10
- package/python/table_client.py +216 -216
- package/typescript/an5Adapter.d.ts +4 -2
- package/typescript/an5Adapter.js +60 -23
- package/typescript/base/uuid.d.ts +5 -0
- package/typescript/base/uuid.js +17 -0
- package/typescript/browser.d.ts +5 -0
- package/typescript/browser.js +12 -0
- package/typescript/googlesheets/adapter.d.ts +1 -1
- package/typescript/googlesheets/adapter.js +22 -11
- package/typescript/googlesheets/an5SheetsAdapter.d.ts +1 -1
- package/typescript/googlesheets/config.d.ts +5 -1
- package/typescript/googlesheets/config.js +2 -1
- package/typescript/googlesheets/helpers.js +28 -9
- package/typescript/googlesheets/index.d.ts +2 -1
- package/typescript/googlesheets/parseConnectionString.js +4 -0
- package/typescript/googlesheets/retry.d.ts +23 -1
- package/typescript/googlesheets/retry.js +87 -11
- package/typescript/googlesheets/tableClient.d.ts +1 -0
- package/typescript/googlesheets/tableClient.js +59 -42
- 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/base/uuid.ts +14 -0
- package/typescript/src/browser.ts +6 -0
- package/typescript/{config.ts → src/config.ts} +95 -95
- package/typescript/{googlesheets → src/googlesheets}/adapter.ts +259 -241
- package/typescript/{googlesheets → src/googlesheets}/an5SheetsAdapter.ts +3 -3
- package/typescript/{googlesheets → src/googlesheets}/config.ts +47 -44
- package/typescript/{googlesheets → src/googlesheets}/helpers.ts +143 -124
- package/typescript/{googlesheets → src/googlesheets}/index.ts +3 -2
- package/typescript/{googlesheets → src/googlesheets}/parseConnectionString.ts +46 -44
- package/typescript/src/googlesheets/retry.ts +126 -0
- package/typescript/{googlesheets → src/googlesheets}/sqlExecutor.ts +301 -301
- package/typescript/{googlesheets → src/googlesheets}/tableClient.ts +455 -435
- 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 -32
|
@@ -1,435 +1,455 @@
|
|
|
1
|
-
import {
|
|
2
|
-
import { getFieldsForModel } from '../base/metadata';
|
|
3
|
-
import type { An5SheetsAdapter } from './adapter';
|
|
4
|
-
import { buildOrderBy, coerceCell, esc, matchWhere, resolveSheetName, sortRows } from './helpers';
|
|
5
|
-
import { withRetry } from './retry';
|
|
6
|
-
|
|
7
|
-
// ───
|
|
8
|
-
|
|
9
|
-
|
|
10
|
-
|
|
11
|
-
|
|
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
|
-
this.adapter.
|
|
42
|
-
|
|
43
|
-
|
|
44
|
-
|
|
45
|
-
|
|
46
|
-
|
|
47
|
-
|
|
48
|
-
|
|
49
|
-
|
|
50
|
-
|
|
51
|
-
|
|
52
|
-
|
|
53
|
-
|
|
54
|
-
|
|
55
|
-
const
|
|
56
|
-
|
|
57
|
-
|
|
58
|
-
|
|
59
|
-
|
|
60
|
-
|
|
61
|
-
|
|
62
|
-
|
|
63
|
-
|
|
64
|
-
|
|
65
|
-
|
|
66
|
-
|
|
67
|
-
|
|
68
|
-
|
|
69
|
-
|
|
70
|
-
|
|
71
|
-
|
|
72
|
-
|
|
73
|
-
|
|
74
|
-
|
|
75
|
-
|
|
76
|
-
|
|
77
|
-
const
|
|
78
|
-
|
|
79
|
-
|
|
80
|
-
await withRetry(() =>
|
|
81
|
-
api.spreadsheets.values.
|
|
82
|
-
spreadsheetId: this.adapter.config.spreadsheetId,
|
|
83
|
-
range: `${this.escSheetName}!A1`,
|
|
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
|
-
const
|
|
152
|
-
return
|
|
153
|
-
|
|
154
|
-
|
|
155
|
-
|
|
156
|
-
|
|
157
|
-
|
|
158
|
-
|
|
159
|
-
|
|
160
|
-
|
|
161
|
-
|
|
162
|
-
|
|
163
|
-
}
|
|
164
|
-
|
|
165
|
-
async
|
|
166
|
-
const rows = await this.findMany({
|
|
167
|
-
return rows
|
|
168
|
-
}
|
|
169
|
-
|
|
170
|
-
async
|
|
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
|
-
const
|
|
214
|
-
|
|
215
|
-
|
|
216
|
-
|
|
217
|
-
|
|
218
|
-
|
|
219
|
-
|
|
220
|
-
|
|
221
|
-
|
|
222
|
-
|
|
223
|
-
|
|
224
|
-
|
|
225
|
-
|
|
226
|
-
|
|
227
|
-
|
|
228
|
-
|
|
229
|
-
|
|
230
|
-
|
|
231
|
-
|
|
232
|
-
|
|
233
|
-
|
|
234
|
-
|
|
235
|
-
|
|
236
|
-
|
|
237
|
-
|
|
238
|
-
|
|
239
|
-
|
|
240
|
-
|
|
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
|
-
const
|
|
283
|
-
const
|
|
284
|
-
if (
|
|
285
|
-
|
|
286
|
-
|
|
287
|
-
const
|
|
288
|
-
|
|
289
|
-
|
|
290
|
-
|
|
291
|
-
|
|
292
|
-
|
|
293
|
-
|
|
294
|
-
|
|
295
|
-
|
|
296
|
-
const
|
|
297
|
-
|
|
298
|
-
|
|
299
|
-
|
|
300
|
-
|
|
301
|
-
|
|
302
|
-
|
|
303
|
-
|
|
304
|
-
|
|
305
|
-
|
|
306
|
-
|
|
307
|
-
|
|
308
|
-
|
|
309
|
-
|
|
310
|
-
|
|
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
|
-
const
|
|
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
|
-
|
|
381
|
-
|
|
382
|
-
|
|
383
|
-
|
|
384
|
-
|
|
385
|
-
const
|
|
386
|
-
|
|
387
|
-
|
|
388
|
-
|
|
389
|
-
|
|
390
|
-
|
|
391
|
-
const
|
|
392
|
-
|
|
393
|
-
|
|
394
|
-
|
|
395
|
-
|
|
396
|
-
|
|
397
|
-
}
|
|
398
|
-
|
|
399
|
-
async
|
|
400
|
-
|
|
401
|
-
|
|
402
|
-
|
|
403
|
-
|
|
404
|
-
|
|
405
|
-
|
|
406
|
-
|
|
407
|
-
|
|
408
|
-
|
|
409
|
-
|
|
410
|
-
|
|
411
|
-
|
|
412
|
-
const
|
|
413
|
-
|
|
414
|
-
|
|
415
|
-
|
|
416
|
-
|
|
417
|
-
|
|
418
|
-
|
|
419
|
-
|
|
420
|
-
|
|
421
|
-
|
|
422
|
-
|
|
423
|
-
|
|
424
|
-
|
|
425
|
-
|
|
426
|
-
|
|
427
|
-
|
|
428
|
-
|
|
429
|
-
|
|
430
|
-
}
|
|
431
|
-
|
|
432
|
-
|
|
433
|
-
|
|
434
|
-
|
|
435
|
-
|
|
1
|
+
import { generateUUID } from '../base/uuid';
|
|
2
|
+
import { getFieldsForModel } from '../base/metadata';
|
|
3
|
+
import type { An5SheetsAdapter } from './adapter';
|
|
4
|
+
import { buildOrderBy, coerceCell, esc, matchWhere, resolveSheetName, sortRows } from './helpers';
|
|
5
|
+
import { withRetry } from './retry';
|
|
6
|
+
|
|
7
|
+
// ─── Utility ──────────────────────────────────────────────────────────────────
|
|
8
|
+
|
|
9
|
+
function resolveIdField(fields: Record<string, any>): string | undefined {
|
|
10
|
+
if (Object.prototype.hasOwnProperty.call(fields, 'id')) return 'id';
|
|
11
|
+
return Object.keys(fields).find(
|
|
12
|
+
name => name.endsWith('_id') || name.endsWith('Id') || name.toLowerCase() === 'id'
|
|
13
|
+
);
|
|
14
|
+
}
|
|
15
|
+
|
|
16
|
+
// ─── Table Client ─────────────────────────────────────────────────────────────
|
|
17
|
+
|
|
18
|
+
export class SheetsTableClient<T = any> {
|
|
19
|
+
constructor(
|
|
20
|
+
private adapter: An5SheetsAdapter,
|
|
21
|
+
private modelName: string,
|
|
22
|
+
private sheetMapping?: Record<string, string>,
|
|
23
|
+
) {}
|
|
24
|
+
|
|
25
|
+
private get sheetName(): string {
|
|
26
|
+
return resolveSheetName(this.modelName, this.sheetMapping);
|
|
27
|
+
}
|
|
28
|
+
|
|
29
|
+
private get escSheetName(): string {
|
|
30
|
+
return esc(this.sheetName);
|
|
31
|
+
}
|
|
32
|
+
|
|
33
|
+
private get fields(): Record<string, { ts: string; sql: string }> {
|
|
34
|
+
return getFieldsForModel(this.modelName);
|
|
35
|
+
}
|
|
36
|
+
|
|
37
|
+
private async ensureSheetExists(): Promise<void> {
|
|
38
|
+
const existing = await this.adapter.getSheetMeta(this.sheetName);
|
|
39
|
+
if (existing) return;
|
|
40
|
+
|
|
41
|
+
const api = await this.adapter.getSheets();
|
|
42
|
+
await withRetry(() =>
|
|
43
|
+
api.spreadsheets.batchUpdate({
|
|
44
|
+
spreadsheetId: this.adapter.config.spreadsheetId,
|
|
45
|
+
requestBody: {
|
|
46
|
+
requests: [{ addSheet: { properties: { title: this.sheetName } } }],
|
|
47
|
+
},
|
|
48
|
+
})
|
|
49
|
+
);
|
|
50
|
+
this.adapter.invalidateCache();
|
|
51
|
+
}
|
|
52
|
+
|
|
53
|
+
private async readAllRows(): Promise<{ headers: string[]; rows: Record<string, any>[] }> {
|
|
54
|
+
const api = await this.adapter.getSheets();
|
|
55
|
+
const res: any = await withRetry(() =>
|
|
56
|
+
api.spreadsheets.values.get({
|
|
57
|
+
spreadsheetId: this.adapter.config.spreadsheetId,
|
|
58
|
+
range: `${this.escSheetName}!A:ZZ`,
|
|
59
|
+
})
|
|
60
|
+
);
|
|
61
|
+
const values = res.data.values || [];
|
|
62
|
+
if (values.length < 1) return { headers: [], rows: [] };
|
|
63
|
+
|
|
64
|
+
const headers = values[0] as string[];
|
|
65
|
+
const rows: Record<string, any>[] = [];
|
|
66
|
+
for (let i = 1; i < values.length; i++) {
|
|
67
|
+
const row: Record<string, any> = { __row: i + 1 };
|
|
68
|
+
for (let j = 0; j < headers.length; j++) {
|
|
69
|
+
row[headers[j]] = coerceCell(values[i][j], this.fields[headers[j]]);
|
|
70
|
+
}
|
|
71
|
+
rows.push(row);
|
|
72
|
+
}
|
|
73
|
+
return { headers, rows };
|
|
74
|
+
}
|
|
75
|
+
|
|
76
|
+
private async getOrCreateHeaders(cols: string[]): Promise<string[]> {
|
|
77
|
+
const api = await this.adapter.getSheets();
|
|
78
|
+
await this.ensureSheetExists();
|
|
79
|
+
|
|
80
|
+
const res: any = await withRetry(() =>
|
|
81
|
+
api.spreadsheets.values.get({
|
|
82
|
+
spreadsheetId: this.adapter.config.spreadsheetId,
|
|
83
|
+
range: `${this.escSheetName}!A1:ZZ1`,
|
|
84
|
+
})
|
|
85
|
+
);
|
|
86
|
+
const existing = (res.data.values || [])[0] || [];
|
|
87
|
+
if (existing.length > 0) return existing as string[];
|
|
88
|
+
|
|
89
|
+
await withRetry(() =>
|
|
90
|
+
api.spreadsheets.values.update({
|
|
91
|
+
spreadsheetId: this.adapter.config.spreadsheetId,
|
|
92
|
+
range: `${this.escSheetName}!A1`,
|
|
93
|
+
valueInputOption: 'RAW',
|
|
94
|
+
requestBody: { values: [cols] },
|
|
95
|
+
})
|
|
96
|
+
);
|
|
97
|
+
return cols;
|
|
98
|
+
}
|
|
99
|
+
|
|
100
|
+
private rowToValues(data: Record<string, any>, headers: string[]): any[] {
|
|
101
|
+
return headers.map(h => {
|
|
102
|
+
const v = data[h];
|
|
103
|
+
if (v instanceof Date) return v.toISOString();
|
|
104
|
+
if (v === null || v === undefined) return '';
|
|
105
|
+
if (typeof v === 'boolean') return v ? 'TRUE' : 'FALSE';
|
|
106
|
+
if (typeof v === 'object') return JSON.stringify(v);
|
|
107
|
+
return String(v);
|
|
108
|
+
});
|
|
109
|
+
}
|
|
110
|
+
|
|
111
|
+
private async deleteRowByIndex(rowIdx: number, sheetIdVal: number): Promise<void> {
|
|
112
|
+
const api = await this.adapter.getSheets();
|
|
113
|
+
await withRetry(() =>
|
|
114
|
+
api.spreadsheets.batchUpdate({
|
|
115
|
+
spreadsheetId: this.adapter.config.spreadsheetId,
|
|
116
|
+
requestBody: {
|
|
117
|
+
requests: [{
|
|
118
|
+
deleteDimension: {
|
|
119
|
+
range: {
|
|
120
|
+
sheetId: sheetIdVal,
|
|
121
|
+
dimension: 'ROWS',
|
|
122
|
+
startIndex: rowIdx - 1,
|
|
123
|
+
endIndex: rowIdx,
|
|
124
|
+
},
|
|
125
|
+
},
|
|
126
|
+
}],
|
|
127
|
+
},
|
|
128
|
+
})
|
|
129
|
+
);
|
|
130
|
+
}
|
|
131
|
+
|
|
132
|
+
// ── CRUD ────────────────────────────────────────────────────────────────────
|
|
133
|
+
|
|
134
|
+
async findMany(args?: { where?: any; orderBy?: any; skip?: number; take?: number; select?: any }): Promise<T[]> {
|
|
135
|
+
const { headers, rows } = await this.readAllRows();
|
|
136
|
+
if (headers.length === 0) return [];
|
|
137
|
+
|
|
138
|
+
let filtered = rows.filter(r => matchWhere(r, args?.where));
|
|
139
|
+
const orderSql = buildOrderBy(args?.orderBy);
|
|
140
|
+
if (orderSql) filtered = sortRows(filtered, orderSql);
|
|
141
|
+
|
|
142
|
+
const skip = args?.skip ?? 0;
|
|
143
|
+
if (args?.take !== undefined) {
|
|
144
|
+
filtered = filtered.slice(skip, skip + args.take);
|
|
145
|
+
} else if (skip > 0) {
|
|
146
|
+
filtered = filtered.slice(skip);
|
|
147
|
+
}
|
|
148
|
+
|
|
149
|
+
const select = args?.select;
|
|
150
|
+
if (select) {
|
|
151
|
+
const selectedKeys = Object.keys(select).filter(k => select[k] === true);
|
|
152
|
+
return filtered.map(r => {
|
|
153
|
+
const obj: any = {};
|
|
154
|
+
for (const k of selectedKeys) obj[k] = r[k];
|
|
155
|
+
return obj as T;
|
|
156
|
+
});
|
|
157
|
+
}
|
|
158
|
+
|
|
159
|
+
return filtered.map(r => {
|
|
160
|
+
const { __row, ...rest } = r;
|
|
161
|
+
return rest as T;
|
|
162
|
+
});
|
|
163
|
+
}
|
|
164
|
+
|
|
165
|
+
async findFirst(args?: { where?: any; orderBy?: any; select?: any }): Promise<T | null> {
|
|
166
|
+
const rows = await this.findMany({ ...args, take: 1 });
|
|
167
|
+
return rows[0] ?? null;
|
|
168
|
+
}
|
|
169
|
+
|
|
170
|
+
async findUnique(args: { where: any }): Promise<T | null> {
|
|
171
|
+
return this.findFirst({ where: args.where });
|
|
172
|
+
}
|
|
173
|
+
|
|
174
|
+
async count(args?: { where?: any }): Promise<number> {
|
|
175
|
+
const rows = await this.findMany({ where: args?.where });
|
|
176
|
+
return rows.length;
|
|
177
|
+
}
|
|
178
|
+
|
|
179
|
+
private assignId(data: any, fields: Record<string, any>): void {
|
|
180
|
+
const idFieldName = resolveIdField(fields);
|
|
181
|
+
if (!idFieldName) return;
|
|
182
|
+
const fieldDef = fields[idFieldName];
|
|
183
|
+
const rawType = typeof fieldDef === 'string' ? fieldDef : (fieldDef?.ts || fieldDef?.sql || '');
|
|
184
|
+
const isStringType = ['string', 'uuid', 'uniqueidentifier', 'nvarchar', 'varchar', 'text'].includes(rawType.toLowerCase());
|
|
185
|
+
if (isStringType && !data[idFieldName]) {
|
|
186
|
+
data[idFieldName] = generateUUID();
|
|
187
|
+
}
|
|
188
|
+
}
|
|
189
|
+
|
|
190
|
+
async create(args: { data: Partial<T> }): Promise<T> {
|
|
191
|
+
const data: any = { ...args.data } as any;
|
|
192
|
+
this.assignId(data, this.fields);
|
|
193
|
+
|
|
194
|
+
const cols = Object.keys(data).filter(k => data[k] !== undefined);
|
|
195
|
+
const headers = await this.getOrCreateHeaders(cols);
|
|
196
|
+
|
|
197
|
+
for (const h of headers) {
|
|
198
|
+
if (data[h] === undefined) data[h] = null;
|
|
199
|
+
}
|
|
200
|
+
|
|
201
|
+
await this.adapter.appendRange(`${this.escSheetName}!A:A`, [
|
|
202
|
+
this.rowToValues(data, headers),
|
|
203
|
+
]);
|
|
204
|
+
|
|
205
|
+
return data as T;
|
|
206
|
+
}
|
|
207
|
+
|
|
208
|
+
async createMany(args: { data: Partial<T>[]; skipDuplicates?: boolean }): Promise<{ count: number }> {
|
|
209
|
+
if (args.data.length === 0) return { count: 0 };
|
|
210
|
+
|
|
211
|
+
const fields = this.fields;
|
|
212
|
+
const allKeys = new Set<string>();
|
|
213
|
+
for (const row of args.data) Object.keys(row as any).forEach(k => allKeys.add(k));
|
|
214
|
+
const cols = [...allKeys];
|
|
215
|
+
|
|
216
|
+
const prepared: Record<string, any>[] = args.data.map(item => {
|
|
217
|
+
const data: any = { ...item } as any;
|
|
218
|
+
this.assignId(data, fields);
|
|
219
|
+
return data;
|
|
220
|
+
});
|
|
221
|
+
|
|
222
|
+
const headers = await this.getOrCreateHeaders(cols);
|
|
223
|
+
const batchValues = prepared.map(data => {
|
|
224
|
+
for (const h of headers) { if (data[h] === undefined) data[h] = null; }
|
|
225
|
+
return this.rowToValues(data, headers);
|
|
226
|
+
});
|
|
227
|
+
|
|
228
|
+
try {
|
|
229
|
+
await this.adapter.appendRange(`${this.escSheetName}!A:A`, batchValues);
|
|
230
|
+
return { count: batchValues.length };
|
|
231
|
+
} catch (e) {
|
|
232
|
+
if (!args.skipDuplicates) throw e;
|
|
233
|
+
let count = 0;
|
|
234
|
+
for (const row of args.data) {
|
|
235
|
+
try { await this.create({ data: row }); count++; } catch { /* skip */ }
|
|
236
|
+
}
|
|
237
|
+
return { count };
|
|
238
|
+
}
|
|
239
|
+
}
|
|
240
|
+
|
|
241
|
+
async update(args: { where: any; data: Partial<T> }): Promise<T> {
|
|
242
|
+
const { headers, rows } = await this.readAllRows();
|
|
243
|
+
const matching = rows.filter(r => matchWhere(r, args.where));
|
|
244
|
+
if (matching.length === 0) throw new Error('No record found matching where clause');
|
|
245
|
+
|
|
246
|
+
const target = matching[0];
|
|
247
|
+
const updated = { ...target, ...args.data as any, __row: target.__row };
|
|
248
|
+
await this.adapter.writeRange(`${this.escSheetName}!A${target.__row}`, [
|
|
249
|
+
this.rowToValues(updated, headers),
|
|
250
|
+
]);
|
|
251
|
+
|
|
252
|
+
const { __row, ...rest } = updated;
|
|
253
|
+
return rest as T;
|
|
254
|
+
}
|
|
255
|
+
|
|
256
|
+
async updateMany(args: { where?: any; data: Partial<T> }): Promise<{ count: number }> {
|
|
257
|
+
const { headers, rows } = await this.readAllRows();
|
|
258
|
+
const matching = rows.filter(r => matchWhere(r, args.where));
|
|
259
|
+
if (matching.length === 0) return { count: 0 };
|
|
260
|
+
|
|
261
|
+
// Batch all writes into a single batchUpdate call
|
|
262
|
+
const api = await this.adapter.getSheets();
|
|
263
|
+
const spreadsheetId = this.adapter.config.spreadsheetId;
|
|
264
|
+
const valueRanges = matching.map(target => {
|
|
265
|
+
const updated = { ...target, ...args.data as any };
|
|
266
|
+
return {
|
|
267
|
+
range: `${this.escSheetName}!A${target.__row}`,
|
|
268
|
+
values: [this.rowToValues(updated, headers)],
|
|
269
|
+
};
|
|
270
|
+
});
|
|
271
|
+
|
|
272
|
+
await withRetry(() =>
|
|
273
|
+
(api as any).spreadsheets.values.batchUpdate({
|
|
274
|
+
spreadsheetId,
|
|
275
|
+
requestBody: { valueInputOption: 'RAW', data: valueRanges },
|
|
276
|
+
})
|
|
277
|
+
);
|
|
278
|
+
return { count: matching.length };
|
|
279
|
+
}
|
|
280
|
+
|
|
281
|
+
async delete(args: { where: any }): Promise<T> {
|
|
282
|
+
const { rows } = await this.readAllRows();
|
|
283
|
+
const matching = rows.filter(r => matchWhere(r, args.where));
|
|
284
|
+
if (matching.length === 0) throw new Error('No record found matching where clause');
|
|
285
|
+
|
|
286
|
+
const target = matching[0];
|
|
287
|
+
const meta = await this.adapter.getSheetMeta(this.sheetName);
|
|
288
|
+
if (!meta) throw new Error(`Sheet "${this.sheetName}" not found`);
|
|
289
|
+
await this.deleteRowByIndex(target.__row, meta.sheetId);
|
|
290
|
+
|
|
291
|
+
const { __row, ...rest } = target;
|
|
292
|
+
return rest as T;
|
|
293
|
+
}
|
|
294
|
+
|
|
295
|
+
async deleteMany(args?: { where?: any }): Promise<{ count: number }> {
|
|
296
|
+
const { rows } = await this.readAllRows();
|
|
297
|
+
const matching = rows.filter(r => matchWhere(r, args?.where));
|
|
298
|
+
if (matching.length === 0) return { count: 0 };
|
|
299
|
+
|
|
300
|
+
const meta = await this.adapter.getSheetMeta(this.sheetName);
|
|
301
|
+
if (!meta) throw new Error(`Sheet "${this.sheetName}" not found`);
|
|
302
|
+
|
|
303
|
+
// Batch all deletes into a single batchUpdate (sorted DESC to keep row indices stable)
|
|
304
|
+
const api = await this.adapter.getSheets();
|
|
305
|
+
const sorted = [...matching].sort((a, b) => b.__row - a.__row);
|
|
306
|
+
const requests = sorted.map(target => ({
|
|
307
|
+
deleteDimension: {
|
|
308
|
+
range: {
|
|
309
|
+
sheetId: meta.sheetId,
|
|
310
|
+
dimension: 'ROWS',
|
|
311
|
+
startIndex: target.__row - 1,
|
|
312
|
+
endIndex: target.__row,
|
|
313
|
+
},
|
|
314
|
+
},
|
|
315
|
+
}));
|
|
316
|
+
|
|
317
|
+
await withRetry(() =>
|
|
318
|
+
api.spreadsheets.batchUpdate({
|
|
319
|
+
spreadsheetId: this.adapter.config.spreadsheetId,
|
|
320
|
+
requestBody: { requests },
|
|
321
|
+
})
|
|
322
|
+
);
|
|
323
|
+
return { count: matching.length };
|
|
324
|
+
}
|
|
325
|
+
|
|
326
|
+
async deleteAll(): Promise<void> {
|
|
327
|
+
const meta = await this.adapter.getSheetMeta(this.sheetName);
|
|
328
|
+
if (!meta) return;
|
|
329
|
+
const api = await this.adapter.getSheets();
|
|
330
|
+
await withRetry(() =>
|
|
331
|
+
api.spreadsheets.batchUpdate({
|
|
332
|
+
spreadsheetId: this.adapter.config.spreadsheetId,
|
|
333
|
+
requestBody: {
|
|
334
|
+
requests: [{
|
|
335
|
+
deleteDimension: {
|
|
336
|
+
range: {
|
|
337
|
+
sheetId: meta.sheetId,
|
|
338
|
+
dimension: 'ROWS',
|
|
339
|
+
startIndex: 1,
|
|
340
|
+
endIndex: 999999,
|
|
341
|
+
},
|
|
342
|
+
},
|
|
343
|
+
}],
|
|
344
|
+
},
|
|
345
|
+
})
|
|
346
|
+
);
|
|
347
|
+
}
|
|
348
|
+
|
|
349
|
+
async clear(): Promise<void> {
|
|
350
|
+
const api = await this.adapter.getSheets();
|
|
351
|
+
await withRetry(() =>
|
|
352
|
+
api.spreadsheets.values.clear({
|
|
353
|
+
spreadsheetId: this.adapter.config.spreadsheetId,
|
|
354
|
+
range: `${this.escSheetName}!A2:ZZ`,
|
|
355
|
+
})
|
|
356
|
+
);
|
|
357
|
+
}
|
|
358
|
+
|
|
359
|
+
async upsert(args: { where: any; create: Partial<T>; update: Partial<T> }): Promise<T> {
|
|
360
|
+
const existing = await this.findFirst({ where: args.where });
|
|
361
|
+
if (existing) {
|
|
362
|
+
return this.update({ where: args.where, data: args.update });
|
|
363
|
+
}
|
|
364
|
+
return this.create({ data: args.create });
|
|
365
|
+
}
|
|
366
|
+
|
|
367
|
+
async aggregate(args: any): Promise<any> {
|
|
368
|
+
const rows = await this.findMany({ where: args?.where });
|
|
369
|
+
|
|
370
|
+
const result: any = {};
|
|
371
|
+
if (args._count !== undefined) result._count = rows.length;
|
|
372
|
+
if (args._sum) {
|
|
373
|
+
for (const f of Object.keys(args._sum)) {
|
|
374
|
+
const nums = rows.map((r: any) => Number(r[f])).filter(n => !isNaN(n));
|
|
375
|
+
result[`_sum_${f}`] = nums.length > 0 ? nums.reduce((s, n) => s + n, 0) : null;
|
|
376
|
+
}
|
|
377
|
+
}
|
|
378
|
+
if (args._avg) {
|
|
379
|
+
for (const f of Object.keys(args._avg)) {
|
|
380
|
+
const nums = rows.map((r: any) => Number(r[f])).filter(n => !isNaN(n));
|
|
381
|
+
result[`_avg_${f}`] = nums.length > 0 ? nums.reduce((s, n) => s + n, 0) / nums.length : null;
|
|
382
|
+
}
|
|
383
|
+
}
|
|
384
|
+
if (args._min) {
|
|
385
|
+
for (const f of Object.keys(args._min)) {
|
|
386
|
+
const nums = rows.map((r: any) => Number(r[f])).filter(n => !isNaN(n));
|
|
387
|
+
result[`_min_${f}`] = nums.length > 0 ? Math.min(...nums) : null;
|
|
388
|
+
}
|
|
389
|
+
}
|
|
390
|
+
if (args._max) {
|
|
391
|
+
for (const f of Object.keys(args._max)) {
|
|
392
|
+
const nums = rows.map((r: any) => Number(r[f])).filter(n => !isNaN(n));
|
|
393
|
+
result[`_max_${f}`] = nums.length > 0 ? Math.max(...nums) : null;
|
|
394
|
+
}
|
|
395
|
+
}
|
|
396
|
+
return result;
|
|
397
|
+
}
|
|
398
|
+
|
|
399
|
+
async groupBy(args: any): Promise<any[]> {
|
|
400
|
+
const rows = await this.findMany({ where: args?.where });
|
|
401
|
+
const byCols: string[] = args.by || [];
|
|
402
|
+
|
|
403
|
+
const groups: Record<string, any[]> = {};
|
|
404
|
+
for (const row of rows) {
|
|
405
|
+
const key = byCols.map(c => (row as any)[c]).join('|');
|
|
406
|
+
if (!groups[key]) groups[key] = [];
|
|
407
|
+
groups[key].push(row);
|
|
408
|
+
}
|
|
409
|
+
|
|
410
|
+
return Object.entries(groups).map(([key, group]) => {
|
|
411
|
+
const entry: any = {};
|
|
412
|
+
const parts = key.split('|');
|
|
413
|
+
byCols.forEach((c, i) => { entry[c] = parts[i]; });
|
|
414
|
+
entry._count = group.length;
|
|
415
|
+
return entry;
|
|
416
|
+
});
|
|
417
|
+
}
|
|
418
|
+
|
|
419
|
+
async vectorSearch(args: {
|
|
420
|
+
vector: number[];
|
|
421
|
+
take?: number;
|
|
422
|
+
where?: any;
|
|
423
|
+
vectorField?: string;
|
|
424
|
+
distanceMetric?: 'cosine' | 'euclidean' | 'dot';
|
|
425
|
+
}): Promise<(T & { distance: number })[]> {
|
|
426
|
+
const rows = await this.findMany({ where: args.where });
|
|
427
|
+
const vectorField = args.vectorField || 'embedding';
|
|
428
|
+
const metric = args.distanceMetric || 'cosine';
|
|
429
|
+
|
|
430
|
+
const scored: { row: any; dist: number }[] = [];
|
|
431
|
+
for (const raw of rows) {
|
|
432
|
+
const row = raw as any;
|
|
433
|
+
const rawVec = row[vectorField];
|
|
434
|
+
if (!rawVec) continue;
|
|
435
|
+
let vec: number[] = [];
|
|
436
|
+
try { vec = typeof rawVec === 'string' ? JSON.parse(rawVec) : rawVec; } catch { continue; }
|
|
437
|
+
if (!Array.isArray(vec) || vec.length !== args.vector.length) continue;
|
|
438
|
+
|
|
439
|
+
let dot = 0, m1 = 0, m2 = 0;
|
|
440
|
+
for (let i = 0; i < args.vector.length; i++) {
|
|
441
|
+
dot += args.vector[i] * vec[i];
|
|
442
|
+
m1 += args.vector[i] ** 2;
|
|
443
|
+
m2 += vec[i] ** 2;
|
|
444
|
+
}
|
|
445
|
+
const cosine = m1 && m2 ? dot / (Math.sqrt(m1) * Math.sqrt(m2)) : 0;
|
|
446
|
+
const dist = metric === 'cosine' ? 1 - cosine
|
|
447
|
+
: metric === 'dot' ? -dot
|
|
448
|
+
: Math.sqrt(args.vector.reduce((s, v, i) => s + (v - vec[i]) ** 2, 0));
|
|
449
|
+
scored.push({ row, dist });
|
|
450
|
+
}
|
|
451
|
+
|
|
452
|
+
scored.sort((a, b) => a.dist - b.dist);
|
|
453
|
+
return scored.slice(0, args.take ?? 10).map(s => ({ ...s.row, distance: s.dist }));
|
|
454
|
+
}
|
|
455
|
+
}
|