diff --git a/.changeset/unlucky-ducks-raise.md b/.changeset/unlucky-ducks-raise.md new file mode 100644 index 00000000000..22de31b403e --- /dev/null +++ b/.changeset/unlucky-ducks-raise.md @@ -0,0 +1,127 @@ +--- +'@keystone-6/core': major +--- +#### Breaking + +##### Move of `image` and `files` in the keystone config into new option `storage` + +The `image` and `files` config options have been removed from Keystone's config - the configuration has +been moved into a new `storage` configuration object. + +Old: + +```ts +export default config({ + image: { upload: 'local' }, + lists: { + Image: { fields: { image: image() } }, + /* ... */ + }, + /* ... */ +}); +``` + +New: +```ts +export default config({ + storage: { + my_images: { + kind: 'local', + type: 'image', + generateUrl: path => `http://localhost:3000/images${path}`, + serverRoute: { path: '/images' }, + storagePath: 'public/images', + }, + }, + lists: { + Image: { fields: { image: image({ storage: 'my_images' }) } }, + /* ... */ + }, + /* ... */ +}); +``` + +You can also now store assets on S3: + +```ts +export default config({ + storage: { + my_image_storage: { + kind: 's3', + type: 'image', + bucketName: S3_BUCKET_NAME, + region: S3_REGION, + accessKeyId: S3_ACCESS_KEY_ID, + secretAccessKey: S3_SECRET_ACCESS_KEY, + }, + }, + lists: { + Image: { fields: { image: image({ storage: 'my_image_storage' }) } }, + /* ... */ + }, + /* ... */ +}); +``` + +##### Removal of refs for `images` and `files` + +Refs were an interesting situation! They allowed you to link images stored in your storage source (s3 or local), and use the same +image anywhere else images are used. This causes a bunch of complication, and prevented Keystone ever reliably being able +to remove images from your source, as it couldn't easily track where images were used. + +To simplify things for you and us, we're removing `refs` as a concept, but don't panic just yet, we are conceptually replacing +them with something you are already familiar with: `relationships`. + +If you wanted refs, where images could be available in multiple places, our new recommendation is: + +```ts +export default config({ + storage: { + my_image_storage: { + /* ... */ + }, + }, + lists: { + Image: { fields: { image: image({ storage: 'my_image_storage' }) } }, + User: { fields: { avatar: relationship({ ref: 'Image' }) } }, + Blog: { fields: { photos: relationship({ ref: 'Image', many: true }) } }, + /* ... */ + }, + /* ... */ +}); +``` + +This allows mirroring of the old functionality, while allowing us to add the below feature/breaking change. + +##### Images and Files will now be deleted when deleted + +Before this change, if you uploaded a file or image, Keystone would never remove it from where it was stored. The inability to tidy up unused +files or images was unwelcome. With the removal of `ref`, we can now remove things from the source, and this will be done by default. + +If you don't want files or images removed, we recommend storing them as a `relationship`, rather than on items themselves, so the files +or images persist separate to lists that use them. + +If you want the existing behaviour of keystone, set `preserve: true` on the storage instead. + +##### `file` and `image` URLs now use `generateUrl`, allowing more control over what is returned to the user + +##### Local images no longer need a baseUrl + +Previously, if you were using local storage (you scallywag), you needed to provide a path for keystone to host images on. You +can still do this, but if you plan on serving them from another location, you can opt into not doing this. + + +```diff +{ +- baseUrl: '/images' ++ serverRoute: { ++ path: '/images' ++ } +} +``` + +#### New bits + +- S3 is now supported! See the `storage` config for all the options for S3. +- `preserve` flag added to both `file` and `image` fields to allow removal of files from the source +- Support for multiple `storage` sources - each `image` and `file` field can now use its own config you want. diff --git a/.github/workflows/tests.yml b/.github/workflows/tests.yml index 67f8f434220..5b5858edf62 100644 --- a/.github/workflows/tests.yml +++ b/.github/workflows/tests.yml @@ -37,8 +37,8 @@ jobs: postgres: image: postgres:12 env: - POSTGRES_USER: keystone5 - POSTGRES_PASSWORD: k3yst0n3 + POSTGRES_USER: testuser + POSTGRES_PASSWORD: testpass POSTGRES_DB: test_db ports: - 5432:5432 @@ -80,7 +80,7 @@ jobs: - name: Unit tests if: needs.should_run_tests.outputs.shouldRunTests == 'true' - run: yarn jest --ci --runInBand api-tests + run: yarn jest --ci --runInBand api-tests --testPathIgnorePatterns=examples-smoke-tests --testPathIgnorePatterns=tests/api-tests/fields/crud env: CLOUDINARY_CLOUD_NAME: ${{ secrets.CLOUDINARY_CLOUD_NAME }} CLOUDINARY_KEY: ${{ secrets.CLOUDINARY_KEY }} @@ -88,7 +88,7 @@ jobs: CI_NODE_TOTAL: 9 CI_NODE_INDEX: ${{ matrix.index }} TEST_ADAPTER: ${{ matrix.adapter }} - DATABASE_URL: ${{ matrix.adapter == 'sqlite' && 'file:./dev.db' || 'postgres://keystone5:k3yst0n3@localhost:5432/test_db' }} + DATABASE_URL: ${{ matrix.adapter == 'sqlite' && 'file:./dev.db' || 'postgres://testuser:testpass@localhost:5432/test_db' }} unit_tests: name: Package Unit Tests @@ -129,6 +129,89 @@ jobs: CLOUDINARY_KEY: ${{ secrets.CLOUDINARY_KEY }} CLOUDINARY_SECRET: ${{ secrets.CLOUDINARY_SECRET }} + field_crud_tests: + name: Field CRUD Tests + needs: should_run_tests + runs-on: ubuntu-latest + services: + postgres: + image: postgres:12 + env: + POSTGRES_USER: testuser + POSTGRES_PASSWORD: testpass + POSTGRES_DB: test_db + ports: + - 5432:5432 + strategy: + fail-fast: false + matrix: + adapter: ['postgresql', 'sqlite'] + steps: + - name: Checkout Repo + if: needs.should_run_tests.outputs.shouldRunTests == 'true' + uses: actions/checkout@v2 + + - name: Setup Node.js LTS + if: needs.should_run_tests.outputs.shouldRunTests == 'true' + uses: actions/setup-node@main + with: + node-version: lts/* + + - name: Get yarn cache directory path + if: needs.should_run_tests.outputs.shouldRunTests == 'true' + id: yarn-cache-dir-path + run: echo "::set-output name=dir::$(yarn cache dir)" + + - uses: actions/cache@v2 + if: needs.should_run_tests.outputs.shouldRunTests == 'true' + id: yarn-cache + with: + path: | + ${{ steps.yarn-cache-dir-path.outputs.dir }} + node_modules + key: ${{ runner.os }}-yarn-v5-${{ hashFiles('yarn.lock') }} + restore-keys: | + ${{ runner.os }}-yarn-v5- + + - name: Install Dependencies + if: needs.should_run_tests.outputs.shouldRunTests == 'true' + run: yarn + - name: Setup local S3 bucket + if: needs.should_run_tests.outputs.shouldRunTests == 'true' + run: | + docker run -d -p 9000:9000 --name minio \ + -e "MINIO_ACCESS_KEY=keystone" \ + -e "MINIO_SECRET_KEY=keystone" \ + -v /tmp/data:/data \ + -v /tmp/config:/root/.minio \ + minio/minio server /data + + export AWS_ACCESS_KEY_ID=keystone + export AWS_SECRET_ACCESS_KEY=keystone + export AWS_EC2_METADATA_DISABLED=true + + + + aws --endpoint-url http://127.0.0.1:9000/ s3 mb s3://keystone-test + aws --endpoint-url http://127.0.0.1:9000/ s3api put-bucket-policy --bucket keystone-test --policy file://tests/api-tests/s3-public-read-policy.json + + - name: Unit tests + if: needs.should_run_tests.outputs.shouldRunTests == 'true' + run: yarn jest --ci --runInBand tests/api-tests/fields/crud + env: + CLOUDINARY_CLOUD_NAME: ${{ secrets.CLOUDINARY_CLOUD_NAME }} + CLOUDINARY_KEY: ${{ secrets.CLOUDINARY_KEY }} + CLOUDINARY_SECRET: ${{ secrets.CLOUDINARY_SECRET }} + S3_ENDPOINT: http://127.0.0.1:9000/ + S3_FORCE_PATH_STYLE: true + S3_BUCKET_NAME: keystone-test + S3_ACCESS_KEY_ID: keystone + S3_SECRET_ACCESS_KEY: keystone + # this doesn't mean anything when we're using a custom s3 endpoint but the sdk wants something so we just give it something + S3_REGION: us-east-1 + TEST_ADAPTER: ${{ matrix.adapter }} + DATABASE_URL: ${{ matrix.adapter == 'sqlite' && 'file:./dev.db' || 'postgres://testuser:testpass@localhost:5432/test_db' }} + examples_tests: name: Testing example project needs: should_run_tests diff --git a/docs/pages/docs/apis/config.mdx b/docs/pages/docs/apis/config.mdx index 2cf008a0554..6f467f61af7 100644 --- a/docs/pages/docs/apis/config.mdx +++ b/docs/pages/docs/apis/config.mdx @@ -24,7 +24,7 @@ export default config({ session: { /* ... */ }, graphql: { /* ... */ }, extendGraphqlSchema: { /* ... */ }, - images: { /* ... */ }, + storage: { /* ... */ }, experimental: { /* ... */ }, }); ``` @@ -307,7 +307,7 @@ export default config({ The created context will be bound to the request, including the current visitor's session, meaning access control will work the same as for GraphQL API requests. -*ProTip!*: `extendExpressApp` can be `async` +_ProTip!_: `extendExpressApp` can be `async` ## session @@ -333,7 +333,7 @@ See the [Session API](./session) for more details on how to configure session ma ## graphql -``` +```ts import type { GraphQLConfig } from '@keystone-6/core/types'; ``` @@ -370,7 +370,7 @@ export default config({ ## extendGraphqlSchema -``` +```ts import type { ExtendGraphqlSchema } from '@keystone-6/core/types'; ``` @@ -390,63 +390,95 @@ export default config({ See the [schema extension guide](../guides/schema-extension) for more details on how to use `graphQLSchemaExtension()` to extend your GraphQL API. -## files +## storage (images and files) -Keystone supports file handling via the [`file`](./fields#file) field type. -In order to use this field type you need to configure Keystone with information about where your files will be stored and served from. -At the moment Keystone supports storing files on the local filesystem, and is agnostic about how files are served. - -```typescript -import { config } from '@keystone-6/core'; - -export default config({ - files: { - upload: 'local', - local: { - storagePath: 'public/files', - baseUrl: '/files', - }, - } - /* ... */ -}); +```ts +import type { StorageConfig } from '@keystone-6/core/types' ``` -Options: +The `storage` config option provides configuration which is used by the [`file`](./fields#file) field type or the +[`image`](./fields#image) field type. You provide an object whose property is a `StorageConfig` object, fields then reference this `storage` by the key. +Each storage is configured separately using the options below. -- `upload`: The storage target when uploading files. Currently only `local` is supported. -- `local`: Configuration options when using the `local` storage target. - - `storagePath`: The path local files are uploaded to. - - `baseUrl`: The base of the URL local files will be served from, outside of keystone. +A single storage may be used by multiple file or image fields, but only for files or images. -## images +Options: -Keystone supports image handling via the [`image`](./fields#image) field type. -In order to use this field type you need to configure Keystone with information about where your images will be stored and served from. -At the moment Keystone supports storing files on the local filesystem, and is agnostic about how images are served. +- `kind`: Whether the storage will be on the machine `"local"`, or in an [s3 bucket](https://aws.amazon.com/s3/) `"s3"` +- `type`: Sets whether image fields or file fields should be used with this storage +- `preserve`: Defines whether the items should be deleted at the source when they are removed from Keystone's database. We + recommend that you set `preserve: false` unless you have a strong reason to preserve files that Keystone cannot reference. The + default is `false`. +- `transformName`: A function that creates the name for the file or image. This works a bit differently for files and images, which we'll explain below + +**For files:** transformName accepts a `filename` and returns a `filename` - the returned filename is what will be used as the name of the file at +the storage location, and will be remmembered by the field to to look up at the database. For the default, we will return `${filename}-${RANDOM_ID}${extension}` +**For images:** tranformName accepts both a `filename` and an `extension` - the passed filename will include the extension. The return should be the +filename you want to use as the `id` for the image, and the unique identifier. We will add the extension on to the id provided. By default we return +a unique identifier here. + +When using `transformName` you should ensure that these are unique + +Local options: + +- `generateUrl`: A function that recieves a partial path with the filename and extension, and the result + of which will be used as the `url` in the field's graphql - this should point to where the client can retrieve the item. +- `serverRoute`: Sets whether or not to add a server route to Keystone. Set it to `null` if you don't want + keystone to host the images, otherwise set it to an object with a `path` property + - `path`: The partial path where keystone will host the images, eg `/images` or `/files` + +S3 options: + +- `bucketName`: The name of your s3 bucket +- `region`: The region your s3 instance is hosted in +- `accessKeyId`: Your s3 access Key ID +- `secretAccessKey`: Your Access Key secret +- `generateUrl`: A funcion that recieves the original s3 url and returns a string that will be returned + from graphql as the `url`. If you want the s3 urls to be returned as is you do not need to set this. +- `pathPrefix`: The prefix for the file, used to set the subfolder of your bucket files will be stored in. +- `endpoint`: The endpoint to use - if provided, this endpoint will be used instead of the default amazon s3 endpoint +- `forcePathStyle`: Force the old pathstyle of using the bucket name after the host ```typescript import { config } from '@keystone-6/core'; +import dotenv from 'dotenv'; +/* ... */ + +dotenv.config(); + +const { + S3_BUCKET_NAME: bucketName = 'keystone-test', + S3_REGION: region = 'ap-southeast-2', + S3_ACCESS_KEY_ID: accessKeyId = 'keystone', + S3_SECRET_ACCESS_KEY: secretAccessKey = 'keystone', +} = process.env; export default config({ - images: { - upload: 'local', - local: { + /* ... */ + storage: { + my_S3_images: { + kind: 's3', + bucketName, + region, + accessKeyId, + secretAccessKey, + proxied: { baseUrl: '/images-proxy' }, + endpoint: 'http://127.0.0.1:9000/', + forcePathStyle: true, + }, + my_local_images: { + kind: 'local', + generateUrl: path => `http://localhost:3000/images${path}`, + serverRoute: { + path: '/images', + }, storagePath: 'public/images', - baseUrl: '/images', }, - } - /* ... */ + }, }); ``` -Options: - -- `upload`: The storage target when uploading images. Currently only `local` is supported. -- `local`: Configuration options when using the `local` storage target. - - `storagePath`: The path local images are uploaded to. - - `baseUrl`: The base of the URL local images will be served from, outside of keystone. - -## experimental +## Experimental Options The following flags allow you to enable features which are still in preview. These features are not guaranteed to work, and should be used with caution. diff --git a/docs/pages/docs/apis/fields.mdx b/docs/pages/docs/apis/fields.mdx index fe3782e19e6..dbd48fd4bc7 100644 --- a/docs/pages/docs/apis/fields.mdx +++ b/docs/pages/docs/apis/fields.mdx @@ -722,7 +722,7 @@ File types allow you to upload different types of files to your Keystone system. A `file` field represents a file of any type. -See [`config.files`](./config#files) for details on how to configure your Keystone system with support for the `file` field type. +See [`config.storage`](./config#storage) for details on how to configure your Keystone system with support for the `file` field type. ```typescript import { config, list } from '@keystone-6/core'; @@ -732,7 +732,7 @@ export default config({ lists: { ListName: list({ fields: { - repo: file(), + repo: file({ storage: 'my_file_storage' }), /* ... */ }, }), @@ -742,11 +742,16 @@ export default config({ }); ``` +Options: + +- `storage`(required): A string that is the key for one of the entries in the storage object. This + is used to determine what storage config will be used. + ### image An `image` field represents an image file, i.e. `.jpg`, `.png`, `.webp`, or `.gif`. -See [`config.images`](./config#images) for details on how to configure your Keystone system with support for the `image` field type. +See [`config.storage`](./config#storage) for details on how to configure your Keystone system with support for the `image` field type. ```typescript import { config, list } from '@keystone-6/core'; @@ -756,7 +761,7 @@ export default config({ lists: { ListName: list({ fields: { - avatar: image(), + avatar: image({ storage: 'my_image_storage' }), /* ... */ }, }), @@ -766,6 +771,11 @@ export default config({ }); ``` +Options: + +- `storage`(required): A string that is the key for one of the entries in the storage object. This + is used to determine what storage config will be used. + ## Complex types ### document diff --git a/examples-staging/assets-cloud/.env.example b/examples-staging/assets-cloud/.env.example deleted file mode 100644 index 07ed6333554..00000000000 --- a/examples-staging/assets-cloud/.env.example +++ /dev/null @@ -1,4 +0,0 @@ -KEYSTONE_CLOUD_API_KEY=KEYSTONE_CLOUD_API_KEY -KEYSTONE_CLOUD_IMAGES_DOMAIN=KEYSTONE_CLOUD_IMAGES_DOMAIN -KEYSTONE_CLOUD_GRAPHQL_API_ENDPOINT=KEYSTONE_CLOUD_GRAPHQL_API_ENDPOINT -KEYSTONE_CLOUD_REST_API_ENDPOINT=KEYSTONE_CLOUD_REST_API_ENDPOINT \ No newline at end of file diff --git a/examples-staging/assets-cloud/CHANGELOG.md b/examples-staging/assets-cloud/CHANGELOG.md deleted file mode 100644 index 0d6135b0405..00000000000 --- a/examples-staging/assets-cloud/CHANGELOG.md +++ /dev/null @@ -1,115 +0,0 @@ -# @keystone-6/example-assets-cloud - -## 0.0.2 - -### Patch Changes - -- [`aa80425a2`](https://github.com/keystonejs/keystone/commit/aa80425a24ccb7768ae516ab3252fcea49361da9) Thanks [@MurzNN](https://github.com/MurzNN)! - Added sandbox configs to allow launching sandboxes on codesandbox.io service - -- Updated dependencies [[`bb60d9a68`](https://github.com/keystonejs/keystone/commit/bb60d9a68ee611011ca0aea2ce45b052ad49517d), [`aced61816`](https://github.com/keystonejs/keystone/commit/aced6181646bd6fc94977ea497801e6d3839f9c0), [`3bb1a5343`](https://github.com/keystonejs/keystone/commit/3bb1a53434b86e8a6294cff01a8699c36dd5df5a), [`0260a30c9`](https://github.com/keystonejs/keystone/commit/0260a30c92a059268cb6bf8de8a077847c7cdd96), [`33fde0a26`](https://github.com/keystonejs/keystone/commit/33fde0a26d23b8ae3b5907abec70704a1c970547), [`20095f04b`](https://github.com/keystonejs/keystone/commit/20095f04be02592da99503d9b54b726d66040e77), [`06feba78b`](https://github.com/keystonejs/keystone/commit/06feba78bda6743bc4a7d8b56305fb905bc2af95), [`b6f571a73`](https://github.com/keystonejs/keystone/commit/b6f571a7310af480be64af56fdc0732a7ebfe3f4), [`62201dd5f`](https://github.com/keystonejs/keystone/commit/62201dd5fcea0fe4cf95c33527c394ab65ddce7d)]: - - @keystone-6/core@1.1.1 - -## 0.0.1 - -### Patch Changes - -- Updated dependencies [[`7dddbe0fd`](https://github.com/keystonejs/keystone/commit/7dddbe0fd5b42a2596ba4dc0bbe1813cb54571c7), [`fb7844ab5`](https://github.com/keystonejs/keystone/commit/fb7844ab50c1d4a6d14b2ad46a568665f6661921), [`3c7a581c1`](https://github.com/keystonejs/keystone/commit/3c7a581c1e53ae49c9f74509de3927ebf2703bde), [`f4554980f`](https://github.com/keystonejs/keystone/commit/f4554980f6243a6545eee6c887d946ff25cd90e3)]: - - @keystone-6/core@1.0.0 - -## 2.0.0 - -### Major Changes - -- [#6957](https://github.com/keystonejs/keystone/pull/6957) [`de8cf44e7`](https://github.com/keystonejs/keystone/commit/de8cf44e7b328ab98e1466d7191d9ee65a57b02a) Thanks [@bladey](https://github.com/bladey)! - Update Node engines to support current Node LTS versions, currently versions 14 and 16. - -### Patch Changes - -- Updated dependencies [[`f2b41df9f`](https://github.com/keystonejs/keystone/commit/f2b41df9f77cf340e5e138cf60bacd6aec8e4548), [`04c54a4eb`](https://github.com/keystonejs/keystone/commit/04c54a4eb4aa6076cf87d441060eaa2091bc903b), [`748538649`](https://github.com/keystonejs/keystone/commit/748538649645d3b0ef32b0baba8fa310f2a493fe), [`4e96c23bb`](https://github.com/keystonejs/keystone/commit/4e96c23bb6c3a134f1324ec7879adac3abf90132), [`76ec35c97`](https://github.com/keystonejs/keystone/commit/76ec35c97a72dcb023e1b0da5b47e876896b6a03), [`760ae82ac`](https://github.com/keystonejs/keystone/commit/760ae82ac0fac5f73e123e2b36f7ba6320312ca6), [`0a7b75838`](https://github.com/keystonejs/keystone/commit/0a7b7583887e3811c23b0b74f4f97633fd484e08), [`622e57689`](https://github.com/keystonejs/keystone/commit/622e57689cf27dbecba7f64c02f0a3b6499d3218), [`bbedee845`](https://github.com/keystonejs/keystone/commit/bbedee84541d22c91a6816872902f6cce8e6aee3), [`82539faa5`](https://github.com/keystonejs/keystone/commit/82539faa53c495be1f5f470deb9eae9861cd31a0), [`760ae82ac`](https://github.com/keystonejs/keystone/commit/760ae82ac0fac5f73e123e2b36f7ba6320312ca6), [`96fd2e220`](https://github.com/keystonejs/keystone/commit/96fd2e22041de84a042f5a0df2cab75ba0dacc35), [`04c54a4eb`](https://github.com/keystonejs/keystone/commit/04c54a4eb4aa6076cf87d441060eaa2091bc903b), [`82539faa5`](https://github.com/keystonejs/keystone/commit/82539faa53c495be1f5f470deb9eae9861cd31a0), [`de8cf44e7`](https://github.com/keystonejs/keystone/commit/de8cf44e7b328ab98e1466d7191d9ee65a57b02a), [`7a7450009`](https://github.com/keystonejs/keystone/commit/7a7450009d68f70173a2af55eb3a845ea3799c99)]: - - @keystone-next/keystone@29.0.0 - -## 1.0.9 - -### Patch Changes - -- Updated dependencies [[`70eb86237`](https://github.com/keystonejs/keystone/commit/70eb86237bd3eafd36b0579f66ad3f1e173357b1), [`990b56291`](https://github.com/keystonejs/keystone/commit/990b56291e677077656b201b935086754c6257f1), [`b981f4c3e`](https://github.com/keystonejs/keystone/commit/b981f4c3ee135a1184188deb5ed8de22f718080c)]: - - @keystone-next/keystone@28.0.0 - -## 1.0.8 - -### Patch Changes - -- Updated dependencies [[`f3e8aac31`](https://github.com/keystonejs/keystone/commit/f3e8aac31efb3eb1573eb340e07a25920084a4aa), [`ddabdbd02`](https://github.com/keystonejs/keystone/commit/ddabdbd02230374ff921998f9d21c0ad7d32b226), [`71600965b`](https://github.com/keystonejs/keystone/commit/71600965b963e098ca77ae1261b850b9573c9f22), [`d9e1ba8fa`](https://github.com/keystonejs/keystone/commit/d9e1ba8fa23c0d9e902ef61167913ee92f5657cb), [`71600965b`](https://github.com/keystonejs/keystone/commit/71600965b963e098ca77ae1261b850b9573c9f22), [`5e61e0050`](https://github.com/keystonejs/keystone/commit/5e61e00503715f0f634d97e573926091a52661e6), [`f38772b27`](https://github.com/keystonejs/keystone/commit/f38772b27d3e9d157127dabfa40036462c235a9f), [`30fc08b51`](https://github.com/keystonejs/keystone/commit/30fc08b515e4f8851fd2583a265a813c683bf604), [`f683dcfe3`](https://github.com/keystonejs/keystone/commit/f683dcfe37d013b3d17f1fbad3df335b2f2ee51c), [`c0661b8ee`](https://github.com/keystonejs/keystone/commit/c0661b8ee9e16a1ffdd7fc77c9c56fead0efda36), [`db7f2311b`](https://github.com/keystonejs/keystone/commit/db7f2311bb2ff8e1e70350cd0f087439b8404a8a), [`023bc7a0b`](https://github.com/keystonejs/keystone/commit/023bc7a0b1e6fb0ebdc5055f0243d9dad255a667), [`cbb9df927`](https://github.com/keystonejs/keystone/commit/cbb9df927a0f106aaa35d107961a405b0d08a751), [`d1141ea82`](https://github.com/keystonejs/keystone/commit/d1141ea8235bca4ce88500991c24b962b06ade45), [`cbb9df927`](https://github.com/keystonejs/keystone/commit/cbb9df927a0f106aaa35d107961a405b0d08a751), [`ddabdbd02`](https://github.com/keystonejs/keystone/commit/ddabdbd02230374ff921998f9d21c0ad7d32b226), [`71600965b`](https://github.com/keystonejs/keystone/commit/71600965b963e098ca77ae1261b850b9573c9f22), [`d107a5bec`](https://github.com/keystonejs/keystone/commit/d107a5becdd16245caf208c3979965fa926e484c), [`44cbef543`](https://github.com/keystonejs/keystone/commit/44cbef5435081311acb9e68dd750f1ca289b8221), [`dcf5241d8`](https://github.com/keystonejs/keystone/commit/dcf5241d8e3e62b080842a5d4bfd47a7f2cce5ca)]: - - @keystone-next/keystone@27.0.0 - -## 1.0.7 - -### Patch Changes - -- [#6744](https://github.com/keystonejs/keystone/pull/6744) [`0ef1ee3cc`](https://github.com/keystonejs/keystone/commit/0ef1ee3ccd99f0f3e1f955f03d00b1a0f238c7cd) Thanks [@bladey](https://github.com/bladey)! - Renamed branch `master` to `main`. - -- Updated dependencies [[`73544fd19`](https://github.com/keystonejs/keystone/commit/73544fd19b865be9fbf3ea9ae68fae5f039eb13f), [`0ef1ee3cc`](https://github.com/keystonejs/keystone/commit/0ef1ee3ccd99f0f3e1f955f03d00b1a0f238c7cd), [`930b7129f`](https://github.com/keystonejs/keystone/commit/930b7129f37beb396bb8ecc8a8dc9f1b3615a7e0), [`fac96cbd1`](https://github.com/keystonejs/keystone/commit/fac96cbd14febcc01bdffbecd1aceee391f6a20a), [`3d289eb3d`](https://github.com/keystonejs/keystone/commit/3d289eb3d00c3e6a0c26ce962fb0f942a08c400a), [`bed3a560a`](https://github.com/keystonejs/keystone/commit/bed3a560a59d4fe787f3beebd65f8148453aae35), [`930b7129f`](https://github.com/keystonejs/keystone/commit/930b7129f37beb396bb8ecc8a8dc9f1b3615a7e0), [`6e4a0cf56`](https://github.com/keystonejs/keystone/commit/6e4a0cf56ce35b2446db7970763c55446de3db0e), [`d64bd4a7f`](https://github.com/keystonejs/keystone/commit/d64bd4a7f3da87e13e9cac41f0eb9757b771835f), [`abeceaf90`](https://github.com/keystonejs/keystone/commit/abeceaf902c231aabe9cf3a383ecf29c09b8f4dd), [`704f68b38`](https://github.com/keystonejs/keystone/commit/704f68b38f970860137380e21c36e04d2c51a7a4), [`576f341e6`](https://github.com/keystonejs/keystone/commit/576f341e61b31bbcf076ba70002d137c7b7ff9a9)]: - - @keystone-next/keystone@26.1.0 - -## 1.0.6 - -### Patch Changes - -- Updated dependencies [[`5c0163e09`](https://github.com/keystonejs/keystone/commit/5c0163e0973e5fee9b1e2c2b1f2834284858a509), [`7f5caff60`](https://github.com/keystonejs/keystone/commit/7f5caff60308112ded832db4703f33eaae00ce24), [`480c875d1`](https://github.com/keystonejs/keystone/commit/480c875d11700f9eb23f403a5bb277aa94c38ce7), [`3ece149e5`](https://github.com/keystonejs/keystone/commit/3ece149e53066661c57c56fdd1467003c5b11c06), [`d0e3c087e`](https://github.com/keystonejs/keystone/commit/d0e3c087e49310774b9538dfa5d2432c00381db0), [`21c5d1aa9`](https://github.com/keystonejs/keystone/commit/21c5d1aa964a19657d4ba7eb913e8ca292bf1714), [`8bbba49c7`](https://github.com/keystonejs/keystone/commit/8bbba49c74fd4b7cf2560613c9cf6bcaddb11a6f), [`42268ee72`](https://github.com/keystonejs/keystone/commit/42268ee72707e94a6197607d24534a438b748649), [`d9e18613a`](https://github.com/keystonejs/keystone/commit/d9e18613a4136f1c1201a197e47d9d4bde292cd2), [`e81947d6c`](https://github.com/keystonejs/keystone/commit/e81947d6ccb0b541387519898fdbbf09274d4c9f), [`5d3fc0b77`](https://github.com/keystonejs/keystone/commit/5d3fc0b77c92efc69d725f943626d8d76a28e799), [`3cfc2a383`](https://github.com/keystonejs/keystone/commit/3cfc2a3839142dd3ccdbf1dd86768257e9acc0dc), [`1da120a38`](https://github.com/keystonejs/keystone/commit/1da120a388a80585e897a06b81b027b7d8011902), [`499c00b44`](https://github.com/keystonejs/keystone/commit/499c00b44b4b378285ed21a385da799b4af0af82), [`eb1a89f3c`](https://github.com/keystonejs/keystone/commit/eb1a89f3c13d4e80516cc372cef3dc505ef864f3), [`4da935870`](https://github.com/keystonejs/keystone/commit/4da935870374414e83900949cc70fce0d4b6de19), [`1faddea9d`](https://github.com/keystonejs/keystone/commit/1faddea9d285c70d2d867958bc5ab2bbfb44dbd6), [`7de13bce3`](https://github.com/keystonejs/keystone/commit/7de13bce32630ee2478a9894e801020c520c64a9), [`271e5d97b`](https://github.com/keystonejs/keystone/commit/271e5d97bc2e4548ce039a568278f9f7569aa41a), [`0218a4215`](https://github.com/keystonejs/keystone/commit/0218a421576fb3ceb38eb5f38223a9ef0af4c4d2), [`273ee446a`](https://github.com/keystonejs/keystone/commit/273ee446a6d3e22c4d01c530d33282df362a6f1b), [`14bfa8a9b`](https://github.com/keystonejs/keystone/commit/14bfa8a9b33fae4c5eb3664ca23bb88850df5e50), [`8bbba49c7`](https://github.com/keystonejs/keystone/commit/8bbba49c74fd4b7cf2560613c9cf6bcaddb11a6f), [`a645861a9`](https://github.com/keystonejs/keystone/commit/a645861a9562748cf3e9786e37acea67c4a0cc17), [`581e130cf`](https://github.com/keystonejs/keystone/commit/581e130cf2a833c2b363801a32f4791bc1c7c62c), [`689d8ecaa`](https://github.com/keystonejs/keystone/commit/689d8ecaa9e93eedc80084aafc319a0396efc593), [`144f7f8e4`](https://github.com/keystonejs/keystone/commit/144f7f8e4e13ec547865927cb224fea7165b98b7), [`f963966ab`](https://github.com/keystonejs/keystone/commit/f963966ab138a315a8f18d83ed7a676f7423a51d), [`b76974736`](https://github.com/keystonejs/keystone/commit/b76974736132a71d693b3e325ffa009d395840a4), [`47c8b53ce`](https://github.com/keystonejs/keystone/commit/47c8b53ce44b7ad34ba40501a257a2b679cdee05), [`a95da1d81`](https://github.com/keystonejs/keystone/commit/a95da1d812574fd17d1fa8bc324415da558a9d9d), [`1b0a2f516`](https://github.com/keystonejs/keystone/commit/1b0a2f516d7d9ffce2e470dcd9ea870a3274500b), [`7621d0db7`](https://github.com/keystonejs/keystone/commit/7621d0db75033b68a510d5f6c9b03d9418980e73), [`67492f37d`](https://github.com/keystonejs/keystone/commit/67492f37dd9fbcd94234c15a072e9c826fa7a665), [`002e1d88b`](https://github.com/keystonejs/keystone/commit/002e1d88b0908c2e1215c1181724b2bc1cc57538), [`ca48072b4`](https://github.com/keystonejs/keystone/commit/ca48072b4d137e879e328c93b703a8364562db8a), [`10c61bd44`](https://github.com/keystonejs/keystone/commit/10c61bd44176ffa7d0e446c28fd9f12ed54790f0), [`1659e1fe5`](https://github.com/keystonejs/keystone/commit/1659e1fe5e0f394df058b3a773ea62bf392fa8db), [`3b9732acd`](https://github.com/keystonejs/keystone/commit/3b9732acd8cd597fa9c70128a2e7129ed02e6775), [`c2b124f8e`](https://github.com/keystonejs/keystone/commit/c2b124f8e4b283022ec473d9e5f32f37de639cf0), [`4048991ba`](https://github.com/keystonejs/keystone/commit/4048991ba7db234a694287000beaf2ea052cd24e), [`79e2cc3aa`](https://github.com/keystonejs/keystone/commit/79e2cc3aa79a90358a6ce1281a8ad5f5632ac185), [`1f952fb10`](https://github.com/keystonejs/keystone/commit/1f952fb10710b7fae6a88112310b25a09ab330ea), [`1b0a2f516`](https://github.com/keystonejs/keystone/commit/1b0a2f516d7d9ffce2e470dcd9ea870a3274500b), [`4e485a914`](https://github.com/keystonejs/keystone/commit/4e485a914cfbc6c4b5ef9eeca9157bf654469b2d), [`3ee4542a8`](https://github.com/keystonejs/keystone/commit/3ee4542a884d8135299178950ab47bb82907bcd9), [`e84f8f655`](https://github.com/keystonejs/keystone/commit/e84f8f6550cff4fbca69982e0371d787e67c8915), [`ca48072b4`](https://github.com/keystonejs/keystone/commit/ca48072b4d137e879e328c93b703a8364562db8a), [`e747ef6f3`](https://github.com/keystonejs/keystone/commit/e747ef6f31590799fa332e1f011b160a443fbeb4), [`5e62702ba`](https://github.com/keystonejs/keystone/commit/5e62702ba3934bf8effb5dce65466017dd868610), [`b00596d3f`](https://github.com/keystonejs/keystone/commit/b00596d3f8b64cddc46ec9e5e4e567dd67264253), [`80cd31303`](https://github.com/keystonejs/keystone/commit/80cd313033b339d90b5e640b252a357a4d60fbcd), [`c8aca958b`](https://github.com/keystonejs/keystone/commit/c8aca958b3650f10011370e0c00b01cb681bb212), [`232c512a0`](https://github.com/keystonejs/keystone/commit/232c512a05250cb8a9c26b70969afe4106e2f8ac), [`8631917d1`](https://github.com/keystonejs/keystone/commit/8631917d14778468652abb8eda06802d2469646c), [`b6c8c3bff`](https://github.com/keystonejs/keystone/commit/b6c8c3bff9d3d98f743c47c015ae27e63db0271e), [`bf5874411`](https://github.com/keystonejs/keystone/commit/bf58744118320493325b3b48aadd007e12d5c680), [`398c08529`](https://github.com/keystonejs/keystone/commit/398c085295d992658a9e7e22aae037f55528c258), [`47cee8c95`](https://github.com/keystonejs/keystone/commit/47cee8c952c1134e503bff54e61dcd48c76b5429), [`9f0a4cc1f`](https://github.com/keystonejs/keystone/commit/9f0a4cc1f6d5133e92a0d326e285152d18689173), [`838845298`](https://github.com/keystonejs/keystone/commit/8388452982277b10c65ff89be442464761a680a7), [`11fb46c91`](https://github.com/keystonejs/keystone/commit/11fb46c918e508cc182d5bd22f069b9329edadba)]: - - @keystone-next/keystone@26.0.0 - -## 1.0.5 - -### Patch Changes - -- [#6432](https://github.com/keystonejs/keystone/pull/6432) [`0a189d5d0`](https://github.com/keystonejs/keystone/commit/0a189d5d0e618ee5598e9beaccea0290d2a3f8d9) Thanks [@renovate](https://github.com/apps/renovate)! - Updated `typescript` dependency to `^4.4.2`. - -- Updated dependencies [[`2a901a121`](https://github.com/keystonejs/keystone/commit/2a901a1210a0b3de0ccd22ca93e9cbcc8ed0f951), [`3008c5110`](https://github.com/keystonejs/keystone/commit/3008c5110a0ebc524eb3609bd8ba901f664f83d3), [`3904a9cf7`](https://github.com/keystonejs/keystone/commit/3904a9cf73e16ef192faae833f2f39ed05f2d707), [`32f024738`](https://github.com/keystonejs/keystone/commit/32f0247384ecf3bce5c3ef14ad8d367c9888459f), [`2e3f3666b`](https://github.com/keystonejs/keystone/commit/2e3f3666b5340b8eb778104a1d4a3f4d52be6528), [`44f2ef60e`](https://github.com/keystonejs/keystone/commit/44f2ef60e29912f3c85b91fc704e09a7d5a15b22), [`9651aff8e`](https://github.com/keystonejs/keystone/commit/9651aff8eb9a51c0fbda6f51b1be0fedb07571da), [`9c5991f43`](https://github.com/keystonejs/keystone/commit/9c5991f43e8f909e576f6b51fd87aab3bbead504), [`069265b9c`](https://github.com/keystonejs/keystone/commit/069265b9cdd5898f4501535793f56debaa247c1c), [`4f36a81af`](https://github.com/keystonejs/keystone/commit/4f36a81afb03591354acc1d0141eff8fe54ff208), [`c76bfc0a2`](https://github.com/keystonejs/keystone/commit/c76bfc0a2ad5aeffb68b8d2006225f608e855a19), [`bc9088f05`](https://github.com/keystonejs/keystone/commit/bc9088f0574af27be6a068483a789a80f7a46a41), [`ee54522d5`](https://github.com/keystonejs/keystone/commit/ee54522d513a9376c1ed1e472a7ff91657e4e693), [`32f024738`](https://github.com/keystonejs/keystone/commit/32f0247384ecf3bce5c3ef14ad8d367c9888459f), [`bd120c7c2`](https://github.com/keystonejs/keystone/commit/bd120c7c296c9adaaefe9bf93cbb384cc7528715), [`595922b48`](https://github.com/keystonejs/keystone/commit/595922b48c909053fa9d34bb1c42177ad41c72d5), [`8f2786535`](https://github.com/keystonejs/keystone/commit/8f2786535272976678427fd13758e63b2c59d955), [`b3eefc1c3`](https://github.com/keystonejs/keystone/commit/b3eefc1c336a9a366c39f7aa2cf5251baaf843fd), [`0aa02a333`](https://github.com/keystonejs/keystone/commit/0aa02a333d989c30647cd10e25325d4d2db61be6), [`bf9b5605f`](https://github.com/keystonejs/keystone/commit/bf9b5605fc684975d9e2cad604c8e0d978eac40a), [`3957c0981`](https://github.com/keystonejs/keystone/commit/3957c098131b3b055cb94b07f1ce55ec82640908), [`af5e59bf4`](https://github.com/keystonejs/keystone/commit/af5e59bf4215aa297495ae603239b1e3510be39b), [`cbc5a68aa`](https://github.com/keystonejs/keystone/commit/cbc5a68aa7547ea55d1254ee5c3b1e543cdc78e2), [`32f024738`](https://github.com/keystonejs/keystone/commit/32f0247384ecf3bce5c3ef14ad8d367c9888459f), [`783290796`](https://github.com/keystonejs/keystone/commit/78329079606d74a2eedd63f96a985116bf0b449c), [`0a189d5d0`](https://github.com/keystonejs/keystone/commit/0a189d5d0e618ee5598e9beaccea0290d2a3f8d9), [`944bce1e8`](https://github.com/keystonejs/keystone/commit/944bce1e834be4d0f4c79f35cd53ccbabb92f555), [`e0f935eb2`](https://github.com/keystonejs/keystone/commit/e0f935eb2ef8ac311a43423c6691e56cd27b6bed), [`2324fa027`](https://github.com/keystonejs/keystone/commit/2324fa027a6c2beabef4724c69a9ad05338a0cf3), [`f2311781a`](https://github.com/keystonejs/keystone/commit/f2311781a990c0ccd3302ac8e7aa889138f70e47), [`88b03bd79`](https://github.com/keystonejs/keystone/commit/88b03bd79112c7d8f0d41c592c8bd4bb226f5f71), [`0aa02a333`](https://github.com/keystonejs/keystone/commit/0aa02a333d989c30647cd10e25325d4d2db61be6), [`5ceccd821`](https://github.com/keystonejs/keystone/commit/5ceccd821b513e2abec3eb24278e7c30bdcdf6d6), [`fd744dcaa`](https://github.com/keystonejs/keystone/commit/fd744dcaa513efb2a8ae954bb2d5d1fa7f0723d6), [`489e128fe`](https://github.com/keystonejs/keystone/commit/489e128fe0835968eda0908b199a8867c0e72a5b), [`bb0c6c626`](https://github.com/keystonejs/keystone/commit/bb0c6c62610eda20ae93a6b67185276bdbba3248)]: - - @keystone-next/keystone@25.0.0 - -## 1.0.4 - -### Patch Changes - -- Updated dependencies [[`e9f3c42d5`](https://github.com/keystonejs/keystone/commit/e9f3c42d5b9d42872cecbd18fbe9bf9d7d53ed82), [`5cd8ffd6c`](https://github.com/keystonejs/keystone/commit/5cd8ffd6cb822dbee8555b47846a5019c4d2b1c3), [`1cbcf54cb`](https://github.com/keystonejs/keystone/commit/1cbcf54cb1206461866b582865e3b1a8fc728f18), [`a92169d04`](https://github.com/keystonejs/keystone/commit/a92169d04e5a1a98deb8e757b8eae3b06fc66450), [`5cd8ffd6c`](https://github.com/keystonejs/keystone/commit/5cd8ffd6cb822dbee8555b47846a5019c4d2b1c3), [`b696a9579`](https://github.com/keystonejs/keystone/commit/b696a9579b503db86f42776381e247c4e1a7409f), [`f3014a627`](https://github.com/keystonejs/keystone/commit/f3014a627060c7cd86440a6937da5caecfd023a0), [`092df6678`](https://github.com/keystonejs/keystone/commit/092df6678cea18d639be16ad250ec4ecc9250f5a), [`5cd8ffd6c`](https://github.com/keystonejs/keystone/commit/5cd8ffd6cb822dbee8555b47846a5019c4d2b1c3), [`6da56b80e`](https://github.com/keystonejs/keystone/commit/6da56b80e03c748a621afcca6c1ec2887fef7271), [`4f4f0351a`](https://github.com/keystonejs/keystone/commit/4f4f0351a056dea9d1614aa2a3a4789d66bb402d), [`697efa354`](https://github.com/keystonejs/keystone/commit/697efa354b1066b3d4b6eb757ca704b458f45e93), [`c7e331d90`](https://github.com/keystonejs/keystone/commit/c7e331d90a28b2ed8236100097cb8d34a11fabe2), [`3a7a06b2c`](https://github.com/keystonejs/keystone/commit/3a7a06b2cc6b5ea157d34d925b15494b471899eb), [`272b97b3a`](https://github.com/keystonejs/keystone/commit/272b97b3a10c0dfada782171d55ef7ac6f47c98f), [`78dac764e`](https://github.com/keystonejs/keystone/commit/78dac764e1860b33f9e2bd8cee6015abeaaa5ec4), [`399561b27`](https://github.com/keystonejs/keystone/commit/399561b2769ddd8f3d3fdf29838f5784404bb053), [`9d361c1c8`](https://github.com/keystonejs/keystone/commit/9d361c1c8625e1390f837b7318b63547d686a63b), [`0dcb1c95b`](https://github.com/keystonejs/keystone/commit/0dcb1c95b5200750cc8649485425f2ae40d023a3), [`94435ffee`](https://github.com/keystonejs/keystone/commit/94435ffee765824091899242e4a2f73c7356b524), [`5cd8ffd6c`](https://github.com/keystonejs/keystone/commit/5cd8ffd6cb822dbee8555b47846a5019c4d2b1c3), [`56044e2a4`](https://github.com/keystonejs/keystone/commit/56044e2a425f4256b66475fd3b1a6342cd6c3bf9), [`f46fd32b7`](https://github.com/keystonejs/keystone/commit/f46fd32b7047dbb5ea2566859f7ecee8db5b0b15), [`874f2c405`](https://github.com/keystonejs/keystone/commit/874f2c4058c9cf006213e84b9ffcf39c5bf144e8), [`8ea4eed55`](https://github.com/keystonejs/keystone/commit/8ea4eed55367aaa213f6b4ffb7473087498e39ae), [`e3fe6498d`](https://github.com/keystonejs/keystone/commit/e3fe6498dc36203d8080dff3c2e0c25f6c98733e), [`1030296d1`](https://github.com/keystonejs/keystone/commit/1030296d1f304dc44246e895089ac1f992e80590), [`3564b342d`](https://github.com/keystonejs/keystone/commit/3564b342d6dc2127ae591d7ac055af9eae90543c), [`8b2d179b2`](https://github.com/keystonejs/keystone/commit/8b2d179b2463d78b082182ca9afa8233109e0ba3), [`e3fefafcc`](https://github.com/keystonejs/keystone/commit/e3fefafcce6f8bf836c9bf0f4d931b8200ba41c7), [`4d9f89f88`](https://github.com/keystonejs/keystone/commit/4d9f89f884e2bf984fdd74ca2cbb7874b25b9cda), [`686c0f1c4`](https://github.com/keystonejs/keystone/commit/686c0f1c4a1feb609e1584aa71738709bbbf984e), [`d214e2f72`](https://github.com/keystonejs/keystone/commit/d214e2f72bae1c798e2415a38410d6063c333e2e), [`f5e64af37`](https://github.com/keystonejs/keystone/commit/f5e64af37df2eb460c89d89fa3c8924fb34970ed)]: - - @keystone-next/fields@14.0.0 - - @keystone-next/keystone@24.0.0 - -## 1.0.3 - -### Patch Changes - -- Updated dependencies [[`3f03b8c1f`](https://github.com/keystonejs/keystone/commit/3f03b8c1fa7005b37371e1cc401c3a03334a4f7a), [`ea0712aa2`](https://github.com/keystonejs/keystone/commit/ea0712aa22487325bd898818ea4fbca543c9dcf1), [`93f1e5d30`](https://github.com/keystonejs/keystone/commit/93f1e5d302701c610b6cba74e0c5c86a3ac8aacc), [`9e2deac5f`](https://github.com/keystonejs/keystone/commit/9e2deac5f340b4baeb03b01ae065f2bec5977523), [`7716315ea`](https://github.com/keystonejs/keystone/commit/7716315ea823dd91d17d54dcbb9155b5445cd956), [`a11e54d69`](https://github.com/keystonejs/keystone/commit/a11e54d692d3cec4ec2439cbf743b590688fb7d3), [`e5f61ad50`](https://github.com/keystonejs/keystone/commit/e5f61ad50133a328fcb32299b838fd9eac574c3f), [`e4e6cf9b5`](https://github.com/keystonejs/keystone/commit/e4e6cf9b59eec461d2b53acfa3b350e4f5a06fc4), [`2ef6fe82c`](https://github.com/keystonejs/keystone/commit/2ef6fe82cee6df7796935d35d1c12cab29aecc75), [`dd7e811e7`](https://github.com/keystonejs/keystone/commit/dd7e811e7ce084c1e832acefc6ed773af371ac9e), [`587a8d0b0`](https://github.com/keystonejs/keystone/commit/587a8d0b074ccecb239d120275359f72779f306f), [`597edbdd8`](https://github.com/keystonejs/keystone/commit/597edbdd81df80982dd3df3d9d600003ef8a15e9), [`1172e1853`](https://github.com/keystonejs/keystone/commit/1172e18531064df6412c06412e74da3b85740b35), [`fbe698461`](https://github.com/keystonejs/keystone/commit/fbe6984616de7a302db7c2b0082851db89c2e314), [`32e9879db`](https://github.com/keystonejs/keystone/commit/32e9879db9cfee77f067eb8105262df65bca6c06)]: - - @keystone-next/keystone@23.0.0 - - @keystone-next/fields@13.0.0 - -## 1.0.2 - -### Patch Changes - -- Updated dependencies [[`38b78f2ae`](https://github.com/keystonejs/keystone/commit/38b78f2aeaf4c5d8176a1751ad8cb5a7acce2790), [`139d7a8de`](https://github.com/keystonejs/keystone/commit/139d7a8def263d40c0d1d5353d2744842d9a0951), [`279403cb0`](https://github.com/keystonejs/keystone/commit/279403cb0b4bffb946763c9a7ef71be57478eeb3), [`253df44c2`](https://github.com/keystonejs/keystone/commit/253df44c2f8d6535a6425b2593eaed5380433d57), [`253df44c2`](https://github.com/keystonejs/keystone/commit/253df44c2f8d6535a6425b2593eaed5380433d57), [`f482db633`](https://github.com/keystonejs/keystone/commit/f482db6332e54a1d5cd469e2805b99b544208e83), [`c536b478f`](https://github.com/keystonejs/keystone/commit/c536b478fc89f2d933cddf8533e7d88030540a63)]: - - @keystone-next/fields@12.0.0 - - @keystone-next/keystone@22.0.0 - -## 1.0.1 - -### Patch Changes - -- Updated dependencies [[`03f535ba6`](https://github.com/keystonejs/keystone/commit/03f535ba6fa1a5e5f3027bcad761feb3fd94587b), [`03f535ba6`](https://github.com/keystonejs/keystone/commit/03f535ba6fa1a5e5f3027bcad761feb3fd94587b)]: - - @keystone-next/keystone@21.0.0 - - @keystone-next/fields@11.0.2 - -## 1.0.0 - -### Major Changes - -- [#5854](https://github.com/keystonejs/keystone/pull/5854) [`7eabb4dee`](https://github.com/keystonejs/keystone/commit/7eabb4dee2552f7baf1e0024d82011b179d418d4) Thanks [@rohan-deshpande](https://github.com/rohan-deshpande)! - Initial version of the `assets-cloud` example. - -### Minor Changes - -- [#5868](https://github.com/keystonejs/keystone/pull/5868) [`84a5e7f3b`](https://github.com/keystonejs/keystone/commit/84a5e7f3bc3a29ff31d642831e7aaadfc8534ba1) Thanks [@rohan-deshpande](https://github.com/rohan-deshpande)! - Added experimental support for the integration with keystone cloud files - -### Patch Changes - -- Updated dependencies [[`a3b07ea16`](https://github.com/keystonejs/keystone/commit/a3b07ea16ffc0f6741c0c0e5e281622a1831e0e7), [`7eabb4dee`](https://github.com/keystonejs/keystone/commit/7eabb4dee2552f7baf1e0024d82011b179d418d4), [`5227234a0`](https://github.com/keystonejs/keystone/commit/5227234a08edd99cd2795c8d888fbb3022810f54), [`a3b07ea16`](https://github.com/keystonejs/keystone/commit/a3b07ea16ffc0f6741c0c0e5e281622a1831e0e7), [`e4c19f808`](https://github.com/keystonejs/keystone/commit/e4c19f8086cc14f7f4a8ef390f1f4e1263004d40), [`4995c682d`](https://github.com/keystonejs/keystone/commit/4995c682dbdcfac2100de9fab98ba1e0e08cbcc2), [`a3b07ea16`](https://github.com/keystonejs/keystone/commit/a3b07ea16ffc0f6741c0c0e5e281622a1831e0e7), [`a3b07ea16`](https://github.com/keystonejs/keystone/commit/a3b07ea16ffc0f6741c0c0e5e281622a1831e0e7), [`a3b07ea16`](https://github.com/keystonejs/keystone/commit/a3b07ea16ffc0f6741c0c0e5e281622a1831e0e7), [`a3b07ea16`](https://github.com/keystonejs/keystone/commit/a3b07ea16ffc0f6741c0c0e5e281622a1831e0e7), [`a3b07ea16`](https://github.com/keystonejs/keystone/commit/a3b07ea16ffc0f6741c0c0e5e281622a1831e0e7), [`881c9ffb7`](https://github.com/keystonejs/keystone/commit/881c9ffb7c5941e9fb214ed955148d8ea567e65f), [`ef14e77ce`](https://github.com/keystonejs/keystone/commit/ef14e77cebc9420db8c7d29dfe61f02140f4a705), [`df7d7b6f6`](https://github.com/keystonejs/keystone/commit/df7d7b6f6f2830573393560f4a1ec35234889947), [`a3b07ea16`](https://github.com/keystonejs/keystone/commit/a3b07ea16ffc0f6741c0c0e5e281622a1831e0e7), [`84a5e7f3b`](https://github.com/keystonejs/keystone/commit/84a5e7f3bc3a29ff31d642831e7aaadfc8534ba1), [`a3b07ea16`](https://github.com/keystonejs/keystone/commit/a3b07ea16ffc0f6741c0c0e5e281622a1831e0e7), [`a3b07ea16`](https://github.com/keystonejs/keystone/commit/a3b07ea16ffc0f6741c0c0e5e281622a1831e0e7), [`a3b07ea16`](https://github.com/keystonejs/keystone/commit/a3b07ea16ffc0f6741c0c0e5e281622a1831e0e7), [`a3b07ea16`](https://github.com/keystonejs/keystone/commit/a3b07ea16ffc0f6741c0c0e5e281622a1831e0e7), [`97fd5e05d`](https://github.com/keystonejs/keystone/commit/97fd5e05d8681bae86001e6b7e8e3f36ebd639b7), [`a3b07ea16`](https://github.com/keystonejs/keystone/commit/a3b07ea16ffc0f6741c0c0e5e281622a1831e0e7)]: - - @keystone-next/keystone@20.0.0 - - @keystone-next/fields@11.0.0 diff --git a/examples-staging/assets-cloud/keystone.ts b/examples-staging/assets-cloud/keystone.ts deleted file mode 100644 index d623c1d41c3..00000000000 --- a/examples-staging/assets-cloud/keystone.ts +++ /dev/null @@ -1,30 +0,0 @@ -import { config } from '@keystone-6/core'; -import dotenv from 'dotenv'; -import { lists } from './schema'; - -dotenv.config(); - -const { KEYSTONE_CLOUD_API_KEY = '' } = process.env; - -export default config({ - db: { - provider: 'sqlite', - url: process.env.DATABASE_URL || 'file:./keystone-example.db', - }, - lists, - images: { - upload: 'cloud', - local: { - storagePath: 'uploads/images', // defaults to 'public/images' - baseUrl: 'http://localhost:3000/images', // defaults to `/images` - }, - }, - files: { - upload: 'cloud', - }, - experimental: { - cloud: { - apiKey: KEYSTONE_CLOUD_API_KEY, - }, - }, -}); diff --git a/examples-staging/assets-cloud/sandbox.config.json b/examples-staging/assets-cloud/sandbox.config.json deleted file mode 100644 index e26e2dbe04f..00000000000 --- a/examples-staging/assets-cloud/sandbox.config.json +++ /dev/null @@ -1,7 +0,0 @@ -{ - "template": "node", - "container": { - "startScript": "keystone dev", - "node": "14" - } -} diff --git a/examples-staging/assets-local/keystone.ts b/examples-staging/assets-local/keystone.ts index d76e3c40100..6b32427116a 100644 --- a/examples-staging/assets-local/keystone.ts +++ b/examples-staging/assets-local/keystone.ts @@ -7,10 +7,24 @@ export default config({ url: process.env.DATABASE_URL || 'file:./keystone-example.db', }, lists, - images: { - upload: 'local', - }, - files: { - upload: 'local', + storage: { + my_images: { + kind: 'local', + type: 'image', + generateUrl: path => `http://localhost:3000/images${path}`, + serverRoute: { + path: '/images', + }, + storagePath: 'public/images', + }, + my_files: { + kind: 'local', + type: 'file', + generateUrl: path => `http://localhost:3000/files${path}`, + serverRoute: { + path: '/files', + }, + storagePath: 'public/files', + }, }, }); diff --git a/examples-staging/assets-local/schema.graphql b/examples-staging/assets-local/schema.graphql index 211aabb259a..12fb48fee41 100644 --- a/examples-staging/assets-local/schema.graphql +++ b/examples-staging/assets-local/schema.graphql @@ -20,13 +20,12 @@ enum PostStatusType { scalar DateTime @specifiedBy(url: "https://datatracker.ietf.org/doc/html/rfc3339#section-5.6") -interface ImageFieldOutput { +type ImageFieldOutput { id: ID! filesize: Int! width: Int! height: Int! extension: ImageExtension! - ref: String! url: String! } @@ -37,10 +36,9 @@ enum ImageExtension { gif } -interface FileFieldOutput { +type FileFieldOutput { filename: String! filesize: Int! - ref: String! url: String! } @@ -48,40 +46,6 @@ input PostWhereUniqueInput { id: ID } -type LocalImageFieldOutput implements ImageFieldOutput { - id: ID! - filesize: Int! - width: Int! - height: Int! - extension: ImageExtension! - ref: String! - url: String! -} - -type CloudImageFieldOutput implements ImageFieldOutput { - id: ID! - filesize: Int! - width: Int! - height: Int! - extension: ImageExtension! - ref: String! - url: String! -} - -type LocalFileFieldOutput implements FileFieldOutput { - filename: String! - filesize: Int! - ref: String! - url: String! -} - -type CloudFileFieldOutput implements FileFieldOutput { - filename: String! - filesize: Int! - ref: String! - url: String! -} - input PostWhereInput { AND: [PostWhereInput!] OR: [PostWhereInput!] @@ -181,8 +145,7 @@ input AuthorRelateToOneForUpdateInput { } input ImageFieldInput { - upload: Upload - ref: String + upload: Upload! } """ @@ -191,8 +154,7 @@ The `Upload` scalar type represents a file upload. scalar Upload input FileFieldInput { - upload: Upload - ref: String + upload: Upload! } input PostUpdateArgs { diff --git a/examples-staging/assets-local/schema.prisma b/examples-staging/assets-local/schema.prisma index cd3cc01a666..fa16d1e4471 100644 --- a/examples-staging/assets-local/schema.prisma +++ b/examples-staging/assets-local/schema.prisma @@ -23,10 +23,8 @@ model Post { hero_extension String? hero_width Int? hero_height Int? - hero_mode String? hero_id String? attachment_filesize Int? - attachment_mode String? attachment_filename String? @@index([authorId]) diff --git a/examples-staging/assets-local/schema.ts b/examples-staging/assets-local/schema.ts index a7f0a690c49..c409093e241 100644 --- a/examples-staging/assets-local/schema.ts +++ b/examples-staging/assets-local/schema.ts @@ -15,8 +15,8 @@ export const lists = { content: text(), publishDate: timestamp(), author: relationship({ ref: 'Author.posts', many: false }), - hero: image(), - attachment: file(), + hero: image({ storage: 'my_images' }), + attachment: file({ storage: 'my_files' }), }, }), Author: list({ diff --git a/examples-staging/assets-s3/.env.example b/examples-staging/assets-s3/.env.example new file mode 100644 index 00000000000..64d19f6c765 --- /dev/null +++ b/examples-staging/assets-s3/.env.example @@ -0,0 +1,4 @@ +S3_BUCKET_NAME=S3_BUCKET_NAME +S3_ACCESS_KEY_ID=S3_ACCESS_KEY_ID +S3_SECRET_ACCESS_KEY=S3_SECRET_ACCESS_KEY +S3_REGION=S3_REGION \ No newline at end of file diff --git a/examples-staging/assets-cloud/.gitignore b/examples-staging/assets-s3/.gitignore similarity index 100% rename from examples-staging/assets-cloud/.gitignore rename to examples-staging/assets-s3/.gitignore diff --git a/examples-staging/assets-s3/keystone.ts b/examples-staging/assets-s3/keystone.ts new file mode 100644 index 00000000000..b25d46b1797 --- /dev/null +++ b/examples-staging/assets-s3/keystone.ts @@ -0,0 +1,40 @@ +import { config } from '@keystone-6/core'; +import dotenv from 'dotenv'; +import { lists } from './schema'; + +dotenv.config(); + +const { + S3_BUCKET_NAME: bucketName = 'keystone-test', + S3_REGION: region = 'ap-southeast-2', + S3_ACCESS_KEY_ID: accessKeyId = 'keystone', + S3_SECRET_ACCESS_KEY: secretAccessKey = 'keystone', +} = process.env; + +export default config({ + db: { + provider: 'sqlite', + url: process.env.DATABASE_URL || 'file:./keystone-example.db', + }, + lists, + storage: { + my_images: { + kind: 's3', + type: 'image', + bucketName, + region, + accessKeyId, + secretAccessKey, + signed: { expiry: 5000 }, + }, + my_files: { + kind: 's3', + type: 'file', + bucketName, + region, + accessKeyId, + secretAccessKey, + signed: { expiry: 5000 }, + }, + }, +}); diff --git a/examples-staging/assets-cloud/package.json b/examples-staging/assets-s3/package.json similarity index 73% rename from examples-staging/assets-cloud/package.json rename to examples-staging/assets-s3/package.json index 9e78bebc8ee..8719bed5c54 100644 --- a/examples-staging/assets-cloud/package.json +++ b/examples-staging/assets-s3/package.json @@ -1,6 +1,6 @@ { - "name": "@keystone-6/example-assets-cloud", - "version": "0.0.2", + "name": "@keystone-6/example-assets-s3", + "version": "0.0.1", "private": true, "license": "MIT", "scripts": { @@ -9,7 +9,7 @@ "build": "keystone build" }, "dependencies": { - "@keystone-6/core": "^1.1.1", + "@keystone-6/core": "^1.0.0", "dotenv": "^16.0.0" }, "devDependencies": { @@ -18,5 +18,5 @@ "engines": { "node": "^14.15 || ^16.13" }, - "repository": "https://github.com/keystonejs/keystone/tree/main/examples-staging/assets-cloud" + "repository": "https://github.com/keystonejs/keystone/tree/main/examples-staging/assets-s3" } diff --git a/examples-staging/assets-cloud/schema.graphql b/examples-staging/assets-s3/schema.graphql similarity index 90% rename from examples-staging/assets-cloud/schema.graphql rename to examples-staging/assets-s3/schema.graphql index 211aabb259a..12fb48fee41 100644 --- a/examples-staging/assets-cloud/schema.graphql +++ b/examples-staging/assets-s3/schema.graphql @@ -20,13 +20,12 @@ enum PostStatusType { scalar DateTime @specifiedBy(url: "https://datatracker.ietf.org/doc/html/rfc3339#section-5.6") -interface ImageFieldOutput { +type ImageFieldOutput { id: ID! filesize: Int! width: Int! height: Int! extension: ImageExtension! - ref: String! url: String! } @@ -37,10 +36,9 @@ enum ImageExtension { gif } -interface FileFieldOutput { +type FileFieldOutput { filename: String! filesize: Int! - ref: String! url: String! } @@ -48,40 +46,6 @@ input PostWhereUniqueInput { id: ID } -type LocalImageFieldOutput implements ImageFieldOutput { - id: ID! - filesize: Int! - width: Int! - height: Int! - extension: ImageExtension! - ref: String! - url: String! -} - -type CloudImageFieldOutput implements ImageFieldOutput { - id: ID! - filesize: Int! - width: Int! - height: Int! - extension: ImageExtension! - ref: String! - url: String! -} - -type LocalFileFieldOutput implements FileFieldOutput { - filename: String! - filesize: Int! - ref: String! - url: String! -} - -type CloudFileFieldOutput implements FileFieldOutput { - filename: String! - filesize: Int! - ref: String! - url: String! -} - input PostWhereInput { AND: [PostWhereInput!] OR: [PostWhereInput!] @@ -181,8 +145,7 @@ input AuthorRelateToOneForUpdateInput { } input ImageFieldInput { - upload: Upload - ref: String + upload: Upload! } """ @@ -191,8 +154,7 @@ The `Upload` scalar type represents a file upload. scalar Upload input FileFieldInput { - upload: Upload - ref: String + upload: Upload! } input PostUpdateArgs { diff --git a/examples-staging/assets-cloud/schema.prisma b/examples-staging/assets-s3/schema.prisma similarity index 94% rename from examples-staging/assets-cloud/schema.prisma rename to examples-staging/assets-s3/schema.prisma index cd3cc01a666..fa16d1e4471 100644 --- a/examples-staging/assets-cloud/schema.prisma +++ b/examples-staging/assets-s3/schema.prisma @@ -23,10 +23,8 @@ model Post { hero_extension String? hero_width Int? hero_height Int? - hero_mode String? hero_id String? attachment_filesize Int? - attachment_mode String? attachment_filename String? @@index([authorId]) diff --git a/examples-staging/assets-cloud/schema.ts b/examples-staging/assets-s3/schema.ts similarity index 89% rename from examples-staging/assets-cloud/schema.ts rename to examples-staging/assets-s3/schema.ts index a7f0a690c49..c409093e241 100644 --- a/examples-staging/assets-cloud/schema.ts +++ b/examples-staging/assets-s3/schema.ts @@ -15,8 +15,8 @@ export const lists = { content: text(), publishDate: timestamp(), author: relationship({ ref: 'Author.posts', many: false }), - hero: image(), - attachment: file(), + hero: image({ storage: 'my_images' }), + attachment: file({ storage: 'my_files' }), }, }), Author: list({ diff --git a/examples-staging/basic/keystone.ts b/examples-staging/basic/keystone.ts index 50e605d3a40..eec9d32cf03 100644 --- a/examples-staging/basic/keystone.ts +++ b/examples-staging/basic/keystone.ts @@ -34,8 +34,26 @@ export default auth.withAuth( // path: '/admin', // isAccessAllowed, }, - images: { upload: 'local' }, - files: { upload: 'local' }, + storage: { + my_images: { + kind: 'local', + type: 'file', + storagePath: 'public/images', + generateUrl: path => `http://localhost:3000/images${path}`, + serverRoute: { + path: '/images', + }, + }, + my_files: { + kind: 'local', + type: 'file', + storagePath: 'public/files', + generateUrl: path => `http://localhost:3000/files${path}`, + serverRoute: { + path: '/files', + }, + }, + }, lists, extendGraphqlSchema, session: statelessSessions({ maxAge: sessionMaxAge, secret: sessionSecret }), diff --git a/examples-staging/basic/schema.graphql b/examples-staging/basic/schema.graphql index 58b60875d7e..acbb51c9188 100644 --- a/examples-staging/basic/schema.graphql +++ b/examples-staging/basic/schema.graphql @@ -96,13 +96,12 @@ type User { randomNumber: Float } -interface ImageFieldOutput { +type ImageFieldOutput { id: ID! filesize: Int! width: Int! height: Int! extension: ImageExtension! - ref: String! url: String! } @@ -113,10 +112,9 @@ enum ImageExtension { gif } -interface FileFieldOutput { +type FileFieldOutput { filename: String! filesize: Int! - ref: String! url: String! } @@ -129,40 +127,6 @@ input UserWhereUniqueInput { email: String } -type LocalImageFieldOutput implements ImageFieldOutput { - id: ID! - filesize: Int! - width: Int! - height: Int! - extension: ImageExtension! - ref: String! - url: String! -} - -type CloudImageFieldOutput implements ImageFieldOutput { - id: ID! - filesize: Int! - width: Int! - height: Int! - extension: ImageExtension! - ref: String! - url: String! -} - -type LocalFileFieldOutput implements FileFieldOutput { - filename: String! - filesize: Int! - ref: String! - url: String! -} - -type CloudFileFieldOutput implements FileFieldOutput { - filename: String! - filesize: Int! - ref: String! - url: String! -} - input UserWhereInput { AND: [UserWhereInput!] OR: [UserWhereInput!] @@ -263,8 +227,7 @@ input UserUpdateInput { } input ImageFieldInput { - upload: Upload - ref: String + upload: Upload! } """ @@ -273,8 +236,7 @@ The `Upload` scalar type represents a file upload. scalar Upload input FileFieldInput { - upload: Upload - ref: String + upload: Upload! } input PhoneNumberRelateToManyForUpdateInput { diff --git a/examples-staging/basic/schema.prisma b/examples-staging/basic/schema.prisma index 236f81c69c0..efde5c9025e 100644 --- a/examples-staging/basic/schema.prisma +++ b/examples-staging/basic/schema.prisma @@ -19,10 +19,8 @@ model User { avatar_extension String? avatar_width Int? avatar_height Int? - avatar_mode String? avatar_id String? attachment_filesize Int? - attachment_mode String? attachment_filename String? password String? isAdmin Boolean @default(false) diff --git a/examples-staging/basic/schema.ts b/examples-staging/basic/schema.ts index 8d5b5c3668b..3b38d4dd987 100644 --- a/examples-staging/basic/schema.ts +++ b/examples-staging/basic/schema.ts @@ -45,8 +45,8 @@ const User: Keystone.Lists.User = list({ /** Email is used to log into the system. */ email: text({ isIndexed: 'unique', validation: { isRequired: true } }), /** Avatar upload for the users profile, stored locally */ - avatar: image(), - attachment: file(), + avatar: image({ storage: 'my_images' }), + attachment: file({ storage: 'my_files' }), /** Used to log in. */ password: password(), /** Administrators have more access to various lists and fields. */ diff --git a/packages/core/package.json b/packages/core/package.json index 9dcb3f29955..7913c736427 100644 --- a/packages/core/package.json +++ b/packages/core/package.json @@ -27,6 +27,9 @@ }, "dependencies": { "@apollo/client": "3.4.17", + "@aws-sdk/client-s3": "^3.83.0", + "@aws-sdk/lib-storage": "^3.83.0", + "@aws-sdk/s3-request-presigner": "^3.83.0", "@babel/core": "^7.16.0", "@babel/runtime": "^7.16.3", "@emotion/hash": "^0.8.0", @@ -163,4 +166,4 @@ ] }, "repository": "https://github.com/keystonejs/keystone/tree/main/packages/core" -} +} \ No newline at end of file diff --git a/packages/core/src/fields/types/file/index.ts b/packages/core/src/fields/types/file/index.ts index 2e57ce86dec..04c9520ed31 100644 --- a/packages/core/src/fields/types/file/index.ts +++ b/packages/core/src/fields/types/file/index.ts @@ -1,97 +1,64 @@ import { FileUpload } from 'graphql-upload'; -import { userInputError } from '../../../lib/core/graphql-errors'; import { fieldType, FieldTypeFunc, CommonFieldConfig, BaseListTypeInfo, KeystoneContext, - FileData, + FileMetadata, } from '../../../types'; import { graphql } from '../../..'; import { resolveView } from '../../resolve-view'; -import { getFileRef } from './utils'; -export type FileFieldConfig = - CommonFieldConfig; +export type FileFieldConfig = { + storage: string; +} & CommonFieldConfig; const FileFieldInput = graphql.inputObject({ name: 'FileFieldInput', fields: { - upload: graphql.arg({ type: graphql.Upload }), - ref: graphql.arg({ type: graphql.String }), + upload: graphql.arg({ type: graphql.nonNull(graphql.Upload) }), }, }); -type FileFieldInputType = - | undefined - | null - | { upload?: Promise | null; ref?: string | null }; +type FileFieldInputType = undefined | null | { upload: Promise }; -const fileFields = graphql.fields()({ - filename: graphql.field({ type: graphql.nonNull(graphql.String) }), - filesize: graphql.field({ type: graphql.nonNull(graphql.Int) }), - ref: graphql.field({ - type: graphql.nonNull(graphql.String), - resolve(data) { - return getFileRef(data.mode, data.filename); - }, - }), - url: graphql.field({ - type: graphql.nonNull(graphql.String), - resolve(data, args, context) { - if (!context.files) { - throw new Error( - 'File context is undefined, this most likely means that you havent configurd keystone with a file config, see https://keystonejs.com/docs/apis/config#files for details' - ); - } - return context.files.getUrl(data.mode, data.filename); - }, - }), -}); - -const FileFieldOutput = graphql.interface()({ +const FileFieldOutput = graphql.object()({ name: 'FileFieldOutput', - fields: fileFields, - resolveType: val => (val.mode === 'local' ? 'LocalFileFieldOutput' : 'CloudFileFieldOutput'), -}); - -const LocalFileFieldOutput = graphql.object()({ - name: 'LocalFileFieldOutput', - interfaces: [FileFieldOutput], - fields: fileFields, -}); - -const CloudFileFieldOutput = graphql.object()({ - name: 'CloudFileFieldOutput', - interfaces: [FileFieldOutput], - fields: fileFields, + fields: { + filename: graphql.field({ type: graphql.nonNull(graphql.String) }), + filesize: graphql.field({ type: graphql.nonNull(graphql.Int) }), + url: graphql.field({ + type: graphql.nonNull(graphql.String), + resolve(data, args, context) { + return context.files(data.storage).getUrl(data.filename); + }, + }), + }, }); -async function inputResolver(data: FileFieldInputType, context: KeystoneContext) { +async function inputResolver(storage: string, data: FileFieldInputType, context: KeystoneContext) { if (data === null || data === undefined) { - return { mode: data, filename: data, filesize: data }; - } - - if (data.ref) { - if (data.upload) { - throw userInputError('Only one of ref and upload can be passed to FileFieldInput'); - } - return context.files!.getDataFromRef(data.ref); - } - if (!data.upload) { - throw userInputError('Either ref or upload must be passed to FileFieldInput'); + return { filename: data, filesize: data }; } const upload = await data.upload; - return context.files!.getDataFromStream(upload.createReadStream(), upload.filename); + return context.files(storage).getDataFromStream(upload.createReadStream(), upload.filename); } export const file = ( - config: FileFieldConfig = {} + config: FileFieldConfig ): FieldTypeFunc => - () => { - if ((config as any).isIndexed === 'unique') { + meta => { + const storage = meta.getStorage(config.storage); + + if (!storage) { + throw new Error( + `${meta.listKey}.${meta.fieldKey} has storage set to ${config.storage}, but no storage configuration was found for that key` + ); + } + + if ('isIndexed' in config) { throw Error("isIndexed: 'unique' is not a supported option for field type file"); } @@ -99,30 +66,52 @@ export const file = kind: 'multi', fields: { filesize: { kind: 'scalar', scalar: 'Int', mode: 'optional' }, - mode: { kind: 'scalar', scalar: 'String', mode: 'optional' }, filename: { kind: 'scalar', scalar: 'String', mode: 'optional' }, }, })({ ...config, + hooks: storage.preserve + ? config.hooks + : { + ...config.hooks, + async beforeOperation(args) { + await config.hooks?.beforeOperation?.(args); + if (args.operation === 'update' || args.operation === 'delete') { + const filenameKey = `${meta.fieldKey}_filename`; + const filename = args.item[filenameKey]; + + // This will occur on an update where a file already existed but has been + // changed, or on a delete, where there is no longer an item + if ( + (args.operation === 'delete' || + typeof args.resolvedData[meta.fieldKey].filename === 'string' || + args.resolvedData[meta.fieldKey].filename === null) && + typeof filename === 'string' + ) { + await args.context.files(config.storage).deleteAtSource(filename); + } + } + }, + }, input: { - create: { arg: graphql.arg({ type: FileFieldInput }), resolve: inputResolver }, - update: { arg: graphql.arg({ type: FileFieldInput }), resolve: inputResolver }, + create: { + arg: graphql.arg({ type: FileFieldInput }), + resolve: (data, context) => inputResolver(config.storage, data, context), + }, + update: { + arg: graphql.arg({ type: FileFieldInput }), + resolve: (data, context) => inputResolver(config.storage, data, context), + }, }, output: graphql.field({ type: FileFieldOutput, - resolve({ value: { filesize, filename, mode } }) { - if ( - filesize === null || - filename === null || - mode === null || - (mode !== 'local' && mode !== 'cloud') - ) { + resolve({ value: { filesize, filename } }) { + if (filesize === null || filename === null) { return null; } - return { mode, filename, filesize }; + return { filename, filesize, storage: config.storage }; }, }), - unreferencedConcreteInterfaceImplementations: [LocalFileFieldOutput, CloudFileFieldOutput], views: resolveView('file/views'), }); }; diff --git a/packages/core/src/fields/types/file/tests/test-fixtures.ts b/packages/core/src/fields/types/file/tests/test-fixtures.ts index 04e79e6a49f..5c1cea5eede 100644 --- a/packages/core/src/fields/types/file/tests/test-fixtures.ts +++ b/packages/core/src/fields/types/file/tests/test-fixtures.ts @@ -1,11 +1,12 @@ import path from 'path'; +import os from 'os'; import fs from 'fs-extra'; import { Upload } from 'graphql-upload'; import mime from 'mime'; import { file } from '..'; -import { expectSingleResolverError } from '../../../../../../../tests/api-tests/utils'; +import { KeystoneConfig } from '../../../../types/config'; -const prepareFile = (_filePath: string) => { +export const prepareFile = (_filePath: string) => { const filePath = path.resolve(`${__dirname}/../test-files/${_filePath}`); const upload = new Upload(); upload.resolve({ @@ -18,6 +19,46 @@ const prepareFile = (_filePath: string) => { return { upload }; }; +export const testMatrix: Array = ['local']; + +if (process.env.S3_BUCKET_NAME) { + testMatrix.push('s3'); +} + +export const TEMP_STORAGE = fs.mkdtempSync(path.join(os.tmpdir(), 'tmp_test_images')); + +export const getRootConfig = (matrixValue: MatrixValue): Partial => { + if (matrixValue === 'local') { + return { + storage: { + test_file: { + kind: 'local', + type: 'file', + storagePath: TEMP_STORAGE, + generateUrl: path => `http://localhost:3000/images${path}`, + serverRoute: { + path: '/images', + }, + }, + }, + }; + } + return { + storage: { + test_file: { + kind: 's3', + type: 'file', + bucketName: process.env.S3_BUCKET_NAME!, + accessKeyId: process.env.S3_ACCESS_KEY_ID!, + secretAccessKey: process.env.S3_SECRET_ACCESS_KEY!, + region: process.env.S3_REGION!, + endpoint: process.env.S3_ENDPOINT, + forcePathStyle: process.env.S3_FORCE_PATH_STYLE === 'true', + }, + }, + }; +}; + export const name = 'File'; export const typeFunction = file; @@ -31,13 +72,13 @@ export const supportsUnique = false; export const skipRequiredTest = true; export const fieldName = 'secretFile'; export const subfieldName = 'filesize'; +export const fieldConfig = () => ({ storage: 'test_file' }); -export const getTestFields = () => ({ secretFile: file() }); +export type MatrixValue = 's3' | 'local'; -export const afterEach = async () => { - // This matches the storagePath in the keystone config in the various test files. - fs.rmdirSync('tmp_test_files', { recursive: true }); -}; +export const getTestFields = () => ({ + secretFile: file({ storage: 'test_file' }), +}); export const initItems = () => [ { secretFile: prepareFile('graphql.jpg'), name: 'file0' }, @@ -60,163 +101,3 @@ export const storedValues = () => [ ]; export const supportedFilters = () => []; - -export const crudTests = (keystoneTestWrapper: any) => { - describe('Create - upload', () => { - test( - 'upload values should match expected', - keystoneTestWrapper(async ({ context }: { context: any }) => { - const filename = 'keystone.jpeg'; - const data = await context.query.Test.createOne({ - data: { secretFile: prepareFile(filename) }, - query: ` - secretFile { - filename - __typename - filesize - ref - url - } - `, - }); - expect(data).not.toBe(null); - expect(data.secretFile.ref).toEqual(`local:file:${data.secretFile.filename}`); - expect(data.secretFile.url).toEqual(`/files/${data.secretFile.filename}`); - expect(data.secretFile.filesize).toEqual(3250); - expect(data.secretFile.__typename).toEqual('LocalFileFieldOutput'); - }) - ); - }); - describe('Create - ref', () => { - test( - 'From existing item succeeds', - keystoneTestWrapper(async ({ context }: { context: any }) => { - // Create an initial item - const initialItem = await context.query.Test.createOne({ - data: { secretFile: prepareFile('keystone.jpg') }, - query: ` - secretFile { - filename - __typename - filesize - ref - url - } - `, - }); - expect(initialItem).not.toBe(null); - - // Create a new item base on the first items ref - const ref = initialItem.secretFile.ref; - const newItem = await context.query.Test.createOne({ - data: { secretFile: { ref } }, - query: ` - secretFile { - filename - __typename - filesize - ref - url - } - `, - }); - expect(newItem).not.toBe(null); - - // Check that the details of both items match - expect(newItem.secretFile).toEqual(initialItem.secretFile); - }) - ); - test( - 'From invalid ref fails', - keystoneTestWrapper(async ({ context }: { context: any }) => { - const { data, errors } = await context.graphql.raw({ - query: ` - mutation ($item: TestCreateInput!) { - createTest(data: $item) { - secretFile { - filename - } - } - } - `, - variables: { item: { secretFile: { ref: 'Invalid ref!' } } }, - }); - expect(data).toEqual({ createTest: null }); - const message = `Invalid file reference`; - expectSingleResolverError(errors, 'createTest', 'Test.secretFile', message); - }) - ); - test( - 'From null ref fails', - keystoneTestWrapper(async ({ context }: { context: any }) => { - const { data, errors } = await context.graphql.raw({ - query: ` - mutation ($item: TestCreateInput!) { - createTest(data: $item) { - secretFile { - filename - } - } - } - `, - variables: { item: { secretFile: { ref: null } } }, - }); - expect(data).toEqual({ createTest: null }); - const message = `Input error: Either ref or upload must be passed to FileFieldInput`; - expectSingleResolverError(errors, 'createTest', 'Test.secretFile', message); - }) - ); - test( - 'Both upload and ref fails - valid ref', - keystoneTestWrapper(async ({ context }: { context: any }) => { - const initialItem = await context.query.Test.createOne({ - data: { secretFile: prepareFile('keystone.jpg') }, - query: `secretFile { ref }`, - }); - expect(initialItem).not.toBe(null); - - const { data, errors } = await context.graphql.raw({ - query: ` - mutation ($item: TestCreateInput!) { - createTest(data: $item) { - secretFile { - filename - } - } - } - `, - variables: { - item: { - secretFile: { ref: initialItem.secretFile.ref, ...prepareFile('keystone.jpg') }, - }, - }, - }); - expect(data).toEqual({ createTest: null }); - const message = `Input error: Only one of ref and upload can be passed to FileFieldInput`; - expectSingleResolverError(errors, 'createTest', 'Test.secretFile', message); - }) - ); - test( - 'Both upload and ref fails - invalid ref', - keystoneTestWrapper(async ({ context }: { context: any }) => { - const { data, errors } = await context.graphql.raw({ - query: ` - mutation ($item: TestCreateInput!) { - createTest(data: $item) { - secretFile { - filename - } - } - } - `, - variables: { - item: { secretFile: { ref: 'Invalid', ...prepareFile('keystone.jpg') } }, - }, - }); - expect(data).toEqual({ createTest: null }); - const message = `Input error: Only one of ref and upload can be passed to FileFieldInput`; - expectSingleResolverError(errors, 'createTest', 'Test.secretFile', message); - }) - ); - }); -}; diff --git a/packages/core/src/fields/types/file/utils.ts b/packages/core/src/fields/types/file/utils.ts deleted file mode 100644 index 051ad9fdc05..00000000000 --- a/packages/core/src/fields/types/file/utils.ts +++ /dev/null @@ -1,16 +0,0 @@ -import { AssetMode } from '../../../types'; - -const FILEREGEX = /^(local|cloud):file:([^\\\/:\n]+)/; - -export const getFileRef = (mode: AssetMode, name: string) => `${mode}:file:${name}`; -export const parseFileRef = (ref: string) => { - const match = ref.match(FILEREGEX); - if (match) { - const [, mode, filename] = match; - return { - mode: mode as AssetMode, - filename: filename as string, - }; - } - return undefined; -}; diff --git a/packages/core/src/fields/types/file/views/Field.tsx b/packages/core/src/fields/types/file/views/Field.tsx index 57dfa3c1b20..376bc9fdb1e 100644 --- a/packages/core/src/fields/types/file/views/Field.tsx +++ b/packages/core/src/fields/types/file/views/Field.tsx @@ -1,88 +1,24 @@ /** @jsxRuntime classic */ /** @jsx jsx */ -import { Fragment, useMemo, useRef, RefObject } from 'react'; -import copy from 'copy-to-clipboard'; +import { useMemo, useRef, RefObject } from 'react'; import bytes from 'bytes'; -import { jsx, Stack, Text, VisuallyHidden } from '@keystone-ui/core'; -import { useToasts } from '@keystone-ui/toast'; +import { jsx, Stack, Text } from '@keystone-ui/core'; import { FieldContainer, FieldLabel } from '@keystone-ui/fields'; -import { TextInput } from '@keystone-ui/fields'; -import { Pill } from '@keystone-ui/pill'; import { Button } from '@keystone-ui/button'; import { FieldProps } from '../../../../types'; - -import { parseFileRef } from '../utils'; import { FileValue } from './index'; -export function validateRef({ ref }: { ref: string }) { - if (!parseFileRef(ref)) { - return 'Invalid ref'; - } -} - -const RefView = ({ - field, - onChange, - onCancel, - error, -}: { - field: any; - onChange: (value: string) => void; - onCancel: () => void; - error?: string; -}) => { - return ( - - - Paste the file ref here - - - { - onChange(event.target.value); - }} - css={{ - width: '100%', - }} - /> - - {error ? ( - - {error} - - ) : null} - - - ); -}; - export function Field({ autoFocus, field, value, - forceValidation, onChange, }: FieldProps) { const inputRef = useRef(null); - const errorMessage = createErrorMessage(value, forceValidation); + const errorMessage = createErrorMessage(value); const onUploadChange = ({ currentTarget: { validity, files }, @@ -105,29 +41,7 @@ export function Field({ return ( {field.label} - {value.kind === 'ref' ? ( - { - onChange?.({ - kind: 'ref', - data: { ref }, - previous: value.previous, - }); - }} - error={forceValidation && errorMessage ? errorMessage : undefined} - onCancel={() => { - onChange?.(value.previous); - }} - /> - ) : ( - - )} + ; + value: FileValue; onChange?: (value: FileValue) => void; inputRef: RefObject; }) { - const { addToast } = useToasts(); - const onSuccess = () => { - addToast({ title: 'Copied file ref to clipboard', tone: 'positive' }); - }; - const onFailure = () => { - addToast({ title: 'Failed to copy file ref to clipboard', tone: 'negative' }); - }; - - const copyRef = () => { - if (value.kind !== 'from-server') { - return; - } - - if (navigator) { - // use the new navigator.clipboard API if it exists - navigator.clipboard.writeText(value?.data.ref).then(onSuccess, onFailure); - return; - } else { - // Fallback to a library that leverages document.execCommand - // for browser versions that dont' support the navigator object. - // As document.execCommand - try { - copy(value?.data.ref); - } catch (e) { - addToast({ title: 'Faild to copy to clipboard', tone: 'negative' }); - } - - return; - } - }; return value.kind === 'from-server' || value.kind === 'upload' ? ( {onChange && ( - + {value.kind === 'from-server' && ( - - - - {`${value.data.filename}`} - - - - - {bytes(value.data.filesize)} + + + {`${value.data.filename}`} + + + Size: {bytes(value.data.filesize)} + + )} + {value.kind === 'upload' && ( + + + File linked, save to complete upload + + Size: {bytes(value.data.file.size)} )} @@ -212,21 +99,6 @@ function FileView({ > Change - {value.kind !== 'upload' ? ( - - ) : null} {value.kind === 'from-server' && ( - {value.kind === 'remove' && value.previous && ( - {error ? ( - - {error} - - ) : null} - - - ); -}; - export function Field({ autoFocus, field, value, - forceValidation, onChange, }: FieldProps) { const inputRef = useRef(null); - const errorMessage = createErrorMessage(value, forceValidation); + const errorMessage = createErrorMessage(value); const onUploadChange = ({ currentTarget: { validity, files }, @@ -109,34 +51,20 @@ export function Field({ // the user selects the same file again) // eslint-disable-next-line react-hooks/exhaustive-deps const inputKey = useMemo(() => Math.random(), [value]); - + const accept = useMemo( + () => SUPPORTED_IMAGE_EXTENSIONS.map(ext => [`.${ext}`, `image/${ext}`].join(', ')).join(', '), + [] + ); return ( {field.label} - {value.kind === 'ref' ? ( - { - onChange?.({ - kind: 'ref', - data: { ref }, - previous: value.previous, - }); - }} - error={forceValidation && errorMessage ? errorMessage : undefined} - onCancel={() => { - onChange?.(value.previous); - }} - /> - ) : ( - - )} + + {errorMessage && ( + + {errorMessage} + + )} ); } @@ -160,210 +100,159 @@ function ImgView({ inputRef, }: { errorMessage?: string; - value: Exclude; + value: ImageValue; onChange?: (value: ImageValue) => void; field: ReturnType; inputRef: RefObject; }) { - const { addToast } = useToasts(); - + const [imageDimensions, setImageDimensions] = useState({ width: 0, height: 0 }); const imagePathFromUpload = useObjectURL( errorMessage === undefined && value.kind === 'upload' ? value.data.file : undefined ); - const onSuccess = () => { - addToast({ title: 'Copied image ref to clipboard', tone: 'positive' }); - }; - const onFailure = () => { - addToast({ title: 'Failed to copy image ref to clipboard', tone: 'negative' }); - }; + const imageSrc = value.kind === 'from-server' ? value.data.src : imagePathFromUpload; - const copyRef = () => { - if (value.kind !== 'from-server') { - return; - } - - if (navigator) { - // use the new navigator.clipboard API if it exists - navigator.clipboard.writeText(value?.data.ref).then(onSuccess, onFailure); - return; - } else { - // Fallback to a library that leverages document.execCommand - // for browser versions that dont' support the navigator object. - // As document.execCommand - try { - copy(value?.data.ref); - } catch (e) { - addToast({ title: 'Faild to oopy to clipboard', tone: 'negative' }); - } - - return; - } - }; - return value.kind === 'from-server' || value.kind === 'upload' ? ( - - {errorMessage === undefined ? ( - value.kind === 'from-server' ? ( - - {field.path} - + return ( + + + {errorMessage || (value.kind !== 'from-server' && value.kind !== 'upload') ? ( + ) : ( - + + {value.kind === 'upload' && ( +
+ Save to complete upload +
+ )} { + if (value.kind === 'upload') { + setImageDimensions({ + width: event.currentTarget.naturalWidth, + height: event.currentTarget.naturalHeight, + }); + } + }} css={{ - height: 'auto', - maxWidth: '100%', + objectFit: 'contain', + width: '100%', + height: '100%', }} - src={imagePathFromUpload} - alt={field.path} + alt={`Image uploaded to ${field.path} field`} + src={imageSrc} /> -
- ) - ) : null} - {onChange && ( - - {value.kind === 'from-server' && ( - - - - - {`${value.data.id}.${value.data.extension}`} - - - - - {`${value.data.width} x ${value.data.height} (${bytes( - value.data.filesize - )})`} - - )} - - - {value.kind !== 'upload' ? ( - + + )} +
+ {value.kind === 'from-server' || value.kind === 'upload' ? ( + onChange && ( + + {errorMessage === undefined ? ( + ) : null} - {value.kind === 'from-server' && ( + - )} - {value.kind === 'upload' && ( - - )} - {errorMessage ? ( - - {errorMessage} - - ) : ( - value.kind === 'upload' && ( - - Save to upload this image - - ) - )} + {value.kind === 'from-server' && ( + + )} + {value.kind === 'upload' && ( + + )} + -
- )} -
- ) : ( - - - - - {value.kind === 'remove' && value.previous && ( + ) + ) : ( + - )} - {value.kind === 'remove' && - // NOTE -- UX decision is to not display this, I think it would only be relevant - // for deleting uploaded images (and we don't support that yet) - // - // Save to remove this image - // - null} - + {value.kind === 'remove' && value.previous && ( + + )} + + )} ); } -export function validateRef({ ref }: { ref: string }) { - if (!parseImageRef(ref)) { - return 'Invalid ref'; - } -} - -function createErrorMessage(value: ImageValue, forceValidation?: boolean) { +function createErrorMessage(value: ImageValue) { if (value.kind === 'upload') { return validateImage(value.data); - } else if (value.kind === 'ref') { - return forceValidation ? validateRef(value.data) : undefined; } } @@ -379,7 +268,19 @@ export function validateImage({ } // check if the file is actually an image if (!file.type.includes('image')) { - return 'Only image files are allowed. Please try again.'; + return `Sorry, that file type isn't accepted. Please try ${SUPPORTED_IMAGE_EXTENSIONS.reduce( + (acc, curr, currentIndex) => { + if (currentIndex === SUPPORTED_IMAGE_EXTENSIONS.length - 1) { + acc += ` or .${curr}`; + } else if (currentIndex > 0) { + acc += `, .${curr}`; + } else { + acc += `.${curr}`; + } + return acc; + }, + '' + )}.`; } } @@ -387,24 +288,83 @@ export function validateImage({ // Styled Components // ============================== -export const ImageWrapper = ({ children }: { children: ReactNode }) => { - const theme = useTheme(); +export const ImageMeta = ({ + width = 0, + height = 0, + size, +}: { + width?: number; + height?: number; + size: number; +}) => { + return ( + + Size: {`${bytes(size)}`} + Dimensions: {`${width} x ${height}`} + + ); +}; +export const ImageWrapper = ({ children, url }: { children: ReactNode; url?: string }) => { + if (url) { + return ( + + {children} + + ); + } return (
{children}
); }; + +export const Placeholder = () => { + return ( + + + + ); +}; diff --git a/packages/core/src/fields/types/image/views/index.tsx b/packages/core/src/fields/types/image/views/index.tsx index 499ba72162d..71469960ff5 100644 --- a/packages/core/src/fields/types/image/views/index.tsx +++ b/packages/core/src/fields/types/image/views/index.tsx @@ -9,7 +9,7 @@ import { FieldController, FieldControllerConfig, } from '../../../../types'; -import { validateImage, validateRef, ImageWrapper } from './Field'; +import { validateImage, ImageWrapper } from './Field'; export { Field } from './Field'; @@ -47,7 +47,6 @@ export const CardValue: CardValueComponent = ({ item, field }) => { type ImageData = { src: string; - ref: string; height: number; width: number; filesize: number; @@ -57,13 +56,6 @@ type ImageData = { export type ImageValue = | { kind: 'empty' } - | { - kind: 'ref'; - data: { - ref: string; - }; - previous: ImageValue; - } | { kind: 'from-server'; data: ImageData; @@ -87,7 +79,6 @@ export const controller = (config: FieldControllerConfig): ImageController => { graphqlSelection: `${config.path} { url id - ref extension width height @@ -111,18 +102,13 @@ export const controller = (config: FieldControllerConfig): ImageController => { }; }, validate(value): boolean { - if (value.kind === 'ref') { - return validateRef(value.data) === undefined; - } return value.kind !== 'upload' || validateImage(value.data) === undefined; }, serialize(value) { if (value.kind === 'upload') { return { [config.path]: { upload: value.data.file } }; } - if (value.kind === 'ref') { - return { [config.path]: { ref: value.data.ref } }; - } + if (value.kind === 'remove') { return { [config.path]: null }; } diff --git a/packages/core/src/lib/assets/createFilesContext.ts b/packages/core/src/lib/assets/createFilesContext.ts new file mode 100644 index 00000000000..aac402bf8f8 --- /dev/null +++ b/packages/core/src/lib/assets/createFilesContext.ts @@ -0,0 +1,74 @@ +import crypto from 'crypto'; +import filenamify from 'filenamify'; + +import slugify from '@sindresorhus/slugify'; +import { KeystoneConfig, FilesContext } from '../../types'; +import { localFileAssetsAPI } from './local'; +import { s3FileAssetsAPI } from './s3'; +import { FileAdapter } from './types'; + +const defaultTransformName = (filename: string) => { + // Appends a UUID to the filename so that people can't brute-force guess stored filenames + // + // This regex lazily matches for any characters that aren't a new line + // it then optionally matches the last instance of a "." symbol + // followed by any alphanumerical character before the end of the string + const [, name, ext] = filename.match(/^([^:\n].*?)(\.[A-Za-z0-9]{0,10})?$/) as RegExpMatchArray; + + const id = crypto + .randomBytes(24) + .toString('base64') + .replace(/[^a-zA-Z0-9]/g, '') + .slice(12); + + // console.log(id, id.length, id.slice(12).length); + const urlSafeName = filenamify(slugify(name), { + maxLength: 100 - id.length - (ext ? ext.length : 0), + replacement: '-', + }); + if (ext) { + return `${urlSafeName}-${id}${ext}`; + } + return `${urlSafeName}-${id}`; +}; + +export function createFilesContext(config: KeystoneConfig): FilesContext { + const adaptersMap = new Map(); + + for (const [storageKey, storageConfig] of Object.entries(config.storage || {})) { + if (storageConfig.type === 'file') { + adaptersMap.set( + storageKey, + storageConfig.kind === 'local' + ? localFileAssetsAPI(storageConfig) + : s3FileAssetsAPI(storageConfig) + ); + } + } + + return (storageString: string) => { + const adapter = adaptersMap.get(storageString); + if (!adapter) { + throw new Error(`No file assets API found for storage string "${storageString}"`); + } + + return { + getUrl: async filename => { + return adapter.url(filename); + }, + getDataFromStream: async (stream, originalFilename) => { + const storageConfig = config.storage![storageString]; + const { transformName = defaultTransformName } = storageConfig as typeof storageConfig & { + type: 'file'; + }; + const filename = await transformName(originalFilename); + + const { filesize } = await adapter.upload(stream, filename); + return { filename, filesize }; + }, + deleteAtSource: async filename => { + await adapter.delete(filename); + }, + }; + }; +} diff --git a/packages/core/src/lib/assets/createImagesContext.ts b/packages/core/src/lib/assets/createImagesContext.ts new file mode 100644 index 00000000000..8dc64c7f767 --- /dev/null +++ b/packages/core/src/lib/assets/createImagesContext.ts @@ -0,0 +1,67 @@ +import { v4 as uuid } from 'uuid'; +import fromBuffer from 'image-type'; +import imageSize from 'image-size'; +import { KeystoneConfig, ImageMetadata, ImagesContext } from '../../types'; +import { ImageAdapter } from './types'; +import { localImageAssetsAPI } from './local'; +import { s3ImageAssetsAPI } from './s3'; +import { streamToBuffer } from './utils'; + +export function getImageMetadataFromBuffer(buffer: Buffer): ImageMetadata { + const fileType = fromBuffer(buffer); + if (!fileType) { + throw new Error('File type not found'); + } + + const extension = fileType.ext; + if (extension !== 'jpg' && extension !== 'png' && extension !== 'webp' && extension !== 'gif') { + throw new Error(`${extension} is not a supported image type`); + } + + const { height, width } = imageSize(buffer); + + if (width === undefined || height === undefined) { + throw new Error('Height and width could not be found for image'); + } + return { width, height, filesize: buffer.length, extension }; +} + +export function createImagesContext(config: KeystoneConfig): ImagesContext { + const imageAssetsAPIs = new Map(); + for (const [storageKey, storageConfig] of Object.entries(config.storage || {})) { + if (storageConfig.type === 'image') { + imageAssetsAPIs.set( + storageKey, + storageConfig.kind === 'local' + ? localImageAssetsAPI(storageConfig) + : s3ImageAssetsAPI(storageConfig) + ); + } + } + + return (storageString: string) => { + const adapter = imageAssetsAPIs.get(storageString); + if (adapter === undefined) { + throw new Error(`No file assets API found for storage string "${storageString}"`); + } + + return { + getUrl: async (id, extension) => { + return adapter.url(id, extension); + }, + getDataFromStream: async (stream, originalFilename) => { + const storageConfig = config.storage![storageString]; + const { transformName = () => uuid() } = storageConfig; + + const buffer = await streamToBuffer(stream); + const { extension, ...rest } = getImageMetadataFromBuffer(buffer); + + const id = await transformName(originalFilename, extension); + + await adapter.upload(buffer, id, extension); + return { id, extension, ...rest }; + }, + deleteAtSource: adapter.delete, + }; + }; +} diff --git a/packages/core/src/lib/assets/local.ts b/packages/core/src/lib/assets/local.ts new file mode 100644 index 00000000000..8f134ec1286 --- /dev/null +++ b/packages/core/src/lib/assets/local.ts @@ -0,0 +1,57 @@ +import path from 'path'; +import { pipeline } from 'stream'; +import fs from 'fs-extra'; + +import { StorageConfig } from '../../types'; +// import { getImageMetadataFromBuffer } from './createImagesContext'; +import { FileAdapter, ImageAdapter } from './types'; + +export function localImageAssetsAPI( + storageConfig: StorageConfig & { kind: 'local' } +): ImageAdapter { + return { + async url(id, extension) { + return storageConfig.generateUrl(`/${id}.${extension}`); + }, + async upload(buffer, id, extension) { + // const buffer = await streamToBuffer(stream); + + await fs.writeFile(path.join(storageConfig.storagePath, `${id}.${extension}`), buffer); + }, + async delete(id, extension) { + await fs.unlink(path.join(storageConfig.storagePath, `${id}.${extension}`)); + }, + }; +} + +export function localFileAssetsAPI(storageConfig: StorageConfig & { kind: 'local' }): FileAdapter { + return { + async url(filename) { + return storageConfig.generateUrl(`/${filename}`); + }, + async upload(stream, filename) { + const writeStream = fs.createWriteStream(path.join(storageConfig.storagePath, filename)); + const pipeStreams: Promise = new Promise((resolve, reject) => { + pipeline(stream, writeStream, err => { + if (err) { + reject(err); + } else { + resolve(); + } + }); + }); + + try { + await pipeStreams; + const { size: filesize } = await fs.stat(path.join(storageConfig.storagePath, filename)); + return { filesize, filename }; + } catch (e) { + await fs.remove(path.join(storageConfig.storagePath, filename)); + throw e; + } + }, + async delete(filename) { + await fs.unlink(path.join(storageConfig.storagePath, filename)); + }, + }; +} diff --git a/packages/core/src/lib/assets/s3.ts b/packages/core/src/lib/assets/s3.ts new file mode 100644 index 00000000000..412697a7deb --- /dev/null +++ b/packages/core/src/lib/assets/s3.ts @@ -0,0 +1,125 @@ +import { getSignedUrl } from '@aws-sdk/s3-request-presigner'; +import { S3, GetObjectCommand } from '@aws-sdk/client-s3'; +import { Upload } from '@aws-sdk/lib-storage'; + +import { StorageConfig } from '../../types'; +import { FileAdapter, ImageAdapter } from './types'; + +export function s3ImageAssetsAPI(storageConfig: StorageConfig & { kind: 's3' }): ImageAdapter { + const { generateUrl, s3, presign, s3Endpoint } = s3AssetsCommon(storageConfig); + return { + async url(id, extension) { + if (!storageConfig.signed) { + return generateUrl(`${s3Endpoint}${storageConfig.pathPrefix || ''}${id}.${extension}`); + } + return generateUrl(await presign(`${id}.${extension}`)); + }, + async upload(buffer, id, extension) { + const upload = new Upload({ + client: s3, + params: { + Bucket: storageConfig.bucketName, + Key: `${storageConfig.pathPrefix || ''}${id}.${extension}`, + Body: buffer, + ContentType: { + png: 'image/png', + webp: 'image/webp', + gif: 'image/gif', + jpg: 'image/jpeg', + }[extension], + }, + }); + await upload.done(); + }, + async delete(id, extension) { + await s3.deleteObject({ + Bucket: storageConfig.bucketName, + Key: `${storageConfig.pathPrefix || ''}${id}.${extension}`, + }); + }, + }; +} + +export function s3FileAssetsAPI(storageConfig: StorageConfig & { kind: 's3' }): FileAdapter { + const { generateUrl, s3, presign, s3Endpoint } = s3AssetsCommon(storageConfig); + + return { + async url(filename) { + if (!storageConfig.signed) { + return generateUrl(`${s3Endpoint}${storageConfig.pathPrefix || ''}${filename}`); + } + return generateUrl(await presign(filename)); + }, + async upload(stream, filename) { + let filesize = 0; + stream.on('data', data => { + filesize += data.length; + }); + + const upload = new Upload({ + client: s3, + params: { + Bucket: storageConfig.bucketName, + Key: (storageConfig.pathPrefix || '') + filename, + Body: stream, + ContentType: 'application/octet-stream', + }, + }); + + await upload.done(); + + return { filename, filesize }; + }, + async delete(filename) { + await s3.deleteObject({ + Bucket: storageConfig.bucketName, + Key: (storageConfig.pathPrefix || '') + filename, + }); + }, + }; +} + +export function getS3AssetsEndpoint(storageConfig: StorageConfig & { kind: 's3' }) { + let endpoint = storageConfig.endpoint + ? new URL(storageConfig.endpoint) + : new URL(`https://s3.${storageConfig.region}.amazonaws.com`); + if (storageConfig.forcePathStyle) { + endpoint = new URL(`/${storageConfig.bucketName}`, endpoint); + } else { + endpoint.hostname = `${storageConfig.bucketName}.${endpoint.hostname}`; + } + + const endpointString = endpoint.toString(); + if (endpointString.endsWith('/')) return endpointString; + return `${endpointString}/`; +} + +function s3AssetsCommon(storageConfig: StorageConfig & { kind: 's3' }) { + const s3 = new S3({ + credentials: { + accessKeyId: storageConfig.accessKeyId, + secretAccessKey: storageConfig.secretAccessKey, + }, + region: storageConfig.region, + endpoint: storageConfig.endpoint, + forcePathStyle: storageConfig.forcePathStyle, + }); + + const s3Endpoint = getS3AssetsEndpoint(storageConfig); + const generateUrl = storageConfig.generateUrl ?? (url => url); + + return { + generateUrl, + s3, + s3Endpoint, + presign: async (filename: string) => { + const command = new GetObjectCommand({ + Bucket: storageConfig.bucketName, + Key: (storageConfig.pathPrefix || '') + filename, + }); + return getSignedUrl(s3, command, { + expiresIn: storageConfig.signed?.expiry, + }); + }, + }; +} diff --git a/packages/core/src/lib/assets/types.ts b/packages/core/src/lib/assets/types.ts new file mode 100644 index 00000000000..64df6049b0a --- /dev/null +++ b/packages/core/src/lib/assets/types.ts @@ -0,0 +1,14 @@ +import { Readable } from 'stream'; +import { ImageExtension, FileMetadata } from '../../types'; + +export type ImageAdapter = { + upload(stream: Buffer, id: string, extension: string): Promise; + delete(id: string, extension: ImageExtension): Promise; + url(id: string, extension: ImageExtension): Promise; +}; + +export type FileAdapter = { + upload(stream: Readable, filename: string): Promise; + delete(id: string): Promise; + url(filename: string): Promise; +}; diff --git a/packages/core/src/lib/assets/utils.ts b/packages/core/src/lib/assets/utils.ts new file mode 100644 index 00000000000..a26b64dd582 --- /dev/null +++ b/packages/core/src/lib/assets/utils.ts @@ -0,0 +1,11 @@ +import { Readable } from 'stream'; + +export async function streamToBuffer(stream: Readable): Promise { + const chunks = []; + + for await (let chunk of stream) { + chunks.push(chunk); + } + + return Buffer.concat(chunks); +} diff --git a/packages/core/src/lib/cloud/assets.ts b/packages/core/src/lib/cloud/assets.ts deleted file mode 100644 index 2472023f431..00000000000 --- a/packages/core/src/lib/cloud/assets.ts +++ /dev/null @@ -1,148 +0,0 @@ -import { Readable } from 'stream'; -import fetch from 'node-fetch'; -import FormData from 'form-data'; -import { FileData, ImageExtension, ImageMetadata } from '../../types/context'; - -function formUploadBody({ - fieldName, - fileName, - data, -}: { - fieldName: string; - fileName: string; - data: Readable; -}) { - const form = new FormData(); - form.append(fieldName, data, fileName); - return form; -} - -export type CloudAssetsAPI = { - images: { - upload(stream: Readable, id: string): Promise; - url(id: string, extension: ImageExtension): string; - metadata(id: string, extension: ImageExtension): Promise; - }; - files: { - upload(stream: Readable, filename: string): Promise; - url(filename: string): string; - metadata(filename: string): Promise; - }; -}; - -type ImageMetadataResponse = { - width: number; - height: number; - filesize: number; - extension: ImageExtension; -}; - -const cloudAssetsConfigCache = new Map(); - -export async function getCloudAssetsAPI({ apiKey }: { apiKey: string }): Promise { - const headers = { - Authorization: `Bearer ${apiKey}`, - 'x-keystone-version': `TODO 6 RC`, - }; - if (!cloudAssetsConfigCache.has(apiKey)) { - const res = await fetch('https://init.keystonejs.cloud/api/rest/config', { headers }); - if (!res.ok) { - throw new Error(`Failed to load cloud config: ${res.status}\n${await res.text()}`); - } - const json = await res.json(); - cloudAssetsConfigCache.set(apiKey, json); - } - - const { - // project, - assets, - } = cloudAssetsConfigCache.get(apiKey)!; - const { - fileGetUrl, - // fileDownloadUrl, - fileUploadUrl, - fileMetaUrl, - imageGetUrl, - imageUploadUrl, - imageMetaUrl, - } = assets; - - return { - images: { - url(id, extension) { - return `${imageGetUrl}/${id}.${extension}`; - }, - async metadata(id, extension): Promise { - const res = await fetch(`${imageMetaUrl}/${id}.${extension}`); - if (!res.ok) { - console.error(`${res.status} ${await res.text()}`); - throw new Error('Error occurred when fetching image metadata'); - } - const metadata: ImageMetadataResponse = await res.json(); - return { - extension: metadata.extension, - height: metadata.height, - width: metadata.width, - filesize: metadata.filesize, - }; - }, - async upload(buffer, id) { - const res = await fetch(imageUploadUrl, { - method: 'POST', - body: formUploadBody({ - data: buffer, - fieldName: 'image', - fileName: id, - }), - headers, - }); - if (!res.ok) { - console.error(`${res.status} ${await res.text()}`); - throw new Error('Error occurred when uploading image'); - } - const metadata: ImageMetadataResponse = await res.json(); - return { - extension: metadata.extension, - filesize: metadata.filesize, - height: metadata.height, - width: metadata.width, - }; - }, - }, - files: { - url(filename) { - return `${fileGetUrl}/${filename}`; - }, - async metadata(filename) { - const res = await fetch(`${fileMetaUrl}/${filename}`); - if (!res.ok) { - console.error(`${res.status} ${await res.text()}`); - throw new Error('Error occurred when fetching file metadata'); - } - const metadata = await res.json(); - return { - mode: 'cloud', - filesize: metadata.filesize, - filename, - }; - }, - async upload(stream, filename) { - const res = await fetch(fileUploadUrl, { - method: 'POST', - body: formUploadBody({ data: stream, fieldName: 'file', fileName: filename }), - headers, - }); - if (!res.ok) { - console.error(`${res.status} ${await res.text()}`); - throw new Error('Error occurred when uploading file'); - } - const metadata = await res.json(); - return { - mode: 'cloud', - filesize: metadata.filesize, - filename, - }; - }, - }, - }; -} diff --git a/packages/core/src/lib/context/createContext.ts b/packages/core/src/lib/context/createContext.ts index 654ec90b478..a01f99d350e 100644 --- a/packages/core/src/lib/context/createContext.ts +++ b/packages/core/src/lib/context/createContext.ts @@ -10,10 +10,9 @@ import { import { PrismaClient } from '../core/utils'; import { InitialisedList } from '../core/types-for-lists'; -import { CloudAssetsAPI } from '../cloud/assets'; +import { createImagesContext } from '../assets/createImagesContext'; +import { createFilesContext } from '../assets/createFilesContext'; import { getDbAPIFactory, itemAPIForList } from './itemAPI'; -import { createImagesContext } from './createImagesContext'; -import { createFilesContext } from './createFilesContext'; export function makeCreateContext({ graphQLSchema, @@ -22,7 +21,6 @@ export function makeCreateContext({ gqlNamesByList, config, lists, - cloudAssetsAPI, }: { graphQLSchema: GraphQLSchema; sudoGraphQLSchema: GraphQLSchema; @@ -30,10 +28,9 @@ export function makeCreateContext({ prismaClient: PrismaClient; gqlNamesByList: Record; lists: Record; - cloudAssetsAPI: () => CloudAssetsAPI; }) { - const images = createImagesContext(config, cloudAssetsAPI); - const files = createFilesContext(config, cloudAssetsAPI); + const images = createImagesContext(config); + const files = createFilesContext(config); // We precompute these helpers here rather than every time createContext is called // because they involve creating a new GraphQLSchema, creating a GraphQL document AST(programmatically, not by parsing) and validating the // note this isn't as big of an optimisation as you would imagine(at least in comparison with the rest of the system), diff --git a/packages/core/src/lib/context/createFilesContext.ts b/packages/core/src/lib/context/createFilesContext.ts deleted file mode 100644 index a10dc47740e..00000000000 --- a/packages/core/src/lib/context/createFilesContext.ts +++ /dev/null @@ -1,119 +0,0 @@ -import path from 'path'; -import crypto from 'crypto'; -import { pipeline } from 'stream'; -import filenamify from 'filenamify'; -import fs from 'fs-extra'; - -import slugify from '@sindresorhus/slugify'; -import { KeystoneConfig, FilesContext } from '../../types'; -import { parseFileRef } from '../../fields/types/file/utils'; -import { CloudAssetsAPI } from '../cloud/assets'; - -const DEFAULT_BASE_URL = '/files'; -export const DEFAULT_FILES_STORAGE_PATH = './public/files'; - -const defaultTransformer = (str: string) => slugify(str); - -const generateSafeFilename = ( - filename: string, - transformFilename: (str: string) => string = defaultTransformer -) => { - // Appends a UUID to the filename so that people can't brute-force guess stored filenames - // - // This regex lazily matches for any characters that aren't a new line - // it then optionally matches the last instance of a "." symbol - // followed by any alphanumerical character before the end of the string - const [, name, ext] = filename.match(/^([^:\n].*?)(\.[A-Za-z0-9]+)?$/) as RegExpMatchArray; - - const id = crypto - .randomBytes(24) - .toString('base64') - .replace(/[^a-zA-Z0-9]/g, '') - .slice(12); - - // console.log(id, id.length, id.slice(12).length); - const urlSafeName = filenamify(transformFilename(name), { - maxLength: 100 - id.length, - replacement: '-', - }); - if (ext) { - return `${urlSafeName}-${id}${ext}`; - } - return `${urlSafeName}-${id}`; -}; - -export function createFilesContext( - config: KeystoneConfig, - cloudAssets: () => CloudAssetsAPI -): FilesContext | undefined { - if (!config.files) { - return; - } - - const { files } = config; - const { baseUrl = DEFAULT_BASE_URL, storagePath = DEFAULT_FILES_STORAGE_PATH } = - files.local || {}; - - if (files.upload === 'local') { - fs.mkdirSync(storagePath, { recursive: true }); - } - - return { - getUrl: async (mode, filename) => { - if (mode === 'cloud') { - return cloudAssets().files.url(filename); - } - - return `${baseUrl}/${filename}`; - }, - getDataFromRef: async (ref: string) => { - const fileRef = parseFileRef(ref); - - if (!fileRef) { - throw new Error('Invalid file reference'); - } - - const { mode, filename } = fileRef; - - if (mode === 'cloud') { - const { filesize } = await cloudAssets().files.metadata(filename); - - return { filesize, ...fileRef }; - } - - const { size: filesize } = await fs.stat(path.join(storagePath, fileRef.filename)); - - return { filesize, ...fileRef }; - }, - getDataFromStream: async (stream, originalFilename) => { - const { upload: mode } = files; - const filename = generateSafeFilename(originalFilename, files.transformFilename); - - if (mode === 'cloud') { - const { filesize } = await cloudAssets().files.upload(stream, filename); - - return { mode, filesize, filename }; - } - - const writeStream = fs.createWriteStream(path.join(storagePath, filename)); - const pipeStreams: Promise = new Promise((resolve, reject) => { - pipeline(stream, writeStream, err => { - if (err) { - reject(err); - } else { - resolve(); - } - }); - }); - - try { - await pipeStreams; - const { size: filesize } = await fs.stat(path.join(storagePath, filename)); - return { mode, filesize, filename }; - } catch (e) { - await fs.remove(path.join(storagePath, filename)); - throw e; - } - }, - }; -} diff --git a/packages/core/src/lib/context/createImagesContext.ts b/packages/core/src/lib/context/createImagesContext.ts deleted file mode 100644 index 39af174ed56..00000000000 --- a/packages/core/src/lib/context/createImagesContext.ts +++ /dev/null @@ -1,102 +0,0 @@ -import path from 'path'; -import { v4 as uuid } from 'uuid'; -import fs from 'fs-extra'; -import fromBuffer from 'image-type'; -import imageSize from 'image-size'; -import { KeystoneConfig, ImageMetadata, ImagesContext } from '../../types'; -import { parseImageRef } from '../../fields/types/image/utils'; -import { CloudAssetsAPI } from '../cloud/assets'; - -const DEFAULT_BASE_URL = '/images'; -export const DEFAULT_IMAGES_STORAGE_PATH = './public/images'; - -const getImageMetadataFromBuffer = async (buffer: Buffer): Promise => { - const filesize = buffer.length; - const fileType = fromBuffer(buffer); - if (!fileType) { - throw new Error('File type not found'); - } - - const extension = fileType.ext; - if (extension !== 'jpg' && extension !== 'png' && extension !== 'webp' && extension !== 'gif') { - throw new Error(`${extension} is not a supported image type`); - } - - const { height, width } = imageSize(buffer); - - if (width === undefined || height === undefined) { - throw new Error('Height and width could not be found for image'); - } - return { width, height, filesize, extension }; -}; - -export function createImagesContext( - config: KeystoneConfig, - cloudAssets: () => CloudAssetsAPI -): ImagesContext | undefined { - if (!config.images) { - return; - } - - const { images } = config; - const { baseUrl = DEFAULT_BASE_URL, storagePath = DEFAULT_IMAGES_STORAGE_PATH } = - images.local || {}; - - if (images.upload === 'local') { - fs.mkdirSync(storagePath, { recursive: true }); - } - - return { - getUrl: async (mode, id, extension) => { - if (mode === 'cloud') { - return cloudAssets().images.url(id, extension); - } - const filename = `${id}.${extension}`; - return `${baseUrl}/${filename}`; - }, - getDataFromRef: async ref => { - const imageRef = parseImageRef(ref); - - if (!imageRef) { - throw new Error('Invalid image reference'); - } - - const { mode } = imageRef; - - if (mode === 'cloud') { - const metadata = await cloudAssets().images.metadata(imageRef.id, imageRef.extension); - return { ...imageRef, ...metadata }; - } - - const buffer = await fs.readFile( - path.join(storagePath, `${imageRef.id}.${imageRef.extension}`) - ); - const metadata = await getImageMetadataFromBuffer(buffer); - - return { ...imageRef, ...metadata }; - }, - getDataFromStream: async stream => { - const { upload: mode } = images; - const id = uuid(); - - if (mode === 'cloud') { - const cloudMetadata = await cloudAssets().images.upload(stream, id); - - return { mode, id, ...cloudMetadata }; - } - - const chunks = []; - - for await (let chunk of stream) { - chunks.push(chunk); - } - - const buffer = Buffer.concat(chunks); - const metadata = await getImageMetadataFromBuffer(buffer); - - await fs.writeFile(path.join(storagePath, `${id}.${metadata.extension}`), buffer); - - return { mode, id, ...metadata }; - }, - }; -} diff --git a/packages/core/src/lib/core/types-for-lists.ts b/packages/core/src/lib/core/types-for-lists.ts index 9c57c5bee42..c8b93f0a5f0 100644 --- a/packages/core/src/lib/core/types-for-lists.ts +++ b/packages/core/src/lib/core/types-for-lists.ts @@ -367,7 +367,13 @@ export function initialiseLists(config: KeystoneConfig): Record config.storage?.[storage], + }); const omit = f.graphql?.omit; const read = omit !== true && !omit?.includes('read'); diff --git a/packages/core/src/lib/createSystem.ts b/packages/core/src/lib/createSystem.ts index 04f20242b3b..46434f956e2 100644 --- a/packages/core/src/lib/createSystem.ts +++ b/packages/core/src/lib/createSystem.ts @@ -5,7 +5,6 @@ import { createAdminMeta } from '../admin-ui/system/createAdminMeta'; import { createGraphQLSchema } from './createGraphQLSchema'; import { makeCreateContext } from './context/createContext'; import { initialiseLists } from './core/types-for-lists'; -import { CloudAssetsAPI, getCloudAssetsAPI } from './cloud/assets'; import { setWriteLimit } from './core/utils'; function getSudoGraphQLSchema(config: KeystoneConfig) { @@ -87,8 +86,6 @@ export function createSystem(config: KeystoneConfig, isLiveReload?: boolean) { prismaClient._engine.child?.kill('SIGINT'); }); - let cloudAssetsAPI: CloudAssetsAPI | undefined = undefined; - const createContext = makeCreateContext({ graphQLSchema, sudoGraphQLSchema, @@ -98,12 +95,6 @@ export function createSystem(config: KeystoneConfig, isLiveReload?: boolean) { Object.entries(lists).map(([listKey, list]) => [listKey, getGqlNames(list)]) ), lists, - cloudAssetsAPI: () => { - if (cloudAssetsAPI === undefined) { - throw new Error('Keystone Cloud config was not loaded'); - } - return cloudAssetsAPI; - }, }); return { @@ -113,15 +104,6 @@ export function createSystem(config: KeystoneConfig, isLiveReload?: boolean) { const context = createContext({ sudo: true }); await config.db.onConnect?.(context); } - if (config.experimental?.cloud?.apiKey) { - try { - cloudAssetsAPI = await getCloudAssetsAPI({ - apiKey: config.experimental.cloud.apiKey, - }); - } catch (err) { - console.error('failed to connect to Keystone Cloud', err); - } - } }, async disconnect() { await prismaClient.$disconnect(); diff --git a/packages/core/src/lib/server/createExpressServer.ts b/packages/core/src/lib/server/createExpressServer.ts index b075df68993..369822a4a66 100644 --- a/packages/core/src/lib/server/createExpressServer.ts +++ b/packages/core/src/lib/server/createExpressServer.ts @@ -6,8 +6,6 @@ import { graphqlUploadExpress } from 'graphql-upload'; import { ApolloServer } from 'apollo-server-express'; import type { KeystoneConfig, CreateContext, SessionStrategy, GraphQLConfig } from '../../types'; import { createSessionContext } from '../../session'; -import { DEFAULT_FILES_STORAGE_PATH } from '../context/createFilesContext'; -import { DEFAULT_IMAGES_STORAGE_PATH } from '../context/createImagesContext'; import { createApolloServerExpress } from './createApolloServer'; import { addHealthCheck } from './addHealthCheck'; @@ -104,18 +102,23 @@ export const createExpressServer = async ( config.server?.extendHttpServer(httpServer, createRequestContext); } - if (config.files) { - expressServer.use( - '/files', - express.static(config.files.local?.storagePath ?? DEFAULT_FILES_STORAGE_PATH) - ); - } - - if (config.images) { - expressServer.use( - '/images', - express.static(config.images.local?.storagePath ?? DEFAULT_IMAGES_STORAGE_PATH) - ); + if (config.storage) { + for (const val of Object.values(config.storage)) { + if (val.kind !== 'local' || !val.serverRoute) continue; + expressServer.use( + val.serverRoute.path, + express.static(val.storagePath, { + setHeaders(res) { + if (val.type === 'file') { + res.setHeader('Content-Type', 'application/octet-stream'); + } + }, + index: false, + redirect: false, + lastModified: false, + }) + ); + } } const apolloServer = await addApolloServer({ diff --git a/packages/core/src/scripts/run/dev.ts b/packages/core/src/scripts/run/dev.ts index 3977895c1e4..7e414977b56 100644 --- a/packages/core/src/scripts/run/dev.ts +++ b/packages/core/src/scripts/run/dev.ts @@ -340,6 +340,15 @@ async function setupInitialKeystone( createContext ); console.log(`✅ GraphQL API ready`); + + // Make local storage folders if used + for (const val of Object.values(config.storage || {})) { + if (val.kind !== 'local') continue; + + fs.mkdirSync(val.storagePath, { recursive: true }); + console.warn(`WARNING: 'mkdir -p ${val.storagePath}' won't happen in production`); + } + return { adminMeta, disconnect: () => keystone.disconnect(), diff --git a/packages/core/src/types/config/index.ts b/packages/core/src/types/config/index.ts index 4a33d1a34b5..a2389c30384 100644 --- a/packages/core/src/types/config/index.ts +++ b/packages/core/src/types/config/index.ts @@ -19,6 +19,64 @@ import type { BaseFields } from './fields'; import type { ListAccessControl, FieldAccessControl } from './access-control'; import type { ListHooks } from './hooks'; +type FileOrImage = + // is given full file name, returns file name that will be used at + | { type: 'file'; transformName?: (filename: string) => MaybePromise } + // return does not include extension, extension is handed over in case they want to use it + | { + type: 'image'; + // is given full file name, returns file name that will be used at + transformName?: (filename: string, extension: string) => MaybePromise; + }; + +export type StorageConfig = ( + | { + /** The kind of storage being configured */ + kind: 'local'; + /** The path to where the asset will be stored on disc, eg 'public/images' */ + storagePath: string; + /** A function that receives a partial url, whose return will be used as the URL in graphql + * + * For example, a local dev usage of this might be: + * ```ts + * path => `http://localhost:3000/images${path}` + * ``` + */ + generateUrl: (path: string) => string; + /** The configuration for keystone's hosting of the assets - if set to null, keystone will not host the assets */ + serverRoute: { + /** The partial path that the assets will be hosted at by keystone, eg `/images` or `/our-cool-files` */ + path: string; + } | null; + /** Sets whether the assets should be preserved locally on removal from keystone's database */ + preserve?: boolean; + transformName?: (filename: string) => string; + } + | { + /** The kind of storage being configured */ + kind: 's3'; + /** Sets signing of the asset - for use when you want private assets */ + signed?: { expiry: number }; + generateUrl?: (path: string) => string; + /** Sets whether the assets should be preserved locally on removal from keystone's database */ + preserve?: boolean; + pathPrefix?: string; + /** Your s3 instance's bucket name */ + bucketName: string; + /** Your s3 instance's region */ + region: string; + /** An access Key ID with write access to your S3 instance */ + accessKeyId: string; + /** The secret access key that gives permissions to your access Key Id */ + secretAccessKey: string; + /** An endpoint to use - to be provided if you are not using AWS as your endpoint */ + endpoint?: string; + /** If true, will force the 'old' S3 path style of putting bucket name at the start of the pathname of the URL */ + forcePathStyle?: boolean; + } +) & + FileOrImage; + export type KeystoneConfig = { lists: ListSchemaConfig; db: DatabaseConfig; @@ -27,8 +85,14 @@ export type KeystoneConfig; graphql?: GraphQLConfig; extendGraphqlSchema?: ExtendGraphqlSchema; - files?: FilesConfig; - images?: ImagesConfig; + /** An object containing configuration about keystone's various external storages. + * + * Each entry should be of either `kind: 'local'` or `kind: 's3'`, and follow the configuration of each. + * + * When configuring a `file` or `image` field that uses the storage, use the key in the storage object + * as the `storage` option for that field. + */ + storage?: Record; /** Experimental config options */ experimental?: { /** Enables nextjs graphql api route mode */ @@ -37,8 +101,6 @@ export type KeystoneConfig GraphQLSchema; -// config.files - export type FilesConfig = { upload: AssetMode; transformFilename?: (str: string) => string; @@ -195,8 +255,6 @@ export type FilesConfig = { }; }; -// config.images - export type ImagesConfig = { upload: AssetMode; local?: { @@ -213,12 +271,6 @@ export type ImagesConfig = { }; }; -// config.experimental.cloud - -export type CloudConfig = { - apiKey?: string; -}; - // Exports from sibling packages export type { ListHooks, ListAccessControl, FieldAccessControl }; diff --git a/packages/core/src/types/context.ts b/packages/core/src/types/context.ts index 078a37e26f5..67e7ad03fad 100644 --- a/packages/core/src/types/context.ts +++ b/packages/core/src/types/context.ts @@ -14,8 +14,8 @@ export type KeystoneContext KeystoneContext; withSession: (session: any) => KeystoneContext; prisma: TypeInfo['prisma']; - files: FilesContext | undefined; - images: ImagesContext | undefined; + files: FilesContext; + images: ImagesContext; totalResults: number; maxTotalResults: number; /** @deprecated */ @@ -160,20 +160,23 @@ export type SessionContext = { endSession(): Promise; }; -export type AssetMode = 'local' | 'cloud'; +export type AssetMode = 'local' | 's3'; // Files API -export type FileData = { - mode: AssetMode; +export type FileMetadata = { filename: string; filesize: number; }; -export type FilesContext = { - getUrl: (mode: AssetMode, filename: string) => Promise; - getDataFromRef: (ref: string) => Promise; +export type FileData = { + filename: string; +} & FileMetadata; + +export type FilesContext = (storage: string) => { + getUrl: (filename: string) => Promise; getDataFromStream: (stream: Readable, filename: string) => Promise; + deleteAtSource: (filename: string) => Promise; }; // Images API @@ -188,12 +191,11 @@ export type ImageMetadata = { }; export type ImageData = { - mode: AssetMode; id: string; } & ImageMetadata; -export type ImagesContext = { - getUrl: (mode: AssetMode, id: string, extension: ImageExtension) => Promise; - getDataFromRef: (ref: string) => Promise; - getDataFromStream: (stream: Readable) => Promise; +export type ImagesContext = (storage: string) => { + getUrl: (id: string, extension: ImageExtension) => Promise; + getDataFromStream: (stream: Readable, filename: string) => Promise; + deleteAtSource: (id: string, extension: ImageExtension) => Promise; }; diff --git a/packages/core/src/types/next-fields.ts b/packages/core/src/types/next-fields.ts index ad9021382fe..1e3d212695c 100644 --- a/packages/core/src/types/next-fields.ts +++ b/packages/core/src/types/next-fields.ts @@ -3,7 +3,7 @@ import { graphql } from '..'; import { BaseListTypeInfo } from './type-info'; import { CommonFieldConfig } from './config'; import { DatabaseProvider } from './core'; -import { AdminMetaRootVal, JSONValue, KeystoneContext, MaybePromise } from '.'; +import { AdminMetaRootVal, JSONValue, KeystoneContext, MaybePromise, StorageConfig } from '.'; export { Decimal }; @@ -14,6 +14,7 @@ export type ListGraphQLTypes = { types: GraphQLTypesForList }; export type FieldData = { lists: Record; provider: DatabaseProvider; + getStorage: (storage: string) => StorageConfig | undefined; listKey: string; fieldKey: string; }; diff --git a/tests/api-tests/fields/crud.test.ts b/tests/api-tests/fields/crud.test.ts index 61774d11a75..6159613b045 100644 --- a/tests/api-tests/fields/crud.test.ts +++ b/tests/api-tests/fields/crud.test.ts @@ -29,8 +29,7 @@ testModules }, }), }, - images: { upload: 'local', local: { storagePath: 'tmp_test_images' } }, - files: { upload: 'local', local: { storagePath: 'tmp_test_files' } }, + ...mod.getRootConfig?.(matrixValue), }), }); const keystoneTestWrapper = (testFn: (args: any) => void = () => {}) => @@ -39,29 +38,35 @@ testModules for (const data of mod.initItems(matrixValue, context)) { await context.query[listKey].createOne({ data }); } - return testFn({ context, listKey, provider: process.env.TEST_ADAPTER, ...rest }); + return testFn({ + context, + listKey, + provider: process.env.TEST_ADAPTER, + matrixValue, + ...rest, + }); }); if (mod.crudTests) { describe(`${mod.name} - ${matrixValue} - Custom CRUD operations`, () => { beforeAll(() => { if (mod.beforeAll) { - mod.beforeAll(); + mod.beforeAll(matrixValue); } }); afterEach(async () => { if (mod.afterEach) { - await mod.afterEach(); + await mod.afterEach(matrixValue); } }); beforeEach(() => { if (mod.beforeEach) { - mod.beforeEach(); + mod.beforeEach(matrixValue); } }); afterAll(async () => { if (mod.afterAll) { - await mod.afterAll(); + await mod.afterAll(matrixValue); } }); mod.crudTests(keystoneTestWrapper); @@ -72,22 +77,22 @@ testModules describe(`${mod.name} - ${matrixValue} - CRUD operations`, () => { beforeEach(() => { if (mod.beforeEach) { - mod.beforeEach(); + mod.beforeEach(matrixValue); } }); beforeAll(() => { if (mod.beforeAll) { - mod.beforeAll(); + mod.beforeAll(matrixValue); } }); afterEach(async () => { if (mod.afterEach) { - await mod.afterEach(); + await mod.afterEach(matrixValue); } }); afterAll(async () => { if (mod.afterAll) { - await mod.afterAll(); + await mod.afterAll(matrixValue); } }); diff --git a/tests/api-tests/fields/filter.test.ts b/tests/api-tests/fields/filter.test.ts index fc27c6969de..b1ce8ebf9b0 100644 --- a/tests/api-tests/fields/filter.test.ts +++ b/tests/api-tests/fields/filter.test.ts @@ -1,3 +1,6 @@ +import fs from 'fs'; +import path from 'path'; +import os from 'os'; import globby from 'globby'; import { list } from '@keystone-6/core'; import { text } from '@keystone-6/core/fields'; @@ -24,8 +27,26 @@ testModules fields: { name: text(), ...mod.getTestFields(matrixValue) }, }), }, - images: { upload: 'local', local: { storagePath: 'tmp_test_images' } }, - files: { upload: 'local', local: { storagePath: 'tmp_test_files' } }, + storage: { + test_image: { + kind: 'local', + type: 'image', + storagePath: fs.mkdtempSync(path.join(os.tmpdir(), 'tmp_test_images')), + generateUrl: path => `http://localhost:3000/images${path}`, + serverRoute: { + path: '/images', + }, + }, + test_file: { + kind: 'local', + type: 'file', + storagePath: fs.mkdtempSync(path.join(os.tmpdir(), 'tmp_test_files')), + generateUrl: path => `http://localhost:3000/files${path}`, + serverRoute: { + path: '/files', + }, + }, + }, }), }); const withKeystone = (testFn: (args: any) => void = () => {}) => @@ -42,22 +63,22 @@ testModules describe(`${mod.name} - ${matrixValue} - Custom Filtering`, () => { beforeEach(() => { if (mod.beforeEach) { - mod.beforeEach(); + mod.beforeEach(matrixValue); } }); afterEach(async () => { if (mod.afterEach) { - await mod.afterEach(); + await mod.afterEach(matrixValue); } }); beforeAll(() => { if (mod.beforeAll) { - mod.beforeAll(); + mod.beforeAll(matrixValue); } }); afterAll(async () => { if (mod.afterAll) { - await mod.afterAll(); + await mod.afterAll(matrixValue); } }); mod.filterTests(withKeystone, matrixValue); @@ -68,22 +89,22 @@ testModules describe(`${mod.name} - ${matrixValue} - Common Filtering`, () => { beforeEach(() => { if (mod.beforeEach) { - mod.beforeEach(); + mod.beforeEach(matrixValue); } }); afterEach(async () => { if (mod.afterEach) { - await mod.afterEach(); + await mod.afterEach(matrixValue); } }); beforeAll(() => { if (mod.beforeAll) { - mod.beforeAll(); + mod.beforeAll(matrixValue); } }); afterAll(async () => { if (mod.afterAll) { - await mod.afterAll(); + await mod.afterAll(matrixValue); } }); const { readFieldName, fieldName, subfieldName, storedValues: _storedValues } = mod; diff --git a/tests/api-tests/fields/image-file-crud.test.ts b/tests/api-tests/fields/image-file-crud.test.ts new file mode 100644 index 00000000000..5cf32f19ec2 --- /dev/null +++ b/tests/api-tests/fields/image-file-crud.test.ts @@ -0,0 +1,394 @@ +import path from 'path'; +import { createHash } from 'crypto'; +import os from 'os'; +import fs from 'fs-extra'; +import fetch from 'node-fetch'; +import { Upload } from 'graphql-upload'; +import mime from 'mime'; +import { file, text, image } from '@keystone-6/core/fields'; +import { list } from '@keystone-6/core'; +import { KeystoneConfig, StorageConfig } from '@keystone-6/core/types'; +import { setupTestRunner } from '@keystone-6/core/testing'; +import { apiTestConfig, expectSingleResolverError } from '../utils'; + +const fieldPath = path.resolve(__dirname, '../../..', 'packages/core/src/fields/types'); + +export const prepareFile = (_filePath: string, kind: 'image' | 'file') => { + const filePath = path.resolve(fieldPath, kind, 'test-files', _filePath); + const upload = new Upload(); + upload.resolve({ + createReadStream: () => fs.createReadStream(filePath), + filename: path.basename(filePath), + // @ts-ignore + mimetype: mime.getType(filePath), + encoding: 'utf-8', + }); + return { upload }; +}; + +type MatrixValue = 's3' | 'local'; +export const testMatrix: Array = ['local']; + +if (process.env.S3_BUCKET_NAME) { + testMatrix.push('s3'); +} + +const s3DefaultStorage = { + kind: 's3', + bucketName: process.env.S3_BUCKET_NAME!, + accessKeyId: process.env.S3_ACCESS_KEY_ID!, + secretAccessKey: process.env.S3_SECRET_ACCESS_KEY!, + region: process.env.S3_REGION!, + endpoint: process.env.S3_ENDPOINT, + forcePathStyle: process.env.S3_FORCE_PATH_STYLE === 'true', +} as const; + +const getRunner = ({ + storage, + fields, +}: { + storage: Record; + fields: KeystoneConfig['lists'][string]['fields']; +}) => + setupTestRunner({ + config: apiTestConfig({ + db: {}, + storage, + lists: { + Test: list({ + fields: { + name: text(), + ...fields, + }, + }), + }, + }), + }); + +const getFileHash = async ( + filename: string, + config: { matrixValue: 's3' } | { matrixValue: 'local'; folder: string } +) => { + let contentFromURL; + + if (config.matrixValue === 's3') { + contentFromURL = await fetch(filename).then(x => x.buffer()); + } else { + contentFromURL = await fs.readFile(path.join(config.folder, filename)); + } + + return createHash('sha1').update(contentFromURL).digest('hex'); +}; + +const checkFile = async ( + filename: string, + config: { matrixValue: 's3' } | { matrixValue: 'local'; folder: string } +) => { + if (config.matrixValue === 's3') { + return await fetch(filename).then(x => x.status === 200); + } else { + return fs.existsSync(path.join(config.folder, filename)); + } +}; + +describe('File - Crud special tests', () => { + const filename = 'keystone.jpeg'; + const fileHash = createHash('sha1') + .update(fs.readFileSync(path.resolve(fieldPath, 'image/test-files', filename))) + .digest('hex'); + + const createItem = (context: any) => + context.query.Test.createOne({ + data: { secretFile: prepareFile(filename, 'image') }, + query: ` + id + secretFile { + filename + __typename + filesize + url + } + `, + }); + + for (let matrixValue of testMatrix) { + const getConfig = (): StorageConfig => ({ + ...(matrixValue === 's3' + ? { ...s3DefaultStorage, preserve: false, type: 'file' } + : { + kind: 'local', + type: 'file', + storagePath: fs.mkdtempSync(path.join(os.tmpdir(), 'file-local-test')), + serverRoute: { path: '/files' }, + generateUrl: (path: string) => `http://localhost:3000/files${path}`, + }), + }); + + const fields = { secretFile: file({ storage: 'test_file' }) }; + + describe(matrixValue, () => { + describe('Create - upload', () => { + const config = getConfig(); + const hashConfig: { matrixValue: 'local'; folder: string } | { matrixValue: 's3' } = + config.kind === 'local' + ? { matrixValue: 'local', folder: `${config.storagePath}/`! } + : { matrixValue: config.kind }; + test( + 'Upload values should match expected corrected', + getRunner({ storage: { test_file: { ...config } }, fields })(async ({ context }) => { + const data = await createItem(context); + expect(data).not.toBe(null); + + expect(data.secretFile).toEqual({ + /* + The url and filename here include a hash, and currently what we are doing + is just checking the url is modified correctly - that said, this sucks + as a test + */ + url: + matrixValue === 's3' + ? expect.stringContaining(`/${data.secretFile.filename}`) + : `http://localhost:3000/files/${data.secretFile.filename}`, + filename: data.secretFile.filename, + __typename: 'FileFieldOutput', + filesize: 3250, + }); + // check file exists at location + expect(fileHash).toEqual(await getFileHash(data.secretFile.filename, hashConfig)); + }) + ); + }); + + describe('After Operation Hook', () => { + const config = getConfig(); + const hashConfig = + config.kind === 'local' + ? { matrixValue: config.kind, folder: `${config.storagePath}/`! } + : { matrixValue: config.kind }; + test( + 'with preserve: true', + getRunner({ storage: { test_file: { ...config, preserve: true } }, fields })( + async ({ context }) => { + const { + id, + secretFile: { filename }, + } = await createItem(context); + + expect(await checkFile(filename, hashConfig)).toBeTruthy(); + + const { + secretFile: { filename: filename2 }, + } = await context.query.Test.updateOne({ + where: { id }, + data: { secretFile: prepareFile('thinkmill.jpg', 'file') }, + query: `secretFile { filename }`, + }); + + expect(await checkFile(filename, hashConfig)).toBeTruthy(); + expect(await checkFile(filename2, hashConfig)).toBeTruthy(); + + await context.query.Test.deleteOne({ where: { id } }); + + expect(await checkFile(filename, hashConfig)).toBeTruthy(); + // TODO test that just nulling the field doesn't delete it + } + ) + ); + test( + 'with preserve: false', + getRunner({ + storage: { test_file: { ...config, preserve: false } }, + fields, + })(async ({ context }) => { + const { + id, + secretFile: { filename }, + } = await createItem(context); + + expect(await checkFile(filename, hashConfig)).toBeTruthy(); + + const { + secretFile: { filename: filename2 }, + } = await context.query.Test.updateOne({ + where: { id }, + data: { secretFile: prepareFile('thinkmill.jpg', 'file') }, + query: ` + secretFile { + filename + }`, + }); + + expect(await checkFile(filename2, hashConfig)).toBeTruthy(); + expect(await checkFile(filename, hashConfig)).toBeFalsy(); + + await context.query.Test.deleteOne({ where: { id } }); + + expect(await checkFile(filename2, hashConfig)).toBeFalsy(); + + // TODO test that just nulling the field removes the file + }) + ); + }); + }); + } +}); + +describe('Image - Crud special tests', () => { + const createItem = async (context: any, filename: string) => + await context.query.Test.createOne({ + data: { avatar: prepareFile(filename, 'image') }, + query: ` + id + avatar { + __typename + id + filesize + width + height + extension + url + } + `, + }); + + for (let matrixValue of testMatrix) { + const getConfig = (): StorageConfig => ({ + ...(matrixValue === 's3' + ? { ...s3DefaultStorage, type: 'image', preserve: false } + : { + kind: 'local', + type: 'image', + storagePath: fs.mkdtempSync(path.join(os.tmpdir(), 'image-local-test')), + serverRoute: { path: '/images' }, + generateUrl: (path: string) => `http://localhost:3000/images${path}`, + preserve: false, + }), + }); + const fields = { avatar: image({ storage: 'test_image' }) }; + const config = getConfig(); + const hashConfig = + config.kind === 'local' + ? { matrixValue: config.kind, folder: `${config.storagePath}/`! } + : { matrixValue: config.kind }; + + describe(matrixValue, () => { + describe('Create - upload', () => { + for (let matrixValue of testMatrix) { + test( + 'upload values should match expected', + getRunner({ fields, storage: { test_image: config } })(async ({ context }) => { + const filenames = ['keystone.jpg']; + for (const filename of filenames) { + const fileHash = createHash('sha1') + .update(fs.readFileSync(path.resolve(fieldPath, 'image/test-files', filename))) + .digest('hex'); + + const data = await createItem(context, filename); + expect(data).not.toBe(null); + + expect(data.avatar).toEqual({ + url: + matrixValue === 's3' + ? expect.stringContaining(`/${data.avatar.id}.jpg`) + : `http://localhost:3000/images/${data.avatar.id}.jpg`, + id: data.avatar.id, + __typename: 'ImageFieldOutput', + filesize: 3250, + width: 150, + height: 152, + extension: 'jpg', + }); + + expect(fileHash).toEqual( + await getFileHash(`${data.avatar.id}.${data.avatar.extension}`, hashConfig) + ); + } + }) + ); + test( + 'if not image file, throw', + getRunner({ fields, storage: { test_image: config } })(async ({ context }) => { + const { data, errors } = await context.graphql.raw({ + query: ` + mutation ($item: TestCreateInput!) { + createTest(data: $item) { + avatar { + id + } + } + } + `, + variables: { item: { avatar: prepareFile('badfile.txt', 'image') } }, + }); + expect(data).toEqual({ createTest: null }); + const message = `File type not found`; + expectSingleResolverError(errors, 'createTest', 'Test.avatar', message); + }) + ); + + describe('After Operation Hook', () => { + test( + 'with preserve: true', + getRunner({ fields, storage: { test_image: { ...config, preserve: true } } })( + async ({ context }) => { + const ogFilename = 'keystone.jpeg'; + + const { id, avatar } = await createItem(context, ogFilename); + + await context.query.Test.updateOne({ + where: { id }, + data: { avatar: prepareFile('thinkmill.jpg', 'image') }, + }); + + expect( + await checkFile(`${avatar.id}.${avatar.extension}`, hashConfig) + ).toBeTruthy(); + + await context.query.Test.deleteOne({ where: { id } }); + + expect( + await checkFile(`${avatar.id}.${avatar.extension}`, hashConfig) + ).toBeTruthy(); + // TODO test that just nulling the field doesn't delete it + } + ) + ); + + test( + 'with preserve: false', + getRunner({ + fields, + storage: { test_image: { ...config, preserve: false } }, + })(async ({ context }) => { + const ogFilename = 'keystone.jpeg'; + const { id, avatar } = await createItem(context, ogFilename); + const filename = `${avatar.id}.${avatar.extension}`; + + expect(await checkFile(filename, hashConfig)).toBeTruthy(); + const { avatar: avatar2 } = await context.query.Test.updateOne({ + where: { id }, + data: { avatar: prepareFile('thinkmill.jpg', 'image') }, + query: `avatar { + id + extension + }`, + }); + + const filename2 = `${avatar2.id}.${avatar2.extension}`; + + expect(await checkFile(filename, hashConfig)).toBeFalsy(); + expect(await checkFile(filename2, hashConfig)).toBeTruthy(); + + await context.query.Test.deleteOne({ where: { id } }); + + expect(await checkFile(filename2, hashConfig)).toBeFalsy(); + + // TODO test that just nulling the field removes the file + }) + ); + }); + } + }); + }); + } +}); diff --git a/tests/api-tests/fields/non-null.test.ts b/tests/api-tests/fields/non-null.test.ts index f5dba723d14..bf6abbdcab1 100644 --- a/tests/api-tests/fields/non-null.test.ts +++ b/tests/api-tests/fields/non-null.test.ts @@ -1,3 +1,6 @@ +import fs from 'fs'; +import path from 'path'; +import os from 'os'; import globby from 'globby'; import { list } from '@keystone-6/core'; import { text } from '@keystone-6/core/fields'; @@ -19,22 +22,22 @@ testModules describe(`${mod.name} - ${matrixValue} - graphql.isNonNull`, () => { beforeEach(() => { if (mod.beforeEach) { - mod.beforeEach(); + mod.beforeEach(matrixValue); } }); afterEach(async () => { if (mod.afterEach) { - await mod.afterEach(); + await mod.afterEach(matrixValue); } }); beforeAll(() => { if (mod.beforeAll) { - mod.beforeAll(); + mod.beforeAll(matrixValue); } }); afterAll(async () => { if (mod.afterAll) { - await mod.afterAll(); + await mod.afterAll(matrixValue); } }); @@ -52,8 +55,26 @@ testModules }, }), }, - images: { upload: 'local', local: { storagePath: 'tmp_test_images' } }, - files: { upload: 'local', local: { storagePath: 'tmp_test_files' } }, + storage: { + test_image: { + kind: 'local', + type: 'image', + storagePath: fs.mkdtempSync(path.join(os.tmpdir(), 'tmp_test_images')), + generateUrl: path => `http://localhost:3000/images${path}`, + serverRoute: { + path: '/images', + }, + }, + test_file: { + kind: 'local', + type: 'file', + storagePath: fs.mkdtempSync(path.join(os.tmpdir(), 'tmp_test_files')), + generateUrl: path => `http://localhost:3000/files${path}`, + serverRoute: { + path: '/files', + }, + }, + }, }), }); return testArgs.context.graphql.schema; diff --git a/tests/api-tests/fields/required.test.ts b/tests/api-tests/fields/required.test.ts index a71ae0cc494..b2905c9e03f 100644 --- a/tests/api-tests/fields/required.test.ts +++ b/tests/api-tests/fields/required.test.ts @@ -1,3 +1,6 @@ +import fs from 'fs'; +import path from 'path'; +import os from 'os'; import globby from 'globby'; import { list } from '@keystone-6/core'; import { text } from '@keystone-6/core/fields'; @@ -19,22 +22,22 @@ testModules describe(`${mod.name} - ${matrixValue} - isRequired`, () => { beforeEach(() => { if (mod.beforeEach) { - mod.beforeEach(); + mod.beforeEach(matrixValue); } }); afterEach(async () => { if (mod.afterEach) { - await mod.afterEach(); + await mod.afterEach(matrixValue); } }); beforeAll(() => { if (mod.beforeAll) { - mod.beforeAll(); + mod.beforeAll(matrixValue); } }); afterAll(async () => { if (mod.afterAll) { - await mod.afterAll(); + await mod.afterAll(matrixValue); } }); @@ -56,8 +59,26 @@ testModules }, }), }, - images: { upload: 'local', local: { storagePath: 'tmp_test_images' } }, - files: { upload: 'local', local: { storagePath: 'tmp_test_files' } }, + storage: { + test_image: { + kind: 'local', + type: 'image', + storagePath: fs.mkdtempSync(path.join(os.tmpdir(), 'tmp_test_images')), + generateUrl: path => `http://localhost:3000/images${path}`, + serverRoute: { + path: '/images', + }, + }, + test_file: { + kind: 'local', + type: 'file', + storagePath: fs.mkdtempSync(path.join(os.tmpdir(), 'tmp_test_files')), + generateUrl: path => `http://localhost:3000/files${path}`, + serverRoute: { + path: '/files', + }, + }, + }, }), }); diff --git a/tests/api-tests/fields/unique.test.ts b/tests/api-tests/fields/unique.test.ts index d823ffbdab7..7092072d22a 100644 --- a/tests/api-tests/fields/unique.test.ts +++ b/tests/api-tests/fields/unique.test.ts @@ -1,3 +1,6 @@ +import fs from 'fs'; +import path from 'path'; +import os from 'os'; import globby from 'globby'; import { list } from '@keystone-6/core'; import { text } from '@keystone-6/core/fields'; @@ -19,22 +22,22 @@ testModules describe(`${mod.name} - ${matrixValue} - isIndexed: 'unique'`, () => { beforeEach(() => { if (mod.beforeEach) { - mod.beforeEach(); + mod.beforeEach(matrixValue); } }); afterEach(async () => { if (mod.afterEach) { - await mod.afterEach(); + await mod.afterEach(matrixValue); } }); beforeAll(() => { if (mod.beforeAll) { - mod.beforeAll(); + mod.beforeAll(matrixValue); } }); afterAll(async () => { if (mod.afterAll) { - await mod.afterAll(); + await mod.afterAll(matrixValue); } }); const runner = setupTestRunner({ @@ -50,8 +53,26 @@ testModules }, }), }, - images: { upload: 'local', local: { storagePath: 'tmp_test_images' } }, - files: { upload: 'local', local: { storagePath: 'tmp_test_files' } }, + storage: { + test_image: { + kind: 'local', + type: 'image', + storagePath: fs.mkdtempSync(path.join(os.tmpdir(), 'tmp_test_images')), + generateUrl: path => `http://localhost:3000/images${path}`, + serverRoute: { + path: '/images', + }, + }, + test_file: { + kind: 'local', + type: 'file', + storagePath: fs.mkdtempSync(path.join(os.tmpdir(), 'tmp_test_files')), + generateUrl: path => `http://localhost:3000/files${path}`, + serverRoute: { + path: '/files', + }, + }, + }, }), }); test( @@ -158,8 +179,26 @@ testModules }, }), }, - images: { upload: 'local', local: { storagePath: 'tmp_test_images' } }, - files: { upload: 'local', local: { storagePath: 'tmp_test_files' } }, + storage: { + test_image: { + kind: 'local', + type: 'image', + storagePath: fs.mkdtempSync(path.join(os.tmpdir(), 'tmp_test_images')), + generateUrl: path => `http://localhost:3000/images${path}`, + serverRoute: { + path: '/images', + }, + }, + test_file: { + kind: 'local', + type: 'file', + storagePath: fs.mkdtempSync(path.join(os.tmpdir(), 'tmp_test_files')), + generateUrl: path => `http://localhost:3000/images${path}`, + serverRoute: { + path: '/images', + }, + }, + }, }), }); } catch (error: any) { diff --git a/tests/api-tests/fields/unsupported.test.ts b/tests/api-tests/fields/unsupported.test.ts index 7d7590dd08a..2bd0f0a0701 100644 --- a/tests/api-tests/fields/unsupported.test.ts +++ b/tests/api-tests/fields/unsupported.test.ts @@ -1,3 +1,6 @@ +import fs from 'fs'; +import path from 'path'; +import os from 'os'; import globby from 'globby'; import { list } from '@keystone-6/core'; import { text } from '@keystone-6/core/fields'; @@ -21,22 +24,22 @@ if (unsupportedModules.length > 0) { describe(`${mod.name} - Unsupported field type`, () => { beforeEach(() => { if (mod.beforeEach) { - mod.beforeEach(); + mod.beforeEach(matrixValue); } }); afterEach(async () => { if (mod.afterEach) { - await mod.afterEach(); + await mod.afterEach(matrixValue); } }); beforeAll(() => { if (mod.beforeAll) { - mod.beforeAll(); + mod.beforeAll(matrixValue); } }); afterAll(async () => { if (mod.afterAll) { - await mod.afterAll(); + await mod.afterAll(matrixValue); } }); @@ -50,8 +53,26 @@ if (unsupportedModules.length > 0) { fields: { name: text(), ...mod.getTestFields(matrixValue) }, }), }, - images: { upload: 'local', local: { storagePath: 'tmp_test_images' } }, - files: { upload: 'local', local: { storagePath: 'tmp_test_files' } }, + storage: { + test_image: { + kind: 'local', + type: 'image', + storagePath: fs.mkdtempSync(path.join(os.tmpdir(), 'tmp_test_images')), + generateUrl: path => `http://localhost:3000/images${path}`, + serverRoute: { + path: '/images', + }, + }, + test_file: { + kind: 'local', + type: 'file', + storagePath: fs.mkdtempSync(path.join(os.tmpdir(), 'tmp_test_files')), + generateUrl: path => `http://localhost:3000/files${path}`, + serverRoute: { + path: '/files', + }, + }, + }, }), }) ).rejects.toThrow(Error); diff --git a/tests/api-tests/package.json b/tests/api-tests/package.json index 17d52d84b19..c03f327cf72 100644 --- a/tests/api-tests/package.json +++ b/tests/api-tests/package.json @@ -18,9 +18,13 @@ "apollo-server-types": "^3.4.0", "cookie-signature": "^1.1.0", "cuid": "^2.1.8", + "fs-extra": "^10.0.0", "globby": "^11.0.4", "graphql": "^15.8.0", + "graphql-upload": "^13.0.0", "memoize-one": "^6.0.0", + "mime": "^3.0.0", + "node-fetch": "^2.6.7", "superagent": "^6.1.0", "supertest": "^6.1.6", "testcheck": "^1.0.0-rc.2", diff --git a/tests/api-tests/s3-public-read-policy.json b/tests/api-tests/s3-public-read-policy.json new file mode 100644 index 00000000000..7d03951591a --- /dev/null +++ b/tests/api-tests/s3-public-read-policy.json @@ -0,0 +1,12 @@ +{ + "Version": "2012-10-17", + "Statement": [ + { + "Sid": "PublicRead", + "Effect": "Allow", + "Principal": "*", + "Action": ["s3:GetObject", "s3:GetObjectVersion"], + "Resource": ["arn:aws:s3:::keystone-test/*"] + } + ] +} diff --git a/yarn.lock b/yarn.lock index cd292fd09f3..94916305a0e 100644 --- a/yarn.lock +++ b/yarn.lock @@ -63,6 +63,930 @@ dependencies: xss "^1.0.8" +"@aws-crypto/crc32@2.0.0": + version "2.0.0" + resolved "https://registry.yarnpkg.com/@aws-crypto/crc32/-/crc32-2.0.0.tgz#4ad432a3c03ec3087c5540ff6e41e6565d2dc153" + integrity sha512-TvE1r2CUueyXOuHdEigYjIZVesInd9KN+K/TFFNfkkxRThiNxO6i4ZqqAVMoEjAamZZ1AA8WXJkjCz7YShHPQA== + dependencies: + "@aws-crypto/util" "^2.0.0" + "@aws-sdk/types" "^3.1.0" + tslib "^1.11.1" + +"@aws-crypto/crc32c@2.0.0": + version "2.0.0" + resolved "https://registry.yarnpkg.com/@aws-crypto/crc32c/-/crc32c-2.0.0.tgz#4235336ef78f169f6a05248906703b9b78da676e" + integrity sha512-vF0eMdMHx3O3MoOXUfBZry8Y4ZDtcuskjjKgJz8YfIDjLStxTZrYXk+kZqtl6A0uCmmiN/Eb/JbC/CndTV1MHg== + dependencies: + "@aws-crypto/util" "^2.0.0" + "@aws-sdk/types" "^3.1.0" + tslib "^1.11.1" + +"@aws-crypto/ie11-detection@^2.0.0": + version "2.0.0" + resolved "https://registry.yarnpkg.com/@aws-crypto/ie11-detection/-/ie11-detection-2.0.0.tgz#bb6c2facf8f03457e949dcf0921477397ffa4c6e" + integrity sha512-pkVXf/dq6PITJ0jzYZ69VhL8VFOFoPZLZqtU/12SGnzYuJOOGNfF41q9GxdI1yqC8R13Rq3jOLKDFpUJFT5eTA== + dependencies: + tslib "^1.11.1" + +"@aws-crypto/sha1-browser@2.0.0": + version "2.0.0" + resolved "https://registry.yarnpkg.com/@aws-crypto/sha1-browser/-/sha1-browser-2.0.0.tgz#71e735df20ea1d38f59259c4b1a2e00ca74a0eea" + integrity sha512-3fIVRjPFY8EG5HWXR+ZJZMdWNRpwbxGzJ9IH9q93FpbgCH8u8GHRi46mZXp3cYD7gealmyqpm3ThZwLKJjWJhA== + dependencies: + "@aws-crypto/ie11-detection" "^2.0.0" + "@aws-crypto/supports-web-crypto" "^2.0.0" + "@aws-sdk/types" "^3.1.0" + "@aws-sdk/util-locate-window" "^3.0.0" + "@aws-sdk/util-utf8-browser" "^3.0.0" + tslib "^1.11.1" + +"@aws-crypto/sha256-browser@2.0.0": + version "2.0.0" + resolved "https://registry.yarnpkg.com/@aws-crypto/sha256-browser/-/sha256-browser-2.0.0.tgz#741c9024df55ec59b51e5b1f5d806a4852699fb5" + integrity sha512-rYXOQ8BFOaqMEHJrLHul/25ckWH6GTJtdLSajhlqGMx0PmSueAuvboCuZCTqEKlxR8CQOwRarxYMZZSYlhRA1A== + dependencies: + "@aws-crypto/ie11-detection" "^2.0.0" + "@aws-crypto/sha256-js" "^2.0.0" + "@aws-crypto/supports-web-crypto" "^2.0.0" + "@aws-crypto/util" "^2.0.0" + "@aws-sdk/types" "^3.1.0" + "@aws-sdk/util-locate-window" "^3.0.0" + "@aws-sdk/util-utf8-browser" "^3.0.0" + tslib "^1.11.1" + +"@aws-crypto/sha256-js@2.0.0": + version "2.0.0" + resolved "https://registry.yarnpkg.com/@aws-crypto/sha256-js/-/sha256-js-2.0.0.tgz#f1f936039bdebd0b9e2dd834d65afdc2aac4efcb" + integrity sha512-VZY+mCY4Nmrs5WGfitmNqXzaE873fcIZDu54cbaDaaamsaTOP1DBImV9F4pICc3EHjQXujyE8jig+PFCaew9ig== + dependencies: + "@aws-crypto/util" "^2.0.0" + "@aws-sdk/types" "^3.1.0" + tslib "^1.11.1" + +"@aws-crypto/sha256-js@^2.0.0": + version "2.0.1" + resolved "https://registry.yarnpkg.com/@aws-crypto/sha256-js/-/sha256-js-2.0.1.tgz#79e1e6cf61f652ef2089c08d471c722ecf1626a9" + integrity sha512-mbHTBSPBvg6o/mN/c18Z/zifM05eJrapj5ggoOIeHIWckvkv5VgGi7r/wYpt+QAO2ySKXLNvH2d8L7bne4xrMQ== + dependencies: + "@aws-crypto/util" "^2.0.1" + "@aws-sdk/types" "^3.1.0" + tslib "^1.11.1" + +"@aws-crypto/supports-web-crypto@^2.0.0": + version "2.0.0" + resolved "https://registry.yarnpkg.com/@aws-crypto/supports-web-crypto/-/supports-web-crypto-2.0.0.tgz#fd6cde30b88f77d5a4f57b2c37c560d918014f9e" + integrity sha512-Ge7WQ3E0OC7FHYprsZV3h0QIcpdyJLvIeg+uTuHqRYm8D6qCFJoiC+edSzSyFiHtZf+NOQDJ1q46qxjtzIY2nA== + dependencies: + tslib "^1.11.1" + +"@aws-crypto/util@^2.0.0", "@aws-crypto/util@^2.0.1": + version "2.0.1" + resolved "https://registry.yarnpkg.com/@aws-crypto/util/-/util-2.0.1.tgz#976cf619cf85084ca85ec5eb947a6ac6b8b5c98c" + integrity sha512-JJmFFwvbm08lULw4Nm5QOLg8+lAQeC8aCXK5xrtxntYzYXCGfHwUJ4Is3770Q7HmICsXthGQ+ZsDL7C2uH3yBQ== + dependencies: + "@aws-sdk/types" "^3.1.0" + "@aws-sdk/util-utf8-browser" "^3.0.0" + tslib "^1.11.1" + +"@aws-sdk/abort-controller@3.78.0": + version "3.78.0" + resolved "https://registry.yarnpkg.com/@aws-sdk/abort-controller/-/abort-controller-3.78.0.tgz#f2b0f8d63954afe51136254f389a18dd24a8f6f3" + integrity sha512-iz1YLwM2feJUj/y97yO4XmDeTxs+yZ1XJwQgoawKuc8IDBKUutnJNCHL5jL04WUKU7Nrlq+Hr2fCTScFh2z9zg== + dependencies: + "@aws-sdk/types" "3.78.0" + tslib "^2.3.1" + +"@aws-sdk/chunked-blob-reader-native@3.58.0": + version "3.58.0" + resolved "https://registry.yarnpkg.com/@aws-sdk/chunked-blob-reader-native/-/chunked-blob-reader-native-3.58.0.tgz#1db413c5c80b32e24f1b62b22e15e9ad74d75cda" + integrity sha512-+D3xnPD5985iphgAqgUerBDs371a2WzzoEVi7eHJUMMsP/gEnSTdSH0HNxsqhYv6CW4EdKtvDAQdAwA1VtCf2A== + dependencies: + "@aws-sdk/util-base64-browser" "3.58.0" + tslib "^2.3.1" + +"@aws-sdk/chunked-blob-reader@3.55.0": + version "3.55.0" + resolved "https://registry.yarnpkg.com/@aws-sdk/chunked-blob-reader/-/chunked-blob-reader-3.55.0.tgz#db240c78e7c4c826e707f0ca32a4d221c41cf6a0" + integrity sha512-o/xjMCq81opAjSBjt7YdHJwIJcGVG5XIV9+C2KXcY5QwVimkOKPybWTv0mXPvSwSilSx+EhpLNhkcJuXdzhw4w== + dependencies: + tslib "^2.3.1" + +"@aws-sdk/client-s3@^3.83.0": + version "3.83.0" + resolved "https://registry.yarnpkg.com/@aws-sdk/client-s3/-/client-s3-3.83.0.tgz#2e9930c2157d793823bfc2b63a3fcc5649614d02" + integrity sha512-fYW/0Sv2H4b8C46tz8cgU/8+u1Wu2NoTS00650WhbGVMPKstk2bNkOFvJfS+QiSKK2cD/tb3M3AcjayP2v5Nsg== + dependencies: + "@aws-crypto/sha1-browser" "2.0.0" + "@aws-crypto/sha256-browser" "2.0.0" + "@aws-crypto/sha256-js" "2.0.0" + "@aws-sdk/client-sts" "3.82.0" + "@aws-sdk/config-resolver" "3.80.0" + "@aws-sdk/credential-provider-node" "3.82.0" + "@aws-sdk/eventstream-serde-browser" "3.78.0" + "@aws-sdk/eventstream-serde-config-resolver" "3.78.0" + "@aws-sdk/eventstream-serde-node" "3.78.0" + "@aws-sdk/fetch-http-handler" "3.78.0" + "@aws-sdk/hash-blob-browser" "3.78.0" + "@aws-sdk/hash-node" "3.78.0" + "@aws-sdk/hash-stream-node" "3.78.0" + "@aws-sdk/invalid-dependency" "3.78.0" + "@aws-sdk/md5-js" "3.78.0" + "@aws-sdk/middleware-bucket-endpoint" "3.80.0" + "@aws-sdk/middleware-content-length" "3.78.0" + "@aws-sdk/middleware-expect-continue" "3.78.0" + "@aws-sdk/middleware-flexible-checksums" "3.78.0" + "@aws-sdk/middleware-host-header" "3.78.0" + "@aws-sdk/middleware-location-constraint" "3.78.0" + "@aws-sdk/middleware-logger" "3.78.0" + "@aws-sdk/middleware-retry" "3.80.0" + "@aws-sdk/middleware-sdk-s3" "3.78.0" + "@aws-sdk/middleware-serde" "3.78.0" + "@aws-sdk/middleware-signing" "3.78.0" + "@aws-sdk/middleware-ssec" "3.78.0" + "@aws-sdk/middleware-stack" "3.78.0" + "@aws-sdk/middleware-user-agent" "3.78.0" + "@aws-sdk/node-config-provider" "3.80.0" + "@aws-sdk/node-http-handler" "3.82.0" + "@aws-sdk/protocol-http" "3.78.0" + "@aws-sdk/signature-v4-multi-region" "3.78.0" + "@aws-sdk/smithy-client" "3.78.0" + "@aws-sdk/types" "3.78.0" + "@aws-sdk/url-parser" "3.78.0" + "@aws-sdk/util-base64-browser" "3.58.0" + "@aws-sdk/util-base64-node" "3.55.0" + "@aws-sdk/util-body-length-browser" "3.55.0" + "@aws-sdk/util-body-length-node" "3.55.0" + "@aws-sdk/util-defaults-mode-browser" "3.78.0" + "@aws-sdk/util-defaults-mode-node" "3.81.0" + "@aws-sdk/util-stream-browser" "3.78.0" + "@aws-sdk/util-stream-node" "3.78.0" + "@aws-sdk/util-user-agent-browser" "3.78.0" + "@aws-sdk/util-user-agent-node" "3.80.0" + "@aws-sdk/util-utf8-browser" "3.55.0" + "@aws-sdk/util-utf8-node" "3.55.0" + "@aws-sdk/util-waiter" "3.78.0" + "@aws-sdk/xml-builder" "3.55.0" + entities "2.2.0" + fast-xml-parser "3.19.0" + tslib "^2.3.1" + +"@aws-sdk/client-sso@3.82.0": + version "3.82.0" + resolved "https://registry.yarnpkg.com/@aws-sdk/client-sso/-/client-sso-3.82.0.tgz#720c9f84554bf951f192e5000ac0d34a10f992df" + integrity sha512-zfscjrufPLh1RwdVMDx+5xxZbrY64UD4aSHlmWcPxE8ySj2MVfoE18EBQcCgY82U5QgssT7yxtUirxyI2b92tw== + dependencies: + "@aws-crypto/sha256-browser" "2.0.0" + "@aws-crypto/sha256-js" "2.0.0" + "@aws-sdk/config-resolver" "3.80.0" + "@aws-sdk/fetch-http-handler" "3.78.0" + "@aws-sdk/hash-node" "3.78.0" + "@aws-sdk/invalid-dependency" "3.78.0" + "@aws-sdk/middleware-content-length" "3.78.0" + "@aws-sdk/middleware-host-header" "3.78.0" + "@aws-sdk/middleware-logger" "3.78.0" + "@aws-sdk/middleware-retry" "3.80.0" + "@aws-sdk/middleware-serde" "3.78.0" + "@aws-sdk/middleware-stack" "3.78.0" + "@aws-sdk/middleware-user-agent" "3.78.0" + "@aws-sdk/node-config-provider" "3.80.0" + "@aws-sdk/node-http-handler" "3.82.0" + "@aws-sdk/protocol-http" "3.78.0" + "@aws-sdk/smithy-client" "3.78.0" + "@aws-sdk/types" "3.78.0" + "@aws-sdk/url-parser" "3.78.0" + "@aws-sdk/util-base64-browser" "3.58.0" + "@aws-sdk/util-base64-node" "3.55.0" + "@aws-sdk/util-body-length-browser" "3.55.0" + "@aws-sdk/util-body-length-node" "3.55.0" + "@aws-sdk/util-defaults-mode-browser" "3.78.0" + "@aws-sdk/util-defaults-mode-node" "3.81.0" + "@aws-sdk/util-user-agent-browser" "3.78.0" + "@aws-sdk/util-user-agent-node" "3.80.0" + "@aws-sdk/util-utf8-browser" "3.55.0" + "@aws-sdk/util-utf8-node" "3.55.0" + tslib "^2.3.1" + +"@aws-sdk/client-sts@3.82.0": + version "3.82.0" + resolved "https://registry.yarnpkg.com/@aws-sdk/client-sts/-/client-sts-3.82.0.tgz#9b316c61980c27df3aef7f60ccdb4d9944f7f9b7" + integrity sha512-gR1dz/a6lMD2U+AUpLUocXDruDDQFCUoknrpzMZPCAVsamrF17dwKKCgKVlz6zseHP6uPMPluuppvYQ16wsYyQ== + dependencies: + "@aws-crypto/sha256-browser" "2.0.0" + "@aws-crypto/sha256-js" "2.0.0" + "@aws-sdk/config-resolver" "3.80.0" + "@aws-sdk/credential-provider-node" "3.82.0" + "@aws-sdk/fetch-http-handler" "3.78.0" + "@aws-sdk/hash-node" "3.78.0" + "@aws-sdk/invalid-dependency" "3.78.0" + "@aws-sdk/middleware-content-length" "3.78.0" + "@aws-sdk/middleware-host-header" "3.78.0" + "@aws-sdk/middleware-logger" "3.78.0" + "@aws-sdk/middleware-retry" "3.80.0" + "@aws-sdk/middleware-sdk-sts" "3.78.0" + "@aws-sdk/middleware-serde" "3.78.0" + "@aws-sdk/middleware-signing" "3.78.0" + "@aws-sdk/middleware-stack" "3.78.0" + "@aws-sdk/middleware-user-agent" "3.78.0" + "@aws-sdk/node-config-provider" "3.80.0" + "@aws-sdk/node-http-handler" "3.82.0" + "@aws-sdk/protocol-http" "3.78.0" + "@aws-sdk/smithy-client" "3.78.0" + "@aws-sdk/types" "3.78.0" + "@aws-sdk/url-parser" "3.78.0" + "@aws-sdk/util-base64-browser" "3.58.0" + "@aws-sdk/util-base64-node" "3.55.0" + "@aws-sdk/util-body-length-browser" "3.55.0" + "@aws-sdk/util-body-length-node" "3.55.0" + "@aws-sdk/util-defaults-mode-browser" "3.78.0" + "@aws-sdk/util-defaults-mode-node" "3.81.0" + "@aws-sdk/util-user-agent-browser" "3.78.0" + "@aws-sdk/util-user-agent-node" "3.80.0" + "@aws-sdk/util-utf8-browser" "3.55.0" + "@aws-sdk/util-utf8-node" "3.55.0" + entities "2.2.0" + fast-xml-parser "3.19.0" + tslib "^2.3.1" + +"@aws-sdk/config-resolver@3.80.0": + version "3.80.0" + resolved "https://registry.yarnpkg.com/@aws-sdk/config-resolver/-/config-resolver-3.80.0.tgz#a804aba4d4767402ab15640757c8c8bb2254eec1" + integrity sha512-vFruNKlmhsaC8yjnHmasi1WW/7EELlEuFTj4mqcqNqR4dfraf0maVvpqF1VSR8EstpFMsGYI5dmoWAnnG4PcLQ== + dependencies: + "@aws-sdk/signature-v4" "3.78.0" + "@aws-sdk/types" "3.78.0" + "@aws-sdk/util-config-provider" "3.55.0" + "@aws-sdk/util-middleware" "3.78.0" + tslib "^2.3.1" + +"@aws-sdk/credential-provider-env@3.78.0": + version "3.78.0" + resolved "https://registry.yarnpkg.com/@aws-sdk/credential-provider-env/-/credential-provider-env-3.78.0.tgz#e3013073bab0db313b0505d790aa79a35bd582d9" + integrity sha512-K41VTIzVHm2RyIwtBER8Hte3huUBXdV1WKO+i7olYVgLFmaqcZUNrlyoGDRqZcQ/u4AbxTzBU9jeMIbIfzMOWg== + dependencies: + "@aws-sdk/property-provider" "3.78.0" + "@aws-sdk/types" "3.78.0" + tslib "^2.3.1" + +"@aws-sdk/credential-provider-imds@3.81.0": + version "3.81.0" + resolved "https://registry.yarnpkg.com/@aws-sdk/credential-provider-imds/-/credential-provider-imds-3.81.0.tgz#1ffd1219b7fd19eec4d4d4b5b06bda66e3bc210e" + integrity sha512-BHopP+gaovTYj+4tSrwCk8NNCR48gE9CWmpIOLkP9ell0gOL81Qh7aCEiIK0BZBZkccv1s16cYq1MSZZGS7PEQ== + dependencies: + "@aws-sdk/node-config-provider" "3.80.0" + "@aws-sdk/property-provider" "3.78.0" + "@aws-sdk/types" "3.78.0" + "@aws-sdk/url-parser" "3.78.0" + tslib "^2.3.1" + +"@aws-sdk/credential-provider-ini@3.82.0": + version "3.82.0" + resolved "https://registry.yarnpkg.com/@aws-sdk/credential-provider-ini/-/credential-provider-ini-3.82.0.tgz#9e2cf1b1100714dc8ae6b608f0382442751d82fe" + integrity sha512-2HrH5Ok/ZpN/81JbIY+HiKjNtGoXP50jyX8a5Dpez41hLuXek7j2ENWRcNOMkPtot+Ri088h661Y7sdzOv1etg== + dependencies: + "@aws-sdk/credential-provider-env" "3.78.0" + "@aws-sdk/credential-provider-imds" "3.81.0" + "@aws-sdk/credential-provider-sso" "3.82.0" + "@aws-sdk/credential-provider-web-identity" "3.78.0" + "@aws-sdk/property-provider" "3.78.0" + "@aws-sdk/shared-ini-file-loader" "3.80.0" + "@aws-sdk/types" "3.78.0" + tslib "^2.3.1" + +"@aws-sdk/credential-provider-node@3.82.0": + version "3.82.0" + resolved "https://registry.yarnpkg.com/@aws-sdk/credential-provider-node/-/credential-provider-node-3.82.0.tgz#ea2cad7ec998079f585d87b4ce375c0a677eea97" + integrity sha512-jjZj5h+tKaFBl/RnZ4Atpdtot6ZK4F2EBC8t+sNnFhPqcnhO42+7tLZ/aXhdY1oCvD54RG3exHFRsY6qDe8MhQ== + dependencies: + "@aws-sdk/credential-provider-env" "3.78.0" + "@aws-sdk/credential-provider-imds" "3.81.0" + "@aws-sdk/credential-provider-ini" "3.82.0" + "@aws-sdk/credential-provider-process" "3.80.0" + "@aws-sdk/credential-provider-sso" "3.82.0" + "@aws-sdk/credential-provider-web-identity" "3.78.0" + "@aws-sdk/property-provider" "3.78.0" + "@aws-sdk/shared-ini-file-loader" "3.80.0" + "@aws-sdk/types" "3.78.0" + tslib "^2.3.1" + +"@aws-sdk/credential-provider-process@3.80.0": + version "3.80.0" + resolved "https://registry.yarnpkg.com/@aws-sdk/credential-provider-process/-/credential-provider-process-3.80.0.tgz#625577774278f845fe5bd0f311ed53973ec92ede" + integrity sha512-3Ro+kMMyLUJHefOhGc5pOO/ibGcJi8bkj0z/Jtqd5I2Sm1qi7avoztST67/k48KMW1OqPnD/FUqxz5T8B2d+FQ== + dependencies: + "@aws-sdk/property-provider" "3.78.0" + "@aws-sdk/shared-ini-file-loader" "3.80.0" + "@aws-sdk/types" "3.78.0" + tslib "^2.3.1" + +"@aws-sdk/credential-provider-sso@3.82.0": + version "3.82.0" + resolved "https://registry.yarnpkg.com/@aws-sdk/credential-provider-sso/-/credential-provider-sso-3.82.0.tgz#f00397e61588a80f2dbbf131bd17e5fd65a92d42" + integrity sha512-cOIFD6dohrp/cz3bkT0rxbfHgNA4wXRtOciitbBpNnfxOdu51M9bp+XZFb3tdTfhE9fIr4Y+BGqF6AXWZkikLg== + dependencies: + "@aws-sdk/client-sso" "3.82.0" + "@aws-sdk/property-provider" "3.78.0" + "@aws-sdk/shared-ini-file-loader" "3.80.0" + "@aws-sdk/types" "3.78.0" + tslib "^2.3.1" + +"@aws-sdk/credential-provider-web-identity@3.78.0": + version "3.78.0" + resolved "https://registry.yarnpkg.com/@aws-sdk/credential-provider-web-identity/-/credential-provider-web-identity-3.78.0.tgz#61cc6c5c065de3d8d34b7633899e3bbfa9a24c9d" + integrity sha512-9/IvqHdJaVqMEABA8xZE3t5YF1S2PepfckVu0Ws9YUglj6oO+2QyVX6aRgMF1xph6781+Yc31TDh8/3eaDja7w== + dependencies: + "@aws-sdk/property-provider" "3.78.0" + "@aws-sdk/types" "3.78.0" + tslib "^2.3.1" + +"@aws-sdk/eventstream-marshaller@3.78.0": + version "3.78.0" + resolved "https://registry.yarnpkg.com/@aws-sdk/eventstream-marshaller/-/eventstream-marshaller-3.78.0.tgz#32df7136d644d0d91a563a9a192b6e2d4df873d0" + integrity sha512-BMbRvLe6wNWQ+NO1pdPw3kGXXEdYV94BxEr3rTkKwr5yHpl8sUb/Va9sJJufUjzggpgE4vYu5nVsrT8ByMYXuA== + dependencies: + "@aws-crypto/crc32" "2.0.0" + "@aws-sdk/types" "3.78.0" + "@aws-sdk/util-hex-encoding" "3.58.0" + tslib "^2.3.1" + +"@aws-sdk/eventstream-serde-browser@3.78.0": + version "3.78.0" + resolved "https://registry.yarnpkg.com/@aws-sdk/eventstream-serde-browser/-/eventstream-serde-browser-3.78.0.tgz#27b019f6f17a54e18cd44041b29ef234cc04f545" + integrity sha512-ehQI2iLsj8MMskDRbrPB7SibIdJq6LleBP6ojT+cgrLJRbVXUOxK+3MPHDZVdGYx4ukVg48E1fA2DzVfAp7Emw== + dependencies: + "@aws-sdk/eventstream-marshaller" "3.78.0" + "@aws-sdk/eventstream-serde-universal" "3.78.0" + "@aws-sdk/types" "3.78.0" + tslib "^2.3.1" + +"@aws-sdk/eventstream-serde-config-resolver@3.78.0": + version "3.78.0" + resolved "https://registry.yarnpkg.com/@aws-sdk/eventstream-serde-config-resolver/-/eventstream-serde-config-resolver-3.78.0.tgz#ea6d24d763413bc53da6230e06660382ba94a40c" + integrity sha512-iUG0wtZH/L7d6XfipwbhgjBHip0uTm9S27EasCn+g0CunbW6w7rXd7rfMqA+gSLVXPTBYjTMPIwRxrTCdRprwA== + dependencies: + "@aws-sdk/types" "3.78.0" + tslib "^2.3.1" + +"@aws-sdk/eventstream-serde-node@3.78.0": + version "3.78.0" + resolved "https://registry.yarnpkg.com/@aws-sdk/eventstream-serde-node/-/eventstream-serde-node-3.78.0.tgz#138d99043b11b7cdfd63425b257fae64ec404374" + integrity sha512-H78LLoZEngZBSdk3lRQkAaR3cGsy/3UIjq9AFPeqoPVQtHkzBob1jVfE/5VSVAMhKLxWn8iqhRPS37AvyBGOwQ== + dependencies: + "@aws-sdk/eventstream-marshaller" "3.78.0" + "@aws-sdk/eventstream-serde-universal" "3.78.0" + "@aws-sdk/types" "3.78.0" + tslib "^2.3.1" + +"@aws-sdk/eventstream-serde-universal@3.78.0": + version "3.78.0" + resolved "https://registry.yarnpkg.com/@aws-sdk/eventstream-serde-universal/-/eventstream-serde-universal-3.78.0.tgz#9d7f3caf83cdc89ca7e3cf3a24734b0bbf43c81c" + integrity sha512-PZTLdyF923/1GJuMNtq9VMGd2vEx33HhsGInXvYtulKDSD5SgaTGj+Dz5wYepqL1gUEuXqZjBD71uZgrY/JgRg== + dependencies: + "@aws-sdk/eventstream-marshaller" "3.78.0" + "@aws-sdk/types" "3.78.0" + tslib "^2.3.1" + +"@aws-sdk/fetch-http-handler@3.78.0": + version "3.78.0" + resolved "https://registry.yarnpkg.com/@aws-sdk/fetch-http-handler/-/fetch-http-handler-3.78.0.tgz#9cd4a02eaf015b4a5a18552e8c9e8fbfce7219a3" + integrity sha512-cR6r2h2kJ1DNEZSXC6GknQB7OKmy+s9ZNV+g3AsNqkrUmNNOaHpFoSn+m6SC3qaclcGd0eQBpqzSu/TDn23Ihw== + dependencies: + "@aws-sdk/protocol-http" "3.78.0" + "@aws-sdk/querystring-builder" "3.78.0" + "@aws-sdk/types" "3.78.0" + "@aws-sdk/util-base64-browser" "3.58.0" + tslib "^2.3.1" + +"@aws-sdk/hash-blob-browser@3.78.0": + version "3.78.0" + resolved "https://registry.yarnpkg.com/@aws-sdk/hash-blob-browser/-/hash-blob-browser-3.78.0.tgz#6f774f58c59bb02749b7239a35be9cf61cc8520e" + integrity sha512-IEkA+t6qJEtEYEZgsqFRRITeZJ3mirw7IHJVHxwb86lpeufTVcbILI59B8/rhbqG+9dk0kWTjYSjC/ZdM+rgHA== + dependencies: + "@aws-sdk/chunked-blob-reader" "3.55.0" + "@aws-sdk/chunked-blob-reader-native" "3.58.0" + "@aws-sdk/types" "3.78.0" + tslib "^2.3.1" + +"@aws-sdk/hash-node@3.78.0": + version "3.78.0" + resolved "https://registry.yarnpkg.com/@aws-sdk/hash-node/-/hash-node-3.78.0.tgz#d03f804a685bc1cea9df3eabf499b2a7659d01fd" + integrity sha512-ev48yXaqZVtMeuKy52LUZPHCyKvkKQ9uiUebqkA+zFxIk+eN8SMPFHmsififIHWuS6ZkXBUSctjH9wmLebH60A== + dependencies: + "@aws-sdk/types" "3.78.0" + "@aws-sdk/util-buffer-from" "3.55.0" + tslib "^2.3.1" + +"@aws-sdk/hash-stream-node@3.78.0": + version "3.78.0" + resolved "https://registry.yarnpkg.com/@aws-sdk/hash-stream-node/-/hash-stream-node-3.78.0.tgz#7b321a4ab4384bd51f19e626e5dae111b8fac4dd" + integrity sha512-y42Pm0Nk6zf/MI6acLFVFAMya0Ncvy6F6Xu5aYAmwIMIoMI0ctNeyuL/Dikgt8+oyxC+kORw+W9jtzgWj2zY/w== + dependencies: + "@aws-sdk/types" "3.78.0" + tslib "^2.3.1" + +"@aws-sdk/invalid-dependency@3.78.0": + version "3.78.0" + resolved "https://registry.yarnpkg.com/@aws-sdk/invalid-dependency/-/invalid-dependency-3.78.0.tgz#c4e30871d69894dbf3450023319385110ce95c81" + integrity sha512-zUo+PbeRMN/Mzj6y+6p9qqk/znuFetT1gmpOcZGL9Rp2T+b9WJWd+daq5ktsL10sVCzIt2UvneJRz6b+aU+bfw== + dependencies: + "@aws-sdk/types" "3.78.0" + tslib "^2.3.1" + +"@aws-sdk/is-array-buffer@3.55.0": + version "3.55.0" + resolved "https://registry.yarnpkg.com/@aws-sdk/is-array-buffer/-/is-array-buffer-3.55.0.tgz#c46122c5636f01d5895e5256a587768c3425ea7a" + integrity sha512-NbiPHVYuPxdqdFd6FxzzN3H1BQn/iWA3ri3Ry7AyLeP/tGs1yzEWMwf8BN8TSMALI0GXT6Sh0GDWy3Ok5xB6DA== + dependencies: + tslib "^2.3.1" + +"@aws-sdk/lib-storage@^3.83.0": + version "3.83.0" + resolved "https://registry.yarnpkg.com/@aws-sdk/lib-storage/-/lib-storage-3.83.0.tgz#916244e7234f52744a93353ffec569b8b3602095" + integrity sha512-HgvI4ppPMjKmwa9ZhQ+hySex4p0POT1xRvlVLAXxcXR1ftS40q7gys0cfztIRPL30QYp+U8rjbtLyfKTZMIIew== + dependencies: + buffer "5.6.0" + events "3.3.0" + stream-browserify "3.0.0" + tslib "^2.3.1" + +"@aws-sdk/md5-js@3.78.0": + version "3.78.0" + resolved "https://registry.yarnpkg.com/@aws-sdk/md5-js/-/md5-js-3.78.0.tgz#a79357e6518778057b7bcbbd45dcb352be5f8e15" + integrity sha512-vKOXJWJvv6QH6rnqMYEWzwAnMr4hfcmY8+t6BAuTcDpcEVF77e3bwUcaajXi2U0JMuNvnLwuJF3h6kL6aX4l6g== + dependencies: + "@aws-sdk/types" "3.78.0" + "@aws-sdk/util-utf8-browser" "3.55.0" + "@aws-sdk/util-utf8-node" "3.55.0" + tslib "^2.3.1" + +"@aws-sdk/middleware-bucket-endpoint@3.80.0": + version "3.80.0" + resolved "https://registry.yarnpkg.com/@aws-sdk/middleware-bucket-endpoint/-/middleware-bucket-endpoint-3.80.0.tgz#0632e94900472eb86d0cfdf521251a1bdefba843" + integrity sha512-FSSx6IgT7xftSlpjxoPKv8XI9nv7EK+OCODo2s3CmElMW1kBRdmQ/ImVuTwvqhdxJEVUeUdgupmC7cqyqgt04w== + dependencies: + "@aws-sdk/protocol-http" "3.78.0" + "@aws-sdk/types" "3.78.0" + "@aws-sdk/util-arn-parser" "3.55.0" + "@aws-sdk/util-config-provider" "3.55.0" + tslib "^2.3.1" + +"@aws-sdk/middleware-content-length@3.78.0": + version "3.78.0" + resolved "https://registry.yarnpkg.com/@aws-sdk/middleware-content-length/-/middleware-content-length-3.78.0.tgz#57d46be61d1176d4c5fce7ba4b0682798c170208" + integrity sha512-5MpKt6lB9TdFy25/AGrpOjPY0iDHZAKpEHc+jSOJBXLl6xunXA7qHdiYaVqkWodLxy70nIckGNHqQ3drabidkA== + dependencies: + "@aws-sdk/protocol-http" "3.78.0" + "@aws-sdk/types" "3.78.0" + tslib "^2.3.1" + +"@aws-sdk/middleware-expect-continue@3.78.0": + version "3.78.0" + resolved "https://registry.yarnpkg.com/@aws-sdk/middleware-expect-continue/-/middleware-expect-continue-3.78.0.tgz#35df662ecf31a1c8540781154f514615f3ca2c97" + integrity sha512-IXfcSugFV3uNk50VQsN/Cm80iCsUSwcYJ5RzEwy7wXbZ+KM03xWXlbXzqkeTDnS74wLWSw09nKF3rkp1eyfDfg== + dependencies: + "@aws-sdk/middleware-header-default" "3.78.0" + "@aws-sdk/protocol-http" "3.78.0" + "@aws-sdk/types" "3.78.0" + tslib "^2.3.1" + +"@aws-sdk/middleware-flexible-checksums@3.78.0": + version "3.78.0" + resolved "https://registry.yarnpkg.com/@aws-sdk/middleware-flexible-checksums/-/middleware-flexible-checksums-3.78.0.tgz#9128b0acb5d9df0f0e0ef06cb1d17a44afe650fc" + integrity sha512-1jjxHcB3Le/2Z7BzugXzZnIwKGlUluNm0d1lB4fF2QVq3GHlA6e8uv0rCtqe/3wSsrzV6YzJ8vjioymKSNIjKQ== + dependencies: + "@aws-crypto/crc32" "2.0.0" + "@aws-crypto/crc32c" "2.0.0" + "@aws-sdk/is-array-buffer" "3.55.0" + "@aws-sdk/protocol-http" "3.78.0" + "@aws-sdk/types" "3.78.0" + tslib "^2.3.1" + +"@aws-sdk/middleware-header-default@3.78.0": + version "3.78.0" + resolved "https://registry.yarnpkg.com/@aws-sdk/middleware-header-default/-/middleware-header-default-3.78.0.tgz#911b7f6ce4b4ae45ab032e32768d527ca6ae1d6c" + integrity sha512-USyOIF7ObBVMKbV/8lOBLDNwMAGdOtujd+RO/9dX6OQLceUTKIS1dOfJoYYwRHgengn7ikpDxoyROyspPYYDZQ== + dependencies: + "@aws-sdk/protocol-http" "3.78.0" + "@aws-sdk/types" "3.78.0" + tslib "^2.3.1" + +"@aws-sdk/middleware-host-header@3.78.0": + version "3.78.0" + resolved "https://registry.yarnpkg.com/@aws-sdk/middleware-host-header/-/middleware-host-header-3.78.0.tgz#9130d176c2839bc658aff01bf2a36fee705f0e86" + integrity sha512-1zL8uaDWGmH50c8B8jjz75e0ePj6/3QeZEhjJgTgL6DTdiqvRt32p3t+XWHW+yDI14fZZUYeTklAaLVxqFrHqQ== + dependencies: + "@aws-sdk/protocol-http" "3.78.0" + "@aws-sdk/types" "3.78.0" + tslib "^2.3.1" + +"@aws-sdk/middleware-location-constraint@3.78.0": + version "3.78.0" + resolved "https://registry.yarnpkg.com/@aws-sdk/middleware-location-constraint/-/middleware-location-constraint-3.78.0.tgz#f3af44e443a0993e413a787a446bb6693b5b0e7e" + integrity sha512-m626H1WwXYJtwHEkV/2DsLlu1ckWq3j57NzsexZki3qS0nU8HEiDl6YYi+k84vDD4Qpba6EI9AdhzwnvZLXtGw== + dependencies: + "@aws-sdk/types" "3.78.0" + tslib "^2.3.1" + +"@aws-sdk/middleware-logger@3.78.0": + version "3.78.0" + resolved "https://registry.yarnpkg.com/@aws-sdk/middleware-logger/-/middleware-logger-3.78.0.tgz#758b84711213b2e78afe0df20bc2d4d70a856da1" + integrity sha512-GBhwxNjhCJUIeQQDaGasX/C23Jay77al2vRyGwmxf8no0DdFsa4J1Ik6/2hhIqkqko+WM4SpCnpZrY4MtnxNvA== + dependencies: + "@aws-sdk/types" "3.78.0" + tslib "^2.3.1" + +"@aws-sdk/middleware-retry@3.80.0": + version "3.80.0" + resolved "https://registry.yarnpkg.com/@aws-sdk/middleware-retry/-/middleware-retry-3.80.0.tgz#d62ebd68ded78bdaf0a8b07bb4cc1c394c99cc8f" + integrity sha512-CTk+tA4+WMUNOcUfR6UQrkhwvPYFpnMsQ1vuHlpLFOGG3nCqywA2hueLMRQmVcDXzP0sGeygce6dzRI9dJB/GA== + dependencies: + "@aws-sdk/protocol-http" "3.78.0" + "@aws-sdk/service-error-classification" "3.78.0" + "@aws-sdk/types" "3.78.0" + "@aws-sdk/util-middleware" "3.78.0" + tslib "^2.3.1" + uuid "^8.3.2" + +"@aws-sdk/middleware-sdk-s3@3.78.0": + version "3.78.0" + resolved "https://registry.yarnpkg.com/@aws-sdk/middleware-sdk-s3/-/middleware-sdk-s3-3.78.0.tgz#5e4eed92f0aa20151d7c96dd4779ad06de69e206" + integrity sha512-gxtfVHaL0CkKDIEwRQnmBequtN3dsCtY5LByZQoP3l5qEuTAzwxgbtvGUfHE8LwDVByBqUEFanzafjv1KJ3F8w== + dependencies: + "@aws-sdk/protocol-http" "3.78.0" + "@aws-sdk/types" "3.78.0" + "@aws-sdk/util-arn-parser" "3.55.0" + tslib "^2.3.1" + +"@aws-sdk/middleware-sdk-sts@3.78.0": + version "3.78.0" + resolved "https://registry.yarnpkg.com/@aws-sdk/middleware-sdk-sts/-/middleware-sdk-sts-3.78.0.tgz#15d91c421380f748b58bb006e1c398cfdf59b290" + integrity sha512-Lu/kN0J0/Kt0ON1hvwNel+y8yvf35licfIgtedHbBCa/ju8qQ9j+uL9Lla6Y5Tqu29yVaye1JxhiIDhscSwrLA== + dependencies: + "@aws-sdk/middleware-signing" "3.78.0" + "@aws-sdk/property-provider" "3.78.0" + "@aws-sdk/protocol-http" "3.78.0" + "@aws-sdk/signature-v4" "3.78.0" + "@aws-sdk/types" "3.78.0" + tslib "^2.3.1" + +"@aws-sdk/middleware-serde@3.78.0": + version "3.78.0" + resolved "https://registry.yarnpkg.com/@aws-sdk/middleware-serde/-/middleware-serde-3.78.0.tgz#d1e1a7b9ac58638b973e533ac4c2ca52f413883c" + integrity sha512-4DPsNOxsl1bxRzfo1WXEZjmD7OEi7qGNpxrDWucVe96Fqj2dH08jR8wxvBIVV1e6bAad07IwdPuCGmivNvwRuQ== + dependencies: + "@aws-sdk/types" "3.78.0" + tslib "^2.3.1" + +"@aws-sdk/middleware-signing@3.78.0": + version "3.78.0" + resolved "https://registry.yarnpkg.com/@aws-sdk/middleware-signing/-/middleware-signing-3.78.0.tgz#2fb41819a9ae0953cf8f428851a57696442469ca" + integrity sha512-OEjJJCNhHHSOprLZ9CzjHIXEKFtPHWP/bG9pMhkV3/6Bmscsgcf8gWHcOnmIrjqX+hT1VALDNpl/RIh0J6/eQw== + dependencies: + "@aws-sdk/property-provider" "3.78.0" + "@aws-sdk/protocol-http" "3.78.0" + "@aws-sdk/signature-v4" "3.78.0" + "@aws-sdk/types" "3.78.0" + tslib "^2.3.1" + +"@aws-sdk/middleware-ssec@3.78.0": + version "3.78.0" + resolved "https://registry.yarnpkg.com/@aws-sdk/middleware-ssec/-/middleware-ssec-3.78.0.tgz#4463c6c6ee26c8b3f2ebc112f7de3ca560ba4f3f" + integrity sha512-3z+UOd95rxvj+iO6WxMjuRNNUMlO6xhXZdBHvQmoiyS+9nMDcNieTu6gfQyLAilVeCh8xU9a0IenJuIYVdJ96g== + dependencies: + "@aws-sdk/types" "3.78.0" + tslib "^2.3.1" + +"@aws-sdk/middleware-stack@3.78.0": + version "3.78.0" + resolved "https://registry.yarnpkg.com/@aws-sdk/middleware-stack/-/middleware-stack-3.78.0.tgz#e9f42039e500bed23ec74359924ae16e7bf9c77a" + integrity sha512-UoNfRh6eAJN3BJHlG1eb+KeuSe+zARTC2cglroJRyHc2j7GxH2i9FD3IJbj5wvzopJEnQzuY/VCs6STFkqWL1g== + dependencies: + tslib "^2.3.1" + +"@aws-sdk/middleware-user-agent@3.78.0": + version "3.78.0" + resolved "https://registry.yarnpkg.com/@aws-sdk/middleware-user-agent/-/middleware-user-agent-3.78.0.tgz#e4c7345d26d718de0e84b60ba02b2b08b566fa15" + integrity sha512-wdN5uoq8RxxhLhj0EPeuDSRFuXfUwKeEqRzCKMsYAOC0cAm+PryaP2leo0oTGJ9LUK8REK7zyfFcmtC4oOzlkA== + dependencies: + "@aws-sdk/protocol-http" "3.78.0" + "@aws-sdk/types" "3.78.0" + tslib "^2.3.1" + +"@aws-sdk/node-config-provider@3.80.0": + version "3.80.0" + resolved "https://registry.yarnpkg.com/@aws-sdk/node-config-provider/-/node-config-provider-3.80.0.tgz#dbb02aa48fb1a0acc3201ca73db5bbf1738895b5" + integrity sha512-vyTOMK04huB7n10ZUv0thd2TE6KlY8livOuLqFTMtj99AJ6vyeB5XBNwKnQtJIt/P7CijYgp8KcFvI9fndOmKg== + dependencies: + "@aws-sdk/property-provider" "3.78.0" + "@aws-sdk/shared-ini-file-loader" "3.80.0" + "@aws-sdk/types" "3.78.0" + tslib "^2.3.1" + +"@aws-sdk/node-http-handler@3.82.0": + version "3.82.0" + resolved "https://registry.yarnpkg.com/@aws-sdk/node-http-handler/-/node-http-handler-3.82.0.tgz#e28064815c6c6caf22a16bb7fee4e9e7e73ef3bb" + integrity sha512-yyq/DA/IMzL4fLJhV7zVfP7aUQWPHfOKTCJjWB3KeV5YPiviJtSKb/KyzNi+gQyO7SmsL/8vQbQrf3/s7N/2OA== + dependencies: + "@aws-sdk/abort-controller" "3.78.0" + "@aws-sdk/protocol-http" "3.78.0" + "@aws-sdk/querystring-builder" "3.78.0" + "@aws-sdk/types" "3.78.0" + tslib "^2.3.1" + +"@aws-sdk/property-provider@3.78.0": + version "3.78.0" + resolved "https://registry.yarnpkg.com/@aws-sdk/property-provider/-/property-provider-3.78.0.tgz#f12341fa87da2b54daac95f623bf7ede1754f8ae" + integrity sha512-PZpLvV0hF6lqg3CSN9YmphrB/t5LVJVWGJLB9d9qm7sJs5ksjTYBb5bY91OQ3zit0F4cqBMU8xt2GQ9J6d4DvQ== + dependencies: + "@aws-sdk/types" "3.78.0" + tslib "^2.3.1" + +"@aws-sdk/protocol-http@3.78.0": + version "3.78.0" + resolved "https://registry.yarnpkg.com/@aws-sdk/protocol-http/-/protocol-http-3.78.0.tgz#8a30db90e3373fe94e2b0007c3cba47b5c9e08bd" + integrity sha512-SQB26MhEK96yDxyXd3UAaxLz1Y/ZvgE4pzv7V3wZiokdEedM0kawHKEn1UQJlqJLEZcQI9QYyysh3rTvHZ3fyg== + dependencies: + "@aws-sdk/types" "3.78.0" + tslib "^2.3.1" + +"@aws-sdk/querystring-builder@3.78.0": + version "3.78.0" + resolved "https://registry.yarnpkg.com/@aws-sdk/querystring-builder/-/querystring-builder-3.78.0.tgz#29068c4d1fad056e26f848779a31335469cb0038" + integrity sha512-aib6RW1WAaTQDqVgRU1Ku9idkhm90gJKbCxVaGId+as6QHNUqMChEfK2v+0afuKiPNOs5uWmqvOXI9+Gt+UGDg== + dependencies: + "@aws-sdk/types" "3.78.0" + "@aws-sdk/util-uri-escape" "3.55.0" + tslib "^2.3.1" + +"@aws-sdk/querystring-parser@3.78.0": + version "3.78.0" + resolved "https://registry.yarnpkg.com/@aws-sdk/querystring-parser/-/querystring-parser-3.78.0.tgz#4c76fe15ef2e9bbf4c387c83889d1c25d2c3a614" + integrity sha512-csaH8YTyN+KMNczeK6fBS8l7iJaqcQcKOIbpQFg5upX4Ly5A56HJn4sVQhY1LSgfSk4xRsNfMy5mu6BlsIiaXA== + dependencies: + "@aws-sdk/types" "3.78.0" + tslib "^2.3.1" + +"@aws-sdk/s3-request-presigner@^3.83.0": + version "3.83.0" + resolved "https://registry.yarnpkg.com/@aws-sdk/s3-request-presigner/-/s3-request-presigner-3.83.0.tgz#3ad8edc16a888f2caad3310be366b288de5b13a8" + integrity sha512-4zLX3esPgz1RAP/sNeuuivNXXfvJ7bL1F8+SkDcc2RrfFnfmqw1VBZfNt6daMSvAcNMCUF4HovjYdEb8X5+hJA== + dependencies: + "@aws-sdk/middleware-sdk-s3" "3.78.0" + "@aws-sdk/protocol-http" "3.78.0" + "@aws-sdk/signature-v4-multi-region" "3.78.0" + "@aws-sdk/smithy-client" "3.78.0" + "@aws-sdk/types" "3.78.0" + "@aws-sdk/util-create-request" "3.78.0" + "@aws-sdk/util-format-url" "3.78.0" + tslib "^2.3.1" + +"@aws-sdk/service-error-classification@3.78.0": + version "3.78.0" + resolved "https://registry.yarnpkg.com/@aws-sdk/service-error-classification/-/service-error-classification-3.78.0.tgz#8d3ac1064e39c180d9b764bb838c7f9de5615281" + integrity sha512-x7Lx8KWctJa01q4Q72Zb4ol9L/era3vy2daASu8l2paHHxsAPBE0PThkvLdUSLZSzlHSVdh3YHESIsT++VsK4w== + +"@aws-sdk/shared-ini-file-loader@3.80.0": + version "3.80.0" + resolved "https://registry.yarnpkg.com/@aws-sdk/shared-ini-file-loader/-/shared-ini-file-loader-3.80.0.tgz#e3d1b0532e9a884e52f967717ba2666ca32bbd74" + integrity sha512-3d5EBJjnWWkjLK9skqLLHYbagtFaZZy+3jUTlbTuOKhlOwe8jF7CUM3j6I4JA6yXNcB3w0exDKKHa8w+l+05aA== + dependencies: + tslib "^2.3.1" + +"@aws-sdk/signature-v4-multi-region@3.78.0": + version "3.78.0" + resolved "https://registry.yarnpkg.com/@aws-sdk/signature-v4-multi-region/-/signature-v4-multi-region-3.78.0.tgz#35b7ac8ed449c62fc7cae295f4b2f31246d0d33c" + integrity sha512-5C+3m4dikUsSLTxW++aBCHP0DT1niiEfXR4UdnjJzcjTtmi/jbL/i8UPG5sCpib9Mu6TMW633tN0h5woVPIIcg== + dependencies: + "@aws-sdk/protocol-http" "3.78.0" + "@aws-sdk/signature-v4" "3.78.0" + "@aws-sdk/types" "3.78.0" + "@aws-sdk/util-arn-parser" "3.55.0" + tslib "^2.3.1" + +"@aws-sdk/signature-v4@3.78.0": + version "3.78.0" + resolved "https://registry.yarnpkg.com/@aws-sdk/signature-v4/-/signature-v4-3.78.0.tgz#adb735b9604d4bb8e44d16f1baa87618d576013b" + integrity sha512-eePjRYuzKoi3VMr/lgrUEF1ytLeH4fA/NMCykr/uR6NMo4bSJA59KrFLYSM7SlWLRIyB0UvJqygVEvSxFluyDw== + dependencies: + "@aws-sdk/is-array-buffer" "3.55.0" + "@aws-sdk/types" "3.78.0" + "@aws-sdk/util-hex-encoding" "3.58.0" + "@aws-sdk/util-middleware" "3.78.0" + "@aws-sdk/util-uri-escape" "3.55.0" + tslib "^2.3.1" + +"@aws-sdk/smithy-client@3.78.0": + version "3.78.0" + resolved "https://registry.yarnpkg.com/@aws-sdk/smithy-client/-/smithy-client-3.78.0.tgz#76a7661c044685dc5e092caf2e4d7c2b6e53433d" + integrity sha512-qweaupZtFPm9rFiEgErnVNgB6co/DylJfhC6/UImHBKa7mGzxv6t2JDm6+d8fs8cNnGNXozN+jJG8Lz6C8Roxw== + dependencies: + "@aws-sdk/middleware-stack" "3.78.0" + "@aws-sdk/types" "3.78.0" + tslib "^2.3.1" + +"@aws-sdk/types@3.78.0": + version "3.78.0" + resolved "https://registry.yarnpkg.com/@aws-sdk/types/-/types-3.78.0.tgz#51dc80b2142ee20821fb9f476bdca6e541021443" + integrity sha512-I9PTlVNSbwhIgMfmDM5as1tqRIkVZunjVmfogb2WVVPp4CaX0Ll01S0FSMSLL9k6tcQLXqh45pFRjrxCl9WKdQ== + +"@aws-sdk/types@^3.1.0": + version "3.55.0" + resolved "https://registry.yarnpkg.com/@aws-sdk/types/-/types-3.55.0.tgz#d524d567e2b2722f2d6be83e2417dd6d46ce1490" + integrity sha512-wrDZjuy1CVAYxDCbm3bWQIKMGfNs7XXmG0eG4858Ixgqmq2avsIn5TORy8ynBxcXn9aekV/+tGEQ7BBSYzIVNQ== + +"@aws-sdk/url-parser@3.78.0": + version "3.78.0" + resolved "https://registry.yarnpkg.com/@aws-sdk/url-parser/-/url-parser-3.78.0.tgz#8903011fda4b04c1207df099a21eda1304573099" + integrity sha512-iQn2AjECUoJE0Ae9XtgHtGGKvUkvE8hhbktGopdj+zsPBe4WrBN2DgVxlKPPrBonG/YlcL1D7a5EXaujWSlUUw== + dependencies: + "@aws-sdk/querystring-parser" "3.78.0" + "@aws-sdk/types" "3.78.0" + tslib "^2.3.1" + +"@aws-sdk/util-arn-parser@3.55.0": + version "3.55.0" + resolved "https://registry.yarnpkg.com/@aws-sdk/util-arn-parser/-/util-arn-parser-3.55.0.tgz#6672eb2975e798a460bedfaf6b5618d4e4b262e1" + integrity sha512-76KJxp4MRWufHYWys7DFl64znr5yeJ3AIQNAPCKKw1sP0hzO7p6Kx0PaJnw9x+CPSzOrT4NbuApL6/srYhKDGg== + dependencies: + tslib "^2.3.1" + +"@aws-sdk/util-base64-browser@3.58.0": + version "3.58.0" + resolved "https://registry.yarnpkg.com/@aws-sdk/util-base64-browser/-/util-base64-browser-3.58.0.tgz#e213f91a5d40dd2d048d340f1ab192ca86c1f40c" + integrity sha512-0ebsXIZNpu/fup9OgsFPnRKfCFbuuI9PPRzvP6twzLxUB0c/aix6Co7LGHFKcRKHZdaykoJMXArf8eHj2Nzv1Q== + dependencies: + tslib "^2.3.1" + +"@aws-sdk/util-base64-node@3.55.0": + version "3.55.0" + resolved "https://registry.yarnpkg.com/@aws-sdk/util-base64-node/-/util-base64-node-3.55.0.tgz#da9a3fd6752be49163572144793e6b23d0186ff4" + integrity sha512-UQ/ZuNoAc8CFMpSiRYmevaTsuRKzLwulZTnM8LNlIt9Wx1tpNvqp80cfvVj7yySKROtEi20wq29h31dZf1eYNQ== + dependencies: + "@aws-sdk/util-buffer-from" "3.55.0" + tslib "^2.3.1" + +"@aws-sdk/util-body-length-browser@3.55.0": + version "3.55.0" + resolved "https://registry.yarnpkg.com/@aws-sdk/util-body-length-browser/-/util-body-length-browser-3.55.0.tgz#9c2637097501032f6a1afddb76687415fe9b44b6" + integrity sha512-Ei2OCzXQw5N6ZkTMZbamUzc1z+z1R1Ja5tMEagz5BxuX4vWdBObT+uGlSzL8yvTbjoPjnxWA2aXyEqaUP3JS8Q== + dependencies: + tslib "^2.3.1" + +"@aws-sdk/util-body-length-node@3.55.0": + version "3.55.0" + resolved "https://registry.yarnpkg.com/@aws-sdk/util-body-length-node/-/util-body-length-node-3.55.0.tgz#67049bbb6c62d794a1bb5a13b9a678988c925489" + integrity sha512-lU1d4I+9wJwydduXs0SxSfd+mHKjxeyd39VwOv6i2KSwWkPbji9UQqpflKLKw+r45jL7+xU/zfeTUg5Tt/3Gew== + dependencies: + tslib "^2.3.1" + +"@aws-sdk/util-buffer-from@3.55.0": + version "3.55.0" + resolved "https://registry.yarnpkg.com/@aws-sdk/util-buffer-from/-/util-buffer-from-3.55.0.tgz#e7c927974b07a29502aa1ad58509b91d0d7cf0f7" + integrity sha512-uVzKG1UgvnV7XX2FPTylBujYMKBPBaq/qFBxfl0LVNfrty7YjpfieQxAe6yRLD+T0Kir/WDQwGvYC+tOYG3IGA== + dependencies: + "@aws-sdk/is-array-buffer" "3.55.0" + tslib "^2.3.1" + +"@aws-sdk/util-config-provider@3.55.0": + version "3.55.0" + resolved "https://registry.yarnpkg.com/@aws-sdk/util-config-provider/-/util-config-provider-3.55.0.tgz#720c6c0ac1aa8d14be29d1dee25e01eb4925c0ce" + integrity sha512-30dzofQQfx6tp1jVZkZ0DGRsT0wwC15nEysKRiAcjncM64A0Cm6sra77d0os3vbKiKoPCI/lMsFr4o3533+qvQ== + dependencies: + tslib "^2.3.1" + +"@aws-sdk/util-create-request@3.78.0": + version "3.78.0" + resolved "https://registry.yarnpkg.com/@aws-sdk/util-create-request/-/util-create-request-3.78.0.tgz#a6b476e1e86b6179660a0a7e7d49fd0df31bdffa" + integrity sha512-aGRuBXGZ/GFYpP+Bkdzo6kyfX1nkH0dhFK6RYZLxe3r7X/AfkMKeUmIco9tyS1sBAiyoy6a7Re/Oux2Y+ASnjg== + dependencies: + "@aws-sdk/middleware-stack" "3.78.0" + "@aws-sdk/smithy-client" "3.78.0" + "@aws-sdk/types" "3.78.0" + tslib "^2.3.1" + +"@aws-sdk/util-defaults-mode-browser@3.78.0": + version "3.78.0" + resolved "https://registry.yarnpkg.com/@aws-sdk/util-defaults-mode-browser/-/util-defaults-mode-browser-3.78.0.tgz#e387d2efb2fa2e7b02a2a68216efbb5a2f4861e7" + integrity sha512-fsKEqlRbrztjpdTsMbZTlWxFpo3Av9QeYYpJuFaZbwfE0ElzinUU54kKwUrKbi60HRroQV+itoUNj3JogQDeHw== + dependencies: + "@aws-sdk/property-provider" "3.78.0" + "@aws-sdk/types" "3.78.0" + bowser "^2.11.0" + tslib "^2.3.1" + +"@aws-sdk/util-defaults-mode-node@3.81.0": + version "3.81.0" + resolved "https://registry.yarnpkg.com/@aws-sdk/util-defaults-mode-node/-/util-defaults-mode-node-3.81.0.tgz#b12528db025196177d27e3ddf60461b4af7b53b5" + integrity sha512-+7YOtl+TxF08oXt2h/ONP5qk6ZZg6GaO1YSAdpjIfco4odhpy7N2AlEGSX0jZyP6Zbfi+8N7yihBa4sOuOf+Cw== + dependencies: + "@aws-sdk/config-resolver" "3.80.0" + "@aws-sdk/credential-provider-imds" "3.81.0" + "@aws-sdk/node-config-provider" "3.80.0" + "@aws-sdk/property-provider" "3.78.0" + "@aws-sdk/types" "3.78.0" + tslib "^2.3.1" + +"@aws-sdk/util-format-url@3.78.0": + version "3.78.0" + resolved "https://registry.yarnpkg.com/@aws-sdk/util-format-url/-/util-format-url-3.78.0.tgz#a363f5f72297dd49663f828335409992057c1e13" + integrity sha512-wdjt8ZAMyBrH/02QrQtB+S9cwtsBJ6bXRJ3XwL6z7L75nwTflKkzOQUS5Ie7HBf3j3JH0KhlqlEbf2nnM9jsPQ== + dependencies: + "@aws-sdk/querystring-builder" "3.78.0" + "@aws-sdk/types" "3.78.0" + tslib "^2.3.1" + +"@aws-sdk/util-hex-encoding@3.58.0": + version "3.58.0" + resolved "https://registry.yarnpkg.com/@aws-sdk/util-hex-encoding/-/util-hex-encoding-3.58.0.tgz#d999eb19329933a94563881540a06d7ac7f515f5" + integrity sha512-Rl+jXUzk/FJkOLYfUVYPhKa2aUmTpeobRP31l8IatQltSzDgLyRHO35f6UEs7Ztn5s1jbu/POatLAZ2WjbgVyg== + dependencies: + tslib "^2.3.1" + +"@aws-sdk/util-locate-window@^3.0.0": + version "3.55.0" + resolved "https://registry.yarnpkg.com/@aws-sdk/util-locate-window/-/util-locate-window-3.55.0.tgz#a4136a20ee1bfcb73967a6614caf769ef79db070" + integrity sha512-0sPmK2JaJE2BbTcnvybzob/VrFKCXKfN4CUKcvn0yGg/me7Bz+vtzQRB3Xp+YSx+7OtWxzv63wsvHoAnXvgxgg== + dependencies: + tslib "^2.3.1" + +"@aws-sdk/util-middleware@3.78.0": + version "3.78.0" + resolved "https://registry.yarnpkg.com/@aws-sdk/util-middleware/-/util-middleware-3.78.0.tgz#d907a9b8b7878265cd3e3ee15996bc17de41db11" + integrity sha512-Hi3wv2b0VogO4mzyeEaeU5KgIt4qeo0LXU5gS6oRrG0T7s2FyKbMBkJW3YDh/Y8fNwqArZ+/QQFujpP0PIKwkA== + dependencies: + tslib "^2.3.1" + +"@aws-sdk/util-stream-browser@3.78.0": + version "3.78.0" + resolved "https://registry.yarnpkg.com/@aws-sdk/util-stream-browser/-/util-stream-browser-3.78.0.tgz#d4578ab9d1ff882f792f3381604c90718310405c" + integrity sha512-EcThf/sJoD4NYTUNO/nehR57lqkOuL6btRoVnm4LGUR8XgQcJ/WMYYgxOMY8E81xXzRFX2ukRHRxL2xmQsbHDw== + dependencies: + "@aws-sdk/types" "3.78.0" + tslib "^2.3.1" + +"@aws-sdk/util-stream-node@3.78.0": + version "3.78.0" + resolved "https://registry.yarnpkg.com/@aws-sdk/util-stream-node/-/util-stream-node-3.78.0.tgz#37b2f07e4ec3b325d93bd49c7eedf5d891c8d69b" + integrity sha512-CHfX37ioUyamAnlS2p4Nq+4BBjCSlZolFkVyxtVJwzPBBksdvjW67nKG+SShR48RBPJ5LEzbgAaEXNRktCSf6w== + dependencies: + "@aws-sdk/types" "3.78.0" + tslib "^2.3.1" + +"@aws-sdk/util-uri-escape@3.55.0": + version "3.55.0" + resolved "https://registry.yarnpkg.com/@aws-sdk/util-uri-escape/-/util-uri-escape-3.55.0.tgz#ee57743c628a1c9f942dfe73205ce890ec011916" + integrity sha512-mmdDLUpFCN2nkfwlLdOM54lTD528GiGSPN1qb8XtGLgZsJUmg3uJSFIN2lPeSbEwJB3NFjVas/rnQC48i7mV8w== + dependencies: + tslib "^2.3.1" + +"@aws-sdk/util-user-agent-browser@3.78.0": + version "3.78.0" + resolved "https://registry.yarnpkg.com/@aws-sdk/util-user-agent-browser/-/util-user-agent-browser-3.78.0.tgz#12509ed9cc77624da0e0c017099565e37a5038d0" + integrity sha512-diGO/Bf4ggBOEnfD7lrrXaaXOwOXGz0bAJ0HhpizwEMlBld5zfDlWXjNpslh+8+u3EHRjPJQ16KGT6mp/Dm+aw== + dependencies: + "@aws-sdk/types" "3.78.0" + bowser "^2.11.0" + tslib "^2.3.1" + +"@aws-sdk/util-user-agent-node@3.80.0": + version "3.80.0" + resolved "https://registry.yarnpkg.com/@aws-sdk/util-user-agent-node/-/util-user-agent-node-3.80.0.tgz#269ea0f9bfab4f378af759afa9137936081f010a" + integrity sha512-QV26qIXws1m6sZXg65NS+XrQ5NhAzbDVQLtEVE4nC39UN8fuieP6Uet/gZm9mlLI9hllwvcV7EfgBM3GSC7pZg== + dependencies: + "@aws-sdk/node-config-provider" "3.80.0" + "@aws-sdk/types" "3.78.0" + tslib "^2.3.1" + +"@aws-sdk/util-utf8-browser@3.55.0", "@aws-sdk/util-utf8-browser@^3.0.0": + version "3.55.0" + resolved "https://registry.yarnpkg.com/@aws-sdk/util-utf8-browser/-/util-utf8-browser-3.55.0.tgz#a045bf1a93f6e0ff9c846631b168ea55bbb37668" + integrity sha512-ljzqJcyjfJpEVSIAxwtIS8xMRUly84BdjlBXyp6cu4G8TUufgjNS31LWdhyGhgmW5vYBNr+LTz0Kwf6J+ou7Ug== + dependencies: + tslib "^2.3.1" + +"@aws-sdk/util-utf8-node@3.55.0": + version "3.55.0" + resolved "https://registry.yarnpkg.com/@aws-sdk/util-utf8-node/-/util-utf8-node-3.55.0.tgz#44cf9f9c8624d144afd65ab8a1786e33134add15" + integrity sha512-FsFm7GFaC7j0tlPEm/ri8bU2QCwFW5WKjxUg8lm1oWaxplCpKGUsmcfPJ4sw58GIoyoGu4QXBK60oCWosZYYdQ== + dependencies: + "@aws-sdk/util-buffer-from" "3.55.0" + tslib "^2.3.1" + +"@aws-sdk/util-waiter@3.78.0": + version "3.78.0" + resolved "https://registry.yarnpkg.com/@aws-sdk/util-waiter/-/util-waiter-3.78.0.tgz#5886f3e06ae6df9a12ef7079a6e75c76921ea4da" + integrity sha512-8pWd0XiNOS8AkWQyac8VNEI+gz/cGWlC2TAE2CJp0rOK5XhvlcNBINai4D6TxQ+9foyJXLOI1b8nuXemekoG8A== + dependencies: + "@aws-sdk/abort-controller" "3.78.0" + "@aws-sdk/types" "3.78.0" + tslib "^2.3.1" + +"@aws-sdk/xml-builder@3.55.0": + version "3.55.0" + resolved "https://registry.yarnpkg.com/@aws-sdk/xml-builder/-/xml-builder-3.55.0.tgz#8e14012ab3ed27cf68964abf1326d06c686b3511" + integrity sha512-BH+i5S2FLprmfSeIuGy3UbNtEoJPVjh8arl5+LV3i2KY/+TmrS4yT8JtztDlDxHF0cMtNLZNO0KEPtsACS6SOg== + dependencies: + tslib "^2.3.1" + "@azure/abort-controller@^1.0.0": version "1.0.4" resolved "https://registry.yarnpkg.com/@azure/abort-controller/-/abort-controller-1.0.4.tgz#fd3c4d46c8ed67aace42498c8e2270960250eafd" @@ -4490,7 +5414,7 @@ balanced-match@^1.0.0: resolved "https://registry.yarnpkg.com/balanced-match/-/balanced-match-1.0.2.tgz#e83e3a7e3f300b34cb9d87f615fa0cbf357690ee" integrity sha512-3oSeUO0TMV67hN1AmbXsK4yaqU7tjiHlbxRDZOpH0KW9+CeX4bRAaX0Anxt0tx2MrpRpWwQaPwIlISEJhYU5Pw== -base64-js@^1.3.1: +base64-js@^1.0.2, base64-js@^1.3.1: version "1.5.1" resolved "https://registry.yarnpkg.com/base64-js/-/base64-js-1.5.1.tgz#1b1b440160a5bf7ad40b650f095963481903930a" integrity sha512-AKpaYlHn8t4SVbOHCy+b5+KKgvR4vrsD8vbvrbiQJps7fKDTkjkDry6ji0rUJjC0kzbNePLwzxq8iypo41qeWA== @@ -4576,6 +5500,11 @@ boolbase@^1.0.0: resolved "https://registry.yarnpkg.com/boolbase/-/boolbase-1.0.0.tgz#68dff5fbe60c51eb37725ea9e3ed310dcc1e776e" integrity sha1-aN/1++YMUes3cl6p4+0xDcwed24= +bowser@^2.11.0: + version "2.11.0" + resolved "https://registry.yarnpkg.com/bowser/-/bowser-2.11.0.tgz#5ca3c35757a7aa5771500c70a73a9f91ef420a8f" + integrity sha512-AlcaJBi/pqqJBIQ8U9Mcpc9i8Aqxn88Skv5d+xBX006BY5u8N3mGLHa5Lgppa7L/HfwgwLgZ6NYs+Ag6uUmJRA== + boxen@^1.3.0: version "1.3.0" resolved "https://registry.yarnpkg.com/boxen/-/boxen-1.3.0.tgz#55c6c39a8ba58d9c61ad22cd877532deb665a20b" @@ -4675,6 +5604,14 @@ buffer-writer@2.0.0: resolved "https://registry.yarnpkg.com/buffer-writer/-/buffer-writer-2.0.0.tgz#ce7eb81a38f7829db09c873f2fbb792c0c98ec04" integrity sha512-a7ZpuTZU1TRtnwyCNW3I5dc0wWNC3VR9S++Ewyk2HHZdrO3CQJqSpd+95Us590V6AL7JqUAH2IwZ/398PmNFgw== +buffer@5.6.0: + version "5.6.0" + resolved "https://registry.yarnpkg.com/buffer/-/buffer-5.6.0.tgz#a31749dc7d81d84db08abf937b6b8c4033f62786" + integrity sha512-/gDYp/UtU0eA1ys8bOs9J6a+E/KWIY+DZ+Q2WESNUA0jFRsJOc0SNUO6xJ5SGA1xueg3NL65W6s+NY5l9cunuw== + dependencies: + base64-js "^1.0.2" + ieee754 "^1.1.4" + buffer@^5.5.0: version "5.7.1" resolved "https://registry.yarnpkg.com/buffer/-/buffer-5.7.1.tgz#ba62e7c13133053582197160851a8f648e99eed0" @@ -5883,7 +6820,7 @@ enquirer@^2.3.0, enquirer@^2.3.5, enquirer@^2.3.6: dependencies: ansi-colors "^4.1.1" -entities@^2.0.0: +entities@2.2.0, entities@^2.0.0: version "2.2.0" resolved "https://registry.yarnpkg.com/entities/-/entities-2.2.0.tgz#098dc90ebb83d8dffa089d55256b351d34c4da55" integrity sha512-p92if5Nz619I0w+akJrLZH0MX0Pb5DX39XOwQTtXSdQQOaYH03S1uIQp4mhOZtAXrxq4ViO67YTiLBo2638o9A== @@ -6227,7 +7164,7 @@ event-stream@=3.3.4: stream-combiner "~0.0.4" through "~2.3.1" -events@^3.0.0: +events@3.3.0, events@^3.0.0: version "3.3.0" resolved "https://registry.yarnpkg.com/events/-/events-3.3.0.tgz#31a95ad0a924e2d2c419a813aeb2c4e878ea7400" integrity sha512-mQw+2fkQbALzQ7V0MY0IqdnXNOeTtP4r0lN9z7AAawCXgqea7bDii20AYrIBrFd/Hx0M2Ocz6S111CaFkUcb0Q== @@ -6469,6 +7406,11 @@ fast-write-atomic@0.2.1: resolved "https://registry.yarnpkg.com/fast-write-atomic/-/fast-write-atomic-0.2.1.tgz#7ee8ef0ce3c1f531043c09ae8e5143361ab17ede" integrity sha512-WvJe06IfNYlr+6cO3uQkdKdy3Cb1LlCJSF8zRs2eT8yuhdbSlR9nIt+TgQ92RUxiRrQm+/S7RARnMfCs5iuAjw== +fast-xml-parser@3.19.0: + version "3.19.0" + resolved "https://registry.yarnpkg.com/fast-xml-parser/-/fast-xml-parser-3.19.0.tgz#cb637ec3f3999f51406dd8ff0e6fc4d83e520d01" + integrity sha512-4pXwmBplsCPv8FOY1WRakF970TjNGnGnfbOnLqjlYvMiF1SR3yOHyxMR/YCXpPTOspNF5gwudqktIP4VsWkvBg== + fastq@^1.6.0: version "1.13.0" resolved "https://registry.yarnpkg.com/fastq/-/fastq-1.13.0.tgz#616760f88a7526bdfc596b7cab8c18938c36b98c" @@ -7387,7 +8329,7 @@ iconv-lite@^0.6.3: dependencies: safer-buffer ">= 2.1.2 < 3.0.0" -ieee754@^1.1.13, ieee754@^1.2.1: +ieee754@^1.1.13, ieee754@^1.1.4, ieee754@^1.2.1: version "1.2.1" resolved "https://registry.yarnpkg.com/ieee754/-/ieee754-1.2.1.tgz#8eb7a10a63fff25d15a57b001586d177d1b0d352" integrity sha512-dcyqhDvX1C46lXZcVqCpK+FtMRQVdIMN6/Df5js2zouUsqG7I6sFxitIC+7KYK29KdXOLHdu9zL4sFnoVQnqaA== @@ -7482,7 +8424,7 @@ inflight@^1.0.4: once "^1.3.0" wrappy "1" -inherits@2, inherits@2.0.4, inherits@^2.0.0, inherits@^2.0.1, inherits@^2.0.3, inherits@^2.0.4, inherits@~2.0.1, inherits@~2.0.3: +inherits@2, inherits@2.0.4, inherits@^2.0.0, inherits@^2.0.1, inherits@^2.0.3, inherits@^2.0.4, inherits@~2.0.1, inherits@~2.0.3, inherits@~2.0.4: version "2.0.4" resolved "https://registry.yarnpkg.com/inherits/-/inherits-2.0.4.tgz#0fa2c64f932917c3433a0ded55363aae37416b7c" integrity sha512-k/vGaX4/Yla3WzyMCvTQOXYeIHvqOKtnqBduzTHpzpQZzAskKMhZ2K+EnBiSM9zGSoIFeMpXKxa4dYeZIQqewQ== @@ -11321,7 +12263,7 @@ readable-stream@^2.0.0, readable-stream@^2.0.2, readable-stream@^2.0.5, readable string_decoder "~1.1.1" util-deprecate "~1.0.1" -readable-stream@^3.1.1, readable-stream@^3.4.0, readable-stream@^3.6.0: +readable-stream@^3.1.1, readable-stream@^3.4.0, readable-stream@^3.5.0, readable-stream@^3.6.0: version "3.6.0" resolved "https://registry.yarnpkg.com/readable-stream/-/readable-stream-3.6.0.tgz#337bbda3adc0706bd3e024426a286d4b4b2c9198" integrity sha512-BViHy7LKeTz4oNnkcLJ+lVSL6vpiFeX6/d3oSH8zCW7UxP2onchk+vTGB143xuFjHS3deTgkKoXXymXqymiIdA== @@ -12319,6 +13261,14 @@ stoppable@^1.1.0: resolved "https://registry.yarnpkg.com/stoppable/-/stoppable-1.1.0.tgz#32da568e83ea488b08e4d7ea2c3bcc9d75015d5b" integrity sha512-KXDYZ9dszj6bzvnEMRYvxgeTHU74QBFL54XKtP3nyMuJ81CFYtABZ3bAzL2EdFUaEwJOBOgENyFj3R7oTzDyyw== +stream-browserify@3.0.0: + version "3.0.0" + resolved "https://registry.yarnpkg.com/stream-browserify/-/stream-browserify-3.0.0.tgz#22b0a2850cdf6503e73085da1fc7b7d0c2122f2f" + integrity sha512-H73RAHsVBapbim0tU2JwwOiXUj+fikfiaoYAKHF3VJfA0pe2BCzkhAHBlLG6REzE+2WNZcxOXjK7lkso+9euLA== + dependencies: + inherits "~2.0.4" + readable-stream "^3.5.0" + stream-combiner@~0.0.4: version "0.0.4" resolved "https://registry.yarnpkg.com/stream-combiner/-/stream-combiner-0.0.4.tgz#4d5e433c185261dde623ca3f44c586bcf5c4ad14" @@ -13027,12 +13977,12 @@ tsconfig-paths@^3.11.0: minimist "^1.2.0" strip-bom "^3.0.0" -tslib@^1.0.0, tslib@^1.8.1, tslib@^1.9.3: +tslib@^1.0.0, tslib@^1.11.1, tslib@^1.8.1, tslib@^1.9.3: version "1.14.1" resolved "https://registry.yarnpkg.com/tslib/-/tslib-1.14.1.tgz#cf2d38bdc34a134bcaf1091c41f6619e2f672d00" integrity sha512-Xni35NKzjgMrwevysHTCArtLDpPvye8zV/0E4EyYn43P7/7qvQwPh9BGkHewbMulVntbigmcT7rdX3BNo9wRJg== -tslib@^2.0.0, tslib@^2.0.3, tslib@^2.1.0, tslib@^2.2.0, tslib@^2.3.0, tslib@~2.3.0: +tslib@^2.0.0, tslib@^2.0.3, tslib@^2.1.0, tslib@^2.2.0, tslib@^2.3.0, tslib@^2.3.1, tslib@~2.3.0: version "2.3.1" resolved "https://registry.yarnpkg.com/tslib/-/tslib-2.3.1.tgz#e8a335add5ceae51aa261d32a490158ef042ef01" integrity sha512-77EbyPPpMz+FRFRuAFlWMtmgUWGe9UOG2Z25NqCwiIjRhOf5iKGuzSe5P2w1laq+FkRy4p+PCuVkJSGkzTEKVw==