Skip to content

feat(core): Auto-initialise customFields for entities that support them#4965

Open
grolmus wants to merge 3 commits into
vendurehq:minorfrom
grolmus:mgrolmus/oss-408-auto-initialise-configcustomfields-for-plugin-entities
Open

feat(core): Auto-initialise customFields for entities that support them#4965
grolmus wants to merge 3 commits into
vendurehq:minorfrom
grolmus:mgrolmus/oss-408-auto-initialise-configcustomfields-for-plugin-entities

Conversation

@grolmus

@grolmus grolmus commented Jul 14, 2026

Copy link
Copy Markdown
Collaborator

Summary

Auto-initialises an empty customFields array for every entity that supports custom fields, so a plugin's configuration callback can extend any such entity (core or plugin-defined) without the defensive guard:

// Before — guard required, or startup crashes with "Cannot read properties of undefined"
configuration: config => {
    if (!config.customFields.CompanyRole) (config.customFields as any).CompanyRole = [];
    config.customFields.CompanyRole.push({ name: 'spendingLimit', type: 'int' });
}

// After — just works
configuration: config => {
    config.customFields.CompanyRole.push({ name: 'spendingLimit', type: 'int' });
}

Resolves OSS-408.

How

  • New getEntityNamesWithCustomFields() (register-custom-entity-fields.ts) detects custom-field-capable entities from the TypeORM metadata (they declare a customFields embedded column) — a HasCustomFields implements check isn't available at runtime.
  • runPluginConfigurations() (bootstrap.ts) pre-seeds config.customFields[EntityName] = [] for each of those before the plugin configuration callbacks run, only where an entry doesn't already exist (never overwrites configured fields).

Empty arrays are ignored by registerCustomEntityFields (it already skips zero-length configs), so this is inert for entities nobody extends — no schema/column changes.

The TypeScript half of the original issue (the as any footgun) is already resolved on main: CustomFields now carries a & { [entity: string]: CustomFieldConfig[] } index signature, so config.customFields.CompanyRole is typed without a cast. This PR adds the matching runtime guarantee.

Tests

bootstrap.spec.ts — custom-field-capable entities get [] initialised; existing entries are preserved; and a @VendurePlugin({ configuration }) can push to another entity's custom fields without a guard. Source typechecks clean; related entity specs pass.

Relates to OSS-408


View with Codesmith Autofix with Codesmith
Need help on this PR? Tag /codesmith with what you need. Autofix is disabled.

@vercel

vercel Bot commented Jul 14, 2026

Copy link
Copy Markdown

The latest updates on your projects. Learn more about Vercel for GitHub.

Project Deployment Actions Updated (UTC)
vendure-storybook Ready Ready Preview, Comment Jul 15, 2026 11:18am

Request Review

@coderabbitai

coderabbitai Bot commented Jul 14, 2026

Copy link
Copy Markdown
Contributor

Important

Review skipped

Auto reviews are disabled on base/target branches other than the default branch.

Please check the settings in the CodeRabbit UI or the .coderabbit.yaml file in this repository. To trigger a single review, invoke the @coderabbitai review command.

⚙️ Run configuration

Configuration used: Path: .coderabbit.yaml

Review profile: CHILL

Plan: Pro

Run ID: f625de63-037e-40a3-9bc4-7464db8d3766

You can disable this status message by setting the reviews.review_status to false in the CodeRabbit configuration file.

Use the checkbox below for a quick retry:

  • 🔍 Trigger review
✨ Finishing Touches
🧪 Generate unit tests (beta)
  • Create PR with unit tests

Thanks for using CodeRabbit! It's free for OSS, and your support helps us grow. If you like it, consider giving us a shout-out.

❤️ Share

Comment @coderabbitai help to get the list of available commands.

…o-initialise-configcustomfields-for-plugin-entities
@grolmus

grolmus commented Jul 15, 2026

Copy link
Copy Markdown
Collaborator Author

CI fix: Field "CreateCollectionTranslationInput.customFields" can only be defined once

After bringing this branch up to date with the latest minor, CI turned red across build, codegen, unit tests and all e2e jobs with:

Error: Field "CreateCollectionTranslationInput.customFields" can only be defined once.

Root cause

This branch auto-initialises config.customFields[EntityName] = [] for every entity that declares a customFields embedded property, detected from the TypeORM metadata via getEntityNamesWithCustomFields().

In minor, the translation entities (CollectionTranslation, ProductTranslation, …, and the new ApiKeyTranslation) now also declare a customFields embedded — that's how localized custom-field values are stored. As a result getEntityNamesWithCustomFields() started returning translation entity names too, and the bootstrap loop began seeding config.customFields.CollectionTranslation = [], and so on.

Translation entities are never valid config.customFields keys — localized custom fields are declared on the base entity (e.g. Collection, with localized: true) and the framework fans them out to the translation table itself. Seeding a translation key makes the GraphQL schema builder (graphql-custom-fields.ts) emit the customFields field on the translation input types twice:

  • from the spurious CollectionTranslation key — the empty-array branch adds customFields: JSON to CreateCollectionTranslationInput unconditionally whenever that input type exists;
  • from the base Collection entity's localized fields — which legitimately add customFields to Create/UpdateCollectionTranslationInput.

Two definitions of the same field → the SDL error, which then cascades into every downstream job.

Worth noting: empty arrays are correctly ignored on the DB side (registerCustomEntityFields has a .length guard), which is why the original comment assumed the empty seed was inert — but the GraphQL builder has no equivalent guard.

Fix

getEntityNamesWithCustomFields() now excludes translation entities, detecting them as the target of a translations relation in the TypeORM metadata — the same signal registerCustomEntityFields already uses to locate the translation type, and a hard framework invariant (Translatable.translations). This removes exactly the spurious translation keys and nothing else; plugin-defined entities (the intended feature) are unaffected, and it doesn't rely on a *Translation name convention.

Verification

  • New regression test in bootstrap.spec.ts asserting translation entities are not auto-initialised (fails before the fix, passes after).
  • bootstrap.spec.ts (4) and graphql-custom-fields.spec.ts (15) green; core typecheck clean.
  • Full schema build / e2e confirmed by CI on this push.

@grolmus grolmus requested a review from michaelbromley July 15, 2026 12:10
@michaelbromley michaelbromley added the T3: Systemic Involves a systemic decision. Decide before implementing. label Jul 16, 2026
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

T3: Systemic Involves a systemic decision. Decide before implementing.

Projects

None yet

Development

Successfully merging this pull request may close these issues.

2 participants