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 +392 -130
- package/TODO.todo +113 -0
- package/api.js +628 -0
- package/docs/API.md +1306 -0
- package/docs/FIXES_APPLIED.md +59 -0
- package/docs/GUIDES.md +1792 -0
- package/docs/RESOURCES_IN_EXPRESS.md +456 -0
- package/docs/example-versioning.js +168 -0
- package/docs/example.js +195 -0
- package/index.js +58 -207
- package/package.json +15 -25
- package/plugins/API guide.md +364 -0
- package/plugins/api-registry.js +142 -0
- package/plugins/api-versioning.md +135 -0
- package/plugins/http.js +273 -0
- package/plugins/logging.js +255 -0
- package/plugins/memory.js +182 -0
- package/plugins/mysql.js +753 -0
- package/plugins/openapi.js +401 -0
- package/plugins/positioning.js +220 -0
- package/plugins/security.js +261 -0
- package/plugins/validation.js +173 -0
- package/plugins/versioning.js +288 -0
- package/resource-helper.js +79 -0
- package/schema.js +481 -0
- package/test-basic.js +107 -0
- package/.jshintrc +0 -48
- package/.npmignore +0 -15
- package/LICENSE +0 -20
- package/examples/client.js +0 -18
- package/examples/client2.js +0 -27
- package/examples/server.js +0 -23
- package/test.js +0 -101
package/docs/example.js
ADDED
|
@@ -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
|
-
|
|
3
|
-
|
|
4
|
-
|
|
5
|
-
|
|
6
|
-
|
|
7
|
-
|
|
8
|
-
|
|
9
|
-
|
|
10
|
-
|
|
11
|
-
|
|
12
|
-
|
|
13
|
-
|
|
14
|
-
|
|
15
|
-
|
|
16
|
-
|
|
17
|
-
|
|
18
|
-
|
|
19
|
-
|
|
20
|
-
|
|
21
|
-
|
|
22
|
-
|
|
23
|
-
|
|
24
|
-
|
|
25
|
-
|
|
26
|
-
|
|
27
|
-
|
|
28
|
-
|
|
29
|
-
|
|
30
|
-
|
|
31
|
-
|
|
32
|
-
|
|
33
|
-
|
|
34
|
-
|
|
35
|
-
|
|
36
|
-
|
|
37
|
-
|
|
38
|
-
|
|
39
|
-
|
|
40
|
-
|
|
41
|
-
|
|
42
|
-
|
|
43
|
-
|
|
44
|
-
|
|
45
|
-
|
|
46
|
-
|
|
47
|
-
|
|
48
|
-
|
|
49
|
-
|
|
50
|
-
|
|
51
|
-
|
|
52
|
-
|
|
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
|
-
|
|
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
|
-
|
|
208
|
-
|
|
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.
|
|
4
|
-
"description": "A 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
|
-
"
|
|
7
|
-
|
|
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": "
|
|
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
|
-
"
|
|
28
|
-
"
|
|
29
|
-
"
|
|
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
|
-
|
|
33
|
-
"
|
|
21
|
+
"devDependencies": {},
|
|
22
|
+
"engines": {
|
|
23
|
+
"node": ">=18"
|
|
34
24
|
}
|
|
35
25
|
}
|