Skip to content

schema-forge site generate has no codegen branch for file field types #52

@rrrodzilla

Description

@rrrodzilla

Problem

schema-forge site generate doesn't have a code branch for file field types in edit.generated.tsx.jinja. Schemas with a file field fall through to the default <input> branch in the form generator and emit:

<input
  id={field.name}
  {...field}
  value={field.value as string | undefined ?? ""}
  className={"input" + (fieldState.error ? " invalid" : "")}
/>

field.value for a file-typed field is a FileAttachment object ({ key, status, size, mime, uploaded_at?, checksum? }), not a string. TypeScript catches the cast at build time:

src/app/pages/<entity>/edit.generated.tsx(140,24): error TS2352: Conversion of
type '{ key: string; status: string; size: number; mime: string; ... }' to type
'string' may be a mistake because neither type sufficiently overlaps with the
other.

Even if you bypass the cast, the runtime renders [object Object] in the input — the generated form is non-functional for file fields. detail.generated.tsx.jinja and list.generated.tsx.jinja have no file handling at all (same hole).

Reproduction

Any schema with a file field, e.g.:

schema Document {
    name:       text(max: 255) required
    attachment: file(bucket: "documents", max_size: "10MB", mime: ["application/pdf"]) required
}
schema-forge site generate -s schemas -o site
cd site && pnpm install && pnpm build
# fails on edit.generated.tsx

What the file branch needs to do

The runtime already exposes a three-endpoint upload flow:

  1. POST /api/v1/forge/schemas/<schema>/entities/<id>/fields/<field>/upload-url — mint presigned PUT URL
  2. PUT <presigned_url> — client uploads bytes directly to S3
  3. POST /api/v1/forge/schemas/<schema>/entities/<id>/fields/<field>/confirm-upload — register the upload

The codegen should emit a <FileUpload> component that:

  • In edit mode with an existing attachment: shows filename / status / size and offers a "Replace" button that runs the three-step flow against the existing entity id
  • In create mode (no entity id yet): defers the upload until after the entity is created — typical pattern is "save first, then upload" with a two-phase form, OR creates a draft entity, OR (simplest) renders a disabled state with copy explaining file uploads happen after first save
  • Surfaces the file-state-machine status (pending → uploaded → scanning → available|quarantined|rejected)
  • Uses the field's access mode (presigned vs proxied) for download links
  • Vendors as src/components/ui/file-upload.tsx alongside the other shadcn primitives (or as a SchemaForge-specific component if it ends up too coupled to the upload flow)

detail.generated.tsx.jinja likely has the same kind of mismatch when displaying file attachments. list.generated.tsx.jinja should never include a file column by default — it's not summarisable in a list cell, and a column would force N extra presigned-URL fetches per page.

Open design questions (resolve before implementation)

  1. Create-mode strategy: two-phase form ("save first, then upload"), draft entity, or disabled-with-copy. Two-phase is simplest and matches typical shadcn/Hook Form stacks; draft entities introduce GC complexity.
  2. Component location: vendored src/components/ui/file-upload.tsx (shadcn convention) or SchemaForge-specific path? It's not a generic primitive — it knows about the 3-endpoint flow and the file-state-machine.
  3. Polling for scanning → available: server-sent event, client polling, or user-triggered refresh? Avirum scan latency varies; need a UX answer before coding.

Workaround applied locally (stub, do not ship)

crates/schema-forge-cli/templates/site/src/app/pages/edit.generated.tsx.jinja has a read-only stub branch for f.kind == "file" that unblocks pnpm build but leaves users without an upload UI:

{%- elif f.kind == "file" %}
              <div className="mono" style={ { ... }}>
                {(() => {
                  const att = field.value as { key?: string; status?: string; size?: number } | null | undefined
                  return att && att.key
                    ? `${att.key} · ${att.status ?? "unknown"}${att.size ? ` · ${att.size} bytes` : ""}`
                    : "no file attached — upload via API"
                })()}
              </div>

Uploads have to be made via direct API calls until a real <FileUpload> widget ships.

Acceptance criteria

  • edit.generated.tsx.jinja has an f.kind == "file" branch that renders a real upload widget driving the three-endpoint flow
  • detail.generated.tsx.jinja renders attachments with at least filename / size / state / download link
  • list.generated.tsx.jinja either hides file fields by default or renders a non-crashing placeholder
  • A vendored <FileUpload> component ships alongside the existing shadcn primitives
  • pnpm build passes on a generated site for any schema using file fields, with no TS escape hatches needed in either generated or preserve files
  • Create-mode behavior is consistent and documented (whichever strategy is chosen)
  • File-state-machine transitions are visible to the user (no silent scanningquarantined failures)

Split-out adjacent issues

Two smaller things spotted while reproducing this — split into their own issues:

Metadata

Metadata

Assignees

No one assigned

    Labels

    enhancementNew feature or request

    Type

    No type

    Projects

    No projects

    Milestone

    No milestone

    Relationships

    None yet

    Development

    No branches or pull requests

    Issue actions