Skip to content

fix: type single relation/select/file fields as single, not arrays - #35

Merged
Karnak19 merged 2 commits into
mainfrom
fix/single-ref-maxselect
Jun 1, 2026
Merged

fix: type single relation/select/file fields as single, not arrays#35
Karnak19 merged 2 commits into
mainfrom
fix/single-ref-maxselect

Conversation

@Karnak19

@Karnak19 Karnak19 commented Jun 1, 2026

Copy link
Copy Markdown
Owner

Closes #28.

Problem

PocketBase stores single-value relation, select, and file fields with maxSelect: 0 — not 1. The generators only treated maxSelect === 1 as single, so every single relation/select/file field was emitted as an array:

// before — owner is a single relation in PocketBase
export type AssessmentBlocksRecord = BaseRecord & {
  owner?: string[] | null   // ❌ should be string
}

Thanks to @-the reporter for the precise diagnosis in the issue.

Fix

Follow PocketBase's own semantics — a field is multiple only when maxSelect > 1 (matching core's IsMultiple()), so 0 and 1 are both single. Centralized in a shared isMultipleField helper to stop the rule from drifting across call sites:

  • type-generator/generate.tsselect, relation, file
  • schema-parser/normalize.tsextractRelations multiple flag (also affected expand-path arity)
  • pbkit-zodselect, relation, file schemas
// after
export type AssessmentBlocksRecord = BaseRecord & {
  owner?: string | null
}

Tests

  • Added regression tests for maxSelect: 0 (relation, select, file) in the type generator
  • Full chain green: bun run ci (lint, typecheck, test, build) — 13/13 turbo tasks pass

Release

Patch changeset added for @karnak19/pbkit and @karnak19/pbkit-zod. The TanStack plugin is unaffected (it derives from generated types, not maxSelect).

🤖 Generated with Claude Code

PocketBase stores single-value relation, select, and file fields with
maxSelect: 0 (not 1). The generators only treated maxSelect === 1 as
single, so those fields were emitted as arrays (string[], (...)[],
z.array(...)).

Follow PocketBase's own rule — multiple iff maxSelect > 1 — via a shared
isMultipleField helper used by the type generator, relation extraction,
and the Zod plugin. Add regression tests for maxSelect: 0.

Closes #28

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>

@github-actions github-actions Bot left a comment

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

This PR correctly fixes the maxSelect: 0 → single-value typing bug across the type generator, relation extraction, and Zod plugin, and the shared isMultipleField helper is a clean abstraction. One test-coverage gap noted below.

  • Missing Zod regression test: the type generator has explicit maxSelect: 0 tests, but the Zod plugin does not — the fixture only uses maxSelect: 1 for single fields, so a future regression in fieldToZod would go undetected.

break;
case "file":
schema = field.options.maxSelect === 1 ? "z.string()" : "z.array(z.string())";
schema = isMultipleField(field) ? "z.array(z.string())" : "z.string()";

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Missing regression test for maxSelect: 0 in the Zod plugin.

The type generator gains three explicit maxSelect: 0 tests in type-generator.test.ts, but the Zod plugin's zod.test.ts has no equivalent. The test fixture (full-schema.json) uses maxSelect: 1 for every single-value relation/select/file field, so the snapshot and assertion tests never exercise the maxSelect: 0 path that this PR fixes.

Impact: if someone later replaces isMultipleField(field) with a direct maxSelect === 1 check (re-introducing the original bug), CI will not catch it in the Zod output.

Fix: either add a maxSelect: 0 field to packages/pbkit-zod/src/test/fixtures/full-schema.json (and update the snapshot), or add a targeted unit test similar to the type generator's:

test("relation maxSelect=0 → z.string() (not array)", () => {
  const schema = parseJson({ collections: [{ name: "x", type: "base", fields: [{ name: "ref", type: "relation", system: false, maxSelect: 0, collectionId: "abc" }] }] });
  const output = generateZod(schema, { ir: schema, typesImport: "./t", sdkImport: "./s" });
  expect(output).toContain("ref: z.string()");
});

Copy link
Copy Markdown
Owner Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Good catch — added in c050a63. New regression test in zod.test.ts builds a collection with maxSelect: 0 relation/select/file fields and asserts they generate z.string() / z.enum([...]) (not z.array(...)), so a future regression in fieldToZod would now fail CI.

The Zod fixture only used maxSelect: 1, so the maxSelect: 0 path fixed in
this PR was unguarded. Add a targeted test asserting single
relation/select/file fields generate z.string()/z.enum(), not z.array().

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
@github-actions

github-actions Bot commented Jun 1, 2026

Copy link
Copy Markdown
Contributor

No new major issues found. The isMultipleField helper correctly centralizes the maxSelect > 1 rule, the fall-through in relation/file cases is clean, and both packages now have maxSelect: 0 regression tests.

@Karnak19
Karnak19 merged commit b6f1729 into main Jun 1, 2026
4 checks passed
@Karnak19
Karnak19 deleted the fix/single-ref-maxselect branch June 1, 2026 11:08
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

All reference fields are being typed as arrays, even when they are single references

1 participant