json-rest-api 0.1.1 → 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.
- package/.claude/settings.local.json +16 -0
- package/LICENSE +674 -20
- package/README.md +728 -120
- 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 +28 -202
- package/lib/payload-validators.js +685 -0
- package/lib/rest-api-errors.js +58 -0
- package/package.json +37 -20
- 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/.jshintrc +0 -48
- package/.npmignore +0 -15
- package/examples/client.js +0 -18
- package/examples/client2.js +0 -27
- package/examples/server.js +0 -23
- package/test.js +0 -101
package/RELATIONSHIPS.md
ADDED
|
@@ -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.
|