diff --git a/dist/lib/model/EloquentAttribute.d.ts b/dist/lib/model/EloquentAttribute.d.ts new file mode 100644 index 0000000..4056c3e --- /dev/null +++ b/dist/lib/model/EloquentAttribute.d.ts @@ -0,0 +1,19 @@ +import { Eloquent } from './Eloquent'; +export declare class EloquentAttribute { + protected known: string[]; + protected dynamic: { + [key: string]: { + name: string; + getter: boolean; + setter: boolean; + accessor?: string; + mutator?: string; + }; + }; + constructor(model: Eloquent, prototype: any); + protected createDynamicAttributeIfNeeded(property: string): void; + isKnownAttribute(name: string | Symbol): boolean; + buildKnownAttributes(model: Eloquent, prototype: any): void; + findGettersAndSetters(prototype: any): void; + findAccessorsAndMutators(prototype: any): void; +} diff --git a/dist/lib/model/EloquentAttribute.js b/dist/lib/model/EloquentAttribute.js new file mode 100644 index 0000000..ce1c413 --- /dev/null +++ b/dist/lib/model/EloquentAttribute.js @@ -0,0 +1,67 @@ +"use strict"; +Object.defineProperty(exports, "__esModule", { value: true }); +const Eloquent_1 = require("./Eloquent"); +const lodash_1 = require("lodash"); +const EloquentProxy_1 = require("./EloquentProxy"); +class EloquentAttribute { + constructor(model, prototype) { + this.dynamic = {}; + this.known = []; + this.findGettersAndSetters(prototype); + this.findAccessorsAndMutators(prototype); + this.buildKnownAttributes(model, prototype); + } + createDynamicAttributeIfNeeded(property) { + if (!this.dynamic[property]) { + this.dynamic[property] = { + name: property, + getter: false, + setter: false + }; + } + } + isKnownAttribute(name) { + if (typeof name === 'symbol') { + return true; + } + return this.known.indexOf(name) !== -1; + } + buildKnownAttributes(model, prototype) { + this.known = Array.from(new Set(model['getReservedProperties']().concat(Object.getOwnPropertyNames(model), EloquentProxy_1.GET_FORWARD_TO_DRIVER_FUNCTIONS, EloquentProxy_1.GET_QUERY_FUNCTIONS, Object.getOwnPropertyNames(Eloquent_1.Eloquent.prototype), Object.getOwnPropertyNames(prototype)))); + } + findGettersAndSetters(prototype) { + const descriptors = Object.getOwnPropertyDescriptors(prototype); + for (const property in descriptors) { + const getter = lodash_1.isFunction(descriptors[property].get); + const setter = lodash_1.isFunction(descriptors[property].set); + if (!getter && !setter) { + continue; + } + this.createDynamicAttributeIfNeeded(property); + this.dynamic[property].getter = getter; + this.dynamic[property].setter = setter; + } + } + findAccessorsAndMutators(prototype) { + const names = Object.getOwnPropertyNames(prototype); + const regex = new RegExp('^(get|set)([a-zA-z0-9_\\-]{1,})Attribute$', 'g'); + names.forEach(name => { + let match; + while ((match = regex.exec(name)) != undefined) { + // javascript RegExp has a bug when the match has length 0 + // if (match.index === regex.lastIndex) { + // ++regex.lastIndex + // } + const property = lodash_1.snakeCase(match[2]); + this.createDynamicAttributeIfNeeded(property); + if (match[1] === 'get') { + this.dynamic[property].accessor = match[0]; + } + else { + this.dynamic[property].mutator = match[0]; + } + } + }); + } +} +exports.EloquentAttribute = EloquentAttribute; diff --git a/dist/lib/model/EloquentMetadata.d.ts b/dist/lib/model/EloquentMetadata.d.ts index 78807f6..99111e7 100644 --- a/dist/lib/model/EloquentMetadata.d.ts +++ b/dist/lib/model/EloquentMetadata.d.ts @@ -1,4 +1,5 @@ import { Eloquent } from './Eloquent'; +import { EloquentAttribute } from './EloquentAttribute'; export declare type EloquentTimestamps = { createdAt: string; updatedAt: string; @@ -7,20 +8,6 @@ export declare type EloquentSoftDelete = { deletedAt: string; overrideMethods: boolean | 'all' | string[]; }; -export declare type EloquentAccessors = { - [key: string]: { - name: string; - type: 'getter' | 'function' | string; - ref?: string; - }; -}; -export declare type EloquentMutators = { - [key: string]: { - name: string; - type: 'setter' | 'function' | string; - ref?: string; - }; -}; /** * This class contains all metadata parsing functions, such as: * - fillable @@ -36,15 +23,8 @@ export declare class EloquentMetadata { protected prototype: any; protected definition: typeof Eloquent; protected knownAttributes: string[]; - protected accessors: EloquentAccessors; - protected mutators: EloquentMutators; + protected attribute: EloquentAttribute; private constructor(); - protected buildKnownAttributes(): void; - /** - * Find accessors and mutators defined in getter/setter, only available for node >= 8.7 - */ - protected findGettersAndSetters(): void; - protected findAccessorsAndMutators(): void; getSettingProperty(property: string, defaultValue: T): T; hasSetting(property: string): boolean; getSettingWithDefaultForTrueValue(property: string, defaultValue: any): any; diff --git a/dist/lib/model/EloquentMetadata.js b/dist/lib/model/EloquentMetadata.js index 575cea3..b7cde00 100644 --- a/dist/lib/model/EloquentMetadata.js +++ b/dist/lib/model/EloquentMetadata.js @@ -1,9 +1,7 @@ "use strict"; Object.defineProperty(exports, "__esModule", { value: true }); const najs_binding_1 = require("najs-binding"); -const Eloquent_1 = require("./Eloquent"); -const EloquentProxy_1 = require("./EloquentProxy"); -const lodash_1 = require("lodash"); +const EloquentAttribute_1 = require("./EloquentAttribute"); const DEFAULT_TIMESTAMPS = { createdAt: 'created_at', updatedAt: 'updated_at' @@ -27,59 +25,7 @@ class EloquentMetadata { this.model = model; this.prototype = Object.getPrototypeOf(this.model); this.definition = Object.getPrototypeOf(model).constructor; - this.accessors = {}; - this.mutators = {}; - this.buildKnownAttributes(); - this.findGettersAndSetters(); - this.findAccessorsAndMutators(); - } - buildKnownAttributes() { - this.knownAttributes = Array.from(new Set(this.model['getReservedProperties']().concat(Object.getOwnPropertyNames(this.model), EloquentProxy_1.GET_FORWARD_TO_DRIVER_FUNCTIONS, EloquentProxy_1.GET_QUERY_FUNCTIONS, Object.getOwnPropertyNames(Eloquent_1.Eloquent.prototype), Object.getOwnPropertyNames(this.prototype)))); - } - /** - * Find accessors and mutators defined in getter/setter, only available for node >= 8.7 - */ - findGettersAndSetters() { - const descriptors = Object.getOwnPropertyDescriptors(this.prototype); - for (const name in descriptors) { - if (lodash_1.isFunction(descriptors[name].get)) { - this.accessors[name] = { - name: name, - type: 'getter' - }; - } - if (lodash_1.isFunction(descriptors[name].set)) { - this.mutators[name] = { - name: name, - type: 'setter' - }; - } - } - } - findAccessorsAndMutators() { - const names = Object.getOwnPropertyNames(this.prototype); - const regex = new RegExp('^(get|set)([a-zA-z0-9_\\-]{1,})Attribute$', 'g'); - names.forEach(name => { - let match; - while ((match = regex.exec(name)) != undefined) { - // javascript RegExp has a bug when the match has length 0 - // if (match.index === regex.lastIndex) { - // ++regex.lastIndex - // } - const property = lodash_1.snakeCase(match[2]); - const data = { - name: property, - type: 'function', - ref: match[0] - }; - if (match[1] === 'get' && typeof this.accessors[property] === 'undefined') { - this.accessors[property] = data; - } - if (match[1] === 'set' && typeof this.mutators[property] === 'undefined') { - this.mutators[property] = data; - } - } - }); + this.attribute = new EloquentAttribute_1.EloquentAttribute(model, this.prototype); } getSettingProperty(property, defaultValue) { if (this.definition[property]) { @@ -116,10 +62,7 @@ class EloquentMetadata { return this.getSettingWithDefaultForTrueValue('softDeletes', defaultValue); } hasAttribute(name) { - if (typeof name === 'symbol') { - return true; - } - return this.knownAttributes.indexOf(name) !== -1; + return this.attribute.isKnownAttribute(name); } static get(model, cache = true) { const className = model.getClassName(); diff --git a/dist/test/model/EloquentAttribute.test.d.ts b/dist/test/model/EloquentAttribute.test.d.ts new file mode 100644 index 0000000..d73a958 --- /dev/null +++ b/dist/test/model/EloquentAttribute.test.d.ts @@ -0,0 +1 @@ +import 'jest'; diff --git a/dist/test/model/EloquentAttribute.test.js b/dist/test/model/EloquentAttribute.test.js new file mode 100644 index 0000000..2992bc7 --- /dev/null +++ b/dist/test/model/EloquentAttribute.test.js @@ -0,0 +1,148 @@ +"use strict"; +Object.defineProperty(exports, "__esModule", { value: true }); +require("jest"); +const najs_binding_1 = require("najs-binding"); +const Eloquent_1 = require("../../lib/model/Eloquent"); +const EloquentAttribute_1 = require("../../lib/model/EloquentAttribute"); +const EloquentDriverProvider_1 = require("../../lib/drivers/EloquentDriverProvider"); +const DummyDriver_1 = require("../../lib/drivers/DummyDriver"); +const EloquentProxy_1 = require("../../lib/model/EloquentProxy"); +EloquentDriverProvider_1.EloquentDriverProvider.register(DummyDriver_1.DummyDriver, 'dummy'); +class Model extends Eloquent_1.Eloquent { + get accessor() { + return ''; + } + set mutator(value) { } + getClassName() { + return 'Model'; + } + modelMethod() { } +} +najs_binding_1.register(Model); +class ChildModel extends Model { + get child_accessor() { + return ''; + } + set child_mutator(value) { } + getClassName() { + return 'ChildModel'; + } + childModelMethod() { } +} +najs_binding_1.register(ChildModel); +const fakeModel = { + getReservedProperties() { + return []; + } +}; +describe('EloquentAttribute', function () { + describe('.findGettersAndSetters()', function () { + it('finds all defined getters and put to accessors with type = getter', function () { + class ClassEmpty { + } + const attribute = new EloquentAttribute_1.EloquentAttribute(fakeModel, {}); + attribute.findGettersAndSetters(Object.getPrototypeOf(new ClassEmpty())); + expect(attribute['dynamic']).toEqual({}); + class Class { + get a() { + return ''; + } + set a(value) { } + get b() { + return ''; + } + } + attribute.findGettersAndSetters(Object.getPrototypeOf(new Class())); + expect(attribute['dynamic']).toEqual({ + a: { name: 'a', getter: true, setter: true }, + b: { name: 'b', getter: true, setter: false } + }); + }); + }); + describe('.findAccessorsAndMutators()', function () { + it('finds all defined getters and put to accessors with type = getter', function () { + class ClassEmpty { + } + const attribute = new EloquentAttribute_1.EloquentAttribute(fakeModel, {}); + attribute.findAccessorsAndMutators(Object.getPrototypeOf(new ClassEmpty())); + expect(attribute['dynamic']).toEqual({}); + class Class { + get a() { + return ''; + } + getAAttribute() { } + getFirstNameAttribute() { } + getWrongFormat() { } + set b(value) { } + setBAttribute() { } + setWrongFormat() { } + setDoublegetDoubleAttribute() { } + get c() { + return ''; + } + set c(value) { } + getCAttribute() { } + setCAttribute() { } + } + attribute.findGettersAndSetters(Object.getPrototypeOf(new Class())); + attribute.findAccessorsAndMutators(Object.getPrototypeOf(new Class())); + expect(attribute['dynamic']).toEqual({ + a: { name: 'a', getter: true, setter: false, accessor: 'getAAttribute' }, + b: { name: 'b', getter: false, setter: true, mutator: 'setBAttribute' }, + c: { name: 'c', getter: true, setter: true, accessor: 'getCAttribute', mutator: 'setCAttribute' }, + first_name: { name: 'first_name', getter: false, setter: false, accessor: 'getFirstNameAttribute' }, + doubleget_double: { + name: 'doubleget_double', + getter: false, + setter: false, + mutator: 'setDoublegetDoubleAttribute' + } + }); + }); + }); + describe('protected .buildKnownAttributes()', function () { + const attribute = new EloquentAttribute_1.EloquentAttribute(fakeModel, {}); + attribute.buildKnownAttributes(new Model(), Model.prototype); + it('merges reserved properties defined in .getReservedProperties() of model and driver', function () { + const props = new Model()['getReservedProperties'](); + for (const name of props) { + expect(attribute['known'].indexOf(name) !== -1).toBe(true); + } + }); + it('merges properties defined Eloquent.prototype', function () { + const props = Object.getOwnPropertyNames(Model.prototype); + for (const name of props) { + expect(attribute['known'].indexOf(name) !== -1).toBe(true); + } + }); + it('merges properties defined in model', function () { + const props = ['accessor', 'mutator', 'modelMethod']; + for (const name of props) { + expect(attribute['known'].indexOf(name) !== -1).toBe(true); + } + // warning: props defined in model is not included in list + expect(attribute['known'].indexOf('props') === -1).toBe(true); + }); + it('merges properties defined in child model', function () { + const childAttribute = new EloquentAttribute_1.EloquentAttribute(new ChildModel(), ChildModel.prototype); + const props = ['child_accessor', 'child_mutator', 'childModelMethod']; + for (const name of props) { + expect(childAttribute['known'].indexOf(name) !== -1).toBe(true); + } + // warning: props defined in model is not included in list + expect(childAttribute['known'].indexOf('child_props') === -1).toBe(true); + }); + it('merges properties defined GET_FORWARD_TO_DRIVER_FUNCTIONS', function () { + const props = EloquentProxy_1.GET_FORWARD_TO_DRIVER_FUNCTIONS; + for (const name of props) { + expect(attribute['known'].indexOf(name) !== -1).toBe(true); + } + }); + it('merges properties defined GET_QUERY_FUNCTIONS', function () { + const props = EloquentProxy_1.GET_QUERY_FUNCTIONS; + for (const name of props) { + expect(attribute['known'].indexOf(name) !== -1).toBe(true); + } + }); + }); +}); diff --git a/dist/test/model/EloquentMetadata.test.js b/dist/test/model/EloquentMetadata.test.js index a6977b9..063258a 100644 --- a/dist/test/model/EloquentMetadata.test.js +++ b/dist/test/model/EloquentMetadata.test.js @@ -7,7 +7,7 @@ const DummyDriver_1 = require("../../lib/drivers/DummyDriver"); const EloquentDriverProvider_1 = require("../../lib/drivers/EloquentDriverProvider"); const Eloquent_1 = require("../../lib/model/Eloquent"); const EloquentMetadata_1 = require("../../lib/model/EloquentMetadata"); -const EloquentProxy_1 = require("../../lib/model/EloquentProxy"); +const EloquentAttribute_1 = require("../../lib/model/EloquentAttribute"); EloquentDriverProvider_1.EloquentDriverProvider.register(DummyDriver_1.DummyDriver, 'dummy'); class Model extends Eloquent_1.Eloquent { get accessor() { @@ -41,175 +41,9 @@ describe('EloquentMetadata', function () { const metadata = EloquentMetadata_1.EloquentMetadata.get(new Model()); expect(metadata['definition'] === Model).toBe(true); }); - }); - describe('protected .buildKnownAttributes()', function () { - it('merges reserved properties defined in .getReservedProperties() of model and driver', function () { - const metadata = EloquentMetadata_1.EloquentMetadata.get(new Model()); - const props = new Model()['getReservedProperties'](); - for (const name of props) { - expect(metadata['knownAttributes'].indexOf(name) !== -1).toBe(true); - } - }); - it('merges properties defined Eloquent.prototype', function () { - const metadata = EloquentMetadata_1.EloquentMetadata.get(new Model()); - const props = Object.getOwnPropertyNames(Model.prototype); - for (const name of props) { - expect(metadata['knownAttributes'].indexOf(name) !== -1).toBe(true); - } - }); - it('merges properties defined in model', function () { + it('create new instances of EloquentAttribute and saves in "attribute"', function () { const metadata = EloquentMetadata_1.EloquentMetadata.get(new Model()); - const props = ['accessor', 'mutator', 'modelMethod']; - for (const name of props) { - expect(metadata['knownAttributes'].indexOf(name) !== -1).toBe(true); - } - // warning: props defined in model is not included in list - expect(metadata['knownAttributes'].indexOf('props') === -1).toBe(true); - }); - it('merges properties defined in child model', function () { - const metadata = EloquentMetadata_1.EloquentMetadata.get(new ChildModel()); - const props = ['child_accessor', 'child_mutator', 'childModelMethod']; - for (const name of props) { - expect(metadata['knownAttributes'].indexOf(name) !== -1).toBe(true); - } - // warning: props defined in model is not included in list - expect(metadata['knownAttributes'].indexOf('child_props') === -1).toBe(true); - }); - it('merges properties defined GET_FORWARD_TO_DRIVER_FUNCTIONS', function () { - const metadata = EloquentMetadata_1.EloquentMetadata.get(new Model()); - const props = EloquentProxy_1.GET_FORWARD_TO_DRIVER_FUNCTIONS; - for (const name of props) { - expect(metadata['knownAttributes'].indexOf(name) !== -1).toBe(true); - } - }); - it('merges properties defined GET_QUERY_FUNCTIONS', function () { - const metadata = EloquentMetadata_1.EloquentMetadata.get(new Model()); - const props = EloquentProxy_1.GET_QUERY_FUNCTIONS; - for (const name of props) { - expect(metadata['knownAttributes'].indexOf(name) !== -1).toBe(true); - } - }); - }); - describe('protected .findGettersAndSetters()', function () { - it('finds all defined getters and put to accessors with type = getter', function () { - class GetterEmpty extends Eloquent_1.Eloquent { - getClassName() { - return 'GetterEmpty'; - } - } - najs_binding_1.register(GetterEmpty); - expect(EloquentMetadata_1.EloquentMetadata.get(new GetterEmpty())['accessors']).toEqual({}); - class GetterA extends Eloquent_1.Eloquent { - get a() { - return ''; - } - get b() { - return ''; - } - getClassName() { - return 'GetterA'; - } - } - najs_binding_1.register(GetterA); - const metadata = EloquentMetadata_1.EloquentMetadata.get(new GetterA()); - expect(metadata['accessors']).toEqual({ - a: { name: 'a', type: 'getter' }, - b: { name: 'b', type: 'getter' } - }); - }); - it('finds all defined setters and put to mutators with type = setter', function () { - class SetterEmpty extends Eloquent_1.Eloquent { - getClassName() { - return 'MutatorEmpty'; - } - } - najs_binding_1.register(SetterEmpty); - expect(EloquentMetadata_1.EloquentMetadata.get(new SetterEmpty())['mutators']).toEqual({}); - class SetterA extends Eloquent_1.Eloquent { - set a(value) { } - set b(value) { } - getClassName() { - return 'SetterA'; - } - } - najs_binding_1.register(SetterA); - const metadata = EloquentMetadata_1.EloquentMetadata.get(new SetterA()); - expect(metadata['mutators']).toEqual({ - a: { name: 'a', type: 'setter' }, - b: { name: 'b', type: 'setter' } - }); - }); - }); - describe('protected .findAccessorsAndMutators()', function () { - it('does thing if there is no function with format `get|set...Attribute`', function () { - class NoAccessorOrMutator extends Eloquent_1.Eloquent { - getClassName() { - return 'NoAccessorOrMutator'; - } - } - najs_binding_1.register(NoAccessorOrMutator); - expect(EloquentMetadata_1.EloquentMetadata.get(new NoAccessorOrMutator())['accessors']).toEqual({}); - expect(EloquentMetadata_1.EloquentMetadata.get(new NoAccessorOrMutator())['mutators']).toEqual({}); - }); - it('puts `get...Attribute` to accessors with type function, but skip if getter of same attribute is defined', function () { - class AccessorA extends Eloquent_1.Eloquent { - get a() { - return ''; - } - getAAttribute() { } - getFirstNameAttribute() { } - getWrongFormat() { } - getDoublegetDoubleAttribute() { } - getClassName() { - return 'AccessorA'; - } - } - najs_binding_1.register(AccessorA); - expect(EloquentMetadata_1.EloquentMetadata.get(new AccessorA())['accessors']).toEqual({ - a: { - name: 'a', - type: 'getter' - }, - first_name: { - name: 'first_name', - type: 'function', - ref: 'getFirstNameAttribute' - }, - doubleget_double: { - name: 'doubleget_double', - type: 'function', - ref: 'getDoublegetDoubleAttribute' - } - }); - }); - it('puts `set...Attribute` to mutators with type function, but skip if setter of same attribute is defined', function () { - class MutatorA extends Eloquent_1.Eloquent { - set a(value) { } - setAAttribute() { } - setFirstNameAttribute() { } - setWrongFormat() { } - setDoublegetDoubleAttribute() { } - getClassName() { - return 'MutatorA'; - } - } - najs_binding_1.register(MutatorA); - expect(EloquentMetadata_1.EloquentMetadata.get(new MutatorA())['mutators']).toEqual({ - a: { - name: 'a', - type: 'setter' - }, - first_name: { - name: 'first_name', - type: 'function', - ref: 'setFirstNameAttribute' - }, - doubleget_double: { - name: 'doubleget_double', - type: 'function', - ref: 'setDoublegetDoubleAttribute' - } - }); + expect(metadata['attribute']).toBeInstanceOf(EloquentAttribute_1.EloquentAttribute); }); }); describe('.getSettingProperty()', function () { @@ -402,13 +236,13 @@ describe('EloquentMetadata', function () { describe('.hasAttribute()', function () { it('returns false if the name not in "knownAttributes"', function () { const metadata = EloquentMetadata_1.EloquentMetadata.get(new Model()); - metadata['knownAttributes'] = ['test']; + metadata['attribute']['known'] = ['test']; expect(metadata.hasAttribute('test')).toEqual(true); expect(metadata.hasAttribute('not-found')).toEqual(false); }); it('always returns true if typeof name is Symbol', function () { const metadata = EloquentMetadata_1.EloquentMetadata.get(new Model()); - metadata['knownAttributes'] = ['test']; + metadata['attribute']['known'] = ['test']; expect(metadata.hasAttribute(Symbol.for('test'))).toEqual(true); expect(metadata.hasAttribute(Symbol.for('not-found'))).toEqual(true); }); diff --git a/lib/model/EloquentAttribute.ts b/lib/model/EloquentAttribute.ts new file mode 100644 index 0000000..cbda5c5 --- /dev/null +++ b/lib/model/EloquentAttribute.ts @@ -0,0 +1,91 @@ +import { Eloquent } from './Eloquent' +import { isFunction, snakeCase } from 'lodash' +import { GET_FORWARD_TO_DRIVER_FUNCTIONS, GET_QUERY_FUNCTIONS } from './EloquentProxy' + +export class EloquentAttribute { + protected known: string[] + protected dynamic: { + [key: string]: { + name: string + getter: boolean + setter: boolean + accessor?: string + mutator?: string + } + } + + constructor(model: Eloquent, prototype: any) { + this.dynamic = {} + this.known = [] + this.findGettersAndSetters(prototype) + this.findAccessorsAndMutators(prototype) + this.buildKnownAttributes(model, prototype) + } + + protected createDynamicAttributeIfNeeded(property: string) { + if (!this.dynamic[property]) { + this.dynamic[property] = { + name: property, + getter: false, + setter: false + } + } + } + + isKnownAttribute(name: string | Symbol) { + if (typeof name === 'symbol') { + return true + } + return this.known.indexOf(name as string) !== -1 + } + + buildKnownAttributes(model: Eloquent, prototype: any) { + this.known = Array.from( + new Set( + model['getReservedProperties']().concat( + Object.getOwnPropertyNames(model), + GET_FORWARD_TO_DRIVER_FUNCTIONS, + GET_QUERY_FUNCTIONS, + Object.getOwnPropertyNames(Eloquent.prototype), + Object.getOwnPropertyNames(prototype) + ) + ) + ) + } + + findGettersAndSetters(prototype: any) { + const descriptors: Object = Object.getOwnPropertyDescriptors(prototype) + for (const property in descriptors) { + const getter = isFunction(descriptors[property].get) + const setter = isFunction(descriptors[property].set) + if (!getter && !setter) { + continue + } + + this.createDynamicAttributeIfNeeded(property) + this.dynamic[property].getter = getter + this.dynamic[property].setter = setter + } + } + + findAccessorsAndMutators(prototype: any) { + const names = Object.getOwnPropertyNames(prototype) + const regex = new RegExp('^(get|set)([a-zA-z0-9_\\-]{1,})Attribute$', 'g') + names.forEach(name => { + let match + while ((match = regex.exec(name)) != undefined) { + // javascript RegExp has a bug when the match has length 0 + // if (match.index === regex.lastIndex) { + // ++regex.lastIndex + // } + const property: string = snakeCase(match[2]) + this.createDynamicAttributeIfNeeded(property) + if (match[1] === 'get') { + this.dynamic[property].accessor = match[0] + } else { + this.dynamic[property].mutator = match[0] + } + } + }) + } +} diff --git a/lib/model/EloquentMetadata.ts b/lib/model/EloquentMetadata.ts index 67dd7f3..7308411 100644 --- a/lib/model/EloquentMetadata.ts +++ b/lib/model/EloquentMetadata.ts @@ -1,7 +1,6 @@ import { make } from 'najs-binding' import { Eloquent } from './Eloquent' -import { GET_FORWARD_TO_DRIVER_FUNCTIONS, GET_QUERY_FUNCTIONS } from './EloquentProxy' -import { isFunction, snakeCase } from 'lodash' +import { EloquentAttribute } from './EloquentAttribute' export type EloquentTimestamps = { createdAt: string; updatedAt: string } @@ -10,22 +9,6 @@ export type EloquentSoftDelete = { overrideMethods: boolean | 'all' | string[] } -export type EloquentAccessors = { - [key: string]: { - name: string - type: 'getter' | 'function' | string - ref?: string - } -} - -export type EloquentMutators = { - [key: string]: { - name: string - type: 'setter' | 'function' | string - ref?: string - } -} - const DEFAULT_TIMESTAMPS: EloquentTimestamps = { createdAt: 'created_at', updatedAt: 'updated_at' @@ -51,79 +34,13 @@ export class EloquentMetadata { protected prototype: any protected definition: typeof Eloquent protected knownAttributes: string[] - protected accessors: EloquentAccessors - protected mutators: EloquentMutators + protected attribute: EloquentAttribute private constructor(model: Eloquent) { this.model = model this.prototype = Object.getPrototypeOf(this.model) this.definition = Object.getPrototypeOf(model).constructor - this.accessors = {} - this.mutators = {} - this.buildKnownAttributes() - this.findGettersAndSetters() - this.findAccessorsAndMutators() - } - - protected buildKnownAttributes() { - this.knownAttributes = Array.from( - new Set( - this.model['getReservedProperties']().concat( - Object.getOwnPropertyNames(this.model), - GET_FORWARD_TO_DRIVER_FUNCTIONS, - GET_QUERY_FUNCTIONS, - Object.getOwnPropertyNames(Eloquent.prototype), - Object.getOwnPropertyNames(this.prototype) - ) - ) - ) - } - - /** - * Find accessors and mutators defined in getter/setter, only available for node >= 8.7 - */ - protected findGettersAndSetters() { - const descriptors: Object = Object.getOwnPropertyDescriptors(this.prototype) - for (const name in descriptors) { - if (isFunction(descriptors[name].get)) { - this.accessors[name] = { - name: name, - type: 'getter' - } - } - if (isFunction(descriptors[name].set)) { - this.mutators[name] = { - name: name, - type: 'setter' - } - } - } - } - - protected findAccessorsAndMutators() { - const names = Object.getOwnPropertyNames(this.prototype) - const regex = new RegExp('^(get|set)([a-zA-z0-9_\\-]{1,})Attribute$', 'g') - names.forEach(name => { - let match - while ((match = regex.exec(name)) != undefined) { - // javascript RegExp has a bug when the match has length 0 - // if (match.index === regex.lastIndex) { - // ++regex.lastIndex - // } - const property: string = snakeCase(match[2]) - const data = { - name: property, - type: 'function', - ref: match[0] - } - if (match[1] === 'get' && typeof this.accessors[property] === 'undefined') { - this.accessors[property] = data - } - if (match[1] === 'set' && typeof this.mutators[property] === 'undefined') { - this.mutators[property] = data - } - } - }) + this.attribute = new EloquentAttribute(model, this.prototype) } getSettingProperty(property: string, defaultValue: T): T { @@ -170,10 +87,7 @@ export class EloquentMetadata { } hasAttribute(name: string | Symbol) { - if (typeof name === 'symbol') { - return true - } - return this.knownAttributes.indexOf(name as string) !== -1 + return this.attribute.isKnownAttribute(name) } /** diff --git a/test/model/EloquentAttribute.test.ts b/test/model/EloquentAttribute.test.ts new file mode 100644 index 0000000..e3426d1 --- /dev/null +++ b/test/model/EloquentAttribute.test.ts @@ -0,0 +1,184 @@ +import 'jest' +import { register } from 'najs-binding' +import { Eloquent } from '../../lib/model/Eloquent' +import { EloquentAttribute } from '../../lib/model/EloquentAttribute' +import { EloquentDriverProvider } from '../../lib/drivers/EloquentDriverProvider' +import { DummyDriver } from '../../lib/drivers/DummyDriver' +import { GET_FORWARD_TO_DRIVER_FUNCTIONS, GET_QUERY_FUNCTIONS } from '../../lib/model/EloquentProxy' + +EloquentDriverProvider.register(DummyDriver, 'dummy') + +class Model extends Eloquent { + props: string + + get accessor() { + return '' + } + + set mutator(value: any) {} + + getClassName() { + return 'Model' + } + + modelMethod() {} +} +register(Model) + +class ChildModel extends Model { + child_props: string + + get child_accessor() { + return '' + } + + set child_mutator(value: any) {} + + getClassName() { + return 'ChildModel' + } + + childModelMethod() {} +} +register(ChildModel) + +const fakeModel = { + getReservedProperties() { + return [] + } +} + +describe('EloquentAttribute', function() { + describe('.findGettersAndSetters()', function() { + it('finds all defined getters and put to accessors with type = getter', function() { + class ClassEmpty {} + const attribute = new EloquentAttribute(fakeModel, {}) + attribute.findGettersAndSetters(Object.getPrototypeOf(new ClassEmpty())) + expect(attribute['dynamic']).toEqual({}) + + class Class { + get a() { + return '' + } + + set a(value: any) {} + + get b() { + return '' + } + } + attribute.findGettersAndSetters(Object.getPrototypeOf(new Class())) + expect(attribute['dynamic']).toEqual({ + a: { name: 'a', getter: true, setter: true }, + b: { name: 'b', getter: true, setter: false } + }) + }) + }) + + describe('.findAccessorsAndMutators()', function() { + it('finds all defined getters and put to accessors with type = getter', function() { + class ClassEmpty {} + const attribute = new EloquentAttribute(fakeModel, {}) + attribute.findAccessorsAndMutators(Object.getPrototypeOf(new ClassEmpty())) + expect(attribute['dynamic']).toEqual({}) + + class Class { + get a() { + return '' + } + + getAAttribute() {} + + getFirstNameAttribute() {} + + getWrongFormat() {} + + set b(value: any) {} + + setBAttribute() {} + + setWrongFormat() {} + + setDoublegetDoubleAttribute() {} + + get c() { + return '' + } + + set c(value: any) {} + + getCAttribute() {} + + setCAttribute() {} + } + + attribute.findGettersAndSetters(Object.getPrototypeOf(new Class())) + attribute.findAccessorsAndMutators(Object.getPrototypeOf(new Class())) + expect(attribute['dynamic']).toEqual({ + a: { name: 'a', getter: true, setter: false, accessor: 'getAAttribute' }, + b: { name: 'b', getter: false, setter: true, mutator: 'setBAttribute' }, + c: { name: 'c', getter: true, setter: true, accessor: 'getCAttribute', mutator: 'setCAttribute' }, + first_name: { name: 'first_name', getter: false, setter: false, accessor: 'getFirstNameAttribute' }, + doubleget_double: { + name: 'doubleget_double', + getter: false, + setter: false, + mutator: 'setDoublegetDoubleAttribute' + } + }) + }) + }) + + describe('protected .buildKnownAttributes()', function() { + const attribute = new EloquentAttribute(fakeModel, {}) + attribute.buildKnownAttributes(new Model(), Model.prototype) + + it('merges reserved properties defined in .getReservedProperties() of model and driver', function() { + const props = new Model()['getReservedProperties']() + for (const name of props) { + expect(attribute['known'].indexOf(name) !== -1).toBe(true) + } + }) + + it('merges properties defined Eloquent.prototype', function() { + const props = Object.getOwnPropertyNames(Model.prototype) + for (const name of props) { + expect(attribute['known'].indexOf(name) !== -1).toBe(true) + } + }) + + it('merges properties defined in model', function() { + const props = ['accessor', 'mutator', 'modelMethod'] + for (const name of props) { + expect(attribute['known'].indexOf(name) !== -1).toBe(true) + } + + // warning: props defined in model is not included in list + expect(attribute['known'].indexOf('props') === -1).toBe(true) + }) + + it('merges properties defined in child model', function() { + const childAttribute = new EloquentAttribute(new ChildModel(), ChildModel.prototype) + const props = ['child_accessor', 'child_mutator', 'childModelMethod'] + for (const name of props) { + expect(childAttribute['known'].indexOf(name) !== -1).toBe(true) + } + // warning: props defined in model is not included in list + expect(childAttribute['known'].indexOf('child_props') === -1).toBe(true) + }) + + it('merges properties defined GET_FORWARD_TO_DRIVER_FUNCTIONS', function() { + const props = GET_FORWARD_TO_DRIVER_FUNCTIONS + for (const name of props) { + expect(attribute['known'].indexOf(name) !== -1).toBe(true) + } + }) + + it('merges properties defined GET_QUERY_FUNCTIONS', function() { + const props = GET_QUERY_FUNCTIONS + for (const name of props) { + expect(attribute['known'].indexOf(name) !== -1).toBe(true) + } + }) + }) +}) diff --git a/test/model/EloquentMetadata.test.ts b/test/model/EloquentMetadata.test.ts index 653a490..84931da 100644 --- a/test/model/EloquentMetadata.test.ts +++ b/test/model/EloquentMetadata.test.ts @@ -5,7 +5,7 @@ import { DummyDriver } from '../../lib/drivers/DummyDriver' import { EloquentDriverProvider } from '../../lib/drivers/EloquentDriverProvider' import { Eloquent } from '../../lib/model/Eloquent' import { EloquentMetadata } from '../../lib/model/EloquentMetadata' -import { GET_FORWARD_TO_DRIVER_FUNCTIONS, GET_QUERY_FUNCTIONS } from '../../lib/model/EloquentProxy' +import { EloquentAttribute } from '../../lib/model/EloquentAttribute' EloquentDriverProvider.register(DummyDriver, 'dummy') @@ -54,203 +54,10 @@ describe('EloquentMetadata', function() { const metadata = EloquentMetadata.get(new Model()) expect(metadata['definition'] === Model).toBe(true) }) - }) - - describe('protected .buildKnownAttributes()', function() { - it('merges reserved properties defined in .getReservedProperties() of model and driver', function() { - const metadata = EloquentMetadata.get(new Model()) - const props = new Model()['getReservedProperties']() - for (const name of props) { - expect(metadata['knownAttributes'].indexOf(name) !== -1).toBe(true) - } - }) - - it('merges properties defined Eloquent.prototype', function() { - const metadata = EloquentMetadata.get(new Model()) - const props = Object.getOwnPropertyNames(Model.prototype) - for (const name of props) { - expect(metadata['knownAttributes'].indexOf(name) !== -1).toBe(true) - } - }) - - it('merges properties defined in model', function() { - const metadata = EloquentMetadata.get(new Model()) - const props = ['accessor', 'mutator', 'modelMethod'] - for (const name of props) { - expect(metadata['knownAttributes'].indexOf(name) !== -1).toBe(true) - } - - // warning: props defined in model is not included in list - expect(metadata['knownAttributes'].indexOf('props') === -1).toBe(true) - }) - - it('merges properties defined in child model', function() { - const metadata = EloquentMetadata.get(new ChildModel()) - const props = ['child_accessor', 'child_mutator', 'childModelMethod'] - for (const name of props) { - expect(metadata['knownAttributes'].indexOf(name) !== -1).toBe(true) - } - // warning: props defined in model is not included in list - expect(metadata['knownAttributes'].indexOf('child_props') === -1).toBe(true) - }) - - it('merges properties defined GET_FORWARD_TO_DRIVER_FUNCTIONS', function() { - const metadata = EloquentMetadata.get(new Model()) - const props = GET_FORWARD_TO_DRIVER_FUNCTIONS - for (const name of props) { - expect(metadata['knownAttributes'].indexOf(name) !== -1).toBe(true) - } - }) - it('merges properties defined GET_QUERY_FUNCTIONS', function() { + it('create new instances of EloquentAttribute and saves in "attribute"', function() { const metadata = EloquentMetadata.get(new Model()) - const props = GET_QUERY_FUNCTIONS - for (const name of props) { - expect(metadata['knownAttributes'].indexOf(name) !== -1).toBe(true) - } - }) - }) - - describe('protected .findGettersAndSetters()', function() { - it('finds all defined getters and put to accessors with type = getter', function() { - class GetterEmpty extends Eloquent { - getClassName() { - return 'GetterEmpty' - } - } - register(GetterEmpty) - expect(EloquentMetadata.get(new GetterEmpty())['accessors']).toEqual({}) - - class GetterA extends Eloquent { - get a() { - return '' - } - - get b() { - return '' - } - - getClassName() { - return 'GetterA' - } - } - register(GetterA) - const metadata = EloquentMetadata.get(new GetterA()) - expect(metadata['accessors']).toEqual({ - a: { name: 'a', type: 'getter' }, - b: { name: 'b', type: 'getter' } - }) - }) - - it('finds all defined setters and put to mutators with type = setter', function() { - class SetterEmpty extends Eloquent { - getClassName() { - return 'MutatorEmpty' - } - } - register(SetterEmpty) - expect(EloquentMetadata.get(new SetterEmpty())['mutators']).toEqual({}) - - class SetterA extends Eloquent { - set a(value: any) {} - - set b(value: any) {} - - getClassName() { - return 'SetterA' - } - } - register(SetterA) - const metadata = EloquentMetadata.get(new SetterA()) - expect(metadata['mutators']).toEqual({ - a: { name: 'a', type: 'setter' }, - b: { name: 'b', type: 'setter' } - }) - }) - }) - - describe('protected .findAccessorsAndMutators()', function() { - it('does thing if there is no function with format `get|set...Attribute`', function() { - class NoAccessorOrMutator extends Eloquent { - getClassName() { - return 'NoAccessorOrMutator' - } - } - register(NoAccessorOrMutator) - expect(EloquentMetadata.get(new NoAccessorOrMutator())['accessors']).toEqual({}) - expect(EloquentMetadata.get(new NoAccessorOrMutator())['mutators']).toEqual({}) - }) - - it('puts `get...Attribute` to accessors with type function, but skip if getter of same attribute is defined', function() { - class AccessorA extends Eloquent { - get a() { - return '' - } - - getAAttribute() {} - - getFirstNameAttribute() {} - - getWrongFormat() {} - - getDoublegetDoubleAttribute() {} - - getClassName() { - return 'AccessorA' - } - } - register(AccessorA) - expect(EloquentMetadata.get(new AccessorA())['accessors']).toEqual({ - a: { - name: 'a', - type: 'getter' - }, - first_name: { - name: 'first_name', - type: 'function', - ref: 'getFirstNameAttribute' - }, - doubleget_double: { - name: 'doubleget_double', - type: 'function', - ref: 'getDoublegetDoubleAttribute' - } - }) - }) - - it('puts `set...Attribute` to mutators with type function, but skip if setter of same attribute is defined', function() { - class MutatorA extends Eloquent { - set a(value: any) {} - - setAAttribute() {} - - setFirstNameAttribute() {} - - setWrongFormat() {} - - setDoublegetDoubleAttribute() {} - - getClassName() { - return 'MutatorA' - } - } - register(MutatorA) - expect(EloquentMetadata.get(new MutatorA())['mutators']).toEqual({ - a: { - name: 'a', - type: 'setter' - }, - first_name: { - name: 'first_name', - type: 'function', - ref: 'setFirstNameAttribute' - }, - doubleget_double: { - name: 'doubleget_double', - type: 'function', - ref: 'setDoublegetDoubleAttribute' - } - }) + expect(metadata['attribute']).toBeInstanceOf(EloquentAttribute) }) }) @@ -483,14 +290,14 @@ describe('EloquentMetadata', function() { describe('.hasAttribute()', function() { it('returns false if the name not in "knownAttributes"', function() { const metadata = EloquentMetadata.get(new Model()) - metadata['knownAttributes'] = ['test'] + metadata['attribute']['known'] = ['test'] expect(metadata.hasAttribute('test')).toEqual(true) expect(metadata.hasAttribute('not-found')).toEqual(false) }) it('always returns true if typeof name is Symbol', function() { const metadata = EloquentMetadata.get(new Model()) - metadata['knownAttributes'] = ['test'] + metadata['attribute']['known'] = ['test'] expect(metadata.hasAttribute(Symbol.for('test'))).toEqual(true) expect(metadata.hasAttribute(Symbol.for('not-found'))).toEqual(true) })