diff --git a/modules/schema/src/main/resources/lucuma/odb/graphql/OdbSchema.graphql b/modules/schema/src/main/resources/lucuma/odb/graphql/OdbSchema.graphql index e835bd787..91b21c29b 100644 --- a/modules/schema/src/main/resources/lucuma/odb/graphql/OdbSchema.graphql +++ b/modules/schema/src/main/resources/lucuma/odb/graphql/OdbSchema.graphql @@ -4775,11 +4775,12 @@ type TargetEdit { """ editType: EditType! + targetId: TargetId! + """ Edited object """ - value: Target! - id: Long! @deprecated(reason: "id is no longer computed; a constant value is returned") + value: Target } """ diff --git a/modules/service/src/main/resources/db/migration/V0924__target_update.sql b/modules/service/src/main/resources/db/migration/V0924__target_update.sql new file mode 100644 index 000000000..f6ba674ac --- /dev/null +++ b/modules/service/src/main/resources/db/migration/V0924__target_update.sql @@ -0,0 +1,25 @@ + +CREATE OR REPLACE FUNCTION ch_target_edit() + RETURNS trigger AS $$ +DECLARE +BEGIN + IF (TG_OP = 'DELETE') THEN + PERFORM pg_notify('ch_target_edit', OLD.c_target_id || ',' || OLD.c_program_id || ',' || TG_OP); + END IF; + IF (TG_OP = 'INSERT' OR TG_OP = 'UPDATE') THEN + PERFORM pg_notify('ch_target_edit', NEW.c_target_id || ',' || NEW.c_program_id || ',' || TG_OP); + END IF; + RETURN NEW; +END; +$$ LANGUAGE plpgsql; + +DROP TRIGGER IF EXISTS ch_target_insert_update_trigger on t_target; + +CREATE CONSTRAINT TRIGGER ch_target_insert_update_trigger + AFTER INSERT OR UPDATE OR DELETE ON t_target + DEFERRABLE + FOR EACH ROW + EXECUTE PROCEDURE ch_target_edit(); + + + diff --git a/modules/service/src/main/scala/lucuma/odb/graphql/mapping/SubscriptionMapping.scala b/modules/service/src/main/scala/lucuma/odb/graphql/mapping/SubscriptionMapping.scala index 631d286b8..de1db9832 100644 --- a/modules/service/src/main/scala/lucuma/odb/graphql/mapping/SubscriptionMapping.scala +++ b/modules/service/src/main/scala/lucuma/odb/graphql/mapping/SubscriptionMapping.scala @@ -182,7 +182,10 @@ trait SubscriptionMapping[F[_]] extends Predicates[F] { .map { e => Result( Environment( - Env("editType" -> e.editType), + Env( + "editType" -> e.editType, + "targetId" -> e.targetId + ), Unique(Filter(Predicates.targetEdit.value.id.eql(e.targetId), child)) ) ) diff --git a/modules/service/src/main/scala/lucuma/odb/graphql/mapping/TargetEditMapping.scala b/modules/service/src/main/scala/lucuma/odb/graphql/mapping/TargetEditMapping.scala index b079db62a..73a2400f1 100644 --- a/modules/service/src/main/scala/lucuma/odb/graphql/mapping/TargetEditMapping.scala +++ b/modules/service/src/main/scala/lucuma/odb/graphql/mapping/TargetEditMapping.scala @@ -8,6 +8,7 @@ package mapping import grackle.Result import lucuma.odb.data.EditType import lucuma.odb.graphql.table.TargetView +import lucuma.core.model.Target trait TargetEditMapping[F[_]] extends TargetView[F] { @@ -16,8 +17,8 @@ trait TargetEditMapping[F[_]] extends TargetView[F] { lazy val TargetEditMapping: ObjectMapping = ObjectMapping(TargetEditType)( SqlField("synthetic-id", TargetView.TargetId, key = true, hidden = true), - CursorField("id", _ => Result(0L), List("synthetic-id")), CursorField("editType", _.envR[EditType]("editType"), List("synthetic-id")), + CursorField("targetId", _.envR[Target.Id]("targetId"), List("synthetic-id")), SqlObject("value") ) diff --git a/modules/service/src/test/scala/lucuma/odb/graphql/subscription/targetEdit.scala b/modules/service/src/test/scala/lucuma/odb/graphql/subscription/targetEdit.scala index 51ffaf4fc..27702e9cb 100644 --- a/modules/service/src/test/scala/lucuma/odb/graphql/subscription/targetEdit.scala +++ b/modules/service/src/test/scala/lucuma/odb/graphql/subscription/targetEdit.scala @@ -14,6 +14,7 @@ import lucuma.core.model.User import lucuma.odb.data.EditType import scala.concurrent.duration.* +import cats.effect.kernel.Ref class targetEdit extends OdbSuite { @@ -76,6 +77,7 @@ class targetEdit extends OdbSuite { subscription { targetEdit$args { editType + targetId value { name } @@ -86,99 +88,120 @@ class targetEdit extends OdbSuite { def targetEdit( editType: EditType, - name: String + name: String, + tid: Target.Id ): Json = Json.obj( "targetEdit" -> Json.obj( "editType" -> Json.fromString(editType.tag.toUpperCase), + "targetId" -> Json.fromString(tid.show), "value" -> Json.obj( "name" -> Json.fromString(name) ) ) ) - def created(name: String): Json = - targetEdit(EditType.Created, name) + def created(name: String, tid: Target.Id): Json = + targetEdit(EditType.Created, name, tid) - def updated(name: String): Json = - targetEdit(EditType.Updated, name) + def updated(name: String, tid: Target.Id): Json = + targetEdit(EditType.Updated, name, tid) test("trigger for a new target in any program") { - subscriptionExpect( - user = pi, - query = nameSubscription(None, None), - mutations = - Right( - createProgramAs(pi).flatMap(createTarget(pi, _, "target 1")) >> - createProgramAs(pi).flatMap(createTarget(pi, _, "target 2")) - ), - expected = List(created("target 1"), created("target 2")) - ) - } - - test("trigger for an updated target in any program") { - subscriptionExpect( - user = pi, - query = nameSubscription(None, None), - mutations = - Right( - for { - pid <- createProgramAs(pi) - tid <- createTarget(pi, pid, "old name") - _ <- updateTarget(pi, tid, "new name") - } yield () - ), - expected = List(created("old name"), updated("new name")) - ) - } - - test("trigger for a new target in [only] a specific program") { - createProgramAs(pi).flatMap { pid => - subscriptionExpect( + Ref.of[IO, List[Target.Id]](List.empty[Target.Id]).flatMap { ref => + subscriptionExpectF( user = pi, - query = nameSubscription(Some(pid), None), + query = nameSubscription(None, None), mutations = Right( - createTarget(pi, pid, "should see this") >> - createProgramAs(pi).flatMap(createTarget(pi, _, "should not see this")) + createProgramAs(pi).flatMap(createTarget(pi, _, "target 1") + .flatTap(i => ref.update(i :: _))) >> + createProgramAs(pi).flatMap(createTarget(pi, _, "target 2") + .flatTap(i => ref.update(i :: _))) ), - expected = List(created("should see this")) + expectedF = ref.get.map(l => List(created("target 1", l(1)), created("target 2", l(0)))) ) } } - test("trigger for an updated target in [only] a specific program") { - createProgramAs(pi).flatMap { pid => - subscriptionExpect( + test("trigger for an updated target in any program") { + Ref.of[IO, List[Target.Id]](List.empty[Target.Id]).flatMap { ref => + subscriptionExpectF( user = pi, - query = nameSubscription(Some(pid), None), + query = nameSubscription(None, None), mutations = Right( - createTarget(pi, pid, "should see this").flatMap(updateTarget(pi, _, "and this")) >> - createProgramAs(pi).flatMap(createTarget(pi, _, "should not see this").flatMap(updateTarget(pi, _, "or this"))) + for { + pid <- createProgramAs(pi) + tid <- createTarget(pi, pid, "old name").flatTap(i => ref.set(List(i))) + _ <- updateTarget(pi, tid, "new name") + } yield () ), - expected = List(created("should see this"), updated("and this")) + expectedF = ref.get.map(l => List(created("old name", l(0)), updated("new name", l(0)))) ) } } - test("trigger for [only] a specific updated target") { - createProgramAs(pi).flatMap { pid => - createTarget(pi, pid, "old name").flatMap { tid => - subscriptionExpect( + test("trigger for a new target in [only] a specific program") { + Ref.of[IO, List[Target.Id]](List.empty[Target.Id]).flatMap { ref => + createProgramAs(pi).flatMap { pid => + subscriptionExpectF( user = pi, - query = nameSubscription(None, Some(tid)), + query = nameSubscription(Some(pid), None), mutations = Right( - updateTarget(pi, tid, "new name") >> - createTarget(pi, pid, "should not see this").flatMap(updateTarget(pi, _, "or this")) + createTarget(pi, pid, "should see this").flatTap(i => ref.set(List(i))) >> + createProgramAs(pi).flatMap(createTarget(pi, _, "should not see this")) ), - expected = List(updated("new name")) + expectedF = ref.get.map(l => List(created("should see this", l(0)))) ) } } } + test("trigger for an updated target in [only] a specific program") { + Ref.of[IO, List[Target.Id]](List.empty[Target.Id]).flatMap { ref => + createProgramAs(pi).flatMap { pid => + subscriptionExpectF( + user = pi, + query = nameSubscription(Some(pid), None), + mutations = + Right( + createTarget(pi, pid, "should see this") + .flatTap(i => ref.update(i :: _)) + .flatMap(updateTarget(pi, _, "and this")) >> + createProgramAs(pi).flatMap( + createTarget(pi, _, "should not see this") + .flatTap(i => ref.update(i :: _)) + .flatMap(updateTarget(pi, _, "or this"))) + ), + expectedF = ref.get.map(l => List(created("should see this", l(1)), updated("and this", l(1)))) + ) + } + } + } + + test("trigger for [only] a specific updated target") { + Ref.of[IO, List[Target.Id]](List.empty[Target.Id]).flatMap { ref => + createProgramAs(pi).flatMap { pid => + createTarget(pi, pid, "old name") + .flatTap(i => ref.set(List(i))) + .flatMap { tid => + subscriptionExpectF( + user = pi, + query = nameSubscription(None, Some(tid)), + mutations = + Right( + updateTarget(pi, tid, "new name") >> + createTarget(pi, pid, "should not see this").flatMap(updateTarget(pi, _, "or this")) + ), + expectedF = ref.get.map(l => List(updated("new name", l(0)))) + ) + } + } + } + } + test("work even if no database fields are selected") { subscriptionExpect( user = pi, @@ -186,7 +209,6 @@ class targetEdit extends OdbSuite { subscription { targetEdit { editType - id } } """, @@ -194,7 +216,7 @@ class targetEdit extends OdbSuite { Right( createProgramAs(pi).flatMap(createTarget(pi, _, "t")).replicateA(2) ), - expected = List.fill(2)(json"""{"targetEdit":{"editType":"CREATED","id":0}}""") + expected = List.fill(2)(json"""{"targetEdit":{"editType":"CREATED"}}""") ) }