Skip to content

Commit

Permalink
fix(Entity): receive Props instead of id
Browse files Browse the repository at this point in the history
BREAKING CHANGE: This change require props object always on Entity

#4
  • Loading branch information
azu committed May 6, 2018
1 parent b10f387 commit ed135c8
Show file tree
Hide file tree
Showing 13 changed files with 270 additions and 132 deletions.
12 changes: 6 additions & 6 deletions package.json
Original file line number Diff line number Diff line change
Expand Up @@ -47,14 +47,14 @@
"devDependencies": {
"@types/mocha": "^2.2.48",
"@types/node": "^9.4.6",
"cross-env": "^5.1.3",
"cross-env": "^5.1.4",
"husky": "^0.14.3",
"lerna": "^2.9.0",
"lint-staged": "^7.0.0",
"mocha": "^5.0.4",
"prettier": "1.11.1",
"ts-node": "^5.0.1",
"typescript": "^2.7.2"
"lint-staged": "^7.0.5",
"mocha": "^5.1.1",
"prettier": "1.12.1",
"ts-node": "^6.0.2",
"typescript": "^2.8.3"
},
"dependencies": {
"map-like": "^2.0.0",
Expand Down
22 changes: 15 additions & 7 deletions src/Entity.ts
Original file line number Diff line number Diff line change
@@ -1,23 +1,31 @@
// MIT © 2017 azu
import { Identifier } from "./Identifier";
import { EntityLike, EntityLikeProps } from "./EntityLike";

export abstract class Entity<Id extends Identifier<any>> {
public readonly id: Id;
export const isEntity = (v: any): v is Entity<any> => {
return v instanceof Entity;
};

constructor(id: Id) {
this.id = id;
export class Entity<Props extends EntityLikeProps<Identifier<any>>> implements EntityLike<Props> {
props: Props;

constructor(props: Props) {
this.props = props;
}

/**
/*
* Check equality by identifier
*/
equals(object?: Entity<Id>): boolean {
equals(object?: Entity<Props>): boolean {
if (object == null || object == undefined) {
return false;
}
if (this === object) {
return true;
}
return this.id.equals(object.id);
if (!isEntity(object)) {
return false;
}
return this.props.id.equals(object.props.id);
}
}
19 changes: 19 additions & 0 deletions src/EntityLike.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,19 @@
import { ImmutableEntity } from "./immutable/ImmutableEntity";
import { Identifier } from "./Identifier";

export const isEntityLike = (entity: any): entity is EntityLike<any> => {
if (!entity) {
return false;
}
return entity instanceof ImmutableEntity;
};

export interface EntityLikeProps<Id extends Identifier<any>> {
id: Id;
}

export interface EntityLike<Props extends EntityLikeProps<Identifier<any>>> {
props: Props;

equals(object?: EntityLike<Props>): boolean;
}
41 changes: 41 additions & 0 deletions src/immutable/ImmutableEntity.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,41 @@
// MIT © 2017 azu
import { Identifier } from "../Identifier";
import { EntityLike, EntityLikeProps } from "../EntityLike";

export const isImmutableEntity = (entity: any): entity is ImmutableEntity<{ id: Identifier<any> }> => {
if (!entity) {
return false;
}
return entity instanceof ImmutableEntity;
};

/**
* ImmutableEntity is readonly Entity.
* It is created with Props and It has `props` property.
* This entity has received through constructor.
*
* @see https://www.quora.com/Why-did-React-use-props-as-an-abbreviation-for-property-properties
*/
export class ImmutableEntity<Props extends EntityLikeProps<Identifier<any>>> implements EntityLike<Props> {
props: Readonly<Props>;

constructor(props: Props) {
this.props = Object.freeze(props);
}

/*
* Check equality by identifier
*/
equals(object?: ImmutableEntity<Props>): boolean {
if (object == null || object == undefined) {
return false;
}
if (this === object) {
return true;
}
if (!isImmutableEntity(object)) {
return false;
}
return this.props.id.equals(object.props.id);
}
}
15 changes: 15 additions & 0 deletions src/immutable/ImmutableValueObject.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,15 @@
import { shallowEqual } from "shallow-equal-object";

export class ImmutableValueObject<Props extends object> {
props: Readonly<Props>;
constructor(props: Props) {
this.props = Object.freeze(props);
}
/**
* Check equality by shallow equals of properties.
* It can be override.
*/
equals(object?: ImmutableValueObject<{}>): boolean {
return shallowEqual(this, object);
}
}
28 changes: 16 additions & 12 deletions src/repository/NonNullableRepository.ts
Original file line number Diff line number Diff line change
@@ -1,45 +1,49 @@
// MIT © 2017 azu
import { Entity } from "../Entity";
import { RepositoryCore } from "./RepositoryCore";
import { MapLike } from "map-like";
import { RepositoryEventEmitter } from "./RepositoryEventEmitter";
import { EntityLike } from "../EntityLike";

/**
* NonNullableRepository has initial value.
* In other words, NonNullableRepository#get always return a value.
*/
export class NonNullableRepository<T extends Entity<any>> {
private core: RepositoryCore<T["id"], T>;
export class NonNullableRepository<
Entity extends EntityLike<any>,
Props extends Entity["props"],
Id extends Props["id"]
> {
private core: RepositoryCore<Entity, Props, Id>;

constructor(protected initialEntity: T) {
this.core = new RepositoryCore(new MapLike());
constructor(protected initialEntity: Entity) {
this.core = new RepositoryCore(new MapLike<string, Entity>());
}

get map(): MapLike<string, T> {
get map(): MapLike<string, Entity> {
return this.core.map;
}

get events(): RepositoryEventEmitter<T> {
get events(): RepositoryEventEmitter<Entity> {
return this.core.events;
}

get(): T {
get(): Entity {
return this.core.getLastSaved() || this.initialEntity;
}

getAll(): T[] {
getAll(): Entity[] {
return this.core.getAll();
}

findById(entityId?: T["id"]): T | undefined {
findById(entityId?: Id): Entity | undefined {
return this.core.findById(entityId);
}

save(entity: T): void {
save(entity: Entity): void {
this.core.save(entity);
}

delete(entity: T) {
delete(entity: Entity) {
this.core.delete(entity);
}

Expand Down
20 changes: 10 additions & 10 deletions src/repository/NullableRepository.ts
Original file line number Diff line number Diff line change
@@ -1,45 +1,45 @@
// MIT © 2017 azu
import { Entity } from "../Entity";
import { RepositoryCore } from "./RepositoryCore";
import { MapLike } from "map-like";
import { RepositoryEventEmitter } from "./RepositoryEventEmitter";
import { EntityLike } from "../EntityLike";

/**
* NullableRepository has not initial value.
* In other word, NullableRepository#get may return undefined.
*/
export class NullableRepository<T extends Entity<any>> {
private core: RepositoryCore<T["id"], T>;
export class NullableRepository<Entity extends EntityLike<any>, Props extends Entity["props"], Id extends Props["id"]> {
private core: RepositoryCore<Entity, Props, Id>;

constructor() {
this.core = new RepositoryCore(new MapLike());
}

get map(): MapLike<string, T> {
get map(): MapLike<string, Entity> {
return this.core.map;
}

get events(): RepositoryEventEmitter<T> {
get events(): RepositoryEventEmitter<Entity> {
return this.core.events;
}

get(): T | undefined {
get(): Entity | undefined {
return this.core.getLastSaved();
}

getAll(): T[] {
getAll(): Entity[] {
return this.core.getAll();
}

findById(entityId?: T["id"]): T | undefined {
findById(entityId?: Id): Entity | undefined {
return this.core.findById(entityId);
}

save(entity: T): void {
save(entity: Entity): void {
this.core.save(entity);
}

delete(entity: T) {
delete(entity: Entity) {
this.core.delete(entity);
}

Expand Down
37 changes: 20 additions & 17 deletions src/repository/RepositoryCore.ts
Original file line number Diff line number Diff line change
@@ -1,36 +1,39 @@
// MIT © 2017 azu
import { Entity } from "../Entity";
import { MapLike } from "map-like";
import { Identifier } from "../Identifier";
import { RepositoryDeletedEvent, RepositoryEventEmitter, RepositorySavedEvent } from "./RepositoryEventEmitter";
import { EntityLike } from "../EntityLike";

/**
* Repository Core implementation
*/
export class RepositoryCore<T extends Identifier<any>, P extends Entity<T>> {
public readonly map: MapLike<string, P>;
public readonly events: RepositoryEventEmitter<P>;
private lastUsed: P | undefined;
export class RepositoryCore<
Entity extends EntityLike<any>,
Props extends Entity["props"] = Entity["props"],
Id extends Props["id"] = Props["id"]
> {
public readonly map: MapLike<string, Entity>;
public readonly events: RepositoryEventEmitter<Entity>;
private lastUsed: Entity | undefined;

constructor(map: MapLike<string, P>) {
constructor(map: MapLike<string, Entity>) {
this.map = map;
this.events = new RepositoryEventEmitter<P>();
this.events = new RepositoryEventEmitter<Entity>();
}

/**
* Get last saved entity if exist.
* This is useful on client-side implementation.
* Because, client-side often access-user is a single user.
*/
getLastSaved(): P | undefined {
getLastSaved(): Entity | undefined {
return this.lastUsed;
}

/**
* Find a entity by `entityIdentifier` that is instance of Identifier class.
* Return `undefined` if not found entity.
*/
findById(entityIdentifier?: T): P | undefined {
findById(entityIdentifier?: Id): Entity | undefined {
if (!entityIdentifier) {
return;
}
Expand All @@ -40,31 +43,31 @@ export class RepositoryCore<T extends Identifier<any>, P extends Entity<T>> {
/**
* Find all entity that `predicate(entity)` return true
*/
findAll(predicate: (entity: P) => boolean): P[] {
findAll(predicate: (entity: Entity) => boolean): Entity[] {
return this.map.values().filter(predicate);
}

/**
* Get all entities
*/
getAll(): P[] {
getAll(): Entity[] {
return this.map.values();
}

/**
* Save entity to the repository.
*/
save(entity: P): void {
save(entity: Entity): void {
this.lastUsed = entity;
this.map.set(String(entity.id.toValue()), entity);
this.map.set(String(entity.props.id.toValue()), entity);
this.events.emit(new RepositorySavedEvent(entity));
}

/**
* Delete entity from the repository.
*/
delete(entity: P) {
this.map.delete(String(entity.id.toValue()));
delete(entity: Entity) {
this.map.delete(String(entity.props.id.toValue()));
if (this.lastUsed === entity) {
delete this.lastUsed;
}
Expand All @@ -74,7 +77,7 @@ export class RepositoryCore<T extends Identifier<any>, P extends Entity<T>> {
/**
* Delete entity by `entityIdentifier` that is instance of Identifier class.
*/
deleteById(entityIdentifier?: T) {
deleteById(entityIdentifier?: Id) {
if (!entityIdentifier) {
return;
}
Expand Down
3 changes: 2 additions & 1 deletion src/repository/RepositoryEventEmitter.ts
Original file line number Diff line number Diff line change
@@ -1,6 +1,7 @@
// MIT © 2017 azu
import { EventEmitter } from "events";
import { Entity } from "../Entity";
import { EntityLike } from "../EntityLike";

const SAVE = "SAVE";
const DELETE = "DELETE";
Expand All @@ -19,7 +20,7 @@ export class RepositoryDeletedEvent<T> {

export type RepositoryEvents<T = Entity<any>> = RepositorySavedEvent<T> | RepositoryDeletedEvent<T>;

export class RepositoryEventEmitter<T extends Entity<any>> {
export class RepositoryEventEmitter<T extends EntityLike<any>> {
private eventEmitter: EventEmitter;

constructor() {
Expand Down
Loading

0 comments on commit ed135c8

Please sign in to comment.