diff --git a/src/ExpressServer.ts b/src/ExpressServer.ts index 5c4d26760..cf975feed 100644 --- a/src/ExpressServer.ts +++ b/src/ExpressServer.ts @@ -1,9 +1,9 @@ import KeyvRedis from '@keyv/redis' import { SystemClock } from 'clock-ts' -import { Effect } from 'effect' +import { Context, Effect } from 'effect' import * as C from 'fp-ts/lib/Console.js' import { pipe } from 'fp-ts/lib/function.js' -import type { Redis } from 'ioredis' +import type { Redis as IoRedis } from 'ioredis' import Keyv from 'keyv' import * as L from 'logger-fp-ts' import fetch from 'make-fetch-happen' @@ -12,7 +12,11 @@ import { P, match } from 'ts-pattern' import { app } from './app.js' import { decodeEnv } from './env.js' -export const expressServer = (redis: Redis) => { +export class Redis extends Context.Tag('Redis')() {} + +export const expressServer = Effect.gen(function* () { + const redis = yield* Redis + const env = decodeEnv(process)() const loggerEnv: L.LoggerEnv = { clock: SystemClock, @@ -32,76 +36,74 @@ export const expressServer = (redis: Redis) => { .exhaustive() const createKeyvStore = () => new KeyvRedis(redis) - return Effect.succeed( - app({ - ...loggerEnv, - allowSiteCrawlers: env.ALLOW_SITE_CRAWLERS, - authorInviteStore: new Keyv({ namespace: 'author-invite', store: createKeyvStore() }), - avatarStore: new Keyv({ namespace: 'avatar-store', store: createKeyvStore() }), - canConnectOrcidProfile: () => true, - canRequestReviews: () => true, - canSeeGatesLogo: true, - canUploadAvatar: () => true, - canUseSearchQueries: () => true, - cloudinaryApi: { cloudName: 'prereview', key: env.CLOUDINARY_API_KEY, secret: env.CLOUDINARY_API_SECRET }, - coarNotifyToken: env.COAR_NOTIFY_TOKEN, - coarNotifyUrl: env.COAR_NOTIFY_URL, - contactEmailAddressStore: new Keyv({ namespace: 'contact-email-address', store: createKeyvStore() }), - environmentLabel: env.ENVIRONMENT_LABEL, - fathomId: env.FATHOM_SITE_ID, - fetch: fetch.defaults({ - cachePath: 'data/cache', - headers: { - 'User-Agent': `PREreview (${env.PUBLIC_URL.href}; mailto:engineering@prereview.org)`, - }, - }), - formStore: new Keyv({ namespace: 'forms', store: createKeyvStore() }), - careerStageStore: new Keyv({ namespace: 'career-stage', store: createKeyvStore() }), - ghostApi: { - key: env.GHOST_API_KEY, - }, - isOpenForRequestsStore: new Keyv({ namespace: 'is-open-for-requests', store: createKeyvStore() }), - isUserBlocked: user => env.BLOCKED_USERS.includes(user), - legacyPrereviewApi: { - app: env.LEGACY_PREREVIEW_API_APP, - key: env.LEGACY_PREREVIEW_API_KEY, - url: env.LEGACY_PREREVIEW_URL, - update: env.LEGACY_PREREVIEW_UPDATE, - }, - languagesStore: new Keyv({ namespace: 'languages', store: createKeyvStore() }), - locationStore: new Keyv({ namespace: 'location', store: createKeyvStore() }), - ...sendMailEnv, - orcidApiUrl: env.ORCID_API_URL, - orcidApiToken: env.ORCID_API_READ_PUBLIC_TOKEN, - orcidOauth: { - authorizeUrl: new URL(`${env.ORCID_URL.origin}/oauth/authorize`), - clientId: env.ORCID_CLIENT_ID, - clientSecret: env.ORCID_CLIENT_SECRET, - revokeUrl: new URL(`${env.ORCID_URL.origin}/oauth/revoke`), - tokenUrl: new URL(`${env.ORCID_URL.origin}/oauth/token`), - }, - orcidTokenStore: new Keyv({ namespace: 'orcid-token', store: createKeyvStore() }), - publicUrl: env.PUBLIC_URL, - redis, - researchInterestsStore: new Keyv({ namespace: 'research-interests', store: createKeyvStore() }), - reviewRequestStore: new Keyv({ namespace: 'review-request', store: createKeyvStore() }), - scietyListToken: env.SCIETY_LIST_TOKEN, - secret: env.SECRET, - sessionCookie: 'session', - sessionStore: new Keyv({ namespace: 'sessions', store: createKeyvStore(), ttl: 1000 * 60 * 60 * 24 * 30 }), - slackOauth: { - authorizeUrl: new URL('https://slack.com/oauth/v2/authorize'), - clientId: env.SLACK_CLIENT_ID, - clientSecret: env.SLACK_CLIENT_SECRET, - tokenUrl: new URL('https://slack.com/api/oauth.v2.access'), + return app({ + ...loggerEnv, + allowSiteCrawlers: env.ALLOW_SITE_CRAWLERS, + authorInviteStore: new Keyv({ namespace: 'author-invite', store: createKeyvStore() }), + avatarStore: new Keyv({ namespace: 'avatar-store', store: createKeyvStore() }), + canConnectOrcidProfile: () => true, + canRequestReviews: () => true, + canSeeGatesLogo: true, + canUploadAvatar: () => true, + canUseSearchQueries: () => true, + cloudinaryApi: { cloudName: 'prereview', key: env.CLOUDINARY_API_KEY, secret: env.CLOUDINARY_API_SECRET }, + coarNotifyToken: env.COAR_NOTIFY_TOKEN, + coarNotifyUrl: env.COAR_NOTIFY_URL, + contactEmailAddressStore: new Keyv({ namespace: 'contact-email-address', store: createKeyvStore() }), + environmentLabel: env.ENVIRONMENT_LABEL, + fathomId: env.FATHOM_SITE_ID, + fetch: fetch.defaults({ + cachePath: 'data/cache', + headers: { + 'User-Agent': `PREreview (${env.PUBLIC_URL.href}; mailto:engineering@prereview.org)`, }, - slackApiToken: env.SLACK_API_TOKEN, - slackApiUpdate: env.SLACK_UPDATE, - slackUserIdStore: new Keyv({ namespace: 'slack-user-id', store: createKeyvStore() }), - userOnboardingStore: new Keyv({ namespace: 'user-onboarding', store: createKeyvStore() }), - wasPrereviewRemoved: id => env.REMOVED_PREREVIEWS.includes(id), - zenodoApiKey: env.ZENODO_API_KEY, - zenodoUrl: env.ZENODO_URL, }), - ) -} + formStore: new Keyv({ namespace: 'forms', store: createKeyvStore() }), + careerStageStore: new Keyv({ namespace: 'career-stage', store: createKeyvStore() }), + ghostApi: { + key: env.GHOST_API_KEY, + }, + isOpenForRequestsStore: new Keyv({ namespace: 'is-open-for-requests', store: createKeyvStore() }), + isUserBlocked: user => env.BLOCKED_USERS.includes(user), + legacyPrereviewApi: { + app: env.LEGACY_PREREVIEW_API_APP, + key: env.LEGACY_PREREVIEW_API_KEY, + url: env.LEGACY_PREREVIEW_URL, + update: env.LEGACY_PREREVIEW_UPDATE, + }, + languagesStore: new Keyv({ namespace: 'languages', store: createKeyvStore() }), + locationStore: new Keyv({ namespace: 'location', store: createKeyvStore() }), + ...sendMailEnv, + orcidApiUrl: env.ORCID_API_URL, + orcidApiToken: env.ORCID_API_READ_PUBLIC_TOKEN, + orcidOauth: { + authorizeUrl: new URL(`${env.ORCID_URL.origin}/oauth/authorize`), + clientId: env.ORCID_CLIENT_ID, + clientSecret: env.ORCID_CLIENT_SECRET, + revokeUrl: new URL(`${env.ORCID_URL.origin}/oauth/revoke`), + tokenUrl: new URL(`${env.ORCID_URL.origin}/oauth/token`), + }, + orcidTokenStore: new Keyv({ namespace: 'orcid-token', store: createKeyvStore() }), + publicUrl: env.PUBLIC_URL, + redis, + researchInterestsStore: new Keyv({ namespace: 'research-interests', store: createKeyvStore() }), + reviewRequestStore: new Keyv({ namespace: 'review-request', store: createKeyvStore() }), + scietyListToken: env.SCIETY_LIST_TOKEN, + secret: env.SECRET, + sessionCookie: 'session', + sessionStore: new Keyv({ namespace: 'sessions', store: createKeyvStore(), ttl: 1000 * 60 * 60 * 24 * 30 }), + slackOauth: { + authorizeUrl: new URL('https://slack.com/oauth/v2/authorize'), + clientId: env.SLACK_CLIENT_ID, + clientSecret: env.SLACK_CLIENT_SECRET, + tokenUrl: new URL('https://slack.com/api/oauth.v2.access'), + }, + slackApiToken: env.SLACK_API_TOKEN, + slackApiUpdate: env.SLACK_UPDATE, + slackUserIdStore: new Keyv({ namespace: 'slack-user-id', store: createKeyvStore() }), + userOnboardingStore: new Keyv({ namespace: 'user-onboarding', store: createKeyvStore() }), + wasPrereviewRemoved: id => env.REMOVED_PREREVIEWS.includes(id), + zenodoApiKey: env.ZENODO_API_KEY, + zenodoUrl: env.ZENODO_URL, + }) +}) diff --git a/src/index.ts b/src/index.ts index e360ef650..f848a9f16 100644 --- a/src/index.ts +++ b/src/index.ts @@ -7,11 +7,11 @@ import * as C from 'fp-ts/lib/Console.js' import * as E from 'fp-ts/lib/Either.js' import { pipe } from 'fp-ts/lib/function.js' import type { JsonRecord } from 'fp-ts/lib/Json.js' -import { Redis } from 'ioredis' +import { Redis as IoRedis } from 'ioredis' import * as L from 'logger-fp-ts' import type { app } from './app.js' import { decodeEnv } from './env.js' -import { expressServer } from './ExpressServer.js' +import { expressServer, Redis } from './ExpressServer.js' const env = decodeEnv(process)() @@ -20,7 +20,7 @@ const loggerEnv: L.LoggerEnv = { logger: pipe(C.log, L.withShow(env.LOG_FORMAT === 'json' ? L.JsonShowLogEntry : L.getColoredShow(L.ShowLogEntry))), } -const redis = new Redis(env.REDIS_URI.href, { commandTimeout: 2 * 1000, enableAutoPipelining: true }) +const redis = new IoRedis(env.REDIS_URI.href, { commandTimeout: 2 * 1000, enableAutoPipelining: true }) redis.on('connect', () => L.debug('Redis connected')(loggerEnv)()) redis.on('close', () => L.debug('Redis connection closed')(loggerEnv)()) @@ -67,6 +67,7 @@ pipe( expressServerLifecycle, Layer.scopedDiscard, Layer.launch, - Effect.provideServiceEffect(Express, expressServer(redis)), + Effect.provideServiceEffect(Express, expressServer), + Effect.provideServiceEffect(Redis, Effect.succeed(redis)), NodeRuntime.runMain, )