json-rest-api 1.0.7 → 1.0.8
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- 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 +90 -32
- 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
|
@@ -1,278 +0,0 @@
|
|
|
1
|
-
/**
|
|
2
|
-
* @module relationship-processor
|
|
3
|
-
* @description Relationship processing functions for REST API plugin
|
|
4
|
-
*
|
|
5
|
-
* This module handles the complex task of extracting relationship data from
|
|
6
|
-
* JSON:API payloads and converting them into database operations. It bridges
|
|
7
|
-
* the gap between JSON:API's relationship format and actual database foreign
|
|
8
|
-
* keys and pivot tables.
|
|
9
|
-
*/
|
|
10
|
-
|
|
11
|
-
import { RestApiValidationError } from '../../../lib/rest-api-errors.js';
|
|
12
|
-
|
|
13
|
-
/**
|
|
14
|
-
* Processes JSON:API relationship data from input records and converts them into database-ready foreign key updates
|
|
15
|
-
* and many-to-many relationship operations.
|
|
16
|
-
*
|
|
17
|
-
* This function is the central relationship processor that handles all types of relationships:
|
|
18
|
-
* 1. **belongsTo (1:1)**: Converts relationship references to foreign key values
|
|
19
|
-
* 2. **Polymorphic belongsTo**: Handles relationships that can point to multiple types
|
|
20
|
-
* 3. **Many-to-many**: Collects relationship data for pivot table operations
|
|
21
|
-
*
|
|
22
|
-
* The function analyzes the incoming JSON:API relationships block and the resource's schema
|
|
23
|
-
* to determine how to process each relationship. It returns two objects:
|
|
24
|
-
* - `belongsToUpdates`: Direct field updates for foreign keys
|
|
25
|
-
* - `manyToManyRelationships`: Array of many-to-many operations to perform
|
|
26
|
-
*
|
|
27
|
-
* @param {Object} scope - The scope object containing schema info
|
|
28
|
-
* @param {Object} deps - Dependencies object
|
|
29
|
-
* @param {Object} deps.context - Request context
|
|
30
|
-
* @param {Object} deps.context.inputRecord - The JSON:API input record containing relationship data
|
|
31
|
-
* @returns {Object} Object with belongsToUpdates and manyToManyRelationships arrays
|
|
32
|
-
* @throws {RestApiValidationError} If polymorphic relationship type is invalid
|
|
33
|
-
*
|
|
34
|
-
* @example
|
|
35
|
-
* // Example 1: Processing a simple belongsTo relationship
|
|
36
|
-
* const scope = api.resources['articles'];
|
|
37
|
-
* const deps = {
|
|
38
|
-
* context: {
|
|
39
|
-
* inputRecord: {
|
|
40
|
-
* data: {
|
|
41
|
-
* type: 'articles',
|
|
42
|
-
* attributes: { title: 'My Article' },
|
|
43
|
-
* relationships: {
|
|
44
|
-
* author: {
|
|
45
|
-
* data: { type: 'users', id: '123' }
|
|
46
|
-
* }
|
|
47
|
-
* }
|
|
48
|
-
* }
|
|
49
|
-
* }
|
|
50
|
-
* }
|
|
51
|
-
* };
|
|
52
|
-
* const result = processRelationships(scope, deps);
|
|
53
|
-
* // Returns:
|
|
54
|
-
* // {
|
|
55
|
-
* // belongsToUpdates: { author_id: '123' }, // Foreign key extracted
|
|
56
|
-
* // manyToManyRelationships: []
|
|
57
|
-
* // }
|
|
58
|
-
*
|
|
59
|
-
* @example
|
|
60
|
-
* // Example 2: Processing polymorphic relationships
|
|
61
|
-
* const scope = api.resources['comments'];
|
|
62
|
-
* const deps = {
|
|
63
|
-
* context: {
|
|
64
|
-
* inputRecord: {
|
|
65
|
-
* data: {
|
|
66
|
-
* type: 'comments',
|
|
67
|
-
* attributes: { text: 'Great!' },
|
|
68
|
-
* relationships: {
|
|
69
|
-
* commentable: {
|
|
70
|
-
* data: { type: 'posts', id: '456' } // Can be 'posts' or 'videos'
|
|
71
|
-
* }
|
|
72
|
-
* }
|
|
73
|
-
* }
|
|
74
|
-
* }
|
|
75
|
-
* }
|
|
76
|
-
* };
|
|
77
|
-
* const result = processRelationships(scope, deps);
|
|
78
|
-
* // Returns:
|
|
79
|
-
* // {
|
|
80
|
-
* // belongsToUpdates: {
|
|
81
|
-
* // commentable_type: 'posts', // Type field set
|
|
82
|
-
* // commentable_id: '456' // ID field set
|
|
83
|
-
* // },
|
|
84
|
-
* // manyToManyRelationships: []
|
|
85
|
-
* // }
|
|
86
|
-
*
|
|
87
|
-
* @example
|
|
88
|
-
* // Example 3: Processing many-to-many relationships
|
|
89
|
-
* const scope = api.resources['articles'];
|
|
90
|
-
* const deps = {
|
|
91
|
-
* context: {
|
|
92
|
-
* inputRecord: {
|
|
93
|
-
* data: {
|
|
94
|
-
* type: 'articles',
|
|
95
|
-
* attributes: { title: 'Tagged Article' },
|
|
96
|
-
* relationships: {
|
|
97
|
-
* tags: {
|
|
98
|
-
* data: [
|
|
99
|
-
* { type: 'tags', id: '10' },
|
|
100
|
-
* { type: 'tags', id: '20' },
|
|
101
|
-
* { type: 'tags', id: '30' }
|
|
102
|
-
* ]
|
|
103
|
-
* }
|
|
104
|
-
* }
|
|
105
|
-
* }
|
|
106
|
-
* }
|
|
107
|
-
* }
|
|
108
|
-
* };
|
|
109
|
-
* const result = processRelationships(scope, deps);
|
|
110
|
-
* // Returns:
|
|
111
|
-
* // {
|
|
112
|
-
* // belongsToUpdates: {},
|
|
113
|
-
* // manyToManyRelationships: [{
|
|
114
|
-
* // relName: 'tags',
|
|
115
|
-
* // relDef: {
|
|
116
|
-
* // through: 'article_tags',
|
|
117
|
-
* // foreignKey: 'article_id',
|
|
118
|
-
* // otherKey: 'tag_id'
|
|
119
|
-
* // },
|
|
120
|
-
* // relData: [
|
|
121
|
-
* // { type: 'tags', id: '10' },
|
|
122
|
-
* // { type: 'tags', id: '20' },
|
|
123
|
-
* // { type: 'tags', id: '30' }
|
|
124
|
-
* // ]
|
|
125
|
-
* // }]
|
|
126
|
-
* // }
|
|
127
|
-
*
|
|
128
|
-
* @example
|
|
129
|
-
* // Example 4: Clearing relationships (setting to null)
|
|
130
|
-
* const scope = api.resources['articles'];
|
|
131
|
-
* const deps = {
|
|
132
|
-
* context: {
|
|
133
|
-
* inputRecord: {
|
|
134
|
-
* data: {
|
|
135
|
-
* type: 'articles',
|
|
136
|
-
* relationships: {
|
|
137
|
-
* author: {
|
|
138
|
-
* data: null // Clear the author
|
|
139
|
-
* },
|
|
140
|
-
* tags: {
|
|
141
|
-
* data: [] // Clear all tags
|
|
142
|
-
* }
|
|
143
|
-
* }
|
|
144
|
-
* }
|
|
145
|
-
* }
|
|
146
|
-
* }
|
|
147
|
-
* };
|
|
148
|
-
* const result = processRelationships(scope, deps);
|
|
149
|
-
* // Returns:
|
|
150
|
-
* // {
|
|
151
|
-
* // belongsToUpdates: { author_id: null }, // Foreign key cleared
|
|
152
|
-
* // manyToManyRelationships: [{
|
|
153
|
-
* // relName: 'tags',
|
|
154
|
-
* // relDef: { ... },
|
|
155
|
-
* // relData: [] // Empty array will clear all pivot records
|
|
156
|
-
* // }]
|
|
157
|
-
* // }
|
|
158
|
-
*
|
|
159
|
-
* @example
|
|
160
|
-
* // Example 5: Invalid polymorphic type throws error
|
|
161
|
-
* const scope = api.resources['comments'];
|
|
162
|
-
* const deps = {
|
|
163
|
-
* context: {
|
|
164
|
-
* inputRecord: {
|
|
165
|
-
* data: {
|
|
166
|
-
* relationships: {
|
|
167
|
-
* commentable: {
|
|
168
|
-
* data: { type: 'invalid_type', id: '123' }
|
|
169
|
-
* }
|
|
170
|
-
* }
|
|
171
|
-
* }
|
|
172
|
-
* }
|
|
173
|
-
* }
|
|
174
|
-
* };
|
|
175
|
-
* // Throws RestApiValidationError:
|
|
176
|
-
* // "Invalid type 'invalid_type' for polymorphic relationship 'commentable'. Allowed types: posts, videos"
|
|
177
|
-
*
|
|
178
|
-
* @example <caption>Why this is useful upstream</caption>
|
|
179
|
-
* // The REST API plugin uses this to:
|
|
180
|
-
* // 1. Convert JSON:API relationships to database-ready foreign keys
|
|
181
|
-
* // 2. Validate polymorphic relationships against allowed types
|
|
182
|
-
* // 3. Prepare many-to-many operations for pivot table updates
|
|
183
|
-
* // 4. Support both setting and clearing relationships (null/empty array)
|
|
184
|
-
* // 5. Maintain data integrity by validating relationships before database writes
|
|
185
|
-
* // 6. Abstract the complexity of different relationship types from storage plugins
|
|
186
|
-
*/
|
|
187
|
-
export const processRelationships = (scope, deps) => {
|
|
188
|
-
// Extract values from scope
|
|
189
|
-
const {
|
|
190
|
-
vars: {
|
|
191
|
-
schemaInfo: { schemaStructure: schemaFields, schemaRelationships: relationships }
|
|
192
|
-
}
|
|
193
|
-
} = scope;
|
|
194
|
-
|
|
195
|
-
// Extract values from deps
|
|
196
|
-
const { context } = deps;
|
|
197
|
-
const { inputRecord } = context;
|
|
198
|
-
const belongsToUpdates = {};
|
|
199
|
-
const manyToManyRelationships = [];
|
|
200
|
-
|
|
201
|
-
if (!inputRecord.data.relationships) {
|
|
202
|
-
return { belongsToUpdates, manyToManyRelationships };
|
|
203
|
-
}
|
|
204
|
-
|
|
205
|
-
for (const [relName, relData] of Object.entries(inputRecord.data.relationships)) {
|
|
206
|
-
const relDef = relationships?.[relName];
|
|
207
|
-
|
|
208
|
-
// Find the schema field that defines this relationship
|
|
209
|
-
const schemaField = Object.entries(schemaFields).find(([fieldName, fieldDef]) =>
|
|
210
|
-
fieldDef.as === relName
|
|
211
|
-
);
|
|
212
|
-
|
|
213
|
-
if (schemaField) {
|
|
214
|
-
const [fieldName, fieldDef] = schemaField;
|
|
215
|
-
|
|
216
|
-
// Handle regular belongsTo (1:1)
|
|
217
|
-
if (fieldDef.belongsTo && !fieldDef.belongsToPolymorphic) {
|
|
218
|
-
if (relData.data === null) {
|
|
219
|
-
belongsToUpdates[fieldName] = null;
|
|
220
|
-
} else if (relData.data?.id) {
|
|
221
|
-
belongsToUpdates[fieldName] = relData.data.id;
|
|
222
|
-
}
|
|
223
|
-
}
|
|
224
|
-
// Handle polymorphic belongsTo
|
|
225
|
-
else if (fieldDef.belongsToPolymorphic) {
|
|
226
|
-
if (relData.data === null) {
|
|
227
|
-
const { typeField, idField } = fieldDef.belongsToPolymorphic;
|
|
228
|
-
belongsToUpdates[typeField] = null;
|
|
229
|
-
belongsToUpdates[idField] = null;
|
|
230
|
-
} else if (relData.data) {
|
|
231
|
-
const { type, id } = relData.data;
|
|
232
|
-
const { types, typeField, idField } = fieldDef.belongsToPolymorphic;
|
|
233
|
-
|
|
234
|
-
// Validate type is allowed
|
|
235
|
-
if (!types.includes(type)) {
|
|
236
|
-
throw new RestApiValidationError(
|
|
237
|
-
`Invalid type '${type}' for polymorphic relationship '${relName}'. Allowed types: ${types.join(', ')}`,
|
|
238
|
-
{
|
|
239
|
-
fields: [`data.relationships.${relName}.data.type`],
|
|
240
|
-
violations: [{
|
|
241
|
-
field: `data.relationships.${relName}.data.type`,
|
|
242
|
-
rule: 'polymorphic_type',
|
|
243
|
-
message: `Type must be one of: ${types.join(', ')}`
|
|
244
|
-
}]
|
|
245
|
-
}
|
|
246
|
-
);
|
|
247
|
-
}
|
|
248
|
-
|
|
249
|
-
belongsToUpdates[typeField] = type;
|
|
250
|
-
belongsToUpdates[idField] = id;
|
|
251
|
-
}
|
|
252
|
-
}
|
|
253
|
-
}
|
|
254
|
-
|
|
255
|
-
// Check for many-to-many relationships defined in relationships object
|
|
256
|
-
if (relDef?.manyToMany && relData.data !== undefined) {
|
|
257
|
-
manyToManyRelationships.push({
|
|
258
|
-
relName,
|
|
259
|
-
relDef: relDef.manyToMany, // Pass the manyToMany object, not the whole relationship
|
|
260
|
-
relData: relData.data || [] // null means empty array for many-to-many
|
|
261
|
-
});
|
|
262
|
-
}
|
|
263
|
-
// Also check for hasMany with through (alternative many-to-many syntax)
|
|
264
|
-
else if (relDef?.hasMany && relDef?.through && relData.data !== undefined) {
|
|
265
|
-
manyToManyRelationships.push({
|
|
266
|
-
relName,
|
|
267
|
-
relDef: {
|
|
268
|
-
through: relDef.through,
|
|
269
|
-
foreignKey: relDef.foreignKey,
|
|
270
|
-
otherKey: relDef.otherKey
|
|
271
|
-
},
|
|
272
|
-
relData: relData.data || [] // null means empty array for many-to-many
|
|
273
|
-
});
|
|
274
|
-
}
|
|
275
|
-
}
|
|
276
|
-
|
|
277
|
-
return { belongsToUpdates, manyToManyRelationships };
|
|
278
|
-
};
|
|
@@ -1,343 +0,0 @@
|
|
|
1
|
-
/**
|
|
2
|
-
* @module schemaHelpers
|
|
3
|
-
* @description Schema helper functions for REST API plugin
|
|
4
|
-
*
|
|
5
|
-
* This module provides utilities for processing and enhancing schemas,
|
|
6
|
-
* particularly for search functionality. It ensures search fields are
|
|
7
|
-
* properly indexed and generates comprehensive search schemas from
|
|
8
|
-
* various configuration sources.
|
|
9
|
-
*/
|
|
10
|
-
|
|
11
|
-
import { RestApiValidationError } from '../../../lib/rest-api-errors.js';
|
|
12
|
-
|
|
13
|
-
/**
|
|
14
|
-
* Automatically marks all fields in a searchSchema as indexed to enable efficient cross-table searches.
|
|
15
|
-
*
|
|
16
|
-
* This function is essential for the cross-table search feature, which allows searching across
|
|
17
|
-
* multiple related resources in a single query. By marking fields as indexed, it signals to
|
|
18
|
-
* storage plugins (like Knex) that these fields should have database indexes created for
|
|
19
|
-
* optimal query performance.
|
|
20
|
-
*
|
|
21
|
-
* The function modifies the searchSchema in-place, adding `indexed: true` to each field
|
|
22
|
-
* definition. This is typically called during scope initialization to prepare search fields
|
|
23
|
-
* for efficient querying.
|
|
24
|
-
*
|
|
25
|
-
* @param {Object} searchSchema - The searchSchema object to process
|
|
26
|
-
* @returns {void} Modifies the searchSchema in-place
|
|
27
|
-
*
|
|
28
|
-
* @example
|
|
29
|
-
* // Example 1: Basic searchSchema indexing
|
|
30
|
-
* const searchSchema = {
|
|
31
|
-
* title: { type: 'string', filterUsing: 'contains' },
|
|
32
|
-
* status: { type: 'string', filterUsing: '=' },
|
|
33
|
-
* created_at: { type: 'datetime', filterUsing: '>=' }
|
|
34
|
-
* };
|
|
35
|
-
* ensureSearchFieldsAreIndexed(searchSchema);
|
|
36
|
-
* // After processing:
|
|
37
|
-
* // {
|
|
38
|
-
* // title: { type: 'string', filterUsing: 'contains', indexed: true },
|
|
39
|
-
* // status: { type: 'string', filterUsing: '=', indexed: true },
|
|
40
|
-
* // created_at: { type: 'datetime', filterUsing: '>=', indexed: true }
|
|
41
|
-
* // }
|
|
42
|
-
*
|
|
43
|
-
* @example
|
|
44
|
-
* // Example 2: Virtual fields for cross-table search
|
|
45
|
-
* const searchSchema = {
|
|
46
|
-
* // Regular field
|
|
47
|
-
* name: { type: 'string', filterUsing: 'contains' },
|
|
48
|
-
* // Virtual field that searches in related table
|
|
49
|
-
* author_name: {
|
|
50
|
-
* type: 'string',
|
|
51
|
-
* filterUsing: 'contains',
|
|
52
|
-
* virtualField: {
|
|
53
|
-
* joinTo: 'users',
|
|
54
|
-
* joinOn: ['author_id', 'id'],
|
|
55
|
-
* searchField: 'name'
|
|
56
|
-
* }
|
|
57
|
-
* }
|
|
58
|
-
* };
|
|
59
|
-
* ensureSearchFieldsAreIndexed(searchSchema);
|
|
60
|
-
* // Both fields are marked as indexed, enabling efficient joins
|
|
61
|
-
*
|
|
62
|
-
* @example
|
|
63
|
-
* // Example 3: Already indexed fields are not modified
|
|
64
|
-
* const searchSchema = {
|
|
65
|
-
* email: { type: 'string', filterUsing: '=', indexed: false }, // Explicitly false
|
|
66
|
-
* username: { type: 'string', filterUsing: '=', indexed: true } // Already true
|
|
67
|
-
* };
|
|
68
|
-
* ensureSearchFieldsAreIndexed(searchSchema);
|
|
69
|
-
* // Results in:
|
|
70
|
-
* // {
|
|
71
|
-
* // email: { type: 'string', filterUsing: '=', indexed: true }, // Overridden to true
|
|
72
|
-
* // username: { type: 'string', filterUsing: '=', indexed: true } // Unchanged
|
|
73
|
-
* // }
|
|
74
|
-
*
|
|
75
|
-
* @example
|
|
76
|
-
* // Example 4: Null or undefined searchSchema is safely handled
|
|
77
|
-
* ensureSearchFieldsAreIndexed(null); // No error, returns immediately
|
|
78
|
-
* ensureSearchFieldsAreIndexed(undefined); // No error, returns immediately
|
|
79
|
-
* ensureSearchFieldsAreIndexed({}); // Empty object remains empty
|
|
80
|
-
*
|
|
81
|
-
* @example <caption>Why this is useful upstream</caption>
|
|
82
|
-
* // The REST API plugin uses this to:
|
|
83
|
-
* // 1. Signal to storage plugins (like Knex) which fields need database indexes
|
|
84
|
-
* // 2. Enable efficient filtering on search fields without full table scans
|
|
85
|
-
* // 3. Support cross-table searches by ensuring join fields are indexed
|
|
86
|
-
* // 4. Improve API response times for filtered queries
|
|
87
|
-
* // 5. Allow storage plugins to create indexes automatically during migrations
|
|
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 a complete searchSchema by intelligently merging field definitions from the main schema
|
|
103
|
-
* (fields with 'search' property) and an explicit searchSchema, with conflict detection.
|
|
104
|
-
*
|
|
105
|
-
* This function is a cornerstone of the REST API's flexible search configuration. It allows
|
|
106
|
-
* developers to define searchable fields in three ways:
|
|
107
|
-
* 1. Inline in the schema with `search: true` or `search: { config }`
|
|
108
|
-
* 2. In a separate explicit searchSchema
|
|
109
|
-
* 3. As virtual fields in schema._virtual.search
|
|
110
|
-
*
|
|
111
|
-
* The function performs intelligent merging while preventing conflicts that could lead to
|
|
112
|
-
* ambiguous search behavior. It supports advanced search patterns like:
|
|
113
|
-
* - Simple field searches with various operators (=, >, <, contains, etc.)
|
|
114
|
-
* - Multiple filters from one field (e.g., date -> date_before, date_after)
|
|
115
|
-
* - Virtual fields that don't exist in the database but map to searches
|
|
116
|
-
*
|
|
117
|
-
* @param {Object} schema - The main resource schema that may contain fields with 'search' property
|
|
118
|
-
* @param {Object} explicitSearchSchema - Optional explicit searchSchema to merge with
|
|
119
|
-
* @returns {Object|null} The complete merged searchSchema, or null if no search fields defined
|
|
120
|
-
* @throws {RestApiValidationError} If the same field is defined in multiple places
|
|
121
|
-
*
|
|
122
|
-
* @example
|
|
123
|
-
* // Example 1: Simple search fields in schema
|
|
124
|
-
* const schema = {
|
|
125
|
-
* title: { type: 'string', search: true }, // Simple search
|
|
126
|
-
* content: { type: 'text' }, // Not searchable
|
|
127
|
-
* status: { type: 'string', search: { filterUsing: '=' } } // Custom operator
|
|
128
|
-
* };
|
|
129
|
-
* const searchSchema = generateSearchSchemaFromSchema(schema, null);
|
|
130
|
-
* // Returns:
|
|
131
|
-
* // {
|
|
132
|
-
* // title: { type: 'string', filterUsing: '=' },
|
|
133
|
-
* // status: { type: 'string', filterUsing: '=' }
|
|
134
|
-
* // }
|
|
135
|
-
*
|
|
136
|
-
* @example
|
|
137
|
-
* // Example 2: Multiple filters from one field
|
|
138
|
-
* const schema = {
|
|
139
|
-
* published_at: {
|
|
140
|
-
* type: 'datetime',
|
|
141
|
-
* search: {
|
|
142
|
-
* published_after: { filterUsing: '>=', type: 'datetime' },
|
|
143
|
-
* published_before: { filterUsing: '<=', type: 'datetime' }
|
|
144
|
-
* }
|
|
145
|
-
* }
|
|
146
|
-
* };
|
|
147
|
-
* const searchSchema = generateSearchSchemaFromSchema(schema, null);
|
|
148
|
-
* // Returns:
|
|
149
|
-
* // {
|
|
150
|
-
* // published_after: {
|
|
151
|
-
* // type: 'datetime',
|
|
152
|
-
* // actualField: 'published_at', // Maps to real field
|
|
153
|
-
* // filterUsing: '>='
|
|
154
|
-
* // },
|
|
155
|
-
* // published_before: {
|
|
156
|
-
* // type: 'datetime',
|
|
157
|
-
* // actualField: 'published_at',
|
|
158
|
-
* // filterUsing: '<='
|
|
159
|
-
* // }
|
|
160
|
-
* // }
|
|
161
|
-
*
|
|
162
|
-
* @example
|
|
163
|
-
* // Example 3: Merging with explicit searchSchema
|
|
164
|
-
* const schema = {
|
|
165
|
-
* name: { type: 'string', search: true }
|
|
166
|
-
* };
|
|
167
|
-
* const explicitSearchSchema = {
|
|
168
|
-
* email: { type: 'string', filterUsing: '=' },
|
|
169
|
-
* age: { type: 'number', filterUsing: '>=' }
|
|
170
|
-
* };
|
|
171
|
-
* const searchSchema = generateSearchSchemaFromSchema(schema, explicitSearchSchema);
|
|
172
|
-
* // Returns:
|
|
173
|
-
* // {
|
|
174
|
-
* // email: { type: 'string', filterUsing: '=' }, // From explicit
|
|
175
|
-
* // age: { type: 'number', filterUsing: '>=' }, // From explicit
|
|
176
|
-
* // name: { type: 'string', filterUsing: '=' } // From schema
|
|
177
|
-
* // }
|
|
178
|
-
*
|
|
179
|
-
* @example
|
|
180
|
-
* // Example 4: Conflict detection
|
|
181
|
-
* const schema = {
|
|
182
|
-
* email: { type: 'string', search: true }
|
|
183
|
-
* };
|
|
184
|
-
* const explicitSearchSchema = {
|
|
185
|
-
* email: { type: 'string', filterUsing: 'contains' }
|
|
186
|
-
* };
|
|
187
|
-
* generateSearchSchemaFromSchema(schema, explicitSearchSchema);
|
|
188
|
-
* // Throws RestApiValidationError:
|
|
189
|
-
* // "Field 'email' is defined in both schema (with search: true) and explicit searchSchema."
|
|
190
|
-
*
|
|
191
|
-
* @example
|
|
192
|
-
* // Example 5: Virtual search fields
|
|
193
|
-
* const schema = {
|
|
194
|
-
* title: { type: 'string' },
|
|
195
|
-
* _virtual: {
|
|
196
|
-
* search: {
|
|
197
|
-
* category_name: {
|
|
198
|
-
* type: 'string',
|
|
199
|
-
* filterUsing: 'contains',
|
|
200
|
-
* virtualField: {
|
|
201
|
-
* joinTo: 'categories',
|
|
202
|
-
* joinOn: ['category_id', 'id'],
|
|
203
|
-
* searchField: 'name'
|
|
204
|
-
* }
|
|
205
|
-
* }
|
|
206
|
-
* }
|
|
207
|
-
* }
|
|
208
|
-
* };
|
|
209
|
-
* const searchSchema = generateSearchSchemaFromSchema(schema, null);
|
|
210
|
-
* // Returns:
|
|
211
|
-
* // {
|
|
212
|
-
* // category_name: {
|
|
213
|
-
* // type: 'string',
|
|
214
|
-
* // filterUsing: 'contains',
|
|
215
|
-
* // virtualField: { ... } // Virtual field config for cross-table search
|
|
216
|
-
* // }
|
|
217
|
-
* // }
|
|
218
|
-
*
|
|
219
|
-
* @example <caption>Why this is useful upstream</caption>
|
|
220
|
-
* // The REST API plugin uses this to:
|
|
221
|
-
* // 1. Provide flexible search configuration without duplicating field definitions
|
|
222
|
-
* // 2. Enable virtual fields that don't exist in the database (like category_name)
|
|
223
|
-
* // 3. Support complex search patterns (date ranges, cross-table searches)
|
|
224
|
-
* // 4. Validate search configuration at startup to catch errors early
|
|
225
|
-
* // 5. Allow storage plugins to optimize queries based on search patterns
|
|
226
|
-
* // 6. Enable API consumers to filter by related data without complex joins
|
|
227
|
-
*/
|
|
228
|
-
export const generateSearchSchemaFromSchema = (schema, explicitSearchSchema) => {
|
|
229
|
-
// Start with explicit searchSchema or empty object
|
|
230
|
-
const searchSchema = explicitSearchSchema ? {...explicitSearchSchema} : {};
|
|
231
|
-
|
|
232
|
-
if (!schema) {
|
|
233
|
-
return Object.keys(searchSchema).length > 0 ? searchSchema : null;
|
|
234
|
-
}
|
|
235
|
-
|
|
236
|
-
// Process schema fields with 'search' property
|
|
237
|
-
Object.entries(schema).forEach(([fieldName, fieldDef]) => {
|
|
238
|
-
const effectiveSearch = fieldDef.search;
|
|
239
|
-
|
|
240
|
-
if (effectiveSearch) {
|
|
241
|
-
if (effectiveSearch === true) {
|
|
242
|
-
// Check for conflicts with explicit searchSchema
|
|
243
|
-
if (searchSchema[fieldName]) {
|
|
244
|
-
throw new RestApiValidationError(
|
|
245
|
-
`Field '${fieldName}' is defined in both schema (with search: true) and explicit searchSchema. Remove one definition to avoid conflicts.`,
|
|
246
|
-
{
|
|
247
|
-
fields: [fieldName],
|
|
248
|
-
violations: [{
|
|
249
|
-
field: fieldName,
|
|
250
|
-
rule: 'duplicate_search_field',
|
|
251
|
-
message: 'Field cannot be defined in both schema and searchSchema'
|
|
252
|
-
}]
|
|
253
|
-
}
|
|
254
|
-
);
|
|
255
|
-
}
|
|
256
|
-
|
|
257
|
-
// Simple boolean - copy entire field definition (except search) and add filterUsing
|
|
258
|
-
const { search, ...fieldDefWithoutSearch } = fieldDef;
|
|
259
|
-
searchSchema[fieldName] = {
|
|
260
|
-
...fieldDefWithoutSearch,
|
|
261
|
-
// Default filter behavior based on type
|
|
262
|
-
filterUsing: fieldDef.type === 'string' ? '=' : '='
|
|
263
|
-
};
|
|
264
|
-
} else if (typeof effectiveSearch === 'object') {
|
|
265
|
-
// Check if search defines multiple filter fields
|
|
266
|
-
const hasNestedFilters = Object.values(effectiveSearch).some(
|
|
267
|
-
v => typeof v === 'object' && v.filterUsing
|
|
268
|
-
);
|
|
269
|
-
|
|
270
|
-
if (hasNestedFilters) {
|
|
271
|
-
// Multiple filters from one field (like published_after/before)
|
|
272
|
-
Object.entries(effectiveSearch).forEach(([filterName, filterDef]) => {
|
|
273
|
-
// Check for conflicts
|
|
274
|
-
if (searchSchema[filterName]) {
|
|
275
|
-
throw new RestApiValidationError(
|
|
276
|
-
`Field '${filterName}' is defined in both schema (with search) and explicit searchSchema. Remove one definition to avoid conflicts.`,
|
|
277
|
-
{
|
|
278
|
-
fields: [filterName],
|
|
279
|
-
violations: [{
|
|
280
|
-
field: filterName,
|
|
281
|
-
rule: 'duplicate_search_field',
|
|
282
|
-
message: 'Field cannot be defined in both schema and searchSchema'
|
|
283
|
-
}]
|
|
284
|
-
}
|
|
285
|
-
);
|
|
286
|
-
}
|
|
287
|
-
|
|
288
|
-
searchSchema[filterName] = {
|
|
289
|
-
type: fieldDef.type,
|
|
290
|
-
actualField: fieldName,
|
|
291
|
-
...filterDef
|
|
292
|
-
};
|
|
293
|
-
});
|
|
294
|
-
} else {
|
|
295
|
-
// Check for conflicts
|
|
296
|
-
if (searchSchema[fieldName]) {
|
|
297
|
-
throw new RestApiValidationError(
|
|
298
|
-
`Field '${fieldName}' is defined in both schema (with search) and explicit searchSchema. Remove one definition to avoid conflicts.`,
|
|
299
|
-
{
|
|
300
|
-
fields: [fieldName],
|
|
301
|
-
violations: [{
|
|
302
|
-
field: fieldName,
|
|
303
|
-
rule: 'duplicate_search_field',
|
|
304
|
-
message: 'Field cannot be defined in both schema and searchSchema'
|
|
305
|
-
}]
|
|
306
|
-
}
|
|
307
|
-
);
|
|
308
|
-
}
|
|
309
|
-
|
|
310
|
-
// Single filter with config
|
|
311
|
-
searchSchema[fieldName] = {
|
|
312
|
-
type: fieldDef.type,
|
|
313
|
-
...effectiveSearch
|
|
314
|
-
};
|
|
315
|
-
}
|
|
316
|
-
}
|
|
317
|
-
}
|
|
318
|
-
});
|
|
319
|
-
|
|
320
|
-
// Handle _virtual search definitions
|
|
321
|
-
if (schema._virtual?.search) {
|
|
322
|
-
Object.entries(schema._virtual.search).forEach(([filterName, filterDef]) => {
|
|
323
|
-
// Check for conflicts
|
|
324
|
-
if (searchSchema[filterName]) {
|
|
325
|
-
throw new RestApiValidationError(
|
|
326
|
-
`Field '${filterName}' is defined in both schema (_virtual.search) and explicit searchSchema. Remove one definition to avoid conflicts.`,
|
|
327
|
-
{
|
|
328
|
-
fields: [filterName],
|
|
329
|
-
violations: [{
|
|
330
|
-
field: filterName,
|
|
331
|
-
rule: 'duplicate_search_field',
|
|
332
|
-
message: 'Field cannot be defined in both schema and searchSchema'
|
|
333
|
-
}]
|
|
334
|
-
}
|
|
335
|
-
);
|
|
336
|
-
}
|
|
337
|
-
|
|
338
|
-
searchSchema[filterName] = filterDef;
|
|
339
|
-
});
|
|
340
|
-
}
|
|
341
|
-
|
|
342
|
-
return Object.keys(searchSchema).length > 0 ? searchSchema : null;
|
|
343
|
-
};
|