@@ -0,0 +1,11 @@ | |||||
root = true | |||||
[*] | |||||
charset = utf-8 | |||||
end_of_line = lf | |||||
indent_size = tab | |||||
indent_style = tab | |||||
insert_final_newline = true | |||||
max_line_length = 120 | |||||
tab_width = 2 | |||||
trim_trailing_whitespace = true |
@@ -0,0 +1,3 @@ | |||||
node_modules | |||||
.DS_Store | |||||
.idea/ |
@@ -0,0 +1,6 @@ | |||||
- We could make a common code generation strategy between | |||||
CLI and Web API since we have unified the code structure. | |||||
- What this means is we just need to declare the connection | |||||
from the core SDK to the CLI or Web API functions, then we | |||||
can run some sort of code generation, so we don't have to | |||||
bother writing the Web API/CLI code manually. |
@@ -0,0 +1,107 @@ | |||||
# Logs | |||||
logs | |||||
*.log | |||||
npm-debug.log* | |||||
yarn-debug.log* | |||||
yarn-error.log* | |||||
lerna-debug.log* | |||||
# Diagnostic reports (https://nodejs.org/api/report.html) | |||||
report.[0-9]*.[0-9]*.[0-9]*.[0-9]*.json | |||||
# Runtime data | |||||
pids | |||||
*.pid | |||||
*.seed | |||||
*.pid.lock | |||||
# Directory for instrumented libs generated by jscoverage/JSCover | |||||
lib-cov | |||||
# Coverage directory used by tools like istanbul | |||||
coverage | |||||
*.lcov | |||||
# nyc test coverage | |||||
.nyc_output | |||||
# Grunt intermediate storage (https://gruntjs.com/creating-plugins#storing-task-files) | |||||
.grunt | |||||
# Bower dependency directory (https://bower.io/) | |||||
bower_components | |||||
# node-waf configuration | |||||
.lock-wscript | |||||
# Compiled binary addons (https://nodejs.org/api/addons.html) | |||||
build/Release | |||||
# Dependency directories | |||||
node_modules/ | |||||
jspm_packages/ | |||||
# TypeScript v1 declaration files | |||||
typings/ | |||||
# TypeScript cache | |||||
*.tsbuildinfo | |||||
# Optional npm cache directory | |||||
.npm | |||||
# Optional eslint cache | |||||
.eslintcache | |||||
# Microbundle cache | |||||
.rpt2_cache/ | |||||
.rts2_cache_cjs/ | |||||
.rts2_cache_es/ | |||||
.rts2_cache_umd/ | |||||
# Optional REPL history | |||||
.node_repl_history | |||||
# Output of 'npm pack' | |||||
*.tgz | |||||
# Yarn Integrity file | |||||
.yarn-integrity | |||||
# dotenv environment variables file | |||||
.env | |||||
.env.production | |||||
.env.development | |||||
# parcel-bundler cache (https://parceljs.org/) | |||||
.cache | |||||
# Next.js build output | |||||
.next | |||||
# Nuxt.js build / generate output | |||||
.nuxt | |||||
dist | |||||
# Gatsby files | |||||
.cache/ | |||||
# Comment in the public line in if your project uses Gatsby and *not* Next.js | |||||
# https://nextjs.org/blog/next-9-1#public-directory-support | |||||
# public | |||||
# vuepress build output | |||||
.vuepress/dist | |||||
# Serverless directories | |||||
.serverless/ | |||||
# FuseBox cache | |||||
.fusebox/ | |||||
# DynamoDB Local files | |||||
.dynamodb/ | |||||
# TernJS port file | |||||
.tern-port | |||||
.npmrc |
@@ -0,0 +1,71 @@ | |||||
{ | |||||
"name": "@modal-sh/cli", | |||||
"version": "0.0.0", | |||||
"files": [ | |||||
"dist", | |||||
"src" | |||||
], | |||||
"engines": { | |||||
"node": ">=12" | |||||
}, | |||||
"license": "UNLICENSED", | |||||
"keywords": [ | |||||
"pridepack" | |||||
], | |||||
"devDependencies": { | |||||
"@types/node": "^20.12.7", | |||||
"@types/yargs": "^17.0.32", | |||||
"pridepack": "2.6.0", | |||||
"tslib": "^2.6.2", | |||||
"typescript": "^5.4.5", | |||||
"vitest": "^1.5.0" | |||||
}, | |||||
"scripts": { | |||||
"prepublishOnly": "pridepack clean && pridepack build", | |||||
"build": "pridepack build", | |||||
"type-check": "pridepack check", | |||||
"lint": "pridepack lint", | |||||
"clean": "pridepack clean", | |||||
"watch": "pridepack watch", | |||||
"start": "pridepack start", | |||||
"dev": "pridepack dev", | |||||
"test": "vitest" | |||||
}, | |||||
"private": true, | |||||
"description": "CLI app.", | |||||
"repository": { | |||||
"url": "", | |||||
"type": "git" | |||||
}, | |||||
"homepage": "", | |||||
"bugs": { | |||||
"url": "" | |||||
}, | |||||
"author": "TheoryOfNekomata <allan.crisostomo@outlook.com>", | |||||
"publishConfig": { | |||||
"access": "restricted" | |||||
}, | |||||
"types": "./dist/types/index.d.ts", | |||||
"main": "./dist/cjs/production/index.js", | |||||
"module": "./dist/esm/production/index.js", | |||||
"exports": { | |||||
".": { | |||||
"development": { | |||||
"require": "./dist/cjs/development/index.js", | |||||
"import": "./dist/esm/development/index.js" | |||||
}, | |||||
"require": "./dist/cjs/production/index.js", | |||||
"import": "./dist/esm/production/index.js", | |||||
"types": "./dist/types/index.d.ts" | |||||
} | |||||
}, | |||||
"typesVersions": { | |||||
"*": {} | |||||
}, | |||||
"dependencies": { | |||||
"@clack/prompts": "^0.7.0", | |||||
"@modal-sh/core": "workspace:*", | |||||
"pino": "^8.20.0", | |||||
"yargs": "^17.7.2" | |||||
} | |||||
} |
@@ -0,0 +1,3 @@ | |||||
{ | |||||
"target": "es2018" | |||||
} |
@@ -0,0 +1,9 @@ | |||||
import { Cli, CliOptions } from './packages/cli-wrapper'; | |||||
export const createCli = (options: CliOptions) => { | |||||
const cli = new Cli(options); | |||||
// TODO setup CLI here | |||||
return cli; | |||||
}; |
@@ -0,0 +1,22 @@ | |||||
import { Cli } from './packages/cli-wrapper'; | |||||
import { AdderController, AdderControllerImpl } from './modules/adder'; | |||||
export const addCommands = (cli: Cli) => { | |||||
const adderController: AdderController = new AdderControllerImpl(); | |||||
return cli | |||||
.command({ | |||||
aliases: ['a'], | |||||
command: 'add', | |||||
parameters: ['<a> <b>'], | |||||
describe: 'Add two numbers', | |||||
handler: adderController.addNumbers, | |||||
}) | |||||
.command({ | |||||
aliases: ['s'], | |||||
command: 'subtract', | |||||
parameters: ['<a> <b>'], | |||||
describe: 'Subtract two numbers', | |||||
handler: adderController.subtractNumbers, | |||||
}); | |||||
}; |
@@ -0,0 +1,11 @@ | |||||
import { addCommands } from '@/commands'; | |||||
import { createCli } from '@/cli'; | |||||
import { hideBin } from 'yargs/helpers'; | |||||
const cli = createCli({ | |||||
name: 'cli', | |||||
}); | |||||
addCommands(cli); | |||||
cli.run(hideBin(process.argv)); |
@@ -0,0 +1,82 @@ | |||||
import { | |||||
AdderService, | |||||
AdderServiceImpl, | |||||
ArgumentOutOfRangeError, | |||||
InvalidArgumentTypeError, | |||||
} from './adder.service'; | |||||
import { CommandHandler } from '../../packages/cli-wrapper'; | |||||
export interface AdderController { | |||||
addNumbers: CommandHandler; | |||||
subtractNumbers: CommandHandler; | |||||
} | |||||
export class AdderControllerImpl implements AdderController { | |||||
constructor( | |||||
private readonly adderService: AdderService = new AdderServiceImpl(), | |||||
) { | |||||
// noop | |||||
} | |||||
readonly addNumbers: CommandHandler = (params) => { | |||||
if (!params.interactive) { | |||||
const checkArgs = params.args as Record<string, unknown>; | |||||
if (typeof checkArgs.a === 'undefined') { | |||||
params.logger.error('Missing required argument: a'); | |||||
return -1; | |||||
} | |||||
if (typeof checkArgs.b === 'undefined') { | |||||
params.logger.error('Missing required argument: b'); | |||||
return -1; | |||||
} | |||||
} | |||||
const { a, b } = params.args; | |||||
try { | |||||
const response = this.adderService.addNumbers({ a: Number(a), b: Number(b) }); | |||||
params.logger.info(response); | |||||
} catch (errorRaw) { | |||||
const error = errorRaw as Error; | |||||
params.logger.error(error.message); | |||||
if (error instanceof InvalidArgumentTypeError) { | |||||
return -1; | |||||
} | |||||
if (error instanceof ArgumentOutOfRangeError) { | |||||
return -2; | |||||
} | |||||
return -3; | |||||
} | |||||
return 0; | |||||
} | |||||
readonly subtractNumbers: CommandHandler = (params) => { | |||||
if (!params.interactive) { | |||||
const checkArgs = params.args as Record<string, unknown>; | |||||
if (typeof checkArgs.a === 'undefined') { | |||||
params.logger.error('Missing required argument: a'); | |||||
return -1; | |||||
} | |||||
if (typeof checkArgs.b === 'undefined') { | |||||
params.logger.error('Missing required argument: b'); | |||||
return -1; | |||||
} | |||||
} | |||||
const { a, b } = params.args; | |||||
try { | |||||
const response = this.adderService.addNumbers({ a: Number(a), b: -(Number(b)) }); | |||||
params.logger.info(response); | |||||
} catch (errorRaw) { | |||||
const error = errorRaw as Error; | |||||
params.logger.error(error.message); | |||||
if (error instanceof InvalidArgumentTypeError) { | |||||
return -1; | |||||
} | |||||
if (error instanceof ArgumentOutOfRangeError) { | |||||
return -2; | |||||
} | |||||
return -3; | |||||
} | |||||
return 0; | |||||
} | |||||
} |
@@ -0,0 +1,21 @@ | |||||
import { | |||||
add, | |||||
AddFunctionOptions, | |||||
ArgumentOutOfRangeError, | |||||
InvalidArgumentTypeError, | |||||
} from '@modal-sh/core'; | |||||
export interface AdderService { | |||||
addNumbers(options: AddFunctionOptions): number; | |||||
} | |||||
export class AdderServiceImpl implements AdderService { | |||||
addNumbers(options: AddFunctionOptions) { | |||||
return add(options); | |||||
} | |||||
} | |||||
export { | |||||
ArgumentOutOfRangeError, | |||||
InvalidArgumentTypeError, | |||||
}; |
@@ -0,0 +1,2 @@ | |||||
export * from './adder.controller'; | |||||
export * from './adder.service'; |
@@ -0,0 +1,337 @@ | |||||
import yargs, { Argv } from 'yargs'; | |||||
import * as clack from '@clack/prompts'; | |||||
import { DummyWriteStream } from './write-stream'; | |||||
import pino, {LogFn} from 'pino'; | |||||
import * as util from 'util'; | |||||
export interface Logger extends pino.BaseLogger {} | |||||
export interface CommandHandlerArgs { | |||||
self: any; | |||||
interactive: boolean; | |||||
send: (message: number) => void; | |||||
logger: Logger; | |||||
args: any; | |||||
} | |||||
export type CommandHandler = (args: CommandHandlerArgs) => void | number | Promise<number> | Promise<void>; | |||||
export interface CommandArgs { | |||||
aliases?: string[]; | |||||
command: string; | |||||
parameters: string[]; | |||||
describe: string; | |||||
handler: CommandHandler; | |||||
interactiveMode?: boolean; | |||||
} | |||||
type PromptValueArgs = [Record<string, string>, ...never[]] | |||||
| [string, string] | |||||
| [[string, string], ...never[]] | |||||
| [[string, string][], ...never[]]; | |||||
export interface TestModeResult { | |||||
exitCode?: number; | |||||
stdout: DummyWriteStream; | |||||
stderr: DummyWriteStream; | |||||
} | |||||
interface InteractiveModeOptions { | |||||
option: string; | |||||
alias: string; | |||||
intro: string; | |||||
cancelled: string; | |||||
outro: string; | |||||
describe: string; | |||||
} | |||||
export interface CliOptions { | |||||
name: string; | |||||
interactiveMode?: Partial<InteractiveModeOptions>; | |||||
logger?: Logger | boolean; | |||||
} | |||||
export class Cli { | |||||
private testMode = false; | |||||
private promptValues = {} as Record<string, unknown>; | |||||
private testModeResult: TestModeResult = { | |||||
exitCode: undefined as number | undefined, | |||||
stdout: new DummyWriteStream(), | |||||
stderr: new DummyWriteStream(), | |||||
}; | |||||
private readonly cli: Argv; | |||||
constructor(private readonly options: CliOptions) { | |||||
this.cli = yargs() | |||||
.scriptName(options.name) | |||||
.help() | |||||
.fail(false) | |||||
.strict(false); | |||||
} | |||||
private exit(code: number) { | |||||
if (this.testMode) { | |||||
this.testModeResult.exitCode = code; | |||||
return; | |||||
} | |||||
process.exit(code); | |||||
} | |||||
private async prompt(messages: Record<string, string>, interactiveModeOptions: InteractiveModeOptions) { | |||||
const { | |||||
intro, | |||||
cancelled, | |||||
outro, | |||||
} = interactiveModeOptions; | |||||
const messagesEntries = Object.entries(messages); | |||||
if (messagesEntries.length > 0) { | |||||
clack.intro(intro); | |||||
const values = await clack.group( | |||||
Object.fromEntries( | |||||
messagesEntries.map(([key, value]) => [key, () => clack.text({ message: value })]) | |||||
), | |||||
{ | |||||
onCancel: () => { | |||||
clack.cancel(cancelled); | |||||
this.exit(1); | |||||
} | |||||
} | |||||
); | |||||
clack.outro(outro); | |||||
return values; | |||||
} | |||||
return {}; | |||||
} | |||||
private async generateCommandArgs( | |||||
context: any, | |||||
originalCommandArgs: Record<string, unknown>, | |||||
interactive: boolean, | |||||
interactiveModeOptions: InteractiveModeOptions, | |||||
) { | |||||
if (!interactive) { | |||||
return originalCommandArgs; | |||||
} | |||||
// TODO properly filter attributes | |||||
const clackGroup = context.optional | |||||
.filter((optional: { cmd: string[] }) => { | |||||
const { | |||||
'$0': _$0, | |||||
i: _i, | |||||
interactive: _interactive, | |||||
test: _test, | |||||
_: __, | |||||
...rest | |||||
} = originalCommandArgs; | |||||
return !Object.keys(rest).includes(optional.cmd[0]); | |||||
}) | |||||
.reduce( | |||||
(group: Record<string, string>, optional: { cmd: string[], describe: string }) => ({ | |||||
...group, | |||||
[optional.cmd[0]]: optional.describe ?? optional.cmd[0], | |||||
}), | |||||
{} as Record<string, string>, | |||||
); | |||||
const promptedArgs = this.testMode | |||||
? this.promptValues | |||||
: await this.prompt(clackGroup, interactiveModeOptions); | |||||
return { | |||||
...originalCommandArgs, | |||||
...promptedArgs, | |||||
}; | |||||
} | |||||
private buildHandler(handlerFn: Function, interactiveModeOptions: InteractiveModeOptions) { | |||||
const thisCli = this; | |||||
return async function handler(this: any, commandArgs: Record<string, unknown>) { | |||||
const self = this; | |||||
const { option, alias } = interactiveModeOptions; | |||||
const interactiveLong = option in commandArgs ? Boolean(commandArgs.interactive) : false; | |||||
const interactiveShort = alias in commandArgs ? Boolean(commandArgs.i) : false; | |||||
const interactive = interactiveLong || interactiveShort; | |||||
const buildArgs = await thisCli.generateCommandArgs( | |||||
self, | |||||
commandArgs, | |||||
interactive, | |||||
interactiveModeOptions, | |||||
); | |||||
const stdoutLogFn: LogFn = (...args: unknown[]) => { | |||||
const stream = thisCli.testMode ? thisCli.testModeResult.stdout : process.stdout; | |||||
const [arg0, arg1, ...etcArgs] = args; | |||||
if (typeof arg0 === 'string' || typeof arg0 === 'number') { | |||||
if (arg1) { | |||||
if (etcArgs.length > 0) { | |||||
stream.write(util.format(`${arg0}\n`, arg1, ...etcArgs)); | |||||
} else { | |||||
stream.write(util.format(`${arg0}\n`, arg1)); | |||||
} | |||||
} else { | |||||
stream.write(util.format(`${arg0}\n`)); | |||||
} | |||||
} else if (typeof arg0 === 'object' && (typeof arg1 === 'string' || typeof arg1 === 'number')) { | |||||
if (etcArgs.length > 0) { | |||||
stream.write(util.format(`${arg1}\n`, ...etcArgs)); | |||||
} else { | |||||
stream.write(util.format(`${arg1}\n`)); | |||||
} | |||||
} | |||||
}; | |||||
const stderrLogFn: LogFn = (...args: unknown[]) => { | |||||
const stream = thisCli.testMode ? thisCli.testModeResult.stderr : process.stderr; | |||||
const [arg0, arg1, ...etcArgs] = args; | |||||
if (typeof arg0 === 'string' || typeof arg0 === 'number') { | |||||
if (arg1) { | |||||
if (etcArgs.length > 0) { | |||||
stream.write(util.format(`${arg0}\n`, arg1, ...etcArgs)); | |||||
} else { | |||||
stream.write(util.format(`${arg0}\n`, arg1)); | |||||
} | |||||
} else { | |||||
stream.write(util.format(`${arg0}\n`)); | |||||
} | |||||
} else if (typeof arg0 === 'object' && (typeof arg1 === 'string' || typeof arg1 === 'number')) { | |||||
if (etcArgs.length > 0) { | |||||
stream.write(util.format(`${arg1}\n`, ...etcArgs)); | |||||
} else { | |||||
stream.write(util.format(`${arg1}\n`)); | |||||
} | |||||
} | |||||
}; | |||||
const loggerFn = thisCli.options.logger; | |||||
const defaultLogger = { | |||||
level: 'info', | |||||
enabled: typeof loggerFn === 'boolean' ? loggerFn : true, | |||||
debug: stdoutLogFn, | |||||
info: stdoutLogFn, | |||||
warn: stdoutLogFn, | |||||
error: stderrLogFn, | |||||
fatal: stderrLogFn, | |||||
trace: stdoutLogFn, | |||||
silent: () => {}, | |||||
} as Logger; | |||||
const loggerBooleanFn = typeof loggerFn === 'boolean' && loggerFn ? defaultLogger : { | |||||
level: 'silent', | |||||
debug: () => {}, | |||||
info: () => {}, | |||||
warn: () => {}, | |||||
error: () => {}, | |||||
fatal: () => {}, | |||||
trace: () => {}, | |||||
silent: () => {}, | |||||
} as Logger; | |||||
const logger = typeof loggerFn === 'undefined' ? defaultLogger : (typeof loggerFn === 'function' ? loggerFn : loggerBooleanFn); | |||||
let exited = false; | |||||
const returnCode = await handlerFn({ | |||||
self, | |||||
interactive, | |||||
logger, | |||||
send: (code: number) => { | |||||
exited = true; | |||||
thisCli.exit(code); | |||||
}, | |||||
args: buildArgs, | |||||
}); | |||||
if (!exited) { | |||||
thisCli.exit(returnCode ? returnCode : 0); | |||||
} | |||||
}; | |||||
} | |||||
command({ parameters, command, interactiveMode = true, handler, ...args }: CommandArgs): Cli { | |||||
// TODO type-safe declaration of positional/optional arguments | |||||
const { | |||||
option = 'interactive', | |||||
alias = 'i', | |||||
describe = 'Interactive mode', | |||||
intro = 'Please provide the following values:', | |||||
cancelled = 'Operation cancelled.', | |||||
outro = 'Thank you!', | |||||
} = this.options?.interactiveMode ?? {}; | |||||
const commandArgs = { | |||||
...args, | |||||
command: `${command} ${parameters.join(' ').replace(/</g, '[').replace(/>/g, ']')}`, | |||||
usage: parameters.map((p) => `${command} ${p}`).join('\n'), | |||||
handler: this.buildHandler(handler, { | |||||
option, | |||||
alias, | |||||
intro, | |||||
cancelled, | |||||
outro, | |||||
describe, | |||||
}), | |||||
} as Record<string, unknown> | |||||
if (interactiveMode) { | |||||
commandArgs.options = { | |||||
...(commandArgs.options ?? {}), | |||||
[option]: { | |||||
alias, | |||||
describe, | |||||
type: 'boolean', | |||||
default: false, | |||||
hidden: true, | |||||
}, | |||||
}; | |||||
} | |||||
this.cli.command(commandArgs as unknown as Parameters<typeof this.cli.command>[0]); | |||||
return this; | |||||
} | |||||
async run(args: string[]): Promise<void> { | |||||
if (args.length > 0) { | |||||
await this.cli.parse(args); | |||||
return; | |||||
} | |||||
this.cli.showHelp(); | |||||
} | |||||
test() { | |||||
this.testMode = true; | |||||
this.promptValues = {}; | |||||
const thisCli = this; | |||||
return { | |||||
promptValue(...args: PromptValueArgs) { | |||||
const [testArg] = args; | |||||
if (typeof testArg === 'string') { | |||||
const [key, value] = args as [string, unknown]; | |||||
thisCli.promptValues[key] = thisCli.promptValues[key] ?? value; | |||||
} | |||||
if (Array.isArray(testArg)) { | |||||
const [key, value] = testArg as [string, unknown]; | |||||
thisCli.promptValues[key] = thisCli.promptValues[key] ?? value; | |||||
} | |||||
if (typeof testArg === 'object' && testArg !== null) { | |||||
Object.entries(testArg).forEach(([key, value]) => { | |||||
thisCli.promptValues[key] = thisCli.promptValues[key] ?? value; | |||||
}); | |||||
} | |||||
return this; | |||||
}, | |||||
async run(args: string[]): Promise<TestModeResult> { | |||||
await thisCli.cli.parse(args); | |||||
return thisCli.testModeResult; | |||||
}, | |||||
}; | |||||
} | |||||
} |
@@ -0,0 +1,19 @@ | |||||
import {WriteStream} from 'tty'; | |||||
export class DummyWriteStream extends WriteStream { | |||||
private bufferInternal = Buffer.from(''); | |||||
constructor() { | |||||
super(0); | |||||
} | |||||
write(input: Uint8Array | string, _encoding?: BufferEncoding | ((err?: Error) => void), _cb?: (err?: Error) => void): boolean { | |||||
this.bufferInternal = Buffer.concat([this.bufferInternal, Buffer.from(input)]); | |||||
// noop | |||||
return true; | |||||
} | |||||
get buffer(): Buffer { | |||||
return this.bufferInternal; | |||||
} | |||||
} |
@@ -0,0 +1,68 @@ | |||||
import { describe, it, expect, vi, beforeAll } from 'vitest'; | |||||
import { createCli } from '../src/cli'; | |||||
import { addCommands } from '../src/commands'; | |||||
import { AdderServiceImpl, InvalidArgumentTypeError, ArgumentOutOfRangeError } from '../src/modules/adder'; | |||||
import { Cli } from '../src/packages/cli-wrapper'; | |||||
vi.mock('process'); | |||||
describe('cli', () => { | |||||
let cli: Cli; | |||||
beforeAll(() => { | |||||
cli = createCli({ | |||||
name: 'cli-test', | |||||
logger: false, | |||||
}); | |||||
addCommands(cli); | |||||
}); | |||||
it('returns result when successful', async () => { | |||||
const response = await cli.test().run(['add', '1', '2']); | |||||
expect(response.exitCode).toBe(0); | |||||
}); | |||||
it('returns error when given invalid inputs', async () => { | |||||
vi.spyOn(AdderServiceImpl.prototype, 'addNumbers').mockImplementationOnce(() => { | |||||
throw new InvalidArgumentTypeError('Invalid input'); | |||||
}); | |||||
const response = await cli.test().run(['add', '1', '2']); | |||||
expect(response.exitCode).toBe(-1); | |||||
}); | |||||
it('returns error when given out-of-range inputs', async () => { | |||||
vi.spyOn(AdderServiceImpl.prototype, 'addNumbers').mockImplementationOnce(() => { | |||||
throw new ArgumentOutOfRangeError('Out of range'); | |||||
}); | |||||
const response = await cli.test().run(['add', '1', '2']); | |||||
expect(response.exitCode).toBe(-2); | |||||
}); | |||||
it('returns error when an unexpected error occurs', async () => { | |||||
vi.spyOn(AdderServiceImpl.prototype, 'addNumbers').mockImplementationOnce(() => { | |||||
throw new Error('Unexpected error'); | |||||
}); | |||||
const response = await cli.test().run(['add', '1', '2']); | |||||
expect(response.exitCode).toBe(-3); | |||||
}); | |||||
it('returns error when given insufficient arguments', async () => { | |||||
const response = await cli.test().run(['add', '1']); | |||||
expect(response.exitCode).toBe(-1); | |||||
}); | |||||
describe('on interactive mode', () => { | |||||
it('prompts values for insufficient arguments', async () => { | |||||
const response = await cli | |||||
.test() | |||||
.promptValue({ | |||||
a: '1', | |||||
b: '2', | |||||
}) | |||||
.run(['add', '-i']); | |||||
expect(response.exitCode).not.toBe(-1); | |||||
}); | |||||
}); | |||||
}); |
@@ -0,0 +1,21 @@ | |||||
{ | |||||
"exclude": ["node_modules"], | |||||
"include": ["src", "types"], | |||||
"compilerOptions": { | |||||
"module": "ESNext", | |||||
"lib": ["ESNext"], | |||||
"importHelpers": true, | |||||
"declaration": true, | |||||
"sourceMap": true, | |||||
"rootDir": "./src", | |||||
"strict": true, | |||||
"noUnusedLocals": true, | |||||
"noUnusedParameters": true, | |||||
"noImplicitReturns": true, | |||||
"noFallthroughCasesInSwitch": true, | |||||
"moduleResolution": "node", | |||||
"jsx": "react", | |||||
"esModuleInterop": true, | |||||
"target": "es2018" | |||||
} | |||||
} |
@@ -0,0 +1,107 @@ | |||||
# Logs | |||||
logs | |||||
*.log | |||||
npm-debug.log* | |||||
yarn-debug.log* | |||||
yarn-error.log* | |||||
lerna-debug.log* | |||||
# Diagnostic reports (https://nodejs.org/api/report.html) | |||||
report.[0-9]*.[0-9]*.[0-9]*.[0-9]*.json | |||||
# Runtime data | |||||
pids | |||||
*.pid | |||||
*.seed | |||||
*.pid.lock | |||||
# Directory for instrumented libs generated by jscoverage/JSCover | |||||
lib-cov | |||||
# Coverage directory used by tools like istanbul | |||||
coverage | |||||
*.lcov | |||||
# nyc test coverage | |||||
.nyc_output | |||||
# Grunt intermediate storage (https://gruntjs.com/creating-plugins#storing-task-files) | |||||
.grunt | |||||
# Bower dependency directory (https://bower.io/) | |||||
bower_components | |||||
# node-waf configuration | |||||
.lock-wscript | |||||
# Compiled binary addons (https://nodejs.org/api/addons.html) | |||||
build/Release | |||||
# Dependency directories | |||||
node_modules/ | |||||
jspm_packages/ | |||||
# TypeScript v1 declaration files | |||||
typings/ | |||||
# TypeScript cache | |||||
*.tsbuildinfo | |||||
# Optional npm cache directory | |||||
.npm | |||||
# Optional eslint cache | |||||
.eslintcache | |||||
# Microbundle cache | |||||
.rpt2_cache/ | |||||
.rts2_cache_cjs/ | |||||
.rts2_cache_es/ | |||||
.rts2_cache_umd/ | |||||
# Optional REPL history | |||||
.node_repl_history | |||||
# Output of 'npm pack' | |||||
*.tgz | |||||
# Yarn Integrity file | |||||
.yarn-integrity | |||||
# dotenv environment variables file | |||||
.env | |||||
.env.production | |||||
.env.development | |||||
# parcel-bundler cache (https://parceljs.org/) | |||||
.cache | |||||
# Next.js build output | |||||
.next | |||||
# Nuxt.js build / generate output | |||||
.nuxt | |||||
dist | |||||
# Gatsby files | |||||
.cache/ | |||||
# Comment in the public line in if your project uses Gatsby and *not* Next.js | |||||
# https://nextjs.org/blog/next-9-1#public-directory-support | |||||
# public | |||||
# vuepress build output | |||||
.vuepress/dist | |||||
# Serverless directories | |||||
.serverless/ | |||||
# FuseBox cache | |||||
.fusebox/ | |||||
# DynamoDB Local files | |||||
.dynamodb/ | |||||
# TernJS port file | |||||
.tern-port | |||||
.npmrc |
@@ -0,0 +1,64 @@ | |||||
{ | |||||
"name": "@modal-sh/core", | |||||
"version": "0.0.0", | |||||
"files": [ | |||||
"dist", | |||||
"src" | |||||
], | |||||
"engines": { | |||||
"node": ">=12" | |||||
}, | |||||
"license": "UNLICENSED", | |||||
"keywords": [ | |||||
"pridepack" | |||||
], | |||||
"devDependencies": { | |||||
"@types/node": "^20.12.7", | |||||
"pridepack": "2.6.0", | |||||
"tslib": "^2.6.2", | |||||
"typescript": "^5.4.5", | |||||
"vitest": "^1.5.0" | |||||
}, | |||||
"scripts": { | |||||
"prepublishOnly": "pridepack clean && pridepack build", | |||||
"build": "pridepack build", | |||||
"type-check": "pridepack check", | |||||
"lint": "pridepack lint", | |||||
"clean": "pridepack clean", | |||||
"watch": "pridepack watch", | |||||
"start": "pridepack start", | |||||
"dev": "pridepack dev", | |||||
"test": "vitest" | |||||
}, | |||||
"private": true, | |||||
"description": "Core library.", | |||||
"repository": { | |||||
"url": "", | |||||
"type": "git" | |||||
}, | |||||
"homepage": "", | |||||
"bugs": { | |||||
"url": "" | |||||
}, | |||||
"author": "TheoryOfNekomata <allan.crisostomo@outlook.com>", | |||||
"publishConfig": { | |||||
"access": "restricted" | |||||
}, | |||||
"types": "./dist/types/index.d.ts", | |||||
"main": "./dist/cjs/production/index.js", | |||||
"module": "./dist/esm/production/index.js", | |||||
"exports": { | |||||
".": { | |||||
"development": { | |||||
"require": "./dist/cjs/development/index.js", | |||||
"import": "./dist/esm/development/index.js" | |||||
}, | |||||
"require": "./dist/cjs/production/index.js", | |||||
"import": "./dist/esm/production/index.js", | |||||
"types": "./dist/types/index.d.ts" | |||||
} | |||||
}, | |||||
"typesVersions": { | |||||
"*": {} | |||||
} | |||||
} |
@@ -0,0 +1,3 @@ | |||||
{ | |||||
"target": "es2018" | |||||
} |
@@ -0,0 +1,34 @@ | |||||
export interface AddFunctionOptions { | |||||
a: number; | |||||
b: number; | |||||
} | |||||
export interface AddFunction { | |||||
(options: AddFunctionOptions): number; | |||||
} | |||||
export class InvalidArgumentTypeError extends TypeError { | |||||
constructor(message: string) { | |||||
super(message); | |||||
this.name = 'InvalidArgumentTypeError'; | |||||
} | |||||
} | |||||
export class ArgumentOutOfRangeError extends RangeError { | |||||
constructor(message: string) { | |||||
super(message); | |||||
this.name = 'ArgumentOutOfRangeError'; | |||||
} | |||||
} | |||||
export const add: AddFunction = (options: AddFunctionOptions): number => { | |||||
const { a, b } = options as unknown as Record<string, unknown>; | |||||
if (typeof a !== 'number' || typeof b !== 'number') { | |||||
throw new InvalidArgumentTypeError('a and b must be numbers'); | |||||
} | |||||
if (!Number.isFinite(a) || !Number.isFinite(b)) { | |||||
throw new ArgumentOutOfRangeError('a and b must be finite numbers'); | |||||
} | |||||
return a + b; | |||||
}; |
@@ -0,0 +1,16 @@ | |||||
import { describe, it, expect } from 'vitest'; | |||||
import { add } from '../src'; | |||||
describe('core', () => { | |||||
it('returns result', () => { | |||||
expect(add({ a: 1, b: 1 })).toEqual(2); | |||||
}); | |||||
it('throws when given invalid argument types', () => { | |||||
expect(() => add({ a: '1' as unknown as number, b: 1 })).toThrow(TypeError); | |||||
}); | |||||
it('throws when given out-of-range arguments', () => { | |||||
expect(() => add({ a: Infinity, b: 1 })).toThrow(RangeError); | |||||
}); | |||||
}); |
@@ -0,0 +1,21 @@ | |||||
{ | |||||
"exclude": ["node_modules"], | |||||
"include": ["src", "types"], | |||||
"compilerOptions": { | |||||
"module": "ESNext", | |||||
"lib": ["ESNext"], | |||||
"importHelpers": true, | |||||
"declaration": true, | |||||
"sourceMap": true, | |||||
"rootDir": "./src", | |||||
"strict": true, | |||||
"noUnusedLocals": true, | |||||
"noUnusedParameters": true, | |||||
"noImplicitReturns": true, | |||||
"noFallthroughCasesInSwitch": true, | |||||
"moduleResolution": "node", | |||||
"jsx": "react", | |||||
"esModuleInterop": true, | |||||
"target": "es2018" | |||||
} | |||||
} |
@@ -0,0 +1,107 @@ | |||||
# Logs | |||||
logs | |||||
*.log | |||||
npm-debug.log* | |||||
yarn-debug.log* | |||||
yarn-error.log* | |||||
lerna-debug.log* | |||||
# Diagnostic reports (https://nodejs.org/api/report.html) | |||||
report.[0-9]*.[0-9]*.[0-9]*.[0-9]*.json | |||||
# Runtime data | |||||
pids | |||||
*.pid | |||||
*.seed | |||||
*.pid.lock | |||||
# Directory for instrumented libs generated by jscoverage/JSCover | |||||
lib-cov | |||||
# Coverage directory used by tools like istanbul | |||||
coverage | |||||
*.lcov | |||||
# nyc test coverage | |||||
.nyc_output | |||||
# Grunt intermediate storage (https://gruntjs.com/creating-plugins#storing-task-files) | |||||
.grunt | |||||
# Bower dependency directory (https://bower.io/) | |||||
bower_components | |||||
# node-waf configuration | |||||
.lock-wscript | |||||
# Compiled binary addons (https://nodejs.org/api/addons.html) | |||||
build/Release | |||||
# Dependency directories | |||||
node_modules/ | |||||
jspm_packages/ | |||||
# TypeScript v1 declaration files | |||||
typings/ | |||||
# TypeScript cache | |||||
*.tsbuildinfo | |||||
# Optional npm cache directory | |||||
.npm | |||||
# Optional eslint cache | |||||
.eslintcache | |||||
# Microbundle cache | |||||
.rpt2_cache/ | |||||
.rts2_cache_cjs/ | |||||
.rts2_cache_es/ | |||||
.rts2_cache_umd/ | |||||
# Optional REPL history | |||||
.node_repl_history | |||||
# Output of 'npm pack' | |||||
*.tgz | |||||
# Yarn Integrity file | |||||
.yarn-integrity | |||||
# dotenv environment variables file | |||||
.env | |||||
.env.production | |||||
.env.development | |||||
# parcel-bundler cache (https://parceljs.org/) | |||||
.cache | |||||
# Next.js build output | |||||
.next | |||||
# Nuxt.js build / generate output | |||||
.nuxt | |||||
dist | |||||
# Gatsby files | |||||
.cache/ | |||||
# Comment in the public line in if your project uses Gatsby and *not* Next.js | |||||
# https://nextjs.org/blog/next-9-1#public-directory-support | |||||
# public | |||||
# vuepress build output | |||||
.vuepress/dist | |||||
# Serverless directories | |||||
.serverless/ | |||||
# FuseBox cache | |||||
.fusebox/ | |||||
# DynamoDB Local files | |||||
.dynamodb/ | |||||
# TernJS port file | |||||
.tern-port | |||||
.npmrc |
@@ -0,0 +1,68 @@ | |||||
{ | |||||
"name": "@modal-sh/web-api", | |||||
"version": "0.0.0", | |||||
"files": [ | |||||
"dist", | |||||
"src" | |||||
], | |||||
"engines": { | |||||
"node": ">=12" | |||||
}, | |||||
"keywords": [ | |||||
"pridepack" | |||||
], | |||||
"devDependencies": { | |||||
"@types/node": "^20.12.7", | |||||
"pridepack": "2.6.0", | |||||
"tslib": "^2.6.2", | |||||
"typescript": "^5.4.5", | |||||
"vite": "^5.2.10", | |||||
"vitest": "^1.5.0" | |||||
}, | |||||
"dependencies": { | |||||
"@modal-sh/core": "workspace:*", | |||||
"fastify": "^4.26.2" | |||||
}, | |||||
"scripts": { | |||||
"prepublishOnly": "pridepack clean && pridepack build", | |||||
"build": "pridepack build", | |||||
"type-check": "pridepack check", | |||||
"lint": "pridepack lint", | |||||
"clean": "pridepack clean", | |||||
"watch": "pridepack watch", | |||||
"start": "pridepack start", | |||||
"dev": "pridepack dev", | |||||
"test": "vitest" | |||||
}, | |||||
"private": true, | |||||
"description": "Web API.", | |||||
"repository": { | |||||
"url": "", | |||||
"type": "git" | |||||
}, | |||||
"homepage": "", | |||||
"bugs": { | |||||
"url": "" | |||||
}, | |||||
"author": "TheoryOfNekomata <allan.crisostomo@outlook.com>", | |||||
"publishConfig": { | |||||
"access": "restricted" | |||||
}, | |||||
"types": "./dist/types/index.d.ts", | |||||
"main": "./dist/cjs/production/index.js", | |||||
"module": "./dist/esm/production/index.js", | |||||
"exports": { | |||||
".": { | |||||
"development": { | |||||
"require": "./dist/cjs/development/index.js", | |||||
"import": "./dist/esm/development/index.js" | |||||
}, | |||||
"require": "./dist/cjs/production/index.js", | |||||
"import": "./dist/esm/production/index.js", | |||||
"types": "./dist/types/index.d.ts" | |||||
} | |||||
}, | |||||
"typesVersions": { | |||||
"*": {} | |||||
} | |||||
} |
@@ -0,0 +1,3 @@ | |||||
{ | |||||
"target": "es2018" | |||||
} |
@@ -0,0 +1,7 @@ | |||||
export namespace meta { | |||||
export const host = process.env.HOST ?? '0.0.0.0'; | |||||
export const port = Number(process.env.PORT ?? 8080); | |||||
export const env = process.env.NODE_ENV ?? 'development'; | |||||
} |
@@ -0,0 +1,20 @@ | |||||
import { createServer } from './server'; | |||||
import { addRoutes } from './routes'; | |||||
import * as config from './config'; | |||||
const server = createServer({ | |||||
logger: config.meta.env !== 'test', | |||||
}); | |||||
addRoutes(server); | |||||
server.listen( | |||||
{ port: config.meta.port, host: config.meta.host }, | |||||
(err, address) => { | |||||
if (err) { | |||||
server.log.error(err.message); | |||||
process.exit(1); | |||||
} | |||||
server.log.info(`server listening on ${address}`); | |||||
}, | |||||
); |
@@ -0,0 +1,60 @@ | |||||
import { RouteHandlerMethod } from 'fastify'; | |||||
import { | |||||
AdderService, | |||||
AdderServiceImpl, | |||||
ArgumentOutOfRangeError, | |||||
InvalidArgumentTypeError, | |||||
} from './adder.service'; | |||||
import { constants } from 'http2'; | |||||
export interface AdderController { | |||||
addNumbers: RouteHandlerMethod; | |||||
subtractNumbers: RouteHandlerMethod; | |||||
} | |||||
export class AdderControllerImpl implements AdderController { | |||||
constructor( | |||||
private readonly adderService: AdderService = new AdderServiceImpl(), | |||||
) { | |||||
// noop | |||||
} | |||||
readonly addNumbers: RouteHandlerMethod = async (request, reply) => { | |||||
const { a, b } = request.body as { a: number; b: number }; | |||||
try { | |||||
const response = this.adderService.addNumbers({ a, b }); | |||||
reply.send(response); | |||||
} catch (errorRaw) { | |||||
if (errorRaw instanceof InvalidArgumentTypeError) { | |||||
request.log.info(errorRaw); | |||||
reply.status(constants.HTTP_STATUS_BAD_REQUEST).send(errorRaw.message); | |||||
return; | |||||
} | |||||
if (errorRaw instanceof ArgumentOutOfRangeError) { | |||||
reply.status(constants.HTTP_STATUS_BAD_REQUEST).send(errorRaw.message); | |||||
return; | |||||
} | |||||
const error = errorRaw as Error; | |||||
reply.status(constants.HTTP_STATUS_INTERNAL_SERVER_ERROR).send(error.message); | |||||
} | |||||
} | |||||
readonly subtractNumbers: RouteHandlerMethod = async (request, reply) => { | |||||
const { a, b } = request.body as { a: number; b: number }; | |||||
try { | |||||
const response = this.adderService.addNumbers({ a, b: -b }); | |||||
reply.send(response); | |||||
} catch (errorRaw) { | |||||
if (errorRaw instanceof InvalidArgumentTypeError) { | |||||
reply.status(constants.HTTP_STATUS_BAD_REQUEST).send(errorRaw.message); | |||||
return; | |||||
} | |||||
if (errorRaw instanceof ArgumentOutOfRangeError) { | |||||
reply.status(constants.HTTP_STATUS_BAD_REQUEST).send(errorRaw.message); | |||||
return; | |||||
} | |||||
const error = errorRaw as Error; | |||||
reply.status(constants.HTTP_STATUS_INTERNAL_SERVER_ERROR).send(error.message); | |||||
} | |||||
} | |||||
} |
@@ -0,0 +1,21 @@ | |||||
import { | |||||
add, | |||||
AddFunctionOptions, | |||||
ArgumentOutOfRangeError, | |||||
InvalidArgumentTypeError, | |||||
} from '@modal-sh/core'; | |||||
export interface AdderService { | |||||
addNumbers(options: AddFunctionOptions): number; | |||||
} | |||||
export class AdderServiceImpl implements AdderService { | |||||
addNumbers(options: AddFunctionOptions) { | |||||
return add(options); | |||||
} | |||||
} | |||||
export { | |||||
ArgumentOutOfRangeError, | |||||
InvalidArgumentTypeError, | |||||
}; |
@@ -0,0 +1,2 @@ | |||||
export * from './adder.controller'; | |||||
export * from './adder.service'; |
@@ -0,0 +1,36 @@ | |||||
import { FastifyInstance } from 'fastify'; | |||||
import { AdderController, AdderControllerImpl } from './modules/adder'; | |||||
export const addRoutes = (server: FastifyInstance) => { | |||||
const adderController: AdderController = new AdderControllerImpl(); | |||||
return server | |||||
.route({ | |||||
method: 'POST', | |||||
url: '/add', | |||||
schema: { | |||||
body: { | |||||
type: 'object', | |||||
properties: { | |||||
a: { type: 'number' }, | |||||
b: { type: 'number' }, | |||||
} | |||||
} | |||||
}, | |||||
handler: adderController.addNumbers, | |||||
}) | |||||
.route({ | |||||
method: 'POST', | |||||
url: '/subtract', | |||||
schema: { | |||||
body: { | |||||
type: 'object', | |||||
properties: { | |||||
a: { type: 'number' }, | |||||
b: { type: 'number' }, | |||||
} | |||||
} | |||||
}, | |||||
handler: adderController.subtractNumbers, | |||||
}); | |||||
}; |
@@ -0,0 +1,11 @@ | |||||
import fastify, { FastifyServerOptions } from 'fastify'; | |||||
export interface CreateServerOptions extends FastifyServerOptions {} | |||||
export const createServer = (options = {} as CreateServerOptions) => { | |||||
const server = fastify(options); | |||||
// TODO set up server stuff here | |||||
return server; | |||||
}; |
@@ -0,0 +1,157 @@ | |||||
import { FastifyInstance } from 'fastify'; | |||||
import { | |||||
describe, | |||||
it, | |||||
expect, | |||||
beforeAll, | |||||
afterAll, | |||||
vi, | |||||
} from 'vitest'; | |||||
import { constants } from 'http2'; | |||||
import { createServer } from '../src/server'; | |||||
import { addRoutes } from '../src/routes'; | |||||
import { | |||||
AdderServiceImpl, | |||||
ArgumentOutOfRangeError, | |||||
InvalidArgumentTypeError, | |||||
} from '../src/modules/adder'; | |||||
describe('add', () => { | |||||
let server: FastifyInstance; | |||||
const body = { a: 1, b: 2 }; | |||||
beforeAll(() => { | |||||
server = createServer(); | |||||
addRoutes(server); | |||||
}); | |||||
afterAll(async () => { | |||||
await server.close(); | |||||
}); | |||||
it('returns result when successful', async () => { | |||||
const response = await server | |||||
.inject() | |||||
.post('/add') | |||||
.body(body) | |||||
.headers({ | |||||
'Accept': 'application/json', | |||||
}); | |||||
expect(response.statusCode).toBe(constants.HTTP_STATUS_OK); | |||||
}); | |||||
it('returns error when given invalid inputs', async () => { | |||||
vi.spyOn(AdderServiceImpl.prototype, 'addNumbers').mockImplementationOnce(() => { | |||||
throw new InvalidArgumentTypeError('Invalid input'); | |||||
}); | |||||
const response = await server | |||||
.inject() | |||||
.post('/add') | |||||
.body(body) | |||||
.headers({ | |||||
'Accept': 'application/json', | |||||
}); | |||||
expect(response.statusCode).toBe(constants.HTTP_STATUS_BAD_REQUEST); | |||||
}); | |||||
it('returns error when given out-of-range inputs', async () => { | |||||
vi.spyOn(AdderServiceImpl.prototype, 'addNumbers').mockImplementationOnce(() => { | |||||
throw new ArgumentOutOfRangeError('Out of range'); | |||||
}); | |||||
const response = await server | |||||
.inject() | |||||
.post('/add') | |||||
.body(body) | |||||
.headers({ | |||||
'Accept': 'application/json', | |||||
}); | |||||
expect(response.statusCode).toBe(constants.HTTP_STATUS_BAD_REQUEST); | |||||
}); | |||||
it('returns error when an unexpected error occurs', async () => { | |||||
vi.spyOn(AdderServiceImpl.prototype, 'addNumbers').mockImplementationOnce(() => { | |||||
throw new Error('Unexpected error'); | |||||
}); | |||||
const response = await server | |||||
.inject() | |||||
.post('/add') | |||||
.body({ a: 1, b: 2 }) | |||||
.headers({ | |||||
'Accept': 'application/json', | |||||
}); | |||||
expect(response.statusCode).toBe(constants.HTTP_STATUS_INTERNAL_SERVER_ERROR); | |||||
}); | |||||
}); | |||||
describe('subtract', () => { | |||||
let server: FastifyInstance; | |||||
const body = { a: 1, b: 2 }; | |||||
beforeAll(() => { | |||||
server = createServer(); | |||||
addRoutes(server); | |||||
}); | |||||
afterAll(async () => { | |||||
await server.close(); | |||||
}); | |||||
it('returns result when successful', async () => { | |||||
const response = await server | |||||
.inject() | |||||
.post('/subtract') | |||||
.body(body) | |||||
.headers({ | |||||
'Accept': 'application/json', | |||||
}); | |||||
expect(response.statusCode).toBe(constants.HTTP_STATUS_OK); | |||||
}); | |||||
it('returns error when given invalid inputs', async () => { | |||||
vi.spyOn(AdderServiceImpl.prototype, 'addNumbers').mockImplementationOnce(() => { | |||||
throw new InvalidArgumentTypeError('Invalid input'); | |||||
}); | |||||
const response = await server | |||||
.inject() | |||||
.post('/subtract') | |||||
.body(body) | |||||
.headers({ | |||||
'Accept': 'application/json', | |||||
}); | |||||
expect(response.statusCode).toBe(constants.HTTP_STATUS_BAD_REQUEST); | |||||
}); | |||||
it('returns error when given out-of-range inputs', async () => { | |||||
vi.spyOn(AdderServiceImpl.prototype, 'addNumbers').mockImplementationOnce(() => { | |||||
throw new ArgumentOutOfRangeError('Out of range'); | |||||
}); | |||||
const response = await server | |||||
.inject() | |||||
.post('/subtract') | |||||
.body(body) | |||||
.headers({ | |||||
'Accept': 'application/json', | |||||
}); | |||||
expect(response.statusCode).toBe(constants.HTTP_STATUS_BAD_REQUEST); | |||||
}); | |||||
it('returns error when an unexpected error occurs', async () => { | |||||
vi.spyOn(AdderServiceImpl.prototype, 'addNumbers').mockImplementationOnce(() => { | |||||
throw new Error('Unexpected error'); | |||||
}); | |||||
const response = await server | |||||
.inject() | |||||
.post('/subtract') | |||||
.body({ a: 1, b: 2 }) | |||||
.headers({ | |||||
'Accept': 'application/json', | |||||
}); | |||||
expect(response.statusCode).toBe(constants.HTTP_STATUS_INTERNAL_SERVER_ERROR); | |||||
}); | |||||
}); |
@@ -0,0 +1,21 @@ | |||||
{ | |||||
"exclude": ["node_modules"], | |||||
"include": ["src", "types"], | |||||
"compilerOptions": { | |||||
"module": "ESNext", | |||||
"lib": ["ESNext"], | |||||
"importHelpers": true, | |||||
"declaration": true, | |||||
"sourceMap": true, | |||||
"rootDir": "./src", | |||||
"strict": true, | |||||
"noUnusedLocals": true, | |||||
"noUnusedParameters": true, | |||||
"noImplicitReturns": true, | |||||
"noFallthroughCasesInSwitch": true, | |||||
"moduleResolution": "node", | |||||
"jsx": "react", | |||||
"esModuleInterop": true, | |||||
"target": "es2018" | |||||
} | |||||
} |
@@ -0,0 +1,2 @@ | |||||
packages: | |||||
- 'packages/*' |