diff --git a/packages/server/src/app.ts b/packages/server/src/app.ts index 3ff9c3e34..b5e970385 100644 --- a/packages/server/src/app.ts +++ b/packages/server/src/app.ts @@ -202,16 +202,16 @@ async function main() { delete connectionCheckLogInfo.connection; appLogger().info('Connection check:', connectionCheckLogInfo); - const appContext = app.context as AppContext; + const ctx = app.context as AppContext; - await setupAppContext(appContext, env, connectionCheck.connection, appLogger); - await initializeJoplinUtils(config(), appContext.models, appContext.services.mustache); + await setupAppContext(ctx, env, connectionCheck.connection, appLogger); + await initializeJoplinUtils(config(), ctx.models, ctx.joplin.services.mustache); appLogger().info('Migrating database...'); - await migrateDb(appContext.db); + await migrateDb(ctx.db); appLogger().info('Starting services...'); - await startServices(appContext); + await startServices(ctx); appLogger().info(`Call this for testing: \`curl ${config().apiBaseUrl}/api/ping\``); diff --git a/packages/server/src/middleware/notificationHandler.test.ts b/packages/server/src/middleware/notificationHandler.test.ts index 0e9773f6f..09c165556 100644 --- a/packages/server/src/middleware/notificationHandler.test.ts +++ b/packages/server/src/middleware/notificationHandler.test.ts @@ -26,15 +26,15 @@ describe('notificationHandler', function() { }); { - const context = await koaAppContext({ sessionId: session.id }); - await notificationHandler(context, koaNext); + const ctx = await koaAppContext({ sessionId: session.id }); + await notificationHandler(ctx, koaNext); const notifications: Notification[] = await models().notification().all(); expect(notifications.length).toBe(1); expect(notifications[0].key).toBe('change_admin_password'); expect(notifications[0].read).toBe(0); - expect(context.notifications.length).toBe(1); + expect(ctx.joplin.notifications.length).toBe(1); } { @@ -43,15 +43,15 @@ describe('notificationHandler', function() { password: 'changed!', }); - const context = await koaAppContext({ sessionId: session.id }); - await notificationHandler(context, koaNext); + const ctx = await koaAppContext({ sessionId: session.id }); + await notificationHandler(ctx, koaNext); const notifications: Notification[] = await models().notification().all(); expect(notifications.length).toBe(1); expect(notifications[0].key).toBe('change_admin_password'); expect(notifications[0].read).toBe(1); - expect(context.notifications.length).toBe(0); + expect(ctx.joplin.notifications.length).toBe(0); } }); diff --git a/packages/server/src/middleware/notificationHandler.ts b/packages/server/src/middleware/notificationHandler.ts index 747618a94..ecdec8bff 100644 --- a/packages/server/src/middleware/notificationHandler.ts +++ b/packages/server/src/middleware/notificationHandler.ts @@ -10,31 +10,31 @@ import { NotificationKey } from '../models/NotificationModel'; const logger = Logger.create('notificationHandler'); async function handleChangeAdminPasswordNotification(ctx: AppContext) { - if (!ctx.owner.is_admin) return; + if (!ctx.joplin.owner.is_admin) return; - const defaultAdmin = await ctx.models.user().login(defaultAdminEmail, defaultAdminPassword); - const notificationModel = ctx.models.notification(); + const defaultAdmin = await ctx.joplin.models.user().login(defaultAdminEmail, defaultAdminPassword); + const notificationModel = ctx.joplin.models.notification(); if (defaultAdmin) { await notificationModel.add( - ctx.owner.id, + ctx.joplin.owner.id, NotificationKey.ChangeAdminPassword, NotificationLevel.Important, - _('The default admin password is insecure and has not been changed! [Change it now](%s)', ctx.models.user().profileUrl()) + _('The default admin password is insecure and has not been changed! [Change it now](%s)', ctx.joplin.models.user().profileUrl()) ); } else { - await notificationModel.markAsRead(ctx.owner.id, NotificationKey.ChangeAdminPassword); + await notificationModel.markAsRead(ctx.joplin.owner.id, NotificationKey.ChangeAdminPassword); } } async function handleSqliteInProdNotification(ctx: AppContext) { - if (!ctx.owner.is_admin) return; + if (!ctx.joplin.owner.is_admin) return; - const notificationModel = ctx.models.notification(); + const notificationModel = ctx.joplin.models.notification(); - if (config().database.client === 'sqlite3' && ctx.env === 'prod') { + if (config().database.client === 'sqlite3' && ctx.joplin.env === 'prod') { await notificationModel.add( - ctx.owner.id, + ctx.joplin.owner.id, NotificationKey.UsingSqliteInProd ); } @@ -43,8 +43,8 @@ async function handleSqliteInProdNotification(ctx: AppContext) { async function makeNotificationViews(ctx: AppContext): Promise { const markdownIt = new MarkdownIt(); - const notificationModel = ctx.models.notification(); - const notifications = await notificationModel.allUnreadByUserId(ctx.owner.id); + const notificationModel = ctx.joplin.models.notification(); + const notifications = await notificationModel.allUnreadByUserId(ctx.joplin.owner.id); const views: NotificationView[] = []; for (const n of notifications) { views.push({ @@ -62,15 +62,15 @@ async function makeNotificationViews(ctx: AppContext): Promise { - ctx.notifications = []; + ctx.joplin.notifications = []; try { if (isApiRequest(ctx)) return next(); - if (!ctx.owner) return next(); + if (!ctx.joplin.owner) return next(); await handleChangeAdminPasswordNotification(ctx); await handleSqliteInProdNotification(ctx); - ctx.notifications = await makeNotificationViews(ctx); + ctx.joplin.notifications = await makeNotificationViews(ctx); } catch (error) { logger.error(error); } diff --git a/packages/server/src/middleware/ownerHandler.test.ts b/packages/server/src/middleware/ownerHandler.test.ts index f0827bb91..e567f5f08 100644 --- a/packages/server/src/middleware/ownerHandler.test.ts +++ b/packages/server/src/middleware/ownerHandler.test.ts @@ -22,12 +22,12 @@ describe('ownerHandler', function() { sessionId: session.id, }); - context.owner = null; + context.joplin.owner = null; await ownerHandler(context, koaNext); - expect(!!context.owner).toBe(true); - expect(context.owner.id).toBe(user.id); + expect(!!context.joplin.owner).toBe(true); + expect(context.joplin.owner.id).toBe(user.id); }); test('should not login user with invalid session ID', async function() { @@ -37,11 +37,11 @@ describe('ownerHandler', function() { sessionId: 'ihack', }); - context.owner = null; + context.joplin.owner = null; await ownerHandler(context, koaNext); - expect(!!context.owner).toBe(false); + expect(!!context.joplin.owner).toBe(false); }); }); diff --git a/packages/server/src/middleware/ownerHandler.ts b/packages/server/src/middleware/ownerHandler.ts index f3bb403fb..05357e4d3 100644 --- a/packages/server/src/middleware/ownerHandler.ts +++ b/packages/server/src/middleware/ownerHandler.ts @@ -3,6 +3,6 @@ import { contextSessionId } from '../utils/requestUtils'; export default async function(ctx: AppContext, next: KoaNext): Promise { const sessionId = contextSessionId(ctx, false); - if (sessionId) ctx.owner = await ctx.models.session().sessionUser(sessionId); + if (sessionId) ctx.joplin.owner = await ctx.joplin.models.session().sessionUser(sessionId); return next(); } diff --git a/packages/server/src/middleware/routeHandler.ts b/packages/server/src/middleware/routeHandler.ts index d414827a6..588696475 100644 --- a/packages/server/src/middleware/routeHandler.ts +++ b/packages/server/src/middleware/routeHandler.ts @@ -7,17 +7,17 @@ export default async function(ctx: AppContext) { const requestStartTime = Date.now(); try { - const responseObject = await execRequest(ctx.routes, ctx); + const responseObject = await execRequest(ctx.joplin.routes, ctx); if (responseObject instanceof Response) { ctx.response = responseObject.response; } else if (isView(responseObject)) { const view = responseObject as View; ctx.response.status = view?.content?.error ? view?.content?.error?.httpCode || 500 : 200; - ctx.response.body = await ctx.services.mustache.renderView(view, { - notifications: ctx.notifications || [], - hasNotifications: !!ctx.notifications && !!ctx.notifications.length, - owner: ctx.owner, + ctx.response.body = await ctx.joplin.services.mustache.renderView(view, { + notifications: ctx.joplin.notifications || [], + hasNotifications: !!ctx.joplin.notifications && !!ctx.joplin.notifications.length, + owner: ctx.joplin.owner, }); } else { ctx.response.status = 200; @@ -25,9 +25,9 @@ export default async function(ctx: AppContext) { } } catch (error) { if (error.httpCode >= 400 && error.httpCode < 500) { - ctx.appLogger().error(`${error.httpCode}: ` + `${ctx.request.method} ${ctx.path}` + ` : ${error.message}`); + ctx.joplin.appLogger().error(`${error.httpCode}: ` + `${ctx.request.method} ${ctx.path}` + ` : ${error.message}`); } else { - ctx.appLogger().error(error); + ctx.joplin.appLogger().error(error); } // Uncomment this when getting HTML blobs as errors while running tests. @@ -47,15 +47,15 @@ export default async function(ctx: AppContext) { content: { error, stack: config().showErrorStackTraces ? error.stack : '', - owner: ctx.owner, + owner: ctx.joplin.owner, }, title: 'Error', }; - ctx.response.body = await ctx.services.mustache.renderView(view); + ctx.response.body = await ctx.joplin.services.mustache.renderView(view); } else { // JSON ctx.response.set('Content-Type', 'application/json'); const r: any = { error: error.message }; - if (ctx.env === Env.Dev && error.stack) r.stack = error.stack; + if (ctx.joplin.env === Env.Dev && error.stack) r.stack = error.stack; if (error.code) r.code = error.code; ctx.response.body = r; } @@ -63,6 +63,6 @@ export default async function(ctx: AppContext) { // Technically this is not the total request duration because there are // other middlewares but that should give a good approximation const requestDuration = Date.now() - requestStartTime; - ctx.appLogger().info(`${ctx.request.method} ${ctx.path} (${requestDuration}ms)`); + ctx.joplin.appLogger().info(`${ctx.request.method} ${ctx.path} (${requestDuration}ms)`); } } diff --git a/packages/server/src/routes/api/batch.ts b/packages/server/src/routes/api/batch.ts index bdb3d77f9..60f8b0def 100644 --- a/packages/server/src/routes/api/batch.ts +++ b/packages/server/src/routes/api/batch.ts @@ -44,13 +44,16 @@ function createSubRequestContext(ctx: AppContext, subRequest: SubRequest): AppCo ...subRequest.headers, }, body: subRequest.body, - appLogger: ctx.appLogger, + joplin: { + ...ctx.joplin, + appLogger: ctx.joplin.appLogger, + services: ctx.joplin.services, + db: ctx.joplin.db, + models: ctx.joplin.models, + routes: ctx.joplin.routes, + }, path: `/${subRequest.url}`, url: fullUrl, - services: ctx.services, - db: ctx.db, - models: ctx.models, - routes: ctx.routes, }; return newContext; diff --git a/packages/server/src/routes/api/debug.ts b/packages/server/src/routes/api/debug.ts index bcd903888..8e7b7363e 100644 --- a/packages/server/src/routes/api/debug.ts +++ b/packages/server/src/routes/api/debug.ts @@ -20,7 +20,7 @@ router.post('api/debug', async (_path: SubPath, ctx: AppContext) => { console.info(`Action: ${query.action}`); if (query.action === 'createTestUsers') { - await createTestUsers(ctx.db, config()); + await createTestUsers(ctx.joplin.db, config()); } }); diff --git a/packages/server/src/routes/api/events.ts b/packages/server/src/routes/api/events.ts index fc1896d2e..31ba798f0 100644 --- a/packages/server/src/routes/api/events.ts +++ b/packages/server/src/routes/api/events.ts @@ -11,7 +11,7 @@ interface Event { const supportedEvents: Record = { syncStart: async (_ctx: AppContext) => { - // await ctx.models.share().updateSharedItems2(ctx.owner.id); + // await ctx.joplin.models.share().updateSharedItems2(ctx.joplin.owner.id); }, }; diff --git a/packages/server/src/routes/api/items.ts b/packages/server/src/routes/api/items.ts index 55558e878..fdab53ab1 100644 --- a/packages/server/src/routes/api/items.ts +++ b/packages/server/src/routes/api/items.ts @@ -17,7 +17,7 @@ const router = new Router(RouteType.Api); const batchMaxSize = 1 * MB; export async function putItemContents(path: SubPath, ctx: AppContext, isBatch: boolean) { - if (!ctx.owner.can_upload) throw new ErrorForbidden('Uploading content is disabled'); + if (!ctx.joplin.owner.can_upload) throw new ErrorForbidden('Uploading content is disabled'); const parsedBody = await formParse(ctx.req); const bodyFields = parsedBody.fields; @@ -49,12 +49,12 @@ export async function putItemContents(path: SubPath, ctx: AppContext, isBatch: b // query parameter. if (ctx.query['share_id']) { saveOptions.shareId = ctx.query['share_id']; - await ctx.models.item().checkIfAllowed(ctx.owner, AclAction.Create, { jop_share_id: saveOptions.shareId }); + await ctx.joplin.models.item().checkIfAllowed(ctx.joplin.owner, AclAction.Create, { jop_share_id: saveOptions.shareId }); } items = [ { - name: ctx.models.item().pathToName(path.id), + name: ctx.joplin.models.item().pathToName(path.id), body: buffer, }, ]; @@ -63,9 +63,9 @@ export async function putItemContents(path: SubPath, ctx: AppContext, isBatch: b } } - const output = await ctx.models.item().saveFromRawContent(ctx.owner, items, saveOptions); + const output = await ctx.joplin.models.item().saveFromRawContent(ctx.joplin.owner, items, saveOptions); for (const [name] of Object.entries(output)) { - if (output[name].item) output[name].item = ctx.models.item().toApiOutput(output[name].item) as Item; + if (output[name].item) output[name].item = ctx.joplin.models.item().toApiOutput(output[name].item) as Item; } return output; } @@ -89,8 +89,8 @@ async function itemFromPath(userId: Uuid, itemModel: ItemModel, path: SubPath, m } router.get('api/items/:id', async (path: SubPath, ctx: AppContext) => { - const itemModel = ctx.models.item(); - const item = await itemFromPath(ctx.owner.id, itemModel, path); + const itemModel = ctx.joplin.models.item(); + const item = await itemFromPath(ctx.joplin.owner.id, itemModel, path); return itemModel.toApiOutput(item); }); @@ -99,12 +99,12 @@ router.del('api/items/:id', async (path: SubPath, ctx: AppContext) => { if (path.id === 'root' || path.id === 'root:/:') { // We use this for testing only and for safety reasons it's probably // best to disable it on production. - if (ctx.env !== 'dev') throw new ErrorMethodNotAllowed('Deleting the root is not allowed'); - await ctx.models.item().deleteAll(ctx.owner.id); + if (ctx.joplin.env !== 'dev') throw new ErrorMethodNotAllowed('Deleting the root is not allowed'); + await ctx.joplin.models.item().deleteAll(ctx.joplin.owner.id); } else { - const item = await itemFromPath(ctx.owner.id, ctx.models.item(), path); - await ctx.models.item().checkIfAllowed(ctx.owner, AclAction.Delete, item); - await ctx.models.item().deleteForUser(ctx.owner.id, item); + const item = await itemFromPath(ctx.joplin.owner.id, ctx.joplin.models.item(), path); + await ctx.joplin.models.item().checkIfAllowed(ctx.joplin.owner, AclAction.Delete, item); + await ctx.joplin.models.item().deleteForUser(ctx.joplin.owner.id, item); } } catch (error) { if (error instanceof ErrorNotFound) { @@ -116,8 +116,8 @@ router.del('api/items/:id', async (path: SubPath, ctx: AppContext) => { }); router.get('api/items/:id/content', async (path: SubPath, ctx: AppContext) => { - const itemModel = ctx.models.item(); - const item = await itemFromPath(ctx.owner.id, itemModel, path); + const itemModel = ctx.joplin.models.item(); + const item = await itemFromPath(ctx.joplin.owner.id, itemModel, path); const serializedContent = await itemModel.serializedContent(item.id); return respondWithItemContent(ctx.response, item, serializedContent); }); @@ -130,14 +130,14 @@ router.put('api/items/:id/content', async (path: SubPath, ctx: AppContext) => { }); router.get('api/items/:id/delta', async (_path: SubPath, ctx: AppContext) => { - const changeModel = ctx.models.change(); - return changeModel.delta(ctx.owner.id, requestDeltaPagination(ctx.query)); + const changeModel = ctx.joplin.models.change(); + return changeModel.delta(ctx.joplin.owner.id, requestDeltaPagination(ctx.query)); }); router.get('api/items/:id/children', async (path: SubPath, ctx: AppContext) => { - const itemModel = ctx.models.item(); + const itemModel = ctx.joplin.models.item(); const parentName = itemModel.pathToName(path.id); - const result = await itemModel.children(ctx.owner.id, parentName, requestPagination(ctx.query)); + const result = await itemModel.children(ctx.joplin.owner.id, parentName, requestPagination(ctx.query)); return result; }); diff --git a/packages/server/src/routes/api/sessions.ts b/packages/server/src/routes/api/sessions.ts index 5e9a6b44e..971b7dda0 100644 --- a/packages/server/src/routes/api/sessions.ts +++ b/packages/server/src/routes/api/sessions.ts @@ -12,10 +12,10 @@ router.public = true; router.post('api/sessions', async (_path: SubPath, ctx: AppContext) => { const fields: User = await bodyFields(ctx.req); - const user = await ctx.models.user().login(fields.email, fields.password); + const user = await ctx.joplin.models.user().login(fields.email, fields.password); if (!user) throw new ErrorForbidden('Invalid username or password'); - const session = await ctx.models.session().createUserSession(user.id); + const session = await ctx.joplin.models.session().createUserSession(user.id); return { id: session.id, user_id: session.user_id }; }); diff --git a/packages/server/src/routes/api/share_users.ts b/packages/server/src/routes/api/share_users.ts index f3d5a68c2..06576481e 100644 --- a/packages/server/src/routes/api/share_users.ts +++ b/packages/server/src/routes/api/share_users.ts @@ -9,11 +9,11 @@ import { AclAction } from '../../models/BaseModel'; const router = new Router(RouteType.Api); router.patch('api/share_users/:id', async (path: SubPath, ctx: AppContext) => { - const shareUserModel = ctx.models.shareUser(); + const shareUserModel = ctx.joplin.models.shareUser(); const shareUser = await shareUserModel.load(path.id); if (!shareUser) throw new ErrorNotFound(); - await shareUserModel.checkIfAllowed(ctx.owner, AclAction.Update, shareUser); + await shareUserModel.checkIfAllowed(ctx.joplin.owner, AclAction.Update, shareUser); const body = await bodyFields(ctx.req); @@ -25,20 +25,20 @@ router.patch('api/share_users/:id', async (path: SubPath, ctx: AppContext) => { }); router.del('api/share_users/:id', async (path: SubPath, ctx: AppContext) => { - const shareUser = await ctx.models.shareUser().load(path.id); + const shareUser = await ctx.joplin.models.shareUser().load(path.id); if (!shareUser) throw new ErrorNotFound(); - await ctx.models.shareUser().checkIfAllowed(ctx.owner, AclAction.Delete, shareUser); - await ctx.models.shareUser().delete(shareUser.id); + await ctx.joplin.models.shareUser().checkIfAllowed(ctx.joplin.owner, AclAction.Delete, shareUser); + await ctx.joplin.models.shareUser().delete(shareUser.id); }); router.get('api/share_users', async (_path: SubPath, ctx: AppContext) => { - const shareUsers = await ctx.models.shareUser().byUserId(ctx.owner.id); + const shareUsers = await ctx.joplin.models.shareUser().byUserId(ctx.joplin.owner.id); const items: any[] = []; for (const su of shareUsers) { - const share = await ctx.models.share().load(su.share_id); - const sharer = await ctx.models.user().load(share.owner_id); + const share = await ctx.joplin.models.share().load(su.share_id); + const sharer = await ctx.joplin.models.user().load(share.owner_id); items.push({ id: su.id, diff --git a/packages/server/src/routes/api/shares.ts b/packages/server/src/routes/api/shares.ts index 9a45b9fae..02875d6f1 100644 --- a/packages/server/src/routes/api/shares.ts +++ b/packages/server/src/routes/api/shares.ts @@ -19,7 +19,7 @@ router.public = true; router.post('api/shares', async (_path: SubPath, ctx: AppContext) => { ownerRequired(ctx); - const shareModel = ctx.models.share(); + const shareModel = ctx.joplin.models.share(); const fields = await bodyFields(ctx.req); const shareInput: ShareApiInput = shareModel.fromApiInput(fields) as ShareApiInput; if (fields.folder_id) shareInput.folder_id = fields.folder_id; @@ -31,9 +31,9 @@ router.post('api/shares', async (_path: SubPath, ctx: AppContext) => { // - Additionally, the App method is available, but not exposed via the API. if (shareInput.folder_id) { - return ctx.models.share().shareFolder(ctx.owner, shareInput.folder_id); + return ctx.joplin.models.share().shareFolder(ctx.joplin.owner, shareInput.folder_id); } else if (shareInput.note_id) { - return ctx.models.share().shareNote(ctx.owner, shareInput.note_id); + return ctx.joplin.models.share().shareNote(ctx.joplin.owner, shareInput.note_id); } else { throw new ErrorBadRequest('Either folder_id or note_id must be provided'); } @@ -47,28 +47,28 @@ router.post('api/shares/:id/users', async (path: SubPath, ctx: AppContext) => { } const fields = await bodyFields(ctx.req) as UserInput; - const user = await ctx.models.user().loadByEmail(fields.email); + const user = await ctx.joplin.models.user().loadByEmail(fields.email); if (!user) throw new ErrorNotFound('User not found'); const shareId = path.id; - await ctx.models.shareUser().checkIfAllowed(ctx.owner, AclAction.Create, { + await ctx.joplin.models.shareUser().checkIfAllowed(ctx.joplin.owner, AclAction.Create, { share_id: shareId, user_id: user.id, }); - return ctx.models.shareUser().addByEmail(shareId, user.email); + return ctx.joplin.models.shareUser().addByEmail(shareId, user.email); }); router.get('api/shares/:id/users', async (path: SubPath, ctx: AppContext) => { ownerRequired(ctx); const shareId = path.id; - const share = await ctx.models.share().load(shareId); - await ctx.models.share().checkIfAllowed(ctx.owner, AclAction.Read, share); + const share = await ctx.joplin.models.share().load(shareId); + await ctx.joplin.models.share().checkIfAllowed(ctx.joplin.owner, AclAction.Read, share); - const shareUsers = await ctx.models.shareUser().byShareId(shareId, null); - const users = await ctx.models.user().loadByIds(shareUsers.map(su => su.user_id)); + const shareUsers = await ctx.joplin.models.shareUser().byShareId(shareId, null); + const users = await ctx.joplin.models.user().loadByIds(shareUsers.map(su => su.user_id)); const items = shareUsers.map(su => { const user = users.find(u => u.id === su.user_id); @@ -90,7 +90,7 @@ router.get('api/shares/:id/users', async (path: SubPath, ctx: AppContext) => { }); router.get('api/shares/:id', async (path: SubPath, ctx: AppContext) => { - const shareModel = ctx.models.share(); + const shareModel = ctx.joplin.models.share(); const share = await shareModel.load(path.id); if (share && share.type === ShareType.Note) { @@ -105,7 +105,7 @@ router.get('api/shares/:id', async (path: SubPath, ctx: AppContext) => { router.get('api/shares', async (_path: SubPath, ctx: AppContext) => { ownerRequired(ctx); - const shares = ctx.models.share().toApiOutput(await ctx.models.share().sharesByUser(ctx.owner.id)) as Share[]; + const shares = ctx.joplin.models.share().toApiOutput(await ctx.joplin.models.share().sharesByUser(ctx.joplin.owner.id)) as Share[]; // Fake paginated results so that it can be added later on, if needed. return { items: shares.map(share => { @@ -123,9 +123,9 @@ router.get('api/shares', async (_path: SubPath, ctx: AppContext) => { router.del('api/shares/:id', async (path: SubPath, ctx: AppContext) => { ownerRequired(ctx); - const share = await ctx.models.share().load(path.id); - await ctx.models.share().checkIfAllowed(ctx.owner, AclAction.Delete, share); - await ctx.models.share().delete(share.id); + const share = await ctx.joplin.models.share().load(path.id); + await ctx.joplin.models.share().checkIfAllowed(ctx.joplin.owner, AclAction.Delete, share); + await ctx.joplin.models.share().delete(share.id); }); export default router; diff --git a/packages/server/src/routes/api/users.ts b/packages/server/src/routes/api/users.ts index 54971c0e9..bd3471382 100644 --- a/packages/server/src/routes/api/users.ts +++ b/packages/server/src/routes/api/users.ts @@ -11,23 +11,23 @@ import uuidgen from '../../utils/uuidgen'; const router = new Router(RouteType.Api); async function fetchUser(path: SubPath, ctx: AppContext): Promise { - const user = await ctx.models.user().load(path.id); + const user = await ctx.joplin.models.user().load(path.id); if (!user) throw new ErrorNotFound(`No user with ID ${path.id}`); return user; } async function postedUserFromContext(ctx: AppContext): Promise { - return ctx.models.user().fromApiInput(await bodyFields(ctx.req)); + return ctx.joplin.models.user().fromApiInput(await bodyFields(ctx.req)); } router.get('api/users/:id', async (path: SubPath, ctx: AppContext) => { const user = await fetchUser(path, ctx); - await ctx.models.user().checkIfAllowed(ctx.owner, AclAction.Read, user); + await ctx.joplin.models.user().checkIfAllowed(ctx.joplin.owner, AclAction.Read, user); return user; }); router.post('api/users', async (_path: SubPath, ctx: AppContext) => { - await ctx.models.user().checkIfAllowed(ctx.owner, AclAction.Create); + await ctx.joplin.models.user().checkIfAllowed(ctx.joplin.owner, AclAction.Create); const user = await postedUserFromContext(ctx); // We set a random password because it's required, but user will have to @@ -35,30 +35,30 @@ router.post('api/users', async (_path: SubPath, ctx: AppContext) => { user.password = uuidgen(); user.must_set_password = 1; user.email_confirmed = 0; - const output = await ctx.models.user().save(user); - return ctx.models.user().toApiOutput(output); + const output = await ctx.joplin.models.user().save(user); + return ctx.joplin.models.user().toApiOutput(output); }); router.get('api/users', async (_path: SubPath, ctx: AppContext) => { - await ctx.models.user().checkIfAllowed(ctx.owner, AclAction.List); + await ctx.joplin.models.user().checkIfAllowed(ctx.joplin.owner, AclAction.List); return { - items: await ctx.models.user().all(), + items: await ctx.joplin.models.user().all(), has_more: false, }; }); router.del('api/users/:id', async (path: SubPath, ctx: AppContext) => { const user = await fetchUser(path, ctx); - await ctx.models.user().checkIfAllowed(ctx.owner, AclAction.Delete, user); - await ctx.models.user().delete(user.id); + await ctx.joplin.models.user().checkIfAllowed(ctx.joplin.owner, AclAction.Delete, user); + await ctx.joplin.models.user().delete(user.id); }); router.patch('api/users/:id', async (path: SubPath, ctx: AppContext) => { const user = await fetchUser(path, ctx); - await ctx.models.user().checkIfAllowed(ctx.owner, AclAction.Update, user); + await ctx.joplin.models.user().checkIfAllowed(ctx.joplin.owner, AclAction.Update, user); const postedUser = await postedUserFromContext(ctx); - await ctx.models.user().save({ id: user.id, ...postedUser }); + await ctx.joplin.models.user().save({ id: user.id, ...postedUser }); }); export default router; diff --git a/packages/server/src/routes/default.ts b/packages/server/src/routes/default.ts index 00295471c..dd03ca912 100644 --- a/packages/server/src/routes/default.ts +++ b/packages/server/src/routes/default.ts @@ -53,7 +53,7 @@ router.public = true; router.get('', async (path: SubPath, ctx: AppContext) => { // Redirect to either /login or /home when trying to access the root if (!path.id && !path.link) { - if (ctx.owner) { + if (ctx.joplin.owner) { return redirect(ctx, 'home'); } else { return redirect(ctx, 'login'); diff --git a/packages/server/src/routes/index/changes.ts b/packages/server/src/routes/index/changes.ts index fa22c9202..6b4fbec80 100644 --- a/packages/server/src/routes/index/changes.ts +++ b/packages/server/src/routes/index/changes.ts @@ -14,11 +14,11 @@ const router = new Router(RouteType.Web); router.get('changes', async (_path: SubPath, ctx: AppContext) => { const pagination = makeTablePagination(ctx.query, 'updated_time', PaginationOrderDir.DESC); - const paginatedChanges = await ctx.models.change().allByUser(ctx.owner.id, pagination); - const items = await ctx.models.item().loadByIds(paginatedChanges.items.map(i => i.item_id), { fields: ['id'] }); + const paginatedChanges = await ctx.joplin.models.change().allByUser(ctx.joplin.owner.id, pagination); + const items = await ctx.joplin.models.item().loadByIds(paginatedChanges.items.map(i => i.item_id), { fields: ['id'] }); const table: Table = { - baseUrl: ctx.models.change().changeUrl(), + baseUrl: ctx.joplin.models.change().changeUrl(), requestQuery: ctx.query, pageCount: paginatedChanges.page_count, pagination, @@ -42,7 +42,7 @@ router.get('changes', async (_path: SubPath, ctx: AppContext) => { { value: change.item_name, stretch: true, - url: showItemUrls(config()) ? (items.find(i => i.id === change.item_id) ? ctx.models.item().itemContentUrl(change.item_id) : '') : null, + url: showItemUrls(config()) ? (items.find(i => i.id === change.item_id) ? ctx.joplin.models.item().itemContentUrl(change.item_id) : '') : null, }, { value: changeTypeToString(change.type), diff --git a/packages/server/src/routes/index/home.ts b/packages/server/src/routes/index/home.ts index 6d2a08a6d..6554b9468 100644 --- a/packages/server/src/routes/index/home.ts +++ b/packages/server/src/routes/index/home.ts @@ -15,7 +15,7 @@ router.get('home', async (_path: SubPath, ctx: AppContext) => { contextSessionId(ctx); if (ctx.method === 'GET') { - const accountProps = accountTypeProperties(ctx.owner.account_type); + const accountProps = accountTypeProperties(ctx.joplin.owner.account_type); const view = defaultView('home', 'Home'); view.content = { @@ -26,7 +26,7 @@ router.get('home', async (_path: SubPath, ctx: AppContext) => { }, { label: 'Is Admin', - value: yesOrNo(ctx.owner.is_admin), + value: yesOrNo(ctx.joplin.owner.is_admin), }, { label: 'Max Item Size', diff --git a/packages/server/src/routes/index/items.ts b/packages/server/src/routes/index/items.ts index c7262f22b..1dd164e51 100644 --- a/packages/server/src/routes/index/items.ts +++ b/packages/server/src/routes/index/items.ts @@ -16,12 +16,12 @@ const router = new Router(RouteType.Web); router.get('items', async (_path: SubPath, ctx: AppContext) => { const pagination = makeTablePagination(ctx.query, 'name', PaginationOrderDir.ASC); - const paginatedItems = await ctx.models.item().children(ctx.owner.id, '', pagination, { fields: ['id', 'name', 'updated_time', 'mime_type', 'content_size'] }); + const paginatedItems = await ctx.joplin.models.item().children(ctx.joplin.owner.id, '', pagination, { fields: ['id', 'name', 'updated_time', 'mime_type', 'content_size'] }); const table: Table = { - baseUrl: ctx.models.item().itemUrl(), + baseUrl: ctx.joplin.models.item().itemUrl(), requestQuery: ctx.query, - pageCount: Math.ceil((await ctx.models.item().childrenCount(ctx.owner.id, '')) / pagination.limit), + pageCount: Math.ceil((await ctx.joplin.models.item().childrenCount(ctx.joplin.owner.id, '')) / pagination.limit), pagination, headers: [ { @@ -72,7 +72,7 @@ router.get('items', async (_path: SubPath, ctx: AppContext) => { }); router.get('items/:id/content', async (path: SubPath, ctx: AppContext) => { - const itemModel = ctx.models.item(); + const itemModel = ctx.joplin.models.item(); const item = await itemModel.loadWithContent(path.id); if (!item) throw new ErrorNotFound(); return respondWithItemContent(ctx.response, item, item.content); @@ -83,13 +83,13 @@ router.post('items', async (_path: SubPath, ctx: AppContext) => { const fields = body.fields; if (fields.delete_all_button) { - const itemModel = ctx.models.item(); - await itemModel.deleteAll(ctx.owner.id); + const itemModel = ctx.joplin.models.item(); + await itemModel.deleteAll(ctx.joplin.owner.id); } else { throw new Error('Invalid form button'); } - return redirect(ctx, await ctx.models.item().itemUrl()); + return redirect(ctx, await ctx.joplin.models.item().itemUrl()); }); export default router; diff --git a/packages/server/src/routes/index/login.ts b/packages/server/src/routes/index/login.ts index 6c06debc5..3738ed735 100644 --- a/packages/server/src/routes/index/login.ts +++ b/packages/server/src/routes/index/login.ts @@ -28,7 +28,7 @@ router.post('login', async (_path: SubPath, ctx: AppContext) => { try { const body = await formParse(ctx.req); - const session = await ctx.models.session().authenticate(body.fields.email, body.fields.password); + const session = await ctx.joplin.models.session().authenticate(body.fields.email, body.fields.password); ctx.cookies.set('sessionId', session.id); return redirect(ctx, `${config().baseUrl}/home`); } catch (error) { diff --git a/packages/server/src/routes/index/logout.ts b/packages/server/src/routes/index/logout.ts index 0fa968dfd..53237c394 100644 --- a/packages/server/src/routes/index/logout.ts +++ b/packages/server/src/routes/index/logout.ts @@ -10,7 +10,7 @@ const router = new Router(RouteType.Web); router.post('logout', async (_path: SubPath, ctx: AppContext) => { const sessionId = contextSessionId(ctx, false); ctx.cookies.set('sessionId', ''); - await ctx.models.session().logout(sessionId); + await ctx.joplin.models.session().logout(sessionId); return redirect(ctx, `${config().baseUrl}/login`); }); diff --git a/packages/server/src/routes/index/notifications.ts b/packages/server/src/routes/index/notifications.ts index a87ad53e8..19d50ee9b 100644 --- a/packages/server/src/routes/index/notifications.ts +++ b/packages/server/src/routes/index/notifications.ts @@ -11,7 +11,7 @@ const router = new Router(RouteType.Web); router.patch('notifications/:id', async (path: SubPath, ctx: AppContext) => { const fields: Notification = await bodyFields(ctx.req); const notificationId = path.id; - const model = ctx.models.notification(); + const model = ctx.joplin.models.notification(); const existingNotification = await model.load(notificationId); if (!existingNotification) throw new ErrorNotFound(); diff --git a/packages/server/src/routes/index/shares.ts b/packages/server/src/routes/index/shares.ts index 55499f197..ce59a8716 100644 --- a/packages/server/src/routes/index/shares.ts +++ b/packages/server/src/routes/index/shares.ts @@ -24,19 +24,19 @@ const router: Router = new Router(RouteType.Web); router.public = true; router.get('shares/:id', async (path: SubPath, ctx: AppContext) => { - const shareModel = ctx.models.share(); + const shareModel = ctx.joplin.models.share(); const share = await shareModel.load(path.id); if (!share) throw new ErrorNotFound(); - const itemModel = ctx.models.item(); + const itemModel = ctx.joplin.models.item(); const item = await itemModel.loadWithContent(share.item_id); if (!item) throw new ErrorNotFound(); const result = await renderItem(ctx, item, share); - ctx.models.share().checkShareUrl(share, ctx.URL.origin); + ctx.joplin.models.share().checkShareUrl(share, ctx.URL.origin); ctx.response.body = result.body; ctx.response.set('Content-Type', result.mime); diff --git a/packages/server/src/routes/index/signup.ts b/packages/server/src/routes/index/signup.ts index bae6561a1..c5dd116c9 100644 --- a/packages/server/src/routes/index/signup.ts +++ b/packages/server/src/routes/index/signup.ts @@ -43,17 +43,17 @@ router.post('signup', async (_path: SubPath, ctx: AppContext) => { const formUser = await bodyFields(ctx.req); const password = checkPassword(formUser, true); - const user = await ctx.models.user().save({ + const user = await ctx.joplin.models.user().save({ ...accountTypeProperties(AccountType.Basic), email: formUser.email, full_name: formUser.full_name, password, }); - const session = await ctx.models.session().createUserSession(user.id); + const session = await ctx.joplin.models.session().createUserSession(user.id); ctx.cookies.set('sessionId', session.id); - await ctx.models.notification().add(user.id, NotificationKey.ConfirmEmail); + await ctx.joplin.models.notification().add(user.id, NotificationKey.ConfirmEmail); return redirect(ctx, `${config().baseUrl}/home`); } catch (error) { diff --git a/packages/server/src/routes/index/stripe.ts b/packages/server/src/routes/index/stripe.ts index 184324c75..d929b757f 100644 --- a/packages/server/src/routes/index/stripe.ts +++ b/packages/server/src/routes/index/stripe.ts @@ -137,7 +137,7 @@ const postHandlers: Record = { const stripeUserId = checkoutSession.customer as string; const stripeSubscriptionId = checkoutSession.subscription as string; - await ctx.models.subscription().saveUserAndSubscription( + await ctx.joplin.models.subscription().saveUserAndSubscription( checkoutSession.customer_details.email || checkoutSession.customer_email, AccountType.Pro, stripeUserId, @@ -158,7 +158,7 @@ const postHandlers: Record = { // saved in checkout.session.completed. const invoice = event.data.object as Stripe.Invoice; - await ctx.models.subscription().handlePayment(invoice.subscription as string, true); + await ctx.joplin.models.subscription().handlePayment(invoice.subscription as string, true); }, 'invoice.payment_failed': async () => { @@ -170,7 +170,7 @@ const postHandlers: Record = { const invoice = event.data.object as Stripe.Invoice; const subId = invoice.subscription as string; - await ctx.models.subscription().handlePayment(subId, false); + await ctx.joplin.models.subscription().handlePayment(subId, false); }, }; @@ -202,9 +202,9 @@ const getHandlers: Record = { }, portal: async (stripe: Stripe, _path: SubPath, ctx: AppContext) => { - if (!ctx.owner) throw new ErrorForbidden('Please login to access the subscription portal'); + if (!ctx.joplin.owner) throw new ErrorForbidden('Please login to access the subscription portal'); - const sub = await ctx.models.subscription().byUserId(ctx.owner.id); + const sub = await ctx.joplin.models.subscription().byUserId(ctx.joplin.owner.id); if (!sub) throw new ErrorNotFound('Could not find subscription'); const billingPortalSession = await stripe.billingPortal.sessions.create({ customer: sub.stripe_user_id as string }); diff --git a/packages/server/src/routes/index/users.ts b/packages/server/src/routes/index/users.ts index c572bd615..b7de1b5d0 100644 --- a/packages/server/src/routes/index/users.ts +++ b/packages/server/src/routes/index/users.ts @@ -78,8 +78,8 @@ function userIsMe(path: SubPath): boolean { const router = new Router(RouteType.Web); router.get('users', async (_path: SubPath, ctx: AppContext) => { - const userModel = ctx.models.user(); - await userModel.checkIfAllowed(ctx.owner, AclAction.List); + const userModel = ctx.joplin.models.user(); + await userModel.checkIfAllowed(ctx.joplin.owner, AclAction.List); const users = await userModel.all(); @@ -101,16 +101,16 @@ router.get('users', async (_path: SubPath, ctx: AppContext) => { }); router.get('users/:id', async (path: SubPath, ctx: AppContext, user: User = null, error: any = null) => { - const owner = ctx.owner; + const owner = ctx.joplin.owner; const isMe = userIsMe(path); const isNew = userIsNew(path); - const userModel = ctx.models.user(); + const userModel = ctx.joplin.models.user(); const userId = userIsMe(path) ? owner.id : path.id; user = !isNew ? user || await userModel.load(userId) : null; if (isNew && !user) user = defaultUser(); - await userModel.checkIfAllowed(ctx.owner, AclAction.Read, user); + await userModel.checkIfAllowed(ctx.joplin.owner, AclAction.Read, user); let postUrl = ''; @@ -147,9 +147,9 @@ router.publicSchemas.push('users/:id/confirm'); router.get('users/:id/confirm', async (path: SubPath, ctx: AppContext, error: Error = null) => { const userId = path.id; const token = ctx.query.token; - if (token) await ctx.models.user().confirmEmail(userId, token); + if (token) await ctx.joplin.models.user().confirmEmail(userId, token); - const user = await ctx.models.user().load(userId); + const user = await ctx.joplin.models.user().load(userId); if (user.must_set_password) { const view: View = { @@ -158,17 +158,17 @@ router.get('users/:id/confirm', async (path: SubPath, ctx: AppContext, error: Er user, error, token, - postUrl: ctx.models.user().confirmUrl(userId, token), + postUrl: ctx.joplin.models.user().confirmUrl(userId, token), }, navbar: false, }; return view; } else { - await ctx.models.token().deleteByValue(userId, token); - await ctx.models.notification().add(userId, NotificationKey.EmailConfirmed); + await ctx.joplin.models.token().deleteByValue(userId, token); + await ctx.joplin.models.notification().add(userId, NotificationKey.EmailConfirmed); - if (ctx.owner) { + if (ctx.joplin.owner) { return redirect(ctx, `${config().baseUrl}/home`); } else { return redirect(ctx, `${config().baseUrl}/login`); @@ -187,17 +187,17 @@ router.post('users/:id/confirm', async (path: SubPath, ctx: AppContext) => { try { const fields = await bodyFields(ctx.req); - await ctx.models.token().checkToken(userId, fields.token); + await ctx.joplin.models.token().checkToken(userId, fields.token); const password = checkPassword(fields, true); - await ctx.models.user().save({ id: userId, password, must_set_password: 0 }); - await ctx.models.token().deleteByValue(userId, fields.token); + await ctx.joplin.models.user().save({ id: userId, password, must_set_password: 0 }); + await ctx.joplin.models.token().deleteByValue(userId, fields.token); - const session = await ctx.models.session().createUserSession(userId); + const session = await ctx.joplin.models.session().createUserSession(userId); ctx.cookies.set('sessionId', session.id); - await ctx.models.notification().add(userId, NotificationKey.PasswordSet); + await ctx.joplin.models.notification().add(userId, NotificationKey.PasswordSet); return redirect(ctx, `${config().baseUrl}/home`); } catch (error) { @@ -217,7 +217,7 @@ interface FormFields { router.post('users', async (path: SubPath, ctx: AppContext) => { let user: User = {}; - const userId = userIsMe(path) ? ctx.owner.id : path.id; + const userId = userIsMe(path) ? ctx.joplin.owner.id : path.id; try { const body = await formParse(ctx.req); @@ -226,11 +226,11 @@ router.post('users', async (path: SubPath, ctx: AppContext) => { if (userIsMe(path)) fields.id = userId; user = makeUser(isNew, fields); - const userModel = ctx.models.user(); + const userModel = ctx.joplin.models.user(); if (fields.post_button) { const userToSave: User = userModel.fromApiInput(user); - await userModel.checkIfAllowed(ctx.owner, isNew ? AclAction.Create : AclAction.Update, userToSave); + await userModel.checkIfAllowed(ctx.joplin.owner, isNew ? AclAction.Create : AclAction.Update, userToSave); if (isNew) { await userModel.save(userToSave); @@ -239,7 +239,7 @@ router.post('users', async (path: SubPath, ctx: AppContext) => { } } else if (fields.delete_button) { const user = await userModel.load(path.id); - await userModel.checkIfAllowed(ctx.owner, AclAction.Delete, user); + await userModel.checkIfAllowed(ctx.joplin.owner, AclAction.Delete, user); await userModel.delete(path.id); } else if (fields.send_reset_password_email) { const user = await userModel.load(path.id); diff --git a/packages/server/src/utils/Router.ts b/packages/server/src/utils/Router.ts index 62beadd01..ba186a9f0 100644 --- a/packages/server/src/utils/Router.ts +++ b/packages/server/src/utils/Router.ts @@ -10,7 +10,7 @@ interface RouteInfo { export default class Router { // When the router is public, we do not check that a valid session is - // available (that ctx.owner is defined). It means by default any user, even + // available (that ctx.joplin.owner is defined). It means by default any user, even // not logged in, can access any route of this router. End points that // should not be publicly available should call ownerRequired(ctx); public public: boolean = false; diff --git a/packages/server/src/utils/requestUtils.ts b/packages/server/src/utils/requestUtils.ts index e2838195f..5c5e86aa7 100644 --- a/packages/server/src/utils/requestUtils.ts +++ b/packages/server/src/utils/requestUtils.ts @@ -43,7 +43,7 @@ export async function bodyFields(req: any/* , filter:string[] = null*/): Prom } export function ownerRequired(ctx: AppContext) { - if (!ctx.owner) throw new ErrorForbidden(); + if (!ctx.joplin.owner) throw new ErrorForbidden(); } export function headerSessionId(headers: any): string { diff --git a/packages/server/src/utils/routeUtils.ts b/packages/server/src/utils/routeUtils.ts index 0b9672af9..1170c099f 100644 --- a/packages/server/src/utils/routeUtils.ts +++ b/packages/server/src/utils/routeUtils.ts @@ -191,7 +191,7 @@ export async function execRequest(routes: Routers, ctx: AppContext) { // This is a generic catch-all for all private end points - if we // couldn't get a valid session, we exit now. Individual end points // might have additional permission checks depending on the action. - if (!match.route.isPublic(match.subPath.schema) && !ctx.owner) throw new ErrorForbidden(); + if (!match.route.isPublic(match.subPath.schema) && !ctx.joplin.owner) throw new ErrorForbidden(); return endPoint.handler(match.subPath, ctx); } diff --git a/packages/server/src/utils/setupAppContext.ts b/packages/server/src/utils/setupAppContext.ts index 2d2441f54..29fbfb3a4 100644 --- a/packages/server/src/utils/setupAppContext.ts +++ b/packages/server/src/utils/setupAppContext.ts @@ -24,14 +24,19 @@ async function setupServices(env: Env, models: Models, config: Config): Promise< } export default async function(appContext: AppContext, env: Env, dbConnection: DbConnection, appLogger: ()=> LoggerWrapper): Promise { - appContext.env = env; - appContext.db = dbConnection; - appContext.models = newModelFactory(appContext.db, config()); - appContext.services = await setupServices(env, appContext.models, config()); - appContext.appLogger = appLogger; - appContext.routes = { ...routes }; + const models = newModelFactory(dbConnection, config()); - if (env === Env.Prod) delete appContext.routes['api/debug']; + appContext.joplin = { + ...appContext.joplin, + env: env, + db: dbConnection, + models: models, + services: await setupServices(env, models, config()), + appLogger: appLogger, + routes: { ...routes }, + }; + + if (env === Env.Prod) delete appContext.joplin.routes['api/debug']; return appContext; } diff --git a/packages/server/src/utils/startServices.ts b/packages/server/src/utils/startServices.ts index e59a815ac..99a0959fc 100644 --- a/packages/server/src/utils/startServices.ts +++ b/packages/server/src/utils/startServices.ts @@ -1,7 +1,7 @@ import { AppContext } from './types'; export default async function startServices(appContext: AppContext) { - const services = appContext.services; + const services = appContext.joplin.services; void services.share.runInBackground(); void services.email.runInBackground(); diff --git a/packages/server/src/utils/testing/testUtils.ts b/packages/server/src/utils/testing/testUtils.ts index 3e907f594..c0bf882e0 100644 --- a/packages/server/src/utils/testing/testUtils.ts +++ b/packages/server/src/utils/testing/testUtils.ts @@ -175,16 +175,20 @@ export async function koaAppContext(options: AppContextTestOptions = null): Prom const appLogger = Logger.create('AppTest'); + const baseAppContext = await setupAppContext({} as any, Env.Dev, db_, () => appLogger); + // Set type to "any" because the Koa context has many properties and we // don't need to mock all of them. const appContext: any = { - ...await setupAppContext({} as any, Env.Dev, db_, () => appLogger), - env: Env.Dev, - db: db_, - models: models(), - appLogger: () => appLogger, + baseAppContext, + joplin: { + ...baseAppContext.joplin, + env: Env.Dev, + db: db_, + models: models(), + owner: owner, + }, path: req.url, - owner: owner, cookies: new FakeCookies(), request: new FakeRequest(req), response: new FakeResponse(), diff --git a/packages/server/src/utils/types.ts b/packages/server/src/utils/types.ts index 48a2e93a2..20716325c 100644 --- a/packages/server/src/utils/types.ts +++ b/packages/server/src/utils/types.ts @@ -18,7 +18,7 @@ export interface NotificationView { closeUrl: string; } -export interface AppContext extends Koa.Context { +interface AppContextJoplin { env: Env; db: DbConnection; models: Models; @@ -29,6 +29,22 @@ export interface AppContext extends Koa.Context { services: Services; } +export interface AppContext extends Koa.Context { + joplin: AppContextJoplin; + + // All the properties under `joplin` were previously at the root, so to make + // sure they are no longer used anywhere we set them to "never", as that + // would trigger the TypeScript compiler. Later on, all this can be removed. + env: never; + db: never; + models: never; + appLogger: never; + notifications: never; + owner: never; + routes: never; + services: never; +} + export enum DatabaseConfigClient { PostgreSQL = 'pg', SQLite = 'sqlite3',