json-rest-api 1.0.13 → 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 +13 -0
- 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 +9 -5
- 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,9 +1,9 @@
|
|
|
1
|
-
import { RestApiResourceError } from '../../../../lib/rest-api-errors.js'
|
|
2
|
-
import { ROW_NUMBER_KEY, DEFAULT_QUERY_LIMIT, DEFAULT_MAX_QUERY_LIMIT, DEFAULT_MAX_INCLUDE_LIMIT } from '../querying-writing/knex-constants.js'
|
|
1
|
+
import { RestApiResourceError } from '../../../../lib/rest-api-errors.js'
|
|
2
|
+
import { ROW_NUMBER_KEY, DEFAULT_QUERY_LIMIT, DEFAULT_MAX_QUERY_LIMIT, DEFAULT_MAX_INCLUDE_LIMIT } from '../querying-writing/knex-constants.js'
|
|
3
3
|
|
|
4
4
|
/**
|
|
5
5
|
* Builds a window function query for limited includes per parent record
|
|
6
|
-
*
|
|
6
|
+
*
|
|
7
7
|
* @param {Object} knex - Knex instance
|
|
8
8
|
* @param {string} tableName - Target table name
|
|
9
9
|
* @param {string} foreignKey - Foreign key field
|
|
@@ -13,7 +13,7 @@ import { ROW_NUMBER_KEY, DEFAULT_QUERY_LIMIT, DEFAULT_MAX_QUERY_LIMIT, DEFAULT_M
|
|
|
13
13
|
* @param {Object} capabilities - Database capabilities
|
|
14
14
|
* @param {Object} scopeVars - Scope variables for defaults and limits
|
|
15
15
|
* @returns {Object} Knex query
|
|
16
|
-
*
|
|
16
|
+
*
|
|
17
17
|
* @example
|
|
18
18
|
* // Input: Load max 3 comments per article, ordered by newest first
|
|
19
19
|
* const query = buildWindowedIncludeQuery(
|
|
@@ -26,22 +26,22 @@ import { ROW_NUMBER_KEY, DEFAULT_QUERY_LIMIT, DEFAULT_MAX_QUERY_LIMIT, DEFAULT_M
|
|
|
26
26
|
* { windowFunctions: true },
|
|
27
27
|
* { queryDefaultLimit: 10 }
|
|
28
28
|
* );
|
|
29
|
-
*
|
|
29
|
+
*
|
|
30
30
|
* // Generated SQL:
|
|
31
31
|
* // WITH _windowed AS (
|
|
32
32
|
* // SELECT id, text, created_at, article_id,
|
|
33
33
|
* // ROW_NUMBER() OVER (
|
|
34
|
-
* // PARTITION BY article_id
|
|
34
|
+
* // PARTITION BY article_id
|
|
35
35
|
* // ORDER BY created_at DESC
|
|
36
36
|
* // ) as __$jsonrestapi_rn$__
|
|
37
37
|
* // FROM comments
|
|
38
38
|
* // WHERE article_id IN (1, 2, 3)
|
|
39
39
|
* // )
|
|
40
|
-
* // SELECT * FROM _windowed
|
|
40
|
+
* // SELECT * FROM _windowed
|
|
41
41
|
* // WHERE __$jsonrestapi_rn$__ <= 3
|
|
42
|
-
*
|
|
42
|
+
*
|
|
43
43
|
* // Result: Each article gets max 3 comments, not 3 total
|
|
44
|
-
*
|
|
44
|
+
*
|
|
45
45
|
* @example
|
|
46
46
|
* // Input: Database doesn't support window functions
|
|
47
47
|
* const query = buildWindowedIncludeQuery(
|
|
@@ -53,23 +53,23 @@ import { ROW_NUMBER_KEY, DEFAULT_QUERY_LIMIT, DEFAULT_MAX_QUERY_LIMIT, DEFAULT_M
|
|
|
53
53
|
* { limit: 5 },
|
|
54
54
|
* { windowFunctions: false, dbInfo: { client: 'mysql', version: '5.7' } }
|
|
55
55
|
* );
|
|
56
|
-
*
|
|
56
|
+
*
|
|
57
57
|
* // Throws RestApiResourceError:
|
|
58
|
-
* // "Include limits require window function support. Your database (mysql 5.7)
|
|
59
|
-
* // does not support this feature. Window functions are supported in:
|
|
58
|
+
* // "Include limits require window function support. Your database (mysql 5.7)
|
|
59
|
+
* // does not support this feature. Window functions are supported in:
|
|
60
60
|
* // PostgreSQL 8.4+, MySQL 8.0+, MariaDB 10.2+, SQLite 3.25+, SQL Server 2005+"
|
|
61
|
-
*
|
|
61
|
+
*
|
|
62
62
|
* @description
|
|
63
63
|
* Used by:
|
|
64
64
|
* - loadHasMany when strategy: 'window' and database supports window functions
|
|
65
65
|
* - Enables per-parent limits for one-to-many relationships
|
|
66
|
-
*
|
|
66
|
+
*
|
|
67
67
|
* Purpose:
|
|
68
68
|
* - Solves the "N+1 limit" problem where you want X records per parent
|
|
69
69
|
* - Without window functions, LIMIT 10 gives 10 total across all parents
|
|
70
70
|
* - With window functions, each parent gets up to 10 related records
|
|
71
71
|
* - Critical for consistent API responses with includes
|
|
72
|
-
*
|
|
72
|
+
*
|
|
73
73
|
* Data flow:
|
|
74
74
|
* 1. Creates subquery with ROW_NUMBER() partitioned by foreign key
|
|
75
75
|
* 2. Numbers rows within each partition based on orderBy
|
|
@@ -87,104 +87,104 @@ export const buildWindowedIncludeQuery = (
|
|
|
87
87
|
capabilities,
|
|
88
88
|
scopeVars = {}
|
|
89
89
|
) => {
|
|
90
|
-
const { orderBy = [] } = includeConfig
|
|
91
|
-
|
|
90
|
+
const { orderBy = [] } = includeConfig
|
|
91
|
+
|
|
92
92
|
// Apply defaults for limit
|
|
93
|
-
const effectiveLimit = includeConfig.limit ?? scopeVars.queryDefaultLimit ?? DEFAULT_QUERY_LIMIT
|
|
94
|
-
|
|
93
|
+
const effectiveLimit = includeConfig.limit ?? scopeVars.queryDefaultLimit ?? DEFAULT_QUERY_LIMIT
|
|
94
|
+
|
|
95
95
|
// Validate against max
|
|
96
96
|
if (scopeVars.queryMaxLimit && effectiveLimit > scopeVars.queryMaxLimit) {
|
|
97
97
|
throw new RestApiResourceError({
|
|
98
98
|
title: 'Include Limit Exceeds Maximum',
|
|
99
99
|
detail: `Requested include limit (${effectiveLimit}) exceeds queryMaxLimit (${scopeVars.queryMaxLimit})`,
|
|
100
100
|
status: 400
|
|
101
|
-
})
|
|
101
|
+
})
|
|
102
102
|
}
|
|
103
|
-
|
|
103
|
+
|
|
104
104
|
// Check if window functions are supported
|
|
105
105
|
if (!capabilities.windowFunctions) {
|
|
106
|
-
const { dbInfo } = capabilities
|
|
106
|
+
const { dbInfo } = capabilities
|
|
107
107
|
throw new RestApiResourceError(
|
|
108
108
|
`Include limits require window function support. Your database (${dbInfo.client} ${dbInfo.version}) does not support this feature. ` +
|
|
109
|
-
|
|
109
|
+
'Window functions are supported in: PostgreSQL 8.4+, MySQL 8.0+, MariaDB 10.2+, SQLite 3.25+, SQL Server 2005+',
|
|
110
110
|
{
|
|
111
111
|
subtype: 'unsupported_operation',
|
|
112
112
|
database: dbInfo.client,
|
|
113
113
|
version: dbInfo.version,
|
|
114
114
|
requiredFeature: 'window_functions'
|
|
115
115
|
}
|
|
116
|
-
)
|
|
116
|
+
)
|
|
117
117
|
}
|
|
118
|
-
|
|
118
|
+
|
|
119
119
|
// Build the window function query
|
|
120
120
|
// This creates a subquery that partitions by the foreign key and numbers rows
|
|
121
121
|
const subquery = knex(tableName)
|
|
122
122
|
.select('*')
|
|
123
123
|
.select(
|
|
124
124
|
knex.raw(
|
|
125
|
-
'ROW_NUMBER() OVER (PARTITION BY ?? ORDER BY ' +
|
|
126
|
-
buildOrderByClause(orderBy) +
|
|
125
|
+
'ROW_NUMBER() OVER (PARTITION BY ?? ORDER BY ' +
|
|
126
|
+
buildOrderByClause(orderBy) +
|
|
127
127
|
') as ' + ROW_NUMBER_KEY,
|
|
128
128
|
[foreignKey]
|
|
129
129
|
)
|
|
130
130
|
)
|
|
131
|
-
.whereIn(foreignKey, parentIds)
|
|
132
|
-
|
|
131
|
+
.whereIn(foreignKey, parentIds)
|
|
132
|
+
|
|
133
133
|
// Apply field selection if specified
|
|
134
134
|
if (fieldsToSelect !== '*' && Array.isArray(fieldsToSelect)) {
|
|
135
135
|
// Include the foreign key and row number in selection
|
|
136
|
-
const fieldsWithFK = [...new Set([...fieldsToSelect, foreignKey, ROW_NUMBER_KEY])]
|
|
137
|
-
subquery.select(fieldsWithFK)
|
|
136
|
+
const fieldsWithFK = [...new Set([...fieldsToSelect, foreignKey, ROW_NUMBER_KEY])]
|
|
137
|
+
subquery.select(fieldsWithFK)
|
|
138
138
|
}
|
|
139
|
-
|
|
139
|
+
|
|
140
140
|
// Wrap in outer query to filter by row number
|
|
141
141
|
const query = knex
|
|
142
142
|
.select('*')
|
|
143
143
|
.from(subquery.as('_windowed'))
|
|
144
|
-
.where(ROW_NUMBER_KEY, '<=', effectiveLimit)
|
|
145
|
-
|
|
146
|
-
return query
|
|
147
|
-
}
|
|
144
|
+
.where(ROW_NUMBER_KEY, '<=', effectiveLimit)
|
|
145
|
+
|
|
146
|
+
return query
|
|
147
|
+
}
|
|
148
148
|
|
|
149
149
|
/**
|
|
150
150
|
* Builds ORDER BY clause from array of sort fields
|
|
151
|
-
*
|
|
151
|
+
*
|
|
152
152
|
* @param {Array<string>} orderBy - Array of field names, prefix with '-' for DESC
|
|
153
153
|
* @param {string} tablePrefix - Optional table name to prefix fields
|
|
154
154
|
* @returns {string} SQL ORDER BY clause
|
|
155
|
-
*
|
|
155
|
+
*
|
|
156
156
|
* @example
|
|
157
157
|
* // Input: Simple ascending sort
|
|
158
158
|
* const clause = buildOrderByClause(['name', 'created_at']);
|
|
159
159
|
* // Output: "name ASC, created_at ASC"
|
|
160
|
-
*
|
|
160
|
+
*
|
|
161
161
|
* @example
|
|
162
162
|
* // Input: Mixed ascending/descending with '-' prefix
|
|
163
163
|
* const clause = buildOrderByClause(['name', '-created_at', 'status']);
|
|
164
164
|
* // Output: "name ASC, created_at DESC, status ASC"
|
|
165
|
-
*
|
|
165
|
+
*
|
|
166
166
|
* @example
|
|
167
167
|
* // Input: With table prefix for joins
|
|
168
168
|
* const clause = buildOrderByClause(['-updated_at', 'title'], 'articles');
|
|
169
169
|
* // Output: "articles.updated_at DESC, articles.title ASC"
|
|
170
|
-
*
|
|
170
|
+
*
|
|
171
171
|
* @example
|
|
172
172
|
* // Input: Empty array (default to id)
|
|
173
173
|
* const clause = buildOrderByClause([]);
|
|
174
174
|
* // Output: "id ASC"
|
|
175
|
-
*
|
|
175
|
+
*
|
|
176
176
|
* @description
|
|
177
177
|
* Used by:
|
|
178
178
|
* - buildWindowedIncludeQuery for PARTITION BY ordering
|
|
179
179
|
* - applyStandardIncludeConfig for regular ORDER BY
|
|
180
180
|
* - Query builders that need consistent sort syntax
|
|
181
|
-
*
|
|
181
|
+
*
|
|
182
182
|
* Purpose:
|
|
183
183
|
* - Converts API sort syntax (with '-' prefix) to SQL ORDER BY
|
|
184
184
|
* - Handles table prefixing for queries with joins
|
|
185
185
|
* - Provides default ordering by id when none specified
|
|
186
186
|
* - Ensures consistent sort behavior across the API
|
|
187
|
-
*
|
|
187
|
+
*
|
|
188
188
|
* Data flow:
|
|
189
189
|
* 1. Receives array of sort fields from API parameters
|
|
190
190
|
* 2. Detects DESC sorts by '-' prefix
|
|
@@ -193,27 +193,27 @@ export const buildWindowedIncludeQuery = (
|
|
|
193
193
|
*/
|
|
194
194
|
export const buildOrderByClause = (orderBy, tablePrefix) => {
|
|
195
195
|
if (!orderBy || orderBy.length === 0) {
|
|
196
|
-
const defaultField = tablePrefix ? `${tablePrefix}.id` : 'id'
|
|
197
|
-
return `${defaultField} ASC
|
|
196
|
+
const defaultField = tablePrefix ? `${tablePrefix}.id` : 'id'
|
|
197
|
+
return `${defaultField} ASC` // Default ordering
|
|
198
198
|
}
|
|
199
|
-
|
|
199
|
+
|
|
200
200
|
return orderBy.map(field => {
|
|
201
|
-
const isDesc = field.startsWith('-')
|
|
202
|
-
const fieldName = isDesc ? field.substring(1) : field
|
|
203
|
-
const qualifiedField = tablePrefix ? `${tablePrefix}.${fieldName}` : fieldName
|
|
204
|
-
return `${qualifiedField} ${isDesc ? 'DESC' : 'ASC'}
|
|
205
|
-
}).join(', ')
|
|
206
|
-
}
|
|
201
|
+
const isDesc = field.startsWith('-')
|
|
202
|
+
const fieldName = isDesc ? field.substring(1) : field
|
|
203
|
+
const qualifiedField = tablePrefix ? `${tablePrefix}.${fieldName}` : fieldName
|
|
204
|
+
return `${qualifiedField} ${isDesc ? 'DESC' : 'ASC'}`
|
|
205
|
+
}).join(', ')
|
|
206
|
+
}
|
|
207
207
|
|
|
208
208
|
/**
|
|
209
209
|
* Applies standard (non-windowed) include configuration to a query
|
|
210
|
-
*
|
|
210
|
+
*
|
|
211
211
|
* @param {Object} query - Knex query builder instance
|
|
212
212
|
* @param {Object} includeConfig - Include configuration object
|
|
213
213
|
* @param {Object} scopeVars - Scope variables with defaults and limits
|
|
214
214
|
* @param {Object} log - Logger instance
|
|
215
215
|
* @returns {Object} Modified query builder
|
|
216
|
-
*
|
|
216
|
+
*
|
|
217
217
|
* @example
|
|
218
218
|
* // Input: Basic include with limit and ordering
|
|
219
219
|
* let query = knex('comments').whereIn('article_id', [1, 2, 3]);
|
|
@@ -223,11 +223,11 @@ export const buildOrderByClause = (orderBy, tablePrefix) => {
|
|
|
223
223
|
* { queryDefaultLimit: 100, queryMaxLimit: 1000 },
|
|
224
224
|
* logger
|
|
225
225
|
* );
|
|
226
|
-
*
|
|
226
|
+
*
|
|
227
227
|
* // Result: Query modified with:
|
|
228
228
|
* // ORDER BY created_at DESC, id ASC
|
|
229
229
|
* // LIMIT 20
|
|
230
|
-
*
|
|
230
|
+
*
|
|
231
231
|
* @example
|
|
232
232
|
* // Input: No explicit limit, uses defaults
|
|
233
233
|
* let query = knex('tags');
|
|
@@ -237,12 +237,12 @@ export const buildOrderByClause = (orderBy, tablePrefix) => {
|
|
|
237
237
|
* { queryDefaultLimit: 50, queryMaxLimit: 500 },
|
|
238
238
|
* logger
|
|
239
239
|
* );
|
|
240
|
-
*
|
|
240
|
+
*
|
|
241
241
|
* // Result: Uses default limit
|
|
242
242
|
* // ORDER BY name ASC
|
|
243
243
|
* // LIMIT 50
|
|
244
244
|
* // Log: "Using default limit"
|
|
245
|
-
*
|
|
245
|
+
*
|
|
246
246
|
* @example
|
|
247
247
|
* // Input: Limit exceeds maximum
|
|
248
248
|
* let query = knex('reviews');
|
|
@@ -250,12 +250,12 @@ export const buildOrderByClause = (orderBy, tablePrefix) => {
|
|
|
250
250
|
* query,
|
|
251
251
|
* { limit: 5000 }, // Exceeds max
|
|
252
252
|
* { queryMaxLimit: 1000, maxIncludeLimit: 500 },
|
|
253
|
-
* logger
|
|
253
|
+
* logger
|
|
254
254
|
* );
|
|
255
|
-
*
|
|
255
|
+
*
|
|
256
256
|
* // Result: Clamped to effective maximum
|
|
257
257
|
* // LIMIT 500 (min of maxIncludeLimit and queryMaxLimit)
|
|
258
|
-
*
|
|
258
|
+
*
|
|
259
259
|
* @example
|
|
260
260
|
* // Input: Explicitly disable limit
|
|
261
261
|
* let query = knex('categories');
|
|
@@ -265,23 +265,23 @@ export const buildOrderByClause = (orderBy, tablePrefix) => {
|
|
|
265
265
|
* { queryDefaultLimit: 100 },
|
|
266
266
|
* logger
|
|
267
267
|
* );
|
|
268
|
-
*
|
|
268
|
+
*
|
|
269
269
|
* // Result: No LIMIT clause added
|
|
270
270
|
* // Log: "No limit applied to include query (explicitly disabled)"
|
|
271
|
-
*
|
|
271
|
+
*
|
|
272
272
|
* @description
|
|
273
273
|
* Used by:
|
|
274
274
|
* - loadHasMany when window functions not available or not requested
|
|
275
275
|
* - loadReversePolymorphic for standard relationship queries
|
|
276
276
|
* - Any include loader that needs consistent limit/order handling
|
|
277
|
-
*
|
|
277
|
+
*
|
|
278
278
|
* Purpose:
|
|
279
279
|
* - Provides fallback when window functions unavailable
|
|
280
280
|
* - Applies consistent ordering based on API sort syntax
|
|
281
281
|
* - Enforces limit hierarchy: explicit > default > max
|
|
282
282
|
* - Respects both queryMaxLimit and maxIncludeLimit
|
|
283
283
|
* - Logs decisions for debugging
|
|
284
|
-
*
|
|
284
|
+
*
|
|
285
285
|
* Data flow:
|
|
286
286
|
* 1. Applies ORDER BY for each field in orderBy array
|
|
287
287
|
* 2. Determines effective limit from explicit/default/max values
|
|
@@ -290,39 +290,39 @@ export const buildOrderByClause = (orderBy, tablePrefix) => {
|
|
|
290
290
|
* 5. Logs the reasoning for the applied limit
|
|
291
291
|
*/
|
|
292
292
|
export const applyStandardIncludeConfig = (query, includeConfig, scopeVars, log) => {
|
|
293
|
-
const { orderBy = [] } = includeConfig
|
|
294
|
-
|
|
293
|
+
const { orderBy = [] } = includeConfig
|
|
294
|
+
|
|
295
295
|
// Apply ordering
|
|
296
296
|
orderBy.forEach(field => {
|
|
297
|
-
const desc = field.startsWith('-')
|
|
298
|
-
const column = desc ? field.substring(1) : field
|
|
299
|
-
query = query.orderBy(column, desc ? 'desc' : 'asc')
|
|
300
|
-
})
|
|
301
|
-
|
|
297
|
+
const desc = field.startsWith('-')
|
|
298
|
+
const column = desc ? field.substring(1) : field
|
|
299
|
+
query = query.orderBy(column, desc ? 'desc' : 'asc')
|
|
300
|
+
})
|
|
301
|
+
|
|
302
302
|
// Apply limit with defaults
|
|
303
|
-
const requestedLimit = includeConfig.limit
|
|
304
|
-
const defaultLimit = scopeVars.queryDefaultLimit ?? DEFAULT_QUERY_LIMIT
|
|
305
|
-
const limit = requestedLimit ?? defaultLimit
|
|
306
|
-
|
|
303
|
+
const requestedLimit = includeConfig.limit
|
|
304
|
+
const defaultLimit = scopeVars.queryDefaultLimit ?? DEFAULT_QUERY_LIMIT
|
|
305
|
+
const limit = requestedLimit ?? defaultLimit
|
|
306
|
+
|
|
307
307
|
// Allow explicit null/false to mean no limit
|
|
308
308
|
if (limit !== null && limit !== false) {
|
|
309
|
-
const maxInclude = scopeVars.maxIncludeLimit || DEFAULT_MAX_INCLUDE_LIMIT
|
|
310
|
-
const maxQuery = scopeVars.queryMaxLimit || DEFAULT_MAX_QUERY_LIMIT
|
|
311
|
-
const effectiveMax = Math.min(maxInclude, maxQuery)
|
|
312
|
-
const effectiveLimit = Math.min(limit, effectiveMax)
|
|
313
|
-
|
|
314
|
-
query = query.limit(effectiveLimit)
|
|
315
|
-
|
|
309
|
+
const maxInclude = scopeVars.maxIncludeLimit || DEFAULT_MAX_INCLUDE_LIMIT
|
|
310
|
+
const maxQuery = scopeVars.queryMaxLimit || DEFAULT_MAX_QUERY_LIMIT
|
|
311
|
+
const effectiveMax = Math.min(maxInclude, maxQuery)
|
|
312
|
+
const effectiveLimit = Math.min(limit, effectiveMax)
|
|
313
|
+
|
|
314
|
+
query = query.limit(effectiveLimit)
|
|
315
|
+
|
|
316
316
|
log.debug('Applied include limit:', {
|
|
317
317
|
requested: requestedLimit,
|
|
318
318
|
default: defaultLimit,
|
|
319
319
|
effective: effectiveLimit,
|
|
320
320
|
maxAllowed: effectiveMax,
|
|
321
321
|
note: requestedLimit !== undefined ? 'Using explicit limit' : 'Using default limit'
|
|
322
|
-
})
|
|
322
|
+
})
|
|
323
323
|
} else {
|
|
324
|
-
log.debug('No limit applied to include query (explicitly disabled)')
|
|
324
|
+
log.debug('No limit applied to include query (explicitly disabled)')
|
|
325
325
|
}
|
|
326
|
-
|
|
327
|
-
return query
|
|
328
|
-
}
|
|
326
|
+
|
|
327
|
+
return query
|
|
328
|
+
}
|
|
@@ -1,80 +1,80 @@
|
|
|
1
1
|
/**
|
|
2
2
|
* URL Helper Functions
|
|
3
|
-
*
|
|
3
|
+
*
|
|
4
4
|
* Centralized functions for URL generation to avoid code duplication
|
|
5
5
|
* and support flexible URL prefix override capabilities.
|
|
6
6
|
*/
|
|
7
7
|
|
|
8
8
|
/**
|
|
9
9
|
* Get the complete URL prefix, handling all calculation and override logic
|
|
10
|
-
*
|
|
10
|
+
*
|
|
11
11
|
* Priority order:
|
|
12
12
|
* 1. context.urlPrefixOverride - Explicit override (from hooks or middleware)
|
|
13
13
|
* 2. context.urlPrefix - Pre-calculated URL prefix
|
|
14
14
|
* 3. Calculate from request if provided
|
|
15
15
|
* 4. scope.vars.transport?.mountPath - Fallback to mount path
|
|
16
16
|
* 5. '' - Empty string as final fallback
|
|
17
|
-
*
|
|
17
|
+
*
|
|
18
18
|
* @param {Object} context - Request context (may have urlPrefixOverride or urlPrefix)
|
|
19
19
|
* @param {Object} scope - Scope/resource object with transport vars
|
|
20
20
|
* @param {Object} req - Optional Express request object for auto-calculation
|
|
21
21
|
* @returns {string} The final URL prefix to use
|
|
22
|
-
*
|
|
22
|
+
*
|
|
23
23
|
* @example
|
|
24
24
|
* // With full context
|
|
25
25
|
* const urlPrefix = getUrlPrefix(context, scope);
|
|
26
|
-
*
|
|
26
|
+
*
|
|
27
27
|
* @example
|
|
28
28
|
* // With request object for auto-calculation
|
|
29
29
|
* const urlPrefix = getUrlPrefix({}, api, req);
|
|
30
30
|
*/
|
|
31
|
-
export function getUrlPrefix(context, scope, req = null) {
|
|
31
|
+
export function getUrlPrefix (context, scope, req = null) {
|
|
32
32
|
// Priority 1: Explicit override
|
|
33
33
|
if (context?.urlPrefixOverride) {
|
|
34
|
-
return context.urlPrefixOverride
|
|
34
|
+
return context.urlPrefixOverride
|
|
35
35
|
}
|
|
36
|
-
|
|
36
|
+
|
|
37
37
|
// Priority 2: Pre-calculated urlPrefix
|
|
38
38
|
if (context?.urlPrefix) {
|
|
39
|
-
return context.urlPrefix
|
|
39
|
+
return context.urlPrefix
|
|
40
40
|
}
|
|
41
|
-
|
|
41
|
+
|
|
42
42
|
// Priority 3: Calculate from request if provided
|
|
43
43
|
if (req) {
|
|
44
|
-
const protocol = req.get?.('x-forwarded-proto') || req.protocol || 'http'
|
|
45
|
-
const host = req.get?.('x-forwarded-host') || req.get?.('host')
|
|
46
|
-
const mountPath = scope?.vars?.transport?.mountPath || ''
|
|
47
|
-
|
|
44
|
+
const protocol = req.get?.('x-forwarded-proto') || req.protocol || 'http'
|
|
45
|
+
const host = req.get?.('x-forwarded-host') || req.get?.('host')
|
|
46
|
+
const mountPath = scope?.vars?.transport?.mountPath || ''
|
|
47
|
+
|
|
48
48
|
if (host) {
|
|
49
|
-
return `${protocol}://${host}${mountPath}
|
|
49
|
+
return `${protocol}://${host}${mountPath}`
|
|
50
50
|
}
|
|
51
51
|
}
|
|
52
|
-
|
|
52
|
+
|
|
53
53
|
// Priority 4: Fallback to mount path
|
|
54
|
-
return scope?.vars?.transport?.mountPath || ''
|
|
54
|
+
return scope?.vars?.transport?.mountPath || ''
|
|
55
55
|
}
|
|
56
56
|
|
|
57
57
|
/**
|
|
58
58
|
* Build a complete resource URL
|
|
59
|
-
*
|
|
59
|
+
*
|
|
60
60
|
* @param {Object} context - Request context
|
|
61
61
|
* @param {Object} scope - Scope containing transport vars
|
|
62
62
|
* @param {string} scopeName - Resource type name
|
|
63
63
|
* @param {string} id - Resource ID
|
|
64
64
|
* @returns {string} Complete resource URL
|
|
65
|
-
*
|
|
65
|
+
*
|
|
66
66
|
* @example
|
|
67
67
|
* const url = buildResourceUrl(context, scope, 'books', '123');
|
|
68
68
|
* // Returns: "https://api.example.com/api/books/123"
|
|
69
69
|
*/
|
|
70
|
-
export function buildResourceUrl(context, scope, scopeName, id) {
|
|
71
|
-
const urlPrefix = getUrlPrefix(context, scope)
|
|
72
|
-
return `${urlPrefix}/${scopeName}/${id}
|
|
70
|
+
export function buildResourceUrl (context, scope, scopeName, id) {
|
|
71
|
+
const urlPrefix = getUrlPrefix(context, scope)
|
|
72
|
+
return `${urlPrefix}/${scopeName}/${id}`
|
|
73
73
|
}
|
|
74
74
|
|
|
75
75
|
/**
|
|
76
76
|
* Build a relationship URL
|
|
77
|
-
*
|
|
77
|
+
*
|
|
78
78
|
* @param {Object} context - Request context
|
|
79
79
|
* @param {Object} scope - Scope containing transport vars
|
|
80
80
|
* @param {string} scopeName - Resource type name
|
|
@@ -82,38 +82,38 @@ export function buildResourceUrl(context, scope, scopeName, id) {
|
|
|
82
82
|
* @param {string} relationshipName - Name of the relationship
|
|
83
83
|
* @param {boolean} isRelationshipEndpoint - If true, builds /relationships/ URL
|
|
84
84
|
* @returns {string} Complete relationship URL
|
|
85
|
-
*
|
|
85
|
+
*
|
|
86
86
|
* @example
|
|
87
87
|
* // For relationship linkage endpoint
|
|
88
88
|
* buildRelationshipUrl(context, scope, 'books', '1', 'author', true);
|
|
89
89
|
* // Returns: "https://api.example.com/api/books/1/relationships/author"
|
|
90
|
-
*
|
|
90
|
+
*
|
|
91
91
|
* // For related resource endpoint
|
|
92
92
|
* buildRelationshipUrl(context, scope, 'books', '1', 'author', false);
|
|
93
93
|
* // Returns: "https://api.example.com/api/books/1/author"
|
|
94
94
|
*/
|
|
95
|
-
export function buildRelationshipUrl(context, scope, scopeName, id, relationshipName, isRelationshipEndpoint = false) {
|
|
96
|
-
const urlPrefix = getUrlPrefix(context, scope)
|
|
95
|
+
export function buildRelationshipUrl (context, scope, scopeName, id, relationshipName, isRelationshipEndpoint = false) {
|
|
96
|
+
const urlPrefix = getUrlPrefix(context, scope)
|
|
97
97
|
if (isRelationshipEndpoint) {
|
|
98
|
-
return `${urlPrefix}/${scopeName}/${id}/relationships/${relationshipName}
|
|
98
|
+
return `${urlPrefix}/${scopeName}/${id}/relationships/${relationshipName}`
|
|
99
99
|
}
|
|
100
|
-
return `${urlPrefix}/${scopeName}/${id}/${relationshipName}
|
|
100
|
+
return `${urlPrefix}/${scopeName}/${id}/${relationshipName}`
|
|
101
101
|
}
|
|
102
102
|
|
|
103
103
|
/**
|
|
104
104
|
* Build a collection URL with optional query string
|
|
105
|
-
*
|
|
105
|
+
*
|
|
106
106
|
* @param {Object} context - Request context
|
|
107
107
|
* @param {Object} scope - Scope containing transport vars
|
|
108
108
|
* @param {string} scopeName - Resource type name
|
|
109
109
|
* @param {string} queryString - Optional query string (with leading ?)
|
|
110
110
|
* @returns {string} Complete collection URL
|
|
111
|
-
*
|
|
111
|
+
*
|
|
112
112
|
* @example
|
|
113
113
|
* buildCollectionUrl(context, scope, 'books', '?page[size]=10');
|
|
114
114
|
* // Returns: "https://api.example.com/api/books?page[size]=10"
|
|
115
115
|
*/
|
|
116
|
-
export function buildCollectionUrl(context, scope, scopeName, queryString = '') {
|
|
117
|
-
const urlPrefix = getUrlPrefix(context, scope)
|
|
118
|
-
return `${urlPrefix}/${scopeName}${queryString}
|
|
119
|
-
}
|
|
116
|
+
export function buildCollectionUrl (context, scope, scopeName, queryString = '') {
|
|
117
|
+
const urlPrefix = getUrlPrefix(context, scope)
|
|
118
|
+
return `${urlPrefix}/${scopeName}${queryString}`
|
|
119
|
+
}
|