json-rest-api 1.0.22 → 1.0.23

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 (260) hide show
  1. package/CLAUDE.md +1 -2
  2. package/JSKIT_BACKEND_MIGRATION_TRACKER.txt +180 -0
  3. package/SINGLE_VALIDATION_CONTRACT_TODO.txt +172 -0
  4. package/docs/API.md +51 -14
  5. package/docs/COMPARISON.md +2 -2
  6. package/docs/GUIDE/GUIDE_1_Initial_Setup.md +28 -1
  7. package/docs/GUIDE/GUIDE_2_2_Manipulating_And_Searching_Tables.md +5 -1
  8. package/docs/GUIDE/GUIDE_2_7_Pagination_And_Ordering.md +3 -1
  9. package/docs/GUIDE/GUIDE_2_8_Effects_of_PUT_and_PATCH.md +12 -60
  10. package/docs/GUIDE/GUIDE_2_Data_And_Relations.md +2 -2
  11. package/docs/GUIDE/GUIDE_3_Field_Transformations.md +5 -1
  12. package/docs/GUIDE/GUIDE_7_Hooks_Data_Management_And_Plugins.md +2 -0
  13. package/docs/GUIDE/GUIDE_X_Autofiltering.md +355 -0
  14. package/docs/GUIDE/GUIDE_X_Fastify.md +78 -0
  15. package/docs/GUIDE/GUIDE_X_Knex_Schema_And_Migrations.md +250 -0
  16. package/docs/GUIDE/GUIDE_X_Query_Projections.md +159 -0
  17. package/docs/GUIDE/GUIDE_X_Writing_Plugins.md +199 -0
  18. package/docs/GUIDE/index.md +18 -9
  19. package/docs/QUICKSTART.md +5 -1
  20. package/docs/index.md +1 -1
  21. package/index.js +3 -2
  22. package/package.json +2 -2
  23. package/plugins/core/connectors/express-plugin.js +97 -184
  24. package/plugins/core/connectors/fastify-plugin.js +206 -0
  25. package/plugins/core/connectors/lib/connector-core.js +195 -0
  26. package/plugins/core/connectors/lib/transport-http-helpers.js +122 -0
  27. package/plugins/core/connectors/lib/transport-route-schemas.js +92 -0
  28. package/plugins/core/lib/dbIntrospection.js +1078 -0
  29. package/plugins/core/lib/dbTablesOperations.js +1104 -160
  30. package/plugins/core/lib/querying/knex-json-api-transformers-querying.js +5 -2
  31. package/plugins/core/lib/querying/knex-process-includes.js +3 -4
  32. package/plugins/core/lib/querying/knex-query-helpers.js +17 -71
  33. package/plugins/core/lib/querying/knex-relationship-includes.js +466 -206
  34. package/plugins/core/lib/querying/query-builder-utils.js +19 -0
  35. package/plugins/core/lib/querying/query-field-sort-helpers.js +135 -0
  36. package/plugins/core/lib/querying/sort-helpers.js +33 -0
  37. package/plugins/core/lib/querying/storage-adapter-utils.js +51 -0
  38. package/plugins/core/lib/querying/url-helpers.js +8 -2
  39. package/plugins/core/lib/querying-writing/compile-schemas.js +12 -1
  40. package/plugins/core/lib/querying-writing/knex-field-helpers.js +83 -13
  41. package/plugins/core/lib/querying-writing/knex-json-api-transformers.js +13 -12
  42. package/plugins/core/lib/querying-writing/query-field-helpers.js +88 -0
  43. package/plugins/core/lib/querying-writing/request-contracts.js +590 -0
  44. package/plugins/core/lib/storage/canonical-storage-mapping.js +113 -0
  45. package/plugins/core/lib/storage/storage-adapter.js +79 -2
  46. package/plugins/core/lib/storage/storage-mapping.js +204 -0
  47. package/plugins/core/rest-api-anyapi-knex-plugin.js +355 -228
  48. package/plugins/core/rest-api-autofilter-plugin.js +414 -0
  49. package/plugins/core/rest-api-knex-plugin.js +249 -211
  50. package/plugins/core/rest-api-plugin-hooks/register-relationship-routes.js +10 -5
  51. package/plugins/core/rest-api-plugin-hooks/register-scope-routes.js +12 -6
  52. package/plugins/core/rest-api-plugin-methods/apply-query-filters.js +41 -0
  53. package/plugins/core/rest-api-plugin-methods/common.js +121 -83
  54. package/plugins/core/rest-api-plugin-methods/enrich-attributes.js +6 -1
  55. package/plugins/core/rest-api-plugin-methods/get.js +23 -14
  56. package/plugins/core/rest-api-plugin-methods/patch.js +28 -12
  57. package/plugins/core/rest-api-plugin-methods/post.js +13 -8
  58. package/plugins/core/rest-api-plugin-methods/put.js +45 -21
  59. package/plugins/core/rest-api-plugin-methods/query.js +15 -53
  60. package/plugins/core/rest-api-plugin.js +2 -0
  61. package/plugins/core/rest-api-positioning-plugin.js +1 -1
  62. package/plugins/core/rest-api-query-projections-plugin.js +130 -0
  63. package/plugins/core/socketio-plugin.js +1 -3
  64. package/tests/autofilter.test.js +463 -0
  65. package/tests/custom-id-simplified-post.test.js +75 -0
  66. package/tests/custom-idproperty-relationships.test.js +33 -0
  67. package/tests/db-introspection.test.js +518 -0
  68. package/tests/db-table-operations.test.js +781 -0
  69. package/tests/fastify-plugin.test.js +141 -0
  70. package/tests/field-getters.test.js +1 -2
  71. package/tests/field-setters.test.js +0 -4
  72. package/tests/fixtures/api-configs.js +188 -219
  73. package/tests/helpers/fake-fastify.js +99 -0
  74. package/tests/http-connectors-parity.test.js +235 -0
  75. package/tests/projected-fields.test.js +319 -0
  76. package/tests/relationships.test.js +3 -3
  77. package/tests/searchschema-knex-behavior.test.js +187 -0
  78. package/tests/searchschema-merge.test.js +21 -69
  79. package/tests/storage-mapping.test.js +210 -0
  80. package/audit/00-charter.md +0 -103
  81. package/audit/01-scope-map.md +0 -84
  82. package/audit/02-coverage-matrix.csv +0 -174
  83. package/audit/03-invariants.md +0 -123
  84. package/audit/04-standards-rubric.md +0 -56
  85. package/audit/05-competitor-rubric.md +0 -77
  86. package/audit/06-findings.md +0 -160
  87. package/audit/07-risk-register.csv +0 -10
  88. package/audit/08-remediation-roadmap.md +0 -177
  89. package/audit/09-signoff-check.md +0 -23
  90. package/audit/10-final-synthesis.md +0 -29
  91. package/audit/evidence/code/all-files-json-rest-api.txt +0 -168
  92. package/audit/evidence/code/all-files-json-rest-schema.txt +0 -12
  93. package/audit/evidence/code/boundary-leak-checks.txt +0 -72
  94. package/audit/evidence/code/critical-pass1/json-rest-api__plugins__core__bulk-operations-plugin.js.txt +0 -480
  95. package/audit/evidence/code/critical-pass1/json-rest-api__plugins__core__connectors__express-plugin.js.txt +0 -532
  96. package/audit/evidence/code/critical-pass1/json-rest-api__plugins__core__connectors__lib__busboy-detector.js.txt +0 -175
  97. package/audit/evidence/code/critical-pass1/json-rest-api__plugins__core__connectors__lib__formidable-detector.js.txt +0 -138
  98. package/audit/evidence/code/critical-pass1/json-rest-api__plugins__core__connectors__lib__request-helpers.js.txt +0 -222
  99. package/audit/evidence/code/critical-pass1/json-rest-api__plugins__core__file-handling-plugin.js.txt +0 -342
  100. package/audit/evidence/code/critical-pass1/json-rest-api__plugins__core__lib__dbTablesOperations.js.txt +0 -304
  101. package/audit/evidence/code/critical-pass1/json-rest-api__plugins__core__lib__fractional-positioning.js.txt +0 -188
  102. package/audit/evidence/code/critical-pass1/json-rest-api__plugins__core__lib__querying-writing__compile-schemas.js.txt +0 -336
  103. package/audit/evidence/code/critical-pass1/json-rest-api__plugins__core__lib__querying-writing__connectors-query-parser.js.txt +0 -165
  104. package/audit/evidence/code/critical-pass1/json-rest-api__plugins__core__lib__querying-writing__database-capabilities.js.txt +0 -265
  105. package/audit/evidence/code/critical-pass1/json-rest-api__plugins__core__lib__querying-writing__database-value-normalizers.js.txt +0 -369
  106. package/audit/evidence/code/critical-pass1/json-rest-api__plugins__core__lib__querying-writing__default-data-helpers.js.txt +0 -72
  107. package/audit/evidence/code/critical-pass1/json-rest-api__plugins__core__lib__querying-writing__field-utils.js.txt +0 -139
  108. package/audit/evidence/code/critical-pass1/json-rest-api__plugins__core__lib__querying-writing__knex-constants.js.txt +0 -45
  109. package/audit/evidence/code/critical-pass1/json-rest-api__plugins__core__lib__querying-writing__knex-field-helpers.js.txt +0 -396
  110. package/audit/evidence/code/critical-pass1/json-rest-api__plugins__core__lib__querying-writing__knex-json-api-transformers.js.txt +0 -162
  111. package/audit/evidence/code/critical-pass1/json-rest-api__plugins__core__lib__querying-writing__payload-validators.js.txt +0 -1314
  112. package/audit/evidence/code/critical-pass1/json-rest-api__plugins__core__lib__querying-writing__schema-helpers.js.txt +0 -488
  113. package/audit/evidence/code/critical-pass1/json-rest-api__plugins__core__lib__querying-writing__scope-validations.js.txt +0 -484
  114. package/audit/evidence/code/critical-pass1/json-rest-api__plugins__core__lib__querying-writing__simplified-helpers.js.txt +0 -857
  115. package/audit/evidence/code/critical-pass1/json-rest-api__plugins__core__lib__querying__knex-cross-table-search.js.txt +0 -565
  116. package/audit/evidence/code/critical-pass1/json-rest-api__plugins__core__lib__querying__knex-json-api-transformers-querying.js.txt +0 -485
  117. package/audit/evidence/code/critical-pass1/json-rest-api__plugins__core__lib__querying__knex-pagination-helpers.js.txt +0 -560
  118. package/audit/evidence/code/critical-pass1/json-rest-api__plugins__core__lib__querying__knex-process-includes.js.txt +0 -167
  119. package/audit/evidence/code/critical-pass1/json-rest-api__plugins__core__lib__querying__knex-query-helpers-base.js.txt +0 -109
  120. package/audit/evidence/code/critical-pass1/json-rest-api__plugins__core__lib__querying__knex-query-helpers.js.txt +0 -1064
  121. package/audit/evidence/code/critical-pass1/json-rest-api__plugins__core__lib__querying__knex-relationship-includes.js.txt +0 -1983
  122. package/audit/evidence/code/critical-pass1/json-rest-api__plugins__core__lib__querying__knex-window-queries.js.txt +0 -328
  123. package/audit/evidence/code/critical-pass1/json-rest-api__plugins__core__lib__querying__url-helpers.js.txt +0 -119
  124. package/audit/evidence/code/critical-pass1/json-rest-api__plugins__core__lib__storage__storage-adapter.js.txt +0 -160
  125. package/audit/evidence/code/critical-pass1/json-rest-api__plugins__core__lib__writing__many-to-many-manipulations.js.txt +0 -300
  126. package/audit/evidence/code/critical-pass1/json-rest-api__plugins__core__lib__writing__relationship-processor.js.txt +0 -245
  127. package/audit/evidence/code/critical-pass1/json-rest-api__plugins__core__rest-api-access.js.txt +0 -459
  128. package/audit/evidence/code/critical-pass1/json-rest-api__plugins__core__rest-api-cors-plugin.js.txt +0 -237
  129. package/audit/evidence/code/critical-pass1/json-rest-api__plugins__core__rest-api-knex-plugin.js.txt +0 -1135
  130. package/audit/evidence/code/critical-pass1/json-rest-api__plugins__core__rest-api-multihome-plugin.js.txt +0 -314
  131. package/audit/evidence/code/critical-pass1/json-rest-api__plugins__core__rest-api-plugin-hooks__compile-resource-schemas.js.txt +0 -7
  132. package/audit/evidence/code/critical-pass1/json-rest-api__plugins__core__rest-api-plugin-hooks__register-relationship-routes.js.txt +0 -71
  133. package/audit/evidence/code/critical-pass1/json-rest-api__plugins__core__rest-api-plugin-hooks__register-scope-routes.js.txt +0 -102
  134. package/audit/evidence/code/critical-pass1/json-rest-api__plugins__core__rest-api-plugin-hooks__turn-scope-init-into-vars.js.txt +0 -49
  135. package/audit/evidence/code/critical-pass1/json-rest-api__plugins__core__rest-api-plugin-hooks__validate-include-configurations.js.txt +0 -39
  136. package/audit/evidence/code/critical-pass1/json-rest-api__plugins__core__rest-api-plugin-methods__add-route.js.txt +0 -23
  137. package/audit/evidence/code/critical-pass1/json-rest-api__plugins__core__rest-api-plugin-methods__check-permissions.js.txt +0 -13
  138. package/audit/evidence/code/critical-pass1/json-rest-api__plugins__core__rest-api-plugin-methods__common.js.txt +0 -639
  139. package/audit/evidence/code/critical-pass1/json-rest-api__plugins__core__rest-api-plugin-methods__delete-relationship.js.txt +0 -117
  140. package/audit/evidence/code/critical-pass1/json-rest-api__plugins__core__rest-api-plugin-methods__delete.js.txt +0 -104
  141. package/audit/evidence/code/critical-pass1/json-rest-api__plugins__core__rest-api-plugin-methods__enrich-attributes.js.txt +0 -199
  142. package/audit/evidence/code/critical-pass1/json-rest-api__plugins__core__rest-api-plugin-methods__get-related.js.txt +0 -353
  143. package/audit/evidence/code/critical-pass1/json-rest-api__plugins__core__rest-api-plugin-methods__get-relationship.js.txt +0 -61
  144. package/audit/evidence/code/critical-pass1/json-rest-api__plugins__core__rest-api-plugin-methods__get.js.txt +0 -184
  145. package/audit/evidence/code/critical-pass1/json-rest-api__plugins__core__rest-api-plugin-methods__patch-relationship.js.txt +0 -55
  146. package/audit/evidence/code/critical-pass1/json-rest-api__plugins__core__rest-api-plugin-methods__patch.js.txt +0 -191
  147. package/audit/evidence/code/critical-pass1/json-rest-api__plugins__core__rest-api-plugin-methods__post-relationship.js.txt +0 -110
  148. package/audit/evidence/code/critical-pass1/json-rest-api__plugins__core__rest-api-plugin-methods__post.js.txt +0 -168
  149. package/audit/evidence/code/critical-pass1/json-rest-api__plugins__core__rest-api-plugin-methods__put.js.txt +0 -253
  150. package/audit/evidence/code/critical-pass1/json-rest-api__plugins__core__rest-api-plugin-methods__query.js.txt +0 -207
  151. package/audit/evidence/code/critical-pass1/json-rest-api__plugins__core__rest-api-plugin-methods__release.js.txt +0 -12
  152. package/audit/evidence/code/critical-pass1/json-rest-api__plugins__core__rest-api-plugin.js.txt +0 -165
  153. package/audit/evidence/code/critical-pass1/json-rest-api__plugins__core__rest-api-positioning-plugin.js.txt +0 -668
  154. package/audit/evidence/code/critical-pass1/json-rest-api__plugins__core__socketio-plugin.js.txt +0 -621
  155. package/audit/evidence/code/critical-pass1/json-rest-api__plugins__storage__local-storage.js.txt +0 -218
  156. package/audit/evidence/code/critical-pass1/json-rest-api__plugins__storage__s3-storage.js.txt +0 -124
  157. package/audit/evidence/code/critical-pass1/json-rest-schema__src__CorePlugin.js.txt +0 -269
  158. package/audit/evidence/code/critical-pass1/json-rest-schema__src__Schema.js.txt +0 -242
  159. package/audit/evidence/code/critical-pass1/json-rest-schema__src__index.js.txt +0 -77
  160. package/audit/evidence/code/critical-pass1-risk-keywords.txt +0 -238
  161. package/audit/evidence/code/critical-pass1-summary.csv +0 -67
  162. package/audit/evidence/code/critical-pass2-summary.csv +0 -67
  163. package/audit/evidence/code/early-silent-zones-no-test-token.txt +0 -57
  164. package/audit/evidence/code/error-model-signals.txt +0 -98
  165. package/audit/evidence/code/git-files-json-rest-api.txt +0 -171
  166. package/audit/evidence/code/git-files-json-rest-schema.txt +0 -27
  167. package/audit/evidence/code/hook-flow-signals.txt +0 -282
  168. package/audit/evidence/code/idproperty-signals.txt +0 -157
  169. package/audit/evidence/code/import-cycles-non-anyapi.txt +0 -3
  170. package/audit/evidence/code/import-hotspots-non-anyapi.txt +0 -96
  171. package/audit/evidence/code/imports-json-rest-api.txt +0 -354
  172. package/audit/evidence/code/imports-json-rest-schema.txt +0 -15
  173. package/audit/evidence/code/in-scope-files-json-rest-api.txt +0 -160
  174. package/audit/evidence/code/in-scope-files-json-rest-schema.txt +0 -13
  175. package/audit/evidence/code/non-anyapi-core-files.txt +0 -101
  176. package/audit/evidence/code/permission-signals.txt +0 -193
  177. package/audit/evidence/code/query-include-signals.txt +0 -687
  178. package/audit/evidence/code/transaction-signals.txt +0 -28
  179. package/audit/evidence/code/validation-signals.txt +0 -650
  180. package/audit/evidence/code/write-path-signals.txt +0 -61
  181. package/audit/evidence/docs/docs-claims-signals.txt +0 -1075
  182. package/audit/evidence/docs/schema-docs-claims-signals.txt +0 -179
  183. package/audit/evidence/external/corpus-freeze.md +0 -21
  184. package/audit/evidence/external/npm-competitor-metadata.txt +0 -70
  185. package/audit/evidence/external/npm-download-signals.txt +0 -23
  186. package/audit/evidence/external/npm-jsonapi-server.txt +0 -8
  187. package/audit/evidence/external/npm-modern-jsonapi-metadata.txt +0 -38
  188. package/audit/evidence/external/readme-fortune-signals.txt +0 -26
  189. package/audit/evidence/external/readme-fortune.md +0 -165
  190. package/audit/evidence/external/readme-json-api-serializer-signals.txt +0 -80
  191. package/audit/evidence/external/readme-json-api-serializer.md +0 -690
  192. package/audit/evidence/external/readme-jsonapi-serde-server-signals.txt +0 -0
  193. package/audit/evidence/external/readme-jsonapi-serde-server.md +0 -22
  194. package/audit/evidence/external/readme-jsonapi-serializer-signals.txt +0 -63
  195. package/audit/evidence/external/readme-jsonapi-serializer.md +0 -315
  196. package/audit/evidence/external/readme-jsonapi-server-signals.txt +0 -19
  197. package/audit/evidence/external/readme-jsonapi-server.md +0 -104
  198. package/audit/evidence/external/readme-nestjs-json-api-signals.txt +0 -4
  199. package/audit/evidence/external/readme-nestjs-json-api.md +0 -94
  200. package/audit/evidence/external/readme-yaser-json-api-nestjs-signals.txt +0 -47
  201. package/audit/evidence/external/readme-yaser-json-api-nestjs.md +0 -447
  202. package/audit/evidence/external/standard-jsonapi-snippets.txt +0 -84
  203. package/audit/evidence/external/standard-rfc-snippets.txt +0 -168
  204. package/audit/evidence/external/standards-sources.txt +0 -6
  205. package/audit/evidence/runtime/baseline-commits.txt +0 -2
  206. package/audit/evidence/runtime/db-adapter-assumptions.txt +0 -3
  207. package/audit/evidence/runtime/environment.txt +0 -4
  208. package/audit/evidence/runtime/invariant-runtime-mapping.md +0 -38
  209. package/audit/evidence/runtime/probe-patch-relationship-error.exitcode +0 -1
  210. package/audit/evidence/runtime/probe-patch-relationship-error.log +0 -76
  211. package/audit/evidence/runtime/probe-patch-relationship-error.mjs +0 -67
  212. package/audit/evidence/runtime/probe-patch-relationship-referenceerror.exitcode +0 -1
  213. package/audit/evidence/runtime/probe-patch-relationship-referenceerror.log +0 -62
  214. package/audit/evidence/runtime/probe-patch-relationship-referenceerror.mjs +0 -47
  215. package/audit/evidence/runtime/test-exitcodes.txt +0 -4
  216. package/audit/evidence/runtime/test-summary.md +0 -107
  217. package/audit/evidence/runtime/trace-anchors.txt +0 -61
  218. package/audit/evidence/tests/access-multitenancy-test-signals.txt +0 -125
  219. package/audit/evidence/tests/failing-tests-summary.txt +0 -25
  220. package/audit/evidence/tests/npm-test-json-rest-api.exitcode +0 -1
  221. package/audit/evidence/tests/npm-test-json-rest-api.log +0 -5885
  222. package/audit/evidence/tests/npm-test-json-rest-schema.exitcode +0 -1
  223. package/audit/evidence/tests/npm-test-json-rest-schema.log +0 -356
  224. package/audit/evidence/tests/query-include-test-signals.txt +0 -948
  225. package/audit/evidence/tests/relationship-test-signals.txt +0 -223
  226. package/audit/evidence/tests/skipped-tests.txt +0 -10
  227. package/audit/evidence/tests/test-suite-list-non-anyapi.txt +0 -30
  228. package/audit/evidence/tests/test-suite-list.txt +0 -32
  229. package/audit/subsystems/docs-drift.md +0 -49
  230. package/audit/subsystems/pass2-cross-review.md +0 -131
  231. package/audit/subsystems/plugins-access.md +0 -51
  232. package/audit/subsystems/plugins-bulk.md +0 -50
  233. package/audit/subsystems/plugins-cors.md +0 -48
  234. package/audit/subsystems/plugins-file.md +0 -53
  235. package/audit/subsystems/plugins-multihome.md +0 -50
  236. package/audit/subsystems/plugins-positioning.md +0 -54
  237. package/audit/subsystems/plugins-socketio.md +0 -50
  238. package/audit/subsystems/query-engine.md +0 -61
  239. package/audit/subsystems/relationships.md +0 -55
  240. package/audit/subsystems/rest-orchestration.md +0 -66
  241. package/audit/subsystems/schema-core.md +0 -64
  242. package/audit/subsystems/storage-knex.md +0 -57
  243. package/audit/subsystems/tests.md +0 -48
  244. package/audit/subsystems/transport-express.md +0 -55
  245. package/audit/traces/trace-read-get.md +0 -22
  246. package/audit/traces/trace-read-query.md +0 -25
  247. package/audit/traces/trace-relationship-endpoints.md +0 -31
  248. package/audit/traces/trace-write-delete.md +0 -17
  249. package/audit/traces/trace-write-patch.md +0 -26
  250. package/audit/traces/trace-write-post.md +0 -22
  251. package/audit/traces/trace-write-put.md +0 -23
  252. package/docs/GUIDE/GUIDE_4_Authentication_And_Permissions.md +0 -707
  253. package/docs/GUIDE/GUIDE_X_Multihome.md +0 -666
  254. package/plugins/core/lib/querying-writing/payload-validators.js +0 -1314
  255. package/plugins/core/rest-api-access.js +0 -459
  256. package/plugins/core/rest-api-multihome-plugin.js +0 -314
  257. package/tests/multihome.test.js +0 -553
  258. package/tests/multihome.test.js.bak +0 -550
  259. package/tests/ownership-optout.test.js +0 -76
  260. package/tests/rest-api-access.test.js +0 -206
