From fdda07bd2e3869799a33e20b1f8911aba71d25c8 Mon Sep 17 00:00:00 2001 From: CarlosGamero Date: Wed, 29 Jan 2025 18:16:26 +0100 Subject: [PATCH] Adding cliCommandWrapper --- scripts/utils/cliCommandWrapper.spec.ts | 67 +++++++++++++++++++++ scripts/utils/cliCommandWrapper.ts | 77 +++++++++++++++++++++++++ 2 files changed, 144 insertions(+) create mode 100644 scripts/utils/cliCommandWrapper.spec.ts create mode 100644 scripts/utils/cliCommandWrapper.ts diff --git a/scripts/utils/cliCommandWrapper.spec.ts b/scripts/utils/cliCommandWrapper.spec.ts new file mode 100644 index 00000000..dbdaeb48 --- /dev/null +++ b/scripts/utils/cliCommandWrapper.spec.ts @@ -0,0 +1,67 @@ +import { type MockInstance, afterEach, beforeEach, describe, expect, it, vi } from 'vitest' +import z from 'zod' +import { cliCommandWrapper } from './cliCommandWrapper.js' + +describe('cliCommandWrapper', () => { + let exitSpy: MockInstance + + beforeEach(() => { + // Mock process.exit before each test + exitSpy = vi.spyOn(process, 'exit').mockImplementation(() => undefined as never) + }) + + afterEach(() => { + vi.restoreAllMocks() + }) + + it.each([ + { inputArgs: ['--key=value'], schema: undefined, expected: undefined }, + { + inputArgs: ['--key=value'], + schema: z.object({ key: z.string() }), + expected: { key: 'value' }, + }, + { inputArgs: ['--flag'], schema: z.object({ flag: z.boolean() }), expected: { flag: true } }, + { + inputArgs: ['--key=value'], + schema: z.object({ flag: z.boolean().optional(), key: z.string() }), + expected: { key: 'value' }, + }, + { + inputArgs: ['--key=value', '-abc', '--flag'], + schema: z.object({ key: z.string(), flag: z.boolean(), a: z.boolean(), b: z.boolean(), c: z.boolean() }), + expected: { key: 'value', flag: true, a: true, b: true, c: true }, + }, + ])('should parse arguments', async ({ inputArgs, schema, expected }) => { + process.argv = ['node', 'script.js', ...inputArgs] + await cliCommandWrapper( + 'command', + (dependencies, requestContext, args) => { + expect(dependencies).toBeDefined() + expect(requestContext).toBeDefined() + expect(args).toEqual(expected) + }, + schema, + ) + expect(exitSpy).toHaveBeenCalledWith(0) + }) + + it('should fail if arguments are not valid', async () => { + process.argv = ['node', 'script.js', '--key=value'] + await cliCommandWrapper( + 'command', + () => undefined, + z.object({ key: z.number() }), + ) + expect(exitSpy).toHaveBeenCalledWith(1) + }) + + it('should fail if cli command fail', async () => { + process.argv = ['node', 'script.js', '--key=value'] + await cliCommandWrapper( + 'command', + () => { throw new Error()}, + ) + expect(exitSpy).toHaveBeenCalledWith(1) + }) +}) diff --git a/scripts/utils/cliCommandWrapper.ts b/scripts/utils/cliCommandWrapper.ts new file mode 100644 index 00000000..961a20b9 --- /dev/null +++ b/scripts/utils/cliCommandWrapper.ts @@ -0,0 +1,77 @@ +import { parseArgs } from 'node:util' +import type { RequestContext } from '@lokalise/fastify-extras' +import { generateMonotonicUuid } from '@lokalise/id-utils' +import { isError } from '@lokalise/universal-ts-utils/node' +import pino from 'pino' +import type z from 'zod' +import { getApp } from '../../src/app.js' +import type { Dependencies } from '../../src/infrastructure/parentDiConfig.js' + +const getArgs = () => { + const { values } = parseArgs({ + args: process.argv, + strict: false, + }) + + return values +} + +export type CliCommand = ( + dependencies: Dependencies, + requestContext: RequestContext, + args?: z.infer, +) => Promise | void + +export const cliCommandWrapper = async ( + cliCommandName: string, + command: CliCommand, + argsSchema?: ArgsSchema, +): Promise => { + const app = await getApp({ + queuesEnabled: false, + jobsEnabled: false, + healthchecksEnabled: false, + monitoringEnabled: false, + }) + + const requestId = generateMonotonicUuid() + const reqContext: RequestContext = { + reqId: requestId, + logger: app.diContainer.cradle.logger.child({ + origin: cliCommandName, + 'x-request-id': requestId, + }), + } + + let args: z.infer | undefined + if (argsSchema) { + const parseResult = argsSchema.safeParse(getArgs()) + if (!parseResult.success) { + reqContext.logger.error( + { + errors: JSON.stringify(parseResult.error.errors), + }, + 'Invalid arguments', + ) + await app.close() + process.exit(1) + } + args = parseResult.data + } + + let isSuccess = true + try { + await command(app.diContainer.cradle, reqContext, args) + } catch (err) { + isSuccess = false + reqContext.logger.error( + { + error: JSON.stringify(isError(err) ? pino.stdSerializers.err(err) : err), + }, + 'Error running command', + ) + } + + await app.close() + process.exit(isSuccess ? 0 : 1) +}