Skip to content

feat: add get_or_create option to foreign_key_extras (#128) - #156

Open
trmdy wants to merge 1 commit into
mainfrom
feat/issue-128
Open

feat: add get_or_create option to foreign_key_extras (#128)#156
trmdy wants to merge 1 commit into
mainfrom
feat/issue-128

Conversation

@trmdy

@trmdy trmdy commented Jun 10, 2026

Copy link
Copy Markdown
Owner

Implements #128 (the get-or-create use case from #119).

Motivation

foreign_key_extras with a nested input type always creates a new related
object. Sometimes you want to reuse an existing one when it already exists —
the canonical example from #119 is a car's number plate: reuse the stored plate
if present, otherwise create it. Today that's impossible, and attempting it hits
a unique-constraint error.

What's new

A get_or_create option on a foreign_key_extras entry:

class CreateCatMutation(DjangoCreateMutation):
    class Meta:
        model = Cat
        foreign_key_extras = {
            "owner": {"type": "CreateUserInput", "get_or_create": ["username"]}
        }
  • get_or_create: [field, ...] — match an existing related object on those fields.
  • get_or_create: True — match on every concrete field present in the input.

If a match is found it is reused; otherwise a new related object is created
exactly as before. Lookup values for foreign-key fields are resolved via
resolve_id, and enum values are unwrapped. The matching logic lives in a new
DjangoCudBase.get_foreign_key_lookup classmethod (overridable).

Compatibility

Purely additive and opt-in — without get_or_create the behaviour is unchanged.
No input-type / schema changes.

Tests

TestCreateMutationForeignKeyGetOrCreate:

  • list form: reuses an existing match, and creates a new object when none matches
    (both branches fail on main with UNIQUE constraint errors),
  • True form: reuses when all provided fields match.

Full suite: 112 passed (poetry run pytest). Docs (nested-fields.rst) and
CHANGELOG.md updated.

🤖 Generated with Claude Code


Note

Low Risk
Opt-in mutation behavior with no schema changes; default nested FK creation is unchanged when get_or_create is omitted.

Overview
Adds an opt-in get_or_create flag on foreign_key_extras so nested related objects can be reused when a DB row already matches the input, instead of always calling create_obj (which can hit unique constraints).

In DjangoCudBase.get_or_create_foreign_obj, when the extra uses a nested input type and get_or_create is set, the mutation builds a lookup via the new overridable get_foreign_key_lookup (field list or True for all concrete fields in the input), runs filter(...).first(), and returns that PK if found; otherwise behavior is unchanged. Lookups resolve FK IDs and unwrap enums.

Docs (nested-fields.rst), CHANGELOG.md, and TestCreateMutationForeignKeyGetOrCreate cover list vs True matching and create-vs-reuse paths.

Reviewed by Cursor Bugbot for commit cea72e2. Bugbot is set up for automated code reviews on this repo. Configure here.

Foreign keys declared in foreign_key_extras always created a brand-new related
object. This adds a get_or_create option that first looks for an existing
object matching the supplied input and reuses it when found, only creating a
new object otherwise.

get_or_create accepts either a list of field names to match on, or True to
match on every concrete field present in the input. Foreign-key lookup values
are resolved via resolve_id and enum values are unwrapped. The matching logic
lives in the new DjangoCudBase.get_foreign_key_lookup helper.

Implements the get-or-create use case from #119.

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>

@chatgpt-codex-connector chatgpt-codex-connector Bot left a comment

Copy link
Copy Markdown

Choose a reason for hiding this comment

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

💡 Codex Review

Here are some automated review suggestions for this pull request.

Reviewed commit: cea72e29a0

ℹ️ About Codex in GitHub

Your team has set up Codex to review pull requests in this repo. Reviews are triggered when you

  • Open a pull request for review
  • Mark a draft as ready
  • Comment "@codex review".

If Codex has suggestions, it will comment; otherwise it will react with 👍.

Codex can also answer questions or update the PR. Try commenting "@codex address that feedback".

Comment on lines +80 to +85
for name in field_names:
related_field = get_model_field_or_none(name, related_model)
if related_field is None or not getattr(related_field, "concrete", False):
continue

lookup_value = value.get(name)

Copy link
Copy Markdown

Choose a reason for hiding this comment

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

P2 Badge Honor field-name mappings when building lookups

When the related input type uses field_name_mappings, this lookup is built against unmapped names: get_or_create=True iterates the GraphQL/input keys, which are then rejected as non-model fields, while a configured list like ["email"] reads value.get("email") even though the input only contains the mapped key. In that scenario the lookup is empty or missing the unique field, so an existing related object is not reused and the mutation falls through to creating a duplicate (often hitting the same unique-constraint failure this option is meant to avoid). The lookup needs the input type's reverse field-name mapping before resolving fields/values.

Useful? React with 👍 / 👎.

Comment on lines +43 to +45
if get_or_create:
lookup = cls.get_foreign_key_lookup(field.related_model, value, get_or_create)
existing_obj = field.related_model.objects.filter(**lookup).first() if lookup else None

Copy link
Copy Markdown

Choose a reason for hiding this comment

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

P2 Badge Include context fields in get-or-create lookups

When the nested input type relies on auto_context_fields for part of its identity, the new lookup ignores those context-derived values even though the create path immediately below passes them into create_obj. For example, with a related model unique on (owner, tag) where owner comes from info.context, get_or_create: ["owner", "tag"] skips owner because it is not in the submitted input and may reuse another owner's row with the same tag (or miss the correct row and create a duplicate). The lookup should incorporate the nested type's auto context fields before filtering.

Useful? React with 👍 / 👎.

Comment on lines +91 to +94
elif isinstance(lookup_value, enum.Enum):
lookup[name] = lookup_value.value
else:
lookup[name] = lookup_value

Copy link
Copy Markdown

Choose a reason for hiding this comment

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

P2 Badge Apply custom handlers before lookup values

When a nested related field included in get_or_create is normalized by a handle_<field> method, the lookup uses the raw GraphQL value while the create path later applies that handler in create_obj. In that configuration, an existing row stored with the normalized value is missed and the mutation falls through to creating a second row (or raising a unique-constraint error), so lookup values should go through the same per-field handler path as creation.

Useful? React with 👍 / 👎.

@cursor cursor Bot left a comment

Copy link
Copy Markdown

Choose a reason for hiding this comment

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

Cursor Bugbot has reviewed your changes and found 1 potential issue.

Fix All in Cursor

❌ Bugbot Autofix is OFF. To automatically fix reported issues with cloud agents, enable autofix in the Cursor dashboard.

Reviewed by Cursor Bugbot for commit cea72e2. Configure here.

elif isinstance(lookup_value, enum.Enum):
lookup[name] = lookup_value.value
else:
lookup[name] = lookup_value

Copy link
Copy Markdown

Choose a reason for hiding this comment

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

Lookup ignores input field mappings

Medium Severity

get_foreign_key_lookup reads nested input with model field names and raw value keys, but nested create inputs use field_name_mappings (e.g. keeperkeeper_id). create_obj already reverses those names; the lookup path does not, so filters often stay empty and get_or_create always creates instead of reusing matches.

Fix in Cursor Fix in Web

Reviewed by Cursor Bugbot for commit cea72e2. Configure here.

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.

1 participant