Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
21 changes: 21 additions & 0 deletions .eslintrc
Original file line number Diff line number Diff line change
@@ -0,0 +1,21 @@
{
"rules": {
"no-console": "off",
"indent": [ "error", 2 ],
"quotes": [ "error", "single" ],
"semi": ["error", "always"],
"linebreak-style": [ "error", "unix" ]
},
"env": {
"es6": true,
"node": true,
"mocha": true,
"jasmine": true
},
"ecmaFeatures": {
"modules": true,
"experimentalObjectRestSpread": true,
"impliedStrict": true
},
"extends": "eslint:recommended"
}
1 change: 1 addition & 0 deletions .gitignore
Original file line number Diff line number Diff line change
@@ -0,0 +1 @@
node_modules
35 changes: 5 additions & 30 deletions README.md
Original file line number Diff line number Diff line change
@@ -1,32 +1,7 @@
![cf](https://i.imgur.com/7v5ASc8.png) Lab 09: Vanilla REST API w/ Persistence
======
# Vanilla API Persistence - 09 Lab

## Submission Instructions
* fork this repository & create a new branch for your work
* write all of your code in a directory named `lab-` + `<your name>` **e.g.** `lab-susan`
* push to your repository
* submit a pull request to this repository
* submit a link to your PR in canvas
* write a question and observation on canvas
### Description:
This app builds out an API where data is stored in the file system. This API stores song data with the schema of name, band, and year.

## Learning Objectives
* students will learn how to save resource data to the file system for a layer of data persistence
* students will learn how to refactor commonly used coding constructs into custom helper modules

## Requirements

#### Configuration
* `package.json`
* `.eslintrc`
* `.gitignore`
* `README.md`
* your `README.md` should include detailed instructions on how to use your API
* this should include documentation on how to access your API endpoints

#### Feature Tasks
* continue working on your vanilla REST API
* refactor your routes to be contained in a separate module (ex: `route/resource-route.js`)
* refactor your `res` messages & status codes to be contained in a separate module (ex: `response.js`)
* refactor the `storage.js` module to use file system persistence
* use the `fs` module to create and read the associated data files
* the name of the file should contain the related resource id
### API:
The URL endpoint to access the api is `/api/song`. Using REST architecture the data is read, written and deleted using `GET`, `POST` and `DELETE` requests.
1 change: 1 addition & 0 deletions data/song/943488f2-4bb6-4fa0-b0b9-fe8cec6b1943.json
Original file line number Diff line number Diff line change
@@ -0,0 +1 @@
{"id":"943488f2-4bb6-4fa0-b0b9-fe8cec6b1943","name":"test name","band":"test band","year":"test year"}
30 changes: 30 additions & 0 deletions lib/parse-json.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,30 @@
'use strict';

module.exports = function(req) {
return new Promise((resolve, reject) => {
if(req.method === 'POST'|| req.method === 'PUT') {
var body = '';

req.on('data', data => {
body += data.toString();
});

req.on('end', () => {
try {
req.body = JSON.parse(body);
resolve(req);
} catch (err) {
console.error(err);
reject(err);
}
});

req.on('error', err => {
console.error(err);
reject(err);
});
return;
}
resolve();
});
};
11 changes: 11 additions & 0 deletions lib/parse-url.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,11 @@
'use strict';

const parseQuery = require('querystring').parse;
const parseUrl = require('url').parse;

module.exports = function(req) {
req.url = parseUrl(req.url);
req.url.query = parseQuery(req.url.query);

return Promise.resolve(req);
};
21 changes: 21 additions & 0 deletions lib/response.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,21 @@
'use strict';

module.exports = exports = {};

exports.sendJSON = function(res, status, data) {
res.writeHead(status, {
'Content-type': 'application/json'
});

res.write(JSON.stringify(data));
res.end();
};

exports.sendText = function(res, status, msg) {
res.writeHead(status, {
'Content-type': 'text/plain'
});

res.write(msg);
res.end();
};
61 changes: 61 additions & 0 deletions lib/router.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,61 @@
'use strict';

const parseUrl = require('./parse-url.js');
const parseJSON = require('./parse-json.js');

const Router = module.exports = function() {
this.routes = {
GET: {},
POST: {},
PUT: {},
DELETE: {}
};
};

Router.prototype.get = function(endpoint, callback) {
this.routes.GET[endpoint] = callback;
};

Router.prototype.post = function(endpoint, callback) {
this.routes.POST[endpoint] = callback;
};

Router.prototype.put = function(endpoint, callback) {
this.routes.PUT[endpoint] = callback;
};

Router.prototype.delete = function(endpoint, callback) {
this.routes.DELETE[endpoint] = callback;
};

Router.prototype.route = function() {
return (req, res) => {
Promise.all([
parseUrl(req),
parseJSON(req)
])
.then( () => {
if(typeof this.routes[req.method][req.url.pathname] === 'function') {
this.routes[req.method][req.url.pathname](req, res);
return;
}
console.error('route not found');

res.writeHead(404, {
'Content-type': 'text/plain'
});
res.write('route not found');
res.end();
})
.catch(err => {
console.log(err);

res.writeHead(400, {
'Content-type': 'text/plain'
});

res.write('bad result');
res.end();
});
};
};
48 changes: 48 additions & 0 deletions lib/storage.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,48 @@
'use strict';

const Promise = require('bluebird');
const fs = Promise.promisifyAll(require('fs'), { suffix: 'Prom' });

module.exports = exports = {};

exports.createItem = function(schemaName, item) {
if(!schemaName) return Promise.reject(new Error('expected schema name'));
if(!item) return Promise.reject(new Error('expected item'));

let json = JSON.stringify(item);

return fs.writeFileProm(`${__dirname}/../data/${schemaName}/${item.id}.json`, json)
.then( () => item)
.catch( err => Promise.reject(err));
};

exports.fetchItem = function(schemaName, id) {
if(!schemaName) return Promise.reject(new Error('expected schema name'));
if(!id) return Promise.reject(new Error('expected id'));

return fs.readFileProm(`${__dirname}/../data/${schemaName}/${id}.json`)
.then( data => {
try {
let item = JSON.parse(data.toString());
return item;
} catch (err) {
Promise.reject(err);
}
})
.catch( err => Promise.reject(err));
};

exports.deleteItem = function(schemaName, item) {
if(!schemaName) return Promise.reject(new Error('expected schema name'));
if(!item) return Promise.reject(new Error('expected item'));

return fs.unlinkProm(`${__dirname}/../data/${schemaName}/${item.id}.json`)
.then( () => {
try {
console.log('song has been deleted');
} catch (err) {
Promise.reject(err);
}
})
.catch( err => Promise.reject(err));
};
14 changes: 14 additions & 0 deletions model/song.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,14 @@
'use strict';

const uuidv4 = require('uuid/v4');

module.exports = function(name, band, year) {
if (!name) throw new Error('expected name');
if (!band) throw new Error('expected band');
if (!year) throw new Error('expected year');

this.id = uuidv4();
this.name = name;
this.band = band;
this.year = year;
};
33 changes: 33 additions & 0 deletions package.json
Original file line number Diff line number Diff line change
@@ -0,0 +1,33 @@
{
"name": "09-vanilla_api_persistence",
"version": "1.0.0",
"description": "",
"main": "server.js",
"directories": {
"test": "test"
},
"dependencies": {
"bluebird": "^3.5.0",
"uuid": "^3.1.0"
},
"devDependencies": {
"chai": "^4.1.0",
"mocha": "^3.4.2",
"superagent": "^3.5.2"
},
"scripts": {
"test": "mocha",
"start": "node server.js"
},
"repository": {
"type": "git",
"url": "git+https://github.com/nickjaz/09-vanilla_api_persistence.git"
},
"keywords": [],
"author": "",
"license": "ISC",
"bugs": {
"url": "https://github.com/nickjaz/09-vanilla_api_persistence/issues"
},
"homepage": "https://github.com/nickjaz/09-vanilla_api_persistence#readme"
}
48 changes: 48 additions & 0 deletions route/song-route.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,48 @@
'use strict';

const storage = require('../lib/storage.js');
const response = require('../lib/response.js');
const Song = require('../model/song.js');

module.exports = function(router) {
router.get('/api/song', function(req, res) {
if(req.url.query.id) {
storage.fetchItem('song', req.url.query.id)
.then( song => {
response.sendJSON(res, 200, song);
})
.catch( err => {
console.error(err);
response.sendText(res, 404, 'song not found');
});
return;
}
response.sendText(res, 400, 'bad request');
});

router.post('/api/song', function(req, res) {
try {
var song = new Song(req.body.name, req.body.band, req.body.year);
storage.createItem('song', song);
response.sendJSON(res, 200, song);
} catch (err) {
console.error(err);
response.sendText(res, 400, 'bad request');
}
});

router.delete('/api/song', function(req, res) {
if(req.url.query.id) {
storage.deleteItem('song', req.url.query.id)
.then( () => {
response.sendText(res, 204, 'song deleted');
})
.catch( err => {
console.log(err);
response.sendText(res, 404, 'song not found');
});
return;
}
response.sendText(res, 400, 'bad request');
});
};
14 changes: 14 additions & 0 deletions server.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,14 @@
'use strict';

const http = require('http');
const Router = require('./lib/router.js');
const PORT = process.env.PORT || 3000;
const router = new Router();

require('./route/song-route.js')(router);

const server = http.createServer(router.route());

server.listen(PORT, function() {
console.log('listening on:', PORT);
});
Loading