json-rest-api 1.0.14 → 1.0.15

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 +97 -98
  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 +8 -8
  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
@@ -1,6 +1,6 @@
1
1
  /**
2
2
  * Schema processing utilities for search and field dependencies
3
- *
3
+ *
4
4
  * @description
5
5
  * This module provides utilities for:
6
6
  * - Marking search fields as indexed for database optimization
@@ -9,14 +9,14 @@
9
9
  * - Detecting circular dependencies in field definitions
10
10
  */
11
11
 
12
- import { RestApiValidationError } from '../../../../lib/rest-api-errors.js';
12
+ import { RestApiValidationError } from '../../../../lib/rest-api-errors.js'
13
13
 
14
14
  /**
15
15
  * Marks all fields in a searchSchema as indexed for database optimization
16
- *
16
+ *
17
17
  * @param {Object} searchSchema - Search schema object to process
18
18
  * @returns {void} Modifies the searchSchema in-place
19
- *
19
+ *
20
20
  * @example
21
21
  * // Input: Search fields without index flags
22
22
  * const searchSchema = {
@@ -25,14 +25,14 @@ import { RestApiValidationError } from '../../../../lib/rest-api-errors.js';
25
25
  * created_at: { type: 'datetime', filterOperator: '>=' }
26
26
  * };
27
27
  * ensureSearchFieldsAreIndexed(searchSchema);
28
- *
28
+ *
29
29
  * // Output: All fields marked as indexed
30
30
  * // {
31
31
  * // title: { type: 'string', filterOperator: 'contains', indexed: true },
32
32
  * // status: { type: 'string', filterOperator: '=', indexed: true },
33
33
  * // created_at: { type: 'datetime', filterOperator: '>=', indexed: true }
34
34
  * // }
35
- *
35
+ *
36
36
  * @example
37
37
  * // Input: Virtual field for cross-table search
38
38
  * const searchSchema = {
@@ -48,13 +48,13 @@ import { RestApiValidationError } from '../../../../lib/rest-api-errors.js';
48
48
  * }
49
49
  * };
50
50
  * ensureSearchFieldsAreIndexed(searchSchema);
51
- *
51
+ *
52
52
  * // Output: Both regular and virtual fields indexed
53
53
  * // {
54
54
  * // name: { ..., indexed: true },
55
55
  * // author_name: { ..., indexed: true } // Enables efficient JOIN
56
56
  * // }
57
- *
57
+ *
58
58
  * @example
59
59
  * // Input: Already indexed or explicitly false
60
60
  * const searchSchema = {
@@ -62,50 +62,50 @@ import { RestApiValidationError } from '../../../../lib/rest-api-errors.js';
62
62
  * username: { type: 'string', indexed: true } // Already true
63
63
  * };
64
64
  * ensureSearchFieldsAreIndexed(searchSchema);
65
- *
65
+ *
66
66
  * // Output: All forced to indexed: true
67
67
  * // {
68
68
  * // email: { type: 'string', indexed: true }, // Overridden
69
69
  * // username: { type: 'string', indexed: true } // Unchanged
70
70
  * // }
71
- *
71
+ *
72
72
  * @description
73
73
  * Used by:
74
74
  * - rest-api-plugin during scope initialization
75
75
  * - Applied to all searchSchema fields before storage setup
76
- *
76
+ *
77
77
  * Purpose:
78
78
  * - Signals storage plugins which fields need database indexes
79
79
  * - Enables efficient filtering without full table scans
80
80
  * - Supports cross-table searches via indexed JOINs
81
81
  * - Improves query performance for filtered API requests
82
- *
82
+ *
83
83
  * Data flow:
84
84
  * 1. Receives searchSchema object (or null)
85
85
  * 2. Iterates through each field definition
86
86
  * 3. Sets indexed: true on all fields
87
87
  * 4. Storage plugins use this flag for index creation
88
88
  */
