json-rest-api 1.0.6 → 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.
- package/.claude/agents/library-guide-expert.md +29 -0
- package/.claude/agents/rest-api-internals-expert.md +53 -0
- package/CLAUDE.md +139 -1
- package/README.md +2 -5
- package/docTest.js +238 -0
- package/docs/GUIDE/GUIDE_1_Initial_Setup.md +955 -0
- package/docs/GUIDE/GUIDE_2_1_The_Starting_Point.md +54 -0
- package/docs/GUIDE/GUIDE_2_2_Manipulating_And_Searching_Tables.md +750 -0
- package/docs/GUIDE/GUIDE_2_3_BelongsTo_Relationships.md +494 -0
- package/docs/GUIDE/GUIDE_2_4_HasMany_Records.md +398 -0
- package/docs/GUIDE/GUIDE_2_5_HasMany_Polymorphic.md +882 -0
- package/docs/GUIDE/GUIDE_2_6_Many_To_Many.md +709 -0
- package/docs/GUIDE/GUIDE_2_7_Pagination_And_Ordering.md +1233 -0
- package/docs/GUIDE/GUIDE_2_8_Effects_of_PUT_and_PATCH.md +796 -0
- package/{README_X_Relationship_Plugin.md → docs/GUIDE/GUIDE_2_9_Relationships_Urls.md} +1 -4
- package/docs/GUIDE/GUIDE_2_Data_And_Relations.md +66 -0
- package/docs/GUIDE/GUIDE_3_Field_Transformations.md +1246 -0
- package/docs/GUIDE/GUIDE_5_Non-Db_Stores.md +0 -0
- package/docs/GUIDE/GUIDE_7_Hooks_Data_Management_And_Plugins.md +1490 -0
- package/{README_X_SocketIO.md → docs/GUIDE/GUIDE_X_SocketIO.md} +17 -17
- package/docs/GUIDE/index.md +58 -0
- package/docs/GUIDE.md +61 -2238
- package/docs/ONBOARDING/Computed_Properties_And_Sparse_Fields.md +398 -0
- package/docs/README.md +57 -2239
- package/{README_2_Computed_and_hidden_fields.md → docs/README_VARIOUS/GUIDE_2_Computed_and_hidden_fields.md} +22 -17
- package/docs/README_VARIOUS/GUIDE_IN_DOCS.md +2277 -0
- package/{README.ORIG.md → docs/README_VARIOUS/README.ORIG.md} +8 -8
- package/{README_8_Polymorphic_Relationships.md → docs/README_VARIOUS/README_8_Polymorphic_Relationships.md} +1 -1
- package/docs/README_VARIOUS/README_IN_DOCS.ms +2277 -0
- package/{RELATIONSHIPS_ADVANCED.md → docs/README_VARIOUS/RELATIONSHIPS_ADVANCED.md} +3 -3
- package/{correct-schema-format.md → docs/README_VARIOUS/correct-schema-format.md} +13 -13
- package/{pagination.md → docs/README_VARIOUS/pagination.md} +11 -8
- package/docs/assets/css/style.scss +78 -5
- package/docs/index.md +2 -12
- package/helper-reorganization-plan.md +85 -0
- package/helper-usage-analysis.md +94 -0
- package/package.json +5 -4
- package/plugins/core/bulk-operations-plugin.js +2 -2
- package/plugins/core/connectors/express-plugin.js +99 -33
- package/plugins/core/lib/querying/knex-cross-table-search.js +609 -0
- package/plugins/core/lib/querying/knex-json-api-transformers-querying.js +491 -0
- package/plugins/core/lib/querying/knex-pagination-helpers.js +544 -0
- package/plugins/core/lib/{knex-process-includes.js → querying/knex-process-includes.js} +65 -50
- package/plugins/core/lib/querying/knex-query-helpers-base.js +74 -0
- package/plugins/core/lib/{knex-query-helpers.js → querying/knex-query-helpers.js} +416 -180
- package/plugins/core/lib/{knex-relationship-includes.js → querying/knex-relationship-includes.js} +430 -114
- package/plugins/core/lib/querying/knex-window-queries.js +328 -0
- package/plugins/core/lib/querying-writing/compile-schemas.js +328 -0
- package/plugins/core/{utils → lib/querying-writing}/connectors-query-parser.js +62 -57
- package/plugins/core/lib/{database-capabilities.js → querying-writing/database-capabilities.js} +113 -7
- package/plugins/core/lib/querying-writing/database-value-normalizers.js +369 -0
- package/plugins/core/lib/querying-writing/default-data-helpers.js +72 -0
- package/plugins/core/{utils → lib/querying-writing}/field-utils.js +41 -31
- package/plugins/core/{utils → lib/querying-writing}/knex-constants.js +1 -0
- package/plugins/core/lib/{knex-field-helpers.js → querying-writing/knex-field-helpers.js} +190 -57
- package/plugins/core/lib/querying-writing/knex-json-api-transformers.js +162 -0
- package/plugins/core/lib/{payload-validators.js → querying-writing/payload-validators.js} +297 -217
- package/plugins/core/lib/querying-writing/schema-helpers.js +497 -0
- package/plugins/core/lib/querying-writing/scope-validations.js +431 -0
- package/plugins/core/lib/{simplified-helpers.js → querying-writing/simplified-helpers.js} +216 -176
- package/plugins/core/lib/writing/knex-json-api-transformers-writing.js +110 -0
- package/plugins/core/lib/writing/many-to-many-manipulations.js +300 -0
- package/plugins/core/lib/writing/relationship-processor.js +252 -0
- package/plugins/core/rest-api-knex-plugin.js +183 -62
- package/plugins/core/rest-api-plugin-hooks/compile-resource-schemas.js +7 -0
- package/plugins/core/rest-api-plugin-hooks/register-relationship-routes.js +72 -0
- package/plugins/core/rest-api-plugin-hooks/register-scope-routes.js +102 -0
- package/plugins/core/rest-api-plugin-hooks/turn-scope-init-into-vars.js +50 -0
- package/plugins/core/rest-api-plugin-hooks/validate-include-configurations.js +39 -0
- package/plugins/core/rest-api-plugin-methods/add-route.js +23 -0
- package/plugins/core/rest-api-plugin-methods/check-permissions.js +18 -0
- package/plugins/core/rest-api-plugin-methods/common.js +605 -0
- package/plugins/core/rest-api-plugin-methods/delete-relationship.js +115 -0
- package/plugins/core/rest-api-plugin-methods/delete.js +109 -0
- package/plugins/core/rest-api-plugin-methods/enrich-attributes.js +186 -0
- package/plugins/core/rest-api-plugin-methods/get-related.js +295 -0
- package/plugins/core/rest-api-plugin-methods/get-relationship.js +62 -0
- package/plugins/core/rest-api-plugin-methods/get.js +187 -0
- package/plugins/core/rest-api-plugin-methods/patch-relationship.js +56 -0
- package/plugins/core/rest-api-plugin-methods/patch.js +193 -0
- package/plugins/core/rest-api-plugin-methods/post-relationship.js +104 -0
- package/plugins/core/rest-api-plugin-methods/post.js +157 -0
- package/plugins/core/rest-api-plugin-methods/put.js +257 -0
- package/plugins/core/rest-api-plugin-methods/query.js +208 -0
- package/plugins/core/rest-api-plugin.js +108 -2498
- package/plugins/core/socketio-plugin.js +28 -5
- package/plugins/storage/local-storage.js +2 -2
- package/tests/computed-fields.test.js +483 -0
- package/tests/cors.test.js +4 -4
- package/tests/curl.test.js +5 -4
- package/tests/custom-idproperty-relationships.test.js +891 -0
- package/tests/field-getters.test.js +353 -0
- package/tests/field-setters.test.js +310 -0
- package/tests/fixtures/api-configs.js +800 -28
- package/tests/jwt-auth.test.js +0 -2
- package/tests/nested-includes.test.js +1 -1
- package/tests/pagination-cursor-multifield.test.js +514 -0
- package/tests/pagination-enhanced.test.js +12 -9
- package/tests/polymorphic-relationships.test.js +10 -8
- package/tests/polymorphic-simple.test.js +6 -4
- package/tests/positioning-advanced.test.js +12 -12
- package/tests/positioning-extended.test.js +25 -25
- package/tests/positioning-multifilter.test.js +24 -24
- package/tests/positioning.test.js +3 -3
- package/tests/query-limits.test.js +24 -8
- package/tests/relationship-endpoints.test.js +4 -5
- package/tests/relationships.test.js +0 -1
- package/tests/socketio.test.js +12 -1
- package/tests/virtual-fields.test.js +13 -13
- package/PLAN_FOR_WEBSOCKETS.md +0 -398
- package/README_1_Initial_Setup.md +0 -393
- package/README_X_Hooks.md +0 -647
- package/TODO_rest-api-plugin-extras.todo +0 -23
- package/connectors_ideas/express-plugin.js +0 -120
- package/connectors_ideas/http-plugin.js +0 -138
- package/docs/examples/express-plugin.js +0 -366
- package/example.js +0 -301
- package/plugins/core/HELPER_MODULES.md +0 -285
- package/plugins/core/lib/compile-schemas.js +0 -245
- package/plugins/core/lib/database-value-normalizers.js +0 -192
- package/plugins/core/lib/default-data-helpers.js +0 -100
- package/plugins/core/lib/knex-cross-table-search.js +0 -442
- package/plugins/core/lib/knex-json-api-helpers.js +0 -84
- package/plugins/core/lib/knex-json-api-transformers.js +0 -449
- package/plugins/core/lib/knex-pagination-helpers.js +0 -193
- package/plugins/core/lib/knex-window-queries.js +0 -142
- package/plugins/core/lib/many-to-many-manipulations.js +0 -343
- package/plugins/core/lib/relationship-processor.js +0 -278
- package/plugins/core/lib/schema-helpers.js +0 -343
- package/plugins/core/lib/scope-validations.js +0 -252
- package/plugins/core/rest-api-relationships-plugin.js +0 -700
- package/plugins/core/utils/knex-query-helpers-base.js +0 -56
- /package/{README_X_Permissions.md → docs/GUIDE/GUIDE_4_Authentication_And_Permissions.md} +0 -0
- /package/{README_X_Positioning.md → docs/GUIDE/GUIDE_6_Positioning.md} +0 -0
- /package/{README_X_Bulk_Operations.md → docs/GUIDE/GUIDE_X_Bulk_Operations.md} +0 -0
- /package/{README_X_Cors.md → docs/GUIDE/GUIDE_X_Cors.md} +0 -0
- /package/{README_X_Multihome.md → docs/GUIDE/GUIDE_X_Multihome.md} +0 -0
- /package/{README_X_Appendices.md → docs/GUIDE/GUIDE_Y_Appendices.md} +0 -0
- /package/{QUERY_DEPTH_PROTECTION.md → docs/README_VARIOUS/QUERY_DEPTH_PROTECTION.md} +0 -0
- /package/{RELATIONSHIPS.md → docs/README_VARIOUS/RELATIONSHIPS.md} +0 -0
- /package/{WEBSOCKET_IMPLEMENTATION_STATUS.md → docs/README_VARIOUS/WEBSOCKET_IMPLEMENTATION_STATUS.md} +0 -0
- /package/{helper-functions-chart.md → docs/README_VARIOUS/helper-functions-chart.md} +0 -0
- /package/docs/{implementation-hidden-computed-fields.md → README_VARIOUS/implementation-hidden-computed-fields.md} +0 -0
- /package/docs/{include-limits-migration-guide.md → README_VARIOUS/include-limits-migration-guide.md} +0 -0
- /package/{readmeSnippets → docs/README_VARIOUS}/test_put_behavior.js +0 -0
- /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
|
+
|