diff --git a/api/package.json b/api/package.json index 1241d8a1..41f861fc 100644 --- a/api/package.json +++ b/api/package.json @@ -21,12 +21,13 @@ "body-parser": "^1.20.2", "cookie-parser": "^1.4.6", "cors": "^2.8.5", - "document-drive": "1.0.0-alpha.24", + "document-drive": "1.0.0-alpha.25", "document-model": "^1.0.35", "document-model-libs": "^1.17.1", "dotenv": "^16.4.5", "ethers": "^5.7.2", "express": "^4.19.2", + "express-async-errors": "^3.1.1", "graphql": "^16.8.1", "graphql-middleware": "^6.1.35", "graphql-playground-html": "^1.6.30", diff --git a/api/pnpm-lock.yaml b/api/pnpm-lock.yaml index cee00c4c..6976b9dc 100644 --- a/api/pnpm-lock.yaml +++ b/api/pnpm-lock.yaml @@ -24,8 +24,8 @@ dependencies: specifier: ^2.8.5 version: 2.8.5 document-drive: - specifier: 1.0.0-alpha.24 - version: 1.0.0-alpha.24(@prisma/client@5.11.0)(document-model-libs@1.17.1)(document-model@1.0.35)(localforage@1.10.0)(sequelize@6.37.2)(sqlite3@5.1.7) + specifier: 1.0.0-alpha.25 + version: 1.0.0-alpha.25(@prisma/client@5.11.0)(document-model-libs@1.17.1)(document-model@1.0.35)(localforage@1.10.0)(sequelize@6.37.2)(sqlite3@5.1.7) document-model: specifier: ^1.0.35 version: 1.0.35 @@ -41,6 +41,9 @@ dependencies: express: specifier: ^4.19.2 version: 4.19.2 + express-async-errors: + specifier: ^3.1.1 + version: 3.1.1(express@4.19.2) graphql: specifier: ^16.8.1 version: 16.8.1 @@ -4701,8 +4704,8 @@ packages: esutils: 2.0.3 dev: true - /document-drive@1.0.0-alpha.24(@prisma/client@5.11.0)(document-model-libs@1.17.1)(document-model@1.0.35)(localforage@1.10.0)(sequelize@6.37.2)(sqlite3@5.1.7): - resolution: {integrity: sha512-wYE3+iax4a5WC5neFla/MQ8iBUvMP0grQh3OuOt07kkq4CDrQY5+3fZlIZ5Azq3voC3cF1+XC2XnDpA/vZPqjg==} + /document-drive@1.0.0-alpha.25(@prisma/client@5.11.0)(document-model-libs@1.17.1)(document-model@1.0.35)(localforage@1.10.0)(sequelize@6.37.2)(sqlite3@5.1.7): + resolution: {integrity: sha512-tM5an9gvPdEYXWU7EVFDgaiBwcNCZxx88uPqwztrCVXijUTG+1qFOWhg7i6MHMsK6+r22ifp6FfvLHNeK1B8Iw==} peerDependencies: '@prisma/client': 5.11.0 document-model: ^1.0.35 @@ -5286,6 +5289,14 @@ packages: engines: {node: '>=6'} dev: false + /express-async-errors@3.1.1(express@4.19.2): + resolution: {integrity: sha512-h6aK1da4tpqWSbyCa3FxB/V6Ehd4EEB15zyQq9qe75OZBp0krinNKuH4rAY+S/U/2I36vdLAUFSjQJ+TFmODng==} + peerDependencies: + express: ^4.16.2 + dependencies: + express: 4.19.2 + dev: false + /express@4.19.2: resolution: {integrity: sha512-5T6nhjsT+EOMzuck8JjBHARTHfMht0POzlA60WV2pMD3gyXw2LZnZ+ueGdNxG+0calOJcWKbpFcuzLZ91YWq9Q==} engines: {node: '>= 0.10.0'} diff --git a/api/src/errors/BadRequestError.ts b/api/src/errors/BadRequestError.ts new file mode 100644 index 00000000..30cbe763 --- /dev/null +++ b/api/src/errors/BadRequestError.ts @@ -0,0 +1,32 @@ +import { CustomError } from "./CustomError"; + +export default class BadRequestError extends CustomError { + private static readonly _statusCode = 400; + private readonly _code: number; + private readonly _logging: boolean; + private readonly _context: { [key: string]: any }; + + constructor(params?: {code?: number, message?: string, logging?: boolean, context?: { [key: string]: any }}) { + const { code, message, logging } = params || {}; + + super(message || "Bad request"); + this._code = code || BadRequestError._statusCode; + this._logging = logging || false; + this._context = params?.context || {}; + + // Only because we are extending a built in class + Object.setPrototypeOf(this, BadRequestError.prototype); + } + + get errors() { + return [{ message: this.message, context: this._context }]; + } + + get statusCode() { + return this._code; + } + + get logging() { + return this._logging; + } +} diff --git a/api/src/errors/CustomError.ts b/api/src/errors/CustomError.ts new file mode 100644 index 00000000..06cc00f3 --- /dev/null +++ b/api/src/errors/CustomError.ts @@ -0,0 +1,17 @@ +export type CustomErrorContent = { + message: string, + context?: { [key: string]: any } +}; + +export abstract class CustomError extends Error { + abstract readonly statusCode: number; + abstract readonly errors: CustomErrorContent[]; + abstract readonly logging: boolean; + + constructor(message: string) { + super(message); + + // Only because we are extending a built in class + Object.setPrototypeOf(this, CustomError.prototype); + } +} diff --git a/api/src/errors/DocumentDriveError.ts b/api/src/errors/DocumentDriveError.ts new file mode 100644 index 00000000..cc15e9d2 --- /dev/null +++ b/api/src/errors/DocumentDriveError.ts @@ -0,0 +1,32 @@ +import { CustomError } from "./CustomError"; + +export default class DocumentDriveError extends CustomError { + private static readonly _statusCode = 400; + private readonly _code: number; + private readonly _logging: boolean; + private readonly _context: { [key: string]: any }; + + constructor(params?: { code?: number, message?: string, logging?: boolean, context?: { [key: string]: any } }) { + const { code, message, logging } = params || {}; + + super(message || "Bad request"); + this._code = code || DocumentDriveError._statusCode; + this._logging = logging || false; + this._context = params?.context || {}; + + // Only because we are extending a built in class + Object.setPrototypeOf(this, DocumentDriveError.prototype); + } + + get errors() { + return [{ message: this.message, context: this._context }]; + } + + get statusCode() { + return this._code; + } + + get logging() { + return this._logging; + } +} diff --git a/api/src/graphql/server/index.ts b/api/src/graphql/server/index.ts index fa16efab..03766134 100644 --- a/api/src/graphql/server/index.ts +++ b/api/src/graphql/server/index.ts @@ -9,9 +9,11 @@ import cors from 'cors'; import { PORT } from '../../env'; import { schemaWithMiddleware as indexSchema } from './index/schema'; import { schemaWithMiddleware as driveSchema } from './drive/schema'; -import { Context as IndexContext, createContext as createIndexContext } from './index/context'; +import { Context, Context as IndexContext, createContext as createIndexContext } from './index/context'; import { Context as DriveContext, createContext as createDriveContext } from './drive/context'; import { getChildLogger } from '../../logger'; +import "express-async-errors"; +import { errorHandler } from '../../middleware/errors'; const logger = getChildLogger({ msgPrefix: 'SERVER' }); @@ -72,6 +74,7 @@ export const startServer = async ( }), ); + app.use(errorHandler); return httpServer.listen({ port: PORT }, () => { logger.info(`Running on ${PORT}`); }); diff --git a/api/src/middleware/errors.ts b/api/src/middleware/errors.ts new file mode 100644 index 00000000..fb483e22 --- /dev/null +++ b/api/src/middleware/errors.ts @@ -0,0 +1,15 @@ +import { NextFunction, Request, Response } from "express"; +import { getChildLogger } from "../logger"; +import { CustomError } from "../errors/CustomError"; + +const logger = getChildLogger({ msgPrefix: 'Generic Error Handler', }); + +export const errorHandler = (err: Error, req: Request, res: Response, next: NextFunction) => { + err.cause = err.cause || 'Unknown'; + logger.error({ + msg: err.message, + }); + + console.log("TEST TEST TEST") + res.status(500).send({ errors: err.message }); +}; diff --git a/api/src/modules/document-drive/drive-resolver.ts b/api/src/modules/document-drive/drive-resolver.ts index 46b3ab96..717532c2 100644 --- a/api/src/modules/document-drive/drive-resolver.ts +++ b/api/src/modules/document-drive/drive-resolver.ts @@ -17,6 +17,7 @@ import stringify from 'json-stringify-deterministic'; import { getChildLogger } from '../../logger'; import { Context } from '../../graphql/server/drive/context'; import { DocumentDriveAction } from 'document-model-libs/document-drive'; +import DocumentDriveError from '../../errors/DocumentDriveError'; const logger = getChildLogger({ msgPrefix: 'Drive Resolver' }); @@ -234,9 +235,8 @@ export const getDrive = queryField('drive', { try { const drive = await ctx.prisma.document.getDrive(ctx.driveId ?? '1'); return drive; - } catch (e) { - logger.error(e); - return null; + } catch (e: any) { + throw new DocumentDriveError({ code: 500, message: e.message ?? "Failed to get drive", logging: true, context: e }) } }, }); @@ -247,17 +247,23 @@ export const registerListener = mutationField('registerPullResponderListener', { filter: nonNull(InputListenerFilter), }, resolve: async (_parent, { filter }, ctx: Context) => { - const result = await ctx.prisma.document.registerPullResponderListener( - ctx.driveId ?? '1', - { + try { + const result = await ctx.prisma.document.registerPullResponderListener( + ctx.driveId ?? '1', + { branch: filter.branch?.filter(b => !!b) as string[] ?? [], documentId: filter.documentId?.filter(b => !!b) as string[] ?? [], documentType: filter.documentType?.filter(b => !!b) as string[] ?? [], scope: filter.scope?.filter(b => !!b) as string[] ?? [], - }, - ); + }, + ); + + return result; + + } catch (e: any) { + throw new DocumentDriveError({ code: 500, message: e.message ?? "Failed to register listener", logging: true, context: e }) + } - return result; }, }); @@ -268,12 +274,16 @@ export const deleteListener = mutationField('deletePullResponderListener', { filter: nonNull(InputListenerFilter), }, resolve: async (_parent, { filter }, ctx: Context) => { - const result = await ctx.prisma.document.deletePullResponderListener( - ctx.driveId ?? '1', - filter, - ); + try { + const result = await ctx.prisma.document.deletePullResponderListener( + ctx.driveId ?? '1', + filter, + ); - return result; + return result; + } catch (e: any) { + throw new DocumentDriveError({ code: 500, message: e.message ?? "Failed to delete listener", logging: true, context: e }) + } }, }); @@ -286,36 +296,42 @@ export const pushUpdates = mutationField('pushUpdates', { logger.info('pushUpdates') if (!strands || strands?.length === 0) return []; - const listenerRevisions: IListenerRevision[] = await Promise.all(strands.map(async (s) => { - const operations = s.operations?.map((o) => ({ - ...o, - input: JSON.parse(o.input), - skip: o.skip ?? 0, - scope: s.scope as OperationScope, - branch: 'main', - scopes: ['global', 'local'], - })) ?? []; - - const result = await ctx.prisma.document.pushUpdates( - s.driveId, - operations as Operation[], - s.documentId ?? undefined, - ); - - if (result.status !== "SUCCESS") logger.error(result.error); - - const revision = result.document?.operations[s.scope as OperationScope].slice().pop()?.index ?? -1; - return { - revision, - branch: s.branch, - documentId: s.documentId ?? '', - driveId: s.driveId, - scope: s.scope as OperationScope, - status: result.status, - }; - })); - - return listenerRevisions; + try { + const listenerRevisions: IListenerRevision[] = await Promise.all(strands.map(async (s) => { + const operations = s.operations?.map((o) => ({ + ...o, + input: JSON.parse(o.input), + skip: o.skip ?? 0, + scope: s.scope as OperationScope, + branch: 'main', + scopes: ['global', 'local'], + })) ?? []; + + const result = await ctx.prisma.document.pushUpdates( + s.driveId, + operations as Operation[], + s.documentId ?? undefined, + ); + + if (result.status !== "SUCCESS") logger.error(result.error); + + const revision = result.document?.operations[s.scope as OperationScope].slice().pop()?.index ?? -1; + return { + revision, + branch: s.branch, + documentId: s.documentId ?? '', + driveId: s.driveId, + scope: s.scope as OperationScope, + status: result.status, + }; + + })); + + + return listenerRevisions; + } catch (e: any) { + throw new DocumentDriveError({ code: 500, message: e.message ?? "Failed to push updates", logging: true, context: e }) + } }, }); @@ -346,9 +362,8 @@ export const acknowledge = mutationField('acknowledge', { ); return result; - } catch (e) { - logger.error(e) - return false; + } catch (e: any) { + throw new DocumentDriveError({ code: 500, message: e.message ?? "Failed to acknowledge", logging: true, context: e }) } }, }); diff --git a/api/src/modules/document-drive/drives-resolver.ts b/api/src/modules/document-drive/drives-resolver.ts index 49da5252..996d0033 100644 --- a/api/src/modules/document-drive/drives-resolver.ts +++ b/api/src/modules/document-drive/drives-resolver.ts @@ -9,6 +9,7 @@ import { import { DocumentDriveState } from './drive-resolver'; import { Context } from '../../graphql/server/drive/context'; import logger from '../../logger'; +import DocumentDriveError from '../../errors/DocumentDriveError'; export const DocumentDriveLocalState = objectType({ name: 'DocumentDriveLocalState', @@ -68,11 +69,15 @@ export const addDrive = mutationField('addDrive', { local: nonNull(DocumentDriveLocalStateInput), }, resolve: async (_parent, { global, local }, ctx: Context) => { - const drive = await ctx.prisma.document.addDrive({ - global: { id: global.id, name: global.name, icon: global.icon ?? null, slug: global.slug ?? null }, - local: { availableOffline: local.availableOffline, sharingType: local.sharingType ?? null, listeners: [], triggers: [] }, - }); - return drive.state; + try { + const drive = await ctx.prisma.document.addDrive({ + global: { id: global.id, name: global.name, icon: global.icon ?? null, slug: global.slug ?? null }, + local: { availableOffline: local.availableOffline, sharingType: local.sharingType ?? null, listeners: [], triggers: [] }, + }); + return drive.state; + } catch (e: any) { + throw new DocumentDriveError({ code: 500, message: e.message ?? "Failed to add drive", logging: true, context: e }) + } }, }); @@ -84,9 +89,8 @@ export const deleteDrive = mutationField('deleteDrive', { resolve: async (_parent, { id }, ctx: Context) => { try { await ctx.prisma.document.deleteDrive(id); - } catch (e) { - logger.error(e); - return false; + } catch (e: any) { + throw new DocumentDriveError({ code: 500, message: e.message ?? "Failed to delete drive", logging: true, context: e }) } return true; diff --git a/api/src/modules/document/model.ts b/api/src/modules/document/model.ts index 9cdfc404..2c2a40d7 100644 --- a/api/src/modules/document/model.ts +++ b/api/src/modules/document/model.ts @@ -45,8 +45,12 @@ export function getDocumentDriveCRUD(prisma: Prisma.TransactionClient) { ); async function initialize() { - await driveServer.initialize(); - await init(driveServer, prisma); + try { + await driveServer.initialize(); + await init(driveServer, prisma); + } catch (e) { + console.log(e); + } } function clearDriveCache() {