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.
Files changed (50) hide show
  1. package/.claude/settings.local.json +16 -0
  2. package/LICENSE +674 -20
  3. package/README.md +728 -120
  4. package/RELATIONSHIPS.md +1013 -0
  5. package/TODO_rest-api-plugin-extras.js +142 -0
  6. package/connectors_ideas/express-plugin.js +120 -0
  7. package/connectors_ideas/fastify.js +219 -0
  8. package/connectors_ideas/http-plugin.js +138 -0
  9. package/connectors_ideas/koa-plugin.js +113 -0
  10. package/docs/fileUploads.md +722 -0
  11. package/docs/mini-guides/file-uploads-complete.md +159 -0
  12. package/examples/.gitkeep +0 -0
  13. package/examples/express-example.js +262 -0
  14. package/examples/express-plugin.js +366 -0
  15. package/examples/file-upload-example.js +205 -0
  16. package/examples/filename-strategies.js +140 -0
  17. package/examples/knex-basic.js +66 -0
  18. package/index.js +28 -202
  19. package/lib/payload-validators.js +685 -0
  20. package/lib/rest-api-errors.js +58 -0
  21. package/package.json +37 -20
  22. package/payloadValdators.js +686 -0
  23. package/plugins/core/connectors/express-plugin.js +449 -0
  24. package/plugins/core/connectors/http-plugin.js +450 -0
  25. package/plugins/core/file-handling-plugin.js +338 -0
  26. package/plugins/core/lib/busboy-detector.js +167 -0
  27. package/plugins/core/lib/formidable-detector.js +130 -0
  28. package/plugins/core/rest-api-knex-plugin.js +200 -0
  29. package/plugins/core/rest-api-plugin.js +1419 -0
  30. package/plugins/storage/local-storage.js +218 -0
  31. package/plugins/storage/s3-storage.js +125 -0
  32. package/test-examples/package-lock.json +906 -0
  33. package/test-examples/package.json +15 -0
  34. package/test-examples/test-basic-api.js +11 -0
  35. package/test-examples/test-books-resource.js +116 -0
  36. package/test-examples/test-complete-file-upload.js +140 -0
  37. package/test-examples/test-debug.js +28 -0
  38. package/test-examples/test-express-plugin.js +177 -0
  39. package/test-examples/test-express-simple.js +28 -0
  40. package/test-examples/test-express-with-import.js +71 -0
  41. package/test-examples/test-file-upload.js +132 -0
  42. package/test-examples/test-http-plugin.js +159 -0
  43. package/test-examples/test-programmatic-api.js +188 -0
  44. package/test-examples/test-rest-api-plugin.js +13 -0
  45. package/.jshintrc +0 -48
  46. package/.npmignore +0 -15
  47. package/examples/client.js +0 -18
  48. package/examples/client2.js +0 -27
  49. package/examples/server.js +0 -23
  50. 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
- * @fileOverview
3
- * Implements a simple REST API in an object. Requests and responses are in
4
- * JSON. Or are supposed to be.
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
- // set a default port to listen for requests
16
- var DEFAULT_PORT = process.env.PORT ? process.env.PORT : 44401;
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
- module.exports = RestApi;
15
+ // Database plugins
16
+ export { RestApiKnexPlugin } from './plugins/core/rest-api-knex-plugin.js';
19
17
 
20
- /**
21
- * RestApi constructor. Creates the HTTP server objects and starts listening on
22
- * the socket.
23
- * @param {Object} [config] An optional configuration object to configure the
24
- * RestApi. Possible properties are: port and bindTo to specify the listening
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
- if (!is.positiveInt(this.statusCode)) {
193
- debug('http.ServerResponse.json bad status param: '+
194
- inspect(this.statusCode));
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
- var str;
199
- try {
200
- str = JSON.stringify(obj);
201
- } catch (err) {
202
- debug('Error parsing JSON body: '+inspect(obj));
203
- debug('Error err: '+inspect(err));
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
- this.writeHead(this.statusCode, {'Content-Type': 'application/json'});
208
- this.end(str, 'utf8');
209
- return true;
210
- };
35
+ // Re-export everything for backward compatibility
36
+ export * from './lib/rest-api-errors.js';