@an5/adapters 0.2.2 → 0.2.4

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 (99) hide show
  1. package/README.md +304 -263
  2. package/{typescript → dist}/an5Adapter.js +2 -2
  3. package/dist/base/uuid.d.ts +5 -0
  4. package/dist/base/uuid.js +17 -0
  5. package/{typescript → dist}/googlesheets/adapter.d.ts +1 -1
  6. package/{typescript → dist}/googlesheets/adapter.js +19 -8
  7. package/{typescript → dist}/googlesheets/config.d.ts +5 -1
  8. package/{typescript → dist}/googlesheets/config.js +2 -1
  9. package/{typescript → dist}/googlesheets/helpers.js +28 -9
  10. package/{typescript → dist}/googlesheets/parseConnectionString.js +4 -0
  11. package/dist/googlesheets/retry.d.ts +25 -0
  12. package/dist/googlesheets/retry.js +115 -0
  13. package/{typescript → dist}/googlesheets/tableClient.d.ts +1 -0
  14. package/{typescript → dist}/googlesheets/tableClient.js +59 -42
  15. package/dotnet/An5Adapter.cs +275 -275
  16. package/dotnet/Base/Core.cs +82 -82
  17. package/dotnet/Mssql/MssqlEngine.cs +144 -144
  18. package/dotnet/Postgres/PostgresEngine.cs +121 -121
  19. package/package.json +22 -10
  20. package/python/an5_adapter.py +97 -97
  21. package/python/base/__init__.py +3 -3
  22. package/python/base/dialects.py +8 -8
  23. package/python/base/metadata.py +10 -10
  24. package/python/base/sql.py +121 -121
  25. package/python/mssql/__init__.py +1 -1
  26. package/python/mssql/provider.py +62 -62
  27. package/python/postgres/__init__.py +1 -1
  28. package/python/postgres/provider.py +15 -15
  29. package/python/table_client.py +216 -216
  30. package/typescript/src/an5Adapter.ts +505 -505
  31. package/typescript/src/base/index.ts +3 -3
  32. package/typescript/src/base/metadata.ts +28 -28
  33. package/typescript/src/base/sql.ts +98 -98
  34. package/typescript/src/base/types.ts +24 -24
  35. package/typescript/src/base/uuid.ts +14 -0
  36. package/typescript/src/browser.ts +6 -6
  37. package/typescript/src/config.ts +95 -95
  38. package/typescript/src/googlesheets/adapter.ts +259 -242
  39. package/typescript/src/googlesheets/an5SheetsAdapter.ts +3 -3
  40. package/typescript/src/googlesheets/config.ts +47 -44
  41. package/typescript/src/googlesheets/helpers.ts +143 -124
  42. package/typescript/src/googlesheets/index.ts +3 -3
  43. package/typescript/src/googlesheets/parseConnectionString.ts +46 -44
  44. package/typescript/src/googlesheets/retry.ts +126 -32
  45. package/typescript/src/googlesheets/sqlExecutor.ts +301 -301
  46. package/typescript/src/googlesheets/tableClient.ts +455 -435
  47. package/typescript/src/googlesheets/types.ts +6 -6
  48. package/typescript/src/index.ts +18 -18
  49. package/typescript/src/mssql/engine.ts +132 -132
  50. package/typescript/src/mssql/index.ts +1 -1
  51. package/typescript/src/mysql/engine.ts +80 -80
  52. package/typescript/src/mysql/index.ts +1 -1
  53. package/typescript/src/postgres/engine.ts +87 -87
  54. package/typescript/src/postgres/index.ts +1 -1
  55. package/typescript/src/sqlite/engine.ts +55 -55
  56. package/typescript/src/sqlite/index.ts +1 -1
  57. package/typescript/googlesheets/retry.d.ts +0 -3
  58. package/typescript/googlesheets/retry.js +0 -39
  59. /package/{typescript → dist}/an5Adapter.d.ts +0 -0
  60. /package/{typescript → dist}/base/index.d.ts +0 -0
  61. /package/{typescript → dist}/base/index.js +0 -0
  62. /package/{typescript → dist}/base/metadata.d.ts +0 -0
  63. /package/{typescript → dist}/base/metadata.js +0 -0
  64. /package/{typescript → dist}/base/sql.d.ts +0 -0
  65. /package/{typescript → dist}/base/sql.js +0 -0
  66. /package/{typescript → dist}/base/types.d.ts +0 -0
  67. /package/{typescript → dist}/base/types.js +0 -0
  68. /package/{typescript → dist}/browser.d.ts +0 -0
  69. /package/{typescript → dist}/browser.js +0 -0
  70. /package/{typescript → dist}/config.d.ts +0 -0
  71. /package/{typescript → dist}/config.js +0 -0
  72. /package/{typescript → dist}/googlesheets/an5SheetsAdapter.d.ts +0 -0
  73. /package/{typescript → dist}/googlesheets/an5SheetsAdapter.js +0 -0
  74. /package/{typescript → dist}/googlesheets/helpers.d.ts +0 -0
  75. /package/{typescript → dist}/googlesheets/index.d.ts +0 -0
  76. /package/{typescript → dist}/googlesheets/index.js +0 -0
  77. /package/{typescript → dist}/googlesheets/parseConnectionString.d.ts +0 -0
  78. /package/{typescript → dist}/googlesheets/sqlExecutor.d.ts +0 -0
  79. /package/{typescript → dist}/googlesheets/sqlExecutor.js +0 -0
  80. /package/{typescript → dist}/googlesheets/types.d.ts +0 -0
  81. /package/{typescript → dist}/googlesheets/types.js +0 -0
  82. /package/{typescript → dist}/index.d.ts +0 -0
  83. /package/{typescript → dist}/index.js +0 -0
  84. /package/{typescript → dist}/mssql/engine.d.ts +0 -0
  85. /package/{typescript → dist}/mssql/engine.js +0 -0
  86. /package/{typescript → dist}/mssql/index.d.ts +0 -0
  87. /package/{typescript → dist}/mssql/index.js +0 -0
  88. /package/{typescript → dist}/mysql/engine.d.ts +0 -0
  89. /package/{typescript → dist}/mysql/engine.js +0 -0
  90. /package/{typescript → dist}/mysql/index.d.ts +0 -0
  91. /package/{typescript → dist}/mysql/index.js +0 -0
  92. /package/{typescript → dist}/postgres/engine.d.ts +0 -0
  93. /package/{typescript → dist}/postgres/engine.js +0 -0
  94. /package/{typescript → dist}/postgres/index.d.ts +0 -0
  95. /package/{typescript → dist}/postgres/index.js +0 -0
  96. /package/{typescript → dist}/sqlite/engine.d.ts +0 -0
  97. /package/{typescript → dist}/sqlite/engine.js +0 -0
  98. /package/{typescript → dist}/sqlite/index.d.ts +0 -0
  99. /package/{typescript → dist}/sqlite/index.js +0 -0
