Skip to content

Commit

Permalink
feat: add connection option entitySkipConstructor
Browse files Browse the repository at this point in the history
this adds the option for bypassing the constructor when deserializing
the Entities.  note that this may cause problems with class-based
JavaScript features that rely on the constructor to operate - such as private
properties or default values
  • Loading branch information
imnotjames committed Jun 28, 2021
1 parent 9bbdb01 commit f43d561
Show file tree
Hide file tree
Showing 8 changed files with 149 additions and 13 deletions.
5 changes: 5 additions & 0 deletions src/connection/BaseConnectionOptions.ts
Original file line number Diff line number Diff line change
Expand Up @@ -103,6 +103,11 @@ export interface BaseConnectionOptions {
*/
readonly entityPrefix?: string;

/**
* When creating new Entity instances, skip all constructors when true.
*/
readonly entitySkipConstructor?: boolean;

/**
* Extra connection options to be passed to the underlying driver.
*
Expand Down
19 changes: 17 additions & 2 deletions src/metadata/EmbeddedMetadata.ts
Original file line number Diff line number Diff line change
Expand Up @@ -86,6 +86,12 @@ export class EmbeddedMetadata {
*/
embeddeds: EmbeddedMetadata[] = [];

/**
* Indicates if the entity should be instantiated using the constructor
* or via allocating a new object via `Object.create()`.
*/
isAlwaysUsingConstructor: boolean = true;

/**
* Indicates if this embedded is in array mode.
*
Expand Down Expand Up @@ -189,8 +195,12 @@ export class EmbeddedMetadata {
/**
* Creates a new embedded object.
*/
create(): any {
return new (this.type as any);
create(options?: { fromDeserializer?: boolean }): any {
if (!options?.fromDeserializer || this.isAlwaysUsingConstructor) {
return new (this.type as any);
} else {
return Object.create(this.type.prototype);
}
}

// ---------------------------------------------------------------------
Expand All @@ -211,6 +221,11 @@ export class EmbeddedMetadata {
this.uniquesFromTree = this.buildUniquesFromTree();
this.relationIdsFromTree = this.buildRelationIdsFromTree();
this.relationCountsFromTree = this.buildRelationCountsFromTree();

if (connection.options.entitySkipConstructor) {
this.isAlwaysUsingConstructor = !connection.options.entitySkipConstructor;
}

return this;
}

Expand Down
21 changes: 19 additions & 2 deletions src/metadata/EntityMetadata.ts
Original file line number Diff line number Diff line change
Expand Up @@ -187,6 +187,12 @@ export class EntityMetadata {
*/
isJunction: boolean = false;

/**
* Indicates if the entity should be instantiated using the constructor
* or via allocating a new object via `Object.create()`.
*/
isAlwaysUsingConstructor: boolean = true;

/**
* Indicates if this entity is a tree, what type of tree it is.
*/
Expand Down Expand Up @@ -525,11 +531,16 @@ export class EntityMetadata {
/**
* Creates a new entity.
*/
create(queryRunner?: QueryRunner): any {
create(queryRunner?: QueryRunner, options?: { fromDeserializer?: boolean }): any {
// if target is set to a function (e.g. class) that can be created then create it
let ret: any;
if (this.target instanceof Function) {
ret = new (<any> this.target)();
if (!options?.fromDeserializer || this.isAlwaysUsingConstructor) {
ret = new (<any> this.target)();
} else {
ret = Object.create(this.target.prototype);
}

this.lazyRelations.forEach(relation => this.connection.relationLoader.enableLazyLoad(relation, ret, queryRunner));
return ret;
}
Expand Down Expand Up @@ -781,6 +792,8 @@ export class EntityMetadata {
build() {
const namingStrategy = this.connection.namingStrategy;
const entityPrefix = this.connection.options.entityPrefix;
const entitySkipConstructor = this.connection.options.entitySkipConstructor;

this.engine = this.tableMetadataArgs.engine;
this.database = this.tableMetadataArgs.type === "entity-child" && this.parentEntityMetadata ? this.parentEntityMetadata.database : this.tableMetadataArgs.database;
if (this.tableMetadataArgs.schema) {
Expand Down Expand Up @@ -815,6 +828,10 @@ export class EntityMetadata {
this.schemaPath = this.buildSchemaPath();
this.orderBy = (this.tableMetadataArgs.orderBy instanceof Function) ? this.tableMetadataArgs.orderBy(this.propertiesMap) : this.tableMetadataArgs.orderBy; // todo: is propertiesMap available here? Looks like its not

if (entitySkipConstructor !== undefined) {
this.isAlwaysUsingConstructor = !entitySkipConstructor;
}

this.isJunction = this.tableMetadataArgs.type === "closure-junction" || this.tableMetadataArgs.type === "junction";
this.isClosureJunction = this.tableMetadataArgs.type === "closure-junction";
}
Expand Down
12 changes: 6 additions & 6 deletions src/query-builder/transformer/DocumentToEntityTransformer.ts
Original file line number Diff line number Diff line change
Expand Up @@ -28,7 +28,7 @@ export class DocumentToEntityTransformer {
}

transform(document: any, metadata: EntityMetadata) {
const entity: any = metadata.create();
const entity: any = metadata.create(undefined, { fromDeserializer: true });
let hasData = false;

// handle _id property the special way
Expand Down Expand Up @@ -87,7 +87,7 @@ export class DocumentToEntityTransformer {

if (embedded.isArray) {
entity[embedded.propertyName] = (document[embedded.prefix] as any[]).map((subValue: any, index: number) => {
const newItem = embedded.create();
const newItem = embedded.create({ fromDeserializer: true });
embedded.columns.forEach(column => {
newItem[column.propertyName] = subValue[column.databaseNameWithoutPrefixes];
});
Expand All @@ -96,15 +96,15 @@ export class DocumentToEntityTransformer {
});

} else {
if (embedded.embeddeds.length && !entity[embedded.propertyName])
entity[embedded.propertyName] = embedded.create();
if (embedded.embeddeds.length && !entity[embedded.propertyName])
entity[embedded.propertyName] = embedded.create({ fromDeserializer: true });

embedded.columns.forEach(column => {
const value = document[embedded.prefix][column.databaseNameWithoutPrefixes];
if (value === undefined) return;

if (!entity[embedded.propertyName])
entity[embedded.propertyName] = embedded.create();
entity[embedded.propertyName] = embedded.create({ fromDeserializer: true });

entity[embedded.propertyName][column.propertyName] = value;
});
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -66,7 +66,7 @@ export class PlainObjectToNewEntityTransformer {

// if such item already exist then merge new data into it, if its not we create a new entity and merge it into the array
if (!objectRelatedValueEntity) {
objectRelatedValueEntity = relation.inverseEntityMetadata.create();
objectRelatedValueEntity = relation.inverseEntityMetadata.create(undefined, { fromDeserializer: true });
entityRelatedValue.push(objectRelatedValueEntity);
}

Expand All @@ -86,7 +86,7 @@ export class PlainObjectToNewEntityTransformer {
}

if (!entityRelatedValue) {
entityRelatedValue = relation.inverseEntityMetadata.create();
entityRelatedValue = relation.inverseEntityMetadata.create(undefined, { fromDeserializer: true });
relation.setEntityValue(entity, entityRelatedValue);
}

Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -102,7 +102,7 @@ export class RawSqlResultsToEntityTransformer {
if (discriminatorMetadata)
metadata = discriminatorMetadata;
}
let entity: any = this.expressionMap.options.indexOf("create-pojo") !== -1 ? {} : metadata.create(this.queryRunner);
let entity: any = this.expressionMap.options.indexOf("create-pojo") !== -1 ? {} : metadata.create(this.queryRunner, { fromDeserializer: true });

// get value from columns selections and put them into newly created entity
const hasColumns = this.transformColumns(rawResults, alias, entity, metadata);
Expand Down
81 changes: 81 additions & 0 deletions test/functional/entity-metadata/entity-metadata-create.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,81 @@
import "reflect-metadata";
import { Connection } from "../../../src/connection/Connection";
import { expect } from "chai";
import { closeTestingConnections, createTestingConnections, reloadTestingDatabases } from "../../utils/test-utils";
import { TestCreate } from "./entity/TestCreate";

describe("entity-metadata > create", () => {
describe("without entitySkipConstructor", () => {
let connections: Connection[];
before(async () => connections = await createTestingConnections({
enabledDrivers: [ "sqlite" ],
entities: [
TestCreate
]
})
);

beforeEach(() => reloadTestingDatabases(connections));
after(() => closeTestingConnections(connections));

it("should call the constructor when creating an object", () => Promise.all(connections.map(async connection => {
const entity = connection.manager.create(TestCreate);

expect(entity.hasCalledConstructor).to.be.true;
})))

it("should set the default property values", () => Promise.all(connections.map(async connection => {
const entity = connection.manager.create(TestCreate);

expect(entity.foo).to.be.equal("bar");
})))

it("should call the constructor when retrieving an object", () => Promise.all(connections.map(async connection => {
const repo = connection.manager.getRepository(TestCreate);

const { id } = await repo.save({ foo: "baz" });

const entity = await repo.findOneOrFail(id);

expect(entity.hasCalledConstructor).to.be.true;
})))
})

describe("with entitySkipConstructor", () => {
let connections: Connection[];
before(async () => connections = await createTestingConnections({
enabledDrivers: [ "sqlite" ],
entities: [
TestCreate
],
driverSpecific: {
entitySkipConstructor: true,
}
}));

beforeEach(() => reloadTestingDatabases(connections));
after(() => closeTestingConnections(connections));

it("should call the constructor when creating an object", () => Promise.all(connections.map(async connection => {
const entity = connection.manager.create(TestCreate);

expect(entity.hasCalledConstructor).to.be.true;
})))

it("should set the default property values when creating an object", () => Promise.all(connections.map(async connection => {
const entity = connection.manager.create(TestCreate);

expect(entity.foo).to.be.equal("bar");
})))

it("should not call the constructor when retrieving an object", () => Promise.all(connections.map(async connection => {
const repo = connection.manager.getRepository(TestCreate);

const { id } = await repo.save({ foo: "baz" });

const entity = await repo.findOneOrFail(id);

expect(entity.hasCalledConstructor).not.to.be.true;
})))
})
})
18 changes: 18 additions & 0 deletions test/functional/entity-metadata/entity/TestCreate.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,18 @@
import { Column } from "../../../../src/decorator/columns/Column";
import { Entity } from "../../../../src/decorator/entity/Entity";
import { PrimaryGeneratedColumn } from "../../../../src";

@Entity()
export class TestCreate {
constructor() {
this.hasCalledConstructor = true;
}

hasCalledConstructor = false;

@PrimaryGeneratedColumn()
id: number;

@Column()
foo: string = 'bar';
}

0 comments on commit f43d561

Please sign in to comment.