json-rest-api 1.0.10 → 1.0.13

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.
Files changed (117) hide show
  1. package/.claude/settings.local.json +9 -1
  2. package/AGENTS.md +34 -0
  3. package/CLAUDE.md +26 -1
  4. package/CRUSH.md +55 -0
  5. package/README.md +3 -3
  6. package/TODO.todo +8 -0
  7. package/docs/API.md +17 -18
  8. package/docs/COMPARISON.md +18 -13
  9. package/docs/GUIDE/GUIDE_1_Initial_Setup.md +17 -21
  10. package/docs/GUIDE/GUIDE_2_1_The_Starting_Point.md +33 -2
  11. package/docs/GUIDE/GUIDE_2_2_Manipulating_And_Searching_Tables.md +61 -7
  12. package/docs/GUIDE/GUIDE_2_3_BelongsTo_Relationships.md +58 -61
  13. package/docs/GUIDE/GUIDE_2_4_HasMany_Records.md +37 -37
  14. package/docs/GUIDE/GUIDE_2_5_HasMany_Polymorphic.md +103 -87
  15. package/docs/GUIDE/GUIDE_2_6_Many_To_Many.md +37 -34
  16. package/docs/GUIDE/GUIDE_2_7_Pagination_And_Ordering.md +45 -42
  17. package/docs/GUIDE/GUIDE_2_8_Effects_of_PUT_and_PATCH.md +3 -3
  18. package/docs/GUIDE/GUIDE_2_Data_And_Relations.md +1 -1
  19. package/docs/GUIDE/GUIDE_3_Field_Transformations.md +5 -5
  20. package/docs/GUIDE/GUIDE_4_Authentication_And_Permissions.md +498 -755
  21. package/docs/GUIDE/GUIDE_6_Positioning.md +1 -3
  22. package/docs/GUIDE/GUIDE_7_Hooks_Data_Management_And_Plugins.md +4 -5
  23. package/docs/GUIDE/GUIDE_X_Bulk_Operations.md +15 -6
  24. package/docs/GUIDE/GUIDE_X_Cors.md +2 -5
  25. package/docs/GUIDE/GUIDE_X_File_Uploads.md +2 -3
  26. package/docs/GUIDE/GUIDE_X_Multihome.md +1 -4
  27. package/docs/GUIDE/GUIDE_X_SocketIO.md +0 -2
  28. package/docs/GUIDE/GUIDE_X_URL_Management.md +317 -0
  29. package/docs/GUIDE/index.md +3 -0
  30. package/docs/QUICKSTART.md +12 -12
  31. package/docs/index.md +2 -2
  32. package/docs/mini-guides/file-uploads-complete.md +0 -1
  33. package/examples/url-override-example.js +137 -0
  34. package/index.js +7 -1
  35. package/package.json +7 -11
  36. package/plugins/core/bulk-operations-plugin.js +2 -0
  37. package/plugins/core/connectors/express-plugin.js +43 -24
  38. package/plugins/core/connectors/lib/request-helpers.js +2 -2
  39. package/plugins/core/lib/anyapi/anyapi-registry.js +821 -0
  40. package/plugins/core/lib/anyapi/query/anyapi-query-adapter.js +647 -0
  41. package/plugins/core/lib/anyapi/schema-utils.js +186 -0
  42. package/plugins/core/lib/anyapi/utils/descriptor-helpers.js +113 -0
  43. package/plugins/core/lib/dbTablesOperations.js +305 -0
  44. package/plugins/core/lib/querying/knex-cross-table-search.js +64 -108
  45. package/plugins/core/lib/querying/knex-json-api-transformers-querying.js +21 -27
  46. package/plugins/core/lib/querying/knex-pagination-helpers.js +36 -20
  47. package/plugins/core/lib/querying/knex-query-helpers-base.js +49 -14
  48. package/plugins/core/lib/querying/knex-query-helpers.js +378 -222
  49. package/plugins/core/lib/querying/knex-relationship-includes.js +262 -87
  50. package/plugins/core/lib/querying/url-helpers.js +119 -0
  51. package/plugins/core/lib/querying-writing/compile-schemas.js +36 -28
  52. package/plugins/core/lib/querying-writing/database-capabilities.js +10 -9
  53. package/plugins/core/lib/querying-writing/knex-field-helpers.js +9 -3
  54. package/plugins/core/lib/querying-writing/knex-json-api-transformers.js +2 -2
  55. package/plugins/core/lib/querying-writing/payload-validators.js +10 -16
  56. package/plugins/core/lib/querying-writing/schema-helpers.js +52 -60
  57. package/plugins/core/lib/querying-writing/scope-validations.js +78 -22
  58. package/plugins/core/lib/querying-writing/simplified-helpers.js +208 -61
  59. package/plugins/core/lib/storage/storage-adapter.js +160 -0
  60. package/plugins/core/lib/writing/relationship-processor.js +4 -11
  61. package/plugins/core/rest-api-access.js +456 -0
  62. package/plugins/core/rest-api-anyapi-knex-plugin.js +2285 -0
  63. package/plugins/core/rest-api-knex-plugin.js +226 -87
  64. package/plugins/core/rest-api-multihome-plugin.js +89 -27
  65. package/plugins/core/rest-api-plugin-hooks/turn-scope-init-into-vars.js +0 -1
  66. package/plugins/core/rest-api-plugin-methods/check-permissions.js +1 -5
  67. package/plugins/core/rest-api-plugin-methods/common.js +46 -10
  68. package/plugins/core/rest-api-plugin-methods/delete-relationship.js +31 -27
  69. package/plugins/core/rest-api-plugin-methods/delete.js +1 -4
  70. package/plugins/core/rest-api-plugin-methods/enrich-attributes.js +13 -1
  71. package/plugins/core/rest-api-plugin-methods/get-related.js +114 -56
  72. package/plugins/core/rest-api-plugin-methods/get-relationship.js +4 -3
  73. package/plugins/core/rest-api-plugin-methods/get.js +2 -5
  74. package/plugins/core/rest-api-plugin-methods/patch.js +15 -16
  75. package/plugins/core/rest-api-plugin-methods/post-relationship.js +21 -14
  76. package/plugins/core/rest-api-plugin-methods/post.js +23 -11
  77. package/plugins/core/rest-api-plugin-methods/put.js +18 -21
  78. package/plugins/core/rest-api-plugin-methods/query.js +3 -4
  79. package/plugins/core/rest-api-plugin-methods/release.js +13 -0
  80. package/plugins/core/rest-api-plugin.js +3 -4
  81. package/plugins/core/rest-api-positioning-plugin.js +253 -76
  82. package/plugins/core/socketio-plugin.js +436 -502
  83. package/quickTest.js +2 -2
  84. package/scripts/debug-anyapi-review-filter.mjs +63 -0
  85. package/scripts/inspect-anyapi-descriptor-country.mjs +19 -0
  86. package/scripts/inspect-anyapi-descriptor.mjs +19 -0
  87. package/test-debug.js +59 -0
  88. package/tests/README.md +7 -1
  89. package/tests/anyapi-basic.test.js +102 -0
  90. package/tests/anyapi-cursor-pagination.test.js +144 -0
  91. package/tests/computed-fields.test.js +27 -12
  92. package/tests/cors.test.js +5 -6
  93. package/tests/curl.test.js +1 -1
  94. package/tests/custom-idproperty-relationships.test.js +37 -29
  95. package/tests/field-getters.test.js +25 -7
  96. package/tests/fixtures/api-configs.js +576 -150
  97. package/tests/helpers/storage-mode.js +65 -0
  98. package/tests/helpers/test-utils.js +85 -3
  99. package/tests/include-depth-validation.test.js +35 -1
  100. package/tests/nested-includes.test.js +130 -103
  101. package/tests/pagination-cursor-multifield.test.js +17 -13
  102. package/tests/pagination-enhanced.test.js +19 -24
  103. package/tests/polymorphic-simple.test.js +9 -9
  104. package/tests/positioning-advanced.test.js +19 -19
  105. package/tests/positioning-extended.test.js +25 -25
  106. package/tests/positioning-multifilter.test.js +62 -62
  107. package/tests/positioning.test.js +7 -7
  108. package/tests/queries.test.js +5 -5
  109. package/tests/query-limits.test.js +77 -92
  110. package/tests/rest-api-access.test.js +206 -0
  111. package/tests/searchschema-merge.test.js +182 -0
  112. package/tests/socketio.test.js +33 -14
  113. package/tests/virtual-fields.test.js +0 -1
  114. package/docs/DOCS_ALL.md +0 -21894
  115. package/plugins/core/jwt-auth-plugin.js +0 -897
  116. package/plugins/core/lib/jwt-auth-helpers.js +0 -130
  117. package/tests/jwt-auth.test.js +0 -692
