Skip to content

Commit

Permalink
CLI command wrapper (#1037)
Browse files Browse the repository at this point in the history
  • Loading branch information
CarlosGamero authored Jan 29, 2025
1 parent bd8df4d commit 0d6ec38
Show file tree
Hide file tree
Showing 7 changed files with 163 additions and 112 deletions.
10 changes: 10 additions & 0 deletions package-lock.json

Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.

1 change: 1 addition & 0 deletions package.json
Original file line number Diff line number Diff line change
Expand Up @@ -50,6 +50,7 @@
"@lokalise/healthcheck-utils": "^1.4.0",
"@lokalise/id-utils": "^2.2.0",
"@lokalise/node-core": "^13.4.0",
"@lokalise/universal-ts-utils": "^3.0.0",
"@lokalise/zod-extras": "^2.1.0",
"@message-queue-toolkit/amqp": "^17.1.0",
"@message-queue-toolkit/core": "^18.1.0",
Expand Down
15 changes: 7 additions & 8 deletions scripts/cmd/getUserImportJobs.ts
Original file line number Diff line number Diff line change
@@ -1,20 +1,19 @@
import type { RequestContext } from '@lokalise/fastify-extras'
import z from 'zod'
import { createCliContext, destroyCliContext } from '../utils/cliContextUtils.js'
import type { Dependencies } from '../../src/infrastructure/parentDiConfig.js'
import { cliCommandWrapper } from '../utils/cliCommandWrapper.js'

const origin = 'getUserImportJobsCommand'
const ARGUMENTS_SCHEMA = z.object({
queue: z.enum(['active', 'failed', 'delayed', 'completed', 'waiting', 'prioritized']),
})
type Arguments = z.infer<typeof ARGUMENTS_SCHEMA>

async function run() {
const { app, logger, args } = await createCliContext<Arguments>(ARGUMENTS_SCHEMA, origin)
const userImportJob = app.diContainer.cradle.userImportJob
const command = async (deps: Dependencies, reqContext: RequestContext, args: Arguments) => {
const userImportJob = deps.userImportJob

const jobs = await userImportJob.getJobsInQueue([args.queue])
logger.info(jobs, `${args.queue} jobs`)

await destroyCliContext(app)
reqContext.logger.info(jobs, `${args.queue} jobs`)
}

void run()
void cliCommandWrapper(origin, command, ARGUMENTS_SCHEMA)
68 changes: 68 additions & 0 deletions scripts/utils/cliCommandWrapper.spec.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,68 @@
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)
})
})
77 changes: 77 additions & 0 deletions scripts/utils/cliCommandWrapper.ts
Original file line number Diff line number Diff line change
@@ -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<
ArgsSchema extends z.Schema | undefined,
Args = ArgsSchema extends z.Schema ? z.infer<ArgsSchema> : undefined,
> = (dependencies: Dependencies, requestContext: RequestContext, args: Args) => Promise<void> | void

export const cliCommandWrapper = async <ArgsSchema extends z.Schema | undefined>(
cliCommandName: string,
command: CliCommand<ArgsSchema>,
argsSchema?: ArgsSchema,
): Promise<void> => {
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 = 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)
}
47 changes: 0 additions & 47 deletions scripts/utils/cliContextUtils.spec.ts

This file was deleted.

57 changes: 0 additions & 57 deletions scripts/utils/cliContextUtils.ts

This file was deleted.

0 comments on commit 0d6ec38

Please sign in to comment.