lolopinto / ent

MIT License
52 stars 6 forks source link

Delete actions do not call `removeInboundEdge` on field edges #1819

Closed Swahvay closed 2 months ago

Swahvay commented 4 months ago

Ents with a field edge get that edge created automatically (and removed automatically when unset) through their builder. This is done in the getEditedFields method of the builder, as defined in the builder template (specifically line 291):

https://github.com/lolopinto/ent/blob/47372cfd6c0376753c805a2249ed3428866077b7/internal/tscode/builder.tmpl#L276-L295

The generated code ends up looking something like this:

private async getEditedFields(): Promise<Map<string, any>> {
  const input = this.input;

  const result = new Map<string, any>();

  const addField = function (key: string, value: any) {
    if (value !== undefined) {
      result.set(key, value);
    }
  };
  addField('competitionEventID', input.competitionEventId);
  if (input.competitionEventId !== undefined) {
    if (input.competitionEventId) {
      this.orchestrator.addInboundEdge(
        input.competitionEventId,
        EdgeType.CompetitionEventToEntries,
        NodeType.CompetitionEvent,
      );
    }
    if (
      this.existingEnt &&
      this.existingEnt.competitionEventId &&
      this.existingEnt.competitionEventId !== input.competitionEventId
    ) {
      this.orchestrator.removeInboundEdge(
        this.existingEnt.competitionEventId,
        EdgeType.CompetitionEventToEntries,
      );
    }
  }
  return $result;
}

This method is passed into the Orchestrator in the constructor of the builder like this:

this.orchestrator = new Orchestrator({
  viewer,
  operation: this.operation,
  tableName: 'generic_competition_entries',
  key: 'id',
  loaderOptions: GenericCompetitionEntry.loaderOptions(),
  builder: this,
  action,
  schema,
  editedFields: () => this.getEditedFields.apply(this), // <--- Right here
  updateInput,
  fieldInfo: genericCompetitionEntryLoaderInfo.fieldInfo,
  ...opts,
});

This works great for insert/update operations, but fails with delete actions. This is because the delete action always defines getInput as

getInput(): GenericCompetitionEntryInput {
  return {};
}

So the checks in getEditedFields always fail. I think the simplest way to get around this is to just add a check for this.operation === WriteOperation.Delete to always call removeInboundEdge even when input is empty. Thus changing the call to look like this:

private async getEditedFields(): Promise<Map<string, any>> {
  const input = this.input;

  const result = new Map<string, any>();

  const addField = function (key: string, value: any) {
    if (value !== undefined) {
      result.set(key, value);
    }
  };
  addField('competitionEventID', input.competitionEventId);
- if (input.competitionEventId !== undefined) {
+ if (input.competitionEventId !== undefined || this.operation === WriteOperation.Delete) {
    if (input.competitionEventId) {
      this.orchestrator.addInboundEdge(
        input.competitionEventId,
        EdgeType.CompetitionEventToEntries,
        NodeType.CompetitionEvent,
      );
    }
    if (
      this.existingEnt &&
      this.existingEnt.competitionEventId &&
      this.existingEnt.competitionEventId !== input.competitionEventId
    ) {
      this.orchestrator.removeInboundEdge(
        this.existingEnt.competitionEventId,
        EdgeType.CompetitionEventToEntries,
      );
    }
  }
  return $result;
}

This bug doesn't have much effect since the ents are deleted so fetching edges doesn't return deleted ents, but when you try to do something like event.queryEntries().queryRawCount(), that only fetches over the edge table, which will now return a bad count.