@@ -1,668 +0,0 @@
1
- 1 import {
2
- 2 calculatePosition,
3
- 3 getInitialPosition,
4
- 4 isValidPosition,
5
- 5 assignInitialPositions,
6
- 6 } from './lib/fractional-positioning.js'
7
- 7 import { createStorageAdapter } from './lib/storage/storage-adapter.js'
8
- 8
9
- 9 export const PositioningPlugin = {
10
- 10 name: 'positioning',
11
- 11 dependencies: ['rest-api', 'rest-api-knex|rest-api-anyapi-knex'],
12
- 12
13
- 13 install ({ api, addHook, vars, helpers, log, scopes, pluginOptions }) {
14
- 14 const installedPlugins = Array.from(api._installedPlugins || [])
15
- 15 const legacyStorageInstalled = installedPlugins.includes('rest-api-knex')
16
- 16 const canonicalStorageInstalled = installedPlugins.includes('rest-api-anyapi-knex')
17
- 17 if (!legacyStorageInstalled && !canonicalStorageInstalled) {
18
- 18 throw new Error(
19
- 19 "Positioning plugin requires either 'rest-api-knex' or 'rest-api-anyapi-knex' to be installed before it."
20
- 20 )
21
- 21 }
22
- 22
23
- 23 if (!api.knex?.instance) {
24
- 24 throw new Error('Positioning plugin requires a storage plugin with knex support (rest-api-knex or rest-api-anyapi-knex)')
25
- 25 }
26
- 26
27
- 27 // Get configuration - hooked-api namespaces options by plugin name
28
- 28 const positioningOptions = pluginOptions || {}
29
- 29
30
- 30 // Store configuration in vars (data only) - inspired by multihome pattern
31
- 31 vars.positioning = {
32
- 32 field: positioningOptions.field || 'position',
33
- 33 filters: positioningOptions.filters || [],
34
- 34 excludeResources: positioningOptions.excludeResources || ['system_migrations', 'system_logs'],
35
- 35 strategy: positioningOptions.strategy || 'fractional',
36
- 36 beforeIdField: positioningOptions.beforeIdField || 'beforeId',
37
- 37 defaultPosition: positioningOptions.defaultPosition || 'last',
38
- 38 autoIndex: positioningOptions.autoIndex !== undefined ? positioningOptions.autoIndex : true,
39
- 39 rebalanceThreshold: positioningOptions.rebalanceThreshold || 50 // Max position string length
40
- 40 }
41
- 41
42
- 42 const resolveStorageAdapter = (scopeName) => {
43
- 43 if (!scopeName) return null
44
- 44 const scope = scopes[scopeName] || api.resources?.[scopeName]
45
- 45 const scopeVars = scope?.vars
46
- 46 if (!scopeVars) return null
47
- 47
48
- 48 if (scopeVars.storageAdapter) {
49
- 49 return scopeVars.storageAdapter
50
- 50 }
51
- 51
52
- 52 if (scopeVars.schemaInfo && api.knex?.instance) {
53
- 53 const adapter = createStorageAdapter({ knex: api.knex.instance, schemaInfo: scopeVars.schemaInfo })
54
- 54 scopeVars.storageAdapter = adapter
55
- 55 return adapter
56
- 56 }
57
- 57
58
- 58 return null
59
- 59 }
60
- 60
61
- 61 // Validate configuration
62
- 62 if (!['fractional', 'integer'].includes(vars.positioning.strategy)) {
63
- 63 throw new Error(`Invalid positioning strategy: ${vars.positioning.strategy}. Must be 'fractional' or 'integer'`)
64
- 64 }
65
- 65
66
- 66 if (vars.positioning.strategy === 'integer') {
67
- 67 throw new Error("Integer positioning strategy not yet implemented. Please use 'fractional'")
68
- 68 }
69
- 69
70
- 70 // Helper to check if a resource should have positioning
71
- 71 function shouldHavePositioning (scopeName) {
72
- 72 return !vars.positioning.excludeResources.includes(scopeName)
73
- 73 }
74
- 74
75
- 75 // Helper to build filter conditions for position groups
76
- 76 function buildPositionFilterConditions (adapter, schemaInfo, {
77
- 77 attributes = {},
78
- 78 rawRecord = {},
79
- 79 simplifiedRecord = {},
80
- 80 inputRelationships = {},
81
- 81 minimalRelationships = {},
82
- 82 }) {
83
- 83 const conditions = {}
84
- 84
85
- 85 log.debug('*** buildPositionFilterConditions', {
86
- 86 filters: vars.positioning.filters,
87
- 87 attributeKeys: Object.keys(attributes || {}),
88
- 88 rawKeys: Object.keys(rawRecord || {}),
89
- 89 simplifiedKeys: Object.keys(simplifiedRecord || {}),
90
- 90 })
91
- 91
92
- 92 const tryAssign = (holder, field) => (holder && field in holder ? holder[field] : undefined)
93
- 93
94
- 94 vars.positioning.filters.forEach((filterField) => {
95
- 95 const searchField = schemaInfo.searchSchemaStructure?.[filterField] || null
96
- 96 const schemaField = schemaInfo.schemaStructure?.[filterField] || null
97
- 97 const isRelationship = Boolean(
98
- 98 searchField?.isRelationship ||
99
- 99 schemaField?.belongsTo ||
100
- 100 schemaField?.belongsToPolymorphic
101
- 101 )
102
- 102
103
- 103 if (!searchField && !schemaField) {
104
- 104 log.warn(`Filter field '${filterField}' not found in search schema or resource schema for positioning`)
105
- 105 }
106
- 106
107
- 107 const dbField = searchField?.actualField || filterField
108
- 108 let value
109
- 109
110
- 110 const attempt = (candidate) => {
111
- 111 if (value === undefined && candidate !== undefined) {
112
- 112 value = candidate
113
- 113 }
114
- 114 }
115
- 115
116
- 116 attempt(tryAssign(rawRecord, filterField))
117
- 117 attempt(tryAssign(simplifiedRecord, filterField))
118
- 118 attempt(tryAssign(rawRecord, dbField))
119
- 119 attempt(tryAssign(attributes, filterField))
120
- 120 attempt(tryAssign(attributes, dbField))
121
- 121
122
- 122 if (isRelationship) {
123
- 123 attempt(inputRelationships?.[filterField]?.data?.id)
124
- 124 attempt(minimalRelationships?.[filterField]?.data?.id)
125
- 125 // Some contexts may expose relationship data under attributes
126
- 126 attempt(tryAssign(rawRecord?.relationships, filterField)?.data?.id)
127
- 127 }
128
- 128
129
- 129 if (value !== undefined) {
130
- 130 const column = adapter.translateColumn(dbField)
131
- 131 const normalizedValue = value === null
132
- 132 ? null
133
- 133 : adapter.translateFilterValue(filterField, value)
134
- 134 conditions[column] = normalizedValue
135
- 135 }
136
- 136 })
137
- 137
138
- 138 log.debug('*** Filter conditions built', { conditions })
139
- 139 return conditions
140
- 140 }
141
- 141
142
- 142 // Validate that resources have position field when added
143
- 143 addHook('scope:added', 'validate-position-field', {}, ({ context, vars }) => {
144
- 144 const { scopeName, scopeOptions } = context
145
- 145
146
- 146 // Skip excluded resources
147
- 147 if (!shouldHavePositioning(scopeName)) {
148
- 148 log.debug(`Resource ${scopeName} excluded from positioning`)
149
- 149 return
150
- 150 }
151
- 151
152
- 152 // Check if schema has position field
153
- 153 const schema = scopeOptions.schema
154
- 154 if (schema && !schema[vars.positioning.field]) {
155
- 155 throw new Error(
156
- 156 `Resource '${scopeName}' must have '${vars.positioning.field}' field in schema to use positioning plugin`
157
- 157 )
158
- 158 }
159
- 159
160
- 160 // Validate filter fields exist (check both relationship names and actual field names)
161
- 161 vars.positioning.filters.forEach(filterField => {
162
- 162 // Check if it's a relationship field by looking for a field with matching 'as' property
163
- 163 let fieldExists = false
164
- 164
165
- 165 // First check if the field exists directly in the schema
166
- 166 if (schema[filterField]) {
167
- 167 fieldExists = true
168
- 168 } else {
169
- 169 // Check if it's a relationship name (as property)
170
- 170 for (const [fieldName, fieldDef] of Object.entries(schema)) {
171
- 171 if (fieldDef.as === filterField) {
172
- 172 fieldExists = true
173
- 173 break
174
- 174 }
175
- 175 }
176
- 176 }
177
- 177
178
- 178 if (!fieldExists) {
179
- 179 throw new Error(
180
- 180 `Resource '${scopeName}' must have '${filterField}' field or relationship in schema for position filtering`
181
- 181 )
182
- 182 }
183
- 183 })
184
- 184 })
185
- 185
186
- 186 // Add index for position field if autoIndex is enabled
187
- 187 addHook('scope:added', 'add-position-index', { afterFunction: 'validate-position-field' }, async ({ context, vars, scopes }) => {
188
- 188 const { scopeName } = context
189
- 189
190
- 190 if (!shouldHavePositioning(scopeName) || !vars.positioning.autoIndex) {
191
- 191 return
192
- 192 }
193
- 193
194
- 194 const scope = scopes[scopeName]
195
- 195 const schemaInfo = scope.vars.schemaInfo
196
- 196
197
- 197 if (!schemaInfo || !api.knex?.instance) {
198
- 198 return // Skip if no database connection
199
- 199 }
200
- 200
201
- 201 try {
202
- 202 const adapter = resolveStorageAdapter(scopeName)
203
- 203 if (adapter.isCanonical()) {
204
- 204 log.debug(`Skipping positioning index for canonical storage on ${scopeName}`)
205
- 205 return
206
- 206 }
207
- 207
208
- 208 const knex = api.knex.instance
209
- 209 const tableName = adapter.getTableName()
210
- 210
211
- 211 // Check if table exists first
212
- 212 const tableExists = await knex.schema.hasTable(tableName)
213
- 213 if (!tableExists) {
214
- 214 log.debug(`Table ${tableName} doesn't exist yet, skipping index creation`)
215
- 215 return
216
- 216 }
217
- 217
218
- 218 // Build index columns: map filter fields to actual database columns
219
- 219 const indexColumns = new Set()
220
- 220
221
- 221 // Map each filter field to its database column
222
- 222 vars.positioning.filters.forEach(filterField => {
223
- 223 const searchField = schemaInfo.searchSchemaStructure?.[filterField]
224
- 224 if (searchField) {
225
- 225 // Use actualField if it exists (for relationships)
226
- 226 const dbField = searchField.actualField || filterField
227
- 227 indexColumns.add(adapter.translateColumn(dbField))
228
- 228 } else if (schemaInfo.schemaStructure[filterField]) {
229
- 229 // Direct database field
230
- 230 indexColumns.add(adapter.translateColumn(filterField))
231
- 231 }
232
- 232 })
233
- 233
234
- 234 // Add the position field
235
- 235 indexColumns.add(adapter.translateColumn(vars.positioning.field))
236
- 236
237
- 237 const indexColumnList = [...indexColumns].filter(Boolean)
238
- 238
239
- 239 const indexName = `idx_${tableName}_positioning`
240
- 240
241
- 241 // Check if index already exists
242
- 242 const hasIndex = await knex.schema.hasIndex(tableName, indexColumnList, indexName)
243
- 243
244
- 244 if (!hasIndex) {
245
- 245 await knex.schema.table(tableName, table => {
246
- 246 table.index(indexColumnList, indexName)
247
- 247 })
248
- 248
249
- 249 log.info(`Created positioning index on ${tableName}`, { columns: indexColumnList })
250
- 250 }
251
- 251 } catch (error) {
252
- 252 log.warn(`Could not create positioning index for ${scopeName}:`, error.message)
253
- 253 }
254
- 254 })
255
- 255
256
- 256 // Process beforeId parameter in requests
257
- 257 addHook('beforeSchemaValidate', 'process-beforeid', {}, async ({ context, scopeName, vars }) => {
258
- 258 log.debug('*** beforeSchemaValidate positioning hook', {
259
- 259 scopeName,
260
- 260 method: context.method,
261
- 261 shouldHavePositioning: shouldHavePositioning(scopeName)
262
- 262 })
263
- 263
264
- 264 // Skip excluded resources
265
- 265 if (!shouldHavePositioning(scopeName)) {
266
- 266 log.debug('*** Skipping beforeId processing - excluded resource')
267
- 267 return
268
- 268 }
269
- 269
270
- 270 // Skip if no positioning context needed
271
- 271 const method = context.method
272
- 272 if (!['post', 'put', 'patch'].includes(method)) {
273
- 273 log.debug('*** Skipping beforeId processing - wrong method', { method })
274
- 274 return
275
- 275 }
276
- 276
277
- 277 // Check if resource has position field
278
- 278 const scope = scopes[scopeName]
279
- 279 const hasPositionField = scope?.vars?.schemaInfo?.schemaStructure?.[vars.positioning.field]
280
- 280
281
- 281 if (!hasPositionField) {
282
- 282 return
283
- 283 }
284
- 284
285
- 285 // Extract beforeId from JSON:API format attributes
286
- 286 const attributes = context.inputRecord?.data?.attributes ||
287
- 287 (context.simplified && context.inputRecord && typeof context.inputRecord === 'object'
288
- 288 ? context.inputRecord
289
- 289 : {})
290
- 290 const beforeId = attributes[vars.positioning.beforeIdField]
291
- 291
292
- 292 // Store beforeId in context for later use
293
- 293 if (beforeId !== undefined) {
294
- 294 context.positioningBeforeId = beforeId
295
- 295
296
- 296 // Remove beforeId from attributes so it doesn't get stored
297
- 297 delete attributes[vars.positioning.beforeIdField]
298
- 298
299
- 299 if (context.simplified && context.inputRecord && typeof context.inputRecord === 'object') {
300
- 300 delete context.inputRecord[vars.positioning.beforeIdField]
301
- 301 }
302
- 302 }
303
- 303
304
- 304 // Remove any manually provided position field - position is managed by this plugin
305
- 305 if (attributes[vars.positioning.field] !== undefined) {
306
- 306 delete attributes[vars.positioning.field]
307
- 307
308
- 308 log.debug('Positioning beforeId extracted', {
309
- 309 scopeName,
310
- 310 beforeId,
311
- 311 method
312
- 312 })
313
- 313
314
- 314 if (context.simplified && context.inputRecord && typeof context.inputRecord === 'object') {
315
- 315 delete context.inputRecord[vars.positioning.field]
316
- 316 }
317
- 317 }
318
- 318
319
- 319 // For new records without explicit position, mark for positioning
320
- 320 if (method === 'post' && !attributes[vars.positioning.field]) {
321
- 321 context.needsPositioning = true
322
- 322 }
323
- 323 })
324
- 324
325
- 325 // Calculate and set position before create/update
326
- 326 addHook('beforeDataCallPost', 'calculate-position-post', {}, async ({ context, scopeName, vars, helpers }) => {
327
- 327 log.debug('*** beforeDataCallPost hook fired for positioning', { scopeName, method: context.method })
328
- 328 await calculateAndSetPosition(context, scopeName, vars, helpers, log, scopes, api)
329
- 329 })
330
- 330
331
- 331 addHook('beforeDataCallPut', 'calculate-position-put', {}, async ({ context, scopeName, vars, helpers }) => {
332
- 332 log.debug('*** beforeDataCallPut hook fired for positioning', { scopeName, method: context.method })
333
- 333 // Only process if beforeId was provided or it's a new record
334
- 334 if (context.positioningBeforeId !== undefined || context.needsPositioning) {
335
- 335 await calculateAndSetPosition(context, scopeName, vars, helpers, log, scopes, api)
336
- 336 }
337
- 337 })
338
- 338
339
- 339 addHook('beforeDataCallPatch', 'calculate-position-patch', {}, async ({ context, scopeName, vars, helpers }) => {
340
- 340 log.debug('*** beforeDataCallPatch hook fired for positioning', { scopeName, method: context.method })
341
- 341 // Only process if beforeId was provided
342
- 342 if (context.positioningBeforeId !== undefined) {
343
- 343 await calculateAndSetPosition(context, scopeName, vars, helpers, log, scopes, api)
344
- 344 }
345
- 345 })
346
- 346
347
- 347 // Helper function to calculate and set position
348
- 348 async function calculateAndSetPosition (context, scopeName, vars, helpers, log, scopes, api) {
349
- 349 log.debug('*** calculateAndSetPosition called', {
350
- 350 scopeName,
351
- 351 method: context.method,
352
- 352 shouldHavePositioning: shouldHavePositioning(scopeName),
353
- 353 beforeId: context.positioningBeforeId,
354
- 354 needsPositioning: context.needsPositioning
355
- 355 })
356
- 356
357
- 357 // Skip if no positioning needed
358
- 358 if (!shouldHavePositioning(scopeName)) {
359
- 359 log.debug('*** Skipping - resource excluded from positioning')
360
- 360 return
361
- 361 }
362
- 362
363
- 363 const beforeId = context.positioningBeforeId
364
- 364 const needsPositioning = context.needsPositioning
365
- 365
366
- 366 // Skip if no positioning action needed
367
- 367 if (beforeId === undefined && !needsPositioning) {
368
- 368 log.debug('*** Skipping - no positioning needed', { beforeId, needsPositioning })
369
- 369 return
370
- 370 }
371
- 371
372
- 372 const recordData = (() => {
373
- 373 if (context.inputRecord?.data?.attributes) {
374
- 374 return { ...context.inputRecord.data.attributes }
375
- 375 }
376
- 376 if (context.simplified && context.inputRecord && typeof context.inputRecord === 'object') {
377
- 377 return { ...context.inputRecord }
378
- 378 }
379
- 379 return {}
380
- 380 })()
381
- 381
382
- 382 log.debug('*** Record data for positioning', {
383
- 383 recordData,
384
- 384 simplified: context.simplified,
385
- 385 inputRecord: context.inputRecord
386
- 386 })
387
- 387
388
- 388 const jsonApiRecord = context.inputRecord?.data || {}
389
- 389 const inputRelationships = jsonApiRecord.relationships || {}
390
- 390 const simplifiedRecord = context.simplified && context.inputRecord && typeof context.inputRecord === 'object'
391
- 391 ? context.inputRecord
392
- 392 : {}
393
- 393
394
- 394 const minimalAttributes = context.minimalRecord?.attributes || {}
395
- 395 const minimalRelationships = context.minimalRecord?.relationships || {}
396
- 396
397
- 397 const combinedAttributes = {
398
- 398 ...minimalAttributes,
399
- 399 ...recordData,
400
- 400 }
401
- 401
402
- 402 // Build filter conditions for the position group
403
- 403 const schemaInfo = context.schemaInfo
404
- 404 const adapter = context.storageAdapter || resolveStorageAdapter(scopeName)
405
- 405
406
- 406 if (!adapter) {
407
- 407 log.warn('*** Positioning skipped - no storage adapter available', { scopeName })
408
- 408 return
409
- 409 }
410
- 410
411
- 411 const filterConditions = buildPositionFilterConditions(adapter, schemaInfo, {
412
- 412 attributes: combinedAttributes,
413
- 413 rawRecord: recordData,
414
- 414 simplifiedRecord,
415
- 415 inputRelationships,
416
- 416 minimalRelationships,
417
- 417 })
418
- 418
419
- 419 log.debug('*** Position filter conditions', { scopeName, filterConditions })
420
- 420
421
- 421 // Query existing items in the same position group
422
- 422 const knex = api.knex.instance
423
- 423 const tableName = adapter.getTableName()
424
- 424 const idProperty = schemaInfo.idProperty
425
- 425 const idColumn = adapter.getIdColumn()
426
- 426 const positionColumn = adapter.translateColumn(vars.positioning.field)
427
- 427
428
- 428 log.debug('*** Building position query', {
429
- 429 tableName,
430
- 430 idProperty,
431
- 431 idColumn,
432
- 432 positionField: vars.positioning.field,
433
- 433 positionColumn,
434
- 434 filterConditions
435
- 435 })
436
- 436
437
- 437 // Check if we have a transaction to use
438
- 438 // Build base query with filter conditions
439
- 439 const baseQuery = adapter.buildBaseQuery({ transaction: context.transaction })
440
- 440 Object.entries(filterConditions).forEach(([column, value]) => {
441
- 441 if (value === null) {
442
- 442 baseQuery.whereNull(column)
443
- 443 } else {
444
- 444 baseQuery.where(column, value)
445
- 445 }
446
- 446 })
447
- 447
448
- 448 // For updates, exclude the current record
449
- 449 if ((context.method === 'patch' || context.method === 'put') && context.id) {
450
- 450 const translatedId = adapter.translateFilterValue(idProperty, context.id)
451
- 451 baseQuery.whereNot(idColumn, translatedId)
452
- 452 }
453
- 453
454
- 454 // Calculate effective beforeId first
455
- 455 let effectiveBeforeId = beforeId
456
- 456
457
- 457 // Handle default positioning when no beforeId is provided
458
- 458 if (beforeId === undefined) {
459
- 459 if (vars.positioning.defaultPosition === 'last') {
460
- 460 effectiveBeforeId = null // null means position at end
461
- 461 } else if (vars.positioning.defaultPosition === 'first') {
462
- 462 effectiveBeforeId = 'FIRST' // Special marker for first position
463
- 463 }
464
- 464 }
465
- 465
466
- 466 let items = []
467
- 467
468
- 468 const selectColumns = {
469
- 469 [idProperty]: idColumn,
470
- 470 [vars.positioning.field]: positionColumn,
471
- 471 }
472
- 472 const selectWithAliases = (builder) => adapter.selectColumns(builder, selectColumns)
473
- 473
474
- 474 try {
475
- 475 log.debug('*** Positioning query start', {
476
- 476 effectiveBeforeId,
477
- 477 selectColumns,
478
- 478 })
479
- 479 if (effectiveBeforeId === null) {
480
- 480 // Positioning at end - only get the last item
481
- 481 const lastItem = await selectWithAliases(baseQuery
482
- 482 .clone())
483
- 483 .orderBy(positionColumn, 'desc')
484
- 484 .first()
485
- 485
486
- 486 if (lastItem) {
487
- 487 items = [lastItem]
488
- 488 }
489
- 489 } else if (effectiveBeforeId === 'FIRST') {
490
- 490 // Positioning at beginning - get the first item
491
- 491 const firstItem = await selectWithAliases(baseQuery
492
- 492 .clone())
493
- 493 .orderBy(positionColumn, 'asc')
494
- 494 .first()
495
- 495
496
- 496 if (firstItem) {
497
- 497 items = [firstItem]
498
- 498 effectiveBeforeId = firstItem[idProperty]
499
- 499 } else {
500
- 500 effectiveBeforeId = null
501
- 501 }
502
- 502 } else if (effectiveBeforeId) {
503
- 503 // Positioning before a specific item - get that item and the one before it
504
- 504 const normalizedTargetId = adapter.translateFilterValue(idProperty, effectiveBeforeId)
505
- 505 const targetItem = await selectWithAliases(baseQuery
506
- 506 .clone())
507
- 507 .where(idColumn, normalizedTargetId)
508
- 508 .first()
509
- 509
510
- 510 if (targetItem) {
511
- 511 // Get the item immediately before the target
512
- 512 const prevItem = await selectWithAliases(baseQuery
513
- 513 .clone())
514
- 514 .where(positionColumn, '<', targetItem[vars.positioning.field])
515
- 515 .orderBy(positionColumn, 'desc')
516
- 516 .first()
517
- 517
518
- 518 items = prevItem ? [prevItem, targetItem] : [targetItem]
519
- 519 }
520
- 520 } else {
521
- 521 // First item in group - no items needed
522
- 522 items = []
523
- 523 }
524
- 524
525
- 525 log.debug('*** Query completed', { itemCount: items.length, items })
526
- 526 } catch (error) {
527
- 527 log.error('*** Query failed', { error: error.message, stack: error.stack })
528
- 528 throw error
529
- 529 }
530
- 530
531
- 531 // Calculate new position
532
- 532 const newPosition = items.length === 0
533
- 533 ? getInitialPosition()
534
- 534 : calculatePosition(items, effectiveBeforeId, idProperty, vars.positioning.field)
535
- 535
536
- 536 // Set the position on the incoming payload so downstream steps persist it
537
- 537 if (context.inputRecord?.data) {
538
- 538 context.inputRecord.data.attributes = context.inputRecord.data.attributes || {}
539
- 539 context.inputRecord.data.attributes[vars.positioning.field] = newPosition
540
- 540 }
541
- 541
542
- 542 if (context.simplified && context.inputRecord && typeof context.inputRecord === 'object') {
543
- 543 context.inputRecord[vars.positioning.field] = newPosition
544
- 544 }
545
- 545
546
- 546 log.debug('Position calculated', {
547
- 547 scopeName,
548
- 548 newPosition,
549
- 549 beforeId: effectiveBeforeId,
550
- 550 filterConditions,
551
- 551 itemCount: items.length,
552
- 552 method: context.method,
553
- 553 id: context.id
554
- 554 })
555
- 555 }
556
- 556
557
- 557 // Add positioning info back to response
558
- 558 addHook('afterGet', 'add-beforeid-to-response', {}, async ({ context, vars }) => {
559
- 559 // Only add if beforeId was in the original request
560
- 560 if (context.positioningBeforeId !== undefined && context.record) {
561
- 561 if (context.simplified) {
562
- 562 context.record[vars.positioning.beforeIdField] = context.positioningBeforeId
563
- 563 } else {
564
- 564 // For JSON:API, add to meta
565
- 565 context.meta = context.meta || {}
566
- 566 context.meta.positioning = {
567
- 567 [vars.positioning.beforeIdField]: context.positioningBeforeId
568
- 568 }
569
- 569 }
570
- 570 }
571
- 571 })
572
- 572
573
- 573 // Apply default sort by position if no other sort specified
574
- 574 addHook('beforeQuery', 'apply-position-sort', {}, async ({ context, scopeName, vars }) => {
575
- 575 if (!shouldHavePositioning(scopeName)) {
576
- 576 return
577
- 577 }
578
- 578
579
- 579 // Check if sort is already specified
580
- 580 const hasSort = context.queryParams?.sort && context.queryParams.sort.length > 0
581
- 581
582
- 582 if (!hasSort) {
583
- 583 // Apply position sort
584
- 584 context.queryParams = context.queryParams || {}
585
- 585 context.queryParams.sort = [vars.positioning.field]
586
- 586
587
- 587 log.trace('Applied default position sort', { scopeName })
588
- 588 }
589
- 589 })
590
- 590
591
- 591 // API methods for positioning operations
592
- 592 api.positioning = {
593
- 593 /**
594
- 594 * Reorder items by providing new positions
595
- 595 * @param {string} scopeName - Resource scope name
596
- 596 * @param {Array} positions - Array of {id, position} or {id, beforeId}
597
- 597 * @param {Object} filters - Filter conditions for position group
598
- 598 */
599
- 599 async reorder (scopeName, positions, filters = {}) {
600
- 600 const scope = scopes[scopeName]
601
- 601 if (!scope) {
602
- 602 throw new Error(`Unknown resource: ${scopeName}`)
603
- 603 }
604
- 604
605
- 605 // Implementation would go here for bulk reordering
606
- 606 log.info('Bulk reorder requested', { scopeName, count: positions.length })
607
- 607 },
608
- 608
609
- 609 /**
610
- 610 * Get positioning configuration
611
- 611 */
612
- 612 getConfig () {
613
- 613 return { ...vars.positioning }
614
- 614 },
615
- 615
616
- 616 /**
617
- 617 * Check if a resource has positioning enabled
618
- 618 */
619
- 619 isEnabled (scopeName) {
620
- 620 return shouldHavePositioning(scopeName)
621
- 621 }
622
- 622 }
623
- 623
624
- 624 log.info('Positioning plugin installed', {
625
- 625 field: vars.positioning.field,
626
- 626 strategy: vars.positioning.strategy,
627
- 627 filters: vars.positioning.filters,
628
- 628 excludedResources: vars.positioning.excludeResources
629
- 629 })
630
- 630 }
631
- 631 }
632
- 632
633
- 633 /*
634
- 634 Usage examples:
635
- 635
636
- 636 // Basic usage - positioning for all resources
637
- 637 await api.use(PositioningPlugin);
638
- 638
639
- 639 // With configuration
640
- 640 await api.use(PositioningPlugin, {
641
- 641 field: 'sortOrder',
642
- 642 filters: ['status', 'projectId'],
643
- 643 excludeResources: ['users', 'logs']
644
- 644 });
645
- 645
646
- 646 // Position grouping example
647
- 647 await api.use(PositioningPlugin, {
648
- 648 filters: ['boardId', 'listId'], // Separate positions per board/list combo
649
- 649 defaultPosition: 'last'
650
- 650 });
651
- 651
652
- 652 // In requests:
653
- 653 // POST /api/tasks
654
- 654 {
655
- 655 "title": "New Task",
656
- 656 "boardId": 123,
657
- 657 "listId": 456,
658
- 658 "beforeId": "task-789" // Position before this task
659
- 659 }
660
- 660
661
- 661 // Or to add at end:
662
- 662 {
663
- 663 "title": "New Task",
664
- 664 "boardId": 123,
665
- 665 "listId": 456,
666
- 666 "beforeId": null // Explicit last position
667
- 667 }
668
- 668 */