GithubHelp home page GithubHelp logo

tommybananas / finale Goto Github PK

View Code? Open in Web Editor NEW
187.0 11.0 36.0 571 KB

Create flexible REST endpoints and controllers from Sequelize models in your Express app

JavaScript 99.89% Dockerfile 0.11%
sequelize sequelizejs rest-api rest restful-api

finale's Introduction

Build Status Dependency Status

Finale

Create flexible REST endpoints and controllers from Sequelize models in your Express or Restify app.

This project aims to be a Sequelize 4.x and 5.x compatible version of Epilogue.

Installation

npm install finale-rest

Getting Started

var Sequelize = require('sequelize'),
    finale = require('finale-rest'),
    http = require('http');

// Define your models
var database = new Sequelize('database', 'root', 'password');
var User = database.define('User', {
  username: Sequelize.STRING,
  birthday: Sequelize.DATE
});

// Initialize server
var server, app;
if (process.env.USE_RESTIFY) {
  var restify = require('restify');
  var corsMiddleware = require('restify-cors-middleware');
  
  app = server = restify.createServer()
  var cors = corsMiddleware({
    preflightMaxAge: 5, // Optional
    origins: ['*'], // Should whitelist actual domains in production
    allowHeaders: ['Authorization', 'API-Token', 'Content-Range'], //Content-range has size info on lists
    exposeHeaders: ['Authorization', 'API-Token-Expiry', 'Content-Range']
  })

  server.pre(cors.preflight)
  server.use(cors.actual)

  server.use(restify.plugins.queryParser()); //{mapParams: true}
  server.use(restify.plugins.bodyParser());  //{mapParams: true, mapFiles: true}
  server.use(restify.plugins.acceptParser(server.acceptable));
} else {
  var express = require('express'),
      bodyParser = require('body-parser');

  var app = express();
  app.use(bodyParser.json());
  app.use(bodyParser.urlencoded({ extended: false }));
  server = http.createServer(app);
}

// Initialize finale
finale.initialize({
  app: app,
  sequelize: database
});

// Create REST resource
var userResource = finale.resource({
  model: User,
  endpoints: ['/users', '/users/:id']
});

// Create database and listen
database
  .sync({ force: true })
  .then(function() {
    server.listen(function() {
      var host = server.address().address,
          port = server.address().port;

      console.log('listening at http://%s:%s', host, port);
    });
  });

Migrate from Epilogue

Finale is built to be a drop-in replacement for Epilogue that supports Sequelize 4.x.x

const epilogue = require('epilogue')
epilogue.initialize(...)

// change to

const finale = require('finale-rest')
finale.initialize(...)

Controllers and endpoints

On the server we now have the following controllers and endpoints:

Controller Endpoint Description
userResource.create POST /users Create a user
userResource.list GET /users Get a listing of users
userResource.read GET /users/:id Get details about a user
userResource.update PUT /users/:id Update a user
userResource.delete DELETE /users/:id Delete a user

Customize behavior

Of course it's likely that we'll want more flexibility. Our users resource has properties for each of the controller actions. Controller actions in turn have hooks for setting and overriding behavior at each step of the request. We have these milestones to work with: start, auth, fetch, data, write, send, and complete.

var ForbiddenError = require('finale-rest').Errors.ForbiddenError;

// disallow deletes on users
userResource.delete.auth(function(req, res, context) {
    throw new ForbiddenError("can't delete a user");
    // optionally:
    // return context.error(403, "can't delete a user");
})

We can set behavior for milestones directly as above, or we can add functionality before and after milestones too:

// check the cache first
userResource.list.fetch.before(function(req, res, context) {
	var instance = cache.get(context.criteria);

	if (instance) {
		// keep a reference to the instance and skip the fetch
		context.instance = instance;
		return context.skip;
	} else {
		// cache miss; we continue on
		return context.continue;
	}
})

Milestones can also be defined in a declarative fashion, and used as middleware with any resource. For example:

// my-middleware.js
module.exports = {
  create: {
    fetch: function(req, res, context) {
      // manipulate the fetch call
      return context.continue;
    }
  },
  list: {
    write: {
      before: function(req, res, context) {
        // modify data before writing list data
        return context.continue;
      },
      action: function(req, res, context) {
        // change behavior of actually writing the data
        return context.continue;
      },
      after: function(req, res, context) {
        // set some sort of flag after writing list data
        return context.continue;
      }
    }
  }
};

// my-app.js
var finale = require('finale-rest'),
    restMiddleware = require('my-middleware');

finale.initialize({
    app: app,
    sequelize: sequelize
});

var userResource = finale.resource({
    model: User,
    endpoints: ['/users', '/users/:id']
});

userResource.use(restMiddleware);

Finale middleware also supports bundling in extra resource configuration by specifying an "extraConfiguration" member of the middleware like so:

// my-middleware.js
module.exports = {
  extraConfiguration: function(resource) {
    // support delete for plural form of a resource
    var app = resource.app;
    app.del(resource.endpoints.plural, function(req, res) {
      resource.controllers.delete._control(req, res);
    });
  }
};

To show an error and halt execution of milestone functions you can throw an error:

var ForbiddenError = require('finale-rest').Errors.ForbiddenError;

before: function(req, res, context) {
    return authenticate.then(function(authed) {
        if(!authed) throw new ForbiddenError();

        return context.continue;
    });
}

REST API

Listing resources support filtering, searching, sorting, and pagination as described below.

Filtering

Add query parameters named after fields to limit results.

$ curl http://localhost/users?name=James+Conrad

HTTP/1.1 200 OK
Content-Type: application/json

[
  {
    "name": "James Conrad",
    "email": "[email protected]"
  }
]

If your query specifies associations to be included โ€“ whether via a model scope (see below), manipulation of Finale's Context object in a custom Milestone handler, or simply by default in your Finale resource definition โ€“ your query parameters can reference fields on the joined models, e.g.

$ curl http://localhost/users?group.type=vip

Filtering using scope

