Skip to content

Commit

Permalink
feat(entity-generator): added support for generated columns (#5250)
Browse files Browse the repository at this point in the history
  • Loading branch information
boenrobot committed Feb 16, 2024
1 parent 0b6d3dd commit d2186da
Show file tree
Hide file tree
Showing 6 changed files with 592 additions and 0 deletions.
8 changes: 8 additions & 0 deletions packages/entity-generator/src/SourceFile.ts
Original file line number Diff line number Diff line change
Expand Up @@ -505,6 +505,10 @@ export class SourceFile {
options.autoincrement = false;
}
}

if (prop.generated) {
options.generated = typeof prop.generated === 'string' ? this.quote(prop.generated) : `${prop.generated}`;
}
}

protected getManyToManyDecoratorOptions(options: Dictionary, prop: EntityProperty) {
Expand Down Expand Up @@ -609,6 +613,10 @@ export class SourceFile {
if (prop.primary) {
options.primary = true;
}

if (prop.generated) {
options.generated = typeof prop.generated === 'string' ? this.quote(prop.generated) : `${prop.generated}`;
}
}

protected getDecoratorType(prop: EntityProperty): string {
Expand Down
2 changes: 2 additions & 0 deletions packages/knex/src/schema/DatabaseTable.ts
Original file line number Diff line number Diff line change
Expand Up @@ -606,6 +606,7 @@ export class DatabaseTable {
const column = this.getColumn(fk.columnNames[0])!;
columnOptions.default = this.getPropertyDefaultValue(schemaHelper, column, type);
columnOptions.defaultRaw = this.getPropertyDefaultValue(schemaHelper, column, type, true);
columnOptions.generated = column.generated;
columnOptions.nullable = column.nullable;
columnOptions.primary = column.primary;
columnOptions.length = column.length;
Expand Down Expand Up @@ -658,6 +659,7 @@ export class DatabaseTable {
name: prop,
type,
kind,
generated: column.generated,
columnType: column.type,
default: this.getPropertyDefaultValue(schemaHelper, column, type),
defaultRaw: this.getPropertyDefaultValue(schemaHelper, column, type, true),
Expand Down
90 changes: 90 additions & 0 deletions tests/features/entity-generator/GeneratedColumns.mysql.test.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,90 @@
import { MikroORM, Utils } from '@mikro-orm/mysql';
import { EntityGenerator } from '@mikro-orm/entity-generator';

let orm: MikroORM;

const schemaName = 'generated_columns_example';
const schema = `
CREATE TABLE IF NOT EXISTS \`allowed_ages_at_creation\`
(
\`age\` TINYINT UNSIGNED NOT NULL,
PRIMARY KEY (\`age\`)
)
ENGINE = InnoDB;
CREATE TABLE IF NOT EXISTS \`users\`
(
\`id\` INT UNSIGNED NOT NULL AUTO_INCREMENT,
\`first_name\` VARCHAR(100) NOT NULL,
\`last_name\` VARCHAR(100) NOT NULL,
\`full_name\` VARCHAR(200) GENERATED ALWAYS AS (CONCAT(\`first_name\`, ' ', \`last_name\`)) VIRTUAL NOT NULL,
\`created_at\` DATETIME NOT NULL DEFAULT CURRENT_TIMESTAMP,
\`date_of_birth\` DATE NOT NULL,
\`age_at_creation\` TINYINT UNSIGNED GENERATED ALWAYS AS (TIMESTAMPDIFF(YEAR, \`date_of_birth\`, \`created_at\`)) STORED NULL,
PRIMARY KEY (\`id\`),
INDEX \`fk_users_allowed_ages_at_creation_idx\` (\`age_at_creation\` ASC) VISIBLE,
CONSTRAINT \`fk_users_allowed_ages_at_creation\`
FOREIGN KEY (\`age_at_creation\`)
REFERENCES \`allowed_ages_at_creation\` (\`age\`)
ON DELETE CASCADE
ON UPDATE RESTRICT
)
ENGINE = InnoDB;
`;

beforeAll(async () => {
orm = await MikroORM.init({
dbName: schemaName,
port: 3308,
discovery: { warnWhenNoEntities: false },
extensions: [EntityGenerator],
multipleStatements: true,
ensureDatabase: false,
});

if (await orm.schema.ensureDatabase({ create: true })) {
await orm.schema.execute(schema);
}

await orm.close(true);
});

beforeEach(async () => {
orm = await MikroORM.init({
dbName: schemaName,
port: 3308,
discovery: { warnWhenNoEntities: false },
extensions: [EntityGenerator],
multipleStatements: true,
});
});

afterEach(async () => {
await orm.close(true);
});

describe(schemaName, () => {
describe.each([true, false])('entitySchema=%s', entitySchema => {
beforeEach(() => {
orm.config.get('entityGenerator').entitySchema = entitySchema;
});

test('generates from db', async () => {
const dump = await orm.entityGenerator.generate();
expect(dump).toMatchSnapshot('dump');
});

test('as functions from extensions', async () => {
orm.config.get('entityGenerator').onInitialMetadata = metadata => {
const usersMeta = metadata.find(meta => meta.className === 'Users')!;
Object.entries(usersMeta.properties).forEach(([propName, propOptions]) => {
if (typeof propOptions.generated === 'string') {
propOptions.generated = Utils.createFunction(new Map(), `return () => ${JSON.stringify(propOptions.generated)}`);
}
});
};
const dump = await orm.entityGenerator.generate();
expect(dump).toMatchSnapshot('dump');
});
});
});
Original file line number Diff line number Diff line change
@@ -0,0 +1,84 @@
import { MikroORM, Utils } from '@mikro-orm/postgresql';
import { EntityGenerator } from '@mikro-orm/entity-generator';

let orm: MikroORM;

const schemaName = 'generated_columns_example';
const schema = `
CREATE TABLE IF NOT EXISTS "allowed_ages_at_creation"
(
"age" SMALLINT,
PRIMARY KEY ("age")
);
CREATE TABLE IF NOT EXISTS "users"
(
"id" SERIAL PRIMARY KEY,
"first_name" VARCHAR(100) NOT NULL,
"last_name" VARCHAR(100) NOT NULL,
"full_name" VARCHAR(200) GENERATED ALWAYS AS ("first_name" || ' ' || "last_name") STORED NOT NULL,
"created_at" TIMESTAMP WITHOUT TIME ZONE NOT NULL DEFAULT CURRENT_TIMESTAMP,
"date_of_birth" DATE NOT NULL,
"age_at_creation" SMALLINT GENERATED ALWAYS AS (EXTRACT(YEAR FROM "created_at") - EXTRACT(YEAR FROM "date_of_birth")) STORED NULL,
CONSTRAINT "fk_users_allowed_ages_at_creation"
FOREIGN KEY ("age_at_creation")
REFERENCES "allowed_ages_at_creation" ("age")
ON DELETE CASCADE
ON UPDATE RESTRICT
);
`;

beforeAll(async () => {
orm = await MikroORM.init({
dbName: schemaName,
discovery: { warnWhenNoEntities: false },
extensions: [EntityGenerator],
multipleStatements: true,
ensureDatabase: false,
});

if (await orm.schema.ensureDatabase({ create: true })) {
await orm.schema.execute(schema);
}

await orm.close(true);
});

beforeEach(async () => {
orm = await MikroORM.init({
dbName: schemaName,
discovery: { warnWhenNoEntities: false },
extensions: [EntityGenerator],
multipleStatements: true,
});
});

afterEach(async () => {
await orm.close(true);
});

describe(schemaName, () => {
describe.each([true, false])('entitySchema=%s', entitySchema => {
beforeEach(() => {
orm.config.get('entityGenerator').entitySchema = entitySchema;
});

test('generates from db', async () => {
const dump = await orm.entityGenerator.generate();
expect(dump).toMatchSnapshot('dump');
});

test('as functions from extensions', async () => {
orm.config.get('entityGenerator').onInitialMetadata = metadata => {
const usersMeta = metadata.find(meta => meta.className === 'Users')!;
Object.entries(usersMeta.properties).forEach(([propName, propOptions]) => {
if (typeof propOptions.generated === 'string') {
propOptions.generated = Utils.createFunction(new Map(), `return () => ${JSON.stringify(propOptions.generated)}`);
}
});
};
const dump = await orm.entityGenerator.generate();
expect(dump).toMatchSnapshot('dump');
});
});
});
Loading

0 comments on commit d2186da

Please sign in to comment.