json-rest-api 1.0.29 → 2.0.0

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 (292) hide show
  1. package/LICENSE +10 -670
  2. package/LICENSE-GPL-3.0 +674 -0
  3. package/LICENSE-MIT +21 -0
  4. package/README.md +39 -30
  5. package/docs/API.md +744 -1273
  6. package/docs/GUIDE/01-getting-started.md +338 -0
  7. package/docs/GUIDE/02-resources-and-relationships.md +33 -0
  8. package/docs/GUIDE/03-running-example.md +87 -0
  9. package/docs/GUIDE/04-creating-and-querying.md +181 -0
  10. package/docs/GUIDE/05-belongs-to.md +177 -0
  11. package/docs/GUIDE/06-has-many.md +184 -0
  12. package/docs/GUIDE/07-polymorphic-relationships.md +191 -0
  13. package/docs/GUIDE/08-many-to-many.md +184 -0
  14. package/docs/GUIDE/09-pagination-and-sorting.md +149 -0
  15. package/docs/GUIDE/10-put-and-patch.md +174 -0
  16. package/docs/GUIDE/11-relationship-endpoints.md +174 -0
  17. package/docs/GUIDE/12-field-transformations.md +287 -0
  18. package/docs/GUIDE/13-hooks-and-lifecycle.md +479 -0
  19. package/docs/GUIDE/14-custom-resource-methods.md +125 -0
  20. package/docs/GUIDE/15-query-projections.md +266 -0
  21. package/docs/GUIDE/{GUIDE_X_Autofiltering.md → 16-autofiltering.md} +77 -15
  22. package/docs/GUIDE/{GUIDE_X_Row_Policies.md → 17-row-policies.md} +137 -44
  23. package/docs/GUIDE/18-labels.md +86 -0
  24. package/docs/GUIDE/19-managed-transactions.md +179 -0
  25. package/docs/GUIDE/20-transaction-outcomes.md +207 -0
  26. package/docs/GUIDE/21-schema-and-migrations.md +634 -0
  27. package/docs/GUIDE/22-version-field-migrations.md +208 -0
  28. package/docs/GUIDE/23-fastify.md +179 -0
  29. package/docs/GUIDE/24-cors.md +172 -0
  30. package/docs/GUIDE/{GUIDE_X_URL_Management.md → 25-url-management.md} +7 -1
  31. package/docs/GUIDE/26-file-uploads.md +278 -0
  32. package/docs/GUIDE/27-bulk-operations.md +277 -0
  33. package/docs/GUIDE/28-socketio.md +325 -0
  34. package/docs/GUIDE/29-writing-plugins.md +278 -0
  35. package/docs/GUIDE/30-backend-capabilities.md +100 -0
  36. package/docs/GUIDE/31-positioning.md +181 -0
  37. package/docs/GUIDE/{GUIDE_Y_Appendices.md → 32-date-and-time.md} +74 -30
  38. package/docs/GUIDE/33-migrating-to-v2.md +3827 -0
  39. package/docs/GUIDE/index.md +51 -59
  40. package/docs/QUICKSTART.md +120 -287
  41. package/docs/architecture.md +75 -0
  42. package/docs/contributing.md +178 -0
  43. package/index.d.ts +36 -0
  44. package/index.js +1 -22
  45. package/lib/enhanced-logger.js +91 -85
  46. package/lib/error-context.js +445 -5
  47. package/lib/error-formatter.js +219 -59
  48. package/lib/knex-transaction.js +64 -0
  49. package/lib/logger-types.d.ts +22 -0
  50. package/lib/missing-package.js +4 -0
  51. package/lib/rest-api-errors.js +103 -8
  52. package/lib/runtime/json-rest-api.js +172 -0
  53. package/lib/transaction-types.d.ts +59 -0
  54. package/package.json +97 -27
  55. package/plugins/core/bulk-operations-plugin.js +191 -184
  56. package/plugins/core/connectors/express-plugin.js +148 -219
  57. package/plugins/core/connectors/fastify-plugin.js +125 -82
  58. package/plugins/core/connectors/lib/busboy-detector.js +74 -156
  59. package/plugins/core/connectors/lib/connector-core.js +144 -20
  60. package/plugins/core/connectors/lib/formidable-detector.js +63 -120
  61. package/plugins/core/connectors/lib/http-validators.js +77 -0
  62. package/plugins/core/connectors/lib/multipart-helpers.js +18 -0
  63. package/plugins/core/connectors/lib/request-helpers.js +10 -1
  64. package/plugins/core/connectors/lib/transport-http-helpers.js +89 -8
  65. package/plugins/core/connectors/lib/transport-route-schemas.js +7 -4
  66. package/plugins/core/file-handling-plugin.js +87 -177
  67. package/plugins/core/lib/anyapi/anyapi-registry.js +213 -164
  68. package/plugins/core/lib/anyapi/canonical-link-store.js +507 -0
  69. package/plugins/core/lib/anyapi/canonical-relationship-reader.js +598 -0
  70. package/plugins/core/lib/anyapi/schema-utils.js +25 -16
  71. package/plugins/core/lib/anyapi/utils/descriptor-helpers.js +0 -64
  72. package/plugins/core/lib/dbIntrospection.js +228 -21
  73. package/plugins/core/lib/dbTablesOperations.js +409 -92
  74. package/plugins/core/lib/fractional-positioning.js +12 -48
  75. package/plugins/core/lib/querying/identifier-query.js +19 -0
  76. package/plugins/core/lib/querying/include-query-helpers.js +364 -0
  77. package/plugins/core/lib/querying/include-to-many.js +428 -0
  78. package/plugins/core/lib/querying/include-to-one.js +579 -0
  79. package/plugins/core/lib/querying/knex-cross-table-search.js +31 -307
  80. package/plugins/core/lib/querying/knex-json-api-transformers-querying.js +99 -401
  81. package/plugins/core/lib/querying/knex-pagination-helpers.js +48 -317
  82. package/plugins/core/lib/querying/knex-process-includes.js +5 -110
  83. package/plugins/core/lib/querying/knex-query-helpers-base.js +21 -56
  84. package/plugins/core/lib/querying/knex-query-helpers.js +298 -448
  85. package/plugins/core/lib/querying/knex-relationship-includes.js +120 -2138
  86. package/plugins/core/lib/querying/knex-window-queries.js +62 -253
  87. package/plugins/core/lib/querying/query-builder-utils.js +44 -4
  88. package/plugins/core/lib/querying/query-constraint.d.ts +12 -0
  89. package/plugins/core/lib/querying/query-constraint.js +27 -0
  90. package/plugins/core/lib/querying/query-field-sort-helpers.js +134 -37
  91. package/plugins/core/lib/querying/relationship-identifiers.js +208 -0
  92. package/plugins/core/lib/querying/sort-helpers.js +13 -1
  93. package/plugins/core/lib/querying/storage-adapter-utils.js +13 -5
  94. package/plugins/core/lib/querying/url-helpers.js +11 -4
  95. package/plugins/core/lib/querying-writing/compile-schemas.js +148 -249
  96. package/plugins/core/lib/querying-writing/connectors-query-parser.js +33 -118
  97. package/plugins/core/lib/querying-writing/database-capabilities.js +149 -223
  98. package/plugins/core/lib/querying-writing/database-value-normalizers.js +179 -194
  99. package/plugins/core/lib/querying-writing/default-data-helpers.js +15 -45
  100. package/plugins/core/lib/querying-writing/field-selection-types.d.ts +43 -0
  101. package/plugins/core/lib/querying-writing/field-utils.js +113 -92
  102. package/plugins/core/lib/querying-writing/knex-constants.d.ts +18 -0
  103. package/plugins/core/lib/querying-writing/knex-constants.js +2 -26
  104. package/plugins/core/lib/querying-writing/knex-field-helpers.js +100 -290
  105. package/plugins/core/lib/querying-writing/knex-json-api-transformers.js +31 -120
  106. package/plugins/core/lib/querying-writing/query-field-helpers.js +108 -32
  107. package/plugins/core/lib/querying-writing/query-field-types.d.ts +60 -0
  108. package/plugins/core/lib/querying-writing/relationship-contracts.js +33 -13
  109. package/plugins/core/lib/querying-writing/request-contracts.js +93 -38
  110. package/plugins/core/lib/querying-writing/resource-id-normalization.js +23 -5
  111. package/plugins/core/lib/querying-writing/response-options.js +35 -0
  112. package/plugins/core/lib/querying-writing/schema-helpers.js +203 -324
  113. package/plugins/core/lib/querying-writing/scope-validations.js +92 -290
  114. package/plugins/core/lib/querying-writing/simplified-helpers.js +113 -712
  115. package/plugins/core/lib/storage/canonical-data-helpers.js +683 -0
  116. package/plugins/core/lib/storage/canonical-storage-mapping.js +51 -20
  117. package/plugins/core/lib/storage/ordinary-data-helpers.js +710 -0
  118. package/plugins/core/lib/storage/storage-adapter.js +128 -62
  119. package/plugins/core/lib/storage/storage-mapping.js +78 -28
  120. package/plugins/core/lib/storage/storage-types.d.ts +502 -0
  121. package/plugins/core/lib/writing/many-to-many-manipulations.js +114 -284
  122. package/plugins/core/lib/writing/relationship-processor.js +94 -158
  123. package/plugins/core/lib/writing/resource-version.js +172 -0
  124. package/plugins/core/lib/writing/reverse-relationship-manipulations.js +110 -0
  125. package/plugins/core/rest-api-anyapi-knex-plugin.js +116 -2485
  126. package/plugins/core/rest-api-autofilter-plugin.js +17 -19
  127. package/plugins/core/rest-api-cors-plugin.js +52 -205
  128. package/plugins/core/rest-api-knex-plugin.js +45 -1077
  129. package/plugins/core/rest-api-label-plugin.js +25 -38
  130. package/plugins/core/rest-api-plugin-hooks/register-relationship-routes.js +5 -5
  131. package/plugins/core/rest-api-plugin-hooks/register-scope-routes.js +9 -6
  132. package/plugins/core/rest-api-plugin-hooks/turn-scope-init-into-vars.js +7 -26
  133. package/plugins/core/rest-api-plugin-hooks/validate-include-configurations.js +20 -26
  134. package/plugins/core/rest-api-plugin-methods/add-route.js +4 -6
  135. package/plugins/core/rest-api-plugin-methods/apply-query-filters.js +42 -13
  136. package/plugins/core/rest-api-plugin-methods/check-permissions.js +11 -5
  137. package/plugins/core/rest-api-plugin-methods/common.js +291 -395
  138. package/plugins/core/rest-api-plugin-methods/delete-relationship.js +49 -47
  139. package/plugins/core/rest-api-plugin-methods/delete.js +31 -25
  140. package/plugins/core/rest-api-plugin-methods/enrich-attributes.js +108 -148
  141. package/plugins/core/rest-api-plugin-methods/get-related.js +113 -305
  142. package/plugins/core/rest-api-plugin-methods/get-relationship.js +25 -18
  143. package/plugins/core/rest-api-plugin-methods/get.js +36 -68
  144. package/plugins/core/rest-api-plugin-methods/lifecycle-types.d.ts +225 -0
  145. package/plugins/core/rest-api-plugin-methods/patch-relationship.js +28 -25
  146. package/plugins/core/rest-api-plugin-methods/patch.js +46 -99
  147. package/plugins/core/rest-api-plugin-methods/post-relationship.js +49 -53
  148. package/plugins/core/rest-api-plugin-methods/post.js +51 -51
  149. package/plugins/core/rest-api-plugin-methods/put.js +69 -148
  150. package/plugins/core/rest-api-plugin-methods/query.js +37 -81
  151. package/plugins/core/rest-api-plugin-methods/release.js +2 -1
  152. package/plugins/core/rest-api-plugin.js +32 -46
  153. package/plugins/core/rest-api-positioning-plugin.js +183 -628
  154. package/plugins/core/rest-api-query-projections-plugin.js +19 -102
  155. package/plugins/core/rest-api-row-policy-plugin.js +3 -3
  156. package/plugins/core/socketio-plugin.js +364 -344
  157. package/plugins/storage/local-storage.js +40 -36
  158. package/types/autofilter.d.ts +57 -0
  159. package/types/bulk-methods.d.ts +63 -0
  160. package/types/errors.d.ts +118 -0
  161. package/types/fastify-options.d.ts +19 -0
  162. package/types/file-detectors.d.ts +33 -0
  163. package/types/file-storage.d.ts +48 -0
  164. package/types/hook-context.d.ts +180 -0
  165. package/types/plugin-options.d.ts +53 -0
  166. package/types/relationship-methods.d.ts +74 -0
  167. package/types/representations.d.ts +50 -0
  168. package/types/resource-methods.d.ts +95 -0
  169. package/types/resource-schema.d.ts +157 -0
  170. package/types/row-policy.d.ts +31 -0
  171. package/types/runtime.d.ts +132 -0
  172. package/types/transactions.d.ts +21 -0
  173. package/.claude/agents/library-guide-expert.md +0 -30
  174. package/.claude/agents/rest-api-internals-expert.md +0 -54
  175. package/.claude/settings.local.json +0 -70
  176. package/AGENTS.md +0 -34
  177. package/CLAUDE.md +0 -160
  178. package/CRUSH.md +0 -55
  179. package/JSKIT_BACKEND_MIGRATION_TRACKER.txt +0 -180
  180. package/SINGLE_VALIDATION_CONTRACT_TODO.txt +0 -172
  181. package/TODO.todo +0 -13
  182. package/connectors_ideas/fastify.js +0 -217
  183. package/connectors_ideas/koa-plugin.js +0 -112
  184. package/docs/.bundle/config +0 -2
  185. package/docs/COMPARISON.md +0 -433
  186. package/docs/GUIDE/GUIDE_1_Initial_Setup.md +0 -1057
  187. package/docs/GUIDE/GUIDE_2_1_The_Starting_Point.md +0 -88
  188. package/docs/GUIDE/GUIDE_2_2_Manipulating_And_Searching_Tables.md +0 -808
  189. package/docs/GUIDE/GUIDE_2_3_BelongsTo_Relationships.md +0 -491
  190. package/docs/GUIDE/GUIDE_2_4_HasMany_Records.md +0 -398
  191. package/docs/GUIDE/GUIDE_2_5_HasMany_Polymorphic.md +0 -898
  192. package/docs/GUIDE/GUIDE_2_6_Many_To_Many.md +0 -712
  193. package/docs/GUIDE/GUIDE_2_7_Pagination_And_Ordering.md +0 -1247
  194. package/docs/GUIDE/GUIDE_2_8_Effects_of_PUT_and_PATCH.md +0 -748
  195. package/docs/GUIDE/GUIDE_2_9_Relationships_Urls.md +0 -517
  196. package/docs/GUIDE/GUIDE_2_Data_And_Relations.md +0 -66
  197. package/docs/GUIDE/GUIDE_3_Field_Transformations.md +0 -1252
  198. package/docs/GUIDE/GUIDE_5_Non-Db_Resources.md +0 -1107
  199. package/docs/GUIDE/GUIDE_6_Positioning.md +0 -692
  200. package/docs/GUIDE/GUIDE_7_Hooks_Data_Management_And_Plugins.md +0 -1491
  201. package/docs/GUIDE/GUIDE_X_Bulk_Operations.md +0 -732
  202. package/docs/GUIDE/GUIDE_X_Cors.md +0 -1957
  203. package/docs/GUIDE/GUIDE_X_Fastify.md +0 -78
  204. package/docs/GUIDE/GUIDE_X_File_Uploads.md +0 -729
  205. package/docs/GUIDE/GUIDE_X_Knex_Schema_And_Migrations.md +0 -370
  206. package/docs/GUIDE/GUIDE_X_Query_Projections.md +0 -159
  207. package/docs/GUIDE/GUIDE_X_SocketIO.md +0 -1040
  208. package/docs/GUIDE/GUIDE_X_Writing_Plugins.md +0 -201
  209. package/docs/Gemfile +0 -6
  210. package/docs/Gemfile.lock +0 -308
  211. package/docs/ONBOARDING.md +0 -315
  212. package/docs/_config.yml +0 -36
  213. package/docs/_layouts/default.html +0 -123
  214. package/docs/apple-touch-icon.svg +0 -4
  215. package/docs/assets/css/style.scss +0 -392
  216. package/docs/favicon.svg +0 -4
  217. package/docs/index.md +0 -164
  218. package/docs/mini-guides/file-uploads-complete.md +0 -161
  219. package/eslint.config.js +0 -13
  220. package/examples/url-override-example.js +0 -137
  221. package/fixing_plan.md +0 -112
  222. package/plugins/core/lib/anyapi/query/anyapi-query-adapter.js +0 -657
  223. package/plugins/core/lib/querying-writing/return-record-settings.js +0 -47
  224. package/plugins/core/lib/writing/knex-json-api-transformers-writing.js +0 -110
  225. package/plugins/storage/s3-storage.js +0 -93
  226. package/quickTest.js +0 -136
  227. package/scripts/debug-anyapi-review-filter.mjs +0 -63
  228. package/scripts/inspect-anyapi-descriptor-country.mjs +0 -19
  229. package/scripts/inspect-anyapi-descriptor.mjs +0 -19
  230. package/scripts/run-docs.js +0 -133
  231. package/test-debug.js +0 -59
  232. package/tests/README.md +0 -202
  233. package/tests/TEST_TEMPLATE.test.js +0 -178
  234. package/tests/anyapi-basic.test.js +0 -105
  235. package/tests/anyapi-cursor-pagination.test.js +0 -148
  236. package/tests/anyapi-custom-idproperty.test.js +0 -163
  237. package/tests/autofilter.test.js +0 -463
  238. package/tests/bulk-operations.test.js +0 -657
  239. package/tests/computed-fields.test.js +0 -586
  240. package/tests/cors.test.js +0 -834
  241. package/tests/curl.test.js +0 -467
  242. package/tests/custom-id-mysql-returning-fallback.test.js +0 -128
  243. package/tests/custom-id-simplified-post.test.js +0 -75
  244. package/tests/custom-idproperty-relationships.test.js +0 -932
  245. package/tests/db-introspection.test.js +0 -521
  246. package/tests/db-table-operations.test.js +0 -798
  247. package/tests/fastify-plugin.test.js +0 -184
  248. package/tests/field-getters.test.js +0 -370
  249. package/tests/field-setters.test.js +0 -306
  250. package/tests/file-handling.test.js +0 -155
  251. package/tests/fixtures/api-configs.js +0 -2566
  252. package/tests/full-jsonapi-belongsto-linkage.test.js +0 -150
  253. package/tests/helpers/fake-fastify.js +0 -99
  254. package/tests/helpers/storage-mode.js +0 -65
  255. package/tests/helpers/test-utils.js +0 -203
  256. package/tests/http-connectors-parity.test.js +0 -395
  257. package/tests/id-normalization.test.js +0 -444
  258. package/tests/include-depth-validation.test.js +0 -331
  259. package/tests/includes.test.js +0 -427
  260. package/tests/local-storage.test.js +0 -96
  261. package/tests/logical-id-write-validation.test.js +0 -151
  262. package/tests/nested-includes.test.js +0 -617
  263. package/tests/pagination-boundaries.test.js +0 -154
  264. package/tests/pagination-contract.test.js +0 -122
  265. package/tests/pagination-cursor-multifield.test.js +0 -641
  266. package/tests/pagination-enhanced.test.js +0 -563
  267. package/tests/polymorphic-relationships.test.js +0 -347
  268. package/tests/polymorphic-simple.test.js +0 -148
  269. package/tests/positioning-advanced.test.js +0 -692
  270. package/tests/positioning-extended.test.js +0 -915
  271. package/tests/positioning-multifilter.test.js +0 -699
  272. package/tests/positioning.test.js +0 -560
  273. package/tests/post-commit-hooks.test.js +0 -120
  274. package/tests/projected-fields.test.js +0 -319
  275. package/tests/public-exports.test.js +0 -15
  276. package/tests/put-create-parity.test.js +0 -216
  277. package/tests/queries.test.js +0 -521
  278. package/tests/query-limits.test.js +0 -584
  279. package/tests/relationship-endpoints.test.js +0 -620
  280. package/tests/relationship-include-storage-adapter.test.js +0 -56
  281. package/tests/relationships.test.js +0 -681
  282. package/tests/rest-api.test.js +0 -337
  283. package/tests/return-record-settings.test.js +0 -184
  284. package/tests/row-policy.test.js +0 -489
  285. package/tests/s3-storage.test.js +0 -38
  286. package/tests/searchschema-knex-behavior.test.js +0 -187
  287. package/tests/searchschema-merge.test.js +0 -151
  288. package/tests/socketio.test.js +0 -860
  289. package/tests/storage-mapping.test.js +0 -362
  290. package/tests/temporal-boundaries.test.js +0 -208
  291. package/tests/test-development-guide.md +0 -370
  292. package/tests/virtual-fields.test.js +0 -404
