Explorar el Código

Initial commit

master
TheoryOfNekomata hace 10 meses
commit
45f9853dd5
Se han modificado 43 ficheros con 5881 adiciones y 0 borrados
  1. +11
    -0
      .editorconfig
  2. +3
    -0
      .gitignore
  3. +6
    -0
      TODO.md
  4. +9
    -0
      packages/cli/.eslintrc
  5. +107
    -0
      packages/cli/.gitignore
  6. +73
    -0
      packages/cli/package.json
  7. +3
    -0
      packages/cli/pridepack.json
  8. +9
    -0
      packages/cli/src/cli.ts
  9. +22
    -0
      packages/cli/src/commands.ts
  10. +11
    -0
      packages/cli/src/index.ts
  11. +82
    -0
      packages/cli/src/modules/adder/adder.controller.ts
  12. +21
    -0
      packages/cli/src/modules/adder/adder.service.ts
  13. +2
    -0
      packages/cli/src/modules/adder/index.ts
  14. +335
    -0
      packages/cli/src/packages/cli-wrapper.ts
  15. +19
    -0
      packages/cli/src/packages/write-stream.ts
  16. +68
    -0
      packages/cli/test/index.test.ts
  17. +24
    -0
      packages/cli/tsconfig.eslint.json
  18. +24
    -0
      packages/cli/tsconfig.json
  19. +9
    -0
      packages/core/.eslintrc
  20. +107
    -0
      packages/core/.gitignore
  21. +66
    -0
      packages/core/package.json
  22. +3
    -0
      packages/core/pridepack.json
  23. +34
    -0
      packages/core/src/index.ts
  24. +16
    -0
      packages/core/test/index.test.ts
  25. +24
    -0
      packages/core/tsconfig.eslint.json
  26. +24
    -0
      packages/core/tsconfig.json
  27. +9
    -0
      packages/web-api/.eslintrc
  28. +107
    -0
      packages/web-api/.gitignore
  29. +71
    -0
      packages/web-api/package.json
  30. +3
    -0
      packages/web-api/pridepack.json
  31. +7
    -0
      packages/web-api/src/config.ts
  32. +20
    -0
      packages/web-api/src/index.ts
  33. +60
    -0
      packages/web-api/src/modules/adder/adder.controller.ts
  34. +21
    -0
      packages/web-api/src/modules/adder/adder.service.ts
  35. +2
    -0
      packages/web-api/src/modules/adder/index.ts
  36. +36
    -0
      packages/web-api/src/routes.ts
  37. +11
    -0
      packages/web-api/src/server.ts
  38. +87
    -0
      packages/web-api/test/index.test.ts
  39. +24
    -0
      packages/web-api/tsconfig.eslint.json
  40. +24
    -0
      packages/web-api/tsconfig.json
  41. +6
    -0
      packages/web-api/vite.config.ts
  42. +4279
    -0
      pnpm-lock.yaml
  43. +2
    -0
      pnpm-workspace.yaml

+ 11
- 0
.editorconfig Ver fichero

@@ -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

+ 3
- 0
.gitignore Ver fichero

@@ -0,0 +1,3 @@
node_modules
.DS_Store
.idea/

+ 6
- 0
TODO.md Ver fichero

@@ -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.

+ 9
- 0
packages/cli/.eslintrc Ver fichero

@@ -0,0 +1,9 @@
{
"root": true,
"extends": [
"lxsmnsyc/typescript"
],
"parserOptions": {
"project": "./tsconfig.eslint.json"
}
}

+ 107
- 0
packages/cli/.gitignore Ver fichero

@@ -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

+ 73
- 0
packages/cli/package.json Ver fichero

@@ -0,0 +1,73 @@
{
"name": "@modal-sh/cli",
"version": "0.0.0",
"files": [
"dist",
"src"
],
"engines": {
"node": ">=12"
},
"license": "UNLICENSED",
"keywords": [
"pridepack"
],
"devDependencies": {
"@types/node": "^18.14.1",
"@types/yargs": "^17.0.24",
"eslint": "^8.35.0",
"eslint-config-lxsmnsyc": "^0.5.0",
"pridepack": "2.4.4",
"tslib": "^2.5.0",
"typescript": "^4.9.5",
"vitest": "^0.28.1"
},
"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.6.3",
"@modal-sh/core": "workspace:*",
"pino": "^8.14.1",
"yargs": "^17.7.2"
}
}

