fix(api): return Zod validation errors from public API routes (#708)

* fix(api): return Zod validation errors from public API routes

Public API handlers now surface ZodError details as 400 VALIDATION_ERROR
responses instead of generic 500 INTERNAL_ERROR payloads. Course import
services also rethrow ZodError so validation failures are not wrapped as
server errors.

Co-authored-by: Rotimi Best <rotimi-best@users.noreply.github.com>

* refactor(utils): centralize course type values without db dependency

Define COURSE_TYPE_VALUES once in @cio/utils/constants and use it for
ZCourseType validation and the Drizzle COURSE_TYPE pgEnum. Removes the
circular @cio/utils -> @cio/db import while keeping course-import schemas
aligned with all four course types including PUBLIC.

Co-authored-by: Rotimi Best <rotimi-best@users.noreply.github.com>

* fix(validation): default missing course metadata allowNewStudent to true

Courses stored without allowNewStudent in metadata failed structure export
validation. Align Zod preprocessing with runtime behavior where absent
values mean enrollments are open.

Co-authored-by: Rotimi Best <rotimi-best@users.noreply.github.com>

* chore(db): drop unused COURSE_TYPE_VALUES re-export from schema

Co-authored-by: Rotimi Best <rotimi-best@users.noreply.github.com>

---------

Co-authored-by: Cursor Agent <cursoragent@cursor.com>
Co-authored-by: Rotimi Best <rotimi-best@users.noreply.github.com>
This commit is contained in:
Rotimi Best
2026-07-02 12:56:58 +01:00
committed by GitHub
co-authored by Rotimi Best Cursor Agent
parent a337363965
commit 0ed0e3864d
12 changed files with 166 additions and 30 deletions
+7 -7
View File
@@ -17,7 +17,7 @@ import {
import { Hono } from '@api/utils/hono';
import { automationKeyMiddleware } from '@api/middlewares/automation-key';
import { automationKeyScopesMiddleware } from '@api/middlewares/automation-key-scopes';
import { handleError } from '@api/utils/errors';
import { handlePublicApiError } from '@api/utils/errors';
import { describeRoute, validator } from 'hono-openapi';
const AudienceListResponse = {
@@ -87,7 +87,7 @@ export const v1AudienceRouter = new Hono()
200
);
} catch (error) {
return handleError(c, error, 'Failed to list audience members');
return handlePublicApiError(c, error, 'Failed to list audience members');
}
}
)
@@ -128,7 +128,7 @@ export const v1AudienceRouter = new Hono()
201
);
} catch (error) {
return handleError(c, error, 'Failed to create audience member');
return handlePublicApiError(c, error, 'Failed to create audience member');
}
}
)
@@ -168,7 +168,7 @@ export const v1AudienceRouter = new Hono()
200
);
} catch (error) {
return handleError(c, error, 'Failed to assign audience to courses');
return handlePublicApiError(c, error, 'Failed to assign audience to courses');
}
}
)
@@ -208,7 +208,7 @@ export const v1AudienceRouter = new Hono()
200
);
} catch (error) {
return handleError(c, error, 'Failed to fetch audience member');
return handlePublicApiError(c, error, 'Failed to fetch audience member');
}
}
)
@@ -249,7 +249,7 @@ export const v1AudienceRouter = new Hono()
200
);
} catch (error) {
return handleError(c, error, 'Failed to remove audience member');
return handlePublicApiError(c, error, 'Failed to remove audience member');
}
}
)
@@ -293,7 +293,7 @@ export const v1AudienceRouter = new Hono()
200
);
} catch (error) {
return handleError(c, error, 'Failed to update audience member');
return handlePublicApiError(c, error, 'Failed to update audience member');
}
}
);
+10 -10
View File
@@ -19,7 +19,7 @@ import {
import { Hono } from '@api/utils/hono';
import { automationKeyMiddleware } from '@api/middlewares/automation-key';
import { automationKeyScopesMiddleware } from '@api/middlewares/automation-key-scopes';
import { handleError } from '@api/utils/errors';
import { handlePublicApiError } from '@api/utils/errors';
import { describeRoute, validator } from 'hono-openapi';
const CoursesListResponse = {
@@ -76,7 +76,7 @@ export const v1CoursesRouter = new Hono()
200
);
} catch (error) {
return handleError(c, error, 'Failed to list courses');
return handlePublicApiError(c, error, 'Failed to list courses');
}
}
)
@@ -117,7 +117,7 @@ export const v1CoursesRouter = new Hono()
201
);
} catch (error) {
return handleError(c, error, 'Failed to create course');
return handlePublicApiError(c, error, 'Failed to create course');
}
}
)
@@ -157,7 +157,7 @@ export const v1CoursesRouter = new Hono()
200
);
} catch (error) {
return handleError(c, error, 'Failed to list course students');
return handlePublicApiError(c, error, 'Failed to list course students');
}
}
)
@@ -197,7 +197,7 @@ export const v1CoursesRouter = new Hono()
200
);
} catch (error) {
return handleError(c, error, 'Failed to export course');
return handlePublicApiError(c, error, 'Failed to export course');
}
}
)
@@ -237,7 +237,7 @@ export const v1CoursesRouter = new Hono()
200
);
} catch (error) {
return handleError(c, error, 'Failed to fetch course structure');
return handlePublicApiError(c, error, 'Failed to fetch course structure');
}
}
)
@@ -281,7 +281,7 @@ export const v1CoursesRouter = new Hono()
200
);
} catch (error) {
return handleError(c, error, 'Failed to update course structure');
return handlePublicApiError(c, error, 'Failed to update course structure');
}
}
)
@@ -321,7 +321,7 @@ export const v1CoursesRouter = new Hono()
200
);
} catch (error) {
return handleError(c, error, 'Failed to fetch course');
return handlePublicApiError(c, error, 'Failed to fetch course');
}
}
)
@@ -364,7 +364,7 @@ export const v1CoursesRouter = new Hono()
200
);
} catch (error) {
return handleError(c, error, 'Failed to update course');
return handlePublicApiError(c, error, 'Failed to update course');
}
}
)
@@ -404,7 +404,7 @@ export const v1CoursesRouter = new Hono()
200
);
} catch (error) {
return handleError(c, error, 'Failed to delete course');
return handlePublicApiError(c, error, 'Failed to delete course');
}
}
);