From 5b098d11c48618183772d697f10aa2ca19751f07 Mon Sep 17 00:00:00 2001 From: Bob Evans Date: Thu, 19 Sep 2024 12:28:58 -0400 Subject: [PATCH] feat: Added a express/knex example application --- README.md | 1 + knex/LICENSE | 22 +++ knex/README.md | 29 ++++ knex/config/database.js | 8 + .../migrations/20180326094647_create_users.js | 14 ++ .../20180326105452_create_projects.js | 15 ++ knex/db/seeds/001_users_seed.js | 19 +++ knex/db/seeds/002_project_seed.js | 26 ++++ knex/docker-compose.yml | 18 +++ knex/docs/index.md | 7 + knex/docs/usage/auth.md | 59 +++++++ knex/docs/usage/projects.md | 145 ++++++++++++++++++ knex/docs/usage/users.md | 140 +++++++++++++++++ knex/env.sample | 5 + knex/knexfile.js | 23 +++ knex/newrelic.js | 52 +++++++ knex/package.json | 24 +++ knex/server/controllers/auth_controller.js | 54 +++++++ knex/server/controllers/project_controller.js | 75 +++++++++ knex/server/controllers/user_controller.js | 70 +++++++++ knex/server/helpers/error_helper.js | 24 +++ knex/server/helpers/model-guts.js | 76 +++++++++ knex/server/index.js | 19 +++ knex/server/middleware/error_middleware.js | 109 +++++++++++++ knex/server/models/index.js | 27 ++++ knex/server/models/project.js | 30 ++++ knex/server/models/user.js | 72 +++++++++ knex/server/routes/auth_routes.js | 15 ++ knex/server/routes/project_routes.js | 21 +++ knex/server/routes/user_routes.js | 21 +++ knex/server/start.js | 11 ++ 31 files changed, 1231 insertions(+) create mode 100644 knex/LICENSE create mode 100644 knex/README.md create mode 100644 knex/config/database.js create mode 100644 knex/db/migrations/20180326094647_create_users.js create mode 100644 knex/db/migrations/20180326105452_create_projects.js create mode 100644 knex/db/seeds/001_users_seed.js create mode 100644 knex/db/seeds/002_project_seed.js create mode 100644 knex/docker-compose.yml create mode 100644 knex/docs/index.md create mode 100644 knex/docs/usage/auth.md create mode 100644 knex/docs/usage/projects.md create mode 100644 knex/docs/usage/users.md create mode 100644 knex/env.sample create mode 100644 knex/knexfile.js create mode 100644 knex/newrelic.js create mode 100644 knex/package.json create mode 100644 knex/server/controllers/auth_controller.js create mode 100644 knex/server/controllers/project_controller.js create mode 100644 knex/server/controllers/user_controller.js create mode 100644 knex/server/helpers/error_helper.js create mode 100644 knex/server/helpers/model-guts.js create mode 100644 knex/server/index.js create mode 100644 knex/server/middleware/error_middleware.js create mode 100644 knex/server/models/index.js create mode 100644 knex/server/models/project.js create mode 100644 knex/server/models/user.js create mode 100644 knex/server/routes/auth_routes.js create mode 100644 knex/server/routes/project_routes.js create mode 100644 knex/server/routes/user_routes.js create mode 100755 knex/server/start.js diff --git a/README.md b/README.md index 8c612d0..9b9d283 100644 --- a/README.md +++ b/README.md @@ -13,6 +13,7 @@ This repository contains example applications and scripts that demonstrate funct * [ESM](./esm-app) - example demonstrating how to load the agent in an [ESM](https://nodejs.org/api/esm.html) application. It also demonstrates how to register custom instrumentation for an ESM package. * [GraphQL Dataloader](./graphql-koa-dataloader) - example using Apollo Server, koa and GraphQL dataloader * [Kafkajs](./kafkajs) - example demonstrating [kafkajs](https://kafka.js.org/) with the Node.js agent + * [Knex](./knex) - example demonstrating knex with the Node.js agent * [Mock Infinite Tracing Server](./mock-infinite-tracing-server) - mock gRPC server to use to locally test [infinite tracing](https://docs.newrelic.com/docs/distributed-tracing/infinite-tracing/introduction-infinite-tracing/) with a Node.js applciation * [Nest](./nestjs) - examples demonstrating Nestjs with the Node.js agent * [Next.js](./nextjs) - examples demonstrating Next.js with the Node.js agent diff --git a/knex/LICENSE b/knex/LICENSE new file mode 100644 index 0000000..61d0cca --- /dev/null +++ b/knex/LICENSE @@ -0,0 +1,22 @@ +The MIT License (MIT) + +Copyright (c) 2018, Rob McLarty (http://robmclarty.com) + +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. diff --git a/knex/README.md b/knex/README.md new file mode 100644 index 0000000..99c86d3 --- /dev/null +++ b/knex/README.md @@ -0,0 +1,29 @@ +# Knex/Express Example Application + +This is an example app that uses the [New Relic hybrid agent](https://github.com/newrelic/newrelic-node-opentelemetry-integration) to demonstrate how to create both New Relic and OpenTelemetry spans. + + +## Setup +**Note**: This is used to demonstrate behavior between the New Relic Node.js agent and hybrid agent. The hybrid agent is currently only available to New Relic employees. + +### Get hybrid agent + +```sh +git clone git@github.com:newrelic/newrelic-node-opentelemetry-integration.git +npm install +npm link +``` + +```sh +npm install +cp env.sample .env +# Fill out `NEW_RELIC_LICENSE_KEY` +npm link @newrelic/opentelemetry-integration +docker compose up -d +npm run db:migrate +npm run db:seed +npm start +``` + +## Exploring Telemetry +More to come later... diff --git a/knex/config/database.js b/knex/config/database.js new file mode 100644 index 0000000..68789f2 --- /dev/null +++ b/knex/config/database.js @@ -0,0 +1,8 @@ +'use strict' + +const env = process.env.NODE_ENV || 'development' +const knexfile = require('../knexfile') +debugger +const knex = require('knex')(knexfile[env]) + +module.exports = knex diff --git a/knex/db/migrations/20180326094647_create_users.js b/knex/db/migrations/20180326094647_create_users.js new file mode 100644 index 0000000..8474f9c --- /dev/null +++ b/knex/db/migrations/20180326094647_create_users.js @@ -0,0 +1,14 @@ +exports.up = knex => { + return knex.schema.createTable('users', t => { + t.increments('id').primary().unsigned() + t.string('username').unique().index() + t.string('password') + t.string('email').unique().index() + t.timestamp('created_at').defaultTo(knex.fn.now()) + t.timestamp('updated_at').defaultTo(knex.fn.now()) + }) +} + +exports.down = knex => { + return knex.schema.dropTable('users') +} diff --git a/knex/db/migrations/20180326105452_create_projects.js b/knex/db/migrations/20180326105452_create_projects.js new file mode 100644 index 0000000..085e6f3 --- /dev/null +++ b/knex/db/migrations/20180326105452_create_projects.js @@ -0,0 +1,15 @@ +exports.up = knex => { + return knex.schema.createTable('projects', t => { + t.increments('id').primary().unsigned() + t.integer('user_id').references('users.id').unsigned().index().onDelete('CASCADE') + t.string('name') + t.text('description') + t.timestamp('completed_at') + t.timestamp('created_at').defaultTo(knex.fn.now()) + t.timestamp('updated_at').defaultTo(knex.fn.now()) + }) +} + +exports.down = knex => { + return knex.schema.dropTable('projects') +} diff --git a/knex/db/seeds/001_users_seed.js b/knex/db/seeds/001_users_seed.js new file mode 100644 index 0000000..155c24a --- /dev/null +++ b/knex/db/seeds/001_users_seed.js @@ -0,0 +1,19 @@ +'use strict' + +const { User } = require('../../server/models') + +exports.seed = knex => knex(User.tableName).del() + .then(() => [ + { + username: 'admin', + password: 'password', + email: 'admin@email.com' + }, + { + username: 'first-user', + password: 'another-password', + email: 'first-user@email.com' + } + ]) + .then(newUsers => Promise.all(newUsers.map(user => User.create(user)))) + .catch(err => console.log('err: ', err)) diff --git a/knex/db/seeds/002_project_seed.js b/knex/db/seeds/002_project_seed.js new file mode 100644 index 0000000..899e97d --- /dev/null +++ b/knex/db/seeds/002_project_seed.js @@ -0,0 +1,26 @@ +'use strict' + +const { User, Project } = require('../../server/models') + +exports.seed = knex => knex(Project.tableName).del() + .then(() => User.findAll()) + .then(users => { + if (users.length <= 0) throw 'No users found' + + return users[0].id + }) + .then(userId => [ + { + user_id: userId, + name: 'Sample Project', + description: 'This is just a sample project to create some data to look at.' + }, + { + user_id: userId, + name: 'Another Project', + description: 'This is another project of sample data.', + completed_at: knex.fn.now() + } + ]) + .then(newProjects => Promise.all(newProjects.map(project => Project.create(project)))) + .catch(err => console.log('err: ', err)) diff --git a/knex/docker-compose.yml b/knex/docker-compose.yml new file mode 100644 index 0000000..975ece2 --- /dev/null +++ b/knex/docker-compose.yml @@ -0,0 +1,18 @@ +version: '3' + +services: + db: + container_name: pg_knex + image: postgres:15 + ports: + - "5436:5436" + environment: + PGPORT: 5436 + POSTGRES_PASSWORD: newrelic + POSTGRES_DB: knex + healthcheck: + test: ["CMD", "pg_isready"] + interval: 1s + timeout: 10s + retries: 30 + diff --git a/knex/docs/index.md b/knex/docs/index.md new file mode 100644 index 0000000..040de06 --- /dev/null +++ b/knex/docs/index.md @@ -0,0 +1,7 @@ +# Knex Express Project Sample Documentation + +## Usage + +- [Auth](./usage/auth.md) +- [Users](./usage/users.md) +- [Projects](./usage/projects.md) diff --git a/knex/docs/usage/auth.md b/knex/docs/usage/auth.md new file mode 100644 index 0000000..2596b71 --- /dev/null +++ b/knex/docs/usage/auth.md @@ -0,0 +1,59 @@ +# Authentication + +This being a sample application, this is by no means a proper authentication +implementation and is only meant to demonstrate some very basic concepts in +the context of knex and express. + +## Login + +### Request + +```shell +curl \ + -X POST \ + -H "Content-Type: application/json" \ + -d '{"username":"admin", "password":"password"}' \ + http://localhost:3000/login +``` + +### Response + +```json +{ + "ok": true, + "message": "Login successful", + "user": { + "id": 1, + "username": "admin", + "email": "admin@email.com", + "created_at": "2018-04-03 14:43:02.183277-04", + "updated_at": "2018-04-03 14:43:02.183277-04" + } +} +``` + +## Register + +```shell +curl \ + -X POST \ + -H "Content-Type: application/json" \ + -d '{"username":"new-user", "password":"my-password", "email":"my@email.com"}' \ + http://localhost:3000/register +``` + +### Response + +```json +{ + "ok": true, + "message": "Registration successful", + "user": { + "id": 2, + "username": "new-user", + "email": "my@email.com", + "created_at": "2018-04-03 14:43:02.183277-04", + "updated_at": "2018-04-03 14:43:02.183277-04" + } +} +``` diff --git a/knex/docs/usage/projects.md b/knex/docs/usage/projects.md new file mode 100644 index 0000000..0588792 --- /dev/null +++ b/knex/docs/usage/projects.md @@ -0,0 +1,145 @@ +# Projects + +Basic CRUD for interacting with the JSON API's project resources. + +## Create + +### Request + +```shell +curl \ + -X POST \ + -H "Content-Type: application/json" \ + -d '{"name":"my-project", "description":"This is my description of this project."}' \ + http://localhost:3000/users/1/projects +``` + +### Response + +```json +{ + "ok": true, + "message": "Project created", + "project": { + "id": 1, + "name": "my-project", + "description": "This is my description of this project.", + "completed_at": null, + "created_at": "2018-04-03 14:43:02.183277-04", + "updated_at": "2018-04-03 14:43:02.183277-04" + } +} +``` + +## List + +### Request + +```shell +curl \ + -X GET \ + http://localhost:3000/users/1/projects +``` + +### Response + +```json +{ + "ok": true, + "message": "Projects found", + "projects": [ + { + "id": 1, + "name": "my-project", + "description": "This is my description of this project.", + "completed_at": null, + "created_at": "2018-04-03 14:43:02.183277-04", + "updated_at": "2018-04-03 14:43:02.183277-04" + }, + { + "id": 2, + "name": "my-other-project", + "description": "This is my other project description.", + "completed_at": "2018-04-03 14:43:02.183277-04", + "created_at": "2018-04-03 14:43:02.183277-04", + "updated_at": "2018-04-03 14:43:02.183277-04" + } + ] +} +``` + +## Get + +### Request + +```shell +curl \ + -X GET \ + http://localhost:3000/projects/1 +``` + +### Response + +```json +{ + "ok": true, + "message": "Project found", + "project": { + "id": 1, + "name": "my-project", + "description": "This is my description of this project.", + "completed_at": null, + "created_at": "2018-04-03 14:43:02.183277-04", + "updated_at": "2018-04-03 14:43:02.183277-04" + } +} +``` + +## Update + +### Request + +```shell +curl \ + -X PUT \ + -H "Content-Type: application/json" \ + -d '{"name":"my-new-name", "completed_at":"2018-04-03 14:43:02"}' \ + http://localhost:3000/projects/1 +``` + +### Response + +```json +{ + "ok": true, + "message": "Project updated", + "project": { + "id": 1, + "name": "my-new-name", + "description": "This is my description of this project.", + "completed_at": "2018-04-03 14:43:02.183277-04", + "created_at": "2018-04-03 14:43:02.183277-04", + "updated_at": "2018-04-03 14:43:02.183277-04" + } +} +``` + +## Delete + +### Request + +```shell +curl \ + -X DELETE \ + http://localhost:3000/projects/2 +``` + +### Response + +```json +{ + "ok": true, + "message": "Project '2' deleted", + "deleteCount": 1 +} +``` diff --git a/knex/docs/usage/users.md b/knex/docs/usage/users.md new file mode 100644 index 0000000..2f4828d --- /dev/null +++ b/knex/docs/usage/users.md @@ -0,0 +1,140 @@ +# Users + +Basic CRUD for interacting with the JSON API's user resources. + +## Create + +### Request + +```shell +curl \ + -X POST \ + -H "Content-Type: application/json" \ + -d '{"username":"new-user", "password":"my-password", "email":"my@email.com"}' \ + http://localhost:3000/users +``` + +### Response + +```json +{ + "ok": true, + "message": "User created", + "user": { + "id": 2, + "username": "new-user", + "email": "my@email.com", + "created_at": "2018-04-03 14:43:02.183277-04", + "updated_at": "2018-04-03 14:43:02.183277-04" + } +} +``` + +## List + +### Request + +```shell +curl \ + -X GET \ + http://localhost:3000/users +``` + +### Response + +```json +{ + "ok": true, + "message": "Users found", + "users": [ + { + "id": 1, + "username": "admin", + "email": "admin@email.com", + "created_at": "2018-04-03 14:43:02.183277-04", + "updated_at": "2018-04-03 14:43:02.183277-04" + }, + { + "id": 2, + "username": "new-user", + "email": "my@email.com", + "created_at": "2018-04-03 14:43:02.183277-04", + "updated_at": "2018-04-03 14:43:02.183277-04" + } + ] +} +``` + +## Get + +### Request + +```shell +curl \ + -X GET \ + http://localhost:3000/users/2 +``` + +### Response + +```json +{ + "ok": true, + "message": "User found", + "user": { + "id": 2, + "username": "new-user", + "email": "my@email.com", + "created_at": "2018-04-03 14:43:02.183277-04", + "updated_at": "2018-04-03 14:43:02.183277-04" + } +} +``` + +## Update + +### Request + +```shell +curl \ + -X PUT \ + -H "Content-Type: application/json" \ + -d '{"username":"name-changed", "password":"new-password", "email":"new@email.com"}' \ + http://localhost:3000/users/2 +``` + +### Response + +```json +{ + "ok": true, + "message": "User updated", + "user": { + "id": 2, + "username": "name-changed", + "email": "new@email.com", + "created_at": "2018-04-03 14:43:02.183277-04", + "updated_at": "2018-04-03 14:43:02.183277-04" + } +} +``` + +## Delete + +### Request + +```shell +curl \ + -X DELETE \ + http://localhost:3000/users/2 +``` + +### Response + +```json +{ + "ok": true, + "message": "User '2' deleted", + "deleteCount": 1 +} +``` diff --git a/knex/env.sample b/knex/env.sample new file mode 100644 index 0000000..8eb344d --- /dev/null +++ b/knex/env.sample @@ -0,0 +1,5 @@ +NEW_RELIC_LICENSE_KEY= + +# For NR devs using staging +#NEW_RELIC_HOST=staging-collector.newrelic.com +#NEW_RELIC_OTLP_ENDPOINT=staging-otlp.nr-data.net diff --git a/knex/knexfile.js b/knex/knexfile.js new file mode 100644 index 0000000..8201e74 --- /dev/null +++ b/knex/knexfile.js @@ -0,0 +1,23 @@ +'use strict' + +// ref: https://devhints.io/knex +// TODO: implement more dynamic env var settings loader +module.exports = { + development: { + client: 'pg', + connection: { + host: '127.0.0.1', + port: 5436, + user: 'postgres', + password: 'newrelic', + database: 'knex' + }, + migrations: { + tableName: 'knex_migrations', + directory: `${ __dirname }/db/migrations` + }, + seeds: { + directory: `${ __dirname }/db/seeds` + } + } +} diff --git a/knex/newrelic.js b/knex/newrelic.js new file mode 100644 index 0000000..ae275a0 --- /dev/null +++ b/knex/newrelic.js @@ -0,0 +1,52 @@ +/* + * Copyright 2024 New Relic Corporation. All rights reserved. + * SPDX-License-Identifier: Apache-2.0 + */ + +'use strict' + +/** + * New Relic agent configuration. + * + * See lib/config/default.js in the agent distribution for a more complete + * description of configuration variables and their potential values. */ + +exports.config = { + app_name: ['Example Knex App'], + logging: { + /** + * Level at which to log. 'trace' is most useful to New Relic when diagnosing + * issues with the agent, 'info' and higher will impose the least overhead on + * production applications. + */ + level: 'info' + }, + /** + * When true, all request headers except for those listed in attributes.exclude + * will be captured for all traces, unless otherwise specified in a destination's + * attributes include/exclude lists. + */ + allow_all_headers: true, + attributes: { + /** + * Prefix of attributes to exclude from all destinations. Allows * as wildcard + * at end. + * + * NOTE: If excluding headers, they must be in camelCase form to be filtered. + * + * @env NEW_RELIC_ATTRIBUTES_EXCLUDE + */ + exclude: [ + 'request.headers.cookie', + 'request.headers.authorization', + 'request.headers.proxyAuthorization', + 'request.headers.setCookie*', + 'request.headers.x*', + 'response.headers.cookie', + 'response.headers.authorization', + 'response.headers.proxyAuthorization', + 'response.headers.setCookie*', + 'response.headers.x*' + ] + } +} diff --git a/knex/package.json b/knex/package.json new file mode 100644 index 0000000..8f6bb3b --- /dev/null +++ b/knex/package.json @@ -0,0 +1,24 @@ +{ + "name": "knex-test", + "version": "0.0.6", + "description": "An example knex app structure.", + "main": "server/index.js", + "scripts": { + "start": "node -r @newrelic/opentelemetry-integration/start --env-file .env ./server/start.js", + "start:debug": "node --inspect-brk -r @newrelic/opentelemetry-integration/start --env-file .env ./server/start.js", + "db:migrate": "npx knex migrate:latest", + "db:seed": "npx knex seed:run" + }, + "author": "", + "license": "MIT", + "engines": { + "node": ">=18" + }, + "dependencies": { + "bcrypt": "^5.1.1", + "body-parser": "1.19.0", + "express": "4.17.1", + "knex": "^3.1.0", + "pg": "^8.12.0" + } +} diff --git a/knex/server/controllers/auth_controller.js b/knex/server/controllers/auth_controller.js new file mode 100644 index 0000000..efadc9b --- /dev/null +++ b/knex/server/controllers/auth_controller.js @@ -0,0 +1,54 @@ +'use strict' + +const { User } = require('../models') +const { + createError, + BAD_REQUEST, + UNAUTHORIZED +} = require('../helpers/error_helper') + +const postLogin = (req, res, next) => { + const username = String(req.body.username) + const password = String(req.body.password) + + if (!username || !password) next(createError({ + status: BAD_REQUEST, + message: '`username` + `password` are required fields' + })) + + User.verify(username, password) + .then(user => res.json({ + ok: true, + message: 'Login successful', + user + })) + .catch(err => next(createError({ + status: UNAUTHORIZED, + message: err + }))) +} + +const postRegister = (req, res, next) => { + const props = req.body.user + + User.findOne({ username: props.username }) + .then(user => { + if (user) return next(createError({ + status: CONFLICT, + message: 'Username already exists' + })) + + return User.create(props) + }) + .then(user => res.json({ + ok: true, + message: 'Registration successful', + user + })) + .catch(next) +} + +module.exports = { + postLogin, + postRegister +} diff --git a/knex/server/controllers/project_controller.js b/knex/server/controllers/project_controller.js new file mode 100644 index 0000000..8f8c130 --- /dev/null +++ b/knex/server/controllers/project_controller.js @@ -0,0 +1,75 @@ +'use strict' + +const { Project, User } = require('../models') + +const postProjects = (req, res, next) => { + const userId = req.params.id + const props = req.body.project + + Project.create({ ...props, user_id: userId }) + .then(project => res.json({ + ok: true, + message: 'Project created', + project, + userId + })) + .catch(next) +} + +const getProjects = (req, res, next) => { + const userId = req.params.id + + Project.findAll() + .then(projects => res.json({ + ok: true, + message: 'Projects found', + projects, + userId + })) + .catch(next) +} + +const getProject = (req, res, next) => { + const projectId = req.params.id + + Project.findById(projectId) + .then(project => res.json({ + ok: true, + message: 'Project found', + project + })) + .catch(next) +} + +const putProject = (req, res, next) => { + const projectId = req.params.id + const props = req.body.project + + Project.update(projectId, props) + .then(project => res.json({ + ok: true, + message: 'Project updated', + project + })) + .catch(next) +} + +const deleteProject = (req, res, next) => { + const projectId = req.params.id + + Project.destroy(projectId) + .then(deleteCount => res.json({ + ok: true, + message: 'Project deleted', + deleteCount + })) + .catch(next) +} + +module.exports = { + postProjects, + getProjects, + getProject, + putProject, + deleteProject +} diff --git a/knex/server/controllers/user_controller.js b/knex/server/controllers/user_controller.js new file mode 100644 index 0000000..214679d --- /dev/null +++ b/knex/server/controllers/user_controller.js @@ -0,0 +1,70 @@ +'use strict' + +const { User } = require('../models') + +const postUsers = (req, res, next) => { + const props = req.body.user + + User.create(props) + .then(user => res.json({ + ok: true, + message: 'User created', + user + })) + .catch(next) +} + +const getUsers = (req, res, next) => { + User.findAll() + .then(users => res.json({ + ok: true, + message: 'Users found', + users + })) + .catch(next) +} + +const getUser = (req, res, next) => { + const userId = req.params.id + + User.findById(userId) + .then(user => res.json({ + ok: true, + message: 'User found', + user + })) + .catch(next) +} + +const putUser = (req, res, next) => { + const userId = req.params.id + const props = req.body.user + + User.update(userId, props) + .then(user => res.json({ + ok: true, + message: 'User updated', + user + })) + .catch(next) +} + +const deleteUser = (req, res, next) => { + const userId = req.params.id + + User.destroy(userId) + .then(deleteCount => res.json({ + ok: true, + message: `User '${ userId }' deleted`, + deleteCount + })) + .catch(next) +} + +module.exports = { + postUsers, + getUsers, + getUser, + putUser, + deleteUser +} diff --git a/knex/server/helpers/error_helper.js b/knex/server/helpers/error_helper.js new file mode 100644 index 0000000..94c379c --- /dev/null +++ b/knex/server/helpers/error_helper.js @@ -0,0 +1,24 @@ +'use strict' + +// Simple helper method to create new errors with a specific status value +// attached to them, to match up with the codes and methods below. +const createError = ({ + status = 500, + message = 'Something went wrong' +}) => { + const error = new Error(message) + error.status = status + + return error +} + +module.exports = { + createError, + BAD_REQUEST: 400, + UNAUTHORIZED: 401, + FORBIDDEN: 403, + CONFLICT: 409, + NOT_FOUND: 404, + UNPROCESSABLE: 422, + GENERIC_ERROR: 500 +} diff --git a/knex/server/helpers/model-guts.js b/knex/server/helpers/model-guts.js new file mode 100644 index 0000000..514e571 --- /dev/null +++ b/knex/server/helpers/model-guts.js @@ -0,0 +1,76 @@ +'use strict' + +// The guts of a model that uses Knexjs to store and retrieve data from a +// database using the provided `knex` instance. Custom functionality can be +// composed on top of this set of common guts. +// +// The idea is that these are the most-used types of functions that most/all +// "models" will want to have. They can be overriden/modified/extended if +// needed by composing a new object out of the one returned by this function ;) +module.exports = ({ + knex = {}, + name = 'name', + tableName = 'tablename', + selectableProps = [], + timeout = 1000 +}) => { + const create = props => { + delete props.id // not allowed to set `id` + + return knex.insert(props) + .returning(selectableProps) + .into(tableName) + .timeout(timeout) + } + + const findAll = () => knex.select(selectableProps) + .from(tableName) + .timeout(timeout) + + const find = filters => knex.select(selectableProps) + .from(tableName) + .where(filters) + .timeout(timeout) + + // Same as `find` but only returns the first match if >1 are found. + const findOne = filters => find(filters) + .then(results => { + if (!Array.isArray(results)) return results + + return results[0] + }) + + const findById = id => knex.select(selectableProps) + .from(tableName) + .where({ id }) + .timeout(timeout) + + const update = (id, props) => { + delete props.id // not allowed to set `id` + + return knex.update(props) + .from(tableName) + .where({ id }) + .returning(selectableProps) + .timeout(timeout) + } + + const destroy = id => knex.del() + .from(tableName) + .where({ id }) + .timeout(timeout) + + return { + name, + tableName, + selectableProps, + timeout, + create, + findAll, + find, + findOne, + findById, + update, + destroy + } +} diff --git a/knex/server/index.js b/knex/server/index.js new file mode 100644 index 0000000..685e2a6 --- /dev/null +++ b/knex/server/index.js @@ -0,0 +1,19 @@ +'use strict' + +const express = require('express') +const bodyParser = require('body-parser') + +const app = express() + +app.use(bodyParser.json()) +app.disable('x-powered-by') + +app.use('/', [ + require('./routes/auth_routes'), + require('./routes/user_routes'), + require('./routes/project_routes') +]) + +app.use(require('./middleware/error_middleware').all) + +module.exports = app diff --git a/knex/server/middleware/error_middleware.js b/knex/server/middleware/error_middleware.js new file mode 100644 index 0000000..9271dae --- /dev/null +++ b/knex/server/middleware/error_middleware.js @@ -0,0 +1,109 @@ +'use strict' + +const { + BAD_REQUEST, + UNAUTHORIZED, + FORBIDDEN, + CONFLICT, + NOT_FOUND, + UNPROCESSABLE, + GENERIC_ERROR +} = require('../helpers/error_helper') + +const unauthorized = (err, req, res, next) => { + if (err.status !== UNAUTHORIZED) return next(err) + + res.status(UNAUTHORIZED).send({ + ok: false, + message: err.message || 'Unauthorized', + errors: [err] + }) +} + +const forbidden = (err, req, res, next) => { + if (err.status !== FORBIDDEN) return next(err) + + res.status(FORBIDDEN).send({ + ok: false, + message: err.message || 'Forbidden', + errors: [err] + }) +} + +const conflict = (err, req, res, next) => { + if (err.status !== CONFLICT) return next(err) + + res.status(CONFLICT).send({ + ok: false, + message: err.message || 'Conflict', + errors: [err] + }) +} + +const badRequest = (err, req, res, next) => { + if (err.status !== BAD_REQUEST) return next(err) + + res.status(BAD_REQUEST).send({ + ok: false, + message: err.message || 'Bad Request', + errors: [err] + }) +} + +const unprocessable = (err, req, res, next) => { + if (err.status !== UNPROCESSABLE) return next(err) + + res.status(UNPROCESSABLE).send({ + ok: false, + message: err.message || 'Unprocessable entity', + errors: [err] + }) +} + +// If there's nothing left to do after all this (and there's no error), +// return a 404 error. +const notFound = (err, req, res, next) => { + if (err.status !== NOT_FOUND) return next(err) + + res.status(NOT_FOUND).send({ + ok: false, + message: err.message || 'The requested resource could not be found' + }) +} + +// If there's still an error at this point, return a generic 500 error. +const genericError = (err, req, res, next) => { + res.status(GENERIC_ERROR).send({ + ok: false, + message: err.message || 'Internal server error', + errors: [err] + }) +} + +// If there's nothing left to do after all this (and there's no error), +// return a 404 error. +const catchall = (req, res, next) => { + res.status(NOT_FOUND).send({ + ok: false, + message: 'The requested resource could not be found' + }) +} + +const exportables = { + unauthorized, + forbidden, + conflict, + badRequest, + unprocessable, + genericError, + notFound, + catchall +} + +// All exportables stored as an array (e.g., for including in Express app.use()) +const all = Object.keys(exportables).map(key => exportables[key]) + +module.exports = { + ...exportables, + all +} diff --git a/knex/server/models/index.js b/knex/server/models/index.js new file mode 100644 index 0000000..9c99d4f --- /dev/null +++ b/knex/server/models/index.js @@ -0,0 +1,27 @@ +'use strict' + +const fs = require('fs') +const path = require('path') +const knex = require('../../config/database') + +const getModelFiles = dir => fs.readdirSync(dir) + .filter(file => (file.indexOf('.') !== 0) && (file !== 'index.js')) + .map(file => path.join(dir, file)) + +// Gather up all model files (i.e., any file present in the current directory +// that is not this file) and export them as properties of an object such that +// they may be imported using destructuring like +// `const { MyModel } = require('./models')` where there is a model named +// `MyModel` present in the exported object of gathered models. +const files = getModelFiles(__dirname) + +const models = files.reduce((modelsObj, filename) => { + const initModel = require(filename) + const model = initModel(knex) + + if (model) modelsObj[model.name] = model + + return modelsObj +}, {}) + +module.exports = models diff --git a/knex/server/models/project.js b/knex/server/models/project.js new file mode 100644 index 0000000..6de599e --- /dev/null +++ b/knex/server/models/project.js @@ -0,0 +1,30 @@ +'use strict' + +const createGuts = require('../helpers/model-guts') + +const name = 'Project' +const tableName = 'projects' + +const selectableProps = [ + 'id', + 'user_id', + 'name', + 'description', + 'completed_at', + 'updated_at', + 'created_at' +] + +module.exports = knex => { + debugger + const guts = createGuts({ + knex, + name, + tableName, + selectableProps + }) + + return { + ...guts + } +} diff --git a/knex/server/models/user.js b/knex/server/models/user.js new file mode 100644 index 0000000..2a6fb0c --- /dev/null +++ b/knex/server/models/user.js @@ -0,0 +1,72 @@ +'use strict' + +const bcrypt = require('bcrypt') +const createGuts = require('../helpers/model-guts') + +const name = 'User' +const tableName = 'users' + +// Properties that are allowed to be selected from the database for reading. +// (e.g., `password` is not included and thus cannot be selected) +const selectableProps = [ + 'id', + 'username', + 'email', + 'updated_at', + 'created_at' +] + +// Bcrypt functions used for hashing password and later verifying it. +const SALT_ROUNDS = 10 +const hashPassword = password => bcrypt.hash(password, SALT_ROUNDS) +const verifyPassword = (password, hash) => bcrypt.compare(password, hash) + +// Always perform this logic before saving to db. This includes always hashing +// the password field prior to writing so it is never saved in plain text. +const beforeSave = user => { + if (!user.password) return Promise.resolve(user) + + // `password` will always be hashed before being saved. + return hashPassword(user.password) + .then(hash => ({ ...user, password: hash })) + .catch(err => `Error hashing password: ${ err }`) +} + +module.exports = knex => { + const guts = createGuts({ + knex, + name, + tableName, + selectableProps + }) + + // Augment default `create` function to include custom `beforeSave` logic. + const create = props => beforeSave(props) + .then(user => guts.create(user)) + + const verify = (username, password) => { + const matchErrorMsg = 'Username or password do not match' + + knex.select() + .from(tableName) + .where({ username }) + .timeout(guts.timeout) + .then(user => { + if (!user) throw matchErrorMsg + + return user + }) + .then(user => Promise.all([user, verifyPassword(password, user.password)])) + .then(([user, isMatch]) => { + if (!isMatch) throw matchErrorMsg + + return user + }) + } + + return { + ...guts, + create, + verify + } +} diff --git a/knex/server/routes/auth_routes.js b/knex/server/routes/auth_routes.js new file mode 100644 index 0000000..a087bfa --- /dev/null +++ b/knex/server/routes/auth_routes.js @@ -0,0 +1,15 @@ +'use strict' + +const router = require('express').Router() +const { + postLogin, + postRegister +} = require('../controllers/auth_controller') + +router.route('/login') + .post(postLogin) + +router.route('/register') + .post(postRegister) + +module.exports = router diff --git a/knex/server/routes/project_routes.js b/knex/server/routes/project_routes.js new file mode 100644 index 0000000..5509744 --- /dev/null +++ b/knex/server/routes/project_routes.js @@ -0,0 +1,21 @@ +'use strict' + +const router = require('express').Router() +const { + postProjects, + getProjects, + getProject, + putProject, + deleteProject +} = require('../controllers/project_controller') + +router.route('/users/:id/projects') + .post(postProjects) + .get(getProjects) + +router.route('/projects/:id') + .get(getProject) + .put(putProject) + .delete(deleteProject) + +module.exports = router diff --git a/knex/server/routes/user_routes.js b/knex/server/routes/user_routes.js new file mode 100644 index 0000000..97011df --- /dev/null +++ b/knex/server/routes/user_routes.js @@ -0,0 +1,21 @@ +'use strict' + +const router = require('express').Router() +const { + postUsers, + getUsers, + getUser, + putUser, + deleteUser +} = require('../controllers/user_controller') + +router.route('/users') + .post(postUsers) + .get(getUsers) + +router.route('/users/:id') + .get(getUser) + .put(putUser) + .delete(deleteUser) + +module.exports = router diff --git a/knex/server/start.js b/knex/server/start.js new file mode 100755 index 0000000..16debf1 --- /dev/null +++ b/knex/server/start.js @@ -0,0 +1,11 @@ +'use strict' + +const PORT = process.env.PORT || 3000 + +const app = require('../server') + +app.listen(PORT, () => { + console.log(`Server started on port ${ PORT }`) +}).on('error', err => { + console.log('ERROR: ', err) +})