+ 3
- 0
packages/cli/pridepack.json Ver fichero

@@ -0,0 +1,3 @@
{
"target": "es2018"
}

+ 9
- 0
packages/cli/src/cli.ts Ver fichero

@@ -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;
};

+ 22
- 0
packages/cli/src/commands.ts Ver fichero

@@ -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,
});
};

+ 11
- 0
packages/cli/src/index.ts Ver fichero

@@ -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));

+ 82
- 0
packages/cli/src/modules/adder/adder.controller.ts Ver fichero

@@ -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;
}
}

+ 21
- 0
packages/cli/src/modules/adder/adder.service.ts Ver fichero

@@ -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,
};

+ 2
- 0
packages/cli/src/modules/adder/index.ts Ver fichero

@@ -0,0 +1,2 @@
export * from './adder.controller';
export * from './adder.service';

+ 335
- 0
packages/cli/src/packages/cli-wrapper.ts Ver fichero

@@ -0,0 +1,335 @@
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',
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 {
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;
},
};
}
}

+ 19
- 0
packages/cli/src/packages/write-stream.ts Ver fichero

@@ -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;
}
}

+ 68
- 0
packages/cli/test/index.test.ts Ver fichero

@@ -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('blah', () => {
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);
});
});
});

+ 24
- 0
packages/cli/tsconfig.eslint.json Ver fichero

@@ -0,0 +1,24 @@
{
"exclude": ["node_modules"],
"include": ["src", "types", "test"],
"compilerOptions": {
"module": "ESNext",
"lib": ["ESNext"],
"importHelpers": true,
"declaration": true,
"sourceMap": true,
"rootDir": "./",
"strict": true,
"noUnusedLocals": true,
"noUnusedParameters": true,
"noImplicitReturns": true,
"noFallthroughCasesInSwitch": true,
"moduleResolution": "node",
"jsx": "react",
"esModuleInterop": true,
"target": "es2018",
"paths": {
"@/*": ["./src/*"],
}
}
}

+ 24
- 0
packages/cli/tsconfig.json Ver fichero

@@ -0,0 +1,24 @@
{
"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",
"paths": {
"@/*": ["./src/*"],
}
}
}

+ 9
- 0
packages/core/.eslintrc Ver fichero

@@ -0,0 +1,9 @@
{
"root": true,
"extends": [
"lxsmnsyc/typescript"
],
"parserOptions": {
"project": "./tsconfig.eslint.json"
}
}

+ 107
- 0
packages/core/.gitignore Ver fichero

@@ -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

+ 66
- 0
packages/core/package.json Ver fichero

@@ -0,0 +1,66 @@
{
"name": "@modal-sh/core",
"version": "0.0.0",
"files": [
"dist",
"src"
],
"engines": {
"node": ">=12"
},
"license": "UNLICENSED",
"keywords": [
"pridepack"
],
"devDependencies": {
"@types/node": "^18.14.1",
"eslint": "^8.35.0",
"eslint-config-lxsmnsyc": "^0.5.0",
"pridepack": "2.4.4",
"tslib": "^2.5.0",
"typescript": "^4.9.5",
"vitest": "^0.28.1"
},
"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": {
"*": {}
}
}

+ 3
- 0
packages/core/pridepack.json Ver fichero

@@ -0,0 +1,3 @@
{
"target": "es2018"
}

+ 34
- 0
packages/core/src/index.ts Ver fichero

@@ -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;
};

+ 16
- 0
packages/core/test/index.test.ts Ver fichero

@@ -0,0 +1,16 @@
import { describe, it, expect } from 'vitest';
import { add } from '../src';

describe('blah', () => {
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);
});
});

+ 24
- 0
packages/core/tsconfig.eslint.json Ver fichero

@@ -0,0 +1,24 @@
{
"exclude": ["node_modules"],
"include": ["src", "types", "test"],
"compilerOptions": {
"module": "ESNext",
"lib": ["ESNext"],
"importHelpers": true,
"declaration": true,
"sourceMap": true,
"rootDir": "./",
"strict": true,
"noUnusedLocals": true,
"noUnusedParameters": true,
"noImplicitReturns": true,
"noFallthroughCasesInSwitch": true,
"moduleResolution": "node",
"jsx": "react",
"esModuleInterop": true,
"target": "es2018",
"paths": {
"@/*": ["./src/*"],
}
}
}

+ 24
- 0
packages/core/tsconfig.json Ver fichero

