json-rest-api 1.0.0 → 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 (67) hide show
  1. package/.claude/settings.local.json +16 -0
  2. package/LICENSE +674 -0
  3. package/README.md +655 -309
  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 +31 -56
  19. package/lib/payload-validators.js +685 -0
  20. package/lib/rest-api-errors.js +58 -0
  21. package/package.json +41 -14
  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/TODO.todo +0 -113
  46. package/api.js +0 -628
  47. package/docs/API.md +0 -1306
  48. package/docs/FIXES_APPLIED.md +0 -59
  49. package/docs/GUIDES.md +0 -1792
  50. package/docs/RESOURCES_IN_EXPRESS.md +0 -456
  51. package/docs/example-versioning.js +0 -168
  52. package/docs/example.js +0 -195
  53. package/plugins/API guide.md +0 -364
  54. package/plugins/api-registry.js +0 -142
  55. package/plugins/api-versioning.md +0 -135
  56. package/plugins/http.js +0 -273
  57. package/plugins/logging.js +0 -255
  58. package/plugins/memory.js +0 -182
  59. package/plugins/mysql.js +0 -753
  60. package/plugins/openapi.js +0 -401
  61. package/plugins/positioning.js +0 -220
  62. package/plugins/security.js +0 -261
  63. package/plugins/validation.js +0 -173
  64. package/plugins/versioning.js +0 -288
  65. package/resource-helper.js +0 -79
  66. package/schema.js +0 -481
  67. package/test-basic.js +0 -107
