diff --git a/.gitignore b/.gitignore index 656d007..134928c 100644 --- a/.gitignore +++ b/.gitignore @@ -5,4 +5,5 @@ docker dev docker-compose.admin.yml full_reload.sh -tmp \ No newline at end of file +tmp +ecosystem.config.js \ No newline at end of file diff --git a/dashboard/.gitignore b/dashboard/.gitignore index 93dd586..265259f 100644 --- a/dashboard/.gitignore +++ b/dashboard/.gitignore @@ -24,6 +24,7 @@ winston-*.ndjson .env.* !.env.example +ecosystem.config.js # Test reports *.report.txt diff --git a/dashboard/ecosystem.config.js b/dashboard/ecosystem.config.js index 10fdbdb..90f34d0 100644 --- a/dashboard/ecosystem.config.js +++ b/dashboard/ecosystem.config.js @@ -1,7 +1,7 @@ module.exports = { apps: [ { - name: 'Dashboard', + name: 'dashboard', port: '3010', exec_mode: 'fork', script: './.output/server/index.mjs', diff --git a/dashboard/package.json b/dashboard/package.json index 0cc4484..89c7ab4 100644 --- a/dashboard/package.json +++ b/dashboard/package.json @@ -13,7 +13,7 @@ "docker-inspect": "docker run -it litlyx-dashboard sh", "docker-run": "docker run -p 3000:3000 litlyx-dashboard", "workspace:shared": "ts-node ../scripts/dashboard/shared.ts", - "workspace:deploy": "ts-node ../scripts/dashboard/deploy.ts --testmode" + "workspace:deploy": "ts-node ../scripts/dashboard/deploy.ts" }, "dependencies": { "@nuxtjs/tailwindcss": "^6.12.0", diff --git a/email/templates/PurchaseEmail.ts b/email/templates/PurchaseEmail.ts index e7483be..6d7da9d 100644 --- a/email/templates/PurchaseEmail.ts +++ b/email/templates/PurchaseEmail.ts @@ -1,4 +1,3 @@ - export const PURCHASE_EMAIL = ` @@ -21,15 +20,10 @@ export const PURCHASE_EMAIL = `

If you have any questions about your new plan or need assistance, feel free to reach out to our support team at help@litlyx.com. We’re here to help you make the most out of your upgraded plan!

-

Thank you for using Litlyx every day as your analytics tool and for being a part of our journey.

- -

We look forward to continuing to support your growth and success!

-

Best regards,

Antonio,

-

CEO | Litlyx

+

CEO @ Litlyx

