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.
Files changed (118) hide show
  1. package/connectors_ideas/fastify.js +66 -68
  2. package/connectors_ideas/koa-plugin.js +50 -51
  3. package/eslint.config.js +11 -2
  4. package/examples/url-override-example.js +63 -63
  5. package/index.js +13 -13
  6. package/lib/enhanced-logger.js +51 -51
  7. package/lib/error-formatter.js +61 -61
  8. package/lib/rest-api-errors.js +28 -28
  9. package/package.json +1 -1
  10. package/plugins/core/bulk-operations-plugin.js +142 -145
  11. package/plugins/core/connectors/express-plugin.js +210 -217
  12. package/plugins/core/connectors/lib/busboy-detector.js +84 -84
  13. package/plugins/core/connectors/lib/formidable-detector.js +50 -50
  14. package/plugins/core/connectors/lib/request-helpers.js +74 -74
  15. package/plugins/core/file-handling-plugin.js +125 -125
  16. package/plugins/core/lib/anyapi/anyapi-registry.js +254 -254
  17. package/plugins/core/lib/anyapi/query/anyapi-query-adapter.js +287 -287
  18. package/plugins/core/lib/anyapi/schema-utils.js +101 -101
  19. package/plugins/core/lib/anyapi/utils/descriptor-helpers.js +41 -41
  20. package/plugins/core/lib/dbTablesOperations.js +134 -135
  21. package/plugins/core/lib/fractional-positioning.js +74 -76
  22. package/plugins/core/lib/querying/knex-cross-table-search.js +320 -320
  23. package/plugins/core/lib/querying/knex-json-api-transformers-querying.js +144 -144
  24. package/plugins/core/lib/querying/knex-pagination-helpers.js +164 -164
  25. package/plugins/core/lib/querying/knex-process-includes.js +50 -50
  26. package/plugins/core/lib/querying/knex-query-helpers-base.js +31 -31
  27. package/plugins/core/lib/querying/knex-query-helpers.js +451 -452
  28. package/plugins/core/lib/querying/knex-relationship-includes.js +1160 -1170
  29. package/plugins/core/lib/querying/knex-window-queries.js +89 -89
  30. package/plugins/core/lib/querying/url-helpers.js +35 -35
  31. package/plugins/core/lib/querying-writing/compile-schemas.js +99 -99
  32. package/plugins/core/lib/querying-writing/connectors-query-parser.js +31 -31
  33. package/plugins/core/lib/querying-writing/database-capabilities.js +94 -94
  34. package/plugins/core/lib/querying-writing/database-value-normalizers.js +76 -76
  35. package/plugins/core/lib/querying-writing/default-data-helpers.js +27 -27
  36. package/plugins/core/lib/querying-writing/field-utils.js +44 -42
  37. package/plugins/core/lib/querying-writing/knex-constants.js +15 -16
  38. package/plugins/core/lib/querying-writing/knex-field-helpers.js +117 -115
  39. package/plugins/core/lib/querying-writing/knex-json-api-transformers.js +41 -41
  40. package/plugins/core/lib/querying-writing/payload-validators.js +275 -275
  41. package/plugins/core/lib/querying-writing/schema-helpers.js +129 -129
  42. package/plugins/core/lib/querying-writing/scope-validations.js +143 -146
  43. package/plugins/core/lib/querying-writing/simplified-helpers.js +133 -129
  44. package/plugins/core/lib/storage/storage-adapter.js +68 -68
  45. package/plugins/core/lib/writing/knex-json-api-transformers-writing.js +31 -31
  46. package/plugins/core/lib/writing/many-to-many-manipulations.js +81 -81
  47. package/plugins/core/lib/writing/relationship-processor.js +58 -58
  48. package/plugins/core/rest-api-access.js +11 -15
  49. package/plugins/core/rest-api-anyapi-knex-plugin.js +920 -920
  50. package/plugins/core/rest-api-cors-plugin.js +170 -171
  51. package/plugins/core/rest-api-knex-plugin.js +482 -493
  52. package/plugins/core/rest-api-multihome-plugin.js +247 -251
  53. package/plugins/core/rest-api-plugin-hooks/compile-resource-schemas.js +5 -5
  54. package/plugins/core/rest-api-plugin-hooks/register-relationship-routes.js +20 -21
  55. package/plugins/core/rest-api-plugin-hooks/register-scope-routes.js +44 -44
  56. package/plugins/core/rest-api-plugin-hooks/turn-scope-init-into-vars.js +23 -23
  57. package/plugins/core/rest-api-plugin-hooks/validate-include-configurations.js +15 -15
  58. package/plugins/core/rest-api-plugin-methods/add-route.js +9 -9
  59. package/plugins/core/rest-api-plugin-methods/check-permissions.js +5 -6
  60. package/plugins/core/rest-api-plugin-methods/common.js +295 -297
  61. package/plugins/core/rest-api-plugin-methods/delete-relationship.js +35 -37
  62. package/plugins/core/rest-api-plugin-methods/delete.js +57 -59
  63. package/plugins/core/rest-api-plugin-methods/enrich-attributes.js +172 -171
  64. package/plugins/core/rest-api-plugin-methods/get-related.js +96 -96
  65. package/plugins/core/rest-api-plugin-methods/get-relationship.js +16 -18
  66. package/plugins/core/rest-api-plugin-methods/get.js +86 -86
  67. package/plugins/core/rest-api-plugin-methods/patch-relationship.js +16 -17
  68. package/plugins/core/rest-api-plugin-methods/patch.js +63 -64
  69. package/plugins/core/rest-api-plugin-methods/post-relationship.js +54 -55
  70. package/plugins/core/rest-api-plugin-methods/post.js +61 -62
  71. package/plugins/core/rest-api-plugin-methods/put.js +86 -87
  72. package/plugins/core/rest-api-plugin-methods/query.js +88 -88
  73. package/plugins/core/rest-api-plugin-methods/release.js +5 -6
  74. package/plugins/core/rest-api-plugin.js +76 -78
  75. package/plugins/core/rest-api-positioning-plugin.js +251 -252
  76. package/plugins/core/socketio-plugin.js +240 -236
  77. package/plugins/storage/local-storage.js +103 -103
  78. package/plugins/storage/s3-storage.js +41 -42
  79. package/quickTest.js +37 -37
  80. package/scripts/debug-anyapi-review-filter.mjs +18 -18
  81. package/scripts/inspect-anyapi-descriptor-country.mjs +10 -10
  82. package/scripts/inspect-anyapi-descriptor.mjs +10 -10
  83. package/scripts/run-docs.js +112 -113
  84. package/test-debug.js +28 -28
  85. package/tests/TEST_TEMPLATE.test.js +51 -51
  86. package/tests/anyapi-basic.test.js +42 -42
  87. package/tests/anyapi-cursor-pagination.test.js +48 -48
  88. package/tests/bulk-operations.test.js +208 -208
  89. package/tests/computed-fields.test.js +204 -204
  90. package/tests/cors.test.js +412 -413
  91. package/tests/curl.test.js +275 -276
  92. package/tests/custom-idproperty-relationships.test.js +266 -266
  93. package/tests/field-getters.test.js +132 -132
  94. package/tests/field-setters.test.js +110 -110
  95. package/tests/fixtures/api-configs.js +699 -699
  96. package/tests/helpers/storage-mode.js +32 -32
  97. package/tests/helpers/test-utils.js +76 -76
  98. package/tests/include-depth-validation.test.js +76 -76
  99. package/tests/includes.test.js +145 -145
  100. package/tests/multihome.test.js +175 -175
  101. package/tests/nested-includes.test.js +255 -255
  102. package/tests/pagination-cursor-multifield.test.js +164 -164
  103. package/tests/pagination-enhanced.test.js +225 -225
  104. package/tests/polymorphic-relationships.test.js +96 -96
  105. package/tests/polymorphic-simple.test.js +44 -44
  106. package/tests/positioning-advanced.test.js +226 -226
  107. package/tests/positioning-extended.test.js +399 -399
  108. package/tests/positioning-multifilter.test.js +204 -204
  109. package/tests/positioning.test.js +266 -266
  110. package/tests/queries.test.js +184 -184
  111. package/tests/query-limits.test.js +154 -155
  112. package/tests/relationship-endpoints.test.js +127 -127
  113. package/tests/relationships.test.js +178 -179
  114. package/tests/rest-api-access.test.js +86 -86
  115. package/tests/rest-api.test.js +103 -103
  116. package/tests/searchschema-merge.test.js +93 -93
  117. package/tests/socketio.test.js +326 -326
  118. package/tests/virtual-fields.test.js +164 -164
