Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

add changelog for flags and values #50

Merged
merged 1 commit into from
Dec 16, 2024
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
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
Loading