@an5/adapters 0.2.1 → 0.2.3
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/README.md +283 -263
- package/dotnet/An5Adapter.cs +275 -275
- package/package.json +56 -53
- package/python/base/metadata.py +10 -10
- package/python/table_client.py +216 -216
- package/typescript/an5Adapter.d.ts +4 -2
- package/typescript/an5Adapter.js +60 -23
- package/typescript/base/uuid.d.ts +5 -0
- package/typescript/base/uuid.js +17 -0
- package/typescript/browser.d.ts +5 -0
- package/typescript/browser.js +12 -0
- package/typescript/googlesheets/adapter.d.ts +1 -1
- package/typescript/googlesheets/adapter.js +22 -11
- package/typescript/googlesheets/an5SheetsAdapter.d.ts +1 -1
- package/typescript/googlesheets/config.d.ts +5 -1
- package/typescript/googlesheets/config.js +2 -1
- package/typescript/googlesheets/helpers.js +28 -9
- package/typescript/googlesheets/index.d.ts +2 -1
- package/typescript/googlesheets/parseConnectionString.js +4 -0
- package/typescript/googlesheets/retry.d.ts +23 -1
- package/typescript/googlesheets/retry.js +87 -11
- package/typescript/googlesheets/tableClient.d.ts +1 -0
- package/typescript/googlesheets/tableClient.js +59 -42
- package/typescript/index.d.ts +4 -2
- package/typescript/{an5Adapter.ts → src/an5Adapter.ts} +505 -474
- package/typescript/{base → src/base}/index.ts +3 -3
- package/typescript/{base → src/base}/metadata.ts +28 -28
- package/typescript/{base → src/base}/sql.ts +98 -98
- package/typescript/{base → src/base}/types.ts +24 -24
- package/typescript/src/base/uuid.ts +14 -0
- package/typescript/src/browser.ts +6 -0
- package/typescript/{config.ts → src/config.ts} +95 -95
- package/typescript/{googlesheets → src/googlesheets}/adapter.ts +259 -241
- package/typescript/{googlesheets → src/googlesheets}/an5SheetsAdapter.ts +3 -3
- package/typescript/{googlesheets → src/googlesheets}/config.ts +47 -44
- package/typescript/{googlesheets → src/googlesheets}/helpers.ts +143 -124
- package/typescript/{googlesheets → src/googlesheets}/index.ts +3 -2
- package/typescript/{googlesheets → src/googlesheets}/parseConnectionString.ts +46 -44
- package/typescript/src/googlesheets/retry.ts +126 -0
- package/typescript/{googlesheets → src/googlesheets}/sqlExecutor.ts +301 -301
- package/typescript/{googlesheets → src/googlesheets}/tableClient.ts +455 -435
- package/typescript/{googlesheets → src/googlesheets}/types.ts +6 -6
- package/typescript/{index.ts → src/index.ts} +19 -15
- package/typescript/{mssql → src/mssql}/engine.ts +132 -132
- package/typescript/{mysql → src/mssql}/index.ts +1 -1
- package/typescript/{mysql → src/mysql}/engine.ts +80 -80
- package/typescript/{postgres → src/mysql}/index.ts +1 -1
- package/typescript/{postgres → src/postgres}/engine.ts +87 -87
- package/typescript/{sqlite → src/postgres}/index.ts +1 -1
- package/typescript/{sqlite → src/sqlite}/engine.ts +55 -55
- package/typescript/{mssql → src/sqlite}/index.ts +1 -1
- package/typescript/googlesheets/retry.ts +0 -32
package/typescript/an5Adapter.js
CHANGED
|
@@ -2,15 +2,11 @@
|
|
|
2
2
|
Object.defineProperty(exports, "__esModule", { value: true });
|
|
3
3
|
exports.createAdapter = exports.AdapterTableClient = exports.An5AdapterTx = exports.An5Adapter = exports.setAdapterMetadata = void 0;
|
|
4
4
|
exports.createAn5Adapter = createAn5Adapter;
|
|
5
|
-
const
|
|
5
|
+
const uuid_1 = require("./base/uuid");
|
|
6
6
|
const googlesheets_1 = require("./googlesheets");
|
|
7
7
|
const parseConnectionString_1 = require("./googlesheets/parseConnectionString");
|
|
8
8
|
const sql_1 = require("./base/sql");
|
|
9
9
|
const metadata_1 = require("./base/metadata");
|
|
10
|
-
const mssql_1 = require("./mssql");
|
|
11
|
-
const postgres_1 = require("./postgres");
|
|
12
|
-
const mysql_1 = require("./mysql");
|
|
13
|
-
const sqlite_1 = require("./sqlite");
|
|
14
10
|
var metadata_2 = require("./base/metadata");
|
|
15
11
|
Object.defineProperty(exports, "setAdapterMetadata", { enumerable: true, get: function () { return metadata_2.setAdapterMetadata; } });
|
|
16
12
|
function isSheetsConfig(config) {
|
|
@@ -18,9 +14,23 @@ function isSheetsConfig(config) {
|
|
|
18
14
|
}
|
|
19
15
|
// ─── An5Adapter ────────────────────────────────────────────────────────────────────
|
|
20
16
|
class An5Adapter {
|
|
21
|
-
get dialect() {
|
|
17
|
+
get dialect() {
|
|
18
|
+
if (this.sheetsAdapter)
|
|
19
|
+
return 'googlesheets';
|
|
20
|
+
if (this._engine)
|
|
21
|
+
return this._engine.dialect;
|
|
22
|
+
if (this._engineType === 'postgres')
|
|
23
|
+
return 'postgres';
|
|
24
|
+
if (this._engineType === 'mysql')
|
|
25
|
+
return 'mysql';
|
|
26
|
+
if (this._engineType === 'sqlite')
|
|
27
|
+
return 'sqlite';
|
|
28
|
+
return 'mssql';
|
|
29
|
+
}
|
|
22
30
|
constructor(adapterConfig) {
|
|
23
|
-
this.
|
|
31
|
+
this._engine = null;
|
|
32
|
+
this._engineType = null;
|
|
33
|
+
this._engineConfig = null;
|
|
24
34
|
this.sheetsAdapter = null;
|
|
25
35
|
if (isSheetsConfig(adapterConfig)) {
|
|
26
36
|
this.sheetsAdapter = new googlesheets_1.An5SheetsAdapter(adapterConfig);
|
|
@@ -31,23 +41,48 @@ class An5Adapter {
|
|
|
31
41
|
this.sheetsAdapter = new googlesheets_1.An5SheetsAdapter((0, parseConnectionString_1.parseSheetsConnectionString)(cs));
|
|
32
42
|
return;
|
|
33
43
|
}
|
|
44
|
+
this._engineConfig = adapterConfig;
|
|
34
45
|
if (cs.startsWith('postgres://') || cs.startsWith('postgresql://')) {
|
|
35
|
-
this.
|
|
46
|
+
this._engineType = 'postgres';
|
|
36
47
|
}
|
|
37
48
|
else if (cs.startsWith('mysql://') || cs.startsWith('mariadb://')) {
|
|
38
|
-
this.
|
|
49
|
+
this._engineType = 'mysql';
|
|
39
50
|
}
|
|
40
51
|
else if (cs.startsWith('sqlite://') || cs.endsWith('.sqlite') || cs.endsWith('.db')) {
|
|
41
|
-
this.
|
|
52
|
+
this._engineType = 'sqlite';
|
|
42
53
|
}
|
|
43
54
|
else {
|
|
44
|
-
this.
|
|
55
|
+
this._engineType = 'mssql';
|
|
45
56
|
}
|
|
46
57
|
}
|
|
47
|
-
requireEngine() {
|
|
48
|
-
if (!this.
|
|
58
|
+
async requireEngine() {
|
|
59
|
+
if (!this._engine && this._engineType) {
|
|
60
|
+
switch (this._engineType) {
|
|
61
|
+
case 'postgres': {
|
|
62
|
+
const { PostgresEngine } = require('./postgres.js');
|
|
63
|
+
this._engine = new PostgresEngine(this._engineConfig);
|
|
64
|
+
break;
|
|
65
|
+
}
|
|
66
|
+
case 'mysql': {
|
|
67
|
+
const { MysqlEngine } = require('./mysql.js');
|
|
68
|
+
this._engine = new MysqlEngine(this._engineConfig);
|
|
69
|
+
break;
|
|
70
|
+
}
|
|
71
|
+
case 'sqlite': {
|
|
72
|
+
const { SqliteEngine } = require('./sqlite.js');
|
|
73
|
+
this._engine = new SqliteEngine(this._engineConfig);
|
|
74
|
+
break;
|
|
75
|
+
}
|
|
76
|
+
case 'mssql': {
|
|
77
|
+
const { MssqlEngine } = require('./mssql.js');
|
|
78
|
+
this._engine = new MssqlEngine(this._engineConfig);
|
|
79
|
+
break;
|
|
80
|
+
}
|
|
81
|
+
}
|
|
82
|
+
}
|
|
83
|
+
if (!this._engine)
|
|
49
84
|
throw new Error('SQL engine is not available for Google Sheets adapter');
|
|
50
|
-
return this.
|
|
85
|
+
return this._engine;
|
|
51
86
|
}
|
|
52
87
|
requireSheetsAdapter() {
|
|
53
88
|
if (!this.sheetsAdapter)
|
|
@@ -57,19 +92,19 @@ class An5Adapter {
|
|
|
57
92
|
async exec(query, params) {
|
|
58
93
|
if (this.sheetsAdapter)
|
|
59
94
|
return this.sheetsAdapter.exec(query, params);
|
|
60
|
-
return this.requireEngine().exec(query, params);
|
|
95
|
+
return (await this.requireEngine()).exec(query, params);
|
|
61
96
|
}
|
|
62
97
|
/** INTERNAL: used by AdapterTableClient for DML statements needing row count */
|
|
63
98
|
async _executeRaw(query, params) {
|
|
64
|
-
return this.requireEngine().executeRaw(query, params);
|
|
99
|
+
return (await this.requireEngine()).executeRaw(query, params);
|
|
65
100
|
}
|
|
66
101
|
/** Execute a raw query with positional values → @p_0, @p_1, ... per dialect */
|
|
67
102
|
async $queryRawUnsafe(query, ...values) {
|
|
68
103
|
if (this.sheetsAdapter)
|
|
69
104
|
return this.sheetsAdapter.$queryRawUnsafe(query, ...values);
|
|
105
|
+
const engine = await this.requireEngine();
|
|
70
106
|
const params = {};
|
|
71
107
|
values.forEach((v, i) => { params[`p_${i}`] = v; });
|
|
72
|
-
// Normalise external positional placeholders → @p_N so each engine can re-transform
|
|
73
108
|
let q = query;
|
|
74
109
|
if (this.dialect === 'postgres') {
|
|
75
110
|
let idx = 0;
|
|
@@ -79,11 +114,12 @@ class An5Adapter {
|
|
|
79
114
|
let idx = 0;
|
|
80
115
|
q = q.replace(/\?/g, () => `@p_${idx++}`);
|
|
81
116
|
}
|
|
82
|
-
return
|
|
117
|
+
return engine.exec(q, params);
|
|
83
118
|
}
|
|
84
119
|
async $executeRaw(query, ...values) {
|
|
85
120
|
if (this.sheetsAdapter)
|
|
86
121
|
return this.sheetsAdapter.$executeRaw(query, ...values);
|
|
122
|
+
const engine = await this.requireEngine();
|
|
87
123
|
const params = {};
|
|
88
124
|
values.forEach((v, i) => { params[`p_${i}`] = v; });
|
|
89
125
|
let q = query;
|
|
@@ -95,7 +131,7 @@ class An5Adapter {
|
|
|
95
131
|
let idx = 0;
|
|
96
132
|
q = q.replace(/\?/g, () => `@p_${idx++}`);
|
|
97
133
|
}
|
|
98
|
-
return
|
|
134
|
+
return engine.executeRaw(q, params);
|
|
99
135
|
}
|
|
100
136
|
async $executeRawUnsafe(query, ...values) {
|
|
101
137
|
return this.$executeRaw(query, ...values);
|
|
@@ -103,19 +139,20 @@ class An5Adapter {
|
|
|
103
139
|
async $connect() {
|
|
104
140
|
if (this.sheetsAdapter)
|
|
105
141
|
return this.sheetsAdapter.$connect();
|
|
106
|
-
await this.requireEngine().connect();
|
|
142
|
+
await (await this.requireEngine()).connect();
|
|
107
143
|
}
|
|
108
144
|
async $disconnect() {
|
|
109
145
|
if (this.sheetsAdapter)
|
|
110
146
|
return this.sheetsAdapter.$disconnect();
|
|
111
|
-
await this.requireEngine().disconnect();
|
|
147
|
+
await (await this.requireEngine()).disconnect();
|
|
112
148
|
}
|
|
113
149
|
async $transaction(fn, _options) {
|
|
114
150
|
if (this.sheetsAdapter)
|
|
115
151
|
return this.sheetsAdapter.$transaction(fn, _options);
|
|
116
152
|
if (Array.isArray(fn))
|
|
117
153
|
return Promise.all(fn);
|
|
118
|
-
const
|
|
154
|
+
const engine = await this.requireEngine();
|
|
155
|
+
const handle = await engine.beginTransaction();
|
|
119
156
|
const txAdapter = new An5AdapterTx(handle, this.dialect);
|
|
120
157
|
try {
|
|
121
158
|
const result = await fn(txAdapter);
|
|
@@ -267,7 +304,7 @@ class AdapterTableClient {
|
|
|
267
304
|
const normalizedType = String(rawType).toLowerCase();
|
|
268
305
|
const isStringType = ['string', 'uuid', 'uniqueidentifier', 'nvarchar', 'varchar', 'text'].includes(normalizedType);
|
|
269
306
|
if (isStringType && !data[idFieldName])
|
|
270
|
-
data[idFieldName] = (0,
|
|
307
|
+
data[idFieldName] = (0, uuid_1.generateUUID)();
|
|
271
308
|
}
|
|
272
309
|
const cols = Object.keys(data).filter(k => data[k] !== undefined);
|
|
273
310
|
const params = {};
|
|
@@ -0,0 +1,17 @@
|
|
|
1
|
+
"use strict";
|
|
2
|
+
Object.defineProperty(exports, "__esModule", { value: true });
|
|
3
|
+
exports.generateUUID = generateUUID;
|
|
4
|
+
/**
|
|
5
|
+
* Cross-platform UUID generator safe for both Browser and Node.js environments.
|
|
6
|
+
* Uses crypto.randomUUID() when available, with a standard RFC4122 v4 fallback.
|
|
7
|
+
*/
|
|
8
|
+
function generateUUID() {
|
|
9
|
+
if (typeof crypto !== 'undefined' && typeof crypto.randomUUID === 'function') {
|
|
10
|
+
return crypto.randomUUID();
|
|
11
|
+
}
|
|
12
|
+
return 'xxxxxxxx-xxxx-4xxx-yxxx-xxxxxxxxxxxx'.replace(/[xy]/g, (c) => {
|
|
13
|
+
const r = (Math.random() * 16) | 0;
|
|
14
|
+
const v = c === 'x' ? r : (r & 0x3) | 0x8;
|
|
15
|
+
return v.toString(16);
|
|
16
|
+
});
|
|
17
|
+
}
|
|
@@ -0,0 +1,5 @@
|
|
|
1
|
+
export { An5SheetsAdapter, SheetsTableClient, createAn5SheetsAdapter } from './googlesheets';
|
|
2
|
+
export type { An5SheetsAdapterConfig } from './googlesheets';
|
|
3
|
+
export type { An5AdapterConfig, Dialect } from './base/types';
|
|
4
|
+
export { getFieldsForModel, getModelToTable, setAdapterMetadata } from './base/metadata';
|
|
5
|
+
export type { AdapterMetadata } from './base/metadata';
|
|
@@ -0,0 +1,12 @@
|
|
|
1
|
+
"use strict";
|
|
2
|
+
Object.defineProperty(exports, "__esModule", { value: true });
|
|
3
|
+
exports.setAdapterMetadata = exports.getModelToTable = exports.getFieldsForModel = exports.createAn5SheetsAdapter = exports.SheetsTableClient = exports.An5SheetsAdapter = void 0;
|
|
4
|
+
// Browser-safe entry — no SQL engine imports
|
|
5
|
+
var googlesheets_1 = require("./googlesheets");
|
|
6
|
+
Object.defineProperty(exports, "An5SheetsAdapter", { enumerable: true, get: function () { return googlesheets_1.An5SheetsAdapter; } });
|
|
7
|
+
Object.defineProperty(exports, "SheetsTableClient", { enumerable: true, get: function () { return googlesheets_1.SheetsTableClient; } });
|
|
8
|
+
Object.defineProperty(exports, "createAn5SheetsAdapter", { enumerable: true, get: function () { return googlesheets_1.createAn5SheetsAdapter; } });
|
|
9
|
+
var metadata_1 = require("./base/metadata");
|
|
10
|
+
Object.defineProperty(exports, "getFieldsForModel", { enumerable: true, get: function () { return metadata_1.getFieldsForModel; } });
|
|
11
|
+
Object.defineProperty(exports, "getModelToTable", { enumerable: true, get: function () { return metadata_1.getModelToTable; } });
|
|
12
|
+
Object.defineProperty(exports, "setAdapterMetadata", { enumerable: true, get: function () { return metadata_1.setAdapterMetadata; } });
|
|
@@ -7,7 +7,7 @@ export declare class An5SheetsAdapter {
|
|
|
7
7
|
config: ReturnType<typeof resolveConfig>;
|
|
8
8
|
private sheetsCache;
|
|
9
9
|
constructor(config: An5SheetsAdapterConfig);
|
|
10
|
-
private get
|
|
10
|
+
private get isFetchMode();
|
|
11
11
|
getSheets(): Promise<any>;
|
|
12
12
|
private getApi;
|
|
13
13
|
invalidateCache(): void;
|
|
@@ -2,17 +2,23 @@
|
|
|
2
2
|
Object.defineProperty(exports, "__esModule", { value: true });
|
|
3
3
|
exports.An5SheetsAdapter = void 0;
|
|
4
4
|
exports.createAn5SheetsAdapter = createAn5SheetsAdapter;
|
|
5
|
-
const googleapis_1 = require("googleapis");
|
|
6
5
|
const config_1 = require("./config");
|
|
7
6
|
const sqlExecutor_1 = require("./sqlExecutor");
|
|
8
7
|
const tableClient_1 = require("./tableClient");
|
|
9
8
|
const retry_1 = require("./retry");
|
|
10
|
-
// ─── Fetch-based API proxy for OAuth Access Token (browser-compatible)
|
|
11
|
-
function createFetchApi(spreadsheetId, accessToken) {
|
|
9
|
+
// ─── Fetch-based API proxy for OAuth Access Token / API Key (browser-compatible) ──
|
|
10
|
+
function createFetchApi(spreadsheetId, accessToken, apiKey) {
|
|
12
11
|
const base = `https://sheets.googleapis.com/v4/spreadsheets/${spreadsheetId}`;
|
|
13
|
-
const headers = {
|
|
12
|
+
const headers = { 'Content-Type': 'application/json' };
|
|
13
|
+
if (accessToken) {
|
|
14
|
+
headers['Authorization'] = `Bearer ${accessToken}`;
|
|
15
|
+
}
|
|
14
16
|
async function request(method, path, body) {
|
|
15
|
-
|
|
17
|
+
let url = path.startsWith('http') ? path : `${base}${path}`;
|
|
18
|
+
if (apiKey) {
|
|
19
|
+
const sep = url.includes('?') ? '&' : '?';
|
|
20
|
+
url += `${sep}key=${encodeURIComponent(apiKey)}`;
|
|
21
|
+
}
|
|
16
22
|
const opts = { method, headers };
|
|
17
23
|
if (body)
|
|
18
24
|
opts.body = JSON.stringify(body);
|
|
@@ -74,23 +80,28 @@ class An5SheetsAdapter {
|
|
|
74
80
|
this.sheetsCache = null;
|
|
75
81
|
this.config = (0, config_1.resolveConfig)(config);
|
|
76
82
|
}
|
|
77
|
-
get
|
|
78
|
-
return !!this.config.accessToken;
|
|
83
|
+
get isFetchMode() {
|
|
84
|
+
return !!this.config.accessToken || !!this.config.apiKey;
|
|
79
85
|
}
|
|
80
86
|
async getSheets() {
|
|
81
|
-
if (this.
|
|
87
|
+
if (this.isFetchMode) {
|
|
82
88
|
if (!this.fetchApi) {
|
|
83
|
-
this.fetchApi = createFetchApi(this.config.spreadsheetId, this.config.accessToken);
|
|
89
|
+
this.fetchApi = createFetchApi(this.config.spreadsheetId, this.config.accessToken, this.config.apiKey);
|
|
84
90
|
}
|
|
85
91
|
return this.fetchApi;
|
|
86
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
|
+
}
|
|
87
97
|
if (!this.sheets) {
|
|
88
|
-
const
|
|
98
|
+
const { google } = await import('googleapis');
|
|
99
|
+
const auth = new google.auth.JWT({
|
|
89
100
|
email: this.config.clientEmail,
|
|
90
101
|
key: this.config.privateKey,
|
|
91
102
|
scopes: ['https://www.googleapis.com/auth/spreadsheets'],
|
|
92
103
|
});
|
|
93
|
-
this.sheets =
|
|
104
|
+
this.sheets = google.sheets({ version: 'v4', auth });
|
|
94
105
|
}
|
|
95
106
|
return this.sheets;
|
|
96
107
|
}
|
|
@@ -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.
|
|
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
|
}
|
|
@@ -1,2 +1,3 @@
|
|
|
1
|
-
export { An5SheetsAdapter, SheetsTableClient, createAn5SheetsAdapter
|
|
1
|
+
export { An5SheetsAdapter, SheetsTableClient, createAn5SheetsAdapter } from './an5SheetsAdapter';
|
|
2
|
+
export type { An5SheetsAdapterConfig } from './an5SheetsAdapter';
|
|
2
3
|
export { parseSheetsConnectionString } from './parseConnectionString';
|
|
@@ -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(',')) {
|
|
@@ -1,3 +1,25 @@
|
|
|
1
|
-
export declare const MAX_RETRIES =
|
|
1
|
+
export declare const MAX_RETRIES = 5;
|
|
2
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
|
+
}
|
|
3
23
|
export declare function withRetry<T>(fn: () => Promise<T>, attempt?: number): Promise<T>;
|
|
24
|
+
/** Expose for testing / custom configurations */
|
|
25
|
+
export { TokenBucket, ConcurrencyLimiter };
|
|
@@ -1,19 +1,83 @@
|
|
|
1
1
|
"use strict";
|
|
2
|
-
// ─── Retry helper
|
|
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
|
|
3
9
|
Object.defineProperty(exports, "__esModule", { value: true });
|
|
4
|
-
exports.BASE_DELAY = exports.MAX_RETRIES = void 0;
|
|
10
|
+
exports.ConcurrencyLimiter = exports.TokenBucket = exports.RATE_LIMIT_RPS = exports.MAX_CONCURRENCY = exports.MAX_DELAY = exports.BASE_DELAY = exports.MAX_RETRIES = void 0;
|
|
5
11
|
exports.withRetry = withRetry;
|
|
6
|
-
|
|
7
|
-
exports.
|
|
8
|
-
|
|
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) {
|
|
9
73
|
const code = err?.code || err?.status || err?.response?.status;
|
|
10
74
|
if (code === 429)
|
|
11
75
|
return true;
|
|
12
|
-
if (code === 500 || code === 503)
|
|
76
|
+
if (code === 500 || code === 503 || code === 502)
|
|
13
77
|
return true;
|
|
14
78
|
if (code === 403) {
|
|
15
79
|
const reason = err?.errors?.[0]?.reason || '';
|
|
16
|
-
if (reason === 'rateLimitExceeded')
|
|
80
|
+
if (reason === 'rateLimitExceeded' || reason === 'userRateLimitExceeded')
|
|
17
81
|
return true;
|
|
18
82
|
if (typeof err?.body === 'string' && err.body.includes('rateLimitExceeded'))
|
|
19
83
|
return true;
|
|
@@ -22,18 +86,30 @@ function isRateLimitError(err) {
|
|
|
22
86
|
}
|
|
23
87
|
return false;
|
|
24
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 ────────────────────────────────────────────────────────────────
|
|
25
98
|
async function withRetry(fn, attempt = 1) {
|
|
99
|
+
await _bucket.acquire();
|
|
100
|
+
await _concurrency.acquire();
|
|
26
101
|
try {
|
|
27
102
|
return await fn();
|
|
28
103
|
}
|
|
29
104
|
catch (err) {
|
|
30
|
-
if (
|
|
31
|
-
const delay = err
|
|
32
|
-
? err.retryAfter * 1000
|
|
33
|
-
: exports.BASE_DELAY * Math.pow(2, attempt - 1) + Math.random() * 500;
|
|
105
|
+
if (isRetryableError(err) && attempt <= exports.MAX_RETRIES) {
|
|
106
|
+
const delay = getRetryDelay(err, attempt);
|
|
34
107
|
await new Promise(r => setTimeout(r, delay));
|
|
35
108
|
return withRetry(fn, attempt + 1);
|
|
36
109
|
}
|
|
37
110
|
throw err;
|
|
38
111
|
}
|
|
112
|
+
finally {
|
|
113
|
+
_concurrency.release();
|
|
114
|
+
}
|
|
39
115
|
}
|