json-rest-api 1.0.29 → 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 +744 -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} +74 -30
- 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 -177
- 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 +48 -317
- 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 +134 -37
- package/plugins/core/lib/querying/relationship-identifiers.js +208 -0
- package/plugins/core/lib/querying/sort-helpers.js +13 -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 +179 -194
- 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 +100 -290
- 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 +93 -38
- 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 +128 -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 -2485
- 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 -1077
- 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 -395
- 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 -148
- 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 +36 -68
- 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 +37 -81
- 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 -1247
- 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 -148
- 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 -155
- package/tests/fixtures/api-configs.js +0 -2566
- 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-boundaries.test.js +0 -154
- package/tests/pagination-contract.test.js +0 -122
- package/tests/pagination-cursor-multifield.test.js +0 -641
- 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/temporal-boundaries.test.js +0 -208
- package/tests/test-development-guide.md +0 -370
- package/tests/virtual-fields.test.js +0 -404
|
@@ -1,25 +1,16 @@
|
|
|
1
|
-
|
|
2
|
-
|
|
3
|
-
|
|
4
|
-
* This plugin creates HTTP endpoints for your REST API by listening to
|
|
5
|
-
* route registrations from the REST API plugin and creating Express routes.
|
|
6
|
-
*
|
|
7
|
-
* Features:
|
|
8
|
-
* - Automatic route creation via addRoute hook
|
|
9
|
-
* - JSON:API compliant request/response handling
|
|
10
|
-
* - Query parameter parsing
|
|
11
|
-
* - Error mapping to HTTP status codes
|
|
12
|
-
* - Content type validation
|
|
13
|
-
* - Middleware injection points
|
|
14
|
-
* - File upload support with busboy or formidable
|
|
15
|
-
*/
|
|
16
|
-
|
|
17
|
-
import { requirePackage } from 'hooked-api'
|
|
1
|
+
import { throwMissingPackage } from '../../../lib/missing-package.js'
|
|
2
|
+
import onHeaders from 'on-headers'
|
|
3
|
+
import { getOperationDiagnosticContext } from '../../../lib/error-context.js'
|
|
18
4
|
import { createContext } from './lib/request-helpers.js'
|
|
19
5
|
import { createEnhancedLogger } from '../../../lib/enhanced-logger.js'
|
|
20
6
|
import {
|
|
21
7
|
isWriteMethod,
|
|
22
8
|
isAllowedWriteContentType,
|
|
9
|
+
isMultipartContentType,
|
|
10
|
+
mergeResponseHeaders,
|
|
11
|
+
acceptsJsonApi,
|
|
12
|
+
getNotAcceptableErrorBody,
|
|
13
|
+
getNotFoundErrorBody,
|
|
23
14
|
getUnsupportedMediaTypeErrorBody
|
|
24
15
|
} from './lib/transport-http-helpers.js'
|
|
25
16
|
import {
|
|
@@ -27,8 +18,11 @@ import {
|
|
|
27
18
|
buildTransportRequestData,
|
|
28
19
|
createConnectorContext,
|
|
29
20
|
runTransportRequestLifecycle,
|
|
21
|
+
applyTransportResponseLifecycle,
|
|
22
|
+
addWriteOutcomeToHttpErrors,
|
|
30
23
|
buildTransportRejectionBody,
|
|
31
24
|
executeConnectorRoute,
|
|
25
|
+
logHttpRequestError,
|
|
32
26
|
handleConnectorError
|
|
33
27
|
} from './lib/connector-core.js'
|
|
34
28
|
|
|
@@ -36,25 +30,15 @@ export const ExpressPlugin = {
|
|
|
36
30
|
name: 'express',
|
|
37
31
|
dependencies: ['rest-api'],
|
|
38
32
|
|
|
39
|
-
async install ({
|
|
40
|
-
addHook('release', 'releaseHook', {},
|
|
41
|
-
async ({ api }) => {} // TODO: Anything to do here?
|
|
42
|
-
)
|
|
43
|
-
|
|
33
|
+
async install ({ vars, helpers, pluginOptions, log, scopes, api, runHooks, addHook }) {
|
|
44
34
|
// Dynamic import for Express
|
|
45
35
|
let express
|
|
46
36
|
try {
|
|
47
37
|
express = (await import('express')).default
|
|
48
38
|
} catch (e) {
|
|
49
|
-
|
|
39
|
+
throwMissingPackage('express', 'express',
|
|
50
40
|
'Express.js is required for HTTP server functionality. This is a peer dependency.')
|
|
51
41
|
}
|
|
52
|
-
// Enhance the logger
|
|
53
|
-
const enhancedLog = createEnhancedLogger(log, {
|
|
54
|
-
logFullErrors: true,
|
|
55
|
-
includeStack: true
|
|
56
|
-
})
|
|
57
|
-
|
|
58
42
|
// Initialize express namespace
|
|
59
43
|
if (!api.http) {
|
|
60
44
|
api.http = {}
|
|
@@ -62,10 +46,12 @@ export const ExpressPlugin = {
|
|
|
62
46
|
api.http.express = {}
|
|
63
47
|
|
|
64
48
|
const expressOptions = pluginOptions || {}
|
|
49
|
+
const httpValidators = expressOptions.httpValidators ?? false
|
|
50
|
+
if (typeof httpValidators !== 'boolean') throw new TypeError('httpValidators must be a boolean')
|
|
65
51
|
|
|
66
52
|
// Get mountPath from options (this is now a transport concern)
|
|
67
53
|
const mountPath = expressOptions.mountPath || ''
|
|
68
|
-
const basePath = mountPath
|
|
54
|
+
const basePath = mountPath.replace(/\/+$/, '')
|
|
69
55
|
const publicBaseUrl = expressOptions.publicBaseUrl || ''
|
|
70
56
|
const strictContentType = expressOptions.strictContentType !== false
|
|
71
57
|
const requestSizeLimit = expressOptions.requestSizeLimit || '1mb'
|
|
@@ -86,19 +72,16 @@ export const ExpressPlugin = {
|
|
|
86
72
|
let detector
|
|
87
73
|
|
|
88
74
|
if (parserLib === 'busboy') {
|
|
89
|
-
|
|
90
|
-
|
|
91
|
-
detector = createBusboyDetector(parserOptions)
|
|
92
|
-
} catch (e) {
|
|
93
|
-
log.warn('Busboy not installed. Install with: npm install busboy')
|
|
94
|
-
}
|
|
75
|
+
const { createBusboyDetector } = await import('./lib/busboy-detector.js')
|
|
76
|
+
detector = createBusboyDetector(parserOptions)
|
|
95
77
|
} else if (parserLib === 'formidable') {
|
|
96
|
-
|
|
97
|
-
|
|
98
|
-
|
|
99
|
-
|
|
100
|
-
|
|
101
|
-
|
|
78
|
+
const { createFormidableDetector } = await import('./lib/formidable-detector.js')
|
|
79
|
+
detector = createFormidableDetector(parserOptions)
|
|
80
|
+
} else if (typeof parserLib === 'function') detector = await parserLib(parserOptions)
|
|
81
|
+
else throw new Error('fileParser must be busboy, formidable, or a detector factory')
|
|
82
|
+
|
|
83
|
+
if (!detector?.name || typeof detector.detect !== 'function' || typeof detector.parse !== 'function') {
|
|
84
|
+
throw new Error('File detector must have name, detect() and parse()')
|
|
102
85
|
}
|
|
103
86
|
|
|
104
87
|
if (detector) {
|
|
@@ -122,94 +105,125 @@ export const ExpressPlugin = {
|
|
|
122
105
|
const router = expressOptions.router || express.Router()
|
|
123
106
|
const notFoundRouter = express.Router()
|
|
124
107
|
|
|
108
|
+
const isApiPath = path => !basePath || path === basePath || path.startsWith(`${basePath}/`)
|
|
109
|
+
const allowsMultipart = () => expressOptions.enableFileUploads !== false && !!api.rest?.fileDetectors?.length
|
|
110
|
+
const ensureContext = (req, res) => {
|
|
111
|
+
if (!req.context || !req.transportData) {
|
|
112
|
+
const requestData = buildTransportRequestData({
|
|
113
|
+
method: req.method,
|
|
114
|
+
url: req.url,
|
|
115
|
+
path: req.path,
|
|
116
|
+
headers: req.headers,
|
|
117
|
+
body: req.body,
|
|
118
|
+
params: req.params,
|
|
119
|
+
query: req.query
|
|
120
|
+
})
|
|
121
|
+
const { context, transportData } = createConnectorContext({
|
|
122
|
+
request: req,
|
|
123
|
+
reply: res,
|
|
124
|
+
source: 'express',
|
|
125
|
+
mountPath: basePath,
|
|
126
|
+
publicBaseUrl,
|
|
127
|
+
requestData,
|
|
128
|
+
createContext,
|
|
129
|
+
urlPrefixOverride: req.urlPrefixOverride
|
|
130
|
+
})
|
|
131
|
+
req.context = context
|
|
132
|
+
req.transportData = transportData
|
|
133
|
+
}
|
|
134
|
+
return { context: req.context, transportData: req.transportData }
|
|
135
|
+
}
|
|
136
|
+
const applyHeaders = (res, headers) => res.set(mergeResponseHeaders({ vary: res.getHeader('Vary') }, headers, { vary: 'Accept' }))
|
|
137
|
+
const sendResponse = async (req, res, status, body) => {
|
|
138
|
+
const { context, transportData } = ensureContext(req, res)
|
|
139
|
+
body = addWriteOutcomeToHttpErrors(body, context)
|
|
140
|
+
const headers = await applyTransportResponseLifecycle({ context, transportData, status, body, runHooks })
|
|
141
|
+
applyHeaders(res, headers)
|
|
142
|
+
return res.status(status).type('application/vnd.api+json').json(body)
|
|
143
|
+
}
|
|
144
|
+
router.use((req, res, next) => next(isApiPath(req.path) ? undefined : 'router'))
|
|
145
|
+
router.use((req, res, next) => {
|
|
146
|
+
onHeaders(res, function () {
|
|
147
|
+
if (httpValidators && req.method === 'PUT') this.removeHeader('ETag')
|
|
148
|
+
if (String(this.getHeader('Content-Type')).split(';')[0] === 'application/vnd.api+json') {
|
|
149
|
+
// Express adds a charset during JSON serialization; JSON:API forbids it.
|
|
150
|
+
this.setHeader('Content-Type', 'application/vnd.api+json')
|
|
151
|
+
}
|
|
152
|
+
})
|
|
153
|
+
next()
|
|
154
|
+
})
|
|
155
|
+
|
|
156
|
+
// Reject unsupported media types before a body parser can report a syntax error.
|
|
157
|
+
if (strictContentType) {
|
|
158
|
+
router.use((req, res, next) => {
|
|
159
|
+
if (isWriteMethod(req.method) && !isAllowedWriteContentType(req.get('Content-Type'), { allowMultipart: allowsMultipart() })) {
|
|
160
|
+
sendResponse(req, res, 415, getUnsupportedMediaTypeErrorBody({ allowMultipart: allowsMultipart() })).catch(next)
|
|
161
|
+
return
|
|
162
|
+
}
|
|
163
|
+
next()
|
|
164
|
+
})
|
|
165
|
+
}
|
|
166
|
+
|
|
125
167
|
// Add body parsing middleware
|
|
126
168
|
router.use(express.json({
|
|
127
169
|
limit: requestSizeLimit,
|
|
170
|
+
strict: false,
|
|
128
171
|
type: ['application/json', 'application/vnd.api+json']
|
|
129
172
|
}))
|
|
130
173
|
|
|
131
174
|
// Add transport hook middleware
|
|
132
|
-
|
|
133
|
-
const
|
|
134
|
-
method: req.method,
|
|
135
|
-
url: req.url,
|
|
136
|
-
path: req.path,
|
|
137
|
-
headers: req.headers,
|
|
138
|
-
body: req.body,
|
|
139
|
-
params: req.params,
|
|
140
|
-
query: req.query
|
|
141
|
-
})
|
|
142
|
-
const { context, transportData } = createConnectorContext({
|
|
143
|
-
request: req,
|
|
144
|
-
reply: res,
|
|
145
|
-
source: 'express',
|
|
146
|
-
mountPath: basePath,
|
|
147
|
-
publicBaseUrl,
|
|
148
|
-
requestData,
|
|
149
|
-
createContext,
|
|
150
|
-
urlPrefixOverride: req.urlPrefixOverride
|
|
151
|
-
})
|
|
175
|
+
const handleTransportRequest = async (req, res, next) => {
|
|
176
|
+
const { context } = ensureContext(req, res)
|
|
152
177
|
const { rejected, handled } = await runTransportRequestLifecycle({
|
|
153
178
|
context,
|
|
154
179
|
runHooks
|
|
155
180
|
})
|
|
156
181
|
|
|
157
182
|
if (rejected) {
|
|
158
|
-
|
|
159
|
-
res.set(transportData.response.headers)
|
|
160
|
-
}
|
|
161
|
-
return res.status(context.rejection.status || 500).json(
|
|
162
|
-
buildTransportRejectionBody(context)
|
|
163
|
-
)
|
|
183
|
+
return sendResponse(req, res, context.rejection.status || 500, buildTransportRejectionBody(context))
|
|
164
184
|
}
|
|
165
185
|
|
|
166
186
|
if (handled) {
|
|
167
187
|
return
|
|
168
188
|
}
|
|
169
189
|
|
|
170
|
-
|
|
171
|
-
req.
|
|
172
|
-
|
|
173
|
-
|
|
174
|
-
})
|
|
175
|
-
|
|
176
|
-
// Content type validation middleware
|
|
177
|
-
if (strictContentType) {
|
|
178
|
-
router.use((req, res, next) => {
|
|
179
|
-
if (isWriteMethod(req.method)) {
|
|
180
|
-
const contentType = req.get('Content-Type')
|
|
190
|
+
res.vary('Accept')
|
|
191
|
+
if (!acceptsJsonApi(req.get('Accept'))) {
|
|
192
|
+
return sendResponse(req, res, 406, getNotAcceptableErrorBody())
|
|
193
|
+
}
|
|
181
194
|
|
|
182
|
-
|
|
183
|
-
return res.status(415).json(
|
|
184
|
-
getUnsupportedMediaTypeErrorBody({ allowMultipart: true })
|
|
185
|
-
)
|
|
186
|
-
}
|
|
187
|
-
}
|
|
188
|
-
next()
|
|
189
|
-
})
|
|
195
|
+
next()
|
|
190
196
|
}
|
|
197
|
+
router.use((req, res, next) => {
|
|
198
|
+
handleTransportRequest(req, res, next).catch(next)
|
|
199
|
+
})
|
|
191
200
|
|
|
192
201
|
/**
|
|
193
202
|
* Error handler - maps REST API errors to HTTP responses
|
|
194
203
|
*/
|
|
195
|
-
const handleError = async (error, req, res) => {
|
|
196
|
-
|
|
204
|
+
const handleError = async (error, req, res, routeMeta) => {
|
|
205
|
+
const { context, transportData } = ensureContext(req, res)
|
|
206
|
+
await logHttpRequestError({
|
|
207
|
+
error,
|
|
208
|
+
context,
|
|
209
|
+
log,
|
|
210
|
+
scopes,
|
|
211
|
+
routeMeta,
|
|
212
|
+
api,
|
|
197
213
|
method: req.method,
|
|
198
|
-
path: req.path,
|
|
199
|
-
|
|
214
|
+
path: req.route?.path,
|
|
215
|
+
message: 'HTTP request error'
|
|
200
216
|
})
|
|
201
217
|
|
|
202
218
|
const { status, body: errorResponse, headers } = await handleConnectorError({
|
|
203
219
|
error,
|
|
204
|
-
context
|
|
205
|
-
transportData
|
|
220
|
+
context,
|
|
221
|
+
transportData,
|
|
206
222
|
runHooks
|
|
207
223
|
})
|
|
208
224
|
|
|
209
|
-
|
|
210
|
-
|
|
211
|
-
}
|
|
212
|
-
res.status(status).json(errorResponse)
|
|
225
|
+
applyHeaders(res, headers)
|
|
226
|
+
res.status(status).type('application/vnd.api+json').json(errorResponse)
|
|
213
227
|
}
|
|
214
228
|
|
|
215
229
|
/**
|
|
@@ -222,45 +236,21 @@ export const ExpressPlugin = {
|
|
|
222
236
|
const beforeMiddleware = expressOptions.middleware?.beforeAll || []
|
|
223
237
|
|
|
224
238
|
try {
|
|
225
|
-
// Extract the handler logic into a shared function to keep it DRY (Don't Repeat Yourself).
|
|
226
239
|
const expressHandler = async (req, res) => {
|
|
227
240
|
try {
|
|
228
|
-
|
|
229
|
-
let transportData = req.transportData
|
|
230
|
-
|
|
231
|
-
if (!context || !transportData) {
|
|
232
|
-
const requestData = buildTransportRequestData({
|
|
233
|
-
method: req.method,
|
|
234
|
-
url: req.url,
|
|
235
|
-
path: req.path,
|
|
236
|
-
headers: req.headers,
|
|
237
|
-
body: req.body,
|
|
238
|
-
params: req.params,
|
|
239
|
-
query: req.query
|
|
240
|
-
})
|
|
241
|
-
const setup = createConnectorContext({
|
|
242
|
-
request: req,
|
|
243
|
-
reply: res,
|
|
244
|
-
source: 'express',
|
|
245
|
-
mountPath: basePath,
|
|
246
|
-
publicBaseUrl,
|
|
247
|
-
requestData,
|
|
248
|
-
createContext,
|
|
249
|
-
urlPrefixOverride: req.urlPrefixOverride
|
|
250
|
-
})
|
|
251
|
-
context = setup.context
|
|
252
|
-
transportData = setup.transportData
|
|
253
|
-
req.context = context
|
|
254
|
-
req.transportData = transportData
|
|
255
|
-
}
|
|
241
|
+
const { context, transportData } = ensureContext(req, res)
|
|
256
242
|
|
|
257
243
|
const outcome = await executeConnectorRoute({
|
|
244
|
+
api,
|
|
245
|
+
httpValidators,
|
|
258
246
|
method: req.method,
|
|
259
247
|
handler,
|
|
260
248
|
queryString: extractQueryString(req.url),
|
|
261
249
|
headers: req.headers,
|
|
262
250
|
params: req.params,
|
|
263
|
-
body: req.
|
|
251
|
+
body: isMultipartContentType(req.get('Content-Type')) && routeMeta?.kind === 'resource' && ['post', 'put', 'patch'].includes(routeMeta.operation)
|
|
252
|
+
? { data: { type: routeMeta.scopeName, attributes: {} } }
|
|
253
|
+
: req.body,
|
|
264
254
|
context,
|
|
265
255
|
transportData,
|
|
266
256
|
routeMeta,
|
|
@@ -269,86 +259,52 @@ export const ExpressPlugin = {
|
|
|
269
259
|
publicBaseUrl,
|
|
270
260
|
runHooks
|
|
271
261
|
})
|
|
272
|
-
res
|
|
262
|
+
applyHeaders(res, outcome.headers)
|
|
273
263
|
|
|
274
|
-
// Set content type
|
|
275
264
|
res.set('Content-Type', 'application/vnd.api+json')
|
|
276
265
|
|
|
277
266
|
if (outcome.location) {
|
|
278
267
|
res.set('Location', outcome.location)
|
|
279
268
|
}
|
|
280
269
|
|
|
281
|
-
// Handle response based on status
|
|
282
270
|
if (outcome.status === 204) {
|
|
283
271
|
res.sendStatus(204)
|
|
272
|
+
} else if (outcome.serialized) {
|
|
273
|
+
res.status(outcome.status).send(outcome.body)
|
|
284
274
|
} else {
|
|
285
275
|
res.status(outcome.status).json(outcome.body)
|
|
286
276
|
}
|
|
287
277
|
} catch (error) {
|
|
288
|
-
handleError(error, req, res)
|
|
278
|
+
await handleError(error, req, res, routeMeta)
|
|
289
279
|
}
|
|
290
280
|
}
|
|
291
281
|
|
|
292
|
-
// CRITICAL: Express routing method selection - wildcard vs specific routes
|
|
293
|
-
//
|
|
294
|
-
// Express provides two different ways to register routes:
|
|
295
|
-
// 1. router.METHOD(path, handler) - e.g., router.get('/users', handler)
|
|
296
|
-
// - Only responds to the SPECIFIC HTTP method
|
|
297
|
-
// - Perfect for normal REST endpoints
|
|
298
|
-
//
|
|
299
|
-
// 2. router.use(path, handler)
|
|
300
|
-
// - Responds to ALL HTTP methods
|
|
301
|
-
// - Needed for wildcard paths that must handle any method
|
|
302
|
-
//
|
|
303
|
-
// The CORS plugin needs wildcard routes because it must handle OPTIONS
|
|
304
|
-
// requests for ANY path under the API prefix, even paths that don't exist
|
|
305
|
-
// as defined routes. For example:
|
|
306
|
-
// - Defined route: GET /api/users
|
|
307
|
-
// - Browser might send: OPTIONS /api/users/invalid/path
|
|
308
|
-
// - CORS must still respond with proper headers
|
|
309
|
-
//
|
|
310
282
|
if (path === vars.transport.matchAll) {
|
|
311
|
-
//
|
|
312
|
-
// We MUST use router.use() because:
|
|
313
|
-
// - We need to catch ALL paths (using '*' or '/api/*')
|
|
314
|
-
// - We need to handle a SPECIFIC method (e.g., OPTIONS)
|
|
315
|
-
// - router.options('*') would NOT work for paths like '/api/some/nested/path'
|
|
316
|
-
|
|
317
|
-
// Since router.use() responds to ALL methods, we need a wrapper
|
|
318
|
-
// that only handles our specific method (e.g., OPTIONS)
|
|
283
|
+
// CORS must also match unknown paths; router.use needs an explicit method check.
|
|
319
284
|
const methodSpecificMiddleware = (req, res, next) => {
|
|
320
285
|
if (req.method.toLowerCase() === method.toLowerCase()) {
|
|
321
286
|
expressHandler(req, res)
|
|
322
287
|
} else {
|
|
323
|
-
// Not our method, pass to next middleware
|
|
324
288
|
next()
|
|
325
289
|
}
|
|
326
290
|
}
|
|
327
|
-
// IMPORTANT: We do NOT need to pass a path to router.use() here!
|
|
328
|
-
// When no path is provided, router.use() matches ALL requests
|
|
329
|
-
// This is exactly what we want for wildcard routes
|
|
330
291
|
router.use(...beforeMiddleware, methodSpecificMiddleware)
|
|
331
292
|
} else {
|
|
332
|
-
//
|
|
333
|
-
// We use router.METHOD() because:
|
|
334
|
-
// - We want to respond to ONLY this specific HTTP method
|
|
335
|
-
// - The path is exact, not a wildcard
|
|
336
|
-
// - This is more efficient than router.use() with method checking
|
|
337
|
-
//
|
|
338
|
-
// Note: Routes from RestApiPlugin already include the full path with mountPath
|
|
339
|
-
// So we use them as-is without adding basePath to avoid double-prefixing
|
|
340
|
-
|
|
293
|
+
// Resource routes already contain the mount path.
|
|
341
294
|
router[method.toLowerCase()](path, ...beforeMiddleware, expressHandler)
|
|
342
|
-
|
|
343
|
-
// Debug logging for route registration
|
|
344
295
|
}
|
|
345
296
|
} catch (routeError) {
|
|
346
|
-
|
|
347
|
-
|
|
348
|
-
|
|
349
|
-
|
|
350
|
-
|
|
351
|
-
|
|
297
|
+
try {
|
|
298
|
+
await createEnhancedLogger(log).logError('Express route registration failed', routeError, {
|
|
299
|
+
...getOperationDiagnosticContext(context, {
|
|
300
|
+
phase: 'routeRegistration',
|
|
301
|
+
method,
|
|
302
|
+
scopeName: routeMeta?.scopeName,
|
|
303
|
+
backend: api.knex?.instance?.client?.config?.client
|
|
304
|
+
}),
|
|
305
|
+
path
|
|
306
|
+
})
|
|
307
|
+
} catch { /* Diagnostic failure must not replace the registration failure. */ }
|
|
352
308
|
throw routeError
|
|
353
309
|
}
|
|
354
310
|
|
|
@@ -359,55 +315,28 @@ export const ExpressPlugin = {
|
|
|
359
315
|
})
|
|
360
316
|
|
|
361
317
|
// Apply global middleware if configured
|
|
362
|
-
|
|
318
|
+
const finalRouter = express.Router()
|
|
363
319
|
if (expressOptions.middleware?.beforeAll) {
|
|
364
|
-
|
|
365
|
-
globalRouter.use(...expressOptions.middleware.beforeAll)
|
|
366
|
-
globalRouter.use(router)
|
|
367
|
-
finalRouter = globalRouter
|
|
320
|
+
finalRouter.use(...expressOptions.middleware.beforeAll)
|
|
368
321
|
}
|
|
322
|
+
finalRouter.use(router)
|
|
323
|
+
// Catch parser, request-hook and route-matching errors on Express 4 and 5.
|
|
324
|
+
finalRouter.use((error, req, res, next) => {
|
|
325
|
+
if (res.headersSent) return next(error)
|
|
326
|
+
handleError(error, req, res).catch(next)
|
|
327
|
+
})
|
|
369
328
|
|
|
370
329
|
// Set up 404 handler in separate router (unless disabled)
|
|
371
330
|
if (expressOptions.handle404 !== false) {
|
|
372
|
-
|
|
373
|
-
if (
|
|
374
|
-
|
|
375
|
-
const context = createContext(req, res, 'express')
|
|
376
|
-
const transportData = {
|
|
377
|
-
request: {
|
|
378
|
-
method: req.method,
|
|
379
|
-
url: req.url,
|
|
380
|
-
path: req.path,
|
|
381
|
-
headers: req.headers
|
|
382
|
-
},
|
|
383
|
-
response: {
|
|
384
|
-
headers: {},
|
|
385
|
-
status: 404,
|
|
386
|
-
body: {
|
|
387
|
-
errors: [{
|
|
388
|
-
status: '404',
|
|
389
|
-
title: 'Not Found',
|
|
390
|
-
detail: `The requested endpoint ${req.method} ${req.path} does not exist`
|
|
391
|
-
}]
|
|
392
|
-
}
|
|
393
|
-
}
|
|
394
|
-
}
|
|
395
|
-
|
|
396
|
-
// Add transport data to context
|
|
397
|
-
context.transport = transportData
|
|
398
|
-
|
|
399
|
-
// Run transport:response hook for 404
|
|
400
|
-
await runHooks('transport:response', context)
|
|
401
|
-
|
|
402
|
-
// Apply response headers from hooks
|
|
403
|
-
if (transportData.response.headers) {
|
|
404
|
-
res.set(transportData.response.headers)
|
|
405
|
-
}
|
|
406
|
-
|
|
407
|
-
res.status(404).json(transportData.response.body)
|
|
331
|
+
const handleNotFound = async (req, res, next) => {
|
|
332
|
+
if (isApiPath(req.path)) {
|
|
333
|
+
await sendResponse(req, res, 404, getNotFoundErrorBody(req.method, req.path))
|
|
408
334
|
} else {
|
|
409
335
|
next()
|
|
410
336
|
}
|
|
337
|
+
}
|
|
338
|
+
notFoundRouter.use((req, res, next) => {
|
|
339
|
+
handleNotFound(req, res, next).catch(error => handleError(error, req, res).catch(next))
|
|
411
340
|
})
|
|
412
341
|
}
|
|
413
342
|
|