@an5/orm 1.0.8 → 1.1.0

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 (73) hide show
  1. package/README.md +174 -175
  2. package/dist/an5Metadata.js +64 -0
  3. package/dist/an5Orm.d.ts +92 -3
  4. package/dist/an5Orm.js +697 -206
  5. package/dist/config.d.ts +9 -1
  6. package/dist/config.js +13 -18
  7. package/dist/errors.d.ts +1 -1
  8. package/dist/errors.js +1 -1
  9. package/dist/generator/src/api.d.ts +35 -0
  10. package/dist/generator/src/api.js +58 -0
  11. package/dist/generator/src/code-generator.d.ts +14 -13
  12. package/dist/generator/src/code-generator.js +249 -225
  13. package/dist/generator/src/config.d.ts +162 -0
  14. package/dist/generator/src/config.js +354 -0
  15. package/dist/generator/src/dialect.d.ts +88 -0
  16. package/dist/generator/src/dialect.js +318 -0
  17. package/dist/generator/src/dotnet-generator.d.ts +21 -11
  18. package/dist/generator/src/dotnet-generator.js +1053 -546
  19. package/dist/generator/src/field-types.d.ts +96 -0
  20. package/dist/generator/src/field-types.js +433 -0
  21. package/dist/generator/src/golang-generator.d.ts +36 -0
  22. package/dist/generator/src/golang-generator.js +1229 -0
  23. package/dist/generator/src/index.d.ts +1 -1
  24. package/dist/generator/src/index.js +137 -88
  25. package/dist/generator/src/metadata-generator.d.ts +11 -8
  26. package/dist/generator/src/metadata-generator.js +105 -90
  27. package/dist/generator/src/parser.d.ts +37 -11
  28. package/dist/generator/src/parser.js +208 -206
  29. package/dist/generator/src/push-apply.d.ts +37 -0
  30. package/dist/generator/src/push-apply.js +109 -0
  31. package/dist/generator/src/push-schema.d.ts +93 -0
  32. package/dist/generator/src/push-schema.js +189 -0
  33. package/dist/generator/src/python-generator.d.ts +37 -9
  34. package/dist/generator/src/python-generator.js +428 -105
  35. package/dist/generator/src/rust-generator.d.ts +38 -0
  36. package/dist/generator/src/rust-generator.js +971 -0
  37. package/dist/generator/src/suggest.d.ts +17 -0
  38. package/dist/generator/src/suggest.js +45 -0
  39. package/dist/generator/src/type-kinds.d.ts +27 -0
  40. package/dist/generator/src/type-kinds.js +72 -0
  41. package/dist/generator/src/types.d.ts +60 -27
  42. package/dist/generator/src/types.js +41 -2
  43. package/dist/index.d.ts +3 -2
  44. package/dist/index.js +19 -8
  45. package/dist/logger.js +1 -1
  46. package/dist/migration-core.d.ts +102 -0
  47. package/dist/migration-core.js +731 -0
  48. package/dist/orm.d.ts +147 -0
  49. package/dist/orm.js +1654 -0
  50. package/dist/provider-support.d.ts +27 -0
  51. package/dist/provider-support.js +50 -0
  52. package/dist/scripts/release-cli.d.ts +2 -2
  53. package/dist/scripts/release-cli.js +115 -115
  54. package/dist/sql-utils.d.ts +3 -1
  55. package/dist/sql-utils.js +94 -24
  56. package/generator/src/api.ts +45 -0
  57. package/generator/src/code-generator.ts +259 -230
  58. package/generator/src/config.ts +496 -0
  59. package/generator/src/dialect.ts +415 -0
  60. package/generator/src/dotnet-generator.ts +1071 -544
  61. package/generator/src/field-types.ts +473 -0
  62. package/generator/src/golang-generator.ts +1267 -0
  63. package/generator/src/index.ts +146 -116
  64. package/generator/src/metadata-generator.ts +116 -101
  65. package/generator/src/parser.ts +234 -202
  66. package/generator/src/push-apply.ts +148 -0
  67. package/generator/src/push-schema.ts +250 -0
  68. package/generator/src/python-generator.ts +480 -114
  69. package/generator/src/rust-generator.ts +974 -0
  70. package/generator/src/suggest.ts +41 -0
  71. package/generator/src/type-kinds.ts +78 -0
  72. package/generator/src/types.ts +77 -29
  73. package/package.json +32 -6