89
- export function ensureSearchFieldsAreIndexed(searchSchema) {
90
- if (!searchSchema) return;
91
-
89
+ export function ensureSearchFieldsAreIndexed (searchSchema) {
90
+ if (!searchSchema) return
91
+
92
92
  Object.keys(searchSchema).forEach(fieldName => {
93
- const fieldDef = searchSchema[fieldName];
93
+ const fieldDef = searchSchema[fieldName]
94
94
  if (fieldDef && typeof fieldDef === 'object') {
95
95
  // Mark the field as indexed for cross-table search support
96
- fieldDef.indexed = true;
96
+ fieldDef.indexed = true
97
97
  }
98
- });
98
+ })
99
99
  }
100
100
 
101
101
  /**
102
102
  * Generates complete searchSchema by merging schema search definitions with explicit searchSchema
103
- *
103
+ *
104
104
  * @param {Object} schema - Main resource schema with optional 'search' properties
105
105
  * @param {Object} explicitSearchSchema - Optional explicit searchSchema to merge
106
106
  * @returns {Object|null} Merged searchSchema or null if no search fields
107
107
  * @throws {RestApiValidationError} If same field defined in multiple places
108
- *
108
+ *
109
109
  * @example
110
110
  * // Input: Simple search fields in schema
111
111
  * const schema = {
@@ -114,13 +114,13 @@ export function ensureSearchFieldsAreIndexed(searchSchema) {
114
114
  * status: { type: 'string', search: { filterOperator: '=' } } // Custom
115
115
  * };
116
116
  * const result = generateSearchSchemaFromSchema(schema, null);
117
- *
117
+ *
118
118
  * // Output: Generated search schema
119
119
  * // {
120
120
  * // title: { type: 'string', filterOperator: '=' }, // Default operator
121
121
  * // status: { type: 'string', filterOperator: '=' } // Specified operator
122
122
  * // }
123
- *
123
+ *
124
124
  * @example
125
125
  * // Input: Multiple filters from one field
126
126
  * const schema = {
@@ -133,21 +133,21 @@ export function ensureSearchFieldsAreIndexed(searchSchema) {
133
133
  * }
134
134
  * };
135
135
  * const result = generateSearchSchemaFromSchema(schema, null);
136
- *
136
+ *
137
137
  * // Output: Two search fields from one database field
138
138
  * // {
139
- * // published_after: {
140
- * // type: 'datetime',
139
+ * // published_after: {
140
+ * // type: 'datetime',
141
141
  * // actualField: 'published_at', // Maps to real field
142
- * // filterOperator: '>='
142
+ * // filterOperator: '>='
143
143
  * // },
144
- * // published_before: {
145
- * // type: 'datetime',
144
+ * // published_before: {
145
+ * // type: 'datetime',
146
146
  * // actualField: 'published_at', // Same field, different operator
147
- * // filterOperator: '<='
147
+ * // filterOperator: '<='
148
148
  * // }
149
149
  * // }
150
- *
150
+ *
151
151
  * @example
152
152
  * // Input: Merging with explicit searchSchema
153
153
  * const schema = {
@@ -158,14 +158,14 @@ export function ensureSearchFieldsAreIndexed(searchSchema) {
158
158
  * age: { type: 'number', filterOperator: '>=' }
159
159
  * };
160
160
  * const result = generateSearchSchemaFromSchema(schema, explicitSearchSchema);
161
- *
161
+ *
162
162
  * // Output: Combined from both sources
163
163
  * // {
164
164
  * // email: { type: 'string', filterOperator: '=' }, // Explicit
165
165
  * // age: { type: 'number', filterOperator: '>=' }, // Explicit
166
166
  * // name: { type: 'string', filterOperator: '=' } // From schema
167
167
  * // }
168
- *
168
+ *
169
169
  * @example
170
170
  * // Input: Explicit searchSchema takes precedence
171
171
  * const schema = {
@@ -179,7 +179,7 @@ export function ensureSearchFieldsAreIndexed(searchSchema) {
179
179
  * // {
180
180
  * // email: { type: 'string', filterOperator: 'contains' } // Uses explicit definition
181
181
  * // }
182
- *
182
+ *
183
183
  * @example
184
184
  * // Input: Virtual search fields
185
185
  * const schema = {
@@ -198,7 +198,7 @@ export function ensureSearchFieldsAreIndexed(searchSchema) {
198
198
  * }
199
199
  * }
200
200
  * };
201
- *
201
+ *
202
202
  * // Output: Virtual field for cross-table search
203
203
  * // {
204
204
  * // category_name: {
@@ -207,12 +207,12 @@ export function ensureSearchFieldsAreIndexed(searchSchema) {
207
207
  * // virtualField: { ... join config ... }
208
208
  * // }
209
209
  * // }
210
- *
210
+ *
211
211
  * @description
212
212
  * Used by:
213
213
  * - rest-api-plugin during scope initialization
214
214
  * - Generates searchSchema from various sources
215
- *
215
+ *
216
216
  * Purpose:
217
217
  * - Provides flexible search configuration options
218
218
  * - Enables virtual fields for cross-table searches
@@ -220,7 +220,7 @@ export function ensureSearchFieldsAreIndexed(searchSchema) {
220
220
  * - Merges search:true fields with explicit searchSchema
221
221
  * - Explicit searchSchema always takes precedence
222
222
  * - Allows storage plugins to optimize queries
223
- *
223
+ *
224
224
  * Data flow:
225
225
  * 1. Starts with explicit searchSchema (if any)
226
226
  * 2. Processes schema fields with 'search' property
@@ -231,118 +231,118 @@ export function ensureSearchFieldsAreIndexed(searchSchema) {
231
231
  */
