temba 0.7.10 → 0.8.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
package/README.md ADDED
@@ -0,0 +1,316 @@
1
+ # Temba
2
+
3
+ <!-- ALL-CONTRIBUTORS-BADGE:START - Do not remove or modify this section -->
4
+
5
+ [![All Contributors](https://img.shields.io/badge/all_contributors-1-orange.svg?style=flat-square)](#contributors-)
6
+
7
+ <!-- ALL-CONTRIBUTORS-BADGE:END -->
8
+
9
+ **Get a simple MongoDB REST API with zero coding in less than 30 seconds (seriously).**
10
+
11
+ For developers who need a quick backend for small projects.
12
+
13
+ Powered by NodeJS, Express and MongoDB.
14
+
15
+ This project is inspired by the fantastic [json-server](https://github.com/typicode/json-server) project, but instead of a JSON file Temba uses a real database. The goal, however, is the same: Get you started with a REST API very quickly.
16
+
17
+ ## Table of contents
18
+
19
+ [Temba?](#temba-1)
20
+
21
+ [Getting Started](#getting-started)
22
+
23
+ [Usage](#usage)
24
+
25
+ ## Temba?
26
+
27
+ > _"Temba, at rest"_
28
+
29
+ A metaphor for the declining of a gift, from the [Star Trek - The Next Generation, episode "Darmok"](https://memory-alpha.fandom.com/wiki/Temba).
30
+
31
+ In the fictional Tamarian language the word _"Temba"_ means something like _"gift"_.
32
+
33
+ ## Getting Started
34
+
35
+ Prerequisites you need to have:
36
+
37
+ - Node, NPM
38
+ - Optional: A MongoDB database, either locally or in the cloud
39
+
40
+ > Wthout a database, Temba also works. However, then data is kept in memory and flushed every time the server restarts.
41
+
42
+ ### Use `npx`
43
+
44
+ Create your own Temba server with the following command and you are up and running!
45
+
46
+ ```bash
47
+ npx create-temba-server my-rest-api
48
+ ```
49
+
50
+ With this command you clone the [Temba-starter](https://github.com/bouwe77/temba-starter) repository and install all dependencies.
51
+
52
+ ### Manually adding to an existing app
53
+
54
+ Alternatively, add Temba to your app manually:
55
+
56
+ 1. `npm i temba`
57
+
58
+ 2. Example code to create a Temba server:
59
+
60
+ ```js
61
+ import temba from ("temba");
62
+ const server = temba.create();
63
+
64
+ const port = process.env.PORT || 3000;
65
+ server.listen(port, () => {
66
+ console.log(`Temba is running on port ${port}`);
67
+ });
68
+ ```
69
+
70
+ ### Configuration
71
+
72
+ By passing a config object to the `create` function you can customize Temba's behavior. Refer to the [config settings](#config-settings-overview) below for the various possibilities.
73
+
74
+ ## Usage
75
+
76
+ ### Introduction
77
+
78
+ Out of the box, Temba gives you a CRUD REST API to any resource name you can think of.
79
+
80
+ Whether you `GET` either `/people`, `/movies`, `/pokemons`, or whatever, it all returns a `200 OK` with a `[]` JSON response. As soon as you `POST` a new resource, followed by a `GET` of that resource, the new resource will be returned. You can also `DELETE`, or `PUT` resources by its ID.
81
+
82
+ For every resource (`movies` is just an example), Temba supports the following requests:
83
+
84
+ - `GET /movies` - Get all movies
85
+ - `GET /movies/:id` - Get a movie by its ID
86
+ - `POST /movies` - Create a new movie
87
+ - `PUT /movies/:id` - Update (fully replace) a movie by its ID
88
+ - `DELETE /movies` - Delete all movies
89
+ - `DELETE /movies/:id` - Delete a movie by its ID
90
+
91
+ ### Supported HTTP methods
92
+
93
+ The HTTP methods that are supported are `GET`, `POST`, `PUT` and `DELETE`. For any other HTTP method a `405 Method Not Allowed` response will be returned.
94
+
95
+ On the root URI (e.g. http://localhost:8080/) only a `GET` request is supported, which shows you a message indicating the API is working. All other HTTP methods on the root URI return a `405 Method Not Allowed` response.
96
+
97
+ ### MongoDB
98
+
99
+ When starting Temba, you can send your requests to it immediately. However, then the data resides in memory and is flushed as soon as the server restarts. To persist your data, provide the `connectionString` config setting for your MongoDB database:
100
+
101
+ ```js
102
+ const config = {
103
+ connectionString: 'mongodb://localhost:27017',
104
+ }
105
+ const server = temba.create(config)
106
+ ```
107
+
108
+ For every resource you use in your requests, a collection is created in the database. However, not until you actually store (create) a resource with a `POST`.
109
+
110
+ ### Allowing specific resources only
111
+
112
+ If you only want to allow specific resource names, configure them by providing a `resourceNames` key in the config object when creating the Temba server:
113
+
114
+ ```js
115
+ const config = { resourceNames: ['movies', 'actors'] }
116
+ const server = temba.create(config)
117
+ ```
118
+
119
+ Requests on these resources only give a `404 Not Found` if the ID does not exist. Requests on any other resource will always return a `404 Not Found`.
120
+
121
+ ### JSON
122
+
123
+ Temba only supports JSON. If you send a request with invalid formatted JSON, a `400 Bad Request` response is returned.
124
+
125
+ When sending JSON data (`POST` and `PUT` requests), adding a `Content-Type: application/json` header is required.
126
+
127
+ IDs are auto generated when creating resources. IDs in the JSON request body are ignored.
128
+
129
+ ### Static assets
130
+
131
+ If you want to host static assets next to the REST API, configure the `staticFolder`:
132
+
133
+ ```js
134
+ const config = { staticFolder: 'build' }
135
+ const server = temba.create(config)
136
+ ```
137
+
138
+ This way, you could create a REST API, and the web app consuming it, in one project.
139
+
140
+ However, to avoid possible conflicts between the API resources and the web app routes you might want to add an `apiPrefix` to the REST API:
141
+
142
+ ### REST URIs prefixes
143
+
144
+ With the `apiPrefix` config setting, all REST resources get an extra path segment in front of them. If the `apiPrefix` is `'api'`, then `/movies/12345` becomes `/api/movies/12345`:
145
+
146
+ ```js
147
+ const config = { apiPrefix: 'api' }
148
+ const server = temba.create(config)
149
+ ```
150
+
151
+ After configuring the `apiPrefix`, requests to the root URL will either return a `404 Not Found` or a `405 Method Not Allowed`, depending on the HTTP method.
152
+
153
+ If you have configured both an `apiPrefix` and a `staticFolder`, a `GET` on the root URL will return the `index.html` in the `staticFolder`, if there is one.
154
+
155
+ ### Request body validation or mutation
156
+
157
+ Temba does not validate request bodies.
158
+
159
+ This means you can store your resources in any format you like. So creating the following two (very different) _movies_ is perfectly fine:
160
+
161
+ ```
162
+ POST /movies
163
+ {
164
+ "title": "O Brother, Where Art Thou?",
165
+ "description": "In the deep south during the 1930s, three escaped convicts search for hidden treasure while a relentless lawman pursues them."
166
+ }
167
+
168
+ POST /movies
169
+ {
170
+ "foo": "bar",
171
+ "baz": "boo"
172
+ }
173
+ ```
174
+
175
+ You can even omit a request body when doing a `POST` or `PUT`. If you don't want that, and build proper validation, use the `requestBodyValidator` config setting:
176
+
177
+ If you want to do input validation before the `POST` or `PUT` request body is saved to the database, configure Temba as follows:
178
+
179
+ ```js
180
+ const config = {
181
+ requestBodyValidator: {
182
+ post: (resourceName, requestBody) {
183
+ // Validate, or even change the requestBody
184
+ },
185
+ put: (resourceName, requestBody) {
186
+ // Validate, or even change the requestBody
187
+ }
188
+ }
189
+ }
190
+
191
+ const server = temba.create(config)
192
+ ```
193
+
194
+ The `requestBodyValidator` is an object with a `post` and/or `put` field, which contains the callback function you want Temba to call before the JSON is saved to the database.
195
+
196
+ The callback function receives two arguments: The `resourceName`, which for example is `movies` if you request `POST /movies`. The second argument is the `requestBody`, which is the JSON object in the request body.
197
+
198
+ Your callback function can return the following things:
199
+
200
+ - `void`: Temba will just save the request body as-is. An example of this is when you have validated the request body and everything looks fine.
201
+ - `string`: If you return a string Temba will return a `400 Bad Request` with the string as error message.
202
+ - `object`: Return an object if you want to change the request body. Temba will save the returned object instead of the original request body.
203
+
204
+ Example:
205
+
206
+ ```js
207
+ const config = {
208
+ requestBodyValidator: {
209
+ post: (resourceName, requestBody) {
210
+ // Do not allow Pokemons to be created: 400 Bad Request
211
+ if (resourceName === 'pokemons') return 'You are not allowed to create new Pokemons'
212
+
213
+ // Add a genre to Star Trek films:
214
+ if (resourceName === 'movies' && requestBody.title.startsWith('Star Trek')) return {...requestBody, genre: 'Science Fiction'}
215
+
216
+ // If you end up here, void will be returned, so the request will just be saved.
217
+ },
218
+ }
219
+ }
220
+
221
+ const server = temba.create(config)
222
+ ```
223
+
224
+ ### Config settings overview
225
+
226
+ Configuring Temba is optional, it already works out of the box.
227
+
228
+ Here is an example of the config settings for Temba, and how you define them:
229
+
230
+ ```js
231
+ const config = {
232
+ resourceNames: ['movies', 'actors'],
233
+ connectionString: 'mongodb://localhost:27017',
234
+ staticFolder: 'build',
235
+ apiPrefix: 'api',
236
+ cacheControl: 'public, max-age=300',
237
+ delay: 500,
238
+ requestBodyValidator: {
239
+ post: (resourceName, requestBody) {
240
+ // Validate, or even change the requestBody
241
+ },
242
+ put: (resourceName, requestBody) {
243
+ // Validate, or even change the requestBody
244
+ }
245
+ }
246
+ }
247
+ const server = temba.create(config)
248
+ ```
249
+
250
+ None of the settings are required, and none of them have default values that actually do something. So only the settings you define are used.
251
+
252
+ These are all the possible settings:
253
+
254
+ | Config setting | Description |
255
+ | :--------------------- | :----------------------------------------------------------------------------------------- |
256
+ | `resourceNames` | See [Allowing specific resources only](#allowing-specific-resources-only) |
257
+ | `connectionString` | See [MongoDB](#mongodb) |
258
+ | `staticFolder` | See [Static assets](#static-assets) |
259
+ | `apiPrefix` | See [REST URIs prefixes](#rest-uris-prefixes) |
260
+ | `cacheControl` | The `Cache-control` response header value for each GET request. |
261
+ | `delay` | After processing the request, the delay in milliseconds before the request should be sent. |
262
+ | `requestBodyValidator` | Bla |
263
+
264
+ ## Not supported (yet?)
265
+
266
+ The following features would be very nice for Temba to support:
267
+
268
+ ### `PATCH` requests
269
+
270
+ Partial updates using `PATCH`, or other HTTP methods are not (yet?) supported.
271
+
272
+ ### Auth
273
+
274
+ Temba offers no ways for authentication or authorization (yet?), so if someone knows how to reach the API, they can read and mutate all your data, unless you restrict this in another way.
275
+
276
+ ### Nested parent-child resources
277
+
278
+ Also nested (parent-child) routes are not supported (yet?), so every URI has the /:resource/:id structure and there is no way to indicate any relation, apart from within the JSON itself perhaps.
279
+
280
+ ### Filtering and sorting
281
+
282
+ And there is no filtering, sorting, searching, etc. (yet?).
283
+
284
+ ## Under the hood
285
+
286
+ Temba is built with JavaScript, [Node](https://nodejs.org), [Express](https://expressjs.com/), [Jest](https://jestjs.io/), [Testing Library](https://testing-library.com/), [Supertest](https://www.npmjs.com/package/supertest), and [@rakered/mongo](https://www.npmjs.com/package/@rakered/mongo).
287
+
288
+ ## Which problem does Temba solve?
289
+
290
+ The problem with JSON file solutions like json-server is the limitations you have when hosting your app, because your data is stored in a file.
291
+
292
+ For example, hosting json-server on GitHub Pages means your API is essentially readonly, because, although mutations are supported, your data is not really persisted.
293
+
294
+ And hosting json-server on Heroku does give you persistence, but is not reliable because of its [ephemeral filesystem](https://devcenter.heroku.com/articles/dynos#ephemeral-filesystem).
295
+
296
+ These limitations are of course the whole idea behind json-server, it's for simple mocking and prototyping. But if you want more (persistence wise) and don't mind having a database, you might want to try Temba.
297
+
298
+ ## Contributors ✨
299
+
300
+ Thanks goes to these wonderful people ([emoji key](https://allcontributors.org/docs/en/emoji-key)):
301
+
302
+ <!-- ALL-CONTRIBUTORS-LIST:START - Do not remove or modify this section -->
303
+ <!-- prettier-ignore-start -->
304
+ <!-- markdownlint-disable -->
305
+ <table>
306
+ <tr>
307
+ <td align="center"><a href="https://bouwe.io"><img src="https://avatars.githubusercontent.com/u/4126793?v=4?s=100" width="100px;" alt=""/><br /><sub><b>Bouwe K. Westerdijk</b></sub></a><br /><a href="https://github.com/bouwe77/temba/commits?author=bouwe77" title="Code">💻</a> <a href="https://github.com/bouwe77/temba/issues?q=author%3Abouwe77" title="Bug reports">🐛</a> <a href="https://github.com/bouwe77/temba/commits?author=bouwe77" title="Documentation">📖</a> <a href="#ideas-bouwe77" title="Ideas, Planning, & Feedback">🤔</a> <a href="https://github.com/bouwe77/temba/commits?author=bouwe77" title="Tests">⚠️</a></td>
308
+ </tr>
309
+ </table>
310
+
311
+ <!-- markdownlint-restore -->
312
+ <!-- prettier-ignore-end -->
313
+
314
+ <!-- ALL-CONTRIBUTORS-LIST:END -->
315
+
316
+ This project follows the [all-contributors](https://github.com/all-contributors/all-contributors) specification. Contributions of any kind welcome!
@@ -23,7 +23,11 @@ var defaultConfig = {
23
23
  apiPrefix: '',
24
24
  connectionString: null,
25
25
  cacheControl: 'no-store',
26
- delay: 0
26
+ delay: 0,
27
+ requestBodyValidator: {
28
+ post: function post() {},
29
+ put: function put() {}
30
+ }
27
31
  };
28
32
 
29
33
  function initConfig(userConfig) {
@@ -60,5 +64,15 @@ function initConfig(userConfig) {
60
64
  config.delay = Number(userConfig.delay);
61
65
  }
62
66
 
67
+ if (userConfig.requestBodyValidator) {
68
+ if (userConfig.requestBodyValidator.post && typeof userConfig.requestBodyValidator.post === 'function') {
69
+ config.requestBodyValidator.post = userConfig.requestBodyValidator.post;
70
+ }
71
+
72
+ if (userConfig.requestBodyValidator.put && typeof userConfig.requestBodyValidator.put === 'function') {
73
+ config.requestBodyValidator.put = userConfig.requestBodyValidator.put;
74
+ }
75
+ }
76
+
63
77
  return config;
64
78
  }
@@ -4,10 +4,18 @@ Object.defineProperty(exports, "__esModule", {
4
4
  value: true
5
5
  });
6
6
  exports.new404NotFoundError = new404NotFoundError;
7
+ exports.new400BadRequestError = new400BadRequestError;
7
8
 
8
9
  function new404NotFoundError() {
9
10
  var message = arguments.length > 0 && arguments[0] !== undefined ? arguments[0] : 'Not Found';
10
11
  var error = new Error(message);
11
12
  error.status = 404;
12
13
  return error;
14
+ }
15
+
16
+ function new400BadRequestError() {
17
+ var message = arguments.length > 0 && arguments[0] !== undefined ? arguments[0] : 'Bad Request';
18
+ var error = new Error(message);
19
+ error.status = 400;
20
+ return error;
13
21
  }
@@ -26,16 +26,17 @@ function createGetRoutes(queries, cacheControl) {
26
26
  case 0:
27
27
  _context.prev = 0;
28
28
  _req$requestInfo = req.requestInfo, resourceName = _req$requestInfo.resourceName, id = _req$requestInfo.id;
29
+ res.set('Cache-control', cacheControl);
29
30
 
30
31
  if (!id) {
31
- _context.next = 14;
32
+ _context.next = 13;
32
33
  break;
33
34
  }
34
35
 
35
- _context.next = 5;
36
+ _context.next = 6;
36
37
  return queries.getById(resourceName, id);
37
38
 
38
- case 5:
39
+ case 6:
39
40
  item = _context.sent;
40
41
 
41
42
  if (item) {
@@ -44,37 +45,34 @@ function createGetRoutes(queries, cacheControl) {
44
45
  }
45
46
 
46
47
  res.status(404);
47
- res.set('Cache-control', cacheControl);
48
48
  return _context.abrupt("return", res.send());
49
49
 
50
50
  case 10:
51
51
  res.status(200);
52
- res.set('Cache-control', cacheControl);
53
52
  res.json(item);
54
53
  return _context.abrupt("return", res.send());
55
54
 
56
- case 14:
57
- _context.next = 16;
55
+ case 13:
56
+ _context.next = 15;
58
57
  return queries.getAll(resourceName);
59
58
 
60
- case 16:
59
+ case 15:
61
60
  items = _context.sent;
62
61
  res.status(200);
63
- res.set('Cache-control', cacheControl);
64
62
  res.json(items);
65
63
  return _context.abrupt("return", res.send());
66
64
 
67
- case 23:
68
- _context.prev = 23;
65
+ case 21:
66
+ _context.prev = 21;
69
67
  _context.t0 = _context["catch"](0);
70
68
  return _context.abrupt("return", next(_context.t0));
71
69
 
72
- case 26:
70
+ case 24:
73
71
  case "end":
74
72
  return _context.stop();
75
73
  }
76
74
  }
77
- }, _callee, null, [[0, 23]]);
75
+ }, _callee, null, [[0, 21]]);
78
76
  }));
79
77
  return _handleGetResource.apply(this, arguments);
80
78
  }
@@ -30,15 +30,16 @@ function createResourceRouter(queries, _ref) {
30
30
  var validateResources = _ref.validateResources,
31
31
  resourceNames = _ref.resourceNames,
32
32
  apiPrefix = _ref.apiPrefix,
33
- cacheControl = _ref.cacheControl;
33
+ cacheControl = _ref.cacheControl,
34
+ requestBodyValidator = _ref.requestBodyValidator;
34
35
 
35
36
  var _createGetRoutes = (0, _get.createGetRoutes)(queries, cacheControl),
36
37
  handleGetResource = _createGetRoutes.handleGetResource;
37
38
 
38
- var _createPostRoutes = (0, _post.createPostRoutes)(queries),
39
+ var _createPostRoutes = (0, _post.createPostRoutes)(queries, requestBodyValidator),
39
40
  handlePost = _createPostRoutes.handlePost;
40
41
 
41
- var _createPutRoutes = (0, _put.createPutRoutes)(queries),
42
+ var _createPutRoutes = (0, _put.createPutRoutes)(queries, requestBodyValidator),
42
43
  handlePut = _createPutRoutes.handlePut;
43
44
 
44
45
  var _createDeleteRoutes = (0, _delete.createDeleteRoutes)(queries),
@@ -13,24 +13,27 @@ var _asyncToGenerator2 = _interopRequireDefault(require("@babel/runtime/helpers/
13
13
 
14
14
  var _url = require("url");
15
15
 
16
- function createPostRoutes(queries) {
16
+ var _validator = require("./validator");
17
+
18
+ function createPostRoutes(queries, requestBodyValidator) {
17
19
  function handlePost(_x, _x2, _x3) {
18
20
  return _handlePost.apply(this, arguments);
19
21
  }
20
22
 
21
23
  function _handlePost() {
22
24
  _handlePost = (0, _asyncToGenerator2["default"])( /*#__PURE__*/_regenerator["default"].mark(function _callee(req, res, next) {
23
- var resourceName, newItem;
25
+ var resourceName, requestBody, newItem;
24
26
  return _regenerator["default"].wrap(function _callee$(_context) {
25
27
  while (1) {
26
28
  switch (_context.prev = _context.next) {
27
29
  case 0:
28
30
  _context.prev = 0;
29
31
  resourceName = req.requestInfo.resourceName;
30
- _context.next = 4;
31
- return queries.create(resourceName, req.body);
32
+ requestBody = (0, _validator.validateRequestBody)(requestBodyValidator.post, resourceName, req.body);
33
+ _context.next = 5;
34
+ return queries.create(resourceName, requestBody);
32
35
 
33
- case 4:
36
+ case 5:
34
37
  newItem = _context.sent;
35
38
  return _context.abrupt("return", res.set({
36
39
  Location: (0, _url.format)({
@@ -40,17 +43,17 @@ function createPostRoutes(queries) {
40
43
  })
41
44
  }).status(201).json(newItem).send());
42
45
 
43
- case 8:
44
- _context.prev = 8;
46
+ case 9:
47
+ _context.prev = 9;
45
48
  _context.t0 = _context["catch"](0);
46
49
  return _context.abrupt("return", next(_context.t0));
47
50
 
48
- case 11:
51
+ case 12:
49
52
  case "end":
50
53
  return _context.stop();
51
54
  }
52
55
  }
53
- }, _callee, null, [[0, 8]]);
56
+ }, _callee, null, [[0, 9]]);
54
57
  }));
55
58
  return _handlePost.apply(this, arguments);
56
59
  }
@@ -15,18 +15,20 @@ var _asyncToGenerator2 = _interopRequireDefault(require("@babel/runtime/helpers/
15
15
 
16
16
  var _errors = require("../errors");
17
17
 
18
+ var _validator = require("./validator");
19
+
18
20
  function ownKeys(object, enumerableOnly) { var keys = Object.keys(object); if (Object.getOwnPropertySymbols) { var symbols = Object.getOwnPropertySymbols(object); if (enumerableOnly) { symbols = symbols.filter(function (sym) { return Object.getOwnPropertyDescriptor(object, sym).enumerable; }); } keys.push.apply(keys, symbols); } return keys; }
19
21
 
20
22
  function _objectSpread(target) { for (var i = 1; i < arguments.length; i++) { var source = arguments[i] != null ? arguments[i] : {}; if (i % 2) { ownKeys(Object(source), true).forEach(function (key) { (0, _defineProperty2["default"])(target, key, source[key]); }); } else if (Object.getOwnPropertyDescriptors) { Object.defineProperties(target, Object.getOwnPropertyDescriptors(source)); } else { ownKeys(Object(source)).forEach(function (key) { Object.defineProperty(target, key, Object.getOwnPropertyDescriptor(source, key)); }); } } return target; }
21
23
 
22
- function createPutRoutes(queries) {
24
+ function createPutRoutes(queries, requestBodyValidator) {
23
25
  function handlePut(_x, _x2, _x3) {
24
26
  return _handlePut.apply(this, arguments);
25
27
  }
26
28
 
27
29
  function _handlePut() {
28
30
  _handlePut = (0, _asyncToGenerator2["default"])( /*#__PURE__*/_regenerator["default"].mark(function _callee(req, res, next) {
29
- var _req$requestInfo, resourceName, id, item, updatedItem;
31
+ var _req$requestInfo, resourceName, id, requestBody, item, updatedItem;
30
32
 
31
33
  return _regenerator["default"].wrap(function _callee$(_context) {
32
34
  while (1) {
@@ -34,49 +36,50 @@ function createPutRoutes(queries) {
34
36
  case 0:
35
37
  _context.prev = 0;
36
38
  _req$requestInfo = req.requestInfo, resourceName = _req$requestInfo.resourceName, id = _req$requestInfo.id;
39
+ requestBody = (0, _validator.validateRequestBody)(requestBodyValidator.put, resourceName, req.body);
37
40
  item = null;
38
41
 
39
42
  if (!id) {
40
- _context.next = 7;
43
+ _context.next = 8;
41
44
  break;
42
45
  }
43
46
 
44
- _context.next = 6;
47
+ _context.next = 7;
45
48
  return queries.getById(resourceName, id);
46
49
 
47
- case 6:
50
+ case 7:
48
51
  item = _context.sent;
49
52
 
50
- case 7:
53
+ case 8:
51
54
  if (item) {
52
- _context.next = 9;
55
+ _context.next = 10;
53
56
  break;
54
57
  }
55
58
 
56
59
  return _context.abrupt("return", next((0, _errors.new404NotFoundError)("ID '".concat(id, "' not found"))));
57
60
 
58
- case 9:
59
- item = _objectSpread(_objectSpread({}, req.body), {}, {
61
+ case 10:
62
+ item = _objectSpread(_objectSpread({}, requestBody), {}, {
60
63
  id: id
61
64
  });
62
- _context.next = 12;
65
+ _context.next = 13;
63
66
  return queries.update(resourceName, item);
64
67
 
65
- case 12:
68
+ case 13:
66
69
  updatedItem = _context.sent;
67
70
  return _context.abrupt("return", res.status(200).json(updatedItem).send());
68
71
 
69
- case 16:
70
- _context.prev = 16;
72
+ case 17:
73
+ _context.prev = 17;
71
74
  _context.t0 = _context["catch"](0);
72
75
  return _context.abrupt("return", next(_context.t0));
73
76
 
74
- case 19:
77
+ case 20:
75
78
  case "end":
76
79
  return _context.stop();
77
80
  }
78
81
  }
79
- }, _callee, null, [[0, 16]]);
82
+ }, _callee, null, [[0, 17]]);
80
83
  }));
81
84
  return _handlePut.apply(this, arguments);
82
85
  }
@@ -0,0 +1,21 @@
1
+ "use strict";
2
+
3
+ Object.defineProperty(exports, "__esModule", {
4
+ value: true
5
+ });
6
+ exports.validateRequestBody = validateRequestBody;
7
+
8
+ var _errors = require("../errors");
9
+
10
+ function validateRequestBody(validator, resourceName, requestBody) {
11
+ var validationResult = validator(resourceName, requestBody);
12
+ if (!validationResult) return requestBody;
13
+
14
+ if (typeof validationResult === 'string') {
15
+ throw (0, _errors.new400BadRequestError)(validationResult);
16
+ } // The requestBody was replaced by something else.
17
+
18
+
19
+ if (validationResult) requestBody = validationResult;
20
+ return requestBody;
21
+ }
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "temba",
3
- "version": "0.7.10",
3
+ "version": "0.8.0",
4
4
  "description": "Get a simple MongoDB REST API with zero coding in less than 30 seconds (seriously).",
5
5
  "main": "dist/server.js",
6
6
  "scripts": {
package/readme.md DELETED
@@ -1,214 +0,0 @@
1
- # Temba
2
-
3
- **Get a simple MongoDB REST API with zero coding in less than 30 seconds (seriously).**
4
-
5
- For developers who need a quick backend for small projects.
6
-
7
- Powered by NodeJS, Express and MongoDB.
8
-
9
- This project is inspired by the fantastic [json-server](https://github.com/typicode/json-server) project, but instead of a JSON file Temba uses a real database. The goal, however, is the same: Get you started with a REST API very quickly.
10
-
11
- ## Table of contents
12
-
13
- [Temba?](#temba-1)
14
-
15
- [Getting Started](#getting-started)
16
-
17
- [Usage](#usage)
18
-
19
- ## Temba?
20
-
21
- > _"Temba, at rest"_
22
-
23
- A metaphor for the declining of a gift, from the [Star Trek - The Next Generation, episode "Darmok"](https://memory-alpha.fandom.com/wiki/Temba).
24
-
25
- In the fictional Tamarian language the word _"Temba"_ means something like _"gift"_.
26
-
27
- ## Getting Started
28
-
29
- Prerequisites you need to have:
30
-
31
- - Node, NPM
32
- - Optional: A MongoDB database, either locally or in the cloud
33
-
34
- > Wthout a database, Temba also works. However, then data is kept in memory and flushed every time the server restarts.
35
-
36
- ### Use `npx`
37
-
38
- Create your own Temba server with the following command and you are up and running!
39
-
40
- ```bash
41
- npx create-temba-server my-rest-api
42
- ```
43
-
44
- ### Manually adding to an existing app
45
-
46
- If you don't want to (or can't) use the starter, add Temba to your app manually:
47
-
48
- 1. `npm i temba`
49
-
50
- 2. Example code to create a Temba server:
51
-
52
- ```js
53
- import temba from ("temba");
54
- const server = temba.create();
55
-
56
- const port = process.env.PORT || 3000;
57
- server.listen(port, () => {
58
- console.log(`Temba is running on port ${port}`);
59
- });
60
- ```
61
-
62
- ### Configuration
63
-
64
- By passing a config object to the `create` function you can customize Temba's behavior. Refer to the documentation below for the various possibilities.
65
-
66
- ## Usage
67
-
68
- ### Introduction
69
-
70
- Out of the box, Temba gives you a CRUD REST API to any resource name you can think of.
71
-
72
- Whether you `GET` either `/people`, `/movies`, `/pokemons`, or whatever, it all returns a `200 OK` with a `[]` JSON response. As soon as you `POST` a new resource, followed by a `GET` of that resource, the new resource will be returned. You can also `DELETE`, or `PUT` resources by its ID.
73
-
74
- For a every resource, for example `/movies`, Temba supports the following requests:
75
-
76
- - `GET /movies` - Get all movies
77
- - `GET /movies/:id` - Get a movie by its ID
78
- - `POST /movies` - Create a new movie
79
- - `PUT /movies/:id` - Update (fully replace) a movie by its ID
80
- - `DELETE /movies` - Delete all movies
81
- - `DELETE /movies/:id` - Delete a movie by its ID
82
-
83
- ### Supported HTTP methods
84
-
85
- Requests with an HTTP method that is not supported, so everything but `GET`, `POST`, `PUT` and `DELETE`, a `405 Method Not Allowed` response will be returned.
86
-
87
- On the root URI (e.g. http://localhost:8080/) only a `GET` request is supported, which shows you a message indicating the API is working. All other HTTP methods on the root URI return a `405 Method Not Allowed` response.
88
-
89
- ### MongoDB
90
-
91
- When starting Temba, you can send your requests to it immediately. However, then the data resides in memory and is flushed as soon as the server restarts. To persist your data, provide the `connectionString` config setting for your MongoDB database:
92
-
93
- ```js
94
- const config = {
95
- connectionString: 'mongodb://localhost:27017',
96
- }
97
- const server = temba.create(config)
98
- ```
99
-
100
- For every resource you use in your requests a collection is created in the database. However, not until you actually store (create) a resource with a `POST`.
101
-
102
- ### Allowing specific resources only
103
-
104
- If you only want to allow specific resource names, configure them by providing a `resourceNames` key in the config object when creating the Temba server:
105
-
106
- ```js
107
- const config = { resourceNames: ['movies', 'actors'] }
108
- const server = temba.create(config)
109
- ```
110
-
111
- Requests on these resources only give a `404 Not Found` if the ID does not exist. Requests on any other resource will always return a `404 Not Found`.
112
-
113
- ### JSON
114
-
115
- Temba only supports JSON. If you send a request with invalid formatted JSON, a `400 Bad Request` response is returned.
116
-
117
- When sending JSON data (`POST` and `PUT` requests), adding a `Content-Type: application/json` header is required.
118
-
119
- IDs are auto generated when creating resources. IDs in the JSON request body are ignored.
120
-
121
- ### Static assets
122
-
123
- If you want to host static assets next to the REST API, configure the `staticFolder`:
124
-
125
- ```js
126
- const config = { staticFolder: 'build' }
127
- const server = temba.create(config)
128
- ```
129
-
130
- This way, you could create a REST API, and the web app consuming it, in one project.
131
-
132
- However, to avoid possible conflicts between the API resources and the routes in your web app you might want to add an `apiPrefix` to the REST API:
133
-
134
- ### REST URIs prefixes
135
-
136
- With the `apiPrefix` config setting, all REST resources get an extra path segment in front of them. If the `apiPrefix` is `'api'`, then `/movies/12345` becomes `/api/movies/12345`:
137
-
138
- ```js
139
- const config = { apiPrefix: 'api' }
140
- const server = temba.create(config)
141
- ```
142
-
143
- After configuring the `apiPrefix`, requests to the root URL will either return a `404 Not Found` or a `405 Method Not Allowed`, depending on the HTTP method.
144
-
145
- If you have configured both an `apiPrefix` and a `staticFolder`, a `GET` on the root URL will return the `index.html` in the `staticFolder`, if there is one.
146
-
147
- ### Config settings overview
148
-
149
- Configuring Temba is optional, it already works out of the box. None of the settings are used until you configure them:
150
-
151
- ```js
152
- const config = {
153
- resourceNames: ['movies', 'actors'],
154
- connectionString: 'mongodb://localhost:27017',
155
- staticFolder: 'build',
156
- apiPrefix: 'api',
157
- cacheControl: 'public, max-age=300',
158
- delay: 500,
159
- }
160
- const server = temba.create(config)
161
- ```
162
-
163
- These are all the possible settings:
164
-
165
- | Config setting | Description |
166
- | :----------------- | :----------------------------------------------------------------------------------------- |
167
- | `resourceNames` | See [Allowing specific resources only](#allowing-specific-resources-only) |
168
- | `connectionString` | See [MongoDB](#mongodb) |
169
- | `staticFolder` | See [Static assets](#static-assets) |
170
- | `apiPrefix` | See [REST URIs prefixes](#rest-uris-prefixes) |
171
- | `cacheControl` | The `Cache-control` response header value for each GET request. |
172
- | `delay` | After processing the request, the delay in milliseconds before the request should be sent. |
173
-
174
- ## Not supported (yet?)
175
-
176
- Temba is still very basic. It does not have any model validation, so you can store your resources in any format you like.
177
-
178
- So creating the following two (very different) movies is perfectly fine:
179
-
180
- ```
181
- POST /movies
182
- {
183
- "title": "O Brother, Where Art Thou?",
184
- "description": "In the deep south during the 1930s, three escaped convicts search for hidden treasure while a relentless lawman pursues them."
185
- }
186
-
187
- POST /movies
188
- {
189
- "foo": "bar",
190
- "baz": "boo"
191
- }
192
- ```
193
-
194
- Partial updates using `PATCH`, or other HTTP methods are not (yet?) supported.
195
-
196
- Temba offers no ways for authentication or authorization (yet?), so if someone knows how to reach the API, they can read and mutate all your data, unless you restrict this in another way.
197
-
198
- Also nested (parent-child) routes are not supported (yet?), so every URI has the /:resource/:id structure and there is no way to indicate any relation, apart from within the JSON itself perhaps.
199
-
200
- And there is no filtering, sorting, searching, custom routes, etc. (yet?).
201
-
202
- ## Under the hood
203
-
204
- Temba is built with JavaScript, Node, Express, Jest, Testing Library, Supertest, and [@rakered/mongo](https://www.npmjs.com/package/@rakered/mongo).
205
-
206
- ## Which problem does Temba solve?
207
-
208
- The problem with JSON file solutions like json-server is the limitations you have when hosting your app, because your data is stored in a file.
209
-
210
- For example, hosting json-server on GitHub Pages means your API is essentially readonly, because, although mutations are supported, your data is not really persisted.
211
-
212
- And hosting json-server on Heroku does give you persistence, but is not reliable because of its [ephemeral filesystem](https://devcenter.heroku.com/articles/dynos#ephemeral-filesystem).
213
-
214
- These limitations are of course the whole idea behind json-server, it's for simple mocking and prototyping. But if you want more (persistence wise) and don't mind having a database, you might want to try Temba.