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
@@ -55,7 +55,15 @@
55
55
  "Bash(npx node:*)",
56
56
  "Bash(NODE_ENV=test LOG_LEVEL=debug npm test -- tests/query-limits.test.js)",
57
57
  "Bash(NODE_ENV=test LOG_LEVEL=debug node --test tests/query-limits.test.js)",
58
- "Bash(LOG_LEVEL=debug node test-debug.js)"
58
+ "Bash(LOG_LEVEL=debug node test-debug.js)",
59
+ "WebSearch",
60
+ "Bash(LOG_LEVEL=debug node --test tests/relationships.test.js)",
61
+ "Read(///**)",
62
+ "Bash(LOG_LEVEL=debug node --test tests/nested-includes.test.js)",
63
+ "Bash(export LOG_LEVEL=debug)",
64
+ "Read(//home/merc/Development/active/jskit/**)",
65
+ "Bash(LOG_LEVEL=trace node --test tests/relationships.test.js)",
66
+ "Bash(LOG_LEVEL=debug timeout 10 node --test tests/relationship-endpoints.test.js)"
59
67
  ],
60
68
  "deny": []
61
69
  }
package/AGENTS.md ADDED
@@ -0,0 +1,34 @@
1
+ # Repository Guidelines
2
+
3
+ ## Project Structure & Module Organization
4
+ - `index.js` re-exports the public plugins from `plugins/` (REST API core, access, CORS, file, connectors) so new features slot in there.
5
+ - Shared helpers and error types live in `lib/`; reuse or extend these modules before adding new utilities elsewhere.
6
+ - Scenario walkthroughs are under `examples/`, long-form references stay in `docs/`, and development scripts reside in `scripts/`.
7
+ - Tests sit in `tests/` with fixtures at `tests/fixtures/`, helpers in `tests/helpers/`, and `quickTest.js` provides a throwaway API for manual poking.
8
+
9
+ ## Build, Test, and Development Commands
10
+ - `npm test` runs `node --test tests/*.test.js` against the in-memory SQLite fixtures; run it before every push.
11
+ - `node quickTest.js` spins up an in-memory API instance for experimenting with endpoints during development.
12
+ - `npm run docs` rebuilds the static docs via `scripts/run-docs.js` whenever reference prose changes.
13
+ - `npm run sloc` prints a line-count summary to gauge change size before opening a PR.
14
+
15
+ ## Coding Style & Naming Conventions
16
+ - Stay with ESM (`"type": "module"`) and export named symbols only; avoid `default` exports for predictable tree-shaking.
17
+ - Use two-space indentation and keep existing spacing in JSON fixtures and migration files.
18
+ - Name runtime files in lower-kebab case (e.g., `rest-api-knex-plugin.js`) and mirror that pattern for tests like `bulk-operations.test.js`.
19
+ - Keep comments brief and reserved for decisions that are not obvious from the code.
20
+
21
+ ## Testing Guidelines
22
+ - Instantiate the API once per suite following `tests/TEST_TEMPLATE.test.js`, and call `cleanTables()` in `beforeEach` to reset state.
23
+ - Seed data through helpers in `tests/fixtures/api-configs.js`; never invoke `api.addResource` directly in test code.
24
+ - Run suites in strict JSON:API mode (`simplified: false`) unless the scenario explicitly covers the simplified flag.
25
+ - Prefer the shared assertion helpers for response checks to keep expectations consistent.
26
+
27
+ ## Commit & Pull Request Guidelines
28
+ - Use concise, capitalised commit subjects that highlight the affected area (e.g., `Access rules tightened`) and group related changes per commit.
29
+ - Pull requests should include a short summary, linked issue reference, impacted plugins or resources, and `npm test` output or the reason it was skipped.
30
+ - Document externally visible updates with sample payloads or screenshots, and note configuration changes in `docs/` for downstream teams.
31
+
32
+ ## Security & Configuration Tips
33
+ - Keep secrets in local `.env` files; never commit environment-specific strings.
34
+ - When adding connectors or auth flows, review `plugins/core/rest-api-cors-plugin.js`, update optional peers (`express`, `redis`, `socket.io`) if required, and document new toggles in `docs/`.
package/CLAUDE.md CHANGED
@@ -30,6 +30,24 @@ npm run docs:dev
30
30
  ### Project Overview
