@dev.smartpricing/message-composer-utils 3.1.2

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 (119) hide show
  1. package/dist/types/api/aiIntegration.js +7 -0
  2. package/dist/types/api/aiIntegration.js.map +1 -0
  3. package/dist/types/api/apiMessages.js +2 -0
  4. package/dist/types/api/apiMessages.js.map +1 -0
  5. package/dist/types/api/index.js +3 -0
  6. package/dist/types/api/index.js.map +1 -0
  7. package/dist/types/app.js +23 -0
  8. package/dist/types/app.js.map +1 -0
  9. package/dist/types/brand.js +31 -0
  10. package/dist/types/brand.js.map +1 -0
  11. package/dist/types/dynamic-values.js +5 -0
  12. package/dist/types/dynamic-values.js.map +1 -0
  13. package/dist/types/emailblocks.v1.defaults.js +83 -0
  14. package/dist/types/emailblocks.v1.defaults.js.map +1 -0
  15. package/dist/types/emailblocks.v1.js +339 -0
  16. package/dist/types/emailblocks.v1.js.map +1 -0
  17. package/dist/types/index.js +16 -0
  18. package/dist/types/index.js.map +1 -0
  19. package/dist/types/kafka-events.js +2 -0
  20. package/dist/types/kafka-events.js.map +1 -0
  21. package/dist/types/language.js +2 -0
  22. package/dist/types/language.js.map +1 -0
  23. package/dist/types/media.js +6 -0
  24. package/dist/types/media.js.map +1 -0
  25. package/dist/types/message.js +41 -0
  26. package/dist/types/message.js.map +1 -0
  27. package/dist/types/meta-template.js +2 -0
  28. package/dist/types/meta-template.js.map +1 -0
  29. package/dist/types/meta-webhook.js +2 -0
  30. package/dist/types/meta-webhook.js.map +1 -0
  31. package/dist/types/submission-job.js +2 -0
  32. package/dist/types/submission-job.js.map +1 -0
  33. package/dist/types/tag.js +2 -0
  34. package/dist/types/tag.js.map +1 -0
  35. package/dist/types/utils.js +2 -0
  36. package/dist/types/utils.js.map +1 -0
  37. package/dist/types/whatsappblocks.v1.js +24 -0
  38. package/dist/types/whatsappblocks.v1.js.map +1 -0
  39. package/dist/utils/array.js +26 -0
  40. package/dist/utils/array.js.map +1 -0
  41. package/dist/utils/async.js +22 -0
  42. package/dist/utils/async.js.map +1 -0
  43. package/dist/utils/date.js +37 -0
  44. package/dist/utils/date.js.map +1 -0
  45. package/dist/utils/index.js +34 -0
  46. package/dist/utils/index.js.map +1 -0
  47. package/dist/utils/intl.js +37 -0
  48. package/dist/utils/intl.js.map +1 -0
  49. package/dist/utils/meta-status.js +21 -0
  50. package/dist/utils/meta-status.js.map +1 -0
  51. package/dist/utils/number.js +24 -0
  52. package/dist/utils/number.js.map +1 -0
  53. package/dist/utils/object.js +87 -0
  54. package/dist/utils/object.js.map +1 -0
  55. package/dist/utils/string.js +22 -0
  56. package/dist/utils/string.js.map +1 -0
  57. package/dist/utils/templates/brand-cascade.js +116 -0
  58. package/dist/utils/templates/brand-cascade.js.map +1 -0
  59. package/dist/utils/templates/email-compiler.js +126 -0
  60. package/dist/utils/templates/email-compiler.js.map +1 -0
  61. package/dist/utils/templates/email-mjml.js +444 -0
  62. package/dist/utils/templates/email-mjml.js.map +1 -0
  63. package/dist/utils/templates/email.js +580 -0
  64. package/dist/utils/templates/email.js.map +1 -0
  65. package/dist/utils/templates/html-sanitizer.js +95 -0
  66. package/dist/utils/templates/html-sanitizer.js.map +1 -0
  67. package/dist/utils/templates/messages.js +33 -0
  68. package/dist/utils/templates/messages.js.map +1 -0
  69. package/dist/utils/templates/meta.js +246 -0
  70. package/dist/utils/templates/meta.js.map +1 -0
  71. package/dist/utils/templates/tiptap.js +167 -0
  72. package/dist/utils/templates/tiptap.js.map +1 -0
  73. package/dist/utils/templates/whatsapp-compiler.js +168 -0
  74. package/dist/utils/templates/whatsapp-compiler.js.map +1 -0
  75. package/dist/utils/templates/whatsapp.js +329 -0
  76. package/dist/utils/templates/whatsapp.js.map +1 -0
  77. package/dist/utils/validations.js +39 -0
  78. package/dist/utils/validations.js.map +1 -0
  79. package/package.json +43 -0
  80. package/src/types/api/aiIntegration.ts +59 -0
  81. package/src/types/api/apiMessages.ts +22 -0
  82. package/src/types/api/index.ts +2 -0
  83. package/src/types/app.ts +76 -0
  84. package/src/types/brand.ts +80 -0
  85. package/src/types/dynamic-values.ts +47 -0
  86. package/src/types/emailblocks.v1.defaults.ts +123 -0
  87. package/src/types/emailblocks.v1.ts +655 -0
  88. package/src/types/index.ts +15 -0
  89. package/src/types/kafka-events.ts +46 -0
  90. package/src/types/language.ts +9 -0
  91. package/src/types/media.ts +26 -0
  92. package/src/types/message.ts +635 -0
  93. package/src/types/meta-template.ts +50 -0
  94. package/src/types/meta-webhook.ts +32 -0
  95. package/src/types/submission-job.ts +30 -0
  96. package/src/types/tag.ts +8 -0
  97. package/src/types/utils.ts +14 -0
  98. package/src/types/whatsappblocks.v1.ts +102 -0
  99. package/src/utils/array.ts +33 -0
  100. package/src/utils/async.ts +30 -0
  101. package/src/utils/date.ts +39 -0
  102. package/src/utils/index.ts +44 -0
  103. package/src/utils/intl.ts +51 -0
  104. package/src/utils/meta-status.ts +24 -0
  105. package/src/utils/number.ts +36 -0
  106. package/src/utils/object.ts +101 -0
  107. package/src/utils/string.ts +24 -0
  108. package/src/utils/templates/brand-cascade.ts +156 -0
  109. package/src/utils/templates/email-compiler.ts +155 -0
  110. package/src/utils/templates/email-mjml.ts +602 -0
  111. package/src/utils/templates/email.ts +713 -0
  112. package/src/utils/templates/html-sanitizer.ts +121 -0
  113. package/src/utils/templates/messages.ts +44 -0
  114. package/src/utils/templates/meta.ts +336 -0
  115. package/src/utils/templates/tiptap.ts +247 -0
  116. package/src/utils/templates/whatsapp-compiler.ts +212 -0
  117. package/src/utils/templates/whatsapp.ts +408 -0
  118. package/src/utils/validations.ts +49 -0
  119. package/tsconfig.json +10 -0
