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
@@ -0,0 +1,218 @@
1
+ /**
2
+ * Local File Storage Adapter
3
+ *
4
+ * Production-ready local storage with secure filename handling
5
+ */
6
+
7
+ import { promises as fs } from 'fs';
8
+ import path from 'path';
9
+ import crypto from 'crypto';
10
+
11
+ export class LocalStorage {
12
+ constructor(options = {}) {
13
+ this.directory = path.resolve(options.directory || './uploads');
14
+ this.baseUrl = options.baseUrl || '/uploads';
15
+ this.nameStrategy = options.nameStrategy || 'hash'; // 'hash', 'timestamp', 'original', 'custom'
16
+ this.nameGenerator = options.nameGenerator; // Custom function
17
+ this.preserveExtension = options.preserveExtension !== false;
18
+ this.allowedExtensions = options.allowedExtensions; // Whitelist
19
+ this.maxFilenameLength = options.maxFilenameLength || 255;
20
+ }
21
+
22
+ /**
23
+ * Generate a safe filename
24
+ */
25
+ async generateFilename(file) {
26
+ let basename, extension;
27
+
28
+ // Extract and validate extension
29
+ const originalExt = path.extname(file.filename).toLowerCase();
30
+ if (this.preserveExtension && originalExt) {
31
+ // Validate extension against whitelist if provided
32
+ if (this.allowedExtensions && !this.allowedExtensions.includes(originalExt)) {
33
+ throw new Error(`File extension '${originalExt}' is not allowed`);
34
+ }
35
+ extension = originalExt;
36
+ } else {
37
+ // Determine extension from MIME type for safety
38
+ extension = this.getExtensionFromMimeType(file.mimetype);
39
+ }
40
+
41
+ // Generate basename based on strategy
42
+ switch (this.nameStrategy) {
43
+ case 'hash':
44
+ // Cryptographically secure random name
45
+ basename = crypto.randomBytes(16).toString('hex');
46
+ break;
47
+
48
+ case 'timestamp':
49
+ // Timestamp + random suffix to prevent collisions
50
+ basename = `${Date.now()}_${crypto.randomBytes(4).toString('hex')}`;
51
+ break;
52
+
53
+ case 'original':
54
+ // Sanitized original filename
55
+ basename = this.sanitizeFilename(
56
+ path.basename(file.filename, originalExt)
57
+ );
58
+ break;
59
+
60
+ case 'custom':
61
+ if (!this.nameGenerator) {
62
+ throw new Error('nameGenerator function required for custom strategy');
63
+ }
64
+ basename = await this.nameGenerator(file);
65
+ break;
66
+
67
+ default:
68
+ throw new Error(`Unknown name strategy: ${this.nameStrategy}`);
69
+ }
70
+
71
+ // Combine basename and extension
72
+ let filename = extension ? `${basename}${extension}` : basename;
73
+
74
+ // Ensure filename doesn't exceed max length
75
+ if (filename.length > this.maxFilenameLength) {
76
+ // Truncate basename to fit
77
+ const maxBasename = this.maxFilenameLength - (extension?.length || 0);
78
+ basename = basename.substring(0, maxBasename);
79
+ filename = extension ? `${basename}${extension}` : basename;
80
+ }
81
+
82
+ // Handle duplicates
83
+ filename = await this.ensureUnique(filename);
84
+
85
+ return filename;
86
+ }
87
+
88
+ /**
89
+ * Sanitize filename for safe storage
90
+ */
91
+ sanitizeFilename(filename) {
92
+ return filename
93
+ // Remove path traversal attempts
94
+ .replace(/\.\./g, '')
95
+ .replace(/[\/\\]/g, '')
96
+ // Remove control characters and special chars
97
+ .replace(/[^\w\s.-]/g, '_')
98
+ // Remove leading/trailing dots and spaces
99
+ .replace(/^[\s.]+|[\s.]+$/g, '')
100
+ // Collapse multiple underscores
101
+ .replace(/_+/g, '_')
102
+ // Default if empty
103
+ || 'unnamed';
104
+ }
105
+
106
+ /**
107
+ * Ensure filename is unique
108
+ */
109
+ async ensureUnique(filename) {
110
+ const filepath = path.join(this.directory, filename);
111
+
112
+ try {
113
+ await fs.access(filepath);
114
+ // File exists, generate unique name
115
+ const ext = path.extname(filename);
116
+ const base = path.basename(filename, ext);
117
+ let counter = 1;
118
+ let newFilename;
119
+
120
+ do {
121
+ newFilename = `${base}_${counter}${ext}`;
122
+ counter++;
123
+ } while (await this.fileExists(path.join(this.directory, newFilename)));
124
+
125
+ return newFilename;
126
+ } catch (error) {
127
+ // File doesn't exist, name is unique
128
+ return filename;
129
+ }
130
+ }
131
+
132
+ /**
133
+ * Check if file exists
134
+ */
135
+ async fileExists(filepath) {
136
+ try {
137
+ await fs.access(filepath);
138
+ return true;
139
+ } catch {
140
+ return false;
141
+ }
142
+ }
143
+
144
+ /**
145
+ * Get safe extension from MIME type
146
+ */
147
+ getExtensionFromMimeType(mimetype) {
148
+ // Safe mappings only
149
+ const mimeToExt = {
150
+ 'image/jpeg': '.jpg',
151
+ 'image/png': '.png',
152
+ 'image/gif': '.gif',
153
+ 'image/webp': '.webp',
154
+ 'application/pdf': '.pdf',
155
+ 'text/plain': '.txt',
156
+ 'application/json': '.json',
157
+ 'video/mp4': '.mp4',
158
+ 'audio/mpeg': '.mp3',
159
+ 'application/zip': '.zip'
160
+ };
161
+
162
+ return mimeToExt[mimetype] || '.bin';
163
+ }
164
+
165
+ /**
166
+ * Upload file with secure filename
167
+ */
168
+ async upload(file) {
169
+ // Ensure directory exists
170
+ await fs.mkdir(this.directory, { recursive: true });
171
+
172
+ // Generate secure filename
173
+ const filename = await this.generateFilename(file);
174
+ const filepath = path.join(this.directory, filename);
175
+
176
+ // Ensure we're not writing outside our directory (defense in depth)
177
+ const resolvedPath = path.resolve(filepath);
178
+ const resolvedDir = path.resolve(this.directory);
179
+ if (!resolvedPath.startsWith(resolvedDir)) {
180
+ throw new Error('Invalid file path');
181
+ }
182
+
183
+ // Write file
184
+ if (file.data) {
185
+ await fs.writeFile(filepath, file.data);
186
+ } else if (file.filepath) {
187
+ await fs.rename(file.filepath, filepath);
188
+ } else {
189
+ throw new Error('File has no data or filepath');
190
+ }
191
+
192
+ // Return public URL
193
+ return `${this.baseUrl}/${filename}`;
194
+ }
195
+
196
+ /**
197
+ * Delete a file
198
+ */
199
+ async delete(url) {
200
+ const filename = path.basename(url);
201
+ const filepath = path.join(this.directory, filename);
202
+
203
+ // Security check
204
+ const resolvedPath = path.resolve(filepath);
205
+ const resolvedDir = path.resolve(this.directory);
206
+ if (!resolvedPath.startsWith(resolvedDir)) {
207
+ throw new Error('Invalid file path');
208
+ }
209
+
210
+ try {
211
+ await fs.unlink(filepath);
212
+ } catch (error) {
213
+ if (error.code !== 'ENOENT') {
214
+ throw error;
215
+ }
216
+ }
217
+ }
218
+ }
@@ -0,0 +1,125 @@
1
+ /**
2
+ * S3 Storage Adapter
3
+ *
4
+ * Stores uploaded files in Amazon S3 or S3-compatible storage.
5
+ *
6
+ * Usage:
7
+ * ```javascript
8
+ * import { S3Storage } from 'jsonrestapi/lib/storage/s3-storage.js';
9
+ *
10
+ * const storage = new S3Storage({
11
+ * bucket: 'my-uploads',
12
+ * region: 'us-east-1',
13
+ * accessKeyId: process.env.AWS_ACCESS_KEY_ID,
14
+ * secretAccessKey: process.env.AWS_SECRET_ACCESS_KEY,
15
+ * prefix: 'uploads/' // optional path prefix
16
+ * });
17
+ *
18
+ * const schema = {
19
+ * image: { type: 'file', storage }
20
+ * };
21
+ * ```
22
+ */
23
+
24
+ import crypto from 'crypto';
25
+ import path from 'path';
26
+
27
+ export class S3Storage {
28
+ constructor(options = {}) {
29
+ this.bucket = options.bucket;
30
+ this.region = options.region || 'us-east-1';
31
+ this.prefix = options.prefix || '';
32
+ this.acl = options.acl || 'public-read';
33
+
34
+ if (!this.bucket) {
35
+ throw new Error('S3Storage requires a bucket name');
36
+ }
37
+
38
+ // Note: In a real implementation, you would initialize the AWS SDK here
39
+ // For this example, we'll just mock the behavior
40
+ this.mockMode = options.mockMode !== false; // Default to mock mode
41
+
42
+ if (!this.mockMode) {
43
+ // Real S3 initialization would go here
44
+ // const { S3Client } = require('@aws-sdk/client-s3');
45
+ // this.s3 = new S3Client({ region: this.region });
46
+ }
47
+ }
48
+
49
+ /**
50
+ * Upload a file to S3
51
+ * @param {Object} file - File object from detector
52
+ * @returns {Promise<string>} URL of the uploaded file
53
+ */
54
+ async upload(file) {
55
+ // Generate S3 key
56
+ const ext = path.extname(file.filename);
57
+ const hash = crypto.randomBytes(16).toString('hex');
58
+ const key = `${this.prefix}${hash}${ext}`;
59
+
60
+ if (this.mockMode) {
61
+ // Mock mode - just return a fake URL
62
+ const url = `https://${this.bucket}.s3.${this.region}.amazonaws.com/${key}`;
63
+
64
+ // Simulate upload delay
65
+ await new Promise(resolve => setTimeout(resolve, 100));
66
+
67
+ return url;
68
+ }
69
+
70
+ // Real S3 upload would go here
71
+ /*
72
+ const { PutObjectCommand } = require('@aws-sdk/client-s3');
73
+
74
+ const command = new PutObjectCommand({
75
+ Bucket: this.bucket,
76
+ Key: key,
77
+ Body: file.data,
78
+ ContentType: file.mimetype,
79
+ ACL: this.acl,
80
+ Metadata: {
81
+ originalName: file.filename
82
+ }
83
+ });
84
+
85
+ await this.s3.send(command);
86
+
87
+ // Return public URL
88
+ if (this.acl === 'public-read') {
89
+ return `https://${this.bucket}.s3.${this.region}.amazonaws.com/${key}`;
90
+ } else {
91
+ // For private files, you might return a signed URL
92
+ return await this.getSignedUrl(key);
93
+ }
94
+ */
95
+ }
96
+
97
+ /**
98
+ * Delete a file from S3
99
+ * @param {string} url - URL returned by upload()
100
+ * @returns {Promise<void>}
101
+ */
102
+ async delete(url) {
103
+ // Extract key from URL
104
+ const urlObj = new URL(url);
105
+ const key = urlObj.pathname.substring(1); // Remove leading /
106
+
107
+ if (this.mockMode) {
108
+ // Mock mode - just simulate deletion
109
+ await new Promise(resolve => setTimeout(resolve, 50));
110
+ return;
111
+ }
112
+
113
+ // Real S3 delete would go here
114
+ /*
115
+ const { DeleteObjectCommand } = require('@aws-sdk/client-s3');
116
+
117
+ const command = new DeleteObjectCommand({
118
+ Bucket: this.bucket,
119
+ Key: key
120
+ });
121
+
122
+ await this.s3.send(command);
123
+ */
124
+ }
125
+ }