Files
classroomio/packages/question-types
Rotimi Best a61942feb9 feat: configurable upload file size limits via .env (#695)
* feat: make upload file size limits configurable via .env

Add UPLOAD_MAX_*_MB environment variables for documents, images, videos,
exercise assignment files, AI assistant documents, landing page images,
and media thumbnails. API presign routes validate file size server-side;
dashboard UI reads the same limits from layout data.

* chore: wire upload limit env vars through Docker compose and docs

Pass UPLOAD_MAX_*_MB from the root .env into api and dashboard containers
in both compose files, document them in .env.example and self-hosting guides.

Co-authored-by: Rotimi Best <[email protected]>

* docs: cross-link upload limits in README, Railway, Coolify, and Cloudflare guides

Co-authored-by: Rotimi Best <[email protected]>

* fix: address PR review feedback on upload limits

- Restore 5 MB default for thumbnail uploads (edit-asset-dialog regression)
- Add getResolvedUploadLimits() fallback for Storybook/tests
- Cache core getUploadLimits() like the dashboard helper
- Document presign fileSize as advisory; allow zero-byte fileSize in schema
- Only send fileSize to presign when greater than zero

* docs: remove upload limits from Cloudflare setup guide

---------
2026-06-29 12:48:51 +01:00
..

@cio/question-types

Shared, framework-agnostic question type contracts for ClassroomIO exercise products.

This package is the source of truth for:

  • Question type keys and DB id mappings
  • Type metadata and capability flags
  • Stable render contracts and list contracts
  • Answer payload and serialization helpers
  • Renderer registry helpers (without UI implementation)
  • Form state primitives for question flows

It is intentionally pure TypeScript and contains no UI components.

Why this package exists

@cio/question-types centralizes exercise domain contracts so every app/package can use the same behavior:

  • apps/dashboard uses the same question keys/types as API and DB consumers
  • apps/api can rely on stable question type IDs
  • @cio/ui can implement renderers against one shared contract
  • Future products (for example, forms/assessments) can reuse the same domain model

Package boundary

What belongs here:

  • Domain constants and type-level contracts
  • Pure mapping/lookup helpers
  • Deterministic utilities (same input => same output)
  • Serialization rules for answer payloads

What does not belong here:

  • Svelte/React/Vue components
  • Package-specific UI behavior
  • Network calls
  • DB query implementations

Architecture

flowchart LR
  subgraph Product Apps
    DASH[apps/dashboard]
    API[apps/api]
    FUTURE[future app e.g. forms]
  end

  subgraph Shared Packages
    QT[@cio/question-types\ncontracts + registry + serializer]
    UI[@cio/ui/custom/exercise-question\nrendering implementation]
    DB[(DB question_types ids)]
  end

  DASH -->|question models, keys, lookups| QT
  FUTURE -->|same contracts| QT
  UI -->|imports types + registry helpers| QT
  DASH -->|renders via props/contracts| UI
  API -->|imports QUESTION_TYPE_IDS| QT
  QT -->|stable id <-> key mapping| DB
  DASH -->|answersByKey| QT
  QT -->|serializeExerciseAnswerMap| API

Source layout

packages/question-types/src/
  answer-serializer.ts
  exercise-types.ts
  form-state.ts
  index.ts
  question-answer-payload.ts
  question-scoring-rules.ts
  question-type-capabilities.ts
  question-type-keys.ts
  question-type-lookup.ts
  question-type-registry.ts
  question-type-types.ts
  render-contract.ts
  renderer-registry.ts

Supported question types

Registry source: src/question-type-registry.ts

key id label autoGradable supportsPartialCredit manualGradingRequired
RADIO 1 Single answer yes no no
CHECKBOX 2 Multiple answers yes yes no
TEXTAREA 3 Paragraph no no yes
TRUE_FALSE 4 True/False yes no no
SHORT_ANSWER 5 Short answer no no yes
NUMERIC 6 Numeric answer yes yes no
FILL_BLANK 7 Fill in the blank yes yes no
FILE_UPLOAD 8 File upload no no yes
MATCHING 9 Matching yes yes no
ORDERING 10 Ordering yes yes no
HOTSPOT 11 Hotspot yes yes no
LINK 12 Links no no yes

Public API overview

1) Keys and metadata

  • QUESTION_TYPE_KEY
  • QuestionTypeKey
  • QUESTION_TYPE_REGISTRY
  • QUESTION_TYPE_IDS
  • QUESTION_TYPE_BY_KEY
  • QUESTION_TYPE_TYPENAME_TO_KEY
  • QUESTION_TYPE_ID_TO_KEY

