json-rest-api 1.0.7 → 1.0.8

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 (146) hide show
  1. package/.claude/agents/library-guide-expert.md +29 -0
  2. package/.claude/agents/rest-api-internals-expert.md +53 -0
  3. package/CLAUDE.md +139 -1
  4. package/README.md +2 -5
  5. package/docTest.js +238 -0
  6. package/docs/GUIDE/GUIDE_1_Initial_Setup.md +955 -0
  7. package/docs/GUIDE/GUIDE_2_1_The_Starting_Point.md +54 -0
  8. package/docs/GUIDE/GUIDE_2_2_Manipulating_And_Searching_Tables.md +750 -0
  9. package/docs/GUIDE/GUIDE_2_3_BelongsTo_Relationships.md +494 -0
  10. package/docs/GUIDE/GUIDE_2_4_HasMany_Records.md +398 -0
  11. package/docs/GUIDE/GUIDE_2_5_HasMany_Polymorphic.md +882 -0
  12. package/docs/GUIDE/GUIDE_2_6_Many_To_Many.md +709 -0
  13. package/docs/GUIDE/GUIDE_2_7_Pagination_And_Ordering.md +1233 -0
  14. package/docs/GUIDE/GUIDE_2_8_Effects_of_PUT_and_PATCH.md +796 -0
  15. package/{README_X_Relationship_Plugin.md → docs/GUIDE/GUIDE_2_9_Relationships_Urls.md} +1 -4
  16. package/docs/GUIDE/GUIDE_2_Data_And_Relations.md +66 -0
  17. package/docs/GUIDE/GUIDE_3_Field_Transformations.md +1246 -0
  18. package/docs/GUIDE/GUIDE_5_Non-Db_Stores.md +0 -0
  19. package/docs/GUIDE/GUIDE_7_Hooks_Data_Management_And_Plugins.md +1490 -0
  20. package/{README_X_SocketIO.md → docs/GUIDE/GUIDE_X_SocketIO.md} +17 -17
  21. package/docs/GUIDE/index.md +58 -0
  22. package/docs/GUIDE.md +61 -2238
  23. package/docs/ONBOARDING/Computed_Properties_And_Sparse_Fields.md +398 -0
  24. package/docs/README.md +57 -2239
  25. package/{README_2_Computed_and_hidden_fields.md → docs/README_VARIOUS/GUIDE_2_Computed_and_hidden_fields.md} +22 -17
  26. package/docs/README_VARIOUS/GUIDE_IN_DOCS.md +2277 -0
  27. package/{README.ORIG.md → docs/README_VARIOUS/README.ORIG.md} +8 -8
  28. package/{README_8_Polymorphic_Relationships.md → docs/README_VARIOUS/README_8_Polymorphic_Relationships.md} +1 -1
  29. package/docs/README_VARIOUS/README_IN_DOCS.ms +2277 -0
  30. package/{RELATIONSHIPS_ADVANCED.md → docs/README_VARIOUS/RELATIONSHIPS_ADVANCED.md} +3 -3
  31. package/{correct-schema-format.md → docs/README_VARIOUS/correct-schema-format.md} +13 -13
  32. package/{pagination.md → docs/README_VARIOUS/pagination.md} +11 -8
  33. package/docs/assets/css/style.scss +78 -5
  34. package/docs/index.md +2 -12
  35. package/helper-reorganization-plan.md +85 -0
  36. package/helper-usage-analysis.md +94 -0
  37. package/package.json +5 -4
  38. package/plugins/core/bulk-operations-plugin.js +2 -2
  39. package/plugins/core/connectors/express-plugin.js +90 -32
  40. package/plugins/core/lib/querying/knex-cross-table-search.js +609 -0
  41. package/plugins/core/lib/querying/knex-json-api-transformers-querying.js +491 -0
  42. package/plugins/core/lib/querying/knex-pagination-helpers.js +544 -0
  43. package/plugins/core/lib/{knex-process-includes.js → querying/knex-process-includes.js} +65 -50
  44. package/plugins/core/lib/querying/knex-query-helpers-base.js +74 -0
  45. package/plugins/core/lib/{knex-query-helpers.js → querying/knex-query-helpers.js} +416 -180
  46. package/plugins/core/lib/{knex-relationship-includes.js → querying/knex-relationship-includes.js} +430 -114
  47. package/plugins/core/lib/querying/knex-window-queries.js +328 -0
  48. package/plugins/core/lib/querying-writing/compile-schemas.js +328 -0
  49. package/plugins/core/{utils → lib/querying-writing}/connectors-query-parser.js +62 -57
  50. package/plugins/core/lib/{database-capabilities.js → querying-writing/database-capabilities.js} +113 -7
  51. package/plugins/core/lib/querying-writing/database-value-normalizers.js +369 -0
  52. package/plugins/core/lib/querying-writing/default-data-helpers.js +72 -0
  53. package/plugins/core/{utils → lib/querying-writing}/field-utils.js +41 -31
  54. package/plugins/core/{utils → lib/querying-writing}/knex-constants.js +1 -0
  55. package/plugins/core/lib/{knex-field-helpers.js → querying-writing/knex-field-helpers.js} +190 -57
  56. package/plugins/core/lib/querying-writing/knex-json-api-transformers.js +162 -0
  57. package/plugins/core/lib/{payload-validators.js → querying-writing/payload-validators.js} +297 -217
  58. package/plugins/core/lib/querying-writing/schema-helpers.js +497 -0
  59. package/plugins/core/lib/querying-writing/scope-validations.js +431 -0
  60. package/plugins/core/lib/{simplified-helpers.js → querying-writing/simplified-helpers.js} +216 -176
  61. package/plugins/core/lib/writing/knex-json-api-transformers-writing.js +110 -0
  62. package/plugins/core/lib/writing/many-to-many-manipulations.js +300 -0
  63. package/plugins/core/lib/writing/relationship-processor.js +252 -0
  64. package/plugins/core/rest-api-knex-plugin.js +183 -62
  65. package/plugins/core/rest-api-plugin-hooks/compile-resource-schemas.js +7 -0
  66. package/plugins/core/rest-api-plugin-hooks/register-relationship-routes.js +72 -0
  67. package/plugins/core/rest-api-plugin-hooks/register-scope-routes.js +102 -0
  68. package/plugins/core/rest-api-plugin-hooks/turn-scope-init-into-vars.js +50 -0
  69. package/plugins/core/rest-api-plugin-hooks/validate-include-configurations.js +39 -0
  70. package/plugins/core/rest-api-plugin-methods/add-route.js +23 -0
  71. package/plugins/core/rest-api-plugin-methods/check-permissions.js +18 -0
  72. package/plugins/core/rest-api-plugin-methods/common.js +605 -0
  73. package/plugins/core/rest-api-plugin-methods/delete-relationship.js +115 -0
  74. package/plugins/core/rest-api-plugin-methods/delete.js +109 -0
  75. package/plugins/core/rest-api-plugin-methods/enrich-attributes.js +186 -0
  76. package/plugins/core/rest-api-plugin-methods/get-related.js +295 -0
  77. package/plugins/core/rest-api-plugin-methods/get-relationship.js +62 -0
  78. package/plugins/core/rest-api-plugin-methods/get.js +187 -0
  79. package/plugins/core/rest-api-plugin-methods/patch-relationship.js +56 -0
  80. package/plugins/core/rest-api-plugin-methods/patch.js +193 -0
  81. package/plugins/core/rest-api-plugin-methods/post-relationship.js +104 -0
  82. package/plugins/core/rest-api-plugin-methods/post.js +157 -0
  83. package/plugins/core/rest-api-plugin-methods/put.js +257 -0
  84. package/plugins/core/rest-api-plugin-methods/query.js +208 -0
  85. package/plugins/core/rest-api-plugin.js +108 -2498
  86. package/plugins/core/socketio-plugin.js +28 -5
  87. package/plugins/storage/local-storage.js +2 -2
  88. package/tests/computed-fields.test.js +483 -0
  89. package/tests/cors.test.js +4 -4
  90. package/tests/curl.test.js +5 -4
  91. package/tests/custom-idproperty-relationships.test.js +891 -0
  92. package/tests/field-getters.test.js +353 -0
  93. package/tests/field-setters.test.js +310 -0
  94. package/tests/fixtures/api-configs.js +800 -28
  95. package/tests/jwt-auth.test.js +0 -2
  96. package/tests/nested-includes.test.js +1 -1
  97. package/tests/pagination-cursor-multifield.test.js +514 -0
  98. package/tests/pagination-enhanced.test.js +12 -9
  99. package/tests/polymorphic-relationships.test.js +10 -8
  100. package/tests/polymorphic-simple.test.js +6 -4
  101. package/tests/positioning-advanced.test.js +12 -12
  102. package/tests/positioning-extended.test.js +25 -25
  103. package/tests/positioning-multifilter.test.js +24 -24
  104. package/tests/positioning.test.js +3 -3
  105. package/tests/query-limits.test.js +24 -8
  106. package/tests/relationship-endpoints.test.js +4 -5
  107. package/tests/relationships.test.js +0 -1
  108. package/tests/socketio.test.js +12 -1
  109. package/tests/virtual-fields.test.js +13 -13
  110. package/PLAN_FOR_WEBSOCKETS.md +0 -398
  111. package/README_1_Initial_Setup.md +0 -393
  112. package/README_X_Hooks.md +0 -647
  113. package/TODO_rest-api-plugin-extras.todo +0 -23
  114. package/connectors_ideas/express-plugin.js +0 -120
  115. package/connectors_ideas/http-plugin.js +0 -138
  116. package/docs/examples/express-plugin.js +0 -366
  117. package/example.js +0 -301
  118. package/plugins/core/HELPER_MODULES.md +0 -285
  119. package/plugins/core/lib/compile-schemas.js +0 -245
  120. package/plugins/core/lib/database-value-normalizers.js +0 -192
  121. package/plugins/core/lib/default-data-helpers.js +0 -100
  122. package/plugins/core/lib/knex-cross-table-search.js +0 -442
  123. package/plugins/core/lib/knex-json-api-helpers.js +0 -84
  124. package/plugins/core/lib/knex-json-api-transformers.js +0 -449
  125. package/plugins/core/lib/knex-pagination-helpers.js +0 -193
  126. package/plugins/core/lib/knex-window-queries.js +0 -142
  127. package/plugins/core/lib/many-to-many-manipulations.js +0 -343
  128. package/plugins/core/lib/relationship-processor.js +0 -278
  129. package/plugins/core/lib/schema-helpers.js +0 -343
  130. package/plugins/core/lib/scope-validations.js +0 -252
  131. package/plugins/core/rest-api-relationships-plugin.js +0 -700
  132. package/plugins/core/utils/knex-query-helpers-base.js +0 -56
  133. /package/{README_X_Permissions.md → docs/GUIDE/GUIDE_4_Authentication_And_Permissions.md} +0 -0
  134. /package/{README_X_Positioning.md → docs/GUIDE/GUIDE_6_Positioning.md} +0 -0
  135. /package/{README_X_Bulk_Operations.md → docs/GUIDE/GUIDE_X_Bulk_Operations.md} +0 -0
  136. /package/{README_X_Cors.md → docs/GUIDE/GUIDE_X_Cors.md} +0 -0
  137. /package/{README_X_Multihome.md → docs/GUIDE/GUIDE_X_Multihome.md} +0 -0
  138. /package/{README_X_Appendices.md → docs/GUIDE/GUIDE_Y_Appendices.md} +0 -0
  139. /package/{QUERY_DEPTH_PROTECTION.md → docs/README_VARIOUS/QUERY_DEPTH_PROTECTION.md} +0 -0
  140. /package/{RELATIONSHIPS.md → docs/README_VARIOUS/RELATIONSHIPS.md} +0 -0
  141. /package/{WEBSOCKET_IMPLEMENTATION_STATUS.md → docs/README_VARIOUS/WEBSOCKET_IMPLEMENTATION_STATUS.md} +0 -0
  142. /package/{helper-functions-chart.md → docs/README_VARIOUS/helper-functions-chart.md} +0 -0
  143. /package/docs/{implementation-hidden-computed-fields.md → README_VARIOUS/implementation-hidden-computed-fields.md} +0 -0
  144. /package/docs/{include-limits-migration-guide.md → README_VARIOUS/include-limits-migration-guide.md} +0 -0
  145. /package/{readmeSnippets → docs/README_VARIOUS}/test_put_behavior.js +0 -0
  146. /package/{readmeSnippets → docs/README_VARIOUS}/test_put_fields_without_defaults.js +0 -0
