json-rest-api 1.0.0 → 1.0.2

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,159 @@
1
+ # Complete File Upload Example with Multiple Files
2
+
3
+ This guide shows a complete example of implementing file uploads with jsonrestapi, including:
4
+ - Multiple file fields (images and PDFs)
5
+ - Different storage configurations for different file types
6
+ - HTML form for testing uploads
7
+ - Express static file serving
8
+
9
+ ## Complete Example
10
+
11
+ ```javascript
12
+ import { Api } from 'hooked-api';
13
+ import { RestApiPlugin, FileHandlingPlugin, ExpressPlugin } from 'json-rest-api';
14
+ import { LocalStorage } from 'json-rest-api/plugins/storage/local-storage.js';
15
+ import express from 'express';
16
+
17
+ // Create API
18
+ const api = new Api({
19
+ name: 'my-library-api',
20
+ version: '1.0.0'
21
+ });
22
+
23
+ // Create storage for different file types
24
+ const coverStorage = new LocalStorage({
25
+ directory: './uploads/covers',
26
+ baseUrl: 'http://localhost:3000/uploads/covers',
27
+ nameStrategy: 'hash',
28
+ allowedExtensions: ['.jpg', '.jpeg', '.png', '.gif']
29
+ });
30
+
31
+ const pdfStorage = new LocalStorage({
32
+ directory: './uploads/pdfs',
33
+ baseUrl: 'http://localhost:3000/uploads/pdfs',
34
+ nameStrategy: 'timestamp'
35
+ });
36
+
37
+ // Use plugins
38
+ api.use(RestApiPlugin);
39
+ api.use(FileHandlingPlugin);
40
+ api.use(ExpressPlugin); // File parser configuration shown in connector plugins section
41
+
42
+ // Define schema with multiple file fields
43
+ api.addResource('books', {
44
+ schema: {
45
+ title: { type: 'string', required: true },
46
+ author: { type: 'string', required: true },
47
+ year: { type: 'number' },
48
+ cover: {
49
+ type: 'file',
50
+ storage: coverStorage,
51
+ accepts: ['image/*'],
52
+ maxSize: '5mb'
53
+ },
54
+ sample: {
55
+ type: 'file',
56
+ storage: pdfStorage,
57
+ accepts: ['application/pdf'],
58
+ maxSize: '10mb',
59
+ required: false
60
+ }
61
+ }
62
+ });
63
+
64
+ // Simple data helpers
65
+ api.vars.helpers.dataPost = async ({ scopeName, inputRecord }) => {
66
+ const newBook = {
67
+ id: String(Date.now()),
68
+ ...inputRecord.data.attributes
69
+ };
70
+
71
+ console.log('Created book:', newBook);
72
+
73
+ return {
74
+ data: {
75
+ type: 'books',
76
+ id: newBook.id,
77
+ attributes: newBook
78
+ }
79
+ };
80
+ };
81
+
82
+ api.vars.helpers.dataQuery = async () => {
83
+ return { data: [] };
84
+ };
85
+
86
+ // Express setup
87
+ const app = express();
88
+ app.use('/uploads', express.static('./uploads'));
89
+
90
+ // Test form
91
+ app.get('/', (req, res) => {
92
+ res.send(`
93
+ <form action="/api/books" method="POST" enctype="multipart/form-data">
94
+ <h2>Add a Book</h2>
95
+ <p>Title: <input name="title" required></p>
96
+ <p>Author: <input name="author" required></p>
97
+ <p>Year: <input name="year" type="number"></p>
98
+ <p>Cover: <input name="cover" type="file" accept="image/*"></p>
99
+ <p>Sample PDF: <input name="sample" type="file" accept=".pdf"></p>
100
+ <button type="submit">Add Book</button>
101
+ </form>
102
+ `);
103
+ });
104
+
105
+ api.express.mount(app);
106
+
107
+ app.listen(3000, () => {
108
+ console.log('Library API running at http://localhost:3000');
109
+ console.log('Test form at http://localhost:3000');
110
+ console.log('API endpoints at http://localhost:3000/api/books');
111
+ });
112
+ ```
113
+
114
+ Remember to install the required peer dependency:
115
+
116
+ ```bash
117
+ npm install busboy
118
+ ```
119
+
120
+ ## Key Features Demonstrated
121
+
122
+ ### Multiple Storage Configurations
123
+
124
+ The example shows how to create different storage configurations for different file types:
125
+ - **Cover images**: Use hash naming strategy and restrict to image files
126
+ - **PDF samples**: Use timestamp naming strategy for PDFs
127
+
128
+ ### HTML Test Form
129
+
130
+ The example includes a simple HTML form for testing file uploads without needing external tools.
131
+
132
+ ### Static File Serving
133
+
134
+ The Express app serves uploaded files directly:
135
+ ```javascript
136
+ app.use('/uploads', express.static('./uploads'));
137
+ ```
138
+
139
+ This allows uploaded files to be accessed via URLs like:
140
+ - `http://localhost:3000/uploads/covers/abc123.jpg`
141
+ - `http://localhost:3000/uploads/pdfs/1234567890.pdf`
142
+
143
+ ## Testing the Example
144
+
145
+ 1. Run the server
146
+ 2. Open http://localhost:3000 in your browser
147
+ 3. Fill out the form and select files
148
+ 4. Submit to see the file upload in action
149
+ 5. Check the console for the created book data
150
+ 6. Access uploaded files via their URLs
151
+
152
+ ## Next Steps
153
+
154
+ - Add validation for file types
155
+ - Implement file deletion when records are deleted
156
+ - Add image resizing for covers
157
+ - Generate thumbnails
158
+ - Add virus scanning for uploaded files
159
+ - Implement S3 storage for production
File without changes
@@ -0,0 +1,262 @@
1
+ import express from 'express';
2
+ import { Api } from './index.js';
3
+ import { RestApiPlugin } from './rest-api-plugin.js';
4
+ import { ExpressPlugin } from './express-plugin.js';
5
+
6
+ // Create an Express app
7
+ const app = express();
8
+
9
+ // Create API instance
10
+ const api = new Api({
11
+ name: 'blog-api',
12
+ version: '1.0.0'
13
+ });
14
+
15
+ // Install plugins
16
+ api.use(RestApiPlugin, {
17
+ idProperty: 'id',
18
+ pageSize: 10,
19
+ sortableFields: ['title', 'created_at']
20
+ });
21
+
22
+ api.use(ExpressPlugin, {
23
+ basePath: '/api',
24
+ strictContentType: false, // Make testing easier
25
+ requestSizeLimit: '5mb'
26
+ });
27
+
28
+ // Define a simple articles scope
29
+ api.addScope('articles', {
30
+ schema: {
31
+ id: { type: 'id' },
32
+ title: { type: 'string', required: true, max: 200 },
33
+ body: { type: 'string', required: true },
34
+ author_id: { belongsTo: 'users', as: 'author' },
35
+ created_at: { type: 'dateTime', default: () => new Date().toISOString() }
36
+ },
37
+ relationships: {
38
+ comments: { hasMany: 'comments' },
39
+ tags: { hasMany: 'tags', through: 'article_tags' }
40
+ }
41
+ });
42
+
43
+ // Define users scope
44
+ api.addScope('users', {
45
+ schema: {
46
+ id: { type: 'id' },
47
+ name: { type: 'string', required: true },
48
+ email: { type: 'string', required: true }
49
+ },
50
+ relationships: {
51
+ articles: { hasMany: 'articles', foreignKey: 'author_id' }
52
+ }
53
+ });
54
+
55
+ // Mock storage plugin for demonstration
56
+ const MockStoragePlugin = {
57
+ name: 'mock-storage',
58
+ install({ helpers }) {
59
+ // Mock data
60
+ const articles = [
61
+ {
62
+ id: 1,
63
+ title: 'First Article',
64
+ body: 'This is the first article',
65
+ author_id: 1,
66
+ created_at: '2024-01-01T10:00:00Z'
67
+ },
68
+ {
69
+ id: 2,
70
+ title: 'Second Article',
71
+ body: 'This is the second article',
72
+ author_id: 2,
73
+ created_at: '2024-01-02T10:00:00Z'
74
+ }
75
+ ];
76
+
77
+ const users = [
78
+ { id: 1, name: 'John Doe', email: 'john@example.com' },
79
+ { id: 2, name: 'Jane Smith', email: 'jane@example.com' }
80
+ ];
81
+
82
+ // Implement storage helpers
83
+ helpers.dataExists = async function({ scopeName, id }) {
84
+ if (scopeName === 'articles') {
85
+ return articles.some(a => a.id == id);
86
+ }
87
+ if (scopeName === 'users') {
88
+ return users.some(u => u.id == id);
89
+ }
90
+ return false;
91
+ };
92
+
93
+ helpers.dataGet = async function({ scopeName, id }) {
94
+ let record;
95
+ if (scopeName === 'articles') {
96
+ record = articles.find(a => a.id == id);
97
+ } else if (scopeName === 'users') {
98
+ record = users.find(u => u.id == id);
99
+ }
100
+
101
+ if (!record) {
102
+ const { RestApiResourceError } = await import('./rest-api-errors.js');
103
+ throw new RestApiResourceError(`${scopeName} with id ${id} not found`, {
104
+ subtype: 'not_found',
105
+ resourceType: scopeName,
106
+ resourceId: id
107
+ });
108
+ }
109
+
110
+ return {
111
+ data: {
112
+ type: scopeName,
113
+ id: String(record.id),
114
+ attributes: { ...record, id: undefined }
115
+ }
116
+ };
117
+ };
118
+
119
+ helpers.dataQuery = async function({ scopeName, queryParams }) {
120
+ let records = [];
121
+ if (scopeName === 'articles') {
122
+ records = [...articles];
123
+ } else if (scopeName === 'users') {
124
+ records = [...users];
125
+ }
126
+
127
+ // Simple sorting
128
+ if (queryParams.sort && queryParams.sort.length > 0) {
129
+ records.sort((a, b) => {
130
+ for (const sortField of queryParams.sort) {
131
+ const desc = sortField.startsWith('-');
132
+ const field = desc ? sortField.substring(1) : sortField;
133
+ const aVal = a[field];
134
+ const bVal = b[field];
135
+ if (aVal < bVal) return desc ? 1 : -1;
136
+ if (aVal > bVal) return desc ? -1 : 1;
137
+ }
138
+ return 0;
139
+ });
140
+ }
141
+
142
+ return {
143
+ data: records.map(record => ({
144
+ type: scopeName,
145
+ id: String(record.id),
146
+ attributes: { ...record, id: undefined }
147
+ }))
148
+ };
149
+ };
150
+
151
+ helpers.dataPost = async function({ scopeName, inputRecord }) {
152
+ const newRecord = {
153
+ ...inputRecord.data.attributes,
154
+ id: Date.now() // Simple ID generation
155
+ };
156
+
157
+ if (scopeName === 'articles') {
158
+ articles.push(newRecord);
159
+ } else if (scopeName === 'users') {
160
+ users.push(newRecord);
161
+ }
162
+
163
+ return {
164
+ data: {
165
+ type: scopeName,
166
+ id: String(newRecord.id),
167
+ attributes: { ...newRecord, id: undefined }
168
+ }
169
+ };
170
+ };
171
+
172
+ helpers.dataPatch = async function({ scopeName, id, inputRecord }) {
173
+ let record;
174
+ if (scopeName === 'articles') {
175
+ record = articles.find(a => a.id == id);
176
+ } else if (scopeName === 'users') {
177
+ record = users.find(u => u.id == id);
178
+ }
179
+
180
+ if (!record) {
181
+ const { RestApiResourceError } = await import('./rest-api-errors.js');
182
+ throw new RestApiResourceError(`${scopeName} with id ${id} not found`, {
183
+ subtype: 'not_found',
184
+ resourceType: scopeName,
185
+ resourceId: id
186
+ });
187
+ }
188
+
189
+ // Update attributes
190
+ Object.assign(record, inputRecord.data.attributes);
191
+
192
+ return {
193
+ data: {
194
+ type: scopeName,
195
+ id: String(record.id),
196
+ attributes: { ...record, id: undefined }
197
+ }
198
+ };
199
+ };
200
+
201
+ helpers.dataPut = async function({ scopeName, id, inputRecord, isCreate }) {
202
+ if (isCreate) {
203
+ return this.dataPost({ scopeName, inputRecord });
204
+ } else {
205
+ // For update, replace all attributes
206
+ let record;
207
+ if (scopeName === 'articles') {
208
+ const index = articles.findIndex(a => a.id == id);
209
+ if (index !== -1) {
210
+ articles[index] = {
211
+ ...inputRecord.data.attributes,
212
+ id: Number(id)
213
+ };
214
+ record = articles[index];
215
+ }
216
+ }
217
+
218
+ return {
219
+ data: {
220
+ type: scopeName,
221
+ id: String(record.id),
222
+ attributes: { ...record, id: undefined }
223
+ }
224
+ };
225
+ }
226
+ };
227
+
228
+ helpers.dataDelete = async function({ scopeName, id }) {
229
+ if (scopeName === 'articles') {
230
+ const index = articles.findIndex(a => a.id == id);
231
+ if (index === -1) {
232
+ const { RestApiResourceError } = await import('./rest-api-errors.js');
233
+ throw new RestApiResourceError(`${scopeName} with id ${id} not found`, {
234
+ subtype: 'not_found',
235
+ resourceType: scopeName,
236
+ resourceId: id
237
+ });
238
+ }
239
+ articles.splice(index, 1);
240
+ }
241
+ };
242
+ }
243
+ };
244
+
245
+ // Install the mock storage plugin
246
+ api.use(MockStoragePlugin);
247
+
248
+ // Mount the API routes
249
+ app.use(api.rest.express.router);
250
+
251
+ // Start the server
252
+ const PORT = process.env.PORT || 3000;
253
+ app.listen(PORT, () => {
254
+ console.log(`Server running on http://localhost:${PORT}`);
255
+ console.log('\nTry these commands:');
256
+ console.log(' curl http://localhost:3000/api/articles');
257
+ console.log(' curl http://localhost:3000/api/articles/1');
258
+ console.log(' curl http://localhost:3000/api/articles?sort=-created_at');
259
+ console.log(' curl -X POST http://localhost:3000/api/articles \\');
260
+ console.log(' -H "Content-Type: application/vnd.api+json" \\');
261
+ console.log(' -d \'{"data":{"type":"articles","attributes":{"title":"New Article","body":"Content here","author_id":1}}}\'');
262
+ });