@@ -0,0 +1,27 @@
1
+ import { Provider } from './generator/src/field-types';
2
+ /**
3
+ * Which commands can only run against SQL Server.
4
+ *
5
+ * `db:push` is not among them: it goes through the dialect layer in
6
+ * `./generator/src/dialect`, so it writes the DDL each provider understands. These
7
+ * three do not — they read `sys.tables`/`sys.columns`/`sys.foreign_keys`, quote
8
+ * identifiers with brackets, and emit T-SQL. Running them against another database
9
+ * fails on the first catalog query with a driver error that says nothing about the
10
+ * real problem, or worse, half-applies a change before it does. So the provider is
11
+ * checked up front, where the message can name it.
12
+ *
13
+ * Not a reason to narrow the type tables though: the tables describe what each
14
+ * provider accepts, which is what the generated client and the schema files must
15
+ * stay within, whoever runs the commands.
16
+ */
17
+ export declare const SQL_SERVER_ONLY_COMMANDS: Record<string, string>;
18
+ /**
19
+ * Why a command cannot run against this provider, or null when it can.
20
+ *
21
+ * Returns the message instead of printing it and exiting, so this stays a plain
22
+ * function: it is shared by four command files, and a module that calls
23
+ * `process.exit` cannot be imported outside a Node CLI — nor unit tested for the
24
+ * thing it exists to say.
25
+ */
26
+ export declare function sqlServerOnlyError(provider: Provider, command: string): string | null;
27
+ //# sourceMappingURL=provider-support.d.ts.map
@@ -0,0 +1,50 @@
1
+ "use strict";
2
+ Object.defineProperty(exports, "__esModule", { value: true });
3
+ exports.SQL_SERVER_ONLY_COMMANDS = void 0;
4
+ exports.sqlServerOnlyError = sqlServerOnlyError;
5
+ const field_types_1 = require("./generator/src/field-types");
6
+ /**
7
+ * Which commands can only run against SQL Server.
8
+ *
9
+ * `db:push` is not among them: it goes through the dialect layer in
10
+ * `./generator/src/dialect`, so it writes the DDL each provider understands. These
11
+ * three do not — they read `sys.tables`/`sys.columns`/`sys.foreign_keys`, quote
12
+ * identifiers with brackets, and emit T-SQL. Running them against another database
13
+ * fails on the first catalog query with a driver error that says nothing about the
14
+ * real problem, or worse, half-applies a change before it does. So the provider is
15
+ * checked up front, where the message can name it.
16
+ *
17
+ * Not a reason to narrow the type tables though: the tables describe what each
18
+ * provider accepts, which is what the generated client and the schema files must
19
+ * stay within, whoever runs the commands.
20
+ */
21
+ exports.SQL_SERVER_ONLY_COMMANDS = {
22
+ 'db:pull': 'read the catalog and write schema files',
23
+ 'db:migrate': 'diff the schema against the database and apply migrations',
24
+ 'db:migrate:diff': 'diff the schema against the database',
25
+ 'db:migrate:generate': 'write a migration file from the schema files',
26
+ 'db:migrate:apply': 'apply pending migrations',
27
+ 'db:migrate:rollback': 'roll a migration back',
28
+ 'db:migrate:status': 'list schema models, database tables and migration state',
29
+ 'db:cleanup': 'drop tables and foreign keys that are no longer in the schema',
30
+ };
31
+ /**
32
+ * Why a command cannot run against this provider, or null when it can.
33
+ *
34
+ * Returns the message instead of printing it and exiting, so this stays a plain
35
+ * function: it is shared by four command files, and a module that calls
36
+ * `process.exit` cannot be imported outside a Node CLI — nor unit tested for the
37
+ * thing it exists to say.
38
+ */
39
+ function sqlServerOnlyError(provider, command) {
40
+ if (provider === 'mssql')
41
+ return null;
42
+ const what = exports.SQL_SERVER_ONLY_COMMANDS[command] ?? 'run';
43
+ return (`❌ ${command} only supports SQL Server, but the connection string selects ` +
44
+ `${field_types_1.PROVIDER_LABELS[provider]}.\n` +
45
+ ` ${command} ${what} using sys.* catalog views and T-SQL, so pointing it at ` +
46
+ `${field_types_1.PROVIDER_LABELS[provider]} would run the wrong SQL.\n` +
47
+ ' Use a SQL Server connection string, or generate and apply the schema with ' +
48
+ `${field_types_1.PROVIDER_LABELS[provider]}'s own tooling.`);
49
+ }
50
+ //# sourceMappingURL=provider-support.js.map
@@ -1,3 +1,3 @@
1
- #!/usr/bin/env tsx
2
- export {};
1
+ #!/usr/bin/env tsx
2
+ export {};
3
3
  //# sourceMappingURL=release-cli.d.ts.map
