Skip to content

Commit

Permalink
Validate args only for HTTP decorators or on special request. (#734)
Browse files Browse the repository at this point in the history
chuck-dbos authored Jan 27, 2025

Verified

This commit was created on GitHub.com and signed with GitHub’s verified signature.
1 parent 98676ab commit 147e4d6
Showing 6 changed files with 78 additions and 1 deletion.
4 changes: 4 additions & 0 deletions src/data_validation.ts
Original file line number Diff line number Diff line change
@@ -24,6 +24,10 @@ export function validateMethodArgs<Args extends unknown[]>(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 ||
1 change: 1 addition & 0 deletions src/dbos.ts
Original file line number Diff line number Diff line change
@@ -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;
}
10 changes: 10 additions & 0 deletions src/decorators.ts
Original file line number Diff line number Diff line change
@@ -132,6 +132,7 @@ export interface RegistrationDefaults
name: string;
requiredRole: string[] | undefined;
defaultArgRequired: ArgRequiredOptions;
defaultArgValidate: boolean;
eventReceiverInfo: Map<DBOSEventReceiver, unknown>;
}

@@ -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 <CT extends { new (...args: unknown[]) : object }
name: string = "";
requiredRole: string[] | undefined;
defaultArgRequired: ArgRequiredOptions = ArgRequiredOptions.REQUIRED;
defaultArgValidate: boolean = false;
needsInitialized: boolean = true;

// eslint-disable-next-line @typescript-eslint/ban-types
@@ -598,6 +602,12 @@ export function DefaultArgRequired<T extends { new (...args: unknown[]) : object
clsreg.defaultArgRequired = ArgRequiredOptions.REQUIRED;
}

export function DefaultArgValidate<T extends { new (...args: unknown[]) : object }>(ctor: T)
{
const clsreg = getOrCreateClassRegistration(ctor);
clsreg.defaultArgValidate = true;
}

export function DefaultArgOptional<T extends { new (...args: unknown[]) : object }>(ctor: T)
{
const clsreg = getOrCreateClassRegistration(ctor);
1 change: 1 addition & 0 deletions src/httpServer/handler.ts
Original file line number Diff line number Diff line change
@@ -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;
}
1 change: 1 addition & 0 deletions src/index.ts
Original file line number Diff line number Diff line change
@@ -60,6 +60,7 @@ export {
DefaultRequiredRole,
DefaultArgRequired,
DefaultArgOptional,
DefaultArgValidate,
// Typeorm Class Decorators
OrmEntities,

62 changes: 61 additions & 1 deletion tests/contextfreeapi.test.ts
Original file line number Diff line number Diff line change
@@ -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);
});

0 comments on commit 147e4d6

Please sign in to comment.