@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.
- package/README.md +174 -175
- package/dist/an5Metadata.js +64 -0
- package/dist/an5Orm.d.ts +92 -3
- package/dist/an5Orm.js +697 -206
- package/dist/config.d.ts +9 -1
- package/dist/config.js +13 -18
- package/dist/errors.d.ts +1 -1
- package/dist/errors.js +1 -1
- package/dist/generator/src/api.d.ts +35 -0
- package/dist/generator/src/api.js +58 -0
- package/dist/generator/src/code-generator.d.ts +14 -13
- package/dist/generator/src/code-generator.js +249 -225
- package/dist/generator/src/config.d.ts +162 -0
- package/dist/generator/src/config.js +354 -0
- package/dist/generator/src/dialect.d.ts +88 -0
- package/dist/generator/src/dialect.js +318 -0
- package/dist/generator/src/dotnet-generator.d.ts +21 -11
- package/dist/generator/src/dotnet-generator.js +1053 -546
- package/dist/generator/src/field-types.d.ts +96 -0
- package/dist/generator/src/field-types.js +433 -0
- package/dist/generator/src/golang-generator.d.ts +36 -0
- package/dist/generator/src/golang-generator.js +1229 -0
- package/dist/generator/src/index.d.ts +1 -1
- package/dist/generator/src/index.js +137 -88
- package/dist/generator/src/metadata-generator.d.ts +11 -8
- package/dist/generator/src/metadata-generator.js +105 -90
- package/dist/generator/src/parser.d.ts +37 -11
- package/dist/generator/src/parser.js +208 -206
- package/dist/generator/src/push-apply.d.ts +37 -0
- package/dist/generator/src/push-apply.js +109 -0
- package/dist/generator/src/push-schema.d.ts +93 -0
- package/dist/generator/src/push-schema.js +189 -0
- package/dist/generator/src/python-generator.d.ts +37 -9
- package/dist/generator/src/python-generator.js +428 -105
- package/dist/generator/src/rust-generator.d.ts +38 -0
- package/dist/generator/src/rust-generator.js +971 -0
- package/dist/generator/src/suggest.d.ts +17 -0
- package/dist/generator/src/suggest.js +45 -0
- package/dist/generator/src/type-kinds.d.ts +27 -0
- package/dist/generator/src/type-kinds.js +72 -0
- package/dist/generator/src/types.d.ts +60 -27
- package/dist/generator/src/types.js +41 -2
- package/dist/index.d.ts +3 -2
- package/dist/index.js +19 -8
- package/dist/logger.js +1 -1
- package/dist/migration-core.d.ts +102 -0
- package/dist/migration-core.js +731 -0
- package/dist/orm.d.ts +147 -0
- package/dist/orm.js +1654 -0
- package/dist/provider-support.d.ts +27 -0
- package/dist/provider-support.js +50 -0
- package/dist/scripts/release-cli.d.ts +2 -2
- package/dist/scripts/release-cli.js +115 -115
- package/dist/sql-utils.d.ts +3 -1
- package/dist/sql-utils.js +94 -24
- package/generator/src/api.ts +45 -0
- package/generator/src/code-generator.ts +259 -230
- package/generator/src/config.ts +496 -0
- package/generator/src/dialect.ts +415 -0
- package/generator/src/dotnet-generator.ts +1071 -544
- package/generator/src/field-types.ts +473 -0
- package/generator/src/golang-generator.ts +1267 -0
- package/generator/src/index.ts +146 -116
- package/generator/src/metadata-generator.ts +116 -101
- package/generator/src/parser.ts +234 -202
- package/generator/src/push-apply.ts +148 -0
- package/generator/src/push-schema.ts +250 -0
- package/generator/src/python-generator.ts +480 -114
- package/generator/src/rust-generator.ts +974 -0
- package/generator/src/suggest.ts +41 -0
- package/generator/src/type-kinds.ts +78 -0
- package/generator/src/types.ts +77 -29
- 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
|
package/dist/sql-utils.d.ts
CHANGED
|
@@ -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
|
|
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
|
|
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"
|
|
87
|
-
const
|
|
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
|
-
|
|
118
|
-
|
|
119
|
-
|
|
120
|
-
|
|
121
|
-
|
|
122
|
-
|
|
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
|
-
|
|
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 === "
|
|
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
|
-
|
|
186
|
-
|
|
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';
|