@an5/adapters 0.1.0

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 (91) hide show
  1. package/README.md +263 -0
  2. package/dotnet/An5Adapter.cs +275 -0
  3. package/dotnet/Base/Core.cs +82 -0
  4. package/dotnet/Mssql/MssqlEngine.cs +144 -0
  5. package/dotnet/Postgres/PostgresEngine.cs +121 -0
  6. package/package.json +46 -0
  7. package/python/an5_adapter.py +97 -0
  8. package/python/base/__init__.py +3 -0
  9. package/python/base/dialects.py +8 -0
  10. package/python/base/metadata.py +9 -0
  11. package/python/base/sql.py +121 -0
  12. package/python/mssql/__init__.py +1 -0
  13. package/python/mssql/provider.py +62 -0
  14. package/python/postgres/__init__.py +1 -0
  15. package/python/postgres/provider.py +15 -0
  16. package/python/table_client.py +209 -0
  17. package/typescript/an5Adapter.d.ts +117 -0
  18. package/typescript/an5Adapter.js +447 -0
  19. package/typescript/an5Adapter.ts +474 -0
  20. package/typescript/base/index.d.ts +3 -0
  21. package/typescript/base/index.js +19 -0
  22. package/typescript/base/index.ts +3 -0
  23. package/typescript/base/metadata.d.ts +14 -0
  24. package/typescript/base/metadata.js +17 -0
  25. package/typescript/base/metadata.ts +28 -0
  26. package/typescript/base/sql.d.ts +4 -0
  27. package/typescript/base/sql.js +143 -0
  28. package/typescript/base/sql.ts +98 -0
  29. package/typescript/base/types.d.ts +21 -0
  30. package/typescript/base/types.js +2 -0
  31. package/typescript/base/types.ts +24 -0
  32. package/typescript/config.d.ts +17 -0
  33. package/typescript/config.js +92 -0
  34. package/typescript/config.ts +95 -0
  35. package/typescript/googlesheets/adapter.d.ts +31 -0
  36. package/typescript/googlesheets/adapter.js +138 -0
  37. package/typescript/googlesheets/adapter.ts +167 -0
  38. package/typescript/googlesheets/an5SheetsAdapter.d.ts +3 -0
  39. package/typescript/googlesheets/an5SheetsAdapter.js +8 -0
  40. package/typescript/googlesheets/an5SheetsAdapter.ts +3 -0
  41. package/typescript/googlesheets/config.d.ts +17 -0
  42. package/typescript/googlesheets/config.js +29 -0
  43. package/typescript/googlesheets/config.ts +33 -0
  44. package/typescript/googlesheets/helpers.d.ts +9 -0
  45. package/typescript/googlesheets/helpers.js +155 -0
  46. package/typescript/googlesheets/helpers.ts +124 -0
  47. package/typescript/googlesheets/index.d.ts +2 -0
  48. package/typescript/googlesheets/index.js +9 -0
  49. package/typescript/googlesheets/index.ts +2 -0
  50. package/typescript/googlesheets/parseConnectionString.d.ts +11 -0
  51. package/typescript/googlesheets/parseConnectionString.js +44 -0
  52. package/typescript/googlesheets/parseConnectionString.ts +44 -0
  53. package/typescript/googlesheets/retry.d.ts +3 -0
  54. package/typescript/googlesheets/retry.js +26 -0
  55. package/typescript/googlesheets/retry.ts +23 -0
  56. package/typescript/googlesheets/sqlExecutor.d.ts +12 -0
  57. package/typescript/googlesheets/sqlExecutor.js +306 -0
  58. package/typescript/googlesheets/sqlExecutor.ts +301 -0
  59. package/typescript/googlesheets/tableClient.d.ts +78 -0
  60. package/typescript/googlesheets/tableClient.js +394 -0
  61. package/typescript/googlesheets/tableClient.ts +434 -0
  62. package/typescript/googlesheets/types.d.ts +4 -0
  63. package/typescript/googlesheets/types.js +3 -0
  64. package/typescript/googlesheets/types.ts +6 -0
  65. package/typescript/index.d.ts +2 -0
  66. package/typescript/index.js +14 -0
  67. package/typescript/index.ts +15 -0
  68. package/typescript/mssql/engine.d.ts +16 -0
  69. package/typescript/mssql/engine.js +138 -0
  70. package/typescript/mssql/engine.ts +132 -0
  71. package/typescript/mssql/index.d.ts +1 -0
  72. package/typescript/mssql/index.js +17 -0
  73. package/typescript/mssql/index.ts +1 -0
  74. package/typescript/mysql/engine.d.ts +13 -0
  75. package/typescript/mysql/engine.js +77 -0
  76. package/typescript/mysql/engine.ts +80 -0
  77. package/typescript/mysql/index.d.ts +1 -0
  78. package/typescript/mysql/index.js +17 -0
  79. package/typescript/mysql/index.ts +1 -0
  80. package/typescript/postgres/engine.d.ts +13 -0
  81. package/typescript/postgres/engine.js +86 -0
  82. package/typescript/postgres/engine.ts +87 -0
  83. package/typescript/postgres/index.d.ts +1 -0
  84. package/typescript/postgres/index.js +17 -0
  85. package/typescript/postgres/index.ts +1 -0
  86. package/typescript/sqlite/engine.d.ts +13 -0
  87. package/typescript/sqlite/engine.js +58 -0
  88. package/typescript/sqlite/engine.ts +55 -0
  89. package/typescript/sqlite/index.d.ts +1 -0
  90. package/typescript/sqlite/index.js +17 -0
  91. package/typescript/sqlite/index.ts +1 -0
