json-rest-api 1.0.10 → 1.0.13
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/.claude/settings.local.json +9 -1
- package/AGENTS.md +34 -0
- package/CLAUDE.md +26 -1
- package/CRUSH.md +55 -0
- package/README.md +3 -3
- package/TODO.todo +8 -0
- package/docs/API.md +17 -18
- package/docs/COMPARISON.md +18 -13
- package/docs/GUIDE/GUIDE_1_Initial_Setup.md +17 -21
- package/docs/GUIDE/GUIDE_2_1_The_Starting_Point.md +33 -2
- package/docs/GUIDE/GUIDE_2_2_Manipulating_And_Searching_Tables.md +61 -7
- package/docs/GUIDE/GUIDE_2_3_BelongsTo_Relationships.md +58 -61
- package/docs/GUIDE/GUIDE_2_4_HasMany_Records.md +37 -37
- package/docs/GUIDE/GUIDE_2_5_HasMany_Polymorphic.md +103 -87
- package/docs/GUIDE/GUIDE_2_6_Many_To_Many.md +37 -34
- package/docs/GUIDE/GUIDE_2_7_Pagination_And_Ordering.md +45 -42
- package/docs/GUIDE/GUIDE_2_8_Effects_of_PUT_and_PATCH.md +3 -3
- package/docs/GUIDE/GUIDE_2_Data_And_Relations.md +1 -1
- package/docs/GUIDE/GUIDE_3_Field_Transformations.md +5 -5
- package/docs/GUIDE/GUIDE_4_Authentication_And_Permissions.md +498 -755
- package/docs/GUIDE/GUIDE_6_Positioning.md +1 -3
- package/docs/GUIDE/GUIDE_7_Hooks_Data_Management_And_Plugins.md +4 -5
- package/docs/GUIDE/GUIDE_X_Bulk_Operations.md +15 -6
- package/docs/GUIDE/GUIDE_X_Cors.md +2 -5
- package/docs/GUIDE/GUIDE_X_File_Uploads.md +2 -3
- package/docs/GUIDE/GUIDE_X_Multihome.md +1 -4
- package/docs/GUIDE/GUIDE_X_SocketIO.md +0 -2
- package/docs/GUIDE/GUIDE_X_URL_Management.md +317 -0
- package/docs/GUIDE/index.md +3 -0
- package/docs/QUICKSTART.md +12 -12
- package/docs/index.md +2 -2
- package/docs/mini-guides/file-uploads-complete.md +0 -1
- package/examples/url-override-example.js +137 -0
- package/index.js +7 -1
- package/package.json +7 -11
- package/plugins/core/bulk-operations-plugin.js +2 -0
- package/plugins/core/connectors/express-plugin.js +43 -24
- package/plugins/core/connectors/lib/request-helpers.js +2 -2
- package/plugins/core/lib/anyapi/anyapi-registry.js +821 -0
- package/plugins/core/lib/anyapi/query/anyapi-query-adapter.js +647 -0
- package/plugins/core/lib/anyapi/schema-utils.js +186 -0
- package/plugins/core/lib/anyapi/utils/descriptor-helpers.js +113 -0
- package/plugins/core/lib/dbTablesOperations.js +305 -0
- package/plugins/core/lib/querying/knex-cross-table-search.js +64 -108
- package/plugins/core/lib/querying/knex-json-api-transformers-querying.js +21 -27
- package/plugins/core/lib/querying/knex-pagination-helpers.js +36 -20
- package/plugins/core/lib/querying/knex-query-helpers-base.js +49 -14
- package/plugins/core/lib/querying/knex-query-helpers.js +378 -222
- package/plugins/core/lib/querying/knex-relationship-includes.js +262 -87
- package/plugins/core/lib/querying/url-helpers.js +119 -0
- package/plugins/core/lib/querying-writing/compile-schemas.js +36 -28
- package/plugins/core/lib/querying-writing/database-capabilities.js +10 -9
- package/plugins/core/lib/querying-writing/knex-field-helpers.js +9 -3
- package/plugins/core/lib/querying-writing/knex-json-api-transformers.js +2 -2
- package/plugins/core/lib/querying-writing/payload-validators.js +10 -16
- package/plugins/core/lib/querying-writing/schema-helpers.js +52 -60
- package/plugins/core/lib/querying-writing/scope-validations.js +78 -22
- package/plugins/core/lib/querying-writing/simplified-helpers.js +208 -61
- package/plugins/core/lib/storage/storage-adapter.js +160 -0
- package/plugins/core/lib/writing/relationship-processor.js +4 -11
- package/plugins/core/rest-api-access.js +456 -0
- package/plugins/core/rest-api-anyapi-knex-plugin.js +2285 -0
- package/plugins/core/rest-api-knex-plugin.js +226 -87
- package/plugins/core/rest-api-multihome-plugin.js +89 -27
- package/plugins/core/rest-api-plugin-hooks/turn-scope-init-into-vars.js +0 -1
- package/plugins/core/rest-api-plugin-methods/check-permissions.js +1 -5
- package/plugins/core/rest-api-plugin-methods/common.js +46 -10
- package/plugins/core/rest-api-plugin-methods/delete-relationship.js +31 -27
- package/plugins/core/rest-api-plugin-methods/delete.js +1 -4
- package/plugins/core/rest-api-plugin-methods/enrich-attributes.js +13 -1
- package/plugins/core/rest-api-plugin-methods/get-related.js +114 -56
- package/plugins/core/rest-api-plugin-methods/get-relationship.js +4 -3
- package/plugins/core/rest-api-plugin-methods/get.js +2 -5
- package/plugins/core/rest-api-plugin-methods/patch.js +15 -16
- package/plugins/core/rest-api-plugin-methods/post-relationship.js +21 -14
- package/plugins/core/rest-api-plugin-methods/post.js +23 -11
- package/plugins/core/rest-api-plugin-methods/put.js +18 -21
- package/plugins/core/rest-api-plugin-methods/query.js +3 -4
- package/plugins/core/rest-api-plugin-methods/release.js +13 -0
- package/plugins/core/rest-api-plugin.js +3 -4
- package/plugins/core/rest-api-positioning-plugin.js +253 -76
- package/plugins/core/socketio-plugin.js +436 -502
- package/quickTest.js +2 -2
- package/scripts/debug-anyapi-review-filter.mjs +63 -0
- package/scripts/inspect-anyapi-descriptor-country.mjs +19 -0
- package/scripts/inspect-anyapi-descriptor.mjs +19 -0
- package/test-debug.js +59 -0
- package/tests/README.md +7 -1
- package/tests/anyapi-basic.test.js +102 -0
- package/tests/anyapi-cursor-pagination.test.js +144 -0
- package/tests/computed-fields.test.js +27 -12
- package/tests/cors.test.js +5 -6
- package/tests/curl.test.js +1 -1
- package/tests/custom-idproperty-relationships.test.js +37 -29
- package/tests/field-getters.test.js +25 -7
- package/tests/fixtures/api-configs.js +576 -150
- package/tests/helpers/storage-mode.js +65 -0
- package/tests/helpers/test-utils.js +85 -3
- package/tests/include-depth-validation.test.js +35 -1
- package/tests/nested-includes.test.js +130 -103
- package/tests/pagination-cursor-multifield.test.js +17 -13
- package/tests/pagination-enhanced.test.js +19 -24
- package/tests/polymorphic-simple.test.js +9 -9
- package/tests/positioning-advanced.test.js +19 -19
- package/tests/positioning-extended.test.js +25 -25
- package/tests/positioning-multifilter.test.js +62 -62
- package/tests/positioning.test.js +7 -7
- package/tests/queries.test.js +5 -5
- package/tests/query-limits.test.js +77 -92
- package/tests/rest-api-access.test.js +206 -0
- package/tests/searchschema-merge.test.js +182 -0
- package/tests/socketio.test.js +33 -14
- package/tests/virtual-fields.test.js +0 -1
- package/docs/DOCS_ALL.md +0 -21894
- package/plugins/core/jwt-auth-plugin.js +0 -897
- package/plugins/core/lib/jwt-auth-helpers.js +0 -130
- package/tests/jwt-auth.test.js +0 -692
|
@@ -60,8 +60,7 @@ import { RestApiKnexPlugin } from './plugins/core/rest-api-knex-plugin.js';
|
|
|
60
60
|
import { PositioningPlugin } from './plugins/core/rest-api-positioning-plugin.js';
|
|
61
61
|
|
|
62
62
|
const api = new Api({
|
|
63
|
-
name: 'my-api'
|
|
64
|
-
version: '1.0.0'
|
|
63
|
+
name: 'my-api'
|
|
65
64
|
});
|
|
66
65
|
|
|
67
66
|
// Core plugins
|
|
@@ -657,7 +656,6 @@ Enable debug logging to see position calculations:
|
|
|
657
656
|
```javascript
|
|
658
657
|
const api = new Api({
|
|
659
658
|
name: 'my-api',
|
|
660
|
-
version: '1.0.0',
|
|
661
659
|
logging: { level: 'debug' }
|
|
662
660
|
});
|
|
663
661
|
```
|
|
@@ -109,7 +109,6 @@ api.customize({
|
|
|
109
109
|
apiMethods: {
|
|
110
110
|
healthCheck: async ({ vars, scopes }) => ({
|
|
111
111
|
status: 'ok',
|
|
112
|
-
version: vars.apiVersion || '1.0.0',
|
|
113
112
|
environment: vars.environment,
|
|
114
113
|
timestamp: new Date().toISOString(),
|
|
115
114
|
resources: Object.keys(scopes)
|
|
@@ -611,6 +610,7 @@ Used for creating new resources.
|
|
|
611
610
|
- Can set defaults or compute values
|
|
612
611
|
|
|
613
612
|
**Note**: After validation, attributes are stored in `context.inputRecord.data.attributes`, not directly in `context.attributes`.
|
|
613
|
+
**Note**: `context.minimalRecord` is always a JSON:API resource object; for POST requests it is a snapshot of the incoming payload (`{ type, id?, attributes, relationships }`).
|
|
614
614
|
|
|
615
615
|
**Example**:
|
|
616
616
|
```javascript
|
|
@@ -781,7 +781,7 @@ hooks: {
|
|
|
781
781
|
if (context.method === 'put' && context.inputRecord) {
|
|
782
782
|
// Prevent changing the author (check if belongsTo relationship changed)
|
|
783
783
|
const newAuthorId = context.belongsToUpdates?.author_id;
|
|
784
|
-
const currentAuthorId = context.minimalRecord?.
|
|
784
|
+
const currentAuthorId = context.minimalRecord?.relationships?.author?.data?.id;
|
|
785
785
|
if (newAuthorId && newAuthorId !== currentAuthorId) {
|
|
786
786
|
throw new Error('Cannot change post author');
|
|
787
787
|
}
|
|
@@ -899,7 +899,7 @@ Used for partially updating a resource.
|
|
|
899
899
|
- `belongsToUpdates` - Modify foreign key values (if any)
|
|
900
900
|
- Can add computed values or prevent updates
|
|
901
901
|
|
|
902
|
-
**Note**: For PATCH, `context.inputRecord.data.attributes` contains only the fields being updated. Use `context.minimalRecord.
|
|
902
|
+
**Note**: For PATCH, `context.inputRecord.data.attributes` contains only the fields being updated. Use `context.minimalRecord.attributes` to access the complete current record.
|
|
903
903
|
|
|
904
904
|
**Example**:
|
|
905
905
|
```javascript
|
|
@@ -908,7 +908,7 @@ hooks: {
|
|
|
908
908
|
if (context.method === 'patch' && context.inputRecord) {
|
|
909
909
|
// If status is being changed to published, set publish date
|
|
910
910
|
if (context.inputRecord.data.attributes.status === 'published' &&
|
|
911
|
-
|
|
911
|
+
context.minimalRecord?.attributes?.status !== 'published') {
|
|
912
912
|
context.inputRecord.data.attributes.published_at = new Date().toISOString();
|
|
913
913
|
}
|
|
914
914
|
|
|
@@ -1487,4 +1487,3 @@ hooks: {
|
|
|
1487
1487
|
---
|
|
1488
1488
|
|
|
1489
1489
|
[Back to Guide](./README.md)
|
|
1490
|
-
|
|
@@ -39,7 +39,6 @@ const knex = knexLib({
|
|
|
39
39
|
// Create API instance
|
|
40
40
|
const api = new Api({
|
|
41
41
|
name: 'book-catalog-api',
|
|
42
|
-
version: '1.0.0'
|
|
43
42
|
});
|
|
44
43
|
|
|
45
44
|
// Install plugins in order
|
|
@@ -81,8 +80,8 @@ await api.addResource('countries', {
|
|
|
81
80
|
code: { type: 'string', max: 2, unique: true }
|
|
82
81
|
},
|
|
83
82
|
relationships: {
|
|
84
|
-
publishers: { hasMany: 'publishers', foreignKey: 'country_id' },
|
|
85
|
-
books: { hasMany: 'books', foreignKey: 'country_id' }
|
|
83
|
+
publishers: { type: 'hasMany', target: 'publishers', foreignKey: 'country_id' },
|
|
84
|
+
books: { type: 'hasMany', target: 'books', foreignKey: 'country_id' }
|
|
86
85
|
}
|
|
87
86
|
});
|
|
88
87
|
|
|
@@ -93,7 +92,7 @@ await api.addResource('publishers', {
|
|
|
93
92
|
country_id: { type: 'number', nullable: true, belongsTo: 'countries', as: 'country' }
|
|
94
93
|
},
|
|
95
94
|
relationships: {
|
|
96
|
-
books: { hasMany: 'books', foreignKey: 'publisher_id' }
|
|
95
|
+
books: { type: 'hasMany', target: 'books', foreignKey: 'publisher_id' }
|
|
97
96
|
}
|
|
98
97
|
});
|
|
99
98
|
|
|
@@ -103,7 +102,12 @@ await api.addResource('authors', {
|
|
|
103
102
|
name: { type: 'string', required: true, max: 200 }
|
|
104
103
|
},
|
|
105
104
|
relationships: {
|
|
106
|
-
books: {
|
|
105
|
+
books: {
|
|
106
|
+
type: 'manyToMany',
|
|
107
|
+
through: 'book_authors',
|
|
108
|
+
foreignKey: 'author_id',
|
|
109
|
+
otherKey: 'book_id'
|
|
110
|
+
}
|
|
107
111
|
}
|
|
108
112
|
});
|
|
109
113
|
|
|
@@ -115,7 +119,12 @@ await api.addResource('books', {
|
|
|
115
119
|
publisher_id: { type: 'number', nullable: true, belongsTo: 'publishers', as: 'publisher' }
|
|
116
120
|
},
|
|
117
121
|
relationships: {
|
|
118
|
-
authors: {
|
|
122
|
+
authors: {
|
|
123
|
+
type: 'manyToMany',
|
|
124
|
+
through: 'book_authors',
|
|
125
|
+
foreignKey: 'book_id',
|
|
126
|
+
otherKey: 'author_id'
|
|
127
|
+
}
|
|
119
128
|
}
|
|
120
129
|
});
|
|
121
130
|
|
|
@@ -115,7 +115,7 @@ import express from 'express';
|
|
|
115
115
|
import knex from 'knex';
|
|
116
116
|
|
|
117
117
|
// Create API instance
|
|
118
|
-
const api = new Api({ name: 'my-api'
|
|
118
|
+
const api = new Api({ name: 'my-api' });
|
|
119
119
|
|
|
120
120
|
// Set up database
|
|
121
121
|
const db = knex({
|
|
@@ -502,13 +502,11 @@ await api.use(CorsPlugin, {
|
|
|
502
502
|
allowedHeaders: [
|
|
503
503
|
'Content-Type',
|
|
504
504
|
'Authorization',
|
|
505
|
-
'X-API-Version', // Client specifies API version
|
|
506
505
|
'X-Request-ID', // For request tracking
|
|
507
506
|
'X-Client-ID', // Identify different client apps
|
|
508
507
|
'X-Feature-Flags' // Client-specific features
|
|
509
508
|
],
|
|
510
509
|
exposedHeaders: [
|
|
511
|
-
'X-API-Version', // Confirm which version was used
|
|
512
510
|
'X-Deprecated', // Warn about deprecated endpoints
|
|
513
511
|
'X-Request-ID', // For debugging
|
|
514
512
|
'X-Cache-Status', // Was this cached?
|
|
@@ -1155,7 +1153,7 @@ await apiV2.use(ExpressPlugin, {
|
|
|
1155
1153
|
await apiV2.use(CorsPlugin, {
|
|
1156
1154
|
'rest-api-cors': {
|
|
1157
1155
|
origin: 'https://app.example.com', // v2 doesn't support legacy
|
|
1158
|
-
allowedHeaders: ['Content-Type', 'Authorization'
|
|
1156
|
+
allowedHeaders: ['Content-Type', 'Authorization']
|
|
1159
1157
|
}
|
|
1160
1158
|
});
|
|
1161
1159
|
|
|
@@ -1468,7 +1466,6 @@ headers['Access-Control-Expose-Headers'] = [
|
|
|
1468
1466
|
'X-RateLimit-Reset',
|
|
1469
1467
|
|
|
1470
1468
|
// API Info
|
|
1471
|
-
'X-API-Version',
|
|
1472
1469
|
'X-Response-Time',
|
|
1473
1470
|
'X-Request-ID'
|
|
1474
1471
|
].join(', ');
|
|
@@ -37,7 +37,7 @@ import { RestApiPlugin, FileHandlingPlugin, ExpressPlugin } from 'json-rest-api'
|
|
|
37
37
|
import { LocalStorage } from 'json-rest-api/plugins/storage/local-storage.js';
|
|
38
38
|
|
|
39
39
|
// Create API
|
|
40
|
-
const api = new Api({ name: 'my-api'
|
|
40
|
+
const api = new Api({ name: 'my-api'});
|
|
41
41
|
|
|
42
42
|
// Create storage
|
|
43
43
|
const storage = new LocalStorage({
|
|
@@ -416,7 +416,7 @@ import { LocalStorage } from 'json-rest-api/plugins/storage/local-storage.js';
|
|
|
416
416
|
import express from 'express';
|
|
417
417
|
|
|
418
418
|
// Setup
|
|
419
|
-
const api = new Api({ name: 'image-api'
|
|
419
|
+
const api = new Api({ name: 'image-api' });
|
|
420
420
|
const storage = new LocalStorage({
|
|
421
421
|
directory: './uploads/images',
|
|
422
422
|
baseUrl: 'http://localhost:3000/uploads/images'
|
|
@@ -641,7 +641,6 @@ Enable debug logging to see what's happening:
|
|
|
641
641
|
```javascript
|
|
642
642
|
const api = new Api({
|
|
643
643
|
name: 'my-api',
|
|
644
|
-
version: '1.0.0',
|
|
645
644
|
logLevel: 'debug' // or 'trace' for more detail
|
|
646
645
|
});
|
|
647
646
|
```
|
|
@@ -80,8 +80,7 @@ import { MultiHomePlugin } from './plugins/core/multihome-plugin.js';
|
|
|
80
80
|
|
|
81
81
|
```javascript
|
|
82
82
|
const api = new Api({
|
|
83
|
-
name: 'my-multi-tenant-api',
|
|
84
|
-
version: '1.0.0'
|
|
83
|
+
name: 'my-multi-tenant-api',
|
|
85
84
|
});
|
|
86
85
|
|
|
87
86
|
// Install required plugins first
|
|
@@ -471,7 +470,6 @@ Enable detailed logging to troubleshoot:
|
|
|
471
470
|
```javascript
|
|
472
471
|
const api = new Api({
|
|
473
472
|
name: 'my-api',
|
|
474
|
-
version: '1.0.0',
|
|
475
473
|
logging: { level: 'trace' }
|
|
476
474
|
});
|
|
477
475
|
```
|
|
@@ -589,7 +587,6 @@ const db = knex({
|
|
|
589
587
|
// Create API
|
|
590
588
|
const api = new Api({
|
|
591
589
|
name: 'saas-api',
|
|
592
|
-
version: '1.0.0',
|
|
593
590
|
logging: { level: 'info' }
|
|
594
591
|
});
|
|
595
592
|
|
|
@@ -47,7 +47,6 @@ import { JWTAuthPlugin } from 'json-rest-api/plugins/jwt-auth';
|
|
|
47
47
|
// Create your API instance
|
|
48
48
|
const api = new Api({
|
|
49
49
|
name: 'my-api',
|
|
50
|
-
version: '1.0.0'
|
|
51
50
|
});
|
|
52
51
|
|
|
53
52
|
// Add required plugins
|
|
@@ -983,7 +982,6 @@ Enable debug logging to troubleshoot:
|
|
|
983
982
|
// Server-side
|
|
984
983
|
const api = new Api({
|
|
985
984
|
name: 'my-api',
|
|
986
|
-
version: '1.0.0',
|
|
987
985
|
logging: { level: 'debug' }
|
|
988
986
|
});
|
|
989
987
|
|
|
@@ -0,0 +1,317 @@
|
|
|
1
|
+
# URL Management in JSON REST API
|
|
2
|
+
|
|
3
|
+
## Overview
|
|
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.
|
|
6
|
+
|
|
7
|
+
## How It Works
|
|
8
|
+
|
|
9
|
+
### Automatic URL Detection
|
|
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`)
|
|
16
|
+
|
|
17
|
+
```javascript
|
|
18
|
+
// Example: Simple setup - no URL configuration needed!
|
|
19
|
+
await api.use(RestApiPlugin);
|
|
20
|
+
await api.use(RestApiKnexPlugin, { knex });
|
|
21
|
+
await api.use(ExpressPlugin, { mountPath: '/api' });
|
|
22
|
+
```
|
|
23
|
+
|
|
24
|
+
### Different Deployment Scenarios
|
|
25
|
+
|
|
26
|
+
#### Local Development
|
|
27
|
+
```
|
|
28
|
+
Request: GET http://localhost:3000/api/books
|
|
29
|
+
Response links: http://localhost:3000/api/books/1
|
|
30
|
+
```
|
|
31
|
+
|
|
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
|
+
```
|
|
37
|
+
|
|
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
|
+
```
|
|
45
|
+
|
|
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
|
+
```
|
|
52
|
+
|
|
53
|
+
#### Multi-Tenant SaaS
|
|
54
|
+
Each tenant gets their own domain in responses automatically:
|
|
55
|
+
```
|
|
56
|
+
Tenant A request: GET https://customer-a.api.com/api/books
|
|
57
|
+
Response links: https://customer-a.api.com/api/books/1
|
|
58
|
+
|
|
59
|
+
Tenant B request: GET https://customer-b.api.com/api/books
|
|
60
|
+
Response links: https://customer-b.api.com/api/books/1
|
|
61
|
+
```
|
|
62
|
+
|
|
63
|
+
## Advanced: Custom URL Override
|
|
64
|
+
|
|
65
|
+
For complex deployments (API gateways, CDNs, etc.), you can override URL generation using hooks.
|
|
66
|
+
|
|
67
|
+
### Recommended Method: API Hooks (Best Practice)
|
|
68
|
+
|
|
69
|
+
Define a hook when creating your API to handle URL overrides:
|
|
70
|
+
|
|
71
|
+
```javascript
|
|
72
|
+
const api = new Api({
|
|
73
|
+
name: 'my-api',
|
|
74
|
+
|
|
75
|
+
// Define hooks at API creation time
|
|
76
|
+
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'];
|
|
84
|
+
}
|
|
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
|
+
}
|
|
104
|
+
]
|
|
105
|
+
}
|
|
106
|
+
});
|
|
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
|
+
```
|
|
113
|
+
|
|
114
|
+
### Alternative Method: Express Middleware
|
|
115
|
+
|
|
116
|
+
If you cannot use hooks, you can use Express middleware before mounting the API:
|
|
117
|
+
|
|
118
|
+
```javascript
|
|
119
|
+
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'];
|
|
123
|
+
}
|
|
124
|
+
next();
|
|
125
|
+
});
|
|
126
|
+
|
|
127
|
+
// Then mount your API
|
|
128
|
+
app.use(api.http.express.router);
|
|
129
|
+
```
|
|
130
|
+
|
|
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
|
+
```
|
|
213
|
+
|
|
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
|
+
```
|
|
227
|
+
|
|
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
|
+
```
|
|
240
|
+
|
|
241
|
+
## Benefits of Per-Request URL Generation
|
|
242
|
+
|
|
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
|
|
248
|
+
|
|
249
|
+
## Troubleshooting
|
|
250
|
+
|
|
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
|
|
254
|
+
|
|
255
|
+
### Need different URLs for different environments
|
|
256
|
+
- Use the `urlPrefixOverride` pattern with environment variables
|
|
257
|
+
- Set `PUBLIC_API_URL` environment variable appropriately
|
|
258
|
+
|
|
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
|
|
262
|
+
|
|
263
|
+
## Complete Working Example
|
|
264
|
+
|
|
265
|
+
Here's a full example showing URL override with hooks:
|
|
266
|
+
|
|
267
|
+
```javascript
|
|
268
|
+
import { Api } from 'hooked-api';
|
|
269
|
+
import { RestApiPlugin, RestApiKnexPlugin, ExpressPlugin } from 'json-rest-api';
|
|
270
|
+
import express from 'express';
|
|
271
|
+
|
|
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
|
+
});
|
|
290
|
+
|
|
291
|
+
// Install plugins
|
|
292
|
+
await api.use(RestApiPlugin);
|
|
293
|
+
await api.use(RestApiKnexPlugin, { knex });
|
|
294
|
+
await api.use(ExpressPlugin, { mountPath: '/api' });
|
|
295
|
+
|
|
296
|
+
// Define resources...
|
|
297
|
+
await api.addResource('books', { /* schema */ });
|
|
298
|
+
|
|
299
|
+
// Create Express app
|
|
300
|
+
const app = express();
|
|
301
|
+
app.use(api.http.express.router);
|
|
302
|
+
app.listen(3000);
|
|
303
|
+
```
|
|
304
|
+
|
|
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
|
|
316
|
+
|
|
317
|
+
Just set your `mountPath` in the Express plugin and optionally define hooks for custom scenarios!
|
package/docs/GUIDE/index.md
CHANGED
|
@@ -45,6 +45,9 @@ Welcome to the comprehensive guide for JSON REST API. This guide will walk you t
|
|
|
45
45
|
- **[Socket.IO Integration](GUIDE_X_SocketIO.md)**
|
|
46
46
|
Add real-time capabilities to your API with WebSocket support.
|
|
47
47
|
|
|
48
|
+
- **[URL Management](GUIDE_X_URL_Management.md)**
|
|
49
|
+
Configure URL generation for proxies, CDNs, and multi-tenant scenarios.
|
|
50
|
+
|
|
48
51
|
- **[Appendices](GUIDE_Y_Appendices.md)**
|
|
49
52
|
Reference material, troubleshooting, and additional resources.
|
|
50
53
|
|
package/docs/QUICKSTART.md
CHANGED
|
@@ -44,10 +44,10 @@ const knex = knexLib({
|
|
|
44
44
|
});
|
|
45
45
|
|
|
46
46
|
// Create API instance
|
|
47
|
-
const api = new Api({ name: 'book-catalog-api'
|
|
47
|
+
const api = new Api({ name: 'book-catalog-api'});
|
|
48
48
|
|
|
49
49
|
// Install plugins
|
|
50
|
-
await api.use(RestApiPlugin
|
|
50
|
+
await api.use(RestApiPlugin); // URLs auto-detected from request headers
|
|
51
51
|
await api.use(RestApiKnexPlugin, { knex });
|
|
52
52
|
await api.use(ExpressPlugin, { mountPath: '/api' }); // Added: Express Plugin
|
|
53
53
|
|
|
@@ -78,7 +78,7 @@ await api.addResource('publishers', {
|
|
|
78
78
|
},
|
|
79
79
|
relationships: {
|
|
80
80
|
// A publisher has many authors
|
|
81
|
-
authors: { hasMany: 'authors', foreignKey: 'publisher_id' },
|
|
81
|
+
authors: { type: 'hasMany', target: 'authors', foreignKey: 'publisher_id' },
|
|
82
82
|
},
|
|
83
83
|
searchSchema: { // Adding search schema for publishers
|
|
84
84
|
name: { type: 'string', filterOperator: 'like' }
|
|
@@ -107,7 +107,7 @@ await api.resources.authors.createKnexTable();
|
|
|
107
107
|
|
|
108
108
|
```bash
|
|
109
109
|
$ node index.js
|
|
110
|
-
2025-08-01T00:25:50.730Z [INFO] [book-catalog-api] Installing plugin 'rest-api'
|
|
110
|
+
2025-08-01T00:25:50.730Z [INFO] [book-catalog-api] Installing plugin 'rest-api'
|
|
111
111
|
2025-08-01T00:25:50.736Z [INFO] [book-catalog-api] Plugin 'rest-api' installed successfully { duration: '2ms' }
|
|
112
112
|
2025-08-01T00:25:50.736Z [INFO] [book-catalog-api] Installing plugin 'rest-api-knex' { options: '[Object with methods]' }
|
|
113
113
|
2025-08-01T00:25:50.745Z [INFO] [book-catalog-api:plugin:rest-api-knex] Database capabilities detected: { database: 'SQLite', version: '3.50.2', windowFunctions: true }
|
|
@@ -162,7 +162,7 @@ console.log('JSON:API response:', inspect(oxfordResult));
|
|
|
162
162
|
const authorResult = await api.resources.authors.post({
|
|
163
163
|
name: 'George',
|
|
164
164
|
surname: 'Orwell',
|
|
165
|
-
|
|
165
|
+
publisher: penguinResult.id
|
|
166
166
|
});
|
|
167
167
|
console.log('Created author:', inspect(authorResult));
|
|
168
168
|
|
|
@@ -211,25 +211,25 @@ JSON:API response: {
|
|
|
211
211
|
},
|
|
212
212
|
links: { self: '/api/publishers/3' }
|
|
213
213
|
}
|
|
214
|
-
Created author: { id: '1', name: 'George', surname: 'Orwell',
|
|
214
|
+
Created author: { id: '1', name: 'George', surname: 'Orwell', publisher: { id: '1' } }
|
|
215
215
|
All publishers: {
|
|
216
216
|
data: [
|
|
217
|
-
{ id: '1', name: 'Penguin Random House',
|
|
218
|
-
{ id: '2', name: 'HarperCollins',
|
|
219
|
-
{ id: '3', name: 'Oxford University Press',
|
|
217
|
+
{ id: '1', name: 'Penguin Random House', authors: [ { id: '1' } ] },
|
|
218
|
+
{ id: '2', name: 'HarperCollins', authors: [] },
|
|
219
|
+
{ id: '3', name: 'Oxford University Press', authors: [] }
|
|
220
220
|
],
|
|
221
221
|
links: { self: '/api/publishers' }
|
|
222
222
|
}
|
|
223
|
-
Publisher with authors: { id: '1', name: 'Penguin Random House',
|
|
223
|
+
Publisher with authors: { id: '1', name: 'Penguin Random House', authors: [ { id: '1' } ] }
|
|
224
224
|
Search results: {
|
|
225
|
-
data: [ { id: '1', name: 'George', surname: 'Orwell',
|
|
225
|
+
data: [ { id: '1', name: 'George', surname: 'Orwell', publisher: { id: '1' } } ],
|
|
226
226
|
links: { self: '/api/authors' }
|
|
227
227
|
}
|
|
228
228
|
Updated author: {
|
|
229
229
|
id: '1',
|
|
230
230
|
name: 'George',
|
|
231
231
|
surname: 'Orwell (Eric Blair)',
|
|
232
|
-
|
|
232
|
+
publisher: { id: '1' }
|
|
233
233
|
}
|
|
234
234
|
```
|
|
235
235
|
|
package/docs/index.md
CHANGED
|
@@ -84,10 +84,10 @@ const knex = knexLib({
|
|
|
84
84
|
});
|
|
85
85
|
|
|
86
86
|
// Create API instance
|
|
87
|
-
const api = new Api({ name: 'book-catalog-api'
|
|
87
|
+
const api = new Api({ name: 'book-catalog-api' });
|
|
88
88
|
|
|
89
89
|
// Install plugins
|
|
90
|
-
await api.use(RestApiPlugin
|
|
90
|
+
await api.use(RestApiPlugin); // Basic REST plugin - URLs auto-detected
|
|
91
91
|
await api.use(RestApiKnexPlugin, { knex }); // Knex connector
|
|
92
92
|
await api.use(ExpressPlugin, { mountPath: '/api' }); // Express plugin
|
|
93
93
|
|