merge: main into update-team-invite

This commit is contained in:
Sushant kumar
2024-05-17 17:59:22 +05:30
53 changed files with 909 additions and 1163 deletions
+2
View File
@@ -5,6 +5,8 @@
/.pnp
.pnp.js
bun.lockb
yarn.lock
package-lock.json
# testing
/coverage
+1 -6
View File
@@ -21,7 +21,7 @@ NEXT_TELEMETRY_DISABLED=1
# Prisma
# https://www.prisma.io/docs/reference/database-reference/connection-urls#env
DATABASE_URL="postgres://captable:password@pg:5432/captable"
DATABASE_URL="postgres://captable:password@127.0.0.1:54321/captable"
# Next Auth
# You can generate a new secret on the command line with:
@@ -57,8 +57,3 @@ UPLOAD_SECRET_ACCESS_KEY="password"
UPLOAD_BUCKET_PUBLIC="captable-public-bucket"
UPLOAD_BUCKET_PRIVATE="captable-private-bucket"
# Create a free trigger.dev account and get your API key from the dashboard
TRIGGER_API_ID="xxxxxxxxxx"
TRIGGER_API_KEY="xxxxxxxxxx"
TRIGGER_API_URL=https://api.trigger.dev
NEXT_PUBLIC_TRIGGER_PUBLIC_API_KEY=""
+11 -5
View File
@@ -3,21 +3,27 @@ name: "Build docker image"
on:
pull_request:
branches: ["main"]
types: [opened, synchronize, reopened, ready_for_review]
concurrency:
group: build-${{ github.ref }}
cancel-in-progress: true
jobs:
build_docker:
name: Build Docker Image
runs-on: ubuntu-latest
if: github.event.pull_request.draft == false
steps:
- name: Checkout
- name: ⬇️ Checkout
uses: actions/checkout@v4
with:
fetch-depth: 2
- name: Set up Docker Buildx
- name: Set up Docker Buildx
uses: docker/setup-buildx-action@v3
- name: Cache Docker layers
- name: 🎁 Cache Docker layers
uses: actions/cache@v4
with:
path: /tmp/.buildx-cache
@@ -25,7 +31,7 @@ jobs:
restore-keys: |
${{ runner.os }}-buildx-
- name: Build Docker Image
- name: 🚀 Build Docker Image
uses: docker/build-push-action@v5
with:
push: false
@@ -38,7 +44,7 @@ jobs:
- # Temp fix
# https://github.com/docker/build-push-action/issues/252
# https://github.com/moby/buildkit/issues/1896
name: Move cache
name: 🚚 Move cache
run: |
rm -rf /tmp/.buildx-cache
mv /tmp/.buildx-cache-new /tmp/.buildx-cache
+1 -1
View File
@@ -1,4 +1,4 @@
name: cleanup caches by a branch
name: Cleanup cache
on:
pull_request:
types:
+1 -1
View File
@@ -1,4 +1,4 @@
name: first-interaction
name: First interaction greeting
on:
pull_request:
-30
View File
@@ -1,30 +0,0 @@
name: Fly Production Deploy
env:
FLY_API_TOKEN: ${{ secrets.FLY_API_TOKEN }}
on:
release:
types: [published]
jobs:
deploy:
name: 🚀 Deploy
runs-on: ubuntu-latest
concurrency: deploy-group # optional: ensure only one action runs at a time
steps:
- name: ⬇️ Checkout repo
uses: actions/checkout@v4
- name: 🚚 Move Dockerfile to root
run: |
mv ./docker/Dockerfile ./Dockerfile
- name: 🪰 Setup flyctl
uses: superfly/flyctl-actions/setup-flyctl@master
with:
version: latest
- name: 🚀 Deploy Production
run: flyctl deploy --remote-only --build-arg COMMIT_SHA=${{ github.sha }}
env:
FLY_API_TOKEN: ${{ secrets.FLY_API_TOKEN }}
+15 -2
View File
@@ -1,4 +1,4 @@
name: Publish Docker image
name: Production
on:
release:
@@ -6,8 +6,11 @@ on:
jobs:
push_to_registries:
name: Push Docker image to multiple registries
name: Publish docker image & deploy to production
runs-on: ubuntu-latest
concurrency:
group: release
cancel-in-progress: true
permissions:
packages: write
contents: read
@@ -48,3 +51,13 @@ jobs:
push: true
tags: ${{ steps.meta.outputs.tags }}
labels: ${{ steps.meta.outputs.labels }}
- name: 🪰 Setup flyctl
uses: superfly/flyctl-actions/setup-flyctl@master
with:
version: latest
- name: 🚀 Deploy Production
run: flyctl deploy --remote-only --image captable/captable:latest
env:
FLY_API_TOKEN: ${{ secrets.FLY_API_TOKEN }}
+1 -1
View File
@@ -1,4 +1,4 @@
name: "Validate PRs"
name: Validate PR title
on:
pull_request_target:
+3 -1
View File
@@ -10,7 +10,9 @@ jobs:
deploy:
name: 🚀 Deploy
runs-on: ubuntu-latest
concurrency: deploy-group # optional: ensure only one action runs at a time
concurrency:
group: staging
cancel-in-progress: true
steps:
- name: ⬇️ Checkout repo
uses: actions/checkout@v4
+4
View File
@@ -4,7 +4,11 @@
/node_modules
/.pnp
.pnp.js
# lockfiles
bun.lockb
package-lock.json
yarn.lock
# testing
/coverage
+2 -3
View File
@@ -5,9 +5,8 @@ tasks:
export NEXTAUTH_SECRET="$(openssl rand -base64 32)" &&
export BASE_URL="$(gp url 3000)" &&
export NEXT_PUBLIC_BASE_URL="$(gp url 3000)" &&
export EMAIL_SERVER_PORT=2500 &&
npx prisma migrate dev
command: pnpm dx
export EMAIL_SERVER_PORT=2500
command: pnpm db:migrate && pnpm dx
ports:
- port: 3000
+4 -5
View File
@@ -1,12 +1,11 @@
// .lintstagedrc.cjs
const path = require('path')
const path = require("node:path");
const buildBiomeCommand = (filenames) =>
`biome check --apply --no-errors-on-unmatched ${filenames
.map((f) => path.relative(process.cwd(), f))
.join(' ')}`
.join(" ")}`;
/** @type {import('lint-staged').Config} */
module.exports = {
'*.{js,jsx,ts,tsx,json,cjs,mjs}': [buildBiomeCommand],
}
"*.{js,ts,cjs,mjs,d.cts,d.mts,jsx,tsx,json,jsonc}": [buildBiomeCommand],
};
+3
View File
@@ -13,5 +13,8 @@
"editor.codeActionsOnSave": {
"source.organizeImports": "explicit",
"source.fixAll": "explicit"
},
"[typescript]": {
"editor.defaultFormatter": "biomejs.biome"
}
}
-173
View File
@@ -1,173 +0,0 @@
# Contribution Guidelines
## Table of contents
- <a href="#stack">Stack</a>
- <a href="#start">Getting started</a>
- <a href="#setup">Setup development environment</a>
- <a href="#changes">Implement your changes</a>
- <a href="#pr">Open a pull request</a>
- <a href="#community">Join our community</a>
<h2 id="stack">Stack</h2>
- [Next.js](https://nextjs.org)
- [Prisma](https://prisma.io)
- [Tailwind CSS](https://tailwindcss.com)
- [tRPC](https://trpc.io)
- [@shadcn/ui](https://ui.shadcn.com/)
---
<h2 id="start">Getting started</h2>
When contributing to **Captable, Inc.**, whether on GitHub or in other community spaces:
- Be respectful, civil, and open-minded.
- Before opening a new pull request, try searching through the [issue tracker](https://github.com/captableinc/captable/issues) for known issues or fixes.
- If you want to make code changes based on your personal opinion(s), make sure you open an issue first describing the changes you want to make, and open a pull request only when your suggestions get approved by maintainers.
In order to not waste your time implementing a change that has already been declined, or is generally not needed, start by [opening an issue](https://github.com/captableinc/captable/issues/new) describing the problem you would like to solve.
<h2 id="setup">Setup development environment</h2>
- <a href="#with-docker">Development environment with Docker</a>
- <a href="#without-docker">Development environment without Docker</a>
- <a href="#commands">Frequently used commands</a>
<h3 id="with-docker">Development environment with Docker</h3>
- [Install Docker](https://docs.docker.com/get-docker/) on your machine.
- [Install Docker Compose](https://docs.docker.com/compose/install/) on your machine.
- [Fork the repository](https://github.com/captableinc/captable/fork)
- [Install node and pnpm](https://pnpm.io/installation) on your machine. (optional)
- Clone the repository
```bash
git clone https://github.com/<your-github-name>/captable.git
```
- Copy `.env.example` to `.env`
```bash
cp .env.example .env`
```
- Run the following command to start the development environment
```bash
# With pnpm installed
pnpm dx
# Without pnpm installed
docker compose up
```
> **Note**
> Everytime you make changes to Dockerfile.dev or compose.yml, you need to rebuild the docker image by running `docker compose up --build`
- Running `docker compose up` will start all the services on their respective ports.
- App will be running on [http://localhost:3000](http://localhost:3000)
- Emails will be intercepted: [http://localhost:8025](http://localhost:8025)
- SMTP will be on PORT `http://localhost:1025`
- Postgres will be on PORT `http://localhost:5432`
---
<h3 id="without-docker">Development environment without Docker</h3>
> This has been tested on Mac OS and works really well. If you are using Linux/Windows/WSL, you might need to install some additional dependencies.
- [Fork the repository](https://github.com/captableinc/captable/fork)
- Clone the repository
```bash
git clone https://github.com/<your-github-name>/envless.git
```
- Copy `.env.example` to `.env`
```bash
cp .env.example .env
```
- Install latest version of node and pnpm
- Install latest version of postgres database
- Install [mailpit](https://mailpit.axllent.org/docs/install/) for SMTP and email interception
- Create database `captable` in postgres database
- Update `.env` file's `DATABASE_URL` with database credentials
- For a quick start, you can use [Supabase](https://supabase.com/) or [Neon](https://neon.tech/) as well.
- Run the following command to install dependencies
```bash
pnpm install
```
- Run the following command to migrate and seed the database
```bash
pnpm db:migrate
pnpm db:seed
```
- Run the following command to start the development server
```bash
pnpm dev
# On a different terminal, run the following command to start the mail server
pnpm email:dev
```
- App will be running on [http://localhost:3000](http://localhost:3000)
- Emails will be intercepted: [http://localhost:8025](http://localhost:8025)
- SMTP will be on PORT `http://localhost:1025`
- Postgres will be on PORT `http://localhost:5432`
---
<h3 id="commands">Frequently used commands</h3>
> TODO - Please take a look at the `package.json` file for more commands.
---
<h2 id="changes">Implement your changes</h2>
When making commits, make sure to follow the [conventional commit](https://www.conventionalcommits.org/en/v1.0.0/) guidelines, i.e. prepending the message with `feat:`, `fix:`, `chore:`, `docs:`, etc...
```bash
git add <file> && git commit -m "feat/fix/chore/docs: commit message"
```
---
<h2 id="pr">Open a pull request</h2>
### When you're done
Make a commit and push your code to your github fork and make a pull-request.
Thanks for your contributions. Much ❤️
---
<h2 id="community">Community</h2>
Please join us on our discord to get help, discuss features, or just hang out.
- [Join Captable on Discord](https://discord.gg/rCpqnD6G6p)
- [Follow Captable on Twitter](https://twitter.com/captableinc)
---
<h2 id="contributors">Contributors</h2>
<a href="https://github.com/captableinc/captable/graphs/contributors">
<p>
<img src="https://contrib.rocks/image?repo=captableinc/captable" alt="A table of avatars from the project's contributors" />
</p>
</a>
+170 -13
View File
@@ -30,14 +30,21 @@
</a>
</p>
<h3 id="toc">Table of contents</h3>
- <a href="#features">Features</a>
- <a href="#community">Community</a>
- <a href="#contributing">Contibuting</a>
- <a href="https://github.com/captableinc/captable/blob/main/SELF-HOSTING.md" target="_blank">Self hosting</a>
<h2 id="features">✨ Key features</h2>
> [!IMPORTANT]
> We envision a world where cap table management is accessible, secure, and empowering for all. Captable, Inc. aims to democratize the handling of cap tables, securities, and stakeholder interactions. Through cutting-edge technology and a commitment to openness, we strive to be the catalyst for positive change in financial ecosystems.
👷 **Incorporation** - Captable, Inc. helps you incorporate your company in minutes, with all the necessary legal documents and filings taken care of. (coming soon...)
👷 **Incorporation** (wip) - Captable, Inc. helps you incorporate your company in minutes, with all the necessary legal documents and filings taken care of.
👷 **Cap table management** - Captable, Inc. helps you keep track of your companys ownership structure, including who owns what percentage of the company, how much stock/options has been issued, and more. (work in progress...)
👷 **Cap table management** (wip) - Captable, Inc. helps you keep track of your companys ownership structure, including who owns what percentage of the company, how much stock/options has been issued, and more.
**Fundraise** - Captable, Inc. can help you raise capital, whether its signing standard or custom SAFE or creating and managing fundraising rounds, tracking investor commitments, and more.
@@ -47,16 +54,14 @@
**Data rooms** - Captable, Inc. provides a secure virtual data room where you can store important documents and share them with investors, employees, and other stakeholders.
<br />
<h2 id="contributors">🤝 Community</h2>
<h2 id="community">🤝 Community</h2>
We have a community of developers, designers, and entrepreneurs who are passionate about building the future of finance. Join us on Discord to connect with like-minded individuals, share your ideas, and collaborate on projects.
* [Join us on Discord](https://discord.gg/rCpqnD6G6p)
* [Follow us on Twitter](https://twitter.com/captableinc)
* [Meet the Founder](https://captable.inc/schedule)
<br />
<h2 id="contributing">🫡 Contributing</h2>
@@ -66,19 +71,171 @@ We have a community of developers, designers, and entrepreneurs who are passiona
- Any contributions you make are truly appreciated.
- Please follow this [Contribution guideline](https://github.com/captableinc/captable/blob/main/CONTRIBUTING.md) to get started.
<br />
<h2 id="contributors">❤️ Contributors</h2>
<h3 id="stack">Stack</h3>
- [Next.js](https://nextjs.org)
- [Prisma](https://prisma.io)
- [Tailwind CSS](https://tailwindcss.com)
- [tRPC](https://trpc.io)
- [@shadcn/ui](https://ui.shadcn.com/)
---
<h3 id="start">Getting started</h3>
When contributing to **Captable, Inc.**, whether on GitHub or in other community spaces:
- Be respectful, civil, and open-minded.
- Before opening a new pull request, try searching through the [issue tracker](https://github.com/captableinc/captable/issues) for known issues or fixes.
<h3 id="setup">Setup development environment</h3>
- <a href="#gitpod">Development environment on Gitpod</a>
- <a href="#with-docker">Development environment with Docker</a>
- <a href="#without-docker">Development environment without Docker</a>
<h4 id="gitpod">Development environment on Gitpod</h4>
- Click the button below to open this project in Gitpod.
[![Open in Gitpod](https://gitpod.io/button/open-in-gitpod.svg)](https://gitpod.io/#https://github.com/captableinc/captable)
---
<h4 id="with-docker">Development environment with Docker</h4>
- <a href="https://docs.docker.com/get-docker/" target="_blank">Install Docker</a> & <a href="https://docs.docker.com/compose/install/" target="_blank">Docker Compose</a>
- <a href="https://github.com/captableinc/captable/fork" target="_blank">Fork</a> & clone the forked repository
- <a href="https://pnpm.io/installation" target="_blank">Install node and pnpm</a>. (optional)
- Copy `.env.example` to `.env`
```bash
cp .env.example .env`
```
- Run the following command to start the development environment
```bash
# With pnpm installed
pnpm dx
# Without pnpm installed
docker compose up
```
- Run the following command to migrate and seed the database
```bash
docker compose run app pnpm db:migrate
docker compose run app pnpm db:seed
```
> **Note**
> Everytime you make changes to Dockerfile.dev or compose.yml, you need to rebuild the docker image by running `docker compose up --build`
- Running `docker compose up` will start all the services on their respective ports.
- App will be running on [http://localhost:3000](http://localhost:3000)
- Emails will be intercepted: [http://localhost:8025](http://localhost:8025)
- SMTP will be on PORT `http://localhost:1025`
- Postgres will be on PORT `http://localhost:5432`
- Frequently used commands
- `docker compose up` - Start the development environment
- `docker compose down` - Stop the development environment
- `docker compose logs -f` - View logs of the running services
- `docker compose up --build` - Rebuild the docker image
- `docker compose run app pnpm db:migrate` - Run database migrations
- `docker compose run app pnpm db:seed` - Seed the database
---
<h4 id="without-docker">Development environment without Docker</h4>
> This has been tested on Mac OS and works really well. If you are using Linux/Windows/WSL, you might need to install some additional dependencies.
- [Fork the repository](https://github.com/captableinc/captable/fork)
- Clone the repository
```bash
git clone https://github.com/<your-github-name>/envless.git
```
- Copy `.env.example` to `.env`
```bash
cp .env.example .env
```
- Install latest version of node and pnpm
- Install latest version of postgres database
- Install [mailpit](https://mailpit.axllent.org/docs/install/) for SMTP and email interception
- Create database `captable` in postgres database
- Update `.env` file's `DATABASE_URL` with database credentials
- For a quick start, you can use [Supabase](https://supabase.com/) or [Neon](https://neon.tech/) as well.
- Run the following command to install dependencies
```bash
pnpm install
```
- Run the following command to migrate and seed the database
```bash
pnpm db:migrate
pnpm db:seed
```
- Run the following command to start the development server
```bash
pnpm dev
# On a different terminal, run the following command to start the mail server
pnpm email:dev
```
- App will be running on [http://localhost:3000](http://localhost:3000)
- Emails will be intercepted: [http://localhost:8025](http://localhost:8025)
- SMTP will be on PORT `http://localhost:1025`
- Postgres will be on PORT `http://localhost:5432`
- Frequently used commands
- `pnpm dev` - Start the development server
- `pnpm email:dev` - Start the mail server
- `pnpm db:migrate` - Run database migrations
- `pnpm db:seed` - Seed the database
<h4 id="changes">Implement your changes</h4>
When making commits, make sure to follow the [conventional commit](https://www.conventionalcommits.org/en/v1.0.0/) guidelines, i.e. prepending the message with `feat:`, `fix:`, `chore:`, `docs:`, etc...
```bash
git add <file> && git commit -m "feat/fix/chore/docs: commit message"
```
<h4 id="pr">Open a pull request</h4>
> When you're done
Make a commit and push your code to your github fork and make a pull-request.
Thanks for your contributions. Much ❤️
---
<h2 id="contributors">💌 Contributors</h2>
<a href="https://github.com/captableinc/captable/graphs/contributors">
<p>
<img src="https://contrib.rocks/image?repo=captableinc/captable" alt="A table of avatars from the project's contributors" />
</p>
</a>
## Stack
- [Next.js](https://nextjs.org)
- [Prisma](https://prisma.io)
- [Tailwind CSS](https://tailwindcss.com)
- [tRPC](https://trpc.io)
- [@shadcn/ui](https://ui.shadcn.com/)
---
![Alt](https://repobeats.axiom.co/api/embed/a8fc8a167d33eec78a71953a2b9e58985ca4b3b6.svg "Captable repo activity")
+17
View File
@@ -0,0 +1,17 @@
<h2 id="hosting">🚀 Self hosting</h2>
Captable, Inc. is an open-source project that you can host on your own server. This gives you full control over your data.
If you like to self-host Captable, Inc., please schedule a call with us, and we will help you set up your own instance.
<a href="https://captable.inc/schedule/"><img alt="Book us" src="https://cal.com/book-with-cal-dark.svg" /></a>
- **Prerequisites**:
- Docker
- Postgres database (RDS, Cloud SQL, Supabase, Neon etc.)
- SMTP provider (Resend, AWS SES, Mailgun, Sendgrid etc.)
- S3 Compatible storage (AWS S3, R2, Minio, Backblaze etc.)
- **Official Docker Images**:
- <a href="https://hub.docker.com/r/captable/captable" target="_blank">Docker hub</a>
- <a href="https://github.com/captableinc/captable/pkgs/container/captable" target="_blank">Github registry</a>
+36 -113
View File
@@ -1,5 +1,33 @@
{
"$schema": "https://biomejs.dev/schemas/1.7.3/schema.json",
"organizeImports": {
"enabled": true,
"ignore": [
"node_modules",
".next",
"dist",
"public/pdf.worker.min.js",
"./prisma/enums.ts"
]
},
"linter": {
"enabled": true,
"rules": {
"recommended": true,
"suspicious": {
"useAwait": "error"
},
"correctness": { "noUnusedVariables": "warn" },
"style": { "useImportType": "error" }
},
"ignore": [
"node_modules",
".next",
"dist",
"public/pdf.worker.min.js",
"./prisma/enums.ts"
]
},
"formatter": {
"enabled": true,
"formatWithErrors": false,
@@ -8,117 +36,12 @@
"lineEnding": "lf",
"lineWidth": 80,
"attributePosition": "auto",
"ignore": ["public/pdf.worker.min.js", "./prisma/enums.ts"]
},
"organizeImports": { "enabled": true },
"linter": {
"enabled": true,
"rules": {
"recommended": false,
"complexity": {
"noBannedTypes": "error",
"noUselessTypeConstraint": "error",
"useLiteralKeys": "error",
"useOptionalChain": "off"
},
"correctness": { "noPrecisionLoss": "error", "noUnusedVariables": "off" },
"style": {
"noInferrableTypes": "error",
"noNamespace": "error",
"useAsConstAssertion": "error",
"useBlockStatements": "off",
"useConsistentArrayType": "off",
"useForOf": "error",
"useShorthandFunctionType": "error"
},
"suspicious": {
"noEmptyBlockStatements": "error",
"noExplicitAny": "error",
"noExtraNonNullAssertion": "error",
"noMisleadingInstantiator": "error",
"noUnsafeDeclarationMerging": "error",
"useAwait": "off",
"useNamespaceKeyword": "error"
}
}
},
"javascript": {
"formatter": {
"jsxQuoteStyle": "double",
"quoteProperties": "asNeeded",
"trailingComma": "all",
"semicolons": "always",
"arrowParentheses": "always",
"bracketSpacing": true,
"bracketSameLine": false,
"quoteStyle": "double",
"attributePosition": "auto"
}
},
"overrides": [
{
"include": ["*.ts", "*.tsx", "*.mts", "*.cts"],
"linter": {
"rules": {
"correctness": {
"noConstAssign": "off",
"noGlobalObjectCalls": "off",
"noInvalidConstructorSuper": "off",
"noNewSymbol": "off",
"noSetterReturn": "off",
"noUndeclaredVariables": "off",
"noUnreachable": "off",
"noUnreachableSuper": "off"
},
"style": {
"noArguments": "error",
"noVar": "error",
"useConst": "error"
},
"suspicious": {
"noDuplicateClassMembers": "off",
"noDuplicateObjectKeys": "off",
"noDuplicateParameters": "off",
"noFunctionAssign": "off",
"noImportAssign": "off",
"noRedeclare": "off",
"noUnsafeNegation": "off",
"useGetterReturn": "off"
}
}
}
},
{
"include": ["*.ts", "*.tsx", "*.mts", "*.cts"],
"linter": {
"rules": {
"correctness": {
"noConstAssign": "off",
"noGlobalObjectCalls": "off",
"noInvalidConstructorSuper": "off",
"noNewSymbol": "off",
"noSetterReturn": "off",
"noUndeclaredVariables": "off",
"noUnreachable": "off",
"noUnreachableSuper": "off"
},
"style": {
"noArguments": "error",
"noVar": "error",
"useConst": "error"
},
"suspicious": {
"noDuplicateClassMembers": "off",
"noDuplicateObjectKeys": "off",
"noDuplicateParameters": "off",
"noFunctionAssign": "off",
"noImportAssign": "off",
"noRedeclare": "off",
"noUnsafeNegation": "off",
"useGetterReturn": "off"
}
}
}
}
]
"ignore": [
"node_modules",
".next",
"dist",
"public/pdf.worker.min.js",
"./prisma/enums.ts"
]
}
}
+3
View File
@@ -30,6 +30,9 @@ const config = {
return config;
},
experimental: {
instrumentationHook: true,
},
eslint: {
// Warning: This allows production builds to successfully complete even if
// your project has ESLint errors.
+8 -14
View File
@@ -11,11 +11,11 @@
"dx:restart": "docker-compose restart",
"dx:migrate": "docker compose run app pnpm db:migrate",
"dx:seed": "docker compose run app pnpm db:seed",
"db:push": "pnpx prisma db push",
"db:studio": "pnpx prisma studio",
"db:migrate": "pnpx prisma migrate dev",
"db:generate": "pnpx prisma generate",
"db:seed": "NODE_ENV='test' pnpx tsx prisma/seeds/index.ts",
"db:push": "prisma db push",
"db:studio": "prisma studio",
"db:migrate": "prisma migrate dev",
"db:generate": "prisma generate",
"db:seed": "NODE_ENV='test' tsx prisma/seeds/index.ts",
"dev": "next dev",
"build": "next build",
"start": "next start",
@@ -23,10 +23,9 @@
"prepare": "husky install",
"lint": "biome check --apply ./src",
"email:dev": "mailpit",
"format": "biome format --write ./src && pnpx prisma format",
"format": "biome format --write ./src && prisma format",
"email:preview": "email preview ./src/emails",
"copy:pdfjs": "node scripts/copy-pdfjs-worker.cjs",
"trigger": "pnpx @trigger.dev/cli@latest dev"
"copy:pdfjs": "node scripts/copy-pdfjs-worker.cjs"
},
"dependencies": {
"@ark-ui/react": "^2.2.3",
@@ -62,9 +61,6 @@
"@tanstack/react-query": "^4.36.1",
"@tanstack/react-table": "^8.16.0",
"@tremor/react": "^3.15.1",
"@trigger.dev/nextjs": "^2.3.18",
"@trigger.dev/react": "^2.3.18",
"@trigger.dev/sdk": "^2.3.18",
"@trpc/client": "^10.43.6",
"@trpc/next": "^10.45.2",
"@trpc/react-query": "^10.43.6",
@@ -91,6 +87,7 @@
"nodemailer": "^6.9.8",
"papaparse": "^5.4.1",
"pdf-lib": "^1.17.1",
"pg-boss": "^9.0.3",
"prisma-json-types-generator": "^3.0.4",
"react": "18.3.1",
"react-dom": "18.2.0",
@@ -135,8 +132,5 @@
},
"ct3aMetadata": {
"initVersion": "7.25.1"
},
"trigger.dev": {
"endpointId": "captable-A0-I"
}
}
+161 -214
View File
@@ -104,15 +104,6 @@ dependencies:
'@tremor/react':
specifier: ^3.15.1
version: 3.16.2(react-dom@18.2.0)(react@18.3.1)(tailwindcss@3.4.3)
'@trigger.dev/nextjs':
specifier: ^2.3.18
version: 2.3.18(@trigger.dev/sdk@2.3.18)(next@14.2.3)
'@trigger.dev/react':
specifier: ^2.3.18
version: 2.3.18(react-dom@18.2.0)(react@18.3.1)
'@trigger.dev/sdk':
specifier: ^2.3.18
version: 2.3.18
'@trpc/client':
specifier: ^10.43.6
version: 10.45.2(@trpc/server@10.45.2)
@@ -191,6 +182,9 @@ dependencies:
pdf-lib:
specifier: ^1.17.1
version: 1.17.1
pg-boss:
specifier: ^9.0.3
version: 9.0.3
prisma-json-types-generator:
specifier: ^3.0.4
version: 3.0.4(prisma@5.14.0)(typescript@5.4.5)
@@ -4657,10 +4651,6 @@ packages:
resolution: {integrity: sha512-DJSilV5+ytBP1FbFcEJovv4rnnm/CokuVvrBEtW/Va9DvuJ3HksbXUJEpI0aV1KtuL4ZoO9AVE6PyNLzF7tLeA==}
dev: false
/@tanstack/query-core@5.0.0-beta.0:
resolution: {integrity: sha512-VGq/H3PuRoj0shOcg1S5Flv3YD2qNz2ttk8w5xe5AHQE1I8NO9EHSBUxezIpk4dD6M7bQDtwHBMqqU2EwMwyUw==}
dev: false
/@tanstack/react-query@4.36.1(react-dom@18.2.0)(react@18.3.1):
resolution: {integrity: sha512-y7ySVHFyyQblPl3J3eQBWpXZkliroki3ARnBKsdJchlgt7yJLRDUcf4B8soufgiYt3pEQIkBWBx1N9/ZPIeUWw==}
peerDependencies:
@@ -4679,24 +4669,6 @@ packages:
use-sync-external-store: 1.2.2(react@18.3.1)
dev: false
/@tanstack/react-query@5.0.0-beta.2(react-dom@18.2.0)(react@18.3.1):
resolution: {integrity: sha512-JdK1HRw20tuwg3GfT3QZTkuS7s2KDa9FeozuJ7jZULlwPczZagouqYmM6+PL0ad6jfCnw8NzmLFtZdlBx6cTmA==}
peerDependencies:
react: ^18.0.0
react-dom: ^18.0.0
react-native: '*'
peerDependenciesMeta:
react-dom:
optional: true
react-native:
optional: true
dependencies:
'@tanstack/query-core': 5.0.0-beta.0
client-only: 0.0.1
react: 18.3.1
react-dom: 18.2.0(react@18.3.1)
dev: false
/@tanstack/react-table@8.17.3(react-dom@18.2.0)(react@18.3.1):
resolution: {integrity: sha512-5gwg5SvPD3lNAXPuJJz1fOCEZYk9/GeBFH3w/hCgnfyszOIzwkwgp5I7Q4MJtn0WECp84b5STQUDdmvGi8m3nA==}
engines: {node: '>=12'}
@@ -4959,75 +4931,6 @@ packages:
- tailwindcss
dev: false
/@trigger.dev/core-backend@2.3.18:
resolution: {integrity: sha512-LVeeerraGeqKNd2gtajQY+mnGWqkYW7Q2r5oWpL5xIZ8aQg3HRhSIfZs1dryexwKlfqnRjGWueGTy2+j1tbzcg==}
engines: {node: '>=18.0.0'}
dev: false
/@trigger.dev/core@2.3.18:
resolution: {integrity: sha512-j2EdCeyMkZ+zlVnnHl5zmBb+YURSw4x75NqQU1G5X08pQAza7G0qEn8DDGIMR5ieUMiHP0WS9oYy/voYdNfibQ==}
engines: {node: '>=18.0.0'}
dependencies:
ulidx: 2.3.0
zod: 3.22.3
zod-error: 1.5.0
dev: false
/@trigger.dev/nextjs@2.3.18(@trigger.dev/sdk@2.3.18)(next@14.2.3):
resolution: {integrity: sha512-ZS0RTZNrzGEKfOLQLYt3iqlNquD7pd39Hpd/+2tvRCaPSQ3qPYQvdjBSueW0OURZSQSiNno5VUYR5vbVBcAaXA==}
engines: {node: '>=18.0.0'}
peerDependencies:
'@trigger.dev/sdk': ^2.3.18
next: '>=12.0.0'
dependencies:
'@trigger.dev/sdk': 2.3.18
debug: 4.3.4
next: 14.2.3(@babel/core@7.24.5)(react-dom@18.2.0)(react@18.3.1)
transitivePeerDependencies:
- supports-color
dev: false
/@trigger.dev/react@2.3.18(react-dom@18.2.0)(react@18.3.1):
resolution: {integrity: sha512-S+frKgYJoT5JEzYejqlL5rkeHVxTR7wuJvvBMk+cFwA+nKV6isr8VnWmGQivGXGDlKMdF8IihwO+Guqs5tv/Hw==}
peerDependencies:
react: ^16.8.0 || ^17.0.0 || ^18
dependencies:
'@tanstack/react-query': 5.0.0-beta.2(react-dom@18.2.0)(react@18.3.1)
'@trigger.dev/core': 2.3.18
debug: 4.3.4
react: 18.3.1
zod: 3.22.3
transitivePeerDependencies:
- react-dom
- react-native
- supports-color
dev: false
/@trigger.dev/sdk@2.3.18:
resolution: {integrity: sha512-Bjxgl4BbWOAL8rhxeBkl7SzvLLRBMJjiftq/7W7u96MDyPRFUoZZvVMSZzTJufnLBf/xS2JTi8LWU8gzhDJDvw==}
engines: {node: '>=18.0.0'}
dependencies:
'@trigger.dev/core': 2.3.18
'@trigger.dev/core-backend': 2.3.18
chalk: 5.3.0
cronstrue: 2.50.0
debug: 4.3.4
evt: 2.5.7
get-caller-file: 2.0.5
git-remote-origin-url: 4.0.0
git-repo-info: 2.1.1
slug: 6.1.0
terminal-link: 3.0.0
ulid: 2.3.0
uuid: 9.0.1
ws: 8.17.0
zod: 3.22.3
transitivePeerDependencies:
- bufferutil
- supports-color
- utf-8-validate
dev: false
/@trpc/client@10.45.2(@trpc/server@10.45.2):
resolution: {integrity: sha512-ykALM5kYWTLn1zYuUOZ2cPWlVfrXhc18HzBDyRhoPYN0jey4iQHEFSEowfnhg1RvYnrAVjNBgHNeSAXjrDbGwg==}
peerDependencies:
@@ -5979,6 +5882,14 @@ packages:
dev: false
optional: true
/aggregate-error@3.1.0:
resolution: {integrity: sha512-4I7Td01quW/RpocfNayFdFVk1qSuoh0E7JrbRJ16nH01HhKFQ88INq9Sd+nd72zqRySlr9BmDA8xlEJ6vJMrYA==}
engines: {node: '>=8'}
dependencies:
clean-stack: 2.2.0
indent-string: 4.0.0
dev: false
/ansi-escapes@4.3.2:
resolution: {integrity: sha512-gKXj5ALrKWQLsYG9jlTRmR/xKluxHV+Z9QEwNIgCfM1/uwPMCuzVVnh5mwTd+OuBZcwSIMbqssNWRm1lE51QaQ==}
engines: {node: '>=8'}
@@ -5986,13 +5897,6 @@ packages:
type-fest: 0.21.3
dev: true
/ansi-escapes@5.0.0:
resolution: {integrity: sha512-5GFMVX8HqE/TB+FuBJGuO5XG0WrsA6ptUqoODaT/n9mmUaZFkqnBueB4leqGBCmrUHnCnC4PCZTCd0E7QQ83bA==}
engines: {node: '>=12'}
dependencies:
type-fest: 1.4.0
dev: false
/ansi-escapes@6.2.1:
resolution: {integrity: sha512-4nJ3yixlEthEJ9Rk4vPcdBRkZvQZlYyu8j4/Mqz5sgIkddmEnH2Yj2ZrnP9S3tQOvSNRUIgVNF/1yPpRAGNRig==}
engines: {node: '>=14.16'}
@@ -6371,6 +6275,7 @@ packages:
/chalk@5.3.0:
resolution: {integrity: sha512-dLitG79d+GV1Nb/VYcCDFivJeK1hiukt9QjRNVOsUtTy1rR1YJsmpGGTZ3qJos+uw7WmWF4wUwBd9jxjocFC2w==}
engines: {node: ^12.17.0 || ^14.13 || >=16.0.0}
dev: true
/character-entities-html4@2.1.0:
resolution: {integrity: sha512-1v7fgQRj6hnSwFpq1Eu0ynr/CDEw0rXo2B61qXrLNdHZmPKgb7fqS1a2JwF0rISo9q77jDI8VMEHoApn8qDoZA==}
@@ -6434,6 +6339,11 @@ packages:
source-map: 0.6.1
dev: false
/clean-stack@2.2.0:
resolution: {integrity: sha512-4diC9HaTE+KRAMWhDhrGOECgWZxoevMc5TlkObMqNSsVU62PYzXZ/SMTjzyGAFF1YusgxGcSWTEXBhp0CPwQ1A==}
engines: {node: '>=6'}
dev: false
/cli-cursor@3.1.0:
resolution: {integrity: sha512-I/zHAwsKf9FqGoXM4WWRACob9+SNukZTd94DWF57E4toouRulbCxcUh6RKUEOQlYTHJnzkPMySvPNaaSLNfLZw==}
engines: {node: '>=8'}
@@ -6678,9 +6588,11 @@ packages:
/crelt@1.0.6:
resolution: {integrity: sha512-VQ2MBenTq1fWZUH9DJNGti7kKv6EeAuYr3cLwxUWhIu1baTaXh4Ib5W2CqHVqib4/MqbYGJqiL3Zb8GJZr3l4g==}
/cronstrue@2.50.0:
resolution: {integrity: sha512-ULYhWIonJzlScCCQrPUG5uMXzXxSixty4djud9SS37DoNxDdkeRocxzHuAo4ImRBUK+mAuU5X9TSwEDccnnuPg==}
hasBin: true
/cron-parser@4.9.0:
resolution: {integrity: sha512-p0SaNjrHOnQeR8/VnfGbmg9te2kfyYSQ7Sc/j/6DtPL3JQvKxmjO9TSjNFpujqV3vEYYBvNNvXSxzyksBWAx1Q==}
engines: {node: '>=12.0.0'}
dependencies:
luxon: 3.4.4
dev: false
/cross-fetch@3.1.8:
@@ -6896,6 +6808,11 @@ packages:
object-keys: 1.1.1
dev: false
/delay@5.0.0:
resolution: {integrity: sha512-ReEBKkIfe4ya47wlPYf/gu5ib6yUG0/Aez0JQZQz94kiWtRQvZIQbTiehsnwHvLSWJnQdhVeqYue7Id1dKr0qw==}
engines: {node: '>=10'}
dev: false
/delegates@1.0.0:
resolution: {integrity: sha512-bd2L678uiWATM6m5Z1VzNCErI3jiGzt6HGY8OVICs40JQq/HALfbyNJmp0UDakEY4pMMaN0Ly5om/B1VI/+xfQ==}
requiresBuild: true
@@ -7194,14 +7111,6 @@ packages:
safe-buffer: 5.2.1
dev: false
/evt@2.5.7:
resolution: {integrity: sha512-dr7Wd16ry5F8WNU1xXLKpFpO3HsoAGg8zC48e08vDdzMzGWCP9/QFGt1PQptEEDh8SwYP3EL8M+d/Gb0kgUp6g==}
dependencies:
minimal-polyfills: 2.2.3
run-exclusive: 2.2.19
tsafe: 1.6.6
dev: false
/execa@8.0.1:
resolution: {integrity: sha512-VyhnebXciFV2DESc+p6B+y0LjSm0krU4OgJN44qFAhBY0TJ+1V61tYD2+wHusZ6F9n5K+vl8k0sTy7PEfV4qpg==}
engines: {node: '>=16.17'}
@@ -7406,6 +7315,7 @@ packages:
/get-caller-file@2.0.5:
resolution: {integrity: sha512-DyFP3BM/3YHTQOCUL/w0OZHR0lpKeGrxotcHWcqNEdnltqFwXVfhEBQ94eIo34AfQpo0rGki4cyIiftY06h2Fg==}
engines: {node: 6.* || 8.* || >= 10.*}
dev: true
/get-east-asian-width@1.2.0:
resolution: {integrity: sha512-2nk+7SIVb14QrgXFHcm84tD4bKQz0RxPuMT8Ag5KPOq7J5fEmAg0UbXdTOSHqNuHSU28k55qnceesxXRZGzKWA==}
@@ -7438,24 +7348,6 @@ packages:
resolve-pkg-maps: 1.0.0
dev: true
/git-remote-origin-url@4.0.0:
resolution: {integrity: sha512-EAxDksNdjuWgmVW9pVvA9jQDi/dmTaiDONktIy7qiRRhBZUI4FQK1YvBvteuTSX24aNKg9lfgxNYJEeeSXe6DA==}
engines: {node: ^12.20.0 || ^14.13.1 || >=16.0.0}
dependencies:
gitconfiglocal: 2.1.0
dev: false
/git-repo-info@2.1.1:
resolution: {integrity: sha512-8aCohiDo4jwjOwma4FmYFd3i97urZulL8XL24nIPxuE+GZnfsAyy/g2Shqx6OjUiFKUXZM+Yy+KHnOmmA3FVcg==}
engines: {node: '>= 4.0'}
dev: false
/gitconfiglocal@2.1.0:
resolution: {integrity: sha512-qoerOEliJn3z+Zyn1HW2F6eoYJqKwS6MgC9cztTLUB/xLWX8gD/6T60pKn4+t/d6tP7JlybI7Z3z+I572CR/Vg==}
dependencies:
ini: 1.3.8
dev: false
/glob-parent@5.1.2:
resolution: {integrity: sha512-AOIgSQCepiJYwP3ARnGx+5VnTu2HBYdzbGP45eLw1vr3zB3vZLeyed1sC9hnbcOc9/SrMyM5RPQrkGz4aS9Zow==}
engines: {node: '>= 6'}
@@ -8024,6 +7916,11 @@ packages:
resolve-cwd: 3.0.0
dev: false
/indent-string@4.0.0:
resolution: {integrity: sha512-EdDDZu4A2OyIK7Lr/2zG+w5jmbuk1DVBnEwREQvBzspBJkCEbRa8GxU1lghYcaGJCnRWibjDXlq779X1/y5xwg==}
engines: {node: '>=8'}
dev: false
/inflight@1.0.6:
resolution: {integrity: sha512-k92I/b08q4wvFscXCLvqfsHCrjrF7yiXsQuIVvVE7N82W3+aqpzuUdBbfhWcy/FZR3/4IgflMgKLOsvPDrGCJA==}
requiresBuild: true
@@ -8383,10 +8280,6 @@ packages:
engines: {node: '>= 8'}
dev: false
/layerr@2.1.0:
resolution: {integrity: sha512-xDD9suWxfBYeXgqffRVH/Wqh+mqZrQcqPRn0I0ijl7iJQ7vu8gMGPt1Qop59pEW/jaIDNUN7+PX1Qk40+vuflg==}
dev: false
/leac@0.6.0:
resolution: {integrity: sha512-y+SqErxb8h7nE/fiEX07jsbuhrpO9lL8eca7/Y1nuWV2moNlXhyd59iDGcRf6moVyDMbmTNzL40SUyrFU/yDpg==}
dev: false
@@ -8540,6 +8433,11 @@ packages:
yallist: 4.0.0
dev: false
/luxon@3.4.4:
resolution: {integrity: sha512-zobTr7akeGHnv7eBOXcRgMeCP6+uyYsczwmeRCauvpvaAltgNyTbLH/+VaEAPUeWBT+1GuNmz4wC/6jtQzbbVA==}
engines: {node: '>=12'}
dev: false
/magic-string@0.30.10:
resolution: {integrity: sha512-iIRwTIf0QKV3UAnYK4PU8uiEc4SRh5jX0mwpIwETPpHdhVM4f53RSwS/vXvN1JhGX+Cs7B8qIq3d6AH49O5fAQ==}
dependencies:
@@ -9054,10 +8952,6 @@ packages:
dev: false
optional: true
/minimal-polyfills@2.2.3:
resolution: {integrity: sha512-oxdmJ9cL+xV72h0xYxp4tP2d5/fTBpP45H8DIOn9pASuF8a3IYTf+25fMGDYGiWW+MFsuog6KD6nfmhZJQ+uUw==}
dev: false
/minimalistic-assert@1.0.1:
resolution: {integrity: sha512-UtJcAD4yEaGtjPezWuO9wC4nwUnVH/8/Im3yEHQP4b67cXlD/Qr9hdITCU1xDbSEXg2XKNaP8jsReV7vQd00/A==}
dev: false
@@ -9516,6 +9410,13 @@ packages:
p-limit: 3.1.0
dev: false
/p-map@4.0.0:
resolution: {integrity: sha512-/bjOqmgETBYB5BoEeGVea8dmvHb2m9GLy1E9W43yeyfP6QQCZGFNa+XRceJEuDB6zqr+gKpIAmlLebMpykw/MQ==}
engines: {node: '>=10'}
dependencies:
aggregate-error: 3.1.0
dev: false
/p-memoize@4.0.4:
resolution: {integrity: sha512-ijdh0DP4Mk6J4FXlOM6vPPoCjPytcEseW8p/k5SDTSSfGV3E9bpt9Yzfifvzp6iohIieoLTkXRb32OWV0fB2Lw==}
engines: {node: '>=10'}
@@ -9669,6 +9570,83 @@ packages:
resolution: {integrity: sha512-XIxfHpEuSJbITd1H3EeQwpcZbTLHc+VVr8ANI9t5sit565tsI4/xK3KWTUFE2e6QiangUkh3B0jihzmGnNrRsQ==}
dev: false
/pg-boss@9.0.3:
resolution: {integrity: sha512-cUWUiv3sr563yNy0nCZ25Tv5U0m59Y9MhX/flm0vTR012yeVCrqpfboaZP4xFOQPdWipMJpuu4g94HR0SncTgw==}
engines: {node: '>=16'}
dependencies:
cron-parser: 4.9.0
delay: 5.0.0
lodash.debounce: 4.0.8
p-map: 4.0.0
pg: 8.11.5
serialize-error: 8.1.0
uuid: 9.0.1
transitivePeerDependencies:
- pg-native
dev: false
/pg-cloudflare@1.1.1:
resolution: {integrity: sha512-xWPagP/4B6BgFO+EKz3JONXv3YDgvkbVrGw2mTo3D6tVDQRh1e7cqVGvyR3BE+eQgAvx1XhW/iEASj4/jCWl3Q==}
requiresBuild: true
dev: false
optional: true
/pg-connection-string@2.6.4:
resolution: {integrity: sha512-v+Z7W/0EO707aNMaAEfiGnGL9sxxumwLl2fJvCQtMn9Fxsg+lPpPkdcyBSv/KFgpGdYkMfn+EI1Or2EHjpgLCA==}
dev: false
/pg-int8@1.0.1:
resolution: {integrity: sha512-WCtabS6t3c8SkpDBUlb1kjOs7l66xsGdKpIPZsg4wR+B3+u9UAum2odSsF9tnvxg80h4ZxLWMy4pRjOsFIqQpw==}
engines: {node: '>=4.0.0'}
dev: false
/pg-pool@3.6.2(pg@8.11.5):
resolution: {integrity: sha512-Htjbg8BlwXqSBQ9V8Vjtc+vzf/6fVUuak/3/XXKA9oxZprwW3IMDQTGHP+KDmVL7rtd+R1QjbnCFPuTHm3G4hg==}
peerDependencies:
pg: '>=8.0'
dependencies:
pg: 8.11.5
dev: false
/pg-protocol@1.6.1:
resolution: {integrity: sha512-jPIlvgoD63hrEuihvIg+tJhoGjUsLPn6poJY9N5CnlPd91c2T18T/9zBtLxZSb1EhYxBRoZJtzScCaWlYLtktg==}
dev: false
/pg-types@2.2.0:
resolution: {integrity: sha512-qTAAlrEsl8s4OiEQY69wDvcMIdQN6wdz5ojQiOy6YRMuynxenON0O5oCpJI6lshc6scgAY8qvJ2On/p+CXY0GA==}
engines: {node: '>=4'}
dependencies:
pg-int8: 1.0.1
postgres-array: 2.0.0
postgres-bytea: 1.0.0
postgres-date: 1.0.7
postgres-interval: 1.2.0
dev: false
/pg@8.11.5:
resolution: {integrity: sha512-jqgNHSKL5cbDjFlHyYsCXmQDrfIX/3RsNwYqpd4N0Kt8niLuNoRNH+aazv6cOd43gPh9Y4DjQCtb+X0MH0Hvnw==}
engines: {node: '>= 8.0.0'}
peerDependencies:
pg-native: '>=3.0.1'
peerDependenciesMeta:
pg-native:
optional: true
dependencies:
pg-connection-string: 2.6.4
pg-pool: 3.6.2(pg@8.11.5)
pg-protocol: 1.6.1
pg-types: 2.2.0
pgpass: 1.0.5
optionalDependencies:
pg-cloudflare: 1.1.1
dev: false
/pgpass@1.0.5:
resolution: {integrity: sha512-FdW9r/jQZhSeohs1Z3sI1yxFQNFvMcnmfuj4WBMUTxOrAyLMaTcE1aAMBiTlbMNaXvBCQuVi0R7hd8udDSP7ug==}
dependencies:
split2: 4.2.0
dev: false
/picocolors@1.0.1:
resolution: {integrity: sha512-anP1Z8qwhkbmu7MFP5iTt+wQKXgwzf7zTyGlcdzabySa9vd0Xt392U0rVmz9poOaBj0uHJKyyo9/upk0HrEQew==}
@@ -9800,6 +9778,28 @@ packages:
picocolors: 1.0.1
source-map-js: 1.2.0
/postgres-array@2.0.0:
resolution: {integrity: sha512-VpZrUqU5A69eQyW2c5CA1jtLecCsN2U/bD6VilrFDWq5+5UIEVO7nazS3TEcHf1zuPYO/sqGvUvW62g86RXZuA==}
engines: {node: '>=4'}
dev: false
/postgres-bytea@1.0.0:
resolution: {integrity: sha512-xy3pmLuQqRBZBXDULy7KbaitYqLcmxigw14Q5sj8QBVLqEwXfeybIKVWiqAXTlcvdvb0+xkOtDbfQMOf4lST1w==}
engines: {node: '>=0.10.0'}
dev: false
/postgres-date@1.0.7:
resolution: {integrity: sha512-suDmjLVQg78nMK2UZ454hAG+OAW+HQPZ6n++TNDUX+L0+uUlLywnoxJKDou51Zm+zTCjrCl0Nq6J9C5hP9vK/Q==}
engines: {node: '>=0.10.0'}
dev: false
/postgres-interval@1.2.0:
resolution: {integrity: sha512-9ZhXKM/rw350N1ovuWHbGxnGh/SNJ4cnxHiM0rxE4VN41wsg8P8zWn9hv/buK00RP4WvlOyr/RBDiptyxVbkZQ==}
engines: {node: '>=0.10.0'}
dependencies:
xtend: 4.0.2
dev: false
/preact-render-to-string@5.2.6(preact@10.21.0):
resolution: {integrity: sha512-JyhErpYOvBV1hEPwIxc/fHWXPfnEGdRKxc8gFdAZ7XV4tlzyzG847XAyEZqoDnynP88akM4eaHcSOzNcLWFguw==}
peerDependencies:
@@ -10725,12 +10725,6 @@ packages:
engines: {node: '>=0.12.0'}
dev: true
/run-exclusive@2.2.19:
resolution: {integrity: sha512-K3mdoAi7tjJ/qT7Flj90L7QyPozwUaAG+CVhkdDje4HLKXUYC3N/Jzkau3flHVDLQVhiHBtcimVodMjN9egYbA==}
dependencies:
minimal-polyfills: 2.2.3
dev: false
/run-parallel@1.2.0:
resolution: {integrity: sha512-5l4VyZR86LZ/lDxZTR6jqL8AFE2S0IFLMP26AbjsLVADxHdhB/c0GUsH+y39UfCi3dzz8OlQuPmnaJOMoDHQBA==}
dependencies:
@@ -10789,6 +10783,13 @@ packages:
hasBin: true
dev: false
/serialize-error@8.1.0:
resolution: {integrity: sha512-3NnuWfM6vBYoy5gZFvHiYsVbafvI9vZv/+jlIigFn4oP4zjNPK3LhcY0xSCgeb1a5L8jO71Mit9LlNoi2UfDDQ==}
engines: {node: '>=10'}
dependencies:
type-fest: 0.20.2
dev: false
/server-only@0.0.1:
resolution: {integrity: sha512-qepMx2JxAa5jjfzxG79yPPq+8BuFToHd1hm7kI+Z4zAq1ftQiP7HcxMhDDItrbtwVeLg/cY2JnKnrcFkmiswNA==}
dev: false
@@ -10942,10 +10943,6 @@ packages:
is-fullwidth-code-point: 5.0.0
dev: true
/slug@6.1.0:
resolution: {integrity: sha512-x6vLHCMasg4DR2LPiyFGI0gJJhywY6DTiGhCrOMzb3SOk/0JVLIaL4UhyFSHu04SD3uAavrKY/K3zZ3i6iRcgA==}
dev: false
/source-map-js@1.2.0:
resolution: {integrity: sha512-itJW8lvSA0TXEphiRoawsCksnlf8SyvmFzIhltqAHluXd88pkCd+cXJVHTDwdCr0IzwptSm035IHQktUu1QUMg==}
engines: {node: '>=0.10.0'}
@@ -10973,6 +10970,11 @@ packages:
/space-separated-tokens@2.0.2:
resolution: {integrity: sha512-PEGlAwrG8yXGXRjW32fGbg66JAlOAwbObuqVoJpv/mRgoWDQfgH1wDPvtzWyUSNAXBGSk8h755YDbbcEy3SH2Q==}
/split2@4.2.0:
resolution: {integrity: sha512-UcjcJOWknrNkF6PLX83qcHM6KHgVKNkV62Y8a5uYDVv9ydGQVwAHMKqHdJje1VTWpljG0WYpCDhrCdAOYH4TWg==}
engines: {node: '>= 10.x'}
dev: false
/std-env@3.7.0:
resolution: {integrity: sha512-JPbdCEQLj1w5GilpiHAx3qJvFndqybBysA3qUOnznweH4QbNYUsW/ea8QzSrnh0vNsezMMw5bcVool8lM0gwzg==}
dev: false
@@ -11128,14 +11130,6 @@ packages:
dependencies:
has-flag: 4.0.0
/supports-hyperlinks@2.3.0:
resolution: {integrity: sha512-RpsAZlpWcDwOPQA22aCH4J0t7L8JmAvsCxfOSEwm7cQs3LshN36QaTkwd70DnBOXDWGssw2eUoc8CaRWT0XunA==}
engines: {node: '>=8'}
dependencies:
has-flag: 4.0.0
supports-color: 7.2.0
dev: false
/supports-preserve-symlinks-flag@1.0.0:
resolution: {integrity: sha512-ot0WnXS9fgdkgIcePe6RHNk1WA8+muPa6cSjeR3V8K27q9BB1rTE3R1p7Hv0z1ZyAc8s6Vvv8DIyWf681MAt0w==}
engines: {node: '>= 0.4'}
@@ -11241,14 +11235,6 @@ packages:
dev: false
optional: true
/terminal-link@3.0.0:
resolution: {integrity: sha512-flFL3m4wuixmf6IfhFJd1YPiLiMuxEc8uHRM1buzIeZPm22Au2pDqBJQgdo7n1WfPU1ONFGv7YDwpFBmHGF6lg==}
engines: {node: '>=12'}
dependencies:
ansi-escapes: 5.0.0
supports-hyperlinks: 2.3.0
dev: false
/thenify-all@1.6.0:
resolution: {integrity: sha512-RNxQH/qI8/t3thXJDwcstUO4zeqo64+Uy/+sNVRBx4Xn2OX+OZ9oP+iJnNFqplFra2ZUVeKCSa2oVWi3T4uVmA==}
engines: {node: '>=0.8'}
@@ -11324,10 +11310,6 @@ packages:
resolution: {integrity: sha512-i+owkHr5RYdQxj8olUgRrqpiWH9x27PuWVfXwDmJ/n/CoF/SAa7WW1i2oUpPDMQpJ4U+bGRUcZkVq7i1m3zFCg==}
dev: false
/tsafe@1.6.6:
resolution: {integrity: sha512-gzkapsdbMNwBnTIjgO758GujLCj031IgHK/PKr2mrmkCSJMhSOR5FeOuSxKLMUoYc0vAA4RGEYYbjt/v6afD3g==}
dev: false
/tslib@1.14.1:
resolution: {integrity: sha512-Xni35NKzjgMrwevysHTCArtLDpPvye8zV/0E4EyYn43P7/7qvQwPh9BGkHewbMulVntbigmcT7rdX3BNo9wRJg==}
dev: false
@@ -11350,16 +11332,16 @@ packages:
resolution: {integrity: sha512-C3TaO7K81YvjCgQH9Q1S3R3P3BtN3RIM8n+OvX4il1K1zgE8ZhI0op7kClgkxtutIE8hQrcrHBXvIheqKUUCxw==}
dev: false
/type-fest@0.20.2:
resolution: {integrity: sha512-Ne+eE4r0/iWnpAxD852z3A+N0Bt5RN//NjJwRd2VFHEmrywxf5vsZlh4R6lixl6B+wz/8d+maTSAkN1FIkI3LQ==}
engines: {node: '>=10'}
dev: false
/type-fest@0.21.3:
resolution: {integrity: sha512-t0rzBq87m3fVcduHDUFhKmyyX+9eo6WQjZvf51Ea/M0Q7+T374Jp1aUiyUl0GKxp8M/OETVHSDvmkyPgvX+X2w==}
engines: {node: '>=10'}
dev: true
/type-fest@1.4.0:
resolution: {integrity: sha512-yGSza74xk0UG8k+pLh5oeoYirvIiWo5t0/o3zHHAO2tRDiZcxWP7fywNlXhqb6/r6sWvwi+RsyQMWhVLe4BVuA==}
engines: {node: '>=10'}
dev: false
/type-fest@4.18.2:
resolution: {integrity: sha512-+suCYpfJLAe4OXS6+PPXjW3urOS4IoP9waSiLuXfLgqZODKw/aWwASvzqE886wA0kQgGy0mIWyhd87VpqIy6Xg==}
engines: {node: '>=16'}
@@ -11377,18 +11359,6 @@ packages:
/uc.micro@2.1.0:
resolution: {integrity: sha512-ARDJmphmdvUk6Glw7y9DQ2bFkKBHwQHLi2lsaH6PPmz/Ka9sFOBsBluozhDltWmnv9u/cF6Rt87znRTPV+yp/A==}
/ulid@2.3.0:
resolution: {integrity: sha512-keqHubrlpvT6G2wH0OEfSW4mquYRcbe/J8NMmveoQOjUqmo+hXtO+ORCpWhdbZ7k72UtY61BL7haGxW6enBnjw==}
hasBin: true
dev: false
/ulidx@2.3.0:
resolution: {integrity: sha512-36piWNqcdp9hKlQewyeehCaALy4lyx3FodsCxHuV6i0YdexSkjDOubwxEVr2yi4kh62L/0MgyrxqG4K+qtovnw==}
engines: {node: '>=16'}
dependencies:
layerr: 2.1.0
dev: false
/undici-types@5.26.5:
resolution: {integrity: sha512-JlCMO+ehdEIKqlFxk6IfVoAUVmgz7cU7zD/h9XZ0qzeosSHmUJVOzSQvvYSYWXkFXC+IfLKSIffhv0sVZup6pA==}
@@ -11858,19 +11828,6 @@ packages:
dev: false
optional: true
/ws@8.17.0:
resolution: {integrity: sha512-uJq6108EgZMAl20KagGkzCKfMEjxmKvZHG7Tlq0Z6nOky7YF7aq4mOx6xK8TJ/i1LeK4Qus7INktacctDgY8Ow==}
engines: {node: '>=10.0.0'}
peerDependencies:
bufferutil: ^4.0.1
utf-8-validate: '>=5.0.2'
peerDependenciesMeta:
bufferutil:
optional: true
utf-8-validate:
optional: true
dev: false
/xtend@4.0.2:
resolution: {integrity: sha512-LKYU1iAXJXUgAXn9URjiu+MWhyUXHsvfp7mcuYm9dSUKK0/CjtrUwFAxD82/mCWbtLsGjFIad0wIsod4zrTAEQ==}
engines: {node: '>=0.4'}
@@ -11956,16 +11913,6 @@ packages:
resolution: {integrity: sha512-tT/oChyDXelLo2A+UVnlW9GU7CsvFMaEnd9kVFsaiCQonFAXd3xrHhkLYu+suwwosrAEQ746xBU+HvYtm1Zs2Q==}
dev: false
/zod-error@1.5.0:
resolution: {integrity: sha512-zzopKZ/skI9iXpqCEPj+iLCKl9b88E43ehcU+sbRoHuwGd9F1IDVGQ70TyO6kmfiRL1g4IXkjsXK+g1gLYl4WQ==}
dependencies:
zod: 3.23.8
dev: false
/zod@3.22.3:
resolution: {integrity: sha512-EjIevzuJRiRPbVH4mGc8nApb/lVLKVpmUhAaR5R5doKGfAnGJ6Gr3CViAVjP+4FWSxCsybeWQdcgCtbX+7oZug==}
dev: false
/zod@3.23.8:
resolution: {integrity: sha512-XBx9AXhXktjUqnepgTiE5flcKIYWi/rme0Eaj+5Y0lftuGBq+jyRu/md4WnuxqgP1ubdpNCsYEYPxrzVHD8d6g==}
dev: false
-17
View File
@@ -1,17 +0,0 @@
import { client } from "@/trigger";
import { createAppRoute } from "@trigger.dev/nextjs";
import "@/jobs/auth-verification-email";
import "@/jobs/esign-confirmation-email";
import "@/jobs/esign-email";
import "@/jobs/esign-pdf";
import "@/jobs/member-inivite-email";
import "@/jobs/password-reset-email";
import "@/jobs/share-data-room-email";
import "@/jobs/share-update-email";
//this route is used to send and receive data with Trigger.dev
export const { POST, dynamic } = createAppRoute(client);
//uncomment this to set a higher max duration (it must be inside your plan limits). Full docs: https://vercel.com/docs/functions/serverless-functions/runtimes#max-duration
//export const maxDuration = 60;
+11 -12
View File
@@ -1,4 +1,5 @@
"use client";
import { fileType } from "@/lib/mime";
import {
RiFileCloudFill,
@@ -16,54 +17,52 @@ type FileIconProps = {
};
const FileIcon = ({ type }: FileIconProps) => {
switch (true) {
case type.includes("image"):
const _type = fileType(type);
switch (_type) {
case "image":
return (
<div className="flex h-8 w-8 items-center justify-center rounded-md bg-pink-100">
<RiFileImageFill className="h-5 w-5 text-pink-500" />
</div>
);
case type.includes("audio"):
case "audio":
return (
<div className="flex h-8 w-8 items-center justify-center rounded-md bg-indigo-100">
<RiFileMusicFill className="h-5 w-5 text-indigo-500" />
</div>
);
case type.includes("video"):
case "video":
return (
<div className="flex h-8 w-8 items-center justify-center rounded-md bg-rose-100">
<RiFileVideoFill className="h-5 w-5 text-rose-500" />
</div>
);
case type.includes("powerpoint") || type.includes("presentation"):
case "powerpoint":
return (
<div className="flex h-8 w-8 items-center justify-center rounded-md bg-orange-100">
<RiFilePptFill className="h-5 w-5 text-orange-500" />
</div>
);
case type.includes("rtf"):
case type.includes("doc"):
case type.includes("word"):
case "doc":
return (
<div className="flex h-8 w-8 items-center justify-center rounded-md bg-blue-100">
<RiFileWordFill className="h-5 w-5 text-blue-500" />
</div>
);
case type.includes("csv"):
case type.includes("excel"):
case type.includes("sheet"):
case "excel":
return (
<div className="flex h-8 w-8 items-center justify-center rounded-md bg-emerald-100">
<RiFileExcelFill className="h-5 w-5 text-emerald-500" />
</div>
);
case type.includes("pdf"):
case "pdf":
return (
<div className="flex h-8 w-8 items-center justify-center rounded-md bg-red-100">
<RiFilePdf2Fill className="h-5 w-5 text-red-500" />
+17
View File
@@ -0,0 +1,17 @@
type OfficeViewerProps = {
url: string;
};
const OfficeViewer = ({ url }: OfficeViewerProps) => {
return (
// biome-ignore lint/a11y/useIframeTitle: <explanation>
<iframe
src={`https://view.officeapps.live.com/op/view.aspx?src=${encodeURIComponent(
url,
)}`}
className="w-full h-full min-h-screen"
/>
);
};
export { OfficeViewer };
+15 -6
View File
@@ -1,6 +1,8 @@
import EmptyState from "@/components/common/empty-state";
import { OfficeViewer } from "@/components/file/office-viewer";
import { Button } from "@/components/ui/button";
import { PdfViewer } from "@/components/ui/pdf-viewer";
import { fileType } from "@/lib/mime";
import { RiFileUnknowFill as UnknownFileIcon } from "@remixicon/react";
type FilePreviewProps = {
@@ -15,6 +17,7 @@ const ImagePreview = ({ url, name }: FilePreviewProps) => {
const AuditPreview = ({ url, name, mimeType }: FilePreviewProps) => {
return (
// biome-ignore lint/a11y/useMediaCaption: <explanation>
<audio controls className="w-full">
<source src={url} type={mimeType} />
Your browser does not support the audio element.
@@ -24,6 +27,7 @@ const AuditPreview = ({ url, name, mimeType }: FilePreviewProps) => {
const VideoPreview = ({ url, name, mimeType }: FilePreviewProps) => {
return (
// biome-ignore lint/a11y/useMediaCaption: <explanation>
<video controls className="w-full rounded">
<source src={url} type={mimeType} />
Your browser does not support the video type.
@@ -34,7 +38,7 @@ const VideoPreview = ({ url, name, mimeType }: FilePreviewProps) => {
const UnknownPreview = ({ url, name, mimeType }: FilePreviewProps) => {
return (
<EmptyState
title="File type not supported"
title="Preview not available"
subtitle={`This file type - ${mimeType} is not yet supported by the previewer. You can download the file by clicking the button below.`}
icon={<UnknownFileIcon />}
>
@@ -47,16 +51,21 @@ const UnknownPreview = ({ url, name, mimeType }: FilePreviewProps) => {
const FilePreview = ({ url, name, mimeType }: FilePreviewProps) => {
mimeType = mimeType || "";
const type = fileType(mimeType);
switch (true) {
case mimeType.includes("pdf"):
switch (type) {
case "pdf":
return <PdfViewer file={url} />;
case mimeType.startsWith("image"):
case "image":
return <ImagePreview url={url} name={name} />;
case mimeType.startsWith("audio"):
case "audio":
return <AuditPreview url={url} name={name} mimeType={mimeType} />;
case mimeType.startsWith("video"):
case "video":
return <VideoPreview url={url} name={name} mimeType={mimeType} />;
case "doc":
case "excel":
case "powerpoint":
return <OfficeViewer url={url} />;
default:
return <UnknownPreview url={url} name={name} mimeType={mimeType} />;
}
+11
View File
@@ -0,0 +1,11 @@
export const JOB_TYPES = {
email: [
"auth-verify",
"share-update",
"share-data-room",
"password-reset",
"esign-confirmation",
"esign-notification",
],
generate: ["esign-pdf"],
} as const;
+2 -2
View File
@@ -10,9 +10,9 @@ import {
Text,
} from "jsx-email";
import { type TConfirmationEmailPayload } from "../jobs/esign-confirmation-email";
import type { ConfirmationEmailPayloadType } from "../jobs/esign-confirmation-email";
type Payload = Omit<TConfirmationEmailPayload, "fileUrl">;
type Payload = Omit<ConfirmationEmailPayloadType, "fileUrl">;
const ESignConfirmationEmail = ({
documentName,
+2 -2
View File
@@ -1,4 +1,4 @@
import type { TEmailPayload } from "@/jobs/esign-email";
import type { EsignEmailPayloadType } from "@/jobs/esign-email";
import {
Body,
Button,
@@ -21,7 +21,7 @@ import {
UserAvatarIcon,
} from "../components/common/icons";
type TEsignEmailProps = TEmailPayload & { signingLink: string };
type TEsignEmailProps = EsignEmailPayloadType & { signingLink: string };
const EsignEmail = ({
signingLink,
-12
View File
@@ -59,11 +59,6 @@ export const env = createEnv({
// google
GOOGLE_CLIENT_ID: z.string().optional(),
GOOGLE_CLIENT_SECRET: z.string().optional(),
// trigger.dev
TRIGGER_API_ID: z.string(),
TRIGGER_API_KEY: z.string().optional(),
TRIGGER_API_URL: z.string().optional(),
},
/**
@@ -76,7 +71,6 @@ export const env = createEnv({
NEXT_PUBLIC_BASE_URL: z.string(),
NEXT_PUBLIC_NODE_ENV: z.string().default("development"),
NEXT_PUBLIC_UPLOAD_DOMAIN: z.string().optional(),
NEXT_PUBLIC_TRIGGER_PUBLIC_API_KEY: z.string().optional(),
},
/**
@@ -111,12 +105,6 @@ export const env = createEnv({
GOOGLE_CLIENT_ID: process.env.GOOGLE_CLIENT_ID,
GOOGLE_CLIENT_SECRET: process.env.GOOGLE_CLIENT_SECRET,
TRIGGER_API_ID: process.env.TRIGGER_API_ID,
TRIGGER_API_KEY: process.env.TRIGGER_API_KEY,
TRIGGER_API_URL: process.env.TRIGGER_API_URL,
NEXT_PUBLIC_TRIGGER_PUBLIC_API_KEY:
process.env.NEXT_PUBLIC_TRIGGER_PUBLIC_API_KEY,
},
/**
* Run `build` or `dev` with `SKIP_ENV_VALIDATION` to skip env validation. This is especially
+7
View File
@@ -0,0 +1,7 @@
export async function register() {
if (process.env.NEXT_RUNTIME === "nodejs") {
const { initPgBoss } = await import("./lib/pg-boss-init");
await initPgBoss();
}
}
+13 -26
View File
@@ -1,20 +1,17 @@
import AccountVerificationEmail from "@/emails/AccountVerificationEmail";
import { env } from "@/env";
import { BaseJob } from "@/lib/pg-boss-base";
import { sendMail } from "@/server/mailer";
import { client } from "@/trigger";
import { eventTrigger } from "@trigger.dev/sdk";
import { render } from "jsx-email";
import { z } from "zod";
import type { Job } from "pg-boss";
const schema = z.object({
email: z.string().email(),
token: z.string(),
});
export type TAuthVerificationPayloadSchema = z.infer<typeof schema>;
export type AuthVerificationPayloadType = {
email: string;
token: string;
};
export const sendAuthVerificationEmail = async (
payload: TAuthVerificationPayloadSchema,
payload: AuthVerificationPayloadType,
) => {
const { email, token } = payload;
const baseUrl = env.BASE_URL;
@@ -34,20 +31,10 @@ export const sendAuthVerificationEmail = async (
});
};
export const triggerName = "email.auth-verify";
export class AuthVerificationEmailJob extends BaseJob<AuthVerificationPayloadType> {
readonly type = "email.auth-verify";
client.defineJob({
id: "auth-verification-email",
name: "authentication verification email",
version: "0.0.1",
trigger: eventTrigger({
name: triggerName,
schema,
}),
run: async (payload, io) => {
await io.runTask("send auth verification email", async () => {
await sendAuthVerificationEmail(payload);
});
},
});
async work(job: Job<AuthVerificationPayloadType>): Promise<void> {
await sendAuthVerificationEmail(job.data);
}
}
+10 -17
View File
@@ -1,10 +1,10 @@
import ESignConfirmationEmail from "@/emails/EsignConfirmationEmail";
import { BaseJob } from "@/lib/pg-boss-base";
import { sendMail } from "@/server/mailer";
import { client } from "@/trigger";
import { eventTrigger } from "@trigger.dev/sdk";
import { render } from "jsx-email";
import type { Job } from "pg-boss";
export type TConfirmationEmailPayload = {
export type ConfirmationEmailPayloadType = {
fileUrl: string;
documentName: string;
senderName: string | null;
@@ -16,7 +16,7 @@ export type TConfirmationEmailPayload = {
};
export const sendEsignConfirmationEmail = async (
payload: TConfirmationEmailPayload,
payload: ConfirmationEmailPayloadType,
) => {
const html = await render(
ESignConfirmationEmail({
@@ -39,17 +39,10 @@ export const sendEsignConfirmationEmail = async (
});
};
client.defineJob({
id: "esign-confirmation-email",
name: "esign confirmation email",
version: "0.0.1",
trigger: eventTrigger({
name: "esign.send-confirmation",
}),
export class EsignConfirmationEmailJob extends BaseJob<ConfirmationEmailPayloadType> {
readonly type = "email.esign-confirmation";
run: async (payload: TConfirmationEmailPayload, io) => {
await io.runTask("send confirmation email", async () => {
await sendEsignConfirmationEmail(payload);
});
},
});
async work(job: Job<ConfirmationEmailPayloadType>): Promise<void> {
await sendEsignConfirmationEmail(job.data);
}
}
+21 -29
View File
@@ -1,12 +1,12 @@
import EsignEmail from "@/emails/EsignEmail";
import { env } from "@/env";
import { BaseJob } from "@/lib/pg-boss-base";
import { db } from "@/server/db";
import { sendMail } from "@/server/mailer";
import { client } from "@/trigger";
import { eventTrigger } from "@trigger.dev/sdk";
import { render } from "jsx-email";
import type { Job } from "pg-boss";
export interface TEmailPayload {
export interface EsignEmailPayloadType {
documentName?: string;
message?: string | null;
recipient: {
@@ -24,14 +24,15 @@ export interface TEmailPayload {
};
}
interface TCorePayload {
interface AdditionalPayloadType {
email: string;
token: string;
}
export type TEsignEmailJob = TEmailPayload & TCorePayload;
export type ExtendedEsignPayloadType = EsignEmailPayloadType &
AdditionalPayloadType;
export const sendEsignEmail = async (payload: TEsignEmailJob) => {
export const sendEsignEmail = async (payload: ExtendedEsignPayloadType) => {
const { email, token, ...rest } = payload;
const baseUrl = env.BASE_URL;
const html = await render(
@@ -42,33 +43,24 @@ export const sendEsignEmail = async (payload: TEsignEmailJob) => {
);
await sendMail({
to: email,
subject: "esign Link",
subject: "eSign Link",
html,
});
};
client.defineJob({
id: "esign-notification-email",
name: "E-sign notification email",
version: "0.0.1",
trigger: eventTrigger({
name: "email.esign",
}),
export class EsignNotificationEmailJob extends BaseJob<ExtendedEsignPayloadType> {
readonly type = "email.esign-notification";
run: async (payload: TEsignEmailJob, io) => {
await io.runTask("send esign email", async () => {
await sendEsignEmail(payload);
async work(job: Job<ExtendedEsignPayloadType>): Promise<void> {
await db.esignRecipient.update({
where: {
id: job.data.recipient.id,
},
data: {
status: "SENT",
},
});
await io.runTask("update recipient status", async () => {
await db.esignRecipient.update({
where: {
id: payload.recipient.id,
},
data: {
status: "SENT",
},
});
});
},
});
await sendEsignEmail(job.data);
}
}
+64 -80
View File
@@ -1,50 +1,43 @@
import { BaseJob } from "@/lib/pg-boss-base";
import { db } from "@/server/db";
import {
type TEsignGetTemplate,
type EsignGetTemplateType,
completeEsignDocuments,
generateEsignPdf,
uploadEsignDocuments,
} from "@/server/esign";
import { getPresignedGetUrl } from "@/server/file-uploads";
import { client } from "@/trigger";
import { eventTrigger } from "@trigger.dev/sdk";
import { z } from "zod";
import { type TConfirmationEmailPayload } from "./esign-confirmation-email";
import type { Job } from "pg-boss";
import {
type ConfirmationEmailPayloadType,
EsignConfirmationEmailJob,
} from "./esign-confirmation-email";
const schema = z.object({
bucketKey: z.string(),
data: z.record(z.string()),
audits: z.array(z.object({ id: z.string(), summary: z.string() })),
fields: z.array(z.any()),
companyId: z.string(),
templateName: z.string(),
requestIp: z.string(),
templateId: z.string(),
uploaderName: z.string(),
userAgent: z.string(),
recipients: z.array(
z.object({ email: z.string(), name: z.string().nullish() }),
),
company: z.object({
name: z.string(),
logo: z.string().nullish(),
}),
});
export type TESignPdfSchema = Omit<z.infer<typeof schema>, "fields"> & {
fields: TEsignGetTemplate["fields"];
export type EsignPdfPayloadType = {
fields: EsignGetTemplateType["fields"];
company: {
name: string;
logo?: string | null | undefined;
};
data: Record<string, string>;
templateId: string;
requestIp: string;
userAgent: string;
audits: {
id: string;
summary: string;
}[];
bucketKey: string;
templateName: string;
companyId: string;
uploaderName: string;
recipients: { email: string; name?: string | null }[];
};
client.defineJob({
id: "esign.complete-pdf",
name: "esign_complete_pdf",
version: "0.0.1",
trigger: eventTrigger({
name: "esign.sign-pdf",
schema,
}),
export class EsignPdfJob extends BaseJob<EsignPdfPayloadType> {
readonly type = "generate.esign-pdf";
run: async (payload, io) => {
async work(job: Job<EsignPdfPayloadType>): Promise<void> {
const {
bucketKey,
data,
@@ -58,55 +51,46 @@ client.defineJob({
templateId,
recipients,
company,
} = payload;
} = job.data;
const uploadedDocument = await io.runTask("upload documents", async () => {
const modifiedPdfBytes = await generateEsignPdf({
bucketKey,
data,
fields,
audits,
});
const { fileUrl: _fileUrl, ...bucketData } = await uploadEsignDocuments({
buffer: Buffer.from(modifiedPdfBytes),
const modifiedPdfBytes = await generateEsignPdf({
bucketKey,
data,
fields,
audits,
});
const { fileUrl: _fileUrl, ...bucketData } = await uploadEsignDocuments({
buffer: Buffer.from(modifiedPdfBytes),
companyId,
templateName,
});
await db.$transaction(async (tx) => {
await completeEsignDocuments({
bucketData: bucketData,
companyId,
db: tx,
requestIp,
templateId,
templateName,
});
return { bucketData, _fileUrl };
});
await io.runTask("complete document", async () => {
await db.$transaction(async (tx) => {
await completeEsignDocuments({
bucketData: uploadedDocument.bucketData,
companyId,
db: tx,
requestIp,
templateId,
templateName,
uploaderName,
userAgent,
});
uploaderName,
userAgent,
});
});
await io.runTask("send all recipients confirmation email", async () => {
const file = await getPresignedGetUrl(uploadedDocument.bucketData.key);
const file = await getPresignedGetUrl(bucketData.key);
const data: { name: string; payload: TConfirmationEmailPayload }[] =
recipients.map((recipient) => ({
payload: {
fileUrl: file.url,
documentName: templateName,
recipient,
company,
senderName: uploaderName,
},
name: "esign.send-confirmation",
}));
const recipientData: { data: ConfirmationEmailPayloadType }[] =
recipients.map((recipient) => ({
data: {
fileUrl: file.url,
documentName: templateName,
recipient,
company,
senderName: uploaderName,
},
}));
await io.sendEvents(`trigger-confirmation-email`, data);
});
},
});
await new EsignConfirmationEmailJob().bulkEmit(recipientData);
}
}
+25 -34
View File
@@ -1,29 +1,28 @@
import MemberInviteEmail from "@/emails/MemberInviteEmail";
import { env } from "@/env";
import { constants } from "@/lib/constants";
import { BaseJob } from "@/lib/pg-boss-base";
import { sendMail } from "@/server/mailer";
import { client } from "@/trigger";
import { eventTrigger } from "@trigger.dev/sdk";
import { render } from "jsx-email";
import { z } from "zod";
import type { Job } from "pg-boss";
const schema = z.object({
email: z.string().email(),
passwordResetToken: z.string(),
verificationToken: z.string(),
company: z.object({
name: z.string(),
id: z.string(),
}),
user: z.object({
email: z.string().email().nullish(),
name: z.string().nullish(),
}),
});
type MemberInvitePayloadType = {
email: string;
passwordResetToken: string;
user: {
email?: string | null | undefined;
name?: string | null | undefined;
};
verificationToken: string;
company: {
name: string;
id: string;
};
};
type TSchema = z.infer<typeof schema>;
export const sendMemberInviteEmail = async (payload: TSchema) => {
export const sendMemberInviteEmail = async (
payload: MemberInvitePayloadType,
) => {
const { email, passwordResetToken, verificationToken, company, user } =
payload;
@@ -43,24 +42,16 @@ export const sendMemberInviteEmail = async (payload: TSchema) => {
MemberInviteEmail({
inviteLink,
companyName: company.name,
invitedBy: (user.name ?? user.email)!,
invitedBy: `${user.name || user.email}`,
}),
),
});
};
client.defineJob({
id: "member-inivte-email",
name: "member invite email",
version: "0.0.1",
trigger: eventTrigger({
name: "email.member-invite",
schema,
}),
export class SendMemberInviteEmailJob extends BaseJob<MemberInvitePayloadType> {
readonly type = "email.password-reset";
run: async (payload, io) => {
await io.runTask("send member invite email", async () => {
await sendMemberInviteEmail(payload);
});
},
});
async work(job: Job<MemberInvitePayloadType>): Promise<void> {
await sendMemberInviteEmail(job.data);
}
}
+13 -26
View File
@@ -1,20 +1,17 @@
import PasswordResetEmail from "@/emails/PasswordResetEmail";
import { env } from "@/env";
import { BaseJob } from "@/lib/pg-boss-base";
import { sendMail } from "@/server/mailer";
import { client } from "@/trigger";
import { eventTrigger } from "@trigger.dev/sdk";
import { render } from "jsx-email";
import { z } from "zod";
import type { Job } from "pg-boss";
const schema = z.object({
email: z.string().email(),
token: z.string(),
});
export type TPasswordResetPayloadSchema = z.infer<typeof schema>;
export type PasswordResetPayloadType = {
email: string;
token: string;
};
export const sendPasswordResetEmail = async (
payload: TPasswordResetPayloadSchema,
payload: PasswordResetPayloadType,
) => {
const { email, token } = payload;
const baseUrl = env.BASE_URL;
@@ -34,20 +31,10 @@ export const sendPasswordResetEmail = async (
});
};
export const triggerName = "email.password-reset";
export class PasswordResetEmailJob extends BaseJob<PasswordResetPayloadType> {
readonly type = "email.password-reset";
client.defineJob({
id: "password-reset-email",
name: "password reset email",
version: "0.0.1",
trigger: eventTrigger({
name: triggerName,
schema,
}),
run: async (payload, io) => {
await io.runTask("send password reset email", async () => {
await sendPasswordResetEmail(payload);
});
},
});
async work(job: Job<PasswordResetPayloadType>): Promise<void> {
await sendPasswordResetEmail(job.data);
}
}
+17 -30
View File
@@ -1,21 +1,18 @@
import ShareDataRoomEmail from "@/emails/ShareDataRoomEmail";
import { BaseJob } from "@/lib/pg-boss-base";
import { sendMail } from "@/server/mailer";
import { client } from "@/trigger";
import { eventTrigger } from "@trigger.dev/sdk";
import { render } from "jsx-email";
import { z } from "zod";
import type { Job } from "pg-boss";
const schema = z.object({
dataRoom: z.string(),
link: z.string(),
companyName: z.string(),
recipientName: z.string().nullish(),
senderName: z.string(),
senderEmail: z.string().nullish(),
email: z.string(),
});
export type DataRoomEmailPayloadType = z.infer<typeof schema>;
export type DataRoomEmailPayloadType = {
link: string;
dataRoom: string;
email: string;
senderName: string;
companyName: string;
recipientName?: string | null | undefined;
senderEmail?: string | null | undefined;
};
export const sendShareDataRoomEmail = async (
payload: DataRoomEmailPayloadType,
@@ -45,20 +42,10 @@ export const sendShareDataRoomEmail = async (
});
};
export const triggerName = "email.share-data-room-email";
export class ShareDataRoomEmailJob extends BaseJob<DataRoomEmailPayloadType> {
readonly type = "email.share-data-room";
client.defineJob({
id: "share-data-room-email",
name: "data room share email",
version: "0.0.1",
trigger: eventTrigger({
name: triggerName,
schema,
}),
run: async (payload, io) => {
await io.runTask("send data room share email", async () => {
await sendShareDataRoomEmail(payload);
});
},
});
async work(job: Job<DataRoomEmailPayloadType>): Promise<void> {
await sendShareDataRoomEmail(job.data);
}
}
+19 -32
View File
@@ -1,23 +1,20 @@
import ShareUpdateEmail from "@/emails/ShareUpdateEmail";
import { BaseJob } from "@/lib/pg-boss-base";
import { sendMail } from "@/server/mailer";
import { client } from "@/trigger";
import { eventTrigger } from "@trigger.dev/sdk";
import { render } from "jsx-email";
import { z } from "zod";
import type { Job } from "pg-boss";
const schema = z.object({
update: z.object({
title: z.string(),
}),
link: z.string(),
companyName: z.string(),
recipientName: z.string().nullish(),
senderName: z.string(),
senderEmail: z.string().nullish(),
email: z.string(),
});
export type UpdateSharePayloadType = z.infer<typeof schema>;
export type UpdateSharePayloadType = {
update: {
title: string;
};
link: string;
companyName: string;
senderName: string;
email: string;
recipientName?: string | null | undefined;
senderEmail?: string | null | undefined;
};
export const sendShareUpdateEmail = async (payload: UpdateSharePayloadType) => {
const {
@@ -45,20 +42,10 @@ export const sendShareUpdateEmail = async (payload: UpdateSharePayloadType) => {
});
};
export const triggerName = "email.share-update-email";
export class ShareUpdateEmailJob extends BaseJob<UpdateSharePayloadType> {
readonly type = "email.share-update";
client.defineJob({
id: "share-update-email",
name: "Investor update share email",
version: "0.0.1",
trigger: eventTrigger({
name: triggerName,
schema,
}),
run: async (payload, io) => {
await io.runTask("Send investor update", async () => {
await sendShareUpdateEmail(payload);
});
},
});
async work(job: Job<UpdateSharePayloadType>): Promise<void> {
await sendShareUpdateEmail(job.data);
}
}
+29
View File
@@ -0,0 +1,29 @@
type FileProps = {
mimeType: string;
};
const fileType = (mimeType: string) => {
switch (true) {
case mimeType.includes("pdf"):
return "pdf";
case mimeType.includes("image"):
return "image";
case mimeType.includes("audio"):
return "audio";
case mimeType.includes("video"):
return "video";
case mimeType.includes("powerpoint") || mimeType.includes("presentation"):
return "powerpoint";
case mimeType.includes("csv") ||
mimeType.includes("excel") ||
mimeType.includes("sheet"):
return "excel";
case mimeType.includes("doc"):
case mimeType.includes("word"):
return "doc";
default:
return "unknown";
}
};
export { fileType };
+76
View File
@@ -0,0 +1,76 @@
import type { JOB_TYPES } from "@/constants/job";
import { env } from "@/env";
import pgBoss from "pg-boss";
import { singleton } from "./singleton";
type JobTypes = typeof JOB_TYPES;
export type JobType = {
[Key in keyof JobTypes]: `${Key}.${JobTypes[Key][number]}`;
}[keyof JobTypes];
interface Job<T extends object> {
type: JobType;
options: pgBoss.SendOptions;
start: () => Promise<void>;
work: (job: pgBoss.Job<T>) => Promise<void>;
emit: (data: T) => Promise<void>;
}
export abstract class BaseJob<T extends object> implements Job<T> {
protected boss: pgBoss;
abstract readonly type: JobType;
readonly options = { retryLimit: 3, retryDelay: 1000 };
constructor() {
this.boss = boss;
}
async start(): Promise<void> {
await this.boss.work(this.type, this.work);
}
abstract work(job: pgBoss.Job<T>): Promise<void>;
async emit(data: T): Promise<void> {
await this.boss.send(this.type, data, this.options);
}
async bulkEmit(data: Omit<pgBoss.JobInsert<T>, "name">[]): Promise<void> {
await this.boss.insert(
data.map((items) => ({ ...items, name: this.type })),
);
}
}
export class JobManager {
private readonly boss: pgBoss;
// biome-ignore lint/suspicious/noExplicitAny: <explanation>
private jobs = new Map<string, Job<any>>();
constructor(boss: pgBoss) {
this.boss = boss;
}
// biome-ignore lint/suspicious/noExplicitAny: <explanation>
register(job: new (boss: pgBoss) => Job<any>): JobManager {
const jobInstance = new job(this.boss);
this.jobs.set(jobInstance.type, jobInstance);
return this;
}
async start(): Promise<void> {
await this.boss.start();
for (const job of this.jobs.values()) {
await job.start();
}
}
}
export const boss = singleton(
"pg-boss",
() =>
new pgBoss({
connectionString: env.DATABASE_URL,
}),
);
+23
View File
@@ -0,0 +1,23 @@
import { AuthVerificationEmailJob } from "@/jobs/auth-verification-email";
import { EsignConfirmationEmailJob } from "@/jobs/esign-confirmation-email";
import { EsignNotificationEmailJob } from "@/jobs/esign-email";
import { EsignPdfJob } from "@/jobs/esign-pdf";
import { SendMemberInviteEmailJob } from "@/jobs/member-inivite-email";
import { PasswordResetEmailJob } from "@/jobs/password-reset-email";
import { ShareDataRoomEmailJob } from "@/jobs/share-data-room-email";
import { ShareUpdateEmailJob } from "@/jobs/share-update-email";
import { JobManager, boss } from "./pg-boss-base";
export async function initPgBoss() {
const jobs = new JobManager(boss)
.register(AuthVerificationEmailJob)
.register(ShareUpdateEmailJob)
.register(ShareDataRoomEmailJob)
.register(SendMemberInviteEmailJob)
.register(PasswordResetEmailJob)
.register(EsignNotificationEmailJob)
.register(EsignConfirmationEmailJob)
.register(EsignPdfJob);
await jobs.start();
}
+12
View File
@@ -0,0 +1,12 @@
// credit https://github.com/epicweb-dev/remember/blob/main/index.js
export function singleton<Value>(name: string, getValue: () => Value): Value {
// biome-ignore lint/suspicious/noExplicitAny: <explanation>
const thusly = globalThis as any;
thusly.__remember_singleton ??= new Map<string, Value>();
if (!thusly.__remember_singleton.has(name)) {
thusly.__remember_singleton.set(name, getValue());
}
// biome-ignore lint/style/noNonNullAssertion: <explanation>
return thusly.__remember_singleton.get(name)!;
}
+11 -12
View File
@@ -8,7 +8,7 @@ import { createDocumentHandler } from "@/trpc/routers/document-router/procedures
import { renderToBuffer } from "@react-pdf/renderer";
import { PDFDocument, StandardFonts } from "pdf-lib";
import { EsignAudit } from "./audit";
import { type PrismaTransactionalClient } from "./db";
import type { PrismaTransactionalClient } from "./db";
interface getEsignAuditsOptions {
templateId: string;
@@ -73,9 +73,9 @@ export function getEsignTemplate({ tx, templateId }: getEsignTemplateOptions) {
});
}
export type TEsignGetTemplate = Awaited<ReturnType<typeof getEsignTemplate>>;
export type EsignGetTemplateType = Awaited<ReturnType<typeof getEsignTemplate>>;
type Field = TEsignGetTemplate["fields"][number];
type Field = EsignGetTemplateType["fields"][number];
interface TGetFieldValue {
type: Field["type"];
id: Field["id"];
@@ -86,10 +86,9 @@ interface TGetFieldValue {
export const getFieldValue = ({ type, id, data, meta }: TGetFieldValue) => {
const value = data?.[id];
const selectValue =
meta && meta.options
? meta.options.find((val) => val.id === value)?.value
: undefined;
const selectValue = meta?.options
? meta.options.find((val) => val.id === value)?.value
: undefined;
return value
? type === "DATE"
@@ -100,10 +99,10 @@ export const getFieldValue = ({ type, id, data, meta }: TGetFieldValue) => {
: undefined;
};
export interface TGenerateEsignSignPdfOptions {
export interface GenerateEsignSignPdfOptionsType {
bucketKey: string;
data: Record<string, string>;
fields: TEsignGetTemplate["fields"];
fields: EsignGetTemplateType["fields"];
audits: TGetEsignAudits;
}
@@ -112,7 +111,7 @@ export async function generateEsignPdf({
data,
fields,
audits,
}: TGenerateEsignSignPdfOptions) {
}: GenerateEsignSignPdfOptionsType) {
const docBuffer = await getFileFromS3(bucketKey);
const pdfDoc = await PDFDocument.load(docBuffer);
@@ -244,7 +243,7 @@ export async function uploadEsignDocuments({
return data;
}
export interface TCompleteEsignDocumentsOptions {
export interface CompleteEsignDocumentsOptionsType {
templateName: string;
companyId: string;
db: PrismaTransactionalClient;
@@ -264,7 +263,7 @@ export async function completeEsignDocuments({
uploaderName,
userAgent,
bucketData,
}: TCompleteEsignDocumentsOptions) {
}: CompleteEsignDocumentsOptionsType) {
await db.template.update({
where: {
id: templateId,
-25
View File
@@ -1,25 +0,0 @@
import { TriggerClient } from "@trigger.dev/sdk";
import { env } from "./env";
const apiKey = env.TRIGGER_API_KEY;
const apiUrl = env.TRIGGER_API_URL;
const id = env.TRIGGER_API_ID;
const getClient = () =>
new TriggerClient({
id,
apiKey,
apiUrl,
});
export const client = getClient();
export function getTriggerClient(): TriggerClient | null {
if (!apiKey || !apiUrl || !id) {
return null;
}
const client = getClient();
return client;
}
@@ -1,11 +1,6 @@
import {
type TPasswordResetPayloadSchema,
sendPasswordResetEmail,
triggerName,
} from "@/jobs/password-reset-email";
import { PasswordResetEmailJob } from "@/jobs/password-reset-email";
import { generatePasswordResetToken } from "@/lib/token";
import { getUserByEmail } from "@/server/user";
import { getTriggerClient } from "@/trigger";
import { withoutAuth } from "@/trpc/api/trpc";
import { TRPCError } from "@trpc/server";
import { z } from "zod";
@@ -13,8 +8,6 @@ import { z } from "zod";
export const forgotPasswordProcedure = withoutAuth
.input(z.string().email())
.mutation(async ({ input }) => {
const trigger = getTriggerClient();
const existingUser = await getUserByEmail(input);
if (!existingUser) {
@@ -25,16 +18,7 @@ export const forgotPasswordProcedure = withoutAuth
}
const { email, token } = await generatePasswordResetToken(input);
const payload: TPasswordResetPayloadSchema = {
email,
token,
};
if (trigger) {
await trigger.sendEvent({ name: triggerName, payload });
} else {
await sendPasswordResetEmail(payload);
}
await new PasswordResetEmailJob().emit({ email, token });
return {
success: true,
@@ -1,12 +1,7 @@
import {
type TAuthVerificationPayloadSchema,
sendAuthVerificationEmail,
triggerName,
} from "@/jobs/auth-verification-email";
import { AuthVerificationEmailJob } from "@/jobs/auth-verification-email";
import { generateVerificationToken } from "@/lib/token";
import { getVerificationTokenByEmail } from "@/server/verification-token";
import { getTriggerClient } from "@/trigger";
import { withoutAuth } from "@/trpc/api/trpc";
import { TRPCError } from "@trpc/server";
import { z } from "zod";
@@ -14,7 +9,6 @@ import { z } from "zod";
export const resendEmailProcedure = withoutAuth
.input(z.string().email())
.mutation(async ({ input }) => {
const trigger = getTriggerClient();
const oldVerificationToken = await getVerificationTokenByEmail(input);
if (!oldVerificationToken) {
@@ -25,17 +19,10 @@ export const resendEmailProcedure = withoutAuth
}
const verificationToken = await generateVerificationToken(input);
const payload: TAuthVerificationPayloadSchema = {
await new AuthVerificationEmailJob().emit({
email: verificationToken.identifier,
token: verificationToken.token,
};
if (trigger) {
await trigger.sendEvent({ name: triggerName, payload });
} else {
await sendAuthVerificationEmail(payload);
}
});
return {
success: true,
message:
+3 -16
View File
@@ -1,11 +1,6 @@
import {
type TAuthVerificationPayloadSchema,
sendAuthVerificationEmail,
triggerName,
} from "@/jobs/auth-verification-email";
import { AuthVerificationEmailJob } from "@/jobs/auth-verification-email";
import { generateVerificationToken } from "@/lib/token";
import { getTriggerClient } from "@/trigger";
import { withoutAuth } from "@/trpc/api/trpc";
import { TRPCError } from "@trpc/server";
import bcrypt from "bcryptjs";
@@ -14,8 +9,6 @@ import { ZSignUpMutationSchema } from "../schema";
export const signupProcedure = withoutAuth
.input(ZSignUpMutationSchema)
.mutation(async ({ ctx, input }) => {
const trigger = getTriggerClient();
const { name, email, password } = input;
const userExists = await ctx.db.user.findFirst({
@@ -42,16 +35,10 @@ export const signupProcedure = withoutAuth
});
const verificationToken = await generateVerificationToken(email);
const payload: TAuthVerificationPayloadSchema = {
await new AuthVerificationEmailJob().emit({
email: verificationToken.identifier,
token: verificationToken.token,
};
if (trigger) {
await trigger.sendEvent({ name: triggerName, payload });
} else {
await sendAuthVerificationEmail(payload);
}
});
return {
success: true,
+4 -10
View File
@@ -2,13 +2,11 @@ import { generatePublicId } from "@/common/id";
import { env } from "@/env";
import {
type DataRoomEmailPayloadType,
sendShareDataRoomEmail,
triggerName,
ShareDataRoomEmailJob,
} from "@/jobs/share-data-room-email";
import { encode } from "@/lib/jwt";
import { ShareRecipientSchema } from "@/schema/contacts";
import { checkMembership } from "@/server/auth";
import { getTriggerClient } from "@/trigger";
import { createTRPCRouter, withAuth } from "@/trpc/api/trpc";
import type { DataRoom } from "@prisma/client";
import { z } from "zod";
@@ -169,7 +167,7 @@ export const dataRoomRouter = createTRPCRouter({
message: "Successfully updated data room",
data: room,
};
} catch (error) {
} catch (_error) {
return {
success: false,
message:
@@ -187,7 +185,6 @@ export const dataRoomRouter = createTRPCRouter({
}),
)
.mutation(async ({ ctx, input }) => {
const trigger = getTriggerClient();
const { session, db } = ctx;
const { dataRoomId, others, selectedContacts } = input;
const { name: senderName, email: senderEmail, companyId } = session.user;
@@ -254,6 +251,7 @@ export const dataRoomRouter = createTRPCRouter({
const link = `${baseUrl}/data-rooms/${dataRoom.publicId}?token=${token}`;
const payload: DataRoomEmailPayloadType = {
// biome-ignore lint/style/noNonNullAssertion: <explanation>
senderName: senderName!,
recipientName: recipient.name,
companyName: company.name,
@@ -263,11 +261,7 @@ export const dataRoomRouter = createTRPCRouter({
senderEmail,
};
if (trigger) {
await trigger.sendEvent({ name: triggerName, payload });
} else {
await sendShareDataRoomEmail(payload);
}
await new ShareDataRoomEmailJob().emit(payload);
}
};
@@ -1,9 +1,8 @@
import { sendMemberInviteEmail } from "@/jobs/member-inivite-email";
import { SendMemberInviteEmailJob } from "@/jobs/member-inivite-email";
import { generatePasswordResetToken } from "@/lib/token";
import { Audit } from "@/server/audit";
import { checkMembership } from "@/server/auth";
import { generateInviteToken, generateMemberIdentifier } from "@/server/member";
import { getTriggerClient } from "@/trigger";
import { withAuth } from "@/trpc/api/trpc";
import { TRPCError } from "@trpc/server";
import { ZodInviteMemberMutationSchema } from "../schema";
@@ -11,7 +10,6 @@ import { ZodInviteMemberMutationSchema } from "../schema";
export const inviteMemberProcedure = withAuth
.input(ZodInviteMemberMutationSchema)
.mutation(async ({ ctx, input }) => {
const trigger = getTriggerClient();
const user = ctx.session.user;
const { name, email, title } = input;
const { userAgent, requestIp, session } = ctx;
@@ -143,11 +141,7 @@ export const inviteMemberProcedure = withAuth
},
};
// if (trigger) {
// await trigger.sendEvent({ name: "email.member-invite", payload });
// } else {
await sendMemberInviteEmail(payload);
// }
await new SendMemberInviteEmailJob().emit(payload);
return { success: true };
});
@@ -1,4 +1,4 @@
import { sendMemberInviteEmail } from "@/jobs/member-inivite-email";
import { SendMemberInviteEmailJob } from "@/jobs/member-inivite-email";
import { generatePasswordResetToken } from "@/lib/token";
import { Audit } from "@/server/audit";
import { checkMembership } from "@/server/auth";
@@ -7,7 +7,6 @@ import {
generateMemberIdentifier,
revokeExistingInviteTokens,
} from "@/server/member";
import { getTriggerClient } from "@/trigger";
import { withAuth } from "@/trpc/api/trpc";
import { ZodReInviteMutationSchema } from "../schema";
@@ -15,7 +14,6 @@ export const reInviteProcedure = withAuth
.input(ZodReInviteMutationSchema)
.mutation(async ({ ctx: { session, db, requestIp, userAgent }, input }) => {
const user = session.user;
const trigger = getTriggerClient();
const { expires, memberInviteTokenHash } = await generateInviteToken();
@@ -106,11 +104,7 @@ export const reInviteProcedure = withAuth
},
};
if (trigger) {
await trigger.sendEvent({ name: "email.member-invite", payload });
} else {
await sendMemberInviteEmail(payload);
}
await new SendMemberInviteEmailJob().emit(payload);
return { success: true };
});
@@ -1,14 +1,14 @@
/* eslint-disable @typescript-eslint/prefer-for-of */
import { withAuth } from "@/trpc/api/trpc";
import { ZodAddFieldMutationSchema } from "../schema";
import type { TEsignEmailJob } from "@/jobs/esign-email";
import { sendEsignEmail } from "@/jobs/esign-email";
import {
EsignNotificationEmailJob,
type ExtendedEsignPayloadType,
} from "@/jobs/esign-email";
import { decode, encode } from "@/lib/jwt";
import { checkMembership } from "@/server/auth";
import { getTriggerClient } from "@/trigger";
import { withAuth } from "@/trpc/api/trpc";
import { TRPCError } from "@trpc/server";
import { z } from "zod";
import { ZodAddFieldMutationSchema } from "../schema";
const emailTokenPayloadSchema = z.object({
id: z.string(),
@@ -44,9 +44,8 @@ export const addFieldProcedure = withAuth
.mutation(async ({ ctx, input }) => {
try {
const user = ctx.session.user;
const triggerClient = getTriggerClient();
const mails: TEsignEmailJob[] = [];
const mails: ExtendedEsignPayloadType[] = [];
if (input.status === "COMPLETE" && (!user.email || !user.name)) {
return {
@@ -56,7 +55,7 @@ export const addFieldProcedure = withAuth
};
}
await ctx.db.$transaction(async (tx) => {
const template = await ctx.db.$transaction(async (tx) => {
const { companyId } = await checkMembership({
tx,
session: ctx.session,
@@ -182,29 +181,17 @@ export const addFieldProcedure = withAuth
}
}
}
return template;
});
if (mails.length) {
if (triggerClient) {
await triggerClient.sendEvents(
mails.map((payload) => ({
name: "email.esign",
payload,
})),
);
} else {
await Promise.all(mails.map((payload) => sendEsignEmail(payload)));
await ctx.db.$transaction(
mails.map((payload) =>
ctx.db.esignRecipient.update({
where: {
id: payload.recipient.id,
},
data: { status: "SENT" },
}),
),
);
}
new EsignNotificationEmailJob().bulkEmit(
mails.map((data) => ({
data,
singletonKey: `esign-notify-${template.id}-${data.recipient.id}`,
})),
);
}
return {
@@ -217,18 +204,12 @@ export const addFieldProcedure = withAuth
: "Your template fields has been created.",
};
} catch (error) {
if (error instanceof TRPCError) {
return {
success: false,
title: "Bad request",
message: error.message,
};
} else {
return {
success: false,
title: "Error",
message: "Uh ohh! Something went wrong. Please try again later.",
};
}
console.error(error);
return {
success: false,
title: "Error",
message: "Uh ohh! Something went wrong. Please try again later.",
};
}
});
@@ -1,33 +1,25 @@
/* eslint-disable @typescript-eslint/prefer-for-of */
import { dayjsExt } from "@/common/dayjs";
import { sendEsignConfirmationEmail } from "@/jobs/esign-confirmation-email";
import { type TEsignEmailJob, sendEsignEmail } from "@/jobs/esign-email";
import { type TESignPdfSchema } from "@/jobs/esign-pdf";
import { EsignNotificationEmailJob } from "@/jobs/esign-email";
import { EsignPdfJob } from "@/jobs/esign-pdf";
import { EsignAudit } from "@/server/audit";
import {
type TCompleteEsignDocumentsOptions,
type TGenerateEsignSignPdfOptions,
completeEsignDocuments,
generateEsignPdf,
type CompleteEsignDocumentsOptionsType,
type GenerateEsignSignPdfOptionsType,
getEsignAudits,
getEsignTemplate,
uploadEsignDocuments,
type uploadEsignDocumentsOptions,
} from "@/server/esign";
import { getPresignedGetUrl } from "@/server/file-uploads";
import { getTriggerClient } from "@/trigger";
import { withoutAuth } from "@/trpc/api/trpc";
import { EncodeEmailToken } from "../../template-field-router/procedures/add-fields";
import { ZodSignTemplateMutationSchema } from "../schema";
import { SignTemplateMutationSchema } from "../schema";
export const signTemplateProcedure = withoutAuth
.input(ZodSignTemplateMutationSchema)
.input(SignTemplateMutationSchema)
.mutation(async ({ ctx, input }) => {
const { db, requestIp, userAgent } = ctx;
const triggerClient = getTriggerClient();
await db.$transaction(async (tx) => {
const template = await getEsignTemplate({
templateId: input.templateId,
@@ -141,7 +133,7 @@ export const signTemplateProcedure = withoutAuth
companyId,
templateName,
fields: template.fields,
uploaderName: "open cap",
uploaderName: "Captable, Inc.",
data,
templateId: template.id,
db: tx,
@@ -231,7 +223,7 @@ export const signTemplateProcedure = withoutAuth
tx,
);
const payload: TEsignEmailJob = {
await new EsignNotificationEmailJob().emit({
email,
token,
message: template.message,
@@ -239,16 +231,7 @@ export const signTemplateProcedure = withoutAuth
recipient: nextDelivery,
company: template.company,
sender: template.uploader.user,
};
if (triggerClient) {
await triggerClient.sendEvent({
name: "email.esign",
payload,
});
} else {
await sendEsignEmail(payload);
}
});
}
}
});
@@ -257,9 +240,9 @@ export const signTemplateProcedure = withoutAuth
});
interface TSignPdfOptions
extends Omit<TGenerateEsignSignPdfOptions, "audits">,
extends Omit<GenerateEsignSignPdfOptionsType, "audits">,
Omit<uploadEsignDocumentsOptions, "buffer">,
Omit<TCompleteEsignDocumentsOptions, "bucketData"> {
Omit<CompleteEsignDocumentsOptionsType, "bucketData"> {
templateId: string;
company: {
name: string;
@@ -282,69 +265,20 @@ async function signPdf({
recipients,
company,
}: TSignPdfOptions) {
const trigger = getTriggerClient();
const audits = await getEsignAudits({ templateId, tx: db });
if (trigger) {
const payload: TESignPdfSchema = {
bucketKey,
data,
fields,
audits,
companyId,
requestIp,
templateId,
templateName,
uploaderName,
userAgent,
recipients,
company,
};
await trigger.sendEvent({
name: "esign.sign-pdf",
payload,
});
} else {
const modifiedPdfBytes = await generateEsignPdf({
bucketKey,
data,
fields,
audits,
});
const pdfBuffer = Buffer.from(modifiedPdfBytes);
const { fileUrl: _fileUrl, ...bucketData } = await uploadEsignDocuments({
buffer: pdfBuffer,
companyId,
templateName,
});
await completeEsignDocuments({
bucketData,
companyId,
db,
requestIp,
templateId,
templateName,
uploaderName,
userAgent,
});
const file = await getPresignedGetUrl(bucketData.key);
await Promise.all(
recipients.map((recipient) =>
sendEsignConfirmationEmail({
fileUrl: file.url,
senderName: uploaderName,
documentName: templateName,
company,
recipient,
}),
),
);
}
await new EsignPdfJob().emit({
bucketKey,
data,
fields,
audits,
companyId,
requestIp,
templateId,
templateName,
uploaderName,
userAgent,
recipients,
company,
});
}
+1 -1
View File
@@ -17,7 +17,7 @@ export const ZodGetTemplateQuerySchema = z.object({
isDraftOnly: z.boolean(),
});
export const ZodSignTemplateMutationSchema = z.object({
export const SignTemplateMutationSchema = z.object({
templateId: z.string(),
data: z.record(z.string()),
recipientId: z.string(),
@@ -1,12 +1,10 @@
import { env } from "@/env";
import {
ShareUpdateEmailJob,
type UpdateSharePayloadType,
sendShareUpdateEmail,
triggerName,
} from "@/jobs/share-update-email";
import { encode } from "@/lib/jwt";
import { ShareRecipientSchema } from "@/schema/contacts";
import { getTriggerClient } from "@/trigger";
import { withAuth } from "@/trpc/api/trpc";
import { z } from "zod";
@@ -19,7 +17,6 @@ export const shareUpdateProcedure = withAuth
}),
)
.mutation(async ({ ctx, input }) => {
const trigger = getTriggerClient();
const { session, db } = ctx;
const { updateId, others, selectedContacts } = input;
const { name: senderName, email: senderEmail, companyId } = session.user;
@@ -87,6 +84,7 @@ export const shareUpdateProcedure = withAuth
const link = `${baseUrl}/updates/${update.publicId}?token=${token}`;
const payload: UpdateSharePayloadType = {
// biome-ignore lint/style/noNonNullAssertion: <explanation>
senderName: senderName!,
recipientName: recipient.name,
companyName: company.name,
@@ -98,11 +96,7 @@ export const shareUpdateProcedure = withAuth
senderEmail,
};
if (trigger) {
await trigger.sendEvent({ name: triggerName, payload });
} else {
await sendShareUpdateEmail(payload);
}
await new ShareUpdateEmailJob().emit(payload);
}
};