json-rest-api 1.0.23 → 1.0.24
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 +22 -1
- package/docs/API.md +145 -24
- package/docs/GUIDE/GUIDE_1_Initial_Setup.md +83 -4
- package/docs/GUIDE/GUIDE_X_Knex_Schema_And_Migrations.md +122 -2
- package/lib/enhanced-logger.js +1 -1
- package/package.json +6 -2
- package/plugins/core/bulk-operations-plugin.js +26 -34
- package/plugins/core/connectors/express-plugin.js +0 -6
- package/plugins/core/connectors/lib/busboy-detector.js +4 -4
- package/plugins/core/lib/anyapi/schema-utils.js +43 -4
- package/plugins/core/lib/anyapi/utils/descriptor-helpers.js +4 -1
- 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 +0 -1
- package/plugins/core/lib/querying/knex-query-helpers.js +5 -6
- package/plugins/core/lib/querying/knex-relationship-includes.js +11 -18
- package/plugins/core/lib/querying-writing/compile-schemas.js +10 -6
- package/plugins/core/lib/querying-writing/knex-field-helpers.js +1 -2
- package/plugins/core/lib/querying-writing/resource-id-normalization.js +131 -0
- package/plugins/core/lib/querying-writing/schema-helpers.js +0 -2
- package/plugins/core/lib/querying-writing/simplified-helpers.js +4 -11
- package/plugins/core/lib/storage/canonical-storage-mapping.js +22 -2
- package/plugins/core/lib/storage/storage-adapter.js +3 -2
- package/plugins/core/lib/storage/storage-mapping.js +86 -16
- package/plugins/core/lib/writing/many-to-many-manipulations.js +4 -2
- package/plugins/core/rest-api-anyapi-knex-plugin.js +194 -102
- package/plugins/core/rest-api-knex-plugin.js +23 -16
- package/plugins/core/rest-api-plugin-hooks/turn-scope-init-into-vars.js +1 -0
- package/plugins/core/rest-api-plugin-methods/common.js +27 -9
- package/plugins/core/rest-api-plugin-methods/delete-relationship.js +11 -2
- package/plugins/core/rest-api-plugin-methods/delete.js +6 -1
- package/plugins/core/rest-api-plugin-methods/get-related.js +7 -2
- package/plugins/core/rest-api-plugin-methods/get-relationship.js +7 -2
- package/plugins/core/rest-api-plugin-methods/get.js +7 -1
- package/plugins/core/rest-api-plugin-methods/patch-relationship.js +10 -3
- package/plugins/core/rest-api-plugin-methods/patch.js +31 -3
- package/plugins/core/rest-api-plugin-methods/post-relationship.js +11 -2
- package/plugins/core/rest-api-plugin-methods/post.js +8 -0
- package/plugins/core/rest-api-plugin-methods/put.js +31 -3
- package/plugins/core/rest-api-plugin-methods/release.js +0 -2
- package/plugins/core/rest-api-plugin.js +4 -0
- package/plugins/core/rest-api-positioning-plugin.js +1 -4
- package/plugins/core/socketio-plugin.js +0 -1
- package/plugins/storage/local-storage.js +1 -1
- package/plugins/storage/s3-storage.js +5 -2
- package/quickTest.js +1 -0
- package/scripts/run-docs.js +80 -84
- package/test-debug.js +1 -1
- package/tests/README.md +7 -0
- package/tests/anyapi-basic.test.js +3 -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 +21 -6
- package/tests/fixtures/api-configs.js +133 -0
- package/tests/id-normalization.test.js +444 -0
- package/tests/logical-id-write-validation.test.js +151 -0
- package/tests/pagination-cursor-multifield.test.js +11 -11
- package/tests/storage-mapping.test.js +55 -6
package/README.md
CHANGED
|
@@ -72,4 +72,25 @@ app.listen(3000, () => {
|
|
|
72
72
|
- [Quick Start](QUICKSTART.md) - Quick start
|
|
73
73
|
- [Complete guide](GUIDE/) - Comprehensive guide
|
|
74
74
|
- [API Reference](API.md) - API reference
|
|
75
|
-
- [Why json-rest-api?](docs/COMPARISON.md) - Step-by-step tutorial
|
|
75
|
+
- [Why json-rest-api?](docs/COMPARISON.md) - Step-by-step tutorial
|
|
76
|
+
|
|
77
|
+
## Development
|
|
78
|
+
|
|
79
|
+
```bash
|
|
80
|
+
# Run the normalizeId contract suites under the default backend and AnyAPI,
|
|
81
|
+
# then rebuild the checked-in docs site
|
|
82
|
+
npm run verify
|
|
83
|
+
|
|
84
|
+
# Run lint separately
|
|
85
|
+
npm run lint
|
|
86
|
+
|
|
87
|
+
# Run the focused normalizeId suites directly
|
|
88
|
+
npm run test:id-contracts
|
|
89
|
+
npm run test:id-contracts:anyapi
|
|
90
|
+
|
|
91
|
+
# Rebuild the checked-in docs site
|
|
92
|
+
npm run docs
|
|
93
|
+
|
|
94
|
+
# Serve the docs locally with live rebuild
|
|
95
|
+
npm run docs:dev
|
|
96
|
+
```
|
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):**
|
|
@@ -430,7 +434,9 @@ const result = await api.resources.[resourceType].post(params, context)
|
|
|
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 |
|
|
@@ -439,6 +445,8 @@ const result = await api.resources.[resourceType].post(params, context)
|
|
|
439
445
|
| `transaction` | Object | No | Database transaction object |
|
|
440
446
|
| `returnFullRecord` | String | No | Override return setting ('no', 'minimal', 'full') |
|
|
441
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`.
|
|
449
|
+
|
|
442
450
|
#### Return Value Behavior
|
|
443
451
|
|
|
444
452
|
The return value depends on `returnFullRecord` setting and whether it's an API or transport call:
|
|
@@ -515,6 +523,25 @@ const result = await api.resources.articles.post({
|
|
|
515
523
|
});
|
|
516
524
|
```
|
|
517
525
|
|
|
526
|
+
**Create with Explicit ID and Normalization:**
|
|
527
|
+
```javascript
|
|
528
|
+
// With normalizeId configured to trim and uppercase ids,
|
|
529
|
+
// this record is stored as ARTICLE-42
|
|
530
|
+
const result = await api.resources.articles.post({
|
|
531
|
+
inputRecord: {
|
|
532
|
+
data: {
|
|
533
|
+
type: 'articles',
|
|
534
|
+
id: ' article-42 ',
|
|
535
|
+
attributes: {
|
|
536
|
+
title: 'Canonical ID Example',
|
|
537
|
+
content: 'Stored under the normalized id'
|
|
538
|
+
}
|
|
539
|
+
}
|
|
540
|
+
},
|
|
541
|
+
simplified: false
|
|
542
|
+
});
|
|
543
|
+
```
|
|
544
|
+
|
|
518
545
|
**Create with Multiple Relationships:**
|
|
519
546
|
```javascript
|
|
520
547
|
// Create article with author and tags (simplified)
|
|
@@ -591,8 +618,8 @@ const result = await api.resources.[resourceType].put(params, context)
|
|
|
591
618
|
| Parameter | Type | Required | Description |
|
|
592
619
|
|-----------|------|----------|-------------|
|
|
593
620
|
| `inputRecord` | Object | Yes | Complete resource data |
|
|
594
|
-
| `inputRecord.id` | String | Yes (simplified) |
|
|
595
|
-
| `inputRecord.data.id` | String | Yes (JSON:API) |
|
|
621
|
+
| `inputRecord.id` | String | Yes (simplified) | Logical resource ID |
|
|
622
|
+
| `inputRecord.data.id` | String | Yes (JSON:API) | Logical resource ID |
|
|
596
623
|
| `inputRecord.data.type` | String | Yes (JSON:API) | Resource type |
|
|
597
624
|
| `inputRecord.data.attributes` | Object | Yes (JSON:API) | All resource attributes |
|
|
598
625
|
| `inputRecord.data.relationships` | Object | No | All relationships (missing ones are nulled) |
|
|
@@ -601,6 +628,10 @@ const result = await api.resources.[resourceType].put(params, context)
|
|
|
601
628
|
| `transaction` | Object | No | Database transaction object |
|
|
602
629
|
| `returnFullRecord` | String | No | Override return setting |
|
|
603
630
|
|
|
631
|
+
`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`.
|
|
632
|
+
|
|
633
|
+
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.
|
|
634
|
+
|
|
604
635
|
#### Return Value
|
|
605
636
|
|
|
606
637
|
Updated resource based on `returnFullRecord` setting (defaults: API='full', transport='no').
|
|
@@ -718,8 +749,8 @@ const result = await api.resources.[resourceType].patch(params, context)
|
|
|
718
749
|
| Parameter | Type | Required | Description |
|
|
719
750
|
|-----------|------|----------|-------------|
|
|
720
751
|
| `inputRecord` | Object | Yes | Partial resource data |
|
|
721
|
-
| `inputRecord.id` | String | Yes (simplified) |
|
|
722
|
-
| `inputRecord.data.id` | String | Yes (JSON:API) |
|
|
752
|
+
| `inputRecord.id` | String | Yes (simplified) | Logical resource ID |
|
|
753
|
+
| `inputRecord.data.id` | String | Yes (JSON:API) | Logical resource ID |
|
|
723
754
|
| `inputRecord.data.type` | String | Yes (JSON:API) | Resource type |
|
|
724
755
|
| `inputRecord.data.attributes` | Object | No | Attributes to update |
|
|
725
756
|
| `inputRecord.data.relationships` | Object | No | Relationships to update |
|
|
@@ -728,6 +759,8 @@ const result = await api.resources.[resourceType].patch(params, context)
|
|
|
728
759
|
| `transaction` | Object | No | Database transaction object |
|
|
729
760
|
| `returnFullRecord` | String | No | Override return setting |
|
|
730
761
|
|
|
762
|
+
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.
|
|
763
|
+
|
|
731
764
|
#### Return Value
|
|
732
765
|
|
|
733
766
|
Updated resource based on `returnFullRecord` setting (defaults: API='full', transport='no').
|
|
@@ -849,6 +882,8 @@ const result = await api.resources.[resourceType].delete(params, context)
|
|
|
849
882
|
| `id` | String\|Number | Yes | ID of resource to delete |
|
|
850
883
|
| `transaction` | Object | No | Database transaction object |
|
|
851
884
|
|
|
885
|
+
`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`.
|
|
886
|
+
|
|
852
887
|
#### Return Value
|
|
853
888
|
|
|
854
889
|
Returns `undefined` (204 No Content)
|
|
@@ -935,6 +970,8 @@ const result = await api.resources.[resourceType].getRelated(params, context)
|
|
|
935
970
|
| `queryParams` | Object | No | Standard query parameters for related resources |
|
|
936
971
|
| `transaction` | Object | No | Database transaction object |
|
|
937
972
|
|
|
973
|
+
`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`.
|
|
974
|
+
|
|
938
975
|
#### Return Value
|
|
939
976
|
|
|
940
977
|
JSON:API response with related resources (supports all query features like filtering, pagination, etc.)
|
|
@@ -1007,6 +1044,8 @@ const result = await api.resources.[resourceType].getRelationship(params, contex
|
|
|
1007
1044
|
}
|
|
1008
1045
|
```
|
|
1009
1046
|
|
|
1047
|
+
`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`.
|
|
1048
|
+
|
|
1010
1049
|
#### Return Value
|
|
1011
1050
|
|
|
1012
1051
|
JSON:API relationship document with resource identifiers only
|
|
@@ -1081,6 +1120,8 @@ const result = await api.resources.[resourceType].postRelationship(params, conte
|
|
|
1081
1120
|
}
|
|
1082
1121
|
```
|
|
1083
1122
|
|
|
1123
|
+
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.
|
|
1124
|
+
|
|
1084
1125
|
#### Return Value
|
|
1085
1126
|
|
|
1086
1127
|
Returns `undefined` (204 No Content)
|
|
@@ -1116,6 +1157,8 @@ await api.resources.articles.postRelationship({
|
|
|
1116
1157
|
// Existing tags remain, new tags are added
|
|
1117
1158
|
```
|
|
1118
1159
|
|
|
1160
|
+
If `normalizeId` is configured, both the parent id and each relationship identifier are normalized before the relationship is written.
|
|
1161
|
+
|
|
1119
1162
|
---
|
|
1120
1163
|
|
|
1121
1164
|
### patchRelationship - Replace Relationship
|
|
@@ -1138,6 +1181,8 @@ const result = await api.resources.[resourceType].patchRelationship(params, cont
|
|
|
1138
1181
|
}
|
|
1139
1182
|
```
|
|
1140
1183
|
|
|
1184
|
+
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.
|
|
1185
|
+
|
|
1141
1186
|
#### Return Value
|
|
1142
1187
|
|
|
1143
1188
|
Returns `undefined` (204 No Content)
|
|
@@ -1219,6 +1264,8 @@ const result = await api.resources.[resourceType].deleteRelationship(params, con
|
|
|
1219
1264
|
}
|
|
1220
1265
|
```
|
|
1221
1266
|
|
|
1267
|
+
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.
|
|
1268
|
+
|
|
1222
1269
|
#### Return Value
|
|
1223
1270
|
|
|
1224
1271
|
Returns `undefined` (204 No Content)
|
|
@@ -2075,24 +2122,30 @@ api.resource('articles').hook('afterDataQuery', async (context) => {
|
|
|
2075
2122
|
### Plugin Configuration
|
|
2076
2123
|
|
|
2077
2124
|
```javascript
|
|
2078
|
-
|
|
2125
|
+
await api.use(RestApiPlugin, {
|
|
2079
2126
|
// API behavior
|
|
2080
2127
|
simplifiedApi: true, // Use simplified mode for programmatic calls (default: true)
|
|
2081
2128
|
simplifiedTransport: false, // Use JSON:API for HTTP transport (default: false)
|
|
2129
|
+
normalizeId: (value) => {
|
|
2130
|
+
if (value === null || value === undefined) {
|
|
2131
|
+
return null
|
|
2132
|
+
}
|
|
2133
|
+
|
|
2134
|
+
const normalized = String(value).trim()
|
|
2135
|
+
return normalized ? normalized.toUpperCase() : null
|
|
2136
|
+
},
|
|
2082
2137
|
|
|
2083
2138
|
// Return record configuration
|
|
2084
2139
|
returnRecordApi: {
|
|
2085
2140
|
post: 'full', // Return full record after create (default)
|
|
2086
2141
|
put: 'full', // Return full record after replace (default)
|
|
2087
|
-
patch: 'full'
|
|
2088
|
-
delete: 'no' // Return nothing after delete (default)
|
|
2142
|
+
patch: 'full' // Return full record after update (default)
|
|
2089
2143
|
},
|
|
2090
2144
|
|
|
2091
2145
|
returnRecordTransport: {
|
|
2092
2146
|
post: 'no', // Return 204 for HTTP POST (default)
|
|
2093
2147
|
put: 'no', // Return 204 for HTTP PUT (default)
|
|
2094
|
-
patch: 'no'
|
|
2095
|
-
delete: 'no' // Return 204 for HTTP DELETE (default)
|
|
2148
|
+
patch: 'no' // Return 204 for HTTP PATCH (default)
|
|
2096
2149
|
},
|
|
2097
2150
|
|
|
2098
2151
|
// Query limits
|
|
@@ -2103,15 +2156,7 @@ const restApiPlugin = new RestApiPlugin({
|
|
|
2103
2156
|
includeDepthLimit: 3, // Maximum relationship nesting depth
|
|
2104
2157
|
|
|
2105
2158
|
// 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
|
-
}
|
|
2159
|
+
enablePaginationCounts: true // Execute count queries for total pages
|
|
2115
2160
|
});
|
|
2116
2161
|
```
|
|
2117
2162
|
|
|
@@ -2126,7 +2171,7 @@ api.addResource({
|
|
|
2126
2171
|
name: 'articles',
|
|
2127
2172
|
|
|
2128
2173
|
// Primary key configuration
|
|
2129
|
-
idProperty: 'id', //
|
|
2174
|
+
idProperty: 'id', // Physical primary-key column name for table-backed resources
|
|
2130
2175
|
|
|
2131
2176
|
schema: {
|
|
2132
2177
|
// Attributes
|
|
@@ -2273,19 +2318,95 @@ api.addResource({
|
|
|
2273
2318
|
|
|
2274
2319
|
#### ID Property Configuration
|
|
2275
2320
|
```javascript
|
|
2276
|
-
//
|
|
2321
|
+
// Physical primary key column: user_id
|
|
2277
2322
|
api.addResource({
|
|
2278
2323
|
name: 'users',
|
|
2279
|
-
idProperty: 'user_id',
|
|
2324
|
+
idProperty: 'user_id',
|
|
2280
2325
|
schema: {
|
|
2281
|
-
|
|
2282
|
-
|
|
2283
|
-
|
|
2326
|
+
id: { type: 'id', required: true, storage: { column: 'user_id' } },
|
|
2327
|
+
email: { type: 'string', required: true },
|
|
2328
|
+
displayName: { type: 'string' }
|
|
2329
|
+
}
|
|
2330
|
+
});
|
|
2331
|
+
```
|
|
2332
|
+
|
|
2333
|
+
`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`.
|
|
2334
|
+
|
|
2335
|
+
#### Resource ID Normalization
|
|
2336
|
+
|
|
2337
|
+
`normalizeId` canonicalizes resource identifiers before the library reads, writes, validates, or links records.
|
|
2338
|
+
|
|
2339
|
+
The default normalizer:
|
|
2340
|
+
|
|
2341
|
+
- trims surrounding whitespace from strings
|
|
2342
|
+
- converts finite numbers and bigints to strings
|
|
2343
|
+
- rejects values that normalize to an empty identifier
|
|
2344
|
+
|
|
2345
|
+
Provide a custom normalizer when your ids need additional canonicalization:
|
|
2346
|
+
|
|
2347
|
+
```javascript
|
|
2348
|
+
await api.use(RestApiPlugin, {
|
|
2349
|
+
normalizeId: (value) => {
|
|
2350
|
+
if (value === null || value === undefined) {
|
|
2351
|
+
return null
|
|
2284
2352
|
}
|
|
2353
|
+
|
|
2354
|
+
const normalized = String(value).trim()
|
|
2355
|
+
return normalized ? normalized.toUpperCase() : null
|
|
2356
|
+
}
|
|
2357
|
+
});
|
|
2358
|
+
```
|
|
2359
|
+
|
|
2360
|
+
Resource-level overrides take precedence over the plugin-level default:
|
|
2361
|
+
|
|
2362
|
+
```javascript
|
|
2363
|
+
await api.addResource('countries', {
|
|
2364
|
+
normalizeId: (value) => {
|
|
2365
|
+
if (value === null || value === undefined) {
|
|
2366
|
+
return null
|
|
2367
|
+
}
|
|
2368
|
+
|
|
2369
|
+
const normalized = String(value).trim()
|
|
2370
|
+
return normalized ? normalized.toLowerCase() : null
|
|
2371
|
+
},
|
|
2372
|
+
schema: {
|
|
2373
|
+
name: { type: 'string', required: true }
|
|
2374
|
+
}
|
|
2375
|
+
});
|
|
2376
|
+
```
|
|
2377
|
+
|
|
2378
|
+
Normalization is applied to:
|
|
2379
|
+
|
|
2380
|
+
- parent ids used by `get`, `put`, `patch`, `delete`, `getRelated`, and `getRelationship`
|
|
2381
|
+
- explicit `POST`, `PUT`, and `PATCH` document ids
|
|
2382
|
+
- relationship identifiers used by `postRelationship`, `patchRelationship`, and `deleteRelationship`
|
|
2383
|
+
|
|
2384
|
+
Relationship identifiers use the normalizer of the related target resource, not the parent resource.
|
|
2385
|
+
|
|
2386
|
+
If an id normalizes to an empty value:
|
|
2387
|
+
|
|
2388
|
+
- existing-resource operations fail as `REST_API_RESOURCE` with subtype `not_found`
|
|
2389
|
+
- explicit `POST` document ids fail as `REST_API_VALIDATION` on `data.id`
|
|
2390
|
+
|
|
2391
|
+
#### Storage Column Naming
|
|
2392
|
+
|
|
2393
|
+
Table-backed resources use snake_case physical columns by default. For example, `displayName` maps to `display_name`.
|
|
2394
|
+
|
|
2395
|
+
Use `storage.column` when a field needs a specific column name:
|
|
2396
|
+
|
|
2397
|
+
```javascript
|
|
2398
|
+
api.addResource({
|
|
2399
|
+
name: 'profiles',
|
|
2400
|
+
schema: {
|
|
2401
|
+
id: { type: 'id' },
|
|
2402
|
+
displayName: { type: 'string', required: true },
|
|
2403
|
+
legacyRef: { type: 'string', storage: { column: 'legacy_profile_ref' } }
|
|
2285
2404
|
}
|
|
2286
2405
|
});
|
|
2287
2406
|
```
|
|
2288
2407
|
|
|
2408
|
+
If physical columns should match logical field names exactly for an entire resource, use `storage: { naming: 'exact' }`.
|
|
2409
|
+
|
|
2289
2410
|
#### Virtual Fields
|
|
2290
2411
|
Virtual fields are excluded from database operations but can be used for temporary UI state:
|
|
2291
2412
|
|
|
@@ -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
|
|
@@ -778,6 +780,7 @@ Here is a full list of parameters and their respective variables:
|
|
|
778
780
|
| `simplifiedApi` | `vars.simplifiedApi` | `true` | Use simplified format for programmatic API calls | ✓ |
|
|
779
781
|
| `simplifiedTransport` | `vars.simplifiedTransport` | `false` | Use simplified format for HTTP/REST endpoints | ✓ |
|
|
780
782
|
| `idProperty` | `vars.idProperty` | `'id'` | Name of the ID field in resources | ✓ |
|
|
783
|
+
| `normalizeId` | `vars.normalizeId` | trim strings, stringify finite numbers | Normalize resource identifiers before lookups and writes | ✓ |
|
|
781
784
|
| `returnRecordApi` | `vars.returnRecordApi` | `{ post: 'full', put: 'full', patch: 'full' }` | What to return for programmatic API write operations | ✓ |
|
|
782
785
|
| `returnRecordTransport` | `vars.returnRecordTransport` | `{ post: 'no', put: 'no', patch: 'no' }` | What to return for HTTP/REST write operations | ✓ |
|
|
783
786
|
|
|
@@ -795,9 +798,79 @@ Here is a full list of parameters and their respective variables:
|
|
|
795
798
|
- An object: `{ post: 'full', put: 'minimal', patch: 'no' }` (per-method configuration)
|
|
796
799
|
- All parameters support the cascade: per-call → resource-level → plugin-level default
|
|
797
800
|
|
|
798
|
-
#
|
|
801
|
+
# Logical IDs and Physical Columns
|
|
802
|
+
|
|
803
|
+
For table-backed resources, `idProperty` defines the physical primary-key column. The API surface still uses the logical resource id.
|
|
804
|
+
|
|
805
|
+
- simplified writes use `inputRecord.id`
|
|
806
|
+
- JSON:API writes use `inputRecord.data.id`
|
|
807
|
+
- responses expose `id` or `data.id`
|
|
808
|
+
- the resource id is not part of `attributes`
|
|
809
|
+
|
|
810
|
+
```javascript
|
|
811
|
+
await api.addResource('profiles', {
|
|
812
|
+
idProperty: 'user_id',
|
|
813
|
+
schema: {
|
|
814
|
+
id: { type: 'id', required: true, storage: { column: 'user_id' } },
|
|
815
|
+
displayName: { type: 'string', required: true },
|
|
816
|
+
loginCount: { type: 'number', defaultTo: 0 }
|
|
817
|
+
},
|
|
818
|
+
tableName: 'profiles'
|
|
819
|
+
});
|
|
820
|
+
```
|
|
821
|
+
|
|
822
|
+
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.
|
|
823
|
+
|
|
824
|
+
## ID normalization
|
|
825
|
+
|
|
826
|
+
`normalizeId` lets you canonicalize resource identifiers before the library reads, writes, validates, or links records.
|
|
827
|
+
|
|
828
|
+
The default normalizer already handles the common cases:
|
|
829
|
+
|
|
830
|
+
- trims surrounding whitespace from strings
|
|
831
|
+
- converts finite numbers and bigints to strings
|
|
832
|
+
- rejects values that normalize to an empty identifier
|
|
833
|
+
|
|
834
|
+
You only need to provide `normalizeId` when your resource ids need additional canonicalization, such as uppercasing, lowercasing, or removing formatting characters.
|
|
835
|
+
|
|
836
|
+
```javascript
|
|
837
|
+
await api.use(RestApiPlugin, {
|
|
838
|
+
normalizeId: (value) => {
|
|
839
|
+
if (value === null || value === undefined) return null
|
|
840
|
+
|
|
841
|
+
const normalized = String(value).trim()
|
|
842
|
+
return normalized ? normalized.toUpperCase() : null
|
|
843
|
+
}
|
|
844
|
+
})
|
|
845
|
+
|
|
846
|
+
await api.addResource('countries', {
|
|
847
|
+
// Override the plugin-wide normalizer for this one resource
|
|
848
|
+
normalizeId: (value) => {
|
|
849
|
+
if (value === null || value === undefined) return null
|
|
850
|
+
|
|
851
|
+
const normalized = String(value).trim()
|
|
852
|
+
return normalized ? normalized.toLowerCase() : null
|
|
853
|
+
},
|
|
854
|
+
schema: {
|
|
855
|
+
name: { type: 'string', required: true }
|
|
856
|
+
}
|
|
857
|
+
})
|
|
858
|
+
```
|
|
859
|
+
|
|
860
|
+
Normalization is applied consistently to:
|
|
861
|
+
|
|
862
|
+
- `get`, `put`, `patch`, `delete`, `getRelated`, and `getRelationship` parent ids
|
|
863
|
+
- explicit resource ids in `POST`, `PUT`, and `PATCH` documents
|
|
864
|
+
- relationship identifiers supplied to `postRelationship`, `patchRelationship`, and `deleteRelationship`
|
|
865
|
+
|
|
866
|
+
The resource-level `normalizeId` override also applies to relationship identifiers when that resource appears as the related target.
|
|
867
|
+
|
|
868
|
+
If an id normalizes to an empty value:
|
|
869
|
+
|
|
870
|
+
- existing-resource operations behave as `not_found`
|
|
871
|
+
- explicit `POST` document ids are rejected as validation errors on `data.id`
|
|
799
872
|
|
|
800
|
-
|
|
873
|
+
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
874
|
|
|
802
875
|
# Helpers and Methods Provided by REST API Plugins
|
|
803
876
|
|
|
@@ -828,8 +901,14 @@ When you install the REST API plugins, the following helpers are added to `api.h
|
|
|
828
901
|
const trx = await api.helpers.newTransaction();
|
|
829
902
|
try {
|
|
830
903
|
// Use transaction in multiple operations
|
|
831
|
-
await api.resources.countries.post({
|
|
832
|
-
|
|
904
|
+
await api.resources.countries.post({
|
|
905
|
+
inputRecord: { name: 'France', code: 'FR' },
|
|
906
|
+
transaction: trx
|
|
907
|
+
});
|
|
908
|
+
await api.resources.publishers.post({
|
|
909
|
+
inputRecord: { name: 'French Press', country: 1 },
|
|
910
|
+
transaction: trx
|
|
911
|
+
});
|
|
833
912
|
await trx.commit();
|
|
834
913
|
} catch (error) {
|
|
835
914
|
await trx.rollback();
|
|
@@ -29,8 +29,8 @@ Example:
|
|
|
29
29
|
await api.addResource('memberships', {
|
|
30
30
|
schema: {
|
|
31
31
|
id: { type: 'id' },
|
|
32
|
-
workspaceId: { type: 'id', required: true
|
|
33
|
-
userId: { type: 'id', required: true
|
|
32
|
+
workspaceId: { type: 'id', required: true },
|
|
33
|
+
userId: { type: 'id', required: true },
|
|
34
34
|
role: { type: 'string', enum: ['owner', 'member'], required: true, defaultTo: 'member' }
|
|
35
35
|
},
|
|
36
36
|
|
|
@@ -64,6 +64,126 @@ await api.addResource('memberships', {
|
|
|
64
64
|
})
|
|
65
65
|
```
|
|
66
66
|
|
|
67
|
+
## Default Column Naming
|
|
68
|
+
|
|
69
|
+
Table-backed resources keep logical field names in the resource schema and map them to physical columns for Knex operations.
|
|
70
|
+
|
|
71
|
+
By default, physical columns use snake_case:
|
|
72
|
+
|
|
73
|
+
- `workspaceId` -> `workspace_id`
|
|
74
|
+
- `userId` -> `user_id`
|
|
75
|
+
- `createdAt` -> `created_at`
|
|
76
|
+
|
|
77
|
+
This keeps the API surface expressive without repeating `storage.column` on every camelCase field.
|
|
78
|
+
|
|
79
|
+
You only need `storage.column` when a field should use a non-standard column name:
|
|
80
|
+
|
|
81
|
+
```js
|
|
82
|
+
await api.addResource('profiles', {
|
|
83
|
+
schema: {
|
|
84
|
+
id: { type: 'id' },
|
|
85
|
+
displayName: { type: 'string', required: true },
|
|
86
|
+
legacyRef: { type: 'string', storage: { column: 'legacy_profile_ref' } }
|
|
87
|
+
},
|
|
88
|
+
tableName: 'profiles'
|
|
89
|
+
})
|
|
90
|
+
```
|
|
91
|
+
|
|
92
|
+
If you need physical columns to match the logical field names exactly for a whole resource, opt out explicitly:
|
|
93
|
+
|
|
94
|
+
```js
|
|
95
|
+
await api.addResource('verbatim_profiles', {
|
|
96
|
+
storage: { naming: 'exact' },
|
|
97
|
+
schema: {
|
|
98
|
+
id: { type: 'id' },
|
|
99
|
+
displayName: { type: 'string', required: true }
|
|
100
|
+
},
|
|
101
|
+
tableName: 'verbatim_profiles'
|
|
102
|
+
})
|
|
103
|
+
```
|
|
104
|
+
|
|
105
|
+
## Logical IDs and `idProperty`
|
|
106
|
+
|
|
107
|
+
`idProperty` names the physical primary-key column for table-backed resources. The API contract still uses the logical resource id.
|
|
108
|
+
|
|
109
|
+
```js
|
|
110
|
+
await api.addResource('profiles', {
|
|
111
|
+
idProperty: 'user_id',
|
|
112
|
+
schema: {
|
|
113
|
+
id: { type: 'id', required: true, storage: { column: 'user_id' } },
|
|
114
|
+
displayName: { type: 'string', required: true },
|
|
115
|
+
loginCount: { type: 'number', defaultTo: 0 }
|
|
116
|
+
},
|
|
117
|
+
tableName: 'profiles'
|
|
118
|
+
})
|
|
119
|
+
```
|
|
120
|
+
|
|
121
|
+
With this definition:
|
|
122
|
+
|
|
123
|
+
- writes send the resource id as `id` or `data.id`
|
|
124
|
+
- reads return the resource id as `id` or `data.id`
|
|
125
|
+
- the primary key is stored in `user_id`
|
|
126
|
+
- `displayName` and `loginCount` are stored in `display_name` and `login_count`
|
|
127
|
+
|
|
128
|
+
The resource id is not part of `attributes`:
|
|
129
|
+
|
|
130
|
+
```js
|
|
131
|
+
await api.resources.profiles.post({
|
|
132
|
+
inputRecord: {
|
|
133
|
+
data: {
|
|
134
|
+
type: 'profiles',
|
|
135
|
+
id: '42',
|
|
136
|
+
attributes: {
|
|
137
|
+
displayName: 'Mercury'
|
|
138
|
+
}
|
|
139
|
+
}
|
|
140
|
+
}
|
|
141
|
+
})
|
|
142
|
+
```
|
|
143
|
+
|
|
144
|
+
### ID normalization
|
|
145
|
+
|
|
146
|
+
If your primary keys need canonicalization beyond the default trim-and-stringify behavior, configure `normalizeId`.
|
|
147
|
+
|
|
148
|
+
```js
|
|
149
|
+
await api.use(RestApiPlugin, {
|
|
150
|
+
normalizeId: (value) => {
|
|
151
|
+
if (value === null || value === undefined) return null
|
|
152
|
+
|
|
153
|
+
const normalized = String(value).trim()
|
|
154
|
+
return normalized ? normalized.toUpperCase() : null
|
|
155
|
+
}
|
|
156
|
+
})
|
|
157
|
+
```
|
|
158
|
+
|
|
159
|
+
You can override that per resource:
|
|
160
|
+
|
|
161
|
+
```js
|
|
162
|
+
await api.addResource('profiles', {
|
|
163
|
+
idProperty: 'user_id',
|
|
164
|
+
normalizeId: (value) => {
|
|
165
|
+
if (value === null || value === undefined) return null
|
|
166
|
+
|
|
167
|
+
const normalized = String(value).trim()
|
|
168
|
+
return normalized ? normalized.toLowerCase() : null
|
|
169
|
+
},
|
|
170
|
+
schema: {
|
|
171
|
+
id: { type: 'id', required: true, storage: { column: 'user_id' } },
|
|
172
|
+
displayName: { type: 'string', required: true }
|
|
173
|
+
},
|
|
174
|
+
tableName: 'profiles'
|
|
175
|
+
})
|
|
176
|
+
```
|
|
177
|
+
|
|
178
|
+
For table-backed resources, `normalizeId` is applied before:
|
|
179
|
+
|
|
180
|
+
- reading a record by id
|
|
181
|
+
- replacing, patching, or deleting a record
|
|
182
|
+
- persisting an explicit `POST` resource id
|
|
183
|
+
- validating or writing relationship identifiers that point at the resource
|
|
184
|
+
|
|
185
|
+
If the normalizer returns an empty value, existing-resource operations fail as `not_found`, while explicit `POST` document ids fail validation on `data.id`.
|
|
186
|
+
|
|
67
187
|
## Create Tables
|
|
68
188
|
|
|
69
189
|
Create the table directly from the resource definition:
|
package/lib/enhanced-logger.js
CHANGED
|
@@ -3,7 +3,7 @@
|
|
|
3
3
|
* Wraps any logger to provide comprehensive error logging
|
|
4
4
|
*/
|
|
5
5
|
|
|
6
|
-
import { formatError, formatErrorSummary
|
|
6
|
+
import { formatError, formatErrorSummary } from './error-formatter.js'
|
|
7
7
|
|
|
8
8
|
/**
|
|
9
9
|
* Creates an enhanced logger that automatically formats errors
|
package/package.json
CHANGED
|
@@ -1,12 +1,16 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "json-rest-api",
|
|
3
|
-
"version": "1.0.
|
|
3
|
+
"version": "1.0.24",
|
|
4
4
|
"main": "index.js",
|
|
5
5
|
"type": "module",
|
|
6
6
|
"scripts": {
|
|
7
7
|
"test": "node --test tests/*.test.js",
|
|
8
8
|
"test:anyapi": "JSON_REST_API_STORAGE=anyapi node --test tests/*.test.js",
|
|
9
|
+
"test:id-contracts": "node --test tests/id-normalization.test.js tests/logical-id-write-validation.test.js tests/custom-id-simplified-post.test.js",
|
|
10
|
+
"test:id-contracts:anyapi": "JSON_REST_API_STORAGE=anyapi node --test tests/id-normalization.test.js",
|
|
9
11
|
"docs": "node scripts/run-docs.js",
|
|
12
|
+
"docs:dev": "node scripts/run-docs.js --serve",
|
|
13
|
+
"verify": "npm run test:id-contracts && npm run test:id-contracts:anyapi && npm run docs",
|
|
10
14
|
"sloc": "sloc --exclude \"(_site|node_modules|\\.git|vendor|docs)\" .",
|
|
11
15
|
"slod": "cd docs;echo 'DOC LINES:'; cat `find . -iname \\*md | grep -v _site | grep -v vendor` | wc",
|
|
12
16
|
"lint": "eslint .",
|
|
@@ -25,7 +29,7 @@
|
|
|
25
29
|
"description": "REST API plugin for hooked-api with JSON:API compliance",
|
|
26
30
|
"dependencies": {
|
|
27
31
|
"hooked-api": "^1.0.24",
|
|
28
|
-
"json-rest-schema": "^1.0.
|
|
32
|
+
"json-rest-schema": "^1.0.14",
|
|
29
33
|
"semver": "^7.7.2"
|
|
30
34
|
},
|
|
31
35
|
"peerDependencies": {
|