ethanresnick / json-api-example

An example API created with my JSON-API library (http://github.com/ethanresnick/json-api)
31 stars 15 forks source link

the api front end always return a "type" field, lost first letter in name #7

Open futurist opened 8 years ago

futurist commented 8 years ago

the api front end always return a "type" field, lost first letter in name

POST /formtype with below JSON:

{"data":{"type":"formtype","attributes":{"name":"form1"}}}

The returned type become ormtypes as below:

{"links":{"self":"http://localhost:4000/formtype"},"data":{"id":"56779ce958f5c1393e73e1b9","type":"ormtypes","attributes":{"name":"form1","updateAt":"2015-12-21T06:32:09.818Z","createAt":"2015-12-21T06:32:09.817Z","desc":"","mainKey":"_id","title":""},"relationships":{"createBy":{"data":null},"updateBy":{"data":null}}}}

Source code as below:

./common contains a mongoose instance

./models/formtype.js file as below:

'use strict'
var mongoose = require('mongoose')
var common = require('../common')

var _ObjectId = mongoose.Schema.Types.ObjectId
var ObjectId = mongoose.Types.ObjectId
var Mixed = mongoose.Schema.Types.Mixed

var formTypeSchema = mongoose.Schema({

  name:{type:String}, 
  title:{type:String, default:''}, 
  mainKey:{type:String, default:'_id'}, 
  desc:{type:String, default:''},

  createAt:{ type: Date, default: function(){ return new Date() } },
  createBy:{type:_ObjectId , ref:'Person' },

  updateAt:{ type: Date, default: function(){ return new Date() } },
  updateBy:{type:_ObjectId , ref:'Person' },

  template: Mixed,
  dom: Mixed,

})

var formTypeModel = common.db.model('formtype', formTypeSchema)

module.exports = formTypeModel

./apiref/formtype.js file as below:

var common = require('../common')
module.exports = {
  urlTemplates: {
    "self": common.host + "/formtype/{id}",
    "relationship": common.host + "/formtype/{ownerId}/relationships/{path}"
  }
}

index.js file as below:

'use strict';
var path     = require('path')
  , express  = require('express')
  , API      = require('json-api')
  , APIError = API.types.Error
  , mongoose = require('mongoose')
  , common = require('./common')

var models = {
  Formtype: require('./models/formtype'),
}

var adapter = new API.dbAdapters.Mongoose(models);
var registry = new API.ResourceTypeRegistry({
  formtype: require('./apidef/formtype'),
}, { dbAdapter: adapter });

var Controller = new API.controllers.API(registry);

// Initialize the automatic documentation.
var Docs = new API.controllers.Documentation(registry, {name: 'Example API'});

// Initialize the express app + front controller.
var app = express();

var Front = new API.httpStrategies.Express(Controller, Docs);
var apiReqHandler = Front.apiRequest.bind(Front);

// Enable CORS. Note: if you copy this code into production, you may want to
// disable this. See https://en.wikipedia.org/wiki/Cross-origin_resource_sharing
app.use(function(req, res, next) {
  res.set('Access-Control-Allow-Origin', '*');
  next();
})

app.get("/", Front.docsRequest.bind(Front));
app.route("/:type(people|formtype)")
  .get(apiReqHandler).post(apiReqHandler).patch(apiReqHandler);
app.route("/:type(people|formtype)/:id")
  .get(apiReqHandler).patch(apiReqHandler).delete(apiReqHandler);
app.route("/:type(people|formtype)/:id/relationships/:relationship")
  .get(apiReqHandler).post(apiReqHandler).patch(apiReqHandler).delete(apiReqHandler);

app.use(function(req, res, next) {
  Front.sendError(new APIError(404, undefined, 'Not Found'), req, res);
});

app.listen(4000);
ethanresnick commented 8 years ago

The convention this library uses is that model names should be singular and start with a capital letter (e.g. Formtype) and the JSON:API types generated from those models will be plural and lowercase (e.g. formtypes). You can override these conventions if you want to do things differently (see below), but it's usually easier just to follow them.

If you do want to follow them, you need to register your model with with mongoose using a starting capital letter. In other words, it would be:

var formTypeModel = common.db.model('Formtype', formTypeSchema)

instead of

var formTypeModel = common.db.model('formtype', formTypeSchema)

Then, the generated type string will be "formtypes", which is the expected value (and which follows the convention used throughout the JSON:API specification).

If you don't like these conventions, though, you can override them, by subclassing the MongooseAdapter, writing your own implementation of the getModelName and getType functions, and then passing an instance of your subclass to the registry. So, in that case, the code:

var adapter = new API.dbAdapters.Mongoose(models);
var registry = new API.ResourceTypeRegistry({
  formtype: require('./apidef/formtype'),
}, { dbAdapter: adapter });

would become:

var adapter = new CustomMongooseAdapterSubClass(models);
var registry = new API.ResourceTypeRegistry({
  formtype: require('./apidef/formtype'),
}, { dbAdapter: adapter });

Let me know if the approaches above don't work or you have other questions!

futurist commented 8 years ago

Thank you for the quick and detailed reply, it's worked as a charm.

But after tried to subclass the dbAdapters, found it's not very handy to integrate to my existing code coze the whole ES6+Babel thing, but my existing code only use CommonJS.

Can provide an API / option instead of subclassing?

Or, just add option don't change model name, return as is, maybe?

futurist commented 8 years ago

the mongodb collection name also pluralized, how to disable it? e.g. the person model became people collection, if want to keep as is, where can subclassing it? or any option?

ethanresnick commented 8 years ago

But after tried to subclass the dbAdapters, found it's not very handy to integrate to my existing code coze the whole ES6+Babel thing, but my existing code only use CommonJS.

ES6 classes are (for the most part) only sugar around old ES5 patterns, so you don't need to use Babel/ES6 to extend an ES6 class. You just need to create a new constructor function (for the subclass) that has the old constructor function in its prototype chain, and that will return instances that have the old classes' instance methods in their prototype chain. So something like this should work:

function AdapterSubClass() {
  return MongooseAdapter.call(this, arguments);
}

AdapterSubClass.getModelName = function() { /* your custom implementation */ }
AdapterSubClass.getType = function() { /* your custom implementation */ }
Object.setPrototypeOf(AdapterSubClass, MongooseAdapter);
Object.setPrototypeOf(AdapterSubClass.prototype, MongooseAdapter.prototype);

var youCustomizedAdapter = new AdapterSubClass(/* standard mongoose adapter options */)

If your curious about more on how/why that code works, this article might be useful.

However, I just realized that there's actually a much simpler way to do this: the MongooseAdapter class takes as its second argument an inflector, which is then used to do all the pluralization/singularizing. So, if you just pass in an inflector that always returns the string it's given (without pluralizing or singularizing it), that should fix this. In other words:

function identity(it) { return it; }
var adapter = new MongooseAdapter(models, {singular: identity, plural: identity});

Try that and let me know how it goes. I suspect you'll be one of the first users to be using a custom inflector, so you may find a couple bugs (which I'll work to get fixed asap).

