json-rest-api 1.0.0 → 1.0.1

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (67) hide show
  1. package/.claude/settings.local.json +16 -0
  2. package/LICENSE +674 -0
  3. package/README.md +655 -309
  4. package/RELATIONSHIPS.md +1013 -0
  5. package/TODO_rest-api-plugin-extras.js +142 -0
  6. package/connectors_ideas/express-plugin.js +120 -0
  7. package/connectors_ideas/fastify.js +219 -0
  8. package/connectors_ideas/http-plugin.js +138 -0
  9. package/connectors_ideas/koa-plugin.js +113 -0
  10. package/docs/fileUploads.md +722 -0
  11. package/docs/mini-guides/file-uploads-complete.md +159 -0
  12. package/examples/.gitkeep +0 -0
  13. package/examples/express-example.js +262 -0
  14. package/examples/express-plugin.js +366 -0
  15. package/examples/file-upload-example.js +205 -0
  16. package/examples/filename-strategies.js +140 -0
  17. package/examples/knex-basic.js +66 -0
  18. package/index.js +31 -56
  19. package/lib/payload-validators.js +685 -0
  20. package/lib/rest-api-errors.js +58 -0
  21. package/package.json +41 -14
  22. package/payloadValdators.js +686 -0
  23. package/plugins/core/connectors/express-plugin.js +449 -0
  24. package/plugins/core/connectors/http-plugin.js +450 -0
  25. package/plugins/core/file-handling-plugin.js +338 -0
  26. package/plugins/core/lib/busboy-detector.js +167 -0
  27. package/plugins/core/lib/formidable-detector.js +130 -0
  28. package/plugins/core/rest-api-knex-plugin.js +200 -0
  29. package/plugins/core/rest-api-plugin.js +1419 -0
  30. package/plugins/storage/local-storage.js +218 -0
  31. package/plugins/storage/s3-storage.js +125 -0
  32. package/test-examples/package-lock.json +906 -0
  33. package/test-examples/package.json +15 -0
  34. package/test-examples/test-basic-api.js +11 -0
  35. package/test-examples/test-books-resource.js +116 -0
  36. package/test-examples/test-complete-file-upload.js +140 -0
  37. package/test-examples/test-debug.js +28 -0
  38. package/test-examples/test-express-plugin.js +177 -0
  39. package/test-examples/test-express-simple.js +28 -0
  40. package/test-examples/test-express-with-import.js +71 -0
  41. package/test-examples/test-file-upload.js +132 -0
  42. package/test-examples/test-http-plugin.js +159 -0
  43. package/test-examples/test-programmatic-api.js +188 -0
  44. package/test-examples/test-rest-api-plugin.js +13 -0
  45. package/TODO.todo +0 -113
  46. package/api.js +0 -628
  47. package/docs/API.md +0 -1306
  48. package/docs/FIXES_APPLIED.md +0 -59
  49. package/docs/GUIDES.md +0 -1792
  50. package/docs/RESOURCES_IN_EXPRESS.md +0 -456
  51. package/docs/example-versioning.js +0 -168
  52. package/docs/example.js +0 -195
  53. package/plugins/API guide.md +0 -364
  54. package/plugins/api-registry.js +0 -142
  55. package/plugins/api-versioning.md +0 -135
  56. package/plugins/http.js +0 -273
  57. package/plugins/logging.js +0 -255
  58. package/plugins/memory.js +0 -182
  59. package/plugins/mysql.js +0 -753
  60. package/plugins/openapi.js +0 -401
  61. package/plugins/positioning.js +0 -220
  62. package/plugins/security.js +0 -261
  63. package/plugins/validation.js +0 -173
  64. package/plugins/versioning.js +0 -288
  65. package/resource-helper.js +0 -79
  66. package/schema.js +0 -481
  67. package/test-basic.js +0 -107
