json-rest-api 1.0.6 → 1.0.8

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (146) hide show
  1. package/.claude/agents/library-guide-expert.md +29 -0
  2. package/.claude/agents/rest-api-internals-expert.md +53 -0
  3. package/CLAUDE.md +139 -1
  4. package/README.md +2 -5
  5. package/docTest.js +238 -0
  6. package/docs/GUIDE/GUIDE_1_Initial_Setup.md +955 -0
  7. package/docs/GUIDE/GUIDE_2_1_The_Starting_Point.md +54 -0
  8. package/docs/GUIDE/GUIDE_2_2_Manipulating_And_Searching_Tables.md +750 -0
  9. package/docs/GUIDE/GUIDE_2_3_BelongsTo_Relationships.md +494 -0
  10. package/docs/GUIDE/GUIDE_2_4_HasMany_Records.md +398 -0
  11. package/docs/GUIDE/GUIDE_2_5_HasMany_Polymorphic.md +882 -0
  12. package/docs/GUIDE/GUIDE_2_6_Many_To_Many.md +709 -0
  13. package/docs/GUIDE/GUIDE_2_7_Pagination_And_Ordering.md +1233 -0
  14. package/docs/GUIDE/GUIDE_2_8_Effects_of_PUT_and_PATCH.md +796 -0
  15. package/{README_X_Relationship_Plugin.md → docs/GUIDE/GUIDE_2_9_Relationships_Urls.md} +1 -4
  16. package/docs/GUIDE/GUIDE_2_Data_And_Relations.md +66 -0
  17. package/docs/GUIDE/GUIDE_3_Field_Transformations.md +1246 -0
  18. package/docs/GUIDE/GUIDE_5_Non-Db_Stores.md +0 -0
  19. package/docs/GUIDE/GUIDE_7_Hooks_Data_Management_And_Plugins.md +1490 -0
  20. package/{README_X_SocketIO.md → docs/GUIDE/GUIDE_X_SocketIO.md} +17 -17
  21. package/docs/GUIDE/index.md +58 -0
  22. package/docs/GUIDE.md +61 -2238
  23. package/docs/ONBOARDING/Computed_Properties_And_Sparse_Fields.md +398 -0
  24. package/docs/README.md +57 -2239
  25. package/{README_2_Computed_and_hidden_fields.md → docs/README_VARIOUS/GUIDE_2_Computed_and_hidden_fields.md} +22 -17
  26. package/docs/README_VARIOUS/GUIDE_IN_DOCS.md +2277 -0
  27. package/{README.ORIG.md → docs/README_VARIOUS/README.ORIG.md} +8 -8
  28. package/{README_8_Polymorphic_Relationships.md → docs/README_VARIOUS/README_8_Polymorphic_Relationships.md} +1 -1
  29. package/docs/README_VARIOUS/README_IN_DOCS.ms +2277 -0
  30. package/{RELATIONSHIPS_ADVANCED.md → docs/README_VARIOUS/RELATIONSHIPS_ADVANCED.md} +3 -3
  31. package/{correct-schema-format.md → docs/README_VARIOUS/correct-schema-format.md} +13 -13
  32. package/{pagination.md → docs/README_VARIOUS/pagination.md} +11 -8
  33. package/docs/assets/css/style.scss +78 -5
  34. package/docs/index.md +2 -12
  35. package/helper-reorganization-plan.md +85 -0
  36. package/helper-usage-analysis.md +94 -0
  37. package/package.json +5 -4
  38. package/plugins/core/bulk-operations-plugin.js +2 -2
  39. package/plugins/core/connectors/express-plugin.js +99 -33
  40. package/plugins/core/lib/querying/knex-cross-table-search.js +609 -0
  41. package/plugins/core/lib/querying/knex-json-api-transformers-querying.js +491 -0
  42. package/plugins/core/lib/querying/knex-pagination-helpers.js +544 -0
  43. package/plugins/core/lib/{knex-process-includes.js → querying/knex-process-includes.js} +65 -50
  44. package/plugins/core/lib/querying/knex-query-helpers-base.js +74 -0
  45. package/plugins/core/lib/{knex-query-helpers.js → querying/knex-query-helpers.js} +416 -180
  46. package/plugins/core/lib/{knex-relationship-includes.js → querying/knex-relationship-includes.js} +430 -114
  47. package/plugins/core/lib/querying/knex-window-queries.js +328 -0
  48. package/plugins/core/lib/querying-writing/compile-schemas.js +328 -0
  49. package/plugins/core/{utils → lib/querying-writing}/connectors-query-parser.js +62 -57
  50. package/plugins/core/lib/{database-capabilities.js → querying-writing/database-capabilities.js} +113 -7
  51. package/plugins/core/lib/querying-writing/database-value-normalizers.js +369 -0
  52. package/plugins/core/lib/querying-writing/default-data-helpers.js +72 -0
  53. package/plugins/core/{utils → lib/querying-writing}/field-utils.js +41 -31
  54. package/plugins/core/{utils → lib/querying-writing}/knex-constants.js +1 -0
  55. package/plugins/core/lib/{knex-field-helpers.js → querying-writing/knex-field-helpers.js} +190 -57
  56. package/plugins/core/lib/querying-writing/knex-json-api-transformers.js +162 -0
  57. package/plugins/core/lib/{payload-validators.js → querying-writing/payload-validators.js} +297 -217
  58. package/plugins/core/lib/querying-writing/schema-helpers.js +497 -0
  59. package/plugins/core/lib/querying-writing/scope-validations.js +431 -0
  60. package/plugins/core/lib/{simplified-helpers.js → querying-writing/simplified-helpers.js} +216 -176
  61. package/plugins/core/lib/writing/knex-json-api-transformers-writing.js +110 -0
  62. package/plugins/core/lib/writing/many-to-many-manipulations.js +300 -0
  63. package/plugins/core/lib/writing/relationship-processor.js +252 -0
  64. package/plugins/core/rest-api-knex-plugin.js +183 -62
  65. package/plugins/core/rest-api-plugin-hooks/compile-resource-schemas.js +7 -0
  66. package/plugins/core/rest-api-plugin-hooks/register-relationship-routes.js +72 -0
  67. package/plugins/core/rest-api-plugin-hooks/register-scope-routes.js +102 -0
  68. package/plugins/core/rest-api-plugin-hooks/turn-scope-init-into-vars.js +50 -0
  69. package/plugins/core/rest-api-plugin-hooks/validate-include-configurations.js +39 -0
  70. package/plugins/core/rest-api-plugin-methods/add-route.js +23 -0
  71. package/plugins/core/rest-api-plugin-methods/check-permissions.js +18 -0
  72. package/plugins/core/rest-api-plugin-methods/common.js +605 -0
  73. package/plugins/core/rest-api-plugin-methods/delete-relationship.js +115 -0
  74. package/plugins/core/rest-api-plugin-methods/delete.js +109 -0
  75. package/plugins/core/rest-api-plugin-methods/enrich-attributes.js +186 -0
  76. package/plugins/core/rest-api-plugin-methods/get-related.js +295 -0
  77. package/plugins/core/rest-api-plugin-methods/get-relationship.js +62 -0
  78. package/plugins/core/rest-api-plugin-methods/get.js +187 -0
  79. package/plugins/core/rest-api-plugin-methods/patch-relationship.js +56 -0
  80. package/plugins/core/rest-api-plugin-methods/patch.js +193 -0
  81. package/plugins/core/rest-api-plugin-methods/post-relationship.js +104 -0
  82. package/plugins/core/rest-api-plugin-methods/post.js +157 -0
  83. package/plugins/core/rest-api-plugin-methods/put.js +257 -0
  84. package/plugins/core/rest-api-plugin-methods/query.js +208 -0
  85. package/plugins/core/rest-api-plugin.js +108 -2498
  86. package/plugins/core/socketio-plugin.js +28 -5
  87. package/plugins/storage/local-storage.js +2 -2
  88. package/tests/computed-fields.test.js +483 -0
  89. package/tests/cors.test.js +4 -4
  90. package/tests/curl.test.js +5 -4
  91. package/tests/custom-idproperty-relationships.test.js +891 -0
  92. package/tests/field-getters.test.js +353 -0
  93. package/tests/field-setters.test.js +310 -0
  94. package/tests/fixtures/api-configs.js +800 -28
  95. package/tests/jwt-auth.test.js +0 -2
  96. package/tests/nested-includes.test.js +1 -1
  97. package/tests/pagination-cursor-multifield.test.js +514 -0
  98. package/tests/pagination-enhanced.test.js +12 -9
  99. package/tests/polymorphic-relationships.test.js +10 -8
  100. package/tests/polymorphic-simple.test.js +6 -4
  101. package/tests/positioning-advanced.test.js +12 -12
  102. package/tests/positioning-extended.test.js +25 -25
  103. package/tests/positioning-multifilter.test.js +24 -24
  104. package/tests/positioning.test.js +3 -3
  105. package/tests/query-limits.test.js +24 -8
  106. package/tests/relationship-endpoints.test.js +4 -5
  107. package/tests/relationships.test.js +0 -1
  108. package/tests/socketio.test.js +12 -1
  109. package/tests/virtual-fields.test.js +13 -13
  110. package/PLAN_FOR_WEBSOCKETS.md +0 -398
  111. package/README_1_Initial_Setup.md +0 -393
  112. package/README_X_Hooks.md +0 -647
  113. package/TODO_rest-api-plugin-extras.todo +0 -23
  114. package/connectors_ideas/express-plugin.js +0 -120
  115. package/connectors_ideas/http-plugin.js +0 -138
  116. package/docs/examples/express-plugin.js +0 -366
  117. package/example.js +0 -301
  118. package/plugins/core/HELPER_MODULES.md +0 -285
  119. package/plugins/core/lib/compile-schemas.js +0 -245
  120. package/plugins/core/lib/database-value-normalizers.js +0 -192
  121. package/plugins/core/lib/default-data-helpers.js +0 -100
  122. package/plugins/core/lib/knex-cross-table-search.js +0 -442
  123. package/plugins/core/lib/knex-json-api-helpers.js +0 -84
  124. package/plugins/core/lib/knex-json-api-transformers.js +0 -449
  125. package/plugins/core/lib/knex-pagination-helpers.js +0 -193
  126. package/plugins/core/lib/knex-window-queries.js +0 -142
  127. package/plugins/core/lib/many-to-many-manipulations.js +0 -343
  128. package/plugins/core/lib/relationship-processor.js +0 -278
  129. package/plugins/core/lib/schema-helpers.js +0 -343
  130. package/plugins/core/lib/scope-validations.js +0 -252
  131. package/plugins/core/rest-api-relationships-plugin.js +0 -700
  132. package/plugins/core/utils/knex-query-helpers-base.js +0 -56
  133. /package/{README_X_Permissions.md → docs/GUIDE/GUIDE_4_Authentication_And_Permissions.md} +0 -0
  134. /package/{README_X_Positioning.md → docs/GUIDE/GUIDE_6_Positioning.md} +0 -0
  135. /package/{README_X_Bulk_Operations.md → docs/GUIDE/GUIDE_X_Bulk_Operations.md} +0 -0
  136. /package/{README_X_Cors.md → docs/GUIDE/GUIDE_X_Cors.md} +0 -0
  137. /package/{README_X_Multihome.md → docs/GUIDE/GUIDE_X_Multihome.md} +0 -0
  138. /package/{README_X_Appendices.md → docs/GUIDE/GUIDE_Y_Appendices.md} +0 -0
  139. /package/{QUERY_DEPTH_PROTECTION.md → docs/README_VARIOUS/QUERY_DEPTH_PROTECTION.md} +0 -0
  140. /package/{RELATIONSHIPS.md → docs/README_VARIOUS/RELATIONSHIPS.md} +0 -0
  141. /package/{WEBSOCKET_IMPLEMENTATION_STATUS.md → docs/README_VARIOUS/WEBSOCKET_IMPLEMENTATION_STATUS.md} +0 -0
  142. /package/{helper-functions-chart.md → docs/README_VARIOUS/helper-functions-chart.md} +0 -0
  143. /package/docs/{implementation-hidden-computed-fields.md → README_VARIOUS/implementation-hidden-computed-fields.md} +0 -0
  144. /package/docs/{include-limits-migration-guide.md → README_VARIOUS/include-limits-migration-guide.md} +0 -0
  145. /package/{readmeSnippets → docs/README_VARIOUS}/test_put_behavior.js +0 -0
  146. /package/{readmeSnippets → docs/README_VARIOUS}/test_put_fields_without_defaults.js +0 -0
@@ -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
- };