feat: mock exercise template api (#613)
* feat: move mock exercise template to api * feat: add template migration file * chore: - remove exercise templates form api - rework api to pull exercise templates from db - add exercise template seed to seed file - declutter seed file * chore: - refactor template fetch to be dependent on the selected tag - add tag to schema, remote db and template's seed file - clear remote db's template table and replace data and their respective tags * chore: add number of questions and center loading screen on the frontend * chore: pr fixes * chore: type fixes * chore: - move seed functionality to /seed folder - add typing for individual seed functions - pr fixes * chore: pr fixes v2 * update output css * refactor: update seeding script and schema definitions - Renamed `seedTypes` to `seedQuestionTypes` for clarity in the seeding process. - Enhanced the seeding script to include a command-line interface for better usability, allowing specific seed functions to be executed based on flags. - Removed redundant unique constraints in the schema definitions for `courseNewsfeedComment` and `exerciseTemplate` to streamline database structure. * feat: add email verification error handling page and update session management - Introduced a new page for handling email verification errors, providing user-friendly messages based on error types. - Updated session management logic to improve handling of public routes and session states. - Enhanced security policies in the Svelte configuration to include additional sources for styles and scripts. - Removed the old email verification error page to streamline the codebase. * move the exercise template to cloudflare cdn --------- Co-authored-by: Rotimi Best <[email protected]>
This commit is contained in:
co-authored by
Rotimi Best
parent
ac3f35a97f
commit
50d6ee3111
+35
-24
@@ -821,6 +821,7 @@ import { authOrApiKeyMiddleware } from '@api/middlewares/auth-or-api-key';
|
||||
```
|
||||
|
||||
**API Key Authentication:**
|
||||
|
||||
- Add `API_KEY` to `apps/api/src/config/env.ts` environment schema
|
||||
- Use `Authorization: Bearer <api-key>` header format (standard across popular APIs)
|
||||
- Use `apiKeyMiddleware` for API-key-only endpoints
|
||||
@@ -1100,7 +1101,7 @@ export const app = new Hono().route('/account', accountRouter); // Route availab
|
||||
### Step 5: Run the api build command (at the root directory)
|
||||
|
||||
```
|
||||
sudo pnpm --filter @cio/api build
|
||||
pnpm --filter @cio/api build
|
||||
```
|
||||
|
||||
---
|
||||
@@ -1219,12 +1220,7 @@ export async function createOrg(profileId: string, data: { name: string; siteNam
|
||||
// Business Logic: Check sitename availability
|
||||
const exists = await checkSiteNameExists(data.siteName);
|
||||
if (exists) {
|
||||
throw new AppError(
|
||||
`Site name '${data.siteName}' already exists`,
|
||||
ErrorCodes.SITENAME_EXISTS,
|
||||
409,
|
||||
'siteName'
|
||||
);
|
||||
throw new AppError(`Site name '${data.siteName}' already exists`, ErrorCodes.SITENAME_EXISTS, 409, 'siteName');
|
||||
}
|
||||
|
||||
// Business Logic: Create org and member in a transaction
|
||||
@@ -1256,12 +1252,7 @@ export async function createOrg(profileId: string, data: { name: string; siteNam
|
||||
} catch (error) {
|
||||
// Handle database constraint violations
|
||||
if (error.code === '23505') {
|
||||
throw new AppError(
|
||||
`Site name '${data.siteName}' already exists`,
|
||||
ErrorCodes.SITENAME_EXISTS,
|
||||
409,
|
||||
'siteName'
|
||||
);
|
||||
throw new AppError(`Site name '${data.siteName}' already exists`, ErrorCodes.SITENAME_EXISTS, 409, 'siteName');
|
||||
}
|
||||
throw new AppError('Failed to create organization', ErrorCodes.ORG_CREATE_FAILED, 500);
|
||||
}
|
||||
@@ -1282,8 +1273,11 @@ import { authMiddleware } from '@api/middlewares/auth';
|
||||
import { handleError } from '@api/utils/errors';
|
||||
import { zValidator } from '@hono/zod-validator';
|
||||
|
||||
export const organizationRouter = new Hono()
|
||||
.post('/', authMiddleware, zValidator('json', ZCreateOrganization), async (c) => {
|
||||
export const organizationRouter = new Hono().post(
|
||||
'/',
|
||||
authMiddleware,
|
||||
zValidator('json', ZCreateOrganization),
|
||||
async (c) => {
|
||||
try {
|
||||
const user = c.get('user');
|
||||
const data = c.req.valid('json');
|
||||
@@ -1294,7 +1288,8 @@ export const organizationRouter = new Hono()
|
||||
} catch (error) {
|
||||
return handleError(c, error, 'Failed to create organization');
|
||||
}
|
||||
});
|
||||
}
|
||||
);
|
||||
```
|
||||
|
||||
**Note**: Use `201 Created` status code for successful POST requests that create resources.
|
||||
@@ -1310,8 +1305,7 @@ export * from './organization';
|
||||
// apps/api/src/app.ts
|
||||
import { organizationRouter } from '@api/routes/organization';
|
||||
|
||||
export const app = new Hono()
|
||||
.route('/organization', organizationRouter); // Route available at POST /organization
|
||||
export const app = new Hono().route('/organization', organizationRouter); // Route available at POST /organization
|
||||
```
|
||||
|
||||
---
|
||||
@@ -1373,6 +1367,7 @@ export const orgApi = new OrgApi();
|
||||
```
|
||||
|
||||
**Key Points:**
|
||||
|
||||
- Use `this.execute()` from `BaseApiWithErrors` for automatic loading/error state
|
||||
- Field-specific errors are automatically mapped to `this.errors[field]`
|
||||
- Update stores in `onSuccess` callback
|
||||
@@ -1385,11 +1380,13 @@ export const orgApi = new OrgApi();
|
||||
Sometimes a service function exists in a different context (e.g., `createOrganizationWithOwner` in onboarding service).
|
||||
|
||||
**When to Reuse:**
|
||||
|
||||
- ✅ The function does exactly what you need
|
||||
- ✅ It's in the same domain (organization operations)
|
||||
- ✅ You can call it from your new route without modification
|
||||
|
||||
**When to Create New:**
|
||||
|
||||
- ❌ The existing function has domain-specific logic you don't want (e.g., onboarding-specific)
|
||||
- ❌ You need different parameters or return values
|
||||
- ❌ The function is in a different domain and mixing concerns would be inappropriate
|
||||
@@ -1455,18 +1452,17 @@ When you have existing components using direct Supabase calls, follow this migra
|
||||
#### Step 1: Identify the Operation
|
||||
|
||||
Analyze the existing Supabase call:
|
||||
|
||||
- **What database operation?** (CREATE, UPDATE, DELETE, SELECT)
|
||||
- **What tables are involved?**
|
||||
- **Are there related operations?** (e.g., create org + create member should be atomic)
|
||||
- **What validation is currently done?** (client-side only or also server-side?)
|
||||
|
||||
**Example Analysis:**
|
||||
|
||||
```typescript
|
||||
// Current code
|
||||
const { data: org, error } = await supabase
|
||||
.from('organization')
|
||||
.insert({ name: orgName, siteName: siteName })
|
||||
.select();
|
||||
const { data: org, error } = await supabase.from('organization').insert({ name: orgName, siteName: siteName }).select();
|
||||
|
||||
const { data, error } = await supabase
|
||||
.from('organizationmember')
|
||||
@@ -1482,21 +1478,25 @@ const { data, error } = await supabase
|
||||
#### Step 2: Check for Existing Services
|
||||
|
||||
Search for existing service functions that might handle this:
|
||||
|
||||
- Check `apps/api/src/services/` for similar operations
|
||||
- Look in related domains (e.g., onboarding might have org creation)
|
||||
- Check if the operation exists but in a different context
|
||||
|
||||
**Decision:**
|
||||
|
||||
- **Reuse existing service** if it does what you need
|
||||
- **Create new service** if you need different logic or parameters
|
||||
|
||||
#### Step 3: Create/Update API Route
|
||||
|
||||
If the route doesn't exist:
|
||||
|
||||
- Follow the "Creating a New Route" workflow above
|
||||
- Use appropriate HTTP method: `POST` (create), `PUT` (update), `DELETE` (delete)
|
||||
|
||||
If the route exists:
|
||||
|
||||
- Add new endpoint to existing router
|
||||
- Follow route naming conventions (see [`apps/api/ROUTE_NAMING_BEST_PRACTICES.md`](apps/api/ROUTE_NAMING_BEST_PRACTICES.md))
|
||||
|
||||
@@ -1542,6 +1542,7 @@ class OrgApi extends BaseApiWithErrors {
|
||||
```
|
||||
|
||||
**Key Points:**
|
||||
|
||||
- Always use `this.execute()` from `BaseApiWithErrors`
|
||||
- Client-side validation before API call
|
||||
- Update stores in `onSuccess` callback
|
||||
@@ -1552,6 +1553,7 @@ class OrgApi extends BaseApiWithErrors {
|
||||
Replace Supabase calls with API class method:
|
||||
|
||||
**Before (Direct Supabase):**
|
||||
|
||||
```typescript
|
||||
<script lang="ts">
|
||||
import { supabase } from '$lib/utils/functions/supabase';
|
||||
@@ -1594,6 +1596,7 @@ Replace Supabase calls with API class method:
|
||||
```
|
||||
|
||||
**After (API Route):**
|
||||
|
||||
```typescript
|
||||
<script lang="ts">
|
||||
import { orgApi } from '$features/org/api/org.svelte';
|
||||
@@ -1627,6 +1630,7 @@ Replace Supabase calls with API class method:
|
||||
```
|
||||
|
||||
**Key Differences:**
|
||||
|
||||
- ✅ **No manual transaction cleanup** - Handled by service layer
|
||||
- ✅ **No separate member creation** - Handled atomically by service
|
||||
- ✅ **Automatic store updates** - Handled by API class `onSuccess`
|
||||
@@ -1639,6 +1643,7 @@ Replace Supabase calls with API class method:
|
||||
The `BaseApiWithErrors` class automatically maps field-specific errors from the API response:
|
||||
|
||||
**API Response Structure:**
|
||||
|
||||
```typescript
|
||||
// Field-specific error
|
||||
{
|
||||
@@ -1658,6 +1663,7 @@ The `BaseApiWithErrors` class automatically maps field-specific errors from the
|
||||
```
|
||||
|
||||
**In Component:**
|
||||
|
||||
```typescript
|
||||
await orgApi.create({ name: orgName, siteName });
|
||||
|
||||
@@ -1666,7 +1672,7 @@ if (!orgApi.success) {
|
||||
if (orgApi.errors.siteName) {
|
||||
errors.siteName = orgApi.errors.siteName;
|
||||
}
|
||||
|
||||
|
||||
// General error automatically in orgApi.errors.general
|
||||
if (orgApi.errors.general) {
|
||||
errors.general = orgApi.errors.general;
|
||||
@@ -1675,6 +1681,7 @@ if (!orgApi.success) {
|
||||
```
|
||||
|
||||
**How It Works:**
|
||||
|
||||
1. API returns error with `field` property
|
||||
2. `BaseApiWithErrors.execute()` automatically sets `this.errors[field] = error`
|
||||
3. Component can access `orgApi.errors.fieldName` directly
|
||||
@@ -1732,7 +1739,7 @@ export const app = new Hono()
|
||||
### Step 5: Run the api build command (at the root directory)
|
||||
|
||||
```
|
||||
sudo pnpm --filter @cio/api build
|
||||
pnpm --filter @cio/api build
|
||||
```
|
||||
|
||||
---
|
||||
@@ -2117,6 +2124,7 @@ export const communityApi = new CommunityApi();
|
||||
#### Using the `execute` Method
|
||||
|
||||
**Pattern:**
|
||||
|
||||
```typescript
|
||||
import { BaseApiWithErrors, classroomio } from '$lib/utils/services/api';
|
||||
import { ZItemUpdate } from '@cio/utils/validation/items';
|
||||
@@ -2177,6 +2185,7 @@ export const itemApi = new ItemApi();
|
||||
```
|
||||
|
||||
**Key Points:**
|
||||
|
||||
- **Always use `this.execute`** - Never call the RPC client directly
|
||||
- **Always validate first** - Use Zod's `safeParse()` before API call
|
||||
- **Type the execute call** with `typeof classroomio.{route}.{method}` for type safety
|
||||
@@ -2187,6 +2196,7 @@ export const itemApi = new ItemApi();
|
||||
- **Return early on validation failure** - Don't make API call if validation fails
|
||||
|
||||
**Benefits:**
|
||||
|
||||
- ✅ Automatic loading state management (`this.isLoading`)
|
||||
- ✅ Automatic error state management (`this.error` or `this.errors`)
|
||||
- ✅ Consistent error handling across all API calls
|
||||
@@ -2326,6 +2336,7 @@ const result = await OrgPlanApiServer.createOrgPlan(planData);
|
||||
4. Create `.server.ts` files with static methods that use `getApiKeyHeaders()` helper
|
||||
|
||||
**Helper Function**: Use `getApiKeyHeaders()` from `$lib/utils/services/api/server` to get API key headers. This helper:
|
||||
|
||||
- Returns the correct headers format for RPC client
|
||||
- Validates that `API_KEY` is configured
|
||||
- Provides consistent API key authentication across all server-side files
|
||||
|
||||
Reference in New Issue
Block a user