json-rest-api 1.0.20 → 1.0.22

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 (173) hide show
  1. package/audit/00-charter.md +103 -0
  2. package/audit/01-scope-map.md +84 -0
  3. package/audit/02-coverage-matrix.csv +174 -0
  4. package/audit/03-invariants.md +123 -0
  5. package/audit/04-standards-rubric.md +56 -0
  6. package/audit/05-competitor-rubric.md +77 -0
  7. package/audit/06-findings.md +160 -0
  8. package/audit/07-risk-register.csv +10 -0
  9. package/audit/08-remediation-roadmap.md +177 -0
  10. package/audit/09-signoff-check.md +23 -0
  11. package/audit/10-final-synthesis.md +29 -0
  12. package/audit/evidence/code/all-files-json-rest-api.txt +168 -0
  13. package/audit/evidence/code/all-files-json-rest-schema.txt +12 -0
  14. package/audit/evidence/code/boundary-leak-checks.txt +72 -0
  15. package/audit/evidence/code/critical-pass1/json-rest-api__plugins__core__bulk-operations-plugin.js.txt +480 -0
  16. package/audit/evidence/code/critical-pass1/json-rest-api__plugins__core__connectors__express-plugin.js.txt +532 -0
  17. package/audit/evidence/code/critical-pass1/json-rest-api__plugins__core__connectors__lib__busboy-detector.js.txt +175 -0
  18. package/audit/evidence/code/critical-pass1/json-rest-api__plugins__core__connectors__lib__formidable-detector.js.txt +138 -0
  19. package/audit/evidence/code/critical-pass1/json-rest-api__plugins__core__connectors__lib__request-helpers.js.txt +222 -0
  20. package/audit/evidence/code/critical-pass1/json-rest-api__plugins__core__file-handling-plugin.js.txt +342 -0
  21. package/audit/evidence/code/critical-pass1/json-rest-api__plugins__core__lib__dbTablesOperations.js.txt +304 -0
  22. package/audit/evidence/code/critical-pass1/json-rest-api__plugins__core__lib__fractional-positioning.js.txt +188 -0
  23. package/audit/evidence/code/critical-pass1/json-rest-api__plugins__core__lib__querying-writing__compile-schemas.js.txt +336 -0
  24. package/audit/evidence/code/critical-pass1/json-rest-api__plugins__core__lib__querying-writing__connectors-query-parser.js.txt +165 -0
  25. package/audit/evidence/code/critical-pass1/json-rest-api__plugins__core__lib__querying-writing__database-capabilities.js.txt +265 -0
  26. package/audit/evidence/code/critical-pass1/json-rest-api__plugins__core__lib__querying-writing__database-value-normalizers.js.txt +369 -0
  27. package/audit/evidence/code/critical-pass1/json-rest-api__plugins__core__lib__querying-writing__default-data-helpers.js.txt +72 -0
  28. package/audit/evidence/code/critical-pass1/json-rest-api__plugins__core__lib__querying-writing__field-utils.js.txt +139 -0
  29. package/audit/evidence/code/critical-pass1/json-rest-api__plugins__core__lib__querying-writing__knex-constants.js.txt +45 -0
  30. package/audit/evidence/code/critical-pass1/json-rest-api__plugins__core__lib__querying-writing__knex-field-helpers.js.txt +396 -0
  31. package/audit/evidence/code/critical-pass1/json-rest-api__plugins__core__lib__querying-writing__knex-json-api-transformers.js.txt +162 -0
  32. package/audit/evidence/code/critical-pass1/json-rest-api__plugins__core__lib__querying-writing__payload-validators.js.txt +1314 -0
  33. package/audit/evidence/code/critical-pass1/json-rest-api__plugins__core__lib__querying-writing__schema-helpers.js.txt +488 -0
  34. package/audit/evidence/code/critical-pass1/json-rest-api__plugins__core__lib__querying-writing__scope-validations.js.txt +484 -0
  35. package/audit/evidence/code/critical-pass1/json-rest-api__plugins__core__lib__querying-writing__simplified-helpers.js.txt +857 -0
  36. package/audit/evidence/code/critical-pass1/json-rest-api__plugins__core__lib__querying__knex-cross-table-search.js.txt +565 -0
  37. package/audit/evidence/code/critical-pass1/json-rest-api__plugins__core__lib__querying__knex-json-api-transformers-querying.js.txt +485 -0
  38. package/audit/evidence/code/critical-pass1/json-rest-api__plugins__core__lib__querying__knex-pagination-helpers.js.txt +560 -0
  39. package/audit/evidence/code/critical-pass1/json-rest-api__plugins__core__lib__querying__knex-process-includes.js.txt +167 -0
  40. package/audit/evidence/code/critical-pass1/json-rest-api__plugins__core__lib__querying__knex-query-helpers-base.js.txt +109 -0
  41. package/audit/evidence/code/critical-pass1/json-rest-api__plugins__core__lib__querying__knex-query-helpers.js.txt +1064 -0
  42. package/audit/evidence/code/critical-pass1/json-rest-api__plugins__core__lib__querying__knex-relationship-includes.js.txt +1983 -0
  43. package/audit/evidence/code/critical-pass1/json-rest-api__plugins__core__lib__querying__knex-window-queries.js.txt +328 -0
  44. package/audit/evidence/code/critical-pass1/json-rest-api__plugins__core__lib__querying__url-helpers.js.txt +119 -0
  45. package/audit/evidence/code/critical-pass1/json-rest-api__plugins__core__lib__storage__storage-adapter.js.txt +160 -0
  46. package/audit/evidence/code/critical-pass1/json-rest-api__plugins__core__lib__writing__many-to-many-manipulations.js.txt +300 -0
  47. package/audit/evidence/code/critical-pass1/json-rest-api__plugins__core__lib__writing__relationship-processor.js.txt +245 -0
  48. package/audit/evidence/code/critical-pass1/json-rest-api__plugins__core__rest-api-access.js.txt +459 -0
  49. package/audit/evidence/code/critical-pass1/json-rest-api__plugins__core__rest-api-cors-plugin.js.txt +237 -0
  50. package/audit/evidence/code/critical-pass1/json-rest-api__plugins__core__rest-api-knex-plugin.js.txt +1135 -0
  51. package/audit/evidence/code/critical-pass1/json-rest-api__plugins__core__rest-api-multihome-plugin.js.txt +314 -0
  52. package/audit/evidence/code/critical-pass1/json-rest-api__plugins__core__rest-api-plugin-hooks__compile-resource-schemas.js.txt +7 -0
  53. package/audit/evidence/code/critical-pass1/json-rest-api__plugins__core__rest-api-plugin-hooks__register-relationship-routes.js.txt +71 -0
  54. package/audit/evidence/code/critical-pass1/json-rest-api__plugins__core__rest-api-plugin-hooks__register-scope-routes.js.txt +102 -0
  55. package/audit/evidence/code/critical-pass1/json-rest-api__plugins__core__rest-api-plugin-hooks__turn-scope-init-into-vars.js.txt +49 -0
  56. package/audit/evidence/code/critical-pass1/json-rest-api__plugins__core__rest-api-plugin-hooks__validate-include-configurations.js.txt +39 -0
  57. package/audit/evidence/code/critical-pass1/json-rest-api__plugins__core__rest-api-plugin-methods__add-route.js.txt +23 -0
  58. package/audit/evidence/code/critical-pass1/json-rest-api__plugins__core__rest-api-plugin-methods__check-permissions.js.txt +13 -0
  59. package/audit/evidence/code/critical-pass1/json-rest-api__plugins__core__rest-api-plugin-methods__common.js.txt +639 -0
  60. package/audit/evidence/code/critical-pass1/json-rest-api__plugins__core__rest-api-plugin-methods__delete-relationship.js.txt +117 -0
  61. package/audit/evidence/code/critical-pass1/json-rest-api__plugins__core__rest-api-plugin-methods__delete.js.txt +104 -0
  62. package/audit/evidence/code/critical-pass1/json-rest-api__plugins__core__rest-api-plugin-methods__enrich-attributes.js.txt +199 -0
  63. package/audit/evidence/code/critical-pass1/json-rest-api__plugins__core__rest-api-plugin-methods__get-related.js.txt +353 -0
  64. package/audit/evidence/code/critical-pass1/json-rest-api__plugins__core__rest-api-plugin-methods__get-relationship.js.txt +61 -0
  65. package/audit/evidence/code/critical-pass1/json-rest-api__plugins__core__rest-api-plugin-methods__get.js.txt +184 -0
  66. package/audit/evidence/code/critical-pass1/json-rest-api__plugins__core__rest-api-plugin-methods__patch-relationship.js.txt +55 -0
  67. package/audit/evidence/code/critical-pass1/json-rest-api__plugins__core__rest-api-plugin-methods__patch.js.txt +191 -0
  68. package/audit/evidence/code/critical-pass1/json-rest-api__plugins__core__rest-api-plugin-methods__post-relationship.js.txt +110 -0
  69. package/audit/evidence/code/critical-pass1/json-rest-api__plugins__core__rest-api-plugin-methods__post.js.txt +168 -0
  70. package/audit/evidence/code/critical-pass1/json-rest-api__plugins__core__rest-api-plugin-methods__put.js.txt +253 -0
  71. package/audit/evidence/code/critical-pass1/json-rest-api__plugins__core__rest-api-plugin-methods__query.js.txt +207 -0
  72. package/audit/evidence/code/critical-pass1/json-rest-api__plugins__core__rest-api-plugin-methods__release.js.txt +12 -0
  73. package/audit/evidence/code/critical-pass1/json-rest-api__plugins__core__rest-api-plugin.js.txt +165 -0
  74. package/audit/evidence/code/critical-pass1/json-rest-api__plugins__core__rest-api-positioning-plugin.js.txt +668 -0
  75. package/audit/evidence/code/critical-pass1/json-rest-api__plugins__core__socketio-plugin.js.txt +621 -0
  76. package/audit/evidence/code/critical-pass1/json-rest-api__plugins__storage__local-storage.js.txt +218 -0
  77. package/audit/evidence/code/critical-pass1/json-rest-api__plugins__storage__s3-storage.js.txt +124 -0
  78. package/audit/evidence/code/critical-pass1/json-rest-schema__src__CorePlugin.js.txt +269 -0
  79. package/audit/evidence/code/critical-pass1/json-rest-schema__src__Schema.js.txt +242 -0
  80. package/audit/evidence/code/critical-pass1/json-rest-schema__src__index.js.txt +77 -0
  81. package/audit/evidence/code/critical-pass1-risk-keywords.txt +238 -0
  82. package/audit/evidence/code/critical-pass1-summary.csv +67 -0
  83. package/audit/evidence/code/critical-pass2-summary.csv +67 -0
  84. package/audit/evidence/code/early-silent-zones-no-test-token.txt +57 -0
  85. package/audit/evidence/code/error-model-signals.txt +98 -0
  86. package/audit/evidence/code/git-files-json-rest-api.txt +171 -0
  87. package/audit/evidence/code/git-files-json-rest-schema.txt +27 -0
  88. package/audit/evidence/code/hook-flow-signals.txt +282 -0
  89. package/audit/evidence/code/idproperty-signals.txt +157 -0
  90. package/audit/evidence/code/import-cycles-non-anyapi.txt +3 -0
  91. package/audit/evidence/code/import-hotspots-non-anyapi.txt +96 -0
  92. package/audit/evidence/code/imports-json-rest-api.txt +354 -0
  93. package/audit/evidence/code/imports-json-rest-schema.txt +15 -0
  94. package/audit/evidence/code/in-scope-files-json-rest-api.txt +160 -0
  95. package/audit/evidence/code/in-scope-files-json-rest-schema.txt +13 -0
  96. package/audit/evidence/code/non-anyapi-core-files.txt +101 -0
  97. package/audit/evidence/code/permission-signals.txt +193 -0
  98. package/audit/evidence/code/query-include-signals.txt +687 -0
  99. package/audit/evidence/code/transaction-signals.txt +28 -0
  100. package/audit/evidence/code/validation-signals.txt +650 -0
  101. package/audit/evidence/code/write-path-signals.txt +61 -0
  102. package/audit/evidence/docs/docs-claims-signals.txt +1075 -0
  103. package/audit/evidence/docs/schema-docs-claims-signals.txt +179 -0
  104. package/audit/evidence/external/corpus-freeze.md +21 -0
  105. package/audit/evidence/external/npm-competitor-metadata.txt +70 -0
  106. package/audit/evidence/external/npm-download-signals.txt +23 -0
  107. package/audit/evidence/external/npm-jsonapi-server.txt +8 -0
  108. package/audit/evidence/external/npm-modern-jsonapi-metadata.txt +38 -0
  109. package/audit/evidence/external/readme-fortune-signals.txt +26 -0
  110. package/audit/evidence/external/readme-fortune.md +165 -0
  111. package/audit/evidence/external/readme-json-api-serializer-signals.txt +80 -0
  112. package/audit/evidence/external/readme-json-api-serializer.md +690 -0
  113. package/audit/evidence/external/readme-jsonapi-serde-server-signals.txt +0 -0
  114. package/audit/evidence/external/readme-jsonapi-serde-server.md +22 -0
  115. package/audit/evidence/external/readme-jsonapi-serializer-signals.txt +63 -0
  116. package/audit/evidence/external/readme-jsonapi-serializer.md +315 -0
  117. package/audit/evidence/external/readme-jsonapi-server-signals.txt +19 -0
  118. package/audit/evidence/external/readme-jsonapi-server.md +104 -0
  119. package/audit/evidence/external/readme-nestjs-json-api-signals.txt +4 -0
  120. package/audit/evidence/external/readme-nestjs-json-api.md +94 -0
  121. package/audit/evidence/external/readme-yaser-json-api-nestjs-signals.txt +47 -0
  122. package/audit/evidence/external/readme-yaser-json-api-nestjs.md +447 -0
  123. package/audit/evidence/external/standard-jsonapi-snippets.txt +84 -0
  124. package/audit/evidence/external/standard-rfc-snippets.txt +168 -0
  125. package/audit/evidence/external/standards-sources.txt +6 -0
  126. package/audit/evidence/runtime/baseline-commits.txt +2 -0
  127. package/audit/evidence/runtime/db-adapter-assumptions.txt +3 -0
  128. package/audit/evidence/runtime/environment.txt +4 -0
  129. package/audit/evidence/runtime/invariant-runtime-mapping.md +38 -0
  130. package/audit/evidence/runtime/probe-patch-relationship-error.exitcode +1 -0
  131. package/audit/evidence/runtime/probe-patch-relationship-error.log +76 -0
  132. package/audit/evidence/runtime/probe-patch-relationship-error.mjs +67 -0
  133. package/audit/evidence/runtime/probe-patch-relationship-referenceerror.exitcode +1 -0
  134. package/audit/evidence/runtime/probe-patch-relationship-referenceerror.log +62 -0
  135. package/audit/evidence/runtime/probe-patch-relationship-referenceerror.mjs +47 -0
  136. package/audit/evidence/runtime/test-exitcodes.txt +4 -0
  137. package/audit/evidence/runtime/test-summary.md +107 -0
  138. package/audit/evidence/runtime/trace-anchors.txt +61 -0
  139. package/audit/evidence/tests/access-multitenancy-test-signals.txt +125 -0
  140. package/audit/evidence/tests/failing-tests-summary.txt +25 -0
  141. package/audit/evidence/tests/npm-test-json-rest-api.exitcode +1 -0
  142. package/audit/evidence/tests/npm-test-json-rest-api.log +5885 -0
  143. package/audit/evidence/tests/npm-test-json-rest-schema.exitcode +1 -0
  144. package/audit/evidence/tests/npm-test-json-rest-schema.log +356 -0
  145. package/audit/evidence/tests/query-include-test-signals.txt +948 -0
  146. package/audit/evidence/tests/relationship-test-signals.txt +223 -0
  147. package/audit/evidence/tests/skipped-tests.txt +10 -0
  148. package/audit/evidence/tests/test-suite-list-non-anyapi.txt +30 -0
  149. package/audit/evidence/tests/test-suite-list.txt +32 -0
  150. package/audit/subsystems/docs-drift.md +49 -0
  151. package/audit/subsystems/pass2-cross-review.md +131 -0
  152. package/audit/subsystems/plugins-access.md +51 -0
  153. package/audit/subsystems/plugins-bulk.md +50 -0
  154. package/audit/subsystems/plugins-cors.md +48 -0
  155. package/audit/subsystems/plugins-file.md +53 -0
  156. package/audit/subsystems/plugins-multihome.md +50 -0
  157. package/audit/subsystems/plugins-positioning.md +54 -0
  158. package/audit/subsystems/plugins-socketio.md +50 -0
  159. package/audit/subsystems/query-engine.md +61 -0
  160. package/audit/subsystems/relationships.md +55 -0
  161. package/audit/subsystems/rest-orchestration.md +66 -0
  162. package/audit/subsystems/schema-core.md +64 -0
  163. package/audit/subsystems/storage-knex.md +57 -0
  164. package/audit/subsystems/tests.md +48 -0
  165. package/audit/subsystems/transport-express.md +55 -0
  166. package/audit/traces/trace-read-get.md +22 -0
  167. package/audit/traces/trace-read-query.md +25 -0
  168. package/audit/traces/trace-relationship-endpoints.md +31 -0
  169. package/audit/traces/trace-write-delete.md +17 -0
  170. package/audit/traces/trace-write-patch.md +26 -0
  171. package/audit/traces/trace-write-post.md +22 -0
  172. package/audit/traces/trace-write-put.md +23 -0
  173. package/package.json +5 -5
