json-rest-api 1.0.29 → 2.1.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 +733 -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 +103 -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 +3838 -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 +37 -0
- package/index.js +3 -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 +70 -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 +586 -418
- 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 +120 -56
- package/plugins/core/lib/anyapi/stored-definition.js +109 -0
- 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 +290 -2475
- 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/anyapi.d.ts +30 -0
- 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 +65 -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,9 +1,13 @@
|
|
|
1
1
|
import { createContext } from './lib/request-helpers.js'
|
|
2
|
-
import {
|
|
3
|
-
import {
|
|
2
|
+
import { buildTransportRouteSchema, getTransportRouteContract } from './lib/transport-route-schemas.js'
|
|
3
|
+
import { validateRequestContractOrThrow } from '../lib/querying-writing/request-contracts.js'
|
|
4
4
|
import {
|
|
5
5
|
isWriteMethod,
|
|
6
6
|
isAllowedWriteContentType,
|
|
7
|
+
mergeResponseHeaders,
|
|
8
|
+
acceptsJsonApi,
|
|
9
|
+
getNotAcceptableErrorBody,
|
|
10
|
+
getNotFoundErrorBody,
|
|
7
11
|
getUnsupportedMediaTypeErrorBody
|
|
8
12
|
} from './lib/transport-http-helpers.js'
|
|
9
13
|
import {
|
|
@@ -11,59 +15,55 @@ import {
|
|
|
11
15
|
buildTransportRequestData,
|
|
12
16
|
createConnectorContext,
|
|
13
17
|
runTransportRequestLifecycle,
|
|
18
|
+
applyTransportResponseLifecycle,
|
|
19
|
+
addWriteOutcomeToHttpErrors,
|
|
14
20
|
buildTransportRejectionBody,
|
|
15
21
|
executeConnectorRoute,
|
|
22
|
+
logHttpRequestError,
|
|
16
23
|
handleConnectorError
|
|
17
24
|
} from './lib/connector-core.js'
|
|
18
25
|
|
|
19
26
|
function applyHeaders (reply, headers = {}) {
|
|
20
|
-
|
|
27
|
+
const merged = mergeResponseHeaders({ vary: reply.getHeader('vary') }, headers, { vary: 'Accept' })
|
|
28
|
+
for (const [headerName, headerValue] of Object.entries(merged)) {
|
|
21
29
|
reply.header(headerName, headerValue)
|
|
22
30
|
}
|
|
23
31
|
}
|
|
24
32
|
|
|
25
|
-
function
|
|
26
|
-
|
|
27
|
-
|
|
28
|
-
|
|
29
|
-
|
|
30
|
-
|
|
31
|
-
app.addContentTypeParser(
|
|
32
|
-
'application/vnd.api+json',
|
|
33
|
-
{ parseAs: 'string' },
|
|
34
|
-
(request, body, done) => {
|
|
35
|
-
if (body === '' || body === undefined || body === null) {
|
|
36
|
-
done(null, {})
|
|
37
|
-
return
|
|
38
|
-
}
|
|
33
|
+
async function setJsonApiResponseType (request, reply, payload) {
|
|
34
|
+
// Fastify adds a charset during serialization; normalize after that step.
|
|
35
|
+
reply.header('Content-Type', 'application/vnd.api+json')
|
|
36
|
+
return payload
|
|
37
|
+
}
|
|
39
38
|
|
|
40
|
-
|
|
41
|
-
|
|
42
|
-
|
|
43
|
-
|
|
44
|
-
|
|
45
|
-
|
|
39
|
+
function registerJsonParsers (app) {
|
|
40
|
+
const parse = (request, body, done) => {
|
|
41
|
+
try { done(null, body === '' ? {} : JSON.parse(body)) } catch (error) {
|
|
42
|
+
error.statusCode = 400
|
|
43
|
+
error.type = 'entity.parse.failed'
|
|
44
|
+
done(error)
|
|
46
45
|
}
|
|
47
|
-
|
|
46
|
+
}
|
|
47
|
+
for (const contentType of ['application/json', 'application/vnd.api+json']) {
|
|
48
|
+
if (app.hasContentTypeParser(contentType)) app.removeContentTypeParser(contentType)
|
|
49
|
+
app.addContentTypeParser(contentType, { parseAs: 'string' }, parse)
|
|
50
|
+
}
|
|
48
51
|
}
|
|
49
52
|
|
|
50
53
|
export const FastifyPlugin = {
|
|
51
54
|
name: 'fastify',
|
|
52
55
|
dependencies: ['rest-api'],
|
|
53
56
|
|
|
54
|
-
async install ({ vars, helpers, pluginOptions, log, api, runHooks, addHook }) {
|
|
57
|
+
async install ({ vars, helpers, pluginOptions, log, api, scopes, runHooks, addHook }) {
|
|
55
58
|
const fastifyOptions = pluginOptions || {}
|
|
59
|
+
const httpValidators = fastifyOptions.httpValidators ?? false
|
|
60
|
+
if (typeof httpValidators !== 'boolean') throw new TypeError('httpValidators must be a boolean')
|
|
56
61
|
const app = fastifyOptions.app
|
|
57
62
|
|
|
58
63
|
if (!app || typeof app.route !== 'function') {
|
|
59
64
|
throw new Error('FastifyPlugin requires a Fastify instance in pluginOptions.app.')
|
|
60
65
|
}
|
|
61
66
|
|
|
62
|
-
const enhancedLog = createEnhancedLogger(log, {
|
|
63
|
-
logFullErrors: true,
|
|
64
|
-
includeStack: true
|
|
65
|
-
})
|
|
66
|
-
|
|
67
67
|
if (!api.http) {
|
|
68
68
|
api.http = {}
|
|
69
69
|
}
|
|
@@ -80,19 +80,50 @@ export const FastifyPlugin = {
|
|
|
80
80
|
publicBaseUrl
|
|
81
81
|
}
|
|
82
82
|
|
|
83
|
-
|
|
83
|
+
const ensureContext = (request, reply) => {
|
|
84
|
+
if (!request.jsonRestContext) {
|
|
85
|
+
const requestData = buildTransportRequestData({
|
|
86
|
+
method: request.method,
|
|
87
|
+
url: request.url,
|
|
88
|
+
path: request.url,
|
|
89
|
+
headers: request.headers,
|
|
90
|
+
body: request.body,
|
|
91
|
+
params: request.params,
|
|
92
|
+
query: request.query
|
|
93
|
+
})
|
|
94
|
+
const { context } = createConnectorContext({
|
|
95
|
+
request, reply, source: 'fastify', mountPath, publicBaseUrl, requestData, createContext
|
|
96
|
+
})
|
|
97
|
+
request.jsonRestContext = context
|
|
98
|
+
}
|
|
99
|
+
return request.jsonRestContext
|
|
100
|
+
}
|
|
101
|
+
const sendResponse = async (request, reply, status, body) => {
|
|
102
|
+
const context = ensureContext(request, reply)
|
|
103
|
+
body = addWriteOutcomeToHttpErrors(body, context)
|
|
104
|
+
const headers = await applyTransportResponseLifecycle({ context, transportData: context.transport, status, body, runHooks })
|
|
105
|
+
applyHeaders(reply, headers)
|
|
106
|
+
return reply.code(status).type('application/vnd.api+json').send(body)
|
|
107
|
+
}
|
|
84
108
|
|
|
85
109
|
const buildFastifyHandler = ({ method, path, handler, routeMeta }) => {
|
|
86
110
|
const routeSchema = buildTransportRouteSchema({ routeMeta, api })
|
|
87
111
|
|
|
88
112
|
const fastifyErrorHandler = async (error, request, reply) => {
|
|
89
|
-
|
|
113
|
+
const context = ensureContext(request, reply)
|
|
114
|
+
await logHttpRequestError({
|
|
115
|
+
error,
|
|
116
|
+
context,
|
|
117
|
+
log,
|
|
118
|
+
scopes,
|
|
119
|
+
routeMeta,
|
|
120
|
+
api,
|
|
90
121
|
method: request.method,
|
|
91
|
-
path
|
|
122
|
+
path,
|
|
123
|
+
message: 'Fastify request error'
|
|
92
124
|
})
|
|
93
125
|
|
|
94
|
-
const
|
|
95
|
-
const transportData = context?.transport || null
|
|
126
|
+
const transportData = context.transport
|
|
96
127
|
const { status, body, headers } = await handleConnectorError({
|
|
97
128
|
error,
|
|
98
129
|
context,
|
|
@@ -107,65 +138,54 @@ export const FastifyPlugin = {
|
|
|
107
138
|
}
|
|
108
139
|
|
|
109
140
|
const preValidation = async (request, reply) => {
|
|
110
|
-
|
|
111
|
-
|
|
141
|
+
const context = ensureContext(request, reply)
|
|
142
|
+
const { rejected, handled } = await runTransportRequestLifecycle({ context, runHooks })
|
|
143
|
+
if (rejected) {
|
|
144
|
+
return sendResponse(request, reply, context.rejection.status || 500, buildTransportRejectionBody(context))
|
|
112
145
|
}
|
|
146
|
+
if (handled) return
|
|
113
147
|
|
|
114
|
-
|
|
115
|
-
if (
|
|
116
|
-
return
|
|
148
|
+
applyHeaders(reply)
|
|
149
|
+
if (!acceptsJsonApi(request.headers?.accept)) {
|
|
150
|
+
return sendResponse(request, reply, 406, getNotAcceptableErrorBody())
|
|
117
151
|
}
|
|
152
|
+
}
|
|
118
153
|
|
|
119
|
-
|
|
120
|
-
|
|
121
|
-
|
|
154
|
+
const preParsing = async (request, reply, payload) => {
|
|
155
|
+
if (strictContentType && isWriteMethod(request.method) && !isAllowedWriteContentType(request.headers?.['content-type'])) {
|
|
156
|
+
await sendResponse(request, reply, 415, getUnsupportedMediaTypeErrorBody())
|
|
157
|
+
return
|
|
158
|
+
}
|
|
159
|
+
return payload
|
|
122
160
|
}
|
|
123
161
|
|
|
124
162
|
return {
|
|
125
163
|
method,
|
|
126
164
|
url: path,
|
|
127
|
-
...(routeSchema
|
|
165
|
+
...(routeSchema
|
|
166
|
+
? {
|
|
167
|
+
schema: routeSchema,
|
|
168
|
+
// Use the existing request contract without Ajv's field removal or coercion.
|
|
169
|
+
validatorCompiler: () => (payload) => {
|
|
170
|
+
try {
|
|
171
|
+
const contract = getTransportRouteContract({ routeMeta, api })
|
|
172
|
+
return { value: validateRequestContractOrThrow(contract, payload) }
|
|
173
|
+
} catch (error) { return { error } }
|
|
174
|
+
}
|
|
175
|
+
}
|
|
176
|
+
: {}),
|
|
128
177
|
errorHandler: fastifyErrorHandler,
|
|
178
|
+
onSend: setJsonApiResponseType,
|
|
179
|
+
preParsing,
|
|
129
180
|
preValidation,
|
|
130
181
|
handler: async (request, reply) => {
|
|
131
|
-
const
|
|
132
|
-
|
|
133
|
-
|
|
134
|
-
path: request.url,
|
|
135
|
-
headers: request.headers,
|
|
136
|
-
body: request.body,
|
|
137
|
-
params: request.params,
|
|
138
|
-
query: request.query
|
|
139
|
-
})
|
|
140
|
-
const { context, transportData } = createConnectorContext({
|
|
141
|
-
request,
|
|
142
|
-
reply,
|
|
143
|
-
source: 'fastify',
|
|
144
|
-
mountPath,
|
|
145
|
-
publicBaseUrl,
|
|
146
|
-
requestData,
|
|
147
|
-
createContext
|
|
148
|
-
})
|
|
149
|
-
request.jsonRestContext = context
|
|
150
|
-
|
|
151
|
-
const { rejected, handled } = await runTransportRequestLifecycle({
|
|
152
|
-
context,
|
|
153
|
-
runHooks
|
|
154
|
-
})
|
|
155
|
-
|
|
156
|
-
if (rejected) {
|
|
157
|
-
const rejectionBody = buildTransportRejectionBody(context)
|
|
158
|
-
applyHeaders(reply, transportData.response.headers)
|
|
159
|
-
reply.code(context.rejection.status || 500)
|
|
160
|
-
reply.type('application/vnd.api+json')
|
|
161
|
-
return reply.send(rejectionBody)
|
|
162
|
-
}
|
|
163
|
-
|
|
164
|
-
if (handled) {
|
|
165
|
-
return
|
|
166
|
-
}
|
|
182
|
+
const context = request.jsonRestContext
|
|
183
|
+
if (context.handled) return
|
|
184
|
+
const transportData = context.transport
|
|
167
185
|
|
|
168
186
|
const outcome = await executeConnectorRoute({
|
|
187
|
+
api,
|
|
188
|
+
httpValidators,
|
|
169
189
|
method,
|
|
170
190
|
handler,
|
|
171
191
|
queryString: extractQueryString(request?.raw?.url || request?.url || ''),
|
|
@@ -199,9 +219,32 @@ export const FastifyPlugin = {
|
|
|
199
219
|
}
|
|
200
220
|
}
|
|
201
221
|
|
|
222
|
+
const pendingRoutes = []
|
|
223
|
+
let registerRoute = route => pendingRoutes.push(route)
|
|
224
|
+
app.register(async (instance) => {
|
|
225
|
+
registerJsonParsers(instance)
|
|
226
|
+
registerRoute = route => instance.route(route)
|
|
227
|
+
for (const route of pendingRoutes.splice(0)) registerRoute(route)
|
|
228
|
+
|
|
229
|
+
if (fastifyOptions.handle404 !== false) {
|
|
230
|
+
instance.register(async (fallback) => {
|
|
231
|
+
const route = buildFastifyHandler({
|
|
232
|
+
method: 'GET',
|
|
233
|
+
handler: async ({ context }) => ({
|
|
234
|
+
statusCode: 404,
|
|
235
|
+
body: getNotFoundErrorBody(context.transport.request.method, context.transport.request.path.split('?')[0])
|
|
236
|
+
})
|
|
237
|
+
})
|
|
238
|
+
fallback.setErrorHandler(route.errorHandler)
|
|
239
|
+
fallback.addHook('onSend', setJsonApiResponseType)
|
|
240
|
+
fallback.addHook('preParsing', route.preParsing)
|
|
241
|
+
fallback.setNotFoundHandler({ preValidation: route.preValidation }, route.handler)
|
|
242
|
+
}, { prefix: mountPath })
|
|
243
|
+
}
|
|
244
|
+
})
|
|
245
|
+
|
|
202
246
|
addHook('addRoute', 'fastifyRouteCreator', {}, async ({ context }) => {
|
|
203
|
-
|
|
204
|
-
app.route(route)
|
|
247
|
+
registerRoute(buildFastifyHandler(context))
|
|
205
248
|
log.trace(`Fastify route created: ${context.method} ${context.path}`)
|
|
206
249
|
})
|
|
207
250
|
|
|
@@ -1,174 +1,92 @@
|
|
|
1
|
-
|
|
2
|
-
|
|
3
|
-
|
|
4
|
-
|
|
5
|
-
|
|
6
|
-
|
|
7
|
-
|
|
8
|
-
|
|
9
|
-
* - Configurable file size limits
|
|
10
|
-
* - Automatic field parsing
|
|
11
|
-
* - No temporary files by default (keeps in memory)
|
|
12
|
-
*
|
|
13
|
-
* Usage:
|
|
14
|
-
* ```javascript
|
|
15
|
-
* import { createBusboyDetector } from 'jsonrestapi/plugins/core/lib/busboy-detector.js';
|
|
16
|
-
*
|
|
17
|
-
* api.use(HttpPlugin, {
|
|
18
|
-
* fileParser: 'busboy',
|
|
19
|
-
* fileParserOptions: {
|
|
20
|
-
* limits: { fileSize: 10 * 1024 * 1024 } // 10MB
|
|
21
|
-
* }
|
|
22
|
-
* });
|
|
23
|
-
* ```
|
|
24
|
-
*/
|
|
25
|
-
|
|
26
|
-
import { requirePackage } from 'hooked-api'
|
|
27
|
-
import { Readable } from 'stream'
|
|
28
|
-
|
|
29
|
-
let Busboy
|
|
30
|
-
try {
|
|
31
|
-
Busboy = (await import('busboy')).default
|
|
32
|
-
} catch (e) {
|
|
33
|
-
requirePackage('busboy', 'express/http-connector',
|
|
34
|
-
'Busboy is required for multipart/form-data file uploads. This is a peer dependency.')
|
|
1
|
+
import { throwMissingPackage } from '../../../../lib/missing-package.js'
|
|
2
|
+
import { RestApiPayloadError } from '../../../../lib/rest-api-errors.js'
|
|
3
|
+
import { isMultipartContentType } from './transport-http-helpers.js'
|
|
4
|
+
import { addMultipartField, multipartLimits } from './multipart-helpers.js'
|
|
5
|
+
|
|
6
|
+
let busboyFactory
|
|
7
|
+
try { busboyFactory = (await import('busboy')).default } catch {
|
|
8
|
+
throwMissingPackage('busboy', 'express-connector', 'Busboy is required for multipart uploads. Install the optional busboy peer.')
|
|
35
9
|
}
|
|
36
10
|
|
|
37
|
-
/**
|
|
38
|
-
* Creates a busboy-based file detector
|
|
39
|
-
*
|
|
40
|
-
* @param {Object} options - Busboy configuration options
|
|
41
|
-
* @param {Object} options.limits - Size limits
|
|
42
|
-
* @param {number} options.limits.fileSize - Max file size in bytes
|
|
43
|
-
* @param {number} options.limits.files - Max number of files
|
|
44
|
-
* @param {number} options.limits.fields - Max number of fields
|
|
45
|
-
* @returns {Object} Detector object with detect() and parse() methods
|
|
46
|
-
*/
|
|
11
|
+
/** Parse a streamed multipart request into buffered files and text fields. */
|
|
47
12
|
export function createBusboyDetector (options = {}) {
|
|
13
|
+
const limits = { ...multipartLimits, ...options.limits }
|
|
48
14
|
return {
|
|
49
15
|
name: 'busboy-multipart',
|
|
50
|
-
|
|
51
|
-
/**
|
|
52
|
-
* Check if this detector can handle the request
|
|
53
|
-
* @param {Object} params - Request parameters
|
|
54
|
-
* @returns {boolean} True if this is a multipart request
|
|
55
|
-
*/
|
|
56
|
-
detect: (params) => {
|
|
57
|
-
const req = params._httpReq || params._expressReq
|
|
58
|
-
if (!req || !req.headers) return false
|
|
59
|
-
|
|
60
|
-
const contentType = req.headers['content-type'] || ''
|
|
61
|
-
return contentType.includes('multipart/form-data')
|
|
62
|
-
},
|
|
63
|
-
|
|
64
|
-
/**
|
|
65
|
-
* Parse multipart data from the request
|
|
66
|
-
* @param {Object} params - Request parameters
|
|
67
|
-
* @returns {Promise<{fields: Object, files: Object}>} Parsed data
|
|
68
|
-
*/
|
|
16
|
+
detect: params => isMultipartContentType((params._httpReq || params._expressReq)?.headers?.['content-type']),
|
|
69
17
|
parse: async (params) => {
|
|
70
18
|
const req = params._httpReq || params._expressReq
|
|
71
|
-
|
|
19
|
+
if (!req?.pipe || req.aborted) throw new RestApiPayloadError('Multipart request is unavailable or aborted')
|
|
72
20
|
return new Promise((resolve, reject) => {
|
|
73
|
-
|
|
74
|
-
|
|
75
|
-
|
|
21
|
+
let parser
|
|
22
|
+
const fields = new Map()
|
|
23
|
+
const files = new Map()
|
|
24
|
+
const fileNames = new Set()
|
|
25
|
+
let failure
|
|
26
|
+
const cleanup = () => {
|
|
27
|
+
req.removeListener('aborted', onAborted)
|
|
28
|
+
req.removeListener('error', onError)
|
|
29
|
+
}
|
|
30
|
+
const fail = error => {
|
|
31
|
+
if (failure) return
|
|
32
|
+
failure = error
|
|
33
|
+
fields.clear()
|
|
34
|
+
files.clear()
|
|
35
|
+
req.unpipe(parser)
|
|
36
|
+
req.resume()
|
|
37
|
+
cleanup()
|
|
38
|
+
// A limit event can run inside Busboy's current write; destroy afterwards.
|
|
39
|
+
queueMicrotask(() => parser?.destroy())
|
|
40
|
+
reject(error)
|
|
41
|
+
}
|
|
42
|
+
const onError = cause => fail(new RestApiPayloadError('Malformed multipart request', { cause }))
|
|
43
|
+
const onAborted = () => fail(new RestApiPayloadError('Multipart request aborted'))
|
|
44
|
+
const limitError = field => new RestApiPayloadError(`Multipart limit exceeded: ${field}`, { path: field, statusCode: 413 })
|
|
45
|
+
try {
|
|
46
|
+
parser = busboyFactory({
|
|
47
|
+
defParamCharset: 'utf8',
|
|
48
|
+
...options,
|
|
49
|
+
headers: req.headers,
|
|
50
|
+
// Busboy reports truncation when a limit is reached, including exact size.
|
|
51
|
+
limits: { ...limits, fileSize: limits.fileSize + 1, fieldSize: limits.fieldSize + 1, parts: limits.parts + 1 }
|
|
52
|
+
})
|
|
53
|
+
} catch (cause) { reject(new RestApiPayloadError('Invalid multipart headers or boundary', { cause })); return }
|
|
54
|
+
req.once('aborted', onAborted)
|
|
55
|
+
req.once('error', onError)
|
|
56
|
+
parser.on('field', (name, value, info) => {
|
|
57
|
+
if (failure) return
|
|
58
|
+
if (info.valueTruncated || info.nameTruncated || Buffer.byteLength(value) > limits.fieldSize) return fail(limitError(name))
|
|
59
|
+
try { addMultipartField(fields, name, value) } catch (error) { fail(error) }
|
|
76
60
|
})
|
|
77
|
-
|
|
78
|
-
|
|
79
|
-
|
|
80
|
-
|
|
81
|
-
|
|
82
|
-
|
|
83
|
-
|
|
84
|
-
// Handle array notation (field[] or field[0])
|
|
85
|
-
const arrayMatch = fieldname.match(/^(.+)\[\d*\]$/)
|
|
86
|
-
if (arrayMatch) {
|
|
87
|
-
const baseName = arrayMatch[1]
|
|
88
|
-
if (!fields[baseName]) {
|
|
89
|
-
fields[baseName] = []
|
|
90
|
-
}
|
|
91
|
-
if (Array.isArray(fields[baseName])) {
|
|
92
|
-
fields[baseName].push(val)
|
|
93
|
-
}
|
|
94
|
-
} else {
|
|
95
|
-
fields[fieldname] = val
|
|
61
|
+
parser.on('file', (name, stream, { filename, encoding, mimeType }) => {
|
|
62
|
+
stream.on('error', onError)
|
|
63
|
+
if (failure) { stream.resume(); return }
|
|
64
|
+
if (fileNames.has(name)) {
|
|
65
|
+
stream.resume()
|
|
66
|
+
fail(new RestApiPayloadError(`Multiple files for field '${name}' are not supported`, { path: name }))
|
|
67
|
+
return
|
|
96
68
|
}
|
|
97
|
-
|
|
98
|
-
|
|
99
|
-
// Handle files
|
|
100
|
-
busboy.on('file', (fieldname, file, filename, encoding, mimetype) => {
|
|
69
|
+
fileNames.add(name)
|
|
101
70
|
const chunks = []
|
|
102
71
|
let size = 0
|
|
103
|
-
|
|
104
|
-
|
|
105
|
-
|
|
106
|
-
|
|
107
|
-
|
|
108
|
-
|
|
109
|
-
|
|
110
|
-
|
|
111
|
-
|
|
112
|
-
})
|
|
113
|
-
|
|
114
|
-
file.on('end', () => {
|
|
115
|
-
files[fieldname] = {
|
|
116
|
-
filename,
|
|
117
|
-
mimetype,
|
|
118
|
-
encoding,
|
|
119
|
-
size,
|
|
120
|
-
data: Buffer.concat(chunks)
|
|
121
|
-
}
|
|
122
|
-
resolve()
|
|
123
|
-
})
|
|
124
|
-
|
|
125
|
-
file.on('error', reject)
|
|
72
|
+
stream.on('data', chunk => {
|
|
73
|
+
if (failure) return
|
|
74
|
+
size += chunk.length
|
|
75
|
+
if (size > limits.fileSize) { fail(limitError(name)); return }
|
|
76
|
+
chunks.push(chunk)
|
|
77
|
+
})
|
|
78
|
+
stream.on('limit', () => fail(limitError(name)))
|
|
79
|
+
stream.on('end', () => {
|
|
80
|
+
if (!failure) files.set(name, { filename, encoding, mimetype: mimeType, size, data: Buffer.concat(chunks) })
|
|
126
81
|
})
|
|
127
|
-
|
|
128
|
-
filePromises.push(filePromise)
|
|
129
|
-
})
|
|
130
|
-
|
|
131
|
-
// Handle completion
|
|
132
|
-
busboy.on('finish', async () => {
|
|
133
|
-
try {
|
|
134
|
-
// Wait for all files to be fully read
|
|
135
|
-
await Promise.all(filePromises)
|
|
136
|
-
resolve({ fields, files })
|
|
137
|
-
} catch (error) {
|
|
138
|
-
reject(error)
|
|
139
|
-
}
|
|
140
|
-
})
|
|
141
|
-
|
|
142
|
-
// Handle errors
|
|
143
|
-
busboy.on('error', (error) => {
|
|
144
|
-
reject(error)
|
|
145
|
-
})
|
|
146
|
-
|
|
147
|
-
// Handle limit errors
|
|
148
|
-
busboy.on('partsLimit', () => {
|
|
149
|
-
reject(new Error('Parts limit exceeded'))
|
|
150
|
-
})
|
|
151
|
-
|
|
152
|
-
busboy.on('filesLimit', () => {
|
|
153
|
-
reject(new Error('Files limit exceeded'))
|
|
154
82
|
})
|
|
155
|
-
|
|
156
|
-
|
|
157
|
-
|
|
83
|
+
for (const event of ['partsLimit', 'filesLimit', 'fieldsLimit']) parser.on(event, () => fail(limitError(event)))
|
|
84
|
+
parser.on('error', onError)
|
|
85
|
+
parser.on('close', () => {
|
|
86
|
+
cleanup()
|
|
87
|
+
if (!failure) resolve({ fields: Object.fromEntries(fields), files: Object.fromEntries(files) })
|
|
158
88
|
})
|
|
159
|
-
|
|
160
|
-
// Pipe request to busboy
|
|
161
|
-
if (req.pipe) {
|
|
162
|
-
req.pipe(busboy)
|
|
163
|
-
} else if (req.on) {
|
|
164
|
-
// For already buffered requests
|
|
165
|
-
const stream = new Readable()
|
|
166
|
-
stream.push(req.body || req)
|
|
167
|
-
stream.push(null)
|
|
168
|
-
stream.pipe(busboy)
|
|
169
|
-
} else {
|
|
170
|
-
reject(new Error('Request object is not a stream'))
|
|
171
|
-
}
|
|
89
|
+
req.pipe(parser)
|
|
172
90
|
})
|
|
173
91
|
}
|
|
174
92
|
}
|