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
package/README_X_Hooks.md
DELETED
|
@@ -1,647 +0,0 @@
|
|
|
1
|
-
# Hook System Guide
|
|
2
|
-
|
|
3
|
-
The hook system in JSON REST API provides a powerful way to extend and customize API behavior at various points in the request lifecycle. This guide explains how to use hooks effectively.
|
|
4
|
-
|
|
5
|
-
## Table of Contents
|
|
6
|
-
- [Overview](#overview)
|
|
7
|
-
- [Hook Basics](#hook-basics)
|
|
8
|
-
- [Available Hooks](#available-hooks)
|
|
9
|
-
- [Hook Context](#hook-context)
|
|
10
|
-
- [Creating Hook Handlers](#creating-hook-handlers)
|
|
11
|
-
- [Hook Execution Order](#hook-execution-order)
|
|
12
|
-
- [Common Patterns](#common-patterns)
|
|
13
|
-
- [Best Practices](#best-practices)
|
|
14
|
-
|
|
15
|
-
## Overview
|
|
16
|
-
|
|
17
|
-
Hooks allow you to:
|
|
18
|
-
- Intercept and modify requests/responses
|
|
19
|
-
- Add custom validation or authorization
|
|
20
|
-
- Transform data before/after operations
|
|
21
|
-
- React to events (like record creation or deletion)
|
|
22
|
-
- Extend core functionality without modifying plugins
|
|
23
|
-
|
|
24
|
-
## Hook Basics
|
|
25
|
-
|
|
26
|
-
### Hook Signatures
|
|
27
|
-
|
|
28
|
-
The hook system uses a simple, consistent signature where all data is passed through a single `context` object.
|
|
29
|
-
|
|
30
|
-
```javascript
|
|
31
|
-
// Running hooks
|
|
32
|
-
await runHooks('hookName', context);
|
|
33
|
-
|
|
34
|
-
// Hook handler
|
|
35
|
-
addHook('hookName', 'handlerName', {}, async ({ context }) => {
|
|
36
|
-
const { someData } = context;
|
|
37
|
-
});
|
|
38
|
-
```
|
|
39
|
-
|
|
40
|
-
### Adding Hook Handlers
|
|
41
|
-
|
|
42
|
-
Hook handlers are added using the `addHook` function:
|
|
43
|
-
|
|
44
|
-
```javascript
|
|
45
|
-
api.addHook(hookName, handlerName, options, handlerFunction);
|
|
46
|
-
```
|
|
47
|
-
|
|
48
|
-
Parameters:
|
|
49
|
-
- `hookName`: The name of the hook to listen to
|
|
50
|
-
- `handlerName`: A unique name for your handler
|
|
51
|
-
- `options`: Configuration object (optional)
|
|
52
|
-
- `order`: Numeric execution order (lower runs first)
|
|
53
|
-
- `sequence`: Alternative to order
|
|
54
|
-
- `handlerFunction`: Async function that receives hook context
|
|
55
|
-
|
|
56
|
-
## Available Hooks
|
|
57
|
-
|
|
58
|
-
### Transport Hooks
|
|
59
|
-
|
|
60
|
-
#### `transport:request`
|
|
61
|
-
Runs when a request is received by the transport layer (e.g., Express).
|
|
62
|
-
|
|
63
|
-
```javascript
|
|
64
|
-
api.addHook('transport:request', 'auth-check', {}, async ({ context }) => {
|
|
65
|
-
// context includes transport-specific data
|
|
66
|
-
const { request, response } = context.transport;
|
|
67
|
-
|
|
68
|
-
// You can:
|
|
69
|
-
// - Extract auth tokens
|
|
70
|
-
// - Set security headers
|
|
71
|
-
// - Log requests
|
|
72
|
-
// - Block requests by setting context.handled = true
|
|
73
|
-
});
|
|
74
|
-
```
|
|
75
|
-
|
|
76
|
-
#### `transport:response`
|
|
77
|
-
Runs before sending the response back to the client.
|
|
78
|
-
|
|
79
|
-
```javascript
|
|
80
|
-
api.addHook('transport:response', 'add-headers', {}, async ({ context }) => {
|
|
81
|
-
const { request, response } = context.transport;
|
|
82
|
-
|
|
83
|
-
// Add custom headers
|
|
84
|
-
response.headers['X-Custom-Header'] = 'value';
|
|
85
|
-
|
|
86
|
-
// Modify response based on request
|
|
87
|
-
if (request.path.startsWith('/api/public')) {
|
|
88
|
-
response.headers['Cache-Control'] = 'public, max-age=3600';
|
|
89
|
-
}
|
|
90
|
-
});
|
|
91
|
-
```
|
|
92
|
-
|
|
93
|
-
### REST API Operation Hooks
|
|
94
|
-
|
|
95
|
-
#### `beforeProcessing`
|
|
96
|
-
Runs before processing any REST API operation (POST, PUT, PATCH).
|
|
97
|
-
|
|
98
|
-
```javascript
|
|
99
|
-
api.addHook('beforeProcessing', 'validate-data', {}, async ({ context }) => {
|
|
100
|
-
const { method, scopeName, params } = context;
|
|
101
|
-
|
|
102
|
-
// Perform validation
|
|
103
|
-
if (method === 'post' && scopeName === 'articles') {
|
|
104
|
-
// Custom validation logic
|
|
105
|
-
}
|
|
106
|
-
});
|
|
107
|
-
```
|
|
108
|
-
|
|
109
|
-
#### `checkPermissions`
|
|
110
|
-
Runs during permission checking phase.
|
|
111
|
-
|
|
112
|
-
```javascript
|
|
113
|
-
api.addHook('checkPermissions', 'custom-permissions', {}, async ({ context }) => {
|
|
114
|
-
const { method, scopeName, auth, id } = context;
|
|
115
|
-
|
|
116
|
-
// Implement custom permission logic
|
|
117
|
-
if (scopeName === 'admin-resources' && !auth?.roles?.includes('admin')) {
|
|
118
|
-
throw new Error('Admin access required');
|
|
119
|
-
}
|
|
120
|
-
});
|
|
121
|
-
```
|
|
122
|
-
|
|
123
|
-
#### `beforeData` / `afterData`
|
|
124
|
-
Run before/after data operations.
|
|
125
|
-
|
|
126
|
-
```javascript
|
|
127
|
-
// Before data operation
|
|
128
|
-
api.addHook('beforeData', 'prepare-data', {}, async ({ context }) => {
|
|
129
|
-
// Modify query parameters, add filters, etc.
|
|
130
|
-
});
|
|
131
|
-
|
|
132
|
-
// After data operation
|
|
133
|
-
api.addHook('afterData', 'transform-data', {}, async ({ context }) => {
|
|
134
|
-
// Transform the returned data
|
|
135
|
-
if (context.record) {
|
|
136
|
-
context.record.attributes.processedAt = new Date();
|
|
137
|
-
}
|
|
138
|
-
});
|
|
139
|
-
```
|
|
140
|
-
|
|
141
|
-
#### `finish`
|
|
142
|
-
Runs after successful operation completion.
|
|
143
|
-
|
|
144
|
-
```javascript
|
|
145
|
-
api.addHook('finish', 'log-success', {}, async ({ context }) => {
|
|
146
|
-
const { method, scopeName, id } = context;
|
|
147
|
-
console.log(`Operation ${method} on ${scopeName}/${id} completed`);
|
|
148
|
-
});
|
|
149
|
-
```
|
|
150
|
-
|
|
151
|
-
### Schema Hooks
|
|
152
|
-
|
|
153
|
-
#### `schema:enrich`
|
|
154
|
-
Allows modification of resource schemas.
|
|
155
|
-
|
|
156
|
-
```javascript
|
|
157
|
-
api.addHook('schema:enrich', 'add-timestamps', {}, async ({ context }) => {
|
|
158
|
-
const { schema, scopeName } = context;
|
|
159
|
-
|
|
160
|
-
// Add timestamp fields to all resources
|
|
161
|
-
schema.attributes.createdAt = {
|
|
162
|
-
type: 'string',
|
|
163
|
-
format: 'date-time',
|
|
164
|
-
computed: true
|
|
165
|
-
};
|
|
166
|
-
schema.attributes.updatedAt = {
|
|
167
|
-
type: 'string',
|
|
168
|
-
format: 'date-time',
|
|
169
|
-
computed: true
|
|
170
|
-
};
|
|
171
|
-
});
|
|
172
|
-
```
|
|
173
|
-
|
|
174
|
-
### Authentication Hooks
|
|
175
|
-
|
|
176
|
-
#### `afterAuthentication`
|
|
177
|
-
Runs after successful JWT authentication.
|
|
178
|
-
|
|
179
|
-
```javascript
|
|
180
|
-
api.addHook('afterAuthentication', 'enrich-auth', {}, async ({ context }) => {
|
|
181
|
-
const { auth, authPayload } = context;
|
|
182
|
-
|
|
183
|
-
// Enrich auth context with additional data
|
|
184
|
-
const user = await db('users').where({ id: auth.userId }).first();
|
|
185
|
-
context.auth.profile = user;
|
|
186
|
-
});
|
|
187
|
-
```
|
|
188
|
-
|
|
189
|
-
#### `afterLogout`
|
|
190
|
-
Runs after user logout.
|
|
191
|
-
|
|
192
|
-
```javascript
|
|
193
|
-
api.addHook('afterLogout', 'cleanup', {}, async ({ context }) => {
|
|
194
|
-
const { logoutUserId } = context;
|
|
195
|
-
|
|
196
|
-
// Clean up user sessions, caches, etc.
|
|
197
|
-
await cache.delete(`user:${logoutUserId}`);
|
|
198
|
-
});
|
|
199
|
-
```
|
|
200
|
-
|
|
201
|
-
### Database Query Hooks (Knex Plugin)
|
|
202
|
-
|
|
203
|
-
#### `knexQueryFiltering`
|
|
204
|
-
Allows modification of database queries.
|
|
205
|
-
|
|
206
|
-
```javascript
|
|
207
|
-
api.addHook('knexQueryFiltering', 'tenant-filter', { order: -100 },
|
|
208
|
-
async ({ context }) => {
|
|
209
|
-
const { query, auth } = context;
|
|
210
|
-
|
|
211
|
-
// Add tenant isolation
|
|
212
|
-
if (auth?.tenantId) {
|
|
213
|
-
query.where('tenant_id', auth.tenantId);
|
|
214
|
-
}
|
|
215
|
-
}
|
|
216
|
-
);
|
|
217
|
-
```
|
|
218
|
-
|
|
219
|
-
## Hook Context
|
|
220
|
-
|
|
221
|
-
The context object passed to hooks contains all relevant data for the operation. Common properties include:
|
|
222
|
-
|
|
223
|
-
```javascript
|
|
224
|
-
{
|
|
225
|
-
// Operation details
|
|
226
|
-
method: 'post', // HTTP method
|
|
227
|
-
scopeName: 'articles', // Resource name
|
|
228
|
-
id: '123', // Resource ID (for single-resource ops)
|
|
229
|
-
|
|
230
|
-
// Authentication
|
|
231
|
-
auth: {
|
|
232
|
-
userId: 'user-123',
|
|
233
|
-
email: 'user@example.com',
|
|
234
|
-
roles: ['admin'],
|
|
235
|
-
permissions: ['articles:write']
|
|
236
|
-
},
|
|
237
|
-
|
|
238
|
-
// Request data
|
|
239
|
-
params: {}, // Request parameters
|
|
240
|
-
inputRecord: {}, // Input data (for mutations)
|
|
241
|
-
queryParams: {}, // Query parameters
|
|
242
|
-
|
|
243
|
-
// Response data
|
|
244
|
-
record: {}, // Single record
|
|
245
|
-
records: [], // Multiple records
|
|
246
|
-
|
|
247
|
-
// Transport-specific (in transport hooks)
|
|
248
|
-
transport: {
|
|
249
|
-
request: {
|
|
250
|
-
method: 'POST',
|
|
251
|
-
path: '/api/articles',
|
|
252
|
-
headers: {},
|
|
253
|
-
body: {}
|
|
254
|
-
},
|
|
255
|
-
response: {
|
|
256
|
-
status: 200,
|
|
257
|
-
headers: {},
|
|
258
|
-
body: {}
|
|
259
|
-
}
|
|
260
|
-
},
|
|
261
|
-
|
|
262
|
-
// Database transaction
|
|
263
|
-
transaction: knexTransaction,
|
|
264
|
-
|
|
265
|
-
// Helpers and utilities
|
|
266
|
-
db: knexInstance,
|
|
267
|
-
log: logger
|
|
268
|
-
}
|
|
269
|
-
```
|
|
270
|
-
|
|
271
|
-
## Creating Hook Handlers
|
|
272
|
-
|
|
273
|
-
### Basic Handler
|
|
274
|
-
|
|
275
|
-
```javascript
|
|
276
|
-
api.addHook('beforeProcessing', 'my-handler', {}, async ({ context }) => {
|
|
277
|
-
// Access context properties
|
|
278
|
-
const { method, scopeName, auth } = context;
|
|
279
|
-
|
|
280
|
-
// Perform operations
|
|
281
|
-
if (scopeName === 'sensitive-data' && !auth) {
|
|
282
|
-
throw new Error('Authentication required');
|
|
283
|
-
}
|
|
284
|
-
|
|
285
|
-
// Modify context (changes are passed to next handlers)
|
|
286
|
-
context.customFlag = true;
|
|
287
|
-
});
|
|
288
|
-
```
|
|
289
|
-
|
|
290
|
-
### Handler with Dependencies
|
|
291
|
-
|
|
292
|
-
```javascript
|
|
293
|
-
// In a plugin
|
|
294
|
-
const MyPlugin = {
|
|
295
|
-
name: 'my-plugin',
|
|
296
|
-
|
|
297
|
-
async install({ api, addHook, vars, helpers }) {
|
|
298
|
-
// Use plugin context
|
|
299
|
-
addHook('beforeData', 'my-handler', {}, async ({ context }) => {
|
|
300
|
-
// Access plugin vars
|
|
301
|
-
const config = vars.myPluginConfig;
|
|
302
|
-
|
|
303
|
-
// Use helpers
|
|
304
|
-
const result = await helpers.validateData(context.inputRecord);
|
|
305
|
-
|
|
306
|
-
// Modify operation
|
|
307
|
-
if (!result.valid) {
|
|
308
|
-
throw new ValidationError(result.errors);
|
|
309
|
-
}
|
|
310
|
-
});
|
|
311
|
-
}
|
|
312
|
-
};
|
|
313
|
-
```
|
|
314
|
-
|
|
315
|
-
### Conditional Handler
|
|
316
|
-
|
|
317
|
-
```javascript
|
|
318
|
-
api.addHook('finish', 'conditional-handler', {}, async ({ context }) => {
|
|
319
|
-
// Only run for specific operations
|
|
320
|
-
if (context.method !== 'post' || context.scopeName !== 'orders') {
|
|
321
|
-
return; // Skip this handler
|
|
322
|
-
}
|
|
323
|
-
|
|
324
|
-
// Send notification for new orders
|
|
325
|
-
await sendNotification({
|
|
326
|
-
type: 'new-order',
|
|
327
|
-
orderId: context.record.id
|
|
328
|
-
});
|
|
329
|
-
});
|
|
330
|
-
```
|
|
331
|
-
|
|
332
|
-
## Hook Execution Order
|
|
333
|
-
|
|
334
|
-
Hooks execute in a specific order determined by:
|
|
335
|
-
|
|
336
|
-
1. **Hook type order** - Different hook types run at different lifecycle points
|
|
337
|
-
2. **Handler order** - Within a hook type, handlers run by their `order` value
|
|
338
|
-
3. **Registration order** - Handlers with same order run in registration sequence
|
|
339
|
-
|
|
340
|
-
```javascript
|
|
341
|
-
// These will execute in order: -1000, -100, 0, 100
|
|
342
|
-
api.addHook('checkPermissions', 'first', { order: -1000 }, handler1);
|
|
343
|
-
api.addHook('checkPermissions', 'second', { order: -100 }, handler2);
|
|
344
|
-
api.addHook('checkPermissions', 'third', {}, handler3); // default order: 0
|
|
345
|
-
api.addHook('checkPermissions', 'fourth', { order: 100 }, handler4);
|
|
346
|
-
```
|
|
347
|
-
|
|
348
|
-
### Stopping Hook Execution
|
|
349
|
-
|
|
350
|
-
Return `false` from a handler to stop the hook chain:
|
|
351
|
-
|
|
352
|
-
```javascript
|
|
353
|
-
api.addHook('transport:request', 'rate-limiter', { order: -1000 },
|
|
354
|
-
async ({ context }) => {
|
|
355
|
-
const { request } = context.transport;
|
|
356
|
-
|
|
357
|
-
if (await isRateLimited(request.ip)) {
|
|
358
|
-
context.rejection = {
|
|
359
|
-
status: 429,
|
|
360
|
-
message: 'Too many requests'
|
|
361
|
-
};
|
|
362
|
-
return false; // Stop processing
|
|
363
|
-
}
|
|
364
|
-
}
|
|
365
|
-
);
|
|
366
|
-
```
|
|
367
|
-
|
|
368
|
-
## Common Patterns
|
|
369
|
-
|
|
370
|
-
### Data Validation
|
|
371
|
-
|
|
372
|
-
```javascript
|
|
373
|
-
api.addHook('beforeProcessing', 'validate-articles', {}, async ({ context }) => {
|
|
374
|
-
if (context.scopeName !== 'articles') return;
|
|
375
|
-
|
|
376
|
-
const { inputRecord, method } = context;
|
|
377
|
-
|
|
378
|
-
if (['post', 'put', 'patch'].includes(method)) {
|
|
379
|
-
const { title, content } = inputRecord.data.attributes;
|
|
380
|
-
|
|
381
|
-
if (title && title.length > 200) {
|
|
382
|
-
throw new ValidationError('Title too long');
|
|
383
|
-
}
|
|
384
|
-
|
|
385
|
-
if (content && content.length < 10) {
|
|
386
|
-
throw new ValidationError('Content too short');
|
|
387
|
-
}
|
|
388
|
-
}
|
|
389
|
-
});
|
|
390
|
-
```
|
|
391
|
-
|
|
392
|
-
### Automatic Field Population
|
|
393
|
-
|
|
394
|
-
```javascript
|
|
395
|
-
api.addHook('beforeProcessing', 'auto-fields', {}, async ({ context }) => {
|
|
396
|
-
const { method, auth, inputRecord } = context;
|
|
397
|
-
|
|
398
|
-
if (method === 'post' && auth) {
|
|
399
|
-
// Set creator
|
|
400
|
-
inputRecord.data.attributes.createdBy = auth.userId;
|
|
401
|
-
inputRecord.data.attributes.createdAt = new Date().toISOString();
|
|
402
|
-
}
|
|
403
|
-
|
|
404
|
-
if (['put', 'patch'].includes(method) && auth) {
|
|
405
|
-
// Set updater
|
|
406
|
-
inputRecord.data.attributes.updatedBy = auth.userId;
|
|
407
|
-
inputRecord.data.attributes.updatedAt = new Date().toISOString();
|
|
408
|
-
}
|
|
409
|
-
});
|
|
410
|
-
```
|
|
411
|
-
|
|
412
|
-
### Filtering Based on User
|
|
413
|
-
|
|
414
|
-
```javascript
|
|
415
|
-
api.addHook('knexQueryFiltering', 'user-filter', {}, async ({ context }) => {
|
|
416
|
-
const { query, auth, scopeName } = context;
|
|
417
|
-
|
|
418
|
-
if (scopeName === 'user-data' && auth) {
|
|
419
|
-
// Users only see their own data
|
|
420
|
-
query.where('user_id', auth.userId);
|
|
421
|
-
}
|
|
422
|
-
});
|
|
423
|
-
```
|
|
424
|
-
|
|
425
|
-
### Audit Logging
|
|
426
|
-
|
|
427
|
-
```javascript
|
|
428
|
-
api.addHook('finish', 'audit-log', { order: 1000 }, async ({ context }) => {
|
|
429
|
-
const { method, scopeName, id, auth, record } = context;
|
|
430
|
-
|
|
431
|
-
// Skip read operations
|
|
432
|
-
if (method === 'get' || method === 'query') return;
|
|
433
|
-
|
|
434
|
-
await db('audit_logs').insert({
|
|
435
|
-
user_id: auth?.userId || 'anonymous',
|
|
436
|
-
action: method,
|
|
437
|
-
resource_type: scopeName,
|
|
438
|
-
resource_id: id || record?.id,
|
|
439
|
-
changes: JSON.stringify(record),
|
|
440
|
-
timestamp: new Date()
|
|
441
|
-
});
|
|
442
|
-
});
|
|
443
|
-
```
|
|
444
|
-
|
|
445
|
-
### Caching
|
|
446
|
-
|
|
447
|
-
```javascript
|
|
448
|
-
const cache = new Map();
|
|
449
|
-
|
|
450
|
-
// Cache reads
|
|
451
|
-
api.addHook('beforeData', 'cache-check', { order: -1000 },
|
|
452
|
-
async ({ context }) => {
|
|
453
|
-
if (context.method !== 'get') return;
|
|
454
|
-
|
|
455
|
-
const cacheKey = `${context.scopeName}:${context.id}`;
|
|
456
|
-
const cached = cache.get(cacheKey);
|
|
457
|
-
|
|
458
|
-
if (cached) {
|
|
459
|
-
context.record = cached;
|
|
460
|
-
context.skipDataOperation = true; // Custom flag
|
|
461
|
-
}
|
|
462
|
-
}
|
|
463
|
-
);
|
|
464
|
-
|
|
465
|
-
// Update cache on writes
|
|
466
|
-
api.addHook('finish', 'cache-update', { order: 1000 },
|
|
467
|
-
async ({ context }) => {
|
|
468
|
-
const { method, scopeName, id, record } = context;
|
|
469
|
-
|
|
470
|
-
if (method === 'get' && record) {
|
|
471
|
-
cache.set(`${scopeName}:${id}`, record);
|
|
472
|
-
}
|
|
473
|
-
|
|
474
|
-
if (['patch', 'put', 'delete'].includes(method)) {
|
|
475
|
-
cache.delete(`${scopeName}:${id}`);
|
|
476
|
-
}
|
|
477
|
-
}
|
|
478
|
-
);
|
|
479
|
-
```
|
|
480
|
-
|
|
481
|
-
## Best Practices
|
|
482
|
-
|
|
483
|
-
### 1. Use Descriptive Handler Names
|
|
484
|
-
|
|
485
|
-
```javascript
|
|
486
|
-
// ❌ Bad
|
|
487
|
-
api.addHook('checkPermissions', 'handler1', {}, async ({ context }) => {});
|
|
488
|
-
|
|
489
|
-
// ✅ Good
|
|
490
|
-
api.addHook('checkPermissions', 'require-admin-for-settings', {},
|
|
491
|
-
async ({ context }) => {});
|
|
492
|
-
```
|
|
493
|
-
|
|
494
|
-
### 2. Handle Errors Gracefully
|
|
495
|
-
|
|
496
|
-
```javascript
|
|
497
|
-
api.addHook('beforeProcessing', 'validate-safely', {}, async ({ context }) => {
|
|
498
|
-
try {
|
|
499
|
-
await validateData(context.inputRecord);
|
|
500
|
-
} catch (error) {
|
|
501
|
-
// Log error with context
|
|
502
|
-
context.log.error('Validation failed', {
|
|
503
|
-
error: error.message,
|
|
504
|
-
scopeName: context.scopeName,
|
|
505
|
-
method: context.method
|
|
506
|
-
});
|
|
507
|
-
|
|
508
|
-
// Re-throw with user-friendly message
|
|
509
|
-
throw new ValidationError('Invalid data provided');
|
|
510
|
-
}
|
|
511
|
-
});
|
|
512
|
-
```
|
|
513
|
-
|
|
514
|
-
### 3. Check Context Before Operating
|
|
515
|
-
|
|
516
|
-
```javascript
|
|
517
|
-
api.addHook('finish', 'send-notifications', {}, async ({ context }) => {
|
|
518
|
-
// Ensure required data exists
|
|
519
|
-
if (!context.record?.id || !context.auth?.userId) {
|
|
520
|
-
return; // Skip if missing required data
|
|
521
|
-
}
|
|
522
|
-
|
|
523
|
-
// Safe to proceed
|
|
524
|
-
await notifyUser(context.auth.userId, context.record);
|
|
525
|
-
});
|
|
526
|
-
```
|
|
527
|
-
|
|
528
|
-
### 4. Use Order for Dependencies
|
|
529
|
-
|
|
530
|
-
```javascript
|
|
531
|
-
// Ensure auth runs before permissions
|
|
532
|
-
api.addHook('checkPermissions', 'populate-user', { order: -100 },
|
|
533
|
-
async ({ context }) => {
|
|
534
|
-
if (context.auth?.userId) {
|
|
535
|
-
context.user = await getUser(context.auth.userId);
|
|
536
|
-
}
|
|
537
|
-
}
|
|
538
|
-
);
|
|
539
|
-
|
|
540
|
-
api.addHook('checkPermissions', 'check-user-status', { order: 0 },
|
|
541
|
-
async ({ context }) => {
|
|
542
|
-
// Can safely use context.user set by previous handler
|
|
543
|
-
if (context.user?.suspended) {
|
|
544
|
-
throw new Error('Account suspended');
|
|
545
|
-
}
|
|
546
|
-
}
|
|
547
|
-
);
|
|
548
|
-
```
|
|
549
|
-
|
|
550
|
-
### 5. Avoid Heavy Operations in Hooks
|
|
551
|
-
|
|
552
|
-
```javascript
|
|
553
|
-
// ❌ Bad - Synchronous heavy operation
|
|
554
|
-
api.addHook('finish', 'process-images', {}, async ({ context }) => {
|
|
555
|
-
if (context.record?.images) {
|
|
556
|
-
// This blocks the response
|
|
557
|
-
await processImages(context.record.images);
|
|
558
|
-
}
|
|
559
|
-
});
|
|
560
|
-
|
|
561
|
-
// ✅ Good - Queue for background processing
|
|
562
|
-
api.addHook('finish', 'queue-image-processing', {}, async ({ context }) => {
|
|
563
|
-
if (context.record?.images) {
|
|
564
|
-
// Quick operation to queue the job
|
|
565
|
-
await jobQueue.add('process-images', {
|
|
566
|
-
recordId: context.record.id,
|
|
567
|
-
images: context.record.images
|
|
568
|
-
});
|
|
569
|
-
}
|
|
570
|
-
});
|
|
571
|
-
```
|
|
572
|
-
|
|
573
|
-
### 6. Document Hook Side Effects
|
|
574
|
-
|
|
575
|
-
```javascript
|
|
576
|
-
/**
|
|
577
|
-
* Auto-assigns articles to the default category if none specified.
|
|
578
|
-
* Modifies: inputRecord.data.relationships.category
|
|
579
|
-
* Depends on: categories table having a 'default' entry
|
|
580
|
-
*/
|
|
581
|
-
api.addHook('beforeProcessing', 'auto-assign-category', {},
|
|
582
|
-
async ({ context }) => {
|
|
583
|
-
if (context.scopeName !== 'articles' || context.method !== 'post') {
|
|
584
|
-
return;
|
|
585
|
-
}
|
|
586
|
-
|
|
587
|
-
const { relationships } = context.inputRecord.data;
|
|
588
|
-
if (!relationships?.category) {
|
|
589
|
-
context.inputRecord.data.relationships = {
|
|
590
|
-
...relationships,
|
|
591
|
-
category: {
|
|
592
|
-
data: { type: 'categories', id: 'default' }
|
|
593
|
-
}
|
|
594
|
-
};
|
|
595
|
-
}
|
|
596
|
-
}
|
|
597
|
-
);
|
|
598
|
-
```
|
|
599
|
-
|
|
600
|
-
### 7. Use Type Guards
|
|
601
|
-
|
|
602
|
-
```javascript
|
|
603
|
-
api.addHook('beforeData', 'type-safe-handler', {}, async ({ context }) => {
|
|
604
|
-
// Check types before using
|
|
605
|
-
if (typeof context.auth?.userId !== 'string') {
|
|
606
|
-
return;
|
|
607
|
-
}
|
|
608
|
-
|
|
609
|
-
if (!Array.isArray(context.queryParams?.include)) {
|
|
610
|
-
context.queryParams.include = [];
|
|
611
|
-
}
|
|
612
|
-
|
|
613
|
-
// Now safe to use
|
|
614
|
-
context.queryParams.include.push('author');
|
|
615
|
-
});
|
|
616
|
-
```
|
|
617
|
-
|
|
618
|
-
## Debugging Hooks
|
|
619
|
-
|
|
620
|
-
Enable debug logging to see hook execution:
|
|
621
|
-
|
|
622
|
-
```javascript
|
|
623
|
-
const api = new Api({
|
|
624
|
-
name: 'my-api',
|
|
625
|
-
log: { level: 'debug' }
|
|
626
|
-
});
|
|
627
|
-
|
|
628
|
-
// Hooks will log execution details
|
|
629
|
-
// [DEBUG] Running hook 'checkPermissions' with 3 handlers
|
|
630
|
-
// [DEBUG] Hook handler 'require-auth' completed in 2ms
|
|
631
|
-
```
|
|
632
|
-
|
|
633
|
-
Add custom debugging:
|
|
634
|
-
|
|
635
|
-
```javascript
|
|
636
|
-
api.addHook('beforeProcessing', 'debug-hook', { order: -9999 },
|
|
637
|
-
async ({ context }) => {
|
|
638
|
-
console.log('Hook context:', {
|
|
639
|
-
method: context.method,
|
|
640
|
-
scopeName: context.scopeName,
|
|
641
|
-
auth: context.auth ? 'authenticated' : 'anonymous',
|
|
642
|
-
hasInput: !!context.inputRecord
|
|
643
|
-
});
|
|
644
|
-
}
|
|
645
|
-
);
|
|
646
|
-
```
|
|
647
|
-
|
|
@@ -1,23 +0,0 @@
|
|
|
1
|
-
Session 1:
|
|
2
|
-
✔ Finish websockets properly, make it actually work @done(25-07-18 14:43)
|
|
3
|
-
✔ Get rid of runPluginHooks, runHooks, on (!!). Only have runHooks, with the runHooks of methods automatically setting context, params @done(25-07-18 14:43)
|
|
4
|
-
|
|
5
|
-
Session 2:
|
|
6
|
-
✔ improve way in which jvt-auth works, no need for those vars and helpers @done(25-07-18 15:50)
|
|
7
|
-
✔ Improve enrichAttributes, split into helpers <--- ??????????????? @done(25-07-18 19:45)
|
|
8
|
-
|
|
9
|
-
Sessoon 3:
|
|
10
|
-
✔ Add 'created_at' module, maybe have 'protected' fields @done(25-07-18 20:46)
|
|
11
|
-
✔ Create cors module @done(25-07-19 22:15)
|
|
12
|
-
|
|
13
|
-
Session 4:
|
|
14
|
-
✔ Improve the CORS plugin documentation BY FAAAAR @done(25-07-20 08:35)
|
|
15
|
-
✔ Write test/documentation for positioning plugin @done(25-07-20 15:17)
|
|
16
|
-
|
|
17
|
-
|
|
18
|
-
Sessions 4:
|
|
19
|
-
✔ Final decision on parameters/usage of non-method hooks (parameters? Context?) @done(25-07-20 17:21)
|
|
20
|
-
☐ Final decision on the way plugins get the parameters; DO NOT get them their own namespace
|
|
21
|
-
☐ Understant why state.authCheckers.set() seems to assign // Handle simplified/flat format (!!) return record[ownerField] === context.auth.userId;
|
|
22
|
-
|
|
23
|
-
☐ Document the lot properly
|