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 }; 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; diff --git a/api/src/chat/schemas/types/pattern.ts b/api/src/chat/schemas/types/pattern.ts index 8b0ceab1..7c43cf11 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; 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; 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; 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; diff --git a/api/src/chat/validation-rules/is-pattern-list.ts b/api/src/chat/validation-rules/is-pattern-list.ts index 528144ee..c64b2d08 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,15 @@ 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; - } - }) - ); + if (!Array.isArray(patterns)) { + return false; + } + + return patterns.every((pattern) => patternSchema.safeParse(pattern).success); } @ValidatorConstraint({ async: false })