@@ -9,50 +9,50 @@
9
9
  * @param {object} definition - The schema definition for the field
10
10
  * @returns {object} The Knex column builder instance
11
11
  */
12
- function mapTypeToKnex(table, columnName, definition) {
13
- const { type, precision, scale, maxLength, unsigned } = definition;
14
-
12
+ function mapTypeToKnex (table, columnName, definition) {
13
+ const { type, precision, scale, maxLength, unsigned } = definition
14
+
15
15
  switch (type) {
16
16
  case 'string':
17
- return maxLength ? table.string(columnName, maxLength) : table.string(columnName);
18
-
17
+ return maxLength ? table.string(columnName, maxLength) : table.string(columnName)
18
+
19
19
  case 'number':
20
20
  if (precision !== undefined && scale !== undefined) {
21
- return table.decimal(columnName, precision, scale);
21
+ return table.decimal(columnName, precision, scale)
22
22
  }
23
- return table.float(columnName);
24
-
23
+ return table.float(columnName)
24
+
25
25
  case 'id':
26
- const col = table.integer(columnName);
27
- return unsigned !== false ? col.unsigned() : col;
28
-
26
+ const col = table.integer(columnName)
27
+ return unsigned !== false ? col.unsigned() : col
28
+
29
29
  case 'boolean':
30
- return table.boolean(columnName);
31
-
30
+ return table.boolean(columnName)
31
+
32
32
  case 'date':
33
- return table.date(columnName);
34
-
33
+ return table.date(columnName)
34
+
35
35
  case 'dateTime':
36
- return table.datetime(columnName);
37
-
36
+ return table.datetime(columnName)
37
+
38
38
  case 'time':
39
- return table.time(columnName);
40
-
39
+ return table.time(columnName)
40
+
41
41
  case 'timestamp':
42
- return table.integer(columnName);
43
-
42
+ return table.integer(columnName)
43
+
44
44
  case 'array':
45
45
  case 'object':
46
46
  case 'serialize':
47
- return table.json(columnName);
48
-
47
+ return table.json(columnName)
48
+
49
49
  case 'blob':
50
50
  case 'file':
51
- return table.binary(columnName);
52
-
51
+ return table.binary(columnName)
52
+
53
53
  case 'none':
54
54
  default:
55
- return table.string(columnName);
55
+ return table.string(columnName)
56
56
  }
57
57
  }
58
58
 
@@ -61,47 +61,47 @@ function mapTypeToKnex(table, columnName, definition) {
61
61
  * @param {object} column - The Knex column builder instance
62
62
  * @param {object} definition - The schema definition for the field
63
63
  */
64
- function applyConstraints(column, definition) {
64
+ function applyConstraints (column, definition) {
65
65
  // Nullability
66
66
  if (definition.nullable === true) {
67
- column.nullable();
67
+ column.nullable()
68
68
  } else if (definition.required === true || definition.nullable === false) {
69
- column.notNullable();
69
+ column.notNullable()
70
70
  }
71
-
71
+
72
72
  // Default value
73
73
  if (definition.defaultTo !== undefined) {
74
- column.defaultTo(definition.defaultTo);
74
+ column.defaultTo(definition.defaultTo)
75
75
  }
76
-
76
+
77
77
  // Database-specific constraints
78
78
  if (definition.unique === true) {
79
- column.unique();
79
+ column.unique()
80
80
  }
81
-
81
+
82
82
  if (definition.primary === true) {
83
- column.primary();
83
+ column.primary()
84
84
  }
85
-
85
+
86
86
  if (definition.index === true) {
87
- column.index();
87
+ column.index()
88
88
  }
89
-
89
+
90
90
  if (definition.references) {
91
91
  column.references(definition.references.column || 'id')
92
- .inTable(definition.references.table);
93
-
92
+ .inTable(definition.references.table)
93
+
94
94
  if (definition.references.onDelete) {
95
- column.onDelete(definition.references.onDelete);
95
+ column.onDelete(definition.references.onDelete)
96
96
  }
97
-
97
+
98
98
  if (definition.references.onUpdate) {
99
- column.onUpdate(definition.references.onUpdate);
99
+ column.onUpdate(definition.references.onUpdate)
100
100
  }
101
101
  }
102
-
102
+
103
103
  if (definition.comment) {
104
- column.comment(definition.comment);
104
+ column.comment(definition.comment)
105
105
  }
106
106
  }
107
107
 
@@ -116,9 +116,8 @@ function applyConstraints(column, definition) {
116
116
  * @param {boolean} [options.timestamps=false] - Whether to add created_at/updated_at columns
117
117
  * @returns {Promise} A promise that resolves when the table is created
118
118
  */
119
- export async function createKnexTable(knex, schemaInfo, tableSchemaInstance, options = {}) {
120
- const { autoIncrement = true, timestamps = false } = options;
121
-
119
+ export async function createKnexTable (knex, schemaInfo, tableSchemaInstance, options = {}) {
120
+ const { autoIncrement = true, timestamps = false } = options
122
121
 
123
122
  const tableName = schemaInfo.tableName
124
123
  const idProperty = schemaInfo.idProperty
@@ -126,57 +125,57 @@ export async function createKnexTable(knex, schemaInfo, tableSchemaInstance, op
126
125
 
127
126
  return knex.schema.createTable(tableName, (table) => {
128
127
  // Check if schema has the idProperty field with primary key
129
- const hasIdField = tableSchemaStructure[idProperty] && tableSchemaStructure[idProperty].primary === true;
130
-
128
+ const hasIdField = tableSchemaStructure[idProperty] && tableSchemaStructure[idProperty].primary === true
129
+
131
130
  // Add auto-incrementing ID if no primary key is defined and autoIncrement is true
132
131
  if (!hasIdField && autoIncrement) {
133
- table.increments(idProperty).primary();
132
+ table.increments(idProperty).primary()
134
133
  }
135
-
134
+
136
135
  // Process each field in the schema
137
136
  for (const [fieldName, definition] of Object.entries(tableSchemaStructure)) {
138
137
  // Skip if this is the ID field and we already handled it
139
138
  if (fieldName === idProperty && !hasIdField && autoIncrement) {
140
- continue;
139
+ continue
141
140
  }
142
-
141
+
143
142
  // Create the column with the appropriate type
144
- const column = mapTypeToKnex(table, fieldName, definition);
145
-
143
+ const column = mapTypeToKnex(table, fieldName, definition)
144
+
146
145
  // Apply constraints
147
- applyConstraints(column, definition);
146
+ applyConstraints(column, definition)
148
147
  }
149
-
148
+
150
149
  // Add timestamps if requested
151
150
  if (timestamps) {
152
- table.timestamps(true, true);
151
+ table.timestamps(true, true)
153
152
  }
154
- });
153
+ })
155
154
  }
156
155
 
157
- export async function addKnexFields(knex, tableName, schema) {
156
+ export async function addKnexFields (knex, tableName, schema) {
158
157
  return knex.schema.alterTable(tableName, (table) => {
159
- for (const [fieldName, definition] of Object.entries(schema.structure)) {
160
- const column = mapTypeToKnex(table, fieldName, definition);
161
- applyConstraints(column, definition);
158
+ for (const [fieldName, definition] of Object.entries(schema.structure)) {
159
+ const column = mapTypeToKnex(table, fieldName, definition)
160
+ applyConstraints(column, definition)
162
161
  }
163
- });
162
+ })
164
163
  }
165
164
 
166
165
  // Helper function to alter multiple fields in an existing table
167
- export async function alterKnexFields(knex, tableName, fields, options = {}) {
166
+ export async function alterKnexFields (knex, tableName, fields, options = {}) {
168
167
  return knex.schema.alterTable(tableName, (table) => {
169
168
  for (const [fieldName, definition] of Object.entries(fields)) {
170
169
  // Create the column with alter flag
171
- const column = mapTypeToKnex(table, fieldName, definition);
170
+ const column = mapTypeToKnex(table, fieldName, definition)
172
171
 
173
172
  // Mark this as an alteration
174
- column.alter();
173
+ column.alter()
175
174
 
176
175
  // Apply constraints (these will be reapplied with the alter)
177
- applyConstraints(column, definition);
176
+ applyConstraints(column, definition)
178
177
  }
179
- });
178
+ })
180
179
  }
181
180
 
182
181
  /**
@@ -186,120 +185,120 @@ export async function alterKnexFields(knex, tableName, fields, options = {}) {
186
185
  * @param {object} [options={}] - Additional options
187
186
  * @returns {string} The migration code as a string
188
187
  */
189
- export function generateKnexMigration(tableName, schema, options = {}) {
190
- const { autoIncrement = true, timestamps = false } = options;
191
-
188
+ export function generateKnexMigration (tableName, schema, options = {}) {
189
+ const { autoIncrement = true, timestamps = false } = options
190
+
192
191
  let migration = `exports.up = function(knex) {
193
- return knex.schema.createTable('${tableName}', (table) => {\n`;
194
-
192
+ return knex.schema.createTable('${tableName}', (table) => {\n`
193
+
195
194
  // Check if schema has an 'id' field with primary key
196
- const hasIdField = schema.structure.id && schema.structure.id.primary === true;
197
-
195
+ const hasIdField = schema.structure.id && schema.structure.id.primary === true
196
+
198
197
  // Add auto-incrementing ID if needed
199
198
  if (!hasIdField && autoIncrement) {
200
- migration += ` table.increments('id').primary();\n`;
199
+ migration += ' table.increments(\'id\').primary();\n'
201
200
  }
202
-
201
+
203
202
  // Process each field
204
203
  for (const [fieldName, definition] of Object.entries(schema.structure)) {
205
204
  if (fieldName === 'id' && !hasIdField && autoIncrement) {
206
- continue;
205
+ continue
207
206
  }
208
-
209
- let line = ' ';
210
-
207
+
208
+ let line = ' '
209
+
211
210
  // Map type
212
211
  switch (definition.type) {
213
212
  case 'string':
214
- line += definition.maxLength
213
+ line += definition.maxLength
215
214
  ? `table.string('${fieldName}', ${definition.maxLength})`
216
- : `table.string('${fieldName}')`;
217
- break;
215
+ : `table.string('${fieldName}')`
216
+ break
218
217
  case 'number':
219
218
  if (definition.precision !== undefined && definition.scale !== undefined) {
220
- line += `table.decimal('${fieldName}', ${definition.precision}, ${definition.scale})`;
219
+ line += `table.decimal('${fieldName}', ${definition.precision}, ${definition.scale})`
221
220
  } else {
222
- line += `table.float('${fieldName}')`;
221
+ line += `table.float('${fieldName}')`
223
222
  }
224
- break;
223
+ break
225
224
  case 'id':
226
- line += `table.integer('${fieldName}')`;
227
- if (definition.unsigned !== false) line += '.unsigned()';
228
- break;
225
+ line += `table.integer('${fieldName}')`
226
+ if (definition.unsigned !== false) line += '.unsigned()'
227
+ break
229
228
  case 'boolean':
230
- line += `table.boolean('${fieldName}')`;
231
- break;
229
+ line += `table.boolean('${fieldName}')`
230
+ break
232
231
  case 'date':
233
- line += `table.date('${fieldName}')`;
234
- break;
232
+ line += `table.date('${fieldName}')`
233
+ break
235
234
  case 'dateTime':
236
- line += `table.datetime('${fieldName}')`;
237
- break;
235
+ line += `table.datetime('${fieldName}')`
236
+ break
238
237
  case 'time':
239
- line += `table.time('${fieldName}')`;
240
- break;
238
+ line += `table.time('${fieldName}')`
239
+ break
241
240
  case 'timestamp':
242
- line += `table.integer('${fieldName}')`;
243
- break;
241
+ line += `table.integer('${fieldName}')`
242
+ break
244
243
  case 'array':
245
244
  case 'object':
246
245
  case 'serialize':
247
- line += `table.json('${fieldName}')`;
248
- break;
246
+ line += `table.json('${fieldName}')`
247
+ break
249
248
  case 'blob':
250
249
  case 'file':
251
- line += `table.binary('${fieldName}')`;
252
- break;
250
+ line += `table.binary('${fieldName}')`
251
+ break
253
252
  default:
254
- line += `table.string('${fieldName}')`;
253
+ line += `table.string('${fieldName}')`
255
254
  }
256
-
255
+
257
256
  // Add constraints
258
257
  if (definition.nullable === true) {
259
- line += '.nullable()';
258
+ line += '.nullable()'
260
259
  } else if (definition.required === true || definition.nullable === false) {
261
- line += '.notNullable()';
260
+ line += '.notNullable()'
262
261
  }
263
-
262
+
264
263
  if (definition.defaultTo !== undefined) {
265
- const defaultValue = typeof definition.defaultTo === 'string'
266
- ? `'${definition.defaultTo}'`
267
- : definition.defaultTo;
268
- line += `.defaultTo(${defaultValue})`;
264
+ const defaultValue = typeof definition.defaultTo === 'string'
265
+ ? `'${definition.defaultTo}'`
266
+ : definition.defaultTo
267
+ line += `.defaultTo(${defaultValue})`
269
268
  }
270
-
271
- if (definition.unique === true) line += '.unique()';
272
- if (definition.primary === true) line += '.primary()';
273
- if (definition.index === true) line += '.index()';
274
-
269
+
270
+ if (definition.unique === true) line += '.unique()'
271
+ if (definition.primary === true) line += '.primary()'
272
+ if (definition.index === true) line += '.index()'
273
+
275
274
  if (definition.references) {
276
- const refCol = definition.references.column || 'id';
277
- line += `.references('${refCol}').inTable('${definition.references.table}')`;
275
+ const refCol = definition.references.column || 'id'
276
+ line += `.references('${refCol}').inTable('${definition.references.table}')`
278
277
  if (definition.references.onDelete) {
279
- line += `.onDelete('${definition.references.onDelete}')`;
278
+ line += `.onDelete('${definition.references.onDelete}')`
280
279
  }
281
280
  if (definition.references.onUpdate) {
282
- line += `.onUpdate('${definition.references.onUpdate}')`;
281
+ line += `.onUpdate('${definition.references.onUpdate}')`
283
282
  }
284
283
  }
285
-
284
+
286
285
  if (definition.comment) {
287
- line += `.comment('${definition.comment}')`;
286
+ line += `.comment('${definition.comment}')`
288
287
  }
289
-
290
- migration += line + ';\n';
288
+
289
+ migration += line + ';\n'
291
290
  }
292
-
291
+
293
292
  if (timestamps) {
294
- migration += ` table.timestamps(true, true);\n`;
293
+ migration += ' table.timestamps(true, true);\n'
295
294
  }
296
-
295
+
297
296
  migration += ` });
298
297
  };
299
298
 
300
299
  exports.down = function(knex) {
301
300
  return knex.schema.dropTable('${tableName}');
302
- };`;
303
-
304
- return migration;
305
- }
301
+ };`
302
+
303
+ return migration
304
+ }
@@ -1,11 +1,11 @@
1
- import { requirePackage } from 'hooked-api';
1
+ import { requirePackage } from 'hooked-api'
2
2
 
3
- let generateKeyBetween;
3
+ let generateKeyBetween
4
4
  try {
5
- ({ generateKeyBetween } = await import('fractional-indexing'));
5
+ ({ generateKeyBetween } = await import('fractional-indexing'))
6
6
  } catch (e) {
7
- requirePackage('fractional-indexing', 'positioning',
8
- 'Fractional indexing is required for the positioning plugin to generate sortable position keys. This is a peer dependency.');
7
+ requirePackage('fractional-indexing', 'positioning',
8
+ 'Fractional indexing is required for the positioning plugin to generate sortable position keys. This is a peer dependency.')
9
9
  }
10
10
 
11
11
  /**
@@ -21,55 +21,53 @@ try {
21
21
  * @param {string} positionField - Name of the position field
22
22
  * @returns {string} New position key
23
23
  */
24
- export function calculatePosition(items, beforeId, idField, positionField) {
24
+ export function calculatePosition (items, beforeId, idField, positionField) {
25
25
  // Sort items by position using simple string comparison
26
26
  // Important: fractional-indexing expects ASCII/Unicode ordering, not locale-specific
27
27
  const sorted = [...items].sort((a, b) => {
28
- const posA = a[positionField] || '';
29
- const posB = b[positionField] || '';
30
- return posA < posB ? -1 : posA > posB ? 1 : 0;
31
- });
32
-
28
+ const posA = a[positionField] || ''
29
+ const posB = b[positionField] || ''
30
+ return posA < posB ? -1 : posA > posB ? 1 : 0
31
+ })
33
32
 
34
33
  // If no items, start in the middle
35
34
  if (sorted.length === 0) {
36
- return generateKeyBetween(null, null); // Returns 'a0'
35
+ return generateKeyBetween(null, null) // Returns 'a0'
37
36
  }
38
37
 
39
38
  // Position at end
40
39
  if (beforeId === null || beforeId === undefined) {
41
- const lastItem = sorted[sorted.length - 1];
42
- return generateKeyBetween(lastItem[positionField], null);
40
+ const lastItem = sorted[sorted.length - 1]
41
+ return generateKeyBetween(lastItem[positionField], null)
43
42
  }
44
43
 
45
44
  // Find the item to position before
46
- const beforeIndex = sorted.findIndex(item => String(item[idField]) === String(beforeId));
47
-
45
+ const beforeIndex = sorted.findIndex(item => String(item[idField]) === String(beforeId))
46
+
48
47
  // If beforeId not found, position at end
49
48
  if (beforeIndex === -1) {
50
- const lastItem = sorted[sorted.length - 1];
51
- return generateKeyBetween(lastItem[positionField], null);
49
+ const lastItem = sorted[sorted.length - 1]
50
+ return generateKeyBetween(lastItem[positionField], null)
52
51
  }
53
52
 
54
53
  // Position before the found item
55
- const beforeItem = sorted[beforeIndex];
56
- const prevItem = beforeIndex > 0 ? sorted[beforeIndex - 1] : null;
57
-
54
+ const beforeItem = sorted[beforeIndex]
55
+ const prevItem = beforeIndex > 0 ? sorted[beforeIndex - 1] : null
56
+
58
57
  const result = generateKeyBetween(
59
58
  prevItem ? prevItem[positionField] : null,
60
59
  beforeItem[positionField]
61
- );
60
+ )
62
61
 
63
-
64
- return result;
62
+ return result
65
63
  }
66
64
 
67
65
  /**
68
66
  * Initialize position for first item in a group
69
67
  * @returns {string} Initial position key
70
68
  */
71
- export function getInitialPosition() {
72
- return generateKeyBetween(null, null); // Returns 'a0'
69
+ export function getInitialPosition () {
70
+ return generateKeyBetween(null, null) // Returns 'a0'
73
71
  }
74
72
 
75
73
  /**
@@ -79,40 +77,40 @@ export function getInitialPosition() {
79
77
  * @param {number} maxLength - Maximum position string length before rebalancing
80
78
  * @returns {Array} Items with new positions (or original if no rebalance needed)
81
79
  */
82
- export function rebalancePositions(items, positionField, maxLength = 50) {
80
+ export function rebalancePositions (items, positionField, maxLength = 50) {
83
81
  // Check if rebalancing is needed
84
- const needsRebalance = items.some(item =>
82
+ const needsRebalance = items.some(item =>
85
83
  item[positionField] && item[positionField].length > maxLength
86
- );
87
-
84
+ )
85
+
88
86
  if (!needsRebalance) {
89
- return items;
87
+ return items
90
88
  }
91
89
 
92
90
  // Sort by current position
93
91
  const sorted = [...items].sort((a, b) => {
94
- const posA = a[positionField] || '';
95
- const posB = b[positionField] || '';
96
- return posA < posB ? -1 : posA > posB ? 1 : 0;
97
- });
92
+ const posA = a[positionField] || ''
93
+ const posB = b[positionField] || ''
94
+ return posA < posB ? -1 : posA > posB ? 1 : 0
95
+ })
98
96
 
99
97
  // Generate evenly spaced positions
100
- const rebalanced = [];
101
- let prevKey = null;
102
-
98
+ const rebalanced = []
99
+ let prevKey = null
100
+
103
101
  for (let i = 0; i < sorted.length; i++) {
104
102
  // For even spacing, we generate keys sequentially
105
- const newPosition = generateKeyBetween(prevKey, null);
106
-
103
+ const newPosition = generateKeyBetween(prevKey, null)
104
+
107
105
  rebalanced.push({
108
106
  ...sorted[i],
109
107
  [positionField]: newPosition
110
- });
111
-
112
- prevKey = newPosition;
108
+ })
109
+
110
+ prevKey = newPosition
113
111
  }
