json-rest-api 1.0.20 → 1.0.22

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 (173) hide show
  1. package/audit/00-charter.md +103 -0
  2. package/audit/01-scope-map.md +84 -0
  3. package/audit/02-coverage-matrix.csv +174 -0
  4. package/audit/03-invariants.md +123 -0
  5. package/audit/04-standards-rubric.md +56 -0
  6. package/audit/05-competitor-rubric.md +77 -0
  7. package/audit/06-findings.md +160 -0
  8. package/audit/07-risk-register.csv +10 -0
  9. package/audit/08-remediation-roadmap.md +177 -0
  10. package/audit/09-signoff-check.md +23 -0
  11. package/audit/10-final-synthesis.md +29 -0
  12. package/audit/evidence/code/all-files-json-rest-api.txt +168 -0
  13. package/audit/evidence/code/all-files-json-rest-schema.txt +12 -0
  14. package/audit/evidence/code/boundary-leak-checks.txt +72 -0
  15. package/audit/evidence/code/critical-pass1/json-rest-api__plugins__core__bulk-operations-plugin.js.txt +480 -0
  16. package/audit/evidence/code/critical-pass1/json-rest-api__plugins__core__connectors__express-plugin.js.txt +532 -0
  17. package/audit/evidence/code/critical-pass1/json-rest-api__plugins__core__connectors__lib__busboy-detector.js.txt +175 -0
  18. package/audit/evidence/code/critical-pass1/json-rest-api__plugins__core__connectors__lib__formidable-detector.js.txt +138 -0
  19. package/audit/evidence/code/critical-pass1/json-rest-api__plugins__core__connectors__lib__request-helpers.js.txt +222 -0
  20. package/audit/evidence/code/critical-pass1/json-rest-api__plugins__core__file-handling-plugin.js.txt +342 -0
  21. package/audit/evidence/code/critical-pass1/json-rest-api__plugins__core__lib__dbTablesOperations.js.txt +304 -0
  22. package/audit/evidence/code/critical-pass1/json-rest-api__plugins__core__lib__fractional-positioning.js.txt +188 -0
  23. package/audit/evidence/code/critical-pass1/json-rest-api__plugins__core__lib__querying-writing__compile-schemas.js.txt +336 -0
  24. package/audit/evidence/code/critical-pass1/json-rest-api__plugins__core__lib__querying-writing__connectors-query-parser.js.txt +165 -0
  25. package/audit/evidence/code/critical-pass1/json-rest-api__plugins__core__lib__querying-writing__database-capabilities.js.txt +265 -0
  26. package/audit/evidence/code/critical-pass1/json-rest-api__plugins__core__lib__querying-writing__database-value-normalizers.js.txt +369 -0
  27. package/audit/evidence/code/critical-pass1/json-rest-api__plugins__core__lib__querying-writing__default-data-helpers.js.txt +72 -0
  28. package/audit/evidence/code/critical-pass1/json-rest-api__plugins__core__lib__querying-writing__field-utils.js.txt +139 -0
  29. package/audit/evidence/code/critical-pass1/json-rest-api__plugins__core__lib__querying-writing__knex-constants.js.txt +45 -0
  30. package/audit/evidence/code/critical-pass1/json-rest-api__plugins__core__lib__querying-writing__knex-field-helpers.js.txt +396 -0
  31. package/audit/evidence/code/critical-pass1/json-rest-api__plugins__core__lib__querying-writing__knex-json-api-transformers.js.txt +162 -0
  32. package/audit/evidence/code/critical-pass1/json-rest-api__plugins__core__lib__querying-writing__payload-validators.js.txt +1314 -0
  33. package/audit/evidence/code/critical-pass1/json-rest-api__plugins__core__lib__querying-writing__schema-helpers.js.txt +488 -0
  34. package/audit/evidence/code/critical-pass1/json-rest-api__plugins__core__lib__querying-writing__scope-validations.js.txt +484 -0
  35. package/audit/evidence/code/critical-pass1/json-rest-api__plugins__core__lib__querying-writing__simplified-helpers.js.txt +857 -0
  36. package/audit/evidence/code/critical-pass1/json-rest-api__plugins__core__lib__querying__knex-cross-table-search.js.txt +565 -0
  37. package/audit/evidence/code/critical-pass1/json-rest-api__plugins__core__lib__querying__knex-json-api-transformers-querying.js.txt +485 -0
  38. package/audit/evidence/code/critical-pass1/json-rest-api__plugins__core__lib__querying__knex-pagination-helpers.js.txt +560 -0
  39. package/audit/evidence/code/critical-pass1/json-rest-api__plugins__core__lib__querying__knex-process-includes.js.txt +167 -0
  40. package/audit/evidence/code/critical-pass1/json-rest-api__plugins__core__lib__querying__knex-query-helpers-base.js.txt +109 -0
  41. package/audit/evidence/code/critical-pass1/json-rest-api__plugins__core__lib__querying__knex-query-helpers.js.txt +1064 -0
  42. package/audit/evidence/code/critical-pass1/json-rest-api__plugins__core__lib__querying__knex-relationship-includes.js.txt +1983 -0
  43. package/audit/evidence/code/critical-pass1/json-rest-api__plugins__core__lib__querying__knex-window-queries.js.txt +328 -0
  44. package/audit/evidence/code/critical-pass1/json-rest-api__plugins__core__lib__querying__url-helpers.js.txt +119 -0
  45. package/audit/evidence/code/critical-pass1/json-rest-api__plugins__core__lib__storage__storage-adapter.js.txt +160 -0
  46. package/audit/evidence/code/critical-pass1/json-rest-api__plugins__core__lib__writing__many-to-many-manipulations.js.txt +300 -0
  47. package/audit/evidence/code/critical-pass1/json-rest-api__plugins__core__lib__writing__relationship-processor.js.txt +245 -0
  48. package/audit/evidence/code/critical-pass1/json-rest-api__plugins__core__rest-api-access.js.txt +459 -0
  49. package/audit/evidence/code/critical-pass1/json-rest-api__plugins__core__rest-api-cors-plugin.js.txt +237 -0
  50. package/audit/evidence/code/critical-pass1/json-rest-api__plugins__core__rest-api-knex-plugin.js.txt +1135 -0
  51. package/audit/evidence/code/critical-pass1/json-rest-api__plugins__core__rest-api-multihome-plugin.js.txt +314 -0
  52. package/audit/evidence/code/critical-pass1/json-rest-api__plugins__core__rest-api-plugin-hooks__compile-resource-schemas.js.txt +7 -0
  53. package/audit/evidence/code/critical-pass1/json-rest-api__plugins__core__rest-api-plugin-hooks__register-relationship-routes.js.txt +71 -0
  54. package/audit/evidence/code/critical-pass1/json-rest-api__plugins__core__rest-api-plugin-hooks__register-scope-routes.js.txt +102 -0
  55. package/audit/evidence/code/critical-pass1/json-rest-api__plugins__core__rest-api-plugin-hooks__turn-scope-init-into-vars.js.txt +49 -0
  56. package/audit/evidence/code/critical-pass1/json-rest-api__plugins__core__rest-api-plugin-hooks__validate-include-configurations.js.txt +39 -0
  57. package/audit/evidence/code/critical-pass1/json-rest-api__plugins__core__rest-api-plugin-methods__add-route.js.txt +23 -0
  58. package/audit/evidence/code/critical-pass1/json-rest-api__plugins__core__rest-api-plugin-methods__check-permissions.js.txt +13 -0
  59. package/audit/evidence/code/critical-pass1/json-rest-api__plugins__core__rest-api-plugin-methods__common.js.txt +639 -0
  60. package/audit/evidence/code/critical-pass1/json-rest-api__plugins__core__rest-api-plugin-methods__delete-relationship.js.txt +117 -0
  61. package/audit/evidence/code/critical-pass1/json-rest-api__plugins__core__rest-api-plugin-methods__delete.js.txt +104 -0
  62. package/audit/evidence/code/critical-pass1/json-rest-api__plugins__core__rest-api-plugin-methods__enrich-attributes.js.txt +199 -0
  63. package/audit/evidence/code/critical-pass1/json-rest-api__plugins__core__rest-api-plugin-methods__get-related.js.txt +353 -0
  64. package/audit/evidence/code/critical-pass1/json-rest-api__plugins__core__rest-api-plugin-methods__get-relationship.js.txt +61 -0
  65. package/audit/evidence/code/critical-pass1/json-rest-api__plugins__core__rest-api-plugin-methods__get.js.txt +184 -0
  66. package/audit/evidence/code/critical-pass1/json-rest-api__plugins__core__rest-api-plugin-methods__patch-relationship.js.txt +55 -0
  67. package/audit/evidence/code/critical-pass1/json-rest-api__plugins__core__rest-api-plugin-methods__patch.js.txt +191 -0
  68. package/audit/evidence/code/critical-pass1/json-rest-api__plugins__core__rest-api-plugin-methods__post-relationship.js.txt +110 -0
  69. package/audit/evidence/code/critical-pass1/json-rest-api__plugins__core__rest-api-plugin-methods__post.js.txt +168 -0
  70. package/audit/evidence/code/critical-pass1/json-rest-api__plugins__core__rest-api-plugin-methods__put.js.txt +253 -0
  71. package/audit/evidence/code/critical-pass1/json-rest-api__plugins__core__rest-api-plugin-methods__query.js.txt +207 -0
  72. package/audit/evidence/code/critical-pass1/json-rest-api__plugins__core__rest-api-plugin-methods__release.js.txt +12 -0
  73. package/audit/evidence/code/critical-pass1/json-rest-api__plugins__core__rest-api-plugin.js.txt +165 -0
  74. package/audit/evidence/code/critical-pass1/json-rest-api__plugins__core__rest-api-positioning-plugin.js.txt +668 -0
  75. package/audit/evidence/code/critical-pass1/json-rest-api__plugins__core__socketio-plugin.js.txt +621 -0
  76. package/audit/evidence/code/critical-pass1/json-rest-api__plugins__storage__local-storage.js.txt +218 -0
  77. package/audit/evidence/code/critical-pass1/json-rest-api__plugins__storage__s3-storage.js.txt +124 -0
  78. package/audit/evidence/code/critical-pass1/json-rest-schema__src__CorePlugin.js.txt +269 -0
  79. package/audit/evidence/code/critical-pass1/json-rest-schema__src__Schema.js.txt +242 -0
  80. package/audit/evidence/code/critical-pass1/json-rest-schema__src__index.js.txt +77 -0
  81. package/audit/evidence/code/critical-pass1-risk-keywords.txt +238 -0
  82. package/audit/evidence/code/critical-pass1-summary.csv +67 -0
  83. package/audit/evidence/code/critical-pass2-summary.csv +67 -0
  84. package/audit/evidence/code/early-silent-zones-no-test-token.txt +57 -0
  85. package/audit/evidence/code/error-model-signals.txt +98 -0
  86. package/audit/evidence/code/git-files-json-rest-api.txt +171 -0
  87. package/audit/evidence/code/git-files-json-rest-schema.txt +27 -0
  88. package/audit/evidence/code/hook-flow-signals.txt +282 -0
  89. package/audit/evidence/code/idproperty-signals.txt +157 -0
  90. package/audit/evidence/code/import-cycles-non-anyapi.txt +3 -0
  91. package/audit/evidence/code/import-hotspots-non-anyapi.txt +96 -0
  92. package/audit/evidence/code/imports-json-rest-api.txt +354 -0
  93. package/audit/evidence/code/imports-json-rest-schema.txt +15 -0
  94. package/audit/evidence/code/in-scope-files-json-rest-api.txt +160 -0
  95. package/audit/evidence/code/in-scope-files-json-rest-schema.txt +13 -0
  96. package/audit/evidence/code/non-anyapi-core-files.txt +101 -0
  97. package/audit/evidence/code/permission-signals.txt +193 -0
  98. package/audit/evidence/code/query-include-signals.txt +687 -0
  99. package/audit/evidence/code/transaction-signals.txt +28 -0
  100. package/audit/evidence/code/validation-signals.txt +650 -0
  101. package/audit/evidence/code/write-path-signals.txt +61 -0
  102. package/audit/evidence/docs/docs-claims-signals.txt +1075 -0
  103. package/audit/evidence/docs/schema-docs-claims-signals.txt +179 -0
  104. package/audit/evidence/external/corpus-freeze.md +21 -0
  105. package/audit/evidence/external/npm-competitor-metadata.txt +70 -0
  106. package/audit/evidence/external/npm-download-signals.txt +23 -0
  107. package/audit/evidence/external/npm-jsonapi-server.txt +8 -0
  108. package/audit/evidence/external/npm-modern-jsonapi-metadata.txt +38 -0
  109. package/audit/evidence/external/readme-fortune-signals.txt +26 -0
  110. package/audit/evidence/external/readme-fortune.md +165 -0
  111. package/audit/evidence/external/readme-json-api-serializer-signals.txt +80 -0
  112. package/audit/evidence/external/readme-json-api-serializer.md +690 -0
  113. package/audit/evidence/external/readme-jsonapi-serde-server-signals.txt +0 -0
  114. package/audit/evidence/external/readme-jsonapi-serde-server.md +22 -0
  115. package/audit/evidence/external/readme-jsonapi-serializer-signals.txt +63 -0
  116. package/audit/evidence/external/readme-jsonapi-serializer.md +315 -0
  117. package/audit/evidence/external/readme-jsonapi-server-signals.txt +19 -0
  118. package/audit/evidence/external/readme-jsonapi-server.md +104 -0
  119. package/audit/evidence/external/readme-nestjs-json-api-signals.txt +4 -0
  120. package/audit/evidence/external/readme-nestjs-json-api.md +94 -0
  121. package/audit/evidence/external/readme-yaser-json-api-nestjs-signals.txt +47 -0
  122. package/audit/evidence/external/readme-yaser-json-api-nestjs.md +447 -0
  123. package/audit/evidence/external/standard-jsonapi-snippets.txt +84 -0
  124. package/audit/evidence/external/standard-rfc-snippets.txt +168 -0
  125. package/audit/evidence/external/standards-sources.txt +6 -0
  126. package/audit/evidence/runtime/baseline-commits.txt +2 -0
  127. package/audit/evidence/runtime/db-adapter-assumptions.txt +3 -0
  128. package/audit/evidence/runtime/environment.txt +4 -0
  129. package/audit/evidence/runtime/invariant-runtime-mapping.md +38 -0
  130. package/audit/evidence/runtime/probe-patch-relationship-error.exitcode +1 -0
  131. package/audit/evidence/runtime/probe-patch-relationship-error.log +76 -0
  132. package/audit/evidence/runtime/probe-patch-relationship-error.mjs +67 -0
  133. package/audit/evidence/runtime/probe-patch-relationship-referenceerror.exitcode +1 -0
  134. package/audit/evidence/runtime/probe-patch-relationship-referenceerror.log +62 -0
  135. package/audit/evidence/runtime/probe-patch-relationship-referenceerror.mjs +47 -0
  136. package/audit/evidence/runtime/test-exitcodes.txt +4 -0
  137. package/audit/evidence/runtime/test-summary.md +107 -0
  138. package/audit/evidence/runtime/trace-anchors.txt +61 -0
  139. package/audit/evidence/tests/access-multitenancy-test-signals.txt +125 -0
  140. package/audit/evidence/tests/failing-tests-summary.txt +25 -0
  141. package/audit/evidence/tests/npm-test-json-rest-api.exitcode +1 -0
  142. package/audit/evidence/tests/npm-test-json-rest-api.log +5885 -0
  143. package/audit/evidence/tests/npm-test-json-rest-schema.exitcode +1 -0
  144. package/audit/evidence/tests/npm-test-json-rest-schema.log +356 -0
  145. package/audit/evidence/tests/query-include-test-signals.txt +948 -0
  146. package/audit/evidence/tests/relationship-test-signals.txt +223 -0
  147. package/audit/evidence/tests/skipped-tests.txt +10 -0
  148. package/audit/evidence/tests/test-suite-list-non-anyapi.txt +30 -0
  149. package/audit/evidence/tests/test-suite-list.txt +32 -0
  150. package/audit/subsystems/docs-drift.md +49 -0
  151. package/audit/subsystems/pass2-cross-review.md +131 -0
  152. package/audit/subsystems/plugins-access.md +51 -0
  153. package/audit/subsystems/plugins-bulk.md +50 -0
  154. package/audit/subsystems/plugins-cors.md +48 -0
  155. package/audit/subsystems/plugins-file.md +53 -0
  156. package/audit/subsystems/plugins-multihome.md +50 -0
  157. package/audit/subsystems/plugins-positioning.md +54 -0
  158. package/audit/subsystems/plugins-socketio.md +50 -0
  159. package/audit/subsystems/query-engine.md +61 -0
  160. package/audit/subsystems/relationships.md +55 -0
  161. package/audit/subsystems/rest-orchestration.md +66 -0
  162. package/audit/subsystems/schema-core.md +64 -0
  163. package/audit/subsystems/storage-knex.md +57 -0
  164. package/audit/subsystems/tests.md +48 -0
  165. package/audit/subsystems/transport-express.md +55 -0
  166. package/audit/traces/trace-read-get.md +22 -0
  167. package/audit/traces/trace-read-query.md +25 -0
  168. package/audit/traces/trace-relationship-endpoints.md +31 -0
  169. package/audit/traces/trace-write-delete.md +17 -0
  170. package/audit/traces/trace-write-patch.md +26 -0
  171. package/audit/traces/trace-write-post.md +22 -0
  172. package/audit/traces/trace-write-put.md +23 -0
  173. package/package.json +5 -5
