json-rest-api 1.0.14 → 1.0.15
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/connectors_ideas/fastify.js +66 -68
- package/connectors_ideas/koa-plugin.js +50 -51
- package/eslint.config.js +11 -2
- package/examples/url-override-example.js +63 -63
- package/index.js +13 -13
- package/lib/enhanced-logger.js +51 -51
- package/lib/error-formatter.js +61 -61
- package/lib/rest-api-errors.js +28 -28
- package/package.json +1 -1
- package/plugins/core/bulk-operations-plugin.js +142 -145
- package/plugins/core/connectors/express-plugin.js +210 -217
- package/plugins/core/connectors/lib/busboy-detector.js +84 -84
- package/plugins/core/connectors/lib/formidable-detector.js +50 -50
- package/plugins/core/connectors/lib/request-helpers.js +74 -74
- package/plugins/core/file-handling-plugin.js +125 -125
- package/plugins/core/lib/anyapi/anyapi-registry.js +254 -254
- package/plugins/core/lib/anyapi/query/anyapi-query-adapter.js +287 -287
- package/plugins/core/lib/anyapi/schema-utils.js +101 -101
- package/plugins/core/lib/anyapi/utils/descriptor-helpers.js +41 -41
- package/plugins/core/lib/dbTablesOperations.js +134 -135
- package/plugins/core/lib/fractional-positioning.js +74 -76
- package/plugins/core/lib/querying/knex-cross-table-search.js +320 -320
- package/plugins/core/lib/querying/knex-json-api-transformers-querying.js +144 -144
- package/plugins/core/lib/querying/knex-pagination-helpers.js +164 -164
- package/plugins/core/lib/querying/knex-process-includes.js +50 -50
- package/plugins/core/lib/querying/knex-query-helpers-base.js +31 -31
- package/plugins/core/lib/querying/knex-query-helpers.js +451 -452
- package/plugins/core/lib/querying/knex-relationship-includes.js +1160 -1170
- package/plugins/core/lib/querying/knex-window-queries.js +89 -89
- package/plugins/core/lib/querying/url-helpers.js +35 -35
- package/plugins/core/lib/querying-writing/compile-schemas.js +97 -98
- package/plugins/core/lib/querying-writing/connectors-query-parser.js +31 -31
- package/plugins/core/lib/querying-writing/database-capabilities.js +94 -94
- package/plugins/core/lib/querying-writing/database-value-normalizers.js +76 -76
- package/plugins/core/lib/querying-writing/default-data-helpers.js +27 -27
- package/plugins/core/lib/querying-writing/field-utils.js +44 -42
- package/plugins/core/lib/querying-writing/knex-constants.js +15 -16
- package/plugins/core/lib/querying-writing/knex-field-helpers.js +117 -115
- package/plugins/core/lib/querying-writing/knex-json-api-transformers.js +41 -41
- package/plugins/core/lib/querying-writing/payload-validators.js +275 -275
- package/plugins/core/lib/querying-writing/schema-helpers.js +129 -129
- package/plugins/core/lib/querying-writing/scope-validations.js +143 -146
- package/plugins/core/lib/querying-writing/simplified-helpers.js +133 -129
- package/plugins/core/lib/storage/storage-adapter.js +68 -68
- package/plugins/core/lib/writing/knex-json-api-transformers-writing.js +31 -31
- package/plugins/core/lib/writing/many-to-many-manipulations.js +81 -81
- package/plugins/core/lib/writing/relationship-processor.js +58 -58
- package/plugins/core/rest-api-access.js +8 -8
- package/plugins/core/rest-api-anyapi-knex-plugin.js +920 -920
- package/plugins/core/rest-api-cors-plugin.js +170 -171
- package/plugins/core/rest-api-knex-plugin.js +482 -493
- package/plugins/core/rest-api-multihome-plugin.js +247 -251
- package/plugins/core/rest-api-plugin-hooks/compile-resource-schemas.js +5 -5
- package/plugins/core/rest-api-plugin-hooks/register-relationship-routes.js +20 -21
- package/plugins/core/rest-api-plugin-hooks/register-scope-routes.js +44 -44
- package/plugins/core/rest-api-plugin-hooks/turn-scope-init-into-vars.js +23 -23
- package/plugins/core/rest-api-plugin-hooks/validate-include-configurations.js +15 -15
- package/plugins/core/rest-api-plugin-methods/add-route.js +9 -9
- package/plugins/core/rest-api-plugin-methods/check-permissions.js +5 -6
- package/plugins/core/rest-api-plugin-methods/common.js +295 -297
- package/plugins/core/rest-api-plugin-methods/delete-relationship.js +35 -37
- package/plugins/core/rest-api-plugin-methods/delete.js +57 -59
- package/plugins/core/rest-api-plugin-methods/enrich-attributes.js +172 -171
- package/plugins/core/rest-api-plugin-methods/get-related.js +96 -96
- package/plugins/core/rest-api-plugin-methods/get-relationship.js +16 -18
- package/plugins/core/rest-api-plugin-methods/get.js +86 -86
- package/plugins/core/rest-api-plugin-methods/patch-relationship.js +16 -17
- package/plugins/core/rest-api-plugin-methods/patch.js +63 -64
- package/plugins/core/rest-api-plugin-methods/post-relationship.js +54 -55
- package/plugins/core/rest-api-plugin-methods/post.js +61 -62
- package/plugins/core/rest-api-plugin-methods/put.js +86 -87
- package/plugins/core/rest-api-plugin-methods/query.js +88 -88
- package/plugins/core/rest-api-plugin-methods/release.js +5 -6
- package/plugins/core/rest-api-plugin.js +76 -78
- package/plugins/core/rest-api-positioning-plugin.js +251 -252
- package/plugins/core/socketio-plugin.js +240 -236
- package/plugins/storage/local-storage.js +103 -103
- package/plugins/storage/s3-storage.js +41 -42
- package/quickTest.js +37 -37
- package/scripts/debug-anyapi-review-filter.mjs +18 -18
- package/scripts/inspect-anyapi-descriptor-country.mjs +10 -10
- package/scripts/inspect-anyapi-descriptor.mjs +10 -10
- package/scripts/run-docs.js +112 -113
- package/test-debug.js +28 -28
- package/tests/TEST_TEMPLATE.test.js +51 -51
- package/tests/anyapi-basic.test.js +42 -42
- package/tests/anyapi-cursor-pagination.test.js +48 -48
- package/tests/bulk-operations.test.js +208 -208
- package/tests/computed-fields.test.js +204 -204
- package/tests/cors.test.js +412 -413
- package/tests/curl.test.js +275 -276
- package/tests/custom-idproperty-relationships.test.js +266 -266
- package/tests/field-getters.test.js +132 -132
- package/tests/field-setters.test.js +110 -110
- package/tests/fixtures/api-configs.js +699 -699
- package/tests/helpers/storage-mode.js +32 -32
- package/tests/helpers/test-utils.js +76 -76
- package/tests/include-depth-validation.test.js +76 -76
- package/tests/includes.test.js +145 -145
- package/tests/multihome.test.js +175 -175
- package/tests/nested-includes.test.js +255 -255
- package/tests/pagination-cursor-multifield.test.js +164 -164
- package/tests/pagination-enhanced.test.js +225 -225
- package/tests/polymorphic-relationships.test.js +96 -96
- package/tests/polymorphic-simple.test.js +44 -44
- package/tests/positioning-advanced.test.js +226 -226
- package/tests/positioning-extended.test.js +399 -399
- package/tests/positioning-multifilter.test.js +204 -204
- package/tests/positioning.test.js +266 -266
- package/tests/queries.test.js +184 -184
- package/tests/query-limits.test.js +154 -155
- package/tests/relationship-endpoints.test.js +127 -127
- package/tests/relationships.test.js +178 -179
- package/tests/rest-api-access.test.js +86 -86
- package/tests/rest-api.test.js +103 -103
- package/tests/searchschema-merge.test.js +93 -93
- package/tests/socketio.test.js +326 -326
- package/tests/virtual-fields.test.js +164 -164
|
@@ -1,32 +1,31 @@
|
|
|
1
1
|
/**
|
|
2
2
|
* @module knex-constants
|
|
3
3
|
* @description Shared constants for the REST API Knex plugin
|
|
4
|
-
*
|
|
4
|
+
*
|
|
5
5
|
* This module contains all the magic strings and shared constants used
|
|
6
6
|
* throughout the plugin to avoid repetition and improve maintainability.
|
|
7
7
|
*/
|
|
8
8
|
|
|
9
9
|
// Internal property names - using unique names to avoid collisions with user data
|
|
10
|
-
export const RELATIONSHIPS_KEY = '__$jsonrestapi_relationships$__'
|
|
11
|
-
export const RELATIONSHIP_METADATA_KEY = '__$jsonrestapi_metadata$__'
|
|
12
|
-
export const ROW_NUMBER_KEY = '__$jsonrestapi_rn$__'
|
|
13
|
-
export const COMPUTED_DEPENDENCIES_KEY = '__$jsonrestapi_computed_deps$__'
|
|
10
|
+
export const RELATIONSHIPS_KEY = '__$jsonrestapi_relationships$__'
|
|
11
|
+
export const RELATIONSHIP_METADATA_KEY = '__$jsonrestapi_metadata$__'
|
|
12
|
+
export const ROW_NUMBER_KEY = '__$jsonrestapi_rn$__'
|
|
13
|
+
export const COMPUTED_DEPENDENCIES_KEY = '__$jsonrestapi_computed_deps$__'
|
|
14
14
|
|
|
15
15
|
// Default query limits
|
|
16
|
-
export const DEFAULT_QUERY_LIMIT = 20
|
|
17
|
-
export const DEFAULT_MAX_QUERY_LIMIT = 100
|
|
18
|
-
export const DEFAULT_MAX_INCLUDE_LIMIT = 1000
|
|
16
|
+
export const DEFAULT_QUERY_LIMIT = 20
|
|
17
|
+
export const DEFAULT_MAX_QUERY_LIMIT = 100
|
|
18
|
+
export const DEFAULT_MAX_INCLUDE_LIMIT = 1000
|
|
19
19
|
export const DEFAULT_INCLUDE_DEPTH_LIMIT = 3
|
|
20
20
|
|
|
21
|
-
|
|
22
21
|
// Schema handling helpers
|
|
23
|
-
export const getSchemaStructure = (schema) => schema?.structure || schema || {}
|
|
24
|
-
export const getIdProperty = (schemaInfo) => schemaInfo?.idProperty || 'id'
|
|
22
|
+
export const getSchemaStructure = (schema) => schema?.structure || schema || {}
|
|
23
|
+
export const getIdProperty = (schemaInfo) => schemaInfo?.idProperty || 'id'
|
|
25
24
|
|
|
26
25
|
// Default values
|
|
27
|
-
export const DEFAULT_PAGE_SIZE = 20
|
|
28
|
-
export const DEFAULT_MAX_PAGE_SIZE = 100
|
|
29
|
-
export const DEFAULT_PAGE_NUMBER = 1
|
|
26
|
+
export const DEFAULT_PAGE_SIZE = 20
|
|
27
|
+
export const DEFAULT_MAX_PAGE_SIZE = 100
|
|
28
|
+
export const DEFAULT_PAGE_NUMBER = 1
|
|
30
29
|
|
|
31
30
|
// Database clients
|
|
32
31
|
export const DB_CLIENTS = {
|
|
@@ -35,7 +34,7 @@ export const DB_CLIENTS = {
|
|
|
35
34
|
MYSQL2: 'mysql2',
|
|
36
35
|
SQLITE: 'sqlite3',
|
|
37
36
|
MSSQL: 'mssql'
|
|
38
|
-
}
|
|
37
|
+
}
|
|
39
38
|
|
|
40
39
|
// Error subtypes
|
|
41
40
|
export const ERROR_SUBTYPES = {
|
|
@@ -43,4 +42,4 @@ export const ERROR_SUBTYPES = {
|
|
|
43
42
|
VALIDATION: 'validation',
|
|
44
43
|
PERMISSION: 'permission',
|
|
45
44
|
CONFLICT: 'conflict'
|
|
46
|
-
}
|
|
45
|
+
}
|
|
@@ -1,10 +1,10 @@
|
|
|
1
1
|
/**
|
|
2
2
|
* Checks if a field exists only in code, not in the database
|
|
3
|
-
*
|
|
3
|
+
*
|
|
4
4
|
* @param {string} fieldName - The field name to check
|
|
5
5
|
* @param {Object} schemaInfo - Schema info with computed fields and structure
|
|
6
6
|
* @returns {boolean} True if field is computed or virtual
|
|
7
|
-
*
|
|
7
|
+
*
|
|
8
8
|
* @example
|
|
9
9
|
* // Input: Check computed field
|
|
10
10
|
* const schemaInfo = {
|
|
@@ -13,44 +13,44 @@
|
|
|
13
13
|
* };
|
|
14
14
|
* isNonDatabaseField('profit_margin', schemaInfo);
|
|
15
15
|
* // Output: true (it's computed, not stored)
|
|
16
|
-
*
|
|
16
|
+
*
|
|
17
17
|
* @example
|
|
18
18
|
* // Input: Check virtual field
|
|
19
19
|
* const schemaInfo = {
|
|
20
20
|
* computed: {},
|
|
21
|
-
* schemaStructure: {
|
|
21
|
+
* schemaStructure: {
|
|
22
22
|
* temp_password: { type: 'string', virtual: true }
|
|
23
23
|
* }
|
|
24
24
|
* };
|
|
25
25
|
* isNonDatabaseField('temp_password', schemaInfo);
|
|
26
26
|
* // Output: true (it's virtual, not stored)
|
|
27
|
-
*
|
|
27
|
+
*
|
|
28
28
|
* @description
|
|
29
29
|
* Used by:
|
|
30
30
|
* - buildFieldSelection to exclude from SELECT queries
|
|
31
31
|
* - dataPost/dataPatch to skip non-database fields
|
|
32
32
|
* - enrichAttributes to identify computed fields
|
|
33
|
-
*
|
|
33
|
+
*
|
|
34
34
|
* Purpose:
|
|
35
35
|
* - Prevents SQL errors by excluding non-existent columns
|
|
36
36
|
* - Identifies fields that need computation or special handling
|
|
37
37
|
* - Supports virtual fields for temporary/input-only data
|
|
38
38
|
*/
|
|
39
39
|
export const isNonDatabaseField = (fieldName, schemaInfo) => {
|
|
40
|
-
const { computed = {}, schemaStructure = {} } = schemaInfo
|
|
41
|
-
if (fieldName in computed) return true
|
|
42
|
-
const fieldDef = schemaStructure[fieldName]
|
|
43
|
-
return fieldDef && fieldDef.virtual === true
|
|
44
|
-
}
|
|
40
|
+
const { computed = {}, schemaStructure = {} } = schemaInfo
|
|
41
|
+
if (fieldName in computed) return true
|
|
42
|
+
const fieldDef = schemaStructure[fieldName]
|
|
43
|
+
return fieldDef && fieldDef.virtual === true
|
|
44
|
+
}
|
|
45
45
|
|
|
46
46
|
/**
|
|
47
47
|
* Builds the field selection list for database queries with sparse fieldset support
|
|
48
|
-
*
|
|
48
|
+
*
|
|
49
49
|
* @param {Object} scope - Resource scope with schema information
|
|
50
50
|
* @param {Object} deps - Dependencies object
|
|
51
51
|
* @param {Object} deps.context - Request context with queryParams and schemaInfo
|
|
52
52
|
* @returns {Promise<Object>} Field selection information
|
|
53
|
-
*
|
|
53
|
+
*
|
|
54
54
|
* @example
|
|
55
55
|
* // Input: Basic sparse fieldset request
|
|
56
56
|
* const deps = {
|
|
@@ -61,7 +61,7 @@ export const isNonDatabaseField = (fieldName, schemaInfo) => {
|
|
|
61
61
|
* }
|
|
62
62
|
* };
|
|
63
63
|
* const result = await buildFieldSelection(scope, deps);
|
|
64
|
-
*
|
|
64
|
+
*
|
|
65
65
|
* // Output: Includes requested fields plus required fields
|
|
66
66
|
* // {
|
|
67
67
|
* // fieldsToSelect: ['id', 'title', 'body', 'author_id', 'category_id'],
|
|
@@ -70,12 +70,12 @@ export const isNonDatabaseField = (fieldName, schemaInfo) => {
|
|
|
70
70
|
* // idProperty: 'id'
|
|
71
71
|
* // }
|
|
72
72
|
* // Note: Foreign keys (author_id, category_id) always included for relationships
|
|
73
|
-
*
|
|
73
|
+
*
|
|
74
74
|
* @example
|
|
75
75
|
* // Input: Computed field with hidden dependencies
|
|
76
76
|
* const schemaInfo = {
|
|
77
77
|
* computed: {
|
|
78
|
-
* profit_margin: {
|
|
78
|
+
* profit_margin: {
|
|
79
79
|
* compute: (record) => (record.price - record.cost) / record.price,
|
|
80
80
|
* dependencies: ['price', 'cost']
|
|
81
81
|
* }
|
|
@@ -91,7 +91,7 @@ export const isNonDatabaseField = (fieldName, schemaInfo) => {
|
|
|
91
91
|
* queryParams: { fields: { products: 'name,profit_margin' } }
|
|
92
92
|
* }
|
|
93
93
|
* };
|
|
94
|
-
*
|
|
94
|
+
*
|
|
95
95
|
* // Output: Fetches hidden dependency for computation
|
|
96
96
|
* // {
|
|
97
97
|
* // fieldsToSelect: ['id', 'name', 'price', 'cost'],
|
|
@@ -99,7 +99,7 @@ export const isNonDatabaseField = (fieldName, schemaInfo) => {
|
|
|
99
99
|
* // computedDependencies: ['cost'], // Will be removed after computation
|
|
100
100
|
* // idProperty: 'id'
|
|
101
101
|
* // }
|
|
102
|
-
*
|
|
102
|
+
*
|
|
103
103
|
* @example
|
|
104
104
|
* // Input: No sparse fieldsets (returns all visible fields)
|
|
105
105
|
* const deps = {
|
|
@@ -108,7 +108,7 @@ export const isNonDatabaseField = (fieldName, schemaInfo) => {
|
|
|
108
108
|
* schemaInfo: { ... }
|
|
109
109
|
* }
|
|
110
110
|
* };
|
|
111
|
-
*
|
|
111
|
+
*
|
|
112
112
|
* // Output: All non-hidden, non-virtual fields
|
|
113
113
|
* // {
|
|
114
114
|
* // fieldsToSelect: ['id', 'title', 'body', 'published_at', 'author_id'],
|
|
@@ -116,20 +116,20 @@ export const isNonDatabaseField = (fieldName, schemaInfo) => {
|
|
|
116
116
|
* // computedDependencies: [],
|
|
117
117
|
* // idProperty: 'id'
|
|
118
118
|
* // }
|
|
119
|
-
*
|
|
119
|
+
*
|
|
120
120
|
* @description
|
|
121
121
|
* Used by:
|
|
122
122
|
* - dataGet to build SELECT query
|
|
123
|
-
* - dataQuery to build SELECT query
|
|
123
|
+
* - dataQuery to build SELECT query
|
|
124
124
|
* - buildQuerySelection as the main field resolver
|
|
125
|
-
*
|
|
125
|
+
*
|
|
126
126
|
* Purpose:
|
|
127
127
|
* - Implements JSON:API sparse fieldsets specification
|
|
128
128
|
* - Ensures relationships work by including foreign keys
|
|
129
129
|
* - Handles computed field dependencies intelligently
|
|
130
130
|
* - Respects field visibility rules (hidden/normallyHidden)
|
|
131
131
|
* - Supports custom ID column names
|
|
132
|
-
*
|
|
132
|
+
*
|
|
133
133
|
* Data flow:
|
|
134
134
|
* 1. Always includes ID (with aliasing if needed)
|
|
135
135
|
* 2. Parses requested fields from query string
|
|
@@ -139,55 +139,57 @@ export const isNonDatabaseField = (fieldName, schemaInfo) => {
|
|
|
139
139
|
* 6. Returns complete field list for SQL SELECT
|
|
140
140
|
*/
|
|
141
141
|
export const buildFieldSelection = async (scope, deps) => {
|
|
142
|
-
|
|
143
|
-
|
|
142
|
+
const fieldsToSelect = new Set()
|
|
143
|
+
const computedDependencies = new Set()
|
|
144
144
|
|
|
145
145
|
// Extract values from scope
|
|
146
|
-
const {
|
|
147
|
-
vars: {
|
|
148
|
-
schemaInfo: { schemaInstance
|
|
146
|
+
const {
|
|
147
|
+
vars: {
|
|
148
|
+
schemaInfo: { schemaInstance, computed: computedFields = {}, schemaStructure }
|
|
149
149
|
}
|
|
150
|
-
} = scope
|
|
151
|
-
|
|
150
|
+
} = scope
|
|
151
|
+
|
|
152
152
|
// Extract values from deps
|
|
153
|
-
const { context } = deps
|
|
154
|
-
const scopeName = context.scopeName
|
|
155
|
-
const requestedFields = context.queryParams?.fields?.[scopeName]
|
|
156
|
-
const idProperty = context.schemaInfo.idProperty
|
|
157
|
-
|
|
153
|
+
const { context } = deps
|
|
154
|
+
const scopeName = context.scopeName
|
|
155
|
+
const requestedFields = context.queryParams?.fields?.[scopeName]
|
|
156
|
+
const idProperty = context.schemaInfo.idProperty
|
|
157
|
+
|
|
158
158
|
// Always include the ID field - required for JSON:API
|
|
159
159
|
// Handle aliasing if idProperty is not 'id'
|
|
160
160
|
if (idProperty !== 'id') {
|
|
161
|
-
fieldsToSelect.add(`${idProperty} as id`)
|
|
161
|
+
fieldsToSelect.add(`${idProperty} as id`)
|
|
162
162
|
} else {
|
|
163
|
-
fieldsToSelect.add('id')
|
|
163
|
+
fieldsToSelect.add('id')
|
|
164
164
|
}
|
|
165
|
-
|
|
165
|
+
|
|
166
166
|
// Handle both Schema objects and plain objects
|
|
167
167
|
if (!schemaStructure) {
|
|
168
|
-
schemaStructure = schemaInstance?.structure || schemaInstance || {}
|
|
168
|
+
schemaStructure = schemaInstance?.structure || schemaInstance || {}
|
|
169
169
|
}
|
|
170
|
-
|
|
170
|
+
|
|
171
171
|
// Get computed fields and virtual fields from schema
|
|
172
|
-
const computedFieldNames = new Set(Object.keys(computedFields))
|
|
173
|
-
|
|
172
|
+
const computedFieldNames = new Set(Object.keys(computedFields))
|
|
173
|
+
|
|
174
174
|
// Find fields marked as virtual in the schema
|
|
175
|
-
const virtualFieldNames = new Set()
|
|
175
|
+
const virtualFieldNames = new Set()
|
|
176
176
|
Object.entries(schemaStructure).forEach(([fieldName, fieldDef]) => {
|
|
177
177
|
if (fieldDef.virtual === true) {
|
|
178
|
-
virtualFieldNames.add(fieldName)
|
|
178
|
+
virtualFieldNames.add(fieldName)
|
|
179
179
|
}
|
|
180
|
-
})
|
|
181
|
-
|
|
182
|
-
const nonDatabaseFields = new Set([...computedFieldNames, ...virtualFieldNames])
|
|
183
|
-
|
|
180
|
+
})
|
|
181
|
+
|
|
182
|
+
const nonDatabaseFields = new Set([...computedFieldNames, ...virtualFieldNames])
|
|
183
|
+
|
|
184
184
|
// Parse requested fields
|
|
185
|
-
const requested = requestedFields
|
|
186
|
-
|
|
187
|
-
|
|
188
|
-
|
|
189
|
-
|
|
190
|
-
|
|
185
|
+
const requested = requestedFields
|
|
186
|
+
? (
|
|
187
|
+
typeof requestedFields === 'string'
|
|
188
|
+
? requestedFields.split(',').map(f => f.trim()).filter(f => f)
|
|
189
|
+
: requestedFields
|
|
190
|
+
)
|
|
191
|
+
: null
|
|
192
|
+
|
|
191
193
|
if (requested && requested.length > 0) {
|
|
192
194
|
// Sparse fieldsets requested - only select specified fields
|
|
193
195
|
// Example: ?fields[products]=name,price,profit_margin
|
|
@@ -195,79 +197,79 @@ export const buildFieldSelection = async (scope, deps) => {
|
|
|
195
197
|
// Skip computed and virtual fields - they don't exist in database
|
|
196
198
|
// Computed fields will be calculated later in enrichAttributes
|
|
197
199
|
// Virtual fields are handled separately (from request input)
|
|
198
|
-
if (nonDatabaseFields.has(field)) return
|
|
199
|
-
|
|
200
|
-
const fieldDef = schemaStructure[field]
|
|
201
|
-
if (!fieldDef) throw new Error(`Unknown sparse field '${field}' requested for '${scopeName}'`)
|
|
200
|
+
if (nonDatabaseFields.has(field)) return
|
|
201
|
+
|
|
202
|
+
const fieldDef = schemaStructure[field]
|
|
203
|
+
if (!fieldDef) throw new Error(`Unknown sparse field '${field}' requested for '${scopeName}'`)
|
|
202
204
|
|
|
203
205
|
if (fieldDef.belongsToPolymorphic) {
|
|
204
|
-
return
|
|
206
|
+
return
|
|
205
207
|
}
|
|
206
|
-
|
|
208
|
+
|
|
207
209
|
// NEVER include hidden fields, even if explicitly requested
|
|
208
210
|
// Example: password_hash with hidden:true is never returned
|
|
209
|
-
if (fieldDef.hidden === true) return
|
|
210
|
-
|
|
211
|
-
fieldsToSelect.add(field)
|
|
212
|
-
})
|
|
213
|
-
|
|
211
|
+
if (fieldDef.hidden === true) return
|
|
212
|
+
|
|
213
|
+
fieldsToSelect.add(field)
|
|
214
|
+
})
|
|
215
|
+
|
|
214
216
|
// Handle computed field dependencies - fetch fields needed for calculations
|
|
215
217
|
// Example: User requests 'profit_margin' which depends on 'price' and 'cost'
|
|
216
218
|
// We need to fetch price and cost from DB even if not explicitly requested
|
|
217
|
-
const requestedComputedFields = requested.filter(f => computedFieldNames.has(f))
|
|
219
|
+
const requestedComputedFields = requested.filter(f => computedFieldNames.has(f))
|
|
218
220
|
for (const computedField of requestedComputedFields) {
|
|
219
|
-
const fieldDef = computedFields[computedField]
|
|
221
|
+
const fieldDef = computedFields[computedField]
|
|
220
222
|
if (fieldDef.dependencies) {
|
|
221
223
|
for (const dep of fieldDef.dependencies) {
|
|
222
|
-
const depFieldDef = schemaStructure[dep]
|
|
224
|
+
const depFieldDef = schemaStructure[dep]
|
|
223
225
|
// Only add dependency if it exists and isn't hidden
|
|
224
226
|
if (depFieldDef && depFieldDef.hidden !== true) {
|
|
225
|
-
fieldsToSelect.add(dep)
|
|
227
|
+
fieldsToSelect.add(dep)
|
|
226
228
|
// Track dependencies that weren't explicitly requested
|
|
227
229
|
// These will be removed from the final response
|
|
228
230
|
if (!requested.includes(dep)) {
|
|
229
|
-
computedDependencies.add(dep)
|
|
231
|
+
computedDependencies.add(dep)
|
|
230
232
|
}
|
|
231
233
|
}
|
|
232
234
|
}
|
|
233
235
|
}
|
|
234
236
|
}
|
|
235
|
-
|
|
237
|
+
|
|
236
238
|
// Still handle normallyHidden fields for backward compatibility
|
|
237
239
|
if (requestedComputedFields.length > 0) {
|
|
238
240
|
Object.entries(schemaStructure).forEach(([field, fieldDef]) => {
|
|
239
241
|
if (fieldDef.normallyHidden === true && fieldDef.hidden !== true) {
|
|
240
242
|
// Only add if not already handled by dependencies
|
|
241
243
|
if (!fieldsToSelect.has(field)) {
|
|
242
|
-
fieldsToSelect.add(field)
|
|
244
|
+
fieldsToSelect.add(field)
|
|
243
245
|
if (!requested.includes(field)) {
|
|
244
|
-
computedDependencies.add(field)
|
|
246
|
+
computedDependencies.add(field)
|
|
245
247
|
}
|
|
246
248
|
}
|
|
247
249
|
}
|
|
248
|
-
})
|
|
250
|
+
})
|
|
249
251
|
}
|
|
250
252
|
} else {
|
|
251
253
|
// No sparse fieldsets - return all visible fields
|
|
252
254
|
// This is the default behavior when no ?fields parameter is provided
|
|
253
255
|
Object.entries(schemaStructure).forEach(([field, fieldDef]) => {
|
|
254
256
|
// Skip virtual fields - they don't exist in database
|
|
255
|
-
if (fieldDef.virtual === true) return
|
|
256
|
-
|
|
257
|
+
if (fieldDef.virtual === true) return
|
|
258
|
+
|
|
257
259
|
// Skip hidden fields - these are NEVER returned
|
|
258
260
|
// Example: password_hash with hidden:true
|
|
259
|
-
if (fieldDef.hidden === true) return
|
|
260
|
-
|
|
261
|
+
if (fieldDef.hidden === true) return
|
|
262
|
+
|
|
261
263
|
// Skip polymorphic placeholder fields - handled via type/id columns
|
|
262
|
-
if (fieldDef.belongsToPolymorphic) return
|
|
264
|
+
if (fieldDef.belongsToPolymorphic) return
|
|
263
265
|
|
|
264
266
|
// Skip normallyHidden fields - these are hidden by default
|
|
265
267
|
// Example: cost with normallyHidden:true (only returned when explicitly requested)
|
|
266
|
-
if (fieldDef.normallyHidden === true) return
|
|
267
|
-
|
|
268
|
-
fieldsToSelect.add(field)
|
|
269
|
-
})
|
|
270
|
-
|
|
268
|
+
if (fieldDef.normallyHidden === true) return
|
|
269
|
+
|
|
270
|
+
fieldsToSelect.add(field)
|
|
271
|
+
})
|
|
272
|
+
|
|
271
273
|
// When no sparse fieldsets, we compute all computed fields
|
|
272
274
|
// So we need to include their dependencies even if normallyHidden
|
|
273
275
|
// Example: profit_margin depends on 'cost' which is normallyHidden
|
|
@@ -275,40 +277,40 @@ export const buildFieldSelection = async (scope, deps) => {
|
|
|
275
277
|
for (const [fieldName, fieldDef] of Object.entries(computedFields)) {
|
|
276
278
|
if (fieldDef.dependencies) {
|
|
277
279
|
for (const dep of fieldDef.dependencies) {
|
|
278
|
-
const depFieldDef = schemaStructure[dep]
|
|
280
|
+
const depFieldDef = schemaStructure[dep]
|
|
279
281
|
if (depFieldDef && depFieldDef.hidden !== true) {
|
|
280
|
-
fieldsToSelect.add(dep)
|
|
282
|
+
fieldsToSelect.add(dep)
|
|
281
283
|
// Track normallyHidden dependencies for later removal
|
|
282
284
|
// These are fetched for computation but not returned
|
|
283
285
|
if (depFieldDef.normallyHidden === true) {
|
|
284
|
-
computedDependencies.add(dep)
|
|
286
|
+
computedDependencies.add(dep)
|
|
285
287
|
}
|
|
286
288
|
}
|
|
287
289
|
}
|
|
288
290
|
}
|
|
289
291
|
}
|
|
290
292
|
}
|
|
291
|
-
|
|
293
|
+
|
|
292
294
|
// Always include foreign keys for relationships (unless hidden)
|
|
293
295
|
Object.entries(schemaStructure).forEach(([field, fieldDef]) => {
|
|
294
296
|
if (fieldDef.belongsTo && fieldDef.hidden !== true) {
|
|
295
|
-
fieldsToSelect.add(field)
|
|
297
|
+
fieldsToSelect.add(field)
|
|
296
298
|
}
|
|
297
|
-
})
|
|
298
|
-
|
|
299
|
+
})
|
|
300
|
+
|
|
299
301
|
// Always include polymorphic type and id fields from relationships
|
|
300
302
|
try {
|
|
301
|
-
const relationships = scope.vars.schemaInfo.schemaRelationships
|
|
303
|
+
const relationships = scope.vars.schemaInfo.schemaRelationships
|
|
302
304
|
Object.entries(relationships || {}).forEach(([relName, relDef]) => {
|
|
303
305
|
if (relDef.belongsToPolymorphic) {
|
|
304
|
-
if (relDef.typeField) fieldsToSelect.add(relDef.typeField)
|
|
305
|
-
if (relDef.idField) fieldsToSelect.add(relDef.idField)
|
|
306
|
+
if (relDef.typeField) fieldsToSelect.add(relDef.typeField)
|
|
307
|
+
if (relDef.idField) fieldsToSelect.add(relDef.idField)
|
|
306
308
|
}
|
|
307
|
-
})
|
|
309
|
+
})
|
|
308
310
|
} catch (e) {
|
|
309
311
|
// Scope might not have relationships
|
|
310
312
|
}
|
|
311
|
-
|
|
313
|
+
|
|
312
314
|
// Return detailed information about field selection
|
|
313
315
|
// This info is used by:
|
|
314
316
|
// 1. SQL query builder to SELECT the right columns
|
|
@@ -319,17 +321,17 @@ export const buildFieldSelection = async (scope, deps) => {
|
|
|
319
321
|
requestedFields: requested, // Fields explicitly requested by user
|
|
320
322
|
computedDependencies: Array.from(computedDependencies), // Dependencies to remove from response
|
|
321
323
|
idProperty // Pass idProperty for reference
|
|
322
|
-
}
|
|
323
|
-
}
|
|
324
|
+
}
|
|
325
|
+
}
|
|
324
326
|
|
|
325
327
|
/**
|
|
326
328
|
* Determines which computed fields to calculate based on sparse fieldsets
|
|
327
|
-
*
|
|
329
|
+
*
|
|
328
330
|
* @param {string} scopeName - Resource name (e.g., 'products')
|
|
329
331
|
* @param {Array<string>|string} requestedFields - Requested fields from query
|
|
330
332
|
* @param {Object} computedFields - Computed field definitions from schema
|
|
331
333
|
* @returns {Array<string>} Names of computed fields to calculate
|
|
332
|
-
*
|
|
334
|
+
*
|
|
333
335
|
* @example
|
|
334
336
|
* // Input: No sparse fieldsets (calculate all computed fields)
|
|
335
337
|
* const computed = {
|
|
@@ -338,7 +340,7 @@ export const buildFieldSelection = async (scope, deps) => {
|
|
|
338
340
|
* };
|
|
339
341
|
* getRequestedComputedFields('users', null, computed);
|
|
340
342
|
* // Output: ['full_name', 'age'] (all computed fields)
|
|
341
|
-
*
|
|
343
|
+
*
|
|
342
344
|
* @example
|
|
343
345
|
* // Input: Sparse fieldsets with some computed fields
|
|
344
346
|
* const requestedFields = 'first_name,full_name,email';
|
|
@@ -348,7 +350,7 @@ export const buildFieldSelection = async (scope, deps) => {
|
|
|
348
350
|
* };
|
|
349
351
|
* getRequestedComputedFields('users', requestedFields, computed);
|
|
350
352
|
* // Output: ['full_name'] (only requested computed field)
|
|
351
|
-
*
|
|
353
|
+
*
|
|
352
354
|
* @example
|
|
353
355
|
* // Input: Sparse fieldsets with no computed fields
|
|
354
356
|
* const requestedFields = ['first_name', 'email'];
|
|
@@ -357,17 +359,17 @@ export const buildFieldSelection = async (scope, deps) => {
|
|
|
357
359
|
* };
|
|
358
360
|
* getRequestedComputedFields('users', requestedFields, computed);
|
|
359
361
|
* // Output: [] (no computed fields requested)
|
|
360
|
-
*
|
|
362
|
+
*
|
|
361
363
|
* @description
|
|
362
364
|
* Used by:
|
|
363
365
|
* - enrichAttributes to determine which computations to run
|
|
364
366
|
* - Applied after fetching data from database
|
|
365
|
-
*
|
|
367
|
+
*
|
|
366
368
|
* Purpose:
|
|
367
369
|
* - Optimizes performance by only computing requested fields
|
|
368
370
|
* - Supports JSON:API sparse fieldsets for computed fields
|
|
369
371
|
* - Handles both string and array input formats
|
|
370
|
-
*
|
|
372
|
+
*
|
|
371
373
|
* Data flow:
|
|
372
374
|
* 1. Checks if sparse fieldsets are specified
|
|
373
375
|
* 2. If not, returns all computed field names
|
|
@@ -375,20 +377,20 @@ export const buildFieldSelection = async (scope, deps) => {
|
|
|
375
377
|
* 4. Normalizes string input to array format
|
|
376
378
|
*/
|
|
377
379
|
export const getRequestedComputedFields = (scopeName, requestedFields, computedFields) => {
|
|
378
|
-
if (!computedFields) return []
|
|
379
|
-
|
|
380
|
-
const allComputedFields = Object.keys(computedFields)
|
|
381
|
-
|
|
380
|
+
if (!computedFields) return []
|
|
381
|
+
|
|
382
|
+
const allComputedFields = Object.keys(computedFields)
|
|
383
|
+
|
|
382
384
|
if (!requestedFields || requestedFields.length === 0) {
|
|
383
385
|
// No sparse fieldsets - return all computed fields
|
|
384
|
-
return allComputedFields
|
|
386
|
+
return allComputedFields
|
|
385
387
|
}
|
|
386
|
-
|
|
388
|
+
|
|
387
389
|
// Parse requested fields if it's a string
|
|
388
390
|
const requested = typeof requestedFields === 'string'
|
|
389
391
|
? requestedFields.split(',').map(f => f.trim()).filter(f => f)
|
|
390
|
-
: requestedFields
|
|
391
|
-
|
|
392
|
+
: requestedFields
|
|
393
|
+
|
|
392
394
|
// Return only requested computed fields that exist
|
|
393
|
-
return requested.filter(field => allComputedFields.includes(field))
|
|
394
|
-
}
|
|
395
|
+
return requested.filter(field => allComputedFields.includes(field))
|
|
396
|
+
}
|