Improved DX for contact update, subscribe, and unsubscribe
This commit is contained in:
+1
-1
@@ -1,6 +1,6 @@
|
||||
{
|
||||
"name": "plunk",
|
||||
"version": "1.0.1",
|
||||
"version": "1.0.2",
|
||||
"private": true,
|
||||
"license": "agpl-3.0",
|
||||
"workspaces": {
|
||||
|
||||
@@ -101,16 +101,16 @@ export class Contacts {
|
||||
throw new NotFound("project");
|
||||
}
|
||||
|
||||
const { id } = UtilitySchemas.id.parse(req.body);
|
||||
const { id, email } = ContactSchemas.manage.parse(req.body);
|
||||
|
||||
const contact = await ContactService.id(id);
|
||||
const contact = id ? await ContactService.id(id) : await ContactService.email(project.id, email as string);
|
||||
|
||||
if (!contact || contact.projectId !== project.id) {
|
||||
throw new NotFound("contact");
|
||||
}
|
||||
|
||||
await prisma.contact.update({
|
||||
where: { id },
|
||||
where: { id: contact.id },
|
||||
data: { subscribed: false },
|
||||
});
|
||||
|
||||
@@ -152,16 +152,16 @@ export class Contacts {
|
||||
throw new NotFound("project");
|
||||
}
|
||||
|
||||
const { id } = UtilitySchemas.id.parse(req.body);
|
||||
const { id, email } = ContactSchemas.manage.parse(req.body);
|
||||
|
||||
const contact = await ContactService.id(id);
|
||||
const contact = id ? await ContactService.id(id) : await ContactService.email(project.id, email as string);
|
||||
|
||||
if (!contact || contact.projectId !== project.id) {
|
||||
throw new NotFound("contact");
|
||||
}
|
||||
|
||||
await prisma.contact.update({
|
||||
where: { id },
|
||||
where: { id: contact.id },
|
||||
data: { subscribed: true },
|
||||
});
|
||||
|
||||
@@ -246,26 +246,43 @@ export class Contacts {
|
||||
throw new NotFound("project");
|
||||
}
|
||||
|
||||
const { id, email, subscribed, data } = ContactSchemas.update.parse(req.body);
|
||||
const { id, email, subscribed, data } = ContactSchemas.manage.parse(req.body);
|
||||
|
||||
let contact = await ContactService.id(id);
|
||||
const contact = id ? await ContactService.id(id) : await ContactService.email(project.id, email as string);
|
||||
|
||||
if (!contact || contact.projectId !== project.id) {
|
||||
throw new NotFound("contact");
|
||||
}
|
||||
|
||||
contact = await prisma.contact.update({
|
||||
where: { id },
|
||||
data: { email, subscribed, data: data ? JSON.stringify(data) : null },
|
||||
include: {
|
||||
triggers: { include: { event: true, action: true } },
|
||||
emails: { where: { subject: { not: null } } },
|
||||
if (data) {
|
||||
const givenUserData = Object.entries(data);
|
||||
const dataToUpdate = JSON.parse(contact.data ?? "{}");
|
||||
|
||||
givenUserData.forEach(([key, value]) => {
|
||||
if (!value) {
|
||||
delete dataToUpdate[key];
|
||||
} else {
|
||||
dataToUpdate[key] = value;
|
||||
}
|
||||
});
|
||||
|
||||
await prisma.contact.update({
|
||||
where: { id: contact.id },
|
||||
data: { data: JSON.stringify(dataToUpdate) },
|
||||
});
|
||||
}
|
||||
|
||||
await prisma.contact.update({
|
||||
where: { id: contact.id },
|
||||
data: {
|
||||
email,
|
||||
subscribed: subscribed ?? contact.subscribed,
|
||||
},
|
||||
});
|
||||
|
||||
await redis.del(Keys.Project.contacts(project.id));
|
||||
await redis.del(Keys.Contact.id(contact.id));
|
||||
await redis.del(Keys.Contact.email(project.id, email));
|
||||
await redis.del(Keys.Contact.email(project.id, contact.email));
|
||||
|
||||
return res.status(200).json({
|
||||
success: true,
|
||||
|
||||
+238
-225
@@ -1,264 +1,277 @@
|
||||
import {z} from 'zod';
|
||||
import {TemplateStyle, TemplateType} from '@prisma/client';
|
||||
import { TemplateStyle, TemplateType } from "@prisma/client";
|
||||
import { z } from "zod";
|
||||
|
||||
const email = z
|
||||
.string({invalid_type_error: 'Email needs to be a string', required_error: 'Email is required'})
|
||||
.email({message: 'Invalid email address'})
|
||||
.transform(e => e.toLowerCase());
|
||||
.string({ invalid_type_error: "Email needs to be a string", required_error: "Email is required" })
|
||||
.email({ message: "Invalid email address" })
|
||||
.transform((e) => e.toLowerCase());
|
||||
|
||||
const password = z.string().min(6, 'Password needs to be at least 6 characters long');
|
||||
const password = z.string().min(6, "Password needs to be at least 6 characters long");
|
||||
|
||||
const id = z
|
||||
.string({invalid_type_error: 'ID needs to be a string', required_error: 'ID is required'})
|
||||
.uuid({message: 'Id needs to be a valid UUID'});
|
||||
.string({ invalid_type_error: "ID needs to be a string", required_error: "ID is required" })
|
||||
.uuid({ message: "Id needs to be a valid UUID" });
|
||||
|
||||
export const UtilitySchemas = {
|
||||
id: z.object({
|
||||
id,
|
||||
}),
|
||||
email: z.object({
|
||||
email,
|
||||
}),
|
||||
pagination: z.object({
|
||||
page: z
|
||||
.number({
|
||||
invalid_type_error: 'Page needs to be a number',
|
||||
required_error: 'Page is required',
|
||||
})
|
||||
.min(1, 'Page needs to be at least 1')
|
||||
.default(1)
|
||||
.or(
|
||||
z.string().transform(s => {
|
||||
return Number(s);
|
||||
}),
|
||||
),
|
||||
}),
|
||||
id: z.object({
|
||||
id,
|
||||
}),
|
||||
email: z.object({
|
||||
email,
|
||||
}),
|
||||
pagination: z.object({
|
||||
page: z
|
||||
.number({
|
||||
invalid_type_error: "Page needs to be a number",
|
||||
required_error: "Page is required",
|
||||
})
|
||||
.min(1, "Page needs to be at least 1")
|
||||
.default(1)
|
||||
.or(
|
||||
z.string().transform((s) => {
|
||||
return Number(s);
|
||||
}),
|
||||
),
|
||||
}),
|
||||
};
|
||||
|
||||
export const UserSchemas = {
|
||||
credentials: z.object({
|
||||
email: email,
|
||||
password: password,
|
||||
}),
|
||||
credentials: z.object({
|
||||
email: email,
|
||||
password: password,
|
||||
}),
|
||||
};
|
||||
|
||||
const zodSchema = z.record(
|
||||
z.union(
|
||||
[
|
||||
z
|
||||
.string({
|
||||
invalid_type_error:
|
||||
'Metadata can only be a string, array of strings or a non-persistent object (https://docs.useplunk.com/working-with-contacts/metadata#non-persistent-metadata)',
|
||||
})
|
||||
.transform(s => {
|
||||
return {persistent: true, value: s};
|
||||
}),
|
||||
z
|
||||
.array(
|
||||
z.string({
|
||||
invalid_type_error:
|
||||
'Metadata can only be a string, array of strings or a non-persistent object (https://docs.useplunk.com/working-with-contacts/metadata#non-persistent-metadata)',
|
||||
}),
|
||||
)
|
||||
.transform(s => {
|
||||
return {persistent: true, value: s};
|
||||
}),
|
||||
z.object(
|
||||
{
|
||||
persistent: z.boolean({invalid_type_error: 'Persistent should be a boolean'}).optional().default(true),
|
||||
value: z.union([z.string(), z.array(z.string())], {
|
||||
invalid_type_error:
|
||||
'Metadata can only be a string, array of strings or a non-persistent object (https://docs.useplunk.com/working-with-contacts/metadata#non-persistent-metadata)',
|
||||
}),
|
||||
},
|
||||
{
|
||||
invalid_type_error:
|
||||
'Metadata can only be a string, array of strings or a non-persistent object (https://docs.useplunk.com/working-with-contacts/metadata#non-persistent-metadata)',
|
||||
},
|
||||
),
|
||||
],
|
||||
{
|
||||
invalid_type_error:
|
||||
'Metadata can only be a string, array of strings or a non-persistent object (https://docs.useplunk.com/working-with-contacts/metadata#non-persistent-metadata)',
|
||||
},
|
||||
),
|
||||
{invalid_type_error: 'Metadata should be an object (https://docs.useplunk.com/working-with-contacts/metadata)'},
|
||||
z.union(
|
||||
[
|
||||
z
|
||||
.string({
|
||||
invalid_type_error:
|
||||
"Metadata can only be a string, array of strings or a non-persistent object (https://docs.useplunk.com/working-with-contacts/metadata#non-persistent-metadata)",
|
||||
})
|
||||
.transform((s) => {
|
||||
return { persistent: true, value: s };
|
||||
}),
|
||||
z
|
||||
.array(
|
||||
z.string({
|
||||
invalid_type_error:
|
||||
"Metadata can only be a string, array of strings or a non-persistent object (https://docs.useplunk.com/working-with-contacts/metadata#non-persistent-metadata)",
|
||||
}),
|
||||
)
|
||||
.transform((s) => {
|
||||
return { persistent: true, value: s };
|
||||
}),
|
||||
z.object(
|
||||
{
|
||||
persistent: z.boolean({ invalid_type_error: "Persistent should be a boolean" }).optional().default(true),
|
||||
value: z.union([z.string(), z.array(z.string())], {
|
||||
invalid_type_error:
|
||||
"Metadata can only be a string, array of strings or a non-persistent object (https://docs.useplunk.com/working-with-contacts/metadata#non-persistent-metadata)",
|
||||
}),
|
||||
},
|
||||
{
|
||||
invalid_type_error:
|
||||
"Metadata can only be a string, array of strings or a non-persistent object (https://docs.useplunk.com/working-with-contacts/metadata#non-persistent-metadata)",
|
||||
},
|
||||
),
|
||||
],
|
||||
{
|
||||
invalid_type_error:
|
||||
"Metadata can only be a string, array of strings or a non-persistent object (https://docs.useplunk.com/working-with-contacts/metadata#non-persistent-metadata)",
|
||||
},
|
||||
),
|
||||
{ invalid_type_error: "Metadata should be an object (https://docs.useplunk.com/working-with-contacts/metadata)" },
|
||||
);
|
||||
export const EventSchemas = {
|
||||
post: z.object({
|
||||
email,
|
||||
subscribed: z
|
||||
.boolean({
|
||||
invalid_type_error:
|
||||
'Subscribed should be a boolean. Read more: https://docs.useplunk.com/api-reference/actions/track',
|
||||
})
|
||||
.nullish(),
|
||||
event: z
|
||||
.string({
|
||||
required_error: 'Event is required. Read more: https://docs.useplunk.com/api-reference/actions/track',
|
||||
invalid_type_error:
|
||||
'Event can only be a string. Read more: https://docs.useplunk.com/api-reference/actions/track',
|
||||
})
|
||||
.transform(n => n.toLowerCase())
|
||||
.transform(n => n.replace(/ /g, '-')),
|
||||
data: zodSchema.nullish(),
|
||||
}),
|
||||
send: z.object({
|
||||
subscribed: z.boolean({invalid_type_error: 'Subscribed should be a boolean'}).nullish(),
|
||||
from: email.nullish(),
|
||||
name: z.string().nullish(),
|
||||
reply: email.nullish(),
|
||||
to: z
|
||||
.array(email)
|
||||
.max(5, 'You can only send transactional emails to 5 people at a time')
|
||||
.or(email.transform(e => [e])),
|
||||
subject: z.string({
|
||||
required_error: 'Subject is required. Read more: https://docs.useplunk.com/api-reference/transactional/send',
|
||||
}),
|
||||
body: z.string({
|
||||
required_error: 'Body is required. Read more: https://docs.useplunk.com/api-reference/transactional/send',
|
||||
}),
|
||||
headers: z.record(z.string()).nullish(),
|
||||
}),
|
||||
post: z.object({
|
||||
email,
|
||||
subscribed: z
|
||||
.boolean({
|
||||
invalid_type_error:
|
||||
"Subscribed should be a boolean. Read more: https://docs.useplunk.com/api-reference/actions/track",
|
||||
})
|
||||
.nullish(),
|
||||
event: z
|
||||
.string({
|
||||
required_error: "Event is required. Read more: https://docs.useplunk.com/api-reference/actions/track",
|
||||
invalid_type_error: "Event can only be a string. Read more: https://docs.useplunk.com/api-reference/actions/track",
|
||||
})
|
||||
.transform((n) => n.toLowerCase())
|
||||
.transform((n) => n.replace(/ /g, "-")),
|
||||
data: zodSchema.nullish(),
|
||||
}),
|
||||
send: z.object({
|
||||
subscribed: z.boolean({ invalid_type_error: "Subscribed should be a boolean" }).nullish(),
|
||||
from: email.nullish(),
|
||||
name: z.string().nullish(),
|
||||
reply: email.nullish(),
|
||||
to: z
|
||||
.array(email)
|
||||
.max(5, "You can only send transactional emails to 5 people at a time")
|
||||
.or(email.transform((e) => [e])),
|
||||
subject: z.string({
|
||||
required_error: "Subject is required. Read more: https://docs.useplunk.com/api-reference/transactional/send",
|
||||
}),
|
||||
body: z.string({
|
||||
required_error: "Body is required. Read more: https://docs.useplunk.com/api-reference/transactional/send",
|
||||
}),
|
||||
headers: z.record(z.string()).nullish(),
|
||||
}),
|
||||
};
|
||||
|
||||
export const CampaignSchemas = {
|
||||
send: z.object({
|
||||
id,
|
||||
live: z.boolean().default(false),
|
||||
delay: z.number().int('Delay needs to be a whole number').nonnegative('Delay needs to be a positive number'),
|
||||
}),
|
||||
create: z.object({
|
||||
subject: z
|
||||
.string()
|
||||
.min(1, 'Subject needs to be at least 1 character long')
|
||||
.max(70, 'Subject needs to be less than 70 characters long'),
|
||||
body: z.string().min(1, 'Body needs to be at least 1 character long'),
|
||||
recipients: z.array(z.string()),
|
||||
style: z.nativeEnum(TemplateStyle).default('PLUNK'),
|
||||
}),
|
||||
update: z.object({
|
||||
id,
|
||||
subject: z
|
||||
.string()
|
||||
.min(1, 'Subject needs to be at least 1 character long')
|
||||
.max(70, 'Subject needs to be less than 70 characters long'),
|
||||
body: z.string().min(1, 'Body needs to be at least 1 character long'),
|
||||
recipients: z.array(z.string()),
|
||||
style: z.nativeEnum(TemplateStyle).default('PLUNK'),
|
||||
}),
|
||||
send: z.object({
|
||||
id,
|
||||
live: z.boolean().default(false),
|
||||
delay: z.number().int("Delay needs to be a whole number").nonnegative("Delay needs to be a positive number"),
|
||||
}),
|
||||
create: z.object({
|
||||
subject: z
|
||||
.string()
|
||||
.min(1, "Subject needs to be at least 1 character long")
|
||||
.max(70, "Subject needs to be less than 70 characters long"),
|
||||
body: z.string().min(1, "Body needs to be at least 1 character long"),
|
||||
recipients: z.array(z.string()),
|
||||
style: z.nativeEnum(TemplateStyle).default("PLUNK"),
|
||||
}),
|
||||
update: z.object({
|
||||
id,
|
||||
subject: z
|
||||
.string()
|
||||
.min(1, "Subject needs to be at least 1 character long")
|
||||
.max(70, "Subject needs to be less than 70 characters long"),
|
||||
body: z.string().min(1, "Body needs to be at least 1 character long"),
|
||||
recipients: z.array(z.string()),
|
||||
style: z.nativeEnum(TemplateStyle).default("PLUNK"),
|
||||
}),
|
||||
};
|
||||
|
||||
export const ActionSchemas = {
|
||||
create: z.object({
|
||||
name: z.string().min(1, 'Name needs to be at least 1 character long'),
|
||||
runOnce: z.boolean().default(false),
|
||||
delay: z.number().int('Delay needs to be a whole number').nonnegative('Delay needs to be a positive number'),
|
||||
template: id,
|
||||
events: z.array(id).min(1, 'Select at least one event'),
|
||||
notevents: z.array(id).optional().default([]),
|
||||
}),
|
||||
update: z.object({
|
||||
id,
|
||||
name: z.string().min(1, 'Name needs to be at least 1 character long'),
|
||||
runOnce: z.boolean().default(false),
|
||||
delay: z.number().int('Delay needs to be a whole number').nonnegative('Delay needs to be a positive number'),
|
||||
template: id,
|
||||
events: z.array(id).default([]),
|
||||
notevents: z.array(id).optional().default([]),
|
||||
}),
|
||||
create: z.object({
|
||||
name: z.string().min(1, "Name needs to be at least 1 character long"),
|
||||
runOnce: z.boolean().default(false),
|
||||
delay: z.number().int("Delay needs to be a whole number").nonnegative("Delay needs to be a positive number"),
|
||||
template: id,
|
||||
events: z.array(id).min(1, "Select at least one event"),
|
||||
notevents: z.array(id).optional().default([]),
|
||||
}),
|
||||
update: z.object({
|
||||
id,
|
||||
name: z.string().min(1, "Name needs to be at least 1 character long"),
|
||||
runOnce: z.boolean().default(false),
|
||||
delay: z.number().int("Delay needs to be a whole number").nonnegative("Delay needs to be a positive number"),
|
||||
template: id,
|
||||
events: z.array(id).default([]),
|
||||
notevents: z.array(id).optional().default([]),
|
||||
}),
|
||||
};
|
||||
|
||||
export const ContactSchemas = {
|
||||
create: z.object({
|
||||
email,
|
||||
data: z
|
||||
.object({})
|
||||
.catchall(z.union([z.string(), z.array(z.string())]))
|
||||
.or(z.string().transform(s => (s === '' ? null : JSON.parse(s))))
|
||||
.nullish(),
|
||||
subscribed: z.boolean(),
|
||||
}),
|
||||
update: z.object({
|
||||
id,
|
||||
email,
|
||||
data: z
|
||||
.object({})
|
||||
.catchall(z.union([z.string(), z.array(z.string())]))
|
||||
.or(z.string().transform(s => (s === '' ? null : JSON.parse(s))))
|
||||
.nullish(),
|
||||
subscribed: z.boolean(),
|
||||
}),
|
||||
create: z.object({
|
||||
email,
|
||||
data: z
|
||||
.object({})
|
||||
.catchall(z.union([z.string(), z.array(z.string())]))
|
||||
.or(z.string().transform((s) => (s === "" ? null : JSON.parse(s))))
|
||||
.nullish(),
|
||||
subscribed: z.boolean(),
|
||||
}),
|
||||
manage: z
|
||||
.object({
|
||||
id: id.optional(),
|
||||
email: email.optional(),
|
||||
data: z
|
||||
.object({})
|
||||
.catchall(z.union([z.string(), z.array(z.string()), z.null()]))
|
||||
.or(z.string().transform((s) => (s === "" ? null : JSON.parse(s))))
|
||||
.nullish(),
|
||||
subscribed: z.boolean().nullish(),
|
||||
})
|
||||
.refine(
|
||||
(data) => {
|
||||
return data.id || data.email;
|
||||
},
|
||||
{ message: "Either id or email should be specified" },
|
||||
)
|
||||
.refine(
|
||||
(data) => {
|
||||
// if id and email are both present
|
||||
return !(data.id && data.email);
|
||||
},
|
||||
{ message: "Either id or email should be specified" },
|
||||
),
|
||||
};
|
||||
|
||||
export const TemplateSchemas = {
|
||||
create: z.object({
|
||||
subject: z.string().min(1, "Subject can't be empty").max(70, 'Subject needs to be less than 70 characters long'),
|
||||
body: z.string().min(1, "Body can't be empty"),
|
||||
type: z.nativeEnum(TemplateType).default('MARKETING'),
|
||||
style: z.nativeEnum(TemplateStyle).default('PLUNK'),
|
||||
}),
|
||||
update: z.object({
|
||||
id,
|
||||
subject: z.string().min(1, "Subject can't be empty").max(70, 'Subject needs to be less than 70 characters long'),
|
||||
body: z.string().min(1, "Body can't be empty"),
|
||||
type: z.nativeEnum(TemplateType).default('MARKETING'),
|
||||
style: z.nativeEnum(TemplateStyle).default('PLUNK'),
|
||||
}),
|
||||
create: z.object({
|
||||
subject: z.string().min(1, "Subject can't be empty").max(70, "Subject needs to be less than 70 characters long"),
|
||||
body: z.string().min(1, "Body can't be empty"),
|
||||
type: z.nativeEnum(TemplateType).default("MARKETING"),
|
||||
style: z.nativeEnum(TemplateStyle).default("PLUNK"),
|
||||
}),
|
||||
update: z.object({
|
||||
id,
|
||||
subject: z.string().min(1, "Subject can't be empty").max(70, "Subject needs to be less than 70 characters long"),
|
||||
body: z.string().min(1, "Body can't be empty"),
|
||||
type: z.nativeEnum(TemplateType).default("MARKETING"),
|
||||
style: z.nativeEnum(TemplateStyle).default("PLUNK"),
|
||||
}),
|
||||
};
|
||||
|
||||
export const MembershipSchemas = {
|
||||
invite: z.object({
|
||||
id,
|
||||
email,
|
||||
role: z.enum(['MEMBER', 'ADMIN']).default('MEMBER'),
|
||||
}),
|
||||
kick: z.object({
|
||||
id,
|
||||
email,
|
||||
}),
|
||||
invite: z.object({
|
||||
id,
|
||||
email,
|
||||
role: z.enum(["MEMBER", "ADMIN"]).default("MEMBER"),
|
||||
}),
|
||||
kick: z.object({
|
||||
id,
|
||||
email,
|
||||
}),
|
||||
};
|
||||
|
||||
export const ProjectSchemas = {
|
||||
secret: z.object({
|
||||
secret: z.string(),
|
||||
}),
|
||||
create: z.object({
|
||||
name: z.string().min(1, "Name can't be empty"),
|
||||
secret: z.object({
|
||||
secret: z.string(),
|
||||
}),
|
||||
create: z.object({
|
||||
name: z.string().min(1, "Name can't be empty"),
|
||||
|
||||
url: z
|
||||
.string()
|
||||
.regex(/^(?:(?:https?):\/\/)?(?:[\w-]+\.)+[a-z]{2,}(?:\/[^\s]*)?$/)
|
||||
.transform(u => (u.startsWith('http') ? u : `https://${u}`)),
|
||||
}),
|
||||
update: z.object({
|
||||
id: id,
|
||||
name: z.string().min(1, "Name can't be empty"),
|
||||
url: z
|
||||
.string()
|
||||
.regex(/^(?:(?:https?):\/\/)?(?:[\w-]+\.)+[a-z]{2,}(?:\/[^\s]*)?$/)
|
||||
.transform((u) => (u.startsWith("http") ? u : `https://${u}`)),
|
||||
}),
|
||||
update: z.object({
|
||||
id: id,
|
||||
name: z.string().min(1, "Name can't be empty"),
|
||||
|
||||
url: z
|
||||
.string()
|
||||
.regex(/^(?:(?:https?):\/\/)?(?:[\w-]+\.)+[a-z]{2,}(?:\/[^\s]*)?$/)
|
||||
.transform(u => (u.startsWith('http') ? u : `https://${u}`)),
|
||||
}),
|
||||
analytics: z.object({
|
||||
method: z.enum(['week', 'month', 'year']).default('week'),
|
||||
}),
|
||||
url: z
|
||||
.string()
|
||||
.regex(/^(?:(?:https?):\/\/)?(?:[\w-]+\.)+[a-z]{2,}(?:\/[^\s]*)?$/)
|
||||
.transform((u) => (u.startsWith("http") ? u : `https://${u}`)),
|
||||
}),
|
||||
analytics: z.object({
|
||||
method: z.enum(["week", "month", "year"]).default("week"),
|
||||
}),
|
||||
};
|
||||
|
||||
export const IdentitySchemas = {
|
||||
create: z.object({
|
||||
id: id,
|
||||
email: email.refine(
|
||||
e => {
|
||||
return !['gmail.com', 'outlook.com', 'hotmail.com', 'yahoo.com', 'useplunk.com', 'useplunk.dev'].includes(
|
||||
e.split('@')[1],
|
||||
);
|
||||
},
|
||||
{message: 'Please use your own domain'},
|
||||
),
|
||||
}),
|
||||
update: z.object({
|
||||
id: id,
|
||||
from: z.string().min(1, "Name can't be empty"),
|
||||
}),
|
||||
create: z.object({
|
||||
id: id,
|
||||
email: email.refine(
|
||||
(e) => {
|
||||
return !["gmail.com", "outlook.com", "hotmail.com", "yahoo.com", "useplunk.com", "useplunk.dev"].includes(
|
||||
e.split("@")[1],
|
||||
);
|
||||
},
|
||||
{ message: "Please use your own domain" },
|
||||
),
|
||||
}),
|
||||
update: z.object({
|
||||
id: id,
|
||||
from: z.string().min(1, "Name can't be empty"),
|
||||
}),
|
||||
};
|
||||
|
||||
Reference in New Issue
Block a user