Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

[DO NOT MERGE] Add core classes for data modeling #62

Closed
wants to merge 13 commits into from

Conversation

raymondfeng
Copy link
Contributor

@raymondfeng raymondfeng commented Feb 20, 2017

cc @bajtos @raymondfeng @ritch @superkhau

WIP to add core classes for data modeling & persistence

@superkhau
Copy link
Contributor

The concept is LGTM.

/**
* A constructor
*/
export type Constructable = new (...args: any[]) => Object;
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Constructor's don't return a generic Object, they return a specific class instance.

I am proposing to use the following definition from TypeScript's wiki:

type Constructor<T> = new(...args: any[]) => T;

I would personally prefer design patterns from TypeScript team over our own inventions, I believe they are less likely to contain subtle flaws and should be easier to understand for new people (TypeScript developers) discovering LoopBack code base.

Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I would personally prefer design patterns from TypeScript team over our own inventions

👍

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I tried that but it gives me quite a bit trouble. One thing I cannot do is Constructor<?> which means a unknown type of constructor.

Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Did you try Constructor<any> or Constructor<Object>? I think the latter should be the same as what you have right now.

@bajtos
Copy link
Member

bajtos commented Feb 21, 2017

mixin<BC extends Constructable>(Base: BC): BC;

My concern is that mixin<BC extends Constructable>(Base: BC): BC will not allow IDEs to auto-complete new members added by the mixin.

The example provided on TS wiki uses an anonymous class as the return type.

function Tagged<T extends Constructor<{}>>(Base: T) {
    return class extends Base {
        _tag: string;
        constructor(...args: any[]) {
            super(...args);
            this._tag = "";
        }
    }
}

This has the unfortunate effect that typescript compiler is reporting Return type of exported function has or is using private name '(Anonymous class)' error when trying to build a .d.ts file (see microsoft/TypeScript#6307 for a bit more of context). Fortunately, this can be worked around:

interface Tagged {
  _tag: string;
}

interface WithTag {
  new(...args: any[]): Tagged;
}

function Tagged<T extends Constructor<{}>>(Base: T): T & WithTag {
    return class extends Base implements Tagged {
        _tag: string;
        constructor(...args: any[]) {
            super(...args);
            this._tag = '';
        }
    };
}

const TaggedPoint = Tagged(Point);
// VSCode shows the following hints:
// [tslint] variable name must be in camelcase or uppercase (variable-name)
// const TaggedPoint: typeof Point & WithTag

Based on that, I think the MixinProvider and MixinFunc APIs should be defined like this:

export interface MixinProvider<Mixin> {
  mixin<BC extends Constructor<BC>>(Base: BC): BC & Mixin;
}

export interface MixinFunc<Mixin> {
  <BC extends Constructor<BC>(Base: BC): BC & Mixin;
}

// usage
const fn: MixinFunc<WithTag> = Tagged;

Thoughts?

* Mix in properties/methods into a base class
* @param Base The base class
*/
mixin<BC extends Constructable>(Base: BC): BC;
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

* See https://en.wikipedia.org/wiki/Domain-driven_design#Building_blocks
*/

export abstract class Model {
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

abstract class or interface - what are the arguments to choose one over another?

As I has shown in my spike #54, TypeScript's type system compares classes (constructors) by reference, while interfaces are compared by content (member signatures).

What if we defined ModelMixin that would add these three new members? That would allow us to convert any TypeScript/JavaScript class (e.g. coming from a 3rd party package) into a model.

Thoughts?

/**
* Base class for entities which have unique ids
*/
export abstract class Entity {
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Is it desirable to have a class that's both a Model and an Entity at the same time? How are we going to achieve that, considering that JavaScript does not support multi-inheritance?

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Entity should extend from Model

@bajtos
Copy link
Member

bajtos commented Feb 21, 2017

@raymondfeng Good start! I reviewed the code, I find it difficult to understand the design in all details without seeing it in action. I think we really need to add a simple connector implementation and few unit tests to verify that all pieces fit together as envisioned. I am almost sure there are places that will need some tweaking to make them work.

@bajtos
Copy link
Member

bajtos commented Feb 21, 2017

There is one design decision that may or may not be subjective.

  • In your proposal, the responsibility of converting a rich model instance into a plain data object lies in the connector - connector methods are receiving the original model instance.
  • In my spike [DO NOT MERGE] Spike - multiple copies of typedefs #54, I implemented conversion from model instance into plain data objects in the Repository class.

I don't know which choice is the best, but I think we should make careful consideration befor picking one.

What I see as benefits of converting model instances into data objects in Repository:

  • No need to repeat this step in every connector implementation. This may also prevent possible bugs where a community-maintained connector does not perform the conversion correctly (e.g. calls for (var prop in modelInstance) to iterate over all properties, instead of calling toObject).
  • I believe the current Connector API contract expects the connectors to receive a plain data object. If we preserve this, then I think it would be very cheap for loopback-next to support all existing connectors. The less work we have to make, the sooner we can release a usable loopback-next version.

@@ -0,0 +1,8 @@
import {ValueObject} from "../../lib/model";
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Nit:

We should be consistent in import... eg:

import {ValueObject} from "juggler" // or in a real app "@loopback/juggler"

@model()
class Order extends Entity {
@property({name: 'qty', mysql: {
column: 'QTY'
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

so clean... 👍

id: string;
customerId: string;

@belongsTo()
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

is there some prior art for this? this doesn't really read right to me...

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

import {property, model, belongsTo} from "../../lib/decorator";
import {Customer} from './customer'

@model()
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

If there isn't an argument to the decorator - can't this be @model

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

@model() can optionally accept an object as the argument to spell out more metadata.


import {MixinBuilder, Constructable} from './mixin';

module.exports = function (ds: DataSource, Model: Constructable) {
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

is this just for compatability with juggler? I find this code below incredibly confusing and obscure...

I'm not even sure where this is used...? Is there example somewhere?

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I'll add more comments in the code.

@ritch
Copy link
Contributor

ritch commented Feb 22, 2017

@raymondfeng can you start by creating a real world or close to real world example where you aren't the one defining the requirements? Eg. take a real set of data models that aren't defined by you and create an example of how you would create them in LB-N?

That way we can really make sure that our design is going the right way.

@superkhau
Copy link
Contributor

Closing for now, open again when the time comes.

@superkhau superkhau closed this Feb 28, 2017
@raymondfeng raymondfeng reopened this Mar 1, 2017
@raymondfeng
Copy link
Contributor Author

The purpose of this PR is to facilitate the on-going reviews/feedbacks. Let's keep it open but not merge it into master yet.

@raymondfeng
Copy link
Contributor Author

I'll add more tests to show case the full usage.

@ritch
Copy link
Contributor

ritch commented Mar 3, 2017

I don't think its a matter of adding more tests - we need real world examples to validate our design

@raymondfeng raymondfeng force-pushed the feature/persistence branch 6 times, most recently from 2d7102a to b1e9e62 Compare March 14, 2017 20:06
@ritch
Copy link
Contributor

ritch commented Apr 19, 2017

Hey @raymondfeng - can you re-open this when it comes back up on your plate? I want to keep the PR list actionable - so we can use it as a sort of TODO list for review / etc.

@ritch ritch closed this Apr 19, 2017
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment
Labels
None yet
Projects
None yet
Development

Successfully merging this pull request may close these issues.

4 participants