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.
Files changed (82) hide show
  1. package/README.md +2 -1
  2. package/docs/API.md +12 -9
  3. package/docs/GUIDE/GUIDE_1_Initial_Setup.md +5 -2
  4. package/docs/GUIDE/GUIDE_2_1_The_Starting_Point.md +3 -3
  5. package/docs/GUIDE/GUIDE_3_Field_Transformations.md +2 -0
  6. package/docs/GUIDE/GUIDE_7_Hooks_Data_Management_And_Plugins.md +1 -1
  7. package/docs/GUIDE/GUIDE_X_Autofiltering.md +63 -0
  8. package/docs/GUIDE/GUIDE_X_File_Uploads.md +17 -12
  9. package/docs/GUIDE/GUIDE_X_Row_Policies.md +603 -0
  10. package/docs/GUIDE/GUIDE_X_SocketIO.md +15 -10
  11. package/docs/GUIDE/GUIDE_X_URL_Management.md +93 -254
  12. package/docs/GUIDE/GUIDE_X_Writing_Plugins.md +2 -0
  13. package/docs/GUIDE/index.md +4 -1
  14. package/fixing_plan.md +112 -0
  15. package/index.js +2 -0
  16. package/package.json +1 -1
  17. package/plugins/core/bulk-operations-plugin.js +5 -4
  18. package/plugins/core/connectors/express-plugin.js +6 -1
  19. package/plugins/core/connectors/fastify-plugin.js +5 -1
  20. package/plugins/core/connectors/lib/connector-core.js +17 -11
  21. package/plugins/core/connectors/lib/transport-route-schemas.js +12 -60
  22. package/plugins/core/file-handling-plugin.js +126 -96
  23. package/plugins/core/lib/anyapi/anyapi-registry.js +21 -3
  24. package/plugins/core/lib/anyapi/schema-utils.js +1 -0
  25. package/plugins/core/lib/anyapi/utils/descriptor-helpers.js +7 -1
  26. package/plugins/core/lib/querying/knex-json-api-transformers-querying.js +20 -15
  27. package/plugins/core/lib/querying/knex-pagination-helpers.js +38 -84
  28. package/plugins/core/lib/querying/knex-relationship-includes.js +29 -28
  29. package/plugins/core/lib/querying/knex-window-queries.js +10 -77
  30. package/plugins/core/lib/querying/url-helpers.js +9 -21
  31. package/plugins/core/lib/querying-writing/connectors-query-parser.js +51 -0
  32. package/plugins/core/lib/querying-writing/database-capabilities.js +1 -1
  33. package/plugins/core/lib/querying-writing/database-value-normalizers.js +18 -9
  34. package/plugins/core/lib/querying-writing/knex-field-helpers.js +6 -1
  35. package/plugins/core/lib/querying-writing/relationship-contracts.js +56 -0
  36. package/plugins/core/lib/querying-writing/request-contracts.js +53 -4
  37. package/plugins/core/lib/querying-writing/return-record-settings.js +47 -0
  38. package/plugins/core/lib/querying-writing/schema-helpers.js +34 -41
  39. package/plugins/core/lib/storage/canonical-storage-mapping.js +2 -1
  40. package/plugins/core/lib/storage/storage-adapter.js +74 -13
  41. package/plugins/core/lib/writing/many-to-many-manipulations.js +4 -3
  42. package/plugins/core/lib/writing/relationship-processor.js +10 -1
  43. package/plugins/core/rest-api-anyapi-knex-plugin.js +210 -97
  44. package/plugins/core/rest-api-knex-plugin.js +70 -60
  45. package/plugins/core/rest-api-plugin-hooks/turn-scope-init-into-vars.js +8 -2
  46. package/plugins/core/rest-api-plugin-methods/common.js +85 -63
  47. package/plugins/core/rest-api-plugin-methods/delete-relationship.js +23 -13
  48. package/plugins/core/rest-api-plugin-methods/delete.js +2 -6
  49. package/plugins/core/rest-api-plugin-methods/get-related.js +15 -13
  50. package/plugins/core/rest-api-plugin-methods/get-relationship.js +1 -1
  51. package/plugins/core/rest-api-plugin-methods/patch-relationship.js +17 -6
  52. package/plugins/core/rest-api-plugin-methods/patch.js +3 -6
  53. package/plugins/core/rest-api-plugin-methods/post-relationship.js +24 -14
  54. package/plugins/core/rest-api-plugin-methods/post.js +4 -7
  55. package/plugins/core/rest-api-plugin-methods/put.js +4 -7
  56. package/plugins/core/rest-api-plugin.js +6 -36
  57. package/plugins/core/rest-api-row-policy-plugin.js +157 -0
  58. package/plugins/storage/local-storage.js +44 -19
  59. package/plugins/storage/s3-storage.js +21 -55
  60. package/tests/README.md +1 -1
  61. package/tests/anyapi-cursor-pagination.test.js +7 -1
  62. package/tests/anyapi-custom-idproperty.test.js +163 -0
  63. package/tests/fastify-plugin.test.js +28 -0
  64. package/tests/field-setters.test.js +10 -10
  65. package/tests/file-handling.test.js +118 -0
  66. package/tests/fixtures/api-configs.js +362 -4
  67. package/tests/full-jsonapi-belongsto-linkage.test.js +150 -0
  68. package/tests/http-connectors-parity.test.js +165 -5
  69. package/tests/local-storage.test.js +96 -0
  70. package/tests/pagination-cursor-multifield.test.js +121 -0
  71. package/tests/pagination-enhanced.test.js +66 -15
  72. package/tests/post-commit-hooks.test.js +120 -0
  73. package/tests/public-exports.test.js +15 -0
  74. package/tests/put-create-parity.test.js +216 -0
  75. package/tests/relationship-endpoints.test.js +259 -0
  76. package/tests/return-record-settings.test.js +184 -0
  77. package/tests/row-policy.test.js +433 -0
  78. package/tests/s3-storage.test.js +38 -0
  79. package/tests/searchschema-merge.test.js +17 -0
  80. package/tests/socketio.test.js +62 -0
  81. package/tests/storage-mapping.test.js +1 -0
  82. package/tests/test-development-guide.md +5 -5
