Skip to content

Instantly share code, notes, and snippets.

@lorenzofox3
Created April 11, 2021 14:21
Show Gist options
  • Save lorenzofox3/ca79210d8cf49d44473e89db4616f4e4 to your computer and use it in GitHub Desktop.
Save lorenzofox3/ca79210d8cf49d44473e89db4616f4e4 to your computer and use it in GitHub Desktop.
separation-of-concern
import {createService} from './movies-service.js';
export default async (instance) => {
const {Movie} = instance; // mongoose model injected
instance.register(async (instance) => {
// we overwrite for the scope by our service instead
instance.decorate('Movie', createService({model: Movie}));
instance.register(routesPlugin);
});
}
export const routesPlugin = async (instance) => {
const {Movie} = instance; // this one will then be the service instead
instance.route({
method: 'GET',
url: '/:movieSlug',
async handler(req, res) {
const {params} = req;
const movie = await Movie.getOneBySlug(params.movieSlug);
instance.assert(movie, 404);
return movie;
}
});
instance.route({
method: 'GET',
url: '/',
async handler(req, res) {
return Movie.listAll();
}
});
instance.route({
method: 'DELETE',
url: '/:movieSlug',
async handler(req, res) {
const slug = req.params.movieSlug;
const movie = await Movie.getOneBySlug(slug);
instance.assert(movie, 404);
await Movie.delete(slug);
res.statusCode = 204;
}
});
instance.route({
method: 'POST',
url: '/',
schema: {
body: {
type: 'object',
properties: {
title: {
type: 'string'
},
description: {
type: 'string'
}
},
required: ['title']
}
},
async handler(req, res) {
const movie = await Movie.getOneByTitle(req.body.title);
instance.assert(!movie, 409, 'movie already exists');
return Movie.create(req.body);
}
});
};
export default {
db: {
uri: 'mongodb://localhost:27017/movies'
},
server: {
port: 9001
}
};
import slug from 'slug';
import mongoose from 'mongoose';
export const MovieSchema = new mongoose.Schema({
title: {
type: String,
required: true
},
slug: {
type: String,
unique: true,
required: true,
default: function () {
return slug(this.title);
}
},
likes: {
type: Number,
default: 0
},
description: {
type: String
},
isDeleted: {
type: Boolean,
default: false
}
}, {
versionKey: false
});
import fp from 'fastify-plugin'
import mongoose from 'mongoose';
import {MovieSchema} from './db-models.js';
export const createDBPlugin = ({db = mongoose } = {}) => fp(async (intance, opts) =>{
await db.connect(opts.db.uri);
intance
.decorate('Movie', db.model('Movie', MovieSchema))
.addHook('onClose', () =>db.close());
});
POST http://localhost:9001/movies
Content-Type: application/json
{
"title": "Forest Gump",
"description": "foo bar bim"
}
###
DELETE http://localhost:9001/movies/forest-gump
import fastify from 'fastify';
import fastifySensible from 'fastify-sensible';
import mongoose from 'mongoose';
import {createDBPlugin} from './db-plugin.js';
import conf from './conf.js';
import api from './api.js';
const db = new mongoose.Mongoose();
const app = fastify({
logger: true
});
app.register(fastifySensible);
// ... eventually other middleware etc
app.register(createDBPlugin({db}), conf);
app.register(api, {...conf, prefix:'/movies'})
app.listen(conf.server.port);
export const createService = ({model}) => {
return {
getOneBySlug(slug) {
return model.findOne({
slug,
isDeleted: {$ne: true}
}).select({
isDeleted: 0
});
},
getOneByTitle(title) {
return model.findOne({
title,
isDeleted: {$ne: true}
}).select({
isDeleted: 0
});
},
listAll() {
return model.find({
isDeleted: {$ne: true}
}).select({
isDeleted: 0
});
},
async create(movie) {
return new model(movie).save();
},
async delete(slug) {
await model.findOneAndUpdate({
slug
}, {
$set: {
isDeleted: true
}
});
}
};
};
{
"name": "separation-of-concerns",
"version": "1.0.0",
"description": "",
"main": "index.js",
"type": "module",
"scripts": {
"test": "pta *.test.js",
"dev": "nodemon movie-store/index.js"
},
"author": "",
"license": "ISC",
"dependencies": {
"fastify": "~3.14.1",
"fastify-sensible": "~3.1.1",
"mongoose": "~5.12.3",
"slug": "~4.0.3"
},
"devDependencies": {
"fastify-plugin": "~3.0.0",
"nodemon": "~2.0.7",
"pta": "~0.2.2",
"sbuts": "~0.4.1"
}
}
import fastify from 'fastify';
import fastifySensible from 'fastify-sensible';
import {routesPlugin} from './api.js';
import stub from 'sbuts';
export default (t) => {
const createTestApp = ({getOneBySlug}) => {
const app = fastify();
app.register(fastifySensible);
// inject mock
app.decorate('Movie', {
getOneBySlug
});
app.register(routesPlugin);
return app;
};
t.test(`GET /:movieSlug should return 200 with the matching movie`, async (t) => {
const forestGump = {
likes: 0,
_id: '6071d76a0d5f31cb3e7e8a31',
title: 'Forest Gump',
description: 'foo bar bim',
slug: 'forest-gump'
};
const getOneBySlug = stub().resolve(forestGump);
const app = createTestApp({getOneBySlug});
const response = await app.inject({
method: 'GET',
url: '/forest-gump'
});
t.eq(response.statusCode, 200);
const json = await response.json();
t.eq(json, forestGump);
t.eq(getOneBySlug.calls, [['forest-gump']])
});
t.test(`GET /:movieSlug should return 404 if no movie matches`, async (t) => {
const getOneBySlug = stub().resolve(null);
const app = createTestApp({getOneBySlug});
const response = await app.inject({
method: 'GET',
url: '/forest-gump'
});
t.eq(response.statusCode, 404);
t.eq(getOneBySlug.calls, [['forest-gump']])
});
}
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment