Skip to content

Commit ad56bc8

Browse files
authored
Merge pull request #18 from devforth/add-zod-schemas
Add zod schemas
2 parents a383d68 + a581314 commit ad56bc8

3 files changed

Lines changed: 211 additions & 124 deletions

File tree

index.ts

Lines changed: 113 additions & 30 deletions
Original file line numberDiff line numberDiff line change
@@ -1,10 +1,63 @@
1-
import { AdminForthFilterOperators, AdminForthPlugin, Filters } from "adminforth";
1+
import { AdminForthFilterOperators, AdminForthPlugin, parseBody, Filters } from "adminforth";
22
import type { IAdminForth, IHttpServer, AdminForthComponentDeclaration, AdminForthResource } from "adminforth";
33
import { suggestIfTypo, filtersTools } from "adminforth";
44
import type { PluginOptions } from './types.js';
55
import Handlebars from 'handlebars';
66
import { RateLimiter } from "adminforth";
77
import { randomUUID } from "crypto";
8+
import { z } from "zod";
9+
10+
const getRecordsBodySchema = z.object({
11+
record: z.array(z.any()).nullish(),
12+
}).strict();
13+
14+
const getImagesBodySchema = z.object({
15+
record: z.array(z.any()).nullish(),
16+
}).strict();
17+
18+
const getOldDataBodySchema = z.object({
19+
recordId: z.union([z.string(), z.number()]).nullish(),
20+
}).strict();
21+
22+
const updateFieldsBodySchema = z.object({
23+
selectedIds: z.array(z.any()).nullish(),
24+
fields: z.any(),
25+
saveImages: z.boolean().nullish(),
26+
}).strict();
27+
28+
const getImageGenerationPromptsBodySchema = z.object({
29+
recordId: z.union([z.string(), z.number()]).nullish(),
30+
customPrompt: z.string().nullish(),
31+
}).strict();
32+
33+
const createJobBodySchema = z.object({
34+
actionType: z.string().nullish(),
35+
recordId: z.any().optional(),
36+
customPrompt: z.string().nullish(),
37+
filterFilledFields: z.boolean().nullish(),
38+
sessionIds: z.array(z.string()).nullish(),
39+
prompt: z.string().nullish(),
40+
fieldName: z.string().nullish(),
41+
fieldToRegenerate: z.string().nullish(),
42+
action: z.string().nullish(),
43+
}).strict();
44+
45+
const jobStatusBodySchema = z.object({
46+
jobId: z.string(),
47+
}).strict();
48+
49+
const updateRateLimitsBodySchema = z.object({
50+
actionType: z.string().nullish(),
51+
}).strict();
52+
53+
const compileOldImageLinkBodySchema = z.object({
54+
image: z.string().nullish(),
55+
columnName: z.string().nullish(),
56+
}).strict();
57+
58+
const getFilteredIdsBodySchema = z.object({
59+
filters: z.any(),
60+
}).passthrough();
861
const STUB_MODE = false;
962
const jobs = new Map();
1063
export default class BulkAiFlowPlugin extends AdminForthPlugin {
@@ -779,17 +832,20 @@ export default class BulkAiFlowPlugin extends AdminForthPlugin {
779832
server.endpoint({
780833
method: 'POST',
781834
path: `/plugin/${this.pluginInstanceId}/get_records`,
782-
handler: async ( body ) => {
783-
if (!Array.isArray(body.body?.record)) {
835+
handler: async ({ body, response }) => {
836+
const parsed = parseBody(getRecordsBodySchema, body, response);
837+
if ('error' in parsed) return parsed.error;
838+
const data = parsed.data;
839+
if (!Array.isArray(data.record)) {
784840
return { records: [] };
785841
}
786842
let records = [];
787843
const primaryKeyColumn = this.resourceConfig.columns.find((col) => col.primaryKey);
788-
records = await this.adminforth.resource(this.resourceConfig.resourceId).list([Filters.IN(primaryKeyColumn.name, body.body.record)]);
844+
records = await this.adminforth.resource(this.resourceConfig.resourceId).list([Filters.IN(primaryKeyColumn.name, data.record)]);
789845
for( const [index, record] of records.entries() ) {
790846
records[index]._label = this.resourceConfig.recordLabel(records[index]);
791847
}
792-
const order = Object.fromEntries(body.body.record.map((id, i) => [id, i]));
848+
const order = Object.fromEntries(data.record.map((id, i) => [id, i]));
793849

794850
const sortedRecords = records.sort(
795851
(a, b) => order[a.id] - order[b.id]
@@ -804,8 +860,11 @@ export default class BulkAiFlowPlugin extends AdminForthPlugin {
804860
server.endpoint({
805861
method: 'POST',
806862
path: `/plugin/${this.pluginInstanceId}/get_old_data`,
807-
handler: async ({ body }) => {
808-
const recordId = body.recordId;
863+
handler: async ({ body, response }) => {
864+
const parsed = parseBody(getOldDataBodySchema, body, response);
865+
if ('error' in parsed) return parsed.error;
866+
const data = parsed.data;
867+
const recordId = data.recordId;
809868
if (recordId === undefined || recordId === null) {
810869
return { ok: false, error: "Missing recordId" };
811870
}
@@ -824,10 +883,13 @@ export default class BulkAiFlowPlugin extends AdminForthPlugin {
824883
server.endpoint({
825884
method: 'POST',
826885
path: `/plugin/${this.pluginInstanceId}/get_images`,
827-
handler: async ( body ) => {
886+
handler: async ({ body, response }) => {
887+
const parsed = parseBody(getImagesBodySchema, body, response);
888+
if ('error' in parsed) return parsed.error;
889+
const data = parsed.data;
828890
let images = [];
829-
if(body.body.record){
830-
for( const record of body.body.record ) {
891+
if(data.record){
892+
for( const record of data.record ) {
831893
if (this.options.attachFiles) {
832894
images.push(await this.options.attachFiles({ record: record }));
833895
}
@@ -843,15 +905,18 @@ export default class BulkAiFlowPlugin extends AdminForthPlugin {
843905
server.endpoint({
844906
method: 'POST',
845907
path: `/plugin/${this.pluginInstanceId}/update_fields`,
846-
handler: async ({ body, adminUser, headers }) => {
908+
handler: async ({ body, adminUser, headers, response }) => {
909+
const parsed = parseBody(updateFieldsBodySchema, body, response);
910+
if ('error' in parsed) return parsed.error;
911+
const data = parsed.data;
847912
let isAllowedToSave: any = { ok: true, error: '' };
848913
if(this.options.isAllowedToSave) {
849914
isAllowedToSave = await this.options.isAllowedToSave({ record: {}, adminUser: adminUser, resource: this.resourceConfig });
850915
}
851916
if (isAllowedToSave.ok !== false) {
852-
const selectedIds = body.selectedIds || [];
853-
const fieldsToUpdate = body.fields || {};
854-
const saveImages = body.saveImages;
917+
const selectedIds = data.selectedIds || [];
918+
const fieldsToUpdate = data.fields || {};
919+
const saveImages = data.saveImages;
855920
const outputImageFields = [];
856921
if (this.options.generateImages) {
857922
for (const [key, value] of Object.entries(this.options.generateImages)) {
@@ -950,9 +1015,12 @@ export default class BulkAiFlowPlugin extends AdminForthPlugin {
9501015
server.endpoint({
9511016
method: 'POST',
9521017
path: `/plugin/${this.pluginInstanceId}/get_image_generation_prompts`,
953-
handler: async ({ body, headers }) => {
954-
const Id = body.recordId || [];
955-
const customPrompt = body.customPrompt || null;
1018+
handler: async ({ body, headers, response }) => {
1019+
const parsed = parseBody(getImageGenerationPromptsBodySchema, body, response);
1020+
if ('error' in parsed) return parsed.error;
1021+
const data = parsed.data;
1022+
const Id = data.recordId || [];
1023+
const customPrompt = data.customPrompt || null;
9561024
const record = await this.adminforth.resource(this.resourceConfig.resourceId).get([Filters.EQ(this.resourceConfig.columns.find(c => c.primaryKey)?.name, Id)]);
9571025
const compiledGenerationOptions = await this.compileGenerationFieldTemplates(record, JSON.stringify({"prompt": customPrompt}));
9581026
return compiledGenerationOptions;
@@ -976,8 +1044,11 @@ export default class BulkAiFlowPlugin extends AdminForthPlugin {
9761044
server.endpoint({
9771045
method: 'POST',
9781046
path: `/plugin/${this.pluginInstanceId}/create-job`,
979-
handler: async ({ body, adminUser, headers }) => {
980-
const { actionType, recordId, customPrompt, filterFilledFields, sessionIds } = body;
1047+
handler: async ({ body, adminUser, headers, response }) => {
1048+
const parsed = parseBody(createJobBodySchema, body, response);
1049+
if ('error' in parsed) return parsed.error;
1050+
const data = parsed.data;
1051+
const { actionType, recordId, customPrompt, filterFilledFields, sessionIds } = data;
9811052
if (this.options.rateLimits) {
9821053
if (sessionIds && sessionIds.length > 0) {
9831054
for (const sessionId of sessionIds) {
@@ -1011,15 +1082,15 @@ export default class BulkAiFlowPlugin extends AdminForthPlugin {
10111082
this.analyze_image(jobId, recordId, adminUser, headers, customPrompt, filterFilledFields);
10121083
break;
10131084
case 'regenerate_images':
1014-
if (!body.prompt || !body.fieldName) {
1085+
if (!data.prompt || !data.fieldName) {
10151086
jobs.set(jobId, { status: "failed", error: "Missing prompt or field name" });
10161087
break;
10171088
}
1018-
this.regenerateImage(jobId, recordId, body.fieldName, body.prompt, adminUser, headers);
1089+
this.regenerateImage(jobId, recordId, data.fieldName, data.prompt, adminUser, headers);
10191090
break;
10201091
case 'regenerate_cell':
1021-
const fieldToRegenerate = body.fieldToRegenerate;
1022-
this.regenerateCell(jobId, fieldToRegenerate, recordId, body.action, body.prompt);
1092+
const fieldToRegenerate = data.fieldToRegenerate;
1093+
this.regenerateCell(jobId, fieldToRegenerate, recordId, data.action, data.prompt);
10231094
break;
10241095
default:
10251096
jobs.set(jobId, { status: "failed", error: "Unknown action type" });
@@ -1036,7 +1107,10 @@ export default class BulkAiFlowPlugin extends AdminForthPlugin {
10361107
method: 'POST',
10371108
path: `/plugin/${this.pluginInstanceId}/get-job-status`,
10381109
handler: async ({ body, adminUser, headers, response }) => {
1039-
const jobId = body.jobId;
1110+
const parsed = parseBody(jobStatusBodySchema, body, response);
1111+
if ('error' in parsed) return parsed.error;
1112+
const data = parsed.data;
1113+
const jobId = data.jobId;
10401114
if (!jobId) {
10411115
response.setStatus(400);
10421116

@@ -1073,8 +1147,11 @@ export default class BulkAiFlowPlugin extends AdminForthPlugin {
10731147
server.endpoint({
10741148
method: 'POST',
10751149
path: `/plugin/${this.pluginInstanceId}/update-rate-limits`,
1076-
handler: async ({ body, adminUser, headers }) => {
1077-
const actionType = body.actionType;
1150+
handler: async ({ body, adminUser, headers, response }) => {
1151+
const parsed = parseBody(updateRateLimitsBodySchema, body, response);
1152+
if ('error' in parsed) return parsed.error;
1153+
const data = parsed.data;
1154+
const actionType = data.actionType;
10781155
const sessionId = randomUUID();
10791156
this.sessionIds.add(sessionId);
10801157
if (actionType === 'analyze' && this.options.rateLimits?.fillFieldsFromImages) {
@@ -1101,9 +1178,12 @@ export default class BulkAiFlowPlugin extends AdminForthPlugin {
11011178
server.endpoint({
11021179
method: 'POST',
11031180
path: `/plugin/${this.pluginInstanceId}/compile_old_image_link`,
1104-
handler: async ({ body, adminUser, headers }) => {
1105-
const image = body.image;
1106-
const columnName = body.columnName;
1181+
handler: async ({ body, adminUser, headers, response }) => {
1182+
const parsed = parseBody(compileOldImageLinkBodySchema, body, response);
1183+
if ('error' in parsed) return parsed.error;
1184+
const data = parsed.data;
1185+
const image = data.image;
1186+
const columnName = data.columnName;
11071187
if (!image) {
11081188
return { ok: false, error: "Can't find image url" };
11091189
}
@@ -1131,7 +1211,10 @@ export default class BulkAiFlowPlugin extends AdminForthPlugin {
11311211
server.endpoint({
11321212
method: 'POST',
11331213
path: `/plugin/${this.pluginInstanceId}/get_filtered_ids`,
1134-
handler: async ({ body, adminUser, headers, query, cookies, requestUrl }) => {
1214+
handler: async ({ body, adminUser, headers, query, cookies, requestUrl, response }) => {
1215+
const parsed = parseBody(getFilteredIdsBodySchema, body, response);
1216+
if ('error' in parsed) return parsed.error;
1217+
const data = parsed.data;
11351218
const resource = this.resourceConfig;
11361219

11371220
for (const hook of resource.hooks?.list?.beforeDatasourceRequest || []) {

package.json

Lines changed: 4 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -25,17 +25,18 @@
2525
"description": "Bulk AI workflow plugin for AdminForth for batch field generation and image processing",
2626
"devDependencies": {
2727
"@types/node": "latest",
28-
"adminforth": "^3.6.21",
28+
"adminforth": "^3.7.1",
2929
"semantic-release": "^24.2.1",
3030
"semantic-release-slack-bot": "^4.0.2",
3131
"typescript": "^5.7.3"
3232
},
3333
"dependencies": {
3434
"@types/handlebars": "^4.0.40",
35-
"handlebars": "^4.7.8"
35+
"handlebars": "^4.7.8",
36+
"zod": "^4.3.6"
3637
},
3738
"peerDependencies": {
38-
"adminforth": "^3.6.21"
39+
"adminforth": "^3.7.1"
3940
},
4041
"release": {
4142
"plugins": [

0 commit comments

Comments
 (0)