@@ -0,0 +1,247 @@
1
+ import { type Content, type Extensions } from "@tiptap/core";
2
+ import { generateJSON } from "@tiptap/html";
3
+ import StarterKit from "@tiptap/starter-kit";
4
+ import Mention from "@tiptap/extension-mention";
5
+ import Emoji, { gitHubEmojis } from "@tiptap/extension-emoji";
6
+ import { CharacterCount } from "@tiptap/extensions";
7
+ import TextAlign from "@tiptap/extension-text-align";
8
+ import type { JSONContent } from "@tiptap/core";
9
+ import { RenderType, WA_BODY_MAX_LENGTH } from "../../types/index.js";
10
+ import { generateHtml, generateText } from "./messages.js";
11
+
12
+ export interface TipTapExtensionOptions {
13
+ preset?: "whatsapp" | "whatsapp-header" | "email";
14
+ bold?: boolean;
15
+ italic?: boolean;
16
+ emoji?: boolean;
17
+ underline?: boolean;
18
+ link?: boolean;
19
+ bulletList?: boolean;
20
+ orderedList?: boolean;
21
+ textAlign?: boolean;
22
+ dynamicValues?: boolean;
23
+ characterLimit?: number;
24
+ horizontalRule?: boolean;
25
+ hardBreak?: boolean;
26
+ }
27
+
28
+ const PRESETS: Record<
29
+ NonNullable<TipTapExtensionOptions["preset"]>,
30
+ Omit<TipTapExtensionOptions, "preset" | "dynamicValues" | "characterLimit">
31
+ > = {
32
+ whatsapp: {
33
+ bold: true,
34
+ italic: true,
35
+ emoji: true,
36
+ },
37
+ "whatsapp-header": {
38
+ bold: true,
39
+ italic: true,
40
+ emoji: true,
41
+ },
42
+ email: {
43
+ bold: true,
44
+ italic: true,
45
+ emoji: true,
46
+ underline: true,
47
+ link: true,
48
+ bulletList: true,
49
+ orderedList: true,
50
+ textAlign: true,
51
+ horizontalRule: true,
52
+ hardBreak: true,
53
+ },
54
+ };
55
+
56
+ export function getTipTapExtensions(
57
+ options: TipTapExtensionOptions = {},
58
+ ): Extensions {
59
+ const presetOptions = options.preset ? PRESETS[options.preset] : {};
60
+ const merged = { ...presetOptions, ...options };
61
+
62
+ const {
63
+ bold = false,
64
+ italic = false,
65
+ emoji = false,
66
+ underline = false,
67
+ link = false,
68
+ bulletList = false,
69
+ orderedList = false,
70
+ textAlign = false,
71
+ dynamicValues = true,
72
+ characterLimit,
73
+ horizontalRule = false,
74
+ hardBreak = false,
75
+ } = merged;
76
+
77
+ const extensions: Extensions = [];
78
+
79
+ // Add character count if limit is set
80
+ if (characterLimit) {
81
+ extensions.push(
82
+ CharacterCount.configure({
83
+ limit: characterLimit,
84
+ }),
85
+ );
86
+ }
87
+
88
+ // Add mention support if dynamic values are provided
89
+ if (dynamicValues) {
90
+ extensions.push(
91
+ Mention.configure({
92
+ renderText: (mention) => `{{${mention.node.attrs.label}}}`,
93
+ renderHTML: (mention) =>
94
+ `{{${mention.node.attrs.label || mention.node.attrs.id}}}`,
95
+ HTMLAttributes: {
96
+ class: "dynamic-data",
97
+ },
98
+ deleteTriggerWithBackspace: true,
99
+ }),
100
+ );
101
+ }
102
+
103
+ // Add emoji support if enabled
104
+ if (emoji) {
105
+ extensions.push(
106
+ Emoji.configure({
107
+ emojis: gitHubEmojis,
108
+ HTMLAttributes: {
109
+ class: "tiptap-emoji",
110
+ },
111
+ suggestion: {
112
+ char: ":",
113
+ items: (props) =>
114
+ gitHubEmojis.filter((emoji) => emoji.name.includes(props.query)),
115
+ },
116
+ }),
117
+ );
118
+ }
119
+
120
+ // Add text align extension if enabled
121
+ if (textAlign) {
122
+ extensions.push(
123
+ TextAlign.configure({
124
+ types: ["paragraph"],
125
+ }),
126
+ );
127
+ }
128
+
129
+ // Configure StarterKit based on enabled features
130
+ extensions.push(
131
+ StarterKit.configure({
132
+ bold: bold ? undefined : false,
133
+ italic: italic ? undefined : false,
134
+ underline: underline ? undefined : false,
135
+ link: link
136
+ ? {
137
+ openOnClick: false,
138
+ }
139
+ : false,
140
+ heading: false,
141
+ bulletList: bulletList ? undefined : false,
142
+ orderedList: orderedList ? undefined : false,
143
+ listItem: bulletList || orderedList ? undefined : false,
144
+ blockquote: false,
145
+ code: false,
146
+ codeBlock: false,
147
+ horizontalRule: horizontalRule
148
+ ? {
149
+ HTMLAttributes: {
150
+ style: "border-top: 1px solid #E5E7EB; margin: 16px 0;",
151
+ },
152
+ }
153
+ : false,
154
+ hardBreak: hardBreak ? undefined : false,
155
+ strike: false,
156
+ }),
157
+ );
158
+
159
+ return extensions;
160
+ }
161
+
162
+ export function getAllMentions(content: Content | undefined): Array<Content> {
163
+ if (!content) return [];
164
+
165
+ function processNode(node: Content): Array<Content> {
166
+ if (typeof node === "string") {
167
+ return [];
168
+ }
169
+
170
+ if (Array.isArray(node)) {
171
+ return node.map(processNode);
172
+ }
173
+
174
+ if (!node || typeof node !== "object") {
175
+ return [];
176
+ }
177
+
178
+ // Process content recursively
179
+ if (node.content) {
180
+ const a = node.content.map(processNode);
181
+ return a.flat();
182
+ }
183
+
184
+ // Handle block-level elements
185
+ switch (node.type) {
186
+ case "mention":
187
+ return [node];
188
+
189
+ default:
190
+ return [];
191
+ }
192
+ }
193
+
194
+ return (processNode(content)?.flat().filter(Boolean) as Array<Content>) ?? [];
195
+ }
196
+
197
+ export function extractText(
198
+ field: JSONContent | string | undefined,
199
+ renderType: RenderType,
200
+ ): string {
201
+ if (!field) return "";
202
+
203
+ // If it's TipTap content (object with type and content)
204
+ if (typeof field === "object" && field.type) {
205
+ return generateHtml(field, renderType);
206
+ }
207
+
208
+ // If it's plain text
209
+ if (typeof field === "string") {
210
+ return field;
211
+ }
212
+
213
+ return "";
214
+ }
215
+
216
+ export function isTiptapEmpty(
217
+ field: JSONContent | string | undefined,
218
+ renderType: RenderType,
219
+ ): boolean {
220
+ if (!field) return true;
221
+
222
+ // For TipTap content
223
+ if (typeof field === "object" && field.type) {
224
+ const text = generateText(field, renderType);
225
+ return !text || text.trim() === "";
226
+ }
227
+
228
+ // For plain text
229
+ if (typeof field === "string") {
230
+ return !field || field.trim() === "";
231
+ }
232
+
233
+ return true;
234
+ }
235
+
236
+ /**
237
+ * Create TipTap JSON structure from plain text
238
+ */
239
+ export function createTipTapContent(
240
+ text: string,
241
+ options: TipTapExtensionOptions = {
242
+ preset: "whatsapp",
243
+ characterLimit: WA_BODY_MAX_LENGTH,
244
+ },
245
+ ) {
246
+ return generateJSON(text, getTipTapExtensions(options));
247
+ }
@@ -0,0 +1,212 @@
1
+ import type {
2
+ WhatsappMessage,
3
+ WhatsAppSendPayload,
4
+ ParameterValidationResult,
5
+ WhatsappHeaderParam,
6
+ } from "../../types/index.js";
7
+ import type { JSONContent } from "@tiptap/core";
8
+ import { convertToWhatsappMarkdown } from "./whatsapp.js";
9
+
10
+ /**
11
+ * Normalize parameters to strings for Meta API compatibility
12
+ * Handles null, undefined, numbers, booleans, objects
13
+ */
14
+ export function normalizeParameters(
15
+ parameters: Record<string, unknown>,
16
+ ): Record<string, string> {
17
+ return Object.fromEntries(
18
+ Object.entries(parameters).map(([key, value]) => {
19
+ // Coerce to string
20
+ if (value === null || value === undefined) return [key, ""];
21
+ if (typeof value === "string") return [key, value]; // Allow empty strings
22
+ return [key, String(value)]; // Numbers, booleans, objects
23
+ }),
24
+ );
25
+ }
26
+
27
+ /**
28
+ * Extract parameter names from text containing {{param_name}} placeholders
29
+ */
30
+ function extractParameters(text: string): string[] {
31
+ const paramRegex = /\{\{([^}]+)\}\}/g;
32
+ const matches = [...text.matchAll(paramRegex)];
33
+ return matches.map((match) => match[1].trim());
34
+ }
35
+
36
+ /**
37
+ * Convert JSONContent to string for parameter extraction
38
+ */
39
+ function contentToString(content: JSONContent | undefined): string {
40
+ if (!content) return "";
41
+ return convertToWhatsappMarkdown(content);
42
+ }
43
+
44
+ /**
45
+ * Extract all required parameters from a WhatsApp message template
46
+ */
47
+ export function extractRequiredParameters(message: WhatsappMessage): string[] {
48
+ const parameters = new Set<string>();
49
+
50
+ // Extract from header if it's text
51
+ if (message.body.header.type === "text" && message.body.header.content) {
52
+ const headerText = contentToString(message.body.header.content);
53
+ extractParameters(headerText).forEach((p) => parameters.add(p));
54
+ }
55
+
56
+ // Extract from body
57
+ if (message.body.body.content) {
58
+ const bodyText = contentToString(message.body.body.content);
59
+ extractParameters(bodyText).forEach((p) => parameters.add(p));
60
+ }
61
+
62
+ // Extract from button URLs
63
+ if (message.body.buttons.enabled) {
64
+ message.body.buttons.buttons.forEach((button) => {
65
+ if (button.type === "link" && button.url) {
66
+ extractParameters(button.url).forEach((p) => parameters.add(p));
67
+ }
68
+ });
69
+ }
70
+
71
+ return Array.from(parameters);
72
+ }
73
+
74
+ /**
75
+ * Validate that all required parameters are provided
76
+ */
77
+ export function validateParameters(
78
+ message: WhatsappMessage,
79
+ parameters: Record<string, unknown>,
80
+ ): ParameterValidationResult {
81
+ const required = extractRequiredParameters(message);
82
+ const provided = Object.keys(parameters);
83
+
84
+ const missing = required.filter((p) => !provided.includes(p));
85
+
86
+ return {
87
+ valid: missing.length === 0,
88
+ required,
89
+ missing,
90
+ };
91
+ }
92
+
93
+ /**
94
+ * Build component parameters array for Meta API
95
+ */
96
+ function buildComponentParameters(
97
+ text: string,
98
+ parameters: Record<string, string>,
99
+ ): Array<{ type: "text"; text: string; parameter_name: string }> {
100
+ const paramNames = extractParameters(text);
101
+ return paramNames.map((paramName) => ({
102
+ type: "text" as const,
103
+ text: parameters[paramName] ?? "",
104
+ parameter_name: paramName,
105
+ }));
106
+ }
107
+
108
+ /**
109
+ * Compile WhatsApp message template into Meta Cloud API send format
110
+ *
111
+ * @param message - The WhatsApp message template to compile
112
+ * @param parameters - Key-value pairs for parameter injection (will be normalized to strings)
113
+ * @returns WhatsApp Cloud API send payload (without 'to' field)
114
+ */
115
+ export function compileWhatsappTemplate(
116
+ message: WhatsappMessage,
117
+ parameters: Record<string, unknown>,
118
+ ): Omit<WhatsAppSendPayload, "to"> {
119
+ // Normalize parameters to strings first
120
+ const normalizedParams = normalizeParameters(parameters);
121
+
122
+ const components: NonNullable<WhatsAppSendPayload["template"]["components"]> =
123
+ [];
124
+
125
+ // Get template name from metadata
126
+ const templateName = message.metadata?.meta?.template_name;
127
+ if (!templateName) {
128
+ throw new Error("Message does not have Meta template name in metadata");
129
+ }
130
+
131
+ // Build header component if exists
132
+ if (message.body.header.type === "text" && message.body.header.content) {
133
+ const headerText = contentToString(message.body.header.content);
134
+ const headerParams = buildComponentParameters(headerText, normalizedParams);
135
+
136
+ if (headerParams.length > 0) {
137
+ components.push({
138
+ type: "HEADER",
139
+ parameters: headerParams,
140
+ });
141
+ }
142
+ } else if (
143
+ (message.body.header.type === "image" ||
144
+ message.body.header.type === "video" ||
145
+ message.body.header.type === "document") &&
146
+ message.body.header.smartchatMediaId
147
+ ) {
148
+ const t = message.body.header.type;
149
+ components.push({
150
+ type: "HEADER",
151
+ parameters: [
152
+ {
153
+ type: t,
154
+ [t]: {
155
+ id: message.body.header.smartchatMediaId,
156
+ },
157
+ } as WhatsappHeaderParam,
158
+ ],
159
+ });
160
+ }
161
+
162
+ // Build body component (always present if it has parameters)
163
+ if (message.body.body.content) {
164
+ const bodyText = contentToString(message.body.body.content);
165
+ const bodyParams = buildComponentParameters(bodyText, normalizedParams);
166
+
167
+ if (bodyParams.length > 0) {
168
+ components.push({
169
+ type: "BODY",
170
+ parameters: bodyParams,
171
+ });
172
+ }
173
+ }
174
+
175
+ // Note: Footer doesn't support parameters in Meta API
176
+
177
+ // Build button components for URL buttons with parameters
178
+ if (message.body.buttons.enabled) {
179
+ message.body.buttons.buttons.forEach((button, index) => {
180
+ if (button.type === "link" && button.url) {
181
+ const urlParams = buildComponentParameters(
182
+ button.url,
183
+ normalizedParams,
184
+ );
185
+ urlParams.forEach((param) => {
186
+ param.parameter_name = `{{${index + 1}}}`;
187
+ });
188
+
189
+ if (urlParams.length > 0) {
190
+ components.push({
191
+ type: "BUTTON",
192
+ sub_type: "url",
193
+ index: String(index),
194
+ parameters: urlParams,
195
+ });
196
+ }
197
+ }
198
+ });
199
+ }
200
+
201
+ return {
202
+ messaging_product: "whatsapp",
203
+ type: "template",
204
+ template: {
205
+ name: templateName,
206
+ language: {
207
+ code: message.language_id,
208
+ },
209
+ components: components.length > 0 ? components : undefined,
210
+ },
211
+ };
212
+ }