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

Adapters architecture #290

Closed
flovilmart opened this issue Feb 7, 2016 · 10 comments
Closed

Adapters architecture #290

flovilmart opened this issue Feb 7, 2016 · 10 comments

Comments

@flovilmart
Copy link
Contributor

It becomes evident that the extensibility of parse-server will be a core element of the server architecture.
Leveraging npm, it would be very simple and efficient to setup servers with custom adapters without the need of writing code.

I am personally looking for 'configuring' standalone parse-server instead of maintaining an express app that would serve the parse-server alongside the rest of my web app.

I'm opening the issue here to discuss this architecture.

I suggest the following proposal:

  • Each extensible part of the server should be implemented through an adapter.
  • On the configuration object (args) we should be able to support adapters as module names.
  • On the configuration object, we should be able to pass adapter options.

example:

Let's say someone implemented parse-server-sendgrid-emailadapter and made it available on npm.

the configuration Object would look like:

{
    appId: "AppID",
    EmailAdapter: "parse-server-sendgrid-emailadapter",
    options: {
      "parse-server-sendgrid-emailadapter": { /*specific options for the module*/}
    }
}

in EmailAdapter:

... 
setAdapter: function(adapter) {
   if (typeof adapter === "function") {
     _adapter = adapter;
   } else if (typeof adapter === "string") {
     _adapter = require(adapter);
   }
} 
...
setOptions: function(options) {
   if (typeof _adapter.setOptions === "function" ){
         _adapter.setOptions(options); 
   } 
}

For each adapter type, we would just have to provide the base API to conform to.
The developer of the module should provide an optional setOptions if the module support options.

For example the options for the mail adapter could be template URL's etc...

Thoughts @gfosco, @drew-gross, @nlutsenko ?

@flessard
Copy link
Contributor

flessard commented Feb 7, 2016

+1

@gnz00
Copy link

gnz00 commented Feb 8, 2016

The way I had implemented it was like this, although it doesn't support the service registry approach you're going for:

See: https://gist.github.com/maysale01/5390485e676ee3745960 and https://github.com/ParsePlatform/parse-server/pull/291/files

That pull request has a method resolveAdapter(adapter, options).

function resolveAdapter(adapter, options) {
    // Support passing in adapter paths
    if (typeof adapter === 'string') {
        adapter = require(adapter);
    }

    // Instantiate the adapter if the class got passed instead of an instance
    if (typeof adapter === 'function') {
        adapter = new adapter(options);
    }

    return adapter;
}

resolveAdapter is used in the ParseServer constructor, but can also be used afterwards:

let SendGridAdapter = require("parse-server-mailgun-adapter");
let server = new ParseServer({
    applicationId: 12345,
    mail: {
        /* adapter can be a module name, class or object/instance */
        /* if its a module name, it can resolve to either a class or object/instance */
        adapter: "parse-server-mailgun-adapter",
        options: {
            apiKey: "12345"
        }
    },
    cache: {
        options: {
            /* no adapter specified, using default */
            defaultTtl: 20 * 60 * 1000 
        }
    }
});

// All of the following are equivalent
let adapter = server.resolveAdapter("parse-server-mailgun-adapter", { apiKey: "12345" });
server.mailProvider.setAdapter(adapter);

adapter = server.resolveAdapter(SendGridAdapter, { apiKey: "12345" });
server.mailProvider.setAdapter(adapter);

adapter = new SendGridAdapter({ apiKey: "12345" });
server.mailProvider.setAdapter(adapter);

// If passed an object it just returns the object (options do nothing);
adapter = server.resolveAdapter(adapter);
server.mailProvider.setAdapter(adapter);

@flovilmart
Copy link
Contributor Author

@mysale01 that's real nice.
I was suggesting we start that discussion now as we'll face more issues in the future around that design.

We should leverage as much as we can npm to inject and configure adapters. That would also reduce the size of the code base and make it more maintainable.

In that sense, I would even go as far as putting the base email adapter in an external repo and as a dependency.

@gnz00
Copy link

gnz00 commented Feb 8, 2016

Indeed. I think it all centers around whether or not "Parse Server" is a
standalone server or an Express middleware. I think the consensus is that
Parse Server should be a standalone server. In which case, I would advocate
moving all the core logic to a "parse-express" package that is leveraged by
Parse Server.

On Sunday, February 7, 2016, Florent Vilmart notifications@github.com
wrote:

@mysale01 that's real nice.
I was suggesting we start that discussion now as we'll face more issues in
the future around that design.

We should leverage as much as we can npm to inject and configure adapters.
That would also reduce the size of the code base and make it more
maintainable.

In that sense, I would even go as far as putting the base email adapter in
an external repo and as a dependency.


Reply to this email directly or view it on GitHub
#290 (comment)
.

@flovilmart
Copy link
Contributor Author

So moving towards that direction we need a refactor of the Adapter architecture, then we can modularize pretty much everything. If the maintainers decide to move a module externally, that won't change anything, but the default string in the require call.

Goes hands in hands with #275, #282, #151, maybe #186 and possibly more.

@drew-gross
Copy link
Contributor