@@ -0,0 +1,44 @@
1
+ import type { An5SheetsAdapterConfig } from './config';
2
+
3
+ /**
4
+ * Parse a `googlesheets://` connection string into An5SheetsAdapterConfig.
5
+ *
6
+ * Format:
7
+ * googlesheets://spreadsheetId;clientEmail=xxx;privateKey=xxx
8
+ * googlesheets://spreadsheetId;clientEmail=xxx;privateKey=xxx;sheetMapping=users:Users,orders:Orders
9
+ *
10
+ * The privateKey must be URL-encoded (use encodeURIComponent on the raw key).
11
+ */
12
+ export function parseSheetsConnectionString(url: string): An5SheetsAdapterConfig {
13
+ const clean = url.replace(/^googlesheets:\/\//, '');
14
+ const parts = clean.split(';');
15
+
16
+ const spreadsheetId = parts[0].trim();
17
+ const config: Record<string, string> = {};
18
+
19
+ for (let i = 1; i < parts.length; i++) {
20
+ const part = parts[i].trim();
21
+ if (!part) continue;
22
+ const eqIdx = part.indexOf('=');
23
+ if (eqIdx === -1) continue;
24
+ const key = part.slice(0, eqIdx).trim().toLowerCase();
25
+ const value = decodeURIComponent(part.slice(eqIdx + 1).trim());
26
+ config[key] = value;
27
+ }
28
+
29
+ const result: An5SheetsAdapterConfig = { spreadsheetId };
30
+
31
+ if (config.clientemail) result.clientEmail = config.clientemail;
32
+ if (config.privatekey) result.privateKey = config.privatekey;
33
+
34
+ if (config.sheetmapping) {
35
+ const mapping: Record<string, string> = {};
36
+ for (const pair of config.sheetmapping.split(',')) {
37
+ const [k, v] = pair.split(':');
38
+ if (k && v) mapping[k.trim()] = v.trim();
39
+ }
40
+ result.sheetMapping = mapping;
41
+ }
42
+
43
+ return result;
44
+ }
@@ -0,0 +1,3 @@
1
+ export declare const MAX_RETRIES = 3;
2
+ export declare const BASE_DELAY = 1000;
3
+ export declare function withRetry<T>(fn: () => Promise<T>, attempt?: number): Promise<T>;
@@ -0,0 +1,26 @@
1
+ "use strict";
2
+ // ─── Retry helper ─────────────────────────────────────────────────────────────
3
+ Object.defineProperty(exports, "__esModule", { value: true });
4
+ exports.BASE_DELAY = exports.MAX_RETRIES = void 0;
5
+ exports.withRetry = withRetry;
6
+ exports.MAX_RETRIES = 3;
7
+ exports.BASE_DELAY = 1000;
8
+ async function withRetry(fn, attempt = 1) {
9
+ try {
10
+ return await fn();
11
+ }
12
+ catch (err) {
13
+ const code = err?.code || err?.status || err?.response?.status;
14
+ if ((code === 429 || code === 500 || code === 503) && attempt <= exports.MAX_RETRIES) {
15
+ const delay = exports.BASE_DELAY * Math.pow(2, attempt - 1) + Math.random() * 500;
16
+ await new Promise(r => setTimeout(r, delay));
17
+ return withRetry(fn, attempt + 1);
18
+ }
19
+ if (code === 403 && err?.errors?.[0]?.reason === 'rateLimitExceeded' && attempt <= exports.MAX_RETRIES) {
20
+ const delay = exports.BASE_DELAY * Math.pow(2, attempt - 1) + Math.random() * 500;
21
+ await new Promise(r => setTimeout(r, delay));
22
+ return withRetry(fn, attempt + 1);
23
+ }
24
+ throw err;
25
+ }
26
+ }
@@ -0,0 +1,23 @@
1
+ // ─── Retry helper ─────────────────────────────────────────────────────────────
2
+
3
+ export const MAX_RETRIES = 3;
4
+ export const BASE_DELAY = 1000;
5
+
6
+ export async function withRetry<T>(fn: () => Promise<T>, attempt = 1): Promise<T> {
7
+ try {
8
+ return await fn();
9
+ } catch (err: any) {
10
+ const code = err?.code || err?.status || err?.response?.status;
11
+ if ((code === 429 || code === 500 || code === 503) && attempt <= MAX_RETRIES) {
12
+ const delay = BASE_DELAY * Math.pow(2, attempt - 1) + Math.random() * 500;
13
+ await new Promise(r => setTimeout(r, delay));
14
+ return withRetry(fn, attempt + 1);
15
+ }
16
+ if (code === 403 && err?.errors?.[0]?.reason === 'rateLimitExceeded' && attempt <= MAX_RETRIES) {
17
+ const delay = BASE_DELAY * Math.pow(2, attempt - 1) + Math.random() * 500;
18
+ await new Promise(r => setTimeout(r, delay));
19
+ return withRetry(fn, attempt + 1);
20
+ }
21
+ throw err;
22
+ }
23
+ }
@@ -0,0 +1,12 @@
1
+ import type { An5SheetsAdapter } from './adapter';
2
+ export type ExecResult = any[] | number;
3
+ export declare function execQuery(adapter: An5SheetsAdapter, query: string, params: Record<string, any> | undefined, _modelMap?: Record<string, string>, _sheetId?: string): Promise<ExecResult>;
4
+ export declare function substParams(sql: string, params?: Record<string, any>): {
5
+ text: string;
6
+ values: any[];
7
+ };
8
+ export declare function matchRowSQL(row: Record<string, any>, whereText: string, values: any[]): boolean;
9
+ export declare function extractColsAndVals(sql: string, params?: Record<string, any>): {
10
+ cols: string[];
11
+ vals: any[];
12
+ };
@@ -0,0 +1,306 @@
1
+ "use strict";
2
+ Object.defineProperty(exports, "__esModule", { value: true });
3
+ exports.execQuery = execQuery;
4
+ exports.substParams = substParams;
5
+ exports.matchRowSQL = matchRowSQL;
6
+ exports.extractColsAndVals = extractColsAndVals;
7
+ const helpers_1 = require("./helpers");
8
+ const retry_1 = require("./retry");
9
+ async function execQuery(adapter, query, params, _modelMap, _sheetId) {
10
+ const q = query.trim();
11
+ const upper = q.toUpperCase();
12
+ if (upper.startsWith('SELECT'))
13
+ return execSelect(adapter, q, params);
14
+ if (upper.startsWith('INSERT'))
15
+ return execInsert(adapter, q, params);
16
+ if (upper.startsWith('UPDATE'))
17
+ return execUpdate(adapter, q, params);
18
+ if (upper.startsWith('DELETE'))
19
+ return execDelete(adapter, q, params);
20
+ // Fallback: treat as raw range or sheet name
21
+ const range = q.includes('!') ? q : (0, helpers_1.esc)(q) + '!A:ZZ';
22
+ const raw = await adapter.readRange(range);
23
+ return raw;
24
+ }
25
+ function substParams(sql, params) {
26
+ if (!params)
27
+ return { text: sql, values: [] };
28
+ const values = [];
29
+ const text = sql.replace(/@(\w+)/g, (_, name) => {
30
+ if (params[name] !== undefined) {
31
+ const idx = values.length;
32
+ values.push(params[name]);
33
+ return `?${idx}`;
34
+ }
35
+ return `@${name}`;
36
+ });
37
+ return { text, values };
38
+ }
39
+ function matchRowSQL(row, whereText, values) {
40
+ if (!whereText)
41
+ return true;
42
+ const trimmed = whereText.trim();
43
+ // Handle simple AND conditions: field = ?0 AND field2 = ?1
44
+ const parts = trimmed.split(/\s+AND\s+/i);
45
+ for (const part of parts) {
46
+ const m = part.match(/^(.+?)\s*(=|!=|<>|>|<|>=|<=|LIKE|CONTAINS)\s*(.+)$/i);
47
+ if (!m)
48
+ continue;
49
+ const field = m[1].trim().replace(/[[\]]/g, '');
50
+ const op = m[2].toUpperCase();
51
+ const valRef = m[3].trim();
52
+ const val = valRef.startsWith('?') ? values[parseInt(valRef.slice(1))] : valRef.replace(/^['"]|['"]$/g, '');
53
+ const cell = row[field];
54
+ switch (op) {
55
+ case '=':
56
+ if (cell != val)
57
+ return false;
58
+ break;
59
+ case '!=':
60
+ case '<>':
61
+ if (cell == val)
62
+ return false;
63
+ break;
64
+ case '>':
65
+ if (!(Number(cell) > Number(val)))
66
+ return false;
67
+ break;
68
+ case '<':
69
+ if (!(Number(cell) < Number(val)))
70
+ return false;
71
+ break;
72
+ case '>=':
73
+ if (!(Number(cell) >= Number(val)))
74
+ return false;
75
+ break;
76
+ case '<=':
77
+ if (!(Number(cell) <= Number(val)))
78
+ return false;
79
+ break;
80
+ case 'LIKE':
81
+ if (!cell || !String(cell).toLowerCase().includes(String(val).toLowerCase().replace(/%/g, '')))
82
+ return false;
83
+ break;
84
+ case 'CONTAINS':
85
+ if (!cell || !String(cell).includes(String(val)))
86
+ return false;
87
+ break;
88
+ }
89
+ }
90
+ return true;
91
+ }
92
+ function extractColsAndVals(sql, params) {
93
+ const cols = [];
94
+ const vals = [];
95
+ const colMatch = sql.match(/\(([^)]+)\)\s*VALUES/i);
96
+ if (colMatch) {
97
+ const rawCols = colMatch[1].split(',').map(c => c.trim().replace(/[[\]]/g, ''));
98
+ // Find actual values
99
+ const valMatch = sql.substring(colMatch.index + colMatch[0].length).match(/\(([^)]+)\)/);
100
+ if (valMatch) {
101
+ const rawVals = valMatch[1].split(',').map(v => v.trim());
102
+ for (let i = 0; i < rawCols.length; i++) {
103
+ cols.push(rawCols[i]);
104
+ const rv = rawVals[i];
105
+ if (rv.startsWith('@') && params && params[rv.slice(1)] !== undefined) {
106
+ vals.push(params[rv.slice(1)]);
107
+ }
108
+ else {
109
+ vals.push(rv.replace(/^['"]|['"]$/g, ''));
110
+ }
111
+ }
112
+ }
113
+ }
114
+ return { cols, vals };
115
+ }
116
+ async function execSelect(adapter, query, params) {
117
+ const m = query.match(/SELECT\s+(.+?)\s+FROM\s+(?:'([^']+)'|"([^"]+)"|(\S+))\s*(?:WHERE\s+(.+))?$/i);
118
+ if (!m) {
119
+ // Try reading as raw range
120
+ const raw = await adapter.readRange((0, helpers_1.esc)(query.replace(/^SELECT\s+\*\s+FROM\s+/i, '').trim()) + '!A:ZZ');
121
+ const headers = raw[0] || [];
122
+ return raw.slice(1).map(r => {
123
+ const obj = {};
124
+ headers.forEach((h, i) => { obj[h] = r[i] ?? null; });
125
+ return obj;
126
+ });
127
+ }
128
+ const sheetName = m[2] || m[3] || m[4];
129
+ const whereClause = m[5];
130
+ const raw = await adapter.readRange((0, helpers_1.esc)(sheetName) + '!A:ZZ');
131
+ if (raw.length < 1)
132
+ return [];
133
+ const headers = raw[0];
134
+ const fieldList = m[1].trim();
135
+ const selectAll = fieldList === '*';
136
+ const rows = raw.slice(1).map(row => {
137
+ const obj = {};
138
+ headers.forEach((h, i) => { obj[h] = row[i] ?? null; });
139
+ return obj;
140
+ });
141
+ if (!whereClause)
142
+ return selectAll ? rows : rows.map(r => pickFields(r, fieldList, headers));
143
+ const { text, values } = substParams(query, params);
144
+ // Extract just the WHERE part
145
+ const whereParts = query.match(/WHERE\s+(.+)$/i);
146
+ const whereText = whereParts ? whereParts[1] : '';
147
+ const { text: whereSubst, values: whereVals } = substParams(whereText, params);
148
+ const result = rows.filter(r => matchRowSQL(r, whereSubst, whereVals));
149
+ return selectAll ? result : result.map(r => pickFields(r, fieldList, headers));
150
+ }
151
+ function pickFields(row, fieldList, headers) {
152
+ const fields = fieldList.split(',').map(f => f.trim().replace(/[[\]]/g, ''));
153
+ const obj = {};
154
+ fields.forEach(f => { obj[f] = row[f]; });
155
+ return obj;
156
+ }
157
+ async function execInsert(adapter, query, params) {
158
+ const m = query.match(/INSERT\s+INTO\s+(?:'([^']+)'|"([^"]+)"|(\S+))\s*(.*)/i);
159
+ if (!m)
160
+ throw new Error('Invalid INSERT syntax');
161
+ const sheetName = m[1] || m[2] || m[3];
162
+ const { cols, vals } = extractColsAndVals(query, params);
163
+ if (cols.length === 0)
164
+ throw new Error('Cannot extract columns from INSERT');
165
+ // Ensure headers exist
166
+ const raw = await adapter.readRange((0, helpers_1.esc)(sheetName) + '!A1:ZZ1');
167
+ const existingHeaders = (raw[0] || []);
168
+ let headers = existingHeaders.length > 0 ? existingHeaders : cols;
169
+ if (existingHeaders.length === 0) {
170
+ // Write headers first
171
+ const api = await adapter.getSheets();
172
+ await (0, retry_1.withRetry)(() => api.spreadsheets.values.update({
173
+ spreadsheetId: adapter.config.spreadsheetId,
174
+ range: (0, helpers_1.esc)(sheetName) + '!A1',
175
+ valueInputOption: 'RAW',
176
+ requestBody: { values: [cols] },
177
+ }));
178
+ headers = cols;
179
+ }
180
+ const rowData = {};
181
+ cols.forEach((c, i) => { rowData[c] = vals[i]; });
182
+ for (const h of headers) {
183
+ if (rowData[h] === undefined)
184
+ rowData[h] = null;
185
+ }
186
+ const rowVals = headers.map(h => {
187
+ const v = rowData[h];
188
+ if (v === null || v === undefined)
189
+ return '';
190
+ if (v instanceof Date)
191
+ return v.toISOString();
192
+ return String(v);
193
+ });
194
+ await adapter.appendRange((0, helpers_1.esc)(sheetName) + '!A:A', [rowVals]);
195
+ return 1;
196
+ }
197
+ async function execUpdate(adapter, query, params) {
198
+ const m = query.match(/UPDATE\s+(?:'([^']+)'|"([^"]+)"|(\S+))\s+SET\s+(.+?)(?:\s+WHERE\s+(.+))?$/i);
199
+ if (!m)
200
+ throw new Error('Invalid UPDATE syntax');
201
+ const sheetName = m[1] || m[2] || m[3];
202
+ const setClause = m[4];
203
+ const whereClause = m[5];
204
+ // Parse SET clause
205
+ const setPairs = setClause.split(',').map(p => p.trim());
206
+ const setFields = [];
207
+ const setVals = [];
208
+ for (const pair of setPairs) {
209
+ const sm = pair.match(/^(\w+)\s*=\s*(.+)$/i);
210
+ if (!sm)
211
+ continue;
212
+ setFields.push(sm[1].replace(/[[\]]/g, ''));
213
+ const rv = sm[2].trim();
214
+ if (rv.startsWith('@') && params && params[rv.slice(1)] !== undefined) {
215
+ setVals.push(params[rv.slice(1)]);
216
+ }
217
+ else {
218
+ setVals.push(rv.replace(/^['"]|['"]$/g, ''));
219
+ }
220
+ }
221
+ // Read data
222
+ const range = (0, helpers_1.esc)(sheetName) + '!A:ZZ';
223
+ const raw = await adapter.readRange(range);
224
+ if (raw.length < 1)
225
+ return 0;
226
+ const headers = raw[0];
227
+ const rows = raw.slice(1);
228
+ // Determine which rows to update
229
+ const { text: whereSubst, values: whereVals } = substParams(whereClause || '', params);
230
+ let count = 0;
231
+ for (let i = 0; i < rows.length; i++) {
232
+ const row = {};
233
+ headers.forEach((h, j) => { row[h] = rows[i][j] ?? null; });
234
+ row.__row = i + 2;
235
+ if (whereClause && !matchRowSQL(row, whereSubst, whereVals))
236
+ continue;
237
+ // Apply SET values
238
+ setFields.forEach((f, idx) => { row[f] = setVals[idx]; });
239
+ const updated = headers.map(h => {
240
+ const v = row[h];
241
+ if (v === null || v === undefined)
242
+ return '';
243
+ if (v instanceof Date)
244
+ return v.toISOString();
245
+ return String(v);
246
+ });
247
+ await adapter.writeRange((0, helpers_1.esc)(sheetName) + `!A${row.__row}`, [updated]);
248
+ count++;
249
+ }
250
+ return count;
251
+ }
252
+ async function execDelete(adapter, query, params) {
253
+ const m = query.match(/DELETE\s+FROM\s+(?:'([^']+)'|"([^"]+)"|(\S+))\s*(?:WHERE\s+(.+))?$/i);
254
+ if (!m)
255
+ throw new Error('Invalid DELETE syntax');
256
+ const sheetName = m[1] || m[2] || m[3];
257
+ const whereClause = m[4];
258
+ const meta = await adapter.getSheetMeta(sheetName);
259
+ if (!meta)
260
+ return 0;
261
+ if (!whereClause) {
262
+ // Delete all rows (keep headers)
263
+ const api = await adapter.getSheets();
264
+ await (0, retry_1.withRetry)(() => api.spreadsheets.batchUpdate({
265
+ spreadsheetId: adapter.config.spreadsheetId,
266
+ requestBody: {
267
+ requests: [{
268
+ deleteDimension: {
269
+ range: { sheetId: meta.sheetId, dimension: 'ROWS', startIndex: 1, endIndex: 999999 },
270
+ },
271
+ }],
272
+ },
273
+ }));
274
+ return -1; // unknown count
275
+ }
276
+ // Read and find matching rows
277
+ const raw = await adapter.readRange((0, helpers_1.esc)(sheetName) + '!A:ZZ');
278
+ if (raw.length < 1)
279
+ return 0;
280
+ const headers = raw[0];
281
+ const { text: whereSubst, values: whereVals } = substParams(whereClause, params);
282
+ const toDelete = [];
283
+ for (let i = 1; i < raw.length; i++) {
284
+ const row = {};
285
+ headers.forEach((h, j) => { row[h] = raw[i][j] ?? null; });
286
+ if (matchRowSQL(row, whereSubst, whereVals)) {
287
+ toDelete.push(i + 1);
288
+ }
289
+ }
290
+ // Delete from bottom to top
291
+ toDelete.sort((a, b) => b - a);
292
+ for (const rowIdx of toDelete) {
293
+ const api = await adapter.getSheets();
294
+ await (0, retry_1.withRetry)(() => api.spreadsheets.batchUpdate({
295
+ spreadsheetId: adapter.config.spreadsheetId,
296
+ requestBody: {
297
+ requests: [{
298
+ deleteDimension: {
299
+ range: { sheetId: meta.sheetId, dimension: 'ROWS', startIndex: rowIdx - 1, endIndex: rowIdx },
300
+ },
301
+ }],
302
+ },
303
+ }));
304
+ }
305
+ return toDelete.length;
306
+ }