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.
- package/.claude/settings.local.json +9 -1
- package/AGENTS.md +34 -0
- package/CLAUDE.md +26 -1
- package/CRUSH.md +55 -0
- package/README.md +3 -3
- package/TODO.todo +8 -0
- package/docs/API.md +17 -18
- package/docs/COMPARISON.md +18 -13
- package/docs/GUIDE/GUIDE_1_Initial_Setup.md +17 -21
- package/docs/GUIDE/GUIDE_2_1_The_Starting_Point.md +33 -2
- package/docs/GUIDE/GUIDE_2_2_Manipulating_And_Searching_Tables.md +61 -7
- package/docs/GUIDE/GUIDE_2_3_BelongsTo_Relationships.md +58 -61
- package/docs/GUIDE/GUIDE_2_4_HasMany_Records.md +37 -37
- package/docs/GUIDE/GUIDE_2_5_HasMany_Polymorphic.md +103 -87
- package/docs/GUIDE/GUIDE_2_6_Many_To_Many.md +37 -34
- package/docs/GUIDE/GUIDE_2_7_Pagination_And_Ordering.md +45 -42
- package/docs/GUIDE/GUIDE_2_8_Effects_of_PUT_and_PATCH.md +3 -3
- package/docs/GUIDE/GUIDE_2_Data_And_Relations.md +1 -1
- package/docs/GUIDE/GUIDE_3_Field_Transformations.md +5 -5
- package/docs/GUIDE/GUIDE_4_Authentication_And_Permissions.md +498 -755
- package/docs/GUIDE/GUIDE_6_Positioning.md +1 -3
- package/docs/GUIDE/GUIDE_7_Hooks_Data_Management_And_Plugins.md +4 -5
- package/docs/GUIDE/GUIDE_X_Bulk_Operations.md +15 -6
- package/docs/GUIDE/GUIDE_X_Cors.md +2 -5
- package/docs/GUIDE/GUIDE_X_File_Uploads.md +2 -3
- package/docs/GUIDE/GUIDE_X_Multihome.md +1 -4
- package/docs/GUIDE/GUIDE_X_SocketIO.md +0 -2
- package/docs/GUIDE/GUIDE_X_URL_Management.md +317 -0
- package/docs/GUIDE/index.md +3 -0
- package/docs/QUICKSTART.md +12 -12
- package/docs/index.md +2 -2
- package/docs/mini-guides/file-uploads-complete.md +0 -1
- package/examples/url-override-example.js +137 -0
- package/index.js +7 -1
- package/package.json +7 -11
- package/plugins/core/bulk-operations-plugin.js +2 -0
- package/plugins/core/connectors/express-plugin.js +43 -24
- package/plugins/core/connectors/lib/request-helpers.js +2 -2
- package/plugins/core/lib/anyapi/anyapi-registry.js +821 -0
- package/plugins/core/lib/anyapi/query/anyapi-query-adapter.js +647 -0
- package/plugins/core/lib/anyapi/schema-utils.js +186 -0
- package/plugins/core/lib/anyapi/utils/descriptor-helpers.js +113 -0
- package/plugins/core/lib/dbTablesOperations.js +305 -0
- package/plugins/core/lib/querying/knex-cross-table-search.js +64 -108
- package/plugins/core/lib/querying/knex-json-api-transformers-querying.js +21 -27
- package/plugins/core/lib/querying/knex-pagination-helpers.js +36 -20
- package/plugins/core/lib/querying/knex-query-helpers-base.js +49 -14
- package/plugins/core/lib/querying/knex-query-helpers.js +378 -222
- package/plugins/core/lib/querying/knex-relationship-includes.js +262 -87
- package/plugins/core/lib/querying/url-helpers.js +119 -0
- package/plugins/core/lib/querying-writing/compile-schemas.js +36 -28
- package/plugins/core/lib/querying-writing/database-capabilities.js +10 -9
- package/plugins/core/lib/querying-writing/knex-field-helpers.js +9 -3
- package/plugins/core/lib/querying-writing/knex-json-api-transformers.js +2 -2
- package/plugins/core/lib/querying-writing/payload-validators.js +10 -16
- package/plugins/core/lib/querying-writing/schema-helpers.js +52 -60
- package/plugins/core/lib/querying-writing/scope-validations.js +78 -22
- package/plugins/core/lib/querying-writing/simplified-helpers.js +208 -61
- package/plugins/core/lib/storage/storage-adapter.js +160 -0
- package/plugins/core/lib/writing/relationship-processor.js +4 -11
- package/plugins/core/rest-api-access.js +456 -0
- package/plugins/core/rest-api-anyapi-knex-plugin.js +2285 -0
- package/plugins/core/rest-api-knex-plugin.js +226 -87
- package/plugins/core/rest-api-multihome-plugin.js +89 -27
- package/plugins/core/rest-api-plugin-hooks/turn-scope-init-into-vars.js +0 -1
- package/plugins/core/rest-api-plugin-methods/check-permissions.js +1 -5
- package/plugins/core/rest-api-plugin-methods/common.js +46 -10
- package/plugins/core/rest-api-plugin-methods/delete-relationship.js +31 -27
- package/plugins/core/rest-api-plugin-methods/delete.js +1 -4
- package/plugins/core/rest-api-plugin-methods/enrich-attributes.js +13 -1
- package/plugins/core/rest-api-plugin-methods/get-related.js +114 -56
- package/plugins/core/rest-api-plugin-methods/get-relationship.js +4 -3
- package/plugins/core/rest-api-plugin-methods/get.js +2 -5
- package/plugins/core/rest-api-plugin-methods/patch.js +15 -16
- package/plugins/core/rest-api-plugin-methods/post-relationship.js +21 -14
- package/plugins/core/rest-api-plugin-methods/post.js +23 -11
- package/plugins/core/rest-api-plugin-methods/put.js +18 -21
- package/plugins/core/rest-api-plugin-methods/query.js +3 -4
- package/plugins/core/rest-api-plugin-methods/release.js +13 -0
- package/plugins/core/rest-api-plugin.js +3 -4
- package/plugins/core/rest-api-positioning-plugin.js +253 -76
- package/plugins/core/socketio-plugin.js +436 -502
- package/quickTest.js +2 -2
- package/scripts/debug-anyapi-review-filter.mjs +63 -0
- package/scripts/inspect-anyapi-descriptor-country.mjs +19 -0
- package/scripts/inspect-anyapi-descriptor.mjs +19 -0
- package/test-debug.js +59 -0
- package/tests/README.md +7 -1
- package/tests/anyapi-basic.test.js +102 -0
- package/tests/anyapi-cursor-pagination.test.js +144 -0
- package/tests/computed-fields.test.js +27 -12
- package/tests/cors.test.js +5 -6
- package/tests/curl.test.js +1 -1
- package/tests/custom-idproperty-relationships.test.js +37 -29
- package/tests/field-getters.test.js +25 -7
- package/tests/fixtures/api-configs.js +576 -150
- package/tests/helpers/storage-mode.js +65 -0
- package/tests/helpers/test-utils.js +85 -3
- package/tests/include-depth-validation.test.js +35 -1
- package/tests/nested-includes.test.js +130 -103
- package/tests/pagination-cursor-multifield.test.js +17 -13
- package/tests/pagination-enhanced.test.js +19 -24
- package/tests/polymorphic-simple.test.js +9 -9
- package/tests/positioning-advanced.test.js +19 -19
- package/tests/positioning-extended.test.js +25 -25
- package/tests/positioning-multifilter.test.js +62 -62
- package/tests/positioning.test.js +7 -7
- package/tests/queries.test.js +5 -5
- package/tests/query-limits.test.js +77 -92
- package/tests/rest-api-access.test.js +206 -0
- package/tests/searchschema-merge.test.js +182 -0
- package/tests/socketio.test.js +33 -14
- package/tests/virtual-fields.test.js +0 -1
- package/docs/DOCS_ALL.md +0 -21894
- package/plugins/core/jwt-auth-plugin.js +0 -897
- package/plugins/core/lib/jwt-auth-helpers.js +0 -130
- 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(
|
|
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
|
-
|
|
50
|
-
|
|
88
|
+
attributeKeys: Object.keys(attributes || {}),
|
|
89
|
+
rawKeys: Object.keys(rawRecord || {}),
|
|
90
|
+
simplifiedKeys: Object.keys(simplifiedRecord || {}),
|
|
51
91
|
});
|
|
52
|
-
|
|
53
|
-
|
|
54
|
-
|
|
55
|
-
|
|
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
|
|
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 =
|
|
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:
|
|
118
|
-
const indexColumns =
|
|
119
|
-
|
|
120
|
-
|
|
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,
|
|
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(
|
|
247
|
+
table.index(indexColumnList, indexName);
|
|
131
248
|
});
|
|
132
249
|
|
|
133
|
-
log.info(`Created positioning index on ${tableName}`, { columns:
|
|
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?.
|
|
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
|
-
|
|
246
|
-
|
|
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
|
|
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 =
|
|
269
|
-
const 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', {
|
|
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 =
|
|
278
|
-
Object.entries(filterConditions).forEach(([
|
|
440
|
+
const baseQuery = adapter.buildBaseQuery({ transaction: context.transaction });
|
|
441
|
+
Object.entries(filterConditions).forEach(([column, value]) => {
|
|
279
442
|
if (value === null) {
|
|
280
|
-
baseQuery.whereNull(
|
|
443
|
+
baseQuery.whereNull(column);
|
|
281
444
|
} else {
|
|
282
|
-
baseQuery.where(
|
|
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
|
-
|
|
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
|
-
.
|
|
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
|
-
.
|
|
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
|
|
334
|
-
|
|
335
|
-
.
|
|
336
|
-
.where(
|
|
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
|
-
.
|
|
344
|
-
.
|
|
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
|
|
367
|
-
|
|
368
|
-
|
|
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
|
+
*/
|