@k-msg/template 0.1.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.
package/dist/index.js ADDED
@@ -0,0 +1,1279 @@
1
+ // src/parser/variable.parser.ts
2
+ var VariableParser = class {
3
+ static VARIABLE_PATTERN = /#{([^}]+)}/g;
4
+ /**
5
+ * 템플릿 내용에서 변수를 추출합니다
6
+ */
7
+ static extractVariables(content) {
8
+ const variables = [];
9
+ const matches = content.matchAll(this.VARIABLE_PATTERN);
10
+ for (const match of matches) {
11
+ const variableName = match[1].trim();
12
+ if (variableName && !variables.includes(variableName)) {
13
+ variables.push(variableName);
14
+ }
15
+ }
16
+ return variables;
17
+ }
18
+ /**
19
+ * 템플릿 내용의 변수를 실제 값으로 치환합니다
20
+ */
21
+ static replaceVariables(content, variables) {
22
+ return content.replace(this.VARIABLE_PATTERN, (match, variableName) => {
23
+ const value = variables[variableName.trim()];
24
+ if (value === void 0 || value === null) {
25
+ return match;
26
+ }
27
+ if (value instanceof Date) {
28
+ return value.toISOString();
29
+ }
30
+ return String(value);
31
+ });
32
+ }
33
+ /**
34
+ * 변수 정의와 실제 제공된 값을 검증합니다
35
+ */
36
+ static validateVariables(variableDefinitions, providedVariables) {
37
+ const errors = [];
38
+ for (const definition of variableDefinitions) {
39
+ const value = providedVariables[definition.name];
40
+ if (definition.required && (value === void 0 || value === null)) {
41
+ errors.push(`Required variable '${definition.name}' is missing`);
42
+ continue;
43
+ }
44
+ if (value === void 0 || value === null) {
45
+ continue;
46
+ }
47
+ if (!this.validateVariableType(value, definition.type)) {
48
+ errors.push(`Variable '${definition.name}' has invalid type. Expected: ${definition.type}`);
49
+ }
50
+ if (definition.maxLength && String(value).length > definition.maxLength) {
51
+ errors.push(`Variable '${definition.name}' exceeds maximum length of ${definition.maxLength}`);
52
+ }
53
+ if (definition.format && !new RegExp(definition.format).test(String(value))) {
54
+ errors.push(`Variable '${definition.name}' does not match required format: ${definition.format}`);
55
+ }
56
+ }
57
+ return {
58
+ isValid: errors.length === 0,
59
+ errors
60
+ };
61
+ }
62
+ static validateVariableType(value, expectedType) {
63
+ switch (expectedType) {
64
+ case "string":
65
+ return typeof value === "string";
66
+ case "number":
67
+ return typeof value === "number" && !isNaN(value);
68
+ case "date":
69
+ return value instanceof Date || !isNaN(Date.parse(value));
70
+ case "custom":
71
+ return true;
72
+ // 커스텀 타입은 별도 검증 로직 필요
73
+ default:
74
+ return false;
75
+ }
76
+ }
77
+ /**
78
+ * 템플릿에서 사용된 변수와 정의된 변수의 일치성을 검사합니다
79
+ */
80
+ static validateTemplateVariables(content, variableDefinitions) {
81
+ const usedVariables = this.extractVariables(content);
82
+ const definedVariables = variableDefinitions.map((v) => v.name);
83
+ const errors = [];
84
+ for (const usedVar of usedVariables) {
85
+ if (!definedVariables.includes(usedVar)) {
86
+ errors.push(`Variable '${usedVar}' is used in template but not defined`);
87
+ }
88
+ }
89
+ for (const definition of variableDefinitions) {
90
+ if (definition.required && !usedVariables.includes(definition.name)) {
91
+ errors.push(`Required variable '${definition.name}' is defined but not used in template`);
92
+ }
93
+ }
94
+ return {
95
+ isValid: errors.length === 0,
96
+ errors
97
+ };
98
+ }
99
+ };
100
+
101
+ // src/types/template.types.ts
102
+ import { z } from "zod";
103
+ var TemplateType = /* @__PURE__ */ ((TemplateType2) => {
104
+ TemplateType2["ALIMTALK"] = "ALIMTALK";
105
+ TemplateType2["SMS"] = "SMS";
106
+ TemplateType2["LMS"] = "LMS";
107
+ TemplateType2["MMS"] = "MMS";
108
+ TemplateType2["RCS"] = "RCS";
109
+ return TemplateType2;
110
+ })(TemplateType || {});
111
+ var TemplateCategory = /* @__PURE__ */ ((TemplateCategory2) => {
112
+ TemplateCategory2["AUTHENTICATION"] = "AUTHENTICATION";
113
+ TemplateCategory2["NOTIFICATION"] = "NOTIFICATION";
114
+ TemplateCategory2["PROMOTION"] = "PROMOTION";
115
+ TemplateCategory2["INFORMATION"] = "INFORMATION";
116
+ TemplateCategory2["RESERVATION"] = "RESERVATION";
117
+ TemplateCategory2["SHIPPING"] = "SHIPPING";
118
+ TemplateCategory2["PAYMENT"] = "PAYMENT";
119
+ return TemplateCategory2;
120
+ })(TemplateCategory || {});
121
+ var TemplateStatus = /* @__PURE__ */ ((TemplateStatus2) => {
122
+ TemplateStatus2["DRAFT"] = "DRAFT";
123
+ TemplateStatus2["PENDING"] = "PENDING";
124
+ TemplateStatus2["APPROVED"] = "APPROVED";
125
+ TemplateStatus2["REJECTED"] = "REJECTED";
126
+ TemplateStatus2["DISABLED"] = "DISABLED";
127
+ return TemplateStatus2;
128
+ })(TemplateStatus || {});
129
+ var TemplateVariableSchema = z.object({
130
+ name: z.string().min(1),
131
+ type: z.enum(["string", "number", "date", "custom"]),
132
+ required: z.boolean(),
133
+ maxLength: z.number().optional(),
134
+ format: z.string().optional(),
135
+ description: z.string().optional(),
136
+ example: z.string().optional()
137
+ });
138
+ var TemplateButtonSchema = z.object({
139
+ type: z.enum(["WL", "AL", "DS", "BK", "MD"]),
140
+ name: z.string().min(1),
141
+ linkMobile: z.string().url().optional(),
142
+ linkPc: z.string().url().optional(),
143
+ linkIos: z.string().url().optional(),
144
+ linkAndroid: z.string().url().optional(),
145
+ schemeIos: z.string().optional(),
146
+ schemeAndroid: z.string().optional()
147
+ });
148
+ var AlimTalkTemplateSchema = z.object({
149
+ id: z.string(),
150
+ code: z.string(),
151
+ name: z.string().min(1),
152
+ content: z.string().min(1),
153
+ variables: z.array(TemplateVariableSchema),
154
+ buttons: z.array(TemplateButtonSchema).optional(),
155
+ category: z.nativeEnum(TemplateCategory),
156
+ status: z.nativeEnum(TemplateStatus),
157
+ provider: z.string(),
158
+ metadata: z.object({
159
+ createdAt: z.date(),
160
+ updatedAt: z.date(),
161
+ approvedAt: z.date().optional(),
162
+ rejectedAt: z.date().optional(),
163
+ rejectionReason: z.string().optional(),
164
+ usage: z.object({
165
+ sent: z.number().min(0),
166
+ delivered: z.number().min(0),
167
+ failed: z.number().min(0)
168
+ })
169
+ })
170
+ });
171
+
172
+ // src/parser/button.parser.ts
173
+ var ButtonParser = class {
174
+ /**
175
+ * 버튼 설정의 유효성을 검증합니다
176
+ */
177
+ static validateButtons(buttons) {
178
+ const errors = [];
179
+ if (buttons.length > 5) {
180
+ errors.push("Maximum 5 buttons are allowed");
181
+ }
182
+ for (let i = 0; i < buttons.length; i++) {
183
+ const button = buttons[i];
184
+ const buttonIndex = i + 1;
185
+ if (!button.name || button.name.trim().length === 0) {
186
+ errors.push(`Button ${buttonIndex}: name is required`);
187
+ } else if (button.name.length > 14) {
188
+ errors.push(`Button ${buttonIndex}: name cannot exceed 14 characters`);
189
+ }
190
+ this.validateButtonByType(button, buttonIndex, errors);
191
+ }
192
+ return {
193
+ isValid: errors.length === 0,
194
+ errors
195
+ };
196
+ }
197
+ static validateButtonByType(button, buttonIndex, errors) {
198
+ switch (button.type) {
199
+ case "WL":
200
+ this.validateWebLinkButton(button, buttonIndex, errors);
201
+ break;
202
+ case "AL":
203
+ this.validateAppLinkButton(button, buttonIndex, errors);
204
+ break;
205
+ case "DS":
206
+ this.validateDeliveryButton(button, buttonIndex, errors);
207
+ break;
208
+ case "BK":
209
+ this.validateBotKeywordButton(button, buttonIndex, errors);
210
+ break;
211
+ case "MD":
212
+ this.validateMessageDeliveryButton(button, buttonIndex, errors);
213
+ break;
214
+ default:
215
+ errors.push(`Button ${buttonIndex}: invalid button type '${button.type}'`);
216
+ }
217
+ }
218
+ static validateWebLinkButton(button, buttonIndex, errors) {
219
+ if (!button.linkMobile && !button.linkPc) {
220
+ errors.push(`Button ${buttonIndex}: web link button must have at least mobile or PC link`);
221
+ }
222
+ if (button.linkMobile && !this.isValidUrl(button.linkMobile)) {
223
+ errors.push(`Button ${buttonIndex}: invalid mobile link URL`);
224
+ }
225
+ if (button.linkPc && !this.isValidUrl(button.linkPc)) {
226
+ errors.push(`Button ${buttonIndex}: invalid PC link URL`);
227
+ }
228
+ }
229
+ static validateAppLinkButton(button, buttonIndex, errors) {
230
+ const hasAnyLink = button.linkIos || button.linkAndroid || button.schemeIos || button.schemeAndroid;
231
+ if (!hasAnyLink) {
232
+ errors.push(`Button ${buttonIndex}: app link button must have at least one app link or scheme`);
233
+ }
234
+ if (button.linkIos && !this.isValidUrl(button.linkIos)) {
235
+ errors.push(`Button ${buttonIndex}: invalid iOS link URL`);
236
+ }
237
+ if (button.linkAndroid && !this.isValidUrl(button.linkAndroid)) {
238
+ errors.push(`Button ${buttonIndex}: invalid Android link URL`);
239
+ }
240
+ }
241
+ static validateDeliveryButton(button, buttonIndex, errors) {
242
+ }
243
+ static validateBotKeywordButton(button, buttonIndex, errors) {
244
+ }
245
+ static validateMessageDeliveryButton(button, buttonIndex, errors) {
246
+ }
247
+ static isValidUrl(url) {
248
+ try {
249
+ new URL(url);
250
+ return true;
251
+ } catch {
252
+ return false;
253
+ }
254
+ }
255
+ /**
256
+ * 버튼을 JSON 문자열로 직렬화합니다 (카카오 API 형식)
257
+ */
258
+ static serializeButtons(buttons) {
259
+ const serializedButtons = buttons.map((button) => ({
260
+ name: button.name,
261
+ type: button.type,
262
+ url_mobile: button.linkMobile,
263
+ url_pc: button.linkPc,
264
+ scheme_ios: button.schemeIos,
265
+ scheme_android: button.schemeAndroid
266
+ }));
267
+ return JSON.stringify(serializedButtons);
268
+ }
269
+ /**
270
+ * JSON 문자열에서 버튼 배열로 역직렬화합니다
271
+ */
272
+ static deserializeButtons(buttonsJson) {
273
+ try {
274
+ const parsed = JSON.parse(buttonsJson);
275
+ return parsed.map((button) => ({
276
+ name: button.name,
277
+ type: button.type,
278
+ linkMobile: button.url_mobile,
279
+ linkPc: button.url_pc,
280
+ linkIos: button.url_ios,
281
+ linkAndroid: button.url_android,
282
+ schemeIos: button.scheme_ios,
283
+ schemeAndroid: button.scheme_android
284
+ }));
285
+ } catch (error) {
286
+ throw new Error(`Failed to parse buttons JSON: ${error}`);
287
+ }
288
+ }
289
+ };
290
+
291
+ // src/parser/validator.ts
292
+ var TemplateValidator = class {
293
+ /**
294
+ * 알림톡 템플릿의 전체적인 유효성을 검증합니다
295
+ */
296
+ static validate(template) {
297
+ const errors = [];
298
+ const warnings = [];
299
+ this.validateBasicFields(template, errors);
300
+ this.validateContent(template, errors, warnings);
301
+ const variableValidation = VariableParser.validateTemplateVariables(
302
+ template.content,
303
+ template.variables
304
+ );
305
+ errors.push(...variableValidation.errors);
306
+ if (template.buttons && template.buttons.length > 0) {
307
+ const buttonValidation = ButtonParser.validateButtons(template.buttons);
308
+ errors.push(...buttonValidation.errors);
309
+ }
310
+ this.validateByCategory(template, errors, warnings);
311
+ return {
312
+ isValid: errors.length === 0,
313
+ errors,
314
+ warnings
315
+ };
316
+ }
317
+ static validateBasicFields(template, errors) {
318
+ if (!template.name || template.name.trim().length === 0) {
319
+ errors.push("Template name is required");
320
+ }
321
+ if (!template.code || template.code.trim().length === 0) {
322
+ errors.push("Template code is required");
323
+ }
324
+ if (!template.provider || template.provider.trim().length === 0) {
325
+ errors.push("Provider is required");
326
+ }
327
+ }
328
+ static validateContent(template, errors, warnings) {
329
+ if (!template.content || template.content.trim().length === 0) {
330
+ errors.push("Template content is required");
331
+ return;
332
+ }
333
+ const content = template.content;
334
+ if (content.length > 1e3) {
335
+ errors.push("Template content cannot exceed 1000 characters");
336
+ }
337
+ if (this.containsProhibitedCharacters(content)) {
338
+ errors.push("Template content contains prohibited characters");
339
+ }
340
+ const lineBreaks = (content.match(/\n/g) || []).length;
341
+ if (lineBreaks > 20) {
342
+ warnings.push("Template content has many line breaks, which may affect readability");
343
+ }
344
+ this.validateUrlsInContent(content, warnings);
345
+ }
346
+ static containsProhibitedCharacters(content) {
347
+ const prohibitedChars = /[<>]/;
348
+ return prohibitedChars.test(content);
349
+ }
350
+ static validateUrlsInContent(content, warnings) {
351
+ const urlPattern = /https?:\/\/[^\s]+/g;
352
+ const urls = content.match(urlPattern) || [];
353
+ for (const url of urls) {
354
+ try {
355
+ new URL(url);
356
+ } catch {
357
+ warnings.push(`Invalid URL found in content: ${url}`);
358
+ }
359
+ }
360
+ }
361
+ static validateByCategory(template, errors, warnings) {
362
+ switch (template.category) {
363
+ case "AUTHENTICATION" /* AUTHENTICATION */:
364
+ this.validateAuthenticationTemplate(template, errors, warnings);
365
+ break;
366
+ case "PROMOTION" /* PROMOTION */:
367
+ this.validatePromotionTemplate(template, errors, warnings);
368
+ break;
369
+ case "PAYMENT" /* PAYMENT */:
370
+ this.validatePaymentTemplate(template, errors, warnings);
371
+ break;
372
+ }
373
+ }
374
+ static validateAuthenticationTemplate(template, errors, warnings) {
375
+ const hasAuthCode = template.variables.some(
376
+ (v) => v.name.includes("\uC778\uC99D") || v.name.includes("\uCF54\uB4DC") || v.name.includes("\uBC88\uD638")
377
+ );
378
+ if (!hasAuthCode) {
379
+ warnings.push("Authentication template should include an authentication code variable");
380
+ }
381
+ if (template.buttons && template.buttons.length > 0) {
382
+ warnings.push("Authentication templates typically should not have buttons");
383
+ }
384
+ }
385
+ static validatePromotionTemplate(template, errors, warnings) {
386
+ if (!template.buttons || template.buttons.length === 0) {
387
+ warnings.push("Promotion templates typically should have action buttons");
388
+ }
389
+ }
390
+ static validatePaymentTemplate(template, errors, warnings) {
391
+ const hasAmountVariable = template.variables.some(
392
+ (v) => v.name.includes("\uAE08\uC561") || v.name.includes("\uAC00\uACA9") || v.name.includes("\uC6D0")
393
+ );
394
+ if (!hasAmountVariable) {
395
+ warnings.push("Payment template should include an amount variable");
396
+ }
397
+ }
398
+ /**
399
+ * 빠른 검증 - 기본적인 필수 필드만 검사
400
+ */
401
+ static quickValidate(template) {
402
+ const errors = [];
403
+ if (!template.name) errors.push("Name is required");
404
+ if (!template.content) errors.push("Content is required");
405
+ if (!template.category) errors.push("Category is required");
406
+ if (!template.provider) errors.push("Provider is required");
407
+ return {
408
+ isValid: errors.length === 0,
409
+ errors,
410
+ warnings: []
411
+ };
412
+ }
413
+ };
414
+
415
+ // src/services/template.service.ts
416
+ var TemplateService = class {
417
+ templates = /* @__PURE__ */ new Map();
418
+ async createTemplate(template) {
419
+ const variables = VariableParser.extractVariables(template.content);
420
+ const newTemplate = {
421
+ ...template,
422
+ id: this.generateTemplateId(),
423
+ variables: variables.map((name) => ({
424
+ name,
425
+ type: "string",
426
+ required: true
427
+ })),
428
+ metadata: {
429
+ createdAt: /* @__PURE__ */ new Date(),
430
+ updatedAt: /* @__PURE__ */ new Date(),
431
+ usage: { sent: 0, delivered: 0, failed: 0 }
432
+ }
433
+ };
434
+ const validation = TemplateValidator.validate(newTemplate);
435
+ if (!validation.isValid) {
436
+ throw new Error(`Template validation failed: ${validation.errors.join(", ")}`);
437
+ }
438
+ this.templates.set(newTemplate.id, newTemplate);
439
+ return newTemplate;
440
+ }
441
+ async getTemplate(templateId) {
442
+ return this.templates.get(templateId) || null;
443
+ }
444
+ async updateTemplate(templateId, updates) {
445
+ const template = this.templates.get(templateId);
446
+ if (!template) {
447
+ throw new Error(`Template ${templateId} not found`);
448
+ }
449
+ const updatedTemplate = {
450
+ ...template,
451
+ ...updates,
452
+ metadata: {
453
+ ...template.metadata,
454
+ updatedAt: /* @__PURE__ */ new Date()
455
+ }
456
+ };
457
+ this.templates.set(templateId, updatedTemplate);
458
+ return updatedTemplate;
459
+ }
460
+ async deleteTemplate(templateId) {
461
+ this.templates.delete(templateId);
462
+ }
463
+ async renderTemplate(templateId, variables) {
464
+ const template = this.templates.get(templateId);
465
+ if (!template) {
466
+ throw new Error(`Template ${templateId} not found`);
467
+ }
468
+ const validation = VariableParser.validateVariables(template.variables, variables);
469
+ if (!validation.isValid) {
470
+ throw new Error(`Variable validation failed: ${validation.errors.join(", ")}`);
471
+ }
472
+ return VariableParser.replaceVariables(template.content, variables);
473
+ }
474
+ generateTemplateId() {
475
+ return `tpl_${Date.now()}_${Math.random().toString(36).substr(2, 9)}`;
476
+ }
477
+ };
478
+
479
+ // src/builder/template.builder.ts
480
+ var TemplateBuilder = class _TemplateBuilder {
481
+ template = {
482
+ variables: [],
483
+ buttons: [],
484
+ status: "DRAFT" /* DRAFT */,
485
+ metadata: {
486
+ createdAt: /* @__PURE__ */ new Date(),
487
+ updatedAt: /* @__PURE__ */ new Date(),
488
+ usage: { sent: 0, delivered: 0, failed: 0 }
489
+ }
490
+ };
491
+ /**
492
+ * Set template name
493
+ */
494
+ name(name) {
495
+ this.template.name = name;
496
+ return this;
497
+ }
498
+ /**
499
+ * Set template code (provider specific)
500
+ */
501
+ code(code) {
502
+ this.template.code = code;
503
+ return this;
504
+ }
505
+ /**
506
+ * Set template content with variables
507
+ */
508
+ content(content) {
509
+ this.template.content = content;
510
+ const extractedVariables = VariableParser.extractVariables(content);
511
+ const existingVariableNames = (this.template.variables || []).map((v) => v.name);
512
+ for (const varName of extractedVariables) {
513
+ if (!existingVariableNames.includes(varName)) {
514
+ this.variable(varName, "string", true);
515
+ }
516
+ }
517
+ return this;
518
+ }
519
+ /**
520
+ * Set template category
521
+ */
522
+ category(category) {
523
+ this.template.category = category;
524
+ return this;
525
+ }
526
+ /**
527
+ * Set template provider
528
+ */
529
+ provider(provider) {
530
+ this.template.provider = provider;
531
+ return this;
532
+ }
533
+ /**
534
+ * Set template status
535
+ */
536
+ status(status) {
537
+ this.template.status = status;
538
+ return this;
539
+ }
540
+ /**
541
+ * Add a variable definition
542
+ */
543
+ variable(name, type = "string", required = true, options = {}) {
544
+ if (!this.template.variables) {
545
+ this.template.variables = [];
546
+ }
547
+ this.template.variables = this.template.variables.filter((v) => v.name !== name);
548
+ const variable = {
549
+ name,
550
+ type,
551
+ required,
552
+ ...options
553
+ };
554
+ this.template.variables.push(variable);
555
+ return this;
556
+ }
557
+ /**
558
+ * Add multiple variables at once
559
+ */
560
+ variables(variables) {
561
+ for (const variable of variables) {
562
+ this.variable(
563
+ variable.name,
564
+ variable.type || "string",
565
+ variable.required ?? true,
566
+ {
567
+ maxLength: variable.maxLength,
568
+ format: variable.format,
569
+ description: variable.description,
570
+ example: variable.example
571
+ }
572
+ );
573
+ }
574
+ return this;
575
+ }
576
+ /**
577
+ * Add a web link button
578
+ */
579
+ webLinkButton(name, mobileUrl, pcUrl) {
580
+ return this.button({
581
+ type: "WL",
582
+ name,
583
+ linkMobile: mobileUrl,
584
+ linkPc: pcUrl
585
+ });
586
+ }
587
+ /**
588
+ * Add an app link button
589
+ */
590
+ appLinkButton(name, options) {
591
+ return this.button({
592
+ type: "AL",
593
+ name,
594
+ linkIos: options.iosUrl,
595
+ linkAndroid: options.androidUrl,
596
+ schemeIos: options.iosScheme,
597
+ schemeAndroid: options.androidScheme
598
+ });
599
+ }
600
+ /**
601
+ * Add a delivery tracking button
602
+ */
603
+ deliveryButton(name) {
604
+ return this.button({
605
+ type: "DS",
606
+ name
607
+ });
608
+ }
609
+ /**
610
+ * Add a bot keyword button
611
+ */
612
+ botKeywordButton(name) {
613
+ return this.button({
614
+ type: "BK",
615
+ name
616
+ });
617
+ }
618
+ /**
619
+ * Add a message delivery button
620
+ */
621
+ messageDeliveryButton(name) {
622
+ return this.button({
623
+ type: "MD",
624
+ name
625
+ });
626
+ }
627
+ /**
628
+ * Add a custom button
629
+ */
630
+ button(button) {
631
+ if (!this.template.buttons) {
632
+ this.template.buttons = [];
633
+ }
634
+ if (this.template.buttons.length >= 5) {
635
+ throw new Error("Maximum 5 buttons are allowed per template");
636
+ }
637
+ this.template.buttons.push(button);
638
+ return this;
639
+ }
640
+ /**
641
+ * Clear all buttons
642
+ */
643
+ clearButtons() {
644
+ this.template.buttons = [];
645
+ return this;
646
+ }
647
+ /**
648
+ * Set template metadata
649
+ */
650
+ metadata(metadata) {
651
+ this.template.metadata = {
652
+ ...this.template.metadata,
653
+ ...metadata,
654
+ updatedAt: /* @__PURE__ */ new Date()
655
+ };
656
+ return this;
657
+ }
658
+ /**
659
+ * Validate the current template
660
+ */
661
+ validate() {
662
+ const quickValidation = TemplateValidator.quickValidate(this.template);
663
+ if (!quickValidation.isValid) {
664
+ return quickValidation;
665
+ }
666
+ try {
667
+ const fullTemplate = this.build();
668
+ return TemplateValidator.validate(fullTemplate);
669
+ } catch (error) {
670
+ return {
671
+ isValid: false,
672
+ errors: [error instanceof Error ? error.message : "Unknown validation error"],
673
+ warnings: []
674
+ };
675
+ }
676
+ }
677
+ /**
678
+ * Preview the template with sample variables
679
+ */
680
+ preview(sampleVariables = {}) {
681
+ if (!this.template.content) {
682
+ throw new Error("Template content is required for preview");
683
+ }
684
+ const variables = { ...this.generateSampleVariables(), ...sampleVariables };
685
+ return VariableParser.replaceVariables(this.template.content, variables);
686
+ }
687
+ /**
688
+ * Generate sample variables based on variable definitions
689
+ */
690
+ generateSampleVariables() {
691
+ const samples = {};
692
+ for (const variable of this.template.variables || []) {
693
+ if (variable.example) {
694
+ samples[variable.name] = variable.example;
695
+ } else {
696
+ switch (variable.type) {
697
+ case "string":
698
+ samples[variable.name] = variable.name.includes("name") || variable.name.includes("\uC774\uB984") ? "\uD64D\uAE38\uB3D9" : variable.name.includes("code") || variable.name.includes("\uCF54\uB4DC") ? "123456" : `\uC0D8\uD50C${variable.name}`;
699
+ break;
700
+ case "number":
701
+ samples[variable.name] = variable.name.includes("amount") || variable.name.includes("\uAE08\uC561") ? 1e4 : variable.name.includes("count") || variable.name.includes("\uAC1C\uC218") ? 1 : 123;
702
+ break;
703
+ case "date":
704
+ samples[variable.name] = /* @__PURE__ */ new Date();
705
+ break;
706
+ default:
707
+ samples[variable.name] = `\uC0D8\uD50C\uAC12`;
708
+ }
709
+ }
710
+ }
711
+ return samples;
712
+ }
713
+ /**
714
+ * Clone the current builder
715
+ */
716
+ clone() {
717
+ const cloned = new _TemplateBuilder();
718
+ cloned.template = JSON.parse(JSON.stringify(this.template));
719
+ return cloned;
720
+ }
721
+ /**
722
+ * Reset the builder to start fresh
723
+ */
724
+ reset() {
725
+ this.template = {
726
+ variables: [],
727
+ buttons: [],
728
+ status: "DRAFT" /* DRAFT */,
729
+ metadata: {
730
+ createdAt: /* @__PURE__ */ new Date(),
731
+ updatedAt: /* @__PURE__ */ new Date(),
732
+ usage: { sent: 0, delivered: 0, failed: 0 }
733
+ }
734
+ };
735
+ return this;
736
+ }
737
+ /**
738
+ * Build the final template
739
+ */
740
+ build() {
741
+ if (!this.template.name) throw new Error("Template name is required");
742
+ if (!this.template.code) throw new Error("Template code is required");
743
+ if (!this.template.content) throw new Error("Template content is required");
744
+ if (!this.template.category) throw new Error("Template category is required");
745
+ if (!this.template.provider) throw new Error("Template provider is required");
746
+ const template = {
747
+ id: this.template.id || this.generateTemplateId(),
748
+ name: this.template.name,
749
+ code: this.template.code,
750
+ content: this.template.content,
751
+ variables: this.template.variables || [],
752
+ buttons: this.template.buttons,
753
+ category: this.template.category,
754
+ status: this.template.status || "DRAFT" /* DRAFT */,
755
+ provider: this.template.provider,
756
+ metadata: {
757
+ ...this.template.metadata,
758
+ updatedAt: /* @__PURE__ */ new Date()
759
+ }
760
+ };
761
+ const validation = TemplateValidator.validate(template);
762
+ if (!validation.isValid) {
763
+ throw new Error(`Template validation failed: ${validation.errors.join(", ")}`);
764
+ }
765
+ return template;
766
+ }
767
+ generateTemplateId() {
768
+ return `tpl_${Date.now()}_${Math.random().toString(36).substr(2, 9)}`;
769
+ }
770
+ };
771
+ var TemplateBuilders = {
772
+ /**
773
+ * Create an authentication template builder
774
+ */
775
+ authentication(name, provider) {
776
+ return new TemplateBuilder().name(name).category("AUTHENTICATION" /* AUTHENTICATION */).provider(provider).variable("code", "string", true, {
777
+ maxLength: 10,
778
+ description: "Authentication code",
779
+ example: "123456"
780
+ });
781
+ },
782
+ /**
783
+ * Create a notification template builder
784
+ */
785
+ notification(name, provider) {
786
+ return new TemplateBuilder().name(name).category("NOTIFICATION" /* NOTIFICATION */).provider(provider).variable("name", "string", true, {
787
+ description: "Recipient name",
788
+ example: "\uD64D\uAE38\uB3D9"
789
+ });
790
+ },
791
+ /**
792
+ * Create a promotion template builder
793
+ */
794
+ promotion(name, provider) {
795
+ return new TemplateBuilder().name(name).category("PROMOTION" /* PROMOTION */).provider(provider).variable("name", "string", true, {
796
+ description: "Customer name",
797
+ example: "\uD64D\uAE38\uB3D9"
798
+ }).variable("discount", "number", false, {
799
+ description: "Discount percentage",
800
+ example: "20"
801
+ });
802
+ },
803
+ /**
804
+ * Create a payment template builder
805
+ */
806
+ payment(name, provider) {
807
+ return new TemplateBuilder().name(name).category("PAYMENT" /* PAYMENT */).provider(provider).variable("name", "string", true, {
808
+ description: "Customer name",
809
+ example: "\uD64D\uAE38\uB3D9"
810
+ }).variable("amount", "number", true, {
811
+ description: "Payment amount",
812
+ example: "10000"
813
+ });
814
+ }
815
+ };
816
+
817
+ // src/registry/template.registry.ts
818
+ import { EventEmitter } from "events";
819
+ var TemplateRegistry = class extends EventEmitter {
820
+ constructor(options = {}) {
821
+ super();
822
+ this.options = options;
823
+ this.options = { ...this.defaultOptions, ...options };
824
+ if (this.options.enableAutoBackup) {
825
+ this.startAutoBackup();
826
+ }
827
+ }
828
+ templates = /* @__PURE__ */ new Map();
829
+ templatesByCode = /* @__PURE__ */ new Map();
830
+ templatesByProvider = /* @__PURE__ */ new Map();
831
+ templatesByCategory = /* @__PURE__ */ new Map();
832
+ templateHistories = /* @__PURE__ */ new Map();
833
+ usageStats = /* @__PURE__ */ new Map();
834
+ backupTimer;
835
+ defaultOptions = {
836
+ enableVersioning: true,
837
+ maxVersionsPerTemplate: 10,
838
+ enableUsageTracking: true,
839
+ enableAutoBackup: false,
840
+ backupInterval: 36e5,
841
+ // 1 hour
842
+ enableValidationOnRegister: true
843
+ };
844
+ /**
845
+ * Register a new template
846
+ */
847
+ async register(template) {
848
+ if (this.options.enableValidationOnRegister) {
849
+ const validation = TemplateValidator.validate(template);
850
+ if (!validation.isValid) {
851
+ throw new Error(`Template validation failed: ${validation.errors.join(", ")}`);
852
+ }
853
+ }
854
+ const existingTemplate = this.getByCode(template.code, template.provider);
855
+ if (existingTemplate && existingTemplate.id !== template.id) {
856
+ throw new Error(`Template with code '${template.code}' already exists for provider '${template.provider}'`);
857
+ }
858
+ this.templates.set(template.id, template);
859
+ this.templatesByCode.set(`${template.provider}:${template.code}`, template);
860
+ this.updateIndexes(template);
861
+ if (this.options.enableVersioning) {
862
+ this.initializeVersionHistory(template);
863
+ }
864
+ if (this.options.enableUsageTracking) {
865
+ this.initializeUsageStats(template);
866
+ }
867
+ this.emit("template:registered", { template });
868
+ }
869
+ /**
870
+ * Update an existing template
871
+ */
872
+ async update(templateId, updates) {
873
+ const existing = this.templates.get(templateId);
874
+ if (!existing) {
875
+ throw new Error(`Template ${templateId} not found`);
876
+ }
877
+ const updatedTemplate = {
878
+ ...existing,
879
+ ...updates,
880
+ id: templateId,
881
+ // Ensure ID doesn't change
882
+ metadata: {
883
+ ...existing.metadata,
884
+ ...updates.metadata,
885
+ updatedAt: /* @__PURE__ */ new Date()
886
+ }
887
+ };
888
+ if (this.options.enableValidationOnRegister) {
889
+ const validation = TemplateValidator.validate(updatedTemplate);
890
+ if (!validation.isValid) {
891
+ throw new Error(`Template validation failed: ${validation.errors.join(", ")}`);
892
+ }
893
+ }
894
+ if (this.options.enableVersioning) {
895
+ this.addVersionToHistory(existing, updatedTemplate);
896
+ }
897
+ this.templates.set(templateId, updatedTemplate);
898
+ this.templatesByCode.set(`${updatedTemplate.provider}:${updatedTemplate.code}`, updatedTemplate);
899
+ this.updateIndexes(updatedTemplate);
900
+ this.emit("template:updated", {
901
+ oldTemplate: existing,
902
+ newTemplate: updatedTemplate
903
+ });
904
+ return updatedTemplate;
905
+ }
906
+ /**
907
+ * Get template by ID
908
+ */
909
+ get(templateId) {
910
+ return this.templates.get(templateId) || null;
911
+ }
912
+ /**
913
+ * Get template by code and provider
914
+ */
915
+ getByCode(code, provider) {
916
+ return this.templatesByCode.get(`${provider}:${code}`) || null;
917
+ }
918
+ /**
919
+ * Search templates with filters and pagination
920
+ */
921
+ search(filters = {}, options = {}) {
922
+ let templates = Array.from(this.templates.values());
923
+ if (filters.provider) {
924
+ templates = templates.filter((t) => t.provider === filters.provider);
925
+ }
926
+ if (filters.category) {
927
+ templates = templates.filter((t) => t.category === filters.category);
928
+ }
929
+ if (filters.status) {
930
+ templates = templates.filter((t) => t.status === filters.status);
931
+ }
932
+ if (filters.nameContains) {
933
+ const searchTerm = filters.nameContains.toLowerCase();
934
+ templates = templates.filter((t) => t.name.toLowerCase().includes(searchTerm));
935
+ }
936
+ if (filters.codeContains) {
937
+ const searchTerm = filters.codeContains.toLowerCase();
938
+ templates = templates.filter((t) => t.code.toLowerCase().includes(searchTerm));
939
+ }
940
+ if (filters.createdAfter) {
941
+ templates = templates.filter((t) => t.metadata.createdAt >= filters.createdAfter);
942
+ }
943
+ if (filters.createdBefore) {
944
+ templates = templates.filter((t) => t.metadata.createdAt <= filters.createdBefore);
945
+ }
946
+ if (filters.usageMin !== void 0) {
947
+ templates = templates.filter((t) => t.metadata.usage.sent >= filters.usageMin);
948
+ }
949
+ if (filters.usageMax !== void 0) {
950
+ templates = templates.filter((t) => t.metadata.usage.sent <= filters.usageMax);
951
+ }
952
+ const sortBy = options.sortBy || "createdAt";
953
+ const sortOrder = options.sortOrder || "desc";
954
+ templates.sort((a, b) => {
955
+ let aValue, bValue;
956
+ switch (sortBy) {
957
+ case "name":
958
+ aValue = a.name;
959
+ bValue = b.name;
960
+ break;
961
+ case "code":
962
+ aValue = a.code;
963
+ bValue = b.code;
964
+ break;
965
+ case "createdAt":
966
+ aValue = a.metadata.createdAt.getTime();
967
+ bValue = b.metadata.createdAt.getTime();
968
+ break;
969
+ case "updatedAt":
970
+ aValue = a.metadata.updatedAt.getTime();
971
+ bValue = b.metadata.updatedAt.getTime();
972
+ break;
973
+ case "usage":
974
+ aValue = a.metadata.usage.sent;
975
+ bValue = b.metadata.usage.sent;
976
+ break;
977
+ default:
978
+ aValue = a.metadata.createdAt.getTime();
979
+ bValue = b.metadata.createdAt.getTime();
980
+ }
981
+ if (sortOrder === "asc") {
982
+ return aValue < bValue ? -1 : aValue > bValue ? 1 : 0;
983
+ } else {
984
+ return aValue > bValue ? -1 : aValue < bValue ? 1 : 0;
985
+ }
986
+ });
987
+ const page = options.page || 1;
988
+ const limit = options.limit || 20;
989
+ const start = (page - 1) * limit;
990
+ const end = start + limit;
991
+ const paginatedTemplates = templates.slice(start, end);
992
+ return {
993
+ templates: paginatedTemplates,
994
+ total: templates.length,
995
+ page,
996
+ limit,
997
+ hasMore: end < templates.length
998
+ };
999
+ }
1000
+ /**
1001
+ * Get templates by provider
1002
+ */
1003
+ getByProvider(provider) {
1004
+ const templateIds = this.templatesByProvider.get(provider) || /* @__PURE__ */ new Set();
1005
+ return Array.from(templateIds).map((id) => this.templates.get(id)).filter((template) => template !== void 0);
1006
+ }
1007
+ /**
1008
+ * Get templates by category
1009
+ */
1010
+ getByCategory(category) {
1011
+ const templateIds = this.templatesByCategory.get(category) || /* @__PURE__ */ new Set();
1012
+ return Array.from(templateIds).map((id) => this.templates.get(id)).filter((template) => template !== void 0);
1013
+ }
1014
+ /**
1015
+ * Delete template
1016
+ */
1017
+ async delete(templateId) {
1018
+ const template = this.templates.get(templateId);
1019
+ if (!template) {
1020
+ return false;
1021
+ }
1022
+ this.templates.delete(templateId);
1023
+ this.templatesByCode.delete(`${template.provider}:${template.code}`);
1024
+ const providerSet = this.templatesByProvider.get(template.provider);
1025
+ if (providerSet) {
1026
+ providerSet.delete(templateId);
1027
+ if (providerSet.size === 0) {
1028
+ this.templatesByProvider.delete(template.provider);
1029
+ }
1030
+ }
1031
+ const categorySet = this.templatesByCategory.get(template.category);
1032
+ if (categorySet) {
1033
+ categorySet.delete(templateId);
1034
+ if (categorySet.size === 0) {
1035
+ this.templatesByCategory.delete(template.category);
1036
+ }
1037
+ }
1038
+ this.templateHistories.delete(templateId);
1039
+ this.usageStats.delete(templateId);
1040
+ this.emit("template:deleted", { template });
1041
+ return true;
1042
+ }
1043
+ /**
1044
+ * Get template version history
1045
+ */
1046
+ getHistory(templateId) {
1047
+ return this.templateHistories.get(templateId) || null;
1048
+ }
1049
+ /**
1050
+ * Get specific template version
1051
+ */
1052
+ getVersion(templateId, version) {
1053
+ const history = this.templateHistories.get(templateId);
1054
+ if (!history) return null;
1055
+ const versionEntry = history.versions.find((v) => v.version === version);
1056
+ return versionEntry ? versionEntry.template : null;
1057
+ }
1058
+ /**
1059
+ * Restore template to a specific version
1060
+ */
1061
+ async restoreVersion(templateId, version) {
1062
+ const versionTemplate = this.getVersion(templateId, version);
1063
+ if (!versionTemplate) {
1064
+ throw new Error(`Version ${version} not found for template ${templateId}`);
1065
+ }
1066
+ return this.update(templateId, {
1067
+ ...versionTemplate,
1068
+ metadata: {
1069
+ ...versionTemplate.metadata,
1070
+ updatedAt: /* @__PURE__ */ new Date()
1071
+ }
1072
+ });
1073
+ }
1074
+ /**
1075
+ * Get template usage statistics
1076
+ */
1077
+ getUsageStats(templateId) {
1078
+ return this.usageStats.get(templateId) || null;
1079
+ }
1080
+ /**
1081
+ * Update template usage statistics
1082
+ */
1083
+ updateUsageStats(templateId, stats) {
1084
+ const template = this.templates.get(templateId);
1085
+ if (!template) return;
1086
+ if (stats.sent) template.metadata.usage.sent += stats.sent;
1087
+ if (stats.delivered) template.metadata.usage.delivered += stats.delivered;
1088
+ if (stats.failed) template.metadata.usage.failed += stats.failed;
1089
+ if (this.options.enableUsageTracking) {
1090
+ const usageStats = this.usageStats.get(templateId);
1091
+ if (usageStats) {
1092
+ if (stats.sent) usageStats.totalSent += stats.sent;
1093
+ if (stats.delivered) usageStats.totalDelivered += stats.delivered;
1094
+ if (stats.failed) usageStats.totalFailed += stats.failed;
1095
+ usageStats.deliveryRate = usageStats.totalSent > 0 ? usageStats.totalDelivered / usageStats.totalSent * 100 : 0;
1096
+ usageStats.failureRate = usageStats.totalSent > 0 ? usageStats.totalFailed / usageStats.totalSent * 100 : 0;
1097
+ usageStats.lastUsed = /* @__PURE__ */ new Date();
1098
+ const today = (/* @__PURE__ */ new Date()).toISOString().split("T")[0];
1099
+ let todayStats = usageStats.usageByDay.find((d) => d.date === today);
1100
+ if (!todayStats) {
1101
+ todayStats = { date: today, sent: 0, delivered: 0, failed: 0 };
1102
+ usageStats.usageByDay.push(todayStats);
1103
+ usageStats.usageByDay = usageStats.usageByDay.sort((a, b) => b.date.localeCompare(a.date)).slice(0, 30);
1104
+ }
1105
+ if (stats.sent) todayStats.sent += stats.sent;
1106
+ if (stats.delivered) todayStats.delivered += stats.delivered;
1107
+ if (stats.failed) todayStats.failed += stats.failed;
1108
+ }
1109
+ }
1110
+ this.emit("usage:updated", { templateId, stats });
1111
+ }
1112
+ /**
1113
+ * Get registry statistics
1114
+ */
1115
+ getStats() {
1116
+ const templates = Array.from(this.templates.values());
1117
+ const byProvider = {};
1118
+ const byCategory = {};
1119
+ const byStatus = {};
1120
+ for (const template of templates) {
1121
+ byProvider[template.provider] = (byProvider[template.provider] || 0) + 1;
1122
+ byCategory[template.category] = (byCategory[template.category] || 0) + 1;
1123
+ byStatus[template.status] = (byStatus[template.status] || 0) + 1;
1124
+ }
1125
+ return {
1126
+ totalTemplates: templates.length,
1127
+ byProvider,
1128
+ byCategory,
1129
+ byStatus
1130
+ };
1131
+ }
1132
+ /**
1133
+ * Export templates to JSON
1134
+ */
1135
+ export(filters) {
1136
+ const result = this.search(filters, { limit: 1e4 });
1137
+ return JSON.stringify({
1138
+ templates: result.templates,
1139
+ exportedAt: (/* @__PURE__ */ new Date()).toISOString(),
1140
+ total: result.total
1141
+ }, null, 2);
1142
+ }
1143
+ /**
1144
+ * Import templates from JSON
1145
+ */
1146
+ async import(jsonData, options = {}) {
1147
+ const result = { imported: 0, skipped: 0, errors: [] };
1148
+ try {
1149
+ const data = JSON.parse(jsonData);
1150
+ const templates = data.templates || [];
1151
+ for (const templateData of templates) {
1152
+ try {
1153
+ const existingTemplate = this.get(templateData.id);
1154
+ if (existingTemplate && !options.overwrite) {
1155
+ result.skipped++;
1156
+ continue;
1157
+ }
1158
+ await this.register(templateData);
1159
+ result.imported++;
1160
+ } catch (error) {
1161
+ result.errors.push(`Template ${templateData.id}: ${error instanceof Error ? error.message : "Unknown error"}`);
1162
+ }
1163
+ }
1164
+ } catch (error) {
1165
+ result.errors.push(`Invalid JSON format: ${error instanceof Error ? error.message : "Unknown error"}`);
1166
+ }
1167
+ return result;
1168
+ }
1169
+ /**
1170
+ * Clear all templates (use with caution!)
1171
+ */
1172
+ clear() {
1173
+ this.templates.clear();
1174
+ this.templatesByCode.clear();
1175
+ this.templatesByProvider.clear();
1176
+ this.templatesByCategory.clear();
1177
+ this.templateHistories.clear();
1178
+ this.usageStats.clear();
1179
+ this.emit("registry:cleared");
1180
+ }
1181
+ /**
1182
+ * Stop the registry and cleanup
1183
+ */
1184
+ destroy() {
1185
+ if (this.backupTimer) {
1186
+ clearInterval(this.backupTimer);
1187
+ this.backupTimer = void 0;
1188
+ }
1189
+ this.removeAllListeners();
1190
+ this.emit("registry:destroyed");
1191
+ }
1192
+ updateIndexes(template) {
1193
+ if (!this.templatesByProvider.has(template.provider)) {
1194
+ this.templatesByProvider.set(template.provider, /* @__PURE__ */ new Set());
1195
+ }
1196
+ this.templatesByProvider.get(template.provider).add(template.id);
1197
+ if (!this.templatesByCategory.has(template.category)) {
1198
+ this.templatesByCategory.set(template.category, /* @__PURE__ */ new Set());
1199
+ }
1200
+ this.templatesByCategory.get(template.category).add(template.id);
1201
+ }
1202
+ initializeVersionHistory(template) {
1203
+ const history = {
1204
+ templateId: template.id,
1205
+ versions: [{
1206
+ version: 1,
1207
+ template: JSON.parse(JSON.stringify(template)),
1208
+ changes: ["Initial version"],
1209
+ createdAt: /* @__PURE__ */ new Date()
1210
+ }],
1211
+ currentVersion: 1
1212
+ };
1213
+ this.templateHistories.set(template.id, history);
1214
+ }
1215
+ addVersionToHistory(oldTemplate, newTemplate) {
1216
+ const history = this.templateHistories.get(newTemplate.id);
1217
+ if (!history) return;
1218
+ const changes = [];
1219
+ if (oldTemplate.name !== newTemplate.name) changes.push("Name changed");
1220
+ if (oldTemplate.content !== newTemplate.content) changes.push("Content changed");
1221
+ if (oldTemplate.category !== newTemplate.category) changes.push("Category changed");
1222
+ if (oldTemplate.status !== newTemplate.status) changes.push("Status changed");
1223
+ if (JSON.stringify(oldTemplate.variables) !== JSON.stringify(newTemplate.variables)) {
1224
+ changes.push("Variables changed");
1225
+ }
1226
+ if (JSON.stringify(oldTemplate.buttons) !== JSON.stringify(newTemplate.buttons)) {
1227
+ changes.push("Buttons changed");
1228
+ }
1229
+ if (changes.length === 0) return;
1230
+ const newVersion = {
1231
+ version: history.currentVersion + 1,
1232
+ template: JSON.parse(JSON.stringify(newTemplate)),
1233
+ changes,
1234
+ createdAt: /* @__PURE__ */ new Date()
1235
+ };
1236
+ history.versions.push(newVersion);
1237
+ history.currentVersion = newVersion.version;
1238
+ if (history.versions.length > this.options.maxVersionsPerTemplate) {
1239
+ history.versions = history.versions.slice(-this.options.maxVersionsPerTemplate);
1240
+ }
1241
+ }
1242
+ initializeUsageStats(template) {
1243
+ const stats = {
1244
+ templateId: template.id,
1245
+ totalSent: template.metadata.usage.sent,
1246
+ totalDelivered: template.metadata.usage.delivered,
1247
+ totalFailed: template.metadata.usage.failed,
1248
+ deliveryRate: 0,
1249
+ failureRate: 0,
1250
+ usageByDay: []
1251
+ };
1252
+ if (stats.totalSent > 0) {
1253
+ stats.deliveryRate = stats.totalDelivered / stats.totalSent * 100;
1254
+ stats.failureRate = stats.totalFailed / stats.totalSent * 100;
1255
+ }
1256
+ this.usageStats.set(template.id, stats);
1257
+ }
1258
+ startAutoBackup() {
1259
+ this.backupTimer = setInterval(() => {
1260
+ this.emit("backup:requested", {
1261
+ data: this.export(),
1262
+ timestamp: /* @__PURE__ */ new Date()
1263
+ });
1264
+ }, this.options.backupInterval);
1265
+ }
1266
+ };
1267
+ export {
1268
+ ButtonParser,
1269
+ TemplateBuilder,
1270
+ TemplateBuilders,
1271
+ TemplateCategory,
1272
+ TemplateRegistry,
1273
+ TemplateService,
1274
+ TemplateStatus,
1275
+ TemplateType,
1276
+ TemplateValidator,
1277
+ VariableParser
1278
+ };
1279
+ //# sourceMappingURL=index.js.map