diff --git a/apps/metamigo-api/.eslintrc.js b/apps/metamigo-api/.eslintrc.js index 72edea5..c3d8a46 100644 --- a/apps/metamigo-api/.eslintrc.js +++ b/apps/metamigo-api/.eslintrc.js @@ -2,25 +2,11 @@ require("eslint-config-link/patch/modern-module-resolution"); module.exports = { extends: [ "eslint-config-link/profile/node", - "eslint-config-link/eslint-config-amigo/profile/typescript", - "eslint-config-link/eslint-config-amigo/profile/jest", + "eslint-config-link/profile/typescript", + "eslint-config-link/profile/jest", ], parserOptions: { tsconfigRootDir: __dirname }, rules: { - "new-cap": "off", - "import/no-extraneous-dependencies": [ - // enable this when this is fixed - // https://github.com/benmosher/eslint-plugin-import/pull/1696 - "off", - { - packageDir: [ - ".", - "node_modules/@digiresilience/amigo", - "node_modules/@digiresilience/amigo-dev", - ], - }, - ], - // TODO: enable this after jest fixes this issue https://github.com/nodejs/node/issues/38343 - "unicorn/prefer-node-protocol": "off" + "new-cap": "off" }, }; \ No newline at end of file diff --git a/apps/metamigo-api/app/plugins/index.ts b/apps/metamigo-api/app/plugins/index.ts deleted file mode 100644 index 8786838..0000000 --- a/apps/metamigo-api/app/plugins/index.ts +++ /dev/null @@ -1,32 +0,0 @@ -import type * as Hapi from "@hapi/hapi"; -import Schmervice from "@hapipal/schmervice"; -import PgPromisePlugin from "@digiresilience/hapi-pg-promise"; - -import type { IAppConfig } from "../../config"; -import { dbInitOptions } from "@digiresilience/metamigo-db"; -import { registerNextAuth } from "./hapi-nextauth"; -import { registerSwagger } from "./swagger"; -import { registerNextAuthJwt } from "./nextauth-jwt"; -import { registerCloudflareAccessJwt } from "./cloudflare-jwt"; - -export const register = async ( - server: Hapi.Server, - config: IAppConfig -): Promise => { - await server.register(Schmervice); - - await server.register({ - plugin: PgPromisePlugin, - options: { - // the only required parameter is the connection string - connection: config.db.connection, - // ... and the pg-promise initialization options - pgpInit: dbInitOptions(config), - }, - }); - - await registerNextAuth(server, config); - await registerSwagger(server); - await registerNextAuthJwt(server, config); - await registerCloudflareAccessJwt(server, config); -}; diff --git a/apps/metamigo-api/config.ts b/apps/metamigo-api/config.ts deleted file mode 100644 index 3f31611..0000000 --- a/apps/metamigo-api/config.ts +++ /dev/null @@ -1,10 +0,0 @@ -import config, { - loadConfig, - loadConfigRaw, - IAppConfig, - IAppConvict, -} from "config"; - -export { IAppConvict, IAppConfig, loadConfig, loadConfigRaw }; - -export default config; diff --git a/apps/metamigo-api/package.json b/apps/metamigo-api/package.json index 4958f9a..691fab5 100644 --- a/apps/metamigo-api/package.json +++ b/apps/metamigo-api/package.json @@ -37,7 +37,8 @@ "pg-promise": "^11.0.2", "postgraphile-plugin-connection-filter": "^2.3.0", "remeda": "^1.6.0", - "twilio": "^3.84.1" + "twilio": "^3.84.1", + "expiry-map": "^2.0.0" }, "devDependencies": { "pino-pretty": "^9.1.1", diff --git a/apps/metamigo-api/app/index.ts b/apps/metamigo-api/src/app/index.ts similarity index 94% rename from apps/metamigo-api/app/index.ts rename to apps/metamigo-api/src/app/index.ts index 6452a38..e95f7c9 100644 --- a/apps/metamigo-api/app/index.ts +++ b/apps/metamigo-api/src/app/index.ts @@ -7,10 +7,10 @@ import * as Plugins from "./plugins"; const AppPlugin = { name: "App", - register: async ( + async register( server: Hapi.Server, options: { config: IAppConfig } - ): Promise => { + ): Promise { // declare our **run-time** plugin dependencies // these are runtime only deps, not registration time // ref: https://hapipal.com/best-practices/handling-plugin-dependencies diff --git a/apps/metamigo-api/app/lib/whatsapp-key-store.ts b/apps/metamigo-api/src/app/lib/whatsapp-key-store.ts similarity index 89% rename from apps/metamigo-api/app/lib/whatsapp-key-store.ts rename to apps/metamigo-api/src/app/lib/whatsapp-key-store.ts index 5cb149e..3f0178e 100644 --- a/apps/metamigo-api/app/lib/whatsapp-key-store.ts +++ b/apps/metamigo-api/src/app/lib/whatsapp-key-store.ts @@ -1,6 +1,6 @@ import { Boom } from "@hapi/boom"; import { Server } from "@hapi/hapi"; -import { randomBytes } from "crypto"; +import { randomBytes } from "node:crypto"; import type { Logger } from "pino"; import { proto, @@ -45,43 +45,38 @@ export const addTransactionCapability = ( ? ids.filter((item) => !(item in dict)) : ids; // only fetch if there are any items to fetch - if (idsRequiringFetch.length) { + if (idsRequiringFetch.length > 0) { const result = await state.get(type, idsRequiringFetch); transactionCache[type] = transactionCache[type] || {}; - // @ts-expect-error Object.assign(transactionCache[type], result); } }; return { - get: async (type, ids) => { + async get(type, ids) { if (inTransaction) { await prefetch(type, ids); return ids.reduce((dict, id) => { const value = transactionCache[type]?.[id]; if (value) { - // @ts-expect-error dict[id] = value; } return dict; }, {}); - } else { - return state.get(type, ids); } + + return state.get(type, ids); + }, - set: (data) => { + set(data) { if (inTransaction) { logger.trace({ types: Object.keys(data) }, "caching in transaction"); for (const key in data) { - // @ts-expect-error transactionCache[key] = transactionCache[key] || {}; - // @ts-expect-error Object.assign(transactionCache[key], data[key]); - // @ts-expect-error mutations[key] = mutations[key] || {}; - // @ts-expect-error Object.assign(mutations[key], data[key]); } } else { @@ -90,11 +85,11 @@ export const addTransactionCapability = ( }, isInTransaction: () => inTransaction, // @ts-expect-error - prefetch: (type, ids) => { + prefetch(type, ids) { logger.trace({ type, ids }, "prefetching"); return prefetch(type, ids); }, - transaction: async (work) => { + async transaction(work) { if (inTransaction) { await work(); } else { @@ -102,7 +97,7 @@ export const addTransactionCapability = ( inTransaction = true; try { await work(); - if (Object.keys(mutations).length) { + if (Object.keys(mutations).length > 0) { logger.debug("committing transaction"); await state.set(mutations); } else { @@ -140,7 +135,7 @@ export const useDatabaseAuthState = ( bot: Bot, server: Server ): { state: AuthenticationState; saveState: () => void } => { - let { logger }: any = server; + const { logger }: any = server; let creds: AuthenticationCreds; let keys: any = {}; @@ -165,7 +160,7 @@ export const useDatabaseAuthState = ( state: { creds, keys: { - get: (type, ids) => { + get(type, ids) { const key = KEY_MAP[type]; return ids.reduce((dict, id) => { let value = keys[key]?.[id]; @@ -174,18 +169,17 @@ export const useDatabaseAuthState = ( // @ts-expect-error value = proto.AppStateSyncKeyData.fromObject(value); } - // @ts-expect-error + dict[id] = value; } return dict; }, {}); }, - set: (data) => { + set(data) { for (const _key in data) { const key = KEY_MAP[_key as keyof SignalDataTypeMap]; keys[key] = keys[key] || {}; - // @ts-expect-error Object.assign(keys[key], data[_key]); } diff --git a/apps/metamigo-api/app/plugins/cloudflare-jwt.ts b/apps/metamigo-api/src/app/plugins/cloudflare-jwt.ts similarity index 82% rename from apps/metamigo-api/app/plugins/cloudflare-jwt.ts rename to apps/metamigo-api/src/app/plugins/cloudflare-jwt.ts index 05cddf4..c6644d0 100644 --- a/apps/metamigo-api/app/plugins/cloudflare-jwt.ts +++ b/apps/metamigo-api/src/app/plugins/cloudflare-jwt.ts @@ -1,7 +1,7 @@ import * as Boom from "@hapi/boom"; import * as Hoek from "@hapi/hoek"; import * as Hapi from "@hapi/hapi"; -import { promisify } from "util"; +import { promisify } from "node:util"; import jwt from "jsonwebtoken"; import jwksClient, { hapiJwt2KeyAsync } from "jwks-rsa"; import type { IAppConfig } from "../../config"; @@ -36,22 +36,20 @@ const verifyToken = (settings: any) => { }; }; -const handleCfJwt = (verify: any) => async ( - request: Hapi.Request, - h: Hapi.ResponseToolkit -) => { - const token = request.headers[CF_JWT_HEADER_NAME]; - if (token) { - try { - await verify(token); - } catch (error) { - console.error(error); - return Boom.unauthorized("invalid cloudflare access token"); +const handleCfJwt = + (verify: any) => async (request: Hapi.Request, h: Hapi.ResponseToolkit) => { + const token = request.headers[CF_JWT_HEADER_NAME]; + if (token) { + try { + await verify(token); + } catch (error) { + console.error(error); + return Boom.unauthorized("invalid cloudflare access token"); + } } - } - return h.continue; -}; + return h.continue; + }; const defaultOpts = { issuer: undefined, @@ -60,7 +58,10 @@ const defaultOpts = { validate: undefined, }; -const cfJwtRegister = async (server: Hapi.Server, options: any): Promise => { +const cfJwtRegister = async ( + server: Hapi.Server, + options: any +): Promise => { server.dependency(["hapi-auth-jwt2"]); const settings = Hoek.applyToDefaults(defaultOpts, options); const verify = verifyToken(settings); @@ -101,7 +102,7 @@ export const registerCloudflareAccessJwt = async ( options: { issuer: `https://${domain}`, audience, - validate: (decoded: any, _request: any) => { + validate(decoded: any, _request: any) { const { email, name } = decoded; return { isValid: true, diff --git a/apps/metamigo-api/app/plugins/hapi-nextauth.ts b/apps/metamigo-api/src/app/plugins/hapi-nextauth.ts similarity index 83% rename from apps/metamigo-api/app/plugins/hapi-nextauth.ts rename to apps/metamigo-api/src/app/plugins/hapi-nextauth.ts index 3b35190..cdd989c 100644 --- a/apps/metamigo-api/app/plugins/hapi-nextauth.ts +++ b/apps/metamigo-api/src/app/plugins/hapi-nextauth.ts @@ -1,8 +1,12 @@ import type * as Hapi from "@hapi/hapi"; import NextAuthPlugin, { AdapterFactory } from "@digiresilience/hapi-nextauth"; import { NextAuthAdapter } from "@digiresilience/metamigo-common"; -import type { SavedUser, UnsavedUser, SavedSession } from "@digiresilience/metamigo-common"; -import { IAppConfig } from "config"; +import type { + SavedUser, + UnsavedUser, + SavedSession, +} from "@digiresilience/metamigo-common"; +import { IAppConfig } from "@digiresilience/metamigo-config"; export const registerNextAuth = async ( server: Hapi.Server, diff --git a/apps/metamigo-api/src/app/plugins/index.ts b/apps/metamigo-api/src/app/plugins/index.ts new file mode 100644 index 0000000..ca9891e --- /dev/null +++ b/apps/metamigo-api/src/app/plugins/index.ts @@ -0,0 +1,40 @@ +import type * as Hapi from "@hapi/hapi"; +import { ServerRegisterPluginObjectDirect } from "@hapi/hapi/lib/types/plugin"; +import type { IInitOptions, IDatabase } from "pg-promise"; +import Schmervice from "@hapipal/schmervice"; +import { makePlugin } from "@digiresilience/hapi-pg-promise"; + +import type { IAppConfig } from "../../config"; +import { dbInitOptions, IRepositories } from "@digiresilience/metamigo-db"; +import { registerNextAuth } from "./hapi-nextauth"; +import { registerSwagger } from "./swagger"; +import { registerNextAuthJwt } from "./nextauth-jwt"; +import { registerCloudflareAccessJwt } from "./cloudflare-jwt"; +import pg from "pg-promise/typescript/pg-subset"; + +export const register = async ( + server: Hapi.Server, + config: IAppConfig +): Promise => { + await server.register(Schmervice); + + const pgpInit = dbInitOptions(config); + const options = { + // the only required parameter is the connection string + connection: config.db.connection, + // ... and the pg-promise initialization options + pgpInit, + }; + + await server.register([ + { + plugin: makePlugin>(), + options, + }, + ]); + + await registerNextAuth(server, config); + await registerSwagger(server); + await registerNextAuthJwt(server, config); + await registerCloudflareAccessJwt(server, config); +}; diff --git a/apps/metamigo-api/app/plugins/nextauth-jwt.ts b/apps/metamigo-api/src/app/plugins/nextauth-jwt.ts similarity index 97% rename from apps/metamigo-api/app/plugins/nextauth-jwt.ts rename to apps/metamigo-api/src/app/plugins/nextauth-jwt.ts index bef23f3..e2ccaa6 100644 --- a/apps/metamigo-api/app/plugins/nextauth-jwt.ts +++ b/apps/metamigo-api/src/app/plugins/nextauth-jwt.ts @@ -46,7 +46,7 @@ export const registerNextAuthJwt = async ( }, options: { jwkeysB64: [config.nextAuth.signingKey], - validate: async (decoded, request: Hapi.Request) => { + async validate(decoded, request: Hapi.Request) { const { email, name, role } = decoded; const user = await request.db().users.findBy({ email }); if (!config.isProd) { diff --git a/apps/metamigo-api/app/plugins/swagger.ts b/apps/metamigo-api/src/app/plugins/swagger.ts similarity index 100% rename from apps/metamigo-api/app/plugins/swagger.ts rename to apps/metamigo-api/src/app/plugins/swagger.ts diff --git a/apps/metamigo-api/app/routes/helpers/index.ts b/apps/metamigo-api/src/app/routes/helpers/index.ts similarity index 100% rename from apps/metamigo-api/app/routes/helpers/index.ts rename to apps/metamigo-api/src/app/routes/helpers/index.ts diff --git a/apps/metamigo-api/app/routes/index.ts b/apps/metamigo-api/src/app/routes/index.ts similarity index 92% rename from apps/metamigo-api/app/routes/index.ts rename to apps/metamigo-api/src/app/routes/index.ts index b0d54f4..8ebdd29 100644 --- a/apps/metamigo-api/app/routes/index.ts +++ b/apps/metamigo-api/src/app/routes/index.ts @@ -1,6 +1,5 @@ import isFunction from "lodash/isFunction"; import type * as Hapi from "@hapi/hapi"; -import * as RandomRoutes from "./random"; import * as UserRoutes from "./users"; import * as VoiceRoutes from "./voice"; import * as WhatsappRoutes from "./whatsapp"; @@ -25,7 +24,6 @@ export const register = async (server: Hapi.Server): Promise => { // Load your routes here. // routes are loaded from the list of exported vars // a route file should export routes directly or an async function that returns the routes. - loadRouteIndex(server, RandomRoutes); loadRouteIndex(server, UserRoutes); loadRouteIndex(server, VoiceRoutes); loadRouteIndex(server, WhatsappRoutes); diff --git a/apps/metamigo-api/app/routes/signal/index.ts b/apps/metamigo-api/src/app/routes/signal/index.ts similarity index 89% rename from apps/metamigo-api/app/routes/signal/index.ts rename to apps/metamigo-api/src/app/routes/signal/index.ts index 735ac2a..c038880 100644 --- a/apps/metamigo-api/app/routes/signal/index.ts +++ b/apps/metamigo-api/src/app/routes/signal/index.ts @@ -3,16 +3,14 @@ import * as Joi from "joi"; import * as Helpers from "../helpers"; import Boom from "boom"; -const getSignalService = (request) => { - return request.services().signaldService; -}; +const getSignalService = (request) => request.services("app").signaldService; export const GetAllSignalBotsRoute = Helpers.withDefaults({ method: "get", path: "/api/signal/bots", options: { description: "Get all bots", - handler: async (request: Hapi.Request, _h: Hapi.ResponseToolkit) => { + async handler(request: Hapi.Request, _h: Hapi.ResponseToolkit) { const signalService = getSignalService(request); const bots = await signalService.findAll(); @@ -35,7 +33,7 @@ export const GetBotsRoute = Helpers.noAuth({ path: "/api/signal/bots/{token}", options: { description: "Get one bot", - handler: async (request: Hapi.Request, _h: Hapi.ResponseToolkit) => { + async handler(request: Hapi.Request, _h: Hapi.ResponseToolkit) { const { token } = request.params; const signalService = getSignalService(request); @@ -65,7 +63,7 @@ export const SendBotRoute = Helpers.noAuth({ path: "/api/signal/bots/{token}/send", options: { description: "Send a message", - handler: async (request: Hapi.Request, _h: Hapi.ResponseToolkit) => { + async handler(request: Hapi.Request, _h: Hapi.ResponseToolkit) { const { token } = request.params; const { phoneNumber, message } = request.payload as MessageRequest; const signalService = getSignalService(request); @@ -101,7 +99,7 @@ export const ResetSessionBotRoute = Helpers.noAuth({ path: "/api/signal/bots/{token}/resetSession", options: { description: "Reset a session with another user", - handler: async (request: Hapi.Request, _h: Hapi.ResponseToolkit) => { + async handler(request: Hapi.Request, _h: Hapi.ResponseToolkit) { const { token } = request.params; const { phoneNumber } = request.payload as ResetSessionRequest; const signalService = getSignalService(request); @@ -131,7 +129,7 @@ export const ReceiveBotRoute = Helpers.withDefaults({ path: "/api/signal/bots/{token}/receive", options: { description: "Receive messages", - handler: async (request: Hapi.Request, _h: Hapi.ResponseToolkit) => { + async handler(request: Hapi.Request, _h: Hapi.ResponseToolkit) { const { token } = request.params; const signalService = getSignalService(request); @@ -153,7 +151,7 @@ export const RegisterBotRoute = Helpers.withDefaults({ path: "/api/signal/bots/{id}/register", options: { description: "Register a bot", - handler: async (request: Hapi.Request, h: Hapi.ResponseToolkit) => { + async handler(request: Hapi.Request, h: Hapi.ResponseToolkit) { const { id } = request.params; const signalService = getSignalService(request); const { code } = request.query; @@ -182,7 +180,7 @@ export const CreateBotRoute = Helpers.withDefaults({ path: "/api/signal/bots", options: { description: "Register a bot", - handler: async (request: Hapi.Request, _h: Hapi.ResponseToolkit) => { + async handler(request: Hapi.Request, _h: Hapi.ResponseToolkit) { const { phoneNumber, description } = request.payload as BotRequest; const signalService = getSignalService(request); console.log("request.auth.credentials:", request.auth.credentials); @@ -216,7 +214,7 @@ export const RequestCodeRoute = Helpers.withDefaults({ captcha: Joi.string(), }), }, - handler: async (request: Hapi.Request, h: Hapi.ResponseToolkit) => { + async handler(request: Hapi.Request, h: Hapi.ResponseToolkit) { const { id } = request.params; const { mode, captcha } = request.query; const signalService = getSignalService(request); @@ -233,16 +231,20 @@ export const RequestCodeRoute = Helpers.withDefaults({ } else if (mode === "voice") { await signalService.requestVoiceVerification(bot, captcha); } + return h.response().code(200); } catch (error) { console.log(error); if (error.name === "CaptchaRequiredException") { return h.response().code(402); - } else if (error.code) { - return h.response().code(error.code); - } else { - return h.response().code(500); } + + if (error.code) { + return h.response().code(error.code); + } + + return h.response().code(500); + } }, }, diff --git a/apps/metamigo-api/app/routes/users/index.ts b/apps/metamigo-api/src/app/routes/users/index.ts similarity index 79% rename from apps/metamigo-api/app/routes/users/index.ts rename to apps/metamigo-api/src/app/routes/users/index.ts index f67225e..c3ba642 100644 --- a/apps/metamigo-api/app/routes/users/index.ts +++ b/apps/metamigo-api/src/app/routes/users/index.ts @@ -1,9 +1,13 @@ import * as Joi from "joi"; import * as Hapi from "@hapi/hapi"; -import { UserRecord, crudRoutesFor, CrudControllerBase } from "@digiresilience/metamigo-common"; +import { + UserRecord, + crudRoutesFor, + CrudControllerBase, +} from "@digiresilience/metamigo-common"; import * as RouteHelpers from "../helpers"; -class UserRecordController extends CrudControllerBase(UserRecord) { } +class UserRecordController extends CrudControllerBase(UserRecord) {} const validator = (): Record => ({ create: { @@ -49,11 +53,12 @@ const validator = (): Record => ({ }, }); -export const UserRoutes = async ( - _server: Hapi.Server -): Promise => { - const controller = new UserRecordController("users", "userId"); - return RouteHelpers.withDefaults( - crudRoutesFor("user", "/api/users", controller, "userId", validator()) - ); -}; +export const UserRoutes = RouteHelpers.withDefaults( + crudRoutesFor( + "user", + "/api/users", + new UserRecordController("users", "userId"), + "userId", + validator() + ) +); diff --git a/apps/metamigo-api/app/routes/voice/index.ts b/apps/metamigo-api/src/app/routes/voice/index.ts similarity index 83% rename from apps/metamigo-api/app/routes/voice/index.ts rename to apps/metamigo-api/src/app/routes/voice/index.ts index 855bcff..aef7201 100644 --- a/apps/metamigo-api/app/routes/voice/index.ts +++ b/apps/metamigo-api/src/app/routes/voice/index.ts @@ -4,11 +4,14 @@ import * as Boom from "@hapi/boom"; import * as R from "remeda"; import * as Helpers from "../helpers"; import Twilio from "twilio"; -import { crudRoutesFor, CrudControllerBase } from "@digiresilience/metamigo-common"; +import { + crudRoutesFor, + CrudControllerBase, +} from "@digiresilience/metamigo-common"; import { VoiceLineRecord, SavedVoiceLine } from "@digiresilience/metamigo-db"; const TwilioHandlers = { - freeNumbers: async (provider, request: Hapi.Request) => { + async freeNumbers(provider, request: Hapi.Request) { const { accountSid, apiKeySid, apiKeySecret } = provider.credentials; const client = Twilio(apiKeySid, apiKeySecret, { accountSid, @@ -44,23 +47,26 @@ export const VoiceProviderRoutes = Helpers.withDefaults([ providerId: Joi.string().uuid().required(), }, }, - handler: async (request: Hapi.Request, _h: Hapi.ResponseToolkit) => { + async handler(request: Hapi.Request, _h: Hapi.ResponseToolkit) { const { providerId } = request.params; const voiceProvidersRepo = request.db().voiceProviders; const provider = await voiceProvidersRepo.findById(providerId); if (!provider) return Boom.notFound(); switch (provider.kind) { - case "TWILIO": + case "TWILIO": { return TwilioHandlers.freeNumbers(provider, request); - default: + } + + default: { return Boom.badImplementation(); + } } }, }, }, ]); -class VoiceLineRecordController extends CrudControllerBase(VoiceLineRecord) { } +class VoiceLineRecordController extends CrudControllerBase(VoiceLineRecord) {} const validator = (): Record => ({ create: { @@ -106,19 +112,14 @@ const validator = (): Record => ({ }, }); -export const VoiceLineRoutes = async ( - _server: Hapi.Server -): Promise => { - const controller = new VoiceLineRecordController("voiceLines", "id"); - return Helpers.withDefaults( - crudRoutesFor( - "voice-line", - "/api/voice/voice-line", - controller, - "id", - validator() - ) - ); -}; +export const VoiceLineRoutes = Helpers.withDefaults( + crudRoutesFor( + "voice-line", + "/api/voice/voice-line", + new VoiceLineRecordController("voiceLines", "id"), + "id", + validator() + ) +); export * from "./twilio"; diff --git a/apps/metamigo-api/app/routes/voice/twilio/index.ts b/apps/metamigo-api/src/app/routes/voice/twilio/index.ts similarity index 92% rename from apps/metamigo-api/app/routes/voice/twilio/index.ts rename to apps/metamigo-api/src/app/routes/voice/twilio/index.ts index 4f87afb..c5a9f78 100644 --- a/apps/metamigo-api/app/routes/voice/twilio/index.ts +++ b/apps/metamigo-api/src/app/routes/voice/twilio/index.ts @@ -4,14 +4,13 @@ import * as Boom from "@hapi/boom"; import Twilio from "twilio"; import { SavedVoiceProvider } from "@digiresilience/metamigo-db"; import pMemoize from "p-memoize"; +import ExpiryMap from "expiry-map"; import ms from "ms"; import * as Helpers from "../../helpers"; import workerUtils from "../../../../worker-utils"; import { SayLanguage, SayVoice } from "twilio/lib/twiml/VoiceResponse"; -const queueRecording = async (meta) => { - return workerUtils.addJob("twilio-recording", meta, { jobKey: meta.callSid }); -}; +const queueRecording = async (meta) => workerUtils.addJob("twilio-recording", meta, { jobKey: meta.callSid }); const twilioClientFor = (provider: SavedVoiceProvider): Twilio.Twilio => { const { accountSid, apiKeySid, apiKeySecret } = provider.credentials; @@ -43,8 +42,9 @@ const _getOrCreateTTSTestApplication = async ( }); }; +const cache = new ExpiryMap(ms("1h")); const getOrCreateTTSTestApplication = pMemoize(_getOrCreateTTSTestApplication, { - maxAge: ms("1h"), + cache, }); export const TwilioRoutes = Helpers.noAuth([ @@ -58,7 +58,7 @@ export const TwilioRoutes = Helpers.noAuth([ voiceLineId: Joi.string().uuid().required(), }, }, - handler: async (request: Hapi.Request, h: Hapi.ResponseToolkit) => { + async handler(request: Hapi.Request, h: Hapi.ResponseToolkit) { const { voiceLineId } = request.params; const voiceLine = await request .db() @@ -89,7 +89,7 @@ export const TwilioRoutes = Helpers.noAuth([ voiceLineId: Joi.string().uuid().required(), }, }, - handler: async (request: Hapi.Request, _h: Hapi.ResponseToolkit) => { + async handler(request: Hapi.Request, _h: Hapi.ResponseToolkit) { const { voiceLineId } = request.params; const { To } = request.payload as { To: string }; const voiceLine = await request.db().voiceLines.findBy({ number: To }); @@ -136,7 +136,7 @@ export const TwilioRoutes = Helpers.noAuth([ voiceLineId: Joi.string().uuid().required(), }, }, - handler: async (request: Hapi.Request, h: Hapi.ResponseToolkit) => { + async handler(request: Hapi.Request, h: Hapi.ResponseToolkit) { const { voiceLineId } = request.params; const voiceLine = await request .db() @@ -169,7 +169,7 @@ export const TwilioRoutes = Helpers.noAuth([ providerId: Joi.string().uuid().required(), }, }, - handler: async (request: Hapi.Request, _h: Hapi.ResponseToolkit) => { + async handler(request: Hapi.Request, _h: Hapi.ResponseToolkit) { const { language, voice, prompt } = request.payload as { language: SayLanguage; voice: SayVoice; @@ -192,7 +192,7 @@ export const TwilioRoutes = Helpers.noAuth([ providerId: Joi.string().uuid().required(), }, }, - handler: async (request: Hapi.Request, h: Hapi.ResponseToolkit) => { + async handler(request: Hapi.Request, h: Hapi.ResponseToolkit) { const { providerId } = request.params as { providerId: string }; const provider: SavedVoiceProvider = await request .db() diff --git a/apps/metamigo-api/app/routes/whatsapp/index.ts b/apps/metamigo-api/src/app/routes/whatsapp/index.ts similarity index 83% rename from apps/metamigo-api/app/routes/whatsapp/index.ts rename to apps/metamigo-api/src/app/routes/whatsapp/index.ts index 8283a6e..08cfa74 100644 --- a/apps/metamigo-api/app/routes/whatsapp/index.ts +++ b/apps/metamigo-api/src/app/routes/whatsapp/index.ts @@ -7,8 +7,8 @@ export const GetAllWhatsappBotsRoute = Helpers.withDefaults({ path: "/api/whatsapp/bots", options: { description: "Get all bots", - handler: async (request: Hapi.Request, _h: Hapi.ResponseToolkit) => { - const { whatsappService } = request.services(); + async handler(request: Hapi.Request, _h: Hapi.ResponseToolkit) { + const { whatsappService } = request.services("app"); const bots = await whatsappService.findAll(); @@ -31,9 +31,9 @@ export const GetBotsRoute = Helpers.noAuth({ path: "/api/whatsapp/bots/{token}", options: { description: "Get one bot", - handler: async (request: Hapi.Request, _h: Hapi.ResponseToolkit) => { + async handler(request: Hapi.Request, _h: Hapi.ResponseToolkit) { const { token } = request.params; - const { whatsappService } = request.services(); + const { whatsappService } = request.services("app"); const bot = await whatsappService.findByToken(token); @@ -61,10 +61,10 @@ export const SendBotRoute = Helpers.noAuth({ path: "/api/whatsapp/bots/{token}/send", options: { description: "Send a message", - handler: async (request: Hapi.Request, _h: Hapi.ResponseToolkit) => { + async handler(request: Hapi.Request, _h: Hapi.ResponseToolkit) { const { token } = request.params; const { phoneNumber, message } = request.payload as MessageRequest; - const { whatsappService } = request.services(); + const { whatsappService } = request.services("app"); const bot = await whatsappService.findByToken(token); @@ -93,9 +93,9 @@ export const ReceiveBotRoute = Helpers.withDefaults({ path: "/api/whatsapp/bots/{token}/receive", options: { description: "Receive messages", - handler: async (request: Hapi.Request, _h: Hapi.ResponseToolkit) => { + async handler(request: Hapi.Request, _h: Hapi.ResponseToolkit) { const { token } = request.params; - const { whatsappService } = request.services(); + const { whatsappService } = request.services("app"); const bot = await whatsappService.findByToken(token); @@ -119,9 +119,9 @@ export const RegisterBotRoute = Helpers.withDefaults({ path: "/api/whatsapp/bots/{id}/register", options: { description: "Register a bot", - handler: async (request: Hapi.Request, _h: Hapi.ResponseToolkit) => { + async handler(request: Hapi.Request, _h: Hapi.ResponseToolkit) { const { id } = request.params; - const { whatsappService } = request.services(); + const { whatsappService } = request.services("app"); const bot = await whatsappService.findById(id); @@ -146,9 +146,9 @@ export const RefreshBotRoute = Helpers.withDefaults({ path: "/api/whatsapp/bots/{id}/refresh", options: { description: "Refresh messages", - handler: async (request: Hapi.Request, _h: Hapi.ResponseToolkit) => { + async handler(request: Hapi.Request, _h: Hapi.ResponseToolkit) { const { id } = request.params; - const { whatsappService } = request.services(); + const { whatsappService } = request.services("app"); const bot = await whatsappService.findById(id); @@ -174,9 +174,9 @@ export const CreateBotRoute = Helpers.withDefaults({ path: "/api/whatsapp/bots", options: { description: "Register a bot", - handler: async (request: Hapi.Request, _h: Hapi.ResponseToolkit) => { + async handler(request: Hapi.Request, _h: Hapi.ResponseToolkit) { const { phoneNumber, description } = request.payload as BotRequest; - const { whatsappService } = request.services(); + const { whatsappService } = request.services("app"); console.log("request.auth.credentials:", request.auth.credentials); const bot = await whatsappService.create( diff --git a/apps/metamigo-api/app/services/index.ts b/apps/metamigo-api/src/app/services/index.ts similarity index 85% rename from apps/metamigo-api/app/services/index.ts rename to apps/metamigo-api/src/app/services/index.ts index c26198b..8b5559b 100644 --- a/apps/metamigo-api/app/services/index.ts +++ b/apps/metamigo-api/src/app/services/index.ts @@ -1,13 +1,11 @@ import type * as Hapi from "@hapi/hapi"; import SettingsService from "./settings"; -import RandomService from "./random"; import WhatsappService from "./whatsapp"; import SignaldService from "./signald"; export const register = async (server: Hapi.Server): Promise => { // register your services here // don't forget to add them to the AppServices interface in ../types/index.ts - server.registerService(RandomService); server.registerService(SettingsService); server.registerService(WhatsappService); server.registerService(SignaldService); diff --git a/apps/metamigo-api/app/services/settings.ts b/apps/metamigo-api/src/app/services/settings.ts similarity index 100% rename from apps/metamigo-api/app/services/settings.ts rename to apps/metamigo-api/src/app/services/settings.ts diff --git a/apps/metamigo-api/app/services/signald.ts b/apps/metamigo-api/src/app/services/signald.ts similarity index 92% rename from apps/metamigo-api/app/services/signald.ts rename to apps/metamigo-api/src/app/services/signald.ts index f6118d7..3936d94 100644 --- a/apps/metamigo-api/app/services/signald.ts +++ b/apps/metamigo-api/src/app/services/signald.ts @@ -1,10 +1,10 @@ -/* eslint-disable @typescript-eslint/no-unused-vars */ + import { Server } from "@hapi/hapi"; import { Service } from "@hapipal/schmervice"; import { SignaldAPI, IncomingMessagev1, - ClientMessageWrapperv1 + ClientMessageWrapperv1, } from "@digiresilience/node-signald"; import { SavedSignalBot as Bot } from "@digiresilience/metamigo-db"; import workerUtils from "../../worker-utils"; @@ -54,12 +54,15 @@ export default class SignaldService extends Service { this.signald.on("transport_connected", async () => { this.onConnected(); }); - this.signald.on("transport_received_payload", async (payload: ClientMessageWrapperv1) => { - this.server.logger.debug({ payload }, "signald payload received"); - if (payload.type === "IncomingMessage") { - this.receiveMessage(payload.data) + this.signald.on( + "transport_received_payload", + async (payload: ClientMessageWrapperv1) => { + this.server.logger.debug({ payload }, "signald payload received"); + if (payload.type === "IncomingMessage") { + this.receiveMessage(payload.data); + } } - }); + ); this.signald.on("transport_sent_payload", async (payload) => { this.server.logger.debug({ payload }, "signald payload sent"); }); @@ -163,7 +166,9 @@ export default class SignaldService extends Service { this.server.logger.error("invalid message received"); } - const bot = await this.server.db().signalBots.findBy({ phoneNumber: account }); + const bot = await this.server + .db() + .signalBots.findBy({ phoneNumber: account }); if (!bot) { this.server.logger.info("message received for unknown bot", { account, diff --git a/apps/metamigo-api/app/services/whatsapp.ts b/apps/metamigo-api/src/app/services/whatsapp.ts similarity index 66% rename from apps/metamigo-api/app/services/whatsapp.ts rename to apps/metamigo-api/src/app/services/whatsapp.ts index a821aeb..7759362 100644 --- a/apps/metamigo-api/app/services/whatsapp.ts +++ b/apps/metamigo-api/src/app/services/whatsapp.ts @@ -1,16 +1,31 @@ import { Server } from "@hapi/hapi"; import { Service } from "@hapipal/schmervice"; import { SavedWhatsappBot as Bot } from "@digiresilience/metamigo-db"; -import makeWASocket, { DisconnectReason, proto, downloadContentFromMessage, MediaType } from "@adiwajshing/baileys"; +import makeWASocket, { + DisconnectReason, + proto, + downloadContentFromMessage, + MediaType, + AnyMessageContent, + WAProto, + MiscMessageGenerationOptions, +} from "@adiwajshing/baileys"; import workerUtils from "../../worker-utils"; import { useDatabaseAuthState } from "../lib/whatsapp-key-store"; -import { connect } from "pg-monitor"; export type AuthCompleteCallback = (error?: string) => void; +export type Connection = { + end: (error: Error | undefined) => void; + sendMessage: ( + jid: string, + content: AnyMessageContent, + options?: MiscMessageGenerationOptions + ) => Promise; +}; + export default class WhatsappService extends Service { - connections: { [key: string]: any } = {}; - loginConnections: { [key: string]: any } = {}; + connections: { [key: string]: Connection } = {}; static browserDescription: [string, string, string] = [ "Metamigo", @@ -31,68 +46,87 @@ export default class WhatsappService extends Service { } private async sleep(ms: number): Promise { - console.log(`pausing ${ms}`) - return new Promise(resolve => setTimeout(resolve, ms)); + console.log(`pausing ${ms}`); + return new Promise((resolve) => setTimeout(resolve, ms)); } private async resetConnections() { for (const connection of Object.values(this.connections)) { try { - connection.end(null) + connection.end(null); } catch (error) { console.log(error); } } + this.connections = {}; } - private createConnection(bot: Bot, server: Server, options: any, authCompleteCallback?: any) { - const { state, saveState } = useDatabaseAuthState(bot, server) + private createConnection( + bot: Bot, + server: Server, + options: any, + authCompleteCallback?: any + ) { + const { state, saveState } = useDatabaseAuthState(bot, server); const connection = makeWASocket({ ...options, auth: state }); let pause = 5000; - connection.ev.on('connection.update', async (update) => { - console.log(`Connection updated ${JSON.stringify(update, null, 2)}`) - const { connection: connectionState, lastDisconnect, qr, isNewLogin } = update + connection.ev.on("connection.update", async (update) => { + console.log(`Connection updated ${JSON.stringify(update, null, 2)}`); + const { + connection: connectionState, + lastDisconnect, + qr, + isNewLogin, + } = update; if (qr) { - console.log('got qr code') + console.log("got qr code"); await this.server.db().whatsappBots.updateQR(bot, qr); } else if (isNewLogin) { - console.log("got new login") - } else if (connectionState === 'open') { - console.log('opened connection') + console.log("got new login"); + } else if (connectionState === "open") { + console.log("opened connection"); } else if (connectionState === "close") { - console.log('connection closed due to ', lastDisconnect.error) - const disconnectStatusCode = (lastDisconnect?.error as any)?.output?.statusCode + console.log("connection closed due to", lastDisconnect.error); + const disconnectStatusCode = (lastDisconnect?.error as any)?.output + ?.statusCode; if (disconnectStatusCode === DisconnectReason.restartRequired) { - console.log('reconnecting after got new login') + console.log("reconnecting after got new login"); const updatedBot = await this.findById(bot.id); - this.createConnection(updatedBot, server, options) - authCompleteCallback() + this.createConnection(updatedBot, server, options); + authCompleteCallback(); } else if (disconnectStatusCode !== DisconnectReason.loggedOut) { - console.log('reconnecting') - await this.sleep(pause) - pause = pause * 2 - this.createConnection(bot, server, options) + console.log("reconnecting"); + await this.sleep(pause); + pause *= 2; + this.createConnection(bot, server, options); } } - }) + }); - connection.ev.on('chats.set', item => console.log(`recv ${item.chats.length} chats (is latest: ${item.isLatest})`)) - connection.ev.on('messages.set', item => console.log(`recv ${item.messages.length} messages (is latest: ${item.isLatest})`)) - connection.ev.on('contacts.set', item => console.log(`recv ${item.contacts.length} contacts`)) - connection.ev.on('messages.upsert', async m => { - console.log("messages upsert") + connection.ev.process(async (events) => { + if (events["messaging-history.set"]) { + const { chats, contacts, messages, isLatest } = + events["messaging-history.set"]; + console.log( + `recv ${chats.length} chats, ${contacts.length} contacts, ${messages.length} msgs (is latest: ${isLatest})` + ); + } + }); + + connection.ev.on("messages.upsert", async (m) => { + console.log("messages upsert"); const { messages } = m; if (messages) { await this.queueUnreadMessages(bot, messages); } - }) - connection.ev.on('messages.update', m => console.log(m)) - connection.ev.on('message-receipt.update', m => console.log(m)) - connection.ev.on('presence.update', m => console.log(m)) - connection.ev.on('chats.update', m => console.log(m)) - connection.ev.on('contacts.upsert', m => console.log(m)) - connection.ev.on('creds.update', saveState) + }); + connection.ev.on("messages.update", (m) => console.log(m)); + connection.ev.on("message-receipt.update", (m) => console.log(m)); + connection.ev.on("presence.update", (m) => console.log(m)); + connection.ev.on("chats.update", (m) => console.log(m)); + connection.ev.on("contacts.upsert", (m) => console.log(m)); + connection.ev.on("creds.update", saveState); this.connections[bot.id] = connection; } @@ -103,14 +137,11 @@ export default class WhatsappService extends Service { const bots = await this.server.db().whatsappBots.findAll(); for await (const bot of bots) { if (bot.isVerified) { - this.createConnection( - bot, - this.server, - { - browser: WhatsappService.browserDescription, - printQRInTerminal: false, - version: [2, 2204, 13], - }) + this.createConnection(bot, this.server, { + browser: WhatsappService.browserDescription, + printQRInTerminal: false, + version: [2, 2204, 13], + }); } } } @@ -126,7 +157,7 @@ export default class WhatsappService extends Service { message.imageMessage || message.videoMessage; - let messageContent = Object.values(message)[0] + const messageContent = Object.values(message)[0]; let messageType: MediaType; let attachment: string; let filename: string; @@ -147,17 +178,21 @@ export default class WhatsappService extends Service { key.id + "." + message.imageMessage.mimetype.split("/").pop(); mimetype = message.imageMessage.mimetype; } else if (message.videoMessage) { - messageType = "video" + messageType = "video"; filename = key.id + "." + message.videoMessage.mimetype.split("/").pop(); mimetype = message.videoMessage.mimetype; } - const stream = await downloadContentFromMessage(messageContent, messageType) - let buffer = Buffer.from([]) + const stream = await downloadContentFromMessage( + messageContent, + messageType + ); + let buffer = Buffer.from([]); for await (const chunk of stream) { - buffer = Buffer.concat([buffer, chunk]) + buffer = Buffer.concat([buffer, chunk]); } + attachment = buffer.toString("base64"); } @@ -214,7 +249,12 @@ export default class WhatsappService extends Service { } async register(bot: Bot, callback: AuthCompleteCallback): Promise { - await this.createConnection(bot, this.server, { version: [2, 2204, 13] }, callback); + await this.createConnection( + bot, + this.server, + { version: [2, 2204, 13] }, + callback + ); } async send(bot: Bot, phoneNumber: string, message: string): Promise { diff --git a/apps/metamigo-api/app/types/index.ts b/apps/metamigo-api/src/app/types/index.ts similarity index 70% rename from apps/metamigo-api/app/types/index.ts rename to apps/metamigo-api/src/app/types/index.ts index 8595e00..871fcb5 100644 --- a/apps/metamigo-api/app/types/index.ts +++ b/apps/metamigo-api/src/app/types/index.ts @@ -6,16 +6,10 @@ import type { IAppConfig } from "../../config"; import type { AppDatabase } from "@digiresilience/metamigo-db"; // add your service interfaces here -interface AppServices { - settingsService: ISettingsService; - whatsappService: WhatsappService; - signaldService: SignaldService; -} // extend the hapi types with our services and config declare module "@hapi/hapi" { export interface Request { - services(): AppServices; db(): AppDatabase; pgp: IMain; } @@ -25,3 +19,15 @@ declare module "@hapi/hapi" { pgp: IMain; } } + +declare module "@hapipal/schmervice" { + interface AppServices { + settingsService: ISettingsService; + whatsappService: WhatsappService; + signaldService: SignaldService; + } + + interface SchmerviceDecorator { + (namespace: "app"): AppServices; + } +} diff --git a/apps/metamigo-api/src/config.ts b/apps/metamigo-api/src/config.ts new file mode 100644 index 0000000..2e66ab3 --- /dev/null +++ b/apps/metamigo-api/src/config.ts @@ -0,0 +1,7 @@ + + + + + + +export {default, loadConfig, loadConfigRaw, IAppConfig, IAppConvict} from "@digiresilience/metamigo-config"; \ No newline at end of file diff --git a/apps/metamigo-api/logger.ts b/apps/metamigo-api/src/logger.ts similarity index 81% rename from apps/metamigo-api/logger.ts rename to apps/metamigo-api/src/logger.ts index 58453d7..5002c6f 100644 --- a/apps/metamigo-api/logger.ts +++ b/apps/metamigo-api/src/logger.ts @@ -1,6 +1,6 @@ import { defState } from "@digiresilience/montar"; import { configureLogger } from "@digiresilience/metamigo-common"; -import config from "config"; +import config from "@digiresilience/metamigo-config"; export const logger = defState("apiLogger", { start: async () => configureLogger(config), diff --git a/apps/metamigo-api/server/index.ts b/apps/metamigo-api/src/server/index.ts similarity index 84% rename from apps/metamigo-api/server/index.ts rename to apps/metamigo-api/src/server/index.ts index 5a872b3..0f017a0 100644 --- a/apps/metamigo-api/server/index.ts +++ b/apps/metamigo-api/src/server/index.ts @@ -16,9 +16,9 @@ export const deployment = async ( return server; }; -export const stopDeployment = async (server: Metamigo.Server): Promise => { - return Metamigo.stopDeployment(server); -}; +export const stopDeployment = async ( + server: Metamigo.Server +): Promise => Metamigo.stopDeployment(server); const server = defState("server", { start: () => deployment(config, true), diff --git a/apps/metamigo-api/server/manifest.ts b/apps/metamigo-api/src/server/manifest.ts similarity index 100% rename from apps/metamigo-api/server/manifest.ts rename to apps/metamigo-api/src/server/manifest.ts diff --git a/apps/metamigo-api/worker-utils.ts b/apps/metamigo-api/src/worker-utils.ts similarity index 84% rename from apps/metamigo-api/worker-utils.ts rename to apps/metamigo-api/src/worker-utils.ts index 2f67370..126366f 100644 --- a/apps/metamigo-api/worker-utils.ts +++ b/apps/metamigo-api/src/worker-utils.ts @@ -9,9 +9,7 @@ const startWorkerUtils = async (): Promise => { return workerUtils; }; -const stopWorkerUtils = async (): Promise => { - return workerUtils.release(); -}; +const stopWorkerUtils = async (): Promise => workerUtils.release(); const workerUtils = defState("apiWorkerUtils", { start: startWorkerUtils, diff --git a/apps/metamigo-api/tsconfig.json b/apps/metamigo-api/tsconfig.json index f0e98f3..fe8c7e3 100644 --- a/apps/metamigo-api/tsconfig.json +++ b/apps/metamigo-api/tsconfig.json @@ -1,10 +1,11 @@ { - "extends": "../tsconfig.json", + "extends": "tsconfig-link", "compilerOptions": { "outDir": "build/main", - "types": ["long", "jest", "node"], + "rootDir": "src", + "types": ["jest", "node", "long"], "lib": ["es2020", "DOM"] }, - "include": ["**/*.ts", "**/.*.ts"], - "exclude": ["node_modules"] + "include": ["src/**/*.ts", "src/**/.*.ts"], + "exclude": ["node_modules/**"] } diff --git a/package-lock.json b/package-lock.json index 44896e2..d47f67f 100644 --- a/package-lock.json +++ b/package-lock.json @@ -447,6 +447,7 @@ "@hapipal/toys": "^4.0.0", "blipp": "^4.0.2", "camelcase-keys": "^8.0.2", + "expiry-map": "^2.0.0", "fluent-ffmpeg": "^2.1.2", "graphile-migrate": "^1.4.1", "graphile-worker": "^0.13.0", @@ -9921,6 +9922,17 @@ "node": "^14.15.0 || ^16.10.0 || >=18.0.0" } }, + "node_modules/expiry-map": { + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/expiry-map/-/expiry-map-2.0.0.tgz", + "integrity": "sha512-K1I5wJe2fiqjyUZf/xhxwTpaopw3F+19DsO7Oggl20+3SVTXDIevVRJav0aBMfposQdkl2E4+gnuOKd3j2X0sA==", + "dependencies": { + "map-age-cleaner": "^0.2.0" + }, + "engines": { + "node": ">=8" + } + }, "node_modules/ext": { "version": "1.7.0", "license": "ISC", @@ -14975,6 +14987,17 @@ "tmpl": "1.0.5" } }, + "node_modules/map-age-cleaner": { + "version": "0.2.0", + "resolved": "https://registry.npmjs.org/map-age-cleaner/-/map-age-cleaner-0.2.0.tgz", + "integrity": "sha512-AvxTC6id0fzSf6OyNBTp1syyCuKO7nOJvHgYlhT0Qkkjvk40zZo+av3ayVgXlxnF/DxEzEfY9mMdd7FHsd+wKQ==", + "dependencies": { + "p-defer": "^1.0.0" + }, + "engines": { + "node": ">=7.6" + } + }, "node_modules/map-obj": { "version": "4.3.0", "license": "MIT", @@ -16380,6 +16403,14 @@ "version": "2.1.0", "license": "MIT" }, + "node_modules/p-defer": { + "version": "1.0.0", + "resolved": "https://registry.npmjs.org/p-defer/-/p-defer-1.0.0.tgz", + "integrity": "sha512-wB3wfAxZpk2AzOfUMJNL+d36xothRSyj8EXOa4f6GMqYDN9BJaaSISbsk+wS9abmnebVw95C2Kb5t85UmpCxuw==", + "engines": { + "node": ">=4" + } + }, "node_modules/p-finally": { "version": "1.0.0", "license": "MIT", @@ -21824,7 +21855,6 @@ "license": "AGPL-3.0-or-later", "dependencies": { "@hapi/hapi": "^21.3.0", - "@hapi/hoek": "^11.0.1", "pg-monitor": "^1.4.1", "pg-promise": "^10.11.0" }, @@ -24902,7 +24932,6 @@ "version": "file:packages/hapi-pg-promise", "requires": { "@hapi/hapi": "^21.3.0", - "@hapi/hoek": "^11.0.1", "pg-monitor": "^1.4.1", "pg-promise": "^10.11.0" }, @@ -30367,6 +30396,14 @@ "jest-util": "^29.5.0" } }, + "expiry-map": { + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/expiry-map/-/expiry-map-2.0.0.tgz", + "integrity": "sha512-K1I5wJe2fiqjyUZf/xhxwTpaopw3F+19DsO7Oggl20+3SVTXDIevVRJav0aBMfposQdkl2E4+gnuOKd3j2X0sA==", + "requires": { + "map-age-cleaner": "^0.2.0" + } + }, "ext": { "version": "1.7.0", "requires": { @@ -34672,6 +34709,14 @@ "tmpl": "1.0.5" } }, + "map-age-cleaner": { + "version": "0.2.0", + "resolved": "https://registry.npmjs.org/map-age-cleaner/-/map-age-cleaner-0.2.0.tgz", + "integrity": "sha512-AvxTC6id0fzSf6OyNBTp1syyCuKO7nOJvHgYlhT0Qkkjvk40zZo+av3ayVgXlxnF/DxEzEfY9mMdd7FHsd+wKQ==", + "requires": { + "p-defer": "^1.0.0" + } + }, "map-obj": { "version": "4.3.0" }, @@ -34823,6 +34868,7 @@ "blipp": "^4.0.2", "camelcase-keys": "^8.0.2", "eslint-config-link": "*", + "expiry-map": "^2.0.0", "fluent-ffmpeg": "^2.1.2", "graphile-migrate": "^1.4.1", "graphile-worker": "^0.13.0", @@ -35731,6 +35777,11 @@ "orderedmap": { "version": "2.1.0" }, + "p-defer": { + "version": "1.0.0", + "resolved": "https://registry.npmjs.org/p-defer/-/p-defer-1.0.0.tgz", + "integrity": "sha512-wB3wfAxZpk2AzOfUMJNL+d36xothRSyj8EXOa4f6GMqYDN9BJaaSISbsk+wS9abmnebVw95C2Kb5t85UmpCxuw==" + }, "p-finally": { "version": "1.0.0" }, diff --git a/packages/hapi-pg-promise/package.json b/packages/hapi-pg-promise/package.json index f48766e..8f7acba 100644 --- a/packages/hapi-pg-promise/package.json +++ b/packages/hapi-pg-promise/package.json @@ -10,7 +10,6 @@ }, "dependencies": { "@hapi/hapi": "^21.3.0", - "@hapi/hoek": "^11.0.1", "pg-monitor": "^1.4.1", "pg-promise": "^10.11.0" }, @@ -19,8 +18,7 @@ "fix:lint": "eslint src --ext .ts --fix", "fmt": "prettier \"src/**/*.ts\" --write", "test": "jest --coverage --forceExit --detectOpenHandles --reporters=default --reporters=jest-junit", - "lint": "eslint src --ext .ts", - "lint-fmt": "prettier \"src/**/*.ts\" --list-different", + "lint": "eslint src --ext .ts && prettier \"src/**/*.ts\" --list-different", "doc": "typedoc src/ --exclude '**/*.test.ts' --exclude '**/*.spec.ts' --name $npm_package_name --readme README.md --target es2019 --mode file --out build/docs", "watch:build": "tsc -p tsconfig.json -w" } diff --git a/packages/hapi-pg-promise/src/index.spec.ts b/packages/hapi-pg-promise/src/index.spec.ts index da5af49..7bf9e07 100644 --- a/packages/hapi-pg-promise/src/index.spec.ts +++ b/packages/hapi-pg-promise/src/index.spec.ts @@ -1,5 +1,7 @@ import * as Hapi from "@hapi/hapi"; -import PgPromisePlugin from "."; +import { makePlugin } from "."; + +const plugin = makePlugin(); describe("plugin option validation", () => { let server; @@ -8,7 +10,7 @@ describe("plugin option validation", () => { }); it("should throw when no connection details defined", async () => { - expect(server.register(PgPromisePlugin)).rejects.toThrow(); + expect(server.register(plugin)).rejects.toThrow(); }); }); @@ -20,7 +22,7 @@ describe("basic plugin runtime", () => { beforeEach(async () => { server = new Hapi.Server({ port: 0 }); await server.register({ - plugin: PgPromisePlugin, + plugin, options: defaultOpts, }); await server.start(); @@ -63,7 +65,7 @@ describe("plugin runtime", () => { expect.assertions(5); await server.register({ - plugin: PgPromisePlugin, + plugin, options: { ...defaultOpts, logSql: true, diff --git a/packages/hapi-pg-promise/src/index.ts b/packages/hapi-pg-promise/src/index.ts index 06f94c6..70dac53 100644 --- a/packages/hapi-pg-promise/src/index.ts +++ b/packages/hapi-pg-promise/src/index.ts @@ -1,22 +1,12 @@ import * as Hapi from "@hapi/hapi"; -import * as Hoek from "@hapi/hoek"; import pgPromise from "pg-promise"; import * as pgMonitor from "pg-monitor"; import type { IConnectionParameters } from "pg-promise/typescript/pg-subset"; import type { IMain, IInitOptions } from "pg-promise"; - import { IPGPPluginOptions, ExtendedProtocol } from "./types"; +import { Plugin } from "@hapi/hapi/lib/types/plugin"; export * from "./types"; -const defaultOptions: IPGPPluginOptions = { - connection: undefined, - pgpInit: {}, - logSql: false, - decorateAs: { - pgp: "pgp", - db: "db", - }, -}; export const startDiagnostics = ( logSql: boolean, @@ -45,59 +35,57 @@ const startDb = async ( return db; }; -const register = async ( - server: Hapi.Server, - userOpts?: IPGPPluginOptions -): Promise => { - const options: IPGPPluginOptions = Hoek.applyToDefaults( - defaultOptions, - userOpts - ) as IPGPPluginOptions; +export function makePlugin(): Plugin, void> { + return { + version: "1.0.0", + name: "pg-promise", + async register( + server: Hapi.Server, + userOpts?: IPGPPluginOptions + ): Promise { + if (userOpts.logSql === undefined) userOpts.logSql = false; + if (!userOpts.decorateAs) userOpts.decorateAs = { pgp: "pgp", db: "db" }; - if (!options.connection) { - throw new Error( - "hapi-pg-promise: connection details are not defined. You must specify a valid connection for the plugin to boot." - ); - } + const options = userOpts; - if ("pgp" in options && "pgpInit" in options) { - throw new Error( - "hapi-pg-promise: options pgp and pgpInit are mutually exclusive" - ); - } + if (!options.connection) { + throw new Error( + "hapi-pg-promise: connection details are not defined. You must specify a valid connection for the plugin to boot." + ); + } - let pgp: IMain; - if ("pgp" in options) { - pgp = options.pgp; - } else { - pgp = await startPgp(options.pgpInit); - startDiagnostics(options.logSql, options.pgpInit); - } + if ("pgp" in options && "pgpInit" in options) { + throw new Error( + "hapi-pg-promise: options pgp and pgpInit are mutually exclusive" + ); + } - const db = await startDb(pgp, options.connection); + let pgp: IMain; + if ("pgp" in options) { + pgp = options.pgp; + } else { + pgp = await startPgp(options.pgpInit || {}); + startDiagnostics(options.logSql, options.pgpInit || {}); + } - if (options.decorateAs) { - if (options.decorateAs) { - server.decorate("request", options.decorateAs.pgp, pgp); - server.decorate("server", options.decorateAs.pgp, pgp); - } + const db = await startDb(pgp, options.connection); - if (options.decorateAs.db) { - server.decorate("server", options.decorateAs.db, () => db); - server.decorate("request", options.decorateAs.db, () => db); - } - } + if (options.decorateAs) { + if (options.decorateAs) { + server.decorate("request", options.decorateAs.pgp, pgp); + server.decorate("server", options.decorateAs.pgp, pgp); + } - server.ext("onPostStop", async () => { - stopDiagnostics(); - await db.$pool.end(); - }); -}; + if (options.decorateAs.db) { + server.decorate("server", options.decorateAs.db, () => db); + server.decorate("request", options.decorateAs.db, () => db); + } + } -const pgPromisePlugin = { - register, - name: "pg-promise", - version: "0.0.1", -}; - -export default pgPromisePlugin; + server.ext("onPostStop", async () => { + stopDiagnostics(); + await db.$pool.end(); + }); + }, + }; +} diff --git a/packages/node-signald/package.json b/packages/node-signald/package.json index 35b9a19..55b2254 100644 --- a/packages/node-signald/package.json +++ b/packages/node-signald/package.json @@ -5,8 +5,8 @@ "author": "Abel Luck ", "license": "AGPL-3.0-only", "private": false, - "main": "dist/index.js", - "types": "dist/index.d.ts", + "main": "build/main/index.js", + "types": "types/main/index.d.ts", "files": [ "/dist" ], @@ -16,7 +16,7 @@ "scripts": { "build": "tsc --build --verbose", "watch": "tsc --build --verbose --watch", - "generate": "node util/generate.js && prettier src/generated.ts -w --loglevel error && yarn build", + "generate": "node util/generate.js && prettier src/generated.ts -w --loglevel error && npm run build", "doc": "typedoc src/ --exclude '**/*.test.ts' --exclude '**/*.spec.ts' --name $npm_package_name --readme README.md --out dist/docs", "fix": "echo n/a", "lint": "echo n/a", diff --git a/turbo.json b/turbo.json index 86b1b58..d886f5b 100644 --- a/turbo.json +++ b/turbo.json @@ -14,6 +14,7 @@ "inputs": ["src/**/*.tsx", "src/**/*.ts", "test/**/*.ts", "test/**/*.tsx"] }, "lint": {}, + "fix:lint": {}, "fmt": {}, "deploy": { "dependsOn": ["build", "test", "lint"]