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
@@ -0,0 +1,491 @@
1
+ import { getForeignKeyFields as getForeignKeyFieldsFromUtils } from '../querying-writing/field-utils.js';
2
+ import { RELATIONSHIPS_KEY, RELATIONSHIP_METADATA_KEY, ROW_NUMBER_KEY, COMPUTED_DEPENDENCIES_KEY, getSchemaStructure } from '../querying-writing/knex-constants.js';
3
+
4
+ /**
5
+ * Transforms a flat database record into JSON:API resource format
6
+ *
7
+ * @private
8
+ * @param {Object} scope - Scope containing schema and configuration
9
+ * @param {Object} record - Database record to transform
10
+ * @param {Object} deps - Dependencies including context with scopeName and polymorphicFields
11
+ * @returns {Object|null} JSON:API resource object or null if no record
12
+ *
13
+ * @example
14
+ * // Input: Database record with foreign keys and internal fields
15
+ * const record = {
16
+ * id: 1,
17
+ * title: 'Hello World',
18
+ * content: 'Article content',
19
+ * author_id: 2, // Foreign key
20
+ * category_id: 5, // Foreign key
21
+ * __$jsonrestapi_rn$__: 1 // Internal field
22
+ * };
23
+ *
24
+ * const jsonApiResource = toJsonApi(scope, record, deps);
25
+ *
26
+ * // Output: Clean JSON:API resource
27
+ * // {
28
+ * // type: 'articles',
29
+ * // id: '1',
30
+ * // attributes: {
31
+ * // title: 'Hello World',
32
+ * // content: 'Article content'
33
+ * // // Note: author_id, category_id, and internal fields are removed
34
+ * // }
35
+ * // }
36
+ *
37
+ * @example
38
+ * // Input: Polymorphic record
39
+ * const record = {
40
+ * id: 10,
41
+ * text: 'Great article!',
42
+ * commentable_type: 'articles', // Polymorphic type field
43
+ * commentable_id: 1 // Polymorphic id field
44
+ * };
45
+ * // deps.context.polymorphicFields = Set(['commentable_type', 'commentable_id'])
46
+ *
47
+ * const jsonApiResource = toJsonApi(scope, record, deps);
48
+ *
49
+ * // Output: Polymorphic fields filtered out
50
+ * // {
51
+ * // type: 'comments',
52
+ * // id: '10',
53
+ * // attributes: {
54
+ * // text: 'Great article!'
55
+ * // }
56
+ * // }
57
+ *
58
+ * @description
59
+ * Used by:
60
+ * - toJsonApiRecord calls this as the core transformation logic
61
+ * - buildJsonApiResponse uses this indirectly through toJsonApiRecord
62
+ *
63
+ * Purpose:
64
+ * - Separates database structure from API structure
65
+ * - Filters out foreign keys that become relationships in JSON:API
66
+ * - Removes internal fields used for query optimization
67
+ * - Ensures clean attribute objects without implementation details
68
+ *
69
+ * Data flow:
70
+ * - Called after database query returns flat records
71
+ * - Transforms each record individually
72
+ * - Output feeds into relationship building and response assembly
73
+ */
74
+ const toJsonApi = (scope, record, deps) => {
75
+ if (!record) return null;
76
+
77
+ const {
78
+ vars: {
79
+ schemaInfo: { schema }
80
+ }
81
+ } = scope;
82
+
83
+ const { context } = deps;
84
+ const scopeName = context.scopeName;
85
+ const idProperty = context.schemaInfo?.idProperty || 'id';
86
+ const polymorphicFields = context.polymorphicFields || new Set();
87
+
88
+ const { id, ...allAttributes } = record;
89
+
90
+ const foreignKeys = schema ? getForeignKeyFieldsFromUtils(schema) : new Set();
91
+
92
+ const internalFields = new Set([
93
+ RELATIONSHIPS_KEY,
94
+ RELATIONSHIP_METADATA_KEY,
95
+ ROW_NUMBER_KEY,
96
+ COMPUTED_DEPENDENCIES_KEY
97
+ ]);
98
+
99
+ const attributes = {};
100
+ Object.entries(allAttributes).forEach(([key, value]) => {
101
+ if (!foreignKeys.has(key) && !polymorphicFields.has(key) && !internalFields.has(key) && key !== idProperty) {
102
+ attributes[key] = value;
103
+ }
104
+ });
105
+
106
+ return {
107
+ type: scopeName,
108
+ id: String(id),
109
+ attributes
110
+ };
111
+ };
112
+
113
+ /**
114
+ * Converts a database record to JSON:API format with proper field filtering
115
+ *
116
+ * @param {Object} scope - Scope containing schema and relationship definitions
117
+ * @param {Object} record - Raw database record
118
+ * @param {string} scopeName - Resource type name
119
+ * @returns {Object} JSON:API formatted resource
120
+ *
121
+ * @example
122
+ * // Input: Database record with various field types
123
+ * const record = {
124
+ * id: 1,
125
+ * title: 'My Article',
126
+ * author_id: 2, // belongsTo relationship
127
+ * category_id: 3, // belongsTo relationship
128
+ * views: 150,
129
+ * published: true
130
+ * };
131
+ *
132
+ * const jsonApi = toJsonApiRecord(scope, record, 'articles');
133
+ *
134
+ * // Output: Foreign keys filtered out
135
+ * // {
136
+ * // type: 'articles',
137
+ * // id: '1',
138
+ * // attributes: {
139
+ * // title: 'My Article',
140
+ * // views: 150,
141
+ * // published: true
142
+ * // }
143
+ * // }
144
+ *
145
+ * @example
146
+ * // Input: Record with polymorphic relationship
147
+ * const record = {
148
+ * id: 5,
149
+ * body: 'Nice post!',
150
+ * author_id: 10,
151
+ * commentable_type: 'posts', // Polymorphic
152
+ * commentable_id: 3 // Polymorphic
153
+ * };
154
+ * // Schema has polymorphic relationship defined
155
+ *
156
+ * const jsonApi = toJsonApiRecord(scope, record, 'comments');
157
+ *
158
+ * // Output: Both foreign keys and polymorphic fields filtered
159
+ * // {
160
+ * // type: 'comments',
161
+ * // id: '5',
162
+ * // attributes: {
163
+ * // body: 'Nice post!'
164
+ * // }
165
+ * // }
166
+ *
167
+ * @description
168
+ * Used by:
169
+ * - rest-api-knex-plugin's dataGet method for single resource responses
170
+ * - rest-api-knex-plugin's dataQuery method for collection responses
171
+ * - processIncludes when transforming included resources
172
+ * - buildJsonApiResponse as the primary transformation function
173
+ *
174
+ * Purpose:
175
+ * - Provides consistent JSON:API transformation across all query operations
176
+ * - Automatically identifies and filters foreign keys based on schema
177
+ * - Handles polymorphic relationship fields (type/id pairs)
178
+ * - Ensures IDs are always strings as required by JSON:API spec
179
+ *
180
+ * Data flow:
181
+ * 1. Database query returns flat records with all fields
182
+ * 2. toJsonApiRecord identifies foreign keys from schema
183
+ * 3. Filters out foreign keys and internal fields from attributes
184
+ * 4. Returns clean JSON:API resource ready for relationship processing
185
+ * 5. buildJsonApiResponse adds relationships and links to complete the response
186
+ */
187
+ export const toJsonApiRecord = (scope, record, scopeName) => {
188
+ const {
189
+ vars: {
190
+ schemaInfo: { schema, schemaRelationships: relationships, idProperty }
191
+ }
192
+ } = scope;
193
+
194
+ const polymorphicFields = new Set();
195
+ try {
196
+ Object.entries(relationships || {}).forEach(([relName, relDef]) => {
197
+ if (relDef.belongsToPolymorphic) {
198
+ const typeFieldName = relDef.belongsToPolymorphic.typeField || `${relName}_type`;
199
+ const idFieldName = relDef.belongsToPolymorphic.idField || `${relName}_id`;
200
+ polymorphicFields.add(typeFieldName);
201
+ polymorphicFields.add(idFieldName);
202
+ }
203
+ });
204
+ } catch (e) {
205
+ }
206
+
207
+ const deps = {
208
+ context: {
209
+ scopeName,
210
+ schemaInfo: { idProperty },
211
+ polymorphicFields
212
+ }
213
+ };
214
+
215
+ return toJsonApi(scope, record, deps);
216
+ };
217
+
218
+ /**
219
+ * Builds complete JSON:API response with data, relationships, links, and optional includes
220
+ *
221
+ * @async
222
+ * @param {Object} scope - Scope containing schema and configuration
223
+ * @param {Array<Object>} records - Primary records to include in response
224
+ * @param {Array<Object>} included - Resources to include in 'included' array
225
+ * @param {boolean} isSingle - Whether this is a single resource response
226
+ * @param {string} scopeName - Resource type name
227
+ * @param {Object} context - Request context with pagination metadata
228
+ * @returns {Promise<Object>} Complete JSON:API response document
229
+ *
230
+ * @example
231
+ * // Input: Single article with author include
232
+ * const records = [{
233
+ * id: 1,
234
+ * title: 'Article Title',
235
+ * content: 'Article content',
236
+ * author_id: 2,
237
+ * category_id: 3
238
+ * }];
239
+ *
240
+ * const included = [{
241
+ * type: 'authors',
242
+ * id: '2',
243
+ * attributes: { name: 'John Doe', email: 'john@example.com' }
244
+ * }];
245
+ *
246
+ * const response = await buildJsonApiResponse(scope, records, included, true, 'articles', context);
247
+ *
248
+ * // Output: Complete JSON:API document
249
+ * // {
250
+ * // data: {
251
+ * // type: 'articles',
252
+ * // id: '1',
253
+ * // attributes: {
254
+ * // title: 'Article Title',
255
+ * // content: 'Article content'
256
+ * // },
257
+ * // relationships: {
258
+ * // author: {
259
+ * // data: { type: 'authors', id: '2' },
260
+ * // links: {
261
+ * // self: '/articles/1/relationships/author',
262
+ * // related: '/articles/1/author'
263
+ * // }
264
+ * // },
265
+ * // category: {
266
+ * // data: { type: 'categories', id: '3' },
267
+ * // links: {
268
+ * // self: '/articles/1/relationships/category',
269
+ * // related: '/articles/1/category'
270
+ * // }
271
+ * // }
272
+ * // },
273
+ * // links: {
274
+ * // self: '/articles/1'
275
+ * // }
276
+ * // },
277
+ * // included: [{
278
+ * // type: 'authors',
279
+ * // id: '2',
280
+ * // attributes: { name: 'John Doe', email: 'john@example.com' },
281
+ * // links: { self: '/authors/2' }
282
+ * // }],
283
+ * // links: {
284
+ * // self: '/articles/1'
285
+ * // }
286
+ * // }
287
+ *
288
+ * @example
289
+ * // Input: Collection with pagination
290
+ * const records = [
291
+ * { id: 1, title: 'Article 1', author_id: 10 },
292
+ * { id: 2, title: 'Article 2', author_id: 10 },
293
+ * { id: 3, title: 'Article 3', author_id: 11 }
294
+ * ];
295
+ *
296
+ * context.returnMeta = {
297
+ * paginationMeta: { page: 2, pageSize: 3, pageCount: 10, total: 30 },
298
+ * paginationLinks: {
299
+ * self: '/articles?page[number]=2&page[size]=3',
300
+ * first: '/articles?page[number]=1&page[size]=3',
301
+ * prev: '/articles?page[number]=1&page[size]=3',
302
+ * next: '/articles?page[number]=3&page[size]=3',
303
+ * last: '/articles?page[number]=10&page[size]=3'
304
+ * }
305
+ * };
306
+ *
307
+ * const response = await buildJsonApiResponse(scope, records, [], false, 'articles', context);
308
+ *
309
+ * // Output: Collection with pagination metadata
310
+ * // {
311
+ * // data: [
312
+ * // { type: 'articles', id: '1', attributes: { title: 'Article 1' }, ... },
313
+ * // { type: 'articles', id: '2', attributes: { title: 'Article 2' }, ... },
314
+ * // { type: 'articles', id: '3', attributes: { title: 'Article 3' }, ... }
315
+ * // ],
316
+ * // meta: {
317
+ * // pagination: { page: 2, pageSize: 3, pageCount: 10, total: 30 }
318
+ * // },
319
+ * // links: {
320
+ * // self: '/articles?page[number]=2&page[size]=3',
321
+ * // first: '/articles?page[number]=1&page[size]=3',
322
+ * // prev: '/articles?page[number]=1&page[size]=3',
323
+ * // next: '/articles?page[number]=3&page[size]=3',
324
+ * // last: '/articles?page[number]=10&page[size]=3'
325
+ * // }
326
+ * // }
327
+ *
328
+ * @description
329
+ * Used by:
330
+ * - rest-api-knex-plugin's dataGet method for single resource responses
331
+ * - rest-api-knex-plugin's dataQuery method for collection responses
332
+ * - Called at the end of the query pipeline to assemble the final response
333
+ *
334
+ * Purpose:
335
+ * - Assembles all parts of a JSON:API response in the correct structure
336
+ * - Adds relationship objects with proper links for each foreign key
337
+ * - Handles both regular belongsTo and polymorphic relationships
338
+ * - Adds self links to all resources as required by JSON:API
339
+ * - Includes pagination metadata and links when applicable
340
+ *
341
+ * Data flow:
342
+ * 1. Query operations fetch primary records and optional includes
343
+ * 2. Records are transformed to JSON:API format via toJsonApiRecord
344
+ * 3. buildJsonApiResponse adds relationship objects for all foreign keys
345
+ * 4. Adds proper links (self, related) for navigating the API
346
+ * 5. Assembles included resources with their own links
347
+ * 6. Adds pagination metadata and links if provided
348
+ * 7. Returns complete JSON:API document ready for HTTP response
349
+ */
350
+ export const buildJsonApiResponse = async (scope, records, included = [], isSingle = false, scopeName, context) => {
351
+ const {
352
+ vars: {
353
+ schemaInfo: { schema, schemaRelationships: relationships, idProperty }
354
+ }
355
+ } = scope;
356
+
357
+ const idField = idProperty || 'id';
358
+
359
+ const schemaStructure = getSchemaStructure(schema);
360
+
361
+ const processedRecords = records.map(record => {
362
+ const { [RELATIONSHIPS_KEY]: _relationships, ...cleanRecord } = record;
363
+ const jsonApiRecord = toJsonApiRecord(scope, cleanRecord, scopeName);
364
+
365
+ if (_relationships) {
366
+ jsonApiRecord.relationships = _relationships;
367
+ }
368
+
369
+ for (const [fieldName, fieldDef] of Object.entries(schemaStructure)) {
370
+ if (fieldDef.belongsTo && fieldDef.as && fieldName in cleanRecord) {
371
+ jsonApiRecord.relationships = jsonApiRecord.relationships || {};
372
+ if (!jsonApiRecord.relationships[fieldDef.as]) {
373
+ if (cleanRecord[fieldName] !== null && cleanRecord[fieldName] !== undefined) {
374
+ const relationshipObject = {
375
+ data: {
376
+ type: fieldDef.belongsTo,
377
+ id: String(cleanRecord[fieldName])
378
+ }
379
+ };
380
+
381
+ const urlPrefix = scope.vars.publicBaseUrl || scope.vars.transport?.mountPath || '';
382
+ relationshipObject.links = {
383
+ self: `${urlPrefix}/${scopeName}/${record[idField]}/relationships/${fieldDef.as}`,
384
+ related: `${urlPrefix}/${scopeName}/${record[idField]}/${fieldDef.as}`
385
+ };
386
+
387
+ jsonApiRecord.relationships[fieldDef.as] = relationshipObject;
388
+ } else {
389
+ const relationshipObject = {
390
+ data: null
391
+ };
392
+
393
+ const urlPrefix = scope.vars.publicBaseUrl || scope.vars.transport?.mountPath || '';
394
+ relationshipObject.links = {
395
+ self: `${urlPrefix}/${scopeName}/${record[idField]}/relationships/${fieldDef.as}`,
396
+ related: `${urlPrefix}/${scopeName}/${record[idField]}/${fieldDef.as}`
397
+ };
398
+
399
+ jsonApiRecord.relationships[fieldDef.as] = relationshipObject;
400
+ }
401
+ }
402
+ }
403
+ }
404
+
405
+ Object.entries(relationships || {}).forEach(([relName, relDef]) => {
406
+ if (relDef.belongsToPolymorphic) {
407
+ const typeValue = cleanRecord[relDef.belongsToPolymorphic.typeField];
408
+ const idValue = cleanRecord[relDef.belongsToPolymorphic.idField];
409
+
410
+ if (typeValue && idValue) {
411
+ jsonApiRecord.relationships = jsonApiRecord.relationships || {};
412
+ const relationshipObject = {
413
+ data: {
414
+ type: typeValue,
415
+ id: String(idValue)
416
+ }
417
+ };
418
+
419
+ const urlPrefix = scope.vars.publicBaseUrl || scope.vars.transport?.mountPath || '';
420
+ relationshipObject.links = {
421
+ self: `${urlPrefix}/${scopeName}/${record[idField]}/relationships/${relName}`,
422
+ related: `${urlPrefix}/${scopeName}/${record[idField]}/${relName}`
423
+ };
424
+
425
+ jsonApiRecord.relationships[relName] = relationshipObject;
426
+ } else if (typeValue === null || idValue === null) {
427
+ jsonApiRecord.relationships = jsonApiRecord.relationships || {};
428
+ const relationshipObject = {
429
+ data: null
430
+ };
431
+
432
+ const urlPrefix = scope.vars.publicBaseUrl || scope.vars.transport?.mountPath || '';
433
+ relationshipObject.links = {
434
+ self: `${urlPrefix}/${scopeName}/${record[idField]}/relationships/${relName}`,
435
+ related: `${urlPrefix}/${scopeName}/${record[idField]}/${relName}`
436
+ };
437
+
438
+ jsonApiRecord.relationships[relName] = relationshipObject;
439
+ }
440
+ }
441
+ });
442
+
443
+ return jsonApiRecord;
444
+ });
445
+
446
+ const urlPrefix = scope.vars.publicBaseUrl || scope.vars.transport?.mountPath || '';
447
+ const normalizedData = isSingle ? processedRecords[0] : processedRecords;
448
+
449
+ if (normalizedData) {
450
+ if (Array.isArray(normalizedData)) {
451
+ normalizedData.forEach(item => {
452
+ if (!item.links) item.links = {};
453
+ item.links.self = `${urlPrefix}/${scopeName}/${item.id}`;
454
+ });
455
+ } else {
456
+ if (!normalizedData.links) normalizedData.links = {};
457
+ normalizedData.links.self = `${urlPrefix}/${scopeName}/${normalizedData.id}`;
458
+ }
459
+ }
460
+
461
+ const response = {
462
+ data: normalizedData
463
+ };
464
+
465
+ if (included.length > 0) {
466
+ included.forEach(item => {
467
+ if (!item.links) item.links = {};
468
+ item.links.self = `${urlPrefix}/${item.type}/${item.id}`;
469
+ });
470
+
471
+ response.included = included;
472
+ }
473
+
474
+ if (context?.returnMeta?.paginationMeta) {
475
+ response.meta = {
476
+ pagination: context.returnMeta.paginationMeta
477
+ };
478
+ }
479
+
480
+ if (context?.returnMeta?.paginationLinks) {
481
+ response.links = context.returnMeta.paginationLinks;
482
+ } else {
483
+ response.links = {
484
+ self: isSingle
485
+ ? `${urlPrefix}/${scopeName}/${normalizedData.id}`
486
+ : `${urlPrefix}/${scopeName}${context?.returnMeta?.queryString || ''}`
487
+ };
488
+ }
489
+
490
+ return response;
491
+ };