@@ -1,135 +0,0 @@
1
-
2
- 1. Creating Versioned APIs
3
-
4
- // Just specify name and version - that's it!
5
- const userApi = createApi({
6
- name: 'users',
7
- version: '1.0.0',
8
- storage: 'memory'
9
- });
10
-
11
- // Create a new version
12
- const userApiV2 = createApi({
13
- name: 'users',
14
- version: '2.0.0',
15
- storage: 'memory'
16
- });
17
-
18
- 2. Using APIs Programmatically
19
-
20
- // Get latest version automatically
21
- const api = Api.get('users', 'latest');
22
-
23
- // Get specific version
24
- const apiV1 = Api.get('users', '1.0.0');
25
-
26
- // Get minimum version (2.0.0 or higher)
27
- const apiV2Plus = Api.get('users', '2.0.0');
28
-
29
- // Use it
30
- const user = await api.insert({
31
- name: 'John',
32
- email: 'john@example.com'
33
- }, { type: 'users' });
34
-
35
- 3. Cross-API Communication
36
-
37
- APIs can access each other automatically with version compatibility:
38
-
39
- const ordersApi = createApi({
40
- name: 'orders',
41
- version: '1.0.0'
42
- });
43
-
44
- // Inside orders API, access users API automatically
45
- ordersApi.hook('afterInsert', async (context) => {
46
- // This gets a compatible users API automatically!
47
- const usersApi = ordersApi.apis.users;
48
- const user = await usersApi.get(context.data.userId, { type: 'users'
49
- });
50
- });
51
-
52
- 4. HTTP Version Negotiation
53
-
54
- The library handles all HTTP version routing automatically:
55
-
56
- // Mount APIs - versioning is automatic
57
- userApiV1.mount(app); // Available at /api/1.0.0/users
58
- userApiV2.mount(app); // Available at /api/2.0.0/users
59
-
60
- // Clients can request versions:
61
- // Via header
62
- fetch('/api/users', {
63
- headers: { 'API-Version': '2.0.0' }
64
- });
65
-
66
- // Via query
67
- fetch('/api/users?v=2.0.0');
68
-
69
- // Via path
70
- fetch('/api/2.0.0/users');
71
-
72
- 5. Version Resolution Rules
73
-
74
- The library automatically finds the right version:
75
-
76
- - 'latest' → Newest version
77
- - '2.0.0' → Exactly 2.0.0 OR the newest version ≥ 2.0.0
78
- - '^2.0.0' → Any 2.x.x version (npm style)
79
- - '~2.1.0' → Any 2.1.x version (npm style)
80
- - '>=2.0.0' → Any version ≥ 2.0.0
81
-
82
- 6. Complete Working Example
83
-
84
- // Define v1
85
- const apiV1 = createApi({
86
- name: 'products',
87
- version: '1.0.0',
88
- storage: 'memory'
89
- });
90
-
91
- apiV1.addResource('products', new Schema({
92
- id: { type: 'id' },
93
- name: { type: 'string', required: true },
94
- price: { type: 'number', required: true }
95
- }));
96
-
97
- // Define v2 with new field
98
- const apiV2 = createApi({
99
- name: 'products',
100
- version: '2.0.0',
101
- storage: 'memory'
102
- });
103
-
104
- apiV2.addResource('products', new Schema({
105
- id: { type: 'id' },
106
- name: { type: 'string', required: true },
107
- price: { type: 'number', required: true },
108
- category: { type: 'string', required: true } // New!
109
- }));
110
-
111
- // Use programmatically - version selection is automatic
112
- async function createProduct(data) {
113
- // Automatically picks the right version based on data
114
- const hasCategory = 'category' in data;
115
- const minVersion = hasCategory ? '2.0.0' : '1.0.0';
116
-
117
- const api = Api.get('products', minVersion);
118
- return api.insert(data, { type: 'products' });
119
- }
120
-
121
- // Works with v1
122
- await createProduct({
123
- name: 'Widget',
124
- price: 9.99
125
- });
126
-
127
- // Automatically uses v2
128
- await createProduct({
129
- name: 'Gadget',
130
- price: 19.99,
131
- category: 'Electronics'
132
- });
133
-
134
- The library handles all the complexity - you just specify name and
135
- version when creating APIs, and the library does the rest!
package/plugins/http.js DELETED
@@ -1,273 +0,0 @@
1
- import express from 'express';
2
-
3
- /**
4
- * HTTP plugin for JSON REST API with JSON:API compliance
5
- */
6
- export const HTTPPlugin = {
7
- install(api, options = {}) {
8
- const router = express.Router();
9
- api.router = router;
10
-
11
- // Middleware for JSON parsing
12
- router.use(express.json({
13
- type: ['application/json', 'application/vnd.api+json']
14
- }));
15
-
16
- // Base path for all routes
17
- const basePath = options.basePath || '/api';
18
-
19
- // Helper to parse JSON:API request body
20
- const parseJsonApiBody = (body) => {
21
- if (body.data) {
22
- if (body.data.attributes) {
23
- // JSON:API format
24
- return { ...body.data.attributes, id: body.data.id };
25
- }
26
- // Assume data is the actual data
27
- return body.data;
28
- }
29
- // Plain format
30
- return body;
31
- };
32
-
33
- // Helper to format JSON:API errors
34
- const formatErrors = (errors, status = 400) => {
35
- if (!Array.isArray(errors)) {
36
- errors = [errors];
37
- }
38
-
39
- return {
40
- errors: errors.map(err => {
41
- if (err.status) return err; // Already formatted
42
-
43
- return {
44
- status: String(status),
45
- title: err.code || 'Error',
46
- detail: err.message || String(err),
47
- source: err.field ? { pointer: `/data/attributes/${err.field}` } : undefined
48
- };
49
- })
50
- };
51
- };
52
-
53
- // Parse query parameters
54
- const parseQueryParams = (req) => {
55
- const params = {
56
- filter: {},
57
- sort: req.query.sort,
58
- page: {
59
- size: req.query['page[size]'] || req.query.pageSize,
60
- number: req.query['page[number]'] || req.query.page
61
- },
62
- include: req.query.include,
63
- fields: {}
64
- };
65
-
66
- // Parse filters
67
- for (const [key, value] of Object.entries(req.query)) {
68
- if (key.startsWith('filter[') && key.endsWith(']')) {
69
- const filterKey = key.slice(7, -1);
70
- params.filter[filterKey] = value;
71
- } else if (key.startsWith('fields[') && key.endsWith(']')) {
72
- const fieldType = key.slice(7, -1);
73
- params.fields[fieldType] = value.split(',');
74
- }
75
- }
76
-
77
- // Legacy filter support
78
- if (Object.keys(params.filter).length === 0) {
79
- // Copy non-standard query params as filters
80
- for (const [key, value] of Object.entries(req.query)) {
81
- if (!['sort', 'page', 'pageSize', 'include', 'fields'].includes(key) &&
82
- !key.includes('[')) {
83
- params.filter[key] = value;
84
- }
85
- }
86
- }
87
-
88
- return params;
89
- };
90
-
91
- // Build links for pagination
92
- const buildLinks = (req, params, meta) => {
93
- const links = {};
94
- const baseUrl = `${req.protocol}://${req.get('host')}${req.baseUrl}${req.path}`;
95
- const queryParams = new URLSearchParams(req.query);
96
-
97
- // Self link
98
- links.self = `${baseUrl}?${queryParams}`;
99
-
100
- if (meta.totalPages > 1) {
101
- const currentPage = meta.pageNumber;
102
-
103
- // First page
104
- queryParams.set('page[number]', '1');
105
- links.first = `${baseUrl}?${queryParams}`;
106
-
107
- // Last page
108
- queryParams.set('page[number]', String(meta.totalPages));
109
- links.last = `${baseUrl}?${queryParams}`;
110
-
111
- // Previous page
112
- if (currentPage > 1) {
113
- queryParams.set('page[number]', String(currentPage - 1));
114
- links.prev = `${baseUrl}?${queryParams}`;
115
- }
116
-
117
- // Next page
118
- if (currentPage < meta.totalPages) {
119
- queryParams.set('page[number]', String(currentPage + 1));
120
- links.next = `${baseUrl}?${queryParams}`;
121
- }
122
- }
123
-
124
- return links;
125
- };
126
-
127
- // Version-aware routing
128
- if (api.options.name && api.options.version) {
129
- // Add version info to all responses
130
- router.use((req, res, next) => {
131
- res.set('API-Version', api.options.version);
132
- next();
133
- });
134
- }
135
-
136
- // Routes
137
-
138
- // GET collection
139
- router.get('/:type', async (req, res) => {
140
- try {
141
- const params = parseQueryParams(req);
142
- const result = await api.query(params, {
143
- type: req.params.type,
144
- ...options.typeOptions?.[req.params.type]
145
- });
146
-
147
- // Add links
148
- if (result.meta) {
149
- result.links = buildLinks(req, params, result.meta);
150
- }
151
-
152
- res.json(result);
153
- } catch (error) {
154
- const status = error.status || 400;
155
- res.status(status).json(formatErrors(error.errors || error, status));
156
- }
157
- });
158
-
159
- // GET single resource
160
- router.get('/:type/:id', async (req, res) => {
161
- try {
162
- const result = await api.get(req.params.id, {
163
- type: req.params.type,
164
- ...options.typeOptions?.[req.params.type]
165
- });
166
-
167
- if (!result.data) {
168
- res.status(404).json(formatErrors({
169
- message: 'Resource not found',
170
- code: 'NOT_FOUND'
171
- }, 404));
172
- return;
173
- }
174
-
175
- res.json(result);
176
- } catch (error) {
177
- const status = error.status || 400;
178
- res.status(status).json(formatErrors(error.errors || error, status));
179
- }
180
- });
181
-
182
- // POST new resource
183
- router.post('/:type', async (req, res) => {
184
- try {
185
- const data = parseJsonApiBody(req.body);
186
- const result = await api.insert(data, {
187
- type: req.params.type,
188
- ...options.typeOptions?.[req.params.type]
189
- });
190
-
191
- res.status(201)
192
- .location(`${basePath}/${req.params.type}/${result.data.id}`)
193
- .json(result);
194
- } catch (error) {
195
- const status = error.status || 400;
196
- res.status(status).json(formatErrors(error.errors || error, status));
197
- }
198
- });
199
-
200
- // PATCH update resource
201
- router.patch('/:type/:id', async (req, res) => {
202
- try {
203
- const data = parseJsonApiBody(req.body);
204
- const result = await api.update(req.params.id, data, {
205
- type: req.params.type,
206
- ...options.typeOptions?.[req.params.type]
207
- });
208
-
209
- if (!result.data) {
210
- res.status(404).json(formatErrors({
211
- message: 'Resource not found',
212
- code: 'NOT_FOUND'
213
- }, 404));
214
- return;
215
- }
216
-
217
- res.json(result);
218
- } catch (error) {
219
- const status = error.status || 400;
220
- res.status(status).json(formatErrors(error.errors || error, status));
221
- }
222
- });
223
-
224
- // DELETE resource
225
- router.delete('/:type/:id', async (req, res) => {
226
- try {
227
- await api.delete(req.params.id, {
228
- type: req.params.type,
229
- ...options.typeOptions?.[req.params.type]
230
- });
231
-
232
- res.status(204).end();
233
- } catch (error) {
234
- const status = error.status || 400;
235
- res.status(status).json(formatErrors(error.errors || error, status));
236
- }
237
- });
238
-
239
- // OPTIONS for CORS
240
- router.options('*', (req, res) => {
241
- res.header('Access-Control-Allow-Methods', 'GET, POST, PATCH, DELETE, OPTIONS');
242
- res.header('Access-Control-Allow-Headers', 'Content-Type, Authorization');
243
- res.status(204).end();
244
- });
245
-
246
- // Mount router on Express app if provided
247
- if (options.app) {
248
- options.app.use(basePath, router);
249
- }
250
-
251
- // Add method to manually mount router
252
- api.mount = (app, path = basePath) => {
253
- app.use(path, router);
254
- return api;
255
- };
256
-
257
- // Add middleware support
258
- api.useMiddleware = (middleware) => {
259
- router.use(middleware);
260
- return api;
261
- };
262
-
263
- // Add route-specific middleware
264
- api.useRouteMiddleware = (method, path, ...middlewares) => {
265
- // Store middleware to be applied when routes are defined
266
- if (!api._routeMiddlewares) {
267
- api._routeMiddlewares = [];
268
- }
269
- api._routeMiddlewares.push({ method, path, middlewares });
270
- return api;
271
- };
272
- }
273
- };
@@ -1,255 +0,0 @@
1
- /**
2
- * Logging plugin implementing structured logging best practices
3
- */
4
- export const LoggingPlugin = {
5
- install(api, options = {}) {
6
- const defaultOptions = {
7
- level: process.env.LOG_LEVEL || 'info',
8
- format: 'json', // 'json' or 'pretty'
9
- includeRequest: true,
10
- includeResponse: true,
11
- includeTiming: true,
12
- sensitiveFields: ['password', 'token', 'secret', 'authorization'],
13
- logger: console, // Can be replaced with winston, bunyan, etc.
14
- ...options
15
- };
16
-
17
- const levels = {
18
- error: 0,
19
- warn: 1,
20
- info: 2,
21
- debug: 3
22
- };
23
-
24
- // Create structured logger
25
- const log = (level, message, meta = {}) => {
26
- if (levels[level] > levels[defaultOptions.level]) return;
27
-
28
- const timestamp = new Date().toISOString();
29
- const logEntry = {
30
- timestamp,
31
- level,
32
- message,
33
- ...meta
34
- };
35
-
36
- // Remove sensitive data
37
- const sanitized = sanitizeLogData(logEntry, defaultOptions.sensitiveFields);
38
-
39
- if (defaultOptions.format === 'json') {
40
- defaultOptions.logger[level](JSON.stringify(sanitized));
41
- } else {
42
- defaultOptions.logger[level](`[${timestamp}] ${level.toUpperCase()}: ${message}`, meta);
43
- }
44
- };
45
-
46
- // Add logging methods to API
47
- api.log = {
48
- error: (message, meta) => log('error', message, meta),
49
- warn: (message, meta) => log('warn', message, meta),
50
- info: (message, meta) => log('info', message, meta),
51
- debug: (message, meta) => log('debug', message, meta)
52
- };
53
-
54
- // Log all operations
55
- const timings = new WeakMap();
56
-
57
- // Start timing
58
- api.hook('beforeValidate', async (context) => {
59
- if (defaultOptions.includeTiming) {
60
- timings.set(context, Date.now());
61
- }
62
-
63
- if (defaultOptions.includeRequest) {
64
- api.log.info(`${context.method.toUpperCase()} ${context.options.type}`, {
65
- operation: context.method,
66
- type: context.options.type,
67
- id: context.id,
68
- requestId: context.options.requestId,
69
- userId: context.options.userId,
70
- data: context.method === 'query' ? context.params : context.data
71
- });
72
- }
73
- });
74
-
75
- // Log successful operations
76
- api.hook('beforeSend', async (context) => {
77
- const duration = defaultOptions.includeTiming && timings.has(context)
78
- ? Date.now() - timings.get(context)
79
- : undefined;
80
-
81
- if (defaultOptions.includeResponse) {
82
- api.log.info(`${context.method.toUpperCase()} ${context.options.type} completed`, {
83
- operation: context.method,
84
- type: context.options.type,
85
- id: context.id || context.result?.id,
86
- requestId: context.options.requestId,
87
- userId: context.options.userId,
88
- duration,
89
- resultCount: context.results?.length
90
- });
91
- }
92
-
93
- // Clean up timing
94
- if (timings.has(context)) {
95
- timings.delete(context);
96
- }
97
- });
98
-
99
- // Log errors
100
- api.hook('afterValidate', async (context) => {
101
- if (context.errors.length > 0) {
102
- api.log.warn('Validation errors', {
103
- operation: context.method,
104
- type: context.options.type,
105
- errors: context.errors,
106
- requestId: context.options.requestId,
107
- userId: context.options.userId
108
- });
109
- }
110
- });
111
-
112
- // Log database operations
113
- if (api.mysqlPools) {
114
- // Wrap MySQL query method to add logging
115
- const originalGetConnection = api.getConnection;
116
- api.getConnection = (name) => {
117
- const conn = originalGetConnection.call(api, name);
118
-
119
- // Wrap the query method
120
- const originalQuery = conn.pool.query.bind(conn.pool);
121
- conn.pool.query = async (sql, params) => {
122
- const start = Date.now();
123
-
124
- try {
125
- const result = await originalQuery(sql, params);
126
-
127
- api.log.debug('SQL query executed', {
128
- sql: sql.substring(0, 200), // Truncate long queries
129
- duration: Date.now() - start,
130
- rowCount: result[0]?.length
131
- });
132
-
133
- return result;
134
- } catch (error) {
135
- api.log.error('SQL query failed', {
136
- sql: sql.substring(0, 200),
137
- error: error.message,
138
- duration: Date.now() - start
139
- });
140
- throw error;
141
- }
142
- };
143
-
144
- return conn;
145
- };
146
- }
147
-
148
- // HTTP request logging
149
- if (api.router) {
150
- api.router.use((req, res, next) => {
151
- const start = Date.now();
152
- const requestId = req.headers['x-request-id'] || generateId();
153
-
154
- // Add request ID to context
155
- req.requestId = requestId;
156
-
157
- // Log request
158
- api.log.info('HTTP request', {
159
- method: req.method,
160
- url: req.url,
161
- ip: req.ip,
162
- userAgent: req.headers['user-agent'],
163
- requestId
164
- });
165
-
166
- // Log response
167
- const originalSend = res.send;
168
- res.send = function(data) {
169
- res.send = originalSend;
170
-
171
- api.log.info('HTTP response', {
172
- method: req.method,
173
- url: req.url,
174
- status: res.statusCode,
175
- duration: Date.now() - start,
176
- requestId
177
- });
178
-
179
- return res.send(data);
180
- };
181
-
182
- next();
183
- });
184
- }
185
-
186
- // Audit logging for sensitive operations
187
- api.hook('afterInsert', async (context) => {
188
- if (options.auditLog) {
189
- api.log.info('Resource created', {
190
- audit: true,
191
- operation: 'create',
192
- type: context.options.type,
193
- id: context.result?.id,
194
- userId: context.options.userId,
195
- changes: context.data
196
- });
197
- }
198
- });
199
-
200
- api.hook('afterUpdate', async (context) => {
201
- if (options.auditLog) {
202
- api.log.info('Resource updated', {
203
- audit: true,
204
- operation: 'update',
205
- type: context.options.type,
206
- id: context.id,
207
- userId: context.options.userId,
208
- changes: context.data
209
- });
210
- }
211
- });
212
-
213
- api.hook('afterDelete', async (context) => {
214
- if (options.auditLog) {
215
- api.log.info('Resource deleted', {
216
- audit: true,
217
- operation: 'delete',
218
- type: context.options.type,
219
- id: context.id,
220
- userId: context.options.userId
221
- });
222
- }
223
- });
224
- }
225
- };
226
-
227
- // Helper to sanitize sensitive data
228
- function sanitizeLogData(data, sensitiveFields) {
229
- if (typeof data !== 'object' || data === null) return data;
230
-
231
- const sanitized = Array.isArray(data) ? [] : {};
232
-
233
- for (const [key, value] of Object.entries(data)) {
234
- const lowerKey = key.toLowerCase();
235
-
236
- // Check if field is sensitive
237
- const isSensitive = sensitiveFields.some(field =>
238
- lowerKey.includes(field.toLowerCase())
239
- );
240
-
241
- if (isSensitive) {
242
- sanitized[key] = '[REDACTED]';
243
- } else if (typeof value === 'object') {
244
- sanitized[key] = sanitizeLogData(value, sensitiveFields);
245
- } else {
246
- sanitized[key] = value;
247
- }
248
- }
249
-
250
- return sanitized;
251
- }
252
-
253
- function generateId() {
254
- return Math.random().toString(36).substring(2, 15);
255
- }