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