json-rest-api 0.1.1 → 1.0.1
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/.claude/settings.local.json +16 -0
- package/LICENSE +674 -20
- package/README.md +728 -120
- package/RELATIONSHIPS.md +1013 -0
- package/TODO_rest-api-plugin-extras.js +142 -0
- package/connectors_ideas/express-plugin.js +120 -0
- package/connectors_ideas/fastify.js +219 -0
- package/connectors_ideas/http-plugin.js +138 -0
- package/connectors_ideas/koa-plugin.js +113 -0
- package/docs/fileUploads.md +722 -0
- package/docs/mini-guides/file-uploads-complete.md +159 -0
- package/examples/.gitkeep +0 -0
- package/examples/express-example.js +262 -0
- package/examples/express-plugin.js +366 -0
- package/examples/file-upload-example.js +205 -0
- package/examples/filename-strategies.js +140 -0
- package/examples/knex-basic.js +66 -0
- package/index.js +28 -202
- package/lib/payload-validators.js +685 -0
- package/lib/rest-api-errors.js +58 -0
- package/package.json +37 -20
- package/payloadValdators.js +686 -0
- package/plugins/core/connectors/express-plugin.js +449 -0
- package/plugins/core/connectors/http-plugin.js +450 -0
- package/plugins/core/file-handling-plugin.js +338 -0
- package/plugins/core/lib/busboy-detector.js +167 -0
- package/plugins/core/lib/formidable-detector.js +130 -0
- package/plugins/core/rest-api-knex-plugin.js +200 -0
- package/plugins/core/rest-api-plugin.js +1419 -0
- package/plugins/storage/local-storage.js +218 -0
- package/plugins/storage/s3-storage.js +125 -0
- package/test-examples/package-lock.json +906 -0
- package/test-examples/package.json +15 -0
- package/test-examples/test-basic-api.js +11 -0
- package/test-examples/test-books-resource.js +116 -0
- package/test-examples/test-complete-file-upload.js +140 -0
- package/test-examples/test-debug.js +28 -0
- package/test-examples/test-express-plugin.js +177 -0
- package/test-examples/test-express-simple.js +28 -0
- package/test-examples/test-express-with-import.js +71 -0
- package/test-examples/test-file-upload.js +132 -0
- package/test-examples/test-http-plugin.js +159 -0
- package/test-examples/test-programmatic-api.js +188 -0
- package/test-examples/test-rest-api-plugin.js +13 -0
- package/.jshintrc +0 -48
- package/.npmignore +0 -15
- package/examples/client.js +0 -18
- package/examples/client2.js +0 -27
- package/examples/server.js +0 -23
- package/test.js +0 -101
|
@@ -0,0 +1,66 @@
|
|
|
1
|
+
import { Api } from 'hooked-api';
|
|
2
|
+
import { RestApiPlugin, RestApiKnexPlugin, ExpressPlugin } from 'json-rest-api';
|
|
3
|
+
import knex from 'knex';
|
|
4
|
+
import express from 'express';
|
|
5
|
+
|
|
6
|
+
// Create a Knex instance (using SQLite for simplicity)
|
|
7
|
+
const db = knex({
|
|
8
|
+
client: 'sqlite3',
|
|
9
|
+
connection: {
|
|
10
|
+
filename: ':memory:'
|
|
11
|
+
},
|
|
12
|
+
useNullAsDefault: true
|
|
13
|
+
});
|
|
14
|
+
|
|
15
|
+
// Create the API
|
|
16
|
+
const api = new Api({
|
|
17
|
+
name: 'library-api',
|
|
18
|
+
version: '1.0.0'
|
|
19
|
+
});
|
|
20
|
+
|
|
21
|
+
// Use plugins
|
|
22
|
+
api.use(RestApiPlugin);
|
|
23
|
+
api.use(RestApiKnexPlugin, {
|
|
24
|
+
knex: { knex: db } // Pass the knex instance
|
|
25
|
+
});
|
|
26
|
+
api.use(ExpressPlugin);
|
|
27
|
+
|
|
28
|
+
// Create the books table
|
|
29
|
+
await db.schema.createTable('books', (table) => {
|
|
30
|
+
table.increments('id');
|
|
31
|
+
table.string('title').notNullable();
|
|
32
|
+
table.string('author').notNullable();
|
|
33
|
+
table.integer('year');
|
|
34
|
+
table.string('isbn');
|
|
35
|
+
});
|
|
36
|
+
|
|
37
|
+
// Define the books resource
|
|
38
|
+
api.addResource('books', {
|
|
39
|
+
schema: {
|
|
40
|
+
title: { type: 'string', required: true },
|
|
41
|
+
author: { type: 'string', required: true },
|
|
42
|
+
year: { type: 'number' },
|
|
43
|
+
isbn: { type: 'string' }
|
|
44
|
+
}
|
|
45
|
+
});
|
|
46
|
+
|
|
47
|
+
// Insert some sample data
|
|
48
|
+
await db('books').insert([
|
|
49
|
+
{ title: 'The Great Gatsby', author: 'F. Scott Fitzgerald', year: 1925, isbn: '978-0-7432-7356-5' },
|
|
50
|
+
{ title: '1984', author: 'George Orwell', year: 1949, isbn: '978-0-452-28423-4' },
|
|
51
|
+
{ title: 'To Kill a Mockingbird', author: 'Harper Lee', year: 1960, isbn: '978-0-06-112008-4' }
|
|
52
|
+
]);
|
|
53
|
+
|
|
54
|
+
// Create Express app
|
|
55
|
+
const app = express();
|
|
56
|
+
api.express.mount(app);
|
|
57
|
+
|
|
58
|
+
// Start the server
|
|
59
|
+
app.listen(3000, () => {
|
|
60
|
+
console.log('Library API with Knex running at http://localhost:3000/api');
|
|
61
|
+
console.log('');
|
|
62
|
+
console.log('Try these commands:');
|
|
63
|
+
console.log(' curl http://localhost:3000/api/books');
|
|
64
|
+
console.log(' curl http://localhost:3000/api/books/1');
|
|
65
|
+
console.log(' curl -X POST http://localhost:3000/api/books -H "Content-Type: application/json" -d \'{"data":{"type":"books","attributes":{"title":"New Book","author":"New Author","year":2024}}}\'');
|
|
66
|
+
});
|
package/index.js
CHANGED
|
@@ -1,210 +1,36 @@
|
|
|
1
1
|
/**
|
|
2
|
-
*
|
|
3
|
-
*
|
|
4
|
-
*
|
|
2
|
+
* JSON REST API - REST API plugin for hooked-api with JSON:API compliance
|
|
3
|
+
*
|
|
4
|
+
* This package provides:
|
|
5
|
+
* - REST API plugin that adds query, get, post, put, patch, delete methods to scopes
|
|
6
|
+
* - Connector plugins for various HTTP frameworks (Express, etc.)
|
|
7
|
+
* - JSON:API compliant request/response handling
|
|
8
|
+
* - Comprehensive error handling and validation
|
|
5
9
|
*/
|
|
6
|
-
'use strict';
|
|
7
|
-
var http = require('http');
|
|
8
|
-
var debug = require('debug')('json-rest-api');
|
|
9
|
-
var url = require('url');
|
|
10
|
-
var util = require('util');
|
|
11
|
-
var inspect = util.inspect;
|
|
12
|
-
var is = require('is2');
|
|
13
|
-
var asyncerr = require('async-err');
|
|
14
10
|
|
|
15
|
-
//
|
|
16
|
-
|
|
11
|
+
// Core plugins
|
|
12
|
+
export { RestApiPlugin } from './plugins/core/rest-api-plugin.js';
|
|
13
|
+
export { FileHandlingPlugin } from './plugins/core/file-handling-plugin.js';
|
|
17
14
|
|
|
18
|
-
|
|
15
|
+
// Database plugins
|
|
16
|
+
export { RestApiKnexPlugin } from './plugins/core/rest-api-knex-plugin.js';
|
|
19
17
|
|
|
20
|
-
|
|
21
|
-
|
|
22
|
-
|
|
23
|
-
|
|
24
|
-
|
|
25
|
-
* port and the address to bind to. If no port is specified the default is 44401
|
|
26
|
-
* and the default address is INADDR_ANY.
|
|
27
|
-
* @param {Function} [cb] An optional callback. If there is a callback, the
|
|
28
|
-
* process will be begin listening on the socket for HTTP requests.
|
|
29
|
-
* @constructor
|
|
30
|
-
*/
|
|
31
|
-
function RestApi(config, cb) {
|
|
32
|
-
var self = this;
|
|
33
|
-
self.routes = {};
|
|
34
|
-
|
|
35
|
-
self.bindTo = (config && config.bindTo) ? config.bindTo : undefined;
|
|
36
|
-
self.port = (config && config.port) ? config.port : DEFAULT_PORT;
|
|
37
|
-
|
|
38
|
-
// create the HTTP server object and on every request, try to match the
|
|
39
|
-
// request with a known route. If there is no match, return a 404 error.
|
|
40
|
-
self.HttpServer = http.createServer(function(req, res) {
|
|
41
|
-
var uriPath;
|
|
42
|
-
var queryStr;
|
|
43
|
-
var urlParts = url.parse(req.url, true);
|
|
44
|
-
if (is.obj(urlParts)) {
|
|
45
|
-
uriPath = urlParts.pathname || undefined;
|
|
46
|
-
queryStr = urlParts.query || undefined;
|
|
47
|
-
debug('queryStr: '+inspect(queryStr));
|
|
48
|
-
} else {
|
|
49
|
-
// no match was found, return a 404 error.
|
|
50
|
-
res.writeHead(400, {'Content-Type': 'application/json; charset=utf-8'});
|
|
51
|
-
res.end('{status:400, message:"Bad URI path."}', 'utf8');
|
|
52
|
-
return;
|
|
53
|
-
}
|
|
54
|
-
|
|
55
|
-
// try to match the request & method with a handler
|
|
56
|
-
for (var path in self.routes[req.method]) {
|
|
57
|
-
if (path === uriPath) {
|
|
58
|
-
self.routes[req.method][path](req, res, queryStr);
|
|
59
|
-
return;
|
|
60
|
-
}
|
|
61
|
-
}
|
|
62
|
-
|
|
63
|
-
// no match was found, return a 404 error.
|
|
64
|
-
res.writeHead(404, {'Content-Type': 'application/json; charset=utf-8'});
|
|
65
|
-
res.end('{status:404, message:"Content not found."}', 'utf8');
|
|
66
|
-
});
|
|
67
|
-
|
|
68
|
-
if (cb) self.listen(cb);
|
|
69
|
-
}
|
|
70
|
-
|
|
71
|
-
/**
|
|
72
|
-
* Start listening on the socket for HTTP requests.
|
|
73
|
-
* @param {Function} [cb] An optional callback called when the server is ready.
|
|
74
|
-
*/
|
|
75
|
-
RestApi.prototype.listen = function(cb) {
|
|
76
|
-
var self = this;
|
|
77
|
-
|
|
78
|
-
self.HttpServer.listen(self.port, self.bindTo, function(err) {
|
|
79
|
-
if (err) {
|
|
80
|
-
var errStr = 'RestApi error: '+inspect(err);
|
|
81
|
-
debug(errStr);
|
|
82
|
-
if (cb)
|
|
83
|
-
return cb(new Error(errStr));
|
|
84
|
-
else
|
|
85
|
-
return;
|
|
86
|
-
}
|
|
87
|
-
debug('REST API listening on port: '+self.port);
|
|
88
|
-
if (cb) cb();
|
|
89
|
-
});
|
|
90
|
-
};
|
|
91
|
-
|
|
92
|
-
/**
|
|
93
|
-
* Stops the server from acccepting new connections and listening on the port.
|
|
94
|
-
* @param {Function} [cb] The callback function. Optional.
|
|
95
|
-
*/
|
|
96
|
-
RestApi.prototype.stop = function(cb) {
|
|
97
|
-
var self = this;
|
|
98
|
-
|
|
99
|
-
if (!is.obj(self.HttpServer))
|
|
100
|
-
return asyncerr(new Error('There server is not running.'), cb);
|
|
101
|
-
|
|
102
|
-
self.HttpServer.close(function(err) {
|
|
103
|
-
if (err && cb) return cb(err);
|
|
104
|
-
if (cb) return cb();
|
|
105
|
-
});
|
|
106
|
-
};
|
|
107
|
-
|
|
108
|
-
/**
|
|
109
|
-
* Add a route along with a function to run.
|
|
110
|
-
* @param {String} verb HTTP verb for route.
|
|
111
|
-
* @param {String} path A valid URI path.
|
|
112
|
-
* @param {Function} func A function to run when the path executes.
|
|
113
|
-
* @return {Boolean|Error} True, if no errors on the parameters, Error
|
|
114
|
-
* otherwise.
|
|
115
|
-
* @return {Error|Boolean} true on success and Error on failure.
|
|
116
|
-
*/
|
|
117
|
-
RestApi.prototype.addRoute = function(verb, path, func) {
|
|
118
|
-
var self = this;
|
|
119
|
-
|
|
120
|
-
if (!is.nonEmptyStr(verb))
|
|
121
|
-
return new Error('Bad verb: '+inspect(verb));
|
|
122
|
-
|
|
123
|
-
if (!is.nonEmptyStr(path))
|
|
124
|
-
return new Error('RestApi.addRoute bad path: '+inspect(path));
|
|
125
|
-
|
|
126
|
-
if (!is.func(func))
|
|
127
|
-
return new Error('RestApi.prototype.addRoute bad func: '+inspect(func));
|
|
128
|
-
|
|
129
|
-
var httpVerb = verb.toUpperCase();
|
|
130
|
-
debug('Adding: '+httpVerb+' '+path);
|
|
131
|
-
if (!self.routes[httpVerb]) self.routes[httpVerb] = {};
|
|
132
|
-
|
|
133
|
-
// Create a handling function for the route that gathers the
|
|
134
|
-
// the HTTP request body and passes that on.
|
|
135
|
-
self.routes[httpVerb][path] = function(req, res, queryStr) {
|
|
136
|
-
var body = ''; // buffer for body.
|
|
137
|
-
|
|
138
|
-
// collect the body in buf;
|
|
139
|
-
req.on('data', function (data) { body += data.toString(); });
|
|
140
|
-
|
|
141
|
-
// we have the body & header, now process.
|
|
142
|
-
req.on('end', function () {
|
|
143
|
-
req.body = body;
|
|
144
|
-
res.setHeader('Content-Type', 'application/json');
|
|
145
|
-
res.statusCode = 200;
|
|
146
|
-
|
|
147
|
-
// there may not be a body
|
|
148
|
-
if (body.length === 0)
|
|
149
|
-
return func(req, res, {}, queryStr);
|
|
150
|
-
|
|
151
|
-
// there is a body, make sure it is valid, parsable JSON
|
|
152
|
-
var jsonBody;
|
|
153
|
-
try {
|
|
154
|
-
jsonBody = JSON.parse(body);
|
|
155
|
-
} catch (err) {
|
|
156
|
-
debug('Error parsing JSON body:',body);
|
|
157
|
-
res.statusCode = 400;
|
|
158
|
-
res.end('{"success": false, "msg": "Bad request."}', 'utf8');
|
|
159
|
-
return;
|
|
160
|
-
}
|
|
161
|
-
func(req, res, jsonBody, queryStr);
|
|
162
|
-
});
|
|
163
|
-
};
|
|
164
|
-
return true;
|
|
165
|
-
};
|
|
166
|
-
|
|
167
|
-
/**
|
|
168
|
-
* A method for the JsonServerResponse prototype to send json by passing an
|
|
169
|
-
* object. Usage: res.json([status|body], [body])
|
|
170
|
-
* @param {Number|Object} Optional. First argument may be either the status
|
|
171
|
-
* code or the object to stringify into JSON.
|
|
172
|
-
* @param {Object} Optional. If the first object is the status code, then the
|
|
173
|
-
* second optional argument, if present, is an object to serialize into JSON.
|
|
174
|
-
* @return {Boolean} true on success, false otherwise.
|
|
175
|
-
*/
|
|
176
|
-
http.ServerResponse.prototype.json = function(obj) {
|
|
177
|
-
|
|
178
|
-
if (arguments.length === 2) {
|
|
179
|
-
if (typeof arguments[1] === 'number') {
|
|
180
|
-
this.statusCode = arguments[1];
|
|
181
|
-
} else {
|
|
182
|
-
this.statusCode = obj;
|
|
183
|
-
obj = arguments[1];
|
|
184
|
-
}
|
|
185
|
-
}
|
|
186
|
-
|
|
187
|
-
if (!is.obj(obj)) {
|
|
188
|
-
debug('http.ServerResponse.json bad object param: '+inspect(obj));
|
|
189
|
-
return false;
|
|
190
|
-
}
|
|
18
|
+
// Connector plugins
|
|
19
|
+
export { ExpressPlugin } from './plugins/core/connectors/express-plugin.js';
|
|
20
|
+
export { HttpPlugin } from './plugins/core/connectors/http-plugin.js';
|
|
21
|
+
// Future: export { FastifyPlugin } from './plugins/core/connectors/fastify-plugin.js';
|
|
22
|
+
// Future: export { KoaPlugin } from './plugins/core/connectors/koa-plugin.js';
|
|
191
23
|
|
|
192
|
-
|
|
193
|
-
|
|
194
|
-
|
|
195
|
-
return false;
|
|
196
|
-
}
|
|
24
|
+
// Storage plugins for file handling
|
|
25
|
+
export { LocalStorage } from './plugins/storage/local-storage.js';
|
|
26
|
+
export { S3Storage } from './plugins/storage/s3-storage.js';
|
|
197
27
|
|
|
198
|
-
|
|
199
|
-
|
|
200
|
-
|
|
201
|
-
|
|
202
|
-
|
|
203
|
-
|
|
204
|
-
return false;
|
|
205
|
-
}
|
|
28
|
+
// Error classes for consumers who need them
|
|
29
|
+
export {
|
|
30
|
+
RestApiValidationError,
|
|
31
|
+
RestApiResourceError,
|
|
32
|
+
RestApiPayloadError
|
|
33
|
+
} from './lib/rest-api-errors.js';
|
|
206
34
|
|
|
207
|
-
|
|
208
|
-
|
|
209
|
-
return true;
|
|
210
|
-
};
|
|
35
|
+
// Re-export everything for backward compatibility
|
|
36
|
+
export * from './lib/rest-api-errors.js';
|