Use scope to add additional filtering (More about scopes in sequelize - http://docs.sequelizejs.com/en/latest/docs/scopes/).

  // Define scope in model
  ...
  scope: {
    verified: {
      where : {
        email_verified: true
        phone_verified: true
      }  
    }
  }
$ curl http://localhost/users?scope=verified

HTTP/1.1 200 OK
Content-Type: application/json

[
  {
    "name": "James Conrad",
    "email": "[email protected]"
    "email_verified": true,
    "phone_verified": true
  }
]

Search

Use the q parameter to perform a substring search across all fields.

$ curl http://localhost/users?q=james

HTTP/1.1 200 OK
Content-Type: application/json

[
  {
    "name": "James Conrad",
    "email": "[email protected]"
  }, {
    "name": "Jim Huntington",
    "email": "[email protected]"
  }
]

Search behavior can be customized to change the parameter used for searching, as well as which attributes are included in the search, like so:

var userResource = finale.resource({
    model: User,
    endpoints: ['/users', '/users/:id'],
    search: {
      param: 'searchOnlyUsernames',
      attributes: [ 'username' ]
    }
});

This would restrict substring searches to the username attribute of the User model, and the search parameter would be 'searchOnlyUsernames':

$ curl http://localhost/users?searchOnlyUsernames=james

By default, the substring search is performed using a {field} LIKE '%{query}%' pattern. However, this behavior can be customized by specifying a search operator. Valid operators include: Op.like (default), Op.iLike, Op.notLike, Op.notILike, Op.ne, Op.eq, Op.not, Op.gte, Op.gt, Op.lte, Op.lt. All "*like" operators can only be used against Sequelize.STRING or Sequelize.TEXT fields. For instance:

var userResource = finale.resource({
    model: User,
    endpoints: ['/users', '/users/:id'],
    search: {
      operator: Sequelize.Op.gt,
      attributes: [ 'age' ]
    }
});

When querying against a Sequelize.BOOLEAN field, you'll need to use the Op.eq operator. You can also add multiple search parameters by passing the search key an array of objects:

var userResource = finale.resource({
    model: User,
    endpoints: ['/users', '/users/:id'],
    search: [
      {operator: Sequelize.Op.eq, param: 'emailVerified', attributes: [ 'email_verified' ]},
      {param: 'searchOnlyUsernames', attributes: [ 'username' ]}
    ] 
});

Sorting

Specify the sort parameter to sort results. Values are field names, optionally preceded by a - to indicate descending order. Multiple sort values may be separated by ,.

$ curl http://localhost/users?sort=-name

HTTP/1.1 200 OK
Content-Type: application/json

[
  {
    "name": "Jim Huntington",
    "email": "[email protected]"
  }, {
    "name": "James Conrad",
    "email": "[email protected]"
  }
]

Sort behavior can be customized to change the parameter used for sorting, as well as which attributes are allowed to be used for sorting like so:

var userResource = finale.resource({
    model: User,
    endpoints: ['/users', '/users/:id'],
    sort: {
      param: 'orderby',
      attributes: [ 'username' ]
    }
});

This would restrict sorting to only the username attribute of the User model, and the sort parameter would be 'orderby':

$ curl http://localhost/users?orderby=username

Default sort criteria can be defined with the default attribute. The expected format for default sort criteria is exactly the same as if it was proceeding the sort parameter in the URL.

var userResource = finale.resource({
    model: User,
    endpoints: ['/users', '/users/:id'],
    sort: {
      default: '-email,username'
    }
});

With this configuration, these two calls would result in the same data:

$ curl http://localhost/users
$ curl http://localhost/users?sort=-email,username

Note that the sort parameter in the URL will override your default criteria.

By default all attributes defined on the model are allowed to be sorted on. Sorting on a attribute not allowed will cause a 400 error to be returned with errors in the format:

$ curl http://localhost/users?sortby=invalid,-otherinvalid,valid

HTTP/1.1 400 BAD REQUEST
Content-Type: application/json

{
  "message": "Sorting not allowed on given attributes",
  "errors": ["invalid", "otherinvalid"]
}

Pagination

List routes support pagination via offset or page and count query parameters. Find metadata about pagination and number of results in the Content-Range response header. Pagination defaults to a default of 100 results per page, and a maximum of 1000 results per page.

# get the third page of results
$ curl http://localhost/users?offset=200&count=100

HTTP/1.1 200 OK
Content-Type: application/json
Content-Range: items 200-299/3230

[
  { "name": "James Conrad", ... },
  ...
]

Alternatively, you can specify that pagination is disabled for a given resource by passing false to the pagination property like so:

var userResource = finale.resource({
    model: User,
    endpoints: ['/users', '/users/:id'],
    pagination: false
});

add_to_children on create and update action

For create and update actions, you can provide an add_to_children object to the context. The attributes of add_to_children will be added to all nested child objects sent in the request, overriding any values in the body. This is useful, for example, to inject common attributes from a session, like created_by_user_id or updated_by_user_id, to all children objects in the create body, without having to specify which ones they are specifically. Note: For doing this for top level writes and updates, you can simply specify context.attributes values. add_to_children is just for nested children objects.

finaleResource["create"].write.before(function(req:Request,res:Response,context:any) { 
    let loggedInUserId =  authManager.getLoggedInUserId(req);
    context.add_to_children = {
      updated_by_user_id :  loggedInUserId,
      created_by_user_id :  loggedInUserId
    }
    return context.continue;
  });
}

finaleResource["update"].write.before(function(req:Request,res:Response,context:any) { 
    let loggedInUserId =  authManager.getLoggedInUserId(req);
    context.add_to_children = {
      updated_by_user_id :  loggedInUserId
    }
    return context.continue;
  });
}


This currently is only supported for one level of nesting. It is not recursive.

Deep vs Shallow Payloads

By default, associations are included in read and list payloads. For list and read queries, you can set a shallow boolean on the context to indicate if you want it to include association child objects or not.

userResource["list"].fetch.before(function(req:Request,res:Response,context:any) { 
    context.shallow = true;
    return context.continue;
});

For finer-grain control over which children are included on a per-query basis, you can set context.shallow to true, and also leverage a children query parameter with a pipe-delimited list of associated children to include. children only works if shallow is set to true. The names used in the children query parameter are the as association names when setting up your sequelize models, or the default created by sequelize.

