Skip to content

Commit

Permalink
feat(core): allow using different PK than id (e.g. uuid)
Browse files Browse the repository at this point in the history
Now you can define PK e.g. like `uuid: string` and pre-compute it when
creating the entity before actual persisting to database (as opposed to
sequential `id`). Works for SQL drivers as mongo requires `_id` PK.

Also moves PK normalization to Platform
  • Loading branch information
B4nan committed Mar 18, 2019
1 parent 5d727e9 commit 40bcdc0
Show file tree
Hide file tree
Showing 36 changed files with 332 additions and 183 deletions.
7 changes: 4 additions & 3 deletions lib/EntityManager.ts
Original file line number Diff line number Diff line change
Expand Up @@ -154,13 +154,14 @@ export class EntityManager {

merge<T extends IEntityType<T>>(entityName: EntityName<T>, data: EntityData<T>): T {
entityName = Utils.className(entityName);
const meta = this.metadata[entityName];

if (!data || (!data.id && !data._id)) {
throw new Error('You cannot merge entity without id!');
if (!data || (!data[meta.primaryKey] && !data[meta.serializedPrimaryKey])) {
throw new Error('You cannot merge entity without identifier!');
}

const entity = Utils.isEntity<T>(data) ? data : this.getEntityFactory().create<T>(entityName, data, true);
EntityAssigner.assign(entity, data);
EntityAssigner.assign(entity, data, true);
this.getUnitOfWork().addToIdentityMap(entity);

return entity as T;
Expand Down
4 changes: 4 additions & 0 deletions lib/decorators/Entity.ts
Original file line number Diff line number Diff line change
Expand Up @@ -35,6 +35,9 @@ export interface IEntity<K = number | string> {
__em: EntityManager;
__initialized?: boolean;
__populated: boolean;
__primaryKey: K;
__primaryKeyField: string & keyof IEntity;
__serializedPrimaryKey: string & keyof IEntity;
}

export type IEntityType<T> = { [k in keyof T]: IEntity | Collection<IEntity> | any; } & IEntity;
Expand Down Expand Up @@ -78,6 +81,7 @@ export interface EntityMetadata<T extends IEntityType<T> = any> {
collection: string;
path: string;
primaryKey: keyof T & string;
serializedPrimaryKey: keyof T & string;
properties: { [K in keyof T & string]: EntityProperty };
customRepository: () => { new (em: EntityManager, entityName: EntityName<T>): EntityRepository<T> };
hooks: Record<string, string[]>;
Expand Down
27 changes: 19 additions & 8 deletions lib/drivers/AbstractSqlDriver.ts
Original file line number Diff line number Diff line change
Expand Up @@ -23,7 +23,8 @@ export abstract class AbstractSqlDriver<C extends Connection> extends DatabaseDr

async findOne<T extends IEntityType<T>>(entityName: string, where: FilterQuery<T> | string, populate: string[] = []): Promise<T | null> {
if (Utils.isPrimaryKey(where)) {
where = { id: where };
const pk = this.metadata[entityName].primaryKey;
where = { [pk]: where };
}

const qb = this.createQueryBuilder(entityName);
Expand All @@ -34,28 +35,33 @@ export abstract class AbstractSqlDriver<C extends Connection> extends DatabaseDr

async count(entityName: string, where: any): Promise<number> {
const qb = this.createQueryBuilder(entityName);
const res = await qb.count('id', true).where(where).execute('get');
const pk = this.metadata[entityName].primaryKey;
const res = await qb.count(pk, true).where(where).execute('get');

return +res.count;
}

async nativeInsert<T extends IEntityType<T>>(entityName: string, data: EntityData<T>): Promise<number> {
const collections = this.extractManyToMany(entityName, data);
const pk = this.metadata[entityName] ? this.metadata[entityName].primaryKey : this.config.getNamingStrategy().referenceColumnName();

if (Object.keys(data).length === 0) {
data.id = null;
data[pk] = null;
}

const qb = this.createQueryBuilder(entityName);
const res = await qb.insert(data).execute('run');
await this.processManyToMany(entityName, res.insertId, collections);
const id = res.insertId || data[pk];
await this.processManyToMany(entityName, id, collections);

return res.insertId;
return id;
}

async nativeUpdate<T extends IEntityType<T>>(entityName: string, where: FilterQuery<T>, data: EntityData<T>): Promise<number> {
const pk = this.metadata[entityName] ? this.metadata[entityName].primaryKey : this.config.getNamingStrategy().referenceColumnName();

if (Utils.isPrimaryKey(where)) {
where = { id: where };
where = { [pk]: where };
}

const collections = this.extractManyToMany(entityName, data);
Expand All @@ -66,14 +72,15 @@ export abstract class AbstractSqlDriver<C extends Connection> extends DatabaseDr
res = await qb.update(data).where(where).execute('run');
}

await this.processManyToMany(entityName, Utils.extractPK(data.id || where)!, collections);
await this.processManyToMany(entityName, Utils.extractPK(data[pk] || where, this.metadata[entityName])!, collections);

return res ? res.affectedRows : 0;
}

async nativeDelete<T extends IEntityType<T>>(entityName: string, where: FilterQuery<T> | string | any): Promise<number> {
if (Utils.isPrimaryKey(where)) {
where = { id: where };
const pk = this.metadata[entityName] ? this.metadata[entityName].primaryKey : this.config.getNamingStrategy().referenceColumnName();
where = { [pk]: where };
}

const qb = this.createQueryBuilder(entityName);
Expand Down Expand Up @@ -107,6 +114,10 @@ export abstract class AbstractSqlDriver<C extends Connection> extends DatabaseDr
}

protected async processManyToMany<T extends IEntityType<T>>(entityName: string, pk: IPrimaryKey, collections: EntityData<T>) {
if (!this.metadata[entityName]) {
return;
}

const props = this.metadata[entityName].properties;
const owners = Object.keys(collections).filter(k => props[k].owner);

Expand Down
10 changes: 1 addition & 9 deletions lib/drivers/DatabaseDriver.ts
Original file line number Diff line number Diff line change
Expand Up @@ -8,7 +8,7 @@ import { Platform } from '../platforms/Platform';

export abstract class DatabaseDriver<C extends Connection> implements IDatabaseDriver<C> {

protected readonly connection: Connection;
protected readonly connection: C;
protected readonly platform: Platform;
protected readonly metadata = MetadataStorage.getMetadata();
protected readonly logger = this.config.getLogger();
Expand Down Expand Up @@ -55,14 +55,6 @@ export abstract class DatabaseDriver<C extends Connection> implements IDatabaseD
return map;
}

normalizePrimaryKey<T = number | string>(data: IPrimaryKey): T {
return data as T;
}

denormalizePrimaryKey(data: number | string): IPrimaryKey {
return data;
}

mapResult<T extends IEntityType<T>>(result: T, meta: EntityMetadata): T {
if (!result || !meta) {
return result || null;
Expand Down
10 changes: 0 additions & 10 deletions lib/drivers/IDatabaseDriver.ts
Original file line number Diff line number Diff line change
Expand Up @@ -27,16 +27,6 @@ export interface IDatabaseDriver<C extends Connection = Connection> {

aggregate(entityName: string, pipeline: any[]): Promise<any[]>;

/**
* Normalizes primary key wrapper to scalar value (e.g. mongodb's ObjectID to string)
*/
normalizePrimaryKey<T = number | string>(data: IPrimaryKey): T;

/**
* Converts scalar primary key representation to native driver wrapper (e.g. string to mongodb's ObjectID)
*/
denormalizePrimaryKey(data: number | string): IPrimaryKey;

/**
* When driver uses pivot tables for M:N, this method will load identifiers for given collections from them
*/
Expand Down
23 changes: 18 additions & 5 deletions lib/entity/ArrayCollection.ts
Original file line number Diff line number Diff line change
Expand Up @@ -28,7 +28,15 @@ export class ArrayCollection<T extends IEntityType<T>> {
});
}

getIdentifiers(field = 'id'): IPrimaryKey[] {
getIdentifiers(field?: string): IPrimaryKey[] {
const items = this.getItems();

if (items.length === 0) {
return [];
}

field = field || this.items[0].__primaryKeyField;

return this.getItems().map(i => i[field as keyof T]);
}

Expand All @@ -51,10 +59,10 @@ export class ArrayCollection<T extends IEntityType<T>> {
remove(...items: T[]): void {
for (const item of items) {
this.handleInverseSide(item, 'remove');
const idx = this.items.findIndex(i => i.id === item.id);
const idx = this.items.findIndex(i => i.__serializedPrimaryKey === item.__serializedPrimaryKey);

if (idx !== -1) {
delete this[this.items.length]; // remove last item
delete this[this.items.length - 1]; // remove last item
this.items.splice(idx, 1);
Object.assign(this, this.items); // reassign array access
}
Expand All @@ -66,7 +74,12 @@ export class ArrayCollection<T extends IEntityType<T>> {
}

contains(item: T): boolean {
return !!this.items.find(i => i === item || !!(i.id && item.id && i.id === item.id));
return !!this.items.find(i => {
const objectIdentity = i === item;
const primaryKeyIdentity = i.__primaryKey && item.__primaryKey && i.__serializedPrimaryKey === item.__serializedPrimaryKey;

return !!(objectIdentity || primaryKeyIdentity);
});
}

count(): number {
Expand All @@ -83,7 +96,7 @@ export class ArrayCollection<T extends IEntityType<T>> {
}
}

protected handleInverseSide(item: T, method: string): void {
protected handleInverseSide(item: T, method: 'add' | 'remove'): void {
if (this.property.owner && this.property.inversedBy && item[this.property.inversedBy as keyof T].isInitialized()) {
item[this.property.inversedBy as keyof T][method](this.owner);
}
Expand Down
15 changes: 8 additions & 7 deletions lib/entity/Collection.ts
Original file line number Diff line number Diff line change
Expand Up @@ -77,14 +77,14 @@ export class Collection<T extends IEntityType<T>> extends ArrayCollection<T> {
const em = this.owner.__em;

if (!this.initialized && this.property.reference === ReferenceType.MANY_TO_MANY && em.getDriver().getPlatform().usesPivotTable()) {
const map = await em.getDriver().loadFromPivotTable<T>(this.property, [this.owner.id]);
this.set(map[this.owner.id].map(item => em.merge<T>(this.property.type, item)), true);
const map = await em.getDriver().loadFromPivotTable<T>(this.property, [this.owner.__primaryKey]);
this.set(map[this.owner.__primaryKey].map(item => em.merge<T>(this.property.type, item)), true);

return this;
}

// do not make db call if we know we will get no results
if (this.property.reference === ReferenceType.MANY_TO_MANY && (this.property.owner || em.getDriver().getPlatform().usesPivotTable()) && this.items.length === 0) {
if (this.property.reference === ReferenceType.MANY_TO_MANY && (this.property.owner || em.getDriver().getPlatform().usesPivotTable()) && this.length === 0) {
this.initialized = true;
this.dirty = false;
this.populated();
Expand Down Expand Up @@ -113,7 +113,7 @@ export class Collection<T extends IEntityType<T>> extends ArrayCollection<T> {
let orderBy = undefined;

if (this.property.reference === ReferenceType.ONE_TO_MANY) {
cond[this.property.fk as string] = this.owner.id;
cond[this.property.fk as string] = this.owner.__primaryKey;
orderBy = this.property.orderBy || { [this.property.referenceColumnName]: QueryOrder.ASC };
} else { // MANY_TO_MANY
this.createManyToManyCondition(cond);
Expand All @@ -124,9 +124,10 @@ export class Collection<T extends IEntityType<T>> extends ArrayCollection<T> {

private createManyToManyCondition(cond: Record<string, any>) {
if (this.property.owner || this.owner.__em.getDriver().getPlatform().usesPivotTable()) {
cond.id = { $in: this.items.map(item => item.id) };
const pk = this.items[0].__primaryKeyField; // we know there is at least one item as it was checked in init method
cond[pk] = { $in: this.items.map(item => item.__primaryKey) };
} else {
cond[this.property.mappedBy] = this.owner.id;
cond[this.property.mappedBy] = this.owner.__primaryKey;
}
}

Expand All @@ -138,7 +139,7 @@ export class Collection<T extends IEntityType<T>> extends ArrayCollection<T> {

private checkInitialized(): void {
if (!this.isInitialized()) {
throw new Error(`Collection ${this.property.type}[] of entity ${this.owner.constructor.name}[${this.owner.id}] not initialized`);
throw new Error(`Collection ${this.property.type}[] of entity ${this.owner.constructor.name}[${this.owner.__primaryKey}] not initialized`);
}
}

Expand Down
16 changes: 10 additions & 6 deletions lib/entity/EntityAssigner.ts
Original file line number Diff line number Diff line change
Expand Up @@ -8,12 +8,15 @@ import { ReferenceType } from './enums';

export class EntityAssigner {

static assign<T extends IEntityType<T>>(entity: T, data: EntityData<T>): void {
const metadata = MetadataStorage.getMetadata();
const meta = metadata[entity.constructor.name];
static assign<T extends IEntityType<T>>(entity: T, data: EntityData<T>, onlyProperties = false): void {
const meta = MetadataStorage.getMetadata(entity.constructor.name);
const props = meta.properties;

Object.keys(data).forEach(prop => {
if (onlyProperties && !props[prop]) {
return;
}

const value = data[prop as keyof EntityData<T>];

if (props[prop] && props[prop].reference === ReferenceType.MANY_TO_ONE && value) {
Expand All @@ -40,10 +43,11 @@ export class EntityAssigner {
return;
}

const id = Utils.extractPK(value);
const meta = MetadataStorage.getMetadata(entity.constructor.name);
const id = Utils.extractPK(value, meta);

if (id) {
const normalized = em.getDriver().normalizePrimaryKey(id);
const normalized = em.getDriver().getPlatform().normalizePrimaryKey(id);
entity[prop.name as keyof T] = em.getReference(prop.type, normalized);
return;
}
Expand All @@ -60,7 +64,7 @@ export class EntityAssigner {
}

if (Utils.isPrimaryKey(item)) {
const id = em.getDriver().normalizePrimaryKey(item);
const id = em.getDriver().getPlatform().normalizePrimaryKey(item);
return em.getReference(prop.type, id);
}

Expand Down
24 changes: 14 additions & 10 deletions lib/entity/EntityFactory.ts
Original file line number Diff line number Diff line change
Expand Up @@ -20,11 +20,14 @@ export class EntityFactory {
entityName = Utils.className(entityName);
data = Object.assign({}, data);
const meta = this.metadata[entityName];

// normalize PK to `id: string`
if (data.id || data._id) {
data.id = this.driver.normalizePrimaryKey(data.id || data._id);
delete data._id;
const platform = this.driver.getPlatform();
const pk = platform.getSerializedPrimaryKeyField(meta.primaryKey);

// denormalize PK to value required by driver
if (data[pk] || data[meta.primaryKey]) {
const id = platform.denormalizePrimaryKey(data[pk] || data[meta.primaryKey]);
delete data[pk];
data[meta.primaryKey] = id;
}

const entity = this.createEntity(data, meta);
Expand All @@ -42,32 +45,33 @@ export class EntityFactory {
private createEntity<T extends IEntityType<T>>(data: EntityData<T>, meta: EntityMetadata<T>): T {
const Entity = require(meta.path)[meta.name];

if (!data.id) {
if (!data[meta.primaryKey]) {
const params = this.extractConstructorParams<T>(meta, data);
meta.constructorParams.forEach(prop => delete data[prop]);
return new Entity(...params);
}

if (this.unitOfWork.getById(meta.name, data.id)) {
return this.unitOfWork.getById<T>(meta.name, data.id);
if (this.unitOfWork.getById(meta.name, data[meta.primaryKey])) {
return this.unitOfWork.getById<T>(meta.name, data[meta.primaryKey]);
}

// creates new entity instance, with possibility to bypass constructor call when instancing already persisted entity
const entity = Object.create(Entity.prototype);
entity.id = data.id as number | string;
entity[meta.primaryKey] = data[meta.primaryKey];
this.unitOfWork.addToIdentityMap(entity);

return entity;
}

createReference<T extends IEntityType<T>>(entityName: EntityName<T>, id: IPrimaryKey): T {
entityName = Utils.className(entityName);
const meta = this.metadata[entityName];

if (this.unitOfWork.getById(entityName, id)) {
return this.unitOfWork.getById<T>(entityName, id);
}

return this.create<T>(entityName, { id } as EntityData<T>, false);
return this.create<T>(entityName, { [meta.primaryKey]: id } as EntityData<T>, false);
}

/**
Expand Down
Loading

0 comments on commit 40bcdc0

Please sign in to comment.