json-rest-api 1.0.10 → 1.0.11

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 (117) hide show
  1. package/.claude/settings.local.json +9 -1
  2. package/AGENTS.md +34 -0
  3. package/CLAUDE.md +26 -1
  4. package/CRUSH.md +55 -0
  5. package/README.md +3 -3
  6. package/TODO.todo +8 -0
  7. package/docs/API.md +17 -18
  8. package/docs/COMPARISON.md +18 -13
  9. package/docs/GUIDE/GUIDE_1_Initial_Setup.md +17 -21
  10. package/docs/GUIDE/GUIDE_2_1_The_Starting_Point.md +33 -2
  11. package/docs/GUIDE/GUIDE_2_2_Manipulating_And_Searching_Tables.md +61 -7
  12. package/docs/GUIDE/GUIDE_2_3_BelongsTo_Relationships.md +58 -61
  13. package/docs/GUIDE/GUIDE_2_4_HasMany_Records.md +37 -37
  14. package/docs/GUIDE/GUIDE_2_5_HasMany_Polymorphic.md +103 -87
  15. package/docs/GUIDE/GUIDE_2_6_Many_To_Many.md +37 -34
  16. package/docs/GUIDE/GUIDE_2_7_Pagination_And_Ordering.md +45 -42
  17. package/docs/GUIDE/GUIDE_2_8_Effects_of_PUT_and_PATCH.md +3 -3
  18. package/docs/GUIDE/GUIDE_2_Data_And_Relations.md +1 -1
  19. package/docs/GUIDE/GUIDE_3_Field_Transformations.md +5 -5
  20. package/docs/GUIDE/GUIDE_4_Authentication_And_Permissions.md +498 -755
  21. package/docs/GUIDE/GUIDE_6_Positioning.md +1 -3
  22. package/docs/GUIDE/GUIDE_7_Hooks_Data_Management_And_Plugins.md +4 -5
  23. package/docs/GUIDE/GUIDE_X_Bulk_Operations.md +15 -6
  24. package/docs/GUIDE/GUIDE_X_Cors.md +2 -5
  25. package/docs/GUIDE/GUIDE_X_File_Uploads.md +2 -3
  26. package/docs/GUIDE/GUIDE_X_Multihome.md +1 -4
  27. package/docs/GUIDE/GUIDE_X_SocketIO.md +0 -2
  28. package/docs/GUIDE/GUIDE_X_URL_Management.md +317 -0
  29. package/docs/GUIDE/index.md +3 -0
  30. package/docs/QUICKSTART.md +12 -12
  31. package/docs/index.md +2 -2
  32. package/docs/mini-guides/file-uploads-complete.md +0 -1
  33. package/examples/url-override-example.js +137 -0
  34. package/index.js +7 -1
  35. package/package.json +5 -9
  36. package/plugins/core/bulk-operations-plugin.js +2 -0
  37. package/plugins/core/connectors/express-plugin.js +43 -24
  38. package/plugins/core/connectors/lib/request-helpers.js +2 -2
  39. package/plugins/core/lib/anyapi/anyapi-registry.js +821 -0
  40. package/plugins/core/lib/anyapi/query/anyapi-query-adapter.js +647 -0
  41. package/plugins/core/lib/anyapi/schema-utils.js +186 -0
  42. package/plugins/core/lib/anyapi/utils/descriptor-helpers.js +113 -0
  43. package/plugins/core/lib/dbTablesOperations.js +305 -0
  44. package/plugins/core/lib/querying/knex-cross-table-search.js +64 -108
  45. package/plugins/core/lib/querying/knex-json-api-transformers-querying.js +21 -27
  46. package/plugins/core/lib/querying/knex-pagination-helpers.js +36 -20
  47. package/plugins/core/lib/querying/knex-query-helpers-base.js +49 -14
  48. package/plugins/core/lib/querying/knex-query-helpers.js +378 -222
  49. package/plugins/core/lib/querying/knex-relationship-includes.js +262 -87
  50. package/plugins/core/lib/querying/url-helpers.js +119 -0
  51. package/plugins/core/lib/querying-writing/compile-schemas.js +36 -28
  52. package/plugins/core/lib/querying-writing/database-capabilities.js +10 -9
  53. package/plugins/core/lib/querying-writing/knex-field-helpers.js +9 -3
  54. package/plugins/core/lib/querying-writing/knex-json-api-transformers.js +2 -2
  55. package/plugins/core/lib/querying-writing/payload-validators.js +10 -16
  56. package/plugins/core/lib/querying-writing/schema-helpers.js +52 -60
  57. package/plugins/core/lib/querying-writing/scope-validations.js +78 -22
  58. package/plugins/core/lib/querying-writing/simplified-helpers.js +208 -61
  59. package/plugins/core/lib/storage/storage-adapter.js +160 -0
  60. package/plugins/core/lib/writing/relationship-processor.js +4 -11
  61. package/plugins/core/rest-api-access.js +456 -0
  62. package/plugins/core/rest-api-anyapi-knex-plugin.js +2285 -0
  63. package/plugins/core/rest-api-knex-plugin.js +226 -87
  64. package/plugins/core/rest-api-multihome-plugin.js +89 -27
  65. package/plugins/core/rest-api-plugin-hooks/turn-scope-init-into-vars.js +0 -1
  66. package/plugins/core/rest-api-plugin-methods/check-permissions.js +1 -5
  67. package/plugins/core/rest-api-plugin-methods/common.js +46 -10
  68. package/plugins/core/rest-api-plugin-methods/delete-relationship.js +31 -27
  69. package/plugins/core/rest-api-plugin-methods/delete.js +1 -4
  70. package/plugins/core/rest-api-plugin-methods/enrich-attributes.js +13 -1
  71. package/plugins/core/rest-api-plugin-methods/get-related.js +114 -56
  72. package/plugins/core/rest-api-plugin-methods/get-relationship.js +4 -3
  73. package/plugins/core/rest-api-plugin-methods/get.js +2 -5
  74. package/plugins/core/rest-api-plugin-methods/patch.js +15 -16
  75. package/plugins/core/rest-api-plugin-methods/post-relationship.js +21 -14
  76. package/plugins/core/rest-api-plugin-methods/post.js +23 -11
  77. package/plugins/core/rest-api-plugin-methods/put.js +18 -21
  78. package/plugins/core/rest-api-plugin-methods/query.js +3 -4
  79. package/plugins/core/rest-api-plugin-methods/release.js +13 -0
  80. package/plugins/core/rest-api-plugin.js +3 -4
  81. package/plugins/core/rest-api-positioning-plugin.js +253 -76
  82. package/plugins/core/socketio-plugin.js +436 -502
  83. package/quickTest.js +2 -2
  84. package/scripts/debug-anyapi-review-filter.mjs +63 -0
  85. package/scripts/inspect-anyapi-descriptor-country.mjs +19 -0
  86. package/scripts/inspect-anyapi-descriptor.mjs +19 -0
  87. package/test-debug.js +59 -0
  88. package/tests/README.md +7 -1
  89. package/tests/anyapi-basic.test.js +102 -0
  90. package/tests/anyapi-cursor-pagination.test.js +144 -0
  91. package/tests/computed-fields.test.js +27 -12
  92. package/tests/cors.test.js +5 -6
  93. package/tests/curl.test.js +1 -1
  94. package/tests/custom-idproperty-relationships.test.js +37 -29
  95. package/tests/field-getters.test.js +25 -7
  96. package/tests/fixtures/api-configs.js +576 -150
  97. package/tests/helpers/storage-mode.js +65 -0
  98. package/tests/helpers/test-utils.js +85 -3
  99. package/tests/include-depth-validation.test.js +35 -1
  100. package/tests/nested-includes.test.js +130 -103
  101. package/tests/pagination-cursor-multifield.test.js +17 -13
  102. package/tests/pagination-enhanced.test.js +19 -24
  103. package/tests/polymorphic-simple.test.js +9 -9
  104. package/tests/positioning-advanced.test.js +19 -19
  105. package/tests/positioning-extended.test.js +25 -25
  106. package/tests/positioning-multifilter.test.js +62 -62
  107. package/tests/positioning.test.js +7 -7
  108. package/tests/queries.test.js +5 -5
  109. package/tests/query-limits.test.js +77 -92
  110. package/tests/rest-api-access.test.js +206 -0
  111. package/tests/searchschema-merge.test.js +182 -0
  112. package/tests/socketio.test.js +33 -14
  113. package/tests/virtual-fields.test.js +0 -1
  114. package/docs/DOCS_ALL.md +0 -21894
  115. package/plugins/core/jwt-auth-plugin.js +0 -897
  116. package/plugins/core/lib/jwt-auth-helpers.js +0 -130
  117. 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?.data?.relationships?.author?.data?.id;
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.data.attributes` to access the complete current record.
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
- context.minimalRecord?.data?.attributes?.status !== 'published') {
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: { hasMany: 'books', through: 'book_authors', foreignKey: 'author_id', otherKey: 'book_id' }
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: { hasMany: 'authors', through: 'book_authors', foreignKey: 'book_id', otherKey: 'author_id' }
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', version: '1.0.0' });
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', 'X-API-Version']
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', version: '1.0.0' });
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', version: '1.0.0' });
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!
@@ -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
 
@@ -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', version: '1.0.0' });
47
+ const api = new Api({ name: 'book-catalog-api'});
48
48
 
49
49
  // Install plugins
50
- await api.use(RestApiPlugin, { publicBaseUrl: '/api' }); // Public URL might be different in case of proxies etc.
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' { options: { publicBaseUrl: '/api/1.0' } }
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
- publisher_id: penguinResult.id
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', publisher_id: '1' }
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', authors_ids: [ '1' ] },
218
- { id: '2', name: 'HarperCollins', authors_ids: [] },
219
- { id: '3', name: 'Oxford University Press', authors_ids: [] }
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', authors_ids: [ '1' ] }
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', publisher_id: '1' } ],
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
- publisher_id: '1'
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', version: '1.0.0' });
87
+ const api = new Api({ name: 'book-catalog-api' });
88
88
 
89
89
  // Install plugins
90
- await api.use(RestApiPlugin, { publicBaseUrl: '/api/1.0' }); // Basic REST plugin
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
 
@@ -17,7 +17,6 @@ import express from 'express';
17
17
  // Create API
18
18
  const api = new Api({
19
19
  name: 'my-library-api',
20
- version: '1.0.0'
21
20
  });
22
21
 
23
22
  // Create storage for different file types