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.
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 +90 -32
  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
@@ -0,0 +1,257 @@
1
+ import { RestApiResourceError } from '../../../lib/rest-api-errors.js';
2
+ import { validatePutPayload } from '../lib/querying-writing/payload-validators.js';
3
+ import { processRelationships } from '../lib/writing/relationship-processor.js';
4
+ import { updateManyToManyRelationship, createPivotRecords } from '../lib/writing/many-to-many-manipulations.js';
5
+ import { ERROR_SUBTYPES } from '../lib/querying-writing/knex-constants.js';
6
+ import {
7
+ setupCommonRequest,
8
+ validateResourceAttributesBeforeWrite,
9
+ validateRelationshipAccess,
10
+ applyFieldSetters,
11
+ validatePivotResource,
12
+ handleRecordReturnAfterWrite,
13
+ handleWriteMethodError
14
+ } from './common.js';
15
+
16
+ /**
17
+ * PUT
18
+ * Updates an existing top-level resource by completely replacing it.
19
+ * This method supports updating both attributes and relationships (1:1 and n:n).
20
+ * PUT is a complete replacement - any relationships not provided will be removed/nulled.
21
+ * This method does NOT support creating new related resources via an `included` array.
22
+ */
23
+ export default async function putMethod({
24
+ params,
25
+ context,
26
+ vars,
27
+ helpers,
28
+ scope,
29
+ scopes,
30
+ runHooks,
31
+ apiOptions,
32
+ pluginOptions,
33
+ scopeOptions,
34
+ scopeName,
35
+ api,
36
+ log
37
+ }) {
38
+ context.method = 'put';
39
+
40
+ try {
41
+ const { schema, schemaStructure, schemaRelationships } = await setupCommonRequest({
42
+ params,
43
+ context,
44
+ vars,
45
+ scopes,
46
+ scopeOptions,
47
+ scopeName,
48
+ api,
49
+ helpers
50
+ });
51
+ context.id = context.inputRecord.data.id;
52
+
53
+ // Run early hooks for pre-processing (e.g., file handling)
54
+ await runHooks('beforeProcessing');
55
+ await runHooks('beforeProcessingPut');
56
+
57
+ // Validate PUT payload to ensure it's a complete resource replacement operation.
58
+ // PUT requires the full resource representation including ID (unlike POST which generates ID).
59
+ // It validates that data.id matches the URL parameter, prevents 'included' array (which is
60
+ // read-only), and ensures the payload represents a complete replacement. Any fields not
61
+ // provided will be removed or reset to defaults - this is the key difference from PATCH.
62
+ // Example: PUT to /articles/123 must have data.id: '123' and all required fields.
63
+ validatePutPayload(context.inputRecord, scopes);
64
+
65
+ // Validate that user has read access to all related resources
66
+ // This ensures users can only create relationships to resources they can access
67
+ await validateRelationshipAccess(context, context.inputRecord, helpers, runHooks, api);
68
+
69
+ // Extract foreign keys from JSON:API relationships and prepare many-to-many operations
70
+ // Example: relationships.author -> author_id: '123' for storage
71
+ // Example: relationships.tags -> array of pivot records to create later
72
+ const { belongsToUpdates, manyToManyRelationships } = processRelationships(
73
+ scope,
74
+ { context }
75
+ );
76
+
77
+ await validateResourceAttributesBeforeWrite({
78
+ context,
79
+ schema,
80
+ belongsToUpdates,
81
+ runHooks
82
+ });
83
+
84
+ // Check existence first
85
+ context.exists = await helpers.dataExists({
86
+ scopeName,
87
+ context
88
+ });
89
+
90
+ context.isCreate = !context.exists;
91
+ context.isUpdate = context.exists;
92
+
93
+ // Fetch minimal record for authorization checks (only for updates)
94
+ if (context.isUpdate) {
95
+ const minimalRecord = await helpers.dataGetMinimal({
96
+ scopeName,
97
+ context,
98
+ runHooks
99
+ });
100
+
101
+ if (!minimalRecord) {
102
+ throw new RestApiResourceError(
103
+ `Resource not found: ${scopeName}/${context.id}`,
104
+ ERROR_SUBTYPES.NOT_FOUND
105
+ );
106
+ }
107
+
108
+ context.minimalRecord = minimalRecord;
109
+ }
110
+
111
+ // For PUT, we also need to handle relationships that are NOT provided
112
+ // (they should be set to null/empty as PUT is a complete replacement)
113
+ const allRelationships = {};
114
+
115
+ // Collect all defined relationships for this resource
116
+ for (const [relName, relDef] of Object.entries(schemaRelationships || {})) {
117
+ if (relDef.manyToMany) {
118
+ allRelationships[relName] = { type: 'manyToMany', relDef: relDef.manyToMany };
119
+ }
120
+ // Also recognize hasMany with through as many-to-many
121
+ else if (relDef.hasMany && relDef.through) {
122
+ allRelationships[relName] = {
123
+ type: 'manyToMany',
124
+ relDef: {
125
+ through: relDef.through,
126
+ foreignKey: relDef.foreignKey,
127
+ otherKey: relDef.otherKey
128
+ }
129
+ };
130
+ }
131
+ }
132
+
133
+ // Also check schema fields for belongsTo relationships
134
+ for (const [fieldName, fieldDef] of Object.entries(schemaStructure)) {
135
+ if (fieldDef.as && fieldDef.belongsTo) {
136
+ allRelationships[fieldDef.as] = {
137
+ type: 'belongsTo',
138
+ fieldName,
139
+ fieldDef
140
+ };
141
+ }
142
+ }
143
+
144
+ // Process missing relationships (PUT should null them out only if relationships object exists)
145
+ const hasRelationshipsObject = context.inputRecord.data.relationships !== undefined;
146
+ const providedRelationships = new Set(Object.keys(context.inputRecord.data.relationships || {}));
147
+
148
+ // Only null out missing relationships if a relationships object was provided
149
+ if (hasRelationshipsObject) {
150
+ for (const [relName, relInfo] of Object.entries(allRelationships)) {
151
+ if (!providedRelationships.has(relName)) {
152
+ if (relInfo.type === 'belongsTo') {
153
+ belongsToUpdates[relInfo.fieldName] = null;
154
+ } else if (relInfo.type === 'manyToMany') {
155
+ // Add to manyToManyRelationships with empty array
156
+ manyToManyRelationships.push({
157
+ relName,
158
+ relDef: relInfo.relDef,
159
+ relData: [] // Empty array means delete all
160
+ });
161
+ }
162
+ }
163
+ }
164
+ }
165
+
166
+ // Merge belongsTo updates with attributes
167
+ if (Object.keys(belongsToUpdates).length > 0) {
168
+ context.inputRecord.data.attributes = {
169
+ ...context.inputRecord.data.attributes,
170
+ ...belongsToUpdates
171
+ };
172
+ }
173
+
174
+ // Centralised checkPermissions function
175
+ await scope.checkPermissions({
176
+ method: 'put',
177
+ auth: context.auth,
178
+ id: context.id,
179
+ minimalRecord: context.minimalRecord,
180
+ isUpdate: context.isUpdate,
181
+ transaction: context.transaction
182
+ });
183
+
184
+ await runHooks('beforeDataCall');
185
+ await runHooks('beforeDataCallPut');
186
+
187
+ // Apply field setters after validation and before storage
188
+ if (context.inputRecord?.data?.attributes) {
189
+ context.inputRecord.data.attributes = await applyFieldSetters(
190
+ context.inputRecord.data.attributes,
191
+ context.schemaInfo,
192
+ context,
193
+ api,
194
+ helpers
195
+ );
196
+ }
197
+
198
+ // Pass the operation type to the helper
199
+ await helpers.dataPut({
200
+ scopeName,
201
+ context
202
+ });
203
+ await runHooks('afterDataCallPut');
204
+ await runHooks('afterDataCall');
205
+
206
+ // Process many-to-many relationships after main record update/creation
207
+ for (const { relName, relDef, relData } of manyToManyRelationships) {
208
+ // Validate pivot resource exists
209
+ await validatePivotResource(scopes, relDef, relName);
210
+
211
+ // Use smart sync for updates (like industry standard ORMs)
212
+ if (context.isUpdate) {
213
+ // Update many-to-many relationships using intelligent synchronization
214
+ // This preserves existing pivot data while efficiently updating relationships
215
+ await updateManyToManyRelationship(null, {
216
+ api,
217
+ context: {
218
+ resourceId: context.id,
219
+ relDef,
220
+ relData,
221
+ transaction: context.transaction
222
+ }
223
+ });
224
+ } else {
225
+ // For new records, just create the pivot records
226
+ if (relData.length > 0) {
227
+ await createPivotRecords(api, context.id, relDef, relData, context.transaction);
228
+ }
229
+ }
230
+ }
231
+
232
+ const ret = await handleRecordReturnAfterWrite({
233
+ context,
234
+ scopeName,
235
+ api,
236
+ scopes,
237
+ schemaStructure,
238
+ schemaRelationships,
239
+ scopeOptions,
240
+ vars,
241
+ runHooks,
242
+ helpers,
243
+ log
244
+ });
245
+
246
+ // Commit transaction if we created it
247
+ if (context.shouldCommit) {
248
+ await context.transaction.commit();
249
+ await runHooks('afterCommit');
250
+ }
251
+
252
+ return ret;
253
+
254
+ } catch (error) {
255
+ await handleWriteMethodError(error, context, 'PUT', scopeName, log, runHooks);
256
+ }
257
+ }
@@ -0,0 +1,208 @@
1
+ import { RestApiValidationError } from '../../../lib/rest-api-errors.js';
2
+ import { validateQueryPayload } from '../lib/querying-writing/payload-validators.js';
3
+ import { normalizeRecordAttributes } from '../lib/querying-writing/database-value-normalizers.js';
4
+ import { getRequestedComputedFields } from '../lib/querying-writing/knex-field-helpers.js';
5
+ import { transformJsonApiToSimplified } from '../lib/querying-writing/simplified-helpers.js';
6
+ import { cascadeConfig } from './common.js';
7
+
8
+ /**
9
+ * QUERY
10
+ * Retrieves a collection of resources (e.g., a list of articles) based on provided criteria.
11
+ * This function sends a GET request to /api/{resourceType}.
12
+ *
13
+ * @param {string} resourceType - The type of resource collection to fetch (e.g., "articles").
14
+ * @param {object} [queryParams={}] - Optional. An object to customize the query for the collection.
15
+ * @param {string[]} [queryParams.include=[]] - An optional array of relationship paths to sideload for each resource in the collection. These paths will be converted to a comma-separated string for the URL (e.g., `['author', 'comments.user']` becomes `author,comments.user`). Supports deep relationships (e.g., "publisher.country").
16
+ * @param {object} [queryParams.fields] - An object to request only specific fields (sparse fieldsets) for each resource in the collection and its included relationships. Keys are resource types, values are comma-separated field names.
17
+ * @param {object} [queryParams.filters] - An object to filter the collection. Keys are filter parameters (specific to your API's implementation, e.g., 'status', 'title'), values are the filter criteria.
18
+ * @param {string[]} [queryParams.sort=[]] - An optional array of fields to sort the collection by. Each string represents a field; prefix with '-' for descending order (e.g., `['title', '-published-date']` becomes `title,-published-date`).
19
+ * @param {object} [queryParams.page] - An object for pagination. Typically includes `number` (page number) and `size` (items per page). E.g., `{ number: 1, size: 10 }`.
20
+ * @returns {Promise<object>} A Promise that resolves to the JSON:API response document containing the resource collection.
21
+ */
22
+ export default async function queryMethod({
23
+ params,
24
+ context,
25
+ vars,
26
+ helpers,
27
+ scope,
28
+ scopes,
29
+ runHooks,
30
+ apiOptions,
31
+ pluginOptions,
32
+ scopeOptions,
33
+ scopeName,
34
+ log,
35
+ api
36
+ }) {
37
+ context.method = 'query';
38
+
39
+ // Determine which simplified setting to use based on transport
40
+ const isTransport = params.isTransport === true;
41
+
42
+ // Use vars which automatically cascade from scope to global
43
+ const defaultSimplified = isTransport ? vars.simplifiedTransport : vars.simplifiedApi;
44
+
45
+ // Get simplified setting - from params only (per-call override) or use default
46
+ context.simplified = params.simplified !== undefined ? params.simplified : defaultSimplified;
47
+
48
+ // Assign common context properties
49
+ context.schemaInfo = scopes[scopeName].vars.schemaInfo; // This is the object variable created by compileSchemas
50
+ context.queryParams = params.queryParams || {};
51
+
52
+ // These only make sense as parameter per query
53
+ context.queryParams.fields = cascadeConfig('fields', [params.queryParams], {});
54
+ context.queryParams.include = cascadeConfig('include', [params.queryParams], []);
55
+ context.queryParams.sort = cascadeConfig('sort', [params.queryParams], []);
56
+ context.queryParams.page = cascadeConfig('page', [params.queryParams], {});
57
+
58
+ context.transaction = params.transaction;
59
+ context.db = context.transaction || api.knex.instance;
60
+
61
+ context.scopeName = scopeName;
62
+
63
+ // These are just shortcuts used in this function and will be returned
64
+ const searchSchema = context.schemaInfo.searchSchema;
65
+ const schemaStructure = context.schemaInfo.schema.structure;
66
+ const schemaRelationships = context.schemaInfo.schemaRelationships;
67
+
68
+ // Sortable fields and sort (mab)
69
+ context.sortableFields = vars.sortableFields;
70
+ // Apply default sort if no sort specified
71
+ if (context.queryParams.sort.length === 0 && vars.defaultSort) {
72
+ context.queryParams.sort = Array.isArray(vars.defaultSort) ? vars.defaultSort : [vars.defaultSort];
73
+ }
74
+
75
+ // Validate query parameters to ensure they follow JSON:API specification and security rules.
76
+ // This checks that filters are valid field names, sort fields exist in sortableFields array
77
+ // (preventing SQL injection), pagination uses valid page[size]/page[number] format, and include
78
+ // paths reference real relationships. Example: sort: ['-createdAt', 'title'] is checked against
79
+ // sortableFields to ensure users can't sort by sensitive fields like 'password_hash'.
80
+ validateQueryPayload({ queryParams: context.queryParams }, context.sortableFields, vars.includeDepthLimit);
81
+
82
+ // Validate search/filter parameters against searchSchema
83
+ if (context.queryParams.filters && Object.keys(context.queryParams.filters).length > 0) {
84
+ // Only allow filtering if searchSchema is defined
85
+ if (!searchSchema) {
86
+ throw new RestApiValidationError(
87
+ `Filtering is not enabled for resource '${scopeName}'. To enable filtering, add 'search: true' to schema fields or define a searchSchema.`,
88
+ {
89
+ fields: Object.keys(context.queryParams.filters).map(field => `filters.${field}`),
90
+ violations: [{
91
+ field: 'filters',
92
+ rule: 'filtering_not_enabled',
93
+ message: 'Resource does not have searchable fields defined'
94
+ }]
95
+ }
96
+ );
97
+ }
98
+
99
+ // Validate the filter parameters searchSchema
100
+ const { validatedObject, errors } = await searchSchema.validate(context.queryParams.filters, {
101
+ onlyObjectValues: true // Partial validation for filters
102
+ });
103
+
104
+ // If there are validation errors, throw an error
105
+ if (Object.keys(errors).length > 0) {
106
+ const violations = Object.entries(errors).map(([field, error]) => ({
107
+ field: `filters.${field}`,
108
+ rule: error.code || 'invalid_value',
109
+ message: error.message
110
+ }));
111
+
112
+ throw new RestApiValidationError(
113
+ 'Invalid filter parameters',
114
+ {
115
+ fields: Object.keys(errors).map(field => `filters.${field}`),
116
+ violations
117
+ }
118
+ );
119
+ }
120
+
121
+ // Replace filter with validated/transformed values
122
+ context.queryParams.filters = validatedObject;
123
+ }
124
+
125
+ // Centralised checkPermissions function
126
+ await scope.checkPermissions({
127
+ method: 'query',
128
+ auth: context.auth,
129
+ transaction: context.transaction
130
+ });
131
+
132
+ await runHooks('beforeData');
133
+ await runHooks('beforeDataQuery');
134
+ context.record = await helpers.dataQuery({
135
+ scopeName,
136
+ context,
137
+ runHooks
138
+ });
139
+
140
+ // Normalize database values (e.g., convert 1/0 to true/false for booleans)
141
+ context.record = normalizeRecordAttributes(context.record, scopes);
142
+
143
+ // Make a backup
144
+ try {
145
+ context.originalRecord = structuredClone(context.record);
146
+ } catch (e) {
147
+ log.error('Failed to clone record:', {
148
+ error: e.message,
149
+ recordKeys: Object.keys(context.record || {}),
150
+ hasHttpRequest: !!context.raw?.req
151
+ });
152
+ throw e;
153
+ }
154
+
155
+ // This will enhance record, which is the WHOLE JSON:API record
156
+ await runHooks('enrichRecord');
157
+
158
+ // Get computed field information for main resource
159
+ const computedFields = scope.vars.schemaInfo?.computed || {};
160
+ const requestedFields = context.queryParams.fields?.[scopeName];
161
+ const requestedComputedFields = getRequestedComputedFields(scopeName, requestedFields, computedFields);
162
+
163
+ // Run enrichAttributes for every single set of attribute, calling it from the right scope
164
+ for (const entry of context.record.data) {
165
+ entry.attributes = await scope.enrichAttributes({
166
+ attributes: entry.attributes,
167
+ parentContext: context,
168
+ requestedComputedFields: requestedComputedFields,
169
+ isMainResource: true,
170
+ computedDependencies: context.computedDependencies
171
+ });
172
+ }
173
+ for (const entry of (context.record.included || [])) {
174
+ const entryScope = scopes[entry.type];
175
+ const entryComputed = entryScope.vars.schemaInfo?.computed || {};
176
+ const entryRequestedFields = context.queryParams.fields?.[entry.type];
177
+ const entryRequestedComputed = getRequestedComputedFields(
178
+ entry.type,
179
+ entryRequestedFields,
180
+ entryComputed
181
+ );
182
+
183
+ entry.attributes = await entryScope.enrichAttributes({
184
+ attributes: entry.attributes,
185
+ parentContext: context,
186
+ requestedComputedFields: entryRequestedComputed,
187
+ isMainResource: false,
188
+ computedDependencies: entry.__$jsonrestapi_computed_deps$__
189
+ });
190
+ }
191
+
192
+ // The called hooks should NOT change context.record
193
+ await runHooks('finish');
194
+ await runHooks('finishQuery');
195
+
196
+ // Transform output if in simplified mode
197
+ if (context.simplified) {
198
+ // Convert JSON:API response back to simplified format
199
+ // Example: {data: {type: 'posts', id: '1', attributes: {title: 'My Post'}, relationships: {author: {data: {type: 'users', id: '123'}}}}}
200
+ // becomes: {id: '1', title: 'My Post', author_id: '123'} - flattens structure and restores foreign keys
201
+ return transformJsonApiToSimplified(
202
+ { record: context.record },
203
+ { context: { schemaStructure, schemaRelationships, scopes } }
204
+ );
205
+ }
206
+
207
+ return context.record;
208
+ }