json-rest-api 1.0.28 → 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.
- 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 +746 -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 +634 -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 +100 -0
- package/docs/GUIDE/31-positioning.md +181 -0
- package/docs/GUIDE/{GUIDE_Y_Appendices.md → 32-date-and-time.md} +90 -31
- package/docs/GUIDE/33-migrating-to-v2.md +3827 -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 +36 -0
- package/index.js +1 -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 +64 -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 -174
- package/plugins/core/lib/anyapi/anyapi-registry.js +213 -164
- 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 +25 -16
- 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 +67 -330
- 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 +205 -30
- package/plugins/core/lib/querying/relationship-identifiers.js +208 -0
- package/plugins/core/lib/querying/sort-helpers.js +20 -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 +238 -187
- 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 +111 -287
- 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 +109 -44
- 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 +142 -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 +116 -2509
- 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 -1069
- 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 -385
- 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 -145
- 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 +38 -69
- 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 +39 -82
- 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/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 +53 -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 -1238
- 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 -150
- 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 -118
- package/tests/fixtures/api-configs.js +0 -2509
- 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-cursor-multifield.test.js +0 -643
- 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/test-development-guide.md +0 -370
- package/tests/virtual-fields.test.js +0 -404
|
@@ -1,915 +0,0 @@
|
|
|
1
|
-
import { describe, it, before, after, beforeEach } from 'node:test'
|
|
2
|
-
import assert from 'node:assert/strict'
|
|
3
|
-
import knexLib from 'knex'
|
|
4
|
-
import { cleanTables } from './helpers/test-utils.js'
|
|
5
|
-
import { createPositioningApi } from './fixtures/api-configs.js'
|
|
6
|
-
import { PositioningPlugin } from '../plugins/core/rest-api-positioning-plugin.js'
|
|
7
|
-
|
|
8
|
-
// Create Knex instance for tests
|
|
9
|
-
const knex = knexLib({
|
|
10
|
-
client: 'better-sqlite3',
|
|
11
|
-
connection: {
|
|
12
|
-
filename: ':memory:'
|
|
13
|
-
},
|
|
14
|
-
useNullAsDefault: true
|
|
15
|
-
})
|
|
16
|
-
|
|
17
|
-
// API instance that persists across tests
|
|
18
|
-
let api
|
|
19
|
-
|
|
20
|
-
describe('Extended Positioning Plugin Tests', { timeout: 30000 }, () => {
|
|
21
|
-
before(async () => {
|
|
22
|
-
// Initialize API once
|
|
23
|
-
api = await createPositioningApi(knex)
|
|
24
|
-
|
|
25
|
-
// Install positioning plugin with default configuration
|
|
26
|
-
await api.use(PositioningPlugin, {
|
|
27
|
-
field: 'position',
|
|
28
|
-
filters: ['category'],
|
|
29
|
-
defaultPosition: 'last',
|
|
30
|
-
autoIndex: true
|
|
31
|
-
})
|
|
32
|
-
})
|
|
33
|
-
|
|
34
|
-
after(async () => {
|
|
35
|
-
await knex.destroy()
|
|
36
|
-
})
|
|
37
|
-
|
|
38
|
-
beforeEach(async () => {
|
|
39
|
-
await cleanTables(knex, [
|
|
40
|
-
'positioning_categories',
|
|
41
|
-
'positioning_tasks',
|
|
42
|
-
'positioning_projects',
|
|
43
|
-
'positioning_items'
|
|
44
|
-
])
|
|
45
|
-
})
|
|
46
|
-
|
|
47
|
-
describe('Edge Cases for Positioning', () => {
|
|
48
|
-
it('should handle positioning multiple items before the same target in sequence', async () => {
|
|
49
|
-
const category = await api.resources.categories.post({ name: 'Test Category' })
|
|
50
|
-
|
|
51
|
-
// Create initial task
|
|
52
|
-
const task1 = await api.resources.tasks.post({
|
|
53
|
-
title: 'Task 1',
|
|
54
|
-
category: category.id
|
|
55
|
-
})
|
|
56
|
-
|
|
57
|
-
// Position multiple tasks before task1
|
|
58
|
-
const taskA = await api.resources.tasks.post({
|
|
59
|
-
title: 'Task A',
|
|
60
|
-
category: category.id,
|
|
61
|
-
beforeId: task1.id
|
|
62
|
-
})
|
|
63
|
-
|
|
64
|
-
const taskB = await api.resources.tasks.post({
|
|
65
|
-
title: 'Task B',
|
|
66
|
-
category: category.id,
|
|
67
|
-
beforeId: task1.id
|
|
68
|
-
})
|
|
69
|
-
|
|
70
|
-
const taskC = await api.resources.tasks.post({
|
|
71
|
-
title: 'Task C',
|
|
72
|
-
category: category.id,
|
|
73
|
-
beforeId: task1.id
|
|
74
|
-
})
|
|
75
|
-
|
|
76
|
-
// Query and verify order
|
|
77
|
-
const { data: tasks } = await api.resources.tasks.query({
|
|
78
|
-
queryParams: {
|
|
79
|
-
filters: { category: category.id },
|
|
80
|
-
sort: ['position']
|
|
81
|
-
}
|
|
82
|
-
})
|
|
83
|
-
|
|
84
|
-
// Should be: A, B, C, 1 (in creation order when positioned before same target)
|
|
85
|
-
assert.equal(tasks.length, 4)
|
|
86
|
-
assert.equal(tasks[0].title, 'Task A')
|
|
87
|
-
assert.equal(tasks[1].title, 'Task B')
|
|
88
|
-
assert.equal(tasks[2].title, 'Task C')
|
|
89
|
-
assert.equal(tasks[3].title, 'Task 1')
|
|
90
|
-
|
|
91
|
-
// Verify positions are unique
|
|
92
|
-
const positions = tasks.map(t => t.position)
|
|
93
|
-
const uniquePositions = [...new Set(positions)]
|
|
94
|
-
assert.equal(uniquePositions.length, 4, 'All positions should be unique')
|
|
95
|
-
})
|
|
96
|
-
|
|
97
|
-
it('should handle positioning between very close positions', async () => {
|
|
98
|
-
const category = await api.resources.categories.post({ name: 'Test Category' })
|
|
99
|
-
|
|
100
|
-
// Create initial tasks
|
|
101
|
-
const task1 = await api.resources.tasks.post({
|
|
102
|
-
title: 'Task 1',
|
|
103
|
-
category: category.id
|
|
104
|
-
})
|
|
105
|
-
|
|
106
|
-
const task2 = await api.resources.tasks.post({
|
|
107
|
-
title: 'Task 2',
|
|
108
|
-
category: category.id
|
|
109
|
-
})
|
|
110
|
-
|
|
111
|
-
// Insert many tasks between 1 and 2
|
|
112
|
-
const insertedTasks = []
|
|
113
|
-
for (let i = 0; i < 10; i++) {
|
|
114
|
-
const task = await api.resources.tasks.post({
|
|
115
|
-
title: `Inserted Task ${i}`,
|
|
116
|
-
category: category.id,
|
|
117
|
-
beforeId: task2.id
|
|
118
|
-
})
|
|
119
|
-
insertedTasks.push(task)
|
|
120
|
-
}
|
|
121
|
-
|
|
122
|
-
// Query and verify all are properly ordered
|
|
123
|
-
const { data: tasks } = await api.resources.tasks.query({
|
|
124
|
-
queryParams: {
|
|
125
|
-
filters: { category: category.id },
|
|
126
|
-
sort: ['position']
|
|
127
|
-
}
|
|
128
|
-
})
|
|
129
|
-
|
|
130
|
-
assert.equal(tasks.length, 12)
|
|
131
|
-
|
|
132
|
-
// Verify positions are in correct order
|
|
133
|
-
for (let i = 1; i < tasks.length; i++) {
|
|
134
|
-
assert(tasks[i - 1].position < tasks[i].position,
|
|
135
|
-
`Task ${tasks[i - 1].title} should come before ${tasks[i].title}`)
|
|
136
|
-
}
|
|
137
|
-
})
|
|
138
|
-
|
|
139
|
-
it('should handle beforeId pointing to non-existent item in same category', async () => {
|
|
140
|
-
const category = await api.resources.categories.post({ name: 'Test Category' })
|
|
141
|
-
|
|
142
|
-
const task1 = await api.resources.tasks.post({
|
|
143
|
-
title: 'Task 1',
|
|
144
|
-
category: category.id
|
|
145
|
-
})
|
|
146
|
-
|
|
147
|
-
// Create task with non-existent beforeId
|
|
148
|
-
const task2 = await api.resources.tasks.post({
|
|
149
|
-
title: 'Task 2',
|
|
150
|
-
category: category.id,
|
|
151
|
-
beforeId: '99999'
|
|
152
|
-
})
|
|
153
|
-
|
|
154
|
-
// Should position at end since beforeId doesn't exist
|
|
155
|
-
const { data: tasks } = await api.resources.tasks.query({
|
|
156
|
-
queryParams: {
|
|
157
|
-
filters: { category: category.id },
|
|
158
|
-
sort: ['position']
|
|
159
|
-
}
|
|
160
|
-
})
|
|
161
|
-
|
|
162
|
-
assert.equal(tasks.length, 2)
|
|
163
|
-
assert.equal(tasks[0].title, 'Task 1')
|
|
164
|
-
assert.equal(tasks[1].title, 'Task 2')
|
|
165
|
-
})
|
|
166
|
-
|
|
167
|
-
it('should handle beforeId from different category', async () => {
|
|
168
|
-
const category1 = await api.resources.categories.post({ name: 'Category 1' })
|
|
169
|
-
const category2 = await api.resources.categories.post({ name: 'Category 2' })
|
|
170
|
-
|
|
171
|
-
const task1 = await api.resources.tasks.post({
|
|
172
|
-
title: 'Task 1',
|
|
173
|
-
category: category1.id
|
|
174
|
-
})
|
|
175
|
-
|
|
176
|
-
const task2 = await api.resources.tasks.post({
|
|
177
|
-
title: 'Task 2',
|
|
178
|
-
category: category2.id,
|
|
179
|
-
beforeId: task1.id // Task from different category
|
|
180
|
-
})
|
|
181
|
-
|
|
182
|
-
// Task 2 should be positioned normally in its category (ignoring beforeId from other category)
|
|
183
|
-
const { data: cat2Tasks } = await api.resources.tasks.query({
|
|
184
|
-
queryParams: {
|
|
185
|
-
filters: { category: category2.id },
|
|
186
|
-
sort: ['position']
|
|
187
|
-
}
|
|
188
|
-
})
|
|
189
|
-
|
|
190
|
-
assert.equal(cat2Tasks.length, 1)
|
|
191
|
-
assert.equal(cat2Tasks[0].title, 'Task 2')
|
|
192
|
-
assert(cat2Tasks[0].position) // Should have a position
|
|
193
|
-
})
|
|
194
|
-
})
|
|
195
|
-
|
|
196
|
-
describe('Complex Update Scenarios', () => {
|
|
197
|
-
it('should handle moving items multiple times', async () => {
|
|
198
|
-
const category = await api.resources.categories.post({ name: 'Test Category' })
|
|
199
|
-
|
|
200
|
-
// Create initial tasks
|
|
201
|
-
const tasks = []
|
|
202
|
-
for (let i = 1; i <= 5; i++) {
|
|
203
|
-
const task = await api.resources.tasks.post({
|
|
204
|
-
title: `Task ${i}`,
|
|
205
|
-
category: category.id
|
|
206
|
-
})
|
|
207
|
-
tasks.push(task)
|
|
208
|
-
}
|
|
209
|
-
|
|
210
|
-
// Move task 5 before task 3
|
|
211
|
-
await api.resources.tasks.patch({
|
|
212
|
-
id: tasks[4].id,
|
|
213
|
-
beforeId: tasks[2].id
|
|
214
|
-
})
|
|
215
|
-
|
|
216
|
-
// Move task 1 to the end
|
|
217
|
-
await api.resources.tasks.patch({
|
|
218
|
-
id: tasks[0].id,
|
|
219
|
-
beforeId: null
|
|
220
|
-
})
|
|
221
|
-
|
|
222
|
-
// Move task 3 to the beginning
|
|
223
|
-
await api.resources.tasks.patch({
|
|
224
|
-
id: tasks[2].id,
|
|
225
|
-
beforeId: 'FIRST'
|
|
226
|
-
})
|
|
227
|
-
|
|
228
|
-
// Query final order
|
|
229
|
-
const { data: finalTasks } = await api.resources.tasks.query({
|
|
230
|
-
queryParams: {
|
|
231
|
-
filters: { category: category.id },
|
|
232
|
-
sort: ['position']
|
|
233
|
-
}
|
|
234
|
-
})
|
|
235
|
-
|
|
236
|
-
// Expected order: 3, 2, 5, 4, 1
|
|
237
|
-
assert.equal(finalTasks[0].title, 'Task 3')
|
|
238
|
-
assert.equal(finalTasks[1].title, 'Task 2')
|
|
239
|
-
assert.equal(finalTasks[2].title, 'Task 5')
|
|
240
|
-
assert.equal(finalTasks[3].title, 'Task 4')
|
|
241
|
-
assert.equal(finalTasks[4].title, 'Task 1')
|
|
242
|
-
})
|
|
243
|
-
|
|
244
|
-
it('should handle category changes with positioning', async () => {
|
|
245
|
-
const category1 = await api.resources.categories.post({ name: 'Category 1' })
|
|
246
|
-
const category2 = await api.resources.categories.post({ name: 'Category 2' })
|
|
247
|
-
|
|
248
|
-
// Create tasks in category 1
|
|
249
|
-
const task1 = await api.resources.tasks.post({
|
|
250
|
-
title: 'Task 1',
|
|
251
|
-
category: category1.id
|
|
252
|
-
})
|
|
253
|
-
|
|
254
|
-
const task2 = await api.resources.tasks.post({
|
|
255
|
-
title: 'Task 2',
|
|
256
|
-
category: category1.id
|
|
257
|
-
})
|
|
258
|
-
|
|
259
|
-
// Create tasks in category 2
|
|
260
|
-
const task3 = await api.resources.tasks.post({
|
|
261
|
-
title: 'Task 3',
|
|
262
|
-
category: category2.id
|
|
263
|
-
})
|
|
264
|
-
|
|
265
|
-
// Move task 1 to category 2, positioning it before task 3
|
|
266
|
-
await api.resources.tasks.patch({
|
|
267
|
-
id: task1.id,
|
|
268
|
-
category: category2.id,
|
|
269
|
-
beforeId: task3.id
|
|
270
|
-
})
|
|
271
|
-
|
|
272
|
-
// Verify category 1 only has task 2
|
|
273
|
-
const { data: cat1Tasks } = await api.resources.tasks.query({
|
|
274
|
-
queryParams: {
|
|
275
|
-
filters: { category: category1.id },
|
|
276
|
-
sort: ['position']
|
|
277
|
-
}
|
|
278
|
-
})
|
|
279
|
-
|
|
280
|
-
assert.equal(cat1Tasks.length, 1)
|
|
281
|
-
assert.equal(cat1Tasks[0].title, 'Task 2')
|
|
282
|
-
|
|
283
|
-
// Verify category 2 has tasks in correct order
|
|
284
|
-
const { data: cat2Tasks } = await api.resources.tasks.query({
|
|
285
|
-
queryParams: {
|
|
286
|
-
filters: { category: category2.id },
|
|
287
|
-
sort: ['position']
|
|
288
|
-
}
|
|
289
|
-
})
|
|
290
|
-
|
|
291
|
-
assert.equal(cat2Tasks.length, 2)
|
|
292
|
-
assert.equal(cat2Tasks[0].title, 'Task 1')
|
|
293
|
-
assert.equal(cat2Tasks[1].title, 'Task 3')
|
|
294
|
-
})
|
|
295
|
-
|
|
296
|
-
it('should maintain position when updating other fields', async () => {
|
|
297
|
-
const category = await api.resources.categories.post({ name: 'Test Category' })
|
|
298
|
-
|
|
299
|
-
// Create tasks
|
|
300
|
-
const tasks = []
|
|
301
|
-
for (let i = 1; i <= 3; i++) {
|
|
302
|
-
const task = await api.resources.tasks.post({
|
|
303
|
-
title: `Task ${i}`,
|
|
304
|
-
category: category.id
|
|
305
|
-
})
|
|
306
|
-
tasks.push(task)
|
|
307
|
-
}
|
|
308
|
-
|
|
309
|
-
// Update title without beforeId
|
|
310
|
-
const originalPosition = tasks[1].position
|
|
311
|
-
await api.resources.tasks.patch({
|
|
312
|
-
id: tasks[1].id,
|
|
313
|
-
title: 'Updated Task 2'
|
|
314
|
-
})
|
|
315
|
-
|
|
316
|
-
// Get updated task
|
|
317
|
-
const updatedTask = await api.resources.tasks.get({ id: tasks[1].id })
|
|
318
|
-
|
|
319
|
-
assert.equal(updatedTask.title, 'Updated Task 2')
|
|
320
|
-
assert.equal(updatedTask.position, originalPosition, 'Position should not change')
|
|
321
|
-
|
|
322
|
-
// Verify order is maintained
|
|
323
|
-
const { data: allTasks } = await api.resources.tasks.query({
|
|
324
|
-
queryParams: {
|
|
325
|
-
filters: { category: category.id },
|
|
326
|
-
sort: ['position']
|
|
327
|
-
}
|
|
328
|
-
})
|
|
329
|
-
|
|
330
|
-
assert.equal(allTasks[0].title, 'Task 1')
|
|
331
|
-
assert.equal(allTasks[1].title, 'Updated Task 2')
|
|
332
|
-
assert.equal(allTasks[2].title, 'Task 3')
|
|
333
|
-
})
|
|
334
|
-
})
|
|
335
|
-
|
|
336
|
-
describe('Bulk Operations', () => {
|
|
337
|
-
it('should handle bulk creation with positioning', async () => {
|
|
338
|
-
const category = await api.resources.categories.post({ name: 'Test Category' })
|
|
339
|
-
|
|
340
|
-
// Create initial task
|
|
341
|
-
const task1 = await api.resources.tasks.post({
|
|
342
|
-
title: 'Task 1',
|
|
343
|
-
category: category.id
|
|
344
|
-
})
|
|
345
|
-
|
|
346
|
-
// Create multiple tasks in parallel (simulating bulk creation)
|
|
347
|
-
const promises = []
|
|
348
|
-
for (let i = 2; i <= 10; i++) {
|
|
349
|
-
promises.push(api.resources.tasks.post({
|
|
350
|
-
title: `Task ${i}`,
|
|
351
|
-
category: category.id
|
|
352
|
-
}))
|
|
353
|
-
}
|
|
354
|
-
|
|
355
|
-
await Promise.all(promises)
|
|
356
|
-
|
|
357
|
-
// Query all tasks
|
|
358
|
-
const { data: tasks } = await api.resources.tasks.query({
|
|
359
|
-
queryParams: {
|
|
360
|
-
filters: { category: category.id },
|
|
361
|
-
sort: ['position']
|
|
362
|
-
}
|
|
363
|
-
})
|
|
364
|
-
|
|
365
|
-
assert.equal(tasks.length, 10)
|
|
366
|
-
|
|
367
|
-
// Verify all have unique positions
|
|
368
|
-
const positions = tasks.map(t => t.position)
|
|
369
|
-
const uniquePositions = [...new Set(positions)]
|
|
370
|
-
assert.equal(uniquePositions.length, 10, 'All positions should be unique')
|
|
371
|
-
|
|
372
|
-
// Verify they're in order
|
|
373
|
-
for (let i = 1; i < tasks.length; i++) {
|
|
374
|
-
assert(tasks[i - 1].position < tasks[i].position)
|
|
375
|
-
}
|
|
376
|
-
})
|
|
377
|
-
|
|
378
|
-
it('should handle bulk positioning before same target', async () => {
|
|
379
|
-
const category = await api.resources.categories.post({ name: 'Test Category' })
|
|
380
|
-
|
|
381
|
-
// Create initial tasks
|
|
382
|
-
const task1 = await api.resources.tasks.post({
|
|
383
|
-
title: 'Task 1',
|
|
384
|
-
category: category.id
|
|
385
|
-
})
|
|
386
|
-
|
|
387
|
-
const task2 = await api.resources.tasks.post({
|
|
388
|
-
title: 'Task 2',
|
|
389
|
-
category: category.id
|
|
390
|
-
})
|
|
391
|
-
|
|
392
|
-
// Create multiple tasks before task2 in parallel
|
|
393
|
-
const promises = []
|
|
394
|
-
for (let i = 1; i <= 5; i++) {
|
|
395
|
-
promises.push(api.resources.tasks.post({
|
|
396
|
-
title: `Inserted Task ${i}`,
|
|
397
|
-
category: category.id,
|
|
398
|
-
beforeId: task2.id
|
|
399
|
-
}))
|
|
400
|
-
}
|
|
401
|
-
|
|
402
|
-
await Promise.all(promises)
|
|
403
|
-
|
|
404
|
-
// Query all tasks
|
|
405
|
-
const { data: tasks } = await api.resources.tasks.query({
|
|
406
|
-
queryParams: {
|
|
407
|
-
filters: { category: category.id },
|
|
408
|
-
sort: ['position']
|
|
409
|
-
}
|
|
410
|
-
})
|
|
411
|
-
|
|
412
|
-
assert.equal(tasks.length, 7)
|
|
413
|
-
|
|
414
|
-
// Task 1 should still be first
|
|
415
|
-
assert.equal(tasks[0].title, 'Task 1')
|
|
416
|
-
|
|
417
|
-
// All inserted tasks should come before Task 2
|
|
418
|
-
const task2Index = tasks.findIndex(t => t.title === 'Task 2')
|
|
419
|
-
assert.equal(task2Index, 6, 'Task 2 should be last')
|
|
420
|
-
|
|
421
|
-
// All positions should be unique
|
|
422
|
-
const positions = tasks.map(t => t.position)
|
|
423
|
-
const uniquePositions = [...new Set(positions)]
|
|
424
|
-
assert.equal(uniquePositions.length, 7, 'All positions should be unique')
|
|
425
|
-
})
|
|
426
|
-
})
|
|
427
|
-
|
|
428
|
-
describe('Query and Filtering', () => {
|
|
429
|
-
it('should sort correctly with mixed position values', async () => {
|
|
430
|
-
const category = await api.resources.categories.post({ name: 'Test Category' })
|
|
431
|
-
|
|
432
|
-
// Create tasks in random order
|
|
433
|
-
const taskC = await api.resources.tasks.post({
|
|
434
|
-
title: 'Task C',
|
|
435
|
-
category: category.id
|
|
436
|
-
})
|
|
437
|
-
|
|
438
|
-
const taskA = await api.resources.tasks.post({
|
|
439
|
-
title: 'Task A',
|
|
440
|
-
category: category.id,
|
|
441
|
-
beforeId: taskC.id
|
|
442
|
-
})
|
|
443
|
-
|
|
444
|
-
const taskE = await api.resources.tasks.post({
|
|
445
|
-
title: 'Task E',
|
|
446
|
-
category: category.id
|
|
447
|
-
})
|
|
448
|
-
|
|
449
|
-
const taskB = await api.resources.tasks.post({
|
|
450
|
-
title: 'Task B',
|
|
451
|
-
category: category.id,
|
|
452
|
-
beforeId: taskC.id
|
|
453
|
-
})
|
|
454
|
-
|
|
455
|
-
const taskD = await api.resources.tasks.post({
|
|
456
|
-
title: 'Task D',
|
|
457
|
-
category: category.id,
|
|
458
|
-
beforeId: taskE.id
|
|
459
|
-
})
|
|
460
|
-
|
|
461
|
-
// Query with ascending sort
|
|
462
|
-
const { data: ascTasks } = await api.resources.tasks.query({
|
|
463
|
-
queryParams: {
|
|
464
|
-
filters: { category: category.id },
|
|
465
|
-
sort: ['position']
|
|
466
|
-
}
|
|
467
|
-
})
|
|
468
|
-
|
|
469
|
-
// Should be alphabetical: A, B, C, D, E
|
|
470
|
-
assert.equal(ascTasks.map(t => t.title).join(','), 'Task A,Task B,Task C,Task D,Task E')
|
|
471
|
-
|
|
472
|
-
// Query with descending sort
|
|
473
|
-
const { data: descTasks } = await api.resources.tasks.query({
|
|
474
|
-
queryParams: {
|
|
475
|
-
filters: { category: category.id },
|
|
476
|
-
sort: ['-position']
|
|
477
|
-
}
|
|
478
|
-
})
|
|
479
|
-
|
|
480
|
-
// Should be reverse: E, D, C, B, A
|
|
481
|
-
assert.equal(descTasks.map(t => t.title).join(','), 'Task E,Task D,Task C,Task B,Task A')
|
|
482
|
-
})
|
|
483
|
-
|
|
484
|
-
it('should handle pagination with position sorting', async () => {
|
|
485
|
-
const category = await api.resources.categories.post({ name: 'Test Category' })
|
|
486
|
-
|
|
487
|
-
// Create 20 tasks
|
|
488
|
-
for (let i = 1; i <= 20; i++) {
|
|
489
|
-
await api.resources.tasks.post({
|
|
490
|
-
title: `Task ${i.toString().padStart(2, '0')}`,
|
|
491
|
-
category: category.id
|
|
492
|
-
})
|
|
493
|
-
}
|
|
494
|
-
|
|
495
|
-
// Query first page
|
|
496
|
-
const { data: page1 } = await api.resources.tasks.query({
|
|
497
|
-
queryParams: {
|
|
498
|
-
filters: { category: category.id },
|
|
499
|
-
sort: ['position'],
|
|
500
|
-
page: { size: 5, number: 1 }
|
|
501
|
-
}
|
|
502
|
-
})
|
|
503
|
-
|
|
504
|
-
assert.equal(page1.length, 5)
|
|
505
|
-
assert.equal(page1[0].title, 'Task 01')
|
|
506
|
-
assert.equal(page1[4].title, 'Task 05')
|
|
507
|
-
|
|
508
|
-
// Query second page
|
|
509
|
-
const { data: page2 } = await api.resources.tasks.query({
|
|
510
|
-
queryParams: {
|
|
511
|
-
filters: { category: category.id },
|
|
512
|
-
sort: ['position'],
|
|
513
|
-
page: { size: 5, number: 2 }
|
|
514
|
-
}
|
|
515
|
-
})
|
|
516
|
-
|
|
517
|
-
assert.equal(page2.length, 5)
|
|
518
|
-
assert.equal(page2[0].title, 'Task 06')
|
|
519
|
-
assert.equal(page2[4].title, 'Task 10')
|
|
520
|
-
|
|
521
|
-
// Verify no overlap and correct ordering
|
|
522
|
-
assert(page1[4].position < page2[0].position)
|
|
523
|
-
})
|
|
524
|
-
|
|
525
|
-
it('should combine position sort with other sorts', async () => {
|
|
526
|
-
const category = await api.resources.categories.post({ name: 'Test Category' })
|
|
527
|
-
|
|
528
|
-
// Create tasks with same titles
|
|
529
|
-
const positions = []
|
|
530
|
-
for (let i = 0; i < 3; i++) {
|
|
531
|
-
const task = await api.resources.tasks.post({
|
|
532
|
-
title: 'Task A',
|
|
533
|
-
category: category.id
|
|
534
|
-
})
|
|
535
|
-
positions.push(task)
|
|
536
|
-
}
|
|
537
|
-
|
|
538
|
-
for (let i = 0; i < 3; i++) {
|
|
539
|
-
const task = await api.resources.tasks.post({
|
|
540
|
-
title: 'Task B',
|
|
541
|
-
category: category.id
|
|
542
|
-
})
|
|
543
|
-
positions.push(task)
|
|
544
|
-
}
|
|
545
|
-
|
|
546
|
-
// Sort by title then position
|
|
547
|
-
const { data: tasks } = await api.resources.tasks.query({
|
|
548
|
-
queryParams: {
|
|
549
|
-
filters: { category: category.id },
|
|
550
|
-
sort: ['title', 'position']
|
|
551
|
-
}
|
|
552
|
-
})
|
|
553
|
-
|
|
554
|
-
// First 3 should be Task A, last 3 should be Task B
|
|
555
|
-
assert.equal(tasks.slice(0, 3).every(t => t.title === 'Task A'), true)
|
|
556
|
-
assert.equal(tasks.slice(3, 6).every(t => t.title === 'Task B'), true)
|
|
557
|
-
|
|
558
|
-
// Within each group, should be sorted by position
|
|
559
|
-
for (let i = 1; i < 3; i++) {
|
|
560
|
-
assert(tasks[i - 1].position < tasks[i].position)
|
|
561
|
-
assert(tasks[i + 2].position < tasks[i + 3].position)
|
|
562
|
-
}
|
|
563
|
-
})
|
|
564
|
-
})
|
|
565
|
-
|
|
566
|
-
describe('Special Characters and Extreme Values', () => {
|
|
567
|
-
it('should handle special position values correctly', async () => {
|
|
568
|
-
const category = await api.resources.categories.post({ name: 'Test Category' })
|
|
569
|
-
|
|
570
|
-
// Create many tasks to test various position values
|
|
571
|
-
const tasks = []
|
|
572
|
-
for (let i = 0; i < 50; i++) {
|
|
573
|
-
const task = await api.resources.tasks.post({
|
|
574
|
-
title: `Task ${i}`,
|
|
575
|
-
category: category.id
|
|
576
|
-
})
|
|
577
|
-
tasks.push(task)
|
|
578
|
-
}
|
|
579
|
-
|
|
580
|
-
// Insert tasks between existing ones
|
|
581
|
-
for (let i = 0; i < 10; i++) {
|
|
582
|
-
await api.resources.tasks.post({
|
|
583
|
-
title: `Inserted ${i}`,
|
|
584
|
-
category: category.id,
|
|
585
|
-
beforeId: tasks[i * 5].id
|
|
586
|
-
})
|
|
587
|
-
}
|
|
588
|
-
|
|
589
|
-
// Query all and verify ordering
|
|
590
|
-
const { data: allTasks } = await api.resources.tasks.query({
|
|
591
|
-
queryParams: {
|
|
592
|
-
filters: { category: category.id },
|
|
593
|
-
sort: ['position'],
|
|
594
|
-
page: { size: 100 }
|
|
595
|
-
}
|
|
596
|
-
})
|
|
597
|
-
|
|
598
|
-
assert.equal(allTasks.length, 60)
|
|
599
|
-
|
|
600
|
-
// Verify all positions are valid strings
|
|
601
|
-
for (const task of allTasks) {
|
|
602
|
-
assert.equal(typeof task.position, 'string')
|
|
603
|
-
assert(task.position.length > 0)
|
|
604
|
-
// Fractional indexing uses alphanumeric characters
|
|
605
|
-
assert.match(task.position, /^[a-zA-Z0-9]+$/)
|
|
606
|
-
}
|
|
607
|
-
|
|
608
|
-
// Verify ordering
|
|
609
|
-
for (let i = 1; i < allTasks.length; i++) {
|
|
610
|
-
assert(allTasks[i - 1].position < allTasks[i].position)
|
|
611
|
-
}
|
|
612
|
-
})
|
|
613
|
-
|
|
614
|
-
it('should handle rapid position updates', async () => {
|
|
615
|
-
const category = await api.resources.categories.post({ name: 'Test Category' })
|
|
616
|
-
|
|
617
|
-
// Create initial tasks
|
|
618
|
-
const tasks = []
|
|
619
|
-
for (let i = 0; i < 10; i++) {
|
|
620
|
-
const task = await api.resources.tasks.post({
|
|
621
|
-
title: `Task ${i}`,
|
|
622
|
-
category: category.id
|
|
623
|
-
})
|
|
624
|
-
tasks.push(task)
|
|
625
|
-
}
|
|
626
|
-
|
|
627
|
-
// Rapidly move last task to different positions
|
|
628
|
-
const lastTask = tasks[9]
|
|
629
|
-
|
|
630
|
-
// Move to beginning
|
|
631
|
-
await api.resources.tasks.patch({
|
|
632
|
-
id: lastTask.id,
|
|
633
|
-
beforeId: tasks[0].id
|
|
634
|
-
})
|
|
635
|
-
|
|
636
|
-
// Move to middle
|
|
637
|
-
await api.resources.tasks.patch({
|
|
638
|
-
id: lastTask.id,
|
|
639
|
-
beforeId: tasks[5].id
|
|
640
|
-
})
|
|
641
|
-
|
|
642
|
-
// Move to near end
|
|
643
|
-
await api.resources.tasks.patch({
|
|
644
|
-
id: lastTask.id,
|
|
645
|
-
beforeId: tasks[8].id
|
|
646
|
-
})
|
|
647
|
-
|
|
648
|
-
// Move back to beginning
|
|
649
|
-
await api.resources.tasks.patch({
|
|
650
|
-
id: lastTask.id,
|
|
651
|
-
beforeId: 'FIRST'
|
|
652
|
-
})
|
|
653
|
-
|
|
654
|
-
// Verify final state
|
|
655
|
-
const { data: finalTasks } = await api.resources.tasks.query({
|
|
656
|
-
queryParams: {
|
|
657
|
-
filters: { category: category.id },
|
|
658
|
-
sort: ['position']
|
|
659
|
-
}
|
|
660
|
-
})
|
|
661
|
-
|
|
662
|
-
assert.equal(finalTasks[0].title, 'Task 9')
|
|
663
|
-
|
|
664
|
-
// Verify all positions are still unique and ordered
|
|
665
|
-
const positions = finalTasks.map(t => t.position)
|
|
666
|
-
const uniquePositions = [...new Set(positions)]
|
|
667
|
-
assert.equal(uniquePositions.length, 10)
|
|
668
|
-
|
|
669
|
-
for (let i = 1; i < finalTasks.length; i++) {
|
|
670
|
-
assert(finalTasks[i - 1].position < finalTasks[i].position)
|
|
671
|
-
}
|
|
672
|
-
})
|
|
673
|
-
})
|
|
674
|
-
|
|
675
|
-
describe('Null and Empty Filter Values', () => {
|
|
676
|
-
it('should handle tasks with null category separately', async () => {
|
|
677
|
-
// Create tasks with categories
|
|
678
|
-
const category = await api.resources.categories.post({ name: 'Test Category' })
|
|
679
|
-
|
|
680
|
-
const taskWithCat1 = await api.resources.tasks.post({
|
|
681
|
-
title: 'Task with Category 1',
|
|
682
|
-
category: category.id
|
|
683
|
-
})
|
|
684
|
-
|
|
685
|
-
const taskWithCat2 = await api.resources.tasks.post({
|
|
686
|
-
title: 'Task with Category 2',
|
|
687
|
-
category: category.id
|
|
688
|
-
})
|
|
689
|
-
|
|
690
|
-
// Create tasks without category - don't send category_id at all for null
|
|
691
|
-
const taskNoCat1 = await api.resources.tasks.post({
|
|
692
|
-
title: 'Task without Category 1'
|
|
693
|
-
})
|
|
694
|
-
|
|
695
|
-
const taskNoCat2 = await api.resources.tasks.post({
|
|
696
|
-
title: 'Task without Category 2'
|
|
697
|
-
})
|
|
698
|
-
|
|
699
|
-
const taskNoCat3 = await api.resources.tasks.post({
|
|
700
|
-
title: 'Task without Category 3',
|
|
701
|
-
beforeId: taskNoCat1.id
|
|
702
|
-
})
|
|
703
|
-
|
|
704
|
-
// Query tasks with category
|
|
705
|
-
const { data: tasksWithCat } = await api.resources.tasks.query({
|
|
706
|
-
queryParams: {
|
|
707
|
-
filters: { category: category.id },
|
|
708
|
-
sort: ['position']
|
|
709
|
-
}
|
|
710
|
-
})
|
|
711
|
-
|
|
712
|
-
assert.equal(tasksWithCat.length, 2)
|
|
713
|
-
assert(tasksWithCat[0].position < tasksWithCat[1].position)
|
|
714
|
-
|
|
715
|
-
// Query tasks without category
|
|
716
|
-
const { data: tasksNoCat } = await api.resources.tasks.query({
|
|
717
|
-
queryParams: {
|
|
718
|
-
filters: { category: null },
|
|
719
|
-
sort: ['position']
|
|
720
|
-
}
|
|
721
|
-
})
|
|
722
|
-
|
|
723
|
-
// Debug: Check what we actually have
|
|
724
|
-
const { data: allTasks } = await api.resources.tasks.query({
|
|
725
|
-
queryParams: {
|
|
726
|
-
sort: ['position']
|
|
727
|
-
}
|
|
728
|
-
})
|
|
729
|
-
console.log('All tasks:', allTasks.map(t => ({ title: t.title, category: t.category_id })))
|
|
730
|
-
console.log('Tasks with null category:', tasksNoCat.length)
|
|
731
|
-
|
|
732
|
-
// Try querying without filter
|
|
733
|
-
const { data: tasksNoFilter } = await api.resources.tasks.query({
|
|
734
|
-
queryParams: {
|
|
735
|
-
sort: ['position']
|
|
736
|
-
}
|
|
737
|
-
})
|
|
738
|
-
const actualNullCategoryTasks = tasksNoFilter.filter(t => t.category_id === null || t.category_id === undefined)
|
|
739
|
-
console.log('Actual tasks without category:', actualNullCategoryTasks.length)
|
|
740
|
-
|
|
741
|
-
assert.equal(tasksNoCat.length, 3)
|
|
742
|
-
assert.equal(tasksNoCat[0].title, 'Task without Category 3') // Positioned before 1
|
|
743
|
-
assert.equal(tasksNoCat[1].title, 'Task without Category 1')
|
|
744
|
-
assert.equal(tasksNoCat[2].title, 'Task without Category 2')
|
|
745
|
-
})
|
|
746
|
-
})
|
|
747
|
-
|
|
748
|
-
describe('Position Field Validation', () => {
|
|
749
|
-
it('should ignore manually set position values', async () => {
|
|
750
|
-
const category = await api.resources.categories.post({ name: 'Test Category' })
|
|
751
|
-
|
|
752
|
-
// Try to set position manually on create
|
|
753
|
-
const task1 = await api.resources.tasks.post({
|
|
754
|
-
title: 'Task 1',
|
|
755
|
-
category: category.id,
|
|
756
|
-
position: 'ZZZZ' // Should be ignored
|
|
757
|
-
})
|
|
758
|
-
|
|
759
|
-
assert.notEqual(task1.position, 'ZZZZ')
|
|
760
|
-
assert.equal(task1.position, 'a0') // First item gets 'a0'
|
|
761
|
-
|
|
762
|
-
// Try to set position manually on update
|
|
763
|
-
const task2 = await api.resources.tasks.post({
|
|
764
|
-
title: 'Task 2',
|
|
765
|
-
category: category.id
|
|
766
|
-
})
|
|
767
|
-
|
|
768
|
-
const originalPosition = task2.position
|
|
769
|
-
|
|
770
|
-
await api.resources.tasks.patch({
|
|
771
|
-
id: task2.id,
|
|
772
|
-
position: 'AAAA' // Should be ignored
|
|
773
|
-
})
|
|
774
|
-
|
|
775
|
-
const updated = await api.resources.tasks.get({ id: task2.id })
|
|
776
|
-
assert.equal(updated.position, originalPosition) // Position unchanged
|
|
777
|
-
})
|
|
778
|
-
|
|
779
|
-
it('should handle both position and beforeId in update', async () => {
|
|
780
|
-
const category = await api.resources.categories.post({ name: 'Test Category' })
|
|
781
|
-
|
|
782
|
-
const task1 = await api.resources.tasks.post({
|
|
783
|
-
title: 'Task 1',
|
|
784
|
-
category: category.id
|
|
785
|
-
})
|
|
786
|
-
|
|
787
|
-
const task2 = await api.resources.tasks.post({
|
|
788
|
-
title: 'Task 2',
|
|
789
|
-
category: category.id
|
|
790
|
-
})
|
|
791
|
-
|
|
792
|
-
const task3 = await api.resources.tasks.post({
|
|
793
|
-
title: 'Task 3',
|
|
794
|
-
category: category.id
|
|
795
|
-
})
|
|
796
|
-
|
|
797
|
-
// Update with both position (ignored) and beforeId (used)
|
|
798
|
-
await api.resources.tasks.patch({
|
|
799
|
-
id: task3.id,
|
|
800
|
-
position: 'XXXX', // Should be ignored
|
|
801
|
-
beforeId: task1.id // Should be used
|
|
802
|
-
})
|
|
803
|
-
|
|
804
|
-
const { data: tasks } = await api.resources.tasks.query({
|
|
805
|
-
queryParams: {
|
|
806
|
-
filters: { category: category.id },
|
|
807
|
-
sort: ['position']
|
|
808
|
-
}
|
|
809
|
-
})
|
|
810
|
-
|
|
811
|
-
// Task 3 should now be before Task 1
|
|
812
|
-
assert.equal(tasks[0].title, 'Task 3')
|
|
813
|
-
assert.equal(tasks[1].title, 'Task 1')
|
|
814
|
-
assert.equal(tasks[2].title, 'Task 2')
|
|
815
|
-
assert.notEqual(tasks[0].position, 'XXXX')
|
|
816
|
-
})
|
|
817
|
-
})
|
|
818
|
-
|
|
819
|
-
describe('Performance and Stress Tests', () => {
|
|
820
|
-
it('should handle large number of tasks efficiently', async () => {
|
|
821
|
-
const category = await api.resources.categories.post({ name: 'Test Category' })
|
|
822
|
-
|
|
823
|
-
// Create 100 tasks
|
|
824
|
-
for (let i = 0; i < 100; i++) {
|
|
825
|
-
await api.resources.tasks.post({
|
|
826
|
-
title: `Task ${i.toString().padStart(3, '0')}`,
|
|
827
|
-
category: category.id
|
|
828
|
-
})
|
|
829
|
-
}
|
|
830
|
-
|
|
831
|
-
// Query with position sort
|
|
832
|
-
const { data: tasks } = await api.resources.tasks.query({
|
|
833
|
-
queryParams: {
|
|
834
|
-
filters: { category: category.id },
|
|
835
|
-
sort: ['position'],
|
|
836
|
-
page: { size: 100 }
|
|
837
|
-
}
|
|
838
|
-
})
|
|
839
|
-
|
|
840
|
-
assert.equal(tasks.length, 100)
|
|
841
|
-
|
|
842
|
-
// Verify all are in correct order
|
|
843
|
-
for (let i = 0; i < tasks.length; i++) {
|
|
844
|
-
assert.equal(tasks[i].title, `Task ${i.toString().padStart(3, '0')}`)
|
|
845
|
-
}
|
|
846
|
-
|
|
847
|
-
// Insert task in the middle
|
|
848
|
-
const middleTask = await api.resources.tasks.post({
|
|
849
|
-
title: 'Middle Task',
|
|
850
|
-
category: category.id,
|
|
851
|
-
beforeId: tasks[50].id
|
|
852
|
-
})
|
|
853
|
-
|
|
854
|
-
// Verify it's positioned correctly
|
|
855
|
-
const { data: updatedTasks } = await api.resources.tasks.query({
|
|
856
|
-
queryParams: {
|
|
857
|
-
filters: { category: category.id },
|
|
858
|
-
sort: ['position'],
|
|
859
|
-
page: { size: 102 }
|
|
860
|
-
}
|
|
861
|
-
})
|
|
862
|
-
|
|
863
|
-
const middleIndex = updatedTasks.findIndex(t => t.title === 'Middle Task')
|
|
864
|
-
assert.equal(middleIndex, 50)
|
|
865
|
-
assert(updatedTasks[49].position < middleTask.position)
|
|
866
|
-
assert(middleTask.position < updatedTasks[51].position)
|
|
867
|
-
})
|
|
868
|
-
|
|
869
|
-
it('should maintain consistency under concurrent updates', async () => {
|
|
870
|
-
const category = await api.resources.categories.post({ name: 'Test Category' })
|
|
871
|
-
|
|
872
|
-
// Create initial tasks
|
|
873
|
-
const tasks = []
|
|
874
|
-
for (let i = 0; i < 20; i++) {
|
|
875
|
-
const task = await api.resources.tasks.post({
|
|
876
|
-
title: `Task ${i}`,
|
|
877
|
-
category: category.id
|
|
878
|
-
})
|
|
879
|
-
tasks.push(task)
|
|
880
|
-
}
|
|
881
|
-
|
|
882
|
-
// Perform concurrent position updates
|
|
883
|
-
const updates = [
|
|
884
|
-
api.resources.tasks.patch({ id: tasks[5].id, beforeId: tasks[0].id }),
|
|
885
|
-
api.resources.tasks.patch({ id: tasks[10].id, beforeId: tasks[0].id }),
|
|
886
|
-
api.resources.tasks.patch({ id: tasks[15].id, beforeId: tasks[0].id }),
|
|
887
|
-
api.resources.tasks.patch({ id: tasks[19].id, beforeId: 'FIRST' }),
|
|
888
|
-
api.resources.tasks.patch({ id: tasks[1].id, beforeId: null }) // Move to end
|
|
889
|
-
]
|
|
890
|
-
|
|
891
|
-
await Promise.all(updates)
|
|
892
|
-
|
|
893
|
-
// Verify final state
|
|
894
|
-
const { data: finalTasks } = await api.resources.tasks.query({
|
|
895
|
-
queryParams: {
|
|
896
|
-
filters: { category: category.id },
|
|
897
|
-
sort: ['position']
|
|
898
|
-
}
|
|
899
|
-
})
|
|
900
|
-
|
|
901
|
-
// All tasks should still exist
|
|
902
|
-
assert.equal(finalTasks.length, 20)
|
|
903
|
-
|
|
904
|
-
// All positions should be unique
|
|
905
|
-
const positions = finalTasks.map(t => t.position)
|
|
906
|
-
const uniquePositions = [...new Set(positions)]
|
|
907
|
-
assert.equal(uniquePositions.length, 20)
|
|
908
|
-
|
|
909
|
-
// Verify ordering is consistent
|
|
910
|
-
for (let i = 1; i < finalTasks.length; i++) {
|
|
911
|
-
assert(finalTasks[i - 1].position < finalTasks[i].position)
|
|
912
|
-
}
|
|
913
|
-
})
|
|
914
|
-
})
|
|
915
|
-
})
|