@an5/adapters 0.2.10 → 0.2.13
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 +163 -2
- package/dist/an5Adapter.d.ts +37 -0
- package/dist/an5Adapter.js +190 -65
- 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 +24 -25
- package/dist/base/types.d.ts +14 -0
- 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/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/index.d.ts +2 -0
- package/dist/index.js +8 -1
- package/dist/sqlite/browserEngine.d.ts +13 -0
- package/dist/sqlite/browserEngine.js +13 -0
- package/dist/sqlite/engine.d.ts +5 -0
- package/dist/sqlite/engine.js +10 -0
- package/dist/sqlite/index.d.ts +1 -0
- package/dist/sqlite/index.js +1 -0
- package/dist/sqlite/vector.d.ts +155 -0
- package/dist/sqlite/vector.js +436 -0
- package/dotnet/An5Adapter.cs +85 -27
- package/dotnet/Base/Core.cs +14 -0
- package/dotnet/Mssql/MssqlEngine.cs +44 -1
- package/dotnet/Postgres/PostgresEngine.cs +2 -0
- package/dotnet/Sqlite/SqliteEngine.cs +82 -2
- package/dotnet/Sqlite/SqliteVectors.cs +401 -0
- package/golang/an5_adapter.go +84 -32
- package/golang/base/vectors.go +263 -0
- package/golang/sqlite/vector.go +248 -0
- package/golang/sqlite_vector_test.go +288 -0
- package/golang/table_client.go +67 -3
- package/java/pom.xml +177 -0
- package/java/src/main/java/an5/adapters/An5Adapter.java +374 -0
- package/java/src/main/java/an5/adapters/An5Aggregate.java +116 -0
- package/java/src/main/java/an5/adapters/An5GroupBy.java +97 -0
- package/java/src/main/java/an5/adapters/An5Query.java +100 -0
- package/java/src/main/java/an5/adapters/An5TableClient.java +1031 -0
- package/java/src/main/java/an5/adapters/An5ViewClient.java +107 -0
- package/java/src/main/java/an5/adapters/base/Dialect.java +103 -0
- package/java/src/main/java/an5/adapters/base/FieldMeta.java +42 -0
- package/java/src/main/java/an5/adapters/base/JdbcUrls.java +201 -0
- package/java/src/main/java/an5/adapters/base/Metadata.java +195 -0
- package/java/src/main/java/an5/adapters/base/RelationDef.java +26 -0
- package/java/src/main/java/an5/adapters/base/SqlBuilder.java +422 -0
- package/java/src/main/java/an5/adapters/base/SqliteVectors.java +375 -0
- package/java/src/main/java/an5/adapters/base/Vectors.java +123 -0
- package/java/src/test/java/An5AdaptersSmoke.java +733 -0
- package/java/src/test/java/Json.java +174 -0
- package/java/src/test/java/QuerySemantics.java +111 -0
- package/kotlin/build.gradle.kts +145 -0
- package/kotlin/settings.gradle.kts +10 -0
- package/kotlin/src/main/kotlin/an5/adapters/Aggregate.kt +89 -0
- package/kotlin/src/main/kotlin/an5/adapters/An5.kt +91 -0
- package/kotlin/src/main/kotlin/an5/adapters/Query.kt +130 -0
- package/kotlin/src/main/kotlin/an5/adapters/Rows.kt +166 -0
- package/kotlin/src/main/kotlin/an5/adapters/TableClient.kt +169 -0
- package/kotlin/src/main/kotlin/an5/adapters/Types.kt +77 -0
- package/kotlin/src/test/kotlin/an5/adapters/An5Smoke.kt +329 -0
- package/native/sqlite/CMakeLists.txt +12 -0
- package/native/sqlite/README.md +117 -0
- package/native/sqlite/an5_vector.c +209 -0
- package/package.json +40 -9
- package/python/an5_adapter.py +37 -6
- package/python/base/__init__.py +15 -0
- package/python/base/vectors.py +434 -0
- package/python/sqlite/__init__.py +2 -2
- package/python/sqlite/provider.py +21 -1
- package/python/table_client.py +129 -59
- package/rust/src/adapter.rs +342 -24
- package/rust/src/base/mod.rs +7 -0
- package/rust/src/base/vectors.rs +293 -0
- package/rust/src/lib.rs +3 -3
- package/scripts/sqlite-native-build.js +32 -0
- package/swift/Package.swift +22 -0
- package/swift/Sources/An5Adapters/An5Adapter.swift +160 -0
- package/swift/Sources/An5Adapters/Metadata.swift +183 -0
- package/swift/Sources/An5Adapters/Query.swift +105 -0
- package/swift/Sources/An5Adapters/Row+Accessors.swift +129 -0
- package/swift/Sources/An5Adapters/SQLBuilder.swift +359 -0
- package/swift/Sources/An5Adapters/SQLiteDriver.swift +301 -0
- package/swift/Sources/An5Adapters/SqliteVectors.swift +243 -0
- package/swift/Sources/An5Adapters/TableClient.swift +671 -0
- package/swift/Sources/An5Adapters/Types.swift +127 -0
- package/swift/Sources/An5Adapters/Vectors.swift +86 -0
- package/swift/Sources/An5Adapters/ViewClient.swift +50 -0
- package/swift/Sources/CSQLite/module.modulemap +8 -0
- package/swift/Sources/CSQLite/shim.h +9 -0
- package/swift/Tests/An5AdaptersTests/QuerySemanticsTests.swift +51 -0
- package/swift/Tests/An5AdaptersTests/RuntimeSmokeTests.swift +488 -0
- package/typescript/src/an5Adapter.ts +200 -58
- package/typescript/src/base/metadata.ts +8 -0
- package/typescript/src/base/sql.ts +23 -16
- package/typescript/src/base/types.ts +15 -0
- package/typescript/src/googlesheets/adapter.ts +17 -8
- package/typescript/src/googlesheets/config.ts +10 -1
- 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/index.ts +13 -0
- package/typescript/src/sqlite/browserEngine.ts +24 -0
- package/typescript/src/sqlite/engine.ts +13 -0
- package/typescript/src/sqlite/index.ts +1 -1
- package/typescript/src/sqlite/vector.ts +541 -0
- package/golang/sqlite/sqlite.go +0 -14
|
@@ -26,10 +26,20 @@ import {
|
|
|
26
26
|
type NBaseAdapterConfig,
|
|
27
27
|
type NBaseVectorClient,
|
|
28
28
|
} from './nbase';
|
|
29
|
-
import { buildOrderBy, parseWhere, quote } from './base/sql';
|
|
29
|
+
import { bindParam, buildOrderBy, parseWhere, quote } from './base/sql';
|
|
30
|
+
import {
|
|
31
|
+
decodeVector,
|
|
32
|
+
isVectorField,
|
|
33
|
+
needsVectorEncoding,
|
|
34
|
+
runSqliteVectorSearch,
|
|
35
|
+
vectorDistance,
|
|
36
|
+
type SqliteVectorSupport,
|
|
37
|
+
type VectorStrategyPreference,
|
|
38
|
+
} from './sqlite/vector';
|
|
30
39
|
import {
|
|
31
40
|
createAdapterProxy,
|
|
32
41
|
getFieldsForModel,
|
|
42
|
+
resolveIdField,
|
|
33
43
|
getModelToTable,
|
|
34
44
|
getRelationMap,
|
|
35
45
|
getRelationsForModel,
|
|
@@ -51,39 +61,59 @@ function sanitizeParamName(name: string): string {
|
|
|
51
61
|
return /^[A-Za-z]/.test(cleaned) ? cleaned : `p_${cleaned}`;
|
|
52
62
|
}
|
|
53
63
|
|
|
64
|
+
/** Match only unique/primary-key violations, never general constraint errors. */
|
|
65
|
+
function isDuplicateKeyError(error: unknown, dialect: Dialect): boolean {
|
|
66
|
+
if (!error || typeof error !== 'object') return false;
|
|
67
|
+
const e = error as any;
|
|
68
|
+
if (dialect === 'postgres') return e.code === '23505';
|
|
69
|
+
if (dialect === 'mysql') return e.code === 'ER_DUP_ENTRY' || e.errno === 1062;
|
|
70
|
+
if (dialect === 'sqlite') {
|
|
71
|
+
return e.code === 'SQLITE_CONSTRAINT_UNIQUE' || e.code === 'SQLITE_CONSTRAINT_PRIMARYKEY'
|
|
72
|
+
|| e.errcode === 2067 || e.errcode === 1555;
|
|
73
|
+
}
|
|
74
|
+
if (dialect === 'mssql') {
|
|
75
|
+
const number = e.number ?? e.originalError?.info?.number;
|
|
76
|
+
return number === 2601 || number === 2627;
|
|
77
|
+
}
|
|
78
|
+
return false;
|
|
79
|
+
}
|
|
80
|
+
|
|
54
81
|
function appendUpdateSet(sets: string[], params: Record<string, any>, col: string, val: any, dialect: Dialect): void {
|
|
55
82
|
const quoted = quote(col, dialect);
|
|
56
83
|
const safeCol = sanitizeParamName(col);
|
|
57
|
-
|
|
84
|
+
// `TypedArray.prototype.set` is the copy method, not the update operator, so a
|
|
85
|
+
// vector written as a float32 BLOB must not be read as `{ set: ... }`.
|
|
86
|
+
const isLiteral = Array.isArray(val) || ArrayBuffer.isView(val) || val instanceof ArrayBuffer;
|
|
87
|
+
if (val && typeof val === 'object' && !(val instanceof Date) && !isLiteral) {
|
|
58
88
|
if (val.increment !== undefined) {
|
|
59
|
-
|
|
60
|
-
|
|
89
|
+
const p = bindParam(params, `s_${safeCol}_inc`, val.increment);
|
|
90
|
+
sets.push(`${quoted} = ${quoted} + @${p}`);
|
|
61
91
|
return;
|
|
62
92
|
}
|
|
63
93
|
if (val.decrement !== undefined) {
|
|
64
|
-
|
|
65
|
-
|
|
94
|
+
const p = bindParam(params, `s_${safeCol}_dec`, val.decrement);
|
|
95
|
+
sets.push(`${quoted} = ${quoted} - @${p}`);
|
|
66
96
|
return;
|
|
67
97
|
}
|
|
68
98
|
if (val.multiply !== undefined) {
|
|
69
|
-
|
|
70
|
-
|
|
99
|
+
const p = bindParam(params, `s_${safeCol}_mul`, val.multiply);
|
|
100
|
+
sets.push(`${quoted} = ${quoted} * @${p}`);
|
|
71
101
|
return;
|
|
72
102
|
}
|
|
73
103
|
if (val.divide !== undefined) {
|
|
74
|
-
|
|
75
|
-
|
|
104
|
+
const p = bindParam(params, `s_${safeCol}_div`, val.divide);
|
|
105
|
+
sets.push(`${quoted} = ${quoted} / @${p}`);
|
|
76
106
|
return;
|
|
77
107
|
}
|
|
78
108
|
if (val.set !== undefined) {
|
|
79
|
-
|
|
80
|
-
|
|
109
|
+
const p = bindParam(params, `s_${safeCol}_set`, val.set);
|
|
110
|
+
sets.push(`${quoted} = @${p}`);
|
|
81
111
|
return;
|
|
82
112
|
}
|
|
83
113
|
}
|
|
84
114
|
|
|
85
|
-
|
|
86
|
-
|
|
115
|
+
const p = bindParam(params, `s_${safeCol}`, val);
|
|
116
|
+
sets.push(`${quoted} = @${p}`);
|
|
87
117
|
}
|
|
88
118
|
|
|
89
119
|
function selectedAggregateFields(fields: any): string[] {
|
|
@@ -368,6 +398,22 @@ export class An5Adapter {
|
|
|
368
398
|
return (await this.requireEngine()).exec<T>(query, params);
|
|
369
399
|
}
|
|
370
400
|
|
|
401
|
+
/**
|
|
402
|
+
* The SQLite engine's vector hooks, or null when this is not a SQLite adapter.
|
|
403
|
+
* `vectorSearch` needs them to load sqlite-vec and register the distance
|
|
404
|
+
* functions; a connection with no hooks still searches in memory.
|
|
405
|
+
*/
|
|
406
|
+
async sqliteVectorSupport(): Promise<SqliteVectorSupport | null> {
|
|
407
|
+
if (this.sheetsAdapter) return null;
|
|
408
|
+
const engine = await this.requireEngine();
|
|
409
|
+
return engine.dialect === 'sqlite' ? (engine.vectorSupport ?? null) : null;
|
|
410
|
+
}
|
|
411
|
+
|
|
412
|
+
/** `vectorStrategy` from the config, so a project can pin one strategy. */
|
|
413
|
+
get vectorStrategy(): VectorStrategyPreference | undefined {
|
|
414
|
+
return this._engineConfig?.vectorStrategy;
|
|
415
|
+
}
|
|
416
|
+
|
|
371
417
|
/** INTERNAL: used by AdapterTableClient for DML statements needing row count */
|
|
372
418
|
async _executeRaw(query: string, params?: Record<string, any>): Promise<number> {
|
|
373
419
|
return (await this.requireEngine()).executeRaw(query, params);
|
|
@@ -714,6 +760,14 @@ export class AdapterTableClient<T = any> {
|
|
|
714
760
|
public get dialect(): Dialect { return this.adapter.dialect; }
|
|
715
761
|
|
|
716
762
|
public get tableName(): string {
|
|
763
|
+
const t = this.mappedTableName;
|
|
764
|
+
if (t.startsWith('[') || t.startsWith('"') || t.startsWith('`')) return t;
|
|
765
|
+
if (t.includes('.')) return t.split('.').map(p => quote(p, this.dialect)).join('.');
|
|
766
|
+
return quote(t, this.dialect);
|
|
767
|
+
}
|
|
768
|
+
|
|
769
|
+
/** The table name this model maps to, before any quoting is applied. */
|
|
770
|
+
private get mappedTableName(): string {
|
|
717
771
|
const name = this.modelName;
|
|
718
772
|
let t = name;
|
|
719
773
|
const modelToTable = getModelToTable();
|
|
@@ -726,9 +780,17 @@ export class AdapterTableClient<T = any> {
|
|
|
726
780
|
if (modelToTable[lower]) t = modelToTable[lower];
|
|
727
781
|
}
|
|
728
782
|
}
|
|
729
|
-
|
|
730
|
-
|
|
731
|
-
|
|
783
|
+
return t;
|
|
784
|
+
}
|
|
785
|
+
|
|
786
|
+
/**
|
|
787
|
+
* The bare table name, for helpers that take a string literal. `@@map` values
|
|
788
|
+
* may be pre-quoted or schema-qualified, neither of which `pragma_table_info`
|
|
789
|
+
* accepts, so the last segment is unwrapped here.
|
|
790
|
+
*/
|
|
791
|
+
public get rawTableName(): string {
|
|
792
|
+
const last = this.mappedTableName.split('.').pop() ?? this.mappedTableName;
|
|
793
|
+
return last.replace(/^[\s]*[\[\]"`]+|[\[\]"`]+[\s]*$/g, '');
|
|
732
794
|
}
|
|
733
795
|
|
|
734
796
|
private get nolock(): string {
|
|
@@ -752,6 +814,53 @@ export class AdapterTableClient<T = any> {
|
|
|
752
814
|
return this.adapter._executeRaw(query, params);
|
|
753
815
|
}
|
|
754
816
|
|
|
817
|
+
/**
|
|
818
|
+
* Encode every `VECTOR(n)` value on its way into SQLite.
|
|
819
|
+
*
|
|
820
|
+
* The column holds a float32 BLOB, which is a third of the size of the JSON
|
|
821
|
+
* text it replaces and is what sqlite-vec reads. Values that already arrived
|
|
822
|
+
* as text or bytes are left alone, so rows written by an older version stay
|
|
823
|
+
* readable.
|
|
824
|
+
*/
|
|
825
|
+
private encodeVectorFields(data: Record<string, any>): Record<string, any> {
|
|
826
|
+
if (this.dialect !== 'sqlite') return data;
|
|
827
|
+
const fields = getFieldsForModel(this.modelName);
|
|
828
|
+
const vectorFields = Object.keys(fields).filter((name) => isVectorField(fields[name]));
|
|
829
|
+
if (vectorFields.length === 0) return data;
|
|
830
|
+
const { encodeVector } = require('./sqlite/vector');
|
|
831
|
+
const out = { ...data };
|
|
832
|
+
for (const name of vectorFields) {
|
|
833
|
+
const value = out[name];
|
|
834
|
+
if (needsVectorEncoding(value)) {
|
|
835
|
+
const parsed = Array.from(value as ArrayLike<number>);
|
|
836
|
+
if (parsed.length > 0) out[name] = encodeVector(parsed);
|
|
837
|
+
}
|
|
838
|
+
}
|
|
839
|
+
return out;
|
|
840
|
+
}
|
|
841
|
+
|
|
842
|
+
/** Decode `VECTOR(n)` columns back into `number[]` after a read. */
|
|
843
|
+
private decodeVectorFields<T>(rows: T[]): T[] {
|
|
844
|
+
if (this.dialect !== 'sqlite' || !Array.isArray(rows) || rows.length === 0) return rows;
|
|
845
|
+
const fields = getFieldsForModel(this.modelName);
|
|
846
|
+
const vectorFields = Object.keys(fields).filter((name) => isVectorField(fields[name]));
|
|
847
|
+
if (vectorFields.length === 0) return rows;
|
|
848
|
+
return rows.map((row: any) => {
|
|
849
|
+
if (!row || typeof row !== 'object') return row;
|
|
850
|
+
let changed = false;
|
|
851
|
+
const out: any = { ...row };
|
|
852
|
+
for (const name of vectorFields) {
|
|
853
|
+
if (!(name in out)) continue;
|
|
854
|
+
const decoded = decodeVector(out[name]);
|
|
855
|
+
if (decoded) {
|
|
856
|
+
out[name] = decoded;
|
|
857
|
+
changed = true;
|
|
858
|
+
}
|
|
859
|
+
}
|
|
860
|
+
return changed ? out : row;
|
|
861
|
+
});
|
|
862
|
+
}
|
|
863
|
+
|
|
755
864
|
async findMany(args?: { where?: any; orderBy?: any; skip?: number; take?: number; select?: any; include?: any }): Promise<T[]> {
|
|
756
865
|
const params: Record<string, any> = {};
|
|
757
866
|
const whereCtx = this.whereContext();
|
|
@@ -789,7 +898,8 @@ export class AdapterTableClient<T = any> {
|
|
|
789
898
|
query = `SELECT ${cols} FROM ${this.tableName}`;
|
|
790
899
|
if (whereSql) query += ` WHERE ${whereSql}`;
|
|
791
900
|
if (orderSql) query += ` ${orderSql}`;
|
|
792
|
-
|
|
901
|
+
const limit = take ?? (this.dialect === 'sqlite' ? '-1' : this.dialect === 'mysql' ? '18446744073709551615' : 'ALL');
|
|
902
|
+
query += ` LIMIT ${limit} OFFSET ${skip}`;
|
|
793
903
|
} else {
|
|
794
904
|
// mssql requires ORDER BY before OFFSET/FETCH
|
|
795
905
|
query = `SELECT ${cols} FROM ${this.tableName}${this.nolock}`;
|
|
@@ -806,7 +916,7 @@ export class AdapterTableClient<T = any> {
|
|
|
806
916
|
if (orderSql) query += ` ${orderSql}`;
|
|
807
917
|
}
|
|
808
918
|
|
|
809
|
-
const rows = await this.doExec<T>(query, params);
|
|
919
|
+
const rows = this.decodeVectorFields(await this.doExec<T>(query, params));
|
|
810
920
|
|
|
811
921
|
if (args?.include) {
|
|
812
922
|
await resolveIncludes(this.modelName, rows, args.include, this.adapter);
|
|
@@ -854,11 +964,9 @@ export class AdapterTableClient<T = any> {
|
|
|
854
964
|
}
|
|
855
965
|
|
|
856
966
|
const fields = getFieldsForModel(this.modelName);
|
|
857
|
-
const idFieldName =
|
|
858
|
-
? 'id'
|
|
859
|
-
: Object.keys(fields).find((name) => name.endsWith('_id') || name.endsWith('Id') || name.toLowerCase() === 'id');
|
|
967
|
+
const idFieldName = resolveIdField(fields);
|
|
860
968
|
|
|
861
|
-
const data: any =
|
|
969
|
+
const data: any = this.encodeVectorFields(rawData);
|
|
862
970
|
if (idFieldName) {
|
|
863
971
|
const fieldDef: any = fields[idFieldName];
|
|
864
972
|
const rawType = typeof fieldDef === 'string' ? fieldDef : (fieldDef?.ts || fieldDef?.sql || fieldDef?.type || '');
|
|
@@ -918,35 +1026,44 @@ export class AdapterTableClient<T = any> {
|
|
|
918
1026
|
if (firstRow === undefined) return { count: 0 };
|
|
919
1027
|
const firstCols = Object.keys(firstRow).filter(k => (firstRow as any)[k] !== undefined);
|
|
920
1028
|
|
|
921
|
-
|
|
922
|
-
|
|
1029
|
+
const sameColumns = args.data.every(row => {
|
|
1030
|
+
const cols = Object.keys(row).filter(k => (row as any)[k] !== undefined);
|
|
1031
|
+
return cols.length === firstCols.length && firstCols.every(col => cols.includes(col));
|
|
1032
|
+
});
|
|
1033
|
+
|
|
1034
|
+
const fields = getFieldsForModel(this.modelName);
|
|
1035
|
+
const idFieldName = resolveIdField(fields);
|
|
1036
|
+
const idDef = idFieldName ? fields[idFieldName] : undefined;
|
|
1037
|
+
const idType = typeof idDef === 'string' ? idDef : (idDef?.ts || idDef?.sql || idDef?.type || '');
|
|
1038
|
+
const generatesId = ['string', 'uuid', 'uniqueidentifier', 'nvarchar', 'varchar', 'text'].includes(String(idType).toLowerCase());
|
|
1039
|
+
const needsGeneratedId = idFieldName && generatesId && args.data.some(row => !(row as any)[idFieldName]);
|
|
1040
|
+
|
|
1041
|
+
// Rows needing generated IDs follow create's default handling.
|
|
1042
|
+
if (firstCols.length > 0 && sameColumns && !needsGeneratedId && !args.skipDuplicates) {
|
|
923
1043
|
const params: Record<string, any> = {};
|
|
924
1044
|
const rowPlaceholders: string[] = [];
|
|
925
1045
|
|
|
926
1046
|
for (let r = 0; r < args.data.length; r++) {
|
|
927
1047
|
const row = args.data[r] as any;
|
|
1048
|
+
const encoded = this.encodeVectorFields(row);
|
|
928
1049
|
const vals = firstCols.map(col => {
|
|
929
1050
|
const p = `r${r}_${col}`;
|
|
930
|
-
params[p] =
|
|
1051
|
+
params[p] = encoded[col] ?? null;
|
|
931
1052
|
return `@${p}`;
|
|
932
1053
|
});
|
|
933
1054
|
rowPlaceholders.push(`(${vals.join(', ')})`);
|
|
934
1055
|
}
|
|
935
1056
|
|
|
936
1057
|
const query = `INSERT INTO ${this.tableName} (${firstCols.map(c => quote(c, this.dialect)).join(', ')}) VALUES ${rowPlaceholders.join(', ')}`;
|
|
937
|
-
|
|
938
|
-
|
|
939
|
-
return { count: args.data.length };
|
|
940
|
-
} catch {
|
|
941
|
-
// fallback below if bulk fails (e.g. mixed column sets)
|
|
942
|
-
}
|
|
1058
|
+
await this.doExecuteRaw(query, params);
|
|
1059
|
+
return { count: args.data.length };
|
|
943
1060
|
}
|
|
944
1061
|
|
|
945
1062
|
// Row-by-row fallback
|
|
946
1063
|
let count = 0;
|
|
947
1064
|
for (const row of args.data) {
|
|
948
1065
|
try { await this.create({ data: row }); count++; }
|
|
949
|
-
catch (e) { if (!args.skipDuplicates) throw e; }
|
|
1066
|
+
catch (e) { if (!args.skipDuplicates || !isDuplicateKeyError(e, this.dialect)) throw e; }
|
|
950
1067
|
}
|
|
951
1068
|
return { count };
|
|
952
1069
|
}
|
|
@@ -963,13 +1080,14 @@ export class AdapterTableClient<T = any> {
|
|
|
963
1080
|
}
|
|
964
1081
|
}
|
|
965
1082
|
|
|
1083
|
+
const data = this.encodeVectorFields(rawData);
|
|
966
1084
|
const setCols = Object.keys(rawData).filter(k => rawData[k] !== undefined);
|
|
967
1085
|
if (setCols.length > 0) {
|
|
968
1086
|
const params: Record<string, any> = {};
|
|
969
1087
|
const whereSql = parseWhere(this.modelName, args.where, params, this.dialect, 'w_', this.whereContext());
|
|
970
1088
|
const sets: string[] = [];
|
|
971
1089
|
for (const col of setCols) {
|
|
972
|
-
appendUpdateSet(sets, params, col,
|
|
1090
|
+
appendUpdateSet(sets, params, col, data[col], this.dialect);
|
|
973
1091
|
}
|
|
974
1092
|
if (sets.length > 0) {
|
|
975
1093
|
const query = `UPDATE ${this.tableName} SET ${sets.join(', ')}${whereSql ? ` WHERE ${whereSql}` : ''}`;
|
|
@@ -1025,11 +1143,12 @@ export class AdapterTableClient<T = any> {
|
|
|
1025
1143
|
async updateMany(args: { where?: any; data: Partial<T> }): Promise<{ count: number }> {
|
|
1026
1144
|
const params: Record<string, any> = {};
|
|
1027
1145
|
const whereSql = parseWhere(this.modelName, args.where, params, this.dialect, 'w_', this.whereContext());
|
|
1146
|
+
const data = this.encodeVectorFields(args.data as Record<string, any>);
|
|
1028
1147
|
const setCols = Object.keys(args.data).filter(k => (args.data as any)[k] !== undefined);
|
|
1029
1148
|
const sets: string[] = [];
|
|
1030
1149
|
|
|
1031
1150
|
for (const col of setCols) {
|
|
1032
|
-
appendUpdateSet(sets, params, col,
|
|
1151
|
+
appendUpdateSet(sets, params, col, data[col], this.dialect);
|
|
1033
1152
|
}
|
|
1034
1153
|
|
|
1035
1154
|
if (sets.length === 0) return { count: 0 };
|
|
@@ -1132,7 +1251,7 @@ export class AdapterTableClient<T = any> {
|
|
|
1132
1251
|
|
|
1133
1252
|
async groupBy(args: any): Promise<any[]> {
|
|
1134
1253
|
const params: Record<string, any> = {};
|
|
1135
|
-
const whereSql = parseWhere(this.modelName, args?.where, params, this.dialect);
|
|
1254
|
+
const whereSql = parseWhere(this.modelName, args?.where, params, this.dialect, '', this.whereContext());
|
|
1136
1255
|
const byFields = normalizeByFields(args?.by);
|
|
1137
1256
|
if (byFields.length === 0) throw new Error("groupBy requires 'by' fields");
|
|
1138
1257
|
const byCols = byFields.map((b: string) => quote(b, this.dialect)).join(', ');
|
|
@@ -1151,8 +1270,8 @@ export class AdapterTableClient<T = any> {
|
|
|
1151
1270
|
const skip = toNonNegativeInt(args?.skip);
|
|
1152
1271
|
const take = toNonNegativeInt(args?.take, 1);
|
|
1153
1272
|
if (this.dialect === 'postgres' || this.dialect === 'mysql' || this.dialect === 'sqlite') {
|
|
1154
|
-
|
|
1155
|
-
query += ` OFFSET ${skip}`;
|
|
1273
|
+
const limit = hasTake ? take : (this.dialect === 'sqlite' ? '-1' : this.dialect === 'mysql' ? '18446744073709551615' : 'ALL');
|
|
1274
|
+
query += ` LIMIT ${limit} OFFSET ${skip}`;
|
|
1156
1275
|
} else {
|
|
1157
1276
|
query += ` OFFSET ${skip} ROWS`;
|
|
1158
1277
|
if (hasTake) query += ` FETCH NEXT ${take} ROWS ONLY`;
|
|
@@ -1385,6 +1504,28 @@ export class AdapterTableClient<T = any> {
|
|
|
1385
1504
|
const msg = String(err?.message || '').toLowerCase();
|
|
1386
1505
|
if (!msg.includes('vector') && !msg.includes('operator does not exist')) throw err;
|
|
1387
1506
|
}
|
|
1507
|
+
} else if (this.dialect === 'sqlite') {
|
|
1508
|
+
// SQLite has no vector type, so the ranking runs through sqlite-vec, a
|
|
1509
|
+
// driver function, or json_each — whichever this connection supports.
|
|
1510
|
+
const whereSql = parseWhere(this.modelName, args.where, params, this.dialect, '', this.whereContext());
|
|
1511
|
+
const support = await (this.adapter as any).sqliteVectorSupport?.() ?? null;
|
|
1512
|
+
const ranked = await runSqliteVectorSearch({
|
|
1513
|
+
exec: (sql, p) => this.doExec<any>(sql, p),
|
|
1514
|
+
vectorSupport: support,
|
|
1515
|
+
table: this.tableName,
|
|
1516
|
+
rawTable: this.rawTableName,
|
|
1517
|
+
column: col,
|
|
1518
|
+
rawColumn: vectorField,
|
|
1519
|
+
vector: args.vector,
|
|
1520
|
+
metric,
|
|
1521
|
+
take,
|
|
1522
|
+
tail: whereSql ? ` WHERE ${whereSql}` : '',
|
|
1523
|
+
params,
|
|
1524
|
+
preference: (this.adapter as any).vectorStrategy,
|
|
1525
|
+
});
|
|
1526
|
+
if (ranked) return this.decodeVectorFields(ranked.rows) as (T & { distance: number })[];
|
|
1527
|
+
// No in-database strategy: score the column in the client instead.
|
|
1528
|
+
return this.scoreVectorsInMemory(args, metric, take, vectorField);
|
|
1388
1529
|
} else if (this.dialect === 'mssql') {
|
|
1389
1530
|
const distFn = metric === 'cosine' ? 'cosine' : metric === 'euclidean' ? 'euclidean' : 'dot';
|
|
1390
1531
|
const whereSql = parseWhere(this.modelName, args.where, params, this.dialect, '', this.whereContext());
|
|
@@ -1411,30 +1552,31 @@ export class AdapterTableClient<T = any> {
|
|
|
1411
1552
|
}
|
|
1412
1553
|
|
|
1413
1554
|
// 2. Fallback: in-memory similarity computation when native vector support is unavailable
|
|
1555
|
+
return this.scoreVectorsInMemory(args, metric, take, vectorField);
|
|
1556
|
+
}
|
|
1557
|
+
|
|
1558
|
+
/**
|
|
1559
|
+
* Rank the vector column in the client. This is the last resort for every
|
|
1560
|
+
* provider, and the only option on SQLite connections that offer neither
|
|
1561
|
+
* sqlite-vec, a driver function nor JSON1.
|
|
1562
|
+
*
|
|
1563
|
+
* The whole table is loaded, so it is only reasonable for a table small
|
|
1564
|
+
* enough to fit in memory.
|
|
1565
|
+
*/
|
|
1566
|
+
private async scoreVectorsInMemory(
|
|
1567
|
+
args: { vector: number[]; where?: any },
|
|
1568
|
+
metric: 'cosine' | 'euclidean' | 'dot',
|
|
1569
|
+
take: number,
|
|
1570
|
+
vectorField = 'embedding',
|
|
1571
|
+
): Promise<(T & { distance: number })[]> {
|
|
1414
1572
|
const rows = await this.findMany({ where: args.where });
|
|
1415
1573
|
|
|
1416
1574
|
const scored: { row: T; dist: number }[] = [];
|
|
1417
1575
|
for (const row of rows) {
|
|
1418
|
-
const
|
|
1419
|
-
if (!
|
|
1420
|
-
|
|
1421
|
-
|
|
1422
|
-
if (!Array.isArray(vec) || vec.length !== args.vector.length) continue;
|
|
1423
|
-
|
|
1424
|
-
let dot = 0, m1 = 0, m2 = 0;
|
|
1425
|
-
for (let i = 0; i < args.vector.length; i++) {
|
|
1426
|
-
const av = args.vector[i] ?? 0;
|
|
1427
|
-
const bv = vec[i] ?? 0;
|
|
1428
|
-
dot += av * bv;
|
|
1429
|
-
m1 += av ** 2;
|
|
1430
|
-
m2 += bv ** 2;
|
|
1431
|
-
}
|
|
1432
|
-
const cosine = m1 && m2 ? dot / (Math.sqrt(m1) * Math.sqrt(m2)) : 0;
|
|
1433
|
-
const dist = metric === 'cosine'
|
|
1434
|
-
? 1 - cosine
|
|
1435
|
-
: metric === 'dot'
|
|
1436
|
-
? -dot
|
|
1437
|
-
: Math.sqrt(args.vector.reduce((s, v, i) => s + (v - (vec[i] ?? 0)) ** 2, 0));
|
|
1576
|
+
const vec = decodeVector((row as any)[vectorField]);
|
|
1577
|
+
if (!vec) continue;
|
|
1578
|
+
const dist = vectorDistance(args.vector, vec, metric);
|
|
1579
|
+
if (dist === null) continue;
|
|
1438
1580
|
scored.push({ row, dist });
|
|
1439
1581
|
}
|
|
1440
1582
|
scored.sort((a, b) => a.dist - b.dist);
|
|
@@ -13,6 +13,14 @@ export interface ModelFieldMeta {
|
|
|
13
13
|
[key: string]: any;
|
|
14
14
|
}
|
|
15
15
|
|
|
16
|
+
/** Prefer schema primary-key metadata; retain name inference for legacy clients. */
|
|
17
|
+
export function resolveIdField(fields: Record<string, any>): string | undefined {
|
|
18
|
+
const primaryKey = Object.keys(fields).find(name => fields[name]?.isId === true);
|
|
19
|
+
if (primaryKey) return primaryKey;
|
|
20
|
+
if (Object.prototype.hasOwnProperty.call(fields, 'id')) return 'id';
|
|
21
|
+
return Object.keys(fields).find(name => name.endsWith('_id') || name.endsWith('Id') || name.toLowerCase() === 'id');
|
|
22
|
+
}
|
|
23
|
+
|
|
16
24
|
export interface AdapterMetadata {
|
|
17
25
|
modelToTable?: Record<string, string>;
|
|
18
26
|
modelFields?: Record<string, any>;
|
|
@@ -39,6 +39,15 @@ function sanitizeParamName(name: string): string {
|
|
|
39
39
|
return /^[A-Za-z]/.test(cleaned) ? cleaned : `p_${cleaned}`;
|
|
40
40
|
}
|
|
41
41
|
|
|
42
|
+
/** Allocate parameters without overwriting values from sibling clauses. */
|
|
43
|
+
export function bindParam(params: Record<string, any>, name: string, value: any): string {
|
|
44
|
+
let candidate = name;
|
|
45
|
+
let suffix = 1;
|
|
46
|
+
while (Object.prototype.hasOwnProperty.call(params, candidate)) candidate = `${name}_${suffix++}`;
|
|
47
|
+
params[candidate] = value;
|
|
48
|
+
return candidate;
|
|
49
|
+
}
|
|
50
|
+
|
|
42
51
|
function normalizeSortDirection(dir: unknown): 'ASC' | 'DESC' {
|
|
43
52
|
return typeof dir === 'string' && dir.toUpperCase() === 'DESC' ? 'DESC' : 'ASC';
|
|
44
53
|
}
|
|
@@ -132,26 +141,24 @@ export function parseWhere(
|
|
|
132
141
|
if (v.not !== undefined) {
|
|
133
142
|
if (v.not === null) { conditions.push(`${col} IS NOT NULL`); }
|
|
134
143
|
else if (v.not && typeof v.not === 'object' && !(v.not instanceof Date) && !Array.isArray(v.not)) {
|
|
135
|
-
const
|
|
136
|
-
const nestedSql = parseWhere(modelName, { [key]: v.not }, nestedParams, dialect, `${prefix}${key}_not_`, ctx);
|
|
137
|
-
Object.assign(params, nestedParams);
|
|
144
|
+
const nestedSql = parseWhere(modelName, { [key]: v.not }, params, dialect, `${prefix}${key}_not_`, ctx);
|
|
138
145
|
if (nestedSql) conditions.push(`NOT (${nestedSql})`);
|
|
139
|
-
} else { const p = `${pname}_not
|
|
146
|
+
} else { const p = bindParam(params, `${pname}_not`, v.not); conditions.push(`${col} <> @${p}`); }
|
|
140
147
|
}
|
|
141
148
|
if (v.equals !== undefined) {
|
|
142
149
|
if (v.equals === null) { conditions.push(`${col} IS NULL`); }
|
|
143
|
-
else { const p = `${pname}_eq
|
|
150
|
+
else { const p = bindParam(params, `${pname}_eq`, v.equals); conditions.push(`${col} = @${p}`); }
|
|
144
151
|
}
|
|
145
|
-
if (v.contains !== undefined) { const p = `${pname}_co
|
|
146
|
-
if (v.startsWith !== undefined) { const p = `${pname}_sw
|
|
147
|
-
if (v.endsWith !== undefined) { const p = `${pname}_ew
|
|
148
|
-
if (v.gte !== undefined) { const p = `${pname}_gte
|
|
149
|
-
if (v.lte !== undefined) { const p = `${pname}_lte
|
|
150
|
-
if (v.gt !== undefined) { const p = `${pname}_gt
|
|
151
|
-
if (v.lt !== undefined) { const p = `${pname}_lt
|
|
152
|
+
if (v.contains !== undefined) { const p = bindParam(params, `${pname}_co`, `%${v.contains}%`); conditions.push(`${col} LIKE @${p}`); }
|
|
153
|
+
if (v.startsWith !== undefined) { const p = bindParam(params, `${pname}_sw`, `${v.startsWith}%`); conditions.push(`${col} LIKE @${p}`); }
|
|
154
|
+
if (v.endsWith !== undefined) { const p = bindParam(params, `${pname}_ew`, `%${v.endsWith}`); conditions.push(`${col} LIKE @${p}`); }
|
|
155
|
+
if (v.gte !== undefined) { const p = bindParam(params, `${pname}_gte`, v.gte); conditions.push(`${col} >= @${p}`); }
|
|
156
|
+
if (v.lte !== undefined) { const p = bindParam(params, `${pname}_lte`, v.lte); conditions.push(`${col} <= @${p}`); }
|
|
157
|
+
if (v.gt !== undefined) { const p = bindParam(params, `${pname}_gt`, v.gt); conditions.push(`${col} > @${p}`); }
|
|
158
|
+
if (v.lt !== undefined) { const p = bindParam(params, `${pname}_lt`, v.lt); conditions.push(`${col} < @${p}`); }
|
|
152
159
|
if (v.in !== undefined) {
|
|
153
160
|
if (Array.isArray(v.in) && v.in.length > 0) {
|
|
154
|
-
const ps = v.in.map((x: any, i: number) => { const p = `${pname}_in${i}
|
|
161
|
+
const ps = v.in.map((x: any, i: number) => { const p = bindParam(params, `${pname}_in${i}`, x); return `@${p}`; });
|
|
155
162
|
conditions.push(`${col} IN (${ps.join(', ')})`);
|
|
156
163
|
} else {
|
|
157
164
|
conditions.push('1=0');
|
|
@@ -159,15 +166,15 @@ export function parseWhere(
|
|
|
159
166
|
}
|
|
160
167
|
if (v.notIn !== undefined) {
|
|
161
168
|
if (Array.isArray(v.notIn) && v.notIn.length > 0) {
|
|
162
|
-
const ps = v.notIn.map((x: any, i: number) => { const p = `${pname}_notin${i}
|
|
169
|
+
const ps = v.notIn.map((x: any, i: number) => { const p = bindParam(params, `${pname}_notin${i}`, x); return `@${p}`; });
|
|
163
170
|
conditions.push(`${col} NOT IN (${ps.join(', ')})`);
|
|
164
171
|
} else {
|
|
165
172
|
conditions.push('1=1');
|
|
166
173
|
}
|
|
167
174
|
}
|
|
168
175
|
} else {
|
|
169
|
-
params
|
|
170
|
-
conditions.push(`${col} = @${
|
|
176
|
+
const p = bindParam(params, pname, value);
|
|
177
|
+
conditions.push(`${col} = @${p}`);
|
|
171
178
|
}
|
|
172
179
|
}
|
|
173
180
|
}
|
|
@@ -1,3 +1,5 @@
|
|
|
1
|
+
import type { SqliteVectorSupport, VectorStrategyPreference } from '../sqlite/vector';
|
|
2
|
+
|
|
1
3
|
export type Dialect = 'mssql' | 'postgres' | 'mysql' | 'sqlite' | 'googlesheets';
|
|
2
4
|
|
|
3
5
|
export interface An5AdapterConfig {
|
|
@@ -8,6 +10,17 @@ export interface An5AdapterConfig {
|
|
|
8
10
|
* run in NBase while the rows stay in `connectionString`'s database.
|
|
9
11
|
*/
|
|
10
12
|
nbase?: string;
|
|
13
|
+
/**
|
|
14
|
+
* Path to sqlite-vec or the AN5 native vector extension. On SQLite it is loaded before the
|
|
15
|
+
* vector capabilities are probed, so `vectorSearch` ranks inside the database
|
|
16
|
+
* instead of loading the table into memory. Other providers ignore it.
|
|
17
|
+
*/
|
|
18
|
+
sqliteVec?: string;
|
|
19
|
+
/**
|
|
20
|
+
* Pin the SQLite vector search strategy. `auto` (the default) probes for
|
|
21
|
+
* sqlite-vec, a driver function and JSON1, and uses the first that works.
|
|
22
|
+
*/
|
|
23
|
+
vectorStrategy?: VectorStrategyPreference;
|
|
11
24
|
engine?: QueryEngine;
|
|
12
25
|
db?: any;
|
|
13
26
|
driver?: any;
|
|
@@ -30,4 +43,6 @@ export interface QueryEngine {
|
|
|
30
43
|
connect(): Promise<void>;
|
|
31
44
|
disconnect(): Promise<void>;
|
|
32
45
|
beginTransaction(): Promise<TransactionHandle>;
|
|
46
|
+
/** SQLite only: extension loading and distance-function registration. */
|
|
47
|
+
vectorSupport?: SqliteVectorSupport;
|
|
33
48
|
}
|
|
@@ -1,4 +1,5 @@
|
|
|
1
1
|
import { An5SheetsAdapterConfig, resolveConfig } from './config';
|
|
2
|
+
import { sheetsTokenProvider } from './oauth';
|
|
2
3
|
import type { sheets_v4 } from 'googleapis';
|
|
3
4
|
import { execQuery } from './sqlExecutor';
|
|
4
5
|
import { SheetsTableClient } from './tableClient';
|
|
@@ -8,12 +9,10 @@ import { createAdapterProxy } from '../base/metadata';
|
|
|
8
9
|
|
|
9
10
|
// ─── Fetch-based API proxy for OAuth Access Token / API Key (browser-compatible) ──
|
|
10
11
|
|
|
11
|
-
function createFetchApi(spreadsheetId: string, accessToken?: string, apiKey?: string) {
|
|
12
|
+
function createFetchApi(spreadsheetId: string, accessToken?: string, apiKey?: string, oauth: An5SheetsAdapterConfig = { spreadsheetId }) {
|
|
12
13
|
const base = `https://sheets.googleapis.com/v4/spreadsheets/${spreadsheetId}`;
|
|
13
14
|
const headers: Record<string, string> = { 'Content-Type': 'application/json' };
|
|
14
|
-
|
|
15
|
-
headers['Authorization'] = `Bearer ${accessToken}`;
|
|
16
|
-
}
|
|
15
|
+
const tokenProvider = sheetsTokenProvider({ ...oauth, ...(accessToken ? { accessToken } : {}) });
|
|
17
16
|
|
|
18
17
|
async function request<T>(method: string, path: string, body?: any): Promise<T> {
|
|
19
18
|
let url = path.startsWith('http') ? path : `${base}${path}`;
|
|
@@ -21,9 +20,14 @@ function createFetchApi(spreadsheetId: string, accessToken?: string, apiKey?: st
|
|
|
21
20
|
const sep = url.includes('?') ? '&' : '?';
|
|
22
21
|
url += `${sep}key=${encodeURIComponent(apiKey)}`;
|
|
23
22
|
}
|
|
24
|
-
const
|
|
23
|
+
const token = await tokenProvider();
|
|
24
|
+
const opts: RequestInit = { method, headers: { ...headers, ...(token ? { Authorization: `Bearer ${token}` } : {}) } };
|
|
25
25
|
if (body) 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}`);
|
|
@@ -85,13 +89,17 @@ export class An5SheetsAdapter {
|
|
|
85
89
|
private sheetsCache: Promise<SheetMeta[]> | null = null;
|
|
86
90
|
|
|
87
91
|
constructor(config: An5SheetsAdapterConfig) {
|
|
92
|
+
const browser = typeof window !== 'undefined' || (typeof self !== 'undefined' && typeof process === 'undefined');
|
|
93
|
+
if (browser && (config.refreshToken || config.oauthClientSecret || config.privateKey || config.credentials)) {
|
|
94
|
+
throw new Error('Browser Google Sheets connections require a user access token. Keep desktop OAuth refresh tokens and service account keys outside the browser.');
|
|
95
|
+
}
|
|
88
96
|
this.config = resolveConfig(config);
|
|
89
97
|
// Dynamic model access (`db.user` / `db.User`), same as An5Adapter.
|
|
90
98
|
return createAdapterProxy(this, (name) => this.table(name));
|
|
91
99
|
}
|
|
92
100
|
|
|
93
101
|
private get isFetchMode(): boolean {
|
|
94
|
-
return !!(this.config as any).accessToken || !!(this.config as any).apiKey;
|
|
102
|
+
return !!(this.config as any).accessToken || !!(this.config as any).refreshToken || !!(this.config as any).apiKey;
|
|
95
103
|
}
|
|
96
104
|
|
|
97
105
|
async getSheets(): Promise<any> {
|
|
@@ -100,7 +108,8 @@ export class An5SheetsAdapter {
|
|
|
100
108
|
this.fetchApi = createFetchApi(
|
|
101
109
|
this.config.spreadsheetId,
|
|
102
110
|
(this.config as any).accessToken,
|
|
103
|
-
(this.config as any).apiKey
|
|
111
|
+
(this.config as any).apiKey,
|
|
112
|
+
this.config as An5SheetsAdapterConfig
|
|
104
113
|
);
|
|
105
114
|
}
|
|
106
115
|
return this.fetchApi;
|
|
@@ -10,6 +10,11 @@ export interface An5SheetsAdapterConfig {
|
|
|
10
10
|
* When provided, the adapter uses raw fetch() instead of googleapis JWT, making it compatible
|
|
11
11
|
* with browser environments. Mutually exclusive with clientEmail/privateKey/credentials. */
|
|
12
12
|
accessToken?: string;
|
|
13
|
+
/** Offline desktop OAuth credentials. Keep these in secure storage, never source control. */
|
|
14
|
+
refreshToken?: string;
|
|
15
|
+
oauthClientId?: string;
|
|
16
|
+
oauthClientSecret?: string;
|
|
17
|
+
tokenExpiresAt?: number;
|
|
13
18
|
/** Google API key for browser-based API access. */
|
|
14
19
|
apiKey?: string;
|
|
15
20
|
}
|
|
@@ -22,10 +27,14 @@ export function normalizeKey(key: string): string {
|
|
|
22
27
|
}
|
|
23
28
|
|
|
24
29
|
export function resolveConfig(config: An5SheetsAdapterConfig) {
|
|
25
|
-
if (config.accessToken || config.apiKey) {
|
|
30
|
+
if (config.accessToken || config.refreshToken || config.apiKey) {
|
|
26
31
|
return {
|
|
27
32
|
spreadsheetId: config.spreadsheetId,
|
|
28
33
|
accessToken: config.accessToken,
|
|
34
|
+
refreshToken: config.refreshToken,
|
|
35
|
+
oauthClientId: config.oauthClientId,
|
|
36
|
+
oauthClientSecret: config.oauthClientSecret,
|
|
37
|
+
tokenExpiresAt: config.tokenExpiresAt,
|
|
29
38
|
apiKey: config.apiKey,
|
|
30
39
|
sheetMapping: config.sheetMapping,
|
|
31
40
|
};
|