@an5/orm 1.0.0 → 1.0.2
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 +24 -15
- package/dist/an5Orm.js +2 -2
- package/dist/cleanup.js +3 -3
- package/dist/cli.js +64 -0
- package/dist/generator/src/code-generator.d.ts +13 -0
- package/dist/generator/src/code-generator.js +225 -0
- package/dist/generator/src/dotnet-generator.d.ts +11 -0
- package/dist/generator/src/dotnet-generator.js +546 -0
- package/dist/generator/src/index.d.ts +1 -0
- package/dist/generator/src/index.js +85 -0
- package/dist/generator/src/metadata-generator.d.ts +8 -0
- package/dist/generator/src/metadata-generator.js +72 -0
- package/dist/generator/src/parser.d.ts +11 -0
- package/dist/generator/src/parser.js +206 -0
- package/dist/generator/src/python-generator.d.ts +9 -0
- package/dist/generator/src/python-generator.js +85 -0
- package/dist/generator/src/types.d.ts +27 -0
- package/dist/generator/src/types.js +2 -0
- package/dist/migrate.js +3 -3
- package/dist/pull.js +3 -3
- package/dist/push.js +95 -24
- package/generator/src/index.ts +1 -1
- package/package.json +6 -2
|
@@ -0,0 +1,72 @@
|
|
|
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
|
+
exports.MetadataGenerator = void 0;
|
|
7
|
+
const fs_1 = __importDefault(require("fs"));
|
|
8
|
+
class MetadataGenerator {
|
|
9
|
+
constructor(outputPath) {
|
|
10
|
+
this.outputPath = outputPath;
|
|
11
|
+
}
|
|
12
|
+
generate(models) {
|
|
13
|
+
let metaContent = '// This file is auto-generated. Do not edit directly.\n\n';
|
|
14
|
+
metaContent += 'export const modelToTable: Record<string, string> = {\n';
|
|
15
|
+
for (const model of models) {
|
|
16
|
+
const props = this.getAllPropertyVariations(model.name);
|
|
17
|
+
const fullTableName = `[${model.schemaName}].[${model.tableName}]`;
|
|
18
|
+
for (const prop of props) {
|
|
19
|
+
metaContent += ` ${prop}: "${fullTableName}",\n`;
|
|
20
|
+
}
|
|
21
|
+
}
|
|
22
|
+
metaContent += '};\n\n';
|
|
23
|
+
metaContent += 'export const modelFields: Record<string, Record<string, { ts: string; sql: string }>> = {\n';
|
|
24
|
+
for (const model of models) {
|
|
25
|
+
const props = this.getAllPropertyVariations(model.name);
|
|
26
|
+
const fieldsStr = `{ ${model.fields.map(f => `${f.name}: { ts: "${f.type}${f.isOptional ? '?' : ''}", sql: "${f.sqlType}" }`).join(', ')} }`;
|
|
27
|
+
for (const prop of props) {
|
|
28
|
+
metaContent += ` ${prop}: ${fieldsStr},\n`;
|
|
29
|
+
}
|
|
30
|
+
}
|
|
31
|
+
metaContent += '};\n\n';
|
|
32
|
+
metaContent += 'export interface RelationDef {\n modelName: string;\n relationType: "many" | "one";\n foreignKey: string;\n localKey: string;\n}\n\n';
|
|
33
|
+
metaContent += 'export const relationMap: Record<string, Record<string, RelationDef>> = {\n';
|
|
34
|
+
for (const model of models) {
|
|
35
|
+
const props = this.getAllPropertyVariations(model.name);
|
|
36
|
+
let relationsContent = ` {\n`;
|
|
37
|
+
for (const rel of model.relations) {
|
|
38
|
+
relationsContent += ` ${rel.name}: { modelName: "${this.toCamelCase(rel.type)}", relationType: "${rel.isArray ? 'many' : 'one'}", foreignKey: "${rel.foreignKey || 'id'}", localKey: "${rel.localKey || 'id'}" },\n`;
|
|
39
|
+
}
|
|
40
|
+
relationsContent += ` }`;
|
|
41
|
+
for (const prop of props) {
|
|
42
|
+
metaContent += ` ${prop}: ${relationsContent},\n`;
|
|
43
|
+
}
|
|
44
|
+
}
|
|
45
|
+
metaContent += '};\n';
|
|
46
|
+
fs_1.default.writeFileSync(this.outputPath, metaContent);
|
|
47
|
+
}
|
|
48
|
+
getAllPropertyVariations(modelName) {
|
|
49
|
+
const variations = new Set();
|
|
50
|
+
// 1. Standard camelCase (e.g. User -> user, McpServer -> mcpServer)
|
|
51
|
+
variations.add(this.toCamelCase(modelName));
|
|
52
|
+
// 2. Handle known acronyms at start (e.g. LLMProvider -> lLMProvider)
|
|
53
|
+
// This is already handled by toCamelCase if it only lowercases the first letter.
|
|
54
|
+
// But we might want 'llmProvider' as well.
|
|
55
|
+
const acronyms = ['LLM', 'AI', 'MCP', 'IT', 'QC', 'HR', 'MR', 'WH', 'SSIS', 'API', 'URL', 'ID', 'JSON'];
|
|
56
|
+
for (const acronym of acronyms) {
|
|
57
|
+
if (modelName.startsWith(acronym)) {
|
|
58
|
+
// e.g. LLMProvider -> llmProvider
|
|
59
|
+
variations.add(acronym.toLowerCase() + modelName.slice(acronym.length));
|
|
60
|
+
// e.g. LLMProvider -> lLMProvider (Prisma style)
|
|
61
|
+
variations.add(acronym[0].toLowerCase() + acronym.slice(1) + modelName.slice(acronym.length));
|
|
62
|
+
}
|
|
63
|
+
}
|
|
64
|
+
return Array.from(variations);
|
|
65
|
+
}
|
|
66
|
+
toCamelCase(str) {
|
|
67
|
+
if (!str)
|
|
68
|
+
return '';
|
|
69
|
+
return str[0].toLowerCase() + str.slice(1);
|
|
70
|
+
}
|
|
71
|
+
}
|
|
72
|
+
exports.MetadataGenerator = MetadataGenerator;
|
|
@@ -0,0 +1,11 @@
|
|
|
1
|
+
import { Model } from './types';
|
|
2
|
+
export declare function sqlTypeToTs(sqlType: string): string;
|
|
3
|
+
export declare class SchemaParser {
|
|
4
|
+
private schemaDir;
|
|
5
|
+
private schemaText;
|
|
6
|
+
constructor(schemaDir: string);
|
|
7
|
+
parse(): Promise<Model[]>;
|
|
8
|
+
private loadSchema;
|
|
9
|
+
private parseModelLine;
|
|
10
|
+
private postProcessRelations;
|
|
11
|
+
}
|
|
@@ -0,0 +1,206 @@
|
|
|
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
|
+
exports.SchemaParser = void 0;
|
|
7
|
+
exports.sqlTypeToTs = sqlTypeToTs;
|
|
8
|
+
const fs_1 = __importDefault(require("fs"));
|
|
9
|
+
const path_1 = __importDefault(require("path"));
|
|
10
|
+
// SQL Server type → TypeScript type mapping
|
|
11
|
+
const AN5_TO_TS = {
|
|
12
|
+
// String types
|
|
13
|
+
'NVARCHAR': 'string',
|
|
14
|
+
'VARCHAR': 'string',
|
|
15
|
+
'CHAR': 'string',
|
|
16
|
+
'NCHAR': 'string',
|
|
17
|
+
'TEXT': 'string',
|
|
18
|
+
'NTEXT': 'string',
|
|
19
|
+
'XML': 'string',
|
|
20
|
+
// Numeric types
|
|
21
|
+
'INT': 'number',
|
|
22
|
+
'SMALLINT': 'number',
|
|
23
|
+
'TINYINT': 'number',
|
|
24
|
+
'BIGINT': 'number | bigint',
|
|
25
|
+
'FLOAT': 'number',
|
|
26
|
+
'REAL': 'number',
|
|
27
|
+
'DECIMAL': 'number',
|
|
28
|
+
'NUMERIC': 'number',
|
|
29
|
+
'MONEY': 'number',
|
|
30
|
+
'SMALLMONEY': 'number',
|
|
31
|
+
// Boolean
|
|
32
|
+
'BIT': 'boolean',
|
|
33
|
+
// Date types
|
|
34
|
+
'DATETIME': 'Date',
|
|
35
|
+
'DATETIME2': 'Date',
|
|
36
|
+
'SMALLDATETIME': 'Date',
|
|
37
|
+
'DATE': 'Date',
|
|
38
|
+
'TIME': 'Date',
|
|
39
|
+
'DATETIMEOFFSET': 'Date',
|
|
40
|
+
// Binary types
|
|
41
|
+
'VARBINARY': 'Buffer',
|
|
42
|
+
'BINARY': 'Buffer',
|
|
43
|
+
'IMAGE': 'Buffer',
|
|
44
|
+
// Other
|
|
45
|
+
'UNIQUEIDENTIFIER': 'string',
|
|
46
|
+
'SQL_VARIANT': 'any',
|
|
47
|
+
'ROWVERSION': 'Buffer',
|
|
48
|
+
'HIERARCHYID': 'string',
|
|
49
|
+
'GEOGRAPHY': 'string',
|
|
50
|
+
'GEOMETRY': 'string',
|
|
51
|
+
'VECTOR': 'number[] | string',
|
|
52
|
+
};
|
|
53
|
+
// Parse base type from "NVARCHAR(255)" → "NVARCHAR"
|
|
54
|
+
function parseSqlType(raw) {
|
|
55
|
+
const match = raw.match(/^(\w+)(?:\((.+)\))?$/);
|
|
56
|
+
if (!match)
|
|
57
|
+
return { base: raw, params: '' };
|
|
58
|
+
return { base: match[1].toUpperCase(), params: match[2] || '' };
|
|
59
|
+
}
|
|
60
|
+
// Map SQL Server type to TypeScript type
|
|
61
|
+
function sqlTypeToTs(sqlType) {
|
|
62
|
+
const { base } = parseSqlType(sqlType);
|
|
63
|
+
return AN5_TO_TS[base] || 'any';
|
|
64
|
+
}
|
|
65
|
+
class SchemaParser {
|
|
66
|
+
constructor(schemaDir) {
|
|
67
|
+
this.schemaDir = schemaDir;
|
|
68
|
+
this.schemaText = '';
|
|
69
|
+
}
|
|
70
|
+
async parse() {
|
|
71
|
+
this.loadSchema();
|
|
72
|
+
const lines = this.schemaText.split('\n');
|
|
73
|
+
const models = [];
|
|
74
|
+
let currentModel = null;
|
|
75
|
+
for (let line of lines) {
|
|
76
|
+
line = line.trim();
|
|
77
|
+
if (!line || line.startsWith('//'))
|
|
78
|
+
continue;
|
|
79
|
+
const modelHeaderMatch = line.match(/^model\s+(\w+)\s*\{/);
|
|
80
|
+
if (modelHeaderMatch) {
|
|
81
|
+
const modelName = modelHeaderMatch[1];
|
|
82
|
+
currentModel = {
|
|
83
|
+
name: modelName,
|
|
84
|
+
tableName: modelName.toLowerCase() + 's',
|
|
85
|
+
schemaName: 'dbo',
|
|
86
|
+
fields: [],
|
|
87
|
+
relations: []
|
|
88
|
+
};
|
|
89
|
+
models.push(currentModel);
|
|
90
|
+
continue;
|
|
91
|
+
}
|
|
92
|
+
if (line === '}') {
|
|
93
|
+
currentModel = null;
|
|
94
|
+
continue;
|
|
95
|
+
}
|
|
96
|
+
if (currentModel) {
|
|
97
|
+
this.parseModelLine(line, currentModel);
|
|
98
|
+
}
|
|
99
|
+
}
|
|
100
|
+
this.postProcessRelations(models);
|
|
101
|
+
return models;
|
|
102
|
+
}
|
|
103
|
+
loadSchema() {
|
|
104
|
+
if (fs_1.default.existsSync(this.schemaDir)) {
|
|
105
|
+
const files = fs_1.default.readdirSync(this.schemaDir).filter(f => f.endsWith('.an5'));
|
|
106
|
+
for (const file of files) {
|
|
107
|
+
this.schemaText += fs_1.default.readFileSync(path_1.default.join(this.schemaDir, file), 'utf8') + '\n';
|
|
108
|
+
}
|
|
109
|
+
}
|
|
110
|
+
else {
|
|
111
|
+
throw new Error(`No schema directory found at ${this.schemaDir}`);
|
|
112
|
+
}
|
|
113
|
+
}
|
|
114
|
+
parseModelLine(line, model) {
|
|
115
|
+
if (line.startsWith('@@map')) {
|
|
116
|
+
const mapMatch = line.match(/@@map\("(.+)"\)/);
|
|
117
|
+
if (mapMatch)
|
|
118
|
+
model.tableName = mapMatch[1];
|
|
119
|
+
return;
|
|
120
|
+
}
|
|
121
|
+
if (line.startsWith('@@schema')) {
|
|
122
|
+
const schemaMatch = line.match(/@@schema\("(.+)"\)/);
|
|
123
|
+
if (schemaMatch)
|
|
124
|
+
model.schemaName = schemaMatch[1];
|
|
125
|
+
return;
|
|
126
|
+
}
|
|
127
|
+
if (line.startsWith('@@unique')) {
|
|
128
|
+
const uniqueMatch = line.match(/@@unique\(\[([\w,\s]+)\]\)/);
|
|
129
|
+
if (uniqueMatch) {
|
|
130
|
+
const fields = uniqueMatch[1].split(',').map(f => f.trim());
|
|
131
|
+
model.compoundUniques = model.compoundUniques || [];
|
|
132
|
+
model.compoundUniques.push(fields);
|
|
133
|
+
}
|
|
134
|
+
return;
|
|
135
|
+
}
|
|
136
|
+
if (line.startsWith('@@description')) {
|
|
137
|
+
const descMatch = line.match(/@@description\("(.+)"\)/);
|
|
138
|
+
if (descMatch)
|
|
139
|
+
model.description = descMatch[1];
|
|
140
|
+
return;
|
|
141
|
+
}
|
|
142
|
+
if (line.startsWith('@@'))
|
|
143
|
+
return;
|
|
144
|
+
const parts = line.split(/\s+/);
|
|
145
|
+
const fieldName = parts[0];
|
|
146
|
+
let fieldType = parts[1];
|
|
147
|
+
if (!fieldName || !fieldType)
|
|
148
|
+
return;
|
|
149
|
+
const isArray = fieldType.endsWith('[]');
|
|
150
|
+
const isOptional = fieldType.endsWith('?');
|
|
151
|
+
const cleanType = fieldType.replace('[]', '').replace('?', '');
|
|
152
|
+
// Parse SQL Server type (e.g., "NVARCHAR(255)" → base="NVARCHAR")
|
|
153
|
+
const { base: sqlBase } = parseSqlType(cleanType);
|
|
154
|
+
let tsType = 'any';
|
|
155
|
+
let isRelation = false;
|
|
156
|
+
// Check if it's a known SQL Server type
|
|
157
|
+
if (AN5_TO_TS[sqlBase]) {
|
|
158
|
+
tsType = sqlTypeToTs(cleanType);
|
|
159
|
+
}
|
|
160
|
+
else if (cleanType[0] === cleanType[0].toUpperCase() && !cleanType.includes('(')) {
|
|
161
|
+
// Uppercase without parens = likely a relation to another model
|
|
162
|
+
tsType = cleanType;
|
|
163
|
+
isRelation = true;
|
|
164
|
+
}
|
|
165
|
+
if (isRelation) {
|
|
166
|
+
let foreignKey = '', localKey = '', relationName = '';
|
|
167
|
+
const nameMatch = line.match(/@relation\("(\w+)"/);
|
|
168
|
+
if (nameMatch)
|
|
169
|
+
relationName = nameMatch[1];
|
|
170
|
+
const relationMatch = line.match(/@relation\((?:.*fields:\s*\[(\w+)\],)?\s*(?:.*references:\s*\[(\w+)\],?)?.*\)/);
|
|
171
|
+
if (relationMatch) {
|
|
172
|
+
foreignKey = relationMatch[1] || '';
|
|
173
|
+
localKey = relationMatch[2] || '';
|
|
174
|
+
}
|
|
175
|
+
model.relations.push({ name: fieldName, type: tsType, isArray, isOptional, foreignKey, localKey, relationName });
|
|
176
|
+
}
|
|
177
|
+
else {
|
|
178
|
+
const hasDefault = line.includes('@default') || line.includes('@updatedAt') || line.includes('@id');
|
|
179
|
+
const isId = line.includes('@id');
|
|
180
|
+
let description;
|
|
181
|
+
const descMatch = line.match(/@description\("(.+)"\)/);
|
|
182
|
+
if (descMatch)
|
|
183
|
+
description = descMatch[1];
|
|
184
|
+
model.fields.push({ name: fieldName, type: tsType, sqlType: cleanType, isOptional, hasDefault, isId, description });
|
|
185
|
+
}
|
|
186
|
+
}
|
|
187
|
+
postProcessRelations(models) {
|
|
188
|
+
for (const model of models) {
|
|
189
|
+
for (const rel of model.relations) {
|
|
190
|
+
if (!rel.foreignKey || !rel.localKey) {
|
|
191
|
+
const targetModel = models.find(m => m.name === rel.type);
|
|
192
|
+
if (targetModel) {
|
|
193
|
+
let opposite = rel.relationName ?
|
|
194
|
+
targetModel.relations.find(r => r.type === model.name && r.relationName === rel.relationName && r.foreignKey && r.localKey) :
|
|
195
|
+
targetModel.relations.find(r => r.type === model.name && r.foreignKey && r.localKey);
|
|
196
|
+
if (opposite) {
|
|
197
|
+
rel.foreignKey = opposite.foreignKey;
|
|
198
|
+
rel.localKey = opposite.localKey;
|
|
199
|
+
}
|
|
200
|
+
}
|
|
201
|
+
}
|
|
202
|
+
}
|
|
203
|
+
}
|
|
204
|
+
}
|
|
205
|
+
}
|
|
206
|
+
exports.SchemaParser = SchemaParser;
|
|
@@ -0,0 +1,85 @@
|
|
|
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
|
+
exports.PythonGenerator = void 0;
|
|
7
|
+
const fs_1 = __importDefault(require("fs"));
|
|
8
|
+
class PythonGenerator {
|
|
9
|
+
constructor(outputPath) {
|
|
10
|
+
this.outputPath = outputPath;
|
|
11
|
+
}
|
|
12
|
+
generate(models) {
|
|
13
|
+
let pyContent = '# This file is auto-generated. Do not edit directly.\n\n';
|
|
14
|
+
// 1. MODEL_TO_TABLE
|
|
15
|
+
pyContent += 'MODEL_TO_TABLE = {\n';
|
|
16
|
+
for (const model of models) {
|
|
17
|
+
const props = this.getAllPropertyVariations(model.name);
|
|
18
|
+
const fullTableName = `[${model.schemaName}].[${model.tableName}]`;
|
|
19
|
+
for (const prop of props) {
|
|
20
|
+
pyContent += ` "${prop}": "${fullTableName}",\n`;
|
|
21
|
+
}
|
|
22
|
+
}
|
|
23
|
+
pyContent += '}\n\n';
|
|
24
|
+
// 2. MODEL_FIELDS
|
|
25
|
+
pyContent += 'MODEL_FIELDS = {\n';
|
|
26
|
+
for (const model of models) {
|
|
27
|
+
const props = this.getAllPropertyVariations(model.name);
|
|
28
|
+
const fieldsStr = `{ ${model.fields.map(f => `"${f.name}": "${f.type}${f.isOptional ? '?' : ''}"`).join(', ')} }`;
|
|
29
|
+
for (const prop of props) {
|
|
30
|
+
pyContent += ` "${prop}": ${fieldsStr},\n`;
|
|
31
|
+
}
|
|
32
|
+
}
|
|
33
|
+
pyContent += '}\n\n';
|
|
34
|
+
// 3. RELATION_MAP
|
|
35
|
+
pyContent += 'RELATION_MAP = {\n';
|
|
36
|
+
for (const model of models) {
|
|
37
|
+
const props = this.getAllPropertyVariations(model.name);
|
|
38
|
+
let relationsContent = `{\n`;
|
|
39
|
+
for (const rel of model.relations) {
|
|
40
|
+
relationsContent += ` "${rel.name}": {\n`;
|
|
41
|
+
relationsContent += ` "modelName": "${this.toCamelCase(rel.type)}",\n`;
|
|
42
|
+
relationsContent += ` "relationType": "${rel.isArray ? 'many' : 'one'}",\n`;
|
|
43
|
+
relationsContent += ` "foreignKey": "${rel.foreignKey || 'id'}",\n`;
|
|
44
|
+
relationsContent += ` "localKey": "${rel.localKey || 'id'}"\n`;
|
|
45
|
+
relationsContent += ` },\n`;
|
|
46
|
+
}
|
|
47
|
+
relationsContent += ` }`;
|
|
48
|
+
for (const prop of props) {
|
|
49
|
+
pyContent += ` "${prop}": ${relationsContent},\n`;
|
|
50
|
+
}
|
|
51
|
+
}
|
|
52
|
+
pyContent += '}\n';
|
|
53
|
+
fs_1.default.writeFileSync(this.outputPath, pyContent);
|
|
54
|
+
}
|
|
55
|
+
getAllPropertyVariations(modelName) {
|
|
56
|
+
const variations = new Set();
|
|
57
|
+
// 1. camelCase (User -> user, McpServer -> mcpServer)
|
|
58
|
+
variations.add(this.toCamelCase(modelName));
|
|
59
|
+
// 2. snake_case (User -> user, McpServer -> mcp_server)
|
|
60
|
+
variations.add(this.toSnakeCase(modelName));
|
|
61
|
+
const acronyms = ['LLM', 'AI', 'MCP', 'IT', 'QC', 'HR', 'MR', 'WH', 'SSIS', 'API', 'URL', 'ID', 'JSON'];
|
|
62
|
+
for (const acronym of acronyms) {
|
|
63
|
+
if (modelName.startsWith(acronym)) {
|
|
64
|
+
variations.add(acronym.toLowerCase() + modelName.slice(acronym.length));
|
|
65
|
+
variations.add(acronym[0].toLowerCase() + acronym.slice(1) + modelName.slice(acronym.length));
|
|
66
|
+
variations.add(this.toSnakeCase(acronym.toLowerCase() + modelName.slice(acronym.length)));
|
|
67
|
+
}
|
|
68
|
+
}
|
|
69
|
+
return Array.from(variations);
|
|
70
|
+
}
|
|
71
|
+
toCamelCase(str) {
|
|
72
|
+
if (!str)
|
|
73
|
+
return '';
|
|
74
|
+
return str[0].toLowerCase() + str.slice(1);
|
|
75
|
+
}
|
|
76
|
+
toSnakeCase(str) {
|
|
77
|
+
if (!str)
|
|
78
|
+
return '';
|
|
79
|
+
return str
|
|
80
|
+
.replace(/([A-Z]+)([A-Z][a-z])/g, '$1_$2')
|
|
81
|
+
.replace(/([a-z\d])([A-Z])/g, '$1_$2')
|
|
82
|
+
.toLowerCase();
|
|
83
|
+
}
|
|
84
|
+
}
|
|
85
|
+
exports.PythonGenerator = PythonGenerator;
|
|
@@ -0,0 +1,27 @@
|
|
|
1
|
+
export interface Field {
|
|
2
|
+
name: string;
|
|
3
|
+
type: string;
|
|
4
|
+
sqlType: string;
|
|
5
|
+
isOptional: boolean;
|
|
6
|
+
hasDefault: boolean;
|
|
7
|
+
isId: boolean;
|
|
8
|
+
description?: string;
|
|
9
|
+
}
|
|
10
|
+
export interface Relation {
|
|
11
|
+
name: string;
|
|
12
|
+
type: string;
|
|
13
|
+
isArray: boolean;
|
|
14
|
+
isOptional: boolean;
|
|
15
|
+
foreignKey: string;
|
|
16
|
+
localKey: string;
|
|
17
|
+
relationName: string;
|
|
18
|
+
}
|
|
19
|
+
export interface Model {
|
|
20
|
+
name: string;
|
|
21
|
+
tableName: string;
|
|
22
|
+
schemaName: string;
|
|
23
|
+
fields: Field[];
|
|
24
|
+
relations: Relation[];
|
|
25
|
+
compoundUniques?: string[][];
|
|
26
|
+
description?: string;
|
|
27
|
+
}
|
package/dist/migrate.js
CHANGED
|
@@ -15,8 +15,8 @@ Object.defineProperty(exports, "__esModule", { value: true });
|
|
|
15
15
|
require("dotenv/config");
|
|
16
16
|
const fs_1 = __importDefault(require("fs"));
|
|
17
17
|
const path_1 = __importDefault(require("path"));
|
|
18
|
-
const an5_adapters_1 = require("an5
|
|
19
|
-
const rootDir =
|
|
18
|
+
const an5_adapters_1 = require("@an5/adapters");
|
|
19
|
+
const rootDir = process.cwd();
|
|
20
20
|
let config = {};
|
|
21
21
|
try {
|
|
22
22
|
const configPath = path_1.default.join(rootDir, 'an5Orm.config.js');
|
|
@@ -295,4 +295,4 @@ main().catch((err) => {
|
|
|
295
295
|
console.error('❌ Migration failed:', err);
|
|
296
296
|
process.exit(1);
|
|
297
297
|
});
|
|
298
|
-
//# sourceMappingURL=migrate.js.map
|
|
298
|
+
//# sourceMappingURL=migrate.js.map
|
package/dist/pull.js
CHANGED
|
@@ -6,8 +6,8 @@ Object.defineProperty(exports, "__esModule", { value: true });
|
|
|
6
6
|
require("dotenv/config");
|
|
7
7
|
const fs_1 = __importDefault(require("fs"));
|
|
8
8
|
const path_1 = __importDefault(require("path"));
|
|
9
|
-
const an5_adapters_1 = require("an5
|
|
10
|
-
const rootDir =
|
|
9
|
+
const an5_adapters_1 = require("@an5/adapters");
|
|
10
|
+
const rootDir = process.cwd();
|
|
11
11
|
let config = {};
|
|
12
12
|
try {
|
|
13
13
|
const configPath = path_1.default.join(rootDir, "an5Orm.config.js");
|
|
@@ -333,4 +333,4 @@ pull().catch((err) => {
|
|
|
333
333
|
console.error("❌ Pull failed:", err);
|
|
334
334
|
process.exit(1);
|
|
335
335
|
});
|
|
336
|
-
//# sourceMappingURL=pull.js.map
|
|
336
|
+
//# sourceMappingURL=pull.js.map
|
package/dist/push.js
CHANGED
|
@@ -6,8 +6,8 @@ Object.defineProperty(exports, "__esModule", { value: true });
|
|
|
6
6
|
require("dotenv/config");
|
|
7
7
|
const fs_1 = __importDefault(require("fs"));
|
|
8
8
|
const path_1 = __importDefault(require("path"));
|
|
9
|
-
const
|
|
10
|
-
const rootDir =
|
|
9
|
+
const adapters_1 = require("@an5/adapters");
|
|
10
|
+
const rootDir = process.cwd();
|
|
11
11
|
let config = {};
|
|
12
12
|
try {
|
|
13
13
|
const configPath = path_1.default.join(rootDir, "an5Orm.config.js");
|
|
@@ -22,7 +22,7 @@ const schemaDir = path_1.default.resolve(rootDir, config.schemaDir || "an5Schema
|
|
|
22
22
|
let _adapter = null;
|
|
23
23
|
async function getDb() {
|
|
24
24
|
if (!_adapter) {
|
|
25
|
-
_adapter = new
|
|
25
|
+
_adapter = new adapters_1.An5Adapter({ connectionString: process.env.DATABASE_URL });
|
|
26
26
|
await _adapter.$connect();
|
|
27
27
|
}
|
|
28
28
|
return _adapter;
|
|
@@ -42,6 +42,75 @@ function parseSqlType(raw) {
|
|
|
42
42
|
const match = raw.match(/^(\w+)/);
|
|
43
43
|
return match ? match[1].toUpperCase() : raw.toUpperCase();
|
|
44
44
|
}
|
|
45
|
+
function parseTableName(raw) {
|
|
46
|
+
const cleaned = raw.replace(/[\[\]"]/g, "");
|
|
47
|
+
const parts = cleaned.split(".").filter(Boolean);
|
|
48
|
+
if (parts.length >= 2) {
|
|
49
|
+
return { schema: parts[parts.length - 2], name: parts[parts.length - 1] };
|
|
50
|
+
}
|
|
51
|
+
return { name: cleaned };
|
|
52
|
+
}
|
|
53
|
+
function tableKey(raw) {
|
|
54
|
+
const table = parseTableName(raw);
|
|
55
|
+
return `${table.schema || "dbo"}.${table.name}`.toLowerCase();
|
|
56
|
+
}
|
|
57
|
+
function tableSqlName(raw) {
|
|
58
|
+
const table = parseTableName(raw);
|
|
59
|
+
return table.schema ? `[${table.schema}].[${table.name}]` : `[${table.name}]`;
|
|
60
|
+
}
|
|
61
|
+
function tableObjectName(raw) {
|
|
62
|
+
const table = parseTableName(raw);
|
|
63
|
+
return table.schema ? `${table.schema}.${table.name}` : table.name;
|
|
64
|
+
}
|
|
65
|
+
function safeIdentifierName(raw) {
|
|
66
|
+
return raw.replace(/[^A-Za-z0-9_]/g, "_").replace(/_+/g, "_").replace(/^_|_$/g, "");
|
|
67
|
+
}
|
|
68
|
+
async function dropTablesNotInSchema(models) {
|
|
69
|
+
var _a;
|
|
70
|
+
if (!((_a = config.push) === null || _a === void 0 ? void 0 : _a.dropTables)) {
|
|
71
|
+
return;
|
|
72
|
+
}
|
|
73
|
+
const validTableKeys = new Set(models.map(model => tableKey(model.tableName)));
|
|
74
|
+
const dbTables = await (await getDb()).$queryRawUnsafe(`
|
|
75
|
+
SELECT s.name AS schemaName, t.name AS tableName
|
|
76
|
+
FROM sys.tables t
|
|
77
|
+
INNER JOIN sys.schemas s ON s.schema_id = t.schema_id
|
|
78
|
+
WHERE t.is_ms_shipped = 0
|
|
79
|
+
`);
|
|
80
|
+
const tablesToDrop = dbTables.filter(table => !validTableKeys.has(`${table.schemaName}.${table.tableName}`.toLowerCase()));
|
|
81
|
+
if (tablesToDrop.length === 0) {
|
|
82
|
+
console.log("✅ No extra tables to drop.");
|
|
83
|
+
return;
|
|
84
|
+
}
|
|
85
|
+
console.warn(`⚠️ Dropping ${tablesToDrop.length} table(s) not present in schema because push.dropTables=true.`);
|
|
86
|
+
const dropKeys = new Set(tablesToDrop.map(table => `${table.schemaName}.${table.tableName}`.toLowerCase()));
|
|
87
|
+
const foreignKeys = await (await getDb()).$queryRawUnsafe(`
|
|
88
|
+
SELECT
|
|
89
|
+
fk.name AS constraintName,
|
|
90
|
+
ps.name AS parentSchema,
|
|
91
|
+
pt.name AS parentTable,
|
|
92
|
+
rs.name AS referencedSchema,
|
|
93
|
+
rt.name AS referencedTable
|
|
94
|
+
FROM sys.foreign_keys fk
|
|
95
|
+
INNER JOIN sys.tables pt ON pt.object_id = fk.parent_object_id
|
|
96
|
+
INNER JOIN sys.schemas ps ON ps.schema_id = pt.schema_id
|
|
97
|
+
INNER JOIN sys.tables rt ON rt.object_id = fk.referenced_object_id
|
|
98
|
+
INNER JOIN sys.schemas rs ON rs.schema_id = rt.schema_id
|
|
99
|
+
`);
|
|
100
|
+
for (const fk of foreignKeys) {
|
|
101
|
+
const parentKey = `${fk.parentSchema}.${fk.parentTable}`.toLowerCase();
|
|
102
|
+
const referencedKey = `${fk.referencedSchema}.${fk.referencedTable}`.toLowerCase();
|
|
103
|
+
if (dropKeys.has(parentKey) || dropKeys.has(referencedKey)) {
|
|
104
|
+
console.log(`Dropping foreign key [${fk.constraintName}] on [${fk.parentSchema}].[${fk.parentTable}]...`);
|
|
105
|
+
await (await getDb()).$executeRawUnsafe(`ALTER TABLE [${fk.parentSchema}].[${fk.parentTable}] DROP CONSTRAINT [${fk.constraintName}]`);
|
|
106
|
+
}
|
|
107
|
+
}
|
|
108
|
+
for (const table of tablesToDrop) {
|
|
109
|
+
console.log(`Dropping table [${table.schemaName}].[${table.tableName}]...`);
|
|
110
|
+
await (await getDb()).$executeRawUnsafe(`DROP TABLE [${table.schemaName}].[${table.tableName}]`);
|
|
111
|
+
console.log(`✅ Dropped [${table.schemaName}].[${table.tableName}]`);
|
|
112
|
+
}
|
|
113
|
+
}
|
|
45
114
|
async function push() {
|
|
46
115
|
let schemaText = "";
|
|
47
116
|
if (fs_1.default.existsSync(schemaDir)) {
|
|
@@ -160,12 +229,15 @@ async function push() {
|
|
|
160
229
|
}
|
|
161
230
|
}
|
|
162
231
|
console.log(`🚀 Pushing schema to database...`);
|
|
232
|
+
await dropTablesNotInSchema(models);
|
|
163
233
|
for (const model of models) {
|
|
164
|
-
|
|
234
|
+
const sqlTableName = tableSqlName(model.tableName);
|
|
235
|
+
const objectName = tableObjectName(model.tableName);
|
|
236
|
+
console.log(`Processing table ${sqlTableName}...`);
|
|
165
237
|
// Check if table exists
|
|
166
|
-
const tableExists = await (await getDb()).$queryRawUnsafe(`SELECT name FROM sys.tables WHERE
|
|
238
|
+
const tableExists = await (await getDb()).$queryRawUnsafe(`SELECT name FROM sys.tables WHERE object_id = OBJECT_ID('${objectName}')`);
|
|
167
239
|
if (tableExists.length === 0) {
|
|
168
|
-
console.log(`Creating table
|
|
240
|
+
console.log(`Creating table ${sqlTableName}...`);
|
|
169
241
|
const colDefs = model.fields.map((f) => {
|
|
170
242
|
let def = `[${f.name}] ${f.sqlType}`;
|
|
171
243
|
if (f.isId)
|
|
@@ -181,32 +253,32 @@ async function push() {
|
|
|
181
253
|
// Add compound uniques as table constraints
|
|
182
254
|
if (model.compoundUniques && model.compoundUniques.length > 0) {
|
|
183
255
|
model.compoundUniques.forEach((fields, idx) => {
|
|
184
|
-
const constraintName = `UQ_${model.tableName}_compound_${idx}`;
|
|
256
|
+
const constraintName = `UQ_${safeIdentifierName(model.tableName)}_compound_${idx}`;
|
|
185
257
|
const fieldsStr = fields.map(f => `[${f}]`).join(", ");
|
|
186
258
|
colDefs.push(`CONSTRAINT [${constraintName}] UNIQUE (${fieldsStr})`);
|
|
187
259
|
});
|
|
188
260
|
}
|
|
189
|
-
const createSql = `CREATE TABLE
|
|
261
|
+
const createSql = `CREATE TABLE ${sqlTableName} (\n ${colDefs.join(",\n ")}\n)`;
|
|
190
262
|
await (await getDb()).$executeRawUnsafe(createSql);
|
|
191
|
-
console.log(`✅ Table
|
|
263
|
+
console.log(`✅ Table ${sqlTableName} created.`);
|
|
192
264
|
// Create indexes
|
|
193
265
|
if (model.indexes && model.indexes.length > 0) {
|
|
194
266
|
for (let idx = 0; idx < model.indexes.length; idx++) {
|
|
195
267
|
const fields = model.indexes[idx];
|
|
196
|
-
const indexName = `IX_${model.tableName}_${fields.join("_")}`;
|
|
268
|
+
const indexName = `IX_${safeIdentifierName(model.tableName)}_${fields.join("_")}`;
|
|
197
269
|
const fieldsStr = fields.map((f) => `[${f}]`).join(", ");
|
|
198
|
-
console.log(`Creating index [${indexName}] on table
|
|
199
|
-
await (await getDb()).$executeRawUnsafe(`CREATE INDEX [${indexName}] ON
|
|
270
|
+
console.log(`Creating index [${indexName}] on table ${sqlTableName}...`);
|
|
271
|
+
await (await getDb()).$executeRawUnsafe(`CREATE INDEX [${indexName}] ON ${sqlTableName} (${fieldsStr})`);
|
|
200
272
|
}
|
|
201
273
|
}
|
|
202
274
|
}
|
|
203
275
|
else {
|
|
204
276
|
// 1. Check for missing columns
|
|
205
277
|
for (const field of model.fields) {
|
|
206
|
-
const colExists = await (await getDb()).$queryRawUnsafe(`SELECT name FROM sys.columns WHERE object_id = OBJECT_ID('${
|
|
278
|
+
const colExists = await (await getDb()).$queryRawUnsafe(`SELECT name FROM sys.columns WHERE object_id = OBJECT_ID('${objectName}') AND name = '${field.name}'`);
|
|
207
279
|
if (colExists.length === 0) {
|
|
208
|
-
console.log(`Adding column [${field.name}] to table
|
|
209
|
-
let alterSql = `ALTER TABLE
|
|
280
|
+
console.log(`Adding column [${field.name}] to table ${sqlTableName}...`);
|
|
281
|
+
let alterSql = `ALTER TABLE ${sqlTableName} ADD [${field.name}] ${field.sqlType}`;
|
|
210
282
|
if (field.defaultValue)
|
|
211
283
|
alterSql += ` ${field.defaultValue}`;
|
|
212
284
|
if (field.isUnique && !field.isId)
|
|
@@ -224,12 +296,12 @@ async function push() {
|
|
|
224
296
|
if (model.compoundUniques && model.compoundUniques.length > 0) {
|
|
225
297
|
for (let idx = 0; idx < model.compoundUniques.length; idx++) {
|
|
226
298
|
const fields = model.compoundUniques[idx];
|
|
227
|
-
const constraintName = `UQ_${model.tableName}_compound_${idx}`;
|
|
228
|
-
const constraintExists = await (await getDb()).$queryRawUnsafe(`SELECT name FROM sys.objects WHERE type = 'UQ' AND parent_object_id = OBJECT_ID('${
|
|
299
|
+
const constraintName = `UQ_${safeIdentifierName(model.tableName)}_compound_${idx}`;
|
|
300
|
+
const constraintExists = await (await getDb()).$queryRawUnsafe(`SELECT name FROM sys.objects WHERE type = 'UQ' AND parent_object_id = OBJECT_ID('${objectName}') AND name = '${constraintName}'`);
|
|
229
301
|
if (constraintExists.length === 0) {
|
|
230
|
-
console.log(`Adding compound unique constraint [${constraintName}] to table
|
|
302
|
+
console.log(`Adding compound unique constraint [${constraintName}] to table ${sqlTableName}...`);
|
|
231
303
|
const fieldsStr = fields.map((f) => `[${f}]`).join(", ");
|
|
232
|
-
await (await getDb()).$executeRawUnsafe(`ALTER TABLE
|
|
304
|
+
await (await getDb()).$executeRawUnsafe(`ALTER TABLE ${sqlTableName} ADD CONSTRAINT [${constraintName}] UNIQUE (${fieldsStr})`);
|
|
233
305
|
}
|
|
234
306
|
}
|
|
235
307
|
}
|
|
@@ -237,12 +309,12 @@ async function push() {
|
|
|
237
309
|
if (model.indexes && model.indexes.length > 0) {
|
|
238
310
|
for (let idx = 0; idx < model.indexes.length; idx++) {
|
|
239
311
|
const fields = model.indexes[idx];
|
|
240
|
-
const indexName = `IX_${model.tableName}_${fields.join("_")}`;
|
|
241
|
-
const indexExists = await (await getDb()).$queryRawUnsafe(`SELECT name FROM sys.indexes WHERE object_id = OBJECT_ID('${
|
|
312
|
+
const indexName = `IX_${safeIdentifierName(model.tableName)}_${fields.join("_")}`;
|
|
313
|
+
const indexExists = await (await getDb()).$queryRawUnsafe(`SELECT name FROM sys.indexes WHERE object_id = OBJECT_ID('${objectName}') AND name = '${indexName}'`);
|
|
242
314
|
if (indexExists.length === 0) {
|
|
243
|
-
console.log(`Creating index [${indexName}] on table
|
|
315
|
+
console.log(`Creating index [${indexName}] on table ${sqlTableName}...`);
|
|
244
316
|
const fieldsStr = fields.map((f) => `[${f}]`).join(", ");
|
|
245
|
-
await (await getDb()).$executeRawUnsafe(`CREATE INDEX [${indexName}] ON
|
|
317
|
+
await (await getDb()).$executeRawUnsafe(`CREATE INDEX [${indexName}] ON ${sqlTableName} (${fieldsStr})`);
|
|
246
318
|
}
|
|
247
319
|
}
|
|
248
320
|
}
|
|
@@ -255,4 +327,3 @@ push().catch((err) => {
|
|
|
255
327
|
console.error("❌ Push failed:", err);
|
|
256
328
|
process.exit(1);
|
|
257
329
|
});
|
|
258
|
-
//# sourceMappingURL=push.js.map
|
package/generator/src/index.ts
CHANGED