json-rest-api 1.0.24 → 1.0.25
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/README.md +2 -1
- package/docs/API.md +12 -9
- package/docs/GUIDE/GUIDE_1_Initial_Setup.md +5 -2
- package/docs/GUIDE/GUIDE_2_1_The_Starting_Point.md +3 -3
- package/docs/GUIDE/GUIDE_3_Field_Transformations.md +2 -0
- package/docs/GUIDE/GUIDE_7_Hooks_Data_Management_And_Plugins.md +1 -1
- package/docs/GUIDE/GUIDE_X_Autofiltering.md +63 -0
- package/docs/GUIDE/GUIDE_X_File_Uploads.md +17 -12
- package/docs/GUIDE/GUIDE_X_Row_Policies.md +603 -0
- package/docs/GUIDE/GUIDE_X_SocketIO.md +15 -10
- package/docs/GUIDE/GUIDE_X_URL_Management.md +93 -254
- package/docs/GUIDE/GUIDE_X_Writing_Plugins.md +2 -0
- package/docs/GUIDE/index.md +4 -1
- package/fixing_plan.md +112 -0
- package/index.js +2 -0
- package/package.json +1 -1
- package/plugins/core/bulk-operations-plugin.js +5 -4
- package/plugins/core/connectors/express-plugin.js +6 -1
- package/plugins/core/connectors/fastify-plugin.js +5 -1
- package/plugins/core/connectors/lib/connector-core.js +17 -11
- package/plugins/core/connectors/lib/transport-route-schemas.js +12 -60
- package/plugins/core/file-handling-plugin.js +126 -96
- package/plugins/core/lib/anyapi/anyapi-registry.js +21 -3
- package/plugins/core/lib/anyapi/schema-utils.js +1 -0
- package/plugins/core/lib/anyapi/utils/descriptor-helpers.js +7 -1
- package/plugins/core/lib/querying/knex-json-api-transformers-querying.js +20 -15
- package/plugins/core/lib/querying/knex-pagination-helpers.js +38 -84
- package/plugins/core/lib/querying/knex-relationship-includes.js +29 -28
- package/plugins/core/lib/querying/knex-window-queries.js +10 -77
- package/plugins/core/lib/querying/url-helpers.js +9 -21
- package/plugins/core/lib/querying-writing/connectors-query-parser.js +51 -0
- package/plugins/core/lib/querying-writing/database-capabilities.js +1 -1
- package/plugins/core/lib/querying-writing/database-value-normalizers.js +18 -9
- package/plugins/core/lib/querying-writing/knex-field-helpers.js +6 -1
- package/plugins/core/lib/querying-writing/relationship-contracts.js +56 -0
- package/plugins/core/lib/querying-writing/request-contracts.js +53 -4
- package/plugins/core/lib/querying-writing/return-record-settings.js +47 -0
- package/plugins/core/lib/querying-writing/schema-helpers.js +34 -41
- package/plugins/core/lib/storage/canonical-storage-mapping.js +2 -1
- package/plugins/core/lib/storage/storage-adapter.js +74 -13
- package/plugins/core/lib/writing/many-to-many-manipulations.js +4 -3
- package/plugins/core/lib/writing/relationship-processor.js +10 -1
- package/plugins/core/rest-api-anyapi-knex-plugin.js +210 -97
- package/plugins/core/rest-api-knex-plugin.js +70 -60
- package/plugins/core/rest-api-plugin-hooks/turn-scope-init-into-vars.js +8 -2
- package/plugins/core/rest-api-plugin-methods/common.js +85 -63
- package/plugins/core/rest-api-plugin-methods/delete-relationship.js +23 -13
- package/plugins/core/rest-api-plugin-methods/delete.js +2 -6
- package/plugins/core/rest-api-plugin-methods/get-related.js +15 -13
- package/plugins/core/rest-api-plugin-methods/get-relationship.js +1 -1
- package/plugins/core/rest-api-plugin-methods/patch-relationship.js +17 -6
- package/plugins/core/rest-api-plugin-methods/patch.js +3 -6
- package/plugins/core/rest-api-plugin-methods/post-relationship.js +24 -14
- package/plugins/core/rest-api-plugin-methods/post.js +4 -7
- package/plugins/core/rest-api-plugin-methods/put.js +4 -7
- package/plugins/core/rest-api-plugin.js +6 -36
- package/plugins/core/rest-api-row-policy-plugin.js +157 -0
- package/plugins/storage/local-storage.js +44 -19
- package/plugins/storage/s3-storage.js +21 -55
- package/tests/README.md +1 -1
- package/tests/anyapi-cursor-pagination.test.js +7 -1
- package/tests/anyapi-custom-idproperty.test.js +163 -0
- package/tests/fastify-plugin.test.js +28 -0
- package/tests/field-setters.test.js +10 -10
- package/tests/file-handling.test.js +118 -0
- package/tests/fixtures/api-configs.js +362 -4
- package/tests/full-jsonapi-belongsto-linkage.test.js +150 -0
- package/tests/http-connectors-parity.test.js +165 -5
- package/tests/local-storage.test.js +96 -0
- package/tests/pagination-cursor-multifield.test.js +121 -0
- package/tests/pagination-enhanced.test.js +66 -15
- package/tests/post-commit-hooks.test.js +120 -0
- package/tests/public-exports.test.js +15 -0
- package/tests/put-create-parity.test.js +216 -0
- package/tests/relationship-endpoints.test.js +259 -0
- package/tests/return-record-settings.test.js +184 -0
- package/tests/row-policy.test.js +433 -0
- package/tests/s3-storage.test.js +38 -0
- package/tests/searchschema-merge.test.js +17 -0
- package/tests/socketio.test.js +62 -0
- package/tests/storage-mapping.test.js +1 -0
- package/tests/test-development-guide.md +5 -5
|
@@ -2,316 +2,155 @@
|
|
|
2
2
|
|
|
3
3
|
## Overview
|
|
4
4
|
|
|
5
|
-
|
|
5
|
+
JSON REST API generates JSON:API links and `Location` headers from explicit library configuration. By default, generated URLs are relative to the configured transport `mountPath`.
|
|
6
6
|
|
|
7
|
-
|
|
7
|
+
Request headers such as `Host`, `X-Forwarded-Host`, and `X-Forwarded-Proto` are not trusted for URL generation. This keeps links from reflecting attacker-controlled request headers.
|
|
8
8
|
|
|
9
|
-
|
|
9
|
+
## Default Relative Links
|
|
10
10
|
|
|
11
|
-
|
|
12
|
-
|
|
13
|
-
1. **Protocol**: Detected from `X-Forwarded-Proto` header or request protocol
|
|
14
|
-
2. **Host**: Detected from `X-Forwarded-Host` or `Host` header
|
|
15
|
-
3. **Mount Path**: Where your API routes are mounted (e.g., `/api`)
|
|
11
|
+
For a standard Express or Fastify setup:
|
|
16
12
|
|
|
17
13
|
```javascript
|
|
18
|
-
// Example: Simple setup - no URL configuration needed!
|
|
19
14
|
await api.use(RestApiPlugin);
|
|
20
15
|
await api.use(RestApiKnexPlugin, { knex });
|
|
21
16
|
await api.use(ExpressPlugin, { mountPath: '/api' });
|
|
22
17
|
```
|
|
23
18
|
|
|
24
|
-
|
|
19
|
+
Generated links use the mount path:
|
|
25
20
|
|
|
26
|
-
|
|
27
|
-
|
|
28
|
-
|
|
29
|
-
|
|
21
|
+
```json
|
|
22
|
+
{
|
|
23
|
+
"data": {
|
|
24
|
+
"type": "books",
|
|
25
|
+
"id": "123",
|
|
26
|
+
"links": {
|
|
27
|
+
"self": "/api/books/123"
|
|
28
|
+
}
|
|
29
|
+
}
|
|
30
|
+
}
|
|
30
31
|
```
|
|
31
32
|
|
|
32
|
-
|
|
33
|
-
```
|
|
34
|
-
Request: GET https://api.example.com/api/books
|
|
35
|
-
Response links: https://api.example.com/api/books/1
|
|
36
|
-
```
|
|
33
|
+
This is the recommended default for most deployments. Browsers and HTTP clients resolve the relative links against the origin they already used to call the API.
|
|
37
34
|
|
|
38
|
-
|
|
39
|
-
```nginx
|
|
40
|
-
# Nginx configuration
|
|
41
|
-
proxy_set_header X-Forwarded-Proto $scheme;
|
|
42
|
-
proxy_set_header X-Forwarded-Host $host;
|
|
43
|
-
proxy_pass http://backend:3000;
|
|
44
|
-
```
|
|
35
|
+
## Absolute Public Links
|
|
45
36
|
|
|
46
|
-
|
|
47
|
-
Request headers:
|
|
48
|
-
X-Forwarded-Proto: https
|
|
49
|
-
X-Forwarded-Host: api.example.com
|
|
50
|
-
Response links: https://api.example.com/api/books/1
|
|
51
|
-
```
|
|
37
|
+
When clients require absolute public URLs, configure the connector with `publicBaseUrl`. The value is the complete public URL prefix for generated API links, including any public path prefix such as `/api`.
|
|
52
38
|
|
|
53
|
-
|
|
54
|
-
|
|
39
|
+
```javascript
|
|
40
|
+
await api.use(ExpressPlugin, {
|
|
41
|
+
mountPath: '/api',
|
|
42
|
+
publicBaseUrl: 'https://api.example.com/api'
|
|
43
|
+
});
|
|
55
44
|
```
|
|
56
|
-
Tenant A request: GET https://customer-a.api.com/api/books
|
|
57
|
-
Response links: https://customer-a.api.com/api/books/1
|
|
58
45
|
|
|
59
|
-
|
|
60
|
-
|
|
46
|
+
Fastify uses the same option:
|
|
47
|
+
|
|
48
|
+
```javascript
|
|
49
|
+
await api.use(FastifyPlugin, {
|
|
50
|
+
app,
|
|
51
|
+
mountPath: '/api',
|
|
52
|
+
publicBaseUrl: 'https://api.example.com/api'
|
|
53
|
+
});
|
|
61
54
|
```
|
|
62
55
|
|
|
63
|
-
|
|
56
|
+
Generated links then use the configured public base URL:
|
|
57
|
+
|
|
58
|
+
```json
|
|
59
|
+
{
|
|
60
|
+
"links": {
|
|
61
|
+
"self": "https://api.example.com/api/books/123"
|
|
62
|
+
}
|
|
63
|
+
}
|
|
64
|
+
```
|
|
64
65
|
|
|
65
|
-
|
|
66
|
+
Trailing slashes on `publicBaseUrl` are normalized before links are built.
|
|
66
67
|
|
|
67
|
-
|
|
68
|
+
## Per-Request Overrides
|
|
68
69
|
|
|
69
|
-
|
|
70
|
+
For multi-tenant deployments or API gateways with request-specific public URLs, set `context.urlPrefixOverride` in a `transport:request` hook. Only set this from trusted configuration or validated request metadata.
|
|
70
71
|
|
|
71
72
|
```javascript
|
|
72
|
-
|
|
73
|
-
name: 'my-api',
|
|
74
|
-
|
|
75
|
-
// Define hooks at API creation time
|
|
73
|
+
await api.customize({
|
|
76
74
|
hooks: {
|
|
77
|
-
'transport:request':
|
|
78
|
-
|
|
79
|
-
|
|
80
|
-
|
|
81
|
-
|
|
82
|
-
|
|
83
|
-
|
|
75
|
+
'transport:request': {
|
|
76
|
+
functionName: 'tenant-url-prefix',
|
|
77
|
+
handler: async ({ context }) => {
|
|
78
|
+
const tenantId = context.auth?.tenantId;
|
|
79
|
+
const tenantPublicUrls = {
|
|
80
|
+
tenant_a: 'https://tenant-a.example.com/api',
|
|
81
|
+
tenant_b: 'https://tenant-b.example.com/api'
|
|
82
|
+
};
|
|
83
|
+
|
|
84
|
+
if (tenantPublicUrls[tenantId]) {
|
|
85
|
+
context.urlPrefixOverride = tenantPublicUrls[tenantId];
|
|
84
86
|
}
|
|
85
|
-
|
|
86
|
-
// Example: API versioning
|
|
87
|
-
if (req?.headers?.['x-api-version'] === 'v2') {
|
|
88
|
-
context.urlPrefixOverride = 'https://api.example.com/v2';
|
|
89
|
-
}
|
|
90
|
-
|
|
91
|
-
// Example: Multi-tenant based on host
|
|
92
|
-
const host = req?.hostname;
|
|
93
|
-
if (host?.includes('tenant-a')) {
|
|
94
|
-
context.urlPrefixOverride = 'https://tenant-a.api.com/api';
|
|
95
|
-
}
|
|
96
|
-
|
|
97
|
-
// Example: Environment-based override
|
|
98
|
-
if (process.env.PUBLIC_API_URL) {
|
|
99
|
-
context.urlPrefixOverride = process.env.PUBLIC_API_URL;
|
|
100
|
-
}
|
|
101
|
-
|
|
102
|
-
return payload;
|
|
103
87
|
}
|
|
104
|
-
|
|
88
|
+
}
|
|
105
89
|
}
|
|
106
90
|
});
|
|
107
|
-
|
|
108
|
-
// Then use plugins normally
|
|
109
|
-
await api.use(RestApiPlugin);
|
|
110
|
-
await api.use(RestApiKnexPlugin, { knex });
|
|
111
|
-
await api.use(ExpressPlugin, { mountPath: '/api' });
|
|
112
91
|
```
|
|
113
92
|
|
|
114
|
-
|
|
115
|
-
|
|
116
|
-
If you cannot use hooks, you can use Express middleware before mounting the API:
|
|
93
|
+
Express middleware can also set `req.urlPrefixOverride` before the API router runs:
|
|
117
94
|
|
|
118
95
|
```javascript
|
|
96
|
+
const allowedPublicUrls = new Set([
|
|
97
|
+
'https://api.example.com/api',
|
|
98
|
+
'https://partner.example.com/api'
|
|
99
|
+
]);
|
|
100
|
+
|
|
119
101
|
app.use((req, res, next) => {
|
|
120
|
-
|
|
121
|
-
if (
|
|
122
|
-
req.urlPrefixOverride =
|
|
102
|
+
const requestedPublicUrl = req.get('x-public-url');
|
|
103
|
+
if (allowedPublicUrls.has(requestedPublicUrl)) {
|
|
104
|
+
req.urlPrefixOverride = requestedPublicUrl;
|
|
123
105
|
}
|
|
124
106
|
next();
|
|
125
107
|
});
|
|
126
108
|
|
|
127
|
-
|
|
128
|
-
app.use(api.http.express.router);
|
|
109
|
+
api.http.express.mount(app);
|
|
129
110
|
```
|
|
130
111
|
|
|
131
|
-
|
|
132
|
-
|
|
133
|
-
1. **API Gateway with Path Rewriting**
|
|
134
|
-
```javascript
|
|
135
|
-
hooks: {
|
|
136
|
-
'transport:request': [async (payload) => {
|
|
137
|
-
if (payload.req?.headers?.['x-forwarded-prefix'] === '/v2') {
|
|
138
|
-
payload.context.urlPrefixOverride = 'https://api.company.com/v2';
|
|
139
|
-
}
|
|
140
|
-
return payload;
|
|
141
|
-
}]
|
|
142
|
-
}
|
|
143
|
-
```
|
|
144
|
-
|
|
145
|
-
2. **CDN with Different Public URL**
|
|
146
|
-
```javascript
|
|
147
|
-
hooks: {
|
|
148
|
-
'transport:request': [async (payload) => {
|
|
149
|
-
if (payload.req?.headers?.['x-cdn-host']) {
|
|
150
|
-
payload.context.urlPrefixOverride = `https://${payload.req.headers['x-cdn-host']}/api`;
|
|
151
|
-
}
|
|
152
|
-
return payload;
|
|
153
|
-
}]
|
|
154
|
-
}
|
|
155
|
-
```
|
|
156
|
-
|
|
157
|
-
3. **Multi-Tenant SaaS**
|
|
158
|
-
```javascript
|
|
159
|
-
hooks: {
|
|
160
|
-
'transport:request': [async (payload) => {
|
|
161
|
-
const { context, req } = payload;
|
|
162
|
-
const tenant = req?.hostname?.split('.')[0];
|
|
163
|
-
if (tenant && tenant !== 'api') {
|
|
164
|
-
context.urlPrefixOverride = `https://${tenant}.api.com/api`;
|
|
165
|
-
}
|
|
166
|
-
return payload;
|
|
167
|
-
}]
|
|
168
|
-
}
|
|
169
|
-
```
|
|
170
|
-
|
|
171
|
-
4. **Environment-Based URLs**
|
|
172
|
-
```javascript
|
|
173
|
-
hooks: {
|
|
174
|
-
'transport:request': [async (payload) => {
|
|
175
|
-
// Force production URLs in staging for testing
|
|
176
|
-
if (process.env.NODE_ENV === 'staging' && process.env.PRODUCTION_URL) {
|
|
177
|
-
payload.context.urlPrefixOverride = process.env.PRODUCTION_URL;
|
|
178
|
-
}
|
|
179
|
-
return payload;
|
|
180
|
-
}]
|
|
181
|
-
}
|
|
182
|
-
```
|
|
183
|
-
|
|
184
|
-
## What You DON'T Need to Configure
|
|
185
|
-
|
|
186
|
-
❌ **No need for:**
|
|
187
|
-
- Static URL configuration
|
|
188
|
-
- Per-environment URL settings
|
|
189
|
-
- Manual protocol detection
|
|
190
|
-
- Proxy configuration in the API
|
|
191
|
-
|
|
192
|
-
The system automatically handles:
|
|
193
|
-
- HTTP vs HTTPS detection
|
|
194
|
-
- Domain detection from headers
|
|
195
|
-
- Proxy headers (`X-Forwarded-*`)
|
|
196
|
-
- Port numbers
|
|
197
|
-
- Mount paths
|
|
198
|
-
|
|
199
|
-
## URL Generation Examples
|
|
200
|
-
|
|
201
|
-
### Resource URLs
|
|
202
|
-
```javascript
|
|
203
|
-
// Generated in responses as:
|
|
204
|
-
{
|
|
205
|
-
"type": "books",
|
|
206
|
-
"id": "123",
|
|
207
|
-
"attributes": { ... },
|
|
208
|
-
"links": {
|
|
209
|
-
"self": "https://api.example.com/api/books/123" // Automatically generated
|
|
210
|
-
}
|
|
211
|
-
}
|
|
212
|
-
```
|
|
112
|
+
Do not copy arbitrary `Host`, `X-Forwarded-Host`, or custom public URL headers into `urlPrefixOverride`. If a reverse proxy supplies public URL metadata, validate it against trusted configuration first.
|
|
213
113
|
|
|
214
|
-
|
|
215
|
-
```javascript
|
|
216
|
-
{
|
|
217
|
-
"relationships": {
|
|
218
|
-
"author": {
|
|
219
|
-
"links": {
|
|
220
|
-
"self": "https://api.example.com/api/books/123/relationships/author",
|
|
221
|
-
"related": "https://api.example.com/api/books/123/author"
|
|
222
|
-
}
|
|
223
|
-
}
|
|
224
|
-
}
|
|
225
|
-
}
|
|
226
|
-
```
|
|
114
|
+
## Priority Order
|
|
227
115
|
|
|
228
|
-
|
|
229
|
-
```javascript
|
|
230
|
-
{
|
|
231
|
-
"links": {
|
|
232
|
-
"self": "https://api.example.com/api/books?page[number]=2",
|
|
233
|
-
"first": "https://api.example.com/api/books?page[number]=1",
|
|
234
|
-
"prev": "https://api.example.com/api/books?page[number]=1",
|
|
235
|
-
"next": "https://api.example.com/api/books?page[number]=3",
|
|
236
|
-
"last": "https://api.example.com/api/books?page[number]=10"
|
|
237
|
-
}
|
|
238
|
-
}
|
|
239
|
-
```
|
|
116
|
+
URL prefix resolution uses this order:
|
|
240
117
|
|
|
241
|
-
|
|
118
|
+
1. `context.urlPrefixOverride`
|
|
119
|
+
2. Connector `publicBaseUrl`
|
|
120
|
+
3. Connector `mountPath`
|
|
121
|
+
4. Empty string
|
|
242
122
|
|
|
243
|
-
|
|
244
|
-
2. **Multi-Domain Safe**: Each request gets appropriate URLs
|
|
245
|
-
3. **Proxy Friendly**: Automatically handles reverse proxies
|
|
246
|
-
4. **Flexible**: Override when needed for complex scenarios
|
|
247
|
-
5. **Stateless**: No global state that can be contaminated
|
|
123
|
+
The same helper is used for resource links, relationship links, pagination links, and connector `Location` headers.
|
|
248
124
|
|
|
249
|
-
##
|
|
125
|
+
## Reverse Proxies
|
|
250
126
|
|
|
251
|
-
|
|
252
|
-
- Ensure your reverse proxy is setting `X-Forwarded-Host` and `X-Forwarded-Proto` headers
|
|
253
|
-
- Check that these headers are being passed through to your application
|
|
127
|
+
A reverse proxy should still forward requests to the Node.js application normally, but URL generation does not require forwarding public host headers.
|
|
254
128
|
|
|
255
|
-
|
|
256
|
-
- Use the `urlPrefixOverride` pattern with environment variables
|
|
257
|
-
- Set `PUBLIC_API_URL` environment variable appropriately
|
|
129
|
+
Use explicit configuration for public links:
|
|
258
130
|
|
|
259
|
-
|
|
260
|
-
|
|
261
|
-
|
|
131
|
+
```javascript
|
|
132
|
+
await api.use(ExpressPlugin, {
|
|
133
|
+
mountPath: '/api',
|
|
134
|
+
publicBaseUrl: process.env.PUBLIC_API_URL
|
|
135
|
+
});
|
|
136
|
+
```
|
|
262
137
|
|
|
263
|
-
|
|
138
|
+
Set `PUBLIC_API_URL` to the externally visible API prefix, for example `https://api.example.com/api`.
|
|
264
139
|
|
|
265
|
-
|
|
140
|
+
## Troubleshooting
|
|
266
141
|
|
|
267
|
-
|
|
268
|
-
import { Api } from 'hooked-api';
|
|
269
|
-
import { RestApiPlugin, RestApiKnexPlugin, ExpressPlugin } from 'json-rest-api';
|
|
270
|
-
import express from 'express';
|
|
142
|
+
### Links are relative
|
|
271
143
|
|
|
272
|
-
|
|
273
|
-
const api = new Api({
|
|
274
|
-
name: 'my-api',
|
|
275
|
-
hooks: {
|
|
276
|
-
'transport:request': [
|
|
277
|
-
async (payload) => {
|
|
278
|
-
const { context, req } = payload;
|
|
279
|
-
|
|
280
|
-
// Check for custom public URL header
|
|
281
|
-
if (req?.headers?.['x-public-url']) {
|
|
282
|
-
context.urlPrefixOverride = req.headers['x-public-url'];
|
|
283
|
-
}
|
|
284
|
-
|
|
285
|
-
return payload;
|
|
286
|
-
}
|
|
287
|
-
]
|
|
288
|
-
}
|
|
289
|
-
});
|
|
144
|
+
This is the default behavior. Configure `publicBaseUrl` if your clients require absolute URLs.
|
|
290
145
|
|
|
291
|
-
|
|
292
|
-
await api.use(RestApiPlugin);
|
|
293
|
-
await api.use(RestApiKnexPlugin, { knex });
|
|
294
|
-
await api.use(ExpressPlugin, { mountPath: '/api' });
|
|
146
|
+
### Links are missing a public path prefix
|
|
295
147
|
|
|
296
|
-
|
|
297
|
-
await api.addResource('books', { /* schema */ });
|
|
148
|
+
Include the public path prefix in `publicBaseUrl`.
|
|
298
149
|
|
|
299
|
-
|
|
300
|
-
|
|
301
|
-
app.use(api.http.express.router);
|
|
302
|
-
app.listen(3000);
|
|
150
|
+
```javascript
|
|
151
|
+
publicBaseUrl: 'https://api.example.com/v2'
|
|
303
152
|
```
|
|
304
153
|
|
|
305
|
-
|
|
306
|
-
- `curl http://localhost:3000/api/books` → URLs: `http://localhost:3000/api/books/1`
|
|
307
|
-
- `curl -H "X-Public-URL: https://cdn.example.com/api" http://localhost:3000/api/books` → URLs: `https://cdn.example.com/api/books/1`
|
|
308
|
-
|
|
309
|
-
## Summary
|
|
310
|
-
|
|
311
|
-
The JSON REST API's URL management system provides:
|
|
312
|
-
- **Automatic detection** for 99% of use cases
|
|
313
|
-
- **Hook-based override** for complex deployments
|
|
314
|
-
- **Per-request isolation** preventing cross-domain issues
|
|
315
|
-
- **No configuration** required for standard deployments
|
|
154
|
+
### Multi-tenant links need different hosts
|
|
316
155
|
|
|
317
|
-
|
|
156
|
+
Use `context.urlPrefixOverride` from trusted tenant or auth state. Avoid deriving tenant URLs directly from untrusted request host headers.
|
|
@@ -106,6 +106,8 @@ addHook('knexQueryFiltering', 'scope-by-workspace', {}, async ({ context }) => {
|
|
|
106
106
|
|
|
107
107
|
`knexQueryFiltering` is the right seam for filtering. It is **not** the right seam for turning ad hoc SQL aliases into first-class fields.
|
|
108
108
|
|
|
109
|
+
For mandatory resource visibility, prefer the supported [`RowPolicyPlugin`](GUIDE_X_Row_Policies.md) contract over registering a raw filtering hook. It validates resource configuration, requires an explicit allow/deny decision, provides logical field translation, exposes query-purpose metadata, and is exercised across pagination counts, single-record preflights, includes, and relationship loading.
|
|
110
|
+
|
|
109
111
|
### `addScopeMethod`
|
|
110
112
|
|
|
111
113
|
Use this when a plugin needs a reusable method on every resource or selected resources.
|
package/docs/GUIDE/index.md
CHANGED
|
@@ -28,7 +28,7 @@ Welcome to the comprehensive guide for JSON REST API. This guide will walk you t
|
|
|
28
28
|
### Additional Topics
|
|
29
29
|
|
|
30
30
|
- **[File Uploads](GUIDE_X_File_Uploads.md)**
|
|
31
|
-
Handle file uploads with
|
|
31
|
+
Handle file uploads with local storage and a mock S3-style demo adapter.
|
|
32
32
|
|
|
33
33
|
- **[Bulk Operations](GUIDE_X_Bulk_Operations.md)**
|
|
34
34
|
Handle multiple records efficiently with bulk create, update, and delete operations.
|
|
@@ -39,6 +39,9 @@ Welcome to the comprehensive guide for JSON REST API. This guide will walk you t
|
|
|
39
39
|
- **[Autofiltering](GUIDE_X_Autofiltering.md)**
|
|
40
40
|
Scope datasets automatically using configured context values, without baking in auth semantics.
|
|
41
41
|
|
|
42
|
+
- **[Row Policies](GUIDE_X_Row_Policies.md)**
|
|
43
|
+
Apply mandatory server-side visibility predicates before pagination, counts, includes, and relationship loading.
|
|
44
|
+
|
|
42
45
|
- **[Knex Schema and Migrations](GUIDE_X_Knex_Schema_And_Migrations.md)**
|
|
43
46
|
Create tables, inspect live table snapshots, and generate create or additive diff migrations from table-backed resource scopes.
|
|
44
47
|
|
package/fixing_plan.md
ADDED
|
@@ -0,0 +1,112 @@
|
|
|
1
|
+
# Fixing Plan
|
|
2
|
+
|
|
3
|
+
Checklist-style plan for addressing the audit findings. Items marked as requiring approval change public behavior, public API, or dependencies.
|
|
4
|
+
|
|
5
|
+
## High Priority
|
|
6
|
+
|
|
7
|
+
- [x] Fix AnyAPI custom `idProperty` canonical mapping.
|
|
8
|
+
- [x] Treat `schemaInfo.idProperty` as an alias of logical `id` in the canonical storage adapter.
|
|
9
|
+
- [x] Skip custom id fields such as `item_id` when building DB attribute selections.
|
|
10
|
+
- [x] Ensure cursor, sort, projection, sparse fieldset, and include paths translate both `id` and `idProperty` to `logical_id`.
|
|
11
|
+
- [x] Add AnyAPI tests for custom-id `post`, `get`, `query`, sparse fields, and cursor pagination.
|
|
12
|
+
|
|
13
|
+
- [x] Fix AnyAPI PUT create parity.
|
|
14
|
+
- [x] Branch `helpers.dataPut` on `context.isCreate`.
|
|
15
|
+
- [x] In create mode, insert into `any_records` with tenant, resource, logical id, and translated attributes.
|
|
16
|
+
- [x] Keep the current update path for update mode.
|
|
17
|
+
- [x] Add legacy Knex and AnyAPI parity tests for `PUT /resource/:id` create, including custom IDs.
|
|
18
|
+
|
|
19
|
+
- [x] Fix pagination link query-string round trips.
|
|
20
|
+
- [x] Replace manual query-string concatenation with one serializer that is the inverse of `parseJsonApiQuery`.
|
|
21
|
+
- [x] Use `URLSearchParams` for keys and values.
|
|
22
|
+
- [x] Map internal `queryParams.filters` back to public JSON:API `filter[...]`.
|
|
23
|
+
- [x] Add tests that parse generated `links.next` and prove filters, fields, sort, include, and page params survive round trip.
|
|
24
|
+
|
|
25
|
+
- [x] Fix relationship PATCH cardinality validation.
|
|
26
|
+
- [x] Generate relationship-aware request contracts from relationship metadata.
|
|
27
|
+
- [x] Allow to-one relationship PATCH data to be `null` or one resource identifier only.
|
|
28
|
+
- [x] Allow to-many relationship data to be arrays only.
|
|
29
|
+
- [x] Make `processRelationships()` throw when a belongsTo relationship receives an array.
|
|
30
|
+
- [x] Add API and transport tests for invalid to-one arrays and invalid to-many scalars.
|
|
31
|
+
|
|
32
|
+
- [x] Run `afterCommit` for relationship routes.
|
|
33
|
+
- [x] After `postRelationship`, `patchRelationship`, and `deleteRelationship` commit their own transaction, run `afterCommit`.
|
|
34
|
+
- [x] Preserve rollback behavior through `afterRollback`.
|
|
35
|
+
- [x] Verify Socket.IO deferred relationship broadcasts work, or define a separate relationship notification policy.
|
|
36
|
+
- [x] Add tests with an `afterCommit` hook counter and Socket.IO relationship-write subscription behavior.
|
|
37
|
+
|
|
38
|
+
- [x] Decide and fix post-commit hook failure semantics.
|
|
39
|
+
- [x] Split transaction work from post-commit side effects.
|
|
40
|
+
- [x] Once `commit()` succeeds, do not call rollback handling.
|
|
41
|
+
- [x] Surface post-commit hook errors while preserving already-committed data.
|
|
42
|
+
- [x] Add tests proving the row remains committed and the response/error semantics are intentional.
|
|
43
|
+
|
|
44
|
+
## Medium Priority
|
|
45
|
+
|
|
46
|
+
- [x] Fix AnyAPI pagination fixture cleanup.
|
|
47
|
+
- [x] Register `cursor_products` and `cursor_items` with `storageMode.registerTable()` in `createCursorPaginationApi`.
|
|
48
|
+
- [x] Add a focused cleanup test proving `cleanTables(knex, ['cursor_products'])` deletes matching `any_records`.
|
|
49
|
+
- [x] Keep metadata-derived cleanup mappings as a later refactor if a broader helper change is approved.
|
|
50
|
+
|
|
51
|
+
- [x] Fix file temp and orphan cleanup.
|
|
52
|
+
- [x] Wrap file processing in `try/finally` so detector temp files are cleaned after validation failures.
|
|
53
|
+
- [x] Track successfully uploaded URLs on `context`.
|
|
54
|
+
- [x] On rollback or later write error, call storage `delete(url)` where available.
|
|
55
|
+
- [x] Document `delete(url)` as the rollback-capable storage contract.
|
|
56
|
+
- [x] Add tests for invalid MIME cleanup and post-upload DB/validation rollback cleanup.
|
|
57
|
+
|
|
58
|
+
- [x] Harden `LocalStorage` path containment.
|
|
59
|
+
- [x] Sanitize or reject custom basenames containing `/`, `\`, drive prefixes, or empty names.
|
|
60
|
+
- [x] Replace `startsWith(resolvedDir)` containment with `path.relative(resolvedDir, resolvedPath)`.
|
|
61
|
+
- [x] Add tests for sibling-prefix traversal such as `../uploads_evil/pwn`.
|
|
62
|
+
|
|
63
|
+
- [x] Fix untrusted proxy URL generation. Requires maintainer approval.
|
|
64
|
+
- [x] Stop trusting `Host` and `X-Forwarded-*` by default for absolute links.
|
|
65
|
+
- [x] Prefer relative links unless `urlPrefixOverride` or an explicit public base URL is configured.
|
|
66
|
+
- [x] If forwarded headers remain supported, gate them behind `trustProxy` and allowed host/proto validation. Resolved by not supporting forwarded headers for URL generation.
|
|
67
|
+
- [x] Add Express and Fastify tests with hostile `Host` and `X-Forwarded-Host` headers.
|
|
68
|
+
|
|
69
|
+
- [x] Fix `S3Storage` non-mock behavior. Dependency/public behavior choice requires maintainer approval.
|
|
70
|
+
- [x] Smallest safe fix: throw a clear "real S3 storage is not implemented" error when `mockMode: false`.
|
|
71
|
+
- [x] Reject full AWS SDK support for this pass because it adds dependency, credential, and public behavior scope.
|
|
72
|
+
- [x] Add tests for mock mode and the chosen non-mock behavior.
|
|
73
|
+
|
|
74
|
+
## Low Priority
|
|
75
|
+
|
|
76
|
+
- [x] Make field setter errors fail closed.
|
|
77
|
+
- [x] Change setter error handling to throw by default.
|
|
78
|
+
- [x] Do not add an opt-in non-fatal mode without a real use case.
|
|
79
|
+
- [x] Add a rollback test where a setter throws and no row is persisted.
|
|
80
|
+
|
|
81
|
+
- [x] Normalize return-record boolean compatibility.
|
|
82
|
+
- [x] Create one shared return-record normalizer.
|
|
83
|
+
- [x] Normalize `true -> 'full'`, `false -> 'no'`, and keep valid strings unchanged.
|
|
84
|
+
- [x] Use the shared normalizer in plugin install and per-call setup.
|
|
85
|
+
- [x] Update tests and docs to prefer `'no'`, `'minimal'`, and `'full'` while preserving boolean compatibility.
|
|
86
|
+
|
|
87
|
+
## Contract Drift
|
|
88
|
+
|
|
89
|
+
- [x] Fix Socket.IO public exports and docs.
|
|
90
|
+
- [x] Export `SocketIOPlugin` from `index.js`.
|
|
91
|
+
- [x] Fix docs to import `Api` from `hooked-api`, not `json-rest-api`.
|
|
92
|
+
- [x] Remove invalid deep imports such as `json-rest-api/plugins/socketio`, or add formal package subpath exports. Subpath exports require maintainer approval.
|
|
93
|
+
- [x] Add a public import smoke test.
|
|
94
|
+
|
|
95
|
+
- [x] Align S3 documentation with implementation.
|
|
96
|
+
- [x] Since real S3 is not implemented, make README and guide text state that the included adapter is mock/demo only.
|
|
97
|
+
- [x] Leave production S3 as a future approved dependency/API design rather than documenting unsupported behavior.
|
|
98
|
+
|
|
99
|
+
- [x] Finish relationship validation contract consolidation.
|
|
100
|
+
- [x] Move relationship-route schemas into the same request-contract surface as resource routes.
|
|
101
|
+
- [x] Do this after the cardinality fix so the centralized contract is correct rather than merely centralized.
|
|
102
|
+
|
|
103
|
+
## Verification Checklist
|
|
104
|
+
|
|
105
|
+
- [x] `npm test`
|
|
106
|
+
- [x] `npm run lint`
|
|
107
|
+
- [x] `npm run test:anyapi`
|
|
108
|
+
- [x] `npm run verify`
|
|
109
|
+
- [x] Narrow repro for AnyAPI custom `idProperty`
|
|
110
|
+
- [x] Narrow repro for relationship route `afterCommit`
|
|
111
|
+
- [x] Narrow repro for pagination link round trip
|
|
112
|
+
- [x] Import smoke test for public exports
|
package/index.js
CHANGED
|
@@ -11,10 +11,12 @@
|
|
|
11
11
|
// Core plugins
|
|
12
12
|
export { RestApiPlugin } from './plugins/core/rest-api-plugin.js'
|
|
13
13
|
export { AutoFilterPlugin } from './plugins/core/rest-api-autofilter-plugin.js'
|
|
14
|
+
export { RowPolicyPlugin } from './plugins/core/rest-api-row-policy-plugin.js'
|
|
14
15
|
export { QueryProjectionsPlugin } from './plugins/core/rest-api-query-projections-plugin.js'
|
|
15
16
|
export { FileHandlingPlugin } from './plugins/core/file-handling-plugin.js'
|
|
16
17
|
export { CorsPlugin } from './plugins/core/rest-api-cors-plugin.js'
|
|
17
18
|
export { LabelPlugin } from './plugins/core/rest-api-label-plugin.js'
|
|
19
|
+
export { SocketIOPlugin } from './plugins/core/socketio-plugin.js'
|
|
18
20
|
|
|
19
21
|
// Database plugins
|
|
20
22
|
export { RestApiKnexPlugin } from './plugins/core/rest-api-knex-plugin.js'
|
package/package.json
CHANGED
|
@@ -193,11 +193,12 @@ export const BulkOperationsPlugin = {
|
|
|
193
193
|
transaction
|
|
194
194
|
}, recordContext)
|
|
195
195
|
|
|
196
|
-
// If patch doesn't return full record, fetch
|
|
197
|
-
|
|
198
|
-
|
|
196
|
+
// If patch doesn't return a full record, fetch the updated record.
|
|
197
|
+
const resultId = result?.data?.id || result?.id || operation.id
|
|
198
|
+
let resultData = result?.data
|
|
199
|
+
if (!resultData && resultId) {
|
|
199
200
|
const fetchedRecord = await scope.get({
|
|
200
|
-
id:
|
|
201
|
+
id: resultId,
|
|
201
202
|
transaction
|
|
202
203
|
}, recordContext)
|
|
203
204
|
resultData = fetchedRecord.data
|
|
@@ -66,6 +66,7 @@ export const ExpressPlugin = {
|
|
|
66
66
|
// Get mountPath from options (this is now a transport concern)
|
|
67
67
|
const mountPath = expressOptions.mountPath || ''
|
|
68
68
|
const basePath = mountPath // Keep basePath for internal use
|
|
69
|
+
const publicBaseUrl = expressOptions.publicBaseUrl || ''
|
|
69
70
|
const strictContentType = expressOptions.strictContentType !== false
|
|
70
71
|
const requestSizeLimit = expressOptions.requestSizeLimit || '1mb'
|
|
71
72
|
|
|
@@ -73,7 +74,8 @@ export const ExpressPlugin = {
|
|
|
73
74
|
vars.transport = {
|
|
74
75
|
type: 'express',
|
|
75
76
|
matchAll: '*', // Express wildcard pattern for matching all routes
|
|
76
|
-
mountPath // Transport-specific mount path
|
|
77
|
+
mountPath, // Transport-specific mount path
|
|
78
|
+
publicBaseUrl
|
|
77
79
|
}
|
|
78
80
|
|
|
79
81
|
// Register file detector if enabled
|
|
@@ -142,6 +144,7 @@ export const ExpressPlugin = {
|
|
|
142
144
|
reply: res,
|
|
143
145
|
source: 'express',
|
|
144
146
|
mountPath: basePath,
|
|
147
|
+
publicBaseUrl,
|
|
145
148
|
requestData,
|
|
146
149
|
createContext,
|
|
147
150
|
urlPrefixOverride: req.urlPrefixOverride
|
|
@@ -240,6 +243,7 @@ export const ExpressPlugin = {
|
|
|
240
243
|
reply: res,
|
|
241
244
|
source: 'express',
|
|
242
245
|
mountPath: basePath,
|
|
246
|
+
publicBaseUrl,
|
|
243
247
|
requestData,
|
|
244
248
|
createContext,
|
|
245
249
|
urlPrefixOverride: req.urlPrefixOverride
|
|
@@ -262,6 +266,7 @@ export const ExpressPlugin = {
|
|
|
262
266
|
routeMeta,
|
|
263
267
|
helpers,
|
|
264
268
|
mountPath: basePath,
|
|
269
|
+
publicBaseUrl,
|
|
265
270
|
runHooks
|
|
266
271
|
})
|
|
267
272
|
res.set(outcome.headers)
|
|
@@ -70,12 +70,14 @@ export const FastifyPlugin = {
|
|
|
70
70
|
api.http.fastify = { app }
|
|
71
71
|
|
|
72
72
|
const mountPath = fastifyOptions.mountPath || ''
|
|
73
|
+
const publicBaseUrl = fastifyOptions.publicBaseUrl || ''
|
|
73
74
|
const strictContentType = fastifyOptions.strictContentType !== false
|
|
74
75
|
|
|
75
76
|
vars.transport = {
|
|
76
77
|
type: 'fastify',
|
|
77
78
|
matchAll: '*',
|
|
78
|
-
mountPath
|
|
79
|
+
mountPath,
|
|
80
|
+
publicBaseUrl
|
|
79
81
|
}
|
|
80
82
|
|
|
81
83
|
registerVendorJsonParser(app)
|
|
@@ -140,6 +142,7 @@ export const FastifyPlugin = {
|
|
|
140
142
|
reply,
|
|
141
143
|
source: 'fastify',
|
|
142
144
|
mountPath,
|
|
145
|
+
publicBaseUrl,
|
|
143
146
|
requestData,
|
|
144
147
|
createContext
|
|
145
148
|
})
|
|
@@ -174,6 +177,7 @@ export const FastifyPlugin = {
|
|
|
174
177
|
routeMeta,
|
|
175
178
|
helpers,
|
|
176
179
|
mountPath,
|
|
180
|
+
publicBaseUrl,
|
|
177
181
|
runHooks
|
|
178
182
|
})
|
|
179
183
|
|