@projectinvicta/nails 2.0.15 → 3.0.0
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 +70 -84
- package/bin/test/test_init.sh +33 -0
- package/index.ts +11 -0
- package/lib/Controller.ts +207 -0
- package/lib/Nails.ts +210 -0
- package/lib/Router.ts +180 -0
- package/lib/{application.js → application.ts} +8 -3
- package/lib/config.ts +74 -0
- package/package.json +14 -11
- package/spec/controller.spec.js +5 -51
- package/spec/nails.spec.js +1 -0
- package/spec/router.spec.js +5 -5
- package/spec/services/integration/config/db.ts +7 -0
- package/spec/services/integration/config/service.js +8 -8
- package/spec/services/integration/server/controllers/classbased_controller.js +2 -2
- package/spec/services/integration/server/controllers/default_json_controller.js +19 -7
- package/spec/services/integration/server/controllers/error_controller.js +2 -2
- package/spec/services/integration/server/controllers/home_controller.js +6 -28
- package/spec/services/integration/server/controllers/json_controller.js +2 -2
- package/spec/services/integration/server/controllers/manualrenderasync_controller.js +2 -2
- package/spec/services/integration/server/controllers/mjs_controller.mjs +2 -6
- package/spec/services/integration/server/controllers/modeltest_controller.js +6 -9
- package/spec/services/integration/server/controllers/websocket_controller.js +1 -1
- package/spec/services/integration/server/models/dog.js +7 -5
- package/spec/services/integration/server/models/owner.js +13 -0
- package/spec/services/integration/server.js +3 -4
- package/spec/services.integration.spec.js +45 -17
- package/templates/default/config/db.ts +13 -0
- package/{spec/services/integration_sequelize/config/mimes.js → templates/default/config/mimes.ts} +42 -61
- package/templates/default/config/routes.ts +22 -0
- package/templates/{config/service.js → default/config/service.ts} +2 -2
- package/templates/default/package.json +8 -2
- package/templates/default/public/README.xml +68 -85
- package/templates/default/server/controllers/home_controller.js +2 -2
- package/templates/{server/controllers/home_controller.js → default/server/controllers/users_controller.ts} +29 -34
- package/templates/default/server/models/Dog.ts +8 -0
- package/templates/default/server/models/User.ts +14 -0
- package/templates/default/spec/User.test.js +7 -5
- package/templates/default/spec/home_controller.test.js +3 -3
- package/index.js +0 -6
- package/lib/collection.js +0 -6
- package/lib/controller.js +0 -182
- package/lib/database_connector.js +0 -12
- package/lib/firebase_connector.js +0 -94
- package/lib/model_v2.js +0 -24
- package/lib/mongoose_connector.js +0 -49
- package/lib/mongoose_mem_connector.js +0 -21
- package/lib/nails.js +0 -244
- package/lib/router.js +0 -202
- package/lib/sequelize_connector.js +0 -31
- package/lib/server.js +0 -1
- package/spec/model_v2.spec.js +0 -75
- package/spec/mongodb_connector.util.js +0 -30
- package/spec/mongoose_connector.util.js +0 -20
- package/spec/sequelize_connector.spec.js +0 -91
- package/spec/sequelize_connector.util.js +0 -18
- package/spec/services/integration/config/db.js +0 -14
- package/spec/services/integration_sequelize/README.md +0 -5
- package/spec/services/integration_sequelize/client/css/styles.css +0 -0
- package/spec/services/integration_sequelize/client/download.jpg +0 -0
- package/spec/services/integration_sequelize/client/favicon.ico +0 -0
- package/spec/services/integration_sequelize/client/index.html +0 -9
- package/spec/services/integration_sequelize/client/js/client.js +0 -0
- package/spec/services/integration_sequelize/client/js/components/app.jsx +0 -15
- package/spec/services/integration_sequelize/config/db.js +0 -4
- package/spec/services/integration_sequelize/config/routes.js +0 -25
- package/spec/services/integration_sequelize/config/service.js +0 -48
- package/spec/services/integration_sequelize/config/ssl/certificate.pem +0 -22
- package/spec/services/integration_sequelize/config/ssl/csr.csr +0 -17
- package/spec/services/integration_sequelize/config/ssl/key.pem +0 -28
- package/spec/services/integration_sequelize/config/ssl/private_key.pem +0 -30
- package/spec/services/integration_sequelize/config/ssl/public_key.pem +0 -9
- package/spec/services/integration_sequelize/package.json +0 -23
- package/spec/services/integration_sequelize/server/controllers/default_json_controller.js +0 -21
- package/spec/services/integration_sequelize/server/controllers/home_controller.js +0 -39
- package/spec/services/integration_sequelize/server/models/dog.js +0 -7
- package/spec/services/integration_sequelize/server/models/owner.js +0 -10
- package/spec/services/integration_sequelize/server/views/defaultjson/testnojson.ejs +0 -1
- package/spec/services/integration_sequelize/server/views/testreact/testreact.ejs +0 -15
- package/spec/services/integration_sequelize/server.js +0 -9
- package/spec/services.integration_sequelize.spec.js +0 -60
- package/templates/bin/promote.sh +0 -20
- package/templates/bin/rollout.sh +0 -74
- package/templates/bin/server.js +0 -6
- package/templates/bin/start.sh +0 -16
- package/templates/common/readme_fetcher.js +0 -4
- package/templates/config/db.js +0 -19
- package/templates/config/mimes.js +0 -59
- package/templates/config/routes.js +0 -38
- package/templates/config/ssl/certificate.pem +0 -22
- package/templates/config/ssl/csr.csr +0 -17
- package/templates/config/ssl/key.pem +0 -28
- package/templates/config/ssl/private_key.pem +0 -30
- package/templates/config/ssl/public_key.pem +0 -9
- package/templates/default/config/db.js +0 -19
- package/templates/default/config/mimes.js +0 -59
- package/templates/default/config/routes.js +0 -38
- package/templates/default/config/service.js +0 -45
- package/templates/default/server/models/User.js +0 -18
- package/templates/package-lock.json +0 -9048
- package/templates/package.json +0 -43
- package/templates/public/README.xml +0 -332
- package/templates/public/css/styles.css +0 -17
- package/templates/public/download.jpg +0 -0
- package/templates/public/favicon.ico +0 -0
- package/templates/public/index.html +0 -9
- package/templates/public/js/client.js +0 -1
- package/templates/server/models/User.js +0 -18
- package/templates/server/views/home/index.ejs +0 -14
- package/templates/server/views/partials/javascripts.ejs +0 -1
- package/templates/server/views/partials/reactapp.ejs +0 -1
- package/templates/server/views/partials/styles.ejs +0 -3
- package/templates/spec/User.test.js +0 -20
- package/templates/spec/home_controller.test.js +0 -28
- package/templates/spec/setupTests.js +0 -0
- package/templates/src/AboutPage.jsx +0 -9
- package/templates/src/HomePage.jsx +0 -9
- package/templates/src/Layout.jsx +0 -78
- package/templates/src/ReadmePage.jsx +0 -7
- package/templates/src/app.jsx +0 -29
- package/templates/src/components/ReadmeLoader.jsx +0 -13
- package/templates/src/styles/appstyles.css +0 -3
- package/templates/vite.config.ts +0 -42
package/README.md
CHANGED
|
@@ -1,33 +1,34 @@
|
|
|
1
|
-
# Nails
|
|
1
|
+
# Nails: A Node Webservice Framework
|
|
2
2
|
|
|
3
|
-
|
|
4
|
-
for node developers. With minimal dependencies, Nails offers a greater
|
|
5
|
-
syntactical familiarity than php alongside the creative freedom of well developed
|
|
6
|
-
server framework solutions like Rails and Django.
|
|
3
|
+
Nails is a lightweight, configurable MVC-inspired backend framework for Node.js. With minimal dependencies, Nails offers a familiar syntax for developers coming from frameworks like Ruby on Rails or Django, while providing the flexibility of JavaScript.
|
|
7
4
|
|
|
8
|
-
|
|
9
|
-
The modules used in Nails Boilerplate can be easily extended to produce the custom
|
|
10
|
-
functionality to fit your needs, and you are encouraged to do so.
|
|
5
|
+
Nails provides the essential building blocks to get your MVC-style application up and running quickly. The modules used in Nails can be easily extended to produce the custom functionality to fit your needs, and you are encouraged to do so.
|
|
11
6
|
|
|
12
|
-
##
|
|
7
|
+
## Getting Started
|
|
13
8
|
|
|
14
|
-
|
|
15
|
-
sudo npm install -g nails-boilerplate
|
|
9
|
+
There are two recommended ways to get started with Nails.
|
|
16
10
|
|
|
17
|
-
|
|
11
|
+
### Using NPX (Recommended)
|
|
12
|
+
You can create a new Nails project without a global installation using `npx`:
|
|
13
|
+
|
|
14
|
+
```bash
|
|
15
|
+
npx @projectinvicta/nails init <app_name>
|
|
18
16
|
```
|
|
19
17
|
|
|
20
|
-
|
|
21
|
-
|
|
22
|
-
getting started. Additional controllers and views will automatically be imported
|
|
23
|
-
into nails. Now just hook the new controllers in with some new routes and you're
|
|
24
|
-
off to a good start.
|
|
18
|
+
### Using Global Install
|
|
19
|
+
Alternatively, you can install the package globally:
|
|
25
20
|
|
|
21
|
+
```bash
|
|
22
|
+
npm install -g @projectinvicta/nails
|
|
23
|
+
|
|
24
|
+
nails init <app_name>
|
|
26
25
|
```
|
|
27
|
-
cd app_name
|
|
28
26
|
|
|
29
|
-
|
|
27
|
+
After initializing your app, navigate into the new directory and start the server:
|
|
30
28
|
|
|
29
|
+
```bash
|
|
30
|
+
cd <app_name>
|
|
31
|
+
npm install
|
|
31
32
|
npm start
|
|
32
33
|
```
|
|
33
34
|
|
|
@@ -36,15 +37,15 @@ npm start
|
|
|
36
37
|
For your convenience, here is a quick outline of the main components of a nails service.
|
|
37
38
|
Remember: each object comes with an example file to use for reference when building your service.
|
|
38
39
|
|
|
39
|
-
###
|
|
40
|
-
|
|
41
|
-
|
|
42
|
-
|
|
43
|
-
service.js
|
|
44
|
-
routes.js
|
|
45
|
-
db.js
|
|
40
|
+
### Named Exports
|
|
41
|
+
`nails` now provides a number of useful named exports. You can import them as follows:
|
|
42
|
+
```js
|
|
43
|
+
import Nails, { Controller, Model, DataTypes, /* config types */ } from '@projectinvicta/nails';
|
|
46
44
|
```
|
|
47
45
|
|
|
46
|
+
### Config
|
|
47
|
+
Your configuration files are stored in app_name/config/.
|
|
48
|
+
|
|
48
49
|
Each default config file is annotated with comments documenting each field to
|
|
49
50
|
help you tailor your service to your needs.
|
|
50
51
|
|
|
@@ -65,7 +66,7 @@ fields. The resulting config will be available to your service through the nails
|
|
|
65
66
|
module:
|
|
66
67
|
|
|
67
68
|
```js
|
|
68
|
-
import
|
|
69
|
+
import Nails from '@projectinvicta/nails';
|
|
69
70
|
|
|
70
71
|
const service_config = nails.config
|
|
71
72
|
```
|
|
@@ -85,7 +86,7 @@ then `service_config.yourCustomField` as defined above will be equal to
|
|
|
85
86
|
|
|
86
87
|
#### routes.js
|
|
87
88
|
*routes.js* is a list defining mappings from a url path to a *Controller* and
|
|
88
|
-
*Action*. Each entry in the list is an array with three elements:
|
|
89
|
+
*Action*. Routes are now strongly typed (see `lib/config.ts` and `lib/Router.ts`). Each entry in the list is an array with three elements:
|
|
89
90
|
`[method, path, options]`
|
|
90
91
|
|
|
91
92
|
**method** is a string defining the HTTP request method of the route. Supported
|
|
@@ -134,15 +135,9 @@ parameters work.
|
|
|
134
135
|
|
|
135
136
|
#### db.js
|
|
136
137
|
|
|
137
|
-
Quickly configure your database connection here.
|
|
138
|
-
|
|
139
|
-
|
|
140
|
-
version of your desired sql database. Check out [Sequelize](https://sequelize.org)
|
|
141
|
-
for more info.
|
|
142
|
-
|
|
143
|
-
Alternatively, you can configure a connection to MongoDB using the mongoose_connector.js.
|
|
144
|
-
If enabled, models will accept [Mongoose](https://mongoosejs.com/docs/) schemas and will
|
|
145
|
-
be backed by the desired MongoDB. Consider using the in-memory DB during development.
|
|
138
|
+
Quickly configure your database connection here. The initial setup uses a *sqlite3* database file `config/development.db`
|
|
139
|
+
and an in-memory database in the test environment. Change the address to change the location and
|
|
140
|
+
version of your desired sql database. Check out [Sequelize](https://sequelize.org) for more info.
|
|
146
141
|
|
|
147
142
|
## Controller
|
|
148
143
|
|
|
@@ -153,10 +148,9 @@ actions, receiving **params**, **request**, and **response** as arguments.
|
|
|
153
148
|
|
|
154
149
|
For Example:
|
|
155
150
|
``` js
|
|
156
|
-
|
|
157
|
-
import nails from 'nails-boilerplate';
|
|
151
|
+
import { Controller } from '@projectinvicta/nails';
|
|
158
152
|
|
|
159
|
-
class HomeController extends
|
|
153
|
+
class HomeController extends Controller {
|
|
160
154
|
index(params, request, response) {
|
|
161
155
|
// default action
|
|
162
156
|
}
|
|
@@ -193,7 +187,9 @@ it will accept GET requests to /data instead. All local routes are
|
|
|
193
187
|
implicitly routed to their respective parent controllers.
|
|
194
188
|
|
|
195
189
|
```js
|
|
196
|
-
|
|
190
|
+
import { Controller } from '@projectinvicta/nails';
|
|
191
|
+
|
|
192
|
+
export default class UsersController extends Controller {
|
|
197
193
|
routes = [
|
|
198
194
|
// Routes requests to /absolute/path
|
|
199
195
|
['get', '/absolute/path', {action: 'actionA'}],
|
|
@@ -280,12 +276,15 @@ You can configure an individual route to respond with JSON by setting the `json`
|
|
|
280
276
|
```
|
|
281
277
|
|
|
282
278
|
##### API Controllers
|
|
283
|
-
By setting `json` to `true`, all actions in a controller will respond with JSON.
|
|
279
|
+
By setting `json` to `true`, all actions in a controller will respond with JSON by default.
|
|
280
|
+
This can be overridden for individual routes by setting `{json: false}` in the route options.
|
|
284
281
|
|
|
285
282
|
```js
|
|
286
|
-
|
|
283
|
+
import { Controller } from '@projectinvicta/nails';
|
|
284
|
+
|
|
285
|
+
class YourApiController extends Controller {
|
|
287
286
|
json = true;
|
|
288
|
-
|
|
287
|
+
|
|
289
288
|
action(params, request, response) {
|
|
290
289
|
return {your: 'jsonresponse'};
|
|
291
290
|
}
|
|
@@ -294,27 +293,28 @@ class YourApiController extends nails.Controller {
|
|
|
294
293
|
|
|
295
294
|
## Model
|
|
296
295
|
|
|
297
|
-
Models are programmatic representations of data you wish to persist in a
|
|
298
|
-
database
|
|
299
|
-
`options` object which is passed to the database connector module.
|
|
296
|
+
Models are programmatic representations of data you wish to persist in a database. Nails connects to
|
|
297
|
+
your database of choice using the Sequelize ORM.
|
|
300
298
|
|
|
301
299
|
### Sequelize Models
|
|
302
300
|
|
|
303
301
|
Sequelize models are subclasses of
|
|
304
302
|
[Sequelize Models][sequelize_model_docs], and come with the `count()`, `findAll()`,
|
|
305
303
|
and `create()` methods, to name a few. You can define your own models by
|
|
306
|
-
extending an instance of the `Model` class provided by Nails
|
|
304
|
+
extending an instance of the `Model` class provided by Nails. Model files must export the Model subclass as a default export and a named `schema` export.
|
|
305
|
+
They can also export an `options` object to provide ModelInitializationOptions as well as `defer`, `finalize`, and `migrate` functions.
|
|
307
306
|
|
|
308
307
|
```js
|
|
309
|
-
|
|
310
|
-
|
|
311
|
-
|
|
312
|
-
schema = {
|
|
308
|
+
import { Model, DataTypes } from '@projectinvicta/nails';
|
|
309
|
+
|
|
310
|
+
// REQUIRED
|
|
311
|
+
export const schema = {
|
|
313
312
|
name: {type: DataTypes.STRING, allowNull: false},
|
|
314
313
|
email: {type: DataTypes.STRING, allowNull: false}
|
|
315
314
|
};
|
|
316
315
|
|
|
317
|
-
|
|
316
|
+
// Optional
|
|
317
|
+
export const options = {
|
|
318
318
|
indexes: [
|
|
319
319
|
{
|
|
320
320
|
unique: true,
|
|
@@ -323,38 +323,37 @@ options = {
|
|
|
323
323
|
],
|
|
324
324
|
};
|
|
325
325
|
|
|
326
|
-
|
|
326
|
+
// REQUIRED
|
|
327
|
+
export default class User extends Model {
|
|
327
328
|
someHelperMethod() {
|
|
328
329
|
// This method will be available on all instances of User and is
|
|
329
330
|
// an ideal way to simplify data manipulation.
|
|
330
331
|
}
|
|
331
332
|
};
|
|
332
333
|
|
|
333
|
-
|
|
334
|
+
// Optional
|
|
335
|
+
export async function defer(models) {
|
|
336
|
+
// define associations here
|
|
337
|
+
}
|
|
334
338
|
|
|
335
|
-
|
|
336
|
-
|
|
337
|
-
|
|
338
|
-
|
|
339
|
-
extending an instance of the `Model` class provided by Nails:
|
|
339
|
+
// Optional
|
|
340
|
+
export async function finalize(models) {
|
|
341
|
+
// any final model setup
|
|
342
|
+
}
|
|
340
343
|
|
|
341
|
-
|
|
342
|
-
|
|
343
|
-
|
|
344
|
-
|
|
345
|
-
export default class User extends new Model("User", {schema: userSchema}) {
|
|
346
|
-
// Define your helper methods here
|
|
347
|
-
};
|
|
344
|
+
// Optional
|
|
345
|
+
export async function migrate(queryInterface) {
|
|
346
|
+
// migration logic
|
|
347
|
+
}
|
|
348
348
|
```
|
|
349
349
|
|
|
350
|
-
The `schema` option for Mongoose Models accepts a schema field that is used
|
|
351
|
-
to define how documents are stored in MongoDB.
|
|
352
|
-
|
|
353
350
|
### Model Library
|
|
354
351
|
Nails will store all instantialized models in a single object called `MODELS`. By accessing these models via the library, you can avoid circular dependencies and ensure all models have been fully initialized.
|
|
355
352
|
|
|
356
353
|
```js
|
|
357
|
-
|
|
354
|
+
import Nails, { Model } from '@projectinvicta/nails';
|
|
355
|
+
// ...
|
|
356
|
+
class User extends Model {
|
|
358
357
|
// A helper method which depends on anoher model using the
|
|
359
358
|
// Nails Model Library rather than directly importing the model.
|
|
360
359
|
async findFriends() {
|
|
@@ -364,18 +363,6 @@ class User extends nails.Model("User", {schema, options}) {
|
|
|
364
363
|
```
|
|
365
364
|
This design pattern is not always necessary, but will help avoid circular dependencies.
|
|
366
365
|
|
|
367
|
-
### Database Connectors
|
|
368
|
-
|
|
369
|
-
Database connectors are intermediaries which define how a Model interacts with
|
|
370
|
-
a database. Database connector modules need to export two methods:
|
|
371
|
-
* _connect(db_config)_ uses the db config defined in *db.js* to connect to
|
|
372
|
-
a database. This function will be called once by Nails.
|
|
373
|
-
* _generateModelSuperclass(name, options)_ uses the provided Model name and
|
|
374
|
-
options to generate a Model prototype for use as an interface. A Model
|
|
375
|
-
interface is generated for each of your models, allowing them to interact with
|
|
376
|
-
a database. Ideally, interfaces will define save() and find() methods, but
|
|
377
|
-
these methods and their implementations are up to the individual connector.
|
|
378
|
-
|
|
379
366
|
## View
|
|
380
367
|
Views are dynamic templates used to render an html response for a browser.
|
|
381
368
|
Nails comes prepackaged with EJS templates.
|
|
@@ -415,5 +402,4 @@ Enjoy! Feature requests, bug reports, and comments are welcome on github.
|
|
|
415
402
|
|
|
416
403
|
[express_routing_docs]: https://expressjs.com/en/guide/routing.html
|
|
417
404
|
[express_request_docs]: https://expressjs.com/en/5x/api.html#req
|
|
418
|
-
[mongoose_model_docs]: https://mongoosejs.com/docs/api/model.html
|
|
419
405
|
[sequelize_model_docs]: https://sequelize.org/docs/v6/core-concepts/model-basics/
|
|
@@ -0,0 +1,33 @@
|
|
|
1
|
+
#!/bin/bash
|
|
2
|
+
|
|
3
|
+
# Store the fully qualified location of the local nails module directory
|
|
4
|
+
NAILS_MODULE_DIR=$(pwd)
|
|
5
|
+
|
|
6
|
+
# Create a temporary directory for the test project with a datestring
|
|
7
|
+
DATE_STRING=$(date +%Y%m%d%H%M%S)
|
|
8
|
+
TEST_DIR="/tmp/test_project_$DATE_STRING"
|
|
9
|
+
mkdir -p $TEST_DIR
|
|
10
|
+
|
|
11
|
+
# Create a new nails project using the local version of nails-boilerplate
|
|
12
|
+
npx $NAILS_MODULE_DIR/ init $TEST_DIR
|
|
13
|
+
|
|
14
|
+
# Change directory to the test project
|
|
15
|
+
cd $TEST_DIR
|
|
16
|
+
|
|
17
|
+
# Link the local @projectinvicta/nails module
|
|
18
|
+
npm link $NAILS_MODULE_DIR
|
|
19
|
+
|
|
20
|
+
# Install other dependencies
|
|
21
|
+
npm install
|
|
22
|
+
|
|
23
|
+
# Run tests in the new test project
|
|
24
|
+
npm test
|
|
25
|
+
|
|
26
|
+
# Return to the original nails module directory
|
|
27
|
+
cd $NAILS_MODULE_DIR
|
|
28
|
+
|
|
29
|
+
# Unlink the local @projectinvicta/nails module
|
|
30
|
+
npm unlink @projectinvicta/nails
|
|
31
|
+
|
|
32
|
+
# Clean up the temporary directory
|
|
33
|
+
rm -rf $TEST_DIR
|
package/index.ts
ADDED
|
@@ -0,0 +1,11 @@
|
|
|
1
|
+
import Nails from './lib/Nails.ts';
|
|
2
|
+
import ControllerInternal from './lib/Controller.ts';
|
|
3
|
+
|
|
4
|
+
export { DataTypes } from 'sequelize';
|
|
5
|
+
|
|
6
|
+
export default Nails;
|
|
7
|
+
|
|
8
|
+
export const Controller = ControllerInternal;
|
|
9
|
+
export * from './lib/config.ts';
|
|
10
|
+
|
|
11
|
+
export { Model } from 'sequelize';
|
|
@@ -0,0 +1,207 @@
|
|
|
1
|
+
import { type Request, type Response } from 'express';
|
|
2
|
+
import { type WebSocket } from 'ws';
|
|
3
|
+
import { type RouteOptions, type RouteDefinition, type ControllerDoRouteParams } from './config.ts'; // Assuming RouteDefinition is exported from Config.ts
|
|
4
|
+
|
|
5
|
+
// Define a basic Router interface based on its usage
|
|
6
|
+
interface Router {
|
|
7
|
+
on(event: string, listener: Function): void;
|
|
8
|
+
removeAllListeners(event: string): void;
|
|
9
|
+
addRoutes(routes: RouteDefinition[]): void;
|
|
10
|
+
}
|
|
11
|
+
|
|
12
|
+
// Global router instance
|
|
13
|
+
let router: Router;
|
|
14
|
+
let controller_proto: Controller; // This will be assigned an instance of Controller
|
|
15
|
+
|
|
16
|
+
// TODO: refactor error generation
|
|
17
|
+
const NAME_REQUIRED_ERROR = (): Error => {
|
|
18
|
+
return new Error(
|
|
19
|
+
'FATAL ERROR::: Named function required for Controller constructor method');
|
|
20
|
+
};
|
|
21
|
+
|
|
22
|
+
const DISABLE_AUTORENDER = new (class DisableAutorender {});
|
|
23
|
+
|
|
24
|
+
// The base controller definition
|
|
25
|
+
class Controller {
|
|
26
|
+
// Static properties
|
|
27
|
+
static router: Router; // Static reference to the router
|
|
28
|
+
static models: Record<string, any>; // Static reference to models, assuming key-value pairs
|
|
29
|
+
|
|
30
|
+
static get DISABLE_AUTORENDER(): typeof DISABLE_AUTORENDER {
|
|
31
|
+
return DISABLE_AUTORENDER;
|
|
32
|
+
}
|
|
33
|
+
|
|
34
|
+
static setRouter(router_singleton: Router): void {
|
|
35
|
+
Controller.router = router = router_singleton;
|
|
36
|
+
}
|
|
37
|
+
|
|
38
|
+
static setModels(models: Record<string, any>): void {
|
|
39
|
+
Controller.prototype.models = models;
|
|
40
|
+
}
|
|
41
|
+
|
|
42
|
+
// Instance properties
|
|
43
|
+
models: Record<string, any>; // This will be assigned via Controller.setModels
|
|
44
|
+
routes: RouteDefinition[]; // Assuming routes are defined on subclasses
|
|
45
|
+
json: boolean;
|
|
46
|
+
|
|
47
|
+
constructor() {
|
|
48
|
+
const controllerName = this._getControllerName();
|
|
49
|
+
router.removeAllListeners('dispatchTo:' + controllerName);
|
|
50
|
+
router.on('dispatchTo:' + controllerName, this._do.bind(this));
|
|
51
|
+
}
|
|
52
|
+
|
|
53
|
+
_getControllerName(): string {
|
|
54
|
+
return this.constructor.name.toLowerCase().replace(/controller$/, '');
|
|
55
|
+
}
|
|
56
|
+
|
|
57
|
+
/** Initializes local and global routes defined on the Controller subclass */
|
|
58
|
+
_registerControllerRoutes(): void {
|
|
59
|
+
// `this.json` is not explicitly defined on Controller, but expected on subclasses
|
|
60
|
+
// assuming it's a boolean or undefined
|
|
61
|
+
const defaultToJson = this.json;
|
|
62
|
+
const controllerName = this._getControllerName();
|
|
63
|
+
if (this.routes && this.routes.length) {
|
|
64
|
+
const localizedRoutes: RouteDefinition[] = this.routes.map(route => {
|
|
65
|
+
// TODO: throw an error if :controller is present in local routes
|
|
66
|
+
// TODO: also account for other malformed local routes
|
|
67
|
+
const routePrefix = `/${controllerName}/`;
|
|
68
|
+
const modifiedDestination = (route[1][0] == '/')
|
|
69
|
+
? route[1]
|
|
70
|
+
: (route[1].startsWith('./') ? route[1].replace('./', routePrefix) : routePrefix + route[1]);
|
|
71
|
+
const modifiedOptions: RouteOptions = {...route[2]};
|
|
72
|
+
|
|
73
|
+
if (!('json' in modifiedOptions)) modifiedOptions.json = defaultToJson;
|
|
74
|
+
if (!('action' in modifiedOptions)
|
|
75
|
+
&& typeof modifiedDestination === 'string' && !modifiedDestination.includes(":action")
|
|
76
|
+
&& !Object.values(modifiedOptions).includes('action')) {
|
|
77
|
+
const destinationParts = modifiedDestination.split("/");
|
|
78
|
+
modifiedOptions.action = destinationParts[destinationParts.length - 1];
|
|
79
|
+
}
|
|
80
|
+
modifiedOptions.controller = controllerName;
|
|
81
|
+
return [route[0], modifiedDestination, modifiedOptions];
|
|
82
|
+
});
|
|
83
|
+
router.addRoutes(localizedRoutes);
|
|
84
|
+
}
|
|
85
|
+
}
|
|
86
|
+
|
|
87
|
+
/**** Network Methods *****/
|
|
88
|
+
/**
|
|
89
|
+
* The main entry function of the controller.
|
|
90
|
+
*
|
|
91
|
+
* @param {string} options.action The action called for this controller
|
|
92
|
+
* @param {object} options.params The parameter hash for this action
|
|
93
|
+
* @param {object} options.request The http request object from the http server
|
|
94
|
+
* @param {object} options.response The http response object from the http server
|
|
95
|
+
* @param {object} options.ws The WebSocket instance
|
|
96
|
+
* @param {object} options.next The Express NextFunction
|
|
97
|
+
*/
|
|
98
|
+
async _do({action, params, request, response, ws, next}: ControllerDoRouteParams): Promise<void> {
|
|
99
|
+
(request as any).handled_by_controller = true;
|
|
100
|
+
const doAction: Function = (this as any)[action]; // Action method on the controller
|
|
101
|
+
|
|
102
|
+
console.log(this.constructor.name, 'doing', action);
|
|
103
|
+
|
|
104
|
+
// TODO: let express handle errors
|
|
105
|
+
if (!doAction || typeof doAction !== 'function') {
|
|
106
|
+
const error = new Error('Action Not Available: #' + action);
|
|
107
|
+
if (response) {
|
|
108
|
+
response.status(404).send({
|
|
109
|
+
error: error.message + '\n' + error.stack
|
|
110
|
+
});
|
|
111
|
+
return;
|
|
112
|
+
}
|
|
113
|
+
if (ws) {
|
|
114
|
+
ws.close(1003, "Action Not available: #" + action);
|
|
115
|
+
}
|
|
116
|
+
return;
|
|
117
|
+
}
|
|
118
|
+
|
|
119
|
+
if (ws && !response) { // If it's a websocket request and not an http response
|
|
120
|
+
await doAction.call(this, params, ws, request);
|
|
121
|
+
return;
|
|
122
|
+
}
|
|
123
|
+
|
|
124
|
+
// Override async with Controller definition.
|
|
125
|
+
// Not using prototypal function objects
|
|
126
|
+
let actionResponse;
|
|
127
|
+
try {
|
|
128
|
+
actionResponse = await doAction.call(this, params, request, response);
|
|
129
|
+
} catch (e) {
|
|
130
|
+
// console.error(e);
|
|
131
|
+
return next(e);
|
|
132
|
+
// response.write({error: e.message, stack: e.stack});
|
|
133
|
+
// actionResponse = 500;
|
|
134
|
+
}
|
|
135
|
+
|
|
136
|
+
if ((typeof actionResponse) == 'number') {
|
|
137
|
+
response.sendStatus(actionResponse);
|
|
138
|
+
return;
|
|
139
|
+
}
|
|
140
|
+
|
|
141
|
+
if (params._json) {
|
|
142
|
+
console.log("Sending JSON");
|
|
143
|
+
console.log("The ACTION RESPONSE", actionResponse);
|
|
144
|
+
response.json(actionResponse || {});
|
|
145
|
+
return;
|
|
146
|
+
}
|
|
147
|
+
//let viewAction = params._action ? params._action.toString() : "index";
|
|
148
|
+
let viewPath = `${this._getControllerName()}/${action}`;
|
|
149
|
+
|
|
150
|
+
const renderView = (templateVars: any): void => {
|
|
151
|
+
if (response.headersSent) {
|
|
152
|
+
return;
|
|
153
|
+
}
|
|
154
|
+
if (!!params._json) {
|
|
155
|
+
response.json(templateVars === undefined ? params : templateVars);
|
|
156
|
+
return;
|
|
157
|
+
}
|
|
158
|
+
response.render(
|
|
159
|
+
viewPath,
|
|
160
|
+
templateVars === undefined ? params : templateVars,
|
|
161
|
+
function(err: Error | null, html?: string) {
|
|
162
|
+
if (err && err.message.match(/Failed to lookup view/)) {
|
|
163
|
+
response.render(viewPath + ".ejs", params);
|
|
164
|
+
} else if (err) {
|
|
165
|
+
console.error(err);
|
|
166
|
+
response.sendStatus(400); // Changed from 400, err to just 400
|
|
167
|
+
} else {
|
|
168
|
+
response.send(html);
|
|
169
|
+
}
|
|
170
|
+
});
|
|
171
|
+
};
|
|
172
|
+
|
|
173
|
+
if (!response.headersSent
|
|
174
|
+
&& !params._async
|
|
175
|
+
&& actionResponse !== DISABLE_AUTORENDER) { // TODO: what does this do?
|
|
176
|
+
if (actionResponse instanceof Promise) {
|
|
177
|
+
// TODO: add a timeout so an unresolved promise doesn't preserve the
|
|
178
|
+
// connection indefinitely.
|
|
179
|
+
actionResponse
|
|
180
|
+
.then(renderView)
|
|
181
|
+
.catch((error: any) => {
|
|
182
|
+
console.error(error);
|
|
183
|
+
response.sendStatus(500); // Changed from 500, error to just 500
|
|
184
|
+
});
|
|
185
|
+
} else {
|
|
186
|
+
renderView(actionResponse);
|
|
187
|
+
}
|
|
188
|
+
}
|
|
189
|
+
}
|
|
190
|
+
}
|
|
191
|
+
|
|
192
|
+
const error_codes = [404, 500];
|
|
193
|
+
error_codes.forEach((ec) => {
|
|
194
|
+
Controller.prototype[ec] = function (params: Record<string, any>, request: Request, response: Response): void {
|
|
195
|
+
//this._render('404');
|
|
196
|
+
console.error(ec.toString() + ' error with params', params);
|
|
197
|
+
const error = params['error'] || {};
|
|
198
|
+
const message = error.message || 'Error';
|
|
199
|
+
const stack_trace = error.stack || error || '';
|
|
200
|
+
response.setHeader('content-type', 'text/plain');
|
|
201
|
+
response.statusCode = ec;
|
|
202
|
+
response.end(message + '\n\n' + stack_trace);
|
|
203
|
+
};
|
|
204
|
+
(Controller.prototype as any)[ec.toString()] = Controller.prototype[ec];
|
|
205
|
+
});
|
|
206
|
+
|
|
207
|
+
export default Controller;
|