Skip to content

Migrate GraphQL layer from graphene to strawberry (query shapes pinned by golden SDL)#2139

Open
JSv4 wants to merge 30 commits into
mainfrom
claude/graphene-strawberry-migration-eyvj57
Open

Migrate GraphQL layer from graphene to strawberry (query shapes pinned by golden SDL)#2139
JSv4 wants to merge 30 commits into
mainfrom
claude/graphene-strawberry-migration-eyvj57

Conversation

@JSv4

@JSv4 JSv4 commented Jul 8, 2026

Copy link
Copy Markdown
Collaborator

Graphene → Strawberry migration

Status: migration in progress — resolver porting fan-out underway. Will be updated on this PR as it completes; do not merge yet.

What this does

Replaces the graphene / graphene-django GraphQL layer (schema types, resolvers, mutations) and the graphene-level auth middlewares with strawberry-graphql, with a machine-verified guarantee of zero query-shape changes:

  • Golden SDL contract: the full graphene SDL was captured at migration time as config/graphql/schema.graphql (10.6k lines). opencontractserver/tests/test_schema_parity.py structurally compares the served strawberry schema against it — every type, field, argument name/type, nullability wrapper, interface, enum member, and printed default must match exactly. The parity test is green.
  • Shared runtime (config/graphql/core/): relay global IDs + Node interface in graphene wire format (base64("TypeName:pk")), countable/PDF-page-aware connection factories, a faithful port of graphene-django's connection resolution (arrayconnection cursors, RELAY_CONNECTION_MAX_LIMIT=100, 1-based offsetafter conversion), django-filter FilterSet argument mapping incl. GlobalIDFilter decoding, GenericScalar/JSONString/BigInt scalars, permission-annotation resolvers, DRF-serializer mutation bases, auth decorators with graphql_jwt-compatible error messages.
  • Auth middlewares replaced: graphql_jwt.middleware.JSONWebTokenMiddleware + the API-key graphene middleware are gone. Per-request authentication happens once in config/graphql/views.py::GraphQLView.get_context via the standard AUTHENTICATION_BACKENDS chain (JWT / Auth0 / API-key / session). tokenAuth / verifyToken / refreshToken are strawberry-native ports preserving long-running refresh-token laziness.
  • Security hardening preserved: DepthLimitValidationRule + DisableIntrospection attach via strawberry's AddValidationRules (which appends to the full spec rule set — the graphene replace-the-rules trap is structurally impossible). The GCS file-URL pre-warm middleware became a strawberry SchemaExtension.
  • Tests keep their substantive cases: graphene.test.Client → drop-in config/graphql/testing.py::Client (same result dict shape); GraphQLTestCase ported for endpoint-level tests; schema.execute(...)schema.execute_sync(...).
  • graphene-django removed from requirements/INSTALLED_APPS; strawberry-graphql==0.320.3 added; django-graphql-jwt retained only as a JWT signing/backend utility (its graphene middleware/mutations are no longer used).

How the port was done

The strawberry schema skeleton (394 types, all field/argument shapes) was generated by introspecting the live graphene schema, guaranteeing shape parity by construction. Custom resolver bodies (465 stubs) are being ported verbatim module-by-module — roots are Django model instances in both frameworks, so bodies transfer nearly unchanged, keeping the service-layer permission patterns intact (the opencontracts.E001 architecture check stays green).

Verification

  • test_schema_parity — green (schema shape vs golden SDL)
  • test_og_metadata_queries — 18/18 green (first fully ported module)
  • test_token_expiration — 10/10 green (JWT auth path)
  • Full backend suite — pending completion of the porting fan-out

🤖 Generated with Claude Code

https://claude.ai/code/session_01SZxDJP7pKPSryHv7d5tiWm


Generated by Claude Code

Comment thread config/graphql/core/permissions.py Fixed
Comment thread config/graphql/core/permissions.py Fixed
Comment thread config/graphql/core/permissions.py Fixed
Comment thread config/graphql/core/relay.py Fixed
Comment thread config/graphql/action_queries.py Fixed
Comment thread config/graphql/action_queries.py Fixed
Comment thread config/graphql/action_queries.py Fixed
Comment thread config/graphql/action_queries.py Fixed
Comment thread config/graphql/action_queries.py Fixed
Comment thread config/graphql/action_queries.py Fixed
Comment thread config/graphql/action_queries.py Fixed
Comment thread config/graphql/action_queries.py Fixed
Comment thread config/graphql/agent_mutations.py Fixed
@codecov

