@carlonicora/nextjs-jsonapi 6.7.0 → 6.9.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (137) hide show
  1. package/dist/{AssistantMessageInterface-0ZQ1EJKO.d.mts → AssistantMessageInterface-BE3QxI54.d.mts} +1 -1
  2. package/dist/{AssistantMessageInterface-CA-hZ5Wj.d.ts → AssistantMessageInterface-nXpD_d-m.d.ts} +1 -1
  3. package/dist/{BlockNoteEditor-HWJBQAKG.mjs → BlockNoteEditor-WN73IHXR.mjs} +4 -4
  4. package/dist/{BlockNoteEditor-BSFR2GPI.js → BlockNoteEditor-XXH3VBZ7.js} +19 -19
  5. package/dist/{BlockNoteEditor-BSFR2GPI.js.map → BlockNoteEditor-XXH3VBZ7.js.map} +1 -1
  6. package/dist/{BlockNoteViewer-NHS6CCMS.js → BlockNoteViewer-EKAWVLIR.js} +3 -3
  7. package/dist/{BlockNoteViewer-NHS6CCMS.js.map → BlockNoteViewer-EKAWVLIR.js.map} +1 -1
  8. package/dist/{BlockNoteViewer-IIBUU7J7.mjs → BlockNoteViewer-RWRI3QO2.mjs} +2 -2
  9. package/dist/{BlogPostInterface-CTGfsR1I.d.ts → BlogPostInterface-2sVqvXFd.d.ts} +1 -1
  10. package/dist/{BlogPostInterface-BN2mXc46.d.mts → BlogPostInterface-BgJLFpaa.d.mts} +1 -1
  11. package/dist/{HandbookSectionInterface-BYsmnPEz.d.mts → HandbookSectionInterface-BPgqqwgw.d.mts} +20 -1
  12. package/dist/{HandbookSectionInterface-OePKPbqN.d.ts → HandbookSectionInterface-_51oA3om.d.ts} +20 -1
  13. package/dist/{auth.interface-DuX87BZ3.d.mts → auth.interface-BAYLbYjs.d.mts} +1 -1
  14. package/dist/{auth.interface-B4tRE0k0.d.ts → auth.interface-BjYwwfy9.d.ts} +1 -1
  15. package/dist/billing/index.js +492 -492
  16. package/dist/billing/index.mjs +3 -3
  17. package/dist/{chunk-EC4PQ44E.mjs → chunk-3TNGKMJU.mjs} +2 -2
  18. package/dist/{chunk-QNIWATQD.mjs → chunk-6HFRXEF5.mjs} +331 -3
  19. package/dist/chunk-6HFRXEF5.mjs.map +1 -0
  20. package/dist/{chunk-OYRJ32KR.mjs → chunk-AWLTSKMM.mjs} +2 -2
  21. package/dist/{chunk-XYKDBOCE.mjs → chunk-DPAM7BCE.mjs} +2 -2
  22. package/dist/{chunk-PYDDFFQM.js → chunk-G3YKDIAL.js} +3 -3
  23. package/dist/{chunk-PYDDFFQM.js.map → chunk-G3YKDIAL.js.map} +1 -1
  24. package/dist/{chunk-3HOJNAZB.js → chunk-KRQRJA4J.js} +8 -8
  25. package/dist/{chunk-3HOJNAZB.js.map → chunk-KRQRJA4J.js.map} +1 -1
  26. package/dist/{chunk-S4NKWMX5.mjs → chunk-Q4XLE3QO.mjs} +1738 -1308
  27. package/dist/chunk-Q4XLE3QO.mjs.map +1 -0
  28. package/dist/{chunk-73XWQA6U.js → chunk-RNZ3IK5J.js} +4 -4
  29. package/dist/{chunk-73XWQA6U.js.map → chunk-RNZ3IK5J.js.map} +1 -1
  30. package/dist/{chunk-6KE7UBYY.js → chunk-SQTXJPRE.js} +505 -177
  31. package/dist/chunk-SQTXJPRE.js.map +1 -0
  32. package/dist/{chunk-PCCUPEBU.js → chunk-ZCM4JLM7.js} +1381 -951
  33. package/dist/chunk-ZCM4JLM7.js.map +1 -0
  34. package/dist/client/index.d.mts +3 -3
  35. package/dist/client/index.d.ts +3 -3
  36. package/dist/client/index.js +4 -4
  37. package/dist/client/index.mjs +3 -3
  38. package/dist/components/index.d.mts +62 -8
  39. package/dist/components/index.d.ts +62 -8
  40. package/dist/components/index.js +14 -4
  41. package/dist/components/index.js.map +1 -1
  42. package/dist/components/index.mjs +13 -3
  43. package/dist/contexts/index.d.mts +4 -4
  44. package/dist/contexts/index.d.ts +4 -4
  45. package/dist/contexts/index.js +4 -4
  46. package/dist/contexts/index.mjs +3 -3
  47. package/dist/core/index.d.mts +146 -13
  48. package/dist/core/index.d.ts +146 -13
  49. package/dist/core/index.js +24 -2
  50. package/dist/core/index.js.map +1 -1
  51. package/dist/core/index.mjs +23 -1
  52. package/dist/email-automations/index.d.mts +6 -1
  53. package/dist/email-automations/index.d.ts +6 -1
  54. package/dist/email-automations/index.js +101 -94
  55. package/dist/email-automations/index.js.map +1 -1
  56. package/dist/email-automations/index.mjs +12 -5
  57. package/dist/email-automations/index.mjs.map +1 -1
  58. package/dist/features/analytics/index.d.mts +1 -1
  59. package/dist/features/analytics/index.d.ts +1 -1
  60. package/dist/features/analytics/index.js +121 -121
  61. package/dist/features/analytics/index.mjs +4 -4
  62. package/dist/features/analytics/server.js +4 -4
  63. package/dist/features/analytics/server.mjs +2 -2
  64. package/dist/features/blog/index.d.mts +3 -3
  65. package/dist/features/blog/index.d.ts +3 -3
  66. package/dist/features/blog/index.js +150 -150
  67. package/dist/features/blog/index.mjs +3 -3
  68. package/dist/features/blog/server.d.mts +3 -3
  69. package/dist/features/blog/server.d.ts +3 -3
  70. package/dist/features/blog/server.js +16 -16
  71. package/dist/features/blog/server.mjs +1 -1
  72. package/dist/features/help/index.js +37 -37
  73. package/dist/features/help/index.mjs +3 -3
  74. package/dist/features/tokenusage/index.js +127 -127
  75. package/dist/features/tokenusage/index.mjs +4 -4
  76. package/dist/index.d.mts +8 -8
  77. package/dist/index.d.ts +8 -8
  78. package/dist/index.js +25 -3
  79. package/dist/index.js.map +1 -1
  80. package/dist/index.mjs +24 -2
  81. package/dist/{notification.interface-DQReXAD7.d.ts → notification.interface-DQItyY_Q.d.ts} +1 -1
  82. package/dist/{notification.interface-CF5h90U2.d.mts → notification.interface-DoVS5RZB.d.mts} +1 -1
  83. package/dist/{s3.service-CRGJbO-6.d.mts → s3.service-BG136Btz.d.mts} +3 -3
  84. package/dist/{s3.service-DZG-VyLY.d.ts → s3.service-CsrsRPRn.d.ts} +3 -3
  85. package/dist/server/index.d.mts +4 -4
  86. package/dist/server/index.d.ts +4 -4
  87. package/dist/server/index.js +3 -3
  88. package/dist/server/index.mjs +2 -2
  89. package/dist/{useSocket-CwSoq6VS.d.mts → useSocket-C8fqU4od.d.mts} +1 -1
  90. package/dist/{useSocket-BM3q9A0C.d.ts → useSocket-IRHtk2xc.d.ts} +1 -1
  91. package/dist/{user.interface-BYnGJ7ss.d.mts → user.interface-Bm-0TbsB.d.mts} +6 -0
  92. package/dist/{user.interface-BqPjVyJI.d.ts → user.interface-CKBIEvNu.d.ts} +6 -0
  93. package/messages/en.json +62 -0
  94. package/messages/it.json +62 -0
  95. package/package.json +1 -1
  96. package/src/components/index.ts +1 -0
  97. package/src/core/index.ts +3 -0
  98. package/src/core/registry/ModuleRegistry.ts +4 -0
  99. package/src/features/company/data/company.interface.ts +7 -0
  100. package/src/features/company/data/company.spec.ts +23 -0
  101. package/src/features/company/data/company.ts +6 -0
  102. package/src/features/company/hooks/useSubscriptionStatus.spec.ts +59 -0
  103. package/src/features/company/hooks/useSubscriptionStatus.ts +5 -3
  104. package/src/features/email-automation/components/unsubscribe/EmailUnsubscribe.tsx +14 -2
  105. package/src/features/email-blast/EmailBlastAudienceModule.ts +12 -0
  106. package/src/features/email-blast/EmailBlastDeliveryModule.ts +12 -0
  107. package/src/features/email-blast/EmailBlastModule.ts +28 -0
  108. package/src/features/email-blast/components/containers/EmailBlastContainer.tsx +145 -0
  109. package/src/features/email-blast/components/containers/EmailBlastsListContainer.tsx +36 -0
  110. package/src/features/email-blast/components/forms/EmailBlastEditor.tsx +132 -0
  111. package/src/features/email-blast/components/forms/EmailBlastSendDialog.tsx +104 -0
  112. package/src/features/email-blast/components/index.ts +5 -0
  113. package/src/features/email-blast/components/lists/EmailBlastsList.tsx +57 -0
  114. package/src/features/email-blast/data/EmailBlast.ts +89 -0
  115. package/src/features/email-blast/data/EmailBlastAudience.ts +22 -0
  116. package/src/features/email-blast/data/EmailBlastAudienceInterface.ts +5 -0
  117. package/src/features/email-blast/data/EmailBlastDelivery.ts +36 -0
  118. package/src/features/email-blast/data/EmailBlastDeliveryInterface.ts +7 -0
  119. package/src/features/email-blast/data/EmailBlastDeliveryService.ts +25 -0
  120. package/src/features/email-blast/data/EmailBlastInterface.ts +22 -0
  121. package/src/features/email-blast/data/EmailBlastService.ts +101 -0
  122. package/src/features/email-blast/data/__tests__/EmailBlast.spec.ts +73 -0
  123. package/src/features/email-blast/data/email-blast.fields.ts +8 -0
  124. package/src/features/email-blast/data/email-blast.segments.ts +10 -0
  125. package/src/features/email-blast/data/index.ts +10 -0
  126. package/src/features/email-blast/hooks/useEmailBlastTableStructure.tsx +104 -0
  127. package/src/features/email-blast/index.ts +4 -0
  128. package/src/features/index.ts +1 -0
  129. package/dist/chunk-6KE7UBYY.js.map +0 -1
  130. package/dist/chunk-PCCUPEBU.js.map +0 -1
  131. package/dist/chunk-QNIWATQD.mjs.map +0 -1
  132. package/dist/chunk-S4NKWMX5.mjs.map +0 -1
  133. /package/dist/{BlockNoteEditor-HWJBQAKG.mjs.map → BlockNoteEditor-WN73IHXR.mjs.map} +0 -0
  134. /package/dist/{BlockNoteViewer-IIBUU7J7.mjs.map → BlockNoteViewer-RWRI3QO2.mjs.map} +0 -0
  135. /package/dist/{chunk-EC4PQ44E.mjs.map → chunk-3TNGKMJU.mjs.map} +0 -0
  136. /package/dist/{chunk-OYRJ32KR.mjs.map → chunk-AWLTSKMM.mjs.map} +0 -0
  137. /package/dist/{chunk-XYKDBOCE.mjs.map → chunk-DPAM7BCE.mjs.map} +0 -0
