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.
- package/.github/workflows/new.yml +7 -11
- package/adapt-authoring.json +1 -1
- package/docs/request-response.md +186 -0
- package/docs/writing-an-api.md +452 -95
- package/package.json +1 -1
- package/.github/workflows/labelled_prs.yml +0 -16
|
@@ -1,19 +1,15 @@
|
|
|
1
|
-
|
|
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
|
-
|
|
14
|
-
|
|
15
|
-
|
|
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 }}
|
package/adapt-authoring.json
CHANGED
|
@@ -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
|
+
```
|
package/docs/writing-an-api.md
CHANGED
|
@@ -1,135 +1,492 @@
|
|
|
1
|
-
# Writing an API
|
|
1
|
+
# Writing an API module
|
|
2
2
|
|
|
3
|
-
|
|
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
|
-
|
|
5
|
+
By extending `AbstractApiModule`, your module automatically gets:
|
|
6
6
|
|
|
7
|
-
|
|
8
|
-
-
|
|
9
|
-
-
|
|
10
|
-
-
|
|
11
|
-
-
|
|
12
|
-
-
|
|
13
|
-
-
|
|
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
|
-
|
|
16
|
+
## Quick navigation
|
|
18
17
|
|
|
19
|
-
|
|
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
|
-
|
|
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
|
-
|
|
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
|
-
|
|
37
|
+
## Quick start
|
|
31
38
|
|
|
32
|
-
|
|
33
|
-
|
|
34
|
-
|
|
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
|
-
|
|
39
|
-
|
|
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
|
-
|
|
114
|
+
*Either `root` or `router` must be set.
|
|
42
115
|
|
|
43
|
-
|
|
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
|
-
|
|
49
|
-
|
|
50
|
-
```
|
|
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
|
-
|
|
56
|
-
|
|
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: '
|
|
63
|
-
handlers: {
|
|
64
|
-
|
|
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
|
-
|
|
71
|
-
|
|
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
|
-
|
|
81
|
-
|
|
82
|
-
}
|
|
151
|
+
modifying: false,
|
|
152
|
+
handlers: { post: queryHandler },
|
|
153
|
+
permissions: { post: ['read:notes'] }
|
|
83
154
|
}
|
|
84
|
-
]
|
|
85
|
-
```
|
|
86
|
-
|
|
87
|
-
|
|
88
|
-
|
|
89
|
-
|
|
90
|
-
|
|
91
|
-
|
|
92
|
-
|
|
93
|
-
|
|
94
|
-
|
|
95
|
-
|
|
96
|
-
|
|
97
|
-
|
|
98
|
-
|
|
99
|
-
|
|
100
|
-
|
|
101
|
-
this.
|
|
102
|
-
|
|
103
|
-
|
|
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: {
|
|
108
|
-
get: 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: '
|
|
113
|
-
handlers: {
|
|
114
|
-
|
|
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
|
-
|
|
121
|
-
|
|
122
|
-
|
|
123
|
-
|
|
124
|
-
|
|
125
|
-
|
|
126
|
-
|
|
127
|
-
|
|
128
|
-
|
|
129
|
-
|
|
130
|
-
|
|
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,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 }}
|