@@ -2,316 +2,155 @@
2
2
 
3
3
  ## Overview
4
4
 
5
- The JSON REST API automatically generates appropriate URLs for all resources in JSON:API responses. URLs are calculated **per-request** based on the incoming request headers, ensuring correct URLs in all deployment scenarios without configuration.
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
- ## How It Works
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
- ### Automatic URL Detection
9
+ ## Default Relative Links
10
10
 
11
- URLs in responses (links, hrefs) are automatically generated based on:
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
- ### Different Deployment Scenarios
19
+ Generated links use the mount path:
25
20
 
26
- #### Local Development
27
- ```
28
- Request: GET http://localhost:3000/api/books
29
- Response links: http://localhost:3000/api/books/1
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
- #### Production with Domain
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
- #### Behind Reverse Proxy (Nginx/Apache)
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
- #### Multi-Tenant SaaS
54
- Each tenant gets their own domain in responses automatically:
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
- Tenant B request: GET https://customer-b.api.com/api/books
60
- Response links: https://customer-b.api.com/api/books/1
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
- ## Advanced: Custom URL Override
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
- For complex deployments (API gateways, CDNs, etc.), you can override URL generation using hooks.
66
+ Trailing slashes on `publicBaseUrl` are normalized before links are built.
66
67
 
67
- ### Recommended Method: API Hooks (Best Practice)
68
+ ## Per-Request Overrides
68
69
 
69
- Define a hook when creating your API to handle URL overrides:
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
- const api = new Api({
73
- name: 'my-api',
74
-
75
- // Define hooks at API creation time
73
+ await api.customize({
76
74
  hooks: {
77
- 'transport:request': [
78
- async (payload) => {
79
- const { context, req } = payload;
80
-
81
- // Example: Override based on custom header
82
- if (req?.headers?.['x-public-url']) {
83
- context.urlPrefixOverride = req.headers['x-public-url'];
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
- ### Alternative Method: Express Middleware
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
- // Set urlPrefixOverride on the request
121
- if (req.headers['x-public-url']) {
122
- req.urlPrefixOverride = req.headers['x-public-url'];
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
- // Then mount your API
128
- app.use(api.http.express.router);
109
+ api.http.express.mount(app);
129
110
  ```
130
111
 
131
- ### Common Use Cases for Override
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
- ### Relationship URLs
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
- ### Pagination URLs
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
- ## Benefits of Per-Request URL Generation
118
+ 1. `context.urlPrefixOverride`
119
+ 2. Connector `publicBaseUrl`
120
+ 3. Connector `mountPath`
121
+ 4. Empty string
242
122
 
243
- 1. **Zero Configuration**: Works correctly out of the box
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
- ## Troubleshooting
125
+ ## Reverse Proxies
250
126
 
251
- ### URLs are showing localhost in production
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
- ### Need different URLs for different environments
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
- ### API Gateway stripping paths
260
- - Use middleware to set `req.urlPrefixOverride` with the full public path
261
- - Include any path prefixes that the gateway strips
131
+ ```javascript
132
+ await api.use(ExpressPlugin, {
133
+ mountPath: '/api',
134
+ publicBaseUrl: process.env.PUBLIC_API_URL
135
+ });
136
+ ```
262
137
 
263
- ## Complete Working Example
138
+ Set `PUBLIC_API_URL` to the externally visible API prefix, for example `https://api.example.com/api`.
264
139
 
265
- Here's a full example showing URL override with hooks:
140
+ ## Troubleshooting
266
141
 
267
- ```javascript
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
- // Create API with URL override hook
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
- // Install plugins
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
- // Define resources...
297
- await api.addResource('books', { /* schema */ });
148
+ Include the public path prefix in `publicBaseUrl`.
298
149
 
299
- // Create Express app
300
- const app = express();
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
- Now your API automatically handles:
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
- Just set your `mountPath` in the Express plugin and optionally define hooks for custom scenarios!
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.
@@ -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 multiple backends (file system, S3).
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
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "json-rest-api",
3
- "version": "1.0.24",
3
+ "version": "1.0.25",
4
4
  "main": "index.js",
5
5
  "type": "module",
6
6
  "scripts": {
@@ -193,11 +193,12 @@ export const BulkOperationsPlugin = {
193
193
  transaction
194
194
  }, recordContext)
195
195
 
196
- // If patch doesn't return full record, fetch it
197
- let resultData = result.data
198
- if (!resultData && result.id) {
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: result.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