adapt-authoring-api 1.3.1 → 1.3.2

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.
@@ -1,19 +1,15 @@
1
- name: Add to main project
1
+ # Calls the org-level reusable workflow to add PRs to the TODO Board
2
+
3
+ name: Add PR to Project
2
4
 
3
5
  on:
4
- issues:
5
- types:
6
- - opened
7
6
  pull_request:
8
7
  types:
9
8
  - opened
9
+ - reopened
10
10
 
11
11
  jobs:
12
12
  add-to-project:
13
- name: Add to main project
14
- runs-on: ubuntu-latest
15
- steps:
16
- - uses: actions/add-to-project@v0.1.0
17
- with:
18
- project-url: https://github.com/orgs/adapt-security/projects/5
19
- github-token: ${{ secrets.PROJECTS_SECRET }}
13
+ uses: adapt-security/.github/.github/workflows/new.yml@main
14
+ secrets:
15
+ PROJECTS_SECRET: ${{ secrets.PROJECTS_SECRET }}
@@ -3,7 +3,7 @@
3
3
  "documentation": {
4
4
  "enable": true,
5
5
  "manualPages": {
6
- "writing-an-api.md": "basics"
6
+ "writing-an-api.md": "development"
7
7
  }
8
8
  }
9
9
  }
@@ -0,0 +1,186 @@
1
+ # Custom request and response data
2
+
3
+ This guide documents the custom properties added to the standard Express.js [Request (req)](https://expressjs.com/en/api.html#req.properties) and [Response (res)](https://expressjs.com/en/api.html#res.properties) objects by various Adapt authoring tool modules.
4
+
5
+ See the [Express.js documentation](https://expressjs.com/en/api.html) for the standard properties.
6
+
7
+ ## Summary
8
+
9
+ | Property | Added by | Purpose |
10
+ | -------- | -------- | ------- |
11
+ | `req.auth` | adapt-authoring-auth | Authentication data (user, token, scopes) |
12
+ | `req.apiData` | adapt-authoring-api | Pre-processed API request data |
13
+ | `req.translate` | adapt-authoring-lang | Translation function |
14
+ | `req.routeConfig` | adapt-authoring-server | Route configuration |
15
+ | `req.hasBody` | adapt-authoring-server | Whether request has body data |
16
+ | `req.hasParams` | adapt-authoring-server | Whether request has route params |
17
+ | `req.hasQuery` | adapt-authoring-server | Whether request has query string |
18
+ | `res.sendError` | adapt-authoring-server | Send formatted error responses |
19
+
20
+ ## Request object (req)
21
+
22
+ ### req.auth
23
+
24
+ Added by: `adapt-authoring-auth`
25
+
26
+ Contains authentication data for the current request. This object is always present but may not contain all the expected data (e.g. in the case of unauthenticated requests).
27
+
28
+ ```javascript
29
+ {
30
+ header: {
31
+ type: 'Bearer', // Auth header type
32
+ value: 'eyJhbGci...' // The raw token string
33
+ },
34
+ user: { // The authenticated user document
35
+ _id: ObjectId('507f1f77bcf86cd799439011'),
36
+ email: 'user@example.com',
37
+ firstName: 'John',
38
+ lastName: 'Doe',
39
+ roles: ['507f1f77bcf86cd799439012']
40
+ },
41
+ token: { // The decoded JWT payload
42
+ type: 'local',
43
+ userId: '507f1f77bcf86cd799439011',
44
+ signature: 'abc123',
45
+ iat: 1609459200,
46
+ exp: 1609545600
47
+ },
48
+ scopes: [ // Array of permission scopes
49
+ 'read:content',
50
+ 'write:content'
51
+ ],
52
+ isSuper: false, // Whether user has super privileges (*:*)
53
+ userSchemaName: 'localauthuser' // Schema used for the user
54
+ }
55
+ ```
56
+
57
+ ### req.apiData _(AbstractApiModule subclasses only)_
58
+
59
+ Added by: `adapt-authoring-api` (AbstractApiModule)
60
+
61
+ Contains pre-processed API request data. This is set by the `processRequestMiddleware` method for routes using `AbstractApiModule`.
62
+
63
+ ```javascript
64
+ {
65
+ collectionName: 'content', // MongoDB collection name
66
+ schemaName: 'course', // JSON schema name for validation
67
+ config: { ... }, // Route configuration
68
+ data: { ... }, // Request body data (for POST/PUT/PATCH)
69
+ query: { ... }, // Combined params and query string
70
+ modifying: true // Whether request modifies data (POST/PUT/PATCH/DELETE)
71
+ validate: false, // Whether data will be validated
72
+ }
73
+ ```
74
+
75
+ ### req.translate
76
+
77
+ Added by: `adapt-authoring-lang` (LangModule.addTranslationUtils)
78
+
79
+ A function for translating strings based on the client's accepted language.
80
+
81
+ **Signature:**
82
+
83
+ ```javascript
84
+ req.translate(key: string, data?: object): string
85
+ ```
86
+
87
+ **Example usage:**
88
+
89
+ ```javascript
90
+ async myHandler (req, res, next) {
91
+ // Simple translation
92
+ const title = req.translate('app.newpagetitle')
93
+
94
+ // Translation with interpolation
95
+ const message = req.translate('app.welcomemessage', { name: 'John' })
96
+
97
+ // Translate a response message
98
+ res.json({ message: req.translate('app.success') })
99
+ }
100
+ ```
101
+
102
+ ### req.routeConfig
103
+
104
+ Added by: `adapt-authoring-server` (ServerUtils.cacheRouteConfig)
105
+
106
+ Contains the configuration object for the current route.
107
+
108
+ ```javascript
109
+ {
110
+ route: '/query',
111
+ internal: false, // Whether route is internal-only
112
+ handlers: { ... },
113
+ permissions: { ... }
114
+ }
115
+ ```
116
+
117
+ ### req.hasBody, req.hasParams, req.hasQuery
118
+
119
+ Added by: `adapt-authoring-server` (ServerUtils.addExistenceProps)
120
+
121
+ Boolean properties indicating whether the request has non-empty body, params, or query data. Useful for quickly checking if data was provided.
122
+
123
+ > **Note:** Body data is ignored for GET requests — `req.hasBody` will always be `false` for GET.
124
+
125
+ **Example usage:**
126
+
127
+ ```javascript
128
+ async myHandler (req, res, next) {
129
+ if (!req.hasParams) {
130
+ return res.sendError(this.app.errors.MISSING_PARAMS)
131
+ }
132
+
133
+ if (!req.hasBody) {
134
+ return res.sendError(this.app.errors.MISSING_BODY)
135
+ }
136
+
137
+ if (req.hasQuery) {
138
+ // Apply query filters
139
+ }
140
+ }
141
+ ```
142
+
143
+ ## Response object (res)
144
+
145
+ ### res.sendError
146
+
147
+ Added by: `adapt-authoring-server` (ServerUtils.addErrorHandler)
148
+
149
+ A convenience method for sending error responses. Automatically formats errors, sets the appropriate status code, and translates the error message if `req.translate` is available.
150
+
151
+ **Signature:**
152
+
153
+ ```javascript
154
+ res.sendError(error: AdaptError | Error): void
155
+ ```
156
+
157
+ **Example usage:**
158
+
159
+ ```javascript
160
+ async myHandler (req, res, next) {
161
+ try {
162
+ const result = await this.doSomething()
163
+ res.json(result)
164
+ } catch (e) {
165
+ // Send a predefined error
166
+ res.sendError(this.app.errors.NOT_FOUND)
167
+
168
+ // Or send with custom data
169
+ res.sendError(this.app.errors.VALIDATION_FAILED.setData({
170
+ field: 'email'
171
+ }))
172
+
173
+ // Or pass through an unexpected error (becomes SERVER_ERROR)
174
+ res.sendError(e)
175
+ }
176
+ }
177
+ ```
178
+
179
+ The error response format:
180
+
181
+ ```json
182
+ {
183
+ "code": "NOT_FOUND",
184
+ "message": "Resource not found"
185
+ }
186
+ ```
@@ -1,135 +1,492 @@
1
- # Writing an API
1
+ # Writing an API module
2
2
 
3
- _**Note:** before using this functionality, it is worth having an understanding of how to write basic custom modules first. See [this page](`adapt-authoring-api` module) for more._
3
+ This guide explains how to create a REST API module by extending `AbstractApiModule`. This is the recommended approach for any module that needs to expose HTTP endpoints for CRUD operations on database collections.
4
4
 
5
- The `adapt-authoring-api` module makes defining custom APIs simple by providing abstract classes and utilities you can use in your own modules to replace the common boilerplate code required when writing an API.
5
+ By extending `AbstractApiModule`, your module automatically gets:
6
6
 
7
- By extending the [AbstractAPIModule](../class/adapt_authoring_restructure/adapt-authoring-api/lib/module.js~AbstractApiModule.html) class, you get the following as standard:
8
- - Boiler-plate code for defining router and endpoints
9
- - Default handlers for incoming HTTP requests (with support for querying)
10
- - Support for custom middleware
11
- - Auto-loading of database schemas
12
- - Automated (and overridable) database interaction
13
- - Default permissions
7
+ - REST endpoints for CRUD operations (POST, GET, PUT, PATCH, DELETE)
8
+ - Request validation against JSON schemas
9
+ - Database interaction via the MongoDB module
10
+ - Permission-based route security
11
+ - Pagination support for list queries
12
+ - Data caching
13
+ - Hooks for customising behaviour at key points
14
14
 
15
- ## Defining your API
16
15
 
17
- To define an API, all you need to do is override the `setValues` function, making sure to change the values as appropriate.
16
+ ## Quick navigation
18
17
 
19
- See the below table for all values:
18
+ - [Prerequisites](#prerequisites)
19
+ - [Quick start](#quick-start)
20
+ - [Module configuration](#module-configuration)
21
+ - [Custom routes](#custom-routes)
22
+ - [Requests](#requests)
23
+ - [Database operations](#database-operations)
24
+ - [Hooks](#hooks)
25
+ - [Overriding methods](#overriding-methods)
26
+ - [Permissions](#permissions)
27
+ - [Caching](#caching)
28
+ - [Error handling](#error-handling)
29
+ - [Writing documentation](#writing-documentation)
20
30
 
21
- | Attribute | Type | Description | Optional |
22
- | --------- | ---- | ----------- | -------- |
23
- | `root` | `String` | This value will be used as the route for any URLs (e.g. `/api/mymodule`). For APIs which deal with collections of data, the general rule is to use the plural form (ie. `/api/items`). | `false` |
24
- | `router` | `Router` | The Router instance used for HTTP requests. If not defined, a new router will be created using the `root` value. | `true` |
25
- | `routes` | `Array<ApiRoute>` | Definitions for any routes to be added to the API router. | `false` |
26
- | `collectionName` | `String` | Default DB collection to store data to (can be overridden by individual handlers). | `false` |
31
+ ## Prerequisites
27
32
 
28
- ### Defining routes
33
+ Before creating an API module, you should understand:
34
+ - [How to write a basic module](../adapt-authoring-core/writing-a-module.md)
35
+ - [How schemas work](../adapt-authoring-jsonschema/defining-schemas.md)
29
36
 
30
- Before your API can handle HTTP requests, you must define which routes the router should respond to. If your module extends AbstractApiModule, this is simply a case of setting the [`routes`](../class/node_modules/adapt-authoring-api/lib/abstractApiModule.js~AbstractApiModule.html#instance-member-routes) instance variable:
37
+ ## Quick start
31
38
 
32
- ```js
33
- this.routes = [
34
- // route config here
35
- ]
39
+ Here's a minimal API module:
40
+
41
+ ```javascript
42
+ import AbstractApiModule from 'adapt-authoring-api'
43
+
44
+ class NotesModule extends AbstractApiModule {
45
+ async setValues () {
46
+ this.root = 'notes'
47
+ this.collectionName = 'notes'
48
+ this.schemaName = 'note'
49
+ this.useDefaultRouteConfig()
50
+ }
51
+ }
52
+
53
+ export default NotesModule
54
+ ```
55
+
56
+ With a schema at `schema/note.schema.json`:
57
+
58
+ ```json
59
+ {
60
+ "$schema": "https://json-schema.org/draft/2020-12/schema",
61
+ "$anchor": "note",
62
+ "type": "object",
63
+ "properties": {
64
+ "title": {
65
+ "type": "string",
66
+ "description": "Note title"
67
+ },
68
+ "content": {
69
+ "type": "string",
70
+ "description": "Note content"
71
+ }
72
+ },
73
+ "required": ["title"]
74
+ }
75
+ ```
76
+
77
+ This creates the following endpoints:
78
+
79
+ | Method | Route | Description |
80
+ | ------ | ----- | ----------- |
81
+ | POST | `/api/notes` | Create a note |
82
+ | GET | `/api/notes` | List all notes |
83
+ | GET | `/api/notes/:_id` | Get a single note |
84
+ | PUT | `/api/notes/:_id` | Replace a note |
85
+ | PATCH | `/api/notes/:_id` | Update a note |
86
+ | DELETE | `/api/notes/:_id` | Delete a note |
87
+ | GET | `/api/notes/schema` | Get the JSON schema |
88
+ | POST | `/api/notes/query` | Advanced query with pagination |
89
+
90
+ ## Module configuration
91
+
92
+ ### Required values
93
+
94
+ Override `setValues()` to configure your module:
95
+
96
+ ```javascript
97
+ async setValues () {
98
+ this.root = 'notes' // URL path: /api/notes
99
+ this.collectionName = 'notes' // MongoDB collection name
100
+ this.schemaName = 'note' // Default schema for validation
101
+ this.useDefaultRouteConfig() // Use standard CRUD routes
102
+ }
36
103
  ```
37
104
 
38
- #### General tips
39
- The Adapt server module maps to Express functionality as closely as possible, and as such adopts the same middleware/handler concepts (with a few minor changes). It is therefore very useful to have some understanding of how the Express stack works, particularly with regards to the execution order of middleware and handlers. For more details on route handling, see the [official Express documentation](https://expressjs.com/en/guide/routing.html).
105
+ | Property | Type | Required | Description |
106
+ | -------- | ---- | -------- | ----------- |
107
+ | `root` | String | Yes* | URL path for the API (e.g., `notes` → `/api/notes`) |
108
+ | `router` | Router | Yes* | Router instance (created automatically if `root` is set) |
109
+ | `routes` | Array | Yes | Route definitions |
110
+ | `collectionName` | String | Yes | MongoDB collection name |
111
+ | `schemaName` | String | No | Default schema name for validation |
112
+ | `permissionsScope` | String | No | Override the scope used for permissions (defaults to `root`) |
40
113
 
41
- Here are a few useful notes/tips:
114
+ *Either `root` or `router` must be set.
42
115
 
43
- - The `route` attribute of each route definition is very powerful, and handles params/queries/etc. in the same way as Express (see [the Express docs](https://expressjs.com/en/guide/routing.html) for more).
44
- - Use the API's middleware to perform any tasks which are common to all routes, such as checking and formatting the request data
45
- - You only need to specify route handlers for the routes/HTTP methods you want to enable, access will be blocked to any route/HTTP method combinations you haven't defined
46
- - You can run route-specific middleware by adding it as a handler to the route config (see example 2. below)
116
+ ### Using default routes
47
117
 
48
- #### Using the default route configuration
49
- Instead of defining each route yourself, the AbstractApiModule class also gives you a set of default routes which you can use if you wish:
50
- ```js
118
+ Call `useDefaultRouteConfig()` to use the standard CRUD routes:
119
+
120
+ ```javascript
121
+ async setValues () {
122
+ this.root = 'notes'
123
+ this.collectionName = 'notes'
124
+ this.schemaName = 'note'
125
+ this.useDefaultRouteConfig()
126
+ }
127
+ ```
128
+
129
+ The default routes are:
130
+
131
+ ```javascript
51
132
  [
52
- // POST, no params
53
133
  {
54
134
  route: '/',
55
- modifying: true,
56
- handlers: {
57
- post: this.requestHandler()
58
- }
135
+ handlers: { post: handler, get: queryHandler },
136
+ permissions: { post: ['write:notes'], get: ['read:notes'] }
59
137
  },
60
- // GET, optional _id param
61
138
  {
62
- route: '/:_id?',
63
- handlers: {
64
- get: this.requestHandler()
65
- }
139
+ route: '/schema',
140
+ handlers: { get: serveSchema },
141
+ permissions: { get: ['read:schema'] }
66
142
  },
67
- // PUT/DELETE, mandatory _id param
68
143
  {
69
144
  route: '/:_id',
70
- modifying: true,
71
- handlers: {
72
- put: this.requestHandler(),
73
- delete: this.requestHandler()
74
- }
145
+ handlers: { put: handler, get: handler, patch: handler, delete: handler },
146
+ permissions: { put: ['write:notes'], get: ['read:notes'], patch: ['write:notes'], delete: ['write:notes'] }
75
147
  },
76
- // POST custom query handler
77
148
  {
78
149
  route: '/query',
79
150
  validate: false,
80
- handlers: {
81
- post: this.queryHandler()
82
- }
151
+ modifying: false,
152
+ handlers: { post: queryHandler },
153
+ permissions: { post: ['read:notes'] }
83
154
  }
84
- ];
85
- ```
86
- To use the above configuration, you simply need to call the `useDefaultRouteConfig` function:
87
- ```js
88
- async setValues() {
89
- this.useDefaultRouteConfig();
90
- }
91
- ```
92
-
93
- ### Example configurations
94
- ```js
95
- /**
96
- * Basic configuration
97
- */
98
- async setValues() {
99
- const server = await this.app.waitForModule('server');
100
- this.root = 'myapi';
101
- this.collectionName = 'mycollection';
102
- this.router = server.api.createChildRouter('myapi'); // optional
103
- this.router.addMiddleware(this.myMiddleware);
155
+ ]
156
+ ```
157
+
158
+ ## Custom routes
159
+
160
+ You can define custom routes by setting `this.routes` directly, or by adding to the default routes:
161
+
162
+ ### Adding routes to the defaults
163
+
164
+ ```javascript
165
+ async setValues () {
166
+ this.root = 'notes'
167
+ this.collectionName = 'notes'
168
+ this.schemaName = 'note'
169
+ // initialise the defaults routes
170
+ this.useDefaultRouteConfig()
171
+ // Add custom route
172
+ this.routes.push({
173
+ route: '/archive/:_id',
174
+ handlers: { post: this.archiveHandler.bind(this) },
175
+ permissions: { post: ['write:notes'] }
176
+ })
177
+ }
178
+ ```
179
+
180
+ ### Fully custom routes
181
+
182
+ ```javascript
183
+ async setValues () {
184
+ this.root = 'notes'
185
+ this.collectionName = 'notes'
186
+ this.schemaName = 'note'
187
+ // here we define our own completely custom list of routes, with no call to this.useDefaultRouteConfig
104
188
  this.routes = [
105
189
  {
106
190
  route: '/',
107
- handlers: { // if you need reference to 'this' in your handler, remember to bind
108
- get: this.myRequestHandler.bind(this)
191
+ handlers: {
192
+ get: this.listHandler.bind(this),
193
+ post: this.createHandler.bind(this)
194
+ },
195
+ permissions: {
196
+ get: ['read:notes'],
197
+ post: ['write:notes']
109
198
  }
110
199
  },
111
200
  {
112
- route: '/two',
113
- handlers: { // example of route-level middleware
114
- post: [ this.myOtherMiddleware, this.myPostHandler ]
201
+ route: '/:_id',
202
+ handlers: {
203
+ get: this.getHandler.bind(this),
204
+ delete: this.deleteHandler.bind(this)
205
+ },
206
+ permissions: {
207
+ get: ['read:notes'],
208
+ delete: ['write:notes']
115
209
  }
116
210
  }
117
- ];
211
+ ]
118
212
  }
119
- /**
120
- * Custom route configuration
121
- */
122
- async setValues() {
123
- // @note other values omitted for brevity
124
- this.routes = [
125
- {
126
- route: '/',
127
- schemaName: 'myschema', // can specify custom schema/collection like this
128
- collectionName: 'myothercollection',
129
- handlers: {
130
- get: this.myRequestHandler.bind(this)
131
- }
213
+ ```
214
+
215
+ ### Route configuration options
216
+
217
+ | Property | Type | Description |
218
+ | -------- | ---- | ----------- |
219
+ | `route` | String | URL path (supports Express route params like `/:_id`) |
220
+ | `handlers` | Object | Map of HTTP method to handler function(s) |
221
+ | `permissions` | Object | Map of HTTP method to required permission scopes |
222
+ | `modifying` | Boolean | Whether the route modifies data (affects `req.apiData.modifying`) |
223
+ | `validate` | Boolean | Whether to validate request data (default: `true`) |
224
+ | `collectionName` | String | Override the default collection for this route |
225
+ | `schemaName` | String | Override the default schema for this route |
226
+ | `meta` | Object | OpenAPI metadata for documentation |
227
+ | `internal` | Boolean | Restrict route to internal requests only (i.e. `localhost`) |
228
+
229
+ ### Route-level middleware
230
+
231
+ You can add middleware to specific routes by passing an array of handlers:
232
+
233
+ ```javascript
234
+ {
235
+ route: '/secure',
236
+ handlers: {
237
+ post: [this.validateInput.bind(this), this.secureHandler.bind(this)]
238
+ }
239
+ }
240
+ ```
241
+
242
+ ## Requests
243
+
244
+ ### How requests are handled
245
+
246
+ When using the default route configuration, `AbstractApiModule` provides two built-in handlers:
247
+
248
+ #### requestHandler
249
+ Handles standard CRUD operations (POST, GET, PUT, PATCH, DELETE). It automatically:
250
+ 1. Invokes the `requestHook` for any pre-processing
251
+ 2. Checks access permissions (before the operation for PUT/PATCH/DELETE, after for GET)
252
+ 3. Calls the appropriate database method based on the HTTP method
253
+ 4. Sanitises the response data to remove internal fields
254
+ 5. Returns the result with the appropriate status code (201 for POST, 200 for GET/PUT/PATCH, 204 for DELETE)
255
+
256
+ #### queryHandler
257
+ Handles advanced queries via POST to `/query`. It supports:
258
+ 1. MongoDB query operators in the request body (e.g., `$or`, `$gt`, `$regex`)
259
+ 2. Pagination via `page` and `limit` query parameters
260
+ 3. Sorting via a `sort` query parameter (e.g., `{"createdAt": -1}`)
261
+ 4. Skipping results via a `skip` query parameter
262
+
263
+ #### Pagination
264
+
265
+ When querying collections, the response includes pagination headers:
266
+
267
+ | Header | Description |
268
+ | ------ | ----------- |
269
+ | `X-Adapt-Page` | Current page number |
270
+ | `X-Adapt-PageSize` | Number of items per page |
271
+ | `X-Adapt-PageTotal` | Total number of pages |
272
+ | `Link` | Navigation links (first, prev, next, last) |
273
+
274
+ Configure default pagination in `adapt-authoring-api` config:
275
+
276
+ ```json
277
+ {
278
+ "defaultPageSize": 100,
279
+ "maxPageSize": 250
280
+ }
281
+ ```
282
+
283
+ ### Writing request handlers
284
+
285
+ Request handlers follow the Express middleware pattern:
286
+
287
+ ```javascript
288
+ async myHandler (req, res, next) {
289
+ try {
290
+ // Handle request
291
+ res.json(result)
292
+ } catch (e) {
293
+ next(e)
294
+ }
295
+ }
296
+ ```
297
+
298
+ ### Accessing request data
299
+
300
+ The `req.apiData` object contains various properties specific to your API which may come in useful when handling requests. [See this guide](request-response) for a full list of properties available on the `req`/`res` objects, including `apiData`.
301
+
302
+
303
+ ## Database operations
304
+
305
+ > For more information on interacting with the database, please [see this guide](using-mongodb?id=using-abstractapimodule)
306
+
307
+ `AbstractApiModule` provides the following methods for database operations:
308
+
309
+ - `find`
310
+ - `findOne`
311
+ - `insert`
312
+ - `update`
313
+ - `updateMany`
314
+ - `delete`
315
+ - `deleteMany`
316
+
317
+ These methods provide automatic data validation, caching, and lifecycle hooks in addition to the actual database operations without needing to duplicate that logic in every call.
318
+
319
+ ## Hooks
320
+
321
+ > For more detailed information on Hooks, [see this guide](hooks).
322
+
323
+ `AbstractApiModule` provides several useful hooks for customising behaviour both inside and outside of the API module. These hooks are automatically invoked by the `AbstractApiModule` code, so need no extra action. They provide a quick and easy way to interact with various important API activities, and are often the best way to extend your API with custom behaviours.
324
+
325
+ See the table below for a list of hooks provided by the `AbstractApiModule` class:
326
+
327
+ | Hook | When it fires | Mutable |
328
+ | ---- | ------------- | ------- |
329
+ | `requestHook` | When an API request is received | Yes |
330
+ | `preInsertHook` | Before inserting a document | Yes |
331
+ | `postInsertHook` | After inserting a document | No |
332
+ | `preUpdateHook` | Before updating a document | Yes |
333
+ | `postUpdateHook` | After updating a document | No |
334
+ | `preDeleteHook` | Before deleting a document | No |
335
+ | `postDeleteHook` | After deleting a document | No |
336
+ | `accessCheckHook` | When checking access to a resource | No |
337
+
338
+ ### Using hooks
339
+
340
+ ```javascript
341
+ import { Hook } from 'adapt-authoring-core'
342
+ import AbstractApiModule from 'adapt-authoring-api'
343
+
344
+ class NotesModule extends AbstractApiModule {
345
+ async init () {
346
+ await super.init()
347
+
348
+ /**
349
+ * Custom hook fired when a note is archived
350
+ * @type {Hook}
351
+ */
352
+ this.archivedHook = new Hook()
353
+
354
+ // Add timestamps when creating documents
355
+ this.preInsertHook.tap(data => {
356
+ data.createdAt = new Date().toISOString()
357
+ })
358
+ }
359
+
360
+ async archive () {
361
+ // Do some archiving...
362
+
363
+ // Invoke custom hook, passing the archived note to any observers
364
+ await this.archivedHook.invoke(note)
365
+ }
366
+ }
367
+ ```
368
+
369
+ ### Access control with accessCheckHook
370
+
371
+ Use `accessCheckHook` to implement custom access control:
372
+
373
+ ```javascript
374
+ this.accessCheckHook.tap(async (req, doc) => {
375
+ // Return true to allow access, false or undefined to deny
376
+ return doc.createdBy === req.auth.user._id.toString()
377
+ })
378
+ ```
379
+
380
+ ## Overriding methods
381
+
382
+ You can override database methods to customise behaviour:
383
+
384
+ ```javascript
385
+ async insert (...args) {
386
+ // Custom logic before insert
387
+ const result = await super.insert(...args)
388
+ // Custom logic after insert
389
+ return result
390
+ }
391
+ ```
392
+
393
+ Note in the above example we call `super.insert`, which will preserve the original function's behaviour. We strongly recommend calling the super class' function like in your override, as you may encounter unexpected issues if you completely replace the existing functionality with your own.
394
+
395
+ ### Overriding getSchemaName
396
+
397
+ A common candidate for override is `getSchemaName()`, as there are cases where you may need to dynamically select a schema based on the request data:
398
+
399
+ ```javascript
400
+ async getSchemaName (data) {
401
+ if (data._type === 'special') {
402
+ return 'specialnote'
403
+ }
404
+ return this.schemaName
405
+ }
406
+ ```
407
+
408
+ ## Permissions
409
+
410
+ Routes are secured using permission scopes. The default routes use `read:<root>` and `write:<root>` scopes, which are perfectly acceptable and recommended in most cases. We would suggest choosing a different scope when your API endpoint(s) perform non-standard actions, and so using a custom scope would be more descriptive - e.g. `build:myfeature` for a build tool API, or `debug` for debugging functionality.
411
+
412
+ > For more information on permissions, see [this guide](auth-permissions)
413
+
414
+ ### Custom permission scope
415
+
416
+ Set `permissionsScope` to use a different scope:
417
+
418
+ ```javascript
419
+ async setValues () {
420
+ this.root = 'notes'
421
+ this.permissionsScope = 'content' // Uses read:content, write:content
422
+ this.useDefaultRouteConfig()
423
+ }
424
+ ```
425
+
426
+ ### Per-route permissions
427
+
428
+ Define permissions per route and HTTP method:
429
+
430
+ ```javascript
431
+ {
432
+ route: '/admin',
433
+ handlers: { get: this.adminHandler.bind(this) },
434
+ permissions: { get: ['read:admin', 'read:notes'] } // Requires both scopes
435
+ }
436
+ ```
437
+
438
+ ## Caching
439
+
440
+ `AbstractApiModule` includes a data cache to reduce database calls. Configure it in `conf/config.schema.json`:
441
+
442
+ ```json
443
+ {
444
+ "enableCache": {
445
+ "type": "boolean",
446
+ "default": true
447
+ },
448
+ "cacheLifespan": {
449
+ "type": "number",
450
+ "default": 60000
451
+ }
452
+ }
453
+ ```
454
+
455
+ The cache is used automatically by the `find()` method.
456
+
457
+ ## Error handling
458
+
459
+ Use the application's error system for consistent error responses:
460
+
461
+ ```javascript
462
+ async myHandler (req, res, next) {
463
+ try {
464
+ const doc = await this.findOne({ _id: req.apiData.query._id })
465
+ if (doc.archived) {
466
+ return next(this.app.errors.UNAUTHORISED.setData({
467
+ reason: 'Cannot modify archived notes'
468
+ }))
132
469
  }
133
- ];
470
+ // Continue processing
471
+ } catch (e) {
472
+ next(e)
473
+ }
134
474
  }
135
475
  ```
476
+
477
+ Common errors from the API module:
478
+
479
+ | Error | Description |
480
+ | ----- | ----------- |
481
+ | `NOT_FOUND` | Resource not found |
482
+ | `UNAUTHORISED` | Access denied |
483
+ | `TOO_MANY_RESULTS` | Query returned multiple results when one was expected |
484
+ | `HTTP_METHOD_NOT_SUPPORTED` | HTTP method not supported for this route |
485
+
486
+ ## Writing documentation
487
+
488
+ If you plan on making your module available to others, it is vital that you provide good quality documentation to explain how it works. The Adapt authoring tool documentation covers three areas: the code, the REST API, and custom user manual pages. See the links below for more information on each:
489
+
490
+ - [JSDoc style guide](jsdoc-guide)
491
+ - [API documentation guide](rest-api-guide)
492
+ - [Developer manual](writing-documentation?id=developer-manual)
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "adapt-authoring-api",
3
- "version": "1.3.1",
3
+ "version": "1.3.2",
4
4
  "description": "Abstract module for creating APIs",
5
5
  "homepage": "https://github.com/adapt-security/adapt-authoring-api",
6
6
  "license": "GPL-3.0",
@@ -1,16 +0,0 @@
1
- name: Add labelled PRs to project
2
-
3
- on:
4
- pull_request:
5
- types: [ labeled ]
6
-
7
- jobs:
8
- add-to-project:
9
- if: ${{ github.event.label.name == 'dependencies' }}
10
- name: Add to main project
11
- runs-on: ubuntu-latest
12
- steps:
13
- - uses: actions/add-to-project@v0.1.0
14
- with:
15
- project-url: https://github.com/orgs/adapt-security/projects/5
16
- github-token: ${{ secrets.PROJECTS_SECRET }}