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.
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 +7 -11
  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
@@ -0,0 +1,137 @@
1
+ #!/usr/bin/env node
2
+
3
+ /**
4
+ * Example: Custom URL Override Using Hooks
5
+ *
6
+ * This example demonstrates how to override URL generation using the
7
+ * hooked-api hooks system. This is the recommended approach for custom
8
+ * URL scenarios like API gateways, CDNs, or multi-tenant applications.
9
+ */
10
+
11
+ import { Api } from 'hooked-api';
12
+ import { RestApiPlugin, RestApiKnexPlugin, ExpressPlugin } from 'json-rest-api';
13
+ import knexLib from 'knex';
14
+ import express from 'express';
15
+
16
+ // Create in-memory database
17
+ const knex = knexLib({
18
+ client: 'better-sqlite3',
19
+ connection: { filename: ':memory:' },
20
+ useNullAsDefault: true
21
+ });
22
+
23
+ // Create API with URL override hook
24
+ const api = new Api({
25
+ name: 'url-override-demo',
26
+
27
+ // Define hooks at API creation time
28
+ hooks: {
29
+ // This hook runs at the start of each request
30
+ 'transport:request': [
31
+ async (payload) => {
32
+ const { context, req } = payload;
33
+
34
+ // Example 1: Override based on custom header
35
+ // Useful for CDN or API gateway scenarios
36
+ if (req?.headers?.['x-public-url']) {
37
+ context.urlPrefixOverride = req.headers['x-public-url'];
38
+ console.log(`✓ URL overridden via X-Public-URL header: ${context.urlPrefixOverride}`);
39
+ }
40
+
41
+ // Example 2: API versioning via header
42
+ // Route different API versions while maintaining same backend
43
+ else if (req?.headers?.['x-api-version'] === 'v2') {
44
+ context.urlPrefixOverride = 'https://api.example.com/v2';
45
+ console.log(`✓ URL overridden for API v2: ${context.urlPrefixOverride}`);
46
+ }
47
+
48
+ // Example 3: Multi-tenant based on host
49
+ // Each tenant gets their own domain in responses
50
+ else if (req?.hostname?.includes('tenant-a')) {
51
+ context.urlPrefixOverride = `https://tenant-a.api.com${req.baseUrl || ''}`;
52
+ console.log(`✓ URL overridden for tenant-a: ${context.urlPrefixOverride}`);
53
+ }
54
+
55
+ // Example 4: Environment-based override
56
+ // Force production URLs in staging environment
57
+ else if (process.env.FORCE_PRODUCTION_URLS === 'true') {
58
+ context.urlPrefixOverride = 'https://api.production.com/api';
59
+ console.log(`✓ URL overridden via environment: ${context.urlPrefixOverride}`);
60
+ }
61
+
62
+ return payload;
63
+ }
64
+ ]
65
+ }
66
+ });
67
+
68
+ // Install plugins
69
+ await api.use(RestApiPlugin, {
70
+ returnRecordTransport: { post: 'full' } // Return full JSON:API response
71
+ });
72
+ await api.use(RestApiKnexPlugin, { knex });
73
+ await api.use(ExpressPlugin, { mountPath: '/api' });
74
+
75
+ // Define a simple resource
76
+ await api.addResource('items', {
77
+ schema: {
78
+ name: { type: 'string', required: true, max: 100 },
79
+ description: { type: 'string', max: 500 }
80
+ }
81
+ });
82
+ await api.resources.items.createKnexTable();
83
+
84
+ // Create some test data
85
+ console.log('\n📝 Creating test data...');
86
+ await api.resources.items.post({
87
+ inputRecord: {
88
+ data: {
89
+ type: 'items',
90
+ attributes: {
91
+ name: 'Test Item 1',
92
+ description: 'This is a test item'
93
+ }
94
+ }
95
+ },
96
+ simplified: false
97
+ });
98
+
99
+ // Create Express app
100
+ const app = express();
101
+ app.use(api.http.express.router);
102
+
103
+ // Start server
104
+ const PORT = process.env.PORT || 3333;
105
+ const server = app.listen(PORT, () => {
106
+ console.log(`\n🚀 Server running on http://localhost:${PORT}`);
107
+ console.log('\n📚 Try these examples:\n');
108
+
109
+ console.log('1. Default URL (auto-detected):');
110
+ console.log(` curl http://localhost:${PORT}/api/items`);
111
+ console.log(' → URLs will be: http://localhost:${PORT}/api/items/1\n');
112
+
113
+ console.log('2. Custom CDN URL:');
114
+ console.log(` curl -H "X-Public-URL: https://cdn.example.com/api" http://localhost:${PORT}/api/items`);
115
+ console.log(' → URLs will be: https://cdn.example.com/api/items/1\n');
116
+
117
+ console.log('3. API Version Header:');
118
+ console.log(` curl -H "X-API-Version: v2" http://localhost:${PORT}/api/items`);
119
+ console.log(' → URLs will be: https://api.example.com/v2/items/1\n');
120
+
121
+ console.log('4. Create with custom URL:');
122
+ console.log(` curl -X POST -H "Content-Type: application/vnd.api+json" \\`);
123
+ console.log(` -H "X-Public-URL: https://public.api.com/api" \\`);
124
+ console.log(` -d '{"data":{"type":"items","attributes":{"name":"New Item"}}}' \\`);
125
+ console.log(` http://localhost:${PORT}/api/items`);
126
+ console.log(' → Response URLs will use: https://public.api.com/api/items/2\n');
127
+
128
+ console.log('Press Ctrl+C to stop the server\n');
129
+ });
130
+
131
+ // Graceful shutdown
132
+ process.on('SIGINT', async () => {
133
+ console.log('\n\n👋 Shutting down gracefully...');
134
+ server.close();
135
+ await knex.destroy();
136
+ process.exit(0);
137
+ });
package/index.js CHANGED
@@ -11,9 +11,12 @@
11
11
  // Core plugins
