json-rest-api 1.0.20 → 1.0.22
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/audit/00-charter.md +103 -0
- package/audit/01-scope-map.md +84 -0
- package/audit/02-coverage-matrix.csv +174 -0
- package/audit/03-invariants.md +123 -0
- package/audit/04-standards-rubric.md +56 -0
- package/audit/05-competitor-rubric.md +77 -0
- package/audit/06-findings.md +160 -0
- package/audit/07-risk-register.csv +10 -0
- package/audit/08-remediation-roadmap.md +177 -0
- package/audit/09-signoff-check.md +23 -0
- package/audit/10-final-synthesis.md +29 -0
- package/audit/evidence/code/all-files-json-rest-api.txt +168 -0
- package/audit/evidence/code/all-files-json-rest-schema.txt +12 -0
- package/audit/evidence/code/boundary-leak-checks.txt +72 -0
- package/audit/evidence/code/critical-pass1/json-rest-api__plugins__core__bulk-operations-plugin.js.txt +480 -0
- package/audit/evidence/code/critical-pass1/json-rest-api__plugins__core__connectors__express-plugin.js.txt +532 -0
- package/audit/evidence/code/critical-pass1/json-rest-api__plugins__core__connectors__lib__busboy-detector.js.txt +175 -0
- package/audit/evidence/code/critical-pass1/json-rest-api__plugins__core__connectors__lib__formidable-detector.js.txt +138 -0
- package/audit/evidence/code/critical-pass1/json-rest-api__plugins__core__connectors__lib__request-helpers.js.txt +222 -0
- package/audit/evidence/code/critical-pass1/json-rest-api__plugins__core__file-handling-plugin.js.txt +342 -0
- package/audit/evidence/code/critical-pass1/json-rest-api__plugins__core__lib__dbTablesOperations.js.txt +304 -0
- package/audit/evidence/code/critical-pass1/json-rest-api__plugins__core__lib__fractional-positioning.js.txt +188 -0
- package/audit/evidence/code/critical-pass1/json-rest-api__plugins__core__lib__querying-writing__compile-schemas.js.txt +336 -0
- package/audit/evidence/code/critical-pass1/json-rest-api__plugins__core__lib__querying-writing__connectors-query-parser.js.txt +165 -0
- package/audit/evidence/code/critical-pass1/json-rest-api__plugins__core__lib__querying-writing__database-capabilities.js.txt +265 -0
- package/audit/evidence/code/critical-pass1/json-rest-api__plugins__core__lib__querying-writing__database-value-normalizers.js.txt +369 -0
- package/audit/evidence/code/critical-pass1/json-rest-api__plugins__core__lib__querying-writing__default-data-helpers.js.txt +72 -0
- package/audit/evidence/code/critical-pass1/json-rest-api__plugins__core__lib__querying-writing__field-utils.js.txt +139 -0
- package/audit/evidence/code/critical-pass1/json-rest-api__plugins__core__lib__querying-writing__knex-constants.js.txt +45 -0
- package/audit/evidence/code/critical-pass1/json-rest-api__plugins__core__lib__querying-writing__knex-field-helpers.js.txt +396 -0
- package/audit/evidence/code/critical-pass1/json-rest-api__plugins__core__lib__querying-writing__knex-json-api-transformers.js.txt +162 -0
- package/audit/evidence/code/critical-pass1/json-rest-api__plugins__core__lib__querying-writing__payload-validators.js.txt +1314 -0
- package/audit/evidence/code/critical-pass1/json-rest-api__plugins__core__lib__querying-writing__schema-helpers.js.txt +488 -0
- package/audit/evidence/code/critical-pass1/json-rest-api__plugins__core__lib__querying-writing__scope-validations.js.txt +484 -0
- package/audit/evidence/code/critical-pass1/json-rest-api__plugins__core__lib__querying-writing__simplified-helpers.js.txt +857 -0
- package/audit/evidence/code/critical-pass1/json-rest-api__plugins__core__lib__querying__knex-cross-table-search.js.txt +565 -0
- package/audit/evidence/code/critical-pass1/json-rest-api__plugins__core__lib__querying__knex-json-api-transformers-querying.js.txt +485 -0
- package/audit/evidence/code/critical-pass1/json-rest-api__plugins__core__lib__querying__knex-pagination-helpers.js.txt +560 -0
- package/audit/evidence/code/critical-pass1/json-rest-api__plugins__core__lib__querying__knex-process-includes.js.txt +167 -0
- package/audit/evidence/code/critical-pass1/json-rest-api__plugins__core__lib__querying__knex-query-helpers-base.js.txt +109 -0
- package/audit/evidence/code/critical-pass1/json-rest-api__plugins__core__lib__querying__knex-query-helpers.js.txt +1064 -0
- package/audit/evidence/code/critical-pass1/json-rest-api__plugins__core__lib__querying__knex-relationship-includes.js.txt +1983 -0
- package/audit/evidence/code/critical-pass1/json-rest-api__plugins__core__lib__querying__knex-window-queries.js.txt +328 -0
- package/audit/evidence/code/critical-pass1/json-rest-api__plugins__core__lib__querying__url-helpers.js.txt +119 -0
- package/audit/evidence/code/critical-pass1/json-rest-api__plugins__core__lib__storage__storage-adapter.js.txt +160 -0
- package/audit/evidence/code/critical-pass1/json-rest-api__plugins__core__lib__writing__many-to-many-manipulations.js.txt +300 -0
- package/audit/evidence/code/critical-pass1/json-rest-api__plugins__core__lib__writing__relationship-processor.js.txt +245 -0
- package/audit/evidence/code/critical-pass1/json-rest-api__plugins__core__rest-api-access.js.txt +459 -0
- package/audit/evidence/code/critical-pass1/json-rest-api__plugins__core__rest-api-cors-plugin.js.txt +237 -0
- package/audit/evidence/code/critical-pass1/json-rest-api__plugins__core__rest-api-knex-plugin.js.txt +1135 -0
- package/audit/evidence/code/critical-pass1/json-rest-api__plugins__core__rest-api-multihome-plugin.js.txt +314 -0
- package/audit/evidence/code/critical-pass1/json-rest-api__plugins__core__rest-api-plugin-hooks__compile-resource-schemas.js.txt +7 -0
- package/audit/evidence/code/critical-pass1/json-rest-api__plugins__core__rest-api-plugin-hooks__register-relationship-routes.js.txt +71 -0
- package/audit/evidence/code/critical-pass1/json-rest-api__plugins__core__rest-api-plugin-hooks__register-scope-routes.js.txt +102 -0
- package/audit/evidence/code/critical-pass1/json-rest-api__plugins__core__rest-api-plugin-hooks__turn-scope-init-into-vars.js.txt +49 -0
- package/audit/evidence/code/critical-pass1/json-rest-api__plugins__core__rest-api-plugin-hooks__validate-include-configurations.js.txt +39 -0
- package/audit/evidence/code/critical-pass1/json-rest-api__plugins__core__rest-api-plugin-methods__add-route.js.txt +23 -0
- package/audit/evidence/code/critical-pass1/json-rest-api__plugins__core__rest-api-plugin-methods__check-permissions.js.txt +13 -0
- package/audit/evidence/code/critical-pass1/json-rest-api__plugins__core__rest-api-plugin-methods__common.js.txt +639 -0
- package/audit/evidence/code/critical-pass1/json-rest-api__plugins__core__rest-api-plugin-methods__delete-relationship.js.txt +117 -0
- package/audit/evidence/code/critical-pass1/json-rest-api__plugins__core__rest-api-plugin-methods__delete.js.txt +104 -0
- package/audit/evidence/code/critical-pass1/json-rest-api__plugins__core__rest-api-plugin-methods__enrich-attributes.js.txt +199 -0
- package/audit/evidence/code/critical-pass1/json-rest-api__plugins__core__rest-api-plugin-methods__get-related.js.txt +353 -0
- package/audit/evidence/code/critical-pass1/json-rest-api__plugins__core__rest-api-plugin-methods__get-relationship.js.txt +61 -0
- package/audit/evidence/code/critical-pass1/json-rest-api__plugins__core__rest-api-plugin-methods__get.js.txt +184 -0
- package/audit/evidence/code/critical-pass1/json-rest-api__plugins__core__rest-api-plugin-methods__patch-relationship.js.txt +55 -0
- package/audit/evidence/code/critical-pass1/json-rest-api__plugins__core__rest-api-plugin-methods__patch.js.txt +191 -0
- package/audit/evidence/code/critical-pass1/json-rest-api__plugins__core__rest-api-plugin-methods__post-relationship.js.txt +110 -0
- package/audit/evidence/code/critical-pass1/json-rest-api__plugins__core__rest-api-plugin-methods__post.js.txt +168 -0
- package/audit/evidence/code/critical-pass1/json-rest-api__plugins__core__rest-api-plugin-methods__put.js.txt +253 -0
- package/audit/evidence/code/critical-pass1/json-rest-api__plugins__core__rest-api-plugin-methods__query.js.txt +207 -0
- package/audit/evidence/code/critical-pass1/json-rest-api__plugins__core__rest-api-plugin-methods__release.js.txt +12 -0
- package/audit/evidence/code/critical-pass1/json-rest-api__plugins__core__rest-api-plugin.js.txt +165 -0
- package/audit/evidence/code/critical-pass1/json-rest-api__plugins__core__rest-api-positioning-plugin.js.txt +668 -0
- package/audit/evidence/code/critical-pass1/json-rest-api__plugins__core__socketio-plugin.js.txt +621 -0
- package/audit/evidence/code/critical-pass1/json-rest-api__plugins__storage__local-storage.js.txt +218 -0
- package/audit/evidence/code/critical-pass1/json-rest-api__plugins__storage__s3-storage.js.txt +124 -0
- package/audit/evidence/code/critical-pass1/json-rest-schema__src__CorePlugin.js.txt +269 -0
- package/audit/evidence/code/critical-pass1/json-rest-schema__src__Schema.js.txt +242 -0
- package/audit/evidence/code/critical-pass1/json-rest-schema__src__index.js.txt +77 -0
- package/audit/evidence/code/critical-pass1-risk-keywords.txt +238 -0
- package/audit/evidence/code/critical-pass1-summary.csv +67 -0
- package/audit/evidence/code/critical-pass2-summary.csv +67 -0
- package/audit/evidence/code/early-silent-zones-no-test-token.txt +57 -0
- package/audit/evidence/code/error-model-signals.txt +98 -0
- package/audit/evidence/code/git-files-json-rest-api.txt +171 -0
- package/audit/evidence/code/git-files-json-rest-schema.txt +27 -0
- package/audit/evidence/code/hook-flow-signals.txt +282 -0
- package/audit/evidence/code/idproperty-signals.txt +157 -0
- package/audit/evidence/code/import-cycles-non-anyapi.txt +3 -0
- package/audit/evidence/code/import-hotspots-non-anyapi.txt +96 -0
- package/audit/evidence/code/imports-json-rest-api.txt +354 -0
- package/audit/evidence/code/imports-json-rest-schema.txt +15 -0
- package/audit/evidence/code/in-scope-files-json-rest-api.txt +160 -0
- package/audit/evidence/code/in-scope-files-json-rest-schema.txt +13 -0
- package/audit/evidence/code/non-anyapi-core-files.txt +101 -0
- package/audit/evidence/code/permission-signals.txt +193 -0
- package/audit/evidence/code/query-include-signals.txt +687 -0
- package/audit/evidence/code/transaction-signals.txt +28 -0
- package/audit/evidence/code/validation-signals.txt +650 -0
- package/audit/evidence/code/write-path-signals.txt +61 -0
- package/audit/evidence/docs/docs-claims-signals.txt +1075 -0
- package/audit/evidence/docs/schema-docs-claims-signals.txt +179 -0
- package/audit/evidence/external/corpus-freeze.md +21 -0
- package/audit/evidence/external/npm-competitor-metadata.txt +70 -0
- package/audit/evidence/external/npm-download-signals.txt +23 -0
- package/audit/evidence/external/npm-jsonapi-server.txt +8 -0
- package/audit/evidence/external/npm-modern-jsonapi-metadata.txt +38 -0
- package/audit/evidence/external/readme-fortune-signals.txt +26 -0
- package/audit/evidence/external/readme-fortune.md +165 -0
- package/audit/evidence/external/readme-json-api-serializer-signals.txt +80 -0
- package/audit/evidence/external/readme-json-api-serializer.md +690 -0
- package/audit/evidence/external/readme-jsonapi-serde-server-signals.txt +0 -0
- package/audit/evidence/external/readme-jsonapi-serde-server.md +22 -0
- package/audit/evidence/external/readme-jsonapi-serializer-signals.txt +63 -0
- package/audit/evidence/external/readme-jsonapi-serializer.md +315 -0
- package/audit/evidence/external/readme-jsonapi-server-signals.txt +19 -0
- package/audit/evidence/external/readme-jsonapi-server.md +104 -0
- package/audit/evidence/external/readme-nestjs-json-api-signals.txt +4 -0
- package/audit/evidence/external/readme-nestjs-json-api.md +94 -0
- package/audit/evidence/external/readme-yaser-json-api-nestjs-signals.txt +47 -0
- package/audit/evidence/external/readme-yaser-json-api-nestjs.md +447 -0
- package/audit/evidence/external/standard-jsonapi-snippets.txt +84 -0
- package/audit/evidence/external/standard-rfc-snippets.txt +168 -0
- package/audit/evidence/external/standards-sources.txt +6 -0
- package/audit/evidence/runtime/baseline-commits.txt +2 -0
- package/audit/evidence/runtime/db-adapter-assumptions.txt +3 -0
- package/audit/evidence/runtime/environment.txt +4 -0
- package/audit/evidence/runtime/invariant-runtime-mapping.md +38 -0
- package/audit/evidence/runtime/probe-patch-relationship-error.exitcode +1 -0
- package/audit/evidence/runtime/probe-patch-relationship-error.log +76 -0
- package/audit/evidence/runtime/probe-patch-relationship-error.mjs +67 -0
- package/audit/evidence/runtime/probe-patch-relationship-referenceerror.exitcode +1 -0
- package/audit/evidence/runtime/probe-patch-relationship-referenceerror.log +62 -0
- package/audit/evidence/runtime/probe-patch-relationship-referenceerror.mjs +47 -0
- package/audit/evidence/runtime/test-exitcodes.txt +4 -0
- package/audit/evidence/runtime/test-summary.md +107 -0
- package/audit/evidence/runtime/trace-anchors.txt +61 -0
- package/audit/evidence/tests/access-multitenancy-test-signals.txt +125 -0
- package/audit/evidence/tests/failing-tests-summary.txt +25 -0
- package/audit/evidence/tests/npm-test-json-rest-api.exitcode +1 -0
- package/audit/evidence/tests/npm-test-json-rest-api.log +5885 -0
- package/audit/evidence/tests/npm-test-json-rest-schema.exitcode +1 -0
- package/audit/evidence/tests/npm-test-json-rest-schema.log +356 -0
- package/audit/evidence/tests/query-include-test-signals.txt +948 -0
- package/audit/evidence/tests/relationship-test-signals.txt +223 -0
- package/audit/evidence/tests/skipped-tests.txt +10 -0
- package/audit/evidence/tests/test-suite-list-non-anyapi.txt +30 -0
- package/audit/evidence/tests/test-suite-list.txt +32 -0
- package/audit/subsystems/docs-drift.md +49 -0
- package/audit/subsystems/pass2-cross-review.md +131 -0
- package/audit/subsystems/plugins-access.md +51 -0
- package/audit/subsystems/plugins-bulk.md +50 -0
- package/audit/subsystems/plugins-cors.md +48 -0
- package/audit/subsystems/plugins-file.md +53 -0
- package/audit/subsystems/plugins-multihome.md +50 -0
- package/audit/subsystems/plugins-positioning.md +54 -0
- package/audit/subsystems/plugins-socketio.md +50 -0
- package/audit/subsystems/query-engine.md +61 -0
- package/audit/subsystems/relationships.md +55 -0
- package/audit/subsystems/rest-orchestration.md +66 -0
- package/audit/subsystems/schema-core.md +64 -0
- package/audit/subsystems/storage-knex.md +57 -0
- package/audit/subsystems/tests.md +48 -0
- package/audit/subsystems/transport-express.md +55 -0
- package/audit/traces/trace-read-get.md +22 -0
- package/audit/traces/trace-read-query.md +25 -0
- package/audit/traces/trace-relationship-endpoints.md +31 -0
- package/audit/traces/trace-write-delete.md +17 -0
- package/audit/traces/trace-write-patch.md +26 -0
- package/audit/traces/trace-write-post.md +22 -0
- package/audit/traces/trace-write-put.md +23 -0
- package/package.json +5 -5
|
@@ -0,0 +1,668 @@
|
|
|
1
|
+
1 import {
|
|
2
|
+
2 calculatePosition,
|
|
3
|
+
3 getInitialPosition,
|
|
4
|
+
4 isValidPosition,
|
|
5
|
+
5 assignInitialPositions,
|
|
6
|
+
6 } from './lib/fractional-positioning.js'
|
|
7
|
+
7 import { createStorageAdapter } from './lib/storage/storage-adapter.js'
|
|
8
|
+
8
|
|
9
|
+
9 export const PositioningPlugin = {
|
|
10
|
+
10 name: 'positioning',
|
|
11
|
+
11 dependencies: ['rest-api', 'rest-api-knex|rest-api-anyapi-knex'],
|
|
12
|
+
12
|
|
13
|
+
13 install ({ api, addHook, vars, helpers, log, scopes, pluginOptions }) {
|
|
14
|
+
14 const installedPlugins = Array.from(api._installedPlugins || [])
|
|
15
|
+
15 const legacyStorageInstalled = installedPlugins.includes('rest-api-knex')
|
|
16
|
+
16 const canonicalStorageInstalled = installedPlugins.includes('rest-api-anyapi-knex')
|
|
17
|
+
17 if (!legacyStorageInstalled && !canonicalStorageInstalled) {
|
|
18
|
+
18 throw new Error(
|
|
19
|
+
19 "Positioning plugin requires either 'rest-api-knex' or 'rest-api-anyapi-knex' to be installed before it."
|
|
20
|
+
20 )
|
|
21
|
+
21 }
|
|
22
|
+
22
|
|
23
|
+
23 if (!api.knex?.instance) {
|
|
24
|
+
24 throw new Error('Positioning plugin requires a storage plugin with knex support (rest-api-knex or rest-api-anyapi-knex)')
|
|
25
|
+
25 }
|
|
26
|
+
26
|
|
27
|
+
27 // Get configuration - hooked-api namespaces options by plugin name
|
|
28
|
+
28 const positioningOptions = pluginOptions || {}
|
|
29
|
+
29
|
|
30
|
+
30 // Store configuration in vars (data only) - inspired by multihome pattern
|
|
31
|
+
31 vars.positioning = {
|
|
32
|
+
32 field: positioningOptions.field || 'position',
|
|
33
|
+
33 filters: positioningOptions.filters || [],
|
|
34
|
+
34 excludeResources: positioningOptions.excludeResources || ['system_migrations', 'system_logs'],
|
|
35
|
+
35 strategy: positioningOptions.strategy || 'fractional',
|
|
36
|
+
36 beforeIdField: positioningOptions.beforeIdField || 'beforeId',
|
|
37
|
+
37 defaultPosition: positioningOptions.defaultPosition || 'last',
|
|
38
|
+
38 autoIndex: positioningOptions.autoIndex !== undefined ? positioningOptions.autoIndex : true,
|
|
39
|
+
39 rebalanceThreshold: positioningOptions.rebalanceThreshold || 50 // Max position string length
|
|
40
|
+
40 }
|
|
41
|
+
41
|
|
42
|
+
42 const resolveStorageAdapter = (scopeName) => {
|
|
43
|
+
43 if (!scopeName) return null
|
|
44
|
+
44 const scope = scopes[scopeName] || api.resources?.[scopeName]
|
|
45
|
+
45 const scopeVars = scope?.vars
|
|
46
|
+
46 if (!scopeVars) return null
|
|
47
|
+
47
|
|
48
|
+
48 if (scopeVars.storageAdapter) {
|
|
49
|
+
49 return scopeVars.storageAdapter
|
|
50
|
+
50 }
|
|
51
|
+
51
|
|
52
|
+
52 if (scopeVars.schemaInfo && api.knex?.instance) {
|
|
53
|
+
53 const adapter = createStorageAdapter({ knex: api.knex.instance, schemaInfo: scopeVars.schemaInfo })
|
|
54
|
+
54 scopeVars.storageAdapter = adapter
|
|
55
|
+
55 return adapter
|
|
56
|
+
56 }
|
|
57
|
+
57
|
|
58
|
+
58 return null
|
|
59
|
+
59 }
|
|
60
|
+
60
|
|
61
|
+
61 // Validate configuration
|
|
62
|
+
62 if (!['fractional', 'integer'].includes(vars.positioning.strategy)) {
|
|
63
|
+
63 throw new Error(`Invalid positioning strategy: ${vars.positioning.strategy}. Must be 'fractional' or 'integer'`)
|
|
64
|
+
64 }
|
|
65
|
+
65
|
|
66
|
+
66 if (vars.positioning.strategy === 'integer') {
|
|
67
|
+
67 throw new Error("Integer positioning strategy not yet implemented. Please use 'fractional'")
|
|
68
|
+
68 }
|
|
69
|
+
69
|
|
70
|
+
70 // Helper to check if a resource should have positioning
|
|
71
|
+
71 function shouldHavePositioning (scopeName) {
|
|
72
|
+
72 return !vars.positioning.excludeResources.includes(scopeName)
|
|
73
|
+
73 }
|
|
74
|
+
74
|
|
75
|
+
75 // Helper to build filter conditions for position groups
|
|
76
|
+
76 function buildPositionFilterConditions (adapter, schemaInfo, {
|
|
77
|
+
77 attributes = {},
|
|
78
|
+
78 rawRecord = {},
|
|
79
|
+
79 simplifiedRecord = {},
|
|
80
|
+
80 inputRelationships = {},
|
|
81
|
+
81 minimalRelationships = {},
|
|
82
|
+
82 }) {
|
|
83
|
+
83 const conditions = {}
|
|
84
|
+
84
|
|
85
|
+
85 log.debug('*** buildPositionFilterConditions', {
|
|
86
|
+
86 filters: vars.positioning.filters,
|
|
87
|
+
87 attributeKeys: Object.keys(attributes || {}),
|
|
88
|
+
88 rawKeys: Object.keys(rawRecord || {}),
|
|
89
|
+
89 simplifiedKeys: Object.keys(simplifiedRecord || {}),
|
|
90
|
+
90 })
|
|
91
|
+
91
|
|
92
|
+
92 const tryAssign = (holder, field) => (holder && field in holder ? holder[field] : undefined)
|
|
93
|
+
93
|
|
94
|
+
94 vars.positioning.filters.forEach((filterField) => {
|
|
95
|
+
95 const searchField = schemaInfo.searchSchemaStructure?.[filterField] || null
|
|
96
|
+
96 const schemaField = schemaInfo.schemaStructure?.[filterField] || null
|
|
97
|
+
97 const isRelationship = Boolean(
|
|
98
|
+
98 searchField?.isRelationship ||
|
|
99
|
+
99 schemaField?.belongsTo ||
|
|
100
|
+
100 schemaField?.belongsToPolymorphic
|
|
101
|
+
101 )
|
|
102
|
+
102
|
|
103
|
+
103 if (!searchField && !schemaField) {
|
|
104
|
+
104 log.warn(`Filter field '${filterField}' not found in search schema or resource schema for positioning`)
|
|
105
|
+
105 }
|
|
106
|
+
106
|
|
107
|
+
107 const dbField = searchField?.actualField || filterField
|
|
108
|
+
108 let value
|
|
109
|
+
109
|
|
110
|
+
110 const attempt = (candidate) => {
|
|
111
|
+
111 if (value === undefined && candidate !== undefined) {
|
|
112
|
+
112 value = candidate
|
|
113
|
+
113 }
|
|
114
|
+
114 }
|
|
115
|
+
115
|
|
116
|
+
116 attempt(tryAssign(rawRecord, filterField))
|
|
117
|
+
117 attempt(tryAssign(simplifiedRecord, filterField))
|
|
118
|
+
118 attempt(tryAssign(rawRecord, dbField))
|
|
119
|
+
119 attempt(tryAssign(attributes, filterField))
|
|
120
|
+
120 attempt(tryAssign(attributes, dbField))
|
|
121
|
+
121
|
|
122
|
+
122 if (isRelationship) {
|
|
123
|
+
123 attempt(inputRelationships?.[filterField]?.data?.id)
|
|
124
|
+
124 attempt(minimalRelationships?.[filterField]?.data?.id)
|
|
125
|
+
125 // Some contexts may expose relationship data under attributes
|
|
126
|
+
126 attempt(tryAssign(rawRecord?.relationships, filterField)?.data?.id)
|
|
127
|
+
127 }
|
|
128
|
+
128
|
|
129
|
+
129 if (value !== undefined) {
|
|
130
|
+
130 const column = adapter.translateColumn(dbField)
|
|
131
|
+
131 const normalizedValue = value === null
|
|
132
|
+
132 ? null
|
|
133
|
+
133 : adapter.translateFilterValue(filterField, value)
|
|
134
|
+
134 conditions[column] = normalizedValue
|
|
135
|
+
135 }
|
|
136
|
+
136 })
|
|
137
|
+
137
|
|
138
|
+
138 log.debug('*** Filter conditions built', { conditions })
|
|
139
|
+
139 return conditions
|
|
140
|
+
140 }
|
|
141
|
+
141
|
|
142
|
+
142 // Validate that resources have position field when added
|
|
143
|
+
143 addHook('scope:added', 'validate-position-field', {}, ({ context, vars }) => {
|
|
144
|
+
144 const { scopeName, scopeOptions } = context
|
|
145
|
+
145
|
|
146
|
+
146 // Skip excluded resources
|
|
147
|
+
147 if (!shouldHavePositioning(scopeName)) {
|
|
148
|
+
148 log.debug(`Resource ${scopeName} excluded from positioning`)
|
|
149
|
+
149 return
|
|
150
|
+
150 }
|
|
151
|
+
151
|
|
152
|
+
152 // Check if schema has position field
|
|
153
|
+
153 const schema = scopeOptions.schema
|
|
154
|
+
154 if (schema && !schema[vars.positioning.field]) {
|
|
155
|
+
155 throw new Error(
|
|
156
|
+
156 `Resource '${scopeName}' must have '${vars.positioning.field}' field in schema to use positioning plugin`
|
|
157
|
+
157 )
|
|
158
|
+
158 }
|
|
159
|
+
159
|
|
160
|
+
160 // Validate filter fields exist (check both relationship names and actual field names)
|
|
161
|
+
161 vars.positioning.filters.forEach(filterField => {
|
|
162
|
+
162 // Check if it's a relationship field by looking for a field with matching 'as' property
|
|
163
|
+
163 let fieldExists = false
|
|
164
|
+
164
|
|
165
|
+
165 // First check if the field exists directly in the schema
|
|
166
|
+
166 if (schema[filterField]) {
|
|
167
|
+
167 fieldExists = true
|
|
168
|
+
168 } else {
|
|
169
|
+
169 // Check if it's a relationship name (as property)
|
|
170
|
+
170 for (const [fieldName, fieldDef] of Object.entries(schema)) {
|
|
171
|
+
171 if (fieldDef.as === filterField) {
|
|
172
|
+
172 fieldExists = true
|
|
173
|
+
173 break
|
|
174
|
+
174 }
|
|
175
|
+
175 }
|
|
176
|
+
176 }
|
|
177
|
+
177
|
|
178
|
+
178 if (!fieldExists) {
|
|
179
|
+
179 throw new Error(
|
|
180
|
+
180 `Resource '${scopeName}' must have '${filterField}' field or relationship in schema for position filtering`
|
|
181
|
+
181 )
|
|
182
|
+
182 }
|
|
183
|
+
183 })
|
|
184
|
+
184 })
|
|
185
|
+
185
|
|
186
|
+
186 // Add index for position field if autoIndex is enabled
|
|
187
|
+
187 addHook('scope:added', 'add-position-index', { afterFunction: 'validate-position-field' }, async ({ context, vars, scopes }) => {
|
|
188
|
+
188 const { scopeName } = context
|
|
189
|
+
189
|
|
190
|
+
190 if (!shouldHavePositioning(scopeName) || !vars.positioning.autoIndex) {
|
|
191
|
+
191 return
|
|
192
|
+
192 }
|
|
193
|
+
193
|
|
194
|
+
194 const scope = scopes[scopeName]
|
|
195
|
+
195 const schemaInfo = scope.vars.schemaInfo
|
|
196
|
+
196
|
|
197
|
+
197 if (!schemaInfo || !api.knex?.instance) {
|
|
198
|
+
198 return // Skip if no database connection
|
|
199
|
+
199 }
|
|
200
|
+
200
|
|
201
|
+
201 try {
|
|
202
|
+
202 const adapter = resolveStorageAdapter(scopeName)
|
|
203
|
+
203 if (adapter.isCanonical()) {
|
|
204
|
+
204 log.debug(`Skipping positioning index for canonical storage on ${scopeName}`)
|
|
205
|
+
205 return
|
|
206
|
+
206 }
|
|
207
|
+
207
|
|
208
|
+
208 const knex = api.knex.instance
|
|
209
|
+
209 const tableName = adapter.getTableName()
|
|
210
|
+
210
|
|
211
|
+
211 // Check if table exists first
|
|
212
|
+
212 const tableExists = await knex.schema.hasTable(tableName)
|
|
213
|
+
213 if (!tableExists) {
|
|
214
|
+
214 log.debug(`Table ${tableName} doesn't exist yet, skipping index creation`)
|
|
215
|
+
215 return
|
|
216
|
+
216 }
|
|
217
|
+
217
|
|
218
|
+
218 // Build index columns: map filter fields to actual database columns
|
|
219
|
+
219 const indexColumns = new Set()
|
|
220
|
+
220
|
|
221
|
+
221 // Map each filter field to its database column
|
|
222
|
+
222 vars.positioning.filters.forEach(filterField => {
|
|
223
|
+
223 const searchField = schemaInfo.searchSchemaStructure?.[filterField]
|
|
224
|
+
224 if (searchField) {
|
|
225
|
+
225 // Use actualField if it exists (for relationships)
|
|
226
|
+
226 const dbField = searchField.actualField || filterField
|
|
227
|
+
227 indexColumns.add(adapter.translateColumn(dbField))
|
|
228
|
+
228 } else if (schemaInfo.schemaStructure[filterField]) {
|
|
229
|
+
229 // Direct database field
|
|
230
|
+
230 indexColumns.add(adapter.translateColumn(filterField))
|
|
231
|
+
231 }
|
|
232
|
+
232 })
|
|
233
|
+
233
|
|
234
|
+
234 // Add the position field
|
|
235
|
+
235 indexColumns.add(adapter.translateColumn(vars.positioning.field))
|
|
236
|
+
236
|
|
237
|
+
237 const indexColumnList = [...indexColumns].filter(Boolean)
|
|
238
|
+
238
|
|
239
|
+
239 const indexName = `idx_${tableName}_positioning`
|
|
240
|
+
240
|
|
241
|
+
241 // Check if index already exists
|
|
242
|
+
242 const hasIndex = await knex.schema.hasIndex(tableName, indexColumnList, indexName)
|
|
243
|
+
243
|
|
244
|
+
244 if (!hasIndex) {
|
|
245
|
+
245 await knex.schema.table(tableName, table => {
|
|
246
|
+
246 table.index(indexColumnList, indexName)
|
|
247
|
+
247 })
|
|
248
|
+
248
|
|
249
|
+
249 log.info(`Created positioning index on ${tableName}`, { columns: indexColumnList })
|
|
250
|
+
250 }
|
|
251
|
+
251 } catch (error) {
|
|
252
|
+
252 log.warn(`Could not create positioning index for ${scopeName}:`, error.message)
|
|
253
|
+
253 }
|
|
254
|
+
254 })
|
|
255
|
+
255
|
|
256
|
+
256 // Process beforeId parameter in requests
|
|
257
|
+
257 addHook('beforeSchemaValidate', 'process-beforeid', {}, async ({ context, scopeName, vars }) => {
|
|
258
|
+
258 log.debug('*** beforeSchemaValidate positioning hook', {
|
|
259
|
+
259 scopeName,
|
|
260
|
+
260 method: context.method,
|
|
261
|
+
261 shouldHavePositioning: shouldHavePositioning(scopeName)
|
|
262
|
+
262 })
|
|
263
|
+
263
|
|
264
|
+
264 // Skip excluded resources
|
|
265
|
+
265 if (!shouldHavePositioning(scopeName)) {
|
|
266
|
+
266 log.debug('*** Skipping beforeId processing - excluded resource')
|
|
267
|
+
267 return
|
|
268
|
+
268 }
|
|
269
|
+
269
|
|
270
|
+
270 // Skip if no positioning context needed
|
|
271
|
+
271 const method = context.method
|
|
272
|
+
272 if (!['post', 'put', 'patch'].includes(method)) {
|
|
273
|
+
273 log.debug('*** Skipping beforeId processing - wrong method', { method })
|
|
274
|
+
274 return
|
|
275
|
+
275 }
|
|
276
|
+
276
|
|
277
|
+
277 // Check if resource has position field
|
|
278
|
+
278 const scope = scopes[scopeName]
|
|
279
|
+
279 const hasPositionField = scope?.vars?.schemaInfo?.schemaStructure?.[vars.positioning.field]
|
|
280
|
+
280
|
|
281
|
+
281 if (!hasPositionField) {
|
|
282
|
+
282 return
|
|
283
|
+
283 }
|
|
284
|
+
284
|
|
285
|
+
285 // Extract beforeId from JSON:API format attributes
|
|
286
|
+
286 const attributes = context.inputRecord?.data?.attributes ||
|
|
287
|
+
287 (context.simplified && context.inputRecord && typeof context.inputRecord === 'object'
|
|
288
|
+
288 ? context.inputRecord
|
|
289
|
+
289 : {})
|
|
290
|
+
290 const beforeId = attributes[vars.positioning.beforeIdField]
|
|
291
|
+
291
|
|
292
|
+
292 // Store beforeId in context for later use
|
|
293
|
+
293 if (beforeId !== undefined) {
|
|
294
|
+
294 context.positioningBeforeId = beforeId
|
|
295
|
+
295
|
|
296
|
+
296 // Remove beforeId from attributes so it doesn't get stored
|
|
297
|
+
297 delete attributes[vars.positioning.beforeIdField]
|
|
298
|
+
298
|
|
299
|
+
299 if (context.simplified && context.inputRecord && typeof context.inputRecord === 'object') {
|
|
300
|
+
300 delete context.inputRecord[vars.positioning.beforeIdField]
|
|
301
|
+
301 }
|
|
302
|
+
302 }
|
|
303
|
+
303
|
|
304
|
+
304 // Remove any manually provided position field - position is managed by this plugin
|
|
305
|
+
305 if (attributes[vars.positioning.field] !== undefined) {
|
|
306
|
+
306 delete attributes[vars.positioning.field]
|
|
307
|
+
307
|
|
308
|
+
308 log.debug('Positioning beforeId extracted', {
|
|
309
|
+
309 scopeName,
|
|
310
|
+
310 beforeId,
|
|
311
|
+
311 method
|
|
312
|
+
312 })
|
|
313
|
+
313
|
|
314
|
+
314 if (context.simplified && context.inputRecord && typeof context.inputRecord === 'object') {
|
|
315
|
+
315 delete context.inputRecord[vars.positioning.field]
|
|
316
|
+
316 }
|
|
317
|
+
317 }
|
|
318
|
+
318
|
|
319
|
+
319 // For new records without explicit position, mark for positioning
|
|
320
|
+
320 if (method === 'post' && !attributes[vars.positioning.field]) {
|
|
321
|
+
321 context.needsPositioning = true
|
|
322
|
+
322 }
|
|
323
|
+
323 })
|
|
324
|
+
324
|
|
325
|
+
325 // Calculate and set position before create/update
|
|
326
|
+
326 addHook('beforeDataCallPost', 'calculate-position-post', {}, async ({ context, scopeName, vars, helpers }) => {
|
|
327
|
+
327 log.debug('*** beforeDataCallPost hook fired for positioning', { scopeName, method: context.method })
|
|
328
|
+
328 await calculateAndSetPosition(context, scopeName, vars, helpers, log, scopes, api)
|
|
329
|
+
329 })
|
|
330
|
+
330
|
|
331
|
+
331 addHook('beforeDataCallPut', 'calculate-position-put', {}, async ({ context, scopeName, vars, helpers }) => {
|
|
332
|
+
332 log.debug('*** beforeDataCallPut hook fired for positioning', { scopeName, method: context.method })
|
|
333
|
+
333 // Only process if beforeId was provided or it's a new record
|
|
334
|
+
334 if (context.positioningBeforeId !== undefined || context.needsPositioning) {
|
|
335
|
+
335 await calculateAndSetPosition(context, scopeName, vars, helpers, log, scopes, api)
|
|
336
|
+
336 }
|
|
337
|
+
337 })
|
|
338
|
+
338
|
|
339
|
+
339 addHook('beforeDataCallPatch', 'calculate-position-patch', {}, async ({ context, scopeName, vars, helpers }) => {
|
|
340
|
+
340 log.debug('*** beforeDataCallPatch hook fired for positioning', { scopeName, method: context.method })
|
|
341
|
+
341 // Only process if beforeId was provided
|
|
342
|
+
342 if (context.positioningBeforeId !== undefined) {
|
|
343
|
+
343 await calculateAndSetPosition(context, scopeName, vars, helpers, log, scopes, api)
|
|
344
|
+
344 }
|
|
345
|
+
345 })
|
|
346
|
+
346
|
|
347
|
+
347 // Helper function to calculate and set position
|
|
348
|
+
348 async function calculateAndSetPosition (context, scopeName, vars, helpers, log, scopes, api) {
|
|
349
|
+
349 log.debug('*** calculateAndSetPosition called', {
|
|
350
|
+
350 scopeName,
|
|
351
|
+
351 method: context.method,
|
|
352
|
+
352 shouldHavePositioning: shouldHavePositioning(scopeName),
|
|
353
|
+
353 beforeId: context.positioningBeforeId,
|
|
354
|
+
354 needsPositioning: context.needsPositioning
|
|
355
|
+
355 })
|
|
356
|
+
356
|
|
357
|
+
357 // Skip if no positioning needed
|
|
358
|
+
358 if (!shouldHavePositioning(scopeName)) {
|
|
359
|
+
359 log.debug('*** Skipping - resource excluded from positioning')
|
|
360
|
+
360 return
|
|
361
|
+
361 }
|
|
362
|
+
362
|
|
363
|
+
363 const beforeId = context.positioningBeforeId
|
|
364
|
+
364 const needsPositioning = context.needsPositioning
|
|
365
|
+
365
|
|
366
|
+
366 // Skip if no positioning action needed
|
|
367
|
+
367 if (beforeId === undefined && !needsPositioning) {
|
|
368
|
+
368 log.debug('*** Skipping - no positioning needed', { beforeId, needsPositioning })
|
|
369
|
+
369 return
|
|
370
|
+
370 }
|
|
371
|
+
371
|
|
372
|
+
372 const recordData = (() => {
|
|
373
|
+
373 if (context.inputRecord?.data?.attributes) {
|
|
374
|
+
374 return { ...context.inputRecord.data.attributes }
|
|
375
|
+
375 }
|
|
376
|
+
376 if (context.simplified && context.inputRecord && typeof context.inputRecord === 'object') {
|
|
377
|
+
377 return { ...context.inputRecord }
|
|
378
|
+
378 }
|
|
379
|
+
379 return {}
|
|
380
|
+
380 })()
|
|
381
|
+
381
|
|
382
|
+
382 log.debug('*** Record data for positioning', {
|
|
383
|
+
383 recordData,
|
|
384
|
+
384 simplified: context.simplified,
|
|
385
|
+
385 inputRecord: context.inputRecord
|
|
386
|
+
386 })
|
|
387
|
+
387
|
|
388
|
+
388 const jsonApiRecord = context.inputRecord?.data || {}
|
|
389
|
+
389 const inputRelationships = jsonApiRecord.relationships || {}
|
|
390
|
+
390 const simplifiedRecord = context.simplified && context.inputRecord && typeof context.inputRecord === 'object'
|
|
391
|
+
391 ? context.inputRecord
|
|
392
|
+
392 : {}
|
|
393
|
+
393
|
|
394
|
+
394 const minimalAttributes = context.minimalRecord?.attributes || {}
|
|
395
|
+
395 const minimalRelationships = context.minimalRecord?.relationships || {}
|
|
396
|
+
396
|
|
397
|
+
397 const combinedAttributes = {
|
|
398
|
+
398 ...minimalAttributes,
|
|
399
|
+
399 ...recordData,
|
|
400
|
+
400 }
|
|
401
|
+
401
|
|
402
|
+
402 // Build filter conditions for the position group
|
|
403
|
+
403 const schemaInfo = context.schemaInfo
|
|
404
|
+
404 const adapter = context.storageAdapter || resolveStorageAdapter(scopeName)
|
|
405
|
+
405
|
|
406
|
+
406 if (!adapter) {
|
|
407
|
+
407 log.warn('*** Positioning skipped - no storage adapter available', { scopeName })
|
|
408
|
+
408 return
|
|
409
|
+
409 }
|
|
410
|
+
410
|
|
411
|
+
411 const filterConditions = buildPositionFilterConditions(adapter, schemaInfo, {
|
|
412
|
+
412 attributes: combinedAttributes,
|
|
413
|
+
413 rawRecord: recordData,
|
|
414
|
+
414 simplifiedRecord,
|
|
415
|
+
415 inputRelationships,
|
|
416
|
+
416 minimalRelationships,
|
|
417
|
+
417 })
|
|
418
|
+
418
|
|
419
|
+
419 log.debug('*** Position filter conditions', { scopeName, filterConditions })
|
|
420
|
+
420
|
|
421
|
+
421 // Query existing items in the same position group
|
|
422
|
+
422 const knex = api.knex.instance
|
|
423
|
+
423 const tableName = adapter.getTableName()
|
|
424
|
+
424 const idProperty = schemaInfo.idProperty
|
|
425
|
+
425 const idColumn = adapter.getIdColumn()
|
|
426
|
+
426 const positionColumn = adapter.translateColumn(vars.positioning.field)
|
|
427
|
+
427
|
|
428
|
+
428 log.debug('*** Building position query', {
|
|
429
|
+
429 tableName,
|
|
430
|
+
430 idProperty,
|
|
431
|
+
431 idColumn,
|
|
432
|
+
432 positionField: vars.positioning.field,
|
|
433
|
+
433 positionColumn,
|
|
434
|
+
434 filterConditions
|
|
435
|
+
435 })
|
|
436
|
+
436
|
|
437
|
+
437 // Check if we have a transaction to use
|
|
438
|
+
438 // Build base query with filter conditions
|
|
439
|
+
439 const baseQuery = adapter.buildBaseQuery({ transaction: context.transaction })
|
|
440
|
+
440 Object.entries(filterConditions).forEach(([column, value]) => {
|
|
441
|
+
441 if (value === null) {
|
|
442
|
+
442 baseQuery.whereNull(column)
|
|
443
|
+
443 } else {
|
|
444
|
+
444 baseQuery.where(column, value)
|
|
445
|
+
445 }
|
|
446
|
+
446 })
|
|
447
|
+
447
|
|
448
|
+
448 // For updates, exclude the current record
|
|
449
|
+
449 if ((context.method === 'patch' || context.method === 'put') && context.id) {
|
|
450
|
+
450 const translatedId = adapter.translateFilterValue(idProperty, context.id)
|
|
451
|
+
451 baseQuery.whereNot(idColumn, translatedId)
|
|
452
|
+
452 }
|
|
453
|
+
453
|
|
454
|
+
454 // Calculate effective beforeId first
|
|
455
|
+
455 let effectiveBeforeId = beforeId
|
|
456
|
+
456
|
|
457
|
+
457 // Handle default positioning when no beforeId is provided
|
|
458
|
+
458 if (beforeId === undefined) {
|
|
459
|
+
459 if (vars.positioning.defaultPosition === 'last') {
|
|
460
|
+
460 effectiveBeforeId = null // null means position at end
|
|
461
|
+
461 } else if (vars.positioning.defaultPosition === 'first') {
|
|
462
|
+
462 effectiveBeforeId = 'FIRST' // Special marker for first position
|
|
463
|
+
463 }
|
|
464
|
+
464 }
|
|
465
|
+
465
|
|
466
|
+
466 let items = []
|
|
467
|
+
467
|
|
468
|
+
468 const selectColumns = {
|
|
469
|
+
469 [idProperty]: idColumn,
|
|
470
|
+
470 [vars.positioning.field]: positionColumn,
|
|
471
|
+
471 }
|
|
472
|
+
472 const selectWithAliases = (builder) => adapter.selectColumns(builder, selectColumns)
|
|
473
|
+
473
|
|
474
|
+
474 try {
|
|
475
|
+
475 log.debug('*** Positioning query start', {
|
|
476
|
+
476 effectiveBeforeId,
|
|
477
|
+
477 selectColumns,
|
|
478
|
+
478 })
|
|
479
|
+
479 if (effectiveBeforeId === null) {
|
|
480
|
+
480 // Positioning at end - only get the last item
|
|
481
|
+
481 const lastItem = await selectWithAliases(baseQuery
|
|
482
|
+
482 .clone())
|
|
483
|
+
483 .orderBy(positionColumn, 'desc')
|
|
484
|
+
484 .first()
|
|
485
|
+
485
|
|
486
|
+
486 if (lastItem) {
|
|
487
|
+
487 items = [lastItem]
|
|
488
|
+
488 }
|
|
489
|
+
489 } else if (effectiveBeforeId === 'FIRST') {
|
|
490
|
+
490 // Positioning at beginning - get the first item
|
|
491
|
+
491 const firstItem = await selectWithAliases(baseQuery
|
|
492
|
+
492 .clone())
|
|
493
|
+
493 .orderBy(positionColumn, 'asc')
|
|
494
|
+
494 .first()
|
|
495
|
+
495
|
|
496
|
+
496 if (firstItem) {
|
|
497
|
+
497 items = [firstItem]
|
|
498
|
+
498 effectiveBeforeId = firstItem[idProperty]
|
|
499
|
+
499 } else {
|
|
500
|
+
500 effectiveBeforeId = null
|
|
501
|
+
501 }
|
|
502
|
+
502 } else if (effectiveBeforeId) {
|
|
503
|
+
503 // Positioning before a specific item - get that item and the one before it
|
|
504
|
+
504 const normalizedTargetId = adapter.translateFilterValue(idProperty, effectiveBeforeId)
|
|
505
|
+
505 const targetItem = await selectWithAliases(baseQuery
|
|
506
|
+
506 .clone())
|
|
507
|
+
507 .where(idColumn, normalizedTargetId)
|
|
508
|
+
508 .first()
|
|
509
|
+
509
|
|
510
|
+
510 if (targetItem) {
|
|
511
|
+
511 // Get the item immediately before the target
|
|
512
|
+
512 const prevItem = await selectWithAliases(baseQuery
|
|
513
|
+
513 .clone())
|
|
514
|
+
514 .where(positionColumn, '<', targetItem[vars.positioning.field])
|
|
515
|
+
515 .orderBy(positionColumn, 'desc')
|
|
516
|
+
516 .first()
|
|
517
|
+
517
|
|
518
|
+
518 items = prevItem ? [prevItem, targetItem] : [targetItem]
|
|
519
|
+
519 }
|
|
520
|
+
520 } else {
|
|
521
|
+
521 // First item in group - no items needed
|
|
522
|
+
522 items = []
|
|
523
|
+
523 }
|
|
524
|
+
524
|
|
525
|
+
525 log.debug('*** Query completed', { itemCount: items.length, items })
|
|
526
|
+
526 } catch (error) {
|
|
527
|
+
527 log.error('*** Query failed', { error: error.message, stack: error.stack })
|
|
528
|
+
528 throw error
|
|
529
|
+
529 }
|
|
530
|
+
530
|
|
531
|
+
531 // Calculate new position
|
|
532
|
+
532 const newPosition = items.length === 0
|
|
533
|
+
533 ? getInitialPosition()
|
|
534
|
+
534 : calculatePosition(items, effectiveBeforeId, idProperty, vars.positioning.field)
|
|
535
|
+
535
|
|
536
|
+
536 // Set the position on the incoming payload so downstream steps persist it
|
|
537
|
+
537 if (context.inputRecord?.data) {
|
|
538
|
+
538 context.inputRecord.data.attributes = context.inputRecord.data.attributes || {}
|
|
539
|
+
539 context.inputRecord.data.attributes[vars.positioning.field] = newPosition
|
|
540
|
+
540 }
|
|
541
|
+
541
|
|
542
|
+
542 if (context.simplified && context.inputRecord && typeof context.inputRecord === 'object') {
|
|
543
|
+
543 context.inputRecord[vars.positioning.field] = newPosition
|
|
544
|
+
544 }
|
|
545
|
+
545
|
|
546
|
+
546 log.debug('Position calculated', {
|
|
547
|
+
547 scopeName,
|
|
548
|
+
548 newPosition,
|
|
549
|
+
549 beforeId: effectiveBeforeId,
|
|
550
|
+
550 filterConditions,
|
|
551
|
+
551 itemCount: items.length,
|
|
552
|
+
552 method: context.method,
|
|
553
|
+
553 id: context.id
|
|
554
|
+
554 })
|
|
555
|
+
555 }
|
|
556
|
+
556
|
|
557
|
+
557 // Add positioning info back to response
|
|
558
|
+
558 addHook('afterGet', 'add-beforeid-to-response', {}, async ({ context, vars }) => {
|
|
559
|
+
559 // Only add if beforeId was in the original request
|
|
560
|
+
560 if (context.positioningBeforeId !== undefined && context.record) {
|
|
561
|
+
561 if (context.simplified) {
|
|
562
|
+
562 context.record[vars.positioning.beforeIdField] = context.positioningBeforeId
|
|
563
|
+
563 } else {
|
|
564
|
+
564 // For JSON:API, add to meta
|
|
565
|
+
565 context.meta = context.meta || {}
|
|
566
|
+
566 context.meta.positioning = {
|
|
567
|
+
567 [vars.positioning.beforeIdField]: context.positioningBeforeId
|
|
568
|
+
568 }
|
|
569
|
+
569 }
|
|
570
|
+
570 }
|
|
571
|
+
571 })
|
|
572
|
+
572
|
|
573
|
+
573 // Apply default sort by position if no other sort specified
|
|
574
|
+
574 addHook('beforeQuery', 'apply-position-sort', {}, async ({ context, scopeName, vars }) => {
|
|
575
|
+
575 if (!shouldHavePositioning(scopeName)) {
|
|
576
|
+
576 return
|
|
577
|
+
577 }
|
|
578
|
+
578
|
|
579
|
+
579 // Check if sort is already specified
|
|
580
|
+
580 const hasSort = context.queryParams?.sort && context.queryParams.sort.length > 0
|
|
581
|
+
581
|
|
582
|
+
582 if (!hasSort) {
|
|
583
|
+
583 // Apply position sort
|
|
584
|
+
584 context.queryParams = context.queryParams || {}
|
|
585
|
+
585 context.queryParams.sort = [vars.positioning.field]
|
|
586
|
+
586
|
|
587
|
+
587 log.trace('Applied default position sort', { scopeName })
|
|
588
|
+
588 }
|
|
589
|
+
589 })
|
|
590
|
+
590
|
|
591
|
+
591 // API methods for positioning operations
|
|
592
|
+
592 api.positioning = {
|
|
593
|
+
593 /**
|
|
594
|
+
594 * Reorder items by providing new positions
|
|
595
|
+
595 * @param {string} scopeName - Resource scope name
|
|
596
|
+
596 * @param {Array} positions - Array of {id, position} or {id, beforeId}
|
|
597
|
+
597 * @param {Object} filters - Filter conditions for position group
|
|
598
|
+
598 */
|
|
599
|
+
599 async reorder (scopeName, positions, filters = {}) {
|
|
600
|
+
600 const scope = scopes[scopeName]
|
|
601
|
+
601 if (!scope) {
|
|
602
|
+
602 throw new Error(`Unknown resource: ${scopeName}`)
|
|
603
|
+
603 }
|
|
604
|
+
604
|
|
605
|
+
605 // Implementation would go here for bulk reordering
|
|
606
|
+
606 log.info('Bulk reorder requested', { scopeName, count: positions.length })
|
|
607
|
+
607 },
|
|
608
|
+
608
|
|
609
|
+
609 /**
|
|
610
|
+
610 * Get positioning configuration
|
|
611
|
+
611 */
|
|
612
|
+
612 getConfig () {
|
|
613
|
+
613 return { ...vars.positioning }
|
|
614
|
+
614 },
|
|
615
|
+
615
|
|
616
|
+
616 /**
|
|
617
|
+
617 * Check if a resource has positioning enabled
|
|
618
|
+
618 */
|
|
619
|
+
619 isEnabled (scopeName) {
|
|
620
|
+
620 return shouldHavePositioning(scopeName)
|
|
621
|
+
621 }
|
|
622
|
+
622 }
|
|
623
|
+
623
|
|
624
|
+
624 log.info('Positioning plugin installed', {
|
|
625
|
+
625 field: vars.positioning.field,
|
|
626
|
+
626 strategy: vars.positioning.strategy,
|
|
627
|
+
627 filters: vars.positioning.filters,
|
|
628
|
+
628 excludedResources: vars.positioning.excludeResources
|
|
629
|
+
629 })
|
|
630
|
+
630 }
|
|
631
|
+
631 }
|
|
632
|
+
632
|
|
633
|
+
633 /*
|
|
634
|
+
634 Usage examples:
|
|
635
|
+
635
|
|
636
|
+
636 // Basic usage - positioning for all resources
|
|
637
|
+
637 await api.use(PositioningPlugin);
|
|
638
|
+
638
|
|
639
|
+
639 // With configuration
|
|
640
|
+
640 await api.use(PositioningPlugin, {
|
|
641
|
+
641 field: 'sortOrder',
|
|
642
|
+
642 filters: ['status', 'projectId'],
|
|
643
|
+
643 excludeResources: ['users', 'logs']
|
|
644
|
+
644 });
|
|
645
|
+
645
|
|
646
|
+
646 // Position grouping example
|
|
647
|
+
647 await api.use(PositioningPlugin, {
|
|
648
|
+
648 filters: ['boardId', 'listId'], // Separate positions per board/list combo
|
|
649
|
+
649 defaultPosition: 'last'
|
|
650
|
+
650 });
|
|
651
|
+
651
|
|
652
|
+
652 // In requests:
|
|
653
|
+
653 // POST /api/tasks
|
|
654
|
+
654 {
|
|
655
|
+
655 "title": "New Task",
|
|
656
|
+
656 "boardId": 123,
|
|
657
|
+
657 "listId": 456,
|
|
658
|
+
658 "beforeId": "task-789" // Position before this task
|
|
659
|
+
659 }
|
|
660
|
+
660
|
|
661
|
+
661 // Or to add at end:
|
|
662
|
+
662 {
|
|
663
|
+
663 "title": "New Task",
|
|
664
|
+
664 "boardId": 123,
|
|
665
|
+
665 "listId": 456,
|
|
666
|
+
666 "beforeId": null // Explicit last position
|
|
667
|
+
667 }
|
|
668
|
+
668 */
|