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
package/docs/GUIDES.md DELETED
@@ -1,1792 +0,0 @@
1
- # JSON REST API - Complete Guide
2
-
3
- Welcome to the JSON REST APIs! This guide will take you from zero to hero, one step at a time. We'll build a complete task management system to learn every feature of this library.
4
-
5
- ## Table of Contents
6
-
7
- 1. [Quick Start Guide](#quick-start-guide)
8
- 2. [Core Concepts Tutorial](#core-concepts-tutorial)
9
- 3. [Plugin Tutorials](#plugin-tutorials)
10
- 4. [Advanced Features](#advanced-features)
11
-
12
- ---
13
-
14
- # Quick Start Guide
15
-
16
- Let's build a simple task management API in 10 minutes!
17
-
18
- ## Step 1: Installation
19
-
20
- ```bash
21
- npm init -y
22
- npm install express mysql2
23
- npm install jsonrestapi
24
- ```
25
-
26
- ## Step 2: Create Your First API
27
-
28
- Create a file called `server.js`:
29
-
30
- ```javascript
31
- import express from 'express';
32
- import { createApi, Schema } from 'jsonrestapi';
33
-
34
- // Create Express app
35
- const app = express();
36
-
37
- // Create your API with a name and version
38
- const api = createApi({
39
- name: 'taskmanager',
40
- version: '1.0.0',
41
- storage: 'memory', // We'll use memory for now
42
- http: { basePath: '/api' } // This automatically adds HTTPPlugin!
43
- });
44
-
45
- // Define a schema for tasks
46
- const taskSchema = new Schema({
47
- id: { type: 'id' },
48
- title: { type: 'string', required: true, min: 1, max: 200 },
49
- description: { type: 'string', max: 1000 },
50
- completed: { type: 'boolean', default: false },
51
- priority: { type: 'string', default: 'medium' },
52
- createdAt: { type: 'timestamp', default: () => Date.now() }
53
- });
54
-
55
- // Register the schema
56
- api.addResource('tasks', taskSchema);
57
-
58
- // Mount the API on Express
59
- api.mount(app);
60
-
61
- // Start the server
62
- app.listen(3000, () => {
63
- console.log('🚀 API running at http://localhost:3000');
64
- console.log('📝 Try: GET http://localhost:3000/api/1.0.0/tasks');
65
- });
66
- ```
67
-
68
- ### 💡 Behind the Scenes: What createApi Does
69
-
70
- The `createApi()` helper is a convenience function that automatically configures plugins for you. Here's what the above code would look like WITHOUT the helper:
71
-
72
- ```javascript
73
- import express from 'express';
74
- import {
75
- Api,
76
- Schema,
77
- ValidationPlugin,
78
- MemoryPlugin,
79
- HTTPPlugin
80
- } from 'jsonrestapi';
81
-
82
- const app = express();
83
-
84
- // Manual approach - full control over plugins
85
- const api = new Api({
86
- name: 'taskmanager',
87
- version: '1.0.0'
88
- });
89
-
90
- // Manually add each plugin
91
- api.use(ValidationPlugin); // Always needed for schemas
92
- api.use(MemoryPlugin); // For in-memory storage
93
- api.use(HTTPPlugin, { basePath: '/api' }); // For REST endpoints
94
-
95
- // Rest is the same...
96
- ```
97
-
98
- The `createApi()` helper automatically adds plugins based on your options:
99
- - `storage: 'memory'` → adds MemoryPlugin
100
- - `storage: 'mysql'` → adds MySQLPlugin
101
- - `http: { ... }` → adds HTTPPlugin
102
- - `positioning: { ... }` → adds PositioningPlugin
103
- - `versioning: { ... }` → adds VersioningPlugin
104
- - ValidationPlugin is always added (unless `validation: false`)
105
-
106
- Use `createApi()` for quick setup, or use `Api` directly when you need full control!
107
-
108
- ## Step 3: Test Your API
109
-
110
- Run your server:
111
- ```bash
112
- node server.js
113
- ```
114
-
115
- Now you can:
116
-
117
- ```bash
118
- # Create a task
119
- curl -X POST http://localhost:3000/api/1.0.0/tasks \
120
- -H "Content-Type: application/json" \
121
- -d '{"data": {"type": "tasks", "attributes": {"title": "Learn JSON REST API"}}}'
122
-
123
- # List all tasks
124
- curl http://localhost:3000/api/1.0.0/tasks
125
-
126
- # Get a specific task
127
- curl http://localhost:3000/api/1.0.0/tasks/1
128
-
129
- # Update a task
130
- curl -X PATCH http://localhost:3000/api/1.0.0/tasks/1 \
131
- -H "Content-Type: application/json" \
132
- -d '{"data": {"attributes": {"completed": true}}}'
133
-
134
- # Delete a task
135
- curl -X DELETE http://localhost:3000/api/1.0.0/tasks/1
136
- ```
137
-
138
- ## Step 4: Add MySQL Storage
139
-
140
- Let's make it persistent! Update your `server.js`:
141
-
142
- ```javascript
143
- import { createApi, Schema, MySQLPlugin } from 'jsonrestapi';
144
-
145
- // Create API with MySQL
146
- const api = createApi({
147
- name: 'taskmanager',
148
- version: '1.0.0',
149
- storage: 'mysql',
150
- mysql: {
151
- connection: {
152
- host: 'localhost',
153
- user: 'root',
154
- password: 'password',
155
- database: 'taskmanager'
156
- }
157
- },
158
- http: { basePath: '/api' }
159
- });
160
-
161
- // Same schema as before
162
- const taskSchema = new Schema({
163
- id: { type: 'id' },
164
- title: { type: 'string', required: true, min: 1, max: 200 },
165
- description: { type: 'string', max: 1000 },
166
- completed: { type: 'boolean', default: false },
167
- priority: { type: 'string', default: 'medium' },
168
- createdAt: { type: 'timestamp', default: () => Date.now() }
169
- });
170
-
171
- api.addResource('tasks', taskSchema);
172
-
173
- // Magic! Sync the database automatically
174
- await api.syncSchema(taskSchema, 'tasks');
175
-
176
- api.mount(app);
177
- ```
178
-
179
- ## Step 5: Add Users with Relationships
180
-
181
- Let's add a users table:
182
-
183
- ```javascript
184
- // User schema
185
- const userSchema = new Schema({
186
- id: { type: 'id' },
187
- username: { type: 'string', required: true, min: 3, max: 50 },
188
- email: { type: 'string', required: true, lowercase: true },
189
- passwordHash: { type: 'string', required: true },
190
- createdAt: { type: 'timestamp', default: () => Date.now() }
191
- });
192
-
193
- // Update task schema to include user
194
- const taskSchema = new Schema({
195
- id: { type: 'id' },
196
- userId: { type: 'id', required: true }, // Link to user
197
- title: { type: 'string', required: true, min: 1, max: 200 },
198
- description: { type: 'string', max: 1000 },
199
- completed: { type: 'boolean', default: false },
200
- priority: { type: 'string', default: 'medium' },
201
- createdAt: { type: 'timestamp', default: () => Date.now() }
202
- });
203
-
204
- // Register both schemas
205
- api.addResource('users', userSchema);
206
- api.addResource('tasks', taskSchema);
207
-
208
- // Sync both tables
209
- await api.syncSchema(userSchema, 'users');
210
- await api.syncSchema(taskSchema, 'tasks');
211
- ```
212
-
213
- Congratulations! You now have a working API with two related tables! 🎉
214
-
215
- ---
216
-
217
- # Core Concepts Tutorial
218
-
219
- ## Understanding Schemas
220
-
221
- Schemas are the heart of your API. They define what your data looks like and how it's validated.
222
-
223
- ### Basic Types
224
-
225
- ```javascript
226
- const productSchema = new Schema({
227
- // Numeric types
228
- id: { type: 'id' }, // Auto-incrementing ID
229
- price: { type: 'number', min: 0 }, // Positive numbers only
230
- stock: { type: 'number', default: 0 }, // Default value
231
-
232
- // String types
233
- name: { type: 'string', required: true },
234
- sku: { type: 'string', uppercase: true }, // Auto-uppercase
235
- description: { type: 'string', max: 1000 },
236
-
237
- // Boolean
238
- active: { type: 'boolean', default: true },
239
- featured: { type: 'boolean' },
240
-
241
- // Dates
242
- createdAt: { type: 'timestamp' },
243
- launchDate: { type: 'date' }, // YYYY-MM-DD
244
- lastModified: { type: 'dateTime' }, // Full datetime
245
-
246
- // Complex types
247
- tags: { type: 'array', default: [] },
248
- metadata: { type: 'object' },
249
-
250
- // Special types
251
- image: { type: 'blob' },
252
- config: { type: 'serialize' } // Handles circular references
253
- });
254
- ```
255
-
256
- ### Advanced Validation
257
-
258
- ```javascript
259
- const advancedSchema = new Schema({
260
- // Custom validation
261
- email: {
262
- type: 'string',
263
- required: true,
264
- lowercase: true,
265
- validator: (value) => {
266
- if (!value.includes('@')) return 'Invalid email';
267
- }
268
- },
269
-
270
- // Conditional defaults
271
- code: {
272
- type: 'string',
273
- default: () => 'PROD-' + Date.now()
274
- },
275
-
276
- // Trimming and length
277
- title: {
278
- type: 'string',
279
- trim: 100, // Max 100 chars
280
- notEmpty: true // Can't be empty string
281
- },
282
-
283
- // Null handling
284
- notes: {
285
- type: 'string',
286
- canBeNull: true, // Allows null
287
- emptyAsNull: true // "" becomes null
288
- }
289
- });
290
- ```
291
-
292
- ## Understanding the Api Class
293
-
294
- The Api class is your central hub:
295
-
296
- ```javascript
297
- // Create with options
298
- const api = new Api({
299
- name: 'myapp',
300
- version: '1.0.0',
301
- idProperty: 'id' // Default is 'id'
302
- });
303
-
304
- // Add plugins
305
- api
306
- .use(ValidationPlugin)
307
- .use(MySQLPlugin, { connection: dbConfig })
308
- .use(HTTPPlugin, { basePath: '/api' });
309
-
310
- // Register schemas
311
- api.addResource('products', productSchema);
312
-
313
- // Use programmatically - OLD WAY (still works)
314
- const product = await api.insert({
315
- name: 'Awesome Widget',
316
- price: 29.99
317
- }, { type: 'products' });
318
-
319
- // NEW WAY - Much more intuitive!
320
- const product = await api.resources.products.create({
321
- name: 'Awesome Widget',
322
- price: 29.99
323
- });
324
-
325
- // All operations available
326
- const found = await api.resources.products.get(123);
327
- const updated = await api.resources.products.update(123, { price: 39.99 });
328
- const results = await api.resources.products.query({ filter: { active: true } });
329
- await api.resources.products.delete(123);
330
- ```
331
-
332
- ## Understanding Hooks
333
-
334
- Hooks let you inject custom logic at any point:
335
-
336
- ```javascript
337
- // Before validation
338
- api.hook('beforeValidate', async (context) => {
339
- // context.method: 'insert', 'update', 'delete', 'get', 'query'
340
- // context.data: The data being processed
341
- // context.options: Options passed to the method
342
-
343
- if (context.method === 'insert') {
344
- context.data.createdBy = context.options.userId;
345
- }
346
- });
347
-
348
- // After validation
349
- api.hook('afterValidate', async (context) => {
350
- // context.errors: Array of validation errors
351
-
352
- // Add custom validation
353
- if (context.data.price > 1000 && !context.data.approved) {
354
- context.errors.push({
355
- field: 'price',
356
- message: 'High-value items need approval',
357
- code: 'NEEDS_APPROVAL'
358
- });
359
- }
360
- });
361
-
362
- // Transform results
363
- api.hook('transformResult', async (context) => {
364
- // context.result: The data being returned
365
-
366
- if (context.result && context.options.type === 'products') {
367
- // Add computed field
368
- context.result.displayPrice = `$${context.result.price.toFixed(2)}`;
369
- }
370
- });
371
- ```
372
-
373
- ---
374
-
375
- # Plugin Tutorials
376
-
377
- ## ValidationPlugin Tutorial
378
-
379
- The ValidationPlugin is automatically included and provides schema validation.
380
-
381
- ### Step 1: Basic Validation
382
-
383
- ```javascript
384
- const api = createApi({ storage: 'memory' });
385
-
386
- // Define strict schema
387
- const userSchema = new Schema({
388
- username: {
389
- type: 'string',
390
- required: true,
391
- min: 3,
392
- max: 20,
393
- pattern: /^[a-zA-Z0-9_]+$/ // Alphanumeric + underscore only
394
- },
395
- age: {
396
- type: 'number',
397
- min: 13,
398
- max: 120
399
- }
400
- });
401
-
402
- api.addResource('users', userSchema);
403
-
404
- // This will fail validation
405
- try {
406
- await api.insert({
407
- username: 'a', // Too short!
408
- age: 200 // Too old!
409
- }, { type: 'users' });
410
- } catch (error) {
411
- console.log(error.errors);
412
- // [
413
- // { field: 'username', message: 'username is too short' },
414
- // { field: 'age', message: 'age is too high' }
415
- // ]
416
- }
417
- ```
418
-
419
- ### Step 2: Custom Validators
420
-
421
- ```javascript
422
- const schema = new Schema({
423
- password: {
424
- type: 'string',
425
- required: true,
426
- validator: (value) => {
427
- if (value.length < 8) return 'Password must be at least 8 characters';
428
- if (!/[A-Z]/.test(value)) return 'Password must contain uppercase';
429
- if (!/[0-9]/.test(value)) return 'Password must contain number';
430
- }
431
- },
432
- confirmPassword: {
433
- type: 'string',
434
- required: true
435
- }
436
- });
437
-
438
- // Cross-field validation
439
- api.hook('afterValidate', async (context) => {
440
- if (context.data.password !== context.data.confirmPassword) {
441
- context.errors.push({
442
- field: 'confirmPassword',
443
- message: 'Passwords do not match'
444
- });
445
- }
446
- });
447
- ```
448
-
449
- ### Step 3: Conditional Validation
450
-
451
- ```javascript
452
- const orderSchema = new Schema({
453
- type: { type: 'string', required: true },
454
- amount: { type: 'number', required: true },
455
- giftMessage: { type: 'string' }
456
- });
457
-
458
- api.hook('afterValidate', async (context) => {
459
- const { data } = context;
460
-
461
- // Gift orders require a message
462
- if (data.type === 'gift' && !data.giftMessage) {
463
- context.errors.push({
464
- field: 'giftMessage',
465
- message: 'Gift orders require a message'
466
- });
467
- }
468
- });
469
- ```
470
-
471
- ### 🚀 Pro Tip: Cleaner Resource Definition
472
-
473
- Instead of adding hooks separately, you can define them with the resource:
474
-
475
- ```javascript
476
- const userSchema = new Schema({
477
- email: { type: 'string', required: true, lowercase: true },
478
- password: { type: 'string', required: true },
479
- role: { type: 'string', default: 'user' }
480
- });
481
-
482
- const userHooks = {
483
- async afterValidate(context) {
484
- const { data, method, errors } = context;
485
-
486
- // Check email uniqueness
487
- if (method === 'insert' || method === 'update') {
488
- const existing = await context.api.query({
489
- filter: { email: data.email }
490
- }, { type: 'users' });
491
-
492
- if (existing.meta.total > 0) {
493
- errors.push({
494
- field: 'email',
495
- message: 'Email already exists'
496
- });
497
- }
498
- }
499
- },
500
-
501
- async beforeInsert(context) {
502
- // Hash password
503
- const bcrypt = await import('bcrypt');
504
- context.data.password = await bcrypt.hash(context.data.password, 10);
505
- },
506
-
507
- async transformResult(context) {
508
- // Never return password
509
- if (context.result?.attributes) {
510
- delete context.result.attributes.password;
511
- }
512
- }
513
- };
514
-
515
- // Add resource with schema AND hooks together!
516
- api.addResource('users', userSchema, userHooks);
517
- ```
518
-
519
- This approach keeps all resource logic in one place, making it easier to maintain!
520
-
521
- ## Modular Resources (Advanced)
522
-
523
- When your API grows, you'll want to organize resources into separate files. Here's the challenge and solution:
524
-
525
- ### The Challenge: Why Not Use createApi() Everywhere?
526
-
527
- You might think to do this in each resource file:
528
- ```javascript
529
- // ❌ DON'T DO THIS - api/1.0.0/users.js
530
- const api = createApi({ ... }); // Creates NEW api instance
531
- api.addResource('users', schema);
532
- export default api;
533
-
534
- // ❌ DON'T DO THIS - api/1.0.0/products.js
535
- const api = createApi({ ... }); // Creates ANOTHER api instance!
536
- api.addResource('products', schema);
537
- export default api;
538
- ```
539
-
540
- **Problems:**
541
- - Each file creates a separate API instance
542
- - Multiple database connections (waste of resources)
543
- - Resources can't interact with each other
544
- - Configuration is duplicated everywhere
545
-
546
- ### The Solution: Shared API Instances
547
-
548
- Resources in the same version should SHARE one API instance:
549
-
550
- ```javascript
551
- // ✅ DO THIS - api/1.0.0/users.js
552
- import { Api, Schema } from 'jsonrestapi';
553
-
554
- // Get existing or create shared instance for v1.0.0
555
- const api = Api.get('myapp', '1.0.0') || new Api({
556
- name: 'myapp',
557
- version: '1.0.0'
558
- });
559
-
560
- // Configure plugins only if not already done
561
- if (!api.hasPlugin(HTTPPlugin)) {
562
- api.use(HTTPPlugin, { basePath: '/api/1.0.0' });
563
- }
564
-
565
- // Define schema and hooks together
566
- const userSchema = new Schema({ ... });
567
- const userHooks = {
568
- async beforeInsert(context) {
569
- // Hash passwords, etc.
570
- }
571
- };
572
-
573
- // Add resource with hooks
574
- api.addResource('users', userSchema, userHooks);
575
-
576
- export default api;
577
- ```
578
-
579
- ### Even Better: Using defineResource Helper
580
-
581
- The library includes a helper that manages this for you:
582
-
583
- ```javascript
584
- // api/config.js - Shared configuration
585
- export const apiConfig = {
586
- name: 'myapp',
587
- mysql: {
588
- connections: [{
589
- name: 'main',
590
- config: { host: 'localhost', ... }
591
- }]
592
- }
593
- };
594
-
595
- // api/1.0.0/users.js - Clean resource file
596
- import { Schema, defineResource } from 'jsonrestapi/resource-helper.js';
597
- import { apiConfig } from '../config.js';
598
-
599
- export default defineResource('1.0.0', 'users', {
600
- api: apiConfig, // Shared config
601
-
602
- schema: new Schema({
603
- id: { type: 'id' },
604
- name: { type: 'string', required: true },
605
- email: { type: 'string', required: true }
606
- }),
607
-
608
- hooks: {
609
- async beforeInsert(context) {
610
- // Your logic here
611
- }
612
- }
613
- });
614
- ```
615
-
616
- ### Loading Resources in Your Server
617
-
618
- ```javascript
619
- // server.js - Super clean!
620
- import express from 'express';
621
- const app = express();
622
-
623
- // Option 1: Manual loading
624
- const apis = [
625
- await import('./api/1.0.0/users.js'),
626
- await import('./api/1.0.0/products.js'),
627
- ];
628
- apis.forEach(module => module.default.mount(app));
629
-
630
- // Option 2: Auto-loading (like the old library!)
631
- import { loadResourcesFromPath } from './load-resources.js';
632
- await loadResourcesFromPath(join(__dirname, 'api'), app);
633
-
634
- app.listen(3000);
635
- ```
636
-
637
- ### Key Concepts to Remember
638
-
639
- 1. **createApi()** = Creates a NEW api instance (use for simple, single-file APIs)
640
- 2. **Api.get() || new Api()** = Gets or creates a SHARED instance (use in resource files)
641
- 3. **defineResource()** = Helper that does the sharing for you (recommended)
642
- 4. **One API instance per version** = All resources in v1.0.0 share the same API
643
- 5. **api.resources.{name}** = NEW intuitive way to access resources!
644
-
645
- This way your server stays clean (one line per resource), resources are modular, and everything shares the same database connections and configuration!
646
-
647
- ## Programmatic API Usage
648
-
649
- The new programmatic API makes working with resources intuitive and chainable:
650
-
651
- ### Basic CRUD Operations
652
-
653
- ```javascript
654
- // After registering a resource
655
- api.addResource('users', userSchema);
656
-
657
- // Create
658
- const user = await api.resources.users.create({
659
- username: 'johndoe',
660
- email: 'john@example.com'
661
- });
662
-
663
- // Read
664
- const found = await api.resources.users.get(user.data.id);
665
-
666
- // Update
667
- const updated = await api.resources.users.update(user.data.id, {
668
- email: 'newemail@example.com'
669
- });
670
-
671
- // Delete
672
- await api.resources.users.delete(user.data.id);
673
-
674
- // Query
675
- const results = await api.resources.users.query({
676
- filter: { active: true },
677
- sort: '-createdAt',
678
- page: { size: 10, number: 1 }
679
- });
680
- ```
681
-
682
- ### Batch Operations
683
-
684
- ```javascript
685
- // Create multiple users at once
686
- const users = await api.resources.users.batch.create([
687
- { username: 'user1', email: 'user1@example.com' },
688
- { username: 'user2', email: 'user2@example.com' },
689
- { username: 'user3', email: 'user3@example.com' }
690
- ]);
691
-
692
- // Update multiple users
693
- await api.resources.users.batch.update([
694
- { id: 1, data: { active: false } },
695
- { id: 2, data: { active: false } }
696
- ]);
697
-
698
- // Delete multiple users
699
- await api.resources.users.batch.delete([1, 2, 3]);
700
- ```
701
-
702
- ### Accessing Different Versions
703
-
704
- ```javascript
705
- // Default: uses the current API version
706
- const user = await api.resources.users.get(123);
707
-
708
- // Access a specific version
709
- const userV1 = await api.resources.users.version('1.0.0').get(123);
710
- const userV2 = await api.resources.users.version('2.0.0').get(123);
711
-
712
- // This is especially useful when migrating between versions
713
- const oldData = await api.resources.users.version('1.0.0').query();
714
- for (const item of oldData.data) {
715
- await api.resources.users.version('2.0.0').create(migrateData(item));
716
- }
717
- ```
718
-
719
- ### Direct Access to Schema and Hooks
720
-
721
- ```javascript
722
- // Get the schema for a resource
723
- const schema = api.resources.users.schema;
724
-
725
- // Get the hooks for a resource
726
- const hooks = api.resources.users.hooks;
727
-
728
- // Check if a resource exists
729
- if ('users' in api.resources) {
730
- console.log('Users resource is available');
731
- }
732
-
733
- // List all resources
734
- const allResources = Object.keys(api.resources);
735
- console.log('Available resources:', allResources);
736
- ```
737
-
738
- ### Alternative Syntax
739
-
740
- If you prefer function calls over property access:
741
-
742
- ```javascript
743
- // Using resource() method
744
- const user = await api.resource('users').get(123);
745
- const posts = await api.resource('posts').query();
746
-
747
- // This is useful when resource names are dynamic
748
- const resourceName = 'users';
749
- const data = await api.resource(resourceName).get(123);
750
- ```
751
-
752
- ## MySQLPlugin Tutorial
753
-
754
- ### Step 1: Basic Setup
755
-
756
- ```javascript
757
- import { Api, MySQLPlugin, Schema } from 'jsonrestapi';
758
-
759
- const api = new Api();
760
- api.use(MySQLPlugin, {
761
- connection: {
762
- host: 'localhost',
763
- user: 'root',
764
- password: 'password',
765
- database: 'myapp',
766
- waitForConnections: true,
767
- connectionLimit: 10,
768
- queueLimit: 0
769
- }
770
- });
771
- ```
772
-
773
- ### Step 2: Schema Synchronization
774
-
775
- ```javascript
776
- // Define your schema
777
- const postSchema = new Schema({
778
- id: { type: 'id' },
779
- title: { type: 'string', required: true, max: 200 },
780
- content: { type: 'string', text: true }, // Creates TEXT column
781
- views: { type: 'number', default: 0 },
782
- published: { type: 'boolean', default: false },
783
- publishDate: { type: 'dateTime' },
784
- tags: { type: 'array' }, // Stored as JSON
785
- metadata: { type: 'object' } // Stored as JSON
786
- });
787
-
788
- // Sync creates/updates the table automatically!
789
- await api.syncSchema(postSchema, 'posts');
790
- ```
791
-
792
- ### Step 3: Advanced MySQL Features
793
-
794
- ```javascript
795
- // Multiple connections
796
- api.use(MySQLPlugin, {
797
- connections: [
798
- {
799
- name: 'primary',
800
- config: { host: 'primary.db', ... }
801
- },
802
- {
803
- name: 'replica',
804
- config: { host: 'replica.db', ... }
805
- }
806
- ]
807
- });
808
-
809
- // Use specific connection - OLD WAY
810
- const data = await api.query({}, {
811
- type: 'posts',
812
- connection: 'replica' // Read from replica
813
- });
814
-
815
- // NEW WAY - Much cleaner!
816
- const data = await api.resources.posts.query({}, {
817
- connection: 'replica' // Read from replica
818
- });
819
-
820
- // Direct SQL access when needed
821
- const { pool } = api.getConnection('primary');
822
- const [rows] = await pool.query(
823
- 'SELECT * FROM posts WHERE MATCH(title, content) AGAINST(?)',
824
- ['search term']
825
- );
826
- ```
827
-
828
- ### Step 4: Transactions
829
-
830
- ```javascript
831
- const { pool } = api.getConnection();
832
- const connection = await pool.getConnection();
833
-
834
- try {
835
- await connection.beginTransaction();
836
-
837
- // Create author
838
- const author = await api.insert({
839
- name: 'John Doe',
840
- email: 'john@example.com'
841
- }, {
842
- type: 'authors',
843
- connection // Use transaction connection
844
- });
845
-
846
- // Create post
847
- const post = await api.insert({
848
- authorId: author.data.id,
849
- title: 'My First Post',
850
- content: 'Hello World!'
851
- }, {
852
- type: 'posts',
853
- connection
854
- });
855
-
856
- await connection.commit();
857
- console.log('Transaction successful!');
858
-
859
- } catch (error) {
860
- await connection.rollback();
861
- console.error('Transaction failed:', error);
862
- } finally {
863
- connection.release();
864
- }
865
- ```
866
-
867
- ## HTTPPlugin Tutorial
868
-
869
- ### Step 1: Basic HTTP Setup
870
-
871
- ```javascript
872
- import express from 'express';
873
-
874
- const app = express();
875
- const api = createApi({
876
- name: 'blog',
877
- version: '1.0.0',
878
- storage: 'memory',
879
- http: {
880
- basePath: '/api',
881
- app // Pass Express app directly
882
- }
883
- });
884
-
885
- // That's it! Your API is now available at:
886
- // GET /api/1.0.0/posts
887
- // GET /api/1.0.0/posts/:id
888
- // POST /api/1.0.0/posts
889
- // PATCH /api/1.0.0/posts/:id
890
- // DELETE /api/1.0.0/posts/:id
891
- ```
892
-
893
- ### Step 2: Query Parameters
894
-
895
- ```javascript
896
- // Filtering
897
- GET /api/1.0.0/posts?filter[published]=true&filter[category]=tech
898
-
899
- // Sorting (-prefix for descending)
900
- GET /api/1.0.0/posts?sort=-createdAt,title
901
-
902
- // Pagination
903
- GET /api/1.0.0/posts?page[size]=10&page[number]=2
904
-
905
- // Combined
906
- GET /api/1.0.0/posts?filter[published]=true&sort=-views&page[size]=5
907
- ```
908
-
909
- ### Step 3: JSON:API Format
910
-
911
- The HTTP plugin follows JSON:API specification:
912
-
913
- ```javascript
914
- // Request body for POST/PATCH
915
- {
916
- "data": {
917
- "type": "posts",
918
- "attributes": {
919
- "title": "Hello World",
920
- "content": "My first post"
921
- }
922
- }
923
- }
924
-
925
- // Response format
926
- {
927
- "data": {
928
- "id": "123",
929
- "type": "posts",
930
- "attributes": {
931
- "title": "Hello World",
932
- "content": "My first post",
933
- "createdAt": 1639094400000
934
- }
935
- }
936
- }
937
-
938
- // Collection response
939
- {
940
- "data": [
941
- { "id": "1", "type": "posts", "attributes": {...} },
942
- { "id": "2", "type": "posts", "attributes": {...} }
943
- ],
944
- "meta": {
945
- "total": 50,
946
- "pageSize": 10,
947
- "pageNumber": 1,
948
- "totalPages": 5
949
- },
950
- "links": {
951
- "self": "/api/1.0.0/posts?page[number]=1",
952
- "next": "/api/1.0.0/posts?page[number]=2",
953
- "last": "/api/1.0.0/posts?page[number]=5"
954
- }
955
- }
956
- ```
957
-
958
- ### Step 4: Custom Middleware
959
-
960
- ```javascript
961
- // Add middleware to all routes
962
- api.useMiddleware((req, res, next) => {
963
- console.log(`${req.method} ${req.path}`);
964
- next();
965
- });
966
-
967
- // Add middleware to specific routes
968
- api.useRouteMiddleware('post', '/:type', async (req, res, next) => {
969
- // Check API key for POST requests
970
- if (!req.headers['x-api-key']) {
971
- return res.status(401).json({
972
- errors: [{ status: '401', title: 'API key required' }]
973
- });
974
- }
975
- next();
976
- });
977
- ```
978
-
979
- ## PositioningPlugin Tutorial
980
-
981
- Perfect for drag-and-drop interfaces!
982
-
983
- ### Step 1: Enable Positioning
984
-
985
- ```javascript
986
- import { PositioningPlugin } from 'jsonrestapi';
987
-
988
- const api = createApi({ storage: 'memory' });
989
- api.use(PositioningPlugin);
990
-
991
- const todoSchema = new Schema({
992
- id: { type: 'id' },
993
- title: { type: 'string', required: true },
994
- position: { type: 'number' } // Required field!
995
- });
996
-
997
- api.addResource('todos', todoSchema);
998
- ```
999
-
1000
- ### Step 2: Insert with Position
1001
-
1002
- ```javascript
1003
- // Insert at the end (default)
1004
- const todo1 = await api.insert({
1005
- title: 'First task'
1006
- }, {
1007
- type: 'todos',
1008
- positioning: { enabled: true }
1009
- });
1010
- // position: 1
1011
-
1012
- // Insert at the end explicitly
1013
- const todo2 = await api.insert({
1014
- title: 'Second task',
1015
- beforeId: null // null = end
1016
- }, {
1017
- type: 'todos',
1018
- positioning: { enabled: true }
1019
- });
1020
- // position: 2
1021
-
1022
- // Insert before another item
1023
- const todo3 = await api.insert({
1024
- title: 'Urgent task',
1025
- beforeId: todo1.data.id // Insert before first task
1026
- }, {
1027
- type: 'todos',
1028
- positioning: { enabled: true }
1029
- });
1030
- // position: 1 (others shift up)
1031
- ```
1032
-
1033
- ### Step 3: Reposition Existing Items
1034
-
1035
- ```javascript
1036
- // Move to end
1037
- await api.reposition('todos', todoId, null);
1038
-
1039
- // Move before another item
1040
- await api.reposition('todos', todoId, beforeTodoId);
1041
-
1042
- // Update with positioning
1043
- await api.update(todoId, {
1044
- title: 'Updated title',
1045
- beforeId: anotherId // Also reposition
1046
- }, {
1047
- type: 'todos',
1048
- positioning: { enabled: true }
1049
- });
1050
- ```
1051
-
1052
- ### Step 4: Position Filters (Groups)
1053
-
1054
- ```javascript
1055
- // Configure positioning within groups
1056
- api.use(PositioningPlugin, {
1057
- positionField: 'position',
1058
- positionFilters: ['projectId', 'status'] // Separate positions per project/status
1059
- });
1060
-
1061
- // Now positions are scoped
1062
- await api.insert({
1063
- title: 'Project A Task',
1064
- projectId: 1,
1065
- status: 'active',
1066
- beforeId: null
1067
- }, {
1068
- type: 'todos',
1069
- positioning: { enabled: true }
1070
- });
1071
- // This gets position 1 within project=1, status=active
1072
- ```
1073
-
1074
- ### Step 5: Normalize Positions
1075
-
1076
- ```javascript
1077
- // Remove gaps in position numbers
1078
- await api.normalizePositions('todos', {
1079
- projectId: 1 // Normalize within this project
1080
- });
1081
-
1082
- // Get next available position
1083
- const nextPos = await api.getNextPosition('todos', {
1084
- projectId: 1,
1085
- status: 'active'
1086
- });
1087
- ```
1088
-
1089
- ## VersioningPlugin Tutorial
1090
-
1091
- ### Part 1: API Versioning
1092
-
1093
- ```javascript
1094
- // Version 1.0.0 of your API
1095
- const apiV1 = createApi({
1096
- name: 'blog',
1097
- version: '1.0.0',
1098
- storage: 'memory'
1099
- });
1100
-
1101
- const postSchemaV1 = new Schema({
1102
- id: { type: 'id' },
1103
- title: { type: 'string', required: true },
1104
- content: { type: 'string', required: true }
1105
- });
1106
-
1107
- apiV1.addResource('posts', postSchemaV1);
1108
-
1109
- // Version 2.0.0 - Added author field
1110
- const apiV2 = createApi({
1111
- name: 'blog',
1112
- version: '2.0.0',
1113
- storage: 'memory'
1114
- });
1115
-
1116
- const postSchemaV2 = new Schema({
1117
- id: { type: 'id' },
1118
- title: { type: 'string', required: true },
1119
- content: { type: 'string', required: true },
1120
- author: { type: 'string', required: true }, // NEW!
1121
- tags: { type: 'array', default: [] } // NEW!
1122
- });
1123
-
1124
- apiV2.addResource('posts', postSchemaV2);
1125
- ```
1126
-
1127
- ### Part 2: Version Selection
1128
-
1129
- ```javascript
1130
- // Get specific version
1131
- const v1 = Api.get('blog', '1.0.0');
1132
-
1133
- // Get latest
1134
- const latest = Api.get('blog', 'latest');
1135
-
1136
- // Get minimum version
1137
- const v2plus = Api.get('blog', '2.0.0'); // Gets 2.0.0 or higher
1138
-
1139
- // Use in your app
1140
- async function createPost(data) {
1141
- // Auto-select version based on data
1142
- const hasAuthor = 'author' in data;
1143
- const api = Api.get('blog', hasAuthor ? '2.0.0' : '1.0.0');
1144
-
1145
- return api.insert(data, { type: 'posts' });
1146
- }
1147
- ```
1148
-
1149
- ### Part 3: Resource Versioning
1150
-
1151
- ```javascript
1152
- import { VersioningPlugin } from 'jsonrestapi';
1153
-
1154
- const api = createApi({ storage: 'mysql' });
1155
- api.use(VersioningPlugin, {
1156
- trackHistory: true,
1157
- optimisticLocking: true,
1158
- versionField: 'version',
1159
- lastModifiedField: 'lastModified',
1160
- modifiedByField: 'modifiedBy'
1161
- });
1162
-
1163
- // Automatic version tracking
1164
- const post = await api.insert({
1165
- title: 'Original Title',
1166
- content: 'Original content'
1167
- }, {
1168
- type: 'posts',
1169
- userId: 'user123'
1170
- });
1171
- // version: 1, modifiedBy: 'user123'
1172
-
1173
- // Update increments version
1174
- const updated = await api.update(post.data.id, {
1175
- title: 'Updated Title'
1176
- }, {
1177
- type: 'posts',
1178
- userId: 'user456'
1179
- });
1180
- // version: 2, modifiedBy: 'user456'
1181
- ```
1182
-
1183
- ### Part 4: Version History
1184
-
1185
- ```javascript
1186
- // Get full history
1187
- const history = await api.getVersionHistory('posts', postId);
1188
- // Returns all versions with timestamps
1189
-
1190
- // Restore old version
1191
- await api.restoreVersion('posts', postId, 1);
1192
- // Post is now back to version 1 content (as version 3)
1193
-
1194
- // Compare versions
1195
- const diff = await api.diffVersions('posts', postId, 1, 2);
1196
- console.log(diff);
1197
- // {
1198
- // version1: 1,
1199
- // version2: 2,
1200
- // changes: [
1201
- // { field: 'title', oldValue: 'Original Title', newValue: 'Updated Title' }
1202
- // ]
1203
- // }
1204
- ```
1205
-
1206
- ### Part 5: Optimistic Locking
1207
-
1208
- ```javascript
1209
- // Prevent concurrent updates
1210
- const post = await api.get(postId, { type: 'posts' });
1211
-
1212
- try {
1213
- await api.update(postId, {
1214
- title: 'New Title',
1215
- version: post.data.attributes.version // Include current version
1216
- }, { type: 'posts' });
1217
- } catch (error) {
1218
- if (error.code === 'VERSION_CONFLICT') {
1219
- console.log('Someone else updated this post!');
1220
- // Reload and try again
1221
- }
1222
- }
1223
- ```
1224
-
1225
- ## SecurityPlugin Tutorial
1226
-
1227
- ### Step 1: Basic Security
1228
-
1229
- ```javascript
1230
- import { SecurityPlugin } from 'jsonrestapi';
1231
-
1232
- api.use(SecurityPlugin, {
1233
- // Rate limiting
1234
- rateLimit: {
1235
- windowMs: 15 * 60 * 1000, // 15 minutes
1236
- max: 100, // 100 requests per window
1237
- message: 'Too many requests'
1238
- },
1239
-
1240
- // CORS
1241
- cors: {
1242
- origin: ['https://app.example.com', 'http://localhost:3000'],
1243
- credentials: true
1244
- },
1245
-
1246
- // Authentication
1247
- authentication: {
1248
- type: 'bearer', // or 'apikey', 'basic'
1249
- required: true,
1250
- header: 'Authorization'
1251
- }
1252
- });
1253
- ```
1254
-
1255
- ### Step 2: Token Authentication
1256
-
1257
- ```javascript
1258
- api.use(SecurityPlugin, {
1259
- authentication: {
1260
- type: 'bearer',
1261
- required: true
1262
- },
1263
- verifyToken: async (token, context) => {
1264
- // Verify JWT token
1265
- try {
1266
- const decoded = jwt.verify(token, process.env.JWT_SECRET);
1267
- return {
1268
- id: decoded.userId,
1269
- email: decoded.email,
1270
- roles: decoded.roles
1271
- };
1272
- } catch (error) {
1273
- throw new Error('Invalid token');
1274
- }
1275
- }
1276
- });
1277
-
1278
- // Token is now available in hooks
1279
- api.hook('beforeInsert', async (context) => {
1280
- context.data.createdBy = context.options.user?.id;
1281
- });
1282
- ```
1283
-
1284
- ### Step 3: API Key Authentication
1285
-
1286
- ```javascript
1287
- api.use(SecurityPlugin, {
1288
- authentication: {
1289
- type: 'apikey',
1290
- header: 'X-API-Key',
1291
- queryParam: 'apikey' // Also allow ?apikey=xxx
1292
- },
1293
- verifyToken: async (apiKey) => {
1294
- // Look up API key in database
1295
- const keyData = await findApiKey(apiKey);
1296
- if (!keyData) throw new Error('Invalid API key');
1297
-
1298
- return {
1299
- id: keyData.userId,
1300
- permissions: keyData.permissions
1301
- };
1302
- }
1303
- });
1304
- ```
1305
-
1306
- ### Step 4: Public/Private Routes
1307
-
1308
- ```javascript
1309
- api.use(SecurityPlugin, {
1310
- publicRead: true, // Allow GET without auth
1311
- authentication: {
1312
- type: 'bearer',
1313
- required: true
1314
- }
1315
- });
1316
-
1317
- // Or fine-grained control
1318
- api.hook('beforeValidate', async (context) => {
1319
- const isPublic =
1320
- context.options.type === 'posts' &&
1321
- context.method === 'query';
1322
-
1323
- if (!isPublic && !context.options.authenticated) {
1324
- context.errors.push({
1325
- field: null,
1326
- message: 'Authentication required',
1327
- code: 'AUTH_REQUIRED'
1328
- });
1329
- }
1330
- });
1331
- ```
1332
-
1333
- ### Step 5: Security Headers
1334
-
1335
- The SecurityPlugin automatically adds:
1336
- - `X-Content-Type-Options: nosniff`
1337
- - `X-Frame-Options: DENY`
1338
- - `X-XSS-Protection: 1; mode=block`
1339
- - `Strict-Transport-Security: max-age=31536000`
1340
- - `Content-Security-Policy` with safe defaults
1341
-
1342
- ## LoggingPlugin Tutorial
1343
-
1344
- ### Step 1: Basic Logging
1345
-
1346
- ```javascript
1347
- import { LoggingPlugin } from 'jsonrestapi';
1348
-
1349
- api.use(LoggingPlugin, {
1350
- level: 'info', // error, warn, info, debug
1351
- format: 'json', // or 'pretty'
1352
- includeRequest: true,
1353
- includeResponse: true,
1354
- includeTiming: true
1355
- });
1356
-
1357
- // Now all operations are logged!
1358
- ```
1359
-
1360
- ### Step 2: Custom Logger
1361
-
1362
- ```javascript
1363
- import winston from 'winston';
1364
-
1365
- const logger = winston.createLogger({
1366
- level: 'info',
1367
- format: winston.format.json(),
1368
- transports: [
1369
- new winston.transports.File({ filename: 'error.log', level: 'error' }),
1370
- new winston.transports.File({ filename: 'combined.log' })
1371
- ]
1372
- });
1373
-
1374
- api.use(LoggingPlugin, {
1375
- logger, // Use Winston instead of console
1376
- sensitiveFields: ['password', 'creditCard', 'ssn']
1377
- });
1378
- ```
1379
-
1380
- ### Step 3: Audit Logging
1381
-
1382
- ```javascript
1383
- api.use(LoggingPlugin, {
1384
- auditLog: true, // Log all data changes
1385
- level: 'info'
1386
- });
1387
-
1388
- // Creates detailed audit trail
1389
- // INFO: Resource created {
1390
- // audit: true,
1391
- // operation: 'create',
1392
- // type: 'users',
1393
- // id: '123',
1394
- // userId: 'admin',
1395
- // changes: { name: 'John', email: 'john@example.com' }
1396
- // }
1397
- ```
1398
-
1399
- ### Step 4: Manual Logging
1400
-
1401
- ```javascript
1402
- // Use the logger directly
1403
- api.log.info('Processing payment', {
1404
- orderId: '12345',
1405
- amount: 99.99,
1406
- currency: 'USD'
1407
- });
1408
-
1409
- api.log.error('Payment failed', {
1410
- orderId: '12345',
1411
- error: 'Insufficient funds',
1412
- attempts: 3
1413
- });
1414
-
1415
- api.log.debug('Cache hit', {
1416
- key: 'user:123',
1417
- ttl: 3600
1418
- });
1419
- ```
1420
-
1421
- ## OpenAPIPlugin Tutorial
1422
-
1423
- ### Step 1: Generate Documentation
1424
-
1425
- ```javascript
1426
- import { OpenAPIPlugin } from 'jsonrestapi';
1427
-
1428
- api.use(OpenAPIPlugin, {
1429
- title: 'My Awesome API',
1430
- version: '1.0.0',
1431
- description: 'The best API ever created',
1432
- servers: [
1433
- { url: 'https://api.example.com', description: 'Production' },
1434
- { url: 'http://localhost:3000', description: 'Development' }
1435
- ],
1436
- contact: {
1437
- name: 'API Support',
1438
- email: 'support@example.com'
1439
- }
1440
- });
1441
-
1442
- // Documentation available at:
1443
- // GET /api/docs - Swagger UI
1444
- // GET /api/openapi.json - OpenAPI spec
1445
- ```
1446
-
1447
- ### Step 2: Enhance Schema Documentation
1448
-
1449
- ```javascript
1450
- const userSchema = new Schema({
1451
- id: {
1452
- type: 'id',
1453
- description: 'Unique user identifier'
1454
- },
1455
- email: {
1456
- type: 'string',
1457
- required: true,
1458
- description: 'User email address',
1459
- example: 'user@example.com'
1460
- },
1461
- role: {
1462
- type: 'string',
1463
- enum: ['user', 'admin', 'moderator'],
1464
- description: 'User role in the system',
1465
- default: 'user'
1466
- }
1467
- });
1468
-
1469
- // This generates rich OpenAPI documentation!
1470
- ```
1471
-
1472
- ---
1473
-
1474
- # Advanced Features
1475
-
1476
- ## Cross-API Communication
1477
-
1478
- APIs can talk to each other!
1479
-
1480
- ```javascript
1481
- // Users API
1482
- const usersApi = createApi({
1483
- name: 'users',
1484
- version: '1.0.0',
1485
- storage: 'mysql'
1486
- });
1487
-
1488
- // Orders API
1489
- const ordersApi = createApi({
1490
- name: 'orders',
1491
- version: '1.0.0',
1492
- storage: 'mysql'
1493
- });
1494
-
1495
- // Orders can access users!
1496
- ordersApi.hook('afterInsert', async (context) => {
1497
- if (context.result) {
1498
- // Automatically gets compatible users API
1499
- const usersApi = ordersApi.apis.users;
1500
-
1501
- // Get user details
1502
- const user = await usersApi.get(
1503
- context.result.userId,
1504
- { type: 'users' }
1505
- );
1506
-
1507
- // Send confirmation email
1508
- await sendEmail(user.data.attributes.email, {
1509
- subject: 'Order Confirmed',
1510
- orderId: context.result.id
1511
- });
1512
- }
1513
- });
1514
- ```
1515
-
1516
- ## Creating Custom Plugins
1517
-
1518
- Build your own plugins!
1519
-
1520
- ```javascript
1521
- const CachePlugin = {
1522
- install(api, options = {}) {
1523
- const cache = new Map();
1524
- const ttl = options.ttl || 60000; // 1 minute default
1525
-
1526
- // Cache GET requests
1527
- api.hook('beforeGet', async (context) => {
1528
- const key = `${context.options.type}:${context.id}`;
1529
- const cached = cache.get(key);
1530
-
1531
- if (cached && cached.expires > Date.now()) {
1532
- context.result = cached.data;
1533
- context.fromCache = true;
1534
- return false; // Skip further processing
1535
- }
1536
- });
1537
-
1538
- // Store in cache after GET
1539
- api.hook('afterGet', async (context) => {
1540
- if (!context.fromCache && context.result) {
1541
- const key = `${context.options.type}:${context.id}`;
1542
- cache.set(key, {
1543
- data: context.result,
1544
- expires: Date.now() + ttl
1545
- });
1546
- }
1547
- });
1548
-
1549
- // Invalidate on update/delete
1550
- api.hook('afterUpdate', async (context) => {
1551
- const key = `${context.options.type}:${context.id}`;
1552
- cache.delete(key);
1553
- });
1554
-
1555
- api.hook('afterDelete', async (context) => {
1556
- const key = `${context.options.type}:${context.id}`;
1557
- cache.delete(key);
1558
- });
1559
-
1560
- // Add cache methods
1561
- api.clearCache = () => cache.clear();
1562
- api.getCacheSize = () => cache.size;
1563
- }
1564
- };
1565
-
1566
- // Use it!
1567
- api.use(CachePlugin, { ttl: 300000 }); // 5 minutes
1568
- ```
1569
-
1570
- ## Complex Validation Scenarios
1571
-
1572
- ```javascript
1573
- // Product with complex rules
1574
- const productSchema = new Schema({
1575
- name: { type: 'string', required: true },
1576
- price: { type: 'number', required: true, min: 0 },
1577
- salePrice: { type: 'number', min: 0 },
1578
- category: { type: 'string', required: true },
1579
- stock: { type: 'number', default: 0, min: 0 },
1580
- sku: { type: 'string', required: true }
1581
- });
1582
-
1583
- // Multi-field validation
1584
- api.hook('afterValidate', async (context) => {
1585
- const { data, errors, method } = context;
1586
-
1587
- // Sale price must be less than regular price
1588
- if (data.salePrice && data.salePrice >= data.price) {
1589
- errors.push({
1590
- field: 'salePrice',
1591
- message: 'Sale price must be less than regular price'
1592
- });
1593
- }
1594
-
1595
- // SKU must be unique
1596
- if ((method === 'insert' || method === 'update') && data.sku) {
1597
- const existing = await api.query({
1598
- filter: { sku: data.sku }
1599
- }, { type: 'products' });
1600
-
1601
- const isDuplicate = method === 'insert'
1602
- ? existing.data.length > 0
1603
- : existing.data.some(p => p.id !== context.id);
1604
-
1605
- if (isDuplicate) {
1606
- errors.push({
1607
- field: 'sku',
1608
- message: 'SKU already exists'
1609
- });
1610
- }
1611
- }
1612
-
1613
- // Category-specific validation
1614
- if (data.category === 'electronics' && !data.warrantyPeriod) {
1615
- errors.push({
1616
- field: 'warrantyPeriod',
1617
- message: 'Electronics must have warranty period'
1618
- });
1619
- }
1620
- });
1621
- ```
1622
-
1623
- ## Performance Optimization
1624
-
1625
- ```javascript
1626
- // Batch operations
1627
- async function importProducts(products) {
1628
- const batchSize = 100;
1629
- const results = [];
1630
-
1631
- for (let i = 0; i < products.length; i += batchSize) {
1632
- const batch = products.slice(i, i + batchSize);
1633
-
1634
- // Process batch in parallel
1635
- const batchResults = await Promise.all(
1636
- batch.map(product =>
1637
- api.insert(product, { type: 'products' })
1638
- .catch(error => ({ error, product }))
1639
- )
1640
- );
1641
-
1642
- results.push(...batchResults);
1643
-
1644
- // Progress
1645
- console.log(`Imported ${Math.min(i + batchSize, products.length)} / ${products.length}`);
1646
- }
1647
-
1648
- return results;
1649
- }
1650
-
1651
- // Optimized queries
1652
- const results = await api.query({
1653
- filter: { category: 'electronics', active: true },
1654
- sort: '-price',
1655
- page: { size: 50 } // Don't fetch everything!
1656
- }, {
1657
- type: 'products',
1658
- fields: ['id', 'name', 'price'] // Only needed fields
1659
- });
1660
- ```
1661
-
1662
- ## Error Recovery
1663
-
1664
- ```javascript
1665
- // Retry logic
1666
- async function reliableInsert(data, options, maxRetries = 3) {
1667
- let lastError;
1668
-
1669
- for (let attempt = 1; attempt <= maxRetries; attempt++) {
1670
- try {
1671
- return await api.insert(data, options);
1672
- } catch (error) {
1673
- lastError = error;
1674
-
1675
- // Don't retry validation errors
1676
- if (error.status === 422) throw error;
1677
-
1678
- // Exponential backoff
1679
- const delay = Math.min(1000 * Math.pow(2, attempt - 1), 10000);
1680
- console.log(`Attempt ${attempt} failed, retrying in ${delay}ms...`);
1681
-
1682
- await new Promise(resolve => setTimeout(resolve, delay));
1683
- }
1684
- }
1685
-
1686
- throw lastError;
1687
- }
1688
-
1689
- // Graceful degradation
1690
- async function getProductWithFallback(id) {
1691
- try {
1692
- // Try primary database
1693
- return await api.get(id, {
1694
- type: 'products',
1695
- connection: 'primary'
1696
- });
1697
- } catch (error) {
1698
- console.warn('Primary database failed, trying cache...');
1699
-
1700
- try {
1701
- // Try cache
1702
- return await cacheApi.get(id, { type: 'products' });
1703
- } catch (cacheError) {
1704
- console.error('Cache also failed, returning degraded response');
1705
-
1706
- // Return degraded response
1707
- return {
1708
- data: {
1709
- id,
1710
- type: 'products',
1711
- attributes: {
1712
- name: 'Product temporarily unavailable',
1713
- available: false
1714
- }
1715
- }
1716
- };
1717
- }
1718
- }
1719
- }
1720
- ```
1721
-
1722
- ## Testing Your API
1723
-
1724
- ```javascript
1725
- import { createApi, Schema } from 'jsonrestapi';
1726
- import { describe, it, expect, beforeEach } from 'vitest';
1727
-
1728
- describe('User API', () => {
1729
- let api;
1730
-
1731
- beforeEach(() => {
1732
- // Fresh API for each test
1733
- api = createApi({
1734
- name: 'test',
1735
- version: '1.0.0',
1736
- storage: 'memory'
1737
- });
1738
-
1739
- const userSchema = new Schema({
1740
- id: { type: 'id' },
1741
- email: { type: 'string', required: true },
1742
- name: { type: 'string', required: true }
1743
- });
1744
-
1745
- api.addResource('users', userSchema);
1746
- });
1747
-
1748
- it('should create a user', async () => {
1749
- const user = await api.insert({
1750
- email: 'test@example.com',
1751
- name: 'Test User'
1752
- }, { type: 'users' });
1753
-
1754
- expect(user.data.attributes.email).toBe('test@example.com');
1755
- expect(user.data.id).toBeDefined();
1756
- });
1757
-
1758
- it('should validate required fields', async () => {
1759
- await expect(
1760
- api.insert({ email: 'test@example.com' }, { type: 'users' })
1761
- ).rejects.toThrow();
1762
- });
1763
-
1764
- it('should update users', async () => {
1765
- const user = await api.insert({
1766
- email: 'test@example.com',
1767
- name: 'Original Name'
1768
- }, { type: 'users' });
1769
-
1770
- const updated = await api.update(user.data.id, {
1771
- name: 'Updated Name'
1772
- }, { type: 'users' });
1773
-
1774
- expect(updated.data.attributes.name).toBe('Updated Name');
1775
- });
1776
- });
1777
- ```
1778
-
1779
- ---
1780
-
1781
- Congratulations! You've mastered the JSON REST API library! 🎉
1782
-
1783
- Remember:
1784
- - Start simple with memory storage
1785
- - Add MySQL when you need persistence
1786
- - Use plugins to add features incrementally
1787
- - Hooks are your friends for custom logic
1788
- - The library handles versioning automatically
1789
- - Always validate your data
1790
- - Security first!
1791
-
1792
- Happy coding! 🚀