1
0
Fork 0
mirror of https://github.com/codex-team/codex.docs.git synced 2025-08-09 15:35:25 +02:00

Initial database and page MVC

This commit is contained in:
gohabereg@gmail.com 2018-08-10 21:04:14 +03:00
parent 2e717f6415
commit 3a1bb7b2a4
12 changed files with 468 additions and 25 deletions

5
.gitignore vendored
View file

@ -61,4 +61,7 @@ typings/
.next
# JetBrains IDE environment
.idea/
.idea/
# Database files
db/

10
app.js
View file

@ -4,8 +4,7 @@ const path = require('path');
const cookieParser = require('cookie-parser');
const logger = require('morgan');
const indexRouter = require('./routes/index');
const usersRouter = require('./routes/users');
const routes = require('./routes');
const app = express();
@ -13,15 +12,14 @@ const app = express();
app.set('views', path.join(__dirname, 'views'));
app.set('view engine', 'twig');
app.use(logger('dev'));
app.use(express.json());
app.use(express.urlencoded({ extended: false }));
app.use(express.urlencoded({extended: true}))
app.use(cookieParser());
app.use(express.static(path.join(__dirname, 'public')));
app.use('/', indexRouter);
app.use('/users', usersRouter);
app.use('/', routes);
// catch 404 and forward to error handler
app.use(function(req, res, next) {
next(createError(404));

33
controllers/pages.js Normal file
View file

@ -0,0 +1,33 @@
const model = require('../models/page');
class Pages {
static get REQUIRED_FIELDS () {
return ['title', 'body'];
}
static async insert(data) {
if (!Pages.validate(data)) {
throw new Error('Invalid request format')
}
const page = new model(null, data);
return page.save();
}
static validate (data) {
return Pages.REQUIRED_FIELDS.every(field => data.hasOwnProperty(field));
}
static async update(id, data) {
const page = new model(id, data);
if (!page._id) {
throw new Error('Page with given id does not exist');
}
return page.save()
}
}
module.exports = Pages;

79
database/index.js Normal file
View file

@ -0,0 +1,79 @@
const pages = require('./pages');
class Database {
constructor (nedbInstance) {
this.db = nedbInstance;
}
async insert (doc) {
return new Promise((res, rej) => this.db.insert(doc, (err, newDoc) => {
if (err) {
rej(err);
}
res(newDoc);
}))
}
async find (query, projection) {
const cbk = (res, rej) => (err, docs) => {
if (err) {
rej(err);
}
res(docs);
}
return new Promise((res, rej) => {
if (projection) {
this.db.find(query, projection, cbk(res, rej));
} else {
this.db.find(query, cbk(res, rej));
}
})
}
async findOne (query, projection) {
const cbk = (res, rej) => (err, doc) => {
if (err) {
rej(err);
}
res(doc);
}
return new Promise((res, rej) => {
if (projection) {
this.db.findOne(query, projection, cbk(res, rej));
} else {
this.db.findOne(query, cbk(res, rej));
}
})
}
async update (query, update, options = {}) {
return new Promise((res, rej) => this.db.update(query, update, options, (err, result) => {
if (err) {
rej(err);
}
res(result);
}))
}
async remove (query, options = {}) {
return new Promise((res, rej) => this.db.remove(query, options, (err, result) => {
if (err) {
rej(err);
}
res(result);
}))
}
}
module.exports = {
pages: new Database(pages)
};

5
database/pages.js Normal file
View file

@ -0,0 +1,5 @@
const Datastore = require('nedb');
const db = new Datastore({filename: './db/pages.db', autoload: true}) ;
module.exports = db;

67
models/page.js Normal file
View file

@ -0,0 +1,67 @@
const uuid4 = require('uuid4');
const {pages} = require('../database');
class Page {
constructor (id, data = {}) {
this.db = pages;
if (id) {
this.db.findOne({_id: id}).then(page => {
console.log(page)
if (!page) {
this.data = data;
return;
}
this._id = page._id;
this.data = page;
}).catch(ignored => {
this.data = data;
})
} else {
this.data = data;
}
}
set data (pageData) {
const {title, body, parent} = pageData;
this.title = title || this.title;
this.body = body || this.body;
this._parent = parent || this._parent;
}
get data () {
return {
_id: this._id,
title: this.title,
body: this.body,
parent: this._parent
}
}
set parent (parentPage) {
this._parent = parentPage.id;
}
get parent () {
return this.db.find({_id: this._parent});
}
async save () {
if (!this._id) {
const insertedRow = await this.db.insert(this.data);
this._id = insertedRow._id;
} else {
await this.db.update({_id: this._id}, this.data);
}
return this;
}
}
module.exports = Page;

View file

@ -6,11 +6,15 @@
"start": "node ./bin/www"
},
"dependencies": {
"body-parser": "latest",
"cookie-parser": "~1.4.3",
"debug": "~2.6.9",
"express": "~4.16.0",
"http-errors": "~1.6.2",
"morgan": "~1.9.0",
"twig": "~0.10.3"
"multer": "^1.3.1",
"nedb": "^1.8.0",
"twig": "~0.10.3",
"uuid4": "^1.0.0"
}
}

