json-rest-api 1.0.29 → 2.0.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (292) hide show
  1. package/LICENSE +10 -670
  2. package/LICENSE-GPL-3.0 +674 -0
  3. package/LICENSE-MIT +21 -0
  4. package/README.md +39 -30
  5. package/docs/API.md +744 -1273
  6. package/docs/GUIDE/01-getting-started.md +338 -0
  7. package/docs/GUIDE/02-resources-and-relationships.md +33 -0
  8. package/docs/GUIDE/03-running-example.md +87 -0
  9. package/docs/GUIDE/04-creating-and-querying.md +181 -0
  10. package/docs/GUIDE/05-belongs-to.md +177 -0
  11. package/docs/GUIDE/06-has-many.md +184 -0
  12. package/docs/GUIDE/07-polymorphic-relationships.md +191 -0
  13. package/docs/GUIDE/08-many-to-many.md +184 -0
  14. package/docs/GUIDE/09-pagination-and-sorting.md +149 -0
  15. package/docs/GUIDE/10-put-and-patch.md +174 -0
  16. package/docs/GUIDE/11-relationship-endpoints.md +174 -0
  17. package/docs/GUIDE/12-field-transformations.md +287 -0
  18. package/docs/GUIDE/13-hooks-and-lifecycle.md +479 -0
  19. package/docs/GUIDE/14-custom-resource-methods.md +125 -0
  20. package/docs/GUIDE/15-query-projections.md +266 -0
  21. package/docs/GUIDE/{GUIDE_X_Autofiltering.md → 16-autofiltering.md} +77 -15
  22. package/docs/GUIDE/{GUIDE_X_Row_Policies.md → 17-row-policies.md} +137 -44
  23. package/docs/GUIDE/18-labels.md +86 -0
  24. package/docs/GUIDE/19-managed-transactions.md +179 -0
  25. package/docs/GUIDE/20-transaction-outcomes.md +207 -0
  26. package/docs/GUIDE/21-schema-and-migrations.md +634 -0
  27. package/docs/GUIDE/22-version-field-migrations.md +208 -0
  28. package/docs/GUIDE/23-fastify.md +179 -0
  29. package/docs/GUIDE/24-cors.md +172 -0
  30. package/docs/GUIDE/{GUIDE_X_URL_Management.md → 25-url-management.md} +7 -1
  31. package/docs/GUIDE/26-file-uploads.md +278 -0
  32. package/docs/GUIDE/27-bulk-operations.md +277 -0
  33. package/docs/GUIDE/28-socketio.md +325 -0
  34. package/docs/GUIDE/29-writing-plugins.md +278 -0
  35. package/docs/GUIDE/30-backend-capabilities.md +100 -0
  36. package/docs/GUIDE/31-positioning.md +181 -0
  37. package/docs/GUIDE/{GUIDE_Y_Appendices.md → 32-date-and-time.md} +74 -30
  38. package/docs/GUIDE/33-migrating-to-v2.md +3827 -0
  39. package/docs/GUIDE/index.md +51 -59
  40. package/docs/QUICKSTART.md +120 -287
  41. package/docs/architecture.md +75 -0
  42. package/docs/contributing.md +178 -0
  43. package/index.d.ts +36 -0
  44. package/index.js +1 -22
  45. package/lib/enhanced-logger.js +91 -85
  46. package/lib/error-context.js +445 -5
  47. package/lib/error-formatter.js +219 -59
  48. package/lib/knex-transaction.js +64 -0
  49. package/lib/logger-types.d.ts +22 -0
  50. package/lib/missing-package.js +4 -0
  51. package/lib/rest-api-errors.js +103 -8
  52. package/lib/runtime/json-rest-api.js +172 -0
  53. package/lib/transaction-types.d.ts +59 -0
  54. package/package.json +97 -27
  55. package/plugins/core/bulk-operations-plugin.js +191 -184
  56. package/plugins/core/connectors/express-plugin.js +148 -219
  57. package/plugins/core/connectors/fastify-plugin.js +125 -82
  58. package/plugins/core/connectors/lib/busboy-detector.js +74 -156
  59. package/plugins/core/connectors/lib/connector-core.js +144 -20
  60. package/plugins/core/connectors/lib/formidable-detector.js +63 -120
  61. package/plugins/core/connectors/lib/http-validators.js +77 -0
  62. package/plugins/core/connectors/lib/multipart-helpers.js +18 -0
  63. package/plugins/core/connectors/lib/request-helpers.js +10 -1
  64. package/plugins/core/connectors/lib/transport-http-helpers.js +89 -8
  65. package/plugins/core/connectors/lib/transport-route-schemas.js +7 -4
  66. package/plugins/core/file-handling-plugin.js +87 -177
  67. package/plugins/core/lib/anyapi/anyapi-registry.js +213 -164
  68. package/plugins/core/lib/anyapi/canonical-link-store.js +507 -0
  69. package/plugins/core/lib/anyapi/canonical-relationship-reader.js +598 -0
  70. package/plugins/core/lib/anyapi/schema-utils.js +25 -16
  71. package/plugins/core/lib/anyapi/utils/descriptor-helpers.js +0 -64
  72. package/plugins/core/lib/dbIntrospection.js +228 -21
  73. package/plugins/core/lib/dbTablesOperations.js +409 -92
  74. package/plugins/core/lib/fractional-positioning.js +12 -48
  75. package/plugins/core/lib/querying/identifier-query.js +19 -0
  76. package/plugins/core/lib/querying/include-query-helpers.js +364 -0
  77. package/plugins/core/lib/querying/include-to-many.js +428 -0
  78. package/plugins/core/lib/querying/include-to-one.js +579 -0
  79. package/plugins/core/lib/querying/knex-cross-table-search.js +31 -307
  80. package/plugins/core/lib/querying/knex-json-api-transformers-querying.js +99 -401
  81. package/plugins/core/lib/querying/knex-pagination-helpers.js +48 -317
  82. package/plugins/core/lib/querying/knex-process-includes.js +5 -110
  83. package/plugins/core/lib/querying/knex-query-helpers-base.js +21 -56
  84. package/plugins/core/lib/querying/knex-query-helpers.js +298 -448
  85. package/plugins/core/lib/querying/knex-relationship-includes.js +120 -2138
  86. package/plugins/core/lib/querying/knex-window-queries.js +62 -253
  87. package/plugins/core/lib/querying/query-builder-utils.js +44 -4
  88. package/plugins/core/lib/querying/query-constraint.d.ts +12 -0
  89. package/plugins/core/lib/querying/query-constraint.js +27 -0
  90. package/plugins/core/lib/querying/query-field-sort-helpers.js +134 -37
  91. package/plugins/core/lib/querying/relationship-identifiers.js +208 -0
  92. package/plugins/core/lib/querying/sort-helpers.js +13 -1
  93. package/plugins/core/lib/querying/storage-adapter-utils.js +13 -5
  94. package/plugins/core/lib/querying/url-helpers.js +11 -4
  95. package/plugins/core/lib/querying-writing/compile-schemas.js +148 -249
  96. package/plugins/core/lib/querying-writing/connectors-query-parser.js +33 -118
  97. package/plugins/core/lib/querying-writing/database-capabilities.js +149 -223
  98. package/plugins/core/lib/querying-writing/database-value-normalizers.js +179 -194
  99. package/plugins/core/lib/querying-writing/default-data-helpers.js +15 -45
  100. package/plugins/core/lib/querying-writing/field-selection-types.d.ts +43 -0
  101. package/plugins/core/lib/querying-writing/field-utils.js +113 -92
  102. package/plugins/core/lib/querying-writing/knex-constants.d.ts +18 -0
  103. package/plugins/core/lib/querying-writing/knex-constants.js +2 -26
  104. package/plugins/core/lib/querying-writing/knex-field-helpers.js +100 -290
  105. package/plugins/core/lib/querying-writing/knex-json-api-transformers.js +31 -120
  106. package/plugins/core/lib/querying-writing/query-field-helpers.js +108 -32
  107. package/plugins/core/lib/querying-writing/query-field-types.d.ts +60 -0
  108. package/plugins/core/lib/querying-writing/relationship-contracts.js +33 -13
  109. package/plugins/core/lib/querying-writing/request-contracts.js +93 -38
  110. package/plugins/core/lib/querying-writing/resource-id-normalization.js +23 -5
  111. package/plugins/core/lib/querying-writing/response-options.js +35 -0
  112. package/plugins/core/lib/querying-writing/schema-helpers.js +203 -324
  113. package/plugins/core/lib/querying-writing/scope-validations.js +92 -290
  114. package/plugins/core/lib/querying-writing/simplified-helpers.js +113 -712
  115. package/plugins/core/lib/storage/canonical-data-helpers.js +683 -0
  116. package/plugins/core/lib/storage/canonical-storage-mapping.js +51 -20
  117. package/plugins/core/lib/storage/ordinary-data-helpers.js +710 -0
  118. package/plugins/core/lib/storage/storage-adapter.js +128 -62
  119. package/plugins/core/lib/storage/storage-mapping.js +78 -28
  120. package/plugins/core/lib/storage/storage-types.d.ts +502 -0
  121. package/plugins/core/lib/writing/many-to-many-manipulations.js +114 -284
  122. package/plugins/core/lib/writing/relationship-processor.js +94 -158
  123. package/plugins/core/lib/writing/resource-version.js +172 -0
  124. package/plugins/core/lib/writing/reverse-relationship-manipulations.js +110 -0
  125. package/plugins/core/rest-api-anyapi-knex-plugin.js +116 -2485
  126. package/plugins/core/rest-api-autofilter-plugin.js +17 -19
  127. package/plugins/core/rest-api-cors-plugin.js +52 -205
  128. package/plugins/core/rest-api-knex-plugin.js +45 -1077
  129. package/plugins/core/rest-api-label-plugin.js +25 -38
  130. package/plugins/core/rest-api-plugin-hooks/register-relationship-routes.js +5 -5
  131. package/plugins/core/rest-api-plugin-hooks/register-scope-routes.js +9 -6
  132. package/plugins/core/rest-api-plugin-hooks/turn-scope-init-into-vars.js +7 -26
  133. package/plugins/core/rest-api-plugin-hooks/validate-include-configurations.js +20 -26
  134. package/plugins/core/rest-api-plugin-methods/add-route.js +4 -6
  135. package/plugins/core/rest-api-plugin-methods/apply-query-filters.js +42 -13
  136. package/plugins/core/rest-api-plugin-methods/check-permissions.js +11 -5
  137. package/plugins/core/rest-api-plugin-methods/common.js +291 -395
  138. package/plugins/core/rest-api-plugin-methods/delete-relationship.js +49 -47
  139. package/plugins/core/rest-api-plugin-methods/delete.js +31 -25
  140. package/plugins/core/rest-api-plugin-methods/enrich-attributes.js +108 -148
  141. package/plugins/core/rest-api-plugin-methods/get-related.js +113 -305
  142. package/plugins/core/rest-api-plugin-methods/get-relationship.js +25 -18
  143. package/plugins/core/rest-api-plugin-methods/get.js +36 -68
  144. package/plugins/core/rest-api-plugin-methods/lifecycle-types.d.ts +225 -0
  145. package/plugins/core/rest-api-plugin-methods/patch-relationship.js +28 -25
  146. package/plugins/core/rest-api-plugin-methods/patch.js +46 -99
  147. package/plugins/core/rest-api-plugin-methods/post-relationship.js +49 -53
  148. package/plugins/core/rest-api-plugin-methods/post.js +51 -51
  149. package/plugins/core/rest-api-plugin-methods/put.js +69 -148
  150. package/plugins/core/rest-api-plugin-methods/query.js +37 -81
  151. package/plugins/core/rest-api-plugin-methods/release.js +2 -1
  152. package/plugins/core/rest-api-plugin.js +32 -46
  153. package/plugins/core/rest-api-positioning-plugin.js +183 -628
  154. package/plugins/core/rest-api-query-projections-plugin.js +19 -102
  155. package/plugins/core/rest-api-row-policy-plugin.js +3 -3
  156. package/plugins/core/socketio-plugin.js +364 -344
  157. package/plugins/storage/local-storage.js +40 -36
  158. package/types/autofilter.d.ts +57 -0
  159. package/types/bulk-methods.d.ts +63 -0
  160. package/types/errors.d.ts +118 -0
  161. package/types/fastify-options.d.ts +19 -0
  162. package/types/file-detectors.d.ts +33 -0
  163. package/types/file-storage.d.ts +48 -0
  164. package/types/hook-context.d.ts +180 -0
  165. package/types/plugin-options.d.ts +53 -0
  166. package/types/relationship-methods.d.ts +74 -0
  167. package/types/representations.d.ts +50 -0
  168. package/types/resource-methods.d.ts +95 -0
  169. package/types/resource-schema.d.ts +157 -0
  170. package/types/row-policy.d.ts +31 -0
  171. package/types/runtime.d.ts +132 -0
  172. package/types/transactions.d.ts +21 -0
  173. package/.claude/agents/library-guide-expert.md +0 -30
  174. package/.claude/agents/rest-api-internals-expert.md +0 -54
  175. package/.claude/settings.local.json +0 -70
  176. package/AGENTS.md +0 -34
  177. package/CLAUDE.md +0 -160
  178. package/CRUSH.md +0 -55
  179. package/JSKIT_BACKEND_MIGRATION_TRACKER.txt +0 -180
  180. package/SINGLE_VALIDATION_CONTRACT_TODO.txt +0 -172
  181. package/TODO.todo +0 -13
  182. package/connectors_ideas/fastify.js +0 -217
  183. package/connectors_ideas/koa-plugin.js +0 -112
  184. package/docs/.bundle/config +0 -2
  185. package/docs/COMPARISON.md +0 -433
  186. package/docs/GUIDE/GUIDE_1_Initial_Setup.md +0 -1057
  187. package/docs/GUIDE/GUIDE_2_1_The_Starting_Point.md +0 -88
  188. package/docs/GUIDE/GUIDE_2_2_Manipulating_And_Searching_Tables.md +0 -808
  189. package/docs/GUIDE/GUIDE_2_3_BelongsTo_Relationships.md +0 -491
  190. package/docs/GUIDE/GUIDE_2_4_HasMany_Records.md +0 -398
  191. package/docs/GUIDE/GUIDE_2_5_HasMany_Polymorphic.md +0 -898
  192. package/docs/GUIDE/GUIDE_2_6_Many_To_Many.md +0 -712
  193. package/docs/GUIDE/GUIDE_2_7_Pagination_And_Ordering.md +0 -1247
  194. package/docs/GUIDE/GUIDE_2_8_Effects_of_PUT_and_PATCH.md +0 -748
  195. package/docs/GUIDE/GUIDE_2_9_Relationships_Urls.md +0 -517
  196. package/docs/GUIDE/GUIDE_2_Data_And_Relations.md +0 -66
  197. package/docs/GUIDE/GUIDE_3_Field_Transformations.md +0 -1252
  198. package/docs/GUIDE/GUIDE_5_Non-Db_Resources.md +0 -1107
  199. package/docs/GUIDE/GUIDE_6_Positioning.md +0 -692
  200. package/docs/GUIDE/GUIDE_7_Hooks_Data_Management_And_Plugins.md +0 -1491
  201. package/docs/GUIDE/GUIDE_X_Bulk_Operations.md +0 -732
  202. package/docs/GUIDE/GUIDE_X_Cors.md +0 -1957
  203. package/docs/GUIDE/GUIDE_X_Fastify.md +0 -78
  204. package/docs/GUIDE/GUIDE_X_File_Uploads.md +0 -729
  205. package/docs/GUIDE/GUIDE_X_Knex_Schema_And_Migrations.md +0 -370
  206. package/docs/GUIDE/GUIDE_X_Query_Projections.md +0 -159
  207. package/docs/GUIDE/GUIDE_X_SocketIO.md +0 -1040
  208. package/docs/GUIDE/GUIDE_X_Writing_Plugins.md +0 -201
  209. package/docs/Gemfile +0 -6
  210. package/docs/Gemfile.lock +0 -308
  211. package/docs/ONBOARDING.md +0 -315
  212. package/docs/_config.yml +0 -36
  213. package/docs/_layouts/default.html +0 -123
  214. package/docs/apple-touch-icon.svg +0 -4
  215. package/docs/assets/css/style.scss +0 -392
  216. package/docs/favicon.svg +0 -4
  217. package/docs/index.md +0 -164
  218. package/docs/mini-guides/file-uploads-complete.md +0 -161
  219. package/eslint.config.js +0 -13
  220. package/examples/url-override-example.js +0 -137
  221. package/fixing_plan.md +0 -112
  222. package/plugins/core/lib/anyapi/query/anyapi-query-adapter.js +0 -657
  223. package/plugins/core/lib/querying-writing/return-record-settings.js +0 -47
  224. package/plugins/core/lib/writing/knex-json-api-transformers-writing.js +0 -110
  225. package/plugins/storage/s3-storage.js +0 -93
  226. package/quickTest.js +0 -136
  227. package/scripts/debug-anyapi-review-filter.mjs +0 -63
  228. package/scripts/inspect-anyapi-descriptor-country.mjs +0 -19
  229. package/scripts/inspect-anyapi-descriptor.mjs +0 -19
  230. package/scripts/run-docs.js +0 -133
  231. package/test-debug.js +0 -59
  232. package/tests/README.md +0 -202
  233. package/tests/TEST_TEMPLATE.test.js +0 -178
  234. package/tests/anyapi-basic.test.js +0 -105
  235. package/tests/anyapi-cursor-pagination.test.js +0 -148
  236. package/tests/anyapi-custom-idproperty.test.js +0 -163
  237. package/tests/autofilter.test.js +0 -463
  238. package/tests/bulk-operations.test.js +0 -657
  239. package/tests/computed-fields.test.js +0 -586
  240. package/tests/cors.test.js +0 -834
  241. package/tests/curl.test.js +0 -467
  242. package/tests/custom-id-mysql-returning-fallback.test.js +0 -128
  243. package/tests/custom-id-simplified-post.test.js +0 -75
  244. package/tests/custom-idproperty-relationships.test.js +0 -932
  245. package/tests/db-introspection.test.js +0 -521
  246. package/tests/db-table-operations.test.js +0 -798
  247. package/tests/fastify-plugin.test.js +0 -184
  248. package/tests/field-getters.test.js +0 -370
  249. package/tests/field-setters.test.js +0 -306
  250. package/tests/file-handling.test.js +0 -155
  251. package/tests/fixtures/api-configs.js +0 -2566
  252. package/tests/full-jsonapi-belongsto-linkage.test.js +0 -150
  253. package/tests/helpers/fake-fastify.js +0 -99
  254. package/tests/helpers/storage-mode.js +0 -65
  255. package/tests/helpers/test-utils.js +0 -203
  256. package/tests/http-connectors-parity.test.js +0 -395
  257. package/tests/id-normalization.test.js +0 -444
  258. package/tests/include-depth-validation.test.js +0 -331
  259. package/tests/includes.test.js +0 -427
  260. package/tests/local-storage.test.js +0 -96
  261. package/tests/logical-id-write-validation.test.js +0 -151
  262. package/tests/nested-includes.test.js +0 -617
  263. package/tests/pagination-boundaries.test.js +0 -154
  264. package/tests/pagination-contract.test.js +0 -122
  265. package/tests/pagination-cursor-multifield.test.js +0 -641
  266. package/tests/pagination-enhanced.test.js +0 -563
  267. package/tests/polymorphic-relationships.test.js +0 -347
  268. package/tests/polymorphic-simple.test.js +0 -148
  269. package/tests/positioning-advanced.test.js +0 -692
  270. package/tests/positioning-extended.test.js +0 -915
  271. package/tests/positioning-multifilter.test.js +0 -699
  272. package/tests/positioning.test.js +0 -560
  273. package/tests/post-commit-hooks.test.js +0 -120
  274. package/tests/projected-fields.test.js +0 -319
  275. package/tests/public-exports.test.js +0 -15
  276. package/tests/put-create-parity.test.js +0 -216
  277. package/tests/queries.test.js +0 -521
  278. package/tests/query-limits.test.js +0 -584
  279. package/tests/relationship-endpoints.test.js +0 -620
  280. package/tests/relationship-include-storage-adapter.test.js +0 -56
  281. package/tests/relationships.test.js +0 -681
  282. package/tests/rest-api.test.js +0 -337
  283. package/tests/return-record-settings.test.js +0 -184
  284. package/tests/row-policy.test.js +0 -489
  285. package/tests/s3-storage.test.js +0 -38
  286. package/tests/searchschema-knex-behavior.test.js +0 -187
  287. package/tests/searchschema-merge.test.js +0 -151
  288. package/tests/socketio.test.js +0 -860
  289. package/tests/storage-mapping.test.js +0 -362
  290. package/tests/temporal-boundaries.test.js +0 -208
  291. package/tests/test-development-guide.md +0 -370
  292. 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