12
12
  export { RestApiPlugin } from './plugins/core/rest-api-plugin.js';
13
13
  export { FileHandlingPlugin } from './plugins/core/file-handling-plugin.js';
14
+ export { CorsPlugin } from './plugins/core/rest-api-cors-plugin.js';
15
+ export { AccessPlugin } from './plugins/core/rest-api-access.js';
14
16
 
15
17
  // Database plugins
16
18
  export { RestApiKnexPlugin } from './plugins/core/rest-api-knex-plugin.js';
19
+ export { RestApiAnyapiKnexPlugin } from './plugins/core/rest-api-anyapi-knex-plugin.js';
17
20
 
18
21
  // Connector plugins
19
22
  export { ExpressPlugin } from './plugins/core/connectors/express-plugin.js';
@@ -32,4 +35,7 @@ export {
32
35
  } from './lib/rest-api-errors.js';
33
36
 
34
37
  // Re-export everything for backward compatibility
35
- export * from './lib/rest-api-errors.js';
38
+ export * from './lib/rest-api-errors.js';
39
+
40
+ // URL helper function for generating API URL prefix
41
+ export { getUrlPrefix } from './plugins/core/lib/querying/url-helpers.js';
package/package.json CHANGED
@@ -1,10 +1,11 @@
1
1
  {
2
2
  "name": "json-rest-api",
3
- "version": "1.0.10",
3
+ "version": "1.0.13",
4
4
  "main": "index.js",
5
5
  "type": "module",
6
6
  "scripts": {
7
7
  "test": "node --test tests/*.test.js",
8
+ "test:anyapi": "JSON_REST_API_STORAGE=anyapi node --test tests/*.test.js",
8
9
  "docs": "node scripts/run-docs.js",
9
10
  "sloc": "sloc --exclude \"(_site|node_modules|\\.git|vendor|docs)\" .",
10
11
  "slod": "cd docs;echo 'DOC LINES:'; cat `find . -iname \\*md | grep -v _site | grep -v vendor` | wc"
@@ -21,8 +22,8 @@
21
22
  "homepage": "https://mobily-enterprises.github.io/json-rest-api",
22
23
  "description": "REST API plugin for hooked-api with JSON:API compliance",
23
24
  "dependencies": {
24
- "hooked-api": "^1.0.14",
25
- "json-rest-schema": "^1.0.8",
25
+ "hooked-api": "^1.0.22",
26
+ "json-rest-schema": "^1.0.10",
26
27
  "semver": "^7.7.2"
27
28
  },
28
29
  "peerDependencies": {
@@ -32,8 +33,7 @@
32
33
  "express": "^4.0.0 || ^5.0.0",
33
34
  "formidable": "^3.0.0",
34
35
  "fractional-indexing": "^3.2.0",
35
- "jsonwebtoken": "^9.0.2",
36
- "jwks-rsa": "^3.2.0",
36
+ "jose": "^5.10.0",
37
37
  "knex": "^3.1.0",
38
38
  "raw-body": "^2.0.0",
39
39
  "redis": "^5.6.0",
@@ -58,10 +58,7 @@
58
58
  "fractional-indexing": {
59
59
  "optional": true
60
60
  },
61
- "jsonwebtoken": {
62
- "optional": true
63
- },
64
- "jwks-rsa": {
61
+ "jose": {
65
62
  "optional": true
66
63
  },
67
64
  "knex": {
@@ -82,8 +79,7 @@
82
79
  "better-sqlite3": "^11.10.0",
83
80
  "express": "^5.1.0",
84
81
  "fractional-indexing": "^3.2.0",
85
- "jsonwebtoken": "^9.0.2",
86
- "jwks-rsa": "^3.2.0",
82
+ "jose": "^5.10.0",
87
83
  "knex": "^3.1.0",
88
84
  "redis": "^5.6.0",
89
85
  "socket.io": "^4.8.1",
@@ -348,6 +348,8 @@ export const BulkOperationsPlugin = {
348
348
 
349
349
  // Hook into scope creation to add bulk routes
350
350
  addHook('afterAddScope', 'bulkOperationsRoutes', {}, async ({ scopeName }) => {
351
+ // Note: context is not available here during route registration
352
+ // The urlPrefix will be calculated per-request in the handler
351
353
  const urlPrefix = api.vars.transport?.mountPath || '';
352
354
  const scopePath = `${urlPrefix}/${scopeName}`;
353
355
 
@@ -24,6 +24,13 @@ export const ExpressPlugin = {
24
24
  dependencies: ['rest-api'],
25
25
 
26
26
  async install({ on, vars, helpers, pluginOptions, log, scopes, api, runHooks, addHook }) {
27
+
28
+
29
+ addHook('release', 'releaseHook', {},
30
+ async ({ api }) => {} // TODO: Anything to do here?
31
+ );
32
+
33
+
27
34
  // Dynamic import for Express
28
35
  let express;
29
36
  try {
@@ -113,16 +120,15 @@ export const ExpressPlugin = {
113
120
  router.use(async (req, res, next) => {
114
121
  const context = createContext(req, res, 'express');
115
122
 
116
- // Auto-detect publicBaseUrl if not explicitly set
117
- if (!vars.publicBaseUrl) {
118
- // Use protocol from X-Forwarded-Proto if behind proxy, otherwise req.protocol
119
- const protocol = req.get('x-forwarded-proto') || req.protocol || 'http';
120
- const host = req.get('x-forwarded-host') || req.get('host');
121
- if (host) {
122
- vars.publicBaseUrl = `${protocol}://${host}${basePath}`;
123
- }
123
+ // Check for Express middleware override first
124
+ if (req.urlPrefixOverride) {
125
+ context.urlPrefixOverride = req.urlPrefixOverride;
124
126
  }
125
127
 
128
+ // Calculate and cache URL prefix (getUrlPrefix handles all logic)
129
+ const { getUrlPrefix } = await import('../lib/querying/url-helpers.js');
130
+ context.urlPrefix = getUrlPrefix(context, { vars: { transport: { mountPath: basePath } } }, req);
131
+
126
132
  // Transport-specific data for hooks
127
133
  const transportData = {
128
134
  request: {
@@ -142,22 +148,28 @@ export const ExpressPlugin = {
142
148
 
143
149
  // Add transport data to context
144
150
  context.transport = transportData;
145
- const shouldContinue = await runHooks('transport:request', context);
146
-
147
- if (!shouldContinue || context.handled) {
148
- if (context.rejection) {
149
- // Apply response headers from hooks
150
- if (transportData.response.headers) {
151
- res.set(transportData.response.headers);
152
- }
153
- return res.status(context.rejection.status || 500).json({
154
- errors: [{
155
- status: String(context.rejection.status || 500),
156
- title: context.rejection.title || 'Request Rejected',
157
- detail: context.rejection.message
158
- }]
159
- });
151
+
152
+ // Run transport hooks - don't check return value for consistency with other hooks
153
+ // Hooks communicate via context flags (rejection, handled) not return values
154
+ await runHooks('transport:request', context);
155
+
156
+ // Check if request was rejected (e.g., authentication failure)
157
+ if (context.rejection) {
158
+ // Apply response headers from hooks
159
+ if (transportData.response.headers) {
160
+ res.set(transportData.response.headers);
160
161
  }
162
+ return res.status(context.rejection.status || 500).json({
163
+ errors: [{
164
+ status: String(context.rejection.status || 500),
165
+ title: context.rejection.title || 'Request Rejected',
166
+ detail: context.rejection.message
167
+ }]
168
+ });
169
+ }
170
+
171
+ // Check if request was already handled by a hook
172
+ if (context.handled) {
161
173
  return;
162
174
  }
163
175
 
@@ -345,7 +357,7 @@ export const ExpressPlugin = {
345
357
  // Set Location header for any successful POST (201 or 204)
346
358
  if (helpers.getLocation) {
347
359
  const location = helpers.getLocation({ scopeName, id: context.id });
348
- const baseUrl = vars.publicBaseUrl || basePath;
360
+ const baseUrl = context.urlPrefix || basePath;
349
361
  res.set('Location', `${baseUrl}${location}`);
350
362
  }
351
363
  }
@@ -415,6 +427,13 @@ export const ExpressPlugin = {
415
427
  // So we use them as-is without adding basePath to avoid double-prefixing
416
428
 
417
429
  router[method.toLowerCase()](path, ...beforeMiddleware, expressHandler);
430
+
431
+ // Debug logging for route registration
432
+
433
+ // Check if the route was actually added
434
+ if (router.stack) {
435
+ const lastRoute = router.stack[router.stack.length - 1];
436
+ }
418
437
  }
419
438
 
420
439
 
@@ -164,7 +164,7 @@ export function extractToken(req) {
164
164
  return bearerMatch[1];
165
165
  }
166
166
  }
167
-
167
+
168
168
  // Check cookies for session token (Auth.js pattern)
169
169
  const cookies = parseCookies(req.headers.cookie || '');
170
170
  if (cookies['__Secure-authjs.session-token']) {
@@ -173,7 +173,7 @@ export function extractToken(req) {
173
173
  if (cookies['authjs.session-token']) {
174
174
  return cookies['authjs.session-token'];
175
175
  }
176
-
176
+
177
177
  return null;
178
178
  }
179
179