json-rest-api 1.0.29 → 2.1.0

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