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