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,729 +0,0 @@
|
|
|
1
|
-
# File Uploads Guide
|
|
2
|
-
|
|
3
|
-
This guide explains how to handle file uploads in JSON REST API using the FileHandlingPlugin. The system is designed to be protocol-agnostic, storage-pluggable, and schema-driven.
|
|
4
|
-
|
|
5
|
-
## Table of Contents
|
|
6
|
-
|
|
7
|
-
1. [Overview](#overview)
|
|
8
|
-
2. [Quick Start](#quick-start)
|
|
9
|
-
3. [Schema Configuration](#schema-configuration)
|
|
10
|
-
4. [Storage Adapters](#storage-adapters)
|
|
11
|
-
5. [Protocol Configuration](#protocol-configuration)
|
|
12
|
-
6. [File Validation](#file-validation)
|
|
13
|
-
7. [Complete Examples](#complete-examples)
|
|
14
|
-
8. [Troubleshooting](#troubleshooting)
|
|
15
|
-
|
|
16
|
-
## Overview
|
|
17
|
-
|
|
18
|
-
The file handling system consists of three main components:
|
|
19
|
-
|
|
20
|
-
1. **FileHandlingPlugin** - Orchestrates file detection and processing
|
|
21
|
-
2. **Protocol Detectors** - Parse files from different protocols (HTTP, Express)
|
|
22
|
-
3. **Storage Adapters** - Save files to local storage or generate mock S3-style URLs for demos
|
|
23
|
-
|
|
24
|
-
### How It Works
|
|
25
|
-
|
|
26
|
-
1. You define file fields in your schema with `type: 'file'`
|
|
27
|
-
2. Protocol plugins detect and parse multipart uploads
|
|
28
|
-
3. FileHandlingPlugin validates and processes files
|
|
29
|
-
4. Storage adapters save files and return URLs
|
|
30
|
-
5. File fields are replaced with URLs in your data
|
|
31
|
-
|
|
32
|
-
## Quick Start
|
|
33
|
-
|
|
34
|
-
```javascript
|
|
35
|
-
import { Api } from 'hooked-api';
|
|
36
|
-
import { RestApiPlugin, FileHandlingPlugin, ExpressPlugin } from 'json-rest-api';
|
|
37
|
-
import { LocalStorage } from 'json-rest-api/plugins/storage/local-storage.js';
|
|
38
|
-
|
|
39
|
-
// Create API
|
|
40
|
-
const api = new Api({ name: 'my-api'});
|
|
41
|
-
|
|
42
|
-
// Create storage
|
|
43
|
-
const storage = new LocalStorage({
|
|
44
|
-
directory: './uploads',
|
|
45
|
-
baseUrl: 'http://localhost:3000/uploads'
|
|
46
|
-
});
|
|
47
|
-
|
|
48
|
-
// Use plugins (order matters!)
|
|
49
|
-
api.use(RestApiPlugin);
|
|
50
|
-
api.use(FileHandlingPlugin);
|
|
51
|
-
api.use(ExpressPlugin);
|
|
52
|
-
|
|
53
|
-
// Define schema with file field
|
|
54
|
-
api.addScope('images', {
|
|
55
|
-
schema: {
|
|
56
|
-
title: { type: 'string', required: true },
|
|
57
|
-
file: {
|
|
58
|
-
type: 'file',
|
|
59
|
-
storage: storage,
|
|
60
|
-
accepts: ['image/*'],
|
|
61
|
-
maxSize: '10mb'
|
|
62
|
-
}
|
|
63
|
-
}
|
|
64
|
-
});
|
|
65
|
-
```
|
|
66
|
-
|
|
67
|
-
## Schema Configuration
|
|
68
|
-
|
|
69
|
-
File fields are defined in your scope schema with `type: 'file'`:
|
|
70
|
-
|
|
71
|
-
```javascript
|
|
72
|
-
api.addScope('documents', {
|
|
73
|
-
schema: {
|
|
74
|
-
// Regular fields
|
|
75
|
-
title: { type: 'string', required: true },
|
|
76
|
-
description: { type: 'string' },
|
|
77
|
-
|
|
78
|
-
// File field
|
|
79
|
-
attachment: {
|
|
80
|
-
type: 'file',
|
|
81
|
-
storage: myStorage, // Required: storage adapter instance
|
|
82
|
-
accepts: ['*'], // Optional: accepted mime types (default: ['*'])
|
|
83
|
-
maxSize: '50mb', // Optional: max file size
|
|
84
|
-
required: false // Optional: is field required? (default: false)
|
|
85
|
-
}
|
|
86
|
-
}
|
|
87
|
-
});
|
|
88
|
-
```
|
|
89
|
-
|
|
90
|
-
### File Field Options
|
|
91
|
-
|
|
92
|
-
- **type**: Must be `'file'`
|
|
93
|
-
- **storage**: Storage adapter instance (required)
|
|
94
|
-
- **accepts**: Array of accepted MIME types
|
|
95
|
-
- `['*']` - Accept any file type (default)
|
|
96
|
-
- `['image/*']` - Accept any image
|
|
97
|
-
- `['image/jpeg', 'image/png']` - Accept specific types
|
|
98
|
-
- `['application/pdf', 'text/*']` - Mix specific and wildcard
|
|
99
|
-
- **maxSize**: Maximum file size
|
|
100
|
-
- `'10mb'`, `'1.5gb'`, `'500kb'` - Human readable format
|
|
101
|
-
- Number of bytes also supported
|
|
102
|
-
- **required**: Whether the file is required
|
|
103
|
-
|
|
104
|
-
## Storage Adapters
|
|
105
|
-
|
|
106
|
-
Storage adapters handle where and how files are saved. The library includes two built-in adapters.
|
|
107
|
-
|
|
108
|
-
### Storage Adapter Comparison
|
|
109
|
-
|
|
110
|
-
| Feature | LocalStorage | S3Storage |
|
|
111
|
-
|---------|-------------|-----------|
|
|
112
|
-
| **Production Ready** | ✅ Yes | ⚠️ Mock only |
|
|
113
|
-
| **Filename Strategies** | 4 (hash, timestamp, original, custom) | 1 (hash only) |
|
|
114
|
-
| **Path Traversal Protection** | ✅ Full | ✅ N/A |
|
|
115
|
-
| **Extension Whitelist** | ✅ Yes | ❌ No |
|
|
116
|
-
| **Duplicate Handling** | ✅ Yes | ✅ Automatic |
|
|
117
|
-
| **Custom Naming** | ✅ Yes | ❌ No |
|
|
118
|
-
| **Best For** | Local file storage | Tests, examples, S3-style URL demos |
|
|
119
|
-
|
|
120
|
-
### S3Storage (Mock/Demo)
|
|
121
|
-
|
|
122
|
-
Generates S3-style URLs without uploading files to Amazon S3:
|
|
123
|
-
|
|
124
|
-
```javascript
|
|
125
|
-
import { S3Storage } from 'json-rest-api/plugins/storage/s3-storage.js';
|
|
126
|
-
|
|
127
|
-
const s3Storage = new S3Storage({
|
|
128
|
-
bucket: 'my-uploads', // S3 bucket name (required)
|
|
129
|
-
region: 'us-east-1', // AWS region (default: 'us-east-1')
|
|
130
|
-
prefix: 'uploads/', // Path prefix in bucket (default: '')
|
|
131
|
-
acl: 'public-read', // Access control (default: 'public-read')
|
|
132
|
-
mockMode: true // Only supported mode
|
|
133
|
-
});
|
|
134
|
-
```
|
|
135
|
-
|
|
136
|
-
**Filename Handling:**
|
|
137
|
-
- Always generates random hash + extension (e.g., `uploads/a7f8d9e2b4c6e1f3.jpg`)
|
|
138
|
-
- Original filenames are never used for security
|
|
139
|
-
|
|
140
|
-
**Note**: The included S3Storage is a mock implementation for demonstration. `mockMode: false` throws a clear error because real S3 uploads are not implemented. For production use, provide your own storage adapter or add an AWS SDK-backed implementation.
|
|
141
|
-
|
|
142
|
-
### LocalStorage
|
|
143
|
-
|
|
144
|
-
Saves files to the local filesystem with secure filename handling:
|
|
145
|
-
|
|
146
|
-
```javascript
|
|
147
|
-
import { LocalStorage } from 'json-rest-api/plugins/storage/local-storage.js';
|
|
148
|
-
|
|
149
|
-
const localStorage = new LocalStorage({
|
|
150
|
-
directory: './uploads', // Where to save files
|
|
151
|
-
baseUrl: '/uploads', // Public URL prefix
|
|
152
|
-
nameStrategy: 'hash', // Filename strategy (see below)
|
|
153
|
-
preserveExtension: true, // Keep file extensions? (default: true)
|
|
154
|
-
allowedExtensions: ['.jpg', '.png'], // Extension whitelist (optional)
|
|
155
|
-
maxFilenameLength: 255, // Max filename length
|
|
156
|
-
nameGenerator: async (file) => {...} // Custom name generator (optional)
|
|
157
|
-
});
|
|
158
|
-
```
|
|
159
|
-
|
|
160
|
-
**Filename Strategies:**
|
|
161
|
-
|
|
162
|
-
1. **`'hash'`** (default) - Cryptographically secure random names
|
|
163
|
-
```javascript
|
|
164
|
-
nameStrategy: 'hash'
|
|
165
|
-
// Result: "a7f8d9e2b4c6e1f3.jpg"
|
|
166
|
-
```
|
|
167
|
-
|
|
168
|
-
2. **`'timestamp'`** - Timestamp with random suffix (sortable)
|
|
169
|
-
```javascript
|
|
170
|
-
nameStrategy: 'timestamp'
|
|
171
|
-
// Result: "1672531200000_a8f9.pdf"
|
|
172
|
-
```
|
|
173
|
-
|
|
174
|
-
3. **`'original'`** - Sanitized original filename (user-friendly)
|
|
175
|
-
```javascript
|
|
176
|
-
nameStrategy: 'original'
|
|
177
|
-
// "My Photo!.jpg" → "My_Photo_.jpg"
|
|
178
|
-
// Duplicates → "My_Photo_1.jpg", "My_Photo_2.jpg"
|
|
179
|
-
```
|
|
180
|
-
|
|
181
|
-
4. **`'custom'`** - Your own naming logic
|
|
182
|
-
```javascript
|
|
183
|
-
nameStrategy: 'custom',
|
|
184
|
-
nameGenerator: async (file) => {
|
|
185
|
-
const userId = file.metadata?.userId || 'anonymous';
|
|
186
|
-
return `user_${userId}_${Date.now()}`;
|
|
187
|
-
}
|
|
188
|
-
// Result: "user_12345_1672531200000.jpg"
|
|
189
|
-
```
|
|
190
|
-
Custom generators must return a basename only, not a path. Values containing `/`, `\`, drive prefixes, or only whitespace are rejected.
|
|
191
|
-
|
|
192
|
-
**Security Features:**
|
|
193
|
-
- Path traversal protection (sanitizes original names and rejects path-like custom names)
|
|
194
|
-
- Custom generated names are restricted to basenames
|
|
195
|
-
- Control character filtering
|
|
196
|
-
- Extension validation against whitelist
|
|
197
|
-
- Automatic duplicate handling
|
|
198
|
-
- MIME type to extension mapping
|
|
199
|
-
|
|
200
|
-
### Custom Storage Adapters
|
|
201
|
-
|
|
202
|
-
Create your own storage adapter by implementing the required interface:
|
|
203
|
-
|
|
204
|
-
```javascript
|
|
205
|
-
class MyCustomStorage {
|
|
206
|
-
async upload(file) {
|
|
207
|
-
// file object contains:
|
|
208
|
-
// - filename: original filename
|
|
209
|
-
// - mimetype: MIME type
|
|
210
|
-
// - size: size in bytes
|
|
211
|
-
// - data: Buffer with file contents
|
|
212
|
-
// - filepath: temp file path (if using formidable)
|
|
213
|
-
// - cleanup: async function to cleanup temp files
|
|
214
|
-
|
|
215
|
-
// Save the file somewhere
|
|
216
|
-
const url = await saveFileSomewhere(file);
|
|
217
|
-
|
|
218
|
-
// Return the public URL
|
|
219
|
-
return url;
|
|
220
|
-
}
|
|
221
|
-
|
|
222
|
-
async delete(url) {
|
|
223
|
-
// Optional but recommended: FileHandlingPlugin calls this after
|
|
224
|
-
// an uploaded file's write transaction rolls back.
|
|
225
|
-
await deleteFileSomewhere(url);
|
|
226
|
-
}
|
|
227
|
-
}
|
|
228
|
-
```
|
|
229
|
-
|
|
230
|
-
`FileHandlingPlugin` always calls detector-provided `file.cleanup()` after parsing, including validation failures. If a file has already been uploaded and the later resource write rolls back, the plugin calls `storage.delete(url)` when the adapter implements it. Storage adapters that need rollback cleanup should make `delete(url)` idempotent for URLs returned by `upload(file)`.
|
|
231
|
-
|
|
232
|
-
## Protocol Configuration
|
|
233
|
-
|
|
234
|
-
Different protocols have different configuration options for file parsing.
|
|
235
|
-
|
|
236
|
-
### ExpressPlugin Configuration
|
|
237
|
-
|
|
238
|
-
The Express plugin supports multiple file parsers:
|
|
239
|
-
|
|
240
|
-
```javascript
|
|
241
|
-
api.use(ExpressPlugin, {
|
|
242
|
-
// Choose parser: 'busboy', 'formidable', or a function
|
|
243
|
-
fileParser: 'busboy',
|
|
244
|
-
|
|
245
|
-
// Parser-specific options
|
|
246
|
-
fileParserOptions: {
|
|
247
|
-
// For busboy
|
|
248
|
-
limits: {
|
|
249
|
-
fileSize: 10 * 1024 * 1024, // 10MB max file size
|
|
250
|
-
files: 5, // Max 5 files per request
|
|
251
|
-
fields: 20, // Max 20 non-file fields
|
|
252
|
-
parts: 25 // Max 25 total parts
|
|
253
|
-
}
|
|
254
|
-
},
|
|
255
|
-
|
|
256
|
-
// Or use formidable
|
|
257
|
-
// fileParser: 'formidable',
|
|
258
|
-
// fileParserOptions: {
|
|
259
|
-
// uploadDir: './temp', // Temp directory
|
|
260
|
-
// keepExtensions: true, // Keep file extensions
|
|
261
|
-
// maxFileSize: 200 * 1024 * 1024 // 200MB max
|
|
262
|
-
// }
|
|
263
|
-
|
|
264
|
-
// Disable file uploads entirely
|
|
265
|
-
// enableFileUploads: false
|
|
266
|
-
});
|
|
267
|
-
```
|
|
268
|
-
|
|
269
|
-
#### Using Express Middleware
|
|
270
|
-
|
|
271
|
-
For advanced use cases, you can use Express middleware for file handling:
|
|
272
|
-
|
|
273
|
-
```javascript
|
|
274
|
-
import multer from 'multer';
|
|
275
|
-
|
|
276
|
-
const upload = multer({ dest: 'uploads/' });
|
|
277
|
-
|
|
278
|
-
api.use(ExpressPlugin, {
|
|
279
|
-
middleware: {
|
|
280
|
-
beforeScope: {
|
|
281
|
-
// Add multer to specific scope
|
|
282
|
-
images: [upload.single('file')],
|
|
283
|
-
|
|
284
|
-
// Multiple files for another scope
|
|
285
|
-
gallery: [upload.array('photos', 10)]
|
|
286
|
-
}
|
|
287
|
-
},
|
|
288
|
-
|
|
289
|
-
// Disable built-in file handling since we're using multer
|
|
290
|
-
enableFileUploads: false
|
|
291
|
-
});
|
|
292
|
-
```
|
|
293
|
-
|
|
294
|
-
### HttpPlugin Configuration
|
|
295
|
-
|
|
296
|
-
The HTTP plugin has similar configuration:
|
|
297
|
-
|
|
298
|
-
```javascript
|
|
299
|
-
api.use(HttpPlugin, {
|
|
300
|
-
// Choose parser: 'busboy', 'formidable', or a function
|
|
301
|
-
fileParser: 'formidable',
|
|
302
|
-
|
|
303
|
-
fileParserOptions: {
|
|
304
|
-
uploadDir: './uploads/temp',
|
|
305
|
-
keepExtensions: true,
|
|
306
|
-
maxFileSize: 100 * 1024 * 1024 // 100MB
|
|
307
|
-
},
|
|
308
|
-
|
|
309
|
-
// Other HTTP options
|
|
310
|
-
port: 3000,
|
|
311
|
-
basePath: '/api'
|
|
312
|
-
});
|
|
313
|
-
```
|
|
314
|
-
|
|
315
|
-
### Custom File Parsers
|
|
316
|
-
|
|
317
|
-
You can provide a custom file parser:
|
|
318
|
-
|
|
319
|
-
```javascript
|
|
320
|
-
api.use(HttpPlugin, {
|
|
321
|
-
fileParser: (options) => ({
|
|
322
|
-
name: 'my-custom-parser',
|
|
323
|
-
detect: (params) => {
|
|
324
|
-
// Return true if this parser can handle the request
|
|
325
|
-
const req = params._httpReq;
|
|
326
|
-
return req.headers['content-type']?.includes('multipart/form-data');
|
|
327
|
-
},
|
|
328
|
-
parse: async (params) => {
|
|
329
|
-
// Parse the request and return { fields, files }
|
|
330
|
-
const req = params._httpReq;
|
|
331
|
-
const { fields, files } = await myCustomParser(req);
|
|
332
|
-
return { fields, files };
|
|
333
|
-
}
|
|
334
|
-
})
|
|
335
|
-
});
|
|
336
|
-
```
|
|
337
|
-
|
|
338
|
-
## File Validation
|
|
339
|
-
|
|
340
|
-
The FileHandlingPlugin automatically validates files based on schema configuration.
|
|
341
|
-
|
|
342
|
-
### MIME Type Validation
|
|
343
|
-
|
|
344
|
-
```javascript
|
|
345
|
-
// Accept only images
|
|
346
|
-
file: {
|
|
347
|
-
type: 'file',
|
|
348
|
-
storage: localStorage,
|
|
349
|
-
accepts: ['image/*']
|
|
350
|
-
}
|
|
351
|
-
|
|
352
|
-
// Accept specific types
|
|
353
|
-
document: {
|
|
354
|
-
type: 'file',
|
|
355
|
-
storage: s3Storage,
|
|
356
|
-
accepts: ['application/pdf', 'application/msword', 'text/plain']
|
|
357
|
-
}
|
|
358
|
-
|
|
359
|
-
// Accept anything (not recommended)
|
|
360
|
-
attachment: {
|
|
361
|
-
type: 'file',
|
|
362
|
-
storage: localStorage,
|
|
363
|
-
accepts: ['*']
|
|
364
|
-
}
|
|
365
|
-
```
|
|
366
|
-
|
|
367
|
-
### Size Validation
|
|
368
|
-
|
|
369
|
-
```javascript
|
|
370
|
-
// Human-readable format
|
|
371
|
-
avatar: {
|
|
372
|
-
type: 'file',
|
|
373
|
-
storage: localStorage,
|
|
374
|
-
maxSize: '5mb'
|
|
375
|
-
}
|
|
376
|
-
|
|
377
|
-
// Supports: b, kb, mb, gb
|
|
378
|
-
largeFile: {
|
|
379
|
-
type: 'file',
|
|
380
|
-
storage: s3Storage,
|
|
381
|
-
maxSize: '1.5gb'
|
|
382
|
-
}
|
|
383
|
-
```
|
|
384
|
-
|
|
385
|
-
### Required Files
|
|
386
|
-
|
|
387
|
-
```javascript
|
|
388
|
-
// This file must be provided
|
|
389
|
-
document: {
|
|
390
|
-
type: 'file',
|
|
391
|
-
storage: s3Storage,
|
|
392
|
-
required: true
|
|
393
|
-
}
|
|
394
|
-
```
|
|
395
|
-
|
|
396
|
-
### Validation Errors
|
|
397
|
-
|
|
398
|
-
When validation fails, you'll get appropriate error responses:
|
|
399
|
-
|
|
400
|
-
```json
|
|
401
|
-
{
|
|
402
|
-
"errors": [{
|
|
403
|
-
"status": "422",
|
|
404
|
-
"title": "Validation Error",
|
|
405
|
-
"detail": "Invalid file type for field 'avatar'",
|
|
406
|
-
"source": {
|
|
407
|
-
"pointer": "/data/attributes/avatar"
|
|
408
|
-
}
|
|
409
|
-
}]
|
|
410
|
-
}
|
|
411
|
-
```
|
|
412
|
-
|
|
413
|
-
## Complete Examples
|
|
414
|
-
|
|
415
|
-
### Basic Image Upload
|
|
416
|
-
|
|
417
|
-
```javascript
|
|
418
|
-
import { Api } from 'hooked-api';
|
|
419
|
-
import { RestApiPlugin, FileHandlingPlugin, ExpressPlugin } from 'json-rest-api';
|
|
420
|
-
import { LocalStorage } from 'json-rest-api/plugins/storage/local-storage.js';
|
|
421
|
-
import express from 'express';
|
|
422
|
-
|
|
423
|
-
// Setup
|
|
424
|
-
const api = new Api({ name: 'image-api' });
|
|
425
|
-
const storage = new LocalStorage({
|
|
426
|
-
directory: './uploads/images',
|
|
427
|
-
baseUrl: 'http://localhost:3000/uploads/images'
|
|
428
|
-
});
|
|
429
|
-
|
|
430
|
-
// Plugins
|
|
431
|
-
api.use(RestApiPlugin);
|
|
432
|
-
api.use(FileHandlingPlugin);
|
|
433
|
-
api.use(ExpressPlugin);
|
|
434
|
-
|
|
435
|
-
// Schema
|
|
436
|
-
api.addScope('photos', {
|
|
437
|
-
schema: {
|
|
438
|
-
caption: { type: 'string', required: true },
|
|
439
|
-
image: {
|
|
440
|
-
type: 'file',
|
|
441
|
-
storage: storage,
|
|
442
|
-
accepts: ['image/jpeg', 'image/png'],
|
|
443
|
-
maxSize: '10mb',
|
|
444
|
-
required: true
|
|
445
|
-
}
|
|
446
|
-
}
|
|
447
|
-
});
|
|
448
|
-
|
|
449
|
-
// Express app
|
|
450
|
-
const app = express();
|
|
451
|
-
app.use('/uploads', express.static('./uploads'));
|
|
452
|
-
api.express.mount(app);
|
|
453
|
-
|
|
454
|
-
// HTML form for testing
|
|
455
|
-
app.get('/', (req, res) => {
|
|
456
|
-
res.send(`
|
|
457
|
-
<form action="/api/photos" method="POST" enctype="multipart/form-data">
|
|
458
|
-
<input name="caption" placeholder="Caption" required>
|
|
459
|
-
<input name="image" type="file" accept="image/*" required>
|
|
460
|
-
<button type="submit">Upload Photo</button>
|
|
461
|
-
</form>
|
|
462
|
-
`);
|
|
463
|
-
});
|
|
464
|
-
|
|
465
|
-
app.listen(3000).on('error', (err) => {
|
|
466
|
-
console.error('Failed to start server:', err);
|
|
467
|
-
process.exit(1)
|
|
468
|
-
});
|
|
469
|
-
```
|
|
470
|
-
|
|
471
|
-
### Multiple Storage Backends
|
|
472
|
-
|
|
473
|
-
```javascript
|
|
474
|
-
// Different storage for different fields
|
|
475
|
-
api.addScope('articles', {
|
|
476
|
-
schema: {
|
|
477
|
-
title: { type: 'string', required: true },
|
|
478
|
-
content: { type: 'string', required: true },
|
|
479
|
-
|
|
480
|
-
// Featured image goes to S3
|
|
481
|
-
featuredImage: {
|
|
482
|
-
type: 'file',
|
|
483
|
-
storage: s3Storage,
|
|
484
|
-
accepts: ['image/*'],
|
|
485
|
-
maxSize: '20mb'
|
|
486
|
-
},
|
|
487
|
-
|
|
488
|
-
// Attachments stay local
|
|
489
|
-
attachment: {
|
|
490
|
-
type: 'file',
|
|
491
|
-
storage: localStorage,
|
|
492
|
-
accepts: ['application/pdf', 'application/zip'],
|
|
493
|
-
maxSize: '100mb'
|
|
494
|
-
}
|
|
495
|
-
}
|
|
496
|
-
});
|
|
497
|
-
```
|
|
498
|
-
|
|
499
|
-
### Filename Handling Examples
|
|
500
|
-
|
|
501
|
-
Different strategies for different use cases:
|
|
502
|
-
|
|
503
|
-
```javascript
|
|
504
|
-
import { LocalStorage } from 'json-rest-api/plugins/storage/local-storage.js';
|
|
505
|
-
|
|
506
|
-
// User avatars - use hash for security and deduplication
|
|
507
|
-
const avatarStorage = new LocalStorage({
|
|
508
|
-
directory: './uploads/avatars',
|
|
509
|
-
baseUrl: '/uploads/avatars',
|
|
510
|
-
nameStrategy: 'hash'
|
|
511
|
-
});
|
|
512
|
-
|
|
513
|
-
// Documents - use timestamp for sorting
|
|
514
|
-
const documentStorage = new LocalStorage({
|
|
515
|
-
directory: './uploads/documents',
|
|
516
|
-
baseUrl: '/uploads/documents',
|
|
517
|
-
nameStrategy: 'timestamp'
|
|
518
|
-
});
|
|
519
|
-
|
|
520
|
-
// User downloads - preserve original names
|
|
521
|
-
const downloadStorage = new LocalStorage({
|
|
522
|
-
directory: './uploads/downloads',
|
|
523
|
-
baseUrl: '/uploads/downloads',
|
|
524
|
-
nameStrategy: 'original',
|
|
525
|
-
maxFilenameLength: 100
|
|
526
|
-
});
|
|
527
|
-
|
|
528
|
-
// High security - no extensions
|
|
529
|
-
const secureStorage = new LocalStorage({
|
|
530
|
-
directory: './uploads/secure',
|
|
531
|
-
baseUrl: '/uploads/secure',
|
|
532
|
-
nameStrategy: 'hash',
|
|
533
|
-
preserveExtension: false, // All files saved as .bin
|
|
534
|
-
allowedExtensions: ['.pdf', '.doc', '.docx'] // Still validates input
|
|
535
|
-
});
|
|
536
|
-
|
|
537
|
-
// Organized by date
|
|
538
|
-
const organizedStorage = new LocalStorage({
|
|
539
|
-
directory: './uploads',
|
|
540
|
-
baseUrl: '/uploads',
|
|
541
|
-
nameStrategy: 'custom',
|
|
542
|
-
nameGenerator: async (file) => {
|
|
543
|
-
const date = new Date();
|
|
544
|
-
const datePrefix = `${date.getFullYear()}-${String(date.getMonth() + 1).padStart(2, '0')}`;
|
|
545
|
-
return `${datePrefix}-${crypto.randomBytes(16).toString('hex')}`;
|
|
546
|
-
}
|
|
547
|
-
});
|
|
548
|
-
// Saves as: "2024-01-a7f8d9e2b4c6e1f3.jpg"
|
|
549
|
-
```
|
|
550
|
-
|
|
551
|
-
### Using cURL
|
|
552
|
-
|
|
553
|
-
```bash
|
|
554
|
-
# Upload with cURL
|
|
555
|
-
curl -X POST http://localhost:3000/api/photos \
|
|
556
|
-
-F "caption=Beautiful sunset" \
|
|
557
|
-
-F "image=@/path/to/sunset.jpg"
|
|
558
|
-
|
|
559
|
-
# Multiple files
|
|
560
|
-
curl -X POST http://localhost:3000/api/articles \
|
|
561
|
-
-F "title=My Article" \
|
|
562
|
-
-F "content=Article content here" \
|
|
563
|
-
-F "featuredImage=@/path/to/hero.jpg" \
|
|
564
|
-
-F "attachment=@/path/to/document.pdf"
|
|
565
|
-
```
|
|
566
|
-
|
|
567
|
-
### Programmatic Upload
|
|
568
|
-
|
|
569
|
-
```javascript
|
|
570
|
-
// Using fetch with FormData
|
|
571
|
-
const formData = new FormData();
|
|
572
|
-
formData.append('caption', 'My photo');
|
|
573
|
-
formData.append('image', fileInput.files[0]);
|
|
574
|
-
|
|
575
|
-
const response = await fetch('/api/photos', {
|
|
576
|
-
method: 'POST',
|
|
577
|
-
body: formData
|
|
578
|
-
});
|
|
579
|
-
|
|
580
|
-
const result = await response.json();
|
|
581
|
-
console.log('Uploaded:', result.data.attributes.image); // URL of uploaded file
|
|
582
|
-
```
|
|
583
|
-
|
|
584
|
-
## Troubleshooting
|
|
585
|
-
|
|
586
|
-
### Common Issues
|
|
587
|
-
|
|
588
|
-
#### 1. "Busboy not available, file uploads disabled"
|
|
589
|
-
|
|
590
|
-
Install the peer dependency:
|
|
591
|
-
```bash
|
|
592
|
-
npm install busboy
|
|
593
|
-
```
|
|
594
|
-
|
|
595
|
-
#### 2. "No storage configured for file field"
|
|
596
|
-
|
|
597
|
-
Make sure you've set the storage property:
|
|
598
|
-
```javascript
|
|
599
|
-
file: {
|
|
600
|
-
type: 'file',
|
|
601
|
-
storage: myStorage // This is required!
|
|
602
|
-
}
|
|
603
|
-
```
|
|
604
|
-
|
|
605
|
-
#### 3. Files not being detected
|
|
606
|
-
|
|
607
|
-
Check that:
|
|
608
|
-
1. The FileHandlingPlugin is loaded AFTER RestApiPlugin
|
|
609
|
-
2. Your protocol plugin has file uploads enabled
|
|
610
|
-
3. The request has proper multipart headers
|
|
611
|
-
4. You're using the correct form encoding
|
|
612
|
-
|
|
613
|
-
```html
|
|
614
|
-
<!-- HTML forms need this -->
|
|
615
|
-
<form enctype="multipart/form-data">
|
|
616
|
-
```
|
|
617
|
-
|
|
618
|
-
#### 4. "File too large" errors
|
|
619
|
-
|
|
620
|
-
Check both:
|
|
621
|
-
1. Schema `maxSize` configuration
|
|
622
|
-
2. Parser limits in plugin options
|
|
623
|
-
|
|
624
|
-
```javascript
|
|
625
|
-
// Both limits apply!
|
|
626
|
-
api.use(ExpressPlugin, {
|
|
627
|
-
fileParserOptions: {
|
|
628
|
-
limits: { fileSize: 10 * 1024 * 1024 } // 10MB parser limit
|
|
629
|
-
}
|
|
630
|
-
});
|
|
631
|
-
|
|
632
|
-
api.addScope('images', {
|
|
633
|
-
schema: {
|
|
634
|
-
photo: {
|
|
635
|
-
type: 'file',
|
|
636
|
-
maxSize: '5mb' // 5MB schema limit (lower wins)
|
|
637
|
-
}
|
|
638
|
-
}
|
|
639
|
-
});
|
|
640
|
-
```
|
|
641
|
-
|
|
642
|
-
### Debug Mode
|
|
643
|
-
|
|
644
|
-
Enable debug logging to see what's happening:
|
|
645
|
-
|
|
646
|
-
```javascript
|
|
647
|
-
const api = new Api({
|
|
648
|
-
name: 'my-api',
|
|
649
|
-
logLevel: 'debug' // or 'trace' for more detail
|
|
650
|
-
});
|
|
651
|
-
```
|
|
652
|
-
|
|
653
|
-
### Testing File Uploads
|
|
654
|
-
|
|
655
|
-
Use the included example to test your setup:
|
|
656
|
-
|
|
657
|
-
```javascript
|
|
658
|
-
// Run the example
|
|
659
|
-
node ./node_modules/json-rest-api/examples/file-upload-example.js
|
|
660
|
-
```
|
|
661
|
-
|
|
662
|
-
Then visit http://localhost:3000 to see the test forms.
|
|
663
|
-
|
|
664
|
-
## Best Practices
|
|
665
|
-
|
|
666
|
-
1. **Always validate file types** - Don't use `accepts: ['*']` in production
|
|
667
|
-
2. **Set reasonable size limits** - Prevent abuse and server overload
|
|
668
|
-
3. **Use appropriate storage** - Local for small files, S3 for large/many files
|
|
669
|
-
4. **Clean up temp files** - Storage adapters should handle cleanup
|
|
670
|
-
5. **Serve files separately** - Don't serve uploaded files through your API
|
|
671
|
-
6. **Validate file contents** - Consider virus scanning for user uploads
|
|
672
|
-
7. **Use CDN for images** - Serve uploaded images through a CDN in production
|
|
673
|
-
|
|
674
|
-
## Security Considerations
|
|
675
|
-
|
|
676
|
-
### Filename Security
|
|
677
|
-
|
|
678
|
-
1. **Never trust user filenames** - Always sanitize or generate new names
|
|
679
|
-
```javascript
|
|
680
|
-
// BAD - Direct use of user filename
|
|
681
|
-
const filename = file.originalname;
|
|
682
|
-
|
|
683
|
-
// GOOD - Generate secure name
|
|
684
|
-
const filename = crypto.randomBytes(16).toString('hex') + path.extname(file.originalname);
|
|
685
|
-
```
|
|
686
|
-
|
|
687
|
-
2. **Prevent path traversal** - Remove dangerous characters
|
|
688
|
-
```javascript
|
|
689
|
-
// Dangerous filenames to watch for:
|
|
690
|
-
// "../../../etc/passwd"
|
|
691
|
-
// "..\\..\\windows\\system32\\config\\sam"
|
|
692
|
-
// "uploads/../../../index.js"
|
|
693
|
-
|
|
694
|
-
// LocalStorage handles this automatically
|
|
695
|
-
```
|
|
696
|
-
|
|
697
|
-
3. **Extension validation** - Whitelist allowed extensions
|
|
698
|
-
```javascript
|
|
699
|
-
// Use LocalStorage with whitelist
|
|
700
|
-
const storage = new LocalStorage({
|
|
701
|
-
allowedExtensions: ['.jpg', '.jpeg', '.png', '.pdf']
|
|
702
|
-
});
|
|
703
|
-
```
|
|
704
|
-
|
|
705
|
-
4. **Consider removing extensions entirely** for sensitive files
|
|
706
|
-
```javascript
|
|
707
|
-
const highSecurityStorage = new LocalStorage({
|
|
708
|
-
nameStrategy: 'hash',
|
|
709
|
-
preserveExtension: false // All files become .bin
|
|
710
|
-
});
|
|
711
|
-
```
|
|
712
|
-
|
|
713
|
-
### General File Security
|
|
714
|
-
|
|
715
|
-
1. **Validate MIME types** - But remember they can be spoofed
|
|
716
|
-
2. **Check file contents** - Use libraries like `file-type` for verification
|
|
717
|
-
3. **Limit upload sizes** - Prevent denial of service
|
|
718
|
-
4. **Store files outside web root** - Prevent direct execution
|
|
719
|
-
5. **Use virus scanning** - For user-uploaded content
|
|
720
|
-
6. **Set proper permissions** - Uploaded files shouldn't be executable
|
|
721
|
-
7. **Serve files with proper headers** - Use Content-Disposition for downloads
|
|
722
|
-
|
|
723
|
-
## Next Steps
|
|
724
|
-
|
|
725
|
-
- Implement production S3 storage with actual AWS SDK
|
|
726
|
-
- Add image processing (thumbnails, resizing)
|
|
727
|
-
- Implement virus scanning for uploads
|
|
728
|
-
- Add progress tracking for large files
|
|
729
|
-
- Create a chunked upload system for very large files
|