232
232
  export const generateSearchSchemaFromSchema = (schema, explicitSearchSchema) => {
233
233
  // Start with explicit searchSchema or empty object
234
- const searchSchema = explicitSearchSchema ? {...explicitSearchSchema} : {};
235
-
234
+ const searchSchema = explicitSearchSchema ? { ...explicitSearchSchema } : {}
235
+
236
236
  if (!schema) {
237
- return Object.keys(searchSchema).length > 0 ? searchSchema : null;
237
+ return Object.keys(searchSchema).length > 0 ? searchSchema : null
238
238
  }
239
-
239
+
240
240
  // Process schema fields with 'search' property
241
241
  Object.entries(schema).forEach(([fieldName, fieldDef]) => {
242
- const effectiveSearch = fieldDef.search;
243
-
242
+ const effectiveSearch = fieldDef.search
243
+
244
244
  if (effectiveSearch) {
245
245
  if (effectiveSearch === true) {
246
246
  // For belongsTo relationships, use the relationship name (as property) instead of the field name
247
- const searchFieldName = (fieldDef.belongsTo && fieldDef.as) ? fieldDef.as : fieldName;
248
-
247
+ const searchFieldName = (fieldDef.belongsTo && fieldDef.as) ? fieldDef.as : fieldName
248
+
249
249
  // Check if field already exists in explicit searchSchema
250
250
  if (searchSchema[searchFieldName]) {
251
251
  // Skip - explicit searchSchema takes precedence
252
252
  // This allows searchSchema to override fields marked with search:true
253
- return;
253
+ return
254
254
  }
255
-
255
+
256
256
  // Simple boolean - copy entire field definition (except search) and add filterOperator
257
- const { search, ...fieldDefWithoutSearch } = fieldDef;
257
+ const { search, ...fieldDefWithoutSearch } = fieldDef
258
258
  const searchEntry = {
259
259
  ...fieldDefWithoutSearch,
260
260
  // Preserve existing filterOperator or default to '='
261
261
  filterOperator: fieldDefWithoutSearch.filterOperator || '='
262
- };
263
-
262
+ }
263
+
264
264
  // For belongsTo relationships, add metadata to map back to the actual field
265
265
  if (fieldDef.belongsTo && fieldDef.as) {
266
- searchEntry.actualField = fieldName; // Maps to the DB column
267
- searchEntry.isRelationship = true;
268
- searchEntry.targetResource = fieldDef.belongsTo;
266
+ searchEntry.actualField = fieldName // Maps to the DB column
267
+ searchEntry.isRelationship = true
268
+ searchEntry.targetResource = fieldDef.belongsTo
269
269
  }
270
-
271
- searchSchema[searchFieldName] = searchEntry;
270
+
271
+ searchSchema[searchFieldName] = searchEntry
272
272
  } else if (typeof effectiveSearch === 'object') {
273
273
  // Check if search defines multiple filter fields
274
274
  const hasNestedFilters = Object.values(effectiveSearch).some(
275
275
  v => typeof v === 'object' && v.filterOperator
276
- );
277
-
276
+ )
277
+
278
278
  if (hasNestedFilters) {
279
279
  // Multiple filters from one field (like published_after/before)
280
280
  Object.entries(effectiveSearch).forEach(([filterName, filterDef]) => {
281
281
  // Check if filter already exists in explicit searchSchema
282
282
  if (searchSchema[filterName]) {
283
283
  // Skip - explicit searchSchema takes precedence
284
- return;
284
+ return
285
285
  }
286
-
286
+
287
287
  searchSchema[filterName] = {
288
288
  type: fieldDef.type,
289
289
  actualField: fieldName,
290
290
  ...filterDef
291
- };
292
- });
291
+ }
292
+ })
293
293
  } else {
294
294
  // For belongsTo relationships, use the relationship name (as property) instead of the field name
295
- const searchFieldName = (fieldDef.belongsTo && fieldDef.as) ? fieldDef.as : fieldName;
296
-
295
+ const searchFieldName = (fieldDef.belongsTo && fieldDef.as) ? fieldDef.as : fieldName
296
+
297
297
  // Check if field already exists in explicit searchSchema
298
298
  if (searchSchema[searchFieldName]) {
299
299
  // Skip - explicit searchSchema takes precedence
300
- return;
300
+ return
301
301
  }
302
-
302
+
303
303
  // Single filter with config
304
304
  const searchEntry = {
305
305
  type: fieldDef.type,
306
306
  ...effectiveSearch
307
- };
308
-
307
+ }
308
+
309
309
  // For belongsTo relationships, add metadata to map back to the actual field
