json-rest-api 1.0.7 → 1.0.8

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 (146) hide show
  1. package/.claude/agents/library-guide-expert.md +29 -0
  2. package/.claude/agents/rest-api-internals-expert.md +53 -0
  3. package/CLAUDE.md +139 -1
  4. package/README.md +2 -5
  5. package/docTest.js +238 -0
  6. package/docs/GUIDE/GUIDE_1_Initial_Setup.md +955 -0
  7. package/docs/GUIDE/GUIDE_2_1_The_Starting_Point.md +54 -0
  8. package/docs/GUIDE/GUIDE_2_2_Manipulating_And_Searching_Tables.md +750 -0
  9. package/docs/GUIDE/GUIDE_2_3_BelongsTo_Relationships.md +494 -0
  10. package/docs/GUIDE/GUIDE_2_4_HasMany_Records.md +398 -0
  11. package/docs/GUIDE/GUIDE_2_5_HasMany_Polymorphic.md +882 -0
  12. package/docs/GUIDE/GUIDE_2_6_Many_To_Many.md +709 -0
  13. package/docs/GUIDE/GUIDE_2_7_Pagination_And_Ordering.md +1233 -0
  14. package/docs/GUIDE/GUIDE_2_8_Effects_of_PUT_and_PATCH.md +796 -0
  15. package/{README_X_Relationship_Plugin.md → docs/GUIDE/GUIDE_2_9_Relationships_Urls.md} +1 -4
  16. package/docs/GUIDE/GUIDE_2_Data_And_Relations.md +66 -0
  17. package/docs/GUIDE/GUIDE_3_Field_Transformations.md +1246 -0
  18. package/docs/GUIDE/GUIDE_5_Non-Db_Stores.md +0 -0
  19. package/docs/GUIDE/GUIDE_7_Hooks_Data_Management_And_Plugins.md +1490 -0
  20. package/{README_X_SocketIO.md → docs/GUIDE/GUIDE_X_SocketIO.md} +17 -17
  21. package/docs/GUIDE/index.md +58 -0
  22. package/docs/GUIDE.md +61 -2238
  23. package/docs/ONBOARDING/Computed_Properties_And_Sparse_Fields.md +398 -0
  24. package/docs/README.md +57 -2239
  25. package/{README_2_Computed_and_hidden_fields.md → docs/README_VARIOUS/GUIDE_2_Computed_and_hidden_fields.md} +22 -17
  26. package/docs/README_VARIOUS/GUIDE_IN_DOCS.md +2277 -0
  27. package/{README.ORIG.md → docs/README_VARIOUS/README.ORIG.md} +8 -8
  28. package/{README_8_Polymorphic_Relationships.md → docs/README_VARIOUS/README_8_Polymorphic_Relationships.md} +1 -1
  29. package/docs/README_VARIOUS/README_IN_DOCS.ms +2277 -0
  30. package/{RELATIONSHIPS_ADVANCED.md → docs/README_VARIOUS/RELATIONSHIPS_ADVANCED.md} +3 -3
  31. package/{correct-schema-format.md → docs/README_VARIOUS/correct-schema-format.md} +13 -13
  32. package/{pagination.md → docs/README_VARIOUS/pagination.md} +11 -8
  33. package/docs/assets/css/style.scss +78 -5
  34. package/docs/index.md +2 -12
  35. package/helper-reorganization-plan.md +85 -0
  36. package/helper-usage-analysis.md +94 -0
  37. package/package.json +5 -4
  38. package/plugins/core/bulk-operations-plugin.js +2 -2
  39. package/plugins/core/connectors/express-plugin.js +90 -32
  40. package/plugins/core/lib/querying/knex-cross-table-search.js +609 -0
  41. package/plugins/core/lib/querying/knex-json-api-transformers-querying.js +491 -0
  42. package/plugins/core/lib/querying/knex-pagination-helpers.js +544 -0
  43. package/plugins/core/lib/{knex-process-includes.js → querying/knex-process-includes.js} +65 -50
  44. package/plugins/core/lib/querying/knex-query-helpers-base.js +74 -0
  45. package/plugins/core/lib/{knex-query-helpers.js → querying/knex-query-helpers.js} +416 -180
  46. package/plugins/core/lib/{knex-relationship-includes.js → querying/knex-relationship-includes.js} +430 -114
  47. package/plugins/core/lib/querying/knex-window-queries.js +328 -0
  48. package/plugins/core/lib/querying-writing/compile-schemas.js +328 -0
  49. package/plugins/core/{utils → lib/querying-writing}/connectors-query-parser.js +62 -57
  50. package/plugins/core/lib/{database-capabilities.js → querying-writing/database-capabilities.js} +113 -7
  51. package/plugins/core/lib/querying-writing/database-value-normalizers.js +369 -0
  52. package/plugins/core/lib/querying-writing/default-data-helpers.js +72 -0
  53. package/plugins/core/{utils → lib/querying-writing}/field-utils.js +41 -31
  54. package/plugins/core/{utils → lib/querying-writing}/knex-constants.js +1 -0
  55. package/plugins/core/lib/{knex-field-helpers.js → querying-writing/knex-field-helpers.js} +190 -57
  56. package/plugins/core/lib/querying-writing/knex-json-api-transformers.js +162 -0
  57. package/plugins/core/lib/{payload-validators.js → querying-writing/payload-validators.js} +297 -217
  58. package/plugins/core/lib/querying-writing/schema-helpers.js +497 -0
  59. package/plugins/core/lib/querying-writing/scope-validations.js +431 -0
  60. package/plugins/core/lib/{simplified-helpers.js → querying-writing/simplified-helpers.js} +216 -176
  61. package/plugins/core/lib/writing/knex-json-api-transformers-writing.js +110 -0
  62. package/plugins/core/lib/writing/many-to-many-manipulations.js +300 -0
  63. package/plugins/core/lib/writing/relationship-processor.js +252 -0
  64. package/plugins/core/rest-api-knex-plugin.js +183 -62
  65. package/plugins/core/rest-api-plugin-hooks/compile-resource-schemas.js +7 -0
  66. package/plugins/core/rest-api-plugin-hooks/register-relationship-routes.js +72 -0
  67. package/plugins/core/rest-api-plugin-hooks/register-scope-routes.js +102 -0
  68. package/plugins/core/rest-api-plugin-hooks/turn-scope-init-into-vars.js +50 -0
  69. package/plugins/core/rest-api-plugin-hooks/validate-include-configurations.js +39 -0
  70. package/plugins/core/rest-api-plugin-methods/add-route.js +23 -0
  71. package/plugins/core/rest-api-plugin-methods/check-permissions.js +18 -0
  72. package/plugins/core/rest-api-plugin-methods/common.js +605 -0
  73. package/plugins/core/rest-api-plugin-methods/delete-relationship.js +115 -0
  74. package/plugins/core/rest-api-plugin-methods/delete.js +109 -0
  75. package/plugins/core/rest-api-plugin-methods/enrich-attributes.js +186 -0
  76. package/plugins/core/rest-api-plugin-methods/get-related.js +295 -0
  77. package/plugins/core/rest-api-plugin-methods/get-relationship.js +62 -0
  78. package/plugins/core/rest-api-plugin-methods/get.js +187 -0
  79. package/plugins/core/rest-api-plugin-methods/patch-relationship.js +56 -0
  80. package/plugins/core/rest-api-plugin-methods/patch.js +193 -0
  81. package/plugins/core/rest-api-plugin-methods/post-relationship.js +104 -0
  82. package/plugins/core/rest-api-plugin-methods/post.js +157 -0
  83. package/plugins/core/rest-api-plugin-methods/put.js +257 -0
  84. package/plugins/core/rest-api-plugin-methods/query.js +208 -0
  85. package/plugins/core/rest-api-plugin.js +108 -2498
  86. package/plugins/core/socketio-plugin.js +28 -5
  87. package/plugins/storage/local-storage.js +2 -2
  88. package/tests/computed-fields.test.js +483 -0
  89. package/tests/cors.test.js +4 -4
  90. package/tests/curl.test.js +5 -4
  91. package/tests/custom-idproperty-relationships.test.js +891 -0
  92. package/tests/field-getters.test.js +353 -0
  93. package/tests/field-setters.test.js +310 -0
  94. package/tests/fixtures/api-configs.js +800 -28
  95. package/tests/jwt-auth.test.js +0 -2
  96. package/tests/nested-includes.test.js +1 -1
  97. package/tests/pagination-cursor-multifield.test.js +514 -0
  98. package/tests/pagination-enhanced.test.js +12 -9
  99. package/tests/polymorphic-relationships.test.js +10 -8
  100. package/tests/polymorphic-simple.test.js +6 -4
  101. package/tests/positioning-advanced.test.js +12 -12
  102. package/tests/positioning-extended.test.js +25 -25
  103. package/tests/positioning-multifilter.test.js +24 -24
  104. package/tests/positioning.test.js +3 -3
  105. package/tests/query-limits.test.js +24 -8
  106. package/tests/relationship-endpoints.test.js +4 -5
  107. package/tests/relationships.test.js +0 -1
  108. package/tests/socketio.test.js +12 -1
  109. package/tests/virtual-fields.test.js +13 -13
  110. package/PLAN_FOR_WEBSOCKETS.md +0 -398
  111. package/README_1_Initial_Setup.md +0 -393
  112. package/README_X_Hooks.md +0 -647
  113. package/TODO_rest-api-plugin-extras.todo +0 -23
  114. package/connectors_ideas/express-plugin.js +0 -120
  115. package/connectors_ideas/http-plugin.js +0 -138
  116. package/docs/examples/express-plugin.js +0 -366
  117. package/example.js +0 -301
  118. package/plugins/core/HELPER_MODULES.md +0 -285
  119. package/plugins/core/lib/compile-schemas.js +0 -245
  120. package/plugins/core/lib/database-value-normalizers.js +0 -192
  121. package/plugins/core/lib/default-data-helpers.js +0 -100
  122. package/plugins/core/lib/knex-cross-table-search.js +0 -442
  123. package/plugins/core/lib/knex-json-api-helpers.js +0 -84
  124. package/plugins/core/lib/knex-json-api-transformers.js +0 -449
  125. package/plugins/core/lib/knex-pagination-helpers.js +0 -193
  126. package/plugins/core/lib/knex-window-queries.js +0 -142
  127. package/plugins/core/lib/many-to-many-manipulations.js +0 -343
  128. package/plugins/core/lib/relationship-processor.js +0 -278
  129. package/plugins/core/lib/schema-helpers.js +0 -343
  130. package/plugins/core/lib/scope-validations.js +0 -252
  131. package/plugins/core/rest-api-relationships-plugin.js +0 -700
  132. package/plugins/core/utils/knex-query-helpers-base.js +0 -56
  133. /package/{README_X_Permissions.md → docs/GUIDE/GUIDE_4_Authentication_And_Permissions.md} +0 -0
  134. /package/{README_X_Positioning.md → docs/GUIDE/GUIDE_6_Positioning.md} +0 -0
  135. /package/{README_X_Bulk_Operations.md → docs/GUIDE/GUIDE_X_Bulk_Operations.md} +0 -0
  136. /package/{README_X_Cors.md → docs/GUIDE/GUIDE_X_Cors.md} +0 -0
  137. /package/{README_X_Multihome.md → docs/GUIDE/GUIDE_X_Multihome.md} +0 -0
  138. /package/{README_X_Appendices.md → docs/GUIDE/GUIDE_Y_Appendices.md} +0 -0
  139. /package/{QUERY_DEPTH_PROTECTION.md → docs/README_VARIOUS/QUERY_DEPTH_PROTECTION.md} +0 -0
  140. /package/{RELATIONSHIPS.md → docs/README_VARIOUS/RELATIONSHIPS.md} +0 -0
  141. /package/{WEBSOCKET_IMPLEMENTATION_STATUS.md → docs/README_VARIOUS/WEBSOCKET_IMPLEMENTATION_STATUS.md} +0 -0
  142. /package/{helper-functions-chart.md → docs/README_VARIOUS/helper-functions-chart.md} +0 -0
  143. /package/docs/{implementation-hidden-computed-fields.md → README_VARIOUS/implementation-hidden-computed-fields.md} +0 -0
  144. /package/docs/{include-limits-migration-guide.md → README_VARIOUS/include-limits-migration-guide.md} +0 -0
  145. /package/{readmeSnippets → docs/README_VARIOUS}/test_put_behavior.js +0 -0
  146. /package/{readmeSnippets → docs/README_VARIOUS}/test_put_fields_without_defaults.js +0 -0
