From 6b162924fc2d2331860f6a4d0578c480e1fdec0d Mon Sep 17 00:00:00 2001 From: Alexey Immoreev Date: Tue, 8 Nov 2022 15:41:38 +0300 Subject: [PATCH] DEC-623: Switch from `joi` to `zod` --- backend/.env.nest | 1 - backend/.env.nest.local.example | 6 +- backend/README.md | 2 +- backend/modules/template/dto.ts | 7 - backend/package.json | 2 - backend/src/config/validate.ts | 338 +++----- backend/src/core/explorer/dto.ts | 42 - .../src/core/explorer/explorer.controller.ts | 17 +- backend/src/core/projects/dto.ts | 118 --- .../src/core/projects/projects.controller.ts | 45 +- backend/src/core/users/dto.ts | 86 --- backend/src/core/users/users.controller.ts | 36 +- backend/src/modules/abi/abi.controller.ts | 8 +- backend/src/modules/abi/dto.ts | 57 -- .../src/modules/alerts/alerts.controller.ts | 61 +- backend/src/modules/alerts/alerts.service.ts | 64 +- backend/src/modules/alerts/dto.spec.ts | 216 ------ backend/src/modules/alerts/dto.ts | 294 ------- .../triggered-alerts.controller.ts | 17 +- .../triggered-alerts.service.ts | 3 +- backend/src/modules/rpcstats/dto.ts | 30 - .../modules/rpcstats/rpcstats.controller.ts | 6 +- backend/src/pipes/JoiValidationPipe.ts | 26 - backend/src/pipes/ZodValidationPipe.ts | 20 + backend/terraform/modules/api/main.tf | 5 - common/package.json | 5 +- common/tsconfig.json | 3 + common/types/abi/abi.schema.ts | 119 ++- common/types/alerts/alerts.schema.ts | 729 ++++++++++++------ .../types/alerts/triggered-alerts.schema.ts | 92 ++- common/types/api.ts | 330 ++++---- common/types/core/explorer-errors.ts | 323 ++++---- common/types/core/explorer.schema.ts | 539 ++++++++----- common/types/core/projects.schema.ts | 245 +++--- common/types/core/types.ts | 153 ++++ common/types/core/users.schema.ts | 221 +++--- common/types/rpcstats/rpcstats.schema.ts | 127 ++- common/types/schemas.ts | 13 + common/types/utils.ts | 10 + package-lock.json | 110 +-- 40 files changed, 2112 insertions(+), 2414 deletions(-) delete mode 100644 backend/modules/template/dto.ts delete mode 100644 backend/src/core/explorer/dto.ts delete mode 100644 backend/src/core/projects/dto.ts delete mode 100644 backend/src/core/users/dto.ts delete mode 100644 backend/src/modules/abi/dto.ts delete mode 100644 backend/src/modules/alerts/dto.spec.ts delete mode 100644 backend/src/modules/alerts/dto.ts delete mode 100644 backend/src/modules/rpcstats/dto.ts delete mode 100644 backend/src/pipes/JoiValidationPipe.ts create mode 100644 backend/src/pipes/ZodValidationPipe.ts create mode 100644 common/tsconfig.json create mode 100644 common/types/core/types.ts create mode 100644 common/types/schemas.ts create mode 100644 common/types/utils.ts diff --git a/backend/.env.nest b/backend/.env.nest index eae18ff07..3e37fb71b 100644 --- a/backend/.env.nest +++ b/backend/.env.nest @@ -16,7 +16,6 @@ LOG_INDEXER=false EMAIL_TOKEN_EXPIRY_MIN=10 RESEND_VERIFICATION_RATE_LIMIT_MILLIS=2000 -TELEGRAM_ENABLE_WEBHOOK=false FRONTEND_BASE_URL=http://localhost:3000 FIREBASE_CLIENT_CONFIG={"apiKey":"AIzaSyAPEZMnzIy-JAi-Q-701bDXxygQCjqwxzs","authDomain":"developer-platform-local.firebaseapp.com","projectId":"developer-platform-local","storageBucket":"developer-platform-local.appspot.com","messagingSenderId":"78552755554","appId":"1:78552755554:web:e20f5431637aa1f0a8503b","measurementId":"G-MHKWBN39VF"} \ No newline at end of file diff --git a/backend/.env.nest.local.example b/backend/.env.nest.local.example index e7af05cee..c39719dca 100644 --- a/backend/.env.nest.local.example +++ b/backend/.env.nest.local.example @@ -4,9 +4,9 @@ FIREBASE_CLIENT_CONFIG= PROJECT_REF_PREFIX= MOCK_KEY_SERVICE= -TELEGRAM_ENABLE_WEBHOOK= -TELEGRAM_BOT_TOKEN= -TELEGRAM_SECRET= +# both keys below are required if you want to enable telegram hook +TELEGRAM_BOT_TOKEN= +TELEGRAM_SECRET= MOCK_EMAIL_SERVICE= MAILGUN_DOMAIN= diff --git a/backend/README.md b/backend/README.md index 0b15f57a3..cdbc67391 100644 --- a/backend/README.md +++ b/backend/README.md @@ -104,7 +104,7 @@ This is an RPC-style API. All endpoints are POSTs and all bodies are JSON. This ## Input Validation -All endpoints which accept input (JSON bodies) should validate that input with [Joi](https://joi.dev/). The best way to learn how to do this is to inspect an existing endpoint. +All endpoints which accept input (JSON bodies) should validate that input with [zod](https://github.com/colinhacks/zod). The best way to learn how to do this is to inspect an existing endpoint. ## Authentication diff --git a/backend/modules/template/dto.ts b/backend/modules/template/dto.ts deleted file mode 100644 index 9273e8987..000000000 --- a/backend/modules/template/dto.ts +++ /dev/null @@ -1,7 +0,0 @@ -// Note: we use Joi instead of Nest's default recommendation of class-validator -// because class-validator was experiencing issues at the time of implementation -// and had many unaddressed github issues - -// import * as Joi from 'joi'; - -// TODO add some DTO schemas and interfaces diff --git a/backend/package.json b/backend/package.json index 051e97fcb..00f2599cf 100644 --- a/backend/package.json +++ b/backend/package.json @@ -31,7 +31,6 @@ "express": "^4.18.1", "firebase-admin": "^10.0.0", "form-data": "^4.0.0", - "joi": "^17.4.2", "kysely": "^0.20.0", "luxon": "^3.0.1", "mailgun.js": "^7.0.2", @@ -55,7 +54,6 @@ "@nestjs/testing": "^8.0.0", "@types/express": "^4.17.13", "@types/jest": "^27.0.1", - "@types/joi": "^17.2.3", "@types/luxon": "^3.0.0", "@types/node": "^16.0.0", "@types/pg": "^8.6.5", diff --git a/backend/src/config/validate.ts b/backend/src/config/validate.ts index e4e833d5f..a02b65c2d 100644 --- a/backend/src/config/validate.ts +++ b/backend/src/config/validate.ts @@ -1,5 +1,4 @@ -import { Net } from '@pc/database/clients/core'; -import * as Joi from 'joi'; +import { z } from 'zod'; // * Adding an environment variable? There are three places in this // * file you must add it: the AppConfig interface, the appConfigSchema @@ -17,10 +16,6 @@ import * as Joi from 'joi'; // * getter example: // * this.config.get('analytics.url', {infer: true}); -// when using Joi, we must maintain a TypeScript interface alongside -// it and ensure they stay in sync. Joi will perform runtime validation -// and casting but cannot provide compile-time types - export type Database = { host: string; database: string; @@ -28,222 +23,124 @@ export type Database = { password: string; }; -type DeployEnv = 'LOCAL' | 'DEVELOPMENT' | 'STAGING' | 'PRODUCTION'; -export interface AppConfig { - deployEnv: DeployEnv; - port: number; - db: { - connectionString: string; - }; - nearRpc: Record; - nearArchivalRpc: Record; - recentTransactionsCount: number; - projectRefPrefix: string; - indexerDatabase: Record; - indexerActivityDatabase: Partial>; - firebase: { - credentials: string; - clientConfig: string; - }; - log: { - queries: boolean; - indexer: boolean; - }; - orgs: { - inviteTokenExpiryMinutes: number; - }; - alerts: { - email: { - tokenExpiryMin: number; - resendVerificationRatelimitMillis: number; - }; - telegram?: { - tokenExpiryMin: number; - botToken?: string; - secret?: string; - }; - }; - mailgun: { - domain: string; - apiKey: string; - }; - email: { - mock: boolean; - noReply: string; - alerts: { - noReply: string; - }; - }; - frontend: { - baseUrl: string; - }; - featureEnabled: { - core: { - contractAddressValidation: boolean; - }; - alerts: { - contractAddressValidation: boolean; - }; - }; - metricsPort: string; - rpcProvisioningService: { - mock: boolean; - apiKey: string; - url: string; - }; -} +const databaseSchema = z.strictObject({ + host: z.string(), + database: z.string(), + user: z.string(), + password: z.string(), +}); -const databaseSchema = Joi.object({ - host: Joi.string(), - database: Joi.string(), - user: Joi.string(), - password: Joi.string(), +const rpcSchema = z.strictObject({ + url: z.string().url(), }); -// Joi docs: https://joi.dev/api -// * When validation is performed, a default option is changed to -// * require all properties to exist. If you are adding an optional -// * environment variable, you must add `.optional()` to its Joi -// * definition -const appConfigSchema = Joi.object({ - deployEnv: Joi.string().valid( - 'LOCAL', - 'DEVELOPMENT', - 'STAGING', - 'PRODUCTION', - ), - port: Joi.number().integer(), - db: { - connectionString: Joi.string(), - }, - nearRpc: { - TESTNET: { - url: Joi.string() - .uri({ scheme: 'https' }) - .optional() - .default('https://rpc.testnet.near.org'), - }, - MAINNET: Joi.object({ - url: Joi.string() - .uri({ scheme: 'https' }) - .optional() - .default('https://rpc.mainnet.near.org'), - }), - }, - nearArchivalRpc: { - TESTNET: { - url: Joi.string() - .uri({ scheme: 'https' }) - .optional() - .default('https://archival-rpc.testnet.near.org'), - }, - MAINNET: Joi.object({ - url: Joi.string() - .uri({ scheme: 'https' }) - .optional() - .default('https://archival-rpc.mainnet.near.org'), - }), - }, - indexerDatabase: Joi.object({ +const appConfigSchema = z.strictObject({ + deployEnv: z.enum(['LOCAL', 'DEVELOPMENT', 'STAGING', 'PRODUCTION']), + port: z.number().int(), + db: z.strictObject({ + connectionString: z.string(), + }), + nearRpc: z.strictObject({ + MAINNET: rpcSchema, + TESTNET: rpcSchema, + }), + nearArchivalRpc: z.strictObject({ + MAINNET: rpcSchema, + TESTNET: rpcSchema, + }), + indexerDatabase: z.strictObject({ MAINNET: databaseSchema, TESTNET: databaseSchema, }), - indexerActivityDatabase: Joi.object({ + indexerActivityDatabase: z.strictObject({ MAINNET: databaseSchema, TESTNET: databaseSchema.optional(), }), - recentTransactionsCount: Joi.number().integer(), - projectRefPrefix: Joi.string().optional().default(''), - firebase: { - credentials: Joi.string(), - clientConfig: Joi.string(), - }, - log: { - queries: Joi.boolean().optional().default(false), - indexer: Joi.boolean().optional().default(false), - }, - orgs: { - inviteTokenExpiryMinutes: Joi.number().optional().default(10000), // TODO set to a small value once requesting a new token is possible - }, - alerts: { - email: Joi.object({ - tokenExpiryMin: Joi.number().optional().default(10000), // TODO set to a small value once requesting a new token is possible - resendVerificationRatelimitMillis: Joi.number().optional().default(2000), + recentTransactionsCount: z.number().int(), + projectRefPrefix: z.string(), + firebase: z.strictObject({ + credentials: z.string(), + clientConfig: z.string(), + }), + log: z.strictObject({ + queries: z.boolean(), + indexer: z.boolean(), + }), + orgs: z.strictObject({ + inviteTokenExpiryMinutes: z.number(), + }), + alerts: z.strictObject({ + email: z.strictObject({ + tokenExpiryMin: z.number(), + resendVerificationRatelimitMillis: z.number(), }), - telegram: Joi.object({ - tokenExpiryMin: Joi.number().optional().default(10000), // TODO set to a small value once requesting a new token is possible - botToken: Joi.string().when('/alerts.telegram.enableWebhook', { - is: Joi.boolean().valid(false), - then: Joi.optional().allow(''), - }), - secret: Joi.string().when('/alerts.telegram.enableWebhook', { - is: Joi.boolean().valid(false), - then: Joi.optional().allow(''), - }), - }).optional(), - }, - mailgun: { - domain: Joi.string(), - apiKey: Joi.string(), - }, - email: { - mock: Joi.boolean().optional().default(false), - noReply: Joi.string(), - alerts: { - noReply: Joi.string(), - }, - }, - frontend: { - baseUrl: Joi.string(), - }, - featureEnabled: { - core: { - contractAddressValidation: Joi.boolean().optional().default(true), - }, - alerts: { - contractAddressValidation: Joi.boolean().optional().default(true), - }, - }, - metricsPort: Joi.number().optional().default(3030), - rpcProvisioningService: { - mock: Joi.boolean().optional().default(false), - url: Joi.string() - .uri({ scheme: 'http' }) - .when('/dev.mock.rpcProvisioningService', { - // the slash accesses off the schema root - is: Joi.boolean().valid(true), - then: Joi.optional().allow(''), - }), - apiKey: Joi.string().when('/dev.mock.rpcProvisioningService', { - is: Joi.boolean().valid(true), - then: Joi.optional().allow(''), + telegram: z + .strictObject({ + tokenExpiryMin: z.number(), + botToken: z.string(), + secret: z.string(), + }) + .optional(), + }), + mailgun: z.strictObject({ + domain: z.string(), + apiKey: z.string(), + }), + email: z.strictObject({ + noReply: z.string(), + alerts: z.strictObject({ + noReply: z.string(), }), - }, + mock: z.boolean(), + }), + frontend: z.strictObject({ + baseUrl: z.string(), + }), + featureEnabled: z.strictObject({ + core: z.strictObject({ + contractAddressValidation: z.boolean().optional().default(true), + }), + alerts: z.strictObject({ + contractAddressValidation: z.boolean().optional().default(true), + }), + }), + metricsPort: z.number().int().optional().default(3030), + rpcProvisioningService: z.strictObject({ + mock: z.boolean(), + url: z.string().url(), + apiKey: z.string(), + }), }); -export default function validate(config: Record): AppConfig { +export type AppConfig = z.infer; + +export default function validate(config: Record): AppConfig { // read environment variables into structured object for // more organized access - const structuredConfig = { - deployEnv: config.DEPLOY_ENV as DeployEnv, + const structuredConfig: AppConfig = { + deployEnv: config.DEPLOY_ENV, port: config.PORT, db: { connectionString: config.DATABASE_URL, }, nearRpc: { TESTNET: { - url: config.NEAR_RPC_URL_TEST, + url: config.NEAR_RPC_URL_TEST || 'https://rpc.testnet.near.org', }, MAINNET: { - url: config.NEAR_RPC_URL_MAIN, + url: config.NEAR_RPC_URL_MAIN || 'https://rpc.mainnet.near.org', }, }, nearArchivalRpc: { TESTNET: { - url: config.NEAR_ARCHIVAL_RPC_URL_TEST, + url: + config.NEAR_ARCHIVAL_RPC_URL_TEST || + 'https://archival-rpc.testnet.near.org', }, MAINNET: { - url: config.NEAR_ARCHIVAL_RPC_URL_MAIN, + url: + config.NEAR_ARCHIVAL_RPC_URL_MAIN || + 'https://archival-rpc.mainnet.near.org', }, }, indexerDatabase: { @@ -288,38 +185,43 @@ export default function validate(config: Record): AppConfig { : undefined, }, recentTransactionsCount: config.RECENT_TRANSACTIONS_COUNT, - projectRefPrefix: config.PROJECT_REF_PREFIX, + projectRefPrefix: config.PROJECT_REF_PREFIX || '', firebase: { credentials: config.FIREBASE_CREDENTIALS, clientConfig: config.FIREBASE_CLIENT_CONFIG, }, log: { - queries: config.LOG_QUERIES, - indexer: config.LOG_INDEXER, + queries: config.LOG_QUERIES || false, + indexer: config.LOG_INDEXER || false, }, orgs: { - inviteTokenExpiryMinutes: config.ORGS_INVITE_TOKEN_EXPIRY_MINUTES, + // TODO set to a small value once requesting a new token is possible + inviteTokenExpiryMinutes: + config.ORGS_INVITE_TOKEN_EXPIRY_MINUTES || 10000, }, alerts: { email: { - tokenExpiryMin: config.EMAIL_TOKEN_EXPIRY_MIN, + // TODO set to a small value once requesting a new token is possible + tokenExpiryMin: config.EMAIL_TOKEN_EXPIRY_MIN || 10000, resendVerificationRatelimitMillis: - config.RESEND_VERIFICATION_RATE_LIMIT_MILLIS, + config.RESEND_VERIFICATION_RATE_LIMIT_MILLIS || 2000, }, - telegram: config.TELEGRAM_ENABLE_WEBHOOK - ? { - tokenExpiryMin: config.TELEGRAM_TOKEN_EXPIRY_MIN, - botToken: config.TELEGRAM_BOT_TOKEN, - secret: config.TELEGRAM_SECRET, - } - : undefined, + telegram: + config.TELEGRAM_BOT_TOKEN && config.TELEGRAM_SECRET + ? { + // TODO set to a small value once requesting a new token is possible + tokenExpiryMin: config.TELEGRAM_TOKEN_EXPIRY_MIN || 10000, + botToken: config.TELEGRAM_BOT_TOKEN, + secret: config.TELEGRAM_SECRET, + } + : undefined, }, mailgun: { domain: config.MAILGUN_DOMAIN, apiKey: config.MAILGUN_API_KEY, }, email: { - mock: config.MOCK_EMAIL_SERVICE, + mock: Boolean(config.MOCK_EMAIL_SERVICE), noReply: config.EMAIL_NO_REPLY, alerts: { noReply: config.EMAIL_ALERTS_NO_REPLY, @@ -346,23 +248,9 @@ export default function validate(config: Record): AppConfig { }, }; - // Joi.attempt will return the validated object with values - // cast to their proper types or throw an error if validation - // fails - try { - const validatedConfig: AppConfig = Joi.attempt( - structuredConfig, - appConfigSchema, - { - presence: 'required', - }, - ); - return validatedConfig; - } catch (e: any) { - if (e.details) { - // very simplistic error formatic since we are replacing Joi soon anyways - throw new Error(JSON.stringify(e.details)); - } - throw e; + const parseResult = appConfigSchema.safeParse(structuredConfig); + if (!parseResult.success) { + throw new Error(parseResult.error.message); } + return parseResult.data; } diff --git a/backend/src/core/explorer/dto.ts b/backend/src/core/explorer/dto.ts deleted file mode 100644 index ca3a4174d..000000000 --- a/backend/src/core/explorer/dto.ts +++ /dev/null @@ -1,42 +0,0 @@ -// Note: we use Joi instead of Nest's default recommendation of class-validator -// because class-validator was experiencing issues at the time of implementation -// and had many unaddressed github issues - -import * as Joi from 'joi'; -import { Api } from '@pc/common/types/api'; - -const netSchema = Joi.alternatives('MAINNET', 'TESTNET'); - -// activity -export const ActivityInputSchemas = Joi.object< - Api.Query.Input<'/explorer/activity'>, - true ->({ - net: netSchema, - contractId: Joi.string(), -}); -// transaction -export const TransactionInputSchemas = Joi.object< - Api.Query.Input<'/explorer/transaction'>, - true ->({ - net: netSchema, - hash: Joi.string(), -}); -// balance changes -export const BalanceChangesInputSchemas = Joi.object< - Api.Query.Input<'/explorer/balanceChanges'>, - true ->({ - net: netSchema, - receiptId: Joi.string(), - accountIds: Joi.array().items(Joi.string()), -}); - -export const GetTransactionsSchema = Joi.object< - Api.Query.Input<'/explorer/getTransactions'>, - true ->({ - contracts: Joi.array().items(Joi.string()), - net: netSchema, -}); diff --git a/backend/src/core/explorer/explorer.controller.ts b/backend/src/core/explorer/explorer.controller.ts index 892e9a6ba..a092c46be 100644 --- a/backend/src/core/explorer/explorer.controller.ts +++ b/backend/src/core/explorer/explorer.controller.ts @@ -7,13 +7,8 @@ import { BadRequestException, } from '@nestjs/common'; import { ExplorerService } from './explorer.service'; -import { JoiValidationPipe } from 'src/pipes/JoiValidationPipe'; -import { - ActivityInputSchemas, - TransactionInputSchemas, - BalanceChangesInputSchemas, - GetTransactionsSchema, -} from './dto'; +import { ZodValidationPipe } from 'src/pipes/ZodValidationPipe'; +import { Explorer } from '@pc/common/types/core'; import { IndexerService } from './indexer.service'; @Controller('explorer') @@ -24,7 +19,7 @@ export class ExplorerController { ) {} @Post('activity') - @UsePipes(new JoiValidationPipe(ActivityInputSchemas)) + @UsePipes(new ZodValidationPipe(Explorer.query.inputs.activity)) async activity( @Body() { net, contractId }: Api.Query.Input<'/explorer/activity'>, ): Promise> { @@ -32,7 +27,7 @@ export class ExplorerController { } @Post('transaction') - @UsePipes(new JoiValidationPipe(TransactionInputSchemas)) + @UsePipes(new ZodValidationPipe(Explorer.query.inputs.transaction)) async transaction( @Body() { net, hash }: Api.Query.Input<'/explorer/transaction'>, ): Promise> { @@ -44,7 +39,7 @@ export class ExplorerController { } @Post('balanceChanges') - @UsePipes(new JoiValidationPipe(BalanceChangesInputSchemas)) + @UsePipes(new ZodValidationPipe(Explorer.query.inputs.balanceChanges)) async balanceChanges( @Body() { net, receiptId, accountIds }: Api.Query.Input<'/explorer/balanceChanges'>, @@ -53,7 +48,7 @@ export class ExplorerController { } @Post('getTransactions') - @UsePipes(new JoiValidationPipe(GetTransactionsSchema)) + @UsePipes(new ZodValidationPipe(Explorer.query.inputs.getTransactions)) async getTransactions( @Body() { contracts, net }: Api.Query.Input<'/explorer/getTransactions'>, ): Promise> { diff --git a/backend/src/core/projects/dto.ts b/backend/src/core/projects/dto.ts deleted file mode 100644 index 4cbc19bd2..000000000 --- a/backend/src/core/projects/dto.ts +++ /dev/null @@ -1,118 +0,0 @@ -// Note: we use Joi instead of Nest's default recommendation of class-validator -// because class-validator was experiencing issues at the time of implementation -// and had many unaddressed github issues - -import { Api } from '@pc/common/types/api'; -import * as Joi from 'joi'; - -const projectNameSchema = Joi.string().required().max(50); - -// create project -export const CreateProjectSchema = Joi.object< - Api.Mutation.Input<'/projects/create'>, - true ->({ - org: Joi.string(), - name: projectNameSchema, - tutorial: Joi.alternatives('NFT_MARKET', 'CROSSWORD'), -}); - -// eject tutorial project -export const EjectTutorialProjectSchema = Joi.object< - Api.Mutation.Input<'/projects/ejectTutorial'>, - true ->({ - slug: Joi.string().required(), -}); - -// delete project -export const DeleteProjectSchema = Joi.object< - Api.Mutation.Input<'/projects/delete'>, - true ->({ - slug: Joi.string().required(), -}); - -// get project details -export const GetProjectDetailsSchema = Joi.object< - Api.Query.Input<'/projects/getDetails'>, - true ->({ - slug: Joi.string().required(), -}); - -// add contract -export const AddContractSchema = Joi.object< - Api.Mutation.Input<'/projects/addContract'>, - true ->({ - project: Joi.string().required(), - environment: Joi.number().integer().required(), - address: Joi.string().required(), -}); - -// remove contract -export const RemoveContractSchema = Joi.object< - Api.Mutation.Input<'/projects/removeContract'>, - true ->({ - slug: Joi.string().required(), -}); - -// get contracts -export const GetContractsSchema = Joi.object< - Api.Query.Input<'/projects/getContracts'>, - true ->({ - project: Joi.string().required(), - environment: Joi.number().integer().required(), -}); - -// get contract -export const GetContractSchema = Joi.object< - Api.Query.Input<'/projects/getContract'>, - true ->({ - slug: Joi.string().required(), -}); - -// get environments -export const GetEnvironmentsSchema = Joi.object< - Api.Query.Input<'/projects/getEnvironments'>, - true ->({ - project: Joi.string().required(), -}); - -// get keys -export const GetKeysSchema = Joi.object< - Api.Query.Input<'/projects/getKeys'>, - true ->({ - project: Joi.string().required(), -}); - -// rotate key -export const RotateKeySchema = Joi.object< - Api.Mutation.Input<'/projects/rotateKey'>, - true ->({ - slug: Joi.string().required(), -}); - -// generate key -export const GenerateKeySchema = Joi.object< - Api.Mutation.Input<'/projects/generateKey'>, - true ->({ - project: Joi.string().required(), - description: Joi.string().required(), -}); - -// delete key -export const DeleteKeySchema = Joi.object< - Api.Mutation.Input<'/projects/deleteKey'>, - true ->({ - slug: Joi.string().required(), -}); diff --git a/backend/src/core/projects/projects.controller.ts b/backend/src/core/projects/projects.controller.ts index 54297fda3..a06c28024 100644 --- a/backend/src/core/projects/projects.controller.ts +++ b/backend/src/core/projects/projects.controller.ts @@ -13,22 +13,8 @@ import { import { BearerAuthGuard } from '../auth/bearer-auth.guard'; import { ProjectsService } from './projects.service'; import { VError } from 'verror'; -import { - AddContractSchema, - CreateProjectSchema, - DeleteKeySchema, - DeleteProjectSchema, - EjectTutorialProjectSchema, - GenerateKeySchema, - GetContractSchema, - GetContractsSchema, - GetEnvironmentsSchema, - GetKeysSchema, - GetProjectDetailsSchema, - RemoveContractSchema, - RotateKeySchema, -} from './dto'; -import { JoiValidationPipe } from 'src/pipes/JoiValidationPipe'; +import { ZodValidationPipe } from 'src/pipes/ZodValidationPipe'; +import { Projects } from '@pc/common/types/core'; import { Api } from '@pc/common/types/api'; @Controller('projects') @@ -37,7 +23,7 @@ export class ProjectsController { @Post('create') @UseGuards(BearerAuthGuard) - @UsePipes(new JoiValidationPipe(CreateProjectSchema)) + @UsePipes(new ZodValidationPipe(Projects.mutation.inputs.create)) async create( @Request() req, @Body() { org, name, tutorial }: Api.Mutation.Input<'/projects/create'>, @@ -57,7 +43,7 @@ export class ProjectsController { @Post('ejectTutorial') @HttpCode(204) @UseGuards(BearerAuthGuard) - @UsePipes(new JoiValidationPipe(EjectTutorialProjectSchema)) + @UsePipes(new ZodValidationPipe(Projects.mutation.inputs.ejectTutorial)) async ejectTutorial( @Request() req, @Body() { slug }: Api.Mutation.Input<'/projects/ejectTutorial'>, @@ -72,7 +58,7 @@ export class ProjectsController { @Post('delete') @HttpCode(204) @UseGuards(BearerAuthGuard) - @UsePipes(new JoiValidationPipe(DeleteProjectSchema)) + @UsePipes(new ZodValidationPipe(Projects.mutation.inputs.delete)) async delete( @Request() req, @Body() { slug }: Api.Mutation.Input<'/projects/delete'>, @@ -86,7 +72,7 @@ export class ProjectsController { @Post('getDetails') @UseGuards(BearerAuthGuard) - @UsePipes(new JoiValidationPipe(GetProjectDetailsSchema)) + @UsePipes(new ZodValidationPipe(Projects.query.inputs.getDetails)) async getDetails( @Request() req, @Body() { slug }: Api.Query.Input<'/projects/getDetails'>, @@ -100,7 +86,7 @@ export class ProjectsController { @Post('addContract') @UseGuards(BearerAuthGuard) - @UsePipes(new JoiValidationPipe(AddContractSchema)) + @UsePipes(new ZodValidationPipe(Projects.mutation.inputs.addContract)) async addContract( @Request() req, @Body() @@ -125,7 +111,7 @@ export class ProjectsController { @Post('removeContract') @HttpCode(204) @UseGuards(BearerAuthGuard) - @UsePipes(new JoiValidationPipe(RemoveContractSchema)) + @UsePipes(new ZodValidationPipe(Projects.mutation.inputs.removeContract)) async removeContract( @Request() req, @Body() { slug }: Api.Mutation.Input<'/projects/removeContract'>, @@ -141,7 +127,7 @@ export class ProjectsController { @Post('getContracts') @UseGuards(BearerAuthGuard) - @UsePipes(new JoiValidationPipe(GetContractsSchema)) + @UsePipes(new ZodValidationPipe(Projects.query.inputs.getContracts)) async getContracts( @Request() req, @Body() { project, environment }: Api.Query.Input<'/projects/getContracts'>, @@ -159,7 +145,7 @@ export class ProjectsController { @Post('getContract') @UseGuards(BearerAuthGuard) - @UsePipes(new JoiValidationPipe(GetContractSchema)) + @UsePipes(new ZodValidationPipe(Projects.query.inputs.getContract)) async getContract( @Request() req, @Body() { slug }: Api.Query.Input<'/projects/getContract'>, @@ -173,6 +159,7 @@ export class ProjectsController { @Post('list') @UseGuards(BearerAuthGuard) + @UsePipes(new ZodValidationPipe(Projects.query.inputs.list)) async list( @Request() req, @Body() _: Api.Query.Input<'/projects/list'>, @@ -182,7 +169,7 @@ export class ProjectsController { @Post('getEnvironments') @UseGuards(BearerAuthGuard) - @UsePipes(new JoiValidationPipe(GetEnvironmentsSchema)) + @UsePipes(new ZodValidationPipe(Projects.query.inputs.getEnvironments)) async getEnvironments( @Request() req, @Body() { project }: Api.Query.Input<'/projects/getEnvironments'>, @@ -198,7 +185,7 @@ export class ProjectsController { @Post('getKeys') @UseGuards(BearerAuthGuard) - @UsePipes(new JoiValidationPipe(GetKeysSchema)) + @UsePipes(new ZodValidationPipe(Projects.query.inputs.getKeys)) async getKeys( @Request() req, @Body() { project }: Api.Query.Input<'/projects/getKeys'>, @@ -212,7 +199,7 @@ export class ProjectsController { @Post('rotateKey') @UseGuards(BearerAuthGuard) - @UsePipes(new JoiValidationPipe(RotateKeySchema)) + @UsePipes(new ZodValidationPipe(Projects.mutation.inputs.rotateKey)) async rotateKey( @Request() req, @Body() { slug }: Api.Mutation.Input<'/projects/rotateKey'>, @@ -226,7 +213,7 @@ export class ProjectsController { @Post('generateKey') @UseGuards(BearerAuthGuard) - @UsePipes(new JoiValidationPipe(GenerateKeySchema)) + @UsePipes(new ZodValidationPipe(Projects.mutation.inputs.generateKey)) async generateKey( @Request() req, @Body() @@ -246,7 +233,7 @@ export class ProjectsController { @Post('deleteKey') @HttpCode(204) @UseGuards(BearerAuthGuard) - @UsePipes(new JoiValidationPipe(DeleteKeySchema)) + @UsePipes(new ZodValidationPipe(Projects.mutation.inputs.deleteKey)) async deleteKey( @Request() req, @Body() { slug }: Api.Mutation.Input<'/projects/deleteKey'>, diff --git a/backend/src/core/users/dto.ts b/backend/src/core/users/dto.ts deleted file mode 100644 index ddd9594c3..000000000 --- a/backend/src/core/users/dto.ts +++ /dev/null @@ -1,86 +0,0 @@ -// Note: we use Joi instead of Nest's default recommendation of class-validator -// because class-validator was experiencing issues at the time of implementation -// and had many unaddressed github issues - -import { Api } from '@pc/common/types/api'; -import * as Joi from 'joi'; - -const OrgRoleSchema = Joi.alternatives('ADMIN', 'COLLABORATOR'); - -// create org -export const CreateOrgSchema = Joi.object< - Api.Mutation.Input<'/users/createOrg'>, - true ->({ - name: Joi.string(), -}); - -// invite to org -export const InviteToOrgSchema = Joi.object< - Api.Mutation.Input<'/users/inviteToOrg'>, - true ->({ - org: Joi.string(), - email: Joi.string().email(), - role: OrgRoleSchema, -}); - -// accept org invite -export const AcceptOrgInviteSchema = Joi.object< - Api.Mutation.Input<'/users/acceptOrgInvite'>, - true ->({ - token: Joi.string(), -}); - -// remove org invite -export const RemoveOrgInviteSchema = Joi.object< - Api.Mutation.Input<'/users/removeOrgInvite'>, - true ->({ - org: Joi.string(), - email: Joi.string().email(), -}); - -// remove from org -export const RemoveFromOrgSchema = Joi.object< - Api.Mutation.Input<'/users/removeFromOrg'>, - true ->({ - org: Joi.string(), - user: Joi.string(), -}); - -// list org members -export const ListOrgMembersSchema = Joi.object< - Api.Query.Input<'/users/listOrgMembers'>, - true ->({ - org: Joi.string(), -}); - -// delete org -export const DeleteOrgSchema = Joi.object< - Api.Mutation.Input<'/users/deleteOrg'>, - true ->({ - org: Joi.string(), -}); - -// change org role -export const ChangeOrgRoleSchema = Joi.object< - Api.Mutation.Input<'/users/changeOrgRole'>, - true ->({ - org: Joi.string(), - role: OrgRoleSchema, - user: Joi.string(), -}); - -// reset password -export const ResetPasswordSchema = Joi.object< - Api.Mutation.Input<'/users/resetPassword'>, - true ->({ - email: Joi.string().required(), -}); diff --git a/backend/src/core/users/users.controller.ts b/backend/src/core/users/users.controller.ts index 238e542f3..c2c1baa26 100644 --- a/backend/src/core/users/users.controller.ts +++ b/backend/src/core/users/users.controller.ts @@ -12,18 +12,8 @@ import { } from '@nestjs/common'; import { UsersService } from './users.service'; import { BearerAuthGuard } from '../auth/bearer-auth.guard'; -import { - AcceptOrgInviteSchema, - ChangeOrgRoleSchema, - CreateOrgSchema, - DeleteOrgSchema, - InviteToOrgSchema, - ListOrgMembersSchema, - RemoveFromOrgSchema, - RemoveOrgInviteSchema, - ResetPasswordSchema, -} from './dto'; -import { JoiValidationPipe } from '@/src/pipes/JoiValidationPipe'; +import { ZodValidationPipe } from '@/src/pipes/ZodValidationPipe'; +import { Users } from '@pc/common/types/core'; import { VError } from 'verror'; import { UserError } from './user-error'; import { Api } from '@pc/common/types/api'; @@ -38,6 +28,7 @@ export class UsersController { // ! request @Post('getAccountDetails') @UseGuards(BearerAuthGuard) + @UsePipes(new ZodValidationPipe(Users.query.inputs.getAccountDetails)) async getAccountDetails( @Request() req, @Body() _: Api.Query.Input<'/users/getAccountDetails'>, @@ -49,6 +40,7 @@ export class UsersController { // Gets a list of orgs that this user is the sole admin of. @Post('listOrgsWithOnlyAdmin') @UseGuards(BearerAuthGuard) + @UsePipes(new ZodValidationPipe(Users.query.inputs.listOrgsWithOnlyAdmin)) async listOrgsWithOnlyAdmin( @Request() req, @Body() _: Api.Query.Input<'/users/listOrgsWithOnlyAdmin'>, @@ -63,6 +55,7 @@ export class UsersController { @Post('deleteAccount') @HttpCode(204) @UseGuards(BearerAuthGuard) + @UsePipes(new ZodValidationPipe(Users.mutation.inputs.deleteAccount)) async deleteAccount( @Request() req, @Body() _: Api.Mutation.Input<'/users/deleteAccount'>, @@ -77,7 +70,7 @@ export class UsersController { @Post('createOrg') @UseGuards(BearerAuthGuard) - @UsePipes(new JoiValidationPipe(CreateOrgSchema)) + @UsePipes(new ZodValidationPipe(Users.mutation.inputs.createOrg)) async create( @Request() req, @Body() { name }: Api.Mutation.Input<'/users/createOrg'>, @@ -92,7 +85,7 @@ export class UsersController { @Post('inviteToOrg') @HttpCode(204) @UseGuards(BearerAuthGuard) - @UsePipes(new JoiValidationPipe(InviteToOrgSchema)) + @UsePipes(new ZodValidationPipe(Users.mutation.inputs.inviteToOrg)) async inviteToOrg( @Request() req, @Body() { org, email, role }: Api.Mutation.Input<'/users/inviteToOrg'>, @@ -106,7 +99,7 @@ export class UsersController { @Post('acceptOrgInvite') @UseGuards(BearerAuthGuard) - @UsePipes(new JoiValidationPipe(AcceptOrgInviteSchema)) + @UsePipes(new ZodValidationPipe(Users.mutation.inputs.acceptOrgInvite)) async acceptOrgInvite( @Request() req, @Body() { token }: Api.Mutation.Input<'/users/acceptOrgInvite'>, @@ -120,7 +113,7 @@ export class UsersController { @Post('listOrgMembers') @UseGuards(BearerAuthGuard) - @UsePipes(new JoiValidationPipe(ListOrgMembersSchema)) + @UsePipes(new ZodValidationPipe(Users.query.inputs.listOrgMembers)) async listOrgMembers( @Request() req, @Body() { org }: Api.Query.Input<'/users/listOrgMembers'>, @@ -134,6 +127,7 @@ export class UsersController { @Post('listOrgs') @UseGuards(BearerAuthGuard) + @UsePipes(new ZodValidationPipe(Users.query.inputs.listOrgs)) async listOrgs( @Request() req, @Body() _: Api.Query.Input<'/users/listOrgs'>, @@ -148,7 +142,7 @@ export class UsersController { @Post('deleteOrg') @HttpCode(204) @UseGuards(BearerAuthGuard) - @UsePipes(new JoiValidationPipe(DeleteOrgSchema)) + @UsePipes(new ZodValidationPipe(Users.mutation.inputs.deleteOrg)) async deleteOrg( @Request() req, @Body() { org }: Api.Mutation.Input<'/users/deleteOrg'>, @@ -162,7 +156,7 @@ export class UsersController { @Post('changeOrgRole') @UseGuards(BearerAuthGuard) - @UsePipes(new JoiValidationPipe(ChangeOrgRoleSchema)) + @UsePipes(new ZodValidationPipe(Users.mutation.inputs.changeOrgRole)) async changeOrgRole( @Request() req, @Body() { org, user, role }: Api.Mutation.Input<'/users/changeOrgRole'>, @@ -176,7 +170,7 @@ export class UsersController { @Post('removeFromOrg') @UseGuards(BearerAuthGuard) - @UsePipes(new JoiValidationPipe(RemoveFromOrgSchema)) + @UsePipes(new ZodValidationPipe(Users.mutation.inputs.removeFromOrg)) async removeFromOrg( @Request() req, @Body() { org, user }: Api.Mutation.Input<'/users/removeFromOrg'>, @@ -190,7 +184,7 @@ export class UsersController { @Post('removeOrgInvite') @UseGuards(BearerAuthGuard) - @UsePipes(new JoiValidationPipe(RemoveOrgInviteSchema)) + @UsePipes(new ZodValidationPipe(Users.mutation.inputs.removeOrgInvite)) async removeOrgInvite( @Request() req, @Body() { org, email }: Api.Mutation.Input<'/users/removeOrgInvite'>, @@ -203,7 +197,7 @@ export class UsersController { } @Post('resetPassword') - @UsePipes(new JoiValidationPipe(ResetPasswordSchema)) + @UsePipes(new ZodValidationPipe(Users.mutation.inputs.resetPassword)) @HttpCode(204) async resetPassword( @Body() { email }: Api.Mutation.Input<'/users/resetPassword'>, diff --git a/backend/src/modules/abi/abi.controller.ts b/backend/src/modules/abi/abi.controller.ts index 52f704d30..964fed8eb 100644 --- a/backend/src/modules/abi/abi.controller.ts +++ b/backend/src/modules/abi/abi.controller.ts @@ -1,5 +1,5 @@ import { BearerAuthGuard } from '@/src/core/auth/bearer-auth.guard'; -import { JoiValidationPipe } from '@/src/pipes/JoiValidationPipe'; +import { ZodValidationPipe } from '@/src/pipes/ZodValidationPipe'; import { BadRequestException, Body, @@ -13,7 +13,7 @@ import { import { VError } from 'verror'; import { Api } from '@pc/common/types/api'; import { AbiService } from './abi.service'; -import { AddContractAbiSchema, GetContractAbiSchema } from './dto'; +import { Abi } from '@pc/common/types/abi'; @Controller('abi') export class AbiController { @@ -21,7 +21,7 @@ export class AbiController { @Post('addContractAbi') @UseGuards(BearerAuthGuard) - @UsePipes(new JoiValidationPipe(AddContractAbiSchema)) + @UsePipes(new ZodValidationPipe(Abi.mutation.inputs.addContractAbi)) async addContractAbi( @Request() req, @Body() { contract, abi }: Api.Mutation.Input<'/abi/addContractAbi'>, @@ -35,7 +35,7 @@ export class AbiController { @Post('getContractAbi') @UseGuards(BearerAuthGuard) - @UsePipes(new JoiValidationPipe(GetContractAbiSchema)) + @UsePipes(new ZodValidationPipe(Abi.query.inputs.getContractAbi)) async getContractAbi( @Request() req, @Body() { contract }: Api.Query.Input<'/abi/getContractAbi'>, diff --git a/backend/src/modules/abi/dto.ts b/backend/src/modules/abi/dto.ts deleted file mode 100644 index c01faa8e4..000000000 --- a/backend/src/modules/abi/dto.ts +++ /dev/null @@ -1,57 +0,0 @@ -// Note: we use Joi instead of Nest's default recommendation of class-validator -// because class-validator was experiencing issues at the time of implementation -// and had many unaddressed github issues - -import { Api } from '@pc/common/types/api'; -import * as Joi from 'joi'; - -// add contract abi -const JsonSchemaSchema = Joi.object({}).unknown(true).required(); -export const AbiSchema = Joi.object({ - schema_version: Joi.string().required(), - metadata: Joi.object({ - name: Joi.string(), - version: Joi.string(), - authors: Joi.array().items(Joi.string()), - }).unknown(true), - body: Joi.object({ - functions: Joi.array() - .items( - Joi.object({ - name: Joi.string().required(), - is_view: Joi.boolean(), - is_init: Joi.boolean(), - is_payable: Joi.boolean(), - is_private: Joi.boolean(), - params: Joi.array().items( - Joi.object({ - name: Joi.string().required(), - type_schema: JsonSchemaSchema, - serialization_type: Joi.string().required(), - }), - ), - result: Joi.object({ - type_schema: JsonSchemaSchema, - serialization_type: Joi.string().required(), - }), - }), - ) - .required(), - root_schema: JsonSchemaSchema, - }).required(), -}).required(); -export const AddContractAbiSchema = Joi.object< - Api.Mutation.Input<'/abi/addContractAbi'>, - true ->({ - contract: Joi.string().required(), - abi: AbiSchema, -}); - -// get contract abi -export const GetContractAbiSchema = Joi.object< - Api.Query.Input<'/abi/getContractAbi'>, - true ->({ - contract: Joi.string().required(), -}); diff --git a/backend/src/modules/alerts/alerts.controller.ts b/backend/src/modules/alerts/alerts.controller.ts index 833047dc3..edea13512 100644 --- a/backend/src/modules/alerts/alerts.controller.ts +++ b/backend/src/modules/alerts/alerts.controller.ts @@ -15,26 +15,10 @@ import { ConfigService } from '@nestjs/config'; import { BearerAuthGuard } from 'src/core/auth/bearer-auth.guard'; import { AppConfig } from 'src/config/validate'; import { assertUnreachable } from 'src/helpers'; -import { JoiValidationPipe } from 'src/pipes/JoiValidationPipe'; +import { ZodValidationPipe } from 'src/pipes/ZodValidationPipe'; import { VError } from 'verror'; import { AlertsService } from './alerts.service'; -import { - CreateAlertSchema, - DeleteAlertSchema, - ListAlertSchema, - UpdateAlertSchema, - GetAlertDetailsSchema, - ListDestinationSchema, - DeleteDestinationSchema, - CreateDestinationSchema, - DisableDestinationSchema, - EnableDestinationSchema, - UpdateDestinationSchema, - VerifyEmailSchema, - ResendEmailVerificationSchema, - UnsubscribeFromEmailAlertSchema, - RotateWebhookDestinationSecretSchema, -} from './dto'; +import { Alerts } from '@pc/common/types/alerts'; import { TooManyRequestsException } from './exception/tooManyRequestsException'; import { TelegramService } from './telegram/telegram.service'; import { Api } from '@pc/common/types/api'; @@ -55,7 +39,7 @@ export class AlertsController { @Post('createAlert') @UseGuards(BearerAuthGuard) - @UsePipes(new JoiValidationPipe(CreateAlertSchema)) + @UsePipes(new ZodValidationPipe(Alerts.mutation.inputs.createAlert)) async createAlert( @Request() req, @Body() dto: Api.Mutation.Input<'/alerts/createAlert'>, @@ -108,7 +92,7 @@ export class AlertsController { @Post('updateAlert') @UseGuards(BearerAuthGuard) - @UsePipes(new JoiValidationPipe(UpdateAlertSchema)) + @UsePipes(new ZodValidationPipe(Alerts.mutation.inputs.updateAlert)) async updateAlert( @Request() req, @Body() { id, name, isPaused }: Api.Mutation.Input<'/alerts/updateAlert'>, @@ -122,7 +106,7 @@ export class AlertsController { @Post('listAlerts') @UseGuards(BearerAuthGuard) - @UsePipes(new JoiValidationPipe(ListAlertSchema)) + @UsePipes(new ZodValidationPipe(Alerts.query.inputs.listAlerts)) async listAlerts( @Request() req, @Body() @@ -142,7 +126,7 @@ export class AlertsController { @Post('deleteAlert') @HttpCode(204) @UseGuards(BearerAuthGuard) - @UsePipes(new JoiValidationPipe(DeleteAlertSchema)) + @UsePipes(new ZodValidationPipe(Alerts.mutation.inputs.deleteAlert)) async deleteAlert( @Request() req, @Body() { id }: Api.Mutation.Input<'/alerts/deleteAlert'>, @@ -156,7 +140,7 @@ export class AlertsController { @Post('getAlertDetails') @UseGuards(BearerAuthGuard) - @UsePipes(new JoiValidationPipe(GetAlertDetailsSchema)) + @UsePipes(new ZodValidationPipe(Alerts.query.inputs.getAlertDetails)) async getAlertDetails( @Request() req, @Body() { id }: Api.Query.Input<'/alerts/getAlertDetails'>, @@ -170,7 +154,7 @@ export class AlertsController { @Post('createDestination') @UseGuards(BearerAuthGuard) - @UsePipes(new JoiValidationPipe(CreateDestinationSchema)) + @UsePipes(new ZodValidationPipe(Alerts.mutation.inputs.createDestination)) async createDestination( @Request() req, @Body() dto: Api.Mutation.Input<'/alerts/createDestination'>, @@ -212,7 +196,7 @@ export class AlertsController { @Post('deleteDestination') @HttpCode(204) @UseGuards(BearerAuthGuard) - @UsePipes(new JoiValidationPipe(DeleteDestinationSchema)) + @UsePipes(new ZodValidationPipe(Alerts.mutation.inputs.deleteDestination)) async deleteDestination( @Request() req, @Body() { id }: Api.Mutation.Input<'/alerts/deleteDestination'>, @@ -226,7 +210,7 @@ export class AlertsController { @Post('listDestinations') @UseGuards(BearerAuthGuard) - @UsePipes(new JoiValidationPipe(ListDestinationSchema)) + @UsePipes(new ZodValidationPipe(Alerts.query.inputs.listDestinations)) async listDestinations( @Request() req, @Body() { projectSlug }: Api.Query.Input<'/alerts/listDestinations'>, @@ -241,7 +225,7 @@ export class AlertsController { @Post('enableDestination') @HttpCode(204) @UseGuards(BearerAuthGuard) - @UsePipes(new JoiValidationPipe(EnableDestinationSchema)) + @UsePipes(new ZodValidationPipe(Alerts.mutation.inputs.enableDestination)) async enableDestination( @Request() req, @Body() @@ -261,7 +245,7 @@ export class AlertsController { @Post('disableDestination') @HttpCode(204) @UseGuards(BearerAuthGuard) - @UsePipes(new JoiValidationPipe(DisableDestinationSchema)) + @UsePipes(new ZodValidationPipe(Alerts.mutation.inputs.disableDestination)) async disableDestination( @Request() req, @Body() @@ -280,7 +264,7 @@ export class AlertsController { @Post('updateDestination') @UseGuards(BearerAuthGuard) - @UsePipes(new JoiValidationPipe(UpdateDestinationSchema)) + @UsePipes(new ZodValidationPipe(Alerts.mutation.inputs.updateDestination)) async updateDestination( @Request() req, @Body() dto: Api.Mutation.Input<'/alerts/updateDestination'>, @@ -320,7 +304,9 @@ export class AlertsController { @Post('verifyEmailDestination') @HttpCode(204) - @UsePipes(new JoiValidationPipe(VerifyEmailSchema)) + @UsePipes( + new ZodValidationPipe(Alerts.mutation.inputs.verifyEmailDestination), + ) async verifyEmailDestination( @Body() { token }: Api.Mutation.Input<'/alerts/verifyEmailDestination'>, ): Promise> { @@ -334,6 +320,7 @@ export class AlertsController { // * Handler for Telegram bot, this is not an endpoint for the DevConsole frontend @Post('telegramWebhook') @HttpCode(200) + @UsePipes(new ZodValidationPipe(Alerts.mutation.inputs.telegramWebhook)) async start( @Headers('X-Telegram-Bot-Api-Secret-Token') secret: string, @Body() body: Api.Mutation.Input<'/alerts/telegramWebhook'>, @@ -400,7 +387,9 @@ export class AlertsController { @Post('resendEmailVerification') @HttpCode(204) @UseGuards(BearerAuthGuard) - @UsePipes(new JoiValidationPipe(ResendEmailVerificationSchema)) + @UsePipes( + new ZodValidationPipe(Alerts.mutation.inputs.resendEmailVerification), + ) async resendEmailVerification( @Request() req, @Body() @@ -418,7 +407,9 @@ export class AlertsController { @Post('unsubscribeFromEmailAlert') @HttpCode(204) - @UsePipes(new JoiValidationPipe(UnsubscribeFromEmailAlertSchema)) + @UsePipes( + new ZodValidationPipe(Alerts.mutation.inputs.unsubscribeFromEmailAlert), + ) async unsubscribeFromEmailAlert( @Body() { token }: Api.Mutation.Input<'/alerts/unsubscribeFromEmailAlert'>, ): Promise> { @@ -432,7 +423,11 @@ export class AlertsController { @Post('rotateWebhookDestinationSecret') @HttpCode(200) @UseGuards(BearerAuthGuard) - @UsePipes(new JoiValidationPipe(RotateWebhookDestinationSecretSchema)) + @UsePipes( + new ZodValidationPipe( + Alerts.mutation.inputs.rotateWebhookDestinationSecret, + ), + ) async rotateWebhookDestinationSecret( @Request() req, @Body() diff --git a/backend/src/modules/alerts/alerts.service.ts b/backend/src/modules/alerts/alerts.service.ts index 1ff011413..afd8feab1 100644 --- a/backend/src/modules/alerts/alerts.service.ts +++ b/backend/src/modules/alerts/alerts.service.ts @@ -191,15 +191,29 @@ export class AlertsService { this.checkAddressExists(chainId, address), ]); - const alertInput = await this.buildCreateAlertInput( - user, - chainId, - alert, - AlertRuleKind.ACTIONS, - matchingRule, - ); + const { name, projectSlug, environmentSubId, destinations } = alert; - return this.createAlert(alertInput); + return this.createAlert({ + name: name!, + alertRuleKind: AlertRuleKind.ACTIONS, + matchingRule: matchingRule as unknown as Prisma.InputJsonValue, + projectSlug, + environmentSubId, + chainId, + createdBy: user.id, + updatedBy: user.id, + enabledDestinations: { + createMany: { + data: destinations + ? destinations.map((el) => ({ + destinationId: el, + createdBy: user.id, + updatedBy: user.id, + })) + : [], + }, + }, + }); } // Checks if the alert's address exists on the Near blockchain. @@ -227,40 +241,6 @@ export class AlertsService { } } - private async buildCreateAlertInput( - user: User, - chainId: ChainId, - alert: Alerts.CreateAlertBaseInput, - alertRuleKind: AlertRuleKind, - matchingRule, - ): Promise { - const { name, projectSlug, environmentSubId, destinations } = alert; - - const alertInput: Prisma.AlertCreateInput = { - name: name!, - alertRuleKind, - matchingRule, - projectSlug, - environmentSubId, - chainId, - createdBy: user.id, - updatedBy: user.id, - enabledDestinations: { - createMany: { - data: destinations - ? destinations.map((el) => ({ - destinationId: el, - createdBy: user.id, - updatedBy: user.id, - })) - : [], - }, - }, - }; - - return alertInput; - } - private async createAlert(data: Prisma.AlertCreateInput) { try { const alert = await this.prisma.alert.create({ diff --git a/backend/src/modules/alerts/dto.spec.ts b/backend/src/modules/alerts/dto.spec.ts deleted file mode 100644 index d995a65a5..000000000 --- a/backend/src/modules/alerts/dto.spec.ts +++ /dev/null @@ -1,216 +0,0 @@ -import { CreateAlertSchema } from './dto'; - -const contract = 'pagoda.near'; -const projectSlug = '123xyz'; -const environmentSubId = 1; - -test.each([ - { projectSlug, environmentSubId, rule: { type: 'TX_SUCCESS', contract } }, - { projectSlug, environmentSubId, rule: { type: 'TX_FAILURE', contract } }, - { - projectSlug, - environmentSubId, - rule: { - type: 'FN_CALL', - contract, - function: '*', - }, - }, - { - projectSlug, - environmentSubId, - rule: { - type: 'EVENT', - contract, - standard: '*', - event: '*', - version: '*', - }, - }, - { - projectSlug, - environmentSubId, - rule: { - type: 'ACCT_BAL_NUM', - contract, - from: null, - to: '34028236692463463374607000000', - }, - }, - { - projectSlug, - environmentSubId, - rule: { - type: 'ACCT_BAL_NUM', - contract, - from: '340283463374607000000', - to: null, - }, - }, - { - projectSlug, - environmentSubId, - rule: { - type: 'ACCT_BAL_PCT', - contract, - from: '10', - to: null, - }, - }, - { - projectSlug, - environmentSubId, - rule: { - type: 'ACCT_BAL_PCT', - contract, - from: null, - to: '100', - }, - }, - { - projectSlug, - environmentSubId, - rule: { - type: 'ACCT_BAL_NUM', - contract, - from: '0', - to: '0', - }, - }, - { - projectSlug, - environmentSubId, - rule: { - type: 'ACCT_BAL_PCT', - contract, - from: '0', - to: '0', - }, - }, - { - projectSlug, - environmentSubId, - rule: { - type: 'ACCT_BAL_PCT', - contract, - from: '0', - }, - }, - { - projectSlug, - environmentSubId, - rule: { - type: 'ACCT_BAL_PCT', - contract, - to: '0', - }, - }, -])('%o should be valid', (input) => { - const result = CreateAlertSchema.validate(input); - expect(result.error).toBeUndefined(); -}); - -test.each([ - { projectSlug, environmentSubId, rule: { type: 'INCORRECT', contract } }, - { - projectSlug, - environmentSubId, - rule: { - type: 'ACCT_BAL_NUM', - contract, - from: null, - to: null, - }, - }, - { - projectSlug, - environmentSubId, - rule: { - type: 'ACCT_BAL_NUM', - contract, - }, - }, - { - projectSlug, - environmentSubId, - rule: { - type: 'ACCT_BAL_NUM', - contract, - from: '-1', - to: null, - }, - }, - { - projectSlug, - environmentSubId, - rule: { - type: 'ACCT_BAL_NUM', - contract, - from: '3', - to: '1', - }, - }, - { - projectSlug, - environmentSubId, - rule: { - type: 'ACCT_BAL_NUM', - contract, - from: '340282366920938463463374607431768211456', - to: null, - }, - }, - { - projectSlug, - environmentSubId, - rule: { - type: 'ACCT_BAL_NUM', - contract, - from: null, - to: '340282366920938463463374607431768211456', - }, - }, - { - projectSlug, - environmentSubId, - rule: { - type: 'ACCT_BAL_PCT', - contract, - from: null, - to: null, - }, - }, - { - projectSlug, - environmentSubId, - rule: { - type: 'ACCT_BAL_PCT', - contract, - from: '101', - to: null, - }, - }, - { - projectSlug, - environmentSubId, - rule: { - type: 'ACCT_BAL_PCT', - contract, - from: '0', - to: '101', - }, - }, - { - projectSlug, - environmentSubId, - rule: { - type: 'ACCT_BAL_PCT', - contract, - from: '-1', - to: null, - }, - }, -])('%o should throw errors', (input) => { - const result = CreateAlertSchema.validate(input); - expect(result.error).toBeDefined(); -}); diff --git a/backend/src/modules/alerts/dto.ts b/backend/src/modules/alerts/dto.ts deleted file mode 100644 index 485b78c91..000000000 --- a/backend/src/modules/alerts/dto.ts +++ /dev/null @@ -1,294 +0,0 @@ -// Note: we use Joi instead of Nest's default recommendation of class-validator -// because class-validator was experiencing issues at the time of implementation -// and had many unaddressed github issues -import * as Joi from 'joi'; -import { Api } from '@pc/common/types/api'; -import { Alerts } from '@pc/common/types/alerts'; - -const TxRuleSchema = Joi.object({ - type: Joi.alternatives('TX_SUCCESS', 'TX_FAILURE'), - contract: Joi.string().required(), -}); -const FnCallRuleSchema = Joi.object({ - type: Joi.string().valid('FN_CALL'), - contract: Joi.string().required(), - function: Joi.string().required(), - // params: Joi.object(), -}); -const EventRuleSchema = Joi.object({ - type: Joi.string().valid('EVENT'), - contract: Joi.string().required(), - standard: Joi.string().required(), - version: Joi.string().required(), - event: Joi.string().required(), - // data: Joi.object(), -}); - -const validateRange = (value, { original: rule }) => { - if (!rule.from && !rule.to) { - throw Error('"rule.from" or "rule.to" is required'); - } - if (rule.from && rule.to && BigInt(rule.from) > BigInt(rule.to)) { - throw Error('"rule.from" must be less than or equal to "rule.to"'); - } - return value; -}; - -const AcctBalPctRuleSchema = Joi.object({ - type: Joi.string().valid('ACCT_BAL_PCT'), - contract: Joi.string().required(), - from: Joi.string() - .empty(null) - .optional() - .regex(/^0$|^[1-9][0-9]?$|^100$/), // Percentage between 0 and 100 - to: Joi.string() - .empty(null) - .optional() - .regex(/^0$|^[1-9][0-9]?$|^100$/), // Percentage between 0 and 100 -}).custom(validateRange, 'Validating range'); - -const validateYoctonearAmount = (value, _) => { - const bnValue = BigInt(value); - const upperBound = BigInt('340282366920938463463374607431768211456'); // 2^128 - if (bnValue < 0 || bnValue >= upperBound) { - throw Error( - 'Values "to" and "from" should be integers within the range of [0, 2^128)', - ); - } - return value; -}; - -const AcctBalNumRuleSchema = Joi.object({ - type: Joi.string().valid('ACCT_BAL_NUM'), - contract: Joi.string().required(), - from: Joi.string() - .empty(null) - .optional() - .regex(/^0$|^[1-9][0-9]*$/) - .custom( - validateYoctonearAmount, - 'Validating proper value of Yoctonear amount', - ), - to: Joi.string() - .empty(null) - .optional() - .regex(/^0$|^[1-9][0-9]*$/) - .custom( - validateYoctonearAmount, - 'Validating proper value of Yoctonear amount', - ), -}).custom(validateRange, 'Validating range'); - -// create alert -export const CreateAlertSchema = Joi.object< - Api.Mutation.Input<'/alerts/createAlert'>, - true ->({ - name: Joi.string(), - projectSlug: Joi.string().required(), - environmentSubId: Joi.number().required(), - destinations: Joi.array().items(Joi.number()).optional(), - rule: Joi.alternatives() - .conditional('rule.type', { - switch: [ - { is: 'TX_SUCCESS', then: TxRuleSchema }, - { is: 'TX_FAILURE', then: TxRuleSchema }, - { is: 'FN_CALL', then: FnCallRuleSchema }, - { is: 'EVENT', then: EventRuleSchema }, - { - is: 'ACCT_BAL_PCT', - then: AcctBalPctRuleSchema, - }, - { - is: 'ACCT_BAL_NUM', - then: AcctBalNumRuleSchema, - }, - ], - }) - // TODO: fix any - .required() as any, -}); - -// update alert -export const UpdateAlertSchema = Joi.object< - Api.Mutation.Input<'/alerts/updateAlert'>, - true ->({ - id: Joi.number().required(), - name: Joi.string(), - // see https://github.com/hapijs/joi/issues/2848 - isPaused: Joi.boolean().optional() as unknown as Joi.AlternativesSchema, -}); - -// list alerts -export const ListAlertSchema = Joi.object< - Api.Query.Input<'/alerts/listAlerts'>, - true ->({ - projectSlug: Joi.string().required(), - environmentSubId: Joi.number().required(), -}); - -// delete alert -export const DeleteAlertSchema = Joi.object< - Api.Mutation.Input<'/alerts/deleteAlert'>, - true ->({ - id: Joi.number().required(), -}); - -// get alert details -export const GetAlertDetailsSchema = Joi.object< - Api.Query.Input<'/alerts/getAlertDetails'>, - true ->({ - id: Joi.number().required(), -}); - -const WebhookDestinationSchema = Joi.object< - Alerts.CreateWebhookDestinationConfig, - true ->({ - type: Joi.string().valid('WEBHOOK').required(), - url: Joi.string().required(), -}); -const EmailDestinationSchema = Joi.object< - Alerts.CreateEmailDestinationConfig, - true ->({ - type: Joi.string().valid('EMAIL').required(), - email: Joi.string().required(), -}); -const TelegramDestinationSchema = Joi.object< - Alerts.CreateTelegramDestinationConfig, - true ->({ - type: Joi.string().valid('TELEGRAM').required(), -}); -export const CreateDestinationSchema = Joi.object< - Api.Mutation.Input<'/alerts/createDestination'>, - true ->({ - name: Joi.string(), - projectSlug: Joi.string().required(), - config: Joi.alternatives([ - WebhookDestinationSchema, - EmailDestinationSchema, - TelegramDestinationSchema, - ]) - // TODO: fix any - .required() as any, -}); - -// delete destinations -export const DeleteDestinationSchema = Joi.object< - Api.Mutation.Input<'/alerts/deleteDestination'>, - true ->({ - id: Joi.number().required(), -}); - -// list destinations -export const ListDestinationSchema = Joi.object< - Api.Query.Input<'/alerts/listDestinations'>, - true ->({ - projectSlug: Joi.string().required(), -}); - -// enable destination -export const EnableDestinationSchema = Joi.object< - Api.Mutation.Input<'/alerts/enableDestination'>, - true ->({ - alert: Joi.number().required(), - destination: Joi.number().required(), -}); - -// disable destination -export const DisableDestinationSchema = EnableDestinationSchema; - -// update destination -const UpdateWebhookDestinationSchema = Joi.object< - Alerts.UpdateWebhookDestinationConfig, - true ->({ - type: Joi.string().valid('WEBHOOK').required(), - url: Joi.string(), -}); -const UpdateEmailDestinationSchema = Joi.object< - Alerts.UpdateEmailDestinationConfig, - true ->({ - type: Joi.string().valid('EMAIL').required(), -}); -const UpdateTelegramDestinationSchema = Joi.object< - Alerts.UpdateTelegramDestinationConfig, - true ->({ - type: Joi.string().valid('TELEGRAM').required(), -}); -export const UpdateDestinationSchema = Joi.object< - Api.Mutation.Input<'/alerts/updateDestination'>, - true ->({ - id: Joi.number().required(), - name: Joi.string(), - config: Joi.alternatives([ - UpdateWebhookDestinationSchema, - UpdateEmailDestinationSchema, - UpdateTelegramDestinationSchema, - ]) as any, -}); - -// verify email -export const VerifyEmailSchema = Joi.object< - Api.Mutation.Input<'/alerts/verifyEmailDestination'>, - true ->({ - token: Joi.string().required(), -}); - -// Triggered Alerts -export const ListTriggeredAlertSchema = Joi.object< - Api.Query.Input<'/triggeredAlerts/listTriggeredAlerts'>, - true ->({ - projectSlug: Joi.string().required(), - environmentSubId: Joi.number().required(), - skip: Joi.number().integer().min(0).optional(), - take: Joi.number().integer().min(0).max(100).optional(), - pagingDateTime: Joi.date().iso().optional() as unknown as Joi.StringSchema, - alertId: Joi.number().integer().positive().optional(), -}); - -export const GetTriggeredAlertDetailsSchema = Joi.object< - Api.Query.Input<'/triggeredAlerts/getTriggeredAlertDetails'>, - true ->({ - slug: Joi.string().required(), -}); - -// resend verification email -export const ResendEmailVerificationSchema = Joi.object< - Api.Mutation.Input<'/alerts/resendEmailVerification'>, - true ->({ - destinationId: Joi.number().required(), -}); - -// unsubscribe from alerts email -export const UnsubscribeFromEmailAlertSchema = Joi.object< - Api.Mutation.Input<'/alerts/unsubscribeFromEmailAlert'>, - true ->({ - token: Joi.string().required(), -}); - -// rotate webhook destination secret -export const RotateWebhookDestinationSecretSchema = Joi.object< - Api.Mutation.Input<'/alerts/rotateWebhookDestinationSecret'>, - true ->({ - destinationId: Joi.number().required(), -}); diff --git a/backend/src/modules/alerts/triggered-alerts/triggered-alerts.controller.ts b/backend/src/modules/alerts/triggered-alerts/triggered-alerts.controller.ts index f79b185ad..c9c83cc83 100644 --- a/backend/src/modules/alerts/triggered-alerts/triggered-alerts.controller.ts +++ b/backend/src/modules/alerts/triggered-alerts/triggered-alerts.controller.ts @@ -9,13 +9,10 @@ import { UsePipes, } from '@nestjs/common'; import { BearerAuthGuard } from 'src/core/auth/bearer-auth.guard'; -import { JoiValidationPipe } from 'src/pipes/JoiValidationPipe'; +import { ZodValidationPipe } from 'src/pipes/ZodValidationPipe'; import { VError } from 'verror'; import { TriggeredAlertsService } from './triggered-alerts.service'; -import { - ListTriggeredAlertSchema, - GetTriggeredAlertDetailsSchema, -} from '../dto'; +import { TriggeredAlerts } from '@pc/common/types/alerts'; import { Api } from '@pc/common/types/api'; @Controller('triggeredAlerts') @@ -28,7 +25,9 @@ export class TriggeredAlertsController { @Post('listTriggeredAlerts') @UseGuards(BearerAuthGuard) - @UsePipes(new JoiValidationPipe(ListTriggeredAlertSchema)) + @UsePipes( + new ZodValidationPipe(TriggeredAlerts.query.inputs.listTriggeredAlerts), + ) async listTriggeredAlerts( @Request() req, @Body() @@ -58,7 +57,11 @@ export class TriggeredAlertsController { @Post('getTriggeredAlertDetails') @UseGuards(BearerAuthGuard) - @UsePipes(new JoiValidationPipe(GetTriggeredAlertDetailsSchema)) + @UsePipes( + new ZodValidationPipe( + TriggeredAlerts.query.inputs.getTriggeredAlertDetails, + ), + ) async getTriggeredAlertDetails( @Request() req, @Body() diff --git a/backend/src/modules/alerts/triggered-alerts/triggered-alerts.service.ts b/backend/src/modules/alerts/triggered-alerts/triggered-alerts.service.ts index 9e126f453..024e0a113 100644 --- a/backend/src/modules/alerts/triggered-alerts/triggered-alerts.service.ts +++ b/backend/src/modules/alerts/triggered-alerts/triggered-alerts.service.ts @@ -11,6 +11,7 @@ import { TxMatchingRule, } from '../serde/db.types'; import { VError } from 'verror'; +import { Alerts } from '@pc/common/types/alerts'; @Injectable() export class TriggeredAlertsService { @@ -152,7 +153,7 @@ export class TriggeredAlertsService { slug, name: alert.name, alertId: alert.id, - type: this.alertsService.toAlertType(rule), + type: this.alertsService.toAlertType(rule) as Alerts.RuleType, triggeredInBlockHash, triggeredInTransactionHash, triggeredInReceiptId, diff --git a/backend/src/modules/rpcstats/dto.ts b/backend/src/modules/rpcstats/dto.ts deleted file mode 100644 index 33c16db75..000000000 --- a/backend/src/modules/rpcstats/dto.ts +++ /dev/null @@ -1,30 +0,0 @@ -// Note: we use Joi instead of Nest's default recommendation of class-validator -// because class-validator was experiencing issues at the time of implementation -// and had many unaddressed github issues -import * as Joi from 'joi'; -import { Api } from '@pc/common/types/api'; - -export const EndpointMetricsSchema = Joi.object< - Api.Query.Input<'/rpcstats/endpointMetrics'>, - true ->({ - projectSlug: Joi.string().required(), - environmentSubId: Joi.number().required(), - startDateTime: Joi.string().required(), - endDateTime: Joi.string().required(), - skip: Joi.number().integer().min(0).optional(), - take: Joi.number().integer().min(0).max(100).optional(), - pagingDateTime: Joi.date().iso().optional() as unknown as Joi.StringSchema, - filter: Joi.alternatives( - Joi.object({ - type: Joi.string().valid('date'), - dateTimeResolution: Joi.alternatives( - 'FIFTEEN_SECONDS', - 'ONE_MINUTE', - 'ONE_HOUR', - 'ONE_DAY', - ), - }), - Joi.object({ type: Joi.string().valid('endpoint') }), - ), -}); diff --git a/backend/src/modules/rpcstats/rpcstats.controller.ts b/backend/src/modules/rpcstats/rpcstats.controller.ts index ea0355981..68393276e 100644 --- a/backend/src/modules/rpcstats/rpcstats.controller.ts +++ b/backend/src/modules/rpcstats/rpcstats.controller.ts @@ -11,11 +11,11 @@ import { import { DateTime } from 'luxon'; import { BearerAuthGuard } from 'src/core/auth/bearer-auth.guard'; -import { JoiValidationPipe } from 'src/pipes/JoiValidationPipe'; +import { ZodValidationPipe } from 'src/pipes/ZodValidationPipe'; import { VError } from 'verror'; import { ProjectsService } from '@/src/core/projects/projects.service'; import { RpcStatsService } from './rpcstats.service'; -import { EndpointMetricsSchema } from './dto'; +import { RpcStats } from '@pc/common/types/rpcstats'; import { Api } from '@pc/common/types/api'; @Controller('rpcstats') @@ -29,7 +29,7 @@ export class RpcStatsController { @Post('endpointMetrics') @UseGuards(BearerAuthGuard) - @UsePipes(new JoiValidationPipe(EndpointMetricsSchema)) + @UsePipes(new ZodValidationPipe(RpcStats.query.inputs.endpointMetrics)) async endpointMetrics( @Request() req, @Body() diff --git a/backend/src/pipes/JoiValidationPipe.ts b/backend/src/pipes/JoiValidationPipe.ts deleted file mode 100644 index 8fec7646c..000000000 --- a/backend/src/pipes/JoiValidationPipe.ts +++ /dev/null @@ -1,26 +0,0 @@ -import { - PipeTransform, - Injectable, - ArgumentMetadata, - BadRequestException, -} from '@nestjs/common'; -import { Schema } from 'joi'; - -@Injectable() -export class JoiValidationPipe implements PipeTransform { - constructor(private schema: Schema) {} - - transform(value: any, _metadata: ArgumentMetadata) { - const { error } = this.schema.validate(value); - - if (error) { - // throw new BadRequestException('Validation failed'); - - // return error message to frontend - throw new BadRequestException( - error?.details?.[0]?.message || 'Validation failed', - ); - } - return value; - } -} diff --git a/backend/src/pipes/ZodValidationPipe.ts b/backend/src/pipes/ZodValidationPipe.ts new file mode 100644 index 000000000..977e1599d --- /dev/null +++ b/backend/src/pipes/ZodValidationPipe.ts @@ -0,0 +1,20 @@ +import { + PipeTransform, + Injectable, + ArgumentMetadata, + BadRequestException, +} from '@nestjs/common'; +import { z } from 'zod'; + +@Injectable() +export class ZodValidationPipe implements PipeTransform { + constructor(private schema: z.Schema) {} + + transform(value: unknown, _metadata: ArgumentMetadata) { + const parsedValue = this.schema.safeParse(value); + if (parsedValue.success === false) { + throw new BadRequestException(parsedValue.error.message); + } + return parsedValue.data; + } +} diff --git a/backend/terraform/modules/api/main.tf b/backend/terraform/modules/api/main.tf index 0b14ada0b..feed9b30c 100644 --- a/backend/terraform/modules/api/main.tf +++ b/backend/terraform/modules/api/main.tf @@ -68,11 +68,6 @@ resource "google_cloud_run_service" "console_api" { } } - env { - name = "TELEGRAM_ENABLE_WEBHOOK" - value = "true" - } - # TODO remove this once it's replaced by EMAIL_ALERTS_NO_REPLY env { name = "EMAIL_VERIFICATION_FROM" diff --git a/common/package.json b/common/package.json index e6296486a..98538aaac 100644 --- a/common/package.json +++ b/common/package.json @@ -11,5 +11,8 @@ "bugs": { "url": "https://github.com/near/pagoda-console/issues" }, - "homepage": "https://github.com/near/pagoda-console#readme" + "homepage": "https://github.com/near/pagoda-console#readme", + "dependencies": { + "zod": "^3.19.1" + } } diff --git a/common/tsconfig.json b/common/tsconfig.json new file mode 100644 index 000000000..3c43903cf --- /dev/null +++ b/common/tsconfig.json @@ -0,0 +1,3 @@ +{ + "extends": "../tsconfig.json" +} diff --git a/common/types/abi/abi.schema.ts b/common/types/abi/abi.schema.ts index 0748ccfa0..e1d0f4097 100644 --- a/common/types/abi/abi.schema.ts +++ b/common/types/abi/abi.schema.ts @@ -1,34 +1,87 @@ import type { AbiRoot } from 'near-abi-client-js'; -import { Abi } from '@pc/database/clients/abi'; - -export namespace Query { - export namespace Inputs { - export type GetContractAbi = { contract: string }; - } - - export namespace Outputs { - export type GetContractAbi = Pick & { - abi: AbiRoot; - }; - } - - export namespace Errors { - export type GetContractAbi = unknown; - } -} - -export namespace Mutation { - export namespace Inputs { - export type AddContractAbi = { contract: string; abi: AbiRoot }; - } - - export namespace Outputs { - export type AddContractAbi = Pick & { - abi: AbiRoot; - }; - } - - export namespace Errors { - export type AddContractAbi = unknown; - } -} +import { z } from 'zod'; +import { contractSlug } from '../core/types'; +import { json } from '../schemas'; + +export const abi = z.strictObject({ + id: z.number(), + contractSlug, + abi: json, + createdAt: z.date().or(z.null()), + createdBy: z.number().or(z.null()), +}); + +const jsonSchemaV7 = z.object({}).passthrough(); +const abiType = z.strictObject({ + type_schema: jsonSchemaV7, + serialization_type: z.string(), +}); +const abiRoot: z.ZodType = z.strictObject({ + schema_version: z.string(), + metadata: z + .object({ + name: z.string(), + version: z.string(), + authors: z.array(z.string()), + }) + .passthrough() + .optional(), + body: z.strictObject({ + functions: z.array( + z.strictObject({ + name: z.string(), + doc: z.string().optional(), + is_view: z.boolean().optional(), + is_init: z.boolean().optional(), + is_payable: z.boolean().optional(), + is_private: z.boolean().optional(), + params: z.array(abiType.extend({ name: z.string() })).optional(), + callbacks: z.any().optional(), + callbacks_vec: abiType.optional(), + result: abiType.optional(), + }), + ), + root_schema: jsonSchemaV7, + }), +}); + +export const query = { + inputs: { + getContractAbi: z.strictObject({ + contract: contractSlug, + }), + }, + + outputs: { + getContractAbi: abi.pick({ contractSlug: true }).and( + z.strictObject({ + abi: abiRoot, + }), + ), + }, + + errors: { + getContractAbi: z.unknown(), + }, +}; + +export const mutation = { + inputs: { + addContractAbi: z.strictObject({ + contract: contractSlug, + abi: abiRoot, + }), + }, + + outputs: { + addContractAbi: abi.pick({ contractSlug: true }).and( + z.strictObject({ + abi: abiRoot, + }), + ), + }, + + errors: { + addContractAbi: z.unknown(), + }, +}; diff --git a/common/types/alerts/alerts.schema.ts b/common/types/alerts/alerts.schema.ts index f1d7302bb..5cbd93504 100644 --- a/common/types/alerts/alerts.schema.ts +++ b/common/types/alerts/alerts.schema.ts @@ -1,49 +1,160 @@ -import { - Alert as AlertDatabase, - Destination as DestinationDatabase, - EmailDestination as EmailDestinationDatabase, - TelegramDestination as TelegramDestinationDatabase, - WebhookDestination as WebhookDestinationDatabase, -} from '@pc/database/clients/alerts'; - -export type RuleType = - | 'TX_SUCCESS' - | 'TX_FAILURE' - | 'FN_CALL' - | 'EVENT' - | 'ACCT_BAL_PCT' - | 'ACCT_BAL_NUM'; - -export type TransactionRule = { - type: 'TX_SUCCESS' | 'TX_FAILURE'; - contract: string; -}; -export type FunctionCallRule = { - type: 'FN_CALL'; - contract: string; - function: string; - // params?: object; -}; -export type EventRule = { - type: 'EVENT'; - contract: string; - standard: string; - version: string; - event: string; - // data?: object; -}; -export type AcctBalPctRule = { - type: 'ACCT_BAL_PCT'; - contract: string; - from?: number; - to?: number; -}; -export type AcctBalNumRule = { - type: 'ACCT_BAL_NUM'; - contract: string; - from?: string; - to?: string; -}; +import { z } from 'zod'; +import { AlertRuleKind, DestinationType } from '@pc/database/clients/alerts'; +import { accountId, environmentId, projectSlug, net } from '../core/types'; +import { json } from '../schemas'; +import { flavored, Flavored } from '../utils'; + +export const destinationType: z.ZodType = z.enum([ + 'WEBHOOK', + 'EMAIL', + 'TELEGRAM', +]); +export const alertRuleKind: z.ZodType = z.enum([ + 'ACTIONS', + 'EVENTS', + 'STATE_CHANGES', +]); + +export const alertId = z.number().refine>(flavored); +export type AlertId = z.infer; + +export const destinationId = z + .number() + .refine>(flavored); +export type DestinationId = z.infer; + +export const alertName = z.string(); +export const databaseAlert = z.strictObject({ + id: z.number(), + alertRuleKind, + name: alertName, + matchingRule: json, + isPaused: z.boolean(), + projectSlug, + environmentSubId: environmentId, + chainId: net, + active: z.boolean(), + createdAt: z.date(), + createdBy: z.number(), + updatedAt: z.date().or(z.null()), + updatedBy: z.number().or(z.null()), +}); + +export const destinationName = z.string(); +export const databaseDestination = z.strictObject({ + id: z.number(), + name: z.string().or(z.null()), + projectSlug, + type: destinationType, + active: z.boolean(), + isValid: z.boolean(), + createdAt: z.date().or(z.null()), + createdBy: z.number().or(z.null()), + updatedAt: z.date().or(z.null()), + updatedBy: z.number().or(z.null()), +}); + +export const databaseWebhookDestination = z.strictObject({ + id: z.number(), + destinationId: z.number(), + url: z.string().url(), + secret: z.string(), + createdAt: z.date().or(z.null()), + createdBy: z.number().or(z.null()), + updatedAt: z.date().or(z.null()), + updatedBy: z.number().or(z.null()), +}); + +export const databaseEmailDestination = z.strictObject({ + id: z.number(), + destinationId: z.number(), + email: z.string().email(), + token: z.string().or(z.null()), + isVerified: z.boolean(), + tokenExpiresAt: z.date().or(z.null()), + tokenCreatedAt: z.date().or(z.null()), + unsubscribeToken: z.string().or(z.null()), + createdAt: z.date().or(z.null()), + createdBy: z.number().or(z.null()), + updatedAt: z.date().or(z.null()), + updatedBy: z.number().or(z.null()), +}); + +export const databaseTelegramDestination = z.strictObject({ + id: z.number(), + destinationId: z.number(), + chatId: z.number().or(z.null()), + chatTitle: z.string().or(z.null()), + isGroupChat: z.boolean().or(z.null()), + startToken: z.string().or(z.null()), + tokenExpiresAt: z.date().or(z.null()), + createdAt: z.date(), + createdBy: z.number().or(z.null()), + updatedAt: z.date().or(z.null()), + updatedBy: z.number().or(z.null()), +}); + +export const comparator = z.enum(['EQ', 'LTE', 'GTE', 'RANGE']); +export type Comparator = z.infer; + +const percentage = z.number().int().min(0).max(100); +const upperBound = BigInt('340282366920938463463374607431768211456'); // 2^128 +const yoctoNear = z + .string() + .regex(/^0$|^[1-9][0-9]*$/) + .refine((input) => { + const bigint = BigInt(input); + return bigint >= 0 && bigint < upperBound; + }, 'Values "to" and "from" should be integers within the range of [0, 2^128)'); + +const acctBalPctRule = z + .strictObject({ + type: z.literal('ACCT_BAL_PCT'), + contract: accountId, + from: percentage.optional(), + to: percentage.optional(), + }) + // Should we use union of + // { from: number, to: number } | + // { from?: number, to: number } | + // { from: number, to?: number } + // to validate this instead? + .refine( + (input) => input.to !== undefined || input.from !== undefined, + '"rule.from" or "rule.to" is required', + ) + .refine( + (input) => + input.to === undefined || + input.from === undefined || + input.from > input.to, + { + message: '"rule.from" must be less than or equal to "rule.to"', + }, + ); +export type AcctBalPctRule = z.infer; + +const acctBalNumRule = z + .strictObject({ + type: z.literal('ACCT_BAL_NUM'), + contract: accountId, + from: yoctoNear.optional(), + to: yoctoNear.optional(), + }) + .refine( + (input) => input.to !== undefined || input.from !== undefined, + '"rule.from" or "rule.to" is required', + ) + .refine( + (input) => + input.to === undefined || + input.from === undefined || + BigInt(input.from) > BigInt(input.to), + { + message: '"rule.from" must be less than or equal to "rule.to"', + }, + ); +export type AcctBalNumRule = z.infer; export type Rule = | TransactionRule @@ -52,214 +163,358 @@ export type Rule = | AcctBalPctRule | AcctBalNumRule; -export type CreateAlertBaseInput = { - name?: string; - projectSlug: string; - environmentSubId: number; - destinations?: Array; -}; -type CreateTxAlertInput = CreateAlertBaseInput & { - rule: TransactionRule; -}; -type CreateFnCallAlertInput = CreateAlertBaseInput & { - rule: FunctionCallRule; -}; -type CreateEventAlertInput = CreateAlertBaseInput & { - rule: EventRule; -}; -type CreateAcctBalPctAlertInput = CreateAlertBaseInput & { - rule: AcctBalPctRule; -}; -type CreateAcctBalNumAlertInput = CreateAlertBaseInput & { - rule: AcctBalNumRule; -}; +type ExtractRefinementType = T extends z.ZodEffects< + infer T, + any, + any +> + ? ExtractRefinementType + : T; -export type CreateAlertInput = - | CreateTxAlertInput - | CreateFnCallAlertInput - | CreateEventAlertInput - | CreateAcctBalPctAlertInput - | CreateAcctBalNumAlertInput; +const transactionRule = z.strictObject({ + type: z.enum(['TX_SUCCESS', 'TX_FAILURE']), + contract: accountId, +}); +export type TransactionRule = z.infer; -export type CreateBaseDestinationInput = { - name?: string; - projectSlug: string; -}; +const functionCallRule = z.strictObject({ + type: z.literal('FN_CALL'), + contract: accountId, + function: z.string(), +}); +export type FunctionCallRule = z.infer; -export type CreateWebhookDestinationConfig = { - type: 'WEBHOOK'; - url: string; -}; -export type CreateEmailDestinationConfig = { - type: 'EMAIL'; - email: string; -}; -export type CreateTelegramDestinationConfig = { - type: 'TELEGRAM'; -}; +const eventRule = z.strictObject({ + type: z.literal('EVENT'), + contract: accountId, + event: z.string(), + standard: z.string(), + version: z.string(), +}); +export type EventRule = z.infer; -export type CreateDestinationInput = CreateBaseDestinationInput & { - config: - | CreateWebhookDestinationConfig - | CreateEmailDestinationConfig - | CreateTelegramDestinationConfig; -}; +const rule = z.union([ + transactionRule, + functionCallRule, + eventRule, + acctBalPctRule, + acctBalNumRule, +]); -type BaseDestination = Pick< - DestinationDatabase, - 'id' | 'name' | 'projectSlug' | 'isValid' +const updateDestinationBaseInput = z.strictObject({ + id: destinationId, + name: destinationName.optional(), +}); +export type UpdateDestinationBaseInput = z.infer< + typeof updateDestinationBaseInput >; -export type WebhookDestination = BaseDestination & { - type: 'WEBHOOK'; - config: Pick; -}; +const updateWebhookDestinationConfig = z.strictObject({ + type: z.literal('WEBHOOK'), + url: z.string(), +}); +export type UpdateWebhookDestinationConfig = z.infer< + typeof updateWebhookDestinationConfig +>; +const updateEmailDestinationConfig = z.strictObject({ + type: z.literal('EMAIL'), +}); +const updateTelegramDestinationConfig = z.strictObject({ + type: z.literal('TELEGRAM'), +}); -export type EmailDestination = BaseDestination & { - type: 'EMAIL'; - config: Pick; -}; +const createBaseDestinationInput = z.strictObject({ + name: destinationName.optional(), + projectSlug, +}); +export type CreateBaseDestinationInput = z.infer< + typeof createBaseDestinationInput +>; -export type TelegramDestination = BaseDestination & { - type: 'TELEGRAM'; - config: Pick; -}; +const createEmailDestinationConfig = z.strictObject({ + type: z.literal('EMAIL'), + email: z.string(), +}); +export type CreateEmailDestinationConfig = z.infer< + typeof createEmailDestinationConfig +>; -export type Destination = - | WebhookDestination - | EmailDestination - | TelegramDestination; +const createWebhookDestinationConfig = z.strictObject({ + type: z.literal('WEBHOOK'), + url: z.string(), +}); +export type CreateWebhookDestinationConfig = z.infer< + typeof createWebhookDestinationConfig +>; -export type UpdateDestinationBaseInput = { - id: number; - name?: string; -}; -export type UpdateWebhookDestinationConfig = { - type: 'WEBHOOK'; - url: string; -}; -export type UpdateEmailDestinationConfig = { - type: 'EMAIL'; -}; -export type UpdateTelegramDestinationConfig = { - type: 'TELEGRAM'; -}; +const createDestinationInput = createBaseDestinationInput.and( + z.strictObject({ + config: z.discriminatedUnion('type', [ + createWebhookDestinationConfig, + createEmailDestinationConfig, + z.strictObject({ + type: z.literal('TELEGRAM'), + }), + ]), + }), +); +export type CreateDestinationInput = z.infer; -export type UpdateDestinationInput = UpdateDestinationBaseInput & { - config: - | UpdateWebhookDestinationConfig - | UpdateEmailDestinationConfig - | UpdateTelegramDestinationConfig; -}; +const createAlertBaseInput = z.strictObject({ + name: alertName.optional(), + projectSlug: projectSlug, + environmentSubId: environmentId, + destinations: z.array(destinationId).optional(), +}); +export type CreateAlertBaseInput = z.infer; -type EnabledDestination = Pick & { - config: - | ({ type: 'EMAIL' } & Pick) - | ({ type: 'WEBHOOK' } & Pick) - | ({ type: 'TELEGRAM' } & Pick< - TelegramDestinationDatabase, - 'chatTitle' | 'startToken' - >); -}; +const enabledDestination = databaseDestination + .pick({ id: true, name: true }) + .and( + z.strictObject({ + config: z.discriminatedUnion('type', [ + z.strictObject({ + type: z.literal('EMAIL'), + config: databaseEmailDestination.pick({ email: true }), + }), + z.strictObject({ + type: z.literal('WEBHOOK'), + config: databaseWebhookDestination.pick({ url: true }), + }), + z.strictObject({ + type: z.literal('TELEGRAM'), + config: databaseTelegramDestination.pick({ + chatTitle: true, + startToken: true, + }), + }), + ]), + }), + ); -export type Alert = Pick< - AlertDatabase, - 'id' | 'name' | 'isPaused' | 'projectSlug' | 'environmentSubId' -> & { - rule: Rule; - enabledDestinations: EnabledDestination[]; -}; +export const ruleType = z.union([ + transactionRule.shape.type, + functionCallRule.shape.type, + eventRule.shape.type, + z.literal('ACCT_BAL_PCT'), + z.literal('ACCT_BAL_NUM'), +]); +export type RuleType = z.infer; -export type TgUpdate = { - update_id: number; - message?: { - chat: TgChat; - text?: string; - }; -}; +const alert = databaseAlert + .pick({ + id: true, + name: true, + isPaused: true, + projectSlug: true, + environmentSubId: true, + }) + .and( + z.strictObject({ + rule, + enabledDestinations: enabledDestination.array(), + }), + ); +export type Alert = z.infer; -export type TgChat = TgPrivateChat | TgGroupChat; +const baseDestination = databaseDestination.pick({ + id: true, + name: true, + projectSlug: true, + isValid: true, +}); +const webhookDestination = baseDestination.and( + z.strictObject({ + type: z.literal('WEBHOOK'), + config: databaseWebhookDestination.pick({ url: true }).and( + z.strictObject({ + secret: z.string(), + }), + ), + }), +); +export type WebhookDestination = z.infer; +const emailDestination = baseDestination.and( + z.strictObject({ + type: z.literal('EMAIL'), + config: databaseEmailDestination.pick({ email: true }), + }), +); +export type EmailDestination = z.infer; +const telegramDestination = baseDestination.and( + z.strictObject({ + type: z.literal('TELEGRAM'), + config: databaseTelegramDestination.pick({ + startToken: true, + chatTitle: true, + }), + }), +); +export type TelegramDestination = z.infer; +const destination = z.union([ + webhookDestination, + emailDestination, + telegramDestination, +]); +export type Destination = z.infer; -type TgPrivateChat = { - id: number; - type: 'private'; - username?: string; -}; +const tgChat = z.union([ + z.strictObject({ + type: z.literal('private'), + id: z.number(), + username: z.string().optional(), + }), + z.strictObject({ + type: z.enum(['group', 'supergroup', 'channel']), + id: z.number(), + title: z.string().optional(), + }), +]); +export type TgChat = z.infer; + +export const query = { + inputs: { + listAlerts: z.strictObject({ + projectSlug, + environmentSubId: environmentId, + }), + getAlertDetails: z.strictObject({ + id: alertId, + }), + listDestinations: z.strictObject({ + projectSlug, + }), + }, + + outputs: { + listAlerts: alert.array(), + getAlertDetails: alert, + listDestinations: destination.array(), + }, -type TgGroupChat = { - id: number; - type: 'group' | 'supergroup' | 'channel'; - title?: string; + errors: { + listAlerts: z.unknown(), + getAlertDetails: z.unknown(), + listDestinations: z.unknown(), + }, }; -export type Comparator = 'EQ' | 'LTE' | 'GTE' | 'RANGE'; - -export namespace Query { - export namespace Inputs { - export type ListAlerts = { projectSlug: string; environmentSubId: number }; - export type GetAlertDetails = { id: number }; - export type ListDestinations = { projectSlug: string }; - } - - export namespace Outputs { - export type ListAlerts = Alert[]; - export type GetAlertDetails = Alert; - export type ListDestinations = Destination[]; - } - - export namespace Errors { - export type ListAlerts = unknown; - export type GetAlertDetails = unknown; - export type ListDestinations = unknown; - } -} - -export namespace Mutation { - export namespace Inputs { - export type CreateAlert = CreateAlertInput; - export type UpdateAlert = { id: number; name?: string; isPaused?: boolean }; - export type DeleteAlert = { id: number }; - export type CreateDestination = CreateDestinationInput; - export type DeleteDestination = { id: number }; - export type EnableDestination = { alert: number; destination: number }; - export type DisableDestination = { alert: number; destination: number }; - export type UpdateDestination = UpdateDestinationInput; - export type VerifyEmailDestination = { token: string }; - export type TelegramWebhook = TgUpdate; - export type ResendEmailVerification = { destinationId: number }; - export type UnsubscribeFromEmailAlert = { token: string }; - export type RotateWebhookDestinationSecret = { destinationId: number }; - } - - export namespace Outputs { - export type CreateAlert = Alert; - export type UpdateAlert = Alert; - export type DeleteAlert = void; - export type CreateDestination = Destination; - export type DeleteDestination = void; - export type EnableDestination = void; - export type DisableDestination = void; - export type UpdateDestination = Destination; - export type VerifyEmailDestination = void; - export type TelegramWebhook = void; - export type ResendEmailVerification = void; - export type UnsubscribeFromEmailAlert = void; - export type RotateWebhookDestinationSecret = WebhookDestination; - } - - export namespace Errors { - export type CreateAlert = unknown; - export type UpdateAlert = unknown; - export type DeleteAlert = unknown; - export type CreateDestination = unknown; - export type DeleteDestination = unknown; - export type EnableDestination = unknown; - export type DisableDestination = unknown; - export type UpdateDestination = unknown; - export type VerifyEmailDestination = unknown; - export type TelegramWebhook = unknown; - export type ResendEmailVerification = unknown; - export type UnsubscribeFromEmailAlert = unknown; - export type RotateWebhookDestinationSecret = unknown; - } -} +export const mutation = { + inputs: { + createAlert: createAlertBaseInput.and( + z.strictObject({ + rule: z.discriminatedUnion('type', [ + z.strictObject({ + type: z.literal('TX_SUCCESS'), + contract: accountId, + }), + z.strictObject({ + type: z.literal('TX_FAILURE'), + contract: accountId, + }), + z.strictObject({ + type: z.literal('FN_CALL'), + contract: accountId, + function: z.string(), + }), + z.strictObject({ + type: z.literal('EVENT'), + contract: accountId, + standard: z.string(), + version: z.string(), + event: z.string(), + }), + // see https://github.com/colinhacks/zod/issues/1490 + acctBalPctRule as unknown as ExtractRefinementType< + typeof acctBalPctRule + >, + acctBalNumRule as unknown as ExtractRefinementType< + typeof acctBalNumRule + >, + ]), + }), + ), + updateAlert: z.strictObject({ + id: alertId, + name: alertName.optional(), + isPaused: z.boolean().optional(), + }), + deleteAlert: z.strictObject({ + id: alertId, + }), + createDestination: createDestinationInput, + deleteDestination: z.strictObject({ + id: destinationId, + }), + enableDestination: z.strictObject({ + alert: alertId, + destination: destinationId, + }), + disableDestination: z.strictObject({ + alert: alertId, + destination: destinationId, + }), + updateDestination: updateDestinationBaseInput.and( + z.strictObject({ + config: z.discriminatedUnion('type', [ + updateWebhookDestinationConfig, + updateEmailDestinationConfig, + updateTelegramDestinationConfig, + ]), + }), + ), + verifyEmailDestination: z.strictObject({ + token: z.string(), + }), + telegramWebhook: z.strictObject({ + update_id: z.number(), + message: z + .strictObject({ + chat: tgChat, + text: z.string().optional(), + }) + .optional(), + }), + resendEmailVerification: z.strictObject({ + destinationId, + }), + unsubscribeFromEmailAlert: z.strictObject({ + token: z.string(), + }), + rotateWebhookDestinationSecret: z.strictObject({ + destinationId, + }), + }, + + outputs: { + createAlert: alert, + updateAlert: alert, + deleteAlert: z.void(), + createDestination: destination, + deleteDestination: z.void(), + enableDestination: z.void(), + disableDestination: z.void(), + updateDestination: destination, + verifyEmailDestination: z.void(), + telegramWebhook: z.void(), + resendEmailVerification: z.void(), + unsubscribeFromEmailAlert: z.void(), + rotateWebhookDestinationSecret: webhookDestination, + }, + + errors: { + createAlert: z.unknown(), + updateAlert: z.unknown(), + deleteAlert: z.unknown(), + createDestination: z.unknown(), + deleteDestination: z.unknown(), + enableDestination: z.unknown(), + disableDestination: z.unknown(), + updateDestination: z.unknown(), + verifyEmailDestination: z.unknown(), + telegramWebhook: z.unknown(), + resendEmailVerification: z.unknown(), + unsubscribeFromEmailAlert: z.unknown(), + rotateWebhookDestinationSecret: z.unknown(), + }, +}; diff --git a/common/types/alerts/triggered-alerts.schema.ts b/common/types/alerts/triggered-alerts.schema.ts index 47b2d7314..8162a0e43 100644 --- a/common/types/alerts/triggered-alerts.schema.ts +++ b/common/types/alerts/triggered-alerts.schema.ts @@ -1,42 +1,60 @@ -import { RuleType } from './alerts.schema'; +import { z } from 'zod'; +import { stringifiedDate } from '../schemas'; +import { + blockHash, + environmentId, + projectSlug, + receiptId, + transactionHash, +} from '../core/types'; +import { ruleType, alertId } from './alerts.schema'; +import { flavored, Flavored } from '../utils'; -export type TriggeredAlert = { - slug: string; - alertId: number; - name: string; - type: RuleType; - triggeredInBlockHash: string; - triggeredInTransactionHash: string | null; - triggeredInReceiptId: string | null; - triggeredAt: string; - extraData?: Record; -}; +export const triggeredAlertSlug = z + .string() + .refine>(flavored); +export type TriggeredAlertSlug = z.infer; -export type TriggeredAlertList = { - count: number; - page: Array; -}; +const triggeredAlert = z.strictObject({ + slug: triggeredAlertSlug, + alertId: z.number(), + name: z.string(), + type: ruleType, + triggeredInBlockHash: blockHash, + triggeredInTransactionHash: transactionHash.or(z.null()), + triggeredInReceiptId: receiptId.or(z.null()), + triggeredAt: stringifiedDate, + extraData: z.record(z.unknown()), +}); +export type TriggeredAlert = z.infer; -export namespace Query { - export namespace Inputs { - export type ListTriggeredAlerts = { - projectSlug: string; - environmentSubId: number; - skip?: number; - take?: number; - pagingDateTime?: string; - alertId?: number; - }; - export type GetTriggeredAlertDetails = { slug: string }; - } +const triggeredAlerts = z.strictObject({ + count: z.number(), + page: triggeredAlert.array(), +}); - export namespace Outputs { - export type ListTriggeredAlerts = TriggeredAlertList; - export type GetTriggeredAlertDetails = TriggeredAlert; - } +export const query = { + inputs: { + listTriggeredAlerts: z.strictObject({ + projectSlug, + environmentSubId: environmentId, + skip: z.number().int().min(0).optional(), + take: z.number().int().min(0).max(100).optional(), + pagingDateTime: stringifiedDate.optional(), + alertId: alertId.optional(), + }), + getTriggeredAlertDetails: z.strictObject({ + slug: triggeredAlertSlug, + }), + }, - export namespace Errors { - export type ListTriggeredAlerts = unknown; - export type GetTriggeredAlertDetails = unknown; - } -} + outputs: { + listTriggeredAlerts: triggeredAlerts, + getTriggeredAlertDetails: triggeredAlert, + }, + + errors: { + listTriggeredAlerts: z.unknown(), + getTriggeredAlertDetails: z.unknown(), + }, +}; diff --git a/common/types/api.ts b/common/types/api.ts index f6cf084a5..e9e873f3f 100644 --- a/common/types/api.ts +++ b/common/types/api.ts @@ -1,3 +1,5 @@ +import { z } from 'zod'; + import type { Abi } from './abi'; import type { Alerts, TriggeredAlerts } from './alerts'; import type { Explorer, Projects, Users } from './core'; @@ -7,116 +9,124 @@ export namespace Api { export namespace Query { type Mapping = { '/explorer/activity': { - input: Explorer.Query.Inputs.Activity; - output: Explorer.Query.Outputs.Activity; - error: Explorer.Query.Errors.Activity; + input: z.infer; + output: z.infer; + error: z.infer; }; '/explorer/balanceChanges': { - input: Explorer.Query.Inputs.BalanceChanges; - output: Explorer.Query.Outputs.BalanceChanges; - error: Explorer.Query.Errors.BalanceChanges; + input: z.infer; + output: z.infer; + error: z.infer; }; '/explorer/transaction': { - input: Explorer.Query.Inputs.GetTransaction; - output: Explorer.Query.Outputs.GetTransaction; - error: Explorer.Query.Errors.GetTransaction; + input: z.infer; + output: z.infer; + error: z.infer; }; '/explorer/getTransactions': { - input: Explorer.Query.Inputs.GetTransactions; - output: Explorer.Query.Outputs.GetTransactions; - error: Explorer.Query.Errors.GetTransactions; + input: z.infer; + output: z.infer; + error: z.infer; }; '/projects/getDetails': { - input: Projects.Query.Inputs.GetDetails; - output: Projects.Query.Outputs.GetDetails; - error: Projects.Query.Errors.GetDetails; + input: z.infer; + output: z.infer; + error: z.infer; }; '/projects/getContracts': { - input: Projects.Query.Inputs.GetContracts; - output: Projects.Query.Outputs.GetContracts; - error: Projects.Query.Errors.GetContracts; + input: z.infer; + output: z.infer; + error: z.infer; }; '/projects/getContract': { - input: Projects.Query.Inputs.GetContract; - output: Projects.Query.Outputs.GetContract; - error: Projects.Query.Errors.GetContract; + input: z.infer; + output: z.infer; + error: z.infer; }; '/projects/list': { - input: Projects.Query.Inputs.List; - output: Projects.Query.Outputs.List; - error: Projects.Query.Errors.List; + input: z.infer; + output: z.infer; + error: z.infer; }; '/projects/getEnvironments': { - input: Projects.Query.Inputs.GetEnvironments; - output: Projects.Query.Outputs.GetEnvironments; - error: Projects.Query.Errors.GetEnvironments; + input: z.infer; + output: z.infer; + error: z.infer; }; '/projects/getKeys': { - input: Projects.Query.Inputs.GetKeys; - output: Projects.Query.Outputs.GetKeys; - error: Projects.Query.Errors.GetKeys; + input: z.infer; + output: z.infer; + error: z.infer; }; '/users/getAccountDetails': { - input: Users.Query.Inputs.GetAccountDetails; + input: z.infer; // TODO: verify those types, no idea where they are from - output: Users.Query.Outputs.GetAccountDetails; - error: Users.Query.Errors.GetAccountDetails; + output: z.infer; + error: z.infer; }; '/users/listOrgsWithOnlyAdmin': { - input: Users.Query.Inputs.ListOrgsWithOnlyAdmin; - output: Users.Query.Outputs.ListOrgsWithOnlyAdmin; - error: Users.Query.Errors.ListOrgsWithOnlyAdmin; + input: z.infer; + output: z.infer; + error: z.infer; }; '/users/listOrgMembers': { - input: Users.Query.Inputs.ListOrgMembers; - output: Users.Query.Outputs.ListOrgMembers; - error: Users.Query.Errors.ListOrgMembers; + input: z.infer; + output: z.infer; + error: z.infer; }; '/users/listOrgs': { - input: Users.Query.Inputs.ListOrgs; - output: Users.Query.Outputs.ListOrgs; - error: Users.Query.Errors.ListOrgs; + input: z.infer; + output: z.infer; + error: z.infer; }; '/abi/getContractAbi': { - input: Abi.Query.Inputs.GetContractAbi; - output: Abi.Query.Outputs.GetContractAbi; - error: Abi.Query.Errors.GetContractAbi; + input: z.infer; + output: z.infer; + error: z.infer; }; '/alerts/listAlerts': { - input: Alerts.Query.Inputs.ListAlerts; - output: Alerts.Query.Outputs.ListAlerts; - error: Alerts.Query.Errors.ListAlerts; + input: z.infer; + output: z.infer; + error: z.infer; }; '/alerts/getAlertDetails': { - input: Alerts.Query.Inputs.GetAlertDetails; - output: Alerts.Query.Outputs.GetAlertDetails; - error: Alerts.Query.Errors.GetAlertDetails; + input: z.infer; + output: z.infer; + error: z.infer; }; '/alerts/listDestinations': { - input: Alerts.Query.Inputs.ListDestinations; - output: Alerts.Query.Outputs.ListDestinations; - error: Alerts.Query.Errors.ListDestinations; + input: z.infer; + output: z.infer; + error: z.infer; }; '/triggeredAlerts/listTriggeredAlerts': { - input: TriggeredAlerts.Query.Inputs.ListTriggeredAlerts; - output: TriggeredAlerts.Query.Outputs.ListTriggeredAlerts; - error: TriggeredAlerts.Query.Errors.ListTriggeredAlerts; + input: z.infer; + output: z.infer< + typeof TriggeredAlerts.query.outputs.listTriggeredAlerts + >; + error: z.infer; }; '/triggeredAlerts/getTriggeredAlertDetails': { - input: TriggeredAlerts.Query.Inputs.GetTriggeredAlertDetails; - output: TriggeredAlerts.Query.Outputs.GetTriggeredAlertDetails; - error: TriggeredAlerts.Query.Errors.GetTriggeredAlertDetails; + input: z.infer< + typeof TriggeredAlerts.query.inputs.getTriggeredAlertDetails + >; + output: z.infer< + typeof TriggeredAlerts.query.outputs.getTriggeredAlertDetails + >; + error: z.infer< + typeof TriggeredAlerts.query.errors.getTriggeredAlertDetails + >; }; '/rpcstats/endpointMetrics': { - input: RpcStats.Query.Inputs.EndpointMetrics; - output: RpcStats.Query.Outputs.EndpointMetrics; - error: RpcStats.Query.Errors.EndpointMetrics; + input: z.infer; + output: z.infer; + error: z.infer; }; }; @@ -129,163 +139,171 @@ export namespace Api { export namespace Mutation { type Mapping = { '/projects/create': { - input: Projects.Mutation.Inputs.Create; - output: Projects.Mutation.Outputs.Create; - error: Projects.Mutation.Errors.Create; + input: z.infer; + output: z.infer; + error: z.infer; }; '/projects/ejectTutorial': { - input: Projects.Mutation.Inputs.EjectTutorial; - output: Projects.Mutation.Outputs.EjectTutorial; - error: Projects.Mutation.Errors.EjectTutorial; + input: z.infer; + output: z.infer; + error: z.infer; }; '/projects/delete': { - input: Projects.Mutation.Inputs.Delete; - output: Projects.Mutation.Outputs.Delete; - error: Projects.Mutation.Errors.Delete; + input: z.infer; + output: z.infer; + error: z.infer; }; '/projects/addContract': { - input: Projects.Mutation.Inputs.AddContract; - output: Projects.Mutation.Outputs.AddContract; - error: Projects.Mutation.Errors.AddContract; + input: z.infer; + output: z.infer; + error: z.infer; }; '/projects/removeContract': { - input: Projects.Mutation.Inputs.RemoveContract; - output: Projects.Mutation.Outputs.RemoveContract; - error: Projects.Mutation.Errors.RemoveContract; + input: z.infer; + output: z.infer; + error: z.infer; }; '/projects/rotateKey': { - input: Projects.Mutation.Inputs.RotateKey; - output: Projects.Mutation.Outputs.RotateKey; - error: Projects.Mutation.Errors.RotateKey; + input: z.infer; + output: z.infer; + error: z.infer; }; '/projects/generateKey': { - input: Projects.Mutation.Inputs.GenerateKey; - output: Projects.Mutation.Outputs.GenerateKey; - error: Projects.Mutation.Errors.GenerateKey; + input: z.infer; + output: z.infer; + error: z.infer; }; '/projects/deleteKey': { - input: Projects.Mutation.Inputs.DeleteKey; - output: Projects.Mutation.Outputs.DeleteKey; - error: Projects.Mutation.Errors.DeleteKey; + input: z.infer; + output: z.infer; + error: z.infer; }; '/users/deleteAccount': { - input: Users.Mutation.Inputs.DeleteAccount; - output: Users.Mutation.Outputs.DeleteAccount; - error: Users.Mutation.Errors.DeleteAccount; + input: z.infer; + output: z.infer; + error: z.infer; }; '/users/createOrg': { - input: Users.Mutation.Inputs.CreateOrg; - output: Users.Mutation.Outputs.CreateOrg; - error: Users.Mutation.Errors.CreateOrg; + input: z.infer; + output: z.infer; + error: z.infer; }; '/users/inviteToOrg': { - input: Users.Mutation.Inputs.InviteToOrg; - output: Users.Mutation.Outputs.InviteToOrg; - error: Users.Mutation.Errors.InviteToOrg; + input: z.infer; + output: z.infer; + error: z.infer; }; '/users/acceptOrgInvite': { - input: Users.Mutation.Inputs.AcceptOrgInvite; - output: Users.Mutation.Outputs.AcceptOrgInvite; - error: Users.Mutation.Errors.AcceptOrgInvite; + input: z.infer; + output: z.infer; + error: z.infer; }; '/users/deleteOrg': { - input: Users.Mutation.Inputs.DeleteOrg; - output: Users.Mutation.Outputs.DeleteOrg; - error: Users.Mutation.Errors.DeleteOrg; + input: z.infer; + output: z.infer; + error: z.infer; }; '/users/changeOrgRole': { - input: Users.Mutation.Inputs.ChangeOrgRole; - output: Users.Mutation.Outputs.ChangeOrgRole; - error: Users.Mutation.Errors.ChangeOrgRole; + input: z.infer; + output: z.infer; + error: z.infer; }; '/users/removeFromOrg': { - input: Users.Mutation.Inputs.RemoveFromOrg; - output: Users.Mutation.Outputs.RemoveFromOrg; - error: Users.Mutation.Errors.RemoveFromOrg; + input: z.infer; + output: z.infer; + error: z.infer; }; '/users/removeOrgInvite': { - input: Users.Mutation.Inputs.RemoveOrgInvite; - output: Users.Mutation.Outputs.RemoveOrgInvite; - error: Users.Mutation.Errors.RemoveOrgInvite; + input: z.infer; + output: z.infer; + error: z.infer; }; '/users/resetPassword': { - input: Users.Mutation.Inputs.ResetPassword; - output: Users.Mutation.Outputs.ResetPassword; - error: Users.Mutation.Errors.ResetPassword; + input: z.infer; + output: z.infer; + error: z.infer; }; '/abi/addContractAbi': { - input: Abi.Mutation.Inputs.AddContractAbi; - output: Abi.Mutation.Outputs.AddContractAbi; - error: Abi.Mutation.Errors.AddContractAbi; + input: z.infer; + output: z.infer; + error: z.infer; }; '/alerts/createAlert': { - input: Alerts.Mutation.Inputs.CreateAlert; - output: Alerts.Mutation.Outputs.CreateAlert; - error: Alerts.Mutation.Errors.CreateAlert; + input: z.infer; + output: z.infer; + error: z.infer; }; '/alerts/updateAlert': { - input: Alerts.Mutation.Inputs.UpdateAlert; - output: Alerts.Mutation.Outputs.UpdateAlert; - error: Alerts.Mutation.Errors.UpdateAlert; + input: z.infer; + output: z.infer; + error: z.infer; }; '/alerts/deleteAlert': { - input: Alerts.Mutation.Inputs.DeleteAlert; - output: Alerts.Mutation.Outputs.DeleteAlert; - error: Alerts.Mutation.Errors.DeleteAlert; + input: z.infer; + output: z.infer; + error: z.infer; }; '/alerts/createDestination': { - input: Alerts.Mutation.Inputs.CreateDestination; - output: Alerts.Mutation.Outputs.CreateDestination; - error: Alerts.Mutation.Errors.CreateDestination; + input: z.infer; + output: z.infer; + error: z.infer; }; '/alerts/deleteDestination': { - input: Alerts.Mutation.Inputs.DeleteDestination; - output: Alerts.Mutation.Outputs.DeleteDestination; - error: Alerts.Mutation.Errors.DeleteDestination; + input: z.infer; + output: z.infer; + error: z.infer; }; '/alerts/enableDestination': { - input: Alerts.Mutation.Inputs.EnableDestination; - output: Alerts.Mutation.Outputs.EnableDestination; - error: Alerts.Mutation.Errors.EnableDestination; + input: z.infer; + output: z.infer; + error: z.infer; }; '/alerts/disableDestination': { - input: Alerts.Mutation.Inputs.DisableDestination; - output: Alerts.Mutation.Outputs.DisableDestination; - error: Alerts.Mutation.Errors.DisableDestination; + input: z.infer; + output: z.infer; + error: z.infer; }; '/alerts/updateDestination': { - input: Alerts.Mutation.Inputs.UpdateDestination; - output: Alerts.Mutation.Outputs.UpdateDestination; - error: Alerts.Mutation.Errors.UpdateDestination; + input: z.infer; + output: z.infer; + error: z.infer; }; '/alerts/verifyEmailDestination': { - input: Alerts.Mutation.Inputs.VerifyEmailDestination; - output: Alerts.Mutation.Outputs.VerifyEmailDestination; - error: Alerts.Mutation.Errors.VerifyEmailDestination; + input: z.infer; + output: z.infer; + error: z.infer; }; // TODO: should we expose that? '/alerts/telegramWebhook': { - input: Alerts.Mutation.Inputs.TelegramWebhook; - output: Alerts.Mutation.Outputs.TelegramWebhook; - error: Alerts.Mutation.Errors.TelegramWebhook; + input: z.infer; + output: z.infer; + error: z.infer; }; '/alerts/resendEmailVerification': { - input: Alerts.Mutation.Inputs.ResendEmailVerification; - output: Alerts.Mutation.Outputs.ResendEmailVerification; - error: Alerts.Mutation.Errors.ResendEmailVerification; + input: z.infer; + output: z.infer; + error: z.infer; }; '/alerts/unsubscribeFromEmailAlert': { - input: Alerts.Mutation.Inputs.UnsubscribeFromEmailAlert; - output: Alerts.Mutation.Outputs.UnsubscribeFromEmailAlert; - error: Alerts.Mutation.Errors.UnsubscribeFromEmailAlert; + input: z.infer; + output: z.infer< + typeof Alerts.mutation.outputs.unsubscribeFromEmailAlert + >; + error: z.infer; }; '/alerts/rotateWebhookDestinationSecret': { - input: Alerts.Mutation.Inputs.RotateWebhookDestinationSecret; - output: Alerts.Mutation.Outputs.RotateWebhookDestinationSecret; - error: Alerts.Mutation.Errors.RotateWebhookDestinationSecret; + input: z.infer< + typeof Alerts.mutation.inputs.rotateWebhookDestinationSecret + >; + output: z.infer< + typeof Alerts.mutation.outputs.rotateWebhookDestinationSecret + >; + error: z.infer< + typeof Alerts.mutation.errors.rotateWebhookDestinationSecret + >; }; }; diff --git a/common/types/core/explorer-errors.ts b/common/types/core/explorer-errors.ts index c84b14055..94ad37edf 100644 --- a/common/types/core/explorer-errors.ts +++ b/common/types/core/explorer-errors.ts @@ -1,142 +1,193 @@ -export type UnknownError = { type: 'unknown' }; +import { z } from 'zod'; +import { accountId, yoctoNear } from './types'; -export type FunctionCallError = - | { - type: 'compilationError'; - error: CompilationError; - } - | { type: 'linkError'; msg: string } - | { type: 'methodResolveError' } - | { type: 'wasmTrap' } - | { type: 'wasmUnknownError' } - | { type: 'hostError' } - | { type: 'evmError' } - | { type: 'executionError'; error: string } - | UnknownError; +export const unknownError = z.strictObject({ type: z.literal('unknown') }); +export type UnknownError = z.infer; -export type NewReceiptValidationError = - | { type: 'invalidPredecessorId'; accountId: string } - | { type: 'invalidReceiverId'; accountId: string } - | { type: 'invalidSignerId'; accountId: string } - | { type: 'invalidDataReceiverId'; accountId: string } - | { type: 'returnedValueLengthExceeded'; length: number; limit: number } - | { - type: 'numberInputDataDependenciesExceeded'; - numberOfInputDataDependencies: number; - limit: number; - } - | { type: 'actionsValidation' } - | UnknownError; +const compilationError = z.discriminatedUnion('type', [ + z.strictObject({ type: z.literal('codeDoesNotExist'), accountId }), + z.strictObject({ type: z.literal('prepareError') }), + z.strictObject({ type: z.literal('wasmerCompileError'), msg: z.string() }), + z.strictObject({ type: z.literal('unsupportedCompiler'), msg: z.string() }), + unknownError, +]); +export type CompilationError = z.infer; -export type CompilationError = - | { type: 'codeDoesNotExist'; accountId: string } - | { type: 'prepareError' } - | { type: 'wasmerCompileError'; msg: string } - | { type: 'unsupportedCompiler'; msg: string } - | UnknownError; +const functionCallError = z.discriminatedUnion('type', [ + z.strictObject({ + type: z.literal('compilationError'), + error: compilationError, + }), + z.strictObject({ type: z.literal('linkError'), msg: z.string() }), + z.strictObject({ type: z.literal('methodResolveError') }), + z.strictObject({ type: z.literal('wasmTrap') }), + z.strictObject({ type: z.literal('wasmUnknownError') }), + z.strictObject({ type: z.literal('hostError') }), + z.strictObject({ type: z.literal('evmError') }), + z.strictObject({ type: z.literal('executionError'), error: z.string() }), + unknownError, +]); +export type FunctionCallError = z.infer; -export type ReceiptActionError = - | { - type: 'accountAlreadyExists'; - accountId: string; - } - | { - type: 'accountDoesNotExist'; - accountId: string; - } - | { - type: 'createAccountOnlyByRegistrar'; - accountId: string; - registrarAccountId: string; - predecessorId: string; - } - | { - type: 'createAccountNotAllowed'; - accountId: string; - predecessorId: string; - } - | { - type: 'actorNoPermission'; - accountId: string; - actorId: string; - } - | { - type: 'deleteKeyDoesNotExist'; - accountId: string; - publicKey: string; - } - | { - type: 'addKeyAlreadyExists'; - accountId: string; - publicKey: string; - } - | { - type: 'deleteAccountStaking'; - accountId: string; - } - | { - type: 'lackBalanceForState'; - accountId: string; - amount: string; - } - | { - type: 'triesToUnstake'; - accountId: string; - } - | { - type: 'triesToStake'; - accountId: string; - stake: string; - locked: string; - balance: string; - } - | { - type: 'insufficientStake'; - accountId: string; - stake: string; - minimumStake: string; - } - | { - type: 'functionCallError'; - error: FunctionCallError; - } - | { - type: 'newReceiptValidationError'; - error: NewReceiptValidationError; - } - | { type: 'onlyImplicitAccountCreationAllowed'; accountId: string } - | { type: 'deleteAccountWithLargeState'; accountId: string } - | UnknownError; +const newReceiptValidationError = z.discriminatedUnion('type', [ + z.strictObject({ type: z.literal('invalidPredecessorId'), accountId }), + z.strictObject({ type: z.literal('invalidReceiverId'), accountId }), + z.strictObject({ type: z.literal('invalidSignerId'), accountId }), + z.strictObject({ type: z.literal('invalidDataReceiverId'), accountId }), + z.strictObject({ + type: z.literal('returnedValueLengthExceeded'), + length: z.number(), + limit: z.number(), + }), + z.strictObject({ + type: z.literal('numberInputDataDependenciesExceeded'), + numberOfInputDataDependencies: z.number(), + limit: z.number(), + }), + z.strictObject({ type: z.literal('actionsValidation') }), + unknownError, +]); +export type NewReceiptValidationError = z.infer< + typeof newReceiptValidationError +>; -export type ReceiptTransactionError = - | { type: 'invalidAccessKeyError' } - | { type: 'invalidSignerId'; signerId: string } - | { type: 'signerDoesNotExist'; signerId: string } - | { type: 'invalidNonce'; transactionNonce: number; akNonce: number } - | { type: 'nonceTooLarge'; transactionNonce: number; upperBound: number } - | { type: 'invalidReceiverId'; receiverId: string } - | { type: 'invalidSignature' } - | { - type: 'notEnoughBalance'; - signerId: string; - balance: string; - cost: string; - } - | { type: 'lackBalanceForState'; signerId: string; amount: string } - | { type: 'costOverflow' } - | { type: 'invalidChain' } - | { type: 'expired' } - | { type: 'actionsValidation' } - | { type: 'transactionSizeExceeded'; size: number; limit: number } - | UnknownError; +const receiptActionError = z.discriminatedUnion('type', [ + z.strictObject({ type: z.literal('accountAlreadyExists'), accountId }), + z.strictObject({ type: z.literal('accountDoesNotExist'), accountId }), + z.strictObject({ + type: z.literal('createAccountOnlyByRegistrar'), + accountId, + registrarAccountId: accountId, + predecessorId: accountId, + }), + z.strictObject({ + type: z.literal('createAccountNotAllowed'), + accountId, + predecessorId: accountId, + }), + z.strictObject({ + type: z.literal('actorNoPermission'), + accountId, + actorId: accountId, + }), + z.strictObject({ + type: z.literal('deleteKeyDoesNotExist'), + accountId, + publicKey: z.string(), + }), + z.strictObject({ + type: z.literal('addKeyAlreadyExists'), + accountId, + publicKey: z.string(), + }), + z.strictObject({ type: z.literal('deleteAccountStaking'), accountId }), + z.strictObject({ + type: z.literal('lackBalanceForState'), + accountId, + amount: yoctoNear, + }), + z.strictObject({ type: z.literal('triesToUnstake'), accountId }), + z.strictObject({ + type: z.literal('triesToStake'), + accountId, + stake: yoctoNear, + locked: yoctoNear, + balance: yoctoNear, + }), + z.strictObject({ + type: z.literal('insufficientStake'), + accountId, + stake: yoctoNear, + minimumStake: yoctoNear, + }), + z.strictObject({ + type: z.literal('functionCallError'), + error: functionCallError, + }), + z.strictObject({ + type: z.literal('newReceiptValidationError'), + error: newReceiptValidationError, + }), + z.strictObject({ + type: z.literal('onlyImplicitAccountCreationAllowed'), + accountId, + }), + z.strictObject({ type: z.literal('deleteAccountWithLargeState'), accountId }), + unknownError, +]); +export type ReceiptActionError = z.infer; -export type ReceiptExecutionStatusError = - | { - type: 'action'; - error: ReceiptActionError; - } - | { - type: 'transaction'; - error: ReceiptTransactionError; - } - | UnknownError; +const receiptTransactionError = z.discriminatedUnion('type', [ + z.strictObject({ + type: z.literal('invalidAccessKeyError'), + }), + z.strictObject({ + type: z.literal('invalidSignerId'), + signerId: accountId, + }), + z.strictObject({ + type: z.literal('signerDoesNotExist'), + signerId: accountId, + }), + z.strictObject({ + type: z.literal('invalidNonce'), + transactionNonce: z.number(), + akNonce: z.number(), + }), + z.strictObject({ + type: z.literal('nonceTooLarge'), + transactionNonce: z.number(), + upperBound: z.number(), + }), + z.strictObject({ + type: z.literal('invalidReceiverId'), + receiverId: accountId, + }), + z.strictObject({ + type: z.literal('invalidSignature'), + }), + z.strictObject({ + type: z.literal('notEnoughBalance'), + signerId: accountId, + balance: yoctoNear, + cost: yoctoNear, + }), + z.strictObject({ + type: z.literal('lackBalanceForState'), + signerId: accountId, + amount: yoctoNear, + }), + z.strictObject({ + type: z.literal('costOverflow'), + }), + z.strictObject({ + type: z.literal('invalidChain'), + }), + z.strictObject({ + type: z.literal('expired'), + }), + z.strictObject({ + type: z.literal('actionsValidation'), + }), + z.strictObject({ + type: z.literal('transactionSizeExceeded'), + size: z.number(), + limit: z.number(), + }), + z.strictObject({ + type: z.literal('unknown'), + }), +]); +export type ReceiptTransactionError = z.infer; + +export const receiptExecutionStatusError = z.discriminatedUnion('type', [ + z.strictObject({ type: z.literal('action'), error: receiptActionError }), + z.strictObject({ + type: z.literal('transaction'), + error: receiptTransactionError, + }), + unknownError, +]); +export type ReceiptExecutionStatusError = z.infer< + typeof receiptExecutionStatusError +>; diff --git a/common/types/core/explorer.schema.ts b/common/types/core/explorer.schema.ts index d94972f46..cc3b42836 100644 --- a/common/types/core/explorer.schema.ts +++ b/common/types/core/explorer.schema.ts @@ -1,228 +1,355 @@ -import { Net } from '@pc/database/clients/core'; -import * as RPC from '../rpc'; +import { z } from 'zod'; +import * as Errors from './explorer-errors'; +import { + net, + accountId, + receiptId, + transactionHash, + yoctoNear, + blockHash, + transactionStatus, +} from './types'; -export namespace Old { - export type MapAction = { - kind: K extends string ? K : keyof K; - args: ActionArgs; - }; +const oldAction = z.discriminatedUnion('kind', [ + z.strictObject({ + kind: z.literal('CreateAccount'), + args: z.strictObject({}), + }), + z.strictObject({ + kind: z.literal('DeployContract'), + args: z.strictObject({ + code: z.string(), + }), + }), + z.strictObject({ + kind: z.literal('FunctionCall'), + args: z.strictObject({ + method_name: z.string(), + args: z.string(), + gas: z.number(), + deposit: yoctoNear, + }), + }), + z.strictObject({ + kind: z.literal('Transfer'), + args: z.strictObject({ + deposit: yoctoNear, + }), + }), + z.strictObject({ + kind: z.literal('Stake'), + args: z.strictObject({ + stake: yoctoNear, + public_key: z.string(), + }), + }), + z.strictObject({ + kind: z.literal('AddKey'), + args: z.strictObject({ + public_key: z.string(), + access_key: z.strictObject({ + nonce: z.number(), + permission: z.union([ + z.strictObject({ + FunctionCall: z.strictObject({ + allowance: yoctoNear.optional(), + receiver_id: accountId, + method_names: z.array(z.string()), + }), + }), + z.literal('FullAccess'), + ]), + }), + }), + }), + z.strictObject({ + kind: z.literal('DeleteKey'), + args: z.strictObject({ + public_key: z.string(), + }), + }), + z.strictObject({ + kind: z.literal('DeleteAccount'), + args: z.strictObject({ + beneficiary_id: accountId, + }), + }), +]); - export type Action = - | MapAction<'CreateAccount'> - | MapAction - | MapAction - | MapAction - | MapAction - | MapAction - | MapAction - | MapAction; - - export type ActionArgs = - K extends string ? {} : K[keyof K]; - - export type Transaction = { - hash: string; - signerId: string; - receiverId: string; - blockHash: string; - blockTimestamp: number; - transactionIndex: number; - actions: Action[]; - }; +const old = { + action: oldAction, + transaction: z.strictObject({ + hash: transactionHash, + signerId: accountId, + receiverId: accountId, + blockHash, + blockTimestamp: z.number(), + transactionIndex: z.number(), + actions: oldAction.array(), + }), +}; + +export namespace Old { + export type Action = z.infer; + export type Transaction = z.infer; } -import { ReceiptExecutionStatusError } from './explorer-errors'; -export * as Errors from './explorer-errors'; +export { Errors }; -export type ReceiptExecutionStatus = - | { - type: 'failure'; - error: ReceiptExecutionStatusError; - } - | { - type: 'successValue'; - value: string; - } - | { - type: 'successReceiptId'; - receiptId: string; - } - | { - type: 'unknown'; - }; +const primitiveAction = z.discriminatedUnion('kind', [ + z.strictObject({ + kind: z.literal('createAccount'), + args: z.strictObject({}), + }), + z.strictObject({ + kind: z.literal('deployContract'), + args: z.strictObject({ + code: z.string(), + }), + }), + z.strictObject({ + kind: z.literal('functionCall'), + args: z.strictObject({ + methodName: z.string(), + args: z.string(), + gas: z.number(), + deposit: yoctoNear, + }), + }), + z.strictObject({ + kind: z.literal('transfer'), + args: z.strictObject({ + deposit: yoctoNear, + }), + }), + z.strictObject({ + kind: z.literal('stake'), + args: z.strictObject({ + stake: yoctoNear, + publicKey: z.string(), + }), + }), + z.strictObject({ + kind: z.literal('addKey'), + args: z.strictObject({ + publicKey: z.string(), + accessKey: z.strictObject({ + nonce: z.number(), + permission: z.discriminatedUnion('type', [ + z.strictObject({ + type: z.literal('fullAccess'), + }), + z.strictObject({ + type: z.literal('functionCall'), + contractId: accountId, + methodNames: z.array(z.string()), + }), + ]), + }), + }), + }), + z.strictObject({ + kind: z.literal('deleteKey'), + args: z.strictObject({ + publicKey: z.string(), + }), + }), + z.strictObject({ + kind: z.literal('deleteAccount'), + args: z.strictObject({ + beneficiaryId: accountId, + }), + }), +]); -export type Action = - | { - kind: 'createAccount'; - args: Record; - } - | { - kind: 'deployContract'; - args: { - code: string; - }; - } - | { - kind: 'functionCall'; - args: { - methodName: string; - args: string; - gas: number; - deposit: string; - }; - } - | { - kind: 'transfer'; - args: { - deposit: string; - }; - } - | { - kind: 'stake'; - args: { - stake: string; - publicKey: string; - }; - } - | { - kind: 'addKey'; - args: { - publicKey: string; - accessKey: { - nonce: number; - permission: - | { - type: 'fullAccess'; - } - | { - type: 'functionCall'; - contractId: string; - methodNames: string[]; - }; - }; - }; - } - | { - kind: 'deleteKey'; - args: { - publicKey: string; - }; - } - | { - kind: 'deleteAccount'; - args: { - beneficiaryId: string; - }; - }; +export type Action = z.infer; -export type ActivityConnection = { - transactionHash: string; - receiptId?: string; - sender: string; - receiver: string; -}; +const validatorRewardAction = z.strictObject({ + kind: z.literal('validatorReward'), + blockHash, +}); -type AccountBatchAction = { - kind: 'batch'; - actions: AccountActivityAction[]; -}; +export type AccountActivityAction = + | z.infer + | z.infer + | { + kind: 'batch'; + actions: AccountActivityAction[]; + }; -type AccountValidatorRewardAction = { - kind: 'validatorReward'; - blockHash: string; -}; +const accountActivityAction: z.ZodType = z.lazy(() => + z.union([ + // see https://github.com/colinhacks/zod/issues/1500 + primitiveAction as z.ZodType>, + validatorRewardAction as unknown as z.ZodType< + z.infer + >, + z.strictObject({ + kind: z.literal('batch'), + actions: z.array(accountActivityAction), + }), + ]), +); -export type AccountActivityAction = - | Action - | AccountValidatorRewardAction - | AccountBatchAction; +const activityConnection = z.strictObject({ + transactionHash, + receiptId: receiptId.optional(), + sender: accountId, + receiver: accountId, +}); +export type ActivityConnection = z.infer; -export type AccountActivityWithConnection = AccountActivityAction & - ActivityConnection; +const accountActivityWithConnection = + accountActivityAction.and(activityConnection); +export type AccountActivityWithConnection = z.infer< + typeof accountActivityWithConnection +>; -export type ActivityConnectionActions = { - parentAction?: AccountActivityWithConnection; - childrenActions?: AccountActivityWithConnection[]; -}; +const activityConnectionActions = z.strictObject({ + parentAction: accountActivityWithConnection.optional(), + childrenActions: z.array(accountActivityWithConnection).optional(), +}); -export type ActivityActionItemAction = AccountActivityAction & - ActivityConnectionActions & - Omit; +const receiptExecutionStatus = z.discriminatedUnion('type', [ + z.strictObject({ + type: z.literal('failure'), + error: Errors.receiptExecutionStatusError, + }), + z.strictObject({ type: z.literal('successValue'), value: z.string() }), + z.strictObject({ type: z.literal('successReceiptId'), receiptId }), + Errors.unknownError, +]); +export type ReceiptExecutionStatus = z.infer; -export type ActivityActionItem = { - involvedAccountId: string | null; - timestamp: number; - direction: 'inbound' | 'outbound'; - deltaAmount: string; - action: ActivityActionItemAction; -}; +const nestedReceiptOutcomeBase = z.strictObject({ + block: z.strictObject({ + hash: blockHash, + height: z.number(), + timestamp: z.number(), + }), + tokensBurnt: yoctoNear, + gasBurnt: z.number(), + status: receiptExecutionStatus, + logs: z.array(z.string()), +}); -export type AccountActivity = { - items: ActivityActionItem[]; - cursor?: { - blockTimestamp: string; - shardId: number; - indexInChunk: number; - }; -}; +const nestedReceiptWithOutcomeBase = z.strictObject({ + id: receiptId, + predecessorId: accountId, + receiverId: accountId, + actions: z.array(primitiveAction), +}); -export type TransactionStatus = 'unknown' | 'failure' | 'success'; - -export type NestedReceiptWithOutcome = { - id: string; - predecessorId: string; - receiverId: string; - actions: Action[]; - outcome: { - block: { - hash: string; - height: number; - timestamp: number; - }; - tokensBurnt: string; - gasBurnt: number; - status: ReceiptExecutionStatus; - logs: string[]; +export type NestedReceiptWithOutcome = z.infer< + typeof nestedReceiptWithOutcomeBase +> & { + outcome: z.infer & { nestedReceipts: NestedReceiptWithOutcome[]; }; }; -export type Transaction = { - hash: string; - timestamp: number; - signerId: string; - receiverId: string; - fee: string; - amount: string; - status: TransactionStatus; - receipt: NestedReceiptWithOutcome; +const nestedReceiptWithOutcome: z.ZodType = z.lazy( + () => + // see https://github.com/colinhacks/zod/issues/1500 + ( + nestedReceiptWithOutcomeBase as unknown as z.ZodType< + z.infer + > + ).and( + z.strictObject({ + outcome: ( + nestedReceiptOutcomeBase as unknown as z.ZodType< + z.infer + > + ).and( + z.strictObject({ + nestedReceipts: z.array(nestedReceiptWithOutcome), + }), + ), + }), + ), +); + +const activityActionItemAction = accountActivityAction + .and(activityConnectionActions) + .and( + activityConnection.omit({ + sender: true, + receiver: true, + }), + ); + +export type ActivityActionItemAction = z.infer; + +const activityActionItem = z.strictObject({ + involvedAccountId: accountId.or(z.null()), + timestamp: z.number(), + direction: z.enum(['inbound', 'outbound']), + deltaAmount: yoctoNear, + action: activityActionItemAction, +}); +export type ActivityActionItem = z.infer; + +const transaction = z.strictObject({ + hash: transactionHash, + timestamp: z.number(), + signerId: accountId, + receiverId: accountId, + fee: yoctoNear, + amount: yoctoNear, + status: transactionStatus, + receipt: nestedReceiptWithOutcome, +}); +export type Transaction = z.infer; + +const accountActivity = z.strictObject({ + items: z.array(activityActionItem), + cursor: z + .strictObject({ + blockTimestamp: z.string(), + shardId: z.number(), + indexInChunk: z.number(), + }) + .optional(), +}); +export type AccountActivity = z.infer; + +export const query = { + inputs: { + activity: z.strictObject({ + net, + contractId: accountId, + }), + balanceChanges: z.strictObject({ + net, + receiptId, + accountIds: z.array(accountId), + }), + transaction: z.strictObject({ + net, + hash: transactionHash, + }), + getTransactions: z.strictObject({ + net, + contracts: z.array(accountId), + }), + }, + + outputs: { + activity: accountActivity, + balanceChanges: z.array(z.string().or(z.null())), + transaction, + getTransactions: old.transaction + .and(z.strictObject({ sourceContract: accountId })) + .array(), + }, + + errors: { + activity: z.unknown(), + balanceChanges: z.unknown(), + transaction: z.unknown(), + getTransactions: z.unknown(), + }, }; -export namespace Query { - export namespace Inputs { - export type Activity = { contractId: string; net: Net }; - export type BalanceChanges = { - net: Net; - receiptId: string; - accountIds: string[]; - }; - export type GetTransaction = { net: Net; hash: string }; - export type GetTransactions = { net: Net; contracts: string[] }; - } - - export namespace Outputs { - export type Activity = { items: ActivityActionItem[] }; - export type BalanceChanges = (string | null)[]; - export type GetTransaction = Transaction; - export type GetTransactions = (Old.Transaction & { - sourceContract: string; - })[]; - } - - export namespace Errors { - export type Activity = unknown; - export type BalanceChanges = unknown; - export type GetTransaction = unknown; - export type GetTransactions = unknown; - } -} +export type { TransactionStatus } from './types'; diff --git a/common/types/core/projects.schema.ts b/common/types/core/projects.schema.ts index 9db559d3b..f69c0b0da 100644 --- a/common/types/core/projects.schema.ts +++ b/common/types/core/projects.schema.ts @@ -1,98 +1,161 @@ +import { z } from 'zod'; + import { - Project, - Org, - Contract, - ProjectTutorial, - ApiKey, - Environment, -} from '@pc/database/clients/core'; + orgSlug, + projectSlug, + environmentId, + contractSlug, + apiKeySlug, + accountId, + projectTutorial, + projectName, + project, + org, + contract, + environment, + apiKey, +} from './types'; -export namespace Query { - export namespace Inputs { - export type GetDetails = { slug: string }; - export type GetContracts = { project: string; environment: number }; - export type GetContract = { slug: string }; - export type List = void; - export type GetEnvironments = { project: string }; - export type GetKeys = { project: string }; - } +export const query = { + inputs: { + getDetails: z.strictObject({ + slug: projectSlug, + }), + getContracts: z.strictObject({ + project: projectSlug, + environment: environmentId, + }), + getContract: z.strictObject({ + slug: contractSlug, + }), + list: z.void(), + getEnvironments: z.strictObject({ + project: projectSlug, + }), + getKeys: z.strictObject({ + project: projectSlug, + }), + }, - export namespace Outputs { - export type GetDetails = Pick & { - org: Pick; - }; - export type GetContracts = Pick[]; - export type GetContract = Pick; - export type List = (Pick< - Project, - 'id' | 'name' | 'slug' | 'tutorial' | 'active' - > & { - org: Pick & { - isPersonal: boolean; - }; - })[]; - export type GetEnvironments = Pick[]; - export type GetKeys = (Pick & { - keySlug: ApiKey['slug']; - key: string; - })[]; - } + outputs: { + getDetails: project.pick({ name: true, slug: true, tutorial: true }).and( + z.strictObject({ + org: org.pick({ name: true, slug: true, personalForUserId: true }), + }), + ), + getContracts: contract + .pick({ slug: true, address: true, net: true }) + .array(), + getContract: contract.pick({ slug: true, address: true, net: true }), + list: z.array( + project + .pick({ + id: true, + name: true, + slug: true, + tutorial: true, + active: true, + }) + .and( + z.strictObject({ + org: org.pick({ slug: true, name: true }).and( + z.strictObject({ + isPersonal: z.boolean(), + }), + ), + }), + ), + ), + getEnvironments: z.array( + environment.pick({ subId: true, net: true, name: true }), + ), + getKeys: z.array( + apiKey.pick({ description: true }).and( + z.strictObject({ + keySlug: apiKey.shape.slug, + key: z.string(), + }), + ), + ), + }, - export namespace Errors { - export type GetDetails = unknown; - export type GetContracts = unknown; - export type GetContract = unknown; - export type List = unknown; - export type GetEnvironments = unknown; - export type GetKeys = unknown; - } -} + errors: { + getDetails: z.unknown(), + getContracts: z.unknown(), + getContract: z.unknown(), + list: z.unknown(), + getEnvironments: z.unknown(), + getKeys: z.unknown(), + }, +}; -export namespace Mutation { - export namespace Inputs { - export type Create = { - org?: string; - name: string; - tutorial?: ProjectTutorial; - }; - export type EjectTutorial = { slug: string }; - export type Delete = { slug: string }; - export type AddContract = { - project: string; - environment: number; - address: string; - }; - export type RemoveContract = { slug: string }; - export type RotateKey = { slug: string }; - export type GenerateKey = { project: string; description: string }; - export type DeleteKey = { slug: string }; - } +export const mutation = { + inputs: { + create: z.strictObject({ + name: projectName, + org: orgSlug.optional(), + tutorial: projectTutorial.optional(), + }), + ejectTutorial: z.strictObject({ + slug: projectSlug, + }), + delete: z.strictObject({ + slug: projectSlug, + }), + addContract: z.strictObject({ + project: projectSlug, + environment: environmentId, + address: accountId, + }), + removeContract: z.strictObject({ + slug: contractSlug, + }), + rotateKey: z.strictObject({ + slug: apiKeySlug, + }), + generateKey: z.strictObject({ + project: projectSlug, + description: z.string(), + }), + deleteKey: z.strictObject({ + slug: apiKeySlug, + }), + }, - export namespace Outputs { - export type Create = Pick; - export type EjectTutorial = void; - export type Delete = void; - export type AddContract = Pick; - export type RemoveContract = void; - export type RotateKey = Pick & { - keySlug: ApiKey['slug']; - key: string; - }; - export type GenerateKey = Pick & { - keySlug: ApiKey['slug']; - key: string; - }; - export type DeleteKey = void; - } + outputs: { + create: project.pick({ name: true, slug: true }), + ejectTutorial: z.void(), + delete: z.void(), + addContract: contract.pick({ + id: true, + slug: true, + address: true, + net: true, + }), + removeContract: z.void(), + rotateKey: apiKey.pick({ description: true }).and( + z.strictObject({ + keySlug: apiKey.shape.slug, + key: z.string(), + }), + ), + generateKey: apiKey.pick({ description: true }).and( + z.strictObject({ + keySlug: apiKey.shape.slug, + key: z.string(), + }), + ), + deleteKey: z.void(), + }, - export namespace Errors { - export type Create = unknown; - export type EjectTutorial = unknown; - export type Delete = unknown; - export type AddContract = unknown; - export type RemoveContract = unknown; - export type RotateKey = unknown; - export type GenerateKey = unknown; - export type DeleteKey = unknown; - } -} + errors: { + create: z.unknown(), + ejectTutorial: z.unknown(), + delete: z.unknown(), + addContract: z.unknown(), + removeContract: z.unknown(), + rotateKey: z.unknown(), + generateKey: z.unknown(), + deleteKey: z.unknown(), + }, +}; diff --git a/common/types/core/types.ts b/common/types/core/types.ts new file mode 100644 index 000000000..cbf6f59a9 --- /dev/null +++ b/common/types/core/types.ts @@ -0,0 +1,153 @@ +import { z } from 'zod'; +import { + OrgRole, + ProjectTutorial, + Net, + UserActionType, +} from '@pc/database/clients/core'; +import { flavored, Flavored } from '../utils'; + +export const userUid = z.string().refine>(flavored); +export type UserUid = z.infer; +export const projectSlug = z.string().refine>(flavored); +export type ProjectSlug = z.infer; +export const orgSlug = z.string().refine>(flavored); +export type OrgSlug = z.infer; +export const contractSlug = z + .string() + .refine>(flavored); +export type ContractSlug = z.infer; +export const apiKeySlug = z.string().refine>(flavored); +export type ApiKeySlug = z.infer; +// TESTNET = 1, MAINNET = 2 +export const environmentId = z.number(); +export type EnvironmentId = z.infer; + +// We're defining types explicitly here to double-check schema type and DB type match. +export const net: z.ZodType = z.enum(['MAINNET', 'TESTNET']); +export { Net }; + +export const userActionType: z.ZodType = + z.literal('ROTATE_API_KEY'); +export { UserActionType }; + +export const user = z.strictObject({ + id: z.number(), + uid: userUid, + email: z.string().email(), + active: z.boolean(), + createdAt: z.date().or(z.null()), + updatedAt: z.date().or(z.null()), +}); + +export const projectTutorial: z.ZodType = z.enum([ + 'NFT_MARKET', + 'CROSSWORD', +]); +export { ProjectTutorial }; +export const projectName = z.string().max(50); +export const project = z.strictObject({ + id: z.number(), + name: projectName, + slug: projectSlug, + active: z.boolean(), + tutorial: projectTutorial.or(z.null()), + createdAt: z.date().or(z.null()), + createdBy: z.number().or(z.null()), + updatedAt: z.date().or(z.null()), + updatedBy: z.number().or(z.null()), + orgSlug, +}); + +export const orgRole: z.ZodType = z.enum(['ADMIN', 'COLLABORATOR']); +export { OrgRole }; +export const orgName = z.string(); +export const org = z.strictObject({ + slug: orgSlug, + name: orgName, + personalForUserId: z.number().or(z.null()), + active: z.boolean(), + emsId: z.string(), + createdAt: z.date().or(z.null()), + createdBy: z.number().or(z.null()), + updatedAt: z.date().or(z.null()), + updatedBy: z.number().or(z.null()), +}); + +export const orgMember = z.strictObject({ + orgSlug, + userId: z.number(), + role: orgRole, + createdAt: z.date().or(z.null()), + createdBy: z.number().or(z.null()), + updatedAt: z.date().or(z.null()), + updatedBy: z.number().or(z.null()), +}); + +export const orgInvite = z.strictObject({ + id: z.number(), + orgSlug, + email: z.string().email(), + role: orgRole, + token: z.string(), + tokenExpiresAt: z.date(), + createdAt: z.date().or(z.null()), + createdBy: z.number().or(z.null()), + updatedAt: z.date().or(z.null()), + updatedBy: z.number().or(z.null()), +}); + +export const contract = z.strictObject({ + id: z.number(), + slug: contractSlug, + environmentId: environmentId, + address: z.string(), + net, + active: z.boolean(), + createdAt: z.date().or(z.null()), + createdBy: z.number().or(z.null()), + updatedAt: z.date().or(z.null()), + updatedBy: z.number().or(z.null()), +}); + +export const environment = z.strictObject({ + id: z.number(), + name: z.string(), + projectId: z.number(), + net, + subId: environmentId, + active: z.boolean(), + createdAt: z.date().or(z.null()), + createdBy: z.number().or(z.null()), + updatedAt: z.date().or(z.null()), + updatedBy: z.number().or(z.null()), +}); + +export const apiKey = z.strictObject({ + id: z.number(), + slug: apiKeySlug, + projectSlug, + orgSlug, + description: z.string(), + active: z.boolean(), + createdAt: z.date().or(z.null()), + createdBy: z.number().or(z.null()), + updatedAt: z.date().or(z.null()), + updatedBy: z.number().or(z.null()), +}); + +// Explorer types +export const accountId = z.string().refine>(flavored); +export type AccountId = z.infer; +export const receiptId = z.string().refine>(flavored); +export type ReceiptId = z.infer; +export const transactionHash = z + .string() + .refine>(flavored); +export type TransactionHash = z.infer; +export const blockHash = z.string().refine>(flavored); +export type BlockHash = z.infer; +export const yoctoNear = z.string().refine>(flavored); +export type YoctoNear = z.infer; +export const transactionStatus = z.enum(['unknown', 'failure', 'success']); +export type TransactionStatus = z.infer; diff --git a/common/types/core/users.schema.ts b/common/types/core/users.schema.ts index 7037d9661..6b5711bc1 100644 --- a/common/types/core/users.schema.ts +++ b/common/types/core/users.schema.ts @@ -1,106 +1,129 @@ +import { z } from 'zod'; import { - Org, - OrgRole, - User, - OrgMember, - OrgInvite, -} from '@pc/database/clients/core'; + orgRole, + orgName, + orgSlug, + userUid, + org, + orgInvite, + user, + orgMember, +} from './types'; -export namespace Query { - export namespace Inputs { - export type GetAccountDetails = void; - export type ListOrgsWithOnlyAdmin = void; - export type ListOrgMembers = { org: Org['slug'] }; - export type ListOrgs = void; - } +export const query = { + inputs: { + getAccountDetails: z.void(), + listOrgsWithOnlyAdmin: z.void(), + listOrgMembers: z.strictObject({ + org: orgSlug, + }), + listOrgs: z.void(), + }, - export namespace Outputs { - export type GetAccountDetails = { - uid?: string; - email?: string; - name?: string; - photoUrl?: string; - }; - export type ListOrgsWithOnlyAdmin = Pick[]; - export type ListOrgMembers = (Pick & - ( - | { - isInvite: true; - user: { - uid: null; - email: OrgInvite['email']; - }; - } - | { - isInvite: false; - user: Pick; - } - ))[]; - export type ListOrgs = (Pick & { - isPersonal: boolean; - })[]; - } + outputs: { + getAccountDetails: z.strictObject({ + uid: z.string().optional(), + email: z.string().optional(), + name: z.string().optional(), + photoUrl: z.string().optional(), + }), + listOrgsWithOnlyAdmin: org.pick({ name: true, slug: true }).array(), + listOrgMembers: z.array( + orgInvite.pick({ orgSlug: true, role: true }).and( + z.union([ + z.strictObject({ + isInvite: z.literal(true), + user: z.strictObject({ + uid: z.null(), + email: orgInvite.shape.email, + }), + }), + z.strictObject({ + isInvite: z.literal(false), + user: user.pick({ email: true, uid: true }), + }), + ]), + ), + ), + listOrgs: z.array( + org.pick({ slug: true, name: true }).and( + z.strictObject({ + isPersonal: z.boolean(), + }), + ), + ), + }, - export namespace Errors { - export type GetAccountDetails = unknown; - export type ListOrgsWithOnlyAdmin = unknown; - export type ListOrgMembers = unknown; - export type ListOrgs = unknown; - } -} + errors: { + getAccountDetails: z.unknown(), + listOrgsWithOnlyAdmin: z.unknown(), + listOrgMembers: z.unknown(), + listOrgs: z.unknown(), + }, +}; -export namespace Mutation { - export namespace Inputs { - export type DeleteAccount = void; - export type CreateOrg = { name: string }; - export type InviteToOrg = { - org: Org['slug']; - email: string; - role: OrgRole; - }; - export type AcceptOrgInvite = { token: string }; - export type DeleteOrg = { org: Org['slug'] }; - export type ChangeOrgRole = { - org: Org['slug']; - role: OrgRole; - user: User['uid']; - }; - export type RemoveFromOrg = { - org: Org['slug']; - user: User['uid']; - }; - export type RemoveOrgInvite = { - org: Org['slug']; - email: string; - }; - export type ResetPassword = { email: string }; - } +export const mutation = { + inputs: { + deleteAccount: z.void(), + createOrg: z.strictObject({ + name: orgName, + }), + inviteToOrg: z.strictObject({ + org: orgSlug, + email: z.string().email(), + role: orgRole, + }), + acceptOrgInvite: z.strictObject({ + token: z.string(), + }), + deleteOrg: z.strictObject({ + org: orgSlug, + }), + changeOrgRole: z.strictObject({ + org: orgSlug, + role: orgRole, + user: userUid, + }), + removeFromOrg: z.strictObject({ + org: orgSlug, + user: userUid, + }), + removeOrgInvite: z.strictObject({ + org: orgSlug, + email: z.string().email(), + }), + resetPassword: z.strictObject({ + email: z.string().email(), + }), + }, - export namespace Outputs { - export type DeleteAccount = void; - export type CreateOrg = Pick & { - isPersonal: false; - }; - export type InviteToOrg = void; - export type AcceptOrgInvite = Pick; - export type DeleteOrg = void; - export type ChangeOrgRole = Pick & { - user: Pick; - }; - export type RemoveFromOrg = void; - export type RemoveOrgInvite = void; - export type ResetPassword = void; - } + outputs: { + deleteAccount: z.void(), + createOrg: org + .pick({ name: true, slug: true }) + .and(z.strictObject({ isPersonal: z.literal(false) })), + inviteToOrg: z.void(), + acceptOrgInvite: org.pick({ name: true, slug: true }), + deleteOrg: z.void(), + changeOrgRole: orgMember.pick({ orgSlug: true, role: true }).and( + z.strictObject({ + user: user.pick({ uid: true, email: true }), + }), + ), + removeFromOrg: z.void(), + removeOrgInvite: z.void(), + resetPassword: z.void(), + }, - export namespace Errors { - export type DeleteAccount = unknown; - export type CreateOrg = unknown; - export type InviteToOrg = unknown; - export type AcceptOrgInvite = unknown; - export type DeleteOrg = unknown; - export type ChangeOrgRole = unknown; - export type RemoveFromOrg = unknown; - export type RemoveOrgInvite = unknown; - export type ResetPassword = unknown; - } -} + errors: { + deleteAccount: z.unknown(), + createOrg: z.unknown(), + inviteToOrg: z.unknown(), + acceptOrgInvite: z.unknown(), + deleteOrg: z.unknown(), + changeOrgRole: z.unknown(), + removeFromOrg: z.unknown(), + removeOrgInvite: z.unknown(), + resetPassword: z.unknown(), + }, +}; diff --git a/common/types/rpcstats/rpcstats.schema.ts b/common/types/rpcstats/rpcstats.schema.ts index 67cb57380..76ef7434a 100644 --- a/common/types/rpcstats/rpcstats.schema.ts +++ b/common/types/rpcstats/rpcstats.schema.ts @@ -1,72 +1,69 @@ -import { Net } from '@pc/database/clients/core'; +import { z } from 'zod'; +import { environmentId, net, projectSlug } from '../core/types'; +import { stringifiedDate } from '../schemas'; -export type DateTimeResolution = - | 'FIFTEEN_SECONDS' - | 'ONE_MINUTE' - | 'ONE_HOUR' - | 'ONE_DAY'; +export const dateTimeResolution = z.enum([ + 'FIFTEEN_SECONDS', + 'ONE_MINUTE', + 'ONE_HOUR', + 'ONE_DAY', +]); +export type DateTimeResolution = z.infer; -export type TimeRangeValue = - | '15_MINS' - | '1_HRS' - | '24_HRS' - | '7_DAYS' - | '30_DAYS'; +export const timeRangeValue = z.enum([ + '15_MINS', + '1_HRS', + '24_HRS', + '7_DAYS', + '30_DAYS', +]); +export type TimeRangeValue = z.infer; -export type MetricGroupBy = 'date' | 'endpoint'; +const metrics = z.strictObject({ + apiKeyIdentifier: z.string(), + endpointGroup: z.string().optional(), + endpointMethod: z.string(), + network: net, + windowStart: stringifiedDate.optional(), + windowEnd: stringifiedDate.optional(), + successCount: z.number(), + errorCount: z.number(), + minLatency: z.number(), + maxLatency: z.number(), + meanLatency: z.number(), +}); +export type Metrics = z.infer; -export type BaseEndpointMetric = { - endpointMethod: string; - successCount: number; - errorCount: number; - totalCount: number; -}; +export const query = { + inputs: { + endpointMetrics: z.strictObject({ + projectSlug, + environmentSubId: environmentId, + startDateTime: stringifiedDate, + endDateTime: stringifiedDate, + skip: z.number().int().min(0).optional(), + take: z.number().int().min(0).max(100).optional(), + pagingDateTime: stringifiedDate.optional(), + filter: z.discriminatedUnion('type', [ + z.strictObject({ + type: z.literal('date'), + dateTimeResolution, + }), + z.strictObject({ + type: z.literal('endpoint'), + }), + ]), + }), + }, -export type Metrics = { - apiKeyIdentifier: string; - endpointGroup?: string; - endpointMethod: string; - network: Net; - windowStart?: string; - windowEnd?: string; - successCount: number; - errorCount: number; - minLatency: number; - maxLatency: number; - meanLatency: number; -}; + outputs: { + endpointMetrics: z.strictObject({ + count: z.number(), + page: metrics.array(), + }), + }, -export type MetricsPage = { - count: number; - page: Metrics[]; + errors: { + endpointMetrics: z.unknown(), + }, }; - -export namespace Query { - export namespace Inputs { - export type EndpointMetrics = { - projectSlug: string; - environmentSubId: number; - startDateTime: string; - endDateTime: string; - skip?: number; - take?: number; - pagingDateTime?: string; - filter: - | { - type: 'date'; - dateTimeResolution: DateTimeResolution; - } - | { - type: 'endpoint'; - }; - }; - } - - export namespace Outputs { - export type EndpointMetrics = MetricsPage; - } - - export namespace Errors { - export type EndpointMetrics = unknown; - } -} diff --git a/common/types/schemas.ts b/common/types/schemas.ts new file mode 100644 index 000000000..af5099c91 --- /dev/null +++ b/common/types/schemas.ts @@ -0,0 +1,13 @@ +import { z } from 'zod'; +import { DateTime } from 'luxon'; + +export const stringifiedDate = z + .string() + .refine((input) => DateTime.fromISO(input).isValid, 'Date is invalid'); + +const literal = z.union([z.string(), z.number(), z.boolean(), z.null()]); +type Literal = z.infer; +type Json = Literal | { [key: string]: Json } | Json[]; +export const json: z.ZodType = z.lazy(() => + z.union([literal, z.array(json), z.record(json)]), +); diff --git a/common/types/utils.ts b/common/types/utils.ts new file mode 100644 index 000000000..00d510499 --- /dev/null +++ b/common/types/utils.ts @@ -0,0 +1,10 @@ +export type Flavored< + T extends string, + R extends string | number = string, +> = R & { + __flavor?: T; +}; + +export const flavored = ( + x: R, +): x is Flavored => true; diff --git a/package-lock.json b/package-lock.json index 7e6eab34b..29064229d 100644 --- a/package-lock.json +++ b/package-lock.json @@ -42,7 +42,6 @@ "express": "^4.18.1", "firebase-admin": "^10.0.0", "form-data": "^4.0.0", - "joi": "^17.4.2", "kysely": "^0.20.0", "luxon": "^3.0.1", "mailgun.js": "^7.0.2", @@ -64,7 +63,6 @@ "@nestjs/testing": "^8.0.0", "@types/express": "^4.17.13", "@types/jest": "^27.0.1", - "@types/joi": "^17.2.3", "@types/luxon": "^3.0.0", "@types/node": "^16.0.0", "@types/pg": "^8.6.5", @@ -87,7 +85,10 @@ "common": { "name": "@pc/common", "version": "1.0.0", - "license": "ISC" + "license": "ISC", + "dependencies": { + "zod": "^3.19.1" + } }, "database": { "name": "@pc/database", @@ -2949,17 +2950,6 @@ "node": ">=6" } }, - "node_modules/@hapi/hoek": { - "version": "9.3.0", - "license": "BSD-3-Clause" - }, - "node_modules/@hapi/topo": { - "version": "5.1.0", - "license": "BSD-3-Clause", - "dependencies": { - "@hapi/hoek": "^9.0.0" - } - }, "node_modules/@humanwhocodes/config-array": { "version": "0.10.5", "dev": true, @@ -5289,21 +5279,6 @@ "node": ">= 8" } }, - "node_modules/@sideway/address": { - "version": "4.1.4", - "license": "BSD-3-Clause", - "dependencies": { - "@hapi/hoek": "^9.0.0" - } - }, - "node_modules/@sideway/formula": { - "version": "3.0.0", - "license": "BSD-3-Clause" - }, - "node_modules/@sideway/pinpoint": { - "version": "2.0.0", - "license": "BSD-3-Clause" - }, "node_modules/@sinonjs/commons": { "version": "1.8.3", "dev": true, @@ -7051,14 +7026,6 @@ "pretty-format": "^27.0.0" } }, - "node_modules/@types/joi": { - "version": "17.2.3", - "dev": true, - "license": "MIT", - "dependencies": { - "joi": "*" - } - }, "node_modules/@types/js-cookie": { "version": "2.2.7", "license": "MIT" @@ -13824,17 +13791,6 @@ "url": "https://github.com/chalk/supports-color?sponsor=1" } }, - "node_modules/joi": { - "version": "17.6.1", - "license": "BSD-3-Clause", - "dependencies": { - "@hapi/hoek": "^9.0.0", - "@hapi/topo": "^5.0.0", - "@sideway/address": "^4.1.3", - "@sideway/formula": "^3.0.0", - "@sideway/pinpoint": "^2.0.0" - } - }, "node_modules/join-component": { "version": "1.1.0", "license": "MIT" @@ -21726,6 +21682,14 @@ "url": "https://github.com/sponsors/sindresorhus" } }, + "node_modules/zod": { + "version": "3.19.1", + "resolved": "https://registry.npmjs.org/zod/-/zod-3.19.1.tgz", + "integrity": "sha512-LYjZsEDhCdYET9ikFu6dVPGp2YH9DegXjdJToSzD9rO6fy4qiRYFoyEYwps88OseJlPyl2NOe2iJuhEhL7IpEA==", + "funding": { + "url": "https://github.com/sponsors/colinhacks" + } + }, "node_modules/zustand": { "version": "4.1.1", "license": "MIT", @@ -23482,15 +23446,6 @@ "yargs": "^16.2.0" } }, - "@hapi/hoek": { - "version": "9.3.0" - }, - "@hapi/topo": { - "version": "5.1.0", - "requires": { - "@hapi/hoek": "^9.0.0" - } - }, "@humanwhocodes/config-array": { "version": "0.10.5", "dev": true, @@ -24305,7 +24260,10 @@ "version": "1.0.0" }, "@pc/common": { - "version": "file:common" + "version": "file:common", + "requires": { + "zod": "^3.19.1" + } }, "@pc/database": { "version": "file:database", @@ -25073,18 +25031,6 @@ "@sentry/cli": "^1.74.4" } }, - "@sideway/address": { - "version": "4.1.4", - "requires": { - "@hapi/hoek": "^9.0.0" - } - }, - "@sideway/formula": { - "version": "3.0.0" - }, - "@sideway/pinpoint": { - "version": "2.0.0" - }, "@sinonjs/commons": { "version": "1.8.3", "dev": true, @@ -26180,13 +26126,6 @@ "pretty-format": "^27.0.0" } }, - "@types/joi": { - "version": "17.2.3", - "dev": true, - "requires": { - "joi": "*" - } - }, "@types/js-cookie": { "version": "2.2.7" }, @@ -27020,7 +26959,6 @@ "@prisma/client": "^3.4.2", "@types/express": "^4.17.13", "@types/jest": "^27.0.1", - "@types/joi": "^17.2.3", "@types/json-schema": "^7.0.11", "@types/luxon": "^3.0.0", "@types/node": "^16.0.0", @@ -27038,7 +26976,6 @@ "firebase-admin": "^10.0.0", "form-data": "^4.0.0", "jest": "^27.0.6", - "joi": "^17.4.2", "kysely": "^0.20.0", "luxon": "^3.0.1", "mailgun.js": "^7.0.2", @@ -30734,16 +30671,6 @@ } } }, - "joi": { - "version": "17.6.1", - "requires": { - "@hapi/hoek": "^9.0.0", - "@hapi/topo": "^5.0.0", - "@sideway/address": "^4.1.3", - "@sideway/formula": "^3.0.0", - "@sideway/pinpoint": "^2.0.0" - } - }, "join-component": { "version": "1.1.0" }, @@ -35484,6 +35411,11 @@ "version": "0.1.0", "devOptional": true }, + "zod": { + "version": "3.19.1", + "resolved": "https://registry.npmjs.org/zod/-/zod-3.19.1.tgz", + "integrity": "sha512-LYjZsEDhCdYET9ikFu6dVPGp2YH9DegXjdJToSzD9rO6fy4qiRYFoyEYwps88OseJlPyl2NOe2iJuhEhL7IpEA==" + }, "zustand": { "version": "4.1.1", "requires": {