mirror of
https://github.com/hexastack/hexabot
synced 2025-06-26 18:27:28 +00:00
feat: replace joi by zod
This commit is contained in:
55
api/package-lock.json
generated
55
api/package-lock.json
generated
@@ -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"
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -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",
|
||||
|
||||
@@ -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';
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -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: [
|
||||
|
||||
@@ -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>>
|
||||
>;
|
||||
|
||||
@@ -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>;
|
||||
|
||||
@@ -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 };
|
||||
|
||||
@@ -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[];
|
||||
};
|
||||
|
||||
@@ -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>;
|
||||
|
||||
@@ -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>;
|
||||
|
||||
@@ -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',
|
||||
},
|
||||
|
||||
@@ -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 {
|
||||
|
||||
@@ -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 })
|
||||
|
||||
@@ -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 })
|
||||
|
||||
@@ -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',
|
||||
},
|
||||
|
||||
@@ -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,
|
||||
|
||||
@@ -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,
|
||||
|
||||
@@ -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, ''),
|
||||
|
||||
@@ -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 {
|
||||
|
||||
@@ -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,
|
||||
|
||||
5
api/src/utils/test/fixtures/block.ts
vendored
5
api/src/utils/test/fixtures/block.ts
vendored
@@ -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',
|
||||
},
|
||||
|
||||
@@ -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',
|
||||
},
|
||||
[
|
||||
{
|
||||
|
||||
Reference in New Issue
Block a user