@an5/adapters 0.1.3 → 0.2.1
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 +44 -44
- package/dotnet/An5Adapter.cs +275 -275
- package/package.json +17 -10
- package/python/base/metadata.py +10 -10
- package/python/table_client.py +216 -219
- package/typescript/googlesheets/adapter.d.ts +3 -2
- package/typescript/googlesheets/adapter.js +74 -4
- package/typescript/googlesheets/adapter.ts +82 -8
- package/typescript/googlesheets/config.d.ts +11 -0
- package/typescript/googlesheets/config.js +7 -0
- package/typescript/googlesheets/config.ts +11 -0
- package/typescript/googlesheets/helpers.js +1 -1
- package/typescript/googlesheets/helpers.ts +1 -1
- package/typescript/googlesheets/retry.js +21 -8
- package/typescript/googlesheets/retry.ts +17 -8
- package/typescript/googlesheets/tableClient.js +4 -3
- package/typescript/googlesheets/tableClient.ts +6 -5
|
@@ -5,10 +5,73 @@ import { SheetsTableClient } from './tableClient';
|
|
|
5
5
|
import { SheetMeta } from './types';
|
|
6
6
|
import { withRetry } from './retry';
|
|
7
7
|
|
|
8
|
+
// ─── Fetch-based API proxy for OAuth Access Token (browser-compatible) ────────
|
|
9
|
+
|
|
10
|
+
function createFetchApi(spreadsheetId: string, accessToken: string) {
|
|
11
|
+
const base = `https://sheets.googleapis.com/v4/spreadsheets/${spreadsheetId}`;
|
|
12
|
+
const headers = { Authorization: `Bearer ${accessToken}`, 'Content-Type': 'application/json' };
|
|
13
|
+
|
|
14
|
+
async function request<T>(method: string, path: string, body?: any): Promise<T> {
|
|
15
|
+
const url = path.startsWith('http') ? path : `${base}${path}`;
|
|
16
|
+
const opts: RequestInit = { method, headers };
|
|
17
|
+
if (body) opts.body = JSON.stringify(body);
|
|
18
|
+
const res = await fetch(url, opts);
|
|
19
|
+
if (!res.ok) {
|
|
20
|
+
const text = await res.text().catch(() => '');
|
|
21
|
+
const err = new Error(`Google Sheets API error (${res.status}): ${text}`);
|
|
22
|
+
(err as any).status = res.status;
|
|
23
|
+
(err as any).body = text;
|
|
24
|
+
const retryAfter = res.headers.get('Retry-After');
|
|
25
|
+
if (retryAfter) (err as any).retryAfter = parseInt(retryAfter, 10);
|
|
26
|
+
throw err;
|
|
27
|
+
}
|
|
28
|
+
const text = await res.text();
|
|
29
|
+
return text ? JSON.parse(text) : (undefined as T);
|
|
30
|
+
}
|
|
31
|
+
|
|
32
|
+
function fmtRange(range: string): string {
|
|
33
|
+
return `/${encodeURIComponent(range)}`;
|
|
34
|
+
}
|
|
35
|
+
|
|
36
|
+
return {
|
|
37
|
+
spreadsheets: {
|
|
38
|
+
get: async (params: any) => {
|
|
39
|
+
const data = await request<any>('GET', `?fields=${params.fields || ''}`);
|
|
40
|
+
return { data };
|
|
41
|
+
},
|
|
42
|
+
batchUpdate: async (params: any) => {
|
|
43
|
+
const data = await request<any>('POST', ':batchUpdate', params.requestBody);
|
|
44
|
+
return { data };
|
|
45
|
+
},
|
|
46
|
+
values: {
|
|
47
|
+
get: async (params: any) => {
|
|
48
|
+
const data = await request<any>('GET', fmtRange(params.range));
|
|
49
|
+
return { data };
|
|
50
|
+
},
|
|
51
|
+
update: async (params: any) => {
|
|
52
|
+
const qs = `?valueInputOption=${params.valueInputOption || 'RAW'}`;
|
|
53
|
+
const data = await request<any>('PUT', `${fmtRange(params.range)}${qs}`, params.requestBody);
|
|
54
|
+
return { data };
|
|
55
|
+
},
|
|
56
|
+
append: async (params: any) => {
|
|
57
|
+
const qs = `?valueInputOption=${params.valueInputOption || 'RAW'}&insertDataOption=${params.insertDataOption || 'INSERT_ROWS'}`;
|
|
58
|
+
await request<any>('POST', `${fmtRange(params.range)}:append${qs}`, params.requestBody);
|
|
59
|
+
return { data: { updates: { updatedRows: (params.requestBody?.values || []).length } } };
|
|
60
|
+
},
|
|
61
|
+
clear: async (params: any) => {
|
|
62
|
+
await request<any>('POST', `${fmtRange(params.range)}:clear`);
|
|
63
|
+
return { data: {} };
|
|
64
|
+
},
|
|
65
|
+
},
|
|
66
|
+
},
|
|
67
|
+
};
|
|
68
|
+
}
|
|
69
|
+
|
|
8
70
|
// ─── Adapter ──────────────────────────────────────────────────────────────────
|
|
9
71
|
|
|
10
72
|
export class An5SheetsAdapter {
|
|
11
73
|
private sheets: sheets_v4.Sheets | null = null;
|
|
74
|
+
private fetchApi: ReturnType<typeof createFetchApi> | null = null;
|
|
12
75
|
config: ReturnType<typeof resolveConfig>;
|
|
13
76
|
private sheetsCache: Promise<SheetMeta[]> | null = null;
|
|
14
77
|
|
|
@@ -16,7 +79,17 @@ export class An5SheetsAdapter {
|
|
|
16
79
|
this.config = resolveConfig(config);
|
|
17
80
|
}
|
|
18
81
|
|
|
19
|
-
|
|
82
|
+
private get isOAuth(): boolean {
|
|
83
|
+
return !!(this.config as any).accessToken;
|
|
84
|
+
}
|
|
85
|
+
|
|
86
|
+
async getSheets(): Promise<any> {
|
|
87
|
+
if (this.isOAuth) {
|
|
88
|
+
if (!this.fetchApi) {
|
|
89
|
+
this.fetchApi = createFetchApi(this.config.spreadsheetId, (this.config as any).accessToken);
|
|
90
|
+
}
|
|
91
|
+
return this.fetchApi;
|
|
92
|
+
}
|
|
20
93
|
if (!this.sheets) {
|
|
21
94
|
const auth = new google.auth.JWT({
|
|
22
95
|
email: this.config.clientEmail,
|
|
@@ -38,31 +111,31 @@ export class An5SheetsAdapter {
|
|
|
38
111
|
|
|
39
112
|
async listSheets(): Promise<string[]> {
|
|
40
113
|
const api = await this.getApi();
|
|
41
|
-
const res = await withRetry(() =>
|
|
114
|
+
const res: any = await withRetry(() =>
|
|
42
115
|
api.spreadsheets.get({
|
|
43
116
|
spreadsheetId: this.config.spreadsheetId,
|
|
44
117
|
fields: 'sheets.properties.title',
|
|
45
118
|
})
|
|
46
119
|
);
|
|
47
|
-
return (res.data.sheets || []).map(s => s.properties?.title || '');
|
|
120
|
+
return (res.data.sheets || []).map((s: any) => s.properties?.title || '');
|
|
48
121
|
}
|
|
49
122
|
|
|
50
123
|
async getSheetMeta(name: string): Promise<SheetMeta | null> {
|
|
51
124
|
const api = await this.getApi();
|
|
52
125
|
if (this.sheetsCache == null) {
|
|
53
126
|
this.sheetsCache = withRetry(async () => {
|
|
54
|
-
const res = await api.spreadsheets.get({
|
|
127
|
+
const res: any = await api.spreadsheets.get({
|
|
55
128
|
spreadsheetId: this.config.spreadsheetId,
|
|
56
129
|
fields: 'sheets.properties',
|
|
57
130
|
});
|
|
58
|
-
return (res.data.sheets || []).map(s => ({
|
|
131
|
+
return (res.data.sheets || []).map((s: any) => ({
|
|
59
132
|
sheetId: s.properties?.sheetId ?? -1,
|
|
60
133
|
title: s.properties?.title ?? '',
|
|
61
|
-
})).filter(m => m.sheetId >= 0);
|
|
134
|
+
})).filter((m: SheetMeta) => m.sheetId >= 0);
|
|
62
135
|
});
|
|
63
136
|
}
|
|
64
137
|
const all = await this.sheetsCache;
|
|
65
|
-
return all.find(m => m.title === name) || null;
|
|
138
|
+
return all.find((m: SheetMeta) => m.title === name) || null;
|
|
66
139
|
}
|
|
67
140
|
|
|
68
141
|
async deleteSheet(name: string): Promise<void> {
|
|
@@ -86,6 +159,7 @@ export class An5SheetsAdapter {
|
|
|
86
159
|
|
|
87
160
|
async $disconnect(): Promise<void> {
|
|
88
161
|
this.sheets = null;
|
|
162
|
+
this.fetchApi = null;
|
|
89
163
|
this.sheetsCache = null;
|
|
90
164
|
}
|
|
91
165
|
|
|
@@ -126,7 +200,7 @@ export class An5SheetsAdapter {
|
|
|
126
200
|
|
|
127
201
|
async readRange<T = any>(range: string): Promise<T[][]> {
|
|
128
202
|
const api = await this.getApi();
|
|
129
|
-
const res = await withRetry(() =>
|
|
203
|
+
const res: any = await withRetry(() =>
|
|
130
204
|
api.spreadsheets.values.get({
|
|
131
205
|
spreadsheetId: this.config.spreadsheetId,
|
|
132
206
|
range,
|
|
@@ -7,11 +7,22 @@ export interface An5SheetsAdapterConfig {
|
|
|
7
7
|
private_key: string;
|
|
8
8
|
};
|
|
9
9
|
sheetMapping?: Record<string, string>;
|
|
10
|
+
/** OAuth2 access token for browser-based authentication (e.g. from Firebase Auth Google sign-in).
|
|
11
|
+
* When provided, the adapter uses raw fetch() instead of googleapis JWT, making it compatible
|
|
12
|
+
* with browser environments. Mutually exclusive with clientEmail/privateKey/credentials. */
|
|
13
|
+
accessToken?: string;
|
|
10
14
|
}
|
|
11
15
|
export declare function normalizeKey(key: string): string;
|
|
12
16
|
export declare function resolveConfig(config: An5SheetsAdapterConfig): {
|
|
17
|
+
spreadsheetId: string;
|
|
18
|
+
accessToken: string;
|
|
19
|
+
sheetMapping: Record<string, string> | undefined;
|
|
20
|
+
clientEmail?: undefined;
|
|
21
|
+
privateKey?: undefined;
|
|
22
|
+
} | {
|
|
13
23
|
spreadsheetId: string;
|
|
14
24
|
clientEmail: string;
|
|
15
25
|
privateKey: string;
|
|
16
26
|
sheetMapping: Record<string, string> | undefined;
|
|
27
|
+
accessToken?: undefined;
|
|
17
28
|
};
|
|
@@ -12,6 +12,13 @@ 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) {
|
|
16
|
+
return {
|
|
17
|
+
spreadsheetId: config.spreadsheetId,
|
|
18
|
+
accessToken: config.accessToken,
|
|
19
|
+
sheetMapping: config.sheetMapping,
|
|
20
|
+
};
|
|
21
|
+
}
|
|
15
22
|
if (config.credentials) {
|
|
16
23
|
return {
|
|
17
24
|
spreadsheetId: config.spreadsheetId,
|
|
@@ -6,6 +6,10 @@ export interface An5SheetsAdapterConfig {
|
|
|
6
6
|
privateKey?: string;
|
|
7
7
|
credentials?: { client_email: string; private_key: string };
|
|
8
8
|
sheetMapping?: Record<string, string>;
|
|
9
|
+
/** OAuth2 access token for browser-based authentication (e.g. from Firebase Auth Google sign-in).
|
|
10
|
+
* When provided, the adapter uses raw fetch() instead of googleapis JWT, making it compatible
|
|
11
|
+
* with browser environments. Mutually exclusive with clientEmail/privateKey/credentials. */
|
|
12
|
+
accessToken?: string;
|
|
9
13
|
}
|
|
10
14
|
|
|
11
15
|
export function normalizeKey(key: string): string {
|
|
@@ -16,6 +20,13 @@ export function normalizeKey(key: string): string {
|
|
|
16
20
|
}
|
|
17
21
|
|
|
18
22
|
export function resolveConfig(config: An5SheetsAdapterConfig) {
|
|
23
|
+
if (config.accessToken) {
|
|
24
|
+
return {
|
|
25
|
+
spreadsheetId: config.spreadsheetId,
|
|
26
|
+
accessToken: config.accessToken,
|
|
27
|
+
sheetMapping: config.sheetMapping,
|
|
28
|
+
};
|
|
29
|
+
}
|
|
19
30
|
if (config.credentials) {
|
|
20
31
|
return {
|
|
21
32
|
spreadsheetId: config.spreadsheetId,
|
|
@@ -95,7 +95,7 @@ function buildOrderBy(orderBy) {
|
|
|
95
95
|
const parts = [];
|
|
96
96
|
for (const entry of entries) {
|
|
97
97
|
for (const [key, dir] of Object.entries(entry)) {
|
|
98
|
-
parts.push(`${key} ${dir.toUpperCase()}`);
|
|
98
|
+
parts.push(`${key} ${String(dir || 'asc').toUpperCase()}`);
|
|
99
99
|
}
|
|
100
100
|
}
|
|
101
101
|
return parts.join(', ');
|
|
@@ -75,7 +75,7 @@ export function buildOrderBy(orderBy: any): string {
|
|
|
75
75
|
const parts: string[] = [];
|
|
76
76
|
for (const entry of entries) {
|
|
77
77
|
for (const [key, dir] of Object.entries(entry)) {
|
|
78
|
-
parts.push(`${key} ${(dir
|
|
78
|
+
parts.push(`${key} ${String(dir || 'asc').toUpperCase()}`);
|
|
79
79
|
}
|
|
80
80
|
}
|
|
81
81
|
return parts.join(', ');
|
|
@@ -5,19 +5,32 @@ exports.BASE_DELAY = exports.MAX_RETRIES = void 0;
|
|
|
5
5
|
exports.withRetry = withRetry;
|
|
6
6
|
exports.MAX_RETRIES = 3;
|
|
7
7
|
exports.BASE_DELAY = 1000;
|
|
8
|
+
function isRateLimitError(err) {
|
|
9
|
+
const code = err?.code || err?.status || err?.response?.status;
|
|
10
|
+
if (code === 429)
|
|
11
|
+
return true;
|
|
12
|
+
if (code === 500 || code === 503)
|
|
13
|
+
return true;
|
|
14
|
+
if (code === 403) {
|
|
15
|
+
const reason = err?.errors?.[0]?.reason || '';
|
|
16
|
+
if (reason === 'rateLimitExceeded')
|
|
17
|
+
return true;
|
|
18
|
+
if (typeof err?.body === 'string' && err.body.includes('rateLimitExceeded'))
|
|
19
|
+
return true;
|
|
20
|
+
if (typeof err?.message === 'string' && err.message.includes('rateLimitExceeded'))
|
|
21
|
+
return true;
|
|
22
|
+
}
|
|
23
|
+
return false;
|
|
24
|
+
}
|
|
8
25
|
async function withRetry(fn, attempt = 1) {
|
|
9
26
|
try {
|
|
10
27
|
return await fn();
|
|
11
28
|
}
|
|
12
29
|
catch (err) {
|
|
13
|
-
|
|
14
|
-
|
|
15
|
-
|
|
16
|
-
|
|
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;
|
|
30
|
+
if (isRateLimitError(err) && attempt <= exports.MAX_RETRIES) {
|
|
31
|
+
const delay = err.retryAfter
|
|
32
|
+
? err.retryAfter * 1000
|
|
33
|
+
: exports.BASE_DELAY * Math.pow(2, attempt - 1) + Math.random() * 500;
|
|
21
34
|
await new Promise(r => setTimeout(r, delay));
|
|
22
35
|
return withRetry(fn, attempt + 1);
|
|
23
36
|
}
|
|
@@ -3,18 +3,27 @@
|
|
|
3
3
|
export const MAX_RETRIES = 3;
|
|
4
4
|
export const BASE_DELAY = 1000;
|
|
5
5
|
|
|
6
|
+
function isRateLimitError(err: any): boolean {
|
|
7
|
+
const code = err?.code || err?.status || err?.response?.status;
|
|
8
|
+
if (code === 429) return true;
|
|
9
|
+
if (code === 500 || code === 503) return true;
|
|
10
|
+
if (code === 403) {
|
|
11
|
+
const reason = err?.errors?.[0]?.reason || '';
|
|
12
|
+
if (reason === 'rateLimitExceeded') return true;
|
|
13
|
+
if (typeof err?.body === 'string' && err.body.includes('rateLimitExceeded')) return true;
|
|
14
|
+
if (typeof err?.message === 'string' && err.message.includes('rateLimitExceeded')) return true;
|
|
15
|
+
}
|
|
16
|
+
return false;
|
|
17
|
+
}
|
|
18
|
+
|
|
6
19
|
export async function withRetry<T>(fn: () => Promise<T>, attempt = 1): Promise<T> {
|
|
7
20
|
try {
|
|
8
21
|
return await fn();
|
|
9
22
|
} catch (err: any) {
|
|
10
|
-
|
|
11
|
-
|
|
12
|
-
|
|
13
|
-
|
|
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;
|
|
23
|
+
if (isRateLimitError(err) && attempt <= MAX_RETRIES) {
|
|
24
|
+
const delay = err.retryAfter
|
|
25
|
+
? err.retryAfter * 1000
|
|
26
|
+
: BASE_DELAY * Math.pow(2, attempt - 1) + Math.random() * 500;
|
|
18
27
|
await new Promise(r => setTimeout(r, delay));
|
|
19
28
|
return withRetry(fn, attempt + 1);
|
|
20
29
|
}
|
|
@@ -315,13 +315,14 @@ class SheetsTableClient {
|
|
|
315
315
|
result._count = rows.length;
|
|
316
316
|
if (args._sum) {
|
|
317
317
|
for (const f of Object.keys(args._sum)) {
|
|
318
|
-
|
|
318
|
+
const nums = rows.map((r) => Number(r[f])).filter(n => !isNaN(n));
|
|
319
|
+
result[`_sum_${f}`] = nums.length > 0 ? nums.reduce((s, n) => s + n, 0) : null;
|
|
319
320
|
}
|
|
320
321
|
}
|
|
321
322
|
if (args._avg) {
|
|
322
323
|
for (const f of Object.keys(args._avg)) {
|
|
323
|
-
const
|
|
324
|
-
result[`_avg_${f}`] =
|
|
324
|
+
const nums = rows.map((r) => Number(r[f])).filter(n => !isNaN(n));
|
|
325
|
+
result[`_avg_${f}`] = nums.length > 0 ? nums.reduce((s, n) => s + n, 0) / nums.length : null;
|
|
325
326
|
}
|
|
326
327
|
}
|
|
327
328
|
if (args._min) {
|
|
@@ -43,7 +43,7 @@ export class SheetsTableClient<T = any> {
|
|
|
43
43
|
|
|
44
44
|
private async readAllRows(): Promise<{ headers: string[]; rows: Record<string, any>[] }> {
|
|
45
45
|
const api = await this.adapter.getSheets();
|
|
46
|
-
const res = await withRetry(() =>
|
|
46
|
+
const res: any = await withRetry(() =>
|
|
47
47
|
api.spreadsheets.values.get({
|
|
48
48
|
spreadsheetId: this.adapter.config.spreadsheetId,
|
|
49
49
|
range: `${this.escSheetName}!A:ZZ`,
|
|
@@ -68,7 +68,7 @@ export class SheetsTableClient<T = any> {
|
|
|
68
68
|
const api = await this.adapter.getSheets();
|
|
69
69
|
await this.ensureSheetExists();
|
|
70
70
|
|
|
71
|
-
const res = await withRetry(() =>
|
|
71
|
+
const res: any = await withRetry(() =>
|
|
72
72
|
api.spreadsheets.values.get({
|
|
73
73
|
spreadsheetId: this.adapter.config.spreadsheetId,
|
|
74
74
|
range: `${this.escSheetName}!A1:ZZ1`,
|
|
@@ -351,13 +351,14 @@ export class SheetsTableClient<T = any> {
|
|
|
351
351
|
if (args._count !== undefined) result._count = rows.length;
|
|
352
352
|
if (args._sum) {
|
|
353
353
|
for (const f of Object.keys(args._sum)) {
|
|
354
|
-
|
|
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;
|
|
355
356
|
}
|
|
356
357
|
}
|
|
357
358
|
if (args._avg) {
|
|
358
359
|
for (const f of Object.keys(args._avg)) {
|
|
359
|
-
const
|
|
360
|
-
result[`_avg_${f}`] =
|
|
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;
|
|
361
362
|
}
|
|
362
363
|
}
|
|
363
364
|
if (args._min) {
|