@ganju/utils 0.0.18 → 0.0.21
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.
- package/dist/constants.d.ts +18 -0
- package/dist/constants.d.ts.map +1 -1
- package/dist/constants.js +50 -3
- package/dist/crypto.d.ts +2 -2
- package/dist/crypto.d.ts.map +1 -1
- package/dist/crypto.js +1 -0
- package/dist/data.d.ts +6 -4
- package/dist/data.d.ts.map +1 -1
- package/dist/data.js +13 -3
- package/dist/dataApi.d.ts +54 -8
- package/dist/dataApi.d.ts.map +1 -1
- package/dist/dataApi.js +40 -7
- package/dist/dataAttachment.d.ts +8 -0
- package/dist/dataAttachment.d.ts.map +1 -0
- package/dist/dataAttachment.js +19 -0
- package/dist/dataCompiler.d.ts +3 -0
- package/dist/dataCompiler.d.ts.map +1 -1
- package/dist/dataCompiler.js +25 -1
- package/dist/dataDdl.d.ts.map +1 -1
- package/dist/dataDdl.js +5 -0
- package/dist/dataEngine.d.ts +9 -3
- package/dist/dataEngine.d.ts.map +1 -1
- package/dist/dataEngine.js +141 -5
- package/dist/dataIndexAdvice.d.ts +36 -0
- package/dist/dataIndexAdvice.d.ts.map +1 -0
- package/dist/dataIndexAdvice.js +89 -0
- package/dist/dataRpc.d.ts +12 -1
- package/dist/dataRpc.d.ts.map +1 -1
- package/dist/dataTools.js +3 -3
- package/dist/dataTriggers.d.ts +23 -0
- package/dist/dataTriggers.d.ts.map +1 -1
- package/dist/dataTriggers.js +46 -2
- package/dist/dataTypes.d.ts +46 -0
- package/dist/dataTypes.d.ts.map +1 -1
- package/dist/dataTypes.js +7 -1
- package/dist/index.d.ts +138 -4
- package/dist/index.d.ts.map +1 -1
- package/dist/index.js +31 -0
- package/dist/localizeDataError.d.ts.map +1 -1
- package/dist/localizeDataError.js +64 -2
- package/dist/schema.d.ts +85 -2
- package/dist/schema.d.ts.map +1 -1
- package/dist/schema.js +94 -1
- package/dist/sdkConstants.d.ts +1 -0
- package/dist/sdkConstants.d.ts.map +1 -1
- package/dist/sdkConstants.js +2 -1
- package/dist/whatsappTemplates.d.ts +77 -0
- package/dist/whatsappTemplates.d.ts.map +1 -0
- package/dist/whatsappTemplates.js +402 -0
- package/dist/whatsappWebhook.d.ts +28 -0
- package/dist/whatsappWebhook.d.ts.map +1 -0
- package/dist/whatsappWebhook.js +30 -0
- package/package.json +1 -1
- package/src/constants.ts +57 -3
- package/src/crypto.ts +3 -2
- package/src/data.ts +14 -0
- package/src/dataApi.ts +44 -6
- package/src/dataAttachment.ts +23 -0
- package/src/dataCompiler.ts +37 -0
- package/src/dataDdl.ts +7 -0
- package/src/dataEngine.ts +195 -5
- package/src/dataIndexAdvice.ts +127 -0
- package/src/dataRpc.ts +17 -1
- package/src/dataTools.ts +3 -3
- package/src/dataTriggers.ts +71 -1
- package/src/dataTypes.ts +62 -0
- package/src/index.ts +74 -1
- package/src/localizeDataError.ts +101 -3
- package/src/schema.ts +115 -1
- package/src/sdkConstants.ts +1 -0
- package/src/whatsappTemplates.ts +465 -0
- package/src/whatsappWebhook.ts +55 -0
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"whatsappTemplates.d.ts","sourceRoot":"","sources":["../src/whatsappTemplates.ts"],"names":[],"mappings":"AAIA,eAAO,MAAM,4BAA4B,mCAAoC,CAAC;AAC9E,MAAM,MAAM,wBAAwB,GAClC,CAAC,OAAO,4BAA4B,CAAC,CAAC,MAAM,CAAC,CAAC;AAGhD,eAAO,MAAM,iCAAiC,aAAa,CAAC;AAE5D,eAAO,MAAM,kCAAkC,UAI9C,CAAC;AAEF,eAAO,MAAM,kCAAkC,UAAyB,CAAC;AAEzE,eAAO,MAAM,0BAA0B,MAAM,CAAC;AAC9C,eAAO,MAAM,0BAA0B,OAAO,CAAC;AAC/C,eAAO,MAAM,4BAA4B,KAAK,CAAC;AAC/C,eAAO,MAAM,4BAA4B,KAAK,CAAC;AAC/C,eAAO,MAAM,4BAA4B,KAAK,CAAC;AAC/C,eAAO,MAAM,6BAA6B,KAAK,CAAC;AAChD,eAAO,MAAM,+BAA+B,KAAK,CAAC;AAClD,eAAO,MAAM,2BAA2B,KAAK,CAAC;AAC9C,eAAO,MAAM,6BAA6B,MAAM,CAAC;AAIjD,eAAO,MAAM,2BAA2B,EAAE;IAAE,IAAI,EAAE,MAAM,CAAC;IAAC,IAAI,EAAE,MAAM,CAAA;CAAE,EAwEvE,CAAC;AAEF,sFAAsF;AACtF,MAAM,WAAW,qBAAqB;IACpC,IAAI,EAAE,MAAM,CAAC;IACb,QAAQ,EAAE,MAAM,CAAC;IACjB,QAAQ,EAAE,wBAAwB,CAAC;IACnC,MAAM,CAAC,EAAE,MAAM,GAAG,IAAI,CAAC;IACvB,IAAI,EAAE,MAAM,CAAC;IACb,MAAM,CAAC,EAAE,MAAM,GAAG,IAAI,CAAC;IACvB,OAAO,CAAC,EAAE,MAAM,EAAE,CAAC;IACnB,QAAQ,CAAC,EAAE,MAAM,EAAE,CAAC;IACpB,MAAM,CAAC,EAAE,MAAM,EAAE,CAAC;CACnB;AAED,2DAA2D;AAC3D,MAAM,WAAW,uBAAuB;IACtC,MAAM,EAAE,MAAM,GAAG,IAAI,CAAC;IACtB,IAAI,EAAE,MAAM,CAAC;IACb,MAAM,EAAE,MAAM,GAAG,IAAI,CAAC;IACtB,OAAO,EAAE,MAAM,EAAE,CAAC;IAClB,QAAQ,EAAE,MAAM,EAAE,CAAC;CACpB;AAED,MAAM,MAAM,yBAAyB,GACjC,aAAa,GACb,WAAW,GACX,UAAU,GACV,UAAU,GACV,YAAY,GACZ,WAAW,GACX,aAAa,GACb,kBAAkB,GAClB,cAAc,GACd,aAAa,GACb,kBAAkB,GAClB,cAAc,GACd,aAAa,GACb,kBAAkB,GAClB,cAAc,GACd,kBAAkB,GAClB,gBAAgB,GAChB,iBAAiB,GACjB,mBAAmB,GACnB,iBAAiB,GACjB,oBAAoB,GACpB,iBAAiB,GACjB,iBAAiB,CAAC;AAEtB,MAAM,WAAW,qBAAqB;IACpC,IAAI,EAAE,yBAAyB,CAAC;IAEhC,CAAC,CAAC,EAAE,MAAM,CAAC;CACZ;AAID,wDAAwD;AACxD,eAAO,MAAM,yBAAyB,GAAI,MAAM,MAAM,KAAG,MAAM,EACC,CAAC;AAEjE,0DAA0D;AAC1D,eAAO,MAAM,6BAA6B,GAAI,MAAM,MAAM,KAAG,MACZ,CAAC;AAQlD;;;;GAIG;AACH,eAAO,MAAM,qBAAqB,GAChC,OAAO,qBAAqB,KAC3B,qBAAqB,EA0DvB,CAAC;AA+BF,eAAO,MAAM,yBAAyB,GAAI,OAAO,qBAAqB,WAGrB,CAAC;AAElD,wDAAwD;AACxD,eAAO,MAAM,0BAA0B,GACrC,OAAO,qBAAqB,KAC3B,MAAM,CAAC,MAAM,EAAE,OAAO,CAAC,EA+BzB,CAAC;AAEF,2DAA2D;AAC3D,eAAO,MAAM,uBAAuB,GAClC,YAAY,OAAO,KAClB,uBA6BF,CAAC;AAEF;;;;GAIG;AACH,eAAO,MAAM,wBAAwB,GAAI,YAAY,OAAO,KAAG,OA4B9D,CAAC;AAEF,8EAA8E;AAC9E,eAAO,MAAM,oBAAoB,GAAI,MAAM,MAAM,EAAE,QAAQ,MAAM,EAAE,KAAG,MACA,CAAC;AAEvE;;;GAGG;AACH,eAAO,MAAM,0BAA0B,GAAI,OAAO,OAAO,KAAG,MAO3D,CAAC;AA0DF,4EAA4E;AAC5E,eAAO,MAAM,iBAAiB,GAC5B,MAAM,MAAM,GAAG,SAAS,EACxB,QAAQ,MAAM,GAAG,SAAS,KACzB;IAAE,IAAI,EAAE,MAAM,CAAC;IAAC,KAAK,EAAE,OAAO,CAAA;CAOhC,CAAC"}
|
|
@@ -0,0 +1,402 @@
|
|
|
1
|
+
"use strict";
|
|
2
|
+
// WhatsApp message templates: the shape Meta's Graph API takes and returns,
|
|
3
|
+
// and the rules it rejects templates for most often, checked before one is
|
|
4
|
+
// sent for review. Imports nothing, so the dashboard runs the same checks.
|
|
5
|
+
Object.defineProperty(exports, "__esModule", { value: true });
|
|
6
|
+
exports.whatsappSendError = exports.cleanWhatsappTemplateParam = exports.fillWhatsappTemplate = exports.whatsappTemplateSendable = exports.whatsappTemplateContent = exports.whatsappTemplateComponents = exports.whatsappTemplateIssueText = exports.checkWhatsappTemplate = exports.whatsappTemplateVariableCount = exports.whatsappTemplateVariables = exports.WHATSAPP_TEMPLATE_LANGUAGES = exports.WHATSAPP_TEMPLATE_EXAMPLE_MAX = exports.WHATSAPP_TEMPLATE_LABEL_MAX = exports.WHATSAPP_TEMPLATE_VARIABLES_MAX = exports.WHATSAPP_TEMPLATE_BUTTONS_MAX = exports.WHATSAPP_TEMPLATE_BUTTON_MAX = exports.WHATSAPP_TEMPLATE_FOOTER_MAX = exports.WHATSAPP_TEMPLATE_HEADER_MAX = exports.WHATSAPP_TEMPLATE_BODY_MAX = exports.WHATSAPP_TEMPLATE_NAME_MAX = exports.WHATSAPP_TEMPLATE_STATUSES_STOPPED = exports.WHATSAPP_TEMPLATE_STATUSES_WAITING = exports.WHATSAPP_TEMPLATE_STATUS_APPROVED = exports.WHATSAPP_TEMPLATE_CATEGORIES = void 0;
|
|
7
|
+
exports.WHATSAPP_TEMPLATE_CATEGORIES = ['UTILITY', 'MARKETING'];
|
|
8
|
+
// What Meta reports. Only APPROVED templates can be sent.
|
|
9
|
+
exports.WHATSAPP_TEMPLATE_STATUS_APPROVED = 'APPROVED';
|
|
10
|
+
// Still in Meta's hands: the hourly sync looks at these again.
|
|
11
|
+
exports.WHATSAPP_TEMPLATE_STATUSES_WAITING = [
|
|
12
|
+
'PENDING',
|
|
13
|
+
'IN_APPEAL',
|
|
14
|
+
'PENDING_DELETION'
|
|
15
|
+
];
|
|
16
|
+
// An approved template Meta stopped: its owners are told.
|
|
17
|
+
exports.WHATSAPP_TEMPLATE_STATUSES_STOPPED = ['PAUSED', 'DISABLED'];
|
|
18
|
+
exports.WHATSAPP_TEMPLATE_NAME_MAX = 512;
|
|
19
|
+
exports.WHATSAPP_TEMPLATE_BODY_MAX = 1024;
|
|
20
|
+
exports.WHATSAPP_TEMPLATE_HEADER_MAX = 60;
|
|
21
|
+
exports.WHATSAPP_TEMPLATE_FOOTER_MAX = 60;
|
|
22
|
+
exports.WHATSAPP_TEMPLATE_BUTTON_MAX = 25;
|
|
23
|
+
exports.WHATSAPP_TEMPLATE_BUTTONS_MAX = 10;
|
|
24
|
+
exports.WHATSAPP_TEMPLATE_VARIABLES_MAX = 20;
|
|
25
|
+
exports.WHATSAPP_TEMPLATE_LABEL_MAX = 40;
|
|
26
|
+
exports.WHATSAPP_TEMPLATE_EXAMPLE_MAX = 200;
|
|
27
|
+
const NAME_PATTERN = /^[a-z0-9_]+$/;
|
|
28
|
+
// Meta's template languages. The first ones are the ones most owners use.
|
|
29
|
+
exports.WHATSAPP_TEMPLATE_LANGUAGES = [
|
|
30
|
+
{ code: 'es', name: 'Spanish' },
|
|
31
|
+
{ code: 'es_MX', name: 'Spanish (Mexico)' },
|
|
32
|
+
{ code: 'es_AR', name: 'Spanish (Argentina)' },
|
|
33
|
+
{ code: 'es_ES', name: 'Spanish (Spain)' },
|
|
34
|
+
{ code: 'en', name: 'English' },
|
|
35
|
+
{ code: 'en_US', name: 'English (US)' },
|
|
36
|
+
{ code: 'en_GB', name: 'English (UK)' },
|
|
37
|
+
{ code: 'pt_BR', name: 'Portuguese (Brazil)' },
|
|
38
|
+
{ code: 'pt_PT', name: 'Portuguese (Portugal)' },
|
|
39
|
+
{ code: 'af', name: 'Afrikaans' },
|
|
40
|
+
{ code: 'sq', name: 'Albanian' },
|
|
41
|
+
{ code: 'ar', name: 'Arabic' },
|
|
42
|
+
{ code: 'az', name: 'Azerbaijani' },
|
|
43
|
+
{ code: 'bn', name: 'Bengali' },
|
|
44
|
+
{ code: 'bg', name: 'Bulgarian' },
|
|
45
|
+
{ code: 'ca', name: 'Catalan' },
|
|
46
|
+
{ code: 'zh_CN', name: 'Chinese (China)' },
|
|
47
|
+
{ code: 'zh_HK', name: 'Chinese (Hong Kong)' },
|
|
48
|
+
{ code: 'zh_TW', name: 'Chinese (Taiwan)' },
|
|
49
|
+
{ code: 'hr', name: 'Croatian' },
|
|
50
|
+
{ code: 'cs', name: 'Czech' },
|
|
51
|
+
{ code: 'da', name: 'Danish' },
|
|
52
|
+
{ code: 'nl', name: 'Dutch' },
|
|
53
|
+
{ code: 'et', name: 'Estonian' },
|
|
54
|
+
{ code: 'fil', name: 'Filipino' },
|
|
55
|
+
{ code: 'fi', name: 'Finnish' },
|
|
56
|
+
{ code: 'fr', name: 'French' },
|
|
57
|
+
{ code: 'ka', name: 'Georgian' },
|
|
58
|
+
{ code: 'de', name: 'German' },
|
|
59
|
+
{ code: 'el', name: 'Greek' },
|
|
60
|
+
{ code: 'gu', name: 'Gujarati' },
|
|
61
|
+
{ code: 'ha', name: 'Hausa' },
|
|
62
|
+
{ code: 'he', name: 'Hebrew' },
|
|
63
|
+
{ code: 'hi', name: 'Hindi' },
|
|
64
|
+
{ code: 'hu', name: 'Hungarian' },
|
|
65
|
+
{ code: 'id', name: 'Indonesian' },
|
|
66
|
+
{ code: 'ga', name: 'Irish' },
|
|
67
|
+
{ code: 'it', name: 'Italian' },
|
|
68
|
+
{ code: 'ja', name: 'Japanese' },
|
|
69
|
+
{ code: 'kn', name: 'Kannada' },
|
|
70
|
+
{ code: 'kk', name: 'Kazakh' },
|
|
71
|
+
{ code: 'rw_RW', name: 'Kinyarwanda' },
|
|
72
|
+
{ code: 'ko', name: 'Korean' },
|
|
73
|
+
{ code: 'ky_KG', name: 'Kyrgyz' },
|
|
74
|
+
{ code: 'lo', name: 'Lao' },
|
|
75
|
+
{ code: 'lv', name: 'Latvian' },
|
|
76
|
+
{ code: 'lt', name: 'Lithuanian' },
|
|
77
|
+
{ code: 'mk', name: 'Macedonian' },
|
|
78
|
+
{ code: 'ms', name: 'Malay' },
|
|
79
|
+
{ code: 'ml', name: 'Malayalam' },
|
|
80
|
+
{ code: 'mr', name: 'Marathi' },
|
|
81
|
+
{ code: 'nb', name: 'Norwegian' },
|
|
82
|
+
{ code: 'fa', name: 'Persian' },
|
|
83
|
+
{ code: 'pl', name: 'Polish' },
|
|
84
|
+
{ code: 'pa', name: 'Punjabi' },
|
|
85
|
+
{ code: 'ro', name: 'Romanian' },
|
|
86
|
+
{ code: 'ru', name: 'Russian' },
|
|
87
|
+
{ code: 'sr', name: 'Serbian' },
|
|
88
|
+
{ code: 'sk', name: 'Slovak' },
|
|
89
|
+
{ code: 'sl', name: 'Slovenian' },
|
|
90
|
+
{ code: 'sw', name: 'Swahili' },
|
|
91
|
+
{ code: 'sv', name: 'Swedish' },
|
|
92
|
+
{ code: 'ta', name: 'Tamil' },
|
|
93
|
+
{ code: 'te', name: 'Telugu' },
|
|
94
|
+
{ code: 'th', name: 'Thai' },
|
|
95
|
+
{ code: 'tr', name: 'Turkish' },
|
|
96
|
+
{ code: 'uk', name: 'Ukrainian' },
|
|
97
|
+
{ code: 'ur', name: 'Urdu' },
|
|
98
|
+
{ code: 'uz', name: 'Uzbek' },
|
|
99
|
+
{ code: 'vi', name: 'Vietnamese' },
|
|
100
|
+
{ code: 'zu', name: 'Zulu' }
|
|
101
|
+
];
|
|
102
|
+
const VARIABLE = /\{\{\s*(\d+)\s*\}\}/g;
|
|
103
|
+
/** Each {{n}} in the text, in the order they appear. */
|
|
104
|
+
const whatsappTemplateVariables = (text) => Array.from(text.matchAll(VARIABLE), match => Number(match[1]));
|
|
105
|
+
exports.whatsappTemplateVariables = whatsappTemplateVariables;
|
|
106
|
+
/** How many variables the body has: its highest {{n}}. */
|
|
107
|
+
const whatsappTemplateVariableCount = (body) => Math.max(0, ...(0, exports.whatsappTemplateVariables)(body));
|
|
108
|
+
exports.whatsappTemplateVariableCount = whatsappTemplateVariableCount;
|
|
109
|
+
const words = (text) => text
|
|
110
|
+
.replace(VARIABLE, ' ')
|
|
111
|
+
.split(/\s+/)
|
|
112
|
+
.filter(word => /[\p{L}\p{N}]/u.test(word)).length;
|
|
113
|
+
/**
|
|
114
|
+
* The rules Meta rejects templates for most often, checked before sending
|
|
115
|
+
* one for review. Meta's review has the last word; this only catches the
|
|
116
|
+
* common mistakes early.
|
|
117
|
+
*/
|
|
118
|
+
const checkWhatsappTemplate = (draft) => {
|
|
119
|
+
const issues = [];
|
|
120
|
+
const add = (code, n) => issues.push(n === undefined ? { code } : { code, n });
|
|
121
|
+
if (!NAME_PATTERN.test(draft.name))
|
|
122
|
+
add('name_format');
|
|
123
|
+
if (draft.name.length > exports.WHATSAPP_TEMPLATE_NAME_MAX)
|
|
124
|
+
add('name_long');
|
|
125
|
+
if (!exports.WHATSAPP_TEMPLATE_LANGUAGES.some(l => l.code === draft.language)) {
|
|
126
|
+
add('language');
|
|
127
|
+
}
|
|
128
|
+
if (!exports.WHATSAPP_TEMPLATE_CATEGORIES.includes(draft.category))
|
|
129
|
+
add('category');
|
|
130
|
+
const header = draft.header?.trim() ?? '';
|
|
131
|
+
if (header.length > exports.WHATSAPP_TEMPLATE_HEADER_MAX)
|
|
132
|
+
add('header_long');
|
|
133
|
+
if ((0, exports.whatsappTemplateVariables)(header).length > 0)
|
|
134
|
+
add('header_variables');
|
|
135
|
+
if (/[\n\r]/.test(header))
|
|
136
|
+
add('header_lines');
|
|
137
|
+
const footer = draft.footer?.trim() ?? '';
|
|
138
|
+
if (footer.length > exports.WHATSAPP_TEMPLATE_FOOTER_MAX)
|
|
139
|
+
add('footer_long');
|
|
140
|
+
if ((0, exports.whatsappTemplateVariables)(footer).length > 0)
|
|
141
|
+
add('footer_variables');
|
|
142
|
+
const buttons = draft.buttons ?? [];
|
|
143
|
+
if (buttons.length > exports.WHATSAPP_TEMPLATE_BUTTONS_MAX)
|
|
144
|
+
add('buttons_many');
|
|
145
|
+
if (buttons.some(button => !button.trim()))
|
|
146
|
+
add('button_empty');
|
|
147
|
+
if (buttons.some(button => button.trim().length > exports.WHATSAPP_TEMPLATE_BUTTON_MAX)) {
|
|
148
|
+
add('button_long');
|
|
149
|
+
}
|
|
150
|
+
if (buttons.some(button => (0, exports.whatsappTemplateVariables)(button).length > 0)) {
|
|
151
|
+
add('button_variables');
|
|
152
|
+
}
|
|
153
|
+
const unique = new Set(buttons.map(button => button.trim().toLowerCase()));
|
|
154
|
+
if (unique.size < buttons.length)
|
|
155
|
+
add('buttons_repeated');
|
|
156
|
+
const body = draft.body.trim();
|
|
157
|
+
if (!body) {
|
|
158
|
+
add('body_empty');
|
|
159
|
+
return issues;
|
|
160
|
+
}
|
|
161
|
+
if (body.length > exports.WHATSAPP_TEMPLATE_BODY_MAX)
|
|
162
|
+
add('body_long');
|
|
163
|
+
const found = (0, exports.whatsappTemplateVariables)(body);
|
|
164
|
+
if (found.length === 0)
|
|
165
|
+
return issues;
|
|
166
|
+
const count = Math.max(...found);
|
|
167
|
+
if (count > exports.WHATSAPP_TEMPLATE_VARIABLES_MAX)
|
|
168
|
+
add('variables_many');
|
|
169
|
+
// {{1}}, {{2}}, … with no gaps, each first used after the one before.
|
|
170
|
+
const firsts = [...new Set(found)];
|
|
171
|
+
if (firsts.some((n, i) => n !== i + 1))
|
|
172
|
+
add('variables_order');
|
|
173
|
+
if (/^\{\{\s*\d+\s*\}\}/.test(body))
|
|
174
|
+
add('variable_at_start');
|
|
175
|
+
if (/\{\{\s*\d+\s*\}\}[\s.!?…]*$/.test(body))
|
|
176
|
+
add('variable_at_end');
|
|
177
|
+
if (/\}\}\s*\{\{/.test(body))
|
|
178
|
+
add('variables_adjacent');
|
|
179
|
+
// Meta wants more words than variables; this is about the ratio it accepts.
|
|
180
|
+
if (words(body) < 2 * firsts.length + 1)
|
|
181
|
+
add('variables_dense');
|
|
182
|
+
const examples = draft.examples ?? [];
|
|
183
|
+
for (let n = 1; n <= count; n++) {
|
|
184
|
+
if (!examples[n - 1]?.trim())
|
|
185
|
+
add('example_missing', n);
|
|
186
|
+
}
|
|
187
|
+
return issues;
|
|
188
|
+
};
|
|
189
|
+
exports.checkWhatsappTemplate = checkWhatsappTemplate;
|
|
190
|
+
const ISSUE_TEXT = {
|
|
191
|
+
name_format: 'The name takes lowercase letters, digits and underscores only.',
|
|
192
|
+
name_long: `The name is at most ${exports.WHATSAPP_TEMPLATE_NAME_MAX} characters.`,
|
|
193
|
+
language: "The language isn't one Meta supports.",
|
|
194
|
+
category: 'The category is Utility or Marketing.',
|
|
195
|
+
body_empty: 'The message needs a body.',
|
|
196
|
+
body_long: `The body is at most ${exports.WHATSAPP_TEMPLATE_BODY_MAX} characters.`,
|
|
197
|
+
header_long: `The header is at most ${exports.WHATSAPP_TEMPLATE_HEADER_MAX} characters.`,
|
|
198
|
+
header_variables: "The header can't have variables.",
|
|
199
|
+
header_lines: 'The header is one line.',
|
|
200
|
+
footer_long: `The footer is at most ${exports.WHATSAPP_TEMPLATE_FOOTER_MAX} characters.`,
|
|
201
|
+
footer_variables: "The footer can't have variables.",
|
|
202
|
+
button_empty: 'A quick reply needs a text.',
|
|
203
|
+
button_long: `A quick reply is at most ${exports.WHATSAPP_TEMPLATE_BUTTON_MAX} characters.`,
|
|
204
|
+
button_variables: "A quick reply can't have variables.",
|
|
205
|
+
buttons_many: `At most ${exports.WHATSAPP_TEMPLATE_BUTTONS_MAX} quick replies.`,
|
|
206
|
+
buttons_repeated: 'Two quick replies say the same.',
|
|
207
|
+
variables_many: `At most ${exports.WHATSAPP_TEMPLATE_VARIABLES_MAX} variables.`,
|
|
208
|
+
variables_order: 'Variables are numbered {{1}}, {{2}}, … in the order they first appear, with no gaps.',
|
|
209
|
+
variable_at_start: 'Meta rejects a body that starts with a variable.',
|
|
210
|
+
variable_at_end: 'Meta rejects a body that ends with a variable.',
|
|
211
|
+
variables_adjacent: 'Meta rejects two variables side by side; put words between them.',
|
|
212
|
+
variables_dense: 'Too many variables for the length of the text; add words or remove variables.',
|
|
213
|
+
example_missing: 'Each variable needs an example value for Meta to review.'
|
|
214
|
+
};
|
|
215
|
+
const whatsappTemplateIssueText = (issue) => issue.n === undefined
|
|
216
|
+
? ISSUE_TEXT[issue.code]
|
|
217
|
+
: `{{${issue.n}}}: ${ISSUE_TEXT[issue.code]}`;
|
|
218
|
+
exports.whatsappTemplateIssueText = whatsappTemplateIssueText;
|
|
219
|
+
/** The components Meta's create and edit calls take. */
|
|
220
|
+
const whatsappTemplateComponents = (draft) => {
|
|
221
|
+
const components = [];
|
|
222
|
+
const header = draft.header?.trim();
|
|
223
|
+
if (header)
|
|
224
|
+
components.push({ type: 'HEADER', format: 'TEXT', text: header });
|
|
225
|
+
const body = draft.body.trim();
|
|
226
|
+
const count = (0, exports.whatsappTemplateVariableCount)(body);
|
|
227
|
+
components.push({
|
|
228
|
+
type: 'BODY',
|
|
229
|
+
text: body,
|
|
230
|
+
...(count > 0
|
|
231
|
+
? {
|
|
232
|
+
example: {
|
|
233
|
+
body_text: [
|
|
234
|
+
Array.from({ length: count }, (_, i) => (draft.examples?.[i] ?? '').trim())
|
|
235
|
+
]
|
|
236
|
+
}
|
|
237
|
+
}
|
|
238
|
+
: {})
|
|
239
|
+
});
|
|
240
|
+
const footer = draft.footer?.trim();
|
|
241
|
+
if (footer)
|
|
242
|
+
components.push({ type: 'FOOTER', text: footer });
|
|
243
|
+
const buttons = (draft.buttons ?? []).map(b => b.trim()).filter(Boolean);
|
|
244
|
+
if (buttons.length > 0) {
|
|
245
|
+
components.push({
|
|
246
|
+
type: 'BUTTONS',
|
|
247
|
+
buttons: buttons.map(text => ({ type: 'QUICK_REPLY', text }))
|
|
248
|
+
});
|
|
249
|
+
}
|
|
250
|
+
return components;
|
|
251
|
+
};
|
|
252
|
+
exports.whatsappTemplateComponents = whatsappTemplateComponents;
|
|
253
|
+
/** A template's text, from the components Meta returns. */
|
|
254
|
+
const whatsappTemplateContent = (components) => {
|
|
255
|
+
const content = {
|
|
256
|
+
header: null,
|
|
257
|
+
body: '',
|
|
258
|
+
footer: null,
|
|
259
|
+
buttons: [],
|
|
260
|
+
examples: []
|
|
261
|
+
};
|
|
262
|
+
if (!Array.isArray(components))
|
|
263
|
+
return content;
|
|
264
|
+
for (const component of components) {
|
|
265
|
+
const type = String(component?.type ?? '').toUpperCase();
|
|
266
|
+
const text = typeof component?.text === 'string' ? component.text : null;
|
|
267
|
+
if (type === 'HEADER' && component.format === 'TEXT')
|
|
268
|
+
content.header = text;
|
|
269
|
+
if (type === 'BODY') {
|
|
270
|
+
content.body = text ?? '';
|
|
271
|
+
const example = component.example;
|
|
272
|
+
const first = example?.body_text?.[0];
|
|
273
|
+
if (Array.isArray(first))
|
|
274
|
+
content.examples = first.map(v => String(v));
|
|
275
|
+
}
|
|
276
|
+
if (type === 'FOOTER')
|
|
277
|
+
content.footer = text;
|
|
278
|
+
if (type === 'BUTTONS' && Array.isArray(component.buttons)) {
|
|
279
|
+
content.buttons = component.buttons
|
|
280
|
+
.filter(b => b?.type === 'QUICK_REPLY' && typeof b.text === 'string')
|
|
281
|
+
.map(b => b.text);
|
|
282
|
+
}
|
|
283
|
+
}
|
|
284
|
+
return content;
|
|
285
|
+
};
|
|
286
|
+
exports.whatsappTemplateContent = whatsappTemplateContent;
|
|
287
|
+
/**
|
|
288
|
+
* Whether Ganju can send it: it fills the body's variables only. A media
|
|
289
|
+
* header, a header variable, or a button that takes a value (a URL with a
|
|
290
|
+
* variable, a code, a flow) needs more, and Meta refuses the send.
|
|
291
|
+
*/
|
|
292
|
+
const whatsappTemplateSendable = (components) => {
|
|
293
|
+
if (!Array.isArray(components))
|
|
294
|
+
return false;
|
|
295
|
+
return components.every(component => {
|
|
296
|
+
const type = String(component?.type ?? '').toUpperCase();
|
|
297
|
+
if (type === 'BODY' || type === 'FOOTER')
|
|
298
|
+
return true;
|
|
299
|
+
if (type === 'HEADER') {
|
|
300
|
+
return (component.format === 'TEXT' &&
|
|
301
|
+
(0, exports.whatsappTemplateVariables)(String(component.text ?? '')).length === 0);
|
|
302
|
+
}
|
|
303
|
+
if (type === 'BUTTONS') {
|
|
304
|
+
const buttons = Array.isArray(component.buttons)
|
|
305
|
+
? component.buttons
|
|
306
|
+
: [];
|
|
307
|
+
return buttons.every(button => {
|
|
308
|
+
const kind = String(button?.type ?? '').toUpperCase();
|
|
309
|
+
if (kind === 'QUICK_REPLY' || kind === 'PHONE_NUMBER')
|
|
310
|
+
return true;
|
|
311
|
+
if (kind === 'URL') {
|
|
312
|
+
return ((0, exports.whatsappTemplateVariables)(String(button.url ?? '')).length === 0);
|
|
313
|
+
}
|
|
314
|
+
return false;
|
|
315
|
+
});
|
|
316
|
+
}
|
|
317
|
+
return false;
|
|
318
|
+
});
|
|
319
|
+
};
|
|
320
|
+
exports.whatsappTemplateSendable = whatsappTemplateSendable;
|
|
321
|
+
/** The body with each {{n}} replaced, for previews and the message record. */
|
|
322
|
+
const fillWhatsappTemplate = (body, values) => body.replace(VARIABLE, (whole, n) => values[Number(n) - 1] ?? whole);
|
|
323
|
+
exports.fillWhatsappTemplate = fillWhatsappTemplate;
|
|
324
|
+
/**
|
|
325
|
+
* A value for a {{n}} as Meta accepts it: no line breaks or tabs, no long
|
|
326
|
+
* runs of spaces, and never empty.
|
|
327
|
+
*/
|
|
328
|
+
const cleanWhatsappTemplateParam = (value) => {
|
|
329
|
+
const text = String(value ?? '')
|
|
330
|
+
.replace(/[\r\n\t]+/g, ' ')
|
|
331
|
+
.replace(/ {2,}/g, ' ')
|
|
332
|
+
.trim()
|
|
333
|
+
.slice(0, 1024);
|
|
334
|
+
return text || '-';
|
|
335
|
+
};
|
|
336
|
+
exports.cleanWhatsappTemplateParam = cleanWhatsappTemplateParam;
|
|
337
|
+
// Meta's error codes for a message that didn't go out, in words an owner can
|
|
338
|
+
// act on. `retry`: the same message may go through later.
|
|
339
|
+
const SEND_ERRORS = {
|
|
340
|
+
1: { text: 'WhatsApp had a temporary problem.', retry: true },
|
|
341
|
+
2: { text: 'WhatsApp had a temporary problem.', retry: true },
|
|
342
|
+
10: { text: "The channel's access token isn't allowed to send messages." },
|
|
343
|
+
190: {
|
|
344
|
+
text: "The channel's access token expired or was revoked; connect a new one in the channel's settings."
|
|
345
|
+
},
|
|
346
|
+
200: { text: "The channel's access token isn't allowed to send messages." },
|
|
347
|
+
368: {
|
|
348
|
+
text: 'Meta has temporarily blocked this WhatsApp Business account for policy violations.'
|
|
349
|
+
},
|
|
350
|
+
130429: {
|
|
351
|
+
text: "The number's sending rate was reached; it will be tried again.",
|
|
352
|
+
retry: true
|
|
353
|
+
},
|
|
354
|
+
131000: { text: 'WhatsApp had a temporary problem.', retry: true },
|
|
355
|
+
131016: { text: 'WhatsApp is temporarily unavailable.', retry: true },
|
|
356
|
+
131026: {
|
|
357
|
+
text: "WhatsApp couldn't deliver it: the customer may have blocked the business, left WhatsApp, or not accepted its latest terms."
|
|
358
|
+
},
|
|
359
|
+
131031: { text: 'Meta locked this WhatsApp Business account.' },
|
|
360
|
+
131042: {
|
|
361
|
+
text: "Meta refused it over a payment problem on the WhatsApp Business account; check its payment method in Meta's Business settings."
|
|
362
|
+
},
|
|
363
|
+
131047: {
|
|
364
|
+
text: "Outside WhatsApp's 24-hour window: the customer last wrote more than 24 hours ago, so only an approved template reaches them."
|
|
365
|
+
},
|
|
366
|
+
131048: {
|
|
367
|
+
text: "Meta limited this number's sending because of its quality: too many customers blocked or reported its messages."
|
|
368
|
+
},
|
|
369
|
+
131049: {
|
|
370
|
+
text: 'Meta chose not to deliver this marketing message, to limit how many each customer gets.'
|
|
371
|
+
},
|
|
372
|
+
131050: {
|
|
373
|
+
text: 'The customer turned off marketing messages from this business.'
|
|
374
|
+
},
|
|
375
|
+
131056: {
|
|
376
|
+
text: 'Too many messages to this customer in a short time; it will be tried again.',
|
|
377
|
+
retry: true
|
|
378
|
+
},
|
|
379
|
+
132000: {
|
|
380
|
+
text: "The number of values doesn't match the template's variables."
|
|
381
|
+
},
|
|
382
|
+
132001: {
|
|
383
|
+
text: 'Meta has no approved template with that name in that language.'
|
|
384
|
+
},
|
|
385
|
+
132005: { text: "The template's text is too long with the values put in." },
|
|
386
|
+
132007: { text: "The template's text breaks WhatsApp's commerce policy." },
|
|
387
|
+
132012: { text: "A value doesn't match what the template expects." },
|
|
388
|
+
132015: { text: 'Meta paused this template because of its quality.' },
|
|
389
|
+
132016: { text: 'Meta disabled this template because of its quality.' },
|
|
390
|
+
132018: { text: 'A value has line breaks, tabs or too many spaces.' }
|
|
391
|
+
};
|
|
392
|
+
/** A Meta send error as an owner reads it, and whether a retry may help. */
|
|
393
|
+
const whatsappSendError = (code, detail) => {
|
|
394
|
+
const known = code !== undefined ? SEND_ERRORS[code] : undefined;
|
|
395
|
+
if (known)
|
|
396
|
+
return { text: known.text, retry: !!known.retry };
|
|
397
|
+
return {
|
|
398
|
+
text: detail ? detail.slice(0, 300) : 'WhatsApp refused the message.',
|
|
399
|
+
retry: true
|
|
400
|
+
};
|
|
401
|
+
};
|
|
402
|
+
exports.whatsappSendError = whatsappSendError;
|
|
@@ -0,0 +1,28 @@
|
|
|
1
|
+
export interface WhatsappWebhookChange<V> {
|
|
2
|
+
field?: string;
|
|
3
|
+
value?: V;
|
|
4
|
+
}
|
|
5
|
+
export interface WhatsappWebhookEnvelope<V> {
|
|
6
|
+
entry?: Array<{
|
|
7
|
+
id?: string;
|
|
8
|
+
changes?: Array<WhatsappWebhookChange<V>>;
|
|
9
|
+
}>;
|
|
10
|
+
}
|
|
11
|
+
export interface WhatsappAddressedValue {
|
|
12
|
+
metadata?: {
|
|
13
|
+
phone_number_id?: string;
|
|
14
|
+
};
|
|
15
|
+
messages?: unknown[];
|
|
16
|
+
}
|
|
17
|
+
/**
|
|
18
|
+
* The change carrying a message for this channel's number, wherever Meta put
|
|
19
|
+
* it in the batch. A change that doesn't say which number it's for is kept.
|
|
20
|
+
*/
|
|
21
|
+
export declare const whatsappMessageChange: <V extends WhatsappAddressedValue>(payload: WhatsappWebhookEnvelope<V>, phoneNumberId: string) => V | undefined;
|
|
22
|
+
/**
|
|
23
|
+
* The changes with one of `fields` from this channel's business account. An
|
|
24
|
+
* entry without an id, or a channel that hasn't saved its account, keeps
|
|
25
|
+
* them: template events only touch the channel's own stored templates.
|
|
26
|
+
*/
|
|
27
|
+
export declare const whatsappAccountChanges: <V>(payload: WhatsappWebhookEnvelope<V>, businessAccountId: string | undefined, fields: readonly string[]) => WhatsappWebhookChange<V>[];
|
|
28
|
+
//# sourceMappingURL=whatsappWebhook.d.ts.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"whatsappWebhook.d.ts","sourceRoot":"","sources":["../src/whatsappWebhook.ts"],"names":[],"mappings":"AAKA,MAAM,WAAW,qBAAqB,CAAC,CAAC;IACtC,KAAK,CAAC,EAAE,MAAM,CAAC;IACf,KAAK,CAAC,EAAE,CAAC,CAAC;CACX;AAED,MAAM,WAAW,uBAAuB,CAAC,CAAC;IACxC,KAAK,CAAC,EAAE,KAAK,CAAC;QAAE,EAAE,CAAC,EAAE,MAAM,CAAC;QAAC,OAAO,CAAC,EAAE,KAAK,CAAC,qBAAqB,CAAC,CAAC,CAAC,CAAC,CAAA;KAAE,CAAC,CAAC;CAC3E;AAED,MAAM,WAAW,sBAAsB;IACrC,QAAQ,CAAC,EAAE;QAAE,eAAe,CAAC,EAAE,MAAM,CAAA;KAAE,CAAC;IACxC,QAAQ,CAAC,EAAE,OAAO,EAAE,CAAC;CACtB;AAED;;;GAGG;AACH,eAAO,MAAM,qBAAqB,GAAI,CAAC,SAAS,sBAAsB,EACpE,SAAS,uBAAuB,CAAC,CAAC,CAAC,EACnC,eAAe,MAAM,KACpB,CAAC,GAAG,SAQM,CAAC;AAEd;;;;GAIG;AACH,eAAO,MAAM,sBAAsB,GAAI,CAAC,EACtC,SAAS,uBAAuB,CAAC,CAAC,CAAC,EACnC,mBAAmB,MAAM,GAAG,SAAS,EACrC,QAAQ,SAAS,MAAM,EAAE,KACxB,qBAAqB,CAAC,CAAC,CAAC,EAStB,CAAC"}
|
|
@@ -0,0 +1,30 @@
|
|
|
1
|
+
"use strict";
|
|
2
|
+
// What one WhatsApp channel takes from a webhook. A Meta app sends every
|
|
3
|
+
// number's events to each of its callbacks, signed with the same app secret,
|
|
4
|
+
// so a channel keeps only what was addressed to its own number and business
|
|
5
|
+
// account; otherwise another channel's bot answers its messages.
|
|
6
|
+
Object.defineProperty(exports, "__esModule", { value: true });
|
|
7
|
+
exports.whatsappAccountChanges = exports.whatsappMessageChange = void 0;
|
|
8
|
+
/**
|
|
9
|
+
* The change carrying a message for this channel's number, wherever Meta put
|
|
10
|
+
* it in the batch. A change that doesn't say which number it's for is kept.
|
|
11
|
+
*/
|
|
12
|
+
const whatsappMessageChange = (payload, phoneNumberId) => (payload.entry ?? [])
|
|
13
|
+
.flatMap(entry => entry.changes ?? [])
|
|
14
|
+
.find(change => {
|
|
15
|
+
const value = change.value;
|
|
16
|
+
if (!value?.messages?.length)
|
|
17
|
+
return false;
|
|
18
|
+
const to = value.metadata?.phone_number_id;
|
|
19
|
+
return !to || to === phoneNumberId;
|
|
20
|
+
})?.value;
|
|
21
|
+
exports.whatsappMessageChange = whatsappMessageChange;
|
|
22
|
+
/**
|
|
23
|
+
* The changes with one of `fields` from this channel's business account. An
|
|
24
|
+
* entry without an id, or a channel that hasn't saved its account, keeps
|
|
25
|
+
* them: template events only touch the channel's own stored templates.
|
|
26
|
+
*/
|
|
27
|
+
const whatsappAccountChanges = (payload, businessAccountId, fields) => (payload.entry ?? [])
|
|
28
|
+
.filter(entry => !entry.id || !businessAccountId || entry.id === businessAccountId)
|
|
29
|
+
.flatMap(entry => (entry.changes ?? []).filter(change => !!change.field && fields.includes(change.field)));
|
|
30
|
+
exports.whatsappAccountChanges = whatsappAccountChanges;
|
package/package.json
CHANGED
package/src/constants.ts
CHANGED
|
@@ -25,6 +25,7 @@ import {
|
|
|
25
25
|
CUSTOM_CODE_BROKER_PATH_DATA,
|
|
26
26
|
CUSTOM_CODE_BROKER_PATH_CHANNELS_SEND,
|
|
27
27
|
CUSTOM_CODE_BROKER_PATH_CHANNELS_POST,
|
|
28
|
+
CUSTOM_CODE_BROKER_PATH_CHANNELS_TEMPLATES,
|
|
28
29
|
CUSTOM_CODE_HEALTH_TOOL,
|
|
29
30
|
CUSTOM_CODE_MAX_LOGS,
|
|
30
31
|
CUSTOM_CODE_MAX_LOG_LENGTH
|
|
@@ -2196,6 +2197,36 @@ const DATA_PURGE_BATCH = 20;
|
|
|
2196
2197
|
// Postgres write per query.
|
|
2197
2198
|
const DATA_ACTIVITY_THROTTLE_MS = 60 * 60 * 1000;
|
|
2198
2199
|
|
|
2200
|
+
// Index advice. A column is worth an index when, over a week, at least 50
|
|
2201
|
+
// queries filtered or sorted on it and they read on average 1,000 rows and
|
|
2202
|
+
// 20 rows for every row returned. An index no query used in 30 days on a
|
|
2203
|
+
// table written at least 1,000 rows (and more rows written than queries) is
|
|
2204
|
+
// flagged as unused. Never dropped automatically.
|
|
2205
|
+
const DATA_INDEX_ADVICE_WINDOW_DAYS = 7;
|
|
2206
|
+
const DATA_INDEX_ADVICE_MIN_QUERIES = 50;
|
|
2207
|
+
const DATA_INDEX_ADVICE_MIN_ROWS_READ = 1_000;
|
|
2208
|
+
const DATA_INDEX_ADVICE_MIN_READ_RATIO = 20;
|
|
2209
|
+
const DATA_INDEX_UNUSED_WINDOW_DAYS = 30;
|
|
2210
|
+
const DATA_INDEX_UNUSED_MIN_ROWS_WRITTEN = 1_000;
|
|
2211
|
+
// Daily stats are kept past the longest window, then removed.
|
|
2212
|
+
const DATA_QUERY_STAT_KEEP_DAYS = 35;
|
|
2213
|
+
const DATA_INDEX_ADVICE_ADD = 'add' as 'add';
|
|
2214
|
+
const DATA_INDEX_ADVICE_UNUSED = 'unused' as 'unused';
|
|
2215
|
+
const DATA_INDEX_ADVICE_OPEN = 'open' as 'open';
|
|
2216
|
+
const DATA_INDEX_ADVICE_DISMISSED = 'dismissed' as 'dismissed';
|
|
2217
|
+
|
|
2218
|
+
// The daily jobs (index advice and the abandoned-database flow) run on the
|
|
2219
|
+
// hourly cron at this UTC hour.
|
|
2220
|
+
const DATA_DAILY_JOB_HOUR_UTC = 13;
|
|
2221
|
+
|
|
2222
|
+
// Days on Free before a database is warned about, reminded about, and listed
|
|
2223
|
+
// for Ganju to delete by hand.
|
|
2224
|
+
const DATA_ABANDON_WARN_DAYS = 30;
|
|
2225
|
+
const DATA_ABANDON_REMIND_DAYS = 41;
|
|
2226
|
+
const DATA_ABANDON_DELETE_DAYS = 44;
|
|
2227
|
+
// How long the export taken before a database is deleted is kept.
|
|
2228
|
+
const DATA_ABANDON_EXPORT_KEEP_DAYS = 90;
|
|
2229
|
+
|
|
2199
2230
|
interface PlanLimits {
|
|
2200
2231
|
// null on any field means "no hard limit" for that plan.
|
|
2201
2232
|
maxProjects: number | null;
|
|
@@ -2271,6 +2302,9 @@ interface PlanLimits {
|
|
|
2271
2302
|
maxDataTablesPerDatabase: number | null;
|
|
2272
2303
|
// Abuse backstop on data trigger runs a month. `null` = no backstop.
|
|
2273
2304
|
triggerRunHardCap: number | null;
|
|
2305
|
+
// Email trigger runs a day per organization, each to at most 10 addresses.
|
|
2306
|
+
// They go out from Ganju's own domain. `null` = no cap.
|
|
2307
|
+
dailyTriggerEmailCap: number | null;
|
|
2274
2308
|
}
|
|
2275
2309
|
|
|
2276
2310
|
const PLAN_LIMITS: Record<
|
|
@@ -2315,7 +2349,8 @@ const PLAN_LIMITS: Record<
|
|
|
2315
2349
|
canUseData: false,
|
|
2316
2350
|
maxDataBytes: null,
|
|
2317
2351
|
maxDataTablesPerDatabase: null,
|
|
2318
|
-
triggerRunHardCap: 0
|
|
2352
|
+
triggerRunHardCap: 0,
|
|
2353
|
+
dailyTriggerEmailCap: 0
|
|
2319
2354
|
},
|
|
2320
2355
|
PRO: {
|
|
2321
2356
|
maxProjects: null,
|
|
@@ -2346,7 +2381,8 @@ const PLAN_LIMITS: Record<
|
|
|
2346
2381
|
canUseData: true,
|
|
2347
2382
|
maxDataBytes: DATA_MAX_DATABASE_BYTES,
|
|
2348
2383
|
maxDataTablesPerDatabase: DATA_MAX_TABLES_PER_DATABASE,
|
|
2349
|
-
triggerRunHardCap: DATA_TRIGGER_RUN_HARD_CAP
|
|
2384
|
+
triggerRunHardCap: DATA_TRIGGER_RUN_HARD_CAP,
|
|
2385
|
+
dailyTriggerEmailCap: 500
|
|
2350
2386
|
},
|
|
2351
2387
|
ENTERPRISE: {
|
|
2352
2388
|
maxProjects: null,
|
|
@@ -2377,7 +2413,8 @@ const PLAN_LIMITS: Record<
|
|
|
2377
2413
|
canUseData: true,
|
|
2378
2414
|
maxDataBytes: DATA_MAX_DATABASE_BYTES,
|
|
2379
2415
|
maxDataTablesPerDatabase: DATA_MAX_TABLES_PER_DATABASE,
|
|
2380
|
-
triggerRunHardCap: null
|
|
2416
|
+
triggerRunHardCap: null,
|
|
2417
|
+
dailyTriggerEmailCap: 5000
|
|
2381
2418
|
}
|
|
2382
2419
|
};
|
|
2383
2420
|
|
|
@@ -2581,6 +2618,22 @@ export const constants = {
|
|
|
2581
2618
|
DATA_DELETE_GRACE_DAYS,
|
|
2582
2619
|
DATA_PURGE_BATCH,
|
|
2583
2620
|
DATA_ACTIVITY_THROTTLE_MS,
|
|
2621
|
+
DATA_INDEX_ADVICE_WINDOW_DAYS,
|
|
2622
|
+
DATA_INDEX_ADVICE_MIN_QUERIES,
|
|
2623
|
+
DATA_INDEX_ADVICE_MIN_ROWS_READ,
|
|
2624
|
+
DATA_INDEX_ADVICE_MIN_READ_RATIO,
|
|
2625
|
+
DATA_INDEX_UNUSED_WINDOW_DAYS,
|
|
2626
|
+
DATA_INDEX_UNUSED_MIN_ROWS_WRITTEN,
|
|
2627
|
+
DATA_QUERY_STAT_KEEP_DAYS,
|
|
2628
|
+
DATA_INDEX_ADVICE_ADD,
|
|
2629
|
+
DATA_INDEX_ADVICE_UNUSED,
|
|
2630
|
+
DATA_INDEX_ADVICE_OPEN,
|
|
2631
|
+
DATA_INDEX_ADVICE_DISMISSED,
|
|
2632
|
+
DATA_DAILY_JOB_HOUR_UTC,
|
|
2633
|
+
DATA_ABANDON_WARN_DAYS,
|
|
2634
|
+
DATA_ABANDON_REMIND_DAYS,
|
|
2635
|
+
DATA_ABANDON_DELETE_DAYS,
|
|
2636
|
+
DATA_ABANDON_EXPORT_KEEP_DAYS,
|
|
2584
2637
|
ARTIFACT_DATABASE_STATUS_CREATING,
|
|
2585
2638
|
ARTIFACT_DATABASE_STATUS_READY,
|
|
2586
2639
|
ARTIFACT_DATABASE_STATUS_FAILED,
|
|
@@ -3113,6 +3166,7 @@ export const constants = {
|
|
|
3113
3166
|
CUSTOM_CODE_BROKER_PATH_DATA,
|
|
3114
3167
|
CUSTOM_CODE_BROKER_PATH_CHANNELS_SEND,
|
|
3115
3168
|
CUSTOM_CODE_BROKER_PATH_CHANNELS_POST,
|
|
3169
|
+
CUSTOM_CODE_BROKER_PATH_CHANNELS_TEMPLATES,
|
|
3116
3170
|
CUSTOM_CODE_OUTBOUND_PARAM_ARTIFACT_ID,
|
|
3117
3171
|
CUSTOM_CODE_OUTBOUND_PARAM_ALLOWED_HOSTS,
|
|
3118
3172
|
CUSTOM_CODE_PLATFORM_HOSTS,
|
package/src/crypto.ts
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
|
-
import { Context } from 'hono';
|
|
2
1
|
import { xchacha20poly1305 } from '@noble/ciphers/chacha.js';
|
|
3
2
|
import { getEnv } from './getEnv';
|
|
3
|
+
import type { EnvSource } from './getEnv';
|
|
4
4
|
import { base64ToBytes, bytesToBase64 } from './base64';
|
|
5
5
|
|
|
6
6
|
const PREFIX = 'enc:v1:';
|
|
@@ -8,7 +8,8 @@ const NONCE_BYTES = 24;
|
|
|
8
8
|
const KEY_BYTES = 32;
|
|
9
9
|
const KEY_ENV = 'CRYPTO_SECRET';
|
|
10
10
|
|
|
11
|
-
|
|
11
|
+
// A request's context, or anything with the worker's env (a cron, a queue).
|
|
12
|
+
export const getCredentialEncryptionKey = (c: EnvSource) => {
|
|
12
13
|
const rawKey = getEnv(c, KEY_ENV);
|
|
13
14
|
if (!rawKey) {
|
|
14
15
|
throw new Error(`Missing env: ${KEY_ENV}`);
|
package/src/data.ts
CHANGED
|
@@ -26,6 +26,8 @@ export {
|
|
|
26
26
|
encodeCursor,
|
|
27
27
|
decodeCursor,
|
|
28
28
|
isSensitiveWrite,
|
|
29
|
+
fileUriOf,
|
|
30
|
+
unknownFileUris,
|
|
29
31
|
tidyArgs,
|
|
30
32
|
zoned
|
|
31
33
|
} from './dataCompiler';
|
|
@@ -47,6 +49,8 @@ export {
|
|
|
47
49
|
rowHistory,
|
|
48
50
|
pruneHistory,
|
|
49
51
|
deleteImpact,
|
|
52
|
+
fileLinks,
|
|
53
|
+
replaceFileLinks,
|
|
50
54
|
createTable,
|
|
51
55
|
updateTable,
|
|
52
56
|
dropTable,
|
|
@@ -64,6 +68,12 @@ export {
|
|
|
64
68
|
exportRows
|
|
65
69
|
} from './dataEngine';
|
|
66
70
|
export type { SqlRow, SqlRunner } from './dataEngine';
|
|
71
|
+
export { adviseIndexes, adviceDoubled } from './dataIndexAdvice';
|
|
72
|
+
export type {
|
|
73
|
+
IndexAdviceCandidate,
|
|
74
|
+
IndexAdviceInput,
|
|
75
|
+
IndexAdviceStat
|
|
76
|
+
} from './dataIndexAdvice';
|
|
67
77
|
export {
|
|
68
78
|
DATA_QUERY_PARAM_TYPES,
|
|
69
79
|
DATA_MAX_QUERIES,
|
|
@@ -118,7 +128,10 @@ export {
|
|
|
118
128
|
utf8Bytes,
|
|
119
129
|
conditionMatches,
|
|
120
130
|
renderTriggerText,
|
|
131
|
+
renderCustomerMessage,
|
|
132
|
+
readEventRow,
|
|
121
133
|
normaliseTrigger,
|
|
134
|
+
normaliseCustomerAction,
|
|
122
135
|
listTriggers,
|
|
123
136
|
getTrigger,
|
|
124
137
|
saveTrigger,
|
|
@@ -138,6 +151,7 @@ export type {
|
|
|
138
151
|
DataTriggerAction,
|
|
139
152
|
DataTriggerActionType,
|
|
140
153
|
DataTriggerCondition,
|
|
154
|
+
DataTriggerCustomerMessage,
|
|
141
155
|
DataTriggerEvent,
|
|
142
156
|
DataTriggerEventMessage,
|
|
143
157
|
DataTriggerInput,
|