Source: AWSClientS3.js

const { S3Client, GetObjectCommand, PutObjectCommand, DeleteObjectCommand, ListObjectsV2Command, CreateBucketCommand, GetBucketVersioningCommand, DeleteBucketCommand, ListBucketsCommand } = require("@aws-sdk/client-s3");
const { getSignedUrl } = require("@aws-sdk/s3-request-presigner");
const AWSClientS3Error = require("./AWSClientS3Error");

/**
 * Transforms a ReadableStream into a Buffer
 * @param {ReadableStream} stream
 * @returns {Promise.<Buffer>} Promise that resolves to original file contents as a Buffer
 */

const streamToBuffer = (stream) => new Promise((resolve, reject) => {
	const chunks = [];
	stream.on("data", chunk => chunks.push(chunk));
	stream.on("error", err => reject(err));
	stream.on("end", () => resolve(Buffer.concat(chunks)));
});

/**
 * Transforms a ReadableStream into a string
 * @param {ReadableStream} stream 
 * @returns {Promise.<string>} Promise that resolves to original file contents as a UTF-8 encoded string
 */

const streamtoString = (stream) => new Promise((resolve, reject) => {
	const chunks = [];
	stream.on("data", chunk => chunks.push(chunk));
	stream.on("error", err => reject(err));
	stream.on("end", () => resolve(Buffer.concat(chunks).toString("utf-8")));
});

