@mikro-orm/entity-generator 7.2.0-dev.3 → 7.2.0-dev.4
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/DefineEntitySourceFile.js +3 -0
- package/EntityGenerator.js +10 -1
- package/EntitySchemaSourceFile.js +5 -2
- package/NativeEnumSourceFile.js +6 -3
- package/RoutineSourceFile.js +3 -1
- package/SourceFile.d.ts +4 -1
- package/SourceFile.js +41 -15
- package/package.json +4 -4
|
@@ -38,6 +38,9 @@ export class DefineEntitySourceFile extends EntitySchemaSourceFile {
|
|
|
38
38
|
if (this.meta.uniques.length > 0) {
|
|
39
39
|
entitySchemaOptions.uniques = this.meta.uniques.map(index => this.getUniqueOptions(index));
|
|
40
40
|
}
|
|
41
|
+
if (this.meta.checks.length > 0) {
|
|
42
|
+
entitySchemaOptions.checks = this.meta.checks.map(check => this.getCheckOptions(check));
|
|
43
|
+
}
|
|
41
44
|
entitySchemaOptions.properties = Object.fromEntries(Object.entries(this.meta.properties).map(([name, prop]) => [name, this.getPropertyBuilder(prop)]));
|
|
42
45
|
// Force top level and properties to be indented, regardless of line length
|
|
43
46
|
entitySchemaOptions[Config] = true;
|
package/EntityGenerator.js
CHANGED
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
import { EntitySchema, ReferenceKind, types, Utils, } from '@mikro-orm/core';
|
|
2
2
|
import { DatabaseSchema, } from '@mikro-orm/sql';
|
|
3
3
|
import { fs } from '@mikro-orm/core/fs-utils';
|
|
4
|
-
import { dirname, join } from 'node:path';
|
|
4
|
+
import { dirname, join, relative, resolve } from 'node:path';
|
|
5
5
|
import { writeFile } from 'node:fs/promises';
|
|
6
6
|
import { DefineEntitySourceFile } from './DefineEntitySourceFile.js';
|
|
7
7
|
import { EntitySchemaSourceFile } from './EntitySchemaSourceFile.js';
|
|
@@ -62,6 +62,15 @@ export class EntityGenerator {
|
|
|
62
62
|
}
|
|
63
63
|
const files = this.#sources.map(file => [file.getBaseName(), file.generate()]);
|
|
64
64
|
if (options.save) {
|
|
65
|
+
// generated files may only land in the project folder, or under the configured path when
|
|
66
|
+
// that points elsewhere, so a file name can never reach an arbitrary filesystem location
|
|
67
|
+
const allowedRoots = [resolve(this.#config.get('baseDir')), resolve(baseDir)];
|
|
68
|
+
for (const [fileName] of files) {
|
|
69
|
+
const target = resolve(baseDir, fileName);
|
|
70
|
+
if (!allowedRoots.some(root => !relative(root, target).startsWith('..'))) {
|
|
71
|
+
throw new Error(`Cannot generate '${fileName}', it resolves outside of the project folder`);
|
|
72
|
+
}
|
|
73
|
+
}
|
|
65
74
|
fs.ensureDir(baseDir);
|
|
66
75
|
const promises = [];
|
|
67
76
|
for (const [fileName, data] of files) {
|
|
@@ -33,6 +33,9 @@ export class EntitySchemaSourceFile extends SourceFile {
|
|
|
33
33
|
if (this.meta.uniques.length > 0) {
|
|
34
34
|
entitySchemaOptions.uniques = this.meta.uniques.map(index => this.getUniqueOptions(index));
|
|
35
35
|
}
|
|
36
|
+
if (this.meta.checks.length > 0) {
|
|
37
|
+
entitySchemaOptions.checks = this.meta.checks.map(check => this.getCheckOptions(check));
|
|
38
|
+
}
|
|
36
39
|
entitySchemaOptions.properties = Object.fromEntries(Object.entries(this.meta.properties).map(([name, prop]) => [name, this.getPropertyOptions(prop)]));
|
|
37
40
|
// Force top level and properties to be indented, regardless of line length
|
|
38
41
|
entitySchemaOptions[Config] = true;
|
|
@@ -64,7 +67,7 @@ export class EntitySchemaSourceFile extends SourceFile {
|
|
|
64
67
|
}
|
|
65
68
|
}
|
|
66
69
|
if (primaryProps.length > 0) {
|
|
67
|
-
const primaryPropNames = primaryProps.map(prop =>
|
|
70
|
+
const primaryPropNames = primaryProps.map(prop => this.quoteKey(prop.name));
|
|
68
71
|
if (primaryProps.length > 1) {
|
|
69
72
|
classBody += `${' '.repeat(2)}[${this.referenceCoreImport('PrimaryKeyProp')}]?: [${primaryPropNames.join(', ')}];\n`;
|
|
70
73
|
}
|
|
@@ -73,7 +76,7 @@ export class EntitySchemaSourceFile extends SourceFile {
|
|
|
73
76
|
}
|
|
74
77
|
}
|
|
75
78
|
if (eagerProperties.length > 0) {
|
|
76
|
-
const eagerPropertyNames = eagerProperties.map(prop =>
|
|
79
|
+
const eagerPropertyNames = eagerProperties.map(prop => this.quoteKey(prop.name)).sort();
|
|
77
80
|
classBody += `${' '.repeat(2)}[${this.referenceCoreImport('EagerProps')}]?: ${eagerPropertyNames.join(' | ')};\n`;
|
|
78
81
|
}
|
|
79
82
|
classBody += props.join('');
|
package/NativeEnumSourceFile.js
CHANGED
|
@@ -24,10 +24,10 @@ export class NativeEnumSourceFile extends SourceFile {
|
|
|
24
24
|
for (const enumValue of enumValues) {
|
|
25
25
|
const enumName = this.namingStrategy.enumValueToEnumProperty(enumValue, this.nativeEnum.name, '', this.nativeEnum.schema);
|
|
26
26
|
if (enumMode === 'dictionary') {
|
|
27
|
-
ret += `${padding}${identifierRegex.test(enumName) ? enumName : this.
|
|
27
|
+
ret += `${padding}${identifierRegex.test(enumName) ? enumName : this.quoteKey(enumName)}: ${this.quote(enumValue)},\n`;
|
|
28
28
|
}
|
|
29
29
|
else {
|
|
30
|
-
ret += `${padding}${identifierRegex.test(enumName) ? enumName : this.
|
|
30
|
+
ret += `${padding}${identifierRegex.test(enumName) ? enumName : this.quoteKey(enumName)} = ${this.quote(enumValue)},\n`;
|
|
31
31
|
}
|
|
32
32
|
}
|
|
33
33
|
if (enumMode === 'dictionary') {
|
|
@@ -42,6 +42,9 @@ export class NativeEnumSourceFile extends SourceFile {
|
|
|
42
42
|
return ret;
|
|
43
43
|
}
|
|
44
44
|
getBaseName(extension = '.ts') {
|
|
45
|
-
|
|
45
|
+
// the enum name comes from the database and ends up in a path, so path separators
|
|
46
|
+
// (and anything else that cannot appear in a file name) collapse to `_`
|
|
47
|
+
const name = this.nativeEnum.name.replaceAll(/[^\p{L}\p{N}$_-]+/gu, '_');
|
|
48
|
+
return `${this.options.fileName(name)}${extension}`;
|
|
46
49
|
}
|
|
47
50
|
}
|
package/RoutineSourceFile.js
CHANGED
|
@@ -20,7 +20,9 @@ function quoteMultiline(val) {
|
|
|
20
20
|
}
|
|
21
21
|
function toPascalCase(name) {
|
|
22
22
|
const pascal = name
|
|
23
|
-
|
|
23
|
+
// anything that cannot appear in an identifier acts as a word separator, which keeps
|
|
24
|
+
// path separators out of both the emitted const name and the generated file name
|
|
25
|
+
.split(/[^\p{L}\p{N}$]+/u)
|
|
24
26
|
.filter(Boolean)
|
|
25
27
|
.map(part => part.charAt(0).toUpperCase() + part.slice(1))
|
|
26
28
|
.join('');
|
package/SourceFile.d.ts
CHANGED
|
@@ -1,4 +1,4 @@
|
|
|
1
|
-
import { type Dictionary, type EmbeddableOptions, type EntityMetadata, type EntityOptions, type EntityPartitionBy, type EntityProperty, type GenerateOptions, type IndexOptions, type NamingStrategy, type OneToOneOptions, type Platform, type UniqueOptions } from '@mikro-orm/core';
|
|
1
|
+
import { type CheckConstraint, type Dictionary, type EmbeddableOptions, type EntityMetadata, type EntityOptions, type EntityPartitionBy, type EntityProperty, type GenerateOptions, type IndexOptions, type NamingStrategy, type OneToOneOptions, type Platform, type UniqueOptions } from '@mikro-orm/core';
|
|
2
2
|
/**
|
|
3
3
|
* @see https://github.com/tc39/proposal-regexp-unicode-property-escapes#other-examples
|
|
4
4
|
*/
|
|
@@ -21,10 +21,13 @@ export declare class SourceFile {
|
|
|
21
21
|
private getColumnOptions;
|
|
22
22
|
protected getIndexOptions(index: EntityMetadata['indexes'][number], isAtEntityLevel?: boolean): IndexOptions<Dictionary, string>;
|
|
23
23
|
protected getUniqueOptions(index: EntityMetadata['uniques'][number], isAtEntityLevel?: boolean): UniqueOptions<Dictionary, string>;
|
|
24
|
+
protected getCheckOptions(check: EntityMetadata['checks'][number]): CheckConstraint<Dictionary>;
|
|
24
25
|
protected generateImports(): string;
|
|
25
26
|
protected getEntityClass(classBody: string): string;
|
|
26
27
|
getBaseName(extension?: string): string;
|
|
27
28
|
protected quote(val: string): string;
|
|
29
|
+
/** For positions that reject a template literal: object keys, import bindings/specifiers, string literal types. */
|
|
30
|
+
protected quoteKey(val: string): string;
|
|
28
31
|
protected getPropertyDefinition(prop: EntityProperty, padLeft: number): string;
|
|
29
32
|
protected getEnumClassDefinition(prop: EntityProperty, padLeft: number): string;
|
|
30
33
|
protected serializeObject(options: {}, wordwrap?: number, spaces?: number, level?: number): string;
|
package/SourceFile.js
CHANGED
|
@@ -45,6 +45,9 @@ export class SourceFile {
|
|
|
45
45
|
}
|
|
46
46
|
ret += `@${this.referenceDecoratorImport('Unique')}(${this.serializeObject(this.getUniqueOptions(index))})\n`;
|
|
47
47
|
}
|
|
48
|
+
for (const check of this.meta.checks) {
|
|
49
|
+
ret += `@${this.referenceDecoratorImport('Check')}(${this.serializeObject(this.getCheckOptions(check))})\n`;
|
|
50
|
+
}
|
|
48
51
|
let classHead = '';
|
|
49
52
|
if (this.meta.className === this.options.customBaseEntityName) {
|
|
50
53
|
const defineConfigTypeSettings = {};
|
|
@@ -79,7 +82,7 @@ export class SourceFile {
|
|
|
79
82
|
}
|
|
80
83
|
});
|
|
81
84
|
if (primaryProps.length > 0) {
|
|
82
|
-
const primaryPropNames = primaryProps.map(prop =>
|
|
85
|
+
const primaryPropNames = primaryProps.map(prop => this.quoteKey(prop.name));
|
|
83
86
|
if (primaryProps.length > 1) {
|
|
84
87
|
classHead += `\n${' '.repeat(2)}[${this.referenceCoreImport('PrimaryKeyProp')}]?: [${primaryPropNames.join(', ')}];\n`;
|
|
85
88
|
}
|
|
@@ -88,7 +91,7 @@ export class SourceFile {
|
|
|
88
91
|
}
|
|
89
92
|
}
|
|
90
93
|
if (eagerProperties.length > 0) {
|
|
91
|
-
const eagerPropertyNames = eagerProperties.map(prop =>
|
|
94
|
+
const eagerPropertyNames = eagerProperties.map(prop => this.quoteKey(prop.name)).sort();
|
|
92
95
|
classHead += `\n${' '.repeat(2)}[${this.referenceCoreImport('EagerProps')}]?: ${eagerPropertyNames.join(' | ')};\n`;
|
|
93
96
|
}
|
|
94
97
|
ret += this.getEntityClass(classBody ? `${classHead}\n${classBody}` : classHead);
|
|
@@ -198,6 +201,14 @@ export class SourceFile {
|
|
|
198
201
|
}
|
|
199
202
|
return uniqueOpt;
|
|
200
203
|
}
|
|
204
|
+
getCheckOptions(check) {
|
|
205
|
+
const checkOpt = {};
|
|
206
|
+
if (typeof check.name === 'string') {
|
|
207
|
+
checkOpt.name = this.quote(check.name);
|
|
208
|
+
}
|
|
209
|
+
checkOpt.expression = this.quote(check.expression);
|
|
210
|
+
return checkOpt;
|
|
211
|
+
}
|
|
201
212
|
generateImports() {
|
|
202
213
|
const imports = new Set();
|
|
203
214
|
if (this.coreImports.size > 0) {
|
|
@@ -242,22 +253,22 @@ export class SourceFile {
|
|
|
242
253
|
if (file.name === '') {
|
|
243
254
|
continue;
|
|
244
255
|
}
|
|
245
|
-
importMap.set(file.path, `import ${this.
|
|
256
|
+
importMap.set(file.path, `import ${this.quoteKey(file.name)};`);
|
|
246
257
|
continue;
|
|
247
258
|
}
|
|
248
259
|
if (file.name === '') {
|
|
249
|
-
importMap.set(file.path, `import * as ${entity} from ${this.
|
|
260
|
+
importMap.set(file.path, `import * as ${entity} from ${this.quoteKey(file.path)};`);
|
|
250
261
|
continue;
|
|
251
262
|
}
|
|
252
263
|
if (file.name === 'default') {
|
|
253
|
-
importMap.set(file.path, `import ${entity} from ${this.
|
|
264
|
+
importMap.set(file.path, `import ${entity} from ${this.quoteKey(file.path)};`);
|
|
254
265
|
continue;
|
|
255
266
|
}
|
|
256
267
|
if (file.name === entity) {
|
|
257
|
-
importMap.set(file.path, `import { ${entity} } from ${this.
|
|
268
|
+
importMap.set(file.path, `import { ${entity} } from ${this.quoteKey(file.path)};`);
|
|
258
269
|
continue;
|
|
259
270
|
}
|
|
260
|
-
importMap.set(file.path, `import { ${identifierRegex.test(file.name) ? file.name : this.
|
|
271
|
+
importMap.set(file.path, `import { ${identifierRegex.test(file.name) ? file.name : this.quoteKey(file.name)} as ${entity} } from ${this.quoteKey(file.path)};`);
|
|
261
272
|
}
|
|
262
273
|
if (this.enumImports.size) {
|
|
263
274
|
for (const [name, exports] of this.enumImports.entries()) {
|
|
@@ -265,7 +276,7 @@ export class SourceFile {
|
|
|
265
276
|
path: `${basePath}/${this.options.fileName(name)}${extension}`,
|
|
266
277
|
name,
|
|
267
278
|
};
|
|
268
|
-
importMap.set(file.path, `import { ${exports.join(', ')} } from ${this.
|
|
279
|
+
importMap.set(file.path, `import { ${exports.join(', ')} } from ${this.quoteKey(file.path)};`);
|
|
269
280
|
}
|
|
270
281
|
}
|
|
271
282
|
for (const key of [...importMap.keys()].sort()) {
|
|
@@ -294,12 +305,26 @@ export class SourceFile {
|
|
|
294
305
|
}
|
|
295
306
|
quote(val) {
|
|
296
307
|
const backtick = val.startsWith(`'`) || val.includes('\n');
|
|
297
|
-
|
|
298
|
-
|
|
308
|
+
// Backslashes first, so the escapes added below don't get neutralized by a preceding `\`.
|
|
309
|
+
// A raw `\r` is a syntax error in a string literal and silently normalizes to `\n` in a template one.
|
|
310
|
+
const escaped = val.replaceAll('\\', '\\\\').replaceAll('\r', '\\r');
|
|
311
|
+
return backtick
|
|
312
|
+
? `\`${escaped.replaceAll('`', '\\`').replaceAll('${', '\\${')}\``
|
|
313
|
+
: `'${escaped.replaceAll(`'`, `\\'`)}'`;
|
|
314
|
+
}
|
|
315
|
+
/** For positions that reject a template literal: object keys, import bindings/specifiers, string literal types. */
|
|
316
|
+
quoteKey(val) {
|
|
317
|
+
const escaped = val
|
|
318
|
+
.replaceAll('\\', '\\\\')
|
|
319
|
+
.replaceAll('\r', '\\r')
|
|
320
|
+
.replaceAll('\n', '\\n')
|
|
321
|
+
.replaceAll('\t', '\\t')
|
|
322
|
+
.replaceAll(`'`, `\\'`);
|
|
323
|
+
return `'${escaped}'`;
|
|
299
324
|
}
|
|
300
325
|
getPropertyDefinition(prop, padLeft) {
|
|
301
326
|
const padding = ' '.repeat(padLeft);
|
|
302
|
-
const propName = identifierRegex.test(prop.name) ? prop.name : this.
|
|
327
|
+
const propName = identifierRegex.test(prop.name) ? prop.name : this.quoteKey(prop.name);
|
|
303
328
|
const enumMode = this.options.enumMode;
|
|
304
329
|
let hiddenType = '';
|
|
305
330
|
if (prop.hidden) {
|
|
@@ -419,10 +444,10 @@ export class SourceFile {
|
|
|
419
444
|
for (const enumValue of enumValues) {
|
|
420
445
|
const enumName = this.namingStrategy.enumValueToEnumProperty(enumValue, prop.fieldNames[0], this.meta.collection, this.meta.schema);
|
|
421
446
|
if (enumMode === 'dictionary') {
|
|
422
|
-
ret += `${padding}${identifierRegex.test(enumName) ? enumName : this.
|
|
447
|
+
ret += `${padding}${identifierRegex.test(enumName) ? enumName : this.quoteKey(enumName)}: ${formatValue(enumValue)},\n`;
|
|
423
448
|
}
|
|
424
449
|
else {
|
|
425
|
-
ret += `${padding}${identifierRegex.test(enumName) ? enumName : this.
|
|
450
|
+
ret += `${padding}${identifierRegex.test(enumName) ? enumName : this.quoteKey(enumName)} = ${formatValue(enumValue)},\n`;
|
|
426
451
|
}
|
|
427
452
|
}
|
|
428
453
|
if (enumMode === 'dictionary') {
|
|
@@ -452,7 +477,7 @@ export class SourceFile {
|
|
|
452
477
|
const entries = Object.entries(options);
|
|
453
478
|
return `{${doIndent ? `\n${' '.repeat(spaces)}` : ' '}${entries
|
|
454
479
|
.map(([opt, val]) => {
|
|
455
|
-
const key = identifierRegex.test(opt) ? opt : this.
|
|
480
|
+
const key = identifierRegex.test(opt) ? opt : this.quoteKey(opt);
|
|
456
481
|
return `${doIndent ? ' '.repeat(level * 2 + (spaces + 2)) : ''}${key}: ${this.serializeValue(val, typeof nextWordwrap === 'number' ? nextWordwrap - key.length - 2 /* ': '.length*/ : undefined, doIndent ? spaces : undefined, level + 1)}`;
|
|
457
482
|
})
|
|
458
483
|
.join(sep)}${doIndent ? `${entries.length > 0 ? ',\n' : ''}${' '.repeat(spaces + level * 2)}` : ' '}}`;
|
|
@@ -673,7 +698,8 @@ export class SourceFile {
|
|
|
673
698
|
prop.defaultRaw !== 'null' &&
|
|
674
699
|
prop.defaultRaw !== '' &&
|
|
675
700
|
prop.defaultRaw !== (typeof prop.default === 'string' ? this.quote(prop.default) : `${prop.default}`)) {
|
|
676
|
-
|
|
701
|
+
// kept as a template literal for readability, but raw SQL may contain `${`, which would otherwise interpolate
|
|
702
|
+
options.defaultRaw = `\`${prop.defaultRaw.replaceAll('\\', '\\\\').replaceAll('`', '\\`').replaceAll('${', '\\${')}\``;
|
|
677
703
|
}
|
|
678
704
|
else if (!(typeof prop.default === 'undefined' || prop.default === null) &&
|
|
679
705
|
(prop.ref ||
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@mikro-orm/entity-generator",
|
|
3
|
-
"version": "7.2.0-dev.
|
|
3
|
+
"version": "7.2.0-dev.4",
|
|
4
4
|
"description": "TypeScript ORM for Node.js based on Data Mapper, Unit of Work and Identity Map patterns. Supports MongoDB, MySQL, PostgreSQL and SQLite databases as well as usage with vanilla JavaScript.",
|
|
5
5
|
"keywords": [
|
|
6
6
|
"data-mapper",
|
|
@@ -47,13 +47,13 @@
|
|
|
47
47
|
"copy": "node ../../scripts/copy.mjs"
|
|
48
48
|
},
|
|
49
49
|
"dependencies": {
|
|
50
|
-
"@mikro-orm/sql": "7.2.0-dev.
|
|
50
|
+
"@mikro-orm/sql": "7.2.0-dev.4"
|
|
51
51
|
},
|
|
52
52
|
"devDependencies": {
|
|
53
|
-
"@mikro-orm/core": "^7.1.
|
|
53
|
+
"@mikro-orm/core": "^7.1.11"
|
|
54
54
|
},
|
|
55
55
|
"peerDependencies": {
|
|
56
|
-
"@mikro-orm/core": "7.2.0-dev.
|
|
56
|
+
"@mikro-orm/core": "7.2.0-dev.4"
|
|
57
57
|
},
|
|
58
58
|
"engines": {
|
|
59
59
|
"node": ">= 22.17.0"
|