feat(cms): gateway sync with Strapi v5 relation clearing#508
Merged
feat(cms): gateway sync with Strapi v5 relation clearing#508
Conversation
…nd videos Add content type schemas for Language, Country, Continent, CountryLanguage, BibleBook, VideoOrigin, VideoEdition, MuxVideo, CloudflareR2, Keyword, VideoVariant, VideoSubtitle, plus components for AudioPreview, BibleCitation, CloudflareImage, and VariantDownload. Extend existing Video content type with gateway fields (i18n, label enum, variants, subtitles, etc.) while preserving existing title/slug/image fields. Implement sync pipeline: - Gateway GraphQL client with timeout, retry, and exponential backoff - Language sync with dynamic Strapi i18n locale registration - Country sync with continent deduplication and country-language junctions - Video sync with limit/offset pagination (default page size 50) - Upsert-by-gatewayId strategy with source tagging (gateway/manager) - Soft-delete for removed records with circuit-breaker protection - In-memory concurrency lock prevents overlapping syncs - Configurable cron schedule via GATEWAY_SYNC_CRON env var - Manual trigger via POST /api/gateway-sync/trigger - Status endpoint via GET /api/gateway-sync/status Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
…el gateway errors
- Use { connect: [{ documentId }] } format for manyToOne relations (Strapi v5 requirement)
- Use { set: [...] } for manyToMany keyword relations
- Catch page-level gateway errors in video pagination and stop gracefully instead of aborting
- Make gateway URL configurable via GATEWAY_SYNC_URL env var (default: root path /)
- Fix controller to use global strapi instance
Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
…odel - Remove `primary: true` from title/description/snippet/studyQuestions/imageAlt queries to fetch all translations (not just primary) - Remove `aspectRatio: banner` from images query to fetch all images - Remove `parents` from query (children already shows the relationship) - Remove `childrenCount` and `availableLanguages` from Video schema (derived from children array and variants respectively) - Reduce DEFAULT_PAGE_SIZE from 50 to 10 to avoid gateway lag - Add VideoStudyQuestion as a proper collection type with i18n, order field, and manyToOne relation to Video (replaces JSON array) - Add aspectRatio, url, blurhash to images component data Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
…video sync query Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
…(requires auth) Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
Add oneToMany reverse relations so all content types can be navigated upward from child to parent: - VideoEdition: variants, subtitles - VideoOrigin: videos - MuxVideo: variants - CloudflareR2: variantAssets, vttSubtitles, srtSubtitles - Language: videoVariants, videoSubtitles, keywords, countryLanguages, videosAsPrimaryLanguage - Continent: countries - Country: countryLanguages Add inversedBy on corresponding manyToOne relations in VideoVariant, VideoSubtitle, CountryLanguage, Keyword, Video, and Country schemas. Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
…ook details - Convert BibleCitation from component to collection type with gatewayId, source tagging, and manyToOne relations to BibleBook and Video - Add oneToMany reverse relation on BibleBook (citations) so videos can be found by bible book - Add BibleBooks query to video sync to fetch full details (name, osisId, alternateName, paratextAbbreviation, isNewTestament, order) - Sync bible books as a first pass before video pagination - Remove old video.bible-citation component (replaced by collection type) Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
- Remove old `image` media field (leftover from pre-gateway schema) - Add `publishedAt` to video sync data (was queried but not mapped) - Add `origin` to gateway query and sync VideoOrigin records per video - All Video schema fields now correspond to data actually pulled from the gateway Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
The primaryLanguage relation already points to the Language record. The gateway ID is still used internally to resolve the relation during sync but is no longer stored as a separate field. Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
…rror Strapi throws 'Cannot use in operator to search for id in undefined' when a component field is explicitly set to undefined. Use spread to only include audioPreview when it exists. Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
…nect
Strapi v5 document service accepts { documentId } directly for manyToOne
relations. The { connect: [{ documentId }] } syntax was causing
"relation does not exist" errors when the related record was just created,
particularly for cross-i18n relationships (e.g. non-i18n CountryLanguage
referencing i18n Country).
Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
Strapi v5 document service accepts bare documentId strings for
manyToOne relation fields (not objects). The { documentId } object
format was causing "relation does not exist" errors, particularly
for cross-i18n relationships.
Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
When a non-i18n type (CountryLanguage) references an i18n type (Country,
Language), Strapi v5 needs explicit locale on the relation object for
reliable resolution. Use { documentId, locale: "en" } format for all
relations targeting i18n content types.
Based on Strapi v5 source analysis of transform/relations/utils/i18n.mjs
and transform/relations/transform/default-locale.mjs.
Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
…c logs Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
- Add @graphql-codegen/cli with typescript and typescript-operations plugins - Extract all sync queries into .graphql files under queries/ - Generate typed query results from gateway introspection - Replace console.log with strapi logger in gateway client - Add pnpm codegen script Key type differences found vs manual types: - videoEdition is non-nullable on subtitles and variants (was nullable) - label, source, quality, aspectRatio are enums (were strings) Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
- All queries tagged with /* GraphQL */ and named for codegen discovery - Codegen now scans .ts files directly (documents: "**/*.ts") - Replace all manual types with generated ones: - GatewayVideo = SyncVideosQuery["videos"][number] - GatewayLanguage = SyncLanguagesQuery["languages"][number] - GatewayCountry = SyncCountriesQuery["countries"][number] - GatewayVariant = SyncVideoVariantsQuery["videoVariants"][number] - GatewayKeyword = SyncKeywordsQuery["keywords"][number] - Delete .graphql files (queries live inline in TS, single source of truth) - Run pnpm codegen to regenerate after query changes Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
…nt preset codegen - Replace hand-rolled fetch client with Apollo Client (no-cache, error handling) - Codegen generates types from inline /* GraphQL */ tagged queries in .ts files - Types in gql/gateway-types.ts, derived via SyncXQuery["field"][number] pattern - Delete .graphql files and old generated/ directory - All sync files use generated types, single source of truth Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
- Use @graphql-codegen/client-preset for fully typed query documents
- All queries use graphql() from generated gql/ — no manual type passing
- Apollo Client used directly via getGatewayClient().query({ query })
- Types derived via ResultOf<typeof QUERY>["field"][number]
- Schema sourced from JesusFilm/core main branch with handleAsSDL: true
- Deduped graphql package versions to fix schemaExtensions error
Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
Pre-filter new locales before loop, log progress every 100 instead of every single locale. Reduces log noise and slightly improves speed. Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
Use direct DB insert only — the plugin create() does validation and lifecycle hooks that add ~500ms per locale. With 2000+ locales this adds 15+ minutes. Direct DB insert is ~10x faster. Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
Insert locales in batches of 500 via raw knex instead of one-at-a-time through Strapi ORM. Should reduce 20+ minutes to seconds. Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
When syncing video variants and subtitles, unresolvable manyToOne
relations (videoEdition, muxVideo) were passed as `undefined`, which
in Strapi v5 means "don't touch" — preserving stale references to
deleted/unpublished editions. This caused publish-time validation
failures. Using `null` instead triggered an internal Strapi v5 bug.
Fix: use `{ set: [] }` (Strapi's relation clearing API) to explicitly
clear unresolvable relations. Also removes the test-relation debug
endpoint and improves formatError to include stack traces.
Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
Add centralized `clearableRelation()` helper to strapi-helpers.ts that
returns `{ set: [] }` for undefined docIds — applied to all 10 optional
relation fields across sync-videos, sync-video-variants, sync-countries,
and sync-keywords. Also gates formatError stack traces behind
NODE_ENV !== production and replaces all inline error formatting with
the formatError helper.
Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
Remove unused type aliases (GatewayCountry, GatewayKeyword) and their imports, drop unused catch variable in sync-languages batch fallback, and regenerate packages/graphql graphql-env.d.ts to match updated CMS schema. Also updates plan with completed phase checkboxes. Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
Video.image (Strapi media) was replaced by Video.images (repeatable CloudflareImage component). Update all GraphQL fragments and component code to use images[0].url instead of image.url. Remove the unused raw-parse watchExperience query from packages/graphql — web uses its own typed version and mobile has a local copy. Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment
Add this suggestion to a batch that can be applied as a single commit.This suggestion is invalid because no changes were made to the code.Suggestions cannot be applied while the pull request is closed.Suggestions cannot be applied while viewing a subset of changes.Only one suggestion per line can be applied in a batch.Add this suggestion to a batch that can be applied as a single commit.Applying suggestions on deleted lines is not supported.You must change the existing code in this line in order to create a valid suggestion.Outdated suggestions cannot be applied.This suggestion has been applied or marked resolved.Suggestions cannot be applied from pending reviews.Suggestions cannot be applied on multi-line comments.Suggestions cannot be applied while the pull request is queued to merge.Suggestion cannot be applied right now. Please check back later.
Summary
{ set: [] }(Strapi v5 relation clearing API) instead ofundefined(preserves broken refs) ornull(triggers Strapi bug)clearableRelation()helper in strapi-helpers.ts applied consistently across all 10 optional relation fields in 4 sync servicesformatErrorstack traces behindNODE_ENV !== productionKey decisions
{ set: [] }for relation clearing: In Strapi v5,undefinedmeans "don't touch" (preserves stale refs),nulltriggers an internal bug. Only{ set: [] }properly clears relations. Documented indocs/solutions/integration-issues/strapi-v5-manytone-relation-clearing.md.graphql()typed documents for type-safe gateway API calls.Test plan
tsc --noEmit)🤖 Generated with Claude Code