9
routes/home.js Normal file
View file

@ -0,0 +1,9 @@
const express = require('express')
const router = express.Router();
/* GET home page. */
router.get('/', function(req, res, next) {
res.render('index', { title: 'Express' });
});
module.exports = router;

View file

@ -1,9 +1,10 @@
const express = require('express')
const router = express.Router();
/* GET home page. */
router.get('/', function(req, res, next) {
res.render('index', { title: 'Express' });
});
const home = require('./home');
const pages = require('./pages');
router.use('/', home);
router.use('/', pages);
module.exports = router;

46
routes/pages.js Normal file
View file

@ -0,0 +1,46 @@
const express = require('express');
const router = express.Router();
const multer = require('multer')();
const Pages = require('../controllers/pages');
/* GET users listing. */
router.put('/page', multer.any(), async (req, res,) => {
try {
const page = await Pages.insert(req.body);
res.json({
success: true,
result: page.data
});
} catch (err) {
res.status(400).json({
success: false,
error: err.message
});
}
});
router.post('/page/:id', multer.any(), async (req, res) => {
const {id} = req.params;
console.log(id);
try {
const page = await Pages.update(id, req.body);
res.json({
success: true,
result: page.data
});
} catch (err) {
res.status(400).json({
success: false,
error: err.message
});
}
})
router.put
module.exports = router;

View file

@ -1,9 +0,0 @@
const express = require('express');
const router = express.Router();
/* GET users listing. */
router.get('/', function(req, res, next) {
res.send('respond with a resource');
});
module.exports = router;

215
yarn.lock
View file

