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.
- package/.claude/settings.local.json +16 -0
- package/LICENSE +674 -0
- package/README.md +655 -309
- package/RELATIONSHIPS.md +1013 -0
- package/TODO_rest-api-plugin-extras.js +142 -0
- package/connectors_ideas/express-plugin.js +120 -0
- package/connectors_ideas/fastify.js +219 -0
- package/connectors_ideas/http-plugin.js +138 -0
- package/connectors_ideas/koa-plugin.js +113 -0
- package/docs/fileUploads.md +722 -0
- package/docs/mini-guides/file-uploads-complete.md +159 -0
- package/examples/.gitkeep +0 -0
- package/examples/express-example.js +262 -0
- package/examples/express-plugin.js +366 -0
- package/examples/file-upload-example.js +205 -0
- package/examples/filename-strategies.js +140 -0
- package/examples/knex-basic.js +66 -0
- package/index.js +31 -56
- package/lib/payload-validators.js +685 -0
- package/lib/rest-api-errors.js +58 -0
- package/package.json +41 -14
- package/payloadValdators.js +686 -0
- package/plugins/core/connectors/express-plugin.js +449 -0
- package/plugins/core/connectors/http-plugin.js +450 -0
- package/plugins/core/file-handling-plugin.js +338 -0
- package/plugins/core/lib/busboy-detector.js +167 -0
- package/plugins/core/lib/formidable-detector.js +130 -0
- package/plugins/core/rest-api-knex-plugin.js +200 -0
- package/plugins/core/rest-api-plugin.js +1419 -0
- package/plugins/storage/local-storage.js +218 -0
- package/plugins/storage/s3-storage.js +125 -0
- package/test-examples/package-lock.json +906 -0
- package/test-examples/package.json +15 -0
- package/test-examples/test-basic-api.js +11 -0
- package/test-examples/test-books-resource.js +116 -0
- package/test-examples/test-complete-file-upload.js +140 -0
- package/test-examples/test-debug.js +28 -0
- package/test-examples/test-express-plugin.js +177 -0
- package/test-examples/test-express-simple.js +28 -0
- package/test-examples/test-express-with-import.js +71 -0
- package/test-examples/test-file-upload.js +132 -0
- package/test-examples/test-http-plugin.js +159 -0
- package/test-examples/test-programmatic-api.js +188 -0
- package/test-examples/test-rest-api-plugin.js +13 -0
- package/TODO.todo +0 -113
- package/api.js +0 -628
- package/docs/API.md +0 -1306
- package/docs/FIXES_APPLIED.md +0 -59
- package/docs/GUIDES.md +0 -1792
- package/docs/RESOURCES_IN_EXPRESS.md +0 -456
- package/docs/example-versioning.js +0 -168
- package/docs/example.js +0 -195
- package/plugins/API guide.md +0 -364
- package/plugins/api-registry.js +0 -142
- package/plugins/api-versioning.md +0 -135
- package/plugins/http.js +0 -273
- package/plugins/logging.js +0 -255
- package/plugins/memory.js +0 -182
- package/plugins/mysql.js +0 -753
- package/plugins/openapi.js +0 -401
- package/plugins/positioning.js +0 -220
- package/plugins/security.js +0 -261
- package/plugins/validation.js +0 -173
- package/plugins/versioning.js +0 -288
- package/resource-helper.js +0 -79
- package/schema.js +0 -481
- 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
|
+
});
|