@@ -11,6 +11,9 @@ import com.ctrlhub.core.projects.workorders.response.WorkOrder
1111import com.fasterxml.jackson.annotation.JsonCreator
1212import com.fasterxml.jackson.annotation.JsonIgnoreProperties
1313import com.fasterxml.jackson.annotation.JsonProperty
14+ import com.fasterxml.jackson.databind.DeserializationFeature
15+ import com.fasterxml.jackson.databind.ObjectMapper
16+ import com.fasterxml.jackson.module.kotlin.kotlinModule
1417import com.github.jasminb.jsonapi.StringIdHandler
1518import com.github.jasminb.jsonapi.annotations.Id
1619import com.github.jasminb.jsonapi.annotations.Meta
@@ -59,11 +62,122 @@ class FormSubmissionVersion @JsonCreator constructor(
5962
6063 @Relationship(" payload_schemes" )
6164 var payloadSchemes : List <Scheme >? = null ,
62- )
65+
66+ // raw JSON:API resource envelopes as returned in the response (kept as Map for backward compatibility)
67+ @JsonProperty(" resources" )
68+ var resources : List <Map <String , Any >>? = null ,
69+ ) {
70+
71+ // shared Jackson mapper configured to ignore unknown properties when hydrating attribute maps
72+ private fun resourceMapper (): ObjectMapper = Companion .mapper
73+
74+ /* *
75+ * Convert the raw resources list (List<Map<...>>) into typed JsonApiEnvelope objects.
76+ * This keeps the original raw structure but provides a typed view over it.
77+ */
78+ fun resourcesAsEnvelopes (): List <JsonApiEnvelope > = resources?.mapNotNull { res ->
79+ try {
80+ resourceMapper().convertValue(res, JsonApiEnvelope ::class .java)
81+ } catch (e: Exception ) {
82+ null
83+ }
84+ } ? : emptyList()
85+
86+ /* *
87+ * Find the full envelope for a resource by id.
88+ */
89+ fun findResourceEnvelopeById (id : String ): JsonApiEnvelope ? = resourcesAsEnvelopes().firstOrNull { it.data?.id == id }
90+
91+ /* *
92+ * Find the inner resource data object by id.
93+ */
94+ fun findResourceDataById (id : String ): JsonApiResourceData ? = findResourceEnvelopeById(id)?.data
95+
96+ /* *
97+ * Hydrate the attributes of a resource (by id) into a target class using Jackson.
98+ * Example: hydrateResourceAttributesById("...", Image::class.java)
99+ * Returns null if resource or attributes are missing or conversion fails.
100+ */
101+ fun <T > hydrateResourceAttributesById (id : String , clazz : Class <T >): T ? {
102+ val attrs = findResourceDataById(id)?.attributes ? : return null
103+ return try {
104+ resourceMapper().convertValue(attrs, clazz)
105+ } catch (e: Exception ) {
106+ null
107+ }
108+ }
109+
110+ /* *
111+ * Auto-hydrate a resource by looking up its JSON:API type and using the registered class for that type.
112+ * Returns the hydrated object or null if not registered or conversion fails.
113+ */
114+ fun autoHydrateById (id : String ): Any? {
115+ val env = findResourceEnvelopeById(id) ? : return null
116+ val type = env.data?.type ? : return null
117+ val clazz = Companion .getRegisteredClass(type) ? : return null
118+ return hydrateResourceAttributesById(id, clazz)
119+ }
120+
121+ /* *
122+ * Reified convenience that attempts to auto-hydrate and cast to the expected type.
123+ */
124+ inline fun <reified T > autoHydrateByIdAs (id : String ): T ? {
125+ val any = autoHydrateById(id) ? : return null
126+ return any as ? T
127+ }
128+
129+ /* *
130+ * Backwards-compatible helper: original simple lookup that returns the inner "data" map.
131+ */
132+ fun findResourceById (id : String ): Map <String , Any >? {
133+ resources?.forEach { res ->
134+ val data = res[" data" ] as ? Map <* , * > ? : return @forEach
135+ val dataId = data[" id" ] as ? String
136+ if (dataId == id) {
137+ @Suppress(" UNCHECKED_CAST" )
138+ return data as Map <String , Any >
139+ }
140+ }
141+ return null
142+ }
143+
144+ companion object {
145+ private val mapper: ObjectMapper = ObjectMapper ()
146+ .registerModule(kotlinModule())
147+ .configure(DeserializationFeature .FAIL_ON_UNKNOWN_PROPERTIES , false )
148+
149+ // simple type registry mapping jsonapi resource "type" -> Class
150+ private val typeRegistry: MutableMap <String , Class <* >> = mutableMapOf ()
151+
152+ fun registerResourceType (type : String , clazz : Class <* >) {
153+ typeRegistry[type] = clazz
154+ }
155+
156+ fun getRegisteredClass (type : String ): Class <* >? = typeRegistry[type]
157+ }
158+ }
63159
64160@JsonIgnoreProperties(ignoreUnknown = true )
65161class FormSubmissionVersionMeta (
66162 @JsonProperty(" created_at" ) val createdAt : LocalDateTime ? = null ,
67163 @JsonProperty(" latest" ) val latest : String? = null ,
68164 @JsonProperty(" is_latest" ) val isLatest : Boolean? = null ,
69165)
166+
167+ /* *
168+ * Lightweight typed representations for JSON:API resource envelopes and resource data.
169+ * These are intentionally simple (attributes are a Map) to support arbitrary resource types.
170+ */
171+ @JsonIgnoreProperties(ignoreUnknown = true )
172+ data class JsonApiResourceData (
173+ @JsonProperty(" id" ) val id : String? = null ,
174+ @JsonProperty(" type" ) val type : String? = null ,
175+ @JsonProperty(" attributes" ) val attributes : Map <String , Any >? = null ,
176+ @JsonProperty(" relationships" ) val relationships : Map <String , Any >? = null ,
177+ )
178+
179+ @JsonIgnoreProperties(ignoreUnknown = true )
180+ data class JsonApiEnvelope (
181+ @JsonProperty(" data" ) val data : JsonApiResourceData ? = null ,
182+ @JsonProperty(" jsonapi" ) val jsonapi : Map <String , Any >? = null ,
183+ )
0 commit comments