@@ -1,116 +1,116 @@
1
- #!/usr/bin/env tsx
2
- "use strict";
3
- var __importDefault = (this && this.__importDefault) || function (mod) {
4
- return (mod && mod.__esModule) ? mod : { "default": mod };
5
- };
6
- Object.defineProperty(exports, "__esModule", { value: true });
7
- const child_process_1 = require("child_process");
8
- const fs_1 = __importDefault(require("fs"));
9
- const path_1 = __importDefault(require("path"));
10
- function run(cmd, allowFail = false) {
11
- try {
12
- return (0, child_process_1.execSync)(cmd, { encoding: 'utf8' }).trim();
13
- }
14
- catch (error) {
15
- if (allowFail)
16
- return error.stdout?.toString()?.trim() || '';
17
- throw error;
18
- }
19
- }
20
- function inferComponent(file) {
21
- const normalized = file.replace('\\', '/').toLowerCase();
22
- if (normalized.includes('/generator/'))
23
- return 'generator';
24
- if (normalized.includes('/an5client/'))
25
- return 'client';
26
- if (normalized.includes('/an5schema/'))
27
- return 'schema';
28
- if (normalized.includes('/.github/'))
29
- return 'ci';
30
- if (normalized.includes('package.json') || normalized.includes('tsconfig'))
31
- return 'build';
32
- if (normalized.includes('.md'))
33
- return 'docs';
34
- return 'misc';
35
- }
36
- function collectChanges() {
37
- const status = run('git status --porcelain');
38
- const files = status
39
- .split('\n')
40
- .map(l => l.trim())
41
- .filter(Boolean)
42
- .map(l => l.slice(3).trim())
43
- .filter(Boolean);
44
- const groups = new Map();
45
- for (const file of files) {
46
- const component = inferComponent(file);
47
- if (!groups.has(component)) {
48
- groups.set(component, { component, files: [], summary: '' });
49
- }
50
- groups.get(component).files.push(file);
51
- }
52
- return Array.from(groups.values()).map(group => ({
53
- ...group,
54
- summary: `${group.component} updates`
55
- }));
56
- }
57
- function generateChangelog(groups) {
58
- const lines = ['# Changelog', ''];
59
- for (const group of groups) {
60
- lines.push(`## ${group.component}`);
61
- lines.push(`- ${group.summary}`);
62
- for (const file of group.files) {
63
- lines.push(` - ${file}`);
64
- }
65
- lines.push('');
66
- }
67
- return lines.join('\n').trim();
68
- }
69
- async function maybeUseLLM(summary) {
70
- let apiKey = '';
71
- try {
72
- const configPath = require('path').join(__dirname, '..', '..', 'an5Adapters', 'typescript', 'config');
73
- const mod = require(configPath);
74
- if (mod?.getLlmConfig) {
75
- const db = await mod.getLlmConfig();
76
- if (db)
77
- apiKey = db.apiKey;
78
- }
79
- }
80
- catch { }
81
- if (!apiKey)
82
- return summary;
83
- try {
84
- const prompt = `Summarize these changes into one concise release note in English:\n${summary}`;
85
- const payload = JSON.stringify({ model: 'gpt-4o-mini', messages: [{ role: 'system', content: 'You are a helpful release note writer.' }, { role: 'user', content: prompt }] });
86
- const response = run(`curl -s https://api.openai.com/v1/chat/completions -H "Authorization: Bearer ${apiKey}" -H "Content-Type: application/json" -d '${payload}'`, true);
87
- const parsed = JSON.parse(response);
88
- return parsed.choices?.[0]?.message?.content?.trim() || summary;
89
- }
90
- catch {
91
- return summary;
92
- }
93
- }
94
- async function main() {
95
- const groups = collectChanges();
96
- if (groups.length === 0) {
97
- console.log('No modified files detected.');
98
- process.exit(0);
99
- }
100
- const changelog = generateChangelog(groups);
101
- const outputPath = path_1.default.resolve(process.cwd(), 'CHANGELOG.md');
102
- fs_1.default.writeFileSync(outputPath, changelog + '\n');
103
- const summary = groups.map(g => `${g.component}: ${g.files.join(', ')}`).join('\n');
104
- const llmSummary = await maybeUseLLM(summary);
105
- console.log('\nGenerated changelog:\n');
106
- console.log(changelog);
107
- console.log('\nLLM summary:\n');
108
- console.log(llmSummary);
109
- const message = groups.map(g => g.component).join(', ');
110
- const commitMessage = `chore: update ${message}`;
111
- run(`git add -A`);
112
- run(`git commit -m "${commitMessage}"`, true);
113
- console.log(`\nCommitted with: ${commitMessage}`);
114
- }
115
- main().catch(console.error);
1
+ #!/usr/bin/env tsx
2
+ "use strict";
3
+ var __importDefault = (this && this.__importDefault) || function (mod) {
4
+ return (mod && mod.__esModule) ? mod : { "default": mod };
5
+ };
6
+ Object.defineProperty(exports, "__esModule", { value: true });
7
+ const child_process_1 = require("child_process");
8
+ const fs_1 = __importDefault(require("fs"));
9
+ const path_1 = __importDefault(require("path"));
10
+ function run(cmd, allowFail = false) {
11
+ try {
12
+ return (0, child_process_1.execSync)(cmd, { encoding: 'utf8' }).trim();
13
+ }
14
+ catch (error) {
15
+ if (allowFail)
16
+ return error.stdout?.toString()?.trim() || '';
17
+ throw error;
18
+ }
19
+ }
20
+ function inferComponent(file) {
21
+ const normalized = file.replace('\\', '/').toLowerCase();
22
+ if (normalized.includes('/generator/'))
23
+ return 'generator';
24
+ if (normalized.includes('/an5client/'))
25
+ return 'client';
26
+ if (normalized.includes('/an5schema/'))
27
+ return 'schema';
28
+ if (normalized.includes('/.github/'))
29
+ return 'ci';
30
+ if (normalized.includes('package.json') || normalized.includes('tsconfig'))
31
+ return 'build';
32
+ if (normalized.includes('.md'))
33
+ return 'docs';
34
+ return 'misc';
35
+ }
36
+ function collectChanges() {
37
+ const status = run('git status --porcelain');
38
+ const files = status
39
+ .split('\n')
40
+ .map(l => l.trim())
41
+ .filter(Boolean)
42
+ .map(l => l.slice(3).trim())
43
+ .filter(Boolean);
44
+ const groups = new Map();
45
+ for (const file of files) {
46
+ const component = inferComponent(file);
47
+ if (!groups.has(component)) {
48
+ groups.set(component, { component, files: [], summary: '' });
49
+ }
50
+ groups.get(component).files.push(file);
51
+ }
52
+ return Array.from(groups.values()).map(group => ({
53
+ ...group,
54
+ summary: `${group.component} updates`
55
+ }));
56
+ }
57
+ function generateChangelog(groups) {
58
+ const lines = ['# Changelog', ''];
59
+ for (const group of groups) {
60
+ lines.push(`## ${group.component}`);
61
+ lines.push(`- ${group.summary}`);
62
+ for (const file of group.files) {
63
+ lines.push(` - ${file}`);
64
+ }
65
+ lines.push('');
66
+ }
67
+ return lines.join('\n').trim();
68
+ }
69
+ async function maybeUseLLM(summary) {
70
+ let apiKey = '';
71
+ try {
72
+ const configPath = require('path').join(__dirname, '..', '..', 'an5Adapters', 'typescript', 'config');
73
+ const mod = require(configPath);
74
+ if (mod?.getLlmConfig) {
75
+ const db = await mod.getLlmConfig();
76
+ if (db)
77
+ apiKey = db.apiKey;
78
+ }
79
+ }
80
+ catch { }
81
+ if (!apiKey)
82
+ return summary;
83
+ try {
84
+ const prompt = `Summarize these changes into one concise release note in English:\n${summary}`;
85
+ const payload = JSON.stringify({ model: 'gpt-4o-mini', messages: [{ role: 'system', content: 'You are a helpful release note writer.' }, { role: 'user', content: prompt }] });
86
+ const response = run(`curl -s https://api.openai.com/v1/chat/completions -H "Authorization: Bearer ${apiKey}" -H "Content-Type: application/json" -d '${payload}'`, true);
87
+ const parsed = JSON.parse(response);
88
+ return parsed.choices?.[0]?.message?.content?.trim() || summary;
89
+ }
90
+ catch {
91
+ return summary;
92
+ }
93
+ }
94
+ async function main() {
95
+ const groups = collectChanges();
96
+ if (groups.length === 0) {
97
+ console.log('No modified files detected.');
98
+ process.exit(0);
99
+ }
100
+ const changelog = generateChangelog(groups);
101
+ const outputPath = path_1.default.resolve(process.cwd(), 'CHANGELOG.md');
102
+ fs_1.default.writeFileSync(outputPath, changelog + '\n');
103
+ const summary = groups.map(g => `${g.component}: ${g.files.join(', ')}`).join('\n');
104
+ const llmSummary = await maybeUseLLM(summary);
105
+ console.log('\nGenerated changelog:\n');
106
+ console.log(changelog);
107
+ console.log('\nLLM summary:\n');
108
+ console.log(llmSummary);
109
+ const message = groups.map(g => g.component).join(', ');
110
+ const commitMessage = `chore: update ${message}`;
111
+ run(`git add -A`);
112
+ run(`git commit -m "${commitMessage}"`, true);
113
+ console.log(`\nCommitted with: ${commitMessage}`);
114
+ }
115
+ main().catch(console.error);
116
116
  //# sourceMappingURL=release-cli.js.map
