From 147e4d6aadedbaa9a31d182b269345e541bff470 Mon Sep 17 00:00:00 2001 From: chuck-dbos <134347445+chuck-dbos@users.noreply.github.com> Date: Mon, 27 Jan 2025 16:17:43 -0500 Subject: [PATCH] Validate args only for HTTP decorators or on special request. (#734) --- src/data_validation.ts | 4 +++ src/dbos.ts | 1 + src/decorators.ts | 10 ++++++ src/httpServer/handler.ts | 1 + src/index.ts | 1 + tests/contextfreeapi.test.ts | 62 +++++++++++++++++++++++++++++++++++- 6 files changed, 78 insertions(+), 1 deletion(-) diff --git a/src/data_validation.ts b/src/data_validation.ts index ffea72918..ac6fd3f66 100644 --- a/src/data_validation.ts +++ b/src/data_validation.ts @@ -24,6 +24,10 @@ export function validateMethodArgs(methReg: MethodRegist return; } + if (!methReg.performArgValidation && !methReg.defaults?.defaultArgValidate && argDescriptor.required !== ArgRequiredOptions.REQUIRED) { + return; + } + // Do we have an arg at all if (idx >= args.length) { if (argDescriptor.required === ArgRequiredOptions.REQUIRED || diff --git a/src/dbos.ts b/src/dbos.ts index bbe9d5cb5..34c3927fb 100644 --- a/src/dbos.ts +++ b/src/dbos.ts @@ -119,6 +119,7 @@ function httpApiDec(verb: APITypes, url: string) { const handlerRegistration = registration as unknown as HandlerRegistrationBase; handlerRegistration.apiURL = url; handlerRegistration.apiType = verb; + registration.performArgValidation = true; return descriptor; } diff --git a/src/decorators.ts b/src/decorators.ts index 7fcc6084b..c133ce21f 100644 --- a/src/decorators.ts +++ b/src/decorators.ts @@ -132,6 +132,7 @@ export interface RegistrationDefaults name: string; requiredRole: string[] | undefined; defaultArgRequired: ArgRequiredOptions; + defaultArgValidate: boolean; eventReceiverInfo: Map; } @@ -140,6 +141,7 @@ export interface MethodRegistrationBase { className: string; args: MethodParameter[]; + performArgValidation: boolean; defaults?: RegistrationDefaults; @@ -175,6 +177,7 @@ implements MethodRegistrationBase requiredRole: string[] | undefined = undefined; + performArgValidation:boolean = false; args: MethodParameter[] = []; passContext: boolean = false; @@ -223,6 +226,7 @@ export class ClassRegistration (ctor: T) +{ + const clsreg = getOrCreateClassRegistration(ctor); + clsreg.defaultArgValidate = true; +} + export function DefaultArgOptional(ctor: T) { const clsreg = getOrCreateClassRegistration(ctor); diff --git a/src/httpServer/handler.ts b/src/httpServer/handler.ts index 7c8853169..244cb6af1 100644 --- a/src/httpServer/handler.ts +++ b/src/httpServer/handler.ts @@ -252,6 +252,7 @@ function generateApiDec(verb: APITypes, url: string) { const handlerRegistration = registration as unknown as HandlerRegistrationBase; handlerRegistration.apiURL = url; handlerRegistration.apiType = verb; + registration.performArgValidation = true; return descriptor; } diff --git a/src/index.ts b/src/index.ts index aafc8d3de..4458de0eb 100644 --- a/src/index.ts +++ b/src/index.ts @@ -60,6 +60,7 @@ export { DefaultRequiredRole, DefaultArgRequired, DefaultArgOptional, + DefaultArgValidate, // Typeorm Class Decorators OrmEntities, diff --git a/tests/contextfreeapi.test.ts b/tests/contextfreeapi.test.ts index 4ebfad200..0a3305b35 100644 --- a/tests/contextfreeapi.test.ts +++ b/tests/contextfreeapi.test.ts @@ -1,4 +1,4 @@ -import { Authentication, DBOS, DBOSResponseError, KoaMiddleware, MiddlewareContext, WorkflowQueue } from '../src'; +import { ArgOptional, ArgRequired, Authentication, DBOS, DBOSResponseError, DefaultArgValidate, KoaMiddleware, MiddlewareContext, WorkflowQueue } from '../src'; import { generateDBOSTestConfig, setUpDBOSTestDb, TestKvTable } from './helpers'; import jwt from "koa-jwt"; @@ -77,6 +77,35 @@ class TestFunctions static async awaitAPromise() { await TestFunctions.awaitThis; } + + @DBOS.workflow() + static async argOptionalWorkflow(arg?:string) { + return Promise.resolve(arg); + } + + @DBOS.workflow() + static async argRequiredWorkflow(@ArgRequired arg:string) { + return Promise.resolve(arg); + } +} + +@DefaultArgValidate +class OptionalArgs +{ + @DBOS.workflow() + static async argOptionalWorkflow(@ArgOptional arg?:string) { + return Promise.resolve(arg); + } + + @DBOS.workflow() + static async argRequiredWorkflow(arg: string) { + return Promise.resolve(arg); + } + + @DBOS.workflow() + static async argOptionalOops(@ArgOptional arg?:string) { + return OptionalArgs.argRequiredWorkflow(arg!); + } } const testTableName = "dbos_test_kv"; @@ -470,6 +499,33 @@ async function main9() { await DBOS.shutdown(); } +async function main10() { + const config = generateDBOSTestConfig(); + await setUpDBOSTestDb(config); + DBOS.setConfig(config); + await DBOS.launch(); + + // Shouldn't throw a validation error + await TestFunctions.argOptionalWorkflow('a'); + await TestFunctions.argOptionalWorkflow(); + await expect(async()=>{ + await TestFunctions.argRequiredWorkflow((undefined as string | undefined)!); + }).rejects.toThrow(); + + await OptionalArgs.argOptionalWorkflow('a'); + await OptionalArgs.argOptionalWorkflow(); + + // await OptionalArgs.argRequiredWorkflow(); // Using the compiler for what it is good at + await OptionalArgs.argRequiredWorkflow('a'); + + await OptionalArgs.argOptionalOops('a'); + await expect(async()=>{ + await OptionalArgs.argOptionalOops(); + }).rejects.toThrow(); + + await DBOS.shutdown(); +} + describe("dbos-v2api-tests-main", () => { test("simple-functions", async () => { await main(); @@ -506,4 +562,8 @@ describe("dbos-v2api-tests-main", () => { test("transitions", async() => { await main9(); }, 15000); + + test("argvalidate", async() => { + await main10(); + }, 15000); });