@an5/adapters 0.2.9 → 0.2.11
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 +30 -0
- package/dist/an5Adapter.js +54 -31
- package/dist/base/metadata.d.ts +2 -0
- package/dist/base/metadata.js +10 -0
- package/dist/base/sql.d.ts +2 -0
- package/dist/base/sql.js +40 -40
- package/dist/googlesheets/adapter.js +16 -8
- package/dist/googlesheets/config.d.ts +13 -0
- package/dist/googlesheets/config.js +5 -1
- package/dist/googlesheets/helpers.js +13 -8
- package/dist/googlesheets/oauth.d.ts +3 -0
- package/dist/googlesheets/oauth.js +43 -0
- package/dist/googlesheets/parseConnectionString.js +12 -0
- package/dist/googlesheets/tableClient.js +5 -23
- package/dist/sqlite/engine.js +2 -1
- package/dotnet/Base/Core.cs +14 -24
- package/golang/base/where.go +32 -49
- package/golang/base/where_test.go +57 -0
- package/package.json +19 -6
- package/python/base/sql.py +6 -7
- package/rust/src/adapter.rs +2 -0
- package/rust/src/base/dialect.rs +38 -9
- package/rust/src/base/metadata.rs +4 -0
- package/rust/src/base/where_builder.rs +9 -28
- package/typescript/src/an5Adapter.ts +56 -30
- package/typescript/src/base/metadata.ts +8 -0
- package/typescript/src/base/sql.ts +38 -30
- package/typescript/src/googlesheets/adapter.ts +17 -8
- package/typescript/src/googlesheets/config.ts +10 -1
- package/typescript/src/googlesheets/helpers.ts +11 -8
- package/typescript/src/googlesheets/oauth.ts +31 -0
- package/typescript/src/googlesheets/parseConnectionString.ts +8 -0
- package/typescript/src/googlesheets/tableClient.ts +5 -19
- package/typescript/src/sqlite/engine.ts +2 -1
package/README.md
CHANGED
|
@@ -276,8 +276,38 @@ db.transaction(perform_transfer)
|
|
|
276
276
|
| `groupBy(args)` | Group by fields with aggregations and pagination |
|
|
277
277
|
| `vectorSearch(args)` | Semantic vector similarity search |
|
|
278
278
|
|
|
279
|
+
Generated TypeScript field metadata marks schema primary keys with `isId: true`.
|
|
280
|
+
SQL and Google Sheets `create` use this marker to identify custom primary-key
|
|
281
|
+
names before falling back to `id` or `...Id` for older metadata. Regenerate the
|
|
282
|
+
client to enable this behavior for existing custom-key schemas.
|
|
283
|
+
|
|
284
|
+
`createMany` uses a bulk statement when every row supplies the same columns.
|
|
285
|
+
Rows with different column sets or missing generated UUID keys are inserted
|
|
286
|
+
individually so omitted columns retain their defaults and UUID keys are assigned. Bulk failures are propagated without retrying
|
|
287
|
+
individual rows. Wrap mixed-column inserts in `$transaction` when they must
|
|
288
|
+
succeed or roll back together.
|
|
289
|
+
|
|
290
|
+
For SQL adapters, `skipDuplicates: true` skips only unique or primary-key
|
|
291
|
+
violations. Connection failures and other constraint errors are propagated.
|
|
292
|
+
Google Sheets does not enforce unique constraints; this flag does not deduplicate
|
|
293
|
+
sheet rows, and failed batch appends are propagated without individual retries.
|
|
294
|
+
|
|
295
|
+
For SQL adapters, `findMany` and `groupBy` accept `skip` without `take` to
|
|
296
|
+
return all remaining rows or groups. Supply `orderBy` for stable row pagination;
|
|
297
|
+
`groupBy` orders by its grouping fields when pagination has no explicit ordering.
|
|
298
|
+
With relation metadata configured, `groupBy.where` supports the same relation
|
|
299
|
+
filters as `findMany.where`, including `some`, `none`, `every`, `is`, and `isNot`.
|
|
300
|
+
|
|
279
301
|
---
|
|
280
302
|
|
|
281
303
|
## License
|
|
282
304
|
|
|
283
305
|
MIT
|
|
306
|
+
|
|
307
|
+
### Google Sheets desktop OAuth
|
|
308
|
+
|
|
309
|
+
Google Sheets connections accept `accessToken`, `refreshToken`, `oauthClientId`, optional `oauthClientSecret`, and `tokenExpiresAt` (Unix milliseconds). The VS Code connection UI can generate and store this URI after browser sign-in and spreadsheet selection. Values in the semicolon-delimited URI must be URL encoded. Keep offline credentials in secure storage; do not commit them.
|
|
310
|
+
|
|
311
|
+
The adapter refreshes expired access tokens before requests, shares concurrent refresh requests, and retries authentication failures once after refreshing. Revoked credentials report a reconnect error. Existing service account, API key and access-token-only configurations remain supported.
|
|
312
|
+
|
|
313
|
+
For web applications, use a Google Web OAuth client and per-user access tokens. The browser adapter rejects offline desktop credentials and service account private keys. A browser connection manager must handle account selection, token reacquisition and SQLite persistence; see the [browser connection design](https://an5orm.github.io/docs/guides/browser-connections/).
|
package/dist/an5Adapter.js
CHANGED
|
@@ -28,38 +28,57 @@ function sanitizeParamName(name) {
|
|
|
28
28
|
const cleaned = String(name).replace(/[^A-Za-z0-9_]/g, '_');
|
|
29
29
|
return /^[A-Za-z]/.test(cleaned) ? cleaned : `p_${cleaned}`;
|
|
30
30
|
}
|
|
31
|
+
/** Match only unique/primary-key violations, never general constraint errors. */
|
|
32
|
+
function isDuplicateKeyError(error, dialect) {
|
|
33
|
+
if (!error || typeof error !== 'object')
|
|
34
|
+
return false;
|
|
35
|
+
const e = error;
|
|
36
|
+
if (dialect === 'postgres')
|
|
37
|
+
return e.code === '23505';
|
|
38
|
+
if (dialect === 'mysql')
|
|
39
|
+
return e.code === 'ER_DUP_ENTRY' || e.errno === 1062;
|
|
40
|
+
if (dialect === 'sqlite') {
|
|
41
|
+
return e.code === 'SQLITE_CONSTRAINT_UNIQUE' || e.code === 'SQLITE_CONSTRAINT_PRIMARYKEY'
|
|
42
|
+
|| e.errcode === 2067 || e.errcode === 1555;
|
|
43
|
+
}
|
|
44
|
+
if (dialect === 'mssql') {
|
|
45
|
+
const number = e.number ?? e.originalError?.info?.number;
|
|
46
|
+
return number === 2601 || number === 2627;
|
|
47
|
+
}
|
|
48
|
+
return false;
|
|
49
|
+
}
|
|
31
50
|
function appendUpdateSet(sets, params, col, val, dialect) {
|
|
32
51
|
const quoted = (0, sql_1.quote)(col, dialect);
|
|
33
52
|
const safeCol = sanitizeParamName(col);
|
|
34
53
|
if (val && typeof val === 'object' && !(val instanceof Date)) {
|
|
35
54
|
if (val.increment !== undefined) {
|
|
36
|
-
|
|
37
|
-
|
|
55
|
+
const p = (0, sql_1.bindParam)(params, `s_${safeCol}_inc`, val.increment);
|
|
56
|
+
sets.push(`${quoted} = ${quoted} + @${p}`);
|
|
38
57
|
return;
|
|
39
58
|
}
|
|
40
59
|
if (val.decrement !== undefined) {
|
|
41
|
-
|
|
42
|
-
|
|
60
|
+
const p = (0, sql_1.bindParam)(params, `s_${safeCol}_dec`, val.decrement);
|
|
61
|
+
sets.push(`${quoted} = ${quoted} - @${p}`);
|
|
43
62
|
return;
|
|
44
63
|
}
|
|
45
64
|
if (val.multiply !== undefined) {
|
|
46
|
-
|
|
47
|
-
|
|
65
|
+
const p = (0, sql_1.bindParam)(params, `s_${safeCol}_mul`, val.multiply);
|
|
66
|
+
sets.push(`${quoted} = ${quoted} * @${p}`);
|
|
48
67
|
return;
|
|
49
68
|
}
|
|
50
69
|
if (val.divide !== undefined) {
|
|
51
|
-
|
|
52
|
-
|
|
70
|
+
const p = (0, sql_1.bindParam)(params, `s_${safeCol}_div`, val.divide);
|
|
71
|
+
sets.push(`${quoted} = ${quoted} / @${p}`);
|
|
53
72
|
return;
|
|
54
73
|
}
|
|
55
74
|
if (val.set !== undefined) {
|
|
56
|
-
|
|
57
|
-
|
|
75
|
+
const p = (0, sql_1.bindParam)(params, `s_${safeCol}_set`, val.set);
|
|
76
|
+
sets.push(`${quoted} = @${p}`);
|
|
58
77
|
return;
|
|
59
78
|
}
|
|
60
79
|
}
|
|
61
|
-
|
|
62
|
-
|
|
80
|
+
const p = (0, sql_1.bindParam)(params, `s_${safeCol}`, val);
|
|
81
|
+
sets.push(`${quoted} = @${p}`);
|
|
63
82
|
}
|
|
64
83
|
function selectedAggregateFields(fields) {
|
|
65
84
|
if (!fields || typeof fields !== 'object')
|
|
@@ -275,7 +294,8 @@ class An5Adapter {
|
|
|
275
294
|
else if (forDialect.startsWith('mysql://') || forDialect.startsWith('mariadb://')) {
|
|
276
295
|
this._engineType = 'mysql';
|
|
277
296
|
}
|
|
278
|
-
else if (forDialect
|
|
297
|
+
else if (forDialect === ':memory:'
|
|
298
|
+
|| forDialect.startsWith('sqlite:')
|
|
279
299
|
|| SQLITE_FILE_SUFFIXES.some((suffix) => forDialect.endsWith(suffix))) {
|
|
280
300
|
this._engineType = 'sqlite';
|
|
281
301
|
}
|
|
@@ -716,7 +736,8 @@ class AdapterTableClient {
|
|
|
716
736
|
query += ` WHERE ${whereSql}`;
|
|
717
737
|
if (orderSql)
|
|
718
738
|
query += ` ${orderSql}`;
|
|
719
|
-
|
|
739
|
+
const limit = take ?? (this.dialect === 'sqlite' ? '-1' : this.dialect === 'mysql' ? '18446744073709551615' : 'ALL');
|
|
740
|
+
query += ` LIMIT ${limit} OFFSET ${skip}`;
|
|
720
741
|
}
|
|
721
742
|
else {
|
|
722
743
|
// mssql requires ORDER BY before OFFSET/FETCH
|
|
@@ -778,9 +799,7 @@ class AdapterTableClient {
|
|
|
778
799
|
}
|
|
779
800
|
}
|
|
780
801
|
const fields = (0, metadata_1.getFieldsForModel)(this.modelName);
|
|
781
|
-
const idFieldName =
|
|
782
|
-
? 'id'
|
|
783
|
-
: Object.keys(fields).find((name) => name.endsWith('_id') || name.endsWith('Id') || name.toLowerCase() === 'id');
|
|
802
|
+
const idFieldName = (0, metadata_1.resolveIdField)(fields);
|
|
784
803
|
const data = { ...rawData };
|
|
785
804
|
if (idFieldName) {
|
|
786
805
|
const fieldDef = fields[idFieldName];
|
|
@@ -835,8 +854,18 @@ class AdapterTableClient {
|
|
|
835
854
|
if (firstRow === undefined)
|
|
836
855
|
return { count: 0 };
|
|
837
856
|
const firstCols = Object.keys(firstRow).filter(k => firstRow[k] !== undefined);
|
|
838
|
-
|
|
839
|
-
|
|
857
|
+
const sameColumns = args.data.every(row => {
|
|
858
|
+
const cols = Object.keys(row).filter(k => row[k] !== undefined);
|
|
859
|
+
return cols.length === firstCols.length && firstCols.every(col => cols.includes(col));
|
|
860
|
+
});
|
|
861
|
+
const fields = (0, metadata_1.getFieldsForModel)(this.modelName);
|
|
862
|
+
const idFieldName = (0, metadata_1.resolveIdField)(fields);
|
|
863
|
+
const idDef = idFieldName ? fields[idFieldName] : undefined;
|
|
864
|
+
const idType = typeof idDef === 'string' ? idDef : (idDef?.ts || idDef?.sql || idDef?.type || '');
|
|
865
|
+
const generatesId = ['string', 'uuid', 'uniqueidentifier', 'nvarchar', 'varchar', 'text'].includes(String(idType).toLowerCase());
|
|
866
|
+
const needsGeneratedId = idFieldName && generatesId && args.data.some(row => !row[idFieldName]);
|
|
867
|
+
// Rows needing generated IDs follow create's default handling.
|
|
868
|
+
if (firstCols.length > 0 && sameColumns && !needsGeneratedId && !args.skipDuplicates) {
|
|
840
869
|
const params = {};
|
|
841
870
|
const rowPlaceholders = [];
|
|
842
871
|
for (let r = 0; r < args.data.length; r++) {
|
|
@@ -849,13 +878,8 @@ class AdapterTableClient {
|
|
|
849
878
|
rowPlaceholders.push(`(${vals.join(', ')})`);
|
|
850
879
|
}
|
|
851
880
|
const query = `INSERT INTO ${this.tableName} (${firstCols.map(c => (0, sql_1.quote)(c, this.dialect)).join(', ')}) VALUES ${rowPlaceholders.join(', ')}`;
|
|
852
|
-
|
|
853
|
-
|
|
854
|
-
return { count: args.data.length };
|
|
855
|
-
}
|
|
856
|
-
catch {
|
|
857
|
-
// fallback below if bulk fails (e.g. mixed column sets)
|
|
858
|
-
}
|
|
881
|
+
await this.doExecuteRaw(query, params);
|
|
882
|
+
return { count: args.data.length };
|
|
859
883
|
}
|
|
860
884
|
// Row-by-row fallback
|
|
861
885
|
let count = 0;
|
|
@@ -865,7 +889,7 @@ class AdapterTableClient {
|
|
|
865
889
|
count++;
|
|
866
890
|
}
|
|
867
891
|
catch (e) {
|
|
868
|
-
if (!args.skipDuplicates)
|
|
892
|
+
if (!args.skipDuplicates || !isDuplicateKeyError(e, this.dialect))
|
|
869
893
|
throw e;
|
|
870
894
|
}
|
|
871
895
|
}
|
|
@@ -1045,7 +1069,7 @@ class AdapterTableClient {
|
|
|
1045
1069
|
}
|
|
1046
1070
|
async groupBy(args) {
|
|
1047
1071
|
const params = {};
|
|
1048
|
-
const whereSql = (0, sql_1.parseWhere)(this.modelName, args?.where, params, this.dialect);
|
|
1072
|
+
const whereSql = (0, sql_1.parseWhere)(this.modelName, args?.where, params, this.dialect, '', this.whereContext());
|
|
1049
1073
|
const byFields = normalizeByFields(args?.by);
|
|
1050
1074
|
if (byFields.length === 0)
|
|
1051
1075
|
throw new Error("groupBy requires 'by' fields");
|
|
@@ -1075,9 +1099,8 @@ class AdapterTableClient {
|
|
|
1075
1099
|
const skip = toNonNegativeInt(args?.skip);
|
|
1076
1100
|
const take = toNonNegativeInt(args?.take, 1);
|
|
1077
1101
|
if (this.dialect === 'postgres' || this.dialect === 'mysql' || this.dialect === 'sqlite') {
|
|
1078
|
-
|
|
1079
|
-
|
|
1080
|
-
query += ` OFFSET ${skip}`;
|
|
1102
|
+
const limit = hasTake ? take : (this.dialect === 'sqlite' ? '-1' : this.dialect === 'mysql' ? '18446744073709551615' : 'ALL');
|
|
1103
|
+
query += ` LIMIT ${limit} OFFSET ${skip}`;
|
|
1081
1104
|
}
|
|
1082
1105
|
else {
|
|
1083
1106
|
query += ` OFFSET ${skip} ROWS`;
|
package/dist/base/metadata.d.ts
CHANGED
|
@@ -11,6 +11,8 @@ export interface ModelFieldMeta {
|
|
|
11
11
|
isId?: boolean;
|
|
12
12
|
[key: string]: any;
|
|
13
13
|
}
|
|
14
|
+
/** Prefer schema primary-key metadata; retain name inference for legacy clients. */
|
|
15
|
+
export declare function resolveIdField(fields: Record<string, any>): string | undefined;
|
|
14
16
|
export interface AdapterMetadata {
|
|
15
17
|
modelToTable?: Record<string, string>;
|
|
16
18
|
modelFields?: Record<string, any>;
|
package/dist/base/metadata.js
CHANGED
|
@@ -1,11 +1,21 @@
|
|
|
1
1
|
"use strict";
|
|
2
2
|
Object.defineProperty(exports, "__esModule", { value: true });
|
|
3
|
+
exports.resolveIdField = resolveIdField;
|
|
3
4
|
exports.setAdapterMetadata = setAdapterMetadata;
|
|
4
5
|
exports.getModelToTable = getModelToTable;
|
|
5
6
|
exports.getFieldsForModel = getFieldsForModel;
|
|
6
7
|
exports.getRelationMap = getRelationMap;
|
|
7
8
|
exports.getRelationsForModel = getRelationsForModel;
|
|
8
9
|
exports.createAdapterProxy = createAdapterProxy;
|
|
10
|
+
/** Prefer schema primary-key metadata; retain name inference for legacy clients. */
|
|
11
|
+
function resolveIdField(fields) {
|
|
12
|
+
const primaryKey = Object.keys(fields).find(name => fields[name]?.isId === true);
|
|
13
|
+
if (primaryKey)
|
|
14
|
+
return primaryKey;
|
|
15
|
+
if (Object.prototype.hasOwnProperty.call(fields, 'id'))
|
|
16
|
+
return 'id';
|
|
17
|
+
return Object.keys(fields).find(name => name.endsWith('_id') || name.endsWith('Id') || name.toLowerCase() === 'id');
|
|
18
|
+
}
|
|
9
19
|
let modelToTable = {};
|
|
10
20
|
let modelFields = {};
|
|
11
21
|
let relationMap = {};
|
package/dist/base/sql.d.ts
CHANGED
|
@@ -3,6 +3,8 @@ import type { RelationDef } from './metadata';
|
|
|
3
3
|
export declare function quote(name: string, dialect: Dialect): string;
|
|
4
4
|
/** Quote a full table reference (may already be quoted, and may contain schema parts). */
|
|
5
5
|
export declare function quoteTable(name: string, dialect: Dialect): string;
|
|
6
|
+
/** Allocate parameters without overwriting values from sibling clauses. */
|
|
7
|
+
export declare function bindParam(params: Record<string, any>, name: string, value: any): string;
|
|
6
8
|
export interface WhereContext {
|
|
7
9
|
/** Relations of the current model keyed by relation name. */
|
|
8
10
|
relationMap?: Record<string, Record<string, RelationDef>>;
|
package/dist/base/sql.js
CHANGED
|
@@ -2,6 +2,7 @@
|
|
|
2
2
|
Object.defineProperty(exports, "__esModule", { value: true });
|
|
3
3
|
exports.quote = quote;
|
|
4
4
|
exports.quoteTable = quoteTable;
|
|
5
|
+
exports.bindParam = bindParam;
|
|
5
6
|
exports.parseWhere = parseWhere;
|
|
6
7
|
exports.buildOrderBy = buildOrderBy;
|
|
7
8
|
// ─── Quoting ───────────────────────────────────────────────────────────────────────
|
|
@@ -38,6 +39,15 @@ function sanitizeParamName(name) {
|
|
|
38
39
|
const cleaned = String(name).replace(/[^A-Za-z0-9_]/g, '_');
|
|
39
40
|
return /^[A-Za-z]/.test(cleaned) ? cleaned : `p_${cleaned}`;
|
|
40
41
|
}
|
|
42
|
+
/** Allocate parameters without overwriting values from sibling clauses. */
|
|
43
|
+
function bindParam(params, name, value) {
|
|
44
|
+
let candidate = name;
|
|
45
|
+
let suffix = 1;
|
|
46
|
+
while (Object.prototype.hasOwnProperty.call(params, candidate))
|
|
47
|
+
candidate = `${name}_${suffix++}`;
|
|
48
|
+
params[candidate] = value;
|
|
49
|
+
return candidate;
|
|
50
|
+
}
|
|
41
51
|
function normalizeSortDirection(dir) {
|
|
42
52
|
return typeof dir === 'string' && dir.toUpperCase() === 'DESC' ? 'DESC' : 'ASC';
|
|
43
53
|
}
|
|
@@ -68,9 +78,10 @@ function parseWhere(modelName, where, params, dialect, prefix = '', ctx) {
|
|
|
68
78
|
if (!where)
|
|
69
79
|
return '';
|
|
70
80
|
const conditions = [];
|
|
81
|
+
const relations = ctx?.relationMap?.[modelName] || {};
|
|
71
82
|
const cleanWhere = {};
|
|
72
83
|
for (const [key, value] of Object.entries(where)) {
|
|
73
|
-
if (key.includes('_') &&
|
|
84
|
+
if (key.includes('_') && !relations[key] &&
|
|
74
85
|
value && typeof value === 'object' &&
|
|
75
86
|
!(value instanceof Date) &&
|
|
76
87
|
!isOperatorValue(value)) {
|
|
@@ -80,13 +91,13 @@ function parseWhere(modelName, where, params, dialect, prefix = '', ctx) {
|
|
|
80
91
|
cleanWhere[key] = value;
|
|
81
92
|
}
|
|
82
93
|
}
|
|
83
|
-
const relations = ctx?.relationMap?.[modelName] || {};
|
|
84
94
|
const colPrefix = ctx?.colPrefix || '';
|
|
85
95
|
for (const [key, value] of Object.entries(cleanWhere)) {
|
|
96
|
+
if (value === undefined)
|
|
97
|
+
continue;
|
|
86
98
|
if (key === 'OR' && Array.isArray(value)) {
|
|
87
|
-
const sub = value.map((v, i) => parseWhere(modelName, v, params, dialect, `${prefix}or_${i}_`, ctx))
|
|
88
|
-
|
|
89
|
-
conditions.push(`(${sub.join(' OR ')})`);
|
|
99
|
+
const sub = value.map((v, i) => parseWhere(modelName, v, params, dialect, `${prefix}or_${i}_`, ctx) || '1=1');
|
|
100
|
+
conditions.push(sub.length > 0 ? `(${sub.join(' OR ')})` : '1=0');
|
|
90
101
|
}
|
|
91
102
|
else if (key === 'AND') {
|
|
92
103
|
const items = Array.isArray(value) ? value : [value];
|
|
@@ -96,12 +107,12 @@ function parseWhere(modelName, where, params, dialect, prefix = '', ctx) {
|
|
|
96
107
|
}
|
|
97
108
|
else if (key === 'NOT') {
|
|
98
109
|
const items = Array.isArray(value) ? value : [value];
|
|
99
|
-
const sub = items.map((v, i) => parseWhere(modelName, v, params, dialect, `${prefix}not_${i}_`, ctx))
|
|
110
|
+
const sub = items.map((v, i) => parseWhere(modelName, v, params, dialect, `${prefix}not_${i}_`, ctx) || '1=1');
|
|
100
111
|
if (sub.length > 0)
|
|
101
|
-
conditions.push(`NOT (${sub.join('
|
|
112
|
+
conditions.push(`NOT (${sub.join(' OR ')})`);
|
|
102
113
|
}
|
|
103
114
|
else if (relations[key] && value && typeof value === 'object') {
|
|
104
|
-
const clause = buildRelationClause(relations[key], value, modelName, params, dialect, prefix
|
|
115
|
+
const clause = buildRelationClause(relations[key], value, modelName, params, dialect, `${prefix}${key}_`, ctx);
|
|
105
116
|
if (clause)
|
|
106
117
|
conditions.push(clause);
|
|
107
118
|
}
|
|
@@ -118,15 +129,12 @@ function parseWhere(modelName, where, params, dialect, prefix = '', ctx) {
|
|
|
118
129
|
conditions.push(`${col} IS NOT NULL`);
|
|
119
130
|
}
|
|
120
131
|
else if (v.not && typeof v.not === 'object' && !(v.not instanceof Date) && !Array.isArray(v.not)) {
|
|
121
|
-
const
|
|
122
|
-
const nestedSql = parseWhere(modelName, { [key]: v.not }, nestedParams, dialect, `${prefix}${key}_not_`, ctx);
|
|
123
|
-
Object.assign(params, nestedParams);
|
|
132
|
+
const nestedSql = parseWhere(modelName, { [key]: v.not }, params, dialect, `${prefix}${key}_not_`, ctx);
|
|
124
133
|
if (nestedSql)
|
|
125
134
|
conditions.push(`NOT (${nestedSql})`);
|
|
126
135
|
}
|
|
127
136
|
else {
|
|
128
|
-
const p = `${pname}_not
|
|
129
|
-
params[p] = v.not;
|
|
137
|
+
const p = bindParam(params, `${pname}_not`, v.not);
|
|
130
138
|
conditions.push(`${col} <> @${p}`);
|
|
131
139
|
}
|
|
132
140
|
}
|
|
@@ -135,49 +143,41 @@ function parseWhere(modelName, where, params, dialect, prefix = '', ctx) {
|
|
|
135
143
|
conditions.push(`${col} IS NULL`);
|
|
136
144
|
}
|
|
137
145
|
else {
|
|
138
|
-
const p = `${pname}_eq
|
|
139
|
-
params[p] = v.equals;
|
|
146
|
+
const p = bindParam(params, `${pname}_eq`, v.equals);
|
|
140
147
|
conditions.push(`${col} = @${p}`);
|
|
141
148
|
}
|
|
142
149
|
}
|
|
143
150
|
if (v.contains !== undefined) {
|
|
144
|
-
const p = `${pname}_co
|
|
145
|
-
params[p] = `%${v.contains}%`;
|
|
151
|
+
const p = bindParam(params, `${pname}_co`, `%${v.contains}%`);
|
|
146
152
|
conditions.push(`${col} LIKE @${p}`);
|
|
147
153
|
}
|
|
148
154
|
if (v.startsWith !== undefined) {
|
|
149
|
-
const p = `${pname}_sw
|
|
150
|
-
params[p] = `${v.startsWith}%`;
|
|
155
|
+
const p = bindParam(params, `${pname}_sw`, `${v.startsWith}%`);
|
|
151
156
|
conditions.push(`${col} LIKE @${p}`);
|
|
152
157
|
}
|
|
153
158
|
if (v.endsWith !== undefined) {
|
|
154
|
-
const p = `${pname}_ew
|
|
155
|
-
params[p] = `%${v.endsWith}`;
|
|
159
|
+
const p = bindParam(params, `${pname}_ew`, `%${v.endsWith}`);
|
|
156
160
|
conditions.push(`${col} LIKE @${p}`);
|
|
157
161
|
}
|
|
158
162
|
if (v.gte !== undefined) {
|
|
159
|
-
const p = `${pname}_gte
|
|
160
|
-
params[p] = v.gte;
|
|
163
|
+
const p = bindParam(params, `${pname}_gte`, v.gte);
|
|
161
164
|
conditions.push(`${col} >= @${p}`);
|
|
162
165
|
}
|
|
163
166
|
if (v.lte !== undefined) {
|
|
164
|
-
const p = `${pname}_lte
|
|
165
|
-
params[p] = v.lte;
|
|
167
|
+
const p = bindParam(params, `${pname}_lte`, v.lte);
|
|
166
168
|
conditions.push(`${col} <= @${p}`);
|
|
167
169
|
}
|
|
168
170
|
if (v.gt !== undefined) {
|
|
169
|
-
const p = `${pname}_gt
|
|
170
|
-
params[p] = v.gt;
|
|
171
|
+
const p = bindParam(params, `${pname}_gt`, v.gt);
|
|
171
172
|
conditions.push(`${col} > @${p}`);
|
|
172
173
|
}
|
|
173
174
|
if (v.lt !== undefined) {
|
|
174
|
-
const p = `${pname}_lt
|
|
175
|
-
params[p] = v.lt;
|
|
175
|
+
const p = bindParam(params, `${pname}_lt`, v.lt);
|
|
176
176
|
conditions.push(`${col} < @${p}`);
|
|
177
177
|
}
|
|
178
178
|
if (v.in !== undefined) {
|
|
179
179
|
if (Array.isArray(v.in) && v.in.length > 0) {
|
|
180
|
-
const ps = v.in.map((x, i) => { const p = `${pname}_in${i}
|
|
180
|
+
const ps = v.in.map((x, i) => { const p = bindParam(params, `${pname}_in${i}`, x); return `@${p}`; });
|
|
181
181
|
conditions.push(`${col} IN (${ps.join(', ')})`);
|
|
182
182
|
}
|
|
183
183
|
else {
|
|
@@ -186,7 +186,7 @@ function parseWhere(modelName, where, params, dialect, prefix = '', ctx) {
|
|
|
186
186
|
}
|
|
187
187
|
if (v.notIn !== undefined) {
|
|
188
188
|
if (Array.isArray(v.notIn) && v.notIn.length > 0) {
|
|
189
|
-
const ps = v.notIn.map((x, i) => { const p = `${pname}_notin${i}
|
|
189
|
+
const ps = v.notIn.map((x, i) => { const p = bindParam(params, `${pname}_notin${i}`, x); return `@${p}`; });
|
|
190
190
|
conditions.push(`${col} NOT IN (${ps.join(', ')})`);
|
|
191
191
|
}
|
|
192
192
|
else {
|
|
@@ -195,8 +195,8 @@ function parseWhere(modelName, where, params, dialect, prefix = '', ctx) {
|
|
|
195
195
|
}
|
|
196
196
|
}
|
|
197
197
|
else {
|
|
198
|
-
params
|
|
199
|
-
conditions.push(`${col} = @${
|
|
198
|
+
const p = bindParam(params, pname, value);
|
|
199
|
+
conditions.push(`${col} = @${p}`);
|
|
200
200
|
}
|
|
201
201
|
}
|
|
202
202
|
}
|
|
@@ -220,16 +220,16 @@ function buildRelationClause(relation, value, modelName, params, dialect, prefix
|
|
|
220
220
|
const parts = [];
|
|
221
221
|
if (isMany) {
|
|
222
222
|
if (value.some !== undefined) {
|
|
223
|
-
const inner = buildInnerWhere(relation.modelName, value.some === null ? {} : value.some, params, dialect, prefix
|
|
223
|
+
const inner = buildInnerWhere(relation.modelName, value.some === null ? {} : value.some, params, dialect, `${prefix}some_`, ctx, childAlias);
|
|
224
224
|
parts.push(buildExists(inner));
|
|
225
225
|
}
|
|
226
226
|
if (value.none !== undefined) {
|
|
227
|
-
const inner = buildInnerWhere(relation.modelName, value.none === null ? {} : value.none, params, dialect, prefix
|
|
227
|
+
const inner = buildInnerWhere(relation.modelName, value.none === null ? {} : value.none, params, dialect, `${prefix}none_`, ctx, childAlias);
|
|
228
228
|
parts.push(`NOT ${buildExists(inner)}`);
|
|
229
229
|
}
|
|
230
230
|
if (value.every !== undefined) {
|
|
231
231
|
// "every" = no related row violates the condition.
|
|
232
|
-
const inner = buildInnerWhere(relation.modelName, value.every === null ? {} : value.every, params, dialect, prefix
|
|
232
|
+
const inner = buildInnerWhere(relation.modelName, value.every === null ? {} : value.every, params, dialect, `${prefix}every_`, ctx, childAlias);
|
|
233
233
|
const negated = inner ? `NOT (${inner})` : '';
|
|
234
234
|
const violated = `EXISTS (SELECT 1 FROM ${childTable} AS ${childAlias} WHERE ${correlation}${negated ? ` AND ${negated}` : ' AND 1=0'})`;
|
|
235
235
|
parts.push(`NOT ${violated}`);
|
|
@@ -237,12 +237,12 @@ function buildRelationClause(relation, value, modelName, params, dialect, prefix
|
|
|
237
237
|
}
|
|
238
238
|
else {
|
|
239
239
|
if (value.is !== undefined) {
|
|
240
|
-
const inner = buildInnerWhere(relation.modelName, value.is === null ? {} : value.is, params, dialect, prefix
|
|
241
|
-
parts.push(buildExists(inner));
|
|
240
|
+
const inner = buildInnerWhere(relation.modelName, value.is === null ? {} : value.is, params, dialect, `${prefix}is_`, ctx, childAlias);
|
|
241
|
+
parts.push(value.is === null ? `NOT ${buildExists(inner)}` : buildExists(inner));
|
|
242
242
|
}
|
|
243
243
|
if (value.isNot !== undefined) {
|
|
244
|
-
const inner = buildInnerWhere(relation.modelName, value.isNot === null ? {} : value.isNot, params, dialect, prefix
|
|
245
|
-
parts.push(`NOT ${buildExists(inner)}`);
|
|
244
|
+
const inner = buildInnerWhere(relation.modelName, value.isNot === null ? {} : value.isNot, params, dialect, `${prefix}isNot_`, ctx, childAlias);
|
|
245
|
+
parts.push(value.isNot === null ? buildExists(inner) : `NOT ${buildExists(inner)}`);
|
|
246
246
|
}
|
|
247
247
|
}
|
|
248
248
|
return parts.length > 0 ? parts.join(' AND ') : null;
|
|
@@ -3,27 +3,31 @@ Object.defineProperty(exports, "__esModule", { value: true });
|
|
|
3
3
|
exports.An5SheetsAdapter = void 0;
|
|
4
4
|
exports.createAn5SheetsAdapter = createAn5SheetsAdapter;
|
|
5
5
|
const config_1 = require("./config");
|
|
6
|
+
const oauth_1 = require("./oauth");
|
|
6
7
|
const sqlExecutor_1 = require("./sqlExecutor");
|
|
7
8
|
const tableClient_1 = require("./tableClient");
|
|
8
9
|
const retry_1 = require("./retry");
|
|
9
10
|
const metadata_1 = require("../base/metadata");
|
|
10
11
|
// ─── Fetch-based API proxy for OAuth Access Token / API Key (browser-compatible) ──
|
|
11
|
-
function createFetchApi(spreadsheetId, accessToken, apiKey) {
|
|
12
|
+
function createFetchApi(spreadsheetId, accessToken, apiKey, oauth = { spreadsheetId }) {
|
|
12
13
|
const base = `https://sheets.googleapis.com/v4/spreadsheets/${spreadsheetId}`;
|
|
13
14
|
const headers = { 'Content-Type': 'application/json' };
|
|
14
|
-
|
|
15
|
-
headers['Authorization'] = `Bearer ${accessToken}`;
|
|
16
|
-
}
|
|
15
|
+
const tokenProvider = (0, oauth_1.sheetsTokenProvider)({ ...oauth, ...(accessToken ? { accessToken } : {}) });
|
|
17
16
|
async function request(method, path, body) {
|
|
18
17
|
let url = path.startsWith('http') ? path : `${base}${path}`;
|
|
19
18
|
if (apiKey) {
|
|
20
19
|
const sep = url.includes('?') ? '&' : '?';
|
|
21
20
|
url += `${sep}key=${encodeURIComponent(apiKey)}`;
|
|
22
21
|
}
|
|
23
|
-
const
|
|
22
|
+
const token = await tokenProvider();
|
|
23
|
+
const opts = { method, headers: { ...headers, ...(token ? { Authorization: `Bearer ${token}` } : {}) } };
|
|
24
24
|
if (body)
|
|
25
25
|
opts.body = JSON.stringify(body);
|
|
26
|
-
|
|
26
|
+
let res = await fetch(url, opts);
|
|
27
|
+
if (res.status === 401 && oauth.refreshToken) {
|
|
28
|
+
const refreshed = await tokenProvider(true);
|
|
29
|
+
res = await fetch(url, { ...opts, headers: { ...headers, Authorization: `Bearer ${refreshed}` } });
|
|
30
|
+
}
|
|
27
31
|
if (!res.ok) {
|
|
28
32
|
const text = await res.text().catch(() => '');
|
|
29
33
|
const err = new Error(`Google Sheets API error (${res.status}): ${text}`);
|
|
@@ -79,17 +83,21 @@ class An5SheetsAdapter {
|
|
|
79
83
|
this.sheets = null;
|
|
80
84
|
this.fetchApi = null;
|
|
81
85
|
this.sheetsCache = null;
|
|
86
|
+
const browser = typeof window !== 'undefined' || (typeof self !== 'undefined' && typeof process === 'undefined');
|
|
87
|
+
if (browser && (config.refreshToken || config.oauthClientSecret || config.privateKey || config.credentials)) {
|
|
88
|
+
throw new Error('Browser Google Sheets connections require a user access token. Keep desktop OAuth refresh tokens and service account keys outside the browser.');
|
|
89
|
+
}
|
|
82
90
|
this.config = (0, config_1.resolveConfig)(config);
|
|
83
91
|
// Dynamic model access (`db.user` / `db.User`), same as An5Adapter.
|
|
84
92
|
return (0, metadata_1.createAdapterProxy)(this, (name) => this.table(name));
|
|
85
93
|
}
|
|
86
94
|
get isFetchMode() {
|
|
87
|
-
return !!this.config.accessToken || !!this.config.apiKey;
|
|
95
|
+
return !!this.config.accessToken || !!this.config.refreshToken || !!this.config.apiKey;
|
|
88
96
|
}
|
|
89
97
|
async getSheets() {
|
|
90
98
|
if (this.isFetchMode) {
|
|
91
99
|
if (!this.fetchApi) {
|
|
92
|
-
this.fetchApi = createFetchApi(this.config.spreadsheetId, this.config.accessToken, this.config.apiKey);
|
|
100
|
+
this.fetchApi = createFetchApi(this.config.spreadsheetId, this.config.accessToken, this.config.apiKey, this.config);
|
|
93
101
|
}
|
|
94
102
|
return this.fetchApi;
|
|
95
103
|
}
|
|
@@ -11,6 +11,11 @@ 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
|
+
/** Offline desktop OAuth credentials. Keep these in secure storage, never source control. */
|
|
15
|
+
refreshToken?: string;
|
|
16
|
+
oauthClientId?: string;
|
|
17
|
+
oauthClientSecret?: string;
|
|
18
|
+
tokenExpiresAt?: number;
|
|
14
19
|
/** Google API key for browser-based API access. */
|
|
15
20
|
apiKey?: string;
|
|
16
21
|
}
|
|
@@ -18,12 +23,20 @@ export declare function normalizeKey(key: string): string;
|
|
|
18
23
|
export declare function resolveConfig(config: An5SheetsAdapterConfig): {
|
|
19
24
|
spreadsheetId: string;
|
|
20
25
|
accessToken: string | undefined;
|
|
26
|
+
refreshToken: string | undefined;
|
|
27
|
+
oauthClientId: string | undefined;
|
|
28
|
+
oauthClientSecret: string | undefined;
|
|
29
|
+
tokenExpiresAt: number | undefined;
|
|
21
30
|
apiKey: string | undefined;
|
|
22
31
|
sheetMapping: Record<string, string> | undefined;
|
|
23
32
|
clientEmail?: never;
|
|
24
33
|
privateKey?: never;
|
|
25
34
|
} | {
|
|
26
35
|
accessToken?: never;
|
|
36
|
+
refreshToken?: never;
|
|
37
|
+
oauthClientId?: never;
|
|
38
|
+
oauthClientSecret?: never;
|
|
39
|
+
tokenExpiresAt?: never;
|
|
27
40
|
apiKey?: never;
|
|
28
41
|
spreadsheetId: string;
|
|
29
42
|
clientEmail: string;
|
|
@@ -12,10 +12,14 @@ 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 || config.apiKey) {
|
|
15
|
+
if (config.accessToken || config.refreshToken || config.apiKey) {
|
|
16
16
|
return {
|
|
17
17
|
spreadsheetId: config.spreadsheetId,
|
|
18
18
|
accessToken: config.accessToken,
|
|
19
|
+
refreshToken: config.refreshToken,
|
|
20
|
+
oauthClientId: config.oauthClientId,
|
|
21
|
+
oauthClientSecret: config.oauthClientSecret,
|
|
22
|
+
tokenExpiresAt: config.tokenExpiresAt,
|
|
19
23
|
apiKey: config.apiKey,
|
|
20
24
|
sheetMapping: config.sheetMapping,
|
|
21
25
|
};
|
|
@@ -25,9 +25,7 @@ function matchWhere(row, where) {
|
|
|
25
25
|
if (key.includes('_') &&
|
|
26
26
|
value && typeof value === 'object' &&
|
|
27
27
|
!(value instanceof Date) &&
|
|
28
|
-
!
|
|
29
|
-
!value.not && !value.gte &&
|
|
30
|
-
!value.lte && !value.gt && !value.lt) {
|
|
28
|
+
!['equals', 'in', 'notIn', 'contains', 'startsWith', 'endsWith', 'not', 'gte', 'lte', 'gt', 'lt'].some(op => op in value)) {
|
|
31
29
|
Object.assign(cleanWhere, value);
|
|
32
30
|
}
|
|
33
31
|
else {
|
|
@@ -35,13 +33,16 @@ function matchWhere(row, where) {
|
|
|
35
33
|
}
|
|
36
34
|
}
|
|
37
35
|
for (const [key, value] of Object.entries(cleanWhere)) {
|
|
36
|
+
if (value === undefined)
|
|
37
|
+
continue;
|
|
38
38
|
if (key === 'OR' && Array.isArray(value)) {
|
|
39
39
|
if (!value.some((v) => matchWhere(row, v)))
|
|
40
40
|
return false;
|
|
41
41
|
continue;
|
|
42
42
|
}
|
|
43
|
-
if (key === 'AND'
|
|
44
|
-
|
|
43
|
+
if (key === 'AND') {
|
|
44
|
+
const clauses = Array.isArray(value) ? value : [value];
|
|
45
|
+
if (!clauses.every((v) => matchWhere(row, v)))
|
|
45
46
|
return false;
|
|
46
47
|
continue;
|
|
47
48
|
}
|
|
@@ -65,21 +66,25 @@ function matchWhere(row, where) {
|
|
|
65
66
|
if (cellVal === null || cellVal === undefined)
|
|
66
67
|
return false;
|
|
67
68
|
}
|
|
69
|
+
else if (v.not && typeof v.not === 'object' && !(v.not instanceof Date) && !Array.isArray(v.not)) {
|
|
70
|
+
if (matchWhere(row, { [key]: v.not }))
|
|
71
|
+
return false;
|
|
72
|
+
}
|
|
68
73
|
else if (normalize(cellVal) == normalize(v.not))
|
|
69
74
|
return false;
|
|
70
75
|
}
|
|
71
76
|
if (v.equals !== undefined && normalize(cellVal) != normalize(v.equals))
|
|
72
77
|
return false;
|
|
73
78
|
if (v.contains !== undefined) {
|
|
74
|
-
if (
|
|
79
|
+
if (cellVal === null || cellVal === undefined || !normalize(String(cellVal)).includes(normalize(v.contains)))
|
|
75
80
|
return false;
|
|
76
81
|
}
|
|
77
82
|
if (v.startsWith !== undefined) {
|
|
78
|
-
if (
|
|
83
|
+
if (cellVal === null || cellVal === undefined || !normalize(String(cellVal)).startsWith(normalize(v.startsWith)))
|
|
79
84
|
return false;
|
|
80
85
|
}
|
|
81
86
|
if (v.endsWith !== undefined) {
|
|
82
|
-
if (
|
|
87
|
+
if (cellVal === null || cellVal === undefined || !normalize(String(cellVal)).endsWith(normalize(v.endsWith)))
|
|
83
88
|
return false;
|
|
84
89
|
}
|
|
85
90
|
if (v.gte !== undefined && !(Number(cellVal) >= Number(v.gte)))
|