- -` \ No newline at end of file +` \ No newline at end of file diff --git a/package.json b/package.json index 06dc075..db858bb 100644 --- a/package.json +++ b/package.json @@ -7,7 +7,10 @@ "dashboard:clear-logs": "ts-node scripts/dashboard/clear-logs.ts", "dashboard:shared": "ts-node scripts/dashboard/shared.ts", "dashboard:deploy": "ts-node scripts/dashboard/deploy.ts", - "producer:shared": "node scripts/producer/shared.js", + + "producer:shared": "ts-node scripts/producer/shared.ts", + "producer:deploy": "ts-node scripts/producer/deploy.ts", + "email:deploy": "ts-node scripts/email/deploy.ts" }, "keywords": [], diff --git a/producer/.gitignore b/producer/.gitignore index 506ca4b..e4e1475 100644 --- a/producer/.gitignore +++ b/producer/.gitignore @@ -1,6 +1,7 @@ node_modules static ecosystem.config.cjs +ecosystem.config.js dist start_dev.js package-lock.json diff --git a/producer/package.json b/producer/package.json index 4ac5e9c..1504016 100644 --- a/producer/package.json +++ b/producer/package.json @@ -1,7 +1,8 @@ { "dependencies": { "cors": "^2.8.5", - "express": "^4.19.2" + "express": "^4.19.2", + "redis": "^4.7.0" }, "devDependencies": { "@types/cors": "^2.8.17", @@ -19,7 +20,9 @@ "build_project": "node ../scripts/build.js", "build": "npm run compile && npm run build_project", "docker-build": "docker build -t litlyx-producer -f Dockerfile ../", - "docker-inspect": "docker run -it litlyx-producer sh" + "docker-inspect": "docker run -it litlyx-producer sh", + "workspace:shared": "ts-node ../scripts/producer/shared.ts", + "workspace:deploy": "ts-node ../scripts/producer/deploy.ts" }, "keywords": [], "author": "Emily", diff --git a/producer/pnpm-lock.yaml b/producer/pnpm-lock.yaml index 866011c..d26ff92 100644 --- a/producer/pnpm-lock.yaml +++ b/producer/pnpm-lock.yaml @@ -14,6 +14,9 @@ importers: express: specifier: ^4.19.2 version: 4.19.2 + redis: + specifier: ^4.7.0 + version: 4.7.0 devDependencies: '@types/cors': specifier: ^2.8.17 @@ -47,6 +50,35 @@ packages: '@jridgewell/trace-mapping@0.3.9': resolution: {integrity: sha512-3Belt6tdc8bPgAtbcmdtNJlirVoTmEb5e2gC94PnkwEW9jI6CAHUeoG85tjWP5WquqfavoMtMwiG4P926ZKKuQ==} + '@redis/bloom@1.2.0': + resolution: {integrity: sha512-HG2DFjYKbpNmVXsa0keLHp/3leGJz1mjh09f2RLGGLQZzSHpkmZWuwJbAvo3QcRY8p80m5+ZdXZdYOSBLlp7Cg==} + peerDependencies: + '@redis/client': ^1.0.0 + + '@redis/client@1.6.0': + resolution: {integrity: sha512-aR0uffYI700OEEH4gYnitAnv3vzVGXCFvYfdpu/CJKvk4pHfLPEy/JSZyrpQ+15WhXe1yJRXLtfQ84s4mEXnPg==} + engines: {node: '>=14'} + + '@redis/graph@1.1.1': + resolution: {integrity: sha512-FEMTcTHZozZciLRl6GiiIB4zGm5z5F3F6a6FZCyrfxdKOhFlGkiAqlexWMBzCi4DcRoyiOsuLfW+cjlGWyExOw==} + peerDependencies: + '@redis/client': ^1.0.0 + + '@redis/json@1.0.7': + resolution: {integrity: sha512-6UyXfjVaTBTJtKNG4/9Z8PSpKE6XgSyEb8iwaqDcy+uKrd/DGYHTWkUdnQDyzm727V7p21WUMhsqz5oy65kPcQ==} + peerDependencies: + '@redis/client': ^1.0.0 + + '@redis/search@1.2.0': + resolution: {integrity: sha512-tYoDBbtqOVigEDMAcTGsRlMycIIjwMCgD8eR2t0NANeQmgK/lvxNAvYyb6bZDD4frHRhIHkJu2TBRvB0ERkOmw==} + peerDependencies: + '@redis/client': ^1.0.0 + + '@redis/time-series@1.1.0': + resolution: {integrity: sha512-c1Q99M5ljsIuc4YdaCwfUEXsofakb9c8+Zse2qxTadu8TalLXuAESzLvFAvNVbkmSlvlzIQOLpBCmWI9wTOt+g==} + peerDependencies: + '@redis/client': ^1.0.0 + '@tsconfig/node10@1.0.11': resolution: {integrity: sha512-DcRjDCujK/kCk/cUe8Xz8ZSpm8mS3mNNpta+jGCA6USEDfktlNvm1+IuZ9eTcDbNk41BHwpHHeW+N1lKCz4zOw==} @@ -126,6 +158,10 @@ packages: resolution: {integrity: sha512-GHTSNSYICQ7scH7sZ+M2rFopRoLh8t2bLSW6BbgrtLsahOIB5iyAVJf9GjWK3cYTDaMj4XdBpM1cA6pIS0Kv2w==} engines: {node: '>= 0.4'} + cluster-key-slot@1.1.2: + resolution: {integrity: sha512-RMr0FhtfXemyinomL4hrWcYJxmX6deFdCxpJzhDttxgO1+bcCnkk+9drydLVDmAMG7NE6aN/fl4F7ucU/90gAA==} + engines: {node: '>=0.10.0'} + content-disposition@0.5.4: resolution: {integrity: sha512-FveZTNuGw04cxlAiWbzi6zTAL/lhehaWbTtgluJh4/E95DqMwTmha3KZN1aAWA8cFIhHzMZUvLevkw5Rqk+tSQ==} engines: {node: '>= 0.6'} @@ -213,6 +249,10 @@ packages: function-bind@1.1.2: resolution: {integrity: sha512-7XHNxH7qX9xG5mIwxkhumTox/MIRNcOgDrxWsMt2pAr23WHp6MrRlN7FBSFpCpr+oVO0F744iUgR82nJMfG2SA==} + generic-pool@3.9.0: + resolution: {integrity: sha512-hymDOu5B53XvN4QT9dBmZxPX4CWhBPPLguTZ9MMFeFa/Kg0xWVfylOVNlJji/E7yTZWFd/q9GO5TxDLq156D7g==} + engines: {node: '>= 4'} + get-intrinsic@1.2.4: resolution: {integrity: sha512-5uYhsJH8VJBTv7oslg4BznJYhDoRI6waYCxMmCdnTrcCrHA/fCFKoTFz2JKKE0HdDFUF7/oQuhzumXJK7paBRQ==} engines: {node: '>= 0.4'} @@ -321,6 +361,9 @@ packages: resolution: {integrity: sha512-8zGqypfENjCIqGhgXToC8aB2r7YrBX+AQAfIPs/Mlk+BtPTztOvTS01NRW/3Eh60J+a48lt8qsCzirQ6loCVfA==} engines: {node: '>= 0.8'} + redis@4.7.0: + resolution: {integrity: sha512-zvmkHEAdGMn+hMRXuMBtu4Vo5P6rHQjLoHftu+lBqq8ZTA3RCVC/WzD790bkKKiNFp7d5/9PcSD19fJyyRvOdQ==} + safe-buffer@5.2.1: resolution: {integrity: sha512-rp3So07KcdmmKbGvgaNxQSJr7bGVSVk5S9Eq1F+ppbRo70+YeaDxkw5Dd8NPN+GD6bjnYm2VuPuCXmpuYvmCXQ==} @@ -395,6 +438,9 @@ packages: resolution: {integrity: sha512-BNGbWLfd0eUPabhkXUVm0j8uuvREyTh5ovRa/dyow/BqAbZJyC+5fU+IzQOzmAKzYqYRAISoRhdQr3eIZ/PXqg==} engines: {node: '>= 0.8'} + yallist@4.0.0: + resolution: {integrity: sha512-3wdGidZyq5PB084XLES5TpOSRA3wjXAlIWMhum2kRcv/41Sn2emQ0dycQW4uZXLejwKvg6EsvbdlVL+FYEct7A==} + yn@3.1.1: resolution: {integrity: sha512-Ux4ygGWsu2c7isFWe8Yu1YluJmqVhxqK2cLXNQA5AcC3QfbGNpM7fu0Y8b/z16pXLnFxZYvWhd3fhBY9DLmC6Q==} engines: {node: '>=6'} @@ -414,6 +460,32 @@ snapshots: '@jridgewell/resolve-uri': 3.1.2 '@jridgewell/sourcemap-codec': 1.4.15 + '@redis/bloom@1.2.0(@redis/client@1.6.0)': + dependencies: + '@redis/client': 1.6.0 + + '@redis/client@1.6.0': + dependencies: + cluster-key-slot: 1.1.2 + generic-pool: 3.9.0 + yallist: 4.0.0 + + '@redis/graph@1.1.1(@redis/client@1.6.0)': + dependencies: + '@redis/client': 1.6.0 + + '@redis/json@1.0.7(@redis/client@1.6.0)': + dependencies: + '@redis/client': 1.6.0 + + '@redis/search@1.2.0(@redis/client@1.6.0)': + dependencies: + '@redis/client': 1.6.0 + + '@redis/time-series@1.1.0(@redis/client@1.6.0)': + dependencies: + '@redis/client': 1.6.0 + '@tsconfig/node10@1.0.11': {} '@tsconfig/node12@1.0.11': {} @@ -514,6 +586,8 @@ snapshots: get-intrinsic: 1.2.4 set-function-length: 1.2.2 + cluster-key-slot@1.1.2: {} + content-disposition@0.5.4: dependencies: safe-buffer: 5.2.1 @@ -615,6 +689,8 @@ snapshots: function-bind@1.1.2: {} + generic-pool@3.9.0: {} + get-intrinsic@1.2.4: dependencies: es-errors: 1.3.0 @@ -707,6 +783,15 @@ snapshots: iconv-lite: 0.4.24 unpipe: 1.0.0 + redis@4.7.0: + dependencies: + '@redis/bloom': 1.2.0(@redis/client@1.6.0) + '@redis/client': 1.6.0 + '@redis/graph': 1.1.1(@redis/client@1.6.0) + '@redis/json': 1.0.7(@redis/client@1.6.0) + '@redis/search': 1.2.0(@redis/client@1.6.0) + '@redis/time-series': 1.1.0(@redis/client@1.6.0) + safe-buffer@5.2.1: {} safer-buffer@2.1.2: {} @@ -795,4 +880,6 @@ snapshots: vary@1.1.2: {} + yallist@4.0.0: {} + yn@3.1.1: {} diff --git a/producer/src/deprecated.ts b/producer/src/deprecated.ts index 08d32ff..920ebdf 100644 --- a/producer/src/deprecated.ts +++ b/producer/src/deprecated.ts @@ -1,7 +1,7 @@ import { Router, json } from "express"; import { createSessionHash, getIPFromRequest } from "./utils"; -import { requireEnv } from "@utils/requireEnv"; -import { RedisStreamService } from "@services/RedisStreamService"; +import { requireEnv } from "./shared/utils/requireEnv"; +import { RedisStreamService } from "./shared/services/RedisStreamService"; const router = Router(); diff --git a/producer/src/index.ts b/producer/src/index.ts index a01a081..826ceef 100644 --- a/producer/src/index.ts +++ b/producer/src/index.ts @@ -1,5 +1,5 @@ -import { requireEnv } from "@utils/requireEnv"; -import { RedisStreamService } from "@services/RedisStreamService"; +import { requireEnv } from "./shared/utils/requireEnv"; +import { RedisStreamService } from "./shared/services/RedisStreamService"; import express from 'express'; import cors from 'cors'; @@ -21,9 +21,9 @@ app.post('/event', express.json(jsonOptions), async (req, res) => { const ip = getIPFromRequest(req); const sessionHash = createSessionHash(req.body.website, ip, req.body.userAgent); const flowHash = createFlowSessionHash(req.body.pid, ip, req.body.userAgent); - await RedisStreamService.addToStream(streamName, { + await RedisStreamService.addToStream(streamName, { ...req.body, _type: 'event', sessionHash, ip, flowHash - }); + }); return res.sendStatus(200); } catch (ex: any) { return res.status(500).json({ error: ex.message }); @@ -59,8 +59,9 @@ app.post('/keep_alive', express.json(jsonOptions), async (req, res) => { }); async function main() { + const PORT = requireEnv("PORT"); await RedisStreamService.connect(); - app.listen(requireEnv("PORT"), () => console.log(`Listening on port ${requireEnv("PORT")}`)); + app.listen(PORT, () => console.log(`Listening on port ${PORT}`)); } main(); diff --git a/producer/src/shared/services/RedisStreamService.ts b/producer/src/shared/services/RedisStreamService.ts new file mode 100644 index 0000000..81a5ff7 --- /dev/null +++ b/producer/src/shared/services/RedisStreamService.ts @@ -0,0 +1,90 @@ +import { createClient } from 'redis'; +import { requireEnv } from '../utils/requireEnv'; + +export type ReadingLoopOptions = { + stream_name: string, + group_name: ConsumerGroup, + consumer_name: string +} + + +type xReadGroupMessage = { id: string, message: { [x: string]: string } } +type xReadGgroupResult = { name: string, messages: xReadGroupMessage[] }[] | null + +const consumerGroups = ['DATABASE'] as const; + +type ConsumerGroup = typeof consumerGroups[number]; + + +export class RedisStreamService { + + private static client = createClient({ + url: requireEnv("REDIS_URL"), + username: requireEnv("REDIS_USERNAME"), + password: requireEnv("REDIS_PASSWORD"), + database: process.env.DEV_MODE === 'true' ? 1 : 0 + }); + + static async connect() { + await this.client.connect(); + } + + static async readFromStream(stream_name: string, group_name: string, consumer_name: string, process_function: (content: Record) => Promise) { + + const result: xReadGgroupResult = await this.client.xReadGroup(group_name, consumer_name, [{ key: stream_name, id: '>' }], { COUNT: 5, BLOCK: 10000 }); + + if (!result) { + setTimeout(() => this.readFromStream(stream_name, group_name, consumer_name, process_function), 10); + return; + } + + for (const entry of result) { + for (const messageData of entry.messages) { + await process_function(messageData.message); + await this.client.xAck(stream_name, group_name, messageData.id); + await this.client.set(`ACK:${group_name}`, messageData.id); + } + } + + await this.trimStream(stream_name); + + setTimeout(() => this.readFromStream(stream_name, group_name, consumer_name, process_function), 10); + return; + + } + + private static async trimStream(stream_name: string) { + + let lastMessageAck = '0'; + + for (const consumerGroup of consumerGroups) { + const lastAck = await this.client.get(`ACK:${consumerGroup}`); + if (!lastAck) continue; + if (lastAck > lastMessageAck) lastMessageAck = lastAck; + } + + await this.client.xTrim(stream_name, 'MINID', lastMessageAck as any); + + } + + static async startReadingLoop(options: ReadingLoopOptions, processFunction: (content: Record) => Promise) { + + if (!consumerGroups.includes(options.group_name)) return console.error('GROUP NAME NOT ALLOWED'); + + console.log('Start reading loop') + + try { + await this.client.xGroupCreate(options.stream_name, options.group_name, '0', { MKSTREAM: true }); + } catch (ex) { + console.log('Group', options.group_name, 'already exist'); + } + + this.readFromStream(options.stream_name, options.group_name, options.consumer_name, processFunction); + } + + static async addToStream(streamName: string, data: Record) { + const result = await this.client.xAdd(streamName, "*", { ...data, timestamp: Date.now().toString() }); + return result; + } + +} diff --git a/producer/src/shared/utils/requireEnv.ts b/producer/src/shared/utils/requireEnv.ts new file mode 100644 index 0000000..a6497e0 --- /dev/null +++ b/producer/src/shared/utils/requireEnv.ts @@ -0,0 +1,9 @@ + +export function requireEnv(name: string, errorMessage?: string) { + if (!process.env[name]) { + console.error(errorMessage || `ENV variable ${name} is required`); + return process.exit(1); + } + console.log('requireEnv', name, process.env[name]); + return process.env[name] as string; +} \ No newline at end of file diff --git a/scripts/dashboard/deploy.ts b/scripts/dashboard/deploy.ts index 6e9590a..f81cce1 100644 --- a/scripts/dashboard/deploy.ts +++ b/scripts/dashboard/deploy.ts @@ -7,17 +7,11 @@ import { DeployHelper } from '../helpers/deploy-helper'; const TMP_PATH = path.join(__dirname, '../../tmp'); const LOCAL_PATH = path.join(__dirname, '../../dashboard'); -const REMOTE_PATH = '/home/testmode/litlyx/dashboard'; +const REMOTE_PATH = '/home/litlyx/dashboard'; const ZIP_NAME = 'dashboard.zip'; -const TESTMODE_PORT = "4010"; -const argvMode = process.argv[2] - -if (argvMode != '--production' && argvMode != '--testmode') { - console.error('use --production or --testmode'); - process.exit(1); -} -const MODE = argvMode === '--production' ? 'production' : 'testmode'; +const MODE = DeployHelper.getMode(); +const SKIP_BUILD = DeployHelper.getArgAt(0) == '--no-build'; console.log('Deploying dashboard in mode:', MODE); @@ -28,20 +22,16 @@ async function main() { if (fs.existsSync(TMP_PATH)) fs.rmSync(TMP_PATH, { force: true, recursive: true }); fs.ensureDirSync(TMP_PATH); - console.log('Building'); - child.execSync(`cd ${LOCAL_PATH} && pnpm i && pnpm run build`) + if (!SKIP_BUILD) { + console.log('Building'); + child.execSync(`cd ${LOCAL_PATH} && pnpm i && pnpm run build`) + } - console.log('Creting zip file'); + console.log('Creating zip file'); const archive = createZip(TMP_PATH + '/' + ZIP_NAME); archive.directory(LOCAL_PATH + '/.output', '/.output'); + archive.file(LOCAL_PATH + '/ecosystem.config.js', { name: '/ecosystem.config.js' }) - if (MODE === 'testmode') { - const ecosystemContent = fs.readFileSync(LOCAL_PATH + '/ecosystem.config.js', 'utf8'); - const devContent = ecosystemContent.replace(/name: '(.*?)'/, "name: 'test-$1'").replace(/3010/, TESTMODE_PORT); - archive.append(Buffer.from(devContent), { name: '/ecosystem.config.js' }); - } else { - archive.file(LOCAL_PATH + '/ecosystem.config.js', { name: '/ecosystem.config.js' }) - } // archive.file(LOCAL_PATH + '/.env', { name: '/.env' }); await archive.finalize(); diff --git a/scripts/email/deploy.ts b/scripts/email/deploy.ts index 4cc8a9c..d81a32e 100644 --- a/scripts/email/deploy.ts +++ b/scripts/email/deploy.ts @@ -5,10 +5,14 @@ import { createZip } from '../helpers/zip-helper'; import { DeployHelper } from '../helpers/deploy-helper'; const TMP_PATH = path.join(__dirname, '../../tmp'); - const LOCAL_PATH = path.join(__dirname, '../../email'); +const REMOTE_PATH = '/home/litlyx/email'; +const ZIP_NAME = 'email.zip'; -const REMOTE_PATH = '/home/production/litlyx/email'; +const MODE = DeployHelper.getMode(); +console.log('Deploying mail-service in mode:', MODE); + +setTimeout(() => { main(); }, 3000); async function main() { @@ -16,11 +20,9 @@ async function main() { fs.ensureDirSync(TMP_PATH); console.log('Creting zip file'); - const archive = createZip(TMP_PATH + '/email.zip'); + const archive = createZip(TMP_PATH + '/' + ZIP_NAME); archive.directory(LOCAL_PATH + '/dist', '/dist'); - archive.file(LOCAL_PATH + '/ecosystem.config.js', { name: '/ecosystem.config.js' }) - archive.file(LOCAL_PATH + '/package.json', { name: '/package.json' }); archive.file(LOCAL_PATH + '/pnpm-lock.yaml', { name: '/pnpm-lock.yaml' }); await archive.finalize(); @@ -42,14 +44,14 @@ async function main() { await scp.mkdir(REMOTE_PATH); console.log('Uploading zip file'); - await scp.uploadFile(TMP_PATH + '/email.zip', REMOTE_PATH + '/email.zip'); + await scp.uploadFile(TMP_PATH + '/' + ZIP_NAME, REMOTE_PATH + '/' + ZIP_NAME); scp.close(); console.log('Cleaning local'); - fs.rmSync(TMP_PATH + '/email.zip', { force: true, recursive: true }); + fs.rmSync(TMP_PATH + '/' + ZIP_NAME, { force: true, recursive: true }); console.log('Extracting remote'); - await DeployHelper.execute(`cd ${REMOTE_PATH} && unzip email.zip && rm -r email.zip`); + await DeployHelper.execute(`cd ${REMOTE_PATH} && unzip ${ZIP_NAME} && rm -r ${ZIP_NAME}`); console.log('Installing remote'); await DeployHelper.execute(`cd ${REMOTE_PATH} && /root/.nvm/versions/node/v21.2.0/bin/pnpm i`); @@ -58,7 +60,4 @@ async function main() { ssh.dispose(); - } - -main(); \ No newline at end of file diff --git a/scripts/helpers/deploy-helper.ts b/scripts/helpers/deploy-helper.ts index 379bf8b..9946695 100644 --- a/scripts/helpers/deploy-helper.ts +++ b/scripts/helpers/deploy-helper.ts @@ -2,22 +2,37 @@ import { Client, ScpClient } from 'node-scp'; import { NodeSSH } from 'node-ssh' import fs from 'fs-extra'; -import { REMOTE_HOST, IDENTITY_FILE } from '../.config' +import { REMOTE_HOST_PRODUCTION, REMOTE_HOST_TESTMODE, IDENTITY_FILE } from '../.config' export class DeployHelper { private static scpClient: ScpClient; private static sshClient: NodeSSH; + static getMode() { + const argvMode = process.argv[2] + if (argvMode != '--production' && argvMode != '--testmode') { + console.error('use --production or --testmode'); + process.exit(1); + } + const MODE = argvMode === '--production' ? 'production' : 'testmode'; + return MODE; + } + + static getArgAt(index: number) { + return process.argv[3 + index]; + } + + static async connect() { this.scpClient = await Client({ - host: REMOTE_HOST, + host: this.getMode() === 'production' ? REMOTE_HOST_PRODUCTION : REMOTE_HOST_TESTMODE, username: 'root', privateKey: fs.readFileSync(IDENTITY_FILE) }) this.sshClient = new NodeSSH(); await this.sshClient.connect({ - host: REMOTE_HOST, + host: this.getMode() === 'production' ? REMOTE_HOST_PRODUCTION : REMOTE_HOST_TESTMODE, username: 'root', privateKeyPath: IDENTITY_FILE }); diff --git a/scripts/helpers/shared-helper.ts b/scripts/helpers/shared-helper.ts index 5b6b876..c3a35f9 100644 --- a/scripts/helpers/shared-helper.ts +++ b/scripts/helpers/shared-helper.ts @@ -8,12 +8,12 @@ export class SharedHelper { constructor(private localSharedPath: string) { } static getSharedPath() { return path.join(__dirname, '../../shared_global'); } - + clear() { if (fs.existsSync(this.localSharedPath)) { fs.rmSync(this.localSharedPath, { force: true, recursive: true }); - fs.mkdirSync(this.localSharedPath); } + fs.mkdirSync(this.localSharedPath); } create(name: string) { diff --git a/scripts/producer/deploy.ts b/scripts/producer/deploy.ts new file mode 100644 index 0000000..68e9122 --- /dev/null +++ b/scripts/producer/deploy.ts @@ -0,0 +1,74 @@ + +import fs from 'fs-extra'; +import path from 'path'; +import { createZip } from '../helpers/zip-helper'; +import { DeployHelper } from '../helpers/deploy-helper'; +import { REMOTE_HOST_TESTMODE } from '../.config'; + +const TMP_PATH = path.join(__dirname, '../../tmp'); +const LOCAL_PATH = path.join(__dirname, '../../producer'); +const REMOTE_PATH = '/home/litlyx/producer'; +const ZIP_NAME = 'producer.zip'; + +const MODE = DeployHelper.getMode(); +console.log('Deploying producer in mode:', MODE); + +setTimeout(() => { main(); }, 3000); + +async function main() { + + if (fs.existsSync(TMP_PATH)) fs.rmSync(TMP_PATH, { force: true, recursive: true }); + fs.ensureDirSync(TMP_PATH); + + console.log('Creting zip file'); + const archive = createZip(TMP_PATH + '/' + ZIP_NAME); + archive.directory(LOCAL_PATH + '/dist', '/dist'); + + if (MODE === 'testmode') { + const ecosystemContent = fs.readFileSync(LOCAL_PATH + '/ecosystem.config.js', 'utf8'); + const devContent = ecosystemContent.replace("redis://litlyx.com", `redis://${REMOTE_HOST_TESTMODE}`); + archive.append(Buffer.from(devContent), { name: '/ecosystem.config.js' }); + } else { + archive.file(LOCAL_PATH + '/ecosystem.config.js', { name: '/ecosystem.config.js' }) + } + + + archive.file(LOCAL_PATH + '/package.json', { name: '/package.json' }); + archive.file(LOCAL_PATH + '/pnpm-lock.yaml', { name: '/pnpm-lock.yaml' }); + await archive.finalize(); + + await DeployHelper.connect(); + + const { scp, ssh } = DeployHelper.instances(); + + console.log('Creating remote structure'); + console.log('Check existing'); + const remoteExist = await scp.exists(REMOTE_PATH); + console.log('Exist', remoteExist); + if (remoteExist) { + console.log('Deleting'); + await DeployHelper.execute(`rm -r ${REMOTE_PATH}`); + } + + console.log('Creating folder'); + await scp.mkdir(REMOTE_PATH); + + console.log('Uploading zip file'); + await scp.uploadFile(TMP_PATH + '/' + ZIP_NAME, REMOTE_PATH + '/' + ZIP_NAME); + scp.close(); + + console.log('Cleaning local'); + fs.rmSync(TMP_PATH + '/' + ZIP_NAME, { force: true, recursive: true }); + + console.log('Extracting remote'); + await DeployHelper.execute(`cd ${REMOTE_PATH} && unzip ${ZIP_NAME} && rm -r ${ZIP_NAME}`); + + console.log('Installing remote'); + await DeployHelper.execute(`cd ${REMOTE_PATH} && /root/.nvm/versions/node/v21.2.0/bin/pnpm i`); + + console.log('Executing remote'); + await DeployHelper.execute(`cd ${REMOTE_PATH} && /root/.nvm/versions/node/v21.2.0/bin/pm2 start ecosystem.config.js`); + + ssh.dispose(); + +} diff --git a/scripts/producer/shared.ts b/scripts/producer/shared.ts index 8b13789..cde34a6 100644 --- a/scripts/producer/shared.ts +++ b/scripts/producer/shared.ts @@ -1 +1,13 @@ +import { SharedHelper } from "../helpers/shared-helper"; +import path from "node:path"; + +const helper = new SharedHelper(path.join(__dirname, '../../producer/src/shared')) + +helper.clear(); + +helper.create('utils'); +helper.copy('utils/requireEnv.ts'); + +helper.create('services'); +helper.copy('services/RedisStreamService.ts'); diff --git a/shared_global/package.json b/shared_global/package.json index b9064b8..81b5cb6 100644 --- a/shared_global/package.json +++ b/shared_global/package.json @@ -3,7 +3,7 @@ "version": "1.0.0", "author": "Emily", "license": "MIT", - "description": "", + "description": "Shared files to be imported", "devDependencies": { "@types/node": "^22.10.10", "date-fns": "^4.1.0", diff --git a/shared_global/tsconfig.json b/shared_global/tsconfig.json index bf396cc..1fe764a 100644 --- a/shared_global/tsconfig.json +++ b/shared_global/tsconfig.json @@ -1,11 +1,12 @@ { - "extends": "../tsconfig.json", "compilerOptions": { "module": "NodeNext", - "target": "ESNext", - "composite": true + "target": "ESNext" }, "include": [ - "**/*.ts" + "./**/*.ts" ], + "exclude": [ + "node_modules" + ] } \ No newline at end of file diff --git a/shared_global/utils/requireEnv.ts b/shared_global/utils/requireEnv.ts index f9b5cdc..a6497e0 100644 --- a/shared_global/utils/requireEnv.ts +++ b/shared_global/utils/requireEnv.ts @@ -4,5 +4,6 @@ export function requireEnv(name: string, errorMessage?: string) { console.error(errorMessage || `ENV variable ${name} is required`); return process.exit(1); } + console.log('requireEnv', name, process.env[name]); return process.env[name] as string; } \ No newline at end of file