scottwrobinson / camo

A class-based ES6 ODM for Mongo-like databases.
556 stars 80 forks source link

Suggestion: Use Decorators for Schema #6

Closed WorldMaker closed 9 years ago

WorldMaker commented 9 years ago

Looking at this project from the perspective of a TypeScript developer it is so close to what I would like to see from an ES6+ ODM. The big issue that keeps it from being perfect for TypeScript right now is that the type of a class property can jump from being "schema object" in the constructor to actual data type everywhere else. It would be great if you could place the schema information into separate symbols (and thus dummy/default values in the actual symbols) so that the typing information flows correctly to instances of that class. One way to do that and keep the class definitions easy to read might be to use ES2016 (ES7) Decorators.

scottwrobinson commented 9 years ago

I should preface this by saying I'm not very familiar with TypeScript, so please forgive my ignorance.

I agree that its a bit weird defining the schema and then using the same properties for their values. I would love to use decorators, but they aren't supported in ES6 (as far as I know, at least). As of right now there is a method, schema(), that allows you to set the schema, but its not as clean as the current way :)

class Person extends Document {
    constructor() {
        super('persons');

        this.schema({
            birthday: Date,
            name: String,
            age: Number
        });
    }
}

I'm up for ideas if you have any. Thanks for your input!

WorldMaker commented 9 years ago

That schema method looks like a great start for what I'm looking for. I will try it when I get a chance.

I recommend you take a look at TypeScript when you get a chance. It's all the smart OO things of ES6 with some benefits of compile time static typing. As someone with a background in static typed languages, I think you might appreciate what TypeScript does.

Yeah, decorators are not ES6, but heavily planned for ES7. (Technically, ES6 is now referred to as ES2015 and Decorators are planned for ES2016...) The Angular team at Google seems very fervent that Decorators make it into ES2016. Almost all of the transpilers (Traceur, Babel, and TypeScript) support Decorators now, so it's a "future feature" of JS that's very easy to explore today.

Ultimately, ES2016 Decorators are very heavily influenced by Python Decorators if you've seen them, and are similar enough to C# Attributes and Java Annotations that you shouldn't be too shocked at how they are/will be used. The link in my first post is pretty informative.

scottwrobinson commented 9 years ago

Thanks for the info! I'll definitely take a look at TypeScript.

I'm really looking forward to ES2016's decorators as I come from a Java and Python background :) I like where things are heading in JS.

Let me know if you run in to any problems using schema(). I haven't used/tested it as much as I should have. It was an early feature that I've kinda forgot about lately. I'd be happy to bolster it up a bit knowing its useful to people.

Closing this issue for now.

scottwrobinson commented 9 years ago

FYI I found some issues with the schema() method, which are now fixed as of v0.5.7. It should work as expected now. Let me know if you have any problems.

developit commented 9 years ago

@scottwrobinson I know this issue is closed, I just came across the project and figured I'd drop a suggestion here: use class properties to define schema methods (and perhaps the collection name?) instead of putting them in the constructor:

class Person extends Document {
  collection = 'person';

  name = { type:String, required:true },
  birthday = Date;
  updated = { type:String, default:Date.now };
}

Ideally, though, since most NodeJS code is not being minified, it would be nice to automatically create a collection name based on the value of this.constructor.name.

Just some thoughts!

WorldMaker commented 9 years ago

@developit From my understanding class properties as you show are syntactic sugar in ES6 for the constructor syntax, or at least, that's how TypeScript transpiles them:

// Input
class Person extends Document {
  name = { type:String, required:true };
  constructor() {
    super('people');
  }
}

// Output (ie, functionally the same as)
class Person extends Document {
  constructor() {
    super('people');
    this.name = { type:String, required:true };
  }
}

So you should already be able to do that in whatever transpiler you are using if it already supports class properties.

That doesn't solve my issue in TypeScript because { type: any, required: boolean } is not the same type as string and thus once you've set the schema in the constructor you can't actually use that instance property.

Decorators help here because you could do something like:

import { Document } from 'camo';
import { collection, field, required } from 'camo/decorators';

@collection('people')
class Person extends Document {
  @field(String)
  @required
  name: string = '';
}

With experimental TypeScript Descriptor metadata a TypeScript-specific version of @field could even deduce that the field was a string without needing to pass it in by getting the type information from the compiler. The same reflection metadata could also probably be used to default the collection name, and doing that at TypeScript compile time would avoid minification issues at least. (Eventually minifiers will probably have to deal with Reflect.metadata, definitely if/when Decorators land...)

scottwrobinson commented 9 years ago

@developit Thanks for the suggestion!

I'd like to get the declarations out of the constructor, but class properties aren't supported in ES6, which is the current focus of Camo. Ideally we'll be able to use decorators, like @WorldMaker suggested, when possible. If there is a way to support this on a transpiler, I'd be happy to add it (although I haven't had time to make myself familiar with any transpilers lately).

As for declaring the collection name, I plan to have it determine the name via this.constructor.name and have it use something like Pluralize to make it plural.

As always, ideas/suggestions are welcome.

developit commented 9 years ago

Using babel-node or Babel's require hook with the {stage:0} option in a .babelrc would enable both decorator and class property support.

@developit https://github.com/developit Thanks for the suggestion!

I'd like to get the declarations out of the constructor, but class properties aren't supported in ES6, which is the current focus of Camo. Ideally we'll be able to use decorators, like @WorldMaker https://github.com/WorldMaker suggested, when possible. If there is a way to support this on a transpiler, I'd be happy to add it (although I haven't had time to make myself familiar with any transpilers lately).

As for declaring the collection name, I plan to have it determine the name via this.constructor.name and have it use something like Pluralize https://github.com/blakeembrey/pluralize to make it plural.

As always, ideas/suggestions are welcome.

— Reply to this email directly or view it on GitHub https://github.com/scottwrobinson/camo/issues/6#issuecomment-134732985.

WorldMaker commented 9 years ago

TypeScript also transpiles both features today. I like the extra type-checking Typescript provides and do recommend it as an option.

scottwrobinson commented 9 years ago

How do you envision this working? Camo's main focus is on Node/io.js, so we'd probably have to fork the project to add this. As these features aren't yet supported by Node, I probably won't be able to put much time in to something like this until after we cross v1.0. There are still quite a few higher priority features I need to add first.

WorldMaker commented 9 years ago

I have projects that I use in Node that are transpiled from Typescript with a build process. Lately I've been using VSCode and the build process is just Ctrl+Shift+B. Source is Typescript, output can be "runs anywhere" ES5 JS.

With Babel, as the other option, you can transpile through a loader. SystemJS is the one of choice for me. It acts like the ES6/ES2015 loader (kind of a polyfill+extra goodness) and will transpile modules when needed. I use SystemJS in the browser mostly, but it supports Node too. I think there is a way to bundle things so that users don't need to use SystemJS directly unless they too want to make use of it, but I could be wrong.

developit commented 9 years ago

Currently the most pain-free option is to use Babel's Require Hook. You just add this to the entry file of a node application and everything else is transpiled and cached on-the-fly:

require("babel/register");

// then jump into your ES6/7/x code
require('./main');