Add email queue (#1)
* Add pgboss queue support * Implement queue for sending emails * Add migrations
This commit is contained in:
@@ -38,6 +38,7 @@
|
|||||||
"mime-types": "^2.1.35",
|
"mime-types": "^2.1.35",
|
||||||
"next": "^14.2.1",
|
"next": "^14.2.1",
|
||||||
"next-auth": "^4.24.6",
|
"next-auth": "^4.24.6",
|
||||||
|
"pg-boss": "^9.0.3",
|
||||||
"pnpm": "^8.15.5",
|
"pnpm": "^8.15.5",
|
||||||
"prisma": "^5.11.0",
|
"prisma": "^5.11.0",
|
||||||
"query-string": "^9.0.0",
|
"query-string": "^9.0.0",
|
||||||
|
@@ -0,0 +1,5 @@
|
|||||||
|
-- AlterEnum
|
||||||
|
ALTER TYPE "EmailStatus" ADD VALUE 'QUEUED';
|
||||||
|
|
||||||
|
-- AlterTable
|
||||||
|
ALTER TABLE "Email" ADD COLUMN "attachments" TEXT;
|
@@ -0,0 +1,2 @@
|
|||||||
|
-- AlterTable
|
||||||
|
ALTER TABLE "Email" ALTER COLUMN "latestStatus" SET DEFAULT 'QUEUED';
|
@@ -139,6 +139,7 @@ model ApiKey {
|
|||||||
}
|
}
|
||||||
|
|
||||||
enum EmailStatus {
|
enum EmailStatus {
|
||||||
|
QUEUED
|
||||||
SENT
|
SENT
|
||||||
OPENED
|
OPENED
|
||||||
CLICKED
|
CLICKED
|
||||||
@@ -158,11 +159,12 @@ model Email {
|
|||||||
subject String
|
subject String
|
||||||
text String?
|
text String?
|
||||||
html String?
|
html String?
|
||||||
latestStatus EmailStatus @default(SENT)
|
latestStatus EmailStatus @default(QUEUED)
|
||||||
teamId Int
|
teamId Int
|
||||||
domainId Int?
|
domainId Int?
|
||||||
createdAt DateTime @default(now())
|
createdAt DateTime @default(now())
|
||||||
updatedAt DateTime @updatedAt
|
updatedAt DateTime @updatedAt
|
||||||
|
attachments String?
|
||||||
team Team @relation(fields: [teamId], references: [id], onDelete: Cascade)
|
team Team @relation(fields: [teamId], references: [id], onDelete: Cascade)
|
||||||
emailEvents EmailEvent[]
|
emailEvents EmailEvent[]
|
||||||
}
|
}
|
||||||
|
@@ -38,6 +38,8 @@ export const env = createEnv({
|
|||||||
UNSEND_URL: z.string(),
|
UNSEND_URL: z.string(),
|
||||||
GOOGLE_CLIENT_ID: z.string(),
|
GOOGLE_CLIENT_ID: z.string(),
|
||||||
GOOGLE_CLIENT_SECRET: z.string(),
|
GOOGLE_CLIENT_SECRET: z.string(),
|
||||||
|
SES_QUEUE_LIMIT: z.string().transform((str) => parseInt(str, 10)),
|
||||||
|
AWS_DEFAULT_REGION: z.string().default("us-east-1"),
|
||||||
},
|
},
|
||||||
|
|
||||||
/**
|
/**
|
||||||
@@ -68,6 +70,8 @@ export const env = createEnv({
|
|||||||
UNSEND_URL: process.env.UNSEND_URL,
|
UNSEND_URL: process.env.UNSEND_URL,
|
||||||
GOOGLE_CLIENT_ID: process.env.GOOGLE_CLIENT_ID,
|
GOOGLE_CLIENT_ID: process.env.GOOGLE_CLIENT_ID,
|
||||||
GOOGLE_CLIENT_SECRET: process.env.GOOGLE_CLIENT_SECRET,
|
GOOGLE_CLIENT_SECRET: process.env.GOOGLE_CLIENT_SECRET,
|
||||||
|
SES_QUEUE_LIMIT: process.env.SES_QUEUE_LIMIT,
|
||||||
|
AWS_DEFAULT_REGION: process.env.AWS_DEFAULT_REGION,
|
||||||
},
|
},
|
||||||
/**
|
/**
|
||||||
* Run `build` or `dev` with `SKIP_ENV_VALIDATION` to skip env validation. This is especially
|
* Run `build` or `dev` with `SKIP_ENV_VALIDATION` to skip env validation. This is especially
|
||||||
|
@@ -172,12 +172,21 @@ export const emailRouter = createTRPCRouter({
|
|||||||
where: {
|
where: {
|
||||||
id: input.id,
|
id: input.id,
|
||||||
},
|
},
|
||||||
include: {
|
select: {
|
||||||
emailEvents: {
|
emailEvents: {
|
||||||
orderBy: {
|
orderBy: {
|
||||||
createdAt: "asc",
|
createdAt: "asc",
|
||||||
},
|
},
|
||||||
},
|
},
|
||||||
|
id: true,
|
||||||
|
createdAt: true,
|
||||||
|
latestStatus: true,
|
||||||
|
subject: true,
|
||||||
|
to: true,
|
||||||
|
from: true,
|
||||||
|
domainId: true,
|
||||||
|
text: true,
|
||||||
|
html: true,
|
||||||
},
|
},
|
||||||
});
|
});
|
||||||
|
|
||||||
|
@@ -1,7 +1,7 @@
|
|||||||
import { EmailContent } from "~/types";
|
import { EmailContent } from "~/types";
|
||||||
import { db } from "../db";
|
import { db } from "../db";
|
||||||
import { sendEmailThroughSes, sendEmailWithAttachments } from "../aws/ses";
|
import { UnsendApiError } from "~/server/public-api/api-error";
|
||||||
import { APP_SETTINGS } from "~/utils/constants";
|
import { queueEmail } from "./job-service";
|
||||||
|
|
||||||
export async function sendEmail(
|
export async function sendEmail(
|
||||||
emailContent: EmailContent & { teamId: number }
|
emailContent: EmailContent & { teamId: number }
|
||||||
@@ -15,72 +15,34 @@ export async function sendEmail(
|
|||||||
});
|
});
|
||||||
|
|
||||||
if (!domain) {
|
if (!domain) {
|
||||||
throw new Error(
|
throw new UnsendApiError({
|
||||||
"Domain of from email is wrong. Use the email verified by unsend"
|
code: "BAD_REQUEST",
|
||||||
);
|
message:
|
||||||
|
"Domain of from email is wrong. Use the email verified by unsend",
|
||||||
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
if (domain.status !== "SUCCESS") {
|
if (domain.status !== "SUCCESS") {
|
||||||
throw new Error("Domain is not verified");
|
throw new UnsendApiError({
|
||||||
}
|
code: "BAD_REQUEST",
|
||||||
|
message: "Domain is not verified",
|
||||||
const messageId = attachments
|
|
||||||
? await sendEmailWithAttachments({
|
|
||||||
to,
|
|
||||||
from,
|
|
||||||
subject,
|
|
||||||
text,
|
|
||||||
html,
|
|
||||||
region: domain.region,
|
|
||||||
configurationSetName: getConfigurationSetName(
|
|
||||||
domain.clickTracking,
|
|
||||||
domain.openTracking
|
|
||||||
),
|
|
||||||
attachments,
|
|
||||||
})
|
|
||||||
: await sendEmailThroughSes({
|
|
||||||
to,
|
|
||||||
from,
|
|
||||||
subject,
|
|
||||||
text,
|
|
||||||
html,
|
|
||||||
region: domain.region,
|
|
||||||
configurationSetName: getConfigurationSetName(
|
|
||||||
domain.clickTracking,
|
|
||||||
domain.openTracking
|
|
||||||
),
|
|
||||||
attachments,
|
|
||||||
});
|
|
||||||
|
|
||||||
if (messageId) {
|
|
||||||
return await db.email.create({
|
|
||||||
data: {
|
|
||||||
to,
|
|
||||||
from,
|
|
||||||
subject,
|
|
||||||
text,
|
|
||||||
html,
|
|
||||||
sesEmailId: messageId,
|
|
||||||
teamId,
|
|
||||||
domainId: domain.id,
|
|
||||||
},
|
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
}
|
|
||||||
|
|
||||||
function getConfigurationSetName(
|
const email = await db.email.create({
|
||||||
clickTracking: boolean,
|
data: {
|
||||||
openTracking: boolean
|
to,
|
||||||
) {
|
from,
|
||||||
if (clickTracking && openTracking) {
|
subject,
|
||||||
return APP_SETTINGS.SES_CONFIGURATION_FULL;
|
text,
|
||||||
}
|
html,
|
||||||
if (clickTracking) {
|
teamId,
|
||||||
return APP_SETTINGS.SES_CONFIGURATION_CLICK_TRACKING;
|
domainId: domain.id,
|
||||||
}
|
attachments: attachments ? JSON.stringify(attachments) : undefined,
|
||||||
if (openTracking) {
|
},
|
||||||
return APP_SETTINGS.SES_CONFIGURATION_OPEN_TRACKING;
|
});
|
||||||
}
|
|
||||||
|
|
||||||
return APP_SETTINGS.SES_CONFIGURATION_GENERAL;
|
queueEmail(email.id);
|
||||||
|
|
||||||
|
return email;
|
||||||
}
|
}
|
||||||
|
95
apps/web/src/server/service/job-service.ts
Normal file
95
apps/web/src/server/service/job-service.ts
Normal file
@@ -0,0 +1,95 @@
|
|||||||
|
import pgBoss from "pg-boss";
|
||||||
|
import { env } from "~/env";
|
||||||
|
import { EmailAttachment, EmailContent } from "~/types";
|
||||||
|
import { db } from "../db";
|
||||||
|
import { sendEmailThroughSes, sendEmailWithAttachments } from "../aws/ses";
|
||||||
|
import { getConfigurationSetName } from "~/utils/ses-utils";
|
||||||
|
|
||||||
|
const boss = new pgBoss({
|
||||||
|
connectionString: env.DATABASE_URL,
|
||||||
|
archiveCompletedAfterSeconds: 60 * 60 * 24, // 24 hours
|
||||||
|
deleteAfterDays: 7, // 7 days
|
||||||
|
});
|
||||||
|
let started = false;
|
||||||
|
|
||||||
|
async function getBoss() {
|
||||||
|
if (!started) {
|
||||||
|
await boss.start();
|
||||||
|
await boss.work(
|
||||||
|
"send_email",
|
||||||
|
{
|
||||||
|
teamConcurrency: env.SES_QUEUE_LIMIT,
|
||||||
|
teamSize: env.SES_QUEUE_LIMIT,
|
||||||
|
teamRefill: true,
|
||||||
|
},
|
||||||
|
executeEmail
|
||||||
|
);
|
||||||
|
started = true;
|
||||||
|
}
|
||||||
|
return boss;
|
||||||
|
}
|
||||||
|
|
||||||
|
export async function queueEmail(emailId: string) {
|
||||||
|
const boss = await getBoss();
|
||||||
|
await boss.send("send_email", { emailId, timestamp: Date.now() });
|
||||||
|
}
|
||||||
|
|
||||||
|
async function executeEmail(
|
||||||
|
job: pgBoss.Job<{ emailId: string; timestamp: number }>
|
||||||
|
) {
|
||||||
|
console.log(
|
||||||
|
`[EmailJob]: Executing email job ${job.data.emailId}, time elapsed: ${Date.now() - job.data.timestamp}ms`
|
||||||
|
);
|
||||||
|
|
||||||
|
const email = await db.email.findUnique({
|
||||||
|
where: { id: job.data.emailId },
|
||||||
|
});
|
||||||
|
|
||||||
|
const domain = email?.domainId
|
||||||
|
? await db.domain.findUnique({
|
||||||
|
where: { id: email?.domainId },
|
||||||
|
})
|
||||||
|
: null;
|
||||||
|
|
||||||
|
if (!email) {
|
||||||
|
console.log(`[EmailJob]: Email not found, skipping`);
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
const attachments: Array<EmailAttachment> = email.attachments
|
||||||
|
? JSON.parse(email.attachments)
|
||||||
|
: [];
|
||||||
|
|
||||||
|
const messageId = attachments.length
|
||||||
|
? await sendEmailWithAttachments({
|
||||||
|
to: email.to,
|
||||||
|
from: email.from,
|
||||||
|
subject: email.subject,
|
||||||
|
text: email.text ?? undefined,
|
||||||
|
html: email.html ?? undefined,
|
||||||
|
region: domain?.region ?? env.AWS_DEFAULT_REGION,
|
||||||
|
configurationSetName: getConfigurationSetName(
|
||||||
|
domain?.clickTracking ?? false,
|
||||||
|
domain?.openTracking ?? false
|
||||||
|
),
|
||||||
|
attachments,
|
||||||
|
})
|
||||||
|
: await sendEmailThroughSes({
|
||||||
|
to: email.to,
|
||||||
|
from: email.from,
|
||||||
|
subject: email.subject,
|
||||||
|
text: email.text ?? undefined,
|
||||||
|
html: email.html ?? undefined,
|
||||||
|
region: domain?.region ?? env.AWS_DEFAULT_REGION,
|
||||||
|
configurationSetName: getConfigurationSetName(
|
||||||
|
domain?.clickTracking ?? false,
|
||||||
|
domain?.openTracking ?? false
|
||||||
|
),
|
||||||
|
attachments,
|
||||||
|
});
|
||||||
|
|
||||||
|
await db.email.update({
|
||||||
|
where: { id: email.id },
|
||||||
|
data: { sesEmailId: messageId, attachments: undefined },
|
||||||
|
});
|
||||||
|
}
|
@@ -4,8 +4,10 @@ export type EmailContent = {
|
|||||||
subject: string;
|
subject: string;
|
||||||
text?: string;
|
text?: string;
|
||||||
html?: string;
|
html?: string;
|
||||||
attachments?: {
|
attachments?: Array<EmailAttachment>;
|
||||||
filename: string;
|
};
|
||||||
content: string;
|
|
||||||
}[];
|
export type EmailAttachment = {
|
||||||
|
filename: string;
|
||||||
|
content: string;
|
||||||
};
|
};
|
||||||
|
18
apps/web/src/utils/ses-utils.ts
Normal file
18
apps/web/src/utils/ses-utils.ts
Normal file
@@ -0,0 +1,18 @@
|
|||||||
|
import { APP_SETTINGS } from "./constants";
|
||||||
|
|
||||||
|
export function getConfigurationSetName(
|
||||||
|
clickTracking: boolean,
|
||||||
|
openTracking: boolean
|
||||||
|
) {
|
||||||
|
if (clickTracking && openTracking) {
|
||||||
|
return APP_SETTINGS.SES_CONFIGURATION_FULL;
|
||||||
|
}
|
||||||
|
if (clickTracking) {
|
||||||
|
return APP_SETTINGS.SES_CONFIGURATION_CLICK_TRACKING;
|
||||||
|
}
|
||||||
|
if (openTracking) {
|
||||||
|
return APP_SETTINGS.SES_CONFIGURATION_OPEN_TRACKING;
|
||||||
|
}
|
||||||
|
|
||||||
|
return APP_SETTINGS.SES_CONFIGURATION_GENERAL;
|
||||||
|
}
|
162
pnpm-lock.yaml
generated
162
pnpm-lock.yaml
generated
@@ -154,6 +154,9 @@ importers:
|
|||||||
next-auth:
|
next-auth:
|
||||||
specifier: ^4.24.6
|
specifier: ^4.24.6
|
||||||
version: 4.24.7(next@14.2.1)(react-dom@18.2.0)(react@18.2.0)
|
version: 4.24.7(next@14.2.1)(react-dom@18.2.0)(react@18.2.0)
|
||||||
|
pg-boss:
|
||||||
|
specifier: ^9.0.3
|
||||||
|
version: 9.0.3
|
||||||
pnpm:
|
pnpm:
|
||||||
specifier: ^8.15.5
|
specifier: ^8.15.5
|
||||||
version: 8.15.5
|
version: 8.15.5
|
||||||
@@ -3601,6 +3604,14 @@ packages:
|
|||||||
resolution: {integrity: sha512-j5QzrmsokwWWp6kUcJQySpbG+xfOBqqKnup3OIk1pz+kB/80SLorZ9V8zHFLO92Lcd+hbvq8bT+zOGoPkmBV0Q==}
|
resolution: {integrity: sha512-j5QzrmsokwWWp6kUcJQySpbG+xfOBqqKnup3OIk1pz+kB/80SLorZ9V8zHFLO92Lcd+hbvq8bT+zOGoPkmBV0Q==}
|
||||||
dev: false
|
dev: false
|
||||||
|
|
||||||
|
/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
|
||||||
|
|
||||||
/ajv@6.12.6:
|
/ajv@6.12.6:
|
||||||
resolution: {integrity: sha512-j3fVLgvTo527anyYyJOGTYJbG+vnnQYvE0m5mmkc1TK+nxAppkCLMIL0aZ4dblVCNoGShhm+kzE4ZUykBoMg4g==}
|
resolution: {integrity: sha512-j3fVLgvTo527anyYyJOGTYJbG+vnnQYvE0m5mmkc1TK+nxAppkCLMIL0aZ4dblVCNoGShhm+kzE4ZUykBoMg4g==}
|
||||||
dependencies:
|
dependencies:
|
||||||
@@ -3971,6 +3982,11 @@ packages:
|
|||||||
escape-string-regexp: 1.0.5
|
escape-string-regexp: 1.0.5
|
||||||
dev: true
|
dev: true
|
||||||
|
|
||||||
|
/clean-stack@2.2.0:
|
||||||
|
resolution: {integrity: sha512-4diC9HaTE+KRAMWhDhrGOECgWZxoevMc5TlkObMqNSsVU62PYzXZ/SMTjzyGAFF1YusgxGcSWTEXBhp0CPwQ1A==}
|
||||||
|
engines: {node: '>=6'}
|
||||||
|
dev: false
|
||||||
|
|
||||||
/client-only@0.0.1:
|
/client-only@0.0.1:
|
||||||
resolution: {integrity: sha512-IV3Ou0jSMzZrd3pZ48nLkT9DA7Ag1pnPzaiQhpW7c3RbcqqzvzzVu+L8gfqMp/8IM2MQtSiqaCxrrcfu8I8rMA==}
|
resolution: {integrity: sha512-IV3Ou0jSMzZrd3pZ48nLkT9DA7Ag1pnPzaiQhpW7c3RbcqqzvzzVu+L8gfqMp/8IM2MQtSiqaCxrrcfu8I8rMA==}
|
||||||
dev: false
|
dev: false
|
||||||
@@ -4037,6 +4053,13 @@ packages:
|
|||||||
is-what: 4.1.16
|
is-what: 4.1.16
|
||||||
dev: false
|
dev: false
|
||||||
|
|
||||||
|
/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-spawn@7.0.3:
|
/cross-spawn@7.0.3:
|
||||||
resolution: {integrity: sha512-iRDPJKUPVEND7dHPO8rkbOnPpyDygcDFtWjpeWNCgy8WP2rXcxXL8TskReQl6OrB2G7+UJrags1q15Fudc7G6w==}
|
resolution: {integrity: sha512-iRDPJKUPVEND7dHPO8rkbOnPpyDygcDFtWjpeWNCgy8WP2rXcxXL8TskReQl6OrB2G7+UJrags1q15Fudc7G6w==}
|
||||||
engines: {node: '>= 8'}
|
engines: {node: '>= 8'}
|
||||||
@@ -4186,6 +4209,11 @@ packages:
|
|||||||
object-keys: 1.1.1
|
object-keys: 1.1.1
|
||||||
dev: true
|
dev: true
|
||||||
|
|
||||||
|
/delay@5.0.0:
|
||||||
|
resolution: {integrity: sha512-ReEBKkIfe4ya47wlPYf/gu5ib6yUG0/Aez0JQZQz94kiWtRQvZIQbTiehsnwHvLSWJnQdhVeqYue7Id1dKr0qw==}
|
||||||
|
engines: {node: '>=10'}
|
||||||
|
dev: false
|
||||||
|
|
||||||
/dequal@2.0.3:
|
/dequal@2.0.3:
|
||||||
resolution: {integrity: sha512-0je+qPKHEMohvfRTCEo3CrPG6cAzAYgmzKyxRiYSSDkS6eGJdyVJm7WaYA5ECaAD9wLB2T4EEeymA5aFVcYXCA==}
|
resolution: {integrity: sha512-0je+qPKHEMohvfRTCEo3CrPG6cAzAYgmzKyxRiYSSDkS6eGJdyVJm7WaYA5ECaAD9wLB2T4EEeymA5aFVcYXCA==}
|
||||||
engines: {node: '>=6'}
|
engines: {node: '>=6'}
|
||||||
@@ -5397,7 +5425,6 @@ packages:
|
|||||||
/indent-string@4.0.0:
|
/indent-string@4.0.0:
|
||||||
resolution: {integrity: sha512-EdDDZu4A2OyIK7Lr/2zG+w5jmbuk1DVBnEwREQvBzspBJkCEbRa8GxU1lghYcaGJCnRWibjDXlq779X1/y5xwg==}
|
resolution: {integrity: sha512-EdDDZu4A2OyIK7Lr/2zG+w5jmbuk1DVBnEwREQvBzspBJkCEbRa8GxU1lghYcaGJCnRWibjDXlq779X1/y5xwg==}
|
||||||
engines: {node: '>=8'}
|
engines: {node: '>=8'}
|
||||||
dev: true
|
|
||||||
|
|
||||||
/inflight@1.0.6:
|
/inflight@1.0.6:
|
||||||
resolution: {integrity: sha512-k92I/b08q4wvFscXCLvqfsHCrjrF7yiXsQuIVvVE7N82W3+aqpzuUdBbfhWcy/FZR3/4IgflMgKLOsvPDrGCJA==}
|
resolution: {integrity: sha512-k92I/b08q4wvFscXCLvqfsHCrjrF7yiXsQuIVvVE7N82W3+aqpzuUdBbfhWcy/FZR3/4IgflMgKLOsvPDrGCJA==}
|
||||||
@@ -5807,6 +5834,10 @@ packages:
|
|||||||
p-locate: 5.0.0
|
p-locate: 5.0.0
|
||||||
dev: true
|
dev: true
|
||||||
|
|
||||||
|
/lodash.debounce@4.0.8:
|
||||||
|
resolution: {integrity: sha512-FT1yDzDYEoYWhnSGnpE/4Kj1fLZkDFyqRb7fNt6FdYOSxlUWAtp42Eh6Wb0rGIv/m9Bgo7x4GhQbm5Ys4SG5ow==}
|
||||||
|
dev: false
|
||||||
|
|
||||||
/lodash.merge@4.6.2:
|
/lodash.merge@4.6.2:
|
||||||
resolution: {integrity: sha512-0KpjqXRVvrYyCsX1swR/XTK0va6VQkQM6MNo7PqW77ByjAhoARA8EfrP1N4+KlKj8YS0ZUCtRT/YUuhyYDujIQ==}
|
resolution: {integrity: sha512-0KpjqXRVvrYyCsX1swR/XTK0va6VQkQM6MNo7PqW77ByjAhoARA8EfrP1N4+KlKj8YS0ZUCtRT/YUuhyYDujIQ==}
|
||||||
dev: true
|
dev: true
|
||||||
@@ -5851,6 +5882,11 @@ packages:
|
|||||||
react: 18.2.0
|
react: 18.2.0
|
||||||
dev: false
|
dev: false
|
||||||
|
|
||||||
|
/luxon@3.4.4:
|
||||||
|
resolution: {integrity: sha512-zobTr7akeGHnv7eBOXcRgMeCP6+uyYsczwmeRCauvpvaAltgNyTbLH/+VaEAPUeWBT+1GuNmz4wC/6jtQzbbVA==}
|
||||||
|
engines: {node: '>=12'}
|
||||||
|
dev: false
|
||||||
|
|
||||||
/merge2@1.4.1:
|
/merge2@1.4.1:
|
||||||
resolution: {integrity: sha512-8q7VEgMJW4J8tcfVPy8g09NcQwZdbwFEqhe/WZkoIzjn/3TGDwtOCYtXGxA3O8tPzpczCCDgv+P2P5y00ZJOOg==}
|
resolution: {integrity: sha512-8q7VEgMJW4J8tcfVPy8g09NcQwZdbwFEqhe/WZkoIzjn/3TGDwtOCYtXGxA3O8tPzpczCCDgv+P2P5y00ZJOOg==}
|
||||||
engines: {node: '>= 8'}
|
engines: {node: '>= 8'}
|
||||||
@@ -6211,6 +6247,13 @@ packages:
|
|||||||
p-limit: 3.1.0
|
p-limit: 3.1.0
|
||||||
dev: true
|
dev: true
|
||||||
|
|
||||||
|
/p-map@4.0.0:
|
||||||
|
resolution: {integrity: sha512-/bjOqmgETBYB5BoEeGVea8dmvHb2m9GLy1E9W43yeyfP6QQCZGFNa+XRceJEuDB6zqr+gKpIAmlLebMpykw/MQ==}
|
||||||
|
engines: {node: '>=10'}
|
||||||
|
dependencies:
|
||||||
|
aggregate-error: 3.1.0
|
||||||
|
dev: false
|
||||||
|
|
||||||
/p-try@2.2.0:
|
/p-try@2.2.0:
|
||||||
resolution: {integrity: sha512-R4nPAVTAU0B9D35/Gk3uJf/7XYbQcyohSKdvAxIRSNghFl4e71hVoGnBNQz9cWaXxO2I10KTC+3jMdvvoKw6dQ==}
|
resolution: {integrity: sha512-R4nPAVTAU0B9D35/Gk3uJf/7XYbQcyohSKdvAxIRSNghFl4e71hVoGnBNQz9cWaXxO2I10KTC+3jMdvvoKw6dQ==}
|
||||||
engines: {node: '>=6'}
|
engines: {node: '>=6'}
|
||||||
@@ -6273,6 +6316,83 @@ packages:
|
|||||||
engines: {node: '>=8'}
|
engines: {node: '>=8'}
|
||||||
dev: true
|
dev: true
|
||||||
|
|
||||||
|
/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.0:
|
/picocolors@1.0.0:
|
||||||
resolution: {integrity: sha512-1fygroTLlHu66zi26VoTDv8yRgm0Fccecssto+MhsZ0D/DGW2sm8E8AjW7NU5VVTRt5GxbeZ5qBuJr+HyLYkjQ==}
|
resolution: {integrity: sha512-1fygroTLlHu66zi26VoTDv8yRgm0Fccecssto+MhsZ0D/DGW2sm8E8AjW7NU5VVTRt5GxbeZ5qBuJr+HyLYkjQ==}
|
||||||
|
|
||||||
@@ -6385,6 +6505,28 @@ packages:
|
|||||||
picocolors: 1.0.0
|
picocolors: 1.0.0
|
||||||
source-map-js: 1.1.0
|
source-map-js: 1.1.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.3(preact@10.11.3):
|
/preact-render-to-string@5.2.3(preact@10.11.3):
|
||||||
resolution: {integrity: sha512-aPDxUn5o3GhWdtJtW0svRC2SS/l8D9MAgo2+AWml+BhDImb27ALf04Q2d+AHqUUOc6RdSXFIBVa2gxzgMKgtZA==}
|
resolution: {integrity: sha512-aPDxUn5o3GhWdtJtW0svRC2SS/l8D9MAgo2+AWml+BhDImb27ALf04Q2d+AHqUUOc6RdSXFIBVa2gxzgMKgtZA==}
|
||||||
peerDependencies:
|
peerDependencies:
|
||||||
@@ -6856,6 +6998,13 @@ packages:
|
|||||||
lru-cache: 6.0.0
|
lru-cache: 6.0.0
|
||||||
dev: true
|
dev: true
|
||||||
|
|
||||||
|
/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:
|
/server-only@0.0.1:
|
||||||
resolution: {integrity: sha512-qepMx2JxAa5jjfzxG79yPPq+8BuFToHd1hm7kI+Z4zAq1ftQiP7HcxMhDDItrbtwVeLg/cY2JnKnrcFkmiswNA==}
|
resolution: {integrity: sha512-qepMx2JxAa5jjfzxG79yPPq+8BuFToHd1hm7kI+Z4zAq1ftQiP7HcxMhDDItrbtwVeLg/cY2JnKnrcFkmiswNA==}
|
||||||
dev: false
|
dev: false
|
||||||
@@ -6983,6 +7132,11 @@ packages:
|
|||||||
engines: {node: '>=12'}
|
engines: {node: '>=12'}
|
||||||
dev: false
|
dev: false
|
||||||
|
|
||||||
|
/split2@4.2.0:
|
||||||
|
resolution: {integrity: sha512-UcjcJOWknrNkF6PLX83qcHM6KHgVKNkV62Y8a5uYDVv9ydGQVwAHMKqHdJje1VTWpljG0WYpCDhrCdAOYH4TWg==}
|
||||||
|
engines: {node: '>= 10.x'}
|
||||||
|
dev: false
|
||||||
|
|
||||||
/streamsearch@1.1.0:
|
/streamsearch@1.1.0:
|
||||||
resolution: {integrity: sha512-Mcc5wHehp9aXz1ax6bZUyY5afg9u2rv5cqQI3mRrYkGC8rW2hM02jWuwjtL++LS5qinSyhj2QfLyNsuc+VsExg==}
|
resolution: {integrity: sha512-Mcc5wHehp9aXz1ax6bZUyY5afg9u2rv5cqQI3mRrYkGC8rW2hM02jWuwjtL++LS5qinSyhj2QfLyNsuc+VsExg==}
|
||||||
engines: {node: '>=10.0.0'}
|
engines: {node: '>=10.0.0'}
|
||||||
@@ -7336,7 +7490,6 @@ packages:
|
|||||||
/type-fest@0.20.2:
|
/type-fest@0.20.2:
|
||||||
resolution: {integrity: sha512-Ne+eE4r0/iWnpAxD852z3A+N0Bt5RN//NjJwRd2VFHEmrywxf5vsZlh4R6lixl6B+wz/8d+maTSAkN1FIkI3LQ==}
|
resolution: {integrity: sha512-Ne+eE4r0/iWnpAxD852z3A+N0Bt5RN//NjJwRd2VFHEmrywxf5vsZlh4R6lixl6B+wz/8d+maTSAkN1FIkI3LQ==}
|
||||||
engines: {node: '>=10'}
|
engines: {node: '>=10'}
|
||||||
dev: true
|
|
||||||
|
|
||||||
/type-fest@0.6.0:
|
/type-fest@0.6.0:
|
||||||
resolution: {integrity: sha512-q+MB8nYR1KDLrgr4G5yemftpMC7/QLqVndBmEEdqzmNj5dcFOO4Oo8qlwZE3ULT3+Zim1F8Kq4cBnikNhlCMlg==}
|
resolution: {integrity: sha512-q+MB8nYR1KDLrgr4G5yemftpMC7/QLqVndBmEEdqzmNj5dcFOO4Oo8qlwZE3ULT3+Zim1F8Kq4cBnikNhlCMlg==}
|
||||||
@@ -7466,6 +7619,11 @@ packages:
|
|||||||
hasBin: true
|
hasBin: true
|
||||||
dev: false
|
dev: false
|
||||||
|
|
||||||
|
/uuid@9.0.1:
|
||||||
|
resolution: {integrity: sha512-b+1eJOlsR9K8HJpow9Ok3fiWOWSIcIzXodvv0rQjVoOVNpWMpxf1wZNpt4y9h10odCNrqnYp1OBzRktckBe3sA==}
|
||||||
|
hasBin: true
|
||||||
|
dev: false
|
||||||
|
|
||||||
/validate-npm-package-license@3.0.4:
|
/validate-npm-package-license@3.0.4:
|
||||||
resolution: {integrity: sha512-DpKm2Ui/xN7/HQKCtpZxoRWBhZ9Z0kqtygG8XCgNQ8ZlDnxuQmWhj566j8fN4Cu3/JmbhsDo7fcAJq4s9h27Ew==}
|
resolution: {integrity: sha512-DpKm2Ui/xN7/HQKCtpZxoRWBhZ9Z0kqtygG8XCgNQ8ZlDnxuQmWhj566j8fN4Cu3/JmbhsDo7fcAJq4s9h27Ew==}
|
||||||
dependencies:
|
dependencies:
|
||||||
|
Reference in New Issue
Block a user