Enable TS noImplicitAny in web (#31636)

GitOrigin-RevId: 18881694770f2476c475f8fef4c6a2678a2a12fe
This commit is contained in:
Anna Claire Fields
2026-03-26 13:53:59 +01:00
committed by Copybot
parent b3489a6792
commit 6113c6c291
58 changed files with 1423 additions and 281 deletions

View File

@@ -8,13 +8,20 @@ const READ_PREFERENCE_SECONDARY =
: ReadPreference.secondaryPreferred.mode
const ONE_MONTH_IN_MS = 1000 * 60 * 60 * 24 * 31
/** @type {ObjectId | null} */
let ID_EDGE_PAST
const ID_EDGE_FUTURE = objectIdFromMs(Date.now() + 1000)
/** @type {boolean} */
let BATCH_DESCENDING
/** @type {number} */
let BATCH_SIZE
/** @type {boolean} */
let VERBOSE_LOGGING
/** @type {ObjectId} */
let BATCH_RANGE_START
/** @type {ObjectId} */
let BATCH_RANGE_END
/** @type {number} */
let BATCH_MAX_TIME_SPAN_IN_MS
let BATCHED_UPDATE_RUNNING = false
@@ -55,7 +62,7 @@ function refreshGlobalOptionsForBatchedUpdate(options = {}) {
if (BATCH_DESCENDING) {
BATCH_RANGE_START = ID_EDGE_FUTURE
} else {
BATCH_RANGE_START = ID_EDGE_PAST
BATCH_RANGE_START = /** @type {ObjectId} */ (ID_EDGE_PAST)
}
}
BATCH_MAX_TIME_SPAN_IN_MS = parseInt(
@@ -66,7 +73,7 @@ function refreshGlobalOptionsForBatchedUpdate(options = {}) {
BATCH_RANGE_END = objectIdFromInput(options.BATCH_RANGE_END)
} else {
if (BATCH_DESCENDING) {
BATCH_RANGE_END = ID_EDGE_PAST
BATCH_RANGE_END = /** @type {ObjectId} */ (ID_EDGE_PAST)
} else {
BATCH_RANGE_END = ID_EDGE_FUTURE
}

View File

@@ -7,3 +7,4 @@ mongo-utils
--node-version=24.13.0
--pipeline-owner=32
--public-repo=False
--tsconfig-no-implicit-any=True

View File

@@ -1,8 +1,8 @@
// @ts-check
/**
* @import { MongoClient } from 'mongodb'
* @import { MongoClient as LegacyMongoClient } from 'mongodb-legacy'
* @typedef {import('mongodb').MongoClient} MongoClient
* @typedef {import('mongodb-legacy').MongoClient} LegacyMongoClient
*/
/**

View File

@@ -1,4 +1,7 @@
{
"extends": "../../tsconfig.backend.json",
"compilerOptions": {
"noImplicitAny": true
},
"include": ["**/*.js", "**/*.cjs", "**/*.ts"]
}

View File

@@ -7,3 +7,4 @@ object-persistor
--node-version=24.13.0
--pipeline-owner=32
--public-repo=False
--tsconfig-no-implicit-any=True

View File

@@ -21,7 +21,7 @@ const hkdf = promisify(Crypto.hkdf)
const AES256_KEY_LENGTH = 32
/**
* @typedef {Object} Settings
* @typedef {Object} EncryptionSettings
* @property {boolean} automaticallyRotateDEKEncryption
* @property {string} dataEncryptionKeyBucketName
* @property {boolean} ignoreErrorsFromDEKReEncryption
@@ -29,6 +29,26 @@ const AES256_KEY_LENGTH = 32
* @property {() => Promise<Array<RootKeyEncryptionKey>>} getRootKeyEncryptionKeys
*/
/**
* @typedef {Object} S3PersistorSettings
* @property {Object<string, import('@aws-sdk/client-s3').StorageClass>} [storageClass]
* @property {string} [key]
* @property {string} [secret]
* @property {string} [region]
* @property {string} [endpoint]
* @property {boolean} [pathStyle]
* @property {number} [maxRetries]
* @property {Object} [httpOptions]
* @property {string} [ca]
* @property {number} [signedUrlExpiryInMs]
* @property {number} [partSize]
* @property {Object<string, {auth_key: string, auth_secret: string}>} [bucketCreds]
*/
/**
* @typedef {S3PersistorSettings & EncryptionSettings} Settings
*/
/**
* @typedef {import('./types').ListDirectoryResult} ListDirectoryResult
*/
@@ -105,10 +125,6 @@ class PerProjectEncryptedS3Persistor extends S3Persistor {
})
}
async ensureKeyEncryptionKeysLoaded() {
await this.#availableKeyEncryptionKeysPromise
}
/**
* @param {string} bucketName
* @param {string} path
@@ -289,6 +305,13 @@ class PerProjectEncryptedS3Persistor extends S3Persistor {
}
}
/**
* @param {string} bucketName
* @param {string} path
* @param {NodeJS.ReadableStream} sourceStream
* @param {import('./S3Persistor.js').StreamOptions} opts
* @return {Promise<void>}
*/
async sendStream(bucketName, path, sourceStream, opts = {}) {
const ssecOptions =
opts.ssecOptions ||
@@ -299,6 +322,13 @@ class PerProjectEncryptedS3Persistor extends S3Persistor {
})
}
/**
* @param {string} bucketName
* @param {string} path
* @param {Object} opts
* @param {SSECOptions} [opts.ssecOptions]
* @return {Promise<NodeJS.ReadableStream>}
*/
async getObjectStream(bucketName, path, opts = {}) {
const ssecOptions =
opts.ssecOptions ||
@@ -309,6 +339,13 @@ class PerProjectEncryptedS3Persistor extends S3Persistor {
})
}
/**
* @param {string} bucketName
* @param {string} path
* @param {Object} opts
* @param {SSECOptions} [opts.ssecOptions]
* @return {Promise<number>}
*/
async getObjectSize(bucketName, path, opts = {}) {
const ssecOptions =
opts.ssecOptions ||
@@ -316,6 +353,13 @@ class PerProjectEncryptedS3Persistor extends S3Persistor {
return await super.getObjectSize(bucketName, path, { ...opts, ssecOptions })
}
/**
* @param {string} bucketName
* @param {string} path
* @param {Object} opts
* @param {SSECOptions} [opts.ssecOptions]
* @return {Promise<string | undefined>}
*/
async getObjectStorageClass(bucketName, path, opts = {}) {
const ssecOptions =
opts.ssecOptions ||
@@ -326,11 +370,23 @@ class PerProjectEncryptedS3Persistor extends S3Persistor {
})
}
/**
* @param {string} bucketName
* @param {string} path
* @param {string} [continuationToken]
* @return {Promise<number>}
*/
async directorySize(bucketName, path, continuationToken) {
// Note: Listing a bucket does not require SSE-C credentials.
return await super.directorySize(bucketName, path, continuationToken)
}
/**
* @param {string} bucketName
* @param {string} path
* @param {string} [continuationToken]
* @return {Promise<void>}
*/
async deleteDirectory(bucketName, path, continuationToken) {
// Let [Settings.pathToProjectFolder] validate the project path before deleting things.
const { projectFolder, dekPath } = this.#buildProjectPaths(bucketName, path)
@@ -344,12 +400,27 @@ class PerProjectEncryptedS3Persistor extends S3Persistor {
}
}
/**
* @param {string} bucketName
* @param {string} path
* @param {Object} opts
* @return {Promise<string>}
*/
async getObjectMd5Hash(bucketName, path, opts = {}) {
// The ETag in object metadata is not the MD5 content hash, skip the HEAD request.
opts = { ...opts, etagIsNotMD5: true }
return await super.getObjectMd5Hash(bucketName, path, opts)
}
/**
* @param {string} bucketName
* @param {string} sourcePath
* @param {string} destinationPath
* @param {Object} opts
* @param {SSECOptions} [opts.ssecOptions]
* @param {SSECOptions} [opts.ssecSrcOptions]
* @return {Promise<void>}
*/
async copyObject(bucketName, sourcePath, destinationPath, opts = {}) {
const ssecOptions =
opts.ssecOptions ||

View File

@@ -77,14 +77,53 @@ class SSECOptions {
}
}
/**
* @typedef {import('@aws-sdk/client-s3').StorageClass} StorageClass
* @typedef {import('@aws-sdk/client-s3').PutObjectCommandInput} PutObjectCommandInput
* @typedef {import('@aws-sdk/client-s3').HeadObjectCommandOutput} HeadObjectCommandOutput
* @typedef {import('@aws-sdk/client-s3').GetObjectCommandOutput} GetObjectCommandOutput
* @typedef {import('@aws-sdk/client-s3').S3ClientConfig} S3ClientConfig
*/
/**
* @typedef {Object} StreamOptions
* @property {string} [contentType]
* @property {string} [contentEncoding]
* @property {number} [contentLength]
* @property {'*'} [ifNoneMatch]
* @property {SSECOptions} [ssecOptions]
*/
/**
* @typedef {Object} S3PersistorSettings
* @property {Object<string, StorageClass>} [storageClass]
* @property {string} [key]
* @property {string} [secret]
* @property {string} [region]
* @property {string} [endpoint]
* @property {boolean} [pathStyle]
* @property {number} [maxRetries]
* @property {Object} [httpOptions]
* @property {string} [ca]
* @property {number} [signedUrlExpiryInMs]
* @property {number} [partSize]
* @property {Object<string, {auth_key: string, auth_secret: string}>} [bucketCreds]
*/
class S3Persistor extends AbstractPersistor {
/** @type {Map<string, S3Client>} */
#clients = new Map()
/** @type {S3PersistorSettings} */
settings
/**
* @param {S3PersistorSettings} [settings]
*/
constructor(settings = {}) {
super()
settings.storageClass = settings.storageClass || {}
settings.storageClass = /** @type {Object<string, StorageClass>} */ (
settings.storageClass || {}
)
this.settings = settings
}
@@ -102,12 +141,7 @@ class S3Persistor extends AbstractPersistor {
* @param {string} bucketName
* @param {string} key
* @param {NodeJS.ReadableStream} readStream
* @param {Object} opts
* @param {string} [opts.contentType]
* @param {string} [opts.contentEncoding]
* @param {number} [opts.contentLength]
* @param {'*'} [opts.ifNoneMatch]
* @param {SSECOptions} [opts.ssecOptions]
* @param {StreamOptions} opts
* @return {Promise<void>}
*/
async sendStream(bucketName, key, readStream, opts = {}) {
@@ -128,8 +162,11 @@ class S3Persistor extends AbstractPersistor {
Body: observer,
}
if (this.settings.storageClass[bucketName]) {
uploadOptions.StorageClass = this.settings.storageClass[bucketName]
const storageClass = /** @type {Object<string, StorageClass>} */ (
this.settings.storageClass
)
if (storageClass[bucketName]) {
uploadOptions.StorageClass = storageClass[bucketName]
}
if ('sourceMd5' in opts) {
@@ -239,7 +276,9 @@ class S3Persistor extends AbstractPersistor {
* @return {Promise<string>}
*/
async getRedirectUrl(bucketName, key) {
const expiresSeconds = Math.round(this.settings.signedUrlExpiryInMs / 1000)
const expiresSeconds = Math.round(
/** @type {number} */ (this.settings.signedUrlExpiryInMs) / 1000
)
try {
return await getSignedUrl(
this._getClientForBucket(bucketName),
@@ -311,6 +350,7 @@ class S3Persistor extends AbstractPersistor {
*/
async #listDirectory(bucketName, key, continuationToken) {
let response
/** @type {{ Bucket: string, Prefix: string, ContinuationToken?: string }} */
const options = { Bucket: bucketName, Prefix: key }
if (continuationToken) {
options.ContinuationToken = continuationToken
@@ -501,6 +541,7 @@ class S3Persistor extends AbstractPersistor {
*/
async directorySize(bucketName, key, continuationToken) {
try {
/** @type {{ Bucket: string, Prefix: string, ContinuationToken?: string }} */
const options = {
Bucket: bucketName,
Prefix: key,
@@ -621,7 +662,7 @@ class S3Persistor extends AbstractPersistor {
}
/**
* @param {Object} bucketCredentials
* @param {{auth_key: string, auth_secret: string} | undefined} bucketCredentials
* @param {import('@aws-sdk/client-s3').S3ClientConfig} clientOptions
* @return {import('@aws-sdk/client-s3').S3ClientConfig}
* @private
@@ -637,7 +678,7 @@ class S3Persistor extends AbstractPersistor {
} else if (this.settings.key) {
options.credentials = {
accessKeyId: this.settings.key,
secretAccessKey: this.settings.secret,
secretAccessKey: /** @type {string} */ (this.settings.secret),
}
} else {
// Use the default credentials provider (process.env -> SSP -> ini -> IAM)
@@ -709,6 +750,9 @@ class S3Persistor extends AbstractPersistor {
}
// test-only
/**
* @param {string} bucketName
*/
_createBucket(bucketName) {
return this._getClientForBucket(bucketName).send(
new CreateBucketCommand({ Bucket: bucketName })
@@ -716,6 +760,10 @@ class S3Persistor extends AbstractPersistor {
}
// test-only
/**
* @param {string} bucketName
* @param {import('@aws-sdk/client-s3').PutObjectCommandInput} uploadOptions
*/
_upload(bucketName, uploadOptions) {
return this._getClientForBucket(bucketName).send(
new PutObjectCommand(uploadOptions)

View File

@@ -1,4 +1,7 @@
{
"extends": "../../tsconfig.backend.json",
"compilerOptions": {
"noImplicitAny": true
},
"include": ["**/*.js", "**/*.cjs", "**/*.ts"]
}

View File

@@ -4,6 +4,7 @@ const crypto = require('node:crypto')
const os = require('node:os')
const { promisify } = require('node:util')
// @ts-ignore - ioredis types may not be available in all contexts
const Redis = require('ioredis')
const {
@@ -20,6 +21,9 @@ const PID = process.pid
const RND = crypto.randomBytes(4).toString('hex')
let COUNT = 0
/**
* @param {any} opts
*/
function createClient(opts) {
const standardOpts = Object.assign({}, opts)
delete standardOpts.key_schema
@@ -42,6 +46,7 @@ function createClient(opts) {
client = new Redis(standardOpts)
}
monkeyPatchIoRedisExec(client)
/** @param {any} callback */
client.healthCheck = callback => {
if (callback) {
// callback based invocation
@@ -54,6 +59,9 @@ function createClient(opts) {
return client
}
/**
* @param {any} client
*/
async function healthCheck(client) {
// check the redis connection by storing and retrieving a unique key/value pair
const uniqueToken = `host=${HOST}:pid=${PID}:random=${RND}:time=${Date.now()}:count=${COUNT++}`
@@ -71,6 +79,11 @@ async function healthCheck(client) {
})
}
/**
* @param {any} client
* @param {any} uniqueToken
* @param {any} context
*/
async function runCheck(client, uniqueToken, context) {
const healthCheckKey = `_redis-wrapper:healthCheckKey:{${uniqueToken}}`
const healthCheckValue = `_redis-wrapper:healthCheckValue:{${uniqueToken}}`
@@ -79,9 +92,11 @@ async function runCheck(client, uniqueToken, context) {
context.stage = 'write'
const writeAck = await client
.set(healthCheckKey, healthCheckValue, 'EX', 60)
.catch(err => {
throw new RedisHealthCheckWriteError('write errored', context, err)
})
.catch(
/** @param {any} err */ err => {
throw new RedisHealthCheckWriteError('write errored', context, err)
}
)
if (writeAck !== 'OK') {
context.writeAck = writeAck
throw new RedisHealthCheckWriteError('write failed', context)
@@ -94,9 +109,15 @@ async function runCheck(client, uniqueToken, context) {
.get(healthCheckKey)
.del(healthCheckKey)
.exec()
.catch(err => {
throw new RedisHealthCheckVerifyError('read/delete errored', context, err)
})
.catch(
/** @param {any} err */ err => {
throw new RedisHealthCheckVerifyError(
'read/delete errored',
context,
err
)
}
)
if (roundTrippedHealthCheckValue !== healthCheckValue) {
context.roundTrippedHealthCheckValue = roundTrippedHealthCheckValue
throw new RedisHealthCheckVerifyError('read failed', context)
@@ -107,6 +128,10 @@ async function runCheck(client, uniqueToken, context) {
}
}
/**
* @param {any} result
* @param {any} callback
*/
function unwrapMultiResult(result, callback) {
// ioredis exec returns a results like:
// [ [null, 42], [null, "foo"] ]
@@ -130,19 +155,30 @@ function unwrapMultiResult(result, callback) {
}
const unwrapMultiResultPromisified = promisify(unwrapMultiResult)
/**
* @param {any} client
*/
function monkeyPatchIoRedisExec(client) {
const _multi = client.multi
client.multi = function () {
const multi = _multi.apply(client, arguments)
const _exec = multi.exec
/** @param {any} callback */
multi.exec = callback => {
if (callback) {
// callback based invocation
_exec.call(multi, (error, result) => {
// The command can fail all-together due to syntax errors
if (error) return callback(error)
unwrapMultiResult(result, callback)
})
_exec.call(
multi,
/**
* @param {any} error
* @param {any} result
*/
(error, result) => {
// The command can fail all-together due to syntax errors
if (error) return callback(error)
unwrapMultiResult(result, callback)
}
)
} else {
// Promise based invocation
return _exec.call(multi).then(unwrapMultiResultPromisified)
@@ -152,7 +188,11 @@ function monkeyPatchIoRedisExec(client) {
}
}
/**
* @param {{runner: any, timeout: any, context: any}} param0
*/
async function runWithTimeout({ runner, timeout, context }) {
/** @type {any} */
let healthCheckDeadline
await Promise.race([
new Promise((resolve, reject) => {

View File

@@ -45,7 +45,7 @@ if (!settingsExist) {
console.warn("No settings or defaults found. I'm flying blind.")
}
module.exports = settings
module.exports = /** @type {any} */ (settings)
function pathIfExists(path) {
if (path && fs.existsSync(path)) {