json-rest-api 1.0.21 → 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.
- package/audit/00-charter.md +103 -0
- package/audit/01-scope-map.md +84 -0
- package/audit/02-coverage-matrix.csv +174 -0
- package/audit/03-invariants.md +123 -0
- package/audit/04-standards-rubric.md +56 -0
- package/audit/05-competitor-rubric.md +77 -0
- package/audit/06-findings.md +160 -0
- package/audit/07-risk-register.csv +10 -0
- package/audit/08-remediation-roadmap.md +177 -0
- package/audit/09-signoff-check.md +23 -0
- package/audit/10-final-synthesis.md +29 -0
- package/audit/evidence/code/all-files-json-rest-api.txt +168 -0
- package/audit/evidence/code/all-files-json-rest-schema.txt +12 -0
- package/audit/evidence/code/boundary-leak-checks.txt +72 -0
- package/audit/evidence/code/critical-pass1/json-rest-api__plugins__core__bulk-operations-plugin.js.txt +480 -0
- package/audit/evidence/code/critical-pass1/json-rest-api__plugins__core__connectors__express-plugin.js.txt +532 -0
- package/audit/evidence/code/critical-pass1/json-rest-api__plugins__core__connectors__lib__busboy-detector.js.txt +175 -0
- package/audit/evidence/code/critical-pass1/json-rest-api__plugins__core__connectors__lib__formidable-detector.js.txt +138 -0
- package/audit/evidence/code/critical-pass1/json-rest-api__plugins__core__connectors__lib__request-helpers.js.txt +222 -0
- package/audit/evidence/code/critical-pass1/json-rest-api__plugins__core__file-handling-plugin.js.txt +342 -0
- package/audit/evidence/code/critical-pass1/json-rest-api__plugins__core__lib__dbTablesOperations.js.txt +304 -0
- package/audit/evidence/code/critical-pass1/json-rest-api__plugins__core__lib__fractional-positioning.js.txt +188 -0
- package/audit/evidence/code/critical-pass1/json-rest-api__plugins__core__lib__querying-writing__compile-schemas.js.txt +336 -0
- package/audit/evidence/code/critical-pass1/json-rest-api__plugins__core__lib__querying-writing__connectors-query-parser.js.txt +165 -0
- package/audit/evidence/code/critical-pass1/json-rest-api__plugins__core__lib__querying-writing__database-capabilities.js.txt +265 -0
- package/audit/evidence/code/critical-pass1/json-rest-api__plugins__core__lib__querying-writing__database-value-normalizers.js.txt +369 -0
- package/audit/evidence/code/critical-pass1/json-rest-api__plugins__core__lib__querying-writing__default-data-helpers.js.txt +72 -0
- package/audit/evidence/code/critical-pass1/json-rest-api__plugins__core__lib__querying-writing__field-utils.js.txt +139 -0
- package/audit/evidence/code/critical-pass1/json-rest-api__plugins__core__lib__querying-writing__knex-constants.js.txt +45 -0
- package/audit/evidence/code/critical-pass1/json-rest-api__plugins__core__lib__querying-writing__knex-field-helpers.js.txt +396 -0
- package/audit/evidence/code/critical-pass1/json-rest-api__plugins__core__lib__querying-writing__knex-json-api-transformers.js.txt +162 -0
- package/audit/evidence/code/critical-pass1/json-rest-api__plugins__core__lib__querying-writing__payload-validators.js.txt +1314 -0
- package/audit/evidence/code/critical-pass1/json-rest-api__plugins__core__lib__querying-writing__schema-helpers.js.txt +488 -0
- package/audit/evidence/code/critical-pass1/json-rest-api__plugins__core__lib__querying-writing__scope-validations.js.txt +484 -0
- package/audit/evidence/code/critical-pass1/json-rest-api__plugins__core__lib__querying-writing__simplified-helpers.js.txt +857 -0
- package/audit/evidence/code/critical-pass1/json-rest-api__plugins__core__lib__querying__knex-cross-table-search.js.txt +565 -0
- package/audit/evidence/code/critical-pass1/json-rest-api__plugins__core__lib__querying__knex-json-api-transformers-querying.js.txt +485 -0
- package/audit/evidence/code/critical-pass1/json-rest-api__plugins__core__lib__querying__knex-pagination-helpers.js.txt +560 -0
- package/audit/evidence/code/critical-pass1/json-rest-api__plugins__core__lib__querying__knex-process-includes.js.txt +167 -0
- package/audit/evidence/code/critical-pass1/json-rest-api__plugins__core__lib__querying__knex-query-helpers-base.js.txt +109 -0
- package/audit/evidence/code/critical-pass1/json-rest-api__plugins__core__lib__querying__knex-query-helpers.js.txt +1064 -0
- package/audit/evidence/code/critical-pass1/json-rest-api__plugins__core__lib__querying__knex-relationship-includes.js.txt +1983 -0
- package/audit/evidence/code/critical-pass1/json-rest-api__plugins__core__lib__querying__knex-window-queries.js.txt +328 -0
- package/audit/evidence/code/critical-pass1/json-rest-api__plugins__core__lib__querying__url-helpers.js.txt +119 -0
- package/audit/evidence/code/critical-pass1/json-rest-api__plugins__core__lib__storage__storage-adapter.js.txt +160 -0
- package/audit/evidence/code/critical-pass1/json-rest-api__plugins__core__lib__writing__many-to-many-manipulations.js.txt +300 -0
- package/audit/evidence/code/critical-pass1/json-rest-api__plugins__core__lib__writing__relationship-processor.js.txt +245 -0
- package/audit/evidence/code/critical-pass1/json-rest-api__plugins__core__rest-api-access.js.txt +459 -0
- package/audit/evidence/code/critical-pass1/json-rest-api__plugins__core__rest-api-cors-plugin.js.txt +237 -0
- package/audit/evidence/code/critical-pass1/json-rest-api__plugins__core__rest-api-knex-plugin.js.txt +1135 -0
- package/audit/evidence/code/critical-pass1/json-rest-api__plugins__core__rest-api-multihome-plugin.js.txt +314 -0
- package/audit/evidence/code/critical-pass1/json-rest-api__plugins__core__rest-api-plugin-hooks__compile-resource-schemas.js.txt +7 -0
- package/audit/evidence/code/critical-pass1/json-rest-api__plugins__core__rest-api-plugin-hooks__register-relationship-routes.js.txt +71 -0
- package/audit/evidence/code/critical-pass1/json-rest-api__plugins__core__rest-api-plugin-hooks__register-scope-routes.js.txt +102 -0
- package/audit/evidence/code/critical-pass1/json-rest-api__plugins__core__rest-api-plugin-hooks__turn-scope-init-into-vars.js.txt +49 -0
- package/audit/evidence/code/critical-pass1/json-rest-api__plugins__core__rest-api-plugin-hooks__validate-include-configurations.js.txt +39 -0
- package/audit/evidence/code/critical-pass1/json-rest-api__plugins__core__rest-api-plugin-methods__add-route.js.txt +23 -0
- package/audit/evidence/code/critical-pass1/json-rest-api__plugins__core__rest-api-plugin-methods__check-permissions.js.txt +13 -0
- package/audit/evidence/code/critical-pass1/json-rest-api__plugins__core__rest-api-plugin-methods__common.js.txt +639 -0
- package/audit/evidence/code/critical-pass1/json-rest-api__plugins__core__rest-api-plugin-methods__delete-relationship.js.txt +117 -0
- package/audit/evidence/code/critical-pass1/json-rest-api__plugins__core__rest-api-plugin-methods__delete.js.txt +104 -0
- package/audit/evidence/code/critical-pass1/json-rest-api__plugins__core__rest-api-plugin-methods__enrich-attributes.js.txt +199 -0
- package/audit/evidence/code/critical-pass1/json-rest-api__plugins__core__rest-api-plugin-methods__get-related.js.txt +353 -0
- package/audit/evidence/code/critical-pass1/json-rest-api__plugins__core__rest-api-plugin-methods__get-relationship.js.txt +61 -0
- package/audit/evidence/code/critical-pass1/json-rest-api__plugins__core__rest-api-plugin-methods__get.js.txt +184 -0
- package/audit/evidence/code/critical-pass1/json-rest-api__plugins__core__rest-api-plugin-methods__patch-relationship.js.txt +55 -0
- package/audit/evidence/code/critical-pass1/json-rest-api__plugins__core__rest-api-plugin-methods__patch.js.txt +191 -0
- package/audit/evidence/code/critical-pass1/json-rest-api__plugins__core__rest-api-plugin-methods__post-relationship.js.txt +110 -0
- package/audit/evidence/code/critical-pass1/json-rest-api__plugins__core__rest-api-plugin-methods__post.js.txt +168 -0
- package/audit/evidence/code/critical-pass1/json-rest-api__plugins__core__rest-api-plugin-methods__put.js.txt +253 -0
- package/audit/evidence/code/critical-pass1/json-rest-api__plugins__core__rest-api-plugin-methods__query.js.txt +207 -0
- package/audit/evidence/code/critical-pass1/json-rest-api__plugins__core__rest-api-plugin-methods__release.js.txt +12 -0
- package/audit/evidence/code/critical-pass1/json-rest-api__plugins__core__rest-api-plugin.js.txt +165 -0
- package/audit/evidence/code/critical-pass1/json-rest-api__plugins__core__rest-api-positioning-plugin.js.txt +668 -0
- package/audit/evidence/code/critical-pass1/json-rest-api__plugins__core__socketio-plugin.js.txt +621 -0
- package/audit/evidence/code/critical-pass1/json-rest-api__plugins__storage__local-storage.js.txt +218 -0
- package/audit/evidence/code/critical-pass1/json-rest-api__plugins__storage__s3-storage.js.txt +124 -0
- package/audit/evidence/code/critical-pass1/json-rest-schema__src__CorePlugin.js.txt +269 -0
- package/audit/evidence/code/critical-pass1/json-rest-schema__src__Schema.js.txt +242 -0
- package/audit/evidence/code/critical-pass1/json-rest-schema__src__index.js.txt +77 -0
- package/audit/evidence/code/critical-pass1-risk-keywords.txt +238 -0
- package/audit/evidence/code/critical-pass1-summary.csv +67 -0
- package/audit/evidence/code/critical-pass2-summary.csv +67 -0
- package/audit/evidence/code/early-silent-zones-no-test-token.txt +57 -0
- package/audit/evidence/code/error-model-signals.txt +98 -0
- package/audit/evidence/code/git-files-json-rest-api.txt +171 -0
- package/audit/evidence/code/git-files-json-rest-schema.txt +27 -0
- package/audit/evidence/code/hook-flow-signals.txt +282 -0
- package/audit/evidence/code/idproperty-signals.txt +157 -0
- package/audit/evidence/code/import-cycles-non-anyapi.txt +3 -0
- package/audit/evidence/code/import-hotspots-non-anyapi.txt +96 -0
- package/audit/evidence/code/imports-json-rest-api.txt +354 -0
- package/audit/evidence/code/imports-json-rest-schema.txt +15 -0
- package/audit/evidence/code/in-scope-files-json-rest-api.txt +160 -0
- package/audit/evidence/code/in-scope-files-json-rest-schema.txt +13 -0
- package/audit/evidence/code/non-anyapi-core-files.txt +101 -0
- package/audit/evidence/code/permission-signals.txt +193 -0
- package/audit/evidence/code/query-include-signals.txt +687 -0
- package/audit/evidence/code/transaction-signals.txt +28 -0
- package/audit/evidence/code/validation-signals.txt +650 -0
- package/audit/evidence/code/write-path-signals.txt +61 -0
- package/audit/evidence/docs/docs-claims-signals.txt +1075 -0
- package/audit/evidence/docs/schema-docs-claims-signals.txt +179 -0
- package/audit/evidence/external/corpus-freeze.md +21 -0
- package/audit/evidence/external/npm-competitor-metadata.txt +70 -0
- package/audit/evidence/external/npm-download-signals.txt +23 -0
- package/audit/evidence/external/npm-jsonapi-server.txt +8 -0
- package/audit/evidence/external/npm-modern-jsonapi-metadata.txt +38 -0
- package/audit/evidence/external/readme-fortune-signals.txt +26 -0
- package/audit/evidence/external/readme-fortune.md +165 -0
- package/audit/evidence/external/readme-json-api-serializer-signals.txt +80 -0
- package/audit/evidence/external/readme-json-api-serializer.md +690 -0
- package/audit/evidence/external/readme-jsonapi-serde-server-signals.txt +0 -0
- package/audit/evidence/external/readme-jsonapi-serde-server.md +22 -0
- package/audit/evidence/external/readme-jsonapi-serializer-signals.txt +63 -0
- package/audit/evidence/external/readme-jsonapi-serializer.md +315 -0
- package/audit/evidence/external/readme-jsonapi-server-signals.txt +19 -0
- package/audit/evidence/external/readme-jsonapi-server.md +104 -0
- package/audit/evidence/external/readme-nestjs-json-api-signals.txt +4 -0
- package/audit/evidence/external/readme-nestjs-json-api.md +94 -0
- package/audit/evidence/external/readme-yaser-json-api-nestjs-signals.txt +47 -0
- package/audit/evidence/external/readme-yaser-json-api-nestjs.md +447 -0
- package/audit/evidence/external/standard-jsonapi-snippets.txt +84 -0
- package/audit/evidence/external/standard-rfc-snippets.txt +168 -0
- package/audit/evidence/external/standards-sources.txt +6 -0
- package/audit/evidence/runtime/baseline-commits.txt +2 -0
- package/audit/evidence/runtime/db-adapter-assumptions.txt +3 -0
- package/audit/evidence/runtime/environment.txt +4 -0
- package/audit/evidence/runtime/invariant-runtime-mapping.md +38 -0
- package/audit/evidence/runtime/probe-patch-relationship-error.exitcode +1 -0
- package/audit/evidence/runtime/probe-patch-relationship-error.log +76 -0
- package/audit/evidence/runtime/probe-patch-relationship-error.mjs +67 -0
- package/audit/evidence/runtime/probe-patch-relationship-referenceerror.exitcode +1 -0
- package/audit/evidence/runtime/probe-patch-relationship-referenceerror.log +62 -0
- package/audit/evidence/runtime/probe-patch-relationship-referenceerror.mjs +47 -0
- package/audit/evidence/runtime/test-exitcodes.txt +4 -0
- package/audit/evidence/runtime/test-summary.md +107 -0
- package/audit/evidence/runtime/trace-anchors.txt +61 -0
- package/audit/evidence/tests/access-multitenancy-test-signals.txt +125 -0
- package/audit/evidence/tests/failing-tests-summary.txt +25 -0
- package/audit/evidence/tests/npm-test-json-rest-api.exitcode +1 -0
- package/audit/evidence/tests/npm-test-json-rest-api.log +5885 -0
- package/audit/evidence/tests/npm-test-json-rest-schema.exitcode +1 -0
- package/audit/evidence/tests/npm-test-json-rest-schema.log +356 -0
- package/audit/evidence/tests/query-include-test-signals.txt +948 -0
- package/audit/evidence/tests/relationship-test-signals.txt +223 -0
- package/audit/evidence/tests/skipped-tests.txt +10 -0
- package/audit/evidence/tests/test-suite-list-non-anyapi.txt +30 -0
- package/audit/evidence/tests/test-suite-list.txt +32 -0
- package/audit/subsystems/docs-drift.md +49 -0
- package/audit/subsystems/pass2-cross-review.md +131 -0
- package/audit/subsystems/plugins-access.md +51 -0
- package/audit/subsystems/plugins-bulk.md +50 -0
- package/audit/subsystems/plugins-cors.md +48 -0
- package/audit/subsystems/plugins-file.md +53 -0
- package/audit/subsystems/plugins-multihome.md +50 -0
- package/audit/subsystems/plugins-positioning.md +54 -0
- package/audit/subsystems/plugins-socketio.md +50 -0
- package/audit/subsystems/query-engine.md +61 -0
- package/audit/subsystems/relationships.md +55 -0
- package/audit/subsystems/rest-orchestration.md +66 -0
- package/audit/subsystems/schema-core.md +64 -0
- package/audit/subsystems/storage-knex.md +57 -0
- package/audit/subsystems/tests.md +48 -0
- package/audit/subsystems/transport-express.md +55 -0
- package/audit/traces/trace-read-get.md +22 -0
- package/audit/traces/trace-read-query.md +25 -0
- package/audit/traces/trace-relationship-endpoints.md +31 -0
- package/audit/traces/trace-write-delete.md +17 -0
- package/audit/traces/trace-write-patch.md +26 -0
- package/audit/traces/trace-write-post.md +22 -0
- package/audit/traces/trace-write-put.md +23 -0
- package/package.json +3 -3
package/audit/evidence/code/critical-pass1/json-rest-api__plugins__storage__local-storage.js.txt
ADDED
|
@@ -0,0 +1,218 @@
|
|
|
1
|
+
1 /**
|
|
2
|
+
2 * Local File Storage Adapter
|
|
3
|
+
3 *
|
|
4
|
+
4 * Production-ready local storage with secure filename handling
|
|
5
|
+
5 */
|
|
6
|
+
6
|
|
7
|
+
7 import { promises as fs } from 'fs'
|
|
8
|
+
8 import path from 'path'
|
|
9
|
+
9 import crypto from 'crypto'
|
|
10
|
+
10
|
|
11
|
+
11 export class LocalStorage {
|
|
12
|
+
12 constructor (options = {}) {
|
|
13
|
+
13 this.directory = path.resolve(options.directory || './uploads')
|
|
14
|
+
14 this.fileBaseUrl = options.fileBaseUrl || '/uploads'
|
|
15
|
+
15 this.nameStrategy = options.nameStrategy || 'hash' // 'hash', 'timestamp', 'original', 'custom'
|
|
16
|
+
16 this.nameGenerator = options.nameGenerator // Custom function
|
|
17
|
+
17 this.preserveExtension = options.preserveExtension !== false
|
|
18
|
+
18 this.allowedExtensions = options.allowedExtensions // Whitelist
|
|
19
|
+
19 this.maxFilenameLength = options.maxFilenameLength || 255
|
|
20
|
+
20 }
|
|
21
|
+
21
|
|
22
|
+
22 /**
|
|
23
|
+
23 * Generate a safe filename
|
|
24
|
+
24 */
|
|
25
|
+
25 async generateFilename (file) {
|
|
26
|
+
26 let basename, extension
|
|
27
|
+
27
|
|
28
|
+
28 // Extract and validate extension
|
|
29
|
+
29 const originalExt = path.extname(file.filename).toLowerCase()
|
|
30
|
+
30 if (this.preserveExtension && originalExt) {
|
|
31
|
+
31 // Validate extension against whitelist if provided
|
|
32
|
+
32 if (this.allowedExtensions && !this.allowedExtensions.includes(originalExt)) {
|
|
33
|
+
33 throw new Error(`File extension '${originalExt}' is not allowed`)
|
|
34
|
+
34 }
|
|
35
|
+
35 extension = originalExt
|
|
36
|
+
36 } else {
|
|
37
|
+
37 // Determine extension from MIME type for safety
|
|
38
|
+
38 extension = this.getExtensionFromMimeType(file.mimetype)
|
|
39
|
+
39 }
|
|
40
|
+
40
|
|
41
|
+
41 // Generate basename based on strategy
|
|
42
|
+
42 switch (this.nameStrategy) {
|
|
43
|
+
43 case 'hash':
|
|
44
|
+
44 // Cryptographically secure random name
|
|
45
|
+
45 basename = crypto.randomBytes(16).toString('hex')
|
|
46
|
+
46 break
|
|
47
|
+
47
|
|
48
|
+
48 case 'timestamp':
|
|
49
|
+
49 // Timestamp + random suffix to prevent collisions
|
|
50
|
+
50 basename = `${Date.now()}_${crypto.randomBytes(4).toString('hex')}`
|
|
51
|
+
51 break
|
|
52
|
+
52
|
|
53
|
+
53 case 'original':
|
|
54
|
+
54 // Sanitized original filename
|
|
55
|
+
55 basename = this.sanitizeFilename(
|
|
56
|
+
56 path.basename(file.filename, originalExt)
|
|
57
|
+
57 )
|
|
58
|
+
58 break
|
|
59
|
+
59
|
|
60
|
+
60 case 'custom':
|
|
61
|
+
61 if (!this.nameGenerator) {
|
|
62
|
+
62 throw new Error('nameGenerator function required for custom strategy')
|
|
63
|
+
63 }
|
|
64
|
+
64 basename = await this.nameGenerator(file)
|
|
65
|
+
65 break
|
|
66
|
+
66
|
|
67
|
+
67 default:
|
|
68
|
+
68 throw new Error(`Unknown name strategy: ${this.nameStrategy}`)
|
|
69
|
+
69 }
|
|
70
|
+
70
|
|
71
|
+
71 // Combine basename and extension
|
|
72
|
+
72 let filename = extension ? `${basename}${extension}` : basename
|
|
73
|
+
73
|
|
74
|
+
74 // Ensure filename doesn't exceed max length
|
|
75
|
+
75 if (filename.length > this.maxFilenameLength) {
|
|
76
|
+
76 // Truncate basename to fit
|
|
77
|
+
77 const maxBasename = this.maxFilenameLength - (extension?.length || 0)
|
|
78
|
+
78 basename = basename.substring(0, maxBasename)
|
|
79
|
+
79 filename = extension ? `${basename}${extension}` : basename
|
|
80
|
+
80 }
|
|
81
|
+
81
|
|
82
|
+
82 // Handle duplicates
|
|
83
|
+
83 filename = await this.ensureUnique(filename)
|
|
84
|
+
84
|
|
85
|
+
85 return filename
|
|
86
|
+
86 }
|
|
87
|
+
87
|
|
88
|
+
88 /**
|
|
89
|
+
89 * Sanitize filename for safe storage
|
|
90
|
+
90 */
|
|
91
|
+
91 sanitizeFilename (filename) {
|
|
92
|
+
92 return filename
|
|
93
|
+
93 // Remove path traversal attempts
|
|
94
|
+
94 .replace(/\.\./g, '')
|
|
95
|
+
95 .replace(/[\/\\]/g, '')
|
|
96
|
+
96 // Remove control characters and special chars
|
|
97
|
+
97 .replace(/[^\w\s.-]/g, '_')
|
|
98
|
+
98 // Remove leading/trailing dots and spaces
|
|
99
|
+
99 .replace(/^[\s.]+|[\s.]+$/g, '')
|
|
100
|
+
100 // Collapse multiple underscores
|
|
101
|
+
101 .replace(/_+/g, '_') ||
|
|
102
|
+
102 // Default if empty
|
|
103
|
+
103 'unnamed'
|
|
104
|
+
104 }
|
|
105
|
+
105
|
|
106
|
+
106 /**
|
|
107
|
+
107 * Ensure filename is unique
|
|
108
|
+
108 */
|
|
109
|
+
109 async ensureUnique (filename) {
|
|
110
|
+
110 const filepath = path.join(this.directory, filename)
|
|
111
|
+
111
|
|
112
|
+
112 try {
|
|
113
|
+
113 await fs.access(filepath)
|
|
114
|
+
114 // File exists, generate unique name
|
|
115
|
+
115 const ext = path.extname(filename)
|
|
116
|
+
116 const base = path.basename(filename, ext)
|
|
117
|
+
117 let counter = 1
|
|
118
|
+
118 let newFilename
|
|
119
|
+
119
|
|
120
|
+
120 do {
|
|
121
|
+
121 newFilename = `${base}_${counter}${ext}`
|
|
122
|
+
122 counter++
|
|
123
|
+
123 } while (await this.fileExists(path.join(this.directory, newFilename)))
|
|
124
|
+
124
|
|
125
|
+
125 return newFilename
|
|
126
|
+
126 } catch (error) {
|
|
127
|
+
127 // File doesn't exist, name is unique
|
|
128
|
+
128 return filename
|
|
129
|
+
129 }
|
|
130
|
+
130 }
|
|
131
|
+
131
|
|
132
|
+
132 /**
|
|
133
|
+
133 * Check if file exists
|
|
134
|
+
134 */
|
|
135
|
+
135 async fileExists (filepath) {
|
|
136
|
+
136 try {
|
|
137
|
+
137 await fs.access(filepath)
|
|
138
|
+
138 return true
|
|
139
|
+
139 } catch {
|
|
140
|
+
140 return false
|
|
141
|
+
141 }
|
|
142
|
+
142 }
|
|
143
|
+
143
|
|
144
|
+
144 /**
|
|
145
|
+
145 * Get safe extension from MIME type
|
|
146
|
+
146 */
|
|
147
|
+
147 getExtensionFromMimeType (mimetype) {
|
|
148
|
+
148 // Safe mappings only
|
|
149
|
+
149 const mimeToExt = {
|
|
150
|
+
150 'image/jpeg': '.jpg',
|
|
151
|
+
151 'image/png': '.png',
|
|
152
|
+
152 'image/gif': '.gif',
|
|
153
|
+
153 'image/webp': '.webp',
|
|
154
|
+
154 'application/pdf': '.pdf',
|
|
155
|
+
155 'text/plain': '.txt',
|
|
156
|
+
156 'application/json': '.json',
|
|
157
|
+
157 'video/mp4': '.mp4',
|
|
158
|
+
158 'audio/mpeg': '.mp3',
|
|
159
|
+
159 'application/zip': '.zip'
|
|
160
|
+
160 }
|
|
161
|
+
161
|
|
162
|
+
162 return mimeToExt[mimetype] || '.bin'
|
|
163
|
+
163 }
|
|
164
|
+
164
|
|
165
|
+
165 /**
|
|
166
|
+
166 * Upload file with secure filename
|
|
167
|
+
167 */
|
|
168
|
+
168 async upload (file) {
|
|
169
|
+
169 // Ensure directory exists
|
|
170
|
+
170 await fs.mkdir(this.directory, { recursive: true })
|
|
171
|
+
171
|
|
172
|
+
172 // Generate secure filename
|
|
173
|
+
173 const filename = await this.generateFilename(file)
|
|
174
|
+
174 const filepath = path.join(this.directory, filename)
|
|
175
|
+
175
|
|
176
|
+
176 // Ensure we're not writing outside our directory (defense in depth)
|
|
177
|
+
177 const resolvedPath = path.resolve(filepath)
|
|
178
|
+
178 const resolvedDir = path.resolve(this.directory)
|
|
179
|
+
179 if (!resolvedPath.startsWith(resolvedDir)) {
|
|
180
|
+
180 throw new Error('Invalid file path')
|
|
181
|
+
181 }
|
|
182
|
+
182
|
|
183
|
+
183 // Write file
|
|
184
|
+
184 if (file.data) {
|
|
185
|
+
185 await fs.writeFile(filepath, file.data)
|
|
186
|
+
186 } else if (file.filepath) {
|
|
187
|
+
187 await fs.rename(file.filepath, filepath)
|
|
188
|
+
188 } else {
|
|
189
|
+
189 throw new Error('File has no data or filepath')
|
|
190
|
+
190 }
|
|
191
|
+
191
|
|
192
|
+
192 // Return public URL
|
|
193
|
+
193 return `${this.fileBaseUrl}/${filename}`
|
|
194
|
+
194 }
|
|
195
|
+
195
|
|
196
|
+
196 /**
|
|
197
|
+
197 * Delete a file
|
|
198
|
+
198 */
|
|
199
|
+
199 async delete (url) {
|
|
200
|
+
200 const filename = path.basename(url)
|
|
201
|
+
201 const filepath = path.join(this.directory, filename)
|
|
202
|
+
202
|
|
203
|
+
203 // Security check
|
|
204
|
+
204 const resolvedPath = path.resolve(filepath)
|
|
205
|
+
205 const resolvedDir = path.resolve(this.directory)
|
|
206
|
+
206 if (!resolvedPath.startsWith(resolvedDir)) {
|
|
207
|
+
207 throw new Error('Invalid file path')
|
|
208
|
+
208 }
|
|
209
|
+
209
|
|
210
|
+
210 try {
|
|
211
|
+
211 await fs.unlink(filepath)
|
|
212
|
+
212 } catch (error) {
|
|
213
|
+
213 if (error.code !== 'ENOENT') {
|
|
214
|
+
214 throw error
|
|
215
|
+
215 }
|
|
216
|
+
216 }
|
|
217
|
+
217 }
|
|
218
|
+
218 }
|
|
@@ -0,0 +1,124 @@
|
|
|
1
|
+
1 /**
|
|
2
|
+
2 * S3 Storage Adapter
|
|
3
|
+
3 *
|
|
4
|
+
4 * Stores uploaded files in Amazon S3 or S3-compatible storage.
|
|
5
|
+
5 *
|
|
6
|
+
6 * Usage:
|
|
7
|
+
7 * ```javascript
|
|
8
|
+
8 * import { S3Storage } from 'jsonrestapi/lib/storage/s3-storage.js';
|
|
9
|
+
9 *
|
|
10
|
+
10 * const storage = new S3Storage({
|
|
11
|
+
11 * bucket: 'my-uploads',
|
|
12
|
+
12 * region: 'us-east-1',
|
|
13
|
+
13 * accessKeyId: process.env.AWS_ACCESS_KEY_ID,
|
|
14
|
+
14 * secretAccessKey: process.env.AWS_SECRET_ACCESS_KEY,
|
|
15
|
+
15 * prefix: 'uploads/' // optional path prefix
|
|
16
|
+
16 * });
|
|
17
|
+
17 *
|
|
18
|
+
18 * const schema = {
|
|
19
|
+
19 * image: { type: 'file', storage }
|
|
20
|
+
20 * };
|
|
21
|
+
21 * ```
|
|
22
|
+
22 */
|
|
23
|
+
23
|
|
24
|
+
24 import crypto from 'crypto'
|
|
25
|
+
25 import path from 'path'
|
|
26
|
+
26
|
|
27
|
+
27 export class S3Storage {
|
|
28
|
+
28 constructor (options = {}) {
|
|
29
|
+
29 this.bucket = options.bucket
|
|
30
|
+
30 this.region = options.region || 'us-east-1'
|
|
31
|
+
31 this.prefix = options.prefix || ''
|
|
32
|
+
32 this.acl = options.acl || 'public-read'
|
|
33
|
+
33
|
|
34
|
+
34 if (!this.bucket) {
|
|
35
|
+
35 throw new Error('S3Storage requires a bucket name')
|
|
36
|
+
36 }
|
|
37
|
+
37
|
|
38
|
+
38 // Note: In a real implementation, you would initialize the AWS SDK here
|
|
39
|
+
39 // For this example, we'll just mock the behavior
|
|
40
|
+
40 this.mockMode = options.mockMode !== false // Default to mock mode
|
|
41
|
+
41
|
|
42
|
+
42 if (!this.mockMode) {
|
|
43
|
+
43 // Real S3 initialization would go here
|
|
44
|
+
44 // const { S3Client } = require('@aws-sdk/client-s3');
|
|
45
|
+
45 // this.s3 = new S3Client({ region: this.region });
|
|
46
|
+
46 }
|
|
47
|
+
47 }
|
|
48
|
+
48
|
|
49
|
+
49 /**
|
|
50
|
+
50 * Upload a file to S3
|
|
51
|
+
51 * @param {Object} file - File object from detector
|
|
52
|
+
52 * @returns {Promise<string>} URL of the uploaded file
|
|
53
|
+
53 */
|
|
54
|
+
54 async upload (file) {
|
|
55
|
+
55 // Generate S3 key
|
|
56
|
+
56 const ext = path.extname(file.filename)
|
|
57
|
+
57 const hash = crypto.randomBytes(16).toString('hex')
|
|
58
|
+
58 const key = `${this.prefix}${hash}${ext}`
|
|
59
|
+
59
|
|
60
|
+
60 if (this.mockMode) {
|
|
61
|
+
61 // Mock mode - just return a fake URL
|
|
62
|
+
62 const url = `https://${this.bucket}.s3.${this.region}.amazonaws.com/${key}`
|
|
63
|
+
63
|
|
64
|
+
64 // Simulate upload delay
|
|
65
|
+
65 await new Promise(resolve => setTimeout(resolve, 100))
|
|
66
|
+
66
|
|
67
|
+
67 return url
|
|
68
|
+
68 }
|
|
69
|
+
69
|
|
70
|
+
70 // Real S3 upload would go here
|
|
71
|
+
71 /*
|
|
72
|
+
72 const { PutObjectCommand } = require('@aws-sdk/client-s3');
|
|
73
|
+
73
|
|
74
|
+
74 const command = new PutObjectCommand({
|
|
75
|
+
75 Bucket: this.bucket,
|
|
76
|
+
76 Key: key,
|
|
77
|
+
77 Body: file.data,
|
|
78
|
+
78 ContentType: file.mimetype,
|
|
79
|
+
79 ACL: this.acl,
|
|
80
|
+
80 Metadata: {
|
|
81
|
+
81 originalName: file.filename
|
|
82
|
+
82 }
|
|
83
|
+
83 });
|
|
84
|
+
84
|
|
85
|
+
85 await this.s3.send(command);
|
|
86
|
+
86
|
|
87
|
+
87 // Return public URL
|
|
88
|
+
88 if (this.acl === 'public-read') {
|
|
89
|
+
89 return `https://${this.bucket}.s3.${this.region}.amazonaws.com/${key}`;
|
|
90
|
+
90 } else {
|
|
91
|
+
91 // For private files, you might return a signed URL
|
|
92
|
+
92 return await this.getSignedUrl(key);
|
|
93
|
+
93 }
|
|
94
|
+
94 */
|
|
95
|
+
95 }
|
|
96
|
+
96
|
|
97
|
+
97 /**
|
|
98
|
+
98 * Delete a file from S3
|
|
99
|
+
99 * @param {string} url - URL returned by upload()
|
|
100
|
+
100 * @returns {Promise<void>}
|
|
101
|
+
101 */
|
|
102
|
+
102 async delete (url) {
|
|
103
|
+
103 // Extract key from URL
|
|
104
|
+
104 const urlObj = new URL(url)
|
|
105
|
+
105 const key = urlObj.pathname.substring(1) // Remove leading /
|
|
106
|
+
106
|
|
107
|
+
107 if (this.mockMode) {
|
|
108
|
+
108 // Mock mode - just simulate deletion
|
|
109
|
+
109 await new Promise(resolve => setTimeout(resolve, 50))
|
|
110
|
+
110 }
|
|
111
|
+
111
|
|
112
|
+
112 // Real S3 delete would go here
|
|
113
|
+
113 /*
|
|
114
|
+
114 const { DeleteObjectCommand } = require('@aws-sdk/client-s3');
|
|
115
|
+
115
|
|
116
|
+
116 const command = new DeleteObjectCommand({
|
|
117
|
+
117 Bucket: this.bucket,
|
|
118
|
+
118 Key: key
|
|
119
|
+
119 });
|
|
120
|
+
120
|
|
121
|
+
121 await this.s3.send(command);
|
|
122
|
+
122 */
|
|
123
|
+
123 }
|
|
124
|
+
124 }
|
|
@@ -0,0 +1,269 @@
|
|
|
1
|
+
1 /**
|
|
2
|
+
2 * @file Contains all the built-in type and validator handlers for the schema library.
|
|
3
|
+
3 */
|
|
4
|
+
4
|
|
5
|
+
5 import * as flatted from 'flatted'
|
|
6
|
+
6
|
|
7
|
+
7 /**
|
|
8
|
+
8 * @typedef {import('./Schema.js').ValidationContext} ValidationContext
|
|
9
|
+
9 */
|
|
10
|
+
10
|
|
11
|
+
11 /**
|
|
12
|
+
12 * The CorePlugin provides the default set of types and validators.
|
|
13
|
+
13 */
|
|
14
|
+
14 const CorePlugin = {
|
|
15
|
+
15 /**
|
|
16
|
+
16 * Installs the core types and validators.
|
|
17
|
+
17 * @param {{addType: Function, addValidator: Function}} api - An object containing registration functions.
|
|
18
|
+
18 */
|
|
19
|
+
19 install (api) {
|
|
20
|
+
20 const { addType, addValidator } = api // Destructure the API functions
|
|
21
|
+
21
|
|
22
|
+
22 // --- Type Handlers ---
|
|
23
|
+
23
|
|
24
|
+
24 addType('none', context => context.value)
|
|
25
|
+
25
|
|
26
|
+
26 addType('file', context => {
|
|
27
|
+
27 const val = context.value
|
|
28
|
+
28 if (val === undefined || val === null) context.throwTypeError()
|
|
29
|
+
29
|
|
30
|
+
30 // Only attempt to convert primitives. Fail on complex objects/arrays.
|
|
31
|
+
31 // A file is expected to be just a file handle
|
|
32
|
+
32 const valType = typeof val
|
|
33
|
+
33 if (valType === 'string' || valType === 'number' || valType === 'boolean') {
|
|
34
|
+
34 const s = val.toString()
|
|
35
|
+
35 return context.definition.noTrim ? s : s.trim()
|
|
36
|
+
36 }
|
|
37
|
+
37
|
|
38
|
+
38 // If it's not a primitive that can be safely converted, it's a type error.
|
|
39
|
+
39 context.throwTypeError()
|
|
40
|
+
40 })
|
|
41
|
+
41
|
|
42
|
+
42 addType('string', context => {
|
|
43
|
+
43 const val = context.value
|
|
44
|
+
44 if (val === undefined || val === null) return ''
|
|
45
|
+
45
|
|
46
|
+
46 // Only attempt to convert primitives. Fail on complex objects/arrays.
|
|
47
|
+
47 const valType = typeof val
|
|
48
|
+
48 if (valType === 'string' || valType === 'number' || valType === 'boolean') {
|
|
49
|
+
49 const s = val.toString()
|
|
50
|
+
50 return context.definition.noTrim ? s : s.trim()
|
|
51
|
+
51 }
|
|
52
|
+
52
|
|
53
|
+
53 // If it's not a primitive that can be safely converted, it's a type error.
|
|
54
|
+
54 context.throwTypeError()
|
|
55
|
+
55 })
|
|
56
|
+
56
|
|
57
|
+
57 addType('blob', context => context.value)
|
|
58
|
+
58 addType('number', context => {
|
|
59
|
+
59 if (context.value === undefined || context.value === null || context.value === '') return 0
|
|
60
|
+
60 const r = Number(context.value)
|
|
61
|
+
61 if (isNaN(r)) context.throwTypeError()
|
|
62
|
+
62 return r
|
|
63
|
+
63 })
|
|
64
|
+
64 addType('timestamp', context => {
|
|
65
|
+
65 const r = Number(context.value)
|
|
66
|
+
66 if (isNaN(r)) context.throwTypeError()
|
|
67
|
+
67 if (!r && context.computedOptions.nullable) return null
|
|
68
|
+
68 return r
|
|
69
|
+
69 })
|
|
70
|
+
70 addType('dateTime', context => {
|
|
71
|
+
71 if (!context.value || context.value === '') return null
|
|
72
|
+
72
|
|
73
|
+
73 // If already a Date object, return it
|
|
74
|
+
74 if (context.value instanceof Date) {
|
|
75
|
+
75 return isNaN(context.value.getTime()) ? null : context.value
|
|
76
|
+
76 }
|
|
77
|
+
77
|
|
78
|
+
78 // Handle string values
|
|
79
|
+
79 if (typeof context.value === 'string') {
|
|
80
|
+
80 // Detect MySQL datetime format: 'YYYY-MM-DD HH:MM:SS'
|
|
81
|
+
81 const isMySQLFormat = /^\d{4}-\d{2}-\d{2} \d{2}:\d{2}:\d{2}/.test(context.value) &&
|
|
82
|
+
82 !context.value.includes('T') &&
|
|
83
|
+
83 !context.value.includes('Z')
|
|
84
|
+
84
|
|
85
|
+
85 if (isMySQLFormat) {
|
|
86
|
+
86 // Convert to ISO format and force UTC interpretation
|
|
87
|
+
87 const d = new Date(context.value.replace(' ', 'T') + 'Z')
|
|
88
|
+
88 if (isNaN(d.getTime())) {
|
|
89
|
+
89 context.throwTypeError()
|
|
90
|
+
90 }
|
|
91
|
+
91 return d
|
|
92
|
+
92 }
|
|
93
|
+
93 }
|
|
94
|
+
94
|
|
95
|
+
95 // Try to parse the value normally
|
|
96
|
+
96 const d = new Date(context.value)
|
|
97
|
+
97 if (isNaN(d.getTime())) {
|
|
98
|
+
98 return null
|
|
99
|
+
99 }
|
|
100
|
+
100
|
|
101
|
+
101 // Return the Date object directly - let Knex handle the database formatting
|
|
102
|
+
102 return d
|
|
103
|
+
103 })
|
|
104
|
+
104 addType('date', context => {
|
|
105
|
+
105 if (!context.value || context.value === '') return null
|
|
106
|
+
106
|
|
107
|
+
107 // Parse the input value to a Date object
|
|
108
|
+
108 let d
|
|
109
|
+
109 if (context.value instanceof Date) {
|
|
110
|
+
110 d = context.value
|
|
111
|
+
111 } else {
|
|
112
|
+
112 let dateStr = String(context.value)
|
|
113
|
+
113
|
|
114
|
+
114 // If it's just a date (YYYY-MM-DD), add time at UTC midnight
|
|
115
|
+
115 if (/^\d{4}-\d{2}-\d{2}$/.test(dateStr)) {
|
|
116
|
+
116 dateStr += 'T00:00:00Z'
|
|
117
|
+
117 }
|
|
118
|
+
118
|
|
119
|
+
119 d = new Date(dateStr)
|
|
120
|
+
120 }
|
|
121
|
+
121
|
|
122
|
+
122 if (isNaN(d.getTime())) {
|
|
123
|
+
123 context.throwTypeError()
|
|
124
|
+
124 }
|
|
125
|
+
125
|
|
126
|
+
126 // Normalize to midnight UTC
|
|
127
|
+
127 const normalized = new Date(Date.UTC(d.getUTCFullYear(), d.getUTCMonth(), d.getUTCDate()))
|
|
128
|
+
128 return normalized
|
|
129
|
+
129 })
|
|
130
|
+
130 addType('time', context => {
|
|
131
|
+
131 if (!context.value || context.value === '') return null
|
|
132
|
+
132
|
|
133
|
+
133 // Try to parse as time string (HH:MM:SS or HH:MM)
|
|
134
|
+
134 if (typeof context.value === 'string') {
|
|
135
|
+
135 // Match HH:MM:SS or HH:MM format
|
|
136
|
+
136 const timeMatch = context.value.match(/^(\d{1,2}):(\d{2})(?::(\d{2}))?$/)
|
|
137
|
+
137 if (timeMatch) {
|
|
138
|
+
138 const hours = parseInt(timeMatch[1], 10)
|
|
139
|
+
139 const minutes = parseInt(timeMatch[2], 10)
|
|
140
|
+
140 const seconds = timeMatch[3] ? parseInt(timeMatch[3], 10) : 0
|
|
141
|
+
141
|
|
142
|
+
142 // Validate ranges
|
|
143
|
+
143 if (hours >= 0 && hours <= 23 && minutes >= 0 && minutes <= 59 && seconds >= 0 && seconds <= 59) {
|
|
144
|
+
144 // Return normalized HH:MM:SS format as string
|
|
145
|
+
145 // Note: We return string because most databases don't have a true time-only type
|
|
146
|
+
146 return `${hours.toString().padStart(2, '0')}:${minutes.toString().padStart(2, '0')}:${seconds.toString().padStart(2, '0')}`
|
|
147
|
+
147 }
|
|
148
|
+
148 }
|
|
149
|
+
149 }
|
|
150
|
+
150
|
|
151
|
+
151 // Try to extract time from a Date object or datetime string
|
|
152
|
+
152 try {
|
|
153
|
+
153 const d = new Date(context.value)
|
|
154
|
+
154 if (!isNaN(d.getTime())) {
|
|
155
|
+
155 // Extract time portion in HH:MM:SS format
|
|
156
|
+
156 return d.toISOString().slice(11, 19)
|
|
157
|
+
157 }
|
|
158
|
+
158 } catch (e) {
|
|
159
|
+
159 // Invalid date
|
|
160
|
+
160 }
|
|
161
|
+
161
|
|
162
|
+
162 context.throwTypeError()
|
|
163
|
+
163 })
|
|
164
|
+
164 addType('array', context => {
|
|
165
|
+
165 if (context.definition.type === 'array' && !Array.isArray(context.value)) {
|
|
166
|
+
166 return [context.value]
|
|
167
|
+
167 }
|
|
168
|
+
168 return context.value
|
|
169
|
+
169 })
|
|
170
|
+
170 addType('object', context => context.value)
|
|
171
|
+
171 addType('serialize', context => {
|
|
172
|
+
172 try {
|
|
173
|
+
173 // First try regular JSON.stringify for non-circular objects
|
|
174
|
+
174 return JSON.stringify(context.value)
|
|
175
|
+
175 } catch (e) {
|
|
176
|
+
176 // If that fails (likely circular reference), use flatted
|
|
177
|
+
177 try {
|
|
178
|
+
178 return flatted.stringify(context.value)
|
|
179
|
+
179 } catch (e2) {
|
|
180
|
+
180 context.throwTypeError()
|
|
181
|
+
181 }
|
|
182
|
+
182 }
|
|
183
|
+
183 })
|
|
184
|
+
184 addType('boolean', context => {
|
|
185
|
+
185 if (typeof context.value === 'string') {
|
|
186
|
+
186 const falseVal = context.definition.stringFalseWhen || 'false'
|
|
187
|
+
187 const trueVal = context.definition.stringTrueWhen || 'true'
|
|
188
|
+
188 const lowerValue = context.value.toLowerCase()
|
|
189
|
+
189 if (lowerValue === falseVal) return false
|
|
190
|
+
190 if ([trueVal, 'on'].includes(lowerValue)) return true
|
|
191
|
+
191 return false
|
|
192
|
+
192 }
|
|
193
|
+
193 return !!context.value
|
|
194
|
+
194 })
|
|
195
|
+
195 addType('id', context => {
|
|
196
|
+
196 const n = parseInt(context.value, 10)
|
|
197
|
+
197 if (isNaN(n)) context.throwTypeError()
|
|
198
|
+
198 return n
|
|
199
|
+
199 })
|
|
200
|
+
200
|
|
201
|
+
201 // --- Validator Handlers ---
|
|
202
|
+
202 addValidator('minLength', context => {
|
|
203
|
+
203 if (context.value === undefined) return
|
|
204
|
+
204 if (context.definition.type === 'string' && context.value.toString && context.value.toString().length < context.parameterValue) {
|
|
205
|
+
205 context.throwParamError('MIN_LENGTH', `Length must be at least ${context.parameterValue} characters.`, { min: context.parameterValue, actual: context.value.toString().length })
|
|
206
|
+
206 }
|
|
207
|
+
207 })
|
|
208
|
+
208
|
|
209
|
+
209 addValidator('min', context => {
|
|
210
|
+
210 if (context.value === undefined) return
|
|
211
|
+
211 if (context.definition.type === 'number' && typeof context.value === 'number' && context.value < context.parameterValue) {
|
|
212
|
+
212 context.throwParamError('MIN_VALUE', `Value must be at least ${context.parameterValue}.`, { min: context.parameterValue, actual: context.value })
|
|
213
|
+
213 }
|
|
214
|
+
214 })
|
|
215
|
+
215 addValidator('maxLength', context => {
|
|
216
|
+
216 if (context.value === undefined) return
|
|
217
|
+
217 if (context.definition.type === 'string' && context.value.toString && context.value.toString().length > context.parameterValue) {
|
|
218
|
+
218 context.throwParamError('MAX_LENGTH', `Length must be no more than ${context.parameterValue} characters.`, { max: context.parameterValue, actual: context.value.toString().length })
|
|
219
|
+
219 }
|
|
220
|
+
220 })
|
|
221
|
+
221
|
|
222
|
+
222 addValidator('max', context => {
|
|
223
|
+
223 if (context.value === undefined) return
|
|
224
|
+
224 if (context.definition.type === 'number' && typeof context.value === 'number' && context.value > context.parameterValue) {
|
|
225
|
+
225 context.throwParamError('MAX_VALUE', `Value must be no more than ${context.parameterValue}.`, { max: context.parameterValue, actual: context.value })
|
|
226
|
+
226 }
|
|
227
|
+
227 })
|
|
228
|
+
228 addValidator('validator', async context => {
|
|
229
|
+
229 if (typeof context.parameterValue !== 'function') {
|
|
230
|
+
230 throw new Error(`Validator for ${context.fieldName} must be a function.`)
|
|
231
|
+
231 }
|
|
232
|
+
232 const r = await context.parameterValue(context.value, context.object, context)
|
|
233
|
+
233 if (typeof r === 'string') {
|
|
234
|
+
234 context.throwParamError('CUSTOM_VALIDATOR_FAILED', r)
|
|
235
|
+
235 }
|
|
236
|
+
236 })
|
|
237
|
+
237 addValidator('uppercase', context => {
|
|
238
|
+
238 if (typeof context.value === 'string') return context.value.toUpperCase()
|
|
239
|
+
239 })
|
|
240
|
+
240 addValidator('lowercase', context => {
|
|
241
|
+
241 if (typeof context.value === 'string') return context.value.toLowerCase()
|
|
242
|
+
242 })
|
|
243
|
+
243 addValidator('length', context => {
|
|
244
|
+
244 if (typeof context.value === 'string') {
|
|
245
|
+
245 return context.value.substr(0, context.parameterValue)
|
|
246
|
+
246 } else if (Number.isInteger(Number(context.valueBeforeCast)) && String(context.valueBeforeCast).length > context.parameterValue) {
|
|
247
|
+
247 context.throwParamError('RANGE_EXCEEDED', 'Numeric value is out of the allowed character range.', { max: context.parameterValue, actual: String(context.valueBeforeCast).length })
|
|
248
|
+
248 }
|
|
249
|
+
249 })
|
|
250
|
+
250 addValidator('notEmpty', context => {
|
|
251
|
+
251 const bc = context.valueBeforeCast
|
|
252
|
+
252 const bcs = (bc !== undefined && bc !== null && bc.toString) ? bc.toString() : ''
|
|
253
|
+
253 if (context.parameterValue && !Array.isArray(context.value) && bc !== undefined && bcs === '') {
|
|
254
|
+
254 context.throwParamError('NOT_EMPTY', 'Field cannot be empty.')
|
|
255
|
+
255 }
|
|
256
|
+
256 })
|
|
257
|
+
257 addValidator('required', () => {}) // Stays as a no-op, logic is handled in _validateField
|
|
258
|
+
258
|
|
259
|
+
259 // Add new validators for database alignment
|
|
260
|
+
260 addValidator('unsigned', () => {}) // No-op, used for schema metadata
|
|
261
|
+
261 addValidator('precision', () => {}) // No-op, used for schema metadata
|
|
262
|
+
262 addValidator('scale', () => {}) // No-op, used for schema metadata
|
|
263
|
+
263 addValidator('nullable', () => {}) // No-op, handled in Schema.js
|
|
264
|
+
264 addValidator('nullOnEmpty', () => {}) // No-op, handled in Schema.js
|
|
265
|
+
265 addValidator('defaultTo', () => {}) // No-op, handled in Schema.js
|
|
266
|
+
266 }
|
|
267
|
+
267 }
|
|
268
|
+
268
|
|
269
|
+
269 export default CorePlugin
|