@@ -1,16 +1,30 @@
1
- import {
2
- calculatePosition,
3
- getInitialPosition,
1
+ import {
2
+ calculatePosition,
3
+ getInitialPosition,
4
4
  isValidPosition,
5
- assignInitialPositions
5
+ assignInitialPositions,
6
6
  } from './lib/fractional-positioning.js';
7
+ import { createStorageAdapter } from './lib/storage/storage-adapter.js';
7
8
 
8
9
  export const PositioningPlugin = {
9
10
  name: 'positioning',
10
- dependencies: ['rest-api', 'rest-api-knex'],
11
+ dependencies: ['rest-api', 'rest-api-knex|rest-api-anyapi-knex'],
11
12
 
12
13
  install({ api, addHook, vars, helpers, log, scopes, pluginOptions }) {
13
-
14
+
15
+ const installedPlugins = Array.from(api._installedPlugins || []);
16
+ const legacyStorageInstalled = installedPlugins.includes('rest-api-knex');
17
+ const canonicalStorageInstalled = installedPlugins.includes('rest-api-anyapi-knex');
18
+ if (!legacyStorageInstalled && !canonicalStorageInstalled) {
19
+ throw new Error(
20
+ "Positioning plugin requires either 'rest-api-knex' or 'rest-api-anyapi-knex' to be installed before it."
21
+ );
22
+ }
23
+
24
+ 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)');
26
+ }
27
+
14
28
  // Get configuration - hooked-api namespaces options by plugin name
15
29
  const positioningOptions = pluginOptions || {};
16
30
 
@@ -26,6 +40,25 @@ export const PositioningPlugin = {
26
40
  rebalanceThreshold: positioningOptions.rebalanceThreshold || 50 // Max position string length
27
41
  };
28
42
 
43
+ 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;
48
+
49
+ if (scopeVars.storageAdapter) {
50
+ return scopeVars.storageAdapter;
51
+ }
52
+
53
+ if (scopeVars.schemaInfo && api.knex?.instance) {
54
+ const adapter = createStorageAdapter({ knex: api.knex.instance, schemaInfo: scopeVars.schemaInfo });
55
+ scopeVars.storageAdapter = adapter;
56
+ return adapter;
57
+ }
58
+
59
+ return null;
60
+ };
61
+
29
62
  // Validate configuration
