json-rest-api 1.0.14 → 1.0.16
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/connectors_ideas/fastify.js +66 -68
- package/connectors_ideas/koa-plugin.js +50 -51
- package/eslint.config.js +11 -2
- package/examples/url-override-example.js +63 -63
- package/index.js +13 -13
- package/lib/enhanced-logger.js +51 -51
- package/lib/error-formatter.js +61 -61
- package/lib/rest-api-errors.js +28 -28
- package/package.json +1 -1
- package/plugins/core/bulk-operations-plugin.js +142 -145
- package/plugins/core/connectors/express-plugin.js +210 -217
- package/plugins/core/connectors/lib/busboy-detector.js +84 -84
- package/plugins/core/connectors/lib/formidable-detector.js +50 -50
- package/plugins/core/connectors/lib/request-helpers.js +74 -74
- package/plugins/core/file-handling-plugin.js +125 -125
- package/plugins/core/lib/anyapi/anyapi-registry.js +254 -254
- package/plugins/core/lib/anyapi/query/anyapi-query-adapter.js +287 -287
- package/plugins/core/lib/anyapi/schema-utils.js +101 -101
- package/plugins/core/lib/anyapi/utils/descriptor-helpers.js +41 -41
- package/plugins/core/lib/dbTablesOperations.js +134 -135
- package/plugins/core/lib/fractional-positioning.js +74 -76
- package/plugins/core/lib/querying/knex-cross-table-search.js +320 -320
- package/plugins/core/lib/querying/knex-json-api-transformers-querying.js +144 -144
- package/plugins/core/lib/querying/knex-pagination-helpers.js +164 -164
- package/plugins/core/lib/querying/knex-process-includes.js +50 -50
- package/plugins/core/lib/querying/knex-query-helpers-base.js +31 -31
- package/plugins/core/lib/querying/knex-query-helpers.js +451 -452
- package/plugins/core/lib/querying/knex-relationship-includes.js +1160 -1170
- package/plugins/core/lib/querying/knex-window-queries.js +89 -89
- package/plugins/core/lib/querying/url-helpers.js +35 -35
- package/plugins/core/lib/querying-writing/compile-schemas.js +99 -99
- package/plugins/core/lib/querying-writing/connectors-query-parser.js +31 -31
- package/plugins/core/lib/querying-writing/database-capabilities.js +94 -94
- package/plugins/core/lib/querying-writing/database-value-normalizers.js +76 -76
- package/plugins/core/lib/querying-writing/default-data-helpers.js +27 -27
- package/plugins/core/lib/querying-writing/field-utils.js +44 -42
- package/plugins/core/lib/querying-writing/knex-constants.js +15 -16
- package/plugins/core/lib/querying-writing/knex-field-helpers.js +117 -115
- package/plugins/core/lib/querying-writing/knex-json-api-transformers.js +41 -41
- package/plugins/core/lib/querying-writing/payload-validators.js +275 -275
- package/plugins/core/lib/querying-writing/schema-helpers.js +129 -129
- package/plugins/core/lib/querying-writing/scope-validations.js +143 -146
- package/plugins/core/lib/querying-writing/simplified-helpers.js +133 -129
- package/plugins/core/lib/storage/storage-adapter.js +68 -68
- package/plugins/core/lib/writing/knex-json-api-transformers-writing.js +31 -31
- package/plugins/core/lib/writing/many-to-many-manipulations.js +81 -81
- package/plugins/core/lib/writing/relationship-processor.js +58 -58
- package/plugins/core/rest-api-access.js +11 -15
- package/plugins/core/rest-api-anyapi-knex-plugin.js +920 -920
- package/plugins/core/rest-api-cors-plugin.js +170 -171
- package/plugins/core/rest-api-knex-plugin.js +482 -493
- package/plugins/core/rest-api-multihome-plugin.js +247 -251
- package/plugins/core/rest-api-plugin-hooks/compile-resource-schemas.js +5 -5
- package/plugins/core/rest-api-plugin-hooks/register-relationship-routes.js +20 -21
- package/plugins/core/rest-api-plugin-hooks/register-scope-routes.js +44 -44
- package/plugins/core/rest-api-plugin-hooks/turn-scope-init-into-vars.js +23 -23
- package/plugins/core/rest-api-plugin-hooks/validate-include-configurations.js +15 -15
- package/plugins/core/rest-api-plugin-methods/add-route.js +9 -9
- package/plugins/core/rest-api-plugin-methods/check-permissions.js +5 -6
- package/plugins/core/rest-api-plugin-methods/common.js +295 -297
- package/plugins/core/rest-api-plugin-methods/delete-relationship.js +35 -37
- package/plugins/core/rest-api-plugin-methods/delete.js +57 -59
- package/plugins/core/rest-api-plugin-methods/enrich-attributes.js +172 -171
- package/plugins/core/rest-api-plugin-methods/get-related.js +96 -96
- package/plugins/core/rest-api-plugin-methods/get-relationship.js +16 -18
- package/plugins/core/rest-api-plugin-methods/get.js +86 -86
- package/plugins/core/rest-api-plugin-methods/patch-relationship.js +16 -17
- package/plugins/core/rest-api-plugin-methods/patch.js +63 -64
- package/plugins/core/rest-api-plugin-methods/post-relationship.js +54 -55
- package/plugins/core/rest-api-plugin-methods/post.js +61 -62
- package/plugins/core/rest-api-plugin-methods/put.js +86 -87
- package/plugins/core/rest-api-plugin-methods/query.js +88 -88
- package/plugins/core/rest-api-plugin-methods/release.js +5 -6
- package/plugins/core/rest-api-plugin.js +76 -78
- package/plugins/core/rest-api-positioning-plugin.js +251 -252
- package/plugins/core/socketio-plugin.js +240 -236
- package/plugins/storage/local-storage.js +103 -103
- package/plugins/storage/s3-storage.js +41 -42
- package/quickTest.js +37 -37
- package/scripts/debug-anyapi-review-filter.mjs +18 -18
- package/scripts/inspect-anyapi-descriptor-country.mjs +10 -10
- package/scripts/inspect-anyapi-descriptor.mjs +10 -10
- package/scripts/run-docs.js +112 -113
- package/test-debug.js +28 -28
- package/tests/TEST_TEMPLATE.test.js +51 -51
- package/tests/anyapi-basic.test.js +42 -42
- package/tests/anyapi-cursor-pagination.test.js +48 -48
- package/tests/bulk-operations.test.js +208 -208
- package/tests/computed-fields.test.js +204 -204
- package/tests/cors.test.js +412 -413
- package/tests/curl.test.js +275 -276
- package/tests/custom-idproperty-relationships.test.js +266 -266
- package/tests/field-getters.test.js +132 -132
- package/tests/field-setters.test.js +110 -110
- package/tests/fixtures/api-configs.js +699 -699
- package/tests/helpers/storage-mode.js +32 -32
- package/tests/helpers/test-utils.js +76 -76
- package/tests/include-depth-validation.test.js +76 -76
- package/tests/includes.test.js +145 -145
- package/tests/multihome.test.js +175 -175
- package/tests/nested-includes.test.js +255 -255
- package/tests/pagination-cursor-multifield.test.js +164 -164
- package/tests/pagination-enhanced.test.js +225 -225
- package/tests/polymorphic-relationships.test.js +96 -96
- package/tests/polymorphic-simple.test.js +44 -44
- package/tests/positioning-advanced.test.js +226 -226
- package/tests/positioning-extended.test.js +399 -399
- package/tests/positioning-multifilter.test.js +204 -204
- package/tests/positioning.test.js +266 -266
- package/tests/queries.test.js +184 -184
- package/tests/query-limits.test.js +154 -155
- package/tests/relationship-endpoints.test.js +127 -127
- package/tests/relationships.test.js +178 -179
- package/tests/rest-api-access.test.js +86 -86
- package/tests/rest-api.test.js +103 -103
- package/tests/searchschema-merge.test.js +93 -93
- package/tests/socketio.test.js +326 -326
- package/tests/virtual-fields.test.js +164 -164
|
@@ -1,94 +1,94 @@
|
|
|
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
|
-
import { promises as fs } from 'fs'
|
|
8
|
-
import path from 'path'
|
|
9
|
-
import crypto from 'crypto'
|
|
7
|
+
import { promises as fs } from 'fs'
|
|
8
|
+
import path from 'path'
|
|
9
|
+
import crypto from 'crypto'
|
|
10
10
|
|
|
11
11
|
export class LocalStorage {
|
|
12
|
-
constructor(options = {}) {
|
|
13
|
-
this.directory = path.resolve(options.directory || './uploads')
|
|
14
|
-
this.fileBaseUrl = options.fileBaseUrl || '/uploads'
|
|
15
|
-
this.nameStrategy = options.nameStrategy || 'hash'
|
|
16
|
-
this.nameGenerator = options.nameGenerator
|
|
17
|
-
this.preserveExtension = options.preserveExtension !== false
|
|
18
|
-
this.allowedExtensions = options.allowedExtensions
|
|
19
|
-
this.maxFilenameLength = options.maxFilenameLength || 255
|
|
12
|
+
constructor (options = {}) {
|
|
13
|
+
this.directory = path.resolve(options.directory || './uploads')
|
|
14
|
+
this.fileBaseUrl = options.fileBaseUrl || '/uploads'
|
|
15
|
+
this.nameStrategy = options.nameStrategy || 'hash' // 'hash', 'timestamp', 'original', 'custom'
|
|
16
|
+
this.nameGenerator = options.nameGenerator // Custom function
|
|
17
|
+
this.preserveExtension = options.preserveExtension !== false
|
|
18
|
+
this.allowedExtensions = options.allowedExtensions // Whitelist
|
|
19
|
+
this.maxFilenameLength = options.maxFilenameLength || 255
|
|
20
20
|
}
|
|
21
|
-
|
|
21
|
+
|
|
22
22
|
/**
|
|
23
23
|
* Generate a safe filename
|
|
24
24
|
*/
|
|
25
|
-
async generateFilename(file) {
|
|
26
|
-
let basename, extension
|
|
27
|
-
|
|
25
|
+
async generateFilename (file) {
|
|
26
|
+
let basename, extension
|
|
27
|
+
|
|
28
28
|
// Extract and validate extension
|
|
29
|
-
const originalExt = path.extname(file.filename).toLowerCase()
|
|
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
|
-
throw new Error(`File extension '${originalExt}' is not allowed`)
|
|
33
|
+
throw new Error(`File extension '${originalExt}' is not allowed`)
|
|
34
34
|
}
|
|
35
|
-
extension = originalExt
|
|
35
|
+
extension = originalExt
|
|
36
36
|
} else {
|
|
37
37
|
// Determine extension from MIME type for safety
|
|
38
|
-
extension = this.getExtensionFromMimeType(file.mimetype)
|
|
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
|
-
basename = crypto.randomBytes(16).toString('hex')
|
|
46
|
-
break
|
|
47
|
-
|
|
45
|
+
basename = crypto.randomBytes(16).toString('hex')
|
|
46
|
+
break
|
|
47
|
+
|
|
48
48
|
case 'timestamp':
|
|
49
49
|
// Timestamp + random suffix to prevent collisions
|
|
50
|
-
basename = `${Date.now()}_${crypto.randomBytes(4).toString('hex')}
|
|
51
|
-
break
|
|
52
|
-
|
|
50
|
+
basename = `${Date.now()}_${crypto.randomBytes(4).toString('hex')}`
|
|
51
|
+
break
|
|
52
|
+
|
|
53
53
|
case 'original':
|
|
54
54
|
// Sanitized original filename
|
|
55
55
|
basename = this.sanitizeFilename(
|
|
56
56
|
path.basename(file.filename, originalExt)
|
|
57
|
-
)
|
|
58
|
-
break
|
|
59
|
-
|
|
57
|
+
)
|
|
58
|
+
break
|
|
59
|
+
|
|
60
60
|
case 'custom':
|
|
61
61
|
if (!this.nameGenerator) {
|
|
62
|
-
throw new Error('nameGenerator function required for custom strategy')
|
|
62
|
+
throw new Error('nameGenerator function required for custom strategy')
|
|
63
63
|
}
|
|
64
|
-
basename = await this.nameGenerator(file)
|
|
65
|
-
break
|
|
66
|
-
|
|
64
|
+
basename = await this.nameGenerator(file)
|
|
65
|
+
break
|
|
66
|
+
|
|
67
67
|
default:
|
|
68
|
-
throw new Error(`Unknown name strategy: ${this.nameStrategy}`)
|
|
68
|
+
throw new Error(`Unknown name strategy: ${this.nameStrategy}`)
|
|
69
69
|
}
|
|
70
|
-
|
|
70
|
+
|
|
71
71
|
// Combine basename and extension
|
|
72
|
-
let filename = extension ? `${basename}${extension}` : basename
|
|
73
|
-
|
|
72
|
+
let filename = extension ? `${basename}${extension}` : basename
|
|
73
|
+
|
|
74
74
|
// Ensure filename doesn't exceed max length
|
|
75
75
|
if (filename.length > this.maxFilenameLength) {
|
|
76
76
|
// Truncate basename to fit
|
|
77
|
-
const maxBasename = this.maxFilenameLength - (extension?.length || 0)
|
|
78
|
-
basename = basename.substring(0, maxBasename)
|
|
79
|
-
filename = extension ? `${basename}${extension}` : basename
|
|
77
|
+
const maxBasename = this.maxFilenameLength - (extension?.length || 0)
|
|
78
|
+
basename = basename.substring(0, maxBasename)
|
|
79
|
+
filename = extension ? `${basename}${extension}` : basename
|
|
80
80
|
}
|
|
81
|
-
|
|
81
|
+
|
|
82
82
|
// Handle duplicates
|
|
83
|
-
filename = await this.ensureUnique(filename)
|
|
84
|
-
|
|
85
|
-
return filename
|
|
83
|
+
filename = await this.ensureUnique(filename)
|
|
84
|
+
|
|
85
|
+
return filename
|
|
86
86
|
}
|
|
87
|
-
|
|
87
|
+
|
|
88
88
|
/**
|
|
89
89
|
* Sanitize filename for safe storage
|
|
90
90
|
*/
|
|
91
|
-
sanitizeFilename(filename) {
|
|
91
|
+
sanitizeFilename (filename) {
|
|
92
92
|
return filename
|
|
93
93
|
// Remove path traversal attempts
|
|
94
94
|
.replace(/\.\./g, '')
|
|
@@ -98,53 +98,53 @@ export class LocalStorage {
|
|
|
98
98
|
// Remove leading/trailing dots and spaces
|
|
99
99
|
.replace(/^[\s.]+|[\s.]+$/g, '')
|
|
100
100
|
// Collapse multiple underscores
|
|
101
|
-
.replace(/_+/g, '_')
|
|
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
|
-
async ensureUnique(filename) {
|
|
110
|
-
const filepath = path.join(this.directory, filename)
|
|
111
|
-
|
|
109
|
+
async ensureUnique (filename) {
|
|
110
|
+
const filepath = path.join(this.directory, filename)
|
|
111
|
+
|
|
112
112
|
try {
|
|
113
|
-
await fs.access(filepath)
|
|
113
|
+
await fs.access(filepath)
|
|
114
114
|
// File exists, generate unique name
|
|
115
|
-
const ext = path.extname(filename)
|
|
116
|
-
const base = path.basename(filename, ext)
|
|
117
|
-
let counter = 1
|
|
118
|
-
let newFilename
|
|
119
|
-
|
|
115
|
+
const ext = path.extname(filename)
|
|
116
|
+
const base = path.basename(filename, ext)
|
|
117
|
+
let counter = 1
|
|
118
|
+
let newFilename
|
|
119
|
+
|
|
120
120
|
do {
|
|
121
|
-
newFilename = `${base}_${counter}${ext}
|
|
122
|
-
counter
|
|
123
|
-
} while (await this.fileExists(path.join(this.directory, newFilename)))
|
|
124
|
-
|
|
125
|
-
return newFilename
|
|
121
|
+
newFilename = `${base}_${counter}${ext}`
|
|
122
|
+
counter++
|
|
123
|
+
} while (await this.fileExists(path.join(this.directory, newFilename)))
|
|
124
|
+
|
|
125
|
+
return newFilename
|
|
126
126
|
} catch (error) {
|
|
127
127
|
// File doesn't exist, name is unique
|
|
128
|
-
return filename
|
|
128
|
+
return filename
|
|
129
129
|
}
|
|
130
130
|
}
|
|
131
|
-
|
|
131
|
+
|
|
132
132
|
/**
|
|
133
133
|
* Check if file exists
|
|
134
134
|
*/
|
|
135
|
-
async fileExists(filepath) {
|
|
135
|
+
async fileExists (filepath) {
|
|
136
136
|
try {
|
|
137
|
-
await fs.access(filepath)
|
|
138
|
-
return true
|
|
137
|
+
await fs.access(filepath)
|
|
138
|
+
return true
|
|
139
139
|
} catch {
|
|
140
|
-
return false
|
|
140
|
+
return false
|
|
141
141
|
}
|
|
142
142
|
}
|
|
143
|
-
|
|
143
|
+
|
|
144
144
|
/**
|
|
145
145
|
* Get safe extension from MIME type
|
|
146
146
|
*/
|
|
147
|
-
getExtensionFromMimeType(mimetype) {
|
|
147
|
+
getExtensionFromMimeType (mimetype) {
|
|
148
148
|
// Safe mappings only
|
|
149
149
|
const mimeToExt = {
|
|
150
150
|
'image/jpeg': '.jpg',
|
|
@@ -157,62 +157,62 @@ export class LocalStorage {
|
|
|
157
157
|
'video/mp4': '.mp4',
|
|
158
158
|
'audio/mpeg': '.mp3',
|
|
159
159
|
'application/zip': '.zip'
|
|
160
|
-
}
|
|
161
|
-
|
|
162
|
-
return mimeToExt[mimetype] || '.bin'
|
|
160
|
+
}
|
|
161
|
+
|
|
162
|
+
return mimeToExt[mimetype] || '.bin'
|
|
163
163
|
}
|
|
164
|
-
|
|
164
|
+
|
|
165
165
|
/**
|
|
166
166
|
* Upload file with secure filename
|
|
167
167
|
*/
|
|
168
|
-
async upload(file) {
|
|
168
|
+
async upload (file) {
|
|
169
169
|
// Ensure directory exists
|
|
170
|
-
await fs.mkdir(this.directory, { recursive: true })
|
|
171
|
-
|
|
170
|
+
await fs.mkdir(this.directory, { recursive: true })
|
|
171
|
+
|
|
172
172
|
// Generate secure filename
|
|
173
|
-
const filename = await this.generateFilename(file)
|
|
174
|
-
const filepath = path.join(this.directory, filename)
|
|
175
|
-
|
|
173
|
+
const filename = await this.generateFilename(file)
|
|
174
|
+
const filepath = path.join(this.directory, filename)
|
|
175
|
+
|
|
176
176
|
// Ensure we're not writing outside our directory (defense in depth)
|
|
177
|
-
const resolvedPath = path.resolve(filepath)
|
|
178
|
-
const resolvedDir = path.resolve(this.directory)
|
|
177
|
+
const resolvedPath = path.resolve(filepath)
|
|
178
|
+
const resolvedDir = path.resolve(this.directory)
|
|
179
179
|
if (!resolvedPath.startsWith(resolvedDir)) {
|
|
180
|
-
throw new Error('Invalid file path')
|
|
180
|
+
throw new Error('Invalid file path')
|
|
181
181
|
}
|
|
182
|
-
|
|
182
|
+
|
|
183
183
|
// Write file
|
|
184
184
|
if (file.data) {
|
|
185
|
-
await fs.writeFile(filepath, file.data)
|
|
185
|
+
await fs.writeFile(filepath, file.data)
|
|
186
186
|
} else if (file.filepath) {
|
|
187
|
-
await fs.rename(file.filepath, filepath)
|
|
187
|
+
await fs.rename(file.filepath, filepath)
|
|
188
188
|
} else {
|
|
189
|
-
throw new Error('File has no data or filepath')
|
|
189
|
+
throw new Error('File has no data or filepath')
|
|
190
190
|
}
|
|
191
|
-
|
|
191
|
+
|
|
192
192
|
// Return public URL
|
|
193
|
-
return `${this.fileBaseUrl}/${filename}
|
|
193
|
+
return `${this.fileBaseUrl}/${filename}`
|
|
194
194
|
}
|
|
195
|
-
|
|
195
|
+
|
|
196
196
|
/**
|
|
197
197
|
* Delete a file
|
|
198
198
|
*/
|
|
199
|
-
async delete(url) {
|
|
200
|
-
const filename = path.basename(url)
|
|
201
|
-
const filepath = path.join(this.directory, filename)
|
|
202
|
-
|
|
199
|
+
async delete (url) {
|
|
200
|
+
const filename = path.basename(url)
|
|
201
|
+
const filepath = path.join(this.directory, filename)
|
|
202
|
+
|
|
203
203
|
// Security check
|
|
204
|
-
const resolvedPath = path.resolve(filepath)
|
|
205
|
-
const resolvedDir = path.resolve(this.directory)
|
|
204
|
+
const resolvedPath = path.resolve(filepath)
|
|
205
|
+
const resolvedDir = path.resolve(this.directory)
|
|
206
206
|
if (!resolvedPath.startsWith(resolvedDir)) {
|
|
207
|
-
throw new Error('Invalid file path')
|
|
207
|
+
throw new Error('Invalid file path')
|
|
208
208
|
}
|
|
209
|
-
|
|
209
|
+
|
|
210
210
|
try {
|
|
211
|
-
await fs.unlink(filepath)
|
|
211
|
+
await fs.unlink(filepath)
|
|
212
212
|
} catch (error) {
|
|
213
213
|
if (error.code !== 'ENOENT') {
|
|
214
|
-
throw error
|
|
214
|
+
throw error
|
|
215
215
|
}
|
|
216
216
|
}
|
|
217
217
|
}
|
|
218
|
-
}
|
|
218
|
+
}
|
|
@@ -1,12 +1,12 @@
|
|
|
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',
|
|
@@ -14,63 +14,63 @@
|
|
|
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
|
-
import crypto from 'crypto'
|
|
25
|
-
import path from 'path'
|
|
24
|
+
import crypto from 'crypto'
|
|
25
|
+
import path from 'path'
|
|
26
26
|
|
|
27
27
|
export class S3Storage {
|
|
28
|
-
constructor(options = {}) {
|
|
29
|
-
this.bucket = options.bucket
|
|
30
|
-
this.region = options.region || 'us-east-1'
|
|
31
|
-
this.prefix = options.prefix || ''
|
|
32
|
-
this.acl = options.acl || 'public-read'
|
|
33
|
-
|
|
28
|
+
constructor (options = {}) {
|
|
29
|
+
this.bucket = options.bucket
|
|
30
|
+
this.region = options.region || 'us-east-1'
|
|
31
|
+
this.prefix = options.prefix || ''
|
|
32
|
+
this.acl = options.acl || 'public-read'
|
|
33
|
+
|
|
34
34
|
if (!this.bucket) {
|
|
35
|
-
throw new Error('S3Storage requires a bucket name')
|
|
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
|
-
this.mockMode = options.mockMode !== false
|
|
41
|
-
|
|
40
|
+
this.mockMode = options.mockMode !== false // Default to mock mode
|
|
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
|
-
async upload(file) {
|
|
54
|
+
async upload (file) {
|
|
55
55
|
// Generate S3 key
|
|
56
|
-
const ext = path.extname(file.filename)
|
|
57
|
-
const hash = crypto.randomBytes(16).toString('hex')
|
|
58
|
-
const key = `${this.prefix}${hash}${ext}
|
|
59
|
-
|
|
56
|
+
const ext = path.extname(file.filename)
|
|
57
|
+
const hash = crypto.randomBytes(16).toString('hex')
|
|
58
|
+
const key = `${this.prefix}${hash}${ext}`
|
|
59
|
+
|
|
60
60
|
if (this.mockMode) {
|
|
61
61
|
// Mock mode - just return a fake URL
|
|
62
|
-
const url = `https://${this.bucket}.s3.${this.region}.amazonaws.com/${key}
|
|
63
|
-
|
|
62
|
+
const url = `https://${this.bucket}.s3.${this.region}.amazonaws.com/${key}`
|
|
63
|
+
|
|
64
64
|
// Simulate upload delay
|
|
65
|
-
await new Promise(resolve => setTimeout(resolve, 100))
|
|
66
|
-
|
|
67
|
-
return url
|
|
65
|
+
await new Promise(resolve => setTimeout(resolve, 100))
|
|
66
|
+
|
|
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,
|
|
@@ -81,9 +81,9 @@ export class S3Storage {
|
|
|
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}`;
|
|
@@ -93,33 +93,32 @@ export class S3Storage {
|
|
|
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
|
-
async delete(url) {
|
|
102
|
+
async delete (url) {
|
|
103
103
|
// Extract key from URL
|
|
104
|
-
const urlObj = new URL(url)
|
|
105
|
-
const key = urlObj.pathname.substring(1)
|
|
106
|
-
|
|
104
|
+
const urlObj = new URL(url)
|
|
105
|
+
const key = urlObj.pathname.substring(1) // Remove leading /
|
|
106
|
+
|
|
107
107
|
if (this.mockMode) {
|
|
108
108
|
// Mock mode - just simulate deletion
|
|
109
|
-
await new Promise(resolve => setTimeout(resolve, 50))
|
|
110
|
-
return;
|
|
109
|
+
await new Promise(resolve => setTimeout(resolve, 50))
|
|
111
110
|
}
|
|
112
|
-
|
|
111
|
+
|
|
113
112
|
// Real S3 delete would go here
|
|
114
113
|
/*
|
|
115
114
|
const { DeleteObjectCommand } = require('@aws-sdk/client-s3');
|
|
116
|
-
|
|
115
|
+
|
|
117
116
|
const command = new DeleteObjectCommand({
|
|
118
117
|
Bucket: this.bucket,
|
|
119
118
|
Key: key
|
|
120
119
|
});
|
|
121
|
-
|
|
120
|
+
|
|
122
121
|
await this.s3.send(command);
|
|
123
122
|
*/
|
|
124
123
|
}
|
|
125
|
-
}
|
|
124
|
+
}
|
package/quickTest.js
CHANGED
|
@@ -1,11 +1,11 @@
|
|
|
1
1
|
//
|
|
2
2
|
// index.js
|
|
3
3
|
//
|
|
4
|
-
import { RestApiPlugin, RestApiKnexPlugin, ExpressPlugin } from './index.js'
|
|
5
|
-
import { Api } from 'hooked-api'
|
|
6
|
-
import knexLib from 'knex'
|
|
7
|
-
import util from 'util'
|
|
8
|
-
import express from 'express'
|
|
4
|
+
import { RestApiPlugin, RestApiKnexPlugin, ExpressPlugin } from './index.js' // Added: ExpressPlugin
|
|
5
|
+
import { Api } from 'hooked-api'
|
|
6
|
+
import knexLib from 'knex'
|
|
7
|
+
import util from 'util'
|
|
8
|
+
import express from 'express' // Added: Express
|
|
9
9
|
|
|
10
10
|
// Utility used throughout this guide
|
|
11
11
|
const inspect = (obj) => util.inspect(obj, { depth: 8 })
|
|
@@ -17,15 +17,15 @@ const knex = knexLib({
|
|
|
17
17
|
filename: ':memory:'
|
|
18
18
|
},
|
|
19
19
|
useNullAsDefault: true
|
|
20
|
-
})
|
|
20
|
+
})
|
|
21
21
|
|
|
22
22
|
// Create API instance
|
|
23
|
-
const api = new Api({ name: 'book-catalog-api', logLevel: 'trace' })
|
|
23
|
+
const api = new Api({ name: 'book-catalog-api', logLevel: 'trace' })
|
|
24
24
|
|
|
25
25
|
// Install plugins
|
|
26
|
-
await api.use(RestApiPlugin, {returnBasePath: '/api' })
|
|
27
|
-
await api.use(RestApiKnexPlugin, { knex })
|
|
28
|
-
await api.use(ExpressPlugin, {
|
|
26
|
+
await api.use(RestApiPlugin, { returnBasePath: '/api' })
|
|
27
|
+
await api.use(RestApiKnexPlugin, { knex })
|
|
28
|
+
await api.use(ExpressPlugin, { mountPath: '/api' }) // Added: Express Plugin
|
|
29
29
|
|
|
30
30
|
// Define publishers resource
|
|
31
31
|
await api.addResource('publishers', {
|
|
@@ -39,8 +39,8 @@ await api.addResource('publishers', {
|
|
|
39
39
|
searchSchema: { // Adding search schema for publishers
|
|
40
40
|
name: { type: 'string', filterOperator: 'like' }
|
|
41
41
|
}
|
|
42
|
-
})
|
|
43
|
-
await api.resources.publishers.createKnexTable()
|
|
42
|
+
})
|
|
43
|
+
await api.resources.publishers.createKnexTable()
|
|
44
44
|
|
|
45
45
|
// Define authors resource, which belongs to a publisher
|
|
46
46
|
await api.addResource('authors', {
|
|
@@ -55,21 +55,21 @@ await api.addResource('authors', {
|
|
|
55
55
|
publisher: { type: 'id', actualField: 'publisher_id', nullable: true },
|
|
56
56
|
publisherName: { type: 'string', actualField: 'publishers.name', filterOperator: 'like' } // Cross-table search
|
|
57
57
|
}
|
|
58
|
-
})
|
|
59
|
-
await api.resources.authors.createKnexTable()
|
|
58
|
+
})
|
|
59
|
+
await api.resources.authors.createKnexTable()
|
|
60
60
|
|
|
61
61
|
// Method 1: Simplified mode without inputRecord (most concise)
|
|
62
62
|
const penguinResult = await api.resources.publishers.post({
|
|
63
63
|
name: 'Penguin Random House'
|
|
64
|
-
})
|
|
65
|
-
console.log('Created publisher:', inspect(penguinResult))
|
|
64
|
+
})
|
|
65
|
+
console.log('Created publisher:', inspect(penguinResult))
|
|
66
66
|
|
|
67
67
|
// Method 2: Simplified mode with inputRecord (explicit)
|
|
68
68
|
const harperResult = await api.resources.publishers.post({
|
|
69
69
|
inputRecord: {
|
|
70
70
|
name: 'HarperCollins'
|
|
71
71
|
}
|
|
72
|
-
})
|
|
72
|
+
})
|
|
73
73
|
|
|
74
74
|
// Method 3: Full JSON:API mode (standards compliant)
|
|
75
75
|
const oxfordResult = await api.resources.publishers.post({
|
|
@@ -82,54 +82,54 @@ const oxfordResult = await api.resources.publishers.post({
|
|
|
82
82
|
}
|
|
83
83
|
},
|
|
84
84
|
simplified: false
|
|
85
|
-
})
|
|
86
|
-
console.log('JSON:API response:', inspect(oxfordResult))
|
|
85
|
+
})
|
|
86
|
+
console.log('JSON:API response:', inspect(oxfordResult))
|
|
87
87
|
|
|
88
88
|
// Create an author linked to the first publisher (simplified)
|
|
89
89
|
const authorResult = await api.resources.authors.post({
|
|
90
90
|
name: 'George',
|
|
91
91
|
surname: 'Orwell',
|
|
92
92
|
publisher_id: penguinResult.id
|
|
93
|
-
})
|
|
94
|
-
console.log('Created author:', inspect(authorResult))
|
|
93
|
+
})
|
|
94
|
+
console.log('Created author:', inspect(authorResult))
|
|
95
95
|
|
|
96
96
|
// Get all publishers
|
|
97
|
-
const allPublishers = await api.resources.publishers.query({})
|
|
98
|
-
console.log('All publishers:', inspect(allPublishers))
|
|
97
|
+
const allPublishers = await api.resources.publishers.query({})
|
|
98
|
+
console.log('All publishers:', inspect(allPublishers))
|
|
99
99
|
|
|
100
100
|
// Get publisher with included authors
|
|
101
101
|
const publisherWithAuthors = await api.resources.publishers.get({
|
|
102
102
|
id: penguinResult.id,
|
|
103
103
|
include: ['authors']
|
|
104
|
-
})
|
|
105
|
-
console.log('Publisher with authors:', inspect(publisherWithAuthors))
|
|
104
|
+
})
|
|
105
|
+
console.log('Publisher with authors:', inspect(publisherWithAuthors))
|
|
106
106
|
|
|
107
107
|
// Search authors by name
|
|
108
108
|
const searchResult = await api.resources.authors.query({
|
|
109
109
|
filter: { name: 'George' }
|
|
110
|
-
})
|
|
111
|
-
console.log('Search results:', inspect(searchResult))
|
|
110
|
+
})
|
|
111
|
+
console.log('Search results:', inspect(searchResult))
|
|
112
112
|
|
|
113
113
|
// Update an author
|
|
114
114
|
const updateResult = await api.resources.authors.patch({
|
|
115
115
|
id: authorResult.id,
|
|
116
116
|
surname: 'Orwell (Eric Blair)'
|
|
117
|
-
})
|
|
118
|
-
console.log('Updated author:', inspect(updateResult))
|
|
117
|
+
})
|
|
118
|
+
console.log('Updated author:', inspect(updateResult))
|
|
119
119
|
|
|
120
120
|
/// *** ...programmatic calls here... ***
|
|
121
121
|
|
|
122
|
-
// Create the express server and add the API's routes
|
|
123
|
-
const app = express()
|
|
124
|
-
app.use(api.http.express.router)
|
|
125
|
-
app.use(api.http.express.notFoundRouter)
|
|
122
|
+
// Create the express server and add the API's routes
|
|
123
|
+
const app = express()
|
|
124
|
+
app.use(api.http.express.router)
|
|
125
|
+
app.use(api.http.express.notFoundRouter)
|
|
126
126
|
|
|
127
127
|
app.listen(3000, () => {
|
|
128
|
-
console.log('Express server started on port 3000. API available at http://localhost:3000/api')
|
|
128
|
+
console.log('Express server started on port 3000. API available at http://localhost:3000/api')
|
|
129
129
|
}).on('error', (err) => {
|
|
130
|
-
console.error('Failed to start server:', err)
|
|
130
|
+
console.error('Failed to start server:', err)
|
|
131
131
|
process.exit(1)
|
|
132
132
|
}).on('error', (err) => {
|
|
133
|
-
console.error('Failed to start server:', err)
|
|
133
|
+
console.error('Failed to start server:', err)
|
|
134
134
|
process.exit(1)
|
|
135
|
-
})
|
|
135
|
+
})
|