@mikro-orm/entity-generator 7.0.17 → 7.0.18-dev.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.
@@ -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,7 @@ 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
+ protected getPartitionByDecl(partitionBy: EntityPartitionBy): Dictionary;
33
34
  protected getEmbeddableDeclOptions(): EmbeddableOptions<unknown>;
34
35
  private getCollectionDecl;
35
36
  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
  }
@@ -478,6 +484,9 @@ export class SourceFile {
478
484
  if (this.meta.comment) {
479
485
  options.comment = this.quote(this.meta.comment);
480
486
  }
487
+ if (this.meta.partitionBy) {
488
+ options.partitionBy = this.getPartitionByDecl(this.meta.partitionBy);
489
+ }
481
490
  if (this.meta.readonly && !this.meta.virtual) {
482
491
  options.readonly = this.meta.readonly;
483
492
  }
@@ -486,6 +495,40 @@ export class SourceFile {
486
495
  }
487
496
  return this.getCollectionDecl(options);
488
497
  }
498
+ getPartitionByDecl(partitionBy) {
499
+ const result = { type: this.quote(partitionBy.type) };
500
+ // Introspected metadata from `toEntityPartitionBy` always emits a string or string[];
501
+ // callback-form expressions only exist in hand-written entity metadata. Fail loud if a
502
+ // callback ever reaches the generator — stringifying `fn.toString()` would produce source
503
+ // that re-imports nothing and will not compile.
504
+ if (typeof partitionBy.expression === 'function') {
505
+ throw new Error(`Cannot emit entity source for ${this.meta.className}: partitionBy.expression is a callback. ` +
506
+ `Entity generator expects string or string[] expressions from catalog introspection.`);
507
+ }
508
+ const expression = partitionBy.expression;
509
+ if (Array.isArray(expression)) {
510
+ result.expression = expression.map(key => this.quote(String(key)));
511
+ }
512
+ else {
513
+ result.expression = this.quote(String(expression));
514
+ }
515
+ if (partitionBy.type === 'hash') {
516
+ result.partitions = Array.isArray(partitionBy.partitions)
517
+ ? partitionBy.partitions.map(name => this.quote(String(name)))
518
+ : partitionBy.partitions;
519
+ }
520
+ else {
521
+ result.partitions = partitionBy.partitions.map(partition => {
522
+ const entry = {};
523
+ if (partition.name) {
524
+ entry.name = this.quote(partition.name);
525
+ }
526
+ entry.values = this.quote(partition.values);
527
+ return entry;
528
+ });
529
+ }
530
+ return result;
531
+ }
489
532
  getEmbeddableDeclOptions() {
490
533
  const options = {};
491
534
  const result = this.getCollectionDecl(options);
@@ -618,6 +661,9 @@ export class SourceFile {
618
661
  if (typeof prop.comment === 'string') {
619
662
  options.comment = this.quote(prop.comment);
620
663
  }
664
+ if (typeof prop.collation === 'string') {
665
+ options.collation = this.quote(prop.collation);
666
+ }
621
667
  // TODO: Composite FKs with default values require additions to default/defaultRaw that are not yet supported.
622
668
  if (prop.fieldNames?.length <= 1) {
623
669
  if (typeof prop.defaultRaw !== 'undefined' &&
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.0.17",
3
+ "version": "7.0.18-dev.1",
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.0.17"
50
+ "@mikro-orm/sql": "7.0.18-dev.1"
51
51
  },
52
52
  "devDependencies": {
53
53
  "@mikro-orm/core": "^7.0.17"
54
54
  },
55
55
  "peerDependencies": {
56
- "@mikro-orm/core": "7.0.17"
56
+ "@mikro-orm/core": "7.0.18-dev.1"
57
57
  },
58
58
  "engines": {
59
59
  "node": ">= 22.17.0"