@@ -37,6 +37,197 @@ def __str__(self) -> str:
3737 return self .value
3838
3939
40+ class ResponseAdapter :
41+ """Adapter for normalizing API responses between v2 and v3 formats.
42+
43+ v2 returns flat records with 'Id' key and 'list' wrapper.
44+ v3 returns records with 'id' key, 'fields' wrapper, and 'records' wrapper.
45+ This adapter normalizes v3 responses to v2-compatible flat format.
46+ """
47+
48+ @staticmethod
49+ def normalize_record (record : dict [str , Any ], api_version : "APIVersion" ) -> dict [str , Any ]:
50+ """Normalize a single record response to flat format.
51+
52+ v2 format: {"Id": 1, "Name": "John", ...}
53+ v3 format: {"id": 1, "fields": {"Name": "John", ...}}
54+
55+ Returns flat format with "Id" key for consistency.
56+ """
57+ if api_version == APIVersion .V2 :
58+ return record
59+
60+ # v3: extract fields and normalize id
61+ result : dict [str , Any ] = {}
62+ if "id" in record :
63+ result ["Id" ] = record ["id" ]
64+ if "fields" in record :
65+ result .update (record ["fields" ])
66+ else :
67+ # Fallback: if no fields wrapper, copy all except 'id'
68+ for k , v in record .items ():
69+ if k == "id" :
70+ result ["Id" ] = v
71+ elif k != "deleted" :
72+ result [k ] = v
73+ return result
74+
75+ @staticmethod
76+ def normalize_records_list (
77+ response : dict [str , Any ], api_version : "APIVersion"
78+ ) -> tuple [list [dict [str , Any ]], dict [str , Any ]]:
79+ """Normalize a records list response.
80+
81+ v2: {"list": [...], "pageInfo": {"isLastPage": true, ...}}
82+ v3: {"records": [...], "next": "...", "prev": null}
83+
84+ Returns (records_list, page_info) where page_info uses v2-compatible format.
85+ """
86+ if api_version == APIVersion .V2 :
87+ records = response .get ("list" , [])
88+ page_info = response .get ("pageInfo" , {})
89+ return records , page_info
90+
91+ # v3: extract from "records" key
92+ raw_records = response .get ("records" , [])
93+ records = [ResponseAdapter .normalize_record (r , api_version ) for r in raw_records ]
94+
95+ # Convert v3 pagination to v2-compatible pageInfo
96+ has_next = response .get ("next" ) is not None
97+ page_info = {
98+ "isLastPage" : not has_next ,
99+ "next" : response .get ("next" ),
100+ "prev" : response .get ("prev" ),
101+ }
102+
103+ return records , page_info
104+
105+ @staticmethod
106+ def extract_record_id (response : dict [str , Any ], api_version : "APIVersion" ) -> Any :
107+ """Extract record ID from a create/update/delete response.
108+
109+ v2: {"Id": 123}
110+ v3: {"id": 123, "fields": {...}} or {"records": [{"id": 123, ...}]}
111+ """
112+ if api_version == APIVersion .V2 :
113+ return response .get ("Id" )
114+
115+ # v3: try direct id first
116+ if "id" in response :
117+ return response ["id" ]
118+
119+ # v3: try records wrapper (bulk responses)
120+ records = response .get ("records" , [])
121+ if records and isinstance (records , list ) and len (records ) > 0 :
122+ return records [0 ].get ("id" )
123+
124+ # Fallback: try "Id" (in case of mixed format)
125+ return response .get ("Id" )
126+
127+ @staticmethod
128+ def extract_record_ids (
129+ response : dict [str , Any ] | list [dict [str , Any ]], api_version : "APIVersion"
130+ ) -> list [Any ]:
131+ """Extract multiple record IDs from bulk operation responses.
132+
133+ v2: [{"Id": 1}, {"Id": 2}]
134+ v3: {"records": [{"id": 1, ...}, {"id": 2, ...}]}
135+ """
136+ if api_version == APIVersion .V2 :
137+ if isinstance (response , list ):
138+ return [
139+ r .get ("Id" ) for r in response if isinstance (r , dict ) and r .get ("Id" ) is not None
140+ ]
141+ elif isinstance (response , dict ) and "Id" in response :
142+ return [response ["Id" ]]
143+ return []
144+
145+ # v3: records wrapper
146+ if isinstance (response , dict ):
147+ records = response .get ("records" , [])
148+ if records :
149+ return [
150+ r .get ("id" ) for r in records if isinstance (r , dict ) and r .get ("id" ) is not None
151+ ]
152+ # Fallback: direct id
153+ if "id" in response :
154+ return [response ["id" ]]
155+ elif isinstance (response , list ):
156+ return [
157+ r .get ("id" ) for r in response if isinstance (r , dict ) and r .get ("id" ) is not None
158+ ]
159+
160+ return []
161+
162+
163+ class RequestAdapter :
164+ """Adapter for formatting API requests between v2 and v3 formats.
165+
166+ v2 sends flat record data: {"Name": "John", "Email": "john@example.com"}
167+ v3 wraps field data: {"fields": {"Name": "John", "Email": "john@example.com"}}
168+ """
169+
170+ @staticmethod
171+ def format_record (record : dict [str , Any ], api_version : "APIVersion" ) -> dict [str , Any ]:
172+ """Format a record for create/update requests.
173+
174+ v2: {"Name": "John", "Email": "john@example.com"}
175+ v3: {"fields": {"Name": "John", "Email": "john@example.com"}}
176+ """
177+ if api_version == APIVersion .V2 :
178+ return record
179+
180+ # v3: wrap in fields, keeping Id/id separate
181+ fields = {}
182+ result : dict [str , Any ] = {}
183+ for k , v in record .items ():
184+ if k in ("Id" , "id" ):
185+ result ["id" ] = v
186+ else :
187+ fields [k ] = v
188+ result ["fields" ] = fields
189+ return result
190+
191+ @staticmethod
192+ def format_records (
193+ records : list [dict [str , Any ]], api_version : "APIVersion"
194+ ) -> list [dict [str , Any ]] | dict [str , Any ]:
195+ """Format multiple records for bulk create/update requests.
196+
197+ v2: [{"Name": "John"}, {"Name": "Jane"}]
198+ v3: {"records": [{"fields": {"Name": "John"}}, {"fields": {"Name": "Jane"}}]}
199+ """
200+ if api_version == APIVersion .V2 :
201+ return records
202+
203+ # v3: wrap in records array with fields
204+ return {"records" : [RequestAdapter .format_record (r , api_version ) for r in records ]}
205+
206+ @staticmethod
207+ def format_delete (record_id : int | str , api_version : "APIVersion" ) -> dict [str , Any ]:
208+ """Format a delete request body.
209+
210+ v2: {"Id": 123}
211+ v3: {"id": 123}
212+ """
213+ if api_version == APIVersion .V2 :
214+ return {"Id" : record_id }
215+ return {"id" : record_id }
216+
217+ @staticmethod
218+ def format_bulk_delete (
219+ record_ids : list [int | str ], api_version : "APIVersion"
220+ ) -> list [dict [str , Any ]] | dict [str , Any ]:
221+ """Format bulk delete request body.
222+
223+ v2: [{"Id": 1}, {"Id": 2}]
224+ v3: {"records": [{"id": 1}, {"id": 2}]}
225+ """
226+ if api_version == APIVersion .V2 :
227+ return [{"Id" : rid } for rid in record_ids ]
228+ return {"records" : [{"id" : rid } for rid in record_ids ]}
229+
230+
40231class QueryParamAdapter :
41232 """Adapter for converting query parameters between API versions."""
42233
0 commit comments