json-rest-api 1.0.14 → 1.0.16
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/connectors_ideas/fastify.js +66 -68
- package/connectors_ideas/koa-plugin.js +50 -51
- package/eslint.config.js +11 -2
- package/examples/url-override-example.js +63 -63
- package/index.js +13 -13
- package/lib/enhanced-logger.js +51 -51
- package/lib/error-formatter.js +61 -61
- package/lib/rest-api-errors.js +28 -28
- package/package.json +1 -1
- package/plugins/core/bulk-operations-plugin.js +142 -145
- package/plugins/core/connectors/express-plugin.js +210 -217
- package/plugins/core/connectors/lib/busboy-detector.js +84 -84
- package/plugins/core/connectors/lib/formidable-detector.js +50 -50
- package/plugins/core/connectors/lib/request-helpers.js +74 -74
- package/plugins/core/file-handling-plugin.js +125 -125
- package/plugins/core/lib/anyapi/anyapi-registry.js +254 -254
- package/plugins/core/lib/anyapi/query/anyapi-query-adapter.js +287 -287
- package/plugins/core/lib/anyapi/schema-utils.js +101 -101
- package/plugins/core/lib/anyapi/utils/descriptor-helpers.js +41 -41
- package/plugins/core/lib/dbTablesOperations.js +134 -135
- package/plugins/core/lib/fractional-positioning.js +74 -76
- package/plugins/core/lib/querying/knex-cross-table-search.js +320 -320
- package/plugins/core/lib/querying/knex-json-api-transformers-querying.js +144 -144
- package/plugins/core/lib/querying/knex-pagination-helpers.js +164 -164
- package/plugins/core/lib/querying/knex-process-includes.js +50 -50
- package/plugins/core/lib/querying/knex-query-helpers-base.js +31 -31
- package/plugins/core/lib/querying/knex-query-helpers.js +451 -452
- package/plugins/core/lib/querying/knex-relationship-includes.js +1160 -1170
- package/plugins/core/lib/querying/knex-window-queries.js +89 -89
- package/plugins/core/lib/querying/url-helpers.js +35 -35
- package/plugins/core/lib/querying-writing/compile-schemas.js +99 -99
- package/plugins/core/lib/querying-writing/connectors-query-parser.js +31 -31
- package/plugins/core/lib/querying-writing/database-capabilities.js +94 -94
- package/plugins/core/lib/querying-writing/database-value-normalizers.js +76 -76
- package/plugins/core/lib/querying-writing/default-data-helpers.js +27 -27
- package/plugins/core/lib/querying-writing/field-utils.js +44 -42
- package/plugins/core/lib/querying-writing/knex-constants.js +15 -16
- package/plugins/core/lib/querying-writing/knex-field-helpers.js +117 -115
- package/plugins/core/lib/querying-writing/knex-json-api-transformers.js +41 -41
- package/plugins/core/lib/querying-writing/payload-validators.js +275 -275
- package/plugins/core/lib/querying-writing/schema-helpers.js +129 -129
- package/plugins/core/lib/querying-writing/scope-validations.js +143 -146
- package/plugins/core/lib/querying-writing/simplified-helpers.js +133 -129
- package/plugins/core/lib/storage/storage-adapter.js +68 -68
- package/plugins/core/lib/writing/knex-json-api-transformers-writing.js +31 -31
- package/plugins/core/lib/writing/many-to-many-manipulations.js +81 -81
- package/plugins/core/lib/writing/relationship-processor.js +58 -58
- package/plugins/core/rest-api-access.js +11 -15
- package/plugins/core/rest-api-anyapi-knex-plugin.js +920 -920
- package/plugins/core/rest-api-cors-plugin.js +170 -171
- package/plugins/core/rest-api-knex-plugin.js +482 -493
- package/plugins/core/rest-api-multihome-plugin.js +247 -251
- package/plugins/core/rest-api-plugin-hooks/compile-resource-schemas.js +5 -5
- package/plugins/core/rest-api-plugin-hooks/register-relationship-routes.js +20 -21
- package/plugins/core/rest-api-plugin-hooks/register-scope-routes.js +44 -44
- package/plugins/core/rest-api-plugin-hooks/turn-scope-init-into-vars.js +23 -23
- package/plugins/core/rest-api-plugin-hooks/validate-include-configurations.js +15 -15
- package/plugins/core/rest-api-plugin-methods/add-route.js +9 -9
- package/plugins/core/rest-api-plugin-methods/check-permissions.js +5 -6
- package/plugins/core/rest-api-plugin-methods/common.js +295 -297
- package/plugins/core/rest-api-plugin-methods/delete-relationship.js +35 -37
- package/plugins/core/rest-api-plugin-methods/delete.js +57 -59
- package/plugins/core/rest-api-plugin-methods/enrich-attributes.js +172 -171
- package/plugins/core/rest-api-plugin-methods/get-related.js +96 -96
- package/plugins/core/rest-api-plugin-methods/get-relationship.js +16 -18
- package/plugins/core/rest-api-plugin-methods/get.js +86 -86
- package/plugins/core/rest-api-plugin-methods/patch-relationship.js +16 -17
- package/plugins/core/rest-api-plugin-methods/patch.js +63 -64
- package/plugins/core/rest-api-plugin-methods/post-relationship.js +54 -55
- package/plugins/core/rest-api-plugin-methods/post.js +61 -62
- package/plugins/core/rest-api-plugin-methods/put.js +86 -87
- package/plugins/core/rest-api-plugin-methods/query.js +88 -88
- package/plugins/core/rest-api-plugin-methods/release.js +5 -6
- package/plugins/core/rest-api-plugin.js +76 -78
- package/plugins/core/rest-api-positioning-plugin.js +251 -252
- package/plugins/core/socketio-plugin.js +240 -236
- package/plugins/storage/local-storage.js +103 -103
- package/plugins/storage/s3-storage.js +41 -42
- package/quickTest.js +37 -37
- package/scripts/debug-anyapi-review-filter.mjs +18 -18
- package/scripts/inspect-anyapi-descriptor-country.mjs +10 -10
- package/scripts/inspect-anyapi-descriptor.mjs +10 -10
- package/scripts/run-docs.js +112 -113
- package/test-debug.js +28 -28
- package/tests/TEST_TEMPLATE.test.js +51 -51
- package/tests/anyapi-basic.test.js +42 -42
- package/tests/anyapi-cursor-pagination.test.js +48 -48
- package/tests/bulk-operations.test.js +208 -208
- package/tests/computed-fields.test.js +204 -204
- package/tests/cors.test.js +412 -413
- package/tests/curl.test.js +275 -276
- package/tests/custom-idproperty-relationships.test.js +266 -266
- package/tests/field-getters.test.js +132 -132
- package/tests/field-setters.test.js +110 -110
- package/tests/fixtures/api-configs.js +699 -699
- package/tests/helpers/storage-mode.js +32 -32
- package/tests/helpers/test-utils.js +76 -76
- package/tests/include-depth-validation.test.js +76 -76
- package/tests/includes.test.js +145 -145
- package/tests/multihome.test.js +175 -175
- package/tests/nested-includes.test.js +255 -255
- package/tests/pagination-cursor-multifield.test.js +164 -164
- package/tests/pagination-enhanced.test.js +225 -225
- package/tests/polymorphic-relationships.test.js +96 -96
- package/tests/polymorphic-simple.test.js +44 -44
- package/tests/positioning-advanced.test.js +226 -226
- package/tests/positioning-extended.test.js +399 -399
- package/tests/positioning-multifilter.test.js +204 -204
- package/tests/positioning.test.js +266 -266
- package/tests/queries.test.js +184 -184
- package/tests/query-limits.test.js +154 -155
- package/tests/relationship-endpoints.test.js +127 -127
- package/tests/relationships.test.js +178 -179
- package/tests/rest-api-access.test.js +86 -86
- package/tests/rest-api.test.js +103 -103
- package/tests/searchschema-merge.test.js +93 -93
- package/tests/socketio.test.js +326 -326
- package/tests/virtual-fields.test.js +164 -164
|
@@ -3,30 +3,29 @@ import {
|
|
|
3
3
|
getInitialPosition,
|
|
4
4
|
isValidPosition,
|
|
5
5
|
assignInitialPositions,
|
|
6
|
-
} from './lib/fractional-positioning.js'
|
|
7
|
-
import { createStorageAdapter } from './lib/storage/storage-adapter.js'
|
|
6
|
+
} from './lib/fractional-positioning.js'
|
|
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
|
-
install({ api, addHook, vars, helpers, log, scopes, pluginOptions }) {
|
|
14
|
-
|
|
15
|
-
const
|
|
16
|
-
const
|
|
17
|
-
const canonicalStorageInstalled = installedPlugins.includes('rest-api-anyapi-knex');
|
|
13
|
+
install ({ api, addHook, vars, helpers, log, scopes, pluginOptions }) {
|
|
14
|
+
const installedPlugins = Array.from(api._installedPlugins || [])
|
|
15
|
+
const legacyStorageInstalled = installedPlugins.includes('rest-api-knex')
|
|
16
|
+
const canonicalStorageInstalled = installedPlugins.includes('rest-api-anyapi-knex')
|
|
18
17
|
if (!legacyStorageInstalled && !canonicalStorageInstalled) {
|
|
19
18
|
throw new Error(
|
|
20
19
|
"Positioning plugin requires either 'rest-api-knex' or 'rest-api-anyapi-knex' to be installed before it."
|
|
21
|
-
)
|
|
20
|
+
)
|
|
22
21
|
}
|
|
23
22
|
|
|
24
23
|
if (!api.knex?.instance) {
|
|
25
|
-
throw new Error('Positioning plugin requires a storage plugin with knex support (rest-api-knex or rest-api-anyapi-knex)')
|
|
24
|
+
throw new Error('Positioning plugin requires a storage plugin with knex support (rest-api-knex or rest-api-anyapi-knex)')
|
|
26
25
|
}
|
|
27
26
|
|
|
28
27
|
// Get configuration - hooked-api namespaces options by plugin name
|
|
29
|
-
const positioningOptions = pluginOptions || {}
|
|
28
|
+
const positioningOptions = pluginOptions || {}
|
|
30
29
|
|
|
31
30
|
// Store configuration in vars (data only) - inspired by multihome pattern
|
|
32
31
|
vars.positioning = {
|
|
@@ -38,394 +37,394 @@ export const PositioningPlugin = {
|
|
|
38
37
|
defaultPosition: positioningOptions.defaultPosition || 'last',
|
|
39
38
|
autoIndex: positioningOptions.autoIndex !== undefined ? positioningOptions.autoIndex : true,
|
|
40
39
|
rebalanceThreshold: positioningOptions.rebalanceThreshold || 50 // Max position string length
|
|
41
|
-
}
|
|
40
|
+
}
|
|
42
41
|
|
|
43
42
|
const resolveStorageAdapter = (scopeName) => {
|
|
44
|
-
if (!scopeName) return null
|
|
45
|
-
const scope = scopes[scopeName] || api.resources?.[scopeName]
|
|
46
|
-
const scopeVars = scope?.vars
|
|
47
|
-
if (!scopeVars) return null
|
|
43
|
+
if (!scopeName) return null
|
|
44
|
+
const scope = scopes[scopeName] || api.resources?.[scopeName]
|
|
45
|
+
const scopeVars = scope?.vars
|
|
46
|
+
if (!scopeVars) return null
|
|
48
47
|
|
|
49
48
|
if (scopeVars.storageAdapter) {
|
|
50
|
-
return scopeVars.storageAdapter
|
|
49
|
+
return scopeVars.storageAdapter
|
|
51
50
|
}
|
|
52
51
|
|
|
53
52
|
if (scopeVars.schemaInfo && api.knex?.instance) {
|
|
54
|
-
const adapter = createStorageAdapter({ knex: api.knex.instance, schemaInfo: scopeVars.schemaInfo })
|
|
55
|
-
scopeVars.storageAdapter = adapter
|
|
56
|
-
return adapter
|
|
53
|
+
const adapter = createStorageAdapter({ knex: api.knex.instance, schemaInfo: scopeVars.schemaInfo })
|
|
54
|
+
scopeVars.storageAdapter = adapter
|
|
55
|
+
return adapter
|
|
57
56
|
}
|
|
58
57
|
|
|
59
|
-
return null
|
|
60
|
-
}
|
|
58
|
+
return null
|
|
59
|
+
}
|
|
61
60
|
|
|
62
61
|
// Validate configuration
|
|
63
62
|
if (!['fractional', 'integer'].includes(vars.positioning.strategy)) {
|
|
64
|
-
throw new Error(`Invalid positioning strategy: ${vars.positioning.strategy}. Must be 'fractional' or 'integer'`)
|
|
63
|
+
throw new Error(`Invalid positioning strategy: ${vars.positioning.strategy}. Must be 'fractional' or 'integer'`)
|
|
65
64
|
}
|
|
66
65
|
|
|
67
66
|
if (vars.positioning.strategy === 'integer') {
|
|
68
|
-
throw new Error("Integer positioning strategy not yet implemented. Please use 'fractional'")
|
|
67
|
+
throw new Error("Integer positioning strategy not yet implemented. Please use 'fractional'")
|
|
69
68
|
}
|
|
70
69
|
|
|
71
70
|
// Helper to check if a resource should have positioning
|
|
72
|
-
function shouldHavePositioning(scopeName) {
|
|
73
|
-
return !vars.positioning.excludeResources.includes(scopeName)
|
|
71
|
+
function shouldHavePositioning (scopeName) {
|
|
72
|
+
return !vars.positioning.excludeResources.includes(scopeName)
|
|
74
73
|
}
|
|
75
74
|
|
|
76
75
|
// Helper to build filter conditions for position groups
|
|
77
|
-
function buildPositionFilterConditions(adapter, schemaInfo, {
|
|
76
|
+
function buildPositionFilterConditions (adapter, schemaInfo, {
|
|
78
77
|
attributes = {},
|
|
79
78
|
rawRecord = {},
|
|
80
79
|
simplifiedRecord = {},
|
|
81
80
|
inputRelationships = {},
|
|
82
81
|
minimalRelationships = {},
|
|
83
82
|
}) {
|
|
84
|
-
const conditions = {}
|
|
83
|
+
const conditions = {}
|
|
85
84
|
|
|
86
85
|
log.debug('*** buildPositionFilterConditions', {
|
|
87
86
|
filters: vars.positioning.filters,
|
|
88
87
|
attributeKeys: Object.keys(attributes || {}),
|
|
89
88
|
rawKeys: Object.keys(rawRecord || {}),
|
|
90
89
|
simplifiedKeys: Object.keys(simplifiedRecord || {}),
|
|
91
|
-
})
|
|
90
|
+
})
|
|
92
91
|
|
|
93
|
-
const tryAssign = (holder, field) => (holder && field in holder ? holder[field] : undefined)
|
|
92
|
+
const tryAssign = (holder, field) => (holder && field in holder ? holder[field] : undefined)
|
|
94
93
|
|
|
95
94
|
vars.positioning.filters.forEach((filterField) => {
|
|
96
|
-
const searchField = schemaInfo.searchSchemaStructure?.[filterField] || null
|
|
97
|
-
const schemaField = schemaInfo.schemaStructure?.[filterField] || null
|
|
95
|
+
const searchField = schemaInfo.searchSchemaStructure?.[filterField] || null
|
|
96
|
+
const schemaField = schemaInfo.schemaStructure?.[filterField] || null
|
|
98
97
|
const isRelationship = Boolean(
|
|
99
|
-
searchField?.isRelationship
|
|
100
|
-
|
|
101
|
-
|
|
102
|
-
)
|
|
98
|
+
searchField?.isRelationship ||
|
|
99
|
+
schemaField?.belongsTo ||
|
|
100
|
+
schemaField?.belongsToPolymorphic
|
|
101
|
+
)
|
|
103
102
|
|
|
104
103
|
if (!searchField && !schemaField) {
|
|
105
|
-
log.warn(`Filter field '${filterField}' not found in search schema or resource schema for positioning`)
|
|
104
|
+
log.warn(`Filter field '${filterField}' not found in search schema or resource schema for positioning`)
|
|
106
105
|
}
|
|
107
106
|
|
|
108
|
-
const dbField = searchField?.actualField || filterField
|
|
109
|
-
let value
|
|
107
|
+
const dbField = searchField?.actualField || filterField
|
|
108
|
+
let value
|
|
110
109
|
|
|
111
110
|
const attempt = (candidate) => {
|
|
112
111
|
if (value === undefined && candidate !== undefined) {
|
|
113
|
-
value = candidate
|
|
112
|
+
value = candidate
|
|
114
113
|
}
|
|
115
|
-
}
|
|
114
|
+
}
|
|
116
115
|
|
|
117
|
-
attempt(tryAssign(rawRecord, filterField))
|
|
118
|
-
attempt(tryAssign(simplifiedRecord, filterField))
|
|
119
|
-
attempt(tryAssign(rawRecord, dbField))
|
|
120
|
-
attempt(tryAssign(attributes, filterField))
|
|
121
|
-
attempt(tryAssign(attributes, dbField))
|
|
116
|
+
attempt(tryAssign(rawRecord, filterField))
|
|
117
|
+
attempt(tryAssign(simplifiedRecord, filterField))
|
|
118
|
+
attempt(tryAssign(rawRecord, dbField))
|
|
119
|
+
attempt(tryAssign(attributes, filterField))
|
|
120
|
+
attempt(tryAssign(attributes, dbField))
|
|
122
121
|
|
|
123
122
|
if (isRelationship) {
|
|
124
|
-
attempt(inputRelationships?.[filterField]?.data?.id)
|
|
125
|
-
attempt(minimalRelationships?.[filterField]?.data?.id)
|
|
123
|
+
attempt(inputRelationships?.[filterField]?.data?.id)
|
|
124
|
+
attempt(minimalRelationships?.[filterField]?.data?.id)
|
|
126
125
|
// Some contexts may expose relationship data under attributes
|
|
127
|
-
attempt(tryAssign(rawRecord?.relationships, filterField)?.data?.id)
|
|
126
|
+
attempt(tryAssign(rawRecord?.relationships, filterField)?.data?.id)
|
|
128
127
|
}
|
|
129
128
|
|
|
130
|
-
|
|
131
|
-
|
|
129
|
+
if (value !== undefined) {
|
|
130
|
+
const column = adapter.translateColumn(dbField)
|
|
132
131
|
const normalizedValue = value === null
|
|
133
132
|
? null
|
|
134
|
-
: adapter.translateFilterValue(filterField, value)
|
|
135
|
-
conditions[column] = normalizedValue
|
|
133
|
+
: adapter.translateFilterValue(filterField, value)
|
|
134
|
+
conditions[column] = normalizedValue
|
|
136
135
|
}
|
|
137
|
-
})
|
|
136
|
+
})
|
|
138
137
|
|
|
139
|
-
log.debug('*** Filter conditions built', { conditions })
|
|
140
|
-
return conditions
|
|
138
|
+
log.debug('*** Filter conditions built', { conditions })
|
|
139
|
+
return conditions
|
|
141
140
|
}
|
|
142
141
|
|
|
143
142
|
// Validate that resources have position field when added
|
|
144
143
|
addHook('scope:added', 'validate-position-field', {}, ({ context, vars }) => {
|
|
145
|
-
const { scopeName, scopeOptions } = context
|
|
144
|
+
const { scopeName, scopeOptions } = context
|
|
146
145
|
|
|
147
146
|
// Skip excluded resources
|
|
148
147
|
if (!shouldHavePositioning(scopeName)) {
|
|
149
|
-
log.debug(`Resource ${scopeName} excluded from positioning`)
|
|
150
|
-
return
|
|
148
|
+
log.debug(`Resource ${scopeName} excluded from positioning`)
|
|
149
|
+
return
|
|
151
150
|
}
|
|
152
151
|
|
|
153
152
|
// Check if schema has position field
|
|
154
|
-
const schema = scopeOptions.schema
|
|
153
|
+
const schema = scopeOptions.schema
|
|
155
154
|
if (schema && !schema[vars.positioning.field]) {
|
|
156
155
|
throw new Error(
|
|
157
156
|
`Resource '${scopeName}' must have '${vars.positioning.field}' field in schema to use positioning plugin`
|
|
158
|
-
)
|
|
157
|
+
)
|
|
159
158
|
}
|
|
160
159
|
|
|
161
160
|
// Validate filter fields exist (check both relationship names and actual field names)
|
|
162
161
|
vars.positioning.filters.forEach(filterField => {
|
|
163
162
|
// Check if it's a relationship field by looking for a field with matching 'as' property
|
|
164
|
-
let fieldExists = false
|
|
165
|
-
|
|
163
|
+
let fieldExists = false
|
|
164
|
+
|
|
166
165
|
// First check if the field exists directly in the schema
|
|
167
166
|
if (schema[filterField]) {
|
|
168
|
-
fieldExists = true
|
|
167
|
+
fieldExists = true
|
|
169
168
|
} else {
|
|
170
169
|
// Check if it's a relationship name (as property)
|
|
171
170
|
for (const [fieldName, fieldDef] of Object.entries(schema)) {
|
|
172
171
|
if (fieldDef.as === filterField) {
|
|
173
|
-
fieldExists = true
|
|
174
|
-
break
|
|
172
|
+
fieldExists = true
|
|
173
|
+
break
|
|
175
174
|
}
|
|
176
175
|
}
|
|
177
176
|
}
|
|
178
|
-
|
|
177
|
+
|
|
179
178
|
if (!fieldExists) {
|
|
180
179
|
throw new Error(
|
|
181
180
|
`Resource '${scopeName}' must have '${filterField}' field or relationship in schema for position filtering`
|
|
182
|
-
)
|
|
181
|
+
)
|
|
183
182
|
}
|
|
184
|
-
})
|
|
185
|
-
})
|
|
183
|
+
})
|
|
184
|
+
})
|
|
186
185
|
|
|
187
186
|
// Add index for position field if autoIndex is enabled
|
|
188
187
|
addHook('scope:added', 'add-position-index', { afterFunction: 'validate-position-field' }, async ({ context, vars, scopes }) => {
|
|
189
|
-
const { scopeName } = context
|
|
190
|
-
|
|
188
|
+
const { scopeName } = context
|
|
189
|
+
|
|
191
190
|
if (!shouldHavePositioning(scopeName) || !vars.positioning.autoIndex) {
|
|
192
|
-
return
|
|
191
|
+
return
|
|
193
192
|
}
|
|
194
193
|
|
|
195
|
-
const scope = scopes[scopeName]
|
|
196
|
-
const schemaInfo = scope.vars.schemaInfo
|
|
197
|
-
|
|
194
|
+
const scope = scopes[scopeName]
|
|
195
|
+
const schemaInfo = scope.vars.schemaInfo
|
|
196
|
+
|
|
198
197
|
if (!schemaInfo || !api.knex?.instance) {
|
|
199
|
-
return
|
|
198
|
+
return // Skip if no database connection
|
|
200
199
|
}
|
|
201
200
|
|
|
202
201
|
try {
|
|
203
|
-
const adapter = resolveStorageAdapter(scopeName)
|
|
202
|
+
const adapter = resolveStorageAdapter(scopeName)
|
|
204
203
|
if (adapter.isCanonical()) {
|
|
205
|
-
log.debug(`Skipping positioning index for canonical storage on ${scopeName}`)
|
|
206
|
-
return
|
|
204
|
+
log.debug(`Skipping positioning index for canonical storage on ${scopeName}`)
|
|
205
|
+
return
|
|
207
206
|
}
|
|
208
207
|
|
|
209
|
-
const knex = api.knex.instance
|
|
210
|
-
const tableName = adapter.getTableName()
|
|
211
|
-
|
|
208
|
+
const knex = api.knex.instance
|
|
209
|
+
const tableName = adapter.getTableName()
|
|
210
|
+
|
|
212
211
|
// Check if table exists first
|
|
213
|
-
const tableExists = await knex.schema.hasTable(tableName)
|
|
212
|
+
const tableExists = await knex.schema.hasTable(tableName)
|
|
214
213
|
if (!tableExists) {
|
|
215
|
-
log.debug(`Table ${tableName} doesn't exist yet, skipping index creation`)
|
|
216
|
-
return
|
|
214
|
+
log.debug(`Table ${tableName} doesn't exist yet, skipping index creation`)
|
|
215
|
+
return
|
|
217
216
|
}
|
|
218
|
-
|
|
217
|
+
|
|
219
218
|
// Build index columns: map filter fields to actual database columns
|
|
220
|
-
const indexColumns = new Set()
|
|
221
|
-
|
|
219
|
+
const indexColumns = new Set()
|
|
220
|
+
|
|
222
221
|
// Map each filter field to its database column
|
|
223
222
|
vars.positioning.filters.forEach(filterField => {
|
|
224
|
-
const searchField = schemaInfo.searchSchemaStructure?.[filterField]
|
|
223
|
+
const searchField = schemaInfo.searchSchemaStructure?.[filterField]
|
|
225
224
|
if (searchField) {
|
|
226
225
|
// Use actualField if it exists (for relationships)
|
|
227
|
-
const dbField = searchField.actualField || filterField
|
|
228
|
-
indexColumns.add(adapter.translateColumn(dbField))
|
|
226
|
+
const dbField = searchField.actualField || filterField
|
|
227
|
+
indexColumns.add(adapter.translateColumn(dbField))
|
|
229
228
|
} else if (schemaInfo.schemaStructure[filterField]) {
|
|
230
229
|
// Direct database field
|
|
231
|
-
indexColumns.add(adapter.translateColumn(filterField))
|
|
230
|
+
indexColumns.add(adapter.translateColumn(filterField))
|
|
232
231
|
}
|
|
233
|
-
})
|
|
234
|
-
|
|
232
|
+
})
|
|
233
|
+
|
|
235
234
|
// Add the position field
|
|
236
|
-
indexColumns.add(adapter.translateColumn(vars.positioning.field))
|
|
235
|
+
indexColumns.add(adapter.translateColumn(vars.positioning.field))
|
|
236
|
+
|
|
237
|
+
const indexColumnList = [...indexColumns].filter(Boolean)
|
|
238
|
+
|
|
239
|
+
const indexName = `idx_${tableName}_positioning`
|
|
237
240
|
|
|
238
|
-
const indexColumnList = [...indexColumns].filter(Boolean);
|
|
239
|
-
|
|
240
|
-
const indexName = `idx_${tableName}_positioning`;
|
|
241
|
-
|
|
242
241
|
// Check if index already exists
|
|
243
|
-
const hasIndex = await knex.schema.hasIndex(tableName, indexColumnList, indexName)
|
|
244
|
-
|
|
242
|
+
const hasIndex = await knex.schema.hasIndex(tableName, indexColumnList, indexName)
|
|
243
|
+
|
|
245
244
|
if (!hasIndex) {
|
|
246
245
|
await knex.schema.table(tableName, table => {
|
|
247
|
-
table.index(indexColumnList, indexName)
|
|
248
|
-
})
|
|
249
|
-
|
|
250
|
-
log.info(`Created positioning index on ${tableName}`, { columns: indexColumnList })
|
|
246
|
+
table.index(indexColumnList, indexName)
|
|
247
|
+
})
|
|
248
|
+
|
|
249
|
+
log.info(`Created positioning index on ${tableName}`, { columns: indexColumnList })
|
|
251
250
|
}
|
|
252
251
|
} catch (error) {
|
|
253
|
-
log.warn(`Could not create positioning index for ${scopeName}:`, error.message)
|
|
252
|
+
log.warn(`Could not create positioning index for ${scopeName}:`, error.message)
|
|
254
253
|
}
|
|
255
|
-
})
|
|
254
|
+
})
|
|
256
255
|
|
|
257
256
|
// Process beforeId parameter in requests
|
|
258
257
|
addHook('beforeSchemaValidate', 'process-beforeid', {}, async ({ context, scopeName, vars }) => {
|
|
259
|
-
log.debug('*** beforeSchemaValidate positioning hook', {
|
|
260
|
-
scopeName,
|
|
258
|
+
log.debug('*** beforeSchemaValidate positioning hook', {
|
|
259
|
+
scopeName,
|
|
261
260
|
method: context.method,
|
|
262
261
|
shouldHavePositioning: shouldHavePositioning(scopeName)
|
|
263
|
-
})
|
|
264
|
-
|
|
262
|
+
})
|
|
263
|
+
|
|
265
264
|
// Skip excluded resources
|
|
266
265
|
if (!shouldHavePositioning(scopeName)) {
|
|
267
|
-
log.debug('*** Skipping beforeId processing - excluded resource')
|
|
268
|
-
return
|
|
266
|
+
log.debug('*** Skipping beforeId processing - excluded resource')
|
|
267
|
+
return
|
|
269
268
|
}
|
|
270
269
|
|
|
271
270
|
// Skip if no positioning context needed
|
|
272
|
-
const method = context.method
|
|
271
|
+
const method = context.method
|
|
273
272
|
if (!['post', 'put', 'patch'].includes(method)) {
|
|
274
|
-
log.debug('*** Skipping beforeId processing - wrong method', { method })
|
|
275
|
-
return
|
|
273
|
+
log.debug('*** Skipping beforeId processing - wrong method', { method })
|
|
274
|
+
return
|
|
276
275
|
}
|
|
277
276
|
|
|
278
277
|
// Check if resource has position field
|
|
279
|
-
const scope = scopes[scopeName]
|
|
280
|
-
const hasPositionField = scope?.vars?.schemaInfo?.schemaStructure?.[vars.positioning.field]
|
|
281
|
-
|
|
278
|
+
const scope = scopes[scopeName]
|
|
279
|
+
const hasPositionField = scope?.vars?.schemaInfo?.schemaStructure?.[vars.positioning.field]
|
|
280
|
+
|
|
282
281
|
if (!hasPositionField) {
|
|
283
|
-
return
|
|
282
|
+
return
|
|
284
283
|
}
|
|
285
284
|
|
|
286
285
|
// Extract beforeId from JSON:API format attributes
|
|
287
|
-
const attributes = context.inputRecord?.data?.attributes
|
|
288
|
-
|
|
286
|
+
const attributes = context.inputRecord?.data?.attributes ||
|
|
287
|
+
(context.simplified && context.inputRecord && typeof context.inputRecord === 'object'
|
|
289
288
|
? context.inputRecord
|
|
290
|
-
: {})
|
|
291
|
-
const beforeId = attributes[vars.positioning.beforeIdField]
|
|
289
|
+
: {})
|
|
290
|
+
const beforeId = attributes[vars.positioning.beforeIdField]
|
|
292
291
|
|
|
293
292
|
// Store beforeId in context for later use
|
|
294
293
|
if (beforeId !== undefined) {
|
|
295
|
-
context.positioningBeforeId = beforeId
|
|
294
|
+
context.positioningBeforeId = beforeId
|
|
296
295
|
|
|
297
296
|
// Remove beforeId from attributes so it doesn't get stored
|
|
298
|
-
delete attributes[vars.positioning.beforeIdField]
|
|
297
|
+
delete attributes[vars.positioning.beforeIdField]
|
|
299
298
|
|
|
300
299
|
if (context.simplified && context.inputRecord && typeof context.inputRecord === 'object') {
|
|
301
|
-
delete context.inputRecord[vars.positioning.beforeIdField]
|
|
300
|
+
delete context.inputRecord[vars.positioning.beforeIdField]
|
|
302
301
|
}
|
|
303
302
|
}
|
|
304
303
|
|
|
305
304
|
// Remove any manually provided position field - position is managed by this plugin
|
|
306
305
|
if (attributes[vars.positioning.field] !== undefined) {
|
|
307
|
-
delete attributes[vars.positioning.field]
|
|
306
|
+
delete attributes[vars.positioning.field]
|
|
308
307
|
|
|
309
|
-
log.debug('Positioning beforeId extracted', {
|
|
310
|
-
scopeName,
|
|
308
|
+
log.debug('Positioning beforeId extracted', {
|
|
309
|
+
scopeName,
|
|
311
310
|
beforeId,
|
|
312
311
|
method
|
|
313
|
-
})
|
|
312
|
+
})
|
|
314
313
|
|
|
315
314
|
if (context.simplified && context.inputRecord && typeof context.inputRecord === 'object') {
|
|
316
|
-
delete context.inputRecord[vars.positioning.field]
|
|
315
|
+
delete context.inputRecord[vars.positioning.field]
|
|
317
316
|
}
|
|
318
317
|
}
|
|
319
318
|
|
|
320
319
|
// For new records without explicit position, mark for positioning
|
|
321
320
|
if (method === 'post' && !attributes[vars.positioning.field]) {
|
|
322
|
-
context.needsPositioning = true
|
|
321
|
+
context.needsPositioning = true
|
|
323
322
|
}
|
|
324
|
-
})
|
|
323
|
+
})
|
|
325
324
|
|
|
326
325
|
// Calculate and set position before create/update
|
|
327
326
|
addHook('beforeDataCallPost', 'calculate-position-post', {}, async ({ context, scopeName, vars, helpers }) => {
|
|
328
|
-
log.debug('*** beforeDataCallPost hook fired for positioning', { scopeName, method: context.method })
|
|
329
|
-
await calculateAndSetPosition(context, scopeName, vars, helpers, log, scopes, api)
|
|
330
|
-
})
|
|
327
|
+
log.debug('*** beforeDataCallPost hook fired for positioning', { scopeName, method: context.method })
|
|
328
|
+
await calculateAndSetPosition(context, scopeName, vars, helpers, log, scopes, api)
|
|
329
|
+
})
|
|
331
330
|
|
|
332
331
|
addHook('beforeDataCallPut', 'calculate-position-put', {}, async ({ context, scopeName, vars, helpers }) => {
|
|
333
|
-
log.debug('*** beforeDataCallPut hook fired for positioning', { scopeName, method: context.method })
|
|
332
|
+
log.debug('*** beforeDataCallPut hook fired for positioning', { scopeName, method: context.method })
|
|
334
333
|
// Only process if beforeId was provided or it's a new record
|
|
335
334
|
if (context.positioningBeforeId !== undefined || context.needsPositioning) {
|
|
336
|
-
await calculateAndSetPosition(context, scopeName, vars, helpers, log, scopes, api)
|
|
335
|
+
await calculateAndSetPosition(context, scopeName, vars, helpers, log, scopes, api)
|
|
337
336
|
}
|
|
338
|
-
})
|
|
337
|
+
})
|
|
339
338
|
|
|
340
339
|
addHook('beforeDataCallPatch', 'calculate-position-patch', {}, async ({ context, scopeName, vars, helpers }) => {
|
|
341
|
-
log.debug('*** beforeDataCallPatch hook fired for positioning', { scopeName, method: context.method })
|
|
340
|
+
log.debug('*** beforeDataCallPatch hook fired for positioning', { scopeName, method: context.method })
|
|
342
341
|
// Only process if beforeId was provided
|
|
343
342
|
if (context.positioningBeforeId !== undefined) {
|
|
344
|
-
await calculateAndSetPosition(context, scopeName, vars, helpers, log, scopes, api)
|
|
343
|
+
await calculateAndSetPosition(context, scopeName, vars, helpers, log, scopes, api)
|
|
345
344
|
}
|
|
346
|
-
})
|
|
345
|
+
})
|
|
347
346
|
|
|
348
347
|
// Helper function to calculate and set position
|
|
349
|
-
async function calculateAndSetPosition(context, scopeName, vars, helpers, log, scopes, api) {
|
|
350
|
-
log.debug('*** calculateAndSetPosition called', {
|
|
351
|
-
scopeName,
|
|
348
|
+
async function calculateAndSetPosition (context, scopeName, vars, helpers, log, scopes, api) {
|
|
349
|
+
log.debug('*** calculateAndSetPosition called', {
|
|
350
|
+
scopeName,
|
|
352
351
|
method: context.method,
|
|
353
352
|
shouldHavePositioning: shouldHavePositioning(scopeName),
|
|
354
353
|
beforeId: context.positioningBeforeId,
|
|
355
354
|
needsPositioning: context.needsPositioning
|
|
356
|
-
})
|
|
357
|
-
|
|
355
|
+
})
|
|
356
|
+
|
|
358
357
|
// Skip if no positioning needed
|
|
359
358
|
if (!shouldHavePositioning(scopeName)) {
|
|
360
|
-
log.debug('*** Skipping - resource excluded from positioning')
|
|
361
|
-
return
|
|
359
|
+
log.debug('*** Skipping - resource excluded from positioning')
|
|
360
|
+
return
|
|
362
361
|
}
|
|
363
362
|
|
|
364
|
-
const beforeId = context.positioningBeforeId
|
|
365
|
-
const needsPositioning = context.needsPositioning
|
|
366
|
-
|
|
363
|
+
const beforeId = context.positioningBeforeId
|
|
364
|
+
const needsPositioning = context.needsPositioning
|
|
365
|
+
|
|
367
366
|
// Skip if no positioning action needed
|
|
368
367
|
if (beforeId === undefined && !needsPositioning) {
|
|
369
|
-
log.debug('*** Skipping - no positioning needed', { beforeId, needsPositioning })
|
|
370
|
-
return
|
|
368
|
+
log.debug('*** Skipping - no positioning needed', { beforeId, needsPositioning })
|
|
369
|
+
return
|
|
371
370
|
}
|
|
372
371
|
|
|
373
372
|
const recordData = (() => {
|
|
374
373
|
if (context.inputRecord?.data?.attributes) {
|
|
375
|
-
return { ...context.inputRecord.data.attributes }
|
|
374
|
+
return { ...context.inputRecord.data.attributes }
|
|
376
375
|
}
|
|
377
376
|
if (context.simplified && context.inputRecord && typeof context.inputRecord === 'object') {
|
|
378
|
-
return { ...context.inputRecord }
|
|
377
|
+
return { ...context.inputRecord }
|
|
379
378
|
}
|
|
380
|
-
return {}
|
|
381
|
-
})()
|
|
382
|
-
|
|
383
|
-
log.debug('*** Record data for positioning', {
|
|
379
|
+
return {}
|
|
380
|
+
})()
|
|
381
|
+
|
|
382
|
+
log.debug('*** Record data for positioning', {
|
|
384
383
|
recordData,
|
|
385
384
|
simplified: context.simplified,
|
|
386
385
|
inputRecord: context.inputRecord
|
|
387
|
-
})
|
|
386
|
+
})
|
|
388
387
|
|
|
389
|
-
const jsonApiRecord = context.inputRecord?.data || {}
|
|
390
|
-
const inputRelationships = jsonApiRecord.relationships || {}
|
|
388
|
+
const jsonApiRecord = context.inputRecord?.data || {}
|
|
389
|
+
const inputRelationships = jsonApiRecord.relationships || {}
|
|
391
390
|
const simplifiedRecord = context.simplified && context.inputRecord && typeof context.inputRecord === 'object'
|
|
392
391
|
? context.inputRecord
|
|
393
|
-
: {}
|
|
392
|
+
: {}
|
|
394
393
|
|
|
395
|
-
const minimalAttributes = context.minimalRecord?.attributes || {}
|
|
396
|
-
const minimalRelationships = context.minimalRecord?.relationships || {}
|
|
394
|
+
const minimalAttributes = context.minimalRecord?.attributes || {}
|
|
395
|
+
const minimalRelationships = context.minimalRecord?.relationships || {}
|
|
397
396
|
|
|
398
397
|
const combinedAttributes = {
|
|
399
398
|
...minimalAttributes,
|
|
400
399
|
...recordData,
|
|
401
|
-
}
|
|
400
|
+
}
|
|
402
401
|
|
|
403
402
|
// Build filter conditions for the position group
|
|
404
|
-
const schemaInfo = context.schemaInfo
|
|
405
|
-
const adapter = context.storageAdapter || resolveStorageAdapter(scopeName)
|
|
403
|
+
const schemaInfo = context.schemaInfo
|
|
404
|
+
const adapter = context.storageAdapter || resolveStorageAdapter(scopeName)
|
|
406
405
|
|
|
407
406
|
if (!adapter) {
|
|
408
|
-
log.warn('*** Positioning skipped - no storage adapter available', { scopeName })
|
|
409
|
-
return
|
|
407
|
+
log.warn('*** Positioning skipped - no storage adapter available', { scopeName })
|
|
408
|
+
return
|
|
410
409
|
}
|
|
411
|
-
|
|
410
|
+
|
|
412
411
|
const filterConditions = buildPositionFilterConditions(adapter, schemaInfo, {
|
|
413
412
|
attributes: combinedAttributes,
|
|
414
413
|
rawRecord: recordData,
|
|
415
414
|
simplifiedRecord,
|
|
416
415
|
inputRelationships,
|
|
417
416
|
minimalRelationships,
|
|
418
|
-
})
|
|
417
|
+
})
|
|
419
418
|
|
|
420
|
-
log.debug('*** Position filter conditions', { scopeName, filterConditions })
|
|
419
|
+
log.debug('*** Position filter conditions', { scopeName, filterConditions })
|
|
421
420
|
|
|
422
421
|
// Query existing items in the same position group
|
|
423
|
-
const knex = api.knex.instance
|
|
424
|
-
const tableName = adapter.getTableName()
|
|
425
|
-
const idProperty = schemaInfo.idProperty
|
|
426
|
-
const idColumn = adapter.getIdColumn()
|
|
427
|
-
const positionColumn = adapter.translateColumn(vars.positioning.field)
|
|
428
|
-
|
|
422
|
+
const knex = api.knex.instance
|
|
423
|
+
const tableName = adapter.getTableName()
|
|
424
|
+
const idProperty = schemaInfo.idProperty
|
|
425
|
+
const idColumn = adapter.getIdColumn()
|
|
426
|
+
const positionColumn = adapter.translateColumn(vars.positioning.field)
|
|
427
|
+
|
|
429
428
|
log.debug('*** Building position query', {
|
|
430
429
|
tableName,
|
|
431
430
|
idProperty,
|
|
@@ -433,117 +432,117 @@ export const PositioningPlugin = {
|
|
|
433
432
|
positionField: vars.positioning.field,
|
|
434
433
|
positionColumn,
|
|
435
434
|
filterConditions
|
|
436
|
-
})
|
|
437
|
-
|
|
435
|
+
})
|
|
436
|
+
|
|
438
437
|
// Check if we have a transaction to use
|
|
439
438
|
// Build base query with filter conditions
|
|
440
|
-
const baseQuery = adapter.buildBaseQuery({ transaction: context.transaction })
|
|
439
|
+
const baseQuery = adapter.buildBaseQuery({ transaction: context.transaction })
|
|
441
440
|
Object.entries(filterConditions).forEach(([column, value]) => {
|
|
442
441
|
if (value === null) {
|
|
443
|
-
baseQuery.whereNull(column)
|
|
442
|
+
baseQuery.whereNull(column)
|
|
444
443
|
} else {
|
|
445
|
-
baseQuery.where(column, value)
|
|
444
|
+
baseQuery.where(column, value)
|
|
446
445
|
}
|
|
447
|
-
})
|
|
446
|
+
})
|
|
448
447
|
|
|
449
448
|
// For updates, exclude the current record
|
|
450
449
|
if ((context.method === 'patch' || context.method === 'put') && context.id) {
|
|
451
|
-
const translatedId = adapter.translateFilterValue(idProperty, context.id)
|
|
452
|
-
baseQuery.whereNot(idColumn, translatedId)
|
|
450
|
+
const translatedId = adapter.translateFilterValue(idProperty, context.id)
|
|
451
|
+
baseQuery.whereNot(idColumn, translatedId)
|
|
453
452
|
}
|
|
454
|
-
|
|
453
|
+
|
|
455
454
|
// Calculate effective beforeId first
|
|
456
|
-
let effectiveBeforeId = beforeId
|
|
457
|
-
|
|
455
|
+
let effectiveBeforeId = beforeId
|
|
456
|
+
|
|
458
457
|
// Handle default positioning when no beforeId is provided
|
|
459
458
|
if (beforeId === undefined) {
|
|
460
459
|
if (vars.positioning.defaultPosition === 'last') {
|
|
461
|
-
effectiveBeforeId = null
|
|
460
|
+
effectiveBeforeId = null // null means position at end
|
|
462
461
|
} else if (vars.positioning.defaultPosition === 'first') {
|
|
463
|
-
effectiveBeforeId = 'FIRST'
|
|
462
|
+
effectiveBeforeId = 'FIRST' // Special marker for first position
|
|
464
463
|
}
|
|
465
464
|
}
|
|
466
|
-
|
|
467
|
-
let items = []
|
|
468
|
-
|
|
465
|
+
|
|
466
|
+
let items = []
|
|
467
|
+
|
|
469
468
|
const selectColumns = {
|
|
470
469
|
[idProperty]: idColumn,
|
|
471
470
|
[vars.positioning.field]: positionColumn,
|
|
472
|
-
}
|
|
473
|
-
const selectWithAliases = (builder) => adapter.selectColumns(builder, selectColumns)
|
|
471
|
+
}
|
|
472
|
+
const selectWithAliases = (builder) => adapter.selectColumns(builder, selectColumns)
|
|
474
473
|
|
|
475
474
|
try {
|
|
476
475
|
log.debug('*** Positioning query start', {
|
|
477
476
|
effectiveBeforeId,
|
|
478
477
|
selectColumns,
|
|
479
|
-
})
|
|
478
|
+
})
|
|
480
479
|
if (effectiveBeforeId === null) {
|
|
481
480
|
// Positioning at end - only get the last item
|
|
482
481
|
const lastItem = await selectWithAliases(baseQuery
|
|
483
482
|
.clone())
|
|
484
483
|
.orderBy(positionColumn, 'desc')
|
|
485
|
-
.first()
|
|
486
|
-
|
|
484
|
+
.first()
|
|
485
|
+
|
|
487
486
|
if (lastItem) {
|
|
488
|
-
items = [lastItem]
|
|
487
|
+
items = [lastItem]
|
|
489
488
|
}
|
|
490
489
|
} else if (effectiveBeforeId === 'FIRST') {
|
|
491
490
|
// Positioning at beginning - get the first item
|
|
492
491
|
const firstItem = await selectWithAliases(baseQuery
|
|
493
492
|
.clone())
|
|
494
493
|
.orderBy(positionColumn, 'asc')
|
|
495
|
-
.first()
|
|
496
|
-
|
|
494
|
+
.first()
|
|
495
|
+
|
|
497
496
|
if (firstItem) {
|
|
498
|
-
items = [firstItem]
|
|
499
|
-
effectiveBeforeId = firstItem[idProperty]
|
|
497
|
+
items = [firstItem]
|
|
498
|
+
effectiveBeforeId = firstItem[idProperty]
|
|
500
499
|
} else {
|
|
501
|
-
effectiveBeforeId = null
|
|
500
|
+
effectiveBeforeId = null
|
|
502
501
|
}
|
|
503
502
|
} else if (effectiveBeforeId) {
|
|
504
503
|
// Positioning before a specific item - get that item and the one before it
|
|
505
|
-
const normalizedTargetId = adapter.translateFilterValue(idProperty, effectiveBeforeId)
|
|
504
|
+
const normalizedTargetId = adapter.translateFilterValue(idProperty, effectiveBeforeId)
|
|
506
505
|
const targetItem = await selectWithAliases(baseQuery
|
|
507
506
|
.clone())
|
|
508
507
|
.where(idColumn, normalizedTargetId)
|
|
509
|
-
.first()
|
|
510
|
-
|
|
508
|
+
.first()
|
|
509
|
+
|
|
511
510
|
if (targetItem) {
|
|
512
511
|
// Get the item immediately before the target
|
|
513
512
|
const prevItem = await selectWithAliases(baseQuery
|
|
514
513
|
.clone())
|
|
515
514
|
.where(positionColumn, '<', targetItem[vars.positioning.field])
|
|
516
515
|
.orderBy(positionColumn, 'desc')
|
|
517
|
-
.first()
|
|
518
|
-
|
|
519
|
-
items = prevItem ? [prevItem, targetItem] : [targetItem]
|
|
516
|
+
.first()
|
|
517
|
+
|
|
518
|
+
items = prevItem ? [prevItem, targetItem] : [targetItem]
|
|
520
519
|
}
|
|
521
520
|
} else {
|
|
522
521
|
// First item in group - no items needed
|
|
523
|
-
items = []
|
|
522
|
+
items = []
|
|
524
523
|
}
|
|
525
|
-
|
|
526
|
-
log.debug('*** Query completed', { itemCount: items.length, items })
|
|
524
|
+
|
|
525
|
+
log.debug('*** Query completed', { itemCount: items.length, items })
|
|
527
526
|
} catch (error) {
|
|
528
|
-
log.error('*** Query failed', { error: error.message, stack: error.stack })
|
|
529
|
-
throw error
|
|
527
|
+
log.error('*** Query failed', { error: error.message, stack: error.stack })
|
|
528
|
+
throw error
|
|
530
529
|
}
|
|
531
|
-
|
|
530
|
+
|
|
532
531
|
// Calculate new position
|
|
533
|
-
const newPosition = items.length === 0
|
|
532
|
+
const newPosition = items.length === 0
|
|
534
533
|
? getInitialPosition()
|
|
535
|
-
: calculatePosition(items, effectiveBeforeId, idProperty, vars.positioning.field)
|
|
536
|
-
|
|
534
|
+
: calculatePosition(items, effectiveBeforeId, idProperty, vars.positioning.field)
|
|
535
|
+
|
|
537
536
|
// Set the position on the incoming payload so downstream steps persist it
|
|
538
537
|
if (context.inputRecord?.data) {
|
|
539
|
-
context.inputRecord.data.attributes = context.inputRecord.data.attributes || {}
|
|
540
|
-
context.inputRecord.data.attributes[vars.positioning.field] = newPosition
|
|
538
|
+
context.inputRecord.data.attributes = context.inputRecord.data.attributes || {}
|
|
539
|
+
context.inputRecord.data.attributes[vars.positioning.field] = newPosition
|
|
541
540
|
}
|
|
542
541
|
|
|
543
542
|
if (context.simplified && context.inputRecord && typeof context.inputRecord === 'object') {
|
|
544
|
-
context.inputRecord[vars.positioning.field] = newPosition
|
|
543
|
+
context.inputRecord[vars.positioning.field] = newPosition
|
|
545
544
|
}
|
|
546
|
-
|
|
545
|
+
|
|
547
546
|
log.debug('Position calculated', {
|
|
548
547
|
scopeName,
|
|
549
548
|
newPosition,
|
|
@@ -552,7 +551,7 @@ export const PositioningPlugin = {
|
|
|
552
551
|
itemCount: items.length,
|
|
553
552
|
method: context.method,
|
|
554
553
|
id: context.id
|
|
555
|
-
})
|
|
554
|
+
})
|
|
556
555
|
}
|
|
557
556
|
|
|
558
557
|
// Add positioning info back to response
|
|
@@ -560,34 +559,34 @@ export const PositioningPlugin = {
|
|
|
560
559
|
// Only add if beforeId was in the original request
|
|
561
560
|
if (context.positioningBeforeId !== undefined && context.record) {
|
|
562
561
|
if (context.simplified) {
|
|
563
|
-
context.record[vars.positioning.beforeIdField] = context.positioningBeforeId
|
|
562
|
+
context.record[vars.positioning.beforeIdField] = context.positioningBeforeId
|
|
564
563
|
} else {
|
|
565
564
|
// For JSON:API, add to meta
|
|
566
|
-
context.meta = context.meta || {}
|
|
565
|
+
context.meta = context.meta || {}
|
|
567
566
|
context.meta.positioning = {
|
|
568
567
|
[vars.positioning.beforeIdField]: context.positioningBeforeId
|
|
569
|
-
}
|
|
568
|
+
}
|
|
570
569
|
}
|
|
571
570
|
}
|
|
572
|
-
})
|
|
571
|
+
})
|
|
573
572
|
|
|
574
573
|
// Apply default sort by position if no other sort specified
|
|
575
574
|
addHook('beforeQuery', 'apply-position-sort', {}, async ({ context, scopeName, vars }) => {
|
|
576
575
|
if (!shouldHavePositioning(scopeName)) {
|
|
577
|
-
return
|
|
576
|
+
return
|
|
578
577
|
}
|
|
579
578
|
|
|
580
579
|
// Check if sort is already specified
|
|
581
|
-
const hasSort = context.queryParams?.sort && context.queryParams.sort.length > 0
|
|
582
|
-
|
|
580
|
+
const hasSort = context.queryParams?.sort && context.queryParams.sort.length > 0
|
|
581
|
+
|
|
583
582
|
if (!hasSort) {
|
|
584
583
|
// Apply position sort
|
|
585
|
-
context.queryParams = context.queryParams || {}
|
|
586
|
-
context.queryParams.sort = [vars.positioning.field]
|
|
587
|
-
|
|
588
|
-
log.trace('Applied default position sort', { scopeName })
|
|
584
|
+
context.queryParams = context.queryParams || {}
|
|
585
|
+
context.queryParams.sort = [vars.positioning.field]
|
|
586
|
+
|
|
587
|
+
log.trace('Applied default position sort', { scopeName })
|
|
589
588
|
}
|
|
590
|
-
})
|
|
589
|
+
})
|
|
591
590
|
|
|
592
591
|
// API methods for positioning operations
|
|
593
592
|
api.positioning = {
|
|
@@ -597,41 +596,41 @@ export const PositioningPlugin = {
|
|
|
597
596
|
* @param {Array} positions - Array of {id, position} or {id, beforeId}
|
|
598
597
|
* @param {Object} filters - Filter conditions for position group
|
|
599
598
|
*/
|
|
600
|
-
async reorder(scopeName, positions, filters = {}) {
|
|
601
|
-
const scope = scopes[scopeName]
|
|
599
|
+
async reorder (scopeName, positions, filters = {}) {
|
|
600
|
+
const scope = scopes[scopeName]
|
|
602
601
|
if (!scope) {
|
|
603
|
-
throw new Error(`Unknown resource: ${scopeName}`)
|
|
602
|
+
throw new Error(`Unknown resource: ${scopeName}`)
|
|
604
603
|
}
|
|
605
604
|
|
|
606
605
|
// Implementation would go here for bulk reordering
|
|
607
|
-
log.info('Bulk reorder requested', { scopeName, count: positions.length })
|
|
606
|
+
log.info('Bulk reorder requested', { scopeName, count: positions.length })
|
|
608
607
|
},
|
|
609
608
|
|
|
610
609
|
/**
|
|
611
610
|
* Get positioning configuration
|
|
612
611
|
*/
|
|
613
|
-
getConfig() {
|
|
614
|
-
return { ...vars.positioning }
|
|
612
|
+
getConfig () {
|
|
613
|
+
return { ...vars.positioning }
|
|
615
614
|
},
|
|
616
615
|
|
|
617
616
|
/**
|
|
618
617
|
* Check if a resource has positioning enabled
|
|
619
618
|
*/
|
|
620
|
-
isEnabled(scopeName) {
|
|
621
|
-
return shouldHavePositioning(scopeName)
|
|
619
|
+
isEnabled (scopeName) {
|
|
620
|
+
return shouldHavePositioning(scopeName)
|
|
622
621
|
}
|
|
623
|
-
}
|
|
622
|
+
}
|
|
624
623
|
|
|
625
624
|
log.info('Positioning plugin installed', {
|
|
626
625
|
field: vars.positioning.field,
|
|
627
626
|
strategy: vars.positioning.strategy,
|
|
628
627
|
filters: vars.positioning.filters,
|
|
629
628
|
excludedResources: vars.positioning.excludeResources
|
|
630
|
-
})
|
|
629
|
+
})
|
|
631
630
|
}
|
|
632
|
-
}
|
|
631
|
+
}
|
|
633
632
|
|
|
634
|
-
/*
|
|
633
|
+
/*
|
|
635
634
|
Usage examples:
|
|
636
635
|
|
|
637
636
|
// Basic usage - positioning for all resources
|