From 8b6464180a82bcc41197f2a97e58b9555a7072cd Mon Sep 17 00:00:00 2001 From: Derek Wene Date: Sun, 23 Apr 2023 05:31:16 -0500 Subject: [PATCH] feat(medusa-file-s3,medusa-file-minio): Upgrade to TypeScript (#3740) --- .changeset/chilly-carpets-explode.md | 6 + packages/medusa-file-minio/package.json | 27 +-- .../medusa-file-minio/src/services/minio.js | 182 --------------- .../medusa-file-minio/src/services/minio.ts | 212 +++++++++++++++++ packages/medusa-file-minio/tsconfig.json | 30 +++ packages/medusa-file-s3/README.md | 57 ++--- packages/medusa-file-s3/package.json | 24 +- packages/medusa-file-s3/src/services/s3.js | 139 ----------- packages/medusa-file-s3/src/services/s3.ts | 157 +++++++++++++ packages/medusa-file-s3/tsconfig.json | 30 +++ yarn.lock | 217 ++++++++++++++---- 11 files changed, 661 insertions(+), 420 deletions(-) create mode 100644 .changeset/chilly-carpets-explode.md delete mode 100644 packages/medusa-file-minio/src/services/minio.js create mode 100644 packages/medusa-file-minio/src/services/minio.ts create mode 100644 packages/medusa-file-minio/tsconfig.json delete mode 100644 packages/medusa-file-s3/src/services/s3.js create mode 100644 packages/medusa-file-s3/src/services/s3.ts create mode 100644 packages/medusa-file-s3/tsconfig.json diff --git a/.changeset/chilly-carpets-explode.md b/.changeset/chilly-carpets-explode.md new file mode 100644 index 0000000000..51eecffec0 --- /dev/null +++ b/.changeset/chilly-carpets-explode.md @@ -0,0 +1,6 @@ +--- +"medusa-file-minio": minor +"medusa-file-s3": minor +--- + +Migrate medusa-file-minio and medusa-file-s3 to typescript. diff --git a/packages/medusa-file-minio/package.json b/packages/medusa-file-minio/package.json index 82e70ed13a..28ad2fc0d0 100644 --- a/packages/medusa-file-minio/package.json +++ b/packages/medusa-file-minio/package.json @@ -2,7 +2,10 @@ "name": "medusa-file-minio", "version": "1.1.6", "description": "MinIO server file connector for Medusa", - "main": "index.js", + "main": "dist/index.js", + "files": [ + "dist" + ], "repository": { "type": "git", "url": "https://github.com/medusajs/medusa", @@ -11,32 +14,22 @@ "author": "Edin Skeja", "license": "MIT", "devDependencies": { - "@babel/cli": "^7.16.0", - "@babel/core": "^7.16.0", - "@babel/node": "^7.16.0", - "@babel/plugin-proposal-class-properties": "^7.16.0", - "@babel/plugin-transform-instanceof": "^7.16.0", - "@babel/plugin-transform-runtime": "^7.16.4", - "@babel/preset-env": "^7.16.4", - "@babel/register": "^7.16.0", - "@babel/runtime": "^7.16.3", - "client-sessions": "^0.8.0", + "@medusajs/medusa": "1.8.0-rc.6", "cross-env": "^5.2.1", "jest": "^25.5.4", - "medusa-interfaces": "^1.3.7" + "typescript": "^4.9.5" }, "scripts": { - "build": "babel src --out-dir dist/ --ignore '**/__tests__','**/__mocks__'", "prepare": "cross-env NODE_ENV=production yarn run build", - "watch": "babel -w src --out-dir dist/ --ignore '**/__tests__','**/__mocks__'", - "test": "jest --passWithNoTests src" + "test": "jest --passWithNoTests src", + "build": "tsc", + "watch": "tsc --watch" }, "peerDependencies": { "medusa-interfaces": "1.3.7" }, "dependencies": { - "@babel/plugin-transform-classes": "^7.16.0", - "aws-sdk": "^2.1043.0", + "aws-sdk": "^2.983.0", "body-parser": "^1.19.0", "express": "^4.17.1", "medusa-core-utils": "^1.2.0", diff --git a/packages/medusa-file-minio/src/services/minio.js b/packages/medusa-file-minio/src/services/minio.js deleted file mode 100644 index 0963ee454a..0000000000 --- a/packages/medusa-file-minio/src/services/minio.js +++ /dev/null @@ -1,182 +0,0 @@ -import stream from "stream" -import aws from "aws-sdk" -import { parse } from "path" -import fs from "fs" -import { AbstractFileService } from "@medusajs/medusa" -import { MedusaError } from "medusa-core-utils" - -class MinioService extends AbstractFileService { - constructor({}, options) { - super({}, options) - - this.bucket_ = options.bucket - this.accessKeyId_ = options.access_key_id - this.secretAccessKey_ = options.secret_access_key - this.private_bucket_ = options.private_bucket - this.private_access_key_id_ = - options.private_access_key_id ?? this.accessKeyId_ - this.private_secret_access_key_ = - options.private_secret_access_key ?? this.secretAccessKey_ - this.endpoint_ = options.endpoint - this.s3ForcePathStyle_ = true - this.signatureVersion_ = "v4" - this.downloadUrlDuration = options.download_url_duration ?? 60 // 60 seconds - } - - upload(file) { - this.updateAwsConfig_() - - return this.uploadFile(file) - } - - uploadProtected(file) { - this.validatePrivateBucketConfiguration_(true) - this.updateAwsConfig_(true) - - return this.uploadFile(file, { isProtected: true }) - } - - uploadFile(file, options = { isProtected: false }) { - const parsedFilename = parse(file.originalname) - const fileKey = `${parsedFilename.name}-${Date.now()}${parsedFilename.ext}` - - const s3 = new aws.S3() - const params = { - ACL: options.isProtected ? "private" : "public-read", - Bucket: options.isProtected ? this.private_bucket_ : this.bucket_, - Body: fs.createReadStream(file.path), - Key: fileKey, - } - - return new Promise((resolve, reject) => { - s3.upload(params, (err, data) => { - if (err) { - reject(err) - return - } - - resolve({ url: data.Location, key: data.Key }) - }) - }) - } - - async delete(file) { - this.updateAwsConfig_() - - const s3 = new aws.S3() - const params = { - Bucket: this.bucket_, - Key: `${file.fileKey}`, - } - - return await Promise.all([ - new Promise((resolve, reject) => - s3.deleteObject({ ...params, Bucket: this.bucket_ }, (err, data) => { - if (err) { - reject(err) - return - } - resolve(data) - }) - ), - new Promise((resolve, reject) => - s3.deleteObject( - { ...params, Bucket: this.private_bucket_ }, - (err, data) => { - if (err) { - reject(err) - return - } - resolve(data) - } - ) - ), - ]) - } - - async getUploadStreamDescriptor({ usePrivateBucket = true, ...fileData }) { - this.validatePrivateBucketConfiguration_(usePrivateBucket) - this.updateAwsConfig_(usePrivateBucket) - - const pass = new stream.PassThrough() - - const fileKey = `${fileData.name}.${fileData.ext}` - const params = { - Bucket: usePrivateBucket ? this.private_bucket_ : this.bucket_, - Body: pass, - Key: fileKey, - } - - const s3 = new aws.S3() - return { - writeStream: pass, - promise: s3.upload(params).promise(), - url: `${this.spacesUrl_}/${fileKey}`, - fileKey, - } - } - - async getDownloadStream({ usePrivateBucket = true, ...fileData }) { - this.validatePrivateBucketConfiguration_(usePrivateBucket) - this.updateAwsConfig_(usePrivateBucket) - - const s3 = new aws.S3() - - const params = { - Bucket: usePrivateBucket ? this.private_bucket_ : this.bucket_, - Key: `${fileData.fileKey}`, - } - - return s3.getObject(params).createReadStream() - } - - async getPresignedDownloadUrl({ usePrivateBucket = true, ...fileData }) { - this.validatePrivateBucketConfiguration_(usePrivateBucket) - this.updateAwsConfig_(usePrivateBucket, { - signatureVersion: "v4", - }) - - const s3 = new aws.S3() - - const params = { - Bucket: usePrivateBucket ? this.private_bucket_ : this.bucket_, - Key: `${fileData.fileKey}`, - Expires: this.downloadUrlDuration, - } - - return await s3.getSignedUrlPromise("getObject", params) - } - - validatePrivateBucketConfiguration_(usePrivateBucket) { - if ( - usePrivateBucket && - (!this.private_access_key_id_ || !this.private_bucket_) - ) { - throw new MedusaError( - MedusaError.Types.UNEXPECTED_STATE, - "Private bucket is not configured" - ) - } - } - - updateAwsConfig_(usePrivateBucket = false, additionalConfiguration = {}) { - aws.config.setPromisesDependency(null) - aws.config.update( - { - accessKeyId: usePrivateBucket - ? this.private_access_key_id_ - : this.accessKeyId_, - secretAccessKey: usePrivateBucket - ? this.private_secret_access_key_ - : this.secretAccessKey_, - endpoint: this.endpoint_, - s3ForcePathStyle: this.s3ForcePathStyle_, - signatureVersion: this.signatureVersion_, - ...additionalConfiguration, - }, - true - ) - } -} - -export default MinioService diff --git a/packages/medusa-file-minio/src/services/minio.ts b/packages/medusa-file-minio/src/services/minio.ts new file mode 100644 index 0000000000..3339383944 --- /dev/null +++ b/packages/medusa-file-minio/src/services/minio.ts @@ -0,0 +1,212 @@ +import stream from "stream" +import aws from "aws-sdk" +import { parse } from "path" +import fs from "fs" +import { + AbstractFileService, + DeleteFileType, + FileServiceUploadResult, + GetUploadedFileType, + IFileService, + UploadStreamDescriptorType, +} from "@medusajs/medusa" +import { MedusaError } from "medusa-core-utils" +import { ClientConfiguration, PutObjectRequest } from "aws-sdk/clients/s3" + +class MinioService extends AbstractFileService implements IFileService { + protected bucket_: string + protected accessKeyId_: string + protected secretAccessKey_: string + protected private_bucket_: string + protected private_access_key_id_: string + protected private_secret_access_key_: string + protected endpoint_: string + protected s3ForcePathStyle_: boolean + protected signatureVersion_: string + protected downloadUrlDuration: string | number + + constructor({}, options) { + super({}, options) + + this.bucket_ = options.bucket + this.accessKeyId_ = options.access_key_id + this.secretAccessKey_ = options.secret_access_key + this.private_bucket_ = options.private_bucket + this.private_access_key_id_ = + options.private_access_key_id ?? this.accessKeyId_ + this.private_secret_access_key_ = + options.private_secret_access_key ?? this.secretAccessKey_ + this.endpoint_ = options.endpoint + this.s3ForcePathStyle_ = true + this.signatureVersion_ = "v4" + this.downloadUrlDuration = options.download_url_duration ?? 60 // 60 seconds + } + + protected buildUrl(bucket: string, key: string) { + return `${this.endpoint_}/${bucket}/${key}` + } + + async upload(file: Express.Multer.File): Promise { + return await this.uploadFile(file) + } + + async uploadProtected( + file: Express.Multer.File + ): Promise { + this.validatePrivateBucketConfiguration_(true) + + return await this.uploadFile(file, { isProtected: true }) + } + + protected async uploadFile( + file: Express.Multer.File, + options: { isProtected: boolean } = { isProtected: false } + ) { + const parsedFilename = parse(file.originalname) + const fileKey = `${parsedFilename.name}-${Date.now()}${parsedFilename.ext}` + + const client = this.getClient(options.isProtected) + + const params = { + ACL: options.isProtected ? "private" : "public-read", + Bucket: options.isProtected ? this.private_bucket_ : this.bucket_, + Body: fs.createReadStream(file.path), + Key: fileKey, + ContentType: file.mimetype, + } + + const result = await client.upload(params).promise() + + return { url: result.Location, key: result.Key } + } + + async delete(file: DeleteFileType): Promise { + const privateClient = this.getClient(false) + const publicClient = this.getClient(true) + + const params = { + Bucket: this.bucket_, + Key: `${file.fileKey}`, + } + + await Promise.all([ + new Promise((resolve, reject) => + publicClient.deleteObject( + { ...params, Bucket: this.bucket_ }, + (err, data) => { + if (err) { + reject(err) + return + } + resolve(data) + } + ) + ), + new Promise((resolve, reject) => + privateClient.deleteObject( + { ...params, Bucket: this.private_bucket_ }, + (err, data) => { + if (err) { + reject(err) + return + } + resolve(data) + } + ) + ), + ]) + } + + async getUploadStreamDescriptor( + fileData: UploadStreamDescriptorType & { + usePrivateBucket?: boolean + contentType?: string + } + ) { + const usePrivateBucket = !!fileData.usePrivateBucket + + this.validatePrivateBucketConfiguration_(usePrivateBucket) + + const client = this.getClient(usePrivateBucket) + + const pass = new stream.PassThrough() + + const fileKey = `${fileData.name}.${fileData.ext}` + + const params: PutObjectRequest = { + Bucket: usePrivateBucket ? this.private_bucket_ : this.bucket_, + Body: pass, + Key: fileKey, + ContentType: fileData.contentType, + } + + return { + writeStream: pass, + promise: client.upload(params).promise(), + url: this.buildUrl(params.Bucket, fileKey), + fileKey, + } + } + + async getDownloadStream( + fileData: GetUploadedFileType & { usePrivateBucket?: boolean } + ) { + const usePrivateBucket = !!fileData.usePrivateBucket + this.validatePrivateBucketConfiguration_(usePrivateBucket) + const client = this.getClient(usePrivateBucket) + + const params = { + Bucket: usePrivateBucket ? this.private_bucket_ : this.bucket_, + Key: `${fileData.fileKey}`, + } + + return client.getObject(params).createReadStream() + } + + async getPresignedDownloadUrl({ usePrivateBucket = true, ...fileData }) { + this.validatePrivateBucketConfiguration_(usePrivateBucket) + const client = this.getClient(usePrivateBucket, { + signatureVersion: "v4", + }) + + const params = { + Bucket: usePrivateBucket ? this.private_bucket_ : this.bucket_, + Key: `${fileData.fileKey}`, + Expires: this.downloadUrlDuration, + } + + return await client.getSignedUrlPromise("getObject", params) + } + + validatePrivateBucketConfiguration_(usePrivateBucket) { + if ( + usePrivateBucket && + (!this.private_access_key_id_ || !this.private_bucket_) + ) { + throw new MedusaError( + MedusaError.Types.UNEXPECTED_STATE, + "Private bucket is not configured" + ) + } + } + + protected getClient( + usePrivateBucket = false, + additionalConfiguration: Partial = {} + ) { + return new aws.S3({ + accessKeyId: usePrivateBucket + ? this.private_access_key_id_ + : this.accessKeyId_, + secretAccessKey: usePrivateBucket + ? this.private_secret_access_key_ + : this.secretAccessKey_, + endpoint: this.endpoint_, + s3ForcePathStyle: this.s3ForcePathStyle_, + signatureVersion: this.signatureVersion_, + ...additionalConfiguration, + }) + } +} + +export default MinioService diff --git a/packages/medusa-file-minio/tsconfig.json b/packages/medusa-file-minio/tsconfig.json new file mode 100644 index 0000000000..862daa96b3 --- /dev/null +++ b/packages/medusa-file-minio/tsconfig.json @@ -0,0 +1,30 @@ +{ + "compilerOptions": { + "lib": ["es5", "es6", "es2019"], + "target": "es5", + "outDir": "./dist", + "rootDir": "src", + "esModuleInterop": true, + "declaration": true, + "module": "commonjs", + "moduleResolution": "node", + "emitDecoratorMetadata": true, + "experimentalDecorators": true, + "sourceMap": true, + "noImplicitReturns": true, + "strictNullChecks": true, + "strictFunctionTypes": true, + "noImplicitThis": true, + "allowJs": true, + "skipLibCheck": true, + "downlevelIteration": true // to use ES5 specific tooling + }, + "include": ["src"], + "exclude": [ + "dist", + "src/**/__tests__", + "src/**/__mocks__", + "src/**/__fixtures__", + "node_modules" + ] +} diff --git a/packages/medusa-file-s3/README.md b/packages/medusa-file-s3/README.md index c50228a054..ee5e433dcd 100644 --- a/packages/medusa-file-s3/README.md +++ b/packages/medusa-file-s3/README.md @@ -23,37 +23,38 @@ Store uploaded files to your Medusa backend on S3. 1\. Run the following command in the directory of the Medusa backend: - ```bash - npm install medusa-file-s3 - ``` +```bash +npm install medusa-file-s3 +``` 2\. Set the following environment variables in `.env`: - ```bash - S3_URL= - S3_BUCKET= - S3_REGION= - S3_ACCESS_KEY_ID= - S3_SECRET_ACCESS_KEY= - ``` +```bash +S3_URL= +S3_BUCKET= +S3_REGION= +S3_ACCESS_KEY_ID= +S3_SECRET_ACCESS_KEY= +``` 3\. In `medusa-config.js` add the following at the end of the `plugins` array: - ```js - const plugins = [ - // ... - { - resolve: `medusa-file-s3`, - options: { - s3_url: process.env.S3_URL, - bucket: process.env.S3_BUCKET, - region: process.env.S3_REGION, - access_key_id: process.env.S3_ACCESS_KEY_ID, - secret_access_key: process.env.S3_SECRET_ACCESS_KEY, - }, +```js +const plugins = [ + // ... + { + resolve: `medusa-file-s3`, + options: { + s3_url: process.env.S3_URL, + bucket: process.env.S3_BUCKET, + region: process.env.S3_REGION, + access_key_id: process.env.S3_ACCESS_KEY_ID, + secret_access_key: process.env.S3_SECRET_ACCESS_KEY, + aws_config_option: {}, }, - ] - ``` + }, +] +``` --- @@ -61,9 +62,9 @@ Store uploaded files to your Medusa backend on S3. 1\. Run the following command in the directory of the Medusa backend to run the backend: - ```bash - npm run start - ``` +```bash +npm run start +``` 2\. Upload an image for a product using the admin dashboard or using [the Admin APIs](https://docs.medusajs.com/api/admin#tag/Upload). @@ -71,4 +72,4 @@ Store uploaded files to your Medusa backend on S3. ## Additional Resources -- [S3 Plugin Documentation](https://docs.medusajs.com/plugins/file-service/s3) \ No newline at end of file +- [S3 Plugin Documentation](https://docs.medusajs.com/plugins/file-service/s3) diff --git a/packages/medusa-file-s3/package.json b/packages/medusa-file-s3/package.json index 7b35cad9b5..bae01a6146 100644 --- a/packages/medusa-file-s3/package.json +++ b/packages/medusa-file-s3/package.json @@ -2,7 +2,10 @@ "name": "medusa-file-s3", "version": "1.1.12", "description": "AWS s3 file connector for Medusa", - "main": "index.js", + "main": "dist/index.js", + "files": [ + "dist" + ], "repository": { "type": "git", "url": "https://github.com/medusajs/medusa", @@ -11,32 +14,23 @@ "author": "Sebastian Mateos Nicolajsen", "license": "MIT", "devDependencies": { - "@babel/cli": "^7.7.5", - "@babel/core": "^7.7.5", - "@babel/node": "^7.7.4", - "@babel/plugin-proposal-class-properties": "^7.7.4", - "@babel/plugin-transform-instanceof": "^7.8.3", - "@babel/plugin-transform-runtime": "^7.7.6", - "@babel/preset-env": "^7.7.5", - "@babel/register": "^7.7.4", - "@babel/runtime": "^7.9.6", - "client-sessions": "^0.8.0", + "@medusajs/medusa": "1.8.0-rc.6", "cross-env": "^5.2.1", "jest": "^25.5.4", "medusa-interfaces": "^1.3.7", - "medusa-test-utils": "^1.1.40" + "medusa-test-utils": "^1.1.40", + "typescript": "^4.9.5" }, "scripts": { "prepare": "cross-env NODE_ENV=production yarn run build", "test": "jest --passWithNoTests src", - "build": "babel src --out-dir . --ignore '**/__tests__','**/__mocks__'", - "watch": "babel -w src --out-dir . --ignore '**/__tests__','**/__mocks__'" + "build": "tsc", + "watch": "tsc --watch" }, "peerDependencies": { "medusa-interfaces": "1.3.7" }, "dependencies": { - "@babel/plugin-transform-classes": "^7.15.4", "aws-sdk": "^2.983.0", "body-parser": "^1.19.0", "express": "^4.17.1", diff --git a/packages/medusa-file-s3/src/services/s3.js b/packages/medusa-file-s3/src/services/s3.js deleted file mode 100644 index 8c91f86bec..0000000000 --- a/packages/medusa-file-s3/src/services/s3.js +++ /dev/null @@ -1,139 +0,0 @@ -import fs from "fs" -import aws from "aws-sdk" -import { parse } from "path" -import { AbstractFileService } from "@medusajs/medusa" -import stream from "stream" - -class S3Service extends AbstractFileService { - // eslint-disable-next-line no-empty-pattern - constructor({}, options) { - super({}, options) - - this.bucket_ = options.bucket - this.s3Url_ = options.s3_url - this.accessKeyId_ = options.access_key_id - this.secretAccessKey_ = options.secret_access_key - this.region_ = options.region - this.endpoint_ = options.endpoint - this.awsConfigObject_ = options.aws_config_object - - this.client_ = new aws.S3() - } - - upload(file) { - this.updateAwsConfig() - - return this.uploadFile(file) - } - - uploadProtected(file) { - this.updateAwsConfig() - - return this.uploadFile(file, { acl: "private" }) - } - - uploadFile(file, options = { isProtected: false, acl: undefined }) { - const parsedFilename = parse(file.originalname) - const fileKey = `${parsedFilename.name}-${Date.now()}${parsedFilename.ext}` - - const params = { - ACL: options.acl ?? (options.isProtected ? "private" : "public-read"), - Bucket: this.bucket_, - Body: fs.createReadStream(file.path), - Key: fileKey, - } - - return new Promise((resolve, reject) => { - this.client_.upload(params, (err, data) => { - if (err) { - reject(err) - return - } - - resolve({ url: data.Location, key: data.Key }) - }) - }) - } - - async delete(file) { - this.updateAwsConfig() - - const params = { - Bucket: this.bucket_, - Key: `${file}`, - } - - return new Promise((resolve, reject) => { - this.client_.deleteObject(params, (err, data) => { - if (err) { - reject(err) - return - } - resolve(data) - }) - }) - } - - async getUploadStreamDescriptor(fileData) { - this.updateAwsConfig() - - const pass = new stream.PassThrough() - - const fileKey = `${fileData.name}.${fileData.ext}` - const params = { - ACL: fileData.acl ?? "private", - Bucket: this.bucket_, - Body: pass, - Key: fileKey, - } - - return { - writeStream: pass, - promise: this.client_.upload(params).promise(), - url: `${this.s3Url_}/${fileKey}`, - fileKey, - } - } - - async getDownloadStream(fileData) { - this.updateAwsConfig() - - const params = { - Bucket: this.bucket_, - Key: `${fileData.fileKey}`, - } - - return this.client_.getObject(params).createReadStream() - } - - async getPresignedDownloadUrl(fileData) { - this.updateAwsConfig({ - signatureVersion: "v4", - }) - - const params = { - Bucket: this.bucket_, - Key: `${fileData.fileKey}`, - Expires: this.downloadUrlDuration, - } - - return await this.client_.getSignedUrlPromise("getObject", params) - } - - updateAwsConfig(additionalConfiguration = {}) { - aws.config.setPromisesDependency(null) - - const config = { - ...additionalConfiguration, - accessKeyId: this.accessKeyId_, - secretAccessKey: this.secretAccessKey_, - region: this.region_, - endpoint: this.endpoint_, - ...this.awsConfigObject_, - } - - aws.config.update(config, true) - } -} - -export default S3Service diff --git a/packages/medusa-file-s3/src/services/s3.ts b/packages/medusa-file-s3/src/services/s3.ts new file mode 100644 index 0000000000..8767af9870 --- /dev/null +++ b/packages/medusa-file-s3/src/services/s3.ts @@ -0,0 +1,157 @@ +import fs from "fs" +import aws from "aws-sdk" +import { parse } from "path" +import { + AbstractFileService, + DeleteFileType, + FileServiceUploadResult, + GetUploadedFileType, + IFileService, + UploadStreamDescriptorType, +} from "@medusajs/medusa" +import stream from "stream" +import { PutObjectRequest } from "aws-sdk/clients/s3" +import { ClientConfiguration } from "aws-sdk/clients/s3" + +class S3Service extends AbstractFileService implements IFileService { + protected bucket_: string + protected s3Url_: string + protected accessKeyId_: string + protected secretAccessKey_: string + protected region_: string + protected endpoint_: string + protected awsConfigObject_: any + protected downloadFileDuration_: string + + constructor({}, options) { + super({}, options) + + this.bucket_ = options.bucket + this.s3Url_ = options.s3_url + this.accessKeyId_ = options.access_key_id + this.secretAccessKey_ = options.secret_access_key + this.region_ = options.region + this.endpoint_ = options.endpoint + this.downloadFileDuration_ = options.download_file_duration + this.awsConfigObject_ = options.aws_config_object ?? {} + } + + protected getClient(overwriteConfig: Partial = {}) { + const config: ClientConfiguration = { + accessKeyId: this.accessKeyId_, + secretAccessKey: this.secretAccessKey_, + region: this.region_, + endpoint: this.endpoint_, + ...this.awsConfigObject_, + ...overwriteConfig, + } + + return new aws.S3(config) + } + + async upload(file: Express.Multer.File): Promise { + return await this.uploadFile(file) + } + + async uploadProtected(file: Express.Multer.File) { + return await this.uploadFile(file, { acl: "private" }) + } + + async uploadFile( + file: Express.Multer.File, + options: { isProtected?: boolean; acl?: string } = { + isProtected: false, + acl: undefined, + } + ) { + const client = this.getClient() + + const parsedFilename = parse(file.originalname) + + const fileKey = `${parsedFilename.name}-${Date.now()}${parsedFilename.ext}` + + const params = { + ACL: options.acl ?? (options.isProtected ? "private" : "public-read"), + Bucket: this.bucket_, + Body: fs.createReadStream(file.path), + Key: fileKey, + ContentType: file.mimetype, + } + + const result = await client.upload(params).promise() + + return { + url: result.Location, + key: result.Key, + } + } + + async delete(file: DeleteFileType): Promise { + const client = this.getClient() + + const params = { + Bucket: this.bucket_, + Key: `${file}`, + } + + return new Promise((resolve, reject) => { + client.deleteObject(params, (err, data) => { + if (err) { + reject(err) + return + } + resolve() + }) + }) + } + + async getUploadStreamDescriptor(fileData: UploadStreamDescriptorType) { + const client = this.getClient() + const pass = new stream.PassThrough() + + const fileKey = `${fileData.name}.${fileData.ext}` + const params: PutObjectRequest = { + ACL: fileData.acl ?? "private", + Bucket: this.bucket_, + Body: pass, + Key: fileKey, + ContentType: fileData.contentType as string, + } + + return { + writeStream: pass, + promise: client.upload(params).promise(), + url: `${this.s3Url_}/${fileKey}`, + fileKey, + } + } + + async getDownloadStream( + fileData: GetUploadedFileType + ): Promise { + const client = this.getClient() + + const params = { + Bucket: this.bucket_, + Key: `${fileData.fileKey}`, + } + + return await client.getObject(params).createReadStream() + } + + async getPresignedDownloadUrl( + fileData: GetUploadedFileType + ): Promise { + const client = this.getClient({ signatureVersion: "v4" }) + + const params = { + Bucket: this.bucket_, + Key: `${fileData.fileKey}`, + Expires: this.downloadFileDuration_, + } + + return await client.getSignedUrlPromise("getObject", params) + } +} + +export default S3Service diff --git a/packages/medusa-file-s3/tsconfig.json b/packages/medusa-file-s3/tsconfig.json new file mode 100644 index 0000000000..348fb6e53e --- /dev/null +++ b/packages/medusa-file-s3/tsconfig.json @@ -0,0 +1,30 @@ +{ + "compilerOptions": { + "lib": ["es5", "es6", "es2019"], + "target": "es5", + "outDir": "./dist", + "rootDir": "src", + "esModuleInterop": true, + "declaration": true, + "module": "commonjs", + "moduleResolution": "node", + "emitDecoratorMetadata": true, + "experimentalDecorators": true, + "sourceMap": true, + "noImplicitReturns": true, + "strictNullChecks": true, + "strictFunctionTypes": true, + "noImplicitThis": true, + "allowJs": true, + "skipLibCheck": true, + "downlevelIteration": true + }, + "include": ["src"], + "exclude": [ + "dist", + "src/**/__tests__", + "src/**/__mocks__", + "src/**/__fixtures__", + "node_modules" + ] +} diff --git a/yarn.lock b/yarn.lock index 0977f07eb1..49d08431dc 100644 --- a/yarn.lock +++ b/yarn.lock @@ -262,7 +262,7 @@ __metadata: languageName: node linkType: hard -"@babel/cli@npm:^7.12.1, @babel/cli@npm:^7.14.3, @babel/cli@npm:^7.15.4, @babel/cli@npm:^7.16.0, @babel/cli@npm:^7.7.5": +"@babel/cli@npm:^7.12.1, @babel/cli@npm:^7.14.3, @babel/cli@npm:^7.15.4, @babel/cli@npm:^7.7.5": version: 7.18.6 resolution: "@babel/cli@npm:7.18.6" dependencies: @@ -995,7 +995,7 @@ __metadata: languageName: node linkType: hard -"@babel/node@npm:^7.12.6, @babel/node@npm:^7.15.4, @babel/node@npm:^7.16.0, @babel/node@npm:^7.7.4": +"@babel/node@npm:^7.12.6, @babel/node@npm:^7.15.4, @babel/node@npm:^7.7.4": version: 7.18.6 resolution: "@babel/node@npm:7.18.6" dependencies: @@ -1113,7 +1113,7 @@ __metadata: languageName: node linkType: hard -"@babel/plugin-proposal-class-properties@npm:^7.0.0, @babel/plugin-proposal-class-properties@npm:^7.10.4, @babel/plugin-proposal-class-properties@npm:^7.12.1, @babel/plugin-proposal-class-properties@npm:^7.14.0, @babel/plugin-proposal-class-properties@npm:^7.14.5, @babel/plugin-proposal-class-properties@npm:^7.16.0, @babel/plugin-proposal-class-properties@npm:^7.18.6, @babel/plugin-proposal-class-properties@npm:^7.7.4": +"@babel/plugin-proposal-class-properties@npm:^7.0.0, @babel/plugin-proposal-class-properties@npm:^7.10.4, @babel/plugin-proposal-class-properties@npm:^7.12.1, @babel/plugin-proposal-class-properties@npm:^7.14.0, @babel/plugin-proposal-class-properties@npm:^7.14.5, @babel/plugin-proposal-class-properties@npm:^7.18.6, @babel/plugin-proposal-class-properties@npm:^7.7.4": version: 7.18.6 resolution: "@babel/plugin-proposal-class-properties@npm:7.18.6" dependencies: @@ -1714,7 +1714,7 @@ __metadata: languageName: node linkType: hard -"@babel/plugin-transform-classes@npm:^7.0.0, @babel/plugin-transform-classes@npm:^7.10.4, @babel/plugin-transform-classes@npm:^7.12.1, @babel/plugin-transform-classes@npm:^7.15.4, @babel/plugin-transform-classes@npm:^7.16.0, @babel/plugin-transform-classes@npm:^7.18.6, @babel/plugin-transform-classes@npm:^7.9.5": +"@babel/plugin-transform-classes@npm:^7.0.0, @babel/plugin-transform-classes@npm:^7.10.4, @babel/plugin-transform-classes@npm:^7.12.1, @babel/plugin-transform-classes@npm:^7.15.4, @babel/plugin-transform-classes@npm:^7.18.6, @babel/plugin-transform-classes@npm:^7.9.5": version: 7.18.8 resolution: "@babel/plugin-transform-classes@npm:7.18.8" dependencies: @@ -1872,7 +1872,7 @@ __metadata: languageName: node linkType: hard -"@babel/plugin-transform-instanceof@npm:^7.10.4, @babel/plugin-transform-instanceof@npm:^7.12.1, @babel/plugin-transform-instanceof@npm:^7.12.13, @babel/plugin-transform-instanceof@npm:^7.14.5, @babel/plugin-transform-instanceof@npm:^7.16.0, @babel/plugin-transform-instanceof@npm:^7.8.3": +"@babel/plugin-transform-instanceof@npm:^7.10.4, @babel/plugin-transform-instanceof@npm:^7.12.1, @babel/plugin-transform-instanceof@npm:^7.12.13, @babel/plugin-transform-instanceof@npm:^7.14.5, @babel/plugin-transform-instanceof@npm:^7.8.3": version: 7.18.6 resolution: "@babel/plugin-transform-instanceof@npm:7.18.6" dependencies: @@ -2195,7 +2195,7 @@ __metadata: languageName: node linkType: hard -"@babel/plugin-transform-runtime@npm:^7.11.5, @babel/plugin-transform-runtime@npm:^7.12.1, @babel/plugin-transform-runtime@npm:^7.15.0, @babel/plugin-transform-runtime@npm:^7.16.4, @babel/plugin-transform-runtime@npm:^7.7.6": +"@babel/plugin-transform-runtime@npm:^7.11.5, @babel/plugin-transform-runtime@npm:^7.12.1, @babel/plugin-transform-runtime@npm:^7.15.0, @babel/plugin-transform-runtime@npm:^7.7.6": version: 7.18.6 resolution: "@babel/plugin-transform-runtime@npm:7.18.6" dependencies: @@ -2426,7 +2426,7 @@ __metadata: languageName: node linkType: hard -"@babel/preset-env@npm:^7.11.5, @babel/preset-env@npm:^7.12.11, @babel/preset-env@npm:^7.12.7, @babel/preset-env@npm:^7.15.4, @babel/preset-env@npm:^7.15.6, @babel/preset-env@npm:^7.16.4, @babel/preset-env@npm:^7.7.5": +"@babel/preset-env@npm:^7.11.5, @babel/preset-env@npm:^7.12.11, @babel/preset-env@npm:^7.12.7, @babel/preset-env@npm:^7.15.4, @babel/preset-env@npm:^7.15.6, @babel/preset-env@npm:^7.7.5": version: 7.18.6 resolution: "@babel/preset-env@npm:7.18.6" dependencies: @@ -2568,7 +2568,7 @@ __metadata: languageName: node linkType: hard -"@babel/register@npm:^7.11.5, @babel/register@npm:^7.12.1, @babel/register@npm:^7.15.3, @babel/register@npm:^7.16.0, @babel/register@npm:^7.18.6, @babel/register@npm:^7.7.4": +"@babel/register@npm:^7.11.5, @babel/register@npm:^7.12.1, @babel/register@npm:^7.15.3, @babel/register@npm:^7.18.6, @babel/register@npm:^7.7.4": version: 7.18.6 resolution: "@babel/register@npm:7.18.6" dependencies: @@ -2615,7 +2615,7 @@ __metadata: languageName: node linkType: hard -"@babel/runtime@npm:^7.0.0, @babel/runtime@npm:^7.10.0, @babel/runtime@npm:^7.10.2, @babel/runtime@npm:^7.11.2, @babel/runtime@npm:^7.12.5, @babel/runtime@npm:^7.15.4, @babel/runtime@npm:^7.16.3, @babel/runtime@npm:^7.17.8, @babel/runtime@npm:^7.18.0, @babel/runtime@npm:^7.18.3, @babel/runtime@npm:^7.3.1, @babel/runtime@npm:^7.5.0, @babel/runtime@npm:^7.5.5, @babel/runtime@npm:^7.7.2, @babel/runtime@npm:^7.7.6, @babel/runtime@npm:^7.8.4, @babel/runtime@npm:^7.9.2, @babel/runtime@npm:^7.9.6": +"@babel/runtime@npm:^7.0.0, @babel/runtime@npm:^7.10.0, @babel/runtime@npm:^7.10.2, @babel/runtime@npm:^7.11.2, @babel/runtime@npm:^7.12.5, @babel/runtime@npm:^7.15.4, @babel/runtime@npm:^7.17.8, @babel/runtime@npm:^7.18.0, @babel/runtime@npm:^7.18.3, @babel/runtime@npm:^7.3.1, @babel/runtime@npm:^7.5.0, @babel/runtime@npm:^7.5.5, @babel/runtime@npm:^7.7.2, @babel/runtime@npm:^7.7.6, @babel/runtime@npm:^7.8.4, @babel/runtime@npm:^7.9.2, @babel/runtime@npm:^7.9.6": version: 7.18.6 resolution: "@babel/runtime@npm:7.18.6" dependencies: @@ -6059,6 +6059,44 @@ __metadata: languageName: unknown linkType: soft +"@medusajs/medusa-cli@npm:1.3.9-rc.2": + version: 1.3.9-rc.2 + resolution: "@medusajs/medusa-cli@npm:1.3.9-rc.2" + dependencies: + "@medusajs/utils": 0.0.2-rc.2 + axios: ^0.21.4 + chalk: ^4.0.0 + configstore: 5.0.1 + core-js: ^3.6.5 + dotenv: ^8.2.0 + execa: ^5.1.1 + fs-exists-cached: ^1.0.0 + fs-extra: ^10.0.0 + hosted-git-info: ^4.0.2 + inquirer: ^8.0.0 + is-valid-path: ^0.1.1 + meant: ^1.0.3 + medusa-core-utils: ^1.2.0-rc.0 + medusa-telemetry: 0.0.16 + open: ^8.0.6 + ora: ^5.4.1 + pg-god: ^1.0.12 + prompts: ^2.4.2 + regenerator-runtime: ^0.13.11 + resolve-cwd: ^3.0.0 + semver: ^7.3.8 + sqlite3: ^5.0.2 + stack-trace: ^0.0.10 + ulid: ^2.3.0 + url: ^0.11.0 + winston: ^3.8.2 + yargs: ^15.3.1 + bin: + medusa: cli.js + checksum: 480011b6850187191c13dd503b894190b102dda0ae90d0413bfcbfa33083310b776a5f504434c70f1dd44721f669d8b9746561296e6a133d9c51526d9a9af88f + languageName: node + linkType: hard + "@medusajs/medusa-js@2.0.2, @medusajs/medusa-js@workspace:packages/medusa-js": version: 0.0.0-use.local resolution: "@medusajs/medusa-js@workspace:packages/medusa-js" @@ -6175,6 +6213,67 @@ __metadata: languageName: unknown linkType: soft +"@medusajs/medusa@npm:1.8.0-rc.6": + version: 1.8.0-rc.6 + resolution: "@medusajs/medusa@npm:1.8.0-rc.6" + dependencies: + "@medusajs/medusa-cli": 1.3.9-rc.2 + "@medusajs/modules-sdk": 0.1.0-rc.4 + "@medusajs/utils": 0.0.2-rc.2 + "@types/ioredis": ^4.28.10 + "@types/lodash": ^4.14.191 + awilix: ^8.0.0 + body-parser: ^1.19.0 + boxen: ^5.0.1 + bullmq: ^3.5.6 + chokidar: ^3.4.2 + class-transformer: ^0.5.1 + class-validator: ^0.13.2 + connect-redis: ^5.0.0 + cookie-parser: ^1.4.6 + core-js: ^3.6.5 + cors: ^2.8.5 + cross-spawn: ^7.0.3 + dotenv: ^16.0.3 + express: ^4.17.1 + express-session: ^1.17.3 + fs-exists-cached: ^1.0.0 + glob: ^7.1.6 + ioredis: ^5.2.5 + ioredis-mock: ^5.6.0 + iso8601-duration: ^1.3.0 + jsonwebtoken: ^8.5.1 + lodash: ^4.17.21 + medusa-core-utils: ^1.2.0-rc.0 + medusa-telemetry: ^0.0.16 + medusa-test-utils: ^1.1.40-rc.0 + morgan: ^1.9.1 + multer: ^1.4.4 + node-schedule: ^2.1.1 + papaparse: ^5.3.2 + passport: ^0.4.1 + passport-http-bearer: ^1.0.1 + passport-jwt: ^4.0.1 + passport-local: ^1.0.0 + randomatic: ^3.1.1 + redis: ^3.0.2 + reflect-metadata: ^0.1.13 + regenerator-runtime: ^0.13.11 + request-ip: ^2.1.3 + scrypt-kdf: ^2.0.1 + ulid: ^2.3.0 + uuid: ^8.3.2 + winston: ^3.8.2 + peerDependencies: + "@medusajs/types": 0.0.2-rc.1 + medusa-interfaces: 1.3.7-rc.0 + typeorm: ^0.3.11 + bin: + medusa: cli.js + checksum: 8260fc5647459569e0c45a2ce86c0a3290085182ec2fb89f8e7a00dafa85c2f6932442a4566911af8962d9a75053b078468b9fa591b9828f2f97a58c3c32fd57 + languageName: node + linkType: hard + "@medusajs/modules-sdk@1.8.2, @medusajs/modules-sdk@workspace:packages/modules-sdk": version: 0.0.0-use.local resolution: "@medusajs/modules-sdk@workspace:packages/modules-sdk" @@ -6192,6 +6291,20 @@ __metadata: languageName: unknown linkType: soft +"@medusajs/modules-sdk@npm:0.1.0-rc.4": + version: 0.1.0-rc.4 + resolution: "@medusajs/modules-sdk@npm:0.1.0-rc.4" + dependencies: + "@medusajs/types": 0.0.2-rc.1 + "@medusajs/utils": 0.0.2-rc.2 + awilix: ^8.0.0 + glob: 7.1.6 + medusa-telemetry: ^0.0.16 + resolve-cwd: ^3.0.0 + checksum: f5d9d6ff6db08dfa4a7e2072b414ab608f89420cecfbcb992ce4e710b309efbbeb3b2d9c0f74c924658b86399f89fafd740a860e592d581c27e0a68bf7920724 + languageName: node + linkType: hard + "@medusajs/oas-github-ci@workspace:packages/oas/oas-github-ci": version: 0.0.0-use.local resolution: "@medusajs/oas-github-ci@workspace:packages/oas/oas-github-ci" @@ -6266,6 +6379,13 @@ __metadata: languageName: unknown linkType: soft +"@medusajs/types@npm:0.0.2-rc.1": + version: 0.0.2-rc.1 + resolution: "@medusajs/types@npm:0.0.2-rc.1" + checksum: 05c122211741d540aad426cc164accd92701b66bde68be5c50d85627959c87295c49507fe5b09136689898fec810b6a7833076675f67039a3b77de6ef4c6ee9e + languageName: node + linkType: hard + "@medusajs/utils@1.8.1, @medusajs/utils@workspace:packages/utils": version: 0.0.0-use.local resolution: "@medusajs/utils@workspace:packages/utils" @@ -6284,6 +6404,19 @@ __metadata: languageName: unknown linkType: soft +"@medusajs/utils@npm:0.0.2-rc.2": + version: 0.0.2-rc.2 + resolution: "@medusajs/utils@npm:0.0.2-rc.2" + dependencies: + awilix: ^8.0.0 + class-transformer: ^0.5.1 + class-validator: ^0.13.2 + typeorm: ^0.3.11 + ulid: ^2.3.0 + checksum: 5404298118fcd8009622f55b44fecaf2deaf0ec948977271e05384541dc6922c0ab2406e7b94aa498b475ba73793fb8e3ebc57ac66a01382557f1f4c3bdda8cd + languageName: node + linkType: hard + "@microsoft/fetch-event-source@npm:2.0.1": version: 2.0.1 resolution: "@microsoft/fetch-event-source@npm:2.0.1" @@ -13984,7 +14117,7 @@ __metadata: languageName: node linkType: hard -"aws-sdk@npm:^2.1043.0, aws-sdk@npm:^2.710.0, aws-sdk@npm:^2.983.0": +"aws-sdk@npm:^2.710.0, aws-sdk@npm:^2.983.0": version: 2.1172.0 resolution: "aws-sdk@npm:2.1172.0" dependencies: @@ -20742,7 +20875,7 @@ __metadata: languageName: node linkType: hard -"fengari-interop@npm:^0.1.3": +"fengari-interop@npm:^0.1.2, fengari-interop@npm:^0.1.3": version: 0.1.3 resolution: "fengari-interop@npm:0.1.3" peerDependencies: @@ -23811,6 +23944,21 @@ __metadata: languageName: node linkType: hard +"ioredis-mock@npm:^5.6.0": + version: 5.9.1 + resolution: "ioredis-mock@npm:5.9.1" + dependencies: + fengari: ^0.1.4 + fengari-interop: ^0.1.2 + lodash: ^4.17.21 + standard-as-callback: ^2.1.0 + peerDependencies: + ioredis: 4.x + redis-commands: 1.x + checksum: 0b898273788c38ec617a3cf94df93c41a8f05a1cf57dfbbc3a97ea72fc07882be3872d55c915e49e615892ec20129351be7bb68e1beb7563a01eec157cc84088 + languageName: node + linkType: hard + "ioredis@npm:^4.27.9": version: 4.28.5 resolution: "ioredis@npm:4.28.5" @@ -29071,7 +29219,7 @@ __metadata: languageName: node linkType: hard -"medusa-core-utils@1.2.0, medusa-core-utils@^1.2.0, medusa-core-utils@workspace:packages/medusa-core-utils": +"medusa-core-utils@1.2.0, medusa-core-utils@^1.2.0, medusa-core-utils@^1.2.0-rc.0, medusa-core-utils@workspace:packages/medusa-core-utils": version: 0.0.0-use.local resolution: "medusa-core-utils@workspace:packages/medusa-core-utils" dependencies: @@ -29114,25 +29262,15 @@ __metadata: version: 0.0.0-use.local resolution: "medusa-file-minio@workspace:packages/medusa-file-minio" dependencies: - "@babel/cli": ^7.16.0 - "@babel/core": ^7.16.0 - "@babel/node": ^7.16.0 - "@babel/plugin-proposal-class-properties": ^7.16.0 - "@babel/plugin-transform-classes": ^7.16.0 - "@babel/plugin-transform-instanceof": ^7.16.0 - "@babel/plugin-transform-runtime": ^7.16.4 - "@babel/preset-env": ^7.16.4 - "@babel/register": ^7.16.0 - "@babel/runtime": ^7.16.3 - aws-sdk: ^2.1043.0 + "@medusajs/medusa": 1.8.0-rc.6 + aws-sdk: ^2.983.0 body-parser: ^1.19.0 - client-sessions: ^0.8.0 cross-env: ^5.2.1 express: ^4.17.1 jest: ^25.5.4 medusa-core-utils: ^1.2.0 - medusa-interfaces: ^1.3.7 medusa-test-utils: ^1.1.40 + typescript: ^4.9.5 peerDependencies: medusa-interfaces: 1.3.7 languageName: unknown @@ -29142,25 +29280,16 @@ __metadata: version: 0.0.0-use.local resolution: "medusa-file-s3@workspace:packages/medusa-file-s3" dependencies: - "@babel/cli": ^7.7.5 - "@babel/core": ^7.7.5 - "@babel/node": ^7.7.4 - "@babel/plugin-proposal-class-properties": ^7.7.4 - "@babel/plugin-transform-classes": ^7.15.4 - "@babel/plugin-transform-instanceof": ^7.8.3 - "@babel/plugin-transform-runtime": ^7.7.6 - "@babel/preset-env": ^7.7.5 - "@babel/register": ^7.7.4 - "@babel/runtime": ^7.9.6 + "@medusajs/medusa": 1.8.0-rc.6 aws-sdk: ^2.983.0 body-parser: ^1.19.0 - client-sessions: ^0.8.0 cross-env: ^5.2.1 express: ^4.17.1 jest: ^25.5.4 medusa-core-utils: ^1.2.0 medusa-interfaces: ^1.3.7 medusa-test-utils: ^1.1.40 + typescript: ^4.9.5 peerDependencies: medusa-interfaces: 1.3.7 languageName: unknown @@ -29840,7 +29969,7 @@ __metadata: languageName: unknown linkType: soft -"medusa-test-utils@^1.1.40, medusa-test-utils@workspace:packages/medusa-test-utils": +"medusa-test-utils@^1.1.40, medusa-test-utils@^1.1.40-rc.0, medusa-test-utils@workspace:packages/medusa-test-utils": version: 0.0.0-use.local resolution: "medusa-test-utils@workspace:packages/medusa-test-utils" dependencies: @@ -30757,7 +30886,7 @@ __metadata: languageName: node linkType: hard -"multer@npm:^1.4.3": +"multer@npm:^1.4.3, multer@npm:^1.4.4": version: 1.4.4 resolution: "multer@npm:1.4.4" dependencies: @@ -32495,6 +32624,16 @@ __metadata: languageName: node linkType: hard +"passport@npm:^0.4.1": + version: 0.4.1 + resolution: "passport@npm:0.4.1" + dependencies: + passport-strategy: 1.x.x + pause: 0.0.1 + checksum: aa1a8eb2e991368734ae1e33d354c94a02c5fcd27c4ef25c3c303b4f3df1e05512ac0159e608cedbfc8c544c166735a153124cfa3bd8d48fb01f5ded500f0c5f + languageName: node + linkType: hard + "passport@npm:^0.6.0": version: 0.6.0 resolution: "passport@npm:0.6.0" @@ -39833,7 +39972,7 @@ __metadata: languageName: node linkType: hard -"typeorm@npm:^0.3.14": +"typeorm@npm:^0.3.11, typeorm@npm:^0.3.14": version: 0.3.14 resolution: "typeorm@npm:0.3.14" dependencies: