diff --git a/app/deps.ts b/app/deps.ts index 83b3e07a5d683aee8f569fb3d09965b99482502a..835f4116137a4a01bf611c1e2bc09c7997d98f4f 100644 --- a/app/deps.ts +++ b/app/deps.ts @@ -1,14 +1,10 @@ -export { serve } from "https://deno.land/std@0.155.0/http/server.ts"; -export { - createSchema, - createYoga, - type YogaInitialContext, -} from "npm:graphql-yoga@5.1.0"; -export { Parser } from "https://deno.land/x/html_parser@v0.1.3/src/mod.ts"; -export { ChannelTypeEnum, Novu } from "npm:@novu/node@0.22.0"; -export { GraphQLError } from "npm:graphql@16.8.1"; +export { serve } from 'https://deno.land/std@0.155.0/http/server.ts' +export { createSchema, createYoga, type YogaInitialContext } from 'npm:graphql-yoga@5.1.0' +export { Parser } from 'https://deno.land/x/html_parser@v0.1.3/src/mod.ts' +export { ChannelTypeEnum, Novu } from 'npm:@novu/node@0.22.0' +export { GraphQLError } from 'npm:graphql@16.8.1' // @deno-types="npm:@types/lodash.groupby@4.6.9" -import groupBy from "npm:lodash.groupby@4.6.0"; +import groupBy from 'npm:lodash.groupby@4.6.0' // @deno-types="npm:@types/lodash.partition@4.6.9" -import partition from "npm:lodash.partition@4.6.0"; -export { groupBy, partition }; +import partition from 'npm:lodash.partition@4.6.0' +export { groupBy, partition } diff --git a/app/dev_deps.ts b/app/dev_deps.ts index b5091eba92df8124e1fbb0394ffab7cc16cd4a15..17e3f205b6e40e6f785d7c89da1ce3918343c136 100644 --- a/app/dev_deps.ts +++ b/app/dev_deps.ts @@ -1,17 +1,4 @@ -export { - assertSpyCall, - assertSpyCalls, - returnsNext, - stub, -} from "https://deno.land/std@0.155.0/testing/mock.ts"; -export type { Stub } from "https://deno.land/std@0.155.0/testing/mock.ts"; -export { - assertEquals, - assertRejects, -} from "https://deno.land/std@0.155.0/testing/asserts.ts"; -export { - afterEach, - beforeEach, - describe, - it, -} from "https://deno.land/std@0.155.0/testing/bdd.ts"; +export { assertSpyCall, assertSpyCalls, returnsNext, stub } from 'https://deno.land/std@0.155.0/testing/mock.ts' +export type { Stub } from 'https://deno.land/std@0.155.0/testing/mock.ts' +export { assertEquals, assertRejects } from 'https://deno.land/std@0.155.0/testing/asserts.ts' +export { afterEach, beforeEach, describe, it } from 'https://deno.land/std@0.155.0/testing/bdd.ts' diff --git a/app/logging.ts b/app/logging.ts index dcd266055c37924d48e605d73952e821f0386a28..dbd14d224c7e6bfb584f1c659b39297532979e27 100644 --- a/app/logging.ts +++ b/app/logging.ts @@ -1,14 +1,14 @@ // Google Cloud LogSeverity levels https://cloud.google.com/logging/docs/reference/v2/rest/v2/LogEntry#logseverity export enum LogSeverity { - DEFAULT = "DEFAULT", - DEBUG = "DEBUG", - INFO = "INFO", - NOTICE = "NOTICE", - WARNING = "WARNING", - ERROR = "ERROR", - CRITICAL = "CRITICAL", - ALERT = "ALERT", - EMERGENCY = "EMERGENCY", + DEFAULT = 'DEFAULT', + DEBUG = 'DEBUG', + INFO = 'INFO', + NOTICE = 'NOTICE', + WARNING = 'WARNING', + ERROR = 'ERROR', + CRITICAL = 'CRITICAL', + ALERT = 'ALERT', + EMERGENCY = 'EMERGENCY', } const sortedLevels = [ @@ -21,20 +21,20 @@ const sortedLevels = [ LogSeverity.CRITICAL, LogSeverity.ALERT, LogSeverity.EMERGENCY, -]; +] /** * The Logger class requires a setUpLogger method to be run during the app's initialization in order get the current environment. */ class Logger { - environment = "development"; + environment = 'development' // minimum level to log - level = LogSeverity.DEFAULT; + level = LogSeverity.DEFAULT setUpLogger(environment: string, level: LogSeverity) { - this.environment = environment; - this.level = level; + this.environment = environment + this.level = level } /** @@ -46,50 +46,48 @@ class Logger { * In other environments it should follow Google Cloud's Structured Logging formatting: https://cloud.google.com/logging/docs/structured-logging */ log(severity: LogSeverity, message: string, options = {}) { - if (!shouldLogLevel(this.level, severity)) return; + if (!shouldLogLevel(this.level, severity)) return - const date = new Date().toISOString(); - const input = this.environment === "development" - ? `${date} ${severity} ${message}` - : JSON.stringify({ - environment: this.environment, - severity, - message, - ...options, - }); + const date = new Date().toISOString() + const input = this.environment === 'development' ? `${date} ${severity} ${message}` : JSON.stringify({ + environment: this.environment, + severity, + message, + ...options, + }) switch (severity) { case LogSeverity.DEFAULT: - console.log(input); - break; + console.log(input) + break case LogSeverity.DEBUG: - console.debug(input); - break; + console.debug(input) + break case LogSeverity.INFO: case LogSeverity.NOTICE: - console.info(input); - break; + console.info(input) + break case LogSeverity.WARNING: - console.warn(input); - break; + console.warn(input) + break case LogSeverity.ERROR: case LogSeverity.CRITICAL: case LogSeverity.ALERT: case LogSeverity.EMERGENCY: - console.error(input); + console.error(input) } } debug(message: string, options = {}) { - this.log(LogSeverity.DEBUG, message, options); + this.log(LogSeverity.DEBUG, message, options) } info(message: string, options = {}) { - this.log(LogSeverity.INFO, message, options); + this.log(LogSeverity.INFO, message, options) } error(message: string, options = {}) { - this.log(LogSeverity.ERROR, message, options); + this.log(LogSeverity.ERROR, message, options) } } @@ -99,9 +97,9 @@ class Logger { * @returns Boolean, true if the current level is accepted and false if it is rejected by the minLevel */ const shouldLogLevel = (minLevel: LogSeverity, level: LogSeverity) => { - const minLevelIndex = sortedLevels.indexOf(minLevel); - const currentLevelIndex = sortedLevels.indexOf(level); - return currentLevelIndex >= minLevelIndex; -}; + const minLevelIndex = sortedLevels.indexOf(minLevel) + const currentLevelIndex = sortedLevels.indexOf(level) + return currentLevelIndex >= minLevelIndex +} -export const logger = new Logger(); +export const logger = new Logger() diff --git a/app/main.ts b/app/main.ts index c38604895e5c77def11b988977da916cbc12c305..dface9b91c95e46da775d3b6c5bbfe161f4fbcaa 100644 --- a/app/main.ts +++ b/app/main.ts @@ -1,47 +1,47 @@ -import { logger, LogSeverity } from "./logging.ts"; -import { DEFAULT_PORT, LOCAL_ENVIRONMENT, startServer } from "./server.ts"; +import { logger, LogSeverity } from './logging.ts' +import { DEFAULT_PORT, LOCAL_ENVIRONMENT, startServer } from './server.ts' -const environment = Deno.env.get("ENVIRONMENT") || "development"; +const environment = Deno.env.get('ENVIRONMENT') || 'development' logger.setUpLogger( environment, - environment === "development" ? LogSeverity.DEFAULT : LogSeverity.INFO, -); + environment === 'development' ? LogSeverity.DEFAULT : LogSeverity.INFO, +) const requiredEnv = <T>( name: string, typeFn: (s: string) => T, fallback?: T, ): T => { - const env = Deno.env.get(name); + const env = Deno.env.get(name) if (env === undefined && fallback === undefined) { - throw Error(`Environment variable "${name}" is required`); + throw Error(`Environment variable "${name}" is required`) } else { - return env !== undefined ? typeFn(env) : fallback!; + return env !== undefined ? typeFn(env) : fallback! } -}; +} -const asBoolean = (str: string) => /^true$/i.test(str); +const asBoolean = (str: string) => /^true$/i.test(str) const serverConfigFromEnv = () => { - const fake = requiredEnv("FAKE", asBoolean, false); // For local development. If set, the API returns dummy data + const fake = requiredEnv('FAKE', asBoolean, false) // For local development. If set, the API returns dummy data return { - port: requiredEnv("PORT", Number, DEFAULT_PORT), + port: requiredEnv('PORT', Number, DEFAULT_PORT), fake, novuConfig: { apiKey: requiredEnv( - "NOVU_API_KEY", + 'NOVU_API_KEY', String, - fake ? "dummy value" : undefined, + fake ? 'dummy value' : undefined, ), apiUrl: requiredEnv( - "NOVU_API_URL", + 'NOVU_API_URL', String, - fake ? "dummy value" : undefined, + fake ? 'dummy value' : undefined, ), }, - environment: requiredEnv("ENVIRONMENT", String, LOCAL_ENVIRONMENT), - }; -}; + environment: requiredEnv('ENVIRONMENT', String, LOCAL_ENVIRONMENT), + } +} -await startServer(serverConfigFromEnv()); +await startServer(serverConfigFromEnv()) diff --git a/app/novu.ts b/app/novu.ts index ca053bc69f2dcfdb81d35814be171e2440e64aae..9adc6002f308e2bbd22a27d5a8271fd656eb07ab 100644 --- a/app/novu.ts +++ b/app/novu.ts @@ -1,32 +1,21 @@ -import { - ChannelTypeEnum, - GraphQLError, - groupBy, - Novu, - partition, -} from "./deps.ts"; -import { logger } from "./logging.ts"; -import { - NovuChannelPreferences, - NovuGlobalPreferences, - NovuResponse, - NovuWorkflowPreferences, -} from "./novu_types.ts"; +import { ChannelTypeEnum, GraphQLError, groupBy, Novu, partition } from './deps.ts' +import { logger } from './logging.ts' +import { NovuChannelPreferences, NovuGlobalPreferences, NovuResponse, NovuWorkflowPreferences } from './novu_types.ts' import { ChannelPreferences, PreferencesResponse, UpdatePreferencesInput, WorkflowGroupPreferences, WorkflowTemplatePreferences, -} from "./types.ts"; +} from './types.ts' export interface NovuConfig { - apiKey: string; - apiUrl: string; + apiKey: string + apiUrl: string } -const CATEGORY_PREFIX = "CATEGORY_"; -const CATEGORY_GLOBAL = "GLOBAL"; +const CATEGORY_PREFIX = 'CATEGORY_' +const CATEGORY_GLOBAL = 'GLOBAL' // Differentiate between false (channel is supported, but not enabled) and null/undefined (channel is not supported) const determineEnabledValue = ( @@ -34,19 +23,19 @@ const determineEnabledValue = ( valueB?: boolean | null, ): boolean | null | undefined => { if (valueA === undefined || valueA === null || valueA === valueB) { - return valueB; + return valueB } if (valueB === undefined || valueA === null) { - return valueA; + return valueA } - return true; -}; + return true +} export const createNovuInstance = (config: NovuConfig) => new Novu(config.apiKey, { backendUrl: config.apiUrl, // TODO: specify retryConfig - }); + }) const aggregatePreferences = (workflows: WorkflowTemplatePreferences[]) => workflows.reduce( @@ -58,7 +47,7 @@ const aggregatePreferences = (workflows: WorkflowTemplatePreferences[]) => ), }), {} as ChannelPreferences, - ); + ) const aggregateGroupedWorkflowPreferences = ( workflows: WorkflowTemplatePreferences[], @@ -69,7 +58,7 @@ const aggregateGroupedWorkflowPreferences = ( category: workflows[0].category, id, preferences: aggregatePreferences(workflows), - })); + })) const transformChannelPreferences = ( preferences: NovuChannelPreferences, @@ -77,24 +66,24 @@ const transformChannelPreferences = ( const { [ChannelTypeEnum.EMAIL]: email, [ChannelTypeEnum.PUSH]: push, - } = preferences; - return { email, push }; -}; + } = preferences + return { email, push } +} const transformWorkflowPreferences = ( workflow: NovuWorkflowPreferences, ): WorkflowTemplatePreferences => { - const [[category = ""], [id = ""]] = partition( + const [[category = ''], [id = '']] = partition( workflow.template.tags, (tag) => tag.startsWith(CATEGORY_PREFIX), - ); + ) return { id, - category: category.replace(CATEGORY_PREFIX, ""), + category: category.replace(CATEGORY_PREFIX, ''), templateId: workflow.template._id, preferences: transformChannelPreferences(workflow.preference.channels), - }; -}; + } +} const createGlobalPreferences = ( preferences: NovuGlobalPreferences, @@ -102,18 +91,17 @@ const createGlobalPreferences = ( id: CATEGORY_GLOBAL, category: CATEGORY_GLOBAL, preferences: transformChannelPreferences(preferences.preference.channels), -}); +}) const fetchGlobalPreferences = (novu: Novu, subscriberId: string) => novu.subscribers.getGlobalPreference(subscriberId).then( - ({ data }: { data: NovuResponse<NovuGlobalPreferences[]> }) => - createGlobalPreferences(data.data[0]), + ({ data }: { data: NovuResponse<NovuGlobalPreferences[]> }) => createGlobalPreferences(data.data[0]), ).catch((error) => { logger.error( `Failed to retrieve global preferences for subscriber: ${error.message}`, - ); - throw error; - }); + ) + throw error + }) const fetchWorkflowTemplatePreferences = ( novu: Novu, @@ -121,33 +109,33 @@ const fetchWorkflowTemplatePreferences = ( ) => novu.subscribers.getPreference(subscriberId).then( ({ data }: { data: NovuResponse<NovuWorkflowPreferences[]> }) => { - return data.data.map(transformWorkflowPreferences); + return data.data.map(transformWorkflowPreferences) }, ).catch((error) => { logger.error( `Failed to retrieve workflow template preferences for subscriber: ${error.message}`, - ); - throw error; - }); + ) + throw error + }) export const fetchPreferences = async ( novu: Novu, subscriberId: string | undefined, ): Promise<PreferencesResponse> => { if (!subscriberId) { - throw new GraphQLError("Unauthorized", { - extensions: { "code": "UNAUTHORIZED" }, - }); + throw new GraphQLError('Unauthorized', { + extensions: { 'code': 'UNAUTHORIZED' }, + }) } const [globalPreferences, templatePreferences] = await Promise.all([ fetchGlobalPreferences(novu, subscriberId), fetchWorkflowTemplatePreferences(novu, subscriberId), - ]); + ]) return aggregateGroupedWorkflowPreferences( [globalPreferences].concat(templatePreferences), - ); -}; + ) +} const applyUpdate = async ( novu: Novu, @@ -158,39 +146,39 @@ const applyUpdate = async ( // Ensure the channel is enabled globally if requested for workflow const enableChannelGlobally = workflowPreferences.id === CATEGORY_GLOBAL && input.id !== CATEGORY_GLOBAL && input.enabled && - !workflowPreferences.preferences[input.channel]; + !workflowPreferences.preferences[input.channel] // Leave preferences unchanged if not matching and not global change if ( !([CATEGORY_GLOBAL, workflowPreferences.id].includes(input.id)) && !enableChannelGlobally ) { - return workflowPreferences; + return workflowPreferences } // Return preferences unchanged if there is nothing to change or the channel // is not supported - const currentPreference = workflowPreferences.preferences[input.channel]; + const currentPreference = workflowPreferences.preferences[input.channel] if (currentPreference === undefined || currentPreference === input.enabled) { - return workflowPreferences; + return workflowPreferences } const channelPreferencesChange = { type: input.channel, enabled: input.enabled, - }; + } const updatedPreferences = { ...workflowPreferences, preferences: { ...workflowPreferences.preferences, [input.channel]: input.enabled, }, - }; + } if ( workflowPreferences.id === CATEGORY_GLOBAL || !workflowPreferences.templateId ) { await novu.subscribers.updateGlobalPreference(subscriberId, { preferences: [channelPreferencesChange], - }); - return updatedPreferences; + }) + return updatedPreferences } await novu.subscribers.updatePreference( subscriberId, @@ -198,9 +186,9 @@ const applyUpdate = async ( { channel: channelPreferencesChange, }, - ); - return updatedPreferences; -}; + ) + return updatedPreferences +} export const updatePreferences = async ( novu: Novu, @@ -208,21 +196,19 @@ export const updatePreferences = async ( input: UpdatePreferencesInput, ) => { if (!subscriberId) { - throw new GraphQLError("Unauthorized", { - extensions: { "code": "UNAUTHORIZED" }, - }); + throw new GraphQLError('Unauthorized', { + extensions: { 'code': 'UNAUTHORIZED' }, + }) } const [globalPreferences, templatePreferences] = await Promise.all([ fetchGlobalPreferences(novu, subscriberId), fetchWorkflowTemplatePreferences(novu, subscriberId), - ]); + ]) const updatedPreferences = await Promise.all( [globalPreferences].concat(templatePreferences) - .map((preferences) => - applyUpdate(novu, subscriberId, preferences, input) - ), - ); + .map((preferences) => applyUpdate(novu, subscriberId, preferences, input)), + ) - return aggregateGroupedWorkflowPreferences(updatedPreferences); -}; + return aggregateGroupedWorkflowPreferences(updatedPreferences) +} diff --git a/app/novu_test.ts b/app/novu_test.ts index a570967e92d13d7d8c5c190dd3771ab157395a8e..18c0ebbe72b537090193f16e83c64c2dd7edc0f2 100644 --- a/app/novu_test.ts +++ b/app/novu_test.ts @@ -7,495 +7,490 @@ import { describe, it, Stub, -} from "./dev_deps.ts"; -import { ChannelTypeEnum, GraphQLError } from "./deps.ts"; +} from './dev_deps.ts' +import { ChannelTypeEnum, GraphQLError } from './deps.ts' -import { fetchPreferences, updatePreferences } from "./novu.ts"; +import { fetchPreferences, updatePreferences } from './novu.ts' import { stubNovuGlobalPreferences, stubNovuUpdateGlobalPreferences, stubNovuUpdateWorkflowPreferences, stubNovuWorkflowPreferences, -} from "./test_helpers.ts"; -import { - globalUpdate, - novuMock, - preferencesAllEnabled, - subscriberId, -} from "./test_data.ts"; - -describe("novu", () => { - let novuGlobalPreferenceStub: Stub; - let novuPreferenceStub: Stub; - let novuUpdateGlobalPreferenceStub: Stub; - let novuUpdatePreferenceStub: Stub; +} from './test_helpers.ts' +import { globalUpdate, novuMock, preferencesAllEnabled, subscriberId } from './test_data.ts' + +describe('novu', () => { + let novuGlobalPreferenceStub: Stub + let novuPreferenceStub: Stub + let novuUpdateGlobalPreferenceStub: Stub + let novuUpdatePreferenceStub: Stub afterEach(() => { if (!novuGlobalPreferenceStub?.restored) { - novuGlobalPreferenceStub?.restore(); + novuGlobalPreferenceStub?.restore() } if (!novuPreferenceStub?.restored) { - novuPreferenceStub?.restore(); + novuPreferenceStub?.restore() } if (!novuUpdateGlobalPreferenceStub?.restored) { - novuUpdateGlobalPreferenceStub?.restore(); + novuUpdateGlobalPreferenceStub?.restore() } if (!novuUpdatePreferenceStub?.restored) { - novuUpdatePreferenceStub?.restore(); + novuUpdatePreferenceStub?.restore() } - }); + }) - describe("fetching preferences", () => { - it("fetches global and workflow preferences for subscriber", async () => { - novuGlobalPreferenceStub = stubNovuGlobalPreferences(); - novuPreferenceStub = stubNovuWorkflowPreferences(); + describe('fetching preferences', () => { + it('fetches global and workflow preferences for subscriber', async () => { + novuGlobalPreferenceStub = stubNovuGlobalPreferences() + novuPreferenceStub = stubNovuWorkflowPreferences() - await fetchPreferences(novuMock, subscriberId); + await fetchPreferences(novuMock, subscriberId) assertSpyCall(novuGlobalPreferenceStub, 0, { self: novuMock.subscribers, args: [subscriberId], - }); + }) assertSpyCall(novuPreferenceStub, 0, { self: novuMock.subscribers, args: [subscriberId], - }); - }); + }) + }) - it("throws unauthorized error for missing subscriber id", async () => { + it('throws unauthorized error for missing subscriber id', async () => { await assertRejects( () => fetchPreferences(novuMock, undefined), GraphQLError, - "Unauthorized", - ); - }); + 'Unauthorized', + ) + }) - it("throws unauthorized error for empty subscriber id", async () => { + it('throws unauthorized error for empty subscriber id', async () => { await assertRejects( - () => fetchPreferences(novuMock, ""), + () => fetchPreferences(novuMock, ''), GraphQLError, - "Unauthorized", - ); - }); + 'Unauthorized', + ) + }) - it("transforms preferences and groups workflow templates", async () => { + it('transforms preferences and groups workflow templates', async () => { const expectedPreferences = [ { - id: "GLOBAL", - category: "GLOBAL", + id: 'GLOBAL', + category: 'GLOBAL', preferences: preferencesAllEnabled, }, { - id: "SPACE_NEW_POST", - category: "SPACES", + id: 'SPACE_NEW_POST', + category: 'SPACES', preferences: { push: false, email: undefined }, }, { - id: "SPACE_MEMBERSHIP_REQUEST", - category: "SPACES", + id: 'SPACE_MEMBERSHIP_REQUEST', + category: 'SPACES', preferences: preferencesAllEnabled, }, - ]; - novuGlobalPreferenceStub = stubNovuGlobalPreferences(); - novuPreferenceStub = stubNovuWorkflowPreferences(); + ] + novuGlobalPreferenceStub = stubNovuGlobalPreferences() + novuPreferenceStub = stubNovuWorkflowPreferences() - const result = await fetchPreferences(novuMock, subscriberId); + const result = await fetchPreferences(novuMock, subscriberId) - assertEquals(result, expectedPreferences); - }); + assertEquals(result, expectedPreferences) + }) - it("only returns global preferences if no workflows were fetched", async () => { + it('only returns global preferences if no workflows were fetched', async () => { const expectedPreferences = [ { - id: "GLOBAL", - category: "GLOBAL", + id: 'GLOBAL', + category: 'GLOBAL', preferences: preferencesAllEnabled, }, - ]; - novuGlobalPreferenceStub = stubNovuGlobalPreferences(); - novuPreferenceStub = stubNovuWorkflowPreferences([]); + ] + novuGlobalPreferenceStub = stubNovuGlobalPreferences() + novuPreferenceStub = stubNovuWorkflowPreferences([]) - const result = await fetchPreferences(novuMock, subscriberId); + const result = await fetchPreferences(novuMock, subscriberId) - assertEquals(result, expectedPreferences); - }); + assertEquals(result, expectedPreferences) + }) - it("throws error if fetching global preferences fails", async () => { - novuGlobalPreferenceStub = stubNovuGlobalPreferences(new Error("Test")); - novuPreferenceStub = stubNovuWorkflowPreferences(); + it('throws error if fetching global preferences fails', async () => { + novuGlobalPreferenceStub = stubNovuGlobalPreferences(new Error('Test')) + novuPreferenceStub = stubNovuWorkflowPreferences() await assertRejects( () => fetchPreferences(novuMock, subscriberId), Error, - "Test", - ); - }); + 'Test', + ) + }) - it("throws error if fetching workflow preferences fails", async () => { - novuGlobalPreferenceStub = stubNovuGlobalPreferences(); - novuPreferenceStub = stubNovuWorkflowPreferences(new Error("Test")); + it('throws error if fetching workflow preferences fails', async () => { + novuGlobalPreferenceStub = stubNovuGlobalPreferences() + novuPreferenceStub = stubNovuWorkflowPreferences(new Error('Test')) await assertRejects( () => fetchPreferences(novuMock, subscriberId), Error, - "Test", - ); - }); + 'Test', + ) + }) - it("throws error if global preferences are invalid", async () => { - novuGlobalPreferenceStub = stubNovuGlobalPreferences([]); - novuPreferenceStub = stubNovuWorkflowPreferences(); + it('throws error if global preferences are invalid', async () => { + novuGlobalPreferenceStub = stubNovuGlobalPreferences([]) + novuPreferenceStub = stubNovuWorkflowPreferences() await assertRejects( () => fetchPreferences(novuMock, subscriberId), Error, - "Cannot read properties of undefined", - ); - }); + 'Cannot read properties of undefined', + ) + }) - it("handles workflow templates without tags", async () => { + it('handles workflow templates without tags', async () => { const expectedPreferences = [ { - id: "GLOBAL", - category: "GLOBAL", + id: 'GLOBAL', + category: 'GLOBAL', preferences: { push: true, email: true }, }, { - id: "", - category: "", + id: '', + category: '', preferences: { push: false, email: undefined }, }, - ]; - novuGlobalPreferenceStub = stubNovuGlobalPreferences(); + ] + novuGlobalPreferenceStub = stubNovuGlobalPreferences() novuPreferenceStub = stubNovuWorkflowPreferences([ { preference: { enabled: true, channels: { - "push": false, - "in_app": true, + 'push': false, + 'in_app': true, }, }, template: { - _id: "space-new-post", + _id: 'space-new-post', tags: [], }, }, - ]); - novuUpdateGlobalPreferenceStub = stubNovuUpdateGlobalPreferences(); - novuUpdatePreferenceStub = stubNovuUpdateWorkflowPreferences(); + ]) + novuUpdateGlobalPreferenceStub = stubNovuUpdateGlobalPreferences() + novuUpdatePreferenceStub = stubNovuUpdateWorkflowPreferences() - const result = await fetchPreferences(novuMock, subscriberId); + const result = await fetchPreferences(novuMock, subscriberId) - assertEquals(result, expectedPreferences); - assertSpyCalls(novuUpdatePreferenceStub, 0); - }); - }); + assertEquals(result, expectedPreferences) + assertSpyCalls(novuUpdatePreferenceStub, 0) + }) + }) - describe("updating preferences", () => { - it("fetches global and workflow preferences for subscriber", async () => { - novuGlobalPreferenceStub = stubNovuGlobalPreferences(); - novuPreferenceStub = stubNovuWorkflowPreferences(); - novuUpdateGlobalPreferenceStub = stubNovuUpdateGlobalPreferences(); - novuUpdatePreferenceStub = stubNovuUpdateWorkflowPreferences(); + describe('updating preferences', () => { + it('fetches global and workflow preferences for subscriber', async () => { + novuGlobalPreferenceStub = stubNovuGlobalPreferences() + novuPreferenceStub = stubNovuWorkflowPreferences() + novuUpdateGlobalPreferenceStub = stubNovuUpdateGlobalPreferences() + novuUpdatePreferenceStub = stubNovuUpdateWorkflowPreferences() - await updatePreferences(novuMock, subscriberId, globalUpdate); + await updatePreferences(novuMock, subscriberId, globalUpdate) assertSpyCall(novuGlobalPreferenceStub, 0, { self: novuMock.subscribers, args: [subscriberId], - }); + }) assertSpyCall(novuPreferenceStub, 0, { self: novuMock.subscribers, args: [subscriberId], - }); - }); + }) + }) - it("throws unauthorized error for missing subscriber id", async () => { + it('throws unauthorized error for missing subscriber id', async () => { await assertRejects( () => updatePreferences(novuMock, undefined, globalUpdate), GraphQLError, - "Unauthorized", - ); - }); + 'Unauthorized', + ) + }) - it("throws unauthorized error for empty subscriber id", async () => { + it('throws unauthorized error for empty subscriber id', async () => { await assertRejects( - () => updatePreferences(novuMock, "", globalUpdate), + () => updatePreferences(novuMock, '', globalUpdate), GraphQLError, - "Unauthorized", - ); - }); + 'Unauthorized', + ) + }) - it("throws error if fetching global preferences fails", async () => { - novuGlobalPreferenceStub = stubNovuGlobalPreferences(new Error("Test")); - novuPreferenceStub = stubNovuWorkflowPreferences(); - novuUpdateGlobalPreferenceStub = stubNovuUpdateGlobalPreferences(); - novuUpdatePreferenceStub = stubNovuUpdateWorkflowPreferences(); + it('throws error if fetching global preferences fails', async () => { + novuGlobalPreferenceStub = stubNovuGlobalPreferences(new Error('Test')) + novuPreferenceStub = stubNovuWorkflowPreferences() + novuUpdateGlobalPreferenceStub = stubNovuUpdateGlobalPreferences() + novuUpdatePreferenceStub = stubNovuUpdateWorkflowPreferences() await assertRejects( () => updatePreferences(novuMock, subscriberId, globalUpdate), Error, - "Test", - ); - }); + 'Test', + ) + }) - it("throws error if fetching workflow preferences fails", async () => { - novuGlobalPreferenceStub = stubNovuGlobalPreferences(); - novuPreferenceStub = stubNovuWorkflowPreferences(new Error("Test")); - novuUpdateGlobalPreferenceStub = stubNovuUpdateGlobalPreferences(); - novuUpdatePreferenceStub = stubNovuUpdateWorkflowPreferences(); + it('throws error if fetching workflow preferences fails', async () => { + novuGlobalPreferenceStub = stubNovuGlobalPreferences() + novuPreferenceStub = stubNovuWorkflowPreferences(new Error('Test')) + novuUpdateGlobalPreferenceStub = stubNovuUpdateGlobalPreferences() + novuUpdatePreferenceStub = stubNovuUpdateWorkflowPreferences() await assertRejects( () => updatePreferences(novuMock, subscriberId, globalUpdate), Error, - "Test", - ); - }); + 'Test', + ) + }) - it("throws error if global preferences are invalid", async () => { - novuGlobalPreferenceStub = stubNovuGlobalPreferences([]); - novuPreferenceStub = stubNovuWorkflowPreferences(); - novuUpdateGlobalPreferenceStub = stubNovuUpdateGlobalPreferences(); - novuUpdatePreferenceStub = stubNovuUpdateWorkflowPreferences(); + it('throws error if global preferences are invalid', async () => { + novuGlobalPreferenceStub = stubNovuGlobalPreferences([]) + novuPreferenceStub = stubNovuWorkflowPreferences() + novuUpdateGlobalPreferenceStub = stubNovuUpdateGlobalPreferences() + novuUpdatePreferenceStub = stubNovuUpdateWorkflowPreferences() await assertRejects( () => updatePreferences(novuMock, subscriberId, globalUpdate), Error, - "Cannot read properties of undefined", - ); - }); + 'Cannot read properties of undefined', + ) + }) - it("throws error if updating global preferences fails", async () => { - novuGlobalPreferenceStub = stubNovuGlobalPreferences(); - novuPreferenceStub = stubNovuWorkflowPreferences(); + it('throws error if updating global preferences fails', async () => { + novuGlobalPreferenceStub = stubNovuGlobalPreferences() + novuPreferenceStub = stubNovuWorkflowPreferences() novuUpdateGlobalPreferenceStub = stubNovuUpdateGlobalPreferences( - new Error("Test"), - ); - novuUpdatePreferenceStub = stubNovuUpdateWorkflowPreferences(); + new Error('Test'), + ) + novuUpdatePreferenceStub = stubNovuUpdateWorkflowPreferences() await assertRejects( () => updatePreferences(novuMock, subscriberId, globalUpdate), Error, - "Test", - ); - }); - - it("throws error if updating workflow preferences fails", async () => { - novuGlobalPreferenceStub = stubNovuGlobalPreferences(); - novuPreferenceStub = stubNovuWorkflowPreferences(); - novuUpdateGlobalPreferenceStub = stubNovuUpdateGlobalPreferences(); + 'Test', + ) + }) + + it('throws error if updating workflow preferences fails', async () => { + novuGlobalPreferenceStub = stubNovuGlobalPreferences() + novuPreferenceStub = stubNovuWorkflowPreferences() + novuUpdateGlobalPreferenceStub = stubNovuUpdateGlobalPreferences() novuUpdatePreferenceStub = stubNovuUpdateWorkflowPreferences( - new Error("Test"), - ); + new Error('Test'), + ) await assertRejects( () => updatePreferences(novuMock, subscriberId, globalUpdate), Error, - "Test", - ); - }); + 'Test', + ) + }) - it("applies global update globally and to every workflow", async () => { - novuGlobalPreferenceStub = stubNovuGlobalPreferences(); - novuPreferenceStub = stubNovuWorkflowPreferences(); - novuUpdateGlobalPreferenceStub = stubNovuUpdateGlobalPreferences(); - novuUpdatePreferenceStub = stubNovuUpdateWorkflowPreferences(); + it('applies global update globally and to every workflow', async () => { + novuGlobalPreferenceStub = stubNovuGlobalPreferences() + novuPreferenceStub = stubNovuWorkflowPreferences() + novuUpdateGlobalPreferenceStub = stubNovuUpdateGlobalPreferences() + novuUpdatePreferenceStub = stubNovuUpdateWorkflowPreferences() - await updatePreferences(novuMock, subscriberId, globalUpdate); + await updatePreferences(novuMock, subscriberId, globalUpdate) assertSpyCall(novuUpdateGlobalPreferenceStub, 0, { self: novuMock.subscribers, args: [subscriberId, { - preferences: [{ enabled: false, type: "push" }], + preferences: [{ enabled: false, type: 'push' }], }], - }); - assertSpyCalls(novuUpdatePreferenceStub, 2); + }) + assertSpyCalls(novuUpdatePreferenceStub, 2) assertSpyCall(novuUpdatePreferenceStub, 0, { self: novuMock.subscribers, - args: [subscriberId, "space-membership-request-accept", { - channel: { enabled: false, type: "push" }, + args: [subscriberId, 'space-membership-request-accept', { + channel: { enabled: false, type: 'push' }, }], - }); + }) assertSpyCall(novuUpdatePreferenceStub, 1, { self: novuMock.subscribers, - args: [subscriberId, "space-membership-request-decline", { - channel: { enabled: false, type: "push" }, + args: [subscriberId, 'space-membership-request-decline', { + channel: { enabled: false, type: 'push' }, }], - }); - }); + }) + }) - it("only applies updates to workflows if channel is supported", async () => { - novuGlobalPreferenceStub = stubNovuGlobalPreferences(); - novuPreferenceStub = stubNovuWorkflowPreferences(); - novuUpdateGlobalPreferenceStub = stubNovuUpdateGlobalPreferences(); - novuUpdatePreferenceStub = stubNovuUpdateWorkflowPreferences(); + it('only applies updates to workflows if channel is supported', async () => { + novuGlobalPreferenceStub = stubNovuGlobalPreferences() + novuPreferenceStub = stubNovuWorkflowPreferences() + novuUpdateGlobalPreferenceStub = stubNovuUpdateGlobalPreferences() + novuUpdatePreferenceStub = stubNovuUpdateWorkflowPreferences() await updatePreferences(novuMock, subscriberId, { ...globalUpdate, channel: ChannelTypeEnum.EMAIL, - }); + }) - assertSpyCalls(novuUpdatePreferenceStub, 1); + assertSpyCalls(novuUpdatePreferenceStub, 1) assertSpyCall(novuUpdatePreferenceStub, 0, { self: novuMock.subscribers, - args: [subscriberId, "space-membership-request-accept", { - channel: { enabled: false, type: "email" }, + args: [subscriberId, 'space-membership-request-accept', { + channel: { enabled: false, type: 'email' }, }], - }); - }); + }) + }) - it("only applies updates to workflows if preference is changed", async () => { - novuGlobalPreferenceStub = stubNovuGlobalPreferences(); - novuPreferenceStub = stubNovuWorkflowPreferences(); - novuUpdateGlobalPreferenceStub = stubNovuUpdateGlobalPreferences(); - novuUpdatePreferenceStub = stubNovuUpdateWorkflowPreferences(); + it('only applies updates to workflows if preference is changed', async () => { + novuGlobalPreferenceStub = stubNovuGlobalPreferences() + novuPreferenceStub = stubNovuWorkflowPreferences() + novuUpdateGlobalPreferenceStub = stubNovuUpdateGlobalPreferences() + novuUpdatePreferenceStub = stubNovuUpdateWorkflowPreferences() await updatePreferences(novuMock, subscriberId, { ...globalUpdate, enabled: true, - }); + }) - assertSpyCalls(novuUpdateGlobalPreferenceStub, 0); - assertSpyCalls(novuUpdatePreferenceStub, 1); + assertSpyCalls(novuUpdateGlobalPreferenceStub, 0) + assertSpyCalls(novuUpdatePreferenceStub, 1) assertSpyCall(novuUpdatePreferenceStub, 0, { self: novuMock.subscribers, - args: [subscriberId, "space-new-post", { - channel: { enabled: true, type: "push" }, + args: [subscriberId, 'space-new-post', { + channel: { enabled: true, type: 'push' }, }], - }); - }); + }) + }) - it("applies workflow specific update", async () => { - novuGlobalPreferenceStub = stubNovuGlobalPreferences(); - novuPreferenceStub = stubNovuWorkflowPreferences(); - novuUpdateGlobalPreferenceStub = stubNovuUpdateGlobalPreferences(); - novuUpdatePreferenceStub = stubNovuUpdateWorkflowPreferences(); + it('applies workflow specific update', async () => { + novuGlobalPreferenceStub = stubNovuGlobalPreferences() + novuPreferenceStub = stubNovuWorkflowPreferences() + novuUpdateGlobalPreferenceStub = stubNovuUpdateGlobalPreferences() + novuUpdatePreferenceStub = stubNovuUpdateWorkflowPreferences() await updatePreferences(novuMock, subscriberId, { ...globalUpdate, - id: "SPACE_MEMBERSHIP_REQUEST", - }); + id: 'SPACE_MEMBERSHIP_REQUEST', + }) - assertSpyCalls(novuUpdateGlobalPreferenceStub, 0); - assertSpyCalls(novuUpdatePreferenceStub, 2); + assertSpyCalls(novuUpdateGlobalPreferenceStub, 0) + assertSpyCalls(novuUpdatePreferenceStub, 2) assertSpyCall(novuUpdatePreferenceStub, 0, { self: novuMock.subscribers, - args: [subscriberId, "space-membership-request-accept", { - channel: { enabled: false, type: "push" }, + args: [subscriberId, 'space-membership-request-accept', { + channel: { enabled: false, type: 'push' }, }], - }); + }) assertSpyCall(novuUpdatePreferenceStub, 1, { self: novuMock.subscribers, - args: [subscriberId, "space-membership-request-decline", { - channel: { enabled: false, type: "push" }, + args: [subscriberId, 'space-membership-request-decline', { + channel: { enabled: false, type: 'push' }, }], - }); - }); + }) + }) - it("enables channel globally during workflow update if necessary", async () => { + it('enables channel globally during workflow update if necessary', async () => { novuGlobalPreferenceStub = stubNovuGlobalPreferences([{ preference: { enabled: true, channels: { - "push": false, - "email": true, + 'push': false, + 'email': true, }, }, - }]); - novuPreferenceStub = stubNovuWorkflowPreferences(); - novuUpdateGlobalPreferenceStub = stubNovuUpdateGlobalPreferences(); - novuUpdatePreferenceStub = stubNovuUpdateWorkflowPreferences(); + }]) + novuPreferenceStub = stubNovuWorkflowPreferences() + novuUpdateGlobalPreferenceStub = stubNovuUpdateGlobalPreferences() + novuUpdatePreferenceStub = stubNovuUpdateWorkflowPreferences() await updatePreferences(novuMock, subscriberId, { - id: "SPACE_NEW_POST", + id: 'SPACE_NEW_POST', channel: ChannelTypeEnum.PUSH, enabled: true, - }); + }) assertSpyCall(novuUpdateGlobalPreferenceStub, 0, { self: novuMock.subscribers, args: [subscriberId, { - preferences: [{ enabled: true, type: "push" }], + preferences: [{ enabled: true, type: 'push' }], }], - }); - assertSpyCalls(novuUpdatePreferenceStub, 1); + }) + assertSpyCalls(novuUpdatePreferenceStub, 1) assertSpyCall(novuUpdatePreferenceStub, 0, { self: novuMock.subscribers, - args: [subscriberId, "space-new-post", { - channel: { enabled: true, type: "push" }, + args: [subscriberId, 'space-new-post', { + channel: { enabled: true, type: 'push' }, }], - }); - }); + }) + }) - it("does nothing for unknown workflows", async () => { - novuGlobalPreferenceStub = stubNovuGlobalPreferences(); - novuPreferenceStub = stubNovuWorkflowPreferences(); - novuUpdateGlobalPreferenceStub = stubNovuUpdateGlobalPreferences(); - novuUpdatePreferenceStub = stubNovuUpdateWorkflowPreferences(); + it('does nothing for unknown workflows', async () => { + novuGlobalPreferenceStub = stubNovuGlobalPreferences() + novuPreferenceStub = stubNovuWorkflowPreferences() + novuUpdateGlobalPreferenceStub = stubNovuUpdateGlobalPreferences() + novuUpdatePreferenceStub = stubNovuUpdateWorkflowPreferences() await updatePreferences(novuMock, subscriberId, { ...globalUpdate, - id: "UNKNOWN", - }); + id: 'UNKNOWN', + }) - assertSpyCalls(novuUpdateGlobalPreferenceStub, 0); - assertSpyCalls(novuUpdatePreferenceStub, 0); - }); + assertSpyCalls(novuUpdateGlobalPreferenceStub, 0) + assertSpyCalls(novuUpdatePreferenceStub, 0) + }) - it("returns aggregation of updated preferences", async () => { + it('returns aggregation of updated preferences', async () => { const expectedPreferences = [ { - id: "GLOBAL", - category: "GLOBAL", + id: 'GLOBAL', + category: 'GLOBAL', preferences: { push: false, email: true }, }, { - id: "SPACE_NEW_POST", - category: "SPACES", + id: 'SPACE_NEW_POST', + category: 'SPACES', preferences: { push: false, email: undefined }, }, { - id: "SPACE_MEMBERSHIP_REQUEST", - category: "SPACES", + id: 'SPACE_MEMBERSHIP_REQUEST', + category: 'SPACES', preferences: { push: false, email: true }, }, - ]; - novuGlobalPreferenceStub = stubNovuGlobalPreferences(); - novuPreferenceStub = stubNovuWorkflowPreferences(); - novuUpdateGlobalPreferenceStub = stubNovuUpdateGlobalPreferences(); - novuUpdatePreferenceStub = stubNovuUpdateWorkflowPreferences(); + ] + novuGlobalPreferenceStub = stubNovuGlobalPreferences() + novuPreferenceStub = stubNovuWorkflowPreferences() + novuUpdateGlobalPreferenceStub = stubNovuUpdateGlobalPreferences() + novuUpdatePreferenceStub = stubNovuUpdateWorkflowPreferences() const result = await updatePreferences( novuMock, subscriberId, globalUpdate, - ); + ) - assertEquals(result, expectedPreferences); - }); + assertEquals(result, expectedPreferences) + }) - it("handles empty list of workflow templates", async () => { + it('handles empty list of workflow templates', async () => { const expectedPreferences = [ { - id: "GLOBAL", - category: "GLOBAL", + id: 'GLOBAL', + category: 'GLOBAL', preferences: { push: false, email: true }, }, - ]; - novuGlobalPreferenceStub = stubNovuGlobalPreferences(); - novuPreferenceStub = stubNovuWorkflowPreferences([]); - novuUpdateGlobalPreferenceStub = stubNovuUpdateGlobalPreferences(); - novuUpdatePreferenceStub = stubNovuUpdateWorkflowPreferences(); + ] + novuGlobalPreferenceStub = stubNovuGlobalPreferences() + novuPreferenceStub = stubNovuWorkflowPreferences([]) + novuUpdateGlobalPreferenceStub = stubNovuUpdateGlobalPreferences() + novuUpdatePreferenceStub = stubNovuUpdateWorkflowPreferences() const result = await updatePreferences( novuMock, subscriberId, globalUpdate, - ); + ) - assertEquals(result, expectedPreferences); - assertSpyCalls(novuUpdatePreferenceStub, 0); - }); - }); -}); + assertEquals(result, expectedPreferences) + assertSpyCalls(novuUpdatePreferenceStub, 0) + }) + }) +}) diff --git a/app/novu_types.ts b/app/novu_types.ts index a4d5386e1b1f5256e2cde02996dc108cf4262cab..12301ffdbbb21b1ba560fe945d33a02cbee1ba11 100644 --- a/app/novu_types.ts +++ b/app/novu_types.ts @@ -1,26 +1,26 @@ -import { ChannelTypeEnum } from "./deps.ts"; +import { ChannelTypeEnum } from './deps.ts' export interface NovuResponse<T> { - data: T; + data: T } interface NovuWorkflowTemplate { - _id: string; - tags: string[]; + _id: string + tags: string[] } export type NovuChannelPreferences = { - [key in ChannelTypeEnum]?: boolean; -}; + [key in ChannelTypeEnum]?: boolean +} interface NovuPreferences { - enabled: boolean; - channels: NovuChannelPreferences; + enabled: boolean + channels: NovuChannelPreferences } export interface NovuWorkflowPreferences { - template: NovuWorkflowTemplate; - preference: NovuPreferences; + template: NovuWorkflowTemplate + preference: NovuPreferences } -export type NovuGlobalPreferences = Omit<NovuWorkflowPreferences, "template">; +export type NovuGlobalPreferences = Omit<NovuWorkflowPreferences, 'template'> diff --git a/app/server.ts b/app/server.ts index ccec357454358e80f25567c64078b6fdc96b2532..a34c612a2b6da6081714cc41ca532e64d202d193 100644 --- a/app/server.ts +++ b/app/server.ts @@ -1,33 +1,22 @@ -import { - createSchema, - createYoga, - Novu, - serve, - YogaInitialContext, -} from "./deps.ts"; -import { logger } from "./logging.ts"; -import { - createNovuInstance, - fetchPreferences, - NovuConfig, - updatePreferences, -} from "./novu.ts"; -import { PreferencesResponse, UpdatePreferencesParameters } from "./types.ts"; +import { createSchema, createYoga, Novu, serve, YogaInitialContext } from './deps.ts' +import { logger } from './logging.ts' +import { createNovuInstance, fetchPreferences, NovuConfig, updatePreferences } from './novu.ts' +import { PreferencesResponse, UpdatePreferencesParameters } from './types.ts' -export const DEFAULT_PORT = 8005; -export const HEADER_USER_ID = "x-holi-user-id"; -export const LOCAL_ENVIRONMENT = "local"; +export const DEFAULT_PORT = 8005 +export const HEADER_USER_ID = 'x-holi-user-id' +export const LOCAL_ENVIRONMENT = 'local' export interface ServerConfig { - port: number; // default: 8005 - fake: boolean; // For local development. If set, the API returns dummy data - novuConfig: NovuConfig; - environment: string; + port: number // default: 8005 + fake: boolean // For local development. If set, the API returns dummy data + novuConfig: NovuConfig + environment: string } export type ServiceContext = { - userId: string; -} & YogaInitialContext; + userId: string +} & YogaInitialContext const typeDefs = ` enum Channel { @@ -60,7 +49,7 @@ const typeDefs = ` updatePreferences(input: UpdatePreferencesInput!): [WorkflowPreferences]! } -`; +` const createResolvers = (novu: Novu, config: ServerConfig) => { return { @@ -72,9 +61,7 @@ const createResolvers = (novu: Novu, config: ServerConfig) => { _parameters: any, context: ServiceContext, ): Promise<PreferencesResponse> => { - return config.fake - ? Promise.resolve([]) - : fetchPreferences(novu, context.userId); + return config.fake ? Promise.resolve([]) : fetchPreferences(novu, context.userId) }, }, Mutation: { @@ -90,49 +77,47 @@ const createResolvers = (novu: Novu, config: ServerConfig) => { parameters.input, ), }, - }; -}; + } +} export const createGraphQLServer = ( novu: Novu, config: ServerConfig, ): GraphQLServer => { - const resolvers = createResolvers(novu, config); + const resolvers = createResolvers(novu, config) return createYoga({ schema: createSchema({ resolvers, typeDefs }), graphiql: config.environment === LOCAL_ENVIRONMENT, context: (context: YogaInitialContext) => { - const headers = new Headers(context.request.headers); + const headers = new Headers(context.request.headers) return { ...context, userId: headers.get(HEADER_USER_ID), - } as ServiceContext; + } as ServiceContext }, - }); -}; + }) +} // deno-lint-ignore no-explicit-any -export type GraphQLServer = any; +export type GraphQLServer = any export const startServer = (config: ServerConfig): Promise<void> => { - const novuInstance = createNovuInstance(config.novuConfig); + const novuInstance = createNovuInstance(config.novuConfig) const graphQLServer: GraphQLServer = createGraphQLServer( novuInstance, config, - ); + ) return serve(graphQLServer.handleRequest, { port: config.port, onListen({ port, hostname }) { logger.info( - `Server started at http://${ - hostname === "0.0.0.0" ? "localhost" : hostname - }:${port}/graphql`, - ); + `Server started at http://${hostname === '0.0.0.0' ? 'localhost' : hostname}:${port}/graphql`, + ) if (config.fake) { logger.info( `Server is serving fake data due to FAKE env var set to true`, - ); + ) } }, - }); -}; + }) +} diff --git a/app/server_test.ts b/app/server_test.ts index b7602f52d0f0f4eb45262791962e48b500ee4a3f..5d1d03574610f55a4feefa6cd83fba101a5f5127 100644 --- a/app/server_test.ts +++ b/app/server_test.ts @@ -1,38 +1,19 @@ -import { - afterEach, - assertEquals, - assertSpyCall, - assertSpyCalls, - beforeEach, - describe, - it, - Stub, -} from "./dev_deps.ts"; -import { GraphQLError } from "./deps.ts"; -import { - createGraphQLServer, - GraphQLServer, - HEADER_USER_ID, -} from "./server.ts"; -import { PreferencesResponse, UpdatePreferencesInput } from "./types.ts"; -import { - globalUpdate, - novuMock, - preferencesAllEnabled, - subscriberId, - testConfig, -} from "./test_data.ts"; +import { afterEach, assertEquals, assertSpyCall, assertSpyCalls, beforeEach, describe, it, Stub } from './dev_deps.ts' +import { GraphQLError } from './deps.ts' +import { createGraphQLServer, GraphQLServer, HEADER_USER_ID } from './server.ts' +import { PreferencesResponse, UpdatePreferencesInput } from './types.ts' +import { globalUpdate, novuMock, preferencesAllEnabled, subscriberId, testConfig } from './test_data.ts' import { processGqlRequest, stubNovuGlobalPreferences, stubNovuUpdateGlobalPreferences, stubNovuUpdateWorkflowPreferences, stubNovuWorkflowPreferences, -} from "./test_helpers.ts"; +} from './test_helpers.ts' const authorizedHeader = { [HEADER_USER_ID]: subscriberId, -}; +} const queryPreferences = ( graphQLServer: GraphQLServer, @@ -48,11 +29,11 @@ const queryPreferences = ( email } } - }`; + }` return processGqlRequest(graphQLServer, query, {}, headers).then( (result) => result?.preferences as PreferencesResponse, - ); -}; + ) +} const mutatePreferences = ( graphQLServer: GraphQLServer, @@ -69,161 +50,161 @@ const mutatePreferences = ( email } } - }`; + }` return processGqlRequest(graphQLServer, query, { input }, headers).then( (result) => result?.updatePreferences as PreferencesResponse, - ); -}; + ) +} -describe("server", () => { - let novuGlobalPreferenceStub: Stub; - let novuPreferenceStub: Stub; - let novuUpdateGlobalPreferenceStub: Stub; - let novuUpdatePreferenceStub: Stub; +describe('server', () => { + let novuGlobalPreferenceStub: Stub + let novuPreferenceStub: Stub + let novuUpdateGlobalPreferenceStub: Stub + let novuUpdatePreferenceStub: Stub beforeEach(() => { - novuGlobalPreferenceStub = stubNovuGlobalPreferences(); - novuPreferenceStub = stubNovuWorkflowPreferences(); - novuUpdateGlobalPreferenceStub = stubNovuUpdateGlobalPreferences(); - novuUpdatePreferenceStub = stubNovuUpdateWorkflowPreferences(); - }); + novuGlobalPreferenceStub = stubNovuGlobalPreferences() + novuPreferenceStub = stubNovuWorkflowPreferences() + novuUpdateGlobalPreferenceStub = stubNovuUpdateGlobalPreferences() + novuUpdatePreferenceStub = stubNovuUpdateWorkflowPreferences() + }) afterEach(() => { if (!novuGlobalPreferenceStub?.restored) { - novuGlobalPreferenceStub?.restore(); + novuGlobalPreferenceStub?.restore() } if (!novuPreferenceStub?.restored) { - novuPreferenceStub?.restore(); + novuPreferenceStub?.restore() } if (!novuUpdateGlobalPreferenceStub?.restored) { - novuUpdateGlobalPreferenceStub?.restore(); + novuUpdateGlobalPreferenceStub?.restore() } if (!novuUpdatePreferenceStub?.restored) { - novuUpdatePreferenceStub?.restore(); + novuUpdatePreferenceStub?.restore() } - }); + }) - describe("preferences", () => { - it("returns aggregated response", async () => { + describe('preferences', () => { + it('returns aggregated response', async () => { const expectedPreferences = [ { - id: "GLOBAL", - category: "GLOBAL", + id: 'GLOBAL', + category: 'GLOBAL', preferences: preferencesAllEnabled, }, { - id: "SPACE_NEW_POST", - category: "SPACES", + id: 'SPACE_NEW_POST', + category: 'SPACES', preferences: { push: false, email: null }, }, { - id: "SPACE_MEMBERSHIP_REQUEST", - category: "SPACES", + id: 'SPACE_MEMBERSHIP_REQUEST', + category: 'SPACES', preferences: preferencesAllEnabled, }, - ]; - const graphQLServer = createGraphQLServer(novuMock, testConfig); + ] + const graphQLServer = createGraphQLServer(novuMock, testConfig) - const result = await queryPreferences(graphQLServer); + const result = await queryPreferences(graphQLServer) - assertEquals(result, expectedPreferences); - }); + assertEquals(result, expectedPreferences) + }) - it("correctly retrieves user id from header", async () => { - const graphQLServer = createGraphQLServer(novuMock, testConfig); + it('correctly retrieves user id from header', async () => { + const graphQLServer = createGraphQLServer(novuMock, testConfig) - await queryPreferences(graphQLServer); + await queryPreferences(graphQLServer) assertSpyCall(novuGlobalPreferenceStub, 0, { self: novuMock.subscribers, args: [subscriberId], - }); + }) assertSpyCall(novuPreferenceStub, 0, { self: novuMock.subscribers, args: [subscriberId], - }); - }); + }) + }) - it("does not fetch preferences if subscriber id is missing", async () => { - const graphQLServer = createGraphQLServer(novuMock, testConfig); + it('does not fetch preferences if subscriber id is missing', async () => { + const graphQLServer = createGraphQLServer(novuMock, testConfig) - const result = await queryPreferences(graphQLServer, {}); + const result = await queryPreferences(graphQLServer, {}) - assertEquals(result, undefined); - assertSpyCalls(novuGlobalPreferenceStub, 0); - assertSpyCalls(novuPreferenceStub, 0); - }); - }); + assertEquals(result, undefined) + assertSpyCalls(novuGlobalPreferenceStub, 0) + assertSpyCalls(novuPreferenceStub, 0) + }) + }) - describe("updatePreferences", () => { - it("returns aggregated response", async () => { + describe('updatePreferences', () => { + it('returns aggregated response', async () => { const expectedPreferences = [ { - id: "GLOBAL", - category: "GLOBAL", + id: 'GLOBAL', + category: 'GLOBAL', preferences: { push: false, email: true }, }, { - id: "SPACE_NEW_POST", - category: "SPACES", + id: 'SPACE_NEW_POST', + category: 'SPACES', preferences: { push: false, email: null }, }, { - id: "SPACE_MEMBERSHIP_REQUEST", - category: "SPACES", + id: 'SPACE_MEMBERSHIP_REQUEST', + category: 'SPACES', preferences: { push: false, email: true }, }, - ]; - const graphQLServer = createGraphQLServer(novuMock, testConfig); + ] + const graphQLServer = createGraphQLServer(novuMock, testConfig) - const result = await mutatePreferences(graphQLServer, globalUpdate); + const result = await mutatePreferences(graphQLServer, globalUpdate) - assertEquals(result, expectedPreferences); - }); + assertEquals(result, expectedPreferences) + }) - it("correctly retrieves user id from header", async () => { - const graphQLServer = createGraphQLServer(novuMock, testConfig); + it('correctly retrieves user id from header', async () => { + const graphQLServer = createGraphQLServer(novuMock, testConfig) - await mutatePreferences(graphQLServer, globalUpdate); + await mutatePreferences(graphQLServer, globalUpdate) assertSpyCall(novuGlobalPreferenceStub, 0, { self: novuMock.subscribers, args: [subscriberId], - }); + }) assertSpyCall(novuPreferenceStub, 0, { self: novuMock.subscribers, args: [subscriberId], - }); + }) assertSpyCall(novuUpdateGlobalPreferenceStub, 0, { self: novuMock.subscribers, args: [subscriberId, { - preferences: [{ enabled: false, type: "push" }], + preferences: [{ enabled: false, type: 'push' }], }], - }); + }) assertSpyCall(novuUpdatePreferenceStub, 0, { self: novuMock.subscribers, - args: [subscriberId, "space-membership-request-accept", { - channel: { enabled: false, type: "push" }, + args: [subscriberId, 'space-membership-request-accept', { + channel: { enabled: false, type: 'push' }, }], - }); + }) assertSpyCall(novuUpdatePreferenceStub, 1, { self: novuMock.subscribers, - args: [subscriberId, "space-membership-request-decline", { - channel: { enabled: false, type: "push" }, + args: [subscriberId, 'space-membership-request-decline', { + channel: { enabled: false, type: 'push' }, }], - }); - }); - - it("does not update preferences if subscriber id is missing", async () => { - const graphQLServer = createGraphQLServer(novuMock, testConfig); - - const result = await mutatePreferences(graphQLServer, globalUpdate, {}); - - assertEquals(result, undefined); - assertSpyCalls(novuGlobalPreferenceStub, 0); - assertSpyCalls(novuPreferenceStub, 0); - assertSpyCalls(novuUpdateGlobalPreferenceStub, 0); - assertSpyCalls(novuUpdatePreferenceStub, 0); - }); - }); -}); + }) + }) + + it('does not update preferences if subscriber id is missing', async () => { + const graphQLServer = createGraphQLServer(novuMock, testConfig) + + const result = await mutatePreferences(graphQLServer, globalUpdate, {}) + + assertEquals(result, undefined) + assertSpyCalls(novuGlobalPreferenceStub, 0) + assertSpyCalls(novuPreferenceStub, 0) + assertSpyCalls(novuUpdateGlobalPreferenceStub, 0) + assertSpyCalls(novuUpdatePreferenceStub, 0) + }) + }) +}) diff --git a/app/test_data.ts b/app/test_data.ts index e46572d24e990966d826e41935fe42604d719947..2b8ff85e07dc08940685da66dfa468c3d6804699 100644 --- a/app/test_data.ts +++ b/app/test_data.ts @@ -1,73 +1,73 @@ -import { ChannelTypeEnum, Novu } from "./deps.ts"; +import { ChannelTypeEnum, Novu } from './deps.ts' export const testConfig = { port: 8005, fake: false, novuConfig: { - apiKey: "TEST-API-KEY", - apiUrl: "TEST-API-URL", + apiKey: 'TEST-API-KEY', + apiUrl: 'TEST-API-URL', }, - environment: "local", -}; -export const subscriberId = "testSubscriberId"; -type NovuSubscribers = Novu["subscribers"]; + environment: 'local', +} +export const subscriberId = 'testSubscriberId' +type NovuSubscribers = Novu['subscribers'] class NovuSubscribersMock implements Partial<NovuSubscribers> {} class NovuMock implements Partial<Novu> { - subscribers = new NovuSubscribersMock() as NovuSubscribers; + subscribers = new NovuSubscribersMock() as NovuSubscribers } -export const novuMock = new NovuMock() as Novu; +export const novuMock = new NovuMock() as Novu const novuPreferencesAllEnabled = { enabled: true, channels: { - "push": true, - "email": true, - "in_app": true, + 'push': true, + 'email': true, + 'in_app': true, }, -}; +} export const testGlobalPreferences = [{ preference: novuPreferencesAllEnabled, -}]; +}] export const testWorkflowPreferences = [ { preference: { enabled: true, channels: { - "push": false, - "in_app": true, + 'push': false, + 'in_app': true, }, }, template: { - _id: "space-new-post", - tags: ["CATEGORY_SPACES", "SPACE_NEW_POST"], + _id: 'space-new-post', + tags: ['CATEGORY_SPACES', 'SPACE_NEW_POST'], }, }, { preference: novuPreferencesAllEnabled, template: { - _id: "space-membership-request-accept", - tags: ["CATEGORY_SPACES", "SPACE_MEMBERSHIP_REQUEST"], + _id: 'space-membership-request-accept', + tags: ['CATEGORY_SPACES', 'SPACE_MEMBERSHIP_REQUEST'], }, }, { preference: { enabled: true, channels: { - "push": true, - "in_app": true, + 'push': true, + 'in_app': true, }, }, template: { - _id: "space-membership-request-decline", - tags: ["CATEGORY_SPACES", "SPACE_MEMBERSHIP_REQUEST"], + _id: 'space-membership-request-decline', + tags: ['CATEGORY_SPACES', 'SPACE_MEMBERSHIP_REQUEST'], }, }, -]; +] export const preferencesAllEnabled = { - "push": true, - "email": true, -}; + 'push': true, + 'email': true, +} export const globalUpdate = { - id: "GLOBAL", + id: 'GLOBAL', channel: ChannelTypeEnum.PUSH, enabled: false, -}; +} diff --git a/app/test_helpers.ts b/app/test_helpers.ts index bae48550e7a5370ebcf3473c8d453e74b167f5cb..319bb2eb5256ad60c20554385961f7fdd76b5a83 100644 --- a/app/test_helpers.ts +++ b/app/test_helpers.ts @@ -1,14 +1,7 @@ -import { returnsNext, stub } from "./dev_deps.ts"; -import { - NovuGlobalPreferences, - NovuWorkflowPreferences, -} from "./novu_types.ts"; -import { GraphQLServer } from "./server.ts"; -import { - novuMock, - testGlobalPreferences, - testWorkflowPreferences, -} from "./test_data.ts"; +import { returnsNext, stub } from './dev_deps.ts' +import { NovuGlobalPreferences, NovuWorkflowPreferences } from './novu_types.ts' +import { GraphQLServer } from './server.ts' +import { novuMock, testGlobalPreferences, testWorkflowPreferences } from './test_data.ts' export const processGqlRequest = ( graphQLServer: GraphQLServer, @@ -16,11 +9,11 @@ export const processGqlRequest = ( variables: Record<string, unknown> = {}, headers: Record<string, unknown> = {}, ): Promise<Record<string, unknown> | undefined | null> => { - const request = new Request("http://localhost:8005/graphql", { - method: "POST", + const request = new Request('http://localhost:8005/graphql', { + method: 'POST', headers: { - "content-type": "application/json", - "accept": "*/*", + 'content-type': 'application/json', + 'accept': '*/*', ...headers, }, body: JSON.stringify({ @@ -28,60 +21,56 @@ export const processGqlRequest = ( variables: variables, query: query, }), - }); + }) return graphQLServer.handleRequest(request) // deno-lint-ignore no-explicit-any .then((response: any) => response.json()) // deno-lint-ignore no-explicit-any - .then((response: any) => response.data); -}; + .then((response: any) => response.data) +} export const stubNovuGlobalPreferences = ( response: NovuGlobalPreferences[] | Error = testGlobalPreferences, ) => stub( novuMock.subscribers, - "getGlobalPreference", + 'getGlobalPreference', // @ts-ignore: Full mock of AxiosResponse not necessary returnsNext([ - response instanceof Error - ? Promise.reject(response) - : Promise.resolve({ data: { data: response } }), + response instanceof Error ? Promise.reject(response) : Promise.resolve({ data: { data: response } }), ]), - ); + ) export const stubNovuWorkflowPreferences = ( response: NovuWorkflowPreferences[] | Error = testWorkflowPreferences, ) => stub( novuMock.subscribers, - "getPreference", + 'getPreference', // @ts-ignore: Full mock of AxiosResponse not necessary returnsNext([ - response instanceof Error - ? Promise.reject(response) - : Promise.resolve({ data: { data: response } }), + response instanceof Error ? Promise.reject(response) : Promise.resolve({ data: { data: response } }), ]), - ); + ) export const stubNovuUpdateGlobalPreferences = (error?: Error) => stub( novuMock.subscribers, - "updateGlobalPreference", + 'updateGlobalPreference', // @ts-ignore: Full mock of AxiosResponse not necessary returnsNext([ error ? Promise.reject(error) : Promise.resolve(), ]), - ); + ) export const stubNovuUpdateWorkflowPreferences = (error?: Error) => stub( novuMock.subscribers, - "updatePreference", + 'updatePreference', returnsNext( (error ? [Promise.reject(error)] : []).concat( // @ts-ignore: Full mock of AxiosResponse not necessary testWorkflowPreferences.map(() => Promise.resolve()), ), ), - ); + ) diff --git a/app/types.ts b/app/types.ts index c232b3f5774dcbad9126b5d115b59cdab60defb1..bce1e04d3bac5a57efe5ec25d1b4865f46879ca9 100644 --- a/app/types.ts +++ b/app/types.ts @@ -1,29 +1,29 @@ -import { ChannelTypeEnum } from "./deps.ts"; +import { ChannelTypeEnum } from './deps.ts' export type ChannelPreferences = { - [key in ChannelTypeEnum]?: boolean | null; -}; + [key in ChannelTypeEnum]?: boolean | null +} export interface WorkflowTemplatePreferences { - id: string; - category: string; - templateId?: string; - preferences: ChannelPreferences; + id: string + category: string + templateId?: string + preferences: ChannelPreferences } export type WorkflowGroupPreferences = Omit< WorkflowTemplatePreferences, - "templateId" ->; + 'templateId' +> -export type PreferencesResponse = WorkflowGroupPreferences[]; +export type PreferencesResponse = WorkflowGroupPreferences[] export interface UpdatePreferencesInput { - id: string; - channel: ChannelTypeEnum; - enabled: boolean; + id: string + channel: ChannelTypeEnum + enabled: boolean } export interface UpdatePreferencesParameters { - input: UpdatePreferencesInput; + input: UpdatePreferencesInput } diff --git a/deno.json b/deno.json index 1cea5f8c7c4dfad48efcb73124c42ad87c9d4538..76adfaa8ca9a4944dc5c19b4dd19cd718ecadb52 100644 --- a/deno.json +++ b/deno.json @@ -19,6 +19,9 @@ } }, "fmt": { + "lineWidth": 120, + "singleQuote": true, + "semiColons": false, "exclude": [ "*.md" ] diff --git a/terraform/environments/scripts/destroy-env.sh b/terraform/environments/scripts/destroy-env.sh index d8137694745e9a6bcded13142bf6a5e666b55e6c..aac6dea64eafc13c326e5b4819351c32365e9b3c 100755 --- a/terraform/environments/scripts/destroy-env.sh +++ b/terraform/environments/scripts/destroy-env.sh @@ -1,4 +1,4 @@ -#!/usr/bin/env bash +#!/usr/bin/env sh # exit when any command fails set -ex @@ -12,7 +12,7 @@ export TF_LOG=DEBUG # * GCP stuff not allowing our resources to be deleted. # Most of the time, retrying a destroy fixes these causes. retry() { - for i in {1..3}; do + for i in $(seq 1 3); do set +e "$@" retval=$?