Skip to content

Commit

Permalink
feat(core): simplify entity definition and rework typings of FilterQuery
Browse files Browse the repository at this point in the history
Now it is no longer needed to merge entities with IEntity interface, that was polluting entity's interface with internal methods.
New interfaces IdentifiedEntity<T>, UuidEntity<T> and MongoEntity<T> and introduced, that should be implemented by entities. They
are not adding any new properties or methods, keeping the entity's interface clean.

This also introduces new strictly typed FilterQuery<T> implementation based on information provided by those new interfaces.

BREAKING CHANGES:
IEntity interface no longer has public methods like toJSON(), toObject() or init(). One can use wrap() method provided by ORM that
will enhance property type when needed with those methods (`await wrap(book.author).init()`).
FilterQuery now does not allow using smart query operators. You can either cast your condition as any or use object syntax instead
(instead of `{ 'age:gte': 18 }` use `{ age: { $gte: 18 } }`).

Closes #124, #171
  • Loading branch information
B4nan committed Oct 9, 2019
1 parent cff0dd4 commit 036bc40
Show file tree
Hide file tree
Showing 77 changed files with 1,102 additions and 715 deletions.
96 changes: 49 additions & 47 deletions lib/EntityManager.ts

Large diffs are not rendered by default.

21 changes: 6 additions & 15 deletions lib/connections/MongoConnection.ts
@@ -1,19 +1,10 @@
import {
Collection,
Db, DeleteWriteOpResultObject,
InsertOneWriteOpResult,
MongoClient,
MongoClientOptions,
ObjectId,
UpdateWriteOpResult,
} from 'mongodb';
import { Collection, Db, DeleteWriteOpResultObject, InsertOneWriteOpResult, MongoClient, MongoClientOptions, ObjectId, UpdateWriteOpResult, FilterQuery as MongoFilterQuery } from 'mongodb';
import { inspect } from 'util';

import { Connection, ConnectionConfig, QueryResult } from './Connection';
import { Utils } from '../utils';
import { QueryOrder, QueryOrderMap } from '../query';
import { FilterQuery, IEntity } from '..';
import { EntityName } from '../decorators';
import { FilterQuery, IEntity, EntityName } from '../types';

