@mikro-orm/entity-generator 7.1.9-dev.1 → 7.1.9-dev.11

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.
@@ -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) {
@@ -64,7 +64,7 @@ export class EntitySchemaSourceFile extends SourceFile {
64
64
  }
65
65
  }
66
66
  if (primaryProps.length > 0) {
67
- const primaryPropNames = primaryProps.map(prop => `'${prop.name}'`);
67
+ const primaryPropNames = primaryProps.map(prop => this.quoteKey(prop.name));
68
68
  if (primaryProps.length > 1) {
69
69
  classBody += `${' '.repeat(2)}[${this.referenceCoreImport('PrimaryKeyProp')}]?: [${primaryPropNames.join(', ')}];\n`;
70
70
  }
@@ -73,7 +73,7 @@ export class EntitySchemaSourceFile extends SourceFile {
73
73
  }
74
74
  }
75
75
  if (eagerProperties.length > 0) {
76
- const eagerPropertyNames = eagerProperties.map(prop => `'${prop.name}'`).sort();
76
+ const eagerPropertyNames = eagerProperties.map(prop => this.quoteKey(prop.name)).sort();
77
77
  classBody += `${' '.repeat(2)}[${this.referenceCoreImport('EagerProps')}]?: ${eagerPropertyNames.join(' | ')};\n`;
78
78
  }
79
79
  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
@@ -25,6 +25,8 @@ export declare class SourceFile {
25
25
  protected getEntityClass(classBody: string): string;
26
26
  getBaseName(extension?: string): string;
27
27
  protected quote(val: string): string;
28
+ /** For positions that reject a template literal: object keys, import bindings/specifiers, string literal types. */
29
+ protected quoteKey(val: string): string;
28
30
  protected getPropertyDefinition(prop: EntityProperty, padLeft: number): string;
29
31
  protected getEnumClassDefinition(prop: EntityProperty, padLeft: number): string;
30
32
  protected serializeObject(options: {}, wordwrap?: number, spaces?: number, level?: number): string;
package/SourceFile.js CHANGED
@@ -79,7 +79,7 @@ export class SourceFile {
79
79
  }
80
80
  });
81
81
  if (primaryProps.length > 0) {
82
- const primaryPropNames = primaryProps.map(prop => `'${prop.name}'`);
82
+ const primaryPropNames = primaryProps.map(prop => this.quoteKey(prop.name));
83
83
  if (primaryProps.length > 1) {
84
84
  classHead += `\n${' '.repeat(2)}[${this.referenceCoreImport('PrimaryKeyProp')}]?: [${primaryPropNames.join(', ')}];\n`;
85
85
  }
@@ -88,7 +88,7 @@ export class SourceFile {
88
88
  }
89
89
  }
90
90
  if (eagerProperties.length > 0) {
91
- const eagerPropertyNames = eagerProperties.map(prop => `'${prop.name}'`).sort();
91
+ const eagerPropertyNames = eagerProperties.map(prop => this.quoteKey(prop.name)).sort();
92
92
  classHead += `\n${' '.repeat(2)}[${this.referenceCoreImport('EagerProps')}]?: ${eagerPropertyNames.join(' | ')};\n`;
93
93
  }
94
94
  ret += this.getEntityClass(classBody ? `${classHead}\n${classBody}` : classHead);
@@ -242,22 +242,22 @@ export class SourceFile {
242
242
  if (file.name === '') {
243
243
  continue;
244
244
  }
245
- importMap.set(file.path, `import ${this.quote(file.name)};`);
245
+ importMap.set(file.path, `import ${this.quoteKey(file.name)};`);
246
246
  continue;
247
247
  }
248
248
  if (file.name === '') {
249
- importMap.set(file.path, `import * as ${entity} from ${this.quote(file.path)};`);
249
+ importMap.set(file.path, `import * as ${entity} from ${this.quoteKey(file.path)};`);
250
250
  continue;
251
251
  }
252
252
  if (file.name === 'default') {
253
- importMap.set(file.path, `import ${entity} from ${this.quote(file.path)};`);
253
+ importMap.set(file.path, `import ${entity} from ${this.quoteKey(file.path)};`);
254
254
  continue;
255
255
  }
256
256
  if (file.name === entity) {
257
- importMap.set(file.path, `import { ${entity} } from ${this.quote(file.path)};`);
257
+ importMap.set(file.path, `import { ${entity} } from ${this.quoteKey(file.path)};`);
258
258
  continue;
259
259
  }
260
- importMap.set(file.path, `import { ${identifierRegex.test(file.name) ? file.name : this.quote(file.name)} as ${entity} } from ${this.quote(file.path)};`);
260
+ importMap.set(file.path, `import { ${identifierRegex.test(file.name) ? file.name : this.quoteKey(file.name)} as ${entity} } from ${this.quoteKey(file.path)};`);
261
261
  }
262
262
  if (this.enumImports.size) {
263
263
  for (const [name, exports] of this.enumImports.entries()) {
@@ -265,7 +265,7 @@ export class SourceFile {
265
265
  path: `${basePath}/${this.options.fileName(name)}${extension}`,
266
266
  name,
267
267
  };
268
- importMap.set(file.path, `import { ${exports.join(', ')} } from ${this.quote(file.path)};`);
268
+ importMap.set(file.path, `import { ${exports.join(', ')} } from ${this.quoteKey(file.path)};`);
269
269
  }
270
270
  }
