json-rest-api 0.1.0 → 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.
@@ -0,0 +1,195 @@
1
+ import express from 'express';
2
+ import {
3
+ createApi,
4
+ Api,
5
+ Schema,
6
+ MySQLPlugin,
7
+ HTTPPlugin,
8
+ ValidationPlugin,
9
+ PositioningPlugin,
10
+ VersioningPlugin
11
+ } from './index.js';
12
+
13
+ // Example 1: Simple API with memory storage
14
+ const simpleApi = createApi({
15
+ storage: 'memory',
16
+ http: {
17
+ basePath: '/api/v1',
18
+ typeOptions: {
19
+ users: {
20
+ searchFields: ['name', 'email']
21
+ }
22
+ }
23
+ }
24
+ });
25
+
26
+ // Define a schema
27
+ const userSchema = new Schema({
28
+ id: { type: 'id' },
29
+ name: { type: 'string', required: true, min: 2, max: 100 },
30
+ email: { type: 'string', required: true, lowercase: true },
31
+ age: { type: 'number', min: 0, max: 150 },
32
+ active: { type: 'boolean', default: true },
33
+ createdAt: { type: 'timestamp', default: () => Date.now() }
34
+ });
35
+
36
+ // Register the schema
37
+ simpleApi.addResource('users', userSchema);
38
+
39
+ // Example 2: Advanced API with MySQL and all features
40
+ const advancedApi = new Api({ idProperty: 'id' });
41
+
42
+ // Configure plugins
43
+ advancedApi
44
+ .use(ValidationPlugin)
45
+ .use(MySQLPlugin, {
46
+ connections: [{
47
+ name: 'main',
48
+ config: {
49
+ host: 'localhost',
50
+ user: 'root',
51
+ password: 'password',
52
+ database: 'myapp'
53
+ }
54
+ }]
55
+ })
56
+ .use(PositioningPlugin, {
57
+ positionField: 'position',
58
+ beforeIdField: 'beforeId'
59
+ })
60
+ .use(VersioningPlugin, {
61
+ apiVersion: '2.0.0',
62
+ trackHistory: true,
63
+ optimisticLocking: true
64
+ })
65
+ .use(HTTPPlugin, {
66
+ basePath: '/api/v2'
67
+ });
68
+
69
+ // Define schemas
70
+ const productSchema = new Schema({
71
+ id: { type: 'id' },
72
+ name: { type: 'string', required: true, min: 1, max: 200 },
73
+ description: { type: 'string', max: 1000 },
74
+ price: { type: 'number', required: true, min: 0, currency: true },
75
+ stock: { type: 'number', default: 0, min: 0 },
76
+ category: { type: 'string', required: true },
77
+ tags: { type: 'array', default: [] },
78
+ metadata: { type: 'object' },
79
+ active: { type: 'boolean', default: true },
80
+ position: { type: 'number', default: 0 },
81
+ version: { type: 'number', default: 1 },
82
+ createdAt: { type: 'timestamp', default: () => Date.now() },
83
+ updatedAt: { type: 'timestamp' }
84
+ });
85
+
86
+ // Create search schema from main schema
87
+ const productSearchSchema = advancedApi.createSearchSchema(productSchema, [
88
+ 'name', 'category', 'active'
89
+ ]);
90
+
91
+ // Register schemas
92
+ advancedApi.addResource('products', productSchema);
93
+
94
+ // Custom validation hook
95
+ advancedApi.hook('afterValidate', async (context) => {
96
+ if (context.method === 'insert' || context.method === 'update') {
97
+ const { data } = context;
98
+
99
+ // Custom business rule: expensive products must have description
100
+ if (data.price > 1000 && !data.description) {
101
+ context.errors.push({
102
+ field: 'description',
103
+ message: 'Products over $1000 must have a description',
104
+ code: 'EXPENSIVE_PRODUCT_NEEDS_DESCRIPTION'
105
+ });
106
+ }
107
+ }
108
+ });
109
+
110
+ // Transform hook to add computed fields
111
+ advancedApi.hook('transformResult', async (context) => {
112
+ if (context.result && context.options.type === 'products') {
113
+ const product = context.result;
114
+
115
+ // Add computed field
116
+ product.displayPrice = `$${(product.price || 0).toFixed(2)}`;
117
+ product.inStock = product.stock > 0;
118
+ }
119
+ });
120
+
121
+ // Example 3: Using the API programmatically
122
+ async function exampleUsage() {
123
+ // Insert a product
124
+ const newProduct = await advancedApi.insert({
125
+ name: 'Premium Widget',
126
+ description: 'A high-quality widget for professionals',
127
+ price: 1299.99,
128
+ stock: 50,
129
+ category: 'widgets',
130
+ tags: ['premium', 'professional'],
131
+ beforeId: null // Will be placed at the end
132
+ }, {
133
+ type: 'products',
134
+ table: 'products',
135
+ positioning: { enabled: true }
136
+ });
137
+
138
+ console.log('Created product:', newProduct);
139
+
140
+ // Query products
141
+ const products = await advancedApi.query({
142
+ filter: { category: 'widgets', active: true },
143
+ sort: '-price,name',
144
+ page: { size: 10, number: 1 }
145
+ }, {
146
+ type: 'products',
147
+ table: 'products'
148
+ });
149
+
150
+ console.log('Found products:', products);
151
+
152
+ // Update with optimistic locking
153
+ const updated = await advancedApi.update(newProduct.data.id, {
154
+ stock: 45,
155
+ version: newProduct.data.attributes.version // For optimistic locking
156
+ }, {
157
+ type: 'products',
158
+ table: 'products'
159
+ });
160
+
161
+ console.log('Updated product:', updated);
162
+
163
+ // Get version history
164
+ const history = await advancedApi.getVersionHistory('products', newProduct.data.id);
165
+ console.log('Version history:', history);
166
+ }
167
+
168
+ // Example 4: Express app setup
169
+ const app = express();
170
+
171
+ // Mount the APIs
172
+ simpleApi.mount(app);
173
+ advancedApi.mount(app);
174
+
175
+ // Add custom middleware
176
+ advancedApi.useMiddleware((req, res, next) => {
177
+ // Add user ID from session for versioning
178
+ req.options = { userId: req.session?.userId };
179
+ next();
180
+ });
181
+
182
+ // Sync database schema
183
+ async function syncDatabase() {
184
+ await advancedApi.syncSchema(productSchema, 'products', {
185
+ connection: 'main'
186
+ });
187
+ }
188
+
189
+ // Start server
190
+ app.listen(3000, () => {
191
+ console.log('API server running on http://localhost:3000');
192
+ });
193
+
194
+ // Export for use in other modules
195
+ export { simpleApi, advancedApi, userSchema, productSchema };
package/index.js CHANGED
@@ -1,210 +1,61 @@
1
- /**
2
- * @fileOverview
3
- * Implements a simple REST API in an object. Requests and responses are in
4
- * JSON. Or are supposed to be.
5
- */
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
-
15
- // set a default port to listen for requests
16
- var DEFAULT_PORT = process.env.PORT ? process.env.PORT : 44401;
17
-
18
- exports.RestApi = RestApi;
19
-
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);
1
+ // Core exports
2
+ export { Api } from './api.js';
3
+ export { Schema } from './schema.js';
4
+
5
+ // Plugin exports
6
+ export { MemoryPlugin } from './plugins/memory.js';
7
+ export { MySQLPlugin } from './plugins/mysql.js';
8
+ export { HTTPPlugin } from './plugins/http.js';
9
+ export { ValidationPlugin } from './plugins/validation.js';
10
+ export { PositioningPlugin } from './plugins/positioning.js';
11
+ export { VersioningPlugin } from './plugins/versioning.js';
12
+ export { SecurityPlugin } from './plugins/security.js';
13
+ export { LoggingPlugin } from './plugins/logging.js';
14
+ export { OpenAPIPlugin } from './plugins/openapi.js';
15
+ export { ApiRegistryPlugin } from './plugins/api-registry.js';
16
+
17
+ // Import for internal use
18
+ import { Api } from './api.js';
19
+ import { MemoryPlugin } from './plugins/memory.js';
20
+ import { MySQLPlugin } from './plugins/mysql.js';
21
+ import { HTTPPlugin } from './plugins/http.js';
22
+ import { ValidationPlugin } from './plugins/validation.js';
23
+ import { PositioningPlugin } from './plugins/positioning.js';
24
+ import { VersioningPlugin } from './plugins/versioning.js';
25
+
26
+ // Convenience function to create a fully configured API
27
+ export function createApi(options = {}) {
28
+ const api = new Api(options);
29
+
30
+ // Add default plugins based on options
31
+ if (options.validation !== false) {
32
+ api.use(ValidationPlugin, options.validation);
33
+ }
34
+
35
+ if (options.storage === 'memory') {
36
+ api.use(MemoryPlugin, options.memory);
37
+ } else if (options.storage === 'mysql') {
38
+ api.use(MySQLPlugin, options.mysql);
39
+ }
40
+
41
+ // Auto-add HTTP plugin if http options are provided
42
+ if (options.http) {
43
+ api.use(HTTPPlugin, options.http);
44
+ }
45
+
46
+ if (options.positioning) {
47
+ api.use(PositioningPlugin, options.positioning);
48
+ }
49
+
50
+ if (options.versioning) {
51
+ api.use(VersioningPlugin, options.versioning);
52
+ }
53
+
54
+ return api;
69
55
  }
