json-rest-api 1.0.29 → 2.1.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 (294) 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 +733 -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 +103 -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 +3838 -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 +37 -0
  44. package/index.js +3 -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 +70 -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 +586 -418
  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 +120 -56
  71. package/plugins/core/lib/anyapi/stored-definition.js +109 -0
  72. package/plugins/core/lib/anyapi/utils/descriptor-helpers.js +0 -64
  73. package/plugins/core/lib/dbIntrospection.js +228 -21
  74. package/plugins/core/lib/dbTablesOperations.js +409 -92
  75. package/plugins/core/lib/fractional-positioning.js +12 -48
  76. package/plugins/core/lib/querying/identifier-query.js +19 -0
  77. package/plugins/core/lib/querying/include-query-helpers.js +364 -0
  78. package/plugins/core/lib/querying/include-to-many.js +428 -0
  79. package/plugins/core/lib/querying/include-to-one.js +579 -0
  80. package/plugins/core/lib/querying/knex-cross-table-search.js +31 -307
  81. package/plugins/core/lib/querying/knex-json-api-transformers-querying.js +99 -401
  82. package/plugins/core/lib/querying/knex-pagination-helpers.js +48 -317
  83. package/plugins/core/lib/querying/knex-process-includes.js +5 -110
  84. package/plugins/core/lib/querying/knex-query-helpers-base.js +21 -56
  85. package/plugins/core/lib/querying/knex-query-helpers.js +298 -448
  86. package/plugins/core/lib/querying/knex-relationship-includes.js +120 -2138
  87. package/plugins/core/lib/querying/knex-window-queries.js +62 -253
  88. package/plugins/core/lib/querying/query-builder-utils.js +44 -4
  89. package/plugins/core/lib/querying/query-constraint.d.ts +12 -0
  90. package/plugins/core/lib/querying/query-constraint.js +27 -0
  91. package/plugins/core/lib/querying/query-field-sort-helpers.js +134 -37
  92. package/plugins/core/lib/querying/relationship-identifiers.js +208 -0
  93. package/plugins/core/lib/querying/sort-helpers.js +13 -1
  94. package/plugins/core/lib/querying/storage-adapter-utils.js +13 -5
  95. package/plugins/core/lib/querying/url-helpers.js +11 -4
  96. package/plugins/core/lib/querying-writing/compile-schemas.js +148 -249
  97. package/plugins/core/lib/querying-writing/connectors-query-parser.js +33 -118
  98. package/plugins/core/lib/querying-writing/database-capabilities.js +149 -223
  99. package/plugins/core/lib/querying-writing/database-value-normalizers.js +179 -194
  100. package/plugins/core/lib/querying-writing/default-data-helpers.js +15 -45
  101. package/plugins/core/lib/querying-writing/field-selection-types.d.ts +43 -0
  102. package/plugins/core/lib/querying-writing/field-utils.js +113 -92
  103. package/plugins/core/lib/querying-writing/knex-constants.d.ts +18 -0
  104. package/plugins/core/lib/querying-writing/knex-constants.js +2 -26
  105. package/plugins/core/lib/querying-writing/knex-field-helpers.js +100 -290
  106. package/plugins/core/lib/querying-writing/knex-json-api-transformers.js +31 -120
  107. package/plugins/core/lib/querying-writing/query-field-helpers.js +108 -32
  108. package/plugins/core/lib/querying-writing/query-field-types.d.ts +60 -0
  109. package/plugins/core/lib/querying-writing/relationship-contracts.js +33 -13
  110. package/plugins/core/lib/querying-writing/request-contracts.js +93 -38
  111. package/plugins/core/lib/querying-writing/resource-id-normalization.js +23 -5
  112. package/plugins/core/lib/querying-writing/response-options.js +35 -0
  113. package/plugins/core/lib/querying-writing/schema-helpers.js +203 -324
  114. package/plugins/core/lib/querying-writing/scope-validations.js +92 -290
  115. package/plugins/core/lib/querying-writing/simplified-helpers.js +113 -712
  116. package/plugins/core/lib/storage/canonical-data-helpers.js +683 -0
  117. package/plugins/core/lib/storage/canonical-storage-mapping.js +51 -20
  118. package/plugins/core/lib/storage/ordinary-data-helpers.js +710 -0
  119. package/plugins/core/lib/storage/storage-adapter.js +128 -62
  120. package/plugins/core/lib/storage/storage-mapping.js +78 -28
  121. package/plugins/core/lib/storage/storage-types.d.ts +502 -0
  122. package/plugins/core/lib/writing/many-to-many-manipulations.js +114 -284
  123. package/plugins/core/lib/writing/relationship-processor.js +94 -158
  124. package/plugins/core/lib/writing/resource-version.js +172 -0
  125. package/plugins/core/lib/writing/reverse-relationship-manipulations.js +110 -0
  126. package/plugins/core/rest-api-anyapi-knex-plugin.js +290 -2475
  127. package/plugins/core/rest-api-autofilter-plugin.js +17 -19
  128. package/plugins/core/rest-api-cors-plugin.js +52 -205
  129. package/plugins/core/rest-api-knex-plugin.js +45 -1077
  130. package/plugins/core/rest-api-label-plugin.js +25 -38
  131. package/plugins/core/rest-api-plugin-hooks/register-relationship-routes.js +5 -5
  132. package/plugins/core/rest-api-plugin-hooks/register-scope-routes.js +9 -6
  133. package/plugins/core/rest-api-plugin-hooks/turn-scope-init-into-vars.js +7 -26
  134. package/plugins/core/rest-api-plugin-hooks/validate-include-configurations.js +20 -26
  135. package/plugins/core/rest-api-plugin-methods/add-route.js +4 -6
  136. package/plugins/core/rest-api-plugin-methods/apply-query-filters.js +42 -13
  137. package/plugins/core/rest-api-plugin-methods/check-permissions.js +11 -5
  138. package/plugins/core/rest-api-plugin-methods/common.js +291 -395
  139. package/plugins/core/rest-api-plugin-methods/delete-relationship.js +49 -47
  140. package/plugins/core/rest-api-plugin-methods/delete.js +31 -25
  141. package/plugins/core/rest-api-plugin-methods/enrich-attributes.js +108 -148
  142. package/plugins/core/rest-api-plugin-methods/get-related.js +113 -305
  143. package/plugins/core/rest-api-plugin-methods/get-relationship.js +25 -18
  144. package/plugins/core/rest-api-plugin-methods/get.js +36 -68
  145. package/plugins/core/rest-api-plugin-methods/lifecycle-types.d.ts +225 -0
  146. package/plugins/core/rest-api-plugin-methods/patch-relationship.js +28 -25
  147. package/plugins/core/rest-api-plugin-methods/patch.js +46 -99
  148. package/plugins/core/rest-api-plugin-methods/post-relationship.js +49 -53
  149. package/plugins/core/rest-api-plugin-methods/post.js +51 -51
  150. package/plugins/core/rest-api-plugin-methods/put.js +69 -148
  151. package/plugins/core/rest-api-plugin-methods/query.js +37 -81
  152. package/plugins/core/rest-api-plugin-methods/release.js +2 -1
  153. package/plugins/core/rest-api-plugin.js +32 -46
  154. package/plugins/core/rest-api-positioning-plugin.js +183 -628
  155. package/plugins/core/rest-api-query-projections-plugin.js +19 -102
  156. package/plugins/core/rest-api-row-policy-plugin.js +3 -3
  157. package/plugins/core/socketio-plugin.js +364 -344
  158. package/plugins/storage/local-storage.js +40 -36
  159. package/types/anyapi.d.ts +30 -0
  160. package/types/autofilter.d.ts +57 -0
  161. package/types/bulk-methods.d.ts +63 -0
  162. package/types/errors.d.ts +118 -0
  163. package/types/fastify-options.d.ts +19 -0
  164. package/types/file-detectors.d.ts +33 -0
  165. package/types/file-storage.d.ts +48 -0
  166. package/types/hook-context.d.ts +180 -0
  167. package/types/plugin-options.d.ts +65 -0
  168. package/types/relationship-methods.d.ts +74 -0
  169. package/types/representations.d.ts +50 -0
  170. package/types/resource-methods.d.ts +95 -0
  171. package/types/resource-schema.d.ts +157 -0
  172. package/types/row-policy.d.ts +31 -0
  173. package/types/runtime.d.ts +132 -0
  174. package/types/transactions.d.ts +21 -0
  175. package/.claude/agents/library-guide-expert.md +0 -30
  176. package/.claude/agents/rest-api-internals-expert.md +0 -54
  177. package/.claude/settings.local.json +0 -70
  178. package/AGENTS.md +0 -34
  179. package/CLAUDE.md +0 -160
  180. package/CRUSH.md +0 -55
  181. package/JSKIT_BACKEND_MIGRATION_TRACKER.txt +0 -180
  182. package/SINGLE_VALIDATION_CONTRACT_TODO.txt +0 -172
  183. package/TODO.todo +0 -13
  184. package/connectors_ideas/fastify.js +0 -217
  185. package/connectors_ideas/koa-plugin.js +0 -112
  186. package/docs/.bundle/config +0 -2
  187. package/docs/COMPARISON.md +0 -433
  188. package/docs/GUIDE/GUIDE_1_Initial_Setup.md +0 -1057
  189. package/docs/GUIDE/GUIDE_2_1_The_Starting_Point.md +0 -88
  190. package/docs/GUIDE/GUIDE_2_2_Manipulating_And_Searching_Tables.md +0 -808
  191. package/docs/GUIDE/GUIDE_2_3_BelongsTo_Relationships.md +0 -491
  192. package/docs/GUIDE/GUIDE_2_4_HasMany_Records.md +0 -398
  193. package/docs/GUIDE/GUIDE_2_5_HasMany_Polymorphic.md +0 -898
  194. package/docs/GUIDE/GUIDE_2_6_Many_To_Many.md +0 -712
  195. package/docs/GUIDE/GUIDE_2_7_Pagination_And_Ordering.md +0 -1247
  196. package/docs/GUIDE/GUIDE_2_8_Effects_of_PUT_and_PATCH.md +0 -748
  197. package/docs/GUIDE/GUIDE_2_9_Relationships_Urls.md +0 -517
  198. package/docs/GUIDE/GUIDE_2_Data_And_Relations.md +0 -66
  199. package/docs/GUIDE/GUIDE_3_Field_Transformations.md +0 -1252
  200. package/docs/GUIDE/GUIDE_5_Non-Db_Resources.md +0 -1107
  201. package/docs/GUIDE/GUIDE_6_Positioning.md +0 -692
  202. package/docs/GUIDE/GUIDE_7_Hooks_Data_Management_And_Plugins.md +0 -1491
  203. package/docs/GUIDE/GUIDE_X_Bulk_Operations.md +0 -732
  204. package/docs/GUIDE/GUIDE_X_Cors.md +0 -1957
  205. package/docs/GUIDE/GUIDE_X_Fastify.md +0 -78
  206. package/docs/GUIDE/GUIDE_X_File_Uploads.md +0 -729
  207. package/docs/GUIDE/GUIDE_X_Knex_Schema_And_Migrations.md +0 -370
  208. package/docs/GUIDE/GUIDE_X_Query_Projections.md +0 -159
  209. package/docs/GUIDE/GUIDE_X_SocketIO.md +0 -1040
  210. package/docs/GUIDE/GUIDE_X_Writing_Plugins.md +0 -201
  211. package/docs/Gemfile +0 -6
  212. package/docs/Gemfile.lock +0 -308
  213. package/docs/ONBOARDING.md +0 -315
  214. package/docs/_config.yml +0 -36
  215. package/docs/_layouts/default.html +0 -123
  216. package/docs/apple-touch-icon.svg +0 -4
  217. package/docs/assets/css/style.scss +0 -392
  218. package/docs/favicon.svg +0 -4
  219. package/docs/index.md +0 -164
  220. package/docs/mini-guides/file-uploads-complete.md +0 -161
  221. package/eslint.config.js +0 -13
  222. package/examples/url-override-example.js +0 -137
  223. package/fixing_plan.md +0 -112
  224. package/plugins/core/lib/anyapi/query/anyapi-query-adapter.js +0 -657
  225. package/plugins/core/lib/querying-writing/return-record-settings.js +0 -47
  226. package/plugins/core/lib/writing/knex-json-api-transformers-writing.js +0 -110
  227. package/plugins/storage/s3-storage.js +0 -93
  228. package/quickTest.js +0 -136
  229. package/scripts/debug-anyapi-review-filter.mjs +0 -63
  230. package/scripts/inspect-anyapi-descriptor-country.mjs +0 -19
  231. package/scripts/inspect-anyapi-descriptor.mjs +0 -19
  232. package/scripts/run-docs.js +0 -133
  233. package/test-debug.js +0 -59
  234. package/tests/README.md +0 -202
  235. package/tests/TEST_TEMPLATE.test.js +0 -178
  236. package/tests/anyapi-basic.test.js +0 -105
  237. package/tests/anyapi-cursor-pagination.test.js +0 -148
  238. package/tests/anyapi-custom-idproperty.test.js +0 -163
  239. package/tests/autofilter.test.js +0 -463
  240. package/tests/bulk-operations.test.js +0 -657
  241. package/tests/computed-fields.test.js +0 -586
  242. package/tests/cors.test.js +0 -834
  243. package/tests/curl.test.js +0 -467
  244. package/tests/custom-id-mysql-returning-fallback.test.js +0 -128
  245. package/tests/custom-id-simplified-post.test.js +0 -75
  246. package/tests/custom-idproperty-relationships.test.js +0 -932
  247. package/tests/db-introspection.test.js +0 -521
  248. package/tests/db-table-operations.test.js +0 -798
  249. package/tests/fastify-plugin.test.js +0 -184
  250. package/tests/field-getters.test.js +0 -370
  251. package/tests/field-setters.test.js +0 -306
  252. package/tests/file-handling.test.js +0 -155
  253. package/tests/fixtures/api-configs.js +0 -2566
  254. package/tests/full-jsonapi-belongsto-linkage.test.js +0 -150
  255. package/tests/helpers/fake-fastify.js +0 -99
  256. package/tests/helpers/storage-mode.js +0 -65
  257. package/tests/helpers/test-utils.js +0 -203
  258. package/tests/http-connectors-parity.test.js +0 -395
  259. package/tests/id-normalization.test.js +0 -444
  260. package/tests/include-depth-validation.test.js +0 -331
  261. package/tests/includes.test.js +0 -427
  262. package/tests/local-storage.test.js +0 -96
  263. package/tests/logical-id-write-validation.test.js +0 -151
  264. package/tests/nested-includes.test.js +0 -617
  265. package/tests/pagination-boundaries.test.js +0 -154
  266. package/tests/pagination-contract.test.js +0 -122
  267. package/tests/pagination-cursor-multifield.test.js +0 -641
  268. package/tests/pagination-enhanced.test.js +0 -563
  269. package/tests/polymorphic-relationships.test.js +0 -347
  270. package/tests/polymorphic-simple.test.js +0 -148
  271. package/tests/positioning-advanced.test.js +0 -692
  272. package/tests/positioning-extended.test.js +0 -915
  273. package/tests/positioning-multifilter.test.js +0 -699
  274. package/tests/positioning.test.js +0 -560
  275. package/tests/post-commit-hooks.test.js +0 -120
  276. package/tests/projected-fields.test.js +0 -319
  277. package/tests/public-exports.test.js +0 -15
  278. package/tests/put-create-parity.test.js +0 -216
  279. package/tests/queries.test.js +0 -521
  280. package/tests/query-limits.test.js +0 -584
  281. package/tests/relationship-endpoints.test.js +0 -620
  282. package/tests/relationship-include-storage-adapter.test.js +0 -56
  283. package/tests/relationships.test.js +0 -681
  284. package/tests/rest-api.test.js +0 -337
  285. package/tests/return-record-settings.test.js +0 -184
  286. package/tests/row-policy.test.js +0 -489
  287. package/tests/s3-storage.test.js +0 -38
  288. package/tests/searchschema-knex-behavior.test.js +0 -187
  289. package/tests/searchschema-merge.test.js +0 -151
  290. package/tests/socketio.test.js +0 -860
  291. package/tests/storage-mapping.test.js +0 -362
  292. package/tests/temporal-boundaries.test.js +0 -208
  293. package/tests/test-development-guide.md +0 -370
  294. 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.