json-rest-api 1.0.24 → 1.0.25

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.
Files changed (82) hide show
  1. package/README.md +2 -1
  2. package/docs/API.md +12 -9
  3. package/docs/GUIDE/GUIDE_1_Initial_Setup.md +5 -2
  4. package/docs/GUIDE/GUIDE_2_1_The_Starting_Point.md +3 -3
  5. package/docs/GUIDE/GUIDE_3_Field_Transformations.md +2 -0
  6. package/docs/GUIDE/GUIDE_7_Hooks_Data_Management_And_Plugins.md +1 -1
  7. package/docs/GUIDE/GUIDE_X_Autofiltering.md +63 -0
  8. package/docs/GUIDE/GUIDE_X_File_Uploads.md +17 -12
  9. package/docs/GUIDE/GUIDE_X_Row_Policies.md +603 -0
  10. package/docs/GUIDE/GUIDE_X_SocketIO.md +15 -10
  11. package/docs/GUIDE/GUIDE_X_URL_Management.md +93 -254
  12. package/docs/GUIDE/GUIDE_X_Writing_Plugins.md +2 -0
  13. package/docs/GUIDE/index.md +4 -1
  14. package/fixing_plan.md +112 -0
  15. package/index.js +2 -0
  16. package/package.json +1 -1
  17. package/plugins/core/bulk-operations-plugin.js +5 -4
  18. package/plugins/core/connectors/express-plugin.js +6 -1
  19. package/plugins/core/connectors/fastify-plugin.js +5 -1
  20. package/plugins/core/connectors/lib/connector-core.js +17 -11
  21. package/plugins/core/connectors/lib/transport-route-schemas.js +12 -60
  22. package/plugins/core/file-handling-plugin.js +126 -96
  23. package/plugins/core/lib/anyapi/anyapi-registry.js +21 -3
  24. package/plugins/core/lib/anyapi/schema-utils.js +1 -0
  25. package/plugins/core/lib/anyapi/utils/descriptor-helpers.js +7 -1
  26. package/plugins/core/lib/querying/knex-json-api-transformers-querying.js +20 -15
  27. package/plugins/core/lib/querying/knex-pagination-helpers.js +38 -84
  28. package/plugins/core/lib/querying/knex-relationship-includes.js +29 -28
  29. package/plugins/core/lib/querying/knex-window-queries.js +10 -77
  30. package/plugins/core/lib/querying/url-helpers.js +9 -21
  31. package/plugins/core/lib/querying-writing/connectors-query-parser.js +51 -0
  32. package/plugins/core/lib/querying-writing/database-capabilities.js +1 -1
  33. package/plugins/core/lib/querying-writing/database-value-normalizers.js +18 -9
  34. package/plugins/core/lib/querying-writing/knex-field-helpers.js +6 -1
  35. package/plugins/core/lib/querying-writing/relationship-contracts.js +56 -0
  36. package/plugins/core/lib/querying-writing/request-contracts.js +53 -4
  37. package/plugins/core/lib/querying-writing/return-record-settings.js +47 -0
  38. package/plugins/core/lib/querying-writing/schema-helpers.js +34 -41
  39. package/plugins/core/lib/storage/canonical-storage-mapping.js +2 -1
  40. package/plugins/core/lib/storage/storage-adapter.js +74 -13
  41. package/plugins/core/lib/writing/many-to-many-manipulations.js +4 -3
  42. package/plugins/core/lib/writing/relationship-processor.js +10 -1
  43. package/plugins/core/rest-api-anyapi-knex-plugin.js +210 -97
  44. package/plugins/core/rest-api-knex-plugin.js +70 -60
  45. package/plugins/core/rest-api-plugin-hooks/turn-scope-init-into-vars.js +8 -2
  46. package/plugins/core/rest-api-plugin-methods/common.js +85 -63
  47. package/plugins/core/rest-api-plugin-methods/delete-relationship.js +23 -13
  48. package/plugins/core/rest-api-plugin-methods/delete.js +2 -6
  49. package/plugins/core/rest-api-plugin-methods/get-related.js +15 -13
  50. package/plugins/core/rest-api-plugin-methods/get-relationship.js +1 -1
  51. package/plugins/core/rest-api-plugin-methods/patch-relationship.js +17 -6
  52. package/plugins/core/rest-api-plugin-methods/patch.js +3 -6
  53. package/plugins/core/rest-api-plugin-methods/post-relationship.js +24 -14
  54. package/plugins/core/rest-api-plugin-methods/post.js +4 -7
  55. package/plugins/core/rest-api-plugin-methods/put.js +4 -7
  56. package/plugins/core/rest-api-plugin.js +6 -36
  57. package/plugins/core/rest-api-row-policy-plugin.js +157 -0
  58. package/plugins/storage/local-storage.js +44 -19
  59. package/plugins/storage/s3-storage.js +21 -55
  60. package/tests/README.md +1 -1
  61. package/tests/anyapi-cursor-pagination.test.js +7 -1
  62. package/tests/anyapi-custom-idproperty.test.js +163 -0
  63. package/tests/fastify-plugin.test.js +28 -0
  64. package/tests/field-setters.test.js +10 -10
  65. package/tests/file-handling.test.js +118 -0
  66. package/tests/fixtures/api-configs.js +362 -4
  67. package/tests/full-jsonapi-belongsto-linkage.test.js +150 -0
  68. package/tests/http-connectors-parity.test.js +165 -5
  69. package/tests/local-storage.test.js +96 -0
  70. package/tests/pagination-cursor-multifield.test.js +121 -0
  71. package/tests/pagination-enhanced.test.js +66 -15
  72. package/tests/post-commit-hooks.test.js +120 -0
  73. package/tests/public-exports.test.js +15 -0
  74. package/tests/put-create-parity.test.js +216 -0
  75. package/tests/relationship-endpoints.test.js +259 -0
  76. package/tests/return-record-settings.test.js +184 -0
  77. package/tests/row-policy.test.js +433 -0
  78. package/tests/s3-storage.test.js +38 -0
  79. package/tests/searchschema-merge.test.js +17 -0
  80. package/tests/socketio.test.js +62 -0
  81. package/tests/storage-mapping.test.js +1 -0
  82. 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
- * **File Uploads** - Built-in support for file handling with multiple storage adapters (local, S3)
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', 'minimal', 'full') |
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 times for different filter types)
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**: Only QUERY method triggers `knexQueryFiltering` hooks
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 automatically generated based on the request headers.)
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 accept three string values:
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); // URLs auto-detected from request headers
26
+ await api.use(RestApiPlugin);
27
27
  await api.use(RestApiKnexPlugin, { knex });
28
- await api.use(ExpressPlugin, { mountPath: '/api' }); // Added: Express Plugin
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 during QUERY operations to apply filter conditions. This is a special hook that allows complex query modifications.
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 different backends (local, S3, etc.)
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 | Cloud storage |
118
+ | **Best For** | Local file storage | Tests, examples, S3-style URL demos |
119
119
 
120
- ### S3Storage
120
+ ### S3Storage (Mock/Demo)
121
121
 
122
- Saves files to Amazon S3 or S3-compatible storage:
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: false // Use mock mode? (default: true)
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, you'll need to implement the actual AWS SDK calls.
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 (removes `..` and `/`)
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: implement file deletion
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 dateDir = `${date.getFullYear()}/${String(date.getMonth() + 1).padStart(2, '0')}`;
540
- return `${dateDir}/${crypto.randomBytes(16).toString('hex')}`;
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/01/a7f8d9e2b4c6e1f3.jpg"
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