json-rest-api 0.1.1 → 1.0.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 CHANGED
@@ -1,165 +1,427 @@
1
- json-rest-api
2
- =============
3
- A lightweight REST API that receives and responds to JSON HTTP requests,
4
- supports all verbs. Expects JSON requests and responds in JSON via an added
5
- method to the http.ServerResponse object called `json` (see below).
6
-
7
- # Motivation
8
- Sometimes you want a simple REST API and don't need all the bells and whistles
9
- of express.js.
10
-
11
- # Installation
12
-
13
- npm install json-rest-api
14
-
15
- # Example
16
-
17
- ## server
18
-
19
- ```JavaScript
20
- 'use strict';
21
- var JsonRestApi = require('json-rest-api');
22
- var inspect = require('util').inspect;
23
- var debug = require('debug')('json-rest-api:example:server');
24
-
25
- var RestApi = new JsonRestApi({port: 8000}, function(err) {
26
- if (err) {
27
- debug('/ping error: '+inspect(err));
28
- return;
29
- }
30
- console.log('Listening on port 8000.');
31
-
32
- // add a "GET" route
33
- RestApi.addRoute('get', '/ping', function(req, res, json, qs) {
34
- var qsExp = {'alpha': 'one', 'beta': '2', 'gamma': 'true'};
35
- assert.deepEqual(qsExp, qs);
36
- res.json({success: true, pong: 'pong'});
37
- });
1
+ # JSON REST API
2
+
3
+ A modern, plugin-based JSON REST API library with JSON:API compliance, schema validation, and extensible storage backends.
4
+
5
+ ## Features
6
+
7
+ - **Plugin Architecture**: Extensible through plugins using `.use()` pattern
8
+ - **JSON:API Compliant**: Follows JSON:API specification for RESTful APIs
9
+ - **Schema Validation**: Built-in schema validation with extensible types and parameters
10
+ - **Multiple Storage Backends**: Memory and MySQL support out of the box
11
+ - **Automatic API Versioning**: Built-in version management with automatic routing
12
+ - **Positioning**: Manage record ordering with drag-and-drop support
13
+ - **Resource Versioning**: Track changes with version history
14
+ - **TypeScript-like Schema**: Define schemas with types, validation rules, and defaults
15
+ - **Hooks System**: Extensible through before/after hooks
16
+ - **Express Integration**: Easy integration with Express.js
17
+ - **Security**: Built-in security plugin with rate limiting, CORS, and authentication
18
+ - **Logging**: Structured logging with sensitive data redaction
19
+ - **OpenAPI**: Auto-generated API documentation
20
+
21
+ ## Installation
22
+
23
+ ```bash
24
+ npm install jsonrestapi
25
+ ```
38
26
 
39
- // add a "POST" route
40
- RestApi.addRoute('get', '/setName', function(req, res, json, qs) {
41
- var qsExp = {'alpha': 'one', 'beta': '2', 'gamma': 'true'};
27
+ ## Quick Start
42
28
 
43
- assert.deepEqual(qsExp, qs);
44
- res.json({success: true, pong: 'pong'});
45
- });
29
+ ```javascript
30
+ import { createApi, Schema } from 'jsonrestapi';
31
+ import express from 'express';
32
+
33
+ // Create an API with automatic versioning
34
+ const api = createApi({
35
+ name: 'myapp',
36
+ version: '1.0.0',
37
+ storage: 'memory',
38
+ http: { basePath: '/api' }
39
+ });
40
+
41
+ // Define a schema
42
+ const userSchema = new Schema({
43
+ id: { type: 'id' },
44
+ name: { type: 'string', required: true, min: 2, max: 100 },
45
+ email: { type: 'string', required: true, lowercase: true },
46
+ active: { type: 'boolean', default: true }
47
+ });
48
+
49
+ // Register the schema
50
+ api.addResource('users', userSchema);
51
+
52
+ // Create Express app and mount API
53
+ const app = express();
54
+ api.mount(app);
55
+
56
+ app.listen(3000);
57
+ ```
58
+
59
+ ## Core Concepts
60
+
61
+ ### Schema Definition
62
+
63
+ Schemas define the structure and validation rules for your data:
64
+
65
+ ```javascript
66
+ const schema = new Schema({
67
+ // Numeric types
68
+ id: { type: 'id' },
69
+ age: { type: 'number', min: 0, max: 150 },
70
+ price: { type: 'number', currency: true },
71
+
72
+ // String types
73
+ name: { type: 'string', required: true, min: 2, max: 50 },
74
+ email: { type: 'string', lowercase: true },
75
+ description: { type: 'string', max: 1000 },
76
+
77
+ // Boolean type
78
+ active: { type: 'boolean', default: true },
79
+
80
+ // Date types
81
+ createdAt: { type: 'timestamp', default: () => Date.now() },
82
+ birthDate: { type: 'date' },
83
+ lastLogin: { type: 'dateTime' },
84
+
85
+ // Complex types
86
+ tags: { type: 'array', default: [] },
87
+ metadata: { type: 'object' },
88
+
89
+ // Special types
90
+ data: { type: 'serialize' }, // Circular JSON support
91
+ file: { type: 'blob' }
46
92
  });
