Coverage for src/lexigram/graphql/core/context/_models.py: 98%

43 statements  

« prev     ^ index     » next       coverage.py v7.15.4, created at 2026-08-25 04:37 +0800

1"""GraphQL request/response/error domain models.""" 

2 

3from __future__ import annotations 

4 

5from dataclasses import dataclass, field 

6from datetime import UTC, datetime 

7from typing import TYPE_CHECKING, Any, Generic, TypeVar 

8import uuid 

9 

10from lexigram.contracts.auth import AuthenticatorProtocol 

11from lexigram.contracts.core import IdGeneratorProtocol 

12from lexigram.contracts.graphql import ( 

13 GraphQLPrincipal, 

14 GraphQLPrincipalResolverProtocol, 

15) 

16from lexigram.domain import DomainModel 

17from lexigram.logging import get_logger 

18from lexigram.primitives import clock as ambient_clock 

19from lexigram.validation import Field 

20 

21if TYPE_CHECKING: 

22 from lexigram.graphql.config import GraphQLConfig 

23 

24logger = get_logger(__name__) 

25 

26 

27T = TypeVar("T") 

28 

29 

30@dataclass(init=False) 

31class GraphQLRequest(DomainModel): 

32 """GraphQL request model. 

33 

34 Represents an incoming GraphQL request with query, 

35 variables, and operation name. 

36 

37 Attributes: 

38 query: The GraphQL query string. 

39 variables: Variables for the query. 

40 operation_name: Name of the operation to execute. 

41 extensions: Optional extensions data. 

42 """ 

43 

44 query: str = Field(..., description="GraphQL query string") 

45 variables: dict[str, Any] = Field( 

46 default_factory=dict, 

47 description="Query variables", 

48 ) 

49 operation_name: str | None = Field( 

50 default=None, 

51 description="Operation name to execute", 

52 ) 

53 extensions: dict[str, Any] = Field( 

54 default_factory=dict, 

55 description="Request extensions", 

56 ) 

57 

58 model_config = {"frozen": False} 

59 

60 

61@dataclass(init=False) 

62class GraphQLErrorPayload(DomainModel): 

63 """GraphQL error payload model. 

64 

65 G-09 FIX: Renamed from GraphQLError to GraphQLErrorPayload to avoid 

66 naming collision with the GraphQLError in exceptions.py. 

67 

68 Represents a GraphQL error following the spec. 

69 

70 Attributes: 

71 message: Human-readable error message. 

72 locations: Source locations of the error. 

73 path: Path to the field that caused the error. 

74 extensions: Additional error information. 

75 """ 

76 

77 message: str = Field(..., description="Error message") 

78 locations: list[dict[str, int]] | None = Field( 

79 default=None, 

80 description="Source locations", 

81 ) 

82 path: list[str | int] | None = Field( 

83 default=None, 

84 description="Path to the error", 

85 ) 

86 extensions: dict[str, Any] = Field( 

87 default_factory=dict, 

88 description="Error extensions", 

89 ) 

90 

91 

92@dataclass(init=False) 

93class GraphQLResponse(DomainModel, Generic[T]): 

94 """GraphQL response model. 

95 

96 Represents a GraphQL response with data and/or errors. 

97 

98 Attributes: 

99 data: The result data. 

100 errors: List of errors if any occurred. 

101 extensions: Optional response extensions. 

102 """ 

103 

104 data: T | None = Field(default=None, description="Response data") 

105 errors: list[GraphQLErrorPayload] | None = Field( 

106 default=None, 

107 description="List of errors", 

108 ) 

109 extensions: dict[str, Any] = Field( 

110 default_factory=dict, 

111 description="Response extensions", 

112 ) 

113 http_headers: dict[str, str] = Field( 

114 default_factory=dict, 

115 description="HTTP response headers (e.g. Cache-Control, Vary) set by the executor", 

116 ) 

117 

118 @property 

119 def has_errors(self) -> bool: 

120 """Check if response has errors.""" 

121 return self.errors is not None and len(self.errors) > 0 

122 

123 @property 

124 def is_successful(self) -> bool: 

125 """Check if response is successful.""" 

126 return not self.has_errors 

127 

128 def add_error( 

129 self, 

130 message: str, 

131 path: list[str | int] | None = None, 

132 extensions: dict[str, Any] | None = None, 

133 ) -> None: 

134 """Add an error to the response. 

135 

136 Args: 

137 message: Error message. 

138 path: Path to the error. 

139 extensions: Additional error data. 

140 """ 

141 if self.errors is None: 

142 self.errors = [] 

143 

144 self.errors.append( 

145 GraphQLErrorPayload( 

146 message=message, 

147 path=path, 

148 extensions=extensions or {}, 

149 ), 

150 )