diff --git a/backend/src/modules/deploys/deploy-error.ts b/backend/src/modules/deploys/deploy-error.ts index ac669ba9..5e4f6f5c 100644 --- a/backend/src/modules/deploys/deploy-error.ts +++ b/backend/src/modules/deploys/deploy-error.ts @@ -3,4 +3,5 @@ export enum DeployError { NAME_CONFLICT = 'NAME_CONFLICT', BAD_REPO = 'BAD_REPO', + TRANSFER_INITIATED = 'TRANSFER_INITIATED', } diff --git a/backend/src/modules/deploys/deploys.controller.ts b/backend/src/modules/deploys/deploys.controller.ts index be0bc365..94d4e117 100644 --- a/backend/src/modules/deploys/deploys.controller.ts +++ b/backend/src/modules/deploys/deploys.controller.ts @@ -11,6 +11,7 @@ import { UseInterceptors, UsePipes, ConflictException, + NotFoundException, } from '@nestjs/common'; import { DeploysService } from './deploys.service'; import { ZodValidationPipe } from 'src/pipes/ZodValidationPipe'; @@ -30,19 +31,21 @@ import { Request as ExpressRequest } from 'express'; export class DeploysController { constructor(private readonly deploysService: DeploysService) {} - @Post('addDeploy') + @Post('addConsoleDeployProject') @UseGuards(BearerAuthGuard) - @UsePipes(new ZodValidationPipe(Deploys.mutation.inputs.addDeploy)) - async addDeploy( + @UsePipes( + new ZodValidationPipe(Deploys.mutation.inputs.addConsoleDeployProject), + ) + async addConsoleDeployProject( @Req() req: ExpressRequest, @Body() { githubRepoFullName, projectName, - }: z.infer, - ): Promise> { + }: z.infer, + ): Promise> { + // called from Console frontend to initialize a new repo for deployment try { - // called from Console frontend to initialize a new repo for deployment const repository = await this.deploysService.createDeployProject({ user: req.user as User, // TODO change to UserDetails from auth service githubRepoFullName, @@ -56,6 +59,25 @@ export class DeploysController { throw mapError(e); } } + + @Post('contractDeployConfigs') + @UseGuards(GithubBasicAuthGuard) + async contractDeployConfigs( + @Req() req: ExpressRequest, + @Body() + { + repoDeploymentSlug, + }: z.infer, + ): Promise> { + try { + return await this.deploysService.getContractDeployConfigs( + repoDeploymentSlug, + ); + } catch (e: any) { + throw mapError(e); + } + } + @Post('isRepositoryTransferred') @UseGuards(BearerAuthGuard) @UsePipes(new ZodValidationPipe(Deploys.query.inputs.isRepositoryTransferred)) @@ -90,49 +112,129 @@ export class DeploysController { }: z.infer, ): Promise> { // called from Console frontend to initialize a new repo for deployment - const repository = await this.deploysService.transferGithubRepository({ - user: req.user as User, // TODO change to UserDetails from auth service - newGithubUsername, - repositorySlug, - }); - return { - repositorySlug: repository.slug, - githubRepoFullName: repository.githubRepoFullName, - }; + try { + const repository = await this.deploysService.transferGithubRepository({ + user: req.user as User, // TODO change to UserDetails from auth service + newGithubUsername, + repositorySlug, + }); + return { + repositorySlug: repository.slug, + githubRepoFullName: repository.githubRepoFullName, + }; + } catch (e: any) { + throw mapError(e); + } } - @Post('deployWasm') + @Post('addRepoDeployment') + @UseGuards(GithubBasicAuthGuard) // Currently used only by github - can be extended to authorize other clients with Bearer tokens + @UsePipes(new ZodValidationPipe(Deploys.mutation.inputs.addRepoDeployment)) + async addRepoDeployment( + @Req() req: Request, + @Body() + { + githubRepoFullName, + commitHash, + commitMessage, + }: z.infer, + ): Promise> { + try { + const repoDeployment = await this.deploysService.addRepoDeployment({ + githubRepoFullName, + commitHash, + commitMessage, + }); + + return { repoDeploymentSlug: repoDeployment.slug }; + } catch (e: any) { + throw mapError(e); + } + } + + @Post('addContractDeployment') + @UseInterceptors(AnyFilesInterceptor()) + @UseGuards(GithubBasicAuthGuard) // Currently used only by github - can be extended to authorize other clients with Bearer tokens + async addContractDeployment( + @Req() req: ExpressRequest, + @UploadedFiles() files: Array, + @Body() body: z.infer, + ): Promise> { + const parsedValue = + Deploys.mutation.inputs.addContractDeployment.safeParse(body); + if (parsedValue.success === false) { + throw new BadRequestException(fromZodError(parsedValue.error).toString()); + } + + const parsedFiles = Deploys.mutation.inputs.wasmFiles.safeParse(files); + if (parsedFiles.success === false) { + throw new BadRequestException(fromZodError(parsedFiles.error).toString()); + } + const { repoDeploymentSlug } = body; + + try { + // called from Console frontend to initialize a new repo for deployment + await this.deploysService.addContractDeployment({ + repoDeploymentSlug, + files, + }); + + return await this.deploysService.getContractDeployConfigs( + repoDeploymentSlug, + ); + } catch (e: any) { + throw mapError(e); + } + } + + @Post('addNearSocialComponentDeployment') @UseInterceptors(AnyFilesInterceptor()) @UseGuards(GithubBasicAuthGuard) // Currently used only by github - can be extended to authorize other clients - async deployWasm( + async addNearSocialComponentDeployment( @Req() req: Request, @UploadedFiles() files: Array, - @Body() body: z.infer, + @Body() + body: z.infer, ) { - const parsedValue = Deploys.mutation.inputs.deployWasm.safeParse(body); + const parsedValue = + Deploys.mutation.inputs.deployNearSocialComponent.safeParse(body); if (parsedValue.success === false) { throw new BadRequestException(fromZodError(parsedValue.error).toString()); } - const parsedFiles = Deploys.mutation.inputs.wasmFiles.safeParse(files); + const parsedFiles = + Deploys.mutation.inputs.nearSocialFiles.safeParse(files); if (parsedFiles.success === false) { throw new BadRequestException(fromZodError(parsedFiles.error).toString()); } - const { githubRepoFullName, commitHash, commitMessage } = body; - - // called from Console frontend to initialize a new repo for deployment - return this.deploysService.deployRepository({ - githubRepoFullName, - commitHash, - commitMessage, - files, - }); + const { + componentName, + componentDescription, + componentIconIpfsCid, + componentTags, + } = body; + try { + return await this.deploysService.addNearSocialComponentDeployment({ + repoDeploymentSlug: body.repoDeploymentSlug, + metadata: { + componentName, + componentDescription, + componentIconIpfsCid, + componentTags, + }, + file: files[0], + }); + } catch (e: any) { + throw mapError(e); + } } - @Post('addFrontend') - @UsePipes(new ZodValidationPipe(Deploys.mutation.inputs.addFrontend)) + @Post('addFrontendDeployment') + @UsePipes( + new ZodValidationPipe(Deploys.mutation.inputs.addFrontendDeployment), + ) @UseGuards(GithubBasicAuthGuard) // Currently used only by github - can be extended to authorize other clients - async addFrontend( + async addFrontendDeployment( @Req() req: Request, @Body() { @@ -140,24 +242,18 @@ export class DeploysController { frontendDeployUrl, cid, packageName, - }: z.infer, - ) { - const repoDeployment = await this.deploysService.getRepoDeploymentBySlug( - repoDeploymentSlug, - ); - if (!repoDeployment) { - throw new BadRequestException( - `RepoDeployment slug ${repoDeploymentSlug} not found`, - ); + }: z.infer, + ): Promise> { + try { + return await this.deploysService.addFrontendDeployment({ + frontendDeployUrl, + cid, + packageName, + repoDeploymentSlug, + }); + } catch (e: any) { + throw mapError(e); } - - return this.deploysService.addFrontend({ - repositorySlug: repoDeployment.repositorySlug, - frontendDeployUrl, - cid, - packageName, - repoDeploymentSlug, - }); } @Post('listRepositories') @@ -198,13 +294,19 @@ function mapError(e: Error) { const code = errorInfo?.code; switch (code) { case 'PERMISSION_DENIED': - return new ForbiddenException(); + return new ForbiddenException(e.message); case 'TOO_MANY_REQUESTS': return new TooManyRequestsException(); case 'NAME_CONFLICT': return new ConflictException(code); case 'CONFLICT': return new ConflictException(code); + case 'BAD_REQUEST': + return new BadRequestException(e.message); + case 'TRANSFER_INITIATED': + return new BadRequestException(e.message); + case 'NOT_FOUND': + return new NotFoundException(e.message); default: return e; } diff --git a/backend/src/modules/deploys/deploys.service.ts b/backend/src/modules/deploys/deploys.service.ts index 29a8a332..05a13778 100644 --- a/backend/src/modules/deploys/deploys.service.ts +++ b/backend/src/modules/deploys/deploys.service.ts @@ -1,14 +1,14 @@ import { ProjectsService } from '@/src/core/projects/projects.service'; import sodium from 'libsodium-wrappers'; -import { - BadRequestException, - ForbiddenException, - Injectable, -} from '@nestjs/common'; +import { Injectable } from '@nestjs/common'; import { customAlphabet } from 'nanoid'; import { PrismaService } from './prisma.service'; import { Environment, Project, User } from '@pc/database/clients/core'; -import { Repository } from '@pc/database/clients/deploys'; +import { + ContractDeployConfig, + NearSocialComponentDeployConfig, + Repository, +} from '@pc/database/clients/deploys'; import { createHash, randomBytes, scryptSync } from 'crypto'; import { encode } from 'bs58'; import { VError } from 'verror'; @@ -19,6 +19,8 @@ import { Octokit } from '@octokit/core'; import { ConfigService } from '@nestjs/config'; import { AppConfig } from '@/src/config/validate'; import { DeployError } from './deploy-error'; +import _ from 'lodash'; +import { parseNearAmount } from 'near-api-js/lib/utils/format'; const nanoid = customAlphabet( '0123456789ABCDEFGHIJKLMNOPQRSTUVWXYZabcdefghijklmnopqrstuvwxyz', @@ -206,7 +208,10 @@ export class DeploysService { ); if (isTransferred) { - throw new BadRequestException('Repository was already transferred'); + throw new VError( + { info: { code: 'BAD_REQUEST' } }, + 'Repository was already transferred', + ); } const repo = await this.prisma.repository.findUnique({ @@ -216,7 +221,10 @@ export class DeploysService { }); if (!repo) { - throw new BadRequestException('repo does not exist'); + throw new VError( + { info: { code: 'BAD_REQUEST' } }, + 'repo does not exist', + ); } const { createdBy } = await this.projectsService.getActiveProject({ @@ -224,7 +232,10 @@ export class DeploysService { }); if (createdBy !== user.id) { - throw new ForbiddenException('User cannot modify this repo'); + throw new VError( + { info: { code: 'PERMISSION_DENIED' } }, + 'User cannot modify this repo', + ); } let octokit: Octokit; @@ -249,6 +260,11 @@ export class DeploysService { if (e.status === 404) { // TODO replace with returning a code to the UI and tell the user they already initiaed a transfer. throw new VError(e, 'Could not find the repository to transfer'); + } else if (e.status === 400) { + throw new VError( + { info: { code: DeployError.TRANSFER_INITIATED } }, + 'Repository transfer already initiated', + ); } throw new VError(e, 'Could not transfer the repository'); } @@ -429,30 +445,15 @@ export class DeploysService { }); } - /** - * Entry point for deploying one or more WASMs from - * a github repo - */ - async deployRepository({ + async addRepoDeployment({ githubRepoFullName, commitHash, commitMessage, - files, }: { githubRepoFullName: string; commitHash: string; commitMessage: string; - files: Array; }) { - /* - - find matching Repository - - create new RepoDeployment - - match each bundle to a ContractDeployConfig - - if a match cannot be found, create a new ContractDeployConfig w/ generateDeployConfig. - This allows us to be dynamic and handle new contracts as they are added to the repo - instead of making the contract set static at time of connecting the repo - - deployContract for contract - */ const repo = await this.prisma.repository.findUnique({ where: { githubRepoFullName, @@ -463,12 +464,13 @@ export class DeploysService { }); if (!repo) { - throw new BadRequestException( + throw new VError( + { info: { code: 'BAD_REQUEST' } }, 'githubRepoFullName not found please add a Repository project to deploy to', ); } - const repoDeployment = await this.prisma.repoDeployment.create({ + return this.prisma.repoDeployment.create({ data: { slug: nanoid(), repositorySlug: repo.slug, @@ -476,6 +478,41 @@ export class DeploysService { commitMessage, }, }); + } + + /** + * Entry point for deploying one or more WASMs from + * a github repo + */ + async addContractDeployment({ + repoDeploymentSlug, + files, + }: { + repoDeploymentSlug: string; + files: Array; + }) { + /* + - find matching Repository + - create new RepoDeployment + - match each bundle to a ContractDeployConfig + - if a match cannot be found, create a new ContractDeployConfig w/ generateDeployConfig. + This allows us to be dynamic and handle new contracts as they are added to the repo + instead of making the contract set static at time of connecting the repo + - deployContract for contract + */ + + const repoDeployment = await this.getRepoDeploymentBySlug( + repoDeploymentSlug, + ); + + if (!repoDeployment) { + throw new VError( + { info: { code: 'BAD_REQUEST' } }, + `Could not find repoDeployment with that slug`, + ); + } + + const repo = repoDeployment.repository; await Promise.all( files.map(async (file) => { @@ -483,10 +520,11 @@ export class DeploysService { ({ filename }) => filename === file.originalname, ); if (!deployConfig) { - deployConfig = await this.generateDeployConfig({ + deployConfig = (await this.generateDeployConfig({ filename: file.originalname, repositorySlug: repo.slug, - }); + type: 'contract', + })) as ContractDeployConfig; } return this.deployContract({ deployConfig, @@ -516,12 +554,183 @@ export class DeploysService { }); } + /** + * Entry point for deploying one or more Near Social Components from + * a github repo + */ + async addNearSocialComponentDeployment({ + repoDeploymentSlug, + metadata = {}, + file, + }: { + repoDeploymentSlug: string; + metadata?: { + componentName?: string; + componentDescription?: string; + componentIconIpfsCid?: string; + componentTags?: string[]; + }; + file: Express.Multer.File; + }) { + const repoDeployment = await this.getRepoDeploymentBySlug( + repoDeploymentSlug, + ); + + if (!repoDeployment) { + throw new VError( + { info: { code: 'BAD_REQUEST' } }, + 'Could not find repoDeployment with that slug', + ); + } + + const repo = repoDeployment.repository; + + const componentName = metadata.componentName || file.originalname; + + let deployConfig = repo.nearSocialComponentDeployConfigs.find( + (deployConfig) => deployConfig.componentName === componentName, + ); + + if (!deployConfig) { + deployConfig = (await this.generateDeployConfig({ + filename: componentName, + repositorySlug: repo.slug, + type: 'component', + })) as NearSocialComponentDeployConfig; + } + + await this.deployNearSocialComponent({ + deployConfig, + file: file.buffer, + repoDeploymentSlug: repoDeployment.slug, + metadata, + }); + + return this.prisma.repoDeployment.findUnique({ + where: { + slug: repoDeployment.slug, + }, + include: { + nearSocialComponentDeployments: { + include: { + nearSocialComponentDeployConfig: { + select: { + componentPath: true, + }, + }, + }, + }, + }, + }); + } + + metadataInputToNearSocialMetaData(metadata) { + return { + name: metadata.componentName, + description: metadata.componentDescription, + image: { + ipfs_cid: metadata.componentIconIpfsCid, + }, + tags: (metadata.componentTags || []).reduce( + (acc, curr) => ({ ...acc, [curr]: '' }), + {}, + ), + }; + } + + async deployNearSocialComponent({ + deployConfig, + file, + repoDeploymentSlug, + metadata, + }) { + const keyPair = KeyPair.fromString(deployConfig.nearPrivateKey); + const keyStore = new keyStores.InMemoryKeyStore(); + const nearConfig = { + networkId: 'testnet', + keyStore, + nodeUrl: 'https://rpc.testnet.near.org', // todo from env? + helperUrl: 'https://helper.testnet.near.org', + headers: {}, + }; + await keyStore.setKey( + nearConfig.networkId, + deployConfig.nearAccountId, + keyPair, + ); + + const near = await connect(nearConfig); + const account = await near.account(deployConfig.nearAccountId); + + const component = await account.viewFunction('v1.social08.testnet', 'get', { + keys: [`${deployConfig.componentPath}/**`], + }); + + const newComponent = { + '': file.toString('utf-8'), + metadata: _.merge( + _.cloneDeep(component.metadata), + this.metadataInputToNearSocialMetaData(metadata), + ), + }; + + let txOutcome; + + const updatedComponentData = _.merge(_.cloneDeep(component), newComponent); + + if (!_.isEqual(newComponent, component)) { + try { + txOutcome = await account.functionCall({ + contractId: 'v1.social08.testnet', + methodName: 'set', + args: { + data: { + [account.accountId]: { + widget: { + [deployConfig.componentName]: updatedComponentData, + }, + }, + }, + }, + attachedDeposit: parseNearAmount('1'), + }); + } catch (e: any) { + await this.prisma.nearSocialComponentDeployment.create({ + data: { + slug: nanoid(), + repoDeploymentSlug, + nearSocialComponentDeployConfigSlug: deployConfig.slug, + status: 'ERROR', + }, + }); + throw new VError( + e, + `Could not set data on socialDB ${updatedComponentData}`, + ); + } + } + + if (txOutcome?.transaction?.hash) { + await this.prisma.nearSocialComponentDeployment.create({ + data: { + slug: nanoid(), + repoDeploymentSlug, + nearSocialComponentDeployConfigSlug: deployConfig.slug, + deployTransactionHash: txOutcome.transaction.hash, + status: 'SUCCESS', + }, + }); + } + } + async generateDeployConfig({ filename, repositorySlug, + type, }: { filename: string; repositorySlug: Repository['slug']; + type: 'contract' | 'component'; }) { const keyStore = new keyStores.InMemoryKeyStore(); const nearConfig = { @@ -540,15 +749,28 @@ export class DeploysService { await near.accountCreator.createAccount(accountId, keyPair.getPublicKey()); await keyStore.setKey(nearConfig.networkId, accountId, keyPair); - return this.prisma.contractDeployConfig.create({ - data: { - slug: nanoid(), - nearPrivateKey: keyPair.toString(), - filename, - repositorySlug, - nearAccountId: accountId, - }, - }); + if (type === 'contract') { + return this.prisma.contractDeployConfig.create({ + data: { + slug: nanoid(), + nearPrivateKey: keyPair.toString(), + filename, + repositorySlug, + nearAccountId: accountId, + }, + }); + } else { + return this.prisma.nearSocialComponentDeployConfig.create({ + data: { + slug: nanoid(), + nearPrivateKey: keyPair.toString(), + componentPath: `${accountId}/widget/${filename}`, + componentName: filename, + repositorySlug, + nearAccountId: accountId, + }, + }); + } } /** @@ -603,15 +825,17 @@ export class DeploysService { } } - await this.prisma.contractDeployment.create({ - data: { - slug: nanoid(), - repoDeploymentSlug, - contractDeployConfigSlug: deployConfig.slug, - deployTransactionHash: txOutcome ? txOutcome.transaction.hash : null, - status: 'SUCCESS', - }, - }); + if (txOutcome?.transaction?.hash) { + await this.prisma.contractDeployment.create({ + data: { + slug: nanoid(), + repoDeploymentSlug, + contractDeployConfigSlug: deployConfig.slug, + deployTransactionHash: txOutcome.transaction.hash, + status: 'SUCCESS', + }, + }); + } try { await this.projectsService.systemAddContract( @@ -630,27 +854,40 @@ export class DeploysService { /** * Sets Frontend Deploy Url */ - async addFrontend({ - repositorySlug, + async addFrontendDeployment({ frontendDeployUrl, cid, packageName, repoDeploymentSlug, }) { - let frontendDeployConfig = await this.prisma.frontendDeployConfig.findFirst( - { - where: { - repositorySlug, - packageName, - }, - }, + const repoDeployment = await this.getRepoDeploymentBySlug( + repoDeploymentSlug, ); + if (!repoDeployment) { + throw new VError( + { + info: { + code: 'CONFLICT', + }, + }, + `RepoDeployment slug ${repoDeploymentSlug} not found`, + ); + } + + let frontendDeployConfig = + repoDeployment.repository.frontendDeployConfigs.find((config) => { + return ( + config.packageName === packageName && + config.repositorySlug === repoDeployment.repositorySlug + ); + }); + if (!frontendDeployConfig) { frontendDeployConfig = await this.prisma.frontendDeployConfig.create({ data: { slug: nanoid(), - repositorySlug, + repositorySlug: repoDeployment.repositorySlug, packageName, }, }); @@ -664,7 +901,8 @@ export class DeploysService { }); if (existingFrontend) { - throw new BadRequestException( + throw new VError( + { info: { code: 'BAD_REQUEST' } }, `Package ${packageName} already added for repo deployment ${repoDeploymentSlug}`, ); } @@ -680,15 +918,39 @@ export class DeploysService { }); } - getRepoDeploymentBySlug(slug) { - return this.prisma.repoDeployment.findUnique({ + async getContractDeployConfigs(slug) { + const repoDeployment = (await this.getRepoDeploymentBySlug(slug)) as any; + return repoDeployment.repository.contractDeployConfigs.reduce( + (acc, curr) => ({ + ...acc, + [curr.filename]: { nearAccountId: curr.nearAccountId }, + }), + {}, + ); + } + + async getRepoDeploymentBySlug(slug) { + const repoDeployment = this.prisma.repoDeployment.findUnique({ where: { slug, }, include: { - repository: true, + repository: { + include: { + contractDeployConfigs: true, + frontendDeployConfigs: true, + nearSocialComponentDeployConfigs: true, + }, + }, }, }); + if (!repoDeployment) { + throw new VError( + { info: { code: 'NOT_FOUND' } }, + `No such repoDeployment found`, + ); + } + return repoDeployment; } getDeployRepository(githubRepoFullName: string) { @@ -799,6 +1061,18 @@ export class DeploysService { status: true, }, }, + nearSocialComponentDeployments: { + select: { + slug: true, + deployTransactionHash: true, + nearSocialComponentDeployConfig: { + select: { + componentName: true, + componentPath: true, + }, + }, + }, + }, }, }); @@ -810,6 +1084,7 @@ export class DeploysService { createdAt, frontendDeployments, contractDeployments, + nearSocialComponentDeployments, repository, } = deployment; @@ -820,6 +1095,7 @@ export class DeploysService { createdAt: createdAt.toISOString(), frontendDeployments, contractDeployments, + nearSocialComponentDeployments, githubRepoFullName: repository.githubRepoFullName, }; }); diff --git a/common/types/api.ts b/common/types/api.ts index 560620de..21b3b441 100644 --- a/common/types/api.ts +++ b/common/types/api.ts @@ -321,10 +321,17 @@ export namespace Api { typeof Alerts.mutation.errors.rotateWebhookDestinationSecret >; }; - '/deploys/addDeploy': { - input: z.infer; - output: z.infer; - error: z.infer; + '/deploys/addConsoleDeployProject': { + input: z.infer; + output: z.infer< + typeof Deploys.mutation.outputs.addConsoleDeployProject + >; + error: z.infer; + }; + '/deploys/contractDeployConfigs': { + input: z.infer; + output: z.infer; + error: z.infer; }; '/deploys/transferGithubRepository': { input: z.infer; @@ -333,20 +340,25 @@ export namespace Api { >; error: z.infer; }; - '/deploys/deployWasm': { - input: z.infer; - output: z.infer; - error: z.infer; + '/deploys/addRepoDeployment': { + input: z.infer; + output: z.infer; + error: z.infer; + }; + '/deploys/addContractDeployment': { + input: z.infer; + output: z.infer; + error: z.infer; }; '/deploys/wasmFiles': { input: z.infer; output: z.infer; error: z.infer; }; - '/deploys/addFrontend': { - input: z.infer; - output: z.infer; - error: z.infer; + '/deploys/addFrontendDeployment': { + input: z.infer; + output: z.infer; + error: z.infer; }; }; diff --git a/common/types/deploys/deploys.schema.ts b/common/types/deploys/deploys.schema.ts index d15a29af..044e5715 100644 --- a/common/types/deploys/deploys.schema.ts +++ b/common/types/deploys/deploys.schema.ts @@ -1,13 +1,27 @@ import { z } from 'zod'; import { stringifiedDate } from '../schemas'; -const frontendDeployments = z.array( +const frontendDeployment = z.strictObject({ + slug: z.string(), + url: z.string().or(z.null()), + cid: z.string().or(z.null()), +}); + +const frontendDeploymentWithConfig = frontendDeployment.extend({ + frontendDeployConfig: z.strictObject({ + packageName: z.string(), + }), +}); + +const frontendDeployments = z.array(frontendDeploymentWithConfig); + +const nearSocialComponentDeployments = z.array( z.strictObject({ slug: z.string(), - url: z.string().or(z.null()), - cid: z.string().or(z.null()), - frontendDeployConfig: z.strictObject({ - packageName: z.string(), + deployTransactionHash: z.string().or(z.null()), + nearSocialComponentDeployConfig: z.strictObject({ + componentName: z.string(), + componentPath: z.string(), }), }), ); @@ -28,6 +42,7 @@ const repoDeployments = z.array( createdAt: stringifiedDate, frontendDeployments, contractDeployments, + nearSocialComponentDeployments, githubRepoFullName: z.string(), }), ); @@ -83,7 +98,7 @@ export const query = { export const mutation = { inputs: { - addDeploy: z.strictObject({ + addConsoleDeployProject: z.strictObject({ githubRepoFullName: z.string().regex(/[\w\.\-]+\/[\w\.\-]+/), // matches e.g. 'near/pagoda-console` projectName: z.string(), }), @@ -91,15 +106,31 @@ export const mutation = { repositorySlug: z.string(), newGithubUsername: z.string(), }), - deployWasm: z.strictObject({ + addContractDeployment: z.strictObject({ + repoDeploymentSlug: z.string(), + }), + addRepoDeployment: z.strictObject({ githubRepoFullName: z.string().regex(/[\w\.\-]+\/[\w\.\-]+/), // matches e.g. 'near/pagoda-console` commitHash: z.string(), commitMessage: z.string(), }), + deployNearSocialComponent: z.strictObject({ + repoDeploymentSlug: z.string(), + componentName: z + .string() + .regex(/^[a-zA-Z0-9]*$/) + .optional(), + componentDescription: z.string().optional(), + componentIconIpfsCid: z.string().optional(), + componentTags: z.array(z.string()).optional(), + }), wasmFiles: z.array( z.object({ mimetype: z.string().startsWith('application/wasm') }), ), - addFrontend: z + nearSocialFiles: z + .array(z.object({ mimetype: z.string().startsWith('text/') })) + .length(1), + addFrontendDeployment: z .strictObject({ repoDeploymentSlug: z.string(), packageName: z.string(), @@ -110,10 +141,13 @@ export const mutation = { (data) => !!data.frontendDeployUrl || !!data.cid, 'Either frontendDeployUrl or cid should be filled in.', ), + contractDeployConfigs: z.strictObject({ + repoDeploymentSlug: z.string(), + }), }, outputs: { - addDeploy: z.strictObject({ + addConsoleDeployProject: z.strictObject({ repositorySlug: z.string(), projectSlug: z.string(), }), @@ -121,16 +155,24 @@ export const mutation = { repositorySlug: z.string(), githubRepoFullName: z.string(), }), - deployWasm: z.void(), + addRepoDeployment: z.strictObject({ + repoDeploymentSlug: z.string(), + }), + addContractDeployment: z.void(), wasmFiles: z.void(), - addFrontend: z.void(), + addFrontendDeployment: frontendDeployment, + contractDeployConfigs: z + .strictObject({}) + .catchall(z.strictObject({ nearAccountId: z.string() })), }, errors: { - addDeploy: z.unknown(), + addConsoleDeployProject: z.unknown(), transferGithubRepository: z.unknown(), - deployWasm: z.unknown(), + addRepoDeployment: z.unknown(), + addContractDeployment: z.unknown(), wasmFiles: z.unknown(), - addFrontend: z.unknown(), + addFrontendDeployment: z.unknown(), + contractDeployConfigs: z.unknown(), }, }; diff --git a/database/schemas/deploys/migrations/20230313182242_near_social_widgets/migration.sql b/database/schemas/deploys/migrations/20230313182242_near_social_widgets/migration.sql new file mode 100644 index 00000000..3809911c --- /dev/null +++ b/database/schemas/deploys/migrations/20230313182242_near_social_widgets/migration.sql @@ -0,0 +1,33 @@ +-- CreateTable +CREATE TABLE "NearSocialWidgetDeployConfig" ( + "slug" TEXT NOT NULL, + "repositorySlug" TEXT NOT NULL, + "widgetName" TEXT NOT NULL, + "nearAccountId" TEXT NOT NULL, + "nearPrivateKey" TEXT NOT NULL, + "widgetPath" TEXT NOT NULL +); + +-- CreateTable +CREATE TABLE "NearSocialWidgetDeployment" ( + "slug" TEXT NOT NULL, + "repoDeploymentSlug" TEXT NOT NULL, + "nearSocialWidgetDeployConfigSlug" TEXT NOT NULL, + "deployTransactionHash" TEXT, + "status" "ContractDeployStatus" NOT NULL DEFAULT E'IN_PROGRESS' +); + +-- CreateIndex +CREATE UNIQUE INDEX "NearSocialWidgetDeployConfig_slug_key" ON "NearSocialWidgetDeployConfig"("slug"); + +-- CreateIndex +CREATE UNIQUE INDEX "NearSocialWidgetDeployment_slug_key" ON "NearSocialWidgetDeployment"("slug"); + +-- AddForeignKey +ALTER TABLE "NearSocialWidgetDeployConfig" ADD CONSTRAINT "NearSocialWidgetDeployConfig_repositorySlug_fkey" FOREIGN KEY ("repositorySlug") REFERENCES "Repository"("slug") ON DELETE RESTRICT ON UPDATE CASCADE; + +-- AddForeignKey +ALTER TABLE "NearSocialWidgetDeployment" ADD CONSTRAINT "NearSocialWidgetDeployment_nearSocialWidgetDeployConfigSlu_fkey" FOREIGN KEY ("nearSocialWidgetDeployConfigSlug") REFERENCES "NearSocialWidgetDeployConfig"("slug") ON DELETE RESTRICT ON UPDATE CASCADE; + +-- AddForeignKey +ALTER TABLE "NearSocialWidgetDeployment" ADD CONSTRAINT "NearSocialWidgetDeployment_repoDeploymentSlug_fkey" FOREIGN KEY ("repoDeploymentSlug") REFERENCES "RepoDeployment"("slug") ON DELETE RESTRICT ON UPDATE CASCADE; diff --git a/database/schemas/deploys/migrations/20230327143149_rename_widget_to_component/migration.sql b/database/schemas/deploys/migrations/20230327143149_rename_widget_to_component/migration.sql new file mode 100644 index 00000000..9ac05795 --- /dev/null +++ b/database/schemas/deploys/migrations/20230327143149_rename_widget_to_component/migration.sql @@ -0,0 +1,55 @@ +/* + Warnings: + + - You are about to drop the `NearSocialWidgetDeployConfig` table. If the table is not empty, all the data it contains will be lost. + - You are about to drop the `NearSocialWidgetDeployment` table. If the table is not empty, all the data it contains will be lost. + +*/ +-- DropForeignKey +ALTER TABLE "NearSocialWidgetDeployConfig" DROP CONSTRAINT "NearSocialWidgetDeployConfig_repositorySlug_fkey"; + +-- DropForeignKey +ALTER TABLE "NearSocialWidgetDeployment" DROP CONSTRAINT "NearSocialWidgetDeployment_nearSocialWidgetDeployConfigSlu_fkey"; + +-- DropForeignKey +ALTER TABLE "NearSocialWidgetDeployment" DROP CONSTRAINT "NearSocialWidgetDeployment_repoDeploymentSlug_fkey"; + +-- DropTable +DROP TABLE "NearSocialWidgetDeployConfig"; + +-- DropTable +DROP TABLE "NearSocialWidgetDeployment"; + +-- CreateTable +CREATE TABLE "NearSocialComponentDeployConfig" ( + "slug" TEXT NOT NULL, + "repositorySlug" TEXT NOT NULL, + "componentName" TEXT NOT NULL, + "nearAccountId" TEXT NOT NULL, + "nearPrivateKey" TEXT NOT NULL, + "componentPath" TEXT NOT NULL +); + +-- CreateTable +CREATE TABLE "NearSocialComponentDeployment" ( + "slug" TEXT NOT NULL, + "repoDeploymentSlug" TEXT NOT NULL, + "nearSocialComponentDeployConfigSlug" TEXT NOT NULL, + "deployTransactionHash" TEXT, + "status" "ContractDeployStatus" NOT NULL DEFAULT E'IN_PROGRESS' +); + +-- CreateIndex +CREATE UNIQUE INDEX "NearSocialComponentDeployConfig_slug_key" ON "NearSocialComponentDeployConfig"("slug"); + +-- CreateIndex +CREATE UNIQUE INDEX "NearSocialComponentDeployment_slug_key" ON "NearSocialComponentDeployment"("slug"); + +-- AddForeignKey +ALTER TABLE "NearSocialComponentDeployConfig" ADD CONSTRAINT "NearSocialComponentDeployConfig_repositorySlug_fkey" FOREIGN KEY ("repositorySlug") REFERENCES "Repository"("slug") ON DELETE RESTRICT ON UPDATE CASCADE; + +-- AddForeignKey +ALTER TABLE "NearSocialComponentDeployment" ADD CONSTRAINT "NearSocialComponentDeployment_nearSocialComponentDeployCon_fkey" FOREIGN KEY ("nearSocialComponentDeployConfigSlug") REFERENCES "NearSocialComponentDeployConfig"("slug") ON DELETE RESTRICT ON UPDATE CASCADE; + +-- AddForeignKey +ALTER TABLE "NearSocialComponentDeployment" ADD CONSTRAINT "NearSocialComponentDeployment_repoDeploymentSlug_fkey" FOREIGN KEY ("repoDeploymentSlug") REFERENCES "RepoDeployment"("slug") ON DELETE RESTRICT ON UPDATE CASCADE; diff --git a/database/schemas/deploys/schema.prisma b/database/schemas/deploys/schema.prisma index 53a70454..aac2be3c 100644 --- a/database/schemas/deploys/schema.prisma +++ b/database/schemas/deploys/schema.prisma @@ -22,6 +22,7 @@ model Repository { repoDeployments RepoDeployment[] enabled Boolean @default(true) frontendDeployConfigs FrontendDeployConfig[] + nearSocialComponentDeployConfigs NearSocialComponentDeployConfig[] authTokenHash Bytes authTokenSalt Bytes } @@ -38,6 +39,18 @@ model ContractDeployConfig { contractDeployments ContractDeployment[] } +// deployment configuration for a given near social component in a Repository +model NearSocialComponentDeployConfig { + slug String @unique + repository Repository @relation(fields: [repositorySlug], references: [slug]) + repositorySlug String + componentName String + nearAccountId String + nearPrivateKey String + componentPath String + nearSocialComponentDeployments NearSocialComponentDeployment[] +} + // deployment configuration for a given frontend in a Repository // (many templates will only have one) model FrontendDeployConfig { @@ -58,6 +71,7 @@ model RepoDeployment { commitMessage String contractDeployments ContractDeployment[] frontendDeployments FrontendDeployment[] + nearSocialComponentDeployments NearSocialComponentDeployment[] createdAt DateTime @default(now()) @db.Timestamptz } @@ -78,6 +92,16 @@ model ContractDeployment { status ContractDeployStatus @default(IN_PROGRESS) } +model NearSocialComponentDeployment { + slug String @unique + repoDeployment RepoDeployment @relation(fields: [repoDeploymentSlug], references: [slug]) + repoDeploymentSlug String + nearSocialComponentDeployConfig NearSocialComponentDeployConfig @relation(fields: [nearSocialComponentDeployConfigSlug], references: [slug]) + nearSocialComponentDeployConfigSlug String + deployTransactionHash String? + status ContractDeployStatus @default(IN_PROGRESS) +} + // an executed deployment action on a single FrontendDeployConfig model FrontendDeployment { slug String @unique diff --git a/frontend/modules/deploys/components/testnet/index.tsx b/frontend/modules/deploys/components/testnet/index.tsx index f3d523bd..3cff91d2 100644 --- a/frontend/modules/deploys/components/testnet/index.tsx +++ b/frontend/modules/deploys/components/testnet/index.tsx @@ -43,11 +43,16 @@ const CopyButton = styled(Flex, { const FrontendDeployment = ({ deployment }) => { const display = - deployment.frontendDeployConfig.packageName || deployment.cid || new URL(deployment.url as string).hostname; + deployment.frontendDeployConfig?.packageName || + deployment.cid || + deployment.nearSocialComponentDeployConfig?.componentName || + (deployment.url ? new URL(deployment.url as string).hostname : ''); // ! If you change the IPFS gateway, make sure you thoroughly test a deployed application. Some gateways don't support // ! calling RPC from the browser because of the server's Content Security policy (w3s.link, etc). const link = deployment.cid ? config.gallery.ipfsGatewayUrl.replace('{{cid}}', deployment.cid) + : deployment.nearSocialComponentDeployConfig?.componentPath + ? `https://test.near.social/#/${deployment.nearSocialComponentDeployConfig?.componentPath}` : (deployment.url as string); return (