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.
- package/LICENSE +10 -670
- package/LICENSE-GPL-3.0 +674 -0
- package/LICENSE-MIT +21 -0
- package/README.md +39 -30
- package/docs/API.md +744 -1273
- package/docs/GUIDE/01-getting-started.md +338 -0
- package/docs/GUIDE/02-resources-and-relationships.md +33 -0
- package/docs/GUIDE/03-running-example.md +87 -0
- package/docs/GUIDE/04-creating-and-querying.md +181 -0
- package/docs/GUIDE/05-belongs-to.md +177 -0
- package/docs/GUIDE/06-has-many.md +184 -0
- package/docs/GUIDE/07-polymorphic-relationships.md +191 -0
- package/docs/GUIDE/08-many-to-many.md +184 -0
- package/docs/GUIDE/09-pagination-and-sorting.md +149 -0
- package/docs/GUIDE/10-put-and-patch.md +174 -0
- package/docs/GUIDE/11-relationship-endpoints.md +174 -0
- package/docs/GUIDE/12-field-transformations.md +287 -0
- package/docs/GUIDE/13-hooks-and-lifecycle.md +479 -0
- package/docs/GUIDE/14-custom-resource-methods.md +125 -0
- package/docs/GUIDE/15-query-projections.md +266 -0
- package/docs/GUIDE/{GUIDE_X_Autofiltering.md → 16-autofiltering.md} +77 -15
- package/docs/GUIDE/{GUIDE_X_Row_Policies.md → 17-row-policies.md} +137 -44
- package/docs/GUIDE/18-labels.md +86 -0
- package/docs/GUIDE/19-managed-transactions.md +179 -0
- package/docs/GUIDE/20-transaction-outcomes.md +207 -0
- package/docs/GUIDE/21-schema-and-migrations.md +733 -0
- package/docs/GUIDE/22-version-field-migrations.md +208 -0
- package/docs/GUIDE/23-fastify.md +179 -0
- package/docs/GUIDE/24-cors.md +172 -0
- package/docs/GUIDE/{GUIDE_X_URL_Management.md → 25-url-management.md} +7 -1
- package/docs/GUIDE/26-file-uploads.md +278 -0
- package/docs/GUIDE/27-bulk-operations.md +277 -0
- package/docs/GUIDE/28-socketio.md +325 -0
- package/docs/GUIDE/29-writing-plugins.md +278 -0
- package/docs/GUIDE/30-backend-capabilities.md +103 -0
- package/docs/GUIDE/31-positioning.md +181 -0
- package/docs/GUIDE/{GUIDE_Y_Appendices.md → 32-date-and-time.md} +74 -30
- package/docs/GUIDE/33-migrating-to-v2.md +3838 -0
- package/docs/GUIDE/index.md +51 -59
- package/docs/QUICKSTART.md +120 -287
- package/docs/architecture.md +75 -0
- package/docs/contributing.md +178 -0
- package/index.d.ts +37 -0
- package/index.js +3 -22
- package/lib/enhanced-logger.js +91 -85
- package/lib/error-context.js +445 -5
- package/lib/error-formatter.js +219 -59
- package/lib/knex-transaction.js +70 -0
- package/lib/logger-types.d.ts +22 -0
- package/lib/missing-package.js +4 -0
- package/lib/rest-api-errors.js +103 -8
- package/lib/runtime/json-rest-api.js +172 -0
- package/lib/transaction-types.d.ts +59 -0
- package/package.json +97 -27
- package/plugins/core/bulk-operations-plugin.js +191 -184
- package/plugins/core/connectors/express-plugin.js +148 -219
- package/plugins/core/connectors/fastify-plugin.js +125 -82
- package/plugins/core/connectors/lib/busboy-detector.js +74 -156
- package/plugins/core/connectors/lib/connector-core.js +144 -20
- package/plugins/core/connectors/lib/formidable-detector.js +63 -120
- package/plugins/core/connectors/lib/http-validators.js +77 -0
- package/plugins/core/connectors/lib/multipart-helpers.js +18 -0
- package/plugins/core/connectors/lib/request-helpers.js +10 -1
- package/plugins/core/connectors/lib/transport-http-helpers.js +89 -8
- package/plugins/core/connectors/lib/transport-route-schemas.js +7 -4
- package/plugins/core/file-handling-plugin.js +87 -177
- package/plugins/core/lib/anyapi/anyapi-registry.js +586 -418
- package/plugins/core/lib/anyapi/canonical-link-store.js +507 -0
- package/plugins/core/lib/anyapi/canonical-relationship-reader.js +598 -0
- package/plugins/core/lib/anyapi/schema-utils.js +120 -56
- package/plugins/core/lib/anyapi/stored-definition.js +109 -0
- package/plugins/core/lib/anyapi/utils/descriptor-helpers.js +0 -64
- package/plugins/core/lib/dbIntrospection.js +228 -21
- package/plugins/core/lib/dbTablesOperations.js +409 -92
- package/plugins/core/lib/fractional-positioning.js +12 -48
- package/plugins/core/lib/querying/identifier-query.js +19 -0
- package/plugins/core/lib/querying/include-query-helpers.js +364 -0
- package/plugins/core/lib/querying/include-to-many.js +428 -0
- package/plugins/core/lib/querying/include-to-one.js +579 -0
- package/plugins/core/lib/querying/knex-cross-table-search.js +31 -307
- package/plugins/core/lib/querying/knex-json-api-transformers-querying.js +99 -401
- package/plugins/core/lib/querying/knex-pagination-helpers.js +48 -317
- package/plugins/core/lib/querying/knex-process-includes.js +5 -110
- package/plugins/core/lib/querying/knex-query-helpers-base.js +21 -56
- package/plugins/core/lib/querying/knex-query-helpers.js +298 -448
- package/plugins/core/lib/querying/knex-relationship-includes.js +120 -2138
- package/plugins/core/lib/querying/knex-window-queries.js +62 -253
- package/plugins/core/lib/querying/query-builder-utils.js +44 -4
- package/plugins/core/lib/querying/query-constraint.d.ts +12 -0
- package/plugins/core/lib/querying/query-constraint.js +27 -0
- package/plugins/core/lib/querying/query-field-sort-helpers.js +134 -37
- package/plugins/core/lib/querying/relationship-identifiers.js +208 -0
- package/plugins/core/lib/querying/sort-helpers.js +13 -1
- package/plugins/core/lib/querying/storage-adapter-utils.js +13 -5
- package/plugins/core/lib/querying/url-helpers.js +11 -4
- package/plugins/core/lib/querying-writing/compile-schemas.js +148 -249
- package/plugins/core/lib/querying-writing/connectors-query-parser.js +33 -118
- package/plugins/core/lib/querying-writing/database-capabilities.js +149 -223
- package/plugins/core/lib/querying-writing/database-value-normalizers.js +179 -194
- package/plugins/core/lib/querying-writing/default-data-helpers.js +15 -45
- package/plugins/core/lib/querying-writing/field-selection-types.d.ts +43 -0
- package/plugins/core/lib/querying-writing/field-utils.js +113 -92
- package/plugins/core/lib/querying-writing/knex-constants.d.ts +18 -0
- package/plugins/core/lib/querying-writing/knex-constants.js +2 -26
- package/plugins/core/lib/querying-writing/knex-field-helpers.js +100 -290
- package/plugins/core/lib/querying-writing/knex-json-api-transformers.js +31 -120
- package/plugins/core/lib/querying-writing/query-field-helpers.js +108 -32
- package/plugins/core/lib/querying-writing/query-field-types.d.ts +60 -0
- package/plugins/core/lib/querying-writing/relationship-contracts.js +33 -13
- package/plugins/core/lib/querying-writing/request-contracts.js +93 -38
- package/plugins/core/lib/querying-writing/resource-id-normalization.js +23 -5
- package/plugins/core/lib/querying-writing/response-options.js +35 -0
- package/plugins/core/lib/querying-writing/schema-helpers.js +203 -324
- package/plugins/core/lib/querying-writing/scope-validations.js +92 -290
- package/plugins/core/lib/querying-writing/simplified-helpers.js +113 -712
- package/plugins/core/lib/storage/canonical-data-helpers.js +683 -0
- package/plugins/core/lib/storage/canonical-storage-mapping.js +51 -20
- package/plugins/core/lib/storage/ordinary-data-helpers.js +710 -0
- package/plugins/core/lib/storage/storage-adapter.js +128 -62
- package/plugins/core/lib/storage/storage-mapping.js +78 -28
- package/plugins/core/lib/storage/storage-types.d.ts +502 -0
- package/plugins/core/lib/writing/many-to-many-manipulations.js +114 -284
- package/plugins/core/lib/writing/relationship-processor.js +94 -158
- package/plugins/core/lib/writing/resource-version.js +172 -0
- package/plugins/core/lib/writing/reverse-relationship-manipulations.js +110 -0
- package/plugins/core/rest-api-anyapi-knex-plugin.js +290 -2475
- package/plugins/core/rest-api-autofilter-plugin.js +17 -19
- package/plugins/core/rest-api-cors-plugin.js +52 -205
- package/plugins/core/rest-api-knex-plugin.js +45 -1077
- package/plugins/core/rest-api-label-plugin.js +25 -38
- package/plugins/core/rest-api-plugin-hooks/register-relationship-routes.js +5 -5
- package/plugins/core/rest-api-plugin-hooks/register-scope-routes.js +9 -6
- package/plugins/core/rest-api-plugin-hooks/turn-scope-init-into-vars.js +7 -26
- package/plugins/core/rest-api-plugin-hooks/validate-include-configurations.js +20 -26
- package/plugins/core/rest-api-plugin-methods/add-route.js +4 -6
- package/plugins/core/rest-api-plugin-methods/apply-query-filters.js +42 -13
- package/plugins/core/rest-api-plugin-methods/check-permissions.js +11 -5
- package/plugins/core/rest-api-plugin-methods/common.js +291 -395
- package/plugins/core/rest-api-plugin-methods/delete-relationship.js +49 -47
- package/plugins/core/rest-api-plugin-methods/delete.js +31 -25
- package/plugins/core/rest-api-plugin-methods/enrich-attributes.js +108 -148
- package/plugins/core/rest-api-plugin-methods/get-related.js +113 -305
- package/plugins/core/rest-api-plugin-methods/get-relationship.js +25 -18
- package/plugins/core/rest-api-plugin-methods/get.js +36 -68
- package/plugins/core/rest-api-plugin-methods/lifecycle-types.d.ts +225 -0
- package/plugins/core/rest-api-plugin-methods/patch-relationship.js +28 -25
- package/plugins/core/rest-api-plugin-methods/patch.js +46 -99
- package/plugins/core/rest-api-plugin-methods/post-relationship.js +49 -53
- package/plugins/core/rest-api-plugin-methods/post.js +51 -51
- package/plugins/core/rest-api-plugin-methods/put.js +69 -148
- package/plugins/core/rest-api-plugin-methods/query.js +37 -81
- package/plugins/core/rest-api-plugin-methods/release.js +2 -1
- package/plugins/core/rest-api-plugin.js +32 -46
- package/plugins/core/rest-api-positioning-plugin.js +183 -628
- package/plugins/core/rest-api-query-projections-plugin.js +19 -102
- package/plugins/core/rest-api-row-policy-plugin.js +3 -3
- package/plugins/core/socketio-plugin.js +364 -344
- package/plugins/storage/local-storage.js +40 -36
- package/types/anyapi.d.ts +30 -0
- package/types/autofilter.d.ts +57 -0
- package/types/bulk-methods.d.ts +63 -0
- package/types/errors.d.ts +118 -0
- package/types/fastify-options.d.ts +19 -0
- package/types/file-detectors.d.ts +33 -0
- package/types/file-storage.d.ts +48 -0
- package/types/hook-context.d.ts +180 -0
- package/types/plugin-options.d.ts +65 -0
- package/types/relationship-methods.d.ts +74 -0
- package/types/representations.d.ts +50 -0
- package/types/resource-methods.d.ts +95 -0
- package/types/resource-schema.d.ts +157 -0
- package/types/row-policy.d.ts +31 -0
- package/types/runtime.d.ts +132 -0
- package/types/transactions.d.ts +21 -0
- package/.claude/agents/library-guide-expert.md +0 -30
- package/.claude/agents/rest-api-internals-expert.md +0 -54
- package/.claude/settings.local.json +0 -70
- package/AGENTS.md +0 -34
- package/CLAUDE.md +0 -160
- package/CRUSH.md +0 -55
- package/JSKIT_BACKEND_MIGRATION_TRACKER.txt +0 -180
- package/SINGLE_VALIDATION_CONTRACT_TODO.txt +0 -172
- package/TODO.todo +0 -13
- package/connectors_ideas/fastify.js +0 -217
- package/connectors_ideas/koa-plugin.js +0 -112
- package/docs/.bundle/config +0 -2
- package/docs/COMPARISON.md +0 -433
- package/docs/GUIDE/GUIDE_1_Initial_Setup.md +0 -1057
- package/docs/GUIDE/GUIDE_2_1_The_Starting_Point.md +0 -88
- package/docs/GUIDE/GUIDE_2_2_Manipulating_And_Searching_Tables.md +0 -808
- package/docs/GUIDE/GUIDE_2_3_BelongsTo_Relationships.md +0 -491
- package/docs/GUIDE/GUIDE_2_4_HasMany_Records.md +0 -398
- package/docs/GUIDE/GUIDE_2_5_HasMany_Polymorphic.md +0 -898
- package/docs/GUIDE/GUIDE_2_6_Many_To_Many.md +0 -712
- package/docs/GUIDE/GUIDE_2_7_Pagination_And_Ordering.md +0 -1247
- package/docs/GUIDE/GUIDE_2_8_Effects_of_PUT_and_PATCH.md +0 -748
- package/docs/GUIDE/GUIDE_2_9_Relationships_Urls.md +0 -517
- package/docs/GUIDE/GUIDE_2_Data_And_Relations.md +0 -66
- package/docs/GUIDE/GUIDE_3_Field_Transformations.md +0 -1252
- package/docs/GUIDE/GUIDE_5_Non-Db_Resources.md +0 -1107
- package/docs/GUIDE/GUIDE_6_Positioning.md +0 -692
- package/docs/GUIDE/GUIDE_7_Hooks_Data_Management_And_Plugins.md +0 -1491
- package/docs/GUIDE/GUIDE_X_Bulk_Operations.md +0 -732
- package/docs/GUIDE/GUIDE_X_Cors.md +0 -1957
- package/docs/GUIDE/GUIDE_X_Fastify.md +0 -78
- package/docs/GUIDE/GUIDE_X_File_Uploads.md +0 -729
- package/docs/GUIDE/GUIDE_X_Knex_Schema_And_Migrations.md +0 -370
- package/docs/GUIDE/GUIDE_X_Query_Projections.md +0 -159
- package/docs/GUIDE/GUIDE_X_SocketIO.md +0 -1040
- package/docs/GUIDE/GUIDE_X_Writing_Plugins.md +0 -201
- package/docs/Gemfile +0 -6
- package/docs/Gemfile.lock +0 -308
- package/docs/ONBOARDING.md +0 -315
- package/docs/_config.yml +0 -36
- package/docs/_layouts/default.html +0 -123
- package/docs/apple-touch-icon.svg +0 -4
- package/docs/assets/css/style.scss +0 -392
- package/docs/favicon.svg +0 -4
- package/docs/index.md +0 -164
- package/docs/mini-guides/file-uploads-complete.md +0 -161
- package/eslint.config.js +0 -13
- package/examples/url-override-example.js +0 -137
- package/fixing_plan.md +0 -112
- package/plugins/core/lib/anyapi/query/anyapi-query-adapter.js +0 -657
- package/plugins/core/lib/querying-writing/return-record-settings.js +0 -47
- package/plugins/core/lib/writing/knex-json-api-transformers-writing.js +0 -110
- package/plugins/storage/s3-storage.js +0 -93
- package/quickTest.js +0 -136
- package/scripts/debug-anyapi-review-filter.mjs +0 -63
- package/scripts/inspect-anyapi-descriptor-country.mjs +0 -19
- package/scripts/inspect-anyapi-descriptor.mjs +0 -19
- package/scripts/run-docs.js +0 -133
- package/test-debug.js +0 -59
- package/tests/README.md +0 -202
- package/tests/TEST_TEMPLATE.test.js +0 -178
- package/tests/anyapi-basic.test.js +0 -105
- package/tests/anyapi-cursor-pagination.test.js +0 -148
- package/tests/anyapi-custom-idproperty.test.js +0 -163
- package/tests/autofilter.test.js +0 -463
- package/tests/bulk-operations.test.js +0 -657
- package/tests/computed-fields.test.js +0 -586
- package/tests/cors.test.js +0 -834
- package/tests/curl.test.js +0 -467
- package/tests/custom-id-mysql-returning-fallback.test.js +0 -128
- package/tests/custom-id-simplified-post.test.js +0 -75
- package/tests/custom-idproperty-relationships.test.js +0 -932
- package/tests/db-introspection.test.js +0 -521
- package/tests/db-table-operations.test.js +0 -798
- package/tests/fastify-plugin.test.js +0 -184
- package/tests/field-getters.test.js +0 -370
- package/tests/field-setters.test.js +0 -306
- package/tests/file-handling.test.js +0 -155
- package/tests/fixtures/api-configs.js +0 -2566
- package/tests/full-jsonapi-belongsto-linkage.test.js +0 -150
- package/tests/helpers/fake-fastify.js +0 -99
- package/tests/helpers/storage-mode.js +0 -65
- package/tests/helpers/test-utils.js +0 -203
- package/tests/http-connectors-parity.test.js +0 -395
- package/tests/id-normalization.test.js +0 -444
- package/tests/include-depth-validation.test.js +0 -331
- package/tests/includes.test.js +0 -427
- package/tests/local-storage.test.js +0 -96
- package/tests/logical-id-write-validation.test.js +0 -151
- package/tests/nested-includes.test.js +0 -617
- package/tests/pagination-boundaries.test.js +0 -154
- package/tests/pagination-contract.test.js +0 -122
- package/tests/pagination-cursor-multifield.test.js +0 -641
- package/tests/pagination-enhanced.test.js +0 -563
- package/tests/polymorphic-relationships.test.js +0 -347
- package/tests/polymorphic-simple.test.js +0 -148
- package/tests/positioning-advanced.test.js +0 -692
- package/tests/positioning-extended.test.js +0 -915
- package/tests/positioning-multifilter.test.js +0 -699
- package/tests/positioning.test.js +0 -560
- package/tests/post-commit-hooks.test.js +0 -120
- package/tests/projected-fields.test.js +0 -319
- package/tests/public-exports.test.js +0 -15
- package/tests/put-create-parity.test.js +0 -216
- package/tests/queries.test.js +0 -521
- package/tests/query-limits.test.js +0 -584
- package/tests/relationship-endpoints.test.js +0 -620
- package/tests/relationship-include-storage-adapter.test.js +0 -56
- package/tests/relationships.test.js +0 -681
- package/tests/rest-api.test.js +0 -337
- package/tests/return-record-settings.test.js +0 -184
- package/tests/row-policy.test.js +0 -489
- package/tests/s3-storage.test.js +0 -38
- package/tests/searchschema-knex-behavior.test.js +0 -187
- package/tests/searchschema-merge.test.js +0 -151
- package/tests/socketio.test.js +0 -860
- package/tests/storage-mapping.test.js +0 -362
- package/tests/temporal-boundaries.test.js +0 -208
- package/tests/test-development-guide.md +0 -370
- 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.
|