upgrade to express v5 (#6304)

exact copy of #6175 

- I will point the uppy deps upgrade pr to this PR's branch
- no other changes needed in this branch as #6175 was reviewed and all
the comments were resolved

this should be the order of merge : 

1. #6300
2. uppy deps upgrade ( yet to be raised ) 
3. merge this to main
4. merge biome changes #6244 

@mifi let me know if you think otherwise.

---------

Co-authored-by: Mikael Finstad <finstaden@gmail.com>
This commit is contained in:
Prakash 2026-05-21 08:44:23 +05:30 committed by GitHub
parent d9742ba4e4
commit d9d44ce5e5
No known key found for this signature in database
GPG key ID: B5690EEEBB952194
27 changed files with 439 additions and 2332 deletions

View file

@ -0,0 +1,5 @@
---
"@uppy/aws-s3": patch
---
Improve type

View file

@ -0,0 +1,5 @@
---
"@uppy/companion": major
---
Upgrade to express 5 - Companion no longer works when used as a middleware with Express 4.

View file

@ -15,7 +15,7 @@
"cookie-parser": "^1.4.7",
"cors": "^2.8.5",
"dotenv": "^16.0.1",
"express": "^4.22.0",
"express": "^5.2.1",
"express-session": "^1.17.3",
"npm-run-all": "^4.1.5",
"vite": "^7.1.11"

View file

@ -15,6 +15,6 @@
"@aws-sdk/s3-request-presigner": "^3.338.0",
"body-parser": "^1.20.4",
"dotenv": "^16.0.0",
"express": "^4.22.0"
"express": "^5.2.1"
}
}

View file

@ -19,7 +19,7 @@
"@uppy/companion": "workspace:*",
"body-parser": "^1.20.4",
"dotenv": "^16.0.1",
"express": "^4.22.0",
"express": "^5.2.1",
"express-session": "^1.15.6",
"npm-run-all": "^4.1.2",
"vite": "^7.1.11"

View file

@ -11,7 +11,7 @@
},
"devDependencies": {
"dotenv": "^16.0.1",
"express": "^4.22.0",
"express": "^5.2.1",
"vite": "^7.1.11"
},
"private": true,

View file

@ -4,7 +4,7 @@
"dependencies": {
"@uppy/companion": "workspace:*",
"body-parser": "^1.20.4",
"express": "^4.22.0",
"express": "^5.2.1",
"express-session": "^1.15.6",
"light-server": "^2.4.0"
},

View file