UserModel.belongsToMany(UserGroupModel), { through: UserGroupRelModel,foreignKey: "user_id" });
UserModel.belongsTo(OrganizationModel), { as: "PrimaryOrganization", foreignKey: "primary_organization_id" });
UserModel.belongsToMany(FooModel), { through: FooRelModel,foreignKey: "user_id" });
...
GET /user/?children=UserGroups|PrimaryOrganization

Finale API

initialize()

Set defaults and give finale a reference to your express app. Send the following parameters:

app

A reference to the Express application

base

Prefix to prepend to resource endpoints

updateMethod

HTTP method to use for update routes, one of POST, PUT, or PATCH

resource()

Create a resource and CRUD actions given a Sequelize model and endpoints. Accepts these parameters:

model

Reference to a Sequelize model

endpoints

Specify endpoints as an array with two sinatra-style URL paths in plural and singular form (e.g., ['/users', '/users/:id']).

actions

Create only the specified list of actions for the resource. Options include create, list, read, update, and delete. Defaults to all.

excludeAttributes

Explicitly remove the specified list of attributes from read and list operations

Milestones & Context

Check out the Milestone docs for information on lifecycle hooks that can be used with finale resources, and how to run custom code at various points during a request.

Protecting Finale REST Endpoints

To protect an endpoint, you must use milestones.

In order to protect and endpoint (for example, to require that only a logged in user or user with the appropriate security token can access a resource) you need to use the appropriate milestone hooks.

Below is an example of how to do this with standard Express middleware, which is commonly used to protect resources. Note that the callback functions required by Finale milestones look similar to express middleware, but the third argument (context) is different.

Suppose you have this resource:

var userResource = rest.resource({
    model: User
});

To protect all endpoints, we'll use userResource.all.auth, a hook used to authorize the endpoint before any operation (create, list, etc). Suppose also we have an express middlware function called authorize(req, res, done). This authorize function might for example be a passport strategy such as passport('local').

To authorize the endpoint, you would do this:

