@prisma-cms/upload-module
Version:
Upload module for @prisma-cms/server
562 lines (344 loc) • 9.35 kB
JavaScript
import fs from "fs";
// import chalk from "chalk";
import PrismaModule from "@prisma-cms/prisma-module";
import PrismaProcessor from "@prisma-cms/prisma-processor";
import MergeSchema from 'merge-graphql-schemas';
import mkdirp from "mkdirp";
import shortid from "shortid";
import path from 'path';
const moduleURL = new URL(import.meta.url);
const __dirname = path.dirname(moduleURL.pathname);
const { createWriteStream, unlinkSync } = fs;
const { fileLoader, mergeTypes } = MergeSchema
export class FileProcessor extends PrismaProcessor {
constructor(props) {
super(props);
this.objectType = "File";
this.private = true;
this.ownable = true;
}
// async create(method, args, info) {
// if (args.data) {
// let {
// ...data
// } = args.data;
// args.data = data;
// }
// return super.create(method, args, info);
// }
async update(method, args, info) {
if (args.data) {
let {
/**
* Пока логика не проработана, эти поля не позволяем менять
*/
path,
filename,
encoding,
hash,
size,
...data
} = args.data;
args.data = data;
}
return super.update(method, args, info);
}
async mutate(method, args, info) {
// if (args.data) {
// let {
// ...data
// } = args.data;
// args.data = data;
// }
return super.mutate(method, args);
}
// async delete(method, args, info) {
// return super.delete(method, args);
// }
}
class PrismaUploadModule extends PrismaModule {
constructor(props = {}) {
const {
uploadDir = "uploads",
} = props;
super(props);
const {
authRequired = true,
} = props;
Object.assign(this, {
uploadDir,
authRequired,
Query: {
file: (parent, args, ctx, info) => this.file(parent, args, ctx, info),
files: (parent, args, ctx, info) => this.files(parent, args, ctx, info),
filesConnection: (parent, args, ctx, info) => this.filesConnection(parent, args, ctx, info),
},
Mutation: {
singleUpload: this.singleUpload.bind(this),
multipleUpload: this.multipleUpload.bind(this),
createFileProcessor: (source, args, ctx, info) => {
return this.getProcessor(ctx).createWithResponse("File", args, info);
},
updateFileProcessor: (source, args, ctx, info) => {
return this.getProcessor(ctx).updateWithResponse("File", args, info);
},
deleteFile: (source, args, ctx, info) => {
return this.getProcessor(ctx).delete("File", args, info);
},
},
});
}
getSchema(types = []) {
let schema = fileLoader(__dirname + '/schema/database/', {
recursive: true,
});
if (schema) {
types = types.concat(schema);
}
let typesArray = super.getSchema(types);
return typesArray;
}
getApiSchema(types = []) {
let baseSchema = [];
let schemaFile = __dirname + "/../schema/generated/prisma.graphql";
if (fs.existsSync(schemaFile)) {
baseSchema = fs.readFileSync(schemaFile, "utf-8");
}
let apiSchema = super.getApiSchema(types.concat(baseSchema), []);
let schema = fileLoader(__dirname + '/schema/api/', {
recursive: true,
});
apiSchema = mergeTypes([apiSchema.concat(schema)], { all: true });
return apiSchema;
}
getProcessor(ctx) {
return new (this.getProcessorClass())(ctx);
}
getProcessorClass() {
return FileProcessor;
}
getResolvers() {
const resolvers = super.getResolvers();
Object.assign(resolvers.Query, this.Query);
Object.assign(resolvers.Mutation, this.Mutation);
Object.assign(resolvers, {
FileResponse: {
data: (source, args, ctx, info) => {
const {
id,
} = source.data || {};
return id ? ctx.db.query.file({
where: {
id,
},
}, info) : null;
},
},
});
return resolvers;
}
file(source, args, ctx, info) {
return ctx.db.query.file({}, info);
}
files(source, args, ctx, info) {
return ctx.db.query.files({}, info);
}
filesConnection(source, args, ctx, info) {
return ctx.db.query.filesConnection({}, info);
}
async singleUpload(parent, args, ctx, info) {
// const {
// file: upload,
// } = args;
return this.processUpload(parent, args, ctx, info);
}
async multipleUpload(parent, args, ctx, info) {
const {
files,
} = args;
let { resolve, reject } = await this.uploadAll(files.map(upload => {
return this.processUpload(parent, {
data: {
file: upload,
},
}, ctx, info);
}));
if (reject.length) {
reject.forEach(({ name, message }) =>
// eslint-disable-next-line no-console
console.error(`${name}: ${message}`)
)
}
resolve = (resolve && resolve
.filter(n => n)
) || null;
return resolve;
}
async storeFS({
stream,
filename,
directory,
}) {
const {
uploadDir: baseDir,
} = this;
const baseDirAbsolute = path.resolve(baseDir);
// console.log("baseDirAbsolute", baseDirAbsolute);
let uploadDir = path.join(baseDir, directory || "");
// console.log("uploadDir", uploadDir);
mkdirp.sync(uploadDir);
// await mkdirp(uploadDir);
const id = shortid.generate()
// const file = `${uploadDir}/${id}-${filename}`;
const file = path.join(uploadDir, `${id}-${filename}`);
// console.log("baseDir", baseDir);
// console.log("file path", file);
let resolved = path.resolve(file);
const normalized = path.normalize(resolved);
// console.log("file path.resolve()", resolved);
// console.log("file normalized", normalized);
// console.log("file .relative()", path.relative(baseDir, normalized));
if (!normalized.startsWith(baseDirAbsolute)) {
throw new Error("Wrong directory");
}
// return;
return new Promise((resolve, reject) => {
try {
const pipe = createWriteStream(file);
pipe.on('error', function (err) {
console.error(err);
reject(err);
});
stream
.on('error', error => {
if (stream.truncated) {
// Delete the truncated file
try {
unlinkSync(file)
}
catch (error) {
console.error(error);
reject();
}
}
reject(error)
})
.on('end', () => resolve({
id,
path: file,
}))
.pipe(pipe)
}
catch (error) {
console.error(error);
}
})
}
async processUpload(parent, args, ctx, info) {
// console.log("processUpload args", JSON.stringify(args, true, 2));
let {
file: upload,
data,
} = args;
let {
file,
directory,
...other
} = data || {};
upload = file ? file : upload;
if (!upload) {
throw new Error("Can not get file");
}
let uploaded = {};
this.assignUser(uploaded, ctx);
const {
stream,
filename,
mimetype,
encoding,
} = await upload;
const writeResult = await this.storeFS({
stream,
filename,
directory,
})
// .
// catch(error => {
// console.error("error", error);
// })
const { path } = writeResult;
if (path) {
const stats = fs.statSync(path);
const {
size,
} = stats;
Object.assign(uploaded, {
...other,
filename,
mimetype,
encoding,
path: path.replace(/^\.\//, ''),
size,
});
return ctx.db.mutation.createFile({
data: uploaded,
}, info)
.catch(error => {
throw error;
});
}
else {
throw new Error(`Can not upload file ${filename}`);
}
}
assignUser(file, ctx) {
const {
currentUser,
} = ctx;
const {
id: userId,
} = currentUser || {};
if (this.authRequired && !userId) {
throw new Error("Authorization required")
}
if (userId) {
Object.assign(file, {
CreatedBy: {
connect: {
id: userId,
}
}
});
}
return file;
}
uploadAll(tasks) {
return new Promise(async (resolve, reject) => {
let result = {
resolve: [],
reject: [],
}
let processor = this.processUploadGenerator(tasks);
for await (const n of processor) {
if (n && n instanceof Error) {
result.reject.push(n);
}
else {
result.resolve.push(n);
}
}
resolve(result);
});
}
async * processUploadGenerator(tasks) {
while (tasks && tasks.length) {
const task = tasks.splice(0, 1)[0];
const result = await task
.catch(error => {
return error;
});
yield result;
}
}
}
export default PrismaUploadModule;