31
31
  JSON REST API is a powerful REST API plugin for hooked-api that provides JSON:API-compliant endpoints. It follows a plugin-based architecture where functionality is composed through plugins that extend the core API capabilities.
32
32
 
33
+ ### Database-First Design Philosophy
34
+ JSON REST API embraces a **database-first** approach that provides different abstraction levels for different audiences:
35
+
36
+ **For Backend Developers:**
37
+ - Schema definitions directly map to database columns (e.g., `author_id`, `category_id`)
38
+ - Hooks operate at the database level with access to actual field names via `context.belongsToUpdates`
39
+ - Search schemas can reference exact database columns and table joins
40
+ - Full access to write raw Knex queries when needed
41
+ - No abstraction layers hiding database implementation details
42
+
43
+ **For API Consumers:**
44
+ - Relationship names abstract away foreign key fields (use `author` instead of `author_id`)
45
+ - Simplified API mode returns clean objects without database implementation details
46
+ - Foreign key fields are automatically filtered from API responses
47
+ - Consistent interface regardless of underlying database structure
48
+
49
+ This separation ensures backend developers have full control and visibility into database operations while API consumers enjoy a clean, intuitive interface.
50
+
33
51
  ### Core Architecture Components
34
52
 
35
53
  1. **Plugin System (hooked-api based)**
@@ -50,12 +68,19 @@ JSON REST API is a powerful REST API plugin for hooked-api that provides JSON:AP
50
68
  - Each plugin can hook into the request lifecycle for customization
51
69
 
52
70
  4. **Key Libraries in `plugins/core/lib/`**
53
- - `knex-json-api-helpers.js` - Core JSON:API transformations
71
+ - `url-helpers.js` - Centralized URL generation functions
72
+ - `knex-json-api-transformers-querying.js` - JSON:API transformations
54
73
  - `relationship-processor.js` - Handles relationship updates
55
74
  - `payload-validators.js` - Request validation
56
75
  - `knex-query-helpers.js` - Database query building
57
76
  - `simplified-helpers.js` - Simplified API mode transformations
58
77
 
78
+ 5. **URL Management**
79
+ - URLs are automatically generated per-request based on headers
80
+ - No static configuration needed - detects from `Host`, `X-Forwarded-*` headers
81
+ - Override capability via `context.urlPrefixOverride` for complex deployments
82
+ - See `docs/GUIDE/GUIDE_URL_Management.md` for details
83
+
59
84
  ## Testing Guidelines
60
85
 
61
86
  ### CRITICAL: Test Template Usage
package/CRUSH.md ADDED
@@ -0,0 +1,55 @@
1
+ # CRUSH.md - JSON REST API Development Guide
2
+
3
+ ## Development Commands
4
+ ```bash
5
+ # Run all tests
6
+ npm test
7
+
8
+ # Run specific test file
9
+ node --test tests/relationships.test.js
10
+
11
+ # Run tests with debug logging
12
+ LOG_LEVEL=debug npm test
13
+
14
+ # Run documentation site
15
+ npm run docs
16
+ ```
17
+
18
+ ## Code Style Guidelines
19
+ - **ES Modules**: Use `import/export` syntax (package.json has `"type": "module"`)
20
+ - **File Naming**: kebab-case for files, camelCase for functions/variables
21
+ - **Error Handling**: Use custom error classes from `lib/rest-api-errors.js`
22
+ - **JSON:API Compliance**: All responses must follow JSON:API specification
23
+ - **Async/Await**: Prefer async/await over callbacks/promises
24
+ - **JSDoc Comments**: Document complex functions with @param and @returns
25
+
26
+ ## Testing Rules (CRITICAL)
27
+ - **API Creation**: Only in `before()` hook, never in `beforeEach()`
28
+ - **Resource Definition**: NEVER use `api.addResource()` in test files - define in `tests/fixtures/api-configs.js`
29
+ - **Data Access**: Use API methods (`api.resources.[resource].get/post/query`), never direct DB queries
30
+ - **Context Parameter**: Pass context as SECOND parameter: `api.resources.books.post({inputRecord}, context)`
31
+ - **Cleanup**: Use `cleanTables()` in `beforeEach()` to reset data
32
+
33
+ ## Import Patterns
34
+ ```javascript
35
+ // Core imports first
36
+ import { validateRelationships } from './lib/querying-writing/scope-validations.js';
37
+
38
+ // Hook imports
39
+ import compileResourceSchemas from './rest-api-plugin-hooks/compile-resource-schemas.js';
40
+
41
+ // Method imports
42
+ import queryMethod from './rest-api-plugin-methods/query.js';
43
+ ```
44
+
45
+ ## Error Handling
46
+ - Use `RestApiValidationError` for validation failures
47
+ - Use `RestApiResourceError` for resource-related errors
48
+ - Use `RestApiPayloadError` for malformed payloads
49
+ - Always include descriptive error messages
50
+
51
+ ## JSON:API Helpers
52
+ - Use `createJsonApiDocument()` for creating request payloads
53
+ - Use `createRelationship()` for belongsTo relationships
54
+ - Use `createToManyRelationship()` for hasMany relationships
55
+ - Use `validateJsonApiStructure()` to validate responses
package/README.md CHANGED
@@ -37,12 +37,12 @@ const knex = knexLib({
37
37
  });
38
38
 
39
39
  // Create API instance
40
- const api = new Api({ name: 'book-catalog-api', version: '1.0.0', logLevel: 'trace' });
40
+ const api = new Api({ name: 'book-catalog-api', logLevel: 'trace' });
41
41
 
42
42
  // Install plugins
43
- await api.use(RestApiPlugin, { publicBaseUrl: '/api' });
43
+ await api.use(RestApiPlugin);
44
44
  await api.use(RestApiKnexPlugin, { knex });
45
- await api.use(ExpressPlugin, { mountPath: '/api' }); // Added: Express Plugin
45
+ await api.use(ExpressPlugin, { mountPath: '/api' }); // Express connector
46
46
 
47
47
 
48
48
  // Define authors resource, which belongs to a publisher
package/TODO.todo CHANGED
@@ -1,3 +1,11 @@
1
+ Potential Enhancements
2
+
3
+ 1. Provider Priority: Consider adding a "primary_provider" field to handle users who authenticate through multiple providers.
4
+ 2. Field Cleanup: Maybe add a helper to remove provider fields when a user unlinks that provider.
5
+ 3. Migration Safety: The automatic column creation via RestApiKnexPlugin is convenient, but you might want provider plugins to check if their
6
+ fields already exist before extending.
7
+
8
+
1
9
  ☐ Delete "Creating Custom Storage Plugins for JSON REST API", make an op plugin instead.
2
10
  ☐ Improve the look of the CSS, especially black and long code fragments, title fonts
3
11
  ☐ Check manually that the API is OK
package/docs/API.md CHANGED
@@ -139,7 +139,7 @@ All parameters are passed within a single `params` object:
139
139
  id: '1',
140
140
  title: 'First Article',
141
141
  content: 'Article content...',
142
- author_id: '10',
142
+ author: '10',
143
143
  author: {
144
144
  id: '10',
145
145
  name: 'John Doe'
@@ -181,13 +181,13 @@ const result = await api.resources.articles.query({
181
181
  queryParams: {
182
182
  filters: {
183
183
  status: 'published',
184
- author_id: '10'
184
+ author: '10'
185
185
  }
186
186
  }
187
187
  });
188
188
 
189
189
  // HTTP equivalent
190
- // GET /articles?filter[status]=published&filter[author_id]=10
190
+ // GET /articles?filter[status]=published&filter[author]=10
191
191
  ```
192
192
 
193
193
  **Query with Sorting and Pagination:**
@@ -325,7 +325,7 @@ const result = await api.resources.[resourceType].get(params, context)
325
325
  id: '1',
326
326
  title: 'Article Title',
327
327
  content: 'Full article content...',
328
- author_id: '10',
328
+ author: '10',
329
329
  author: {
330
330
  id: '10',
331
331
  name: 'John Doe'
@@ -483,7 +483,7 @@ const result = await api.resources.articles.post({
483
483
  title: 'New Article',
484
484
  content: 'Article content...',
485
485
  status: 'draft',
486
- author_id: '10'
486
+ author: '10'
487
487
  }
488
488
  });
489
489
 
@@ -520,8 +520,8 @@ const result = await api.resources.articles.post({
520
520
  inputRecord: {
521
521
  title: 'New Article',
522
522
  content: 'Article content...',
523
- author_id: '10',
524
- tag_ids: ['1', '2', '3']
523
+ author: '10',
524
+ tags: ['1', '2', '3']
525
525
  },
526
526
  queryParams: {
527
527
  include: ['author', 'tags']
@@ -639,7 +639,7 @@ const result = await api.resources.articles.put({
639
639
  title: 'Completely New Title',
640
640
  content: 'Entirely new content',
641
641
  status: 'published',
642
- author_id: '10'
642
+ author: '10'
643
643
  // Note: All attributes must be provided
644
644
  }
645
645
  });
@@ -681,8 +681,8 @@ const result = await api.resources.articles.put({
681
681
  title: 'Article Without Author',
682
682
  content: 'Content...',
683
683
  status: 'draft',
684
- author_id: null, // Remove author
685
- tag_ids: [] // Remove all tags
684
+ author: null, // Remove author
685
+ tags: [] // Remove all tags
686
686
  }
687
687
  });
688
688
  ```
@@ -805,8 +805,8 @@ const result = await api.resources.articles.patch({
805
805
  const result = await api.resources.articles.patch({
806
806
  inputRecord: {
807
807
  id: '1',
808
- author_id: '30',
809
- tag_ids: ['3', '4', '5']
808
+ author: '30',
809
+ tags: ['3', '4', '5']
810
810
  }
811
811
  });
812
812
  ```
@@ -1496,7 +1496,7 @@ api.resource('articles').hook('afterSchemaValidate', async (context) => {
1496
1496
  ```javascript
1497
1497
  api.resource('articles').hook('checkDataPermissionsDelete', async (context) => {
1498
1498
  // Only author or admin can delete
1499
- if (context.auth.userId !== context.existingRecord.author_id &&
1499
+ if (context.auth.userId !== context.existingRecord.relationships?.author?.data?.id &&
1500
1500
  !context.auth.isAdmin) {
1501
1501
  throw new Error('Unauthorized to delete this article');
1502
1502
  }
@@ -1533,7 +1533,7 @@ api.resource('articles').hook('beforeDataQuery', async (context) => {
1533
1533
  if (context.auth.userId && !context.auth.isAdmin) {
1534
1534
  context.storageParams.filters = {
1535
1535
  ...context.storageParams.filters,
1536
- author_id: context.auth.userId
1536
+ author: context.auth.userId
1537
1537
  };
1538
1538
  }
1539
1539
  });
@@ -1633,7 +1633,6 @@ api.resource('articles').hook('finish', async (context) => {
1633
1633
  context.response.meta = {
1634
1634
  ...context.response.meta,
1635
1635
  generated_at: new Date().toISOString(),
1636
- api_version: '1.0'
1637
1636
  };
1638
1637
  });
1639
1638
  ```
@@ -1741,13 +1740,13 @@ const result = await api.resources.articles.query({
1741
1740
  queryParams: {
1742
1741
  filters: {
1743
1742
  status: 'published',
1744
- author_id: '10'
1743
+ author: '10'
1745
1744
  }
1746
1745
  }
1747
1746
  });
1748
1747
 
1749
1748
  // HTTP equivalent
1750
- // GET /articles?filter[status]=published&filter[author_id]=10
1749
+ // GET /articles?filter[status]=published&filter[author]=10
1751
1750
  ```
1752
1751
 
1753
1752
  #### Operator-based Filtering
@@ -2234,7 +2233,7 @@ api.addResource({
2234
2233
  type: 'datetime',
2235
2234
  operators: ['gt', 'gte', 'lt', 'lte']
2236
2235
  },
2237
- author_id: {
2236
+ author: {
2238
2237
  type: 'number',
2239
2238
  operators: ['eq', 'in']
2240
2239
  },
@@ -67,8 +67,8 @@ const BookSerializer = new JSONAPISerializer('books', {
67
67
  import { Api } from 'hooked-api';
68
68
  import { RestApiPlugin, RestApiKnexPlugin } from 'json-rest-api';
69
69
 
70
- const api = new Api({ name: 'bookstore-api', version: '1.0.0' });
71
- await api.use(RestApiPlugin, { publicBaseUrl: '/api/v1' });
70
+ const api = new Api({ name: 'bookstore-api'});
71
+ await api.use(RestApiPlugin); // URLs auto-detected
72
72
  await api.use(RestApiKnexPlugin, { knex });
73
73
 
74
74
  await api.addResource('books', {
@@ -109,7 +109,7 @@ export class BookEntity {
109
109
  import { Api } from 'hooked-api';
110
110
  import { RestApiPlugin, ExpressPlugin, SocketIOPlugin } from 'json-rest-api';
111
111
 
112
- const api = new Api({ name: 'books-api', version: '1.0.0' });
112
+ const api = new Api({ name: 'books-api' });
113
113
  await api.use(RestApiPlugin);
114
114
  await api.use(ExpressPlugin); // HTTP transport
115
115
  await api.use(SocketIOPlugin); // WebSocket transport
@@ -173,9 +173,9 @@ const response = await fetch('/api/books', {
173
173
  // Simplified mode (for internal/programmatic use)
174
174
  const book = await api.resources.books.post({
175
175
  title: 'My Book',
176
- author_id: 1
176
+ author: 1
177
177
  });
178
- // Returns: { id: '1', title: 'My Book', author_id: 1 }
178
+ // Returns: { id: '1', title: 'My Book', author: { id: 1 } }
179
179
 
180
180
  // Configure both modes simultaneously
181
181
  await api.use(RestApiPlugin, {
@@ -201,10 +201,10 @@ import {
201
201
  SocketIOPlugin
202
202
  } from 'json-rest-api';
203
203
 
204
- const api = new Api({ name: 'full-featured-api', version: '1.0.0' });
204
+ const api = new Api({ name: 'full-featured-api' });
205
205
 
206
206
  // Core functionality
207
- await api.use(RestApiPlugin, { publicBaseUrl: '/api/v1' });
207
+ await api.use(RestApiPlugin); // URLs auto-detected
208
208
  await api.use(RestApiKnexPlugin, { knex: connection });
209
209
 
210
210
  // Transport layer
@@ -264,7 +264,7 @@ await api.addResource('comments', {
264
264
  // Parent resources define reverse relationship
265
265
  await api.addResource('posts', {
266
266
  relationships: {
267
- comments: { hasMany: 'comments', via: 'commentable' }
267
+ comments: { type: 'hasMany', target: 'comments', via: 'commentable' }
268
268
  }
269
269
  });
270
270
 
@@ -290,20 +290,25 @@ Automatic optimization that competitors lack:
290
290
  // Define resources with nested relationships
291
291
  await api.addResource('countries', {
292
292
  relationships: {
293
- publishers: { hasMany: 'publishers', foreignKey: 'country_id' }
293
+ publishers: { type: 'hasMany', target: 'publishers', foreignKey: 'country_id' }
294
294
  }
295
295
  });
296
296
 
297
297
  await api.addResource('publishers', {
298
298
  relationships: {
299
- books: { hasMany: 'books', foreignKey: 'publisher_id' },
299
+ books: { type: 'hasMany', target: 'books', foreignKey: 'publisher_id' },
300
300
  country: { belongsTo: 'countries' }
301
301
  }
302
302
  });
303
303
 
304
304
  await api.addResource('books', {
305
305
  relationships: {
306
- authors: { hasMany: 'authors', through: 'book_authors', foreignKey: 'book_id', otherKey: 'author_id' },
306
+ authors: {
307
+ type: 'manyToMany',
308
+ through: 'book_authors',
309
+ foreignKey: 'book_id',
310
+ otherKey: 'author_id'
311
+ },
307
312
  publisher: { belongsTo: 'publishers' }
308
313
  }
309
314
  });
@@ -362,7 +367,7 @@ app.get('/books', async (req, res) => {
362
367
  import { Api } from 'hooked-api';
363
368
  import { RestApiPlugin, RestApiKnexPlugin, ExpressPlugin } from 'json-rest-api';
364
369
 
365
- const api = new Api({ name: 'books-api', version: '1.0.0' });
370
+ const api = new Api({ name: 'books-api' });
366
371
  await api.use(RestApiPlugin);
367
372
  await api.use(RestApiKnexPlugin, { knex: db });
368
373
  await api.use(ExpressPlugin, { mountPath: '/api' });
@@ -398,7 +403,7 @@ export class BooksController {
398
403
  import { Api } from 'hooked-api';
399
404
  import { RestApiPlugin, RestApiKnexPlugin } from 'json-rest-api';
400
405
 
401
- const api = new Api({ name: 'books-api', version: '1.0.0' });
406
+ const api = new Api({ name: 'books-api' });
402
407
  await api.use(RestApiPlugin);
403
408
  await api.use(RestApiKnexPlugin, { knex });
404
409
 
@@ -41,10 +41,10 @@ const knex = knexLib({
41
41
  });
42
42
 
43
43
  // Create API instance
44
- const api = new Api({ name: 'book-catalog-api', version: '1.0.0' });
44
+ const api = new Api({ name: 'book-catalog-api' });
45
45
 
46
46
  // Install plugins
47
- await api.use(RestApiPlugin, { publicBaseUrl: '/api/1.0' });
47
+ await api.use(RestApiPlugin);
48
48
  await api.use(RestApiKnexPlugin, { knex });
49
49
 
50
50
  // Define schemas for our book catalog system
@@ -56,8 +56,8 @@ await api.addResource('countries', {
56
56
  code: { type: 'string', max: 2, unique: true, search: true }, // ISO country code
57
57
  },
58
58
  relationships: {
59
- publishers: { hasMany: 'publishers', foreignKey: 'country_id' },
60
- books: { hasMany: 'books', foreignKey: 'country_id' }
59
+ publishers: { type: 'hasMany', target: 'publishers', foreignKey: 'country_id' },
60
+ books: { type: 'hasMany', target: 'books', foreignKey: 'country_id' }
61
61
  },
62
62
  });
63
63
  await api.resources.countries.createKnexTable()
@@ -89,7 +89,6 @@ To change loglevels, pass a logLevel option to the API:
89
89
  ```javascript
90
90
  const api = new Api({
91
91
  name: 'book-catalog-api',
92
- version: '1.0.0',
93
92
  logLevel: 'warn' // Only show warnings and errors
94
93
  });
95
94
  ```
@@ -220,9 +219,9 @@ console.log('Created Country:', inspect(countryUs));
220
219
  // type: 'countries',
221
220
  // id: '1',
222
221
  // attributes: { name: 'United States', code: 'US' },
223
- // links: { self: '/api/1.0/countries/1' }
222
+ // links: { self: '/api/countries/1' }
224
223
  // },
225
- // links: { self: '/api/1.0/countries/1' }
224
+ // links: { self: '/api/countries/1' }
226
225
  // }
227
226
 
228
227
  // Fetch a country by ID (non-simplified mode)
@@ -237,13 +236,13 @@ console.log('Refetched Country:', inspect(countryUsRefetched));
237
236
  // type: 'countries',
238
237
  // id: '1',
239
238
  // attributes: { name: 'United States', code: 'US' },
240
- // links: { self: '/api/1.0/countries/1' }
239
+ // links: { self: '/api/countries/1' }
241
240
  // },
242
- // links: { self: '/api/1.0/countries/1' }
241
+ // links: { self: '/api/countries/1' }
243
242
  // }
244
243
  ```
245
244
 
246
- (Note that the full JSON:API record includes links to resources, which use `publicBaseUrl` set in when `use()`ing the `json-rest-api` plugin.)
245
+ (Note that the full JSON:API record includes links to resources, which are automatically generated based on the request headers.)
247
246
 
248
247
  As you can see, when `simplified: false` is used:
249
248
 
@@ -348,7 +347,7 @@ Here's how these settings work:
348
347
 
349
348
  ```javascript
350
349
  // Example 1: Using defaults
351
- const api = new Api({ name: 'api', version: '1.0.0' });
350
+ const api = new Api({ name: 'api' });
352
351
  await api.use(RestApiPlugin);
353
352
  // Default: returnRecordApi='full', returnRecordTransport='no'
354
353
 
@@ -418,9 +417,9 @@ console.log('Full JSON:API response:', inspect(fullJsonApi));
418
417
  // type: 'countries',
419
418
  // id: '4',
420
419
  // attributes: { name: 'France', code: 'FR' },
421
- // links: { self: '/api/1.0/countries/4' }
420
+ // links: { self: '/api/countries/4' }
422
421
  // },
423
- // links: { self: '/api/1.0/countries/4' }
422
+ // links: { self: '/api/countries/4' }
424
423
  // }
425
424
 
426
425
  // Non-simplified mode with minimal return
@@ -542,12 +541,12 @@ const knex = knexLib({
542
541
  });
543
542
 
544
543
  // Create API instance
545
- const api = new Api({ name: 'book-catalog-api', version: '1.0.0' });
544
+ const api = new Api({ name: 'book-catalog-api' });
546
545
 
547
546
  // Install plugins
548
- await api.use(RestApiPlugin, { publicBaseUrl: '/api/1.0' });
547
+ await api.use(RestApiPlugin);
549
548
  await api.use(RestApiKnexPlugin, { knex });
550
- await api.use(ExpressPlugin, { mountPath: '/api' }); // Added: Express Plugin
549
+ await api.use(ExpressPlugin, { mountPath: '/api' }); // Added: Express Plugin
551
550
 
552
551
  // Countries table
553
552
  await api.addResource('countries', {
@@ -757,12 +756,11 @@ Keep-Alive: timeout=5
757
756
  When passing a parameter, `rest-api-plugin` normalises them (when needed) and stores them into plugin variables. This means that these two ways of defining `returnRecordApi` is identical:
758
757
 
759
758
  ```javascript
760
- await api.use(RestApiPlugin, { publicBaseUrl: '/api/1.0', returnRecordTransport: 'minimal'});
759
+ await api.use(RestApiPlugin, { returnRecordTransport: 'minimal' });
761
760
 
762
761
  // ...or...
763
762
 
764
763
  await api.use(RestApiPlugin, {
765
- publicBaseUrl: '/api/1.0',
766
764
  vars: {
767
765
  returnRecordTransport: 'minimal'
768
766
  }
@@ -776,7 +774,6 @@ Here is a full list of parameters and their respective variables:
776
774
  | `queryDefaultLimit` | `vars.queryDefaultLimit` | `25` | Default number of records returned in query results | ✓ |
777
775
  | `queryMaxLimit` | `vars.queryMaxLimit` | `100` | Maximum allowed limit for query results | ✓ |
778
776
  | `includeDepthLimit` | `vars.includeDepthLimit` | `3` | Maximum depth for nested relationship includes | ✓ |
779
- | `publicBaseUrl` | `vars.publicBaseUrl` | `''` | Base URL for generated links (e.g., `https://api.example.com/v1`) | ✓ |
780
777
  | `enablePaginationCounts` | `vars.enablePaginationCounts` | `true` | Whether to include total count in pagination metadata | ✓ |
781
778
  | `simplifiedApi` | `vars.simplifiedApi` | `true` | Use simplified format for programmatic API calls | ✓ |
782
779
  | `simplifiedTransport` | `vars.simplifiedTransport` | `false` | Use simplified format for HTTP/REST endpoints | ✓ |
@@ -832,7 +829,7 @@ When you install the REST API plugins, the following helpers are added to `api.h
832
829
  try {
833
830
  // Use transaction in multiple operations
834
831
  await api.resources.countries.post({ name: 'France', code: 'FR' }, { transaction: trx });
835
- await api.resources.publishers.post({ name: 'French Press', country_id: 1 }, { transaction: trx });
832
+ await api.resources.publishers.post({ name: 'French Press', country: 1 }, { transaction: trx });
836
833
  await trx.commit();
837
834
  } catch (error) {
838
835
  await trx.rollback();
@@ -938,7 +935,6 @@ Each resource (added via `api.addResource()`) gets these methods automatically:
938
935
 
939
936
  - **`api.rest.registerFileDetector(detector)`** - Registers file upload detectors (requires FileHandlingPlugin)
940
937
  - **`api.rest.fileDetectors`** - Registry of file detectors for handling uploads
941
- - **`api.knex.helpers.crossTableSearch`** - Utilities for searching across related tables
942
938
 
943
939
  ## Summary
944
940
 
@@ -20,10 +20,10 @@ const knex = knexLib({
20
20
  });
21
21
 
22
22
  // Create API instance
23
- const api = new Api({ name: 'book-catalog-api', version: '1.0.0' });
23
+ const api = new Api({ name: 'book-catalog-api' });
24
24
 
25
25
  // Install plugins
26
- await api.use(RestApiPlugin, { publicBaseUrl: '/api/1.0' });
26
+ await api.use(RestApiPlugin); // URLs auto-detected from request headers
27
27
  await api.use(RestApiKnexPlugin, { knex });
28
28
  await api.use(ExpressPlugin, { mountPath: '/api' }); // Added: Express Plugin
29
29
 
@@ -52,6 +52,37 @@ This will ensure that each example has a fresh start.
52
52
 
53
53
  Each example will be introduced programmatically first, and then via HTTP. The HTTP calls will be run assuming that the API calls (and any data created with them) stay. The use of the in-memory database will be assumed, which means that the data will start afresh each time.
54
54
 
55
+ ## Database-First Design Philosophy
56
+
57
+ JSON REST API follows a **database-first** approach, providing different levels of abstraction for different audiences:
58
+
59
+ ### For Backend Developers (Schema Definition, Hooks, and Internal Logic)
60
+
61
+ When defining schemas and writing hooks, you work directly with database reality:
62
+ - Schema fields map directly to database columns (`author_id`, `category_id`, `commentable_type`)
63
+ - Hooks receive actual database field names in `context.belongsToUpdates`
64
+ - Search schemas can reference exact database columns and table structures
65
+ - Full access to write raw Knex queries when needed
66
+
67
+ This direct approach ensures backend developers have complete control and visibility into database operations.
68
+
69
+ ### For API Consumers (External Interface)
70
+
71
+ The API layer provides a clean abstraction that shields consumers from database implementation details:
72
+ - Relationships use semantic names (`author`, `category`) instead of foreign key fields
73
+ - Foreign key fields (`author_id`, `category_id`) are automatically filtered from API responses
74
+ - Polymorphic type/id fields (`commentable_type`, `commentable_id`) are hidden behind relationship objects
75
+ - Simplified mode returns intuitive objects without exposing database structure
76
+
77
+ ### Why This Matters for Relationships
78
+
79
+ This abstraction is particularly important for **belongsTo** and **polymorphic** relationships, where foreign key fields would otherwise be exposed in API responses. The system ensures that:
80
+ - Input: Consumers provide relationships using clean names (`author: 123`)
81
+ - Output: Responses return relationship objects (`author: { id: '123' }`)
82
+ - Internal: Hooks and database operations work with actual columns (`author_id`)
83
+
84
+ This design provides the best of both worlds: backend developers get full database access for powerful implementations, while API consumers enjoy a clean, intuitive interface.
85
+
55
86
  ---
56
87
 
57
88
  [Back to Guide](./README.md) | [Next: 2.2 Manipulating and searching tables with no relationships](./GUIDE_2_2_Manipulating_And_Searching_Tables.md)