From 8ca12e3a3d04a98910a48d5de6415c893b9894ac Mon Sep 17 00:00:00 2001 From: abdou6666 Date: Mon, 3 Feb 2025 10:40:17 +0100 Subject: [PATCH 01/11] feat: zod attachment --- api/package-lock.json | 11 +++++++- api/package.json | 7 ++--- api/src/chat/schemas/types/attachment.ts | 34 +++++++++++++++--------- 3 files changed, 35 insertions(+), 17 deletions(-) diff --git a/api/package-lock.json b/api/package-lock.json index c9982881..404f3eaf 100644 --- a/api/package-lock.json +++ b/api/package-lock.json @@ -57,7 +57,8 @@ "sanitize-filename": "^1.6.3", "slug": "^8.2.2", "ts-migrate-mongoose": "^3.8.4", - "uuid": "^9.0.1" + "uuid": "^9.0.1", + "zod": "^3.24.1" }, "devDependencies": { "@compodoc/compodoc": "^1.1.24", @@ -20280,6 +20281,14 @@ "resolved": "https://registry.npmjs.org/zepto/-/zepto-1.2.0.tgz", "integrity": "sha512-C1x6lfvBICFTQIMgbt3JqMOno3VOtkWat/xEakLTOurskYIHPmzJrzd1e8BnmtdDVJlGuk5D+FxyCA8MPmkIyA==", "dev": true + }, + "node_modules/zod": { + "version": "3.24.1", + "resolved": "https://registry.npmjs.org/zod/-/zod-3.24.1.tgz", + "integrity": "sha512-muH7gBL9sI1nciMZV67X5fTKKBLtwpZ5VBp1vsOQzj1MhrBZ4wlVCm3gedKZWLp0Oyel8sIGfeiz54Su+OVT+A==", + "funding": { + "url": "https://github.com/sponsors/colinhacks" + } } } } diff --git a/api/package.json b/api/package.json index cb1d9a61..2cd82dbe 100644 --- a/api/package.json +++ b/api/package.json @@ -92,7 +92,8 @@ "sanitize-filename": "^1.6.3", "slug": "^8.2.2", "ts-migrate-mongoose": "^3.8.4", - "uuid": "^9.0.1" + "uuid": "^9.0.1", + "zod": "^3.24.1" }, "devDependencies": { "@compodoc/compodoc": "^1.1.24", @@ -144,8 +145,8 @@ }, "optionalDependencies": { "@css-inline/css-inline-linux-arm64-musl": "^0.14.1", - "@resvg/resvg-js-linux-arm64-musl": "^2.6.2", - "@resvg/resvg-js-darwin-arm64": "^2.6.2" + "@resvg/resvg-js-darwin-arm64": "^2.6.2", + "@resvg/resvg-js-linux-arm64-musl": "^2.6.2" }, "overrides": { "mjml": "5.0.0-alpha.4" diff --git a/api/src/chat/schemas/types/attachment.ts b/api/src/chat/schemas/types/attachment.ts index bb52b303..d4bac205 100644 --- a/api/src/chat/schemas/types/attachment.ts +++ b/api/src/chat/schemas/types/attachment.ts @@ -6,6 +6,8 @@ * 2. All derivative works must include clear attribution to the original creator and software, Hexastack and Hexabot, in a prominent location (e.g., in the software's "About" section, documentation, and README file). */ +import { z } from 'zod'; + export enum FileType { image = 'image', video = 'video', @@ -14,6 +16,8 @@ export enum FileType { unknown = 'unknown', } +export const fileTypeSchema = z.nativeEnum(FileType); + /** * The `AttachmentRef` type defines two possible ways to reference an attachment: * 1. By `id`: This is used when the attachment is uploaded and stored in the Hexabot system. @@ -22,20 +26,24 @@ export enum FileType { * the content is generated or retrieved by a plugin that consumes a third-party API. * In this case, the `url` field contains the direct link to the external resource. */ -export type AttachmentRef = - | { - id: string | null; - } - | { - /** @deprecated To be used only for external URLs (plugins), for stored attachments use "id" instead */ - url: string; - }; -/** IMPORTANT: No need to use generic type here */ -export interface AttachmentPayload { - type: FileType; - payload: T; -} +export const attachmentRefSchema = z.union([ + z.object({ + id: z.string().nullable(), + }), + z.object({ + url: z.string(), + }), +]); + +export type AttachmentRef = z.infer; + +export const attachmentPayloadSchema = z.object({ + type: fileTypeSchema, + payload: attachmentRefSchema, +}); + +export type AttachmentPayload = z.infer; /** @deprecated */ export type WithUrl = A & { url?: string }; From 87e068b2fe695d234563319f2a5f902f142e6a84 Mon Sep 17 00:00:00 2001 From: abdou6666 Date: Mon, 3 Feb 2025 10:44:56 +0100 Subject: [PATCH 02/11] feat: zod capture-var --- api/src/chat/schemas/types/capture-var.ts | 20 ++++++++++++-------- 1 file changed, 12 insertions(+), 8 deletions(-) diff --git a/api/src/chat/schemas/types/capture-var.ts b/api/src/chat/schemas/types/capture-var.ts index 002340ab..f6044cce 100644 --- a/api/src/chat/schemas/types/capture-var.ts +++ b/api/src/chat/schemas/types/capture-var.ts @@ -1,15 +1,19 @@ /* - * Copyright © 2024 Hexastack. All rights reserved. + * Copyright © 2025 Hexastack. All rights reserved. * * Licensed under the GNU Affero General Public License v3.0 (AGPLv3) with the following additional terms: * 1. The name "Hexabot" is a trademark of Hexastack. You may not use this name in derivative works without express written permission. * 2. All derivative works must include clear attribution to the original creator and software, Hexastack and Hexabot, in a prominent location (e.g., in the software's "About" section, documentation, and README file). */ -export interface CaptureVar { - // entity=`-1` to match text message - // entity=`-2` for postback payload - // entity is `String` for NLP entities - entity: number | string; - context_var: string; -} +import { z } from 'zod'; + +// entity=`-1` to match text message +// entity=`-2` for postback payload +// entity is `String` for NLP entities +export const captureVarSchema = z.object({ + entity: z.union([z.number().min(-2).max(-1), z.string()]), + context_var: z.string(), +}); + +export type CaptureVar = z.infer; From 8084623b061f7aca60978211bf133a99970abe14 Mon Sep 17 00:00:00 2001 From: abdou6666 Date: Mon, 3 Feb 2025 10:48:40 +0100 Subject: [PATCH 03/11] feat: zod position --- api/src/chat/schemas/types/position.ts | 14 +++++++++----- 1 file changed, 9 insertions(+), 5 deletions(-) diff --git a/api/src/chat/schemas/types/position.ts b/api/src/chat/schemas/types/position.ts index b8440f0a..7ff3545a 100644 --- a/api/src/chat/schemas/types/position.ts +++ b/api/src/chat/schemas/types/position.ts @@ -1,12 +1,16 @@ /* - * Copyright © 2024 Hexastack. All rights reserved. + * Copyright © 2025 Hexastack. All rights reserved. * * Licensed under the GNU Affero General Public License v3.0 (AGPLv3) with the following additional terms: * 1. The name "Hexabot" is a trademark of Hexastack. You may not use this name in derivative works without express written permission. * 2. All derivative works must include clear attribution to the original creator and software, Hexastack and Hexabot, in a prominent location (e.g., in the software's "About" section, documentation, and README file). */ -export type Position = { - x: number; - y: number; -}; +import { z } from 'zod'; + +export const positionSchema = z.object({ + x: z.number(), + y: z.number(), +}); + +export type Position = z.infer; From b09f198da359ae604929ca2c79baee5fe0663c02 Mon Sep 17 00:00:00 2001 From: abdou6666 Date: Mon, 3 Feb 2025 10:52:43 +0100 Subject: [PATCH 04/11] feat: zod subscriber-context --- api/src/chat/schemas/types/subscriberContext.ts | 12 ++++++++---- 1 file changed, 8 insertions(+), 4 deletions(-) diff --git a/api/src/chat/schemas/types/subscriberContext.ts b/api/src/chat/schemas/types/subscriberContext.ts index 9eebff43..100c054d 100644 --- a/api/src/chat/schemas/types/subscriberContext.ts +++ b/api/src/chat/schemas/types/subscriberContext.ts @@ -1,11 +1,15 @@ /* - * Copyright © 2024 Hexastack. All rights reserved. + * Copyright © 2025 Hexastack. All rights reserved. * * Licensed under the GNU Affero General Public License v3.0 (AGPLv3) with the following additional terms: * 1. The name "Hexabot" is a trademark of Hexastack. You may not use this name in derivative works without express written permission. * 2. All derivative works must include clear attribution to the original creator and software, Hexastack and Hexabot, in a prominent location (e.g., in the software's "About" section, documentation, and README file). */ -export interface SubscriberContext { - vars?: { [key: string]: any }; -} +import { z } from 'zod'; + +export const subscriberContextSchema = z.object({ + vars: z.record(z.any()).optional(), +}); + +export type SubscriberContext = z.infer; From 22046d57e97ad45cda9c1e1f2c83821619a483d8 Mon Sep 17 00:00:00 2001 From: abdou6666 Date: Mon, 3 Feb 2025 10:58:53 +0100 Subject: [PATCH 05/11] feat: zod quick reply --- api/src/chat/schemas/types/quick-reply.ts | 47 ++++++++++++++--------- 1 file changed, 28 insertions(+), 19 deletions(-) diff --git a/api/src/chat/schemas/types/quick-reply.ts b/api/src/chat/schemas/types/quick-reply.ts index 29ef17cd..e58cea15 100644 --- a/api/src/chat/schemas/types/quick-reply.ts +++ b/api/src/chat/schemas/types/quick-reply.ts @@ -6,21 +6,10 @@ * 2. All derivative works must include clear attribution to the original creator and software, Hexastack and Hexabot, in a prominent location (e.g., in the software's "About" section, documentation, and README file). */ -import { AttachmentPayload } from './attachment'; -import { PayloadType } from './message'; +import { z } from 'zod'; -export type Payload = - | { - type: PayloadType.location; - coordinates: { - lat: number; - lon: number; - }; - } - | { - type: PayloadType.attachments; - attachment: AttachmentPayload; - }; +import { attachmentPayloadSchema } from './attachment'; +import { PayloadType } from './message'; export enum QuickReplyType { text = 'text', @@ -29,8 +18,28 @@ export enum QuickReplyType { user_email = 'user_email', } -export interface StdQuickReply { - content_type: QuickReplyType; - title: string; - payload: string; -} +export const cordinatesSchema = z.object({ + lat: z.number(), + lon: z.number(), +}); + +export const payloadSchema = z.discriminatedUnion('type', [ + z.object({ + type: z.literal(PayloadType.location), + coordinates: cordinatesSchema, + }), + z.object({ + type: z.literal(PayloadType.attachments), + attachment: attachmentPayloadSchema, + }), +]); + +export const stdQuickReplySchema = z.object({ + content_type: z.nativeEnum(QuickReplyType), + title: z.string(), + payload: z.string(), +}); + +export type Payload = z.infer; + +export type StdQuickReply = z.infer; From aa8c54c511463b5afa98a6bb5a40d90e455f97e4 Mon Sep 17 00:00:00 2001 From: abdou6666 Date: Mon, 3 Feb 2025 11:09:23 +0100 Subject: [PATCH 06/11] feat: zod pattern --- api/src/chat/schemas/types/pattern.ts | 49 +++++++++++++++++---------- 1 file changed, 31 insertions(+), 18 deletions(-) diff --git a/api/src/chat/schemas/types/pattern.ts b/api/src/chat/schemas/types/pattern.ts index 8b0ceab1..9116a9ec 100644 --- a/api/src/chat/schemas/types/pattern.ts +++ b/api/src/chat/schemas/types/pattern.ts @@ -1,29 +1,42 @@ /* - * Copyright © 2024 Hexastack. All rights reserved. + * Copyright © 2025 Hexastack. All rights reserved. * * Licensed under the GNU Affero General Public License v3.0 (AGPLv3) with the following additional terms: * 1. The name "Hexabot" is a trademark of Hexastack. You may not use this name in derivative works without express written permission. * 2. All derivative works must include clear attribution to the original creator and software, Hexastack and Hexabot, in a prominent location (e.g., in the software's "About" section, documentation, and README file). */ +import { z } from 'zod'; + import { PayloadType } from './message'; -export interface PayloadPattern { - label: string; - value: string; - // @todo : rename 'attachment' to 'attachments' - type?: PayloadType; -} +export const PayloadPatternSchema = z.object({ + label: z.string(), + value: z.string(), + type: z.nativeEnum(PayloadType).optional(), +}); -export type NlpPattern = - | { - entity: string; - match: 'entity'; - } - | { - entity: string; - match: 'value'; - value: string; - }; +export type PayloadPattern = z.infer; -export type Pattern = string | RegExp | PayloadPattern | NlpPattern[]; +export const nlpPatternSchema = z.discriminatedUnion('match', [ + z.object({ + entity: z.string(), + match: z.literal('entity'), + }), + z.object({ + entity: z.string(), + match: z.literal('value'), + value: z.string(), + }), +]); + +export type NlpPattern = z.infer; + +export const PatternSchema = z.union([ + z.string(), + z.instanceof(RegExp), + PayloadPatternSchema, + z.array(nlpPatternSchema), +]); + +export type Pattern = z.infer; From 22007e451aec49e1020aca45c01bc94faf89efca Mon Sep 17 00:00:00 2001 From: abdou6666 Date: Mon, 3 Feb 2025 12:36:32 +0100 Subject: [PATCH 07/11] fix: update pattern schema camel case --- api/src/chat/schemas/types/pattern.ts | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/api/src/chat/schemas/types/pattern.ts b/api/src/chat/schemas/types/pattern.ts index 9116a9ec..80f191d9 100644 --- a/api/src/chat/schemas/types/pattern.ts +++ b/api/src/chat/schemas/types/pattern.ts @@ -32,11 +32,11 @@ export const nlpPatternSchema = z.discriminatedUnion('match', [ export type NlpPattern = z.infer; -export const PatternSchema = z.union([ +export const patternSchema = z.union([ z.string(), z.instanceof(RegExp), PayloadPatternSchema, z.array(nlpPatternSchema), ]); -export type Pattern = z.infer; +export type Pattern = z.infer; From a6d4f78c39b3c690dbb5ae81a6e6d0a0b1ce6192 Mon Sep 17 00:00:00 2001 From: abdou6666 Date: Mon, 3 Feb 2025 13:38:03 +0100 Subject: [PATCH 08/11] fix: change payloadPatternSchema to camel case --- api/src/chat/schemas/types/pattern.ts | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/api/src/chat/schemas/types/pattern.ts b/api/src/chat/schemas/types/pattern.ts index 80f191d9..7c43cf11 100644 --- a/api/src/chat/schemas/types/pattern.ts +++ b/api/src/chat/schemas/types/pattern.ts @@ -10,13 +10,13 @@ import { z } from 'zod'; import { PayloadType } from './message'; -export const PayloadPatternSchema = z.object({ +export const payloadPatternSchema = z.object({ label: z.string(), value: z.string(), type: z.nativeEnum(PayloadType).optional(), }); -export type PayloadPattern = z.infer; +export type PayloadPattern = z.infer; export const nlpPatternSchema = z.discriminatedUnion('match', [ z.object({ @@ -35,7 +35,7 @@ export type NlpPattern = z.infer; export const patternSchema = z.union([ z.string(), z.instanceof(RegExp), - PayloadPatternSchema, + payloadPatternSchema, z.array(nlpPatternSchema), ]); From 7a8545e646ac142972a10417885901a936371c5a Mon Sep 17 00:00:00 2001 From: abdou6666 Date: Mon, 3 Feb 2025 13:52:49 +0100 Subject: [PATCH 09/11] feat: replace joi with zod is-pattern-list --- .../chat/validation-rules/is-pattern-list.ts | 63 +++---------------- 1 file changed, 9 insertions(+), 54 deletions(-) diff --git a/api/src/chat/validation-rules/is-pattern-list.ts b/api/src/chat/validation-rules/is-pattern-list.ts index 528144ee..c6f85852 100644 --- a/api/src/chat/validation-rules/is-pattern-list.ts +++ b/api/src/chat/validation-rules/is-pattern-list.ts @@ -1,5 +1,5 @@ /* - * Copyright © 2024 Hexastack. All rights reserved. + * Copyright © 2025 Hexastack. All rights reserved. * * Licensed under the GNU Affero General Public License v3.0 (AGPLv3) with the following additional terms: * 1. The name "Hexabot" is a trademark of Hexastack. You may not use this name in derivative works without express written permission. @@ -12,62 +12,17 @@ import { ValidatorConstraint, ValidatorConstraintInterface, } from 'class-validator'; -import Joi from 'joi'; -import { Pattern } from '../schemas/types/pattern'; +import { Pattern, patternSchema } from '../schemas/types/pattern'; export function isPatternList(patterns: Pattern[]) { - return ( - Array.isArray(patterns) && - patterns.every((pattern) => { - if (typeof pattern === 'string') { - // Check if valid regex - if (pattern.endsWith('/') && pattern.startsWith('/')) { - try { - new RegExp(pattern.slice(1, -1), 'gi'); - } catch (err) { - return false; - } - return true; - } - // Check if valid string (Equals/Like) - return pattern !== ''; - } else if (Array.isArray(pattern)) { - // Check if valid NLP pattern - const nlpSchema = Joi.array() - .items( - Joi.object().keys({ - entity: Joi.string().required(), - match: Joi.string().valid('entity', 'value').required(), - value: Joi.string().required(), - }), - ) - .min(1); - const nlpCheck = nlpSchema.validate(pattern); - if (nlpCheck.error) { - // console.log('Message validation failed! ', nlpCheck); - } - return !nlpCheck.error; - } else if (typeof pattern === 'object') { - // Invalid structure? - const payloadSchema = Joi.object().keys({ - label: Joi.string().required(), - value: Joi.any().required(), - type: Joi.string(), - }); - const payloadCheck = payloadSchema.validate(pattern); - if (payloadCheck.error) { - // console.log( - // 'Message validation failed! ', - // payloadCheck, - // ); - } - return !payloadCheck.error; - } else { - return false; - } - }) - ); + patterns.every((pattern) => { + const result = patternSchema.safeParse(pattern); + if (!result.success) { + return false; + } + }); + return true; } @ValidatorConstraint({ async: false }) From d4a6c26919a4a2467cf443747aec41a3af6582e7 Mon Sep 17 00:00:00 2001 From: abdou6666 Date: Mon, 3 Feb 2025 14:00:32 +0100 Subject: [PATCH 10/11] fix: ensure the input being passed is an array --- api/src/chat/validation-rules/is-pattern-list.ts | 3 +++ 1 file changed, 3 insertions(+) diff --git a/api/src/chat/validation-rules/is-pattern-list.ts b/api/src/chat/validation-rules/is-pattern-list.ts index c6f85852..77ad43f5 100644 --- a/api/src/chat/validation-rules/is-pattern-list.ts +++ b/api/src/chat/validation-rules/is-pattern-list.ts @@ -16,6 +16,9 @@ import { import { Pattern, patternSchema } from '../schemas/types/pattern'; export function isPatternList(patterns: Pattern[]) { + if (!Array.isArray(patterns)) { + return false; + } patterns.every((pattern) => { const result = patternSchema.safeParse(pattern); if (!result.success) { From 37486150df99a52b57035f71ddbbdc804ad25a89 Mon Sep 17 00:00:00 2001 From: abdou6666 Date: Mon, 3 Feb 2025 14:18:47 +0100 Subject: [PATCH 11/11] fix: apply feedback --- api/src/chat/validation-rules/is-pattern-list.ts | 9 ++------- 1 file changed, 2 insertions(+), 7 deletions(-) diff --git a/api/src/chat/validation-rules/is-pattern-list.ts b/api/src/chat/validation-rules/is-pattern-list.ts index 77ad43f5..c64b2d08 100644 --- a/api/src/chat/validation-rules/is-pattern-list.ts +++ b/api/src/chat/validation-rules/is-pattern-list.ts @@ -19,13 +19,8 @@ export function isPatternList(patterns: Pattern[]) { if (!Array.isArray(patterns)) { return false; } - patterns.every((pattern) => { - const result = patternSchema.safeParse(pattern); - if (!result.success) { - return false; - } - }); - return true; + + return patterns.every((pattern) => patternSchema.safeParse(pattern).success); } @ValidatorConstraint({ async: false })