userResource.all.auth(function (req, res, context) {
  return new Promise(function(resolve, reject) {
    authorize(req, res, function (arg) {
      if (arg) {
        // Middleware function returned an error; this means the operation
        // should not be authorized.
        res.status(401).send({message: "Unauthorized"});
        resolve(context.stop);
      } else {
        resolve(context.continue);
      }
  });
})

In this code, note that userResource.all.auth is simply reusing the express middleware to do whatever authorization checking your code requires. We are passing a custom done function to the middleware, which resolves a promise as either context.stop or context.continue, indicating to finale whether or not to proceed. Note that in the case where the transaction isn't authorized, finale won't proceed, so it is your responsibility to send a response back to the client.

Protecting sub-resources

When models have assocations between them, to achieve the nested endpoints a la /user/1/UserGroups, finale creates sub-resources. Remember to set authorizations on those sub-resources as well. To get the sub-resources for a particular resource, you can use the string array subResourceNames attribute on the resource. Each name is also the name of an attribute on the resource.

userResource.all.auth(function (req, res, context) {
...
});

for(sub_resource_name in userResource.subResourceNames) {
    userResource[sub_resource_name].all.auth(function (req, res, context) {
      ...
    });
}

Further Information on Protecting Endpoints

The milestone documentation provides many other hooks for finer-grained operations, i.e. permitting all users to list but only some users to delete can be implemented by using the same approach described above, with different milestones.

Tests, Docker, OS X

The test suite requires use of Dtrace, which can be problematic on MacOS/OS X, which limits use of Dtrace. The base Dockerfile can be used to run tests.

docker build -t finale_test ./
docker run finale_test

Note: good errors happen, so stacktraces in the output are not necessarily indicative of a problem.

License

Copyright (C) 2012-2015 David Chester Copyright (C) 2014-2015 Matt Broadstone Copyright (C) 2017 Tom Juszczyk

Permission is hereby granted, free of charge, to any person obtaining a copy of this software and associated documentation files (the "Software"), to deal in the Software without restriction, including without limitation the rights to use, copy, modify, merge, publish, distribute, sublicense, and/or sell copies of the Software, and to permit persons to whom the Software is furnished to do so, subject to the following conditions:

The above copyright notice and this permission notice shall be included in all copies or substantial portions of the Software.

THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE.

finale's People

Contributors

ackerdev avatar adampflug avatar asmodehn avatar bighitbiker3 avatar chaunax avatar chinayuans avatar dchester avatar dependabot[bot] avatar drama-ostrich avatar eluinhost avatar felipemsantana avatar fridus avatar gabeatwork avatar h256 avatar johngiudici avatar jorrit avatar karolisg avatar ktret avatar mbroadst avatar michailpanagiotis avatar moxious avatar mykola-den avatar petekeller2 avatar prayagverma avatar rrozza avatar rrozza-apolitical avatar satyajeetcmm avatar tommybananas avatar treefort avatar wilzbach avatar

Stargazers

 avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar

Watchers

 avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar

finale's Issues

Restify instructions outdated

I opted to follow the README instructions/docs regarding the Restify support, but ran into a few issues due to changes to Restify releases.

Some time ago in prior restify versions, the queryParser() and bodyParser() plugins were moved into restify.plugins, also if making the query where CORS is an issue, they also moved cors support out into middleware package, so a basic example of restify setup now is:

const restify = require('restify');
const corsMiddleware = require('restify-cors-middleware')
const finale = require('finale-rest')

const server = restify.createServer()

const cors = corsMiddleware({
  preflightMaxAge: 5, // Optional
  origins: ['*'], // Should whitelist actual domains in production
})

server.pre(cors.preflight)
server.use(cors.actual)

server.use(restify.plugins.queryParser());
server.use(restify.plugins.bodyParser());

Listing an association's foreign key in `excludeAttributes` breaks `include`

When considering the models:

User (id, name STRING, favoriteSport INT, ...)
Sport (id, name STRING, ...)

If a User resource uses include: [favoriteSport], the resulting output:

{
  id: 1,
  name: 'Student McStudent',
  favoriteSportId: 1,
  favoriteSport: {
    id: 5,
    name: 'Baseball',
    ...
  },
  ...

But by listing favoriteSportId in the excludeAttributes, it also breaks the include.

excludeAttributes on nested endpoints

I managed to exclude attributes on associated models, both via excludeAttributes object, and associations object,
(like in test for #161)

    test.uResource = rest.resource({
      model: test.models.AncientGod,
      associations: true,
      excludeAttributes: {
        own: ['nickname', 'special_power'],
        Activities: ['description']
      },
      endpoints: ['/api/ancientgods', '/api/ancientgods/:id']
    });

or

    test.uResource = rest.resource({
      model: test.models.AncientGod,
      associations: {
        Activities:{
          excludeAttributes: ['description']
        }
      },
      endpoints: ['/api/ancientgods', '/api/ancientgods/:id']
    });

it works if we get the list data from /api/ancientgods or individual from /api/ancientgods/1 both cases, Activities list doesn't have a description field.

BUT if we get the list from /api/ancientgods/1/activities or /api/ancientgods/1/activities/2 the activities will have the excluded description field.

I'm planing to expand the excludeAttributes on the associations resources, so this is mere documentation before a PR

Is sequelize sync mandatory for running finale?

I tried finale on my existing project using sequelize 5 and express 4, but can't get my finale configuration working, but in a new fresh project I tried using your example and it works, I see that the different was in the sequelize.sync is that operations mandatory in order for finale-rest working?

Confusion with endpoints

Under resource() the docs state:

endpoints

Specify endpoints as an array with two sinatra-style URL paths in plural and singular form (e.g., ['/users', '/users/:id']).

That also states plural and singular form, but both forms are given as plural, was it meant to be ['/users', '/user/:id']?

I have the following:

const userResource = finale.resource({
  model: Users,
  endpoints: ['/users',  '/users/:username'],
  actions: ['read']
});

When I define a resource like this, only the 2nd endpoint works(looking up that column for the given value in the URL. The docs imply that I should still be able to query the plain /users route with query params, such as /users?username=polarathene or /users?userid=42, instead I get the following response back:

{"code":"ResourceNotFound","message":"/users does not exist"}

If I remove the 2nd parameterized endpoint, then any query to /users works fine.

No new releases on GitHub since `v1.0.6`

It seems like Finale has had no new releases since v1.0.6, even though, new versions of the module were published on NPM.

The fact that v1.0.6 was created a few years ago, makes the GitHub repo seem a bit dormant.

Instance removed from context on DELETE

For DELETES, in this line https://github.com/tommybananas/finale/blob/master/lib/Controllers/delete.js#L23 the instance is removed from the context, which greatly limits what can be done in hooks after write.

A temp solution I have is backing up the instance in another hook, but this seems to be limiting. Sequelize destroy hooks still retain the instance in memory so that hooks can use information on the newly deleted instance.

The use case here is pretty common, creating an event from the event. Another common use case would be calling a third party service, like S3 to delete related blob items.

Could we either 1) Remove that line 2) Move the instance to something like context.deletedInstance?

Missing use of transactions

I'd like to start a discussion (with my vote for yes) on if we would like Finale to support the use of transactions, either by default or by optional configuration.

My use-case:
I have a Person model, which uses the standard Finale/Sequelize setup. On my Person Sequelize model, I have some after hooks that are defined to publish AWS SNS messages enforming other systems of the updates. If the SNS messages are not published (which would only happen if SNS is down, or if our credentials/setup of it was incorrect), I'd like the whole operation to fail, and rollback. These SNS Publishes have to be on the Sequelize model not the Finale model, in order for the rollbacks to be transactional, as well as to support updates that didn't come from Finale defined routes.

Problem
Sequelize Managed Transactions will handle the above use-case out of the box (http://docs.sequelizejs.com/manual/tutorial/transactions.html#managed-transaction-auto-callback-) but only if you use the transactions, which Finale does not currently have the capability to do so. So errors thrown in the after hooks on my models have no impact on the update. Whereas if I create a set of routes to call the same functionality with a transaction, the errors thrown in the after hooks have the desired effect of triggering the Sequelize auto-rollback feature.

Discussion

  • My main question is if others think this would be helpful for them as well, and if there is any down-side?

If people are interested, then I'm happy to discuss at exactly which step the transaction would be created, and how it would be passed in.

Thanks.

UnhandledPromiseRejectionWarning

I swear that when I threw any kind of Error from inside my middleware, before I went to bed, would result in it being caught by Finale and Finale sending back a JSON response, and terminating the middleware chain.

But today the chain continues even when my auth middleware throws a generic Error and I am met with:

(node:18540) UnhandledPromiseRejectionWarning: Error: You must send an Authorization header
    at verifyToken (C:\Users\robin\Documents\GIT\bergmann\auth.js:10:29)
    at List.finaleMiddleware (C:\Users\robin\Documents\GIT\bergmann\auth.js:32:5)
    at runHookFunction (C:\Users\robin\Documents\GIT\bergmann\node_modules\finale-rest\lib\Controllers\base.js:147:44)
    at tryCatcher (C:\Users\robin\Documents\GIT\bergmann\node_modules\bluebird\js\release\util.js:16:23)
    at Promise._settlePromiseFromHandler (C:\Users\robin\Documents\GIT\bergmann\node_modules\bluebird\js\release\promise.js:547:31)
    at Promise._settlePromise (C:\Users\robin\Documents\GIT\bergmann\node_modules\bluebird\js\release\promise.js:604:18)
    at Promise._settlePromiseCtx (C:\Users\robin\Documents\GIT\bergmann\node_modules\bluebird\js\release\promise.js:641:10)
    at _drainQueueStep (C:\Users\robin\Documents\GIT\bergmann\node_modules\bluebird\js\release\async.js:97:12)
    at _drainQueue (C:\Users\robin\Documents\GIT\bergmann\node_modules\bluebird\js\release\async.js:86:9)
    at Async._drainQueues (C:\Users\robin\Documents\GIT\bergmann\node_modules\bluebird\js\release\async.js:102:5)
    at Immediate.Async.drainQueues (C:\Users\robin\Documents\GIT\bergmann\node_modules\bluebird\js\release\async.js:15:14)
    at runCallback (timers.js:705:18)
    at tryOnImmediate (timers.js:676:5)
    at processImmediate (timers.js:658:5)
(node:18540) UnhandledPromiseRejectionWarning: Unhandled promise rejection. This error originated either by throwing inside of an async function without a catch block, or by rejecting a promise which was not handled with .catch(). (rejection id: 1)
(node:18540) [DEP0018] DeprecationWarning: Unhandled promise rejections are deprecated. In the future, promise rejections that are not handled will terminate the Node.js process with a non-zero exit code.

How can I accept binary files via multer?

I'm doing:


    const audioResource = finale.resource({
        model: db.models.Audio,
        endpoints: ['/audios', '/audios/:id']
    })

    audioResource.use(multer().single("file"))
    audioResource.use(resources.audio)

Where resources.audio is:

module.exports = {
    create: {
        write: {
            before: (req, res, context) => {
                console.log(Object.keys(req))
                console.log(req.body)
                console.log("HERE I AM!")
                console.log(req.file)
            }
        }
    }
}

However, I can't access req.file. Is it possible to set up with the multer middleware?

Is that possible to add a batch remove in route DELETE

Currently the DELETE only remove one record once. Is that possible to add a feature that remove a bunch of record in one DELETE.

Router example: /model/delete/:ids

The ids can be a list of id. Separated by "," or other characters.

Data milestone not implemented?

The documentation mentions a milestone called data, which I believe is supposed to occur when the values of an entity are changed. I'm attempting to perform some logging of updates, and I need to know which properties of the entity have changed. Unfortunately, there appears to be no milestone between the altering of the properties and the saving of the entity. This means I'm unable to use entity.changed() (because it's either not set yet before write or already cleared after write). Am I missing something here?

EDIT

If the above is correct, it seems like the data milestone could still be implemented without any breaking changes by using this order: before fetch, after fetch, before write, before data, after data, after write. This would keep the behavior of the before write and after write the same--it would just add a couple data milestones in-between to allow some custom behavior between the properties changing and the entity being saved.

context.criteria empty on list.fetch.before

I'm trying to implement a cache but following the example on the doc it seems that context.criteria is always null on the list.fetch.before milestone.
The suggested cache implementation pattern is not working.

multiple scope support

feature

I want to use multiple scope that is supported by sequelize.
?scope=scope1,scope2...&and-more-query

you can transform above request this sequelize code.
sequelize.models['name'].scope('scope1').scope('scope2')

workaround.

now I use milestone.

const multipleScopeMilestone = {
  list: {
    fetch: {
      before: function (req, res, context) {
        if (req.query.scope) {
          this.model = this.model.scope('defaultScope').scope(req.query.scope.split(','));
          delete req.query.scope;
        }
        return context.continue;
      }
    }
  }
}

Finale does not support the paranoid option

When creating a resource with Finale, there should be an option to set paranoid to true or false. Sequelize has built in support for soft-deletes with this option, but finale-rest doesn't give you any way to pass it through.

Sorting ignored when obtaining hasMany relationship models

When I have the following setup:

const Group = database.define('groups', {
    name: Sequelize.STRING,
    status: Sequelize.INTEGER,
    userId: {
        type: Sequelize.INTEGER,
        references: {
            model: 'users',
            key: 'id'
        }
    }
});

const User = database.define('users', {
    type: Sequelize.INTEGER,
    name: Sequelize.STRING
});

// Ensure one to many relationships are set-up.
Group.hasMany(User);

finale.resource({
    model: Group,
    endpoints: ['/groups', '/groups/:id'],
    associations: true
});

finale.resource({
    model: User,
    actions: ['list', 'read', 'update'],
    endpoints: ['/users', '/users/:id'],
    sort: {
        default: 'name'
    }
});

If I use a GET to /users, the sorting works as expected and the ORDER BY is seen in the SQL output to the logs. However, if I use a GET to /groups the users array will be sorted by the Id and not using the default sort given.

Please offer suggestions for work-around or to fork and fix.

Thanks!

Pass unhandled errors back to Express

I would like to request that certain errors are passed back to Express so they may be handled by route handlers later. This relates to the following code in base.js:

  hookChain
    .catch(errors.RequestCompleted, _.noop)
    .catch(self.model.sequelize.ValidationError, function(err) {
      var errorList = _.reduce(err.errors, function(result, error) {
        result.push({ field: error.path, message: error.message });
        return result;
      }, []);

      self.error(req, res, new errors.BadRequestError(err.message, errorList, err));
    })
    .catch(errors.FinaleError, function(err) {
      self.error(req, res, err);
    })
    .catch(function(err) {
      self.error(req, res, new errors.FinaleError(500, 'internal error', [err.message], err));
    });

The last catch statement seems to concern all unexpected errors which I would like to handle in Express for logging purposes.

The change seems to be quite simple, I just would like to know if the maintainer is willing to consider such a change. If so, I will make a PR.

PUT does not update associated tables

This issue was copied from the epilogue issues and is, obviously, still a problem in finale.


I'm trying to use PUT to update a model with associations.

I have a Map model which HasMany Layers. Map 4 has one layer in the DB. map.json specifies two layers.

When I use PUT to update map 4, the request succeeds and returns a modified Map:

$ http PUT localhost:1337/object/map/4 < map.json
HTTP/1.1 200 OK
Connection: keep-alive
Content-Length: 514
Content-Type: application/json; charset=utf-8
Date: Tue, 01 Nov 2016 19:03:54 GMT
ETag: W/"202-avvKiF6AQG/hPzW3CJAakQ"
X-Powered-By: Express

{
    "author": "danvk",
    "baseMapStyle": "{}",
    "createdAt": "2016-11-01T16:05:30.086Z",
    "description": "This map has two layers",
    "id": 4,
    "lat": 43.22,
    "layer": [
        {
            "config": "config = {}",
            "description": "It's a really great layer, trust me.",
            "name": "My layer",
            "sqlQuery": "SELECT * FROM dataset",
            "zIndex": 0
        },
        {
            "config": "config = {}",
            "description": "It's an even better layer, trust me.",
            "name": "My layer",
            "sqlQuery": "SELECT * FROM dataset",
            "zIndex": 1
        }
    ],
    "lng": -137.22,
    "name": "My Map",
    "updatedAt": "2016-11-01T19:03:54.447Z",
    "zoomLevel": 12
}

A subsequent GET, however, shows that the new data wasn't written to the database:

$ http GET localhost:1337/object/map/4                
HTTP/1.1 200 OK
Connection: keep-alive
Content-Length: 744
Content-Type: application/json; charset=utf-8
Date: Tue, 01 Nov 2016 19:12:39 GMT
ETag: W/"2e8-bsUeBJOAcTqI1e8ugkbqpg"
X-Powered-By: Express

{
    "author": "danvk",
    "baseMapStyle": "{}",
    "createdAt": "2016-11-01T16:05:30.086Z",
    "description": "This map has two layers",
    "id": 4,
    "lat": 43.22,
    "layer": [
        {
            "SavedMapId": 4,
            "createdAt": "2016-11-01T16:05:30.123Z",
            "description": null,
            "id": 3,
            "name": "Untitled Layer",
            "sqlQuery": "SELECT *\nFROM blah",
            "updatedAt": "2016-11-01T16:05:30.123Z",
            "zIndex": 1
        }
    ],
    "lng": -137.22,
    "name": "My Map",
    "updatedAt": "2016-11-01T19:03:54.447Z",
    "zoomLevel": 12
}

I'm running from HEAD using Postgres.

TypeError: restify.queryParser is not a function

Code like this:
var restify = require('restify'); app = server = restify.createServer() app.use(restify.queryParser()); app.use(restify.bodyParser());

get an error :

app.use(restify.queryParser());
^

TypeError: restify.queryParser is not a function
at Object. (/Users/xxxxxxx/sequelize/finale.js:34:17)
at Module._compile (module.js:635:30)
at Object.Module._extensions..js (module.js:646:10)
at Module.load (module.js:554:32)
at tryModuleLoad (module.js:497:12)
at Function.Module._load (module.js:489:3)
at Function.Module.runMain (module.js:676:10)
at startup (bootstrap_node.js:187:16)
at bootstrap_node.js:608:3

How to use AND in search ?

I'm looking to do something like localhost:8080/api/v0/media?campaign_id=7&name=09. Which should result in every media with campaign_id == 7 and where the name contains "09" (%09%)..

But i'm not able to find how to do it with "epilogue".. when i log the sequelize call i get :
image

What i'm looking to get is :

where: {
    campaign_id: 7,
    name: {
        $like: "%09%"
    }
}

Cannot get $gte or other search operators to work

I've created a finale REST server with these endpoints:

let vehiclereportResource2 = finale.resource({
model: vehiclereport,
endpoints: ['/vehiclereport2', '/vehiclereport2/:id'],
search: {
operator: '$gte',
param: 'headerTime',
attributes: [ 'header_time' ]
},
sort: {
param: 'orderby',
default: 'id'
}
})

In my code, I'm generating this for the queryURL:

queryURL =/vehiclereport2/?count=1&header_source_id=3&headerTime=2017-06-21T05:00:00.000Z

When the SQL is generated from Finale, this is what it looks like:

SELECT "id", "header_time", "header_source_id", "meteorology_pressure", "meteorology_temperature", "meteorology_true_wind_direction", "meteorology_true_wind_speed" FROM "vehiclereport" AS "vehiclereport" WHERE ("vehiclereport"."header_time" = '2018-07-05 05:00:00.000 +00:00') AND "vehiclereport"."header_source_id" = 3 ORDER BY "vehiclereport"."id" ASC LIMIT 1 OFFSET 0;

How do I get the SQL search ">=" operator generated rather than just equal?

README is outdated.

Reason

I'm a newbie and I encountered some issues while I'm following the README. The reason is the outdated API.

What I see

Some APIs are outdated, for example:

app.use(restify.queryParser());
app.use(restify.bodyParser());

should be

app.use(restify.plugins.queryParser());
app.use(restify.plugins.bodyParser());

And importing http module is unnecessary to launch an express app now.

After updating a foreign key, the result data does not include the updated data

Suppose you have 2 tables: cars and brands and each car has an association to brand

After updating a car's brand (setting brandId), and if the brand model is associated in the finale resource() method, the result will show the updated brandId value but the brand object on the model will still show the previous value.

Let's say we have a car that looks like this: { id: 3, name: 'First car', brandId: 7 }
When we fetch it, it includes the brand: { id: 3, name: 'First car', brandId: 7, Brand: { id: 7, name: 'Oldsmobile' } }

Now, if we update brandId to 4, the result will be: { id: 3, name: 'First car', brandId: 4, Brand: { id: 7, name: 'Oldsmobile' } }

The Brand associated object still shows the old data until the next fetch.

TypeScript type definition?

I'm not seeing any typings for this project. Any plans in the works to provide one?

I had put together a typings file for epilogue. Would that be a straightforward migration to finale?

Filtering/sorting by association fields

hi is there any way to filter/sort by association fields?

I have a Project model with FK to User as project manager. I have turned on associations in sequelize and finale but now I want to be able to select all projects with project manager last name being XXXX.

It is achievable via sequelize so I assume it should be somehow possible with epilogue as well?
My resources are defined like that:


var resources = Object.keys(app.get('models'));
resources.pop();

resources.forEach(function (resource) {
   var res = finale.resource({
   model: app.get('models')[resource],
   endpoints: ['/' + resource, '/' + resource + '/:id'],
   associations: true,
 });
});

where models are sequelize models

Can't search for substrings

I have a simple resource described as

  const leafSpectraResource = finale.resource({
    model: db.LeafSpectra,
    endpoints: ['leaf_spectra/', 'leaf_spectra/:id'],
    search: [
      {operator: db.Sequelize.Op.substring, param: 'scientific_name', attributes: ['scientific_name']},
    ]
  })

I have values in the table that have value 'Acer saccharum' for the scientific_name column. If I do a query with http://.../leaf_spectra/?scientific_name=Acer saccharum, I get the proper response. However, if just do http://.../leaf_spectra/?scientific_name=Acer, I get no response. I've tried with substring, iLike and no operators and I always get an empty result unless I specify the entire string. Any idea what I'm doing wrong? With latest versions of Finale and Sequelize.

[Feature] Aliasing associations routes

Having to associate the same model 3 times to the same model with 3 different foreign keys, will not result in the correct association routes. I know that we can't actually use multiple foreign keys because of sequelize's implementation but can we actually have an alias for each route?

Example:

  • Model User { id, name }
  • Model Order { id, salesUserId, purchaserUserId, shopUserId }

Associations
Order.belongsTo(User, {as: 'sales', foreignKey: salesUserId})
Order.belongsTo(User, {as: 'purchaser', foreignKey: purchaserUserId})
Order.belongsTo(User, {as: 'shop', foreignKey: shopUserId})

Result Routes: [/order, /order/:id, /order/:id/user]
Expected Routes: [/order, /order/:id, /order/:id/sales, /order/:id/purchaser, /order/:id/shop]

Multiple Datacenters

I have multiple data centres (MSSQL) which all have the same database, username, passwords and the same table name & schema.

I'm trying to use the same express instance to map different end points that correspond to each Sequalize instance.

servers.forEach(server => {
  let sequelizeInstance = new Sequelize(database, username, password, {
    ...options,
    host: server.address
  });

  let Client = sequelizeInstance.define(
    "Client",
    {
      Client: { primaryKey: true, type: Sequelize.STRING },
      DatabaseName: Sequelize.STRING
    },
    { timestamps: false, freezeTableName: true }
  );

  finale.initialize({
    app: app,
    sequelize: sequelizeInstance
  });

  finale.resource({
    model: Client,
    pagination: false,
    endpoints: [`${server.site}/clients`, `${server.site}/clients/:id`],
    actions: ["list", "read"]
  });

  db[server.site] = {
    sequelize: sequelizeInstance
  };
});

I've also tried setting a base in the initialize():

  finale.initialize({
    app: app,
    base: server.site,
    sequelize: sequelizeInstance
  });

app._router.stack returns:

[ { path: 'nz/clients', method: 'get' },
  { path: 'nz/clients/:id', method: 'get' },
  { path: 'au/clients', method: 'get' },
  { path: 'au/clients/:id', method: 'get' },
  { path: 'ca/clients', method: 'get' },
  { path: 'ca/clients/:id', method: 'get' },
  { path: 'za/clients', method: 'get' },
  { path: 'za/clients/:id', method: 'get' } ]

But performing a GET request to http://localhost:62946/nz/clients returns Cannot GET /nz/clients

I had no trouble using a single instance of Sequalize with finale-rest. What am I doing wrong?

Information needed on advanced filtering

I want to perform advanced filtering like:

  1. Where name is not "James Conrad"
  2. Where name is "James Conrad" or "Ethan Hawke"
  3. Where price is greater than 2000

What kind of URLs (and query parameters) should I use? I believe this is possible but the documentation does not cover it. I tried to use scope as covered in "Filtering using scope", but I get "invalid scope" error

Any suggestions will be appreciated

Error: Can't set headers after they are sent.

path: finale-rest/lib/Controllers/base.js
lines: 193

self.error(req, res, new errors.FinaleError(500, 'internal error', [err.message], err));
if (self.app.name !== 'restify') {
next(err);
}

Do not send the error and use next(err) at the same time.

api/ base route

Hello it's probably a stupid question but im running into clashes with my other project pages.

How do i change the API base link to something other than "/" .. eg. api/

Please help

Ability to sort sub-resources

I'd like to use sequelize ordering on a sub-resource of one of my top-level resources. I think this could be done by enhance the options that "associations" recognizes, when specified as an object. Something like this:

var groupResource = finale.resource({
    model: db.models.Group,
    actions: ['read'],
    associations: {
        Members: {
            order: ['index']
        }  
    }
});

Authentication

Hey, good work at this library. I'm using it and it works well.

About authentication, I realized the params of .auth hook are different of passport params, as I couldnt make

//myHooksAsMiddleware.js
module.exports = {
    create: {
          auth: myCustomPassportAuth
    },
    list: {},
    update: {},
    delete: {},
    read: {}
};

because the 3rd param of myCustomPassportAuth middleware is a next, but finale-rest .auth has a 3rd param that is an object that contains the next function. So I had to change to

//myHooksAsMiddleware.js
module.exports = {
    create: {
          auth: (req, res, context) => myCustomPassportAuth(req, res, context.continue)
    },
    list: {},
    update: {},
    delete: {},
    read: {}
};

Does have some different aproach to do this authentication?

Non Sequelize Resources

I'd like a facility to support non sequelize, generally read only, resources.
I also wish to provide a dictionary of all the end points registered.
It would be useful to be able to manage all these within Finale even if we arent using most of the POST and relational functionality from the ORM. We still get use of milestones and other functionality, and this registry which is of great use in the client,

As background
I am re-implementing my stack and revisiting some issues.
I have a model loader for sequelize, it uses requireDir to read model definition, including the relations, one per file.
As part of my stack I return the list of epillogue resources as a collection of model definitions at the root of the api (/api/), including associations between models, and hence can instantiate a dictionary of model and collection definitions and default instances in the client. I include in that collection some other end points for non sequelize controllers and complex generally purely query only resources. I hacked it up just as an add on array but I will try to submit these functionally limited resources as stubbed out Sequelize model classes to Finale. I will use the same requireDir() and manufacture a class for each of these "special" end points.
I subsequently have Backbone.Dictionary, to read and instantiate models defined in this root dictionary, and , a Marionette base view extension that allows you to just go something like this anywhere. Essentially ALL the intrface is in templates, I dont need a jQuery anchor naming scheme, and code to instantiate models and views. Just glue the template in where you want the stuff to appear.

<ViewPoint 
    model="MyModel"  
   [cursor="{{value}}"] 
   [template="my template"] 
   [collection=true] 
   [clone=true] 
   [view="your own view (from a named list you have instaitaiated)"]> 
or you can have inline handlebars template code here e.g. {{afield}} in my model
</ViewPoint>

Can individual resources be exported for lambda use?

If I have something like:


    const conversationResource = finale.resource({
        model: db.models.Conversation,
        endpoints: ['/conversations', '/conversations/:id'],
        include: [{
            model: db.models.Audio,
            excludeAttributes: ['processingData']

        }]
    })
    conversationResource.list.fetch.before(cacheLib)
    conversationResource.read.fetch.before(cacheLib)
    conversationResource.use(resources.conversation)

Can I simply do module.exports = conversationResource and will it then match the function signature that the lambda expects of (req, res)?

Tests failing: search included model field doesn't work on restify

Hey my recent PR has a test that fails for restify. I noticed that providing a search query like ?model.includedmodel=value allows searching for fields in included models, but apparently that isn't true for restify.

In list.js, in the section to search for extraSearchCriteria, the param key would either be model or model.includedmodel depending on if you're using restify or not. This is due to the way Restify parses the req.query. With Restify the query would be an object with the shape of

{ model: { includedmodel: 'value' } }

and without Restify it will be

{ model.indludedmodel: 'value' }

So I think there's a question here of if this type of search query should be allowed or encouraged at all. For now I'll put up a PR to disable the test for Restify.

Not getting routes

OK, i configurated finale but even if I set a userResource, it give me 404 when accessing http://localhost:3000/users/.

I'm using express+sequelize with express-generator. I've placed configurations under bin/www

#!/usr/bin/env node

/**
 * Module dependencies.
 */

var app = require('../app');
var debug = require('debug')('express-sequelize');
var http = require('http');
var models = require('../models');

var Sequelize = require('sequelize'),
  finale = require('finale-rest');

/**
 * Get port from environment and store in Express.
 */

var port = normalizePort(process.env.PORT || '3000');
app.set('port', port);
/**
 * Create HTTP server.
 */
var server = http.createServer(app);

// Initialize finale
finale.initialize({
  app: app,
  sequelize: models.sequelize
});

// Create REST resource
var userResource = finale.resource({
  model: models.User,
  endpoints: ['/users', '/users/:id']
});

models.sequelize.sync().then(function () {
  /**
   * Listen on provided port, on all network interfaces.
   */
  server.listen(port, function () {
    debug('Express server listening on port ' + server.address().port);
  });
  server.on('error', onError);
  server.on('listening', onListening);
});

/**
 * Normalize a port into a number, string, or false.
 */

function normalizePort(val) {
  var port = parseInt(val, 10);

  if (isNaN(port)) {
    // named pipe
    return val;
  }

  if (port >= 0) {
    // port number
    return port;
  }

  return false;
}

/**
 * Event listener for HTTP server "error" event.
 */

function onError(error) {
  if (error.syscall !== 'listen') {
    throw error;
  }

  var bind = typeof port === 'string'
    ? 'Pipe ' + port
    : 'Port ' + port;

  // handle specific listen errors with friendly messages
  switch (error.code) {
    case 'EACCES':
      console.error(bind + ' requires elevated privileges');
      process.exit(1);
      break;
    case 'EADDRINUSE':
      console.error(bind + ' is already in use');
      process.exit(1);
      break;
    default:
      throw error;
  }
}

/**
 * Event listener for HTTP server "listening" event.
 */

function onListening() {
  var addr = server.address();
  var bind = typeof addr === 'string'
    ? 'pipe ' + addr
    : 'port ' + addr.port;
  debug('Listening on ' + bind);
}

Bug: Nested search uses strict equal instead of like

This bug can be reproduced with the following example:

Child Resource:

      const childEntity = finale.resource({
        model: childSchema,
        endpoints: 'child_entities',
        include: [{
          as: 'parent',
          attributes: ['name'],
          model: parentSchema,
        }],
        sort: {
          attributes: ['parent.name'], // allow sort only by parent.name
          default: 'parent.name', // default sort by parent.name
          param: 'sort',
        },
      });

Endpoint: /child-entity?slug=some-slug&parent.name=some-name

Query executed:

SELECT 
  "child_entities"."id",
  "child_entities"."slug",
  "child_entities"."parentId",
  "child_entities"."createdAt", 
  "child_entity"."updatedAt", 
FROM 
  "child_entities" AS "child_entity" 
  INNER JOIN "parents" AS "parent" ON "child_entity"."parentId" = "parent"."id" 
  AND "parent"."name" = 'some-name' 
WHERE 
  "child_entity"."slug" = 'some-slug' 
ORDER BY 
  "parent.name" ASC 
LIMIT 
  100 OFFSET 0;

The problem originates in the strict use of = at the time of joining the table, see AND "parent"."name" = 'some-name'

Expected query:

SELECT 
  "child_entities"."id",
  "child_entities"."slug",
  "child_entities"."parentId",
  "child_entities"."createdAt", 
  "child_entity"."updatedAt", 
FROM 
  "child_entities" AS "child_entity" 
  INNER JOIN "parents" AS "parent" ON "child_entity"."parentId" = "parent"."id" 
  AND "parent"."name" LIKE '%some-name%' 
WHERE 
  "child_entity"."slug" = 'some-slug' 
ORDER BY 
  "parent.name" ASC 
LIMIT 
  100 OFFSET 0;

Note that the difference at AND "parent"."name" LIKE '%some-name%'

Recommend Projects

  • React photo React

    A declarative, efficient, and flexible JavaScript library for building user interfaces.

  • Vue.js photo Vue.js

    ๐Ÿ–– Vue.js is a progressive, incrementally-adoptable JavaScript framework for building UI on the web.

  • Typescript photo Typescript

    TypeScript is a superset of JavaScript that compiles to clean JavaScript output.

  • TensorFlow photo TensorFlow

    An Open Source Machine Learning Framework for Everyone

  • Django photo Django

    The Web framework for perfectionists with deadlines.

  • D3 photo D3

    Bring data to life with SVG, Canvas and HTML. ๐Ÿ“Š๐Ÿ“ˆ๐ŸŽ‰

Recommend Topics

  • javascript

    JavaScript (JS) is a lightweight interpreted programming language with first-class functions.

  • web

    Some thing interesting about web. New door for the world.

  • server

    A server is a program made to process requests and deliver data to clients.

  • Machine learning

    Machine learning is a way of modeling and interpreting data that allows a piece of software to respond intelligently.

  • Game

    Some thing interesting about game, make everyone happy.

Recommend Org

  • Facebook photo Facebook

    We are working to build community through open source technology. NB: members must have two-factor auth.

  • Microsoft photo Microsoft

    Open source projects and samples from Microsoft.

  • Google photo Google

    Google โค๏ธ Open Source for everyone.

  • D3 photo D3

    Data-Driven Documents codes.