feat: replace joi by zod

This commit is contained in:
Mohamed Marrouchi
2024-11-13 04:21:42 +00:00
parent e642ea093e
commit a7e243de39
22 changed files with 315 additions and 360 deletions

55
api/package-lock.json generated
View File

@@ -33,7 +33,6 @@
"dotenv": "^16.3.1",
"ejs": "^3.1.9",
"express-session": "^1.17.3",
"joi": "^17.11.0",
"module-alias": "^2.2.3",
"mongoose": "^8.0.0",
"mongoose-lean-defaults": "^2.2.1",
@@ -54,7 +53,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.23.8"
},
"devDependencies": {
"@compodoc/compodoc": "^1.1.24",
@@ -3628,19 +3628,6 @@
"integrity": "sha512-59SgoZ3EXbkfSX7b63tsou/SDGzwUEK6MuB5sKqgVK1/XE0fxmpsOb9DQI8LXW3KfGnAjImCGhhEb7uPPAUVNA==",
"dev": true
},
"node_modules/@hapi/hoek": {
"version": "9.3.0",
"resolved": "https://registry.npmjs.org/@hapi/hoek/-/hoek-9.3.0.tgz",
"integrity": "sha512-/c6rf4UJlmHlC9b5BaNvzAcFv7HZ2QHaV0D4/HNlBdvFnvQq8RI4kYdhyPCl7Xj+oWvTWQ8ujhqS53LIgAe6KQ=="
},
"node_modules/@hapi/topo": {
"version": "5.1.0",
"resolved": "https://registry.npmjs.org/@hapi/topo/-/topo-5.1.0.tgz",
"integrity": "sha512-foQZKJig7Ob0BMAYBfcJk8d77QtOe7Wo4ox7ff1lQYoNNAb6jwcY1ncdoy2e9wQZzvNy7ODZCYJkK8kzmcAnAg==",
"dependencies": {
"@hapi/hoek": "^9.0.0"
}
},
"node_modules/@humanwhocodes/config-array": {
"version": "0.11.13",
"resolved": "https://registry.npmjs.org/@humanwhocodes/config-array/-/config-array-0.11.13.tgz",
@@ -5238,24 +5225,6 @@
"url": "https://ko-fi.com/killymxi"
}
},
"node_modules/@sideway/address": {
"version": "4.1.4",
"resolved": "https://registry.npmjs.org/@sideway/address/-/address-4.1.4.tgz",
"integrity": "sha512-7vwq+rOHVWjyXxVlR76Agnvhy8I9rpzjosTESvmhNeXOXdZZB15Fl+TI9x1SiHZH5Jv2wTGduSxFDIaq0m3DUw==",
"dependencies": {
"@hapi/hoek": "^9.0.0"
}
},
"node_modules/@sideway/formula": {
"version": "3.0.1",
"resolved": "https://registry.npmjs.org/@sideway/formula/-/formula-3.0.1.tgz",
"integrity": "sha512-/poHZJJVjx3L+zVD6g9KgHfYnb443oi7wLu/XKojDviHy6HOEOA6z1Trk5aR1dGcmPenJEgb2sK2I80LeS3MIg=="
},
"node_modules/@sideway/pinpoint": {
"version": "2.0.0",
"resolved": "https://registry.npmjs.org/@sideway/pinpoint/-/pinpoint-2.0.0.tgz",
"integrity": "sha512-RNiOoTPkptFtSVzQevY/yWtZwf/RxyVnPy/OcA9HBM3MlGDnBEYL5B41H0MTn0Uec8Hi+2qUtTfG2WWZBmMejQ=="
},
"node_modules/@sinclair/typebox": {
"version": "0.27.8",
"resolved": "https://registry.npmjs.org/@sinclair/typebox/-/typebox-0.27.8.tgz",
@@ -12930,18 +12899,6 @@
"url": "https://github.com/chalk/supports-color?sponsor=1"
}
},
"node_modules/joi": {
"version": "17.11.0",
"resolved": "https://registry.npmjs.org/joi/-/joi-17.11.0.tgz",
"integrity": "sha512-NgB+lZLNoqISVy1rZocE9PZI36bL/77ie924Ri43yEvi9GUUMPeyVIr8KdFTMUlby1p0PBYMk9spIxEUQYqrJQ==",
"dependencies": {
"@hapi/hoek": "^9.0.0",
"@hapi/topo": "^5.0.0",
"@sideway/address": "^4.1.3",
"@sideway/formula": "^3.0.1",
"@sideway/pinpoint": "^2.0.0"
}
},
"node_modules/js-stringify": {
"version": "1.0.2",
"resolved": "https://registry.npmjs.org/js-stringify/-/js-stringify-1.0.2.tgz",
@@ -19421,6 +19378,14 @@
"resolved": "https://registry.npmjs.org/zepto/-/zepto-1.2.0.tgz",
"integrity": "sha512-C1x6lfvBICFTQIMgbt3JqMOno3VOtkWat/xEakLTOurskYIHPmzJrzd1e8BnmtdDVJlGuk5D+FxyCA8MPmkIyA==",
"dev": true
},
"node_modules/zod": {
"version": "3.23.8",
"resolved": "https://registry.npmjs.org/zod/-/zod-3.23.8.tgz",
"integrity": "sha512-XBx9AXhXktjUqnepgTiE5flcKIYWi/rme0Eaj+5Y0lftuGBq+jyRu/md4WnuxqgP1ubdpNCsYEYPxrzVHD8d6g==",
"funding": {
"url": "https://github.com/sponsors/colinhacks"
}
}
}
}

