@mikro-orm/entity-generator 7.1.0-dev.9 → 7.1.0

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.
@@ -6,6 +6,7 @@ import { writeFile } from 'node:fs/promises';
6
6
  import { DefineEntitySourceFile } from './DefineEntitySourceFile.js';
7
7
  import { EntitySchemaSourceFile } from './EntitySchemaSourceFile.js';
8
8
  import { NativeEnumSourceFile } from './NativeEnumSourceFile.js';
9
+ import { RoutineSourceFile } from './RoutineSourceFile.js';
9
10
  import { SourceFile } from './SourceFile.js';
10
11
  /** Generates entity source files by introspecting an existing database schema. */
11
12
  export class EntityGenerator {
@@ -33,6 +34,8 @@ export class EntityGenerator {
33
34
  async generate(options = {}) {
34
35
  options = Utils.mergeConfig({}, this.#config.get('entityGenerator'), options);
35
36
  const schema = await DatabaseSchema.create(this.#connection, this.#platform, this.#config, undefined, undefined, options.takeTables, options.skipTables);
37
+ // The generator emits whatever it can find, so always load routines here.
38
+ await schema.loadRoutines(this.#connection, this.#platform);
36
39
  const metadata = await this.getEntityMetadata(schema, options);
37
40
  const defaultPath = `${this.#config.get('baseDir')}/generated-entities`;
38
41
  const baseDir = fs.normalizePath(options.path ?? defaultPath);
@@ -54,6 +57,9 @@ export class EntityGenerator {
54
57
  for (const nativeEnum of Object.values(schema.getNativeEnums())) {
55
58
  this.#sources.push(new NativeEnumSourceFile({}, this.#namingStrategy, this.#platform, options, nativeEnum));
56
59
  }
60
+ for (const routine of schema.getRoutines()) {
61
+ this.#sources.push(new RoutineSourceFile(routine, this.#namingStrategy, this.#platform, options));
62
+ }
57
63
  const files = this.#sources.map(file => [file.getBaseName(), file.generate()]);
58
64
  if (options.save) {
59
65
  fs.ensureDir(baseDir);
package/README.md CHANGED
@@ -2,7 +2,7 @@
2
2
  <a href="https://mikro-orm.io"><img src="https://raw.githubusercontent.com/mikro-orm/mikro-orm/master/docs/static/img/logo-readme.svg?sanitize=true" alt="MikroORM" /></a>
3
3
  </h1>
4
4
 
5
- TypeScript ORM for Node.js based on Data Mapper, [Unit of Work](https://mikro-orm.io/docs/unit-of-work/) and [Identity Map](https://mikro-orm.io/docs/identity-map/) patterns. Supports MongoDB, MySQL, MariaDB, PostgreSQL, SQLite (including libSQL), MSSQL and Oracle databases.
5
+ TypeScript ORM for Node.js based on Data Mapper, [Unit of Work](https://mikro-orm.io/docs/unit-of-work/) and [Identity Map](https://mikro-orm.io/docs/identity-map/) patterns. Supports MongoDB, MySQL, MariaDB, PostgreSQL (including CockroachDB and PGlite), SQLite (including libSQL), MSSQL and Oracle databases.
6
6
 
7
7
  > Heavily inspired by [Doctrine](https://www.doctrine-project.org/) and [Hibernate](https://hibernate.org/).
8
8
 
@@ -19,6 +19,7 @@ Install a driver package for your database:
19
19
 
20
20
  ```sh
21
21
  npm install @mikro-orm/postgresql # PostgreSQL
22
+ npm install @mikro-orm/pglite # PGlite (embedded PostgreSQL in WASM)
22
23
  npm install @mikro-orm/mysql # MySQL
23
24
  npm install @mikro-orm/mariadb # MariaDB
24
25
  npm install @mikro-orm/sqlite # SQLite
@@ -0,0 +1,17 @@
1
+ import type { GenerateOptions, NamingStrategy, Platform } from '@mikro-orm/core';
2
+ import type { SqlRoutineDef } from '@mikro-orm/sql';
3
+ /** Always emits the class-less `new Routine(...)` form; there is no decorator form for routines. */
4
+ export declare class RoutineSourceFile {
5
+ private readonly routine;
6
+ private readonly namingStrategy;
7
+ private readonly platform;
8
+ private readonly options;
9
+ constructor(routine: SqlRoutineDef, namingStrategy: NamingStrategy, platform: Platform, options: GenerateOptions);
10
+ generate(): string;
11
+ getBaseName(extension?: string): string;
12
+ getClassName(): string;
13
+ private buildConfig;
14
+ private formatParams;
15
+ private formatReturns;
16
+ private emitRoutine;
17
+ }
@@ -0,0 +1,136 @@
1
+ const identifierRegex = /^(?:[$_\p{ID_Start}])(?:[$\p{ID_Continue}])*$/u;
2
+ function quote(val) {
3
+ // Backslashes first so subsequent escapes don't double up the `\` we just added.
4
+ const escaped = val
5
+ .replaceAll('\\', '\\\\')
6
+ .replaceAll('\r', '\\r')
7
+ .replaceAll('\n', '\\n')
8
+ .replaceAll('\t', '\\t')
9
+ .replaceAll(`'`, `\\'`);
10
+ return `'${escaped}'`;
11
+ }
12
+ function safeKey(name) {
13
+ return identifierRegex.test(name) ? name : quote(name);
14
+ }
15
+ function quoteMultiline(val) {
16
+ if (val.includes('\n') || val.includes('`') || val.includes('${')) {
17
+ return `\`${val.replaceAll('\\', '\\\\').replaceAll('`', '\\`').replaceAll('${', '\\${')}\``;
18
+ }
19
+ return quote(val);
20
+ }
21
+ function toPascalCase(name) {
22
+ const pascal = name
23
+ .split(/[_\s-]+/)
24
+ .filter(Boolean)
25
+ .map(part => part.charAt(0).toUpperCase() + part.slice(1))
26
+ .join('');
27
+ // A routine name starting with a digit (e.g. `2fa_check`) would yield an invalid identifier;
28
+ // prefix `_` so the emitted `export const ...` parses.
29
+ return /^[A-Za-z_$]/.test(pascal) ? pascal : `_${pascal}`;
30
+ }
31
+ // `satisfies` ties this list to `RoutineRuntimeType` so the runtime set can't drift from the union.
32
+ const ROUTINE_RUNTIME_TYPES = [
33
+ 'string',
34
+ 'number',
35
+ 'boolean',
36
+ 'bigint',
37
+ 'Buffer',
38
+ 'Date',
39
+ 'object',
40
+ 'any',
41
+ ];
42
+ const ROUTINE_RUNTIME_TYPE_SET = new Set(ROUTINE_RUNTIME_TYPES);
43
+ function narrowRuntimeType(runtimeType) {
44
+ return runtimeType && ROUTINE_RUNTIME_TYPE_SET.has(runtimeType) ? runtimeType : 'any';
45
+ }
46
+ /** Always emits the class-less `new Routine(...)` form; there is no decorator form for routines. */
47
+ export class RoutineSourceFile {
48
+ routine;
49
+ namingStrategy;
50
+ platform;
51
+ options;
52
+ constructor(routine, namingStrategy, platform, options) {
53
+ this.routine = routine;
54
+ this.namingStrategy = namingStrategy;
55
+ this.platform = platform;
56
+ this.options = options;
57
+ }
58
+ generate() {
59
+ return this.emitRoutine(this.getClassName(), this.buildConfig());
60
+ }
61
+ getBaseName(extension = '.ts') {
62
+ return `${this.options.fileName(this.getClassName())}${extension}`;
63
+ }
64
+ getClassName() {
65
+ return toPascalCase(this.routine.name);
66
+ }
67
+ buildConfig() {
68
+ const lines = [];
69
+ lines.push(` name: ${quote(this.routine.name)},`);
70
+ lines.push(` type: ${quote(this.routine.type)},`);
71
+ if (this.routine.schema) {
72
+ lines.push(` schema: ${quote(this.routine.schema)},`);
73
+ }
74
+ if (this.routine.language) {
75
+ lines.push(` language: ${quote(this.routine.language)},`);
76
+ }
77
+ if (this.routine.security) {
78
+ lines.push(` security: ${quote(this.routine.security)},`);
79
+ }
80
+ if (this.routine.deterministic != null) {
81
+ lines.push(` deterministic: ${this.routine.deterministic},`);
82
+ }
83
+ if (this.routine.comment) {
84
+ lines.push(` comment: ${quote(this.routine.comment)},`);
85
+ }
86
+ if (this.routine.params.length > 0) {
87
+ lines.push(` params: ${this.formatParams(this.routine.params)},`);
88
+ }
89
+ else {
90
+ lines.push(` params: {},`);
91
+ }
92
+ if (this.routine.returns) {
93
+ lines.push(` returns: ${this.formatReturns(this.routine.returns)},`);
94
+ }
95
+ if (this.routine.body) {
96
+ lines.push(` body: ${quoteMultiline(this.routine.body)},`);
97
+ }
98
+ else if (this.routine.expression) {
99
+ lines.push(` expression: ${quoteMultiline(this.routine.expression)},`);
100
+ }
101
+ return `{\n${lines.join('\n')}\n}`;
102
+ }
103
+ formatParams(params) {
104
+ const lines = params.map(p => {
105
+ const parts = [`type: ${quote(p.type)}`];
106
+ if (p.direction !== 'in') {
107
+ parts.push(`direction: ${quote(p.direction)}`);
108
+ }
109
+ if (p.direction === 'out' || p.direction === 'inout') {
110
+ parts.push(`ref: true`);
111
+ }
112
+ if (p.nullable) {
113
+ parts.push(`nullable: true`);
114
+ }
115
+ if (p.defaultRaw) {
116
+ parts.push(`defaultRaw: ${quote(p.defaultRaw)}`);
117
+ }
118
+ return ` ${safeKey(p.name)}: { ${parts.join(', ')} },`;
119
+ });
120
+ return `{\n${lines.join('\n')}\n }`;
121
+ }
122
+ formatReturns(returns) {
123
+ const parts = [];
124
+ // Narrow to `RoutineRuntimeType`; unrecognised types collapse to `'any'`.
125
+ const inferred = returns.runtimeType ?? this.platform.getMappedType(returns.type)?.runtimeType;
126
+ parts.push(`runtimeType: ${quote(narrowRuntimeType(inferred))}`);
127
+ parts.push(`columnType: ${quote(returns.type)}`);
128
+ if (returns.nullable) {
129
+ parts.push(`nullable: true`);
130
+ }
131
+ return `{ ${parts.join(', ')} }`;
132
+ }
133
+ emitRoutine(className, config) {
134
+ return `import { Routine } from '@mikro-orm/core';\n\nexport const ${className} = new Routine(${config});\n`;
135
+ }
136
+ }
package/SourceFile.d.ts CHANGED
@@ -1,4 +1,4 @@
1
- import { type Dictionary, type EmbeddableOptions, type EntityMetadata, type EntityOptions, type EntityProperty, type GenerateOptions, type IndexOptions, type NamingStrategy, type OneToOneOptions, type Platform, type UniqueOptions } from '@mikro-orm/core';
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';
2
2
  /**
3
3
  * @see https://github.com/tc39/proposal-regexp-unicode-property-escapes#other-examples
4
4
  */
@@ -30,6 +30,8 @@ export declare class SourceFile {
30
30
  protected serializeObject(options: {}, wordwrap?: number, spaces?: number, level?: number): string;
31
31
  protected serializeValue(val: unknown, wordwrap?: number, spaces?: number, level?: number): unknown;
32
32
  protected getEntityDeclOptions(): EntityOptions<unknown>;
33
+ private isDiscriminatorPropertyUserDefined;
34
+ protected getPartitionByDecl(partitionBy: EntityPartitionBy): Dictionary;
33
35
  protected getEmbeddableDeclOptions(): EmbeddableOptions<unknown>;
34
36
  private getCollectionDecl;
35
37
  private getPropertyDecorator;
package/SourceFile.js CHANGED
@@ -130,6 +130,9 @@ export class SourceFile {
130
130
  else if (typeof index.expression === 'function') {
131
131
  indexOpt.expression = `${index.expression}`.replace(')=>`', ') => `');
132
132
  }
133
+ if (typeof index.where === 'string') {
134
+ indexOpt.where = this.quote(index.where);
135
+ }
133
136
  if (isAtEntityLevel && index.properties) {
134
137
  indexOpt.properties = Utils.asArray(index.properties).map(prop => this.quote('' + prop));
135
138
  }
@@ -170,6 +173,9 @@ export class SourceFile {
170
173
  else if (typeof index.expression === 'function') {
171
174
  uniqueOpt.expression = `${index.expression}`.replace(')=>`', ') => `');
172
175
  }
176
+ if (typeof index.where === 'string') {
177
+ uniqueOpt.where = this.quote(index.where);
178
+ }
173
179
  if (isAtEntityLevel && index.properties) {
174
180
  uniqueOpt.properties = Utils.asArray(index.properties).map(prop => this.quote('' + prop));
175
181
  }
@@ -397,8 +403,11 @@ export class SourceFile {
397
403
  const enumTypeName = this.namingStrategy.getEnumTypeName(prop.fieldNames[0], this.meta.collection, this.meta.schema);
398
404
  const padding = ' '.repeat(padLeft);
399
405
  const enumValues = prop.items;
406
+ // numeric enums arrive as strings; emit raw numbers so the generated TS reads `Foo: 1` not `Foo: '1'`
407
+ const allNumeric = enumValues.length > 0 && enumValues.every(item => /^-?\d+$/.test(item));
408
+ const formatValue = (item) => (allNumeric ? item : this.quote(item));
400
409
  if (enumMode === 'union-type') {
401
- return `export type ${enumTypeName} = ${enumValues.map(item => this.quote(item)).join(' | ')};\n`;
410
+ return `export type ${enumTypeName} = ${enumValues.map(formatValue).join(' | ')};\n`;
402
411
  }
403
412
  let ret = '';
404
413
  if (enumMode === 'dictionary') {
@@ -410,10 +419,10 @@ export class SourceFile {
410
419
  for (const enumValue of enumValues) {
411
420
  const enumName = this.namingStrategy.enumValueToEnumProperty(enumValue, prop.fieldNames[0], this.meta.collection, this.meta.schema);
412
421
  if (enumMode === 'dictionary') {
413
- ret += `${padding}${identifierRegex.test(enumName) ? enumName : this.quote(enumName)}: ${this.quote(enumValue)},\n`;
422
+ ret += `${padding}${identifierRegex.test(enumName) ? enumName : this.quote(enumName)}: ${formatValue(enumValue)},\n`;
414
423
  }
415
424
  else {
416
- ret += `${padding}${identifierRegex.test(enumName) ? enumName : this.quote(enumName)} = ${this.quote(enumValue)},\n`;
425
+ ret += `${padding}${identifierRegex.test(enumName) ? enumName : this.quote(enumName)} = ${formatValue(enumValue)},\n`;
417
426
  }
418
427
  }
419
428
  if (enumMode === 'dictionary') {
@@ -475,24 +484,64 @@ export class SourceFile {
475
484
  if (this.meta.comment) {
476
485
  options.comment = this.quote(this.meta.comment);
477
486
  }
487
+ if (this.meta.partitionBy) {
488
+ options.partitionBy = this.getPartitionByDecl(this.meta.partitionBy);
489
+ }
478
490
  if (this.meta.readonly && !this.meta.virtual) {
479
491
  options.readonly = this.meta.readonly;
480
492
  }
481
493
  if (this.meta.virtual) {
482
494
  options.virtual = this.meta.virtual;
483
495
  }
484
- return this.getCollectionDecl(options);
496
+ // when the discriminator references an explicitly defined property, emit the property-oriented
497
+ // `discriminator` name; fall back to `discriminatorColumn` when the discriminator property is
498
+ // auto-managed by the ORM and not part of the entity definition.
499
+ const key = this.isDiscriminatorPropertyUserDefined() ? 'discriminator' : 'discriminatorColumn';
500
+ return this.getCollectionDecl(options, key);
485
501
  }
486
- getEmbeddableDeclOptions() {
487
- const options = {};
488
- const result = this.getCollectionDecl(options);
489
- if (result.discriminatorColumn) {
490
- result.discriminator = result.discriminatorColumn;
491
- delete result.discriminatorColumn;
502
+ isDiscriminatorPropertyUserDefined() {
503
+ const name = this.meta.discriminatorColumn;
504
+ return !!name && this.meta.properties[name]?.userDefined !== false;
505
+ }
506
+ getPartitionByDecl(partitionBy) {
507
+ const result = { type: this.quote(partitionBy.type) };
508
+ // Introspected metadata from `toEntityPartitionBy` always emits a string or string[];
509
+ // callback-form expressions only exist in hand-written entity metadata. Fail loud if a
510
+ // callback ever reaches the generator — stringifying `fn.toString()` would produce source
511
+ // that re-imports nothing and will not compile.
512
+ if (typeof partitionBy.expression === 'function') {
513
+ throw new Error(`Cannot emit entity source for ${this.meta.className}: partitionBy.expression is a callback. ` +
514
+ `Entity generator expects string or string[] expressions from catalog introspection.`);
515
+ }
516
+ const expression = partitionBy.expression;
517
+ if (Array.isArray(expression)) {
518
+ result.expression = expression.map(key => this.quote(String(key)));
519
+ }
520
+ else {
521
+ result.expression = this.quote(String(expression));
522
+ }
523
+ if (partitionBy.type === 'hash') {
524
+ result.partitions = Array.isArray(partitionBy.partitions)
525
+ ? partitionBy.partitions.map(name => this.quote(String(name)))
526
+ : partitionBy.partitions;
527
+ }
528
+ else {
529
+ result.partitions = partitionBy.partitions.map(partition => {
530
+ const entry = {};
531
+ if (partition.name) {
532
+ entry.name = this.quote(partition.name);
533
+ }
534
+ entry.values = this.quote(partition.values);
535
+ return entry;
536
+ });
492
537
  }
493
538
  return result;
494
539
  }
495
- getCollectionDecl(options) {
540
+ getEmbeddableDeclOptions() {
541
+ const options = {};
542
+ return this.getCollectionDecl(options, 'discriminator');
543
+ }
544
+ getCollectionDecl(options, discriminatorKey) {
496
545
  if (this.meta.abstract) {
497
546
  options.abstract = true;
498
547
  }
@@ -503,7 +552,7 @@ export class SourceFile {
503
552
  : this.meta.discriminatorValue;
504
553
  }
505
554
  if (this.meta.discriminatorColumn) {
506
- options.discriminatorColumn = this.quote(this.meta.discriminatorColumn);
555
+ options[discriminatorKey] = this.quote(this.meta.discriminatorColumn);
507
556
  }
508
557
  if (this.meta.discriminatorMap) {
509
558
  options.discriminatorMap = Object.fromEntries(Object.entries(this.meta.discriminatorMap).map(([discriminatorValue, cls]) => [
@@ -615,6 +664,9 @@ export class SourceFile {
615
664
  if (typeof prop.comment === 'string') {
616
665
  options.comment = this.quote(prop.comment);
617
666
  }
667
+ if (typeof prop.collation === 'string') {
668
+ options.collation = this.quote(prop.collation);
669
+ }
618
670
  // TODO: Composite FKs with default values require additions to default/defaultRaw that are not yet supported.
619
671
  if (prop.fieldNames?.length <= 1) {
620
672
  if (typeof prop.defaultRaw !== 'undefined' &&
@@ -708,7 +760,9 @@ export class SourceFile {
708
760
  }
709
761
  if (prop.enum) {
710
762
  if (this.options.enumMode === 'union-type') {
711
- options.items = `[${prop.items.map(item => this.quote(item)).join(', ')}]`;
763
+ const items = prop.items;
764
+ const allNumeric = items.length > 0 && items.every(item => /^-?\d+$/.test(item));
765
+ options.items = `[${items.map(item => (allNumeric ? item : this.quote(item))).join(', ')}]`;
712
766
  }
713
767
  else if (prop.nativeEnumName) {
714
768
  const enumClassName = this.namingStrategy.getEnumClassName(prop.nativeEnumName, undefined, this.meta.schema);
package/index.d.ts CHANGED
@@ -3,3 +3,4 @@
3
3
  * @module entity-generator
4
4
  */
5
5
  export * from './EntityGenerator.js';
6
+ export * from './RoutineSourceFile.js';
package/index.js CHANGED
@@ -3,3 +3,4 @@
3
3
  * @module entity-generator
4
4
  */
5
5
  export * from './EntityGenerator.js';
6
+ export * from './RoutineSourceFile.js';
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@mikro-orm/entity-generator",
3
- "version": "7.1.0-dev.9",
3
+ "version": "7.1.0",
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.0-dev.9"
50
+ "@mikro-orm/sql": "7.1.0"
51
51
  },
52
52
  "devDependencies": {
53
- "@mikro-orm/core": "^7.0.11"
53
+ "@mikro-orm/core": "^7.1.0"
54
54
  },
55
55
  "peerDependencies": {
56
- "@mikro-orm/core": "7.1.0-dev.9"
56
+ "@mikro-orm/core": "7.1.0"
57
57
  },
58
58
  "engines": {
59
59
  "node": ">= 22.17.0"