@an5/orm 1.0.7 โ 1.0.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.
- package/README.md +97 -83
- package/dist/an5Metadata.d.ts +10 -0
- package/dist/an5Metadata.js +93 -0
- package/dist/an5Orm.d.ts +95 -3
- package/dist/an5Orm.js +808 -512
- package/dist/config.d.ts +2 -0
- package/dist/config.js +24 -0
- package/dist/errors.d.ts +14 -0
- package/dist/errors.js +16 -0
- package/dist/generator/src/code-generator.d.ts +1 -0
- package/dist/generator/src/code-generator.js +44 -20
- package/dist/generator/src/dotnet-generator.d.ts +2 -0
- package/dist/generator/src/dotnet-generator.js +332 -6
- package/dist/generator/src/index.js +18 -2
- package/dist/generator/src/metadata-generator.d.ts +4 -1
- package/dist/generator/src/metadata-generator.js +22 -8
- package/dist/generator/src/python-generator.d.ts +10 -0
- package/dist/generator/src/python-generator.js +242 -2
- package/dist/index.d.ts +6 -2
- package/dist/index.js +23 -8
- package/dist/logger.d.ts +53 -0
- package/dist/logger.js +117 -0
- package/dist/metadata.d.ts +24 -0
- package/dist/metadata.js +15 -0
- package/dist/migration-core.d.ts +87 -0
- package/dist/migration-core.js +692 -0
- package/dist/sql-utils.d.ts +31 -0
- package/dist/sql-utils.js +290 -0
- package/generator/src/code-generator.ts +45 -20
- package/generator/src/dotnet-generator.ts +344 -6
- package/generator/src/golang-generator.ts +1242 -0
- package/generator/src/index.ts +8 -1
- package/generator/src/metadata-generator.ts +29 -12
- package/generator/src/python-generator.ts +314 -4
- package/package.json +12 -10
- package/dist/cleanup.d.ts +0 -2
- package/dist/cleanup.js +0 -120
- package/dist/cli.js +0 -64
- package/dist/migrate.d.ts +0 -11
- package/dist/migrate.js +0 -301
- package/dist/pull.d.ts +0 -2
- package/dist/pull.js +0 -339
- package/dist/push.d.ts +0 -2
- package/dist/push.js +0 -281
- package/dist/seed.d.ts +0 -2
- package/dist/seed.js +0 -401
package/generator/src/index.ts
CHANGED
|
@@ -4,6 +4,7 @@ import { CodeGenerator } from './code-generator';
|
|
|
4
4
|
import { MetadataGenerator } from './metadata-generator';
|
|
5
5
|
import { PythonGenerator } from './python-generator';
|
|
6
6
|
import { DotnetGenerator } from './dotnet-generator';
|
|
7
|
+
import { GolangGenerator } from './golang-generator';
|
|
7
8
|
|
|
8
9
|
import fs from 'fs';
|
|
9
10
|
|
|
@@ -53,12 +54,14 @@ async function main() {
|
|
|
53
54
|
const outputMetadataPath = path.resolve(rootDir, config.outputs?.typescript?.metadataFile || 'an5Client/typescript/an5Metadata.ts');
|
|
54
55
|
const outputPythonMetadataPath = path.resolve(rootDir, config.outputs?.python?.metadataFile || 'an5Client/python/an5_metadata.py');
|
|
55
56
|
const outputDotnetDir = path.resolve(rootDir, config.outputs?.dotnet?.outputDir || 'an5Client/dotnet');
|
|
57
|
+
const outputGolangDir = path.resolve(rootDir, config.outputs?.golang?.outputDir || 'an5Client/golang');
|
|
56
58
|
|
|
57
59
|
console.log('๐ Starting ORM generation...');
|
|
58
60
|
|
|
59
61
|
try {
|
|
60
62
|
clearGeneratedFiles(outputTypesDir, '.ts');
|
|
61
63
|
clearGeneratedFiles(outputDotnetDir, '.cs');
|
|
64
|
+
clearGeneratedFiles(outputGolangDir, '.go');
|
|
62
65
|
if (fs.existsSync(outputMetadataPath)) {
|
|
63
66
|
fs.unlinkSync(outputMetadataPath);
|
|
64
67
|
}
|
|
@@ -88,12 +91,16 @@ async function main() {
|
|
|
88
91
|
}
|
|
89
92
|
const pythonGen = new PythonGenerator(outputPythonMetadataPath);
|
|
90
93
|
pythonGen.generate(models);
|
|
91
|
-
console.log(`โจ Generated Python metadata in ${
|
|
94
|
+
console.log(`โจ Generated Python metadata and client models in ${pythonDir}`);
|
|
92
95
|
|
|
93
96
|
const dotnetGen = new DotnetGenerator(outputDotnetDir);
|
|
94
97
|
dotnetGen.generate(models);
|
|
95
98
|
console.log(`โจ Generated .NET models in ${outputDotnetDir}`);
|
|
96
99
|
|
|
100
|
+
const golangGen = new GolangGenerator(outputGolangDir);
|
|
101
|
+
golangGen.generate(models);
|
|
102
|
+
console.log(`โจ Generated Golang models in ${outputGolangDir}`);
|
|
103
|
+
|
|
97
104
|
console.log('โ
ORM generation completed successfully.');
|
|
98
105
|
} catch (error) {
|
|
99
106
|
console.error('โ ORM generation failed:', error);
|
|
@@ -3,10 +3,14 @@ import path from 'path';
|
|
|
3
3
|
import { Model } from './types';
|
|
4
4
|
|
|
5
5
|
export class MetadataGenerator {
|
|
6
|
-
constructor(
|
|
6
|
+
constructor(
|
|
7
|
+
private outputPath: string,
|
|
8
|
+
private relationImport: string = 'import type { RelationDef } from "@an5/orm";'
|
|
9
|
+
) {}
|
|
7
10
|
|
|
8
11
|
public generate(models: Model[]) {
|
|
9
12
|
let metaContent = '// This file is auto-generated. Do not edit directly.\n\n';
|
|
13
|
+
metaContent += `${this.relationImport}\n\n`;
|
|
10
14
|
|
|
11
15
|
metaContent += 'export const modelToTable: Record<string, string> = {\n';
|
|
12
16
|
for (const model of models) {
|
|
@@ -37,8 +41,6 @@ export class MetadataGenerator {
|
|
|
37
41
|
}
|
|
38
42
|
metaContent += '};\n\n';
|
|
39
43
|
|
|
40
|
-
metaContent += 'export interface RelationDef {\n modelName: string;\n relationType: "many" | "one";\n foreignKey: string;\n localKey: string;\n}\n\n';
|
|
41
|
-
|
|
42
44
|
metaContent += 'export const relationMap: Record<string, Record<string, RelationDef>> = {\n';
|
|
43
45
|
for (const model of models) {
|
|
44
46
|
const props = this.getAllPropertyVariations(model.name);
|
|
@@ -59,28 +61,43 @@ export class MetadataGenerator {
|
|
|
59
61
|
|
|
60
62
|
private getAllPropertyVariations(modelName: string): string[] {
|
|
61
63
|
const variations = new Set<string>();
|
|
62
|
-
|
|
63
|
-
// 1.
|
|
64
|
+
|
|
65
|
+
// 1. PascalCase (User)
|
|
66
|
+
variations.add(modelName);
|
|
67
|
+
|
|
68
|
+
// 2. camelCase (user)
|
|
64
69
|
variations.add(this.toCamelCase(modelName));
|
|
65
70
|
|
|
66
|
-
//
|
|
67
|
-
|
|
68
|
-
|
|
71
|
+
// 3. Plural PascalCase (Users)
|
|
72
|
+
variations.add(modelName + 's');
|
|
73
|
+
|
|
74
|
+
// 4. Plural camelCase (users)
|
|
75
|
+
variations.add(this.toCamelCase(modelName) + 's');
|
|
76
|
+
|
|
77
|
+
// 5. snake_case (user, mc_server)
|
|
78
|
+
variations.add(this.toSnakeCase(modelName));
|
|
79
|
+
variations.add(this.toSnakeCase(modelName) + 's');
|
|
80
|
+
|
|
69
81
|
const acronyms = ['LLM', 'AI', 'MCP', 'IT', 'QC', 'HR', 'MR', 'WH', 'SSIS', 'API', 'URL', 'ID', 'JSON'];
|
|
70
|
-
|
|
71
82
|
for (const acronym of acronyms) {
|
|
72
83
|
if (modelName.startsWith(acronym)) {
|
|
73
|
-
// e.g. LLMProvider -> llmProvider
|
|
74
84
|
variations.add(acronym.toLowerCase() + modelName.slice(acronym.length));
|
|
75
|
-
|
|
76
|
-
// e.g. LLMProvider -> lLMProvider (Prisma style)
|
|
77
85
|
variations.add(acronym[0].toLowerCase() + acronym.slice(1) + modelName.slice(acronym.length));
|
|
86
|
+
variations.add(acronym.toLowerCase() + modelName.slice(acronym.length) + 's');
|
|
78
87
|
}
|
|
79
88
|
}
|
|
80
89
|
|
|
81
90
|
return Array.from(variations);
|
|
82
91
|
}
|
|
83
92
|
|
|
93
|
+
private toSnakeCase(str: string): string {
|
|
94
|
+
if (!str) return '';
|
|
95
|
+
return str
|
|
96
|
+
.replace(/([A-Z]+)([A-Z][a-z])/g, '$1_$2')
|
|
97
|
+
.replace(/([a-z\d])([A-Z])/g, '$1_$2')
|
|
98
|
+
.toLowerCase();
|
|
99
|
+
}
|
|
100
|
+
|
|
84
101
|
private formatFieldMetadata(field: Model['fields'][number]): string {
|
|
85
102
|
const entries = [
|
|
86
103
|
`ts: ${JSON.stringify(`${field.type}${field.isOptional ? '?' : ''}`)}`,
|
|
@@ -6,6 +6,120 @@ export class PythonGenerator {
|
|
|
6
6
|
constructor(private outputPath: string) {}
|
|
7
7
|
|
|
8
8
|
public generate(models: Model[]) {
|
|
9
|
+
const outputDir = path.dirname(this.outputPath);
|
|
10
|
+
if (!fs.existsSync(outputDir)) {
|
|
11
|
+
fs.mkdirSync(outputDir, { recursive: true });
|
|
12
|
+
}
|
|
13
|
+
|
|
14
|
+
// 1. Generate an5_metadata.py
|
|
15
|
+
this.generateMetadata(models);
|
|
16
|
+
|
|
17
|
+
// 2. Generate an5_models.py
|
|
18
|
+
this.generateModels(models, outputDir);
|
|
19
|
+
|
|
20
|
+
// 3. Generate an5_orm_types.py - type-safe ORM filter/args dataclasses
|
|
21
|
+
this.generateOrmTypes(models, outputDir);
|
|
22
|
+
|
|
23
|
+
// 4. Generate an5_client.py
|
|
24
|
+
this.generateClient(models, outputDir);
|
|
25
|
+
|
|
26
|
+
// 5. Generate __init__.py
|
|
27
|
+
this.generateInit(models, outputDir);
|
|
28
|
+
}
|
|
29
|
+
|
|
30
|
+
private getPyFilterType(fieldType: string): string {
|
|
31
|
+
const lower = fieldType.toLowerCase();
|
|
32
|
+
if (['datetime', 'datetime2', 'date', 'smalldatetime', 'datetimeoffset', 'timestamp'].includes(lower)) return 'DateTimeFilter';
|
|
33
|
+
if (['bool', 'boolean', 'bit'].includes(lower)) return 'BoolFilter';
|
|
34
|
+
if (['int', 'integer', 'smallint', 'tinyint', 'bigint', 'number'].includes(lower)) return 'IntFilter';
|
|
35
|
+
if (['float', 'real', 'double', 'decimal', 'numeric', 'money'].includes(lower)) return 'NumberFilter';
|
|
36
|
+
return 'StringFilter';
|
|
37
|
+
}
|
|
38
|
+
|
|
39
|
+
private generateOrmTypes(models: Model[], outputDir: string) {
|
|
40
|
+
let content = '# This file is auto-generated. Do not edit directly.\n';
|
|
41
|
+
content += '"""\nAN5 ORM typed filter/args dataclasses for type-safe queries.\n\nUsage example::\n\n db.user.find_many(where=UserWhereInput(name=StringFilter(contains="John")),\n order_by=UserOrderByInput(created_at="desc"),\n take=10)\n"""\n';
|
|
42
|
+
content += 'from __future__ import annotations\n';
|
|
43
|
+
content += 'from dataclasses import dataclass, field\n';
|
|
44
|
+
content += 'from typing import List, Optional, Any\n';
|
|
45
|
+
content += 'from datetime import datetime\n\n';
|
|
46
|
+
|
|
47
|
+
// Base filter types
|
|
48
|
+
content += '# โโโ Base filter types โโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโ\n\n';
|
|
49
|
+
content += `@dataclass\nclass StringFilter:\n """Type-safe filter for string fields."""\n equals: Optional[str] = None\n not_: Optional[str] = None\n contains: Optional[str] = None\n starts_with: Optional[str] = None\n ends_with: Optional[str] = None\n in_: Optional[List[str]] = None\n not_in: Optional[List[str]] = None\n gt: Optional[str] = None\n gte: Optional[str] = None\n lt: Optional[str] = None\n lte: Optional[str] = None\n\n`;
|
|
50
|
+
|
|
51
|
+
content += `@dataclass\nclass IntFilter:\n """Type-safe filter for integer fields."""\n equals: Optional[int] = None\n not_: Optional[int] = None\n in_: Optional[List[int]] = None\n not_in: Optional[List[int]] = None\n gt: Optional[int] = None\n gte: Optional[int] = None\n lt: Optional[int] = None\n lte: Optional[int] = None\n\n`;
|
|
52
|
+
|
|
53
|
+
content += `@dataclass\nclass NumberFilter:\n """Type-safe filter for float/decimal fields."""\n equals: Optional[float] = None\n not_: Optional[float] = None\n in_: Optional[List[float]] = None\n not_in: Optional[List[float]] = None\n gt: Optional[float] = None\n gte: Optional[float] = None\n lt: Optional[float] = None\n lte: Optional[float] = None\n\n`;
|
|
54
|
+
|
|
55
|
+
content += `@dataclass\nclass BoolFilter:\n """Type-safe filter for boolean fields."""\n equals: Optional[bool] = None\n\n`;
|
|
56
|
+
|
|
57
|
+
content += `@dataclass\nclass DateTimeFilter:\n """Type-safe filter for datetime fields."""\n equals: Optional[datetime] = None\n not_: Optional[datetime] = None\n in_: Optional[List[datetime]] = None\n not_in: Optional[List[datetime]] = None\n gt: Optional[datetime] = None\n gte: Optional[datetime] = None\n lt: Optional[datetime] = None\n lte: Optional[datetime] = None\n\n`;
|
|
58
|
+
|
|
59
|
+
// Per-model types
|
|
60
|
+
for (const model of models) {
|
|
61
|
+
const name = this.capitalize(model.name);
|
|
62
|
+
content += `# โโโ ${name} ORM Types โโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโ\n\n`;
|
|
63
|
+
|
|
64
|
+
// WhereInput
|
|
65
|
+
content += `@dataclass\nclass ${name}WhereInput:\n """Type-safe WHERE filter for ${name} queries."""\n`;
|
|
66
|
+
content += ` AND: Optional[List['${name}WhereInput']] = None\n`;
|
|
67
|
+
content += ` OR: Optional[List['${name}WhereInput']] = None\n`;
|
|
68
|
+
content += ` NOT: Optional['${name}WhereInput'] = None\n`;
|
|
69
|
+
for (const f of model.fields) {
|
|
70
|
+
const ft = this.getPyFilterType(f.type);
|
|
71
|
+
content += ` ${this.toSnakeCase(f.name)}: Optional[${ft}] = None\n`;
|
|
72
|
+
}
|
|
73
|
+
content += '\n';
|
|
74
|
+
|
|
75
|
+
// OrderByInput
|
|
76
|
+
content += `@dataclass\nclass ${name}OrderByInput:\n """Type-safe ORDER BY for ${name} queries. Value: 'asc' or 'desc'."""\n`;
|
|
77
|
+
for (const f of model.fields) {
|
|
78
|
+
content += ` ${this.toSnakeCase(f.name)}: Optional[str] = None\n`;
|
|
79
|
+
}
|
|
80
|
+
content += '\n';
|
|
81
|
+
|
|
82
|
+
// CreateInput
|
|
83
|
+
const requiredFields = model.fields.filter(f => !f.isId && !f.isOptional && !f.hasDefault);
|
|
84
|
+
const optionalFields = model.fields.filter(f => !f.isId && (f.isOptional || f.hasDefault));
|
|
85
|
+
content += `@dataclass\nclass ${name}CreateInput:\n """Typed data for creating a new ${name} record."""\n`;
|
|
86
|
+
for (const f of requiredFields) {
|
|
87
|
+
content += ` ${this.toSnakeCase(f.name)}: ${this.mapPyType(f.type)}\n`;
|
|
88
|
+
}
|
|
89
|
+
for (const f of optionalFields) {
|
|
90
|
+
content += ` ${this.toSnakeCase(f.name)}: Optional[${this.mapPyType(f.type)}] = None\n`;
|
|
91
|
+
}
|
|
92
|
+
if (requiredFields.length === 0 && optionalFields.length === 0) content += ' pass\n';
|
|
93
|
+
content += '\n';
|
|
94
|
+
|
|
95
|
+
// UpdateInput
|
|
96
|
+
const updateFields = model.fields.filter(f => !f.isId);
|
|
97
|
+
content += `@dataclass\nclass ${name}UpdateInput:\n """Typed data for updating an existing ${name} record."""\n`;
|
|
98
|
+
for (const f of updateFields) {
|
|
99
|
+
content += ` ${this.toSnakeCase(f.name)}: Optional[${this.mapPyType(f.type)}] = None\n`;
|
|
100
|
+
}
|
|
101
|
+
if (updateFields.length === 0) content += ' pass\n';
|
|
102
|
+
content += '\n';
|
|
103
|
+
|
|
104
|
+
// FindManyArgs
|
|
105
|
+
content += `@dataclass\nclass ${name}FindManyArgs:\n """ORM-style args for ${name}.find_many()."""\n`;
|
|
106
|
+
content += ` where: Optional[${name}WhereInput] = None\n`;
|
|
107
|
+
content += ` order_by: Optional[${name}OrderByInput] = None\n`;
|
|
108
|
+
content += ` take: Optional[int] = None\n`;
|
|
109
|
+
content += ` skip: int = 0\n`;
|
|
110
|
+
content += ` select: Optional[List[str]] = None\n\n`;
|
|
111
|
+
|
|
112
|
+
// FindFirstArgs
|
|
113
|
+
content += `@dataclass\nclass ${name}FindFirstArgs:\n """ORM-style args for ${name}.find_first()."""\n`;
|
|
114
|
+
content += ` where: Optional[${name}WhereInput] = None\n`;
|
|
115
|
+
content += ` order_by: Optional[${name}OrderByInput] = None\n`;
|
|
116
|
+
content += ` select: Optional[List[str]] = None\n\n`;
|
|
117
|
+
}
|
|
118
|
+
|
|
119
|
+
fs.writeFileSync(path.join(outputDir, 'an5_orm_types.py'), content);
|
|
120
|
+
}
|
|
121
|
+
|
|
122
|
+
private generateMetadata(models: Model[]) {
|
|
9
123
|
let pyContent = '# This file is auto-generated. Do not edit directly.\n\n';
|
|
10
124
|
|
|
11
125
|
// 1. MODEL_TO_TABLE
|
|
@@ -64,13 +178,204 @@ export class PythonGenerator {
|
|
|
64
178
|
fs.writeFileSync(this.outputPath, pyContent);
|
|
65
179
|
}
|
|
66
180
|
|
|
181
|
+
private mapPyType(fieldType: string): string {
|
|
182
|
+
const lower = fieldType.toLowerCase();
|
|
183
|
+
if (['int', 'integer', 'smallint', 'tinyint', 'bigint', 'number'].includes(lower)) {
|
|
184
|
+
return 'int';
|
|
185
|
+
}
|
|
186
|
+
if (['float', 'real', 'double', 'decimal', 'numeric', 'money'].includes(lower)) {
|
|
187
|
+
return 'float';
|
|
188
|
+
}
|
|
189
|
+
if (['bool', 'boolean', 'bit'].includes(lower)) {
|
|
190
|
+
return 'bool';
|
|
191
|
+
}
|
|
192
|
+
if (['datetime', 'datetime2', 'date', 'smalldatetime', 'datetimeoffset', 'timestamp'].includes(lower)) {
|
|
193
|
+
return 'datetime';
|
|
194
|
+
}
|
|
195
|
+
if (['bytes', 'binary', 'varbinary'].includes(lower)) {
|
|
196
|
+
return 'bytes';
|
|
197
|
+
}
|
|
198
|
+
return 'str';
|
|
199
|
+
}
|
|
200
|
+
|
|
201
|
+
private generateModels(models: Model[], outputDir: string) {
|
|
202
|
+
let content = '# This file is auto-generated. Do not edit directly.\n';
|
|
203
|
+
content += 'from dataclasses import dataclass, field\n';
|
|
204
|
+
content += 'from typing import Optional, List, Any, TypedDict\n';
|
|
205
|
+
content += 'from datetime import datetime\n\n';
|
|
206
|
+
|
|
207
|
+
for (const model of models) {
|
|
208
|
+
if (model.description) {
|
|
209
|
+
content += `"""${model.description}"""\n`;
|
|
210
|
+
}
|
|
211
|
+
content += `@dataclass\nclass ${model.name}:\n`;
|
|
212
|
+
|
|
213
|
+
if (model.fields.length === 0 && model.relations.length === 0) {
|
|
214
|
+
content += ' pass\n\n';
|
|
215
|
+
continue;
|
|
216
|
+
}
|
|
217
|
+
|
|
218
|
+
// Required fields first
|
|
219
|
+
const required = model.fields.filter(f => !f.isOptional && !f.hasDefault);
|
|
220
|
+
const optionals = model.fields.filter(f => f.isOptional || f.hasDefault);
|
|
221
|
+
|
|
222
|
+
for (const f of required) {
|
|
223
|
+
const pyType = this.mapPyType(f.type);
|
|
224
|
+
const snakeName = this.toSnakeCase(f.name);
|
|
225
|
+
content += ` ${snakeName}: ${pyType}\n`;
|
|
226
|
+
}
|
|
227
|
+
|
|
228
|
+
for (const f of optionals) {
|
|
229
|
+
const pyType = this.mapPyType(f.type);
|
|
230
|
+
const snakeName = this.toSnakeCase(f.name);
|
|
231
|
+
content += ` ${snakeName}: Optional[${pyType}] = None\n`;
|
|
232
|
+
}
|
|
233
|
+
|
|
234
|
+
// Relations
|
|
235
|
+
for (const rel of model.relations) {
|
|
236
|
+
const relName = this.toSnakeCase(rel.name);
|
|
237
|
+
if (rel.isArray) {
|
|
238
|
+
content += ` ${relName}: List[Any] = field(default_factory=list)\n`;
|
|
239
|
+
} else {
|
|
240
|
+
content += ` ${relName}: Optional[Any] = None\n`;
|
|
241
|
+
}
|
|
242
|
+
}
|
|
243
|
+
|
|
244
|
+
content += '\n';
|
|
245
|
+
|
|
246
|
+
// TypedDict row shape: mirrors what the runtime actually returns
|
|
247
|
+
// (plain dicts), so `AdapterTableClient["<Model>Row"]` delegates
|
|
248
|
+
// type-check. Required keys stay required via a total=True base;
|
|
249
|
+
// everything else is optional (total=False), keeping partial
|
|
250
|
+
// selects valid without extra dependencies.
|
|
251
|
+
const rowRequired = model.fields.filter(f => !f.isOptional && !f.hasDefault);
|
|
252
|
+
const rowOptional = model.fields.filter(f => f.isOptional || f.hasDefault);
|
|
253
|
+
content += `class _${model.name}Required(TypedDict):\n`;
|
|
254
|
+
content += ` """Required keys of a ${model.name} row."""\n`;
|
|
255
|
+
if (rowRequired.length === 0) {
|
|
256
|
+
content += ' pass\n';
|
|
257
|
+
}
|
|
258
|
+
for (const f of rowRequired) {
|
|
259
|
+
content += ` ${this.toSnakeCase(f.name)}: ${this.mapPyType(f.type)}\n`;
|
|
260
|
+
}
|
|
261
|
+
content += `\nclass ${model.name}Row(_${model.name}Required, total=False):\n`;
|
|
262
|
+
content += ` """Row shape returned for ${model.name} queries."""\n`;
|
|
263
|
+
if (rowOptional.length === 0 && model.relations.length === 0) {
|
|
264
|
+
content += ' pass\n';
|
|
265
|
+
}
|
|
266
|
+
for (const f of rowOptional) {
|
|
267
|
+
content += ` ${this.toSnakeCase(f.name)}: ${this.mapPyType(f.type)}\n`;
|
|
268
|
+
}
|
|
269
|
+
for (const rel of model.relations) {
|
|
270
|
+
const relName = this.toSnakeCase(rel.name);
|
|
271
|
+
content += rel.isArray ? ` ${relName}: List[Any]\n` : ` ${relName}: Any\n`;
|
|
272
|
+
}
|
|
273
|
+
content += '\n';
|
|
274
|
+
}
|
|
275
|
+
|
|
276
|
+
fs.writeFileSync(path.join(outputDir, 'an5_models.py'), content);
|
|
277
|
+
}
|
|
278
|
+
|
|
279
|
+
private generateClient(models: Model[], outputDir: string) {
|
|
280
|
+
let content = '# This file is auto-generated. Do not edit directly.\n';
|
|
281
|
+
content += 'import os\n';
|
|
282
|
+
content += 'from typing import Dict, List, Optional, Any, Callable, TYPE_CHECKING\n\n';
|
|
283
|
+
|
|
284
|
+
content += 'try:\n';
|
|
285
|
+
content += ' from an5_adapter import An5Adapter, AdapterTableClient, create_an5_adapter, set_adapter_metadata\n';
|
|
286
|
+
content += 'except ImportError:\n';
|
|
287
|
+
content += ' from .an5_adapter import An5Adapter, AdapterTableClient, create_an5_adapter, set_adapter_metadata\n\n';
|
|
288
|
+
|
|
289
|
+
content += 'try:\n';
|
|
290
|
+
content += ' from .an5_metadata import MODEL_TO_TABLE, MODEL_FIELDS\n';
|
|
291
|
+
content += 'except ImportError:\n';
|
|
292
|
+
content += ' from an5_metadata import MODEL_TO_TABLE, MODEL_FIELDS\n\n';
|
|
293
|
+
|
|
294
|
+
if (models.length > 0) {
|
|
295
|
+
const rowTypes = models.map(m => `${m.name}Row`).join(', ');
|
|
296
|
+
content += 'if TYPE_CHECKING:\n';
|
|
297
|
+
content += ' try:\n';
|
|
298
|
+
content += ` from an5_models import ${rowTypes}\n`;
|
|
299
|
+
content += ' except ImportError:\n';
|
|
300
|
+
content += ` from .an5_models import ${rowTypes}\n\n`;
|
|
301
|
+
}
|
|
302
|
+
|
|
303
|
+
content += 'class An5Client:\n';
|
|
304
|
+
content += ' """AN5 Python ORM Client - type-safe database access.\n\n';
|
|
305
|
+
content += ' Usage:\n';
|
|
306
|
+
content += ' db = An5Client()\n';
|
|
307
|
+
content += ' users = db.user.find_many(where={"name": {"contains": "John"}}, order_by={"created_at": "asc"}, take=10)\n';
|
|
308
|
+
content += ' user = db.user.find_first(where={"id": "abc"})\n';
|
|
309
|
+
content += ' new = db.user.create(data={"name": "Alice", "email": "alice@example.com"})\n';
|
|
310
|
+
content += ' """\n';
|
|
311
|
+
content += ' def __init__(self, connection_string: Optional[str] = None):\n';
|
|
312
|
+
content += ' conn_str = (connection_string or \n';
|
|
313
|
+
content += ' os.getenv("DATABASE_URL") or \n';
|
|
314
|
+
content += ' "Server=localhost;Database=master;Trusted_Connection=True;TrustServerCertificate=True;")\n';
|
|
315
|
+
content += ' set_adapter_metadata({"model_to_table": MODEL_TO_TABLE, "model_fields": MODEL_FIELDS})\n';
|
|
316
|
+
content += ' self.adapter: An5Adapter = create_an5_adapter(conn_str)\n\n';
|
|
317
|
+
|
|
318
|
+
for (const model of models) {
|
|
319
|
+
const propName = this.toSnakeCase(model.name) + 's';
|
|
320
|
+
const singleName = this.toSnakeCase(model.name);
|
|
321
|
+
content += ` client = AdapterTableClient(self.adapter, "${model.name}")\n`;
|
|
322
|
+
content += ` self.${model.name}: AdapterTableClient["${model.name}Row"] = client\n`;
|
|
323
|
+
content += ` self.${model.name}s: AdapterTableClient["${model.name}Row"] = client\n`;
|
|
324
|
+
content += ` self.${singleName}: AdapterTableClient["${model.name}Row"] = client\n`;
|
|
325
|
+
content += ` self.${propName}: AdapterTableClient["${model.name}Row"] = client\n`;
|
|
326
|
+
}
|
|
327
|
+
|
|
328
|
+
content += '\n';
|
|
329
|
+
content += ' def __getattr__(self, name: str) -> AdapterTableClient[Any]:\n';
|
|
330
|
+
content += ' return self.adapter.table(name)\n\n';
|
|
331
|
+
content += ' def query_raw(self, sql: str, *params) -> List[Dict]:\n';
|
|
332
|
+
content += ' return self.adapter.query_raw(sql, *params)\n\n';
|
|
333
|
+
content += ' def execute_raw(self, sql: str, *params) -> int:\n';
|
|
334
|
+
content += ' return self.adapter.execute_raw(sql, *params)\n\n';
|
|
335
|
+
content += ' def transaction(self, fn: Callable) -> Any:\n';
|
|
336
|
+
content += ' return self.adapter.transaction(fn)\n';
|
|
337
|
+
|
|
338
|
+
fs.writeFileSync(path.join(outputDir, 'an5_client.py'), content);
|
|
339
|
+
}
|
|
340
|
+
|
|
341
|
+
private generateInit(models: Model[], outputDir: string) {
|
|
342
|
+
let content = '# This file is auto-generated. Do not edit directly.\n';
|
|
343
|
+
content += 'from .an5_metadata import MODEL_TO_TABLE, MODEL_DESCRIPTIONS, MODEL_FIELDS, RELATION_MAP\n';
|
|
344
|
+
content += 'from .an5_models import *\n';
|
|
345
|
+
content += 'from .an5_orm_types import (\n';
|
|
346
|
+
content += ' StringFilter, IntFilter, NumberFilter, BoolFilter, DateTimeFilter,\n';
|
|
347
|
+
for (const m of models) {
|
|
348
|
+
const name = this.capitalize(m.name);
|
|
349
|
+
content += ` ${name}WhereInput, ${name}OrderByInput,\n`;
|
|
350
|
+
content += ` ${name}CreateInput, ${name}UpdateInput,\n`;
|
|
351
|
+
content += ` ${name}FindManyArgs, ${name}FindFirstArgs,\n`;
|
|
352
|
+
}
|
|
353
|
+
content += ')\n';
|
|
354
|
+
content += 'from .an5_client import An5Client\n\n';
|
|
355
|
+
content += '__all__ = [\n';
|
|
356
|
+
content += ' "MODEL_TO_TABLE",\n';
|
|
357
|
+
content += ' "MODEL_DESCRIPTIONS",\n';
|
|
358
|
+
content += ' "MODEL_FIELDS",\n';
|
|
359
|
+
content += ' "RELATION_MAP",\n';
|
|
360
|
+
content += ' "An5Client",\n';
|
|
361
|
+
content += ' "StringFilter", "IntFilter", "NumberFilter", "BoolFilter", "DateTimeFilter",\n';
|
|
362
|
+
|
|
363
|
+
for (const m of models) {
|
|
364
|
+
const name = this.capitalize(m.name);
|
|
365
|
+
content += ` "${m.name}",\n`;
|
|
366
|
+
content += ` "${name}WhereInput", "${name}OrderByInput",\n`;
|
|
367
|
+
content += ` "${name}CreateInput", "${name}UpdateInput",\n`;
|
|
368
|
+
content += ` "${name}FindManyArgs", "${name}FindFirstArgs",\n`;
|
|
369
|
+
}
|
|
370
|
+
content += ']\n';
|
|
371
|
+
|
|
372
|
+
fs.writeFileSync(path.join(outputDir, '__init__.py'), content);
|
|
373
|
+
}
|
|
374
|
+
|
|
67
375
|
private getAllPropertyVariations(modelName: string): string[] {
|
|
68
376
|
const variations = new Set<string>();
|
|
69
|
-
|
|
70
|
-
// 1. camelCase (User -> user, McpServer -> mcpServer)
|
|
377
|
+
|
|
71
378
|
variations.add(this.toCamelCase(modelName));
|
|
72
|
-
|
|
73
|
-
// 2. snake_case (User -> user, McpServer -> mcp_server)
|
|
74
379
|
variations.add(this.toSnakeCase(modelName));
|
|
75
380
|
|
|
76
381
|
const acronyms = ['LLM', 'AI', 'MCP', 'IT', 'QC', 'HR', 'MR', 'WH', 'SSIS', 'API', 'URL', 'ID', 'JSON'];
|
|
@@ -111,4 +416,9 @@ export class PythonGenerator {
|
|
|
111
416
|
.replace(/([a-z\d])([A-Z])/g, '$1_$2')
|
|
112
417
|
.toLowerCase();
|
|
113
418
|
}
|
|
419
|
+
|
|
420
|
+
private capitalize(str: string): string {
|
|
421
|
+
if (!str) return '';
|
|
422
|
+
return str[0].toUpperCase() + str.slice(1);
|
|
423
|
+
}
|
|
114
424
|
}
|
package/package.json
CHANGED
|
@@ -1,14 +1,10 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@an5/orm",
|
|
3
|
-
"version": "1.0.
|
|
4
|
-
"description": "The lightweight ORM for SQL Server with support for TypeScript, Python,
|
|
3
|
+
"version": "1.0.9",
|
|
4
|
+
"description": "The lightweight ORM for SQL Server with support for TypeScript, Python, .NET (C#), and Go.",
|
|
5
5
|
"license": "MIT",
|
|
6
6
|
"main": "dist/index.js",
|
|
7
7
|
"types": "dist/index.d.ts",
|
|
8
|
-
"bin": {
|
|
9
|
-
"an5": "./dist/cli.js",
|
|
10
|
-
"an5-orm": "./dist/cli.js"
|
|
11
|
-
},
|
|
12
8
|
"files": [
|
|
13
9
|
"dist/**/*.{js,d.ts}",
|
|
14
10
|
"generator/src/**/*.ts",
|
|
@@ -21,6 +17,9 @@
|
|
|
21
17
|
"url": "https://github.com/an5ORM/an5Orm.git"
|
|
22
18
|
},
|
|
23
19
|
"scripts": {
|
|
20
|
+
"build": "tsc -p tsconfig.json",
|
|
21
|
+
"pretest": "npm run build",
|
|
22
|
+
"prepack": "npm run build",
|
|
24
23
|
"generate": "npx tsx generator/src/index.ts",
|
|
25
24
|
"db:push": "npx tsx push.ts",
|
|
26
25
|
"db:pull": "npx tsx pull.ts",
|
|
@@ -29,15 +28,18 @@
|
|
|
29
28
|
"db:migrate": "npx tsx migrate.ts",
|
|
30
29
|
"db:migrate:diff": "npx tsx migrate.ts diff",
|
|
31
30
|
"db:migrate:generate": "npx tsx migrate.ts generate",
|
|
31
|
+
"db:migrate:apply": "npx tsx migrate.ts apply",
|
|
32
|
+
"db:migrate:rollback": "npx tsx migrate.ts rollback",
|
|
32
33
|
"db:migrate:status": "npx tsx migrate.ts status",
|
|
33
34
|
"release:cli": "npx tsx scripts/release-cli.ts",
|
|
34
35
|
"release": "node scripts/release.js",
|
|
35
|
-
"test": "node test/smoke.test.js",
|
|
36
|
+
"test": "node test/smoke.test.js && node test/unit.test.js",
|
|
36
37
|
"test:unit": "node test/unit.test.js",
|
|
37
|
-
"test:generator": "npm run generate && node test/generator.test.js"
|
|
38
|
+
"test:generator": "npm run generate && node test/generator.test.js",
|
|
39
|
+
"test:integration:live": "node test/live-db.integration.test.js"
|
|
38
40
|
},
|
|
39
41
|
"dependencies": {
|
|
40
|
-
"@an5/adapters": "^0.2.
|
|
42
|
+
"@an5/adapters": "^0.2.6",
|
|
41
43
|
"bcryptjs": "^2.4.3",
|
|
42
44
|
"dotenv": "^16.4.5",
|
|
43
45
|
"mssql": "^12.5.5"
|
|
@@ -47,7 +49,7 @@
|
|
|
47
49
|
"@types/mssql": "^9.1.8",
|
|
48
50
|
"@types/node": "^20.19.43",
|
|
49
51
|
"tsx": "^4.7.1",
|
|
50
|
-
"typescript": "^
|
|
52
|
+
"typescript": "^7.0.2"
|
|
51
53
|
},
|
|
52
54
|
"publishConfig": {
|
|
53
55
|
"access": "public"
|
package/dist/cleanup.d.ts
DELETED
package/dist/cleanup.js
DELETED
|
@@ -1,120 +0,0 @@
|
|
|
1
|
-
"use strict";
|
|
2
|
-
var __importDefault = (this && this.__importDefault) || function (mod) {
|
|
3
|
-
return (mod && mod.__esModule) ? mod : { "default": mod };
|
|
4
|
-
};
|
|
5
|
-
Object.defineProperty(exports, "__esModule", { value: true });
|
|
6
|
-
require("dotenv/config");
|
|
7
|
-
const an5_adapters_1 = require("@an5/adapters");
|
|
8
|
-
const fs_1 = __importDefault(require("fs"));
|
|
9
|
-
const path_1 = __importDefault(require("path"));
|
|
10
|
-
const rootDir = process.cwd();
|
|
11
|
-
let config = {};
|
|
12
|
-
try {
|
|
13
|
-
let configPath = path_1.default.join(rootDir, "an5Orm.config.js");
|
|
14
|
-
if (!fs_1.default.existsSync(configPath)) {
|
|
15
|
-
configPath = path_1.default.join(rootDir, "an5Orm.config.cjs");
|
|
16
|
-
}
|
|
17
|
-
if (fs_1.default.existsSync(configPath)) {
|
|
18
|
-
config = require(configPath);
|
|
19
|
-
}
|
|
20
|
-
}
|
|
21
|
-
catch (err) {
|
|
22
|
-
console.warn("โ ๏ธ Could not load config file in cleanup.ts, using defaults.");
|
|
23
|
-
}
|
|
24
|
-
const schemaDir = path_1.default.resolve(rootDir, config.schemaDir || "an5Schema");
|
|
25
|
-
let _adapter = null;
|
|
26
|
-
async function getDb() {
|
|
27
|
-
if (!_adapter) {
|
|
28
|
-
_adapter = new an5_adapters_1.An5Adapter({ connectionString: process.env.DATABASE_URL });
|
|
29
|
-
await _adapter.$connect();
|
|
30
|
-
}
|
|
31
|
-
return _adapter;
|
|
32
|
-
}
|
|
33
|
-
async function cleanup() {
|
|
34
|
-
console.log("๐งน Starting database cleanup...");
|
|
35
|
-
let schemaText = "";
|
|
36
|
-
if (fs_1.default.existsSync(schemaDir)) {
|
|
37
|
-
const files = fs_1.default.readdirSync(schemaDir).filter(f => f.endsWith(".an5"));
|
|
38
|
-
for (const file of files) {
|
|
39
|
-
schemaText += fs_1.default.readFileSync(path_1.default.join(schemaDir, file), "utf8") + "\n";
|
|
40
|
-
}
|
|
41
|
-
}
|
|
42
|
-
else {
|
|
43
|
-
const schemaPath = path_1.default.join(__dirname, "schema.an5");
|
|
44
|
-
if (fs_1.default.existsSync(schemaPath)) {
|
|
45
|
-
schemaText = fs_1.default.readFileSync(schemaPath, "utf8");
|
|
46
|
-
}
|
|
47
|
-
else {
|
|
48
|
-
console.error("No schema found!");
|
|
49
|
-
process.exit(1);
|
|
50
|
-
}
|
|
51
|
-
}
|
|
52
|
-
const lines = schemaText.split("\n");
|
|
53
|
-
const validTables = new Set();
|
|
54
|
-
for (let line of lines) {
|
|
55
|
-
line = line.trim();
|
|
56
|
-
if (line.startsWith("model ")) {
|
|
57
|
-
const modelName = line.split(" ")[1].replace("{", "").trim();
|
|
58
|
-
// Default table name is model name + 's' (lowercase)
|
|
59
|
-
validTables.add(modelName.toLowerCase() + "s");
|
|
60
|
-
}
|
|
61
|
-
if (line.startsWith("@@map")) {
|
|
62
|
-
const mapMatch = line.match(/@@map\("(.+)"\)/);
|
|
63
|
-
if (mapMatch) {
|
|
64
|
-
// The last model's default name should be replaced by the map name
|
|
65
|
-
// Since we add the default name first, we need to be careful.
|
|
66
|
-
// Actually, let's just add the map name.
|
|
67
|
-
validTables.add(mapMatch[1]);
|
|
68
|
-
}
|
|
69
|
-
}
|
|
70
|
-
}
|
|
71
|
-
// Also include the capitalization variations from an5Metadata if we can,
|
|
72
|
-
// but schema.an5 is the source of truth.
|
|
73
|
-
console.log("Valid tables from schema:", Array.from(validTables));
|
|
74
|
-
const dbTables = await (await getDb()).$queryRawUnsafe(`
|
|
75
|
-
SELECT name FROM sys.tables WHERE is_ms_shipped = 0
|
|
76
|
-
`);
|
|
77
|
-
const tablesToDrop = dbTables
|
|
78
|
-
.map(t => t.name)
|
|
79
|
-
.filter(name => !validTables.has(name) && !validTables.has(name.toLowerCase()));
|
|
80
|
-
if (tablesToDrop.length === 0) {
|
|
81
|
-
console.log("โ
No extra tables found. Database is clean.");
|
|
82
|
-
process.exit(0);
|
|
83
|
-
}
|
|
84
|
-
console.log("Tables to be DROPPED:", tablesToDrop);
|
|
85
|
-
for (const table of tablesToDrop) {
|
|
86
|
-
console.log(`Dropping table [${table}]...`);
|
|
87
|
-
try {
|
|
88
|
-
// We need to handle foreign key constraints.
|
|
89
|
-
// For a thorough cleanup, we might need to drop constraints first.
|
|
90
|
-
// But for now, we'll try a simple DROP TABLE.
|
|
91
|
-
await (await getDb()).$executeRawUnsafe(`DROP TABLE [${table}]`);
|
|
92
|
-
console.log(`โ
Dropped [${table}]`);
|
|
93
|
-
}
|
|
94
|
-
catch (err) {
|
|
95
|
-
console.error(`โ Failed to drop [${table}]: ${err.message}`);
|
|
96
|
-
if (err.message.includes("FOREIGN KEY constraint")) {
|
|
97
|
-
console.log(`Attempting to find and drop constraints for [${table}]...`);
|
|
98
|
-
const constraints = await (await getDb()).$queryRawUnsafe(`
|
|
99
|
-
SELECT fk.name AS constraint_name, OBJECT_NAME(fk.parent_object_id) AS table_name
|
|
100
|
-
FROM sys.foreign_keys AS fk
|
|
101
|
-
WHERE OBJECT_NAME(fk.referenced_object_id) = @p_0 OR OBJECT_NAME(fk.parent_object_id) = @p_0
|
|
102
|
-
`, table);
|
|
103
|
-
for (const c of constraints) {
|
|
104
|
-
console.log(`Dropping constraint [${c.constraint_name}] on [${c.table_name}]...`);
|
|
105
|
-
await (await getDb()).$executeRawUnsafe(`ALTER TABLE [${c.table_name}] DROP CONSTRAINT [${c.constraint_name}]`);
|
|
106
|
-
}
|
|
107
|
-
// Try dropping again
|
|
108
|
-
await (await getDb()).$executeRawUnsafe(`DROP TABLE [${table}]`);
|
|
109
|
-
console.log(`โ
Dropped [${table}] after removing constraints.`);
|
|
110
|
-
}
|
|
111
|
-
}
|
|
112
|
-
}
|
|
113
|
-
console.log("โ
Cleanup completed.");
|
|
114
|
-
process.exit(0);
|
|
115
|
-
}
|
|
116
|
-
cleanup().catch(err => {
|
|
117
|
-
console.error("โ Cleanup failed:", err);
|
|
118
|
-
process.exit(1);
|
|
119
|
-
});
|
|
120
|
-
//# sourceMappingURL=cleanup.js.map
|