export class MongoConnection extends Connection {

Expand Down Expand Up @@ -74,7 +65,7 @@ export class MongoConnection extends Connection {
options.projection = fields.reduce((o, k) => ({ ...o, [k]: 1 }), {});
}

const resultSet = this.getCollection(collection).find(where, options);
const resultSet = this.getCollection(collection).find<T>(where, options);
let query = `db.getCollection('${collection}').find(${this.logObject(where)}, ${this.logObject(options)})`;

if (orderBy && Object.keys(orderBy).length > 0) {
Expand Down Expand Up @@ -149,12 +140,12 @@ export class MongoConnection extends Connection {
case 'updateMany':
const payload = Object.keys(data).some(k => k.startsWith('$')) ? data : { $set: data };
query = `db.getCollection('${collection}').updateMany(${this.logObject(where)}, ${this.logObject(payload)});`;
res = await this.getCollection(collection).updateMany(where, payload);
res = await this.getCollection(collection).updateMany(where as MongoFilterQuery<T>, payload);
break;
case 'deleteMany':
case 'countDocuments':
query = `db.getCollection('${collection}').${method}(${this.logObject(where)});`;
res = await this.getCollection(collection)[method as 'deleteMany'](where); // cast to deleteMany to fix some typing weirdness
res = await this.getCollection(collection)[method as 'deleteMany'](where as MongoFilterQuery<T>); // cast to deleteMany to fix some typing weirdness
break;
}

Expand Down Expand Up @@ -203,7 +194,7 @@ export class MongoConnection extends Connection {
return meta ? meta.collection : name;
}

private logObject(o: object): string {
private logObject(o: any): string {
return inspect(o, { depth: 5, compact: true, breakLength: 300 });
}

Expand Down
101 changes: 6 additions & 95 deletions lib/decorators/Entity.ts
@@ -1,13 +1,11 @@
import { MetadataStorage } from '../metadata';
import { EntityManager } from '../EntityManager';
import { IPrimaryKey } from './PrimaryKey';
import { AssignOptions, Cascade, Collection, EntityRepository, ReferenceType } from '../entity';
import { EntityRepository } from '../entity';
import { Utils } from '../utils';
import { QueryOrder } from '../query';
import { LockMode } from '../unit-of-work';
import { EntityName, IEntity } from '../types';

export function Entity(options: EntityOptions = {}): Function {
return function <T extends { new(...args: any[]): IEntity }>(target: T) {
export function Entity(options: EntityOptions<any> = {}): Function {
return function <T extends { new(...args: any[]): IEntity<T> }>(target: T) {
const meta = MetadataStorage.getMetadata(target.name);
Utils.merge(meta, options);
meta.name = target.name;
Expand All @@ -19,94 +17,7 @@ export function Entity(options: EntityOptions = {}): Function {
};
}

export type EntityOptions = {
export type EntityOptions<T extends IEntity<T>> = {
collection?: string;
customRepository?: () => { new (em: EntityManager, entityName: EntityName<IEntity>): EntityRepository<IEntity> };
customRepository?: () => { new (em: EntityManager, entityName: EntityName<T>): EntityRepository<T> };
};

export interface IEntity<K = number | string> {
id: K;
isInitialized(): boolean;
populated(populated?: boolean): void;
init(populated?: boolean, lockMode?: LockMode): Promise<this>;
toObject(ignoreFields?: string[]): Record<string, any>;
toJSON(...args: any[]): Record<string, any>;
assign(data: any, options?: AssignOptions | boolean): this;
__uuid: string;
__meta: EntityMetadata;
__em: EntityManager;
__initialized?: boolean;
__populated: boolean;
__lazyInitialized: boolean;
__primaryKey: K;
__primaryKeyField: string & keyof IEntity;
__serializedPrimaryKey: string & keyof IEntity;
__serializedPrimaryKeyField: string;
}

export type IEntityType<T> = { [k in keyof T]: IEntity | Collection<IEntity> | any; } & IEntity;

export type EntityClass<T extends IEntityType<T>> = Function & { prototype: T };

export type EntityClassGroup<T extends IEntityType<T>> = {
entity: EntityClass<T>;
schema: EntityMetadata<T>;
};

export type EntityName<T extends IEntityType<T>> = string | EntityClass<T>;

export type EntityData<T extends IEntityType<T>> = { [P in keyof T]?: T[P] | IPrimaryKey; } & Record<string, any>;

export interface EntityProperty<T extends IEntityType<T> = any> {
name: string & keyof T;
entity: () => EntityName<T>;
type: string;
columnType: string;
primary: boolean;
length?: any;
reference: ReferenceType;
wrappedReference?: boolean;
fieldName: string;
default?: any;
unique?: boolean;
nullable?: boolean;
unsigned: boolean;
persist?: boolean;
hidden?: boolean;
version?: boolean;
eager?: boolean;
setter?: boolean;
getter?: boolean;
getterName?: keyof T;
cascade: Cascade[];
orphanRemoval?: boolean;
onUpdate?: () => any;
owner: boolean;
inversedBy: string;
mappedBy: string;
orderBy?: { [field: string]: QueryOrder };
pivotTable: string;
joinColumn: string;
inverseJoinColumn: string;
referenceColumnName: string;
referencedTableName: string;
}

export type HookType = 'onInit' | 'beforeCreate' | 'afterCreate' | 'beforeUpdate' | 'afterUpdate' | 'beforeDelete' | 'afterDelete';

export interface EntityMetadata<T extends IEntityType<T> = any> {
name: string;
className: string;
constructorParams: (keyof T & string)[];
toJsonParams: string[];
extends: string;
collection: string;
path: string;
primaryKey: keyof T & string;
versionProperty: keyof T & string;
serializedPrimaryKey: keyof T & string;
properties: { [K in keyof T & string]: EntityProperty<T> };
customRepository: () => { new (em: EntityManager, entityName: EntityName<T>): EntityRepository<T> };
hooks: Partial<Record<HookType, (string & keyof T)[]>>;
prototype: T;
}
2 changes: 1 addition & 1 deletion lib/decorators/ManyToMany.ts
@@ -1,8 +1,8 @@
import { ReferenceOptions } from './Property';
import { EntityName, EntityProperty, IEntity, IEntityType } from './Entity';
import { MetadataStorage } from '../metadata';
import { Utils } from '../utils';
import { Cascade, ReferenceType } from '../entity';
import { EntityName, EntityProperty, IEntity, IEntityType } from '../types';

export function ManyToMany<T extends IEntityType<T>>(
entity: ManyToManyOptions<T> | string | (() => EntityName<T>),
Expand Down
2 changes: 1 addition & 1 deletion lib/decorators/ManyToOne.ts
@@ -1,8 +1,8 @@
import { ReferenceOptions } from './Property';
import { EntityName, EntityProperty, IEntity, IEntityType } from './Entity';
import { MetadataStorage } from '../metadata';
import { Utils } from '../utils';
import { Cascade, ReferenceType } from '../entity';
import { EntityName, EntityProperty, IEntity, IEntityType } from '../types';

export function ManyToOne<T extends IEntityType<T>>(
entity: ManyToOneOptions<T> | string | ((e?: any) => EntityName<T>) = {},
Expand Down
2 changes: 1 addition & 1 deletion lib/decorators/OneToMany.ts
@@ -1,9 +1,9 @@
import { ReferenceOptions } from './Property';
import { EntityName, EntityProperty, IEntity, IEntityType } from './Entity';
import { MetadataStorage } from '../metadata';
import { Utils } from '../utils';
import { Cascade, ReferenceType } from '../entity';
import { QueryOrder } from '../query';
import { EntityName, EntityProperty, IEntity, IEntityType } from '../types';

export function OneToMany<T extends IEntityType<T>>(
entity: OneToManyOptions<T> | string | ((e?: any) => EntityName<T>),
Expand Down
3 changes: 2 additions & 1 deletion lib/decorators/OneToOne.ts
@@ -1,6 +1,7 @@
import { EntityName, IEntityType } from './Entity';

import { ReferenceType } from '../entity';
import { createOneToDecorator, OneToManyOptions } from './OneToMany';
import { EntityName, IEntityType } from '../types';

export function OneToOne<T extends IEntityType<T>>(
entity?: OneToOneOptions<T> | string | ((e?: any) => EntityName<T>),
Expand Down
4 changes: 2 additions & 2 deletions lib/decorators/PrimaryKey.ts
Expand Up @@ -14,8 +14,8 @@ export function PrimaryKey(options: PrimaryKeyOptions = {}): Function {
}

export interface PrimaryKeyOptions extends PropertyOptions {
name?: string;
type?: any;
}

export type IPrimaryKey = number | string | { toString?(): string; toHexString?(): string };
export type IPrimaryKeyValue = number | string | { toHexString(): string };
export type IPrimaryKey<T extends IPrimaryKeyValue = IPrimaryKeyValue> = T;
3 changes: 2 additions & 1 deletion lib/decorators/Property.ts
@@ -1,7 +1,8 @@
import { EntityName, EntityProperty, IEntity, IEntityType } from './Entity';

import { MetadataStorage } from '../metadata';
import { Utils } from '../utils';
import { Cascade, ReferenceType } from '../entity';
import { EntityName, EntityProperty, IEntity, IEntityType } from '../types';

export function Property(options: PropertyOptions = {}): Function {
return function (target: IEntity, propertyName: string) {
Expand Down
18 changes: 18 additions & 0 deletions lib/decorators/SerializedPrimaryKey.ts
@@ -0,0 +1,18 @@
import { MetadataStorage } from '../metadata';
import { ReferenceType } from '../entity';
import { EntityProperty, IEntity, PropertyOptions } from '.';
import { Utils } from '../utils';

export function SerializedPrimaryKey(options: SerializedPrimaryKeyOptions = {}): Function {
return function (target: IEntity, propertyName: string) {
const meta = MetadataStorage.getMetadata(target.constructor.name);
options.name = propertyName;
meta.serializedPrimaryKey = propertyName;
meta.properties[propertyName] = Object.assign({ reference: ReferenceType.SCALAR }, options) as EntityProperty;
Utils.lookupPathFromDecorator(meta);
};
}

export interface SerializedPrimaryKeyOptions extends PropertyOptions {
type?: any;
}
2 changes: 1 addition & 1 deletion lib/decorators/hooks.ts
@@ -1,5 +1,5 @@
import { MetadataStorage } from '../metadata';
import { HookType } from './Entity';
import { HookType } from '../types';

export function BeforeCreate() {
return hook('beforeCreate');
Expand Down
2 changes: 2 additions & 0 deletions lib/decorators/index.ts
@@ -1,8 +1,10 @@
export * from './PrimaryKey';
export * from './SerializedPrimaryKey';
export * from './Entity';
export * from './OneToOne';
export * from './ManyToOne';
export * from './ManyToMany';
export { OneToMany, OneToManyOptions } from './OneToMany';
export * from './Property';
export * from './hooks';
export * from '../types';
8 changes: 4 additions & 4 deletions lib/drivers/AbstractSqlDriver.ts
Expand Up @@ -4,7 +4,7 @@ import { DatabaseDriver } from './DatabaseDriver';
import { QueryResult } from '../connections';
import { AbstractSqlConnection } from '../connections/AbstractSqlConnection';
import { ReferenceType } from '../entity';
import { FilterQuery } from './IDatabaseDriver';
import { FilterQuery } from '../types';
import { QueryBuilder, QueryOrderMap } from '../query';
import { Configuration, Utils } from '../utils';
import { LockMode } from '../unit-of-work';
Expand Down Expand Up @@ -41,13 +41,13 @@ export abstract class AbstractSqlDriver<C extends AbstractSqlConnection = Abstra
return qb.execute('all');
}

async findOne<T extends IEntityType<T>>(entityName: string, where: FilterQuery<T> | string, populate: string[] = [], orderBy: QueryOrderMap = {}, fields?: string[], lockMode?: LockMode, ctx?: Transaction): Promise<T | null> {
async findOne<T extends IEntityType<T>>(entityName: string, where: FilterQuery<T>, populate: string[] = [], orderBy: QueryOrderMap = {}, fields?: string[], lockMode?: LockMode, ctx?: Transaction): Promise<T | null> {
const meta = this.metadata.get(entityName);
populate = this.populateMissingReferences(meta, populate);
const pk = meta.primaryKey;

if (Utils.isPrimaryKey(where)) {
where = { [pk]: where };
where = { [pk]: where } as FilterQuery<T>;
}

if (fields && !fields.includes(pk)) {
Expand Down Expand Up @@ -88,7 +88,7 @@ export abstract class AbstractSqlDriver<C extends AbstractSqlConnection = Abstra
const pk = this.getPrimaryKeyField(entityName);

if (Utils.isPrimaryKey(where)) {
where = { [pk]: where };
where = { [pk]: where } as FilterQuery<T>;
}

const collections = this.extractManyToMany(entityName, data);
Expand Down
20 changes: 10 additions & 10 deletions lib/drivers/DatabaseDriver.ts
@@ -1,5 +1,5 @@
import { FilterQuery, IDatabaseDriver } from './IDatabaseDriver';
import { EntityData, EntityMetadata, EntityProperty, IEntity, IEntityType, IPrimaryKey } from '../decorators';
import { IDatabaseDriver } from './IDatabaseDriver';
import { EntityData, EntityMetadata, EntityProperty, FilterQuery, IEntity, IPrimaryKey } from '../decorators';
import { MetadataStorage } from '../metadata';
import { Connection, QueryResult, Transaction } from '../connections';
import { Configuration, ConnectionOptions, Utils } from '../utils';
Expand All @@ -18,23 +18,23 @@ export abstract class DatabaseDriver<C extends Connection> implements IDatabaseD
protected constructor(protected readonly config: Configuration,
protected readonly dependencies: string[]) { }

abstract async find<T extends IEntity>(entityName: string, where: FilterQuery<T>, populate?: string[], orderBy?: QueryOrderMap, fields?: string[], limit?: number, offset?: number, ctx?: Transaction): Promise<T[]>;
abstract async find<T extends IEntity<T>>(entityName: string, where: FilterQuery<T>, populate?: string[], orderBy?: QueryOrderMap, fields?: string[], limit?: number, offset?: number, ctx?: Transaction): Promise<T[]>;

abstract async findOne<T extends IEntity>(entityName: string, where: FilterQuery<T> | string, populate: string[], orderBy?: QueryOrderMap, fields?: string[], lockMode?: LockMode, ctx?: Transaction): Promise<T | null>;
abstract async findOne<T extends IEntity<T>>(entityName: string, where: FilterQuery<T>, populate: string[], orderBy?: QueryOrderMap, fields?: string[], lockMode?: LockMode, ctx?: Transaction): Promise<T | null>;

abstract async nativeInsert<T extends IEntityType<T>>(entityName: string, data: EntityData<T>, ctx?: Transaction): Promise<QueryResult>;
abstract async nativeInsert<T extends IEntity<T>>(entityName: string, data: EntityData<T>, ctx?: Transaction): Promise<QueryResult>;

abstract async nativeUpdate<T extends IEntity>(entityName: string, where: FilterQuery<IEntity> | IPrimaryKey, data: EntityData<T>, ctx?: Transaction): Promise<QueryResult>;
abstract async nativeUpdate<T extends IEntity<T>>(entityName: string, where: FilterQuery<T>, data: EntityData<T>, ctx?: Transaction): Promise<QueryResult>;

abstract async nativeDelete<T extends IEntity>(entityName: string, where: FilterQuery<IEntity> | IPrimaryKey, ctx?: Transaction): Promise<QueryResult>;
abstract async nativeDelete<T extends IEntity<T>>(entityName: string, where: FilterQuery<T>, ctx?: Transaction): Promise<QueryResult>;

abstract async count<T extends IEntity>(entityName: string, where: FilterQuery<T>, ctx?: Transaction): Promise<number>;
abstract async count<T extends IEntity<T>>(entityName: string, where: FilterQuery<T>, ctx?: Transaction): Promise<number>;

async aggregate(entityName: string, pipeline: any[]): Promise<any[]> {
throw new Error(`Aggregations are not supported by ${this.constructor.name} driver`);
}

async loadFromPivotTable<T extends IEntity>(prop: EntityProperty, owners: IPrimaryKey[], ctx?: Transaction): Promise<Record<string, T[]>> {
async loadFromPivotTable<T extends IEntity<T>>(prop: EntityProperty, owners: IPrimaryKey[], ctx?: Transaction): Promise<Record<string, T[]>> {
if (!this.platform.usesPivotTable()) {
throw new Error(`${this.constructor.name} does not use pivot tables`);
}
Expand All @@ -56,7 +56,7 @@ export abstract class DatabaseDriver<C extends Connection> implements IDatabaseD
return map;
}

mapResult<T extends IEntityType<T>>(result: EntityData<T>, meta: EntityMetadata): T | null {
mapResult<T extends IEntity<T>>(result: EntityData<T>, meta: EntityMetadata): T | null {
if (!result || !meta) {
return null;
}
Expand Down

0 comments on commit 036bc40

Please sign in to comment.