@ -9,10 +9,18 @@ accepts@~1.3.5:
mime-types "~2.1.18"
negotiator "0.6.1"
append-field@^0.1.0:
version "0.1.0"
resolved "https://registry.yarnpkg.com/append-field/-/append-field-0.1.0.tgz#6ddc58fa083c7bc545d3c5995b2830cc2366d44a"
array-flatten@1.1.1:
version "1.1.1"
resolved "https://registry.yarnpkg.com/array-flatten/-/array-flatten-1.1.1.tgz#9a5f699051b1e7073328f2a008968b64ea2955d2"
async@0.2.10:
version "0.2.10"
resolved "https://registry.yarnpkg.com/async/-/async-0.2.10.tgz#b6bbe0b0674b9d719708ca38de8c237cb526c3d1"
balanced-match@^1.0.0:
version "1.0.0"
resolved "https://registry.yarnpkg.com/balanced-match/-/balanced-match-1.0.0.tgz#89b4d199ab2bee49de164ea02b89ce462d71b767"
@ -23,6 +31,12 @@ basic-auth@~2.0.0:
dependencies:
safe-buffer "5.1.1"
binary-search-tree@0.2.5:
version "0.2.5"
resolved "https://registry.yarnpkg.com/binary-search-tree/-/binary-search-tree-0.2.5.tgz#7dbb3b210fdca082450dad2334c304af39bdc784"
dependencies:
underscore "~1.4.4"
body-parser@1.18.2:
version "1.18.2"
resolved "https://registry.yarnpkg.com/body-parser/-/body-parser-1.18.2.tgz#87678a19d84b47d859b83199bd59bce222b10454"
@ -38,6 +52,21 @@ body-parser@1.18.2:
raw-body "2.3.2"
type-is "~1.6.15"
body-parser@latest:
version "1.18.3"
resolved "https://registry.yarnpkg.com/body-parser/-/body-parser-1.18.3.tgz#5b292198ffdd553b3a0f20ded0592b956955c8b4"
dependencies:
bytes "3.0.0"
content-type "~1.0.4"
debug "2.6.9"
depd "~1.1.2"
http-errors "~1.6.3"
iconv-lite "0.4.23"
on-finished "~2.3.0"
qs "6.5.2"
raw-body "2.3.3"
type-is "~1.6.16"
brace-expansion@^1.1.7:
version "1.1.11"
resolved "https://registry.yarnpkg.com/brace-expansion/-/brace-expansion-1.1.11.tgz#3c7fcbf529d87226f3d2f52b966ff5271eb441dd"
@ -45,6 +74,17 @@ brace-expansion@^1.1.7:
balanced-match "^1.0.0"
concat-map "0.0.1"
buffer-from@^1.0.0:
version "1.1.1"
resolved "https://registry.yarnpkg.com/buffer-from/-/buffer-from-1.1.1.tgz#32713bc028f75c02fdb710d7c7bcec1f2c6070ef"
busboy@^0.2.11:
version "0.2.14"
resolved "https://registry.yarnpkg.com/busboy/-/busboy-0.2.14.tgz#6c2a622efcf47c57bbbe1e2a9c37ad36c7925453"
dependencies:
dicer "0.2.5"
readable-stream "1.1.x"
bytes@3.0.0:
version "3.0.0"
resolved "https://registry.yarnpkg.com/bytes/-/bytes-3.0.0.tgz#d32815404d689699f85a4ea4fa8755dd13a96048"
@ -53,6 +93,15 @@ concat-map@0.0.1:
version "0.0.1"
resolved "https://registry.yarnpkg.com/concat-map/-/concat-map-0.0.1.tgz#d8a96bd77fd68df7793a73036a3ba0d5405d477b"
concat-stream@^1.5.2:
version "1.6.2"
resolved "https://registry.yarnpkg.com/concat-stream/-/concat-stream-1.6.2.tgz#904bdf194cd3122fc675c77fc4ac3d4ff0fd1a34"
dependencies:
buffer-from "^1.0.0"
inherits "^2.0.3"
readable-stream "^2.2.2"
typedarray "^0.0.6"
content-disposition@0.5.2:
version "0.5.2"
resolved "https://registry.yarnpkg.com/content-disposition/-/content-disposition-0.5.2.tgz#0cf68bb9ddf5f2be7961c3a85178cb85dba78cb4"
@ -76,6 +125,10 @@ cookie@0.3.1:
version "0.3.1"
resolved "https://registry.yarnpkg.com/cookie/-/cookie-0.3.1.tgz#e7e0a1f9ef43b4c8ba925c5c5a96e806d16873bb"
core-util-is@~1.0.0:
version "1.0.2"
resolved "https://registry.yarnpkg.com/core-util-is/-/core-util-is-1.0.2.tgz#b5fd54220aa2bc5ab57aab7140c940754503c1a7"
debug@2.6.9, debug@~2.6.9:
version "2.6.9"
resolved "https://registry.yarnpkg.com/debug/-/debug-2.6.9.tgz#5d128515df134ff327e90a4c93f4e077a536341f"
@ -94,6 +147,13 @@ destroy@~1.0.4:
version "1.0.4"
resolved "https://registry.yarnpkg.com/destroy/-/destroy-1.0.4.tgz#978857442c44749e4206613e37946205826abd80"
dicer@0.2.5:
version "0.2.5"
resolved "https://registry.yarnpkg.com/dicer/-/dicer-0.2.5.tgz#5996c086bb33218c812c090bddc09cd12facb70f"
dependencies:
readable-stream "1.1.x"
streamsearch "0.1.2"
ee-first@1.1.1:
version "1.1.1"
resolved "https://registry.yarnpkg.com/ee-first/-/ee-first-1.1.1.tgz#590c61156b0ae2f4f0255732a158b266bc56b21d"
@ -178,7 +238,7 @@ http-errors@1.6.2:
setprototypeof "1.0.3"
statuses ">= 1.3.1 < 2"
http-errors@~1.6.2:
http-errors@1.6.3, http-errors@~1.6.2, http-errors@~1.6.3:
version "1.6.3"
resolved "https://registry.yarnpkg.com/http-errors/-/http-errors-1.6.3.tgz#8b55680bb4be283a0b5bf4ea2e38580be1d9320d"
dependencies:
@ -191,7 +251,17 @@ iconv-lite@0.4.19:
version "0.4.19"
resolved "https://registry.yarnpkg.com/iconv-lite/-/iconv-lite-0.4.19.tgz#f7468f60135f5e5dad3399c0a81be9a1603a082b"
inherits@2.0.3:
iconv-lite@0.4.23:
version "0.4.23"
resolved "https://registry.yarnpkg.com/iconv-lite/-/iconv-lite-0.4.23.tgz#297871f63be507adcfbfca715d0cd0eed84e9a63"
dependencies:
safer-buffer ">= 2.1.2 < 3"
immediate@~3.0.5:
version "3.0.6"
resolved "https://registry.yarnpkg.com/immediate/-/immediate-3.0.6.tgz#9db1dbd0faf8de6fbe0f5dd5e56bb606280de69b"
inherits@2.0.3, inherits@^2.0.3, inherits@~2.0.1, inherits@~2.0.3:
version "2.0.3"
resolved "https://registry.yarnpkg.com/inherits/-/inherits-2.0.3.tgz#633c2c83e3da42a502f52466022480f4208261de"
@ -199,6 +269,26 @@ ipaddr.js@1.8.0:
version "1.8.0"
resolved "https://registry.yarnpkg.com/ipaddr.js/-/ipaddr.js-1.8.0.tgz#eaa33d6ddd7ace8f7f6fe0c9ca0440e706738b1e"
isarray@0.0.1:
version "0.0.1"
resolved "https://registry.yarnpkg.com/isarray/-/isarray-0.0.1.tgz#8a18acfca9a8f4177e09abfc6038939b05d1eedf"
isarray@~1.0.0:
version "1.0.0"
resolved "https://registry.yarnpkg.com/isarray/-/isarray-1.0.0.tgz#bb935d48582cba168c06834957a54a3e07124f11"
lie@3.1.1:
version "3.1.1"
resolved "https://registry.yarnpkg.com/lie/-/lie-3.1.1.tgz#9a436b2cc7746ca59de7a41fa469b3efb76bd87e"
dependencies:
immediate "~3.0.5"
localforage@^1.3.0:
version "1.7.2"
resolved "https://registry.yarnpkg.com/localforage/-/localforage-1.7.2.tgz#fa4442602f806edd2bca6a54ab4e656f031f121c"
dependencies:
lie "3.1.1"
locutus@^2.0.5:
version "2.0.9"
resolved "https://registry.yarnpkg.com/locutus/-/locutus-2.0.9.tgz#e265af1e85fd19173e74386373888560783a02fc"
@ -235,6 +325,16 @@ minimatch@3.0.x:
dependencies:
brace-expansion "^1.1.7"
minimist@0.0.8:
version "0.0.8"
resolved "https://registry.yarnpkg.com/minimist/-/minimist-0.0.8.tgz#857fcabfc3397d2625b8228262e86aa7a011b05d"
mkdirp@^0.5.1, mkdirp@~0.5.1:
version "0.5.1"
resolved "https://registry.yarnpkg.com/mkdirp/-/mkdirp-0.5.1.tgz#30057438eac6cf7f8c4767f38648d6697d75c903"
dependencies:
minimist "0.0.8"
morgan@~1.9.0:
version "1.9.0"
resolved "https://registry.yarnpkg.com/morgan/-/morgan-1.9.0.tgz#d01fa6c65859b76fcf31b3cb53a3821a311d8051"
@ -249,11 +349,38 @@ ms@2.0.0:
version "2.0.0"
resolved "https://registry.yarnpkg.com/ms/-/ms-2.0.0.tgz#5608aeadfc00be6c2901df5f9861788de0d597c8"
multer@^1.3.1:
version "1.3.1"
resolved "https://registry.yarnpkg.com/multer/-/multer-1.3.1.tgz#c3fb3b35f50c7eefe873532f90d3dde02ce6e040"
dependencies:
append-field "^0.1.0"
busboy "^0.2.11"
concat-stream "^1.5.2"
mkdirp "^0.5.1"
object-assign "^3.0.0"
on-finished "^2.3.0"
type-is "^1.6.4"
xtend "^4.0.0"
nedb@^1.8.0:
version "1.8.0"
resolved "https://registry.yarnpkg.com/nedb/-/nedb-1.8.0.tgz#0e3502cd82c004d5355a43c9e55577bd7bd91d88"
dependencies:
async "0.2.10"
binary-search-tree "0.2.5"
localforage "^1.3.0"
mkdirp "~0.5.1"
underscore "~1.4.4"
negotiator@0.6.1:
version "0.6.1"
resolved "https://registry.yarnpkg.com/negotiator/-/negotiator-0.6.1.tgz#2b327184e8992101177b28563fb5e7102acd0ca9"
on-finished@~2.3.0:
object-assign@^3.0.0:
version "3.0.0"
resolved "https://registry.yarnpkg.com/object-assign/-/object-assign-3.0.0.tgz#9bedd5ca0897949bca47e7ff408062d549f587f2"
on-finished@^2.3.0, on-finished@~2.3.0:
version "2.3.0"
resolved "https://registry.yarnpkg.com/on-finished/-/on-finished-2.3.0.tgz#20f1336481b083cd75337992a16971aa2d906947"
dependencies:
@ -271,6 +398,10 @@ path-to-regexp@0.1.7:
version "0.1.7"
resolved "https://registry.yarnpkg.com/path-to-regexp/-/path-to-regexp-0.1.7.tgz#df604178005f522f15eb4490e7247a1bfaa67f8c"
process-nextick-args@~2.0.0:
version "2.0.0"
resolved "https://registry.yarnpkg.com/process-nextick-args/-/process-nextick-args-2.0.0.tgz#a37d732f4271b4ab1ad070d35508e8290788ffaa"
proxy-addr@~2.0.3:
version "2.0.4"
resolved "https://registry.yarnpkg.com/proxy-addr/-/proxy-addr-2.0.4.tgz#ecfc733bf22ff8c6f407fa275327b9ab67e48b93"
@ -282,6 +413,10 @@ qs@6.5.1:
version "6.5.1"
resolved "https://registry.yarnpkg.com/qs/-/qs-6.5.1.tgz#349cdf6eef89ec45c12d7d5eb3fc0c870343a6d8"
qs@6.5.2:
version "6.5.2"
resolved "https://registry.yarnpkg.com/qs/-/qs-6.5.2.tgz#cb3ae806e8740444584ef154ce8ee98d403f3e36"
range-parser@~1.2.0:
version "1.2.0"
resolved "https://registry.yarnpkg.com/range-parser/-/range-parser-1.2.0.tgz#f49be6b487894ddc40dcc94a322f611092e00d5e"
@ -295,10 +430,48 @@ raw-body@2.3.2:
iconv-lite "0.4.19"
unpipe "1.0.0"
raw-body@2.3.3:
version "2.3.3"
resolved "https://registry.yarnpkg.com/raw-body/-/raw-body-2.3.3.tgz#1b324ece6b5706e153855bc1148c65bb7f6ea0c3"
dependencies:
bytes "3.0.0"
http-errors "1.6.3"
iconv-lite "0.4.23"
unpipe "1.0.0"
readable-stream@1.1.x:
version "1.1.14"
resolved "https://registry.yarnpkg.com/readable-stream/-/readable-stream-1.1.14.tgz#7cf4c54ef648e3813084c636dd2079e166c081d9"
dependencies:
core-util-is "~1.0.0"
inherits "~2.0.1"
isarray "0.0.1"
string_decoder "~0.10.x"
readable-stream@^2.2.2:
version "2.3.6"
resolved "https://registry.yarnpkg.com/readable-stream/-/readable-stream-2.3.6.tgz#b11c27d88b8ff1fbe070643cf94b0c79ae1b0aaf"
dependencies:
core-util-is "~1.0.0"
inherits "~2.0.3"
isarray "~1.0.0"
process-nextick-args "~2.0.0"
safe-buffer "~5.1.1"
string_decoder "~1.1.1"
util-deprecate "~1.0.1"
safe-buffer@5.1.1:
version "5.1.1"
resolved "https://registry.yarnpkg.com/safe-buffer/-/safe-buffer-5.1.1.tgz#893312af69b2123def71f57889001671eeb2c853"
safe-buffer@~5.1.0, safe-buffer@~5.1.1:
version "5.1.2"
resolved "https://registry.yarnpkg.com/safe-buffer/-/safe-buffer-5.1.2.tgz#991ec69d296e0313747d59bdfd2b745c35f8828d"
"safer-buffer@>= 2.1.2 < 3":
version "2.1.2"
resolved "https://registry.yarnpkg.com/safer-buffer/-/safer-buffer-2.1.2.tgz#44fa161b0187b9549dd84bb91802f9bd8385cd6a"
send@0.16.2:
version "0.16.2"
resolved "https://registry.yarnpkg.com/send/-/send-0.16.2.tgz#6ecca1e0f8c156d141597559848df64730a6bbc1"
@ -342,6 +515,20 @@ statuses@~1.4.0:
version "1.4.0"
resolved "https://registry.yarnpkg.com/statuses/-/statuses-1.4.0.tgz#bb73d446da2796106efcc1b601a253d6c46bd087"
streamsearch@0.1.2:
version "0.1.2"
resolved "https://registry.yarnpkg.com/streamsearch/-/streamsearch-0.1.2.tgz#808b9d0e56fc273d809ba57338e929919a1a9f1a"
string_decoder@~0.10.x:
version "0.10.31"
resolved "https://registry.yarnpkg.com/string_decoder/-/string_decoder-0.10.31.tgz#62e203bc41766c6c28c9fc84301dab1c5310fa94"
string_decoder@~1.1.1:
version "1.1.1"
resolved "https://registry.yarnpkg.com/string_decoder/-/string_decoder-1.1.1.tgz#9cf1611ba62685d7030ae9e4ba34149c3af03fc8"
dependencies:
safe-buffer "~5.1.0"
twig@~0.10.3:
version "0.10.3"
resolved "https://registry.yarnpkg.com/twig/-/twig-0.10.3.tgz#67604e08e1920ebf2faf80a901e256189c8a3c67"
@ -350,21 +537,37 @@ twig@~0.10.3:
minimatch "3.0.x"
walk "2.3.x"
type-is@~1.6.15, type-is@~1.6.16:
type-is@^1.6.4, type-is@~1.6.15, type-is@~1.6.16:
version "1.6.16"
resolved "https://registry.yarnpkg.com/type-is/-/type-is-1.6.16.tgz#f89ce341541c672b25ee7ae3c73dee3b2be50194"
dependencies:
media-typer "0.3.0"
mime-types "~2.1.18"
typedarray@^0.0.6:
version "0.0.6"
resolved "https://registry.yarnpkg.com/typedarray/-/typedarray-0.0.6.tgz#867ac74e3864187b1d3d47d996a78ec5c8830777"
underscore@~1.4.4:
version "1.4.4"
resolved "https://registry.yarnpkg.com/underscore/-/underscore-1.4.4.tgz#61a6a32010622afa07963bf325203cf12239d604"
unpipe@1.0.0, unpipe@~1.0.0:
version "1.0.0"
resolved "https://registry.yarnpkg.com/unpipe/-/unpipe-1.0.0.tgz#b2bf4ee8514aae6165b4817829d21b2ef49904ec"
util-deprecate@~1.0.1:
version "1.0.2"
resolved "https://registry.yarnpkg.com/util-deprecate/-/util-deprecate-1.0.2.tgz#450d4dc9fa70de732762fbd2d4a28981419a0ccf"
utils-merge@1.0.1:
version "1.0.1"
resolved "https://registry.yarnpkg.com/utils-merge/-/utils-merge-1.0.1.tgz#9f95710f50a267947b2ccc124741c1028427e713"
uuid4@^1.0.0:
version "1.0.0"
resolved "https://registry.yarnpkg.com/uuid4/-/uuid4-1.0.0.tgz#813aaeaf11ea2f68909c5ad57d894f83202d6720"
vary@~1.1.2:
version "1.1.2"
resolved "https://registry.yarnpkg.com/vary/-/vary-1.1.2.tgz#2299f02c6ded30d4a5961b0b9f74524a18f634fc"
@ -374,3 +577,7 @@ walk@2.3.x:
resolved "https://registry.yarnpkg.com/walk/-/walk-2.3.14.tgz#60ec8631cfd23276ae1e7363ce11d626452e1ef3"
dependencies:
foreachasync "^3.0.0"
xtend@^4.0.0:
version "4.0.1"
resolved "https://registry.yarnpkg.com/xtend/-/xtend-4.0.1.tgz#a5c6d532be656e23db820efb943a1f04998d63af"