70
56
 
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
- }
191
-
192
- if (!is.positiveInt(this.statusCode)) {
193
- debug('http.ServerResponse.json bad status param: '+
194
- inspect(this.statusCode));
195
- return false;
196
- }
197
-
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
- }
57
+ // Re-export circular-json-es6 for convenience
58
+ export { stringify as serialize, parse as deserialize } from 'circular-json-es6';
206
59
 
207
- this.writeHead(this.statusCode, {'Content-Type': 'application/json'});
208
- this.end(str, 'utf8');
209
- return true;
210
- };
60
+ // Export version
61
+ export const VERSION = '1.0.0';
package/package.json CHANGED
@@ -1,35 +1,25 @@
1
1
  {
2
2
  "name": "json-rest-api",
3
- "version": "0.1.0",
4
- "description": "A simple lightweight REST API that receives and responds to JSON HTTP requests, supports all verbs. Very simple.",
3
+ "version": "1.0.0",
4
+ "description": "A simple, extensible RESTful JSON API library with schema validation and positioning",
5
5
  "main": "index.js",
6
- "scripts": {
7
- "test": "./node_modules/.bin/mocha --reporter list ./test.js"
8
- },
6
+ "type": "module",
7
+ "keywords": ["rest", "json-api", "mysql", "schema", "validation"],
8
+ "author": "Tony Mobily",
9
+ "license": "MIT",
9
10
  "repository": {
10
11
  "type": "git",
11
- "url": "git://github.com/stdarg/json-rest-api.git"
12
- },
13
- "keywords": [
14
- "simple",
15
- "REST",
16
- "HTTP",
17
- "JSON",
18
- "API",
19
- "interface"
20
- ],
21
- "author": "Edmond Meinfelder",
22
- "license": "MIT",
23
- "bugs": {
24
- "url": "https://github.com/stdarg/json-rest-api/issues"
12
+ "url": "https://github.com/mobily-enterprises/jsonrestapi.git"
25
13
  },
26
14
  "dependencies": {
27
- "async-err": "0.0.2",
28
- "debug": "0.7.4",
29
- "is2": "0.0.12"
15
+ "circular-json-es6": "^2.0.2",
16
+ "express": "^4.21.1",
17
+ "mysql2": "^3.13.1",
18
+ "thenby": "^1.3.4",
19
+ "semver": "^7.5.4"
30
20
  },
31
- "devDependencies": {
32
- "mocha": "1.17.1",
33
- "request": "2.33.0"
21
+ "devDependencies": {},
22
+ "engines": {
23
+ "node": ">=18"
34
24
  }
35
25
  }