Skip to main content

Event Origin & Other IDs

Why an event needs its own identity

A tournamentRecord has exactly one tournamentId. An event inside it may have been sanctioned somewhere else entirely — by a governing body that holds its own tournament, with its own internal id, in its own system.

A single record can carry events from several sanctioning systems at once. When that happens there is no single "the" sanctioning tournamentId for the record, and the event's origin cannot be inferred from the record that carries it.

Event.eventOtherIds[] is where that identity lives.

Shape

UnifiedEventID is the event-grain member of the Unified*ID family, alongside UnifiedTournamentID (tournament.tournamentOtherIds), UnifiedDrawID (drawDefinition.drawOtherIds), UnifiedParticipantID (participant.participantOtherIds), UnifiedPersonID (person.personOtherIds) and UnifiedVenueID (venue.venueOtherIds).

See Unified Identity for the rules shared across the whole family, and for the tournament and draw grains in particular — the tournament grain answers "where did this whole record come from?", which is the natural question for an ingested record, and is independent of the per-event sanctioning origin described here.

event.eventOtherIds = [
{ organisationId: 'ITA', tournamentId: 'ita-4471', eventId: 'ita-ev-9', isOrigin: true },
{ organisationId: 'USTA', tournamentId: 'usta-88', eventId: 'usta-ev-2' },
];
attributemeaning
organisationIdthe organisation whose system this id belongs to (required)
tournamentIdthat organisation's tournamentId — not the carrying record's
eventIdthat organisation's id for this event
isOriginmarks the entry as the sanctioning source the event came from
uniqueOrganisationNamehuman-readable name of the organisation

Three properties of the shape are load-bearing:

  • tournamentId is optional and belongs to organisationId. It names a tournament in that organisation's system, never the carrying record. It is not assumed to resolve locally — see the two linking modes below.
  • eventId is optional. An event sanctioned but not yet created in the origin system has no id there. It is written back after the event is copied to the origin — after the fact, or through an external API integration.
  • At most one entry carries isOrigin. Everything else in the array is a system the event is merely also known to. An event with no flagged entry simply has no declared origin, which is the ordinary single-sanction case.

Two linking modes

The sanctioning workflow can attach an event to either kind of origin, and the same UnifiedEventID expresses both:

modeorganisationIddoes tournamentId resolve here?
External — a tournament sanctioned outside this ecosystem, linked in as an event of the tournament being sanctioneda foreign governing bodyusually no; the body may hold no record carrying the event
Internal — a tournament already sanctioned within this ecosystem, linked as an eventa CourtHive provideryes, it is a real tournamentRecord

The distinction is data, not schema: nothing branches on it, and a consumer that wants the origin's local record simply attempts the lookup. This is exactly why the projected origin_tournament_id cannot carry a foreign key — a constraint satisfiable for internal links is impossible for external ones. Resolution is a read-time LEFT JOIN, never a write-time constraint.

Reading the origin

import { readModel } from 'tods-competition-factory';

const origin = readModel.eventOrigin(event); // the isOrigin entry, or undefined

Writing

Through the standard mutation. The array is replaced wholesale — the natural grain, since a caller reconciling against a sanctioning body holds the full list. Pass null to clear.

engine.modifyEvent({
eventUpdates: {
eventOtherIds: [{ organisationId: 'ITA', tournamentId: 'ita-4471', isOrigin: true }],
},
eventId,
});

This dispatches MODIFY_EVENT, so any subscriber maintaining a projection of events sees the change.

In the read model

cast() and the incremental projection producer both flatten the flagged entry onto the events row:

columnsource
tournament_idthe record that carries the event
origin_organisation_ideventOrigin(event).organisationId
origin_tournament_ideventOrigin(event).tournamentId
origin_event_ideventOrigin(event).eventId

tournament_id and origin_tournament_id are independent by design. Reading one for the other is the mistake this concept exists to prevent.

The identity a third party actually needs

An outside sanctioning body is not expected to hold a CODES tournamentRecord. It has its own model and its own API, and an integration layer transforms results outward to it. Nothing is replicated to them. Our whole obligation is to carry enough identity that the results are addressable — three things:

identitywhere it lives
sanctioned tournamentIdevent.eventOtherIds[isOrigin].tournamentId
sanctioned eventIdevent.eventOtherIds[isOrigin].eventId
sanctioned participant idparticipant.participantOtherIds[]

The third has its own field for a specific reason. person.personOtherIds carries an outside body's id for a person — but it hangs off participant.person, and a PAIR or TEAM participant has no person at all. addPersonOtherId says so in its own error text: "only INDIVIDUAL participants accept personOtherIds". A registered pair or team therefore had nowhere to record the id the sanctioning body knows it by.

Participant.participantOtherIds[] (UnifiedParticipantID) sits on the participant, so it works uniformly for INDIVIDUAL, PAIR and TEAM:

participant.participantOtherIds = [{ organisationId: 'ITA', participantId: 'ita-entry-771' }];

Append or update one with addParticipantOtherId — the participant-grain sibling of addPersonOtherId, keyed on organisationId and idempotent:

engine.addParticipantOtherId({
participantId, // ours
organisationId: 'ITA',
otherParticipantId: 'ita-entry-771', // theirs
});

otherParticipantId is named distinctly from participantId on purpose: both are participant ids, and silently transposing them would stamp a participant with its own id and still look like it worked.

Where activation fits

activateFromSanctioning stamps the event origin automatically. A proposal that arrives carrying its own eventOtherIds — a sanction that originated outside this ecosystem — keeps that entry untouched, because it is the address results are sent back to. A proposal that carries none gets the sanctioning body's own identity stamped as the origin, so a locally sanctioned tournament is queryable on exactly the same terms rather than being a special case.