47
93
  ```
48
94
 
49
- ## client
95
+ ### Plugin System
96
+
97
+ Extend functionality through plugins:
98
+
99
+ ```javascript
100
+ const api = new Api();
101
+
102
+ api
103
+ .use(ValidationPlugin)
104
+ .use(MySQLPlugin, { connection: dbConfig })
105
+ .use(HTTPPlugin, { basePath: '/api' })
106
+ .use(PositioningPlugin)
107
+ .use(VersioningPlugin, { trackHistory: true });
108
+ ```
109
+
110
+ ### Custom Plugins
50
111
 
51
- ```JavaScript
52
- 'use strict';
53
- var request = require('request');
54
- var inspect = require('util').inspect;
55
- var debug = require('debug')('json-rest-api:example:client');
56
- var assert = require('assert');
112
+ Create your own plugins:
57
113
 
58
- var qs = {
59
- alpha: 'one',
60
- beta: 2,
61
- gamma: true
114
+ ```javascript
115
+ const MyPlugin = {
116
+ install(api, options) {
117
+ // Add new methods
118
+ api.myMethod = () => { /* ... */ };
119
+
120
+ // Register hooks
121
+ api.hook('beforeInsert', async (context) => {
122
+ // Modify context before insert
123
+ });
124
+
125
+ // Add custom types
126
+ api.registerType('myType', ({ value }) => {
127
+ // Custom type logic
128
+ return processedValue;
129
+ });
130
+ }
62
131
  };
63
132
 
64
- request({json:true, qs:qs, uri: 'http://localhost:8000/ping'}, function(err, res, json) {
65
- if (err) {
66
- debug('Error: '+inspect(err));
67
- return;
68
- }
133
+ api.use(MyPlugin, { /* options */ });
134
+ ```
135
+
136
+ ## API Methods
137
+
138
+ ### CRUD Operations
139
+
140
+ ```javascript
141
+ // Get single resource
142
+ const user = await api.get('123', { type: 'users' });
143
+
144
+ // Query resources
145
+ const users = await api.query({
146
+ filter: { active: true },
147
+ sort: '-createdAt,name',
148
+ page: { size: 20, number: 1 }
149
+ }, { type: 'users' });
150
+
151
+ // Insert resource
152
+ const newUser = await api.insert({
153
+ name: 'John Doe',
154
+ email: 'john@example.com'
155
+ }, { type: 'users' });
156
+
157
+ // Update resource
158
+ const updated = await api.update('123', {
159
+ name: 'Jane Doe'
160
+ }, { type: 'users' });
161
+
162
+ // Delete resource
163
+ await api.delete('123', { type: 'users' });
164
+ ```
165
+
166
+ ### HTTP Endpoints
167
+
168
+ When using HTTPPlugin, the following endpoints are available:
69
169
 
70
- assert.ok(json.success === true);
71
- assert.ok(json.pong === 'pong');
72
- console.log('Success!');
170
+ - `GET /api/{type}` - List resources
171
+ - `GET /api/{type}/{id}` - Get single resource
172
+ - `POST /api/{type}` - Create resource
173
+ - `PATCH /api/{type}/{id}` - Update resource
174
+ - `DELETE /api/{type}/{id}` - Delete resource
175
+
176
+ Query parameters:
177
+ - `filter[field]=value` - Filter by field
178
+ - `sort=-field1,field2` - Sort by fields
179
+ - `page[size]=10&page[number]=2` - Pagination
180
+ - `include=relation1,relation2` - Include relationships
181
+
182
+ ## Storage Backends
183
+
184
+ ### Memory Storage
185
+
186
+ ```javascript
187
+ api.use(MemoryPlugin, {
188
+ initialData: [
189
+ { id: 1, name: 'Item 1' },
190
+ { id: 2, name: 'Item 2' }
191
+ ]
192
+ });
193
+ ```
194
+
195
+ ### MySQL Storage
196
+
197
+ ```javascript
198
+ api.use(MySQLPlugin, {
199
+ connection: {
200
+ host: 'localhost',
201
+ user: 'root',
202
+ password: 'password',
203
+ database: 'myapp'
204
+ }
73
205
  });
74
206
 
75
- request({json:true, qs:qs, uri: 'http://localhost:8000/setName'}, function(err, res, json) {
76
- if (err) {
77
- debug('Error: '+inspect(err));
78
- return;
79
- }
207
+ // Sync schema with database
208
+ await api.syncSchema(userSchema, 'users');
209
+ ```
210
+
211
+ ## Advanced Features
80
212
 
81
- var qsExp = {'alpha': 'one', 'beta': '2', 'gamma': 'true'};
213
+ ### Positioning
82
214
 
83
- assert.ok(json.success === true);
84
- assert.ok(json.cmd === 'setName');
85
- assert.deepEqual(json.qs, qsExp);
86
- console.log('Success!');
215
+ Manage record order:
216
+
217
+ ```javascript
218
+ api.use(PositioningPlugin);
219
+
220
+ // Insert at specific position
221
+ await api.insert({
222
+ name: 'Item',
223
+ beforeId: '123' // Place before item with ID 123
224
+ }, {
225
+ type: 'items',
226
+ positioning: { enabled: true }
227
+ });
228
+
229
+ // Reposition existing item
230
+ await api.reposition('items', '456', null); // Move to end
231
+ ```
232
+
233
+ ### API Versioning
234
+
235
+ Create multiple versions of your API with automatic version negotiation:
236
+
237
+ ```javascript
238
+ // Version 1.0.0
239
+ const apiV1 = createApi({
240
+ name: 'users',
241
+ version: '1.0.0',
242
+ storage: 'memory'
243
+ });
244
+
245
+ apiV1.addResource('users', new Schema({
246
+ id: { type: 'id' },
247
+ name: { type: 'string', required: true },
248
+ email: { type: 'string', required: true }
249
+ }));
250
+
251
+ // Version 2.0.0 with new fields
252
+ const apiV2 = createApi({
253
+ name: 'users',
254
+ version: '2.0.0',
255
+ storage: 'memory'
87
256
  });
257
+
258
+ apiV2.addResource('users', new Schema({
259
+ id: { type: 'id' },
260
+ name: { type: 'string', required: true },
261
+ email: { type: 'string', required: true },
262
+ phone: { type: 'string' }, // New field
263
+ active: { type: 'boolean', default: true } // New field
264
+ }));
265
+
266
+ // Automatic version selection
267
+ const api = Api.get('users', 'latest'); // Gets v2.0.0
268
+ const apiV1Compat = Api.get('users', '1.0.0'); // Gets v1.0.0 or higher
269
+
270
+ // HTTP endpoints with automatic versioning
271
+ // GET /api/1.0.0/users
272
+ // GET /api/2.0.0/users
273
+ // GET /api/users?v=2.0.0
274
+ // GET /api/users (with API-Version: 2.0.0 header)
88
275
  ```
89
276
 
90
- # API
277
+ ### Resource Versioning
91
278
 
92
- ## RestApi([config], [cb])
93
- RestApi constructor. Creates the HTTP server objects and starts listening on
94
- the socket.
279
+ Track changes to individual resources:
95
280
 
96
- ### Params:
97
- * **Object** *[config]* Optional configuration object to configure the server.
98
- The configuration options are:
99
- * bindTo - Optional. The address to bind, default: INADDR_ANY.
100
- * port - Optional. The port to bind the server, default: 44401.
101
- * **Function** *[cb]* An optional callback. If there is a callback, the process
102
- will be begin listening on the socket for HTTP requests. Without a callback,
103
- you must call listen explicitly.
281
+ ```javascript
282
+ api.use(VersioningPlugin, {
283
+ trackHistory: true,
284
+ optimisticLocking: true
285
+ });
104
286
 
105
- ## listen([cb])
106
- Start listening on the socket for HTTP requests.
287
+ // Automatic version tracking
288
+ const user = await api.insert({ name: 'John' }, { type: 'users' });
289
+ // user.version = 1
107
290
 
108
- ### Params:
109
- * **Function** *[cb]* An optional callback called when the server is ready.
291
+ const updated = await api.update(user.id, { name: 'Jane' }, { type: 'users' });
292
+ // updated.version = 2
110
293
 
111
- ## stop([cb])
112
- Stops the server from acccepting new connections and listening on the port.
294
+ // Get version history
295
+ const history = await api.getVersionHistory('users', user.id);
113
296
 
114
- ### Params:
115
- * **Function** *[cb]* The callback function. Optional.
297
+ // Restore previous version
298
+ await api.restoreVersion('users', user.id, 1);
116
299
 
117
- ## addRoute(verb, path, cb)
118
- Add a route along with a function to run.
300
+ // Compare versions
301
+ const diff = await api.diffVersions('users', user.id, 1, 2);
302
+ ```
119
303
 
120
- ### Params:
121
- * **String** *verb* HTTP verb for route. e.g.: 'get', 'post'.
122
- * **String** *path* A valid URI path.
123
- * **Function** *cb* A function to run when the path executes. Which has the
124
- following form: fn(req, res, json, qs):
125
- * @param {Object} req - Node.js HTTP request object
126
- * @param {Object} res - Node.js HTTP response object
127
- * @param {Object} json - The JSON body of the request, if present.
128
- * @param {Object} qs - The query string of the request, if present.
304
+ ### Hooks
129
305
 
130
- ### Return:
131
- * **Boolean|Error** True, if no errors on the parameters, Error
132
- * **Error|Boolean** true on success and Error on failure.
306
+ Extend behavior with hooks:
133
307
 
134
- ## json([status code,] jsonObject)
135
- A method for the JsonServerResponse prototype to send JSON by passing an object.
308
+ ```javascript
309
+ // Validation hooks
310
+ api.hook('beforeValidate', async (context) => {
311
+ // Modify data before validation
312
+ });
136
313
 
137
- ### Params:
138
- * **Number|Object** *Optional.* First argument may be either the status code or
139
- the object to convert to JSON.
140
- * **Object** *Optional.* If the first object is the status code, then the second
141
- param must be the object to convert to JSON.
314
+ api.hook('afterValidate', async (context) => {
315
+ // Add custom validation
316
+ if (context.data.price < 0) {
317
+ context.errors.push({
318
+ field: 'price',
319
+ message: 'Price cannot be negative'
320
+ });
321
+ }
322
+ });
142
323
 
143
- # LICENSE
324
+ // CRUD hooks
325
+ api.hook('beforeInsert', async (context) => {
326
+ context.data.createdBy = context.options.userId;
327
+ });
144
328
 
145
- The MIT License (MIT)
329
+ api.hook('transformResult', async (context) => {
330
+ // Add computed fields
331
+ if (context.result) {
332
+ context.result.displayName = context.result.name.toUpperCase();
333
+ }
334
+ });
335
+ ```
146
336
 
147
- Copyright (c) 2013,2014 Edmond Meinfelder
337
+ ## Schema Extensions
148
338
 
149
- Permission is hereby granted, free of charge, to any person obtaining a copy of
150
- this software and associated documentation files (the "Software"), to deal in
151
- the Software without restriction, including without limitation the rights to
152
- use, copy, modify, merge, publish, distribute, sublicense, and/or sell copies of
153
- the Software, and to permit persons to whom the Software is furnished to do so,
154
- subject to the following conditions:
339
+ Add custom types and parameters:
155
340
 
156
- The above copyright notice and this permission notice shall be included in all
157
- copies or substantial portions of the Software.
341
+ ```javascript
342
+ // Custom type
343
+ schema.registerType('phone', ({ value }) => {
344
+ const cleaned = value.replace(/\D/g, '');
345
+ if (cleaned.length !== 10) {
346
+ throw new Error('Invalid phone number');
347
+ }
348
+ return cleaned;
349
+ });
350
+
351
+ // Custom parameter
352
+ schema.registerParam('unique', async ({ value, fieldName, object }) => {
353
+ const exists = await checkUniqueness(fieldName, value);
354
+ if (exists) {
355
+ throw new Error(`${fieldName} must be unique`);
356
+ }
357
+ });
358
+
359
+ // Use in schema
360
+ const schema = new Schema({
361
+ phone: { type: 'phone', required: true },
362
+ username: { type: 'string', unique: true }
363
+ });
364
+ ```
365
+
366
+ ## Cross-API Communication
367
+
368
+ APIs can access each other with automatic version compatibility:
369
+
370
+ ```javascript
371
+ // Orders API can access Users API
372
+ const ordersApi = createApi({
373
+ name: 'orders',
374
+ version: '1.0.0'
375
+ });
376
+
377
+ ordersApi.hook('afterInsert', async (context) => {
378
+ // Automatically gets a compatible users API
379
+ const usersApi = ordersApi.apis.users;
380
+
381
+ if (usersApi && context.data.userId) {
382
+ const user = await usersApi.get(context.data.userId, { type: 'users' });
383
+ console.log('Order created for:', user);
384
+ }
385
+ });
386
+ ```
387
+
388
+ ## Security Best Practices
389
+
390
+ Enable comprehensive security features:
391
+
392
+ ```javascript
393
+ import { SecurityPlugin, LoggingPlugin } from 'jsonrestapi';
394
+
395
+ api
396
+ .use(SecurityPlugin, {
397
+ rateLimit: { max: 100, windowMs: 15 * 60 * 1000 },
398
+ authentication: { type: 'bearer', required: true },
399
+ cors: { origin: 'https://app.example.com' }
400
+ })
401
+ .use(LoggingPlugin, {
402
+ level: 'info',
403
+ auditLog: true
404
+ });
405
+ ```
406
+
407
+ ## Error Handling
408
+
409
+ Errors follow JSON:API format:
410
+
411
+ ```javascript
412
+ try {
413
+ await api.insert({ name: '' }, { type: 'users' });
414
+ } catch (error) {
415
+ console.log(error.errors);
416
+ // [{
417
+ // status: '422',
418
+ // title: 'Validation Error',
419
+ // detail: 'Field required',
420
+ // source: { pointer: '/data/attributes/name' }
421
+ // }]
422
+ }
423
+ ```
158
424
 
159
- THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
160
- IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, FITNESS
161
- FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR
162
- COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER
163
- IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN
164
- CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE.
425
+ ## License
165
426
 
427
+ MIT
package/TODO.todo ADDED
@@ -0,0 +1,113 @@
1
+ TODO
2
+
3
+ 10. Utility Features
4
+
5
+ - Missing from original:
6
+ - artificial delay for testing <-- ADD!!!
7
+ - Request decoration pattern <-- I DON'T KNOW WHAT THIS IS, BUT IF IT'S BEST PRACTICE/INDUSTRU STANDARD, TELL ME ABOUT IT
8
+ - Store inheritance/extension <--- THIS, I NEVER WANTED, NEVER USED IN 10 YEARS IN PRODUCTION
9
+ - Automatic timestamp fields <-- WOULD BE NICE
10
+ - Soft delete support <-- WOULD BE NICE BUT I DON'T THINK THE ORIGINAL HAD IT. I WILL IMPLEMENT IT IN THE FUTURE
11
+
12
+
13
+ 5. Error Handling
14
+
15
+ - Missing structured error classes:
16
+ - BadRequest, NotFound, Conflict classes
17
+ - Error code standardization
18
+ - Multi-field validation errors
19
+ - Error context preservation
20
+
21
+ 6. Hook System Gaps
22
+
23
+ - Missing hooks from original:
24
+ - beforeId, afterId
25
+ - beforeRetrieve, afterRetrieve
26
+ - beforeInsert, afterInsert (full lifecycle)
27
+ - queryFieldsAndJoins
28
+ - updateConditionsAndArgs
29
+ - deleteConditionsAndArgs
30
+
31
+ 7. Store Templates
32
+
33
+ - No equivalent to storeTemplate.js:
34
+ - Automatic REST endpoint generation
35
+ - Built-in authentication handling
36
+ - Session management
37
+ - Request/response transformation
38
+
39
+ 8. Memory Store Features
40
+
41
+ - Current memory plugin lacks:
42
+ - Query operators ($like, $in, $gt, etc.)
43
+ - Sorting by multiple fields
44
+ - Limit/offset with total count
45
+ - Deep object filtering
46
+
47
+
48
+
49
+ 11. JSON:API Compliance Gaps
50
+
51
+ - Current HTTP plugin missing:
52
+ - Proper relationship handling
53
+ - Compound documents
54
+ - Sparse fieldsets
55
+ - Include parameter processing
56
+ - Meta information structure
57
+
58
+ 12. Performance Features
59
+
60
+ - Not implemented:
61
+ - Connection pooling configuration
62
+ - Query result caching
63
+ - Batch operations
64
+ - Transaction support across operations
65
+
66
+
67
+
68
+ **************************************************************************
69
+ DONE
70
+ **************************************************************************
71
+
72
+ 2. MySQL Schema Synchronization
73
+
74
+ - Current MySQL plugin is basic, missing:
75
+ - Automatic table creation/modification
76
+ - Column type mapping from schema
77
+ - Index creation for searchable fields
78
+ - Foreign key constraint management
79
+ - Schema migration tracking
80
+
81
+ 9. Schema Features
82
+
83
+ - SimplSchema integration missing:
84
+ - onlyObjectValues for partial updates
85
+ - Conditional defaults based on other fields
86
+ - Array validation with item schemas
87
+ - Custom error messages per validator
88
+
89
+ 1. Positioning System
90
+
91
+ - Current has basic positioning plugin but lacks:
92
+ - Filtering support during repositioning
93
+ - Gap-free positioning algorithm
94
+ - Complex position calculations with conditions
95
+ - Batch repositioning operations
96
+
97
+ 3. Advanced Querying
98
+
99
+ - Missing query capabilities:
100
+ - Complex field joins (queryFieldsAndJoins hook)
101
+ - Nested object queries
102
+ - Array field queries
103
+ - Custom SQL conditions
104
+ - Aggregation support
105
+
106
+ 4. Versioning Management
107
+
108
+ - Current versioning is rudimentary, missing:
109
+ - Version range negotiation
110
+ - Store registry with semver matching
111
+ - Version-specific schema transformations
112
+ - Backward compatibility helpers
113
+