@@ -0,0 +1,253 @@
1
+ 1 import { RestApiResourceError } from '../../../lib/rest-api-errors.js'
2
+ 2 import { validatePutPayload } from '../lib/querying-writing/payload-validators.js'
3
+ 3 import { processRelationships } from '../lib/writing/relationship-processor.js'
4
+ 4 import { updateManyToManyRelationship, createPivotRecords } from '../lib/writing/many-to-many-manipulations.js'
5
+ 5 import { ERROR_SUBTYPES } from '../lib/querying-writing/knex-constants.js'
6
+ 6 import {
7
+ 7 setupCommonRequest,
8
+ 8 validateResourceAttributesBeforeWrite,
9
+ 9 validateRelationshipAccess,
10
+ 10 applyFieldSetters,
11
+ 11 validatePivotResource,
12
+ 12 handleRecordReturnAfterWrite,
13
+ 13 handleWriteMethodError
14
+ 14 } from './common.js'
15
+ 15
16
+ 16 /**
17
+ 17 * PUT
18
+ 18 * Updates an existing top-level resource by completely replacing it.
19
+ 19 * This method supports updating both attributes and relationships (1:1 and n:n).
20
+ 20 * PUT is a complete replacement - any relationships not provided will be removed/nulled.
21
+ 21 * This method does NOT support creating new related resources via an `included` array.
22
+ 22 */
23
+ 23 export default async function putMethod ({
24
+ 24 params,
25
+ 25 context,
26
+ 26 vars,
27
+ 27 helpers,
28
+ 28 scope,
29
+ 29 scopes,
30
+ 30 runHooks,
31
+ 31 apiOptions,
32
+ 32 pluginOptions,
33
+ 33 scopeOptions,
34
+ 34 scopeName,
35
+ 35 api,
36
+ 36 log
37
+ 37 }) {
38
+ 38 context.method = 'put'
39
+ 39
40
+ 40 try {
41
+ 41 const { schema, schemaStructure, schemaRelationships } = await setupCommonRequest({
42
+ 42 params,
43
+ 43 context,
44
+ 44 vars,
45
+ 45 scopes,
46
+ 46 scopeOptions,
47
+ 47 scopeName,
48
+ 48 api,
49
+ 49 helpers
50
+ 50 })
51
+ 51 context.id = context.inputRecord.data.id
52
+ 52
53
+ 53 // Run early hooks for pre-processing (e.g., file handling)
54
+ 54 await runHooks('beforeProcessing')
55
+ 55 await runHooks('beforeProcessingPut')
56
+ 56
57
+ 57 // Validate PUT payload to ensure it's a complete resource replacement operation.
58
+ 58 // PUT requires the full resource representation including ID (unlike POST which generates ID).
59
+ 59 // It validates that data.id matches the URL parameter, prevents 'included' array (which is
60
+ 60 // read-only), and ensures the payload represents a complete replacement. Any fields not
61
+ 61 // provided will be removed or reset to defaults - this is the key difference from PATCH.
62
+ 62 // Example: PUT to /articles/123 must have data.id: '123' and all required fields.
63
+ 63 validatePutPayload(context.inputRecord, scopes)
64
+ 64
65
+ 65 // Validate that user has read access to all related resources
66
+ 66 // This ensures users can only create relationships to resources they can access
67
+ 67 await validateRelationshipAccess(context, context.inputRecord, helpers, runHooks, api)
68
+ 68
69
+ 69 // Extract foreign keys from JSON:API relationships and prepare many-to-many operations
70
+ 70 // Example: relationships.author -> author_id: '123' for storage
71
+ 71 // Example: relationships.tags -> array of pivot records to create later
72
+ 72 const { belongsToUpdates, manyToManyRelationships } = processRelationships(
73
+ 73 scope,
74
+ 74 { context }
75
+ 75 )
76
+ 76
77
+ 77 await validateResourceAttributesBeforeWrite({
78
+ 78 context,
79
+ 79 schema,
80
+ 80 belongsToUpdates,
81
+ 81 runHooks
82
+ 82 })
83
+ 83
84
+ 84 // Check existence first
85
+ 85 context.exists = await helpers.dataExists({
86
+ 86 scopeName,
87
+ 87 context
88
+ 88 })
89
+ 89
90
+ 90 context.isCreate = !context.exists
91
+ 91 context.isUpdate = context.exists
92
+ 92
93
+ 93 // Fetch minimal record for authorization checks (only for updates)
94
+ 94 if (context.isUpdate) {
95
+ 95 const minimalRecord = await helpers.dataGetMinimal({
96
+ 96 scopeName,
97
+ 97 context,
98
+ 98 runHooks
99
+ 99 })
100
+ 100
101
+ 101 if (!minimalRecord) {
102
+ 102 throw new RestApiResourceError(
103
+ 103 `Resource not found: ${scopeName}/${context.id}`,
104
+ 104 ERROR_SUBTYPES.NOT_FOUND
105
+ 105 )
106
+ 106 }
107
+ 107
108
+ 108 context.minimalRecord = minimalRecord
109
+ 109 }
110
+ 110
111
+ 111 // For PUT, we also need to handle relationships that are NOT provided
112
+ 112 // (they should be set to null/empty as PUT is a complete replacement)
113
+ 113 const allRelationships = {}
114
+ 114
115
+ 115 // Collect all defined relationships for this resource
116
+ 116 for (const [relName, relDef] of Object.entries(schemaRelationships || {})) {
117
+ 117 if (relDef.type === 'manyToMany') {
118
+ 118 allRelationships[relName] = {
119
+ 119 type: 'manyToMany',
120
+ 120 relDef: {
121
+ 121 through: relDef.through,
122
+ 122 foreignKey: relDef.foreignKey,
123
+ 123 otherKey: relDef.otherKey
124
+ 124 }
125
+ 125 }
126
+ 126 }
127
+ 127 }
128
+ 128
129
+ 129 // Also check schema fields for belongsTo relationships
130
+ 130 for (const [fieldName, fieldDef] of Object.entries(schemaStructure)) {
131
+ 131 if (fieldDef.as && fieldDef.belongsTo) {
132
+ 132 allRelationships[fieldDef.as] = {
133
+ 133 type: 'belongsTo',
134
+ 134 fieldName,
135
+ 135 fieldDef
136
+ 136 }
137
+ 137 }
138
+ 138 }
139
+ 139
140
+ 140 // Process missing relationships (PUT should null them out only if relationships object exists)
141
+ 141 const hasRelationshipsObject = context.inputRecord.data.relationships !== undefined
142
+ 142 const providedRelationships = new Set(Object.keys(context.inputRecord.data.relationships || {}))
143
+ 143
144
+ 144 // Only null out missing relationships if a relationships object was provided
145
+ 145 if (hasRelationshipsObject) {
146
+ 146 for (const [relName, relInfo] of Object.entries(allRelationships)) {
147
+ 147 if (!providedRelationships.has(relName)) {
148
+ 148 if (relInfo.type === 'belongsTo') {
149
+ 149 belongsToUpdates[relInfo.fieldName] = null
150
+ 150 } else if (relInfo.type === 'manyToMany') {
151
+ 151 // Add to manyToManyRelationships with empty array
152
+ 152 manyToManyRelationships.push({
153
+ 153 relName,
154
+ 154 relDef: relInfo.relDef,
155
+ 155 relData: [] // Empty array means delete all
156
+ 156 })
157
+ 157 }
158
+ 158 }
159
+ 159 }
160
+ 160 }
161
+ 161
162
+ 162 // Merge belongsTo updates with attributes
163
+ 163 if (Object.keys(belongsToUpdates).length > 0) {
164
+ 164 context.inputRecord.data.attributes = {
165
+ 165 ...context.inputRecord.data.attributes,
166
+ 166 ...belongsToUpdates
167
+ 167 }
168
+ 168 }
169
+ 169
170
+ 170 // Centralised checkPermissions function
171
+ 171 await scope.checkPermissions({
172
+ 172 method: 'put',
173
+ 173 originalContext: context,
174
+ 174 })
175
+ 175
176
+ 176 await runHooks('beforeDataCall')
177
+ 177 await runHooks('beforeDataCallPut')
178
+ 178
179
+ 179 // Apply field setters after validation and before storage
180
+ 180 if (context.inputRecord?.data?.attributes) {
181
+ 181 context.inputRecord.data.attributes = await applyFieldSetters(
182
+ 182 context.inputRecord.data.attributes,
183
+ 183 context.schemaInfo,
184
+ 184 context,
185
+ 185 api,
186
+ 186 helpers
187
+ 187 )
188
+ 188 }
189
+ 189
190
+ 190 // Pass the operation type to the helper
191
+ 191 await helpers.dataPut({
192
+ 192 scopeName,
193
+ 193 context
194
+ 194 })
195
+ 195 await runHooks('afterDataCallPut')
196
+ 196 await runHooks('afterDataCall')
197
+ 197
198
+ 198 // Process many-to-many relationships after main record update/creation
199
+ 199 for (const { relName, relDef, relData } of manyToManyRelationships) {
200
+ 200 if (relDef?.through && api.anyapi?.links?.syncMany) {
201
+ 201 await api.anyapi.links.syncMany({
202
+ 202 context,
203
+ 203 scopeName,
204
+ 204 relName,
205
+ 205 relDef,
206
+ 206 relData,
207
+ 207 isUpdate: context.isUpdate,
208
+ 208 })
209
+ 209 continue
210
+ 210 }
211
+ 211
212
+ 212 await validatePivotResource(scopes, relDef, relName)
213
+ 213
214
+ 214 if (context.isUpdate) {
215
+ 215 await updateManyToManyRelationship(null, {
216
+ 216 api,
217
+ 217 context: {
218
+ 218 resourceId: context.id,
219
+ 219 relDef,
220
+ 220 relData,
221
+ 221 transaction: context.transaction
222
+ 222 }
223
+ 223 })
224
+ 224 } else if (relData.length > 0) {
225
+ 225 await createPivotRecords(api, context.id, relDef, relData, context.transaction)
226
+ 226 }
227
+ 227 }
228
+ 228
229
+ 229 const ret = await handleRecordReturnAfterWrite({
230
+ 230 context,
231
+ 231 scopeName,
232
+ 232 api,
233
+ 233 scopes,
234
+ 234 schemaStructure,
235
+ 235 schemaRelationships,
236
+ 236 scopeOptions,
237
+ 237 vars,
238
+ 238 runHooks,
239
+ 239 helpers,
240
+ 240 log
241
+ 241 })
242
+ 242
243
+ 243 // Commit transaction if we created it
244
+ 244 if (context.shouldCommit) {
245
+ 245 await context.transaction.commit()
246
+ 246 await runHooks('afterCommit')
247
+ 247 }
248
+ 248
249
+ 249 return ret
250
+ 250 } catch (error) {
251
+ 251 await handleWriteMethodError(error, context, 'PUT', scopeName, log, runHooks)
252
+ 252 }
253
+ 253 }
@@ -0,0 +1,207 @@
1
+ 1 import { RestApiValidationError } from '../../../lib/rest-api-errors.js'
2
+ 2 import { validateQueryPayload } from '../lib/querying-writing/payload-validators.js'
3
+ 3 import { normalizeRecordAttributes } from '../lib/querying-writing/database-value-normalizers.js'
4
+ 4 import { getRequestedComputedFields } from '../lib/querying-writing/knex-field-helpers.js'
5
+ 5 import { transformJsonApiToSimplified } from '../lib/querying-writing/simplified-helpers.js'
6
+ 6 import { cascadeConfig } from './common.js'
7
+ 7
8
+ 8 /**
9
+ 9 * QUERY
10
+ 10 * Retrieves a collection of resources (e.g., a list of articles) based on provided criteria.
11
+ 11 * This function sends a GET request to /api/{resourceType}.
12
+ 12 *
13
+ 13 * @param {string} resourceType - The type of resource collection to fetch (e.g., "articles").
14
+ 14 * @param {object} [queryParams={}] - Optional. An object to customize the query for the collection.
15
+ 15 * @param {string[]} [queryParams.include=[]] - An optional array of relationship paths to sideload for each resource in the collection. These paths will be converted to a comma-separated string for the URL (e.g., `['author', 'comments.user']` becomes `author,comments.user`). Supports deep relationships (e.g., "publisher.country").
16
+ 16 * @param {object} [queryParams.fields] - An object to request only specific fields (sparse fieldsets) for each resource in the collection and its included relationships. Keys are resource types, values are comma-separated field names.
17
+ 17 * @param {object} [queryParams.filters] - An object to filter the collection. Keys are filter parameters (specific to your API's implementation, e.g., 'status', 'title'), values are the filter criteria.
18
+ 18 * @param {string[]} [queryParams.sort=[]] - An optional array of fields to sort the collection by. Each string represents a field; prefix with '-' for descending order (e.g., `['title', '-published-date']` becomes `title,-published-date`).
19
+ 19 * @param {object} [queryParams.page] - An object for pagination. Typically includes `number` (page number) and `size` (items per page). E.g., `{ number: 1, size: 10 }`.
20
+ 20 * @returns {Promise<object>} A Promise that resolves to the JSON:API response document containing the resource collection.
21
+ 21 */
22
+ 22 export default async function queryMethod ({
23
+ 23 params,
24
+ 24 context,
25
+ 25 vars,
26
+ 26 helpers,
27
+ 27 scope,
28
+ 28 scopes,
29
+ 29 runHooks,
30
+ 30 apiOptions,
31
+ 31 pluginOptions,
32
+ 32 scopeOptions,
33
+ 33 scopeName,
34
+ 34 log,
35
+ 35 api
36
+ 36 }) {
37
+ 37 context.method = 'query'
38
+ 38
39
+ 39 // Determine which simplified setting to use based on transport
40
+ 40 const isTransport = params.isTransport === true
41
+ 41
42
+ 42 // Use vars which automatically cascade from scope to global
43
+ 43 const defaultSimplified = isTransport ? vars.simplifiedTransport : vars.simplifiedApi
44
+ 44
45
+ 45 // Get simplified setting - from params only (per-call override) or use default
46
+ 46 context.simplified = params.simplified !== undefined ? params.simplified : defaultSimplified
47
+ 47
48
+ 48 // Assign common context properties
49
+ 49 context.schemaInfo = scopes[scopeName].vars.schemaInfo // This is the object variable created by compileSchemas
50
+ 50 context.queryParams = params.queryParams || {}
51
+ 51
52
+ 52 // These only make sense as parameter per query
53
+ 53 context.queryParams.fields = cascadeConfig('fields', [params.queryParams], {})
54
+ 54 context.queryParams.include = cascadeConfig('include', [params.queryParams], [])
55
+ 55 context.queryParams.sort = cascadeConfig('sort', [params.queryParams], [])
56
+ 56 context.queryParams.page = cascadeConfig('page', [params.queryParams], {})
57
+ 57
58
+ 58 context.transaction = params.transaction
59
+ 59 context.db = context.transaction || api.knex.instance
60
+ 60
61
+ 61 context.scopeName = scopeName
62
+ 62
63
+ 63 // These are just shortcuts used in this function and will be returned
64
+ 64 const searchSchema = context.schemaInfo.searchSchemaInstance
65
+ 65 const schemaStructure = context.schemaInfo.schemaInstance.structure
66
+ 66 const schemaRelationships = context.schemaInfo.schemaRelationships
67
+ 67
68
+ 68 // Sortable fields and sort (mab)
69
+ 69 context.sortableFields = vars.sortableFields
70
+ 70 // Apply default sort if no sort specified
71
+ 71 if (context.queryParams.sort.length === 0 && vars.defaultSort) {
72
+ 72 context.queryParams.sort = Array.isArray(vars.defaultSort) ? vars.defaultSort : [vars.defaultSort]
73
+ 73 }
74
+ 74
75
+ 75 // Validate query parameters to ensure they follow JSON:API specification and security rules.
76
+ 76 // This checks that filters are valid field names, sort fields exist in sortableFields array
77
+ 77 // (preventing SQL injection), pagination uses valid page[size]/page[number] format, and include
78
+ 78 // paths reference real relationships. Example: sort: ['-createdAt', 'title'] is checked against
79
+ 79 // sortableFields to ensure users can't sort by sensitive fields like 'password_hash'.
80
+ 80 validateQueryPayload({ queryParams: context.queryParams }, context.sortableFields, vars.includeDepthLimit)
81
+ 81
82
+ 82 // Validate search/filter parameters against searchSchema
83
+ 83 if (context.queryParams.filters && Object.keys(context.queryParams.filters).length > 0) {
84
+ 84 // Only allow filtering if searchSchema is defined
85
+ 85 if (!searchSchema) {
86
+ 86 throw new RestApiValidationError(
87
+ 87 `Filtering is not enabled for resource '${scopeName}'. To enable filtering, add 'search: true' to schema fields or define a searchSchema.`,
88
+ 88 {
89
+ 89 fields: Object.keys(context.queryParams.filters).map(field => `filters.${field}`),
90
+ 90 violations: [{
91
+ 91 field: 'filters',
92
+ 92 rule: 'filtering_not_enabled',
93
+ 93 message: 'Resource does not have searchable fields defined'
94
+ 94 }]
95
+ 95 }
96
+ 96 )
97
+ 97 }
98
+ 98
99
+ 99 // Validate the filter parameters searchSchema
100
+ 100 const { validatedObject, errors } = await searchSchema.validate(context.queryParams.filters, {
101
+ 101 onlyObjectValues: true // Partial validation for filters
102
+ 102 })
103
+ 103
104
+ 104 // If there are validation errors, throw an error
105
+ 105 if (Object.keys(errors).length > 0) {
106
+ 106 const violations = Object.entries(errors).map(([field, error]) => ({
107
+ 107 field: `filters.${field}`,
108
+ 108 rule: error.code || 'invalid_value',
109
+ 109 message: error.message
110
+ 110 }))
111
+ 111
112
+ 112 throw new RestApiValidationError(
113
+ 113 'Invalid filter parameters',
114
+ 114 {
115
+ 115 fields: Object.keys(errors).map(field => `filters.${field}`),
116
+ 116 violations
117
+ 117 }
118
+ 118 )
119
+ 119 }
120
+ 120
121
+ 121 // Replace filter with validated/transformed values
122
+ 122 context.queryParams.filters = validatedObject
123
+ 123 }
124
+ 124
125
+ 125 // Centralised checkPermissions function
126
+ 126 await scope.checkPermissions({
127
+ 127 method: 'query',
128
+ 128 originalContext: context,
129
+ 129 })
130
+ 130
131
+ 131 await runHooks('beforeData')
132
+ 132 await runHooks('beforeDataQuery')
133
+ 133 context.record = await helpers.dataQuery({
134
+ 134 scopeName,
135
+ 135 context,
136
+ 136 runHooks
137
+ 137 })
138
+ 138
139
+ 139 // Normalize database values (e.g., convert 1/0 to true/false for booleans)
140
+ 140 context.record = normalizeRecordAttributes(context.record, scopes)
141
+ 141
142
+ 142 // Make a backup
143
+ 143 try {
144
+ 144 context.originalRecord = structuredClone(context.record)
145
+ 145 } catch (e) {
146
+ 146 log.error('Failed to clone record:', {
147
+ 147 error: e.message,
148
+ 148 recordKeys: Object.keys(context.record || {}),
149
+ 149 hasHttpRequest: !!context.raw?.req
150
+ 150 })
151
+ 151 throw e
152
+ 152 }
153
+ 153
154
+ 154 // This will enhance record, which is the WHOLE JSON:API record
155
+ 155 await runHooks('enrichRecord')
156
+ 156
157
+ 157 // Get computed field information for main resource
158
+ 158 const computedFields = scope.vars.schemaInfo?.computed || {}
159
+ 159 const requestedFields = context.queryParams.fields?.[scopeName]
160
+ 160 const requestedComputedFields = getRequestedComputedFields(scopeName, requestedFields, computedFields)
161
+ 161
162
+ 162 // Run enrichAttributes for every single set of attribute, calling it from the right scope
163
+ 163 for (const entry of context.record.data) {
164
+ 164 entry.attributes = await scope.enrichAttributes({
165
+ 165 attributes: entry.attributes,
166
+ 166 parentContext: context,
167
+ 167 requestedComputedFields,
168
+ 168 isMainResource: true,
169
+ 169 computedDependencies: context.computedDependencies
170
+ 170 })
171
+ 171 }
172
+ 172 for (const entry of (context.record.included || [])) {
173
+ 173 const entryScope = scopes[entry.type]
174
+ 174 const entryComputed = entryScope.vars.schemaInfo?.computed || {}
175
+ 175 const entryRequestedFields = context.queryParams.fields?.[entry.type]
176
+ 176 const entryRequestedComputed = getRequestedComputedFields(
177
+ 177 entry.type,
178
+ 178 entryRequestedFields,
179
+ 179 entryComputed
180
+ 180 )
181
+ 181
182
+ 182 entry.attributes = await entryScope.enrichAttributes({
183
+ 183 attributes: entry.attributes,
184
+ 184 parentContext: context,
185
+ 185 requestedComputedFields: entryRequestedComputed,
186
+ 186 isMainResource: false,
187
+ 187 computedDependencies: entry.__$jsonrestapi_computed_deps$__
188
+ 188 })
189
+ 189 }
190
+ 190
191
+ 191 // The called hooks should NOT change context.record
192
+ 192 await runHooks('finish')
193
+ 193 await runHooks('finishQuery')
194
+ 194
195
+ 195 // Transform output if in simplified mode
196
+ 196 if (context.simplified) {
197
+ 197 // Convert JSON:API response back to simplified format
198
+ 198 // Example: {data: {type: 'posts', id: '1', attributes: {title: 'My Post'}, relationships: {author: {data: {type: 'users', id: '123'}}}}}
199
+ 199 // becomes: {id: '1', title: 'My Post', author_id: '123'} - flattens structure and restores foreign keys
200
+ 200 return transformJsonApiToSimplified(
201
+ 201 { record: context.record },
202
+ 202 { context: { schemaStructure, schemaRelationships, scopes } }
203
+ 203 )
204
+ 204 }
205
+ 205
206
+ 206 return context.record
207
+ 207 }
@@ -0,0 +1,12 @@
1
+ 1 import { ValidationError } from 'hooked-api'
2
+ 2
3
+ 3 /**
4
+ 4 * release
5
+ 5 * Runs the hook that tells plugins to release resources
6
+ 6 */
7
+ 7 export default async ({ runHooks }) => {
8
+ 8 // Run the addRoute hook to notify transport plugins
9
+ 9 await runHooks('release')
10
+ 10
11
+ 11 return { }
12
+ 12 }
@@ -0,0 +1,165 @@
1
+ 1 import { validateRelationships } from './lib/querying-writing/scope-validations.js'
2
+ 2
3
+ 3 // Import hook functions
4
+ 4 import compileResourceSchemas from './rest-api-plugin-hooks/compile-resource-schemas.js'
5
+ 5 import validateIncludeConfigurations from './rest-api-plugin-hooks/validate-include-configurations.js'
6
+ 6 import turnScopeInitIntoVars from './rest-api-plugin-hooks/turn-scope-init-into-vars.js'
7
+ 7 import registerScopeRoutes from './rest-api-plugin-hooks/register-scope-routes.js'
8
+ 8 import registerRelationshipRoutes from './rest-api-plugin-hooks/register-relationship-routes.js'
9
+ 9
10
+ 10 // Import method functions
11
+ 11 import queryMethod from './rest-api-plugin-methods/query.js'
12
+ 12 import getMethod from './rest-api-plugin-methods/get.js'
13
+ 13 import postMethod from './rest-api-plugin-methods/post.js'
14
+ 14 import putMethod from './rest-api-plugin-methods/put.js'
15
+ 15 import patchMethod from './rest-api-plugin-methods/patch.js'
16
+ 16 import deleteMethod from './rest-api-plugin-methods/delete.js'
17
+ 17 import enrichAttributesMethod from './rest-api-plugin-methods/enrich-attributes.js'
18
+ 18 import checkPermissionsMethod from './rest-api-plugin-methods/check-permissions.js'
19
+ 19 import addRouteMethod from './rest-api-plugin-methods/add-route.js'
20
+ 20 import releaseMethod from './rest-api-plugin-methods/release.js'
21
+ 21 import { defaultDataHelpers } from './lib/querying-writing/default-data-helpers.js'
22
+ 22 import { DEFAULT_QUERY_LIMIT, DEFAULT_MAX_QUERY_LIMIT, DEFAULT_INCLUDE_DEPTH_LIMIT } from './lib/querying-writing/knex-constants.js'
23
+ 23
24
+ 24 import getRelatedMethod from './rest-api-plugin-methods/get-related.js'
25
+ 25 import postRelationshipMethod from './rest-api-plugin-methods/post-relationship.js'
26
+ 26 import getRelationshipMethod from './rest-api-plugin-methods/get-relationship.js'
27
+ 27 import patchRelationshipMethod from './rest-api-plugin-methods/patch-relationship.js'
28
+ 28 import deleteRelationshipMethod from './rest-api-plugin-methods/delete-relationship.js'
29
+ 29
30
+ 30 export const RestApiPlugin = {
31
+ 31 name: 'rest-api',
32
+ 32
33
+ 33 install ({ helpers, addScopeMethod, addApiMethod, vars, addHook, runHooks, apiOptions, pluginOptions, api, setScopeAlias, scopes, log, on }) {
34
+ 34 // **************
35
+ 35 // Initial setup
36
+ 36 // **************
37
+ 37
38
+ 38 // Initialize the rest namespace for REST API functionality
39
+ 39 api.rest = {}
40
+ 40
41
+ 41 // Set up REST-friendly aliases
42
+ 42 setScopeAlias('resources', 'addResource')
43
+ 43
44
+ 44 // **********
45
+ 45 // Variables
46
+ 46 // **********
47
+ 47
48
+ 48 // Initialize default vars for the plugin from pluginOptions
49
+ 49 const restApiOptions = pluginOptions || {}
50
+ 50
51
+ 51 // These will be used as default fallbacks by the vars proxy if
52
+ 52 // they are not set in the scope options
53
+ 53 vars.queryDefaultLimit = restApiOptions.queryDefaultLimit || DEFAULT_QUERY_LIMIT
54
+ 54 vars.queryMaxLimit = restApiOptions.queryMaxLimit || DEFAULT_MAX_QUERY_LIMIT
55
+ 55 vars.includeDepthLimit = restApiOptions.includeDepthLimit || DEFAULT_INCLUDE_DEPTH_LIMIT
56
+ 56 vars.enablePaginationCounts = restApiOptions.enablePaginationCounts || true
57
+ 57
58
+ 58 // New simplified settings
59
+ 59 vars.simplifiedTransport = restApiOptions.simplifiedTransport !== undefined
60
+ 60 ? restApiOptions.simplifiedTransport
61
+ 61 : false // Default false for JSON:API compliance over the wire
62
+ 62
63
+ 63 vars.simplifiedApi = restApiOptions.simplifiedApi !== undefined
64
+ 64 ? restApiOptions.simplifiedApi
65
+ 65 : true // Default true for better DX in programmatic API
66
+ 66
67
+ 67 vars.idProperty = restApiOptions.idProperty || 'id'
68
+ 68
69
+ 69 // Return full record configuration for API and Transport
70
+ 70 // Support values: 'no', 'minimal', 'full'
71
+ 71 const normalizeReturnValue = (value, defaultValue) => {
72
+ 72 if (['no', 'minimal', 'full'].includes(value)) return value
73
+ 73 return defaultValue
74
+ 74 }
75
+ 75
76
+ 76 const optionConfigs = [
77
+ 77 {
78
+ 78 propName: 'returnRecordApi',
79
+ 79 defaultValue: 'full',
80
+ 80 },
81
+ 81 {
82
+ 82 propName: 'returnRecordTransport',
83
+ 83 defaultValue: 'no',
84
+ 84 },
85
+ 85 ]
86
+ 86
87
+ 87 for (const config of optionConfigs) {
88
+ 88 const optionValue = restApiOptions[config.propName]
89
+ 89 let processedValue
90
+ 90
91
+ 91 if (typeof optionValue === 'object' && optionValue !== null) {
92
+ 92 processedValue = {
93
+ 93 post: normalizeReturnValue(optionValue.post, config.defaultValue),
94
+ 94 put: normalizeReturnValue(optionValue.put, config.defaultValue),
95
+ 95 patch: normalizeReturnValue(optionValue.patch, config.defaultValue),
96
+ 96 }
97
+ 97 } else if (optionValue !== undefined) {
98
+ 98 const normalized = normalizeReturnValue(optionValue, config.defaultValue)
99
+ 99 processedValue = { post: normalized, put: normalized, patch: normalized }
100
+ 100 } else {
101
+ 101 processedValue = { post: config.defaultValue, put: config.defaultValue, patch: config.defaultValue }
102
+ 102 }
103
+ 103 vars[config.propName] = processedValue
104
+ 104 }
105
+ 105
106
+ 106 log.debug('returnRecordApi configuration:', vars.returnRecordApi)
107
+ 107 log.debug('returnRecordTransport configuration:', vars.returnRecordTransport)
108
+ 108
109
+ 109 // Schema cache vars
110
+ 110 vars.schemaProcessed = false
111
+ 111 vars.schema = null
112
+ 112
113
+ 113 // ******************************
114
+ 114 // Scope (resources) added hooks
115
+ 115 // ******************************
116
+ 116
117
+ 117 addHook('scope:added', 'validateRelationships', {}, validateRelationships)
118
+ 118 addHook('scope:added', 'compileResourceSchemas', {}, compileResourceSchemas)
119
+ 119 addHook('scope:added', 'validateIncludeConfigurations', {}, validateIncludeConfigurations)
120
+ 120 addHook('scope:added', 'turnScopeInitIntoVars', {}, turnScopeInitIntoVars)
121
+ 121
122
+ 122 // *********
123
+ 123 // Methods
124
+ 124 // *********
125
+ 125
126
+ 126 addApiMethod('addRoute', addRouteMethod)
127
+ 127
128
+ 128 addApiMethod('release', releaseMethod)
129
+ 129
130
+ 130 // Main REST methods
131
+ 131 addScopeMethod('query', queryMethod)
132
+ 132 addScopeMethod('get', getMethod)
133
+ 133 addScopeMethod('post', postMethod)
134
+ 134 addScopeMethod('put', putMethod)
135
+ 135 addScopeMethod('patch', patchMethod)
136
+ 136 addScopeMethod('delete', deleteMethod)
137
+ 137
138
+ 138 // Relationship methods
139
+ 139 addScopeMethod('getRelationship', getRelationshipMethod)
140
+ 140 addScopeMethod('getRelated', getRelatedMethod)
141
+ 141 addScopeMethod('postRelationship', postRelationshipMethod)
142
+ 142 addScopeMethod('patchRelationship', patchRelationshipMethod)
143
+ 143 addScopeMethod('deleteRelationship', deleteRelationshipMethod)
144
+ 144
145
+ 145 addHook('scope:added', 'registerRelationshipRoutes', {}, registerRelationshipRoutes)
146
+ 146 addHook('scope:added', 'registerScopeRoutes', {}, registerScopeRoutes)
147
+ 147
148
+ 148 // Non-URL methods
149
+ 149 addScopeMethod('enrichAttributes', enrichAttributesMethod)
150
+ 150 addScopeMethod('checkPermissions', checkPermissionsMethod)
151
+ 151
152
+ 152 // *********
153
+ 153 // Helpers
154
+ 154 // *********
155
+ 155
156
+ 156 // Initialize default data helpers that throw errors until a storage plugin is installed
157
+ 157 // These placeholders show storage plugin developers what methods to implement
158
+ 158 // Example: helpers.dataGet, helpers.dataPost, etc. will throw "No storage implementation" errors
159
+ 159 Object.assign(helpers, defaultDataHelpers)
160
+ 160
161
+ 161 // Add default getLocation helper for generating resource URLs
162
+ 162 // This can be overridden by storage plugins if needed
163
+ 163 helpers.getLocation = ({ scopeName, id }) => `/${scopeName}/${id}`
164
+ 164 }
165
+ 165 }