Skip to content

Commit

Permalink
Added async lifecycle callbacks.
Browse files Browse the repository at this point in the history
  • Loading branch information
meirgottlieb committed Mar 28, 2016
1 parent 14ebbb2 commit f1d9688
Show file tree
Hide file tree
Showing 17 changed files with 338 additions and 207 deletions.
42 changes: 35 additions & 7 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -604,12 +604,18 @@ By default, the name of the class is used.
### Lifecycle Callbacks

Hydrate provides callbacks that can be called on an entity during various stages of the entity lifecycle similar to
[JPA Lifecycle Callbacks](http://openjpa.apache.org/builds/1.2.3/apache-openjpa/docs/jpa_overview_pc_callbacks.html). A few
important restrictions:

* Lifecycle callbacks are synchronous and parameterless.
* Lifecycle callbacks must not modify any entities other than the entity they are called on; otherwise, results are unpredictable.
* Lifecycle callbacks should avoid accessing the [Session](https://artifacthealth.github.io/hydrate-mongodb/interfaces/session.html).
[JPA Lifecycle Callbacks](http://openjpa.apache.org/builds/1.2.3/apache-openjpa/docs/jpa_overview_pc_callbacks.html).
Callbacks are indicated by adding a decorator to a class method. A few important restrictions:

* If the method is parameterless, it is executed synchronously.
* If the method has a single parameter, it is executed as an asynchronous method and passed a callback for it to call
when finished. Any errors passed to the callback by the method get returned on the
[Session](https://artifacthealth.github.io/hydrate-mongodb/interfaces/session.html) operation that triggered the callback.
* The method must not have more than one parameter.
* The method must not modify any entities other than the entity that the method is called on; otherwise, results are unpredictable.
* The method should avoid accessing the [Session](https://artifacthealth.github.io/hydrate-mongodb/interfaces/session.html).
Because of the way operations are queued on the session, executing an operation on the session during a lifecycle callback
could cause the callback to hang.

Lifecycle callbacks are defined by adding the corresponding decorator to the class method as follows:

Expand All @@ -619,7 +625,7 @@ class Person {

@Field()
modified: Date;

@PreUpdate()
private _beforeUpdate(): void {

Expand All @@ -629,6 +635,28 @@ class Person {
}
```

Lifecycle callbacks can be used to validate entities.

```typescript
@Entity()
class Document {

@Field()
owner: Person;

@PrePersist()
@PreUpdate()
private _validate(callback: Callback): void {

if(!this.owner) {
return callback(new Error("A document must have an owner.");
}

callback();
}
}
```
The following decorators are available for lifecycle callbacks:
* [PrePersist](https://artifacthealth.github.io/hydrate-mongodb/globals.html#prePersist): Call method before a new
Expand Down
20 changes: 12 additions & 8 deletions benchmarks/sessionImpl.bench.ts
Original file line number Diff line number Diff line change
Expand Up @@ -103,28 +103,32 @@ class DummyPersister implements Persister {
this.identity = (<EntityMapping>mapping.inheritanceRoot).identity;
}

dirtyCheck(batch: Batch, entity: any, originalDocument: any): Object {
return originalDocument;
dirtyCheck(batch: Batch, entity: any, originalDocument: any, callback: ResultCallback<Object>): void {
callback(null, originalDocument);
}

addInsert(batch: Batch, entity: any): Object {
return {};
addInsert(batch: Batch, entity: any, callback: ResultCallback<Object>): void {
callback(null, {});
}

addRemove(batch: Batch, entity: any): void {
addRemove(batch: Batch, entity: any, callback: Callback): void {

callback();
}

postUpdate(entity: Object): void {
postUpdate(entity: Object, callback: Callback): void {

callback();
}

postInsert(entity: Object): void {
postInsert(entity: Object, callback: Callback): void {

callback();
}

postRemove(entity: Object): void {
postRemove(entity: Object, callback: Callback): void {

callback();
}

refresh(entity: any, callback: ResultCallback<any>): void {
Expand Down
5 changes: 3 additions & 2 deletions package.json
Original file line number Diff line number Diff line change
@@ -1,7 +1,7 @@
{
"name": "hydrate-mongodb",
"description": "An Object Document Mapper (ODM) for MongoDB.",
"version": "0.2.19",
"version": "0.2.20",
"author": {
"name": "Artifact Health, LLC",
"url": "http://www.artifacthealth.com"
Expand Down Expand Up @@ -50,6 +50,7 @@
"database",
"document",
"model",
"nosql"
"nosql",
"orm"
]
}
5 changes: 0 additions & 5 deletions src/batch.ts
Original file line number Diff line number Diff line change
Expand Up @@ -12,11 +12,6 @@ export class Batch implements Command {
private _commands: Command[] = [];
private _executed = false;

/**
* Error that occurred while building the batch.
*/
error: Error;

/**
* Gets a command from the batch.
* @param id The id of the command.
Expand Down
2 changes: 0 additions & 2 deletions src/mapping/arrayMapping.ts
Original file line number Diff line number Diff line change
@@ -1,8 +1,6 @@
import * as async from "async";
import {InternalMapping} from "./internalMapping";
import {MappingBase} from "./mappingBase";
import {MappingError} from "./mappingError";
import {Changes} from "./changes";
import {Reference} from "../reference";
import {MappingModel} from "./mappingModel";
import {InternalSession} from "../session";
Expand Down
3 changes: 0 additions & 3 deletions src/mapping/booleanMapping.ts
Original file line number Diff line number Diff line change
@@ -1,8 +1,5 @@
import {MappingBase} from "./mappingBase";
import {MappingError} from "./mappingError";
import {MappingModel} from "./mappingModel";
import {Changes} from "./changes";
import {InternalSession} from "../session";
import {ReadContext} from "./readContext";
import {WriteContext} from "./writeContext";

Expand Down
2 changes: 0 additions & 2 deletions src/mapping/bufferMapping.ts
Original file line number Diff line number Diff line change
@@ -1,8 +1,6 @@
import {Binary} from "mongodb";
import {MappingBase} from "./mappingBase";
import {MappingError} from "./mappingError";
import {MappingModel} from "./mappingModel";
import {InternalSession} from "../session";
import {ReadContext} from "./readContext";
import {WriteContext} from "./writeContext";

Expand Down
69 changes: 48 additions & 21 deletions src/mapping/entityMapping.ts
Original file line number Diff line number Diff line change
@@ -1,13 +1,10 @@
import * as async from "async";
import {IdentityGenerator} from "../config/configuration";
import {MappingError} from "./mappingError";
import {ClassMapping} from "./classMapping";
import {ChangeTrackingType} from "./mappingModel";
import {Index} from "./index";
import {CollectionOptions} from "./collectionOptions";
import {MappingRegistry} from "./mappingRegistry";
import {MappingModel} from "./mappingModel";
import {Changes} from "./changes";
import {Reference} from "../reference";
import {InternalSession} from "../session";
import {ResultCallback} from "../core/callback";
Expand All @@ -16,7 +13,6 @@ import {ReadContext} from "./readContext";
import {Observer} from "../observer";
import {Property} from "./property";
import {WriteContext} from "./writeContext";
import {PostPersist} from "./providers/decorators";

/**
* @hidden
Expand Down Expand Up @@ -274,18 +270,31 @@ export class EntityMapping extends ClassMapping {
super.resolveCore(context);
}

private _lifecycleCallbacks: { [event: number]: MappingModel.LifecycleCallback[] };
private _lifecycleCallbacks: LifecycleEventList;
private _lifecycleCallbacksAsync: LifecycleEventList;

addLifecycleCallback(event: MappingModel.LifecycleEvent, callback: MappingModel.LifecycleCallback): void {
addLifecycleCallback(event: MappingModel.LifecycleEvent, method: Function, async: boolean): void {

if(!this._lifecycleCallbacks) {
this._lifecycleCallbacks = [];
var events: LifecycleEventList;

if(async) {
if (!this._lifecycleCallbacksAsync) {
this._lifecycleCallbacksAsync = [];
}
events = this._lifecycleCallbacksAsync;
}
var callbacks = this._lifecycleCallbacks[event];
if(!callbacks) {
callbacks = this._lifecycleCallbacks[event] = [];
else {
if (!this._lifecycleCallbacks) {
this._lifecycleCallbacks = [];
}
events = this._lifecycleCallbacks;
}

var callbacks = events[event];
if (!callbacks) {
callbacks = events[event] = [];
}
callbacks.push(callback);
callbacks.push(method);
}

/**
Expand All @@ -294,20 +303,38 @@ export class EntityMapping extends ClassMapping {
* @param event The lifecycle event.
* @param callback Called after lifecycle callbacks have executed.
*/
executeLifecycleCallbacks(entity: Object, event: MappingModel.LifecycleEvent): number {
executeLifecycleCallbacks(entity: Object, event: MappingModel.LifecycleEvent, callback: ResultCallback<number>): void {

var l = 0;

if(!this._lifecycleCallbacks) {
return 0;
if(this._lifecycleCallbacks) {
var callbacks = this._lifecycleCallbacks[event];
if(callbacks) {
l = callbacks.length;
for(var i = 0; i < l; i++) {
callbacks[i].call(entity);
}
}
}
var callbacks = this._lifecycleCallbacks[event];
if(!callbacks) {
return 0;

if(!this._lifecycleCallbacksAsync) {
return callback(null, l);
}

for(var i = 0, l = callbacks.length; i < l; i++) {
callbacks[i].call(entity);
var callbacks = this._lifecycleCallbacksAsync[event];
if(!callbacks) {
return callback(null, l);
}

return l;
async.eachSeries(callbacks, (item, done) => {
item.call(entity, done);
}, (err) => {
if(err) return callback(err);
callback(null, l + callbacks.length);
});
}
}

interface LifecycleEventList {
[event: number]: Function[];
}
14 changes: 4 additions & 10 deletions src/mapping/mappingModel.ts
Original file line number Diff line number Diff line change
Expand Up @@ -188,14 +188,6 @@ export namespace MappingModel {
Dereference = 0x00002000
}

/**
* A lifecycle event callback.
*/
export interface LifecycleCallback {

(): void;
}

/**
* Lifecycle events.
*/
Expand Down Expand Up @@ -328,9 +320,11 @@ export namespace MappingModel {
/**
* Adds a lifecycle callback to the entity mapping.
* @param event The lifecycle event.
* @param callback The callback.
* @param callback The method implementation.
* @param async Indicates if the method is async. If the method is async then it should take a single parameter
* which is the callback that it calls when it is finished; otherwise, it should be parameterless.
*/
addLifecycleCallback(event: LifecycleEvent, callback: LifecycleCallback): void;
addLifecycleCallback(event: LifecycleEvent, method: Function, async: boolean): void;
}

/**
Expand Down
7 changes: 3 additions & 4 deletions src/mapping/providers/annotations.ts
Original file line number Diff line number Diff line change
Expand Up @@ -677,13 +677,12 @@ export class LifecycleEventAnnotation extends Annotation implements MethodAnnota

if(!context.assertEntityMapping(mapping)) return;

if(method.parameters.length != 0) {
console.log(method.parameters);
context.addError("Lifecycle callback must be parameterless.");
if(method.parameters.length > 1) {
context.addError("Lifecycle callback method must have one or no parameters.");
return;
}

mapping.addLifecycleCallback(this.event, method.parent.ctr.prototype[method.name]);
mapping.addLifecycleCallback(this.event, method.parent.ctr.prototype[method.name], method.parameters.length == 1);
}
}

Expand Down
Loading

0 comments on commit f1d9688

Please sign in to comment.