nails-boilerplate 1.2.0 → 1.2.1
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/README.md +90 -15
- package/lib/controller.js +25 -2
- package/lib/nails.js +18 -12
- package/package.json +2 -2
- package/spec/services/integration/server/controllers/classbased_controller.js +11 -0
- package/spec/services.integration.spec.js +164 -135
- package/templates/public/README.xml +73 -15
- package/templates/server/controllers/home_controller.js +31 -7
package/README.md
CHANGED
|
@@ -2,8 +2,8 @@
|
|
|
2
2
|
|
|
3
3
|
This framework is designed to provide a lightweight, configurable MVC backend
|
|
4
4
|
for node developers. With minimal dependencies, Nails offers a greater
|
|
5
|
-
syntactical familiarity than php alongside the creative freedom of
|
|
6
|
-
solutions like Rails and Django.
|
|
5
|
+
syntactical familiarity than php alongside the creative freedom of well developed
|
|
6
|
+
server framework solutions like Rails and Django.
|
|
7
7
|
|
|
8
8
|
This boilerplate offers the basic necessities to get your MVC site off the ground.
|
|
9
9
|
The modules used in Nails Boilerplate can be easily extended to produce the custom
|
|
@@ -53,7 +53,7 @@ service.js contains information necessary to run your server. By default, it
|
|
|
53
53
|
specifies the port and the location of important libraries. To override these
|
|
54
54
|
values in different runtime environments, add a child object.
|
|
55
55
|
```js
|
|
56
|
-
|
|
56
|
+
export default {
|
|
57
57
|
...
|
|
58
58
|
PORT: 3000,
|
|
59
59
|
PROD: {
|
|
@@ -77,7 +77,7 @@ var service_config = require('nails-boilerplate').config
|
|
|
77
77
|
If the config contains a custom field,
|
|
78
78
|
|
|
79
79
|
```js
|
|
80
|
-
|
|
80
|
+
export default {
|
|
81
81
|
...
|
|
82
82
|
PORT: 3000,
|
|
83
83
|
yourCustomField: 'yourCustomValue'
|
|
@@ -137,7 +137,16 @@ parameters work.
|
|
|
137
137
|
to dynamically route your request to the appropriate handler.
|
|
138
138
|
|
|
139
139
|
#### db.js
|
|
140
|
-
|
|
140
|
+
|
|
141
|
+
Quickly configure your database connection here. Nails comes pre-configured to
|
|
142
|
+
use the sequelize connector, giving your models sequelize support. The initial setup
|
|
143
|
+
uses an in-memory *sqlite3* database. Change the address to change the location and
|
|
144
|
+
version of your desired sql database. Check out [Sequelize](https://sequelize.org)
|
|
145
|
+
for more info.
|
|
146
|
+
|
|
147
|
+
Alternatively, you can configure a connection to MongoDB using the mongoose_connector.js.
|
|
148
|
+
If enabled, models will accept [Mongoose](https://mongoosejs.com/docs/) schemas and will
|
|
149
|
+
be backed by the desired MongoDB. Consider using the in-memory DB during development.
|
|
141
150
|
|
|
142
151
|
## Controller
|
|
143
152
|
|
|
@@ -148,8 +157,10 @@ actions, receiving **params**, **request**, and **response** as arguments.
|
|
|
148
157
|
|
|
149
158
|
For Example:
|
|
150
159
|
``` js
|
|
151
|
-
const Controller = requre("nails-boilerplate").Controller
|
|
152
|
-
|
|
160
|
+
// const Controller = requre("nails-boilerplate").Controller
|
|
161
|
+
import nails from 'nails-boilerplate';
|
|
162
|
+
|
|
163
|
+
class HomeController extends nails.Controller {
|
|
153
164
|
index(params, request, response) {
|
|
154
165
|
// default action
|
|
155
166
|
}
|
|
@@ -158,7 +169,7 @@ class HomeController extends Controller {
|
|
|
158
169
|
// does something then renders a view
|
|
159
170
|
}
|
|
160
171
|
}
|
|
161
|
-
|
|
172
|
+
export default HomeController;
|
|
162
173
|
|
|
163
174
|
function helperMethod() {
|
|
164
175
|
// does something but does not have access to response
|
|
@@ -168,6 +179,42 @@ function helperMethod() {
|
|
|
168
179
|
defines a controller which will match any route to *home#\<action\>*. **index**
|
|
169
180
|
and **signin** are actions which can be used to render a response to the client.
|
|
170
181
|
|
|
182
|
+
### Local Routes
|
|
183
|
+
|
|
184
|
+
You can define a local routing table directly in the controller.
|
|
185
|
+
Local routes take precidence over global routes. All local routes
|
|
186
|
+
are prefixed with the controller name unless they start with '/'.
|
|
187
|
+
For example, in HomeController the following route:
|
|
188
|
+
|
|
189
|
+
`["get", "data", {action: 'getData', json: true}],`
|
|
190
|
+
|
|
191
|
+
will accept GET requests to /home/data and respond with the json
|
|
192
|
+
object returned by the getData function. If the route is changed to:
|
|
193
|
+
|
|
194
|
+
`["get", "/data", {action: 'getData', json: true}],`
|
|
195
|
+
|
|
196
|
+
it will accept GET requests to /data instead. All local routes are
|
|
197
|
+
implicitly routed to their respective parent controllers.
|
|
198
|
+
|
|
199
|
+
```js
|
|
200
|
+
export default class UsersController extends nails.Controller {
|
|
201
|
+
routes = [
|
|
202
|
+
// Routes requests to /absolute/path
|
|
203
|
+
['get', '/absolute/path', {action: 'actionA'}],
|
|
204
|
+
// Routes requests to /users/relative/path
|
|
205
|
+
['get', './relative/path', {action: 'actionB'}],
|
|
206
|
+
// Routes requests to /users/relative/path
|
|
207
|
+
['get', 'relative/path', {action: 'actionB'}],
|
|
208
|
+
]
|
|
209
|
+
|
|
210
|
+
// Handles requests to /absolute/path
|
|
211
|
+
actionA(request, response, params) {}
|
|
212
|
+
|
|
213
|
+
// Handles requests to /users/relative/path
|
|
214
|
+
actionB(request, response, params) {}
|
|
215
|
+
}
|
|
216
|
+
```
|
|
217
|
+
|
|
171
218
|
### Actions
|
|
172
219
|
Actions are used to define how nails should respond to an incoming request.
|
|
173
220
|
If no action has been defined for a route, nails will default to the index
|
|
@@ -218,23 +265,50 @@ overridden to allow for the rendering of views by name.
|
|
|
218
265
|
## Model
|
|
219
266
|
|
|
220
267
|
Models are programmatic representations of data you wish to persist in a
|
|
221
|
-
database.
|
|
268
|
+
database. The constructor for Model accepts two arguments: the `modelName` and an
|
|
269
|
+
`options` object which is passed to the database connector module.
|
|
270
|
+
|
|
271
|
+
### Sequelize Models
|
|
272
|
+
|
|
273
|
+
Sequelize models are subclasses of
|
|
274
|
+
[Sequelize Models][sequelize_model_docs], and come with the `count()`, `findAll()`,
|
|
275
|
+
and `create()` methods, to name a few. You can define your own models by
|
|
276
|
+
extending an instance of the `Model` class provided by Nails:
|
|
277
|
+
|
|
278
|
+
```js
|
|
279
|
+
// const Model = require("nails-boilerplate").Model;
|
|
280
|
+
import nails from 'nails-boilerplate';
|
|
281
|
+
import {DataTypes} from 'sequelize';
|
|
282
|
+
userSchema = {
|
|
283
|
+
name: {type: DataTypes.STRING, allowNull: false},
|
|
284
|
+
email: {type: DataTypes.STRING, allowNull: false}
|
|
285
|
+
};
|
|
286
|
+
export default class User extends new Model("User", userSchema) {
|
|
287
|
+
/**
|
|
288
|
+
* It is not recommended to add helper methods to Sequelize models. Define
|
|
289
|
+
* them in the schema instead.
|
|
290
|
+
*/
|
|
291
|
+
};
|
|
292
|
+
|
|
293
|
+
```
|
|
294
|
+
|
|
295
|
+
### Mongoose Models
|
|
296
|
+
Mongoose models are subclasses of
|
|
222
297
|
[Mongoose Models][mongoose_model_docs], and come with the `save()`, `find()`,
|
|
223
298
|
and `where()` methods, to name a few. You can define your own models by
|
|
224
299
|
extending an instance of the `Model` class provided by Nails:
|
|
225
300
|
|
|
226
301
|
``` js
|
|
227
|
-
const Model = require("nails-boilerplate").Model;
|
|
302
|
+
// const Model = require("nails-boilerplate").Model;
|
|
303
|
+
import nails from 'nails-boilerplate';
|
|
228
304
|
const userSchema = {name: String, email: String};
|
|
229
|
-
|
|
305
|
+
export default class User extends new Model("User", {schema: userSchema}) {
|
|
230
306
|
// Define your helper methods here
|
|
231
307
|
};
|
|
232
308
|
```
|
|
233
309
|
|
|
234
|
-
The
|
|
235
|
-
|
|
236
|
-
Mongoose connector accepts a schema field that is used to describe how
|
|
237
|
-
documents are stored in MongoDB.
|
|
310
|
+
The `schema` option for Mongoose Models accepts a schema field that is used
|
|
311
|
+
to define how documents are stored in MongoDB.
|
|
238
312
|
|
|
239
313
|
### Database Connectors
|
|
240
314
|
|
|
@@ -268,3 +342,4 @@ Enjoy! Feature requests, bug reports, and comments are welcome on github.
|
|
|
268
342
|
[express_routing_docs]: https://expressjs.com/en/guide/routing.html
|
|
269
343
|
[express_request_docs]: https://expressjs.com/en/5x/api.html#req
|
|
270
344
|
[mongoose_model_docs]: https://mongoosejs.com/docs/api/model.html
|
|
345
|
+
[sequelize_model_docs]: https://sequelize.org/docs/v6/core-concepts/model-basics/
|
package/lib/controller.js
CHANGED
|
@@ -21,8 +21,8 @@ const DISABLE_AUTORENDER = new (class DisableAutorender {});
|
|
|
21
21
|
class Controller {
|
|
22
22
|
constructor() {
|
|
23
23
|
var subclassName = this.constructor.name;
|
|
24
|
-
var controllerName =
|
|
25
|
-
subclassName.toLowerCase().replace(/controller$/, '');
|
|
24
|
+
var controllerName = this._getControllerName();
|
|
25
|
+
// subclassName.toLowerCase().replace(/controller$/, '');
|
|
26
26
|
router.removeAllListeners('dispatchTo:' + controllerName);
|
|
27
27
|
router.on('dispatchTo:' + controllerName, this._do.bind(this));
|
|
28
28
|
}
|
|
@@ -55,6 +55,29 @@ class Controller {
|
|
|
55
55
|
return constructed;
|
|
56
56
|
}
|
|
57
57
|
|
|
58
|
+
_getControllerName() {
|
|
59
|
+
return this.constructor.name.toLowerCase().replace(/controller$/, '');
|
|
60
|
+
}
|
|
61
|
+
|
|
62
|
+
/** Initializes local and global routes defined on the Controller subclass */
|
|
63
|
+
_registerControllerRoutes() {
|
|
64
|
+
const controllerName = this._getControllerName();
|
|
65
|
+
if (this.routes && this.routes.length) {
|
|
66
|
+
const localizedRoutes = this.routes.map(route => {
|
|
67
|
+
// TODO: throw an error if :controller is present in local routes
|
|
68
|
+
// TODO: also account for other malformed local routes
|
|
69
|
+
const routePrefix = `/${controllerName}/`;
|
|
70
|
+
const modifiedDestination = route[1][0] == '/'
|
|
71
|
+
? route[1]
|
|
72
|
+
: route[1].startsWith('./') ? route[1].replace('./', routePrefix) : routePrefix + route[1];
|
|
73
|
+
const modifiedOptions = {...route[2]};
|
|
74
|
+
modifiedOptions.controller = controllerName;
|
|
75
|
+
return [route[0], modifiedDestination, modifiedOptions];
|
|
76
|
+
});
|
|
77
|
+
router.addRoutes(localizedRoutes);
|
|
78
|
+
}
|
|
79
|
+
}
|
|
80
|
+
|
|
58
81
|
/**** Network Methods *****/
|
|
59
82
|
/**
|
|
60
83
|
* The main entry function of the controller.
|
package/lib/nails.js
CHANGED
|
@@ -66,7 +66,8 @@ async function configure( app_config ) {
|
|
|
66
66
|
// set up router and controllers
|
|
67
67
|
express_app.set("public_root", app_config.config.PUBLIC_ROOT);
|
|
68
68
|
console.log("Initializing Router...");
|
|
69
|
-
application.router = new Router( app_config.routes || [] );
|
|
69
|
+
// application.router = new Router( app_config.routes || [] );
|
|
70
|
+
application.router = new Router( [] );
|
|
70
71
|
console.log("Application Router initialized");
|
|
71
72
|
|
|
72
73
|
// init models
|
|
@@ -76,7 +77,7 @@ async function configure( app_config ) {
|
|
|
76
77
|
// TODO: deprecate the old Model style
|
|
77
78
|
if (app_config.config.MODELS_ROOT) {
|
|
78
79
|
Model.set_connector(DBConnector, app_config.db);
|
|
79
|
-
init_models(app_config.config.MODELS_ROOT);
|
|
80
|
+
await init_models(app_config.config.MODELS_ROOT);
|
|
80
81
|
}
|
|
81
82
|
// TODO: make this Model style mandatory
|
|
82
83
|
console.log("Connecting to DB...");
|
|
@@ -84,7 +85,7 @@ async function configure( app_config ) {
|
|
|
84
85
|
console.log("Generating model superclass...");
|
|
85
86
|
await DBConnector.connect(app_config.db);
|
|
86
87
|
ModelV2.setConnector(DBConnector);
|
|
87
|
-
init_models_v2(app_config.config.MODELS_ROOT);
|
|
88
|
+
await init_models_v2(app_config.config.MODELS_ROOT);
|
|
88
89
|
DBConnector.afterInitialization();
|
|
89
90
|
} else {
|
|
90
91
|
console.log("Instantiating DBConnector...");
|
|
@@ -99,7 +100,8 @@ async function configure( app_config ) {
|
|
|
99
100
|
Controller.setRouter(application.router);
|
|
100
101
|
application.controller = Controller.extend(ApplicationController);
|
|
101
102
|
console.log('initializing controllers: ', app_config.config.CONTROLLERS_ROOT);
|
|
102
|
-
init_controllers(app_config.config.CONTROLLERS_ROOT);
|
|
103
|
+
await init_controllers(app_config.config.CONTROLLERS_ROOT);
|
|
104
|
+
application.router.addRoutes(app_config.routes);
|
|
103
105
|
};
|
|
104
106
|
|
|
105
107
|
function startServer(config) {
|
|
@@ -145,11 +147,11 @@ function startServer(config) {
|
|
|
145
147
|
}
|
|
146
148
|
|
|
147
149
|
// TODO: create an initializer lib file.
|
|
148
|
-
function init_controllers(controller_lib) {
|
|
149
|
-
|
|
150
|
+
async function init_controllers(controller_lib) {
|
|
151
|
+
await init_app_lib(Controller, controller_lib);
|
|
150
152
|
}
|
|
151
|
-
function init_models(model_lib) {
|
|
152
|
-
|
|
153
|
+
async function init_models(model_lib) {
|
|
154
|
+
await init_app_lib(Model, model_lib);
|
|
153
155
|
}
|
|
154
156
|
async function init_app_lib(superclass, abs_path) {
|
|
155
157
|
console.log('attempting to import:', abs_path);
|
|
@@ -161,11 +163,15 @@ async function init_app_lib(superclass, abs_path) {
|
|
|
161
163
|
if (!superclass.isPrototypeOf(subclass))
|
|
162
164
|
return superclass.extend(subclass);
|
|
163
165
|
// ES6 Class was provided
|
|
164
|
-
|
|
166
|
+
const subInstance = new subclass();
|
|
167
|
+
if (superclass == Controller) {
|
|
168
|
+
subInstance._registerControllerRoutes();
|
|
169
|
+
}
|
|
170
|
+
return subInstance;
|
|
171
|
+
}
|
|
172
|
+
for (const rel_path of fs.readdirSync(abs_path)) {
|
|
173
|
+
await init_app_lib(superclass, path.join(abs_path, rel_path));
|
|
165
174
|
}
|
|
166
|
-
fs.readdirSync(abs_path).forEach(function(rel_path) {
|
|
167
|
-
init_app_lib(superclass, path.join(abs_path, rel_path));
|
|
168
|
-
});
|
|
169
175
|
}
|
|
170
176
|
|
|
171
177
|
async function init_models_v2(abs_path) {
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "nails-boilerplate",
|
|
3
|
-
"version": "1.2.
|
|
3
|
+
"version": "1.2.1",
|
|
4
4
|
"description": "A node.js webserver scaffold",
|
|
5
5
|
"type": "module",
|
|
6
6
|
"main": "index.js",
|
|
@@ -8,7 +8,7 @@
|
|
|
8
8
|
"nails": "./bin/lib/nails.js"
|
|
9
9
|
},
|
|
10
10
|
"scripts": {
|
|
11
|
-
"copyreadme": "
|
|
11
|
+
"copyreadme": "node bin/convertreadmetohtml.js",
|
|
12
12
|
"test": "mocha --exit spec",
|
|
13
13
|
"debug": "mocha --exit debug spec"
|
|
14
14
|
},
|
|
@@ -1,6 +1,11 @@
|
|
|
1
1
|
import nails from "../../../../../index.js";
|
|
2
2
|
// require("../../../../../index.js").Controller;
|
|
3
3
|
export default class ClassbasedController extends nails.Controller {
|
|
4
|
+
routes = [
|
|
5
|
+
["get", "./arbi/trary/testLocalRoutes", {action: 'testLocalRoutes', json: true}],
|
|
6
|
+
["get", "arbi00/trary00/testLocalRoutes", {action: 'testLocalRoutes', json: true}],
|
|
7
|
+
["get", "/cl4ssb4sed/arbi/trary/testLocalRoutes", {action: 'testLocalRoutes', json: true}],
|
|
8
|
+
];
|
|
4
9
|
// DO NOT OVERRIDE CONSTRUCTOR
|
|
5
10
|
index(params, request, response) {
|
|
6
11
|
response.json({
|
|
@@ -19,4 +24,10 @@ export default class ClassbasedController extends nails.Controller {
|
|
|
19
24
|
classbased_testpromise: true
|
|
20
25
|
});
|
|
21
26
|
}
|
|
27
|
+
|
|
28
|
+
testLocalRoutes(params, request, response) {
|
|
29
|
+
return {
|
|
30
|
+
testLocalRoutes: true
|
|
31
|
+
};
|
|
32
|
+
}
|
|
22
33
|
}
|
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
// Import the dependencies for testing
|
|
2
2
|
import * as chai from 'chai';
|
|
3
|
-
import {default as chaiHttp, request} from "chai-http";
|
|
3
|
+
import { default as chaiHttp, request } from "chai-http";
|
|
4
4
|
// import chaiHttp from 'chai-http';
|
|
5
5
|
import { assert } from 'chai';
|
|
6
6
|
// import { WebSocket } from 'ws';
|
|
@@ -15,110 +15,139 @@ let mongod = null;
|
|
|
15
15
|
chai.use(chaiHttp);
|
|
16
16
|
chai.should();
|
|
17
17
|
|
|
18
|
-
describe("Integration", function() {
|
|
19
|
-
before(async function() {
|
|
20
|
-
|
|
21
|
-
|
|
22
|
-
|
|
23
|
-
|
|
24
|
-
|
|
25
|
-
|
|
26
|
-
|
|
27
|
-
|
|
28
|
-
|
|
29
|
-
|
|
30
|
-
|
|
31
|
-
|
|
32
|
-
|
|
33
|
-
});
|
|
18
|
+
describe("Integration", function () {
|
|
19
|
+
before(async function () {
|
|
20
|
+
// this.timeout(30000);
|
|
21
|
+
try {
|
|
22
|
+
var nails = (await import('./services/integration/server.js')).default;
|
|
23
|
+
} catch (e) {
|
|
24
|
+
console.log("could not import server");
|
|
25
|
+
console.log(e);
|
|
26
|
+
}
|
|
27
|
+
console.log("got here");
|
|
28
|
+
express_app = nails.application;
|
|
29
|
+
return new Promise((resolve, reject) => {
|
|
30
|
+
nails.events.on("ready", () => {
|
|
31
|
+
console.log("ready was emitted!");
|
|
32
|
+
resolve();
|
|
34
33
|
});
|
|
34
|
+
});
|
|
35
35
|
// })
|
|
36
36
|
// MongoMemoryServer.create({instance: service_config.db}).then((mongodb) => {
|
|
37
37
|
// mongod = mongodb;
|
|
38
|
-
|
|
38
|
+
|
|
39
39
|
// });
|
|
40
40
|
});
|
|
41
|
-
describe("GET /", function() {
|
|
42
|
-
it('should return the expected JSON from index', function(done) {
|
|
41
|
+
describe("GET /", function () {
|
|
42
|
+
it('should return the expected JSON from index', function (done) {
|
|
43
43
|
request.execute(express_app)
|
|
44
|
-
|
|
45
|
-
|
|
46
|
-
|
|
47
|
-
|
|
48
|
-
|
|
49
|
-
|
|
44
|
+
.get('/')
|
|
45
|
+
.end((err, res) => {
|
|
46
|
+
res.should.have.status(200);
|
|
47
|
+
assert(res.text == JSON.stringify({ home_index: true }));
|
|
48
|
+
done();
|
|
49
|
+
});
|
|
50
50
|
});
|
|
51
51
|
});
|
|
52
|
-
describe("GET /classbased", function() {
|
|
53
|
-
it('should return the expected JSON from index', function(done) {
|
|
52
|
+
describe("GET /classbased", function () {
|
|
53
|
+
it('should return the expected JSON from index', function (done) {
|
|
54
54
|
request.execute(express_app)
|
|
55
|
-
|
|
56
|
-
|
|
57
|
-
|
|
58
|
-
|
|
59
|
-
|
|
60
|
-
|
|
55
|
+
.get('/classbased')
|
|
56
|
+
.end((err, res) => {
|
|
57
|
+
res.should.have.status(200);
|
|
58
|
+
assert(res.text == JSON.stringify({ classbased_index: true }));
|
|
59
|
+
done();
|
|
60
|
+
});
|
|
61
61
|
});
|
|
62
62
|
});
|
|
63
|
-
describe("
|
|
64
|
-
it
|
|
63
|
+
describe("Get /classbased/arbi/trary/testLocalRoutes", function () {
|
|
64
|
+
it("Should respect the defined local route and return the expected JSON", function (done) {
|
|
65
65
|
request.execute(express_app)
|
|
66
|
-
|
|
67
|
-
|
|
68
|
-
|
|
69
|
-
|
|
70
|
-
|
|
71
|
-
|
|
66
|
+
.get('/classbased/arbi00/trary00/testLocalRoutes')
|
|
67
|
+
.end((err, res) => {
|
|
68
|
+
res.should.have.status(200);
|
|
69
|
+
assert(res.text == JSON.stringify({ testLocalRoutes: true }));
|
|
70
|
+
done();
|
|
71
|
+
});
|
|
72
72
|
});
|
|
73
|
-
it(
|
|
73
|
+
it("Should not rewrite local route prefix and return the expected JSON", function (done) {
|
|
74
74
|
request.execute(express_app)
|
|
75
|
-
|
|
76
|
-
|
|
77
|
-
|
|
78
|
-
|
|
79
|
-
|
|
80
|
-
|
|
75
|
+
.get('/cl4ssb4sed/arbi/trary/testLocalRoutes')
|
|
76
|
+
.end((err, res) => {
|
|
77
|
+
res.should.have.status(200);
|
|
78
|
+
assert(res.text == JSON.stringify({ testLocalRoutes: true }));
|
|
79
|
+
done();
|
|
80
|
+
});
|
|
81
81
|
});
|
|
82
|
-
it(
|
|
82
|
+
it("Should rewrite local route prefix, './' and return the expected JSON", function (done) {
|
|
83
83
|
request.execute(express_app)
|
|
84
|
-
|
|
85
|
-
|
|
86
|
-
|
|
87
|
-
|
|
88
|
-
|
|
89
|
-
|
|
84
|
+
.get('/classbased/arbi/trary/testLocalRoutes')
|
|
85
|
+
.end((err, res) => {
|
|
86
|
+
res.should.have.status(200);
|
|
87
|
+
assert(res.text == JSON.stringify({ testLocalRoutes: true }));
|
|
88
|
+
done();
|
|
89
|
+
});
|
|
90
|
+
});
|
|
91
|
+
});
|
|
92
|
+
describe("GET /^\\/(\\w+)\\/(\\w+)$/i", function () {
|
|
93
|
+
it('should route to home_controller#testaction', function (done) {
|
|
94
|
+
request.execute(express_app)
|
|
95
|
+
.get('/home/testaction')
|
|
96
|
+
.end((err, res) => {
|
|
97
|
+
res.should.have.status(200);
|
|
98
|
+
assert(res.text == JSON.stringify({ home_testaction: true }));
|
|
99
|
+
done();
|
|
100
|
+
});
|
|
101
|
+
});
|
|
102
|
+
it('should route to classbased_controller#testaction', function (done) {
|
|
103
|
+
request.execute(express_app)
|
|
104
|
+
.get('/classbased/testaction')
|
|
105
|
+
.end((err, res) => {
|
|
106
|
+
res.should.have.status(200);
|
|
107
|
+
assert(res.text == JSON.stringify({ classbased_testaction: true }));
|
|
108
|
+
done();
|
|
109
|
+
});
|
|
110
|
+
});
|
|
111
|
+
it('should render correctly when a promise is returned', function (done) {
|
|
112
|
+
request.execute(express_app)
|
|
113
|
+
.get('/classbased/testpromise')
|
|
114
|
+
.end((err, res) => {
|
|
115
|
+
res.should.have.status(200);
|
|
116
|
+
assert(res.text == JSON.stringify({ classbased_testpromise: true }));
|
|
117
|
+
done();
|
|
118
|
+
});
|
|
90
119
|
})
|
|
91
120
|
});
|
|
92
121
|
|
|
93
|
-
describe("GET /error/fivehundred", function() {
|
|
94
|
-
it('should log the stack trace', function(done) {
|
|
122
|
+
describe("GET /error/fivehundred", function () {
|
|
123
|
+
it('should log the stack trace', function (done) {
|
|
95
124
|
request.execute(express_app)
|
|
96
|
-
|
|
97
|
-
|
|
98
|
-
|
|
99
|
-
|
|
100
|
-
|
|
101
|
-
|
|
102
|
-
|
|
103
|
-
|
|
125
|
+
.get('/error/fivehundred/500')
|
|
126
|
+
.end((err, res) => {
|
|
127
|
+
res.should.have.status(500);
|
|
128
|
+
//console.log(res.text);
|
|
129
|
+
//console.log(res);
|
|
130
|
+
//console.log(err);
|
|
131
|
+
done();
|
|
132
|
+
})
|
|
104
133
|
});
|
|
105
|
-
it('should return meaningful JSON', function(done) {
|
|
134
|
+
it('should return meaningful JSON', function (done) {
|
|
106
135
|
request.execute(express_app)
|
|
107
|
-
|
|
108
|
-
|
|
109
|
-
|
|
110
|
-
|
|
111
|
-
|
|
112
|
-
|
|
113
|
-
|
|
114
|
-
|
|
115
|
-
|
|
116
|
-
|
|
136
|
+
.get('/error/fivehundred')
|
|
137
|
+
.end((err, res) => {
|
|
138
|
+
res.should.have.status(500);
|
|
139
|
+
console.log("ZZZZZZZZZZ");
|
|
140
|
+
console.log(res.text);
|
|
141
|
+
//console.log(res.text);
|
|
142
|
+
//console.log(res);
|
|
143
|
+
//console.log(err);
|
|
144
|
+
done();
|
|
145
|
+
})
|
|
117
146
|
});
|
|
118
147
|
});
|
|
119
148
|
|
|
120
|
-
describe("WebSockets", function() {
|
|
121
|
-
it("should listen on /", function(done) {
|
|
149
|
+
describe("WebSockets", function () {
|
|
150
|
+
it("should listen on /", function (done) {
|
|
122
151
|
this.timeout(2000);
|
|
123
152
|
var requester = request.execute(express_app).keepOpen();
|
|
124
153
|
var wsClient = new WebSocket("ws://localhost:3333/");
|
|
@@ -128,17 +157,17 @@ describe("Integration", function() {
|
|
|
128
157
|
assert(message == "It worked");
|
|
129
158
|
});
|
|
130
159
|
});
|
|
131
|
-
it("should listen on /voodoo", function(done) {
|
|
160
|
+
it("should listen on /voodoo", function (done) {
|
|
132
161
|
this.timeout(2000);
|
|
133
162
|
var requester = request.execute(express_app).keepOpen();
|
|
134
163
|
var wsClient = new WebSocket("ws://localhost:3333/voodoo");
|
|
135
164
|
|
|
136
165
|
wsClient.on('message', (message) => {
|
|
137
|
-
|
|
166
|
+
assert(message == "Voodoo worked");
|
|
138
167
|
});
|
|
139
168
|
wsClient.on('close', (code, reason) => done());
|
|
140
169
|
});
|
|
141
|
-
it("should not listen on /voodootwo", function(done) {
|
|
170
|
+
it("should not listen on /voodootwo", function (done) {
|
|
142
171
|
// TODO: this doesn't fail. Figure out why.
|
|
143
172
|
//done();
|
|
144
173
|
this.timeout(2000);
|
|
@@ -157,9 +186,9 @@ describe("Integration", function() {
|
|
|
157
186
|
it("should correctly handle dynamic controller and action");
|
|
158
187
|
});
|
|
159
188
|
|
|
160
|
-
describe("GET /json/:action", function() {
|
|
189
|
+
describe("GET /json/:action", function () {
|
|
161
190
|
it('should render params if nothing is returned by the action',
|
|
162
|
-
function(done) {
|
|
191
|
+
function (done) {
|
|
163
192
|
request.execute(express_app)
|
|
164
193
|
// Route to index action.
|
|
165
194
|
.get('/json/testparams?testkey=testvalue')
|
|
@@ -170,79 +199,79 @@ describe("Integration", function() {
|
|
|
170
199
|
});
|
|
171
200
|
}
|
|
172
201
|
);
|
|
173
|
-
it('should render the returned object as json', function(done) {
|
|
202
|
+
it('should render the returned object as json', function (done) {
|
|
174
203
|
request.execute(express_app)
|
|
175
|
-
|
|
204
|
+
.get('/json/testaction')
|
|
205
|
+
.end((err, res) => {
|
|
206
|
+
res.should.have.status(200);
|
|
207
|
+
assert(res.text == JSON.stringify({ json_testaction: true }));
|
|
208
|
+
done();
|
|
209
|
+
});
|
|
210
|
+
});
|
|
211
|
+
it('should asynchronously render the resolved promise as json',
|
|
212
|
+
function (done) {
|
|
213
|
+
request.execute(express_app)
|
|
214
|
+
.get('/json/testpromise')
|
|
176
215
|
.end((err, res) => {
|
|
177
216
|
res.should.have.status(200);
|
|
178
|
-
assert(res.text == JSON.stringify({
|
|
217
|
+
assert(res.text == JSON.stringify({ json_testpromise: true }));
|
|
179
218
|
done();
|
|
180
219
|
});
|
|
181
|
-
});
|
|
182
|
-
it('should asynchronously render the resolved promise as json',
|
|
183
|
-
function(done) {
|
|
184
|
-
request.execute(express_app)
|
|
185
|
-
.get('/json/testpromise')
|
|
186
|
-
.end((err, res) => {
|
|
187
|
-
res.should.have.status(200);
|
|
188
|
-
assert(res.text == JSON.stringify({json_testpromise: true}));
|
|
189
|
-
done();
|
|
190
|
-
});
|
|
191
220
|
}
|
|
192
221
|
);
|
|
193
222
|
});
|
|
194
|
-
describe("Get /ManualRenderAsync", function() {
|
|
223
|
+
describe("Get /ManualRenderAsync", function () {
|
|
195
224
|
it("./testmanualrenderasync Should not throw an exception after manually"
|
|
196
|
-
|
|
225
|
+
+ " rendering json asynchronously",
|
|
197
226
|
done => {
|
|
198
227
|
request.execute(express_app)
|
|
199
|
-
|
|
200
|
-
|
|
201
|
-
|
|
202
|
-
|
|
203
|
-
|
|
204
|
-
|
|
205
|
-
|
|
228
|
+
.get("/manualrenderasync/testmanualrenderasync")
|
|
229
|
+
.end((err, res) => {
|
|
230
|
+
res.should.have.status(200);
|
|
231
|
+
assert(res.text ==
|
|
232
|
+
JSON.stringify({ json_testmanualrenderasync: true }));
|
|
233
|
+
done();
|
|
234
|
+
});
|
|
206
235
|
});
|
|
207
236
|
|
|
208
|
-
|
|
209
|
-
|
|
210
|
-
|
|
211
|
-
|
|
212
|
-
|
|
213
|
-
|
|
214
|
-
|
|
215
|
-
|
|
216
|
-
|
|
217
|
-
|
|
218
|
-
|
|
219
|
-
|
|
220
|
-
|
|
237
|
+
it("./testmanualrenderexplicitasync Should not throw an exception after"
|
|
238
|
+
+ " manually rendering json asynchronously using the async function"
|
|
239
|
+
+ " tag",
|
|
240
|
+
done => {
|
|
241
|
+
request.execute(express_app)
|
|
242
|
+
.get("/manualrenderasync/testmanualrenderexplicitasync")
|
|
243
|
+
.end((err, res) => {
|
|
244
|
+
res.should.have.status(200);
|
|
245
|
+
assert(res.text ==
|
|
246
|
+
JSON.stringify({ json_testmanualrenderexplicitasync: true }));
|
|
247
|
+
done();
|
|
248
|
+
});
|
|
249
|
+
});
|
|
221
250
|
|
|
222
251
|
});
|
|
223
|
-
describe("Mongoose Model", function() {
|
|
224
|
-
beforeEach(async function() {
|
|
252
|
+
describe("Mongoose Model", function () {
|
|
253
|
+
beforeEach(async function () {
|
|
225
254
|
// Used to initialize mongod here
|
|
226
255
|
});
|
|
227
|
-
afterEach(async function() {
|
|
256
|
+
afterEach(async function () {
|
|
228
257
|
//await mongod.stop();
|
|
229
258
|
});
|
|
230
|
-
it('should save to the correct database', function(done) {
|
|
259
|
+
it('should save to the correct database', function (done) {
|
|
231
260
|
let dogName = "Penny";
|
|
232
261
|
let dogId = null;
|
|
233
262
|
request.execute(express_app)
|
|
234
|
-
|
|
235
|
-
|
|
236
|
-
|
|
237
|
-
|
|
238
|
-
|
|
239
|
-
|
|
240
|
-
|
|
241
|
-
|
|
242
|
-
|
|
243
|
-
|
|
244
|
-
|
|
245
|
-
|
|
263
|
+
.get(`/modeltest/createdog?name=${dogName}`)
|
|
264
|
+
.end((err, res) => {
|
|
265
|
+
res.should.have.status(200);
|
|
266
|
+
dogId = res.text.replaceAll('"', '');
|
|
267
|
+
request.execute(express_app)
|
|
268
|
+
.get(`/modeltest/getdogbyid?id=${dogId}`)
|
|
269
|
+
.end((err, res) => {
|
|
270
|
+
assert.equal(
|
|
271
|
+
res.text, JSON.stringify({ name: dogName, good: true }));
|
|
272
|
+
done();
|
|
273
|
+
});
|
|
274
|
+
});
|
|
246
275
|
});
|
|
247
276
|
|
|
248
277
|
});
|
|
@@ -1,8 +1,8 @@
|
|
|
1
1
|
<h1 id="nailsboilerplateanodewebserviceframework">Nails-Boilerplate: A Node Webservice Framework</h1>
|
|
2
2
|
<p>This framework is designed to provide a lightweight, configurable MVC backend
|
|
3
3
|
for node developers. With minimal dependencies, Nails offers a greater
|
|
4
|
-
syntactical familiarity than php alongside the creative freedom of
|
|
5
|
-
solutions like Rails and Django.</p>
|
|
4
|
+
syntactical familiarity than php alongside the creative freedom of well developed
|
|
5
|
+
server framework solutions like Rails and Django.</p>
|
|
6
6
|
<p>This boilerplate offers the basic necessities to get your MVC site off the ground.
|
|
7
7
|
The modules used in Nails Boilerplate can be easily extended to produce the custom
|
|
8
8
|
functionality to fit your needs, and you are encouraged to do so.</p>
|
|
@@ -37,7 +37,7 @@ help you tailor your service to your needs.</p>
|
|
|
37
37
|
<p>service.js contains information necessary to run your server. By default, it
|
|
38
38
|
specifies the port and the location of important libraries. To override these
|
|
39
39
|
values in different runtime environments, add a child object.</p>
|
|
40
|
-
<pre><code class="js language-js">
|
|
40
|
+
<pre><code class="js language-js">export default {
|
|
41
41
|
...
|
|
42
42
|
PORT: 3000,
|
|
43
43
|
PROD: {
|
|
@@ -54,7 +54,7 @@ module:</p>
|
|
|
54
54
|
<pre><code class="js language-js">var service_config = require('nails-boilerplate').config
|
|
55
55
|
</code></pre>
|
|
56
56
|
<p>If the config contains a custom field,</p>
|
|
57
|
-
<pre><code class="js language-js">
|
|
57
|
+
<pre><code class="js language-js">export default {
|
|
58
58
|
...
|
|
59
59
|
PORT: 3000,
|
|
60
60
|
yourCustomField: 'yourCustomValue'
|
|
@@ -107,15 +107,24 @@ than indices. You can name your regex captures "controller" and/or "action"
|
|
|
107
107
|
to dynamically route your request to the appropriate handler.</li>
|
|
108
108
|
</ul>
|
|
109
109
|
<h4 id="dbjs">db.js</h4>
|
|
110
|
-
<p
|
|
110
|
+
<p>Quickly configure your database connection here. Nails comes pre-configured to
|
|
111
|
+
use the sequelize connector, giving your models sequelize support. The initial setup
|
|
112
|
+
uses an in-memory <em>sqlite3</em> database. Change the address to change the location and
|
|
113
|
+
version of your desired sql database. Check out <a href="https://sequelize.org">Sequelize</a>
|
|
114
|
+
for more info.</p>
|
|
115
|
+
<p>Alternatively, you can configure a connection to MongoDB using the mongoose_connector.js.
|
|
116
|
+
If enabled, models will accept <a href="https://mongoosejs.com/docs/">Mongoose</a> schemas and will
|
|
117
|
+
be backed by the desired MongoDB. Consider using the in-memory DB during development.</p>
|
|
111
118
|
<h2 id="controller">Controller</h2>
|
|
112
119
|
<p>Controllers are defined in app/controllers/. Each controller module should
|
|
113
120
|
define a Controller subclass. The name will be used to match routes defined in
|
|
114
121
|
config/routes.js for incoming requests. Methods on the controller can be used as
|
|
115
122
|
actions, receiving <strong>params</strong>, <strong>request</strong>, and <strong>response</strong> as arguments.</p>
|
|
116
123
|
<p>For Example:</p>
|
|
117
|
-
<pre><code class="js language-js"
|
|
118
|
-
|
|
124
|
+
<pre><code class="js language-js">// const Controller = requre("nails-boilerplate").Controller
|
|
125
|
+
import nails from 'nails-boilerplate';
|
|
126
|
+
|
|
127
|
+
class HomeController extends nails.Controller {
|
|
119
128
|
index(params, request, response) {
|
|
120
129
|
// default action
|
|
121
130
|
}
|
|
@@ -124,7 +133,7 @@ class HomeController extends Controller {
|
|
|
124
133
|
// does something then renders a view
|
|
125
134
|
}
|
|
126
135
|
}
|
|
127
|
-
|
|
136
|
+
export default HomeController;
|
|
128
137
|
|
|
129
138
|
function helperMethod() {
|
|
130
139
|
// does something but does not have access to response
|
|
@@ -132,6 +141,34 @@ function helperMethod() {
|
|
|
132
141
|
</code></pre>
|
|
133
142
|
<p>defines a controller which will match any route to <em>home#\<action></em>. <strong>index</strong>
|
|
134
143
|
and <strong>signin</strong> are actions which can be used to render a response to the client.</p>
|
|
144
|
+
<h3 id="localroutes">Local Routes</h3>
|
|
145
|
+
<p>You can define a local routing table directly in the controller.
|
|
146
|
+
Local routes take precidence over global routes. All local routes
|
|
147
|
+
are prefixed with the controller name unless they start with '/'.
|
|
148
|
+
For example, in HomeController the following route:</p>
|
|
149
|
+
<p><code>["get", "data", {action: 'getData', json: true}],</code></p>
|
|
150
|
+
<p>will accept GET requests to /home/data and respond with the json
|
|
151
|
+
object returned by the getData function. If the route is changed to:</p>
|
|
152
|
+
<p><code>["get", "/data", {action: 'getData', json: true}],</code></p>
|
|
153
|
+
<p>it will accept GET requests to /data instead. All local routes are
|
|
154
|
+
implicitly routed to their respective parent controllers.</p>
|
|
155
|
+
<pre><code class="js language-js">export default class UsersController extends nails.Controller {
|
|
156
|
+
routes = [
|
|
157
|
+
// Routes requests to /absolute/path
|
|
158
|
+
['get', '/absolute/path', {action: 'actionA'}],
|
|
159
|
+
// Routes requests to /users/relative/path
|
|
160
|
+
['get', './relative/path', {action: 'actionB'}],
|
|
161
|
+
// Routes requests to /users/relative/path
|
|
162
|
+
['get', 'relative/path', {action: 'actionB'}],
|
|
163
|
+
]
|
|
164
|
+
|
|
165
|
+
// Handles requests to /absolute/path
|
|
166
|
+
actionA(request, response, params) {}
|
|
167
|
+
|
|
168
|
+
// Handles requests to /users/relative/path
|
|
169
|
+
actionB(request, response, params) {}
|
|
170
|
+
}
|
|
171
|
+
</code></pre>
|
|
135
172
|
<h3 id="actions">Actions</h3>
|
|
136
173
|
<p>Actions are used to define how nails should respond to an incoming request.
|
|
137
174
|
If no action has been defined for a route, nails will default to the index
|
|
@@ -172,20 +209,41 @@ params object:</p>
|
|
|
172
209
|
overridden to allow for the rendering of views by name.</p>
|
|
173
210
|
<h2 id="model">Model</h2>
|
|
174
211
|
<p>Models are programmatic representations of data you wish to persist in a
|
|
175
|
-
database.
|
|
212
|
+
database. The constructor for Model accepts two arguments: the <code>modelName</code> and an
|
|
213
|
+
<code>options</code> object which is passed to the database connector module.</p>
|
|
214
|
+
<h3 id="sequelizemodels">Sequelize Models</h3>
|
|
215
|
+
<p>Sequelize models are subclasses of
|
|
216
|
+
<a href="https://sequelize.org/docs/v6/core-concepts/model-basics/">Sequelize Models</a>, and come with the <code>count()</code>, <code>findAll()</code>,
|
|
217
|
+
and <code>create()</code> methods, to name a few. You can define your own models by
|
|
218
|
+
extending an instance of the <code>Model</code> class provided by Nails:</p>
|
|
219
|
+
<pre><code class="js language-js">// const Model = require("nails-boilerplate").Model;
|
|
220
|
+
import nails from 'nails-boilerplate';
|
|
221
|
+
import {DataTypes} from 'sequelize';
|
|
222
|
+
userSchema = {
|
|
223
|
+
name: {type: DataTypes.STRING, allowNull: false},
|
|
224
|
+
email: {type: DataTypes.STRING, allowNull: false}
|
|
225
|
+
};
|
|
226
|
+
export default class User extends new Model("User", userSchema) {
|
|
227
|
+
/**
|
|
228
|
+
* It is not recommended to add helper methods to Sequelize models. Define
|
|
229
|
+
* them in the schema instead.
|
|
230
|
+
*/
|
|
231
|
+
};
|
|
232
|
+
</code></pre>
|
|
233
|
+
<h3 id="mongoosemodels">Mongoose Models</h3>
|
|
234
|
+
<p>Mongoose models are subclasses of
|
|
176
235
|
<a href="https://mongoosejs.com/docs/api/model.html">Mongoose Models</a>, and come with the <code>save()</code>, <code>find()</code>,
|
|
177
236
|
and <code>where()</code> methods, to name a few. You can define your own models by
|
|
178
237
|
extending an instance of the <code>Model</code> class provided by Nails:</p>
|
|
179
|
-
<pre><code class="js language-js"
|
|
238
|
+
<pre><code class="js language-js">// const Model = require("nails-boilerplate").Model;
|
|
239
|
+
import nails from 'nails-boilerplate';
|
|
180
240
|
const userSchema = {name: String, email: String};
|
|
181
|
-
|
|
241
|
+
export default class User extends new Model("User", {schema: userSchema}) {
|
|
182
242
|
// Define your helper methods here
|
|
183
243
|
};
|
|
184
244
|
</code></pre>
|
|
185
|
-
<p>The
|
|
186
|
-
|
|
187
|
-
Mongoose connector accepts a schema field that is used to describe how
|
|
188
|
-
documents are stored in MongoDB.</p>
|
|
245
|
+
<p>The <code>schema</code> option for Mongoose Models accepts a schema field that is used
|
|
246
|
+
to define how documents are stored in MongoDB.</p>
|
|
189
247
|
<h3 id="databaseconnectors">Database Connectors</h3>
|
|
190
248
|
<p>Database connectors are intermediaries which define how a Model interacts with
|
|
191
249
|
a database. Database connector modules need to export two methods:</p>
|
|
@@ -1,10 +1,34 @@
|
|
|
1
1
|
import nails from "nails-boilerplate";
|
|
2
2
|
|
|
3
3
|
export default class HomeController extends nails.Controller {
|
|
4
|
-
|
|
5
|
-
|
|
6
|
-
|
|
7
|
-
|
|
8
|
-
|
|
9
|
-
|
|
10
|
-
|
|
4
|
+
/**
|
|
5
|
+
* You can define a local routing table directly in the controller.
|
|
6
|
+
* Local routes take precidence over global routes. All local routes
|
|
7
|
+
* are prefixed with the controller name unless they start with '/'.
|
|
8
|
+
* For example, in HomeController the following route:
|
|
9
|
+
*
|
|
10
|
+
* ["get", "data", {action: 'getData', json: true}],
|
|
11
|
+
*
|
|
12
|
+
* will accept GET requests to /home/data and respond with the json
|
|
13
|
+
* object returned by the getData function. If the route is changed to:
|
|
14
|
+
*
|
|
15
|
+
* ["get", "/data", {action: 'getData', json: true}],
|
|
16
|
+
*
|
|
17
|
+
* it will accept GET requests to /data instead. All local routes are
|
|
18
|
+
* implicitly routed to their respective parent controllers.
|
|
19
|
+
*/
|
|
20
|
+
routes = [
|
|
21
|
+
["get", "data", {action: 'getData', json: true}],
|
|
22
|
+
];
|
|
23
|
+
|
|
24
|
+
index(params, request, response) {
|
|
25
|
+
return {
|
|
26
|
+
title: "My first Nails Service",
|
|
27
|
+
welcome_message: "Welcome to Nails"
|
|
28
|
+
};
|
|
29
|
+
}
|
|
30
|
+
|
|
31
|
+
getData(params, request, response) {
|
|
32
|
+
return {testData: Math.floor(Math.random() * 10000000)}
|
|
33
|
+
}
|
|
34
|
+
};
|