30
63
  if (!['fractional', 'integer'].includes(vars.positioning.strategy)) {
31
64
  throw new Error(`Invalid positioning strategy: ${vars.positioning.strategy}. Must be 'fractional' or 'integer'`);
@@ -41,18 +74,65 @@ export const PositioningPlugin = {
41
74
  }
42
75
 
43
76
  // Helper to build filter conditions for position groups
44
- function buildPositionFilterConditions(record, schemaInfo) {
77
+ function buildPositionFilterConditions(adapter, schemaInfo, {
78
+ attributes = {},
79
+ rawRecord = {},
80
+ simplifiedRecord = {},
81
+ inputRelationships = {},
82
+ minimalRelationships = {},
83
+ }) {
45
84
  const conditions = {};
46
-
47
- log.debug('*** buildPositionFilterConditions', {
85
+
86
+ log.debug('*** buildPositionFilterConditions', {
48
87
  filters: vars.positioning.filters,
49
- record: record,
50
- recordKeys: Object.keys(record)
88
+ attributeKeys: Object.keys(attributes || {}),
89
+ rawKeys: Object.keys(rawRecord || {}),
90
+ simplifiedKeys: Object.keys(simplifiedRecord || {}),
51
91
  });
52
-
53
- vars.positioning.filters.forEach(filterField => {
54
- if (record[filterField] !== undefined) {
55
- conditions[filterField] = record[filterField];
92
+
93
+ const tryAssign = (holder, field) => (holder && field in holder ? holder[field] : undefined);
94
+
95
+ vars.positioning.filters.forEach((filterField) => {
96
+ const searchField = schemaInfo.searchSchemaStructure?.[filterField] || null;
97
+ const schemaField = schemaInfo.schemaStructure?.[filterField] || null;
98
+ const isRelationship = Boolean(
99
+ searchField?.isRelationship
100
+ || schemaField?.belongsTo
101
+ || schemaField?.belongsToPolymorphic
102
+ );
103
+
104
+ if (!searchField && !schemaField) {
105
+ log.warn(`Filter field '${filterField}' not found in search schema or resource schema for positioning`);
106
+ }
107
+
108
+ const dbField = searchField?.actualField || filterField;
109
+ let value;
110
+
111
+ const attempt = (candidate) => {
112
+ if (value === undefined && candidate !== undefined) {
113
+ value = candidate;
114
+ }
115
+ };
116
+
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));
122
+
123
+ if (isRelationship) {
124
+ attempt(inputRelationships?.[filterField]?.data?.id);
125
+ attempt(minimalRelationships?.[filterField]?.data?.id);
126
+ // Some contexts may expose relationship data under attributes
127
+ attempt(tryAssign(rawRecord?.relationships, filterField)?.data?.id);
128
+ }
129
+
130
+ if (value !== undefined) {
131
+ const column = adapter.translateColumn(dbField);
132
+ const normalizedValue = value === null
133
+ ? null
134
+ : adapter.translateFilterValue(filterField, value);
135
+ conditions[column] = normalizedValue;
56
136
  }
57
137
  });
58
138
 
@@ -78,11 +158,27 @@ export const PositioningPlugin = {
78
158
  );
79
159
  }
80
160
 
81
- // Validate filter fields exist
161
+ // Validate filter fields exist (check both relationship names and actual field names)
82
162
  vars.positioning.filters.forEach(filterField => {
83
- if (!schema[filterField]) {
163
+ // Check if it's a relationship field by looking for a field with matching 'as' property
164
+ let fieldExists = false;
165
+
166
+ // First check if the field exists directly in the schema
167
+ if (schema[filterField]) {
168
+ fieldExists = true;
169
+ } else {
170
+ // Check if it's a relationship name (as property)
171
+ for (const [fieldName, fieldDef] of Object.entries(schema)) {
172
+ if (fieldDef.as === filterField) {
173
+ fieldExists = true;
174
+ break;
175
+ }
176
+ }
177
+ }
178
+
179
+ if (!fieldExists) {
84
180
  throw new Error(
85
- `Resource '${scopeName}' must have '${filterField}' field in schema for position filtering`
181
+ `Resource '${scopeName}' must have '${filterField}' field or relationship in schema for position filtering`
86
182
  );
87
183
  }
88
184
  });
@@ -104,8 +200,14 @@ export const PositioningPlugin = {
104
200
  }
105
201
 
106
202
  try {
203
+ const adapter = resolveStorageAdapter(scopeName);
204
+ if (adapter.isCanonical()) {
205
+ log.debug(`Skipping positioning index for canonical storage on ${scopeName}`);
206
+ return;
207
+ }
208
+
107
209
  const knex = api.knex.instance;
108
- const tableName = schemaInfo.tableName;
210
+ const tableName = adapter.getTableName();
109
211
 
110
212
  // Check if table exists first
111
213
  const tableExists = await knex.schema.hasTable(tableName);
@@ -114,23 +216,38 @@ export const PositioningPlugin = {
114
216
  return;
115
217
  }
116
218
 
117
- // Build index columns: filters + position field
118
- const indexColumns = [
119
- ...vars.positioning.filters,
120
- vars.positioning.field
121
- ];
219
+ // Build index columns: map filter fields to actual database columns
220
+ const indexColumns = new Set();
221
+
222
+ // Map each filter field to its database column
223
+ vars.positioning.filters.forEach(filterField => {
224
+ const searchField = schemaInfo.searchSchemaStructure?.[filterField];
225
+ if (searchField) {
226
+ // Use actualField if it exists (for relationships)
227
+ const dbField = searchField.actualField || filterField;
228
+ indexColumns.add(adapter.translateColumn(dbField));
229
+ } else if (schemaInfo.schemaStructure[filterField]) {
230
+ // Direct database field
231
+ indexColumns.add(adapter.translateColumn(filterField));
232
+ }
233
+ });
234
+
235
+ // Add the position field
236
+ indexColumns.add(adapter.translateColumn(vars.positioning.field));
237
+
238
+ const indexColumnList = [...indexColumns].filter(Boolean);
122
239
 
123
240
  const indexName = `idx_${tableName}_positioning`;
124
241
 
125
242
  // Check if index already exists
126
- const hasIndex = await knex.schema.hasIndex(tableName, indexColumns, indexName);
243
+ const hasIndex = await knex.schema.hasIndex(tableName, indexColumnList, indexName);
127
244
 
128
245
  if (!hasIndex) {
129
246
  await knex.schema.table(tableName, table => {
130
- table.index(indexColumns, indexName);
247
+ table.index(indexColumnList, indexName);
131
248
  });
132
249
 
133
- log.info(`Created positioning index on ${tableName}`, { columns: indexColumns });
250
+ log.info(`Created positioning index on ${tableName}`, { columns: indexColumnList });
134
251
  }
135
252
  } catch (error) {
136
253
  log.warn(`Could not create positioning index for ${scopeName}:`, error.message);
@@ -160,33 +277,44 @@ export const PositioningPlugin = {
160
277
 
161
278
  // Check if resource has position field
162
279
  const scope = scopes[scopeName];
163
- const hasPositionField = scope?.vars?.schemaInfo?.schema?.structure?.[vars.positioning.field];
280
+ const hasPositionField = scope?.vars?.schemaInfo?.schemaStructure?.[vars.positioning.field];
164
281
 
165
282
  if (!hasPositionField) {
166
283
  return;
167
284
  }
168
285
 
169
286
  // Extract beforeId from JSON:API format attributes
170
- const attributes = context.inputRecord?.data?.attributes || {};
287
+ const attributes = context.inputRecord?.data?.attributes
288
+ || (context.simplified && context.inputRecord && typeof context.inputRecord === 'object'
289
+ ? context.inputRecord
290
+ : {});
171
291
  const beforeId = attributes[vars.positioning.beforeIdField];
172
292
 
173
293
  // Store beforeId in context for later use
174
294
  if (beforeId !== undefined) {
175
295
  context.positioningBeforeId = beforeId;
176
-
296
+
177
297
  // Remove beforeId from attributes so it doesn't get stored
178
298
  delete attributes[vars.positioning.beforeIdField];
299
+
300
+ if (context.simplified && context.inputRecord && typeof context.inputRecord === 'object') {
301
+ delete context.inputRecord[vars.positioning.beforeIdField];
302
+ }
179
303
  }
180
-
304
+
181
305
  // Remove any manually provided position field - position is managed by this plugin
182
306
  if (attributes[vars.positioning.field] !== undefined) {
183
307
  delete attributes[vars.positioning.field];
184
-
308
+
185
309
  log.debug('Positioning beforeId extracted', {
186
310
  scopeName,
187
311
  beforeId,
188
312
  method
189
313
  });
314
+
315
+ if (context.simplified && context.inputRecord && typeof context.inputRecord === 'object') {
316
+ delete context.inputRecord[vars.positioning.field];
317
+ }
190
318
  }
191
319
 
192
320
  // For new records without explicit position, mark for positioning
@@ -242,8 +370,15 @@ export const PositioningPlugin = {
242
370
  return;
243
371
  }
244
372
 
245
- // Always work with JSON:API format - get attributes
246
- const recordData = context.inputRecord?.data?.attributes || {};
373
+ const recordData = (() => {
374
+ if (context.inputRecord?.data?.attributes) {
375
+ return { ...context.inputRecord.data.attributes };
376
+ }
377
+ if (context.simplified && context.inputRecord && typeof context.inputRecord === 'object') {
378
+ return { ...context.inputRecord };
379
+ }
380
+ return {};
381
+ })();
247
382
 
248
383
  log.debug('*** Record data for positioning', {
249
384
  recordData,
@@ -251,41 +386,70 @@ export const PositioningPlugin = {
251
386
  inputRecord: context.inputRecord
252
387
  });
253
388
 
389
+ const jsonApiRecord = context.inputRecord?.data || {};
390
+ const inputRelationships = jsonApiRecord.relationships || {};
391
+ const simplifiedRecord = context.simplified && context.inputRecord && typeof context.inputRecord === 'object'
392
+ ? context.inputRecord
393
+ : {};
394
+
395
+ const minimalAttributes = context.minimalRecord?.attributes || {};
396
+ const minimalRelationships = context.minimalRecord?.relationships || {};
397
+
398
+ const combinedAttributes = {
399
+ ...minimalAttributes,
400
+ ...recordData,
401
+ };
254
402
 
255
- // For PATCH/PUT, we need to get the current record's filter values
256
- let filterData = recordData;
257
- if ((context.method === 'patch' || context.method === 'put') && context.minimalRecord) {
258
- // Use the minimalRecord that was already fetched by REST API plugin
259
- // Merge current record's filter fields with the update data
260
- filterData = { ...context.minimalRecord, ...recordData };
261
- }
262
-
263
403
  // Build filter conditions for the position group
264
- const filterConditions = buildPositionFilterConditions(filterData, context.schemaInfo);
404
+ const schemaInfo = context.schemaInfo;
405
+ const adapter = context.storageAdapter || resolveStorageAdapter(scopeName);
406
+
407
+ if (!adapter) {
408
+ log.warn('*** Positioning skipped - no storage adapter available', { scopeName });
409
+ return;
410
+ }
265
411
 
412
+ const filterConditions = buildPositionFilterConditions(adapter, schemaInfo, {
413
+ attributes: combinedAttributes,
414
+ rawRecord: recordData,
415
+ simplifiedRecord,
416
+ inputRelationships,
417
+ minimalRelationships,
418
+ });
419
+
420
+ log.debug('*** Position filter conditions', { scopeName, filterConditions });
421
+
266
422
  // Query existing items in the same position group
267
423
  const knex = api.knex.instance;
268
- const tableName = context.schemaInfo.tableName;
269
- const idProperty = context.schemaInfo.idProperty;
424
+ const tableName = adapter.getTableName();
425
+ const idProperty = schemaInfo.idProperty;
426
+ const idColumn = adapter.getIdColumn();
427
+ const positionColumn = adapter.translateColumn(vars.positioning.field);
270
428
 
271
- log.debug('*** Building position query', { tableName, idProperty, filterConditions });
429
+ log.debug('*** Building position query', {
430
+ tableName,
431
+ idProperty,
432
+ idColumn,
433
+ positionField: vars.positioning.field,
434
+ positionColumn,
435
+ filterConditions
436
+ });
272
437
 
273
438
  // Check if we have a transaction to use
274
- const db = context.transaction || knex;
275
-
276
439
  // Build base query with filter conditions
277
- const baseQuery = db(tableName);
278
- Object.entries(filterConditions).forEach(([field, value]) => {
440
+ const baseQuery = adapter.buildBaseQuery({ transaction: context.transaction });
441
+ Object.entries(filterConditions).forEach(([column, value]) => {
279
442
  if (value === null) {
280
- baseQuery.whereNull(field);
443
+ baseQuery.whereNull(column);
281
444
  } else {
282
- baseQuery.where(field, value);
445
+ baseQuery.where(column, value);
283
446
  }
284
447
  });
285
-
448
+
286
449
  // For updates, exclude the current record
287
450
  if ((context.method === 'patch' || context.method === 'put') && context.id) {
288
- baseQuery.whereNot(idProperty, context.id);
451
+ const translatedId = adapter.translateFilterValue(idProperty, context.id);
452
+ baseQuery.whereNot(idColumn, translatedId);
289
453
  }
290
454
 
291
455
  // Calculate effective beforeId first
@@ -302,13 +466,22 @@ export const PositioningPlugin = {
302
466
 
303
467
  let items = [];
304
468
 
469
+ const selectColumns = {
470
+ [idProperty]: idColumn,
471
+ [vars.positioning.field]: positionColumn,
472
+ };
473
+ const selectWithAliases = (builder) => adapter.selectColumns(builder, selectColumns);
474
+
305
475
  try {
476
+ log.debug('*** Positioning query start', {
477
+ effectiveBeforeId,
478
+ selectColumns,
479
+ });
306
480
  if (effectiveBeforeId === null) {
307
481
  // Positioning at end - only get the last item
308
- const lastItem = await baseQuery
309
- .clone()
310
- .select(idProperty, vars.positioning.field)
311
- .orderBy(vars.positioning.field, 'desc')
482
+ const lastItem = await selectWithAliases(baseQuery
483
+ .clone())
484
+ .orderBy(positionColumn, 'desc')
312
485
  .first();
313
486
 
314
487
  if (lastItem) {
@@ -316,10 +489,9 @@ export const PositioningPlugin = {
316
489
  }
317
490
  } else if (effectiveBeforeId === 'FIRST') {
318
491
  // Positioning at beginning - get the first item
319
- const firstItem = await baseQuery
320
- .clone()
321
- .select(idProperty, vars.positioning.field)
322
- .orderBy(vars.positioning.field, 'asc')
492
+ const firstItem = await selectWithAliases(baseQuery
493
+ .clone())
494
+ .orderBy(positionColumn, 'asc')
323
495
  .first();
324
496
 
325
497
  if (firstItem) {
@@ -330,19 +502,18 @@ export const PositioningPlugin = {
330
502
  }
331
503
  } else if (effectiveBeforeId) {
332
504
  // Positioning before a specific item - get that item and the one before it
333
- const targetItem = await baseQuery
334
- .clone()
335
- .select(idProperty, vars.positioning.field)
336
- .where(idProperty, effectiveBeforeId)
505
+ const normalizedTargetId = adapter.translateFilterValue(idProperty, effectiveBeforeId);
506
+ const targetItem = await selectWithAliases(baseQuery
507
+ .clone())
508
+ .where(idColumn, normalizedTargetId)
337
509
  .first();
338
510
 
339
511
  if (targetItem) {
340
512
  // Get the item immediately before the target
341
- const prevItem = await baseQuery
342
- .clone()
343
- .select(idProperty, vars.positioning.field)
344
- .where(vars.positioning.field, '<', targetItem[vars.positioning.field])
345
- .orderBy(vars.positioning.field, 'desc')
513
+ const prevItem = await selectWithAliases(baseQuery
514
+ .clone())
515
+ .where(positionColumn, '<', targetItem[vars.positioning.field])
516
+ .orderBy(positionColumn, 'desc')
346
517
  .first();
347
518
 
348
519
  items = prevItem ? [prevItem, targetItem] : [targetItem];
@@ -352,7 +523,7 @@ export const PositioningPlugin = {
352
523
  items = [];
353
524
  }
354
525
 
355
- log.debug('*** Query completed', { itemCount: items.length });
526
+ log.debug('*** Query completed', { itemCount: items.length, items });
356
527
  } catch (error) {
357
528
  log.error('*** Query failed', { error: error.message, stack: error.stack });
358
529
  throw error;
@@ -363,9 +534,15 @@ export const PositioningPlugin = {
363
534
  ? getInitialPosition()
364
535
  : calculatePosition(items, effectiveBeforeId, idProperty, vars.positioning.field);
365
536
 
366
- // Set the position in JSON:API format
367
- context.inputRecord.data.attributes = context.inputRecord.data.attributes || {};
368
- context.inputRecord.data.attributes[vars.positioning.field] = newPosition;
537
+ // Set the position on the incoming payload so downstream steps persist it
538
+ if (context.inputRecord?.data) {
539
+ context.inputRecord.data.attributes = context.inputRecord.data.attributes || {};
540
+ context.inputRecord.data.attributes[vars.positioning.field] = newPosition;
541
+ }
542
+
543
+ if (context.simplified && context.inputRecord && typeof context.inputRecord === 'object') {
544
+ context.inputRecord[vars.positioning.field] = newPosition;
545
+ }
369
546
 
370
547
  log.debug('Position calculated', {
371
548
  scopeName,
@@ -489,4 +666,4 @@ await api.use(PositioningPlugin, {
489
666
  "listId": 456,
490
667
  "beforeId": null // Explicit last position
491
668
  }
492
- */
669
+ */