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
@@ -0,0 +1,1490 @@
1
+ # Hooks and Data Management
2
+
3
+ This guide provides a comprehensive reference for all hooks available in the json-rest-api system, including system-wide hooks from hooked-api and method-specific hooks from the REST API plugin.
4
+
5
+ An important concept when working with hooks is understanding how simplified mode affects data flow.
6
+
7
+ Inside hooks, JSON:API format is king _regardless of simplified mode_.
8
+ Simplified mode only ever affects:
9
+ - **Input**: How parameters are passed to the API (simplified data is converted to JSON:API before entering the lifecycle)
10
+ - **Output**: How the record is returned to the client (JSON:API data is converted to simplified format before returning)
11
+ Hook context _always_ contains: Full JSON:API formatted records with `data`, `type`, `attributes`, and `relationships`. So, as far as the hooks are concerned, `context.inputRecord` is _always_ a full JSON:API object.
12
+
13
+ This means when writing hooks, you always work with the standard JSON:API structure:
14
+ ```javascript
15
+ // In a hook, the record is ALWAYS JSON:API format:
16
+ hooks: {
17
+ beforeData: async ({ context }) => {
18
+ // Even in simplified mode, inputRecord has JSON:API structure
19
+ if (context.method === 'post' && context.inputRecord) {
20
+ // Always access via data.attributes
21
+ context.inputRecord.data.attributes.created_at = new Date().toISOString();
22
+ }
23
+ },
24
+
25
+ // IMPORTANT: Use enrichAttributes to modify attributes, NOT enrichRecord
26
+ enrichAttributes: async ({ context }) => {
27
+ // This is called for ALL records (main and included/child records)
28
+ // Add computed fields directly to context.attributes
29
+ context.attributes.computed_field = 'value';
30
+ context.attributes.word_count = context.attributes.content?.split(' ').length || 0;
31
+ }
32
+ }
33
+ ```
34
+
35
+ One of the main practical use of hooks is to manupulate data before it's committed to the database.
36
+
37
+ ## Customizing the API as a whole with customize()
38
+
39
+ The `customize()` method is the primary way to extend your API with hooks, variables, and helper functions. This method is available on the API instance and provides a cleaner alternative to calling individual methods like `addHook()`.
40
+
41
+ The `customize()` method accepts an object with the following properties:
42
+ - `hooks` - Hook handlers for various lifecycle events
43
+ - `vars` - Variables accessible throughout the API
44
+ - `helpers` - Reusable functions
45
+ - `apiMethods` - Methods added to the API instance
46
+ - `scopeMethods` - Methods added to _all_ scopes/resources
47
+
48
+ ### Basic Example
49
+
50
+ The `customize()` method accepts an object with hooks, vars (shared state), helpers (reusable functions), apiMethods (global methods), and scopeMethods (methods for all scopes):
51
+
52
+ ```javascript
53
+ api.customize({
54
+ // Shared variables accessible throughout the API
55
+ vars: {
56
+ appName: 'My Application',
57
+ userRoles: ['admin', 'editor', 'viewer'],
58
+ environment: process.env.NODE_ENV
59
+ },
60
+
61
+ // Reusable helper functions
62
+ helpers: {
63
+ formatDate: (date) => new Date(date).toLocaleDateString(),
64
+ validateEmail: (email) => /^[^\s@]+@[^\s@]+\.[^\s@]+$/.test(email),
65
+ hashPassword: async (password) => {
66
+ const salt = await bcrypt.genSalt(10);
67
+ return bcrypt.hash(password, salt);
68
+ }
69
+ },
70
+
71
+ // Hooks for customizing behavior
72
+ hooks: {
73
+ beforeData: async ({ context, vars, helpers, log }) => {
74
+ log.info(`${context.method} operation on ${context.scopeName}`);
75
+
76
+ // Use vars for configuration
77
+ if (vars.environment === 'production') {
78
+ // Production-specific logic
79
+ }
80
+
81
+ // Modify data for POST requests
82
+ if (context.method === 'post' && context.inputRecord) {
83
+ // Set timestamps
84
+ context.inputRecord.data.attributes.created_at = new Date().toISOString();
85
+
86
+ // Validate and transform data using helpers
87
+ if (context.scopeName === 'users') {
88
+ // Hash password
89
+ if (context.inputRecord.data.attributes.password) {
90
+ context.inputRecord.data.attributes.password = await helpers.hashPassword(
91
+ context.inputRecord.data.attributes.password
92
+ );
93
+ }
94
+
95
+ }
96
+ }
97
+ },
98
+
99
+ enrichAttributes: async ({ context }) => {
100
+ // Add computed fields to posts
101
+ if (context.scopeName === 'posts') {
102
+ // NOTE that 'preview' MUST be an existing record on the database
103
+ context.attributes.preview = context.attributes.content?.substring(0, 200) + '...';
104
+ }
105
+ }
106
+ },
107
+
108
+ // API-level methods: api.methodName()
109
+ apiMethods: {
110
+ healthCheck: async ({ vars, scopes }) => ({
111
+ status: 'ok',
112
+ version: vars.apiVersion || '1.0.0',
113
+ environment: vars.environment,
114
+ timestamp: new Date().toISOString(),
115
+ resources: Object.keys(scopes)
116
+ })
117
+ },
118
+
119
+ // Scope methods: api.scopes.resourceName.methodName()
120
+ scopeMethods: {
121
+ count: async ({ scopeName, db }) => {
122
+ const result = await db(scopeName).count('* as total').first();
123
+ return result.total;
124
+ }
125
+ }
126
+ });
127
+
128
+ // Usage examples:
129
+ const health = await api.healthCheck();
130
+ const userCount = await api.scopes.users.count();
131
+ ```
132
+
133
+ **Important**: Hooks added via `customize()` are registered with the plugin name `api-custom:{apiName}` for tracking purposes.
134
+
135
+ ## Customizing Resources with extras (per resource)
136
+
137
+ While `customize()` applies globally to all resources, you can customize individual resources using the `extras` parameter in `addScope()`. This provides resource-specific hooks, methods, variables, and helpers that apply **only to that particular resource**.
138
+
139
+ ### Using extras in addScope
140
+
141
+ ```javascript
142
+ api.addScope('posts', {
143
+ // Schema and options
144
+ }, {
145
+ // extras parameter - everything here applies ONLY to posts
146
+ hooks: {
147
+
148
+ // This will change data before getting posted
149
+ beforeDataPost: async ({ context }) => {
150
+ const attributes = context.inputRecord.data.attributes
151
+ // This hook only runs for POST operations on posts
152
+ attributes.created_at = new Date().toISOString();
153
+ },
154
+
155
+ // This will enrich all attributes (main and included) when fetched
156
+ // It's for adding computed fields to the RESPONSE, NOT for saving to database
157
+ // These computed fields are sent to the client but never stored
158
+ enrichAttributes: async ({ context }) => {
159
+ // This wordCount is calculated on-the-fly and included in the response
160
+ // It is NOT saved to the database
161
+ context.attributes.wordCount = context.attributes.content?.split(' ').length || 0;
162
+
163
+ // Example: Add a reading time estimate (also not stored)
164
+ const wordsPerMinute = 200;
165
+ context.attributes.readingTimeMinutes = Math.ceil(context.attributes.wordCount / wordsPerMinute);
166
+ }
167
+ },
168
+
169
+ // CLAUDE: NO. This should use patch, since direct database operations
170
+ // will skip the lifecycle (e.g. Sockets notifications etc.)
171
+ scopeMethods: {
172
+ // This method is ONLY available on posts: api.scopes.posts.publish()
173
+ publish: async ({ params, context, db, runHooks }) => {
174
+ const id = params.id;
175
+ const result = await db('posts')
176
+ .where('id', id)
177
+ .update({
178
+ status: 'published',
179
+ published_at: new Date().toISOString()
180
+ });
181
+ return { success: true, published: result };
182
+ },
183
+
184
+ // Only on posts: api.scopes.posts.findByAuthor()
185
+ findByAuthor: async ({ params, scope }) => {
186
+ return await scope.query({
187
+ filters: { author_id: params.authorId }
188
+ });
189
+ }
190
+ },
191
+
192
+ vars: {
193
+ // Resource-specific configuration
194
+ maxTitleLength: 200,
195
+ allowedStatuses: ['draft', 'published', 'archived'],
196
+ defaultStatus: 'draft'
197
+ },
198
+
199
+ helpers: {
200
+ // Resource-specific helper functions
201
+ generateSlug: (title) => {
202
+ return title.toLowerCase()
203
+ .replace(/[^a-z0-9]+/g, '-')
204
+ .replace(/^-|-$/g, '');
205
+ },
206
+
207
+ validateStatus: (status, vars) => {
208
+ // Note: can access vars through second parameter
209
+ return vars.allowedStatuses.includes(status);
210
+ }
211
+ }
212
+ });
213
+ ```
214
+
215
+ ### Variable and Helper Fallback System
216
+
217
+ An important feature of resource-specific vars and helpers is the **fallback system**:
218
+
219
+ 1. **Variables (vars)**: When you access a variable in a resource context, it first checks the resource's vars. If not found, it falls back to the global API vars.
220
+
221
+ ```javascript
222
+ // Global vars
223
+ api.customize({
224
+ vars: {
225
+ appName: 'My Blog',
226
+ defaultPageSize: 20,
227
+ maxUploadSize: 5242880 // 5MB
228
+ }
229
+ });
230
+
231
+ // Resource-specific vars
232
+ api.addScope('posts', {}, {
233
+ vars: {
234
+ defaultPageSize: 10, // Override for posts only
235
+ maxTitleLength: 200 // Posts-specific var
236
+ }
237
+ });
238
+
239
+ // In a posts hook or method:
240
+ // vars.defaultPageSize → 10 (from posts vars)
241
+ // vars.maxUploadSize → 5242880 (fallback to global)
242
+ // vars.maxTitleLength → 200 (posts-specific)
243
+ // vars.appName → 'My Blog' (fallback to global)
244
+ ```
245
+
246
+ 2. **Helpers**: Same fallback behavior - resource helpers are checked first, then global helpers.
247
+
248
+ ```javascript
249
+ // Global helpers
250
+ api.customize({
251
+ helpers: {
252
+ formatDate: (date) => new Date(date).toLocaleDateString(),
253
+ sanitizeHtml: (html) => { /* ... */ }
254
+ }
255
+ });
256
+
257
+ // Resource-specific helpers
258
+ api.addScope('posts', {}, {
259
+ helpers: {
260
+ formatDate: (date) => new Date(date).toISOString(), // Override for posts
261
+ generateExcerpt: (content) => content.substring(0, 150) + '...'
262
+ }
263
+ });
264
+
265
+ // In posts context:
266
+ // helpers.formatDate() → uses posts version (ISO format)
267
+ // helpers.sanitizeHtml() → uses global version (fallback)
268
+ // helpers.generateExcerpt() → posts-specific helper
269
+ ```
270
+
271
+ This means that you are able to specify api-wide variables and helpers, but can then override them by resource.
272
+
273
+ ### Resource-Specific vs Global Customization
274
+
275
+ | Feature | Global (`customize()`) | Resource-Specific (`extras`) |
276
+ |---------|----------------------|----------------------------|
277
+ | **Scope** | Applies to all resources | Applies to one resource only |
278
+ | **Hooks** | Must check `context.scopeName` | Automatically scoped |
279
+ | **Methods** | `apiMethods` → `api.methodName()`<br>`scopeMethods` → all scopes | `scopeMethods` → only this scope |
280
+ | **Vars** | Global defaults | Resource-specific with fallback |
281
+ | **Helpers** | Global utilities | Resource-specific with fallback |
282
+
283
+
284
+ ### Best Practices for Resource Customization
285
+
286
+ 1. **Use extras for resource-specific logic** - Don't clutter global hooks with scopeName checks
287
+ 2. **Leverage the fallback system** - Define common utilities globally, override when needed
288
+ 3. **Keep resource methods focused** - Methods should relate to that specific resource
289
+ 4. **Document resource-specific vars** - Make it clear what configuration is available
290
+ 5. **Avoid naming conflicts** - Be aware that resource vars/helpers can shadow global ones
291
+
292
+ ### Best Practices for Global Customization
293
+
294
+ 1. **Check scopeName in Hooks** - Since customize() is API-level only, use `context.scopeName` to implement resource-specific logic
295
+ 2. **Keep Helpers Pure** - Make helpers independent functions that are easier to test and reuse
296
+ 3. **Use Vars for Configuration** - Store configuration values in vars instead of hardcoding them
297
+ 4. **Avoid Mutable Shared State** - Be careful with objects/arrays in vars as they're shared across all requests
298
+ 5. **Handle Errors Gracefully** - Thrown errors in hooks will stop the operation and return to the client
299
+ 6. **Use Method-Specific Hooks** - Use `beforeDataPost`, `afterDataPatch`, etc. for operation-specific logic
300
+
301
+
302
+ ## REST API Method Hooks
303
+
304
+ These hooks are triggered by the REST API plugin during CRUD operations. Each method follows a consistent pattern but with method-specific variations.
305
+
306
+ ### Important Context Properties
307
+
308
+ The context object contains different properties depending on the method and stage of execution:
309
+
310
+ **Common Properties**:
311
+ - `method` (string) - The HTTP method: 'query', 'get', 'post', 'put', 'patch', or 'delete'
312
+ - `simplified` (boolean) - Whether simplified mode is active (affects input/output format, not hook data)
313
+ - `schemaInfo` (object) - Compiled schema information containing:
314
+ - `schema` - The full schema object
315
+ - `searchSchema` - Schema for filtering (query method)
316
+ - `tableName` - Database table name
317
+ - `idProperty` - Primary key field name
318
+ - `schemaRelationships` - Relationship definitions
319
+ - `scopeName` (string) - Name of the current resource scope
320
+ - `transaction` (object/null) - Database transaction if provided
321
+ - `db` (object) - Database connection (knex instance or transaction)
322
+ - `auth` (object) - Authentication context if provided
323
+
324
+ **Write Operation Properties** (POST/PUT/PATCH):
325
+ - `inputRecord` (object) - The JSON:API formatted input containing:
326
+ - `data.type` - Resource type
327
+ - `data.attributes` - Resource attributes (this is where you modify input data)
328
+ - `data.relationships` - Resource relationships
329
+ - `belongsToUpdates` (object) - Foreign key updates extracted from relationships
330
+ - `returnRecordSetting` (object) - Configuration for what to return
331
+
332
+ **Query/Read Properties**:
333
+ - `queryParams` (object) - Query parameters containing:
334
+ - `fields` - Sparse fieldset selections
335
+ - `include` - Relationships to include
336
+ - `sort` - Sort fields array (query only)
337
+ - `page` - Pagination settings (query only)
338
+ - `filters` - Filter conditions (query only)
339
+ - `record` (object) - The fetched/created record in JSON:API format
340
+ - `originalRecord` (object) - Backup of the record before modifications
341
+
342
+ ### Hook Execution Pattern
343
+
344
+ All REST API methods follow this general pattern:
345
+
346
+ 1. **Before hooks** - Run before the main operation
347
+ 2. **Permission checks** - For GET method
348
+ 3. **Main operation** - The actual database operation
349
+ 4. **After hooks** - Run after the operation
350
+ 5. **Enrichment hooks** - To enhance the record
351
+ 6. **Transaction hooks** - Commit/rollback for write operations
352
+ 7. **Finish hooks** - Final cleanup/processing
353
+
354
+ ## QUERY Method Hooks
355
+
356
+ Used for retrieving collections of resources with filtering, sorting, and pagination.
357
+
358
+ ### beforeData
359
+
360
+ **When**: Before executing the database query
361
+ **Purpose**: Modify query parameters, add custom filters, set defaults
362
+
363
+ **Context contains**:
364
+ - `method` (string) - "query"
365
+ - `simplified` (boolean) - Whether simplified mode is active (affects input/output format, not hook data)
366
+ - `schemaInfo` (object) - Compiled schema information
367
+ - `schema` - Full schema object
368
+ - `searchSchema` - Schema for filtering
369
+ - `tableName` - Database table name
370
+ - `idProperty` - Primary key field name
371
+ - `queryParams` (object) - Query parameters
372
+ - `fields` - Sparse fieldset selections
373
+ - `include` - Relationships to include
374
+ - `sort` - Sort fields array
375
+ - `page` - Pagination settings
376
+ - `filters` - Filter conditions
377
+ - `transaction` (object/null) - Database transaction if provided
378
+ - `db` (object) - Database connection (knex instance or transaction)
379
+ - `scopeName` (string) - Name of the current scope
380
+ - `sortableFields` (array) - Fields allowed for sorting
381
+ - `auth` (object) - Authentication context if provided
382
+
383
+ **What can be changed**:
384
+ - `queryParams.filters` - Modify filter conditions
385
+ - `queryParams.sort` - Modify sort order
386
+ - `queryParams.page` - Modify pagination
387
+ - `queryParams.fields` - Modify field selection
388
+ - `queryParams.include` - Modify includes
389
+ - Any custom properties added to context
390
+
391
+ **Example**:
392
+ ```javascript
393
+ // In api.addScope('posts', {}, extras):
394
+ hooks: {
395
+ beforeData: async ({ context }) => {
396
+ if (context.method === 'query' && context.auth?.userId) {
397
+ // Only show posts by the current user
398
+ context.queryParams.filters = {
399
+ ...context.queryParams.filters,
400
+ author_id: context.auth.userId
401
+ };
402
+ }
403
+ }
404
+ }
405
+ ```
406
+
407
+ ### beforeDataQuery
408
+
409
+ **When**: Immediately after `beforeData`, query-specific
410
+ **Purpose**: Query-specific modifications
411
+
412
+ **Context**: Same as `beforeData`
413
+ **What can be changed**: Same as `beforeData`
414
+
415
+ ### enrichRecord
416
+
417
+ **When**: After data is fetched from database and normalized
418
+ **Purpose**: Modify the response structure, add metadata, or handle response-level concerns
419
+
420
+ **IMPORTANT**: Do NOT use this hook to add/modify attributes. Use `enrichAttributes` instead.
421
+
422
+ **Context contains**:
423
+ - All previous context properties
424
+ - `record` (object) - The JSON:API formatted response
425
+ - `data` - Array of resource objects
426
+ - `included` - Array of included resources
427
+ - `meta` - Metadata (pagination, etc.)
428
+ - `links` - Pagination links
429
+ - `originalRecord` (object) - Backup of the record before enrichment
430
+
431
+ **What can be changed**:
432
+ - `record.meta` - Add or modify metadata
433
+ - `record.links` - Modify links
434
+ - Response structure modifications (but NOT attributes)
435
+ - Should NOT modify attributes - use `enrichAttributes` hook instead
436
+
437
+ **Example**:
438
+ ```javascript
439
+ hooks: {
440
+ enrichAttributes: async ({ context }) => {
441
+ if (context.parentContext?.method === 'query') {
442
+ // Add computed fields that are NOT stored in database
443
+ // These are calculated fresh for each response
444
+ context.attributes.wordCount = context.attributes.content?.split(' ').length || 0;
445
+ context.attributes.excerpt = context.attributes.content?.substring(0, 150) + '...';
446
+
447
+ // Transform display values (original remains in database)
448
+ // Database still has lowercase title, this is just for this response
449
+ context.attributes.displayTitle = context.attributes.title?.toUpperCase();
450
+ }
451
+ }
452
+ }
453
+ ```
454
+
455
+ ### finish
456
+
457
+ **When**: Before returning the response
458
+ **Purpose**: Final logging, metrics collection
459
+
460
+ **Context contains**: All previous context properties
461
+ **What can be changed**: Nothing - hooks should NOT change `context.record` at this stage
462
+
463
+ ### finishQuery
464
+
465
+ **When**: Immediately after `finish`, query-specific
466
+ **Purpose**: Query-specific final processing
467
+
468
+ **Context**: Same as `finish`
469
+ **What can be changed**: Nothing - informational only
470
+
471
+ ## GET Method Hooks
472
+
473
+ Used for retrieving a single resource by ID.
474
+
475
+ ### beforeData
476
+
477
+ **When**: Before fetching the single resource
478
+ **Purpose**: Modify query parameters, prepare for fetch
479
+
480
+ **Context contains**:
481
+ - `method` (string) - "get"
482
+ - `simplified` (boolean) - Whether simplified mode is active (affects input/output format, not hook data)
483
+ - `schemaInfo` (object) - Compiled schema information
484
+ - `queryParams` (object) - Query parameters
485
+ - `fields` - Sparse fieldset selections
486
+ - `include` - Relationships to include
487
+ - `transaction` (object/null) - Database transaction if provided
488
+ - `db` (object) - Database connection
489
+ - `scopeName` (string) - Name of the current scope
490
+ - `id` (string/number) - The ID of the resource to fetch
491
+ - `minimalRecord` (object) - Minimal record fetched for authorization
492
+ - `auth` (object) - Authentication context if provided
493
+
494
+ **What can be changed**:
495
+ - `queryParams.fields` - Modify field selection
496
+ - `queryParams.include` - Modify includes
497
+ - Custom context properties
498
+
499
+ ### beforeDataGet
500
+
501
+ **When**: Immediately after `beforeData`, get-specific
502
+ **Context**: Same as `beforeData`
503
+ **What can be changed**: Same as `beforeData`
504
+
505
+ ### checkDataPermissions
506
+
507
+ **When**: After the record is fetched, before enrichment
508
+ **Purpose**: Implement row-level security, check access permissions
509
+
510
+ **Context contains**:
511
+ - All previous context properties
512
+ - `record` (object) - The fetched JSON:API record
513
+
514
+ **What can be changed**:
515
+ - Can throw errors to deny access
516
+ - Should NOT modify the record
517
+
518
+ **Example**:
519
+ ```javascript
520
+ hooks: {
521
+ checkDataPermissions: async ({ context }) => {
522
+ if (context.method === 'get') {
523
+ const post = context.record.data;
524
+ if (post.attributes.status === 'draft' && post.attributes.author_id !== context.auth?.userId) {
525
+ throw new Error('Access denied: Cannot view draft posts by other authors');
526
+ }
527
+ }
528
+ }
529
+ }
530
+ ```
531
+
532
+ ### checkDataPermissionsGet
533
+
534
+ **When**: Immediately after `checkDataPermissions`, get-specific
535
+ **Context**: Same as `checkDataPermissions`
536
+ **What can be changed**: Same as `checkDataPermissions`
537
+
538
+ ### enrichRecord
539
+
540
+ **When**: After permission checks
541
+ **Purpose**: Modify the response structure or add metadata
542
+
543
+ **IMPORTANT**: Do NOT use this hook to add/modify attributes. Use `enrichAttributes` instead.
544
+
545
+ **Context contains**:
546
+ - All previous context properties
547
+ - `record` (object) - The JSON:API formatted response
548
+ - `originalRecord` (object) - Backup of the record
549
+ - `computedDependencies` (object) - Fields needed for computed fields
550
+
551
+ **What can be changed**:
552
+ - Response structure (but NOT attributes)
553
+ - `record.meta` - Add metadata
554
+ - Should NOT modify attributes - use `enrichAttributes` hook instead
555
+
556
+ ### enrichRecordWithRelationships
557
+
558
+ **When**: After basic enrichment
559
+ **Purpose**: Add relationship metadata, enhance relationship data
560
+
561
+ **Context**: Same as `enrichRecord`
562
+ **What can be changed**:
563
+ - Can modify relationship data
564
+ - Can add relationship metadata
565
+
566
+ ### finish
567
+
568
+ **When**: Before returning the response
569
+ **Context**: All accumulated context
570
+ **What can be changed**: Nothing - informational only
571
+
572
+ ### finishGet
573
+
574
+ **When**: Immediately after `finish`, get-specific
575
+ **Context**: Same as `finish`
576
+ **What can be changed**: Nothing - informational only
577
+
578
+ ## POST Method Hooks
579
+
580
+ Used for creating new resources.
581
+
582
+ ### beforeData
583
+
584
+ **When**: Before creating the new resource
585
+ **Purpose**: Validate data, set defaults, compute values
586
+
587
+ **Context contains**:
588
+ - `method` (string) - "post"
589
+ - `simplified` (boolean) - Whether simplified mode is active (affects input/output format, not hook data)
590
+ - `schemaInfo` (object) - Compiled schema information
591
+ - `inputRecord` (object) - The JSON:API formatted input data
592
+ - `data.type` - Resource type
593
+ - `data.attributes` - Resource attributes
594
+ - `data.relationships` - Resource relationships
595
+ - `params` (object) - Original parameters (may contain `returnFullRecord`)
596
+ - `queryParams` (object) - Contains `fields`, `include` for response
597
+ - `transaction` (object) - Database transaction (created if not provided)
598
+ - `shouldCommit` (boolean) - Whether to commit the transaction
599
+ - `db` (object) - Database connection
600
+ - `scopeName` (string) - Name of the current scope
601
+ - `returnRecordSetting` (object) - Settings for what to return
602
+ - `post` - 'no', 'minimal', or 'full'
603
+ - `put` - 'no', 'minimal', or 'full'
604
+ - `patch` - 'no', 'minimal', or 'full'
605
+ - `auth` (object) - Authentication context if provided
606
+
607
+ **What can be changed**:
608
+ - `inputRecord.data.attributes` - Modify validated attributes before insert
609
+ - `belongsToUpdates` - Modify foreign key values (but these are usually already merged into attributes)
610
+ - Custom context properties
611
+ - Can set defaults or compute values
612
+
613
+ **Note**: After validation, attributes are stored in `context.inputRecord.data.attributes`, not directly in `context.attributes`.
614
+
615
+ **Example**:
616
+ ```javascript
617
+ hooks: {
618
+ beforeData: async ({ context }) => {
619
+ if (context.method === 'post' && context.inputRecord) {
620
+ // Set default status
621
+ if (!context.inputRecord.data.attributes.status) {
622
+ context.inputRecord.data.attributes.status = 'draft';
623
+ }
624
+
625
+ // Set author from auth context (this would typically be in belongsToUpdates)
626
+ if (context.auth?.userId && context.belongsToUpdates) {
627
+ context.belongsToUpdates.author_id = context.auth.userId;
628
+ }
629
+
630
+ // Add creation timestamp
631
+ context.inputRecord.data.attributes.created_at = new Date().toISOString();
632
+ }
633
+ }
634
+ }
635
+ ```
636
+
637
+ ### beforeDataPost
638
+
639
+ **When**: Immediately after `beforeData`, post-specific
640
+ **Context**: Same as `beforeData`
641
+ **What can be changed**: Same as `beforeData`
642
+
643
+ ### afterData
644
+
645
+ **When**: After the resource is created in the database
646
+ **Purpose**: Trigger side effects, create related records
647
+
648
+ **Context contains**:
649
+ - All previous context properties
650
+ - `id` (string/number) - The ID of the created resource
651
+ - `newRecord` (object) - The raw database record
652
+
653
+ **What can be changed**:
654
+ - Can perform side effects (create related records, etc.)
655
+ - Can add properties to context for later hooks
656
+ - Should NOT modify `newRecord` directly
657
+
658
+ **Example**:
659
+ ```javascript
660
+ hooks: {
661
+ afterData: async ({ context, scopes }) => {
662
+ if (context.method === 'post') {
663
+ // Create a notification for new post
664
+ await scopes.notifications.create({
665
+ type: 'new_post',
666
+ post_id: context.id,
667
+ user_id: context.belongsToUpdates.author_id,
668
+ created_at: new Date().toISOString()
669
+ });
670
+ }
671
+ }
672
+ }
673
+ ```
674
+
675
+ ### afterDataPost
676
+
677
+ **When**: Immediately after `afterData`, post-specific
678
+ **Context**: Same as `afterData`
679
+ **What can be changed**: Same as `afterData`
680
+
681
+ ### enrichRecord
682
+
683
+ **When**: After fetching the created record (if `returnFullRecord` is not 'no')
684
+ **Purpose**: Modify response structure or add metadata
685
+
686
+ **IMPORTANT**: Do NOT use this hook to add/modify attributes. Use `enrichAttributes` instead.
687
+
688
+ **Context contains**:
689
+ - All previous context properties
690
+ - `record` (object) - The JSON:API formatted response
691
+ - `originalRecord` (object) - Backup of the record
692
+
693
+ **What can be changed**:
694
+ - Response structure (but NOT attributes)
695
+ - `record.meta` - Add metadata
696
+ - Should NOT modify attributes - use `enrichAttributes` hook instead
697
+
698
+ ### afterCommit
699
+
700
+ **When**: After the transaction is committed (only if `shouldCommit` is true)
701
+ **Purpose**: Trigger post-commit side effects like sending emails, webhooks
702
+
703
+ **Context**: All accumulated context
704
+ **What can be changed**: Nothing - for side effects only
705
+
706
+ **Example**:
707
+ ```javascript
708
+ hooks: {
709
+ afterCommit: async ({ context, helpers }) => {
710
+ if (context.method === 'post') {
711
+ // Send email notification (safe to do after commit)
712
+ await helpers.emailService.send({
713
+ template: 'new_post',
714
+ data: {
715
+ postId: context.id,
716
+ title: context.inputRecord.data.attributes.title
717
+ }
718
+ });
719
+ }
720
+ }
721
+ }
722
+ ```
723
+
724
+ ### afterRollback
725
+
726
+ **When**: If an error occurs and transaction is rolled back
727
+ **Purpose**: Clean up any external resources, log failures
728
+
729
+ **Context**: All accumulated context plus error information
730
+ **What can be changed**: Nothing - for cleanup/logging only
731
+
732
+ ### finish
733
+
734
+ **When**: Before returning the response
735
+ **Context**: All accumulated context
736
+ **What can be changed**: Nothing - informational only
737
+
738
+ ### finishPost
739
+
740
+ **When**: Immediately after `finish`, post-specific
741
+ **Context**: Same as `finish`
742
+ **What can be changed**: Nothing - informational only
743
+
744
+ ## PUT Method Hooks
745
+
746
+ Used for completely replacing a resource.
747
+
748
+ ### beforeData
749
+
750
+ **When**: Before replacing the resource
751
+ **Purpose**: Validate replacement data, check permissions
752
+
753
+ **Context contains**:
754
+ - `method` (string) - "put"
755
+ - `simplified` (boolean) - Whether simplified mode is active (affects input/output format, not hook data)
756
+ - `schemaInfo` (object) - Compiled schema information
757
+ - `inputRecord` (object) - The JSON:API formatted input data
758
+ - `id` (string/number) - The ID from URL or input record
759
+ - `params` (object) - Original parameters
760
+ - `queryParams` (object) - Contains `fields`, `include` for response
761
+ - `transaction` (object) - Database transaction
762
+ - `shouldCommit` (boolean) - Whether to commit the transaction
763
+ - `db` (object) - Database connection
764
+ - `scopeName` (string) - Name of the current scope
765
+ - `returnRecordSetting` (object) - Settings for what to return
766
+ - `minimalRecord` (object) - Existing record for authorization
767
+ - `existingRelationships` (object) - Current hasMany/manyToMany relationships
768
+ - `auth` (object) - Authentication context if provided
769
+
770
+ **What can be changed**:
771
+ - `inputRecord.data.attributes` - Modify validated attributes before update
772
+ - `belongsToUpdates` - Modify foreign key values (but these are usually already merged into attributes)
773
+ - Can prevent certain field updates
774
+
775
+ **Note**: After validation, attributes are stored in `context.inputRecord.data.attributes`.
776
+
777
+ **Example**:
778
+ ```javascript
779
+ hooks: {
780
+ beforeData: async ({ context }) => {
781
+ if (context.method === 'put' && context.inputRecord) {
782
+ // Prevent changing the author (check if belongsTo relationship changed)
783
+ const newAuthorId = context.belongsToUpdates?.author_id;
784
+ const currentAuthorId = context.minimalRecord?.data?.relationships?.author?.data?.id;
785
+ if (newAuthorId && newAuthorId !== currentAuthorId) {
786
+ throw new Error('Cannot change post author');
787
+ }
788
+
789
+ // Add update timestamp
790
+ context.inputRecord.data.attributes.updated_at = new Date().toISOString();
791
+ }
792
+ }
793
+ }
794
+ ```
795
+
796
+ ### beforeDataPut
797
+
798
+ **When**: Immediately after `beforeData`, put-specific
799
+ **Context**: Same as `beforeData`
800
+ **What can be changed**: Same as `beforeData`
801
+
802
+ ### afterData
803
+
804
+ **When**: After the resource is updated and relationships are replaced
805
+ **Purpose**: Handle relationship changes, trigger updates
806
+
807
+ **Context contains**:
808
+ - All previous context properties
809
+ - `updatedRecord` (object) - The updated database record
810
+ - `relationshipChanges` (object) - Details of relationship modifications
811
+
812
+ **What can be changed**:
813
+ - Can perform side effects
814
+ - Can clean up orphaned relationships
815
+ - Should NOT modify the database record
816
+
817
+ ### afterDataPut
818
+
819
+ **When**: Immediately after `afterData`, put-specific
820
+ **Context**: Same as `afterData`
821
+ **What can be changed**: Same as `afterData`
822
+
823
+ ### enrichRecord
824
+
825
+ **When**: After fetching the updated record (if `returnFullRecord` is not 'no')
826
+ **Purpose**: Modify response structure or add metadata
827
+
828
+ **IMPORTANT**: Do NOT use this hook to add/modify attributes. Use `enrichAttributes` instead.
829
+
830
+ **Context contains**:
831
+ - All previous context properties
832
+ - `record` (object) - The JSON:API formatted response
833
+ - `originalRecord` (object) - Backup of the record
834
+
835
+ **What can be changed**:
836
+ - Response structure (but NOT attributes)
837
+ - `record.meta` - Add metadata
838
+ - Should NOT modify attributes - use `enrichAttributes` hook instead
839
+
840
+ ### enrichRecordWithRelationships
841
+
842
+ **When**: After basic enrichment
843
+ **Context**: Same as `enrichRecord`
844
+ **What can be changed**:
845
+ - Can modify relationship data
846
+ - Can add relationship metadata
847
+
848
+ ### afterCommit
849
+
850
+ **When**: After the transaction is committed
851
+ **Context**: All accumulated context
852
+ **What can be changed**: Nothing - for side effects only
853
+
854
+ ### afterRollback
855
+
856
+ **When**: If an error occurs and transaction is rolled back
857
+ **Context**: All accumulated context plus error information
858
+ **What can be changed**: Nothing - for cleanup only
859
+
860
+ ### finish
861
+
862
+ **When**: Before returning the response
863
+ **Context**: All accumulated context
864
+ **What can be changed**: Nothing - informational only
865
+
866
+ ### finishPut
867
+
868
+ **When**: Immediately after `finish`, put-specific
869
+ **Context**: Same as `finish`
870
+ **What can be changed**: Nothing - informational only
871
+
872
+ ## PATCH Method Hooks
873
+
874
+ Used for partially updating a resource.
875
+
876
+ ### beforeData
877
+
878
+ **When**: Before partially updating the resource
879
+ **Purpose**: Validate partial updates, compute derived values
880
+
881
+ **Context contains**:
882
+ - `method` (string) - "patch"
883
+ - `simplified` (boolean) - Whether simplified mode is active (affects input/output format, not hook data)
884
+ - `schemaInfo` (object) - Compiled schema information
885
+ - `inputRecord` (object) - The JSON:API formatted input data (partial)
886
+ - `id` (string/number) - The ID from URL or input record
887
+ - `params` (object) - Original parameters
888
+ - `queryParams` (object) - Contains `fields`, `include` for response
889
+ - `transaction` (object) - Database transaction
890
+ - `shouldCommit` (boolean) - Whether to commit the transaction
891
+ - `db` (object) - Database connection
892
+ - `scopeName` (string) - Name of the current scope
893
+ - `returnRecordSetting` (object) - Settings for what to return
894
+ - `minimalRecord` (object) - Existing record for authorization
895
+ - `auth` (object) - Authentication context if provided
896
+
897
+ **What can be changed**:
898
+ - `inputRecord.data.attributes` - Modify validated attributes before update
899
+ - `belongsToUpdates` - Modify foreign key values (if any)
900
+ - Can add computed values or prevent updates
901
+
902
+ **Note**: For PATCH, `context.inputRecord.data.attributes` contains only the fields being updated. Use `context.minimalRecord.data.attributes` to access the complete current record.
903
+
904
+ **Example**:
905
+ ```javascript
906
+ hooks: {
907
+ beforeData: async ({ context }) => {
908
+ if (context.method === 'patch' && context.inputRecord) {
909
+ // If status is being changed to published, set publish date
910
+ if (context.inputRecord.data.attributes.status === 'published' &&
911
+ context.minimalRecord?.data?.attributes?.status !== 'published') {
912
+ context.inputRecord.data.attributes.published_at = new Date().toISOString();
913
+ }
914
+
915
+ // Always update the modified timestamp
916
+ context.inputRecord.data.attributes.updated_at = new Date().toISOString();
917
+ }
918
+ }
919
+ }
920
+ ```
921
+
922
+ ### beforeDataPatch
923
+
924
+ **When**: Immediately after `beforeData`, patch-specific
925
+ **Context**: Same as `beforeData`
926
+ **What can be changed**: Same as `beforeData`
927
+
928
+ ### afterData
929
+
930
+ **When**: After the partial update is applied
931
+ **Purpose**: React to specific changes, trigger conditional side effects
932
+
933
+ **Context contains**:
934
+ - All previous context properties
935
+ - `updatedRecord` (object) - The updated database record
936
+ - `relationshipChanges` (object) - Details of any relationship modifications
937
+
938
+ **What can be changed**:
939
+ - Can perform side effects based on what changed
940
+ - Should NOT modify the database record
941
+
942
+ ### afterDataPatch
943
+
944
+ **When**: Immediately after `afterData`, patch-specific
945
+ **Context**: Same as `afterData`
946
+ **What can be changed**: Same as `afterData`
947
+
948
+ ### enrichRecord
949
+
950
+ **When**: After fetching the updated record (if `returnFullRecord` is not 'no')
951
+ **Purpose**: Modify response structure or add metadata
952
+
953
+ **IMPORTANT**: Do NOT use this hook to add/modify attributes. Use `enrichAttributes` instead.
954
+
955
+ **Context contains**:
956
+ - All previous context properties
957
+ - `record` (object) - The JSON:API formatted response
958
+ - `originalRecord` (object) - Backup of the record
959
+
960
+ **What can be changed**:
961
+ - Response structure (but NOT attributes)
962
+ - `record.meta` - Add metadata
963
+ - Should NOT modify attributes - use `enrichAttributes` hook instead
964
+
965
+ ### enrichRecordWithRelationships
966
+
967
+ **When**: After basic enrichment
968
+ **Context**: Same as `enrichRecord`
969
+ **What can be changed**:
970
+ - Can modify relationship data
971
+ - Can add relationship metadata
972
+
973
+ ### afterCommit
974
+
975
+ **When**: After the transaction is committed
976
+ **Context**: All accumulated context
977
+ **What can be changed**: Nothing - for side effects only
978
+
979
+ ### afterRollback
980
+
981
+ **When**: If an error occurs and transaction is rolled back
982
+ **Context**: All accumulated context plus error information
983
+ **What can be changed**: Nothing - for cleanup only
984
+
985
+ ### finish
986
+
987
+ **When**: Before returning the response
988
+ **Context**: All accumulated context
989
+ **What can be changed**: Nothing - informational only
990
+
991
+ ### finishPatch
992
+
993
+ **When**: Immediately after `finish`, patch-specific
994
+ **Context**: Same as `finish`
995
+ **What can be changed**: Nothing - informational only
996
+
997
+ ## DELETE Method Hooks
998
+
999
+ Used for removing resources.
1000
+
1001
+ ### beforeData
1002
+
1003
+ **When**: Before deleting the resource
1004
+ **Purpose**: Validate deletion, check for dependencies
1005
+
1006
+ **Context contains**:
1007
+ - `method` (string) - "delete"
1008
+ - `schemaInfo` (object) - Compiled schema information
1009
+ - `id` (string/number) - The ID of the resource to delete
1010
+ - `transaction` (object) - Database transaction
1011
+ - `shouldCommit` (boolean) - Whether to commit the transaction
1012
+ - `db` (object) - Database connection
1013
+ - `scopeName` (string) - Name of the current scope
1014
+ - `minimalRecord` (object) - Record fetched for authorization checks
1015
+ - `auth` (object) - Authentication context if provided
1016
+
1017
+ **What can be changed**:
1018
+ - Can throw errors to prevent deletion
1019
+ - Can add properties to context for later hooks
1020
+ - Cannot modify the deletion itself
1021
+
1022
+ **Example**:
1023
+ ```javascript
1024
+ hooks: {
1025
+ beforeData: async ({ context }) => {
1026
+ if (context.method === 'delete') {
1027
+ // Check if post has comments
1028
+ const commentCount = await context.db('comments')
1029
+ .where('post_id', context.id)
1030
+ .count('* as count')
1031
+ .first();
1032
+
1033
+ if (commentCount.count > 0) {
1034
+ throw new Error('Cannot delete post with comments');
1035
+ }
1036
+ }
1037
+ }
1038
+ }
1039
+ ```
1040
+
1041
+ ### beforeDataDelete
1042
+
1043
+ **When**: Immediately after `beforeData`, delete-specific
1044
+ **Context**: Same as `beforeData`
1045
+ **What can be changed**: Same as `beforeData`
1046
+
1047
+ ### afterData
1048
+
1049
+ **When**: After the resource is deleted from the database
1050
+ **Purpose**: Clean up related data, log deletions
1051
+
1052
+ **Context contains**:
1053
+ - All previous context properties
1054
+ - `deletedCount` (number) - Number of records deleted (should be 1)
1055
+ - `deletedRecord` (object) - The record that was deleted
1056
+
1057
+ **What can be changed**:
1058
+ - Can perform cascading deletes or cleanup
1059
+ - Can log the deletion
1060
+ - Cannot undo the deletion
1061
+
1062
+ **Example**:
1063
+ ```javascript
1064
+ hooks: {
1065
+ afterData: async ({ context, scopes }) => {
1066
+ if (context.method === 'delete') {
1067
+ // Log the deletion
1068
+ await scopes.audit_logs.create({
1069
+ action: 'delete',
1070
+ resource_type: 'posts',
1071
+ resource_id: context.id,
1072
+ user_id: context.auth?.userId,
1073
+ timestamp: new Date().toISOString()
1074
+ });
1075
+
1076
+ // Clean up orphaned images
1077
+ await context.db('post_images')
1078
+ .where('post_id', context.id)
1079
+ .delete();
1080
+ }
1081
+ }
1082
+ }
1083
+ ```
1084
+
1085
+ ### afterDataDelete
1086
+
1087
+ **When**: Immediately after `afterData`, delete-specific
1088
+ **Context**: Same as `afterData`
1089
+ **What can be changed**: Same as `afterData`
1090
+
1091
+ ### afterCommit
1092
+
1093
+ **When**: After the transaction is committed
1094
+ **Purpose**: Trigger post-deletion side effects
1095
+
1096
+ **Context**: All accumulated context
1097
+ **What can be changed**: Nothing - for side effects only
1098
+
1099
+ ### afterRollback
1100
+
1101
+ **When**: If an error occurs and transaction is rolled back
1102
+ **Context**: All accumulated context plus error information
1103
+ **What can be changed**: Nothing - for cleanup only
1104
+
1105
+ ### finish
1106
+
1107
+ **When**: Before returning the response (typically empty for DELETE)
1108
+ **Context**: All accumulated context
1109
+ **What can be changed**: Nothing - informational only
1110
+
1111
+ ### finishDelete
1112
+
1113
+ **When**: Immediately after `finish`, delete-specific
1114
+ **Context**: Same as `finish`
1115
+ **What can be changed**: Nothing - informational only
1116
+
1117
+ ## Special Hooks
1118
+
1119
+ ### enrichAttributes
1120
+
1121
+ The `enrichAttributes` hook is the correct way to add or modify attributes on records. This hook is called for ALL records - both main records and included/related records.
1122
+
1123
+ **When**: After records are fetched and before they are returned
1124
+ **Purpose**: Add computed fields, transform attribute values, enhance record data
1125
+
1126
+ **Context contains**:
1127
+ - `attributes` (object) - The record's attributes that should be modified
1128
+ - `parentContext` (object) - The parent context from the calling method (contains method, queryParams, etc.)
1129
+ - `computedFields` (object) - Computed field definitions from schema
1130
+ - `requestedComputedFields` (array) - Which computed fields were requested
1131
+ - `scopeName` (string) - Name of the current scope
1132
+ - `helpers` (object) - Helper functions
1133
+ - `api` (object) - API instance
1134
+
1135
+ **What can be changed**:
1136
+ - Modify `context.attributes` to add new properties
1137
+ - Transform existing attribute values
1138
+ - Remove sensitive attributes
1139
+
1140
+ **Important**:
1141
+ - This hook is called for EVERY record (main and included)
1142
+ - Works with both single records and collections
1143
+ - Modify `context.attributes` directly
1144
+
1145
+ **Example**:
1146
+ ```javascript
1147
+ // In global customize()
1148
+ api.customize({
1149
+ hooks: {
1150
+ enrichAttributes: async ({ context }) => {
1151
+ // Add computed fields based on scope
1152
+ if (context.scopeName === 'posts') {
1153
+ context.attributes.wordCount = context.attributes.content?.split(' ').length || 0;
1154
+ context.attributes.readingTime = Math.ceil(context.attributes.wordCount / 200) + ' min';
1155
+ context.attributes.preview = context.attributes.content?.substring(0, 150) + '...';
1156
+ }
1157
+
1158
+ if (context.scopeName === 'users') {
1159
+ // Hide sensitive data
1160
+ delete context.attributes.password;
1161
+ delete context.attributes.resetToken;
1162
+
1163
+ // Add display name
1164
+ context.attributes.displayName = `${context.attributes.firstName} ${context.attributes.lastName}`;
1165
+ }
1166
+ }
1167
+ }
1168
+ });
1169
+
1170
+ // In resource-specific extras
1171
+ api.addScope('articles', {}, {
1172
+ hooks: {
1173
+ enrichAttributes: async ({ context }) => {
1174
+ // This only runs for articles
1175
+ context.attributes.isPublished = context.attributes.status === 'published';
1176
+ context.attributes.isNew = new Date() - new Date(context.attributes.created_at) < 7 * 24 * 60 * 60 * 1000;
1177
+
1178
+ // Format dates for display
1179
+ context.attributes.formattedDate = new Date(context.attributes.created_at).toLocaleDateString();
1180
+ }
1181
+ }
1182
+ });
1183
+ ```
1184
+
1185
+ ### knexQueryFiltering
1186
+
1187
+ The `knexQueryFiltering` hook is called during QUERY operations to apply filter conditions. This is a special hook that allows complex query modifications.
1188
+
1189
+ **When**: During `dataQuery` execution, before sorting and pagination
1190
+ **Purpose**: Apply filters, add JOINs, modify query conditions
1191
+
1192
+ **Context contains**:
1193
+ - `knexQuery` (object) - Temporary object with:
1194
+ - `query` (knex query builder) - The active query being built
1195
+ - `filters` (object) - Filter parameters from request
1196
+ - `searchSchema` (object) - Schema defining searchable fields
1197
+ - `scopeName` (string) - Current resource scope
1198
+ - `tableName` (string) - Database table name
1199
+ - `db` (object) - Database connection
1200
+ - All other standard query context properties
1201
+
1202
+ The REST API Knex Plugin registers three sub-hooks that run in sequence:
1203
+
1204
+ #### 1. polymorphicFiltersHook
1205
+
1206
+ **Purpose**: Handles filtering on polymorphic relationships
1207
+ **What it does**:
1208
+ - Detects polymorphic filter fields (e.g., `commentable.title`)
1209
+ - Adds appropriate JOINs for each polymorphic type
1210
+ - Builds WHERE conditions with proper type checking
1211
+
1212
+ **Example**:
1213
+ ```javascript
1214
+ // This is handled automatically by the plugin
1215
+ // When filtering: ?filters[commentable.title]=Hello
1216
+ // It generates SQL like:
1217
+ // LEFT JOIN posts ON (comments.commentable_type = 'posts' AND comments.commentable_id = posts.id)
1218
+ // WHERE posts.title = 'Hello'
1219
+ ```
1220
+
1221
+ #### 2. crossTableFiltersHook
1222
+
1223
+ **Purpose**: Handles filtering on cross-table fields
1224
+ **What it does**:
1225
+ - Detects cross-table filter fields (e.g., `author.name`)
1226
+ - Adds JOINs to related tables
1227
+ - Qualifies field names to avoid ambiguity
1228
+
1229
+ **Example**:
1230
+ ```javascript
1231
+ // This is handled automatically by the plugin
1232
+ // When filtering: ?filters[author.name]=John
1233
+ // It generates SQL like:
1234
+ // INNER JOIN users ON posts.author_id = users.id
1235
+ // WHERE users.name = 'John'
1236
+ ```
1237
+
1238
+ #### 3. basicFiltersHook
1239
+
1240
+ **Purpose**: Handles simple filters on the main table
1241
+ **What it does**:
1242
+ - Processes standard field filters
1243
+ - Handles special operators (contains, starts_with, etc.)
1244
+ - Applies filters to non-joined fields
1245
+
1246
+ **Custom Filter Hook Example**:
1247
+ ```javascript
1248
+ hooks: {
1249
+ knexQueryFiltering: async ({ context }) => {
1250
+ if (context.knexQuery && context.knexQuery.filters) {
1251
+ const { query, filters, tableName } = context.knexQuery;
1252
+
1253
+ // Add custom filter logic
1254
+ if (filters.special_filter) {
1255
+ query.where(function() {
1256
+ this.where(`${tableName}.status`, 'active')
1257
+ .orWhere(`${tableName}.featured`, true);
1258
+ });
1259
+ }
1260
+ }
1261
+ }
1262
+ }
1263
+ ```
1264
+
1265
+ ## Hook Best Practices
1266
+
1267
+ ### 1. Hook Order Matters
1268
+
1269
+ Hooks run in registration order. Consider dependencies between hooks:
1270
+
1271
+ ```javascript
1272
+ hooks: {
1273
+ beforeData: [
1274
+ async ({ context }) => {
1275
+ // Validation runs first
1276
+ if (!context.inputRecord?.data?.attributes?.title) {
1277
+ throw new Error('Title is required');
1278
+ }
1279
+ },
1280
+ async ({ context }) => {
1281
+ // Enrichment runs second, after validation
1282
+ context.inputRecord.data.attributes.slug = context.inputRecord.data.attributes.title
1283
+ .toLowerCase()
1284
+ .replace(/\s+/g, '-');
1285
+ }
1286
+ ]
1287
+ }
1288
+ ```
1289
+
1290
+ ### 2. Use Proper Hook Placement
1291
+
1292
+ If using addHook directly (less common), you can control placement:
1293
+
1294
+ ```javascript
1295
+ // Use afterPlugin to ensure your hook runs after the plugin's hooks
1296
+ api.addHook('beforeData', 'myHook', { afterPlugin: 'rest-api-knex' }, handler);
1297
+ ```
1298
+
1299
+ ### 3. Context Mutation Guidelines
1300
+
1301
+ - **DO**: Modify allowed properties as documented
1302
+ - **DON'T**: Change properties marked as read-only
1303
+ - **DO**: Add custom properties for communication between hooks
1304
+ - **DON'T**: Remove required properties
1305
+
1306
+ ### 4. Error Handling
1307
+
1308
+ Throwing an error in any hook will:
1309
+ - Stop the operation
1310
+ - Trigger rollback for write operations
1311
+ - Return the error to the client
1312
+
1313
+ ```javascript
1314
+ hooks: {
1315
+ beforeData: async ({ context }) => {
1316
+ if (context.inputRecord?.data?.attributes?.price < 0) {
1317
+ throw new RestApiValidationError('Price cannot be negative', {
1318
+ fields: ['data.attributes.price']
1319
+ });
1320
+ }
1321
+ }
1322
+ }
1323
+ ```
1324
+
1325
+ ### 5. Performance Considerations
1326
+
1327
+ - Keep `enrichRecord` hooks lightweight for queries with many results
1328
+ - Use database transactions appropriately
1329
+ - Batch operations when possible
1330
+ - Avoid N+1 queries in hooks
1331
+
1332
+ ### 6. Transaction Safety
1333
+
1334
+ For write operations:
1335
+ - Use `afterCommit` for external side effects (emails, webhooks)
1336
+ - Use `afterData` for database-related side effects
1337
+ - Always handle `afterRollback` for cleanup
1338
+
1339
+ ### 7. Scope-Specific Hooks
1340
+
1341
+ Add hooks to specific scopes to avoid checking in every hook:
1342
+
1343
+ ```javascript
1344
+ // Better: Add hooks in the scope's extras parameter
1345
+ api.addScope('posts', {}, {
1346
+ hooks: {
1347
+ beforeData: async ({ context }) => {
1348
+ // This only runs for posts
1349
+ }
1350
+ }
1351
+ });
1352
+
1353
+ // Less ideal: Check scopeName in global hooks
1354
+ hooks: {
1355
+ beforeData: async ({ context }) => {
1356
+ if (context.scopeName === 'posts') {
1357
+ // ...
1358
+ }
1359
+ }
1360
+ }
1361
+ ```
1362
+
1363
+ ### 8. Hook Communication
1364
+
1365
+ Use context properties to communicate between hooks:
1366
+
1367
+ ```javascript
1368
+ hooks: {
1369
+ beforeData: async ({ context }) => {
1370
+ context.customData = { processed: true };
1371
+ },
1372
+
1373
+ afterData: async ({ context }) => {
1374
+ if (context.customData?.processed) {
1375
+ // React to first hook
1376
+ }
1377
+ }
1378
+ }
1379
+
1380
+ ## System-Wide Hooks
1381
+
1382
+ These hooks are managed by the hooked-api framework and are triggered during core API operations.
1383
+
1384
+ ### plugin:installed
1385
+
1386
+ **When**: After a plugin is successfully installed
1387
+ **Purpose**: React to plugin installations, set up inter-plugin communication
1388
+
1389
+ **Context contains**:
1390
+ - `pluginName` (string) - Name of the installed plugin
1391
+ - `pluginOptions` (object) - Options passed to the plugin
1392
+ - `plugin` (object) - The plugin object itself (informational only)
1393
+
1394
+ **What can be changed**: Nothing - this is an informational hook
1395
+
1396
+ **Example**:
1397
+ ```javascript
1398
+ hooks: {
1399
+ 'plugin:installed': async ({ context }) => {
1400
+ console.log(`Plugin ${context.pluginName} installed with options:`, context.pluginOptions);
1401
+ }
1402
+ }
1403
+ ```
1404
+
1405
+ ### scope:added
1406
+
1407
+ **When**: After a scope is added to the API
1408
+ **Purpose**: Initialize scope-specific settings, validate configurations, compile schemas
1409
+
1410
+ **Context contains**:
1411
+ - `scopeName` (string) - Name of the added scope
1412
+ - `scopeOptions` (object) - Immutable copy of initial options
1413
+ - `scopeExtras` (object) - Immutable copy of initial extras
1414
+ - `vars` (proxy) - Proxy for current scope vars (can be mutated)
1415
+ - `helpers` (proxy) - Proxy for current scope helpers (can be mutated)
1416
+
1417
+ **What can be changed**:
1418
+ - Can add/modify scope vars through the proxy
1419
+ - Can add/modify scope helpers through the proxy
1420
+ - Cannot modify scopeOptions (frozen after hook runs)
1421
+
1422
+ **Example**:
1423
+ ```javascript
1424
+ hooks: {
1425
+ 'scope:added': async ({ context }) => {
1426
+ // Add a default value to scope vars
1427
+ context.vars.defaultPageSize = 20;
1428
+
1429
+ // Add a helper function
1430
+ context.helpers.formatDate = (date) => new Date(date).toISOString();
1431
+ }
1432
+ }
1433
+ ```
1434
+
1435
+ ### method:api:added
1436
+
1437
+ **When**: After an API method is added
1438
+ **Purpose**: Wrap or modify API method handlers
1439
+
1440
+ **Context contains**:
1441
+ - `methodName` (string) - Name of the added method
1442
+ - `handler` (function) - The method handler function
1443
+
1444
+ **What can be changed**:
1445
+ - `handler` - Can wrap or replace the handler function
1446
+
1447
+ **Example**:
1448
+ ```javascript
1449
+ hooks: {
1450
+ 'method:api:added': async ({ context }) => {
1451
+ const originalHandler = context.handler;
1452
+ context.handler = async (params) => {
1453
+ console.log(`Calling ${context.methodName}`);
1454
+ const result = await originalHandler(params);
1455
+ console.log(`${context.methodName} completed`);
1456
+ return result;
1457
+ };
1458
+ }
1459
+ }
1460
+ ```
1461
+
1462
+ ### method:scope:adding
1463
+
1464
+ **When**: Before adding a scope method
1465
+ **Purpose**: Validate or modify scope methods before they're registered
1466
+
1467
+ **Context contains**:
1468
+ - `methodName` (string) - Name of the method being added
1469
+ - `handler` (function) - The method handler function
1470
+
1471
+ **What can be changed**:
1472
+ - `handler` - Can wrap or replace the handler function before it's added
1473
+
1474
+ ### method:scope:added
1475
+
1476
+ **When**: After a scope method is added
1477
+ **Purpose**: React to scope method additions
1478
+
1479
+ **Context contains**:
1480
+ - `methodName` (string) - Name of the added method
1481
+ - `handler` (function) - The method handler function
1482
+
1483
+ **What can be changed**: Nothing - this is an informational hook
1484
+
1485
+ ```
1486
+
1487
+ ---
1488
+
1489
+ [Back to Guide](./README.md)
1490
+