@@ -0,0 +1,132 @@
1
+ "use client";
2
+
3
+ import { zodResolver } from "@hookform/resolvers/zod";
4
+ import { useTranslations } from "next-intl";
5
+ import { ReactNode, useCallback, useMemo } from "react";
6
+ import { useForm } from "react-hook-form";
7
+ import { v4 } from "uuid";
8
+ import { z } from "zod";
9
+ import { EditorSheet, FormInput, FormTextarea } from "../../../../components";
10
+ import { Modules } from "../../../../core";
11
+ import { useI18nRouter } from "../../../../i18n";
12
+ import { EmailBlastInput, EmailBlastInterface } from "../../data/EmailBlastInterface";
13
+ import { EmailBlastService } from "../../data/EmailBlastService";
14
+
15
+ /** The fixed variable set the blast renderer fills. */
16
+ const TEMPLATE_VARIABLES = ["{{name}}", "{{email}}", "{{url}}", "{{unsubscribeAllUrl}}"];
17
+
18
+ export type EmailBlastEditorProps = {
19
+ blast?: EmailBlastInterface;
20
+ propagateChanges?: (blast: EmailBlastInterface) => void;
21
+ onSuccess?: () => void | Promise<void>;
22
+ trigger?: ReactNode;
23
+ forceShow?: boolean;
24
+ onClose?: () => void;
25
+ dialogOpen?: boolean;
26
+ onDialogOpenChange?: (open: boolean) => void;
27
+ };
28
+
29
+ function EmailBlastEditorInternal({
30
+ blast,
31
+ propagateChanges,
32
+ onSuccess,
33
+ trigger,
34
+ forceShow,
35
+ onClose,
36
+ dialogOpen,
37
+ onDialogOpenChange,
38
+ }: EmailBlastEditorProps) {
39
+ const t = useTranslations();
40
+ const router = useI18nRouter();
41
+ const isEdit = !!blast;
42
+
43
+ const formSchema = useMemo(
44
+ () =>
45
+ z.object({
46
+ id: z.uuidv4(),
47
+ subject: z.string().min(1, { message: t("emailBlasts.editor.fields.subject.error") }),
48
+ body: z.string().min(1, { message: t("emailBlasts.editor.fields.body.error") }),
49
+ replyTo: z.string().refine((value) => value === "" || z.email().safeParse(value).success, {
50
+ message: t("emailBlasts.editor.fields.replyTo.error"),
51
+ }),
52
+ }),
53
+ [t],
54
+ );
55
+
56
+ const getDefaultValues = useCallback(
57
+ () => ({
58
+ id: blast?.id ?? v4(),
59
+ subject: blast?.subject ?? "",
60
+ body: blast?.body ?? "",
61
+ replyTo: blast?.replyTo ?? "",
62
+ }),
63
+ [blast],
64
+ );
65
+
66
+ const form = useForm<z.infer<typeof formSchema>>({
67
+ resolver: zodResolver(formSchema),
68
+ defaultValues: getDefaultValues(),
69
+ });
70
+
71
+ return (
72
+ <EditorSheet
73
+ form={form}
74
+ entityType={t("emailBlasts.editor.title")}
75
+ entityName={blast?.subject}
76
+ title={isEdit ? t("emailBlasts.editor.editTitle") : t("emailBlasts.editor.title")}
77
+ isEdit={isEdit}
78
+ module={Modules.EmailBlast}
79
+ size="lg"
80
+ propagateChanges={propagateChanges}
81
+ onSuccess={onSuccess}
82
+ onNavigate={(url) => router.push(url)}
83
+ onSubmit={async (values) => {
84
+ const payload: EmailBlastInput = {
85
+ id: values.id,
86
+ subject: values.subject,
87
+ body: values.body,
88
+ replyTo: values.replyTo || undefined,
89
+ };
90
+ return isEdit ? await EmailBlastService.update(payload) : await EmailBlastService.create(payload);
91
+ }}
92
+ onReset={getDefaultValues}
93
+ trigger={trigger}
94
+ forceShow={forceShow}
95
+ onClose={onClose}
96
+ dialogOpen={dialogOpen}
97
+ onDialogOpenChange={onDialogOpenChange}
98
+ >
99
+ <div className="flex w-full flex-col gap-y-4">
100
+ <FormInput form={form} id="subject" name={t("emailBlasts.editor.fields.subject.label")} isRequired />
101
+ <FormInput form={form} id="replyTo" name={t("emailBlasts.editor.fields.replyTo.label")} />
102
+ {/* FormTextarea has no `rows` prop; min-h-72 is the height of ~14 rows. */}
103
+ <FormTextarea
104
+ form={form}
105
+ id="body"
106
+ name={t("emailBlasts.editor.fields.body.label")}
107
+ className="min-h-72"
108
+ isRequired
109
+ description={
110
+ <>
111
+ <span className="block">
112
+ {t("emailBlasts.editor.variablesHint")}{" "}
113
+ {TEMPLATE_VARIABLES.map((variable) => (
114
+ <span key={variable} className="me-2 text-foreground">
115
+ {variable}
116
+ </span>
117
+ ))}
118
+ </span>
119
+ <span className="mt-1 block">{t("emailBlasts.editor.unsubscribeHint")}</span>
120
+ </>
121
+ }
122
+ />
123
+ </div>
124
+ </EditorSheet>
125
+ );
126
+ }
127
+
128
+ export function EmailBlastEditor(props: EmailBlastEditorProps) {
129
+ return <EmailBlastEditorInternal {...props} />;
130
+ }
131
+
132
+ export default EmailBlastEditor;
@@ -0,0 +1,104 @@
1
+ "use client";
2
+
3
+ import { LoaderCircleIcon, SendIcon } from "lucide-react";
4
+ import { useTranslations } from "next-intl";
5
+ import { useEffect, useState } from "react";
6
+ import { errorToast } from "../../../../components/errors/errorToast";
7
+ import {
8
+ AlertDialog,
9
+ AlertDialogContent,
10
+ AlertDialogDescription,
11
+ AlertDialogHeader,
12
+ AlertDialogTitle,
13
+ AlertDialogTrigger,
14
+ } from "../../../../shadcnui/ui/alert-dialog";
15
+ import { Button } from "../../../../shadcnui/ui/button";
16
+ import { EmailBlastInterface } from "../../data/EmailBlastInterface";
17
+ import { EmailBlastService } from "../../data/EmailBlastService";
18
+
19
+ export type EmailBlastSendDialogProps = {
20
+ blast: EmailBlastInterface;
21
+ onSuccess?: () => void | Promise<void>;
22
+ };
23
+
24
+ /**
25
+ * Header action on a draft blast: asks how many people it reaches now, then sends.
26
+ * Same AlertDialog shape as AccountDeletionAdminTrigger.
27
+ */
28
+ export function EmailBlastSendDialog({ blast, onSuccess }: EmailBlastSendDialogProps) {
29
+ const t = useTranslations();
30
+ const [open, setOpen] = useState<boolean>(false);
31
+ const [count, setCount] = useState<number | undefined>(undefined);
32
+ const [isSending, setIsSending] = useState<boolean>(false);
33
+
34
+ // The count is read each time the dialog opens: consent and opt-outs change.
35
+ useEffect(() => {
36
+ if (!open) return;
37
+ let cancelled = false;
38
+ setCount(undefined);
39
+ EmailBlastService.findAudience({ id: blast.id })
40
+ .then((audience) => {
41
+ if (!cancelled) setCount(audience.recipientCount);
42
+ })
43
+ .catch((error) => {
44
+ if (!cancelled) errorToast({ title: t("common.errors.error"), error });
45
+ });
46
+ return () => {
47
+ cancelled = true;
48
+ };
49
+ }, [open, blast.id, t]);
50
+
51
+ const isLoading = count === undefined;
52
+ const canSend = !isLoading && count > 0 && !isSending;
53
+
54
+ const handleSend = async () => {
55
+ setIsSending(true);
56
+ try {
57
+ await EmailBlastService.send({ id: blast.id });
58
+ await onSuccess?.();
59
+ setOpen(false);
60
+ } catch (error) {
61
+ errorToast({ title: t("common.errors.error"), error });
62
+ } finally {
63
+ setIsSending(false);
64
+ }
65
+ };
66
+
67
+ return (
68
+ <AlertDialog open={open} onOpenChange={setOpen}>
69
+ <AlertDialogTrigger render={<Button size="sm" variant="default" />}>
70
+ <SendIcon />
71
+ {t("emailBlasts.send.button")}
72
+ </AlertDialogTrigger>
73
+ <AlertDialogContent className="flex max-h-[70vh] max-w-3xl flex-col overflow-y-auto">
74
+ <AlertDialogHeader>
75
+ <AlertDialogTitle>{t("emailBlasts.send.title")}</AlertDialogTitle>
76
+ <AlertDialogDescription>
77
+ {isLoading ? (
78
+ <LoaderCircleIcon className="animate-spin-slow h-5 w-5" />
79
+ ) : count === 0 ? (
80
+ t("emailBlasts.send.nobody")
81
+ ) : (
82
+ t("emailBlasts.send.confirm", { count })
83
+ )}
84
+ </AlertDialogDescription>
85
+ </AlertDialogHeader>
86
+ <div className="flex justify-end">
87
+ <Button className="me-2" variant="outline" type="button" onClick={() => setOpen(false)} disabled={isSending}>
88
+ {t("emailBlasts.send.cancel")}
89
+ </Button>
90
+ <Button type="button" variant="default" onClick={handleSend} disabled={!canSend}>
91
+ {isSending ? (
92
+ <>
93
+ {t("emailBlasts.send.action")}
94
+ <LoaderCircleIcon className="animate-spin-slow h-5 w-5" />
95
+ </>
96
+ ) : (
97
+ t("emailBlasts.send.action")
98
+ )}
99
+ </Button>
100
+ </div>
101
+ </AlertDialogContent>
102
+ </AlertDialog>
103
+ );
104
+ }
@@ -0,0 +1,5 @@
1
+ export * from "./containers/EmailBlastsListContainer";
2
+ export * from "./containers/EmailBlastContainer";
3
+ export * from "./lists/EmailBlastsList";
4
+ export * from "./forms/EmailBlastEditor";
5
+ export * from "./forms/EmailBlastSendDialog";
@@ -0,0 +1,57 @@
1
+ "use client";
2
+
3
+ import { SendIcon } from "lucide-react";
4
+ import { useTranslations } from "next-intl";
5
+ import { ReactNode } from "react";
6
+ import { ContentListTable } from "../../../../components";
7
+ import { Modules } from "../../../../core";
8
+ import { DataListRetriever, useDataListRetriever } from "../../../../hooks";
9
+ import { EmptyState } from "../../../../shadcnui";
10
+ import { EmailBlastFields } from "../../data/email-blast.fields";
11
+ import { EmailBlastInterface } from "../../data/EmailBlastInterface";
12
+ import { EmailBlastService } from "../../data/EmailBlastService";
13
+ // Registers the "email-blasts" table generator. The blast components ship in the
14
+ // shared ./components entry, which does not re-export feature hooks (HowToList pattern).
15
+ import "../../hooks/useEmailBlastTableStructure";
16
+ import EmailBlastEditor from "../forms/EmailBlastEditor";
17
+
18
+ type EmailBlastsListProps = {
19
+ /**
20
+ * Pass this whenever the list is the page's own content inside a
21
+ * `RoundPageContainer fullWidth`. Without it ContentListTable wraps itself in
22
+ * `rounded-md border`, drawing a second bordered card inside the page's
23
+ * rounded shell.
24
+ */
25
+ fullWidth?: boolean;
26
+ };
27
+
28
+ export function EmailBlastsList({ fullWidth }: EmailBlastsListProps = {}) {
29
+ const t = useTranslations();
30
+
31
+ const data: DataListRetriever<EmailBlastInterface> = useDataListRetriever({
32
+ retriever: (params) => EmailBlastService.findMany(params),
33
+ retrieverParams: {},
34
+ module: Modules.EmailBlast,
35
+ });
36
+
37
+ const functions: ReactNode[] = [<EmailBlastEditor key="create-email-blast" />];
38
+
39
+ return (
40
+ <ContentListTable
41
+ data={data}
42
+ fields={[
43
+ EmailBlastFields.subject,
44
+ EmailBlastFields.status,
45
+ EmailBlastFields.sentAt,
46
+ EmailBlastFields.sent,
47
+ EmailBlastFields.failed,
48
+ EmailBlastFields.unsubscribed,
49
+ ]}
50
+ tableGeneratorType={Modules.EmailBlast}
51
+ functions={functions}
52
+ fullWidth={fullWidth}
53
+ title={t("emailBlasts.admin.title")}
54
+ emptyState={<EmptyState icon={SendIcon} title={t("emailBlasts.admin.empty")} />}
55
+ />
56
+ );
57
+ }
@@ -0,0 +1,89 @@
1
+ import { AbstractApiData, JsonApiHydratedDataInterface, Modules } from "../../../core";
2
+ import { EmailBlastInput, EmailBlastInterface, EmailBlastStatus } from "./EmailBlastInterface";
3
+
4
+ export class EmailBlast extends AbstractApiData implements EmailBlastInterface {
5
+ private _subject?: string;
6
+ private _body?: string;
7
+ private _replyTo?: string;
8
+ private _status: EmailBlastStatus = "draft";
9
+ private _sentAt?: Date;
10
+ private _recipientCount?: number;
11
+ private _sentCount?: number;
12
+ private _failedCount?: number;
13
+ private _unsubscribedCount?: number;
14
+
15
+ get subject(): string {
16
+ return this._subject ?? "";
17
+ }
18
+
19
+ get body(): string {
20
+ return this._body ?? "";
21
+ }
22
+
23
+ get replyTo(): string | undefined {
24
+ return this._replyTo;
25
+ }
26
+
27
+ get status(): EmailBlastStatus {
28
+ return this._status;
29
+ }
30
+
31
+ get sentAt(): Date | undefined {
32
+ return this._sentAt;
33
+ }
34
+
35
+ get recipientCount(): number | undefined {
36
+ return this._recipientCount;
37
+ }
38
+
39
+ get sentCount(): number {
40
+ return this._sentCount ?? 0;
41
+ }
42
+
43
+ get failedCount(): number {
44
+ return this._failedCount ?? 0;
45
+ }
46
+
47
+ get unsubscribedCount(): number {
48
+ return this._unsubscribedCount ?? 0;
49
+ }
50
+
51
+ rehydrate(data: JsonApiHydratedDataInterface): this {
52
+ super.rehydrate(data);
53
+
54
+ const attributes = data.jsonApi.attributes;
55
+ this._subject = attributes.subject;
56
+ this._body = attributes.body;
57
+ this._replyTo = attributes.replyTo ?? undefined;
58
+ this._status = attributes.status ?? "draft";
59
+ this._sentAt = attributes.sentAt ? new Date(attributes.sentAt) : undefined;
60
+ this._recipientCount = attributes.recipientCount ?? undefined;
61
+ this._sentCount = attributes.sentCount;
62
+ this._failedCount = attributes.failedCount;
63
+ this._unsubscribedCount = attributes.unsubscribedCount;
64
+
65
+ return this;
66
+ }
67
+
68
+ /**
69
+ * Only the editable text travels: status, counters and sentAt are set by the API.
70
+ */
71
+ createJsonApi(data: EmailBlastInput) {
72
+ const response: any = {
73
+ data: {
74
+ type: Modules.EmailBlast.name,
75
+ id: data.id,
76
+ attributes: {},
77
+ meta: {},
78
+ relationships: {},
79
+ },
80
+ included: [],
81
+ };
82
+
83
+ if (data.subject !== undefined) response.data.attributes.subject = data.subject;
84
+ if (data.body !== undefined) response.data.attributes.body = data.body;
85
+ if (data.replyTo !== undefined) response.data.attributes.replyTo = data.replyTo;
86
+
87
+ return response;
88
+ }
89
+ }
@@ -0,0 +1,22 @@
1
+ import { AbstractApiData, JsonApiHydratedDataInterface } from "../../../core";
2
+ import { EmailBlastAudienceInterface } from "./EmailBlastAudienceInterface";
3
+
4
+ export class EmailBlastAudience extends AbstractApiData implements EmailBlastAudienceInterface {
5
+ private _recipientCount?: number;
6
+
7
+ get recipientCount(): number {
8
+ return this._recipientCount ?? 0;
9
+ }
10
+
11
+ rehydrate(data: JsonApiHydratedDataInterface): this {
12
+ super.rehydrate(data);
13
+
14
+ this._recipientCount = data.jsonApi.attributes.recipientCount;
15
+
16
+ return this;
17
+ }
18
+
19
+ createJsonApi(_data?: any): any {
20
+ throw new Error("EmailBlastAudience is computed by the API and is read-only");
21
+ }
22
+ }
@@ -0,0 +1,5 @@
1
+ import { ApiDataInterface } from "../../../core";
2
+
3
+ export interface EmailBlastAudienceInterface extends ApiDataInterface {
4
+ get recipientCount(): number;
5
+ }
@@ -0,0 +1,36 @@
1
+ import { AbstractApiData, JsonApiHydratedDataInterface } from "../../../core";
2
+ import { EmailBlastDeliveryInterface, EmailBlastDeliveryStatus } from "./EmailBlastDeliveryInterface";
3
+
4
+ /** JSON:API type of the POST email-blasts/unsubscribe/:token request body (not a module: nothing is stored). */
5
+ export const EMAIL_BLAST_UNSUBSCRIBE_TYPE = "email-blast-unsubscribes";
6
+
7
+ export class EmailBlastDelivery extends AbstractApiData implements EmailBlastDeliveryInterface {
8
+ private _status: EmailBlastDeliveryStatus = "pending";
9
+
10
+ get status(): EmailBlastDeliveryStatus {
11
+ return this._status;
12
+ }
13
+
14
+ rehydrate(data: JsonApiHydratedDataInterface): this {
15
+ super.rehydrate(data);
16
+
17
+ this._status = data.jsonApi.attributes.status ?? "pending";
18
+
19
+ return this;
20
+ }
21
+
22
+ createJsonApi(_data?: any): any {
23
+ throw new Error("EmailBlastDelivery is created by the API and cannot be created from the client");
24
+ }
25
+
26
+ /**
27
+ * Body of POST email-blasts/unsubscribe/:token.
28
+ */
29
+ createUnsubscribeJsonApi() {
30
+ return {
31
+ data: {
32
+ type: EMAIL_BLAST_UNSUBSCRIBE_TYPE,
33
+ },
34
+ };
35
+ }
36
+ }
@@ -0,0 +1,7 @@
1
+ import { ApiDataInterface } from "../../../core";
2
+
3
+ export type EmailBlastDeliveryStatus = "pending" | "sending" | "sent" | "skipped" | "failed";
4
+
5
+ export interface EmailBlastDeliveryInterface extends ApiDataInterface {
6
+ get status(): EmailBlastDeliveryStatus;
7
+ }
@@ -0,0 +1,25 @@
1
+ import { AbstractService, EndpointCreator, HttpMethod, Modules } from "../../../core";
2
+ import { EMAIL_BLAST_SEGMENTS } from "./email-blast.segments";
3
+ import { EmailBlastDelivery } from "./EmailBlastDelivery";
4
+
5
+ export class EmailBlastDeliveryService extends AbstractService {
6
+ /**
7
+ * POST email-blasts/unsubscribe/:token (public). Replies 204: the token holder
8
+ * receives nothing back.
9
+ */
10
+ static async unsubscribe(params: { token: string }): Promise<void> {
11
+ const delivery = new EmailBlastDelivery();
12
+
13
+ await this.callApi({
14
+ type: Modules.EmailBlastDelivery,
15
+ method: HttpMethod.POST,
16
+ endpoint: new EndpointCreator({
17
+ endpoint: Modules.EmailBlast,
18
+ id: EMAIL_BLAST_SEGMENTS.unsubscribe,
19
+ childEndpoint: params.token,
20
+ }).generate(),
21
+ input: delivery.createUnsubscribeJsonApi(),
22
+ overridesJsonApiCreation: true,
23
+ });
24
+ }
25
+ }
@@ -0,0 +1,22 @@
1
+ import { ApiDataInterface } from "../../../core";
2
+
3
+ export type EmailBlastStatus = "draft" | "sending" | "sent";
4
+
5
+ export type EmailBlastInput = {
6
+ id: string;
7
+ subject: string;
8
+ body: string;
9
+ replyTo?: string;
10
+ };
11
+
12
+ export interface EmailBlastInterface extends ApiDataInterface {
13
+ get subject(): string;
14
+ get body(): string;
15
+ get replyTo(): string | undefined;
16
+ get status(): EmailBlastStatus;
17
+ get sentAt(): Date | undefined;
18
+ get recipientCount(): number | undefined;
19
+ get sentCount(): number;
20
+ get failedCount(): number;
21
+ get unsubscribedCount(): number;
22
+ }
@@ -0,0 +1,101 @@
1
+ import { AbstractService, EndpointCreator, HttpMethod, Modules, NextRef, PreviousRef } from "../../../core";
2
+ import { EMAIL_BLAST_SEGMENTS } from "./email-blast.segments";
3
+ import { EmailBlastAudienceInterface } from "./EmailBlastAudienceInterface";
4
+ import { EmailBlastInput, EmailBlastInterface } from "./EmailBlastInterface";
5
+
6
+ export class EmailBlastService extends AbstractService {
7
+ static async findMany(params: { next?: NextRef; previous?: PreviousRef } = {}): Promise<EmailBlastInterface[]> {
8
+ const endpoint = new EndpointCreator({ endpoint: Modules.EmailBlast });
9
+
10
+ if (Modules.EmailBlast.inclusions?.lists?.fields)
11
+ endpoint.limitToFields(Modules.EmailBlast.inclusions.lists.fields);
12
+ if (Modules.EmailBlast.inclusions?.lists?.types) endpoint.limitToType(Modules.EmailBlast.inclusions.lists.types);
13
+
14
+ return this.callApi<EmailBlastInterface[]>({
15
+ type: Modules.EmailBlast,
16
+ method: HttpMethod.GET,
17
+ endpoint: endpoint.generate(),
18
+ next: params.next,
19
+ previous: params.previous,
20
+ });
21
+ }
22
+
23
+ static async findOne(params: { id: string }): Promise<EmailBlastInterface> {
24
+ return this.callApi<EmailBlastInterface>({
25
+ type: Modules.EmailBlast,
26
+ method: HttpMethod.GET,
27
+ endpoint: new EndpointCreator({ endpoint: Modules.EmailBlast, id: params.id }).generate(),
28
+ });
29
+ }
30
+
31
+ static async create(params: EmailBlastInput): Promise<EmailBlastInterface> {
32
+ return this.callApi<EmailBlastInterface>({
33
+ type: Modules.EmailBlast,
34
+ method: HttpMethod.POST,
35
+ endpoint: new EndpointCreator({ endpoint: Modules.EmailBlast }).generate(),
36
+ input: params,
37
+ });
38
+ }
39
+
40
+ static async update(params: EmailBlastInput): Promise<EmailBlastInterface> {
41
+ return this.callApi<EmailBlastInterface>({
42
+ type: Modules.EmailBlast,
43
+ method: HttpMethod.PATCH,
44
+ endpoint: new EndpointCreator({ endpoint: Modules.EmailBlast, id: params.id }).generate(),
45
+ input: params,
46
+ });
47
+ }
48
+
49
+ static async delete(params: { id: string }): Promise<void> {
50
+ await this.callApi({
51
+ type: Modules.EmailBlast,
52
+ method: HttpMethod.DELETE,
53
+ endpoint: new EndpointCreator({ endpoint: Modules.EmailBlast, id: params.id }).generate(),
54
+ });
55
+ }
56
+
57
+ /**
58
+ * GET email-blasts/:id/recipient-count: how many people the blast would reach now.
59
+ */
60
+ static async findAudience(params: { id: string }): Promise<EmailBlastAudienceInterface> {
61
+ return this.callApi<EmailBlastAudienceInterface>({
62
+ type: Modules.EmailBlastAudience,
63
+ method: HttpMethod.GET,
64
+ endpoint: new EndpointCreator({
65
+ endpoint: Modules.EmailBlast,
66
+ id: params.id,
67
+ childEndpoint: EMAIL_BLAST_SEGMENTS.audience,
68
+ }).generate(),
69
+ });
70
+ }
71
+
72
+ /**
73
+ * POST email-blasts/:id/test: sends the draft to the calling administrator. Replies 204.
74
+ */
75
+ static async sendTest(params: { id: string }): Promise<void> {
76
+ await this.callApi({
77
+ type: Modules.EmailBlast,
78
+ method: HttpMethod.POST,
79
+ endpoint: new EndpointCreator({
80
+ endpoint: Modules.EmailBlast,
81
+ id: params.id,
82
+ childEndpoint: EMAIL_BLAST_SEGMENTS.test,
83
+ }).generate(),
84
+ });
85
+ }
86
+
87
+ /**
88
+ * POST email-blasts/:id/send: starts the send and returns the blast, now `sending`.
89
+ */
90
+ static async send(params: { id: string }): Promise<EmailBlastInterface> {
91
+ return this.callApi<EmailBlastInterface>({
92
+ type: Modules.EmailBlast,
93
+ method: HttpMethod.POST,
94
+ endpoint: new EndpointCreator({
95
+ endpoint: Modules.EmailBlast,
96
+ id: params.id,
97
+ childEndpoint: EMAIL_BLAST_SEGMENTS.send,
98
+ }).generate(),
99
+ });
100
+ }
101
+ }