@@ -8,6 +8,8 @@ import type { RelationDef } from "./metadata";
8
8
  * user-supplied column / alias names.
9
9
  */
10
10
  export declare function quoteIdentifier(name: string): string;
11
+ /** Quotes one-part or multipart SQL Server table identifiers safely. */
12
+ export declare function quoteTableIdentifier(name: string): string;
11
13
  /** Sanitizes a parameter name to a safe T-SQL identifier fragment. */
12
14
  export declare function sanitizeParamName(name: string): string;
13
15
  /** Validates a direction token, defaulting to ASC. */
@@ -20,7 +22,7 @@ export interface QueryContext {
20
22
  }
21
23
  export declare function buildOrderBy(orderBy: any): string;
22
24
  /**
23
- * Recursively builds a WHERE clause from a Prisma-style filter object.
25
+ * Recursively builds a WHERE clause from a structured filter object.
24
26
  *
25
27
  * Values are always bound as parameters (`@name`); only identifiers are
26
28
  * interpolated directly, and they are run through {@link quoteIdentifier}.
package/dist/sql-utils.js CHANGED
@@ -1,6 +1,7 @@
1
1
  "use strict";
2
2
  Object.defineProperty(exports, "__esModule", { value: true });
3
3
  exports.quoteIdentifier = quoteIdentifier;
4
+ exports.quoteTableIdentifier = quoteTableIdentifier;
4
5
  exports.sanitizeParamName = sanitizeParamName;
5
6
  exports.normalizeSortDirection = normalizeSortDirection;
6
7
  exports.toNonNegativeInt = toNonNegativeInt;
@@ -18,6 +19,48 @@ function quoteIdentifier(name) {
18
19
  const s = String(name);
19
20
  return `[${s.replace(/\]/g, "]]")}]`;
20
21
  }
22
+ function unquoteBracketIdentifier(part) {
23
+ const trimmed = String(part).trim();
24
+ if (trimmed.startsWith("[") && trimmed.endsWith("]")) {
25
+ return trimmed.slice(1, -1).replace(/\]\]/g, "]");
26
+ }
27
+ return trimmed;
28
+ }
29
+ function splitMultipartIdentifier(name) {
30
+ const parts = [];
31
+ let current = "";
32
+ let inBracket = false;
33
+ const input = String(name).trim();
34
+ for (let i = 0; i < input.length; i += 1) {
35
+ const ch = input[i];
36
+ if (ch === "[")
37
+ inBracket = true;
38
+ if (ch === "]") {
39
+ if (input[i + 1] === "]") {
40
+ current += "]]";
41
+ i += 1;
42
+ continue;
43
+ }
44
+ inBracket = false;
45
+ }
46
+ if (ch === "." && !inBracket) {
47
+ if (current.trim())
48
+ parts.push(current.trim());
49
+ current = "";
50
+ continue;
51
+ }
52
+ current += ch;
53
+ }
54
+ if (current.trim())
55
+ parts.push(current.trim());
56
+ return parts.length > 0 ? parts : [input];
57
+ }
58
+ /** Quotes one-part or multipart SQL Server table identifiers safely. */
59
+ function quoteTableIdentifier(name) {
60
+ return splitMultipartIdentifier(name)
61
+ .map(part => quoteIdentifier(unquoteBracketIdentifier(part)))
62
+ .join(".");
63
+ }
21
64
  /** Sanitizes a parameter name to a safe T-SQL identifier fragment. */
