Skip to content

Commit

Permalink
add changelog for flags and values
Browse files Browse the repository at this point in the history
  • Loading branch information
m.kindritskiy committed Dec 16, 2024
1 parent 9e8faf1 commit 1df6078
Show file tree
Hide file tree
Showing 17 changed files with 2,490 additions and 273 deletions.
134 changes: 84 additions & 50 deletions featureflags/graph/graph.py
Original file line number Diff line number Diff line change
@@ -1,6 +1,8 @@
from uuid import UUID
from collections import defaultdict

import aiopg.sa
from hiku.enum import Enum
from hiku.engine import Engine, pass_context
from hiku.expr.core import (
S,
Expand Down Expand Up @@ -32,6 +34,7 @@
Sequence,
String,
TypeRef,
EnumRef,
)
from sqlalchemy import select

Expand All @@ -41,6 +44,7 @@
GRAPH_PULL_TIME_HISTOGRAM,
)
from featureflags.graph.types import (
Action,
AddCheckOp,
AddConditionOp,
AddValueConditionOp,
Expand All @@ -55,6 +59,7 @@
SaveValueResult,
DeleteVariableResult,
DeleteProjectResult,
ValueAction,
)
from featureflags.graph.utils import is_valid_uuid
from featureflags.metrics import wrap_metric
Expand All @@ -74,6 +79,7 @@
from featureflags.utils import (
exec_expression,
exec_scalar,
exec_many,
)


Expand Down Expand Up @@ -258,30 +264,6 @@ async def flag_project(ids: list[int]) -> list[int]:
async def value_project(ids: list[int]) -> list[int]:
return ids


@pass_context
async def get_flag_last_action_timestamp(
ctx: dict, fields: list[Field]
) -> list[str | None]:
if not ctx[GraphContext.USER_SESSION].is_authenticated:
return []

[field] = fields
opts = field.options
flag_id = UUID(opts["id"])

result = await exec_scalar(
ctx[GraphContext.DB_ENGINE],
(
select([Changelog.timestamp])
.where(Changelog.flag == flag_id)
.order_by(Changelog.timestamp.desc())
.limit(1)
),
)
return [str(result) if result else None]


