@an5/adapters 0.2.4 → 0.2.6
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 +189 -304
- package/dist/an5Adapter.d.ts +85 -7
- package/dist/an5Adapter.js +736 -64
- package/dist/base/metadata.d.ts +10 -0
- package/dist/base/metadata.js +107 -0
- package/dist/base/sql.d.ts +19 -1
- package/dist/base/sql.js +152 -24
- package/dist/base/types.d.ts +4 -1
- package/dist/browser.d.ts +5 -2
- package/dist/browser.js +10 -2
- package/dist/googlesheets/adapter.d.ts +1 -0
- package/dist/googlesheets/adapter.js +9 -2
- package/dist/googlesheets/config.d.ts +4 -4
- package/dist/googlesheets/helpers.js +2 -0
- package/dist/googlesheets/parseConnectionString.js +5 -2
- package/dist/googlesheets/sqlExecutor.js +29 -24
- package/dist/googlesheets/tableClient.js +14 -5
- package/dist/index.d.ts +5 -1
- package/dist/index.js +12 -1
- package/dist/mssql/engine.js +8 -4
- package/dist/mysql/engine.js +1 -1
- package/dist/postgres/engine.js +1 -1
- package/dist/sqlite/browserEngine.d.ts +27 -0
- package/dist/sqlite/browserEngine.js +161 -0
- package/dist/sqlite/engine.js +15 -3
- package/dist/sqlite/index.d.ts +1 -0
- package/dist/sqlite/index.js +1 -0
- package/dist/typed.d.ts +5 -0
- package/dist/typed.js +2 -0
- package/dotnet/An5Adapter.cs +1089 -275
- package/dotnet/Base/Core.cs +340 -82
- package/dotnet/Mssql/MssqlEngine.cs +145 -144
- package/dotnet/Postgres/PostgresEngine.cs +121 -121
- package/golang/an5_adapter.go +314 -0
- package/golang/base/base.go +100 -0
- package/golang/base/metadata.go +116 -0
- package/golang/base/where.go +311 -0
- package/golang/go.mod +3 -0
- package/golang/mssql/mssql.go +32 -0
- package/golang/postgres/postgres.go +37 -0
- package/golang/sqlite/sqlite.go +14 -0
- package/golang/sqlite_test.go +42 -0
- package/golang/table_client.go +1329 -0
- package/package.json +18 -8
- package/python/an5_adapter.py +119 -97
- package/python/base/__init__.py +3 -3
- package/python/base/dialects.py +11 -8
- package/python/base/metadata.py +35 -10
- package/python/base/sql.py +172 -121
- package/python/mssql/__init__.py +1 -1
- package/python/mssql/provider.py +62 -62
- package/python/postgres/__init__.py +1 -1
- package/python/postgres/provider.py +15 -15
- package/python/table_client.py +619 -216
- package/typescript/src/an5Adapter.ts +1220 -505
- package/typescript/src/base/index.ts +3 -3
- package/typescript/src/base/metadata.ts +148 -28
- package/typescript/src/base/sql.ts +264 -98
- package/typescript/src/base/types.ts +27 -24
- package/typescript/src/browser.ts +10 -6
- package/typescript/src/config.ts +95 -95
- package/typescript/src/googlesheets/adapter.ts +269 -259
- package/typescript/src/googlesheets/an5SheetsAdapter.ts +3 -3
- package/typescript/src/googlesheets/config.ts +47 -47
- package/typescript/src/googlesheets/helpers.ts +144 -143
- package/typescript/src/googlesheets/index.ts +3 -3
- package/typescript/src/googlesheets/parseConnectionString.ts +48 -46
- package/typescript/src/googlesheets/retry.ts +126 -126
- package/typescript/src/googlesheets/sqlExecutor.ts +304 -301
- package/typescript/src/googlesheets/tableClient.ts +461 -455
- package/typescript/src/googlesheets/types.ts +6 -6
- package/typescript/src/index.ts +32 -19
- package/typescript/src/mssql/engine.ts +134 -132
- package/typescript/src/mssql/index.ts +1 -1
- package/typescript/src/mysql/engine.ts +80 -80
- package/typescript/src/mysql/index.ts +1 -1
- package/typescript/src/postgres/engine.ts +87 -87
- package/typescript/src/postgres/index.ts +1 -1
- package/typescript/src/sqlite/browserEngine.ts +192 -0
- package/typescript/src/sqlite/engine.ts +66 -55
- package/typescript/src/sqlite/index.ts +3 -1
- package/typescript/src/typed.ts +52 -0
|
@@ -1,3 +1,3 @@
|
|
|
1
|
-
export * from './types';
|
|
2
|
-
export * from './sql';
|
|
3
|
-
export * from './metadata';
|
|
1
|
+
export * from './types';
|
|
2
|
+
export * from './sql';
|
|
3
|
+
export * from './metadata';
|
|
@@ -1,28 +1,148 @@
|
|
|
1
|
-
export interface
|
|
2
|
-
|
|
3
|
-
|
|
4
|
-
|
|
5
|
-
|
|
6
|
-
|
|
7
|
-
|
|
8
|
-
|
|
9
|
-
|
|
10
|
-
|
|
11
|
-
|
|
12
|
-
|
|
13
|
-
|
|
14
|
-
|
|
15
|
-
|
|
16
|
-
|
|
17
|
-
|
|
18
|
-
|
|
19
|
-
|
|
20
|
-
}
|
|
21
|
-
|
|
22
|
-
|
|
23
|
-
|
|
24
|
-
}
|
|
25
|
-
|
|
26
|
-
|
|
27
|
-
|
|
28
|
-
|
|
1
|
+
export interface RelationDef {
|
|
2
|
+
modelName: string;
|
|
3
|
+
relationType: "many" | "one";
|
|
4
|
+
foreignKey: string;
|
|
5
|
+
localKey: string;
|
|
6
|
+
}
|
|
7
|
+
|
|
8
|
+
export interface ModelFieldMeta {
|
|
9
|
+
name?: string;
|
|
10
|
+
ts?: string;
|
|
11
|
+
sql?: string;
|
|
12
|
+
isId?: boolean;
|
|
13
|
+
[key: string]: any;
|
|
14
|
+
}
|
|
15
|
+
|
|
16
|
+
export interface AdapterMetadata {
|
|
17
|
+
modelToTable?: Record<string, string>;
|
|
18
|
+
modelFields?: Record<string, any>;
|
|
19
|
+
relationMap?: Record<string, Record<string, RelationDef>>;
|
|
20
|
+
}
|
|
21
|
+
|
|
22
|
+
let modelToTable: Record<string, string> = {};
|
|
23
|
+
let modelFields: Record<string, any> = {};
|
|
24
|
+
let relationMap: Record<string, Record<string, RelationDef>> = {};
|
|
25
|
+
let autoLoaded = false;
|
|
26
|
+
|
|
27
|
+
function loadMetadataFromConfig(): any {
|
|
28
|
+
// Auto-loading scans the filesystem at runtime, so it only applies in Node.
|
|
29
|
+
// Lazy requires keep Node builtins out of browser builds.
|
|
30
|
+
if (typeof process === 'undefined' || !process.cwd) return null;
|
|
31
|
+
const path = require('path');
|
|
32
|
+
const fs = require('fs');
|
|
33
|
+
const rootDir = process.cwd();
|
|
34
|
+
const configNames = ['an5Orm.config.js', 'an5Orm.config.cjs', 'an5Orm.config.json'];
|
|
35
|
+
const candidatesDirs = [rootDir, path.resolve(rootDir, '..'), path.resolve(rootDir, '../..')];
|
|
36
|
+
|
|
37
|
+
for (const dir of candidatesDirs) {
|
|
38
|
+
for (const name of configNames) {
|
|
39
|
+
const configPath = path.join(dir, name);
|
|
40
|
+
if (fs.existsSync(configPath)) {
|
|
41
|
+
try {
|
|
42
|
+
const config = name.endsWith('.json')
|
|
43
|
+
? JSON.parse(fs.readFileSync(configPath, 'utf8'))
|
|
44
|
+
: require(configPath);
|
|
45
|
+
const metaFile = config?.outputs?.typescript?.metadataFile;
|
|
46
|
+
if (metaFile) {
|
|
47
|
+
const resolvedPath = path.isAbsolute(metaFile) ? metaFile : path.resolve(dir, metaFile);
|
|
48
|
+
const withoutExt = resolvedPath.replace(/\.(ts|js|cjs|mjs)$/, '');
|
|
49
|
+
const extensions = ['', '.js', '.ts', '.cjs'];
|
|
50
|
+
for (const ext of extensions) {
|
|
51
|
+
const checkPath = withoutExt + ext;
|
|
52
|
+
if (fs.existsSync(checkPath)) {
|
|
53
|
+
try {
|
|
54
|
+
const m = require(withoutExt);
|
|
55
|
+
if (m && (m.modelToTable || m.relationMap || m.modelFields)) {
|
|
56
|
+
return m;
|
|
57
|
+
}
|
|
58
|
+
} catch {}
|
|
59
|
+
}
|
|
60
|
+
}
|
|
61
|
+
}
|
|
62
|
+
} catch {}
|
|
63
|
+
}
|
|
64
|
+
}
|
|
65
|
+
}
|
|
66
|
+
return null;
|
|
67
|
+
}
|
|
68
|
+
|
|
69
|
+
function tryAutoLoadMetadata(): void {
|
|
70
|
+
if (autoLoaded || Object.keys(modelToTable).length > 0) return;
|
|
71
|
+
autoLoaded = true;
|
|
72
|
+
|
|
73
|
+
const configMeta = loadMetadataFromConfig();
|
|
74
|
+
if (configMeta && (configMeta.modelToTable || configMeta.relationMap || configMeta.modelFields)) {
|
|
75
|
+
modelToTable = { ...(configMeta.modelToTable || {}) };
|
|
76
|
+
modelFields = { ...(configMeta.modelFields || {}) };
|
|
77
|
+
relationMap = { ...(configMeta.relationMap || {}) };
|
|
78
|
+
}
|
|
79
|
+
}
|
|
80
|
+
|
|
81
|
+
export function setAdapterMetadata(metadata: AdapterMetadata): void {
|
|
82
|
+
modelToTable = { ...(metadata.modelToTable || {}) };
|
|
83
|
+
modelFields = { ...(metadata.modelFields || {}) };
|
|
84
|
+
relationMap = { ...(metadata.relationMap || {}) };
|
|
85
|
+
}
|
|
86
|
+
|
|
87
|
+
export function getModelToTable(): Record<string, string> {
|
|
88
|
+
tryAutoLoadMetadata();
|
|
89
|
+
return modelToTable;
|
|
90
|
+
}
|
|
91
|
+
|
|
92
|
+
export function getFieldsForModel(modelName: string): any {
|
|
93
|
+
tryAutoLoadMetadata();
|
|
94
|
+
return modelFields[modelName] || {};
|
|
95
|
+
}
|
|
96
|
+
|
|
97
|
+
export function getRelationMap(): Record<string, Record<string, RelationDef>> {
|
|
98
|
+
tryAutoLoadMetadata();
|
|
99
|
+
return relationMap;
|
|
100
|
+
}
|
|
101
|
+
|
|
102
|
+
export function getRelationsForModel(modelName: string): Record<string, RelationDef> {
|
|
103
|
+
tryAutoLoadMetadata();
|
|
104
|
+
return relationMap[modelName] || {};
|
|
105
|
+
}
|
|
106
|
+
|
|
107
|
+
// ─── Dynamic model proxy ─────────────────────────────────────────────────────
|
|
108
|
+
// Shared by An5Adapter and An5SheetsAdapter: `db.user` / `db.User` /
|
|
109
|
+
// `db.users` resolve case-insensitively through the registered metadata and
|
|
110
|
+
// delegate to `table(modelName)`. Real members (`$`-prefixed helpers and
|
|
111
|
+
// methods) always win over model names.
|
|
112
|
+
export function createAdapterProxy<T extends object>(
|
|
113
|
+
targetObj: T,
|
|
114
|
+
tableGetter: (modelName: string) => any
|
|
115
|
+
): T {
|
|
116
|
+
return new Proxy(targetObj, {
|
|
117
|
+
get(target: any, prop: string | symbol, receiver) {
|
|
118
|
+
if (typeof prop === 'string') {
|
|
119
|
+
if (prop in target || prop.startsWith('_') || prop.startsWith('$') || typeof target[prop] === 'function') {
|
|
120
|
+
const val = target[prop];
|
|
121
|
+
return typeof val === 'function' ? val.bind(target) : val;
|
|
122
|
+
}
|
|
123
|
+
let modelName = prop;
|
|
124
|
+
const modelToTable = getModelToTable();
|
|
125
|
+
if (!modelToTable[prop]) {
|
|
126
|
+
const lowerProp = prop.toLowerCase();
|
|
127
|
+
for (const [mName, tName] of Object.entries(modelToTable)) {
|
|
128
|
+
const lowerM = mName.toLowerCase();
|
|
129
|
+
const lowerT = (tName as string).toLowerCase();
|
|
130
|
+
if (
|
|
131
|
+
lowerM === lowerProp ||
|
|
132
|
+
lowerT === lowerProp ||
|
|
133
|
+
lowerM + 's' === lowerProp ||
|
|
134
|
+
lowerM + 'es' === lowerProp ||
|
|
135
|
+
lowerT + 's' === lowerProp ||
|
|
136
|
+
lowerT + 'es' === lowerProp
|
|
137
|
+
) {
|
|
138
|
+
modelName = mName;
|
|
139
|
+
break;
|
|
140
|
+
}
|
|
141
|
+
}
|
|
142
|
+
}
|
|
143
|
+
return tableGetter(modelName);
|
|
144
|
+
}
|
|
145
|
+
return Reflect.get(target, prop, receiver);
|
|
146
|
+
},
|
|
147
|
+
});
|
|
148
|
+
}
|
|
@@ -1,98 +1,264 @@
|
|
|
1
|
-
import type { Dialect } from './types';
|
|
2
|
-
|
|
3
|
-
|
|
4
|
-
|
|
5
|
-
|
|
6
|
-
|
|
7
|
-
|
|
8
|
-
|
|
9
|
-
|
|
10
|
-
|
|
11
|
-
|
|
12
|
-
|
|
13
|
-
|
|
14
|
-
return
|
|
15
|
-
}
|
|
16
|
-
|
|
17
|
-
|
|
18
|
-
}
|
|
19
|
-
|
|
20
|
-
//
|
|
21
|
-
|
|
22
|
-
|
|
23
|
-
|
|
24
|
-
|
|
25
|
-
|
|
26
|
-
|
|
27
|
-
|
|
28
|
-
|
|
29
|
-
|
|
30
|
-
|
|
31
|
-
|
|
32
|
-
|
|
33
|
-
|
|
34
|
-
|
|
35
|
-
|
|
36
|
-
|
|
37
|
-
|
|
38
|
-
|
|
39
|
-
|
|
40
|
-
|
|
41
|
-
|
|
42
|
-
|
|
43
|
-
|
|
44
|
-
|
|
45
|
-
|
|
46
|
-
|
|
47
|
-
|
|
48
|
-
|
|
49
|
-
|
|
50
|
-
|
|
51
|
-
|
|
52
|
-
|
|
53
|
-
|
|
54
|
-
|
|
55
|
-
|
|
56
|
-
|
|
57
|
-
|
|
58
|
-
|
|
59
|
-
|
|
60
|
-
|
|
61
|
-
|
|
62
|
-
|
|
63
|
-
|
|
64
|
-
|
|
65
|
-
|
|
66
|
-
|
|
67
|
-
|
|
68
|
-
|
|
69
|
-
|
|
70
|
-
|
|
71
|
-
|
|
72
|
-
|
|
73
|
-
|
|
74
|
-
|
|
75
|
-
|
|
76
|
-
|
|
77
|
-
|
|
78
|
-
|
|
79
|
-
|
|
80
|
-
|
|
81
|
-
|
|
82
|
-
|
|
83
|
-
|
|
84
|
-
|
|
85
|
-
|
|
86
|
-
|
|
87
|
-
|
|
88
|
-
|
|
89
|
-
|
|
90
|
-
|
|
91
|
-
const
|
|
92
|
-
for (const
|
|
93
|
-
|
|
94
|
-
|
|
95
|
-
|
|
96
|
-
|
|
97
|
-
|
|
98
|
-
|
|
1
|
+
import type { Dialect } from './types';
|
|
2
|
+
import type { RelationDef } from './metadata';
|
|
3
|
+
|
|
4
|
+
// ─── Quoting ───────────────────────────────────────────────────────────────────────
|
|
5
|
+
|
|
6
|
+
function stripWrapping(name: string, left: string, right: string): string {
|
|
7
|
+
return name.startsWith(left) && name.endsWith(right) ? name.slice(left.length, -right.length) : name;
|
|
8
|
+
}
|
|
9
|
+
|
|
10
|
+
export function quote(name: string, dialect: Dialect): string {
|
|
11
|
+
const raw = String(name);
|
|
12
|
+
if (dialect === 'postgres') {
|
|
13
|
+
const unwrapped = stripWrapping(stripWrapping(raw, '[', ']'), '"', '"');
|
|
14
|
+
return `"${unwrapped.replace(/"/g, '""')}"`;
|
|
15
|
+
}
|
|
16
|
+
if (dialect === 'mysql') {
|
|
17
|
+
const unwrapped = stripWrapping(stripWrapping(raw, '[', ']'), '`', '`');
|
|
18
|
+
return `\`${unwrapped.replace(/`/g, '``')}\``;
|
|
19
|
+
}
|
|
20
|
+
// mssql / sqlite
|
|
21
|
+
const unwrapped = stripWrapping(raw, '[', ']');
|
|
22
|
+
return `[${unwrapped.replace(/\]/g, ']]')}]`;
|
|
23
|
+
}
|
|
24
|
+
|
|
25
|
+
/** Quote a full table reference (may already be quoted, and may contain schema parts). */
|
|
26
|
+
export function quoteTable(name: string, dialect: Dialect): string {
|
|
27
|
+
const raw = String(name);
|
|
28
|
+
if (raw.startsWith('[') || raw.startsWith('"') || raw.startsWith('`')) return raw;
|
|
29
|
+
if (raw.includes('.')) {
|
|
30
|
+
const open = dialect === 'mysql' ? '`' : dialect === 'postgres' ? '"' : '[';
|
|
31
|
+
const close = open === '[' ? ']' : open;
|
|
32
|
+
return raw.split('.').map(p => `${open}${p.replace(new RegExp(close.replace(/[[\]]/g, '\\$&'), 'g'), close.repeat(2))}${close}`).join('.');
|
|
33
|
+
}
|
|
34
|
+
return quote(raw, dialect);
|
|
35
|
+
}
|
|
36
|
+
|
|
37
|
+
function sanitizeParamName(name: string): string {
|
|
38
|
+
const cleaned = String(name).replace(/[^A-Za-z0-9_]/g, '_');
|
|
39
|
+
return /^[A-Za-z]/.test(cleaned) ? cleaned : `p_${cleaned}`;
|
|
40
|
+
}
|
|
41
|
+
|
|
42
|
+
function normalizeSortDirection(dir: unknown): 'ASC' | 'DESC' {
|
|
43
|
+
return typeof dir === 'string' && dir.toUpperCase() === 'DESC' ? 'DESC' : 'ASC';
|
|
44
|
+
}
|
|
45
|
+
|
|
46
|
+
// ─── Where clause builder ──────────────────────────────────────────────────────────
|
|
47
|
+
|
|
48
|
+
const OPERATOR_KEYS = ['equals', 'in', 'notIn', 'contains', 'startsWith', 'endsWith', 'not', 'gte', 'lte', 'gt', 'lt'];
|
|
49
|
+
|
|
50
|
+
function isOperatorValue(value: any): boolean {
|
|
51
|
+
if (!value || typeof value !== 'object' || value instanceof Date) return false;
|
|
52
|
+
return OPERATOR_KEYS.some(op => op in value);
|
|
53
|
+
}
|
|
54
|
+
|
|
55
|
+
export interface WhereContext {
|
|
56
|
+
/** Relations of the current model keyed by relation name. */
|
|
57
|
+
relationMap?: Record<string, Record<string, RelationDef>>;
|
|
58
|
+
/** model name → table name for resolving related tables. */
|
|
59
|
+
modelToTable?: Record<string, string>;
|
|
60
|
+
/** Fully-qualified reference to the current (self) table, e.g. `[users]` or `[dbo].[users]`. */
|
|
61
|
+
selfRef?: string;
|
|
62
|
+
/** Column prefix to disambiguate inside a correlated subquery (e.g. `__r.`). Includes trailing dot. */
|
|
63
|
+
colPrefix?: string;
|
|
64
|
+
}
|
|
65
|
+
|
|
66
|
+
function resolveTable(modelName: string, ctx?: WhereContext): string {
|
|
67
|
+
if (ctx?.modelToTable?.[modelName]) return ctx.modelToTable[modelName];
|
|
68
|
+
const camel = modelName.charAt(0).toLowerCase() + modelName.slice(1);
|
|
69
|
+
if (ctx?.modelToTable?.[camel]) return ctx.modelToTable[camel];
|
|
70
|
+
const lower = modelName.toLowerCase();
|
|
71
|
+
if (ctx?.modelToTable?.[lower]) return ctx.modelToTable[lower];
|
|
72
|
+
return modelName;
|
|
73
|
+
}
|
|
74
|
+
|
|
75
|
+
/**
|
|
76
|
+
* Build a WHERE clause. Supports scalar fields, operators and relation filters
|
|
77
|
+
* (`some`/`none`/`every` for to-many, `is`/`isNot` for to-one) via EXISTS
|
|
78
|
+
* subqueries when `ctx.relationMap` is provided.
|
|
79
|
+
*/
|
|
80
|
+
export function parseWhere(
|
|
81
|
+
modelName: string,
|
|
82
|
+
where: any,
|
|
83
|
+
params: Record<string, any>,
|
|
84
|
+
dialect: Dialect,
|
|
85
|
+
prefix = '',
|
|
86
|
+
ctx?: WhereContext,
|
|
87
|
+
): string {
|
|
88
|
+
if (!where) return '';
|
|
89
|
+
const conditions: string[] = [];
|
|
90
|
+
|
|
91
|
+
const cleanWhere: Record<string, any> = {};
|
|
92
|
+
for (const [key, value] of Object.entries(where)) {
|
|
93
|
+
if (
|
|
94
|
+
key.includes('_') &&
|
|
95
|
+
value && typeof value === 'object' &&
|
|
96
|
+
!(value instanceof Date) &&
|
|
97
|
+
!isOperatorValue(value)
|
|
98
|
+
) {
|
|
99
|
+
Object.assign(cleanWhere, value);
|
|
100
|
+
} else {
|
|
101
|
+
cleanWhere[key] = value;
|
|
102
|
+
}
|
|
103
|
+
}
|
|
104
|
+
|
|
105
|
+
const relations = ctx?.relationMap?.[modelName] || {};
|
|
106
|
+
const colPrefix = ctx?.colPrefix || '';
|
|
107
|
+
|
|
108
|
+
for (const [key, value] of Object.entries(cleanWhere)) {
|
|
109
|
+
if (key === 'OR' && Array.isArray(value)) {
|
|
110
|
+
const sub = value.map((v, i) => parseWhere(modelName, v, params, dialect, `${prefix}or_${i}_`, ctx)).filter(Boolean);
|
|
111
|
+
if (sub.length > 0) conditions.push(`(${sub.join(' OR ')})`);
|
|
112
|
+
} else if (key === 'AND') {
|
|
113
|
+
const items = Array.isArray(value) ? value : [value];
|
|
114
|
+
const sub = items.map((v, i) => parseWhere(modelName, v, params, dialect, `${prefix}and_${i}_`, ctx)).filter(Boolean);
|
|
115
|
+
if (sub.length > 0) conditions.push(`(${sub.join(' AND ')})`);
|
|
116
|
+
} else if (key === 'NOT') {
|
|
117
|
+
const items = Array.isArray(value) ? value : [value];
|
|
118
|
+
const sub = items.map((v, i) => parseWhere(modelName, v, params, dialect, `${prefix}not_${i}_`, ctx)).filter(Boolean);
|
|
119
|
+
if (sub.length > 0) conditions.push(`NOT (${sub.join(' AND ')})`);
|
|
120
|
+
} else if (relations[key] && value && typeof value === 'object') {
|
|
121
|
+
const clause = buildRelationClause(relations[key], value, modelName, params, dialect, prefix, ctx);
|
|
122
|
+
if (clause) conditions.push(clause);
|
|
123
|
+
} else {
|
|
124
|
+
const pname = sanitizeParamName(`${prefix}${key}`);
|
|
125
|
+
const col = `${colPrefix}${quote(key, dialect)}`;
|
|
126
|
+
|
|
127
|
+
if (value === null) {
|
|
128
|
+
conditions.push(`${col} IS NULL`);
|
|
129
|
+
} else if (typeof value === 'object' && !(value instanceof Date)) {
|
|
130
|
+
const v = value as any;
|
|
131
|
+
if (v.not !== undefined) {
|
|
132
|
+
if (v.not === null) { conditions.push(`${col} IS NOT NULL`); }
|
|
133
|
+
else if (v.not && typeof v.not === 'object' && !(v.not instanceof Date) && !Array.isArray(v.not)) {
|
|
134
|
+
const nestedParams: Record<string, any> = {};
|
|
135
|
+
const nestedSql = parseWhere(modelName, { [key]: v.not }, nestedParams, dialect, `${prefix}${key}_not_`, ctx);
|
|
136
|
+
Object.assign(params, nestedParams);
|
|
137
|
+
if (nestedSql) conditions.push(`NOT (${nestedSql})`);
|
|
138
|
+
} else { const p = `${pname}_not`; params[p] = v.not; conditions.push(`${col} <> @${p}`); }
|
|
139
|
+
}
|
|
140
|
+
if (v.equals !== undefined) {
|
|
141
|
+
if (v.equals === null) { conditions.push(`${col} IS NULL`); }
|
|
142
|
+
else { const p = `${pname}_eq`; params[p] = v.equals; conditions.push(`${col} = @${p}`); }
|
|
143
|
+
}
|
|
144
|
+
if (v.contains !== undefined) { const p = `${pname}_co`; params[p] = `%${v.contains}%`; conditions.push(`${col} LIKE @${p}`); }
|
|
145
|
+
if (v.startsWith !== undefined) { const p = `${pname}_sw`; params[p] = `${v.startsWith}%`; conditions.push(`${col} LIKE @${p}`); }
|
|
146
|
+
if (v.endsWith !== undefined) { const p = `${pname}_ew`; params[p] = `%${v.endsWith}`; conditions.push(`${col} LIKE @${p}`); }
|
|
147
|
+
if (v.gte !== undefined) { const p = `${pname}_gte`; params[p] = v.gte; conditions.push(`${col} >= @${p}`); }
|
|
148
|
+
if (v.lte !== undefined) { const p = `${pname}_lte`; params[p] = v.lte; conditions.push(`${col} <= @${p}`); }
|
|
149
|
+
if (v.gt !== undefined) { const p = `${pname}_gt`; params[p] = v.gt; conditions.push(`${col} > @${p}`); }
|
|
150
|
+
if (v.lt !== undefined) { const p = `${pname}_lt`; params[p] = v.lt; conditions.push(`${col} < @${p}`); }
|
|
151
|
+
if (v.in !== undefined) {
|
|
152
|
+
if (Array.isArray(v.in) && v.in.length > 0) {
|
|
153
|
+
const ps = v.in.map((x: any, i: number) => { const p = `${pname}_in${i}`; params[p] = x; return `@${p}`; });
|
|
154
|
+
conditions.push(`${col} IN (${ps.join(', ')})`);
|
|
155
|
+
} else {
|
|
156
|
+
conditions.push('1=0');
|
|
157
|
+
}
|
|
158
|
+
}
|
|
159
|
+
if (v.notIn !== undefined) {
|
|
160
|
+
if (Array.isArray(v.notIn) && v.notIn.length > 0) {
|
|
161
|
+
const ps = v.notIn.map((x: any, i: number) => { const p = `${pname}_notin${i}`; params[p] = x; return `@${p}`; });
|
|
162
|
+
conditions.push(`${col} NOT IN (${ps.join(', ')})`);
|
|
163
|
+
} else {
|
|
164
|
+
conditions.push('1=1');
|
|
165
|
+
}
|
|
166
|
+
}
|
|
167
|
+
} else {
|
|
168
|
+
params[pname] = value;
|
|
169
|
+
conditions.push(`${col} = @${pname}`);
|
|
170
|
+
}
|
|
171
|
+
}
|
|
172
|
+
}
|
|
173
|
+
return conditions.join(' AND ');
|
|
174
|
+
}
|
|
175
|
+
|
|
176
|
+
function buildRelationClause(
|
|
177
|
+
relation: RelationDef,
|
|
178
|
+
value: any,
|
|
179
|
+
modelName: string,
|
|
180
|
+
params: Record<string, any>,
|
|
181
|
+
dialect: Dialect,
|
|
182
|
+
prefix: string,
|
|
183
|
+
ctx?: WhereContext,
|
|
184
|
+
): string | null {
|
|
185
|
+
const isMany = relation.relationType === 'many';
|
|
186
|
+
const qname = `${prefix}rel_${sanitizeParamName(relation.modelName)}`;
|
|
187
|
+
const childAlias = `__r_${sanitizeParamName(relation.modelName)}${qname.length}`.replace(/[^A-Za-z0-9_]/g, '_');
|
|
188
|
+
const childTable = quoteTable(resolveTable(relation.modelName, ctx), dialect);
|
|
189
|
+
const fk = quote(relation.foreignKey, dialect);
|
|
190
|
+
const lk = quote(relation.localKey, dialect);
|
|
191
|
+
const selfRef = (ctx?.selfRef || quoteTable(modelName, dialect)) + `.`;
|
|
192
|
+
|
|
193
|
+
// Correlation between the parent row and the related row.
|
|
194
|
+
const correlation = `${childAlias}.${fk} = ${selfRef}${lk}`;
|
|
195
|
+
|
|
196
|
+
// Correlated subquery for "there exists a related row matching <innerWhere>".
|
|
197
|
+
const buildExists = (innerWhere: string): string => {
|
|
198
|
+
const inner = innerWhere ? ` AND (${innerWhere})` : '';
|
|
199
|
+
return `EXISTS (SELECT 1 FROM ${childTable} AS ${childAlias} WHERE ${correlation}${inner})`;
|
|
200
|
+
};
|
|
201
|
+
|
|
202
|
+
const parts: string[] = [];
|
|
203
|
+
|
|
204
|
+
if (isMany) {
|
|
205
|
+
if (value.some !== undefined) {
|
|
206
|
+
const inner = buildInnerWhere(relation.modelName, value.some === null ? {} : value.some, params, dialect, prefix, ctx, childAlias);
|
|
207
|
+
parts.push(buildExists(inner));
|
|
208
|
+
}
|
|
209
|
+
if (value.none !== undefined) {
|
|
210
|
+
const inner = buildInnerWhere(relation.modelName, value.none === null ? {} : value.none, params, dialect, prefix, ctx, childAlias);
|
|
211
|
+
parts.push(`NOT ${buildExists(inner)}`);
|
|
212
|
+
}
|
|
213
|
+
if (value.every !== undefined) {
|
|
214
|
+
// "every" = no related row violates the condition.
|
|
215
|
+
const inner = buildInnerWhere(relation.modelName, value.every === null ? {} : value.every, params, dialect, prefix, ctx, childAlias);
|
|
216
|
+
const negated = inner ? `NOT (${inner})` : '';
|
|
217
|
+
const violated = `EXISTS (SELECT 1 FROM ${childTable} AS ${childAlias} WHERE ${correlation}${negated ? ` AND ${negated}` : ' AND 1=0'})`;
|
|
218
|
+
parts.push(`NOT ${violated}`);
|
|
219
|
+
}
|
|
220
|
+
} else {
|
|
221
|
+
if (value.is !== undefined) {
|
|
222
|
+
const inner = buildInnerWhere(relation.modelName, value.is === null ? {} : value.is, params, dialect, prefix, ctx, childAlias);
|
|
223
|
+
parts.push(buildExists(inner));
|
|
224
|
+
}
|
|
225
|
+
if (value.isNot !== undefined) {
|
|
226
|
+
const inner = buildInnerWhere(relation.modelName, value.isNot === null ? {} : value.isNot, params, dialect, prefix, ctx, childAlias);
|
|
227
|
+
parts.push(`NOT ${buildExists(inner)}`);
|
|
228
|
+
}
|
|
229
|
+
}
|
|
230
|
+
|
|
231
|
+
return parts.length > 0 ? parts.join(' AND ') : null;
|
|
232
|
+
}
|
|
233
|
+
|
|
234
|
+
function buildInnerWhere(
|
|
235
|
+
relModel: string,
|
|
236
|
+
where: any,
|
|
237
|
+
params: Record<string, any>,
|
|
238
|
+
dialect: Dialect,
|
|
239
|
+
prefix: string,
|
|
240
|
+
ctx?: WhereContext,
|
|
241
|
+
childAlias?: string,
|
|
242
|
+
): string {
|
|
243
|
+
const subCtx: WhereContext = {
|
|
244
|
+
...(ctx?.relationMap !== undefined ? { relationMap: ctx.relationMap } : {}),
|
|
245
|
+
...(ctx?.modelToTable !== undefined ? { modelToTable: ctx.modelToTable } : {}),
|
|
246
|
+
selfRef: childAlias || quoteTable(resolveTable(relModel, ctx), dialect),
|
|
247
|
+
colPrefix: childAlias ? `${childAlias}.` : '',
|
|
248
|
+
};
|
|
249
|
+
return parseWhere(relModel, where, params, dialect, `${prefix}rel_inner_`, subCtx);
|
|
250
|
+
}
|
|
251
|
+
|
|
252
|
+
// ─── Order by builder ──────────────────────────────────────────────────────────────
|
|
253
|
+
|
|
254
|
+
export function buildOrderBy(orderBy: any, dialect: Dialect): string {
|
|
255
|
+
if (!orderBy) return '';
|
|
256
|
+
const entries = Array.isArray(orderBy) ? orderBy : [orderBy];
|
|
257
|
+
const parts: string[] = [];
|
|
258
|
+
for (const entry of entries) {
|
|
259
|
+
for (const [key, dir] of Object.entries(entry)) {
|
|
260
|
+
parts.push(`${quote(key, dialect)} ${normalizeSortDirection(dir)}`);
|
|
261
|
+
}
|
|
262
|
+
}
|
|
263
|
+
return parts.length > 0 ? `ORDER BY ${parts.join(', ')}` : '';
|
|
264
|
+
}
|
|
@@ -1,24 +1,27 @@
|
|
|
1
|
-
export type Dialect = 'mssql' | 'postgres' | 'mysql' | 'sqlite' | 'googlesheets';
|
|
2
|
-
|
|
3
|
-
export interface An5AdapterConfig {
|
|
4
|
-
connectionString
|
|
5
|
-
|
|
6
|
-
|
|
7
|
-
|
|
8
|
-
|
|
9
|
-
|
|
10
|
-
|
|
11
|
-
|
|
12
|
-
|
|
13
|
-
|
|
14
|
-
|
|
15
|
-
|
|
16
|
-
|
|
17
|
-
|
|
18
|
-
|
|
19
|
-
|
|
20
|
-
|
|
21
|
-
|
|
22
|
-
|
|
23
|
-
|
|
24
|
-
|
|
1
|
+
export type Dialect = 'mssql' | 'postgres' | 'mysql' | 'sqlite' | 'googlesheets';
|
|
2
|
+
|
|
3
|
+
export interface An5AdapterConfig {
|
|
4
|
+
connectionString?: string;
|
|
5
|
+
engine?: QueryEngine;
|
|
6
|
+
db?: any;
|
|
7
|
+
driver?: any;
|
|
8
|
+
poolMax?: number;
|
|
9
|
+
requestTimeout?: number;
|
|
10
|
+
connectionTimeout?: number;
|
|
11
|
+
}
|
|
12
|
+
|
|
13
|
+
export interface TransactionHandle {
|
|
14
|
+
exec<T>(query: string, params?: Record<string, any>): Promise<T[]>;
|
|
15
|
+
executeRaw(query: string, params?: Record<string, any>): Promise<number>;
|
|
16
|
+
commit(): Promise<void>;
|
|
17
|
+
rollback(): Promise<void>;
|
|
18
|
+
}
|
|
19
|
+
|
|
20
|
+
export interface QueryEngine {
|
|
21
|
+
dialect: Dialect;
|
|
22
|
+
exec<T>(query: string, params?: Record<string, any>): Promise<T[]>;
|
|
23
|
+
executeRaw(query: string, params?: Record<string, any>): Promise<number>;
|
|
24
|
+
connect(): Promise<void>;
|
|
25
|
+
disconnect(): Promise<void>;
|
|
26
|
+
beginTransaction(): Promise<TransactionHandle>;
|
|
27
|
+
}
|
|
@@ -1,6 +1,10 @@
|
|
|
1
|
-
// Browser-safe entry —
|
|
2
|
-
export { An5SheetsAdapter, SheetsTableClient, createAn5SheetsAdapter } from './googlesheets';
|
|
3
|
-
export type { An5SheetsAdapterConfig } from './googlesheets';
|
|
4
|
-
export
|
|
5
|
-
export {
|
|
6
|
-
export
|
|
1
|
+
// Browser-safe entry — supports in-browser SQLite (sql.js / WASM / custom driver) and Google Sheets
|
|
2
|
+
export { An5SheetsAdapter, SheetsTableClient, createAn5SheetsAdapter, parseSheetsConnectionString } from './googlesheets';
|
|
3
|
+
export type { An5SheetsAdapterConfig } from './googlesheets';
|
|
4
|
+
export { SqliteBrowserEngine, createBrowserSqliteAdapter } from './sqlite/browserEngine';
|
|
5
|
+
export type { SqliteDriver, SqliteBrowserConfig } from './sqlite/browserEngine';
|
|
6
|
+
export { An5Adapter, createAn5Adapter, createAdapter } from './an5Adapter';
|
|
7
|
+
export type { An5AdapterConfig, Dialect, QueryEngine, TransactionHandle } from './base/types';
|
|
8
|
+
export { getFieldsForModel, getModelToTable, setAdapterMetadata } from './base/metadata';
|
|
9
|
+
export type { AdapterMetadata } from './base/metadata';
|
|
10
|
+
|