From 289a8270e40d151799cb7b7abaceda85090e8c0c Mon Sep 17 00:00:00 2001 From: Steve Fan <29133953+stevefan1999-personal@users.noreply.github.com> Date: Thu, 28 Nov 2019 01:48:46 +0800 Subject: [PATCH 1/5] Add more decorators --- src/decorators/Attribute.ts | 7 +++-- src/decorators/BelongsTo.ts | 13 +++++++++ src/decorators/BelongsToMany.ts | 15 ++++++++++ src/decorators/Bool.ts | 7 +++-- src/decorators/DecoratedModel.ts | 41 ++++++++++++++++++++++++++ src/decorators/Field.ts | 6 ++-- src/decorators/HasMany.ts | 13 +++++++++ src/decorators/HasManyBy.ts | 13 +++++++++ src/decorators/HasManyThrough.ts | 15 ++++++++++ src/decorators/HasOne.ts | 13 +++++++++ src/decorators/Increment.ts | 8 ++++++ src/decorators/MorphMany.ts | 13 +++++++++ src/decorators/MorphOne.ts | 13 +++++++++ src/decorators/MorphTo.ts | 11 +++++++ src/decorators/MorphToMany.ts | 15 ++++++++++ src/decorators/MorphedByMany.ts | 15 ++++++++++ src/decorators/Num.ts | 7 +++-- src/decorators/PrimaryKey.ts | 12 ++++++++ src/decorators/Primitive.ts | 12 +++++--- src/decorators/Str.ts | 7 +++-- src/decorators/index.ts | 20 +++++++++++++ src/index.cjs.ts | 28 ++++++++++++++---- src/index.ts | 49 +++++++++++++++++++++++++++----- src/utils.ts | 4 +++ 24 files changed, 331 insertions(+), 26 deletions(-) create mode 100644 src/decorators/BelongsTo.ts create mode 100644 src/decorators/BelongsToMany.ts create mode 100644 src/decorators/DecoratedModel.ts create mode 100644 src/decorators/HasMany.ts create mode 100644 src/decorators/HasManyBy.ts create mode 100644 src/decorators/HasManyThrough.ts create mode 100644 src/decorators/HasOne.ts create mode 100644 src/decorators/Increment.ts create mode 100644 src/decorators/MorphMany.ts create mode 100644 src/decorators/MorphOne.ts create mode 100644 src/decorators/MorphTo.ts create mode 100644 src/decorators/MorphToMany.ts create mode 100644 src/decorators/MorphedByMany.ts create mode 100644 src/decorators/PrimaryKey.ts create mode 100644 src/decorators/index.ts create mode 100644 src/utils.ts diff --git a/src/decorators/Attribute.ts b/src/decorators/Attribute.ts index a4026ad..26d42c3 100644 --- a/src/decorators/Attribute.ts +++ b/src/decorators/Attribute.ts @@ -1,9 +1,12 @@ import PropertyDecorator from '../contracts/PropertyDecorator' +import { FunctorOrValue, unwrapFunctorOrValue } from '../utils' import Field from './Field' /** * Create a attribute decorator. */ -export default function Attribute (value: any = null): PropertyDecorator { - return Field(model => model.attr(value)) +export function Attribute (value: FunctorOrValue = null): PropertyDecorator { + return Field(model => model.attr(unwrapFunctorOrValue(value))) } + +export default Attribute diff --git a/src/decorators/BelongsTo.ts b/src/decorators/BelongsTo.ts new file mode 100644 index 0000000..32f8239 --- /dev/null +++ b/src/decorators/BelongsTo.ts @@ -0,0 +1,13 @@ +import PropertyDecorator from '@/contracts/PropertyDecorator' +import { Model } from '@vuex-orm/core' +import { FunctorOrValue, unwrapFunctorOrValue } from '../utils' +import Field from './Field' + +export function BelongsTo ( + parent: FunctorOrValue, + foreignKey: FunctorOrValue, ownerKey?: FunctorOrValue +): PropertyDecorator { + return Field(model => (model.belongsTo as any)(...[parent, foreignKey, ownerKey].map(unwrapFunctorOrValue))) +} + +export default BelongsTo diff --git a/src/decorators/BelongsToMany.ts b/src/decorators/BelongsToMany.ts new file mode 100644 index 0000000..6a6a7aa --- /dev/null +++ b/src/decorators/BelongsToMany.ts @@ -0,0 +1,15 @@ +import PropertyDecorator from '@/contracts/PropertyDecorator' +import { Model } from '@vuex-orm/core' +import { FunctorOrValue, unwrapFunctorOrValue } from '../utils' +import Field from './Field' + +export function BelongsToMany ( + related: FunctorOrValue, + pivot: FunctorOrValue, + foreignPivotKey: FunctorOrValue, relatedPivotKey: FunctorOrValue, + parentKey?: FunctorOrValue, relatedKey?: FunctorOrValue +): PropertyDecorator { + return Field(model => (model.belongsToMany as any)(...[related, pivot, foreignPivotKey, relatedPivotKey, parentKey, relatedKey].map(unwrapFunctorOrValue))) +} + +export default BelongsToMany diff --git a/src/decorators/Bool.ts b/src/decorators/Bool.ts index 9bb8a08..1c93861 100644 --- a/src/decorators/Bool.ts +++ b/src/decorators/Bool.ts @@ -1,10 +1,13 @@ import PropertyDecorator from '../contracts/PropertyDecorator' import { TypeOptions } from '../options/Options' +import { FunctorOrValue, unwrapFunctorOrValue } from '../utils' import Primitive from './Primitive' /** * Create a bool decorator. */ -export default function Bool (value: boolean | null, options?: TypeOptions): PropertyDecorator { - return Primitive(model => model.boolean(value), options) +export function Bool (value?: FunctorOrValue, options?: TypeOptions): PropertyDecorator { + return Primitive(model => model.boolean(unwrapFunctorOrValue(value)), options) } + +export default Bool diff --git a/src/decorators/DecoratedModel.ts b/src/decorators/DecoratedModel.ts new file mode 100644 index 0000000..98cf0bb --- /dev/null +++ b/src/decorators/DecoratedModel.ts @@ -0,0 +1,41 @@ +import { Model } from '@vuex-orm/core' +import InheritanceTypes from '@vuex-orm/core/lib/model/contracts/InheritanceTypes' +import PropertyDecorator from '../contracts/PropertyDecorator' + +/** + * Create a generic field decorator. + */ +export function DecoratedModel ( + entityName: string, + options: { + parentEntity?: string, + types?: InheritanceTypes, + typeKey?: string + } +): PropertyDecorator { + return (target: Model, _: string): void => { + const model = target.constructor as typeof Model + // Do this temporarily until upstream vuex-orm declare this field officially + // I want to use this to notify hot reloader in the future + ;(model as any).isDecorated = true + + model.entity = entityName + + // generate base entity key assignment + if (options?.parentEntity) { + model.baseEntity = options.parentEntity + } + + // generate type discriminator + if (options?.types) { + model.types = () => options.types! + + // assignment type key for this entity + if (options?.typeKey) { + model.typeKey = options.typeKey + } + } + } +} + +export default DecoratedModel diff --git a/src/decorators/Field.ts b/src/decorators/Field.ts index c86b4a3..81cb6ec 100644 --- a/src/decorators/Field.ts +++ b/src/decorators/Field.ts @@ -1,4 +1,4 @@ -import { Model, Attribute } from '@vuex-orm/core' +import { Attribute, Model } from '@vuex-orm/core' import PropertyDecorator from '../contracts/PropertyDecorator' type Callback = (model: typeof Model) => Attribute @@ -6,7 +6,7 @@ type Callback = (model: typeof Model) => Attribute /** * Create a generic field decorator. */ -export default function Field (callback: Callback): PropertyDecorator { +export function Field (callback: Callback): PropertyDecorator { return (target: Model, propertyKey: string): void => { const model = target.constructor as typeof Model @@ -21,3 +21,5 @@ export default function Field (callback: Callback): PropertyDecorator { model.cachedFields[model.entity][propertyKey] = callback(model) } } + +export default Field diff --git a/src/decorators/HasMany.ts b/src/decorators/HasMany.ts new file mode 100644 index 0000000..6740f27 --- /dev/null +++ b/src/decorators/HasMany.ts @@ -0,0 +1,13 @@ +import PropertyDecorator from '@/contracts/PropertyDecorator' +import { Model } from '@vuex-orm/core' +import { FunctorOrValue, unwrapFunctorOrValue } from '../utils' +import Field from './Field' + +export function HasMany ( + related: FunctorOrValue, foreignKey: FunctorOrValue, + localKey?: FunctorOrValue +): PropertyDecorator { + return Field(model => (model.hasMany as any)(...[related, foreignKey, localKey].map(unwrapFunctorOrValue))) +} + +export default HasMany diff --git a/src/decorators/HasManyBy.ts b/src/decorators/HasManyBy.ts new file mode 100644 index 0000000..638aec3 --- /dev/null +++ b/src/decorators/HasManyBy.ts @@ -0,0 +1,13 @@ +import PropertyDecorator from '@/contracts/PropertyDecorator' +import { Model } from '@vuex-orm/core' +import { FunctorOrValue, unwrapFunctorOrValue } from '../utils' +import Field from './Field' + +export function HasManyBy ( + parent: FunctorOrValue, foreignKey: FunctorOrValue, + ownerKey?: FunctorOrValue +): PropertyDecorator { + return Field(model => (model.hasManyBy as any)(...[parent, foreignKey, ownerKey].map(unwrapFunctorOrValue))) +} + +export default HasManyBy diff --git a/src/decorators/HasManyThrough.ts b/src/decorators/HasManyThrough.ts new file mode 100644 index 0000000..185c745 --- /dev/null +++ b/src/decorators/HasManyThrough.ts @@ -0,0 +1,15 @@ +import PropertyDecorator from '@/contracts/PropertyDecorator' +import { Model } from '@vuex-orm/core' +import { FunctorOrValue, unwrapFunctorOrValue } from '../utils' +import Field from './Field' + +export function HasManyThrough ( + related: FunctorOrValue, + through: FunctorOrValue, + firstKey: FunctorOrValue, secondKey: FunctorOrValue, + localKey?: FunctorOrValue, secondLocalKey?: FunctorOrValue +): PropertyDecorator { + return Field(model => (model.hasManyThrough as any)(...[related, through, firstKey, secondKey, localKey, secondLocalKey].map(unwrapFunctorOrValue))) +} + +export default HasManyThrough diff --git a/src/decorators/HasOne.ts b/src/decorators/HasOne.ts new file mode 100644 index 0000000..5714e53 --- /dev/null +++ b/src/decorators/HasOne.ts @@ -0,0 +1,13 @@ +import PropertyDecorator from '@/contracts/PropertyDecorator' +import { Model } from '@vuex-orm/core' +import { FunctorOrValue, unwrapFunctorOrValue } from '../utils' +import Field from './Field' + +export function HasOne ( + related: FunctorOrValue, foreignKey: FunctorOrValue, + localKey?: FunctorOrValue +): PropertyDecorator { + return Field(model => (model.hasOne as any)(...[related, foreignKey, localKey].map(unwrapFunctorOrValue))) +} + +export default HasOne diff --git a/src/decorators/Increment.ts b/src/decorators/Increment.ts new file mode 100644 index 0000000..1737b08 --- /dev/null +++ b/src/decorators/Increment.ts @@ -0,0 +1,8 @@ +import PropertyDecorator from '../contracts/PropertyDecorator' +import Field from './Field' + +export function Increment (): PropertyDecorator { + return Field(model => model.increment()) +} + +export default Increment diff --git a/src/decorators/MorphMany.ts b/src/decorators/MorphMany.ts new file mode 100644 index 0000000..0e4e9cd --- /dev/null +++ b/src/decorators/MorphMany.ts @@ -0,0 +1,13 @@ +import PropertyDecorator from '@/contracts/PropertyDecorator' +import { Model } from '@vuex-orm/core' +import { FunctorOrValue, unwrapFunctorOrValue } from '../utils' +import Field from './Field' + +export function MorphMany ( + related: FunctorOrValue, + id: FunctorOrValue, type: FunctorOrValue, localKey?: FunctorOrValue +): PropertyDecorator { + return Field(model => (model.morphMany as any)(...[related, id, type, localKey].map(unwrapFunctorOrValue))) +} + +export default MorphMany diff --git a/src/decorators/MorphOne.ts b/src/decorators/MorphOne.ts new file mode 100644 index 0000000..5677a31 --- /dev/null +++ b/src/decorators/MorphOne.ts @@ -0,0 +1,13 @@ +import PropertyDecorator from '@/contracts/PropertyDecorator' +import { Model } from '@vuex-orm/core' +import { FunctorOrValue, unwrapFunctorOrValue } from '../utils' +import Field from './Field' + +export function MorphOne ( + related: FunctorOrValue, + id: FunctorOrValue, type: FunctorOrValue, localKey?: FunctorOrValue +): PropertyDecorator { + return Field(model => (model.morphOne as any)(...[related, id, type, localKey].map(unwrapFunctorOrValue))) +} + +export default MorphOne diff --git a/src/decorators/MorphTo.ts b/src/decorators/MorphTo.ts new file mode 100644 index 0000000..25bbb6c --- /dev/null +++ b/src/decorators/MorphTo.ts @@ -0,0 +1,11 @@ +import PropertyDecorator from '@/contracts/PropertyDecorator' +import { FunctorOrValue, unwrapFunctorOrValue } from '../utils' +import Field from './Field' + +export function MorphTo ( + id: FunctorOrValue, type: FunctorOrValue +): PropertyDecorator { + return Field(model => (model.morphTo as any)(...[id, type].map(unwrapFunctorOrValue))) +} + +export default MorphTo diff --git a/src/decorators/MorphToMany.ts b/src/decorators/MorphToMany.ts new file mode 100644 index 0000000..839f175 --- /dev/null +++ b/src/decorators/MorphToMany.ts @@ -0,0 +1,15 @@ +import PropertyDecorator from '@/contracts/PropertyDecorator' +import { Model } from '@vuex-orm/core' +import { FunctorOrValue, unwrapFunctorOrValue } from '../utils' +import Field from './Field' + +export function MorphToMany ( + related: FunctorOrValue, + pivot: FunctorOrValue, + relatedId: FunctorOrValue, id: FunctorOrValue, type: FunctorOrValue, + parentKey?: FunctorOrValue, relatedKey?: FunctorOrValue +): PropertyDecorator { + return Field(model => (model.morphToMany as any)(...[related, pivot, relatedId, id, type, parentKey, relatedKey].map(unwrapFunctorOrValue))) +} + +export default MorphToMany diff --git a/src/decorators/MorphedByMany.ts b/src/decorators/MorphedByMany.ts new file mode 100644 index 0000000..fbcce54 --- /dev/null +++ b/src/decorators/MorphedByMany.ts @@ -0,0 +1,15 @@ +import PropertyDecorator from '@/contracts/PropertyDecorator' +import { Model } from '@vuex-orm/core' +import { FunctorOrValue, unwrapFunctorOrValue } from '../utils' +import Field from './Field' + +export function MorphedByMany ( + related: FunctorOrValue, + pivot: FunctorOrValue, + relatedId: FunctorOrValue, id: FunctorOrValue, type: FunctorOrValue, + parentKey?: FunctorOrValue, relatedKey?: FunctorOrValue +): PropertyDecorator { + return Field(model => (model.morphedByMany as any)(...[related, pivot, relatedId, id, type, parentKey, relatedKey].map(unwrapFunctorOrValue))) +} + +export default MorphedByMany diff --git a/src/decorators/Num.ts b/src/decorators/Num.ts index d42f93b..f929eae 100644 --- a/src/decorators/Num.ts +++ b/src/decorators/Num.ts @@ -1,10 +1,13 @@ import PropertyDecorator from '../contracts/PropertyDecorator' import { TypeOptions } from '../options/Options' +import { FunctorOrValue, unwrapFunctorOrValue } from '../utils' import Primitive from './Primitive' /** * Create a num decorator. */ -export default function Num (value: number | null, options?: TypeOptions): PropertyDecorator { - return Primitive(model => model.number(value), options) +export function Num (value?: FunctorOrValue, options?: TypeOptions): PropertyDecorator { + return Primitive(model => model.number(unwrapFunctorOrValue(value)), options) } + +export default Num diff --git a/src/decorators/PrimaryKey.ts b/src/decorators/PrimaryKey.ts new file mode 100644 index 0000000..6a1e70d --- /dev/null +++ b/src/decorators/PrimaryKey.ts @@ -0,0 +1,12 @@ +import PropertyDecorator from '@/contracts/PropertyDecorator' +import { Model } from '@vuex-orm/core' + +export function PrimaryKey (): PropertyDecorator { + return (target: Model, propertyKey: string): void => { + const model = target.constructor as typeof Model + + model.primaryKey = propertyKey + } +} + +export default PrimaryKey diff --git a/src/decorators/Primitive.ts b/src/decorators/Primitive.ts index a5a05ad..c04b1b9 100644 --- a/src/decorators/Primitive.ts +++ b/src/decorators/Primitive.ts @@ -8,20 +8,24 @@ type Callback = (model: typeof Model) => Type /** * Create a generic type decorator. */ -export default function Primitive (callback: Callback, options?: TypeOptions): PropertyDecorator { +export function Primitive (callback: Callback, options?: TypeOptions): PropertyDecorator { return Field((model) => { const type = callback(model) - if (type.value === null && !options?.nullable) { + if (!(type.value || options?.nullable)) { throw new Error( - "[Vuex ORM] You've defined the default value of a field as `null` " + + '[Vuex ORM] You\'ve defined the default value of a field as `null` ' + 'without enabling `nullable` option. If you want the field to accept' + '`null`, set `nullable` option to `true`.' ) } - options?.nullable && type.nullable() + if (options?.nullable) { + type.nullable() + } return type }) } + +export default Primitive diff --git a/src/decorators/Str.ts b/src/decorators/Str.ts index fcc2d88..a015b82 100644 --- a/src/decorators/Str.ts +++ b/src/decorators/Str.ts @@ -1,10 +1,13 @@ import PropertyDecorator from '../contracts/PropertyDecorator' import { TypeOptions } from '../options/Options' +import { FunctorOrValue, unwrapFunctorOrValue } from '../utils' import Primitive from './Primitive' /** * Create a str decorator. */ -export default function Str (value: string | null, options?: TypeOptions): PropertyDecorator { - return Primitive(model => model.string(value), options) +export function Str (value?: FunctorOrValue, options?: TypeOptions): PropertyDecorator { + return Primitive(model => model.string(unwrapFunctorOrValue(value)), options) } + +export default Str diff --git a/src/decorators/index.ts b/src/decorators/index.ts new file mode 100644 index 0000000..400ced3 --- /dev/null +++ b/src/decorators/index.ts @@ -0,0 +1,20 @@ +export { Attribute } from './Attribute' +export { BelongsTo } from './BelongsTo' +export { BelongsToMany } from './BelongsToMany' +export { Bool } from './Bool' +export { DecoratedModel } from './DecoratedModel' +export { Field } from './Field' +export { HasMany } from './HasMany' +export { HasManyBy } from './HasManyBy' +export { HasManyThrough } from './HasManyThrough' +export { HasOne } from './HasOne' +export { Increment } from './Increment' +export { MorphedByMany } from './MorphedByMany' +export { MorphMany } from './MorphMany' +export { MorphOne } from './MorphOne' +export { MorphTo } from './MorphTo' +export { MorphToMany } from './MorphToMany' +export { Num } from './Num' +export { PrimaryKey } from './PrimaryKey' +export { Primitive } from './Primitive' +export { Str } from './Str' diff --git a/src/index.cjs.ts b/src/index.cjs.ts index e8fba20..5202f38 100644 --- a/src/index.cjs.ts +++ b/src/index.cjs.ts @@ -1,9 +1,27 @@ -import Attribute from './decorators/Attribute' -import Str from './decorators/Str' -import Bool from './decorators/Bool' +import { + Attribute, BelongsTo, BelongsToMany, Bool, DecoratedModel, Field, HasMany, HasManyBy, HasManyThrough, HasOne, Increment, MorphedByMany, MorphMany, MorphOne, MorphTo, + MorphToMany, Num, PrimaryKey, Primitive, Str +} from './decorators' export default { Attribute, - Str, - Bool + BelongsTo, + BelongsToMany, + Bool, + DecoratedModel, + Field, + HasMany, + HasManyBy, + HasManyThrough, + HasOne, + Increment, + MorphedByMany, + MorphMany, + MorphOne, + MorphTo, + MorphToMany, + Num, + PrimaryKey, + Primitive, + Str } diff --git a/src/index.ts b/src/index.ts index a22294b..d59f282 100644 --- a/src/index.ts +++ b/src/index.ts @@ -1,15 +1,50 @@ -import Attribute from './decorators/Attribute' -import Str from './decorators/Str' -import Bool from './decorators/Bool' +import { + Attribute, BelongsTo, BelongsToMany, Bool, DecoratedModel, Field, HasMany, HasManyBy, HasManyThrough, HasOne, Increment, MorphedByMany, MorphMany, MorphOne, MorphTo, + MorphToMany, Num, PrimaryKey, Primitive, Str +} from './decorators' export { Attribute, - Str, - Bool + BelongsTo, + BelongsToMany, + Bool, + DecoratedModel, + Field, + HasMany, + HasManyBy, + HasManyThrough, + HasOne, + Increment, + MorphedByMany, + MorphMany, + MorphOne, + MorphTo, + MorphToMany, + Num, + PrimaryKey, + Primitive, + Str } export default { Attribute, - Str, - Bool + BelongsTo, + BelongsToMany, + Bool, + Field, + DecoratedModel, + HasMany, + HasManyBy, + HasManyThrough, + HasOne, + Increment, + MorphedByMany, + MorphMany, + MorphOne, + MorphTo, + MorphToMany, + Num, + PrimaryKey, + Primitive, + Str } diff --git a/src/utils.ts b/src/utils.ts new file mode 100644 index 0000000..556d4fa --- /dev/null +++ b/src/utils.ts @@ -0,0 +1,4 @@ +export type FunctorOrValue = (() => T) | T + +export const unwrapFunctorOrValue = (functorOrValue: FunctorOrValue): T => + ((functorOrValue instanceof Function && functorOrValue()) || functorOrValue) as T From 5742c06c6fb86665fd27ed2d8dde78a90c095568 Mon Sep 17 00:00:00 2001 From: Steve Fan <29133953+stevefan1999-personal@users.noreply.github.com> Date: Thu, 28 Nov 2019 03:20:47 +0800 Subject: [PATCH 2/5] Add testdeck --- package.json | 1 + 1 file changed, 1 insertion(+) diff --git a/package.json b/package.json index b4f6641..dafe925 100644 --- a/package.json +++ b/package.json @@ -38,6 +38,7 @@ "@vuex-orm/core": "^0.33.0" }, "devDependencies": { + "@testdeck/jest": "0.0.6", "@types/jest": "^24.0.23", "codecov": "^3.5.0", "jest": "^24.8.0", From 7e831847a6aeeae03446426127eb29ada5e84d6f Mon Sep 17 00:00:00 2001 From: Steve Fan <29133953+stevefan1999-personal@users.noreply.github.com> Date: Thu, 28 Nov 2019 03:21:03 +0800 Subject: [PATCH 3/5] Add more tests --- src/decorators/Bool.ts | 2 +- src/decorators/DecoratedModel.ts | 14 +++++++------- src/decorators/Field.ts | 4 ++-- src/decorators/Num.ts | 2 +- src/decorators/PrimaryKey.ts | 11 ++++++++++- src/decorators/Primitive.ts | 17 ++++++++++------- src/decorators/Str.ts | 2 +- test/unit/Attribute.spec.ts | 20 +++++++++++++------- test/unit/Bool.spec.ts | 22 ++++++++++++++-------- test/unit/Num.spec.ts | 22 ++++++++++++++-------- test/unit/Primitive.spec.ts | 14 +++++++++----- test/unit/Str.spec.ts | 24 +++++++++++++++--------- test/unit/cyclic/Cyclic.spec.ts | 14 ++++++++++++++ test/unit/cyclic/fixtures/EntityA.ts | 16 ++++++++++++++++ test/unit/cyclic/fixtures/EntityB.ts | 19 +++++++++++++++++++ test/unit/cyclic/fixtures/EntityC.ts | 16 ++++++++++++++++ test/unit/cyclic/fixtures/index.ts | 3 +++ 17 files changed, 165 insertions(+), 57 deletions(-) create mode 100644 test/unit/cyclic/Cyclic.spec.ts create mode 100644 test/unit/cyclic/fixtures/EntityA.ts create mode 100644 test/unit/cyclic/fixtures/EntityB.ts create mode 100644 test/unit/cyclic/fixtures/EntityC.ts create mode 100644 test/unit/cyclic/fixtures/index.ts diff --git a/src/decorators/Bool.ts b/src/decorators/Bool.ts index 1c93861..f755229 100644 --- a/src/decorators/Bool.ts +++ b/src/decorators/Bool.ts @@ -6,7 +6,7 @@ import Primitive from './Primitive' /** * Create a bool decorator. */ -export function Bool (value?: FunctorOrValue, options?: TypeOptions): PropertyDecorator { +export function Bool (value?: FunctorOrValue | null, options?: TypeOptions): PropertyDecorator { return Primitive(model => model.boolean(unwrapFunctorOrValue(value)), options) } diff --git a/src/decorators/DecoratedModel.ts b/src/decorators/DecoratedModel.ts index 98cf0bb..a259dfa 100644 --- a/src/decorators/DecoratedModel.ts +++ b/src/decorators/DecoratedModel.ts @@ -1,20 +1,18 @@ import { Model } from '@vuex-orm/core' import InheritanceTypes from '@vuex-orm/core/lib/model/contracts/InheritanceTypes' -import PropertyDecorator from '../contracts/PropertyDecorator' -/** - * Create a generic field decorator. - */ +// Creates an optional class decorator to be used as means to add static fields export function DecoratedModel ( entityName: string, - options: { + options?: { parentEntity?: string, types?: InheritanceTypes, typeKey?: string } -): PropertyDecorator { - return (target: Model, _: string): void => { +): (target: any) => any | void { + return (target: any): any | void => { const model = target.constructor as typeof Model + // Do this temporarily until upstream vuex-orm declare this field officially // I want to use this to notify hot reloader in the future ;(model as any).isDecorated = true @@ -35,6 +33,8 @@ export function DecoratedModel ( model.typeKey = options.typeKey } } + + return target } } diff --git a/src/decorators/Field.ts b/src/decorators/Field.ts index 81cb6ec..6a3659f 100644 --- a/src/decorators/Field.ts +++ b/src/decorators/Field.ts @@ -1,7 +1,7 @@ import { Attribute, Model } from '@vuex-orm/core' import PropertyDecorator from '../contracts/PropertyDecorator' -type Callback = (model: typeof Model) => Attribute +type Callback = (model: typeof Model, propertyKey: string) => Attribute /** * Create a generic field decorator. @@ -18,7 +18,7 @@ export function Field (callback: Callback): PropertyDecorator { model.cachedFields[model.entity] = {} } - model.cachedFields[model.entity][propertyKey] = callback(model) + model.cachedFields[model.entity][propertyKey] = callback(model, propertyKey) } } diff --git a/src/decorators/Num.ts b/src/decorators/Num.ts index f929eae..340baab 100644 --- a/src/decorators/Num.ts +++ b/src/decorators/Num.ts @@ -6,7 +6,7 @@ import Primitive from './Primitive' /** * Create a num decorator. */ -export function Num (value?: FunctorOrValue, options?: TypeOptions): PropertyDecorator { +export function Num (value?: FunctorOrValue | null, options?: TypeOptions): PropertyDecorator { return Primitive(model => model.number(unwrapFunctorOrValue(value)), options) } diff --git a/src/decorators/PrimaryKey.ts b/src/decorators/PrimaryKey.ts index 6a1e70d..619b72f 100644 --- a/src/decorators/PrimaryKey.ts +++ b/src/decorators/PrimaryKey.ts @@ -5,7 +5,16 @@ export function PrimaryKey (): PropertyDecorator { return (target: Model, propertyKey: string): void => { const model = target.constructor as typeof Model - model.primaryKey = propertyKey + // making the primary key values an array + // even if you have one and only one value there's no effect in functionality + if (!model.primaryKey) { + model.primaryKey = [] + } else if (typeof model.primaryKey === 'string') { + const oldPrimaryKey = model.primaryKey + model.primaryKey = [oldPrimaryKey] + } + + model.primaryKey.push(propertyKey) } } diff --git a/src/decorators/Primitive.ts b/src/decorators/Primitive.ts index c04b1b9..7236778 100644 --- a/src/decorators/Primitive.ts +++ b/src/decorators/Primitive.ts @@ -9,15 +9,18 @@ type Callback = (model: typeof Model) => Type * Create a generic type decorator. */ export function Primitive (callback: Callback, options?: TypeOptions): PropertyDecorator { - return Field((model) => { + return Field((model, propertyKey) => { const type = callback(model) - if (!(type.value || options?.nullable)) { - throw new Error( - '[Vuex ORM] You\'ve defined the default value of a field as `null` ' + - 'without enabling `nullable` option. If you want the field to accept' + - '`null`, set `nullable` option to `true`.' - ) + if ( + typeof type?.value !== 'number' // if that number is 0, what will happen?! always remember it was coerced to false + && !(type?.value || options?.nullable) + ) { + throw new Error(` +[Vuex ORM] You've defined the default value of a field as \`null\` without enabling \`nullable\` option. +If you want the field to accept\`null\`, set \`nullable\` option to \`true\`. +Problematic class name: ${model.name}; Related property key: ${propertyKey} + `) } if (options?.nullable) { diff --git a/src/decorators/Str.ts b/src/decorators/Str.ts index a015b82..f7643bc 100644 --- a/src/decorators/Str.ts +++ b/src/decorators/Str.ts @@ -6,7 +6,7 @@ import Primitive from './Primitive' /** * Create a str decorator. */ -export function Str (value?: FunctorOrValue, options?: TypeOptions): PropertyDecorator { +export function Str (value?: FunctorOrValue | null, options?: TypeOptions): PropertyDecorator { return Primitive(model => model.string(unwrapFunctorOrValue(value)), options) } diff --git a/test/unit/Attribute.spec.ts b/test/unit/Attribute.spec.ts index 419fb47..45eb7f6 100644 --- a/test/unit/Attribute.spec.ts +++ b/test/unit/Attribute.spec.ts @@ -1,8 +1,12 @@ import { Model, Attr } from '@vuex-orm/core' -import Attribute from '@/decorators/Attribute' +import { Attribute, DecoratedModel } from '@/decorators' +import { suite, test } from '@testdeck/jest' -describe('Attribute', () => { - it('can define `attr` field', () => { +@suite +export class AttributeSpec { + @test + 'can define `attr` field' () { + @DecoratedModel('users') class User extends Model { @Attribute() id!: number @@ -10,9 +14,11 @@ describe('Attribute', () => { expect(User.getFields().id).toBeInstanceOf(Attr) expect((new User()).id).toBe(null) - }) + } - it('can define `attr` field with default value', () => { + @test + 'can define `attr` field with default value' () { + @DecoratedModel('users') class User extends Model { @Attribute(1) id!: number @@ -20,5 +26,5 @@ describe('Attribute', () => { expect(User.getFields().id).toBeInstanceOf(Attr) expect((new User()).id).toBe(1) - }) -}) + } +} diff --git a/test/unit/Bool.spec.ts b/test/unit/Bool.spec.ts index 2080377..4bc935a 100644 --- a/test/unit/Bool.spec.ts +++ b/test/unit/Bool.spec.ts @@ -1,8 +1,12 @@ import { Model, Boolean } from '@vuex-orm/core' -import Bool from '@/decorators/Bool' +import { DecoratedModel, Bool } from '@/decorators' +import { suite, test } from '@testdeck/jest' -describe('Bool', () => { - it('can define `bool` field', () => { +@suite +export class BoolSpec { + @test + 'can define `bool` field' () { + @DecoratedModel('users') class User extends Model { @Bool(true) active!: boolean @@ -10,15 +14,17 @@ describe('Bool', () => { expect(User.getFields().active).toBeInstanceOf(Boolean) expect((new User()).active).toBe(true) - }) + } - it('can define `bool` field as nullable', () => { + @test + 'can define `bool` field as nullable' () { + @DecoratedModel('users') class User extends Model { @Bool(null, { nullable: true }) - active!: boolean + active?: boolean } expect(User.getFields().active).toBeInstanceOf(Boolean) expect((new User()).active).toBe(null) - }) -}) + } +} diff --git a/test/unit/Num.spec.ts b/test/unit/Num.spec.ts index 85a8747..681a709 100644 --- a/test/unit/Num.spec.ts +++ b/test/unit/Num.spec.ts @@ -1,8 +1,12 @@ import { Model, Number } from '@vuex-orm/core' -import Num from '@/decorators/Num' +import { Num, DecoratedModel } from '@/decorators' +import { suite, test } from '@testdeck/jest' -describe('Num', () => { - it('can define `number` field', () => { +@suite +export class NumSpec { + @test + 'can define `number` field' () { + @DecoratedModel('users') class User extends Model { @Num(34) age!: number @@ -10,15 +14,17 @@ describe('Num', () => { expect(User.getFields().age).toBeInstanceOf(Number) expect((new User()).age).toBe(34) - }) + } - it('can define `number` as nullable', () => { + @test + 'can define `number` as nullable' () { + @DecoratedModel('users') class User extends Model { @Num(null, { nullable: true }) - age!: number + age?: number } expect(User.getFields().age).toBeInstanceOf(Number) expect((new User()).age).toBe(null) - }) -}) + } +} diff --git a/test/unit/Primitive.spec.ts b/test/unit/Primitive.spec.ts index d074820..2ce6182 100644 --- a/test/unit/Primitive.spec.ts +++ b/test/unit/Primitive.spec.ts @@ -1,9 +1,13 @@ import { Model } from '@vuex-orm/core' -import Str from '@/decorators/Str' +import { Str, DecoratedModel } from '@/decorators' +import { suite, test } from '@testdeck/jest' -describe('Primitive', () => { - it('throws if default value is `null` without `nullable` option is set to `true`', () => { +@suite +export class PrimitiveSpec { + @test + 'throws if default value is `null` without `nullable` option is set to `true`' () { expect(() => { + @DecoratedModel('users') class User extends Model { @Str(null) name!: string @@ -11,5 +15,5 @@ describe('Primitive', () => { console.log(User) }).toThrow() - }) -}) + } +} diff --git a/test/unit/Str.spec.ts b/test/unit/Str.spec.ts index c0af789..14dfd52 100644 --- a/test/unit/Str.spec.ts +++ b/test/unit/Str.spec.ts @@ -1,8 +1,12 @@ +import { DecoratedModel, Str } from '@/decorators' import { Model, String } from '@vuex-orm/core' -import Str from '@/decorators/Str' +import { suite, test } from '@testdeck/jest' -describe('Str', () => { - it('can define `string` field', () => { +@suite +export class StrSpec { + @test + 'can define `string` field' () { + @DecoratedModel('users') class User extends Model { @Str('John Doe') name!: string @@ -14,19 +18,21 @@ describe('Str', () => { expect(User.getFields().name).toBeInstanceOf(String) expect((new User()).name).toBe('John Doe') expect((new User()).email).toBe('john.doe@example.com') - }) + } - it('can define `string` as nullable', () => { + @test + 'can define `string` as nullable' () { + @DecoratedModel('users') class User extends Model { @Str(null, { nullable: true }) - name!: string + name?: string @Str(null, { nullable: true }) - email!: string + email?: string } expect(User.getFields().name).toBeInstanceOf(String) expect((new User()).name).toBe(null) expect((new User()).email).toBe(null) - }) -}) + } +} diff --git a/test/unit/cyclic/Cyclic.spec.ts b/test/unit/cyclic/Cyclic.spec.ts new file mode 100644 index 0000000..097ddde --- /dev/null +++ b/test/unit/cyclic/Cyclic.spec.ts @@ -0,0 +1,14 @@ +import { suite, test } from '@testdeck/jest' +import { BelongsTo, HasMany } from '@vuex-orm/core' +import { EntityA, EntityB, EntityC } from './fixtures' + +@suite +export class CyclicSpec { + @test + 'should run perfectly' () { + expect(EntityA.getFields().manyB).toBeInstanceOf(HasMany) + expect(EntityB.getFields().oneA).toBeInstanceOf(BelongsTo) + expect(EntityB.getFields().cS).toBeInstanceOf(HasMany) + expect(EntityC.getFields().bS).toBeInstanceOf(HasMany) + } +} diff --git a/test/unit/cyclic/fixtures/EntityA.ts b/test/unit/cyclic/fixtures/EntityA.ts new file mode 100644 index 0000000..49f1b1f --- /dev/null +++ b/test/unit/cyclic/fixtures/EntityA.ts @@ -0,0 +1,16 @@ +import { DecoratedModel, HasMany, Num, PrimaryKey, Str } from '@/decorators' +import { Model } from '@vuex-orm/core' +import * as Entities from './' + +@DecoratedModel('A') +export class EntityA extends Model { + @PrimaryKey() + @Num(0) + public id!: number + + @Str('foo', { nullable: false }) + public foo!: string + + @HasMany(() => Entities.EntityB, 'id') + public manyB!: Entities.EntityB[] +} diff --git a/test/unit/cyclic/fixtures/EntityB.ts b/test/unit/cyclic/fixtures/EntityB.ts new file mode 100644 index 0000000..5945760 --- /dev/null +++ b/test/unit/cyclic/fixtures/EntityB.ts @@ -0,0 +1,19 @@ +import { BelongsTo, DecoratedModel, HasMany, Num, PrimaryKey, Str } from '@/decorators' +import { Model } from '@vuex-orm/core' +import * as Entities from './index' + +@DecoratedModel('A') +export class EntityB extends Model { + @PrimaryKey() + @Num(0) + public id!: number + + @Str(null, { nullable: true }) + public bar?: string + + @BelongsTo(() => Entities.EntityA, 'id') + public oneA!: Entities.EntityA + + @HasMany(() => Entities.EntityC, 'id', 'id') + public cS!: Entities.EntityC[] +} diff --git a/test/unit/cyclic/fixtures/EntityC.ts b/test/unit/cyclic/fixtures/EntityC.ts new file mode 100644 index 0000000..f65aaae --- /dev/null +++ b/test/unit/cyclic/fixtures/EntityC.ts @@ -0,0 +1,16 @@ +import { DecoratedModel, HasMany, Num, PrimaryKey, Str } from '@/decorators' +import { Model } from '@vuex-orm/core' +import * as Entities from './index' + +@DecoratedModel('A') +export class EntityC extends Model { + @PrimaryKey() + @Num(0) + public id!: number + + @Str('baz') + public baz!: string + + @HasMany(() => Entities.EntityB, 'id', 'id') + public bS!: Entities.EntityB[] +} diff --git a/test/unit/cyclic/fixtures/index.ts b/test/unit/cyclic/fixtures/index.ts new file mode 100644 index 0000000..4d333e9 --- /dev/null +++ b/test/unit/cyclic/fixtures/index.ts @@ -0,0 +1,3 @@ +export { EntityA } from './EntityA' +export { EntityB } from './EntityB' +export { EntityC } from './EntityC' From 7c4a65f4c287eb2d2249b8131fe037a6557d3766 Mon Sep 17 00:00:00 2001 From: Steve Fan <29133953+stevefan1999-personal@users.noreply.github.com> Date: Mon, 23 Dec 2019 19:37:44 +0800 Subject: [PATCH 4/5] removing testdeck :( --- package.json | 1 - test/unit/Attribute.spec.ts | 23 ++++------------------- test/unit/Bool.spec.ts | 16 ++++++---------- test/unit/Num.spec.ts | 16 ++++++---------- test/unit/Primitive.spec.ts | 11 ++++------- test/unit/Str.spec.ts | 16 ++++++---------- test/unit/cyclic/Cyclic.spec.ts | 11 ++++------- 7 files changed, 30 insertions(+), 64 deletions(-) diff --git a/package.json b/package.json index dafe925..b4f6641 100644 --- a/package.json +++ b/package.json @@ -38,7 +38,6 @@ "@vuex-orm/core": "^0.33.0" }, "devDependencies": { - "@testdeck/jest": "0.0.6", "@types/jest": "^24.0.23", "codecov": "^3.5.0", "jest": "^24.8.0", diff --git a/test/unit/Attribute.spec.ts b/test/unit/Attribute.spec.ts index 45eb7f6..a3c9d82 100644 --- a/test/unit/Attribute.spec.ts +++ b/test/unit/Attribute.spec.ts @@ -1,11 +1,8 @@ import { Model, Attr } from '@vuex-orm/core' import { Attribute, DecoratedModel } from '@/decorators' -import { suite, test } from '@testdeck/jest' -@suite -export class AttributeSpec { - @test - 'can define `attr` field' () { +describe('attribute', () => { + it('can define `attr` field', function () { @DecoratedModel('users') class User extends Model { @Attribute() @@ -14,17 +11,5 @@ export class AttributeSpec { expect(User.getFields().id).toBeInstanceOf(Attr) expect((new User()).id).toBe(null) - } - - @test - 'can define `attr` field with default value' () { - @DecoratedModel('users') - class User extends Model { - @Attribute(1) - id!: number - } - - expect(User.getFields().id).toBeInstanceOf(Attr) - expect((new User()).id).toBe(1) - } -} + }) +}) diff --git a/test/unit/Bool.spec.ts b/test/unit/Bool.spec.ts index 4bc935a..129c53c 100644 --- a/test/unit/Bool.spec.ts +++ b/test/unit/Bool.spec.ts @@ -1,11 +1,8 @@ import { Model, Boolean } from '@vuex-orm/core' import { DecoratedModel, Bool } from '@/decorators' -import { suite, test } from '@testdeck/jest' -@suite -export class BoolSpec { - @test - 'can define `bool` field' () { +describe('boolean', () => { + it('can define `bool` field', function () { @DecoratedModel('users') class User extends Model { @Bool(true) @@ -14,10 +11,9 @@ export class BoolSpec { expect(User.getFields().active).toBeInstanceOf(Boolean) expect((new User()).active).toBe(true) - } + }) - @test - 'can define `bool` field as nullable' () { + it('can define `bool` field as nullable', function () { @DecoratedModel('users') class User extends Model { @Bool(null, { nullable: true }) @@ -26,5 +22,5 @@ export class BoolSpec { expect(User.getFields().active).toBeInstanceOf(Boolean) expect((new User()).active).toBe(null) - } -} + }) +}) diff --git a/test/unit/Num.spec.ts b/test/unit/Num.spec.ts index 681a709..4fccbf2 100644 --- a/test/unit/Num.spec.ts +++ b/test/unit/Num.spec.ts @@ -1,11 +1,8 @@ import { Model, Number } from '@vuex-orm/core' import { Num, DecoratedModel } from '@/decorators' -import { suite, test } from '@testdeck/jest' -@suite -export class NumSpec { - @test - 'can define `number` field' () { +describe('number', () => { + it('can define `number` field', function () { @DecoratedModel('users') class User extends Model { @Num(34) @@ -14,10 +11,9 @@ export class NumSpec { expect(User.getFields().age).toBeInstanceOf(Number) expect((new User()).age).toBe(34) - } + }) - @test - 'can define `number` as nullable' () { + it('can define `number` as nullable', function () { @DecoratedModel('users') class User extends Model { @Num(null, { nullable: true }) @@ -26,5 +22,5 @@ export class NumSpec { expect(User.getFields().age).toBeInstanceOf(Number) expect((new User()).age).toBe(null) - } -} + }) +}) diff --git a/test/unit/Primitive.spec.ts b/test/unit/Primitive.spec.ts index 2ce6182..c0cb042 100644 --- a/test/unit/Primitive.spec.ts +++ b/test/unit/Primitive.spec.ts @@ -1,11 +1,8 @@ import { Model } from '@vuex-orm/core' import { Str, DecoratedModel } from '@/decorators' -import { suite, test } from '@testdeck/jest' -@suite -export class PrimitiveSpec { - @test - 'throws if default value is `null` without `nullable` option is set to `true`' () { +describe('primitive', () => { + it('throws if default value is `null` without `nullable` option is set to `true`', function () { expect(() => { @DecoratedModel('users') class User extends Model { @@ -15,5 +12,5 @@ export class PrimitiveSpec { console.log(User) }).toThrow() - } -} + }) +}) diff --git a/test/unit/Str.spec.ts b/test/unit/Str.spec.ts index 14dfd52..66944e8 100644 --- a/test/unit/Str.spec.ts +++ b/test/unit/Str.spec.ts @@ -1,11 +1,8 @@ import { DecoratedModel, Str } from '@/decorators' import { Model, String } from '@vuex-orm/core' -import { suite, test } from '@testdeck/jest' -@suite -export class StrSpec { - @test - 'can define `string` field' () { +describe('string', () => { + it('can define `string` field', function () { @DecoratedModel('users') class User extends Model { @Str('John Doe') @@ -18,10 +15,9 @@ export class StrSpec { expect(User.getFields().name).toBeInstanceOf(String) expect((new User()).name).toBe('John Doe') expect((new User()).email).toBe('john.doe@example.com') - } + }) - @test - 'can define `string` as nullable' () { + it('can define `string` as nullable', function () { @DecoratedModel('users') class User extends Model { @Str(null, { nullable: true }) @@ -34,5 +30,5 @@ export class StrSpec { expect(User.getFields().name).toBeInstanceOf(String) expect((new User()).name).toBe(null) expect((new User()).email).toBe(null) - } -} + }) +}) diff --git a/test/unit/cyclic/Cyclic.spec.ts b/test/unit/cyclic/Cyclic.spec.ts index 097ddde..da7c130 100644 --- a/test/unit/cyclic/Cyclic.spec.ts +++ b/test/unit/cyclic/Cyclic.spec.ts @@ -1,14 +1,11 @@ -import { suite, test } from '@testdeck/jest' import { BelongsTo, HasMany } from '@vuex-orm/core' import { EntityA, EntityB, EntityC } from './fixtures' -@suite -export class CyclicSpec { - @test - 'should run perfectly' () { +describe('cyclic', function () { + it('should run perfectly', function () { expect(EntityA.getFields().manyB).toBeInstanceOf(HasMany) expect(EntityB.getFields().oneA).toBeInstanceOf(BelongsTo) expect(EntityB.getFields().cS).toBeInstanceOf(HasMany) expect(EntityC.getFields().bS).toBeInstanceOf(HasMany) - } -} + }) +}) From 4b11226f154fa00cea03fca4c458a163bc717529 Mon Sep 17 00:00:00 2001 From: Steve Fan <29133953+stevefan1999-personal@users.noreply.github.com> Date: Mon, 23 Dec 2019 19:39:13 +0800 Subject: [PATCH 5/5] re add missing test --- test/unit/Attribute.spec.ts | 10 ++++++++++ 1 file changed, 10 insertions(+) diff --git a/test/unit/Attribute.spec.ts b/test/unit/Attribute.spec.ts index a3c9d82..6bbef07 100644 --- a/test/unit/Attribute.spec.ts +++ b/test/unit/Attribute.spec.ts @@ -12,4 +12,14 @@ describe('attribute', () => { expect(User.getFields().id).toBeInstanceOf(Attr) expect((new User()).id).toBe(null) }) + + it('can define `attr` field with default value', function () { + class User extends Model { + @Attribute(1) + id!: number + } + + expect(User.getFields().id).toBeInstanceOf(Attr) + expect((new User()).id).toBe(1) + }) })