package/docs/GUIDE.md CHANGED
@@ -1,2277 +1,100 @@
1
- # JSON REST API Tutorial
1
+ # JSON REST API
2
2
 
3
- This tutorial will guide you through building a REST API using `jsonrestapi`. We'll start with the basics and gradually add more features.
3
+ A powerful REST API plugin for [hooked-api](https://github.com/mobily-enterprises/hooked-api) that provides JSON:API-compliant endpoints with minimal configuration. This library makes it easy to create fully-featured REST APIs with support for relationships, filtering, sorting, pagination, and file uploads.
4
4
 
5
- ## Quick Start
6
-
7
- Here's a complete working example to get you started:
8
-
9
- ```javascript
10
- import { Api } from 'hooked-api';
11
- import { RestApiPlugin, HttpPlugin, FileHandlingPlugin } from 'json-rest-api';
12
-
13
- // Create API
14
- const api = new Api({ name: 'my-api' });
15
-
16
- // Create a reusable in-memory storage plugin
17
- // (In production, you'd use RestApiKnexPlugin or another database plugin)
18
- const inMemoryStoragePlugin = {
19
- name: 'in-memory-storage',
20
- install({ helpers }) {
21
- const storage = new Map();
22
-
23
- // Seed with some initial data
24
- storage.set('books', [
25
- { id: '1', title: 'The Great Gatsby', author: 'F. Scott Fitzgerald', year: 1925 },
26
- { id: '2', title: '1984', author: 'George Orwell', year: 1949 },
27
- { id: '3', title: 'To Kill a Mockingbird', author: 'Harper Lee', year: 1960 }
28
- ]);
29
-
30
- helpers.dataQuery = async ({ scopeName }) => {
31
- const records = storage.get(scopeName) || [];
32
- return {
33
- data: records.map(record => ({
34
- type: scopeName,
35
- id: record.id,
36
- attributes: record
37
- }))
38
- };
39
- };
40
-
41
- helpers.dataGet = async ({ scopeName, id }) => {
42
- const records = storage.get(scopeName) || [];
43
- const record = records.find(r => r.id === id);
44
-
45
- if (!record) {
46
- return { data: null };
47
- }
48
-
49
- return {
50
- data: {
51
- type: scopeName,
52
- id: record.id,
53
- attributes: record
54
- }
55
- };
56
- };
57
-
58
- helpers.dataPost = async ({ scopeName, inputRecord }) => {
59
- const records = storage.get(scopeName) || [];
60
- const newRecord = {
61
- id: Date.now().toString(),
62
- ...inputRecord.data.attributes
63
- };
64
-
65
- records.push(newRecord);
66
- storage.set(scopeName, records);
67
-
68
- return {
69
- data: {
70
- type: scopeName,
71
- id: newRecord.id,
72
- attributes: newRecord
73
- }
74
- };
75
- };
76
-
77
- helpers.dataPut = async ({ scopeName, id, inputRecord }) => {
78
- const records = storage.get(scopeName) || [];
79
- const index = records.findIndex(r => r.id === id);
80
-
81
- if (index === -1) {
82
- throw new Error('Record not found');
83
- }
84
-
85
- records[index] = {
86
- id,
87
- ...inputRecord.data.attributes
88
- };
89
-
90
- return {
91
- data: {
92
- type: scopeName,
93
- id: id,
94
- attributes: records[index]
95
- }
96
- };
97
- };
98
-
99
- helpers.dataPatch = async ({ scopeName, id, inputRecord }) => {
100
- const records = storage.get(scopeName) || [];
101
- const index = records.findIndex(r => r.id === id);
102
-
103
- if (index === -1) {
104
- throw new Error('Record not found');
105
- }
106
-
107
- records[index] = {
108
- ...records[index],
109
- ...inputRecord.data.attributes
110
- };
111
-
112
- return {
113
- data: {
114
- type: scopeName,
115
- id: id,
116
- attributes: records[index]
117
- }
118
- };
119
- };
120
-
121
- helpers.dataDelete = async ({ scopeName, id }) => {
122
- const records = storage.get(scopeName) || [];
123
- const filtered = records.filter(r => r.id !== id);
124
- storage.set(scopeName, filtered);
125
- return { success: true };
126
- };
127
-
128
- helpers.dataExists = async ({ scopeName, id }) => {
129
- const records = storage.get(scopeName) || [];
130
- return records.some(r => r.id === id);
131
- };
132
- }
133
- };
134
-
135
- // Install plugins (order matters!)
136
- await api.use(RestApiPlugin);
137
- await api.use(inMemoryStoragePlugin);
138
- await api.use(HttpPlugin);
139
-
140
- // Define a resource
141
- api.addResource('books', {
142
- schema: {
143
- title: { type: 'string', required: true },
144
- author: { type: 'string', required: true },
145
- year: { type: 'number' }
146
- }
147
- });
148
-
149
- // Start the server
150
- api.http.startServer(3000);
151
- console.log('API running at http://localhost:3000/api');
152
-
153
- // Your API is now ready!
154
- // GET http://localhost:3000/api/books
155
- // POST http://localhost:3000/api/books
156
- // etc.
157
- ```
158
-
159
- ## Table of Contents
160
-
161
- 1. [Getting Started](#getting-started)
162
- 2. [Basic REST API Plugin](#basic-rest-api-plugin)
163
- 3. [Response Control and ID Handling](#response-control-and-id-handling)
164
- - [Return Full Record Configuration](#return-full-record-configuration)
165
- - [Strict ID Handling](#strict-id-handling)
166
- 4. [Connector Plugins](#connector-plugins)
167
- - [Express Plugin](#express-plugin)
168
- - [HTTP Plugin](#http-plugin)
169
- 5. [File Storage](#file-storage)
170
- - [File Uploads with Express](#file-uploads-with-express)
171
- - [Storage Adapters](#storage-adapters)
172
-
173
- ## Getting Started
174
-
175
- First, install the required packages:
176
-
177
- ```bash
178
- npm install hooked-api json-rest-api
179
- # If you get peer dependency conflicts, use:
180
- # npm install hooked-api json-rest-api --legacy-peer-deps
181
- ```
182
-
183
- Create a new file called `api.js`:
184
-
185
- ```javascript
186
- import { Api } from 'hooked-api';
187
-
188
- // Create a new API instance
189
- const api = new Api({
190
- name: 'my-library-api',
191
- version: '1.0.0'
192
- });
193
- ```
194
-
195
- This is a pure Hooked API with no plugins. So, it doesn't do anything.
196
-
197
- ## Basic REST API Plugin
198
-
199
- The `RestApiPlugin` is the foundation of jsonrestapi. It adds REST methods (query, get, post, put, patch, delete) to your API scopes.
200
-
201
- ### Adding the REST API Plugin
202
-
203
- ```javascript
204
- import { Api } from 'hooked-api';
205
- import { RestApiPlugin } from 'json-rest-api';
206
-
207
- const api = new Api({
208
- name: 'my-library-api',
209
- version: '1.0.0'
210
- });
211
-
212
- // Use the REST API plugin
213
- await api.use(RestApiPlugin);
214
- ```
215
-
216
- ### Creating a Books Resource
217
-
218
- Now let's create a simple "books" resource with fake data:
219
-
220
- ```javascript
221
- // Define a books resource
222
- api.addResource('books', {
223
- schema: {
224
- title: { type: 'string', required: true },
225
- author: { type: 'string', required: true },
226
- year: { type: 'number' },
227
- isbn: { type: 'string' }
228
- }
229
- });
230
-
231
- // To actually use the REST API methods, you need a storage plugin
232
- // Use the same inMemoryStoragePlugin from the Quick Start example above
233
- await api.use(inMemoryStoragePlugin);
234
- ```
235
-
236
- ### Using the API Programmatically
237
-
238
- With the REST API plugin and a storage plugin, you can now use the API programmatically:
239
-
240
- ```javascript
241
- // Query all books
242
- const allBooks = await api.resources.books.query({});
243
- console.log(allBooks);
244
- // Output: { data: [{ type: 'books', id: '1', attributes: {...} }, ...] }
245
-
246
- // Get a single book
247
- const book = await api.resources.books.get({ id: '1' });
248
- console.log(book);
249
- // Output: { data: { type: 'books', id: '1', attributes: {...} } }
250
-
251
- // Create a new book
252
- const newBook = await api.resources.books.post({
253
- inputRecord: {
254
- data: {
255
- type: 'books',
256
- attributes: {
257
- title: 'New Book',
258
- author: 'New Author',
259
- year: 2024
260
- }
261
- }
262
- }
263
- });
264
- console.log(newBook);
265
-
266
- // Update a book (replace all fields)
267
- const updatedBook = await api.resources.books.put({
268
- inputRecord: {
269
- data: {
270
- type: 'books',
271
- id: '1',
272
- attributes: {
273
- title: 'Updated Title',
274
- author: 'Updated Author',
275
- year: 2024
276
- }
277
- }
278
- }
279
- });
280
-
281
- // Partially update a book
282
- const patchedBook = await api.resources.books.patch({
283
- inputRecord: {
284
- data: {
285
- type: 'books',
286
- id: '1',
287
- attributes: {
288
- title: 'New Title Only'
289
- }
290
- }
291
- }
292
- });
293
-
294
- // Delete a book
295
- await api.resources.books.delete({ id: '1' });
296
- ```
297
-
298
- ## Response Control and ID Handling
299
-
300
- The REST API plugin provides advanced configuration options for controlling response payloads and ID validation behavior.
301
-
302
- ### Return Full Record Configuration
303
-
304
- By default, POST, PUT, and PATCH operations return the complete record including all fields, defaults, and relationships. You can configure this behavior to return minimal responses containing only the data that was sent in the request.
305
-
306
- #### Configuration Levels
307
-
308
- The `returnFullRecord` option can be configured at three levels (from lowest to highest priority):
309
-
310
- 1. **API-level** (default for all resources)
311
- 2. **Resource-level** (override for specific resources)
312
- 3. **Method parameter** (override for individual calls)
313
-
314
- #### Basic Configuration
315
-
316
- ```javascript
317
- // API-level configuration
318
- await api.use(RestApiPlugin, {
319
- returnFullRecord: {
320
- post: false, // Don't return full record for POST
321
- put: false, // Don't return full record for PUT
322
- patch: false, // Don't return full record for PATCH
323
- allowRemoteOverride: true // Allow clients to override via query param
324
- }
325
- });
326
-
327
- // Resource-level override
328
- api.addResource('articles', {
329
- schema: {
330
- title: { type: 'string', required: true },
331
- body: { type: 'string' },
332
- status: { type: 'string', default: 'draft' }
333
- },
334
- returnFullRecord: {
335
- post: true, // Always return full record for articles POST
336
- allowRemoteOverride: false // Don't allow client override for articles
337
- }
338
- });
339
-
340
- // Method-level override
341
- const result = await api.resources.articles.post({
342
- inputRecord: {
343
- data: {
344
- type: 'articles',
345
- attributes: { title: 'New Article' }
346
- }
347
- },
348
- returnFullRecord: false // Override for this specific call
349
- });
350
- ```
351
-
352
- #### Behavior Differences
353
-
354
- **Full Record (default, `returnFullRecord: true`):**
355
- - Returns the complete record after creation/update
356
- - Includes all fields with their current database values
357
- - Includes default values applied by schema or database
358
- - Can include relationships if requested via `include` parameter
359
- - Equivalent to performing a GET request after the operation
360
-
361
- **Minimal Record (`returnFullRecord: false`):**
362
- - Returns only the fields that were provided in the request
363
- - Includes the generated/updated ID
364
- - Does not include default values or unchanged fields
365
- - More efficient for large records or when defaults aren't needed
366
- - Useful for bandwidth-constrained environments
367
-
368
- #### Example Responses
369
-
370
- ```javascript
371
- // Input
372
- const input = {
373
- data: {
374
- type: 'articles',
375
- attributes: {
376
- title: 'My Article'
377
- }
378
- }
379
- };
380
-
381
- // With returnFullRecord: true (default)
382
- {
383
- "data": {
384
- "type": "articles",
385
- "id": "123",
386
- "attributes": {
387
- "title": "My Article",
388
- "status": "draft", // Default value included
389
- "createdAt": "2024-01-01T00:00:00Z", // Database-generated
390
- "updatedAt": "2024-01-01T00:00:00Z" // Database-generated
391
- }
392
- }
393
- }
394
-
395
- // With returnFullRecord: false
396
- {
397
- "data": {
398
- "type": "articles",
399
- "id": "123",
400
- "attributes": {
401
- "title": "My Article" // Only what was provided
402
- }
403
- }
404
- }
405
- ```
406
-
407
- #### Remote Override via Query Parameters
408
-
409
- When `allowRemoteOverride` is enabled, clients can control the response format using query parameters:
410
-
411
- ```bash
412
- # Request minimal response
413
- POST /api/articles?returnFullRecord=false
414
-
415
- # Request full response (when API default is false)
416
- POST /api/articles?returnFullRecord=true
417
- ```
418
-
419
- #### Network vs Programmatic Usage
420
-
421
- ### Complete Example
422
-
423
- Here's a complete example demonstrating both features:
424
-
425
- ```javascript
426
- import { Api } from 'hooked-api';
427
- import { RestApiPlugin, ExpressPlugin, RestApiKnexPlugin } from 'json-rest-api';
428
- import express from 'express';
429
-
430
- const api = new Api({ name: 'my-api' });
431
-
432
- // Configure REST API with custom settings
433
- await api.use(RestApiPlugin, {
434
- returnFullRecord: {
435
- post: false,
436
- put: false,
437
- patch: true, // Only PATCH returns full record by default
438
- allowRemoteOverride: true
439
- }
440
- });
441
-
442
- // Add Express connector
443
- await api.use(ExpressPlugin, { basePath: '/api' });
444
-
445
- // Add your database plugin
446
- await api.use(RestApiKnexPlugin, { /* your config */ });
447
-
448
- // Define resources with overrides
449
- api.addResource('users', {
450
- schema: {
451
- name: { type: 'string', required: true },
452
- email: { type: 'string', required: true },
453
- role: { type: 'string', default: 'user' },
454
- createdAt: { type: 'string', default: () => new Date().toISOString() }
455
- },
456
- // Users always return full record for audit purposes
457
- returnFullRecord: {
458
- post: true,
459
- put: true,
460
- patch: true,
461
- allowRemoteOverride: false // Don't allow clients to change this
462
- }
463
- });
464
-
465
- api.addResource('posts', {
466
- schema: {
467
- title: { type: 'string', required: true },
468
- content: { type: 'string' },
469
- status: { type: 'string', default: 'draft' }
470
- }
471
- // Uses API defaults (minimal response)
472
- });
473
-
474
- // Start server
475
- const app = express();
476
- app.use(api.expressRouter);
477
- app.listen(3000);
478
-
479
- // Client examples:
480
-
481
- // 1. Create post with minimal response (API default)
482
- // POST /api/posts
483
- // Returns: { "data": { "type": "posts", "id": "1", "attributes": { "title": "Hello" } } }
484
-
485
- // 2. Create post with full response (query override)
486
- // POST /api/posts?returnFullRecord=true
487
- // Returns: { "data": { "type": "posts", "id": "1", "attributes": { "title": "Hello", "status": "draft" } } }
488
-
489
- // 3. Create user (always returns full record)
490
- // POST /api/users?returnFullRecord=false // Ignored due to allowRemoteOverride: false
491
- // Returns: { "data": { "type": "users", "id": "2", "attributes": { "name": "John", "email": "john@example.com", "role": "user", "createdAt": "2024-01-01T00:00:00Z" } } }
492
-
493
- // 4. Update with relaxed ID handling (programmatic)
494
- await api.resources.posts.patch({
495
- id: '1',
496
- inputRecord: {
497
- data: {
498
- type: 'posts',
499
- id: 1,
500
- attributes: { status: 'published' }
501
- }
502
- },
503
- returnFullRecord: false // Override to get minimal response
504
- });
505
- ```
506
-
507
- ### Use Cases
508
-
509
- **When to use minimal responses (`returnFullRecord: false`):**
510
- - High-volume APIs where bandwidth is a concern
511
- - When clients already have the full record and only need confirmation
512
- - Mobile applications with limited data plans
513
- - When default values are computed client-side
514
-
515
- The connector plugins allow you to expose the API in several ways. The core plugins expose the API to Express or to pure Node.
516
-
517
- ## Connector Plugins
518
-
519
- While the REST API plugin provides the core functionality, you need a connector plugin to expose your API over HTTP. You can choose between Express or the pure HTTP plugin.
520
-
521
- > **Important**: Use either the Express plugin OR the HTTP plugin, not both!
522
-
523
- ### Features (Common to Both Plugins)
524
-
525
- Both connector plugins provide these features:
526
-
527
- - Automatic route creation for all resources (GET, POST, PUT, PATCH, DELETE)
528
- - Full JSON:API compliant request/response handling
529
- - Query parameter parsing (include, fields, filter, sort, page)
530
- - Request body parsing with configurable size limits
531
- - Strict content type validation with `strictContentType` option (default: true)
532
- - Enforces `application/vnd.api+json` or `application/json` for POST/PUT/PATCH
533
- - Returns 415 Unsupported Media Type for invalid content types
534
- - Error responses in JSON:API format with proper HTTP status codes (400, 403, 404, 405, 409, 415, 422, 500)
535
- - File upload support when used with FileHandlingPlugin
536
- - Access to raw request/response objects for advanced use cases
537
- - Dynamic resource support (resources added after server startup work automatically)
538
-
539
- Example error response (same for both plugins):
540
- ```json
541
- {
542
- "errors": [{
543
- "status": "422",
544
- "title": "Validation Error",
545
- "detail": "Title is required",
546
- "source": { "pointer": "/data/attributes/title" }
547
- }]
548
- }
549
- ```
550
-
551
- ### Express Plugin
552
-
553
- The Express plugin integrates your API with Express.js, the popular Node.js web framework. In addition to the common features above, it provides:
554
-
555
- - Middleware injection support (before/after routes, per-resource)
556
- - Works with any Express middleware ecosystem
557
- - Familiar Express router patterns
558
-
559
- First, install Express:
560
-
561
- ```bash
562
- npm install express
563
- ```
564
-
565
- Then update your code:
566
-
567
- ```javascript
568
- import { Api } from 'hooked-api';
569
- import { RestApiPlugin, ExpressPlugin } from 'json-rest-api';
570
- import express from 'express';
571
-
572
- const api = new Api({
573
- name: 'my-library-api',
574
- version: '1.0.0'
575
- });
576
-
577
- // Add plugins
578
- await api.use(RestApiPlugin);
579
- await api.use(ExpressPlugin, {
580
- basePath: '/api', // Optional, defaults to '/api'
581
- });
582
-
583
- // Add your books resource (you'll need a storage plugin for it to work)
584
- // ...
585
-
586
- // Create Express app
587
- const app = express();
588
-
589
- // Mount the API routes - both approaches work:
590
-
591
- // Approach 1: Direct router usage (standard Express pattern)
592
- app.use(api.http.express.router); // Mount at root
593
- // OR
594
- app.use('/v1', api.http.express.router); // Mount at specific path
595
-
596
- // Approach 2: Convenience method (adds logging)
597
- api.http.express.mount(app); // Mount at root
598
- // OR
599
- api.http.express.mount(app, '/v1'); // Mount at specific path
600
-
601
- // Note: The mount() method is just syntactic sugar that calls app.use() and logs the mount path
602
-
603
- // Start the server
604
- app.listen(3000, () => {
605
- console.log('API running at http://localhost:3000/api');
606
- });
607
- ```
608
-
609
- Now your API is ready to receive HTTP requests!
610
-
611
- ### HTTP Plugin
612
-
613
- If you prefer not to use Express, the HTTP plugin provides a lightweight alternative using only Node.js built-in modules. In addition to the common features above, it provides:
614
-
615
- - Zero framework dependencies (pure Node.js)
616
- - Built-in HTTP server with configurable port
617
- - Multiple deployment options (standalone server, HTTPS with custom handler, integration with existing servers)
618
- - Character encoding support (respects Content-Type charset)
619
-
620
- #### Configuration Options
621
-
622
- ```javascript
623
- import { Api } from 'hooked-api';
624
- import { RestApiPlugin, HttpPlugin } from 'json-rest-api';
625
-
626
- const api = new Api({
627
- name: 'my-library-api',
628
- version: '1.0.0'
629
- });
630
-
631
- // Add plugins
632
- await api.use(RestApiPlugin);
633
- await api.use(HttpPlugin, {
634
- port: 3000, // Server port (default: 3000)
635
- basePath: '/api', // API base path (default: '/api')
636
- strictContentType: true, // Enforce JSON content types (default: true)
637
- requestSizeLimit: '10mb' // Max request body size (default: '1mb')
638
- });
639
-
640
- // Add your books resource (you'll need a storage plugin for it to work)
641
- // ...
642
-
643
- // Start the HTTP server - multiple options:
644
-
645
- // Option 1: Start on configured port (3000 by default)
646
- api.http.startServer();
647
-
648
- // Option 2: Start on a different port
649
- api.http.startServer(4000);
650
-
651
- // Option 3: Access the raw server for custom configuration
652
- const server = api.http.server;
653
- server.timeout = 60000; // 60 second timeout
654
- server.listen(3000);
655
-
656
- // Option 4: Use with HTTPS
657
- import { createServer } from 'https';
658
- const httpsServer = createServer(sslOptions, api.http.handler);
659
- httpsServer.listen(443);
660
-
661
- // Option 5: Integrate into existing server
662
- myExistingServer.on('request', (req, res) => {
663
- if (req.url.startsWith('/api')) {
664
- api.http.handler(req, res);
665
- } else {
666
- // Handle other routes
667
- }
668
- });
669
-
670
- console.log('API running at http://localhost:3000/api');
671
- ```
672
-
673
-
674
- #### Character Encoding
675
-
676
- The plugin respects the charset parameter in Content-Type headers:
677
-
678
- ```bash
679
- curl -X POST http://localhost:3000/api/books \
680
- -H "Content-Type: application/json; charset=utf-16" \
681
- -d '...'
682
- ```
683
-
684
- The HTTP plugin provides the same REST endpoints as Express but with zero external framework dependencies.
685
-
686
- ### Using Your REST API
687
-
688
- Once you've set up either the Express or HTTP plugin, you can interact with your API using any HTTP client. Here are examples using curl that show the **full JSON:API compliant responses** you'll receive:
689
-
690
- ```bash
691
- # Get all books
692
- curl http://localhost:3000/api/books
693
-
694
- # Returns:
695
- {
696
- "data": [
697
- {
698
- "type": "books",
699
- "id": "1",
700
- "attributes": {
701
- "title": "The Great Gatsby",
702
- "author": "F. Scott Fitzgerald",
703
- "year": 1925
704
- }
705
- },
706
- {
707
- "type": "books",
708
- "id": "2",
709
- "attributes": {
710
- "title": "1984",
711
- "author": "George Orwell",
712
- "year": 1949
713
- }
714
- }
715
- ]
716
- }
717
-
718
- # Get a single book
719
- curl http://localhost:3000/api/books/1
720
-
721
- # Returns:
722
- {
723
- "data": {
724
- "type": "books",
725
- "id": "1",
726
- "attributes": {
727
- "title": "The Great Gatsby",
728
- "author": "F. Scott Fitzgerald",
729
- "year": 1925
730
- }
731
- }
732
- }
733
-
734
- # Create a new book
735
- curl -X POST http://localhost:3000/api/books \
736
- -H "Content-Type: application/json" \
737
- -d '{
738
- "data": {
739
- "type": "books",
740
- "attributes": {
741
- "title": "New Book",
742
- "author": "New Author",
743
- "year": 2024
744
- }
745
- }
746
- }'
747
-
748
- # Returns (201 Created):
749
- {
750
- "data": {
751
- "type": "books",
752
- "id": "1234567890",
753
- "attributes": {
754
- "title": "New Book",
755
- "author": "New Author",
756
- "year": 2024
757
- }
758
- }
759
- }
760
-
761
- # Update a book
762
- curl -X PATCH http://localhost:3000/api/books/1 \
763
- -H "Content-Type: application/json" \
764
- -d '{
765
- "data": {
766
- "type": "books",
767
- "id": "1",
768
- "attributes": {
769
- "title": "Updated Title"
770
- }
771
- }
772
- }'
773
-
774
- # Returns:
775
- {
776
- "data": {
777
- "type": "books",
778
- "id": "1",
779
- "attributes": {
780
- "title": "Updated Title",
781
- "author": "F. Scott Fitzgerald",
782
- "year": 1925
783
- }
784
- }
785
- }
786
-
787
- # Delete a book
788
- curl -X DELETE http://localhost:3000/api/books/1
789
-
790
- # Returns: 204 No Content (empty response)
791
- ```
792
-
793
- ### File Upload Support in Connector Plugins
794
-
795
- Both the Express and HTTP plugins provide support for the FileHandlingPlugin by:
796
-
797
- 1. **Providing raw request/response access**: The plugins add `_httpReq` and `_httpRes` parameters that the FileHandlingPlugin uses to detect and parse multipart uploads.
798
-
799
- 2. **Registering file detectors**: When file uploads are enabled, the connector plugins register appropriate file detectors with the REST API.
800
-
801
- #### Configuration
802
-
803
- ##### Express Plugin
804
- ```javascript
805
- import { Api } from 'hooked-api';
806
- import { RestApiPlugin, ExpressPlugin } from 'json-rest-api';
807
-
808
- const api = new Api({ name: 'my-api' });
809
- await api.use(RestApiPlugin);
810
-
811
- // Configure Express plugin with file upload options
812
- await api.use(ExpressPlugin, {
813
- enableFileUploads: true, // Enable file upload support (default: true)
814
- fileParser: 'busboy', // Parser: 'busboy' or 'formidable'
815
- fileParserOptions: { // Options passed to the file parser
816
- limits: {
817
- fileSize: 10 * 1024 * 1024, // 10MB file size limit
818
- files: 5 // Max 5 files per request
819
- }
820
- }
821
- });
822
- ```
823
-
824
- ##### HTTP Plugin
825
- ```javascript
826
- import { Api } from 'hooked-api';
827
- import { RestApiPlugin, HttpPlugin } from 'json-rest-api';
828
-
829
- const api = new Api({ name: 'my-api' });
830
- await api.use(RestApiPlugin);
831
-
832
- // Configure HTTP plugin with file upload options
833
- await api.use(HttpPlugin, {
834
- enableFileUploads: true, // Enable file upload support (default: true)
835
- fileParser: 'busboy', // Parser: 'busboy' or 'formidable'
836
- fileParserOptions: { // Options passed to the file parser
837
- limits: {
838
- fileSize: 10 * 1024 * 1024, // 10MB file size limit
839
- files: 5 // Max 5 files per request
840
- }
841
- }
842
- });
843
- ```
844
-
845
- #### File Parsers
846
-
847
- Both plugins support the same file parsers:
848
-
849
- - **busboy**: Streaming parser, memory efficient, good for large files
850
- - **formidable**: Saves to temp files, includes progress tracking
851
-
852
- **Installation**: File parsers must be installed separately:
853
- ```bash
854
- # For busboy (recommended)
855
- npm install busboy
856
-
857
- # For formidable
858
- npm install formidable
859
- ```
860
-
861
- **Note**: File parsing requires the FileHandlingPlugin to be loaded. The connector plugins only provide the detection and parsing capabilities; the actual file handling logic is implemented by the FileHandlingPlugin.
862
-
863
- ## File Storage
864
-
865
- The file handling system allows you to accept file uploads in your API. It consists of three parts:
866
-
867
- 1. The `FileHandlingPlugin` - orchestrates file processing
868
- 2. File detectors - parse multipart uploads (built into connector plugins)
869
- 3. Storage adapters - save files to disk or cloud storage
870
-
871
- ### Setting Up File Uploads
872
-
873
- First, let's add file support to our books API. We'll allow users to upload book covers:
874
-
875
- ```javascript
876
- import { Api } from 'hooked-api';
877
- import { RestApiPlugin, FileHandlingPlugin, ExpressPlugin } from 'json-rest-api';
878
- import { LocalStorage } from 'jsonrestapi/plugins/storage/local-storage.js';
879
- import express from 'express';
880
-
881
- const api = new Api({
882
- name: 'my-library-api',
883
- version: '1.0.0'
884
- });
885
-
886
- // Create a storage adapter
887
- const coverStorage = new LocalStorage({
888
- directory: './uploads/covers',
889
- baseUrl: 'http://localhost:3000/uploads/covers'
890
- });
891
-
892
- // Add plugins (ORDER MATTERS!)
893
- await api.use(RestApiPlugin);
894
- await api.use(FileHandlingPlugin); // Must come after RestApiPlugin
895
- await api.use(ExpressPlugin); // Must come after FileHandlingPlugin
896
-
897
- // Update the books schema to include a cover image
898
- api.addResource('books', {
899
- schema: {
900
- title: { type: 'string', required: true },
901
- author: { type: 'string', required: true },
902
- year: { type: 'number' },
903
- isbn: { type: 'string' },
904
- cover: {
905
- type: 'file',
906
- storage: coverStorage,
907
- accepts: ['image/jpeg', 'image/png', 'image/gif'],
908
- maxSize: '5mb'
909
- }
910
- }
911
- });
912
-
913
- // You'll need a storage plugin to handle the data
914
- // Use the same inMemoryStoragePlugin from the Quick Start example
915
- await api.use(inMemoryStoragePlugin);
916
-
917
- // Create Express app
918
- const app = express();
919
-
920
- // Serve uploaded files
921
- app.use('/uploads', express.static('./uploads'));
922
-
923
- // Mount the API (both approaches work)
924
- app.use(api.express.router); // Direct Express approach
925
- // OR
926
- api.express.mount(app); // Convenience method
927
-
928
- app.listen(3000, () => {
929
- console.log('API with file uploads running at http://localhost:3000/api');
930
- });
931
- ```
932
-
933
- ### File Uploads with Express
934
-
935
- To upload a file, you need to send a multipart/form-data request. Here's an example HTML form:
936
-
937
- ```html
938
- <!DOCTYPE html>
939
- <html>
940
- <body>
941
- <h2>Add a Book</h2>
942
- <form action="http://localhost:3000/api/books" method="POST" enctype="multipart/form-data">
943
- <label>Title: <input name="title" required></label><br>
944
- <label>Author: <input name="author" required></label><br>
945
- <label>Year: <input name="year" type="number"></label><br>
946
- <label>Cover: <input name="cover" type="file" accept="image/*"></label><br>
947
- <button type="submit">Add Book</button>
948
- </form>
949
- </body>
950
- </html>
951
- ```
952
-
953
- Or using curl:
954
-
955
- ```bash
956
- curl -X POST http://localhost:3000/api/books \
957
- -F "title=The Hobbit" \
958
- -F "author=J.R.R. Tolkien" \
959
- -F "year=1937" \
960
- -F "cover=@/path/to/cover.jpg"
961
- ```
962
-
963
- The file handling plugin will:
964
- 1. Detect the multipart upload
965
- 2. Parse the files using busboy (you'll need to install it: `npm install busboy`)
966
- 3. Validate file type and size according to your schema
967
- 4. Upload the file using the storage adapter
968
- 5. Replace the file field with the uploaded file's URL
969
- 6. Convert everything to JSON:API format
970
-
971
- ### Storage Adapters
972
-
973
- #### LocalStorage
974
-
975
- The LocalStorage adapter saves files to your local filesystem:
976
-
977
- ```javascript
978
- import { LocalStorage } from 'jsonrestapi/plugins/storage/local-storage.js';
979
-
980
- // Basic usage
981
- const storage = new LocalStorage({
982
- directory: './uploads',
983
- baseUrl: 'http://localhost:3000/uploads'
984
- });
985
-
986
- // With security options
987
- const secureStorage = new LocalStorage({
988
- directory: './uploads',
989
- baseUrl: 'http://localhost:3000/uploads',
990
- nameStrategy: 'hash', // Generate random filenames
991
- allowedExtensions: ['.jpg', '.jpeg', '.png', '.pdf'],
992
- maxFilenameLength: 255
993
- });
994
-
995
- // Different naming strategies
996
- const customStorage = new LocalStorage({
997
- directory: './uploads',
998
- baseUrl: 'http://localhost:3000/uploads',
999
- nameStrategy: 'custom',
1000
- nameGenerator: async (file) => {
1001
- // Generate your own filename
1002
- const timestamp = Date.now();
1003
- const random = Math.random().toString(36).substring(7);
1004
- return `book_cover_${timestamp}_${random}`;
1005
- }
1006
- });
1007
- ```
1008
-
1009
- #### S3Storage (Mock Implementation)
1010
-
1011
- The package includes a mock S3 storage adapter for development:
1012
-
1013
- ```javascript
1014
- import { S3Storage } from 'jsonrestapi/plugins/storage/s3-storage.js';
1015
-
1016
- const s3Storage = new S3Storage({
1017
- bucket: 'my-book-covers',
1018
- region: 'us-east-1',
1019
- prefix: 'covers/',
1020
- mockMode: true // Currently only mock mode is implemented
1021
- });
1022
- ```
1023
-
1024
- > **Looking for a complete example with multiple files?** Check out our [Complete File Upload Guide](docs/mini-guides/file-uploads-complete.md) that shows multiple file fields, HTML forms, and static file serving.
1025
-
1026
- ## Transactions
1027
-
1028
- The JSON REST API supports database transactions for atomic operations across multiple resources. This is essential for maintaining data consistency when creating or updating related resources.
1029
-
1030
- ### Basic Transaction Usage
1031
-
1032
- ```javascript
1033
- // Manual transaction management
1034
- const trx = await api.knex.instance.transaction();
1035
- try {
1036
- // All operations share the same transaction
1037
- const author = await api.resources.people.post({
1038
- transaction: trx,
1039
- inputRecord: {
1040
- data: {
1041
- type: 'people',
1042
- attributes: { name: 'New Author' }
1043
- }
1044
- }
1045
- });
1046
-
1047
- const book = await api.resources.books.post({
1048
- transaction: trx,
1049
- inputRecord: {
1050
- data: {
1051
- type: 'books',
1052
- attributes: { title: 'New Book' },
1053
- relationships: {
1054
- author: { data: { type: 'people', id: author.data.id } }
1055
- }
1056
- }
1057
- }
1058
- });
1059
-
1060
- // Commit if everything succeeds
1061
- await trx.commit();
1062
- return { author, book };
1063
-
1064
- } catch (error) {
1065
- // Rollback on any error
1066
- await trx.rollback();
1067
- throw error;
1068
- }
1069
- ```
1070
-
1071
- ### Automatic Transaction Handling
1072
-
1073
- POST operations automatically use transactions when creating resources with many-to-many relationships:
1074
-
1075
- ```javascript
1076
- // This single POST will use a transaction internally
1077
- const book = await api.resources.books.post({
1078
- inputRecord: {
1079
- data: {
1080
- type: 'books',
1081
- attributes: { title: 'Tagged Book' },
1082
- relationships: {
1083
- tags: {
1084
- data: [
1085
- { type: 'tags', id: '1' },
1086
- { type: 'tags', id: '2' }
1087
- ]
1088
- }
1089
- }
1090
- }
1091
- }
1092
- });
1093
- // The book and pivot table entries are created atomically
1094
- ```
1095
-
1096
- ### Nested Transactions
1097
-
1098
- The system intelligently handles nested transactions by reusing existing ones:
1099
-
1100
- ```javascript
1101
- const trx = await api.knex.instance.transaction();
1102
- try {
1103
- // This POST will use the existing transaction
1104
- const book = await api.resources.books.post({
1105
- transaction: trx,
1106
- inputRecord: {
1107
- data: {
1108
- type: 'books',
1109
- attributes: { title: 'Book with Tags' },
1110
- relationships: {
1111
- tags: { data: [{ type: 'tags', id: '1' }] }
1112
- }
1113
- }
1114
- }
1115
- });
1116
- // Even though POST creates its own transaction for many-to-many,
1117
- // it detects and uses the existing one
1118
-
1119
- await trx.commit();
1120
- } catch (error) {
1121
- await trx.rollback();
1122
- }
1123
- ```
1124
-
1125
- ### Transaction Support Across Methods
1126
-
1127
- All REST API methods support transactions:
1128
-
1129
- ```javascript
1130
- const trx = await api.knex.instance.transaction();
1131
- try {
1132
- // Query with transaction
1133
- const books = await api.resources.books.query({
1134
- transaction: trx,
1135
- queryParams: { filters: { status: 'draft' } }
1136
- });
1137
-
1138
- // Update with transaction
1139
- await api.resources.books.patch({
1140
- transaction: trx,
1141
- id: '123',
1142
- inputRecord: {
1143
- data: {
1144
- type: 'books',
1145
- id: '123',
1146
- attributes: { status: 'published' }
1147
- }
1148
- }
1149
- });
1150
-
1151
- // Delete with transaction
1152
- await api.resources.drafts.delete({
1153
- transaction: trx,
1154
- id: '456'
1155
- });
1156
-
1157
- await trx.commit();
1158
- } catch (error) {
1159
- await trx.rollback();
1160
- }
1161
- ```
1162
-
1163
- ## Updating Resources with Relationships
1164
-
1165
- The REST API plugin now fully supports updating relationships in PUT and PATCH operations according to JSON:API specifications.
1166
-
1167
- ### PUT - Complete Replacement
1168
-
1169
- PUT replaces the entire resource, including all relationships. Any relationships not provided in the request will be removed.
1170
-
1171
- ```javascript
1172
- // Example: Update an article with new author and categories
1173
- const updatedArticle = await api.resources.articles.put({
1174
- id: '123',
1175
- inputRecord: {
1176
- data: {
1177
- type: 'articles',
1178
- id: '123',
1179
- attributes: {
1180
- title: 'Updated Title',
1181
- body: 'New content'
1182
- },
1183
- relationships: {
1184
- // 1:1 relationship - will update author_id in the database
1185
- author: {
1186
- data: { type: 'users', id: '42' }
1187
- },
1188
- // n:n relationship - will replace ALL category associations
1189
- categories: {
1190
- data: [
1191
- { type: 'categories', id: '1' },
1192
- { type: 'categories', id: '3' }
1193
- ]
1194
- }
1195
- // Note: any other relationships not listed here will be cleared!
1196
- }
1197
- }
1198
- }
1199
- });
1200
- ```
1201
-
1202
- ### PATCH - Partial Update
1203
-
1204
- PATCH only updates the fields and relationships explicitly provided, leaving everything else unchanged.
1205
-
1206
- ```javascript
1207
- // Example: Update only the author, keeping categories unchanged
1208
- const patchedArticle = await api.resources.articles.patch({
1209
- id: '123',
1210
- inputRecord: {
1211
- data: {
1212
- type: 'articles',
1213
- id: '123',
1214
- relationships: {
1215
- // Only update the author relationship
1216
- author: {
1217
- data: { type: 'users', id: '99' }
1218
- }
1219
- // categories relationship remains untouched
1220
- }
1221
- }
1222
- }
1223
- });
1224
-
1225
- // Example: Clear a relationship by setting it to null
1226
- const clearedRelationship = await api.resources.articles.patch({
1227
- id: '123',
1228
- inputRecord: {
1229
- data: {
1230
- type: 'articles',
1231
- id: '123',
1232
- relationships: {
1233
- reviewer: {
1234
- data: null // Removes the reviewer
1235
- }
1236
- }
1237
- }
1238
- }
1239
- });
1240
-
1241
- // Example: Update many-to-many relationship
1242
- const updatedCategories = await api.resources.articles.patch({
1243
- id: '123',
1244
- inputRecord: {
1245
- data: {
1246
- type: 'articles',
1247
- id: '123',
1248
- relationships: {
1249
- // This will replace the categories for this article
1250
- categories: {
1251
- data: [
1252
- { type: 'categories', id: '5' },
1253
- { type: 'categories', id: '7' },
1254
- { type: 'categories', id: '9' }
1255
- ]
1256
- }
1257
- }
1258
- }
1259
- }
1260
- });
1261
- ```
1262
-
1263
- ### Transaction Support for Relationship Updates
1264
-
1265
- All relationship updates are performed within the same transaction as the main record update, ensuring data consistency:
1266
-
1267
- ```javascript
1268
- // This entire operation is atomic
1269
- const result = await api.resources.articles.put({
1270
- id: '123',
1271
- inputRecord: {
1272
- data: {
1273
- type: 'articles',
1274
- id: '123',
1275
- attributes: {
1276
- title: 'New Title',
1277
- status: 'published'
1278
- },
1279
- relationships: {
1280
- author: {
1281
- data: { type: 'users', id: '10' }
1282
- },
1283
- categories: {
1284
- data: [
1285
- { type: 'categories', id: '1' },
1286
- { type: 'categories', id: '2' }
1287
- ]
1288
- },
1289
- tags: {
1290
- data: [
1291
- { type: 'tags', id: '100' },
1292
- { type: 'tags', id: '101' }
1293
- ]
1294
- }
1295
- }
1296
- }
1297
- }
1298
- });
1299
- // If any part fails, everything is rolled back
1300
- ```
1301
-
1302
- ## Database Configuration with MySQL
1303
-
1304
- The REST API Knex Plugin supports MySQL databases out of the box. Here's how to configure your API to use MySQL:
1305
-
1306
- ### Installing MySQL Driver
1307
-
1308
- First, install the MySQL driver for Knex:
1309
-
1310
- ```bash
1311
- npm install mysql2
1312
- ```
1313
-
1314
- ### Basic MySQL Configuration
1315
-
1316
- ```javascript
1317
- import { Api } from 'hooked-api';
1318
- import { RestApiPlugin, RestApiKnexPlugin, HttpPlugin } from 'json-rest-api';
1319
- import knex from 'knex';
1320
-
1321
- // Create a Knex instance with MySQL configuration
1322
- const db = knex({
1323
- client: 'mysql2',
1324
- connection: {
1325
- host: 'localhost',
1326
- port: 3306,
1327
- user: 'your_database_user',
1328
- password: 'your_database_password',
1329
- database: 'your_database_name'
1330
- },
1331
- pool: {
1332
- min: 2,
1333
- max: 10
1334
- }
1335
- });
1336
-
1337
- // Create the API
1338
- const api = new Api({ name: 'my-api' });
1339
-
1340
- // Install plugins
1341
- await api.use(RestApiPlugin);
1342
- await api.use(RestApiKnexPlugin, { knex: db });
1343
- await api.use(HttpPlugin);
1344
-
1345
- // The Knex instance is now available as api.knex for direct queries
1346
- console.log('Database connected:', await api.knex.raw('SELECT 1+1 AS result'));
5
+ [Official Website](https://mobily-enterprises.github.io/json-rest-api/)
1347
6
 
1348
- // Add a sample resource
1349
- api.addResource('users', {
1350
- schema: {
1351
- id: { type: 'id' },
1352
- name: { type: 'string', required: true },
1353
- email: { type: 'string', required: true }
1354
- }
1355
- });
1356
-
1357
- // Create the users table if it doesn't exist
1358
- await api.knex.schema.createTableIfNotExists('users', (table) => {
1359
- table.increments('id');
1360
- table.string('name');
1361
- table.string('email');
1362
- table.timestamps(true, true);
1363
- });
7
+ ## Features
1364
8
 
1365
- // Start the HTTP server
1366
- api.http.startServer();
1367
- console.log('API server running at http://localhost:3000/api');
1368
- console.log('Try: curl http://localhost:3000/api/users');
1369
- ```
1370
-
1371
- ### Testing the API with curl
9
+ * **JSON:API Compliant** - Full support for the JSON:API specification
10
+ * **Relationship Support** - `belongsTo`, `hasMany`, and many-to-many relationships, including polymorphic
11
+ * **Advanced Querying** - Filtering, sorting, pagination, and field selection (sparse fieldsets)
12
+ * **File Uploads** - Built-in support for file handling with multiple storage adapters (local, S3)
13
+ * **Framework Agnostic** - Works with raw Node.js HTTP, Express, and other Node.js frameworks via flexible connectors
14
+ * **Validation** - Schema-based validation with detailed error messages and custom rules
15
+ * **Simplified Mode** - A developer-friendly option to work with plain JavaScript objects instead of verbose JSON:API structure
16
+ * **Extensible** - Built on `hooked-api`'s powerful plugin and hook system for deep customization
1372
17
 
1373
- Once your server is running, you can test it with these curl commands:
18
+ ## Installation
1374
19
 
1375
20
  ```bash
1376
- # Get all users (initially empty)
1377
- curl http://localhost:3000/api/users
1378
-
1379
- # Create a new user
1380
- curl -X POST http://localhost:3000/api/users \
1381
- -H "Content-Type: application/json" \
1382
- -d '{
1383
- "data": {
1384
- "type": "users",
1385
- "attributes": {
1386
- "name": "John Doe",
1387
- "email": "john@example.com"
1388
- }
1389
- }
1390
- }'
1391
-
1392
- # Get all users again (should show the created user)
1393
- curl http://localhost:3000/api/users
1394
-
1395
- # Get a specific user
1396
- curl http://localhost:3000/api/users/1
1397
-
1398
- # Update a user
1399
- curl -X PATCH http://localhost:3000/api/users/1 \
1400
- -H "Content-Type: application/json" \
1401
- -d '{
1402
- "data": {
1403
- "type": "users",
1404
- "id": "1",
1405
- "attributes": {
1406
- "name": "John Smith"
1407
- }
1408
- }
1409
- }'
1410
-
1411
- # Delete a user
1412
- curl -X DELETE http://localhost:3000/api/users/1
1413
- ```
1414
-
1415
- ### Using Environment Variables
1416
-
1417
- For production deployments, use environment variables to store sensitive credentials:
1418
-
1419
- ```javascript
1420
- // Load environment variables (using dotenv package)
1421
- import dotenv from 'dotenv';
1422
- dotenv.config();
1423
-
1424
- const db = knex({
1425
- client: 'mysql2',
1426
- connection: {
1427
- host: process.env.DB_HOST || 'localhost',
1428
- port: process.env.DB_PORT || 3306,
1429
- user: process.env.DB_USER,
1430
- password: process.env.DB_PASSWORD,
1431
- database: process.env.DB_NAME
1432
- },
1433
- pool: {
1434
- min: 2,
1435
- max: 10
1436
- }
1437
- });
1438
- ```
1439
-
1440
- Create a `.env` file (remember to add it to `.gitignore`):
1441
-
1442
- ```
1443
- DB_HOST=localhost
1444
- DB_PORT=3306
1445
- DB_USER=myapp_user
1446
- DB_PASSWORD=secret_password
1447
- DB_NAME=myapp_db
1448
- ```
1449
-
1450
- ### Connection URL Format
1451
-
1452
- You can also use a connection URL:
1453
-
1454
- ```javascript
1455
- const db = knex({
1456
- client: 'mysql2',
1457
- connection: process.env.DATABASE_URL || 'mysql://user:pass@localhost:3306/dbname'
1458
- });
1459
- ```
1460
-
1461
- ### Advanced MySQL Configuration
1462
-
1463
- For production environments, consider these additional options:
1464
-
1465
- ```javascript
1466
- const db = knex({
1467
- client: 'mysql2',
1468
- connection: {
1469
- host: process.env.DB_HOST,
1470
- port: process.env.DB_PORT,
1471
- user: process.env.DB_USER,
1472
- password: process.env.DB_PASSWORD,
1473
- database: process.env.DB_NAME,
1474
- ssl: {
1475
- // For MySQL servers with SSL/TLS
1476
- rejectUnauthorized: true,
1477
- ca: fs.readFileSync('path/to/ca.pem')
1478
- },
1479
- timezone: 'UTC',
1480
- charset: 'utf8mb4',
1481
- connectTimeout: 60000, // 60 seconds
1482
- stringifyObjects: false,
1483
- multipleStatements: true
1484
- },
1485
- pool: {
1486
- min: 2,
1487
- max: 10,
1488
- createTimeoutMillis: 30000,
1489
- acquireTimeoutMillis: 30000,
1490
- idleTimeoutMillis: 30000,
1491
- reapIntervalMillis: 1000,
1492
- createRetryIntervalMillis: 100
1493
- },
1494
- acquireConnectionTimeout: 60000,
1495
- debug: process.env.NODE_ENV === 'development'
1496
- });
1497
- ```
1498
-
1499
- ### Creating Tables
1500
-
1501
- Once connected, you can create tables using Knex migrations or directly:
1502
-
1503
- ```javascript
1504
- // Create a books table
1505
- await api.knex.schema.createTable('books', (table) => {
1506
- table.increments('id').primary();
1507
- table.string('title', 255).notNullable();
1508
- table.string('author', 255).notNullable();
1509
- table.integer('year').unsigned();
1510
- table.string('isbn', 20).unique();
1511
- table.text('description');
1512
- table.decimal('price', 10, 2);
1513
- table.boolean('in_stock').defaultTo(true);
1514
- table.timestamps(true, true); // created_at, updated_at
1515
-
1516
- // Indexes for better query performance
1517
- table.index(['author']);
1518
- table.index(['year']);
1519
- });
21
+ npm install json-rest-api hooked-api json-rest-schema knex sqlite3
1520
22
  ```
1521
23
 
1522
- ### Complete Example with MySQL
24
+ > **Note:** `json-rest-schema` and a database driver like `knex` and `sqlite3` are peer dependencies, meaning you need to install them explicitly. This gives you control over your database choice.
1523
25
 
1524
- Here's a complete working example:
26
+ ## Quick Start
1525
27
 
1526
28
  ```javascript
1527
29
  import { Api } from 'hooked-api';
1528
- import { RestApiPlugin, RestApiKnexPlugin, HttpPlugin } from 'json-rest-api';
30
+ import { RestApiPlugin } from 'json-rest-api/plugins/core/rest-api-plugin.js';
31
+ import { RestApiKnexPlugin } from 'json-rest-api/plugins/core/rest-api-knex-plugin.js';
32
+ import { ExpressPlugin } from 'json-rest-api/plugins/core/connectors/express-plugin.js';
33
+ import express from 'express';
1529
34
  import knex from 'knex';
1530
- import dotenv from 'dotenv';
1531
35
 
1532
- // Load environment variables
1533
- dotenv.config();
36
+ // Create API instance
37
+ const api = new Api({ name: 'my-api' });
1534
38
 
1535
- // Configure MySQL connection
39
+ // Set up database
1536
40
  const db = knex({
1537
- client: 'mysql2',
1538
- connection: {
1539
- host: process.env.DB_HOST || 'localhost',
1540
- port: process.env.DB_PORT || 3306,
1541
- user: process.env.DB_USER || 'root',
1542
- password: process.env.DB_PASSWORD || '',
1543
- database: process.env.DB_NAME || 'bookstore'
1544
- },
1545
- pool: { min: 2, max: 10 }
41
+ client: 'sqlite3',
42
+ connection: { filename: ':memory:' },
43
+ useNullAsDefault: true
1546
44
  });
1547
45
 
1548
- // Create API instance
1549
- const api = new Api({ name: 'bookstore-api' });
1550
-
1551
46
  // Install plugins
1552
47
  await api.use(RestApiPlugin);
1553
48
  await api.use(RestApiKnexPlugin, { knex: db });
1554
- await api.use(HttpPlugin);
1555
-
1556
- // Define the books resource with filtering
1557
- api.addResource('books', {
1558
- schema: {
1559
- id: { type: 'id' },
1560
- title: {
1561
- type: 'string',
1562
- required: true,
1563
- search: { filterUsing: 'like' } // Enable LIKE filtering
1564
- },
1565
- author: {
1566
- type: 'string',
1567
- required: true,
1568
- search: true // Enable exact match filtering
1569
- },
1570
- year: {
1571
- type: 'number',
1572
- search: {
1573
- year_min: { filterUsing: '>=' },
1574
- year_max: { filterUsing: '<=' }
1575
- }
1576
- },
1577
- price: {
1578
- type: 'decimal',
1579
- search: true
1580
- },
1581
- in_stock: {
1582
- type: 'boolean',
1583
- search: true
1584
- }
1585
- },
1586
- sortableFields: ['title', 'author', 'year', 'price'],
1587
- tableName: 'books' // Explicitly set table name (optional)
1588
- });
1589
-
1590
- // Start the HTTP server
1591
- api.http.startServer(3000);
1592
- console.log('API running at http://localhost:3000/api');
1593
-
1594
- // Test the database connection
1595
- try {
1596
- await db.raw('SELECT 1+1 AS result');
1597
- console.log('Database connected successfully');
1598
- } catch (error) {
1599
- console.error('Database connection failed:', error);
1600
- }
1601
- ```
1602
-
1603
- ### Troubleshooting MySQL Connections
1604
-
1605
- Common issues and solutions:
1606
-
1607
- 1. **Authentication Error**: If you get "ER_NOT_SUPPORTED_AUTH_MODE", your MySQL 8.0 server might be using the newer authentication plugin. Fix:
1608
- ```sql
1609
- ALTER USER 'your_user'@'localhost' IDENTIFIED WITH mysql_native_password BY 'your_password';
1610
- FLUSH PRIVILEGES;
1611
- ```
1612
-
1613
- 2. **Connection Timeout**: Increase the `connectTimeout` in your configuration.
1614
-
1615
- 3. **SSL/TLS Issues**: For cloud MySQL instances (AWS RDS, Google Cloud SQL), you may need to configure SSL:
1616
- ```javascript
1617
- connection: {
1618
- // ... other config
1619
- ssl: {
1620
- rejectUnauthorized: true,
1621
- ca: fs.readFileSync('path/to/server-ca.pem')
1622
- }
1623
- }
1624
- ```
1625
-
1626
- 4. **Character Set Issues**: Use `utf8mb4` for full Unicode support including emojis:
1627
- ```javascript
1628
- connection: {
1629
- // ... other config
1630
- charset: 'utf8mb4'
1631
- }
1632
- ```
1633
-
1634
- ### Direct Database Access
1635
-
1636
- The plugin exposes the Knex instance as `api.knex`, allowing you to run complex queries directly:
1637
-
1638
- ```javascript
1639
- // Complex query with joins
1640
- const results = await api.knex('books')
1641
- .join('authors', 'books.author_id', 'authors.id')
1642
- .select('books.*', 'authors.name as author_name')
1643
- .where('books.year', '>', 2020)
1644
- .orderBy('books.created_at', 'desc')
1645
- .limit(10);
1646
-
1647
- // Raw SQL queries
1648
- const stats = await api.knex.raw(`
1649
- SELECT
1650
- COUNT(*) as total_books,
1651
- AVG(price) as avg_price,
1652
- MAX(price) as max_price
1653
- FROM books
1654
- WHERE in_stock = ?
1655
- `, [true]);
1656
-
1657
- // Transactions
1658
- await api.knex.transaction(async (trx) => {
1659
- const [authorId] = await trx('authors').insert({ name: 'New Author' });
1660
- await trx('books').insert({
1661
- title: 'New Book',
1662
- author_id: authorId,
1663
- year: 2024
1664
- });
1665
- });
1666
- ```
1667
-
1668
- With this configuration, your REST API will automatically use MySQL for all database operations, with full support for filtering, sorting, and pagination as described in the previous sections.
1669
-
1670
- ### Advanced Filtering with Hooks
1671
-
1672
- The REST API Knex Plugin supports extensible filtering through hooks. When adding custom filters, always use grouping to prevent accidental security bypasses:
1673
-
1674
- ```javascript
1675
- // Add a tenant isolation filter
1676
- api.addHook('knexQueryFiltering', 'tenantFilter', { order: -100 }, ({ query }) => {
1677
- query.where(function() {
1678
- this.where('tenant_id', getCurrentTenant());
1679
- this.where('deleted_at', null);
1680
- });
1681
- });
1682
-
1683
- // Add a complex search filter
1684
- api.addHook('knexQueryFiltering', 'searchFilter', {}, ({ query, filters }) => {
1685
- if (filters.search) {
1686
- query.where(function() {
1687
- const term = `%${filters.search}%`;
1688
- this.where('title', 'like', term)
1689
- .orWhere('description', 'like', term)
1690
- .orWhere('tags', 'like', term);
1691
- });
1692
- }
1693
- });
1694
-
1695
- // Add region-based filtering
1696
- api.addHook('knexQueryFiltering', 'regionFilter', {}, ({ query }) => {
1697
- query.where(function() {
1698
- const userRegion = getUserRegion();
1699
- // Show items from user's region, global items, or items without a region
1700
- this.where('region', userRegion)
1701
- .orWhere('region', 'global')
1702
- .orWhereNull('region');
1703
- });
1704
- });
1705
- ```
1706
-
1707
- These filters will be combined with AND, ensuring that security filters cannot be bypassed:
1708
- - `WHERE (tenant_id = 123 AND deleted_at IS NULL) AND (title LIKE '%search%' OR description LIKE '%search%' OR tags LIKE '%search%') AND (region = 'US' OR region = 'global' OR region IS NULL)`
1709
-
1710
- **Important**: Always wrap your filter conditions in `query.where(function() { ... })` to ensure proper grouping. This prevents OR conditions from accidentally bypassing other filters.
1711
-
1712
- ## Many-to-Many Relationships
1713
-
1714
- Many-to-many relationships require a pivot table. In the JSON REST API system, the pivot table is treated as a full resource with its own schema and endpoints.
1715
-
1716
- ### Understanding Relationships and Including Related Data
1717
-
1718
- Before diving into many-to-many relationships, it's important to understand how relationships work in the JSON REST API system.
1719
-
1720
- #### Relationship Types
1721
-
1722
- The system supports three types of relationships:
1723
-
1724
- 1. **belongsTo** (Many-to-One) - A foreign key relationship where a resource belongs to another resource
1725
- 2. **hasMany** (One-to-Many) - The inverse of belongsTo, where a resource has many related resources
1726
- 3. **hasMany with through** (Many-to-Many) - A relationship through a pivot/junction table
1727
-
1728
- #### Including Related Resources with the Include Parameter
1729
-
1730
- You can fetch related resources in a single request using the `include` query parameter. This follows the JSON:API specification and helps prevent N+1 query problems.
1731
-
1732
- ```javascript
1733
- // Example: Get a book with its author included
1734
- GET /api/books/1?include=author
1735
-
1736
- // Example: Get a book with nested includes
1737
- GET /api/books/1?include=author.company,reviews.reviewer
1738
- ```
1739
-
1740
- #### Defining Relationships
1741
-
1742
- ##### For belongsTo Relationships
49
+ await api.use(ExpressPlugin, { app: express() });
1743
50
 
1744
- ```javascript
1745
- api.addResource('books', {
1746
- schema: {
1747
- author_id: {
1748
- belongsTo: 'authors',
1749
- as: 'author'
1750
- }
1751
- }
1752
- });
1753
-
1754
- // This allows:
1755
- // GET /api/books?include=author
1756
- ```
1757
-
1758
- ##### For hasMany Relationships
1759
-
1760
- ```javascript
1761
- api.addResource('authors', {
1762
- schema: {
1763
- id: { type: 'id' },
1764
- name: { type: 'string' }
1765
- },
1766
- relationships: {
1767
- books: {
1768
- hasMany: 'books',
1769
- foreignKey: 'author_id'
1770
- }
1771
- }
1772
- });
1773
-
1774
- // This allows:
1775
- // GET /api/authors/1?include=books
1776
- ```
1777
-
1778
- ##### For Many-to-Many Relationships
1779
-
1780
- Many-to-many relationships use `hasMany` with `through` to specify the pivot table:
1781
-
1782
- ```javascript
1783
- api.addResource('books', {
1784
- schema: {
1785
- id: { type: 'id' },
1786
- title: { type: 'string' }
1787
- },
1788
- relationships: {
1789
- tags: {
1790
- hasMany: 'tags',
1791
- through: 'book_tags', // Pivot table
1792
- foreignKey: 'book_id', // Key for this resource
1793
- otherKey: 'tag_id' // Key for related resource
1794
- }
1795
- }
1796
- });
1797
-
1798
- // This allows:
1799
- // GET /api/books/1?include=tags
1800
- ```
1801
-
1802
- #### Important Notes on Including Relationships
1803
-
1804
- 1. **Performance Consideration**: Including many related resources can impact performance. Be mindful when including hasMany relationships.
1805
-
1806
- 2. **Nested Includes**: You can include nested relationships using dot notation:
1807
- ```
1808
- GET /api/books/1?include=author.company,reviews.reviewer.department
1809
- ```
1810
-
1811
- 3. **Sparse Fieldsets**: Combine includes with sparse fieldsets to fetch only needed fields:
1812
- ```
1813
- GET /api/books/1?include=author&fields[authors]=name,email
1814
- ```
1815
-
1816
- 4. **Cross-Table Search**: You can define searchSchema fields that reference related tables:
1817
- ```javascript
1818
- api.addResource('books', {
1819
- schema: { /* ... */ },
1820
- searchSchema: {
1821
- authorName: {
1822
- type: 'string',
1823
- actualField: 'authors.name', // Reference field in related table
1824
- filterUsing: 'like'
1825
- }
1826
- }
1827
- });
1828
- ```
1829
-
1830
- ### Books with Multiple Authors Example
1831
-
1832
- Let's update our books example to support multiple authors. Previously, books had a single `author` field. Now we'll create a many-to-many relationship between `books` and `people` (authors).
1833
-
1834
- #### Step 1: Define the Pivot Resource
1835
-
1836
- ```javascript
1837
- // Define the book_authors pivot table as a resource
1838
- api.addResource('book_authors', {
1839
- schema: {
1840
- id: { type: 'id' },
1841
- book_id: {
1842
- type: 'number',
1843
- belongsTo: 'books',
1844
- as: 'book'
1845
- },
1846
- author_id: {
1847
- type: 'number',
1848
- belongsTo: 'people',
1849
- as: 'author'
1850
- },
1851
- author_order: { type: 'number' }, // Display order for multiple authors
1852
- contribution_type: { type: 'string' } // 'primary', 'co-author', 'contributor'
1853
- },
1854
- searchSchema: {
1855
- // Search by book title
1856
- bookTitle: {
1857
- type: 'string',
1858
- actualField: 'books.title',
1859
- filterUsing: 'like'
1860
- },
1861
- // Search by author name
1862
- authorName: {
1863
- type: 'string',
1864
- actualField: 'people.name',
1865
- filterUsing: 'like'
1866
- }
1867
- }
1868
- });
1869
- ```
1870
-
1871
- #### Step 2: Update Books and People Resources
1872
-
1873
- ```javascript
1874
- // Update books resource (remove single author field)
1875
- api.addResource('books', {
1876
- schema: {
1877
- id: { type: 'id' },
1878
- title: { type: 'string', required: true, indexed: true },
1879
- year: { type: 'number' },
1880
- isbn: { type: 'string' }
1881
- // Note: removed 'author' field - now using many-to-many
1882
- },
1883
- relationships: {
1884
- authors: {
1885
- hasMany: 'book_authors',
1886
- foreignKey: 'book_id',
1887
- as: 'authors'
1888
- }
1889
- }
1890
- });
1891
-
1892
- // Define people resource
1893
- api.addResource('people', {
1894
- schema: {
1895
- id: { type: 'id' },
1896
- name: { type: 'string', required: true, indexed: true },
1897
- email: { type: 'string' }
1898
- },
1899
- relationships: {
1900
- books: {
1901
- hasMany: 'book_authors',
1902
- foreignKey: 'author_id',
1903
- as: 'books'
1904
- }
1905
- }
1906
- });
1907
- ```
1908
-
1909
- ### Using Many-to-Many Relationships
1910
-
1911
- #### NEW: Creating Books with Authors in One Request
1912
-
1913
- With the enhanced POST implementation, you can now create a book and establish many-to-many relationships in a single atomic request:
1914
-
1915
- ```javascript
1916
- // First, define the pivot table resource (with searchable fields for updates)
1917
- api.addResource('book_tags', {
1918
- schema: {
1919
- id: { type: 'id' },
1920
- book_id: { type: 'number', required: true, search: true },
1921
- tag_id: { type: 'number', required: true, search: true }
1922
- }
1923
- // Note: You can also define these in a separate searchSchema instead:
1924
- // searchSchema: {
1925
- // book_id: { type: 'number' },
1926
- // tag_id: { type: 'number' }
1927
- // }
1928
- });
1929
-
1930
- // Then define the tags resource
1931
- api.addResource('tags', {
1932
- schema: {
1933
- id: { type: 'id' },
1934
- name: { type: 'string', required: true }
1935
- }
1936
- });
1937
-
1938
- // Finally, define the books resource with the many-to-many relationship
1939
- api.addResource('books', {
1940
- schema: {
1941
- id: { type: 'id' },
1942
- title: { type: 'string', required: true },
1943
- year: { type: 'number' }
1944
- },
1945
- relationships: {
1946
- tags: {
1947
- hasMany: 'tags',
1948
- through: 'book_tags', // Pivot table resource
1949
- foreignKey: 'book_id', // Key for this resource
1950
- otherKey: 'tag_id' // Key for related resource
1951
- }
1952
- }
1953
- });
1954
-
1955
- // Create book with tags in one request
1956
- const response = await api.resources.books.post({
1957
- inputRecord: {
1958
- "data": {
1959
- "type": "books",
1960
- "attributes": {
1961
- "title": "Good Omens",
1962
- "year": 1990
1963
- },
1964
- "relationships": {
1965
- "tags": {
1966
- "data": [
1967
- { "type": "tags", "id": "1" }, // Must be existing tags
1968
- { "type": "tags", "id": "2" }
1969
- ]
1970
- }
1971
- }
1972
- }
1973
- }
1974
- });
1975
- ```
1976
-
1977
- This will atomically:
1978
- 1. Create the book
1979
- 2. Create entries in the `book_tags` pivot table
1980
- 3. Return the book with its relationships
1981
-
1982
- #### Traditional Method: Creating Relationships Separately
1983
-
1984
- Alternatively, you can still use the traditional approach:
1985
-
1986
- ```bash
1987
- # First, create a book
1988
- curl -X POST http://localhost:3000/api/books \
1989
- -H "Content-Type: application/json" \
1990
- -d '{
1991
- "data": {
1992
- "type": "books",
1993
- "attributes": {
1994
- "title": "Good Omens",
1995
- "year": 1990
1996
- }
1997
- }
1998
- }'
1999
-
2000
- # Then link authors to the book via pivot table
2001
- curl -X POST http://localhost:3000/api/book_authors \
2002
- -H "Content-Type: application/json" \
2003
- -d '{
2004
- "data": {
2005
- "type": "book_authors",
2006
- "attributes": {
2007
- "author_order": 1,
2008
- "contribution_type": "co-author"
2009
- },
2010
- "relationships": {
2011
- "book": { "data": { "type": "books", "id": "1" } },
2012
- "author": { "data": { "type": "people", "id": "1" } }
2013
- }
2014
- }
2015
- }'
2016
- ```
2017
-
2018
- #### Querying Books with Authors
2019
-
2020
- ```bash
2021
- # Get a book with all its authors
2022
- curl "http://localhost:3000/api/books/1?include=authors.author"
2023
-
2024
- # Search for books by author name
2025
- curl "http://localhost:3000/api/book_authors?filter[authorName]=Fitzgerald&include=book"
2026
-
2027
- # Get all books by a specific author
2028
- curl "http://localhost:3000/api/people/1?include=books.book"
2029
- ```
2030
-
2031
- #### Example Response
2032
-
2033
- ```json
2034
- {
2035
- "data": {
2036
- "type": "books",
2037
- "id": "1",
2038
- "attributes": {
2039
- "title": "Good Omens",
2040
- "year": 1990
2041
- },
2042
- "relationships": {
2043
- "authors": {
2044
- "data": [
2045
- { "type": "book_authors", "id": "1" },
2046
- { "type": "book_authors", "id": "2" }
2047
- ]
2048
- }
2049
- }
2050
- },
2051
- "included": [
2052
- {
2053
- "type": "book_authors",
2054
- "id": "1",
2055
- "attributes": {
2056
- "author_order": 1,
2057
- "contribution_type": "co-author"
2058
- },
2059
- "relationships": {
2060
- "author": { "data": { "type": "people", "id": "10" } }
2061
- }
2062
- },
2063
- {
2064
- "type": "book_authors",
2065
- "id": "2",
2066
- "attributes": {
2067
- "author_order": 2,
2068
- "contribution_type": "co-author"
2069
- },
2070
- "relationships": {
2071
- "author": { "data": { "type": "people", "id": "11" } }
2072
- }
2073
- },
2074
- {
2075
- "type": "people",
2076
- "id": "10",
2077
- "attributes": { "name": "Terry Pratchett" }
2078
- },
2079
- {
2080
- "type": "people",
2081
- "id": "11",
2082
- "attributes": { "name": "Neil Gaiman" }
2083
- }
2084
- ]
2085
- }
2086
- ```
2087
-
2088
- ## Polymorphic Relationships
2089
-
2090
- Polymorphic relationships allow a model to belong to multiple other models using a single association.
2091
-
2092
- ### Comments Example
2093
-
2094
- Let's add a commenting system where comments can be attached to books or articles.
2095
-
2096
- #### Define Comments with Polymorphic Relationship
2097
-
2098
- ```javascript
2099
- api.addResource('comments', {
2100
- schema: {
2101
- id: { type: 'id' },
2102
- body: { type: 'string', required: true },
2103
- user_id: {
2104
- type: 'number',
2105
- belongsTo: 'people',
2106
- as: 'author'
2107
- },
2108
- // Polymorphic relationship - defines both the relationship and the fields
2109
- commentable: {
2110
- belongsToPolymorphic: {
2111
- types: ['books', 'articles'],
2112
- typeField: 'commentable_type',
2113
- idField: 'commentable_id'
2114
- },
2115
- as: 'commentable'
2116
- }
2117
- },
2118
- searchSchema: {
2119
- // Search by title of the commented item
2120
- commentableTitle: {
2121
- type: 'string',
2122
- filterUsing: 'like',
2123
- polymorphicField: 'commentable',
2124
- targetFields: {
2125
- books: 'title',
2126
- articles: 'title'
51
+ // Define a resource
52
+ await api.addScope('articles', {
53
+ restApi: {
54
+ schema: {
55
+ attributes: {
56
+ title: { type: 'string', required: true },
57
+ content: { type: 'string' },
58
+ publishedAt: { type: 'string', format: 'date-time' }
2127
59
  }
2128
60
  }
2129
61
  }
2130
62
  });
2131
63
 
2132
- // Add reverse relationship to books
2133
- api.addResource('books', {
2134
- schema: {
2135
- // ... existing schema ...
2136
- },
2137
- relationships: {
2138
- // ... existing relationships ...
2139
- comments: {
2140
- hasMany: 'comments',
2141
- via: 'commentable',
2142
- as: 'comments'
2143
- }
2144
- }
2145
- });
2146
-
2147
- // Define articles resource
2148
- api.addResource('articles', {
2149
- schema: {
2150
- id: { type: 'id' },
2151
- title: { type: 'string', required: true, indexed: true },
2152
- content: { type: 'text' }
2153
- },
2154
- relationships: {
2155
- comments: {
2156
- hasMany: 'comments',
2157
- via: 'commentable',
2158
- as: 'comments'
2159
- }
2160
- }
2161
- });
2162
- ```
2163
-
2164
- ### Using Polymorphic Relationships
2165
-
2166
- #### Creating Comments
2167
-
2168
- ```bash
2169
- # Comment on a book
2170
- curl -X POST http://localhost:3000/api/comments \
2171
- -H "Content-Type: application/json" \
2172
- -d '{
2173
- "data": {
2174
- "type": "comments",
2175
- "attributes": {
2176
- "body": "This book changed my perspective!"
2177
- },
2178
- "relationships": {
2179
- "author": { "data": { "type": "people", "id": "1" } },
2180
- "commentable": { "data": { "type": "books", "id": "1" } }
2181
- }
2182
- }
2183
- }'
64
+ // Mount Express routes
65
+ const app = express();
66
+ api.http.express.mount(app);
67
+ app.listen(3000);
2184
68
 
2185
- # Comment on an article
2186
- curl -X POST http://localhost:3000/api/comments \
2187
- -H "Content-Type: application/json" \
2188
- -d '{
2189
- "data": {
2190
- "type": "comments",
2191
- "attributes": {
2192
- "body": "Great article!"
2193
- },
2194
- "relationships": {
2195
- "author": { "data": { "type": "people", "id": "1" } },
2196
- "commentable": { "data": { "type": "articles", "id": "5" } }
2197
- }
2198
- }
2199
- }'
69
+ // Your API is ready! Try:
70
+ // GET http://localhost:3000/api/articles
71
+ // POST http://localhost:3000/api/articles
2200
72
  ```
2201
73
 
2202
- #### Querying Polymorphic Relationships
74
+ ## Documentation
2203
75
 
2204
- ```bash
2205
- # Get all comments with their parent (book or article)
2206
- curl "http://localhost:3000/api/comments?include=commentable"
76
+ ### Core Guides
77
+ - [Initial Setup Guide](README_1_Initial_Setup.md) - Complete walkthrough for getting started
78
+ - [Computed and Hidden Fields](README_2_Computed_and_hidden_fields.md) - Advanced field handling
79
+ - [Hook System Guide](README_X_Hooks.md) - Extend and customize API behavior
80
+ - [API Tutorial](docs/GUIDE.md) - Step-by-step tutorial
2207
81
 
2208
- # Search comments on items with "REST" in title
2209
- curl "http://localhost:3000/api/comments?filter[commentableTitle]=REST"
82
+ ### Feature Guides
83
+ - [Permissions & Authentication](README_X_Permissions.md) - JWT auth and declarative permissions
84
+ - [CORS Configuration](README_X_Cors.md) - Cross-origin resource sharing setup
85
+ - [Relationships](README_X_Relationship_Plugin.md) - Working with related data
86
+ - [Bulk Operations](README_X_Bulk_Operations.md) - Efficient batch processing
87
+ - [File Uploads](docs/fileUploads.md) - Handling file uploads
88
+ - [WebSocket Support](README_X_SocketIO.md) - Real-time updates with Socket.IO
2210
89
 
2211
- # Get all comments for a specific book
2212
- curl "http://localhost:3000/api/books/1?include=comments.author"
2213
- ```
90
+ ### Advanced Topics
91
+ - [Polymorphic Relationships](README_8_Polymorphic_Relationships.md) - Flexible relationship patterns
92
+ - [Multi-tenancy](README_X_Multihome.md) - Isolating data by tenant
93
+ - [Advanced Relationships](RELATIONSHIPS_ADVANCED.md) - Complex relationship scenarios
2214
94
 
2215
- #### Example Response
95
+ ## License
2216
96
 
2217
- ```json
2218
- {
2219
- "data": [
2220
- {
2221
- "type": "comments",
2222
- "id": "1",
2223
- "attributes": {
2224
- "body": "Great book!"
2225
- },
2226
- "relationships": {
2227
- "commentable": { "data": { "type": "books", "id": "1" } }
2228
- }
2229
- },
2230
- {
2231
- "type": "comments",
2232
- "id": "2",
2233
- "attributes": {
2234
- "body": "Interesting article!"
2235
- },
2236
- "relationships": {
2237
- "commentable": { "data": { "type": "articles", "id": "5" } }
2238
- }
2239
- }
2240
- ],
2241
- "included": [
2242
- {
2243
- "type": "books",
2244
- "id": "1",
2245
- "attributes": { "title": "The Great Gatsby" }
2246
- },
2247
- {
2248
- "type": "articles",
2249
- "id": "5",
2250
- "attributes": { "title": "Understanding REST APIs" }
2251
- }
2252
- ]
2253
- }
2254
- ```
97
+ MIT
2255
98
 
2256
- ### How Polymorphic Search Works
2257
99
 
2258
- The system generates conditional JOINs for polymorphic searches:
2259
100
 
2260
- ```sql
2261
- -- GET /comments?filter[commentableTitle]=gatsby
2262
- SELECT comments.*
2263
- FROM comments
2264
- LEFT JOIN books AS comments_commentable_books
2265
- ON comments.commentable_type = 'books'
2266
- AND comments.commentable_id = comments_commentable_books.id
2267
- LEFT JOIN articles AS comments_commentable_articles
2268
- ON comments.commentable_type = 'articles'
2269
- AND comments.commentable_id = comments_commentable_articles.id
2270
- WHERE (
2271
- (comments.commentable_type = 'books'
2272
- AND comments_commentable_books.title LIKE '%gatsby%')
2273
- OR
2274
- (comments.commentable_type = 'articles'
2275
- AND comments_commentable_articles.title LIKE '%gatsby%')
2276
- )
2277
- ```