@an5/adapters 0.2.0 → 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.
@@ -1,216 +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
- 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
+ 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
- getSheets(): Promise<sheets_v4.Sheets>;
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) {