the mongodb collection name also pluralized, how to disable it?

This is configured in your mongoose settings for each model and shouldn't have anything to do with the json-api library.

futurist commented 8 years ago

Tried the options:

var adapter = new MongooseAdapter(models, {singular: identity, plural: identity});

all is ok except the model name became uppercase, so person model will became Person and throw an error

"Person" has not been registered with the MongooseAdapter

Stack trace:

at MongooseAdapter.getModel (./node_modules/json-api/build/src/db-adapters/Mongoose/MongooseAdapter.js:426:19)
at DocumentationController.getTypeInfo (./node_modules/json-api/build/src/controllers/Documentation.js:148:27)
at ./node_modules/json-api/build/src/controllers/Documentation.js:84:43
at Array.forEach (native)
at new DocumentationController (./node_modules/json-api/build/src/controllers/Documentation.js:83:31)
at Object.<anonymous> (./index.js:30:12)
ethanresnick commented 8 years ago

ok except the model name became uppercase, so person model will became Person and throw an error

The model name shouldn't be being transformed at all once the identity functions are in use.

Check that you're using the same capitalization in every place where you're passing the model. In other words, the models object that you pass to MongooseAdapter as its first argument should have a "person" as its key, if you earlier named the model person when you registered it with mongoose.model.

futurist commented 8 years ago

checked all person is lowercase in ./index.js , ./model/person.js, ./apidef/person.js When I hardcoded a line in ./node_modules/json-api/build/src/db-adapters/Mongoose/MongooseAdapter.js file

key: "getModelName",
    value: function getModelName(type) {
      return type

......
key: "getType",
    value: function getType(modelName) {
      return modelName

the error gone and things worked.