-
-
-
-
-
-
-
-
-
-
- Congrats! You're successfully running JSON Server.
-
-
-
-
-
-
Routes
-
- Here are the resources that JSON Server has loaded:
-
-
-
-
-
-
- You can view database current state at any time:
-
-
-
-
- You can use any HTTP verbs (GET, POST, PUT, PATCH and DELETE) and access your resources from anywhere
- using CORS or JSONP.
-
-
-
Documentation
-
- View
- README
- on GitHub.
-
-
-
Issues
-
Please go
- here.
-
-
-
-
-
- To replace this page, create an index.html file in ./public, JSON Server will load it.
-
-
-
-
-
-
-
diff --git a/src/server/public/stylesheets/style.css b/src/server/public/stylesheets/style.css
deleted file mode 100644
index 098c6a4c2..000000000
--- a/src/server/public/stylesheets/style.css
+++ /dev/null
@@ -1,16 +0,0 @@
-a {
- color: #1882BC !important;
-}
-
-img {
- padding-top: 50px;
- padding-bottom: 20px;
-}
-
-li {
- list-style-type: square;
-}
-
-h4 {
- padding-top: 20px;
-}
\ No newline at end of file
diff --git a/src/server/rewriter.js b/src/server/rewriter.js
deleted file mode 100644
index d7f3c7471..000000000
--- a/src/server/rewriter.js
+++ /dev/null
@@ -1,37 +0,0 @@
-const express = require('express')
-const url = require('url')
-const _ = require('lodash')
-function updateQueryString (target, sourceUrl) {
- return ~sourceUrl.indexOf('?') ? _.assign(target, url.parse(sourceUrl, true).query) : {}
-}
-module.exports = (routes) => {
- const router = express.Router()
-
- router.get('/__rules', (req, res) => {
- res.json(routes)
- })
-
- Object.keys(routes).forEach((route) => {
- if (route.indexOf(':') !== -1) {
- router.all(route, (req, res, next) => {
- // Rewrite target url using params
- let target = routes[route]
- for (let param in req.params) {
- target = target.replace(':' + param, req.params[param])
- }
- req.url = target
- req.query = updateQueryString(req.query, req.url)
- next()
- })
- } else {
- router.all(route + '*', (req, res, next) => {
- // Rewrite url by replacing prefix
- req.url = req.url.replace(route, routes[route])
- req.query = updateQueryString(req.query, req.url)
- next()
- })
- }
- })
-
- return router
-}
diff --git a/src/server/router/index.js b/src/server/router/index.js
deleted file mode 100644
index 7a7ad6eaf..000000000
--- a/src/server/router/index.js
+++ /dev/null
@@ -1,90 +0,0 @@
-const express = require('express')
-const methodOverride = require('method-override')
-const _ = require('lodash')
-const _db = require('underscore-db')
-const low = require('lowdb')
-const fileAsync = require('lowdb/lib/file-async')
-const bodyParser = require('../body-parser')
-const validateData = require('./validate-data')
-const plural = require('./plural')
-const nested = require('./nested')
-const singular = require('./singular')
-const mixins = require('../mixins')
-
-module.exports = (source) => {
- // Create router
- const router = express.Router()
-
- // Add middlewares
- router.use(methodOverride())
- router.use(bodyParser)
-
- // Create database
- let db
- if (_.isObject(source)) {
- db = low()
- db.setState(source)
- } else {
- db = low(source, { storage: fileAsync })
- }
-
- validateData(db.getState())
-
- // Add underscore-db methods to db
- db._.mixin(_db)
-
- // Add specific mixins
- db._.mixin(mixins)
-
- // Expose database
- router.db = db
-
- // Expose render
- router.render = (req, res) => {
- res.jsonp(res.locals.data)
- }
-
- // GET /db
- router.get('/db', (req, res) => {
- res.jsonp(db.getState())
- })
-
- // Handle /:parent/:parentId/:resource
- router.use(nested())
-
- // Create routes
- db.forEach((value, key) => {
- if (_.isPlainObject(value)) {
- router.use(`/${key}`, singular(db, key))
- return
- }
-
- if (_.isArray(value)) {
- router.use(`/${key}`, plural(db, key))
- return
- }
-
- const msg =
- `Type of "${key}" (${typeof value}) ` +
- (_.isObject(source) ? '' : `in ${source}`) + ' is not supported. ' +
- 'Use objects or arrays of objects.'
-
- throw new Error(msg)
- }).value()
-
- router.use((req, res) => {
- if (!res.locals.data) {
- res.status(404)
- res.locals.data = {}
- }
-
- router.render(req, res)
- })
-
- router.use((err, req, res, next) => {
- console.error(err.stack)
- res.status(500).send(err.stack)
- })
-
- return router
-}
diff --git a/src/server/router/nested.js b/src/server/router/nested.js
deleted file mode 100644
index 61a1a5926..000000000
--- a/src/server/router/nested.js
+++ /dev/null
@@ -1,26 +0,0 @@
-const express = require('express')
-const pluralize = require('pluralize')
-
-module.exports = () => {
- const router = express.Router()
-
- // Rewrite URL (/:resource/:id/:nested -> /:nested) and request query
- function get (req, res, next) {
- const prop = pluralize.singular(req.params.resource)
- req.query[`${prop}Id`] = req.params.id
- req.url = `/${req.params.nested}`
- next()
- }
-
- // Rewrite URL (/:resource/:id/:nested -> /:nested) and request body
- function post (req, res, next) {
- const prop = pluralize.singular(req.params.resource)
- req.body[`${prop}Id`] = req.params.id
- req.url = `/${req.params.nested}`
- next()
- }
-
- return router
- .get('/:resource/:id/:nested', get)
- .post('/:resource/:id/:nested', post)
-}
diff --git a/src/server/router/plural.js b/src/server/router/plural.js
deleted file mode 100644
index 0c8b1173b..000000000
--- a/src/server/router/plural.js
+++ /dev/null
@@ -1,303 +0,0 @@
-const url = require('url')
-const express = require('express')
-const _ = require('lodash')
-const pluralize = require('pluralize')
-const utils = require('../utils')
-
-module.exports = (db, name) => {
- // Create router
- const router = express.Router()
-
- // Embed function used in GET /name and GET /name/id
- function embed (resource, e) {
- e && [].concat(e)
- .forEach((externalResource) => {
- if (db.get(externalResource).value) {
- const query = {}
- const singularResource = pluralize.singular(name)
- query[`${singularResource}Id`] = resource.id
- resource[externalResource] = db.get(externalResource).filter(query).value()
- }
- })
- }
-
- // Expand function used in GET /name and GET /name/id
- function expand (resource, e) {
- e && [].concat(e)
- .forEach((innerResource) => {
- const plural = pluralize(innerResource)
- if (db.get(plural).value()) {
- const prop = `${innerResource}Id`
- resource[innerResource] = db.get(plural).getById(resource[prop]).value()
- }
- })
- }
-
- function getFullURL (req) {
- const root = url.format({
- protocol: req.protocol,
- host: req.get('host')
- })
-
- return `${root}${req.originalUrl}`
- }
-
- // GET /name
- // GET /name?q=
- // GET /name?attr=&attr=
- // GET /name?_end=&
- // GET /name?_start=&_end=&
- // GET /name?_embed=&_expand=
- function list (req, res, next) {
- // Resource chain
- let chain = db.get(name)
-
- // Remove q, _start, _end, ... from req.query to avoid filtering using those
- // parameters
- let q = req.query.q
- let _start = req.query._start
- let _end = req.query._end
- let _page = req.query._page
- let _sort = req.query._sort
- let _order = req.query._order
- let _limit = req.query._limit
- let _embed = req.query._embed
- let _expand = req.query._expand
- delete req.query.q
- delete req.query._start
- delete req.query._end
- delete req.query._sort
- delete req.query._order
- delete req.query._limit
- delete req.query._embed
- delete req.query._expand
-
- // Automatically delete query parameters that can't be found
- // in the database
- Object.keys(req.query).forEach((query) => {
- const arr = db.get(name).value()
- for (let i in arr) {
- if (
- _.has(arr[i], query) ||
- query === 'callback' ||
- query === '_' ||
- /_lte$/.test(query) ||
- /_gte$/.test(query) ||
- /_ne$/.test(query) ||
- /_like$/.test(query)
- ) return
- }
- delete req.query[query]
- })
-
- if (q) {
- // Full-text search
- q = q.toLowerCase()
-
- chain = chain.filter((obj) => {
- for (let key in obj) {
- const value = obj[key]
- if (db._.deepQuery(value, q)) {
- return true
- }
- }
- })
- }
-
- Object.keys(req.query).forEach((key) => {
- // Don't take into account JSONP query parameters
- // jQuery adds a '_' query parameter too
- if (key !== 'callback' && key !== '_') {
- // Always use an array, in case req.query is an array
- const arr = [].concat(req.query[key])
-
- chain = chain.filter((element) => {
- return arr
- .map(function (value) {
- const isDifferent = /_ne$/.test(key)
- const isRange = /_lte$/.test(key) || /_gte$/.test(key)
- const isLike = /_like$/.test(key)
- const path = key.replace(/(_lte|_gte|_ne|_like)$/, '')
- const elementValue = _.get(element, path)
-
- if (elementValue === undefined) {
- return
- }
-
- if (isRange) {
- const isLowerThan = /_gte$/.test(key)
-
- return isLowerThan
- ? value <= elementValue
- : value >= elementValue
- } else if (isDifferent) {
- return value !== elementValue.toString()
- } else if (isLike) {
- return new RegExp(value, 'i').test(elementValue.toString())
- } else {
- return value === elementValue.toString()
- }
- })
- .reduce((a, b) => a || b)
- })
- }
- })
-
- // Sort
- if (_sort) {
- _order = _order || 'ASC'
-
- chain = chain.sortBy(function (element) {
- return _.get(element, _sort)
- })
-
- if (_order === 'DESC') {
- chain = chain.reverse()
- }
- }
-
- // Slice result
- if (_end || _limit || _page) {
- res.setHeader('X-Total-Count', chain.size())
- res.setHeader('Access-Control-Expose-Headers', 'X-Total-Count' + (_page ? ', Link' : ''))
- }
-
- if (_page) {
- _page = parseInt(_page, 10)
- _page = _page >= 1 ? _page : 1
- _limit = parseInt(_limit, 10) || 10
- const page = utils.getPage(chain.value(), _page, _limit)
- const links = {}
- const fullURL = getFullURL(req)
-
- if (page.first) {
- links.first = fullURL.replace('page=' + page.current, 'page=' + page.first)
- }
-
- if (page.prev) {
- links.prev = fullURL.replace('page=' + page.current, 'page=' + page.prev)
- }
-
- if (page.next) {
- links.next = fullURL.replace('page=' + page.current, 'page=' + page.next)
- }
-
- if (page.last) {
- links.last = fullURL.replace('page=' + page.current, 'page=' + page.last)
- }
-
- res.links(links)
- chain = _.chain(page.items)
- } else if (_end) {
- _start = parseInt(_start, 10) || 0
- _end = parseInt(_end, 10)
- chain = chain.slice(_start, _end)
- } else if (_limit) {
- _start = parseInt(_start, 10) || 0
- _limit = parseInt(_limit, 10)
- chain = chain.slice(_start, _start + _limit)
- }
-
- // embed and expand
- chain = chain
- .cloneDeep()
- .forEach(function (element) {
- embed(element, _embed)
- expand(element, _expand)
- })
-
- res.locals.data = chain.value()
- next()
- }
-
- // GET /name/:id
- // GET /name/:id?_embed=&_expand
- function show (req, res, next) {
- const _embed = req.query._embed
- const _expand = req.query._expand
- const resource = db.get(name)
- .getById(req.params.id)
- .value()
-
- if (resource) {
- // Clone resource to avoid making changes to the underlying object
- const clone = _.cloneDeep(resource)
-
- // Embed other resources based on resource id
- // /posts/1?_embed=comments
- embed(clone, _embed)
-
- // Expand inner resources based on id
- // /posts/1?_expand=user
- expand(clone, _expand)
-
- res.locals.data = clone
- }
-
- next()
- }
-
- // POST /name
- function create (req, res, next) {
- const resource = db.get(name)
- .insert(req.body)
- .value()
-
- res.status(201)
- res.locals.data = resource
- next()
- }
-
- // PUT /name/:id
- // PATCH /name/:id
- function update (req, res, next) {
- const id = req.params.id
- let chain = db.get(name)
-
- chain = req.method === 'PATCH'
- ? chain.updateById(id, req.body)
- : chain.replaceById(id, req.body)
-
- const resource = chain.value()
-
- if (resource) {
- res.locals.data = resource
- }
-
- next()
- }
-
- // DELETE /name/:id
- function destroy (req, res, next) {
- const resource = db.get(name)
- .removeById(req.params.id)
- .value()
-
- // Remove dependents documents
- const removable = db._.getRemovable(db.getState())
-
- removable.forEach((item) => {
- db.get(item.name)
- .removeById(item.id)
- .value()
- })
-
- if (resource) {
- res.locals.data = {}
- }
-
- next()
- }
-
- router.route('/')
- .get(list)
- .post(create)
-
- router.route('/:id')
- .get(show)
- .put(update)
- .patch(update)
- .delete(destroy)
-
- return router
-}
diff --git a/src/server/router/singular.js b/src/server/router/singular.js
deleted file mode 100644
index f33bbda09..000000000
--- a/src/server/router/singular.js
+++ /dev/null
@@ -1,39 +0,0 @@
-const express = require('express')
-
-module.exports = (db, name) => {
- const router = express.Router()
-
- function show (req, res, next) {
- res.locals.data = db.get(name).value()
- next()
- }
-
- function create (req, res, next) {
- db.set(name, req.body).value()
- res.locals.data = db.get(name).value()
- res.status(201)
- next()
- }
-
- function update (req, res, next) {
- if (req.method === 'PUT') {
- db.set(name, req.body)
- .value()
- } else {
- db.get(name)
- .assign(req.body)
- .value()
- }
-
- res.locals.data = db.get(name).value()
- next()
- }
-
- router.route('/')
- .get(show)
- .post(create)
- .put(update)
- .patch(update)
-
- return router
-}
diff --git a/src/server/router/validate-data.js b/src/server/router/validate-data.js
deleted file mode 100644
index 5bb5dbc1e..000000000
--- a/src/server/router/validate-data.js
+++ /dev/null
@@ -1,26 +0,0 @@
-const _ = require('lodash')
-
-function validateKey (key) {
- if (key.indexOf('/') !== -1) {
- const msg = [
- `Oops, found / character in database property '${key}'.`,
- '',
- '/ aren\'t supported, if you want to tweak default routes, see',
- 'https://github.com/typicode/json-server/tree/next#add-custom-routes'
- ].join('\n')
- throw new Error(msg)
- }
-}
-
-module.exports = (obj) => {
- if (_.isPlainObject(obj)) {
- Object
- .keys(obj)
- .forEach(validateKey)
- } else {
- throw new Error(
- `Data must be an object. Found ${typeof obj}.` +
- 'See https://github.com/typicode/json-server for example.'
- )
- }
-}
diff --git a/src/server/utils.js b/src/server/utils.js
deleted file mode 100644
index f7a5c9a50..000000000
--- a/src/server/utils.js
+++ /dev/null
@@ -1,30 +0,0 @@
-module.exports = {
- getPage
-}
-
-function getPage (array, page, perPage) {
- var obj = {}
- var start = (page - 1) * perPage
- var end = page * perPage
-
- obj.items = array.slice(start, end)
- if (obj.items.length === 0) {
- return obj
- }
-
- if (page > 1) {
- obj.prev = page - 1
- }
-
- if (end < array.length) {
- obj.next = page + 1
- }
-
- if (obj.items.length !== array.length) {
- obj.current = page
- obj.first = 1
- obj.last = Math.ceil(array.length / perPage)
- }
-
- return obj
-}
diff --git a/src/service.test.ts b/src/service.test.ts
new file mode 100644
index 000000000..818a2457b
--- /dev/null
+++ b/src/service.test.ts
@@ -0,0 +1,392 @@
+import assert from 'node:assert/strict'
+import test from 'node:test'
+
+import { Low, Memory } from 'lowdb'
+
+import { Data, Item, PaginatedItems, Service } from './service.js'
+
+const defaultData = { posts: [], comments: [], object: {} }
+const adapter = new Memory