diff --git a/migrations/20230312_210600_create_configs_table.js b/migrations/20230312_210600_create_configs_table.js new file mode 100644 index 00000000..45aed174 --- /dev/null +++ b/migrations/20230312_210600_create_configs_table.js @@ -0,0 +1,13 @@ +exports.up = function (knex) { + return knex.schema.createTable('configs', (config) => { + config.unique(['key', 'category']) + config.uuid('id').primary().defaultTo(knex.raw('uuid_generate_v4()')) + config.text('key').notNullable().index() + config.jsonb('value').notNullable().index() + config.text('category').notNullable().index() + }) +} + +exports.down = function (knex) { + return knex.schema.dropTable('configs') +} diff --git a/package-lock.json b/package-lock.json index decde40f..5dc8da28 100644 --- a/package-lock.json +++ b/package-lock.json @@ -10,7 +10,7 @@ "license": "MIT", "dependencies": { "@noble/secp256k1": "1.7.1", - "axios": "^1.2.3", + "axios": "1.2.3", "bech32": "2.0.0", "body-parser": "1.20.1", "debug": "4.3.4", @@ -20,7 +20,7 @@ "helmet": "6.0.1", "joi": "17.7.0", "js-yaml": "4.1.0", - "knex": "^2.4.1", + "knex": "2.4.1", "pg": "8.8.0", "pg-query-stream": "4.2.4", "ramda": "0.28.0", diff --git a/resources/default-settings.yaml b/resources/default-settings.yaml index 62bd4c4e..4351fd8d 100755 --- a/resources/default-settings.yaml +++ b/resources/default-settings.yaml @@ -10,7 +10,7 @@ payments: feeSchedules: admission: - enabled: false - descripton: Admission fee charged per public key in msats (1000 msats = 1 satoshi) + description: Admission fee charged per public key in msats (1000 msats = 1 satoshi) amount: 1000000 whitelists: pubkeys: diff --git a/seeds/configs.json b/seeds/configs.json new file mode 100644 index 00000000..04d0ca6c --- /dev/null +++ b/seeds/configs.json @@ -0,0 +1,348 @@ +[ + { + "key": "relay_url", + "value": "wss://nostream.your-domain.com", + "category": "info" + }, + { + "key": "name", + "value": "nostream.your-domain.com", + "category": "info" + }, + { + "key": "description", + "value": "A nostr relay written in Typescript.", + "category": "info" + }, + { + "key": "pubkey", + "value": "replace-with-your-pubkey-in-hex", + "category": "info" + }, + { + "key": "contact", + "value": "operator@your-domain.com", + "category": "info" + }, + { + "key": "enabled", + "value": false, + "category": "authentication" + }, + { + "key": "enabled", + "value": false, + "category": "payments" + }, + { + "key": "processor", + "value": "zebedee", + "category": "payments" + }, + { + "key": "feeSchedules", + "value": { + "admission": [ + { + "enabled": false, + "description": "Admission fee charged per public key in msats (1000 msats = 1 satoshi)", + "amount": 1000000, + "whitelists": { + "pubkeys": [ + "replace-with-your-pubkey-in-hex" + ] + } + } + ], + "publication": [ + { + "enabled": false, + "description": "Publication fee charged per event in msats (1000 msats = 1 satoshi)", + "amount": 10, + "whitelists": { + "pubkeys": [ + "replace-with-your-pubkey-in-hex" + ] + } + } + ] + }, + "category": "payments" + }, + { + "key": "zebedee", + "value": { + "baseURL": "https://api.zebedee.io/", + "callbackBaseURL": "https://nostream.your-domain.com/callbacks/zebedee", + "ipWhitelist": [ + "3.225.112.64", + "::ffff:3.225.112.64" + ] + }, + "category": "paymentsProcessors" + }, + { + "key": "maxPayloadSize", + "value": 131072, + "category": "network" + }, + { + "key": "remoteIpHeader", + "value": "x-forwarded-for", + "category": "network" + }, + { + "key": "idleTimeout", + "value": 60, + "category": "network" + }, + { + "key": "count", + "value": 0, + "category": "workers" + }, + { + "key": "static", + "value": [], + "category": "mirroring" + }, + { + "key": "invoice", + "value": { + "rateLimits": [ + { + "period": 60000, + "rate": 3 + }, + { + "period": 3600000, + "rate": 10 + }, + { + "period": 86400000, + "rate": 20 + } + ], + "ipWhitelist": [ + "::1", + "10.10.10.1", + "::ffff:10.10.10.1" + ] + }, + "category": "limits" + }, + { + "key": "connection", + "value": { + "rateLimits": [ + { + "period": 1000, + "rate": 6 + }, + { + "period": 60000, + "rate": 30 + }, + { + "period": 3600000, + "rate": 300 + }, + { + "period": 86400000, + "rate": 1440 + } + ], + "ipWhitelist": [ + "::1", + "10.10.10.1", + "::ffff:10.10.10.1" + ] + }, + "category": "limits" + }, + { + "key": "event", + "value": { + "eventId": { + "minLeadingZeroBits": 0 + }, + "kind": { + "whitelist": [], + "blacklist": [] + }, + "pubkey": { + "minBalance": 0, + "minLeadingZeroBits": 0, + "whitelist": [], + "blacklist": [] + }, + "createdAt": { + "maxPositiveDelta": 900, + "maxNegativeDelta": 0 + }, + "content": [ + { + "description": "64 KB for event kind ranges 0-10 and 40-49", + "kinds": [ + [ + 0, + 10 + ], + [ + 40, + 49 + ] + ], + "maxLength": 65536 + }, + { + "description": "96 KB for event kind ranges 11-39 and 50-max", + "kinds": [ + [ + 11, + 39 + ], + [ + 50, + 9007199254740991 + ] + ], + "maxLength": 98304 + } + ], + "rateLimits": [ + { + "description": "6 events/min for event kinds 0, 3, 40 and 41", + "kinds": [ + 0, + 3, + 40, + 41 + ], + "period": 60000, + "rate": 6 + }, + { + "description": "12 events/min for event kinds 1, 2, 4 and 42", + "kinds": [ + 1, + 2, + 4, + 42 + ], + "period": 60000, + "rate": 12 + }, + { + "description": "360 events/hour for event kinds 1, 2, 4 and 42", + "kinds": [ + 1, + 2, + 4, + 42 + ], + "period": 3600000, + "rate": 360 + }, + { + "description": "30 events/min for event kind ranges 5-7 and 43-49", + "kinds": [ + [ + 5, + 7 + ], + [ + 43, + 49 + ] + ], + "period": 60000, + "rate": 30 + }, + { + "description": "24 events/min for replaceable events and parameterized replaceable events", + "kinds": [ + [ + 10000, + 19999 + ], + [ + 30000, + 39999 + ] + ], + "period": 60000, + "rate": 24 + }, + { + "description": "60 events/min for ephemeral events", + "kinds": [ + [ + 20000, + 29999 + ] + ], + "period": 60000, + "rate": 60 + }, + { + "description": "720 events/hour for all events", + "period": 3600000, + "rate": 720 + }, + { + "description": "2880 events/day for all events", + "period": 86400000, + "rate": 2880 + } + ], + "whitelists": { + "pubkeys": [], + "ipAddresses": [ + "::1", + "10.10.10.1", + "::ffff:10.10.10.1" + ] + } + }, + "category": "limits" + }, + { + "key": "client", + "value": { + "subscription": { + "maxSubscriptions": 10, + "maxFilters": 10 + } + }, + "category": "limits" + }, + { + "key": "message", + "value": { + "rateLimits": [ + { + "description": "120 raw messages/min", + "period": 60000, + "rate": 120 + }, + { + "description": "3600 raw messages/hour", + "period": 3600000, + "rate": 3600 + }, + { + "description": "86400 raw messages/day", + "period": 86400000, + "rate": 86400 + } + ], + "ipWhitelist": [ + "::1", + "10.10.10.1", + "::ffff:10.10.10.1" + ] + }, + "category": "limits" + } +] \ No newline at end of file diff --git a/seeds/initial-configs.js b/seeds/initial-configs.js new file mode 100644 index 00000000..d310fe0b --- /dev/null +++ b/seeds/initial-configs.js @@ -0,0 +1,100 @@ +/* eslint-disable @typescript-eslint/no-var-requires */ +const { extname, join } = require('path') +const fs = require('fs') +const yaml = require('js-yaml') +const { mergeDeepLeft } = require('ramda') + +const SettingsFileTypes = { + yaml: 'yaml', + json: 'json', +} + +exports.seed = async function (knex) { + const settingsFilePath = `${process.cwd()}/seeds/configs.json` + const defaultConfigs = JSON.parse(fs.readFileSync(settingsFilePath, 'utf-8')) + + const rawConfigs = getConfigs() + const parsedConfigs = parseAll(rawConfigs) + + const rawMergedSettings = mergeDeepLeft(defaultConfigs, parsedConfigs) + const mergedSettings = Object.keys(rawMergedSettings).map(s => { + const setting = rawMergedSettings[s] + setting.value = JSON.stringify(setting.value) + return setting + }) + + for (const setting of mergedSettings) { + try { + await knex('configs').insert([setting]) + } catch (err) { + console.log('Warning: failed to insert config due to error: ', err) + } + } +} + +const getConfigs = () => { + const settingsFilePath = process.env.NOSTR_CONFIG_DIR ?? join(process.cwd(), '.nostr') + + const files = fs.readdirSync(settingsFilePath) + const settingsFilesTotal = files.filter(file => file.match(/settings/)) + + if (settingsFilesTotal.length > 1) { + throw new Error('There are more than 1 settings file, please delete all files that contain the word settings in their name, and restart the relay') + } + + const filteredFile = files.find(fn => fn.startsWith('settings')) + + let settingsFile + if (filteredFile) { + const extension = extname(filteredFile).substring(1) + if (SettingsFileTypes[extension]) { + const settingsFileNamePath = `${settingsFilePath}/settings.${extension}` + if (extension === SettingsFileTypes.json) { + settingsFile = loadAndParseJsonFile(settingsFileNamePath) + } else { + settingsFile = loadAndParseYamlFile(settingsFileNamePath) + } + } + } + + return settingsFile +} + +const loadAndParseJsonFile = path => { + return JSON.parse( + fs.readFileSync( + path, + { encoding: 'utf-8' } + ) + ) +} + +const loadAndParseYamlFile = path => { + const defaultSettingsFileContent = fs.readFileSync(path, { encoding: 'utf-8' }) + const defaults = yaml.load(defaultSettingsFileContent) + return defaults +} + +const parseAll = (jsonConfigs) => { + if (!jsonConfigs) return + + const keys = Object.keys(jsonConfigs) + + const configs = keys.map(key => { + return parseOneLevelDeepConfigs(jsonConfigs[key], key) + }) + + return configs.flat() +} + +const parseOneLevelDeepConfigs = (configs, category) => { + const flattenedConfigs = Object.keys(configs).map(key => { + return { + key, + value: JSON.stringify(configs[key]), + category, + } + }) + + return flattenedConfigs +} diff --git a/src/@types/repositories.ts b/src/@types/repositories.ts index 2470bff2..efa5fc61 100644 --- a/src/@types/repositories.ts +++ b/src/@types/repositories.ts @@ -3,6 +3,7 @@ import { PassThrough } from 'stream' import { DatabaseClient, EventId, Pubkey } from './base' import { DBEvent, Event } from './event' import { Invoice } from './invoice' +import { Setting } from './setting' import { SubscriptionFilter } from './subscription' import { User } from './user' @@ -41,3 +42,9 @@ export interface IUserRepository { upsert(user: Partial, client?: DatabaseClient): Promise getBalanceByPubkey(pubkey: Pubkey, client?: DatabaseClient): Promise } + +export interface ISettingRepository { + getSetting(category: string, key: string, client?: DatabaseClient): Promise + getSettings(): Promise + upsertSetting(config: Partial, client?: DatabaseClient): Promise +} diff --git a/src/@types/setting.ts b/src/@types/setting.ts new file mode 100644 index 00000000..2a93499e --- /dev/null +++ b/src/@types/setting.ts @@ -0,0 +1,30 @@ +export interface Setting { + key: string, + value: object, + category: Category, + createdAt: Date, + updatedAt: Date, +} + +enum Category { + info = 'info', + payments = 'payments', + paymentsProcessors = 'paymentsProcessors', + network = 'network', + workers = 'workers', + mirroring = 'mirroring', + limits = 'limits', + // invoice + // connection + // event + // client + // message +} + +export interface DBSetting { + key: string, + value: object, + category: Category, + created_at: Date, + updated_at: Date, +} diff --git a/src/app/app.ts b/src/app/app.ts index 7e02cbbc..65a7ea9b 100644 --- a/src/app/app.ts +++ b/src/app/app.ts @@ -9,7 +9,6 @@ import { IRunnable } from '../@types/base' import packageJson from '../../package.json' import { Serializable } from 'child_process' import { Settings } from '../@types/settings' -import { SettingsStatic } from '../utils/settings' const debug = createLogger('app-primary') @@ -38,7 +37,6 @@ export class App implements IRunnable { public run(): void { const settings = this.settings() - this.watchers = SettingsStatic.watchSettings() console.log(` ███▄ █ ▒█████ ██████ ▄▄▄█████▓ ██▀███ ▓█████ ▄▄▄ ███▄ ▄███▓ ██ ▀█ █ ▒██▒ ██▒▒██ ▒ ▓ ██▒ ▓▒▓██ ▒ ██▒▓█ ▀▒████▄ ▓██▒▀█▀ ██▒ diff --git a/src/app/worker.ts b/src/app/worker.ts index b5cce811..b6a1adad 100644 --- a/src/app/worker.ts +++ b/src/app/worker.ts @@ -3,7 +3,6 @@ import { IWebSocketServerAdapter } from '../@types/adapters' import { createLogger } from '../factories/logger-factory' import { FSWatcher } from 'fs' -import { SettingsStatic } from '../utils/settings' const debug = createLogger('app-worker') export class AppWorker implements IRunnable { @@ -23,8 +22,6 @@ export class AppWorker implements IRunnable { } public run(): void { - this.watchers = SettingsStatic.watchSettings() - const port = process.env.PORT || process.env.RELAY_PORT || 8008 this.adapter.listen(typeof port === 'number' ? port : Number(port)) } diff --git a/src/index.ts b/src/index.ts index 7d8ef98b..75a1e116 100644 --- a/src/index.ts +++ b/src/index.ts @@ -4,26 +4,36 @@ dotenv.config() import { appFactory } from './factories/app-factory' import { maintenanceWorkerFactory } from './factories/maintenance-worker-factory' +import { SettingsStatic } from './utils/settings' import { staticMirroringWorkerFactory } from './factories/static-mirroring.worker-factory' import { workerFactory } from './factories/worker-factory' -export const getRunner = () => { - if (cluster.isPrimary) { - return appFactory() - } else { - switch (process.env.WORKER_TYPE) { - case 'worker': - return workerFactory() - case 'maintenance': - return maintenanceWorkerFactory() - case 'static-mirroring': - return staticMirroringWorkerFactory() - default: - throw new Error(`Unknown worker: ${process.env.WORKER_TYPE}`) - } - } +export const getRunner = (): any => { + const settingsInstance = SettingsStatic.instance + + settingsInstance.init() + .then(() => { + if (cluster.isPrimary) { + return appFactory().run() + } else { + switch (process.env.WORKER_TYPE) { + case 'worker': + return workerFactory().run() + case 'maintenance': + return maintenanceWorkerFactory().run() + case 'static-mirroring': + return staticMirroringWorkerFactory().run() + default: + throw new Error(`Unknown worker: ${process.env.WORKER_TYPE}`) + } + } + }) + .catch(error => { + console.log('whoooops---------', error) + throw new Error('Failed to load settings', error) + }) } if (require.main === module) { - getRunner().run() + getRunner() } diff --git a/src/repositories/settings-repository.ts b/src/repositories/settings-repository.ts new file mode 100644 index 00000000..a398a450 --- /dev/null +++ b/src/repositories/settings-repository.ts @@ -0,0 +1,79 @@ +import { always, applySpec, omit, prop } from 'ramda' + +import { DBSetting, Setting } from '../@types/setting' +import { createLogger } from '../factories/logger-factory' +import { DatabaseClient } from '../@types/base' +import { fromDBSetting } from '../utils/transform' +import { ISettingRepository } from '../@types/repositories' + +const debug = createLogger('config-repository') + +export class SettingRepository implements ISettingRepository { + public constructor(private readonly dbClient: DatabaseClient) { } + + public async getSetting( + category: string, + key: string, + client: DatabaseClient = this.dbClient + ): Promise { + debug('find config by key: %s and category %s', category, key) + const [dbsetting] = await client('configs') + .where('key', key) + .where('category', category) + .select() + + if (!dbsetting) { + return + } + + return fromDBSetting(dbsetting) + } + + public async getSettings( + client: DatabaseClient = this.dbClient + ): Promise { + debug('get all configs') + const settings = await client('configs') + .select() + + if (!settings) { + return + } + + return settings + } + + public async upsertSetting( + config: Setting, + client: DatabaseClient = this.dbClient, + ): Promise { + debug('upsert: %o', config) + + const date = new Date() + + const row = applySpec({ + key: prop('key'), + value: prop('value'), + category: prop('category'), + updated_at: always(date), + created_at: always(date), + })(config) + + const query = client('configs') + .insert(row) + .onConflict('key') + .merge( + omit([ + 'value', + 'category', + 'created_at', + ])(row) + ) + + return { + then: (onfulfilled: (value: number) => T1 | PromiseLike, onrejected: (reason: any) => T2 | PromiseLike) => query.then(prop('rowCount') as () => number).then(onfulfilled, onrejected), + catch: (onrejected: (reason: any) => T | PromiseLike) => query.catch(onrejected), + toString: (): string => query.toString(), + } as Promise + } +} diff --git a/src/utils/event.ts b/src/utils/event.ts index 7edd0263..133a28c4 100644 --- a/src/utils/event.ts +++ b/src/utils/event.ts @@ -289,7 +289,7 @@ export const getEventExpiration = (event: Event): number | undefined => { const expirationTime = Number(rawExpirationTime) if ((Number.isSafeInteger(expirationTime) && Math.log10(expirationTime))) { return expirationTime - } + } } export const getEventProofOfWork = (eventId: EventId): number => { diff --git a/src/utils/settings.ts b/src/utils/settings.ts index 55092c33..93ec57fd 100644 --- a/src/utils/settings.ts +++ b/src/utils/settings.ts @@ -1,138 +1,81 @@ -import fs from 'fs' -import yaml from 'js-yaml' - -import { extname, join } from 'path' -import { mergeDeepRight } from 'ramda' - import { createLogger } from '../factories/logger-factory' +import { DatabaseClient } from '../@types/base' +import { getMasterDbClient } from '../database/client' +import { Setting } from '../@types/setting' +import { SettingRepository } from '../repositories/settings-repository' import { Settings } from '../@types/settings' const debug = createLogger('settings') -export enum SettingsFileTypes { - yaml = 'yaml', - json = 'json', -} - export class SettingsStatic { - static _settings: Settings | undefined - - public static getSettingsFileBasePath(): string { - return process.env.NOSTR_CONFIG_DIR ?? join(process.cwd(), '.nostr') - } - - public static getDefaultSettingsFilePath(): string { - return join(process.cwd(), 'resources', 'default-settings.yaml') - } + private static _instance: SettingsStatic + private static dbClient: DatabaseClient + static _settings: any | undefined + static settingsRepository: SettingRepository | undefined + + private constructor(dbClient: DatabaseClient) { + SettingsStatic.dbClient = dbClient + SettingsStatic.settingsRepository = new SettingRepository(dbClient) + if (SettingsStatic._instance) { + throw new Error('Use Singleton.instance instead of new.') + } - public static loadAndParseYamlFile(path: string): Settings { - const defaultSettingsFileContent = fs.readFileSync(path, { encoding: 'utf-8' }) - const defaults = yaml.load(defaultSettingsFileContent) as Settings - return defaults + SettingsStatic._instance = this } - public static loadAndParseJsonFile(path: string) { - return JSON.parse( - fs.readFileSync( - path, - { encoding: 'utf-8' } - ) - ) + public async init() { + debug('SettingsStatic.init()') + await SettingsStatic.loadSettingsFromDb() + //const settingsPromise = await SettingsStatic.loadSettingsFromDb(SettingsStatic.constructSettingsJsonBlob) + //if (settingsPromise) { + // resolve('success') + //} + // reject('Failed to initialize settings') } - public static settingsFileType(path: string): SettingsFileTypes | undefined { - const files: string[] = fs.readdirSync(path) - const filteredFile = files.find(fn => fn.startsWith('settings')) - if (filteredFile) { - const extension = extname(filteredFile).substring(1) - if (SettingsFileTypes[extension]) { - return SettingsFileTypes[extension] - } - } - - return SettingsFileTypes.yaml + static get instance() { + return SettingsStatic._instance ?? (SettingsStatic._instance = new SettingsStatic(getMasterDbClient())) } - public static loadSettings(path: string, fileType: SettingsFileTypes) { - debug('loading settings from %s', path) - - switch (fileType) { - case SettingsFileTypes.json: { - console.warn('settings.json is deprecated, please use a yaml file based on resources/default-settings.yaml') - return SettingsStatic.loadAndParseJsonFile(path) - } - case SettingsFileTypes.yaml: { - return SettingsStatic.loadAndParseYamlFile(path) - } - default: { - throw new Error('settings file was missing or did not contain .yaml or .json extensions.') - } - } + private static async loadSettingsFromDb() { + debug('SettingsStatic.loadSettingsFromDb()') + const rawDbSettings = await SettingsStatic.settingsRepository.getSettings() + const parsedSettings = SettingsStatic.constructSettingsJsonBlob(rawDbSettings) + this._settings = parsedSettings + console.log('rawDbSettings', rawDbSettings) + console.log('parsedSettings', parsedSettings) + + // return promise.then(rawSettingsFromDb => { + // const settingsJsonBlob = callback(rawSettingsFromDb) + // this._settings = settingsJsonBlob + // }) } public static createSettings(): Settings { - if (SettingsStatic._settings) { - return SettingsStatic._settings - } - debug('creating settings') - - const basePath = SettingsStatic.getSettingsFileBasePath() - if (!fs.existsSync(basePath)) { - fs.mkdirSync(basePath) - } - const defaultsFilePath = SettingsStatic.getDefaultSettingsFilePath() - const fileType = SettingsStatic.settingsFileType(basePath) - const settingsFilePath = join(basePath, `settings.${fileType}`) - - const defaults = SettingsStatic.loadSettings(defaultsFilePath, SettingsFileTypes.yaml) - - try { - if (fileType) { - SettingsStatic._settings = mergeDeepRight( - defaults, - SettingsStatic.loadSettings(settingsFilePath, fileType) - ) - } else { - SettingsStatic.saveSettings(basePath, defaults) - SettingsStatic._settings = mergeDeepRight({}, defaults) - } - - if (typeof SettingsStatic._settings === 'undefined') { - throw new Error('Unable to set settings') - } + return this._settings + } - return SettingsStatic._settings - } catch (error) { - debug('error reading config file at %s: %o', settingsFilePath, error) + public static async updateSetting(config: Setting) { + await SettingsStatic.settingsRepository.upsertSetting(config) - return defaults - } + this.updateSingletonSettings(config) } - public static saveSettings(path: string, settings: Settings) { - debug('saving settings to %s: %o', path, settings) - return fs.writeFileSync( - join(path, 'settings.yaml'), - yaml.dump(settings), - { encoding: 'utf-8' }, - ) + private static updateSingletonSettings(setting) { + const updatedSettings = this._settings + updatedSettings[setting.category][setting.key] = setting.value + this._settings = updatedSettings } - public static watchSettings() { - const basePath = SettingsStatic.getSettingsFileBasePath() - const defaultsFilePath = SettingsStatic.getDefaultSettingsFilePath() - const fileType = SettingsStatic.settingsFileType(basePath) - const settingsFilePath = join(basePath, `settings.${fileType}`) - - const reload = () => { - console.log('reloading settings') - SettingsStatic._settings = undefined - SettingsStatic.createSettings() - } + private static constructSettingsJsonBlob(rawSettingsFromDb): any { + const settings = {} + rawSettingsFromDb.forEach(setting => { + if (!settings[setting.category]) { + settings[setting.category] = {} + } + settings[setting.category][setting.key] = setting.value + }) - return [ - fs.watch(defaultsFilePath, 'utf8', reload), - fs.watch(settingsFilePath, 'utf8', reload), - ] + return settings } } diff --git a/src/utils/transform.ts b/src/utils/transform.ts index a5b8ccf9..459940ff 100644 --- a/src/utils/transform.ts +++ b/src/utils/transform.ts @@ -2,6 +2,7 @@ import { always, applySpec, ifElse, is, isNil, path, pipe, prop, propSatisfies } import { bech32 } from 'bech32' import { Invoice } from '../@types/invoice' +import { Setting } from '../@types/setting' import { User } from '../@types/user' export const toJSON = (input: any) => JSON.stringify(input) @@ -41,6 +42,14 @@ export const fromDBUser = applySpec({ updatedAt: prop('updated_at'), }) +export const fromDBSetting = applySpec({ + key: prop('key'), + value: prop('value'), + category: prop('category'), + createdAt: prop('created_at'), + updatedAt: prop('updated_at'), +}) + export const fromBech32 = (input: string) => { const { prefix, words } = bech32.decode(input) if (!input.startsWith(prefix)) { diff --git a/test/integration/features/shared.ts b/test/integration/features/shared.ts index 38aaa854..3965514d 100644 --- a/test/integration/features/shared.ts +++ b/test/integration/features/shared.ts @@ -11,7 +11,6 @@ import { import { assocPath, pipe } from 'ramda' import { fromEvent, map, Observable, ReplaySubject, Subject, takeUntil } from 'rxjs' import WebSocket, { MessageEvent } from 'ws' -import Sinon from 'sinon' import { connect, createIdentity, createSubscription, sendEvent } from './helpers' import { getMasterDbClient, getReadReplicaDbClient } from '../../../src/database/client' @@ -39,7 +38,6 @@ BeforeAll({ timeout: 1000 }, async function () { dbClient = getMasterDbClient() rrDbClient = getReadReplicaDbClient() await dbClient.raw('SELECT 1=1') - Sinon.stub(SettingsStatic, 'watchSettings') const settings = SettingsStatic.createSettings() SettingsStatic._settings = pipe(