114
112
 
115
- return rebalanced;
113
+ return rebalanced
116
114
  }
117
115
 
118
116
  /**
@@ -120,13 +118,13 @@ export function rebalancePositions(items, positionField, maxLength = 50) {
120
118
  * @param {string} position - Position value to check
121
119
  * @returns {boolean} True if valid
122
120
  */
123
- export function isValidPosition(position) {
121
+ export function isValidPosition (position) {
124
122
  if (!position || typeof position !== 'string') {
125
- return false;
123
+ return false
126
124
  }
127
-
125
+
128
126
  // Fractional keys should match pattern: lowercase letters and digits
129
- return /^[a-z0-9]+$/i.test(position);
127
+ return /^[a-z0-9]+$/i.test(position)
130
128
  }
131
129
 
132
130
  /**
@@ -135,8 +133,8 @@ export function isValidPosition(position) {
135
133
  * @param {string} positionField - Name of the position field
136
134
  * @returns {Array} Items without valid positions
137
135
  */
138
- export function getUnpositionedItems(items, positionField) {
139
- return items.filter(item => !isValidPosition(item[positionField]));
136
+ export function getUnpositionedItems (items, positionField) {
137
+ return items.filter(item => !isValidPosition(item[positionField]))
140
138
  }
141
139
 
142
140
  /**
@@ -146,45 +144,45 @@ export function getUnpositionedItems(items, positionField) {
146
144
  * @param {string} orderByField - Optional field to order by initially
147
145
  * @returns {Array} Items with positions assigned
148
146
  */
149
- export function assignInitialPositions(items, positionField, orderByField = null) {
150
- const positioned = items.filter(item => isValidPosition(item[positionField]));
151
- const unpositioned = items.filter(item => !isValidPosition(item[positionField]));
152
-
147
+ export function assignInitialPositions (items, positionField, orderByField = null) {
148
+ const positioned = items.filter(item => isValidPosition(item[positionField]))
149
+ const unpositioned = items.filter(item => !isValidPosition(item[positionField]))
150
+
153
151
  if (unpositioned.length === 0) {
154
- return items;
152
+ return items
155
153
  }
156
154
 
157
155
  // Sort unpositioned items if orderBy field provided
158
156
  if (orderByField) {
159
157
  unpositioned.sort((a, b) => {
160
- const valA = a[orderByField];
161
- const valB = b[orderByField];
162
- if (valA < valB) return -1;
163
- if (valA > valB) return 1;
164
- return 0;
165
- });
158
+ const valA = a[orderByField]
159
+ const valB = b[orderByField]
160
+ if (valA < valB) return -1
161
+ if (valA > valB) return 1
162
+ return 0
163
+ })
166
164
  }
167
165
 
168
166
  // Get the last positioned item
169
167
  const sortedPositioned = [...positioned].sort((a, b) => {
170
- const posA = a[positionField] || '';
171
- const posB = b[positionField] || '';
172
- return posA < posB ? -1 : posA > posB ? 1 : 0;
173
- });
174
- const lastPositioned = sortedPositioned[sortedPositioned.length - 1];
175
-
176
- let lastKey = lastPositioned ? lastPositioned[positionField] : null;
177
-
168
+ const posA = a[positionField] || ''
169
+ const posB = b[positionField] || ''
170
+ return posA < posB ? -1 : posA > posB ? 1 : 0
171
+ })
172
+ const lastPositioned = sortedPositioned[sortedPositioned.length - 1]
173
+
174
+ let lastKey = lastPositioned ? lastPositioned[positionField] : null
175
+
178
176
  // Assign positions to unpositioned items
179
177
  const newlyPositioned = unpositioned.map(item => {
180
- const newKey = generateKeyBetween(lastKey, null);
181
- lastKey = newKey;
182
-
178
+ const newKey = generateKeyBetween(lastKey, null)
179
+ lastKey = newKey
180
+
183
181
  return {
184
182
  ...item,
185
183
  [positionField]: newKey
186
- };
187
- });
184
+ }
185
+ })
188
186
 
189
- return [...positioned, ...newlyPositioned];
190
- }
187
+ return [...positioned, ...newlyPositioned]
188
+ }