22
65
  function sanitizeParamName(name) {
23
66
  const s = String(name);
@@ -34,7 +77,7 @@ function toNonNegativeInt(value, fallback = 0) {
34
77
  const n = Number.parseInt(String(value), 10);
35
78
  return Number.isFinite(n) && n >= 0 ? n : fallback;
36
79
  }
37
- const OPERATOR_KEYS = ["in", "notIn", "contains", "startsWith", "endsWith", "not", "gte", "lte", "gt", "lt"];
80
+ const OPERATOR_KEYS = ["equals", "in", "notIn", "contains", "startsWith", "endsWith", "not", "gte", "lte", "gt", "lt"];
38
81
  function isOperatorValue(value) {
39
82
  if (!value || typeof value !== "object" || value instanceof Date)
40
83
  return false;
@@ -55,7 +98,7 @@ function buildOrderBy(orderBy) {
55
98
  return orderClauses.length > 0 ? ` ORDER BY ${orderClauses.join(", ")}` : "";
56
99
  }
57
100
  /**
58
- * Recursively builds a WHERE clause from a Prisma-style filter object.
101
+ * Recursively builds a WHERE clause from a structured filter object.
59
102
  *
60
103
  * Values are always bound as parameters (`@name`); only identifiers are
61
104
  * interpolated directly, and they are run through {@link quoteIdentifier}.
@@ -83,18 +126,27 @@ function parseWhere(modelName, where, params, prefix = "", ctx = {}) {
83
126
  conditions.push(`(${filtered.join(" OR ")})`);
84
127
  }
85
128
  }
86
- else if (key === "AND" && Array.isArray(value)) {
87
- const andConditions = value.map((subWhere, idx) => parseWhere(modelName, subWhere, params, `${prefix}and_${idx}_`, ctx));
129
+ else if (key === "AND") {
130
+ const andItems = Array.isArray(value) ? value : [value];
131
+ const andConditions = andItems.map((subWhere, idx) => parseWhere(modelName, subWhere, params, `${prefix}and_${idx}_`, ctx));
88
132
  const filtered = andConditions.filter(Boolean);
89
133
  if (filtered.length > 0) {
90
134
  conditions.push(`(${filtered.join(" AND ")})`);
91
135
  }
92
136
  }
137
+ else if (key === "NOT") {
138
+ const notItems = Array.isArray(value) ? value : [value];
139
+ const notConditions = notItems.map((subWhere, idx) => parseWhere(modelName, subWhere, params, `${prefix}not_${idx}_`, ctx));
140
+ const filtered = notConditions.filter(Boolean);
141
+ if (filtered.length > 0) {
142
+ conditions.push(`NOT (${filtered.join(" AND ")})`);
143
+ }
144
+ }
93
145
  else {
94
146
  const modelRelations = relationMap[modelName];
95
147
  const relation = modelRelations?.[key];
96
148
  if (relation) {
97
- const relationTable = modelToTable[relation.modelName] || relation.modelName;
149
+ const relationTable = quoteTableIdentifier(modelToTable[relation.modelName] || relation.modelName);
98
150
  const subParams = {};
99
151
  let subWhere = value;
100
152
  let op = "some";
@@ -114,26 +166,23 @@ function parseWhere(modelName, where, params, prefix = "", ctx = {}) {
114
166
  }
115
167
  const subWhereSql = parseWhere(relation.modelName, subWhere, subParams, `${prefix}${key}_`, ctx);
116
168
  Object.assign(params, subParams);
117
- if (subWhereSql) {
118
- const fk = quoteIdentifier(relation.foreignKey);
119
- const lk = quoteIdentifier(relation.localKey);
120
- if (relation.relationType === "one") {
121
- if (op === "none") {
122
- conditions.push(`${fk} NOT IN (SELECT ${lk} FROM ${relationTable} WITH (NOLOCK) WHERE ${subWhereSql})`);
123
- }
124
- else {
125
- conditions.push(`${fk} IN (SELECT ${lk} FROM ${relationTable} WITH (NOLOCK) WHERE ${subWhereSql})`);
126
- }
169
+ const fk = quoteIdentifier(relation.foreignKey);
170
+ const lk = quoteIdentifier(relation.localKey);
171
+ const outerKey = relation.relationType === "one" ? fk : lk;
172
+ const innerKey = relation.relationType === "one" ? lk : fk;
173
+ if (op === "every") {
174
+ if (subWhereSql) {
175
+ conditions.push(`${outerKey} NOT IN (SELECT ${innerKey} FROM ${relationTable} WITH (NOLOCK) WHERE NOT (${subWhereSql}))`);
127
176
  }
128
177
  else {
129
- if (op === "none") {
130
- conditions.push(`${lk} NOT IN (SELECT ${fk} FROM ${relationTable} WITH (NOLOCK) WHERE ${subWhereSql})`);
131
- }
132
- else {
133
- conditions.push(`${lk} IN (SELECT ${fk} FROM ${relationTable} WITH (NOLOCK) WHERE ${subWhereSql})`);
134
- }
178
+ conditions.push("1 = 1");
135
179
  }
136
180
  }
181
+ else {
182
+ const relationPredicate = subWhereSql ? ` WHERE ${subWhereSql}` : "";
183
+ const operator = op === "none" ? "NOT IN" : "IN";
184
+ conditions.push(`${outerKey} ${operator} (SELECT ${innerKey} FROM ${relationTable} WITH (NOLOCK)${relationPredicate})`);
185
+ }
137
186
  }
138
187
  else {
139
188
  const col = quoteIdentifier(key);
@@ -141,7 +190,16 @@ function parseWhere(modelName, where, params, prefix = "", ctx = {}) {
141
190
  if (value && typeof value === "object" && !(value instanceof Date)) {
142
191
  const ops = Object.entries(value);
143
192
  for (const [op, opVal] of ops) {
144
- if (op === "in" && Array.isArray(opVal)) {
193
+ if (op === "equals") {
194
+ if (opVal === null) {
195
+ conditions.push(`${col} IS NULL`);
196
+ }
197
+ else {
198
+ conditions.push(`${col} = @${paramName}_equals`);
199
+ params[`${paramName}_equals`] = opVal;
200
+ }
201
+ }
202
+ else if (op === "in" && Array.isArray(opVal)) {
145
203
  if (opVal.length === 0) {
146
204
  conditions.push("1 = 0");
147
205
  }
@@ -182,8 +240,20 @@ function parseWhere(modelName, where, params, prefix = "", ctx = {}) {
182
240
  params[`${paramName}_endsWith`] = `%${opVal}`;
183
241
  }
184
242
  else if (op === "not") {
185
- conditions.push(`${col} <> @${paramName}_not`);
186
- params[`${paramName}_not`] = opVal;
243
+ if (opVal === null) {
244
+ conditions.push(`${col} IS NOT NULL`);
245
+ }
246
+ else if (opVal && typeof opVal === "object" && !(opVal instanceof Date) && !Array.isArray(opVal)) {
247
+ const nestedParams = {};
248
+ const nestedSql = parseWhere(modelName, { [key]: opVal }, nestedParams, `${prefix}${key}_not_`, ctx);
249
+ Object.assign(params, nestedParams);
250
+ if (nestedSql)
251
+ conditions.push(`NOT (${nestedSql})`);
252
+ }
253
+ else {
254
+ conditions.push(`${col} <> @${paramName}_not`);
255
+ params[`${paramName}_not`] = opVal;
256
+ }
187
257
  }
188
258
  else if (op === "gte") {
189
259
  conditions.push(`${col} >= @${paramName}_gte`);
@@ -0,0 +1,45 @@
1
+ /**
2
+ * Side-effect-free entrypoint for programmatic use of the code generator.
3
+ *
4
+ * Importing `./index` runs the CLI (`main()`), so tooling that wants to drive
5
+ * the generator in-process — such as the an5Agent `generateClientCode` tool —
6
+ * imports this module instead.
7
+ *
8
+ * The provider is optional and defaults to SQL Server. Pass one derived from
9
+ * the connection string when the schema targets another database, so field
10
+ * types are checked against that database's types — `providerForProject` reads
11
+ * it from the config file for callers that only know the project directory:
12
+ *
13
+ * ```ts
14
+ * import { SchemaParser, RustGenerator, providerForProject } from '@an5/orm/generator';
15
+ *
16
+ * const provider = providerForProject(__dirname);
17
+ * const models = await new SchemaParser('an5Schema', provider).parse();
18
+ * new RustGenerator('an5Client/rust').generate(models);
19
+ * ```
20
+ */
21
+ export { SchemaParser, sqlTypeToTs } from './parser';
22
+ export { CodeGenerator } from './code-generator';
23
+ export { MetadataGenerator } from './metadata-generator';
24
+ export { PythonGenerator } from './python-generator';
25
+ export { DotnetGenerator } from './dotnet-generator';
26
+ export { GolangGenerator } from './golang-generator';
27
+ export { RustGenerator } from './rust-generator';
28
+ export { detectProvider, providerFromConfig, providerForProject } from './config';
29
+ export { dialectFor, defaultClause, sqlLiteral } from './dialect';
30
+ export { fieldKind } from './type-kinds';
31
+ export type { FieldKind } from './type-kinds';
32
+ export type { ColumnSpec, PushDialect } from './dialect';
33
+ export {
34
+ DEFAULT_PROVIDER,
35
+ FieldTypeError,
36
+ PROVIDERS,
37
+ PROVIDER_FIELD_TYPES,
38
+ PROVIDER_LABELS,
39
+ defaultSqlTypeForTs,
40
+ fieldTypesFor,
41
+ resolveFieldType,
42
+ unknownFieldTypeMessage,
43
+ } from './field-types';
44
+ export type { FieldTypeIssue, Provider, TsType } from './field-types';
45
+ export type { Model, Field, Relation } from './types';