310
310
  if (fieldDef.belongsTo && fieldDef.as) {
311
- searchEntry.actualField = fieldName; // Maps to the DB column
312
- searchEntry.isRelationship = true;
313
- searchEntry.targetResource = fieldDef.belongsTo;
311
+ searchEntry.actualField = fieldName // Maps to the DB column
312
+ searchEntry.isRelationship = true
313
+ searchEntry.targetResource = fieldDef.belongsTo
314
314
  }
315
-
316
- searchSchema[searchFieldName] = searchEntry;
315
+
316
+ searchSchema[searchFieldName] = searchEntry
317
317
  }
318
318
  }
319
319
  }
320
- });
321
-
320
+ })
321
+
322
322
  // Handle _virtual search definitions
323
323
  if (schema._virtual?.search) {
324
324
  Object.entries(schema._virtual.search).forEach(([filterName, filterDef]) => {
325
325
  // Check if filter already exists in explicit searchSchema
326
326
  if (searchSchema[filterName]) {
327
327
  // Skip - explicit searchSchema takes precedence
328
- return;
328
+ return
329
329
  }
330
-
331
- searchSchema[filterName] = filterDef;
332
- });
330
+
331
+ searchSchema[filterName] = filterDef
332
+ })
333
333
  }
334
-
335
- return Object.keys(searchSchema).length > 0 ? searchSchema : null;
336
- };
334
+
335
+ return Object.keys(searchSchema).length > 0 ? searchSchema : null
336
+ }
337
337
 
