reflect-metadata icon indicating copy to clipboard operation
reflect-metadata copied to clipboard

How to remember what properties with my decorator?

Open cncolder opened this issue 6 years ago • 4 comments

Image this example:

class Pet extends Model {
  @field({ required: true })
  id: number
}
class Cat extends Pet {
  @field({ min: 3, max: 20 })
  name: string
}

field decorator write options into metadata Reflect.defineMetadata('orm:options', options, target, key)

Then how could I get all properties ['name', 'id'] with @field?

Now my codes like this. But I wish a better way:

function field(target, key, desc, [options]) {
  Reflect.defineMetadata('orm:options', options, target, key)
  Reflect.defineMetadata(`orm:key:${key}`, true, target)
}

class Model {
  static getFieldKeys() {
    return Reflect.getMetadataKeys(this.prototype)
      .filter(k => k.startsWith('orm:key:'))
      .map(k => k.substr(8))
  }
}

cncolder avatar Apr 13 '18 07:04 cncolder

Your decorator can hold the option inside itself, like this:

class Pet extends Model {
  id: number
}
class Cat extends Pet {
  @field({ min: 3, max: 20 })
  name: string
}
function field({ min: number, max: number }) {
  let _val: any;
  return function (target: any, key: string) {
    const getter = function () {
      return _val;
    };
    const setter = function (newVal) {
      if (newVal.length <= max && newVal.length >= min) {
        _val = newVal;
      }
    };

    Object.defineProperty(target, key, {
      get: getter,
      set: setter
    });
  }
}

This way you don't have to expose metadata and the logic will stay inside your decorator.

DavideCarvalho avatar May 16 '18 17:05 DavideCarvalho

Kind of misses the point of a metadata-based approach to decorators, no? The whole point is to avoid modifying behaviour of an object via the presence of the annotation. Instead, allow the annotation to be given meaning AT RUN TIME. That way, I can use the object in a context that doesn't require the annotation without any change in behaviour or code. The annotation creates metadata that is available via reflection in order to conditionally apply the annotation or not during code execution. If you will be using it in a context that requires the annotations, pass class definitions or class instances through a function that 'enables' an annotation by looking at the metadata and applying the functionality that is appropriate. This is actually why it is useful to be able to say "give me all of the methods of the target that have been annotated with key 'x', because now I can just iterate over all of them, wrapping each with the decorator functionality. Without that, I have to iterate over every property, checking to see if it has a particular metadata key, and then make the change. It's not a particularly big change. It's just a convenience function in the API that's being asked for, though it would probably be more efficient to map in both directions so that using the convenience function is more than just a macro for iterating over all properties, building a list with a particular metadata. You can implement the reverse map in a slightly simpler manner than the OP did, however:

function field(target, key, desc, [options]) {
  Reflect.defineMetadata('orm:options', options, target, key)
  let keys = Reflect.getMetadata('orm:keys', target)
  if (keys == undefined) {
    keys = { 'field': new Set(key) }
  } else {
    // also should check that 'field' key has an object before callling method on it.
    keys['field'].add(key)
  }
  Reflect.defineMetadata('orm:keys', keys, target)
}

class Model {
  static getFieldKeys() {
    return Reflect.getMetadata('orm:keys', this.prototype)
  }
}

You can store lists and maps in metadata - so just maintain the Set of fields that have been seen by a particular decorator in the metadata. If you don't have multiple decorators with different names, there's no need for a map of Sets, just store the Set directly into 'orm:fields'

ghost avatar Oct 08 '18 00:10 ghost

I want to use Reflect.getMetadata("design:type", target, propertyKeys) for get all info of class, but it define by typescript, not me.

maybe support some api to get property names will be better?

s97712 avatar Oct 25 '18 04:10 s97712

You can easily implement a reverse lookup yourself. When you write the design:type metadata into target.property, you can also write design:types metadata into target - maintaining a list of propertyKeys that have been seen. You can retrieve the set of properties which have the annotation by simply looking up design:types and then iterating over each value, looking up design:type for target.propertyKey. It requires so little code to do this that there really doesn't seem to be much reason to insist upon its inclusion in the reflect-metadata API itself.

ghost avatar Oct 25 '18 07:10 ghost