Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Tidy up Cloudinary package and add missing changeset #8836

Merged
merged 4 commits into from Sep 26, 2023
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Jump to
Jump to file
Failed to load files.
Diff view
Diff view
5 changes: 5 additions & 0 deletions .changeset/fix-cloudinary-init.md
@@ -0,0 +1,5 @@
---
'@keystone-6/cloudinary': patch
---

Remove runtime errors from the Cloudinary field, fall back to the `cloudinary` package for Cloudinary errors
159 changes: 0 additions & 159 deletions packages/cloudinary/src/cloudinary.ts

This file was deleted.

92 changes: 65 additions & 27 deletions packages/cloudinary/src/index.ts
@@ -1,13 +1,8 @@
import { randomBytes } from 'node:crypto';
import {
CommonFieldConfig,
BaseListTypeInfo,
FieldTypeFunc,
jsonFieldTypePolyfilledForSQLite,
} from '@keystone-6/core/types';
import type { CommonFieldConfig, BaseListTypeInfo, FieldTypeFunc } from '@keystone-6/core/types';
import { jsonFieldTypePolyfilledForSQLite } from '@keystone-6/core/types';
import { graphql } from '@keystone-6/core';
import cloudinary from 'cloudinary';
import { CloudinaryAdapter } from './cloudinary';

type StoredFile = {
id: string;
Expand Down Expand Up @@ -106,21 +101,20 @@ export const outputType: graphql.ObjectType<CloudinaryImage_File> =
},
});

export const cloudinaryImage =
<ListTypeInfo extends BaseListTypeInfo>({
cloudinary,
...config
}: CloudinaryImageFieldConfig<ListTypeInfo>): FieldTypeFunc<ListTypeInfo> =>
meta => {
// TODO: no delete support
Copy link
Member Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

This field implicitly preserves the images that are uploaded, as mentioned in #8816 (comment), we should align this field with the storage approaches we have elsewhere - or at least revisit that design

export function cloudinaryImage<ListTypeInfo extends BaseListTypeInfo>({
cloudinary: cloudinaryConfig,
...config
}: CloudinaryImageFieldConfig<ListTypeInfo>): FieldTypeFunc<ListTypeInfo> {
return meta => {
if ((config as any).isIndexed === 'unique') {
throw Error("isIndexed: 'unique' is not a supported option for field type cloudinaryImage");
}

const adapter = new CloudinaryAdapter(cloudinary);
const inputArg = graphql.arg({ type: graphql.Upload });
const resolveInput = async (
async function resolveInput(
uploadData: graphql.InferValueFromArg<typeof inputArg>
): Promise<StoredFile | undefined | null | 'DbNull'> => {
): Promise<StoredFile | undefined | null | 'DbNull'> {
if (uploadData === null) {
return meta.provider === 'postgresql' || meta.provider === 'mysql' ? 'DbNull' : null;
}
Expand All @@ -131,21 +125,42 @@ export const cloudinaryImage =
const { createReadStream, filename: originalFilename, mimetype, encoding } = await uploadData;
const stream = createReadStream();

// TODO: FIXME: stream can be null
if (!stream) {
// TODO: FIXME: Handle when stream is null. Can happen when:
// Updating some other part of the item, but not the file (gets null
// because no File DOM element is uploaded)
return undefined;
}

const { id, filename, _meta } = await adapter.save({
stream,
filename: originalFilename,
id: randomBytes(20).toString('base64url'),
const id = randomBytes(20).toString('base64url');
const _meta = await new Promise<cloudinary.UploadApiResponse>((resolve, reject) => {
const cloudinaryStream = cloudinary.v2.uploader.upload_stream(
{
public_id: id,
folder: cloudinaryConfig.folder,
api_key: cloudinaryConfig.apiKey,
api_secret: cloudinaryConfig.apiSecret,
cloud_name: cloudinaryConfig.cloudName,
},
(error, result) => {
if (error || !result) {
return reject(error);
}
resolve(result);
}
);

stream.pipe(cloudinaryStream);
});

return { id, filename, originalFilename, mimetype, encoding, _meta };
};
return {
id,
filename: originalFilename,
originalFilename,
mimetype,
encoding,
_meta,
};
}

return jsonFieldTypePolyfilledForSQLite(
meta.provider,
{
Expand All @@ -163,14 +178,36 @@ export const cloudinaryImage =
}
const val = value as any;
return {
publicUrl: adapter.publicUrl(val),
publicUrl: val?._meta?.secure_url ?? null,
publicUrlTransformed: ({
transformation,
}: {
transformation: graphql.InferValueFromArg<
graphql.Arg<typeof CloudinaryImageFormat>
>;
}) => adapter.publicUrlTransformed(val, transformation ?? {}),
}) => {
if (!val._meta) return null;

const { prettyName, ...rest } = transformation ?? {};

// no formatting options provided, return the publicUrl field
if (!Object.keys(rest).length) {
return val?._meta?.secure_url ?? null;
}

const { public_id, format } = val._meta;

// ref https://github.com/cloudinary/cloudinary_npm/blob/439586eac73cee7f2803cf19f885e98f237183b3/src/utils.coffee#L472
// @ts-ignore
return cloudinary.url(public_id, {
type: 'upload',
format,
secure: true,
url_suffix: prettyName,
transformation,
cloud_name: cloudinaryConfig.cloudName,
});
},
...val,
};
},
Expand All @@ -182,3 +219,4 @@ export const cloudinaryImage =
}
);
};
}