@pass_context
async def get_value_last_action_timestamp(
ctx: dict, fields: list[Field]
Expand Down Expand Up @@ -453,6 +435,31 @@ async def get_value_last_action_timestamp(
to_column=Condition.id,
)


@pass_context
async def link_flag_changes(
ctx: dict, flag_ids: list[UUID],
) -> list[list[UUID]]:
if not ctx[GraphContext.USER_SESSION].is_authenticated:
return []

data = await exec_many(
ctx[GraphContext.DB_ENGINE],
(
select([Changelog])
.where(Changelog.flag.in_(flag_ids))
.order_by(Changelog.timestamp.desc())
),
)

result = defaultdict(list)

for row in data:
result[row.flag].append(row.id)

return [result[flag_id] for flag_id in flag_ids]


FlagNode = Node(
"Flag",
[
Expand All @@ -475,6 +482,7 @@ async def get_value_last_action_timestamp(
),
Field("created_timestamp", None, flag_sg),
Field("reported_timestamp", None, flag_sg),
Link("changes", Sequence["Change"], link_flag_changes, requires="id"),
],
)

Expand All @@ -486,6 +494,32 @@ async def get_value_last_action_timestamp(
to_column=ValueCondition.id,
)


@pass_context
async def link_value_changes(
ctx: dict, value_ids: list[UUID],
) -> list[list[UUID]]:
if not ctx[GraphContext.USER_SESSION].is_authenticated:
return []

data = await exec_many(
ctx[GraphContext.DB_ENGINE],
(
select([ValueChangelog])
.where(ValueChangelog.value.in_(value_ids))
.order_by(ValueChangelog.timestamp.desc())
),
)

result = defaultdict(list)

for row in data:
result[row.value].append(row.id)

return [result[value_id] for value_id in value_ids]



ValueNode = Node(
"Value",
[
Expand Down Expand Up @@ -513,6 +547,7 @@ async def get_value_last_action_timestamp(
Field("value_override", None, value_sg),
Field("created_timestamp", None, value_sg),
Field("reported_timestamp", None, value_sg),
Link("changes", Sequence["ValueChange"], link_value_changes, requires="id", description="Changes, recent first"),
],
)

Expand Down Expand Up @@ -579,7 +614,7 @@ async def get_value_last_action_timestamp(
Field("timestamp", None, change_sg),
Field("_user", None, change_sg.c(S.this.auth_user)),
Field("_flag", None, change_sg.c(S.this.flag)),
Field("actions", None, change_sg),
Field("actions", Sequence[EnumRef['FlagAction']], change_sg),
Link("flag", TypeRef["Flag"], direct_link, requires="_flag"),
Link("user", TypeRef["User"], direct_link, requires="_user"),
],
Expand All @@ -594,26 +629,14 @@ async def get_value_last_action_timestamp(
Field("timestamp", None, value_change_sg),
Field("_user", None, value_change_sg.c(S.this.auth_user)),
Field("_value", None, value_change_sg.c(S.this.value)),
Field("actions", None, value_change_sg),
Field("actions", Sequence[EnumRef['ValueAction']], value_change_sg),
Link("value", TypeRef["Value"], direct_link, requires="_value"),
Link("user", TypeRef["User"], direct_link, requires="_user"),
],
)

RootNode = Root(
[
Field(
"flagLastActionTimestamp",
Optional[String],
get_flag_last_action_timestamp,
options=[Option("id", String)],
),
Field(
"valueLastActionTimestamp",
Optional[String],
get_value_last_action_timestamp,
options=[Option("id", String)],
),
Link(
"flag",
Optional["Flag"],
Expand Down Expand Up @@ -888,6 +911,11 @@ def get_field(name: str) -> str | None:
],
)

data_types = {
"SaveFlagOperation": Record[{"type": String, "payload": Any}],
"SaveValueOperation": Record[{"type": String, "payload": Any}],
}

GRAPH = Graph(
[
ProjectNode,
Expand All @@ -901,6 +929,22 @@ def get_field(name: str) -> str | None:
ChangeNode,
ValueChangeNode,
RootNode,

SignInNode,
SignOutNode,
SaveFlagNode,
SaveValueNode,
ResetFlagNode,
ResetValueNode,
DeleteFlagNode,
DeleteValueNode,
DeleteVariableNode,
DeleteProjectNode,
],
data_types=data_types,
enums=[
Enum.from_builtin(Action, name="FlagAction"),
Enum.from_builtin(ValueAction),
]
)

Expand Down Expand Up @@ -1198,24 +1242,14 @@ async def delete_project(ctx: dict, options: dict) -> DeleteProjectResult:
return DeleteProjectResult(None)


mutation_data_types = {
data_types = {
"SaveFlagOperation": Record[{"type": String, "payload": Any}],
"SaveValueOperation": Record[{"type": String, "payload": Any}],
}

MUTATION_GRAPH = Graph(
[
*GRAPH.nodes,
SignInNode,
SignOutNode,
SaveFlagNode,
SaveValueNode,
ResetFlagNode,
ResetValueNode,
DeleteFlagNode,
DeleteValueNode,
DeleteVariableNode,
DeleteProjectNode,
Root(
[
Link(
Expand Down Expand Up @@ -1297,7 +1331,7 @@ async def delete_project(ctx: dict, options: dict) -> DeleteProjectResult:
]
),
],
data_types=mutation_data_types,
data_types=data_types,
)

GRAPH = apply(GRAPH, [AsyncGraphMetrics("public")])
Expand Down
5 changes: 5 additions & 0 deletions featureflags/utils.py
Original file line number Diff line number Diff line change
Expand Up @@ -92,6 +92,11 @@ async def exec_expression(engine: Engine, stmt: Any) -> Any:
result = await conn.execute(stmt)
return [r[0] for r in await result.fetchall()]

async def exec_many(engine: Engine, stmt: Any) -> Any:
async with engine.acquire() as conn:
result = await conn.execute(stmt)
return [r for r in await result.fetchall()]


def escape_dn_chars(s: str) -> str:
"""
Expand Down
2 changes: 1 addition & 1 deletion featureflags/web/container.py
Original file line number Diff line number Diff line change
Expand Up @@ -31,7 +31,7 @@ class Container(containers.DeclarativeContainer):
Engine,
providers.Callable(AsyncIOExecutor),
)
graphql_endpoint: AsyncBatchGraphQLEndpoint = providers.Factory(
graphql_endpoint: AsyncBatchGraphQLEndpoint = providers.Singleton(
AsyncBatchGraphQLEndpoint,
engine=graph_engine,
query_graph=graph.GRAPH,
Expand Down
4 changes: 2 additions & 2 deletions featureflags/web/types.py
Original file line number Diff line number Diff line change
Expand Up @@ -4,6 +4,6 @@


class GraphQueryRequest(BaseModel):
operationName: str # noqa: N815
variables: dict[str, Any]
query: str
operationName: str | None = None # noqa: N815
variables: dict[str, Any] | None = None
Loading

0 comments on commit 1df6078

Please sign in to comment.