338
338
  /**
339
339
  * Generic topological sort for handling dependencies
340
- *
340
+ *
341
341
  * @param {Array} items - Array of items to sort
342
342
  * @param {Function} getDependencies - Function that returns dependencies for an item
343
343
  * @returns {Array} Sorted array respecting dependencies
344
344
  * @throws {Error} If circular dependencies or unknown dependencies detected
345
- *
345
+ *
346
346
  * @example
347
347
  * // Input: Simple dependency chain
348
348
  * const items = ['a', 'b', 'c'];
@@ -350,80 +350,80 @@ export const generateSearchSchemaFromSchema = (schema, explicitSearchSchema) =>
350
350
  * const sorted = topologicalSort(items, item => deps[item]);
351
351
  * // Output: ['c', 'b', 'a']
352
352
  * // c first (no deps), then b (depends on c), then a (depends on b)
353
- *
353
+ *
354
354
  * @example
355
355
  * // Input: Circular dependency
356
356
  * const items = ['a', 'b'];
357
357
  * const deps = { a: ['b'], b: ['a'] }; // Circular!
358
358
  * topologicalSort(items, item => deps[item]);
359
359
  * // Throws: Error "Circular dependency detected: b"
360
- *
360
+ *
361
361
  * @example
362
362
  * // Input: Unknown dependency
363
363
  * const items = ['a', 'b'];
364
364
  * const deps = { a: ['c'], b: [] }; // 'c' not in items
365
365
  * topologicalSort(items, item => deps[item]);
366
366
  * // Throws: Error "Unknown dependency 'c' for 'a'"
367
- *
367
+ *
368
368
  * @description
369
369
  * Used by:
370
370
  * - sortFieldsByDependencies for ordering field operations
371
371
  * - Any code needing dependency-based ordering
372
- *
372
+ *
373
373
  * Purpose:
374
374
  * - Orders items so dependencies come before dependents
375
375
  * - Detects circular dependencies early
376
376
  * - Validates all dependencies exist
377
377
  * - Uses depth-first search algorithm
378
378
  */