@@ -0,0 +1,24 @@
{
"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",
"paths": {
"@/*": ["./src/*"],
}
}
}

+ 9
- 0
packages/web-api/.eslintrc Ver fichero

@@ -0,0 +1,9 @@
{
"root": true,
"extends": [
"lxsmnsyc/typescript"
],
"parserOptions": {
"project": "./tsconfig.eslint.json"
}
}

+ 107
- 0
packages/web-api/.gitignore Ver fichero

@@ -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

+ 71
- 0
packages/web-api/package.json Ver fichero

@@ -0,0 +1,71 @@
{
"name": "@modal-sh/web-api",
"version": "0.0.0",
"files": [
"dist",
"src"
],
"engines": {
"node": ">=12"
},
"keywords": [
"pridepack"
],
"devDependencies": {
"@types/node": "^18.14.1",
"eslint": "^8.35.0",
"eslint-config-lxsmnsyc": "^0.5.0",
"pridepack": "2.4.4",
"tslib": "^2.5.0",
"typescript": "^4.9.5",
"vite": "^4.3.9",
"vite-tsconfig-paths": "^4.2.0",
"vitest": "^0.28.1"
},
"dependencies": {
"@modal-sh/core": "workspace:*",
"fastify": "^4.12.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": "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": {
"*": {}
}
}

+ 3
- 0
packages/web-api/pridepack.json Ver fichero

@@ -0,0 +1,3 @@
{
"target": "es2018"
}

+ 7
- 0
packages/web-api/src/config.ts Ver fichero

@@ -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';
}

+ 20
- 0
packages/web-api/src/index.ts Ver fichero

@@ -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}`);
},
);

+ 60
- 0
packages/web-api/src/modules/adder/adder.controller.ts Ver fichero

@@ -0,0 +1,60 @@
import { RouteHandlerMethod } from 'fastify';
import {
AdderService,
AdderServiceImpl,
ArgumentOutOfRangeError,
InvalidArgumentTypeError,
} from '@/modules/adder/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);
}
}
}

+ 21
- 0
packages/web-api/src/modules/adder/adder.service.ts Ver fichero

@@ -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,
};

+ 2
- 0
packages/web-api/src/modules/adder/index.ts Ver fichero

@@ -0,0 +1,2 @@
export * from './adder.controller';
export * from './adder.service';

+ 36
- 0
packages/web-api/src/routes.ts Ver fichero

@@ -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,
});
};

+ 11
- 0
packages/web-api/src/server.ts Ver fichero

@@ -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;
};

+ 87
- 0
packages/web-api/test/index.test.ts Ver fichero

@@ -0,0 +1,87 @@
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('Example', () => {
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('/')
.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('/')
.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('/')
.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('/')
.body({ a: 1, b: 2 })
.headers({
'Accept': 'application/json',
});
expect(response.statusCode).toBe(constants.HTTP_STATUS_INTERNAL_SERVER_ERROR);
});
});

+ 24
- 0
packages/web-api/tsconfig.eslint.json Ver fichero

@@ -0,0 +1,24 @@
{
"exclude": ["node_modules"],
"include": ["src", "types", "test"],
"compilerOptions": {
"module": "ESNext",
"lib": ["DOM", "ESNext"],
"importHelpers": true,
"declaration": true,
"sourceMap": true,
"rootDir": "./",
"strict": true,
"noUnusedLocals": true,
"noUnusedParameters": true,
"noImplicitReturns": true,
"noFallthroughCasesInSwitch": true,
"moduleResolution": "node",
"jsx": "react",
"esModuleInterop": true,
"target": "es2018",
"paths": {
"@/*": ["./src/*"],
}
}
}

+ 24
- 0
packages/web-api/tsconfig.json Ver fichero

@@ -0,0 +1,24 @@
{
"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",
"paths": {
"@/*": ["./src/*"],
}
}
}

+ 6
- 0
packages/web-api/vite.config.ts Ver fichero

@@ -0,0 +1,6 @@
import { defineConfig } from 'vite';
import tsconfigPaths from 'vite-tsconfig-paths';

export default defineConfig({
plugins: [tsconfigPaths()],
});

+ 4279
- 0
pnpm-lock.yaml
La diferencia del archivo ha sido suprimido porque es demasiado grande
Ver fichero


+ 2
- 0
pnpm-workspace.yaml Ver fichero

@@ -0,0 +1,2 @@
packages:
- 'packages/*'

Cargando…
Cancelar
Guardar