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.
Files changed (106) hide show
  1. package/README.md +24 -2
  2. package/docs/API.md +157 -33
  3. package/docs/GUIDE/GUIDE_1_Initial_Setup.md +88 -6
  4. package/docs/GUIDE/GUIDE_2_1_The_Starting_Point.md +3 -3
  5. package/docs/GUIDE/GUIDE_3_Field_Transformations.md +2 -0
  6. package/docs/GUIDE/GUIDE_7_Hooks_Data_Management_And_Plugins.md +1 -1
  7. package/docs/GUIDE/GUIDE_X_Autofiltering.md +63 -0
  8. package/docs/GUIDE/GUIDE_X_File_Uploads.md +17 -12
  9. package/docs/GUIDE/GUIDE_X_Knex_Schema_And_Migrations.md +122 -2
  10. package/docs/GUIDE/GUIDE_X_Row_Policies.md +603 -0
  11. package/docs/GUIDE/GUIDE_X_SocketIO.md +15 -10
  12. package/docs/GUIDE/GUIDE_X_URL_Management.md +93 -254
  13. package/docs/GUIDE/GUIDE_X_Writing_Plugins.md +2 -0
  14. package/docs/GUIDE/index.md +4 -1
  15. package/fixing_plan.md +112 -0
  16. package/index.js +2 -0
  17. package/lib/enhanced-logger.js +1 -1
  18. package/package.json +6 -2
  19. package/plugins/core/bulk-operations-plugin.js +31 -38
  20. package/plugins/core/connectors/express-plugin.js +6 -7
  21. package/plugins/core/connectors/fastify-plugin.js +5 -1
  22. package/plugins/core/connectors/lib/busboy-detector.js +4 -4
  23. package/plugins/core/connectors/lib/connector-core.js +17 -11
  24. package/plugins/core/connectors/lib/transport-route-schemas.js +12 -60
  25. package/plugins/core/file-handling-plugin.js +126 -96
  26. package/plugins/core/lib/anyapi/anyapi-registry.js +21 -3
  27. package/plugins/core/lib/anyapi/schema-utils.js +44 -4
  28. package/plugins/core/lib/anyapi/utils/descriptor-helpers.js +11 -2
  29. package/plugins/core/lib/dbIntrospection.js +1 -1
  30. package/plugins/core/lib/dbTablesOperations.js +10 -3
  31. package/plugins/core/lib/querying/knex-cross-table-search.js +0 -7
  32. package/plugins/core/lib/querying/knex-json-api-transformers-querying.js +20 -16
  33. package/plugins/core/lib/querying/knex-pagination-helpers.js +38 -84
  34. package/plugins/core/lib/querying/knex-query-helpers.js +5 -6
  35. package/plugins/core/lib/querying/knex-relationship-includes.js +40 -46
  36. package/plugins/core/lib/querying/knex-window-queries.js +10 -77
  37. package/plugins/core/lib/querying/url-helpers.js +9 -21
  38. package/plugins/core/lib/querying-writing/compile-schemas.js +10 -6
  39. package/plugins/core/lib/querying-writing/connectors-query-parser.js +51 -0
  40. package/plugins/core/lib/querying-writing/database-capabilities.js +1 -1
  41. package/plugins/core/lib/querying-writing/database-value-normalizers.js +18 -9
  42. package/plugins/core/lib/querying-writing/knex-field-helpers.js +7 -3
  43. package/plugins/core/lib/querying-writing/relationship-contracts.js +56 -0
  44. package/plugins/core/lib/querying-writing/request-contracts.js +53 -4
  45. package/plugins/core/lib/querying-writing/resource-id-normalization.js +131 -0
  46. package/plugins/core/lib/querying-writing/return-record-settings.js +47 -0
  47. package/plugins/core/lib/querying-writing/schema-helpers.js +34 -43
  48. package/plugins/core/lib/querying-writing/simplified-helpers.js +4 -11
  49. package/plugins/core/lib/storage/canonical-storage-mapping.js +24 -3
  50. package/plugins/core/lib/storage/storage-adapter.js +76 -14
  51. package/plugins/core/lib/storage/storage-mapping.js +86 -16
  52. package/plugins/core/lib/writing/many-to-many-manipulations.js +8 -5
  53. package/plugins/core/lib/writing/relationship-processor.js +10 -1
  54. package/plugins/core/rest-api-anyapi-knex-plugin.js +402 -197
  55. package/plugins/core/rest-api-knex-plugin.js +93 -76
  56. package/plugins/core/rest-api-plugin-hooks/turn-scope-init-into-vars.js +9 -2
  57. package/plugins/core/rest-api-plugin-methods/common.js +112 -72
  58. package/plugins/core/rest-api-plugin-methods/delete-relationship.js +34 -15
  59. package/plugins/core/rest-api-plugin-methods/delete.js +8 -7
  60. package/plugins/core/rest-api-plugin-methods/get-related.js +22 -15
  61. package/plugins/core/rest-api-plugin-methods/get-relationship.js +8 -3
  62. package/plugins/core/rest-api-plugin-methods/get.js +7 -1
  63. package/plugins/core/rest-api-plugin-methods/patch-relationship.js +26 -8
  64. package/plugins/core/rest-api-plugin-methods/patch.js +34 -9
  65. package/plugins/core/rest-api-plugin-methods/post-relationship.js +35 -16
  66. package/plugins/core/rest-api-plugin-methods/post.js +12 -7
  67. package/plugins/core/rest-api-plugin-methods/put.js +35 -10
  68. package/plugins/core/rest-api-plugin-methods/release.js +0 -2
  69. package/plugins/core/rest-api-plugin.js +11 -37
  70. package/plugins/core/rest-api-positioning-plugin.js +1 -4
  71. package/plugins/core/rest-api-row-policy-plugin.js +157 -0
  72. package/plugins/core/socketio-plugin.js +0 -1
  73. package/plugins/storage/local-storage.js +45 -20
  74. package/plugins/storage/s3-storage.js +23 -54
  75. package/quickTest.js +1 -0
  76. package/scripts/run-docs.js +80 -84
  77. package/test-debug.js +1 -1
  78. package/tests/README.md +8 -1
  79. package/tests/anyapi-basic.test.js +3 -0
  80. package/tests/anyapi-cursor-pagination.test.js +7 -1
  81. package/tests/anyapi-custom-idproperty.test.js +163 -0
  82. package/tests/custom-id-mysql-returning-fallback.test.js +128 -0
  83. package/tests/db-introspection.test.js +4 -1
  84. package/tests/db-table-operations.test.js +25 -20
  85. package/tests/fastify-plugin.test.js +49 -6
  86. package/tests/field-setters.test.js +10 -10
  87. package/tests/file-handling.test.js +118 -0
  88. package/tests/fixtures/api-configs.js +495 -4
  89. package/tests/full-jsonapi-belongsto-linkage.test.js +150 -0
  90. package/tests/http-connectors-parity.test.js +165 -5
  91. package/tests/id-normalization.test.js +444 -0
  92. package/tests/local-storage.test.js +96 -0
  93. package/tests/logical-id-write-validation.test.js +151 -0
  94. package/tests/pagination-cursor-multifield.test.js +132 -11
  95. package/tests/pagination-enhanced.test.js +66 -15
  96. package/tests/post-commit-hooks.test.js +120 -0
  97. package/tests/public-exports.test.js +15 -0
  98. package/tests/put-create-parity.test.js +216 -0
  99. package/tests/relationship-endpoints.test.js +259 -0
  100. package/tests/return-record-settings.test.js +184 -0
  101. package/tests/row-policy.test.js +433 -0
  102. package/tests/s3-storage.test.js +38 -0
  103. package/tests/searchschema-merge.test.js +17 -0
  104. package/tests/socketio.test.js +62 -0
  105. package/tests/storage-mapping.test.js +56 -6
  106. 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
