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
|
@@ -1,456 +0,0 @@
|
|
|
1
|
-
# Organizing Resources in Express with jsonrestapi
|
|
2
|
-
|
|
3
|
-
This guide shows you how to organize your API resources in a scalable, maintainable way with minimal server setup.
|
|
4
|
-
|
|
5
|
-
## Directory Structure
|
|
6
|
-
|
|
7
|
-
```
|
|
8
|
-
project/
|
|
9
|
-
├── server.js
|
|
10
|
-
├── api/
|
|
11
|
-
│ ├── 1.0.0/
|
|
12
|
-
│ │ ├── users.js # Self-contained users resource
|
|
13
|
-
│ │ ├── products.js # Self-contained products resource
|
|
14
|
-
│ │ └── orders.js # Self-contained orders resource
|
|
15
|
-
│ └── 2.0.0/
|
|
16
|
-
│ ├── users.js # Updated users resource
|
|
17
|
-
│ ├── products.js # Updated products resource
|
|
18
|
-
│ └── orders.js # Orders resource
|
|
19
|
-
└── config/
|
|
20
|
-
└── database.js # Database configuration
|
|
21
|
-
```
|
|
22
|
-
|
|
23
|
-
## Resource File Structure
|
|
24
|
-
|
|
25
|
-
Each resource file is self-contained and handles its own setup.
|
|
26
|
-
|
|
27
|
-
### Example: api/1.0.0/users.js
|
|
28
|
-
|
|
29
|
-
```javascript
|
|
30
|
-
import { Api, Schema, MySQLPlugin, ValidationPlugin, HTTPPlugin } from 'jsonrestapi';
|
|
31
|
-
import { dbConfig } from '../../config/database.js';
|
|
32
|
-
|
|
33
|
-
// Get or create the API instance for this version
|
|
34
|
-
const api = Api.get('myapp', '1.0.0') || new Api({
|
|
35
|
-
name: 'myapp',
|
|
36
|
-
version: '1.0.0'
|
|
37
|
-
});
|
|
38
|
-
|
|
39
|
-
// Ensure plugins are loaded (safe to call multiple times)
|
|
40
|
-
api
|
|
41
|
-
.use(ValidationPlugin)
|
|
42
|
-
.use(MySQLPlugin, {
|
|
43
|
-
connections: [{
|
|
44
|
-
name: 'main',
|
|
45
|
-
config: dbConfig
|
|
46
|
-
}]
|
|
47
|
-
})
|
|
48
|
-
.use(HTTPPlugin, {
|
|
49
|
-
basePath: '/api/1.0.0'
|
|
50
|
-
});
|
|
51
|
-
|
|
52
|
-
// Define schema
|
|
53
|
-
const userSchema = new Schema({
|
|
54
|
-
id: { type: 'id' },
|
|
55
|
-
username: { type: 'string', required: true, min: 3, max: 50 },
|
|
56
|
-
email: { type: 'string', required: true, lowercase: true },
|
|
57
|
-
password: { type: 'string', required: true, min: 8 },
|
|
58
|
-
firstName: { type: 'string', max: 100 },
|
|
59
|
-
lastName: { type: 'string', max: 100 },
|
|
60
|
-
role: { type: 'string', default: 'user' },
|
|
61
|
-
active: { type: 'boolean', default: true },
|
|
62
|
-
createdAt: { type: 'timestamp', default: () => Date.now() },
|
|
63
|
-
updatedAt: { type: 'timestamp' }
|
|
64
|
-
});
|
|
65
|
-
|
|
66
|
-
// Define hooks for this resource
|
|
67
|
-
const userHooks = {
|
|
68
|
-
async afterValidate(context) {
|
|
69
|
-
const { data, method, errors } = context;
|
|
70
|
-
|
|
71
|
-
if (method === 'insert' || method === 'update') {
|
|
72
|
-
// Check for duplicate email
|
|
73
|
-
const existing = await context.api.query({
|
|
74
|
-
filter: { email: data.email }
|
|
75
|
-
}, { type: 'users' });
|
|
76
|
-
|
|
77
|
-
if (existing.meta.total > 0 && existing.results[0].id !== data.id) {
|
|
78
|
-
errors.push({
|
|
79
|
-
field: 'email',
|
|
80
|
-
message: 'Email already in use',
|
|
81
|
-
code: 'DUPLICATE_EMAIL'
|
|
82
|
-
});
|
|
83
|
-
}
|
|
84
|
-
}
|
|
85
|
-
},
|
|
86
|
-
|
|
87
|
-
async transformResult(context) {
|
|
88
|
-
const { result } = context;
|
|
89
|
-
|
|
90
|
-
// Never return password field
|
|
91
|
-
if (result && result.attributes) {
|
|
92
|
-
delete result.attributes.password;
|
|
93
|
-
}
|
|
94
|
-
|
|
95
|
-
// Add computed fields
|
|
96
|
-
if (result && result.attributes) {
|
|
97
|
-
result.attributes.fullName =
|
|
98
|
-
`${result.attributes.firstName || ''} ${result.attributes.lastName || ''}`.trim();
|
|
99
|
-
}
|
|
100
|
-
},
|
|
101
|
-
|
|
102
|
-
async beforeInsert(context) {
|
|
103
|
-
const { data } = context;
|
|
104
|
-
|
|
105
|
-
// Hash password before storing
|
|
106
|
-
if (data.password) {
|
|
107
|
-
const bcrypt = await import('bcrypt');
|
|
108
|
-
data.password = await bcrypt.hash(data.password, 10);
|
|
109
|
-
}
|
|
110
|
-
|
|
111
|
-
// Set timestamps
|
|
112
|
-
data.createdAt = Date.now();
|
|
113
|
-
data.updatedAt = Date.now();
|
|
114
|
-
}
|
|
115
|
-
};
|
|
116
|
-
|
|
117
|
-
// Add the resource with schema and hooks
|
|
118
|
-
api.addResource('users', userSchema, userHooks);
|
|
119
|
-
|
|
120
|
-
// Export for server to mount
|
|
121
|
-
export default api;
|
|
122
|
-
```
|
|
123
|
-
|
|
124
|
-
### Even Cleaner Approach with Shared Configuration
|
|
125
|
-
|
|
126
|
-
The library includes a helper that manages shared API instances per version:
|
|
127
|
-
|
|
128
|
-
```javascript
|
|
129
|
-
// api/config.js - Shared configuration
|
|
130
|
-
import { dbConfig } from '../config/database.js';
|
|
131
|
-
|
|
132
|
-
export const apiConfig = {
|
|
133
|
-
name: 'myapp',
|
|
134
|
-
mysql: {
|
|
135
|
-
connections: [{
|
|
136
|
-
name: 'main',
|
|
137
|
-
config: dbConfig
|
|
138
|
-
}]
|
|
139
|
-
},
|
|
140
|
-
http: {
|
|
141
|
-
// Additional HTTP options
|
|
142
|
-
}
|
|
143
|
-
};
|
|
144
|
-
```
|
|
145
|
-
|
|
146
|
-
Then use the helper in your resources:
|
|
147
|
-
|
|
148
|
-
```javascript
|
|
149
|
-
// api/1.0.0/users.js
|
|
150
|
-
import { Schema, defineResource } from 'jsonrestapi/resource-helper.js';
|
|
151
|
-
import { apiConfig } from '../config.js';
|
|
152
|
-
|
|
153
|
-
export default defineResource('1.0.0', 'users', {
|
|
154
|
-
api: apiConfig, // Shared configuration
|
|
155
|
-
schema: new Schema({
|
|
156
|
-
id: { type: 'id' },
|
|
157
|
-
username: { type: 'string', required: true, min: 3, max: 50 },
|
|
158
|
-
email: { type: 'string', required: true, lowercase: true },
|
|
159
|
-
password: { type: 'string', required: true, min: 8 },
|
|
160
|
-
firstName: { type: 'string', max: 100 },
|
|
161
|
-
lastName: { type: 'string', max: 100 },
|
|
162
|
-
role: { type: 'string', default: 'user' },
|
|
163
|
-
active: { type: 'boolean', default: true },
|
|
164
|
-
createdAt: { type: 'timestamp', default: () => Date.now() },
|
|
165
|
-
updatedAt: { type: 'timestamp' }
|
|
166
|
-
}),
|
|
167
|
-
|
|
168
|
-
storage: {
|
|
169
|
-
table: 'users',
|
|
170
|
-
searchFields: ['username', 'email', 'firstName', 'lastName']
|
|
171
|
-
},
|
|
172
|
-
|
|
173
|
-
hooks: {
|
|
174
|
-
async afterValidate(context) {
|
|
175
|
-
const { data, method, errors } = context;
|
|
176
|
-
|
|
177
|
-
if (method === 'insert' || method === 'update') {
|
|
178
|
-
const existing = await context.api.query({
|
|
179
|
-
filter: { email: data.email }
|
|
180
|
-
}, { type: 'users' });
|
|
181
|
-
|
|
182
|
-
if (existing.meta.total > 0 && existing.results[0].id !== data.id) {
|
|
183
|
-
errors.push({
|
|
184
|
-
field: 'email',
|
|
185
|
-
message: 'Email already in use',
|
|
186
|
-
code: 'DUPLICATE_EMAIL'
|
|
187
|
-
});
|
|
188
|
-
}
|
|
189
|
-
}
|
|
190
|
-
},
|
|
191
|
-
|
|
192
|
-
async transformResult(context) {
|
|
193
|
-
const { result } = context;
|
|
194
|
-
|
|
195
|
-
if (result?.attributes) {
|
|
196
|
-
delete result.attributes.password;
|
|
197
|
-
result.attributes.fullName =
|
|
198
|
-
`${result.attributes.firstName || ''} ${result.attributes.lastName || ''}`.trim();
|
|
199
|
-
}
|
|
200
|
-
},
|
|
201
|
-
|
|
202
|
-
async beforeInsert(context) {
|
|
203
|
-
const bcrypt = await import('bcrypt');
|
|
204
|
-
context.data.password = await bcrypt.hash(context.data.password, 10);
|
|
205
|
-
context.data.createdAt = Date.now();
|
|
206
|
-
context.data.updatedAt = Date.now();
|
|
207
|
-
}
|
|
208
|
-
}
|
|
209
|
-
});
|
|
210
|
-
```
|
|
211
|
-
|
|
212
|
-
## Minimal Server Setup
|
|
213
|
-
|
|
214
|
-
### Option 1: Manual Loading (Most Control)
|
|
215
|
-
|
|
216
|
-
```javascript
|
|
217
|
-
// server.js
|
|
218
|
-
import express from 'express';
|
|
219
|
-
import session from 'express-session';
|
|
220
|
-
|
|
221
|
-
const app = express();
|
|
222
|
-
|
|
223
|
-
// Middleware
|
|
224
|
-
app.use(express.json());
|
|
225
|
-
app.use(session({
|
|
226
|
-
secret: process.env.SESSION_SECRET,
|
|
227
|
-
resave: false,
|
|
228
|
-
saveUninitialized: false
|
|
229
|
-
}));
|
|
230
|
-
|
|
231
|
-
// Load all resources - ONE LINE PER RESOURCE!
|
|
232
|
-
const apis = [
|
|
233
|
-
await import('./api/1.0.0/users.js'),
|
|
234
|
-
await import('./api/1.0.0/products.js'),
|
|
235
|
-
await import('./api/1.0.0/orders.js'),
|
|
236
|
-
await import('./api/2.0.0/users.js'),
|
|
237
|
-
await import('./api/2.0.0/products.js'),
|
|
238
|
-
await import('./api/2.0.0/orders.js'),
|
|
239
|
-
];
|
|
240
|
-
|
|
241
|
-
// Mount all APIs - that's it!
|
|
242
|
-
apis.forEach(module => module.default.mount(app));
|
|
243
|
-
|
|
244
|
-
// Start server
|
|
245
|
-
app.listen(3000, () => {
|
|
246
|
-
console.log('Server running on http://localhost:3000');
|
|
247
|
-
});
|
|
248
|
-
```
|
|
249
|
-
|
|
250
|
-
### Option 2: Auto-Loading (Like the Old Library)
|
|
251
|
-
|
|
252
|
-
Create a helper function:
|
|
253
|
-
|
|
254
|
-
```javascript
|
|
255
|
-
// load-resources.js
|
|
256
|
-
import { readdir } from 'fs/promises';
|
|
257
|
-
import { join } from 'path';
|
|
258
|
-
|
|
259
|
-
export async function loadResourcesFromPath(apiPath, app) {
|
|
260
|
-
const versions = await readdir(apiPath);
|
|
261
|
-
|
|
262
|
-
for (const version of versions) {
|
|
263
|
-
const versionPath = join(apiPath, version);
|
|
264
|
-
const files = await readdir(versionPath);
|
|
265
|
-
|
|
266
|
-
for (const file of files) {
|
|
267
|
-
if (file.endsWith('.js')) {
|
|
268
|
-
const module = await import(join(versionPath, file));
|
|
269
|
-
if (module.default?.mount) {
|
|
270
|
-
module.default.mount(app);
|
|
271
|
-
}
|
|
272
|
-
}
|
|
273
|
-
}
|
|
274
|
-
}
|
|
275
|
-
}
|
|
276
|
-
```
|
|
277
|
-
|
|
278
|
-
Then in your server:
|
|
279
|
-
|
|
280
|
-
```javascript
|
|
281
|
-
// server.js
|
|
282
|
-
import express from 'express';
|
|
283
|
-
import { fileURLToPath } from 'url';
|
|
284
|
-
import { dirname, join } from 'path';
|
|
285
|
-
import { loadResourcesFromPath } from './load-resources.js';
|
|
286
|
-
|
|
287
|
-
const __dirname = dirname(fileURLToPath(import.meta.url));
|
|
288
|
-
const app = express();
|
|
289
|
-
|
|
290
|
-
app.use(express.json());
|
|
291
|
-
|
|
292
|
-
// ONE LINE to load everything!
|
|
293
|
-
await loadResourcesFromPath(join(__dirname, 'api'), app);
|
|
294
|
-
|
|
295
|
-
app.listen(3000, () => {
|
|
296
|
-
console.log('Server running on http://localhost:3000');
|
|
297
|
-
});
|
|
298
|
-
```
|
|
299
|
-
|
|
300
|
-
## Programmatic Access Between Resources
|
|
301
|
-
|
|
302
|
-
Resources that share an API instance can interact with each other using the new intuitive API:
|
|
303
|
-
|
|
304
|
-
```javascript
|
|
305
|
-
// api/1.0.0/orders.js
|
|
306
|
-
import { Api, Schema } from 'jsonrestapi';
|
|
307
|
-
|
|
308
|
-
const api = Api.get('myapp', '1.0.0');
|
|
309
|
-
|
|
310
|
-
// Define orders schema
|
|
311
|
-
const orderSchema = new Schema({
|
|
312
|
-
id: { type: 'id' },
|
|
313
|
-
userId: { type: 'id', required: true },
|
|
314
|
-
productId: { type: 'id', required: true },
|
|
315
|
-
quantity: { type: 'number', min: 1 },
|
|
316
|
-
status: { type: 'string', default: 'pending' }
|
|
317
|
-
});
|
|
318
|
-
|
|
319
|
-
const orderHooks = {
|
|
320
|
-
async afterInsert(context) {
|
|
321
|
-
const { result } = context;
|
|
322
|
-
const order = result.data;
|
|
323
|
-
|
|
324
|
-
// Access other resources using the intuitive API
|
|
325
|
-
const user = await api.resources.users.get(order.attributes.userId);
|
|
326
|
-
const product = await api.resources.products.get(order.attributes.productId);
|
|
327
|
-
|
|
328
|
-
// Send email notification
|
|
329
|
-
await sendOrderConfirmation(user.data.attributes.email, {
|
|
330
|
-
order,
|
|
331
|
-
product: product.data.attributes
|
|
332
|
-
});
|
|
333
|
-
},
|
|
334
|
-
|
|
335
|
-
async transformResult(context) {
|
|
336
|
-
const { result } = context;
|
|
337
|
-
|
|
338
|
-
if (result?.data) {
|
|
339
|
-
// Enrich order with user and product data
|
|
340
|
-
const [user, product] = await Promise.all([
|
|
341
|
-
api.resources.users.get(result.data.attributes.userId),
|
|
342
|
-
api.resources.products.get(result.data.attributes.productId)
|
|
343
|
-
]);
|
|
344
|
-
|
|
345
|
-
result.data.relationships = {
|
|
346
|
-
user: { data: { type: 'users', id: user.data.id } },
|
|
347
|
-
product: { data: { type: 'products', id: product.data.id } }
|
|
348
|
-
};
|
|
349
|
-
|
|
350
|
-
result.included = [
|
|
351
|
-
user.data,
|
|
352
|
-
product.data
|
|
353
|
-
];
|
|
354
|
-
}
|
|
355
|
-
}
|
|
356
|
-
};
|
|
357
|
-
|
|
358
|
-
api.addResource('orders', orderSchema, orderHooks);
|
|
359
|
-
|
|
360
|
-
export default api;
|
|
361
|
-
```
|
|
362
|
-
|
|
363
|
-
## Adding Authentication/Permissions
|
|
364
|
-
|
|
365
|
-
You can add middleware to your resources:
|
|
366
|
-
|
|
367
|
-
```javascript
|
|
368
|
-
// api/1.0.0/secure-resource.js
|
|
369
|
-
import { Schema } from 'jsonrestapi';
|
|
370
|
-
import { createResource } from '../resource-helper.js';
|
|
371
|
-
|
|
372
|
-
const api = createResource('1.0.0', 'secrets', {
|
|
373
|
-
schema: new Schema({
|
|
374
|
-
id: { type: 'id' },
|
|
375
|
-
content: { type: 'string', required: true },
|
|
376
|
-
level: { type: 'number', default: 1 }
|
|
377
|
-
})
|
|
378
|
-
});
|
|
379
|
-
|
|
380
|
-
// Add authentication middleware
|
|
381
|
-
api.useMiddleware((req, res, next) => {
|
|
382
|
-
if (!req.session?.userId) {
|
|
383
|
-
return res.status(401).json({
|
|
384
|
-
error: 'Authentication required'
|
|
385
|
-
});
|
|
386
|
-
}
|
|
387
|
-
next();
|
|
388
|
-
});
|
|
389
|
-
|
|
390
|
-
// Or use hooks for fine-grained control
|
|
391
|
-
api.hook('beforeOperation', async (context) => {
|
|
392
|
-
if (context.options.type !== 'secrets') return;
|
|
393
|
-
|
|
394
|
-
const { method, request } = context;
|
|
395
|
-
|
|
396
|
-
if (!request.session?.userId) {
|
|
397
|
-
throw Object.assign(new Error('Authentication required'), {
|
|
398
|
-
status: 401
|
|
399
|
-
});
|
|
400
|
-
}
|
|
401
|
-
|
|
402
|
-
if (method === 'delete' && request.session.role !== 'admin') {
|
|
403
|
-
throw Object.assign(new Error('Admin access required'), {
|
|
404
|
-
status: 403
|
|
405
|
-
});
|
|
406
|
-
}
|
|
407
|
-
});
|
|
408
|
-
|
|
409
|
-
export default api;
|
|
410
|
-
```
|
|
411
|
-
|
|
412
|
-
## Why Not Use createApi()?
|
|
413
|
-
|
|
414
|
-
You might wonder why we don't use `createApi()` in each resource file. Here's why:
|
|
415
|
-
|
|
416
|
-
1. **Shared Instance**: All resources in a version should share the same API instance, not create separate ones
|
|
417
|
-
2. **Single Configuration**: Database connections, plugins, etc. should be configured once per version
|
|
418
|
-
3. **Resource Interaction**: Resources can query each other when they share an API instance
|
|
419
|
-
4. **Memory Efficiency**: One connection pool, one router, one set of hooks per version
|
|
420
|
-
|
|
421
|
-
The `defineResource()` helper gives us the best of both worlds:
|
|
422
|
-
- Simple like `createApi()`
|
|
423
|
-
- But properly manages shared instances
|
|
424
|
-
- Allows resource-specific configuration
|
|
425
|
-
|
|
426
|
-
## Benefits
|
|
427
|
-
|
|
428
|
-
1. **Minimal Server Code**: Server setup is just a few lines
|
|
429
|
-
2. **Self-Contained Resources**: Each resource file has everything it needs
|
|
430
|
-
3. **No Repetition**: The helper function eliminates boilerplate
|
|
431
|
-
4. **Easy to Add Resources**: Just create a file and it works
|
|
432
|
-
5. **Version Isolation**: Each version's resources are independent
|
|
433
|
-
6. **Familiar Pattern**: Similar to the old library's approach but cleaner
|
|
434
|
-
|
|
435
|
-
## Quick Start
|
|
436
|
-
|
|
437
|
-
1. Create your resource file:
|
|
438
|
-
```javascript
|
|
439
|
-
// api/1.0.0/my-resource.js
|
|
440
|
-
import { Schema } from 'jsonrestapi';
|
|
441
|
-
import { createResource } from '../resource-helper.js';
|
|
442
|
-
|
|
443
|
-
export default createResource('1.0.0', 'my-resource', {
|
|
444
|
-
schema: new Schema({
|
|
445
|
-
id: { type: 'id' },
|
|
446
|
-
name: { type: 'string', required: true }
|
|
447
|
-
})
|
|
448
|
-
});
|
|
449
|
-
```
|
|
450
|
-
|
|
451
|
-
2. That's it! The resource is automatically available at:
|
|
452
|
-
- GET /api/1.0.0/my-resource
|
|
453
|
-
- GET /api/1.0.0/my-resource/:id
|
|
454
|
-
- POST /api/1.0.0/my-resource
|
|
455
|
-
- PATCH /api/1.0.0/my-resource/:id
|
|
456
|
-
- DELETE /api/1.0.0/my-resource/:id
|
|
@@ -1,168 +0,0 @@
|
|
|
1
|
-
import express from 'express';
|
|
2
|
-
import { Api, Schema, createApi } from './index.js';
|
|
3
|
-
|
|
4
|
-
// Example: Automatic API Versioning
|
|
5
|
-
// The library handles all version negotiation automatically!
|
|
6
|
-
|
|
7
|
-
// Version 1.0.0 - Original User API
|
|
8
|
-
const userApiV1 = createApi({
|
|
9
|
-
name: 'users',
|
|
10
|
-
version: '1.0.0',
|
|
11
|
-
storage: 'memory'
|
|
12
|
-
});
|
|
13
|
-
|
|
14
|
-
const userSchemaV1 = new Schema({
|
|
15
|
-
id: { type: 'id' },
|
|
16
|
-
name: { type: 'string', required: true },
|
|
17
|
-
email: { type: 'string', required: true }
|
|
18
|
-
});
|
|
19
|
-
|
|
20
|
-
userApiV1.addResource('users', userSchemaV1);
|
|
21
|
-
|
|
22
|
-
// Version 2.0.0 - Added phone number
|
|
23
|
-
const userApiV2 = createApi({
|
|
24
|
-
name: 'users',
|
|
25
|
-
version: '2.0.0',
|
|
26
|
-
storage: 'memory'
|
|
27
|
-
});
|
|
28
|
-
|
|
29
|
-
const userSchemaV2 = new Schema({
|
|
30
|
-
id: { type: 'id' },
|
|
31
|
-
name: { type: 'string', required: true },
|
|
32
|
-
email: { type: 'string', required: true },
|
|
33
|
-
phone: { type: 'string' }, // New field!
|
|
34
|
-
active: { type: 'boolean', default: true } // New field!
|
|
35
|
-
});
|
|
36
|
-
|
|
37
|
-
userApiV2.addResource('users', userSchemaV2);
|
|
38
|
-
|
|
39
|
-
// Version 2.1.0 - Added address
|
|
40
|
-
const userApiV2_1 = createApi({
|
|
41
|
-
name: 'users',
|
|
42
|
-
version: '2.1.0',
|
|
43
|
-
storage: 'memory'
|
|
44
|
-
});
|
|
45
|
-
|
|
46
|
-
const userSchemaV2_1 = new Schema({
|
|
47
|
-
id: { type: 'id' },
|
|
48
|
-
name: { type: 'string', required: true },
|
|
49
|
-
email: { type: 'string', required: true },
|
|
50
|
-
phone: { type: 'string' },
|
|
51
|
-
active: { type: 'boolean', default: true },
|
|
52
|
-
address: { type: 'object' } // New field!
|
|
53
|
-
});
|
|
54
|
-
|
|
55
|
-
userApiV2_1.addResource('users', userSchemaV2_1);
|
|
56
|
-
|
|
57
|
-
// USAGE - The library handles version selection automatically!
|
|
58
|
-
|
|
59
|
-
// 1. Get specific version
|
|
60
|
-
const api1 = Api.get('users', '1.0.0');
|
|
61
|
-
console.log('Got v1.0.0:', api1.options.version);
|
|
62
|
-
|
|
63
|
-
// 2. Get latest version
|
|
64
|
-
const apiLatest = Api.get('users', 'latest');
|
|
65
|
-
console.log('Got latest:', apiLatest.options.version); // 2.1.0
|
|
66
|
-
|
|
67
|
-
// 3. Get minimum version (2.0.0 or higher)
|
|
68
|
-
const api2Plus = Api.get('users', '2.0.0');
|
|
69
|
-
console.log('Got 2.0.0+:', api2Plus.options.version); // 2.1.0 (highest compatible)
|
|
70
|
-
|
|
71
|
-
// 4. From within an API, access other APIs automatically
|
|
72
|
-
const ordersApi = createApi({
|
|
73
|
-
name: 'orders',
|
|
74
|
-
version: '1.0.0',
|
|
75
|
-
storage: 'memory'
|
|
76
|
-
});
|
|
77
|
-
|
|
78
|
-
// The orders API can access the users API automatically
|
|
79
|
-
ordersApi.hook('afterInsert', async (context) => {
|
|
80
|
-
if (context.options.type === 'orders') {
|
|
81
|
-
// This automatically gets a compatible users API!
|
|
82
|
-
const usersApi = ordersApi.apis.users;
|
|
83
|
-
|
|
84
|
-
if (usersApi) {
|
|
85
|
-
const user = await usersApi.get(context.data.userId, { type: 'users' });
|
|
86
|
-
console.log('Order created for user:', user);
|
|
87
|
-
}
|
|
88
|
-
}
|
|
89
|
-
});
|
|
90
|
-
|
|
91
|
-
// 5. Express setup with automatic version routing
|
|
92
|
-
const app = express();
|
|
93
|
-
|
|
94
|
-
// Mount all versions - the library handles routing!
|
|
95
|
-
userApiV1.mount(app, '/api'); // Available at /api/1.0.0/users
|
|
96
|
-
userApiV2.mount(app, '/api'); // Available at /api/2.0.0/users
|
|
97
|
-
userApiV2_1.mount(app, '/api'); // Available at /api/2.1.0/users
|
|
98
|
-
|
|
99
|
-
// Version negotiation middleware
|
|
100
|
-
app.use('/api/users/*', (req, res, next) => {
|
|
101
|
-
// Get requested version from header or query
|
|
102
|
-
const requestedVersion =
|
|
103
|
-
req.headers['api-version'] ||
|
|
104
|
-
req.query.v ||
|
|
105
|
-
'latest';
|
|
106
|
-
|
|
107
|
-
// Library finds the right version automatically
|
|
108
|
-
const api = Api.get('users', requestedVersion);
|
|
109
|
-
|
|
110
|
-
if (!api) {
|
|
111
|
-
return res.status(404).json({
|
|
112
|
-
errors: [{
|
|
113
|
-
status: '404',
|
|
114
|
-
title: 'Version Not Found',
|
|
115
|
-
detail: `No compatible version for users API ${requestedVersion}`
|
|
116
|
-
}]
|
|
117
|
-
});
|
|
118
|
-
}
|
|
119
|
-
|
|
120
|
-
// Forward to the correct API version
|
|
121
|
-
api.router(req, res, next);
|
|
122
|
-
});
|
|
123
|
-
|
|
124
|
-
// 6. Programmatic usage with automatic version selection
|
|
125
|
-
async function createUser(data, minVersion = '1.0.0') {
|
|
126
|
-
// Get a compatible API automatically
|
|
127
|
-
const api = Api.get('users', minVersion);
|
|
128
|
-
|
|
129
|
-
if (!api) {
|
|
130
|
-
throw new Error(`No users API version >= ${minVersion}`);
|
|
131
|
-
}
|
|
132
|
-
|
|
133
|
-
return api.insert(data, { type: 'users' });
|
|
134
|
-
}
|
|
135
|
-
|
|
136
|
-
// This works with v1.0.0 data
|
|
137
|
-
await createUser({
|
|
138
|
-
name: 'John Doe',
|
|
139
|
-
email: 'john@example.com'
|
|
140
|
-
}, '1.0.0');
|
|
141
|
-
|
|
142
|
-
// This requires v2.0.0+ for phone field
|
|
143
|
-
await createUser({
|
|
144
|
-
name: 'Jane Doe',
|
|
145
|
-
email: 'jane@example.com',
|
|
146
|
-
phone: '+1234567890'
|
|
147
|
-
}, '2.0.0');
|
|
148
|
-
|
|
149
|
-
// Client examples:
|
|
150
|
-
|
|
151
|
-
// Via header
|
|
152
|
-
fetch('/api/users', {
|
|
153
|
-
headers: {
|
|
154
|
-
'API-Version': '2.0.0' // Get v2.0.0 or higher
|
|
155
|
-
}
|
|
156
|
-
});
|
|
157
|
-
|
|
158
|
-
// Via query parameter
|
|
159
|
-
fetch('/api/users?v=1.0.0'); // Get exactly v1.0.0
|
|
160
|
-
|
|
161
|
-
// Via path (automatic routing)
|
|
162
|
-
fetch('/api/2.1.0/users'); // Get exactly v2.1.0
|
|
163
|
-
|
|
164
|
-
// Show all registered APIs
|
|
165
|
-
console.log('Registered APIs:', Api.getRegistry());
|
|
166
|
-
// Output: { users: ['2.1.0', '2.0.0', '1.0.0'] }
|
|
167
|
-
|
|
168
|
-
export { userApiV1, userApiV2, userApiV2_1 };
|