Skip to content

Commit

Permalink
feat(embeddables): allow using m:1 properties inside embeddables (#1948)
Browse files Browse the repository at this point in the history
  • Loading branch information
B4nan committed Jun 24, 2021
1 parent e450ecd commit ffca73e
Show file tree
Hide file tree
Showing 21 changed files with 2,528 additions and 104 deletions.
4 changes: 3 additions & 1 deletion docs/docs/embeddables.md
Original file line number Diff line number Diff line change
Expand Up @@ -13,7 +13,9 @@ are the primary use case for this feature.
> Embeddables needs to be discovered just like regular entities, don't forget to
> add them to the list of entities when initializing the ORM.
Embeddables can only contain properties with basic `@Property()` mapping.
Embeddables can contain properties with basic `@Property()` mapping, nested
`@Embedded()` properties or arrays of `@Embedded()` properties. From version
5.0 we can also use `@ManyToOne()` properties.

For the purposes of this tutorial, we will assume that you have a `User` class in
your application and you would like to store an address in the `User` class. We will
Expand Down
6 changes: 4 additions & 2 deletions packages/core/src/entity/EntityHelper.ts
Original file line number Diff line number Diff line change
Expand Up @@ -15,6 +15,7 @@ export class EntityHelper {

static decorate<T extends AnyEntity<T>>(meta: EntityMetadata<T>, em: EntityManager): void {
if (meta.embeddable) {
EntityHelper.defineBaseProperties(meta, meta.prototype, em);
return;
}

Expand Down Expand Up @@ -53,16 +54,17 @@ export class EntityHelper {
}

private static defineBaseProperties<T extends AnyEntity<T>>(meta: EntityMetadata<T>, prototype: T, em: EntityManager) {
const helperParams = meta.embeddable ? [] : [em.getComparator().getPkGetter(meta), em.getComparator().getPkSerializer(meta), em.getComparator().getPkGetterConverted(meta)];
Object.defineProperties(prototype, {
__entity: { value: true },
__entity: { value: !meta.embeddable },
__meta: { value: meta },
__platform: { value: em.getPlatform() },
[entityHelperSymbol]: { value: null, writable: true, enumerable: false },
__helper: {
get(): WrappedEntity<T, keyof T> {
if (!this[entityHelperSymbol]) {
Object.defineProperty(this, entityHelperSymbol, {
value: new WrappedEntity(this, em.getComparator().getPkGetter(meta), em.getComparator().getPkSerializer(meta), em.getComparator().getPkGetterConverted(meta)),
value: new WrappedEntity(this, ...helperParams),
enumerable: false,
});
}
Expand Down
8 changes: 5 additions & 3 deletions packages/core/src/entity/EntityLoader.ts
Original file line number Diff line number Diff line change
Expand Up @@ -136,7 +136,7 @@ export class EntityLoader {
const meta = this.metadata.find<T>(entityName)!;
const prop = meta.properties[field as string];

if (prop.reference === ReferenceType.SCALAR && prop.lazy) {
if ((prop.reference === ReferenceType.SCALAR && prop.lazy) || prop.reference === ReferenceType.EMBEDDED) {
return [];
}

Expand Down Expand Up @@ -235,6 +235,7 @@ export class EntityLoader {
}

await this.populateMany<T>(entityName, entities, populate, options);
const prop = this.metadata.find(entityName)!.properties[populate.field];
const children: T[] = [];

for (const entity of entities) {
Expand All @@ -244,11 +245,12 @@ export class EntityLoader {
children.push(entity[populate.field].unwrap());
} else if (Utils.isCollection(entity[populate.field])) {
children.push(...entity[populate.field].getItems());
} else if (entity[populate.field] && prop.reference === ReferenceType.EMBEDDED) {
children.push(...Utils.asArray(entity[populate.field]));
}
}

const filtered = Utils.unique(children);
const prop = this.metadata.find(entityName)!.properties[populate.field];
const fields = this.buildFields(prop, options);
await this.populate<T>(prop.type, filtered, populate.children, {
where: await this.extractChildCondition(options, prop, false) as FilterQuery<T>,
Expand Down Expand Up @@ -362,7 +364,7 @@ export class EntityLoader {
children.push(...filtered.map(e => (e[prop.name] as unknown as Collection<T>).owner));
} else if (prop.reference === ReferenceType.MANY_TO_MANY && prop.owner) {
children.push(...filtered.reduce((a, b) => [...a, ...(b[prop.name] as unknown as Collection<AnyEntity>).getItems()], [] as AnyEntity[]));
} else if (prop.reference === ReferenceType.MANY_TO_MANY) { // inversed side
} else if (prop.reference === ReferenceType.MANY_TO_MANY) { // inverse side
children.push(...filtered);
} else { // MANY_TO_ONE or ONE_TO_ONE
children.push(...this.filterReferences(entities, prop.name, refresh));
Expand Down
12 changes: 6 additions & 6 deletions packages/core/src/entity/WrappedEntity.ts
Original file line number Diff line number Diff line change
Expand Up @@ -25,9 +25,9 @@ export class WrappedEntity<T extends AnyEntity<T>, PK extends keyof T> {
__identifier?: EntityIdentifier;

constructor(private readonly entity: T,
private readonly pkGetter: (e: T) => Primary<T>,
private readonly pkSerializer: (e: T) => string,
private readonly pkGetterConverted: (e: T) => Primary<T>) { }
private readonly pkGetter?: (e: T) => Primary<T>,
private readonly pkSerializer?: (e: T) => string,
private readonly pkGetterConverted?: (e: T) => Primary<T>) { }

isInitialized(): boolean {
return this.__initialized;
Expand Down Expand Up @@ -82,10 +82,10 @@ export class WrappedEntity<T extends AnyEntity<T>, PK extends keyof T> {

getPrimaryKey(convertCustomTypes = false): Primary<T> | null {
if (convertCustomTypes) {
return this.pkGetterConverted(this.entity);
return this.pkGetterConverted!(this.entity);
}

return this.pkGetter(this.entity);
return this.pkGetter!(this.entity);
}

getPrimaryKeys(convertCustomTypes = false): Primary<T>[] | null {
Expand Down Expand Up @@ -118,7 +118,7 @@ export class WrappedEntity<T extends AnyEntity<T>, PK extends keyof T> {
}

getSerializedPrimaryKey(): string {
return this.pkSerializer(this.entity);
return this.pkSerializer!(this.entity);
}

get __meta(): EntityMetadata<T> {
Expand Down
56 changes: 28 additions & 28 deletions packages/core/src/hydration/ObjectHydrator.ts
Original file line number Diff line number Diff line change
Expand Up @@ -37,7 +37,7 @@ export class ObjectHydrator extends Hydrator {
/**
* @internal Highly performance-sensitive method.
*/
private getEntityHydrator<T extends AnyEntity<T>>(meta: EntityMetadata<T>, type: 'full' | 'returning' | 'reference'): EntityHydrator<T> {
getEntityHydrator<T extends AnyEntity<T>>(meta: EntityMetadata<T>, type: 'full' | 'returning' | 'reference'): EntityHydrator<T> {
const exists = this.hydrators[type].get(meta.className);

if (exists) {
Expand Down Expand Up @@ -107,31 +107,31 @@ export class ObjectHydrator extends Hydrator {
};

/* istanbul ignore next */
const propName = (name: string) => 'data' + (name.includes(' ') ? `['${name}']` : `.${name}`);
const propName = (name: string, property = 'data') => property + (name.match(/[ -]/) ? `['${name}']` : `.${name}`);

const hydrateToOne = (prop: EntityProperty) => {
const hydrateToOne = (prop: EntityProperty, dataKey: string, entityKey: string) => {
const ret: string[] = [];

ret.push(` if (${propName(prop.name)} === null) {\n entity.${prop.name} = null;`);
ret.push(` } else if (typeof ${propName(prop.name)} !== 'undefined') {`);
ret.push(` if (isPrimaryKey(${propName(prop.name)}, true)) {`);
ret.push(` if (${propName(dataKey)} === null) {\n entity.${entityKey} = null;`);
ret.push(` } else if (typeof ${propName(dataKey)} !== 'undefined') {`);
ret.push(` if (isPrimaryKey(${propName(dataKey)}, true)) {`);

if (prop.mapToPk) {
ret.push(` entity.${prop.name} = ${propName(prop.name)};`);
ret.push(` entity.${entityKey} = ${propName(dataKey)};`);
} else if (prop.wrappedReference) {
ret.push(` entity.${prop.name} = new Reference(factory.createReference('${prop.type}', ${propName(prop.name)}, { merge: true, convertCustomTypes }));`);
ret.push(` entity.${entityKey} = new Reference(factory.createReference('${prop.type}', ${propName(dataKey)}, { merge: true, convertCustomTypes }));`);
} else {
ret.push(` entity.${prop.name} = factory.createReference('${prop.type}', ${propName(prop.name)}, { merge: true, convertCustomTypes });`);
ret.push(` entity.${entityKey} = factory.createReference('${prop.type}', ${propName(dataKey)}, { merge: true, convertCustomTypes });`);
}

ret.push(` } else if (${propName(prop.name)} && typeof ${propName(prop.name)} === 'object') {`);
ret.push(` } else if (${propName(dataKey)} && typeof ${propName(dataKey)} === 'object') {`);

if (prop.mapToPk) {
ret.push(` entity.${prop.name} = ${propName(prop.name)};`);
ret.push(` entity.${entityKey} = ${propName(dataKey)};`);
} else if (prop.wrappedReference) {
ret.push(` entity.${prop.name} = new Reference(factory.create('${prop.type}', ${propName(prop.name)}, { initialized: true, merge: true, newEntity, convertCustomTypes }));`);
ret.push(` entity.${entityKey} = new Reference(factory.create('${prop.type}', ${propName(dataKey)}, { initialized: true, merge: true, newEntity, convertCustomTypes }));`);
} else {
ret.push(` entity.${prop.name} = factory.create('${prop.type}', ${propName(prop.name)}, { initialized: true, merge: true, newEntity, convertCustomTypes });`);
ret.push(` entity.${entityKey} = factory.create('${prop.type}', ${propName(dataKey)}, { initialized: true, merge: true, newEntity, convertCustomTypes });`);
}

ret.push(` }`);
Expand All @@ -142,43 +142,43 @@ export class ObjectHydrator extends Hydrator {
const prop2 = meta2.properties[prop.inversedBy || prop.mappedBy];

if (prop2) {
ret.push(` if (entity.${prop.name} && !entity.${prop.name}.${prop2.name}) {`);
ret.push(` entity.${prop.name}.${prop.wrappedReference ? 'unwrap().' : ''}${prop2.name} = ${prop2.wrappedReference ? 'new Reference(entity)' : 'entity'};`);
ret.push(` if (entity.${entityKey} && !entity.${entityKey}.${prop2.name}) {`);
ret.push(` entity.${entityKey}.${prop.wrappedReference ? 'unwrap().' : ''}${prop2.name} = ${prop2.wrappedReference ? 'new Reference(entity)' : 'entity'};`);
ret.push(` }`);
}
}

if (prop.customType) {
context.set(`convertToDatabaseValue_${prop.name}`, (val: any) => prop.customType.convertToDatabaseValue(val, this.platform));

ret.push(` if (${propName(prop.name)} != null && convertCustomTypes) {`);
ret.push(` ${propName(prop.name)} = convertToDatabaseValue_${prop.name}(entity.${prop.name}.__helper.getPrimaryKey());`); // make sure the value is comparable
ret.push(` if (${propName(dataKey)} != null && convertCustomTypes) {`);
ret.push(` ${propName(dataKey)} = convertToDatabaseValue_${prop.name}(entity.${entityKey}.__helper.getPrimaryKey());`); // make sure the value is comparable
ret.push(` }`);
}

return ret;
};

const hydrateToMany = (prop: EntityProperty) => {
const hydrateToMany = (prop: EntityProperty, dataKey: string, entityKey: string) => {
const ret: string[] = [];

ret.push(...this.createCollectionItemMapper(prop));
ret.push(` if (${propName(prop.name)} && !Array.isArray(${propName(prop.name)}) && typeof ${propName(prop.name)} === 'object') {`);
ret.push(` ${propName(prop.name)} = [${propName(prop.name)}];`);
ret.push(` if (${propName(dataKey)} && !Array.isArray(${propName(dataKey)}) && typeof ${propName(dataKey)} === 'object') {`);
ret.push(` ${propName(dataKey)} = [${propName(dataKey)}];`);
ret.push(` }`);
ret.push(` if (Array.isArray(${propName(prop.name)})) {`);
ret.push(` const items = ${propName(prop.name)}.map(value => createCollectionItem_${prop.name}(value));`);
ret.push(` if (Array.isArray(${propName(dataKey)})) {`);
ret.push(` const items = ${propName(dataKey)}.map(value => createCollectionItem_${prop.name}(value));`);
ret.push(` const coll = Collection.create(entity, '${prop.name}', items, newEntity);`);
ret.push(` if (newEntity) {`);
ret.push(` coll.setDirty();`);
ret.push(` } else {`);
ret.push(` coll.takeSnapshot();`);
ret.push(` }`);
ret.push(` } else if (!entity.${prop.name} && ${propName(prop.name)} instanceof Collection) {`);
ret.push(` entity.${prop.name} = ${propName(prop.name)};`);
ret.push(` } else if (!entity.${prop.name}) {`);
ret.push(` } else if (!entity.${entityKey} && ${propName(dataKey)} instanceof Collection) {`);
ret.push(` entity.${entityKey} = ${propName(dataKey)};`);
ret.push(` } else if (!entity.${entityKey}) {`);
const items = this.platform.usesPivotTable() || !prop.owner ? 'undefined' : '[]';
ret.push(` const coll = Collection.create(entity, '${prop.name}', ${items}, !!${propName(prop.name)} || newEntity);`);
ret.push(` const coll = Collection.create(entity, '${prop.name}', ${items}, !!${propName(dataKey)} || newEntity);`);
ret.push(` coll.setDirty(false);`);
ret.push(` }`);

Expand Down Expand Up @@ -238,9 +238,9 @@ export class ObjectHydrator extends Hydrator {
const ret: string[] = [];

if (prop.reference === ReferenceType.MANY_TO_ONE || prop.reference === ReferenceType.ONE_TO_ONE) {
ret.push(...hydrateToOne(prop));
ret.push(...hydrateToOne(prop, dataKey, entityKey));
} else if (prop.reference === ReferenceType.ONE_TO_MANY || prop.reference === ReferenceType.MANY_TO_MANY) {
ret.push(...hydrateToMany(prop));
ret.push(...hydrateToMany(prop, dataKey, entityKey));
} else if (prop.reference === ReferenceType.EMBEDDED) {
if (prop.array) {
ret.push(...hydrateEmbeddedArray(prop, path, dataKey));
Expand Down
2 changes: 1 addition & 1 deletion packages/core/src/metadata/MetadataStorage.ts
Original file line number Diff line number Diff line change
Expand Up @@ -102,7 +102,7 @@ export class MetadataStorage {

decorate(em: EntityManager): void {
Object.values(this.metadata)
.filter(meta => meta.prototype && !Utils.isEntity(meta.prototype))
.filter(meta => meta.prototype && !meta.prototype.__meta)
.forEach(meta => EntityHelper.decorate(meta, em));
}

Expand Down
9 changes: 8 additions & 1 deletion packages/core/src/platforms/Platform.ts
Original file line number Diff line number Diff line change
Expand Up @@ -9,6 +9,9 @@ import {
TinyIntType, Type, UuidType, StringType, IntegerType, FloatType, DateTimeType, TextType, EnumType, UnknownType,
} from '../types';
import { Utils } from '../utils/Utils';
import clone from 'clone';

export const JsonProperty = Symbol('JsonProperty');

export abstract class Platform {

Expand Down Expand Up @@ -316,7 +319,11 @@ export abstract class Platform {
}

cloneEmbeddable<T>(data: T): T {
return JSON.parse(JSON.stringify(data));
const copy = clone(data);
// tag the copy so we know it should be stringified when quoting (so we know how to treat JSON arrays)
Object.defineProperty(copy, JsonProperty, { enumerable: false, value: true });

return copy;
}

setConfig(config: Configuration): void {
Expand Down
25 changes: 19 additions & 6 deletions packages/core/src/unit-of-work/ChangeSetComputer.ts
Original file line number Diff line number Diff line change
Expand Up @@ -60,23 +60,36 @@ export class ChangeSetComputer {
return data;
}

private processProperty<T extends AnyEntity<T>>(changeSet: ChangeSet<T>, prop: EntityProperty<T>): void {
const target = changeSet.entity[prop.name];
private processProperty<T extends AnyEntity<T>>(changeSet: ChangeSet<T>, prop: EntityProperty<T>, target?: unknown): void {
if (!target) {
const targets = Utils.unwrapProperty(changeSet.entity, changeSet.entity.__meta!, prop);
targets.forEach(([t]) => this.processProperty(changeSet, prop, t));
return;
}

if (Utils.isCollection(target)) { // m:n or 1:m
this.processToMany(prop, changeSet);
} else if (prop.reference !== ReferenceType.SCALAR && target) { // m:1 or 1:1
}

if ([ReferenceType.MANY_TO_ONE, ReferenceType.ONE_TO_ONE].includes(prop.reference)) {
this.processToOne(prop, changeSet);
}
}

private processToOne<T extends AnyEntity<T>>(prop: EntityProperty<T>, changeSet: ChangeSet<T>): void {
const entity = changeSet.entity[prop.name] as unknown as T;
const isToOneOwner = prop.reference === ReferenceType.MANY_TO_ONE || (prop.reference === ReferenceType.ONE_TO_ONE && prop.owner);

if (isToOneOwner && !entity.__helper!.hasPrimaryKey()) {
changeSet.payload[prop.name] = entity.__helper!.__identifier;
if (!isToOneOwner) {
return;
}

const targets = Utils.unwrapProperty(changeSet.entity, changeSet.entity.__meta!, prop) as [AnyEntity, number[]][];

targets.forEach(([target, idx]) => {
if (!target.__helper!.hasPrimaryKey()) {
Utils.setPayloadProperty<T>(changeSet.payload, this.metadata.find(changeSet.name)!, prop, target.__helper!.__identifier, idx);
}
});
}

private processToMany<T extends AnyEntity<T>>(prop: EntityProperty<T>, changeSet: ChangeSet<T>): void {
Expand Down
14 changes: 11 additions & 3 deletions packages/core/src/unit-of-work/ChangeSetPersister.ts
Original file line number Diff line number Diff line change
Expand Up @@ -243,12 +243,20 @@ export class ChangeSetPersister {
}

private processProperty<T extends AnyEntity<T>>(changeSet: ChangeSet<T>, prop: EntityProperty<T>): void {
const value = changeSet.payload[prop.name];
const meta = this.metadata.find(changeSet.name)!;
const values = Utils.unwrapProperty(changeSet.payload, meta, prop, true); // for object embeddables
const value = changeSet.payload[prop.name] as unknown; // for inline embeddables

if (value as unknown instanceof EntityIdentifier) {
changeSet.payload[prop.name] = value.getValue();
if (value instanceof EntityIdentifier) {
Utils.setPayloadProperty<T>(changeSet.payload, meta, prop, value.getValue());
}

values.forEach(([value, indexes]) => {
if (value instanceof EntityIdentifier) {
Utils.setPayloadProperty<T>(changeSet.payload, meta, prop, value.getValue(), indexes);
}
});

if (prop.onCreate && changeSet.type === ChangeSetType.CREATE) {
changeSet.entity[prop.name] = prop.onCreate(changeSet.entity);
changeSet.payload[prop.name] = prop.customType ? prop.customType.convertToDatabaseValue(changeSet.entity[prop.name], this.platform) : changeSet.entity[prop.name];
Expand Down
17 changes: 10 additions & 7 deletions packages/core/src/unit-of-work/UnitOfWork.ts
Original file line number Diff line number Diff line change
Expand Up @@ -341,7 +341,7 @@ export class UnitOfWork {
return [...this.collectionDeletions];
}

private findNewEntities<T extends AnyEntity<T>>(entity: T, visited = new WeakSet<AnyEntity>()): void {
private findNewEntities<T extends AnyEntity<T>>(entity: T, visited = new WeakSet<AnyEntity>(), idx = 0): void {
if (visited.has(entity)) {
return;
}
Expand All @@ -356,8 +356,11 @@ export class UnitOfWork {
this.initIdentifier(entity);

for (const prop of entity.__meta!.relations) {
const reference = Reference.unwrapReference(entity[prop.name]);
this.processReference(entity, prop, reference, visited);
const targets = Utils.unwrapProperty(entity, entity.__meta!, prop);
targets.forEach(([target, idx2]) => {
const reference = Reference.unwrapReference(target as AnyEntity);
this.processReference(entity, prop, reference, visited, idx);
});
}

const changeSet = this.changeSetComputer.computeChangeSet(entity);
Expand Down Expand Up @@ -406,21 +409,21 @@ export class UnitOfWork {
wrapped.__identifier = new EntityIdentifier();
}

private processReference<T extends AnyEntity<T>>(parent: T, prop: EntityProperty<T>, reference: any, visited: WeakSet<AnyEntity>): void {
private processReference<T extends AnyEntity<T>>(parent: T, prop: EntityProperty<T>, reference: any, visited: WeakSet<AnyEntity>, idx: number): void {
const isToOne = prop.reference === ReferenceType.MANY_TO_ONE || prop.reference === ReferenceType.ONE_TO_ONE;

if (isToOne && reference) {
return this.processToOneReference(reference, visited);
return this.processToOneReference(reference, visited, idx);
}

if (Utils.isCollection<any>(reference, prop, ReferenceType.MANY_TO_MANY) && reference.isDirty()) {
this.processToManyReference(reference, visited, parent, prop);
}
}

private processToOneReference<T extends AnyEntity<T>>(reference: any, visited: WeakSet<AnyEntity>): void {
private processToOneReference<T extends AnyEntity<T>>(reference: any, visited: WeakSet<AnyEntity>, idx: number): void {
if (!reference.__helper!.__managed) {
this.findNewEntities(reference, visited);
this.findNewEntities(reference, visited, idx);
}
}

Expand Down
Loading

0 comments on commit ffca73e

Please sign in to comment.