- * **File Uploads** - Built-in support for file handling with multiple storage adapters (local, S3)
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', 'minimal', 'full') |
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) | Resource ID |
595
- | `inputRecord.data.id` | String | Yes (JSON:API) | Must match the resource ID |
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) | Resource ID |
722
- | `inputRecord.data.id` | String | Yes (JSON:API) | Resource ID |
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 times for different filter types)
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**: Only QUERY method triggers `knexQueryFiltering` hooks
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
- const restApiPlugin = new RestApiPlugin({
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', // Return full record after update (default)
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', // Return 204 for HTTP PATCH (default)
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, // Execute count queries for total pages
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', // Custom primary key field (default: '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
- // Custom primary key
2324
+ // Physical primary key column: user_id
2277
2325
  api.addResource({
2278
2326
  name: 'users',
2279
- idProperty: 'user_id', // Use 'user_id' instead of 'id'
2327
+ idProperty: 'user_id',
2280
2328
  schema: {
2281
- attributes: {
2282
- user_id: { type: 'number', required: true },
2283
- email: { type: 'string', required: true }
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 automatically generated based on the request headers.)
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 accept three string values:
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
- # Custom ID parameter
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
- TODO: Explain how idParam works, clarify that for the api it's always 'id' and there is no ID in the attributes
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({ name: 'France', code: 'FR' }, { transaction: trx });
832
- await api.resources.publishers.post({ name: 'French Press', country: 1 }, { transaction: trx });
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); // URLs auto-detected from request headers
26
+ await api.use(RestApiPlugin);
27
27
  await api.use(RestApiKnexPlugin, { knex });
28
- await api.use(ExpressPlugin, { mountPath: '/api' }); // Added: Express Plugin
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 during QUERY operations to apply filter conditions. This is a special hook that allows complex query modifications.
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