271
271
  for (const key of [...importMap.keys()].sort()) {
@@ -294,12 +294,26 @@ export class SourceFile {
294
294
  }
295
295
  quote(val) {
296
296
  const backtick = val.startsWith(`'`) || val.includes('\n');
297
- /* v8 ignore next */
298
- return backtick ? `\`${val.replaceAll('`', '\\``')}\`` : `'${val.replaceAll(`'`, `\\'`)}'`;
297
+ // Backslashes first, so the escapes added below don't get neutralized by a preceding `\`.
298
+ // A raw `\r` is a syntax error in a string literal and silently normalizes to `\n` in a template one.
299
+ const escaped = val.replaceAll('\\', '\\\\').replaceAll('\r', '\\r');
300
+ return backtick
301
+ ? `\`${escaped.replaceAll('`', '\\`').replaceAll('${', '\\${')}\``
302
+ : `'${escaped.replaceAll(`'`, `\\'`)}'`;
303
+ }
304
+ /** For positions that reject a template literal: object keys, import bindings/specifiers, string literal types. */
305
+ quoteKey(val) {
306
+ const escaped = val
307
+ .replaceAll('\\', '\\\\')
308
+ .replaceAll('\r', '\\r')
309
+ .replaceAll('\n', '\\n')
310
+ .replaceAll('\t', '\\t')
311
+ .replaceAll(`'`, `\\'`);
312
+ return `'${escaped}'`;
299
313
  }
300
314
  getPropertyDefinition(prop, padLeft) {
301
315
  const padding = ' '.repeat(padLeft);
302
- const propName = identifierRegex.test(prop.name) ? prop.name : this.quote(prop.name);
316
+ const propName = identifierRegex.test(prop.name) ? prop.name : this.quoteKey(prop.name);
303
317
  const enumMode = this.options.enumMode;
304
318
  let hiddenType = '';
305
319
  if (prop.hidden) {
@@ -419,10 +433,10 @@ export class SourceFile {
419
433
  for (const enumValue of enumValues) {
420
434
  const enumName = this.namingStrategy.enumValueToEnumProperty(enumValue, prop.fieldNames[0], this.meta.collection, this.meta.schema);
421
435
  if (enumMode === 'dictionary') {
422
- ret += `${padding}${identifierRegex.test(enumName) ? enumName : this.quote(enumName)}: ${formatValue(enumValue)},\n`;
436
+ ret += `${padding}${identifierRegex.test(enumName) ? enumName : this.quoteKey(enumName)}: ${formatValue(enumValue)},\n`;
423
437
  }
424
438
  else {
425
- ret += `${padding}${identifierRegex.test(enumName) ? enumName : this.quote(enumName)} = ${formatValue(enumValue)},\n`;
439
+ ret += `${padding}${identifierRegex.test(enumName) ? enumName : this.quoteKey(enumName)} = ${formatValue(enumValue)},\n`;
426
440
  }
427
441
  }
428
442
  if (enumMode === 'dictionary') {
@@ -452,7 +466,7 @@ export class SourceFile {
452
466
  const entries = Object.entries(options);
453
467
  return `{${doIndent ? `\n${' '.repeat(spaces)}` : ' '}${entries
454
468
  .map(([opt, val]) => {
455
- const key = identifierRegex.test(opt) ? opt : this.quote(opt);
469
+ const key = identifierRegex.test(opt) ? opt : this.quoteKey(opt);
456
470
  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
471
  })
458
472
  .join(sep)}${doIndent ? `${entries.length > 0 ? ',\n' : ''}${' '.repeat(spaces + level * 2)}` : ' '}}`;
@@ -673,7 +687,8 @@ export class SourceFile {
673
687
  prop.defaultRaw !== 'null' &&
674
688
  prop.defaultRaw !== '' &&
675
689
  prop.defaultRaw !== (typeof prop.default === 'string' ? this.quote(prop.default) : `${prop.default}`)) {
676
- options.defaultRaw = `\`${prop.defaultRaw}\``;
690
+ // kept as a template literal for readability, but raw SQL may contain `${`, which would otherwise interpolate
691
+ options.defaultRaw = `\`${prop.defaultRaw.replaceAll('\\', '\\\\').replaceAll('`', '\\`').replaceAll('${', '\\${')}\``;
677
692
  }
678
693
  else if (!(typeof prop.default === 'undefined' || prop.default === null) &&
679
694
  (prop.ref ||
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@mikro-orm/entity-generator",
3
- "version": "7.1.9-dev.1",
3
+ "version": "7.1.9-dev.11",
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.1.9-dev.1"
50
+ "@mikro-orm/sql": "7.1.9-dev.11"
51
51
  },
52
52
  "devDependencies": {
53
53
  "@mikro-orm/core": "^7.1.8"
54
54
  },
55
55
  "peerDependencies": {
56
- "@mikro-orm/core": "7.1.9-dev.1"
56
+ "@mikro-orm/core": "7.1.9-dev.11"
57
57
  },
58
58
  "engines": {
59
59
  "node": ">= 22.17.0"