Skip to content

Commit

Permalink
Keystone cloud files (#5868)
Browse files Browse the repository at this point in the history
* Added support for integration with Keystone Cloud files
  • Loading branch information
rohan-deshpande authored Jun 10, 2021
1 parent 98482ef commit 84a5e7f
Show file tree
Hide file tree
Showing 10 changed files with 199 additions and 35 deletions.
8 changes: 8 additions & 0 deletions .changeset/sixty-grapes-roll.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,8 @@
---
'@keystone-next/example-assets-cloud': minor
'@keystone-next/fields': minor
'@keystone-next/keystone': minor
'@keystone-next/types': minor
---

Added experimental support for the integration with keystone cloud files
3 changes: 3 additions & 0 deletions examples-staging/assets-cloud/keystone.ts
Original file line number Diff line number Diff line change
Expand Up @@ -20,6 +20,9 @@ export default config({
images: {
upload: 'keystone-cloud',
},
files: {
upload: 'keystone-cloud',
},
experimental: {
keystoneCloud: {
apiKey: KEYSTONE_CLOUD_API_KEY,
Expand Down
22 changes: 22 additions & 0 deletions examples-staging/assets-cloud/schema.graphql
Original file line number Diff line number Diff line change
Expand Up @@ -9,6 +9,7 @@ type Post {
publishDate: String
author: Author
hero: ImageFieldOutput
attachment: FileFieldOutput
}

enum PostStatusType {
Expand All @@ -33,6 +34,13 @@ enum ImageExtension {
gif
}

interface FileFieldOutput {
filename: String!
filesize: Int!
ref: String!
src: String!
}

type LocalImageFieldOutput implements ImageFieldOutput {
id: ID!
filesize: Int!
Expand All @@ -43,6 +51,13 @@ type LocalImageFieldOutput implements ImageFieldOutput {
src: String!
}

type LocalFileFieldOutput implements FileFieldOutput {
filename: String!
filesize: Int!
ref: String!
src: String!
}

input PostWhereInput {
AND: [PostWhereInput!]
OR: [PostWhereInput!]
Expand Down Expand Up @@ -119,6 +134,7 @@ input PostUpdateInput {
publishDate: String
author: AuthorRelateToOneInput
hero: ImageFieldInput
attachment: FileFieldInput
}

input AuthorRelateToOneInput {
Expand All @@ -138,6 +154,11 @@ The `Upload` scalar type represents a file upload.
"""
scalar Upload

input FileFieldInput {
upload: Upload
ref: String
}

input PostsUpdateInput {
id: ID!
data: PostUpdateInput
Expand All @@ -150,6 +171,7 @@ input PostCreateInput {
publishDate: String
author: AuthorRelateToOneInput
hero: ImageFieldInput
attachment: FileFieldInput
}

input PostsCreateInput {
Expand Down
29 changes: 16 additions & 13 deletions examples-staging/assets-cloud/schema.prisma
Original file line number Diff line number Diff line change
Expand Up @@ -9,19 +9,22 @@ generator client {
}

model Post {
id Int @id @default(autoincrement())
title String?
status String?
content String?
publishDate DateTime?
author Author? @relation("Post_author", fields: [authorId], references: [id])
authorId Int? @map("author")
hero_filesize Int?
hero_extension String?
hero_width Int?
hero_height Int?
hero_mode String?
hero_id String?
id Int @id @default(autoincrement())
title String?
status String?
content String?
publishDate DateTime?
author Author? @relation("Post_author", fields: [authorId], references: [id])
authorId Int? @map("author")
hero_filesize Int?
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])
}
Expand Down
3 changes: 2 additions & 1 deletion examples-staging/assets-cloud/schema.ts
Original file line number Diff line number Diff line change
@@ -1,5 +1,5 @@
import { createSchema, list } from '@keystone-next/keystone/schema';
import { select, relationship, text, timestamp, image } from '@keystone-next/fields';
import { select, relationship, text, timestamp, image, file } from '@keystone-next/fields';

export const lists = createSchema({
Post: list({
Expand All @@ -16,6 +16,7 @@ export const lists = createSchema({
publishDate: timestamp(),
author: relationship({ ref: 'Author.posts', many: false }),
hero: image(),
attachment: file(),
},
}),
Author: list({
Expand Down
2 changes: 1 addition & 1 deletion packages-next/keystone/src/lib/context/createContext.ts
Original file line number Diff line number Diff line change
Expand Up @@ -27,7 +27,7 @@ export function makeCreateContext({
gqlNamesByList: Record<string, GqlNames>;
}) {
const images = createImagesContext(config);
const files = createFilesContext(config.files);
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),
Expand Down
72 changes: 58 additions & 14 deletions packages-next/keystone/src/lib/context/createFilesContext.ts
Original file line number Diff line number Diff line change
Expand Up @@ -2,11 +2,16 @@ import path from 'path';
import crypto from 'crypto';
import { pipeline } from 'stream';
import filenamify from 'filenamify';
import { FilesConfig, FilesContext } from '@keystone-next/types';
import { KeystoneConfig, FilesContext } from '@keystone-next/types';
import fs from 'fs-extra';

import { parseFileRef } from '@keystone-next/utils-legacy';
import { parseFileRef, isLocalAsset, isKeystoneCloudAsset } from '@keystone-next/utils-legacy';
import slugify from '@sindresorhus/slugify';
import {
buildKeystoneCloudFileSrc,
uploadFileToKeystoneCloud,
getFileFromKeystoneCloud,
} from '../keystone-cloud/assets';

const DEFAULT_BASE_URL = '/files';
const DEFAULT_STORAGE_PATH = './public/files';
Expand Down Expand Up @@ -41,31 +46,70 @@ const generateSafeFilename = (
return `${urlSafeName}-${id}`;
};

export function createFilesContext(config?: FilesConfig): FilesContext | undefined {
if (!config) {
export function createFilesContext(config: KeystoneConfig): FilesContext | undefined {
if (!config.files) {
return;
}

const { baseUrl = DEFAULT_BASE_URL, storagePath = DEFAULT_STORAGE_PATH } = config.local || {};
const { files, experimental } = config;
const { baseUrl = DEFAULT_BASE_URL, storagePath = DEFAULT_STORAGE_PATH } = files.local || {};
const {
apiKey = '',
graphqlApiEndpoint = '',
restApiEndpoint = '',
} = experimental?.keystoneCloud || {};

fs.mkdirSync(storagePath, { recursive: true });
if (isLocalAsset(files.upload)) {
fs.mkdirSync(storagePath, { recursive: true });
}

return {
getSrc: (mode, filename) => {
getSrc: async (mode, filename) => {
if (isKeystoneCloudAsset(mode)) {
return await buildKeystoneCloudFileSrc({ apiKey, graphqlApiEndpoint, 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 (isKeystoneCloudAsset(mode)) {
const { filesize } = await getFileFromKeystoneCloud({
apiKey,
filename,
restApiEndpoint,
});

return { filesize, ...fileRef };
}

const { size: filesize } = await fs.stat(path.join(storagePath, fileRef.filename));

return { filesize, ...fileRef };
},
getDataFromStream: async (stream, filename) => {
const { upload: mode } = config;
const safeFilename = generateSafeFilename(filename, config.transformFilename);
const writeStream = fs.createWriteStream(path.join(storagePath, safeFilename));
getDataFromStream: async (stream, originalFilename) => {
const { upload: mode } = files;
const filename = generateSafeFilename(originalFilename, files.transformFilename);

if (isKeystoneCloudAsset(mode)) {
const { filesize } = await uploadFileToKeystoneCloud({
apiKey,
stream,
filename,
restApiEndpoint,
});

return { mode, filesize, filename };
}

const writeStream = fs.createWriteStream(path.join(storagePath, filename));
const pipeStreams: Promise<void> = new Promise((resolve, reject) => {
pipeline(stream, writeStream, err => {
if (err) {
Expand All @@ -78,10 +122,10 @@ export function createFilesContext(config?: FilesConfig): FilesContext | undefin

try {
await pipeStreams;
const { size: filesize } = await fs.stat(path.join(storagePath, safeFilename));
return { mode, filesize, filename: safeFilename };
const { size: filesize } = await fs.stat(path.join(storagePath, filename));
return { mode, filesize, filename };
} catch (e) {
await fs.remove(path.join(storagePath, safeFilename));
await fs.remove(path.join(storagePath, filename));
throw e;
}
},
Expand Down
Loading

1 comment on commit 84a5e7f

@vercel
Copy link

@vercel vercel bot commented on 84a5e7f Jun 10, 2021

Choose a reason for hiding this comment

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

Please sign in to comment.