@@ -0,0 +1,1013 @@
1
+ # Hooked API REST Plugin - Relationship Schema Guide
2
+
3
+ This guide provides a comprehensive explanation of how relationships work in the Hooked API REST plugin, with detailed examples showing exactly what happens at the database level.
4
+
5
+ ## Table of Contents
6
+ 1. [Core Concepts](#core-concepts)
7
+ 2. [Example Database Schema](#example-database-schema)
8
+ 3. [Schema Definitions](#schema-definitions)
9
+ 4. [Understanding belongsTo Relationships](#understanding-belongsTo-relationships)
10
+ 5. [Understanding hasMany Relationships](#understanding-hasmany-relationships)
11
+ 6. [Understanding hasOne Relationships](#understanding-hasone-relationships)
12
+ 7. [Many-to-Many Relationships Deep Dive](#many-to-many-relationships-deep-dive)
13
+ 8. [Complete API Request Examples](#complete-api-request-examples)
14
+ 9. [Storage Plugin Implementation](#storage-plugin-implementation)
15
+
16
+ ## Core Concepts
17
+
18
+ The Hooked API REST plugin follows these fundamental principles:
19
+
20
+ 1. **Schema = Table Structure**: A schema defines ONLY the columns that physically exist in that database table
21
+ 2. **Explicit Relationships**: Virtual relationships (hasMany, hasOne) are defined separately from the schema
22
+ 3. **Column-First Design**: We work with actual database column names, not abstract concepts
23
+ 4. **No Magic**: Every relationship must be explicitly configured
24
+
25
+ ### Key Terms
26
+
27
+ - **belongsTo**: A relationship where the foreign key is IN THIS table
28
+ - **hasMany**: A relationship where the foreign key is in ANOTHER table (expects multiple related records)
29
+ - **hasOne**: A relationship where the foreign key is in ANOTHER table (expects one related record)
30
+ - **through**: Specifies a join table for many-to-many relationships
31
+
32
+ ## Example Database Schema
33
+
34
+ Let's work with a blog system that has articles, authors (people), comments, and tags. Here's our complete database structure with sample data:
35
+
36
+ ### 1. people table (authors, editors, users)
37
+ ```sql
38
+ id | name | email | bio
39
+ ---|------------------|--------------------------|----------------------------------
40
+ 1 | Alice Johnson | alice@techblog.com | Senior tech writer and editor
41
+ 2 | Bob Smith | bob@techblog.com | Freelance journalist
42
+ 3 | Carol White | carol@techblog.com | Technology enthusiast
43
+ 4 | David Lee | david@techblog.com | Chief Editor
44
+ 5 | Eve Brown | eve@readers.com | Regular reader and commenter
45
+ ```
46
+
47
+ ### 2. articles table
48
+ ```sql
49
+ id | title | slug | body | published_at | author_id | editor_id | view_count
50
+ ---|----------------------------|-------------------------|-------------------------------|---------------------|-----------|-----------|------------
51
+ 1 | Understanding JavaScript | understanding-js | JavaScript is a versatile... | 2024-01-15 10:00:00 | 1 | 4 | 1250
52
+ 2 | REST API Best Practices | rest-api-practices | When designing REST APIs... | 2024-01-20 14:30:00 | 2 | 4 | 890
53
+ 3 | Database Design Patterns | database-patterns | Proper database design... | 2024-01-25 09:15:00 | 1 | NULL | 2100
54
+ 4 | Intro to GraphQL | intro-graphql | GraphQL is a query language...| NULL | 3 | 4 | 0
55
+ ```
56
+
57
+ ### 3. comments table
58
+ ```sql
59
+ id | body | article_id | user_id | created_at | parent_comment_id
60
+ ---|-----------------------------------------|------------|---------|---------------------|-------------------
61
+ 1 | Great article! Very helpful. | 1 | 5 | 2024-01-16 08:30:00 | NULL
62
+ 2 | I have a question about closures... | 1 | 3 | 2024-01-16 10:15:00 | NULL
63
+ 3 | Thanks! Let me explain closures... | 1 | 1 | 2024-01-16 11:00:00 | 2
64
+ 4 | This clarifies REST principles nicely | 2 | 5 | 2024-01-21 09:00:00 | NULL
65
+ 5 | What about GraphQL vs REST? | 2 | 3 | 2024-01-21 14:20:00 | NULL
66
+ ```
67
+
68
+ ### 4. tags table
69
+ ```sql
70
+ id | name | slug | description
71
+ ---|--------------|--------------|----------------------------------------
72
+ 1 | JavaScript | javascript | All things JavaScript
73
+ 2 | API Design | api-design | API design patterns and best practices
74
+ 3 | Database | database | Database design and optimization
75
+ 4 | Tutorial | tutorial | Step-by-step guides
76
+ 5 | Advanced | advanced | Advanced technical topics
77
+ ```
78
+
79
+ ### 5. article_tags table (join table for many-to-many)
80
+ ```sql
81
+ article_id | tag_id | created_at
82
+ -----------|--------|---------------------
83
+ 1 | 1 | 2024-01-15 10:00:00
84
+ 1 | 4 | 2024-01-15 10:00:00
85
+ 2 | 2 | 2024-01-20 14:30:00
86
+ 2 | 4 | 2024-01-20 14:30:00
87
+ 3 | 3 | 2024-01-25 09:15:00
88
+ 3 | 5 | 2024-01-25 09:15:00
89
+ 4 | 2 | 2024-01-30 11:00:00
90
+ ```
91
+
92
+ ### 6. images table (for hasOne example)
93
+ ```sql
94
+ id | url | alt_text | featured_article_id | uploaded_by
95
+ ---|------------------------------|------------------------|---------------------|-------------
96
+ 1 | /uploads/js-header.jpg | JavaScript logo | 1 | 1
97
+ 2 | /uploads/rest-diagram.png | REST API diagram | 2 | 2
98
+ 3 | /uploads/db-schema.jpg | Database schema example| 3 | 1
99
+ 4 | /uploads/profile-alice.jpg | Alice Johnson profile | NULL | 1
100
+ 5 | /uploads/profile-bob.jpg | Bob Smith profile | NULL | 2
101
+ ```
102
+
103
+ ## Schema Definitions
104
+
105
+ Now let's define the schemas for each table. Remember, schemas contain ONLY the columns that exist in that specific table:
106
+
107
+ ### people schema
108
+ ```javascript
109
+ const peopleSchema = {
110
+ // Regular columns
111
+ id: { type: 'id' },
112
+ name: { type: 'string', required: true, max: 100 },
113
+ email: { type: 'string', required: true, unique: true },
114
+ bio: { type: 'string', nullable: true }
115
+
116
+ // Note: No relationships defined here! People table has no foreign keys.
117
+ }
118
+ ```
119
+
120
+ ### articles schema
121
+ ```javascript
122
+ const articlesSchema = {
123
+ // Regular columns
124
+ id: { type: 'id' },
125
+ title: { type: 'string', required: true, max: 200 },
126
+ slug: { type: 'string', required: true, unique: true },
127
+ body: { type: 'string', required: true },
128
+ published_at: { type: 'dateTime', nullable: true },
129
+ view_count: { type: 'number', default: 0 },
130
+
131
+ // Foreign key columns (belongsTo relationships)
132
+ author_id: { belongsTo: 'people', as: 'author' },
133
+ editor_id: { belongsTo: 'people', as: 'editor', nullable: true }
134
+ }
135
+ ```
136
+
137
+ ### comments schema
138
+ ```javascript
139
+ const commentsSchema = {
140
+ // Regular columns
141
+ id: { type: 'id' },
142
+ body: { type: 'string', required: true },
143
+ created_at: { type: 'dateTime', default: 'CURRENT_TIMESTAMP' },
144
+
145
+ // Foreign key columns (belongsTo relationships)
146
+ article_id: { belongsTo: 'articles' },
147
+ user_id: { belongsTo: 'people', as: 'user' },
148
+ parent_comment_id: { belongsTo: 'comments', as: 'parent', nullable: true }
149
+ }
150
+ ```
151
+
152
+ ### tags schema
153
+ ```javascript
154
+ const tagsSchema = {
155
+ // Regular columns only
156
+ id: { type: 'id' },
157
+ name: { type: 'string', required: true, max: 50 },
158
+ slug: { type: 'string', required: true, unique: true },
159
+ description: { type: 'string', nullable: true }
160
+ }
161
+ ```
162
+
163
+ ### images schema
164
+ ```javascript
165
+ const imagesSchema = {
166
+ // Regular columns
167
+ id: { type: 'id' },
168
+ url: { type: 'string', required: true },
169
+ alt_text: { type: 'string', nullable: true },
170
+
171
+ // Foreign key columns
172
+ featured_article_id: { belongsTo: 'articles', as: 'featured_article', nullable: true },
173
+ uploaded_by: { belongsTo: 'people', as: 'uploader' }
174
+ }
175
+ ```
176
+
177
+ ## Using Different Table Names
178
+
179
+ Sometimes your database table name doesn't match your scope name. You can specify a custom table name in the schema:
180
+
181
+ ```javascript
182
+ // If your database table is named 'blog_posts' but you want the API endpoint to be 'articles'
183
+ const articlesSchema = {
184
+ tableName: 'blog_posts', // <-- Specify the actual database table name
185
+
186
+ // Regular columns
187
+ id: { type: 'id' },
188
+ title: { type: 'string', required: true, max: 200 },
189
+ // ... rest of schema
190
+ }
191
+
192
+ // The API will use 'articles' as the scope name
193
+ api.scope('articles', {
194
+ schema: articlesSchema, // Contains tableName: 'blog_posts'
195
+ relationships: { /* ... */ }
196
+ })
197
+
198
+ // API endpoint: GET /articles
199
+ // Database query: SELECT * FROM blog_posts
200
+ ```
201
+
202
+ ## Relationship Configuration
203
+
204
+ Virtual relationships (hasMany, hasOne) are configured separately when defining scopes:
205
+
206
+ ```javascript
207
+ // Articles scope with relationships
208
+ api.scope('articles', {
209
+ schema: articlesSchema,
210
+ relationships: {
211
+ // hasMany - foreign key is in the comments table
212
+ comments: { hasMany: 'comments' },
213
+
214
+ // hasOne - foreign key is in the images table
215
+ featured_image: { hasOne: 'images', foreignKey: 'featured_article_id' },
216
+
217
+ // Many-to-many - through join table
218
+ tags: { hasMany: 'tags', through: 'article_tags' }
219
+ }
220
+ })
221
+
222
+ // People scope with relationships
223
+ api.scope('people', {
224
+ schema: peopleSchema,
225
+ relationships: {
226
+ // hasMany - foreign key is in articles table
227
+ authored_articles: { hasMany: 'articles', foreignKey: 'author_id' },
228
+ edited_articles: { hasMany: 'articles', foreignKey: 'editor_id' },
229
+ comments: { hasMany: 'comments', foreignKey: 'user_id' },
230
+ uploaded_images: { hasMany: 'images', foreignKey: 'uploaded_by' }
231
+ }
232
+ })
233
+
234
+ // Comments scope with relationships
235
+ api.scope('comments', {
236
+ schema: commentsSchema,
237
+ relationships: {
238
+ // hasMany - self-referential for nested comments
239
+ replies: { hasMany: 'comments', foreignKey: 'parent_comment_id' }
240
+ }
241
+ })
242
+
243
+ // Tags scope with relationships
244
+ api.scope('tags', {
245
+ schema: tagsSchema,
246
+ relationships: {
247
+ // Many-to-many - through join table
248
+ articles: { hasMany: 'articles', through: 'article_tags' }
249
+ }
250
+ })
251
+ ```
252
+
253
+ ## Explicit Keys for Through Relationships
254
+
255
+ When using many-to-many relationships with a join table, you can explicitly specify the foreign key columns:
256
+
257
+ ```javascript
258
+ // Basic through relationship (uses conventions)
259
+ tags: { hasMany: 'tags', through: 'article_tags' }
260
+ // Assumes: article_tags.article_id and article_tags.tag_id
261
+
262
+ // Explicit keys for non-standard column names
263
+ categories: {
264
+ hasMany: 'categories',
265
+ through: 'post_categories',
266
+ foreignKey: 'post_id', // Column in join table pointing to THIS table
267
+ otherKey: 'category_id' // Column in join table pointing to OTHER table
268
+ }
269
+
270
+ // Example with a more complex join table
271
+ api.scope('users', {
272
+ schema: usersSchema,
273
+ relationships: {
274
+ // User's roles with metadata in join table
275
+ roles: {
276
+ hasMany: 'roles',
277
+ through: 'user_roles',
278
+ foreignKey: 'user_id',
279
+ otherKey: 'role_id'
280
+ }
281
+ }
282
+ })
283
+
284
+ // The join table might have additional columns:
285
+ // user_roles table:
286
+ // | user_id | role_id | assigned_at | assigned_by |
287
+ // |---------|---------|-------------|-------------|
288
+ // | 1 | 3 | 2024-01-15 | 5 |
289
+ ```
290
+
291
+ ## Understanding belongsTo Relationships
292
+
293
+ A `belongsTo` relationship means the foreign key is IN THIS table. Let's examine how this works:
294
+
295
+ ### Example: Article belongs to Author
296
+
297
+ In the articles schema:
298
+ ```javascript
299
+ author_id: { belongsTo: 'people', as: 'author' }
300
+ ```
301
+
302
+ This tells us:
303
+ - The `articles` table has a column called `author_id`
304
+ - This column contains the ID of a record in the `people` table
305
+ - In JSON:API, this will show as a relationship called `author`
306
+
307
+ ### Database Query Process
308
+
309
+ When you request `GET /articles/1`:
310
+
311
+ 1. **First query - Get the article:**
312
+ ```sql
313
+ SELECT * FROM articles WHERE id = 1;
314
+ -- Returns: {id: 1, title: "Understanding JavaScript", author_id: 1, editor_id: 4, ...}
315
+ ```
316
+
317
+ 2. **The storage plugin sees:**
318
+ - `author_id = 1` (this is Alice Johnson)
319
+ - `editor_id = 4` (this is David Lee)
320
+
321
+ 3. **JSON:API output:**
322
+ ```json
323
+ {
324
+ "data": {
325
+ "type": "articles",
326
+ "id": "1",
327
+ "attributes": {
328
+ "title": "Understanding JavaScript",
329
+ "slug": "understanding-js",
330
+ "body": "JavaScript is a versatile...",
331
+ "published_at": "2024-01-15T10:00:00Z",
332
+ "view_count": 1250
333
+ },
334
+ "relationships": {
335
+ "author": {
336
+ "data": { "type": "people", "id": "1" }
337
+ },
338
+ "editor": {
339
+ "data": { "type": "people", "id": "4" }
340
+ }
341
+ }
342
+ }
343
+ }
344
+ ```
345
+
346
+ ### With Includes
347
+
348
+ When you request `GET /articles/1?include=author,editor`:
349
+
350
+ 1. **Get the article** (same as above)
351
+ 2. **Get the related people:**
352
+ ```sql
353
+ SELECT * FROM people WHERE id IN (1, 4);
354
+ ```
355
+ 3. **Add to included section:**
356
+ ```json
357
+ {
358
+ "data": { ... },
359
+ "included": [
360
+ {
361
+ "type": "people",
362
+ "id": "1",
363
+ "attributes": {
364
+ "name": "Alice Johnson",
365
+ "email": "alice@techblog.com",
366
+ "bio": "Senior tech writer and editor"
367
+ }
368
+ },
369
+ {
370
+ "type": "people",
371
+ "id": "4",
372
+ "attributes": {
373
+ "name": "David Lee",
374
+ "email": "david@techblog.com",
375
+ "bio": "Chief Editor"
376
+ }
377
+ }
378
+ ]
379
+ }
380
+ ```
381
+
382
+ ## Understanding hasMany Relationships
383
+
384
+ A `hasMany` relationship means the foreign key is in ANOTHER table. Let's see how this works:
385
+
386
+ ### Example: Article has many Comments
387
+
388
+ In the articles scope configuration:
389
+ ```javascript
390
+ relationships: {
391
+ comments: { hasMany: 'comments' }
392
+ }
393
+ ```
394
+
395
+ This tells us:
396
+ - The `comments` table has a foreign key pointing to articles
397
+ - By convention, it looks for `article_id` in the comments table
398
+ - Multiple comments can belong to one article
399
+
400
+ ### Database Query Process
401
+
402
+ When you request `GET /articles/1?include=comments`:
403
+
404
+ 1. **Get the article:**
405
+ ```sql
406
+ SELECT * FROM articles WHERE id = 1;
407
+ ```
408
+
409
+ 2. **Get all related comments:**
410
+ ```sql
411
+ SELECT * FROM comments WHERE article_id = 1;
412
+ -- Returns 3 comments: IDs 1, 2, and 3
413
+ ```
414
+
415
+ 3. **JSON:API output:**
416
+ ```json
417
+ {
418
+ "data": {
419
+ "type": "articles",
420
+ "id": "1",
421
+ "relationships": {
422
+ "comments": {
423
+ "data": [
424
+ { "type": "comments", "id": "1" },
425
+ { "type": "comments", "id": "2" },
426
+ { "type": "comments", "id": "3" }
427
+ ]
428
+ }
429
+ }
430
+ },
431
+ "included": [
432
+ {
433
+ "type": "comments",
434
+ "id": "1",
435
+ "attributes": {
436
+ "body": "Great article! Very helpful.",
437
+ "created_at": "2024-01-16T08:30:00Z"
438
+ },
439
+ "relationships": {
440
+ "article_id": { "data": { "type": "articles", "id": "1" } },
441
+ "user": { "data": { "type": "people", "id": "5" } }
442
+ }
443
+ },
444
+ // ... more comments
445
+ ]
446
+ }
447
+ ```
448
+
449
+ ## Understanding hasOne Relationships
450
+
451
+ A `hasOne` relationship is like `hasMany` but expects only one related record:
452
+
453
+ ### Example: Article has one Featured Image
454
+
455
+ In the articles scope configuration:
456
+ ```javascript
457
+ relationships: {
458
+ featured_image: { hasOne: 'images', foreignKey: 'featured_article_id' }
459
+ }
460
+ ```
461
+
462
+ ### Database Query Process
463
+
464
+ When you request `GET /articles/1?include=featured_image`:
465
+
466
+ 1. **Get the article** (as before)
467
+ 2. **Get the featured image:**
468
+ ```sql
469
+ SELECT * FROM images WHERE featured_article_id = 1 LIMIT 1;
470
+ -- Returns: {id: 1, url: "/uploads/js-header.jpg", ...}
471
+ ```
472
+
473
+ 3. **JSON:API output:**
474
+ ```json
475
+ {
476
+ "data": {
477
+ "type": "articles",
478
+ "id": "1",
479
+ "relationships": {
480
+ "featured_image": {
481
+ "data": { "type": "images", "id": "1" }
482
+ }
483
+ }
484
+ },
485
+ "included": [
486
+ {
487
+ "type": "images",
488
+ "id": "1",
489
+ "attributes": {
490
+ "url": "/uploads/js-header.jpg",
491
+ "alt_text": "JavaScript logo"
492
+ }
493
+ }
494
+ ]
495
+ }
496
+ ```
497
+
498
+ ### Important Note on hasOne
499
+
500
+ The `hasOne` relationship expects that `featured_article_id` is unique (only one image per article). If your database doesn't enforce this with a UNIQUE constraint, multiple images could have the same `featured_article_id`. The storage plugin should:
501
+ - Return only the first result
502
+ - Optionally warn if multiple results exist
503
+
504
+ ## Many-to-Many Relationships Deep Dive
505
+
506
+ Many-to-many relationships are the most complex but also very powerful. They use a join table to connect two tables where each record in one table can be related to multiple records in another table, and vice versa.
507
+
508
+ ### Example: Articles and Tags
509
+
510
+ An article can have multiple tags, and a tag can be applied to multiple articles. This is accomplished through the `article_tags` join table.
511
+
512
+ #### Configuration
513
+
514
+ In articles scope:
515
+ ```javascript
516
+ relationships: {
517
+ tags: { hasMany: 'tags', through: 'article_tags' }
518
+ }
519
+ ```
520
+
521
+ In tags scope:
522
+ ```javascript
523
+ relationships: {
524
+ articles: { hasMany: 'articles', through: 'article_tags' }
525
+ }
526
+ ```
527
+
528
+ ### How the Join Table Works
529
+
530
+ The `article_tags` table is simple but crucial:
531
+ ```sql
532
+ article_id | tag_id | created_at
533
+ -----------|--------|---------------------
534
+ 1 | 1 | 2024-01-15 10:00:00 -- Article 1 has tag "JavaScript"
535
+ 1 | 4 | 2024-01-15 10:00:00 -- Article 1 has tag "Tutorial"
536
+ 2 | 2 | 2024-01-20 14:30:00 -- Article 2 has tag "API Design"
537
+ 2 | 4 | 2024-01-20 14:30:00 -- Article 2 has tag "Tutorial"
538
+ ```
539
+
540
+ ### Database Query Process for Many-to-Many
541
+
542
+ When you request `GET /articles/1?include=tags`:
543
+
544
+ 1. **Get the article:**
545
+ ```sql
546
+ SELECT * FROM articles WHERE id = 1;
547
+ ```
548
+
549
+ 2. **Get the relationships from the join table:**
550
+ ```sql
551
+ SELECT * FROM article_tags WHERE article_id = 1;
552
+ -- Returns: [{article_id: 1, tag_id: 1}, {article_id: 1, tag_id: 4}]
553
+ ```
554
+
555
+ 3. **Get the actual tags:**
556
+ ```sql
557
+ SELECT * FROM tags WHERE id IN (1, 4);
558
+ -- Returns: JavaScript and Tutorial tags
559
+ ```
560
+
561
+ 4. **JSON:API output:**
562
+ ```json
563
+ {
564
+ "data": {
565
+ "type": "articles",
566
+ "id": "1",
567
+ "attributes": {
568
+ "title": "Understanding JavaScript"
569
+ },
570
+ "relationships": {
571
+ "tags": {
572
+ "data": [
573
+ { "type": "tags", "id": "1" },
574
+ { "type": "tags", "id": "4" }
575
+ ]
576
+ }
577
+ }
578
+ },
579
+ "included": [
580
+ {
581
+ "type": "tags",
582
+ "id": "1",
583
+ "attributes": {
584
+ "name": "JavaScript",
585
+ "slug": "javascript",
586
+ "description": "All things JavaScript"
587
+ }
588
+ },
589
+ {
590
+ "type": "tags",
591
+ "id": "4",
592
+ "attributes": {
593
+ "name": "Tutorial",
594
+ "slug": "tutorial",
595
+ "description": "Step-by-step guides"
596
+ }
597
+ }
598
+ ]
599
+ }
600
+ ```
601
+
602
+ ### Creating Many-to-Many Relationships
603
+
604
+ When you POST a new article with tags:
605
+
606
+ ```json
607
+ POST /articles
608
+ {
609
+ "data": {
610
+ "type": "articles",
611
+ "attributes": {
612
+ "title": "New Article",
613
+ "body": "Content..."
614
+ },
615
+ "relationships": {
616
+ "tags": {
617
+ "data": [
618
+ { "type": "tags", "id": "1" },
619
+ { "type": "tags", "id": "3" }
620
+ ]
621
+ }
622
+ }
623
+ }
624
+ }
625
+ ```
626
+
627
+ The storage plugin must:
628
+ 1. Insert the article into the `articles` table
629
+ 2. Get the new article's ID (let's say it's 5)
630
+ 3. Insert records into `article_tags`:
631
+ ```sql
632
+ INSERT INTO article_tags (article_id, tag_id) VALUES (5, 1), (5, 3);
633
+ ```
634
+
635
+ ### Updating Many-to-Many Relationships
636
+
637
+ When updating tags for an article, the storage plugin typically:
638
+ 1. Deletes all existing relationships:
639
+ ```sql
640
+ DELETE FROM article_tags WHERE article_id = 1;
641
+ ```
642
+ 2. Inserts the new relationships:
643
+ ```sql
644
+ INSERT INTO article_tags (article_id, tag_id) VALUES (1, 2), (1, 5);
645
+ ```
646
+
647
+ ## Complete API Request Examples
648
+
649
+ Let's walk through some complete examples showing the full data flow:
650
+
651
+ ### Example 1: Get Article with All Relationships
652
+
653
+ Request: `GET /articles/1?include=author,editor,comments.user,tags,featured_image`
654
+
655
+ #### Step 1: Get the main article
656
+ ```sql
657
+ SELECT * FROM articles WHERE id = 1;
658
+ ```
659
+ Result: Article #1 with author_id=1, editor_id=4
660
+
661
+ #### Step 2: Process belongsTo relationships
662
+ The storage plugin identifies:
663
+ - author_id = 1 → Will need to fetch person #1
664
+ - editor_id = 4 → Will need to fetch person #4
665
+
666
+ #### Step 3: Process hasMany relationships
667
+ ```sql
668
+ -- Get comments
669
+ SELECT * FROM comments WHERE article_id = 1;
670
+ -- Returns comments #1, #2, #3
671
+
672
+ -- Get tag relationships
673
+ SELECT * FROM article_tags WHERE article_id = 1;
674
+ -- Returns tag_ids: 1, 4
675
+ ```
676
+
677
+ #### Step 4: Process hasOne relationship
678
+ ```sql
679
+ SELECT * FROM images WHERE featured_article_id = 1 LIMIT 1;
680
+ -- Returns image #1
681
+ ```
682
+
683
+ #### Step 5: Get all related records
684
+ ```sql
685
+ -- Get people (for author, editor, and comment users)
686
+ SELECT * FROM people WHERE id IN (1, 4, 5, 3);
687
+
688
+ -- Get tags
689
+ SELECT * FROM tags WHERE id IN (1, 4);
690
+ ```
691
+
692
+ #### Step 6: Build JSON:API response
693
+ ```json
694
+ {
695
+ "data": {
696
+ "type": "articles",
697
+ "id": "1",
698
+ "attributes": {
699
+ "title": "Understanding JavaScript",
700
+ "slug": "understanding-js",
701
+ "body": "JavaScript is a versatile...",
702
+ "published_at": "2024-01-15T10:00:00Z",
703
+ "view_count": 1250
704
+ },
705
+ "relationships": {
706
+ "author": {
707
+ "data": { "type": "people", "id": "1" }
708
+ },
709
+ "editor": {
710
+ "data": { "type": "people", "id": "4" }
711
+ },
712
+ "comments": {
713
+ "data": [
714
+ { "type": "comments", "id": "1" },
715
+ { "type": "comments", "id": "2" },
716
+ { "type": "comments", "id": "3" }
717
+ ]
718
+ },
719
+ "tags": {
720
+ "data": [
721
+ { "type": "tags", "id": "1" },
722
+ { "type": "tags", "id": "4" }
723
+ ]
724
+ },
725
+ "featured_image": {
726
+ "data": { "type": "images", "id": "1" }
727
+ }
728
+ }
729
+ },
730
+ "included": [
731
+ {
732
+ "type": "people",
733
+ "id": "1",
734
+ "attributes": {
735
+ "name": "Alice Johnson",
736
+ "email": "alice@techblog.com",
737
+ "bio": "Senior tech writer and editor"
738
+ }
739
+ },
740
+ {
741
+ "type": "people",
742
+ "id": "4",
743
+ "attributes": {
744
+ "name": "David Lee",
745
+ "email": "david@techblog.com",
746
+ "bio": "Chief Editor"
747
+ }
748
+ },
749
+ {
750
+ "type": "comments",
751
+ "id": "1",
752
+ "attributes": {
753
+ "body": "Great article! Very helpful.",
754
+ "created_at": "2024-01-16T08:30:00Z"
755
+ },
756
+ "relationships": {
757
+ "article_id": {
758
+ "data": { "type": "articles", "id": "1" }
759
+ },
760
+ "user": {
761
+ "data": { "type": "people", "id": "5" }
762
+ }
763
+ }
764
+ },
765
+ // ... more included resources
766
+ ]
767
+ }
768
+ ```
769
+
770
+ ### Example 2: Create Article with Relationships
771
+
772
+ Request:
773
+ ```json
774
+ POST /articles
775
+ {
776
+ "data": {
777
+ "type": "articles",
778
+ "attributes": {
779
+ "title": "Getting Started with TypeScript",
780
+ "slug": "getting-started-typescript",
781
+ "body": "TypeScript adds static typing to JavaScript..."
782
+ },
783
+ "relationships": {
784
+ "author": {
785
+ "data": { "type": "people", "id": "2" }
786
+ },
787
+ "tags": {
788
+ "data": [
789
+ { "type": "tags", "id": "1" },
790
+ { "type": "tags", "id": "4" }
791
+ ]
792
+ }
793
+ }
794
+ }
795
+ }
796
+ ```
797
+
798
+ Storage plugin process:
799
+
800
+ 1. **Validate the relationships exist:**
801
+ ```sql
802
+ SELECT id FROM people WHERE id = 2; -- Verify author exists
803
+ SELECT id FROM tags WHERE id IN (1, 4); -- Verify tags exist
804
+ ```
805
+
806
+ 2. **Insert the article:**
807
+ ```sql
808
+ INSERT INTO articles (title, slug, body, author_id, view_count)
809
+ VALUES ('Getting Started with TypeScript', 'getting-started-typescript',
810
+ 'TypeScript adds static typing...', 2, 0);
811
+ -- Returns new ID: 5
812
+ ```
813
+
814
+ 3. **Create many-to-many relationships:**
815
+ ```sql
816
+ INSERT INTO article_tags (article_id, tag_id, created_at)
817
+ VALUES (5, 1, NOW()), (5, 4, NOW());
818
+ ```
819
+
820
+ 4. **Return the created article with relationships**
821
+
822
+ ## Storage Plugin Implementation
823
+
824
+ Storage plugins receive helper functions that have access to the full API context, including scopes, schemas, and relationships. Here's how to access this configuration:
825
+
826
+ ### Accessing Scope Configuration
827
+
828
+ ```javascript
829
+ // In your storage plugin's helper functions
830
+ helpers.dataGet = async function({ scopeName, id, queryParams, idProperty }) {
831
+ // Access scope configuration
832
+ const scope = this.scopes[scopeName];
833
+ const schema = scope.schema;
834
+ const relationships = scope.relationships;
835
+ const tableName = schema.tableName || scopeName;
836
+
837
+ // Now you can use this information for your queries
838
+ const query = `SELECT * FROM ${tableName} WHERE ${idProperty} = ?`;
839
+ const article = await db.query(query, [id]);
840
+
841
+ // ... rest of implementation
842
+ };
843
+ ```
844
+
845
+ ### For belongsTo Relationships
846
+
847
+ ```javascript
848
+ // When fetching an article
849
+ const scope = this.scopes[scopeName];
850
+ const schema = scope.schema;
851
+ const tableName = schema.tableName || scopeName;
852
+
853
+ const article = await db.query(`SELECT * FROM ${tableName} WHERE id = ?`, [id]);
854
+
855
+ // The belongsTo columns are already in the result
856
+ // article.author_id = 1
857
+ // article.editor_id = 4
858
+
859
+ // For includes, collect all the IDs
860
+ const peopleIds = [article.author_id, article.editor_id].filter(Boolean);
861
+ const people = await db.query('SELECT * FROM people WHERE id IN (?)', [peopleIds]);
862
+ ```
863
+
864
+ ### For hasMany Relationships
865
+
866
+ ```javascript
867
+ // Get all comments for an article
868
+ const comments = await db.query(
869
+ 'SELECT * FROM comments WHERE article_id = ?',
870
+ [articleId]
871
+ );
872
+
873
+ // For nested includes (e.g., comments.user)
874
+ const userIds = comments.map(c => c.user_id);
875
+ const users = await db.query('SELECT * FROM people WHERE id IN (?)', [userIds]);
876
+ ```
877
+
878
+ ### For Many-to-Many Relationships
879
+
880
+ ```javascript
881
+ // Get relationship configuration
882
+ const relationship = scope.relationships.tags;
883
+ const throughTable = relationship.through;
884
+ const foreignKey = relationship.foreignKey || `${scopeName.slice(0, -1)}_id`; // Default: article_id
885
+ const otherKey = relationship.otherKey || `${relationship.hasMany.slice(0, -1)}_id`; // Default: tag_id
886
+
887
+ // Get tags for an article using explicit keys
888
+ const tagRelations = await db.query(
889
+ `SELECT ${otherKey} FROM ${throughTable} WHERE ${foreignKey} = ?`,
890
+ [articleId]
891
+ );
892
+ const tagIds = tagRelations.map(r => r[otherKey]);
893
+ const tags = await db.query('SELECT * FROM tags WHERE id IN (?)', [tagIds]);
894
+
895
+ // Creating new relationships with explicit keys
896
+ await db.query(`DELETE FROM ${throughTable} WHERE ${foreignKey} = ?`, [articleId]);
897
+ for (const tagId of newTagIds) {
898
+ await db.query(
899
+ `INSERT INTO ${throughTable} (${foreignKey}, ${otherKey}) VALUES (?, ?)`,
900
+ [articleId, tagId]
901
+ );
902
+ }
903
+ ```
904
+
905
+ ### For hasOne Relationships
906
+
907
+ ```javascript
908
+ // Get featured image for an article
909
+ const image = await db.query(
910
+ 'SELECT * FROM images WHERE featured_article_id = ? LIMIT 1',
911
+ [articleId]
912
+ );
913
+
914
+ // Verify it's actually one-to-one
915
+ const count = await db.query(
916
+ 'SELECT COUNT(*) as count FROM images WHERE featured_article_id = ?',
917
+ [articleId]
918
+ );
919
+ if (count > 1) {
920
+ console.warn(`hasOne relationship returned ${count} results`);
921
+ }
922
+ ```
923
+
924
+ ### Complete Storage Plugin Example
925
+
926
+ Here's a complete example of a storage plugin helper that uses all the configuration:
927
+
928
+ ```javascript
929
+ // Example storage plugin
930
+ export const MySQLStoragePlugin = {
931
+ name: 'mysql-storage',
932
+
933
+ install({ helpers, apiOptions }) {
934
+ helpers.dataQuery = async function({ scopeName, queryParams, idProperty }) {
935
+ // Access full scope configuration
936
+ const scope = this.scopes[scopeName];
937
+ const schema = scope.schema;
938
+ const relationships = scope.relationships || {};
939
+ const tableName = schema.tableName || scopeName;
940
+
941
+ // Build base query
942
+ let query = `SELECT * FROM ${tableName}`;
943
+ const params = [];
944
+
945
+ // Handle filters if searchSchema is defined
946
+ if (queryParams.filter && scope.searchSchema) {
947
+ const whereConditions = [];
948
+ for (const [field, value] of Object.entries(queryParams.filter)) {
949
+ if (scope.searchSchema[field]) {
950
+ whereConditions.push(`${field} = ?`);
951
+ params.push(value);
952
+ }
953
+ }
954
+ if (whereConditions.length > 0) {
955
+ query += ` WHERE ${whereConditions.join(' AND ')}`;
956
+ }
957
+ }
958
+
959
+ // Handle sorting
960
+ if (queryParams.sort && queryParams.sort.length > 0) {
961
+ const sortClauses = queryParams.sort.map(field => {
962
+ const desc = field.startsWith('-');
963
+ const fieldName = desc ? field.substring(1) : field;
964
+ return `${fieldName} ${desc ? 'DESC' : 'ASC'}`;
965
+ });
966
+ query += ` ORDER BY ${sortClauses.join(', ')}`;
967
+ }
968
+
969
+ // Handle pagination
970
+ if (queryParams.page) {
971
+ const limit = queryParams.page.size || 20;
972
+ const offset = ((queryParams.page.number || 1) - 1) * limit;
973
+ query += ` LIMIT ${limit} OFFSET ${offset}`;
974
+ }
975
+
976
+ // Execute query
977
+ const records = await db.query(query, params);
978
+
979
+ // Convert to JSON:API format
980
+ return {
981
+ data: records.map(record => ({
982
+ type: scopeName,
983
+ id: String(record[idProperty]),
984
+ attributes: this.filterAttributes(record, schema),
985
+ relationships: this.buildRelationships(record, relationships)
986
+ }))
987
+ };
988
+ };
989
+ }
990
+ };
991
+ ```
992
+
993
+ ## Best Practices
994
+
995
+ 1. **Always validate foreign keys exist** before creating relationships
996
+ 2. **Use transactions** when creating/updating multiple related records
997
+ 3. **Consider indexes** on foreign key columns for performance
998
+ 4. **Use UNIQUE constraints** for hasOne relationships
999
+ 5. **Be consistent** with naming conventions (e.g., always use `_id` suffix)
1000
+ 6. **Document non-standard foreign keys** clearly in your schema
1001
+ 7. **Use the tableName property** when your database table names don't match API scope names
1002
+ 8. **Leverage the context object** (`this`) in storage plugins to access all scope configurations
1003
+
1004
+ ## Summary
1005
+
1006
+ The Hooked API REST plugin's relationship system:
1007
+ - Keeps schemas focused on actual database columns
1008
+ - Makes relationships explicit and configurable
1009
+ - Supports all common relationship types
1010
+ - Maps cleanly to JSON:API output
1011
+ - Gives storage plugins all the information they need
1012
+
1013
+ By separating table structure (schema) from relationships (configuration), we achieve a clean, maintainable system that accurately reflects your database while providing a powerful API.