379
- export function topologicalSort(items, getDependencies) {
380
- const sorted = [];
381
- const visited = new Set();
382
- const visiting = new Set();
383
-
384
- function visit(item) {
385
- if (visited.has(item)) return;
386
-
379
+ export function topologicalSort (items, getDependencies) {
380
+ const sorted = []
381
+ const visited = new Set()
382
+ const visiting = new Set()
383
+
384
+ function visit (item) {
385
+ if (visited.has(item)) return
386
+
387
387
  if (visiting.has(item)) {
388
- throw new Error(`Circular dependency detected: ${item}`);
388
+ throw new Error(`Circular dependency detected: ${item}`)
389
389
  }
390
-
391
- visiting.add(item);
392
-
393
- const dependencies = getDependencies(item) || [];
390
+
391
+ visiting.add(item)
392
+
393
+ const dependencies = getDependencies(item) || []
394
394
  for (const dep of dependencies) {
395
395
  if (!items.includes(dep)) {
396
- throw new Error(`Unknown dependency '${dep}' for '${item}'`);
396
+ throw new Error(`Unknown dependency '${dep}' for '${item}'`)
397
397
  }
398
- visit(dep);
398
+ visit(dep)
399
399
  }
400
-
401
- visiting.delete(item);
402
- visited.add(item);
403
- sorted.push(item);
400
+
401
+ visiting.delete(item)
402
+ visited.add(item)
403
+ sorted.push(item)
404
404
  }
405
-
405
+
406
406
  for (const item of items) {
407
- visit(item);
407
+ visit(item)
408
408
  }
409
-
410
- return sorted;
409
+
410
+ return sorted
411
411
  }
412
412
 
413
413
  /**
414
414
  * Sorts fields by their dependencies using topological sort
415
- *
415
+ *
416
416
  * @param {Object} fields - Object with field definitions
417
417
  * @param {string} dependencyProperty - Property name containing dependencies
418
418
  * @returns {Array} Field names sorted by dependencies
419
419
  * @throws {Error} If circular dependencies or unknown fields detected
420
- *
420
+ *
421
421
  * @example
422
422
  * // Input: Getter dependencies (fullName needs firstName and lastName)
423
423
  * const fields = {
424
424
  * firstName: { getter: v => v.trim() },
425
425
  * lastName: { getter: v => v.trim() },
426
- * fullName: {
426
+ * fullName: {
427
427
  * getter: (v, ctx) => `${ctx.attributes.firstName} ${ctx.attributes.lastName}`,
428
428
  * runGetterAfter: ['firstName', 'lastName']
429
429
  * }
@@ -431,7 +431,7 @@ export function topologicalSort(items, getDependencies) {
431
431
  * const sorted = sortFieldsByDependencies(fields, 'runGetterAfter');
432
432
  * // Output: ['firstName', 'lastName', 'fullName']
433
433
  * // Ensures firstName/lastName getters run before fullName
434
- *
434
+ *
435
435
  * @example
436
436
  * // Input: Complex dependency chain
437
437
  * const fields = {
@@ -443,7 +443,7 @@ export function topologicalSort(items, getDependencies) {
443
443
  * const sorted = sortFieldsByDependencies(fields, 'runGetterAfter');
444
444
  * // Output: ['d', 'b', 'c', 'a']
445
445
  * // d first, then b/c (both need d), then a (needs b/c)
446
- *
446
+ *
447
447
  * @example
448
448
  * // Input: Circular dependency error
449
449
  * const fields = {
@@ -452,38 +452,38 @@ export function topologicalSort(items, getDependencies) {
452
452
  * };
453
453
  * sortFieldsByDependencies(fields, 'runGetterAfter');
454
454
  * // Throws: Error "Circular dependency detected: a in runGetterAfter"
455
- *
455
+ *
456
456
  * @description
457
457
  * Used by:
458
458
  * - Schema processing for getter/setter ordering
459
459
  * - Ensures dependent fields process after dependencies
460
- *
460
+ *
461
461
  * Purpose:
462
462
  * - Orders field operations by dependencies
463
463
  * - Enables computed fields that depend on other fields
464
464
  * - Validates dependency graph is acyclic
465
465
  * - Provides clear error messages for debugging
466
- *
466
+ *
467
467
  * Data flow:
468
468
  * 1. Extracts field names from object
469
469
  * 2. Calls topologicalSort with dependency function
470
470
  * 3. Returns ordered field names
471
471
  * 4. Enhances error messages with property name
472
472
  */
473
- export function sortFieldsByDependencies(fields, dependencyProperty) {
474
- const fieldNames = Object.keys(fields);
475
-
476
- if (fieldNames.length === 0) return [];
477
-
473
+ export function sortFieldsByDependencies (fields, dependencyProperty) {
474
+ const fieldNames = Object.keys(fields)
475
+
476
+ if (fieldNames.length === 0) return []
477
+
478
478
  try {
479
479
  return topologicalSort(fieldNames, (fieldName) => {
480
- const field = fields[fieldName];
481
- return field[dependencyProperty] || [];
482
- });
480
+ const field = fields[fieldName]
481
+ return field[dependencyProperty] || []
482
+ })
483
483
  } catch (error) {
484
484
  if (error.message.includes('Circular dependency')) {
485
- throw new Error(`${error.message} in ${dependencyProperty}`);
485
+ throw new Error(`${error.message} in ${dependencyProperty}`)
486
486
  }
487
- throw error;
487
+ throw error
488
488
  }
489
- }
489
+ }