diff --git a/packages/providers/upload-aws-s3/src/__tests__/utils.test.ts b/packages/providers/upload-aws-s3/src/__tests__/utils.test.ts index cf48d5b3bd..05c03432ba 100644 --- a/packages/providers/upload-aws-s3/src/__tests__/utils.test.ts +++ b/packages/providers/upload-aws-s3/src/__tests__/utils.test.ts @@ -16,36 +16,6 @@ const defaultOptions = { describe('Utils', () => { describe('Extract credentials for V4 different aws provider configurations', () => { - test('[Legacy] Credentials directly in the options', () => { - const options: InitOptions = { - accessKeyId, - secretAccessKey, - ...defaultOptions, - }; - const credentials = extractCredentials(options); - - expect(credentials).toEqual({ - accessKeyId, - secretAccessKey, - }); - }); - - test('[Legacy] credentials directly in s3Options', () => { - const options: InitOptions = { - s3Options: { - accessKeyId, - secretAccessKey, - ...defaultOptions, - }, - }; - const credentials = extractCredentials(options); - - expect(credentials).toEqual({ - accessKeyId, - secretAccessKey, - }); - }); - test('Credentials in credentials object inside s3Options', () => { const options: InitOptions = { s3Options: { diff --git a/packages/providers/upload-aws-s3/src/utils.ts b/packages/providers/upload-aws-s3/src/utils.ts index 656971db8a..303efc9b54 100644 --- a/packages/providers/upload-aws-s3/src/utils.ts +++ b/packages/providers/upload-aws-s3/src/utils.ts @@ -88,26 +88,7 @@ function getBucketFromAwsUrl(fileUrl: string): BucketInfo { return { bucket: prefix.substring(0, prefix.length - 1) }; } -// TODO Remove this in V5 since we will only support the new config structure export const extractCredentials = (options: InitOptions): AwsCredentialIdentity | null => { - // legacy - if (options.accessKeyId && options.secretAccessKey) { - return { - accessKeyId: options.accessKeyId, - secretAccessKey: options.secretAccessKey, - }; - } - // Legacy - if (options.s3Options?.accessKeyId && options.s3Options.secretAccessKey) { - process.emitWarning( - 'Credentials passed directly to s3Options is deprecated and will be removed in a future release. Please wrap them inside a credentials object.' - ); - return { - accessKeyId: options.s3Options.accessKeyId, - secretAccessKey: options.s3Options.secretAccessKey, - }; - } - // V5 if (options.s3Options?.credentials) { return { accessKeyId: options.s3Options.credentials.accessKeyId, diff --git a/packages/utils/upgrade/resources/codemods/5.0.0/s3-keys-wrapped-in-credentials.code.ts b/packages/utils/upgrade/resources/codemods/5.0.0/s3-keys-wrapped-in-credentials.code.ts new file mode 100644 index 0000000000..a0826687a2 --- /dev/null +++ b/packages/utils/upgrade/resources/codemods/5.0.0/s3-keys-wrapped-in-credentials.code.ts @@ -0,0 +1,169 @@ +import core, { + Transform, + ASTPath, + Property, + ArrowFunctionExpression, + ObjectExpression, +} from 'jscodeshift'; +import path from 'node:path'; + +const findUploadPropertyInBody = (j: core.JSCodeshift, body: ObjectExpression) => { + return body.properties.find( + (prop: any) => + j.Property.check(prop) && j.Identifier.check(prop.key) && prop.key.name === 'upload' + ); +}; + +const findConfigInUpload = (j: core.JSCodeshift, upload: ObjectExpression) => { + return upload.properties.find( + (prop) => j.Property.check(prop) && j.Identifier.check(prop.key) && prop.key.name === 'config' + ); +}; + +const getProviderProperty = (j: core.JSCodeshift, config: ObjectExpression) => { + return config.properties.find( + (prop) => + j.Property.check(prop) && + j.Identifier.check(prop.key) && + prop.key.name === 'provider' && + j.Literal.check(prop.value) && + prop.value.value === 'aws-s3' + ); +}; + +const getProviderOptions = (j: core.JSCodeshift, config: ObjectExpression) => { + return config.properties.find( + (prop) => + j.Property.check(prop) && j.Identifier.check(prop.key) && prop.key.name === 'providerOptions' + ); +}; + +const getPropertyByKeyName = (j: core.JSCodeshift, object: ObjectExpression, keyName: string) => { + return object.properties.find( + (prop) => j.Property.check(prop) && j.Identifier.check(prop.key) && prop.key.name === keyName + ) as Property; +}; + +/** + * This codemod only affects users that are using the `aws-s3` provider. + * It will wrap the `accessKeyId` and `secretAccessKey` properties inside a `credentials` object. + */ +const transform: Transform = (file, api) => { + // Check if the current file is 'config/plugins.js' + const cwd = process.cwd(); + const jsPluginsPath = path.join(cwd, 'config/plugins.js'); + const tsPluginsPath = path.join(cwd, 'config/plugins.ts'); + + if (file.path !== jsPluginsPath && file.path !== tsPluginsPath) { + return file.source; + } + + const { j } = api; + const root = j(file.source); + + root + .find(j.ArrowFunctionExpression) + .forEach((arrowFunctionPath: ASTPath) => { + const body = arrowFunctionPath.node.body; + + // Check that the body of the arrow function is an object + if (!j.ObjectExpression.check(body)) { + return file.source; + } + + const uploadProperty = findUploadPropertyInBody(j, body); + + // Check that we found an upload property and that it is an object + if (!j.Property.check(uploadProperty) || !j.ObjectExpression.check(uploadProperty.value)) { + return file.source; + } + + const configProperty = findConfigInUpload(j, uploadProperty.value); + + if (!j.Property.check(configProperty) || !j.ObjectExpression.check(configProperty.value)) { + return file.source; + } + + const providerProperty = getProviderProperty(j, configProperty.value); + + // If there is not a provider property or it is not 'aws-s3', return the source + if (!providerProperty) { + return file.source; + } + + const providerOptions = getProviderOptions(j, configProperty.value); + + if (!j.Property.check(providerOptions) || !j.ObjectExpression.check(providerOptions.value)) { + return file.source; + } + + let accessKeyId: Property | undefined; + let secretAccessKey: Property | undefined; + + // Check for accessKeyId and secretAccessKey directly under providerOptions + const directAccessKeyId = getPropertyByKeyName(j, providerOptions.value, 'accessKeyId'); + + const directSecretAccessKey = getPropertyByKeyName( + j, + providerOptions.value, + 'secretAccessKey' + ); + + let s3Options = getPropertyByKeyName(j, providerOptions.value, 's3Options'); + + if (!s3Options) { + // Create s3Options if it doesn't exist + s3Options = j.property('init', j.identifier('s3Options'), j.objectExpression([])); + providerOptions.value.properties.push(s3Options); + } + + if (directAccessKeyId && directSecretAccessKey) { + accessKeyId = directAccessKeyId; + secretAccessKey = directSecretAccessKey; + + // Remove these properties from providerOptions + providerOptions.value.properties = providerOptions.value.properties.filter( + (prop) => + j.Property.check(prop) && + j.Identifier.check(prop.key) && + prop.key.name !== 'accessKeyId' && + prop.key.name !== 'secretAccessKey' + ); + } else if (j.Property.check(s3Options) && j.ObjectExpression.check(s3Options.value)) { + // Look inside s3Options + accessKeyId = getPropertyByKeyName(j, s3Options.value, 'accessKeyId'); + secretAccessKey = getPropertyByKeyName(j, s3Options.value, 'secretAccessKey'); + } + + if ( + accessKeyId && + secretAccessKey && + j.Property.check(s3Options) && + j.ObjectExpression.check(s3Options.value) + ) { + // Create the credentials object + const credentials = j.objectExpression([ + j.property('init', j.identifier('accessKeyId'), accessKeyId.value), + j.property('init', j.identifier('secretAccessKey'), secretAccessKey.value), + ]); + + // Remove the old properties from s3Options + s3Options.value.properties = s3Options.value.properties.filter( + (prop) => + j.Property.check(prop) && + j.Identifier.check(prop.key) && + prop.key.name !== 'accessKeyId' && + prop.key.name !== 'secretAccessKey' + ); + + // Add the new credentials object to s3Options + s3Options.value.properties.push( + j.property('init', j.identifier('credentials'), credentials) + ); + } + }); + + return root.toSource(); +}; + +export default transform;