Skip to content

Instantly share code, notes, and snippets.

@gautam-divyanshu
Last active October 26, 2025 13:34
Show Gist options
  • Select an option

  • Save gautam-divyanshu/91ff226c8157c990f8226cbc524525bf to your computer and use it in GitHub Desktop.

Select an option

Save gautam-divyanshu/91ff226c8157c990f8226cbc524525bf to your computer and use it in GitHub Desktop.
GSOC 2025 Project Report | Implementing Efficient Recurring Events and Solidifying Core Features | Divyanshu Gautam

GSoC 2025 Final Project Report

image

📋 Project Summary

This Google Summer of Code 2025 project significantly enhanced the Talawa platform's event management system by implementing comprehensive support for recurring events, action items, and volunteer coordination. The work focused on both backend (talawa-api) and frontend (talawa-admin) components, introducing advanced features for event scheduling, task management, and community engagement.

🔗 Related Pull Requests

Events

Event Action Items

Event Volunteers and Volunteer Groups

Event attendance

🚀 Features Implemented

📅 Events in Talawa: How they work

  • There are two kinds of events:
    • Standalone events: single occurrence stored directly in events.
    • Recurring events: defined by a template in events + a recurrence rule in recurrence_rules, with materialized child instances in recurring_event_instances.
  • Users never edit the template rows directly. They work with instances or with high-level actions (entire series, this and following, this instance only).
  • A background worker pre-creates instances within a 12‑month "hot window" and keeps cleaning old ones. Cron runs hourly (generation) and daily (cleanup).
  • Updates support three scopes: this instance only, entire series (name/description), and this and following (split the series).
  • Deletes support three scopes: this instance only, this and following, and entire series.

🗄️ Data model at a glance

Tables in talawa-api (Drizzle ORM):

  • events

    • Stores both standalone events and recurring templates.
    • is_recurring_template (aka isRecurringEventTemplate) tells if a row is a recurring template.
    • Standalone events: is_recurring_template = false and their own start_at/end_at are final.
    • Recurring templates: is_recurring_template = true and act as the parent/defaults for instances.
  • recurrence_rules

    • One per recurring template.
    • Holds the RRULE-like data (frequency, interval, byDay, byMonth, byMonthDay, recurrenceStartDate, optional recurrenceEndDate or count).
    • base_recurring_event_id links back to the template in events.
    • original_series_id ties together all splits of the same logical series across time.
  • recurring_event_instances

    • Materialized occurrences for recurring series inside the hot window.
    • Key fields:
      • base_recurring_event_id → template
      • recurrence_rule_id → source rule
      • original_series_id → logical series identity across splits
      • original_instance_start_time → when the RRULE said it should start
      • actual_start_time/actual_end_time → after applying exceptions and duration
      • sequence_number, total_count (if finite), is_cancelled
  • event_exceptions

    • Stores per-instance overrides when a user edits a single occurrence (e.g., rename just one date, shift time, etc.).
    • Exception JSON holds only the fields that differ from the template.
  • event_generation_windows

    • Per-organization config for the hot window and retention:
      • hot_window_months_ahead default 12 (1 year forward)
      • history_retention_months default 3 (cleanup window)
      • Tracks window boundaries and processing metadata.

🔍 How templates, instances, and standalone events differ

  • Standalone event

    • Single row in events, shown directly in calendars and lists.
    • Users edit/delete the event itself.
  • Recurring template (parent)

    • Row in events with is_recurring_template = true.
    • Paired with a recurrence_rules row.
    • Not directly shown or edited by users as an event occurrence.
  • Recurring instance (child)

    • Row in recurring_event_instances created by the background worker.
    • Inherits default fields (name, description, location, visibility, registerable, etc.) from the template.
    • May have a matching event_exceptions row to override specific fields for that one occurrence.
    • What users usually see and interact with on the calendar.

⚙️ How instances get created (background worker)

