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,261 +0,0 @@
1
- /**
2
- * Security plugin implementing industry-standard security practices
3
- */
4
- export const SecurityPlugin = {
5
- install(api, options = {}) {
6
- const defaultOptions = {
7
- rateLimit: {
8
- windowMs: 15 * 60 * 1000, // 15 minutes
9
- max: 100, // limit each IP to 100 requests per windowMs
10
- message: 'Too many requests from this IP'
11
- },
12
- cors: {
13
- origin: '*',
14
- credentials: true,
15
- methods: ['GET', 'POST', 'PATCH', 'DELETE', 'OPTIONS'],
16
- allowedHeaders: ['Content-Type', 'Authorization', 'X-Requested-With'],
17
- exposedHeaders: ['X-Total-Count', 'Link']
18
- },
19
- contentSecurityPolicy: {
20
- directives: {
21
- defaultSrc: ["'self'"],
22
- styleSrc: ["'self'", "'unsafe-inline'"],
23
- scriptSrc: ["'self'"],
24
- imgSrc: ["'self'", 'data:', 'https:'],
25
- connectSrc: ["'self'"],
26
- fontSrc: ["'self'"],
27
- objectSrc: ["'none'"],
28
- mediaSrc: ["'self'"],
29
- frameSrc: ["'none'"]
30
- }
31
- },
32
- authentication: {
33
- type: 'bearer', // 'bearer', 'basic', 'apikey'
34
- header: 'Authorization',
35
- queryParam: 'api_key'
36
- },
37
- ...options
38
- };
39
-
40
- // Store rate limit data
41
- const rateLimitStore = new Map();
42
-
43
- // Add security headers
44
- if (api.router) {
45
- // CORS
46
- api.router.use((req, res, next) => {
47
- const cors = defaultOptions.cors;
48
- res.header('Access-Control-Allow-Origin', cors.origin);
49
- res.header('Access-Control-Allow-Methods', cors.methods.join(', '));
50
- res.header('Access-Control-Allow-Headers', cors.allowedHeaders.join(', '));
51
- res.header('Access-Control-Expose-Headers', cors.exposedHeaders.join(', '));
52
- res.header('Access-Control-Allow-Credentials', cors.credentials);
53
-
54
- // Security headers
55
- res.header('X-Content-Type-Options', 'nosniff');
56
- res.header('X-Frame-Options', 'DENY');
57
- res.header('X-XSS-Protection', '1; mode=block');
58
- res.header('Strict-Transport-Security', 'max-age=31536000; includeSubDomains');
59
- res.header('Referrer-Policy', 'strict-origin-when-cross-origin');
60
-
61
- // Content Security Policy
62
- const csp = Object.entries(defaultOptions.contentSecurityPolicy.directives)
63
- .map(([key, values]) => `${key} ${values.join(' ')}`)
64
- .join('; ');
65
- res.header('Content-Security-Policy', csp);
66
-
67
- next();
68
- });
69
-
70
- // Rate limiting
71
- api.router.use((req, res, next) => {
72
- const ip = req.ip || req.connection.remoteAddress;
73
- const now = Date.now();
74
- const windowStart = now - defaultOptions.rateLimit.windowMs;
75
-
76
- // Clean old entries
77
- const requests = rateLimitStore.get(ip) || [];
78
- const recentRequests = requests.filter(time => time > windowStart);
79
-
80
- if (recentRequests.length >= defaultOptions.rateLimit.max) {
81
- return res.status(429).json({
82
- errors: [{
83
- status: '429',
84
- title: 'Too Many Requests',
85
- detail: defaultOptions.rateLimit.message
86
- }]
87
- });
88
- }
89
-
90
- recentRequests.push(now);
91
- rateLimitStore.set(ip, recentRequests);
92
-
93
- // Add rate limit headers
94
- res.header('X-RateLimit-Limit', defaultOptions.rateLimit.max);
95
- res.header('X-RateLimit-Remaining', defaultOptions.rateLimit.max - recentRequests.length);
96
- res.header('X-RateLimit-Reset', new Date(now + defaultOptions.rateLimit.windowMs).toISOString());
97
-
98
- next();
99
- });
100
-
101
- // Request ID for tracing
102
- api.router.use((req, res, next) => {
103
- req.id = req.headers['x-request-id'] || generateRequestId();
104
- res.header('X-Request-ID', req.id);
105
- next();
106
- });
107
- }
108
-
109
- // Authentication hook
110
- api.hook('beforeValidate', async (context) => {
111
- // Skip auth check for read operations if configured
112
- if (options.publicRead && (context.method === 'get' || context.method === 'query')) {
113
- return;
114
- }
115
-
116
- const authConfig = defaultOptions.authentication;
117
- let token = null;
118
-
119
- // Extract token based on auth type
120
- if (context.options.request) {
121
- const req = context.options.request;
122
-
123
- switch (authConfig.type) {
124
- case 'bearer':
125
- const authHeader = req.headers[authConfig.header.toLowerCase()];
126
- if (authHeader && authHeader.startsWith('Bearer ')) {
127
- token = authHeader.substring(7);
128
- }
129
- break;
130
-
131
- case 'apikey':
132
- token = req.headers[authConfig.header.toLowerCase()] ||
133
- req.query[authConfig.queryParam];
134
- break;
135
-
136
- case 'basic':
137
- const basicAuth = req.headers[authConfig.header.toLowerCase()];
138
- if (basicAuth && basicAuth.startsWith('Basic ')) {
139
- token = basicAuth.substring(6);
140
- }
141
- break;
142
- }
143
- }
144
-
145
- // Verify token
146
- if (!token && authConfig.required !== false) {
147
- context.errors.push({
148
- field: null,
149
- message: 'Authentication required',
150
- code: 'AUTH_REQUIRED'
151
- });
152
- return;
153
- }
154
-
155
- if (token && options.verifyToken) {
156
- try {
157
- const user = await options.verifyToken(token, context);
158
- context.options.user = user;
159
- context.options.authenticated = true;
160
- } catch (error) {
161
- context.errors.push({
162
- field: null,
163
- message: 'Invalid authentication token',
164
- code: 'AUTH_INVALID'
165
- });
166
- }
167
- }
168
- });
169
-
170
- // Input sanitization
171
- api.hook('beforeValidate', async (context) => {
172
- if (context.data) {
173
- context.data = sanitizeObject(context.data);
174
- }
175
- });
176
-
177
- // SQL injection protection for MySQL
178
- api.hook('beforeQuery', async (context) => {
179
- if (context.params.filter) {
180
- // Validate filter keys against schema
181
- const type = context.options.type;
182
- const schema = api.schemas.get(type);
183
-
184
- if (schema) {
185
- for (const key in context.params.filter) {
186
- if (!schema.structure[key] && !options.allowUnknownFilters) {
187
- delete context.params.filter[key];
188
- }
189
- }
190
- }
191
- }
192
- });
193
-
194
- // Add security methods
195
- api.generateToken = (payload, expiresIn = '24h') => {
196
- // This would use JWT in production
197
- return Buffer.from(JSON.stringify({
198
- ...payload,
199
- exp: Date.now() + parseDuration(expiresIn)
200
- })).toString('base64');
201
- };
202
-
203
- api.verifyToken = (token) => {
204
- try {
205
- const decoded = JSON.parse(Buffer.from(token, 'base64').toString());
206
- if (decoded.exp && decoded.exp < Date.now()) {
207
- throw new Error('Token expired');
208
- }
209
- return decoded;
210
- } catch (error) {
211
- throw new Error('Invalid token');
212
- }
213
- };
214
- }
215
- };
216
-
217
- // Helper functions
218
- function generateRequestId() {
219
- return `req_${Date.now()}_${Math.random().toString(36).substr(2, 9)}`;
220
- }
221
-
222
- function sanitizeObject(obj) {
223
- if (typeof obj !== 'object' || obj === null) return obj;
224
-
225
- const sanitized = Array.isArray(obj) ? [] : {};
226
-
227
- for (const [key, value] of Object.entries(obj)) {
228
- // Remove potential XSS in keys
229
- const cleanKey = key.replace(/[<>]/g, '');
230
-
231
- if (typeof value === 'string') {
232
- // Basic XSS prevention
233
- sanitized[cleanKey] = value
234
- .replace(/</g, '&lt;')
235
- .replace(/>/g, '&gt;')
236
- .replace(/"/g, '&quot;')
237
- .replace(/'/g, '&#x27;')
238
- .replace(/\//g, '&#x2F;');
239
- } else if (typeof value === 'object') {
240
- sanitized[cleanKey] = sanitizeObject(value);
241
- } else {
242
- sanitized[cleanKey] = value;
243
- }
244
- }
245
-
246
- return sanitized;
247
- }
248
-
249
- function parseDuration(duration) {
250
- const units = {
251
- s: 1000,
252
- m: 60 * 1000,
253
- h: 60 * 60 * 1000,
254
- d: 24 * 60 * 60 * 1000
255
- };
256
-
257
- const match = duration.match(/^(\d+)([smhd])$/);
258
- if (!match) return 24 * 60 * 60 * 1000; // Default 24h
259
-
260
- return parseInt(match[1]) * units[match[2]];
261
- }
@@ -1,173 +0,0 @@
1
- /**
2
- * Validation plugin for JSON REST API
3
- */
4
- export const ValidationPlugin = {
5
- install(api, options = {}) {
6
- // Initialize schemas map if not already present
7
- if (!api.schemas) {
8
- api.schemas = new Map();
9
- }
10
-
11
- // Get schema for a type
12
- api.getSchema = (type) => {
13
- return api.schemas.get(type);
14
- };
15
-
16
- // Add validation hooks
17
- api.hook('beforeValidate', async (context) => {
18
- const { method, data, options } = context;
19
- const type = options.type || 'default';
20
- const schema = api.schemas.get(type);
21
-
22
- if (!schema) {
23
- return; // No schema registered for this type
24
- }
25
-
26
- // Skip validation for certain methods
27
- if (method === 'delete' || method === 'get' || method === 'query') {
28
- return;
29
- }
30
-
31
- // Determine validation options
32
- const validationOptions = {
33
- emptyAsNull: options.emptyAsNull ?? schema.options?.emptyAsNull ?? false,
34
- canBeNull: options.canBeNull ?? schema.options?.canBeNull ?? false,
35
- onlyObjectValues: method === 'update' && !options.fullRecord,
36
- skipParams: options.skipParams
37
- };
38
-
39
- // For updates, we might need to fetch the existing record
40
- if (method === 'update' && options.validateFullRecord) {
41
- const impl = api.implementers.get('get');
42
- if (impl) {
43
- const existingRecord = await impl({
44
- id: context.id,
45
- options: context.options
46
- });
47
- if (existingRecord) {
48
- context.existingRecord = existingRecord;
49
- context.data = { ...existingRecord, ...data };
50
- }
51
- }
52
- }
53
-
54
- // Validate the data
55
- const { validatedObject, errors } = await schema.validate(
56
- context.data,
57
- validationOptions
58
- );
59
-
60
- // Update context with validated data and errors
61
- context.data = validatedObject;
62
- context.errors.push(...errors);
63
- });
64
-
65
- // Add permission checking hook
66
- api.hook('afterValidate', async (context) => {
67
- const { method, options } = context;
68
- const type = options.type || 'default';
69
-
70
- // Check permissions if handler is registered
71
- if (options.checkPermissions) {
72
- const permissionHandler = options.checkPermissions;
73
- const { granted, message } = await permissionHandler(context);
74
-
75
- if (!granted) {
76
- context.errors.push({
77
- field: null,
78
- message: message || 'Permission denied',
79
- code: 'PERMISSION_DENIED'
80
- });
81
- }
82
- }
83
-
84
- // Run custom validation if provided
85
- if (options.validate) {
86
- await options.validate(context, context.errors);
87
- }
88
- });
89
-
90
- // Add search schema validation for queries
91
- api.hook('beforeQuery', async (context) => {
92
- const { params, options } = context;
93
- const type = options.type || 'default';
94
- const schema = api.schemas.get(type);
95
-
96
- if (!schema || !options.searchSchema) {
97
- return;
98
- }
99
-
100
- // Validate search parameters
101
- const searchData = {};
102
-
103
- // Extract search parameters from query params
104
- if (params.filter) {
105
- Object.assign(searchData, params.filter);
106
- }
107
-
108
- // Add any custom search fields
109
- if (params.search && options.searchField) {
110
- searchData[options.searchField] = params.search;
111
- }
112
-
113
- // Validate search parameters
114
- const { validatedObject, errors } = await options.searchSchema.validate(
115
- searchData,
116
- { onlyObjectValues: true }
117
- );
118
-
119
- if (errors.length > 0) {
120
- context.errors.push(...errors);
121
- } else {
122
- // Update filter with validated values
123
- if (params.filter) {
124
- params.filter = validatedObject;
125
- }
126
- }
127
- });
128
-
129
- // Helper to create a search schema from main schema
130
- api.createSearchSchema = (schema, searchableFields) => {
131
- const searchStructure = {};
132
-
133
- for (const field of searchableFields) {
134
- if (schema.structure[field]) {
135
- searchStructure[field] = {
136
- ...schema.structure[field],
137
- required: false // Search fields are never required
138
- };
139
- }
140
- }
141
-
142
- // Use the same Schema class as the main schema
143
- return new schema.constructor(searchStructure);
144
- };
145
-
146
- // Add field cleanup functionality
147
- api.hook('transformResult', async (context) => {
148
- const { result, options } = context;
149
- const type = options.type || 'default';
150
- const schema = api.schemas.get(type);
151
-
152
- if (!result || !schema || !options.cleanupFields) {
153
- return;
154
- }
155
-
156
- // Remove fields not in schema
157
- for (const key in result) {
158
- if (!schema.structure[key]) {
159
- delete result[key];
160
- }
161
- }
162
-
163
- // Remove silent fields if requested
164
- if (options.removeSilentFields) {
165
- for (const [key, definition] of Object.entries(schema.structure)) {
166
- if (definition.silent && result[key] !== undefined) {
167
- delete result[key];
168
- }
169
- }
170
- }
171
- });
172
- }
173
- };