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
|
@@ -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
|
-
};
|