@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
package/python/table_client.py
CHANGED
|
@@ -1,219 +1,216 @@
|
|
|
1
|
-
import json
|
|
2
|
-
import uuid
|
|
3
|
-
from typing import Dict, List, Optional
|
|
4
|
-
try:
|
|
5
|
-
from .base import DIALECT_MSSQL, DIALECT_POSTGRES, model_fields, _build_order_by, _parse_where, _quote, _quote_table, _resolve_table
|
|
6
|
-
except ImportError:
|
|
7
|
-
from base import DIALECT_MSSQL, DIALECT_POSTGRES, model_fields, _build_order_by, _parse_where, _quote, _quote_table, _resolve_table
|
|
8
|
-
|
|
9
|
-
# ─── Table Client ───────────────────────────────────────────────────────────────────
|
|
10
|
-
|
|
11
|
-
class AdapterTableClient:
|
|
12
|
-
def __init__(self, adapter: An5Adapter, model_name: str):
|
|
13
|
-
self._adapter = adapter
|
|
14
|
-
self._model = model_name
|
|
15
|
-
self._dialect = adapter._dialect
|
|
16
|
-
|
|
17
|
-
@property
|
|
18
|
-
def _table(self) -> str:
|
|
19
|
-
return _resolve_table(self._model)
|
|
20
|
-
|
|
21
|
-
@property
|
|
22
|
-
def _table_sql(self) -> str:
|
|
23
|
-
return _quote_table(self._table, self._dialect)
|
|
24
|
-
|
|
25
|
-
@property
|
|
26
|
-
def _nolock(self) -> str:
|
|
27
|
-
return "" if self._dialect == DIALECT_POSTGRES else " WITH (NOLOCK)"
|
|
28
|
-
|
|
29
|
-
@property
|
|
30
|
-
def _fields(self) -> List[Dict]:
|
|
31
|
-
fields = model_fields.get(self._model, [])
|
|
32
|
-
if isinstance(fields, dict):
|
|
33
|
-
|
|
34
|
-
|
|
35
|
-
|
|
36
|
-
|
|
37
|
-
|
|
38
|
-
|
|
39
|
-
|
|
40
|
-
|
|
41
|
-
|
|
42
|
-
|
|
43
|
-
|
|
44
|
-
|
|
45
|
-
|
|
46
|
-
|
|
47
|
-
|
|
48
|
-
|
|
49
|
-
|
|
50
|
-
|
|
51
|
-
|
|
52
|
-
|
|
53
|
-
|
|
54
|
-
|
|
55
|
-
|
|
56
|
-
|
|
57
|
-
|
|
58
|
-
|
|
59
|
-
|
|
60
|
-
|
|
61
|
-
|
|
62
|
-
|
|
63
|
-
|
|
64
|
-
|
|
65
|
-
return
|
|
66
|
-
|
|
67
|
-
def
|
|
68
|
-
|
|
69
|
-
|
|
70
|
-
|
|
71
|
-
|
|
72
|
-
|
|
73
|
-
|
|
74
|
-
if
|
|
75
|
-
|
|
76
|
-
|
|
77
|
-
|
|
78
|
-
|
|
79
|
-
|
|
80
|
-
|
|
81
|
-
|
|
82
|
-
|
|
83
|
-
|
|
84
|
-
|
|
85
|
-
|
|
86
|
-
|
|
87
|
-
|
|
88
|
-
|
|
89
|
-
|
|
90
|
-
|
|
91
|
-
|
|
92
|
-
|
|
93
|
-
|
|
94
|
-
|
|
95
|
-
|
|
96
|
-
|
|
97
|
-
|
|
98
|
-
|
|
99
|
-
|
|
100
|
-
|
|
101
|
-
|
|
102
|
-
|
|
103
|
-
|
|
104
|
-
|
|
105
|
-
|
|
106
|
-
|
|
107
|
-
|
|
108
|
-
|
|
109
|
-
|
|
110
|
-
|
|
111
|
-
|
|
112
|
-
|
|
113
|
-
|
|
114
|
-
|
|
115
|
-
|
|
116
|
-
|
|
117
|
-
|
|
118
|
-
|
|
119
|
-
|
|
120
|
-
|
|
121
|
-
|
|
122
|
-
|
|
123
|
-
|
|
124
|
-
|
|
125
|
-
|
|
126
|
-
|
|
127
|
-
|
|
128
|
-
|
|
129
|
-
|
|
130
|
-
|
|
131
|
-
|
|
132
|
-
|
|
133
|
-
|
|
134
|
-
|
|
135
|
-
|
|
136
|
-
|
|
137
|
-
|
|
138
|
-
|
|
139
|
-
|
|
140
|
-
|
|
141
|
-
|
|
142
|
-
params
|
|
143
|
-
|
|
144
|
-
|
|
145
|
-
|
|
146
|
-
|
|
147
|
-
|
|
148
|
-
|
|
149
|
-
|
|
150
|
-
|
|
151
|
-
|
|
152
|
-
|
|
153
|
-
|
|
154
|
-
|
|
155
|
-
|
|
156
|
-
|
|
157
|
-
|
|
158
|
-
|
|
159
|
-
|
|
160
|
-
|
|
161
|
-
|
|
162
|
-
|
|
163
|
-
|
|
164
|
-
|
|
165
|
-
|
|
166
|
-
|
|
167
|
-
|
|
168
|
-
|
|
169
|
-
if
|
|
170
|
-
for f in (
|
|
171
|
-
aggs.append(f"
|
|
172
|
-
if
|
|
173
|
-
for f in (
|
|
174
|
-
aggs.append(f"
|
|
175
|
-
if
|
|
176
|
-
for f in (
|
|
177
|
-
aggs.append(f"
|
|
178
|
-
if
|
|
179
|
-
|
|
180
|
-
|
|
181
|
-
|
|
182
|
-
|
|
183
|
-
|
|
184
|
-
|
|
185
|
-
if
|
|
186
|
-
|
|
187
|
-
|
|
188
|
-
|
|
189
|
-
|
|
190
|
-
|
|
191
|
-
|
|
192
|
-
|
|
193
|
-
|
|
194
|
-
|
|
195
|
-
|
|
196
|
-
|
|
197
|
-
|
|
198
|
-
|
|
199
|
-
|
|
200
|
-
|
|
201
|
-
|
|
202
|
-
|
|
203
|
-
|
|
204
|
-
|
|
205
|
-
|
|
206
|
-
|
|
207
|
-
|
|
208
|
-
|
|
209
|
-
|
|
210
|
-
|
|
211
|
-
|
|
212
|
-
|
|
213
|
-
|
|
214
|
-
|
|
215
|
-
|
|
216
|
-
|
|
217
|
-
|
|
218
|
-
scored.sort(key=lambda x: x[1])
|
|
219
|
-
return [{**row, "distance": dist} for row, dist in scored[:take]]
|
|
1
|
+
import json
|
|
2
|
+
import uuid
|
|
3
|
+
from typing import Dict, List, Optional
|
|
4
|
+
try:
|
|
5
|
+
from .base import DIALECT_MSSQL, DIALECT_POSTGRES, model_fields, _build_order_by, _parse_where, _quote, _quote_table, _resolve_table
|
|
6
|
+
except ImportError:
|
|
7
|
+
from base import DIALECT_MSSQL, DIALECT_POSTGRES, model_fields, _build_order_by, _parse_where, _quote, _quote_table, _resolve_table
|
|
8
|
+
|
|
9
|
+
# ─── Table Client ───────────────────────────────────────────────────────────────────
|
|
10
|
+
|
|
11
|
+
class AdapterTableClient:
|
|
12
|
+
def __init__(self, adapter: An5Adapter, model_name: str):
|
|
13
|
+
self._adapter = adapter
|
|
14
|
+
self._model = model_name
|
|
15
|
+
self._dialect = adapter._dialect
|
|
16
|
+
|
|
17
|
+
@property
|
|
18
|
+
def _table(self) -> str:
|
|
19
|
+
return _resolve_table(self._model)
|
|
20
|
+
|
|
21
|
+
@property
|
|
22
|
+
def _table_sql(self) -> str:
|
|
23
|
+
return _quote_table(self._table, self._dialect)
|
|
24
|
+
|
|
25
|
+
@property
|
|
26
|
+
def _nolock(self) -> str:
|
|
27
|
+
return "" if self._dialect == DIALECT_POSTGRES else " WITH (NOLOCK)"
|
|
28
|
+
|
|
29
|
+
@property
|
|
30
|
+
def _fields(self) -> List[Dict]:
|
|
31
|
+
fields = model_fields.get(self._model, [])
|
|
32
|
+
if isinstance(fields, dict):
|
|
33
|
+
raise TypeError(
|
|
34
|
+
"AN5 Python metadata is out of date. Regenerate with @an5/orm >= 1.0.4 "
|
|
35
|
+
"so MODEL_FIELDS uses a list of field objects."
|
|
36
|
+
)
|
|
37
|
+
return fields
|
|
38
|
+
|
|
39
|
+
def _pagination(self, take: Optional[int], skip: int, order_sql: str) -> str:
|
|
40
|
+
if take is None:
|
|
41
|
+
return ""
|
|
42
|
+
if self._dialect == DIALECT_POSTGRES:
|
|
43
|
+
return f" LIMIT {take} OFFSET {skip}"
|
|
44
|
+
order_prefix = "" if order_sql else " ORDER BY (SELECT NULL)"
|
|
45
|
+
return f"{order_prefix} OFFSET {skip} ROWS FETCH NEXT {take} ROWS ONLY"
|
|
46
|
+
|
|
47
|
+
def find_many(self, where=None, order_by=None, skip: int = 0, take: Optional[int] = None, select=None) -> List[Dict]:
|
|
48
|
+
params: Dict = {}
|
|
49
|
+
where_sql = _parse_where(self._model, where, params, self._dialect)
|
|
50
|
+
order_sql = _build_order_by(order_by, self._dialect)
|
|
51
|
+
|
|
52
|
+
query = f"SELECT * FROM {self._table_sql}{self._nolock}"
|
|
53
|
+
if where_sql:
|
|
54
|
+
query += f" WHERE {where_sql}"
|
|
55
|
+
if order_sql:
|
|
56
|
+
query += f" {order_sql}"
|
|
57
|
+
query += self._pagination(take, skip, order_sql)
|
|
58
|
+
return self._adapter.exec(query, list(params.values()))
|
|
59
|
+
|
|
60
|
+
def find_first(self, where=None, order_by=None, select=None) -> Optional[Dict]:
|
|
61
|
+
rows = self.find_many(where=where, order_by=order_by, take=1, select=select)
|
|
62
|
+
return rows[0] if rows else None
|
|
63
|
+
|
|
64
|
+
def find_unique(self, where: Dict) -> Optional[Dict]:
|
|
65
|
+
return self.find_first(where=where)
|
|
66
|
+
|
|
67
|
+
def count(self, where=None) -> int:
|
|
68
|
+
params: Dict = {}
|
|
69
|
+
where_sql = _parse_where(self._model, where, params, self._dialect)
|
|
70
|
+
query = f"SELECT COUNT(*) AS cnt FROM {self._table_sql}{self._nolock}"
|
|
71
|
+
if where_sql:
|
|
72
|
+
query += f" WHERE {where_sql}"
|
|
73
|
+
rows = self._adapter.exec(query, list(params.values()))
|
|
74
|
+
return int(rows[0]["cnt"]) if rows else 0
|
|
75
|
+
|
|
76
|
+
def create(self, data: Dict) -> Dict:
|
|
77
|
+
id_field = next((f for f in self._fields if f.get("isId")), None)
|
|
78
|
+
if id_field and id_field["name"] not in data:
|
|
79
|
+
data = {**data, id_field["name"]: str(uuid.uuid4())}
|
|
80
|
+
|
|
81
|
+
cols = [k for k, v in data.items() if v is not None]
|
|
82
|
+
values = [data[c] for c in cols]
|
|
83
|
+
placeholders = ", ".join(["?" if self._dialect == DIALECT_MSSQL else "%s"] * len(cols))
|
|
84
|
+
col_list = ", ".join(_quote(c, self._dialect) for c in cols)
|
|
85
|
+
query = f"INSERT INTO {self._table_sql} ({col_list}) VALUES ({placeholders})"
|
|
86
|
+
self._adapter.execute(query, values)
|
|
87
|
+
|
|
88
|
+
if id_field:
|
|
89
|
+
return self.find_first(where={id_field["name"]: data[id_field["name"]]}) or data
|
|
90
|
+
return data
|
|
91
|
+
|
|
92
|
+
def create_many(self, data: List[Dict], skip_duplicates: bool = False) -> Dict:
|
|
93
|
+
count = 0
|
|
94
|
+
for row in data:
|
|
95
|
+
try:
|
|
96
|
+
self.create(row)
|
|
97
|
+
count += 1
|
|
98
|
+
except Exception:
|
|
99
|
+
if not skip_duplicates:
|
|
100
|
+
raise
|
|
101
|
+
return {"count": count}
|
|
102
|
+
|
|
103
|
+
def update(self, where: Dict, data: Dict) -> Optional[Dict]:
|
|
104
|
+
params: Dict = {}
|
|
105
|
+
where_sql = _parse_where(self._model, where, params, self._dialect, "w_")
|
|
106
|
+
set_parts: List[str] = []
|
|
107
|
+
set_values: List = []
|
|
108
|
+
placeholder = "?" if self._dialect == DIALECT_MSSQL else "%s"
|
|
109
|
+
for col, val in data.items():
|
|
110
|
+
if val is not None:
|
|
111
|
+
set_parts.append(f"{_quote(col, self._dialect)} = {placeholder}")
|
|
112
|
+
set_values.append(val)
|
|
113
|
+
|
|
114
|
+
all_values = set_values + list(params.values())
|
|
115
|
+
query = f"UPDATE {self._table_sql} SET {', '.join(set_parts)}"
|
|
116
|
+
if where_sql:
|
|
117
|
+
query += f" WHERE {where_sql}"
|
|
118
|
+
self._adapter.execute(query, all_values)
|
|
119
|
+
return self.find_first(where=where)
|
|
120
|
+
|
|
121
|
+
def update_many(self, where: Optional[Dict], data: Dict) -> Dict:
|
|
122
|
+
params: Dict = {}
|
|
123
|
+
where_sql = _parse_where(self._model, where, params, self._dialect, "w_")
|
|
124
|
+
set_parts: List[str] = []
|
|
125
|
+
set_values: List = []
|
|
126
|
+
placeholder = "?" if self._dialect == DIALECT_MSSQL else "%s"
|
|
127
|
+
for col, val in data.items():
|
|
128
|
+
set_parts.append(f"{_quote(col, self._dialect)} = {placeholder}")
|
|
129
|
+
set_values.append(val)
|
|
130
|
+
all_values = set_values + list(params.values())
|
|
131
|
+
query = f"UPDATE {self._table_sql} SET {', '.join(set_parts)}"
|
|
132
|
+
if where_sql:
|
|
133
|
+
query += f" WHERE {where_sql}"
|
|
134
|
+
count = self._adapter.execute(query, all_values)
|
|
135
|
+
return {"count": count}
|
|
136
|
+
|
|
137
|
+
def delete(self, where: Dict) -> Optional[Dict]:
|
|
138
|
+
existing = self.find_first(where=where)
|
|
139
|
+
params: Dict = {}
|
|
140
|
+
where_sql = _parse_where(self._model, where, params, self._dialect)
|
|
141
|
+
query = f"DELETE FROM {self._table_sql} WHERE {where_sql}"
|
|
142
|
+
self._adapter.execute(query, list(params.values()))
|
|
143
|
+
return existing
|
|
144
|
+
|
|
145
|
+
def delete_many(self, where: Optional[Dict] = None) -> Dict:
|
|
146
|
+
params: Dict = {}
|
|
147
|
+
where_sql = _parse_where(self._model, where, params, self._dialect)
|
|
148
|
+
query = f"DELETE FROM {self._table_sql}"
|
|
149
|
+
if where_sql:
|
|
150
|
+
query += f" WHERE {where_sql}"
|
|
151
|
+
count = self._adapter.execute(query, list(params.values()))
|
|
152
|
+
return {"count": count}
|
|
153
|
+
|
|
154
|
+
def upsert(self, where: Dict, create: Dict, update: Dict) -> Dict:
|
|
155
|
+
existing = self.find_first(where=where)
|
|
156
|
+
if existing:
|
|
157
|
+
return self.update(where=where, data=update) or existing
|
|
158
|
+
return self.create(data=create)
|
|
159
|
+
|
|
160
|
+
def aggregate(self, where=None, _count=None, _sum=None, _avg=None, _min=None, _max=None) -> Dict:
|
|
161
|
+
params: Dict = {}
|
|
162
|
+
where_sql = _parse_where(self._model, where, params, self._dialect)
|
|
163
|
+
aggs: List[str] = []
|
|
164
|
+
if _count:
|
|
165
|
+
aggs.append("COUNT(*) AS _count")
|
|
166
|
+
if _sum:
|
|
167
|
+
for f in (_sum if isinstance(_sum, list) else [_sum]):
|
|
168
|
+
aggs.append(f"SUM({_quote(f, self._dialect)}) AS _sum_{f}")
|
|
169
|
+
if _avg:
|
|
170
|
+
for f in (_avg if isinstance(_avg, list) else [_avg]):
|
|
171
|
+
aggs.append(f"AVG({_quote(f, self._dialect)}) AS _avg_{f}")
|
|
172
|
+
if _min:
|
|
173
|
+
for f in (_min if isinstance(_min, list) else [_min]):
|
|
174
|
+
aggs.append(f"MIN({_quote(f, self._dialect)}) AS _min_{f}")
|
|
175
|
+
if _max:
|
|
176
|
+
for f in (_max if isinstance(_max, list) else [_max]):
|
|
177
|
+
aggs.append(f"MAX({_quote(f, self._dialect)}) AS _max_{f}")
|
|
178
|
+
if not aggs:
|
|
179
|
+
aggs = ["COUNT(*) AS _count"]
|
|
180
|
+
|
|
181
|
+
query = f"SELECT {', '.join(aggs)} FROM {self._table_sql}"
|
|
182
|
+
if where_sql:
|
|
183
|
+
query += f" WHERE {where_sql}"
|
|
184
|
+
rows = self._adapter.exec(query, list(params.values()))
|
|
185
|
+
return rows[0] if rows else {}
|
|
186
|
+
|
|
187
|
+
def vector_search(self, vector: List[float], take: int = 10, where=None, vector_field: str = "embedding", distance_metric: str = "cosine") -> List[Dict]:
|
|
188
|
+
rows = self.find_many(where=where)
|
|
189
|
+
scored = []
|
|
190
|
+
for row in rows:
|
|
191
|
+
raw = row.get(vector_field)
|
|
192
|
+
if raw is None:
|
|
193
|
+
continue
|
|
194
|
+
try:
|
|
195
|
+
vec = json.loads(raw) if isinstance(raw, str) else list(raw)
|
|
196
|
+
except Exception:
|
|
197
|
+
continue
|
|
198
|
+
if not vec or len(vec) != len(vector):
|
|
199
|
+
continue
|
|
200
|
+
|
|
201
|
+
dot = sum(a * b for a, b in zip(vector, vec))
|
|
202
|
+
m1 = sum(a ** 2 for a in vector) ** 0.5
|
|
203
|
+
m2 = sum(b ** 2 for b in vec) ** 0.5
|
|
204
|
+
cosine = dot / (m1 * m2) if m1 and m2 else 0.0
|
|
205
|
+
|
|
206
|
+
if distance_metric == "cosine":
|
|
207
|
+
dist = 1.0 - cosine
|
|
208
|
+
elif distance_metric == "dot":
|
|
209
|
+
dist = -dot
|
|
210
|
+
else:
|
|
211
|
+
dist = sum((a - b) ** 2 for a, b in zip(vector, vec)) ** 0.5
|
|
212
|
+
|
|
213
|
+
scored.append((row, dist))
|
|
214
|
+
|
|
215
|
+
scored.sort(key=lambda x: x[1])
|
|
216
|
+
return [{**row, "distance": dist} for row, dist in scored[:take]]
|
|
@@ -1,13 +1,14 @@
|
|
|
1
|
-
import { sheets_v4 } from 'googleapis';
|
|
2
1
|
import { An5SheetsAdapterConfig, resolveConfig } from './config';
|
|
3
2
|
import { SheetsTableClient } from './tableClient';
|
|
4
3
|
import { SheetMeta } from './types';
|
|
5
4
|
export declare class An5SheetsAdapter {
|
|
6
5
|
private sheets;
|
|
6
|
+
private fetchApi;
|
|
7
7
|
config: ReturnType<typeof resolveConfig>;
|
|
8
8
|
private sheetsCache;
|
|
9
9
|
constructor(config: An5SheetsAdapterConfig);
|
|
10
|
-
|
|
10
|
+
private get isOAuth();
|
|
11
|
+
getSheets(): Promise<any>;
|
|
11
12
|
private getApi;
|
|
12
13
|
invalidateCache(): void;
|
|
13
14
|
listSheets(): Promise<string[]>;
|
|
@@ -7,14 +7,83 @@ const config_1 = require("./config");
|
|
|
7
7
|
const sqlExecutor_1 = require("./sqlExecutor");
|
|
8
8
|
const tableClient_1 = require("./tableClient");
|
|
9
9
|
const retry_1 = require("./retry");
|
|
10
|
+
// ─── Fetch-based API proxy for OAuth Access Token (browser-compatible) ────────
|
|
11
|
+
function createFetchApi(spreadsheetId, accessToken) {
|
|
12
|
+
const base = `https://sheets.googleapis.com/v4/spreadsheets/${spreadsheetId}`;
|
|
13
|
+
const headers = { Authorization: `Bearer ${accessToken}`, 'Content-Type': 'application/json' };
|
|
14
|
+
async function request(method, path, body) {
|
|
15
|
+
const url = path.startsWith('http') ? path : `${base}${path}`;
|
|
16
|
+
const opts = { method, headers };
|
|
17
|
+
if (body)
|
|
18
|
+
opts.body = JSON.stringify(body);
|
|
19
|
+
const res = await fetch(url, opts);
|
|
20
|
+
if (!res.ok) {
|
|
21
|
+
const text = await res.text().catch(() => '');
|
|
22
|
+
const err = new Error(`Google Sheets API error (${res.status}): ${text}`);
|
|
23
|
+
err.status = res.status;
|
|
24
|
+
err.body = text;
|
|
25
|
+
const retryAfter = res.headers.get('Retry-After');
|
|
26
|
+
if (retryAfter)
|
|
27
|
+
err.retryAfter = parseInt(retryAfter, 10);
|
|
28
|
+
throw err;
|
|
29
|
+
}
|
|
30
|
+
const text = await res.text();
|
|
31
|
+
return text ? JSON.parse(text) : undefined;
|
|
32
|
+
}
|
|
33
|
+
function fmtRange(range) {
|
|
34
|
+
return `/${encodeURIComponent(range)}`;
|
|
35
|
+
}
|
|
36
|
+
return {
|
|
37
|
+
spreadsheets: {
|
|
38
|
+
get: async (params) => {
|
|
39
|
+
const data = await request('GET', `?fields=${params.fields || ''}`);
|
|
40
|
+
return { data };
|
|
41
|
+
},
|
|
42
|
+
batchUpdate: async (params) => {
|
|
43
|
+
const data = await request('POST', ':batchUpdate', params.requestBody);
|
|
44
|
+
return { data };
|
|
45
|
+
},
|
|
46
|
+
values: {
|
|
47
|
+
get: async (params) => {
|
|
48
|
+
const data = await request('GET', fmtRange(params.range));
|
|
49
|
+
return { data };
|
|
50
|
+
},
|
|
51
|
+
update: async (params) => {
|
|
52
|
+
const qs = `?valueInputOption=${params.valueInputOption || 'RAW'}`;
|
|
53
|
+
const data = await request('PUT', `${fmtRange(params.range)}${qs}`, params.requestBody);
|
|
54
|
+
return { data };
|
|
55
|
+
},
|
|
56
|
+
append: async (params) => {
|
|
57
|
+
const qs = `?valueInputOption=${params.valueInputOption || 'RAW'}&insertDataOption=${params.insertDataOption || 'INSERT_ROWS'}`;
|
|
58
|
+
await request('POST', `${fmtRange(params.range)}:append${qs}`, params.requestBody);
|
|
59
|
+
return { data: { updates: { updatedRows: (params.requestBody?.values || []).length } } };
|
|
60
|
+
},
|
|
61
|
+
clear: async (params) => {
|
|
62
|
+
await request('POST', `${fmtRange(params.range)}:clear`);
|
|
63
|
+
return { data: {} };
|
|
64
|
+
},
|
|
65
|
+
},
|
|
66
|
+
},
|
|
67
|
+
};
|
|
68
|
+
}
|
|
10
69
|
// ─── Adapter ──────────────────────────────────────────────────────────────────
|
|
11
70
|
class An5SheetsAdapter {
|
|
12
71
|
constructor(config) {
|
|
13
72
|
this.sheets = null;
|
|
73
|
+
this.fetchApi = null;
|
|
14
74
|
this.sheetsCache = null;
|
|
15
75
|
this.config = (0, config_1.resolveConfig)(config);
|
|
16
76
|
}
|
|
77
|
+
get isOAuth() {
|
|
78
|
+
return !!this.config.accessToken;
|
|
79
|
+
}
|
|
17
80
|
async getSheets() {
|
|
81
|
+
if (this.isOAuth) {
|
|
82
|
+
if (!this.fetchApi) {
|
|
83
|
+
this.fetchApi = createFetchApi(this.config.spreadsheetId, this.config.accessToken);
|
|
84
|
+
}
|
|
85
|
+
return this.fetchApi;
|
|
86
|
+
}
|
|
18
87
|
if (!this.sheets) {
|
|
19
88
|
const auth = new googleapis_1.google.auth.JWT({
|
|
20
89
|
email: this.config.clientEmail,
|
|
@@ -37,7 +106,7 @@ class An5SheetsAdapter {
|
|
|
37
106
|
spreadsheetId: this.config.spreadsheetId,
|
|
38
107
|
fields: 'sheets.properties.title',
|
|
39
108
|
}));
|
|
40
|
-
return (res.data.sheets || []).map(s => s.properties?.title || '');
|
|
109
|
+
return (res.data.sheets || []).map((s) => s.properties?.title || '');
|
|
41
110
|
}
|
|
42
111
|
async getSheetMeta(name) {
|
|
43
112
|
const api = await this.getApi();
|
|
@@ -47,14 +116,14 @@ class An5SheetsAdapter {
|
|
|
47
116
|
spreadsheetId: this.config.spreadsheetId,
|
|
48
117
|
fields: 'sheets.properties',
|
|
49
118
|
});
|
|
50
|
-
return (res.data.sheets || []).map(s => ({
|
|
119
|
+
return (res.data.sheets || []).map((s) => ({
|
|
51
120
|
sheetId: s.properties?.sheetId ?? -1,
|
|
52
121
|
title: s.properties?.title ?? '',
|
|
53
|
-
})).filter(m => m.sheetId >= 0);
|
|
122
|
+
})).filter((m) => m.sheetId >= 0);
|
|
54
123
|
});
|
|
55
124
|
}
|
|
56
125
|
const all = await this.sheetsCache;
|
|
57
|
-
return all.find(m => m.title === name) || null;
|
|
126
|
+
return all.find((m) => m.title === name) || null;
|
|
58
127
|
}
|
|
59
128
|
async deleteSheet(name) {
|
|
60
129
|
const meta = await this.getSheetMeta(name);
|
|
@@ -74,6 +143,7 @@ class An5SheetsAdapter {
|
|
|
74
143
|
}
|
|
75
144
|
async $disconnect() {
|
|
76
145
|
this.sheets = null;
|
|
146
|
+
this.fetchApi = null;
|
|
77
147
|
this.sheetsCache = null;
|
|
78
148
|
}
|
|
79
149
|
async exec(query, params) {
|