json-rest-api 1.0.20 → 1.0.22
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/audit/00-charter.md +103 -0
- package/audit/01-scope-map.md +84 -0
- package/audit/02-coverage-matrix.csv +174 -0
- package/audit/03-invariants.md +123 -0
- package/audit/04-standards-rubric.md +56 -0
- package/audit/05-competitor-rubric.md +77 -0
- package/audit/06-findings.md +160 -0
- package/audit/07-risk-register.csv +10 -0
- package/audit/08-remediation-roadmap.md +177 -0
- package/audit/09-signoff-check.md +23 -0
- package/audit/10-final-synthesis.md +29 -0
- package/audit/evidence/code/all-files-json-rest-api.txt +168 -0
- package/audit/evidence/code/all-files-json-rest-schema.txt +12 -0
- package/audit/evidence/code/boundary-leak-checks.txt +72 -0
- package/audit/evidence/code/critical-pass1/json-rest-api__plugins__core__bulk-operations-plugin.js.txt +480 -0
- package/audit/evidence/code/critical-pass1/json-rest-api__plugins__core__connectors__express-plugin.js.txt +532 -0
- package/audit/evidence/code/critical-pass1/json-rest-api__plugins__core__connectors__lib__busboy-detector.js.txt +175 -0
- package/audit/evidence/code/critical-pass1/json-rest-api__plugins__core__connectors__lib__formidable-detector.js.txt +138 -0
- package/audit/evidence/code/critical-pass1/json-rest-api__plugins__core__connectors__lib__request-helpers.js.txt +222 -0
- package/audit/evidence/code/critical-pass1/json-rest-api__plugins__core__file-handling-plugin.js.txt +342 -0
- package/audit/evidence/code/critical-pass1/json-rest-api__plugins__core__lib__dbTablesOperations.js.txt +304 -0
- package/audit/evidence/code/critical-pass1/json-rest-api__plugins__core__lib__fractional-positioning.js.txt +188 -0
- package/audit/evidence/code/critical-pass1/json-rest-api__plugins__core__lib__querying-writing__compile-schemas.js.txt +336 -0
- package/audit/evidence/code/critical-pass1/json-rest-api__plugins__core__lib__querying-writing__connectors-query-parser.js.txt +165 -0
- package/audit/evidence/code/critical-pass1/json-rest-api__plugins__core__lib__querying-writing__database-capabilities.js.txt +265 -0
- package/audit/evidence/code/critical-pass1/json-rest-api__plugins__core__lib__querying-writing__database-value-normalizers.js.txt +369 -0
- package/audit/evidence/code/critical-pass1/json-rest-api__plugins__core__lib__querying-writing__default-data-helpers.js.txt +72 -0
- package/audit/evidence/code/critical-pass1/json-rest-api__plugins__core__lib__querying-writing__field-utils.js.txt +139 -0
- package/audit/evidence/code/critical-pass1/json-rest-api__plugins__core__lib__querying-writing__knex-constants.js.txt +45 -0
- package/audit/evidence/code/critical-pass1/json-rest-api__plugins__core__lib__querying-writing__knex-field-helpers.js.txt +396 -0
- package/audit/evidence/code/critical-pass1/json-rest-api__plugins__core__lib__querying-writing__knex-json-api-transformers.js.txt +162 -0
- package/audit/evidence/code/critical-pass1/json-rest-api__plugins__core__lib__querying-writing__payload-validators.js.txt +1314 -0
- package/audit/evidence/code/critical-pass1/json-rest-api__plugins__core__lib__querying-writing__schema-helpers.js.txt +488 -0
- package/audit/evidence/code/critical-pass1/json-rest-api__plugins__core__lib__querying-writing__scope-validations.js.txt +484 -0
- package/audit/evidence/code/critical-pass1/json-rest-api__plugins__core__lib__querying-writing__simplified-helpers.js.txt +857 -0
- package/audit/evidence/code/critical-pass1/json-rest-api__plugins__core__lib__querying__knex-cross-table-search.js.txt +565 -0
- package/audit/evidence/code/critical-pass1/json-rest-api__plugins__core__lib__querying__knex-json-api-transformers-querying.js.txt +485 -0
- package/audit/evidence/code/critical-pass1/json-rest-api__plugins__core__lib__querying__knex-pagination-helpers.js.txt +560 -0
- package/audit/evidence/code/critical-pass1/json-rest-api__plugins__core__lib__querying__knex-process-includes.js.txt +167 -0
- package/audit/evidence/code/critical-pass1/json-rest-api__plugins__core__lib__querying__knex-query-helpers-base.js.txt +109 -0
- package/audit/evidence/code/critical-pass1/json-rest-api__plugins__core__lib__querying__knex-query-helpers.js.txt +1064 -0
- package/audit/evidence/code/critical-pass1/json-rest-api__plugins__core__lib__querying__knex-relationship-includes.js.txt +1983 -0
- package/audit/evidence/code/critical-pass1/json-rest-api__plugins__core__lib__querying__knex-window-queries.js.txt +328 -0
- package/audit/evidence/code/critical-pass1/json-rest-api__plugins__core__lib__querying__url-helpers.js.txt +119 -0
- package/audit/evidence/code/critical-pass1/json-rest-api__plugins__core__lib__storage__storage-adapter.js.txt +160 -0
- package/audit/evidence/code/critical-pass1/json-rest-api__plugins__core__lib__writing__many-to-many-manipulations.js.txt +300 -0
- package/audit/evidence/code/critical-pass1/json-rest-api__plugins__core__lib__writing__relationship-processor.js.txt +245 -0
- package/audit/evidence/code/critical-pass1/json-rest-api__plugins__core__rest-api-access.js.txt +459 -0
- package/audit/evidence/code/critical-pass1/json-rest-api__plugins__core__rest-api-cors-plugin.js.txt +237 -0
- package/audit/evidence/code/critical-pass1/json-rest-api__plugins__core__rest-api-knex-plugin.js.txt +1135 -0
- package/audit/evidence/code/critical-pass1/json-rest-api__plugins__core__rest-api-multihome-plugin.js.txt +314 -0
- package/audit/evidence/code/critical-pass1/json-rest-api__plugins__core__rest-api-plugin-hooks__compile-resource-schemas.js.txt +7 -0
- package/audit/evidence/code/critical-pass1/json-rest-api__plugins__core__rest-api-plugin-hooks__register-relationship-routes.js.txt +71 -0
- package/audit/evidence/code/critical-pass1/json-rest-api__plugins__core__rest-api-plugin-hooks__register-scope-routes.js.txt +102 -0
- package/audit/evidence/code/critical-pass1/json-rest-api__plugins__core__rest-api-plugin-hooks__turn-scope-init-into-vars.js.txt +49 -0
- package/audit/evidence/code/critical-pass1/json-rest-api__plugins__core__rest-api-plugin-hooks__validate-include-configurations.js.txt +39 -0
- package/audit/evidence/code/critical-pass1/json-rest-api__plugins__core__rest-api-plugin-methods__add-route.js.txt +23 -0
- package/audit/evidence/code/critical-pass1/json-rest-api__plugins__core__rest-api-plugin-methods__check-permissions.js.txt +13 -0
- package/audit/evidence/code/critical-pass1/json-rest-api__plugins__core__rest-api-plugin-methods__common.js.txt +639 -0
- package/audit/evidence/code/critical-pass1/json-rest-api__plugins__core__rest-api-plugin-methods__delete-relationship.js.txt +117 -0
- package/audit/evidence/code/critical-pass1/json-rest-api__plugins__core__rest-api-plugin-methods__delete.js.txt +104 -0
- package/audit/evidence/code/critical-pass1/json-rest-api__plugins__core__rest-api-plugin-methods__enrich-attributes.js.txt +199 -0
- package/audit/evidence/code/critical-pass1/json-rest-api__plugins__core__rest-api-plugin-methods__get-related.js.txt +353 -0
- package/audit/evidence/code/critical-pass1/json-rest-api__plugins__core__rest-api-plugin-methods__get-relationship.js.txt +61 -0
- package/audit/evidence/code/critical-pass1/json-rest-api__plugins__core__rest-api-plugin-methods__get.js.txt +184 -0
- package/audit/evidence/code/critical-pass1/json-rest-api__plugins__core__rest-api-plugin-methods__patch-relationship.js.txt +55 -0
- package/audit/evidence/code/critical-pass1/json-rest-api__plugins__core__rest-api-plugin-methods__patch.js.txt +191 -0
- package/audit/evidence/code/critical-pass1/json-rest-api__plugins__core__rest-api-plugin-methods__post-relationship.js.txt +110 -0
- package/audit/evidence/code/critical-pass1/json-rest-api__plugins__core__rest-api-plugin-methods__post.js.txt +168 -0
- package/audit/evidence/code/critical-pass1/json-rest-api__plugins__core__rest-api-plugin-methods__put.js.txt +253 -0
- package/audit/evidence/code/critical-pass1/json-rest-api__plugins__core__rest-api-plugin-methods__query.js.txt +207 -0
- package/audit/evidence/code/critical-pass1/json-rest-api__plugins__core__rest-api-plugin-methods__release.js.txt +12 -0
- package/audit/evidence/code/critical-pass1/json-rest-api__plugins__core__rest-api-plugin.js.txt +165 -0
- package/audit/evidence/code/critical-pass1/json-rest-api__plugins__core__rest-api-positioning-plugin.js.txt +668 -0
- package/audit/evidence/code/critical-pass1/json-rest-api__plugins__core__socketio-plugin.js.txt +621 -0
- package/audit/evidence/code/critical-pass1/json-rest-api__plugins__storage__local-storage.js.txt +218 -0
- package/audit/evidence/code/critical-pass1/json-rest-api__plugins__storage__s3-storage.js.txt +124 -0
- package/audit/evidence/code/critical-pass1/json-rest-schema__src__CorePlugin.js.txt +269 -0
- package/audit/evidence/code/critical-pass1/json-rest-schema__src__Schema.js.txt +242 -0
- package/audit/evidence/code/critical-pass1/json-rest-schema__src__index.js.txt +77 -0
- package/audit/evidence/code/critical-pass1-risk-keywords.txt +238 -0
- package/audit/evidence/code/critical-pass1-summary.csv +67 -0
- package/audit/evidence/code/critical-pass2-summary.csv +67 -0
- package/audit/evidence/code/early-silent-zones-no-test-token.txt +57 -0
- package/audit/evidence/code/error-model-signals.txt +98 -0
- package/audit/evidence/code/git-files-json-rest-api.txt +171 -0
- package/audit/evidence/code/git-files-json-rest-schema.txt +27 -0
- package/audit/evidence/code/hook-flow-signals.txt +282 -0
- package/audit/evidence/code/idproperty-signals.txt +157 -0
- package/audit/evidence/code/import-cycles-non-anyapi.txt +3 -0
- package/audit/evidence/code/import-hotspots-non-anyapi.txt +96 -0
- package/audit/evidence/code/imports-json-rest-api.txt +354 -0
- package/audit/evidence/code/imports-json-rest-schema.txt +15 -0
- package/audit/evidence/code/in-scope-files-json-rest-api.txt +160 -0
- package/audit/evidence/code/in-scope-files-json-rest-schema.txt +13 -0
- package/audit/evidence/code/non-anyapi-core-files.txt +101 -0
- package/audit/evidence/code/permission-signals.txt +193 -0
- package/audit/evidence/code/query-include-signals.txt +687 -0
- package/audit/evidence/code/transaction-signals.txt +28 -0
- package/audit/evidence/code/validation-signals.txt +650 -0
- package/audit/evidence/code/write-path-signals.txt +61 -0
- package/audit/evidence/docs/docs-claims-signals.txt +1075 -0
- package/audit/evidence/docs/schema-docs-claims-signals.txt +179 -0
- package/audit/evidence/external/corpus-freeze.md +21 -0
- package/audit/evidence/external/npm-competitor-metadata.txt +70 -0
- package/audit/evidence/external/npm-download-signals.txt +23 -0
- package/audit/evidence/external/npm-jsonapi-server.txt +8 -0
- package/audit/evidence/external/npm-modern-jsonapi-metadata.txt +38 -0
- package/audit/evidence/external/readme-fortune-signals.txt +26 -0
- package/audit/evidence/external/readme-fortune.md +165 -0
- package/audit/evidence/external/readme-json-api-serializer-signals.txt +80 -0
- package/audit/evidence/external/readme-json-api-serializer.md +690 -0
- package/audit/evidence/external/readme-jsonapi-serde-server-signals.txt +0 -0
- package/audit/evidence/external/readme-jsonapi-serde-server.md +22 -0
- package/audit/evidence/external/readme-jsonapi-serializer-signals.txt +63 -0
- package/audit/evidence/external/readme-jsonapi-serializer.md +315 -0
- package/audit/evidence/external/readme-jsonapi-server-signals.txt +19 -0
- package/audit/evidence/external/readme-jsonapi-server.md +104 -0
- package/audit/evidence/external/readme-nestjs-json-api-signals.txt +4 -0
- package/audit/evidence/external/readme-nestjs-json-api.md +94 -0
- package/audit/evidence/external/readme-yaser-json-api-nestjs-signals.txt +47 -0
- package/audit/evidence/external/readme-yaser-json-api-nestjs.md +447 -0
- package/audit/evidence/external/standard-jsonapi-snippets.txt +84 -0
- package/audit/evidence/external/standard-rfc-snippets.txt +168 -0
- package/audit/evidence/external/standards-sources.txt +6 -0
- package/audit/evidence/runtime/baseline-commits.txt +2 -0
- package/audit/evidence/runtime/db-adapter-assumptions.txt +3 -0
- package/audit/evidence/runtime/environment.txt +4 -0
- package/audit/evidence/runtime/invariant-runtime-mapping.md +38 -0
- package/audit/evidence/runtime/probe-patch-relationship-error.exitcode +1 -0
- package/audit/evidence/runtime/probe-patch-relationship-error.log +76 -0
- package/audit/evidence/runtime/probe-patch-relationship-error.mjs +67 -0
- package/audit/evidence/runtime/probe-patch-relationship-referenceerror.exitcode +1 -0
- package/audit/evidence/runtime/probe-patch-relationship-referenceerror.log +62 -0
- package/audit/evidence/runtime/probe-patch-relationship-referenceerror.mjs +47 -0
- package/audit/evidence/runtime/test-exitcodes.txt +4 -0
- package/audit/evidence/runtime/test-summary.md +107 -0
- package/audit/evidence/runtime/trace-anchors.txt +61 -0
- package/audit/evidence/tests/access-multitenancy-test-signals.txt +125 -0
- package/audit/evidence/tests/failing-tests-summary.txt +25 -0
- package/audit/evidence/tests/npm-test-json-rest-api.exitcode +1 -0
- package/audit/evidence/tests/npm-test-json-rest-api.log +5885 -0
- package/audit/evidence/tests/npm-test-json-rest-schema.exitcode +1 -0
- package/audit/evidence/tests/npm-test-json-rest-schema.log +356 -0
- package/audit/evidence/tests/query-include-test-signals.txt +948 -0
- package/audit/evidence/tests/relationship-test-signals.txt +223 -0
- package/audit/evidence/tests/skipped-tests.txt +10 -0
- package/audit/evidence/tests/test-suite-list-non-anyapi.txt +30 -0
- package/audit/evidence/tests/test-suite-list.txt +32 -0
- package/audit/subsystems/docs-drift.md +49 -0
- package/audit/subsystems/pass2-cross-review.md +131 -0
- package/audit/subsystems/plugins-access.md +51 -0
- package/audit/subsystems/plugins-bulk.md +50 -0
- package/audit/subsystems/plugins-cors.md +48 -0
- package/audit/subsystems/plugins-file.md +53 -0
- package/audit/subsystems/plugins-multihome.md +50 -0
- package/audit/subsystems/plugins-positioning.md +54 -0
- package/audit/subsystems/plugins-socketio.md +50 -0
- package/audit/subsystems/query-engine.md +61 -0
- package/audit/subsystems/relationships.md +55 -0
- package/audit/subsystems/rest-orchestration.md +66 -0
- package/audit/subsystems/schema-core.md +64 -0
- package/audit/subsystems/storage-knex.md +57 -0
- package/audit/subsystems/tests.md +48 -0
- package/audit/subsystems/transport-express.md +55 -0
- package/audit/traces/trace-read-get.md +22 -0
- package/audit/traces/trace-read-query.md +25 -0
- package/audit/traces/trace-relationship-endpoints.md +31 -0
- package/audit/traces/trace-write-delete.md +17 -0
- package/audit/traces/trace-write-patch.md +26 -0
- package/audit/traces/trace-write-post.md +22 -0
- package/audit/traces/trace-write-put.md +23 -0
- package/package.json +5 -5
|
@@ -0,0 +1,328 @@
|
|
|
1
|
+
1 import { RestApiResourceError } from '../../../../lib/rest-api-errors.js'
|
|
2
|
+
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
|
|
10
|
+
10 * @param {Array} parentIds - Parent record IDs
|
|
11
|
+
11 * @param {Array} fieldsToSelect - Fields to select
|
|
12
|
+
12 * @param {Object} includeConfig - Include configuration (limit, orderBy)
|
|
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(
|
|
20
|
+
20 * knex,
|
|
21
|
+
21 * 'comments',
|
|
22
|
+
22 * 'article_id',
|
|
23
|
+
23 * [1, 2, 3], // Article IDs
|
|
24
|
+
24 * ['id', 'text', 'created_at'],
|
|
25
|
+
25 * { limit: 3, orderBy: ['-created_at'] },
|
|
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
|
+
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
|
+
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(
|
|
48
|
+
48 * knex,
|
|
49
|
+
49 * 'comments',
|
|
50
|
+
50 * 'article_id',
|
|
51
|
+
51 * [1, 2, 3],
|
|
52
|
+
52 * '*',
|
|
53
|
+
53 * { limit: 5 },
|
|
54
|
+
54 * { windowFunctions: false, dbInfo: { client: 'mysql', version: '5.7' } }
|
|
55
|
+
55 * );
|
|
56
|
+
56 *
|
|
57
|
+
57 * // Throws RestApiResourceError:
|
|
58
|
+
58 * // "Include limits require window function support. Your database (mysql 5.7)
|
|
59
|
+
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
|
|
76
|
+
76 * 3. Outer query filters to keep only rows within limit
|
|
77
|
+
77 * 4. Returns query ready for execution
|
|
78
|
+
78 * 5. Caller removes the ROW_NUMBER column from results
|
|
79
|
+
79 */
|
|
80
|
+
80 export const buildWindowedIncludeQuery = (
|
|
81
|
+
81 knex,
|
|
82
|
+
82 tableName,
|
|
83
|
+
83 foreignKey,
|
|
84
|
+
84 parentIds,
|
|
85
|
+
85 fieldsToSelect,
|
|
86
|
+
86 includeConfig,
|
|
87
|
+
87 capabilities,
|
|
88
|
+
88 scopeVars = {}
|
|
89
|
+
89 ) => {
|
|
90
|
+
90 const { orderBy = [] } = includeConfig
|
|
91
|
+
91
|
|
92
|
+
92 // Apply defaults for limit
|
|
93
|
+
93 const effectiveLimit = includeConfig.limit ?? scopeVars.queryDefaultLimit ?? DEFAULT_QUERY_LIMIT
|
|
94
|
+
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
|
+
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
|
+
125 'ROW_NUMBER() OVER (PARTITION BY ?? ORDER BY ' +
|
|
126
|
+
126 buildOrderByClause(orderBy) +
|
|
127
|
+
127 ') as ' + ROW_NUMBER_KEY,
|
|
128
|
+
128 [foreignKey]
|
|
129
|
+
129 )
|
|
130
|
+
130 )
|
|
131
|
+
131 .whereIn(foreignKey, parentIds)
|
|
132
|
+
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
|
+
136 const fieldsWithFK = [...new Set([...fieldsToSelect, foreignKey, ROW_NUMBER_KEY])]
|
|
137
|
+
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
|
+
144 .where(ROW_NUMBER_KEY, '<=', effectiveLimit)
|
|
145
|
+
145
|
|
146
|
+
146 return query
|
|
147
|
+
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
|
|
191
|
+
191 * 3. Optionally prefixes with table name
|
|
192
|
+
192 * 4. Joins into SQL-compatible ORDER BY clause
|
|
193
|
+
193 */
|
|
194
|
+
194 export const buildOrderByClause = (orderBy, tablePrefix) => {
|
|
195
|
+
195 if (!orderBy || orderBy.length === 0) {
|
|
196
|
+
196 const defaultField = tablePrefix ? `${tablePrefix}.id` : 'id'
|
|
197
|
+
197 return `${defaultField} ASC` // Default ordering
|
|
198
|
+
198 }
|
|
199
|
+
199
|
|
200
|
+
200 return orderBy.map(field => {
|
|
201
|
+
201 const isDesc = field.startsWith('-')
|
|
202
|
+
202 const fieldName = isDesc ? field.substring(1) : field
|
|
203
|
+
203 const qualifiedField = tablePrefix ? `${tablePrefix}.${fieldName}` : fieldName
|
|
204
|
+
204 return `${qualifiedField} ${isDesc ? 'DESC' : 'ASC'}`
|
|
205
|
+
205 }).join(', ')
|
|
206
|
+
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]);
|
|
220
|
+
220 * query = applyStandardIncludeConfig(
|
|
221
|
+
221 * query,
|
|
222
|
+
222 * { limit: 20, orderBy: ['-created_at', 'id'] },
|
|
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');
|
|
234
|
+
234 * query = applyStandardIncludeConfig(
|
|
235
|
+
235 * query,
|
|
236
|
+
236 * { orderBy: ['name'] }, // No limit specified
|
|
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');
|
|
249
|
+
249 * query = applyStandardIncludeConfig(
|
|
250
|
+
250 * query,
|
|
251
|
+
251 * { limit: 5000 }, // Exceeds max
|
|
252
|
+
252 * { queryMaxLimit: 1000, maxIncludeLimit: 500 },
|
|
253
|
+
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');
|
|
262
|
+
262 * query = applyStandardIncludeConfig(
|
|
263
|
+
263 * query,
|
|
264
|
+
264 * { limit: null }, // Explicitly no limit
|
|
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
|
|
288
|
+
288 * 3. Clamps limit to maximum allowed values
|
|
289
|
+
289 * 4. Adds LIMIT clause unless explicitly disabled
|
|
290
|
+
290 * 5. Logs the reasoning for the applied limit
|
|
291
|
+
291 */
|
|
292
|
+
292 export const applyStandardIncludeConfig = (query, includeConfig, scopeVars, log) => {
|
|
293
|
+
293 const { orderBy = [] } = includeConfig
|
|
294
|
+
294
|
|
295
|
+
295 // Apply ordering
|
|
296
|
+
296 orderBy.forEach(field => {
|
|
297
|
+
297 const desc = field.startsWith('-')
|
|
298
|
+
298 const column = desc ? field.substring(1) : field
|
|
299
|
+
299 query = query.orderBy(column, desc ? 'desc' : 'asc')
|
|
300
|
+
300 })
|
|
301
|
+
301
|
|
302
|
+
302 // Apply limit with defaults
|
|
303
|
+
303 const requestedLimit = includeConfig.limit
|
|
304
|
+
304 const defaultLimit = scopeVars.queryDefaultLimit ?? DEFAULT_QUERY_LIMIT
|
|
305
|
+
305 const limit = requestedLimit ?? defaultLimit
|
|
306
|
+
306
|
|
307
|
+
307 // Allow explicit null/false to mean no limit
|
|
308
|
+
308 if (limit !== null && limit !== false) {
|
|
309
|
+
309 const maxInclude = scopeVars.maxIncludeLimit || DEFAULT_MAX_INCLUDE_LIMIT
|
|
310
|
+
310 const maxQuery = scopeVars.queryMaxLimit || DEFAULT_MAX_QUERY_LIMIT
|
|
311
|
+
311 const effectiveMax = Math.min(maxInclude, maxQuery)
|
|
312
|
+
312 const effectiveLimit = Math.min(limit, effectiveMax)
|
|
313
|
+
313
|
|
314
|
+
314 query = query.limit(effectiveLimit)
|
|
315
|
+
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
|
+
324 log.debug('No limit applied to include query (explicitly disabled)')
|
|
325
|
+
325 }
|
|
326
|
+
326
|
|
327
|
+
327 return query
|
|
328
|
+
328 }
|
|
@@ -0,0 +1,119 @@
|
|
|
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
|
+
31 export function getUrlPrefix (context, scope, req = null) {
|
|
32
|
+
32 // Priority 1: Explicit override
|
|
33
|
+
33 if (context?.urlPrefixOverride) {
|
|
34
|
+
34 return context.urlPrefixOverride
|
|
35
|
+
35 }
|
|
36
|
+
36
|
|
37
|
+
37 // Priority 2: Pre-calculated urlPrefix
|
|
38
|
+
38 if (context?.urlPrefix) {
|
|
39
|
+
39 return context.urlPrefix
|
|
40
|
+
40 }
|
|
41
|
+
41
|
|
42
|
+
42 // Priority 3: Calculate from request if provided
|
|
43
|
+
43 if (req) {
|
|
44
|
+
44 const protocol = req.get?.('x-forwarded-proto') || req.protocol || 'http'
|
|
45
|
+
45 const host = req.get?.('x-forwarded-host') || req.get?.('host')
|
|
46
|
+
46 const mountPath = scope?.vars?.transport?.mountPath || ''
|
|
47
|
+
47
|
|
48
|
+
48 if (host) {
|
|
49
|
+
49 return `${protocol}://${host}${mountPath}`
|
|
50
|
+
50 }
|
|
51
|
+
51 }
|
|
52
|
+
52
|
|
53
|
+
53 // Priority 4: Fallback to mount path
|
|
54
|
+
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
|
+
70 export function buildResourceUrl (context, scope, scopeName, id) {
|
|
71
|
+
71 const urlPrefix = getUrlPrefix(context, scope)
|
|
72
|
+
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
|
|
81
|
+
81 * @param {string} id - Resource 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
|
+
95 export function buildRelationshipUrl (context, scope, scopeName, id, relationshipName, isRelationshipEndpoint = false) {
|
|
96
|
+
96 const urlPrefix = getUrlPrefix(context, scope)
|
|
97
|
+
97 if (isRelationshipEndpoint) {
|
|
98
|
+
98 return `${urlPrefix}/${scopeName}/${id}/relationships/${relationshipName}`
|
|
99
|
+
99 }
|
|
100
|
+
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
|
+
116 export function buildCollectionUrl (context, scope, scopeName, queryString = '') {
|
|
117
|
+
117 const urlPrefix = getUrlPrefix(context, scope)
|
|
118
|
+
118 return `${urlPrefix}/${scopeName}${queryString}`
|
|
119
|
+
119 }
|
|
@@ -0,0 +1,160 @@
|
|
|
1
|
+
1 const passthrough = (value) => value
|
|
2
|
+
2 const identityTranslate = (_field, value) => value
|
|
3
|
+
3 const identityScope = (query) => query
|
|
4
|
+
4
|
|
5
|
+
5 const selectColumnsOnBuilder = (builder, columns) => {
|
|
6
|
+
6 if (!columns) return builder
|
|
7
|
+
7 if (Array.isArray(columns)) {
|
|
8
|
+
8 columns.forEach((column) => builder.select(column))
|
|
9
|
+
9 } else if (typeof columns === 'object') {
|
|
10
|
+
10 builder.select(columns)
|
|
11
|
+
11 }
|
|
12
|
+
12 return builder
|
|
13
|
+
13 }
|
|
14
|
+
14
|
|
15
|
+
15 const normalizeArray = (value, normalizeFn) => value.map((entry) => normalizeFn(entry))
|
|
16
|
+
16
|
|
17
|
+
17 const normalizeBelongsToValue = (value) => {
|
|
18
|
+
18 if (value === null || value === undefined) return value
|
|
19
|
+
19 if (Array.isArray(value)) {
|
|
20
|
+
20 return normalizeArray(value, normalizeBelongsToValue)
|
|
21
|
+
21 }
|
|
22
|
+
22 return String(value)
|
|
23
|
+
23 }
|
|
24
|
+
24
|
|
25
|
+
25 const baseAdapter = ({
|
|
26
|
+
26 knex,
|
|
27
|
+
27 tableName,
|
|
28
|
+
28 idColumn,
|
|
29
|
+
29 translateColumn,
|
|
30
|
+
30 translateFilterValue = identityTranslate,
|
|
31
|
+
31 applyResourceScope = identityScope,
|
|
32
|
+
32 isCanonical = false,
|
|
33
|
+
33 }) => {
|
|
34
|
+
34 const buildBaseQuery = ({ transaction } = {}) => {
|
|
35
|
+
35 const query = (transaction || knex)(tableName)
|
|
36
|
+
36 return applyResourceScope(query)
|
|
37
|
+
37 }
|
|
38
|
+
38
|
|
39
|
+
39 return {
|
|
40
|
+
40 isCanonical: () => isCanonical,
|
|
41
|
+
41 getTableName: () => tableName,
|
|
42
|
+
42 getIdColumn: () => idColumn,
|
|
43
|
+
43 translateColumn,
|
|
44
|
+
44 translateFilterValue,
|
|
45
|
+
45 applyResourceScope,
|
|
46
|
+
46 buildBaseQuery,
|
|
47
|
+
47 selectColumns: (builder, columns) => selectColumnsOnBuilder(builder, columns),
|
|
48
|
+
48 }
|
|
49
|
+
49 }
|
|
50
|
+
50
|
|
51
|
+
51 const createLegacyAdapter = ({ knex, schemaInfo }) => {
|
|
52
|
+
52 const tableName = schemaInfo.tableName
|
|
53
|
+
53 const idColumn = schemaInfo.idProperty || 'id'
|
|
54
|
+
54
|
|
55
|
+
55 const translateColumn = (column) => column
|
|
56
|
+
56
|
|
57
|
+
57 return baseAdapter({
|
|
58
|
+
58 knex,
|
|
59
|
+
59 tableName,
|
|
60
|
+
60 idColumn,
|
|
61
|
+
61 translateColumn,
|
|
62
|
+
62 translateFilterValue: identityTranslate,
|
|
63
|
+
63 applyResourceScope: identityScope,
|
|
64
|
+
64 isCanonical: false,
|
|
65
|
+
65 })
|
|
66
|
+
66 }
|
|
67
|
+
67
|
|
68
|
+
68 const createCanonicalAdapter = ({ knex, schemaInfo }) => {
|
|
69
|
+
69 const descriptor = schemaInfo.descriptor || {}
|
|
70
|
+
70 const canonical = descriptor.canonical || {}
|
|
71
|
+
71 const canonicalFieldMap = descriptor.canonicalFieldMap || {}
|
|
72
|
+
72 const fieldsInfo = descriptor.fields || {}
|
|
73
|
+
73 const belongsToInfo = descriptor.belongsTo || {}
|
|
74
|
+
74
|
|
75
|
+
75 const translateColumn = (field) => {
|
|
76
|
+
76 if (!field) return field
|
|
77
|
+
77 if (field === 'id') return 'id'
|
|
78
|
+
78
|
|
79
|
+
79 const canonicalEntry = canonicalFieldMap[field]
|
|
80
|
+
80 if (typeof canonicalEntry === 'string') {
|
|
81
|
+
81 return canonicalEntry
|
|
82
|
+
82 }
|
|
83
|
+
83 if (canonicalEntry && typeof canonicalEntry === 'object') {
|
|
84
|
+
84 if (canonicalEntry.slot) return canonicalEntry.slot
|
|
85
|
+
85 if (canonicalEntry.slotColumn) return canonicalEntry.slotColumn
|
|
86
|
+
86 if (canonicalEntry.idSlot) return canonicalEntry.idSlot
|
|
87
|
+
87 if (canonicalEntry.typeSlot && field.endsWith('_type')) return canonicalEntry.typeSlot
|
|
88
|
+
88 }
|
|
89
|
+
89
|
|
90
|
+
90 if (!canonicalEntry && field.endsWith('_id')) {
|
|
91
|
+
91 const alias = field.slice(0, -3)
|
|
92
|
+
92 const aliasEntry = canonicalFieldMap[alias]
|
|
93
|
+
93 if (typeof aliasEntry === 'string') return aliasEntry
|
|
94
|
+
94 if (aliasEntry?.idSlot) return aliasEntry.idSlot
|
|
95
|
+
95 }
|
|
96
|
+
96
|
|
97
|
+
97 if (!canonicalEntry && field.endsWith('_type')) {
|
|
98
|
+
98 const alias = field.slice(0, -5)
|
|
99
|
+
99 const aliasEntry = canonicalFieldMap[alias]
|
|
100
|
+
100 if (aliasEntry?.typeSlot) return aliasEntry.typeSlot
|
|
101
|
+
101 }
|
|
102
|
+
102
|
|
103
|
+
103 const fieldInfo = fieldsInfo[field]
|
|
104
|
+
104 if (fieldInfo?.slot) return fieldInfo.slot
|
|
105
|
+
105
|
|
106
|
+
106 const belongsInfo = belongsToInfo[field]
|
|
107
|
+
107 if (belongsInfo?.idColumn) return belongsInfo.idColumn
|
|
108
|
+
108
|
|
109
|
+
109 return field
|
|
110
|
+
110 }
|
|
111
|
+
111
|
|
112
|
+
112 const translateFilterValue = (field, value) => {
|
|
113
|
+
113 if (value === null || value === undefined) return value
|
|
114
|
+
114
|
|
115
|
+
115 const searchField = schemaInfo.searchSchemaStructure?.[field]
|
|
116
|
+
116 const schemaField = schemaInfo.schemaStructure?.[field]
|
|
117
|
+
117 const isRelationship = Boolean(
|
|
118
|
+
118 searchField?.isRelationship ||
|
|
119
|
+
119 schemaField?.belongsTo ||
|
|
120
|
+
120 schemaField?.belongsToPolymorphic
|
|
121
|
+
121 )
|
|
122
|
+
122
|
|
123
|
+
123 if (!isRelationship) {
|
|
124
|
+
124 if (Array.isArray(value)) {
|
|
125
|
+
125 return normalizeArray(value, passthrough)
|
|
126
|
+
126 }
|
|
127
|
+
127 return value
|
|
128
|
+
128 }
|
|
129
|
+
129
|
|
130
|
+
130 return normalizeBelongsToValue(value)
|
|
131
|
+
131 }
|
|
132
|
+
132
|
|
133
|
+
133 const applyResourceScope = (query) => {
|
|
134
|
+
134 return query
|
|
135
|
+
135 .where(canonical.tenantColumn, descriptor.tenant)
|
|
136
|
+
136 .where(canonical.resourceColumn, descriptor.resource)
|
|
137
|
+
137 }
|
|
138
|
+
138
|
|
139
|
+
139 return baseAdapter({
|
|
140
|
+
140 knex,
|
|
141
|
+
141 tableName: canonical.tableName,
|
|
142
|
+
142 idColumn: 'id',
|
|
143
|
+
143 translateColumn,
|
|
144
|
+
144 translateFilterValue,
|
|
145
|
+
145 applyResourceScope,
|
|
146
|
+
146 isCanonical: true,
|
|
147
|
+
147 })
|
|
148
|
+
148 }
|
|
149
|
+
149
|
|
150
|
+
150 export const createStorageAdapter = ({ knex, schemaInfo }) => {
|
|
151
|
+
151 if (!schemaInfo) {
|
|
152
|
+
152 throw new Error('createStorageAdapter requires schemaInfo')
|
|
153
|
+
153 }
|
|
154
|
+
154
|
|
155
|
+
155 if (schemaInfo?.descriptor?.canonical?.tableName) {
|
|
156
|
+
156 return createCanonicalAdapter({ knex, schemaInfo })
|
|
157
|
+
157 }
|
|
158
|
+
158
|
|
159
|
+
159 return createLegacyAdapter({ knex, schemaInfo })
|
|
160
|
+
160 }
|