type-mongodb
Advanced tools
Comparing version 0.0.1-beta8 to 1.0.0-beta.0
@@ -17,3 +17,11 @@ import { Newable } from '../types'; | ||
export declare function Parent(): PropertyDecorator; | ||
export interface AbstractDiscriminatorOptions { | ||
property: string; | ||
} | ||
export interface DiscriminatorOptions { | ||
value: string; | ||
} | ||
export declare function Discriminator(options: AbstractDiscriminatorOptions | DiscriminatorOptions): ClassDecorator; | ||
export declare function MappedDiscriminator(type: string, discriminator: () => any): ClassDecorator; | ||
export {}; | ||
//# sourceMappingURL=index.d.ts.map |
"use strict"; | ||
Object.defineProperty(exports, "__esModule", { value: true }); | ||
exports.Parent = exports.Field = exports.Document = void 0; | ||
exports.MappedDiscriminator = exports.Discriminator = exports.Parent = exports.Field = exports.Document = void 0; | ||
const definitionStorage_1 = require("../utils/definitionStorage"); | ||
@@ -44,2 +44,46 @@ function Document(options = {}) { | ||
exports.Parent = Parent; | ||
function Discriminator(options) { | ||
return (target) => { | ||
if (options.property) { | ||
// this is the base abstract discriminator | ||
const opts = options; | ||
const mapping = { | ||
DocumentClass: target, | ||
propertyName: opts.property, | ||
isMapped: true, | ||
map: {} | ||
}; | ||
definitionStorage_1.definitionStorage.discriminators.set(target, Object.assign(Object.assign({}, (definitionStorage_1.definitionStorage.discriminators.get(target) || {})), mapping)); | ||
} | ||
else { | ||
const opts = options; | ||
let definition; | ||
// locate abstract class | ||
let proto = Object.getPrototypeOf(target); | ||
while (proto && proto.prototype) { | ||
definition = definitionStorage_1.definitionStorage.discriminators.get(proto); | ||
if (definition) { | ||
console.log(definition); | ||
break; | ||
} | ||
proto = Object.getPrototypeOf(proto); | ||
} | ||
if (!definition) { | ||
throw new Error(`Discriminator value "${target.name}" does not have a properly mapped base "@Discriminator()"`); | ||
} | ||
definition.map[opts.value] = () => target; | ||
} | ||
}; | ||
} | ||
exports.Discriminator = Discriminator; | ||
function MappedDiscriminator(type, discriminator) { | ||
return (target) => { | ||
const DocumentClass = discriminator(); | ||
const def = definitionStorage_1.definitionStorage.discriminators.has(DocumentClass) | ||
? definitionStorage_1.definitionStorage.discriminators.get(DocumentClass) | ||
: { DocumentClass, map: {} }; | ||
definitionStorage_1.definitionStorage.discriminators.set(DocumentClass, Object.assign(Object.assign({}, def), { map: Object.assign(Object.assign({}, def.map), { [type]: () => target }) })); | ||
}; | ||
} | ||
exports.MappedDiscriminator = MappedDiscriminator; | ||
//# sourceMappingURL=index.js.map |
@@ -10,2 +10,3 @@ import { DocumentClass, Newable } from '..'; | ||
private compiledFromDB; | ||
private compiledInit; | ||
private compiledMerge; | ||
@@ -12,0 +13,0 @@ private constructor(); |
@@ -33,5 +33,6 @@ "use strict"; | ||
} | ||
this.compiledToDB = this.createCompiler(false, true); | ||
this.compiledFromDB = this.createCompiler(true, false); | ||
this.compiledMerge = this.createCompiler(false, false); | ||
this.compiledToDB = this.createCompiler('toDB', false, true); | ||
this.compiledFromDB = this.createCompiler('fromDB', true, false); | ||
this.compiledInit = this.createCompiler('init', false, false); | ||
this.compiledMerge = this.createCompiler('merge', false, false); | ||
this.isCompiled = true; | ||
@@ -41,6 +42,28 @@ } | ||
this.assertIsCompiled(); | ||
return this.compiledMerge(this.prepare(new this.meta.DocumentClass()), props, parent); | ||
if (this.meta.discriminator) { | ||
const { propertyName, mapping } = this.meta.discriminator; | ||
return props[propertyName] && mapping.has(props[propertyName]) | ||
? mapping.get(props[propertyName]).transformer.init(props, parent) | ||
: undefined; | ||
} | ||
return this.compiledInit(this.prepare(new this.meta.DocumentClass()), props, parent); | ||
} | ||
merge(model, props, parent) { | ||
this.assertIsCompiled(); | ||
if (!model) { | ||
return this.init(props, parent); | ||
} | ||
model = this.prepare(model); | ||
if (this.meta.discriminator) { | ||
const { propertyName, mapping } = this.meta.discriminator; | ||
if (!props[propertyName] || !mapping.has(props[propertyName])) { | ||
return; | ||
} | ||
const { DocumentClass, transformer } = mapping.get(props[propertyName]); | ||
// when a discriminator type changes, it is brand new, so lets create | ||
// it from scratch. | ||
return model instanceof DocumentClass | ||
? transformer.merge(model, props, parent) | ||
: transformer.init(props, parent); | ||
} | ||
return this.compiledMerge(this.prepare(model), props, parent); | ||
@@ -50,2 +73,8 @@ } | ||
this.assertIsCompiled(); | ||
if (this.meta.discriminator) { | ||
const { fieldName, mapping } = this.meta.discriminator; | ||
return doc[fieldName] && mapping.has(doc[fieldName]) | ||
? mapping.get(doc[fieldName]).transformer.fromDB(doc, parent) | ||
: undefined; | ||
} | ||
return this.compiledFromDB(new this.meta.DocumentClass(), doc, parent); | ||
@@ -55,5 +84,11 @@ } | ||
this.assertIsCompiled(); | ||
if (this.meta.discriminator) { | ||
const { propertyName, mapping } = this.meta.discriminator; | ||
return model[propertyName] && mapping.has(model[propertyName]) | ||
? mapping.get(model[propertyName]).transformer.toDB(model) | ||
: undefined; | ||
} | ||
return this.compiledToDB({}, this.prepare(model)); | ||
} | ||
createCompiler(isFromDB, isToDB) { | ||
createCompiler(transformerFnName, isFromDB, isToDB) { | ||
const context = new Map(); | ||
@@ -75,10 +110,12 @@ const has = (accessor) => { | ||
const transformerFnVar = reserveVariable(`${fieldName}_transformer`); | ||
const transformerFn = isToDB | ||
? embeddedTransformer.toDB | ||
: embeddedTransformer.fromDB; | ||
const initTransformerFnVar = reserveVariable(`${fieldName}_init_transformer`); | ||
const transformerFn = embeddedTransformer[transformerFnName]; | ||
context.set(transformerFnVar, transformerFn.bind(embeddedTransformer)); | ||
context.set(initTransformerFnVar, embeddedTransformer.init.bind(embeddedTransformer)); | ||
if (isEmbeddedArray) { | ||
return ` | ||
if (${has(accessor)} && Array.isArray(source["${accessor}"])) { | ||
target["${setter}"] = source["${accessor}"].map(v => ${transformerFnVar}(v, target)); | ||
${transformerFnName === 'merge' | ||
? `target["${setter}"] = source["${accessor}"].map(v => ${transformerFnVar}(undefined, v, target));` | ||
: `target["${setter}"] = source["${accessor}"].map(v => ${transformerFnVar}(v, target));`} | ||
} | ||
@@ -89,3 +126,5 @@ `; | ||
if (${has(accessor)}) { | ||
target["${setter}"] = ${transformerFnVar}(source["${accessor}"], target); | ||
${transformerFnName === 'merge' | ||
? `target["${setter}"] = ${transformerFnVar}(target["${setter}"], source["${accessor}"], target);` | ||
: `target["${setter}"] = ${transformerFnVar}(source["${accessor}"], target);`} | ||
} | ||
@@ -92,0 +131,0 @@ `; |
@@ -6,2 +6,3 @@ import { ObjectId } from 'mongodb'; | ||
import { ParentDefinition } from './definitions'; | ||
import { DiscriminatorMetadata } from './DiscriminatorMetadata'; | ||
export declare type FieldsMetadata = Map<string, FieldMetadata>; | ||
@@ -18,3 +19,4 @@ /** | ||
readonly parent?: ParentDefinition; | ||
constructor(DocumentClass: D, fields: FieldsMetadata, parent?: ParentDefinition); | ||
readonly discriminator?: DiscriminatorMetadata; | ||
constructor(DocumentClass: D, fields: FieldsMetadata, parent?: ParentDefinition, discriminator?: DiscriminatorMetadata); | ||
addField(prop: FieldMetadata): void; | ||
@@ -21,0 +23,0 @@ /** |
@@ -11,3 +11,3 @@ "use strict"; | ||
class AbstractDocumentMetadata { | ||
constructor(DocumentClass, fields, parent) { | ||
constructor(DocumentClass, fields, parent, discriminator) { | ||
this.DocumentClass = DocumentClass; | ||
@@ -17,2 +17,3 @@ this.name = DocumentClass.name; | ||
this.parent = parent; | ||
this.discriminator = discriminator; | ||
this.transformer = DocumentTransformer_1.DocumentTransformer.create(this); | ||
@@ -60,3 +61,3 @@ } | ||
init(props) { | ||
return this.transformer.merge(new this.DocumentClass(), props); | ||
return this.transformer.init(props); | ||
} | ||
@@ -63,0 +64,0 @@ /** |
@@ -26,2 +26,11 @@ import { DocumentClass, Newable } from '../types'; | ||
} | ||
export interface DiscriminatorDefinition<T = any> { | ||
DocumentClass: DocumentClass<T>; | ||
isMapped?: boolean; | ||
propertyName?: string; | ||
fieldName?: string; | ||
map: { | ||
[type: string]: () => Newable; | ||
}; | ||
} | ||
//# sourceMappingURL=definitions.d.ts.map |
@@ -50,3 +50,5 @@ import { DocumentMetadata } from './DocumentMetadata'; | ||
protected buildFields(target: DocumentClass, fields?: FieldsMetadata): FieldsMetadata; | ||
private locateParentDefinition; | ||
private buildDiscriminatorMetadata; | ||
} | ||
//# sourceMappingURL=DocumentMetadataFactory.d.ts.map |
@@ -19,2 +19,3 @@ "use strict"; | ||
const isPromise_1 = require("../utils/isPromise"); | ||
const DiscriminatorMetadata_1 = require("./DiscriminatorMetadata"); | ||
/** | ||
@@ -132,3 +133,8 @@ * DocumentMetadataFactory builds and validates all the Document's metadata. | ||
buildEmbeddedDocumentMetadata(DocumentClass) { | ||
return new EmbeddedDocumentMetadata_1.EmbeddedDocumentMetadata(DocumentClass, this.buildFields(DocumentClass), definitionStorage_1.definitionStorage.parents.get(DocumentClass)); | ||
if (this.loadedEmbeddedDocumentMetadata.has(DocumentClass)) { | ||
return this.loadedEmbeddedDocumentMetadata.get(DocumentClass); | ||
} | ||
const embeddedMetadata = new EmbeddedDocumentMetadata_1.EmbeddedDocumentMetadata(DocumentClass, this.buildFields(DocumentClass), this.locateParentDefinition(DocumentClass), this.buildDiscriminatorMetadata(DocumentClass)); | ||
this.loadedEmbeddedDocumentMetadata.set(DocumentClass, embeddedMetadata); | ||
return embeddedMetadata; | ||
} | ||
@@ -152,3 +158,2 @@ /** | ||
const embeddedMetadata = this.buildEmbeddedDocumentMetadata(embeddedType); | ||
this.loadedEmbeddedDocumentMetadata.set(embeddedType, embeddedMetadata); | ||
fields.set(prop.fieldName, new FieldMetadata_1.FieldMetadata(Object.assign(Object.assign({}, prop), { isEmbeddedArray, | ||
@@ -160,10 +165,15 @@ embeddedMetadata, | ||
} | ||
let parent = Object.getPrototypeOf(target); | ||
while (parent && parent.prototype) { | ||
if (definitionStorage_1.definitionStorage.fields.has(parent)) { | ||
this.buildFields(parent, fields); | ||
// locate inherited decorated fields | ||
let proto = Object.getPrototypeOf(target); | ||
while (proto && proto.prototype) { | ||
if (definitionStorage_1.definitionStorage.fields.has(proto)) { | ||
this.buildFields(proto, fields); | ||
} | ||
parent = Object.getPrototypeOf(parent); | ||
proto = Object.getPrototypeOf(proto); | ||
} | ||
if (!fields.size) { | ||
// make the error more clear for discriminator mapped classes | ||
if (definitionStorage_1.definitionStorage.discriminators.has(target)) { | ||
DiscriminatorMetadata_1.DiscriminatorMetadata.assertValid(definitionStorage_1.definitionStorage.discriminators.get(target)); | ||
} | ||
throw new Error(`"${target.name}" does not have any fields`); | ||
@@ -173,4 +183,28 @@ } | ||
} | ||
locateParentDefinition(target) { | ||
if (definitionStorage_1.definitionStorage.parents.get(target)) { | ||
return definitionStorage_1.definitionStorage.parents.get(target); | ||
} | ||
// locate inherited `Parent()` | ||
let proto = Object.getPrototypeOf(target); | ||
while (proto && proto.prototype) { | ||
if (definitionStorage_1.definitionStorage.parents.get(proto)) { | ||
return definitionStorage_1.definitionStorage.parents.get(proto); | ||
} | ||
proto = Object.getPrototypeOf(proto); | ||
} | ||
} | ||
buildDiscriminatorMetadata(target) { | ||
if (!definitionStorage_1.definitionStorage.discriminators.has(target)) { | ||
return; | ||
} | ||
const def = definitionStorage_1.definitionStorage.discriminators.get(target); | ||
const map = new Map(); | ||
Object.keys(def.map).forEach((type) => { | ||
map.set(type, this.buildEmbeddedDocumentMetadata(def.map[type]())); | ||
}); | ||
return new DiscriminatorMetadata_1.DiscriminatorMetadata(def, map); | ||
} | ||
} | ||
exports.DocumentMetadataFactory = DocumentMetadataFactory; | ||
//# sourceMappingURL=DocumentMetadataFactory.js.map |
import { DocumentClass } from '../types'; | ||
import { DocumentDefinition, FieldDefinition, ParentDefinition } from '../metadata/definitions'; | ||
import { DocumentDefinition, FieldDefinition, ParentDefinition, DiscriminatorDefinition } from '../metadata/definitions'; | ||
declare type FieldName = string; | ||
declare type DocumentStorage = Map<DocumentClass<any>, DocumentDefinition>; | ||
declare type FieldStorage = Map<DocumentClass<any>, Map<FieldName, FieldDefinition>>; | ||
declare type ParentStorage = Map<DocumentClass<any>, ParentDefinition>; | ||
declare type DocumentStorage = Map<DocumentClass, DocumentDefinition>; | ||
declare type FieldStorage = Map<DocumentClass, Map<FieldName, FieldDefinition>>; | ||
declare type ParentStorage = Map<DocumentClass, ParentDefinition>; | ||
declare type DiscriminatorStorage = Map<DocumentClass, DiscriminatorDefinition>; | ||
export declare const definitionStorage: { | ||
@@ -11,4 +12,5 @@ documents: DocumentStorage; | ||
parents: ParentStorage; | ||
discriminators: DiscriminatorStorage; | ||
}; | ||
export {}; | ||
//# sourceMappingURL=definitionStorage.d.ts.map |
@@ -7,4 +7,5 @@ "use strict"; | ||
fields: new Map(), | ||
parents: new Map() | ||
parents: new Map(), | ||
discriminators: new Map() | ||
}; | ||
//# sourceMappingURL=definitionStorage.js.map |
{ | ||
"name": "type-mongodb", | ||
"version": "0.0.1-beta8", | ||
"version": "1.0.0-beta.0", | ||
"description": "A simple decorator based MongoDB ODM.", | ||
@@ -5,0 +5,0 @@ "keywords": [], |
263
README.md
@@ -5,2 +5,263 @@ <h1 align="center" style="border-bottom: none;">🔗 type-mongodb</h1> | ||
**type-mongodb** makes it easy to map classes to MongoDB documents and back using `@decorators`. | ||
``` | ||
## Features | ||
- Extremely simply `@Decorator()` based document mapping | ||
- Very fast 🚀! (thanks to JIT compilation) | ||
- RAW. MongoDB is already extremely easy to use. It's best to use the driver | ||
as it's intended. No validation, no change-set tracking, no magic -- just class mapping | ||
- Custom Repositories | ||
- Event Subscribers | ||
- Transaction Support | ||
- Discriminator Mapping | ||
- & more! | ||
## How to use | ||
`type-orm` allows you to create a base document class for common functionality. Notice | ||
that we don't enforce strict types. MongoDB is "schema-less", so we've decided to just | ||
support their main types and not do anything fancy. Again, we wanted to keep it as close | ||
to the core driver as possible. | ||
```typescript | ||
import { Field } from 'type-mongodb'; | ||
import { ObjectId } from 'mongodb'; | ||
abstract class BaseDocument { | ||
@Field() | ||
_id: ObjectId = new ObjectId(); | ||
get id(): string { | ||
return this._id.toHexString(); | ||
} | ||
@Field() | ||
createdAt: Date = new Date(); | ||
@Field() | ||
updatedAt: Date = new Date(); | ||
} | ||
``` | ||
Now create our document class with some fields. | ||
```typescript | ||
import { Document, Field } from 'type-mongodb'; | ||
import { BaseDocument, Address, Pet } from './models'; | ||
@Document() | ||
class User extends BaseDocument { | ||
@Field() | ||
name: string; | ||
@Field(() => Address) | ||
address: Address; // single embedded document | ||
@Field(() => [Address]) | ||
addresses: Address[] = []; // array of embedded documents | ||
@Field(() => [Pet]) | ||
pets: Pet[] = []; // array of discriminator mapped documents | ||
@Field(() => [Pet]) | ||
favoritePet: Pet = []; // single discriminator mapped document | ||
} | ||
``` | ||
And here's the embedded `Address` document. | ||
```typescript | ||
import { Field } from 'type-mongodb'; | ||
class Address { | ||
@Field() | ||
city: string; | ||
@Field() | ||
state: string; | ||
} | ||
``` | ||
`type-mongodb` also has support for discriminator mapping (polymorphism). You do this | ||
by creating a base class mapped by `@Discriminator({ property: '...' })` with a `@Field()` with the | ||
name of the "property". Then decorate discriminator types with `@Discriminator({ value: '...' })` | ||
and `type-mongodb` takes care of the rest. | ||
```typescript | ||
import { Discriminator, Field } from 'type-mongodb'; | ||
@Discriminator({ property: 'type' }) | ||
abstract class Pet { | ||
@Field() | ||
abstract type: string; | ||
@Field() | ||
abstract sound: string; | ||
speak(): string { | ||
return this.sound; | ||
} | ||
} | ||
@Discriminator({ value: 'dog' }) | ||
class Dog extends Pet { | ||
type: string = 'dog'; | ||
sound: string = 'ruff'; | ||
// dog specific fields & methods | ||
} | ||
@Discriminator({ value: 'cat' }) | ||
class Cat extends Pet { | ||
type: string = 'cat'; | ||
sound: string = 'meow'; | ||
// cat specific fields & methods | ||
} | ||
``` | ||
And now, lets see the magic! | ||
```typescript | ||
import { DocumentManager } from 'type-mongodb'; | ||
import { User } from './models'; | ||
async () => { | ||
const dm = await DocumentManager.create({ | ||
connection: { | ||
uri: process.env.MONGO_URI, | ||
database: process.env.MONGO_DB | ||
}, | ||
documents: [User] | ||
}); | ||
const repository = dm.getRepository(User); | ||
await repository.create({ | ||
name: 'John Doe', | ||
address: { | ||
city: 'San Diego', | ||
state: 'CA' | ||
}, | ||
addresses: [ | ||
{ | ||
city: 'San Diego', | ||
state: 'CA' | ||
} | ||
], | ||
pets: [{ type: 'dog', sound: 'ruff' }], | ||
favoritePet: { type: 'dog', sound: 'ruff' } | ||
}); | ||
const users = await repository.find().toArray(); | ||
}; | ||
``` | ||
What about custom repositories? Well, that's easy too: | ||
```typescript | ||
import { Repository } from 'type-mongodb'; | ||
import { User } from './models'; | ||
export class UserRepository extends Repository<User> { | ||
async findJohnDoe(): Promise<User> { | ||
return this.findOneOrFail({ name: 'John Doe' }); | ||
} | ||
} | ||
``` | ||
Then register this repository with the `User` class: | ||
```typescript | ||
import { UserRepository } from './repositories'; | ||
// ... | ||
@Document({ repository: () => UserRepository }) | ||
class User extends BaseDocument { | ||
// ... | ||
} | ||
``` | ||
... and finally, to use: | ||
```typescript | ||
const repository = dm.getRepository<UserRepository>(User); | ||
``` | ||
What about events? We want the base class to have createdAt and updatedAt be mapped | ||
correctly. | ||
```typescript | ||
import { | ||
EventSubscriber, | ||
DocumentManager, | ||
InsertEvent, | ||
UpdateEvent | ||
} from 'type-mongodb'; | ||
import { BaseDocument } from './models'; | ||
export class TimestampableSubscriber implements EventSubscriber<BaseDocument> { | ||
// Find all documents that extend BaseDocument | ||
getSubscribedDocuments?(dm: DocumentManager): any[] { | ||
return dm | ||
.filterMetadata( | ||
(meta) => meta.DocumentClass.prototype instanceof BaseDocument | ||
) | ||
.map((meta) => meta.DocumentClass); | ||
} | ||
beforeInsert(e: InsertEvent<BaseDocument>) { | ||
if (!e.model.updatedAt) { | ||
e.model.updatedAt = new Date(); | ||
} | ||
if (!e.model.createdAt) { | ||
e.model.createdAt = new Date(); | ||
} | ||
} | ||
beforeUpdate(e: UpdateEvent<BaseDocument>) { | ||
this.prepareUpdate(e); | ||
} | ||
beforeUpdateMany(e: UpdateEvent<BaseDocument>) { | ||
this.prepareUpdate(e); | ||
} | ||
prepareUpdate(e: UpdateEvent<BaseDocument>) { | ||
e.update.$set = { | ||
updatedAt: new Date(), | ||
...(e.update.$set || {}) | ||
}; | ||
e.update.$setOnInsert = { | ||
createdAt: new Date(), | ||
...(e.update.$setOnInsert || {}) | ||
}; | ||
} | ||
} | ||
``` | ||
...then register TimestampableSubscriber: | ||
```typescript | ||
const dm = await DocumentManager.create({ | ||
/// ..., | ||
subscribers: [TimestampableSubscriber] | ||
}); | ||
``` | ||
#### Other Common Features | ||
```typescript | ||
// custom collection and database | ||
@Document({ database: 'app', collection: 'users' }) | ||
// using internal hydration methods | ||
dm.toDB(User, user); | ||
dm.fromDB(User, { /* document class */ }); | ||
dm.init(User, { /* user props */ }); | ||
dm.merge(User, user, { /* user props */ }); | ||
``` | ||
For more advanced usage and examples, check out the tests. |
Sorry, the diff of this file is not supported yet
Sorry, the diff of this file is not supported yet
Sorry, the diff of this file is not supported yet
Sorry, the diff of this file is not supported yet
Sorry, the diff of this file is not supported yet
Sorry, the diff of this file is not supported yet
Sorry, the diff of this file is not supported yet
Sorry, the diff of this file is not supported yet
Sorry, the diff of this file is not supported yet
Sorry, the diff of this file is not supported yet
Sorry, the diff of this file is not supported yet
License Policy Violation
LicenseThis package is not allowed per your license policy. Review the package's license to ensure compliance.
Found 1 instance in 1 package
License Policy Violation
LicenseThis package is not allowed per your license policy. Review the package's license to ensure compliance.
Found 1 instance in 1 package
201973
114
2483
267