Talawa pre-creates occurrences for recurring events in a forward window to make queries fast and simple.

  • Window: The default hot window is 12 months ahead; retention keeps ~3 months of history. Config lives in event_generation_windows.
  • Cron: A background worker runs on a schedule (node-cron). Defaults:
    • Generation: hourly (EVENT_GENERATION_CRON_SCHEDULE, default 0 * * * *).
    • Cleanup: daily at 02:00 UTC (CLEANUP_CRON_SCHEDULE, default 0 2 * * *).
  • Pipeline (high level):
    1. Discover work: find orgs whose window needs extension and their recurring templates.
    2. Normalize recurrence rules (convert COUNT to a derived end date for consistent window math).
    3. Calculate all occurrences between window start and window end.
    4. Insert only missing instances (dedupe by original_instance_start_time).
    5. Update housekeeping (window end, last processed counts, etc.).
  • Key behavior:
    • If a rule is never-ending (no end date and no count), we only generate up to window end.
    • If a rule has COUNT, the engine estimates/derives completion and stops accordingly.
    • Instances store both the original schedule and the final start/end — so exceptions and re-calculations remain consistent.

🔄 Updating a recurring series

Three user-facing options map to distinct backend behaviors.

  1. Update this event only
  • Mutation: updateSingleRecurringEventInstance
  • What happens:
    • We write an event_exceptions row for that instance with only changed fields (e.g., name, description, location, allDay, isPublic, isRegisterable, startAt, endAt).
    • If times are changed, we also update actual_start_time/actual_end_time on the instance.
    • Other instances are unaffected.
  1. Update entire series (name/description)
  • Mutation: updateEntireRecurringEventSeries
  • What happens:
    • We resolve the logical series via original_series_id which is template.
    • We update only safe, series-wide fields on all base templates across the whole logical series (even if the series was split earlier):
      • Supported: name, description
      • Not changed here: timing/recurrence/location/visibility/registration flags
    • We bump last_updated_at on instances so caches and clients know something changed.
    • Because instances inherit name/description, the change is reflected everywhere (past/present/future), unless an instance has its own exception for those fields.
  1. Update this and following (split the series)
  • Mutation: updateThisAndFollowingEvents
  • What happens:
    • We “cut” the current template’s recurrence by setting its recurrence_end_date to just before the targeted instance.
    • We delete materialized instances from this point forward for the current template.
    • We create a brand-new template event, apply provided changes (name, desc, location, timing, flags), and create a new recurrence_rules row for it.
    • We set the new rule’s original_series_id (used as the logical series identifier going forward).
    • The background worker immediately generates future instances for the new template within the hot window.
    • Net effect in the UI: everything before the split stays as-is under the old template; the targeted instance becomes the first occurrence of a new series with your updates; everything after follows the new pattern.

Notes on original_series_id

  • This field links templates and instances that belong to the same logical series across splits.
  • Actions like “update entire series” and “delete this and following” consult original_series_id to operate across all templates created from earlier splits.

🗑️ Deleting recurring events

Three user-facing options:

  1. Delete this event only

    • Mutation: deleteSingleEventInstance
    • What happens:
      • We delete action items linked to this instance.
      • We set is_cancelled = true on the instance (kept for history/reporting), and update last_updated_at.
      • Other instances/series are unaffected.
  2. Delete this and following

    • Mutation: deleteThisAndFollowingEvents
    • What happens:
      • We set the current rule's recurrence_end_date to just before this occurrence (effectively trimming the series).
      • We find all instances with the same original_series_id whose actual_start_time >= this instance and delete them.
      • We also delete their exceptions and action items.
      • Past instances remain.
  3. Delete entire series

    • Mutation: deleteEntireRecurringEventSeries
    • Input is the template (base event) ID.
    • What happens:
      • Using the template's rule, we get its original_series_id.
      • We delete, in order: action items (for all templates and instances), instance exceptions, all instances in the series, all recurrence rules in the series, and finally all template rows in events that belong to this logical series.
      • Attachments for the requested template are cleaned up from storage.

📖 How reading/merging works (what the UI gets)

  • The GraphQL Event type represents both standalone events and recurring instances with a unified shape.
  • For recurring instances, "resolved" fields come from: template defaults + per-instance exceptions + instance timing.
  • Useful instance metadata:
    • sequenceNumber (e.g., "5 of 12"), totalCount when finite.
    • hasExceptions and appliedExceptionData flags to indicate customizations.

⏰ Scheduling, windows, and cleanup

  • Generation cron (hourly by default) grows the forward window and materializes missing instances.
  • Cleanup cron (daily by default) removes old instances beyond history_retention_months for each org and advances retention_start_date.
  • Defaults live in event_generation_windows and can be tuned per organization.

