@an5/adapters 0.2.4 → 0.2.6

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