codecov Bot commented Jul 8, 2026

Copy link
Copy Markdown

Codecov Report

✅ All modified and coverable lines are covered by tests.

📢 Thoughts on this report? Let us know!

@claude

claude Bot commented Jul 8, 2026

Copy link
Copy Markdown

Review: Graphene → Strawberry GraphQL migration

Reviewed via the local merge commit (dc214be...aec1b89), which let me diff the full 194-file / +35548/-25213 changeset directly rather than through gh pr diff (which 406s on this PR — it exceeds GitHub's 20k-line diff cap).

Overview
This replaces the graphene/graphene-django GraphQL layer with strawberry-graphql end-to-end: schema types, resolvers, mutations, auth middleware, and the permission-annotation mixin, backed by a golden-SDL parity test (config/graphql/schema.graphql + test_schema_parity.py) intended to guarantee zero query-shape drift. The PR is explicitly marked WIP ("do not merge yet"), and the review below is consistent with that — it surfaces exactly the kind of gaps you'd expect from an in-flight porting fan-out.

Strengths

  • Golden SDL contract is a genuinely good approach for a mechanical framework swap — pinning the schema shape structurally (types/fields/args/nullability/enums/defaults) is a much stronger guarantee than eyeballing a diff of this size.
  • config/graphql/views.py::authenticate_request is a clean, well-reasoned replacement for the old JWT + API-key graphene middlewares — single auth pass via AUTHENTICATION_BACKENDS, session precedence preserved, JWT errors re-surfaced as GraphQL-shaped errors in dispatch().
  • config/graphql/core/permissions.py is a careful, faithful port of AnnotatePermissionsForReadMixin. Worth calling out: resolve_object_shared_with explicitly documents and preserves a pre-existing graphene-era bug (the this_model_permission_id_map lookup in that path was always empty, so shared-with entries silently raised KeyError and got swallowed) rather than silently fixing it — the right call for a parity-focused migration, but it should become a tracked follow-up bug fix once this lands (the one test exercising this path is currently broken — see below).
  • CLAUDE.md and requirements/base.txt comments were updated in the same PR to reflect the new architecture, per the project's own docs convention.
  • changelog.d/graphene-strawberry-migration.changed.md fragment added per the changelog convention.

Confirmed test breakage (should block merge as-is)
Grepping the merged tree for imports of modules this PR deletes turned up real, currently-broken tests:

  • opencontractserver/tests/test_mention_permissions.py — ~35 local imports of from config.graphql.queries import Query across CorpusMentionPermissionTestCase, DocumentMentionPermissionTestCase, MentionIDORProtectionTestCase, CorpusScopedMentionSearchTestCase, AgentMentionCorpusScopingTestCase. config/graphql/queries.py is fully deleted by this PR. This file is untouched by the diff (0 changes) and is IDOR/permission-relevant.
  • opencontractserver/tests/test_document_path_migration.py (lines 479/491/508/526) — TestDocumentPathTypeCaching's 4 tests import from config.graphql.graphene_types import DocumentPathType; graphene_types.py is fully deleted. File is untouched by the diff.
  • opencontractserver/tests/test_user_privacy.py:483_resolve_shared_with() (used by test_shared_with_returns_slug_only and siblings) does from config.graphql.graphene_types import CorpusType and calls CorpusType.resolve_object_shared_with(...) as a classmethod. That symbol is gone; the equivalent logic moved to the free function config.graphql.core.permissions.resolve_object_shared_with(instance, info). Notably, this is the only regression test for the deliberately-preserved KeyError-swallowing quirk mentioned above, so that behavior is currently untested. (The rest of this file's graphene.test.Client imports were correctly updated to config.graphql.testing.Client; this one call site was missed.)
  • opencontractserver/tests/test_security_hardening.py:1902-1986TestDRFMutationValidationError and TestIOSettingsRequiredFieldsGuard import DRFMutation/DRFDeletion/_require_io_setting from config.graphql.base, which is fully deleted (346 lines removed, no visible core/ equivalent test). This is a security-hardening test file by name and purpose — worth prioritizing.
  • scripts/test-django-ratelimit.py:99-100 — manual script, lower priority, but from config.graphql.mutations import CreateLabelset / from config.graphql.queries import Query will also break.

All five are local (function-scope) imports, so pytest collects the files fine and only fails at the specific test methods — consistent with the PR's own "Full backend suite — pending completion of the porting fan-out" caveat. Recommend a final repo-wide sweep for references to config.graphql.base, graphene_types, queries, mutations, custom_connections, jwt_overrides, and permissioning.permission_annotator.mixins before dropping WIP status — a plain import grep won't catch call-site signature changes like the CorpusType classmethod → free-function move above.

Other observations

  • config/graphql/_port_manifest.json (2327 lines, all 465 stubs) hardcodes absolute paths from the migration tool's sandbox, e.g. "ref": "/home/user/oc-graphene-ref/config/graphql/research_types.py:52". That path won't resolve for anyone else and reads as leftover scaffolding from the automated porting run rather than durable project documentation. Consider dropping it before merge, or repointing ref at repo-relative paths if it's meant to stay as an audit trail.
  • The schema-parity test guarantees shape but says nothing about resolver behavior (pagination edge cases, permission-annotation caching, GenericScalar/JSONString/BigInt serialization could all differ subtly while still matching the SDL). Worth confirming the full backend suite passes, not just the three modules named green in the PR description, given ~465 hand/tool-ported resolver bodies across ~90 files.
  • Given the WIP status and that this is fundamentally an atomic framework swap (hard to land incrementally since the golden-SDL test needs the whole schema to match at once), the overall size is understandable — flagging only that a true line-by-line review of every resolver body wasn't practical here and should lean on the full test suite once green.

Suggested next steps before removing WIP status

  1. Fix the broken test files above (plus the manual script).
  2. Run the full backend suite (pytest -n auto --dist loadscope) and confirm green, not just the 3 modules named in the PR description.
  3. Decide on _port_manifest.json's fate (keep with cleaned-up refs, or drop).

claude added 9 commits July 8, 2026 03:05
…emantics

Node fields using relay.Node.Field resolved via DjangoObjectType.get_node
(type get_queryset + .get(pk)), NOT the permission-filtered OpenContractsNode
path. Types without a get_queryset (e.g. MessageType) resolved unfiltered by
pk with per-field visibility enforcement. The core default over-filtered,
breaking test_mentions.test_permission_enforcement_corpus. CorpusType keeps
its permission-aware custom get_node (ported OpenContractsNode).
…port, DRF deletion guard, mention resolver names
Comment thread config/graphql/action_queries.py Fixed
Comment thread config/graphql/core/ids.py Fixed
Comment thread config/graphql/core/relay.py Fixed
Comment thread config/graphql/core/relay.py Fixed
claude added 2 commits July 8, 2026 04:59
…Node; fix bulk-upload list fields; install get_node/get_queryset compat aliases
…graduated test files

The strawberry migration replaced the untyped graphene test base classes
(graphene_django GraphQLTestCase / graphene.test.Client) with typed
first-party equivalents in config/graphql/testing.py. Subclassing an
Any-typed base had made mypy skip those test bodies entirely, so 22 test
modules were only vacuously 'graduated' — their latent setUpTestData
class-attribute pattern, dynamic resolver-alias calls, and self.client
shadowing were hidden, not absent. Swapping in a concretely-typed base
surfaces them.

- Add per-module disable-error-code headers to the generated strawberry
  schema modules (name-defined/valid-type/arg-type) with documented
  rationale; fix the hand-written config/graphql/core/* modules so the
  entire config/graphql surface type-checks clean.
- Re-baseline the 22 pre-existing test modules in mypy.ini with a
  documented block (non-bug patterns, no test-logic changes).
- Fix the newly-authored test_schema_parity.py in place (cast the
  parallel served-type variable after the kind check) rather than
  baselining it, keeping it fully type-checked.
Comment thread config/graphql/core/relay.py Fixed
…linter job

Two CI failures on the migration branch, both regressions it introduced:

frontend-e2e ("Login + Navigate All Views"): createCorpus returned HTTP
500 in the live runserver, so the corpus-workflow / routing-round-trip /
threads-discussions specs failed at 'create corpus via UI'. Root cause:
opencontractserver/pipeline/embedders/test_embedder.py was deleted as
collateral in an earlier migration commit, but config/settings/test.py
still names it as DEFAULT_EMBEDDER. Corpus creation seeds a structural
Readme.CAML document whose post-save hook runs calculate_embedding_for_doc_text
eagerly (CELERY_TASK_ALWAYS_EAGER in test settings), which imports the
missing module and 500s. The pytest suite masks this because a
session-autouse conftest fixture disconnects the document post-save
signals; the e2e runserver has no such fixture. Restored the file from
main verbatim.

linter (pre-commit --all-files):
- mypy hook: its isolated env could not import strawberry.ext.mypy_plugin
  (added to mypy.ini) because additional_dependencies still listed the
  removed graphene-django. Swapped graphene-django==3.2.3 for
  strawberry-graphql==0.320.3.
- flake8/pyupgrade/black/isort: applied the repo's standard formatting to
  the generated strawberry schema modules and migration-touched test files
  (they had been committed without pre-commit). Schema-shape parity is
  unchanged (test_schema_parity still passes); the golden SDL only lost
  trailing whitespace on blank docstring lines, which parity ignores.
Comment thread config/graphql/action_queries.py Fixed
Comment thread config/graphql/agent_types.py Fixed
Comment thread config/graphql/authority_frontier_mutations.py Fixed
Comment thread config/graphql/authority_mapping_mutations.py Fixed
Comment thread config/graphql/authority_namespace_mutations.py Fixed
Comment thread config/graphql/badge_mutations.py Fixed
Comment thread config/graphql/base_types.py Fixed
@claude

claude Bot commented Jul 8, 2026

Copy link
Copy Markdown

Review

This is a huge, carefully-engineered migration (60k+ lines) with a genuinely strong verification story — the golden-SDL parity test, the deliberate preservation of graphene-django's get_node/get_queryset asymmetry, and the honest documentation of preserved quirks (e.g. the resolve_object_shared_with KeyError-swallowing behavior) all show real care. I reviewed it with several parallel deep-dives into the core runtime (config/graphql/core/), auth/JWT/schema wiring, and a sample of the largest ported resolver files, cross-checking findings against git diff dc214be..HEAD myself. Per the PR description this is explicitly WIP/"do not merge yet," so treat the following as the checklist for that point, not a blocker on today's state.

🔴 Critical — IDOR regression: 7 singular "by ID" query fields lost permission filtering

get_node_from_global_id (config/graphql/core/relay.py:239-269) resolves a type via its registered get_node hook, or — if none is registered — falls back to entry.model._default_manager.get_queryset().get(pk=_pk) (after applying get_queryset if one exists). That fallback is fully unfiltered when a type has neither hook.

Seven model-backed types are registered with no get_queryset/get_node at all, and each has a live top-level singular query field that forwards straight to this unfiltered path:

  • relationshipconfig/graphql/annotation_queries.py:1374 (RelationshipType, registered annotation_types.py:2014)
  • annotationLabelannotation_queries.py:~1469 (AnnotationLabelType, annotation_types.py:1445)
  • labelsetannotation_queries.py:~1556 (LabelSetType, annotation_types.py:1683)
  • chatMessageconfig/graphql/conversation_queries.py:432 (MessageType, conversation_types.py:1929)
  • fieldsetconfig/graphql/extract_queries.py:174 (FieldsetType, extract_types.py:1495)
  • columnextract_queries.py:241 (ColumnType, extract_types.py:1639)
  • datacellextract_queries.py:492 (DatacellType, extract_types.py:1889)

I verified three of these directly against the pre-migration graphene resolvers (git show dc214be:...):

  • old resolve_relationship called BaseService.filter_visible(Relationship, info.context.user, request=info.context).get(id=django_pk) — the port drops this entirely.
  • old resolve_chat_message was @login_required and called BaseService.get_or_none(ChatMessage, django_pk, info.context.user, request=info.context) — the port has neither the auth gate nor the permission check, so any unauthenticated caller can fetch arbitrary private conversation messages by forging base64("MessageType:<id>") (trivial, not a secret).
  • old resolve_datacell used BaseService.get_or_none(Datacell, django_pk, info.context.user, request=info.context) — same drop, leaking extraction results across corpora/documents the caller has no access to.

The corresponding plural resolvers a few lines below each (relationships, annotationLabels, labelsets, fieldsets, columns, datacells) still correctly call BaseService.filter_visible(...) — only the singular-by-ID path was missed in the port. This is the same bug class the PR already fixed for Corpus (commit 2a1bbf1, giving CorpusType a custom permission-aware get_node) — that audit just wasn't extended to these 7 types.

Suggested fix: give each of these types a get_node/get_queryset hook mirroring the old resolver (BaseService.get_or_none/filter_visible), the same pattern already used for DocumentType/AnnotationType (get_queryset) and ExtractType/AnalysisType/ConversationType (get_node). Worth a final grep for other model-backed register_type(...) calls with no hook + a live singular field before considering this done — I didn't exhaustively check every type (e.g. CorpusReferenceType, NoteRevisionType, ModerationActionType, AssignmentType, CorpusCategoryType, CorpusActionType are also registered without hooks; I didn't verify whether each has an exposed singular resolver or whether the underlying data is sensitive enough to matter).

🟠 Medium — unhandled AuthenticationFailed crashes the request on a bad API key

config/graphql/views.py:46 (authenticate_request) calls django.contrib.auth.authenticate(request=request) for any request carrying an Authorization header, which walks into ApiKeyBackend.authenticate() (config/graphql_api_token_auth/backends.py:66-105). That backend raises rest_framework.exceptions.AuthenticationFailed for a malformed/unknown/inactive-user API key — not a graphql_jwt exception. GraphQLView.dispatch (views.py:60-73) only catches graphql_jwt.exceptions.JSONWebTokenError, so AuthenticationFailed propagates unhandled out of the view.

Under graphene this same exception was raised inside ApiKeyTokenMiddleware.resolve, i.e. inside graphql-core's per-field resolution, so the execution engine caught it and returned a normal {"errors": [...]} 200 response. Now get_context() runs before query execution begins, outside that try/except, so the same input produces an unhandled 500 (or a DEBUG traceback) instead. Failure scenario: any GraphQL request sent with Authorization: Token <bad-key> when USE_API_KEY_AUTH=True. Not an auth bypass, but a real robustness regression, and no existing test (test_security_hardening.py, test_jwt_utils.py) exercises an invalid API key through the view. Suggest catching AuthenticationFailed alongside JSONWebTokenError in dispatch().

🟡 Worth a second look (pre-existing, not introduced by this PR)

  • GlobalIDFilter type confusion (config/graphql/core/filtering.py:90-102): decodes a relay global ID but discards the type-name half, so a global ID encoded for the wrong type (base64("AnnotationLabel:5")) filters by pk 5 under any filter regardless of the label. Confirmed this is a line-for-line port of graphene_django.filter.filters.GlobalIDFilter and predates this PR — not a regression, and the decoded value only ever reaches the ORM as a parameterized filter kwarg (no injection), but it's an inherited footgun on top of whatever visibility filtering wraps the queryset. Worth a follow-up issue.
  • resolve_object_shared_with (config/graphql/core/permissions.py:213-276): the docstring itself documents that this_model_permission_id_map is looked up empty, so any object with an actual per-user share raises KeyError inside the loop — caught only by except AttributeError. This is preserved faithfully from the graphene original (verified byte-for-byte against dc214be), so it's not a regression, but since it's now explicitly documented rather than silently inherited, it'd be a good time to file a follow-up to actually fix it (or confirm it's genuinely unreachable, e.g. if the map is always populated by a wrapping call before this runs).

Everything else checked out clean

  • corpus_types.py's custom get_node (the earlier IDOR fix) is correct and intentionally not wired to the cached BaseService path for the top-level corpus(id:) query, per a documented incident with stale cached objects across permission mutations mid-request.
  • CorpusType.documents resolver correctly uses CorpusDocumentService.get_corpus_documents_visible_to_user (the MIN-permission variant) per the CLAUDE.md Reconcile CorpusObjsService corpus-as-gate vs GraphQL MIN-permission semantic #1682 invariant — no regression there.
  • annotation_mutations.py / extract_mutations.py: ownership/hybrid permission checks, require_permission's inverted-truthiness convention, and structural-annotation read-only enforcement are all intact.
  • opencontractserver/tests/architecture/test_graphql_service_layer.py and the opencontracts.E001 check (opencontractserver/shared/checks.py) have zero diff — the inline-Tier-0 enforcement mechanism wasn't weakened, and no new inline visible_to_user/user_can composition was found in config/graphql/'s added lines.
  • Depth-limit + introspection-disable wiring via AddValidationRules genuinely appends to (rather than replaces) the spec rule set, avoiding the graphene "replace-the-rules" trap the migration explicitly set out to fix; config/graphql/security.py itself is byte-identical to before.
  • core/scalars.py has no unsafe deserialization (no eval/pickle); core/mutations.py's DRF permission gating and generic-error-on-exception behavior matches the old DRFMutation/DRFDeletion exactly.
  • schema.py's schema.graphql_schema = schema._schema reaches into a strawberry-private attribute for backwards compatibility — documented and low-risk, but worth a comment/upstream-issue if strawberry's internal layout changes across versions.

Test coverage / CI

The PR description is upfront that the full backend suite is still pending completion of the porting fan-out, and that matches what I see in gh pr checks: linter, the e2e job (Login + Navigate All Views), and redis-integration were still in progress at review time, and I don't see a Django/pytest backend-suite check in the status rollup at all yet. Given the IDOR finding above, I'd flag opencontractserver/tests/test_add_annotation_idor.py (already touched by this PR) as a template — extending IDOR-style coverage to relationship/chatMessage/datacell/fieldset/column/annotationLabel/labelset singular queries would have caught this mechanically, and would be good regression insurance before merge.

The migration ported the singular "fetch one object by global Relay ID"
query fields to config/graphql/core/relay.py::get_node_from_global_id.
That helper falls back to an UNFILTERED model._default_manager.get(pk=)
when the target type has no get_node/get_queryset hook, so any caller
(anonymous, for chatMessage) could fetch private rows by forging
base64("<Type>:<id>"). Thirteen model-backed types had lost the
permission filtering their graphene resolvers performed.

Registered a permission-aware get_node hook on each, mirroring the
graphene resolver (BaseService.get_or_none / filter_visible / the owning
service): RelationshipType, AnnotationLabelType, LabelSetType, MessageType,
FieldsetType, ColumnType, DatacellType, AnalyzerType, GremlinEngineType_READ,
AgentConfigurationType, BadgeType, UserExportType, UserImportType, and
AssignmentType (keeps its deprecated superuser-or-participant gate).
moderationAction was already safe (custom gated resolver, not the fallback).

Also: GraphQLView.dispatch now catches DRF AuthenticationFailed (raised by
ApiKeyBackend for a bad API key when USE_API_KEY_AUTH=True) and returns a
200 GraphQL error, matching the graphene middleware instead of an unhandled
500 (auth now runs in get_context, outside query execution's try/except).

Added opencontractserver/tests/test_singular_node_idor.py: a structural
guard that asserts every type resolved via get_node_from_global_id carries
a permission hook (prevents recurrence), plus a behavioral non-owner-denied
check. All 12 config/graphql surface checks stay green (schema parity, mypy,
flake8, E001 service-layer).

JSv4 commented Jul 8, 2026

Copy link
Copy Markdown
Collaborator Author

Thanks — the 🔴 IDOR finding was spot-on and is now fixed in 99202ae, and I extended the audit as you suggested.

Method: I grepped every get_node_from_global_id(info, id, only_type_name="X") call site and cross-checked each target X against its register_type hooks. That surfaced the 7 you flagged plus 7 more the same bug affected:

  • Your 7: relationship, annotationLabel, labelset, chatMessage, fieldset, column, datacell
  • Also fixed: analyzer, gremlinEngine (its only_type_name is GremlinEngineType_READ — the underscore made it easy to miss), agent (AgentConfigurationType), badge, userexport, userimport, and assignment

Each now registers a get_node hook mirroring the graphene resolver — BaseService.get_or_none / filter_visible, AgentConfigurationService.get_agent_by_id for agent, and assignment keeps its deprecated superuser-or-participant gate. Of the types you named as unverified: moderationAction was already safe (custom gated resolver via conversation.can_moderate, not the fallback path); corpusReference/noteRevision/corpusCategory/corpusAction aren't exposed as singular by-ID queries.

🟠 medium: also fixed — GraphQLView.dispatch now catches DRF AuthenticationFailed alongside JSONWebTokenError, so a bad API key returns a 200 GraphQL error instead of an unhandled 500.

Regression insurance: added test_singular_node_idor.py — a structural guard that fails if any type resolved via get_node_from_global_id lacks a permission hook (so this class can't silently return), plus a behavioral non-owner-denied check.

The two 🟡 pre-existing items (GlobalIDFilter type-name discard, resolve_object_shared_with KeyError) are faithful ports and out of scope for this migration — leaving them as-is for a separate follow-up as you suggested.


Generated by Claude Code

Comment on lines +472 to +474
connection_type=lambda edges, pageInfo: ConnectionValue( # type: ignore[arg-type]
edges, pageInfo
),
JSv4 added 2 commits July 9, 2026 03:13
…ing test-pollution bug

- Restore three per-field relay max_limit overrides the port silently
  dropped (documentRelationships, annotations, extracts), each falling
  back to the global 100-record cap instead of their higher, deliberate
  ceilings.
- Re-expose DocumentType._assert_user_can_read as a class staticmethod
  so the existing ported logic is reachable via the graphene-era
  bound-method call convention tests rely on.
- Fix test_file_url_prewarm.py to reference the correctly-renamed
  FileUrlPrewarmExtension instead of the deleted middleware class.
- Fix a pre-existing, migration-unrelated bug in test_mentions.py: the
  conversation fixture used the wrong-case "THREAD" string instead of
  the ConversationTypeChoices.THREAD enum value ("thread"), which
  silently persisted (Django doesn't validate choices on save) and
  broke non-creator message visibility.
- Fix test_pipeline_component_queries.py permanently deleting the
  shared opencontractserver/pipeline/embedders/test_embedder.py
  (the suite's default embedder) in tearDownClass instead of
  restoring it, which cascaded into ~90 unrelated failures in any
  full-suite run that reached this test class.

Full backend suite now green (0 failed, 0 errors) under both
docker compose -f test.yml and -f local.yml.
…oms corpora; fix bulk-ingest ext gate and relationship-embedding bottleneck

- opencontractserver/enrichment/services/customs_ruling_citation_service.py: detects HTS tariff
  codes (plain annotations) and CBP ruling-number citations (CorpusReference rows resolved
  against sibling document titles) from each document's own post-parse text, reusing
  EnrichmentWriter for persistence. Runnable via manage.py enrich_customs_rulings.
  Validated at 100% precision/recall against a 96-doc pilot vs. the source dataset's own
  golden-tested extraction.
- ingest_corpus management command: ext_ok now unions get_convertible_extensions() so files
  eligible for the configured file converter (e.g. Gotenberg -> .doc) are actually accepted,
  matching the command's own docstring.
- embeddings_task.py: calculate_embeddings_for_relationship_batch's explicit-embedder path now
  batches through embed_texts_batch() (shared _batch_embed_items helper, also used by the
  annotation path) instead of one HTTP call per relationship -- a real bottleneck for parsers
  that emit many relationships per document (e.g. Warp-Ingest's heading-hierarchy
  OC_SUBTREE_GROUP rows).
- celery worker start script: ignore .pilot_data so watchfiles doesn't choke on large
  locally-materialized ingest batches.
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.

2 participants