@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.
@@ -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;
@@ -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 => `'${prop.name}'`);
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 => `'${prop.name}'`).sort();
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('');
@@ -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.quote(enumName)}: ${this.quote(enumValue)},\n`;
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.quote(enumName)} = ${this.quote(enumValue)},\n`;
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
- return `${this.options.fileName(this.nativeEnum.name)}${extension}`;
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
  }
@@ -20,7 +20,9 @@ function quoteMultiline(val) {
20
20
  }
21
21
  function toPascalCase(name) {
22
22
  const pascal = name
23
- .split(/[_\s-]+/)
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 => `'${prop.name}'`);
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 => `'${prop.name}'`).sort();
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.quote(file.name)};`);
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.quote(file.path)};`);
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.quote(file.path)};`);
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.quote(file.path)};`);
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.quote(file.name)} as ${entity} } from ${this.quote(file.path)};`);
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.quote(file.path)};`);
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
- /* v8 ignore next */
298
- return backtick ? `\`${val.replaceAll('`', '\\``')}\`` : `'${val.replaceAll(`'`, `\\'`)}'`;
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.quote(prop.name);
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.quote(enumName)}: ${formatValue(enumValue)},\n`;
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.quote(enumName)} = ${formatValue(enumValue)},\n`;
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.quote(opt);
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
- options.defaultRaw = `\`${prop.defaultRaw}\``;
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",
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.3"
50
+ "@mikro-orm/sql": "7.2.0-dev.4"
51
51
  },
52
52
  "devDependencies": {
53
- "@mikro-orm/core": "^7.1.7"
53
+ "@mikro-orm/core": "^7.1.11"
54
54
  },
55
55
  "peerDependencies": {
56
- "@mikro-orm/core": "7.2.0-dev.3"
56
+ "@mikro-orm/core": "7.2.0-dev.4"
57
57
  },
58
58
  "engines": {
59
59
  "node": ">= 22.17.0"