@ -20,14 +20,14 @@
"@uppy/transloadit": "workspace:*",
"@uppy/tus": "workspace:*",
"@uppy/xhr-upload": "workspace:*",
"express": "^4.22.0",
"express": "^5.2.1",
"react": "^18.2.0",
"react-dom": "^18.2.0",
"react-router": "^7.12.0"
},
"devDependencies": {
"@react-router/dev": "^7.12.0",
"@types/express": "^4.17.20",
"@types/express": "^5.0.6",
"@types/react": "^18.2.20",
"@types/react-dom": "^18.2.7",
"tsx": "^4.0.0",

View file

@ -41,7 +41,9 @@ async function startServer() {
// TUS upload endpoints (before React Router)
app.all('/api/upload/tus', (req, res) => tusServer.handle(req, res))
app.all('/api/upload/tus/*', (req, res) => tusServer.handle(req, res))
app.all('/api/upload/tus/{*splat}', (req, res) =>
tusServer.handle(req, res),
)
// Handle Chrome DevTools requests silently
app.get('/.well-known/appspecific/com.chrome.devtools.json', (req, res) => {
@ -49,7 +51,7 @@ async function startServer() {
})
// React Router handles all other routes
app.all('*', reactRouterHandler)
app.all('/{*splat}', reactRouterHandler)
const port = process.env.PORT || 3000
const server = app.listen(port, () => {

View file

@ -15,7 +15,7 @@
"@uppy/remote-sources": "workspace:*",
"@uppy/transloadit": "workspace:*",
"@uppy/webcam": "workspace:*",
"express": "^4.22.0",
"express": "^5.2.1",
"he": "^1.2.0"
},
"private": true,

View file

@ -6,7 +6,7 @@
"@uppy/dashboard": "workspace:*",
"@uppy/xhr-upload": "workspace:*",
"cors": "^2.8.5",
"express": "^4.22.0",
"express": "^5.2.1",
"multer": "^2.0.2"
},
"devDependencies": {

View file

@ -662,10 +662,11 @@ export default class AwsS3Multipart<
querify: true,
})
const query = new URLSearchParams({ filename, type, ...metadata } as Record<
string,
string
>)
const query = new URLSearchParams({
filename,
...(type != null && { type }),
...metadata,
})
return this.#client.get(`s3/params?${query}`, options)
}

View file

@ -47,9 +47,9 @@
"cors": "^2.8.5",
"escape-goat": "3.0.0",
"escape-string-regexp": "4.0.0",
"express": "4.21.2",
"express": "^5.2.1",
"express-interceptor": "1.2.0",
"express-prom-bundle": "7",
"express-prom-bundle": "^8.0.0",
"express-session": "1.18.1",
"fast-safe-stringify": "^2.1.1",
"formdata-node": "^6.0.3",

View file

@ -125,6 +125,9 @@ export function app(optionsArg: CompanionInitOptions) {
const app = express()
// Needed for e.g. s3 `/params` endpoint metadata, like `metadata[key]=value` to be parsed into a metadata object
app.set('query parser', 'extended')
if (options.metrics) {
app.use(middlewares.metrics({ path: options.server.path }))
}
@ -136,10 +139,10 @@ export function app(optionsArg: CompanionInitOptions) {
app.use(interceptGrantErrorResponse)
// override provider credentials at request time
// Making `POST` request to the `/connect/:provider/:override?` route requires a form body parser middleware:
// Making `POST` request to the `/connect/:provider{/:override}` route requires a form body parser middleware:
// See https://github.com/simov/grant#dynamic-http
app.use(
'/connect/:oauthProvider/:override?',
'/connect/:oauthProvider{/:override}',
express.urlencoded({ extended: false }),
getCredentialsOverrideMiddleware(
providers as Record<string, typeof Provider>,
@ -159,7 +162,7 @@ export function app(optionsArg: CompanionInitOptions) {
app.use(middlewares.cors(options))
// add uppy options to the request object so it can be accessed by subsequent handlers.
app.use('*', middlewares.getCompanionMiddleware(options))
app.use(middlewares.getCompanionMiddleware(options))
app.use('/s3', s3(options.s3))
if (options.enableUrlEndpoint) app.use('/url', url())
if (options.enableGooglePickerEndpoint)
@ -232,7 +235,7 @@ export function app(optionsArg: CompanionInitOptions) {
)
app.get(
'/:providerName/list/:id?',
'/:providerName/list{/:id}',
middlewares.hasSessionAndProvider,
middlewares.verifyToken,
controllers.list,

View file

@ -20,7 +20,7 @@ export default function callback(
const providerName = req.params['providerName']
const { companion } = req
if (providerName == null || providerName.length === 0) {
if (typeof providerName !== 'string' || providerName.length === 0) {
res.sendStatus(400)
return
}

View file

@ -7,8 +7,9 @@ export default async function list(
next: NextFunction,
): Promise<void> {
const { query, params, companion } = req
const id = params['id']
const { providerUserSession, provider } = companion
if (!provider) {
if (!provider || (typeof id !== 'string' && id != null)) {
res.sendStatus(400)
return
}
@ -17,7 +18,7 @@ export default async function list(
const data = await provider.list({
companion,
providerUserSession,
directory: params['id'],
directory: id,
query,
})
res.json(data)

View file

@ -14,7 +14,7 @@ export default function preauth(req: Request, res: Response): void {
}
const providerName = req.params['providerName']
if (providerName == null || providerName.length === 0) {
if (typeof providerName !== 'string' || providerName.length === 0) {
res.sendStatus(400)
return
}

View file

@ -12,7 +12,7 @@ export default async function refreshToken(
next: NextFunction,
): Promise<void> {
const providerName = req.params['providerName']
if (providerName == null) {
if (typeof providerName !== 'string' || providerName.length === 0) {
res.sendStatus(400)
return
}

View file

@ -1,3 +1,4 @@
import assert from 'node:assert'
import type { Part, S3Client } from '@aws-sdk/client-s3'
import {
AbortMultipartUploadCommand,
@ -247,6 +248,11 @@ export default function s3(
const { uploadId } = req.params
const { key } = req.query
assert(
typeof uploadId === 'string' && uploadId.length > 0,
's3: uploadId must be provided.',
)
if (typeof key !== 'string') {
res.status(400).json({
error:
@ -260,7 +266,7 @@ export default function s3(
const parts: Part[] = []
function listPartsPage(startAt?: string) {
const listPartsPage = (startAt?: string) => {
s3Client
.send(
new ListPartsCommand({
@ -303,7 +309,7 @@ export default function s3(
const partNumber = req.params['partNumber']
const key = req.query['key']
if (uploadId == null || uploadId.length === 0) {
if (typeof uploadId !== 'string' || uploadId.length === 0) {
res.status(400).json({ error: 's3: uploadId must be provided.' })
return
}
@ -314,7 +320,7 @@ export default function s3(
})
return
}
if (partNumber == null || !parseInt(partNumber, 10)) {
if (typeof partNumber !== 'string' || !parseInt(partNumber, 10)) {
res.status(400).json({
error: 's3: the part number must be a number between 1 and 10000.',
})
@ -363,7 +369,7 @@ export default function s3(
const key = req.query['key']
const partNumbers = req.query['partNumbers']
if (uploadId == null || uploadId.length === 0) {
if (typeof uploadId !== 'string' || uploadId.length === 0) {
res.status(400).json({ error: 's3: uploadId must be provided.' })
return
}
@ -443,6 +449,11 @@ export default function s3(
const { uploadId } = req.params
const { key } = req.query
assert(
typeof uploadId === 'string' && uploadId.length > 0,
's3: uploadId must be provided.',
)
if (typeof key !== 'string') {
res.status(400).json({
error:
@ -488,6 +499,11 @@ export default function s3(
const { key } = req.query
const { parts }: { parts: unknown } = req.body
assert(
typeof uploadId === 'string' && uploadId.length > 0,
's3: uploadId must be provided.',
)
if (typeof key !== 'string') {
res.status(400).json({
error:

View file

@ -9,7 +9,7 @@ export default async function simpleAuth(
next: NextFunction,
): Promise<void> {
const providerName = req.params['providerName']
if (providerName == null || providerName.length === 0) {
if (typeof providerName !== 'string' || providerName.length === 0) {
res.sendStatus(400)
return
}

View file

@ -7,7 +7,7 @@ async function thumbnail(
next: NextFunction,
): Promise<void> {
const id = req.params['id']
if (id == null) {
if (typeof id !== 'string' || id.length === 0) {
res.sendStatus(400)
return
}

View file

@ -97,7 +97,7 @@ export const verifyToken: RequestHandler = (req, res, next) => {
}
const { secret } = req.companion.options
const providerName = req.params['providerName']
if (providerName == null || providerName.length === 0) {
if (typeof providerName !== 'string' || providerName.length === 0) {
res.sendStatus(400)
return
}
@ -124,7 +124,9 @@ export const verifyToken: RequestHandler = (req, res, next) => {
const { providerOptions } = req.companion.options
const providerName = req.params['providerName']
const providerOption =
providerName != null ? providerOptions?.[providerName] : undefined
typeof providerName === 'string'
? providerOptions?.[providerName]
: undefined
const key = providerOption?.key
if (!key) {
logger.info(
@ -146,7 +148,7 @@ export const verifyToken: RequestHandler = (req, res, next) => {
// does not fail if token is invalid
export const gentleVerifyToken: RequestHandler = (req, res, next) => {
const providerName = req.params['providerName']
if (providerName == null || providerName.length === 0) {
if (typeof providerName !== 'string' || providerName.length === 0) {
next()
return
}

View file

@ -9,7 +9,9 @@ const port: string | number =
const { app, companionOptions } = standalone()
companion.socket(app.listen(port), companionOptions)
const server = app.listen(port, () => {
logger.info(`Welcome to Companion! v${packageJson.version}`)
logger.info(`Listening on http://localhost:${port}`)
companion.socket(server, companionOptions)
})

View file

@ -426,4 +426,26 @@ describe('S3 controller', () => {
),
)
})
test('getUploadParameters supports bracket metadata query format', async () => {
const server = await getServer({
COMPANION_AWS_KEY: 'test_key',
COMPANION_AWS_SECRET: 'test_secret',
COMPANION_AWS_BUCKET: 'test-bucket',
COMPANION_AWS_REGION: 'us-east-1',
})
return request(server)
.get('/s3/params')
.query({
filename: 'test.txt',
type: 'text/plain',
'metadata[name]': 'demo-file',
})
.expect(200)
.then((res) => {
const fields = res.body.fields as Record<string, string>
expect(fields['x-amz-meta-name']).toBe('demo-file')
})
})
})

View file

@ -19,7 +19,7 @@ async function run({
next()
})
app.use(cors(corsOptions))
app.options('*', (_req, res) => {
app.options('/{*splat}', (_req, res) => {
res.status(204).end()
})

View file

@ -101,7 +101,7 @@ export const getServerWithEmitter = async (
authServer.use(
session({ secret: 'grant', resave: true, saveUninitialized: true }),
)
authServer.all('*/callback', (req, res, next) => {
authServer.all('{*splat}/callback', (req, res, next) => {
if (req.session) {
req.session['grant'] = {
response: { access_token: grantToken },
@ -109,7 +109,9 @@ export const getServerWithEmitter = async (
}
next()
})
authServer.all(['*/send-token', '*/redirect'], (req, res, next) => {
authServer.all(
['{*splat}/send-token', '{*splat}/redirect'],
(req, res, next) => {
const state =
typeof req.query['state'] === 'string' ? req.query['state'] : null
if (req.session) {
@ -118,7 +120,8 @@ export const getServerWithEmitter = async (
}
}
next()
})
},
)
const { app, emitter } = standalone()
authServer.use(app)

2611
yarn.lock

File diff suppressed because it is too large Load diff