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.
- package/connectors_ideas/fastify.js +66 -68
- package/connectors_ideas/koa-plugin.js +50 -51
- package/eslint.config.js +11 -2
- package/examples/url-override-example.js +63 -63
- package/index.js +13 -13
- package/lib/enhanced-logger.js +51 -51
- package/lib/error-formatter.js +61 -61
- package/lib/rest-api-errors.js +28 -28
- package/package.json +1 -1
- package/plugins/core/bulk-operations-plugin.js +142 -145
- package/plugins/core/connectors/express-plugin.js +210 -217
- package/plugins/core/connectors/lib/busboy-detector.js +84 -84
- package/plugins/core/connectors/lib/formidable-detector.js +50 -50
- package/plugins/core/connectors/lib/request-helpers.js +74 -74
- package/plugins/core/file-handling-plugin.js +125 -125
- package/plugins/core/lib/anyapi/anyapi-registry.js +254 -254
- package/plugins/core/lib/anyapi/query/anyapi-query-adapter.js +287 -287
- package/plugins/core/lib/anyapi/schema-utils.js +101 -101
- package/plugins/core/lib/anyapi/utils/descriptor-helpers.js +41 -41
- package/plugins/core/lib/dbTablesOperations.js +134 -135
- package/plugins/core/lib/fractional-positioning.js +74 -76
- package/plugins/core/lib/querying/knex-cross-table-search.js +320 -320
- package/plugins/core/lib/querying/knex-json-api-transformers-querying.js +144 -144
- package/plugins/core/lib/querying/knex-pagination-helpers.js +164 -164
- package/plugins/core/lib/querying/knex-process-includes.js +50 -50
- package/plugins/core/lib/querying/knex-query-helpers-base.js +31 -31
- package/plugins/core/lib/querying/knex-query-helpers.js +451 -452
- package/plugins/core/lib/querying/knex-relationship-includes.js +1160 -1170
- package/plugins/core/lib/querying/knex-window-queries.js +89 -89
- package/plugins/core/lib/querying/url-helpers.js +35 -35
- package/plugins/core/lib/querying-writing/compile-schemas.js +99 -99
- package/plugins/core/lib/querying-writing/connectors-query-parser.js +31 -31
- package/plugins/core/lib/querying-writing/database-capabilities.js +94 -94
- package/plugins/core/lib/querying-writing/database-value-normalizers.js +76 -76
- package/plugins/core/lib/querying-writing/default-data-helpers.js +27 -27
- package/plugins/core/lib/querying-writing/field-utils.js +44 -42
- package/plugins/core/lib/querying-writing/knex-constants.js +15 -16
- package/plugins/core/lib/querying-writing/knex-field-helpers.js +117 -115
- package/plugins/core/lib/querying-writing/knex-json-api-transformers.js +41 -41
- package/plugins/core/lib/querying-writing/payload-validators.js +275 -275
- package/plugins/core/lib/querying-writing/schema-helpers.js +129 -129
- package/plugins/core/lib/querying-writing/scope-validations.js +143 -146
- package/plugins/core/lib/querying-writing/simplified-helpers.js +133 -129
- package/plugins/core/lib/storage/storage-adapter.js +68 -68
- package/plugins/core/lib/writing/knex-json-api-transformers-writing.js +31 -31
- package/plugins/core/lib/writing/many-to-many-manipulations.js +81 -81
- package/plugins/core/lib/writing/relationship-processor.js +58 -58
- package/plugins/core/rest-api-access.js +11 -15
- package/plugins/core/rest-api-anyapi-knex-plugin.js +920 -920
- package/plugins/core/rest-api-cors-plugin.js +170 -171
- package/plugins/core/rest-api-knex-plugin.js +482 -493
- package/plugins/core/rest-api-multihome-plugin.js +247 -251
- package/plugins/core/rest-api-plugin-hooks/compile-resource-schemas.js +5 -5
- package/plugins/core/rest-api-plugin-hooks/register-relationship-routes.js +20 -21
- package/plugins/core/rest-api-plugin-hooks/register-scope-routes.js +44 -44
- package/plugins/core/rest-api-plugin-hooks/turn-scope-init-into-vars.js +23 -23
- package/plugins/core/rest-api-plugin-hooks/validate-include-configurations.js +15 -15
- package/plugins/core/rest-api-plugin-methods/add-route.js +9 -9
- package/plugins/core/rest-api-plugin-methods/check-permissions.js +5 -6
- package/plugins/core/rest-api-plugin-methods/common.js +295 -297
- package/plugins/core/rest-api-plugin-methods/delete-relationship.js +35 -37
- package/plugins/core/rest-api-plugin-methods/delete.js +57 -59
- package/plugins/core/rest-api-plugin-methods/enrich-attributes.js +172 -171
- package/plugins/core/rest-api-plugin-methods/get-related.js +96 -96
- package/plugins/core/rest-api-plugin-methods/get-relationship.js +16 -18
- package/plugins/core/rest-api-plugin-methods/get.js +86 -86
- package/plugins/core/rest-api-plugin-methods/patch-relationship.js +16 -17
- package/plugins/core/rest-api-plugin-methods/patch.js +63 -64
- package/plugins/core/rest-api-plugin-methods/post-relationship.js +54 -55
- package/plugins/core/rest-api-plugin-methods/post.js +61 -62
- package/plugins/core/rest-api-plugin-methods/put.js +86 -87
- package/plugins/core/rest-api-plugin-methods/query.js +88 -88
- package/plugins/core/rest-api-plugin-methods/release.js +5 -6
- package/plugins/core/rest-api-plugin.js +76 -78
- package/plugins/core/rest-api-positioning-plugin.js +251 -252
- package/plugins/core/socketio-plugin.js +240 -236
- package/plugins/storage/local-storage.js +103 -103
- package/plugins/storage/s3-storage.js +41 -42
- package/quickTest.js +37 -37
- package/scripts/debug-anyapi-review-filter.mjs +18 -18
- package/scripts/inspect-anyapi-descriptor-country.mjs +10 -10
- package/scripts/inspect-anyapi-descriptor.mjs +10 -10
- package/scripts/run-docs.js +112 -113
- package/test-debug.js +28 -28
- package/tests/TEST_TEMPLATE.test.js +51 -51
- package/tests/anyapi-basic.test.js +42 -42
- package/tests/anyapi-cursor-pagination.test.js +48 -48
- package/tests/bulk-operations.test.js +208 -208
- package/tests/computed-fields.test.js +204 -204
- package/tests/cors.test.js +412 -413
- package/tests/curl.test.js +275 -276
- package/tests/custom-idproperty-relationships.test.js +266 -266
- package/tests/field-getters.test.js +132 -132
- package/tests/field-setters.test.js +110 -110
- package/tests/fixtures/api-configs.js +699 -699
- package/tests/helpers/storage-mode.js +32 -32
- package/tests/helpers/test-utils.js +76 -76
- package/tests/include-depth-validation.test.js +76 -76
- package/tests/includes.test.js +145 -145
- package/tests/multihome.test.js +175 -175
- package/tests/nested-includes.test.js +255 -255
- package/tests/pagination-cursor-multifield.test.js +164 -164
- package/tests/pagination-enhanced.test.js +225 -225
- package/tests/polymorphic-relationships.test.js +96 -96
- package/tests/polymorphic-simple.test.js +44 -44
- package/tests/positioning-advanced.test.js +226 -226
- package/tests/positioning-extended.test.js +399 -399
- package/tests/positioning-multifilter.test.js +204 -204
- package/tests/positioning.test.js +266 -266
- package/tests/queries.test.js +184 -184
- package/tests/query-limits.test.js +154 -155
- package/tests/relationship-endpoints.test.js +127 -127
- package/tests/relationships.test.js +178 -179
- package/tests/rest-api-access.test.js +86 -86
- package/tests/rest-api.test.js +103 -103
- package/tests/searchschema-merge.test.js +93 -93
- package/tests/socketio.test.js +326 -326
- 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
|
|
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
|
|
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
|
+
}
|