View File

@@ -71,7 +71,6 @@
"dotenv": "^16.3.1",
"ejs": "^3.1.9",
"express-session": "^1.17.3",
"joi": "^17.11.0",
"module-alias": "^2.2.3",
"mongoose": "^8.0.0",
"mongoose-lean-defaults": "^2.2.1",
@@ -92,7 +91,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.23.8"
},
"devDependencies": {
"@compodoc/compodoc": "^1.1.24",

View File

@@ -102,14 +102,14 @@ export class Attachment extends BaseSchema {
* @returns The attachment type ('image', 'audio', 'video' or 'file')
*/
static getTypeByMime(mimeType: string): FileType {
if (mimeType.startsWith(FileType.image)) {
return FileType.image;
} else if (mimeType.startsWith(FileType.audio)) {
return FileType.audio;
} else if (mimeType.startsWith(FileType.video)) {
return FileType.video;
if (mimeType.startsWith('image')) {
return 'image';
} else if (mimeType.startsWith('audio')) {
return 'audio';
} else if (mimeType.startsWith('video')) {
return 'video';
} else {
return FileType.file;
return 'file';
}
}
}

View File

@@ -10,7 +10,6 @@ import { Attachment } from '@/attachment/schemas/attachment.schema';
import { WithUrl } from '@/chat/schemas/types/attachment';
import { ButtonType } from '@/chat/schemas/types/button';
import {
FileType,
OutgoingMessageFormat,
StdOutgoingAttachmentMessage,
StdOutgoingButtonsMessage,
@@ -160,7 +159,7 @@ export const attachmentMessage: StdOutgoingAttachmentMessage<
WithUrl<Attachment>
> = {
attachment: {
type: FileType.image,
type: 'image',
payload: attachmentWithUrl,
},
quickReplies: [

View File

@@ -6,33 +6,47 @@
* 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 { Attachment } from '@/attachment/schemas/attachment.schema';
export enum FileType {
image = 'image',
video = 'video',
audio = 'audio',
file = 'file',
unknown = 'unknown',
}
// Enum for FileType
export const fileTypeSchema = z.enum([
'image',
'video',
'audio',
'file',
'unknown',
]);
export type AttachmentForeignKey = {
url?: string;
attachment_id: string;
};
export type FileType = z.infer<typeof fileTypeSchema>;
// AttachmentForeignKey type schema
export const attachmentForeignKeySchema = z.object({
url: z.string().url().optional(),
attachment_id: z.string().nullable(),
});
export type AttachmentForeignKey = z.infer<typeof attachmentForeignKeySchema>;
// WithUrl helper type
export type WithUrl<A> = A & { url?: string };
export interface AttachmentPayload<
// Generic AttachmentPayload type schema
export const attachmentPayloadSchema = <
A extends WithUrl<Attachment> | AttachmentForeignKey,
> {
type: FileType;
payload: A;
}
>(
payloadSchema: z.ZodType<A>,
) =>
z.object({
type: fileTypeSchema,
payload: payloadSchema,
});
export interface IncomingAttachmentPayload {
type: FileType;
payload: {
url: string;
};
}
export type AttachmentPayload<
A extends WithUrl<Attachment> | AttachmentForeignKey,
> = z.infer<ReturnType<typeof attachmentPayloadSchema<A>>>;
export type IncomingAttachmentPayload = z.infer<
ReturnType<typeof attachmentPayloadSchema<AttachmentForeignKey>>
>;

View File

@@ -6,23 +6,50 @@
* 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';
// Enum for ButtonType
export enum ButtonType {
postback = 'postback',
web_url = 'web_url',
}
export type PostBackButton = {
type: ButtonType.postback;
title: string;
payload: string;
};
// Zod schema for ButtonType
export const buttonTypeSchema = z.enum([
ButtonType.postback,
ButtonType.web_url,
]);
export type WebUrlButton = {
type: ButtonType.web_url;
title: string;
url: string;
messenger_extensions?: boolean;
webview_height_ratio?: 'compact' | 'tall' | 'full';
};
// Base schema for shared fields
export const baseButtonSchema = z.object({
type: buttonTypeSchema,
title: z.string().max(20),
});
export type Button = PostBackButton | WebUrlButton;
// Conditional schemas
export const postBackButtonSchema = baseButtonSchema.extend({
type: z.literal(ButtonType.postback),
payload: z.string().max(1000),
// No `url`, `messenger_extensions`, or `webview_height_ratio` fields here
});
export const webUrlButtonSchema = baseButtonSchema.extend({
type: z.literal(ButtonType.web_url),
url: z.string().url(),
messenger_extensions: z.boolean().optional(),
webview_height_ratio: z.enum(['compact', 'tall', 'full']).optional(),
// No `payload` field here
});
// Union schema for Button
export const buttonSchema = z.union([postBackButtonSchema, webUrlButtonSchema]);
// Array schema for buttons
export const buttonsSchema = z.array(buttonSchema).max(3);
// Infer types
export type PostBackButton = z.infer<typeof postBackButtonSchema>;
export type WebUrlButton = z.infer<typeof webUrlButtonSchema>;
export type Button = z.infer<typeof buttonSchema>;

View File

@@ -6,10 +6,27 @@
* 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';
// Zod schema for CaptureVar
const captureVarSchema = z.object({
entity: z.union([
// entity=`-1` to match text message
// entity=`-2` for postback payload
// entity is `String` for NLP entities
z
.number()
.int()
.refine((val) => val === -1 || val === -2, {
message: "entity must be -1 or -2 when it's a number",
}),
z.string(), // entity is a string for NLP entities
]),
context_var: z.string(),
});
// Infer the TypeScript type
type CaptureVar = z.infer<typeof captureVarSchema>;
// Export the schema and type
export { CaptureVar, captureVarSchema };

View File

@@ -56,22 +56,6 @@ export enum OutgoingMessageFormat {
carousel = 'carousel',
}
/**
* FileType enum is declared, and currently not used
**/
export enum FileType {
image = 'image',
video = 'video',
audio = 'audio',
file = 'file',
unknown = 'unknown',
}
export enum PayloadType {
location = 'location',
attachments = 'attachments',
}
export type StdOutgoingTextMessage = { text: string };
export type StdOutgoingQuickRepliesMessage = {
@@ -130,7 +114,7 @@ export type StdIncomingPostBackMessage = StdIncomingTextMessage & {
};
export type StdIncomingLocationMessage = {
type: PayloadType.location;
type: 'location';
coordinates: {
lat: number;
lon: number;
@@ -138,7 +122,7 @@ export type StdIncomingLocationMessage = {
};
export type StdIncomingAttachmentMessage = {
type: PayloadType.attachments;
type: 'attachments';
serialized_text: string;
attachment: IncomingAttachmentPayload | IncomingAttachmentPayload[];
};

View File

@@ -6,24 +6,44 @@
* 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 { PayloadType } from './message';
import { z } from 'zod';
export interface PayloadPattern {
label: string;
value: string;
// @todo : rename 'attachment' to 'attachments'
type?: PayloadType;
}
export const payloadTypeSchema = z.enum(['location', 'attachments']);
export type NlpPattern =
| {
entity: string;
match: 'entity';
}
| {
entity: string;
match: 'value';
value: string;
};
// Define PayloadPattern schema
export const PayloadPatternSchema = z.object({
label: z.string(),
value: z.string(),
type: payloadTypeSchema.optional(), // Optional field
});
export type Pattern = string | RegExp | PayloadPattern | NlpPattern[];
export type PayloadPattern = z.infer<typeof PayloadPatternSchema>;
// Define NlpPattern schema
export const NlpPatternEntitySchema = z.object({
entity: z.string(),
match: z.literal('entity'),
});
export const NlpPatternValueSchema = z.object({
entity: z.string(),
match: z.literal('value'),
value: z.string(),
});
export const NlpPatternSchema = z.union([
NlpPatternEntitySchema,
NlpPatternValueSchema,
]);
export type NlpPattern = z.infer<typeof NlpPatternSchema>;
// Define Pattern as a union of possible types
export const patternSchema = z.union([
z.string(),
z.instanceof(RegExp),
PayloadPatternSchema,
z.array(NlpPatternSchema),
]);
export type Pattern = z.infer<typeof patternSchema>;

View File

@@ -6,26 +6,47 @@
* 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 { IncomingAttachmentPayload } from './attachment';
import { z } from 'zod';
export enum PayloadType {
location = 'location',
attachments = 'attachments',
}
import {
attachmentForeignKeySchema,
attachmentPayloadSchema,
IncomingAttachmentPayload,
} from './attachment';
export const payloadTypeSchema = z.enum(['location', 'attachments']);
export type PayloadType = z.infer<typeof payloadTypeSchema>;
// Define the Payload schema
export const payloadSchema = z.union([
z.object({
type: z.literal('location'),
coordinates: z.object({
lat: z.number(),
lon: z.number(),
}),
}),
z.object({
type: z.literal('attachments'),
attachments: attachmentPayloadSchema(attachmentForeignKeySchema),
}),
]);
export type Payload =
| {
type: PayloadType.location;
type: 'location';
coordinates: {
lat: number;
lon: number;
};
}
| {
type: PayloadType.attachments;
type: 'attachments';
attachments: IncomingAttachmentPayload;
};
// Enum for QuickReplyType
export enum QuickReplyType {
text = 'text',
location = 'location',
@@ -33,8 +54,42 @@ export enum QuickReplyType {
user_email = 'user_email',
}
export interface StdQuickReply {
content_type: QuickReplyType;
title: string;
payload: string;
}
export const quickReplyTypeSchema = z.enum(
Object.values(QuickReplyType) as [string, ...string[]],
);
// Schema for StdQuickReply with conditional constraints using superRefine
export const stdQuickReplySchema = z
.object({
content_type: quickReplyTypeSchema,
title: z.string().max(20).optional(),
payload: z.string().max(1000).optional(),
})
.superRefine((val, ctx) => {
if (val.content_type === 'text') {
if (!val.title) {
ctx.addIssue({
path: ['title'],
code: z.ZodIssueCode.custom,
message: "Title is required when content_type is 'text'.",
});
}
if (!val.payload) {
ctx.addIssue({
path: ['payload'],
code: z.ZodIssueCode.custom,
message: "Payload is required when content_type is 'text'.",
});
}
}
});
export type StdQuickReply = z.infer<typeof stdQuickReplySchema>;
// Schema for the array with max 11 items
export const quickRepliesArraySchema = z
.array(stdQuickReplySchema)
.max(11, { message: 'You can provide up to 11 quick replies.' });
export type QuickRepliesArray = z.infer<typeof quickRepliesArraySchema>;

View File

@@ -62,9 +62,8 @@ import { BlockRepository } from '../repositories/block.repository';
import { Block, BlockModel } from '../schemas/block.schema';
import { Category, CategoryModel } from '../schemas/category.schema';
import { LabelModel } from '../schemas/label.schema';
import { FileType } from '../schemas/types/attachment';
import { Context } from '../schemas/types/context';
import { PayloadType, StdOutgoingListMessage } from '../schemas/types/message';
import { StdOutgoingListMessage } from '../schemas/types/message';
import { SubscriberContext } from '../schemas/types/subscriberContext';
import { CategoryRepository } from './../repositories/category.repository';
@@ -360,7 +359,7 @@ describe('BlockService', () => {
it("should match payload when it's an attachment location", () => {
const result = blockService.matchPayload(
{
type: PayloadType.location,
type: 'location',
coordinates: {
lat: 15,
lon: 23,
@@ -374,9 +373,9 @@ describe('BlockService', () => {
it("should match payload when it's an attachment file", () => {
const result = blockService.matchPayload(
{
type: PayloadType.attachments,
type: 'attachments',
attachments: {
type: FileType.file,
type: 'file',
payload: {
url: 'http://link.to/the/file',
},

View File

@@ -12,103 +12,61 @@ import {
ValidatorConstraint,
ValidatorConstraintInterface,
} from 'class-validator';
import Joi from 'joi';
import { z } from 'zod';
import attachmentSchema from '@/attachment/schemas/attachment.schema';
import { buttonsSchema } from '../schemas/types/button';
import { BlockMessage } from '../schemas/types/message';
import { quickRepliesArraySchema } from '../schemas/types/quick-reply';
export function isValidMessage(msg: any) {
if (typeof msg === 'string' && msg !== '') {
// Custom code
const MESSAGE_REGEX = /^function \(context\) \{[^]+\}/;
if (!MESSAGE_REGEX.test(msg)) {
// eslint-disable-next-line
console.error('Block Model : Invalid custom code.', msg);
return false;
} else {
return true;
}
} else if (Array.isArray(msg)) {
// Simple text message
const textSchema = Joi.array().items(Joi.string().max(1000).required());
const textCheck = textSchema.validate(msg);
return !textCheck.error;
} else if (typeof msg === 'object') {
if ('plugin' in msg) {
return true;
} else {
const buttonsSchema = Joi.array().items(
Joi.object().keys({
type: Joi.string().valid('postback', 'web_url').required(),
title: Joi.string().max(20),
payload: Joi.alternatives().conditional('type', {
is: 'postback',
then: Joi.string().max(1000).required(),
otherwise: Joi.forbidden(),
}),
url: Joi.alternatives().conditional('type', {
is: 'web_url',
then: Joi.string().uri(),
otherwise: Joi.forbidden(),
}),
messenger_extensions: Joi.alternatives().conditional('type', {
is: 'web_url',
then: Joi.boolean(),
otherwise: Joi.forbidden(),
}),
webview_height_ratio: Joi.alternatives().conditional('type', {
is: 'web_url',
then: Joi.string().valid('compact', 'tall', 'full'),
otherwise: Joi.forbidden(),
}),
}),
);
// Attachment message
const objectSchema = Joi.object().keys({
text: Joi.string().max(1000),
attachment: Joi.object().keys({
type: Joi.string()
.valid('image', 'audio', 'video', 'file', 'unknown')
.required(),
payload: Joi.object().keys({
url: Joi.string().uri(),
attachment_id: Joi.string().allow(null),
}),
}),
elements: Joi.boolean(),
cards: Joi.object().keys({
default_action: buttonsSchema.max(1),
buttons: buttonsSchema.max(3),
}),
buttons: buttonsSchema.max(3),
quickReplies: Joi.array()
.items(
Joi.object().keys({
content_type: Joi.string()
.valid('text', 'location', 'user_phone_number', 'user_email')
.required(),
title: Joi.alternatives().conditional('content_type', {
is: 'text',
then: Joi.string().max(20).required(),
}),
payload: Joi.alternatives().conditional('content_type', {
is: 'text',
then: Joi.string().max(1000).required(),
}),
}),
)
.max(11),
});
const objectCheck = objectSchema.validate(msg);
if (objectCheck.error) {
// eslint-disable-next-line
console.log('Message validation failed! ', objectCheck);
}
return !objectCheck.error;
}
} else {
return false;
}
}
// Schemas for different components
const textSchema = z.array(z.string().max(1000));
const pluginSchema = z.object({
plugin: z.string(),
args: z.record(z.any()), // Plugin-specific settings
});
// Message schema variations
const baseTextMessageSchema = z.object({
text: z.string().max(1000).optional(),
});
const messageWithButtonsSchema = baseTextMessageSchema.extend({
buttons: buttonsSchema,
});
const messageWithQuickRepliesSchema = baseTextMessageSchema.extend({
quickReplies: quickRepliesArraySchema,
});
const messageWithAttachmentSchema = z.object({
attachment: attachmentSchema,
quickReplies: quickRepliesArraySchema.optional(),
});
const messageWithElementsSchema = z.object({
elements: z.array(z.record(z.any())), // Array of generic elements
});
const messageWithPluginSchema = z.object({
plugin: pluginSchema,
});
// Union of all possible message types
const messageSchema = z.union([
textSchema,
messageWithButtonsSchema,
messageWithQuickRepliesSchema,
messageWithAttachmentSchema,
messageWithElementsSchema,
messageWithPluginSchema,
]);
export const isValidMessage = (msg: unknown): boolean => {
return messageSchema.safeParse(msg).success;
};
@ValidatorConstraint({ async: false })
export class MessageValidator implements ValidatorConstraintInterface {

View File

@@ -12,62 +12,14 @@ import {
ValidatorConstraint,
ValidatorConstraintInterface,
} from 'class-validator';
import Joi from 'joi';
import { z } from 'zod';
import { Pattern } from '../schemas/types/pattern';
import { Pattern, patternSchema } from '../schemas/types/pattern';
// Function to check if the given input is a valid Pattern list
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;
}
})
);
const patternArraySchema = z.array(patternSchema);
return patternArraySchema.safeParse(patterns).success;
}
@ValidatorConstraint({ async: false })

View File

@@ -12,43 +12,16 @@ import {
ValidatorConstraint,
ValidatorConstraintInterface,
} from 'class-validator';
import Joi from 'joi';
import { z } from 'zod';
type Tentity = -1 | -2;
import { CaptureVar, captureVarSchema } from '../schemas/types/capture-var';
export interface CaptureVar {
// entity=`-1` to match text message
// entity=`-2` for postback payload
// entity is `String` for NLP entities
entity: Tentity | string;
context_var: string;
}
const allowedEntityValues: Tentity[] = [-1, -2];
// Define the array schema
const captureVarArraySchema = z.array(captureVarSchema);
// Validation function
export function isValidVarCapture(vars: CaptureVar[]) {
const captureSchema = Joi.array().items(
Joi.object().keys({
entity: Joi.alternatives().try(
// `-1` to match text message & `-2` for postback payload
Joi.number()
.valid(...allowedEntityValues)
.required(),
// String for NLP entities
Joi.string().required(),
),
context_var: Joi.string()
.regex(/^[a-z][a-z_0-9]*$/)
.required(),
}),
);
const captureCheck = captureSchema.validate(vars);
if (captureCheck.error) {
// eslint-disable-next-line
console.log('Capture vars validation failed!', captureCheck.error);
}
return !captureCheck.error;
return captureVarArraySchema.safeParse(vars).success;
}
@ValidatorConstraint({ async: false })

View File

@@ -13,7 +13,6 @@ import { Test, TestingModule } from '@nestjs/testing';
import { AttachmentRepository } from '@/attachment/repositories/attachment.repository';
import { AttachmentModel } from '@/attachment/schemas/attachment.schema';
import { AttachmentService } from '@/attachment/services/attachment.service';
import { FileType } from '@/chat/schemas/types/attachment';
import { OutgoingMessageFormat } from '@/chat/schemas/types/message';
import { ContentOptions } from '@/chat/schemas/types/options';
import { LoggerService } from '@/logger/logger.service';
@@ -116,7 +115,7 @@ describe('ContentService', () => {
status: true,
dynamicFields: {
image: {
type: FileType.image,
type: 'image',
payload: {
attachment_id: '123',
},
@@ -132,7 +131,7 @@ describe('ContentService', () => {
status: true,
dynamicFields: {
image: {
type: FileType.image,
type: 'image',
payload: {
attachment_id: '456',
},
@@ -148,7 +147,7 @@ describe('ContentService', () => {
status: true,
dynamicFields: {
image: {
type: FileType.image,
type: 'image',
payload: {
url: 'https://remote.file/image.jpg',
},

View File

@@ -9,7 +9,6 @@
import { textMessage } from '@/channel/lib/__test__/common.mock';
import { VIEW_MORE_PAYLOAD } from '@/chat/helpers/constants';
import { ButtonType } from '@/chat/schemas/types/button';
import { FileType } from '@/chat/schemas/types/message';
import { QuickReplyType } from '@/chat/schemas/types/quick-reply';
import { Web } from '../types';
@@ -139,7 +138,7 @@ export const webAttachment: Web.OutgoingMessageBase = {
title: 'Next >',
},
],
type: FileType.image,
type: 'image',
url: 'http://localhost:4000/attachment/download/1/attachment.jpg',
},
type: Web.OutgoingMessageType.file,

View File

@@ -6,7 +6,6 @@
* 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 { FileType } from '@/chat/schemas/types/attachment';
import {
IncomingMessageType,
StdEventType,
@@ -55,7 +54,7 @@ const webEventLocation: Web.IncomingMessage = {
const webEventFile: Web.Event = {
type: Web.IncomingMessageType.file,
data: {
type: FileType.image,
type: 'image',
url: img_url,
size: 500,
},
@@ -149,7 +148,7 @@ export const webEvents: [string, Web.IncomingMessage, any][] = [
payload: {
type: IncomingMessageType.attachments,
attachments: {
type: FileType.image,
type: 'image',
payload: {
url: img_url,
},
@@ -160,7 +159,7 @@ export const webEvents: [string, Web.IncomingMessage, any][] = [
payload: {
url: img_url,
},
type: FileType.image,
type: 'image',
},
serialized_text: `attachment:image:${img_url}`,
type: IncomingMessageType.attachments,

View File

@@ -31,11 +31,9 @@ import { WithUrl } from '@/chat/schemas/types/attachment';
import { Button, ButtonType } from '@/chat/schemas/types/button';
import {
AnyMessage,
FileType,
IncomingMessage,
OutgoingMessage,
OutgoingMessageFormat,
PayloadType,
StdEventType,
StdOutgoingAttachmentMessage,
StdOutgoingButtonsMessage,
@@ -138,7 +136,7 @@ export default abstract class BaseWebChannelHandler<
): Web.IncomingMessageBase {
// Format incoming message
if ('type' in incoming.message) {
if (incoming.message.type === PayloadType.location) {
if (incoming.message.type === 'location') {
const coordinates = incoming.message.coordinates;
return {
type: Web.IncomingMessageType.location,
@@ -682,7 +680,7 @@ export default abstract class BaseWebChannelHandler<
this.storeAttachment(
{
name: file.filename,
type: file.mimetype as FileType, // @Todo : test this
type: Attachment.getTypeByMime(file.mimetype), // @Todo : test this
size: file.size,
},
file.path.replace(dirPath, ''),

View File

@@ -7,8 +7,8 @@
*/
import { SubscriberFull } from '@/chat/schemas/subscriber.schema';
import { FileType } from '@/chat/schemas/types/attachment';
import { Button, WebUrlButton } from '@/chat/schemas/types/button';
import { FileType } from '@/chat/schemas/types/message';
import { StdQuickReply } from '@/chat/schemas/types/quick-reply';
export namespace Web {

View File

@@ -14,7 +14,6 @@ import {
} from '@/chat/schemas/types/attachment';
import {
IncomingMessageType,
PayloadType,
StdEventType,
StdIncomingMessage,
} from '@/chat/schemas/types/message';
@@ -215,7 +214,7 @@ export default class WebEventWrapper<
case IncomingMessageType.location: {
const coordinates = this._adapter.raw.data.coordinates;
return {
type: PayloadType.location,
type: 'location',
coordinates: {
lat: coordinates.lat,
lon: coordinates.lng,
@@ -224,7 +223,7 @@ export default class WebEventWrapper<
}
case IncomingMessageType.attachments:
return {
type: PayloadType.attachments,
type: 'attachments',
attachments: {
type: this._adapter.raw.data.type,
payload: {
@@ -260,7 +259,7 @@ export default class WebEventWrapper<
case IncomingMessageType.location: {
const coordinates = this._adapter.raw.data.coordinates;
return {
type: PayloadType.location,
type: 'location',
coordinates: {
lat: coordinates.lat,
lon: coordinates.lng,
@@ -271,7 +270,7 @@ export default class WebEventWrapper<
case IncomingMessageType.attachments: {
const attachment = this._adapter.raw.data;
return {
type: PayloadType.attachments,
type: 'attachments',
serialized_text: `attachment:${attachment.type}:${attachment.url}`,
attachment: {
type: attachment.type,

View File

@@ -9,9 +9,8 @@
import mongoose from 'mongoose';
import { BlockCreateDto } from '@/chat/dto/block.dto';
import { BlockModel, Block } from '@/chat/schemas/block.schema';
import { Block, BlockModel } from '@/chat/schemas/block.schema';
import { CategoryModel } from '@/chat/schemas/category.schema';
import { FileType } from '@/chat/schemas/types/attachment';
import { ButtonType } from '@/chat/schemas/types/button';
import { QuickReplyType } from '@/chat/schemas/types/quick-reply';
@@ -129,7 +128,7 @@ export const blocks: BlockCreateDto[] = [
},
message: {
attachment: {
type: FileType.image,
type: 'image',
payload: {
attachment_id: '1',
},

View File

@@ -11,13 +11,12 @@ import {
labelMock,
} from '@/channel/lib/__test__/label.mock';
import { BlockFull } from '@/chat/schemas/block.schema';
import { FileType } from '@/chat/schemas/types/attachment';
import { ButtonType } from '@/chat/schemas/types/button';
import { CaptureVar } from '@/chat/schemas/types/capture-var';
import { OutgoingMessageFormat } from '@/chat/schemas/types/message';
import { BlockOptions, ContentOptions } from '@/chat/schemas/types/options';
import { Pattern } from '@/chat/schemas/types/pattern';
import { PayloadType, QuickReplyType } from '@/chat/schemas/types/quick-reply';
import { CaptureVar } from '@/chat/validation-rules/is-valid-capture';
import { QuickReplyType } from '@/chat/schemas/types/quick-reply';
import { modelInstance } from './misc';
@@ -183,7 +182,7 @@ export const attachmentBlock: BlockFull = {
patterns: ['image'],
message: {
attachment: {
type: FileType.image,
type: 'image',
payload: {
url: 'https://fr.facebookbrand.com/wp-content/uploads/2016/09/messenger_icon2.png',
attachment_id: '1234',
@@ -221,12 +220,12 @@ export const blockGetStarted: BlockFull = {
{
label: 'Tounes',
value: 'Tounes',
type: PayloadType.location,
type: 'location',
},
{
label: 'Livre',
value: 'Livre',
type: PayloadType.attachments,
type: 'attachments',
},
[
{