@an5/orm 1.0.9 → 1.1.1
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 +10 -3
- package/dist/config.d.ts +9 -1
- package/dist/config.js +13 -18
- package/dist/generator/src/api.d.ts +35 -0
- package/dist/generator/src/api.js +58 -0
- 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 +8 -0
- package/dist/generator/src/dotnet-generator.js +282 -101
- 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 +1230 -0
- package/dist/generator/src/index.js +69 -36
- package/dist/generator/src/metadata-generator.js +2 -1
- package/dist/generator/src/parser.d.ts +28 -2
- package/dist/generator/src/parser.js +92 -90
- 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 +19 -1
- package/dist/generator/src/python-generator.js +147 -64
- 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 +33 -0
- package/dist/generator/src/types.js +39 -0
- package/dist/migration-core.d.ts +16 -1
- package/dist/migration-core.js +60 -21
- 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/sql-utils.js +8 -3
- package/generator/src/api.ts +45 -0
- package/generator/src/config.ts +496 -0
- package/generator/src/dialect.ts +415 -0
- package/generator/src/dotnet-generator.ts +277 -88
- package/generator/src/field-types.ts +473 -0
- package/generator/src/golang-generator.ts +94 -68
- package/generator/src/index.ts +71 -36
- package/generator/src/metadata-generator.ts +2 -2
- package/generator/src/parser.ts +122 -90
- package/generator/src/push-apply.ts +148 -0
- package/generator/src/push-schema.ts +250 -0
- package/generator/src/python-generator.ts +118 -62
- 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 +48 -0
- package/package.json +26 -6
|
@@ -4,12 +4,15 @@ var __importDefault = (this && this.__importDefault) || function (mod) {
|
|
|
4
4
|
};
|
|
5
5
|
Object.defineProperty(exports, "__esModule", { value: true });
|
|
6
6
|
const path_1 = __importDefault(require("path"));
|
|
7
|
+
const config_1 = require("./config");
|
|
8
|
+
const field_types_1 = require("./field-types");
|
|
7
9
|
const parser_1 = require("./parser");
|
|
8
10
|
const code_generator_1 = require("./code-generator");
|
|
9
11
|
const metadata_generator_1 = require("./metadata-generator");
|
|
10
12
|
const python_generator_1 = require("./python-generator");
|
|
11
13
|
const dotnet_generator_1 = require("./dotnet-generator");
|
|
12
14
|
const golang_generator_1 = require("./golang-generator");
|
|
15
|
+
const rust_generator_1 = require("./rust-generator");
|
|
13
16
|
const fs_1 = __importDefault(require("fs"));
|
|
14
17
|
function clearGeneratedFiles(outputDir, extension) {
|
|
15
18
|
if (!fs_1.default.existsSync(outputDir)) {
|
|
@@ -26,61 +29,81 @@ function clearGeneratedFiles(outputDir, extension) {
|
|
|
26
29
|
}
|
|
27
30
|
}
|
|
28
31
|
}
|
|
32
|
+
function clearGeneratedPythonFiles(outputDir) {
|
|
33
|
+
if (!fs_1.default.existsSync(outputDir)) {
|
|
34
|
+
fs_1.default.mkdirSync(outputDir, { recursive: true });
|
|
35
|
+
return;
|
|
36
|
+
}
|
|
37
|
+
// Delete generated .py files but preserve hand-maintained adapter.
|
|
38
|
+
const preserve = new Set(['an5_adapter.py']);
|
|
39
|
+
for (const entry of fs_1.default.readdirSync(outputDir)) {
|
|
40
|
+
const fullPath = path_1.default.join(outputDir, entry);
|
|
41
|
+
const stat = fs_1.default.statSync(fullPath);
|
|
42
|
+
if (stat.isDirectory())
|
|
43
|
+
continue;
|
|
44
|
+
if (entry.endsWith('.py') && !preserve.has(entry)) {
|
|
45
|
+
fs_1.default.unlinkSync(fullPath);
|
|
46
|
+
}
|
|
47
|
+
}
|
|
48
|
+
}
|
|
29
49
|
async function main() {
|
|
30
|
-
|
|
31
|
-
|
|
50
|
+
// Loaded through the validating loader, so a mistyped key or a wrong type
|
|
51
|
+
// stops here instead of quietly generating somewhere else.
|
|
52
|
+
let config;
|
|
32
53
|
try {
|
|
33
|
-
|
|
34
|
-
if (!fs_1.default.existsSync(configPath)) {
|
|
35
|
-
configPath = path_1.default.join(rootDir, 'an5Orm.config.cjs');
|
|
36
|
-
}
|
|
37
|
-
if (!fs_1.default.existsSync(configPath)) {
|
|
38
|
-
configPath = path_1.default.join(rootDir, '..', 'an5Orm.config.js');
|
|
39
|
-
}
|
|
40
|
-
if (!fs_1.default.existsSync(configPath)) {
|
|
41
|
-
configPath = path_1.default.join(rootDir, '..', 'an5Orm.config.cjs');
|
|
42
|
-
}
|
|
43
|
-
if (fs_1.default.existsSync(configPath)) {
|
|
44
|
-
config = require(configPath);
|
|
45
|
-
rootDir = path_1.default.dirname(configPath);
|
|
46
|
-
}
|
|
47
|
-
else if (fs_1.default.existsSync(path_1.default.join(rootDir, '..', 'an5Schema'))) {
|
|
48
|
-
rootDir = path_1.default.resolve(rootDir, '..');
|
|
49
|
-
}
|
|
54
|
+
config = (0, config_1.loadConfig)();
|
|
50
55
|
}
|
|
51
56
|
catch (err) {
|
|
57
|
+
if (err instanceof config_1.ConfigError) {
|
|
58
|
+
console.error(`❌ ${err.message}:`);
|
|
59
|
+
console.error((0, config_1.formatIssues)(err.issues));
|
|
60
|
+
process.exit(1);
|
|
61
|
+
}
|
|
52
62
|
console.warn('⚠️ Could not load an5Orm.config.js/.cjs, using defaults.', err);
|
|
63
|
+
config = (0, config_1.loadConfig)(path_1.default.join(process.cwd(), 'no-such-dir'));
|
|
53
64
|
}
|
|
54
|
-
const
|
|
55
|
-
const
|
|
56
|
-
const
|
|
57
|
-
const
|
|
58
|
-
const
|
|
59
|
-
const
|
|
65
|
+
const { rootDir } = config;
|
|
66
|
+
const schemaDir = config.outputs.schemaDir;
|
|
67
|
+
const outputTypesDir = config.outputs.typescriptDir;
|
|
68
|
+
const outputMetadataPath = config.outputs.typescriptMetadataFile;
|
|
69
|
+
const outputPythonMetadataPath = config.outputs.pythonMetadataFile;
|
|
70
|
+
const outputDotnetDir = config.outputs.dotnetDir;
|
|
71
|
+
const outputGolangDir = config.outputs.golangDir;
|
|
72
|
+
const outputRustDir = config.outputs.rustDir;
|
|
73
|
+
const generateMetadata = config.config.generation.generateMetadata;
|
|
60
74
|
console.log('🚀 Starting ORM generation...');
|
|
75
|
+
// The target provider comes from the connection string: both the allowed
|
|
76
|
+
// field types and the SQL follow it, instead of one shared list.
|
|
77
|
+
const provider = (0, config_1.providerFromConfig)(config.config, process.env);
|
|
78
|
+
console.log(`🗄️ Provider: ${field_types_1.PROVIDER_LABELS[provider]} (${provider})`);
|
|
61
79
|
try {
|
|
62
80
|
clearGeneratedFiles(outputTypesDir, '.ts');
|
|
63
81
|
clearGeneratedFiles(outputDotnetDir, '.cs');
|
|
64
82
|
clearGeneratedFiles(outputGolangDir, '.go');
|
|
65
|
-
|
|
83
|
+
clearGeneratedFiles(path_1.default.join(outputRustDir, 'src'), '.rs');
|
|
84
|
+
const pythonDirEarly = path_1.default.dirname(outputPythonMetadataPath);
|
|
85
|
+
clearGeneratedPythonFiles(pythonDirEarly);
|
|
86
|
+
if (config.config.generation.generateMetadata && fs_1.default.existsSync(outputMetadataPath)) {
|
|
66
87
|
fs_1.default.unlinkSync(outputMetadataPath);
|
|
67
88
|
}
|
|
68
|
-
|
|
69
|
-
fs_1.default.unlinkSync(outputPythonMetadataPath);
|
|
70
|
-
}
|
|
71
|
-
const parser = new parser_1.SchemaParser(schemaDir);
|
|
89
|
+
const parser = new parser_1.SchemaParser(schemaDir, provider);
|
|
72
90
|
const models = await parser.parse();
|
|
73
91
|
console.log(`📦 Parsed ${models.length} models from schema.`);
|
|
74
92
|
const codeGen = new code_generator_1.CodeGenerator(outputTypesDir);
|
|
75
93
|
codeGen.generate(models);
|
|
76
94
|
console.log(`✨ Generated modular types in ${outputTypesDir}`);
|
|
77
|
-
|
|
78
|
-
|
|
79
|
-
fs_1.default.
|
|
95
|
+
if (generateMetadata) {
|
|
96
|
+
const metadataDir = path_1.default.dirname(outputMetadataPath);
|
|
97
|
+
if (!fs_1.default.existsSync(metadataDir)) {
|
|
98
|
+
fs_1.default.mkdirSync(metadataDir, { recursive: true });
|
|
99
|
+
}
|
|
100
|
+
const metadataGen = new metadata_generator_1.MetadataGenerator(outputMetadataPath);
|
|
101
|
+
metadataGen.generate(models);
|
|
102
|
+
console.log(`✨ Generated metadata in ${outputMetadataPath}`);
|
|
103
|
+
}
|
|
104
|
+
else {
|
|
105
|
+
console.log('⏭ Skipping metadata (generation.generateMetadata is false)');
|
|
80
106
|
}
|
|
81
|
-
const metadataGen = new metadata_generator_1.MetadataGenerator(outputMetadataPath);
|
|
82
|
-
metadataGen.generate(models);
|
|
83
|
-
console.log(`✨ Generated metadata in ${outputMetadataPath}`);
|
|
84
107
|
const pythonDir = path_1.default.dirname(outputPythonMetadataPath);
|
|
85
108
|
if (!fs_1.default.existsSync(pythonDir)) {
|
|
86
109
|
fs_1.default.mkdirSync(pythonDir, { recursive: true });
|
|
@@ -94,9 +117,19 @@ async function main() {
|
|
|
94
117
|
const golangGen = new golang_generator_1.GolangGenerator(outputGolangDir);
|
|
95
118
|
golangGen.generate(models);
|
|
96
119
|
console.log(`✨ Generated Golang models in ${outputGolangDir}`);
|
|
120
|
+
const rustGen = new rust_generator_1.RustGenerator(outputRustDir);
|
|
121
|
+
rustGen.generate(models);
|
|
122
|
+
console.log(`✨ Generated Rust client in ${outputRustDir}`);
|
|
97
123
|
console.log('✅ ORM generation completed successfully.');
|
|
98
124
|
}
|
|
99
125
|
catch (error) {
|
|
126
|
+
// Print every bad field type with its provider, instead of the parser's
|
|
127
|
+
// nested error object.
|
|
128
|
+
if (error instanceof field_types_1.FieldTypeError) {
|
|
129
|
+
console.error(`❌ ${error.message}:`);
|
|
130
|
+
console.error((0, config_1.formatIssues)(error.issues));
|
|
131
|
+
process.exit(1);
|
|
132
|
+
}
|
|
100
133
|
console.error('❌ ORM generation failed:', error);
|
|
101
134
|
process.exit(1);
|
|
102
135
|
}
|
|
@@ -5,6 +5,7 @@ var __importDefault = (this && this.__importDefault) || function (mod) {
|
|
|
5
5
|
Object.defineProperty(exports, "__esModule", { value: true });
|
|
6
6
|
exports.MetadataGenerator = void 0;
|
|
7
7
|
const fs_1 = __importDefault(require("fs"));
|
|
8
|
+
const types_1 = require("./types");
|
|
8
9
|
class MetadataGenerator {
|
|
9
10
|
constructor(outputPath, relationImport = 'import type { RelationDef } from "@an5/orm";') {
|
|
10
11
|
this.outputPath = outputPath;
|
|
@@ -16,7 +17,7 @@ class MetadataGenerator {
|
|
|
16
17
|
metaContent += 'export const modelToTable: Record<string, string> = {\n';
|
|
17
18
|
for (const model of models) {
|
|
18
19
|
const props = this.getAllPropertyVariations(model.name);
|
|
19
|
-
const fullTableName =
|
|
20
|
+
const fullTableName = (0, types_1.bracketedTableName)(model);
|
|
20
21
|
for (const prop of props) {
|
|
21
22
|
metaContent += ` ${prop}: "${fullTableName}",\n`;
|
|
22
23
|
}
|
|
@@ -1,11 +1,37 @@
|
|
|
1
1
|
import { Model } from './types';
|
|
2
|
-
|
|
2
|
+
import { Provider } from './field-types';
|
|
3
|
+
/**
|
|
4
|
+
* Maps a column type of a provider to its TypeScript type.
|
|
5
|
+
*
|
|
6
|
+
* `provider` defaults to SQL Server so existing callers keep their behaviour;
|
|
7
|
+
* the generator passes the provider derived from the connection string.
|
|
8
|
+
*/
|
|
9
|
+
export declare function sqlTypeToTs(sqlType: string, provider?: Provider): string;
|
|
3
10
|
export declare class SchemaParser {
|
|
4
11
|
private schemaDir;
|
|
12
|
+
private provider;
|
|
5
13
|
private schemaText;
|
|
6
|
-
|
|
14
|
+
private issues;
|
|
15
|
+
private modelRefs;
|
|
16
|
+
private models;
|
|
17
|
+
/**
|
|
18
|
+
* @param schemaDir directory holding the `.an5` files
|
|
19
|
+
* @param provider target database; decides which field types are valid
|
|
20
|
+
*/
|
|
21
|
+
constructor(schemaDir: string, provider?: Provider);
|
|
7
22
|
parse(): Promise<Model[]>;
|
|
8
23
|
private loadSchema;
|
|
9
24
|
private parseModelLine;
|
|
25
|
+
/**
|
|
26
|
+
* Settles the fields that matched no type: a name matching a model in the
|
|
27
|
+
* schema is a relation, anything else is a bad type — reported with the
|
|
28
|
+
* provider name and a suggestion.
|
|
29
|
+
*
|
|
30
|
+
* The decision has to wait for the whole schema, because a relation may point
|
|
31
|
+
* at a model declared later. Every upper-case token used to count as a
|
|
32
|
+
* relation, so `INTEGER` written for SQL Server became a relation to a model
|
|
33
|
+
* named `INTEGER` and nothing ever complained.
|
|
34
|
+
*/
|
|
35
|
+
private resolveModelRefs;
|
|
10
36
|
private postProcessRelations;
|
|
11
37
|
}
|
|
@@ -7,67 +7,33 @@ exports.SchemaParser = void 0;
|
|
|
7
7
|
exports.sqlTypeToTs = sqlTypeToTs;
|
|
8
8
|
const fs_1 = __importDefault(require("fs"));
|
|
9
9
|
const path_1 = __importDefault(require("path"));
|
|
10
|
-
|
|
11
|
-
|
|
12
|
-
|
|
13
|
-
|
|
14
|
-
|
|
15
|
-
|
|
16
|
-
|
|
17
|
-
|
|
18
|
-
|
|
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';
|
|
10
|
+
const field_types_1 = require("./field-types");
|
|
11
|
+
/**
|
|
12
|
+
* Maps a column type of a provider to its TypeScript type.
|
|
13
|
+
*
|
|
14
|
+
* `provider` defaults to SQL Server so existing callers keep their behaviour;
|
|
15
|
+
* the generator passes the provider derived from the connection string.
|
|
16
|
+
*/
|
|
17
|
+
function sqlTypeToTs(sqlType, provider = field_types_1.DEFAULT_PROVIDER) {
|
|
18
|
+
return (0, field_types_1.resolveFieldType)(sqlType, provider)?.ts ?? 'any';
|
|
64
19
|
}
|
|
65
20
|
class SchemaParser {
|
|
66
|
-
|
|
21
|
+
/**
|
|
22
|
+
* @param schemaDir directory holding the `.an5` files
|
|
23
|
+
* @param provider target database; decides which field types are valid
|
|
24
|
+
*/
|
|
25
|
+
constructor(schemaDir, provider = field_types_1.DEFAULT_PROVIDER) {
|
|
67
26
|
this.schemaDir = schemaDir;
|
|
27
|
+
this.provider = provider;
|
|
68
28
|
this.schemaText = '';
|
|
29
|
+
this.issues = [];
|
|
30
|
+
this.modelRefs = [];
|
|
31
|
+
this.models = [];
|
|
69
32
|
}
|
|
70
33
|
async parse() {
|
|
34
|
+
this.issues = [];
|
|
35
|
+
this.modelRefs = [];
|
|
36
|
+
this.models = [];
|
|
71
37
|
this.loadSchema();
|
|
72
38
|
const lines = this.schemaText.split('\n');
|
|
73
39
|
const models = [];
|
|
@@ -82,7 +48,12 @@ class SchemaParser {
|
|
|
82
48
|
currentModel = {
|
|
83
49
|
name: modelName,
|
|
84
50
|
tableName: modelName.toLowerCase() + 's',
|
|
85
|
-
|
|
51
|
+
// SQL Server's default schema, and only SQL Server's. For SQLite or MySQL
|
|
52
|
+
// an unqualified name is what the connection resolves, and for PostgreSQL
|
|
53
|
+
// it follows `search_path` — the same rule `db:push` now uses, so the
|
|
54
|
+
// generated client and the pushed table cannot drift apart.
|
|
55
|
+
schemaName: this.provider === 'mssql' ? 'dbo' : '',
|
|
56
|
+
provider: this.provider,
|
|
86
57
|
fields: [],
|
|
87
58
|
relations: []
|
|
88
59
|
};
|
|
@@ -97,6 +68,13 @@ class SchemaParser {
|
|
|
97
68
|
this.parseModelLine(line, currentModel);
|
|
98
69
|
}
|
|
99
70
|
}
|
|
71
|
+
this.models = models;
|
|
72
|
+
this.resolveModelRefs();
|
|
73
|
+
// Every bad type is reported at once, so one pass over the schema is enough
|
|
74
|
+
// instead of re-running the generator after every line.
|
|
75
|
+
if (this.issues.length > 0) {
|
|
76
|
+
throw new field_types_1.FieldTypeError(this.provider, this.issues);
|
|
77
|
+
}
|
|
100
78
|
this.postProcessRelations(models);
|
|
101
79
|
return models;
|
|
102
80
|
}
|
|
@@ -119,9 +97,13 @@ class SchemaParser {
|
|
|
119
97
|
return;
|
|
120
98
|
}
|
|
121
99
|
if (line.startsWith('@@schema')) {
|
|
122
|
-
|
|
100
|
+
// `(.*)` rather than `(.+)`: `@@schema("")` is how a model says "no
|
|
101
|
+
// schema", needed for databases that have no schema concept. With `(.+)`
|
|
102
|
+
// the empty string does not match, the directive is silently ignored and
|
|
103
|
+
// the model keeps `dbo`.
|
|
104
|
+
const schemaMatch = line.match(/@@schema\("(.*)"\)/);
|
|
123
105
|
if (schemaMatch)
|
|
124
|
-
model.schemaName = schemaMatch[1];
|
|
106
|
+
model.schemaName = schemaMatch[1].trim();
|
|
125
107
|
return;
|
|
126
108
|
}
|
|
127
109
|
if (line.startsWith('@@unique')) {
|
|
@@ -141,48 +123,68 @@ class SchemaParser {
|
|
|
141
123
|
}
|
|
142
124
|
if (line.startsWith('@@'))
|
|
143
125
|
return;
|
|
144
|
-
const
|
|
145
|
-
|
|
146
|
-
|
|
147
|
-
|
|
126
|
+
const head = (0, field_types_1.readFieldLineHead)(line);
|
|
127
|
+
if (!head)
|
|
128
|
+
return;
|
|
129
|
+
// No table entry for this provider. This used to fall through to
|
|
130
|
+
// `tsType = 'any'` (or, worse, become a relation to a model that does not
|
|
131
|
+
// exist) and the client was still generated; the failure only surfaced
|
|
132
|
+
// later at the database layer, in a message unrelated to the schema.
|
|
133
|
+
const resolved = (0, field_types_1.resolveFieldType)(head.type, this.provider);
|
|
134
|
+
if (!resolved) {
|
|
135
|
+
this.modelRefs.push({ model, name: head.name, type: head.type, isArray: head.isArray, isOptional: head.isOptional, line });
|
|
148
136
|
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
137
|
}
|
|
165
|
-
|
|
138
|
+
const hasDefault = line.includes('@default') || line.includes('@updatedAt') || line.includes('@id');
|
|
139
|
+
const isId = line.includes('@id');
|
|
140
|
+
let description;
|
|
141
|
+
const descMatch = line.match(/@description\("(.+)"\)/);
|
|
142
|
+
if (descMatch)
|
|
143
|
+
description = descMatch[1];
|
|
144
|
+
model.fields.push({ name: head.name, type: resolved.ts, sqlType: head.type, isOptional: head.isOptional, hasDefault, isId, description });
|
|
145
|
+
}
|
|
146
|
+
/**
|
|
147
|
+
* Settles the fields that matched no type: a name matching a model in the
|
|
148
|
+
* schema is a relation, anything else is a bad type — reported with the
|
|
149
|
+
* provider name and a suggestion.
|
|
150
|
+
*
|
|
151
|
+
* The decision has to wait for the whole schema, because a relation may point
|
|
152
|
+
* at a model declared later. Every upper-case token used to count as a
|
|
153
|
+
* relation, so `INTEGER` written for SQL Server became a relation to a model
|
|
154
|
+
* named `INTEGER` and nothing ever complained.
|
|
155
|
+
*/
|
|
156
|
+
resolveModelRefs() {
|
|
157
|
+
const modelNames = this.models.map((model) => model.name);
|
|
158
|
+
for (const ref of this.modelRefs) {
|
|
159
|
+
if (!modelNames.includes(ref.type)) {
|
|
160
|
+
this.issues.push({
|
|
161
|
+
path: `${ref.model.name}.${ref.name}`,
|
|
162
|
+
message: (0, field_types_1.unknownFieldTypeMessage)(ref.type, this.provider, modelNames),
|
|
163
|
+
});
|
|
164
|
+
continue;
|
|
165
|
+
}
|
|
166
166
|
let foreignKey = '', localKey = '', relationName = '';
|
|
167
|
-
const nameMatch = line.match(/@relation\("(\w+)"/);
|
|
167
|
+
const nameMatch = ref.line.match(/@relation\("(\w+)"/);
|
|
168
168
|
if (nameMatch)
|
|
169
169
|
relationName = nameMatch[1];
|
|
170
|
-
const relationMatch = line.match(/@relation\((?:.*fields:\s*\[(\w+)\],)?\s*(?:.*references:\s*\[(\w+)\],?)?.*\)/);
|
|
170
|
+
const relationMatch = ref.line.match(/@relation\((?:.*fields:\s*\[(\w+)\],)?\s*(?:.*references:\s*\[(\w+)\],?)?.*\)/);
|
|
171
171
|
if (relationMatch) {
|
|
172
172
|
foreignKey = relationMatch[1] || '';
|
|
173
173
|
localKey = relationMatch[2] || '';
|
|
174
174
|
}
|
|
175
|
-
|
|
176
|
-
|
|
177
|
-
|
|
178
|
-
|
|
179
|
-
|
|
180
|
-
|
|
181
|
-
|
|
182
|
-
|
|
183
|
-
|
|
184
|
-
|
|
175
|
+
const description = ref.line.match(/@description\("(.+)"\)/)?.[1];
|
|
176
|
+
ref.model.relations.push({
|
|
177
|
+
name: ref.name,
|
|
178
|
+
type: ref.type,
|
|
179
|
+
isArray: ref.isArray,
|
|
180
|
+
isOptional: ref.isOptional,
|
|
181
|
+
foreignKey,
|
|
182
|
+
localKey,
|
|
183
|
+
relationName,
|
|
184
|
+
...(description ? { description } : {}),
|
|
185
|
+
});
|
|
185
186
|
}
|
|
187
|
+
this.modelRefs = [];
|
|
186
188
|
}
|
|
187
189
|
postProcessRelations(models) {
|
|
188
190
|
for (const model of models) {
|
|
@@ -0,0 +1,37 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Applying a schema to a database: what `db:push` does once the schema is read.
|
|
3
|
+
*
|
|
4
|
+
* Kept out of `push.ts` so the sequence can be run against a real database in a
|
|
5
|
+
* test — the CLI needs a live server, and a test that re-implements the sequence
|
|
6
|
+
* instead of calling it proves only that the copy works.
|
|
7
|
+
*
|
|
8
|
+
* The database is two functions rather than the adapter, which keeps this testable
|
|
9
|
+
* and keeps the driver out of the decision making.
|
|
10
|
+
*/
|
|
11
|
+
import { PushDialect } from './dialect';
|
|
12
|
+
import { PushModel } from './push-schema';
|
|
13
|
+
/** The two things this needs from a database connection. */
|
|
14
|
+
export interface PushDatabase {
|
|
15
|
+
/** Runs a query and returns its rows; existence checks are `length > 0`. */
|
|
16
|
+
query(sql: string): Promise<readonly unknown[]>;
|
|
17
|
+
/** Runs one statement. */
|
|
18
|
+
execute(sql: string): Promise<void>;
|
|
19
|
+
/** Progress, so the CLI prints what it is doing. */
|
|
20
|
+
log(message: string): void;
|
|
21
|
+
}
|
|
22
|
+
export interface ApplyResult {
|
|
23
|
+
/** How many tables were created, and how many altered. */
|
|
24
|
+
tablesCreated: number;
|
|
25
|
+
tablesAltered: number;
|
|
26
|
+
columnsAdded: number;
|
|
27
|
+
indexesCreated: number;
|
|
28
|
+
uniqueConstraintsAdded: number;
|
|
29
|
+
}
|
|
30
|
+
/**
|
|
31
|
+
* Creates whatever the schema has and the database does not.
|
|
32
|
+
*
|
|
33
|
+
* The order is deliberate: a table first, then missing columns, then unique
|
|
34
|
+
* constraints and indexes — a unique over a column that does not exist yet fails.
|
|
35
|
+
* Everything is checked before it is created, so running it twice changes nothing.
|
|
36
|
+
*/
|
|
37
|
+
export declare function applySchema(models: readonly PushModel[], dialect: PushDialect, db: PushDatabase): Promise<ApplyResult>;
|
|
@@ -0,0 +1,109 @@
|
|
|
1
|
+
"use strict";
|
|
2
|
+
Object.defineProperty(exports, "__esModule", { value: true });
|
|
3
|
+
exports.applySchema = applySchema;
|
|
4
|
+
const push_schema_1 = require("./push-schema");
|
|
5
|
+
/**
|
|
6
|
+
* Creates whatever the schema has and the database does not.
|
|
7
|
+
*
|
|
8
|
+
* The order is deliberate: a table first, then missing columns, then unique
|
|
9
|
+
* constraints and indexes — a unique over a column that does not exist yet fails.
|
|
10
|
+
* Everything is checked before it is created, so running it twice changes nothing.
|
|
11
|
+
*/
|
|
12
|
+
async function applySchema(models, dialect, db) {
|
|
13
|
+
const result = {
|
|
14
|
+
tablesCreated: 0,
|
|
15
|
+
tablesAltered: 0,
|
|
16
|
+
columnsAdded: 0,
|
|
17
|
+
indexesCreated: 0,
|
|
18
|
+
uniqueConstraintsAdded: 0,
|
|
19
|
+
};
|
|
20
|
+
for (const model of models) {
|
|
21
|
+
const tableName = (0, push_schema_1.qualifiedTableName)(model);
|
|
22
|
+
const sqlTableName = dialect.quoteTable(tableName);
|
|
23
|
+
const quoteFields = (fields) => fields.map((field) => dialect.quote(field));
|
|
24
|
+
db.log(`Processing table ${sqlTableName}...`);
|
|
25
|
+
const safeName = (0, push_schema_1.safeIdentifierName)(model.tableName);
|
|
26
|
+
// SQLite has no named unique constraints — a UNIQUE there becomes an index the
|
|
27
|
+
// engine names itself — so the unique index carries our name instead, and that
|
|
28
|
+
// name is what the existence check can look for.
|
|
29
|
+
const compoundUniques = model.compoundUniques.map((directive, idx) => ({
|
|
30
|
+
name: (0, push_schema_1.directiveName)(directive, model.tableName, 'compound', idx),
|
|
31
|
+
fields: directive.fields,
|
|
32
|
+
directive,
|
|
33
|
+
}));
|
|
34
|
+
const tableExists = (await db.query(dialect.tableExists(tableName))).length > 0;
|
|
35
|
+
if (!tableExists) {
|
|
36
|
+
db.log(`Creating table ${sqlTableName}...`);
|
|
37
|
+
const columnDefs = model.fields.map((field) => dialect.columnDefinition(field));
|
|
38
|
+
for (const entry of compoundUniques) {
|
|
39
|
+
if (!dialect.supportsNamedUniqueConstraint)
|
|
40
|
+
continue;
|
|
41
|
+
const fieldsStr = quoteFields(entry.fields).join(', ');
|
|
42
|
+
columnDefs.push(`CONSTRAINT ${dialect.quote(entry.name)} UNIQUE (${fieldsStr})`);
|
|
43
|
+
}
|
|
44
|
+
await db.execute(dialect.createTable(sqlTableName, columnDefs));
|
|
45
|
+
db.log(`✅ Table ${sqlTableName} created.`);
|
|
46
|
+
result.tablesCreated += 1;
|
|
47
|
+
}
|
|
48
|
+
else {
|
|
49
|
+
for (const field of model.fields) {
|
|
50
|
+
const columnExists = (await db.query(dialect.columnExists(tableName, field.name))).length > 0;
|
|
51
|
+
if (columnExists)
|
|
52
|
+
continue;
|
|
53
|
+
db.log(`Adding column ${dialect.quote(field.name)} to table ${sqlTableName}...`);
|
|
54
|
+
await db.execute(dialect.addColumn(sqlTableName, field));
|
|
55
|
+
result.columnsAdded += 1;
|
|
56
|
+
// A new `@unique` column needs its unique too, and on SQLite that cannot
|
|
57
|
+
// ride along with the ADD COLUMN — SQLite refuses to add a UNIQUE column.
|
|
58
|
+
if (field.isUnique && !field.isId && !dialect.supportsUniqueInlineOnAddColumn) {
|
|
59
|
+
const indexName = `UQ_${safeName}_${field.name}`;
|
|
60
|
+
const quoted = dialect.quote(indexName);
|
|
61
|
+
if ((await db.query(dialect.uniqueConstraintExists(tableName, indexName))).length === 0) {
|
|
62
|
+
db.log(`Creating unique index ${quoted} on table ${sqlTableName}...`);
|
|
63
|
+
await db.execute(dialect.createUniqueIndex(quoted, sqlTableName, quoteFields([field.name])));
|
|
64
|
+
result.uniqueConstraintsAdded += 1;
|
|
65
|
+
}
|
|
66
|
+
}
|
|
67
|
+
}
|
|
68
|
+
result.tablesAltered += 1;
|
|
69
|
+
}
|
|
70
|
+
// Unique constraints that are not part of the column definitions above.
|
|
71
|
+
for (const entry of compoundUniques) {
|
|
72
|
+
if (await hasUniqueConstraint(db, dialect, tableName, entry.name))
|
|
73
|
+
continue;
|
|
74
|
+
const quoted = dialect.quote(entry.name);
|
|
75
|
+
if (dialect.supportsNamedUniqueConstraint) {
|
|
76
|
+
db.log(`Adding compound unique constraint ${quoted} to table ${sqlTableName}...`);
|
|
77
|
+
await db.execute(dialect.addUniqueConstraint(quoted, sqlTableName, quoteFields(entry.fields)));
|
|
78
|
+
}
|
|
79
|
+
else {
|
|
80
|
+
db.log(`Creating unique index ${quoted} on table ${sqlTableName}...`);
|
|
81
|
+
await db.execute(dialect.createUniqueIndex(quoted, sqlTableName, quoteFields(entry.fields)));
|
|
82
|
+
result.indexesCreated += 1;
|
|
83
|
+
result.uniqueConstraintsAdded += 1;
|
|
84
|
+
}
|
|
85
|
+
result.uniqueConstraintsAdded += 1;
|
|
86
|
+
}
|
|
87
|
+
for (const [idx, directive] of model.indexes.entries()) {
|
|
88
|
+
const name = (0, push_schema_1.directiveName)(directive, model.tableName, 'index', idx);
|
|
89
|
+
if ((await db.query(dialect.indexExists(tableName, name))).length > 0)
|
|
90
|
+
continue;
|
|
91
|
+
const quoted = dialect.quote(name);
|
|
92
|
+
db.log(`Creating index ${quoted} on table ${sqlTableName}...`);
|
|
93
|
+
await db.execute(dialect.createIndex(quoted, sqlTableName, quoteFields(directive.fields)));
|
|
94
|
+
result.indexesCreated += 1;
|
|
95
|
+
}
|
|
96
|
+
}
|
|
97
|
+
return result;
|
|
98
|
+
}
|
|
99
|
+
/**
|
|
100
|
+
* Whether the unique is already there.
|
|
101
|
+
*
|
|
102
|
+
* A compound unique created inside `CREATE TABLE` has no separate object to look
|
|
103
|
+
* for on the providers that keep them, so it is treated as present once the table
|
|
104
|
+
* exists; the per-column uniques inside a column definition are handled with the
|
|
105
|
+
* column above.
|
|
106
|
+
*/
|
|
107
|
+
async function hasUniqueConstraint(db, dialect, tableName, name) {
|
|
108
|
+
return (await db.query(dialect.uniqueConstraintExists(tableName, name))).length > 0;
|
|
109
|
+
}
|