json-rest-api 1.0.23 → 1.0.25
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/README.md +24 -2
- package/docs/API.md +157 -33
- package/docs/GUIDE/GUIDE_1_Initial_Setup.md +88 -6
- package/docs/GUIDE/GUIDE_2_1_The_Starting_Point.md +3 -3
- package/docs/GUIDE/GUIDE_3_Field_Transformations.md +2 -0
- package/docs/GUIDE/GUIDE_7_Hooks_Data_Management_And_Plugins.md +1 -1
- package/docs/GUIDE/GUIDE_X_Autofiltering.md +63 -0
- package/docs/GUIDE/GUIDE_X_File_Uploads.md +17 -12
- package/docs/GUIDE/GUIDE_X_Knex_Schema_And_Migrations.md +122 -2
- package/docs/GUIDE/GUIDE_X_Row_Policies.md +603 -0
- package/docs/GUIDE/GUIDE_X_SocketIO.md +15 -10
- package/docs/GUIDE/GUIDE_X_URL_Management.md +93 -254
- package/docs/GUIDE/GUIDE_X_Writing_Plugins.md +2 -0
- package/docs/GUIDE/index.md +4 -1
- package/fixing_plan.md +112 -0
- package/index.js +2 -0
- package/lib/enhanced-logger.js +1 -1
- package/package.json +6 -2
- package/plugins/core/bulk-operations-plugin.js +31 -38
- package/plugins/core/connectors/express-plugin.js +6 -7
- package/plugins/core/connectors/fastify-plugin.js +5 -1
- package/plugins/core/connectors/lib/busboy-detector.js +4 -4
- package/plugins/core/connectors/lib/connector-core.js +17 -11
- package/plugins/core/connectors/lib/transport-route-schemas.js +12 -60
- package/plugins/core/file-handling-plugin.js +126 -96
- package/plugins/core/lib/anyapi/anyapi-registry.js +21 -3
- package/plugins/core/lib/anyapi/schema-utils.js +44 -4
- package/plugins/core/lib/anyapi/utils/descriptor-helpers.js +11 -2
- package/plugins/core/lib/dbIntrospection.js +1 -1
- package/plugins/core/lib/dbTablesOperations.js +10 -3
- package/plugins/core/lib/querying/knex-cross-table-search.js +0 -7
- package/plugins/core/lib/querying/knex-json-api-transformers-querying.js +20 -16
- package/plugins/core/lib/querying/knex-pagination-helpers.js +38 -84
- package/plugins/core/lib/querying/knex-query-helpers.js +5 -6
- package/plugins/core/lib/querying/knex-relationship-includes.js +40 -46
- package/plugins/core/lib/querying/knex-window-queries.js +10 -77
- package/plugins/core/lib/querying/url-helpers.js +9 -21
- package/plugins/core/lib/querying-writing/compile-schemas.js +10 -6
- package/plugins/core/lib/querying-writing/connectors-query-parser.js +51 -0
- package/plugins/core/lib/querying-writing/database-capabilities.js +1 -1
- package/plugins/core/lib/querying-writing/database-value-normalizers.js +18 -9
- package/plugins/core/lib/querying-writing/knex-field-helpers.js +7 -3
- package/plugins/core/lib/querying-writing/relationship-contracts.js +56 -0
- package/plugins/core/lib/querying-writing/request-contracts.js +53 -4
- package/plugins/core/lib/querying-writing/resource-id-normalization.js +131 -0
- package/plugins/core/lib/querying-writing/return-record-settings.js +47 -0
- package/plugins/core/lib/querying-writing/schema-helpers.js +34 -43
- package/plugins/core/lib/querying-writing/simplified-helpers.js +4 -11
- package/plugins/core/lib/storage/canonical-storage-mapping.js +24 -3
- package/plugins/core/lib/storage/storage-adapter.js +76 -14
- package/plugins/core/lib/storage/storage-mapping.js +86 -16
- package/plugins/core/lib/writing/many-to-many-manipulations.js +8 -5
- package/plugins/core/lib/writing/relationship-processor.js +10 -1
- package/plugins/core/rest-api-anyapi-knex-plugin.js +402 -197
- package/plugins/core/rest-api-knex-plugin.js +93 -76
- package/plugins/core/rest-api-plugin-hooks/turn-scope-init-into-vars.js +9 -2
- package/plugins/core/rest-api-plugin-methods/common.js +112 -72
- package/plugins/core/rest-api-plugin-methods/delete-relationship.js +34 -15
- package/plugins/core/rest-api-plugin-methods/delete.js +8 -7
- package/plugins/core/rest-api-plugin-methods/get-related.js +22 -15
- package/plugins/core/rest-api-plugin-methods/get-relationship.js +8 -3
- package/plugins/core/rest-api-plugin-methods/get.js +7 -1
- package/plugins/core/rest-api-plugin-methods/patch-relationship.js +26 -8
- package/plugins/core/rest-api-plugin-methods/patch.js +34 -9
- package/plugins/core/rest-api-plugin-methods/post-relationship.js +35 -16
- package/plugins/core/rest-api-plugin-methods/post.js +12 -7
- package/plugins/core/rest-api-plugin-methods/put.js +35 -10
- package/plugins/core/rest-api-plugin-methods/release.js +0 -2
- package/plugins/core/rest-api-plugin.js +11 -37
- package/plugins/core/rest-api-positioning-plugin.js +1 -4
- package/plugins/core/rest-api-row-policy-plugin.js +157 -0
- package/plugins/core/socketio-plugin.js +0 -1
- package/plugins/storage/local-storage.js +45 -20
- package/plugins/storage/s3-storage.js +23 -54
- package/quickTest.js +1 -0
- package/scripts/run-docs.js +80 -84
- package/test-debug.js +1 -1
- package/tests/README.md +8 -1
- package/tests/anyapi-basic.test.js +3 -0
- package/tests/anyapi-cursor-pagination.test.js +7 -1
- package/tests/anyapi-custom-idproperty.test.js +163 -0
- package/tests/custom-id-mysql-returning-fallback.test.js +128 -0
- package/tests/db-introspection.test.js +4 -1
- package/tests/db-table-operations.test.js +25 -20
- package/tests/fastify-plugin.test.js +49 -6
- package/tests/field-setters.test.js +10 -10
- package/tests/file-handling.test.js +118 -0
- package/tests/fixtures/api-configs.js +495 -4
- package/tests/full-jsonapi-belongsto-linkage.test.js +150 -0
- package/tests/http-connectors-parity.test.js +165 -5
- package/tests/id-normalization.test.js +444 -0
- package/tests/local-storage.test.js +96 -0
- package/tests/logical-id-write-validation.test.js +151 -0
- package/tests/pagination-cursor-multifield.test.js +132 -11
- package/tests/pagination-enhanced.test.js +66 -15
- package/tests/post-commit-hooks.test.js +120 -0
- package/tests/public-exports.test.js +15 -0
- package/tests/put-create-parity.test.js +216 -0
- package/tests/relationship-endpoints.test.js +259 -0
- package/tests/return-record-settings.test.js +184 -0
- package/tests/row-policy.test.js +433 -0
- package/tests/s3-storage.test.js +38 -0
- package/tests/searchschema-merge.test.js +17 -0
- package/tests/socketio.test.js +62 -0
- package/tests/storage-mapping.test.js +56 -6
- package/tests/test-development-guide.md +5 -5
package/README.md
CHANGED
|
@@ -9,7 +9,8 @@ A powerful REST API plugin for [hooked-api](https://github.com/mobily-enterprise
|
|
|
9
9
|
* **JSON:API Compliant** - Full support for the JSON:API specification
|
|
10
10
|
* **Relationship Support** - `belongsTo`, `hasMany`, and many-to-many relationships, including polymorphic
|
|
11
11
|
* **Advanced Querying** - Filtering, sorting, pagination, and field selection (sparse fieldsets)
|
|
12
|
-
* **
|
|
12
|
+
* **Server-Side Row Policies** - Apply mandatory visibility rules before pagination, counts, and relationship loading
|
|
13
|
+
* **File Uploads** - Built-in support for file handling with local storage and a mock S3-style demo adapter
|
|
13
14
|
* **Framework Agnostic** - Works with raw Node.js HTTP, Express, and other Node.js frameworks via flexible connectors
|
|
14
15
|
* **Validation** - Schema-based validation with detailed error messages and custom rules
|
|
15
16
|
* **Simplified Mode** - A developer-friendly option to work with plain JavaScript objects instead of verbose JSON:API structure
|
|
@@ -72,4 +73,25 @@ app.listen(3000, () => {
|
|
|
72
73
|
- [Quick Start](QUICKSTART.md) - Quick start
|
|
73
74
|
- [Complete guide](GUIDE/) - Comprehensive guide
|
|
74
75
|
- [API Reference](API.md) - API reference
|
|
75
|
-
- [Why json-rest-api?](docs/COMPARISON.md) - Step-by-step tutorial
|
|
76
|
+
- [Why json-rest-api?](docs/COMPARISON.md) - Step-by-step tutorial
|
|
77
|
+
|
|
78
|
+
## Development
|
|
79
|
+
|
|
80
|
+
```bash
|
|
81
|
+
# Run the normalizeId contract suites under the default backend and AnyAPI,
|
|
82
|
+
# then rebuild the checked-in docs site
|
|
83
|
+
npm run verify
|
|
84
|
+
|
|
85
|
+
# Run lint separately
|
|
86
|
+
npm run lint
|
|
87
|
+
|
|
88
|
+
# Run the focused normalizeId suites directly
|
|
89
|
+
npm run test:id-contracts
|
|
90
|
+
npm run test:id-contracts:anyapi
|
|
91
|
+
|
|
92
|
+
# Rebuild the checked-in docs site
|
|
93
|
+
npm run docs
|
|
94
|
+
|
|
95
|
+
# Serve the docs locally with live rebuild
|
|
96
|
+
npm run docs:dev
|
|
97
|
+
```
|
package/docs/API.md
CHANGED
|
@@ -14,6 +14,8 @@ This reference provides comprehensive documentation for all methods, parameters,
|
|
|
14
14
|
|
|
15
15
|
4. **All parameters must be passed within a single params object** as the first argument to each method.
|
|
16
16
|
|
|
17
|
+
5. **Simplified shorthand is payload-only**: when you pass attributes directly (for example, `post({ title: 'Hello' })`), that plain object is treated as the resource payload. If you also need per-call options such as `transaction`, `queryParams`, `simplified`, or `returnFullRecord`, put the record under `inputRecord` and keep those options at the top level of `params`.
|
|
18
|
+
|
|
17
19
|
## Table of Contents
|
|
18
20
|
|
|
19
21
|
1. [Core API Methods](#core-api-methods)
|
|
@@ -291,6 +293,8 @@ const result = await api.resources.[resourceType].get(params, context)
|
|
|
291
293
|
| `simplified` | Boolean | No | Override simplified mode setting (default: true) |
|
|
292
294
|
| `transaction` | Object | No | Database transaction object |
|
|
293
295
|
|
|
296
|
+
`id` is normalized with the effective `normalizeId` function before validation and lookup. If it normalizes to an empty value, the operation fails as `REST_API_RESOURCE` with subtype `not_found`.
|
|
297
|
+
|
|
294
298
|
#### Return Value
|
|
295
299
|
|
|
296
300
|
**JSON:API Mode (simplified: false):**
|
|
@@ -423,21 +427,25 @@ const result = await api.resources.[resourceType].post(params, context)
|
|
|
423
427
|
},
|
|
424
428
|
simplified: Boolean, // Override simplified mode (default: true for API)
|
|
425
429
|
transaction: Object, // Database transaction object
|
|
426
|
-
returnFullRecord: String // Override return setting ('no', 'minimal', 'full')
|
|
430
|
+
returnFullRecord: String|Boolean // Override return setting ('no', 'minimal', 'full')
|
|
427
431
|
}
|
|
428
432
|
```
|
|
429
433
|
|
|
430
434
|
| Parameter | Type | Required | Description |
|
|
431
435
|
|-----------|------|----------|-------------|
|
|
432
436
|
| `inputRecord` | Object | Yes | Resource data to create |
|
|
437
|
+
| `inputRecord.id` | String | No (simplified) | Optional explicit logical resource ID |
|
|
433
438
|
| `inputRecord.data` | Object | Yes (JSON:API) | Resource data container |
|
|
439
|
+
| `inputRecord.data.id` | String | No (JSON:API) | Optional explicit logical resource ID |
|
|
434
440
|
| `inputRecord.data.type` | String | Yes (JSON:API) | Resource type |
|
|
435
441
|
| `inputRecord.data.attributes` | Object | Yes (JSON:API) | Resource attributes |
|
|
436
442
|
| `inputRecord.data.relationships` | Object | No | Related resources |
|
|
437
443
|
| `queryParams` | Object | No | For includes/fields in response |
|
|
438
444
|
| `simplified` | Boolean | No | Override simplified mode (default: true) |
|
|
439
445
|
| `transaction` | Object | No | Database transaction object |
|
|
440
|
-
| `returnFullRecord` | String | No | Override return setting ('no'
|
|
446
|
+
| `returnFullRecord` | String\|Boolean | No | Override return setting (`'no'`, `'minimal'`, `'full'`; legacy `true` = `'full'`, `false` = `'no'`) |
|
|
447
|
+
|
|
448
|
+
When an explicit resource id is provided, `normalizeId` runs before persistence and before any follow-up record fetch used for the return payload. If the normalized id is empty, the request fails as `REST_API_VALIDATION` on `data.id`.
|
|
441
449
|
|
|
442
450
|
#### Return Value Behavior
|
|
443
451
|
|
|
@@ -452,6 +460,8 @@ The return value depends on `returnFullRecord` setting and whether it's an API o
|
|
|
452
460
|
- `'minimal'`: Returns resource with ID only
|
|
453
461
|
- `'full'`: Returns complete resource with all fields
|
|
454
462
|
|
|
463
|
+
Boolean values are accepted for backward compatibility: `true` is normalized to `'full'` and `false` is normalized to `'no'`. New code should use the string values.
|
|
464
|
+
|
|
455
465
|
#### HTTP Equivalent
|
|
456
466
|
|
|
457
467
|
```http
|
|
@@ -515,6 +525,25 @@ const result = await api.resources.articles.post({
|
|
|
515
525
|
});
|
|
516
526
|
```
|
|
517
527
|
|
|
528
|
+
**Create with Explicit ID and Normalization:**
|
|
529
|
+
```javascript
|
|
530
|
+
// With normalizeId configured to trim and uppercase ids,
|
|
531
|
+
// this record is stored as ARTICLE-42
|
|
532
|
+
const result = await api.resources.articles.post({
|
|
533
|
+
inputRecord: {
|
|
534
|
+
data: {
|
|
535
|
+
type: 'articles',
|
|
536
|
+
id: ' article-42 ',
|
|
537
|
+
attributes: {
|
|
538
|
+
title: 'Canonical ID Example',
|
|
539
|
+
content: 'Stored under the normalized id'
|
|
540
|
+
}
|
|
541
|
+
}
|
|
542
|
+
},
|
|
543
|
+
simplified: false
|
|
544
|
+
});
|
|
545
|
+
```
|
|
546
|
+
|
|
518
547
|
**Create with Multiple Relationships:**
|
|
519
548
|
```javascript
|
|
520
549
|
// Create article with author and tags (simplified)
|
|
@@ -584,22 +613,26 @@ const result = await api.resources.[resourceType].put(params, context)
|
|
|
584
613
|
},
|
|
585
614
|
simplified: Boolean, // Override simplified mode (default: true for API)
|
|
586
615
|
transaction: Object, // Database transaction object
|
|
587
|
-
returnFullRecord: String // Override return setting ('no', 'minimal', 'full')
|
|
616
|
+
returnFullRecord: String|Boolean // Override return setting ('no', 'minimal', 'full')
|
|
588
617
|
}
|
|
589
618
|
```
|
|
590
619
|
|
|
591
620
|
| Parameter | Type | Required | Description |
|
|
592
621
|
|-----------|------|----------|-------------|
|
|
593
622
|
| `inputRecord` | Object | Yes | Complete resource data |
|
|
594
|
-
| `inputRecord.id` | String | Yes (simplified) |
|
|
595
|
-
| `inputRecord.data.id` | String | Yes (JSON:API) |
|
|
623
|
+
| `inputRecord.id` | String | Yes (simplified) | Logical resource ID |
|
|
624
|
+
| `inputRecord.data.id` | String | Yes (JSON:API) | Logical resource ID |
|
|
596
625
|
| `inputRecord.data.type` | String | Yes (JSON:API) | Resource type |
|
|
597
626
|
| `inputRecord.data.attributes` | Object | Yes (JSON:API) | All resource attributes |
|
|
598
627
|
| `inputRecord.data.relationships` | Object | No | All relationships (missing ones are nulled) |
|
|
599
628
|
| `queryParams` | Object | No | For response formatting |
|
|
600
629
|
| `simplified` | Boolean | No | Override simplified mode (default: true) |
|
|
601
630
|
| `transaction` | Object | No | Database transaction object |
|
|
602
|
-
| `returnFullRecord` | String | No | Override return setting |
|
|
631
|
+
| `returnFullRecord` | String\|Boolean | No | Override return setting (`'no'`, `'minimal'`, `'full'`; legacy `true` = `'full'`, `false` = `'no'`) |
|
|
632
|
+
|
|
633
|
+
`inputRecord.id` and `inputRecord.data.id` always refer to the logical resource id. `idProperty` and storage mapping affect the backing column name, not the API field name, and the resource id is not part of `attributes`.
|
|
634
|
+
|
|
635
|
+
If both the URL id and body id are present, both are normalized before the equality check. If either id normalizes to an empty value, the operation fails before storage is touched.
|
|
603
636
|
|
|
604
637
|
#### Return Value
|
|
605
638
|
|
|
@@ -711,22 +744,24 @@ const result = await api.resources.[resourceType].patch(params, context)
|
|
|
711
744
|
},
|
|
712
745
|
simplified: Boolean, // Override simplified mode (default: true for API)
|
|
713
746
|
transaction: Object, // Database transaction object
|
|
714
|
-
returnFullRecord: String // Override return setting ('no', 'minimal', 'full')
|
|
747
|
+
returnFullRecord: String|Boolean // Override return setting ('no', 'minimal', 'full')
|
|
715
748
|
}
|
|
716
749
|
```
|
|
717
750
|
|
|
718
751
|
| Parameter | Type | Required | Description |
|
|
719
752
|
|-----------|------|----------|-------------|
|
|
720
753
|
| `inputRecord` | Object | Yes | Partial resource data |
|
|
721
|
-
| `inputRecord.id` | String | Yes (simplified) |
|
|
722
|
-
| `inputRecord.data.id` | String | Yes (JSON:API) |
|
|
754
|
+
| `inputRecord.id` | String | Yes (simplified) | Logical resource ID |
|
|
755
|
+
| `inputRecord.data.id` | String | Yes (JSON:API) | Logical resource ID |
|
|
723
756
|
| `inputRecord.data.type` | String | Yes (JSON:API) | Resource type |
|
|
724
757
|
| `inputRecord.data.attributes` | Object | No | Attributes to update |
|
|
725
758
|
| `inputRecord.data.relationships` | Object | No | Relationships to update |
|
|
726
759
|
| `queryParams` | Object | No | For response formatting |
|
|
727
760
|
| `simplified` | Boolean | No | Override simplified mode (default: true) |
|
|
728
761
|
| `transaction` | Object | No | Database transaction object |
|
|
729
|
-
| `returnFullRecord` | String | No | Override return setting |
|
|
762
|
+
| `returnFullRecord` | String\|Boolean | No | Override return setting (`'no'`, `'minimal'`, `'full'`; legacy `true` = `'full'`, `false` = `'no'`) |
|
|
763
|
+
|
|
764
|
+
If both the URL id and body id are present, both are normalized before the equality check. If either id normalizes to an empty value, the operation fails before storage is touched.
|
|
730
765
|
|
|
731
766
|
#### Return Value
|
|
732
767
|
|
|
@@ -849,6 +884,8 @@ const result = await api.resources.[resourceType].delete(params, context)
|
|
|
849
884
|
| `id` | String\|Number | Yes | ID of resource to delete |
|
|
850
885
|
| `transaction` | Object | No | Database transaction object |
|
|
851
886
|
|
|
887
|
+
`id` is normalized with the effective `normalizeId` function before validation and lookup. If it normalizes to an empty value, the operation fails as `REST_API_RESOURCE` with subtype `not_found`.
|
|
888
|
+
|
|
852
889
|
#### Return Value
|
|
853
890
|
|
|
854
891
|
Returns `undefined` (204 No Content)
|
|
@@ -935,6 +972,8 @@ const result = await api.resources.[resourceType].getRelated(params, context)
|
|
|
935
972
|
| `queryParams` | Object | No | Standard query parameters for related resources |
|
|
936
973
|
| `transaction` | Object | No | Database transaction object |
|
|
937
974
|
|
|
975
|
+
`id` is normalized with the effective `normalizeId` function before validation and lookup. If it normalizes to an empty value, the operation fails as `REST_API_RESOURCE` with subtype `not_found`.
|
|
976
|
+
|
|
938
977
|
#### Return Value
|
|
939
978
|
|
|
940
979
|
JSON:API response with related resources (supports all query features like filtering, pagination, etc.)
|
|
@@ -1007,6 +1046,8 @@ const result = await api.resources.[resourceType].getRelationship(params, contex
|
|
|
1007
1046
|
}
|
|
1008
1047
|
```
|
|
1009
1048
|
|
|
1049
|
+
`id` is normalized with the effective `normalizeId` function before validation and lookup. If it normalizes to an empty value, the operation fails as `REST_API_RESOURCE` with subtype `not_found`.
|
|
1050
|
+
|
|
1010
1051
|
#### Return Value
|
|
1011
1052
|
|
|
1012
1053
|
JSON:API relationship document with resource identifiers only
|
|
@@ -1081,6 +1122,8 @@ const result = await api.resources.[resourceType].postRelationship(params, conte
|
|
|
1081
1122
|
}
|
|
1082
1123
|
```
|
|
1083
1124
|
|
|
1125
|
+
The parent `id` is normalized with the parent resource's normalizer. Each `relationshipData[*].id` is normalized with the related target resource's normalizer before validation and persistence.
|
|
1126
|
+
|
|
1084
1127
|
#### Return Value
|
|
1085
1128
|
|
|
1086
1129
|
Returns `undefined` (204 No Content)
|
|
@@ -1116,6 +1159,8 @@ await api.resources.articles.postRelationship({
|
|
|
1116
1159
|
// Existing tags remain, new tags are added
|
|
1117
1160
|
```
|
|
1118
1161
|
|
|
1162
|
+
If `normalizeId` is configured, both the parent id and each relationship identifier are normalized before the relationship is written.
|
|
1163
|
+
|
|
1119
1164
|
---
|
|
1120
1165
|
|
|
1121
1166
|
### patchRelationship - Replace Relationship
|
|
@@ -1138,6 +1183,8 @@ const result = await api.resources.[resourceType].patchRelationship(params, cont
|
|
|
1138
1183
|
}
|
|
1139
1184
|
```
|
|
1140
1185
|
|
|
1186
|
+
The parent `id` is normalized with the parent resource's normalizer. Each relationship identifier inside `relationshipData` is normalized with the related target resource's normalizer before validation and persistence.
|
|
1187
|
+
|
|
1141
1188
|
#### Return Value
|
|
1142
1189
|
|
|
1143
1190
|
Returns `undefined` (204 No Content)
|
|
@@ -1219,6 +1266,8 @@ const result = await api.resources.[resourceType].deleteRelationship(params, con
|
|
|
1219
1266
|
}
|
|
1220
1267
|
```
|
|
1221
1268
|
|
|
1269
|
+
The parent `id` is normalized with the parent resource's normalizer. Each `relationshipData[*].id` is normalized with the related target resource's normalizer before validation and persistence.
|
|
1270
|
+
|
|
1222
1271
|
#### Return Value
|
|
1223
1272
|
|
|
1224
1273
|
Returns `undefined` (204 No Content)
|
|
@@ -1268,7 +1317,7 @@ Each API method has its own specific hook execution order. Here's the exact sequ
|
|
|
1268
1317
|
```
|
|
1269
1318
|
1. beforeData
|
|
1270
1319
|
2. beforeDataQuery
|
|
1271
|
-
3. knexQueryFiltering (multiple
|
|
1320
|
+
3. knexQueryFiltering (multiple handlers for built-in filters and optional server-side policies)
|
|
1272
1321
|
- polymorphicFiltersHook
|
|
1273
1322
|
- crossTableFiltersHook
|
|
1274
1323
|
- basicFiltersHook
|
|
@@ -1403,7 +1452,7 @@ Each API method has its own specific hook execution order. Here's the exact sequ
|
|
|
1403
1452
|
1. **Processing Hooks**: Only POST, PUT, and PATCH have `beforeProcessing` hooks
|
|
1404
1453
|
2. **Schema Validation**: Only POST, PUT, and PATCH have schema validation hooks
|
|
1405
1454
|
3. **Permission Hooks**: GET uses `checkDataPermissions`, while relationship methods use `checkPermissions`
|
|
1406
|
-
4. **Query Filtering**:
|
|
1455
|
+
4. **Query Filtering**: Collection queries and storage-backed scoped lookups trigger `knexQueryFiltering`; use `RowPolicyPlugin` for mandatory visibility rather than relying on method-specific hook assumptions
|
|
1407
1456
|
5. **Enrichment**: Only GET and QUERY have `enrichRecord` hooks
|
|
1408
1457
|
6. **Relationships**: Only GET has `enrichRecordWithRelationships`
|
|
1409
1458
|
7. **Transactions**: All write methods (POST, PUT, PATCH, DELETE) can trigger `afterCommit`/`afterRollback`
|
|
@@ -2075,24 +2124,30 @@ api.resource('articles').hook('afterDataQuery', async (context) => {
|
|
|
2075
2124
|
### Plugin Configuration
|
|
2076
2125
|
|
|
2077
2126
|
```javascript
|
|
2078
|
-
|
|
2127
|
+
await api.use(RestApiPlugin, {
|
|
2079
2128
|
// API behavior
|
|
2080
2129
|
simplifiedApi: true, // Use simplified mode for programmatic calls (default: true)
|
|
2081
2130
|
simplifiedTransport: false, // Use JSON:API for HTTP transport (default: false)
|
|
2131
|
+
normalizeId: (value) => {
|
|
2132
|
+
if (value === null || value === undefined) {
|
|
2133
|
+
return null
|
|
2134
|
+
}
|
|
2135
|
+
|
|
2136
|
+
const normalized = String(value).trim()
|
|
2137
|
+
return normalized ? normalized.toUpperCase() : null
|
|
2138
|
+
},
|
|
2082
2139
|
|
|
2083
2140
|
// Return record configuration
|
|
2084
2141
|
returnRecordApi: {
|
|
2085
2142
|
post: 'full', // Return full record after create (default)
|
|
2086
2143
|
put: 'full', // Return full record after replace (default)
|
|
2087
|
-
patch: 'full'
|
|
2088
|
-
delete: 'no' // Return nothing after delete (default)
|
|
2144
|
+
patch: 'full' // Return full record after update (default)
|
|
2089
2145
|
},
|
|
2090
2146
|
|
|
2091
2147
|
returnRecordTransport: {
|
|
2092
2148
|
post: 'no', // Return 204 for HTTP POST (default)
|
|
2093
2149
|
put: 'no', // Return 204 for HTTP PUT (default)
|
|
2094
|
-
patch: 'no'
|
|
2095
|
-
delete: 'no' // Return 204 for HTTP DELETE (default)
|
|
2150
|
+
patch: 'no' // Return 204 for HTTP PATCH (default)
|
|
2096
2151
|
},
|
|
2097
2152
|
|
|
2098
2153
|
// Query limits
|
|
@@ -2103,15 +2158,7 @@ const restApiPlugin = new RestApiPlugin({
|
|
|
2103
2158
|
includeDepthLimit: 3, // Maximum relationship nesting depth
|
|
2104
2159
|
|
|
2105
2160
|
// Performance
|
|
2106
|
-
enablePaginationCounts: true
|
|
2107
|
-
|
|
2108
|
-
// Error handling
|
|
2109
|
-
exposeErrors: false, // Include error details in responses
|
|
2110
|
-
|
|
2111
|
-
// Custom serializers
|
|
2112
|
-
serializers: {
|
|
2113
|
-
articles: customArticleSerializer
|
|
2114
|
-
}
|
|
2161
|
+
enablePaginationCounts: true // Execute count queries for total pages
|
|
2115
2162
|
});
|
|
2116
2163
|
```
|
|
2117
2164
|
|
|
@@ -2126,7 +2173,7 @@ api.addResource({
|
|
|
2126
2173
|
name: 'articles',
|
|
2127
2174
|
|
|
2128
2175
|
// Primary key configuration
|
|
2129
|
-
idProperty: 'id', //
|
|
2176
|
+
idProperty: 'id', // Physical primary-key column name for table-backed resources
|
|
2130
2177
|
|
|
2131
2178
|
schema: {
|
|
2132
2179
|
// Attributes
|
|
@@ -2247,7 +2294,8 @@ api.addResource({
|
|
|
2247
2294
|
|
|
2248
2295
|
// No built-in permissions option exists here.
|
|
2249
2296
|
// Use permission hooks for access decisions, and optional plugins
|
|
2250
|
-
// such as AutoFilterPlugin for dataset scoping
|
|
2297
|
+
// such as AutoFilterPlugin for equality-based dataset scoping or
|
|
2298
|
+
// RowPolicyPlugin for mandatory SQL visibility predicates.
|
|
2251
2299
|
|
|
2252
2300
|
// Soft delete configuration
|
|
2253
2301
|
softDelete: {
|
|
@@ -2273,19 +2321,95 @@ api.addResource({
|
|
|
2273
2321
|
|
|
2274
2322
|
#### ID Property Configuration
|
|
2275
2323
|
```javascript
|
|
2276
|
-
//
|
|
2324
|
+
// Physical primary key column: user_id
|
|
2277
2325
|
api.addResource({
|
|
2278
2326
|
name: 'users',
|
|
2279
|
-
idProperty: 'user_id',
|
|
2327
|
+
idProperty: 'user_id',
|
|
2280
2328
|
schema: {
|
|
2281
|
-
|
|
2282
|
-
|
|
2283
|
-
|
|
2329
|
+
id: { type: 'id', required: true, storage: { column: 'user_id' } },
|
|
2330
|
+
email: { type: 'string', required: true },
|
|
2331
|
+
displayName: { type: 'string' }
|
|
2332
|
+
}
|
|
2333
|
+
});
|
|
2334
|
+
```
|
|
2335
|
+
|
|
2336
|
+
`idProperty` names the physical primary-key column for table-backed resources. At the API layer, the resource id remains `id`, so writes use `inputRecord.id` or `inputRecord.data.id`, reads return `id` or `data.id`, and the resource id is not part of `attributes`.
|
|
2337
|
+
|
|
2338
|
+
#### Resource ID Normalization
|
|
2339
|
+
|
|
2340
|
+
`normalizeId` canonicalizes resource identifiers before the library reads, writes, validates, or links records.
|
|
2341
|
+
|
|
2342
|
+
The default normalizer:
|
|
2343
|
+
|
|
2344
|
+
- trims surrounding whitespace from strings
|
|
2345
|
+
- converts finite numbers and bigints to strings
|
|
2346
|
+
- rejects values that normalize to an empty identifier
|
|
2347
|
+
|
|
2348
|
+
Provide a custom normalizer when your ids need additional canonicalization:
|
|
2349
|
+
|
|
2350
|
+
```javascript
|
|
2351
|
+
await api.use(RestApiPlugin, {
|
|
2352
|
+
normalizeId: (value) => {
|
|
2353
|
+
if (value === null || value === undefined) {
|
|
2354
|
+
return null
|
|
2284
2355
|
}
|
|
2356
|
+
|
|
2357
|
+
const normalized = String(value).trim()
|
|
2358
|
+
return normalized ? normalized.toUpperCase() : null
|
|
2359
|
+
}
|
|
2360
|
+
});
|
|
2361
|
+
```
|
|
2362
|
+
|
|
2363
|
+
Resource-level overrides take precedence over the plugin-level default:
|
|
2364
|
+
|
|
2365
|
+
```javascript
|
|
2366
|
+
await api.addResource('countries', {
|
|
2367
|
+
normalizeId: (value) => {
|
|
2368
|
+
if (value === null || value === undefined) {
|
|
2369
|
+
return null
|
|
2370
|
+
}
|
|
2371
|
+
|
|
2372
|
+
const normalized = String(value).trim()
|
|
2373
|
+
return normalized ? normalized.toLowerCase() : null
|
|
2374
|
+
},
|
|
2375
|
+
schema: {
|
|
2376
|
+
name: { type: 'string', required: true }
|
|
2377
|
+
}
|
|
2378
|
+
});
|
|
2379
|
+
```
|
|
2380
|
+
|
|
2381
|
+
Normalization is applied to:
|
|
2382
|
+
|
|
2383
|
+
- parent ids used by `get`, `put`, `patch`, `delete`, `getRelated`, and `getRelationship`
|
|
2384
|
+
- explicit `POST`, `PUT`, and `PATCH` document ids
|
|
2385
|
+
- relationship identifiers used by `postRelationship`, `patchRelationship`, and `deleteRelationship`
|
|
2386
|
+
|
|
2387
|
+
Relationship identifiers use the normalizer of the related target resource, not the parent resource.
|
|
2388
|
+
|
|
2389
|
+
If an id normalizes to an empty value:
|
|
2390
|
+
|
|
2391
|
+
- existing-resource operations fail as `REST_API_RESOURCE` with subtype `not_found`
|
|
2392
|
+
- explicit `POST` document ids fail as `REST_API_VALIDATION` on `data.id`
|
|
2393
|
+
|
|
2394
|
+
#### Storage Column Naming
|
|
2395
|
+
|
|
2396
|
+
Table-backed resources use snake_case physical columns by default. For example, `displayName` maps to `display_name`.
|
|
2397
|
+
|
|
2398
|
+
Use `storage.column` when a field needs a specific column name:
|
|
2399
|
+
|
|
2400
|
+
```javascript
|
|
2401
|
+
api.addResource({
|
|
2402
|
+
name: 'profiles',
|
|
2403
|
+
schema: {
|
|
2404
|
+
id: { type: 'id' },
|
|
2405
|
+
displayName: { type: 'string', required: true },
|
|
2406
|
+
legacyRef: { type: 'string', storage: { column: 'legacy_profile_ref' } }
|
|
2285
2407
|
}
|
|
2286
2408
|
});
|
|
2287
2409
|
```
|
|
2288
2410
|
|
|
2411
|
+
If physical columns should match logical field names exactly for an entire resource, use `storage: { naming: 'exact' }`.
|
|
2412
|
+
|
|
2289
2413
|
#### Virtual Fields
|
|
2290
2414
|
Virtual fields are excluded from database operations but can be used for temporary UI state:
|
|
2291
2415
|
|
|
@@ -242,7 +242,7 @@ console.log('Refetched Country:', inspect(countryUsRefetched));
|
|
|
242
242
|
// }
|
|
243
243
|
```
|
|
244
244
|
|
|
245
|
-
(Note that the full JSON:API record includes links to resources, which are
|
|
245
|
+
(Note that the full JSON:API record includes links to resources, which are generated from explicit URL configuration such as the connector `mountPath` or `publicBaseUrl`.)
|
|
246
246
|
|
|
247
247
|
As you can see, when `simplified: false` is used:
|
|
248
248
|
|
|
@@ -325,6 +325,8 @@ const result = await api.resources.countries.post({
|
|
|
325
325
|
});
|
|
326
326
|
```
|
|
327
327
|
|
|
328
|
+
If you need per-call options such as `transaction`, `returnFullRecord`, or `queryParams`, move the record under `inputRecord` and keep those options at the top level of the params object.
|
|
329
|
+
|
|
328
330
|
By default, `simplifiedApi` is `true` for programmatic usage, making it easier to work with the API in your code while still maintaining full JSON:API compliance internally.
|
|
329
331
|
|
|
330
332
|
#### API usage and returning records
|
|
@@ -338,11 +340,13 @@ There are TWO separate settings for this:
|
|
|
338
340
|
|
|
339
341
|
This separation allows you to have different behaviors for internal API usage versus external HTTP clients. For example, your internal code might want full records for convenience, while HTTP clients might prefer minimal responses for performance.
|
|
340
342
|
|
|
341
|
-
Both settings
|
|
343
|
+
Both settings prefer three string values:
|
|
342
344
|
- **`'full'`**: Returns the complete record with all attributes, relationships, computed fields, and links
|
|
343
345
|
- **`'minimal'`**: Returns only the resource type and ID
|
|
344
346
|
- **`'no'`**: Returns nothing (undefined in programmatic calls, 204 No Content in HTTP)
|
|
345
347
|
|
|
348
|
+
Boolean values are accepted for backward compatibility: `true` is normalized to `'full'` and `false` is normalized to `'no'`. New code should use the string values.
|
|
349
|
+
|
|
346
350
|
Here's how these settings work:
|
|
347
351
|
|
|
348
352
|
```javascript
|
|
@@ -778,6 +782,7 @@ Here is a full list of parameters and their respective variables:
|
|
|
778
782
|
| `simplifiedApi` | `vars.simplifiedApi` | `true` | Use simplified format for programmatic API calls | ✓ |
|
|
779
783
|
| `simplifiedTransport` | `vars.simplifiedTransport` | `false` | Use simplified format for HTTP/REST endpoints | ✓ |
|
|
780
784
|
| `idProperty` | `vars.idProperty` | `'id'` | Name of the ID field in resources | ✓ |
|
|
785
|
+
| `normalizeId` | `vars.normalizeId` | trim strings, stringify finite numbers | Normalize resource identifiers before lookups and writes | ✓ |
|
|
781
786
|
| `returnRecordApi` | `vars.returnRecordApi` | `{ post: 'full', put: 'full', patch: 'full' }` | What to return for programmatic API write operations | ✓ |
|
|
782
787
|
| `returnRecordTransport` | `vars.returnRecordTransport` | `{ post: 'no', put: 'no', patch: 'no' }` | What to return for HTTP/REST write operations | ✓ |
|
|
783
788
|
|
|
@@ -793,11 +798,82 @@ Here is a full list of parameters and their respective variables:
|
|
|
793
798
|
- `returnRecordApi` and `returnRecordTransport` can be either:
|
|
794
799
|
- A string: `'no'`, `'minimal'`, or `'full'` (applies to all methods)
|
|
795
800
|
- An object: `{ post: 'full', put: 'minimal', patch: 'no' }` (per-method configuration)
|
|
801
|
+
- Legacy booleans: `true` is treated as `'full'`, `false` as `'no'`
|
|
796
802
|
- All parameters support the cascade: per-call → resource-level → plugin-level default
|
|
797
803
|
|
|
798
|
-
#
|
|
804
|
+
# Logical IDs and Physical Columns
|
|
805
|
+
|
|
806
|
+
For table-backed resources, `idProperty` defines the physical primary-key column. The API surface still uses the logical resource id.
|
|
807
|
+
|
|
808
|
+
- simplified writes use `inputRecord.id`
|
|
809
|
+
- JSON:API writes use `inputRecord.data.id`
|
|
810
|
+
- responses expose `id` or `data.id`
|
|
811
|
+
- the resource id is not part of `attributes`
|
|
812
|
+
|
|
813
|
+
```javascript
|
|
814
|
+
await api.addResource('profiles', {
|
|
815
|
+
idProperty: 'user_id',
|
|
816
|
+
schema: {
|
|
817
|
+
id: { type: 'id', required: true, storage: { column: 'user_id' } },
|
|
818
|
+
displayName: { type: 'string', required: true },
|
|
819
|
+
loginCount: { type: 'number', defaultTo: 0 }
|
|
820
|
+
},
|
|
821
|
+
tableName: 'profiles'
|
|
822
|
+
});
|
|
823
|
+
```
|
|
824
|
+
|
|
825
|
+
For table-backed resources, storage columns default to snake_case, so `displayName` maps to `display_name` and `loginCount` maps to `login_count`. Use `storage.column` for a per-field override, or `storage: { naming: 'exact' }` when physical column names should match logical field names exactly.
|
|
826
|
+
|
|
827
|
+
## ID normalization
|
|
828
|
+
|
|
829
|
+
`normalizeId` lets you canonicalize resource identifiers before the library reads, writes, validates, or links records.
|
|
830
|
+
|
|
831
|
+
The default normalizer already handles the common cases:
|
|
832
|
+
|
|
833
|
+
- trims surrounding whitespace from strings
|
|
834
|
+
- converts finite numbers and bigints to strings
|
|
835
|
+
- rejects values that normalize to an empty identifier
|
|
836
|
+
|
|
837
|
+
You only need to provide `normalizeId` when your resource ids need additional canonicalization, such as uppercasing, lowercasing, or removing formatting characters.
|
|
838
|
+
|
|
839
|
+
```javascript
|
|
840
|
+
await api.use(RestApiPlugin, {
|
|
841
|
+
normalizeId: (value) => {
|
|
842
|
+
if (value === null || value === undefined) return null
|
|
843
|
+
|
|
844
|
+
const normalized = String(value).trim()
|
|
845
|
+
return normalized ? normalized.toUpperCase() : null
|
|
846
|
+
}
|
|
847
|
+
})
|
|
848
|
+
|
|
849
|
+
await api.addResource('countries', {
|
|
850
|
+
// Override the plugin-wide normalizer for this one resource
|
|
851
|
+
normalizeId: (value) => {
|
|
852
|
+
if (value === null || value === undefined) return null
|
|
853
|
+
|
|
854
|
+
const normalized = String(value).trim()
|
|
855
|
+
return normalized ? normalized.toLowerCase() : null
|
|
856
|
+
},
|
|
857
|
+
schema: {
|
|
858
|
+
name: { type: 'string', required: true }
|
|
859
|
+
}
|
|
860
|
+
})
|
|
861
|
+
```
|
|
862
|
+
|
|
863
|
+
Normalization is applied consistently to:
|
|
864
|
+
|
|
865
|
+
- `get`, `put`, `patch`, `delete`, `getRelated`, and `getRelationship` parent ids
|
|
866
|
+
- explicit resource ids in `POST`, `PUT`, and `PATCH` documents
|
|
867
|
+
- relationship identifiers supplied to `postRelationship`, `patchRelationship`, and `deleteRelationship`
|
|
868
|
+
|
|
869
|
+
The resource-level `normalizeId` override also applies to relationship identifiers when that resource appears as the related target.
|
|
870
|
+
|
|
871
|
+
If an id normalizes to an empty value:
|
|
872
|
+
|
|
873
|
+
- existing-resource operations behave as `not_found`
|
|
874
|
+
- explicit `POST` document ids are rejected as validation errors on `data.id`
|
|
799
875
|
|
|
800
|
-
|
|
876
|
+
For full examples of table creation, physical column naming, and migration generation, see [Knex Schema and Migrations](GUIDE_X_Knex_Schema_And_Migrations.md).
|
|
801
877
|
|
|
802
878
|
# Helpers and Methods Provided by REST API Plugins
|
|
803
879
|
|
|
@@ -828,8 +904,14 @@ When you install the REST API plugins, the following helpers are added to `api.h
|
|
|
828
904
|
const trx = await api.helpers.newTransaction();
|
|
829
905
|
try {
|
|
830
906
|
// Use transaction in multiple operations
|
|
831
|
-
await api.resources.countries.post({
|
|
832
|
-
|
|
907
|
+
await api.resources.countries.post({
|
|
908
|
+
inputRecord: { name: 'France', code: 'FR' },
|
|
909
|
+
transaction: trx
|
|
910
|
+
});
|
|
911
|
+
await api.resources.publishers.post({
|
|
912
|
+
inputRecord: { name: 'French Press', country: 1 },
|
|
913
|
+
transaction: trx
|
|
914
|
+
});
|
|
833
915
|
await trx.commit();
|
|
834
916
|
} catch (error) {
|
|
835
917
|
await trx.rollback();
|
|
@@ -23,9 +23,9 @@ const knex = knexLib({
|
|
|
23
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);
|
|
27
27
|
await api.use(RestApiKnexPlugin, { knex });
|
|
28
|
-
await api.use(ExpressPlugin, {
|
|
28
|
+
await api.use(ExpressPlugin, { mountPath: '/api' }); // Links are generated under /api
|
|
29
29
|
|
|
30
30
|
/// *** ...programmatic calls here... ***
|
|
31
31
|
|
|
@@ -85,4 +85,4 @@ This design provides the best of both worlds: backend developers get full databa
|
|
|
85
85
|
|
|
86
86
|
---
|
|
87
87
|
|
|
88
|
-
[Back to Guide](./README.md) | [Next: 2.2 Manipulating and searching tables with no relationships](./GUIDE_2_2_Manipulating_And_Searching_Tables.md)
|
|
88
|
+
[Back to Guide](./README.md) | [Next: 2.2 Manipulating and searching tables with no relationships](./GUIDE_2_2_Manipulating_And_Searching_Tables.md)
|
|
@@ -132,6 +132,8 @@ await api.addResource('users', {
|
|
|
132
132
|
});
|
|
133
133
|
```
|
|
134
134
|
|
|
135
|
+
If a setter throws or rejects, the write fails with a validation error and the transaction rolls back instead of storing the untransformed value.
|
|
136
|
+
|
|
135
137
|
### Getters - Transform After Retrieval
|
|
136
138
|
|
|
137
139
|
```javascript
|
|
@@ -1186,7 +1186,7 @@ api.addScope('articles', {}, {
|
|
|
1186
1186
|
|
|
1187
1187
|
### knexQueryFiltering
|
|
1188
1188
|
|
|
1189
|
-
The `knexQueryFiltering` hook is called
|
|
1189
|
+
The `knexQueryFiltering` hook is called while storage-backed resource selections are being built. Collection queries run it before sorting and pagination; scoped lookup and relationship paths can also run it. This is a special hook that allows complex query modifications. Use the supported [Row Policies](GUIDE_X_Row_Policies.md) contract for mandatory resource visibility.
|
|
1190
1190
|
|
|
1191
1191
|
**When**: During `dataQuery` execution, before sorting and pagination
|
|
1192
1192
|
**Purpose**: Apply filters, add JOINs, modify query conditions
|