@@ -0,0 +1,497 @@
1
+ /**
2
+ * Schema processing utilities for search and field dependencies
3
+ *
4
+ * @description
5
+ * This module provides utilities for:
6
+ * - Marking search fields as indexed for database optimization
7
+ * - Generating search schemas from various configuration sources
8
+ * - Sorting fields by their dependencies (for getters/setters)
9
+ * - Detecting circular dependencies in field definitions
10
+ */
11
+
12
+ import { RestApiValidationError } from '../../../../lib/rest-api-errors.js';
13
+
14
+ /**
15
+ * Marks all fields in a searchSchema as indexed for database optimization
16
+ *
17
+ * @param {Object} searchSchema - Search schema object to process
18
+ * @returns {void} Modifies the searchSchema in-place
19
+ *
20
+ * @example
21
+ * // Input: Search fields without index flags
22
+ * const searchSchema = {
23
+ * title: { type: 'string', filterOperator: 'contains' },
24
+ * status: { type: 'string', filterOperator: '=' },
25
+ * created_at: { type: 'datetime', filterOperator: '>=' }
26
+ * };
27
+ * ensureSearchFieldsAreIndexed(searchSchema);
28
+ *
29
+ * // Output: All fields marked as indexed
30
+ * // {
31
+ * // title: { type: 'string', filterOperator: 'contains', indexed: true },
32
+ * // status: { type: 'string', filterOperator: '=', indexed: true },
33
+ * // created_at: { type: 'datetime', filterOperator: '>=', indexed: true }
34
+ * // }
35
+ *
36
+ * @example
37
+ * // Input: Virtual field for cross-table search
38
+ * const searchSchema = {
39
+ * name: { type: 'string', filterOperator: 'contains' },
40
+ * author_name: { // Virtual field
41
+ * type: 'string',
42
+ * filterOperator: 'contains',
43
+ * virtualField: {
44
+ * joinTo: 'users',
45
+ * joinOn: ['author_id', 'id'],
46
+ * searchField: 'name'
47
+ * }
48
+ * }
49
+ * };
50
+ * ensureSearchFieldsAreIndexed(searchSchema);
51
+ *
52
+ * // Output: Both regular and virtual fields indexed
53
+ * // {
54
+ * // name: { ..., indexed: true },
55
+ * // author_name: { ..., indexed: true } // Enables efficient JOIN
56
+ * // }
57
+ *
58
+ * @example
59
+ * // Input: Already indexed or explicitly false
60
+ * const searchSchema = {
61
+ * email: { type: 'string', indexed: false }, // Explicitly false
62
+ * username: { type: 'string', indexed: true } // Already true
63
+ * };
64
+ * ensureSearchFieldsAreIndexed(searchSchema);
65
+ *
66
+ * // Output: All forced to indexed: true
67
+ * // {
68
+ * // email: { type: 'string', indexed: true }, // Overridden
69
+ * // username: { type: 'string', indexed: true } // Unchanged
70
+ * // }
71
+ *
72
+ * @description
73
+ * Used by:
74
+ * - rest-api-plugin during scope initialization
75
+ * - Applied to all searchSchema fields before storage setup
76
+ *
77
+ * Purpose:
78
+ * - Signals storage plugins which fields need database indexes
79
+ * - Enables efficient filtering without full table scans
80
+ * - Supports cross-table searches via indexed JOINs
81
+ * - Improves query performance for filtered API requests
82
+ *
83
+ * Data flow:
84
+ * 1. Receives searchSchema object (or null)
85
+ * 2. Iterates through each field definition
86
+ * 3. Sets indexed: true on all fields
87
+ * 4. Storage plugins use this flag for index creation
88
+ */
89
+ export function ensureSearchFieldsAreIndexed(searchSchema) {
90
+ if (!searchSchema) return;
91
+
92
+ Object.keys(searchSchema).forEach(fieldName => {
93
+ const fieldDef = searchSchema[fieldName];
94
+ if (fieldDef && typeof fieldDef === 'object') {
95
+ // Mark the field as indexed for cross-table search support
96
+ fieldDef.indexed = true;
97
+ }
98
+ });
99
+ }
100
+
101
+ /**
102
+ * Generates complete searchSchema by merging schema search definitions with explicit searchSchema
103
+ *
104
+ * @param {Object} schema - Main resource schema with optional 'search' properties
105
+ * @param {Object} explicitSearchSchema - Optional explicit searchSchema to merge
106
+ * @returns {Object|null} Merged searchSchema or null if no search fields
107
+ * @throws {RestApiValidationError} If same field defined in multiple places
108
+ *
109
+ * @example
110
+ * // Input: Simple search fields in schema
111
+ * const schema = {
112
+ * title: { type: 'string', search: true }, // Simple
113
+ * content: { type: 'text' }, // Not searchable
114
+ * status: { type: 'string', search: { filterOperator: '=' } } // Custom
115
+ * };
116
+ * const result = generateSearchSchemaFromSchema(schema, null);
117
+ *
118
+ * // Output: Generated search schema
119
+ * // {
120
+ * // title: { type: 'string', filterOperator: '=' }, // Default operator
121
+ * // status: { type: 'string', filterOperator: '=' } // Specified operator
122
+ * // }
123
+ *
124
+ * @example
125
+ * // Input: Multiple filters from one field
126
+ * const schema = {
127
+ * published_at: {
128
+ * type: 'datetime',
129
+ * search: {
130
+ * published_after: { filterOperator: '>=', type: 'datetime' },
131
+ * published_before: { filterOperator: '<=', type: 'datetime' }
132
+ * }
133
+ * }
134
+ * };
135
+ * const result = generateSearchSchemaFromSchema(schema, null);
136
+ *
137
+ * // Output: Two search fields from one database field
138
+ * // {
139
+ * // published_after: {
140
+ * // type: 'datetime',
141
+ * // actualField: 'published_at', // Maps to real field
142
+ * // filterOperator: '>='
143
+ * // },
144
+ * // published_before: {
145
+ * // type: 'datetime',
146
+ * // actualField: 'published_at', // Same field, different operator
147
+ * // filterOperator: '<='
148
+ * // }
149
+ * // }
150
+ *
151
+ * @example
152
+ * // Input: Merging with explicit searchSchema
153
+ * const schema = {
154
+ * name: { type: 'string', search: true }
155
+ * };
156
+ * const explicitSearchSchema = {
157
+ * email: { type: 'string', filterOperator: '=' },
158
+ * age: { type: 'number', filterOperator: '>=' }
159
+ * };
160
+ * const result = generateSearchSchemaFromSchema(schema, explicitSearchSchema);
161
+ *
162
+ * // Output: Combined from both sources
163
+ * // {
164
+ * // email: { type: 'string', filterOperator: '=' }, // Explicit
165
+ * // age: { type: 'number', filterOperator: '>=' }, // Explicit
166
+ * // name: { type: 'string', filterOperator: '=' } // From schema
167
+ * // }
168
+ *
169
+ * @example
170
+ * // Input: Conflict detection
171
+ * const schema = {
172
+ * email: { type: 'string', search: true }
173
+ * };
174
+ * const explicitSearchSchema = {
175
+ * email: { type: 'string', filterOperator: 'contains' }
176
+ * };
177
+ * generateSearchSchemaFromSchema(schema, explicitSearchSchema);
178
+ * // Throws: RestApiValidationError
179
+ * // "Field 'email' is defined in both schema (with search: true) and explicit searchSchema."
180
+ *
181
+ * @example
182
+ * // Input: Virtual search fields
183
+ * const schema = {
184
+ * title: { type: 'string' },
185
+ * _virtual: {
186
+ * search: {
187
+ * category_name: { // Doesn't exist in DB
188
+ * type: 'string',
189
+ * filterOperator: 'contains',
190
+ * virtualField: {
191
+ * joinTo: 'categories',
192
+ * joinOn: ['category_id', 'id'], // JOIN condition
193
+ * searchField: 'name' // Field in related table
194
+ * }
195
+ * }
196
+ * }
197
+ * }
198
+ * };
199
+ *
200
+ * // Output: Virtual field for cross-table search
201
+ * // {
202
+ * // category_name: {
203
+ * // type: 'string',
204
+ * // filterOperator: 'contains',
205
+ * // virtualField: { ... join config ... }
206
+ * // }
207
+ * // }
208
+ *
209
+ * @description
210
+ * Used by:
211
+ * - rest-api-plugin during scope initialization
212
+ * - Generates searchSchema from various sources
213
+ *
214
+ * Purpose:
215
+ * - Provides flexible search configuration options
216
+ * - Enables virtual fields for cross-table searches
217
+ * - Supports range queries (before/after patterns)
218
+ * - Validates no duplicate definitions exist
219
+ * - Allows storage plugins to optimize queries
220
+ *
221
+ * Data flow:
222
+ * 1. Starts with explicit searchSchema (if any)
223
+ * 2. Processes schema fields with 'search' property
224
+ * 3. Handles multiple filters from single field
225
+ * 4. Processes virtual search definitions
226
+ * 5. Detects and throws on conflicts
227
+ * 6. Returns merged searchSchema or null
228
+ */
229
+ export const generateSearchSchemaFromSchema = (schema, explicitSearchSchema) => {
230
+ // Start with explicit searchSchema or empty object
231
+ const searchSchema = explicitSearchSchema ? {...explicitSearchSchema} : {};
232
+
233
+ if (!schema) {
234
+ return Object.keys(searchSchema).length > 0 ? searchSchema : null;
235
+ }
236
+
237
+ // Process schema fields with 'search' property
238
+ Object.entries(schema).forEach(([fieldName, fieldDef]) => {
239
+ const effectiveSearch = fieldDef.search;
240
+
241
+ if (effectiveSearch) {
242
+ if (effectiveSearch === true) {
243
+ // Check for conflicts with explicit searchSchema
244
+ if (searchSchema[fieldName]) {
245
+ throw new RestApiValidationError(
246
+ `Field '${fieldName}' is defined in both schema (with search: true) and explicit searchSchema. Remove one definition to avoid conflicts.`,
247
+ {
248
+ fields: [fieldName],
249
+ violations: [{
250
+ field: fieldName,
251
+ rule: 'duplicate_search_field',
252
+ message: 'Field cannot be defined in both schema and searchSchema'
253
+ }]
254
+ }
255
+ );
256
+ }
257
+
258
+ // Simple boolean - copy entire field definition (except search) and add filterOperator
259
+ const { search, ...fieldDefWithoutSearch } = fieldDef;
260
+ searchSchema[fieldName] = {
261
+ ...fieldDefWithoutSearch,
262
+ // Preserve existing filterOperator or default to '='
263
+ filterOperator: fieldDefWithoutSearch.filterOperator || '='
264
+ };
265
+ } else if (typeof effectiveSearch === 'object') {
266
+ // Check if search defines multiple filter fields
267
+ const hasNestedFilters = Object.values(effectiveSearch).some(
268
+ v => typeof v === 'object' && v.filterOperator
269
+ );
270
+
271
+ if (hasNestedFilters) {
272
+ // Multiple filters from one field (like published_after/before)
273
+ Object.entries(effectiveSearch).forEach(([filterName, filterDef]) => {
274
+ // Check for conflicts
275
+ if (searchSchema[filterName]) {
276
+ throw new RestApiValidationError(
277
+ `Field '${filterName}' is defined in both schema (with search) and explicit searchSchema. Remove one definition to avoid conflicts.`,
278
+ {
279
+ fields: [filterName],
280
+ violations: [{
281
+ field: filterName,
282
+ rule: 'duplicate_search_field',
283
+ message: 'Field cannot be defined in both schema and searchSchema'
284
+ }]
285
+ }
286
+ );
287
+ }
288
+
289
+ searchSchema[filterName] = {
290
+ type: fieldDef.type,
291
+ actualField: fieldName,
292
+ ...filterDef
293
+ };
294
+ });
295
+ } else {
296
+ // Check for conflicts
297
+ if (searchSchema[fieldName]) {
298
+ throw new RestApiValidationError(
299
+ `Field '${fieldName}' is defined in both schema (with search) and explicit searchSchema. Remove one definition to avoid conflicts.`,
300
+ {
301
+ fields: [fieldName],
302
+ violations: [{
303
+ field: fieldName,
304
+ rule: 'duplicate_search_field',
305
+ message: 'Field cannot be defined in both schema and searchSchema'
306
+ }]
307
+ }
308
+ );
309
+ }
310
+
311
+ // Single filter with config
312
+ searchSchema[fieldName] = {
313
+ type: fieldDef.type,
314
+ ...effectiveSearch
315
+ };
316
+ }
317
+ }
318
+ }
319
+ });
320
+
321
+ // Handle _virtual search definitions
322
+ if (schema._virtual?.search) {
323
+ Object.entries(schema._virtual.search).forEach(([filterName, filterDef]) => {
324
+ // Check for conflicts
325
+ if (searchSchema[filterName]) {
326
+ throw new RestApiValidationError(
327
+ `Field '${filterName}' is defined in both schema (_virtual.search) and explicit searchSchema. Remove one definition to avoid conflicts.`,
328
+ {
329
+ fields: [filterName],
330
+ violations: [{
331
+ field: filterName,
332
+ rule: 'duplicate_search_field',
333
+ message: 'Field cannot be defined in both schema and searchSchema'
334
+ }]
335
+ }
336
+ );
337
+ }
338
+
339
+ searchSchema[filterName] = filterDef;
340
+ });
341
+ }
342
+
343
+ return Object.keys(searchSchema).length > 0 ? searchSchema : null;
344
+ };
345
+
346
+ /**
347
+ * Generic topological sort for handling dependencies
348
+ *
349
+ * @param {Array} items - Array of items to sort
350
+ * @param {Function} getDependencies - Function that returns dependencies for an item
351
+ * @returns {Array} Sorted array respecting dependencies
352
+ * @throws {Error} If circular dependencies or unknown dependencies detected
353
+ *
354
+ * @example
355
+ * // Input: Simple dependency chain
356
+ * const items = ['a', 'b', 'c'];
357
+ * const deps = { a: ['b'], b: ['c'], c: [] };
358
+ * const sorted = topologicalSort(items, item => deps[item]);
359
+ * // Output: ['c', 'b', 'a']
360
+ * // c first (no deps), then b (depends on c), then a (depends on b)
361
+ *
362
+ * @example
363
+ * // Input: Circular dependency
364
+ * const items = ['a', 'b'];
365
+ * const deps = { a: ['b'], b: ['a'] }; // Circular!
366
+ * topologicalSort(items, item => deps[item]);
367
+ * // Throws: Error "Circular dependency detected: b"
368
+ *
369
+ * @example
370
+ * // Input: Unknown dependency
371
+ * const items = ['a', 'b'];
372
+ * const deps = { a: ['c'], b: [] }; // 'c' not in items
373
+ * topologicalSort(items, item => deps[item]);
374
+ * // Throws: Error "Unknown dependency 'c' for 'a'"
375
+ *
376
+ * @description
377
+ * Used by:
378
+ * - sortFieldsByDependencies for ordering field operations
379
+ * - Any code needing dependency-based ordering
380
+ *
381
+ * Purpose:
382
+ * - Orders items so dependencies come before dependents
383
+ * - Detects circular dependencies early
384
+ * - Validates all dependencies exist
385
+ * - Uses depth-first search algorithm
386
+ */
387
+ export function topologicalSort(items, getDependencies) {
388
+ const sorted = [];
389
+ const visited = new Set();
390
+ const visiting = new Set();
391
+
392
+ function visit(item) {
393
+ if (visited.has(item)) return;
394
+
395
+ if (visiting.has(item)) {
396
+ throw new Error(`Circular dependency detected: ${item}`);
397
+ }
398
+
399
+ visiting.add(item);
400
+
401
+ const dependencies = getDependencies(item) || [];
402
+ for (const dep of dependencies) {
403
+ if (!items.includes(dep)) {
404
+ throw new Error(`Unknown dependency '${dep}' for '${item}'`);
405
+ }
406
+ visit(dep);
407
+ }
408
+
409
+ visiting.delete(item);
410
+ visited.add(item);
411
+ sorted.push(item);
412
+ }
413
+
414
+ for (const item of items) {
415
+ visit(item);
416
+ }
417
+
418
+ return sorted;
419
+ }
420
+
421
+ /**
422
+ * Sorts fields by their dependencies using topological sort
423
+ *
424
+ * @param {Object} fields - Object with field definitions
425
+ * @param {string} dependencyProperty - Property name containing dependencies
426
+ * @returns {Array} Field names sorted by dependencies
427
+ * @throws {Error} If circular dependencies or unknown fields detected
428
+ *
429
+ * @example
430
+ * // Input: Getter dependencies (fullName needs firstName and lastName)
431
+ * const fields = {
432
+ * firstName: { getter: v => v.trim() },
433
+ * lastName: { getter: v => v.trim() },
434
+ * fullName: {
435
+ * getter: (v, ctx) => `${ctx.attributes.firstName} ${ctx.attributes.lastName}`,
436
+ * runGetterAfter: ['firstName', 'lastName']
437
+ * }
438
+ * };
439
+ * const sorted = sortFieldsByDependencies(fields, 'runGetterAfter');
440
+ * // Output: ['firstName', 'lastName', 'fullName']
441
+ * // Ensures firstName/lastName getters run before fullName
442
+ *
443
+ * @example
444
+ * // Input: Complex dependency chain
445
+ * const fields = {
446
+ * a: { runGetterAfter: ['b', 'c'] }, // a needs b and c
447
+ * b: { runGetterAfter: ['d'] }, // b needs d
448
+ * c: { runGetterAfter: ['d'] }, // c needs d
449
+ * d: { runGetterAfter: [] } // d needs nothing
450
+ * };
451
+ * const sorted = sortFieldsByDependencies(fields, 'runGetterAfter');
452
+ * // Output: ['d', 'b', 'c', 'a']
453
+ * // d first, then b/c (both need d), then a (needs b/c)
454
+ *
455
+ * @example
456
+ * // Input: Circular dependency error
457
+ * const fields = {
458
+ * a: { runGetterAfter: ['b'] },
459
+ * b: { runGetterAfter: ['a'] } // Circular!
460
+ * };
461
+ * sortFieldsByDependencies(fields, 'runGetterAfter');
462
+ * // Throws: Error "Circular dependency detected: a in runGetterAfter"
463
+ *
464
+ * @description
465
+ * Used by:
466
+ * - Schema processing for getter/setter ordering
467
+ * - Ensures dependent fields process after dependencies
468
+ *
469
+ * Purpose:
470
+ * - Orders field operations by dependencies
471
+ * - Enables computed fields that depend on other fields
472
+ * - Validates dependency graph is acyclic
473
+ * - Provides clear error messages for debugging
474
+ *
475
+ * Data flow:
476
+ * 1. Extracts field names from object
477
+ * 2. Calls topologicalSort with dependency function
478
+ * 3. Returns ordered field names
479
+ * 4. Enhances error messages with property name
480
+ */
481
+ export function sortFieldsByDependencies(fields, dependencyProperty) {
482
+ const fieldNames = Object.keys(fields);
483
+
484
+ if (fieldNames.length === 0) return [];
485
+
486
+ try {
487
+ return topologicalSort(fieldNames, (fieldName) => {
488
+ const field = fields[fieldName];
489
+ return field[dependencyProperty] || [];
490
+ });
491
+ } catch (error) {
492
+ if (error.message.includes('Circular dependency')) {
493
+ throw new Error(`${error.message} in ${dependencyProperty}`);
494
+ }
495
+ throw error;
496
+ }
497
+ }