class AWSClientS3 extends S3Client {
	/**
	 * @constructs AWSClientS3
	 * @param {object} config - [AWSClientS3 configuration options]{@link https://docs.aws.amazon.com/AWSJavaScriptSDK/v3/latest/clients/client-s3/interfaces/s3clientconfig.html}
	 * @param {string} config.region - Destination AWS S3 bucket's region
	 * @param {object} config.credentials - AWS programmatic access credentials 
	 * @param {string} config.credentials.accessKeyId - AWS Access Key ID
	 * @param {string} config.credentials.secretAccessKey - AWS Secret Access Key
	 * @throws Will throw an Error if required arguments are missing
	 */
	constructor(config) {
		if (!config.region) throw new AWSClientS3Error({
			message: "The S3 client couldn't be configured properly",
			error: "Missing AWS S3 buckets region"
		});
		else if (!(config.credentials.accessKeyId && config.credentials.secretAccessKey)) throw new AWSClientS3Error({
			message: "The S3 client couldn't be configured properly",
			error: "Missing AWS Access Key ID and/or Secret Access Key"
		});
		super(config);
	}
	/**
	 * Returns pre-signed URL for specific S3 object
	 * @param {object} objectParams - Target object's location data (bucket and key)
	 * @param {string} objectParams.bucket - Target object's bucket
	 * @param {string} objectParams.key - Target object's key
	 * @param {object} [presignedUrlOptions] - Config options for resultant pre-signed URL
	 * @throws Will throw an Error if required arguments are missing
	 * @returns {Promise<string>} Promise which resolves to the pre-sgiend URL for the requested object
	 */
	async getPresignedUrl(objectParams, presignedUrlOptions=null) {
		if (!(objectParams.bucket && objectParams.key)) throw new AWSClientS3Error({
			message: "Couldn't generate pre-signed URL",
			error: "Missing object bucket and/or key"
		});
		const command = new GetObjectCommand({
			Bucket: objectParams.bucket,
			Key: objectParams.key
		});
		return await getSignedUrl(this, command, presignedUrlOptions);
	}
	/**
	 * Returns contents of specified S3 object
	 * @param {object} objectParams - Target object's location data (bucket and key)
	 * @param {string} objectParams.bucket - Target object's bucket
	 * @param {string} objectParams.key - Target object's key
	 * @param {string} [returnType=stream] - Return type for requested S3 object
	 * @throws Will throw an Error if required arguments are missing
	 * @returns {Promise<ReadableStream|Buffer|string>} Promise which resolves to the requested file's contents as the specified data type
	 */
	async readFile(objectParams, returnType="stream") {
		if (!(objectParams.bucket && objectParams.key)) throw new AWSClientS3Error({
			message: "Couldn't access S3 file to read",
			error: "Missing object bucket and/or key"
		});
		const command = new GetObjectCommand({
			Bucket: objectParams.bucket,
			Key: objectParams.key
		});
		const { Body: fileBodyStream } = await this.send(command);
		switch (returnType) {
		case "stream":
			return fileBodyStream;
		case "string":
			return streamtoString(fileBodyStream);
		case "buffer":
			return streamToBuffer(fileBodyStream);
		}
	}
	/**
	 * @async
	 * @description Uploads file object to specified S3 bucket and key destination
	 * @param {Buffer} file - File-to-upload as Buffer
	 * @param {object} objectParams - File-to-upload destination data (bucket and key)
	 * @param {string} objectParams.bucket - File-to-upload destination bucket
	 * @param {string} objectParams.key - File-to-upload destination key
	 * @throws Will throw an Error if required arguments are missing or not of expected type
	 * @returns {Promise.<PutObjectCommandOutput>}
	 */
	async uploadFile(file, objectParams) {
		if (!(objectParams.bucket && objectParams.key)) throw new AWSClientS3Error({
			message: "Couldn't upload file to S3",
			error: "Missing object destination bucket and/or key"
		});
		else if (!Buffer.isBuffer(file)) throw new AWSClientS3Error({
			message: "Couldn't upload file to S3",
			error: `Parameter 'file' must be of type Buffer. Received type ${typeof file}`
		});
		const command = new PutObjectCommand({
			Bucket: objectParams.bucket,
			Key: objectParams.key,
			Body: file
		});
		return await this.send(command);
	}
	/**
	 * @async
	 * @description Deletes specified file from S3 bucket at specified key
	 * @param {object} objectParams
	 * @param {string} objectParams.bucket - File-to-delete origin bucket
	 * @param {string} objectParams.key - File-to-delete origin key
	 * @throws Will throw an Error if required arguments are missing
	 * @returns {Promise.<DeleteObjectCommandOutput>}
	 */
	async deleteFile(objectParams) {
		if (!(objectParams.bucket && objectParams.key)) throw new AWSClientS3Error({
			message: "Couldn't delete file",
			error: "Missing object-to-delete origin bucket and/or key"
		});
		const command = new DeleteObjectCommand({
			Bucket: objectParams.bucket,
			Key: objectParams.key
		});
		return await this.send(command);
	}
	/**
	 * @async
	 * @description List objects of specified bucket with specified prefix (or all bucket objects if no prefix specified)
	 * @param {string} bucketName 
	 * @param {string} [prefix] 
	 * @throws Will throw an Error if required arguments are missing or not of expected type
	 * @returns {Promise.<ListObjectsV2CommandOutput>}
	 */
	async listBucketObjects(bucketName, prefix=null) {
		if (typeof bucketName !== "string") throw new TypeError("Parameter 'bucketName' must be of type string");
		else if (!bucketName.trim()) throw new RangeError("Parameter 'bucketName' cannot be an empty string");
		const command = new ListObjectsV2Command({
			Bucket: bucketName,
			Prefix: prefix
		});
		return await this.send(command);
	}
	/**
	 * @async
	 * @description Creates bucket with `bucketName` - If `bucketOptions` passed, these will override default options
	 * @param {string} bucketName - Name of new bucket
	 * @param {object} [bucketOptions] Bucket options as defined by the [AWS S3 SDK docs]{@link https://docs.aws.amazon.com/AWSJavaScriptSDK/v3/latest/clients/client-s3/interfaces/createbucketcommandinput.html}
	 * @throws Will throw an Error if required arguments are missing
	 * @returns {Promise.<CreateBucketCommandOutput>}
	 */
	async createBucket(bucketName, bucketOptions=null) {
		if (!bucketName) throw new AWSClientS3Error({
			message:" Couldn't create new bucket on S3",
			error: "Missing bucket-to-create name"
		});
		const command = new CreateBucketCommand({ 
			...bucketOptions,
			Bucket: bucketName
		});
		return await this.send(command);
	}
	/**
	 * @async
	 * @description Deletes specified bucket
	 * @param {string} bucketName - Name of bucket to delete
	 * @throws Will throw an Error if specified bucket is Versioning Enabled
	 * @returns {Promise.<DeleteBucketCommandOutput>}
	 */
	async deleteBucket(bucketName) {
		const { status: isBucketVersioningEnabled } = await this.send(new GetBucketVersioningCommand({
			Bucket: bucketName
		}));
		if (isBucketVersioningEnabled) throw new AWSClientS3({
			message: "Couldn't delete bucket",
			error: "Cannot programatically delete versioning-enabled bucket. Use the AWS Console or the AWS CLI for this operation"
		});
		const command = new DeleteBucketCommand({
			Bucket: bucketName
		});
		return await this.send(command);
	}
	/**
	 * Lists all buckets
	 * @returns {Promise.<ListBucketsCommandOutput>}
	 */
	async listBuckets() {
		const command = new ListBucketsCommand();
		return await this.send(command);
	}
};

module.exports = AWSClientS3;