Skip to content

Commit

Permalink
added moment.js support
Browse files Browse the repository at this point in the history
  • Loading branch information
marcj committed Dec 20, 2019
1 parent c3e7359 commit e4905ae
Show file tree
Hide file tree
Showing 11 changed files with 256 additions and 56 deletions.
3 changes: 1 addition & 2 deletions .travis.yml
@@ -1,8 +1,7 @@
language: node_js
node_js:
- '10'
- '11'
- '12'
- '13'
services:
- mongodb
sudo: false
Expand Down
69 changes: 45 additions & 24 deletions README.md
Expand Up @@ -250,7 +250,7 @@ you might have the need to serialize only one field.
* partialPlainToMongo
* partialMongoToPlain

## Types / Decorators
## `@f` decorator: define types

Class fields are annotated using mainly [@f](https://marshal.marcj.dev/modules/_marcj_marshal.html#f).
You can define primitives, class mappings, relations between parents, and indices for the database (currently MongoDB).
Expand All @@ -277,6 +277,7 @@ class Page {
````

Example *not* valid decorators:

```typescript
import {f} from '@marcj/marshal';
Expand All @@ -286,11 +287,11 @@ class Page {
}
````


More examples:

```typescript
import {f, uuid} from '@marcj/marshal';
import * as moment from 'moment';

class MyModel {
@f.primary().uuid()
Expand All @@ -299,38 +300,30 @@ class MyModel {
@f.optional().index()
name?: string;

@f.array(String).optional()
tags?: string[];
@f()
created: Date = new Date;

@f.moment()
modified?: moment.Moment = moment();
}
```

## TypeORM
### Moment.js

The meta information about your entity can be exported to TypeORM EntitySchema.
Instead of `Date` object you can use `Moment` if `moment` is installed.

In MongoDB it's stored as `Date`. In JSON its encoded as ISO8601 string.

```typescript
// typeorm.js
import {getTypeOrmEntity} from "@marcj/marshal-mongo";
import {f} from '@marcj/marshal';
import * as moment from 'moment';

const TypeOrmSchema = getTypeOrmEntity(MyEntity);
module.exports = {
type: "mongodb",
host: "localhost",
port: 27017,
database: "test",
useNewUrlParser: true,
synchronize: true,
entities: [TypeOrmSchema]
class MyModel {
@f.moment()
created?: moment.Moment = moment();
}
```

Marshal.ts uses only TypeORM for connection abstraction and to generate a `EntitySchema` for your typeOrm use-cases.
You need in most cases only to use the @Field decorator with some other Marshal decorators (like @EnumField, @IDField, @UUIDField, @Index)
on your entity.

You can generate a schema for Typeorm using `getTypeOrmEntity` and then pass this to your `createConnection` call,
which makes it possible to sync the schema defined only with Marshal decorators with your database managed by Typeorm.

### Exclude

`exclude` lets you exclude properties from a class in a certain
Expand Down Expand Up @@ -530,6 +523,34 @@ expect(plain['childrenCollection.1']).toEqual({label: 'Bar4'});
expect(plain['childrenCollection.2.label']).toEqual('Bar5');
```

### TypeORM

The meta information about your entity can be exported to TypeORM EntitySchema.

```typescript
// typeorm.js
import {getTypeOrmEntity} from "@marcj/marshal-mongo";
const TypeOrmSchema = getTypeOrmEntity(MyEntity);
module.exports = {
type: "mongodb",
host: "localhost",
port: 27017,
database: "test",
useNewUrlParser: true,
synchronize: true,
entities: [TypeOrmSchema]
}
```

Marshal.ts uses only TypeORM for connection abstraction and to generate a `EntitySchema` for your typeOrm use-cases.
You need in most cases only to use the @Field decorator with some other Marshal decorators (like @EnumField, @IDField, @UUIDField, @Index)
on your entity.

You can generate a schema for Typeorm using `getTypeOrmEntity` and then pass this to your `createConnection` call,
which makes it possible to sync the schema defined only with Marshal decorators with your database managed by Typeorm.


## Mongo Database

Marshal's MongoDB database abstraction makes it super easy to
Expand Down
8 changes: 7 additions & 1 deletion packages/core/package.json
Expand Up @@ -15,7 +15,11 @@
"author": "Marc J. Schmidt <marc@marcjschmidt.de>",
"license": "MIT",
"peerDependencies": {
"buffer": "^5.2.1"
"buffer": "^5.2.1",
"moment": "^2.0.0"
},
"optionalDependencies": {
"moment": "^2.0.0"
},
"dependencies": {
"@marcj/estdlib": "^0.1.15",
Expand All @@ -25,6 +29,8 @@
},
"devDependencies": {
"@types/clone": "^0.1.30",
"@types/moment": "^2.0.0",
"moment": "^2.0.0",
"buffer": "^5.2.1"
},
"jest": {
Expand Down
100 changes: 83 additions & 17 deletions packages/core/src/decorators.ts
Expand Up @@ -1163,6 +1163,10 @@ fRaw['enum'] = function (this: FieldDecoratorResult, clazz: any, allowLabelsAsVa
return EnumField(clazz, allowLabelsAsValue);
};

fRaw['moment'] = function (this: FieldDecoratorResult): FieldDecoratorResult {
return MomentField();
};

fRaw['forward'] = function (this: FieldDecoratorResult, f: () => TYPES): FieldDecoratorResult {
return Field(forwardRef(f));
};
Expand All @@ -1175,13 +1179,7 @@ fRaw['forwardMap'] = function (this: FieldDecoratorResult, f: () => TYPES): Fiel
return Field(forwardRef(f)).asMap();
};

/**
* THis is the main decorator to define a properties on class or arguments on methods.
*
* @see FieldDecoratorResult
* @category Decorator
*/
export const f: FieldDecoratorResult & {
export interface MainDecorator {
/**
* Defines a type for a certain field. This is only necessary for custom classes
* if the Typescript compiler does not include the reflection type in the build output.
Expand All @@ -1193,7 +1191,7 @@ export const f: FieldDecoratorResult & {
* }
* ```
*/
type: (type: TYPES) => FieldDecoratorResult,
type(type: TYPES): FieldDecoratorResult;

/**
* Marks a field as array.
Expand All @@ -1205,7 +1203,7 @@ export const f: FieldDecoratorResult & {
* }
* ```
*/
array: (type: TYPES) => FieldDecoratorResult,
array(type: TYPES): FieldDecoratorResult;

/**
* Marks a field as enum.
Expand All @@ -1225,12 +1223,20 @@ export const f: FieldDecoratorResult & {
*
* If allowLabelsAsValue is set, you can use the enum labels as well for setting the property value using plainToClass().
*/
enum: <T>(type: any, allowLabelsAsValue?: boolean) => FieldDecoratorResult,
enum<T>(type: any, allowLabelsAsValue?: boolean): FieldDecoratorResult;

/**
* Marks a field as Moment.js value. Mongo and JSON transparent uses its toJSON() result.
* In MongoDB its stored as Date.
*
* You have to install moment npm package in order to use it.
*/
moment(): FieldDecoratorResult;

/**
* Marks a field as type any. It does not transform the value and directly uses JSON.parse/stringify.
*/
any: () => FieldDecoratorResult,
any(): FieldDecoratorResult;

/**
* Marks a field as map.
Expand All @@ -1245,7 +1251,7 @@ export const f: FieldDecoratorResult & {
* }
* ```
*/
map: (type: TYPES) => FieldDecoratorResult,
map(type: TYPES): FieldDecoratorResult;

/**
* Forward references a type, required for circular reference.
Expand All @@ -1257,7 +1263,7 @@ export const f: FieldDecoratorResult & {
* }
* ```
*/
forward: (f: () => TYPES) => FieldDecoratorResult,
forward(f: () => TYPES): FieldDecoratorResult;

/**
* Forward references a type in an array, required for circular reference.
Expand All @@ -1269,7 +1275,7 @@ export const f: FieldDecoratorResult & {
* }
* ```
*/
forwardArray: (f: () => TYPES) => FieldDecoratorResult,
forwardArray(f: () => TYPES): FieldDecoratorResult;

/**
* Forward references a type in a map, required for circular reference.
Expand All @@ -1281,8 +1287,58 @@ export const f: FieldDecoratorResult & {
* }
* ```
*/
forwardMap: (f: () => TYPES) => FieldDecoratorResult,
} = fRaw as any;
forwardMap(f: () => TYPES): FieldDecoratorResult;
}

/**
* This is the main decorator to define a properties on class or arguments on methods.
*
* ```typescript
* class SubModel {
* @f label: string;
* }
*
* export enum Plan {
* DEFAULT,
* PRO,
* ENTERPRISE,
* }
*
* class SimpleModel {
* @f.primary().uuid()
* id: string = uuid();
*
* @f.array(String)
* tags: string[] = [];
*
* @f.type(Buffer).optional() //binary
* picture?: Buffer;
*
* @f
* type: number = 0;
*
* @f.enum(Plan)
* plan: Plan = Plan.DEFAULT;
*
* @f
* created: Date = new Date;
*
* @f.array(SubModel)
* children: SubModel[] = [];
*
* @f.map(SubModel)
* childrenMap: {[key: string]: SubModel} = {};
*
* constructor(
* @f.index().asName('name') //asName is required for minimized code
* public name: string
* ) {}
* }
* ```
*
* @category Decorator
*/
export const f: MainDecorator & FieldDecoratorResult = fRaw as any;

/**
* @hidden
Expand Down Expand Up @@ -1336,8 +1392,9 @@ export function MultiIndex(fields: string[], options: IndexOptions, name?: strin

/**
* Used to define a field as Enum.
*
* If allowLabelsAsValue is set, you can use the enum labels as well for setting the property value using plainToClass().
*
* @internal
*/
function EnumField<T>(type: any, allowLabelsAsValue = false) {
return FieldDecoratorWrapper((target, property, returnType?: any) => {
Expand All @@ -1348,3 +1405,12 @@ function EnumField<T>(type: any, allowLabelsAsValue = false) {
}
});
}

/**
* @internal
*/
function MomentField<T>() {
return FieldDecoratorWrapper((target, property, returnType?: any) => {
property.type = 'moment';
});
}
26 changes: 21 additions & 5 deletions packages/core/src/mapper.ts
Expand Up @@ -23,13 +23,24 @@ export type Types =
| 'uuid'
| 'binary'
| 'class'
| 'moment'
| 'date'
| 'string'
| 'boolean'
| 'number'
| 'enum'
| 'any';

export let moment: any = () => {
throw new Error('Moment.js not installed')
};

declare function require(moduleName: string): any;

try {
moment = require('moment');
} catch(e) {}

const cache = new Map<Object, Map<string, any>>();

/**
Expand Down Expand Up @@ -75,7 +86,7 @@ export interface ResolvedReflectionFound {
*/
export type ResolvedReflection = ResolvedReflectionFound | undefined;

type ResolvedReflectionCaches = {[path: string]: ResolvedReflection};
type ResolvedReflectionCaches = { [path: string]: ResolvedReflection };
const resolvedReflectionCaches = new Map<ClassType<any>, ResolvedReflectionCaches>();

/**
Expand Down Expand Up @@ -249,10 +260,6 @@ export function propertyClassToPlain<T>(classType: ClassType<T>, propertyName: s
const {type, typeValue, array, map} = reflection;

function convert(value: any) {
if ('date' === type && value instanceof Date) {
return value.toJSON();
}

if ('string' === type) {
return String(value);
}
Expand Down Expand Up @@ -280,6 +287,11 @@ export function propertyClassToPlain<T>(classType: ClassType<T>, propertyName: s
return classToPlain(typeValue, value);
}

//Date/moment automatically is converted since it has toJSON() method.
if (value && 'function' === typeof value.toJSON) {
return value.toJSON();
}

return value;
}

Expand Down Expand Up @@ -333,6 +345,10 @@ export function propertyPlainToClass<T>(
return new Date(value);
}

if ('moment' === type && ('string' === typeof value || 'number' === typeof value)) {
return moment(value);
}

if ('string' === type && 'string' !== typeof value) {
return String(value);
}
Expand Down

0 comments on commit e4905ae

Please sign in to comment.