Try to get uploads to work

This commit is contained in:
Amit Jakubowicz 2019-11-23 10:51:20 +01:00
parent 7ccc951107
commit 04101bf674
8 changed files with 180 additions and 98 deletions

14
@types/graphql.d.ts vendored
View File

@ -72,6 +72,7 @@ declare namespace GQL {
location: ILocation;
occurrences: Array<IEventOccurrence | null> | null;
tags: Array<IEventTag | null> | null;
images: IEventImages | null;
}
interface IInfoOnCalendarEventArguments {
@ -127,6 +128,19 @@ declare namespace GQL {
text: string;
}
interface IEventImages {
__typename: 'EventImages';
thumb: IImage | null;
cover: IImage | null;
poster: IImage | null;
gallery: Array<IImage> | null;
}
interface IImage {
__typename: 'Image';
url: string;
}
interface IUserRole {
__typename: 'UserRole';
user: IUser;

View File

@ -23,6 +23,7 @@
"date-fns": "^2.7.0",
"dotenv": "^8.2.0",
"express": "^4.17.1",
"fs-capacitor": "^5.0.0",
"graphql": "^14.4.2",
"graphql-import": "^0.7.1",
"graphql-tag": "^2.10.1",

View File

@ -21,7 +21,6 @@ interface UploadImageOptions {
interface FileMeta {
filename: string
mimetype: string
encoding: string
}
// General pattern for filename:
@ -38,22 +37,36 @@ export default class ImageBucketService {
this.gcsBucket = new Storage().bucket(this.options.gcsBucketName)
// quick fail in case of bad credentials
this.gcsBucket.getFiles()
this.gcsBucket.getFiles().catch(err => {
console.error("Error accessing bucket", err)
})
}
saveLocally(stream: ReadableStream, fileMeta: FileMeta): string {
const splitFileName = fileMeta.filename.split(".")
addRandomHashToFilename = (filename: string) => {
const splitFileName = filename.split(".")
const randomSequence = randomstring({ length: 6 })
splitFileName.reverse()
splitFileName.splice(1, 0, randomstring(6))
splitFileName.splice(1, 0, randomSequence)
splitFileName.reverse()
const targetFileName = splitFileName.join(".")
}
async saveLocally(
stream: ReadableStream,
fileMeta: FileMeta
): Promise<string> {
const targetFileName = this.addRandomHashToFilename(fileMeta.filename)
const targetFullPath = `${this.options.tmpLocalPath}/${targetFileName}`
fs.writeFileSync(targetFullPath, stream, {
encoding: fileMeta.encoding,
console.log('::STREAM::',typeof stream, JSON.stringify(stream))
return new Promise((resolve, reject) => {
fs.writeFile(targetFullPath, stream, err => {
if (err) {
console.error("error writing file", err)
reject(err)
}
resolve(targetFullPath)
})
})
return targetFullPath
}
async deleteLocally(path: string) {
@ -61,31 +74,40 @@ export default class ImageBucketService {
}
async uploadToBucket(file: FileUpload, options: UploadImageOptions) {
const localTempPath = this.saveLocally(file.createReadStream(), {
encoding: file.encoding,
const localTempPath = await this.saveLocally(file.createReadStream(), {
filename: file.filename,
mimetype: file.mimetype,
})
console.log("local save call terminated", localTempPath)
const prefix = `e/${options.eventId}/${options.imageType}_`
const [existingFiles] = await this.gcsBucket.getFiles({
prefix,
})
const highest: number = existingFiles
.map(file => parseInt(file.name.split(prefix)[1]))
.sort()
.reverse()[0]
console.log("existing filenames", existingFiles.map(file => file.name))
const existingIds: number[] = existingFiles.map(file =>
parseInt(file.name.split(prefix)[1])
)
const highest = Math.max(0, ...existingIds)
const next = `${highest}`.padStart(4, "0")
const bucketDestination = `${prefix}${next}`
const [bucketFile, _]: [
File,
Metadata
] = await this.gcsBucket.upload(localTempPath, {
destination: bucketDestination,
})
console.log(
"will bucket upload from",
localTempPath,
"to",
bucketDestination
)
const [bucketFile]: [File, Metadata] = await this.gcsBucket.upload(
localTempPath,
{
destination: bucketDestination,
}
)
this.deleteLocally(localTempPath) // don't care about result
// this.deleteLocally(localTempPath) // don't care about result
return `${this.options.buckerPublicURLBase}/${bucketFile.name}`
}
}

View File

@ -5,7 +5,10 @@ import { User } from "../Auth/User.entity";
import { FileUpload } from "graphql-upload";
import { EventImage, ImageType } from "./EventImage.entity";
const canChangeEvent = async (event: Event, user: User) => {
const canChangeEvent = async (event: Event, user?: User) => {
if (!user) {
return false
}
const roles = (await user.roles).map(role => role.type)
if (roles.includes("admin") || roles.includes("embassador")) {
return true
@ -24,6 +27,7 @@ export const EventImageResolvers = (
context: Context,
info
) => {
console.log('Mutation resolver: setEventCoverImage')
const event = await Event.findOne(req.input.id)
if (!event) {
throw new Error(`Event with id ${req.input.id} not found`)
@ -34,15 +38,15 @@ export const EventImageResolvers = (
)
}
console.log('will get upload form input')
const fileUpload: FileUpload = await req.input.file
fileUpload.filename
console.log('got a file upload', JSON.stringify(fileUpload))
const coverImageURL = await imageBucketService.uploadToBucket(
fileUpload,
{
fileMeta: {
filename: fileUpload.filename,
mimetype: fileUpload.mimetype,
encoding: fileUpload.encoding,
},
imageType: ImageType.Cover,
eventId: req.input.id,

View File

@ -1,17 +1,18 @@
import {ApolloServer} from 'apollo-server-express'
import {makeExecutableSchema} from "graphql-tools"
import EventsResolvers from './Events/eventsResolvers'
import UserResolvers from './Auth/userResolvers'
import {importSchema} from 'graphql-import'
import { ApolloServer } from "apollo-server-express"
import { makeExecutableSchema } from "graphql-tools"
import EventsResolvers from "./Events/eventsResolvers"
import UserResolvers from "./Auth/userResolvers"
import { importSchema } from "graphql-import"
import AuthResolvers from "./Auth/authResolvers"
import {Connection} from "typeorm"
import {PostOffice} from "./post_office"
import {Session} from "./Auth/Session.entity"
import {Context} from "./@types/graphql-utils"
import { Connection } from "typeorm"
import { PostOffice } from "./post_office"
import { Session } from "./Auth/Session.entity"
import { Context } from "./@types/graphql-utils"
import SessionManager from "./Auth/SessionManager"
import { tagResolvers } from "./Calendar/tagsResolvers";
import ImageBucketService from "./Image/ImageBucketService";
import { EventImageResolvers } from "./Image/eventImageResolvers";
import { tagResolvers } from "./Calendar/tagsResolvers"
import ImageBucketService from "./Image/ImageBucketService"
import { EventImageResolvers } from "./Image/eventImageResolvers"
import { GraphQLUpload } from "graphql-upload"
interface Dependencies {
typeormConnection: Connection
@ -24,35 +25,44 @@ interface Dependencies {
const resolvers = {
Query: {},
Mutation: {}
Mutation: {},
}
export const createServer = async (dependencies: Dependencies) => {
export const createServer = async (
dependencies: Dependencies
): Promise<ApolloServer> => {
const authResolvers = new AuthResolvers({
sendEmail: dependencies.sendEmail,
emailTargetDomain: dependencies.domain,
sessionManager: dependencies.sessionManager
sessionManager: dependencies.sessionManager,
})
const eventImageResolvers = EventImageResolvers(dependencies.imageBucketService)
const typeDefs = importSchema(__dirname + '/schema.graphql')
const { Query: EventQueryResolvers, Mutation: EventResolversMutation,...eventResolvers} = EventsResolvers
const { Query: UserQueryResolvers, Mutation: UserMutationResolvers, ...userResolvers} = UserResolvers
const eventImageResolvers = EventImageResolvers(
dependencies.imageBucketService
)
const typeDefs = importSchema(__dirname + "/schema.graphql")
const {
Query: EventQueryResolvers,
Mutation: EventResolversMutation,
...eventResolvers
} = EventsResolvers
const {
Query: UserQueryResolvers,
Mutation: UserMutationResolvers,
...userResolvers
} = UserResolvers
const schema = makeExecutableSchema({
typeDefs: [
typeDefs
],
typeDefs: [typeDefs],
resolvers: {
Upload: GraphQLUpload,
Query: {
...resolvers.Query,
...EventQueryResolvers,
...authResolvers.Query,
...UserQueryResolvers,
...UserQueryResolvers,
...tagResolvers.Query,
...eventImageResolvers.Query
...eventImageResolvers.Query,
},
Mutation: {
...resolvers.Mutation,
@ -60,13 +70,13 @@ export const createServer = async (dependencies: Dependencies) => {
...authResolvers.Mutation,
...UserMutationResolvers,
...tagResolvers.Mutation,
...eventImageResolvers.Mutation
...eventImageResolvers.Mutation,
},
UserSession: {
...authResolvers.UserSession
...authResolvers.UserSession,
},
EventTag: {
...tagResolvers.EventTag
...tagResolvers.EventTag,
},
...eventResolvers,
...userResolvers,
@ -75,30 +85,38 @@ export const createServer = async (dependencies: Dependencies) => {
return new ApolloServer({
schema,
context: dependencies.customContext ? dependencies.customContext : async (a) => {
const ctx: Context = {
req: a.req
}
let authToken
if (a.req.header('authentication')) {
authToken = a.req.header('authentication')
}
if (a.req.headers.cookie) {
console.log('a.req.headers.cookie', a.req.headers.cookie)
const cookies: any = a.req.headers.cookie.split(';').map(s => s.trim().split('=')).reduce((acc, val)=> {acc[val[0]] = val[1]; return acc},{})
context: dependencies.customContext
? dependencies.customContext
: async a => {
const ctx: Context = {
req: a.req,
}
let authToken
if (a.req.header("authentication")) {
authToken = a.req.header("authentication")
}
if (a.req.headers.cookie) {
console.log("a.req.headers.cookie", a.req.headers.cookie)
const cookies: any = a.req.headers.cookie
.split(";")
.map(s => s.trim().split("="))
.reduce((acc, val) => {
acc[val[0]] = val[1]
return acc
}, {})
if (cookies.authentication) {
authToken = cookies.authentication as string
}
}
if (authToken) {
const session = await Session.findOne({hash: authToken})
if (session) {
ctx.user = session.user
}
}
if (cookies.authentication) {
authToken = cookies.authentication as string
}
}
if (authToken) {
const session = await Session.findOne({ hash: authToken })
if (session) {
ctx.user = session.user
}
}
return ctx
}
return ctx
},
})
}

View File

@ -1,21 +1,21 @@
require('dotenv').config()
require("dotenv").config()
import { createServer } from "./graphql"
import typeormConfig from './ormconfig'
import {createConnection} from "typeorm"
import { sendEmail} from "./post_office"
import * as config from './config'
import express from 'express'
import typeormConfig from "./ormconfig"
import { createConnection } from "typeorm"
import { sendEmail } from "./post_office"
import * as config from "./config"
import express from "express"
import authHttpHandlers from "./Auth/authHttpHandlers"
import SessionManager from "./Auth/SessionManager"
import morgan from 'morgan'
import ImageBucketService from "./Image/ImageBucketService";
import morgan from "morgan"
import ImageBucketService from "./Image/ImageBucketService"
const start = async () => {
const sessionManager = new SessionManager({
sendEmail: sendEmail,
emailTargetDomain: config.domain,
emailSenderDomain: config.mailgun.domain
emailSenderDomain: config.mailgun.domain,
})
const imageBucketService = new ImageBucketService({
@ -23,32 +23,39 @@ const start = async () => {
tmpLocalPath: config.eventImage.imageTempLocalPath,
gcsBucketName: config.eventImage.imageCGSBucketName,
})
console.log(`Starting with db: ${typeormConfig.database} and config:\n ${JSON.stringify(typeormConfig,null,'\t')}`)
console.log(
`Starting with db: ${typeormConfig.database} and config:\n ${JSON.stringify(
typeormConfig,
null,
"\t"
)}`
)
const connection = await createConnection(typeormConfig)
console.log('Will look for migrations and run them')
console.log("Will look for migrations and run them")
await connection.runMigrations()
console.log('No more migrations to run')
console.log("No more migrations to run")
const server = await createServer({
typeormConnection: connection,
sendEmail,
domain: config.domain,
sessionManager,
imageBucketService
imageBucketService,
})
const authHandlers = authHttpHandlers(sessionManager)
const app = express()
app.use(express.json())
app.use(morgan('combined'))
app.use(morgan("combined"))
app.post("/api/signup", authHandlers.signupHandler)
app.post("/api/login", authHandlers.loginHandler)
app.post("/api/init-session", authHandlers.initializeSessionHandler)
app.get("/api/signout", authHandlers.signoutHandler)
app.post('/api/signup', authHandlers.signupHandler)
app.post('/api/login', authHandlers.loginHandler)
app.post('/api/init-session', authHandlers.initializeSessionHandler)
app.get('/api/signout', authHandlers.signoutHandler)
server.applyMiddleware({ app })
app.listen({port: 4000}, () => {
app.listen({ port: 4000 }, () => {
console.log(`🚀 Server ready at localhost:4000/${server.graphqlPath}`)
})

View File

@ -15,6 +15,7 @@ type UserRole {
scalar Date
scalar RoleType
scalar Translations
scalar Upload
type UserSession {
hash: String!
@ -41,6 +42,16 @@ input RequestInviteInput {
# Events
# ------
type Image {
url: String!
}
type EventImages {
thumb: Image
cover: Image
poster: Image
gallery: [Image!]
}
type CalendarEvent {
id: ID!
@ -52,6 +63,7 @@ type CalendarEvent {
location: Location!
occurrences: [EventOccurrence]
tags: [EventTag]
images: EventImages
}
type EventTag {
@ -196,7 +208,6 @@ input DeleteEventTagInput {
id: ID!
}
scalar Upload
input EventImageUploadInput {
id: ID!
file: Upload!

View File

@ -2343,6 +2343,11 @@ fs-capacitor@^2.0.4:
resolved "https://registry.yarnpkg.com/fs-capacitor/-/fs-capacitor-2.0.4.tgz#5a22e72d40ae5078b4fe64fe4d08c0d3fc88ad3c"
integrity sha512-8S4f4WsCryNw2mJJchi46YgB6CR5Ze+4L1h8ewl9tEpL4SJ3ZO+c/bS4BWhB8bK+O3TMqhuZarTitd0S0eh2pA==
fs-capacitor@^5.0.0:
version "5.0.0"
resolved "https://registry.yarnpkg.com/fs-capacitor/-/fs-capacitor-5.0.0.tgz#0d98d1278d5dd3247411f51ccd2a0bdcfe72320a"
integrity sha512-M1CNCxyeL81Tyr6qpxBgRJ3GEHKrzFdpen7ttoDd9KtRbf74myW7Kb39QHbNzo9rf/2CNJlEUbAKnfi7N40Pdg==
fs-minipass@^1.2.5:
version "1.2.6"
resolved "https://registry.yarnpkg.com/fs-minipass/-/fs-minipass-1.2.6.tgz#2c5cc30ded81282bfe8a0d7c7c1853ddeb102c07"