⚠️ Edge cases and notes

  • Never-ending series: only materialized up to the current hot window; cron extends as time moves on.
  • Count-based rules: internally normalized to derive an end date for consistent generation; the original count semantics are preserved when calculating occurrences.
  • Instance de-duplication: before inserting, generation checks for existing original_instance_start_time within the window to avoid duplicates.

If you need deeper implementation details, see these backend modules:

  • Tables: src/drizzle/tables/events.ts, recurrenceRules.ts, recurringEventInstances.ts, recurringEventExceptions.ts, eventGenerationWindows.ts
  • Generation service: src/services/eventGeneration/*
  • Background workers and cron: src/workers/backgroundWorkerService.ts, src/workers/eventGeneration/*, src/workers/eventCleanupWorker.ts
  • GraphQL mutations: src/graphql/types/Mutation/*RecurringEvent* and related inputs.

✅ Event Action Items

This guide explains how action items relate to events in Talawa, with a focus on recurring series and per-instance behavior. It complements events.md.

TL;DR

  • Two kinds of action items:
    • Series (template) action items: attached to the recurring event template; inherit to every instance unless overridden.
    • Instance-only action items: attached directly to one recurring instance (or a standalone event).
  • Per-instance changes to a series item are stored as action item exceptions (override, complete, or delete just for that date).
  • Reading an instance's action items = series items ± exceptions + instance-only items.
  • Deletions cascade appropriately:
    • Delete instance → removes its instance-only items and exceptions.
    • Delete this and following → removes items/exceptions for affected future instances.
    • Delete entire series → removes template items, all instance-only items, and all exceptions across the series.

🗄️ Data model at a glance

Tables in talawa-api (Drizzle ORM):

  • actionitems

    • Series-level: eventId = template event id; isTemplate=true (advisory flag).
    • Instance-only: recurringEventInstanceId = instance id (no inheritance).
    • Other fields: assignedAt, preCompletionNotes, postCompletionNotes, isCompleted, volunteerId, volunteerGroupId, categoryId, organizationId, creatorId, updaterId.
  • actionitem_exceptions

    • One row per (actionId, recurring instance) when a series item is overridden for a specific occurrence.
    • Fields: completed, deleted, assignedAt, preCompletionNotes, postCompletionNotes, volunteerId, volunteerGroupId, categoryId, assigneeId.
    • Unique(actionId, eventId) enforces one exception per action item per instance.
  • recurring_event_instances

    • Materialized occurrences for recurring events; used to anchor instance-only items and exceptions.

📋 Concepts

  • Action item: A task/todo associated with an event (title, description/notes, status, assignees, due date, etc.).
  • Standalone event action item: Attached directly to a single, non-recurring event.
  • Template (series) action item: Attached to a recurring event template and inherited by each instance.
  • Instance action item: Attached to one specific occurrence ("this instance only"). Also called a standalone action item for that instance.
  • Action item exception: A per-instance override for a template action item (e.g., changed title/status for a particular date, or suppressed/deleted just for that one instance).

🔗 How association works

  • Standalone events

    • Action items are linked directly to the event. No recurrence logic is involved.
  • Recurring events

    • Two scopes are supported when creating action items:
      1. Series (template): Create a template action item that applies to all instances by default.
      2. This instance only: Create an instance action item that exists only for the selected occurrence.
    • Reading an instance's action items resolves as:
      • Start with all template action items for the series.
      • Apply any action item exceptions for this specific instance (overrides or suppressions).
      • Add any standalone instance action items.

📖 How instance action items are resolved (read path)

  • Determine baseEventId = instance.baseRecurringEventId (for recurring) or event.id (for standalone).
  • Query actionitems where:
    • eventId = baseEventId (series/template items), OR
    • recurringEventInstanceId = instance.id (instance-only items).
  • Query actionitem_exceptions where actionId IN (series items) AND eventId = instance.id.
  • For each series item:
    • If exception.deleted = true → exclude for this instance.
    • Else override provided fields (completed, notes, assignedAt, volunteer/group/category, assignee).
  • Return final list (series-with-overrides + instance-only), then paginate.

➕ Creating and updating action items

Create
  • Series (template) action item
    • Attaches to the template; appears on every instance unless overridden/suppressed by an exception.
  • Instance-only action item
    • Attaches to the selected occurrence; no impact on other dates.
Update
  • Update an instance-only action item
    • Directly updates that single record; no series impact.
  • Update a template action item for this instance only
    • Creates or updates an action item exception for that instance which overrides the template item (e.g., changed status to Completed, edited notes, adjusted due date).
  • Update a template action item for the entire series
    • Edits the template record; affects all instances except those with per-instance exceptions for the changed fields.
Complete
  • Completing a template action item for this instance only
    • Records an exception on that instance, setting its status to Completed; other instances remain unchanged.
  • Completing a template action item for the entire series
    • Marks the template as Completed; instances reflect completion unless an instance has an exception stating otherwise.
Delete
  • Delete an instance-only action item
    • Removes only that occurrence's action item.
  • Delete a template action item for this instance only
    • Records an exception that suppresses that template action item for the chosen instance (it is treated as deleted only for this date).
  • Delete a template action item for the entire series
    • Removes the template item; it disappears from all instances except where a per-instance action item (standalone) exists.
Update this and following (series split)
  • When a series is split, the new series starts fresh with its own template action items.
  • Existing instance-only items and exceptions remain attached to the old instances; no automatic copying of template items to the new series unless explicitly implemented.

⚙️ Exceptions model (how overrides are applied)

  • For any given instance, the resolved action item list is computed by:
    1. Listing all template (series) action items.
    2. Applying per-instance exceptions (overrides/complete/suppress) to those template items.
    3. Adding all instance-only action items.
  • This is analogous to how event field exceptions work for recurring instances.

🛠️ Backend data model (from talawa-api)

Key tables and fields used to associate action items with events and instances:

  • actionitems (src/drizzle/tables/actionItems.ts)

    • eventId: UUID → references events.id (recurring template or standalone event)
    • recurringEventInstanceId: UUID → references recurring_event_instances.id (instance-only action item)
    • isTemplate: boolean → when true, indicates a series-level (template) action item
    • organizationId, creatorId, updaterId, assignedAt, preCompletionNotes, postCompletionNotes, isCompleted, categoryId, volunteerId, volunteerGroupId
  • actionitem_exceptions (src/drizzle/tables/actionItemExceptions.ts)

    • actionId: UUID → references actionitems.id (the series-level action item being overridden)
    • eventId: UUID → references recurring_event_instances.id (the specific instance where the override applies)
    • Unique(actionId, eventId) → one override per action item per instance
    • Fields mirrored for overrides: assignedAt, preCompletionNotes, postCompletionNotes, completed, deleted, volunteerId, volunteerGroupId, categoryId, assigneeId
  • recurring_event_instances (src/drizzle/tables/recurringEventInstances.ts)

    • Holds materialized instances for recurring events, used to scope exceptions and instance-only action items.
Read path in GraphQL

Resolver: src/graphql/types/Event/actionItems.ts

  • For a given Event node (standalone or instance):
    • Determine baseEventId = parent.baseRecurringEventId || parent.id
    • Query actionitems where:
      • eventId = baseEventId (series-level items), OR
      • recurringEventInstanceId = parent.id (instance-only items)
    • Fetch actionitem_exceptions for the current instance: where actionId IN (above) AND eventId = parent.id
    • Apply per-item logic:
      • If exception.deleted = true → exclude this item for this instance
      • Otherwise override fields (completed, assignedAt, notes, volunteer/volunteerGroup/category, etc.)
    • Return paginated list
Mutations (instance scope) that create exceptions
  • completeActionItemForInstance → sets exception.completed = true and postCompletionNotes
  • markActionItemAsPendingForInstance → sets exception.completed = false
  • updateActionItemForInstance → upserts instance-specific overrides (assignedAt, preCompletionNotes, volunteer, group, category)
  • deleteActionItemForInstance → sets exception.deleted = true (suppresses series item just for this date)
Mutations (template scope)
  • deleteActionItem → deletes the template action item, after first deleting all its exceptions

📚 References (code)

  • Tables

    • actionItems: src/drizzle/tables/actionItems.ts
    • actionItemExceptions: src/drizzle/tables/actionItemExceptions.ts
    • recurringEventInstances: src/drizzle/tables/recurringEventInstances.ts
  • Resolvers

    • Event.actionItems: src/graphql/types/Event/actionItems.ts
  • Mutations (instance scope)

    • completeActionItemForInstance: src/graphql/types/Mutation/completeActionItemForInstance.ts
    • markActionItemAsPendingForInstance: src/graphql/types/Mutation/markActionItemAsPendingForInstance.ts
    • updateActionItemForInstance: src/graphql/types/Mutation/updateActionItemForInstance.ts
    • deleteActionItemForInstance: src/graphql/types/Mutation/deleteActionItemForInstance.ts
  • Mutations (template scope)

    • deleteActionItem: src/graphql/types/Mutation/deleteActionItem.ts
  • Mutations (event lifecycle)

    • deleteSingleEventInstance: src/graphql/types/Mutation/deleteSingleEventInstance.ts
    • deleteThisAndFollowingEvents: src/graphql/types/Mutation/deleteThisAndFollowingEvents.ts
    • deleteEntireRecurringEventSeries: src/graphql/types/Mutation/deleteEntireRecurringEventSeries.ts

👥 Event Volunteers and Volunteer Groups

This guide explains how individual volunteers, volunteer groups, and memberships work for events in Talawa, including recurring series behavior.

TL;DR

  • Two attachment scopes for recurring events:
    • Entire series (template): volunteer/group applies to every instance unless explicitly excluded for specific dates.
    • This instance only: volunteer/group exists only for one occurrence.
  • Per-instance removal uses "exceptions" rows rather than deleting the series record.

🗄️ Data model at a glance

Tables in talawa-api (Drizzle ORM):

  • event_volunteers

    • One row per user volunteering for an event context.
    • Template volunteer (series-wide): eventId = base template event id, isTemplate = true, recurringEventInstanceId = null.
    • Instance-only volunteer: eventId = base template event id, isTemplate = false, recurringEventInstanceId = instance id.
    • Other fields: hasAccepted, isPublic, hoursVolunteered, creatorId, updaterId.
    • Unique(userId, eventId, recurringEventInstanceId) prevents duplicates across series vs one instance.
  • event_volunteer_groups

    • Volunteer group definition for an event context.
    • Template group (series-wide): eventId = base template event id, isTemplate = true, recurringEventInstanceId = null.
    • Instance-only group: eventId = base template event id, isTemplate = false, recurringEventInstanceId = instance id.
    • Other fields: leaderId, name, description, volunteersRequired, creatorId, updaterId.
    • Unique(eventId, name, recurringEventInstanceId) prevents name collisions per instance.
  • event_volunteer_memberships

    • Connects an EventVolunteer to an optional EventVolunteerGroup for an event.
    • Fields: volunteerId, groupId (nullable for individual volunteers), eventId, status (invited/requested/accepted/rejected), audit fields.
    • Unique(volunteerId, groupId, eventId).
    • Note: in create requests, eventId is stored as provided (instance or template) to support admin requests UI; the volunteer row itself is linked to the base event id.
  • Exceptions tables (per-instance exclusions)

    • event_volunteer_exceptions: unique(volunteerId, recurringEventInstanceId). Marks a template volunteer as "excluded" on a specific instance.
    • event_volunteer_group_exceptions: unique(volunteerGroupId, recurringEventInstanceId). Marks a template group as "excluded" on a specific instance.
  • recurring_event_instances

    • Materialized occurrences for recurring events; anchor for instance-only volunteers/groups and per-instance exceptions.

📋 Concepts

  • Event Volunteer: a user attached to an event series or a single occurrence.
  • Volunteer Group: a named group with a leader, optionally with a target number of volunteers required.
  • Volunteer Membership: the relationship (with status) between a volunteer and a group and/or event; supports invitations and requests.
  • Template (series-wide): applies automatically to every past/present/future instance unless excluded by an exception for a specific date.
  • Instance-only: exists only for a single occurrence.
  • Exception: per-instance exclusion record that hides a template volunteer/group from one occurrence.

📖 Read path (how volunteers/groups resolve for an instance)

  • Determine baseEventId = instance.baseRecurringEventId (for recurring) or event.id (for standalone).
  • Volunteers for a recurring instance:
    • Query event_volunteers where eventId = baseEventId (template volunteers) OR recurringEventInstanceId = instance.id (instance-only volunteers).
    • Query event_volunteer_exceptions where recurringEventInstanceId = instance.id and exclude those volunteer IDs from the template set.
  • Volunteer groups for a recurring instance:
    • Query event_volunteer_groups where eventId = baseEventId (template groups) OR recurringEventInstanceId = instance.id (instance-only groups).
    • Query event_volunteer_group_exceptions where recurringEventInstanceId = instance.id and exclude those group IDs from the template set.
  • For standalone events, simply query by eventId = event.id and ignore exceptions/instances.

➕ Creating and updating

Volunteers
  • Create
    • createEventVolunteer supports a scope:
      • ENTIRE_SERIES → create or reuse a template volunteer (isTemplate=true, recurringEventInstanceId=null), converts/removes prior instance-only duplicates for the same user/event.
      • THIS_INSTANCE_ONLY → create an instance-only volunteer for recurringEventInstanceId.
  • Update
    • updateEventVolunteer updates fields such as hasAccepted, isPublic for the specific volunteer row (template or instance-only).
  • Delete
    • Entire row: deleteEventVolunteer removes the volunteer and cascades memberships.
    • For one instance of a template volunteer: deleteEventVolunteerForInstance upserts an exception row to exclude only that date; the template volunteer remains for all other dates.
Volunteer Groups
  • Create
    • createEventVolunteerGroup supports a scope:
      • ENTIRE_SERIES → create or reuse a template group (isTemplate=true), converts/removes prior instance-only duplicates with same name.
      • THIS_INSTANCE_ONLY → create an instance-only group for recurringEventInstanceId.
    • Optional volunteerUserIds allows inviting/creating volunteers and adding memberships to the new group, matching old API behavior.
  • Update
    • updateEventVolunteerGroup updates name/description/volunteersRequired; checks for name conflicts within the same event context.
  • Delete
    • Entire row: deleteEventVolunteerGroup removes the group and cascades its memberships.
    • For one instance of a template group: deleteEventVolunteerGroupForInstance upserts an exception row to exclude only that date; the template group remains for other dates.
Volunteer Memberships
  • Create
    • createVolunteerMembership creates a membership with status (invited/requested/accepted/rejected).
    • If no EventVolunteer exists yet for the user + base event, it creates one (template vs instance-only determined by scope and recurringEventInstanceId).
    • Stores eventId as provided (instance or template) to support admin review screens.
  • Update
    • updateVolunteerMembership updates the status. If accepted, the linked EventVolunteer.hasAccepted is synced appropriately.

📚 References (code)

  • Tables

    • eventVolunteers: src/drizzle/tables/eventVolunteers.ts
    • eventVolunteerGroups: src/drizzle/tables/eventVolunteerGroups.ts
    • eventVolunteerMemberships: src/drizzle/tables/eventVolunteerMemberships.ts
    • eventVolunteerExceptions: src/drizzle/tables/eventVolunteerExceptions.ts
    • eventVolunteerGroupExceptions: src/drizzle/tables/eventVolunteerGroupExceptions.ts
    • recurringEventInstances: src/drizzle/tables/recurringEventInstances.ts
  • Resolvers (read path)

    • Event.volunteers: src/graphql/types/Event/volunteers.ts
    • Event.volunteerGroups: src/graphql/types/Event/volunteerGroups.ts
  • Mutations (volunteers)

    • createEventVolunteer: src/graphql/types/Mutation/createEventVolunteer.ts
    • updateEventVolunteer: src/graphql/types/Mutation/updateEventVolunteer.ts
    • deleteEventVolunteer: src/graphql/types/Mutation/deleteEventVolunteer.ts
    • deleteEventVolunteerForInstance: src/graphql/types/Mutation/deleteEventVolunteerForInstance.ts
  • Mutations (volunteer groups)

    • createEventVolunteerGroup: src/graphql/types/Mutation/createEventVolunteerGroup.ts
    • updateEventVolunteerGroup: src/graphql/types/Mutation/updateEventVolunteerGroup.ts
    • deleteEventVolunteerGroup: src/graphql/types/Mutation/deleteEventVolunteerGroup.ts
    • deleteEventVolunteerGroupForInstance: src/graphql/types/Mutation/deleteEventVolunteerGroupForInstance.ts
  • Mutations (memberships)

    • createVolunteerMembership: src/graphql/types/Mutation/createVolunteerMembership.ts
    • updateVolunteerMembership: src/graphql/types/Mutation/updateVolunteerMembership.ts
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment