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
|
@@ -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
|
-
- `
|
|
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',
|
|
40
|
+
const api = new Api({ name: 'book-catalog-api', logLevel: 'trace' });
|
|
41
41
|
|
|
42
42
|
// Install plugins
|
|
43
|
-
await api.use(RestApiPlugin
|
|
43
|
+
await api.use(RestApiPlugin);
|
|
44
44
|
await api.use(RestApiKnexPlugin, { knex });
|
|
45
|
-
await api.use(ExpressPlugin, {
|
|
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
|
-
|
|
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
|
-
|
|
184
|
+
author: '10'
|
|
185
185
|
}
|
|
186
186
|
}
|
|
187
187
|
});
|
|
188
188
|
|
|
189
189
|
// HTTP equivalent
|
|
190
|
-
// GET /articles?filter[status]=published&filter[
|
|
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
|
-
|
|
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
|
-
|
|
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
|
-
|
|
524
|
-
|
|
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
|
-
|
|
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
|
-
|
|
685
|
-
|
|
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
|
-
|
|
809
|
-
|
|
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.
|
|
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
|
-
|
|
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
|
-
|
|
1743
|
+
author: '10'
|
|
1745
1744
|
}
|
|
1746
1745
|
}
|
|
1747
1746
|
});
|
|
1748
1747
|
|
|
1749
1748
|
// HTTP equivalent
|
|
1750
|
-
// GET /articles?filter[status]=published&filter[
|
|
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
|
-
|
|
2236
|
+
author: {
|
|
2238
2237
|
type: 'number',
|
|
2239
2238
|
operators: ['eq', 'in']
|
|
2240
2239
|
},
|
package/docs/COMPARISON.md
CHANGED
|
@@ -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'
|
|
71
|
-
await api.use(RestApiPlugin
|
|
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'
|
|
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
|
-
|
|
176
|
+
author: 1
|
|
177
177
|
});
|
|
178
|
-
// Returns: { id: '1', title: 'My Book',
|
|
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'
|
|
204
|
+
const api = new Api({ name: 'full-featured-api' });
|
|
205
205
|
|
|
206
206
|
// Core functionality
|
|
207
|
-
await api.use(RestApiPlugin
|
|
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: {
|
|
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'
|
|
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'
|
|
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'
|
|
44
|
+
const api = new Api({ name: 'book-catalog-api' });
|
|
45
45
|
|
|
46
46
|
// Install plugins
|
|
47
|
-
await api.use(RestApiPlugin
|
|
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/
|
|
222
|
+
// links: { self: '/api/countries/1' }
|
|
224
223
|
// },
|
|
225
|
-
// links: { self: '/api/
|
|
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/
|
|
239
|
+
// links: { self: '/api/countries/1' }
|
|
241
240
|
// },
|
|
242
|
-
// links: { self: '/api/
|
|
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
|
|
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'
|
|
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/
|
|
420
|
+
// links: { self: '/api/countries/4' }
|
|
422
421
|
// },
|
|
423
|
-
// links: { self: '/api/
|
|
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'
|
|
544
|
+
const api = new Api({ name: 'book-catalog-api' });
|
|
546
545
|
|
|
547
546
|
// Install plugins
|
|
548
|
-
await api.use(RestApiPlugin
|
|
547
|
+
await api.use(RestApiPlugin);
|
|
549
548
|
await api.use(RestApiKnexPlugin, { knex });
|
|
550
|
-
await api.use(ExpressPlugin, {
|
|
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, {
|
|
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',
|
|
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'
|
|
23
|
+
const api = new Api({ name: 'book-catalog-api' });
|
|
24
24
|
|
|
25
25
|
// Install plugins
|
|
26
|
-
await api.use(RestApiPlugin
|
|
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)
|