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,300 @@
1
+ 1 import { RestApiResourceError } from '../../../../lib/rest-api-errors.js'
2
+ 2 import { transformSimplifiedToJsonApi } from '../querying-writing/simplified-helpers.js'
3
+ 3
4
+ 4 /**
5
+ 5 * Updates many-to-many relationships intelligently by synchronizing pivot table records
6
+ 6 *
7
+ 7 * @param {Object} scope - The scope object (not used directly, for consistency)
8
+ 8 * @param {Object} deps - Dependencies object
9
+ 9 * @returns {Promise<void>}
10
+ 10 *
11
+ 11 * @example
12
+ 12 * // Input: Article currently has tags [1, 2], want to change to [1, 3]
13
+ 13 * const deps = {
14
+ 14 * api,
15
+ 15 * context: {
16
+ 16 * resourceId: '100',
17
+ 17 * relDef: {
18
+ 18 * through: 'article_tags', // Pivot table
19
+ 19 * foreignKey: 'article_id', // Points to article
20
+ 20 * otherKey: 'tag_id' // Points to tag
21
+ 21 * },
22
+ 22 * relData: [
23
+ 23 * { type: 'tags', id: '1' }, // Keep this
24
+ 24 * { type: 'tags', id: '3' } // Add this
25
+ 25 * ],
26
+ 26 * transaction: trx
27
+ 27 * }
28
+ 28 * };
29
+ 29 *
30
+ 30 * // Before: article_tags table
31
+ 31 * // article_id | tag_id | created_at
32
+ 32 * // 100 | 1 | 2024-01-01
33
+ 33 * // 100 | 2 | 2024-01-02
34
+ 34 *
35
+ 35 * await updateManyToManyRelationship(null, deps);
36
+ 36 *
37
+ 37 * // After: article_tags table
38
+ 38 * // article_id | tag_id | created_at
39
+ 39 * // 100 | 1 | 2024-01-01 (preserved!)
40
+ 40 * // 100 | 3 | 2024-12-01 (new)
41
+ 41 * // Tag 2 was deleted, Tag 1 kept its metadata
42
+ 42 *
43
+ 43 * @example
44
+ 44 * // Input: Pivot table has extra fields to preserve
45
+ 45 * // article_tags has: article_id, tag_id, display_order, featured
46
+ 46 *
47
+ 47 * // Current data:
48
+ 48 * // article_id | tag_id | display_order | featured
49
+ 49 * // 100 | 1 | 1 | true
50
+ 50 * // 100 | 2 | 2 | false
51
+ 51 *
52
+ 52 * const deps = {
53
+ 53 * context: {
54
+ 54 * resourceId: '100',
55
+ 55 * relData: [
56
+ 56 * { type: 'tags', id: '1' }, // Keep tag 1
57
+ 57 * { type: 'tags', id: '5' } // Add tag 5
58
+ 58 * ]
59
+ 59 * }
60
+ 60 * };
61
+ 61 *
62
+ 62 * await updateManyToManyRelationship(null, deps);
63
+ 63 *
64
+ 64 * // Result:
65
+ 65 * // article_id | tag_id | display_order | featured
66
+ 66 * // 100 | 1 | 1 | true (preserved!)
67
+ 67 * // 100 | 5 | NULL | NULL (new with defaults)
68
+ 68 *
69
+ 69 * @example
70
+ 70 * // Input: Clear all relationships
71
+ 71 * const deps = {
72
+ 72 * context: {
73
+ 73 * resourceId: '100',
74
+ 74 * relData: [] // Empty array means remove all
75
+ 75 * }
76
+ 76 * };
77
+ 77 *
78
+ 78 * await updateManyToManyRelationship(null, deps);
79
+ 79 * // All article_tags records for article 100 are deleted
80
+ 80 *
81
+ 81 * @description
82
+ 82 * Used by:
83
+ 83 * - relationship-processor.js calls this for many-to-many updates
84
+ 84 * - Used in PATCH operations to sync relationships
85
+ 85 * - Also used in PUT operations (replaces deleteExistingPivotRecords pattern)
86
+ 86 *
87
+ 87 * Purpose:
88
+ 88 * - Intelligently syncs pivot table to match desired state
89
+ 89 * - Preserves existing pivot records that should remain (with their metadata)
90
+ 90 * - Only deletes records that should be removed
91
+ 91 * - Only creates records that are new
92
+ 92 * - Much better than delete-all-then-recreate pattern
93
+ 93 *
94
+ 94 * Data flow:
95
+ 95 * 1. Queries existing pivot records for the resource
96
+ 96 * 2. Compares existing IDs with desired IDs
97
+ 97 * 3. Calculates which to delete and which to add
98
+ 98 * 4. Validates new related resources exist (optional)
99
+ 99 * 5. Performs bulk delete for removed relationships
100
+ 100 * 6. Performs bulk insert for new relationships
101
+ 101 * 7. Records that exist in both are untouched (metadata preserved)
102
+ 102 */
103
+ 103 export const updateManyToManyRelationship = async (scope, deps) => {
104
+ 104 // Extract values from deps
105
+ 105 const { api, context } = deps
106
+ 106 const { resourceId, relDef, relData, transaction: trx } = context
107
+ 107
108
+ 108 // Get the knex instance from the pivot scope
109
+ 109 const pivotScope = api.resources[relDef.through]
110
+ 110 if (!pivotScope) {
111
+ 111 throw new Error(`Pivot table resource '${relDef.through}' not found`)
112
+ 112 }
113
+ 113
114
+ 114 // Get the actual database table name (might be different from scope name)
115
+ 115 const tableName = pivotScope.vars.schemaInfo.tableName || relDef.through
116
+ 116
117
+ 117 // Get existing pivot records directly from database
118
+ 118 const existingRecords = await trx(tableName)
119
+ 119 .where(relDef.foreignKey, resourceId)
120
+ 120 .select(relDef.otherKey)
121
+ 121
122
+ 122 // Create sets for efficient comparison
123
+ 123 const existingIds = new Set(existingRecords.map(r => String(r[relDef.otherKey])))
124
+ 124 const newIds = new Set(relData.map(r => String(r.id)))
125
+ 125
126
+ 126 // Determine what to delete and add
127
+ 127 const toDelete = [...existingIds].filter(id => !newIds.has(id))
128
+ 128 const toAdd = [...newIds].filter(id => !existingIds.has(id))
129
+ 129
130
+ 130 // Validate related resources exist if needed (do this before any changes)
131
+ 131 if (relDef.validateExists !== false && toAdd.length > 0) {
132
+ 132 for (const relatedId of toAdd) {
133
+ 133 const related = relData.find(r => String(r.id) === relatedId)
134
+ 134 try {
135
+ 135 await api.resources[related.type].get({
136
+ 136 id: related.id,
137
+ 137 transaction: trx
138
+ 138 })
139
+ 139 } catch (error) {
140
+ 140 throw new RestApiResourceError(
141
+ 141 `Related ${related.type} with id ${related.id} not found`,
142
+ 142 {
143
+ 143 subtype: 'not_found',
144
+ 144 resourceType: related.type,
145
+ 145 resourceId: related.id
146
+ 146 }
147
+ 147 )
148
+ 148 }
149
+ 149 }
150
+ 150 }
151
+ 151
152
+ 152 // Bulk delete records that should be removed
153
+ 153 if (toDelete.length > 0) {
154
+ 154 await trx(tableName)
155
+ 155 .where(relDef.foreignKey, resourceId)
156
+ 156 .whereIn(relDef.otherKey, toDelete)
157
+ 157 .delete()
158
+ 158 }
159
+ 159
160
+ 160 // Bulk insert new records
161
+ 161 if (toAdd.length > 0) {
162
+ 162 const recordsToInsert = toAdd.map(relatedId => ({
163
+ 163 [relDef.foreignKey]: resourceId,
164
+ 164 [relDef.otherKey]: relatedId
165
+ 165 }))
166
+ 166
167
+ 167 await trx(tableName).insert(recordsToInsert)
168
+ 168 }
169
+ 169
170
+ 170 // Records that exist in both are automatically preserved with their pivot data
171
+ 171 }
172
+ 172
173
+ 173 // Note: deleteExistingPivotRecords has been removed in favor of using
174
+ 174 // updateManyToManyRelationship for all sync operations (including PUT).
175
+ 175 // This aligns with industry standards where ORMs use intelligent sync
176
+ 176 // rather than delete-all-then-recreate patterns.
177
+ 177
178
+ 178 /**
179
+ 179 * Creates new pivot table records for many-to-many relationships
180
+ 180 *
181
+ 181 * @param {Object} api - The API instance with access to resources
182
+ 182 * @param {string|number} resourceId - The ID of the primary resource
183
+ 183 * @param {Object} relDef - The relationship definition
184
+ 184 * @param {Array} relData - Array of related resources to link
185
+ 185 * @param {Object} trx - Database transaction object
186
+ 186 * @returns {Promise<void>}
187
+ 187 *
188
+ 188 * @example
189
+ 189 * // Input: Create article-tag relationships
190
+ 190 * const relDef = {
191
+ 191 * through: 'article_tags',
192
+ 192 * foreignKey: 'article_id',
193
+ 193 * otherKey: 'tag_id'
194
+ 194 * };
195
+ 195 * const relData = [
196
+ 196 * { type: 'tags', id: '10' },
197
+ 197 * { type: 'tags', id: '20' },
198
+ 198 * { type: 'tags', id: '30' }
199
+ 199 * ];
200
+ 200 *
201
+ 201 * await createPivotRecords(api, '100', relDef, relData, trx);
202
+ 202 *
203
+ 203 * // Result: 3 new records in article_tags table
204
+ 204 * // article_id | tag_id
205
+ 205 * // 100 | 10
206
+ 206 * // 100 | 20
207
+ 207 * // 100 | 30
208
+ 208 *
209
+ 209 * @example
210
+ 210 * // Input: Validation ensures related resources exist
211
+ 211 * const relData = [
212
+ 212 * { type: 'tags', id: '999' } // Non-existent tag
213
+ 213 * ];
214
+ 214 *
215
+ 215 * try {
216
+ 216 * await createPivotRecords(api, '100', relDef, relData, trx);
217
+ 217 * } catch (error) {
218
+ 218 * console.log(error.message);
219
+ 219 * // "Related tags with id 999 not found"
220
+ 220 * // Transaction rolled back, no records created
221
+ 221 * }
222
+ 222 *
223
+ 223 * @example
224
+ 224 * // Input: Skip validation for performance
225
+ 225 * const relDef = {
226
+ 226 * through: 'user_permissions',
227
+ 227 * foreignKey: 'user_id',
228
+ 228 * otherKey: 'permission_id',
229
+ 229 * validateExists: false // Skip GET requests
230
+ 230 * };
231
+ 231 *
232
+ 232 * // With 100 permissions, saves 100 GET requests
233
+ 233 * await createPivotRecords(api, userId, relDef, permissions, trx);
234
+ 234 *
235
+ 235 * // Risk: Could create orphaned relationships if permissions don't exist
236
+ 236 * // Benefit: Much faster for bulk operations when you trust the data
237
+ 237 *
238
+ 238 * @description
239
+ 239 * Used by:
240
+ 240 * - relationship-processor.js for POST operations
241
+ 241 * - updateManyToManyRelationship internally for new relationships
242
+ 242 * - Any code that needs to create pivot records
243
+ 243 *
244
+ 244 * Purpose:
245
+ 245 * - Creates pivot table records to link resources
246
+ 246 * - Validates related resources exist by default (referential integrity)
247
+ 247 * - Supports bulk insert for efficiency
248
+ 248 * - Works within transactions for atomicity
249
+ 249 * - Allows skipping validation when performance matters
250
+ 250 *
251
+ 251 * Data flow:
252
+ 252 * 1. Validates pivot table resource exists
253
+ 253 * 2. Gets actual database table name
254
+ 254 * 3. Optionally validates each related resource exists (GET requests)
255
+ 255 * 4. Prepares bulk insert data with foreign keys
256
+ 256 * 5. Performs single INSERT with all records
257
+ 257 * 6. Returns (no data returned, throws on error)
258
+ 258 */
259
+ 259 export const createPivotRecords = async (api, resourceId, relDef, relData, trx) => {
260
+ 260 if (relData.length === 0) return // Early exit if nothing to create
261
+ 261
262
+ 262 // Get pivot table info
263
+ 263 const pivotScope = api.resources[relDef.through]
264
+ 264 if (!pivotScope) {
265
+ 265 throw new Error(`Pivot table resource '${relDef.through}' not found`)
266
+ 266 }
267
+ 267
268
+ 268 // Get the actual database table name (might be different from scope name)
269
+ 269 const tableName = pivotScope.vars.schemaInfo.tableName || relDef.through
270
+ 270
271
+ 271 // Validate all related resources exist if needed (do this before any inserts)
272
+ 272 if (relDef.validateExists !== false) {
273
+ 273 for (const related of relData) {
274
+ 274 try {
275
+ 275 await api.resources[related.type].get({
276
+ 276 id: related.id,
277
+ 277 transaction: trx
278
+ 278 })
279
+ 279 } catch (error) {
280
+ 280 throw new RestApiResourceError(
281
+ 281 `Related ${related.type} with id ${related.id} not found`,
282
+ 282 {
283
+ 283 subtype: 'not_found',
284
+ 284 resourceType: related.type,
285
+ 285 resourceId: related.id
286
+ 286 }
287
+ 287 )
288
+ 288 }
289
+ 289 }
290
+ 290 }
291
+ 291
292
+ 292 // Prepare records for bulk insert
293
+ 293 const recordsToInsert = relData.map(related => ({
294
+ 294 [relDef.foreignKey]: resourceId,
295
+ 295 [relDef.otherKey]: related.id
296
+ 296 }))
297
+ 297
298
+ 298 // Bulk insert all pivot records in a single query
299
+ 299 await trx(tableName).insert(recordsToInsert)
300
+ 300 }
@@ -0,0 +1,245 @@
1
+ 1 import { RestApiValidationError } from '../../../../lib/rest-api-errors.js'
2
+ 2
3
+ 3 /**
4
+ 4 * Processes JSON:API relationship data and converts to database operations
5
+ 5 *
6
+ 6 * @param {Object} scope - The scope object containing schema info
7
+ 7 * @param {Object} deps - Dependencies object
8
+ 8 * @returns {Object} Object with belongsToUpdates and manyToManyRelationships arrays
9
+ 9 *
10
+ 10 * @example
11
+ 11 * // Input: Simple belongsTo relationship
12
+ 12 * const inputRecord = {
13
+ 13 * data: {
14
+ 14 * type: 'articles',
15
+ 15 * attributes: { title: 'My Article' },
16
+ 16 * relationships: {
17
+ 17 * author: {
18
+ 18 * data: { type: 'users', id: '123' }
19
+ 19 * },
20
+ 20 * category: {
21
+ 21 * data: { type: 'categories', id: '5' }
22
+ 22 * }
23
+ 23 * }
24
+ 24 * }
25
+ 25 * };
26
+ 26 *
27
+ 27 * // Schema has:
28
+ 28 * // author_id: { belongsTo: 'users', as: 'author' }
29
+ 29 * // category_id: { belongsTo: 'categories', as: 'category' }
30
+ 30 *
31
+ 31 * const result = processRelationships(scope, { context: { inputRecord } });
32
+ 32 *
33
+ 33 * // Output: Foreign keys extracted
34
+ 34 * // {
35
+ 35 * // belongsToUpdates: {
36
+ 36 * // author_id: '123',
37
+ 37 * // category_id: '5'
38
+ 38 * // },
39
+ 39 * // manyToManyRelationships: []
40
+ 40 * // }
41
+ 41 *
42
+ 42 * @example
43
+ 43 * // Input: Polymorphic relationship
44
+ 44 * const inputRecord = {
45
+ 45 * data: {
46
+ 46 * type: 'comments',
47
+ 47 * relationships: {
48
+ 48 * commentable: {
49
+ 49 * data: { type: 'posts', id: '456' }
50
+ 50 * }
51
+ 51 * }
52
+ 52 * }
53
+ 53 * };
54
+ 54 *
55
+ 55 * // Schema relationships has:
56
+ 56 * // commentable: {
57
+ 57 * // belongsToPolymorphic: {
58
+ 58 * // types: ['posts', 'videos'],
59
+ 59 * // typeField: 'commentable_type',
60
+ 60 * // idField: 'commentable_id'
61
+ 61 * // }
62
+ 62 * // }
63
+ 63 *
64
+ 64 * const result = processRelationships(scope, { context: { inputRecord } });
65
+ 65 *
66
+ 66 * // Output: Both type and id fields set
67
+ 67 * // {
68
+ 68 * // belongsToUpdates: {
69
+ 69 * // commentable_type: 'posts',
70
+ 70 * // commentable_id: '456'
71
+ 71 * // },
72
+ 72 * // manyToManyRelationships: []
73
+ 73 * // }
74
+ 74 *
75
+ 75 * @example
76
+ 76 * // Input: Many-to-many relationship
77
+ 77 * const inputRecord = {
78
+ 78 * data: {
79
+ 79 * type: 'articles',
80
+ 80 * relationships: {
81
+ 81 * tags: {
82
+ 82 * data: [
83
+ 83 * { type: 'tags', id: '10' },
84
+ 84 * { type: 'tags', id: '20' }
85
+ 85 * ]
86
+ 86 * }
87
+ 87 * }
88
+ 88 * }
89
+ 89 * };
90
+ 90 *
91
+ 91 * // Schema relationships has:
92
+ 92 * // tags: {
93
+ 93 * // type: 'manyToMany',
94
+ 94 * // through: 'article_tags',
95
+ 95 * // foreignKey: 'article_id',
96
+ 96 * // otherKey: 'tag_id'
97
+ 97 * // }
98
+ 98 *
99
+ 99 * const result = processRelationships(scope, { context: { inputRecord } });
100
+ 100 *
101
+ 101 * // Output: Many-to-many data collected
102
+ 102 * // {
103
+ 103 * // belongsToUpdates: {},
104
+ 104 * // manyToManyRelationships: [{
105
+ 105 * // relName: 'tags',
106
+ 106 * // relDef: {
107
+ 107 * // through: 'article_tags',
108
+ 108 * // foreignKey: 'article_id',
109
+ 109 * // otherKey: 'tag_id'
110
+ 110 * // },
111
+ 111 * // relData: [
112
+ 112 * // { type: 'tags', id: '10' },
113
+ 113 * // { type: 'tags', id: '20' }
114
+ 114 * // ]
115
+ 115 * // }]
116
+ 116 * // }
117
+ 117 *
118
+ 118 * @example
119
+ 119 * // Input: Clearing relationships
120
+ 120 * const inputRecord = {
121
+ 121 * data: {
122
+ 122 * relationships: {
123
+ 123 * author: { data: null }, // Clear belongsTo
124
+ 124 * tags: { data: [] } // Clear many-to-many
125
+ 125 * }
126
+ 126 * }
127
+ 127 * };
128
+ 128 *
129
+ 129 * const result = processRelationships(scope, { context: { inputRecord } });
130
+ 130 *
131
+ 131 * // Output: Null for belongsTo, empty array for many-to-many
132
+ 132 * // {
133
+ 133 * // belongsToUpdates: { author_id: null },
134
+ 134 * // manyToManyRelationships: [{
135
+ 135 * // relName: 'tags',
136
+ 136 * // relDef: { ... },
137
+ 137 * // relData: [] // Will delete all pivot records
138
+ 138 * // }]
139
+ 139 * // }
140
+ 140 *
141
+ 141 * @description
142
+ 142 * Used by:
143
+ 143 * - rest-api-knex-plugin's dataPut and dataPatch methods
144
+ 144 * - Called before database writes to prepare relationship updates
145
+ 145 *
146
+ 146 * Purpose:
147
+ 147 * - Converts JSON:API relationship format to database operations
148
+ 148 * - Handles all relationship types: belongsTo, polymorphic, many-to-many
149
+ 149 * - Validates polymorphic types against allowed values
150
+ 150 * - Separates concerns: foreign keys vs pivot table operations
151
+ 151 *
152
+ 152 * Data flow:
153
+ 153 * 1. Receives JSON:API document with relationships section
154
+ 154 * 2. Analyzes schema to determine relationship types
155
+ 155 * 3. For belongsTo: extracts foreign key values
156
+ 156 * 4. For polymorphic: extracts both type and id fields
157
+ 157 * 5. For many-to-many: collects data for pivot operations
158
+ 158 * 6. Returns structured data for database updates
159
+ 159 */
160
+ 160 export const processRelationships = (scope, deps) => {
161
+ 161 // Extract values from scope
162
+ 162 const {
163
+ 163 vars: {
164
+ 164 schemaInfo: { schemaStructure: schemaFields, schemaRelationships: relationships }
165
+ 165 }
166
+ 166 } = scope
167
+ 167
168
+ 168 // Extract values from deps
169
+ 169 const { context } = deps
170
+ 170 const { inputRecord } = context
171
+ 171 const belongsToUpdates = {}
172
+ 172 const manyToManyRelationships = []
173
+ 173
174
+ 174 if (!inputRecord.data.relationships) {
175
+ 175 return { belongsToUpdates, manyToManyRelationships }
176
+ 176 }
177
+ 177
178
+ 178 for (const [relName, relData] of Object.entries(inputRecord.data.relationships)) {
179
+ 179 const relDef = relationships?.[relName]
180
+ 180
181
+ 181 // Find the schema field that defines this relationship
182
+ 182 const schemaField = Object.entries(schemaFields).find(([fieldName, fieldDef]) =>
183
+ 183 fieldDef.as === relName
184
+ 184 )
185
+ 185
186
+ 186 if (schemaField) {
187
+ 187 const [fieldName, fieldDef] = schemaField
188
+ 188
189
+ 189 // Handle regular belongsTo (1:1)
190
+ 190 if (fieldDef.belongsTo) {
191
+ 191 if (relData.data === null) {
192
+ 192 belongsToUpdates[fieldName] = null
193
+ 193 } else if (relData.data?.id) {
194
+ 194 belongsToUpdates[fieldName] = relData.data.id
195
+ 195 }
196
+ 196 }
197
+ 197 }
198
+ 198
199
+ 199 // Check for polymorphic belongsTo defined in relationships object (not as schema field)
200
+ 200 if (!schemaField && relDef?.belongsToPolymorphic && relData.data !== undefined) {
201
+ 201 if (relData.data === null) {
202
+ 202 const { typeField, idField } = relDef.belongsToPolymorphic
203
+ 203 belongsToUpdates[typeField] = null
204
+ 204 belongsToUpdates[idField] = null
205
+ 205 } else if (relData.data) {
206
+ 206 const { type, id } = relData.data
207
+ 207 const { types, typeField, idField } = relDef.belongsToPolymorphic
208
+ 208
209
+ 209 // Validate type is allowed
210
+ 210 if (!types.includes(type)) {
211
+ 211 throw new RestApiValidationError(
212
+ 212 `Invalid type '${type}' for polymorphic relationship '${relName}'. Allowed types: ${types.join(', ')}`,
213
+ 213 {
214
+ 214 fields: [`data.relationships.${relName}.data.type`],
215
+ 215 violations: [{
216
+ 216 field: `data.relationships.${relName}.data.type`,
217
+ 217 rule: 'polymorphic_type',
218
+ 218 message: `Type must be one of: ${types.join(', ')}`
219
+ 219 }]
220
+ 220 }
221
+ 221 )
222
+ 222 }
223
+ 223
224
+ 224 belongsToUpdates[typeField] = type
225
+ 225 belongsToUpdates[idField] = id
226
+ 226 }
227
+ 227 }
228
+ 228
229
+ 229 // Check relationship type and process accordingly
230
+ 230 if (relDef?.type === 'manyToMany' && relData.data !== undefined) {
231
+ 231 manyToManyRelationships.push({
232
+ 232 relName,
233
+ 233 relDef: {
234
+ 234 through: relDef.through,
235
+ 235 foreignKey: relDef.foreignKey,
236
+ 236 otherKey: relDef.otherKey
237
+ 237 },
238
+ 238 relData: relData.data || [] // null means empty array for many-to-many
239
+ 239 })
240
+ 240 }
241
+ 241 // Note: hasOne and hasMany don't need processing here as they don't update the current record
242
+ 242 }
243
+ 243
244
+ 244 return { belongsToUpdates, manyToManyRelationships }
245
+ 245 }