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,449 +0,0 @@
1
- /**
2
- * @module knex-json-api-transformers
3
- * @description JSON:API transformation functions for REST API Knex Plugin
4
- *
5
- * This module handles the transformation between flat database records and
6
- * hierarchical JSON:API format. It manages the conversion of foreign keys
7
- * to relationships and builds proper JSON:API response structures.
8
- */
9
-
10
- import { getForeignKeyFields } from './knex-field-helpers.js';
11
- import { toJsonApi } from './knex-json-api-helpers.js';
12
- import { RELATIONSHIPS_KEY, getSchemaStructure } from '../utils/knex-constants.js';
13
- import {
14
- generatePaginationLinks,
15
- generateCursorPaginationLinks
16
- } from './knex-pagination-helpers.js';
17
-
18
- /**
19
- * Converts a database record to JSON:API format.
20
- *
21
- * This function transforms a flat database record into the hierarchical JSON:API
22
- * structure, separating regular attributes from foreign keys. Foreign key fields
23
- * are filtered out of attributes because they're represented as relationships
24
- * in JSON:API format. This maintains the clean separation between data and
25
- * relationships that JSON:API requires.
26
- *
27
- * @param {Object} scope - The scope object containing schema and relationship info
28
- * @param {Object} record - The database record to transform
29
- * @param {string} scopeName - The name of the scope/resource type
30
- * @returns {Object} JSON:API formatted resource object
31
- *
32
- * @example <caption>Basic transformation with foreign keys</caption>
33
- * const dbRecord = {
34
- * id: 1,
35
- * title: 'Hello World',
36
- * content: 'Article content',
37
- * author_id: 2,
38
- * category_id: 5
39
- * };
40
- * const jsonApiRecord = toJsonApiRecord('articles', dbRecord, schema, scopes, vars);
41
- * // Returns:
42
- * // {
43
- * // type: 'articles',
44
- * // id: '1',
45
- * // attributes: {
46
- * // title: 'Hello World',
47
- * // content: 'Article content'
48
- * // // Note: author_id and category_id are NOT in attributes
49
- * // }
50
- * // }
51
- *
52
- * @example <caption>Polymorphic fields are also filtered</caption>
53
- * const commentRecord = {
54
- * id: 10,
55
- * text: 'Great article!',
56
- * commentable_type: 'articles',
57
- * commentable_id: 1
58
- * };
59
- * const jsonApiComment = toJsonApiRecord('comments', commentRecord, schema, scopes, vars);
60
- * // commentable_type and commentable_id are filtered out of attributes
61
- *
62
- * @example <caption>Why this is useful upstream</caption>
63
- * // The REST API Knex plugin uses this to:
64
- * // 1. Transform flat SQL records to hierarchical JSON:API format
65
- * // 2. Maintain clean separation between attributes and relationships
66
- * // 3. Filter out foreign keys that become relationship links
67
- * // 4. Handle polymorphic relationship fields automatically
68
- * // 5. Ensure consistent ID formatting (always strings in JSON:API)
69
- */
70
- export const toJsonApiRecord = (scope, record, scopeName) => {
71
- // Extract values from scope
72
- const {
73
- vars: {
74
- schemaInfo: { schema, schemaRelationships: relationships, idProperty }
75
- }
76
- } = scope;
77
-
78
- // Build polymorphic fields set
79
- const polymorphicFields = new Set();
80
- try {
81
- Object.entries(relationships || {}).forEach(([relName, relDef]) => {
82
- if (relDef.belongsToPolymorphic) {
83
- if (relDef.typeField) polymorphicFields.add(relDef.typeField);
84
- if (relDef.idField) polymorphicFields.add(relDef.idField);
85
- }
86
- });
87
- } catch (e) {
88
- // Scope might not have relationships
89
- }
90
-
91
- // Create deps object for toJsonApi
92
- const deps = {
93
- context: {
94
- scopeName,
95
- schemaInfo: { idProperty },
96
- polymorphicFields
97
- }
98
- };
99
-
100
- return toJsonApi(scope, record, deps);
101
- };
102
-
103
- /**
104
- * Builds the final JSON:API response with data and optional included resources
105
- *
106
- * @param {Object} scope - The scope object containing schema and relationship info
107
- * @param {Array<Object>} records - The primary records to transform
108
- * @param {Array<Object>} included - Optional array of included resources
109
- * @param {boolean} isSingle - Whether this is a single resource response
110
- * @param {string} scopeName - The name of the scope/resource type
111
- * @returns {Promise<Object>} Complete JSON:API response object
112
- *
113
- * @example
114
- * const scope = api.resources['articles'];
115
- * const response = await buildJsonApiResponse(scope, [article], [author], true, 'articles');
116
- * // Returns { data: { type: 'articles', id: '1', ... }, included: [...] }
117
- */
118
- export const buildJsonApiResponse = async (scope, records, included = [], isSingle = false, scopeName) => {
119
- // Extract values from scope
120
- const {
121
- vars: {
122
- schemaInfo: { schema, schemaRelationships: relationships, idProperty }
123
- }
124
- } = scope;
125
-
126
- const idField = idProperty || 'id';
127
-
128
- // Handle both Schema objects and plain objects
129
- const schemaStructure = getSchemaStructure(schema);
130
-
131
- // Process records to JSON:API format
132
- const processedRecords = records.map(record => {
133
- const { [RELATIONSHIPS_KEY]: _relationships, ...cleanRecord } = record;
134
- const jsonApiRecord = toJsonApiRecord(scope, cleanRecord, scopeName);
135
-
136
- // Add any loaded relationships
137
- if (_relationships) {
138
- jsonApiRecord.relationships = _relationships;
139
- }
140
-
141
- // Add regular belongsTo relationships (only if not already loaded)
142
- for (const [fieldName, fieldDef] of Object.entries(schemaStructure)) {
143
- if (fieldDef.belongsTo && fieldDef.as && fieldName in cleanRecord) {
144
- jsonApiRecord.relationships = jsonApiRecord.relationships || {};
145
- if (!jsonApiRecord.relationships[fieldDef.as]) {
146
- if (cleanRecord[fieldName] !== null && cleanRecord[fieldName] !== undefined) {
147
- const relationshipObject = {
148
- data: {
149
- type: fieldDef.belongsTo,
150
- id: String(cleanRecord[fieldName])
151
- }
152
- };
153
-
154
- // Add links if urlPrefix is configured
155
- const urlPrefix = scope.vars.resourceUrlPrefix;
156
- if (urlPrefix) {
157
- relationshipObject.links = {
158
- self: `${urlPrefix}/${scopeName}/${record[idField]}/relationships/${fieldDef.as}`,
159
- related: `${urlPrefix}/${scopeName}/${record[idField]}/${fieldDef.as}`
160
- };
161
- }
162
-
163
- jsonApiRecord.relationships[fieldDef.as] = relationshipObject;
164
- } else {
165
- // Explicitly null relationship
166
- const relationshipObject = {
167
- data: null
168
- };
169
-
170
- // Add links even for null relationships
171
- const urlPrefix = scope.vars.resourceUrlPrefix;
172
- if (urlPrefix) {
173
- relationshipObject.links = {
174
- self: `${urlPrefix}/${scopeName}/${record[idField]}/relationships/${fieldDef.as}`,
175
- related: `${urlPrefix}/${scopeName}/${record[idField]}/${fieldDef.as}`
176
- };
177
- }
178
-
179
- jsonApiRecord.relationships[fieldDef.as] = relationshipObject;
180
- }
181
- }
182
- }
183
- }
184
-
185
- // Add polymorphic relationships
186
- Object.entries(relationships || {}).forEach(([relName, relDef]) => {
187
- if (relDef.belongsToPolymorphic) {
188
- const typeValue = cleanRecord[relDef.typeField];
189
- const idValue = cleanRecord[relDef.idField];
190
-
191
- if (typeValue && idValue) {
192
- jsonApiRecord.relationships = jsonApiRecord.relationships || {};
193
- const relationshipObject = {
194
- data: {
195
- type: typeValue,
196
- id: String(idValue)
197
- }
198
- };
199
-
200
- // Add links if urlPrefix is configured
201
- const urlPrefix = scope.vars.resourceUrlPrefix;
202
- if (urlPrefix) {
203
- relationshipObject.links = {
204
- self: `${urlPrefix}/${scopeName}/${record[idField]}/relationships/${relName}`,
205
- related: `${urlPrefix}/${scopeName}/${record[idField]}/${relName}`
206
- };
207
- }
208
-
209
- jsonApiRecord.relationships[relName] = relationshipObject;
210
- } else if (typeValue === null || idValue === null) {
211
- // Explicitly null relationship
212
- jsonApiRecord.relationships = jsonApiRecord.relationships || {};
213
- const relationshipObject = {
214
- data: null
215
- };
216
-
217
- // Add links even for null relationships
218
- const urlPrefix = scope.vars.resourceUrlPrefix;
219
- if (urlPrefix) {
220
- relationshipObject.links = {
221
- self: `${urlPrefix}/${scopeName}/${record[idField]}/relationships/${relName}`,
222
- related: `${urlPrefix}/${scopeName}/${record[idField]}/${relName}`
223
- };
224
- }
225
-
226
- jsonApiRecord.relationships[relName] = relationshipObject;
227
- }
228
- }
229
- });
230
-
231
- return jsonApiRecord;
232
- });
233
-
234
- // Add self links to individual resources if urlPrefix is configured
235
- const urlPrefix = scope.vars.resourceUrlPrefix;
236
- const normalizedData = isSingle ? processedRecords[0] : processedRecords;
237
-
238
- if (urlPrefix && normalizedData) {
239
- if (Array.isArray(normalizedData)) {
240
- // Collection: add self link to each item
241
- normalizedData.forEach(item => {
242
- if (!item.links) item.links = {};
243
- item.links.self = `${urlPrefix}/${scopeName}/${item.id}`;
244
- });
245
- } else {
246
- // Single resource: add self link
247
- if (!normalizedData.links) normalizedData.links = {};
248
- normalizedData.links.self = `${urlPrefix}/${scopeName}/${normalizedData.id}`;
249
- }
250
- }
251
-
252
- // Build response
253
- const response = {
254
- data: normalizedData
255
- };
256
-
257
- // Add included resources if any
258
- if (included.length > 0) {
259
- // Add self links to included resources
260
- if (urlPrefix) {
261
- included.forEach(item => {
262
- if (!item.links) item.links = {};
263
- item.links.self = `${urlPrefix}/${item.type}/${item.id}`;
264
- });
265
- }
266
-
267
- response.included = included;
268
- }
269
-
270
- // Add pagination metadata if provided
271
- if (scope.vars.paginationMeta) {
272
- response.meta = {
273
- pagination: scope.vars.paginationMeta
274
- };
275
- }
276
-
277
- // Add links to response
278
- if (scope.vars.paginationLinks) {
279
- response.links = scope.vars.paginationLinks;
280
- } else if (urlPrefix) {
281
- // Add basic self link for the response when no pagination links
282
- response.links = {
283
- self: isSingle
284
- ? `${urlPrefix}/${scopeName}/${normalizedData.id}`
285
- : `${urlPrefix}/${scopeName}${scope.vars.queryString || ''}`
286
- };
287
- }
288
-
289
- return response;
290
- };
291
-
292
- /**
293
- * Converts a database record to JSON:API format with belongsTo relationships.
294
- * This is used internally by dataGetMinimal to provide consistent JSON:API
295
- * structure without requiring additional database queries.
296
- *
297
- * @param {Object} scope - The scope object containing schema and relationship info
298
- * @param {Object} record - The raw database record to transform
299
- * @param {string} scopeName - The name of the scope/resource type
300
- * @returns {Object} JSON:API formatted resource object with belongsTo relationships
301
- *
302
- * @example
303
- * const dbRecord = {
304
- * id: 1,
305
- * title: 'Hello World',
306
- * author_id: 2,
307
- * publisher_id: 3,
308
- * category_id: null
309
- * };
310
- * const jsonApiRecord = toJsonApiRecordWithBelongsTo(scope, dbRecord, 'articles');
311
- * // Returns:
312
- * // {
313
- * // type: 'articles',
314
- * // id: '1',
315
- * // attributes: {
316
- * // title: 'Hello World'
317
- * // },
318
- * // relationships: {
319
- * // author: {
320
- * // data: { type: 'authors', id: '2' }
321
- * // },
322
- * // publisher: {
323
- * // data: { type: 'publishers', id: '3' }
324
- * // },
325
- * // category: {
326
- * // data: null
327
- * // }
328
- * // }
329
- * // }
330
- */
331
- export const toJsonApiRecordWithBelongsTo = (scope, record, scopeName) => {
332
- if (!record) return null;
333
-
334
- // Get the basic JSON:API structure (without relationships)
335
- const jsonApiRecord = toJsonApiRecord(scope, record, scopeName);
336
-
337
- // Extract schema info from scope
338
- const {
339
- vars: {
340
- schemaInfo: { schema, schemaRelationships: relationships, idProperty }
341
- }
342
- } = scope;
343
-
344
- const idField = idProperty || 'id';
345
-
346
- // Get schema structure
347
- const schemaStructure = getSchemaStructure(schema);
348
-
349
- // Initialize relationships object
350
- jsonApiRecord.relationships = {};
351
-
352
- // Process regular belongsTo relationships from schema
353
- for (const [fieldName, fieldDef] of Object.entries(schemaStructure)) {
354
- if (fieldDef.belongsTo && fieldDef.as) {
355
- const foreignKeyValue = record[fieldName];
356
-
357
- if (foreignKeyValue !== null && foreignKeyValue !== undefined) {
358
- jsonApiRecord.relationships[fieldDef.as] = {
359
- data: {
360
- type: fieldDef.belongsTo,
361
- id: String(foreignKeyValue)
362
- }
363
- };
364
- } else {
365
- // Explicitly null relationship
366
- jsonApiRecord.relationships[fieldDef.as] = {
367
- data: null
368
- };
369
- }
370
- }
371
- }
372
-
373
- // Process polymorphic belongsTo relationships
374
- Object.entries(relationships || {}).forEach(([relName, relDef]) => {
375
- if (relDef.belongsToPolymorphic) {
376
- const typeValue = record[relDef.typeField];
377
- const idValue = record[relDef.idField];
378
-
379
- if (typeValue && idValue) {
380
- jsonApiRecord.relationships[relName] = {
381
- data: {
382
- type: typeValue,
383
- id: String(idValue)
384
- }
385
- };
386
- } else {
387
- // Explicitly null relationship
388
- jsonApiRecord.relationships[relName] = {
389
- data: null
390
- };
391
- }
392
- }
393
- });
394
-
395
- // Remove relationships object if empty
396
- if (Object.keys(jsonApiRecord.relationships).length === 0) {
397
- delete jsonApiRecord.relationships;
398
- }
399
-
400
- return jsonApiRecord;
401
- };
402
-
403
- /**
404
- * Processes belongsTo relationships from JSON:API input to foreign key updates
405
- *
406
- * @param {Object} scope - The scope object containing schema info
407
- * @param {Object} deps - Dependencies object containing context with inputRecord
408
- * @returns {Object} Object mapping foreign key fields to their values
409
- *
410
- * @example
411
- * const scope = api.resources['articles'];
412
- * const deps = { context: { inputRecord: jsonApiData } };
413
- * const updates = processBelongsToRelationships(scope, deps);
414
- * // Converts relationships.author.data.id to { author_id: '123' }
415
- * // Returns: { author_id: '123', category_id: '456' }
416
- */
417
- export const processBelongsToRelationships = (scope, deps) => {
418
- const foreignKeyUpdates = {};
419
-
420
- // Extract values from deps
421
- const { context } = deps;
422
- const inputRecord = context.inputRecord;
423
-
424
- if (!inputRecord.data.relationships) {
425
- return foreignKeyUpdates;
426
- }
427
-
428
- // Extract schema from scope
429
- const {
430
- vars: {
431
- schemaInfo: { schema }
432
- }
433
- } = scope;
434
-
435
- // Schema might be a Schema object or plain object
436
- const schemaStructure = getSchemaStructure(schema);
437
-
438
- for (const [fieldName, fieldDef] of Object.entries(schemaStructure)) {
439
- if (fieldDef.belongsTo && fieldDef.as) {
440
- const relName = fieldDef.as;
441
- if (inputRecord.data.relationships[relName] !== undefined) {
442
- const relData = inputRecord.data.relationships[relName];
443
- foreignKeyUpdates[fieldName] = relData.data?.id || null;
444
- }
445
- }
446
- }
447
-
448
- return foreignKeyUpdates;
449
- };
@@ -1,193 +0,0 @@
1
- /**
2
- * Pagination helper utilities for JSON:API compliant pagination
3
- */
4
-
5
- /**
6
- * Calculate pagination metadata
7
- */
8
- export const calculatePaginationMeta = (total, page, pageSize) => {
9
- // Validate inputs
10
- if (pageSize <= 0) {
11
- throw new Error('Page size must be greater than 0');
12
- }
13
- if (page < 1) {
14
- throw new Error('Page number must be greater than 0');
15
- }
16
-
17
- const pageCount = Math.ceil(total / pageSize);
18
- const currentPage = page || 1;
19
- const hasMore = currentPage < pageCount;
20
-
21
- return {
22
- page: currentPage,
23
- pageSize,
24
- pageCount,
25
- total,
26
- hasMore
27
- };
28
- };
29
-
30
- /**
31
- * Generate pagination links for offset-based pagination
32
- */
33
- export const generatePaginationLinks = (urlPrefix, scopeName, queryParams, paginationMeta) => {
34
- if (!urlPrefix) return null;
35
-
36
- const { page, pageCount, pageSize } = paginationMeta;
37
- const links = {};
38
-
39
- // Build query string without page params
40
- const otherParams = Object.entries(queryParams)
41
- .filter(([key]) => key !== 'page')
42
- .map(([key, value]) => {
43
- if (Array.isArray(value)) {
44
- // Handle arrays (like sort)
45
- return value.map(v => `${key}=${encodeURIComponent(v)}`).join('&');
46
- } else if (typeof value === 'object' && value !== null) {
47
- // Handle nested objects (like filter)
48
- const parts = [];
49
- const processObject = (obj, prefix) => {
50
- Object.entries(obj).forEach(([k, v]) => {
51
- const newKey = prefix ? `${prefix}[${k}]` : `${key}[${k}]`;
52
- if (typeof v === 'object' && v !== null && !Array.isArray(v)) {
53
- processObject(v, newKey);
54
- } else {
55
- parts.push(`${newKey}=${encodeURIComponent(v)}`);
56
- }
57
- });
58
- };
59
- processObject(value, key);
60
- return parts.join('&');
61
- }
62
- return `${key}=${encodeURIComponent(value)}`;
63
- })
64
- .join('&');
65
-
66
- const baseUrl = `${urlPrefix}/${scopeName}`;
67
- const queryPrefix = otherParams ? `?${otherParams}&` : '?';
68
-
69
- // Self link
70
- links.self = `${baseUrl}${queryPrefix}page[number]=${page}&page[size]=${pageSize}`;
71
-
72
- // Only add navigation links if we have page count (counts enabled)
73
- if (pageCount !== undefined) {
74
- // First and last are always available
75
- links.first = `${baseUrl}${queryPrefix}page[number]=1&page[size]=${pageSize}`;
76
- links.last = `${baseUrl}${queryPrefix}page[number]=${pageCount}&page[size]=${pageSize}`;
77
-
78
- // Prev link (if not on first page)
79
- if (page > 1) {
80
- links.prev = `${baseUrl}${queryPrefix}page[number]=${page - 1}&page[size]=${pageSize}`;
81
- }
82
-
83
- // Next link (if not on last page)
84
- if (page < pageCount) {
85
- links.next = `${baseUrl}${queryPrefix}page[number]=${page + 1}&page[size]=${pageSize}`;
86
- }
87
- }
88
-
89
- return links;
90
- };
91
-
92
- /**
93
- * Create a cursor from record data
94
- */
95
- export const createCursor = (record, sortFields = ['id']) => {
96
- const cursorData = {};
97
- sortFields.forEach(field => {
98
- if (record[field] !== undefined) {
99
- cursorData[field] = record[field];
100
- }
101
- });
102
- return Buffer.from(JSON.stringify(cursorData)).toString('base64url');
103
- };
104
-
105
- /**
106
- * Parse a cursor back to data
107
- */
108
- export const parseCursor = (cursor) => {
109
- try {
110
- return JSON.parse(Buffer.from(cursor, 'base64url').toString());
111
- } catch (e) {
112
- throw new Error('Invalid cursor format');
113
- }
114
- };
115
-
116
- /**
117
- * Generate cursor-based pagination links
118
- */
119
- export const generateCursorPaginationLinks = (urlPrefix, scopeName, queryParams, records, pageSize, hasMore, sortFields = ['id']) => {
120
- if (!urlPrefix || !records.length) return null;
121
-
122
- const links = {};
123
- const baseUrl = `${urlPrefix}/${scopeName}`;
124
-
125
- // Build query string without page params
126
- const otherParams = Object.entries(queryParams)
127
- .filter(([key]) => key !== 'page')
128
- .map(([key, value]) => {
129
- if (Array.isArray(value)) {
130
- // Handle arrays (like sort)
131
- return value.map(v => `${key}=${encodeURIComponent(v)}`).join('&');
132
- } else if (typeof value === 'object' && value !== null) {
133
- // Handle nested objects (like filter)
134
- const parts = [];
135
- const processObject = (obj, prefix) => {
136
- Object.entries(obj).forEach(([k, v]) => {
137
- const newKey = prefix ? `${prefix}[${k}]` : `${key}[${k}]`;
138
- if (typeof v === 'object' && v !== null && !Array.isArray(v)) {
139
- processObject(v, newKey);
140
- } else {
141
- parts.push(`${newKey}=${encodeURIComponent(v)}`);
142
- }
143
- });
144
- };
145
- processObject(value, key);
146
- return parts.join('&');
147
- }
148
- return `${key}=${encodeURIComponent(value)}`;
149
- })
150
- .join('&');
151
-
152
- const queryPrefix = otherParams ? `?${otherParams}&` : '?';
153
-
154
- // Self link
155
- if (queryParams.page?.after) {
156
- links.self = `${baseUrl}${queryPrefix}page[size]=${pageSize}&page[after]=${queryParams.page.after}`;
157
- } else if (queryParams.page?.before) {
158
- links.self = `${baseUrl}${queryPrefix}page[size]=${pageSize}&page[before]=${queryParams.page.before}`;
159
- } else {
160
- links.self = `${baseUrl}${queryPrefix}page[size]=${pageSize}`;
161
- }
162
-
163
- // First link (no cursor)
164
- links.first = `${baseUrl}${queryPrefix}page[size]=${pageSize}`;
165
-
166
- // Next link (if hasMore)
167
- if (hasMore && records.length > 0) {
168
- const lastRecord = records[records.length - 1];
169
- const nextCursor = createCursor(lastRecord, sortFields);
170
- links.next = `${baseUrl}${queryPrefix}page[size]=${pageSize}&page[after]=${nextCursor}`;
171
- }
172
-
173
- return links;
174
- };
175
-
176
- /**
177
- * Build cursor-based meta information
178
- */
179
- export const buildCursorMeta = (records, pageSize, hasMore, sortFields = ['id']) => {
180
- const meta = {
181
- pageSize,
182
- hasMore
183
- };
184
-
185
- if (hasMore && records.length > 0) {
186
- const lastRecord = records[records.length - 1];
187
- meta.cursor = {
188
- next: createCursor(lastRecord, sortFields)
189
- };
190
- }
191
-
192
- return meta;
193
- };