json-rest-api 1.0.24 → 1.0.26
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/README.md +2 -1
- package/docs/API.md +12 -9
- package/docs/GUIDE/GUIDE_1_Initial_Setup.md +5 -2
- package/docs/GUIDE/GUIDE_2_1_The_Starting_Point.md +3 -3
- package/docs/GUIDE/GUIDE_3_Field_Transformations.md +2 -0
- package/docs/GUIDE/GUIDE_7_Hooks_Data_Management_And_Plugins.md +1 -1
- package/docs/GUIDE/GUIDE_X_Autofiltering.md +63 -0
- package/docs/GUIDE/GUIDE_X_File_Uploads.md +17 -12
- package/docs/GUIDE/GUIDE_X_Row_Policies.md +605 -0
- package/docs/GUIDE/GUIDE_X_SocketIO.md +15 -10
- package/docs/GUIDE/GUIDE_X_URL_Management.md +93 -254
- package/docs/GUIDE/GUIDE_X_Writing_Plugins.md +2 -0
- package/docs/GUIDE/index.md +4 -1
- package/fixing_plan.md +112 -0
- package/index.js +2 -0
- package/package.json +1 -1
- package/plugins/core/bulk-operations-plugin.js +5 -4
- package/plugins/core/connectors/express-plugin.js +6 -1
- package/plugins/core/connectors/fastify-plugin.js +5 -1
- package/plugins/core/connectors/lib/connector-core.js +17 -11
- package/plugins/core/connectors/lib/transport-route-schemas.js +12 -60
- package/plugins/core/file-handling-plugin.js +126 -96
- package/plugins/core/lib/anyapi/anyapi-registry.js +21 -3
- package/plugins/core/lib/anyapi/query/anyapi-query-adapter.js +36 -26
- package/plugins/core/lib/anyapi/schema-utils.js +1 -0
- package/plugins/core/lib/anyapi/utils/descriptor-helpers.js +7 -1
- package/plugins/core/lib/querying/knex-json-api-transformers-querying.js +20 -15
- package/plugins/core/lib/querying/knex-pagination-helpers.js +38 -84
- package/plugins/core/lib/querying/knex-relationship-includes.js +29 -28
- package/plugins/core/lib/querying/knex-window-queries.js +10 -77
- package/plugins/core/lib/querying/storage-adapter-utils.js +8 -8
- package/plugins/core/lib/querying/url-helpers.js +9 -21
- package/plugins/core/lib/querying-writing/connectors-query-parser.js +51 -0
- package/plugins/core/lib/querying-writing/database-capabilities.js +1 -1
- package/plugins/core/lib/querying-writing/database-value-normalizers.js +18 -9
- package/plugins/core/lib/querying-writing/knex-field-helpers.js +6 -1
- package/plugins/core/lib/querying-writing/relationship-contracts.js +56 -0
- package/plugins/core/lib/querying-writing/request-contracts.js +53 -4
- package/plugins/core/lib/querying-writing/return-record-settings.js +47 -0
- package/plugins/core/lib/querying-writing/schema-helpers.js +34 -41
- package/plugins/core/lib/storage/canonical-storage-mapping.js +2 -1
- package/plugins/core/lib/storage/storage-adapter.js +74 -13
- package/plugins/core/lib/writing/many-to-many-manipulations.js +4 -3
- package/plugins/core/lib/writing/relationship-processor.js +10 -1
- package/plugins/core/rest-api-anyapi-knex-plugin.js +210 -97
- package/plugins/core/rest-api-knex-plugin.js +70 -60
- package/plugins/core/rest-api-plugin-hooks/turn-scope-init-into-vars.js +8 -2
- package/plugins/core/rest-api-plugin-methods/common.js +85 -63
- package/plugins/core/rest-api-plugin-methods/delete-relationship.js +23 -13
- package/plugins/core/rest-api-plugin-methods/delete.js +2 -6
- package/plugins/core/rest-api-plugin-methods/get-related.js +15 -13
- package/plugins/core/rest-api-plugin-methods/get-relationship.js +1 -1
- package/plugins/core/rest-api-plugin-methods/patch-relationship.js +17 -6
- package/plugins/core/rest-api-plugin-methods/patch.js +3 -6
- package/plugins/core/rest-api-plugin-methods/post-relationship.js +24 -14
- package/plugins/core/rest-api-plugin-methods/post.js +4 -7
- package/plugins/core/rest-api-plugin-methods/put.js +4 -7
- package/plugins/core/rest-api-plugin.js +6 -36
- package/plugins/core/rest-api-row-policy-plugin.js +157 -0
- package/plugins/storage/local-storage.js +44 -19
- package/plugins/storage/s3-storage.js +21 -55
- package/tests/README.md +1 -1
- package/tests/anyapi-cursor-pagination.test.js +7 -1
- package/tests/anyapi-custom-idproperty.test.js +163 -0
- package/tests/fastify-plugin.test.js +28 -0
- package/tests/field-setters.test.js +10 -10
- package/tests/file-handling.test.js +118 -0
- package/tests/fixtures/api-configs.js +383 -4
- package/tests/full-jsonapi-belongsto-linkage.test.js +150 -0
- package/tests/http-connectors-parity.test.js +165 -5
- package/tests/local-storage.test.js +96 -0
- package/tests/pagination-cursor-multifield.test.js +121 -0
- package/tests/pagination-enhanced.test.js +66 -15
- package/tests/post-commit-hooks.test.js +120 -0
- package/tests/public-exports.test.js +15 -0
- package/tests/put-create-parity.test.js +216 -0
- package/tests/relationship-endpoints.test.js +259 -0
- package/tests/return-record-settings.test.js +184 -0
- package/tests/row-policy.test.js +489 -0
- package/tests/s3-storage.test.js +38 -0
- package/tests/searchschema-merge.test.js +17 -0
- package/tests/socketio.test.js +62 -0
- package/tests/storage-mapping.test.js +1 -0
- package/tests/test-development-guide.md +5 -5
package/README.md
CHANGED
|
@@ -9,7 +9,8 @@ A powerful REST API plugin for [hooked-api](https://github.com/mobily-enterprise
|
|
|
9
9
|
* **JSON:API Compliant** - Full support for the JSON:API specification
|
|
10
10
|
* **Relationship Support** - `belongsTo`, `hasMany`, and many-to-many relationships, including polymorphic
|
|
11
11
|
* **Advanced Querying** - Filtering, sorting, pagination, and field selection (sparse fieldsets)
|
|
12
|
-
* **
|
|
12
|
+
* **Server-Side Row Policies** - Apply mandatory visibility rules before pagination, counts, and relationship loading
|
|
13
|
+
* **File Uploads** - Built-in support for file handling with local storage and a mock S3-style demo adapter
|
|
13
14
|
* **Framework Agnostic** - Works with raw Node.js HTTP, Express, and other Node.js frameworks via flexible connectors
|
|
14
15
|
* **Validation** - Schema-based validation with detailed error messages and custom rules
|
|
15
16
|
* **Simplified Mode** - A developer-friendly option to work with plain JavaScript objects instead of verbose JSON:API structure
|
package/docs/API.md
CHANGED
|
@@ -427,7 +427,7 @@ const result = await api.resources.[resourceType].post(params, context)
|
|
|
427
427
|
},
|
|
428
428
|
simplified: Boolean, // Override simplified mode (default: true for API)
|
|
429
429
|
transaction: Object, // Database transaction object
|
|
430
|
-
returnFullRecord: String // Override return setting ('no', 'minimal', 'full')
|
|
430
|
+
returnFullRecord: String|Boolean // Override return setting ('no', 'minimal', 'full')
|
|
431
431
|
}
|
|
432
432
|
```
|
|
433
433
|
|
|
@@ -443,7 +443,7 @@ const result = await api.resources.[resourceType].post(params, context)
|
|
|
443
443
|
| `queryParams` | Object | No | For includes/fields in response |
|
|
444
444
|
| `simplified` | Boolean | No | Override simplified mode (default: true) |
|
|
445
445
|
| `transaction` | Object | No | Database transaction object |
|
|
446
|
-
| `returnFullRecord` | String | No | Override return setting ('no'
|
|
446
|
+
| `returnFullRecord` | String\|Boolean | No | Override return setting (`'no'`, `'minimal'`, `'full'`; legacy `true` = `'full'`, `false` = `'no'`) |
|
|
447
447
|
|
|
448
448
|
When an explicit resource id is provided, `normalizeId` runs before persistence and before any follow-up record fetch used for the return payload. If the normalized id is empty, the request fails as `REST_API_VALIDATION` on `data.id`.
|
|
449
449
|
|
|
@@ -460,6 +460,8 @@ The return value depends on `returnFullRecord` setting and whether it's an API o
|
|
|
460
460
|
- `'minimal'`: Returns resource with ID only
|
|
461
461
|
- `'full'`: Returns complete resource with all fields
|
|
462
462
|
|
|
463
|
+
Boolean values are accepted for backward compatibility: `true` is normalized to `'full'` and `false` is normalized to `'no'`. New code should use the string values.
|
|
464
|
+
|
|
463
465
|
#### HTTP Equivalent
|
|
464
466
|
|
|
465
467
|
```http
|
|
@@ -611,7 +613,7 @@ const result = await api.resources.[resourceType].put(params, context)
|
|
|
611
613
|
},
|
|
612
614
|
simplified: Boolean, // Override simplified mode (default: true for API)
|
|
613
615
|
transaction: Object, // Database transaction object
|
|
614
|
-
returnFullRecord: String // Override return setting ('no', 'minimal', 'full')
|
|
616
|
+
returnFullRecord: String|Boolean // Override return setting ('no', 'minimal', 'full')
|
|
615
617
|
}
|
|
616
618
|
```
|
|
617
619
|
|
|
@@ -626,7 +628,7 @@ const result = await api.resources.[resourceType].put(params, context)
|
|
|
626
628
|
| `queryParams` | Object | No | For response formatting |
|
|
627
629
|
| `simplified` | Boolean | No | Override simplified mode (default: true) |
|
|
628
630
|
| `transaction` | Object | No | Database transaction object |
|
|
629
|
-
| `returnFullRecord` | String | No | Override return setting |
|
|
631
|
+
| `returnFullRecord` | String\|Boolean | No | Override return setting (`'no'`, `'minimal'`, `'full'`; legacy `true` = `'full'`, `false` = `'no'`) |
|
|
630
632
|
|
|
631
633
|
`inputRecord.id` and `inputRecord.data.id` always refer to the logical resource id. `idProperty` and storage mapping affect the backing column name, not the API field name, and the resource id is not part of `attributes`.
|
|
632
634
|
|
|
@@ -742,7 +744,7 @@ const result = await api.resources.[resourceType].patch(params, context)
|
|
|
742
744
|
},
|
|
743
745
|
simplified: Boolean, // Override simplified mode (default: true for API)
|
|
744
746
|
transaction: Object, // Database transaction object
|
|
745
|
-
returnFullRecord: String // Override return setting ('no', 'minimal', 'full')
|
|
747
|
+
returnFullRecord: String|Boolean // Override return setting ('no', 'minimal', 'full')
|
|
746
748
|
}
|
|
747
749
|
```
|
|
748
750
|
|
|
@@ -757,7 +759,7 @@ const result = await api.resources.[resourceType].patch(params, context)
|
|
|
757
759
|
| `queryParams` | Object | No | For response formatting |
|
|
758
760
|
| `simplified` | Boolean | No | Override simplified mode (default: true) |
|
|
759
761
|
| `transaction` | Object | No | Database transaction object |
|
|
760
|
-
| `returnFullRecord` | String | No | Override return setting |
|
|
762
|
+
| `returnFullRecord` | String\|Boolean | No | Override return setting (`'no'`, `'minimal'`, `'full'`; legacy `true` = `'full'`, `false` = `'no'`) |
|
|
761
763
|
|
|
762
764
|
If both the URL id and body id are present, both are normalized before the equality check. If either id normalizes to an empty value, the operation fails before storage is touched.
|
|
763
765
|
|
|
@@ -1315,7 +1317,7 @@ Each API method has its own specific hook execution order. Here's the exact sequ
|
|
|
1315
1317
|
```
|
|
1316
1318
|
1. beforeData
|
|
1317
1319
|
2. beforeDataQuery
|
|
1318
|
-
3. knexQueryFiltering (multiple
|
|
1320
|
+
3. knexQueryFiltering (multiple handlers for built-in filters and optional server-side policies)
|
|
1319
1321
|
- polymorphicFiltersHook
|
|
1320
1322
|
- crossTableFiltersHook
|
|
1321
1323
|
- basicFiltersHook
|
|
@@ -1450,7 +1452,7 @@ Each API method has its own specific hook execution order. Here's the exact sequ
|
|
|
1450
1452
|
1. **Processing Hooks**: Only POST, PUT, and PATCH have `beforeProcessing` hooks
|
|
1451
1453
|
2. **Schema Validation**: Only POST, PUT, and PATCH have schema validation hooks
|
|
1452
1454
|
3. **Permission Hooks**: GET uses `checkDataPermissions`, while relationship methods use `checkPermissions`
|
|
1453
|
-
4. **Query Filtering**:
|
|
1455
|
+
4. **Query Filtering**: Collection queries and storage-backed scoped lookups trigger `knexQueryFiltering`; use `RowPolicyPlugin` for mandatory visibility rather than relying on method-specific hook assumptions
|
|
1454
1456
|
5. **Enrichment**: Only GET and QUERY have `enrichRecord` hooks
|
|
1455
1457
|
6. **Relationships**: Only GET has `enrichRecordWithRelationships`
|
|
1456
1458
|
7. **Transactions**: All write methods (POST, PUT, PATCH, DELETE) can trigger `afterCommit`/`afterRollback`
|
|
@@ -2292,7 +2294,8 @@ api.addResource({
|
|
|
2292
2294
|
|
|
2293
2295
|
// No built-in permissions option exists here.
|
|
2294
2296
|
// Use permission hooks for access decisions, and optional plugins
|
|
2295
|
-
// such as AutoFilterPlugin for dataset scoping
|
|
2297
|
+
// such as AutoFilterPlugin for equality-based dataset scoping or
|
|
2298
|
+
// RowPolicyPlugin for mandatory SQL visibility predicates.
|
|
2296
2299
|
|
|
2297
2300
|
// Soft delete configuration
|
|
2298
2301
|
softDelete: {
|
|
@@ -242,7 +242,7 @@ console.log('Refetched Country:', inspect(countryUsRefetched));
|
|
|
242
242
|
// }
|
|
243
243
|
```
|
|
244
244
|
|
|
245
|
-
(Note that the full JSON:API record includes links to resources, which are
|
|
245
|
+
(Note that the full JSON:API record includes links to resources, which are generated from explicit URL configuration such as the connector `mountPath` or `publicBaseUrl`.)
|
|
246
246
|
|
|
247
247
|
As you can see, when `simplified: false` is used:
|
|
248
248
|
|
|
@@ -340,11 +340,13 @@ There are TWO separate settings for this:
|
|
|
340
340
|
|
|
341
341
|
This separation allows you to have different behaviors for internal API usage versus external HTTP clients. For example, your internal code might want full records for convenience, while HTTP clients might prefer minimal responses for performance.
|
|
342
342
|
|
|
343
|
-
Both settings
|
|
343
|
+
Both settings prefer three string values:
|
|
344
344
|
- **`'full'`**: Returns the complete record with all attributes, relationships, computed fields, and links
|
|
345
345
|
- **`'minimal'`**: Returns only the resource type and ID
|
|
346
346
|
- **`'no'`**: Returns nothing (undefined in programmatic calls, 204 No Content in HTTP)
|
|
347
347
|
|
|
348
|
+
Boolean values are accepted for backward compatibility: `true` is normalized to `'full'` and `false` is normalized to `'no'`. New code should use the string values.
|
|
349
|
+
|
|
348
350
|
Here's how these settings work:
|
|
349
351
|
|
|
350
352
|
```javascript
|
|
@@ -796,6 +798,7 @@ Here is a full list of parameters and their respective variables:
|
|
|
796
798
|
- `returnRecordApi` and `returnRecordTransport` can be either:
|
|
797
799
|
- A string: `'no'`, `'minimal'`, or `'full'` (applies to all methods)
|
|
798
800
|
- An object: `{ post: 'full', put: 'minimal', patch: 'no' }` (per-method configuration)
|
|
801
|
+
- Legacy booleans: `true` is treated as `'full'`, `false` as `'no'`
|
|
799
802
|
- All parameters support the cascade: per-call → resource-level → plugin-level default
|
|
800
803
|
|
|
801
804
|
# Logical IDs and Physical Columns
|
|
@@ -23,9 +23,9 @@ const knex = knexLib({
|
|
|
23
23
|
const api = new Api({ name: 'book-catalog-api' });
|
|
24
24
|
|
|
25
25
|
// Install plugins
|
|
26
|
-
await api.use(RestApiPlugin);
|
|
26
|
+
await api.use(RestApiPlugin);
|
|
27
27
|
await api.use(RestApiKnexPlugin, { knex });
|
|
28
|
-
await api.use(ExpressPlugin, {
|
|
28
|
+
await api.use(ExpressPlugin, { mountPath: '/api' }); // Links are generated under /api
|
|
29
29
|
|
|
30
30
|
/// *** ...programmatic calls here... ***
|
|
31
31
|
|
|
@@ -85,4 +85,4 @@ This design provides the best of both worlds: backend developers get full databa
|
|
|
85
85
|
|
|
86
86
|
---
|
|
87
87
|
|
|
88
|
-
[Back to Guide](./README.md) | [Next: 2.2 Manipulating and searching tables with no relationships](./GUIDE_2_2_Manipulating_And_Searching_Tables.md)
|
|
88
|
+
[Back to Guide](./README.md) | [Next: 2.2 Manipulating and searching tables with no relationships](./GUIDE_2_2_Manipulating_And_Searching_Tables.md)
|
|
@@ -132,6 +132,8 @@ await api.addResource('users', {
|
|
|
132
132
|
});
|
|
133
133
|
```
|
|
134
134
|
|
|
135
|
+
If a setter throws or rejects, the write fails with a validation error and the transaction rolls back instead of storing the untransformed value.
|
|
136
|
+
|
|
135
137
|
### Getters - Transform After Retrieval
|
|
136
138
|
|
|
137
139
|
```javascript
|
|
@@ -1186,7 +1186,7 @@ api.addScope('articles', {}, {
|
|
|
1186
1186
|
|
|
1187
1187
|
### knexQueryFiltering
|
|
1188
1188
|
|
|
1189
|
-
The `knexQueryFiltering` hook is called
|
|
1189
|
+
The `knexQueryFiltering` hook is called while storage-backed resource selections are being built. Collection queries run it before sorting and pagination; scoped lookup and relationship paths can also run it. This is a special hook that allows complex query modifications. Use the supported [Row Policies](GUIDE_X_Row_Policies.md) contract for mandatory resource visibility.
|
|
1190
1190
|
|
|
1191
1191
|
**When**: During `dataQuery` execution, before sorting and pagination
|
|
1192
1192
|
**Purpose**: Apply filters, add JOINs, modify query conditions
|
|
@@ -77,6 +77,69 @@ await api.use(AutoFilterPlugin, {
|
|
|
77
77
|
})
|
|
78
78
|
```
|
|
79
79
|
|
|
80
|
+
## Storage Backends
|
|
81
|
+
|
|
82
|
+
`AutoFilterPlugin` works with either database storage plugin:
|
|
83
|
+
|
|
84
|
+
- `RestApiKnexPlugin` for resources backed by normal database tables
|
|
85
|
+
- `RestApiAnyapiKnexPlugin` for logical resources backed by AnyAPI canonical storage
|
|
86
|
+
|
|
87
|
+
An `Api` instance has one storage engine. Install either storage plugin, then install `AutoFilterPlugin`. If an application keeps core resources in normal tables and user-defined resources in AnyAPI, create one `Api` instance for each storage engine. Each instance owns its resources and autofilter configuration.
|
|
88
|
+
|
|
89
|
+
Autofilter definitions always use logical schema field names. The selected storage plugin translates those fields to normal table columns or AnyAPI canonical slots.
|
|
90
|
+
|
|
91
|
+
### Workspace Scoping with AnyAPI
|
|
92
|
+
|
|
93
|
+
AnyAPI's `tenantId` is fixed when `RestApiAnyapiKnexPlugin` is installed. It identifies the metadata and canonical-storage namespace; it is not a per-request workspace value. For one AnyAPI instance serving multiple application workspaces, use a stable internal `tenantId` and persist `workspace_id` on every workspace-scoped resource:
|
|
94
|
+
|
|
95
|
+
```js
|
|
96
|
+
import { Api } from 'hooked-api'
|
|
97
|
+
import {
|
|
98
|
+
RestApiPlugin,
|
|
99
|
+
RestApiAnyapiKnexPlugin,
|
|
100
|
+
AutoFilterPlugin
|
|
101
|
+
} from 'json-rest-api'
|
|
102
|
+
|
|
103
|
+
const api = new Api({ name: 'form-data-api' })
|
|
104
|
+
|
|
105
|
+
await api.use(RestApiPlugin)
|
|
106
|
+
await api.use(RestApiAnyapiKnexPlugin, {
|
|
107
|
+
knex,
|
|
108
|
+
tenantId: 'application'
|
|
109
|
+
})
|
|
110
|
+
|
|
111
|
+
await api.use(AutoFilterPlugin, {
|
|
112
|
+
resolvers: {
|
|
113
|
+
workspace: ({ context }) => context.session?.workspaceId
|
|
114
|
+
},
|
|
115
|
+
presets: {
|
|
116
|
+
workspace: {
|
|
117
|
+
filters: [
|
|
118
|
+
{ field: 'workspace_id', resolver: 'workspace' }
|
|
119
|
+
]
|
|
120
|
+
}
|
|
121
|
+
}
|
|
122
|
+
})
|
|
123
|
+
|
|
124
|
+
await api.addResource('form_123', {
|
|
125
|
+
schema: {
|
|
126
|
+
id: { type: 'id' },
|
|
127
|
+
workspace_id: { type: 'string', required: true },
|
|
128
|
+
customer_name: { type: 'string', required: true },
|
|
129
|
+
subscribed: { type: 'boolean' }
|
|
130
|
+
},
|
|
131
|
+
autofilter: 'workspace'
|
|
132
|
+
})
|
|
133
|
+
```
|
|
134
|
+
|
|
135
|
+
In this arrangement:
|
|
136
|
+
|
|
137
|
+
- `tenantId: 'application'` selects one fixed AnyAPI metadata and canonical-storage namespace. Do not derive it from request context.
|
|
138
|
+
- `workspace_id` isolates application workspaces. `AutoFilterPlugin` stamps it on writes and filters by it on reads.
|
|
139
|
+
- A trusted resolver supplies the current workspace. Do not resolve it directly from an unverified request field.
|
|
140
|
+
- User-defined forms with different schemas need distinct resource names within the AnyAPI namespace, such as `form_123` and `form_456`. Put the workspace autofilter on each resource.
|
|
141
|
+
- Workspaces may use the same logical resource when they share its schema; `workspace_id` still keeps their records separate.
|
|
142
|
+
|
|
80
143
|
## Resource Configuration
|
|
81
144
|
|
|
82
145
|
Apply a preset by name:
|
|
@@ -19,7 +19,7 @@ The file handling system consists of three main components:
|
|
|
19
19
|
|
|
20
20
|
1. **FileHandlingPlugin** - Orchestrates file detection and processing
|
|
21
21
|
2. **Protocol Detectors** - Parse files from different protocols (HTTP, Express)
|
|
22
|
-
3. **Storage Adapters** - Save files to
|
|
22
|
+
3. **Storage Adapters** - Save files to local storage or generate mock S3-style URLs for demos
|
|
23
23
|
|
|
24
24
|
### How It Works
|
|
25
25
|
|
|
@@ -115,11 +115,11 @@ Storage adapters handle where and how files are saved. The library includes two
|
|
|
115
115
|
| **Extension Whitelist** | ✅ Yes | ❌ No |
|
|
116
116
|
| **Duplicate Handling** | ✅ Yes | ✅ Automatic |
|
|
117
117
|
| **Custom Naming** | ✅ Yes | ❌ No |
|
|
118
|
-
| **Best For** | Local file storage |
|
|
118
|
+
| **Best For** | Local file storage | Tests, examples, S3-style URL demos |
|
|
119
119
|
|
|
120
|
-
### S3Storage
|
|
120
|
+
### S3Storage (Mock/Demo)
|
|
121
121
|
|
|
122
|
-
|
|
122
|
+
Generates S3-style URLs without uploading files to Amazon S3:
|
|
123
123
|
|
|
124
124
|
```javascript
|
|
125
125
|
import { S3Storage } from 'json-rest-api/plugins/storage/s3-storage.js';
|
|
@@ -129,7 +129,7 @@ const s3Storage = new S3Storage({
|
|
|
129
129
|
region: 'us-east-1', // AWS region (default: 'us-east-1')
|
|
130
130
|
prefix: 'uploads/', // Path prefix in bucket (default: '')
|
|
131
131
|
acl: 'public-read', // Access control (default: 'public-read')
|
|
132
|
-
mockMode:
|
|
132
|
+
mockMode: true // Only supported mode
|
|
133
133
|
});
|
|
134
134
|
```
|
|
135
135
|
|
|
@@ -137,7 +137,7 @@ const s3Storage = new S3Storage({
|
|
|
137
137
|
- Always generates random hash + extension (e.g., `uploads/a7f8d9e2b4c6e1f3.jpg`)
|
|
138
138
|
- Original filenames are never used for security
|
|
139
139
|
|
|
140
|
-
**Note**: The included S3Storage is a mock implementation for demonstration. For production use,
|
|
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
141
|
|
|
142
142
|
### LocalStorage
|
|
143
143
|
|
|
@@ -187,9 +187,11 @@ const localStorage = new LocalStorage({
|
|
|
187
187
|
}
|
|
188
188
|
// Result: "user_12345_1672531200000.jpg"
|
|
189
189
|
```
|
|
190
|
+
Custom generators must return a basename only, not a path. Values containing `/`, `\`, drive prefixes, or only whitespace are rejected.
|
|
190
191
|
|
|
191
192
|
**Security Features:**
|
|
192
|
-
- Path traversal protection (
|
|
193
|
+
- Path traversal protection (sanitizes original names and rejects path-like custom names)
|
|
194
|
+
- Custom generated names are restricted to basenames
|
|
193
195
|
- Control character filtering
|
|
194
196
|
- Extension validation against whitelist
|
|
195
197
|
- Automatic duplicate handling
|
|
@@ -218,12 +220,15 @@ class MyCustomStorage {
|
|
|
218
220
|
}
|
|
219
221
|
|
|
220
222
|
async delete(url) {
|
|
221
|
-
// Optional:
|
|
223
|
+
// Optional but recommended: FileHandlingPlugin calls this after
|
|
224
|
+
// an uploaded file's write transaction rolls back.
|
|
222
225
|
await deleteFileSomewhere(url);
|
|
223
226
|
}
|
|
224
227
|
}
|
|
225
228
|
```
|
|
226
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
|
+
|
|
227
232
|
## Protocol Configuration
|
|
228
233
|
|
|
229
234
|
Different protocols have different configuration options for file parsing.
|
|
@@ -536,11 +541,11 @@ const organizedStorage = new LocalStorage({
|
|
|
536
541
|
nameStrategy: 'custom',
|
|
537
542
|
nameGenerator: async (file) => {
|
|
538
543
|
const date = new Date();
|
|
539
|
-
const
|
|
540
|
-
return `${
|
|
544
|
+
const datePrefix = `${date.getFullYear()}-${String(date.getMonth() + 1).padStart(2, '0')}`;
|
|
545
|
+
return `${datePrefix}-${crypto.randomBytes(16).toString('hex')}`;
|
|
541
546
|
}
|
|
542
547
|
});
|
|
543
|
-
// Saves as: "2024
|
|
548
|
+
// Saves as: "2024-01-a7f8d9e2b4c6e1f3.jpg"
|
|
544
549
|
```
|
|
545
550
|
|
|
546
551
|
### Using cURL
|
|
@@ -721,4 +726,4 @@ Then visit http://localhost:3000 to see the test forms.
|
|
|
721
726
|
- Add image processing (thumbnails, resizing)
|
|
722
727
|
- Implement virus scanning for uploads
|
|
723
728
|
- Add progress tracking for large files
|
|
724
|
-
- Create a chunked upload system for very large files
|
|
729
|
+
- Create a chunked upload system for very large files
|