Thanks for starting this discussion. One thing that really excites me about having an open source version of Parse is the ability to have much more powerful configuration options. It wasn't possible on Parse.com to use JS functions in configuration due to security issues, process isolation issues, and other multi-tenant issues. But having a single tenant parse-server opens the door door to things like what I mentioned in #275 about having a mailer that generates mail based on fields on the current User object.

Having the configuration be in a separate place from the actual server app could also be useful in helping guys like Heroku provide a web based configuration and easy "getting started" flow, which I think could be awesome for beginner developers who are maybe making their first app that has a web component (or maybe even their first time programming at all)

I really like where this discussion is going :) here is my suggestion for an interface:

Config method 1: Pass an object that conforms to an interface in the configuration object. This will satisfy the power users who want full control over everything that goes on in their application, and enables some fancy features.

var AdvancedEmailAdapter = require('advanced-email-adapter');

{
  appId: "appID",
  EmailAdapter: new AdvancedEmailAdapter("option1", () => { return "option2" }),
}

The REALLY advanced users can even implement the adapter directly, which probably doesn't make much sense for the big things like email or database adapters, but could make sense for smaller things, like choosing where Cloud Code is loaded from (if we expose that as an extensibility point):

{
  appId: "appID",
  EmailAdapter: {
    sendVerificationEmail: (user, token) => { ... },
    sendPasswordResetEmail: (user, token) => { ... },
    sendPasswordResetSuccessEmail: (user) => { ... },
  }
}

Config option 2: Have the server also accept the npm-module-name approach. This allows simple configurations to be pure JSON, which will make life easier for the people building parse hosting services. @maysale01 I think this also covers the use cases for having the config be a class, but if not please let me know, I'm sure we could have that as option as well if you had some use cases in mind.

{
  appId: "appID",
  EmailAdapter: {
    nodeModule: "parse-mailgun-adapter",
    options: {
      mailgunKey: "abcdef",
      mailgunSecret: "..."
    }
  }
}

I personally dislike having a lot of places where you check the typeof something and then branch based off of that, so I chose where to put keys based on attempting to minimize that, but I'll leave the final decision on that stuff up to whoever ends up implementing this :) It would probably be nice to do typeof introspection anyway, and have the parse-server refuse to start if the adapter has wrong types.

Having the parse-server generally be designed to be used standalone I think is a good idea. If thats the case, that would mean things like changing an adapter or it's options while the server is running would be a little weird and maybe that isn't something we should spend a lot of effort supporting. Happy to hear other opinions though.

@maysale01 if we moved the core logic to parse-express, what would be left in parse-server?

@gnz00
Copy link

gnz00 commented Feb 8, 2016

@drew-gross

Re: parse-express
I'm not entirely sure. I think a parse-express library would encapsulate the route handlers and the adapters, but not CloudCode, views, internationalization, schema editor/dashboard, analytics. It would open up parse-server to become fully opinionated and more comprehensive (i.e. full blown replacement for Parse/accommodating all the quick start people). Might be a bit premature to consider at this point.

Re: Adapter submodules
This probably needs to happen just for the benefit of npm versioning. Currently, if a feature is added to say the S3Adapter, the entire project has to be bumped and republished.

Re: Adapter configuration
Changing the configuration was just a matter of showing the flexibility of the type checking in resolveAdapter. Nothing is branching, it's all just being resolved to an object/instance. The real significance is during construction:

// Instance.
import { default as MemoryCache } from 'parse-server-memory-cache';
new ParseServer({
    cache: {
        adapter: new MemoryCache({ defaultTtl: 10 * 60 * 1000 })
    }
});

// Override methods
MemoryCache.prototype.filter = myFilterFn;
new ParseServer({
    cache: {
        adapter: new MemoryCache({ defaultTtl: 10 * 60 * 1000 })
    }
});
// Class/Function. 
// Useful if the provider needs to create future instances, e.g. multiple apps with distinct caches
import { default as MemoryCache } from 'parse-server-memory-cache';
new ParseServer({
    cache: {
        adapter: MemoryCache,
        options: {
            defaultTtl: Infinity
        }
    }
});
// String. Allows the config to be dynamic, e.g. JSON payload
// The module could return an object, instance, or class (constructor function) .
new ParseServer({
    cache: {
        adapter: "parse-server-memory-cache",
        options: {
            defaultTtl: Infinity
        }
    }
});
// Object. 
new ParseServer({
    cache: {
        adapter: {
            put: (key, value) => { ... },
            del: (key) => { ... },
            get: (key) => { ... },
            clear: () => { ... }
        }
    }
});

@nitrag
Copy link

nitrag commented Feb 8, 2016

I don't know what you guys are talking about but it sounds Smart and I like that! Keep it up! :)

@drew-gross
Copy link
Contributor

@maysale01 this looks like a good interface for adapters. You have a very good point about NPM versioning and having a default adapter class for multiple apps. I'm still not sure about moving the core logic to parse-express though. The dashboard is already planned to be in a separate repo, and analytics is something we don't plan to build a replacement for right now. CloudCode, views, and i18n could be done through adapters probably, but I think thats something we could explore later.

@flovilmart
Copy link
Contributor Author

@drew-gross I'm reimplementing the whole cloud code stack now, in sub processes, and also to allow external servers to run cloud code for the multiple apps implementation. I just need to be able to skip the beforeSave hook after update in Cloud code.

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

No branches or pull requests

6 participants