json-rest-api 1.0.28 → 2.0.0
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/LICENSE +10 -670
- package/LICENSE-GPL-3.0 +674 -0
- package/LICENSE-MIT +21 -0
- package/README.md +39 -30
- package/docs/API.md +746 -1273
- package/docs/GUIDE/01-getting-started.md +338 -0
- package/docs/GUIDE/02-resources-and-relationships.md +33 -0
- package/docs/GUIDE/03-running-example.md +87 -0
- package/docs/GUIDE/04-creating-and-querying.md +181 -0
- package/docs/GUIDE/05-belongs-to.md +177 -0
- package/docs/GUIDE/06-has-many.md +184 -0
- package/docs/GUIDE/07-polymorphic-relationships.md +191 -0
- package/docs/GUIDE/08-many-to-many.md +184 -0
- package/docs/GUIDE/09-pagination-and-sorting.md +149 -0
- package/docs/GUIDE/10-put-and-patch.md +174 -0
- package/docs/GUIDE/11-relationship-endpoints.md +174 -0
- package/docs/GUIDE/12-field-transformations.md +287 -0
- package/docs/GUIDE/13-hooks-and-lifecycle.md +479 -0
- package/docs/GUIDE/14-custom-resource-methods.md +125 -0
- package/docs/GUIDE/15-query-projections.md +266 -0
- package/docs/GUIDE/{GUIDE_X_Autofiltering.md → 16-autofiltering.md} +77 -15
- package/docs/GUIDE/{GUIDE_X_Row_Policies.md → 17-row-policies.md} +137 -44
- package/docs/GUIDE/18-labels.md +86 -0
- package/docs/GUIDE/19-managed-transactions.md +179 -0
- package/docs/GUIDE/20-transaction-outcomes.md +207 -0
- package/docs/GUIDE/21-schema-and-migrations.md +634 -0
- package/docs/GUIDE/22-version-field-migrations.md +208 -0
- package/docs/GUIDE/23-fastify.md +179 -0
- package/docs/GUIDE/24-cors.md +172 -0
- package/docs/GUIDE/{GUIDE_X_URL_Management.md → 25-url-management.md} +7 -1
- package/docs/GUIDE/26-file-uploads.md +278 -0
- package/docs/GUIDE/27-bulk-operations.md +277 -0
- package/docs/GUIDE/28-socketio.md +325 -0
- package/docs/GUIDE/29-writing-plugins.md +278 -0
- package/docs/GUIDE/30-backend-capabilities.md +100 -0
- package/docs/GUIDE/31-positioning.md +181 -0
- package/docs/GUIDE/{GUIDE_Y_Appendices.md → 32-date-and-time.md} +90 -31
- package/docs/GUIDE/33-migrating-to-v2.md +3827 -0
- package/docs/GUIDE/index.md +51 -59
- package/docs/QUICKSTART.md +120 -287
- package/docs/architecture.md +75 -0
- package/docs/contributing.md +178 -0
- package/index.d.ts +36 -0
- package/index.js +1 -22
- package/lib/enhanced-logger.js +91 -85
- package/lib/error-context.js +445 -5
- package/lib/error-formatter.js +219 -59
- package/lib/knex-transaction.js +64 -0
- package/lib/logger-types.d.ts +22 -0
- package/lib/missing-package.js +4 -0
- package/lib/rest-api-errors.js +103 -8
- package/lib/runtime/json-rest-api.js +172 -0
- package/lib/transaction-types.d.ts +59 -0
- package/package.json +97 -27
- package/plugins/core/bulk-operations-plugin.js +191 -184
- package/plugins/core/connectors/express-plugin.js +148 -219
- package/plugins/core/connectors/fastify-plugin.js +125 -82
- package/plugins/core/connectors/lib/busboy-detector.js +74 -156
- package/plugins/core/connectors/lib/connector-core.js +144 -20
- package/plugins/core/connectors/lib/formidable-detector.js +63 -120
- package/plugins/core/connectors/lib/http-validators.js +77 -0
- package/plugins/core/connectors/lib/multipart-helpers.js +18 -0
- package/plugins/core/connectors/lib/request-helpers.js +10 -1
- package/plugins/core/connectors/lib/transport-http-helpers.js +89 -8
- package/plugins/core/connectors/lib/transport-route-schemas.js +7 -4
- package/plugins/core/file-handling-plugin.js +87 -174
- package/plugins/core/lib/anyapi/anyapi-registry.js +213 -164
- package/plugins/core/lib/anyapi/canonical-link-store.js +507 -0
- package/plugins/core/lib/anyapi/canonical-relationship-reader.js +598 -0
- package/plugins/core/lib/anyapi/schema-utils.js +25 -16
- package/plugins/core/lib/anyapi/utils/descriptor-helpers.js +0 -64
- package/plugins/core/lib/dbIntrospection.js +228 -21
- package/plugins/core/lib/dbTablesOperations.js +409 -92
- package/plugins/core/lib/fractional-positioning.js +12 -48
- package/plugins/core/lib/querying/identifier-query.js +19 -0
- package/plugins/core/lib/querying/include-query-helpers.js +364 -0
- package/plugins/core/lib/querying/include-to-many.js +428 -0
- package/plugins/core/lib/querying/include-to-one.js +579 -0
- package/plugins/core/lib/querying/knex-cross-table-search.js +31 -307
- package/plugins/core/lib/querying/knex-json-api-transformers-querying.js +99 -401
- package/plugins/core/lib/querying/knex-pagination-helpers.js +67 -330
- package/plugins/core/lib/querying/knex-process-includes.js +5 -110
- package/plugins/core/lib/querying/knex-query-helpers-base.js +21 -56
- package/plugins/core/lib/querying/knex-query-helpers.js +298 -448
- package/plugins/core/lib/querying/knex-relationship-includes.js +120 -2138
- package/plugins/core/lib/querying/knex-window-queries.js +62 -253
- package/plugins/core/lib/querying/query-builder-utils.js +44 -4
- package/plugins/core/lib/querying/query-constraint.d.ts +12 -0
- package/plugins/core/lib/querying/query-constraint.js +27 -0
- package/plugins/core/lib/querying/query-field-sort-helpers.js +205 -30
- package/plugins/core/lib/querying/relationship-identifiers.js +208 -0
- package/plugins/core/lib/querying/sort-helpers.js +20 -1
- package/plugins/core/lib/querying/storage-adapter-utils.js +13 -5
- package/plugins/core/lib/querying/url-helpers.js +11 -4
- package/plugins/core/lib/querying-writing/compile-schemas.js +148 -249
- package/plugins/core/lib/querying-writing/connectors-query-parser.js +33 -118
- package/plugins/core/lib/querying-writing/database-capabilities.js +149 -223
- package/plugins/core/lib/querying-writing/database-value-normalizers.js +238 -187
- package/plugins/core/lib/querying-writing/default-data-helpers.js +15 -45
- package/plugins/core/lib/querying-writing/field-selection-types.d.ts +43 -0
- package/plugins/core/lib/querying-writing/field-utils.js +113 -92
- package/plugins/core/lib/querying-writing/knex-constants.d.ts +18 -0
- package/plugins/core/lib/querying-writing/knex-constants.js +2 -26
- package/plugins/core/lib/querying-writing/knex-field-helpers.js +111 -287
- package/plugins/core/lib/querying-writing/knex-json-api-transformers.js +31 -120
- package/plugins/core/lib/querying-writing/query-field-helpers.js +108 -32
- package/plugins/core/lib/querying-writing/query-field-types.d.ts +60 -0
- package/plugins/core/lib/querying-writing/relationship-contracts.js +33 -13
- package/plugins/core/lib/querying-writing/request-contracts.js +109 -44
- package/plugins/core/lib/querying-writing/resource-id-normalization.js +23 -5
- package/plugins/core/lib/querying-writing/response-options.js +35 -0
- package/plugins/core/lib/querying-writing/schema-helpers.js +203 -324
- package/plugins/core/lib/querying-writing/scope-validations.js +92 -290
- package/plugins/core/lib/querying-writing/simplified-helpers.js +113 -712
- package/plugins/core/lib/storage/canonical-data-helpers.js +683 -0
- package/plugins/core/lib/storage/canonical-storage-mapping.js +51 -20
- package/plugins/core/lib/storage/ordinary-data-helpers.js +710 -0
- package/plugins/core/lib/storage/storage-adapter.js +142 -62
- package/plugins/core/lib/storage/storage-mapping.js +78 -28
- package/plugins/core/lib/storage/storage-types.d.ts +502 -0
- package/plugins/core/lib/writing/many-to-many-manipulations.js +114 -284
- package/plugins/core/lib/writing/relationship-processor.js +94 -158
- package/plugins/core/lib/writing/resource-version.js +172 -0
- package/plugins/core/lib/writing/reverse-relationship-manipulations.js +110 -0
- package/plugins/core/rest-api-anyapi-knex-plugin.js +116 -2509
- package/plugins/core/rest-api-autofilter-plugin.js +17 -19
- package/plugins/core/rest-api-cors-plugin.js +52 -205
- package/plugins/core/rest-api-knex-plugin.js +45 -1069
- package/plugins/core/rest-api-label-plugin.js +25 -38
- package/plugins/core/rest-api-plugin-hooks/register-relationship-routes.js +5 -5
- package/plugins/core/rest-api-plugin-hooks/register-scope-routes.js +9 -6
- package/plugins/core/rest-api-plugin-hooks/turn-scope-init-into-vars.js +7 -26
- package/plugins/core/rest-api-plugin-hooks/validate-include-configurations.js +20 -26
- package/plugins/core/rest-api-plugin-methods/add-route.js +4 -6
- package/plugins/core/rest-api-plugin-methods/apply-query-filters.js +42 -13
- package/plugins/core/rest-api-plugin-methods/check-permissions.js +11 -5
- package/plugins/core/rest-api-plugin-methods/common.js +291 -385
- package/plugins/core/rest-api-plugin-methods/delete-relationship.js +49 -47
- package/plugins/core/rest-api-plugin-methods/delete.js +31 -25
- package/plugins/core/rest-api-plugin-methods/enrich-attributes.js +108 -145
- package/plugins/core/rest-api-plugin-methods/get-related.js +113 -305
- package/plugins/core/rest-api-plugin-methods/get-relationship.js +25 -18
- package/plugins/core/rest-api-plugin-methods/get.js +38 -69
- package/plugins/core/rest-api-plugin-methods/lifecycle-types.d.ts +225 -0
- package/plugins/core/rest-api-plugin-methods/patch-relationship.js +28 -25
- package/plugins/core/rest-api-plugin-methods/patch.js +46 -99
- package/plugins/core/rest-api-plugin-methods/post-relationship.js +49 -53
- package/plugins/core/rest-api-plugin-methods/post.js +51 -51
- package/plugins/core/rest-api-plugin-methods/put.js +69 -148
- package/plugins/core/rest-api-plugin-methods/query.js +39 -82
- package/plugins/core/rest-api-plugin-methods/release.js +2 -1
- package/plugins/core/rest-api-plugin.js +32 -46
- package/plugins/core/rest-api-positioning-plugin.js +183 -628
- package/plugins/core/rest-api-query-projections-plugin.js +19 -102
- package/plugins/core/rest-api-row-policy-plugin.js +3 -3
- package/plugins/core/socketio-plugin.js +364 -344
- package/plugins/storage/local-storage.js +40 -36
- package/types/autofilter.d.ts +57 -0
- package/types/bulk-methods.d.ts +63 -0
- package/types/errors.d.ts +118 -0
- package/types/fastify-options.d.ts +19 -0
- package/types/file-detectors.d.ts +33 -0
- package/types/file-storage.d.ts +48 -0
- package/types/hook-context.d.ts +180 -0
- package/types/plugin-options.d.ts +53 -0
- package/types/relationship-methods.d.ts +74 -0
- package/types/representations.d.ts +50 -0
- package/types/resource-methods.d.ts +95 -0
- package/types/resource-schema.d.ts +157 -0
- package/types/row-policy.d.ts +31 -0
- package/types/runtime.d.ts +132 -0
- package/types/transactions.d.ts +21 -0
- package/.claude/agents/library-guide-expert.md +0 -30
- package/.claude/agents/rest-api-internals-expert.md +0 -54
- package/.claude/settings.local.json +0 -70
- package/AGENTS.md +0 -34
- package/CLAUDE.md +0 -160
- package/CRUSH.md +0 -55
- package/JSKIT_BACKEND_MIGRATION_TRACKER.txt +0 -180
- package/SINGLE_VALIDATION_CONTRACT_TODO.txt +0 -172
- package/TODO.todo +0 -13
- package/connectors_ideas/fastify.js +0 -217
- package/connectors_ideas/koa-plugin.js +0 -112
- package/docs/.bundle/config +0 -2
- package/docs/COMPARISON.md +0 -433
- package/docs/GUIDE/GUIDE_1_Initial_Setup.md +0 -1057
- package/docs/GUIDE/GUIDE_2_1_The_Starting_Point.md +0 -88
- package/docs/GUIDE/GUIDE_2_2_Manipulating_And_Searching_Tables.md +0 -808
- package/docs/GUIDE/GUIDE_2_3_BelongsTo_Relationships.md +0 -491
- package/docs/GUIDE/GUIDE_2_4_HasMany_Records.md +0 -398
- package/docs/GUIDE/GUIDE_2_5_HasMany_Polymorphic.md +0 -898
- package/docs/GUIDE/GUIDE_2_6_Many_To_Many.md +0 -712
- package/docs/GUIDE/GUIDE_2_7_Pagination_And_Ordering.md +0 -1238
- package/docs/GUIDE/GUIDE_2_8_Effects_of_PUT_and_PATCH.md +0 -748
- package/docs/GUIDE/GUIDE_2_9_Relationships_Urls.md +0 -517
- package/docs/GUIDE/GUIDE_2_Data_And_Relations.md +0 -66
- package/docs/GUIDE/GUIDE_3_Field_Transformations.md +0 -1252
- package/docs/GUIDE/GUIDE_5_Non-Db_Resources.md +0 -1107
- package/docs/GUIDE/GUIDE_6_Positioning.md +0 -692
- package/docs/GUIDE/GUIDE_7_Hooks_Data_Management_And_Plugins.md +0 -1491
- package/docs/GUIDE/GUIDE_X_Bulk_Operations.md +0 -732
- package/docs/GUIDE/GUIDE_X_Cors.md +0 -1957
- package/docs/GUIDE/GUIDE_X_Fastify.md +0 -78
- package/docs/GUIDE/GUIDE_X_File_Uploads.md +0 -729
- package/docs/GUIDE/GUIDE_X_Knex_Schema_And_Migrations.md +0 -370
- package/docs/GUIDE/GUIDE_X_Query_Projections.md +0 -159
- package/docs/GUIDE/GUIDE_X_SocketIO.md +0 -1040
- package/docs/GUIDE/GUIDE_X_Writing_Plugins.md +0 -201
- package/docs/Gemfile +0 -6
- package/docs/Gemfile.lock +0 -308
- package/docs/ONBOARDING.md +0 -315
- package/docs/_config.yml +0 -36
- package/docs/_layouts/default.html +0 -123
- package/docs/apple-touch-icon.svg +0 -4
- package/docs/assets/css/style.scss +0 -392
- package/docs/favicon.svg +0 -4
- package/docs/index.md +0 -164
- package/docs/mini-guides/file-uploads-complete.md +0 -161
- package/eslint.config.js +0 -13
- package/examples/url-override-example.js +0 -137
- package/fixing_plan.md +0 -112
- package/plugins/core/lib/anyapi/query/anyapi-query-adapter.js +0 -657
- package/plugins/core/lib/querying-writing/return-record-settings.js +0 -47
- package/plugins/core/lib/writing/knex-json-api-transformers-writing.js +0 -110
- package/plugins/storage/s3-storage.js +0 -93
- package/quickTest.js +0 -136
- package/scripts/debug-anyapi-review-filter.mjs +0 -63
- package/scripts/inspect-anyapi-descriptor-country.mjs +0 -19
- package/scripts/inspect-anyapi-descriptor.mjs +0 -19
- package/scripts/run-docs.js +0 -133
- package/test-debug.js +0 -59
- package/tests/README.md +0 -202
- package/tests/TEST_TEMPLATE.test.js +0 -178
- package/tests/anyapi-basic.test.js +0 -105
- package/tests/anyapi-cursor-pagination.test.js +0 -150
- package/tests/anyapi-custom-idproperty.test.js +0 -163
- package/tests/autofilter.test.js +0 -463
- package/tests/bulk-operations.test.js +0 -657
- package/tests/computed-fields.test.js +0 -586
- package/tests/cors.test.js +0 -834
- package/tests/curl.test.js +0 -467
- package/tests/custom-id-mysql-returning-fallback.test.js +0 -128
- package/tests/custom-id-simplified-post.test.js +0 -75
- package/tests/custom-idproperty-relationships.test.js +0 -932
- package/tests/db-introspection.test.js +0 -521
- package/tests/db-table-operations.test.js +0 -798
- package/tests/fastify-plugin.test.js +0 -184
- package/tests/field-getters.test.js +0 -370
- package/tests/field-setters.test.js +0 -306
- package/tests/file-handling.test.js +0 -118
- package/tests/fixtures/api-configs.js +0 -2509
- package/tests/full-jsonapi-belongsto-linkage.test.js +0 -150
- package/tests/helpers/fake-fastify.js +0 -99
- package/tests/helpers/storage-mode.js +0 -65
- package/tests/helpers/test-utils.js +0 -203
- package/tests/http-connectors-parity.test.js +0 -395
- package/tests/id-normalization.test.js +0 -444
- package/tests/include-depth-validation.test.js +0 -331
- package/tests/includes.test.js +0 -427
- package/tests/local-storage.test.js +0 -96
- package/tests/logical-id-write-validation.test.js +0 -151
- package/tests/nested-includes.test.js +0 -617
- package/tests/pagination-cursor-multifield.test.js +0 -643
- package/tests/pagination-enhanced.test.js +0 -563
- package/tests/polymorphic-relationships.test.js +0 -347
- package/tests/polymorphic-simple.test.js +0 -148
- package/tests/positioning-advanced.test.js +0 -692
- package/tests/positioning-extended.test.js +0 -915
- package/tests/positioning-multifilter.test.js +0 -699
- package/tests/positioning.test.js +0 -560
- package/tests/post-commit-hooks.test.js +0 -120
- package/tests/projected-fields.test.js +0 -319
- package/tests/public-exports.test.js +0 -15
- package/tests/put-create-parity.test.js +0 -216
- package/tests/queries.test.js +0 -521
- package/tests/query-limits.test.js +0 -584
- package/tests/relationship-endpoints.test.js +0 -620
- package/tests/relationship-include-storage-adapter.test.js +0 -56
- package/tests/relationships.test.js +0 -681
- package/tests/rest-api.test.js +0 -337
- package/tests/return-record-settings.test.js +0 -184
- package/tests/row-policy.test.js +0 -489
- package/tests/s3-storage.test.js +0 -38
- package/tests/searchschema-knex-behavior.test.js +0 -187
- package/tests/searchschema-merge.test.js +0 -151
- package/tests/socketio.test.js +0 -860
- package/tests/storage-mapping.test.js +0 -362
- package/tests/test-development-guide.md +0 -370
- package/tests/virtual-fields.test.js +0 -404
|
@@ -1,11 +1,26 @@
|
|
|
1
1
|
import { getUrlPrefix } from '../../lib/querying/url-helpers.js'
|
|
2
|
+
import { lockRelationshipParent } from '../../lib/writing/relationship-processor.js'
|
|
3
|
+
import { parseJsonApiQuery } from '../../lib/querying-writing/connectors-query-parser.js'
|
|
4
|
+
import { serializableTransaction } from '../../../../lib/knex-transaction.js'
|
|
5
|
+
import { cloneRequestHeaders } from './request-helpers.js'
|
|
6
|
+
import { RestApiPreconditionFailedError, RestApiValidationError, RestApiWriteError } from '../../../../lib/rest-api-errors.js'
|
|
7
|
+
import { createStrongEntityTag, matchesIfMatch, parseIfMatch } from './http-validators.js'
|
|
8
|
+
import { getOperationDiagnosticContext, getWriteOutcome, isRestApiError, wrapWriteError } from '../../../../lib/error-context.js'
|
|
9
|
+
import { createEnhancedLogger } from '../../../../lib/enhanced-logger.js'
|
|
2
10
|
import {
|
|
3
11
|
determineResponseStatus,
|
|
4
|
-
|
|
12
|
+
getHttpDiagnosticError,
|
|
13
|
+
isWriteMethod,
|
|
14
|
+
mapRestApiErrorToHttp,
|
|
15
|
+
mergeResponseHeaders
|
|
5
16
|
} from './transport-http-helpers.js'
|
|
6
17
|
|
|
18
|
+
const responseLifecycleStarted = Symbol('responseLifecycleStarted')
|
|
19
|
+
|
|
7
20
|
export function extractQueryString (url = '') {
|
|
8
|
-
|
|
21
|
+
const value = String(url || '')
|
|
22
|
+
const index = value.indexOf('?')
|
|
23
|
+
return index === -1 ? '' : value.slice(index + 1)
|
|
9
24
|
}
|
|
10
25
|
|
|
11
26
|
export function buildTransportRequestData ({
|
|
@@ -74,6 +89,17 @@ export async function runTransportRequestLifecycle ({ context, runHooks }) {
|
|
|
74
89
|
}
|
|
75
90
|
}
|
|
76
91
|
|
|
92
|
+
export function addWriteOutcomeToHttpErrors (body, context) {
|
|
93
|
+
if (!isWriteMethod(context.transport?.request?.method) || !Array.isArray(body?.errors)) return body
|
|
94
|
+
return {
|
|
95
|
+
...body,
|
|
96
|
+
errors: body.errors.map(error => ({
|
|
97
|
+
...error,
|
|
98
|
+
meta: { ...error.meta, transactionOutcome: error.meta?.transactionOutcome || getWriteOutcome(context) }
|
|
99
|
+
}))
|
|
100
|
+
}
|
|
101
|
+
}
|
|
102
|
+
|
|
77
103
|
export function buildTransportRejectionBody (context) {
|
|
78
104
|
return {
|
|
79
105
|
errors: [{
|
|
@@ -99,7 +125,11 @@ export async function applyTransportResponseLifecycle ({
|
|
|
99
125
|
transportData.response.body = body
|
|
100
126
|
context.transport = transportData
|
|
101
127
|
|
|
102
|
-
|
|
128
|
+
// A failing response hook is reported as an error, without rerunning hooks.
|
|
129
|
+
if (!context[responseLifecycleStarted]) {
|
|
130
|
+
context[responseLifecycleStarted] = true
|
|
131
|
+
await runHooks('transport:response', context)
|
|
132
|
+
}
|
|
103
133
|
|
|
104
134
|
return transportData.response.headers || {}
|
|
105
135
|
}
|
|
@@ -117,7 +147,7 @@ export function getConnectorLocationHeader ({
|
|
|
117
147
|
publicBaseUrl = ''
|
|
118
148
|
}) {
|
|
119
149
|
if (String(method || '').toUpperCase() !== 'POST') return null
|
|
120
|
-
if (
|
|
150
|
+
if (context?.id === undefined || context.id === null || !routeMeta?.scopeName || !helpers?.getLocation) return null
|
|
121
151
|
|
|
122
152
|
const location = helpers.getLocation({
|
|
123
153
|
scopeName: routeMeta.scopeName,
|
|
@@ -131,6 +161,8 @@ export function getConnectorLocationHeader ({
|
|
|
131
161
|
}
|
|
132
162
|
|
|
133
163
|
export async function executeConnectorRoute ({
|
|
164
|
+
api,
|
|
165
|
+
httpValidators = false,
|
|
134
166
|
method,
|
|
135
167
|
handler,
|
|
136
168
|
queryString,
|
|
@@ -145,13 +177,72 @@ export async function executeConnectorRoute ({
|
|
|
145
177
|
publicBaseUrl = '',
|
|
146
178
|
runHooks
|
|
147
179
|
}) {
|
|
148
|
-
const
|
|
180
|
+
const requestMethod = String(method).toUpperCase()
|
|
181
|
+
const ignoreConditions = ['OPTIONS', 'CONNECT', 'TRACE'].includes(requestMethod)
|
|
182
|
+
const condition = httpValidators && !ignoreConditions ? parseIfMatch(headers?.['if-match']) : undefined
|
|
183
|
+
const isGet = ['GET', 'HEAD'].includes(requestMethod)
|
|
184
|
+
const conditionalWrite = condition !== undefined && !isGet && routeMeta?.kind === 'resource' && ['put', 'patch', 'delete'].includes(routeMeta.operation)
|
|
185
|
+
if (condition !== undefined && !isGet && !conditionalWrite) {
|
|
186
|
+
throw new RestApiValidationError('If-Match is not supported for this route', { fields: ['headers.if-match'] })
|
|
187
|
+
}
|
|
188
|
+
const request = {
|
|
149
189
|
queryString,
|
|
150
190
|
headers,
|
|
151
191
|
params,
|
|
152
192
|
body,
|
|
153
193
|
context
|
|
154
|
-
}
|
|
194
|
+
}
|
|
195
|
+
let result
|
|
196
|
+
if (conditionalWrite) {
|
|
197
|
+
result = await api.transaction(async transaction => {
|
|
198
|
+
const readTransport = buildTransportData({ ...transportData.request, headers: cloneRequestHeaders(transportData.request.headers), method: 'GET', body: undefined })
|
|
199
|
+
let readContext
|
|
200
|
+
if (condition.wildcard) {
|
|
201
|
+
readContext = { ...context }
|
|
202
|
+
} else {
|
|
203
|
+
readContext = {
|
|
204
|
+
...context,
|
|
205
|
+
request: { ...context.request, headers: cloneRequestHeaders(context.request?.headers), method: 'GET' },
|
|
206
|
+
transport: readTransport,
|
|
207
|
+
precondition: true
|
|
208
|
+
}
|
|
209
|
+
}
|
|
210
|
+
let preconditionPassed = false
|
|
211
|
+
const precondition = async () => {
|
|
212
|
+
preconditionPassed = false
|
|
213
|
+
let representation
|
|
214
|
+
try {
|
|
215
|
+
representation = await api.resources[routeMeta.scopeName].get({
|
|
216
|
+
id: params.id,
|
|
217
|
+
format: 'jsonapi',
|
|
218
|
+
transaction,
|
|
219
|
+
...(condition.wildcard ? {} : { queryParams: parseJsonApiQuery(queryString) })
|
|
220
|
+
}, readContext)
|
|
221
|
+
await lockRelationshipParent({ context: readContext, helpers, scopeName: routeMeta.scopeName })
|
|
222
|
+
} catch (error) {
|
|
223
|
+
// Conditional PUT requires an existing visible representation.
|
|
224
|
+
if (routeMeta.operation === 'put' && isRestApiError(error) && error.subtype === 'not_found') {
|
|
225
|
+
throw new RestApiPreconditionFailedError({ resourceType: routeMeta.scopeName, resourceId: params.id })
|
|
226
|
+
}
|
|
227
|
+
throw error
|
|
228
|
+
}
|
|
229
|
+
if (!condition.wildcard) {
|
|
230
|
+
await applyTransportResponseLifecycle({ context: readContext, transportData: readTransport, status: 200, body: representation, runHooks })
|
|
231
|
+
const bytes = JSON.stringify(getConnectorResponseBody(readTransport.response.body))
|
|
232
|
+
const tag = bytes === undefined ? undefined : parseIfMatch(createStrongEntityTag(bytes)).tags[0]
|
|
233
|
+
if (!matchesIfMatch(condition, { exists: bytes !== undefined, tag })) {
|
|
234
|
+
throw new RestApiPreconditionFailedError({ resourceType: routeMeta.scopeName, resourceId: params.id })
|
|
235
|
+
}
|
|
236
|
+
}
|
|
237
|
+
preconditionPassed = true
|
|
238
|
+
}
|
|
239
|
+
const result = await handler({ ...request, transaction, precondition })
|
|
240
|
+
if (!preconditionPassed) throw new RestApiValidationError('Resource handler did not evaluate its HTTP precondition', { fields: ['headers.if-match'] })
|
|
241
|
+
return result
|
|
242
|
+
}, { [serializableTransaction]: !condition.wildcard })
|
|
243
|
+
} else {
|
|
244
|
+
result = await handler(request)
|
|
245
|
+
}
|
|
155
246
|
|
|
156
247
|
const status = determineResponseStatus(method, result)
|
|
157
248
|
const location = getConnectorLocationHeader({
|
|
@@ -170,32 +261,65 @@ export async function executeConnectorRoute ({
|
|
|
170
261
|
runHooks
|
|
171
262
|
})
|
|
172
263
|
|
|
264
|
+
let responseBody
|
|
265
|
+
if (status !== 204) {
|
|
266
|
+
const response = context && transportData ? transportData.response.body : result
|
|
267
|
+
responseBody = getConnectorResponseBody(response)
|
|
268
|
+
}
|
|
269
|
+
const serialized = httpValidators && isGet && status === 200 && responseBody !== undefined
|
|
270
|
+
const responseBytes = serialized ? JSON.stringify(responseBody) : undefined
|
|
271
|
+
const validatorHeaders = serialized ? { etag: createStrongEntityTag(responseBytes) } : undefined
|
|
272
|
+
if (condition !== undefined && isGet && status >= 200 && status < 300) {
|
|
273
|
+
const tag = validatorHeaders ? parseIfMatch(validatorHeaders.etag).tags[0] : undefined
|
|
274
|
+
if (!matchesIfMatch(condition, { exists: status === 200, tag })) {
|
|
275
|
+
throw new RestApiPreconditionFailedError({ resourceType: routeMeta?.scopeName, resourceId: params?.id })
|
|
276
|
+
}
|
|
277
|
+
}
|
|
173
278
|
return {
|
|
174
279
|
status,
|
|
175
|
-
body:
|
|
176
|
-
|
|
177
|
-
|
|
178
|
-
...transportHeaders
|
|
179
|
-
},
|
|
280
|
+
body: serialized ? responseBytes : responseBody,
|
|
281
|
+
serialized,
|
|
282
|
+
headers: mergeResponseHeaders(result?.headers, transportHeaders, validatorHeaders),
|
|
180
283
|
location,
|
|
181
284
|
result
|
|
182
285
|
}
|
|
183
286
|
}
|
|
184
287
|
|
|
288
|
+
export async function logHttpRequestError ({ error, context, log, scopes, routeMeta, api, method, path, message }) {
|
|
289
|
+
try {
|
|
290
|
+
const schemaInfo = scopes[routeMeta?.scopeName]?.vars?.schemaInfo || context.schemaInfo
|
|
291
|
+
const logger = createEnhancedLogger(log, { schemaInfo, redactFields: ['body', 'headers'] })
|
|
292
|
+
await logger.logError(message, getHttpDiagnosticError(error), {
|
|
293
|
+
...getOperationDiagnosticContext(context, {
|
|
294
|
+
phase: 'httpError',
|
|
295
|
+
method,
|
|
296
|
+
scopeName: routeMeta?.scopeName || context.scopeName,
|
|
297
|
+
backend: api.knex?.instance?.client?.config?.client
|
|
298
|
+
}),
|
|
299
|
+
path
|
|
300
|
+
})
|
|
301
|
+
} catch (error) {
|
|
302
|
+
(context.cleanupErrors ||= []).push({ phase: 'logging', during: 'httpError', error })
|
|
303
|
+
}
|
|
304
|
+
}
|
|
305
|
+
|
|
185
306
|
export async function handleConnectorError ({
|
|
186
307
|
error,
|
|
187
308
|
context,
|
|
188
309
|
transportData,
|
|
189
310
|
runHooks
|
|
190
311
|
}) {
|
|
312
|
+
const write = isWriteMethod(transportData?.request?.method)
|
|
313
|
+
if (write && !(isRestApiError(error) && error instanceof RestApiWriteError)) error = wrapWriteError(error, context)
|
|
191
314
|
const { status, body } = mapRestApiErrorToHttp(error)
|
|
192
|
-
|
|
193
|
-
context,
|
|
194
|
-
transportData,
|
|
195
|
-
|
|
196
|
-
|
|
197
|
-
|
|
198
|
-
|
|
199
|
-
|
|
200
|
-
|
|
315
|
+
try {
|
|
316
|
+
const headers = await applyTransportResponseLifecycle({ context, transportData, status, body, runHooks })
|
|
317
|
+
return { status, body: context && transportData ? transportData.response.body : body, headers }
|
|
318
|
+
} catch (responseError) {
|
|
319
|
+
const failure = new AggregateError([error, responseError], 'Request and response hook failed', { cause: error })
|
|
320
|
+
const outcome = mapRestApiErrorToHttp(write ? wrapWriteError(failure, context) : failure)
|
|
321
|
+
// The first attempt marked the lifecycle started, so this only updates metadata.
|
|
322
|
+
const headers = await applyTransportResponseLifecycle({ context, transportData, ...outcome, runHooks })
|
|
323
|
+
return { ...outcome, headers }
|
|
324
|
+
}
|
|
201
325
|
}
|
|
@@ -1,138 +1,81 @@
|
|
|
1
|
-
|
|
2
|
-
|
|
3
|
-
|
|
4
|
-
|
|
5
|
-
|
|
6
|
-
|
|
7
|
-
|
|
8
|
-
* Features:
|
|
9
|
-
* - Disk-based storage (better for large files)
|
|
10
|
-
* - Automatic temp file cleanup
|
|
11
|
-
* - Progress tracking support
|
|
12
|
-
* - Built-in file type detection
|
|
13
|
-
*
|
|
14
|
-
* Usage:
|
|
15
|
-
* ```javascript
|
|
16
|
-
* import { createFormidableDetector } from 'jsonrestapi/plugins/core/lib/formidable-detector.js';
|
|
17
|
-
*
|
|
18
|
-
* api.use(ExpressPlugin, {
|
|
19
|
-
* fileParser: 'formidable',
|
|
20
|
-
* fileParserOptions: {
|
|
21
|
-
* uploadDir: './uploads/temp',
|
|
22
|
-
* keepExtensions: true,
|
|
23
|
-
* maxFileSize: 200 * 1024 * 1024 // 200MB
|
|
24
|
-
* }
|
|
25
|
-
* });
|
|
26
|
-
* ```
|
|
27
|
-
*/
|
|
28
|
-
|
|
29
|
-
import { requirePackage } from 'hooked-api'
|
|
30
|
-
import { promises as fs } from 'fs'
|
|
31
|
-
import path from 'path'
|
|
1
|
+
import { throwMissingPackage } from '../../../../lib/missing-package.js'
|
|
2
|
+
import { promises as fs } from 'node:fs'
|
|
3
|
+
import path from 'node:path'
|
|
4
|
+
import { tmpdir } from 'node:os'
|
|
5
|
+
import { RestApiPayloadError } from '../../../../lib/rest-api-errors.js'
|
|
6
|
+
import { isMultipartContentType } from './transport-http-helpers.js'
|
|
7
|
+
import { addMultipartField, multipartLimits } from './multipart-helpers.js'
|
|
32
8
|
|
|
33
9
|
let formidable
|
|
34
|
-
try {
|
|
35
|
-
formidable
|
|
36
|
-
} catch (e) {
|
|
37
|
-
requirePackage('formidable', 'express/http-connector',
|
|
38
|
-
'Formidable is required for multipart/form-data file uploads with disk storage. This is a peer dependency.')
|
|
10
|
+
try { formidable = (await import('formidable')).default } catch {
|
|
11
|
+
throwMissingPackage('formidable', 'express-connector', 'Formidable is required for multipart uploads. Install the optional formidable peer.')
|
|
39
12
|
}
|
|
40
13
|
|
|
41
|
-
/**
|
|
42
|
-
* Creates a formidable-based file detector
|
|
43
|
-
*
|
|
44
|
-
* @param {Object} options - Formidable configuration options
|
|
45
|
-
* @param {string} options.uploadDir - Directory for temporary files
|
|
46
|
-
* @param {boolean} options.keepExtensions - Keep file extensions
|
|
47
|
-
* @param {number} options.maxFileSize - Max file size in bytes
|
|
48
|
-
* @param {boolean} options.multiples - Allow multiple files per field
|
|
49
|
-
* @returns {Object} Detector object with detect() and parse() methods
|
|
50
|
-
*/
|
|
14
|
+
/** Buffer completed uploads, then remove this request's temporary directory. */
|
|
51
15
|
export function createFormidableDetector (options = {}) {
|
|
52
|
-
|
|
53
|
-
const uploadDir = options.uploadDir || path.join(process.cwd(), 'uploads', 'temp')
|
|
54
|
-
|
|
16
|
+
const uploadDir = path.resolve(options.uploadDir || tmpdir())
|
|
55
17
|
return {
|
|
56
18
|
name: 'formidable-multipart',
|
|
57
|
-
|
|
58
|
-
/**
|
|
59
|
-
* Check if this detector can handle the request
|
|
60
|
-
* @param {Object} params - Request parameters
|
|
61
|
-
* @returns {boolean} True if this is a multipart request
|
|
62
|
-
*/
|
|
63
|
-
detect: (params) => {
|
|
64
|
-
const req = params._httpReq || params._expressReq
|
|
65
|
-
if (!req || !req.headers) return false
|
|
66
|
-
|
|
67
|
-
const contentType = req.headers['content-type'] || ''
|
|
68
|
-
return contentType.includes('multipart/form-data')
|
|
69
|
-
},
|
|
70
|
-
|
|
71
|
-
/**
|
|
72
|
-
* Parse multipart data from the request
|
|
73
|
-
* @param {Object} params - Request parameters
|
|
74
|
-
* @returns {Promise<{fields: Object, files: Object}>} Parsed data
|
|
75
|
-
*/
|
|
19
|
+
detect: params => isMultipartContentType((params._httpReq || params._expressReq)?.headers?.['content-type']),
|
|
76
20
|
parse: async (params) => {
|
|
77
21
|
const req = params._httpReq || params._expressReq
|
|
78
|
-
|
|
79
|
-
|
|
80
|
-
|
|
81
|
-
|
|
82
|
-
|
|
83
|
-
|
|
84
|
-
uploadDir,
|
|
85
|
-
|
|
86
|
-
|
|
87
|
-
|
|
88
|
-
|
|
89
|
-
|
|
90
|
-
|
|
91
|
-
|
|
92
|
-
|
|
93
|
-
|
|
94
|
-
|
|
95
|
-
|
|
96
|
-
|
|
97
|
-
const
|
|
98
|
-
|
|
99
|
-
|
|
100
|
-
|
|
101
|
-
|
|
102
|
-
|
|
103
|
-
//
|
|
104
|
-
|
|
105
|
-
|
|
106
|
-
|
|
107
|
-
|
|
22
|
+
if (!req?.on || req.aborted) throw new RestApiPayloadError('Multipart request is unavailable or aborted')
|
|
23
|
+
let directory, failure, rejectParsing, result
|
|
24
|
+
const onAborted = () => rejectParsing?.(new RestApiPayloadError('Multipart request aborted'))
|
|
25
|
+
req.once('aborted', onAborted)
|
|
26
|
+
try {
|
|
27
|
+
await fs.mkdir(uploadDir, { recursive: true })
|
|
28
|
+
directory = await fs.mkdtemp(path.join(uploadDir, 'json-rest-upload-'))
|
|
29
|
+
if (req.aborted) throw new RestApiPayloadError('Multipart request aborted')
|
|
30
|
+
const form = formidable({
|
|
31
|
+
maxFileSize: multipartLimits.fileSize,
|
|
32
|
+
maxTotalFileSize: multipartLimits.fileSize * multipartLimits.files,
|
|
33
|
+
maxFiles: multipartLimits.files,
|
|
34
|
+
maxFields: multipartLimits.fields,
|
|
35
|
+
maxFieldsSize: multipartLimits.fieldSize * multipartLimits.fields,
|
|
36
|
+
allowEmptyFiles: true,
|
|
37
|
+
minFileSize: 0,
|
|
38
|
+
...options,
|
|
39
|
+
uploadDir: directory
|
|
40
|
+
})
|
|
41
|
+
const entries = []
|
|
42
|
+
const uploads = []
|
|
43
|
+
form.on('field', (name, value) => entries.push([name, value]))
|
|
44
|
+
form.on('file', (name, file) => uploads.push([name, file]))
|
|
45
|
+
await new Promise((resolve, reject) => {
|
|
46
|
+
rejectParsing = reject
|
|
47
|
+
// Callback mode avoids an unobserved internal promise on early abort.
|
|
48
|
+
form.parse(req, error => error ? reject(error) : resolve()).catch(reject)
|
|
49
|
+
})
|
|
50
|
+
const fields = new Map()
|
|
51
|
+
const files = new Map()
|
|
52
|
+
for (const [name, value] of entries) addMultipartField(fields, name, value)
|
|
53
|
+
for (const [name, file] of uploads) {
|
|
54
|
+
if (files.has(name)) throw new RestApiPayloadError(`Multiple files for field '${name}' are not supported`, { path: name })
|
|
55
|
+
files.set(name, {
|
|
108
56
|
filename: file.originalFilename || 'unknown',
|
|
109
57
|
mimetype: file.mimetype || 'application/octet-stream',
|
|
110
58
|
size: file.size,
|
|
111
|
-
data
|
|
112
|
-
|
|
113
|
-
|
|
114
|
-
|
|
115
|
-
|
|
116
|
-
|
|
117
|
-
|
|
118
|
-
|
|
119
|
-
|
|
120
|
-
|
|
121
|
-
|
|
122
|
-
|
|
123
|
-
|
|
124
|
-
|
|
59
|
+
data: await fs.readFile(file.filepath)
|
|
60
|
+
})
|
|
61
|
+
}
|
|
62
|
+
result = { fields: Object.fromEntries(fields), files: Object.fromEntries(files) }
|
|
63
|
+
} catch (error) {
|
|
64
|
+
failure = [400, 413].includes(error.httpCode)
|
|
65
|
+
? new RestApiPayloadError(error.message, { statusCode: error.httpCode, cause: error })
|
|
66
|
+
: error
|
|
67
|
+
} finally {
|
|
68
|
+
req.removeListener('aborted', onAborted)
|
|
69
|
+
if (failure) req.resume()
|
|
70
|
+
if (directory) {
|
|
71
|
+
try { await fs.rm(directory, { recursive: true, force: true }) } catch (error) {
|
|
72
|
+
if (failure) failure.cleanupError = error
|
|
73
|
+
else failure = error
|
|
125
74
|
}
|
|
126
75
|
}
|
|
127
76
|
}
|
|
128
|
-
|
|
129
|
-
|
|
130
|
-
const normalizedFields = {}
|
|
131
|
-
for (const [key, value] of Object.entries(fields)) {
|
|
132
|
-
normalizedFields[key] = Array.isArray(value) && value.length === 1 ? value[0] : value
|
|
133
|
-
}
|
|
134
|
-
|
|
135
|
-
return { fields: normalizedFields, files }
|
|
77
|
+
if (failure) throw failure
|
|
78
|
+
return result
|
|
136
79
|
}
|
|
137
80
|
}
|
|
138
81
|
}
|
|
@@ -0,0 +1,77 @@
|
|
|
1
|
+
// @ts-check
|
|
2
|
+
/** @typedef {{ readonly weak: boolean, readonly value: string }} EntityTag */
|
|
3
|
+
/** @typedef {{ readonly wildcard: true, readonly tags: readonly [] } | { readonly wildcard: false, readonly tags: readonly EntityTag[] }} IfMatchCondition */
|
|
4
|
+
import { RestApiValidationError } from '../../../../lib/rest-api-errors.js'
|
|
5
|
+
import { createHash } from 'node:crypto'
|
|
6
|
+
|
|
7
|
+
const MAX_HEADER_LENGTH = 8192
|
|
8
|
+
const MAX_LIST_ITEMS = 128
|
|
9
|
+
const invalidIfMatch = () => new RestApiValidationError('Invalid or oversized If-Match header', { fields: ['headers.if-match'] })
|
|
10
|
+
|
|
11
|
+
// Hash serialized bytes only; callers must send these same bytes without reserialization.
|
|
12
|
+
/**
|
|
13
|
+
* @param {string | Buffer} body
|
|
14
|
+
* @param {{ contentType?: string, contentEncoding?: string }} [options]
|
|
15
|
+
* @returns {string}
|
|
16
|
+
*/
|
|
17
|
+
export function createStrongEntityTag (body, { contentType = 'application/vnd.api+json', contentEncoding = 'identity' } = {}) {
|
|
18
|
+
if (typeof body !== 'string' && !Buffer.isBuffer(body)) throw new TypeError('Entity tags require a serialized string or Buffer')
|
|
19
|
+
if (typeof contentType !== 'string' || typeof contentEncoding !== 'string') throw new TypeError('Entity tag representation metadata must be strings')
|
|
20
|
+
const digest = createHash('sha256').update(JSON.stringify([contentType, contentEncoding])).update('\0').update(body).digest('base64url')
|
|
21
|
+
return `"jra1-${digest}"`
|
|
22
|
+
}
|
|
23
|
+
|
|
24
|
+
// Entity tags are opaque octets, not JSON strings: backslashes do not escape quotes.
|
|
25
|
+
/** @param {unknown} input @returns {IfMatchCondition | undefined} */
|
|
26
|
+
export function parseIfMatch (input) {
|
|
27
|
+
if (input === undefined) return undefined
|
|
28
|
+
/** @type {readonly unknown[]} */
|
|
29
|
+
const lines = Array.isArray(input) ? input : [input]
|
|
30
|
+
if (lines.length > MAX_LIST_ITEMS) throw invalidIfMatch()
|
|
31
|
+
let length = Math.max(0, lines.length - 1)
|
|
32
|
+
for (const line of lines) {
|
|
33
|
+
if (typeof line !== 'string' || (length += line.length) > MAX_HEADER_LENGTH) throw invalidIfMatch()
|
|
34
|
+
}
|
|
35
|
+
const text = lines.join(',')
|
|
36
|
+
if (text.replace(/^[ \t]+|[ \t]+$/g, '') === '*') return { wildcard: true, tags: [] }
|
|
37
|
+
const tags = []
|
|
38
|
+
let position = 0
|
|
39
|
+
let separators = 0
|
|
40
|
+
const whitespace = () => { while (text[position] === ' ' || text[position] === '\t') position++ }
|
|
41
|
+
while (position < text.length) {
|
|
42
|
+
whitespace()
|
|
43
|
+
if (position === text.length) break
|
|
44
|
+
if (text[position] === ',') {
|
|
45
|
+
if (++separators > MAX_LIST_ITEMS) throw invalidIfMatch()
|
|
46
|
+
position++
|
|
47
|
+
continue
|
|
48
|
+
}
|
|
49
|
+
const weak = text.startsWith('W/', position)
|
|
50
|
+
if (weak) position += 2
|
|
51
|
+
if (text[position++] !== '"') throw invalidIfMatch()
|
|
52
|
+
const start = position
|
|
53
|
+
while (position < text.length && text[position] !== '"') {
|
|
54
|
+
const code = text.charCodeAt(position++)
|
|
55
|
+
if (!(code === 0x21 || (code >= 0x23 && code <= 0x7e) || (code >= 0x80 && code <= 0xff))) throw invalidIfMatch()
|
|
56
|
+
}
|
|
57
|
+
if (position === text.length) throw invalidIfMatch()
|
|
58
|
+
tags.push({ weak, value: text.slice(start, position++) })
|
|
59
|
+
if (tags.length > MAX_LIST_ITEMS) throw invalidIfMatch()
|
|
60
|
+
whitespace()
|
|
61
|
+
if (position < text.length && text[position] !== ',') throw invalidIfMatch()
|
|
62
|
+
}
|
|
63
|
+
return { wildcard: false, tags }
|
|
64
|
+
}
|
|
65
|
+
|
|
66
|
+
/**
|
|
67
|
+
* @param {IfMatchCondition | undefined} condition
|
|
68
|
+
* @param {{ exists: boolean, tag?: EntityTag }} representation
|
|
69
|
+
* @returns {boolean}
|
|
70
|
+
*/
|
|
71
|
+
export function matchesIfMatch (condition, { exists, tag }) {
|
|
72
|
+
if (condition === undefined) return true
|
|
73
|
+
if (!exists) return false
|
|
74
|
+
if (condition.wildcard) return true
|
|
75
|
+
if (!tag || tag.weak) return false
|
|
76
|
+
return condition.tags.some(candidate => !candidate.weak && candidate.value === tag.value)
|
|
77
|
+
}
|
|
@@ -0,0 +1,18 @@
|
|
|
1
|
+
import { RestApiPayloadError } from '../../../../lib/rest-api-errors.js'
|
|
2
|
+
|
|
3
|
+
export const multipartLimits = Object.freeze({
|
|
4
|
+
fileSize: 10 * 1024 * 1024,
|
|
5
|
+
files: 10,
|
|
6
|
+
fields: 100,
|
|
7
|
+
fieldSize: 64 * 1024,
|
|
8
|
+
parts: 110
|
|
9
|
+
})
|
|
10
|
+
|
|
11
|
+
export function addMultipartField (fields, name, value) {
|
|
12
|
+
if (typeof name !== 'string' || !name) throw new RestApiPayloadError('Multipart fields must have a name')
|
|
13
|
+
const array = name.match(/^(.+)\[\d*\]$/)
|
|
14
|
+
const key = array ? array[1] : name
|
|
15
|
+
if (fields.has(key)) {
|
|
16
|
+
fields.set(key, [...[].concat(fields.get(key)), value])
|
|
17
|
+
} else fields.set(key, array ? [value] : value)
|
|
18
|
+
}
|
|
@@ -3,9 +3,18 @@
|
|
|
3
3
|
* Used by both HTTP and Express plugins
|
|
4
4
|
*/
|
|
5
5
|
|
|
6
|
+
/**
|
|
7
|
+
* Copy HTTP header fields, including repeated-value arrays, for a separate view.
|
|
8
|
+
* @param {import('node:http').IncomingHttpHeaders} [headers]
|
|
9
|
+
* @returns {import('node:http').IncomingHttpHeaders}
|
|
10
|
+
*/
|
|
11
|
+
export function cloneRequestHeaders (headers = {}) {
|
|
12
|
+
return Object.fromEntries(Object.entries(headers).map(([name, value]) => [name, Array.isArray(value) ? [...value] : value]))
|
|
13
|
+
}
|
|
14
|
+
|
|
6
15
|
/**
|
|
7
16
|
* Extract the real client IP address, handling proxies
|
|
8
|
-
* @param {Object} req - The request object
|
|
17
|
+
* @param {Object} req - The request object
|
|
9
18
|
* @returns {string} The client IP address
|
|
10
19
|
*/
|
|
11
20
|
export function getClientIP (req) {
|