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,692 +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
|
-
import { rebalancePositions, isValidPosition, getUnpositionedItems, assignInitialPositions } from '../plugins/core/lib/fractional-positioning.js'
|
|
8
|
-
|
|
9
|
-
// Create Knex instance for tests
|
|
10
|
-
const knex = knexLib({
|
|
11
|
-
client: 'better-sqlite3',
|
|
12
|
-
connection: {
|
|
13
|
-
filename: ':memory:'
|
|
14
|
-
},
|
|
15
|
-
useNullAsDefault: true
|
|
16
|
-
})
|
|
17
|
-
|
|
18
|
-
describe('Advanced Positioning Plugin Tests', { timeout: 30000 }, () => {
|
|
19
|
-
let api
|
|
20
|
-
|
|
21
|
-
before(async () => {
|
|
22
|
-
// Initialize API once
|
|
23
|
-
api = await createPositioningApi(knex, {
|
|
24
|
-
apiName: 'positioning-advanced-test',
|
|
25
|
-
tablePrefix: 'pos_adv'
|
|
26
|
-
})
|
|
27
|
-
|
|
28
|
-
// Install positioning plugin with default configuration
|
|
29
|
-
await api.use(PositioningPlugin, {
|
|
30
|
-
field: 'position',
|
|
31
|
-
filters: ['category'],
|
|
32
|
-
defaultPosition: 'last',
|
|
33
|
-
autoIndex: true
|
|
34
|
-
})
|
|
35
|
-
})
|
|
36
|
-
|
|
37
|
-
after(async () => {
|
|
38
|
-
await knex.destroy()
|
|
39
|
-
})
|
|
40
|
-
|
|
41
|
-
beforeEach(async () => {
|
|
42
|
-
// Only clean tables for the main test API
|
|
43
|
-
await cleanTables(knex, [
|
|
44
|
-
'pos_adv_categories',
|
|
45
|
-
'pos_adv_tasks',
|
|
46
|
-
'pos_adv_projects',
|
|
47
|
-
'pos_adv_items'
|
|
48
|
-
])
|
|
49
|
-
})
|
|
50
|
-
|
|
51
|
-
describe('Position Rebalancing', () => {
|
|
52
|
-
it('should detect when positions need rebalancing', () => {
|
|
53
|
-
// Create items with increasingly long position strings
|
|
54
|
-
const items = [
|
|
55
|
-
{ id: 1, name: 'Item 1', position: 'a0' },
|
|
56
|
-
{ id: 2, name: 'Item 2', position: 'a0V' },
|
|
57
|
-
{ id: 3, name: 'Item 3', position: 'a0VV' },
|
|
58
|
-
{ id: 4, name: 'Item 4', position: 'a0VVV' },
|
|
59
|
-
{ id: 5, name: 'Item 5', position: 'a0VVVV' },
|
|
60
|
-
{ id: 6, name: 'Item 6', position: 'a0VVVVV' },
|
|
61
|
-
{ id: 7, name: 'Item 7', position: 'a0VVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVV' } // 51 chars
|
|
62
|
-
]
|
|
63
|
-
|
|
64
|
-
// Test with default threshold (50)
|
|
65
|
-
const rebalanced = rebalancePositions(items, 'position', 50)
|
|
66
|
-
|
|
67
|
-
// Should have rebalanced because item 7 exceeds threshold
|
|
68
|
-
assert.notEqual(rebalanced[6].position, items[6].position)
|
|
69
|
-
|
|
70
|
-
// All positions should be shorter after rebalancing
|
|
71
|
-
for (const item of rebalanced) {
|
|
72
|
-
assert(item.position.length <= 50)
|
|
73
|
-
}
|
|
74
|
-
|
|
75
|
-
// Order should be preserved
|
|
76
|
-
for (let i = 1; i < rebalanced.length; i++) {
|
|
77
|
-
assert(rebalanced[i - 1].position < rebalanced[i].position)
|
|
78
|
-
}
|
|
79
|
-
})
|
|
80
|
-
|
|
81
|
-
it('should not rebalance when not needed', async () => {
|
|
82
|
-
const items = [
|
|
83
|
-
{ id: 1, name: 'Item 1', position: 'a0' },
|
|
84
|
-
{ id: 2, name: 'Item 2', position: 'a1' },
|
|
85
|
-
{ id: 3, name: 'Item 3', position: 'a2' },
|
|
86
|
-
{ id: 4, name: 'Item 4', position: 'a3' }
|
|
87
|
-
]
|
|
88
|
-
|
|
89
|
-
const result = rebalancePositions(items, 'position', 50)
|
|
90
|
-
|
|
91
|
-
// Should return same items (no rebalancing needed)
|
|
92
|
-
assert.deepEqual(result, items)
|
|
93
|
-
})
|
|
94
|
-
|
|
95
|
-
it('should handle position validation', () => {
|
|
96
|
-
// Valid positions
|
|
97
|
-
assert(isValidPosition('a0'))
|
|
98
|
-
assert(isValidPosition('Zz'))
|
|
99
|
-
assert(isValidPosition('a0V'))
|
|
100
|
-
assert(isValidPosition('ABC123xyz'))
|
|
101
|
-
|
|
102
|
-
// Invalid positions
|
|
103
|
-
assert(!isValidPosition(''))
|
|
104
|
-
assert(!isValidPosition(null))
|
|
105
|
-
assert(!isValidPosition(undefined))
|
|
106
|
-
assert(!isValidPosition('a-0')) // Contains hyphen
|
|
107
|
-
assert(!isValidPosition('a_0')) // Contains underscore
|
|
108
|
-
assert(!isValidPosition('a 0')) // Contains space
|
|
109
|
-
assert(!isValidPosition('!@#')) // Special characters
|
|
110
|
-
assert(!isValidPosition(123)) // Not a string
|
|
111
|
-
})
|
|
112
|
-
|
|
113
|
-
it('should identify unpositioned items', () => {
|
|
114
|
-
const items = [
|
|
115
|
-
{ id: 1, name: 'Item 1', position: 'a0' },
|
|
116
|
-
{ id: 2, name: 'Item 2', position: null },
|
|
117
|
-
{ id: 3, name: 'Item 3', position: 'a1' },
|
|
118
|
-
{ id: 4, name: 'Item 4', position: '' },
|
|
119
|
-
{ id: 5, name: 'Item 5', position: undefined },
|
|
120
|
-
{ id: 6, name: 'Item 6', position: 'invalid-position' }
|
|
121
|
-
]
|
|
122
|
-
|
|
123
|
-
const unpositioned = getUnpositionedItems(items, 'position')
|
|
124
|
-
|
|
125
|
-
assert.equal(unpositioned.length, 4)
|
|
126
|
-
assert.equal(unpositioned[0].id, 2)
|
|
127
|
-
assert.equal(unpositioned[1].id, 4)
|
|
128
|
-
assert.equal(unpositioned[2].id, 5)
|
|
129
|
-
assert.equal(unpositioned[3].id, 6)
|
|
130
|
-
})
|
|
131
|
-
|
|
132
|
-
it('should assign initial positions to unpositioned items', () => {
|
|
133
|
-
const items = [
|
|
134
|
-
{ id: 1, name: 'Item 1', position: 'a0' },
|
|
135
|
-
{ id: 2, name: 'Item 2', position: null },
|
|
136
|
-
{ id: 3, name: 'Item 3', position: 'a2' },
|
|
137
|
-
{ id: 4, name: 'Item 4', position: '' },
|
|
138
|
-
{ id: 5, name: 'Item 5', position: 'a3' }
|
|
139
|
-
]
|
|
140
|
-
|
|
141
|
-
const result = assignInitialPositions(items, 'position', 'id')
|
|
142
|
-
|
|
143
|
-
assert.equal(result.length, 5)
|
|
144
|
-
|
|
145
|
-
// Previously positioned items should keep their positions
|
|
146
|
-
const item1 = result.find(i => i.id === 1)
|
|
147
|
-
assert.equal(item1.position, 'a0')
|
|
148
|
-
|
|
149
|
-
const item3 = result.find(i => i.id === 3)
|
|
150
|
-
assert.equal(item3.position, 'a2')
|
|
151
|
-
|
|
152
|
-
const item5 = result.find(i => i.id === 5)
|
|
153
|
-
assert.equal(item5.position, 'a3')
|
|
154
|
-
|
|
155
|
-
// Unpositioned items should have valid positions
|
|
156
|
-
const item2 = result.find(i => i.id === 2)
|
|
157
|
-
assert(isValidPosition(item2.position))
|
|
158
|
-
assert(item2.position > 'a3') // Should be after last positioned item
|
|
159
|
-
|
|
160
|
-
const item4 = result.find(i => i.id === 4)
|
|
161
|
-
assert(isValidPosition(item4.position))
|
|
162
|
-
assert(item4.position > item2.position) // Should be after item2
|
|
163
|
-
})
|
|
164
|
-
})
|
|
165
|
-
|
|
166
|
-
describe('Default Position Configuration', () => {
|
|
167
|
-
it('should respect defaultPosition: first configuration', async () => {
|
|
168
|
-
const api = await createPositioningApi(knex, {
|
|
169
|
-
apiName: 'positioning-first-default',
|
|
170
|
-
tablePrefix: 'pos_first_def'
|
|
171
|
-
})
|
|
172
|
-
|
|
173
|
-
await api.use(PositioningPlugin, {
|
|
174
|
-
field: 'position',
|
|
175
|
-
filters: ['category'],
|
|
176
|
-
defaultPosition: 'first' // New items go to beginning
|
|
177
|
-
})
|
|
178
|
-
|
|
179
|
-
const category = await api.resources.categories.post({ name: 'Test Category' })
|
|
180
|
-
|
|
181
|
-
// Create tasks - they should be added at the beginning
|
|
182
|
-
const task1 = await api.resources.tasks.post({
|
|
183
|
-
title: 'Task 1',
|
|
184
|
-
category: category.id
|
|
185
|
-
})
|
|
186
|
-
|
|
187
|
-
const task2 = await api.resources.tasks.post({
|
|
188
|
-
title: 'Task 2',
|
|
189
|
-
category: category.id
|
|
190
|
-
})
|
|
191
|
-
|
|
192
|
-
const task3 = await api.resources.tasks.post({
|
|
193
|
-
title: 'Task 3',
|
|
194
|
-
category: category.id
|
|
195
|
-
})
|
|
196
|
-
|
|
197
|
-
// Query tasks
|
|
198
|
-
const { data: tasks } = await api.resources.tasks.query({
|
|
199
|
-
queryParams: {
|
|
200
|
-
filters: { category: category.id },
|
|
201
|
-
sort: ['position']
|
|
202
|
-
}
|
|
203
|
-
})
|
|
204
|
-
|
|
205
|
-
// With defaultPosition: 'first', the order should be reversed
|
|
206
|
-
// Task 3 should be first (added last, positioned first)
|
|
207
|
-
assert.equal(tasks.length, 3)
|
|
208
|
-
assert.equal(tasks[0].title, 'Task 3')
|
|
209
|
-
assert.equal(tasks[1].title, 'Task 2')
|
|
210
|
-
assert.equal(tasks[2].title, 'Task 1')
|
|
211
|
-
|
|
212
|
-
// Verify positions are ordered correctly
|
|
213
|
-
assert(tasks[0].position < tasks[1].position)
|
|
214
|
-
assert(tasks[1].position < tasks[2].position)
|
|
215
|
-
})
|
|
216
|
-
|
|
217
|
-
it('should handle beforeId with defaultPosition: first', async () => {
|
|
218
|
-
const api = await createPositioningApi(knex, {
|
|
219
|
-
apiName: 'positioning-first-before',
|
|
220
|
-
tablePrefix: 'pos_first_bef'
|
|
221
|
-
})
|
|
222
|
-
|
|
223
|
-
await api.use(PositioningPlugin, {
|
|
224
|
-
field: 'position',
|
|
225
|
-
filters: ['category'],
|
|
226
|
-
defaultPosition: 'first'
|
|
227
|
-
})
|
|
228
|
-
|
|
229
|
-
const category = await api.resources.categories.post({ name: 'Test Category' })
|
|
230
|
-
|
|
231
|
-
const task1 = await api.resources.tasks.post({
|
|
232
|
-
title: 'Task 1',
|
|
233
|
-
category: category.id
|
|
234
|
-
})
|
|
235
|
-
|
|
236
|
-
const task2 = await api.resources.tasks.post({
|
|
237
|
-
title: 'Task 2',
|
|
238
|
-
category: category.id
|
|
239
|
-
})
|
|
240
|
-
|
|
241
|
-
// Create task with explicit beforeId (should override defaultPosition)
|
|
242
|
-
const task3 = await api.resources.tasks.post({
|
|
243
|
-
title: 'Task 3',
|
|
244
|
-
category: category.id,
|
|
245
|
-
beforeId: task1.id
|
|
246
|
-
})
|
|
247
|
-
|
|
248
|
-
const { data: tasks } = await api.resources.tasks.query({
|
|
249
|
-
queryParams: {
|
|
250
|
-
filters: { category: category.id },
|
|
251
|
-
sort: ['position']
|
|
252
|
-
}
|
|
253
|
-
})
|
|
254
|
-
|
|
255
|
-
// Order should be: Task 3, Task 2, Task 1
|
|
256
|
-
// (Task 3 explicitly before Task 1, Task 2 at beginning by default)
|
|
257
|
-
assert.equal(tasks[0].title, 'Task 2')
|
|
258
|
-
assert.equal(tasks[1].title, 'Task 3')
|
|
259
|
-
assert.equal(tasks[2].title, 'Task 1')
|
|
260
|
-
})
|
|
261
|
-
})
|
|
262
|
-
|
|
263
|
-
describe('Custom Field Names', () => {
|
|
264
|
-
it('should work with custom position field name', async () => {
|
|
265
|
-
const api = await createPositioningApi(knex, {
|
|
266
|
-
apiName: 'positioning-custom-field',
|
|
267
|
-
tablePrefix: 'pos_custom_fld'
|
|
268
|
-
})
|
|
269
|
-
|
|
270
|
-
await api.use(PositioningPlugin, {
|
|
271
|
-
field: 'sort_order', // Custom field name
|
|
272
|
-
filters: ['project'],
|
|
273
|
-
defaultPosition: 'last'
|
|
274
|
-
})
|
|
275
|
-
|
|
276
|
-
const project = await api.resources.projects.post({ name: 'Test Project' })
|
|
277
|
-
|
|
278
|
-
const item1 = await api.resources.items.post({
|
|
279
|
-
name: 'Item 1',
|
|
280
|
-
project: project.id
|
|
281
|
-
})
|
|
282
|
-
|
|
283
|
-
const item2 = await api.resources.items.post({
|
|
284
|
-
name: 'Item 2',
|
|
285
|
-
project: project.id
|
|
286
|
-
})
|
|
287
|
-
|
|
288
|
-
// Should have sort_order field, not position
|
|
289
|
-
assert(item1.sort_order)
|
|
290
|
-
assert(!item1.position)
|
|
291
|
-
assert(item2.sort_order)
|
|
292
|
-
assert(!item2.position)
|
|
293
|
-
|
|
294
|
-
// Should be properly ordered
|
|
295
|
-
assert(item1.sort_order < item2.sort_order)
|
|
296
|
-
|
|
297
|
-
// Query should work with custom field
|
|
298
|
-
const { data: items } = await api.resources.items.query({
|
|
299
|
-
queryParams: {
|
|
300
|
-
filters: { project: project.id },
|
|
301
|
-
sort: ['sort_order']
|
|
302
|
-
}
|
|
303
|
-
})
|
|
304
|
-
|
|
305
|
-
assert.equal(items.length, 2)
|
|
306
|
-
assert.equal(items[0].name, 'Item 1')
|
|
307
|
-
assert.equal(items[1].name, 'Item 2')
|
|
308
|
-
})
|
|
309
|
-
|
|
310
|
-
it('should handle custom beforeId field name', async () => {
|
|
311
|
-
const api = await createPositioningApi(knex, {
|
|
312
|
-
apiName: 'positioning-custom-beforeid',
|
|
313
|
-
tablePrefix: 'pos_custom_bid'
|
|
314
|
-
})
|
|
315
|
-
|
|
316
|
-
await api.use(PositioningPlugin, {
|
|
317
|
-
field: 'position',
|
|
318
|
-
filters: ['project'],
|
|
319
|
-
beforeIdField: 'insertBefore', // Custom virtual field name
|
|
320
|
-
defaultPosition: 'last'
|
|
321
|
-
})
|
|
322
|
-
|
|
323
|
-
const project = await api.resources.projects.post({ name: 'Test Project' })
|
|
324
|
-
|
|
325
|
-
const item1 = await api.resources.items.post({
|
|
326
|
-
name: 'Item 1',
|
|
327
|
-
project: project.id
|
|
328
|
-
})
|
|
329
|
-
|
|
330
|
-
const item2 = await api.resources.items.post({
|
|
331
|
-
name: 'Item 2',
|
|
332
|
-
project: project.id
|
|
333
|
-
})
|
|
334
|
-
|
|
335
|
-
// Use custom beforeId field name
|
|
336
|
-
const item3 = await api.resources.items.post({
|
|
337
|
-
name: 'Item 3',
|
|
338
|
-
project: project.id,
|
|
339
|
-
insertBefore: item2.id
|
|
340
|
-
})
|
|
341
|
-
|
|
342
|
-
const { data: items } = await api.resources.items.query({
|
|
343
|
-
queryParams: {
|
|
344
|
-
filters: { project: project.id },
|
|
345
|
-
sort: ['position']
|
|
346
|
-
}
|
|
347
|
-
})
|
|
348
|
-
|
|
349
|
-
assert.equal(items.length, 3)
|
|
350
|
-
assert.equal(items[0].name, 'Item 1')
|
|
351
|
-
assert.equal(items[1].name, 'Item 3')
|
|
352
|
-
assert.equal(items[2].name, 'Item 2')
|
|
353
|
-
})
|
|
354
|
-
})
|
|
355
|
-
|
|
356
|
-
describe('Error Recovery and Consistency', () => {
|
|
357
|
-
it('should handle concurrent inserts at same position gracefully', async () => {
|
|
358
|
-
// Use the main api instance
|
|
359
|
-
|
|
360
|
-
const category = await api.resources.categories.post({ name: 'Test Category' })
|
|
361
|
-
|
|
362
|
-
const task1 = await api.resources.tasks.post({
|
|
363
|
-
title: 'Task 1',
|
|
364
|
-
category: category.id
|
|
365
|
-
})
|
|
366
|
-
|
|
367
|
-
const task2 = await api.resources.tasks.post({
|
|
368
|
-
title: 'Task 2',
|
|
369
|
-
category: category.id
|
|
370
|
-
})
|
|
371
|
-
|
|
372
|
-
// Simulate concurrent inserts before task2
|
|
373
|
-
const promises = []
|
|
374
|
-
for (let i = 0; i < 5; i++) {
|
|
375
|
-
promises.push(
|
|
376
|
-
api.resources.tasks.post({
|
|
377
|
-
title: `Concurrent Task ${i}`,
|
|
378
|
-
category: category.id,
|
|
379
|
-
beforeId: task2.id
|
|
380
|
-
})
|
|
381
|
-
)
|
|
382
|
-
}
|
|
383
|
-
|
|
384
|
-
const results = await Promise.all(promises)
|
|
385
|
-
|
|
386
|
-
// All should succeed
|
|
387
|
-
assert.equal(results.length, 5)
|
|
388
|
-
|
|
389
|
-
// Query all tasks
|
|
390
|
-
const { data: tasks } = await api.resources.tasks.query({
|
|
391
|
-
queryParams: {
|
|
392
|
-
filters: { category: category.id },
|
|
393
|
-
sort: ['position']
|
|
394
|
-
}
|
|
395
|
-
})
|
|
396
|
-
|
|
397
|
-
assert.equal(tasks.length, 7)
|
|
398
|
-
|
|
399
|
-
// Task 1 should still be first
|
|
400
|
-
assert.equal(tasks[0].title, 'Task 1')
|
|
401
|
-
|
|
402
|
-
// Task 2 should be last
|
|
403
|
-
assert.equal(tasks[6].title, 'Task 2')
|
|
404
|
-
|
|
405
|
-
// All positions should be unique
|
|
406
|
-
const positions = tasks.map(t => t.position)
|
|
407
|
-
const uniquePositions = [...new Set(positions)]
|
|
408
|
-
assert.equal(uniquePositions.length, 7)
|
|
409
|
-
|
|
410
|
-
// All should be properly ordered
|
|
411
|
-
for (let i = 1; i < tasks.length; i++) {
|
|
412
|
-
assert(tasks[i - 1].position < tasks[i].position)
|
|
413
|
-
}
|
|
414
|
-
})
|
|
415
|
-
|
|
416
|
-
it('should maintain consistency when moving items rapidly', async () => {
|
|
417
|
-
// Use the main api instance
|
|
418
|
-
|
|
419
|
-
const category = await api.resources.categories.post({ name: 'Test Category' })
|
|
420
|
-
|
|
421
|
-
// Create initial items
|
|
422
|
-
const items = []
|
|
423
|
-
for (let i = 0; i < 10; i++) {
|
|
424
|
-
const item = await api.resources.tasks.post({
|
|
425
|
-
title: `Task ${i}`,
|
|
426
|
-
category: category.id
|
|
427
|
-
})
|
|
428
|
-
items.push(item)
|
|
429
|
-
}
|
|
430
|
-
|
|
431
|
-
// Rapidly move items around
|
|
432
|
-
const updates = []
|
|
433
|
-
|
|
434
|
-
// Move multiple items to beginning
|
|
435
|
-
updates.push(
|
|
436
|
-
api.resources.tasks.patch({ id: items[5].id, beforeId: 'FIRST' }),
|
|
437
|
-
api.resources.tasks.patch({ id: items[7].id, beforeId: 'FIRST' }),
|
|
438
|
-
api.resources.tasks.patch({ id: items[9].id, beforeId: 'FIRST' })
|
|
439
|
-
)
|
|
440
|
-
|
|
441
|
-
// Move multiple items to end
|
|
442
|
-
updates.push(
|
|
443
|
-
api.resources.tasks.patch({ id: items[0].id, beforeId: null }),
|
|
444
|
-
api.resources.tasks.patch({ id: items[2].id, beforeId: null })
|
|
445
|
-
)
|
|
446
|
-
|
|
447
|
-
// Move items to middle positions
|
|
448
|
-
updates.push(
|
|
449
|
-
api.resources.tasks.patch({ id: items[1].id, beforeId: items[4].id }),
|
|
450
|
-
api.resources.tasks.patch({ id: items[3].id, beforeId: items[6].id })
|
|
451
|
-
)
|
|
452
|
-
|
|
453
|
-
await Promise.all(updates)
|
|
454
|
-
|
|
455
|
-
// Verify final state
|
|
456
|
-
const { data: finalTasks } = await api.resources.tasks.query({
|
|
457
|
-
queryParams: {
|
|
458
|
-
filters: { category: category.id },
|
|
459
|
-
sort: ['position']
|
|
460
|
-
}
|
|
461
|
-
})
|
|
462
|
-
|
|
463
|
-
assert.equal(finalTasks.length, 10)
|
|
464
|
-
|
|
465
|
-
// All positions should be unique
|
|
466
|
-
const positions = finalTasks.map(t => t.position)
|
|
467
|
-
const uniquePositions = [...new Set(positions)]
|
|
468
|
-
assert.equal(uniquePositions.length, 10)
|
|
469
|
-
|
|
470
|
-
// All should be properly ordered
|
|
471
|
-
for (let i = 1; i < finalTasks.length; i++) {
|
|
472
|
-
assert(finalTasks[i - 1].position < finalTasks[i].position)
|
|
473
|
-
}
|
|
474
|
-
})
|
|
475
|
-
|
|
476
|
-
// Removed test for missing position field - not a realistic scenario
|
|
477
|
-
})
|
|
478
|
-
|
|
479
|
-
describe('Special Positioning Values', () => {
|
|
480
|
-
it('should handle FIRST positioning correctly', async () => {
|
|
481
|
-
// Use the main api instance
|
|
482
|
-
|
|
483
|
-
const category = await api.resources.categories.post({ name: 'Test Category' })
|
|
484
|
-
|
|
485
|
-
// Create initial tasks
|
|
486
|
-
const tasks = []
|
|
487
|
-
for (let i = 1; i <= 5; i++) {
|
|
488
|
-
const task = await api.resources.tasks.post({
|
|
489
|
-
title: `Task ${i}`,
|
|
490
|
-
category: category.id
|
|
491
|
-
})
|
|
492
|
-
tasks.push(task)
|
|
493
|
-
}
|
|
494
|
-
|
|
495
|
-
// Position new task at beginning
|
|
496
|
-
const firstTask = await api.resources.tasks.post({
|
|
497
|
-
title: 'First Task',
|
|
498
|
-
category: category.id,
|
|
499
|
-
beforeId: 'FIRST'
|
|
500
|
-
})
|
|
501
|
-
|
|
502
|
-
const { data: allTasks } = await api.resources.tasks.query({
|
|
503
|
-
queryParams: {
|
|
504
|
-
filters: { category: category.id },
|
|
505
|
-
sort: ['position']
|
|
506
|
-
}
|
|
507
|
-
})
|
|
508
|
-
|
|
509
|
-
assert.equal(allTasks[0].title, 'First Task')
|
|
510
|
-
assert(firstTask.position < tasks[0].position)
|
|
511
|
-
|
|
512
|
-
// Update existing task to move to beginning
|
|
513
|
-
await api.resources.tasks.patch({
|
|
514
|
-
id: tasks[2].id,
|
|
515
|
-
beforeId: 'FIRST'
|
|
516
|
-
})
|
|
517
|
-
|
|
518
|
-
const { data: updatedTasks } = await api.resources.tasks.query({
|
|
519
|
-
queryParams: {
|
|
520
|
-
filters: { category: category.id },
|
|
521
|
-
sort: ['position']
|
|
522
|
-
}
|
|
523
|
-
})
|
|
524
|
-
|
|
525
|
-
assert.equal(updatedTasks[0].title, 'Task 3')
|
|
526
|
-
assert.equal(updatedTasks[1].title, 'First Task')
|
|
527
|
-
})
|
|
528
|
-
|
|
529
|
-
it('should handle null beforeId as positioning at end', async () => {
|
|
530
|
-
// Use the main api instance
|
|
531
|
-
|
|
532
|
-
const category = await api.resources.categories.post({ name: 'Test Category' })
|
|
533
|
-
|
|
534
|
-
// Create tasks
|
|
535
|
-
const task1 = await api.resources.tasks.post({
|
|
536
|
-
title: 'Task 1',
|
|
537
|
-
category: category.id
|
|
538
|
-
})
|
|
539
|
-
|
|
540
|
-
const task2 = await api.resources.tasks.post({
|
|
541
|
-
title: 'Task 2',
|
|
542
|
-
category: category.id
|
|
543
|
-
})
|
|
544
|
-
|
|
545
|
-
// Move task 1 to end with explicit null
|
|
546
|
-
await api.resources.tasks.patch({
|
|
547
|
-
id: task1.id,
|
|
548
|
-
beforeId: null
|
|
549
|
-
})
|
|
550
|
-
|
|
551
|
-
const { data: tasks } = await api.resources.tasks.query({
|
|
552
|
-
queryParams: {
|
|
553
|
-
filters: { category: category.id },
|
|
554
|
-
sort: ['position']
|
|
555
|
-
}
|
|
556
|
-
})
|
|
557
|
-
|
|
558
|
-
assert.equal(tasks[0].title, 'Task 2')
|
|
559
|
-
assert.equal(tasks[1].title, 'Task 1')
|
|
560
|
-
})
|
|
561
|
-
})
|
|
562
|
-
|
|
563
|
-
describe('Integration with Other Plugins', () => {
|
|
564
|
-
it('should work with soft delete scenarios', async () => {
|
|
565
|
-
const softDeleteApi = await createPositioningApi(knex, {
|
|
566
|
-
apiName: 'positioning-soft-delete-test',
|
|
567
|
-
tablePrefix: 'pos_soft'
|
|
568
|
-
})
|
|
569
|
-
|
|
570
|
-
await softDeleteApi.use(PositioningPlugin, {
|
|
571
|
-
field: 'position',
|
|
572
|
-
filters: ['category'],
|
|
573
|
-
defaultPosition: 'last',
|
|
574
|
-
excludeResources: ['system_migrations', 'system_logs', 'categories', 'projects']
|
|
575
|
-
})
|
|
576
|
-
|
|
577
|
-
const category = await softDeleteApi.resources.categories.post({ name: 'Test Category' })
|
|
578
|
-
|
|
579
|
-
// Create tasks
|
|
580
|
-
const tasks = []
|
|
581
|
-
for (let i = 1; i <= 5; i++) {
|
|
582
|
-
const task = await softDeleteApi.resources.tasks.post({
|
|
583
|
-
title: `Task ${i}`,
|
|
584
|
-
category: category.id
|
|
585
|
-
})
|
|
586
|
-
tasks.push(task)
|
|
587
|
-
}
|
|
588
|
-
|
|
589
|
-
// Soft delete task 3
|
|
590
|
-
await softDeleteApi.resources.tasks.patch({
|
|
591
|
-
id: tasks[2].id,
|
|
592
|
-
deleted_at: new Date().toISOString()
|
|
593
|
-
})
|
|
594
|
-
|
|
595
|
-
// Create new task - should still position correctly
|
|
596
|
-
const newTask = await softDeleteApi.resources.tasks.post({
|
|
597
|
-
title: 'New Task',
|
|
598
|
-
category: category.id,
|
|
599
|
-
beforeId: tasks[3].id // Before task 4
|
|
600
|
-
})
|
|
601
|
-
|
|
602
|
-
// Query non-deleted tasks
|
|
603
|
-
const { data: activeTasks } = await softDeleteApi.resources.tasks.query({
|
|
604
|
-
queryParams: {
|
|
605
|
-
filters: {
|
|
606
|
-
category: category.id,
|
|
607
|
-
deleted_at: null
|
|
608
|
-
},
|
|
609
|
-
sort: ['position']
|
|
610
|
-
}
|
|
611
|
-
})
|
|
612
|
-
|
|
613
|
-
// Should have 5 tasks (original 5 - 1 deleted + 1 new)
|
|
614
|
-
assert.equal(activeTasks.length, 5)
|
|
615
|
-
|
|
616
|
-
// New task should be before Task 4
|
|
617
|
-
const newTaskIndex = activeTasks.findIndex(t => t.title === 'New Task')
|
|
618
|
-
const task4Index = activeTasks.findIndex(t => t.title === 'Task 4')
|
|
619
|
-
assert(newTaskIndex < task4Index)
|
|
620
|
-
})
|
|
621
|
-
|
|
622
|
-
it('should handle versioning scenarios', async () => {
|
|
623
|
-
const versionApi = await createPositioningApi(knex, {
|
|
624
|
-
apiName: 'positioning-version-test',
|
|
625
|
-
tablePrefix: 'pos_ver'
|
|
626
|
-
})
|
|
627
|
-
|
|
628
|
-
await versionApi.use(PositioningPlugin, {
|
|
629
|
-
field: 'position',
|
|
630
|
-
filters: ['category', 'version'],
|
|
631
|
-
defaultPosition: 'last',
|
|
632
|
-
excludeResources: ['system_migrations', 'system_logs', 'categories', 'projects']
|
|
633
|
-
})
|
|
634
|
-
|
|
635
|
-
const category = await versionApi.resources.categories.post({ name: 'Test Category' })
|
|
636
|
-
|
|
637
|
-
// Create v1 tasks
|
|
638
|
-
const v1Tasks = []
|
|
639
|
-
for (let i = 1; i <= 3; i++) {
|
|
640
|
-
const task = await versionApi.resources.tasks.post({
|
|
641
|
-
title: `Task ${i} v1`,
|
|
642
|
-
category: category.id,
|
|
643
|
-
version: 1
|
|
644
|
-
})
|
|
645
|
-
v1Tasks.push(task)
|
|
646
|
-
}
|
|
647
|
-
|
|
648
|
-
// Create v2 of task 2
|
|
649
|
-
const task2v2 = await versionApi.resources.tasks.post({
|
|
650
|
-
title: 'Task 2 v2',
|
|
651
|
-
category: category.id,
|
|
652
|
-
version: 2
|
|
653
|
-
})
|
|
654
|
-
|
|
655
|
-
// Create v2 of task 1
|
|
656
|
-
const task1v2 = await versionApi.resources.tasks.post({
|
|
657
|
-
title: 'Task 1 v2',
|
|
658
|
-
category: category.id,
|
|
659
|
-
version: 2
|
|
660
|
-
})
|
|
661
|
-
|
|
662
|
-
// Query v1 tasks
|
|
663
|
-
const { data: v1Items } = await versionApi.resources.tasks.query({
|
|
664
|
-
queryParams: {
|
|
665
|
-
filters: { category: category.id, version: 1 },
|
|
666
|
-
sort: ['position']
|
|
667
|
-
}
|
|
668
|
-
})
|
|
669
|
-
|
|
670
|
-
assert.equal(v1Items.length, 3)
|
|
671
|
-
assert.equal(v1Items[0].title, 'Task 1 v1')
|
|
672
|
-
assert.equal(v1Items[1].title, 'Task 2 v1')
|
|
673
|
-
assert.equal(v1Items[2].title, 'Task 3 v1')
|
|
674
|
-
|
|
675
|
-
// Query v2 tasks
|
|
676
|
-
const { data: v2Items } = await versionApi.resources.tasks.query({
|
|
677
|
-
queryParams: {
|
|
678
|
-
filters: { category: category.id, version: 2 },
|
|
679
|
-
sort: ['position']
|
|
680
|
-
}
|
|
681
|
-
})
|
|
682
|
-
|
|
683
|
-
assert.equal(v2Items.length, 2)
|
|
684
|
-
assert.equal(v2Items[0].title, 'Task 2 v2')
|
|
685
|
-
assert.equal(v2Items[1].title, 'Task 1 v2')
|
|
686
|
-
|
|
687
|
-
// Positions should be independent between versions
|
|
688
|
-
assert.equal(v1Tasks[0].position, 'a0')
|
|
689
|
-
assert.equal(task2v2.position, 'a0') // First in v2 group
|
|
690
|
-
})
|
|
691
|
-
})
|
|
692
|
-
})
|