@@ -0,0 +1,342 @@
1
+ 1 /**
2
+ 2 * File Handling Plugin for JSON REST API
3
+ 3 *
4
+ 4 * This plugin provides automatic file upload handling based on schema definitions.
5
+ 5 * It works with any protocol connector (HTTP, Express, WebSocket, etc.) by using
6
+ 6 * a detector registry pattern.
7
+ 7 *
8
+ 8 * Features:
9
+ 9 * - Schema-driven: Detects file fields from type: 'file' in schemas
10
+ 10 * - Protocol-agnostic: Works with any connector that registers a detector
11
+ 11 * - Storage pluggable: Different fields can use different storage backends
12
+ 12 * - Zero configuration: Just define file fields in your schema
13
+ 13 *
14
+ 14 * Usage:
15
+ 15 * ```javascript
16
+ 16 * // 1. Define schema with file fields
17
+ 17 * const imageSchema = {
18
+ 18 * title: { type: 'string' },
19
+ 19 * uploadedImage: {
20
+ 20 * type: 'file',
21
+ 21 * storage: S3Storage,
22
+ 22 * accepts: ['image/jpeg', 'image/png']
23
+ 23 * }
24
+ 24 * };
25
+ 25 *
26
+ 26 * // 2. Use plugins (order matters - file-handling depends on rest-api)
27
+ 27 * api.use(RestApiPlugin);
28
+ 28 * api.use(FileHandlingPlugin);
29
+ 29 * api.use(ExpressPlugin); // Or any other connector
30
+ 30 *
31
+ 31 * // 3. Files are automatically handled!
32
+ 32 * ```
33
+ 33 */
34
+ 34
35
+ 35 import { RestApiValidationError } from '../../lib/rest-api-errors.js'
36
+ 36
37
+ 37 export const FileHandlingPlugin = {
38
+ 38 name: 'file-handling',
39
+ 39 dependencies: ['rest-api'],
40
+ 40
41
+ 41 install ({ addHook, helpers, scopes, log, vars, on, api }) {
42
+ 42 // Track which scopes have file fields
43
+ 43 const fileScopes = new Map() // scopeName -> fileField[]
44
+ 44
45
+ 45 // Registry of file detectors from various protocols
46
+ 46 const detectorRegistry = []
47
+ 47
48
+ 48 /**
49
+ 49 * Register a file detector from a protocol plugin
50
+ 50 *
51
+ 51 * @param {Object} detector - The detector object
52
+ 52 * @param {string} detector.name - Name of the detector (e.g., 'http-multipart')
53
+ 53 * @param {Function} detector.detect - Function to check if this detector applies
54
+ 54 * @param {Function} detector.parse - Function to parse files from the request
55
+ 55 */
56
+ 56 api.rest.registerFileDetector = (detector) => {
57
+ 57 if (!detector || !detector.name || !detector.detect || !detector.parse) {
58
+ 58 throw new Error('File detector must have name, detect(), and parse() properties')
59
+ 59 }
60
+ 60
61
+ 61 detectorRegistry.push(detector)
62
+ 62 log.debug(`Registered file detector: ${detector.name}`)
63
+ 63 }
64
+ 64
65
+ 65 // Store detectors array for inspection
66
+ 66 api.rest.fileDetectors = detectorRegistry
67
+ 67
68
+ 68 /**
69
+ 69 * Analyze a scope's schema to find file fields
70
+ 70 */
71
+ 71 const analyzeScopeSchema = (scopeName, scopeOptions) => {
72
+ 72 const schema = scopeOptions?.schema
73
+ 73 if (!schema) return
74
+ 74
75
+ 75 const fileFields = []
76
+ 76
77
+ 77 // Look for fields with type: 'file'
78
+ 78 for (const [fieldName, fieldConfig] of Object.entries(schema)) {
79
+ 79 if (fieldConfig && fieldConfig.type === 'file') {
80
+ 80 fileFields.push({
81
+ 81 field: fieldName,
82
+ 82 storage: fieldConfig.storage,
83
+ 83 accepts: fieldConfig.accepts || ['*'],
84
+ 84 maxSize: fieldConfig.maxSize,
85
+ 85 required: fieldConfig.required || false
86
+ 86 })
87
+ 87 }
88
+ 88 }
89
+ 89
90
+ 90 if (fileFields.length > 0) {
91
+ 91 fileScopes.set(scopeName, fileFields)
92
+ 92 log.info(`Scope '${scopeName}' has ${fileFields.length} file field(s): ${fileFields.map(f => f.field).join(', ')}`)
93
+ 93 }
94
+ 94 }
95
+ 95
96
+ 96 // Analyze existing scopes
97
+ 97 for (const [scopeName, scope] of Object.entries(scopes)) {
98
+ 98 if (scope._scopeOptions) {
99
+ 99 analyzeScopeSchema(scopeName, scope._scopeOptions)
100
+ 100 }
101
+ 101 }
102
+ 102
103
+ 103 // Listen for new scopes being added
104
+ 104 addHook('scope:added', 'analyzeFileFields', {}, ({ context }) => {
105
+ 105 const { scopeName, scopeOptions } = context
106
+ 106 analyzeScopeSchema(scopeName, scopeOptions)
107
+ 107 })
108
+ 108
109
+ 109 /**
110
+ 110 * Process files for a scope if it has file fields
111
+ 111 */
112
+ 112 const processFiles = async (scopeName, params, context) => {
113
+ 113 const fileFields = fileScopes.get(scopeName)
114
+ 114 if (!fileFields || fileFields.length === 0) {
115
+ 115 return // This scope doesn't have file fields
116
+ 116 }
117
+ 117
118
+ 118 // Try each detector to see if we have files
119
+ 119 let parsed = null
120
+ 120 let detectorUsed = null
121
+ 121
122
+ 122 for (const detector of detectorRegistry) {
123
+ 123 try {
124
+ 124 if (await detector.detect(params, context)) {
125
+ 125 log.debug(`Detector '${detector.name}' matched for scope '${scopeName}'`)
126
+ 126 parsed = await detector.parse(params, context)
127
+ 127 detectorUsed = detector.name
128
+ 128 break
129
+ 129 }
130
+ 130 } catch (error) {
131
+ 131 log.warn(`Detector '${detector.name}' failed:`, error)
132
+ 132 }
133
+ 133 }
134
+ 134
135
+ 135 if (!parsed) {
136
+ 136 // No files detected - check if any were required
137
+ 137 for (const fieldConfig of fileFields) {
138
+ 138 if (fieldConfig.required && !params.inputRecord?.data?.attributes?.[fieldConfig.field]) {
139
+ 139 throw new RestApiValidationError(
140
+ 140 `Required file field '${fieldConfig.field}' is missing`,
141
+ 141 {
142
+ 142 fields: [fieldConfig.field],
143
+ 143 violations: [{
144
+ 144 field: fieldConfig.field,
145
+ 145 message: 'This field is required'
146
+ 146 }]
147
+ 147 }
148
+ 148 )
149
+ 149 }
150
+ 150 }
151
+ 151 return
152
+ 152 }
153
+ 153
154
+ 154 log.debug(`Processing files with detector '${detectorUsed}'`)
155
+ 155 const { fields, files } = parsed
156
+ 156
157
+ 157 // Process each file field defined in schema
158
+ 158 for (const fieldConfig of fileFields) {
159
+ 159 const file = files[fieldConfig.field]
160
+ 160
161
+ 161 if (!file) {
162
+ 162 if (fieldConfig.required) {
163
+ 163 throw new RestApiValidationError(
164
+ 164 `Required file field '${fieldConfig.field}' is missing`,
165
+ 165 {
166
+ 166 fields: [fieldConfig.field],
167
+ 167 violations: [{
168
+ 168 field: fieldConfig.field,
169
+ 169 message: 'This field is required'
170
+ 170 }]
171
+ 171 }
172
+ 172 )
173
+ 173 }
174
+ 174 continue
175
+ 175 }
176
+ 176
177
+ 177 // Validate mime type
178
+ 178 if (fieldConfig.accepts[0] !== '*') {
179
+ 179 const acceptable = fieldConfig.accepts.some(pattern => {
180
+ 180 if (pattern.endsWith('/*')) {
181
+ 181 // e.g., 'image/*'
182
+ 182 const prefix = pattern.slice(0, -2)
183
+ 183 return file.mimetype.startsWith(prefix + '/')
184
+ 184 }
185
+ 185 return file.mimetype === pattern
186
+ 186 })
187
+ 187
188
+ 188 if (!acceptable) {
189
+ 189 throw new RestApiValidationError(
190
+ 190 `Invalid file type for field '${fieldConfig.field}'`,
191
+ 191 {
192
+ 192 fields: [fieldConfig.field],
193
+ 193 violations: [{
194
+ 194 field: fieldConfig.field,
195
+ 195 message: `Expected ${fieldConfig.accepts.join(' or ')}, got ${file.mimetype}`
196
+ 196 }]
197
+ 197 }
198
+ 198 )
199
+ 199 }
200
+ 200 }
201
+ 201
202
+ 202 // Validate file size
203
+ 203 if (fieldConfig.maxSize) {
204
+ 204 const maxBytes = parseSize(fieldConfig.maxSize)
205
+ 205 if (file.size > maxBytes) {
206
+ 206 throw new RestApiValidationError(
207
+ 207 `File too large for field '${fieldConfig.field}'`,
208
+ 208 {
209
+ 209 fields: [fieldConfig.field],
210
+ 210 violations: [{
211
+ 211 field: fieldConfig.field,
212
+ 212 message: `Maximum size is ${fieldConfig.maxSize}, got ${formatSize(file.size)}`
213
+ 213 }]
214
+ 214 }
215
+ 215 )
216
+ 216 }
217
+ 217 }
218
+ 218
219
+ 219 // Upload to storage
220
+ 220 if (!fieldConfig.storage) {
221
+ 221 throw new Error(`No storage configured for file field '${fieldConfig.field}'`)
222
+ 222 }
223
+ 223
224
+ 224 try {
225
+ 225 const storedUrl = await fieldConfig.storage.upload(file)
226
+ 226 fields[fieldConfig.field] = storedUrl
227
+ 227 log.debug(`Uploaded file for field '${fieldConfig.field}' to: ${storedUrl}`)
228
+ 228 } catch (error) {
229
+ 229 // Cleanup if file has cleanup function
230
+ 230 if (file.cleanup) {
231
+ 231 try {
232
+ 232 await file.cleanup()
233
+ 233 } catch (cleanupError) {
234
+ 234 log.warn('Failed to cleanup file after upload error:', cleanupError)
235
+ 235 }
236
+ 236 }
237
+ 237
238
+ 238 throw new RestApiValidationError(
239
+ 239 `Failed to upload file for field '${fieldConfig.field}': ${error.message}`,
240
+ 240 {
241
+ 241 fields: [fieldConfig.field],
242
+ 242 violations: [{
243
+ 243 field: fieldConfig.field,
244
+ 244 message: error.message
245
+ 245 }]
246
+ 246 }
247
+ 247 )
248
+ 248 }
249
+ 249 }
250
+ 250
251
+ 251 // Replace inputRecord with processed data
252
+ 252 if (!params.inputRecord) {
253
+ 253 params.inputRecord = { data: { attributes: {} } }
254
+ 254 }
255
+ 255 if (!params.inputRecord.data) {
256
+ 256 params.inputRecord.data = { attributes: {} }
257
+ 257 }
258
+ 258 if (!params.inputRecord.data.attributes) {
259
+ 259 params.inputRecord.data.attributes = {}
260
+ 260 }
261
+ 261
262
+ 262 // Merge fields into attributes
263
+ 263 Object.assign(params.inputRecord.data.attributes, fields)
264
+ 264
265
+ 265 // Cleanup any remaining temp files
266
+ 266 for (const file of Object.values(files)) {
267
+ 267 if (file.cleanup) {
268
+ 268 try {
269
+ 269 await file.cleanup()
270
+ 270 } catch (error) {
271
+ 271 log.warn('Failed to cleanup temp file:', error)
272
+ 272 }
273
+ 273 }
274
+ 274 }
275
+ 275 }
276
+ 276
277
+ 277 /**
278
+ 278 * Hook into REST API methods to process files
279
+ 279 */
280
+ 280 addHook('beforeProcessing', 'processFiles', {}, async ({ context }) => {
281
+ 281 const method = context.method
282
+ 282 const scopeName = context.scopeName
283
+ 283 const params = context.params
284
+ 284
285
+ 285 // Only process for mutation methods
286
+ 286 if (!['post', 'put', 'patch'].includes(method)) {
287
+ 287 return
288
+ 288 }
289
+ 289
290
+ 290 // Process files if this scope has file fields
291
+ 291 await processFiles(scopeName, params, context)
292
+ 292 })
293
+ 293
294
+ 294 log.info('File handling plugin initialized successfully')
295
+ 295 }
296
+ 296 }
297
+ 297
298
+ 298 /**
299
+ 299 * Parse size string to bytes
300
+ 300 * @param {string} size - Size string like '10mb', '1.5GB'
301
+ 301 * @returns {number} Size in bytes
302
+ 302 */
303
+ 303 function parseSize (size) {
304
+ 304 const units = {
305
+ 305 b: 1,
306
+ 306 kb: 1024,
307
+ 307 mb: 1024 * 1024,
308
+ 308 gb: 1024 * 1024 * 1024
309
+ 309 }
310
+ 310
311
+ 311 const match = size.toLowerCase().match(/^(\d+(?:\.\d+)?)\s*([a-z]+)$/)
312
+ 312 if (!match) {
313
+ 313 throw new Error(`Invalid size format: ${size}`)
314
+ 314 }
315
+ 315
316
+ 316 const [, num, unit] = match
317
+ 317 const multiplier = units[unit]
318
+ 318
319
+ 319 if (!multiplier) {
320
+ 320 throw new Error(`Unknown size unit: ${unit}`)
321
+ 321 }
322
+ 322
323
+ 323 return parseFloat(num) * multiplier
324
+ 324 }
325
+ 325
326
+ 326 /**
327
+ 327 * Format bytes to human readable size
328
+ 328 * @param {number} bytes - Size in bytes
329
+ 329 * @returns {string} Human readable size
330
+ 330 */
331
+ 331 function formatSize (bytes) {
332
+ 332 const units = ['B', 'KB', 'MB', 'GB']
333
+ 333 let size = bytes
334
+ 334 let unitIndex = 0
335
+ 335
336
+ 336 while (size >= 1024 && unitIndex < units.length - 1) {
337
+ 337 size /= 1024
338
+ 338 unitIndex++
339
+ 339 }
340
+ 340
341
+ 341 return `${size.toFixed(1)}${units[unitIndex]}`
342
+ 342 }
@@ -0,0 +1,304 @@
1
+ 1 /**
2
+ 2 * @file Creates Knex table definitions from json-rest-schema definitions
3
+ 3 */
4
+ 4
5
+ 5 /**
6
+ 6 * Maps json-rest-schema types to Knex column types
7
+ 7 * @param {object} table - The Knex table builder instance
8
+ 8 * @param {string} columnName - The name of the column
9
+ 9 * @param {object} definition - The schema definition for the field
10
+ 10 * @returns {object} The Knex column builder instance
11
+ 11 */
12
+ 12 function mapTypeToKnex (table, columnName, definition) {
13
+ 13 const { type, precision, scale, maxLength, unsigned } = definition
14
+ 14
15
+ 15 switch (type) {
16
+ 16 case 'string':
17
+ 17 return maxLength ? table.string(columnName, maxLength) : table.string(columnName)
18
+ 18
19
+ 19 case 'number':
20
+ 20 if (precision !== undefined && scale !== undefined) {
21
+ 21 return table.decimal(columnName, precision, scale)
22
+ 22 }
23
+ 23 return table.float(columnName)
24
+ 24
25
+ 25 case 'id':
26
+ 26 const col = table.integer(columnName)
27
+ 27 return unsigned !== false ? col.unsigned() : col
28
+ 28
29
+ 29 case 'boolean':
30
+ 30 return table.boolean(columnName)
31
+ 31
32
+ 32 case 'date':
33
+ 33 return table.date(columnName)
34
+ 34
35
+ 35 case 'dateTime':
36
+ 36 return table.datetime(columnName)
37
+ 37
38
+ 38 case 'time':
39
+ 39 return table.time(columnName)
40
+ 40
41
+ 41 case 'timestamp':
42
+ 42 return table.integer(columnName)
43
+ 43
44
+ 44 case 'array':
45
+ 45 case 'object':
46
+ 46 case 'serialize':
47
+ 47 return table.json(columnName)
48
+ 48
49
+ 49 case 'blob':
50
+ 50 case 'file':
51
+ 51 return table.binary(columnName)
52
+ 52
53
+ 53 case 'none':
54
+ 54 default:
55
+ 55 return table.string(columnName)
56
+ 56 }
57
+ 57 }
58
+ 58
59
+ 59 /**
60
+ 60 * Applies schema constraints to a Knex column
61
+ 61 * @param {object} column - The Knex column builder instance
62
+ 62 * @param {object} definition - The schema definition for the field
63
+ 63 */
64
+ 64 function applyConstraints (column, definition) {
65
+ 65 // Nullability
66
+ 66 if (definition.nullable === true) {
67
+ 67 column.nullable()
68
+ 68 } else if (definition.required === true || definition.nullable === false) {
69
+ 69 column.notNullable()
70
+ 70 }
71
+ 71
72
+ 72 // Default value
73
+ 73 if (definition.defaultTo !== undefined) {
74
+ 74 column.defaultTo(definition.defaultTo)
75
+ 75 }
76
+ 76
77
+ 77 // Database-specific constraints
78
+ 78 if (definition.unique === true) {
79
+ 79 column.unique()
80
+ 80 }
81
+ 81
82
+ 82 if (definition.primary === true) {
83
+ 83 column.primary()
84
+ 84 }
85
+ 85
86
+ 86 if (definition.index === true) {
87
+ 87 column.index()
88
+ 88 }
89
+ 89
90
+ 90 if (definition.references) {
91
+ 91 column.references(definition.references.column || 'id')
92
+ 92 .inTable(definition.references.table)
93
+ 93
94
+ 94 if (definition.references.onDelete) {
95
+ 95 column.onDelete(definition.references.onDelete)
96
+ 96 }
97
+ 97
98
+ 98 if (definition.references.onUpdate) {
99
+ 99 column.onUpdate(definition.references.onUpdate)
100
+ 100 }
101
+ 101 }
102
+ 102
103
+ 103 if (definition.comment) {
104
+ 104 column.comment(definition.comment)
105
+ 105 }
106
+ 106 }
107
+ 107
108
+ 108 /**
109
+ 109 * Creates a Knex table from a json-rest-schema definition
110
+ 110 * @param {object} knex - The Knex instance
111
+ 111 * @param {string} tableName - The name of the table to create
112
+ 112 * @param {object} schema - The json-rest-schema instance
113
+ 113 * @param {string} [idProperty='id'] - The name of the ID column
114
+ 114 * @param {object} [options={}] - Additional options
115
+ 115 * @param {boolean} [options.autoIncrement=true] - Whether to use auto-incrementing IDs
116
+ 116 * @param {boolean} [options.timestamps=false] - Whether to add created_at/updated_at columns
117
+ 117 * @returns {Promise} A promise that resolves when the table is created
118
+ 118 */
119
+ 119 export async function createKnexTable (knex, schemaInfo, tableSchemaInstance, options = {}) {
120
+ 120 const { autoIncrement = true, timestamps = false } = options
121
+ 121
122
+ 122 const tableName = schemaInfo.tableName
123
+ 123 const idProperty = schemaInfo.idProperty
124
+ 124 const tableSchemaStructure = tableSchemaInstance.structure
125
+ 125
126
+ 126 return knex.schema.createTable(tableName, (table) => {
127
+ 127 // Check if schema has the idProperty field with primary key
128
+ 128 const hasIdField = tableSchemaStructure[idProperty] && tableSchemaStructure[idProperty].primary === true
129
+ 129
130
+ 130 // Add auto-incrementing ID if no primary key is defined and autoIncrement is true
131
+ 131 if (!hasIdField && autoIncrement) {
132
+ 132 table.increments(idProperty).primary()
133
+ 133 }
134
+ 134
135
+ 135 // Process each field in the schema
136
+ 136 for (const [fieldName, definition] of Object.entries(tableSchemaStructure)) {
137
+ 137 // Skip if this is the ID field and we already handled it
138
+ 138 if (fieldName === idProperty && !hasIdField && autoIncrement) {
139
+ 139 continue
140
+ 140 }
141
+ 141
142
+ 142 // Create the column with the appropriate type
143
+ 143 const column = mapTypeToKnex(table, fieldName, definition)
144
+ 144
145
+ 145 // Apply constraints
146
+ 146 applyConstraints(column, definition)
147
+ 147 }
148
+ 148
149
+ 149 // Add timestamps if requested
150
+ 150 if (timestamps) {
151
+ 151 table.timestamps(true, true)
152
+ 152 }
153
+ 153 })
154
+ 154 }
155
+ 155
156
+ 156 export async function addKnexFields (knex, tableName, schema) {
157
+ 157 return knex.schema.alterTable(tableName, (table) => {
158
+ 158 for (const [fieldName, definition] of Object.entries(schema.structure)) {
159
+ 159 const column = mapTypeToKnex(table, fieldName, definition)
160
+ 160 applyConstraints(column, definition)
161
+ 161 }
162
+ 162 })
163
+ 163 }
164
+ 164
165
+ 165 // Helper function to alter multiple fields in an existing table
166
+ 166 export async function alterKnexFields (knex, tableName, fields, options = {}) {
167
+ 167 return knex.schema.alterTable(tableName, (table) => {
168
+ 168 for (const [fieldName, definition] of Object.entries(fields)) {
169
+ 169 // Create the column with alter flag
170
+ 170 const column = mapTypeToKnex(table, fieldName, definition)
171
+ 171
172
+ 172 // Mark this as an alteration
173
+ 173 column.alter()
174
+ 174
175
+ 175 // Apply constraints (these will be reapplied with the alter)
176
+ 176 applyConstraints(column, definition)
177
+ 177 }
178
+ 178 })
179
+ 179 }
180
+ 180
181
+ 181 /**
182
+ 182 * Generates a Knex migration string from a json-rest-schema definition
183
+ 183 * @param {string} tableName - The name of the table
184
+ 184 * @param {object} schema - The json-rest-schema instance
185
+ 185 * @param {object} [options={}] - Additional options
186
+ 186 * @returns {string} The migration code as a string
187
+ 187 */
188
+ 188 export function generateKnexMigration (tableName, schema, options = {}) {
189
+ 189 const { autoIncrement = true, timestamps = false } = options
190
+ 190
191
+ 191 let migration = `exports.up = function(knex) {
192
+ 192 return knex.schema.createTable('${tableName}', (table) => {\n`
193
+ 193
194
+ 194 // Check if schema has an 'id' field with primary key
195
+ 195 const hasIdField = schema.structure.id && schema.structure.id.primary === true
196
+ 196
197
+ 197 // Add auto-incrementing ID if needed
198
+ 198 if (!hasIdField && autoIncrement) {
199
+ 199 migration += ' table.increments(\'id\').primary();\n'
200
+ 200 }
201
+ 201
202
+ 202 // Process each field
203
+ 203 for (const [fieldName, definition] of Object.entries(schema.structure)) {
204
+ 204 if (fieldName === 'id' && !hasIdField && autoIncrement) {
205
+ 205 continue
206
+ 206 }
207
+ 207
208
+ 208 let line = ' '
209
+ 209
210
+ 210 // Map type
211
+ 211 switch (definition.type) {
212
+ 212 case 'string':
213
+ 213 line += definition.maxLength
214
+ 214 ? `table.string('${fieldName}', ${definition.maxLength})`
215
+ 215 : `table.string('${fieldName}')`
216
+ 216 break
217
+ 217 case 'number':
218
+ 218 if (definition.precision !== undefined && definition.scale !== undefined) {
219
+ 219 line += `table.decimal('${fieldName}', ${definition.precision}, ${definition.scale})`
220
+ 220 } else {
221
+ 221 line += `table.float('${fieldName}')`
222
+ 222 }
223
+ 223 break
224
+ 224 case 'id':
225
+ 225 line += `table.integer('${fieldName}')`
226
+ 226 if (definition.unsigned !== false) line += '.unsigned()'
227
+ 227 break
228
+ 228 case 'boolean':
229
+ 229 line += `table.boolean('${fieldName}')`
230
+ 230 break
231
+ 231 case 'date':
232
+ 232 line += `table.date('${fieldName}')`
233
+ 233 break
234
+ 234 case 'dateTime':
235
+ 235 line += `table.datetime('${fieldName}')`
236
+ 236 break
237
+ 237 case 'time':
238
+ 238 line += `table.time('${fieldName}')`
239
+ 239 break
240
+ 240 case 'timestamp':
241
+ 241 line += `table.integer('${fieldName}')`
242
+ 242 break
243
+ 243 case 'array':
244
+ 244 case 'object':
245
+ 245 case 'serialize':
246
+ 246 line += `table.json('${fieldName}')`
247
+ 247 break
248
+ 248 case 'blob':
249
+ 249 case 'file':
250
+ 250 line += `table.binary('${fieldName}')`
251
+ 251 break
252
+ 252 default:
253
+ 253 line += `table.string('${fieldName}')`
254
+ 254 }
255
+ 255
256
+ 256 // Add constraints
257
+ 257 if (definition.nullable === true) {
258
+ 258 line += '.nullable()'
259
+ 259 } else if (definition.required === true || definition.nullable === false) {
260
+ 260 line += '.notNullable()'
261
+ 261 }
262
+ 262
263
+ 263 if (definition.defaultTo !== undefined) {
264
+ 264 const defaultValue = typeof definition.defaultTo === 'string'
265
+ 265 ? `'${definition.defaultTo}'`
266
+ 266 : definition.defaultTo
267
+ 267 line += `.defaultTo(${defaultValue})`
268
+ 268 }
269
+ 269
270
+ 270 if (definition.unique === true) line += '.unique()'
271
+ 271 if (definition.primary === true) line += '.primary()'
272
+ 272 if (definition.index === true) line += '.index()'
273
+ 273
274
+ 274 if (definition.references) {
275
+ 275 const refCol = definition.references.column || 'id'
276
+ 276 line += `.references('${refCol}').inTable('${definition.references.table}')`
277
+ 277 if (definition.references.onDelete) {
278
+ 278 line += `.onDelete('${definition.references.onDelete}')`
279
+ 279 }
280
+ 280 if (definition.references.onUpdate) {
281
+ 281 line += `.onUpdate('${definition.references.onUpdate}')`
282
+ 282 }
283
+ 283 }
284
+ 284
285
+ 285 if (definition.comment) {
286
+ 286 line += `.comment('${definition.comment}')`
287
+ 287 }
288
+ 288
289
+ 289 migration += line + ';\n'
290
+ 290 }
291
+ 291
292
+ 292 if (timestamps) {
293
+ 293 migration += ' table.timestamps(true, true);\n'
294
+ 294 }
295
+ 295
296
+ 296 migration += ` });
297
+ 297 };
298
+ 298
299
+ 299 exports.down = function(knex) {
300
+ 300 return knex.schema.dropTable('${tableName}');
301
+ 301 };`
302
+ 302
303
+ 303 return migration
304
+ 304 }