@@ -1,692 +0,0 @@
1
- # Positioning Plugin Guide
2
-
3
- The Positioning Plugin adds sophisticated ordering capabilities to your REST API resources, enabling drag-and-drop interfaces, sortable lists, and maintaining custom order across different groupings. It uses fractional indexing for infinite precision without requiring batch updates.
4
-
5
- ## Table of Contents
6
-
7
- 1. [Overview](#overview)
8
- 2. [Installation](#installation)
9
- 3. [Basic Usage](#basic-usage)
10
- 4. [Configuration Options](#configuration-options)
11
- 5. [Position Grouping](#position-grouping)
12
- 6. [API Usage](#api-usage)
13
- 7. [Real-World Examples](#real-world-examples)
14
- 8. [How It Works](#how-it-works)
15
- 9. [Migration Guide](#migration-guide)
16
- 10. [Performance Considerations](#performance-considerations)
17
- 11. [Troubleshooting](#troubleshooting)
18
-
19
- ## Overview
20
-
21
- The Positioning Plugin provides:
22
-
23
- - **Fractional indexing**: Insert items between any two positions without updating other records
24
- - **Position groups**: Maintain separate orderings for different categories/statuses/projects
25
- - **BeforeId API**: Natural interface for drag-and-drop operations
26
- - **Automatic positioning**: Items without explicit positions are placed appropriately
27
- - **Zero conflicts**: Multiple users can reorder simultaneously without issues
28
-
29
- ### Why Fractional Indexing?
30
-
31
- Traditional integer-based positioning requires updating multiple records when inserting:
32
-
33
- ```sql
34
- -- Traditional approach - requires updating many records
35
- UPDATE tasks SET position = position + 1 WHERE position >= 3;
36
- INSERT INTO tasks (title, position) VALUES ('New Task', 3);
37
- ```
38
-
39
- Fractional indexing only updates the moved item:
40
-
41
- ```sql
42
- -- Fractional approach - single record update
43
- INSERT INTO tasks (title, position) VALUES ('New Task', 'a0m');
44
- ```
45
-
46
- ## Installation
47
-
48
- First, ensure you have the required dependency:
49
-
50
- ```bash
51
- npm install fractional-indexing
52
- ```
53
-
54
- Then, use the plugin in your API:
55
-
56
- ```javascript
57
- import { Api } from 'hooked-api';
58
- import { RestApiPlugin } from './plugins/core/rest-api-plugin.js';
59
- import { RestApiKnexPlugin } from './plugins/core/rest-api-knex-plugin.js';
60
- import { PositioningPlugin } from './plugins/core/rest-api-positioning-plugin.js';
61
-
62
- const api = new Api({
63
- name: 'my-api'
64
- });
65
-
66
- // Core plugins
67
- await api.use(RestApiPlugin);
68
- await api.use(RestApiKnexPlugin, { knex: knexInstance });
69
-
70
- // Add positioning capabilities
71
- await api.use(PositioningPlugin);
72
- ```
73
-
74
- ## Basic Usage
75
-
76
- ### Simple List Ordering
77
-
78
- For a basic sortable list, just add the plugin:
79
-
80
- ```javascript
81
- await api.use(PositioningPlugin);
82
-
83
- // Define a resource
84
- api.addResource('tasks', {
85
- schema: {
86
- title: { type: 'string', required: true },
87
- completed: { type: 'boolean', defaultTo: false }
88
- // 'position' field is automatically added
89
- }
90
- });
91
- ```
92
-
93
- Now you can create ordered tasks:
94
-
95
- ```javascript
96
- // First task
97
- POST /api/tasks
98
- {
99
- "title": "First task"
100
- }
101
- // Response includes position: "a0"
102
-
103
- // Add task at the end
104
- POST /api/tasks
105
- {
106
- "title": "Last task",
107
- "beforeId": null // Explicit "place at end"
108
- }
109
- // Gets position: "a1"
110
-
111
- // Insert between first and last
112
- POST /api/tasks
113
- {
114
- "title": "Middle task",
115
- "beforeId": 2 // Place before task with ID 2
116
- }
117
- // Gets position: "a0m" (between "a0" and "a1")
118
- ```
119
-
120
- ### Retrieving Ordered Lists
121
-
122
- Lists are automatically sorted by position:
123
-
124
- ```javascript
125
- GET /api/tasks
126
-
127
- // Returns tasks in position order:
128
- {
129
- "data": [
130
- { "id": 1, "attributes": { "title": "First task", "position": "a0" } },
131
- { "id": 3, "attributes": { "title": "Middle task", "position": "a0m" } },
132
- { "id": 2, "attributes": { "title": "Last task", "position": "a1" } }
133
- ]
134
- }
135
- ```
136
-
137
- ## Configuration Options
138
-
139
- Configure the plugin behavior:
140
-
141
- ```javascript
142
- await api.use(PositioningPlugin, {
143
- // Position field name (default: 'position')
144
- field: 'sortOrder',
145
-
146
- // Grouping fields - create separate position sequences per group
147
- filters: ['status', 'projectId'],
148
-
149
- // Resources to exclude from positioning
150
- excludeResources: ['users', 'system_logs'],
151
-
152
- // Positioning strategy (currently only 'fractional' is supported)
153
- strategy: 'fractional',
154
-
155
- // Field name for beforeId in requests (default: 'beforeId')
156
- beforeIdField: 'insertBefore',
157
-
158
- // Default position for new items without beforeId (default: 'last')
159
- defaultPosition: 'last', // or 'first'
160
-
161
- // Automatically create database index (default: true)
162
- autoIndex: true,
163
-
164
- // Maximum position string length before rebalancing (default: 50)
165
- rebalanceThreshold: 50
166
- });
167
- ```
168
-
169
- ## Position Grouping
170
-
171
- Position grouping is one of the most powerful features. It maintains separate position sequences for different combinations of field values.
172
-
173
- ### Understanding Position Groups
174
-
175
- When you configure filters like `['status', 'projectId']`, the plugin creates independent position sequences for each unique combination:
176
-
177
- - Project 1 + Status "todo" → positions: a0, a1, a2...
178
- - Project 1 + Status "done" → positions: a0, a1, a2... (separate sequence!)
179
- - Project 2 + Status "todo" → positions: a0, a1, a2... (another separate sequence!)
180
-
181
- This means:
182
- - The first item in each group gets position "a0"
183
- - Items can have the same position value if they're in different groups
184
- - Moving between groups requires explicit positioning with `beforeId`
185
-
186
- ### Kanban Board Example
187
-
188
- ```javascript
189
- await api.use(PositioningPlugin, {
190
- filters: ['boardId', 'columnId']
191
- });
192
-
193
- api.addResource('cards', {
194
- schema: {
195
- title: { type: 'string', required: true },
196
- boardId: { type: 'id', required: true },
197
- columnId: { type: 'string', required: true },
198
- description: { type: 'string' }
199
- }
200
- });
201
- ```
202
-
203
- Each board/column combination maintains its own positions:
204
-
205
- ```javascript
206
- // First card in "To Do" column
207
- POST /api/cards
208
- {
209
- "title": "Design mockups",
210
- "boardId": 1,
211
- "columnId": "todo"
212
- }
213
- // Position: "a0" in board 1, todo column
214
-
215
- // Second card in "To Do"
216
- POST /api/cards
217
- {
218
- "title": "Write tests",
219
- "boardId": 1,
220
- "columnId": "todo"
221
- }
222
- // Position: "a1" in board 1, todo column
223
-
224
- // First card in "In Progress" - gets its own sequence!
225
- POST /api/cards
226
- {
227
- "title": "Implement feature",
228
- "boardId": 1,
229
- "columnId": "in-progress"
230
- }
231
- // Position: "a0" in board 1, in-progress column
232
- ```
233
-
234
- ### Moving Between Groups
235
-
236
- **Important Behavior**: When you change a filter field value (like moving a card between columns), the item keeps its existing position value. The plugin does NOT automatically reassign positions when filter values change.
237
-
238
- ```javascript
239
- // Move card from "todo" to "in-progress"
240
- PATCH /api/cards/1
241
- {
242
- "columnId": "in-progress"
243
- }
244
- // Result: Card moves to in-progress but KEEPS its existing position (e.g., "a0m")
245
- // This might place it in an unexpected location in the new column!
246
- ```
247
-
248
- To move an item to a specific position in the new group, you MUST provide a `beforeId`:
249
-
250
- ```javascript
251
- // Move card and position it correctly
252
- PATCH /api/cards/1
253
- {
254
- "columnId": "in-progress",
255
- "beforeId": null // Explicitly place at end of new column
256
- }
257
- // OR
258
- PATCH /api/cards/1
259
- {
260
- "columnId": "in-progress",
261
- "beforeId": 456 // Place before card 456 in the new column
262
- }
263
- ```
264
-
265
- **Why this behavior?** The plugin cannot guess where you want the item positioned in the new group. Should it go first? Last? In the middle? You must explicitly specify the desired position.
266
-
267
- ### Multi-Tenant Positioning
268
-
269
- ```javascript
270
- await api.use(PositioningPlugin, {
271
- filters: ['tenantId', 'listId']
272
- });
273
-
274
- // Each tenant has independent position sequences
275
- // Tenant A's positions don't affect Tenant B's positions
276
- ```
277
-
278
- ## API Usage
279
-
280
- ### Creating Items
281
-
282
- ```javascript
283
- // Add at end (default)
284
- POST /api/items
285
- { "name": "New item" }
286
-
287
- // Add at end explicitly
288
- POST /api/items
289
- { "name": "New item", "beforeId": null }
290
-
291
- // Add at specific position
292
- POST /api/items
293
- { "name": "New item", "beforeId": 123 }
294
-
295
- // Note: Manual position values are ignored!
296
- // The plugin always calculates positions to ensure consistency
297
- POST /api/items
298
- { "name": "New item", "position": "a0abc" } // 'position' will be recalculated!
299
- ```
300
-
301
- ### Updating Positions
302
-
303
- ```javascript
304
- // Move item before another
305
- PATCH /api/items/456
306
- { "beforeId": 789 }
307
-
308
- // Move to end
309
- PATCH /api/items/456
310
- { "beforeId": null }
311
-
312
- // Update other fields without changing position
313
- PATCH /api/items/456
314
- { "name": "Updated name" }
315
- // Position remains unchanged
316
-
317
- // IMPORTANT: Changing filter fields without beforeId
318
- PATCH /api/items/456
319
- { "status": "done" }
320
- // Item moves to 'done' group but KEEPS its position value!
321
- // May appear in unexpected location in the new group
322
- ```
323
-
324
- ### Simplified Format
325
-
326
- The plugin works with both JSON:API and simplified formats:
327
-
328
- ```javascript
329
- // Simplified format
330
- POST /api/items
331
- {
332
- "name": "New item",
333
- "categoryId": 5,
334
- "beforeId": 10
335
- }
336
-
337
- // JSON:API format
338
- POST /api/items
339
- {
340
- "data": {
341
- "type": "items",
342
- "attributes": {
343
- "name": "New item",
344
- "categoryId": 5,
345
- "beforeId": 10
346
- }
347
- }
348
- }
349
- ```
350
-
351
- ## Important Behaviors to Understand
352
-
353
- ### 1. Position Values are Immutable by Design
354
-
355
- The plugin NEVER changes an item's position unless you explicitly request it with `beforeId`. This means:
356
-
357
- - Changing filter fields (status, category, etc.) does NOT reposition the item
358
- - The item keeps its position value when moving between groups
359
- - You must provide `beforeId` to position items in their new group
360
-
361
- ### 2. Position Groups are Independent
362
-
363
- Each combination of filter values creates a completely separate position space:
364
-
365
- ```javascript
366
- // These items can all have position "a0" because they're in different groups:
367
- item1: { projectId: 1, status: 'todo', position: 'a0' }
368
- item2: { projectId: 1, status: 'done', position: 'a0' } // Different status
369
- item3: { projectId: 2, status: 'todo', position: 'a0' } // Different project
370
- ```
371
-
372
- ### 3. BeforeId Context Matters
373
-
374
- The `beforeId` only works within the same position group:
375
-
376
- ```javascript
377
- // This will NOT work as expected:
378
- PATCH /api/items/1
379
- {
380
- "status": "done",
381
- "beforeId": 2 // Item 2 is in the 'todo' group, not 'done'!
382
- }
383
- // Result: Item 1 moves to 'done' but ignores beforeId (item not found in target group)
384
- ```
385
-
386
- ### 4. Manual Position Values are Ignored
387
-
388
- The plugin always calculates positions to ensure consistency:
389
-
390
- ```javascript
391
- // This position value will be ignored:
392
- POST /api/items
393
- {
394
- "name": "Test",
395
- "position": "zzz" // Ignored! Plugin calculates actual position
396
- }
397
- ```
398
-
399
- ### 5. Null Values in Filters
400
-
401
- Null values in filter fields create their own position group:
402
-
403
- ```javascript
404
- // These are THREE different position groups:
405
- items.where({ projectId: 1, status: 'active' }) // Group 1
406
- items.where({ projectId: 1, status: null }) // Group 2 (null status)
407
- items.where({ projectId: null, status: 'active' }) // Group 3 (null project)
408
- ```
409
-
410
- ## Real-World Examples
411
-
412
- ### 1. Trello-Style Board
413
-
414
- ```javascript
415
- // Configure with board and list grouping
416
- await api.use(PositioningPlugin, {
417
- filters: ['boardId', 'listId']
418
- });
419
-
420
- // Moving a card
421
- async function moveCard(cardId, targetListId, targetPosition) {
422
- const response = await fetch(`/api/cards/${cardId}`, {
423
- method: 'PATCH',
424
- headers: { 'Content-Type': 'application/json' },
425
- body: JSON.stringify({
426
- listId: targetListId,
427
- beforeId: targetPosition // ID of card to insert before
428
- })
429
- });
430
- return response.json();
431
- }
432
- ```
433
-
434
- ### 2. Priority Task List
435
-
436
- ```javascript
437
- // Configure with status grouping
438
- await api.use(PositioningPlugin, {
439
- filters: ['status'],
440
- defaultPosition: 'first' // New tasks go to top
441
- });
442
-
443
- // Reorder within status
444
- async function reprioritizeTask(taskId, beforeTaskId) {
445
- return fetch(`/api/tasks/${taskId}`, {
446
- method: 'PATCH',
447
- body: JSON.stringify({ beforeId: beforeTaskId })
448
- });
449
- }
450
- ```
451
-
452
- ### 3. Playlist Manager
453
-
454
- ```javascript
455
- // No filters - global ordering
456
- await api.use(PositioningPlugin, {
457
- field: 'playOrder'
458
- });
459
-
460
- // Add song to playlist
461
- async function addToPlaylist(songId, position = null) {
462
- return fetch('/api/playlist-songs', {
463
- method: 'POST',
464
- body: JSON.stringify({
465
- songId,
466
- beforeId: position // null = add to end
467
- })
468
- });
469
- }
470
- ```
471
-
472
- ### 4. Multi-Level Navigation Menu
473
-
474
- ```javascript
475
- // Configure with parent grouping
476
- await api.use(PositioningPlugin, {
477
- filters: ['parentId'], // Each menu level has its own ordering
478
- field: 'menuOrder'
479
- });
480
-
481
- // Create menu structure
482
- await createMenuItem({ title: 'Products', parentId: null }); // Top level
483
- await createMenuItem({ title: 'Software', parentId: 1 }); // Under Products
484
- await createMenuItem({ title: 'Hardware', parentId: 1, beforeId: 2 }); // Before Software
485
- ```
486
-
487
- ## How It Works
488
-
489
- ### Fractional Indexing Algorithm
490
-
491
- The plugin uses fractional indexing to generate position keys that can be infinitely subdivided:
492
-
493
- 1. **Initial positions**: First item gets "a0", second gets "a1", etc.
494
- 2. **Inserting between**: Between "a0" and "a1", we generate "a0m"
495
- 3. **Further subdivision**: Between "a0" and "a0m", we get "a0g"
496
- 4. **Infinite precision**: Can always find a key between any two keys
497
-
498
- **Why these strange strings?** The fractional-indexing algorithm uses a base-62 encoding (0-9, a-z, A-Z) to create sortable strings that can be infinitely subdivided. The strings are designed to:
499
- - Sort correctly as strings (no numeric parsing needed)
500
- - Allow insertion between any two values
501
- - Minimize string length growth
502
- - Work with any database that can sort strings
503
-
504
- ### Position Calculation Flow
505
-
506
- 1. **Request arrives** with optional `beforeId`
507
- 2. **Plugin extracts** the beforeId and filter field values
508
- 3. **Determines if positioning is needed**:
509
- - For POST: Always calculates position
510
- - For PATCH/PUT: Only if `beforeId` is provided
511
- - Changing filter fields alone does NOT trigger repositioning
512
- 4. **Query database** for items in the same position group (based on filter fields)
513
- 5. **Calculate position**:
514
- - If `beforeId` is null → place at end of the group
515
- - If `beforeId` is 'FIRST' → place at beginning of the group
516
- - If `beforeId` is an ID → find that item and place before it
517
- - If target item not found → place at end (fail-safe behavior)
518
- 6. **Store position** in the position field
519
- 7. **Save record** with calculated position
520
-
521
- **Key Insight**: The position is calculated relative to other items in the same "position group" (items with matching filter field values). An item with `status: 'todo'` has no position relationship with items where `status: 'done'`.
522
-
523
- ### Database Structure
524
-
525
- The plugin automatically creates an efficient composite index:
526
-
527
- ```sql
528
- CREATE INDEX idx_tasks_positioning ON tasks(status, projectId, position);
529
- ```
530
-
531
- This ensures fast queries for:
532
- - Retrieving ordered items within a group
533
- - Finding specific positions for insertion
534
- - Moving items between groups
535
-
536
- ## Migration Guide
537
-
538
- ### From Integer-Based Positioning
539
-
540
- If you have existing integer positions, you can migrate gradually:
541
-
542
- ```javascript
543
- // 1. Add the plugin (it works alongside existing positions)
544
- await api.use(PositioningPlugin, {
545
- field: 'sort_order' // Your existing field
546
- });
547
-
548
- // 2. New items will get fractional positions
549
- // 3. Existing integer positions still work (treated as strings)
550
- // 4. Optionally, batch convert integers to fractional:
551
-
552
- async function migratePositions() {
553
- const items = await knex('tasks').select('id', 'sort_order');
554
-
555
- for (let i = 0; i < items.length; i++) {
556
- const fractionalPos = generateKeyBetween(
557
- i > 0 ? items[i-1].sort_order : null,
558
- null
559
- );
560
-
561
- await knex('tasks')
562
- .where('id', items[i].id)
563
- .update({ sort_order: fractionalPos });
564
- }
565
- }
566
- ```
567
-
568
- ### Adding to Existing Resources
569
-
570
- The plugin automatically adds the position field to schemas:
571
-
572
- ```javascript
573
- // Before plugin
574
- api.addResource('items', {
575
- schema: {
576
- name: { type: 'string' }
577
- }
578
- });
579
-
580
- // After adding plugin
581
- // 'position' field is automatically added to the schema
582
- ```
583
-
584
- ## Performance Considerations
585
-
586
- ### Indexing
587
-
588
- The plugin creates optimal indexes automatically:
589
-
590
- ```sql
591
- -- For ungrouped positioning
592
- CREATE INDEX ON items(position);
593
-
594
- -- For grouped positioning
595
- CREATE INDEX ON items(status, projectId, position);
596
- ```
597
-
598
- ### Query Performance
599
-
600
- - **Retrieving ordered lists**: O(log n) with index
601
- - **Inserting items**: O(log n) to find position + O(1) to insert
602
- - **Moving items**: O(log n) to find positions + O(1) to update
603
-
604
- ### Position String Length
605
-
606
- Fractional keys can grow longer with many insertions in the same spot:
607
-
608
- - Starting positions: "a0", "a1" (2 characters)
609
- - After many insertions: "a0zzzzz" (7+ characters)
610
- - Plugin monitors length and can trigger rebalancing
611
- - In practice, this rarely happens with normal usage
612
-
613
- ### Best Practices
614
-
615
- 1. **Use grouping** when items have natural categories
616
- 2. **Avoid manual positions** unless migrating data
617
- 3. **Let the plugin handle positioning** for consistency
618
- 4. **Monitor position lengths** in high-activity systems
619
-
620
- ## Troubleshooting
621
-
622
- ### Common Issues
623
-
624
- **Items not maintaining order**
625
- - Check that no other sorting is applied in queries
626
- - Verify the position field contains valid fractional keys
627
- - Ensure you're querying within the correct position group
628
- - Remember: position values are strings, sorted lexicographically ("a10" comes before "a2"!)
629
-
630
- **Position field not present in schema**
631
- - The position field must exist in your schema
632
- - Check `excludeResources` configuration
633
- - The plugin will throw an error if the field is missing
634
- - Look for plugin initialization errors in logs
635
-
636
- **BeforeId not working**
637
- - Ensure the target item exists in the same position group
638
- - Check that filter field values match (e.g., same status, same projectId)
639
- - Verify beforeId is a valid ID (string or number)
640
- - Note: You cannot position relative to items in different groups
641
-
642
- **Items appear in wrong position after moving between groups**
643
- - This is expected behavior! Items keep their position when filter values change
644
- - Always provide a `beforeId` when changing filter fields
645
- - The plugin cannot guess where you want the item in the new group
646
-
647
- **Performance degradation**
648
- - Check if indexes were created successfully
649
- - Monitor position string lengths
650
- - Consider rebalancing if strings are very long
651
-
652
- ### Debug Logging
653
-
654
- Enable debug logging to see position calculations:
655
-
656
- ```javascript
657
- const api = new Api({
658
- name: 'my-api',
659
- logging: { level: 'debug' }
660
- });
661
- ```
662
-
663
- ### Manual Position Management
664
-
665
- For advanced use cases, you can work directly with positions:
666
-
667
- ```javascript
668
- import { generateKeyBetween } from 'fractional-indexing';
669
-
670
- // Generate a position between two items
671
- const newPosition = generateKeyBetween('a0', 'a1'); // Returns 'a0m'
672
-
673
- // Generate first position
674
- const firstPosition = generateKeyBetween(null, null); // Returns 'a0'
675
-
676
- // Generate last position after 'z5'
677
- const lastPosition = generateKeyBetween('z5', null); // Returns 'z6'
678
- ```
679
-
680
- ## Summary
681
-
682
- The Positioning Plugin provides a production-ready solution for maintaining custom order in your REST API resources. With fractional indexing and position grouping, it handles complex ordering requirements while maintaining excellent performance and avoiding conflicts.
683
-
684
- Key benefits:
685
- - **No batch updates** - Only the moved item is updated
686
- - **Infinite precision** - Always room to insert between items
687
- - **Natural API** - Works with drag-and-drop interfaces
688
- - **Grouped positioning** - Separate sequences per category
689
- - **Automatic indexes** - Optimal database performance
690
- - **Zero conflicts** - Multiple users can reorder simultaneously
691
-
692
- The plugin integrates seamlessly with the REST API plugin ecosystem, requiring minimal configuration while providing powerful positioning capabilities for modern applications.