json-rest-api 1.0.21 → 1.0.22
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/audit/00-charter.md +103 -0
- package/audit/01-scope-map.md +84 -0
- package/audit/02-coverage-matrix.csv +174 -0
- package/audit/03-invariants.md +123 -0
- package/audit/04-standards-rubric.md +56 -0
- package/audit/05-competitor-rubric.md +77 -0
- package/audit/06-findings.md +160 -0
- package/audit/07-risk-register.csv +10 -0
- package/audit/08-remediation-roadmap.md +177 -0
- package/audit/09-signoff-check.md +23 -0
- package/audit/10-final-synthesis.md +29 -0
- package/audit/evidence/code/all-files-json-rest-api.txt +168 -0
- package/audit/evidence/code/all-files-json-rest-schema.txt +12 -0
- package/audit/evidence/code/boundary-leak-checks.txt +72 -0
- package/audit/evidence/code/critical-pass1/json-rest-api__plugins__core__bulk-operations-plugin.js.txt +480 -0
- package/audit/evidence/code/critical-pass1/json-rest-api__plugins__core__connectors__express-plugin.js.txt +532 -0
- package/audit/evidence/code/critical-pass1/json-rest-api__plugins__core__connectors__lib__busboy-detector.js.txt +175 -0
- package/audit/evidence/code/critical-pass1/json-rest-api__plugins__core__connectors__lib__formidable-detector.js.txt +138 -0
- package/audit/evidence/code/critical-pass1/json-rest-api__plugins__core__connectors__lib__request-helpers.js.txt +222 -0
- package/audit/evidence/code/critical-pass1/json-rest-api__plugins__core__file-handling-plugin.js.txt +342 -0
- package/audit/evidence/code/critical-pass1/json-rest-api__plugins__core__lib__dbTablesOperations.js.txt +304 -0
- package/audit/evidence/code/critical-pass1/json-rest-api__plugins__core__lib__fractional-positioning.js.txt +188 -0
- package/audit/evidence/code/critical-pass1/json-rest-api__plugins__core__lib__querying-writing__compile-schemas.js.txt +336 -0
- package/audit/evidence/code/critical-pass1/json-rest-api__plugins__core__lib__querying-writing__connectors-query-parser.js.txt +165 -0
- package/audit/evidence/code/critical-pass1/json-rest-api__plugins__core__lib__querying-writing__database-capabilities.js.txt +265 -0
- package/audit/evidence/code/critical-pass1/json-rest-api__plugins__core__lib__querying-writing__database-value-normalizers.js.txt +369 -0
- package/audit/evidence/code/critical-pass1/json-rest-api__plugins__core__lib__querying-writing__default-data-helpers.js.txt +72 -0
- package/audit/evidence/code/critical-pass1/json-rest-api__plugins__core__lib__querying-writing__field-utils.js.txt +139 -0
- package/audit/evidence/code/critical-pass1/json-rest-api__plugins__core__lib__querying-writing__knex-constants.js.txt +45 -0
- package/audit/evidence/code/critical-pass1/json-rest-api__plugins__core__lib__querying-writing__knex-field-helpers.js.txt +396 -0
- package/audit/evidence/code/critical-pass1/json-rest-api__plugins__core__lib__querying-writing__knex-json-api-transformers.js.txt +162 -0
- package/audit/evidence/code/critical-pass1/json-rest-api__plugins__core__lib__querying-writing__payload-validators.js.txt +1314 -0
- package/audit/evidence/code/critical-pass1/json-rest-api__plugins__core__lib__querying-writing__schema-helpers.js.txt +488 -0
- package/audit/evidence/code/critical-pass1/json-rest-api__plugins__core__lib__querying-writing__scope-validations.js.txt +484 -0
- package/audit/evidence/code/critical-pass1/json-rest-api__plugins__core__lib__querying-writing__simplified-helpers.js.txt +857 -0
- package/audit/evidence/code/critical-pass1/json-rest-api__plugins__core__lib__querying__knex-cross-table-search.js.txt +565 -0
- package/audit/evidence/code/critical-pass1/json-rest-api__plugins__core__lib__querying__knex-json-api-transformers-querying.js.txt +485 -0
- package/audit/evidence/code/critical-pass1/json-rest-api__plugins__core__lib__querying__knex-pagination-helpers.js.txt +560 -0
- package/audit/evidence/code/critical-pass1/json-rest-api__plugins__core__lib__querying__knex-process-includes.js.txt +167 -0
- package/audit/evidence/code/critical-pass1/json-rest-api__plugins__core__lib__querying__knex-query-helpers-base.js.txt +109 -0
- package/audit/evidence/code/critical-pass1/json-rest-api__plugins__core__lib__querying__knex-query-helpers.js.txt +1064 -0
- package/audit/evidence/code/critical-pass1/json-rest-api__plugins__core__lib__querying__knex-relationship-includes.js.txt +1983 -0
- package/audit/evidence/code/critical-pass1/json-rest-api__plugins__core__lib__querying__knex-window-queries.js.txt +328 -0
- package/audit/evidence/code/critical-pass1/json-rest-api__plugins__core__lib__querying__url-helpers.js.txt +119 -0
- package/audit/evidence/code/critical-pass1/json-rest-api__plugins__core__lib__storage__storage-adapter.js.txt +160 -0
- package/audit/evidence/code/critical-pass1/json-rest-api__plugins__core__lib__writing__many-to-many-manipulations.js.txt +300 -0
- package/audit/evidence/code/critical-pass1/json-rest-api__plugins__core__lib__writing__relationship-processor.js.txt +245 -0
- package/audit/evidence/code/critical-pass1/json-rest-api__plugins__core__rest-api-access.js.txt +459 -0
- package/audit/evidence/code/critical-pass1/json-rest-api__plugins__core__rest-api-cors-plugin.js.txt +237 -0
- package/audit/evidence/code/critical-pass1/json-rest-api__plugins__core__rest-api-knex-plugin.js.txt +1135 -0
- package/audit/evidence/code/critical-pass1/json-rest-api__plugins__core__rest-api-multihome-plugin.js.txt +314 -0
- package/audit/evidence/code/critical-pass1/json-rest-api__plugins__core__rest-api-plugin-hooks__compile-resource-schemas.js.txt +7 -0
- package/audit/evidence/code/critical-pass1/json-rest-api__plugins__core__rest-api-plugin-hooks__register-relationship-routes.js.txt +71 -0
- package/audit/evidence/code/critical-pass1/json-rest-api__plugins__core__rest-api-plugin-hooks__register-scope-routes.js.txt +102 -0
- package/audit/evidence/code/critical-pass1/json-rest-api__plugins__core__rest-api-plugin-hooks__turn-scope-init-into-vars.js.txt +49 -0
- package/audit/evidence/code/critical-pass1/json-rest-api__plugins__core__rest-api-plugin-hooks__validate-include-configurations.js.txt +39 -0
- package/audit/evidence/code/critical-pass1/json-rest-api__plugins__core__rest-api-plugin-methods__add-route.js.txt +23 -0
- package/audit/evidence/code/critical-pass1/json-rest-api__plugins__core__rest-api-plugin-methods__check-permissions.js.txt +13 -0
- package/audit/evidence/code/critical-pass1/json-rest-api__plugins__core__rest-api-plugin-methods__common.js.txt +639 -0
- package/audit/evidence/code/critical-pass1/json-rest-api__plugins__core__rest-api-plugin-methods__delete-relationship.js.txt +117 -0
- package/audit/evidence/code/critical-pass1/json-rest-api__plugins__core__rest-api-plugin-methods__delete.js.txt +104 -0
- package/audit/evidence/code/critical-pass1/json-rest-api__plugins__core__rest-api-plugin-methods__enrich-attributes.js.txt +199 -0
- package/audit/evidence/code/critical-pass1/json-rest-api__plugins__core__rest-api-plugin-methods__get-related.js.txt +353 -0
- package/audit/evidence/code/critical-pass1/json-rest-api__plugins__core__rest-api-plugin-methods__get-relationship.js.txt +61 -0
- package/audit/evidence/code/critical-pass1/json-rest-api__plugins__core__rest-api-plugin-methods__get.js.txt +184 -0
- package/audit/evidence/code/critical-pass1/json-rest-api__plugins__core__rest-api-plugin-methods__patch-relationship.js.txt +55 -0
- package/audit/evidence/code/critical-pass1/json-rest-api__plugins__core__rest-api-plugin-methods__patch.js.txt +191 -0
- package/audit/evidence/code/critical-pass1/json-rest-api__plugins__core__rest-api-plugin-methods__post-relationship.js.txt +110 -0
- package/audit/evidence/code/critical-pass1/json-rest-api__plugins__core__rest-api-plugin-methods__post.js.txt +168 -0
- package/audit/evidence/code/critical-pass1/json-rest-api__plugins__core__rest-api-plugin-methods__put.js.txt +253 -0
- package/audit/evidence/code/critical-pass1/json-rest-api__plugins__core__rest-api-plugin-methods__query.js.txt +207 -0
- package/audit/evidence/code/critical-pass1/json-rest-api__plugins__core__rest-api-plugin-methods__release.js.txt +12 -0
- package/audit/evidence/code/critical-pass1/json-rest-api__plugins__core__rest-api-plugin.js.txt +165 -0
- package/audit/evidence/code/critical-pass1/json-rest-api__plugins__core__rest-api-positioning-plugin.js.txt +668 -0
- package/audit/evidence/code/critical-pass1/json-rest-api__plugins__core__socketio-plugin.js.txt +621 -0
- package/audit/evidence/code/critical-pass1/json-rest-api__plugins__storage__local-storage.js.txt +218 -0
- package/audit/evidence/code/critical-pass1/json-rest-api__plugins__storage__s3-storage.js.txt +124 -0
- package/audit/evidence/code/critical-pass1/json-rest-schema__src__CorePlugin.js.txt +269 -0
- package/audit/evidence/code/critical-pass1/json-rest-schema__src__Schema.js.txt +242 -0
- package/audit/evidence/code/critical-pass1/json-rest-schema__src__index.js.txt +77 -0
- package/audit/evidence/code/critical-pass1-risk-keywords.txt +238 -0
- package/audit/evidence/code/critical-pass1-summary.csv +67 -0
- package/audit/evidence/code/critical-pass2-summary.csv +67 -0
- package/audit/evidence/code/early-silent-zones-no-test-token.txt +57 -0
- package/audit/evidence/code/error-model-signals.txt +98 -0
- package/audit/evidence/code/git-files-json-rest-api.txt +171 -0
- package/audit/evidence/code/git-files-json-rest-schema.txt +27 -0
- package/audit/evidence/code/hook-flow-signals.txt +282 -0
- package/audit/evidence/code/idproperty-signals.txt +157 -0
- package/audit/evidence/code/import-cycles-non-anyapi.txt +3 -0
- package/audit/evidence/code/import-hotspots-non-anyapi.txt +96 -0
- package/audit/evidence/code/imports-json-rest-api.txt +354 -0
- package/audit/evidence/code/imports-json-rest-schema.txt +15 -0
- package/audit/evidence/code/in-scope-files-json-rest-api.txt +160 -0
- package/audit/evidence/code/in-scope-files-json-rest-schema.txt +13 -0
- package/audit/evidence/code/non-anyapi-core-files.txt +101 -0
- package/audit/evidence/code/permission-signals.txt +193 -0
- package/audit/evidence/code/query-include-signals.txt +687 -0
- package/audit/evidence/code/transaction-signals.txt +28 -0
- package/audit/evidence/code/validation-signals.txt +650 -0
- package/audit/evidence/code/write-path-signals.txt +61 -0
- package/audit/evidence/docs/docs-claims-signals.txt +1075 -0
- package/audit/evidence/docs/schema-docs-claims-signals.txt +179 -0
- package/audit/evidence/external/corpus-freeze.md +21 -0
- package/audit/evidence/external/npm-competitor-metadata.txt +70 -0
- package/audit/evidence/external/npm-download-signals.txt +23 -0
- package/audit/evidence/external/npm-jsonapi-server.txt +8 -0
- package/audit/evidence/external/npm-modern-jsonapi-metadata.txt +38 -0
- package/audit/evidence/external/readme-fortune-signals.txt +26 -0
- package/audit/evidence/external/readme-fortune.md +165 -0
- package/audit/evidence/external/readme-json-api-serializer-signals.txt +80 -0
- package/audit/evidence/external/readme-json-api-serializer.md +690 -0
- package/audit/evidence/external/readme-jsonapi-serde-server-signals.txt +0 -0
- package/audit/evidence/external/readme-jsonapi-serde-server.md +22 -0
- package/audit/evidence/external/readme-jsonapi-serializer-signals.txt +63 -0
- package/audit/evidence/external/readme-jsonapi-serializer.md +315 -0
- package/audit/evidence/external/readme-jsonapi-server-signals.txt +19 -0
- package/audit/evidence/external/readme-jsonapi-server.md +104 -0
- package/audit/evidence/external/readme-nestjs-json-api-signals.txt +4 -0
- package/audit/evidence/external/readme-nestjs-json-api.md +94 -0
- package/audit/evidence/external/readme-yaser-json-api-nestjs-signals.txt +47 -0
- package/audit/evidence/external/readme-yaser-json-api-nestjs.md +447 -0
- package/audit/evidence/external/standard-jsonapi-snippets.txt +84 -0
- package/audit/evidence/external/standard-rfc-snippets.txt +168 -0
- package/audit/evidence/external/standards-sources.txt +6 -0
- package/audit/evidence/runtime/baseline-commits.txt +2 -0
- package/audit/evidence/runtime/db-adapter-assumptions.txt +3 -0
- package/audit/evidence/runtime/environment.txt +4 -0
- package/audit/evidence/runtime/invariant-runtime-mapping.md +38 -0
- package/audit/evidence/runtime/probe-patch-relationship-error.exitcode +1 -0
- package/audit/evidence/runtime/probe-patch-relationship-error.log +76 -0
- package/audit/evidence/runtime/probe-patch-relationship-error.mjs +67 -0
- package/audit/evidence/runtime/probe-patch-relationship-referenceerror.exitcode +1 -0
- package/audit/evidence/runtime/probe-patch-relationship-referenceerror.log +62 -0
- package/audit/evidence/runtime/probe-patch-relationship-referenceerror.mjs +47 -0
- package/audit/evidence/runtime/test-exitcodes.txt +4 -0
- package/audit/evidence/runtime/test-summary.md +107 -0
- package/audit/evidence/runtime/trace-anchors.txt +61 -0
- package/audit/evidence/tests/access-multitenancy-test-signals.txt +125 -0
- package/audit/evidence/tests/failing-tests-summary.txt +25 -0
- package/audit/evidence/tests/npm-test-json-rest-api.exitcode +1 -0
- package/audit/evidence/tests/npm-test-json-rest-api.log +5885 -0
- package/audit/evidence/tests/npm-test-json-rest-schema.exitcode +1 -0
- package/audit/evidence/tests/npm-test-json-rest-schema.log +356 -0
- package/audit/evidence/tests/query-include-test-signals.txt +948 -0
- package/audit/evidence/tests/relationship-test-signals.txt +223 -0
- package/audit/evidence/tests/skipped-tests.txt +10 -0
- package/audit/evidence/tests/test-suite-list-non-anyapi.txt +30 -0
- package/audit/evidence/tests/test-suite-list.txt +32 -0
- package/audit/subsystems/docs-drift.md +49 -0
- package/audit/subsystems/pass2-cross-review.md +131 -0
- package/audit/subsystems/plugins-access.md +51 -0
- package/audit/subsystems/plugins-bulk.md +50 -0
- package/audit/subsystems/plugins-cors.md +48 -0
- package/audit/subsystems/plugins-file.md +53 -0
- package/audit/subsystems/plugins-multihome.md +50 -0
- package/audit/subsystems/plugins-positioning.md +54 -0
- package/audit/subsystems/plugins-socketio.md +50 -0
- package/audit/subsystems/query-engine.md +61 -0
- package/audit/subsystems/relationships.md +55 -0
- package/audit/subsystems/rest-orchestration.md +66 -0
- package/audit/subsystems/schema-core.md +64 -0
- package/audit/subsystems/storage-knex.md +57 -0
- package/audit/subsystems/tests.md +48 -0
- package/audit/subsystems/transport-express.md +55 -0
- package/audit/traces/trace-read-get.md +22 -0
- package/audit/traces/trace-read-query.md +25 -0
- package/audit/traces/trace-relationship-endpoints.md +31 -0
- package/audit/traces/trace-write-delete.md +17 -0
- package/audit/traces/trace-write-patch.md +26 -0
- package/audit/traces/trace-write-post.md +22 -0
- package/audit/traces/trace-write-put.md +23 -0
- package/package.json +3 -3
|
@@ -0,0 +1,488 @@
|
|
|
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
|
|
7
|
+
7 * - Generating search schemas from various configuration sources
|
|
8
|
+
8 * - Sorting fields by their dependencies (for getters/setters)
|
|
9
|
+
9 * - Detecting circular dependencies in field definitions
|
|
10
|
+
10 */
|
|
11
|
+
11
|
|
12
|
+
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 = {
|
|
23
|
+
23 * title: { type: 'string', filterOperator: 'contains' },
|
|
24
|
+
24 * status: { type: 'string', filterOperator: '=' },
|
|
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 = {
|
|
39
|
+
39 * name: { type: 'string', filterOperator: 'contains' },
|
|
40
|
+
40 * author_name: { // Virtual field
|
|
41
|
+
41 * type: 'string',
|
|
42
|
+
42 * filterOperator: 'contains',
|
|
43
|
+
43 * virtualField: {
|
|
44
|
+
44 * joinTo: 'users',
|
|
45
|
+
45 * joinOn: ['author_id', 'id'],
|
|
46
|
+
46 * searchField: 'name'
|
|
47
|
+
47 * }
|
|
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 = {
|
|
61
|
+
61 * email: { type: 'string', indexed: false }, // Explicitly false
|
|
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
|
+
89 export function ensureSearchFieldsAreIndexed (searchSchema) {
|
|
90
|
+
90 if (!searchSchema) return
|
|
91
|
+
91
|
|
92
|
+
92 Object.keys(searchSchema).forEach(fieldName => {
|
|
93
|
+
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
|
+
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 = {
|
|
112
|
+
112 * title: { type: 'string', search: true }, // Simple
|
|
113
|
+
113 * content: { type: 'text' }, // Not searchable
|
|
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' }, // No operator set; resolver applies defaults
|
|
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 = {
|
|
127
|
+
127 * published_at: {
|
|
128
|
+
128 * type: 'datetime',
|
|
129
|
+
129 * search: {
|
|
130
|
+
130 * published_after: { filterOperator: '>=', type: 'datetime' },
|
|
131
|
+
131 * published_before: { filterOperator: '<=', type: 'datetime' }
|
|
132
|
+
132 * }
|
|
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
|
+
139 * // published_after: {
|
|
140
|
+
140 * // type: 'datetime',
|
|
141
|
+
141 * // actualField: 'published_at', // Maps to real field
|
|
142
|
+
142 * // filterOperator: '>='
|
|
143
|
+
143 * // },
|
|
144
|
+
144 * // published_before: {
|
|
145
|
+
145 * // type: 'datetime',
|
|
146
|
+
146 * // actualField: 'published_at', // Same field, different operator
|
|
147
|
+
147 * // filterOperator: '<='
|
|
148
|
+
148 * // }
|
|
149
|
+
149 * // }
|
|
150
|
+
150 *
|
|
151
|
+
151 * @example
|
|
152
|
+
152 * // Input: Merging with explicit searchSchema
|
|
153
|
+
153 * const schema = {
|
|
154
|
+
154 * name: { type: 'string', search: true }
|
|
155
|
+
155 * };
|
|
156
|
+
156 * const explicitSearchSchema = {
|
|
157
|
+
157 * email: { type: 'string', filterOperator: '=' },
|
|
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' } // From schema (no operator set)
|
|
167
|
+
167 * // }
|
|
168
|
+
168 *
|
|
169
|
+
169 * @example
|
|
170
|
+
170 * // Input: Explicit searchSchema takes precedence
|
|
171
|
+
171 * const schema = {
|
|
172
|
+
172 * email: { type: 'string', search: true }
|
|
173
|
+
173 * };
|
|
174
|
+
174 * const explicitSearchSchema = {
|
|
175
|
+
175 * email: { type: 'string', filterOperator: 'contains' }
|
|
176
|
+
176 * };
|
|
177
|
+
177 * const result = generateSearchSchemaFromSchema(schema, explicitSearchSchema);
|
|
178
|
+
178 * // Output: Explicit searchSchema wins
|
|
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 = {
|
|
186
|
+
186 * title: { type: 'string' },
|
|
187
|
+
187 * _virtual: {
|
|
188
|
+
188 * search: {
|
|
189
|
+
189 * category_name: { // Doesn't exist in DB
|
|
190
|
+
190 * type: 'string',
|
|
191
|
+
191 * filterOperator: 'contains',
|
|
192
|
+
192 * virtualField: {
|
|
193
|
+
193 * joinTo: 'categories',
|
|
194
|
+
194 * joinOn: ['category_id', 'id'], // JOIN condition
|
|
195
|
+
195 * searchField: 'name' // Field in related table
|
|
196
|
+
196 * }
|
|
197
|
+
197 * }
|
|
198
|
+
198 * }
|
|
199
|
+
199 * }
|
|
200
|
+
200 * };
|
|
201
|
+
201 *
|
|
202
|
+
202 * // Output: Virtual field for cross-table search
|
|
203
|
+
203 * // {
|
|
204
|
+
204 * // category_name: {
|
|
205
|
+
205 * // type: 'string',
|
|
206
|
+
206 * // filterOperator: 'contains',
|
|
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
|
|
219
|
+
219 * - Supports range queries (before/after patterns)
|
|
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
|
|
227
|
+
227 * 3. Skips fields already in explicit searchSchema (no conflicts)
|
|
228
|
+
228 * 4. Handles multiple filters from single field
|
|
229
|
+
229 * 5. Processes virtual search definitions
|
|
230
|
+
230 * 6. Returns merged searchSchema or null
|
|
231
|
+
231 */
|
|
232
|
+
232 export const generateSearchSchemaFromSchema = (schema, explicitSearchSchema) => {
|
|
233
|
+
233 // Start with explicit searchSchema or empty object
|
|
234
|
+
234 const searchSchema = explicitSearchSchema ? { ...explicitSearchSchema } : {}
|
|
235
|
+
235
|
|
236
|
+
236 if (!schema) {
|
|
237
|
+
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
|
+
242 const effectiveSearch = fieldDef.search
|
|
243
|
+
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
|
+
247 const searchFieldName = (fieldDef.belongsTo && fieldDef.as) ? fieldDef.as : fieldName
|
|
248
|
+
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
|
+
253 return
|
|
254
|
+
254 }
|
|
255
|
+
255
|
|
256
|
+
256 // Simple boolean - copy entire field definition (except search)
|
|
257
|
+
257 const { search, ...fieldDefWithoutSearch } = fieldDef
|
|
258
|
+
258 const searchEntry = {
|
|
259
|
+
259 ...fieldDefWithoutSearch,
|
|
260
|
+
260 // Do not set filterOperator here; centralized resolver will apply sensible defaults
|
|
261
|
+
261 }
|
|
262
|
+
262
|
|
263
|
+
263 // For belongsTo relationships, add metadata to map back to the actual field
|
|
264
|
+
264 if (fieldDef.belongsTo && fieldDef.as) {
|
|
265
|
+
265 searchEntry.actualField = fieldName // Maps to the DB column
|
|
266
|
+
266 searchEntry.isRelationship = true
|
|
267
|
+
267 searchEntry.targetResource = fieldDef.belongsTo
|
|
268
|
+
268 }
|
|
269
|
+
269
|
|
270
|
+
270 searchSchema[searchFieldName] = searchEntry
|
|
271
|
+
271 } else if (typeof effectiveSearch === 'object') {
|
|
272
|
+
272 // Check if search defines multiple filter fields
|
|
273
|
+
273 const hasNestedFilters = Object.values(effectiveSearch).some(
|
|
274
|
+
274 v => typeof v === 'object' && v.filterOperator
|
|
275
|
+
275 )
|
|
276
|
+
276
|
|
277
|
+
277 if (hasNestedFilters) {
|
|
278
|
+
278 // Multiple filters from one field (like published_after/before)
|
|
279
|
+
279 Object.entries(effectiveSearch).forEach(([filterName, filterDef]) => {
|
|
280
|
+
280 // Check if filter already exists in explicit searchSchema
|
|
281
|
+
281 if (searchSchema[filterName]) {
|
|
282
|
+
282 // Skip - explicit searchSchema takes precedence
|
|
283
|
+
283 return
|
|
284
|
+
284 }
|
|
285
|
+
285
|
|
286
|
+
286 searchSchema[filterName] = {
|
|
287
|
+
287 type: fieldDef.type,
|
|
288
|
+
288 actualField: fieldName,
|
|
289
|
+
289 ...filterDef
|
|
290
|
+
290 }
|
|
291
|
+
291 })
|
|
292
|
+
292 } else {
|
|
293
|
+
293 // For belongsTo relationships, use the relationship name (as property) instead of the field name
|
|
294
|
+
294 const searchFieldName = (fieldDef.belongsTo && fieldDef.as) ? fieldDef.as : fieldName
|
|
295
|
+
295
|
|
296
|
+
296 // Check if field already exists in explicit searchSchema
|
|
297
|
+
297 if (searchSchema[searchFieldName]) {
|
|
298
|
+
298 // Skip - explicit searchSchema takes precedence
|
|
299
|
+
299 return
|
|
300
|
+
300 }
|
|
301
|
+
301
|
|
302
|
+
302 // Single filter with config
|
|
303
|
+
303 const searchEntry = {
|
|
304
|
+
304 type: fieldDef.type,
|
|
305
|
+
305 ...effectiveSearch
|
|
306
|
+
306 }
|
|
307
|
+
307
|
|
308
|
+
308 // For belongsTo relationships, add metadata to map back to the actual field
|
|
309
|
+
309 if (fieldDef.belongsTo && fieldDef.as) {
|
|
310
|
+
310 searchEntry.actualField = fieldName // Maps to the DB column
|
|
311
|
+
311 searchEntry.isRelationship = true
|
|
312
|
+
312 searchEntry.targetResource = fieldDef.belongsTo
|
|
313
|
+
313 }
|
|
314
|
+
314
|
|
315
|
+
315 searchSchema[searchFieldName] = searchEntry
|
|
316
|
+
316 }
|
|
317
|
+
317 }
|
|
318
|
+
318 }
|
|
319
|
+
319 })
|
|
320
|
+
320
|
|
321
|
+
321 // Handle _virtual search definitions
|
|
322
|
+
322 if (schema._virtual?.search) {
|
|
323
|
+
323 Object.entries(schema._virtual.search).forEach(([filterName, filterDef]) => {
|
|
324
|
+
324 // Check if filter already exists in explicit searchSchema
|
|
325
|
+
325 if (searchSchema[filterName]) {
|
|
326
|
+
326 // Skip - explicit searchSchema takes precedence
|
|
327
|
+
327 return
|
|
328
|
+
328 }
|
|
329
|
+
329
|
|
330
|
+
330 searchSchema[filterName] = filterDef
|
|
331
|
+
331 })
|
|
332
|
+
332 }
|
|
333
|
+
333
|
|
334
|
+
334 return Object.keys(searchSchema).length > 0 ? searchSchema : null
|
|
335
|
+
335 }
|
|
336
|
+
336
|
|
337
|
+
337 /**
|
|
338
|
+
338 * Generic topological sort for handling dependencies
|
|
339
|
+
339 *
|
|
340
|
+
340 * @param {Array} items - Array of items to sort
|
|
341
|
+
341 * @param {Function} getDependencies - Function that returns dependencies for an item
|
|
342
|
+
342 * @returns {Array} Sorted array respecting dependencies
|
|
343
|
+
343 * @throws {Error} If circular dependencies or unknown dependencies detected
|
|
344
|
+
344 *
|
|
345
|
+
345 * @example
|
|
346
|
+
346 * // Input: Simple dependency chain
|
|
347
|
+
347 * const items = ['a', 'b', 'c'];
|
|
348
|
+
348 * const deps = { a: ['b'], b: ['c'], c: [] };
|
|
349
|
+
349 * const sorted = topologicalSort(items, item => deps[item]);
|
|
350
|
+
350 * // Output: ['c', 'b', 'a']
|
|
351
|
+
351 * // c first (no deps), then b (depends on c), then a (depends on b)
|
|
352
|
+
352 *
|
|
353
|
+
353 * @example
|
|
354
|
+
354 * // Input: Circular dependency
|
|
355
|
+
355 * const items = ['a', 'b'];
|
|
356
|
+
356 * const deps = { a: ['b'], b: ['a'] }; // Circular!
|
|
357
|
+
357 * topologicalSort(items, item => deps[item]);
|
|
358
|
+
358 * // Throws: Error "Circular dependency detected: b"
|
|
359
|
+
359 *
|
|
360
|
+
360 * @example
|
|
361
|
+
361 * // Input: Unknown dependency
|
|
362
|
+
362 * const items = ['a', 'b'];
|
|
363
|
+
363 * const deps = { a: ['c'], b: [] }; // 'c' not in items
|
|
364
|
+
364 * topologicalSort(items, item => deps[item]);
|
|
365
|
+
365 * // Throws: Error "Unknown dependency 'c' for 'a'"
|
|
366
|
+
366 *
|
|
367
|
+
367 * @description
|
|
368
|
+
368 * Used by:
|
|
369
|
+
369 * - sortFieldsByDependencies for ordering field operations
|
|
370
|
+
370 * - Any code needing dependency-based ordering
|
|
371
|
+
371 *
|
|
372
|
+
372 * Purpose:
|
|
373
|
+
373 * - Orders items so dependencies come before dependents
|
|
374
|
+
374 * - Detects circular dependencies early
|
|
375
|
+
375 * - Validates all dependencies exist
|
|
376
|
+
376 * - Uses depth-first search algorithm
|
|
377
|
+
377 */
|
|
378
|
+
378 export function topologicalSort (items, getDependencies) {
|
|
379
|
+
379 const sorted = []
|
|
380
|
+
380 const visited = new Set()
|
|
381
|
+
381 const visiting = new Set()
|
|
382
|
+
382
|
|
383
|
+
383 function visit (item) {
|
|
384
|
+
384 if (visited.has(item)) return
|
|
385
|
+
385
|
|
386
|
+
386 if (visiting.has(item)) {
|
|
387
|
+
387 throw new Error(`Circular dependency detected: ${item}`)
|
|
388
|
+
388 }
|
|
389
|
+
389
|
|
390
|
+
390 visiting.add(item)
|
|
391
|
+
391
|
|
392
|
+
392 const dependencies = getDependencies(item) || []
|
|
393
|
+
393 for (const dep of dependencies) {
|
|
394
|
+
394 if (!items.includes(dep)) {
|
|
395
|
+
395 throw new Error(`Unknown dependency '${dep}' for '${item}'`)
|
|
396
|
+
396 }
|
|
397
|
+
397 visit(dep)
|
|
398
|
+
398 }
|
|
399
|
+
399
|
|
400
|
+
400 visiting.delete(item)
|
|
401
|
+
401 visited.add(item)
|
|
402
|
+
402 sorted.push(item)
|
|
403
|
+
403 }
|
|
404
|
+
404
|
|
405
|
+
405 for (const item of items) {
|
|
406
|
+
406 visit(item)
|
|
407
|
+
407 }
|
|
408
|
+
408
|
|
409
|
+
409 return sorted
|
|
410
|
+
410 }
|
|
411
|
+
411
|
|
412
|
+
412 /**
|
|
413
|
+
413 * Sorts fields by their dependencies using topological sort
|
|
414
|
+
414 *
|
|
415
|
+
415 * @param {Object} fields - Object with field definitions
|
|
416
|
+
416 * @param {string} dependencyProperty - Property name containing dependencies
|
|
417
|
+
417 * @returns {Array} Field names sorted by dependencies
|
|
418
|
+
418 * @throws {Error} If circular dependencies or unknown fields detected
|
|
419
|
+
419 *
|
|
420
|
+
420 * @example
|
|
421
|
+
421 * // Input: Getter dependencies (fullName needs firstName and lastName)
|
|
422
|
+
422 * const fields = {
|
|
423
|
+
423 * firstName: { getter: v => v.trim() },
|
|
424
|
+
424 * lastName: { getter: v => v.trim() },
|
|
425
|
+
425 * fullName: {
|
|
426
|
+
426 * getter: (v, ctx) => `${ctx.attributes.firstName} ${ctx.attributes.lastName}`,
|
|
427
|
+
427 * runGetterAfter: ['firstName', 'lastName']
|
|
428
|
+
428 * }
|
|
429
|
+
429 * };
|
|
430
|
+
430 * const sorted = sortFieldsByDependencies(fields, 'runGetterAfter');
|
|
431
|
+
431 * // Output: ['firstName', 'lastName', 'fullName']
|
|
432
|
+
432 * // Ensures firstName/lastName getters run before fullName
|
|
433
|
+
433 *
|
|
434
|
+
434 * @example
|
|
435
|
+
435 * // Input: Complex dependency chain
|
|
436
|
+
436 * const fields = {
|
|
437
|
+
437 * a: { runGetterAfter: ['b', 'c'] }, // a needs b and c
|
|
438
|
+
438 * b: { runGetterAfter: ['d'] }, // b needs d
|
|
439
|
+
439 * c: { runGetterAfter: ['d'] }, // c needs d
|
|
440
|
+
440 * d: { runGetterAfter: [] } // d needs nothing
|
|
441
|
+
441 * };
|
|
442
|
+
442 * const sorted = sortFieldsByDependencies(fields, 'runGetterAfter');
|
|
443
|
+
443 * // Output: ['d', 'b', 'c', 'a']
|
|
444
|
+
444 * // d first, then b/c (both need d), then a (needs b/c)
|
|
445
|
+
445 *
|
|
446
|
+
446 * @example
|
|
447
|
+
447 * // Input: Circular dependency error
|
|
448
|
+
448 * const fields = {
|
|
449
|
+
449 * a: { runGetterAfter: ['b'] },
|
|
450
|
+
450 * b: { runGetterAfter: ['a'] } // Circular!
|
|
451
|
+
451 * };
|
|
452
|
+
452 * sortFieldsByDependencies(fields, 'runGetterAfter');
|
|
453
|
+
453 * // Throws: Error "Circular dependency detected: a in runGetterAfter"
|
|
454
|
+
454 *
|
|
455
|
+
455 * @description
|
|
456
|
+
456 * Used by:
|
|
457
|
+
457 * - Schema processing for getter/setter ordering
|
|
458
|
+
458 * - Ensures dependent fields process after dependencies
|
|
459
|
+
459 *
|
|
460
|
+
460 * Purpose:
|
|
461
|
+
461 * - Orders field operations by dependencies
|
|
462
|
+
462 * - Enables computed fields that depend on other fields
|
|
463
|
+
463 * - Validates dependency graph is acyclic
|
|
464
|
+
464 * - Provides clear error messages for debugging
|
|
465
|
+
465 *
|
|
466
|
+
466 * Data flow:
|
|
467
|
+
467 * 1. Extracts field names from object
|
|
468
|
+
468 * 2. Calls topologicalSort with dependency function
|
|
469
|
+
469 * 3. Returns ordered field names
|
|
470
|
+
470 * 4. Enhances error messages with property name
|
|
471
|
+
471 */
|
|
472
|
+
472 export function sortFieldsByDependencies (fields, dependencyProperty) {
|
|
473
|
+
473 const fieldNames = Object.keys(fields)
|
|
474
|
+
474
|
|
475
|
+
475 if (fieldNames.length === 0) return []
|
|
476
|
+
476
|
|
477
|
+
477 try {
|
|
478
|
+
478 return topologicalSort(fieldNames, (fieldName) => {
|
|
479
|
+
479 const field = fields[fieldName]
|
|
480
|
+
480 return field[dependencyProperty] || []
|
|
481
|
+
481 })
|
|
482
|
+
482 } catch (error) {
|
|
483
|
+
483 if (error.message.includes('Circular dependency')) {
|
|
484
|
+
484 throw new Error(`${error.message} in ${dependencyProperty}`)
|
|
485
|
+
485 }
|
|
486
|
+
486 throw error
|
|
487
|
+
487 }
|
|
488
|
+
488 }
|