json-rest-api 1.0.7 → 1.0.8
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/.claude/agents/library-guide-expert.md +29 -0
- package/.claude/agents/rest-api-internals-expert.md +53 -0
- package/CLAUDE.md +139 -1
- package/README.md +2 -5
- package/docTest.js +238 -0
- package/docs/GUIDE/GUIDE_1_Initial_Setup.md +955 -0
- package/docs/GUIDE/GUIDE_2_1_The_Starting_Point.md +54 -0
- package/docs/GUIDE/GUIDE_2_2_Manipulating_And_Searching_Tables.md +750 -0
- package/docs/GUIDE/GUIDE_2_3_BelongsTo_Relationships.md +494 -0
- package/docs/GUIDE/GUIDE_2_4_HasMany_Records.md +398 -0
- package/docs/GUIDE/GUIDE_2_5_HasMany_Polymorphic.md +882 -0
- package/docs/GUIDE/GUIDE_2_6_Many_To_Many.md +709 -0
- package/docs/GUIDE/GUIDE_2_7_Pagination_And_Ordering.md +1233 -0
- package/docs/GUIDE/GUIDE_2_8_Effects_of_PUT_and_PATCH.md +796 -0
- package/{README_X_Relationship_Plugin.md → docs/GUIDE/GUIDE_2_9_Relationships_Urls.md} +1 -4
- package/docs/GUIDE/GUIDE_2_Data_And_Relations.md +66 -0
- package/docs/GUIDE/GUIDE_3_Field_Transformations.md +1246 -0
- package/docs/GUIDE/GUIDE_5_Non-Db_Stores.md +0 -0
- package/docs/GUIDE/GUIDE_7_Hooks_Data_Management_And_Plugins.md +1490 -0
- package/{README_X_SocketIO.md → docs/GUIDE/GUIDE_X_SocketIO.md} +17 -17
- package/docs/GUIDE/index.md +58 -0
- package/docs/GUIDE.md +61 -2238
- package/docs/ONBOARDING/Computed_Properties_And_Sparse_Fields.md +398 -0
- package/docs/README.md +57 -2239
- package/{README_2_Computed_and_hidden_fields.md → docs/README_VARIOUS/GUIDE_2_Computed_and_hidden_fields.md} +22 -17
- package/docs/README_VARIOUS/GUIDE_IN_DOCS.md +2277 -0
- package/{README.ORIG.md → docs/README_VARIOUS/README.ORIG.md} +8 -8
- package/{README_8_Polymorphic_Relationships.md → docs/README_VARIOUS/README_8_Polymorphic_Relationships.md} +1 -1
- package/docs/README_VARIOUS/README_IN_DOCS.ms +2277 -0
- package/{RELATIONSHIPS_ADVANCED.md → docs/README_VARIOUS/RELATIONSHIPS_ADVANCED.md} +3 -3
- package/{correct-schema-format.md → docs/README_VARIOUS/correct-schema-format.md} +13 -13
- package/{pagination.md → docs/README_VARIOUS/pagination.md} +11 -8
- package/docs/assets/css/style.scss +78 -5
- package/docs/index.md +2 -12
- package/helper-reorganization-plan.md +85 -0
- package/helper-usage-analysis.md +94 -0
- package/package.json +5 -4
- package/plugins/core/bulk-operations-plugin.js +2 -2
- package/plugins/core/connectors/express-plugin.js +90 -32
- package/plugins/core/lib/querying/knex-cross-table-search.js +609 -0
- package/plugins/core/lib/querying/knex-json-api-transformers-querying.js +491 -0
- package/plugins/core/lib/querying/knex-pagination-helpers.js +544 -0
- package/plugins/core/lib/{knex-process-includes.js → querying/knex-process-includes.js} +65 -50
- package/plugins/core/lib/querying/knex-query-helpers-base.js +74 -0
- package/plugins/core/lib/{knex-query-helpers.js → querying/knex-query-helpers.js} +416 -180
- package/plugins/core/lib/{knex-relationship-includes.js → querying/knex-relationship-includes.js} +430 -114
- package/plugins/core/lib/querying/knex-window-queries.js +328 -0
- package/plugins/core/lib/querying-writing/compile-schemas.js +328 -0
- package/plugins/core/{utils → lib/querying-writing}/connectors-query-parser.js +62 -57
- package/plugins/core/lib/{database-capabilities.js → querying-writing/database-capabilities.js} +113 -7
- package/plugins/core/lib/querying-writing/database-value-normalizers.js +369 -0
- package/plugins/core/lib/querying-writing/default-data-helpers.js +72 -0
- package/plugins/core/{utils → lib/querying-writing}/field-utils.js +41 -31
- package/plugins/core/{utils → lib/querying-writing}/knex-constants.js +1 -0
- package/plugins/core/lib/{knex-field-helpers.js → querying-writing/knex-field-helpers.js} +190 -57
- package/plugins/core/lib/querying-writing/knex-json-api-transformers.js +162 -0
- package/plugins/core/lib/{payload-validators.js → querying-writing/payload-validators.js} +297 -217
- package/plugins/core/lib/querying-writing/schema-helpers.js +497 -0
- package/plugins/core/lib/querying-writing/scope-validations.js +431 -0
- package/plugins/core/lib/{simplified-helpers.js → querying-writing/simplified-helpers.js} +216 -176
- package/plugins/core/lib/writing/knex-json-api-transformers-writing.js +110 -0
- package/plugins/core/lib/writing/many-to-many-manipulations.js +300 -0
- package/plugins/core/lib/writing/relationship-processor.js +252 -0
- package/plugins/core/rest-api-knex-plugin.js +183 -62
- package/plugins/core/rest-api-plugin-hooks/compile-resource-schemas.js +7 -0
- package/plugins/core/rest-api-plugin-hooks/register-relationship-routes.js +72 -0
- package/plugins/core/rest-api-plugin-hooks/register-scope-routes.js +102 -0
- package/plugins/core/rest-api-plugin-hooks/turn-scope-init-into-vars.js +50 -0
- package/plugins/core/rest-api-plugin-hooks/validate-include-configurations.js +39 -0
- package/plugins/core/rest-api-plugin-methods/add-route.js +23 -0
- package/plugins/core/rest-api-plugin-methods/check-permissions.js +18 -0
- package/plugins/core/rest-api-plugin-methods/common.js +605 -0
- package/plugins/core/rest-api-plugin-methods/delete-relationship.js +115 -0
- package/plugins/core/rest-api-plugin-methods/delete.js +109 -0
- package/plugins/core/rest-api-plugin-methods/enrich-attributes.js +186 -0
- package/plugins/core/rest-api-plugin-methods/get-related.js +295 -0
- package/plugins/core/rest-api-plugin-methods/get-relationship.js +62 -0
- package/plugins/core/rest-api-plugin-methods/get.js +187 -0
- package/plugins/core/rest-api-plugin-methods/patch-relationship.js +56 -0
- package/plugins/core/rest-api-plugin-methods/patch.js +193 -0
- package/plugins/core/rest-api-plugin-methods/post-relationship.js +104 -0
- package/plugins/core/rest-api-plugin-methods/post.js +157 -0
- package/plugins/core/rest-api-plugin-methods/put.js +257 -0
- package/plugins/core/rest-api-plugin-methods/query.js +208 -0
- package/plugins/core/rest-api-plugin.js +108 -2498
- package/plugins/core/socketio-plugin.js +28 -5
- package/plugins/storage/local-storage.js +2 -2
- package/tests/computed-fields.test.js +483 -0
- package/tests/cors.test.js +4 -4
- package/tests/curl.test.js +5 -4
- package/tests/custom-idproperty-relationships.test.js +891 -0
- package/tests/field-getters.test.js +353 -0
- package/tests/field-setters.test.js +310 -0
- package/tests/fixtures/api-configs.js +800 -28
- package/tests/jwt-auth.test.js +0 -2
- package/tests/nested-includes.test.js +1 -1
- package/tests/pagination-cursor-multifield.test.js +514 -0
- package/tests/pagination-enhanced.test.js +12 -9
- package/tests/polymorphic-relationships.test.js +10 -8
- package/tests/polymorphic-simple.test.js +6 -4
- package/tests/positioning-advanced.test.js +12 -12
- package/tests/positioning-extended.test.js +25 -25
- package/tests/positioning-multifilter.test.js +24 -24
- package/tests/positioning.test.js +3 -3
- package/tests/query-limits.test.js +24 -8
- package/tests/relationship-endpoints.test.js +4 -5
- package/tests/relationships.test.js +0 -1
- package/tests/socketio.test.js +12 -1
- package/tests/virtual-fields.test.js +13 -13
- package/PLAN_FOR_WEBSOCKETS.md +0 -398
- package/README_1_Initial_Setup.md +0 -393
- package/README_X_Hooks.md +0 -647
- package/TODO_rest-api-plugin-extras.todo +0 -23
- package/connectors_ideas/express-plugin.js +0 -120
- package/connectors_ideas/http-plugin.js +0 -138
- package/docs/examples/express-plugin.js +0 -366
- package/example.js +0 -301
- package/plugins/core/HELPER_MODULES.md +0 -285
- package/plugins/core/lib/compile-schemas.js +0 -245
- package/plugins/core/lib/database-value-normalizers.js +0 -192
- package/plugins/core/lib/default-data-helpers.js +0 -100
- package/plugins/core/lib/knex-cross-table-search.js +0 -442
- package/plugins/core/lib/knex-json-api-helpers.js +0 -84
- package/plugins/core/lib/knex-json-api-transformers.js +0 -449
- package/plugins/core/lib/knex-pagination-helpers.js +0 -193
- package/plugins/core/lib/knex-window-queries.js +0 -142
- package/plugins/core/lib/many-to-many-manipulations.js +0 -343
- package/plugins/core/lib/relationship-processor.js +0 -278
- package/plugins/core/lib/schema-helpers.js +0 -343
- package/plugins/core/lib/scope-validations.js +0 -252
- package/plugins/core/rest-api-relationships-plugin.js +0 -700
- package/plugins/core/utils/knex-query-helpers-base.js +0 -56
- /package/{README_X_Permissions.md → docs/GUIDE/GUIDE_4_Authentication_And_Permissions.md} +0 -0
- /package/{README_X_Positioning.md → docs/GUIDE/GUIDE_6_Positioning.md} +0 -0
- /package/{README_X_Bulk_Operations.md → docs/GUIDE/GUIDE_X_Bulk_Operations.md} +0 -0
- /package/{README_X_Cors.md → docs/GUIDE/GUIDE_X_Cors.md} +0 -0
- /package/{README_X_Multihome.md → docs/GUIDE/GUIDE_X_Multihome.md} +0 -0
- /package/{README_X_Appendices.md → docs/GUIDE/GUIDE_Y_Appendices.md} +0 -0
- /package/{QUERY_DEPTH_PROTECTION.md → docs/README_VARIOUS/QUERY_DEPTH_PROTECTION.md} +0 -0
- /package/{RELATIONSHIPS.md → docs/README_VARIOUS/RELATIONSHIPS.md} +0 -0
- /package/{WEBSOCKET_IMPLEMENTATION_STATUS.md → docs/README_VARIOUS/WEBSOCKET_IMPLEMENTATION_STATUS.md} +0 -0
- /package/{helper-functions-chart.md → docs/README_VARIOUS/helper-functions-chart.md} +0 -0
- /package/docs/{implementation-hidden-computed-fields.md → README_VARIOUS/implementation-hidden-computed-fields.md} +0 -0
- /package/docs/{include-limits-migration-guide.md → README_VARIOUS/include-limits-migration-guide.md} +0 -0
- /package/{readmeSnippets → docs/README_VARIOUS}/test_put_behavior.js +0 -0
- /package/{readmeSnippets → docs/README_VARIOUS}/test_put_fields_without_defaults.js +0 -0
|
@@ -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
|
+
};
|