json-rest-api 1.0.21 → 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 +3 -3
@@ -0,0 +1,188 @@
1
+ 1 import { requirePackage } from 'hooked-api'
2
+ 2
3
+ 3 let generateKeyBetween
4
+ 4 try {
5
+ 5 ({ generateKeyBetween } = await import('fractional-indexing'))
6
+ 6 } catch (e) {
7
+ 7 requirePackage('fractional-indexing', 'positioning',
8
+ 8 'Fractional indexing is required for the positioning plugin to generate sortable position keys. This is a peer dependency.')
9
+ 9 }
10
+ 10
11
+ 11 /**
12
+ 12 * Helper functions for fractional positioning
13
+ 13 * Uses the fractional-indexing library for generating position keys
14
+ 14 */
15
+ 15
16
+ 16 /**
17
+ 17 * Calculate position for a new/moved item based on beforeId
18
+ 18 * @param {Array} items - Array of items with position field
19
+ 19 * @param {string} beforeId - ID to position before (null = last)
20
+ 20 * @param {string} idField - Name of the ID field
21
+ 21 * @param {string} positionField - Name of the position field
22
+ 22 * @returns {string} New position key
23
+ 23 */
24
+ 24 export function calculatePosition (items, beforeId, idField, positionField) {
25
+ 25 // Sort items by position using simple string comparison
26
+ 26 // Important: fractional-indexing expects ASCII/Unicode ordering, not locale-specific
27
+ 27 const sorted = [...items].sort((a, b) => {
28
+ 28 const posA = a[positionField] || ''
29
+ 29 const posB = b[positionField] || ''
30
+ 30 return posA < posB ? -1 : posA > posB ? 1 : 0
31
+ 31 })
32
+ 32
33
+ 33 // If no items, start in the middle
34
+ 34 if (sorted.length === 0) {
35
+ 35 return generateKeyBetween(null, null) // Returns 'a0'
36
+ 36 }
37
+ 37
38
+ 38 // Position at end
39
+ 39 if (beforeId === null || beforeId === undefined) {
40
+ 40 const lastItem = sorted[sorted.length - 1]
41
+ 41 return generateKeyBetween(lastItem[positionField], null)
42
+ 42 }
43
+ 43
44
+ 44 // Find the item to position before
45
+ 45 const beforeIndex = sorted.findIndex(item => String(item[idField]) === String(beforeId))
46
+ 46
47
+ 47 // If beforeId not found, position at end
48
+ 48 if (beforeIndex === -1) {
49
+ 49 const lastItem = sorted[sorted.length - 1]
50
+ 50 return generateKeyBetween(lastItem[positionField], null)
51
+ 51 }
52
+ 52
53
+ 53 // Position before the found item
54
+ 54 const beforeItem = sorted[beforeIndex]
55
+ 55 const prevItem = beforeIndex > 0 ? sorted[beforeIndex - 1] : null
56
+ 56
57
+ 57 const result = generateKeyBetween(
58
+ 58 prevItem ? prevItem[positionField] : null,
59
+ 59 beforeItem[positionField]
60
+ 60 )
61
+ 61
62
+ 62 return result
63
+ 63 }
64
+ 64
65
+ 65 /**
66
+ 66 * Initialize position for first item in a group
67
+ 67 * @returns {string} Initial position key
68
+ 68 */
69
+ 69 export function getInitialPosition () {
70
+ 70 return generateKeyBetween(null, null) // Returns 'a0'
71
+ 71 }
72
+ 72
73
+ 73 /**
74
+ 74 * Rebalance positions if they get too long (optional maintenance)
75
+ 75 * @param {Array} items - Array of items to rebalance
76
+ 76 * @param {string} positionField - Name of the position field
77
+ 77 * @param {number} maxLength - Maximum position string length before rebalancing
78
+ 78 * @returns {Array} Items with new positions (or original if no rebalance needed)
79
+ 79 */
80
+ 80 export function rebalancePositions (items, positionField, maxLength = 50) {
81
+ 81 // Check if rebalancing is needed
82
+ 82 const needsRebalance = items.some(item =>
83
+ 83 item[positionField] && item[positionField].length > maxLength
84
+ 84 )
85
+ 85
86
+ 86 if (!needsRebalance) {
87
+ 87 return items
88
+ 88 }
89
+ 89
90
+ 90 // Sort by current position
91
+ 91 const sorted = [...items].sort((a, b) => {
92
+ 92 const posA = a[positionField] || ''
93
+ 93 const posB = b[positionField] || ''
94
+ 94 return posA < posB ? -1 : posA > posB ? 1 : 0
95
+ 95 })
96
+ 96
97
+ 97 // Generate evenly spaced positions
98
+ 98 const rebalanced = []
99
+ 99 let prevKey = null
100
+ 100
101
+ 101 for (let i = 0; i < sorted.length; i++) {
102
+ 102 // For even spacing, we generate keys sequentially
103
+ 103 const newPosition = generateKeyBetween(prevKey, null)
104
+ 104
105
+ 105 rebalanced.push({
106
+ 106 ...sorted[i],
107
+ 107 [positionField]: newPosition
108
+ 108 })
109
+ 109
110
+ 110 prevKey = newPosition
111
+ 111 }
112
+ 112
113
+ 113 return rebalanced
114
+ 114 }
115
+ 115
116
+ 116 /**
117
+ 117 * Check if a position value is valid
118
+ 118 * @param {string} position - Position value to check
119
+ 119 * @returns {boolean} True if valid
120
+ 120 */
121
+ 121 export function isValidPosition (position) {
122
+ 122 if (!position || typeof position !== 'string') {
123
+ 123 return false
124
+ 124 }
125
+ 125
126
+ 126 // Fractional keys should match pattern: lowercase letters and digits
127
+ 127 return /^[a-z0-9]+$/i.test(position)
128
+ 128 }
129
+ 129
130
+ 130 /**
131
+ 131 * Get items that need position assignment (for migration)
132
+ 132 * @param {Array} items - Array of items
133
+ 133 * @param {string} positionField - Name of the position field
134
+ 134 * @returns {Array} Items without valid positions
135
+ 135 */
136
+ 136 export function getUnpositionedItems (items, positionField) {
137
+ 137 return items.filter(item => !isValidPosition(item[positionField]))
138
+ 138 }
139
+ 139
140
+ 140 /**
141
+ 141 * Assign initial positions to items that don't have them
142
+ 142 * @param {Array} items - Array of items
143
+ 143 * @param {string} positionField - Name of the position field
144
+ 144 * @param {string} orderByField - Optional field to order by initially
145
+ 145 * @returns {Array} Items with positions assigned
146
+ 146 */
147
+ 147 export function assignInitialPositions (items, positionField, orderByField = null) {
148
+ 148 const positioned = items.filter(item => isValidPosition(item[positionField]))
149
+ 149 const unpositioned = items.filter(item => !isValidPosition(item[positionField]))
150
+ 150
151
+ 151 if (unpositioned.length === 0) {
152
+ 152 return items
153
+ 153 }
154
+ 154
155
+ 155 // Sort unpositioned items if orderBy field provided
156
+ 156 if (orderByField) {
157
+ 157 unpositioned.sort((a, b) => {
158
+ 158 const valA = a[orderByField]
159
+ 159 const valB = b[orderByField]
160
+ 160 if (valA < valB) return -1
161
+ 161 if (valA > valB) return 1
162
+ 162 return 0
163
+ 163 })
164
+ 164 }
165
+ 165
166
+ 166 // Get the last positioned item
167
+ 167 const sortedPositioned = [...positioned].sort((a, b) => {
168
+ 168 const posA = a[positionField] || ''
169
+ 169 const posB = b[positionField] || ''
170
+ 170 return posA < posB ? -1 : posA > posB ? 1 : 0
171
+ 171 })
172
+ 172 const lastPositioned = sortedPositioned[sortedPositioned.length - 1]
173
+ 173
174
+ 174 let lastKey = lastPositioned ? lastPositioned[positionField] : null
175
+ 175
176
+ 176 // Assign positions to unpositioned items
177
+ 177 const newlyPositioned = unpositioned.map(item => {
178
+ 178 const newKey = generateKeyBetween(lastKey, null)
179
+ 179 lastKey = newKey
180
+ 180
181
+ 181 return {
182
+ 182 ...item,
183
+ 183 [positionField]: newKey
184
+ 184 }
185
+ 185 })
186
+ 186
187
+ 187 return [...positioned, ...newlyPositioned]
188
+ 188 }
@@ -0,0 +1,336 @@
1
+ 1 import { createSchema } from 'json-rest-schema'
2
+ 2 import { ensureSearchFieldsAreIndexed, generateSearchSchemaFromSchema, sortFieldsByDependencies } from './schema-helpers.js'
3
+ 3
4
+ 4 /**
5
+ 5 * Compiles and enriches schemas for a resource scope
6
+ 6 *
7
+ 7 * @param {Object} scope - Scope containing raw schema options
8
+ 8 * @param {Object} deps - Dependencies with context and hooks
9
+ 9 * @returns {Promise<void>} Resolves when compilation complete
10
+ 10 *
11
+ 11 * @example
12
+ 12 * // Input: Raw schema with computed field
13
+ 13 * const rawFields = {
14
+ 14 * title: { type: 'string', required: true },
15
+ 15 * content: { type: 'string' },
16
+ 16 * author_id: { belongsTo: 'users', as: 'author' }, // Missing type
17
+ 17 * word_count: {
18
+ 18 * type: 'number',
19
+ 19 * computed: true,
20
+ 20 * compute: (record) => record.content.split(' ').length
21
+ 21 * }
22
+ 22 * };
23
+ 23 *
24
+ 24 * await compileSchemas(scope, deps);
25
+ 25 *
26
+ 26 * // Output in scope.vars.schemaInfo:
27
+ 27 * // {
28
+ 28 * // schema: Schema {}, // json-rest-schema instance
29
+ 29 * // schemaStructure: {
30
+ 30 * // title: { type: 'string', required: true },
31
+ 31 * // content: { type: 'string' },
32
+ 32 * // author_id: { type: 'id', belongsTo: 'users', as: 'author' } // Type added!
33
+ 33 * // // Note: word_count removed (it's computed)
34
+ 34 * // },
35
+ 35 * // computed: {
36
+ 36 * // word_count: { type: 'number', computed: true, compute: [Function] }
37
+ 37 * // }
38
+ 38 * // }
39
+ 39 *
40
+ 40 * @example
41
+ 41 * // Input: Schema with search fields
42
+ 42 * const rawFields = {
43
+ 43 * title: { type: 'string', search: true }, // Searchable
44
+ 44 * status: { type: 'string' }, // Not searchable
45
+ 45 * author_id: { belongsTo: 'users', as: 'author' }
46
+ 46 * };
47
+ 47 *
48
+ 48 * // Output: Auto-generated searchSchema
49
+ 49 * // searchSchemaObject contains:
50
+ 50 * // {
51
+ 51 * // title: {
52
+ 52 * // type: 'string',
53
+ 53 * // indexed: true, // Added for DB optimization
54
+ 54 * // filterOperator: '=' // Default operator
55
+ 55 * // }
56
+ 56 * // }
57
+ 57 *
58
+ 58 * @example
59
+ 59 * // Input: Pivot table auto-detection
60
+ 60 * const pivotSchema = {
61
+ 61 * article_id: { belongsTo: 'articles', as: 'article' },
62
+ 62 * tag_id: { belongsTo: 'tags', as: 'tag' },
63
+ 63 * display_order: { type: 'number' }
64
+ 64 * };
65
+ 65 *
66
+ 66 * // Output: Both foreign keys become searchable
67
+ 67 * // schemaStructure will have:
68
+ 68 * // {
69
+ 69 * // article_id: { type: 'id', belongsTo: 'articles', as: 'article', search: true },
70
+ 70 * // tag_id: { type: 'id', belongsTo: 'tags', as: 'tag', search: true },
71
+ 71 * // display_order: { type: 'number' }
72
+ 72 * // }
73
+ 73 * // This enables efficient many-to-many filtering!
74
+ 74 *
75
+ 75 * @example
76
+ 76 * // Input: Schema with dependent getters
77
+ 77 * const schema = {
78
+ 78 * first_name: { type: 'string' },
79
+ 79 * last_name: { type: 'string' },
80
+ 80 * full_name: {
81
+ 81 * type: 'string',
82
+ 82 * getter: (record) => `${record.first_name} ${record.last_name}`,
83
+ 83 * runGetterAfter: ['first_name', 'last_name'] // Dependencies
84
+ 84 * },
85
+ 85 * email: {
86
+ 86 * type: 'string',
87
+ 87 * setter: (value) => value.toLowerCase().trim()
88
+ 88 * }
89
+ 89 * };
90
+ 90 *
91
+ 91 * // Output includes dependency-sorted fields:
92
+ 92 * // {
93
+ 93 * // fieldGetters: { full_name: { getter: [Function], runGetterAfter: [...] } },
94
+ 94 * // sortedGetterFields: ['full_name'], // Topologically sorted
95
+ 95 * // fieldSetters: { email: { setter: [Function], runSetterAfter: [] } },
96
+ 96 * // sortedSetterFields: ['email']
97
+ 97 * // }
98
+ 98 *
99
+ 99 * @description
100
+ 100 * Used by:
101
+ 101 * - Every REST method calls this before any operations
102
+ 102 * - Compilation happens once per scope (cached)
103
+ 103 *
104
+ 104 * Purpose:
105
+ 105 * - Separates computed fields from database fields
106
+ 106 * - Adds missing types to belongsTo relationships
107
+ 107 * - Auto-detects pivot tables for many-to-many
108
+ 108 * - Generates search schemas from field markers
109
+ 109 * - Validates getter/setter dependencies
110
+ 110 * - Provides hooks for schema enrichment
111
+ 111 * - Caches everything for performance
112
+ 112 *
113
+ 113 * Data flow:
114
+ 114 * 1. Extracts computed fields to separate object
115
+ 115 * 2. Adds type:'id' to belongsTo fields
116
+ 116 * 3. Detects pivot tables (2+ foreign keys, 40%+ of fields)
117
+ 117 * 4. Runs schema:enrich hook for plugins
118
+ 118 * 5. Creates json-rest-schema validation instance
119
+ 119 * 6. Generates searchSchema from search:true fields
120
+ 120 * 7. Runs searchSchema:enrich hook
121
+ 121 * 8. Extracts and topologically sorts getters/setters
122
+ 122 * 9. Caches all results in scope.vars.schemaInfo
123
+ 123 */
124
+ 124 export async function compileSchemas (scope, deps) {
125
+ 125 // Extract scopeName from context
126
+ 126 const { context, runHooks } = deps
127
+ 127 const scopeName = context.scopeName
128
+ 128
129
+ 129 // Get raw schema
130
+ 130 const rawFields = scope.scopeOptions?.schema || {}
131
+ 131
132
+ 132 // Extract computed fields from schema and build enriched schema
133
+ 133 const computedFields = {}
134
+ 134 const enrichedFields = {}
135
+ 135
136
+ 136 for (const [fieldName, fieldDef] of Object.entries(rawFields)) {
137
+ 137 if (fieldDef.computed === true) {
138
+ 138 // Extract computed field - copy entire definition
139
+ 139 computedFields[fieldName] = { ...fieldDef }
140
+ 140
141
+ 141 // Validate computed field
142
+ 142 if (!fieldDef.type) {
143
+ 143 throw new Error(`Computed field '${fieldName}' in scope '${scopeName}' must have a type`)
144
+ 144 }
145
+ 145
146
+ 146 if (fieldDef.compute && typeof fieldDef.compute !== 'function') {
147
+ 147 throw new Error(`Computed field '${fieldName}' in scope '${scopeName}' has invalid compute function`)
148
+ 148 }
149
+ 149
150
+ 150 // Don't include computed fields in the validation schema
151
+ 151 } else {
152
+ 152 enrichedFields[fieldName] = { ...fieldDef }
153
+ 153 }
154
+ 154 }
155
+ 155
156
+ 156 // Default enrichment: Add type to belongsTo fields
157
+ 157 for (const [fieldName, fieldDef] of Object.entries(enrichedFields)) {
158
+ 158 if (fieldDef.belongsTo && !fieldDef.type) {
159
+ 159 fieldDef.type = 'id'
160
+ 160 }
161
+ 161 }
162
+ 162
163
+ 163 // ADD LOGIC TO MAKE FIELDS SEARCHABLE HERE
164
+ 164 // Auto-detect pivot tables and make their foreign key fields searchable
165
+ 165 // This is critical for many-to-many relationship operations which need to filter pivot records
166
+ 166 const fields = Object.entries(enrichedFields)
167
+ 167 const belongsToFields = fields.filter(([_, def]) => def.belongsTo)
168
+ 168
169
+ 169 // If 2+ belongsTo fields and they make up 40%+ of non-id fields, likely a pivot table
170
+ 170 const nonIdFields = fields.filter(([name, _]) => name !== 'id')
171
+ 171 const isProbablyPivot = belongsToFields.length >= 2 &&
172
+ 172 (nonIdFields.length === 0 || belongsToFields.length / nonIdFields.length >= 0.4)
173
+ 173
174
+ 174 if (isProbablyPivot) {
175
+ 175 // Make all belongsTo fields searchable for pivot operations
176
+ 176 for (const [fieldName, fieldDef] of belongsToFields) {
177
+ 177 if (!fieldDef.search) {
178
+ 178 fieldDef.search = true
179
+ 179 }
180
+ 180 }
181
+ 181 }
182
+ 182
183
+ 183 // Hook: schema:enrich
184
+ 184 const schemaContext = {
185
+ 185 fields: enrichedFields, // Mutable
186
+ 186 originalFields: rawFields, // Read-only
187
+ 187 scopeName,
188
+ 188 scopeOptions: scope.scopeOptions || {}
189
+ 189 }
190
+ 190 await runHooks('schema:enrich', schemaContext)
191
+ 191
192
+ 192 // Create schema object
193
+ 193 const schemaObject = createSchema(schemaContext.fields)
194
+ 194
195
+ 195 // Generate searchSchema by merging explicit searchSchema with fields marked search:true.
196
+ 196 // This allows two ways to define searchable fields: either mark fields with search:true
197
+ 197 // in the main schema, or provide an explicit searchSchema with more control over filtering.
198
+ 198 // The explicit searchSchema takes precedence when there are conflicts - it can override
199
+ 199 // fields marked with search:true. Fields with search:true that are NOT in the explicit
200
+ 200 // searchSchema will be added automatically.
201
+ 201 // Example: title: {search: true} auto-generates a searchable field with sensible defaults,
202
+ 202 // while searchSchema can specify filterOperator: 'contains' or complex join configurations.
203
+ 203 const rawSearchFields = generateSearchSchemaFromSchema(
204
+ 204 schemaContext.fields,
205
+ 205 scope.scopeOptions.searchSchema
206
+ 206 )
207
+ 207
208
+ 208 if (rawSearchFields) {
209
+ 209 // Mark all search fields as indexed for database optimization.
210
+ 210 // This ensures that any field used for filtering will have a database index created,
211
+ 211 // dramatically improving query performance. Without indexes, filtering large tables
212
+ 212 // would require full table scans. The storage plugin uses these hints to create indexes.
213
+ 213 // Example: A status field marked for search gets indexed: true, enabling efficient
214
+ 214 // WHERE status = 'published' queries that can use index lookups instead of scanning all rows.
215
+ 215 ensureSearchFieldsAreIndexed(rawSearchFields)
216
+ 216
217
+ 217 // Hook: searchSchema:enrich
218
+ 218 const searchSchemaContext = {
219
+ 219 fields: schemaContext.fields, // Mutable
220
+ 220 originalFields: rawSearchFields, // Read-only enriched schema
221
+ 221 scopeName
222
+ 222 }
223
+ 223 await runHooks('searchSchema:enrich', searchSchemaContext)
224
+ 224
225
+ 225 // Create searchSchema object
226
+ 226 var searchSchemaObject = createSchema(rawSearchFields)
227
+ 227 } else {
228
+ 228 var searchSchemaObject = createSchema({})
229
+ 229 }
230
+ 230
231
+ 231 // Build schemaRelationships including polymorphic fields from schema
232
+ 232 const schemaRelationships = { ...(scope.scopeOptions.relationships || {}) }
233
+ 233
234
+ 234 // Validate belongsTo fields
235
+ 235 for (const [fieldName, fieldDef] of Object.entries(schemaContext.fields)) {
236
+ 236 // Validate that belongsTo fields have 'as' property
237
+ 237 if (fieldDef.belongsTo && !fieldDef.as) {
238
+ 238 throw new Error(
239
+ 239 `Field '${fieldName}' in resource '${scopeName}' has belongsTo: '${fieldDef.belongsTo}' but is missing the required 'as' property. ` +
240
+ 240 'The \'as\' property defines the relationship name used in JSON:API payloads. ' +
241
+ 241 `Example: ${fieldName}: { type: 'number', belongsTo: '${fieldDef.belongsTo}', as: '${fieldName.replace(/_id$/, '')}' }`
242
+ 242 )
243
+ 243 }
244
+ 244 }
245
+ 245
246
+ 246 // Extract and validate getter definitions
247
+ 247 const fieldGetters = {}
248
+ 248 const getterFields = []
249
+ 249
250
+ 250 for (const [fieldName, fieldDef] of Object.entries(schemaContext.fields)) {
251
+ 251 if (fieldDef.getter && typeof fieldDef.getter === 'function') {
252
+ 252 fieldGetters[fieldName] = {
253
+ 253 getter: fieldDef.getter,
254
+ 254 runGetterAfter: fieldDef.runGetterAfter || [],
255
+ 255 fieldDef
256
+ 256 }
257
+ 257 getterFields.push(fieldName)
258
+ 258
259
+ 259 // Validate dependencies exist
260
+ 260 if (fieldDef.runGetterAfter && Array.isArray(fieldDef.runGetterAfter)) {
261
+ 261 for (const dep of fieldDef.runGetterAfter) {
262
+ 262 if (!schemaContext.fields[dep]) {
263
+ 263 throw new Error(
264
+ 264 `Field '${fieldName}' in resource '${scopeName}' has getter dependency '${dep}' that does not exist in schema`
265
+ 265 )
266
+ 266 }
267
+ 267 }
268
+ 268 }
269
+ 269 }
270
+ 270 }
271
+ 271
272
+ 272 // Sort getters by dependencies
273
+ 273 let sortedGetterFields = []
274
+ 274 if (Object.keys(fieldGetters).length > 0) {
275
+ 275 try {
276
+ 276 sortedGetterFields = sortFieldsByDependencies(fieldGetters, 'runGetterAfter')
277
+ 277 } catch (error) {
278
+ 278 throw new Error(`Invalid getter dependencies in ${scopeName}: ${error.message}`)
279
+ 279 }
280
+ 280 }
281
+ 281
282
+ 282 // Extract and validate setter definitions
283
+ 283 const fieldSetters = {}
284
+ 284 const setterFields = []
285
+ 285
286
+ 286 for (const [fieldName, fieldDef] of Object.entries(schemaContext.fields)) {
287
+ 287 if (fieldDef.setter && typeof fieldDef.setter === 'function') {
288
+ 288 fieldSetters[fieldName] = {
289
+ 289 setter: fieldDef.setter,
290
+ 290 runSetterAfter: fieldDef.runSetterAfter || [],
291
+ 291 fieldDef
292
+ 292 }
293
+ 293 setterFields.push(fieldName)
294
+ 294
295
+ 295 // Validate dependencies exist
296
+ 296 if (fieldDef.runSetterAfter && Array.isArray(fieldDef.runSetterAfter)) {
297
+ 297 for (const dep of fieldDef.runSetterAfter) {
298
+ 298 if (!schemaContext.fields[dep]) {
299
+ 299 throw new Error(
300
+ 300 `Field '${fieldName}' in resource '${scopeName}' has setter dependency '${dep}' that does not exist in schema`
301
+ 301 )
302
+ 302 }
303
+ 303 }
304
+ 304 }
305
+ 305 }
306
+ 306 }
307
+ 307
308
+ 308 // Sort setters by dependencies
309
+ 309 let sortedSetterFields = []
310
+ 310 if (Object.keys(fieldSetters).length > 0) {
311
+ 311 try {
312
+ 312 sortedSetterFields = sortFieldsByDependencies(fieldSetters, 'runSetterAfter')
313
+ 313 } catch (error) {
314
+ 314 throw new Error(`Invalid setter dependencies in ${scopeName}: ${error.message}`)
315
+ 315 }
316
+ 316 }
317
+ 317
318
+ 318 // Cache everything
319
+ 319 scope.vars.schemaInfo = {
320
+ 320
321
+ 321 schemaInstance: schemaObject,
322
+ 322 schemaStructure: schemaObject.structure,
323
+ 323
324
+ 324 searchSchemaInstance: searchSchemaObject,
325
+ 325 searchSchemaStructure: searchSchemaObject.structure,
326
+ 326
327
+ 327 computed: computedFields,
328
+ 328 schemaRelationships,
329
+ 329 tableName: scope.scopeOptions.tableName || scopeName,
330
+ 330 idProperty: scope.scopeOptions.idProperty || scope.vars.idProperty,
331
+ 331 fieldGetters,
332
+ 332 sortedGetterFields,
333
+ 333 fieldSetters,
334
+ 334 sortedSetterFields
335
+ 335 }
336
+ 336 }
@@ -0,0 +1,165 @@
1
+ 1 /**
2
+ 2 * Parses URL query strings into JSON:API compliant parameter objects
3
+ 3 *
4
+ 4 * @param {string} queryString - The query string part of URL (without ?)
5
+ 5 * @returns {object} Parsed query parameters in JSON:API format
6
+ 6 *
7
+ 7 * @example
8
+ 8 * // Input: Basic include and sort
9
+ 9 * const query = parseJsonApiQuery('include=author&sort=-created_at');
10
+ 10 *
11
+ 11 * // Output: Parsed into arrays
12
+ 12 * // {
13
+ 13 * // include: ['author'], // Split by comma
14
+ 14 * // fields: {},
15
+ 15 * // filters: {},
16
+ 16 * // sort: ['-created_at'], // Split by comma
17
+ 17 * // page: {}
18
+ 18 * // }
19
+ 19 *
20
+ 20 * @example
21
+ 21 * // Input: Filters with bracket notation
22
+ 22 * const query = parseJsonApiQuery('filter[status]=published&filter[author_id]=123');
23
+ 23 *
24
+ 24 * // Output: Extracted filter keys
25
+ 25 * // {
26
+ 26 * // include: [],
27
+ 27 * // fields: {},
28
+ 28 * // filters: {
29
+ 29 * // status: 'published', // filter[status] → filters.status
30
+ 30 * // author_id: '123' // Values kept as strings
31
+ 31 * // },
32
+ 32 * // sort: [],
33
+ 33 * // page: {}
34
+ 34 * // }
35
+ 35 *
36
+ 36 * @example
37
+ 37 * // Input: Sparse fieldsets
38
+ 38 * const query = parseJsonApiQuery('fields[articles]=title,body&fields[users]=name');
39
+ 39 *
40
+ 40 * // Output: Fields kept as comma-separated strings
41
+ 41 * // {
42
+ 42 * // include: [],
43
+ 43 * // fields: {
44
+ 44 * // articles: 'title,body', // NOT split into array
45
+ 45 * // users: 'name' // Validation expects strings
46
+ 46 * // },
47
+ 47 * // filters: {},
48
+ 48 * // sort: [],
49
+ 49 * // page: {}
50
+ 50 * // }
51
+ 51 *
52
+ 52 * @example
53
+ 53 * // Input: Pagination with numeric conversion
54
+ 54 * const query = parseJsonApiQuery('page[size]=20&page[number]=3&page[cursor]=abc123');
55
+ 55 *
56
+ 56 * // Output: Numbers converted, strings preserved
57
+ 57 * // {
58
+ 58 * // include: [],
59
+ 59 * // fields: {},
60
+ 60 * // filters: {},
61
+ 61 * // sort: [],
62
+ 62 * // page: {
63
+ 63 * // size: 20, // "20" → 20 (numeric)
64
+ 64 * // number: 3, // "3" → 3 (numeric)
65
+ 65 * // cursor: 'abc123' // Non-numeric stays string
66
+ 66 * // }
67
+ 67 * // }
68
+ 68 *
69
+ 69 * @example
70
+ 70 * // Input: Complex real-world query
71
+ 71 * const query = parseJsonApiQuery(
72
+ 72 * 'include=author,comments.user&' +
73
+ 73 * 'filter[status]=published&' +
74
+ 74 * 'filter[created_after]=2024-01-01&' +
75
+ 75 * 'fields[articles]=title,summary&' +
76
+ 76 * 'sort=-created_at,title&' +
77
+ 77 * 'page[size]=10'
78
+ 78 * );
79
+ 79 *
80
+ 80 * // Output: All parameters properly categorized
81
+ 81 * // {
82
+ 82 * // include: ['author', 'comments.user'],
83
+ 83 * // fields: { articles: 'title,summary' },
84
+ 84 * // filters: {
85
+ 85 * // status: 'published',
86
+ 86 * // created_after: '2024-01-01'
87
+ 87 * // },
88
+ 88 * // sort: ['-created_at', 'title'],
89
+ 89 * // page: { size: 10 }
90
+ 90 * // }
91
+ 91 *
92
+ 92 * @description
93
+ 93 * Used by:
94
+ 94 * - express-plugin parses req.query with this
95
+ 95 * - http-plugin parses URL query strings with this
96
+ 96 * - websocket-plugin parses message query parameters with this
97
+ 97 *
98
+ 98 * Purpose:
99
+ 99 * - Provides uniform JSON:API query parsing across all transports
100
+ 100 * - Handles bracket notation without regex complexity
101
+ 101 * - Converts data types appropriately (numbers for pagination)
102
+ 102 * - Ignores non-JSON:API parameters gracefully
103
+ 103 * - Returns consistent structure even for empty input
104
+ 104 *
105
+ 105 * Data flow:
106
+ 106 * 1. Uses URLSearchParams for reliable parsing
107
+ 107 * 2. Categorizes parameters by their prefix pattern
108
+ 108 * 3. Extracts bracketed keys (filter[x] → x)
109
+ 109 * 4. Splits comma-separated values for include/sort
110
+ 110 * 5. Converts numeric strings for pagination
111
+ 111 * 6. Returns normalized structure for REST API
112
+ 112 */
113
+ 113 export function parseJsonApiQuery (queryString) {
114
+ 114 if (!queryString) {
115
+ 115 return {
116
+ 116 include: [],
117
+ 117 fields: {},
118
+ 118 filters: {},
119
+ 119 sort: [],
120
+ 120 page: {}
121
+ 121 }
122
+ 122 }
123
+ 123
124
+ 124 const params = new URLSearchParams(queryString)
125
+ 125 const result = {
126
+ 126 include: [],
127
+ 127 fields: {},
128
+ 128 filters: {},
129
+ 129 sort: [],
130
+ 130 page: {}
131
+ 131 }
132
+ 132
133
+ 133 for (const [key, value] of params) {
134
+ 134 if (key === 'include') {
135
+ 135 // Parse include (comma-separated string to array)
136
+ 136 result.include = value.split(',').map(s => s.trim()).filter(s => s.length > 0)
137
+ 137 } else if (key === 'sort') {
138
+ 138 // Parse sort (comma-separated string to array)
139
+ 139 result.sort = value.split(',').map(s => s.trim()).filter(s => s.length > 0)
140
+ 140 } else if (key.startsWith('filter[') && key.endsWith(']')) {
141
+ 141 // Parse filter[key] = value into filters: { key: value }
142
+ 142 const filterKey = key.slice(7, -1) // Remove 'filter[' and ']'
143
+ 143 if (filterKey) {
144
+ 144 result.filters[filterKey] = value
145
+ 145 }
146
+ 146 } else if (key.startsWith('fields[') && key.endsWith(']')) {
147
+ 147 // Parse fields[type] = fields into fields: { type: "field1,field2" }
148
+ 148 // Keep as comma-separated string to match REST API validation expectations
149
+ 149 const fieldType = key.slice(7, -1) // Remove 'fields[' and ']'
150
+ 150 if (fieldType) {
151
+ 151 result.fields[fieldType] = value
152
+ 152 }
153
+ 153 } else if (key.startsWith('page[') && key.endsWith(']')) {
154
+ 154 // Parse page[size] = 10 into page: { size: 10 }
155
+ 155 const pageKey = key.slice(5, -1) // Remove 'page[' and ']'
156
+ 156 if (pageKey) {
157
+ 157 // Convert to number if it's a valid number, otherwise keep as string
158
+ 158 result.page[pageKey] = isNaN(value) ? value : parseInt(value, 10)
159
+ 159 }
160
+ 160 }
161
+ 161 // Ignore other query parameters that don't match JSON:API patterns
162
+ 162 }
163
+ 163
164
+ 164 return result
165
+ 165 }