json-rest-api 1.0.6 → 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.
- package/.claude/agents/library-guide-expert.md +29 -0
- package/.claude/agents/rest-api-internals-expert.md +53 -0
- package/CLAUDE.md +139 -1
- package/README.md +2 -5
- package/docTest.js +238 -0
- package/docs/GUIDE/GUIDE_1_Initial_Setup.md +955 -0
- package/docs/GUIDE/GUIDE_2_1_The_Starting_Point.md +54 -0
- package/docs/GUIDE/GUIDE_2_2_Manipulating_And_Searching_Tables.md +750 -0
- package/docs/GUIDE/GUIDE_2_3_BelongsTo_Relationships.md +494 -0
- package/docs/GUIDE/GUIDE_2_4_HasMany_Records.md +398 -0
- package/docs/GUIDE/GUIDE_2_5_HasMany_Polymorphic.md +882 -0
- package/docs/GUIDE/GUIDE_2_6_Many_To_Many.md +709 -0
- package/docs/GUIDE/GUIDE_2_7_Pagination_And_Ordering.md +1233 -0
- package/docs/GUIDE/GUIDE_2_8_Effects_of_PUT_and_PATCH.md +796 -0
- package/{README_X_Relationship_Plugin.md → docs/GUIDE/GUIDE_2_9_Relationships_Urls.md} +1 -4
- package/docs/GUIDE/GUIDE_2_Data_And_Relations.md +66 -0
- package/docs/GUIDE/GUIDE_3_Field_Transformations.md +1246 -0
- package/docs/GUIDE/GUIDE_5_Non-Db_Stores.md +0 -0
- package/docs/GUIDE/GUIDE_7_Hooks_Data_Management_And_Plugins.md +1490 -0
- package/{README_X_SocketIO.md → docs/GUIDE/GUIDE_X_SocketIO.md} +17 -17
- package/docs/GUIDE/index.md +58 -0
- package/docs/GUIDE.md +61 -2238
- package/docs/ONBOARDING/Computed_Properties_And_Sparse_Fields.md +398 -0
- package/docs/README.md +57 -2239
- package/{README_2_Computed_and_hidden_fields.md → docs/README_VARIOUS/GUIDE_2_Computed_and_hidden_fields.md} +22 -17
- package/docs/README_VARIOUS/GUIDE_IN_DOCS.md +2277 -0
- package/{README.ORIG.md → docs/README_VARIOUS/README.ORIG.md} +8 -8
- package/{README_8_Polymorphic_Relationships.md → docs/README_VARIOUS/README_8_Polymorphic_Relationships.md} +1 -1
- package/docs/README_VARIOUS/README_IN_DOCS.ms +2277 -0
- package/{RELATIONSHIPS_ADVANCED.md → docs/README_VARIOUS/RELATIONSHIPS_ADVANCED.md} +3 -3
- package/{correct-schema-format.md → docs/README_VARIOUS/correct-schema-format.md} +13 -13
- package/{pagination.md → docs/README_VARIOUS/pagination.md} +11 -8
- package/docs/assets/css/style.scss +78 -5
- package/docs/index.md +2 -12
- package/helper-reorganization-plan.md +85 -0
- package/helper-usage-analysis.md +94 -0
- package/package.json +5 -4
- package/plugins/core/bulk-operations-plugin.js +2 -2
- package/plugins/core/connectors/express-plugin.js +99 -33
- package/plugins/core/lib/querying/knex-cross-table-search.js +609 -0
- package/plugins/core/lib/querying/knex-json-api-transformers-querying.js +491 -0
- package/plugins/core/lib/querying/knex-pagination-helpers.js +544 -0
- package/plugins/core/lib/{knex-process-includes.js → querying/knex-process-includes.js} +65 -50
- package/plugins/core/lib/querying/knex-query-helpers-base.js +74 -0
- package/plugins/core/lib/{knex-query-helpers.js → querying/knex-query-helpers.js} +416 -180
- package/plugins/core/lib/{knex-relationship-includes.js → querying/knex-relationship-includes.js} +430 -114
- package/plugins/core/lib/querying/knex-window-queries.js +328 -0
- package/plugins/core/lib/querying-writing/compile-schemas.js +328 -0
- package/plugins/core/{utils → lib/querying-writing}/connectors-query-parser.js +62 -57
- package/plugins/core/lib/{database-capabilities.js → querying-writing/database-capabilities.js} +113 -7
- package/plugins/core/lib/querying-writing/database-value-normalizers.js +369 -0
- package/plugins/core/lib/querying-writing/default-data-helpers.js +72 -0
- package/plugins/core/{utils → lib/querying-writing}/field-utils.js +41 -31
- package/plugins/core/{utils → lib/querying-writing}/knex-constants.js +1 -0
- package/plugins/core/lib/{knex-field-helpers.js → querying-writing/knex-field-helpers.js} +190 -57
- package/plugins/core/lib/querying-writing/knex-json-api-transformers.js +162 -0
- package/plugins/core/lib/{payload-validators.js → querying-writing/payload-validators.js} +297 -217
- package/plugins/core/lib/querying-writing/schema-helpers.js +497 -0
- package/plugins/core/lib/querying-writing/scope-validations.js +431 -0
- package/plugins/core/lib/{simplified-helpers.js → querying-writing/simplified-helpers.js} +216 -176
- package/plugins/core/lib/writing/knex-json-api-transformers-writing.js +110 -0
- package/plugins/core/lib/writing/many-to-many-manipulations.js +300 -0
- package/plugins/core/lib/writing/relationship-processor.js +252 -0
- package/plugins/core/rest-api-knex-plugin.js +183 -62
- package/plugins/core/rest-api-plugin-hooks/compile-resource-schemas.js +7 -0
- package/plugins/core/rest-api-plugin-hooks/register-relationship-routes.js +72 -0
- package/plugins/core/rest-api-plugin-hooks/register-scope-routes.js +102 -0
- package/plugins/core/rest-api-plugin-hooks/turn-scope-init-into-vars.js +50 -0
- package/plugins/core/rest-api-plugin-hooks/validate-include-configurations.js +39 -0
- package/plugins/core/rest-api-plugin-methods/add-route.js +23 -0
- package/plugins/core/rest-api-plugin-methods/check-permissions.js +18 -0
- package/plugins/core/rest-api-plugin-methods/common.js +605 -0
- package/plugins/core/rest-api-plugin-methods/delete-relationship.js +115 -0
- package/plugins/core/rest-api-plugin-methods/delete.js +109 -0
- package/plugins/core/rest-api-plugin-methods/enrich-attributes.js +186 -0
- package/plugins/core/rest-api-plugin-methods/get-related.js +295 -0
- package/plugins/core/rest-api-plugin-methods/get-relationship.js +62 -0
- package/plugins/core/rest-api-plugin-methods/get.js +187 -0
- package/plugins/core/rest-api-plugin-methods/patch-relationship.js +56 -0
- package/plugins/core/rest-api-plugin-methods/patch.js +193 -0
- package/plugins/core/rest-api-plugin-methods/post-relationship.js +104 -0
- package/plugins/core/rest-api-plugin-methods/post.js +157 -0
- package/plugins/core/rest-api-plugin-methods/put.js +257 -0
- package/plugins/core/rest-api-plugin-methods/query.js +208 -0
- package/plugins/core/rest-api-plugin.js +108 -2498
- package/plugins/core/socketio-plugin.js +28 -5
- package/plugins/storage/local-storage.js +2 -2
- package/tests/computed-fields.test.js +483 -0
- package/tests/cors.test.js +4 -4
- package/tests/curl.test.js +5 -4
- package/tests/custom-idproperty-relationships.test.js +891 -0
- package/tests/field-getters.test.js +353 -0
- package/tests/field-setters.test.js +310 -0
- package/tests/fixtures/api-configs.js +800 -28
- package/tests/jwt-auth.test.js +0 -2
- package/tests/nested-includes.test.js +1 -1
- package/tests/pagination-cursor-multifield.test.js +514 -0
- package/tests/pagination-enhanced.test.js +12 -9
- package/tests/polymorphic-relationships.test.js +10 -8
- package/tests/polymorphic-simple.test.js +6 -4
- package/tests/positioning-advanced.test.js +12 -12
- package/tests/positioning-extended.test.js +25 -25
- package/tests/positioning-multifilter.test.js +24 -24
- package/tests/positioning.test.js +3 -3
- package/tests/query-limits.test.js +24 -8
- package/tests/relationship-endpoints.test.js +4 -5
- package/tests/relationships.test.js +0 -1
- package/tests/socketio.test.js +12 -1
- package/tests/virtual-fields.test.js +13 -13
- package/PLAN_FOR_WEBSOCKETS.md +0 -398
- package/README_1_Initial_Setup.md +0 -393
- package/README_X_Hooks.md +0 -647
- package/TODO_rest-api-plugin-extras.todo +0 -23
- package/connectors_ideas/express-plugin.js +0 -120
- package/connectors_ideas/http-plugin.js +0 -138
- package/docs/examples/express-plugin.js +0 -366
- package/example.js +0 -301
- package/plugins/core/HELPER_MODULES.md +0 -285
- package/plugins/core/lib/compile-schemas.js +0 -245
- package/plugins/core/lib/database-value-normalizers.js +0 -192
- package/plugins/core/lib/default-data-helpers.js +0 -100
- package/plugins/core/lib/knex-cross-table-search.js +0 -442
- package/plugins/core/lib/knex-json-api-helpers.js +0 -84
- package/plugins/core/lib/knex-json-api-transformers.js +0 -449
- package/plugins/core/lib/knex-pagination-helpers.js +0 -193
- package/plugins/core/lib/knex-window-queries.js +0 -142
- package/plugins/core/lib/many-to-many-manipulations.js +0 -343
- package/plugins/core/lib/relationship-processor.js +0 -278
- package/plugins/core/lib/schema-helpers.js +0 -343
- package/plugins/core/lib/scope-validations.js +0 -252
- package/plugins/core/rest-api-relationships-plugin.js +0 -700
- package/plugins/core/utils/knex-query-helpers-base.js +0 -56
- /package/{README_X_Permissions.md → docs/GUIDE/GUIDE_4_Authentication_And_Permissions.md} +0 -0
- /package/{README_X_Positioning.md → docs/GUIDE/GUIDE_6_Positioning.md} +0 -0
- /package/{README_X_Bulk_Operations.md → docs/GUIDE/GUIDE_X_Bulk_Operations.md} +0 -0
- /package/{README_X_Cors.md → docs/GUIDE/GUIDE_X_Cors.md} +0 -0
- /package/{README_X_Multihome.md → docs/GUIDE/GUIDE_X_Multihome.md} +0 -0
- /package/{README_X_Appendices.md → docs/GUIDE/GUIDE_Y_Appendices.md} +0 -0
- /package/{QUERY_DEPTH_PROTECTION.md → docs/README_VARIOUS/QUERY_DEPTH_PROTECTION.md} +0 -0
- /package/{RELATIONSHIPS.md → docs/README_VARIOUS/RELATIONSHIPS.md} +0 -0
- /package/{WEBSOCKET_IMPLEMENTATION_STATUS.md → docs/README_VARIOUS/WEBSOCKET_IMPLEMENTATION_STATUS.md} +0 -0
- /package/{helper-functions-chart.md → docs/README_VARIOUS/helper-functions-chart.md} +0 -0
- /package/docs/{implementation-hidden-computed-fields.md → README_VARIOUS/implementation-hidden-computed-fields.md} +0 -0
- /package/docs/{include-limits-migration-guide.md → README_VARIOUS/include-limits-migration-guide.md} +0 -0
- /package/{readmeSnippets → docs/README_VARIOUS}/test_put_behavior.js +0 -0
- /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
|
+
}
|