Lookup helpers:

  • getQuestionTypeByKey(key)
  • getQuestionTypeByTypename(typename)
  • getQuestionTypeById(id)
  • getQuestionTypeId(key)

Capability helpers:

  • isAutoGradableQuestionType(key)
  • supportsPartialCredit(key)
  • requiresManualGrading(key)

2) Exercise rendering contracts

Core types (src/exercise-types.ts):

  • ExerciseQuestionModel
  • ExerciseQuestionOption
  • ExerciseAnswerValue
  • ExerciseRendererMode (edit | take | preview | view)
  • ExerciseRendererDefinition<TRenderer>
  • ExerciseQuestionRendererProps

Render contracts (src/render-contract.ts):

  • ExerciseQuestionRenderContract
  • ExerciseQuestionListRenderContract
  • ExerciseQuestionImageUploader
  • getExerciseQuestionContractKey(question, fallbackIndex)

3) Renderer registry helpers (UI-agnostic)

From src/renderer-registry.ts:

  • ExerciseRendererRegistry<TRenderer>
  • getRendererDefinition(registry, questionType, fallback)
  • getRendererForMode(registry, questionType, mode, fallback)

Important behavior:

  • mode: 'view' resolves to 'take' internally in getRendererForMode.

4) Answer payload and serialization

Answer payload union (src/question-answer-payload.ts):

  • QuestionAnswerPayload and per-type payload interfaces (RadioAnswerPayload, CheckboxAnswerPayload, etc.)

Serializer (src/answer-serializer.ts):

  • serializeExerciseAnswer(question, answer)
  • serializeExerciseAnswerMap(questions, answersByKey)
  • SerializedExerciseAnswer

5) Form state and scoring constants

Form state (src/form-state.ts):

  • ExerciseQuestionFormState
  • createExerciseQuestionFormState(initialQuestions)
  • getQuestionStateKey(question, fallbackIndex)

Scoring constants (src/question-scoring-rules.ts):

  • QUESTION_SCORING_MODE
  • ATTEMPT_SCORING_RULE
  • SHOW_CORRECT_ANSWERS_POLICY

Deterministic contract guarantees

The following helpers are deterministic and should be treated as stable contracts:

  • getExerciseQuestionContractKey
  • getQuestionStateKey
  • getQuestionTypeById / getQuestionTypeId
  • getRendererForMode
  • serializeExerciseAnswer / serializeExerciseAnswerMap

If given the same inputs, they always return the same outputs.

Usage examples

Resolve question type from id

import { getQuestionTypeById } from '@cio/question-types';

const metadata = getQuestionTypeById(10);
// => { key: 'ORDERING', label: 'Ordering', ... }

Build a renderer registry in another package

import {
  QUESTION_TYPE_KEY,
  getRendererForMode,
  type ExerciseRendererRegistry,
  type ExerciseRendererDefinition
} from '@cio/question-types';

type Renderer = unknown;

const registry: ExerciseRendererRegistry<Renderer> = {
  [QUESTION_TYPE_KEY.RADIO]: {
    edit: 'RadioEditRenderer',
    take: 'RadioTakeRenderer',
    preview: 'RadioPreviewRenderer'
  }
};

const fallback: ExerciseRendererDefinition<Renderer> = {
  edit: 'FallbackEdit',
  take: 'FallbackTake',
  preview: 'FallbackPreview'
};

const renderer = getRendererForMode(registry, QUESTION_TYPE_KEY.RADIO, 'view', fallback);
// 'view' resolves to 'take'

Serialize answers for API submission

import { serializeExerciseAnswerMap } from '@cio/question-types';

const payload = serializeExerciseAnswerMap(questions, answersByKey);
// => Array<{ questionId, optionId?, answer?, payload? }>

Create question form state

import { createExerciseQuestionFormState } from '@cio/question-types';

const state = createExerciseQuestionFormState(initialQuestions);

Adding a new question type

See ADDING_A_QUESTION_TYPE.md for a step-by-step guide. At a high level:

Build

pnpm -C packages/question-types build

Notes for consumers

  • Prefer importing keys/lookup helpers from this package instead of duplicating constants.
  • Use getExerciseQuestionContractKey consistently for stable answer maps.
  • Keep UI implementation in @cio/ui; keep domain contracts in @cio/question-types.