@an5/adapters 0.2.5 → 0.2.9

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.
Files changed (99) hide show
  1. package/README.md +283 -305
  2. package/dist/an5Adapter.d.ts +142 -7
  3. package/dist/an5Adapter.js +998 -68
  4. package/dist/base/metadata.d.ts +10 -0
  5. package/dist/base/metadata.js +107 -0
  6. package/dist/base/sql.d.ts +19 -1
  7. package/dist/base/sql.js +152 -24
  8. package/dist/base/types.d.ts +10 -1
  9. package/dist/browser.d.ts +4 -1
  10. package/dist/browser.js +9 -2
  11. package/dist/googlesheets/adapter.d.ts +1 -0
  12. package/dist/googlesheets/adapter.js +9 -2
  13. package/dist/googlesheets/config.d.ts +4 -4
  14. package/dist/googlesheets/helpers.js +2 -0
  15. package/dist/googlesheets/parseConnectionString.js +5 -2
  16. package/dist/googlesheets/sqlExecutor.js +29 -24
  17. package/dist/googlesheets/tableClient.js +14 -5
  18. package/dist/index.d.ts +5 -1
  19. package/dist/index.js +15 -1
  20. package/dist/mssql/engine.js +8 -4
  21. package/dist/mysql/engine.js +1 -1
  22. package/dist/nbase/client.d.ts +92 -0
  23. package/dist/nbase/client.js +147 -0
  24. package/dist/nbase/index.d.ts +47 -0
  25. package/dist/nbase/index.js +80 -0
  26. package/dist/postgres/engine.js +1 -1
  27. package/dist/sqlite/browserEngine.d.ts +27 -0
  28. package/dist/sqlite/browserEngine.js +161 -0
  29. package/dist/sqlite/engine.js +15 -3
  30. package/dist/sqlite/index.d.ts +1 -0
  31. package/dist/sqlite/index.js +1 -0
  32. package/dist/typed.d.ts +5 -0
  33. package/dist/typed.js +2 -0
  34. package/dotnet/An5Adapter.cs +1112 -275
  35. package/dotnet/Base/Core.cs +380 -82
  36. package/dotnet/Mssql/MssqlEngine.cs +145 -144
  37. package/dotnet/Postgres/PostgresEngine.cs +149 -121
  38. package/dotnet/Sqlite/SqliteEngine.cs +214 -0
  39. package/golang/an5_adapter.go +314 -0
  40. package/golang/base/base.go +100 -0
  41. package/golang/base/metadata.go +116 -0
  42. package/golang/base/where.go +311 -0
  43. package/golang/go.mod +3 -0
  44. package/golang/mssql/mssql.go +32 -0
  45. package/golang/postgres/postgres.go +37 -0
  46. package/golang/sqlite/sqlite.go +14 -0
  47. package/golang/sqlite_test.go +42 -0
  48. package/golang/table_client.go +1329 -0
  49. package/package.json +26 -9
  50. package/python/an5_adapter.py +184 -97
  51. package/python/base/__init__.py +3 -3
  52. package/python/base/dialects.py +11 -8
  53. package/python/base/metadata.py +59 -10
  54. package/python/base/sql.py +202 -121
  55. package/python/mssql/__init__.py +1 -1
  56. package/python/mssql/provider.py +62 -62
  57. package/python/postgres/__init__.py +1 -1
  58. package/python/postgres/provider.py +15 -15
  59. package/python/sqlite/__init__.py +3 -0
  60. package/python/sqlite/provider.py +83 -0
  61. package/python/table_client.py +636 -216
  62. package/rust/Cargo.toml +31 -0
  63. package/rust/README.md +71 -0
  64. package/rust/src/adapter.rs +1312 -0
  65. package/rust/src/base/dialect.rs +172 -0
  66. package/rust/src/base/metadata.rs +134 -0
  67. package/rust/src/base/mod.rs +17 -0
  68. package/rust/src/base/where_builder.rs +405 -0
  69. package/rust/src/lib.rs +75 -0
  70. package/typescript/src/an5Adapter.ts +1515 -505
  71. package/typescript/src/base/index.ts +3 -3
  72. package/typescript/src/base/metadata.ts +148 -28
  73. package/typescript/src/base/sql.ts +264 -98
  74. package/typescript/src/base/types.ts +33 -24
  75. package/typescript/src/browser.ts +10 -6
  76. package/typescript/src/config.ts +95 -95
  77. package/typescript/src/googlesheets/adapter.ts +269 -259
  78. package/typescript/src/googlesheets/an5SheetsAdapter.ts +3 -3
  79. package/typescript/src/googlesheets/config.ts +47 -47
  80. package/typescript/src/googlesheets/helpers.ts +144 -143
  81. package/typescript/src/googlesheets/index.ts +3 -3
  82. package/typescript/src/googlesheets/parseConnectionString.ts +48 -46
  83. package/typescript/src/googlesheets/retry.ts +126 -126
  84. package/typescript/src/googlesheets/sqlExecutor.ts +304 -301
  85. package/typescript/src/googlesheets/tableClient.ts +461 -455
  86. package/typescript/src/googlesheets/types.ts +6 -6
  87. package/typescript/src/index.ts +48 -27
  88. package/typescript/src/mssql/engine.ts +134 -132
  89. package/typescript/src/mssql/index.ts +1 -1
  90. package/typescript/src/mysql/engine.ts +80 -80
  91. package/typescript/src/mysql/index.ts +1 -1
  92. package/typescript/src/nbase/client.ts +214 -0
  93. package/typescript/src/nbase/index.ts +104 -0
  94. package/typescript/src/postgres/engine.ts +87 -87
  95. package/typescript/src/postgres/index.ts +1 -1
  96. package/typescript/src/sqlite/browserEngine.ts +192 -0
  97. package/typescript/src/sqlite/engine.ts +66 -55
  98. package/typescript/src/sqlite/index.ts +3 -1
  99. 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 ModelFieldMeta {
2
- name?: string;
3
- ts?: string;
4
- sql?: string;
5
- isId?: boolean;
6
- [key: string]: any;
7
- }
8
-
9
- export interface AdapterMetadata {
10
- modelToTable?: Record<string, string>;
11
- modelFields?: Record<string, any>;
12
- }
13
-
14
- let modelToTable: Record<string, string> = {};
15
- let modelFields: Record<string, any> = {};
16
-
17
- export function setAdapterMetadata(metadata: AdapterMetadata): void {
18
- modelToTable = { ...(metadata.modelToTable || {}) };
19
- modelFields = { ...(metadata.modelFields || {}) };
20
- }
21
-
22
- export function getModelToTable(): Record<string, string> {
23
- return modelToTable;
24
- }
25
-
26
- export function getFieldsForModel(modelName: string): any {
27
- return modelFields[modelName] || {};
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
- // ─── Quoting ───────────────────────────────────────────────────────────────────────
4
-
5
- export function quote(name: string, dialect: Dialect): string {
6
- if (dialect === 'postgres') {
7
- if (name.startsWith('[') && name.endsWith(']')) return `"${name.slice(1, -1)}"`;
8
- if (name.startsWith('"')) return name;
9
- return `"${name}"`;
10
- }
11
- if (dialect === 'mysql') {
12
- if (name.startsWith('[') && name.endsWith(']')) return `\`${name.slice(1, -1)}\``;
13
- if (name.startsWith('`')) return name;
14
- return `\`${name}\``;
15
- }
16
- // mssql / sqlite
17
- return name.startsWith('[') ? name : `[${name}]`;
18
- }
19
-
20
- // ─── Where clause builder ──────────────────────────────────────────────────────────
21
-
22
- export function parseWhere(modelName: string, where: any, params: Record<string, any>, dialect: Dialect, prefix = ''): string {
23
- if (!where) return '';
24
- const conditions: string[] = [];
25
-
26
- const cleanWhere: Record<string, any> = {};
27
- for (const [key, value] of Object.entries(where)) {
28
- if (
29
- key.includes('_') &&
30
- value && typeof value === 'object' &&
31
- !(value instanceof Date) &&
32
- !(value as any).in && !(value as any).contains &&
33
- !(value as any).not && !(value as any).gte &&
34
- !(value as any).lte && !(value as any).gt && !(value as any).lt
35
- ) {
36
- Object.assign(cleanWhere, value);
37
- } else {
38
- cleanWhere[key] = value;
39
- }
40
- }
41
-
42
- for (const [key, value] of Object.entries(cleanWhere)) {
43
- if (key === 'OR' && Array.isArray(value)) {
44
- const sub = value.map((v, i) => parseWhere(modelName, v, params, dialect, `${prefix}or_${i}_`)).filter(Boolean);
45
- if (sub.length > 0) conditions.push(`(${sub.join(' OR ')})`);
46
- } else if (key === 'AND' && Array.isArray(value)) {
47
- const sub = value.map((v, i) => parseWhere(modelName, v, params, dialect, `${prefix}and_${i}_`)).filter(Boolean);
48
- if (sub.length > 0) conditions.push(`(${sub.join(' AND ')})`);
49
- } else {
50
- const pname = `${prefix}${key}`;
51
- const col = quote(key, dialect);
52
-
53
- if (value === null) {
54
- conditions.push(`${col} IS NULL`);
55
- } else if (typeof value === 'object' && !(value instanceof Date)) {
56
- const v = value as any;
57
- if (v.not !== undefined) {
58
- if (v.not === null) { conditions.push(`${col} IS NOT NULL`); }
59
- else { const p = `${pname}_not`; params[p] = v.not; conditions.push(`${col} <> @${p}`); }
60
- }
61
- if (v.equals !== undefined) { const p = `${pname}_eq`; params[p] = v.equals; conditions.push(`${col} = @${p}`); }
62
- if (v.contains !== undefined) { const p = `${pname}_co`; params[p] = `%${v.contains}%`; conditions.push(`${col} LIKE @${p}`); }
63
- if (v.startsWith !== undefined) { const p = `${pname}_sw`; params[p] = `${v.startsWith}%`; conditions.push(`${col} LIKE @${p}`); }
64
- if (v.endsWith !== undefined) { const p = `${pname}_ew`; params[p] = `%${v.endsWith}`; conditions.push(`${col} LIKE @${p}`); }
65
- if (v.gte !== undefined) { const p = `${pname}_gte`; params[p] = v.gte; conditions.push(`${col} >= @${p}`); }
66
- if (v.lte !== undefined) { const p = `${pname}_lte`; params[p] = v.lte; conditions.push(`${col} <= @${p}`); }
67
- if (v.gt !== undefined) { const p = `${pname}_gt`; params[p] = v.gt; conditions.push(`${col} > @${p}`); }
68
- if (v.lt !== undefined) { const p = `${pname}_lt`; params[p] = v.lt; conditions.push(`${col} < @${p}`); }
69
- if (v.in !== undefined) {
70
- if (Array.isArray(v.in) && v.in.length > 0) {
71
- const ps = v.in.map((x: any, i: number) => { const p = `${pname}_in${i}`; params[p] = x; return `@${p}`; });
72
- conditions.push(`${col} IN (${ps.join(', ')})`);
73
- } else {
74
- conditions.push('1=0');
75
- }
76
- }
77
- } else {
78
- params[pname] = value;
79
- conditions.push(`${col} = @${pname}`);
80
- }
81
- }
82
- }
83
- return conditions.join(' AND ');
84
- }
85
-
86
- // ─── Order by builder ──────────────────────────────────────────────────────────────
87
-
88
- export function buildOrderBy(orderBy: any, dialect: Dialect): string {
89
- if (!orderBy) return '';
90
- const entries = Array.isArray(orderBy) ? orderBy : [orderBy];
91
- const parts: string[] = [];
92
- for (const entry of entries) {
93
- for (const [key, dir] of Object.entries(entry)) {
94
- parts.push(`${quote(key, dialect)} ${(dir as string).toUpperCase()}`);
95
- }
96
- }
97
- return parts.length > 0 ? `ORDER BY ${parts.join(', ')}` : '';
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,33 @@
1
- export type Dialect = 'mssql' | 'postgres' | 'mysql' | 'sqlite' | 'googlesheets';
2
-
3
- export interface An5AdapterConfig {
4
- connectionString: string;
5
- poolMax?: number;
6
- requestTimeout?: number;
7
- connectionTimeout?: number;
8
- }
9
-
10
- export interface TransactionHandle {
11
- exec<T>(query: string, params?: Record<string, any>): Promise<T[]>;
12
- executeRaw(query: string, params?: Record<string, any>): Promise<number>;
13
- commit(): Promise<void>;
14
- rollback(): Promise<void>;
15
- }
16
-
17
- export interface QueryEngine {
18
- dialect: Dialect;
19
- exec<T>(query: string, params?: Record<string, any>): Promise<T[]>;
20
- executeRaw(query: string, params?: Record<string, any>): Promise<number>;
21
- connect(): Promise<void>;
22
- disconnect(): Promise<void>;
23
- beginTransaction(): Promise<TransactionHandle>;
24
- }
1
+ export type Dialect = 'mssql' | 'postgres' | 'mysql' | 'sqlite' | 'googlesheets';
2
+
3
+ export interface An5AdapterConfig {
4
+ connectionString?: string;
5
+ /**
6
+ * NBase (Neural Vector Database) endpoint, written as a connection string,
7
+ * e.g. `nbase://localhost:1307`. Attach it here when vector search should
8
+ * run in NBase while the rows stay in `connectionString`'s database.
9
+ */
10
+ nbase?: string;
11
+ engine?: QueryEngine;
12
+ db?: any;
13
+ driver?: any;
14
+ poolMax?: number;
15
+ requestTimeout?: number;
16
+ connectionTimeout?: number;
17
+ }
18
+
19
+ export interface TransactionHandle {
20
+ exec<T>(query: string, params?: Record<string, any>): Promise<T[]>;
21
+ executeRaw(query: string, params?: Record<string, any>): Promise<number>;
22
+ commit(): Promise<void>;
23
+ rollback(): Promise<void>;
24
+ }
25
+
26
+ export interface QueryEngine {
27
+ dialect: Dialect;
28
+ exec<T>(query: string, params?: Record<string, any>): Promise<T[]>;
29
+ executeRaw(query: string, params?: Record<string, any>): Promise<number>;
30
+ connect(): Promise<void>;
31
+ disconnect(): Promise<void>;
32
+ beginTransaction(): Promise<TransactionHandle>;
33
+ }
@@ -1,6 +1,10 @@
1
- // Browser-safe entry — no SQL engine imports
2
- export { An5SheetsAdapter, SheetsTableClient, createAn5SheetsAdapter, parseSheetsConnectionString } from './googlesheets';
3
- export type { An5SheetsAdapterConfig } from './googlesheets';
4
- export type { An5AdapterConfig, Dialect } from './base/types';
5
- export { getFieldsForModel, getModelToTable, setAdapterMetadata } from './base/metadata';
6
- export type { AdapterMetadata } from './base/metadata';
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
+