@@ -6,12 +6,19 @@ const config_1 = require("./config");
6
6
  const sqlExecutor_1 = require("./sqlExecutor");
7
7
  const tableClient_1 = require("./tableClient");
8
8
  const retry_1 = require("./retry");
9
- // ─── Fetch-based API proxy for OAuth Access Token (browser-compatible) ────────
10
- function createFetchApi(spreadsheetId, accessToken) {
9
+ // ─── Fetch-based API proxy for OAuth Access Token / API Key (browser-compatible) ──
10
+ function createFetchApi(spreadsheetId, accessToken, apiKey) {
11
11
  const base = `https://sheets.googleapis.com/v4/spreadsheets/${spreadsheetId}`;
12
- const headers = { Authorization: `Bearer ${accessToken}`, 'Content-Type': 'application/json' };
12
+ const headers = { 'Content-Type': 'application/json' };
13
+ if (accessToken) {
14
+ headers['Authorization'] = `Bearer ${accessToken}`;
15
+ }
13
16
  async function request(method, path, body) {
14
- const url = path.startsWith('http') ? path : `${base}${path}`;
17
+ let url = path.startsWith('http') ? path : `${base}${path}`;
18
+ if (apiKey) {
19
+ const sep = url.includes('?') ? '&' : '?';
20
+ url += `${sep}key=${encodeURIComponent(apiKey)}`;
21
+ }
15
22
  const opts = { method, headers };
16
23
  if (body)
17
24
  opts.body = JSON.stringify(body);
@@ -73,16 +80,20 @@ class An5SheetsAdapter {
73
80
  this.sheetsCache = null;
74
81
  this.config = (0, config_1.resolveConfig)(config);
75
82
  }
76
- get isOAuth() {
77
- return !!this.config.accessToken;
83
+ get isFetchMode() {
84
+ return !!this.config.accessToken || !!this.config.apiKey;
78
85
  }
79
86
  async getSheets() {
80
- if (this.isOAuth) {
87
+ if (this.isFetchMode) {
81
88
  if (!this.fetchApi) {
82
- this.fetchApi = createFetchApi(this.config.spreadsheetId, this.config.accessToken);
89
+ this.fetchApi = createFetchApi(this.config.spreadsheetId, this.config.accessToken, this.config.apiKey);
83
90
  }
84
91
  return this.fetchApi;
85
92
  }
93
+ const isBrowser = typeof window !== 'undefined' && typeof window.document !== 'undefined';
94
+ if (isBrowser) {
95
+ throw new Error('Google Sheets adapter in browser mode requires "accessToken" or "apiKey" configuration.');
96
+ }
86
97
  if (!this.sheets) {
87
98
  const { google } = await import('googleapis');
88
99
  const auth = new google.auth.JWT({
@@ -11,11 +11,14 @@ export interface An5SheetsAdapterConfig {
11
11
  * When provided, the adapter uses raw fetch() instead of googleapis JWT, making it compatible
12
12
  * with browser environments. Mutually exclusive with clientEmail/privateKey/credentials. */
13
13
  accessToken?: string;
14
+ /** Google API key for browser-based API access. */
15
+ apiKey?: string;
14
16
  }
15
17
  export declare function normalizeKey(key: string): string;
16
18
  export declare function resolveConfig(config: An5SheetsAdapterConfig): {
17
19
  spreadsheetId: string;
18
- accessToken: string;
20
+ accessToken: string | undefined;
21
+ apiKey: string | undefined;
19
22
  sheetMapping: Record<string, string> | undefined;
20
23
  clientEmail?: undefined;
21
24
  privateKey?: undefined;
@@ -25,4 +28,5 @@ export declare function resolveConfig(config: An5SheetsAdapterConfig): {
25
28
  privateKey: string;
26
29
  sheetMapping: Record<string, string> | undefined;
27
30
  accessToken?: undefined;
31
+ apiKey?: undefined;
28
32
  };
@@ -12,10 +12,11 @@ function normalizeKey(key) {
12
12
  return `-----BEGIN PRIVATE KEY-----\n${lines}\n-----END PRIVATE KEY-----`;
13
13
  }
14
14
  function resolveConfig(config) {
15
- if (config.accessToken) {
15
+ if (config.accessToken || config.apiKey) {
16
16
  return {
17
17
  spreadsheetId: config.spreadsheetId,
18
18
  accessToken: config.accessToken,
19
+ apiKey: config.apiKey,
19
20
  sheetMapping: config.sheetMapping,
20
21
  };
21
22
  }
@@ -45,6 +45,12 @@ function matchWhere(row, where) {
45
45
  return false;
46
46
  continue;
47
47
  }
48
+ if (key === 'NOT') {
49
+ const notClauses = Array.isArray(value) ? value : [value];
50
+ if (notClauses.some((v) => matchWhere(row, v)))
51
+ return false;
52
+ continue;
53
+ }
48
54
  const cellVal = row[key];
49
55
  if (value === null) {
50
56
  if (cellVal !== null && cellVal !== undefined)
@@ -52,22 +58,30 @@ function matchWhere(row, where) {
52
58
  }
53
59
  else if (typeof value === 'object' && !(value instanceof Date)) {
54
60
  const v = value;
61
+ const caseInsensitive = v.mode === 'insensitive';
62
+ const normalize = (s) => caseInsensitive ? String(s ?? '').toLowerCase() : s;
55
63
  if (v.not !== undefined) {
56
64
  if (v.not === null) {
57
65
  if (cellVal === null || cellVal === undefined)
58
66
  return false;
59
67
  }
60
- else if (cellVal == v.not)
68
+ else if (normalize(cellVal) == normalize(v.not))
61
69
  return false;
62
70
  }
63
- if (v.equals !== undefined && cellVal != v.equals)
64
- return false;
65
- if (v.contains !== undefined && (!cellVal || !String(cellVal).includes(v.contains)))
66
- return false;
67
- if (v.startsWith !== undefined && (!cellVal || !String(cellVal).startsWith(v.startsWith)))
68
- return false;
69
- if (v.endsWith !== undefined && (!cellVal || !String(cellVal).endsWith(v.endsWith)))
71
+ if (v.equals !== undefined && normalize(cellVal) != normalize(v.equals))
70
72
  return false;
73
+ if (v.contains !== undefined) {
74
+ if (!cellVal || !normalize(String(cellVal)).includes(normalize(v.contains)))
75
+ return false;
76
+ }
77
+ if (v.startsWith !== undefined) {
78
+ if (!cellVal || !normalize(String(cellVal)).startsWith(normalize(v.startsWith)))
79
+ return false;
80
+ }
81
+ if (v.endsWith !== undefined) {
82
+ if (!cellVal || !normalize(String(cellVal)).endsWith(normalize(v.endsWith)))
83
+ return false;
84
+ }
71
85
  if (v.gte !== undefined && !(Number(cellVal) >= Number(v.gte)))
72
86
  return false;
73
87
  if (v.lte !== undefined && !(Number(cellVal) <= Number(v.lte)))
@@ -77,11 +91,16 @@ function matchWhere(row, where) {
77
91
  if (v.lt !== undefined && !(Number(cellVal) < Number(v.lt)))
78
92
  return false;
79
93
  if (v.in !== undefined) {
80
- if (!Array.isArray(v.in) || !v.in.includes(cellVal))
94
+ if (!Array.isArray(v.in) || !v.in.some((item) => item == cellVal))
95
+ return false;
96
+ }
97
+ if (v.notIn !== undefined) {
98
+ if (Array.isArray(v.notIn) && v.notIn.some((item) => item == cellVal))
81
99
  return false;
82
100
  }
83
101
  }
84
102
  else {
103
+ // loose equality to handle string/number coercion from sheet cells
85
104
  if (cellVal != value)
86
105
  return false;
87
106
  }
@@ -31,6 +31,10 @@ function parseSheetsConnectionString(url) {
31
31
  result.clientEmail = config.clientemail;
32
32
  if (config.privatekey)
33
33
  result.privateKey = config.privatekey;
34
+ if (config.accesstoken)
35
+ result.accessToken = config.accesstoken;
36
+ if (config.apikey)
37
+ result.apiKey = config.apikey;
34
38
  if (config.sheetmapping) {
35
39
  const mapping = {};
36
40
  for (const pair of config.sheetmapping.split(',')) {
@@ -0,0 +1,25 @@
1
+ export declare const MAX_RETRIES = 5;
2
+ export declare const BASE_DELAY = 1000;
3
+ export declare const MAX_DELAY = 32000;
4
+ export declare const MAX_CONCURRENCY = 4;
5
+ export declare const RATE_LIMIT_RPS = 0.9;
6
+ declare class TokenBucket {
7
+ private tokens;
8
+ private lastRefill;
9
+ private readonly ratePerMs;
10
+ private readonly capacity;
11
+ constructor(ratePerSecond: number, capacity: number);
12
+ /** Wait until a token is available, then consume one. */
13
+ acquire(): Promise<void>;
14
+ }
15
+ declare class ConcurrencyLimiter {
16
+ private readonly max;
17
+ private running;
18
+ private queue;
19
+ constructor(max: number);
20
+ acquire(): Promise<void>;
21
+ release(): void;
22
+ }
23
+ export declare function withRetry<T>(fn: () => Promise<T>, attempt?: number): Promise<T>;
24
+ /** Expose for testing / custom configurations */
25
+ export { TokenBucket, ConcurrencyLimiter };
@@ -0,0 +1,115 @@
1
+ "use strict";
2
+ // ─── Rate Limit & Retry helper ─────────────────────────────────────────────────
3
+ //
4
+ // Google Sheets API quota (default): 100 requests per 100 seconds per project.
5
+ // Strategy:
6
+ // 1. Token-bucket rate limiter — proactively throttles before hitting quota
7
+ // 2. Concurrency limiter — max N parallel requests to avoid burst
8
+ // 3. Exponential backoff + jitter — reactive recovery when 429/403/5xx hit
9
+ Object.defineProperty(exports, "__esModule", { value: true });
10
+ exports.ConcurrencyLimiter = exports.TokenBucket = exports.RATE_LIMIT_RPS = exports.MAX_CONCURRENCY = exports.MAX_DELAY = exports.BASE_DELAY = exports.MAX_RETRIES = void 0;
11
+ exports.withRetry = withRetry;
12
+ // ─── Config ────────────────────────────────────────────────────────────────────
13
+ exports.MAX_RETRIES = 5;
14
+ exports.BASE_DELAY = 1000; // ms
15
+ exports.MAX_DELAY = 32000; // ms cap for backoff
16
+ exports.MAX_CONCURRENCY = 4; // parallel in-flight requests
17
+ exports.RATE_LIMIT_RPS = 0.9; // tokens/second (stay under 1 req/s safely)
18
+ // ─── Token Bucket ──────────────────────────────────────────────────────────────
19
+ class TokenBucket {
20
+ constructor(ratePerSecond, capacity) {
21
+ this.ratePerMs = ratePerSecond / 1000;
22
+ this.capacity = capacity;
23
+ this.tokens = capacity;
24
+ this.lastRefill = Date.now();
25
+ }
26
+ /** Wait until a token is available, then consume one. */
27
+ async acquire() {
28
+ while (true) {
29
+ const now = Date.now();
30
+ const elapsed = now - this.lastRefill;
31
+ this.tokens = Math.min(this.capacity, this.tokens + elapsed * this.ratePerMs);
32
+ this.lastRefill = now;
33
+ if (this.tokens >= 1) {
34
+ this.tokens -= 1;
35
+ return;
36
+ }
37
+ // Wait for enough time to refill one token
38
+ const waitMs = Math.ceil((1 - this.tokens) / this.ratePerMs);
39
+ await new Promise(r => setTimeout(r, waitMs));
40
+ }
41
+ }
42
+ }
43
+ exports.TokenBucket = TokenBucket;
44
+ // ─── Concurrency Limiter ───────────────────────────────────────────────────────
45
+ class ConcurrencyLimiter {
46
+ constructor(max) {
47
+ this.max = max;
48
+ this.running = 0;
49
+ this.queue = [];
50
+ }
51
+ async acquire() {
52
+ if (this.running < this.max) {
53
+ this.running++;
54
+ return;
55
+ }
56
+ await new Promise(resolve => this.queue.push(resolve));
57
+ this.running++;
58
+ }
59
+ release() {
60
+ this.running--;
61
+ if (this.queue.length > 0) {
62
+ const next = this.queue.shift();
63
+ next();
64
+ }
65
+ }
66
+ }
67
+ exports.ConcurrencyLimiter = ConcurrencyLimiter;
68
+ // ─── Singleton shared across all adapter instances ─────────────────────────────
69
+ const _bucket = new TokenBucket(exports.RATE_LIMIT_RPS, 10);
70
+ const _concurrency = new ConcurrencyLimiter(exports.MAX_CONCURRENCY);
71
+ // ─── Error classification ──────────────────────────────────────────────────────
72
+ function isRetryableError(err) {
73
+ const code = err?.code || err?.status || err?.response?.status;
74
+ if (code === 429)
75
+ return true;
76
+ if (code === 500 || code === 503 || code === 502)
77
+ return true;
78
+ if (code === 403) {
79
+ const reason = err?.errors?.[0]?.reason || '';
80
+ if (reason === 'rateLimitExceeded' || reason === 'userRateLimitExceeded')
81
+ return true;
82
+ if (typeof err?.body === 'string' && err.body.includes('rateLimitExceeded'))
83
+ return true;
84
+ if (typeof err?.message === 'string' && err.message.includes('rateLimitExceeded'))
85
+ return true;
86
+ }
87
+ return false;
88
+ }
89
+ function getRetryDelay(err, attempt) {
90
+ // Honour server-provided Retry-After
91
+ if (err?.retryAfter)
92
+ return err.retryAfter * 1000;
93
+ // Full jitter exponential backoff: random in [0, min(cap, base * 2^attempt)]
94
+ const cap = Math.min(exports.MAX_DELAY, exports.BASE_DELAY * Math.pow(2, attempt));
95
+ return Math.random() * cap;
96
+ }
97
+ // ─── Public API ────────────────────────────────────────────────────────────────
98
+ async function withRetry(fn, attempt = 1) {
99
+ await _bucket.acquire();
100
+ await _concurrency.acquire();
101
+ try {
102
+ return await fn();
103
+ }
104
+ catch (err) {
105
+ if (isRetryableError(err) && attempt <= exports.MAX_RETRIES) {
106
+ const delay = getRetryDelay(err, attempt);
107
+ await new Promise(r => setTimeout(r, delay));
108
+ return withRetry(fn, attempt + 1);
109
+ }
110
+ throw err;
111
+ }
112
+ finally {
113
+ _concurrency.release();
114
+ }
115
+ }
@@ -30,6 +30,7 @@ export declare class SheetsTableClient<T = any> {
30
30
  count(args?: {
31
31
  where?: any;
32
32
  }): Promise<number>;
33
+ private assignId;
33
34
  create(args: {
34
35
  data: Partial<T>;
35
36
  }): Promise<T>;
@@ -1,10 +1,16 @@
1
1
  "use strict";
2
2
  Object.defineProperty(exports, "__esModule", { value: true });
3
3
  exports.SheetsTableClient = void 0;
4
- const crypto_1 = require("crypto");
4
+ const uuid_1 = require("../base/uuid");
5
5
  const metadata_1 = require("../base/metadata");
6
6
  const helpers_1 = require("./helpers");
7
7
  const retry_1 = require("./retry");
8
+ // ─── Utility ──────────────────────────────────────────────────────────────────
9
+ function resolveIdField(fields) {
10
+ if (Object.prototype.hasOwnProperty.call(fields, 'id'))
11
+ return 'id';
12
+ return Object.keys(fields).find(name => name.endsWith('_id') || name.endsWith('Id') || name.toLowerCase() === 'id');
13
+ }
8
14
  // ─── Table Client ─────────────────────────────────────────────────────────────
9
15
  class SheetsTableClient {
10
16
  constructor(adapter, modelName, sheetMapping) {
@@ -146,20 +152,20 @@ class SheetsTableClient {
146
152
  const rows = await this.findMany({ where: args?.where });
147
153
  return rows.length;
148
154
  }
155
+ assignId(data, fields) {
156
+ const idFieldName = resolveIdField(fields);
157
+ if (!idFieldName)
158
+ return;
159
+ const fieldDef = fields[idFieldName];
160
+ const rawType = typeof fieldDef === 'string' ? fieldDef : (fieldDef?.ts || fieldDef?.sql || '');
161
+ const isStringType = ['string', 'uuid', 'uniqueidentifier', 'nvarchar', 'varchar', 'text'].includes(rawType.toLowerCase());
162
+ if (isStringType && !data[idFieldName]) {
163
+ data[idFieldName] = (0, uuid_1.generateUUID)();
164
+ }
165
+ }
149
166
  async create(args) {
150
- const fields = this.fields;
151
- const idFieldName = Object.prototype.hasOwnProperty.call(fields, 'id')
152
- ? 'id'
153
- : Object.keys(fields).find(name => name.endsWith('_id') || name.endsWith('Id') || name.toLowerCase() === 'id');
154
167
  const data = { ...args.data };
155
- if (idFieldName) {
156
- const fieldDef = fields[idFieldName];
157
- const rawType = typeof fieldDef === 'string' ? fieldDef : (fieldDef?.ts || fieldDef?.sql || '');
158
- const isStringType = ['string', 'uuid', 'uniqueidentifier', 'nvarchar', 'varchar', 'text'].includes(rawType.toLowerCase());
159
- if (isStringType && !data[idFieldName]) {
160
- data[idFieldName] = (0, crypto_1.randomUUID)();
161
- }
162
- }
168
+ this.assignId(data, this.fields);
163
169
  const cols = Object.keys(data).filter(k => data[k] !== undefined);
164
170
  const headers = await this.getOrCreateHeaders(cols);
165
171
  for (const h of headers) {
@@ -174,28 +180,16 @@ class SheetsTableClient {
174
180
  async createMany(args) {
175
181
  if (args.data.length === 0)
176
182
  return { count: 0 };
183
+ const fields = this.fields;
177
184
  const allKeys = new Set();
178
- for (const row of args.data) {
185
+ for (const row of args.data)
179
186
  Object.keys(row).forEach(k => allKeys.add(k));
180
- }
181
187
  const cols = [...allKeys];
182
- const fields = this.fields;
183
- const idFieldName = Object.prototype.hasOwnProperty.call(fields, 'id')
184
- ? 'id'
185
- : Object.keys(fields).find(name => name.endsWith('_id') || name.endsWith('Id') || name.toLowerCase() === 'id');
186
- const prepared = [];
187
- for (const item of args.data) {
188
+ const prepared = args.data.map(item => {
188
189
  const data = { ...item };
189
- if (idFieldName) {
190
- const fieldDef = fields[idFieldName];
191
- const rawType = typeof fieldDef === 'string' ? fieldDef : (fieldDef?.ts || fieldDef?.sql || '');
192
- const isStringType = ['string', 'uuid', 'uniqueidentifier', 'nvarchar', 'varchar', 'text'].includes(rawType.toLowerCase());
193
- if (isStringType && !data[idFieldName]) {
194
- data[idFieldName] = (0, crypto_1.randomUUID)();
195
- }
196
- }
197
- prepared.push(data);
198
- }
190
+ this.assignId(data, fields);
191
+ return data;
192
+ });
199
193
  const headers = await this.getOrCreateHeaders(cols);
200
194
  const batchValues = prepared.map(data => {
201
195
  for (const h of headers) {
@@ -238,16 +232,26 @@ class SheetsTableClient {
238
232
  async updateMany(args) {
239
233
  const { headers, rows } = await this.readAllRows();
240
234
  const matching = rows.filter(r => (0, helpers_1.matchWhere)(r, args.where));
241
- for (const target of matching) {
242
- const updated = { ...target, ...args.data, __row: target.__row };
243
- await this.adapter.writeRange(`${this.escSheetName}!A${target.__row}`, [
244
- this.rowToValues(updated, headers),
245
- ]);
246
- }
235
+ if (matching.length === 0)
236
+ return { count: 0 };
237
+ // Batch all writes into a single batchUpdate call
238
+ const api = await this.adapter.getSheets();
239
+ const spreadsheetId = this.adapter.config.spreadsheetId;
240
+ const valueRanges = matching.map(target => {
241
+ const updated = { ...target, ...args.data };
242
+ return {
243
+ range: `${this.escSheetName}!A${target.__row}`,
244
+ values: [this.rowToValues(updated, headers)],
245
+ };
246
+ });
247
+ await (0, retry_1.withRetry)(() => api.spreadsheets.values.batchUpdate({
248
+ spreadsheetId,
249
+ requestBody: { valueInputOption: 'RAW', data: valueRanges },
250
+ }));
247
251
  return { count: matching.length };
248
252
  }
249
253
  async delete(args) {
250
- const { headers, rows } = await this.readAllRows();
254
+ const { rows } = await this.readAllRows();
251
255
  const matching = rows.filter(r => (0, helpers_1.matchWhere)(r, args.where));
252
256
  if (matching.length === 0)
253
257
  throw new Error('No record found matching where clause');
@@ -260,17 +264,30 @@ class SheetsTableClient {
260
264
  return rest;
261
265
  }
262
266
  async deleteMany(args) {
263
- const { headers, rows } = await this.readAllRows();
267
+ const { rows } = await this.readAllRows();
264
268
  const matching = rows.filter(r => (0, helpers_1.matchWhere)(r, args?.where));
265
269
  if (matching.length === 0)
266
270
  return { count: 0 };
267
271
  const meta = await this.adapter.getSheetMeta(this.sheetName);
268
272
  if (!meta)
269
273
  throw new Error(`Sheet "${this.sheetName}" not found`);
274
+ // Batch all deletes into a single batchUpdate (sorted DESC to keep row indices stable)
275
+ const api = await this.adapter.getSheets();
270
276
  const sorted = [...matching].sort((a, b) => b.__row - a.__row);
271
- for (const target of sorted) {
272
- await this.deleteRowByIndex(target.__row, meta.sheetId);
273
- }
277
+ const requests = sorted.map(target => ({
278
+ deleteDimension: {
279
+ range: {
280
+ sheetId: meta.sheetId,
281
+ dimension: 'ROWS',
282
+ startIndex: target.__row - 1,
283
+ endIndex: target.__row,
284
+ },
285
+ },
286
+ }));
287
+ await (0, retry_1.withRetry)(() => api.spreadsheets.batchUpdate({
288
+ spreadsheetId: this.adapter.config.spreadsheetId,
289
+ requestBody: { requests },
290
+ }));
274
291
  return { count: matching.length };
275
292
  }
276
293
  async deleteAll() {