rez_core 6.5.23 → 6.5.26

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (395) hide show
  1. package/dist/app.module.js +3 -3
  2. package/dist/app.module.js.map +1 -1
  3. package/dist/core.module.js +38 -60
  4. package/dist/core.module.js.map +1 -1
  5. package/dist/migrations/1732612800000-AddEntityJsonGinIndex.d.ts +6 -0
  6. package/dist/migrations/1732612800000-AddEntityJsonGinIndex.js +32 -0
  7. package/dist/migrations/1732612800000-AddEntityJsonGinIndex.js.map +1 -0
  8. package/dist/module/auth/strategies/jwt.strategy.d.ts +1 -2
  9. package/dist/module/auth/strategies/jwt.strategy.js +2 -3
  10. package/dist/module/auth/strategies/jwt.strategy.js.map +1 -1
  11. package/dist/module/dashboard/dashboard.module.js +1 -1
  12. package/dist/module/dashboard/dashboard.module.js.map +1 -1
  13. package/dist/module/dashboard/service/dashboard.service.js +3 -2
  14. package/dist/module/dashboard/service/dashboard.service.js.map +1 -1
  15. package/dist/module/enterprise/controller/organization.controller.d.ts +4 -12
  16. package/dist/module/enterprise/controller/organization.controller.js +8 -64
  17. package/dist/module/enterprise/controller/organization.controller.js.map +1 -1
  18. package/dist/module/enterprise/enterprise.module.js +15 -10
  19. package/dist/module/enterprise/enterprise.module.js.map +1 -1
  20. package/dist/module/enterprise/entity/enterprise.entity.d.ts +3 -1
  21. package/dist/module/enterprise/entity/enterprise.entity.js +12 -4
  22. package/dist/module/enterprise/entity/enterprise.entity.js.map +1 -1
  23. package/dist/module/enterprise/entity/organization-app-mapping.entity.d.ts +1 -6
  24. package/dist/module/enterprise/entity/organization-app-mapping.entity.js +4 -21
  25. package/dist/module/enterprise/entity/organization-app-mapping.entity.js.map +1 -1
  26. package/dist/module/enterprise/entity/organization.entity.d.ts +18 -3
  27. package/dist/module/enterprise/entity/organization.entity.js +74 -8
  28. package/dist/module/enterprise/entity/organization.entity.js.map +1 -1
  29. package/dist/module/enterprise/repository/enterprise.repository.d.ts +2 -4
  30. package/dist/module/enterprise/repository/enterprise.repository.js +4 -19
  31. package/dist/module/enterprise/repository/enterprise.repository.js.map +1 -1
  32. package/dist/module/enterprise/service/brand.service.d.ts +3 -0
  33. package/dist/module/enterprise/service/brand.service.js +17 -0
  34. package/dist/module/enterprise/service/brand.service.js.map +1 -1
  35. package/dist/module/enterprise/service/enterprise.service.d.ts +2 -2
  36. package/dist/module/enterprise/service/enterprise.service.js +4 -4
  37. package/dist/module/enterprise/service/enterprise.service.js.map +1 -1
  38. package/dist/module/enterprise/service/organization.service.d.ts +4 -4
  39. package/dist/module/enterprise/service/organization.service.js +24 -101
  40. package/dist/module/enterprise/service/organization.service.js.map +1 -1
  41. package/dist/module/entity_json/controller/entity_json.controller.d.ts +2 -9
  42. package/dist/module/entity_json/controller/entity_json.controller.js.map +1 -1
  43. package/dist/module/entity_json/entity/entityJson.entity.d.ts +2 -1
  44. package/dist/module/entity_json/entity/entityJson.entity.js +5 -1
  45. package/dist/module/entity_json/entity/entityJson.entity.js.map +1 -1
  46. package/dist/module/entity_json/entity_json.module.js +7 -2
  47. package/dist/module/entity_json/entity_json.module.js.map +1 -1
  48. package/dist/module/entity_json/service/entity_json.service.d.ts +2 -10
  49. package/dist/module/entity_json/service/entity_json.service.js +97 -23
  50. package/dist/module/entity_json/service/entity_json.service.js.map +1 -1
  51. package/dist/module/filter/controller/filter.controller.d.ts +12 -0
  52. package/dist/module/filter/controller/filter.controller.js +1 -1
  53. package/dist/module/filter/controller/filter.controller.js.map +1 -1
  54. package/dist/module/filter/filter.module.js +11 -2
  55. package/dist/module/filter/filter.module.js.map +1 -1
  56. package/dist/module/filter/service/filter.service.d.ts +38 -2
  57. package/dist/module/filter/service/filter.service.js +43 -50
  58. package/dist/module/filter/service/filter.service.js.map +1 -1
  59. package/dist/module/filter/service/flatjson-filter.service.d.ts +32 -0
  60. package/dist/module/filter/service/flatjson-filter.service.js +632 -0
  61. package/dist/module/filter/service/flatjson-filter.service.js.map +1 -0
  62. package/dist/module/filter/service/saved-filter.service.d.ts +3 -2
  63. package/dist/module/filter/service/saved-filter.service.js +14 -18
  64. package/dist/module/filter/service/saved-filter.service.js.map +1 -1
  65. package/dist/module/integration/service/integration.service.d.ts +1 -0
  66. package/dist/module/integration/service/integration.service.js +2 -1
  67. package/dist/module/integration/service/integration.service.js.map +1 -1
  68. package/dist/module/integration/service/wrapper.service.js +1 -0
  69. package/dist/module/integration/service/wrapper.service.js.map +1 -1
  70. package/dist/module/layout/controller/layout.controller.d.ts +3 -1
  71. package/dist/module/layout/controller/layout.controller.js +7 -3
  72. package/dist/module/layout/controller/layout.controller.js.map +1 -1
  73. package/dist/module/layout/entity/header-section.entity.d.ts +2 -0
  74. package/dist/module/layout/entity/header-section.entity.js +8 -0
  75. package/dist/module/layout/entity/header-section.entity.js.map +1 -1
  76. package/dist/module/layout/layout.module.js +2 -1
  77. package/dist/module/layout/layout.module.js.map +1 -1
  78. package/dist/module/layout/repository/header-section.repository.d.ts +1 -0
  79. package/dist/module/layout/repository/header-section.repository.js +5 -0
  80. package/dist/module/layout/repository/header-section.repository.js.map +1 -1
  81. package/dist/module/layout/service/header-section.service.d.ts +1 -1
  82. package/dist/module/layout/service/header-section.service.js +1 -1
  83. package/dist/module/layout/service/header-section.service.js.map +1 -1
  84. package/dist/module/linked_attributes/controller/linked_attributes.controller.d.ts +41 -0
  85. package/dist/module/linked_attributes/controller/linked_attributes.controller.js +90 -0
  86. package/dist/module/linked_attributes/controller/linked_attributes.controller.js.map +1 -1
  87. package/dist/module/linked_attributes/dto/create-linked-attribute-smart.dto.d.ts +13 -0
  88. package/dist/module/linked_attributes/dto/create-linked-attribute-smart.dto.js +64 -0
  89. package/dist/module/linked_attributes/dto/create-linked-attribute-smart.dto.js.map +1 -0
  90. package/dist/module/linked_attributes/linked_attributes.module.js +8 -1
  91. package/dist/module/linked_attributes/linked_attributes.module.js.map +1 -1
  92. package/dist/module/linked_attributes/service/linked_attributes.service.d.ts +65 -1
  93. package/dist/module/linked_attributes/service/linked_attributes.service.js +287 -2
  94. package/dist/module/linked_attributes/service/linked_attributes.service.js.map +1 -1
  95. package/dist/module/listmaster/service/list-master.service.js +8 -1
  96. package/dist/module/listmaster/service/list-master.service.js.map +1 -1
  97. package/dist/module/meta/controller/meta.controller.d.ts +6 -1
  98. package/dist/module/meta/controller/meta.controller.js +19 -1
  99. package/dist/module/meta/controller/meta.controller.js.map +1 -1
  100. package/dist/module/meta/entity/app-master.entity.d.ts +4 -8
  101. package/dist/module/meta/entity/app-master.entity.js +12 -30
  102. package/dist/module/meta/entity/app-master.entity.js.map +1 -1
  103. package/dist/module/meta/entity/entity-master.entity.d.ts +1 -0
  104. package/dist/module/meta/entity/entity-master.entity.js +8 -1
  105. package/dist/module/meta/entity/entity-master.entity.js.map +1 -1
  106. package/dist/module/meta/entity.module.js +5 -2
  107. package/dist/module/meta/entity.module.js.map +1 -1
  108. package/dist/module/meta/repository/app-master.repository.js +1 -1
  109. package/dist/module/meta/repository/app-master.repository.js.map +1 -1
  110. package/dist/module/meta/service/attribute-master.service.d.ts +6 -1
  111. package/dist/module/meta/service/attribute-master.service.js +22 -2
  112. package/dist/module/meta/service/attribute-master.service.js.map +1 -1
  113. package/dist/module/meta/service/entity-master.service.js +1 -0
  114. package/dist/module/meta/service/entity-master.service.js.map +1 -1
  115. package/dist/module/meta/service/entity-relation.service.d.ts +4 -3
  116. package/dist/module/meta/service/entity-relation.service.js +10 -4
  117. package/dist/module/meta/service/entity-relation.service.js.map +1 -1
  118. package/dist/module/meta/service/entity-service-impl.service.d.ts +1 -1
  119. package/dist/module/meta/service/entity-service-impl.service.js +14 -10
  120. package/dist/module/meta/service/entity-service-impl.service.js.map +1 -1
  121. package/dist/module/meta/service/entity-table.service.d.ts +2 -3
  122. package/dist/module/meta/service/entity-table.service.js +19 -23
  123. package/dist/module/meta/service/entity-table.service.js.map +1 -1
  124. package/dist/module/meta/service/populate-meta.service.d.ts +13 -0
  125. package/dist/module/{enterprise → meta}/service/populate-meta.service.js +8 -2
  126. package/dist/module/meta/service/populate-meta.service.js.map +1 -0
  127. package/dist/module/meta/service/resolver.service.d.ts +1 -1
  128. package/dist/module/meta/service/resolver.service.js +6 -3
  129. package/dist/module/meta/service/resolver.service.js.map +1 -1
  130. package/dist/module/module/controller/menu.controller.js +2 -2
  131. package/dist/module/module/controller/menu.controller.js.map +1 -1
  132. package/dist/module/module/controller/module-access.controller.d.ts +4 -3
  133. package/dist/module/module/controller/module-access.controller.js +10 -15
  134. package/dist/module/module/controller/module-access.controller.js.map +1 -1
  135. package/dist/module/module/entity/menu.entity.d.ts +2 -6
  136. package/dist/module/module/entity/menu.entity.js +7 -19
  137. package/dist/module/module/entity/menu.entity.js.map +1 -1
  138. package/dist/module/module/entity/module-access.entity.d.ts +0 -15
  139. package/dist/module/module/entity/module-access.entity.js +0 -50
  140. package/dist/module/module/entity/module-access.entity.js.map +1 -1
  141. package/dist/module/module/entity/module-action.entity.d.ts +1 -4
  142. package/dist/module/module/entity/module-action.entity.js +3 -12
  143. package/dist/module/module/entity/module-action.entity.js.map +1 -1
  144. package/dist/module/module/entity/module.entity.d.ts +5 -3
  145. package/dist/module/module/entity/module.entity.js +18 -8
  146. package/dist/module/module/entity/module.entity.js.map +1 -1
  147. package/dist/module/module/module.module.d.ts +1 -1
  148. package/dist/module/module/module.module.js +7 -5
  149. package/dist/module/module/module.module.js.map +1 -1
  150. package/dist/module/module/repository/menu.repository.d.ts +4 -4
  151. package/dist/module/module/repository/menu.repository.js +27 -17
  152. package/dist/module/module/repository/menu.repository.js.map +1 -1
  153. package/dist/module/module/repository/module-access.repository.d.ts +7 -7
  154. package/dist/module/module/repository/module-access.repository.js +44 -82
  155. package/dist/module/module/repository/module-access.repository.js.map +1 -1
  156. package/dist/module/module/service/menu.service.d.ts +5 -3
  157. package/dist/module/module/service/menu.service.js +12 -9
  158. package/dist/module/module/service/menu.service.js.map +1 -1
  159. package/dist/module/module/service/module-access.service.d.ts +11 -8
  160. package/dist/module/module/service/module-access.service.js +26 -24
  161. package/dist/module/module/service/module-access.service.js.map +1 -1
  162. package/dist/module/notification/entity/notification.entity.d.ts +2 -17
  163. package/dist/module/notification/entity/notification.entity.js +2 -68
  164. package/dist/module/notification/entity/notification.entity.js.map +1 -1
  165. package/dist/module/notification/notification.module.js +2 -0
  166. package/dist/module/notification/notification.module.js.map +1 -1
  167. package/dist/module/notification/service/notification.service.d.ts +3 -1
  168. package/dist/module/notification/service/notification.service.js +5 -2
  169. package/dist/module/notification/service/notification.service.js.map +1 -1
  170. package/dist/module/notification/service/otp.service.js.map +1 -1
  171. package/dist/module/user/controller/login.controller.d.ts +3 -1
  172. package/dist/module/user/controller/login.controller.js +6 -2
  173. package/dist/module/user/controller/login.controller.js.map +1 -1
  174. package/dist/module/user/controller/user.controller.d.ts +2 -0
  175. package/dist/module/user/controller/user.controller.js +13 -0
  176. package/dist/module/user/controller/user.controller.js.map +1 -1
  177. package/dist/module/user/dto/create-user.dto.d.ts +3 -6
  178. package/dist/module/user/dto/create-user.dto.js +11 -17
  179. package/dist/module/user/dto/create-user.dto.js.map +1 -1
  180. package/dist/module/user/entity/role.entity.d.ts +6 -19
  181. package/dist/module/user/entity/role.entity.js +19 -68
  182. package/dist/module/user/entity/role.entity.js.map +1 -1
  183. package/dist/module/user/entity/user-role-mapping.entity.d.ts +0 -10
  184. package/dist/module/user/entity/user-role-mapping.entity.js +1 -33
  185. package/dist/module/user/entity/user-role-mapping.entity.js.map +1 -1
  186. package/dist/module/user/entity/user-session.entity.js +3 -2
  187. package/dist/module/user/entity/user-session.entity.js.map +1 -1
  188. package/dist/module/user/entity/user.entity.d.ts +5 -19
  189. package/dist/module/user/entity/user.entity.js +15 -69
  190. package/dist/module/user/entity/user.entity.js.map +1 -1
  191. package/dist/module/user/repository/role.repository.d.ts +2 -6
  192. package/dist/module/user/repository/role.repository.js +8 -16
  193. package/dist/module/user/repository/role.repository.js.map +1 -1
  194. package/dist/module/user/repository/user-role-mapping.repository.d.ts +0 -1
  195. package/dist/module/user/repository/user-role-mapping.repository.js +0 -3
  196. package/dist/module/user/repository/user-role-mapping.repository.js.map +1 -1
  197. package/dist/module/user/repository/user.repository.d.ts +2 -4
  198. package/dist/module/user/repository/user.repository.js +7 -19
  199. package/dist/module/user/repository/user.repository.js.map +1 -1
  200. package/dist/module/user/repository/userSession.repository.d.ts +0 -1
  201. package/dist/module/user/repository/userSession.repository.js +0 -3
  202. package/dist/module/user/repository/userSession.repository.js.map +1 -1
  203. package/dist/module/user/service/login.service.d.ts +5 -3
  204. package/dist/module/user/service/login.service.js +42 -43
  205. package/dist/module/user/service/login.service.js.map +1 -1
  206. package/dist/module/user/service/role.service.d.ts +11 -24
  207. package/dist/module/user/service/role.service.js +38 -42
  208. package/dist/module/user/service/role.service.js.map +1 -1
  209. package/dist/module/user/service/user-role-mapping.service.d.ts +0 -2
  210. package/dist/module/user/service/user-role-mapping.service.js +0 -6
  211. package/dist/module/user/service/user-role-mapping.service.js.map +1 -1
  212. package/dist/module/user/service/user-session.service.d.ts +1 -2
  213. package/dist/module/user/service/user-session.service.js +5 -9
  214. package/dist/module/user/service/user-session.service.js.map +1 -1
  215. package/dist/module/user/service/user.service.d.ts +20 -29
  216. package/dist/module/user/service/user.service.js +56 -44
  217. package/dist/module/user/service/user.service.js.map +1 -1
  218. package/dist/module/user/user.module.js +7 -1
  219. package/dist/module/user/user.module.js.map +1 -1
  220. package/dist/module/workflow/repository/action-data.repository.d.ts +1 -1
  221. package/dist/module/workflow/repository/action-data.repository.js +8 -6
  222. package/dist/module/workflow/repository/action-data.repository.js.map +1 -1
  223. package/dist/module/workflow/repository/action.repository.d.ts +1 -1
  224. package/dist/module/workflow/repository/action.repository.js +10 -10
  225. package/dist/module/workflow/repository/action.repository.js.map +1 -1
  226. package/dist/module/workflow/repository/form-master.repository.d.ts +1 -1
  227. package/dist/module/workflow/repository/form-master.repository.js +2 -2
  228. package/dist/module/workflow/repository/form-master.repository.js.map +1 -1
  229. package/dist/module/workflow/service/action-data.service.js +2 -1
  230. package/dist/module/workflow/service/action-data.service.js.map +1 -1
  231. package/dist/module/workflow/service/action.service.js +2 -2
  232. package/dist/module/workflow/service/action.service.js.map +1 -1
  233. package/dist/module/workflow/service/comm-template.service.js +2 -0
  234. package/dist/module/workflow/service/comm-template.service.js.map +1 -1
  235. package/dist/module/workflow/service/entity-modification.service.js +1 -0
  236. package/dist/module/workflow/service/entity-modification.service.js.map +1 -1
  237. package/dist/module/workflow/service/form-master.service.js +2 -2
  238. package/dist/module/workflow/service/form-master.service.js.map +1 -1
  239. package/dist/module/workflow/service/populate-workflow.service.d.ts +1 -1
  240. package/dist/module/workflow/service/populate-workflow.service.js +1 -1
  241. package/dist/module/workflow/service/populate-workflow.service.js.map +1 -1
  242. package/dist/module/workflow/service/task.service.js +3 -0
  243. package/dist/module/workflow/service/task.service.js.map +1 -1
  244. package/dist/module/workflow/service/workflow-meta.service.js +7 -2
  245. package/dist/module/workflow/service/workflow-meta.service.js.map +1 -1
  246. package/dist/module/workflow/service/workflow.service.js +2 -2
  247. package/dist/module/workflow/service/workflow.service.js.map +1 -1
  248. package/dist/module/workflow/workflow.module.js +0 -2
  249. package/dist/module/workflow/workflow.module.js.map +1 -1
  250. package/dist/module/workflow-automation/service/workflow-automation.service.js +6 -11
  251. package/dist/module/workflow-automation/service/workflow-automation.service.js.map +1 -1
  252. package/dist/module/workflow-automation/workflow-automation.module.js +1 -3
  253. package/dist/module/workflow-automation/workflow-automation.module.js.map +1 -1
  254. package/dist/module/workflow-schedule/service/workflow-schedule.service.js +2 -0
  255. package/dist/module/workflow-schedule/service/workflow-schedule.service.js.map +1 -1
  256. package/dist/table.config.d.ts +3 -2
  257. package/dist/tsconfig.build.tsbuildinfo +1 -1
  258. package/package.json +2 -2
  259. package/src/app.module.ts +5 -5
  260. package/src/core.module.ts +44 -55
  261. package/src/migrations/1732612800000-AddEntityJsonGinIndex.ts +41 -0
  262. package/src/module/auth/strategies/jwt.strategy.ts +2 -4
  263. package/src/module/dashboard/dashboard.module.ts +3 -3
  264. package/src/module/dashboard/service/dashboard.service.ts +3 -2
  265. package/src/module/enterprise/controller/organization.controller.ts +4 -60
  266. package/src/module/enterprise/enterprise.module.ts +18 -16
  267. package/src/module/enterprise/entity/enterprise.entity.ts +11 -5
  268. package/src/module/enterprise/entity/organization-app-mapping.entity.ts +4 -18
  269. package/src/module/enterprise/entity/organization.entity.ts +59 -9
  270. package/src/module/enterprise/repository/enterprise.repository.ts +4 -26
  271. package/src/module/enterprise/service/brand.service.ts +5 -75
  272. package/src/module/enterprise/service/enterprise.service.ts +4 -10
  273. package/src/module/enterprise/service/organization.service.ts +23 -140
  274. package/src/module/entity_json/controller/entity_json.controller.ts +13 -0
  275. package/src/module/entity_json/docs/FlatJson_Filterin_System.md +2804 -0
  276. package/src/module/entity_json/entity/entityJson.entity.ts +4 -1
  277. package/src/module/entity_json/entity_json.module.ts +9 -5
  278. package/src/module/entity_json/service/entity_json.service.ts +232 -49
  279. package/src/module/filter/controller/filter.controller.ts +1 -3
  280. package/src/module/filter/filter.module.ts +12 -3
  281. package/src/module/filter/service/filter.service.ts +130 -73
  282. package/src/module/filter/service/flatjson-filter.service.ts +903 -0
  283. package/src/module/filter/service/saved-filter.service.ts +16 -26
  284. package/src/module/filter/test/flatjson-filter.service.spec.ts +415 -0
  285. package/src/module/integration/service/integration.service.ts +6 -2
  286. package/src/module/integration/service/wrapper.service.ts +1 -0
  287. package/src/module/layout/controller/layout.controller.ts +8 -1
  288. package/src/module/layout/entity/header-section.entity.ts +6 -0
  289. package/src/module/layout/layout.module.ts +1 -1
  290. package/src/module/layout/repository/header-section.repository.ts +6 -0
  291. package/src/module/layout/service/header-section.service.ts +1 -1
  292. package/src/module/linked_attributes/controller/linked_attributes.controller.ts +100 -0
  293. package/src/module/linked_attributes/dto/create-linked-attribute-smart.dto.ts +54 -0
  294. package/src/module/linked_attributes/linked_attributes.module.ts +9 -2
  295. package/src/module/linked_attributes/service/linked_attributes.service.ts +578 -3
  296. package/src/module/linked_attributes/test/linked-attributes.service.spec.ts +244 -0
  297. package/src/module/listmaster/service/list-master.service.ts +9 -1
  298. package/src/module/meta/controller/meta.controller.ts +25 -3
  299. package/src/module/meta/entity/app-master.entity.ts +9 -22
  300. package/src/module/meta/entity/entity-master.entity.ts +9 -3
  301. package/src/module/meta/entity.module.ts +6 -5
  302. package/src/module/meta/repository/app-master.repository.ts +1 -1
  303. package/src/module/meta/service/attribute-master.service.ts +31 -1
  304. package/src/module/meta/service/entity-master.service.ts +1 -0
  305. package/src/module/meta/service/entity-relation.service.ts +10 -6
  306. package/src/module/meta/service/entity-service-impl.service.ts +14 -19
  307. package/src/module/meta/service/entity-table.service.ts +28 -20
  308. package/src/module/meta/service/entity.service.ts +0 -1
  309. package/src/module/{enterprise → meta}/service/populate-meta.service.ts +5 -2
  310. package/src/module/meta/service/resolver.service.ts +4 -0
  311. package/src/module/module/controller/menu.controller.ts +2 -2
  312. package/src/module/module/controller/module-access.controller.ts +11 -16
  313. package/src/module/module/entity/menu.entity.ts +7 -17
  314. package/src/module/module/entity/module-access.entity.ts +1 -41
  315. package/src/module/module/entity/module-action.entity.ts +4 -11
  316. package/src/module/module/entity/module.entity.ts +14 -7
  317. package/src/module/module/module.module.ts +3 -2
  318. package/src/module/module/repository/menu.repository.ts +31 -19
  319. package/src/module/module/repository/module-access.repository.ts +60 -97
  320. package/src/module/module/service/menu.service.ts +13 -11
  321. package/src/module/module/service/module-access.service.ts +38 -26
  322. package/src/module/notification/entity/notification.entity.ts +3 -53
  323. package/src/module/notification/notification.module.ts +2 -0
  324. package/src/module/notification/service/notification.service.ts +1 -0
  325. package/src/module/notification/service/otp.service.ts +2 -3
  326. package/src/module/user/controller/login.controller.ts +8 -7
  327. package/src/module/user/controller/user.controller.ts +9 -0
  328. package/src/module/user/dto/create-user.dto.ts +6 -19
  329. package/src/module/user/entity/role.entity.ts +16 -62
  330. package/src/module/user/entity/user-role-mapping.entity.ts +3 -29
  331. package/src/module/user/entity/user-session.entity.ts +4 -3
  332. package/src/module/user/entity/user.entity.ts +13 -54
  333. package/src/module/user/repository/role.repository.ts +12 -24
  334. package/src/module/user/repository/user-role-mapping.repository.ts +1 -5
  335. package/src/module/user/repository/user.repository.ts +8 -28
  336. package/src/module/user/repository/userSession.repository.ts +1 -5
  337. package/src/module/user/service/login.service.ts +51 -47
  338. package/src/module/user/service/role.service.ts +60 -53
  339. package/src/module/user/service/user-role-mapping.service.ts +1 -23
  340. package/src/module/user/service/user-session.service.ts +6 -12
  341. package/src/module/user/service/user.service.ts +91 -64
  342. package/src/module/user/user.module.ts +5 -3
  343. package/src/module/workflow/repository/action-data.repository.ts +8 -6
  344. package/src/module/workflow/repository/action.repository.ts +11 -11
  345. package/src/module/workflow/repository/form-master.repository.ts +2 -2
  346. package/src/module/workflow/service/action-data.service.ts +2 -3
  347. package/src/module/workflow/service/action.service.ts +2 -2
  348. package/src/module/workflow/service/comm-template.service.ts +2 -0
  349. package/src/module/workflow/service/entity-modification.service.ts +1 -0
  350. package/src/module/workflow/service/form-master.service.ts +2 -2
  351. package/src/module/workflow/service/populate-workflow.service.ts +1 -1
  352. package/src/module/workflow/service/task.service.ts +3 -0
  353. package/src/module/workflow/service/workflow-meta.service.ts +7 -2
  354. package/src/module/workflow/service/workflow.service.ts +2 -2
  355. package/src/module/workflow/workflow.module.ts +0 -2
  356. package/src/module/workflow-automation/service/workflow-automation.service.ts +7 -19
  357. package/src/module/workflow-automation/workflow-automation.module.ts +3 -4
  358. package/src/module/workflow-schedule/service/workflow-schedule.service.ts +2 -0
  359. package/src/resources/dev.properties.yaml +2 -2
  360. package/.claude/settings.local.json +0 -26
  361. package/.idea/250218_nodejs_core.iml +0 -9
  362. package/.idea/codeStyles/Project.xml +0 -59
  363. package/.idea/codeStyles/codeStyleConfig.xml +0 -5
  364. package/.idea/copilot.data.migration.agent.xml +0 -6
  365. package/.idea/copilot.data.migration.ask.xml +0 -6
  366. package/.idea/copilot.data.migration.ask2agent.xml +0 -6
  367. package/.idea/copilot.data.migration.edit.xml +0 -6
  368. package/.idea/inspectionProfiles/Project_Default.xml +0 -6
  369. package/.idea/misc.xml +0 -6
  370. package/.idea/modules.xml +0 -8
  371. package/.idea/prettier.xml +0 -6
  372. package/.idea/vcs.xml +0 -6
  373. package/dist/constant/status.constant.d.ts +0 -4
  374. package/dist/constant/status.constant.js +0 -9
  375. package/dist/constant/status.constant.js.map +0 -1
  376. package/dist/module/enterprise/controller/enterprise.controller.d.ts +0 -12
  377. package/dist/module/enterprise/controller/enterprise.controller.js +0 -57
  378. package/dist/module/enterprise/controller/enterprise.controller.js.map +0 -1
  379. package/dist/module/enterprise/controller/meta.controller.d.ts +0 -9
  380. package/dist/module/enterprise/controller/meta.controller.js +0 -43
  381. package/dist/module/enterprise/controller/meta.controller.js.map +0 -1
  382. package/dist/module/enterprise/service/brand-profile.service.d.ts +0 -0
  383. package/dist/module/enterprise/service/brand-profile.service.js +0 -1
  384. package/dist/module/enterprise/service/brand-profile.service.js.map +0 -1
  385. package/dist/module/enterprise/service/populate-meta.service.d.ts +0 -9
  386. package/dist/module/enterprise/service/populate-meta.service.js.map +0 -1
  387. package/dist/module/enterprise/service/school.service.d.ts +0 -0
  388. package/dist/module/enterprise/service/school.service.js +0 -1
  389. package/dist/module/enterprise/service/school.service.js.map +0 -1
  390. package/server.log +0 -850
  391. package/src/constant/status.constant.ts +0 -4
  392. package/src/module/enterprise/controller/enterprise.controller.ts +0 -40
  393. package/src/module/enterprise/controller/meta.controller.ts +0 -23
  394. package/src/module/enterprise/service/brand-profile.service.ts +0 -10
  395. package/src/module/enterprise/service/school.service.ts +0 -5
@@ -0,0 +1,2804 @@
1
+ # Flatjson Filtering System - Complete Implementation Guide
2
+
3
+ ## Project Overview
4
+ We are migrating from MySQL to PostgreSQL and implementing a sophisticated flatjson system for optimized entity data querying. This involves:
5
+ 1. Automating Linked Attribute creation (custom 1:M to 1:1 mappings)
6
+ 2. Building a new filtering service that queries JSONB flatjson directly
7
+ 3. Maintaining backward compatibility with existing FilterService
8
+
9
+ ## Technology Stack
10
+ - **Backend:** Node.js 18+, TypeScript 5.x, NestJS
11
+ - **Database:** PostgreSQL 14+ with JSONB support
12
+ - **ORM:** TypeORM
13
+ - **Testing:** Jest
14
+
15
+ ## Project Structure
16
+ ```
17
+ src/
18
+ ├── module/
19
+ │ ├── filter/ # Existing filter logic (keep intact)
20
+ │ │ ├── service/
21
+ │ │ │ ├── filter.service.ts
22
+ │ │ │ ├── filter-evaluator.service.ts
23
+ │ │ │ └── saved-filter.service.ts
24
+ │ │ ├── entity/
25
+ │ │ └── dto/
26
+ │ ├── linked_attributes/ # Custom 1:M attribute management
27
+ │ │ ├── service/
28
+ │ │ │ └── linked_attributes.service.ts
29
+ │ │ ├── entity/
30
+ │ │ │ └── linked_attribute.entity.ts
31
+ │ │ └── controller/
32
+ │ ├── entity_json/ # Flatjson building and storage
33
+ │ │ ├── service/
34
+ │ │ │ └── entity_json.service.ts
35
+ │ │ ├── entity/
36
+ │ │ │ └── entityJson.entity.ts
37
+ │ │ └── repository/
38
+ │ └── meta/ # Metadata services
39
+ ├── migrations/ # Database migrations
40
+ └── common/ # Shared utilities
41
+ ```
42
+
43
+ ## Key Conventions
44
+ - **Flatjson Keys:** `ENTITY__attribute_key` or `ENTITY__attribute_key__sequence`
45
+ - Example: `LEAD__name`, `LFMG__name__1`, `LFMG__name__2`
46
+ - **Text Storage:** All text stored in lowercase
47
+ - **Date Storage:** Epoch milliseconds (bigint)
48
+ - **Multi-select:** Stored as JSON arrays
49
+ - **Naming:** PascalCase for classes, camelCase for methods
50
+ - **Async:** Always use async/await, no callbacks
51
+ - **Types:** Explicit TypeScript types, avoid `any`
52
+ - **Comments:** JSDoc for all public methods
53
+
54
+ ## Database Tables
55
+ - `frm_linked_attribute` - Custom 1:M attribute definitions
56
+ - `frm_entity_json` - Stores flatjson data (entity_type, entity_id, json_data)
57
+ - `frm_saved_filter_master` - Saved filter definitions
58
+ - `frm_saved_filter_detail` - Saved filter conditions
59
+ - `frm_entity_master` - Entity metadata
60
+ - `frm_attribute_master` - Attribute metadata
61
+
62
+ ---
63
+
64
+ # PHASE 1: LINKED ATTRIBUTE AUTOMATION
65
+ **Priority: CRITICAL | Duration: 12-15 hours**
66
+
67
+ ## Task 1.1: Sequence & Key Generation Logic
68
+ **Duration: 2 hours | Files: linked_attributes.service.ts**
69
+
70
+ ### Objective
71
+ Add methods to automatically generate sequence numbers and attribute keys for linked attributes.
72
+
73
+ ### Requirements
74
+
75
+ 1. **Add private async method: generateNextSequence()**
76
+ ```typescript
77
+ /**
78
+ * Generate the next sequence number for a linked attribute
79
+ * @param mapped_entity_type - Main entity type (e.g., "LEAD")
80
+ * @param applicable_entity_type - Source 1:M entity (e.g., "LFMG")
81
+ * @param applicable_attribute_key - Source attribute (e.g., "name")
82
+ * @param organization_id - Organization ID
83
+ * @returns Next available sequence number
84
+ */
85
+ private async generateNextSequence(
86
+ mapped_entity_type: string,
87
+ applicable_entity_type: string,
88
+ applicable_attribute_key: string,
89
+ organization_id: number
90
+ ): Promise<number>
91
+ ```
92
+
93
+ **Implementation:**
94
+ - Query `frm_linked_attribute` table
95
+ - Find MAX(sequence) for the given combination
96
+ - Return max + 1, or 1 if no records exist
97
+ - Use TypeORM QueryBuilder pattern (as seen in existing methods)
98
+
99
+ 2. **Add public method: generateAttributeKey()**
100
+ ```typescript
101
+ /**
102
+ * Generate a flatjson attribute key in format: ENTITY__attribute__N
103
+ * @param applicable_entity_type - Entity code (e.g., "LFMG")
104
+ * @param applicable_attribute_key - Attribute key (e.g., "name")
105
+ * @param sequence - Sequence number (e.g., 1)
106
+ * @returns Generated key (e.g., "LFMG__name__1")
107
+ * @example generateAttributeKey('LFMG', 'name', 1) => 'LFMG__name__1'
108
+ */
109
+ public generateAttributeKey(
110
+ applicable_entity_type: string,
111
+ applicable_attribute_key: string,
112
+ sequence: number
113
+ ): string
114
+ ```
115
+
116
+ **Implementation:**
117
+ - Validate inputs (throw error if invalid)
118
+ - Return formatted string: `${applicable_entity_type}__${applicable_attribute_key}__${sequence}`
119
+
120
+ ### Testing Requirements
121
+ - Create unit test file: `linked_attributes.service.spec.ts` (if not exists)
122
+ - Test cases:
123
+ - generateNextSequence() with no existing records → returns 1
124
+ - generateNextSequence() with existing records → returns max + 1
125
+ - generateAttributeKey() with valid inputs → correct format
126
+ - generateAttributeKey() with invalid inputs → throws error
127
+
128
+ ### Acceptance Criteria
129
+ - [ ] Both methods implemented and working
130
+ - [ ] TypeScript types are explicit (no `any`)
131
+ - [ ] JSDoc comments added
132
+ - [ ] Unit tests pass
133
+ - [ ] No breaking changes to existing code
134
+
135
+ ---
136
+
137
+ ## Task 1.2: Validation Engine
138
+ **Duration: 3 hours | Files: linked_attributes.service.ts**
139
+
140
+ ### Objective
141
+ Add comprehensive validation logic to prevent invalid linked attributes from being created.
142
+
143
+ ### Requirements
144
+
145
+ 1. **Add public async method: validateLinkedAttribute()**
146
+ ```typescript
147
+ /**
148
+ * Validate linked attribute payload before creation
149
+ * @param payload - Linked attribute data to validate
150
+ * @param loggedInUser - Current user context
151
+ * @returns Validation result with errors array
152
+ */
153
+ async validateLinkedAttribute(
154
+ payload: {
155
+ field_name: string;
156
+ mapped_entity_type: string;
157
+ applicable_entity_type: string;
158
+ applicable_attribute_key: string;
159
+ attribute_key?: string;
160
+ organization_id: number;
161
+ },
162
+ loggedInUser: any
163
+ ): Promise<{ valid: boolean; errors: string[] }>
164
+ ```
165
+
166
+ 2. **Validation Checks (implement as private helper methods):**
167
+
168
+ a. **checkDuplicateAttributeKey()**
169
+ - Query `frm_linked_attribute` for existing attribute_key
170
+ - Error: "Attribute key {key} already exists for entity {entity}"
171
+
172
+ b. **validateEntityTypeExists()**
173
+ - Query `frm_entity_master` to verify entity_type exists
174
+ - Error: "Entity type {type} does not exist"
175
+ - Inject `EntityMasterService` if available, or query directly
176
+
177
+ c. **validateAttributeKeyExists()**
178
+ - Query `frm_attribute_master` to verify attribute exists
179
+ - Error: "Attribute {key} does not exist in entity {entity}"
180
+ - Inject `AttributeMasterService` if available, or query directly
181
+
182
+ d. **validateRequiredFields()**
183
+ - Check field_name, mapped_entity_type, applicable_entity_type, applicable_attribute_key
184
+ - Error: "Required field {field} is missing or empty"
185
+
186
+ 3. **Aggregate all validations:**
187
+ ```typescript
188
+ const errors: string[] = [];
189
+
190
+ // Run all validations
191
+ await Promise.all([
192
+ this.checkDuplicateAttributeKey(...).then(err => err && errors.push(err)),
193
+ this.validateEntityTypeExists(...).then(err => err && errors.push(err)),
194
+ this.validateAttributeKeyExists(...).then(err => err && errors.push(err)),
195
+ ]);
196
+
197
+ this.validateRequiredFields(payload, errors);
198
+
199
+ return { valid: errors.length === 0, errors };
200
+ ```
201
+
202
+ ### Dependencies to Inject
203
+ You may need to inject these services (add to constructor):
204
+ ```typescript
205
+ @Inject() private readonly entityMasterService: EntityMasterService,
206
+ @Inject() private readonly attributeMasterService: AttributeMasterService,
207
+ ```
208
+
209
+ ### Testing Requirements
210
+ - Test each validation method independently
211
+ - Test validateLinkedAttribute() with:
212
+ - All valid data → returns { valid: true, errors: [] }
213
+ - Duplicate key → returns error
214
+ - Invalid entity type → returns error
215
+ - Invalid attribute → returns error
216
+ - Multiple errors → returns all errors
217
+
218
+ ### Acceptance Criteria
219
+ - [ ] All validation methods implemented
220
+ - [ ] Validation aggregates multiple errors
221
+ - [ ] Clear, user-friendly error messages
222
+ - [ ] Unit tests pass
223
+ - [ ] No performance issues (validations run in parallel)
224
+
225
+ ---
226
+
227
+ ## Task 1.3: Auto SavedFilter Creation
228
+ **Duration: 4 hours | Files: linked_attributes.service.ts**
229
+
230
+ ### Objective
231
+ Automatically create SavedFilter records when a linked attribute needs filter conditions.
232
+
233
+ ### Requirements
234
+
235
+ 1. **Inject dependencies:**
236
+ ```typescript
237
+ @Inject() private readonly savedFilterService: SavedFilterService,
238
+ @Inject() private readonly savedFilterDetailRepository: SavedFilterDetailRepository,
239
+ ```
240
+
241
+ 2. **Add private async method: createFilterForLinkedAttribute()**
242
+ ```typescript
243
+ /**
244
+ * Create a SavedFilter for a linked attribute
245
+ * @param entity_type - Entity type to filter (e.g., "LFMG")
246
+ * @param conditions - Array of filter conditions
247
+ * @param attribute_key - Linked attribute key (for naming)
248
+ * @param loggedInUser - Current user context
249
+ * @returns Generated filter code
250
+ */
251
+ private async createFilterForLinkedAttribute(
252
+ entity_type: string,
253
+ conditions: Array<{
254
+ filter_attribute: string;
255
+ filter_operator: string;
256
+ filter_value: any;
257
+ }>,
258
+ attribute_key: string,
259
+ loggedInUser: any
260
+ ): Promise<string>
261
+ ```
262
+
263
+ 3. **Implementation Steps:**
264
+
265
+ a. Generate unique filter code:
266
+ ```typescript
267
+ const timestamp = Date.now();
268
+ const filterCode = `LA_${entity_type}_${attribute_key}_${timestamp}`;
269
+ ```
270
+
271
+ b. Create SavedFilterMaster:
272
+ ```typescript
273
+ const filterMaster = await this.savedFilterService.createEntity({
274
+ name: `Auto Filter - ${attribute_key}`,
275
+ code: filterCode,
276
+ mapped_entity_type: entity_type,
277
+ filter_scope: 'FILTER',
278
+ organization_id: loggedInUser.organization_id,
279
+ level_type: loggedInUser.level_type,
280
+ level_id: loggedInUser.level_id,
281
+ }, loggedInUser);
282
+ ```
283
+
284
+ c. Create SavedFilterDetail records (bulk or loop):
285
+ ```typescript
286
+ for (const condition of conditions) {
287
+ await this.savedFilterDetailRepository.save({
288
+ mapped_filter_code: filterCode,
289
+ filter_entity_type: entity_type,
290
+ filter_attribute: condition.filter_attribute,
291
+ filter_operator: condition.filter_operator,
292
+ filter_value: String(condition.filter_value),
293
+ organization_id: loggedInUser.organization_id,
294
+ // Add other required fields from SavedFilterDetail entity
295
+ });
296
+ }
297
+ ```
298
+
299
+ d. Return the generated filter code
300
+
301
+ 4. **Wrap in transaction:**
302
+ ```typescript
303
+ return await this.dataSource.transaction(async (manager) => {
304
+ // Create filter master & details here
305
+ // If any step fails, entire transaction rolls back
306
+ });
307
+ ```
308
+
309
+ ### Testing Requirements
310
+ - Test filter creation with 1 condition
311
+ - Test filter creation with multiple conditions
312
+ - Test filter code uniqueness
313
+ - Test transaction rollback on error
314
+ - Verify filter is actually created in database
315
+
316
+ ### Acceptance Criteria
317
+ - [ ] Creates SavedFilterMaster with unique code
318
+ - [ ] Creates SavedFilterDetail for each condition
319
+ - [ ] Wrapped in transaction (all-or-nothing)
320
+ - [ ] Returns generated filter code
321
+ - [ ] Unit tests pass
322
+ - [ ] Integration test verifies database records
323
+
324
+ ---
325
+
326
+ ## Task 1.4: Smart Create Method
327
+ **Duration: 2 hours | Files: linked_attributes.service.ts, DTO files**
328
+
329
+ ### Objective
330
+ Create a single method that orchestrates the entire linked attribute creation process with auto-generation.
331
+
332
+ ### Requirements
333
+
334
+ 1. **Create DTO file: `src/module/linked_attributes/dto/create-linked-attribute-smart.dto.ts`**
335
+ ```typescript
336
+ import { IsString, IsNotEmpty, IsArray, IsOptional, IsBoolean, ValidateNested } from 'class-validator';
337
+ import { Type } from 'class-transformer';
338
+
339
+ export class FilterConditionDto {
340
+ @IsString()
341
+ @IsNotEmpty()
342
+ filter_attribute: string;
343
+
344
+ @IsString()
345
+ @IsNotEmpty()
346
+ filter_operator: string;
347
+
348
+ @IsNotEmpty()
349
+ filter_value: any;
350
+ }
351
+
352
+ export class CreateLinkedAttributeSmartDto {
353
+ @IsString()
354
+ @IsNotEmpty()
355
+ field_name: string;
356
+
357
+ @IsString()
358
+ @IsNotEmpty()
359
+ mapped_entity_type: string;
360
+
361
+ @IsString()
362
+ @IsNotEmpty()
363
+ applicable_entity_type: string;
364
+
365
+ @IsString()
366
+ @IsNotEmpty()
367
+ applicable_attribute_key: string;
368
+
369
+ @IsArray()
370
+ @IsOptional()
371
+ @ValidateNested({ each: true })
372
+ @Type(() => FilterConditionDto)
373
+ filter_conditions?: FilterConditionDto[];
374
+
375
+ @IsBoolean()
376
+ @IsOptional()
377
+ backfill?: boolean;
378
+ }
379
+ ```
380
+
381
+ 2. **Add public async method: createLinkedAttributeSmart()**
382
+ ```typescript
383
+ /**
384
+ * Smart creation of linked attribute with auto-generation
385
+ * @param payload - Linked attribute data
386
+ * @param loggedInUser - Current user context
387
+ * @returns Created LinkedAttribute entity
388
+ */
389
+ async createLinkedAttributeSmart(
390
+ payload: CreateLinkedAttributeSmartDto,
391
+ loggedInUser: any
392
+ ): Promise<LinkedAttributes>
393
+ ```
394
+
395
+ 3. **Implementation Flow:**
396
+ ```typescript
397
+ async createLinkedAttributeSmart(payload, loggedInUser) {
398
+ // Step 1: Validate input
399
+ const validation = await this.validateLinkedAttribute({
400
+ field_name: payload.field_name,
401
+ mapped_entity_type: payload.mapped_entity_type,
402
+ applicable_entity_type: payload.applicable_entity_type,
403
+ applicable_attribute_key: payload.applicable_attribute_key,
404
+ organization_id: loggedInUser.organization_id,
405
+ }, loggedInUser);
406
+
407
+ if (!validation.valid) {
408
+ throw new BadRequestException(validation.errors.join(', '));
409
+ }
410
+
411
+ // Step 2: Generate sequence number
412
+ const sequence = await this.generateNextSequence(
413
+ payload.mapped_entity_type,
414
+ payload.applicable_entity_type,
415
+ payload.applicable_attribute_key,
416
+ loggedInUser.organization_id
417
+ );
418
+
419
+ // Step 3: Generate attribute_key
420
+ const attribute_key = this.generateAttributeKey(
421
+ payload.applicable_entity_type,
422
+ payload.applicable_attribute_key,
423
+ sequence
424
+ );
425
+
426
+ // Step 4: Create saved filter if conditions provided
427
+ let saved_filter_code = null;
428
+ if (payload.filter_conditions?.length > 0) {
429
+ saved_filter_code = await this.createFilterForLinkedAttribute(
430
+ payload.applicable_entity_type,
431
+ payload.filter_conditions,
432
+ attribute_key,
433
+ loggedInUser
434
+ );
435
+ }
436
+
437
+ // Step 5: Create LinkedAttribute entity
438
+ const linkedAttrData = {
439
+ field_name: payload.field_name,
440
+ attribute_key,
441
+ applicable_entity_type: payload.applicable_entity_type,
442
+ applicable_attribute_key: payload.applicable_attribute_key,
443
+ mapped_entity_type: payload.mapped_entity_type,
444
+ saved_filter_code,
445
+ sequence,
446
+ organization_id: loggedInUser.organization_id,
447
+ level_type: loggedInUser.level_type,
448
+ level_id: loggedInUser.level_id,
449
+ };
450
+
451
+ const linkedAttribute = await super.createEntity(linkedAttrData, loggedInUser);
452
+
453
+ // Step 6: Trigger backfill if requested (async, don't await)
454
+ if (payload.backfill) {
455
+ this.backfillLinkedAttribute(linkedAttribute.id, loggedInUser)
456
+ .catch(err => console.error('Backfill failed:', err));
457
+ }
458
+
459
+ return linkedAttribute;
460
+ }
461
+ ```
462
+
463
+ ### Testing Requirements
464
+ - Test complete happy path (all steps succeed)
465
+ - Test validation failure (should throw BadRequestException)
466
+ - Test with filter_conditions
467
+ - Test without filter_conditions
468
+ - Test with backfill: true
469
+ - Test with backfill: false
470
+
471
+ ### Acceptance Criteria
472
+ - [ ] DTO file created with validation decorators
473
+ - [ ] Smart create method implemented
474
+ - [ ] All steps execute in correct order
475
+ - [ ] Wrapped in transaction (optional but recommended)
476
+ - [ ] Returns created LinkedAttribute
477
+ - [ ] Unit tests pass
478
+ - [ ] Integration test verifies end-to-end flow
479
+
480
+ ---
481
+
482
+ ## Task 1.5: Backfill Mechanism
483
+ **Duration: 3 hours | Files: linked_attributes.service.ts**
484
+
485
+ ### Objective
486
+ Implement a mechanism to update existing entity flatjsons with newly created linked attributes.
487
+
488
+ ### Requirements
489
+
490
+ 1. **Add public async method: backfillLinkedAttribute()**
491
+ ```typescript
492
+ /**
493
+ * Backfill existing entities with a new linked attribute
494
+ * @param linked_attribute_id - ID of the linked attribute to backfill
495
+ * @param loggedInUser - Current user context
496
+ * @returns Summary of backfill operation
497
+ */
498
+ async backfillLinkedAttribute(
499
+ linked_attribute_id: number,
500
+ loggedInUser: any
501
+ ): Promise<{
502
+ total: number;
503
+ updated: number;
504
+ failed: number;
505
+ errors: Array<{ entity_id: number; error: string }>;
506
+ }>
507
+ ```
508
+
509
+ 2. **Inject EntityJSONService dependency:**
510
+ ```typescript
511
+ @Inject() private readonly entityJsonService: EntityJSONService,
512
+ ```
513
+
514
+ 3. **Implementation:**
515
+ ```typescript
516
+ async backfillLinkedAttribute(linked_attribute_id, loggedInUser) {
517
+ // Step 1: Load linked attribute
518
+ const linkedAttr = await this.dataSource
519
+ .getRepository(LinkedAttributes)
520
+ .findOne({ where: { id: linked_attribute_id } });
521
+
522
+ if (!linkedAttr) {
523
+ throw new NotFoundException(`LinkedAttribute with ID ${linked_attribute_id} not found`);
524
+ }
525
+
526
+ // Step 2: Get all entities of this type
527
+ const entityJsonRecords = await this.dataSource
528
+ .getRepository('frm_entity_json')
529
+ .createQueryBuilder('ej')
530
+ .select(['ej.entity_id'])
531
+ .where('ej.entity_type = :entityType', { entityType: linkedAttr.mapped_entity_type })
532
+ .andWhere('ej.organization_id = :orgId', { orgId: loggedInUser.organization_id })
533
+ .getMany();
534
+
535
+ // Step 3: Process in batches
536
+ const batchSize = 100;
537
+ const results = {
538
+ total: entityJsonRecords.length,
539
+ updated: 0,
540
+ failed: 0,
541
+ errors: []
542
+ };
543
+
544
+ for (let i = 0; i < entityJsonRecords.length; i += batchSize) {
545
+ const batch = entityJsonRecords.slice(i, i + batchSize);
546
+
547
+ for (const record of batch) {
548
+ try {
549
+ await this.entityJsonService.updateEntityJSON(
550
+ linkedAttr.mapped_entity_type,
551
+ record.entity_id,
552
+ loggedInUser
553
+ );
554
+ results.updated++;
555
+ } catch (error) {
556
+ results.failed++;
557
+ results.errors.push({
558
+ entity_id: record.entity_id,
559
+ error: error.message
560
+ });
561
+ console.error(`Failed to update entity ${record.entity_id}:`, error);
562
+ }
563
+ }
564
+
565
+ // Log progress
566
+ console.log(`Backfill progress: ${Math.min(i + batchSize, results.total)}/${results.total}`);
567
+ }
568
+
569
+ return results;
570
+ }
571
+ ```
572
+
573
+ 4. **Add helper method: backfillAllForEntity()**
574
+ ```typescript
575
+ /**
576
+ * Backfill all linked attributes for an entity type
577
+ * @param mapped_entity_type - Entity type to backfill
578
+ * @param loggedInUser - Current user context
579
+ * @returns Summary of backfill operation
580
+ */
581
+ async backfillAllForEntity(
582
+ mapped_entity_type: string,
583
+ loggedInUser: any
584
+ ): Promise<{ updated: number; failed: number }>
585
+ ```
586
+
587
+ Implementation: Similar to above, but updates all entities regardless of linked attributes.
588
+
589
+ ### Testing Requirements
590
+ - Test backfill with 0 entities (edge case)
591
+ - Test backfill with 1 entity
592
+ - Test backfill with 1000+ entities (performance test)
593
+ - Test handling of partial failures (some entities fail)
594
+ - Mock EntityJSONService.updateEntityJSON
595
+
596
+ ### Acceptance Criteria
597
+ - [ ] Backfill method implemented
598
+ - [ ] Processes in batches (configurable size)
599
+ - [ ] Continues on individual failures
600
+ - [ ] Returns detailed summary
601
+ - [ ] Logs progress to console
602
+ - [ ] Unit tests pass
603
+ - [ ] Performance acceptable (<10 seconds for 10k entities)
604
+
605
+ ---
606
+
607
+ ## Task 1.6: Update Existing CRUD & Controller
608
+ **Duration: 1 hour | Files: linked_attributes.controller.ts, linked_attributes.service.ts**
609
+
610
+ ### Objective
611
+ Add new API endpoints for smart creation and backfilling.
612
+
613
+ ### Requirements
614
+
615
+ 1. **Update Controller: `linked_attributes.controller.ts`**
616
+
617
+ Add new endpoints:
618
+ ```typescript
619
+ import { Body, Controller, Post, Param, Get, Query } from '@nestjs/common';
620
+ import { ApiOperation, ApiResponse, ApiTags } from '@nestjs/swagger';
621
+ import { CreateLinkedAttributeSmartDto } from '../dto/create-linked-attribute-smart.dto';
622
+
623
+ @ApiTags('Linked Attributes')
624
+ @Controller('linked-attributes')
625
+ export class LinkedAttributesController {
626
+ constructor(private readonly linkedAttributesService: LinkedAttributesService) {}
627
+
628
+ @Post('/smart')
629
+ @ApiOperation({ summary: 'Create linked attribute with auto-generation' })
630
+ @ApiResponse({ status: 201, description: 'Linked attribute created successfully' })
631
+ @ApiResponse({ status: 400, description: 'Validation failed' })
632
+ async createSmart(
633
+ @Body() dto: CreateLinkedAttributeSmartDto,
634
+ @Req() req: any
635
+ ) {
636
+ return await this.linkedAttributesService.createLinkedAttributeSmart(
637
+ dto,
638
+ req.loggedInUser
639
+ );
640
+ }
641
+
642
+ @Post('/:id/backfill')
643
+ @ApiOperation({ summary: 'Backfill existing entities with new linked attribute' })
644
+ @ApiResponse({ status: 200, description: 'Backfill completed' })
645
+ @ApiResponse({ status: 404, description: 'Linked attribute not found' })
646
+ async backfill(
647
+ @Param('id') id: number,
648
+ @Req() req: any
649
+ ) {
650
+ return await this.linkedAttributesService.backfillLinkedAttribute(
651
+ id,
652
+ req.loggedInUser
653
+ );
654
+ }
655
+
656
+ @Post('/backfill-all')
657
+ @ApiOperation({ summary: 'Backfill all entities of a given type' })
658
+ @ApiResponse({ status: 200, description: 'Backfill completed' })
659
+ async backfillAll(
660
+ @Query('entity_type') entity_type: string,
661
+ @Req() req: any
662
+ ) {
663
+ return await this.linkedAttributesService.backfillAllForEntity(
664
+ entity_type,
665
+ req.loggedInUser
666
+ );
667
+ }
668
+
669
+ @Get('/preview-key')
670
+ @ApiOperation({ summary: 'Preview generated attribute key before creation' })
671
+ @ApiResponse({ status: 200, description: 'Preview generated successfully' })
672
+ async previewKey(
673
+ @Query('entity_type') entity_type: string,
674
+ @Query('attribute_key') attribute_key: string,
675
+ @Query('organization_id') organization_id: number,
676
+ @Req() req: any
677
+ ) {
678
+ const sequence = await this.linkedAttributesService['generateNextSequence'](
679
+ req.query.mapped_entity_type,
680
+ entity_type,
681
+ attribute_key,
682
+ organization_id
683
+ );
684
+
685
+ const generated_key = this.linkedAttributesService.generateAttributeKey(
686
+ entity_type,
687
+ attribute_key,
688
+ sequence
689
+ );
690
+
691
+ return { generated_key, sequence };
692
+ }
693
+ }
694
+ ```
695
+
696
+ 2. **Update existing createEntity() with deprecation warning:**
697
+ ```typescript
698
+ async createEntity(payload: any, loggedInUser: any): Promise<any> {
699
+ console.warn('[DEPRECATED] Use createLinkedAttributeSmart() instead of createEntity()');
700
+
701
+ // Keep old logic for backward compatibility
702
+ if (!payload.attribute_key || payload.attribute_key.trim() === '') {
703
+ payload.attribute_key = payload.field_name
704
+ .trim()
705
+ .toLowerCase()
706
+ .replace(/\s+/g, '_');
707
+ }
708
+
709
+ return super.createEntity(payload, loggedInUser);
710
+ }
711
+ ```
712
+
713
+ ### Testing Requirements
714
+ - Test each new endpoint with Postman or automated tests
715
+ - Verify Swagger documentation renders correctly
716
+ - Test backward compatibility (old endpoint still works)
717
+
718
+ ### Acceptance Criteria
719
+ - [ ] New endpoints added to controller
720
+ - [ ] Swagger documentation complete
721
+ - [ ] Old endpoints still functional (backward compatible)
722
+ - [ ] Deprecation warning logged for old method
723
+ - [ ] All endpoints tested
724
+
725
+ ---
726
+
727
+ ## Task 1.7: Testing & Edge Cases
728
+ **Duration: 2 hours | Files: *.spec.ts**
729
+
730
+ ### Objective
731
+ Write comprehensive tests for all Phase 1 functionality.
732
+
733
+ ### Requirements
734
+
735
+ 1. **Unit Tests (linked_attributes.service.spec.ts):**
736
+
737
+ Test suites to create:
738
+
739
+ a. **generateNextSequence()**
740
+ - Returns 1 when no records exist
741
+ - Returns max + 1 when records exist
742
+ - Handles NULL max correctly
743
+
744
+ b. **generateAttributeKey()**
745
+ - Generates correct format
746
+ - Throws error for empty entity_type
747
+ - Throws error for empty attribute_key
748
+ - Throws error for sequence < 1
749
+
750
+ c. **validateLinkedAttribute()**
751
+ - Returns valid: true for correct data
752
+ - Returns error for duplicate key
753
+ - Returns error for invalid entity type
754
+ - Returns error for invalid attribute
755
+ - Returns multiple errors when multiple issues exist
756
+
757
+ d. **createFilterForLinkedAttribute()**
758
+ - Creates filter master with unique code
759
+ - Creates filter details for each condition
760
+ - Returns generated filter code
761
+ - Rolls back on error
762
+
763
+ e. **createLinkedAttributeSmart()**
764
+ - Happy path: creates successfully
765
+ - Throws BadRequestException on validation failure
766
+ - Creates filter when conditions provided
767
+ - Skips filter when no conditions
768
+ - Triggers backfill when requested
769
+
770
+ f. **backfillLinkedAttribute()**
771
+ - Handles 0 entities gracefully
772
+ - Updates all entities successfully
773
+ - Continues on partial failures
774
+ - Returns correct summary
775
+
776
+ 2. **Integration Tests (linked_attributes.integration.spec.ts):**
777
+
778
+ Create end-to-end test:
779
+ ```typescript
780
+ describe('LinkedAttributes Integration', () => {
781
+ it('should create linked attribute and backfill entities', async () => {
782
+ // Setup: Create test entities
783
+ // Action: Create linked attribute with backfill
784
+ // Assert: Verify flatjson updated for all entities
785
+ });
786
+ });
787
+ ```
788
+
789
+ 3. **Edge Cases to Test:**
790
+ - Concurrent creation of linked attributes (race conditions)
791
+ - Very long field names
792
+ - Special characters in entity/attribute names
793
+ - Large batch backfill (1000+ entities)
794
+ - Filter conditions with various operators
795
+
796
+ ### Testing Tools Setup
797
+ ```typescript
798
+ // Mock setup example
799
+ const mockDataSource = {
800
+ getRepository: jest.fn().mockReturnValue({
801
+ createQueryBuilder: jest.fn().mockReturnThis(),
802
+ where: jest.fn().mockReturnThis(),
803
+ andWhere: jest.fn().mockReturnThis(),
804
+ getRawOne: jest.fn(),
805
+ getMany: jest.fn(),
806
+ }),
807
+ transaction: jest.fn((cb) => cb(mockDataSource)),
808
+ };
809
+ ```
810
+
811
+ ### Acceptance Criteria
812
+ - [ ] All unit tests pass
813
+ - [ ] Integration tests pass
814
+ - [ ] Code coverage > 80%
815
+ - [ ] All edge cases covered
816
+ - [ ] Performance tests show acceptable speed
817
+ - [ ] No race condition issues
818
+
819
+ ---
820
+
821
+ # PHASE 2: FLATJSON FILTERING SERVICE
822
+ **Priority: CRITICAL | Duration: 18-20 hours**
823
+
824
+ ## Task 2.1: Database Index Creation
825
+ **Duration: 30 minutes | Files: migrations/**
826
+
827
+ ### Objective
828
+ Create GIN index on the flatjson column for optimized JSONB queries.
829
+
830
+ ### Requirements
831
+
832
+ 1. **Create migration file:**
833
+ ```bash
834
+ npm run typeorm migration:create src/migrations/AddEntityJsonGinIndex
835
+ ```
836
+
837
+ 2. **Implement migration: `{timestamp}-AddEntityJsonGinIndex.ts`**
838
+ ```typescript
839
+ import { MigrationInterface, QueryRunner } from 'typeorm';
840
+
841
+ export class AddEntityJsonGinIndex{timestamp} implements MigrationInterface {
842
+ name = 'AddEntityJsonGinIndex{timestamp}';
843
+
844
+ public async up(queryRunner: QueryRunner): Promise<void> {
845
+ // GIN index for JSONB operations (containment, existence checks)
846
+ await queryRunner.query(`
847
+ CREATE INDEX IF NOT EXISTS idx_entity_json_data_gin
848
+ ON frm_entity_json
849
+ USING GIN (json_data jsonb_path_ops);
850
+ `);
851
+
852
+ // Standard B-tree index for entity_type lookups
853
+ await queryRunner.query(`
854
+ CREATE INDEX IF NOT EXISTS idx_entity_json_entity_type
855
+ ON frm_entity_json (entity_type);
856
+ `);
857
+
858
+ // Composite index for entity_type + entity_id lookups
859
+ await queryRunner.query(`
860
+ CREATE INDEX IF NOT EXISTS idx_entity_json_composite
861
+ ON frm_entity_json (entity_type, entity_id);
862
+ `);
863
+
864
+ console.log('✅ GIN indexes created successfully');
865
+ }
866
+
867
+ public async up(queryRunner: QueryRunner): Promise<void> {
868
+ await queryRunner.query(`DROP INDEX IF EXISTS idx_entity_json_data_gin;`);
869
+ await queryRunner.query(`DROP INDEX IF EXISTS idx_entity_json_entity_type;`);
870
+ await queryRunner.query(`DROP INDEX IF EXISTS idx_entity_json_composite;`);
871
+
872
+ console.log('✅ GIN indexes dropped successfully');
873
+ }
874
+ }
875
+ ```
876
+
877
+ 3. **Run migration:**
878
+ ```bash
879
+ npm run typeorm migration:run
880
+ ```
881
+
882
+ 4. **Verify index creation:**
883
+ ```sql
884
+ -- Run this query to verify
885
+ SELECT
886
+ schemaname,
887
+ tablename,
888
+ indexname,
889
+ indexdef
890
+ FROM pg_indexes
891
+ WHERE tablename = 'frm_entity_json';
892
+ ```
893
+
894
+ ### Testing Requirements
895
+ - Test migration up (creates indexes)
896
+ - Test migration down (drops indexes)
897
+ - Verify query planner uses GIN index with EXPLAIN ANALYZE
898
+
899
+ ### Acceptance Criteria
900
+ - [ ] Migration file created
901
+ - [ ] Migration runs successfully
902
+ - [ ] All 3 indexes created
903
+ - [ ] Query planner shows index usage
904
+ - [ ] Rollback migration works
905
+
906
+ ---
907
+
908
+ ## Task 2.2: Service Skeleton & Infrastructure
909
+ **Duration: 2 hours | Files: filter/service/flatjson-filter.service.ts**
910
+
911
+ ### Objective
912
+ Create the base structure for the new flatjson filtering service.
913
+
914
+ ### Requirements
915
+
916
+ 1. **Create service file: `src/module/filter/service/flatjson-filter.service.ts`**
917
+ ```typescript
918
+ import { Injectable, BadRequestException } from '@nestjs/common';
919
+ import { EntityManager, SelectQueryBuilder } from 'typeorm';
920
+ import { EntityMasterService } from 'src/module/meta/service/entity-master.service';
921
+ import { AttributeMasterService } from 'src/module/meta/service/attribute-master.service';
922
+ import { ResolverService } from 'src/module/meta/service/resolver.service';
923
+ import { LoggingService } from 'src/utils/service/loggingUtil.service';
924
+ import { ConfigService } from '@nestjs/config';
925
+ import { FilterRequestDto, FilterCondition, SortConfig } from '../dto/filter-request.dto';
926
+ import { EntityJson } from 'src/module/entity_json/entity/entityJson.entity';
927
+
928
+ /**
929
+ * JSONB query condition structure
930
+ */
931
+ interface JsonbCondition {
932
+ query: string;
933
+ params: Record<string, any>;
934
+ }
935
+
936
+ @Injectable()
937
+ export class FlatjsonFilterService {
938
+ constructor(
939
+ private readonly entityManager: EntityManager,
940
+ private readonly entityMasterService: EntityMasterService,
941
+ private readonly attributeMasterService: AttributeMasterService,
942
+ private readonly resolverService: ResolverService,
943
+ private readonly loggingService: LoggingService,
944
+ private readonly configService: ConfigService,
945
+ ) {}
946
+
947
+ /**
948
+ * Main filtering method - queries frm_entity_json table
949
+ */
950
+ async applyFlatjsonFilter(dto: FilterRequestDto): Promise<any> {
951
+ // TODO: Implement in Task 2.7
952
+ throw new Error('Not implemented yet');
953
+ }
954
+
955
+ /**
956
+ * Build JSONB where clauses from filter conditions
957
+ */
958
+ private buildJsonbConditions(
959
+ filters: FilterCondition[],
960
+ attributeMetaMap: Record<string, any>
961
+ ): JsonbCondition[] {
962
+ // TODO: Implement in Task 2.7
963
+ return [];
964
+ }
965
+
966
+ /**
967
+ * Build a single JSONB condition based on data type
968
+ */
969
+ private buildJsonbCondition(
970
+ filter: FilterCondition,
971
+ meta: any
972
+ ): JsonbCondition | null {
973
+ if (!meta) return null;
974
+
975
+ const flatJsonKey = meta.flat_json_key || `${meta.mapped_entity_type}__${filter.filter_attribute}`;
976
+ const op = filter.filter_operator;
977
+ const val = filter.filter_value;
978
+ const key = `param_${filter.filter_attribute}_${Math.random().toString(36).substring(2, 8)}`;
979
+
980
+ switch (meta.data_type) {
981
+ case 'text':
982
+ return this.buildTextCondition(flatJsonKey, op, val, key);
983
+ case 'number':
984
+ return this.buildNumberCondition(flatJsonKey, op, val, key);
985
+ case 'date':
986
+ return this.buildDateCondition(flatJsonKey, op, val, key);
987
+ case 'select':
988
+ case 'radio':
989
+ return this.buildSelectCondition(flatJsonKey, op, val, key);
990
+ case 'multiselect':
991
+ case 'checkbox':
992
+ return this.buildMultiSelectCondition(flatJsonKey, op, val, key);
993
+ case 'year':
994
+ return this.buildYearCondition(flatJsonKey, op, val, key);
995
+ default:
996
+ return null;
997
+ }
998
+ }
999
+
1000
+ /**
1001
+ * Build text condition (JSONB ->> operator)
1002
+ */
1003
+ private buildTextCondition(
1004
+ flatJsonKey: string,
1005
+ operator: string,
1006
+ value: any,
1007
+ paramKey: string
1008
+ ): JsonbCondition | null {
1009
+ // TODO: Implement in Task 2.3
1010
+ return null;
1011
+ }
1012
+
1013
+ /**
1014
+ * Build number condition (JSONB ->> with ::int cast)
1015
+ */
1016
+ private buildNumberCondition(
1017
+ flatJsonKey: string,
1018
+ operator: string,
1019
+ value: any,
1020
+ paramKey: string
1021
+ ): JsonbCondition | null {
1022
+ // TODO: Implement in Task 2.4
1023
+ return null;
1024
+ }
1025
+
1026
+ /**
1027
+ * Build date condition (JSONB ->> with ::bigint cast for epoch ms)
1028
+ */
1029
+ private buildDateCondition(
1030
+ flatJsonKey: string,
1031
+ operator: string,
1032
+ value: any,
1033
+ paramKey: string
1034
+ ): JsonbCondition | null {
1035
+ // TODO: Implement in Task 2.5
1036
+ return null;
1037
+ }
1038
+
1039
+ /**
1040
+ * Build select condition (single value)
1041
+ */
1042
+ private buildSelectCondition(
1043
+ flatJsonKey: string,
1044
+ operator: string,
1045
+ value: any,
1046
+ paramKey: string
1047
+ ): JsonbCondition | null {
1048
+ // TODO: Implement in Task 2.6
1049
+ return null;
1050
+ }
1051
+
1052
+ /**
1053
+ * Build multiselect condition (JSONB -> with ? operators for arrays)
1054
+ */
1055
+ private buildMultiSelectCondition(
1056
+ flatJsonKey: string,
1057
+ operator: string,
1058
+ value: any,
1059
+ paramKey: string
1060
+ ): JsonbCondition | null {
1061
+ // TODO: Implement in Task 2.6
1062
+ return null;
1063
+ }
1064
+
1065
+ /**
1066
+ * Build year condition
1067
+ */
1068
+ private buildYearCondition(
1069
+ flatJsonKey: string,
1070
+ operator: string,
1071
+ value: any,
1072
+ paramKey: string
1073
+ ): JsonbCondition | null {
1074
+ // Similar to number condition
1075
+ return this.buildNumberCondition(flatJsonKey, operator, value, paramKey);
1076
+ }
1077
+
1078
+ /**
1079
+ * Get tab aggregation counts from JSONB
1080
+ */
1081
+ private async getJsonbTabCounts(
1082
+ entity_type: string,
1083
+ flatJsonKey: string,
1084
+ whereClauses: JsonbCondition[]
1085
+ ): Promise<Array<{ tab_value: string; tab_value_count: number }>> {
1086
+ // TODO: Implement in Task 2.8
1087
+ return [];
1088
+ }
1089
+
1090
+ /**
1091
+ * Apply sorting on JSONB fields
1092
+ */
1093
+ private applyJsonbSorting(
1094
+ qb: SelectQueryBuilder<EntityJson>,
1095
+ sortby: SortConfig[],
1096
+ attributeMetaMap: Record<string, any>
1097
+ ): void {
1098
+ // TODO: Implement in Task 2.9
1099
+ }
1100
+ }
1101
+ ```
1102
+
1103
+ 2. **Register service in module: `src/module/filter/filter.module.ts`**
1104
+ ```typescript
1105
+ import { FlatjsonFilterService } from './service/flatjson-filter.service';
1106
+
1107
+ @Module({
1108
+ imports: [TypeOrmModule.forFeature([...])],
1109
+ providers: [
1110
+ FilterService,
1111
+ FlatjsonFilterService, // Add this
1112
+ SavedFilterService,
1113
+ // ... other providers
1114
+ ],
1115
+ exports: [
1116
+ FilterService,
1117
+ FlatjsonFilterService, // Add this
1118
+ SavedFilterService,
1119
+ ],
1120
+ })
1121
+ export class FilterModule {}
1122
+ ```
1123
+
1124
+ ### Testing Requirements
1125
+ - Service can be instantiated
1126
+ - All dependencies inject correctly
1127
+ - Skeleton methods exist (even if throwing "Not implemented")
1128
+
1129
+ ### Acceptance Criteria
1130
+ - [ ] Service file created with all method signatures
1131
+ - [ ] Registered in module
1132
+ - [ ] All dependencies injected
1133
+ - [ ] TypeScript compiles without errors
1134
+ - [ ] Service can be imported in other modules
1135
+
1136
+ ---
1137
+
1138
+ ## Task 2.3: JSONB Query Builders - Text Type
1139
+ **Duration: 2 hours | Files: flatjson-filter.service.ts**
1140
+
1141
+ ### Objective
1142
+ Implement JSONB query building for text data type.
1143
+
1144
+ ### Requirements
1145
+
1146
+ **Implement `buildTextCondition()` method:**
1147
+ ```typescript
1148
+ private buildTextCondition(
1149
+ flatJsonKey: string,
1150
+ operator: string,
1151
+ value: any,
1152
+ paramKey: string
1153
+ ): JsonbCondition | null {
1154
+ // Text is already stored lowercase in flatjson
1155
+ const lowerValue = value ? String(value).toLowerCase() : '';
1156
+
1157
+ switch (operator) {
1158
+ case 'equal':
1159
+ return {
1160
+ query: `json_data->>'${flatJsonKey}' = :${paramKey}`,
1161
+ params: { [paramKey]: lowerValue }
1162
+ };
1163
+
1164
+ case 'not_equal':
1165
+ return {
1166
+ query: `json_data->>'${flatJsonKey}' != :${paramKey}`,
1167
+ params: { [paramKey]: lowerValue }
1168
+ };
1169
+
1170
+ case 'contains':
1171
+ return {
1172
+ query: `json_data->>'${flatJsonKey}' LIKE :${paramKey}`,
1173
+ params: { [paramKey]: `%${lowerValue}%` }
1174
+ };
1175
+
1176
+ case 'not_contains':
1177
+ return {
1178
+ query: `json_data->>'${flatJsonKey}' NOT LIKE :${paramKey}`,
1179
+ params: { [paramKey]: `%${lowerValue}%` }
1180
+ };
1181
+
1182
+ case 'starts_with':
1183
+ return {
1184
+ query: `json_data->>'${flatJsonKey}' LIKE :${paramKey}`,
1185
+ params: { [paramKey]: `${lowerValue}%` }
1186
+ };
1187
+
1188
+ case 'ends_with':
1189
+ return {
1190
+ query: `json_data->>'${flatJsonKey}' LIKE :${paramKey}`,
1191
+ params: { [paramKey]: `%${lowerValue}` }
1192
+ };
1193
+
1194
+ case 'empty':
1195
+ return {
1196
+ query: `(json_data->>'${flatJsonKey}' IS NULL OR json_data->>'${flatJsonKey}' = '')`,
1197
+ params: {}
1198
+ };
1199
+
1200
+ case 'not_empty':
1201
+ return {
1202
+ query: `(json_data->>'${flatJsonKey}' IS NOT NULL AND json_data->>'${flatJsonKey}' != '')`,
1203
+ params: {}
1204
+ };
1205
+
1206
+ default:
1207
+ console.warn(`Unsupported text operator: ${operator}`);
1208
+ return null;
1209
+ }
1210
+ }
1211
+ ```
1212
+
1213
+ ### Testing Requirements
1214
+ Create test file: `flatjson-filter.service.spec.ts`
1215
+ ```typescript
1216
+ describe('FlatjsonFilterService - Text Conditions', () => {
1217
+ it('should build equal condition', () => {
1218
+ const condition = service['buildTextCondition']('LEAD__name', 'equal', 'John', 'param1');
1219
+ expect(condition.query).toBe("json_data->>'LEAD__name' = :param1");
1220
+ expect(condition.params.param1).toBe('john');
1221
+ });
1222
+
1223
+ it('should build contains condition', () => {
1224
+ const condition = service['buildTextCondition']('LEAD__name', 'contains', 'John', 'param1');
1225
+ expect(condition.query).toContain('LIKE');
1226
+ expect(condition.params.param1).toBe('%john%');
1227
+ });
1228
+
1229
+ it('should handle empty operator', () => {
1230
+ const condition = service['buildTextCondition']('LEAD__name', 'empty', null, 'param1');
1231
+ expect(condition.query).toContain('IS NULL');
1232
+ });
1233
+
1234
+ // Add tests for all operators
1235
+ });
1236
+ ```
1237
+
1238
+ ### Acceptance Criteria
1239
+ - [ ] All text operators implemented
1240
+ - [ ] Values converted to lowercase
1241
+ - [ ] LIKE patterns correct for contains/starts_with/ends_with
1242
+ - [ ] Empty/not_empty handle NULL and empty string
1243
+ - [ ] Unit tests pass
1244
+ - [ ] No SQL injection vulnerabilities (parameterized queries)
1245
+
1246
+ ---
1247
+
1248
+ ## Task 2.4: JSONB Query Builders - Number Type
1249
+ **Duration: 1.5 hours | Files: flatjson-filter.service.ts**
1250
+
1251
+ ### Objective
1252
+ Implement JSONB query building for number data type.
1253
+
1254
+ ### Requirements
1255
+
1256
+ **Implement `buildNumberCondition()` method:**
1257
+ ```typescript
1258
+ private buildNumberCondition(
1259
+ flatJsonKey: string,
1260
+ operator: string,
1261
+ value: any,
1262
+ paramKey: string
1263
+ ): JsonbCondition | null {
1264
+ // Cast JSONB text to numeric for comparison
1265
+ const jsonbField = `(json_data->>'${flatJsonKey}')::numeric`;
1266
+
1267
+ switch (operator) {
1268
+ case 'equal':
1269
+ return {
1270
+ query: `${jsonbField} = :${paramKey}`,
1271
+ params: { [paramKey]: Number(value) }
1272
+ };
1273
+
1274
+ case 'not_equal':
1275
+ return {
1276
+ query: `${jsonbField} != :${paramKey}`,
1277
+ params: { [paramKey]: Number(value) }
1278
+ };
1279
+
1280
+ case 'greater_than':
1281
+ return {
1282
+ query: `${jsonbField} > :${paramKey}`,
1283
+ params: { [paramKey]: Number(value) }
1284
+ };
1285
+
1286
+ case 'less_than':
1287
+ return {
1288
+ query: `${jsonbField} < :${paramKey}`,
1289
+ params: { [paramKey]: Number(value) }
1290
+ };
1291
+
1292
+ case 'greater_than_equal_to':
1293
+ return {
1294
+ query: `${jsonbField} >= :${paramKey}`,
1295
+ params: { [paramKey]: Number(value) }
1296
+ };
1297
+
1298
+ case 'less_than_equal_to':
1299
+ return {
1300
+ query: `${jsonbField} <= :${paramKey}`,
1301
+ params: { [paramKey]: Number(value) }
1302
+ };
1303
+
1304
+ case 'between':
1305
+ // Value should be array [min, max] or string "min,max"
1306
+ let range: number[];
1307
+ if (typeof value === 'string') {
1308
+ range = value.split(',').map(v => Number(v.trim()));
1309
+ } else if (Array.isArray(value)) {
1310
+ range = value.map(v => Number(v));
1311
+ } else {
1312
+ return null;
1313
+ }
1314
+
1315
+ if (range.length !== 2) return null;
1316
+
1317
+ return {
1318
+ query: `${jsonbField} BETWEEN :${paramKey}_min AND :${paramKey}_max`,
1319
+ params: {
1320
+ [`${paramKey}_min`]: range[0],
1321
+ [`${paramKey}_max`]: range[1]
1322
+ }
1323
+ };
1324
+
1325
+ case 'empty':
1326
+ return {
1327
+ query: `json_data->>'${flatJsonKey}' IS NULL`,
1328
+ params: {}
1329
+ };
1330
+
1331
+ case 'not_empty':
1332
+ return {
1333
+ query: `json_data->>'${flatJsonKey}' IS NOT NULL`,
1334
+ params: {}
1335
+ };
1336
+
1337
+ default:
1338
+ console.warn(`Unsupported number operator: ${operator}`);
1339
+ return null;
1340
+ }
1341
+ }
1342
+ ```
1343
+
1344
+ ### Testing Requirements
1345
+ ```typescript
1346
+ describe('FlatjsonFilterService - Number Conditions', () => {
1347
+ it('should build greater_than condition with type casting', () => {
1348
+ const condition = service['buildNumberCondition']('LEAD__age', 'greater_than', 18, 'param1');
1349
+ expect(condition.query).toContain('::numeric >');
1350
+ expect(condition.params.param1).toBe(18);
1351
+ });
1352
+
1353
+ it('should build between condition', () => {
1354
+ const condition = service['buildNumberCondition']('LEAD__age', 'between', '18,65', 'param1');
1355
+ expect(condition.query).toContain('BETWEEN');
1356
+ expect(condition.params.param1_min).toBe(18);
1357
+ expect(condition.params.param1_max).toBe(65);
1358
+ });
1359
+
1360
+ // Add tests for all operators
1361
+ });
1362
+ ```
1363
+
1364
+ ### Acceptance Criteria
1365
+ - [ ] All number operators implemented
1366
+ - [ ] Proper type casting to `::numeric`
1367
+ - [ ] Between operator handles array and string input
1368
+ - [ ] Values converted to Number type
1369
+ - [ ] Unit tests pass
1370
+
1371
+ ---
1372
+
1373
+ ## Task 2.5: JSONB Query Builders - Date Type
1374
+ **Duration: 3 hours | Files: flatjson-filter.service.ts**
1375
+
1376
+ ### Objective
1377
+ Implement JSONB query building for date data type (stored as epoch milliseconds).
1378
+
1379
+ ### Requirements
1380
+
1381
+ **Implement `buildDateCondition()` method:**
1382
+
1383
+ This is the most complex condition builder due to various date operators. Reference the existing `FilterService.buildDateCondition()` and adapt for JSONB.
1384
+ ```typescript
1385
+ private buildDateCondition(
1386
+ flatJsonKey: string,
1387
+ operator: string,
1388
+ value: any,
1389
+ paramKey: string
1390
+ ): JsonbCondition | null {
1391
+ // Dates stored as epoch milliseconds (bigint)
1392
+ const jsonbField = `(json_data->>'${flatJsonKey}')::bigint`;
1393
+
1394
+ // Helper: Convert date string to epoch ms
1395
+ const toEpochMs = (dateStr: string): number => {
1396
+ return new Date(dateStr).getTime();
1397
+ };
1398
+
1399
+ // Helper: Get date N days ago
1400
+ const daysAgo = (days: number): number => {
1401
+ const d = new Date();
1402
+ d.setDate(d.getDate() - days);
1403
+ return d.getTime();
1404
+ };
1405
+
1406
+ // Helper: Get date N days from now
1407
+ const daysFromNow = (days: number): number => {
1408
+ const d = new Date();
1409
+ d.setDate(d.getDate() + days);
1410
+ return d.getTime();
1411
+ };
1412
+
1413
+ // Helper: Subtract business days (skip weekends)
1414
+ const subtractBusinessDays = (days: number): number => {
1415
+ let d = new Date();
1416
+ let count = 0;
1417
+
1418
+ while (count < days) {
1419
+ d.setDate(d.getDate() - 1);
1420
+ const day = d.getDay(); // 0=Sun, 6=Sat
1421
+ if (day !== 0 && day !== 6) {
1422
+ count++;
1423
+ }
1424
+ }
1425
+
1426
+ return d.getTime();
1427
+ };
1428
+
1429
+ const numVal = Number(value);
1430
+
1431
+ switch (operator) {
1432
+ // Basic comparisons
1433
+ case 'equal':
1434
+ case 'is':
1435
+ return {
1436
+ query: `${jsonbField} = :${paramKey}`,
1437
+ params: { [paramKey]: toEpochMs(value) }
1438
+ };
1439
+
1440
+ case 'before':
1441
+ case 'is_before':
1442
+ return {
1443
+ query: `${jsonbField} < :${paramKey}`,
1444
+ params: { [paramKey]: toEpochMs(value) }
1445
+ };
1446
+
1447
+ case 'after':
1448
+ case 'is_after':
1449
+ return {
1450
+ query: `${jsonbField} > :${paramKey}`,
1451
+ params: { [paramKey]: toEpochMs(value) }
1452
+ };
1453
+
1454
+ case 'is_on_or_before':
1455
+ return {
1456
+ query: `${jsonbField} <= :${paramKey}`,
1457
+ params: { [paramKey]: toEpochMs(value) }
1458
+ };
1459
+
1460
+ case 'is_on_or_after':
1461
+ return {
1462
+ query: `${jsonbField} >= :${paramKey}`,
1463
+ params: { [paramKey]: toEpochMs(value) }
1464
+ };
1465
+
1466
+ // Day offset logic
1467
+ case 'is_day_before':
1468
+ if (isNaN(numVal)) return null;
1469
+ return {
1470
+ query: `${jsonbField} <= :${paramKey}`,
1471
+ params: { [paramKey]: daysAgo(numVal) }
1472
+ };
1473
+
1474
+ case 'is_day_after':
1475
+ if (isNaN(numVal)) return null;
1476
+ return {
1477
+ query: `${jsonbField} >= :${paramKey}`,
1478
+ params: { [paramKey]: daysFromNow(numVal) }
1479
+ };
1480
+
1481
+ // Business days (skip weekends)
1482
+ case 'is_before_business_days':
1483
+ if (isNaN(numVal)) return null;
1484
+ return {
1485
+ query: `${jsonbField} <= :${paramKey}`,
1486
+ params: { [paramKey]: subtractBusinessDays(numVal) }
1487
+ };
1488
+
1489
+ case 'is_after_business_days':
1490
+ if (isNaN(numVal)) return null;
1491
+ // Similar implementation for forward business days
1492
+ return null; // Implement as needed
1493
+
1494
+ // Range operators
1495
+ case 'between':
1496
+ let range: string[];
1497
+ if (typeof value === 'string') {
1498
+ range = value.split(',').map(v => v.trim());
1499
+ } else if (Array.isArray(value)) {
1500
+ range = value;
1501
+ } else {
1502
+ return null;
1503
+ }
1504
+
1505
+ if (range.length !== 2) return null;
1506
+
1507
+ return {
1508
+ query: `${jsonbField} BETWEEN :${paramKey}_start AND :${paramKey}_end`,
1509
+ params: {
1510
+ [`${paramKey}_start`]: toEpochMs(range[0]),
1511
+ [`${paramKey}_end`]: toEpochMs(range[1])
1512
+ }
1513
+ };
1514
+
1515
+ case 'in_last_day':
1516
+ if (isNaN(numVal)) return null;
1517
+ return {
1518
+ query: `${jsonbField} BETWEEN :${paramKey}_start AND :${paramKey}_end`,
1519
+ params: {
1520
+ [`${paramKey}_start`]: daysAgo(numVal),
1521
+ [`${paramKey}_end`]: Date.now()
1522
+ }
1523
+ };
1524
+
1525
+ case 'in_next_day':
1526
+ if (isNaN(numVal)) return null;
1527
+ return {
1528
+ query: `${jsonbField} BETWEEN :${paramKey}_start AND :${paramKey}_end`,
1529
+ params: {
1530
+ [`${paramKey}_start`]: Date.now(),
1531
+ [`${paramKey}_end`]: daysFromNow(numVal)
1532
+ }
1533
+ };
1534
+
1535
+ // Special cases
1536
+ case 'today':
1537
+ const todayStart = new Date().setHours(0, 0, 0, 0);
1538
+ const todayEnd = new Date().setHours(23, 59, 59, 999);
1539
+ return {
1540
+ query: `${jsonbField} BETWEEN :${paramKey}_start AND :${paramKey}_end`,
1541
+ params: {
1542
+ [`${paramKey}_start`]: todayStart,
1543
+ [`${paramKey}_end`]: todayEnd
1544
+ }
1545
+ };
1546
+
1547
+ case 'empty':
1548
+ return {
1549
+ query: `json_data->>'${flatJsonKey}' IS NULL`,
1550
+ params: {}
1551
+ };
1552
+
1553
+ case 'not_empty':
1554
+ return {
1555
+ query: `json_data->>'${flatJsonKey}' IS NOT NULL`,
1556
+ params: {}
1557
+ };
1558
+
1559
+ default:
1560
+ console.warn(`Unsupported date operator: ${operator}`);
1561
+ return null;
1562
+ }
1563
+ }
1564
+ ```
1565
+
1566
+ ### Testing Requirements
1567
+ ```typescript
1568
+ describe('FlatjsonFilterService - Date Conditions', () => {
1569
+ it('should build is_before condition with epoch ms', () => {
1570
+ const condition = service['buildDateCondition'](
1571
+ 'LEAD__created_at',
1572
+ 'is_before',
1573
+ '2024-01-01',
1574
+ 'param1'
1575
+ );
1576
+ expect(condition.query).toContain('::bigint <');
1577
+ expect(condition.params.param1).toBe(new Date('2024-01-01').getTime());
1578
+ });
1579
+
1580
+ it('should build in_last_day condition', () => {
1581
+ const condition = service['buildDateCondition'](
1582
+ 'LEAD__created_at',
1583
+ 'in_last_day',
1584
+ 7,
1585
+ 'param1'
1586
+ );
1587
+ expect(condition.query).toContain('BETWEEN');
1588
+ expect(condition.params.param1_start).toBeLessThan(Date.now());
1589
+ });
1590
+
1591
+ it('should build today condition', () => {
1592
+ const condition = service['buildDateCondition'](
1593
+ 'LEAD__created_at',
1594
+ 'today',
1595
+ null,
1596
+ 'param1'
1597
+ );
1598
+ expect(condition.query).toContain('BETWEEN');
1599
+ });
1600
+
1601
+ // Add tests for all operators, especially business days
1602
+ });
1603
+ ```
1604
+
1605
+ ### Acceptance Criteria
1606
+ - [ ] All date operators from FilterService ported
1607
+ - [ ] Proper type casting to `::bigint`
1608
+ - [ ] Epoch millisecond conversions correct
1609
+ - [ ] Business day logic works (skips weekends)
1610
+ - [ ] Range operators (between, in_last_day) work
1611
+ - [ ] Unit tests pass
1612
+ - [ ] Edge cases covered (leap years, month boundaries)
1613
+
1614
+ ---
1615
+
1616
+ ## Task 2.6: JSONB Query Builders - Select/Multiselect
1617
+ **Duration: 2 hours | Files: flatjson-filter.service.ts**
1618
+
1619
+ ### Objective
1620
+ Implement JSONB query building for select (single) and multiselect (array) data types.
1621
+
1622
+ ### Requirements
1623
+
1624
+ **1. Implement `buildSelectCondition()` for single-value selects:**
1625
+ ```typescript
1626
+ private buildSelectCondition(
1627
+ flatJsonKey: string,
1628
+ operator: string,
1629
+ value: any,
1630
+ paramKey: string
1631
+ ): JsonbCondition | null {
1632
+ switch (operator) {
1633
+ case 'equal':
1634
+ if (Array.isArray(value)) {
1635
+ // IN operator for multiple values
1636
+ return {
1637
+ query: `json_data->>'${flatJsonKey}' = ANY(:${paramKey})`,
1638
+ params: { [paramKey]: value.map(String) }
1639
+ };
1640
+ }
1641
+ return {
1642
+ query: `json_data->>'${flatJsonKey}' = :${paramKey}`,
1643
+ params: { [paramKey]: String(value) }
1644
+ };
1645
+
1646
+ case 'not_equal':
1647
+ if (Array.isArray(value)) {
1648
+ // NOT IN operator
1649
+ return {
1650
+ query: `json_data->>'${flatJsonKey}' != ALL(:${paramKey})`,
1651
+ params: { [paramKey]: value.map(String) }
1652
+ };
1653
+ }
1654
+ return {
1655
+ query: `json_data->>'${flatJsonKey}' != :${paramKey}`,
1656
+ params: { [paramKey]: String(value) }
1657
+ };
1658
+
1659
+ case 'in':
1660
+ const inValues = Array.isArray(value) ? value : [value];
1661
+ return {
1662
+ query: `json_data->>'${flatJsonKey}' = ANY(:${paramKey})`,
1663
+ params: { [paramKey]: inValues.map(String) }
1664
+ };
1665
+
1666
+ case 'not_in':
1667
+ const notInValues = Array.isArray(value) ? value : [value];
1668
+ return {
1669
+ query: `json_data->>'${flatJsonKey}' != ALL(:${paramKey})`,
1670
+ params: { [paramKey]: notInValues.map(String) }
1671
+ };
1672
+
1673
+ case 'empty':
1674
+ return {
1675
+ query: `json_data->>'${flatJsonKey}' IS NULL`,
1676
+ params: {}
1677
+ };
1678
+
1679
+ case 'not_empty':
1680
+ return {
1681
+ query: `json_data->>'${flatJsonKey}' IS NOT NULL`,
1682
+ params: {}
1683
+ };
1684
+
1685
+ default:
1686
+ console.warn(`Unsupported select operator: ${operator}`);
1687
+ return null;
1688
+ }
1689
+ }
1690
+ ```
1691
+
1692
+ **2. Implement `buildMultiSelectCondition()` for array values:**
1693
+ ```typescript
1694
+ private buildMultiSelectCondition(
1695
+ flatJsonKey: string,
1696
+ operator: string,
1697
+ value: any,
1698
+ paramKey: string
1699
+ ): JsonbCondition | null {
1700
+ // Convert value to array if not already
1701
+ let arr: string[];
1702
+ if (Array.isArray(value)) {
1703
+ arr = value.map(String);
1704
+ } else if (typeof value === 'string') {
1705
+ arr = value.split(',').map(v => v.trim());
1706
+ } else {
1707
+ arr = [String(value)];
1708
+ }
1709
+
1710
+ if (arr.length === 0) {
1711
+ return { query: '1=1', params: {} }; // Always true for empty array
1712
+ }
1713
+
1714
+ switch (operator) {
1715
+ case 'contains':
1716
+ case 'has':
1717
+ // Check if JSON array contains this element
1718
+ // json_data->'LEAD__languages' ? 'hindi'
1719
+ if (arr.length === 1) {
1720
+ return {
1721
+ query: `json_data->'${flatJsonKey}' ? :${paramKey}`,
1722
+ params: { [paramKey]: arr[0] }
1723
+ };
1724
+ }
1725
+ // For multiple values, check if contains ANY
1726
+ return {
1727
+ query: `json_data->'${flatJsonKey}' ?| ARRAY[:...${paramKey}]`,
1728
+ params: { [paramKey]: arr }
1729
+ };
1730
+
1731
+ case 'contains_all':
1732
+ // Check if JSON array contains ALL elements
1733
+ // json_data->'LEAD__languages' ?& ARRAY['hindi', 'english']
1734
+ return {
1735
+ query: `json_data->'${flatJsonKey}' ?& ARRAY[:...${paramKey}]`,
1736
+ params: { [paramKey]: arr }
1737
+ };
1738
+
1739
+ case 'contains_any':
1740
+ // Check if JSON array contains ANY element
1741
+ // json_data->'LEAD__languages' ?| ARRAY['hindi', 'english']
1742
+ return {
1743
+ query: `json_data->'${flatJsonKey}' ?| ARRAY[:...${paramKey}]`,
1744
+ params: { [paramKey]: arr }
1745
+ };
1746
+
1747
+ case 'not_contains':
1748
+ // Check if JSON array does NOT contain element
1749
+ if (arr.length === 1) {
1750
+ return {
1751
+ query: `NOT (json_data->'${flatJsonKey}' ? :${paramKey})`,
1752
+ params: { [paramKey]: arr[0] }
1753
+ };
1754
+ }
1755
+ return {
1756
+ query: `NOT (json_data->'${flatJsonKey}' ?| ARRAY[:...${paramKey}])`,
1757
+ params: { [paramKey]: arr }
1758
+ };
1759
+
1760
+ case 'equal':
1761
+ // Exact array match (order matters in PostgreSQL)
1762
+ // This is tricky - might need to compare as JSON
1763
+ return {
1764
+ query: `json_data->'${flatJsonKey}'::jsonb = :${paramKey}::jsonb`,
1765
+ params: { [paramKey]: JSON.stringify(arr) }
1766
+ };
1767
+
1768
+ case 'empty':
1769
+ return {
1770
+ query: `(json_data->>'${flatJsonKey}' IS NULL OR json_data->'${flatJsonKey}' = '[]'::jsonb)`,
1771
+ params: {}
1772
+ };
1773
+
1774
+ case 'not_empty':
1775
+ return {
1776
+ query: `(json_data->>'${flatJsonKey}' IS NOT NULL AND json_data->'${flatJsonKey}' != '[]'::jsonb)`,
1777
+ params: {}
1778
+ };
1779
+
1780
+ default:
1781
+ console.warn(`Unsupported multiselect operator: ${operator}`);
1782
+ return null;
1783
+ }
1784
+ }
1785
+ ```
1786
+
1787
+ ### Important JSONB Array Operators
1788
+ - `?` - Does the string exist as a top-level key/element?
1789
+ - `?|` - Do any of these array strings exist?
1790
+ - `?&` - Do all of these array strings exist?
1791
+ - `@>` - Does the left JSON contain the right JSON?
1792
+ - `<@` - Is the left JSON contained in the right JSON?
1793
+
1794
+ ### Testing Requirements
1795
+ ```typescript
1796
+ describe('FlatjsonFilterService - Select Conditions', () => {
1797
+ it('should build equal condition for single value', () => {
1798
+ const condition = service['buildSelectCondition']('LEAD__status', 'equal', 'active', 'param1');
1799
+ expect(condition.query).toContain("json_data->>'LEAD__status' =");
1800
+ expect(condition.params.param1).toBe('active');
1801
+ });
1802
+
1803
+ it('should build IN condition for array value', () => {
1804
+ const condition = service['buildSelectCondition'](
1805
+ 'LEAD__status',
1806
+ 'equal',
1807
+ ['active', 'pending'],
1808
+ 'param1'
1809
+ );
1810
+ expect(condition.query).toContain('ANY');
1811
+ });
1812
+ });
1813
+
1814
+ describe('FlatjsonFilterService - Multiselect Conditions', () => {
1815
+ it('should build contains condition using ? operator', () => {
1816
+ const condition = service['buildMultiSelectCondition'](
1817
+ 'LEAD__languages',
1818
+ 'contains',
1819
+ 'hindi',
1820
+ 'param1'
1821
+ );
1822
+ expect(condition.query).toContain("json_data->'LEAD__languages' ?");
1823
+ expect(condition.params.param1).toBe('hindi');
1824
+ });
1825
+
1826
+ it('should build contains_all condition using ?& operator', () => {
1827
+ const condition = service['buildMultiSelectCondition'](
1828
+ 'LEAD__languages',
1829
+ 'contains_all',
1830
+ ['hindi', 'english'],
1831
+ 'param1'
1832
+ );
1833
+ expect(condition.query).toContain('?&');
1834
+ expect(condition.params.param1).toEqual(['hindi', 'english']);
1835
+ });
1836
+
1837
+ it('should handle empty array', () => {
1838
+ const condition = service['buildMultiSelectCondition'](
1839
+ 'LEAD__languages',
1840
+ 'empty',
1841
+ null,
1842
+ 'param1'
1843
+ );
1844
+ expect(condition.query).toContain('IS NULL');
1845
+ });
1846
+ });
1847
+ ```
1848
+
1849
+ ### Acceptance Criteria
1850
+ - [ ] Select conditions work for single and array values
1851
+ - [ ] Multiselect uses proper JSONB operators (`?`, `?|`, `?&`)
1852
+ - [ ] Array values converted to strings
1853
+ - [ ] Empty/not_empty handle NULL and empty arrays
1854
+ - [ ] Unit tests pass
1855
+ - [ ] Verify queries work with actual database
1856
+
1857
+ ---
1858
+
1859
+ ## Task 2.7: Main Filter Method Implementation
1860
+ **Duration: 4 hours | Files: flatjson-filter.service.ts**
1861
+
1862
+ ### Objective
1863
+ Implement the core `applyFlatjsonFilter()` method that orchestrates the entire filtering process.
1864
+
1865
+ ### Requirements
1866
+
1867
+ **Implement the main filtering method:**
1868
+ ```typescript
1869
+ async applyFlatjsonFilter(dto: FilterRequestDto): Promise<any> {
1870
+ const {
1871
+ entity_type,
1872
+ quickFilter = [],
1873
+ savedFilterCode,
1874
+ attributeFilter = [],
1875
+ tabs,
1876
+ sortby,
1877
+ loggedInUser,
1878
+ queryParams,
1879
+ page = 1,
1880
+ size = 10,
1881
+ } = dto;
1882
+
1883
+ await this.loggingService.log(
1884
+ 'info',
1885
+ 'FlatjsonFilterService',
1886
+ 'applyFlatjsonFilter',
1887
+ `Filtering ${entity_type} using flatjson`
1888
+ );
1889
+
1890
+ // Step 1: Load entity metadata
1891
+ const entityMeta = await this.entityMasterService.getEntityData(
1892
+ entity_type,
1893
+ loggedInUser
1894
+ );
1895
+
1896
+ if (!entityMeta) {
1897
+ throw new BadRequestException(`Invalid entity_type: ${entity_type}`);
1898
+ }
1899
+
1900
+ // Step 2: Load attribute metadata
1901
+ const attributes = await this.attributeMasterService.findAttributesByMappedEntityType(
1902
+ entity_type,
1903
+ loggedInUser
1904
+ );
1905
+
1906
+ const attributeMetaMap = attributes.reduce((acc, attr) => {
1907
+ acc[attr.attribute_key] = attr;
1908
+ return acc;
1909
+ }, {} as Record<string, any>);
1910
+
1911
+ // Step 3: Merge all filters
1912
+ const savedFilters = savedFilterCode
1913
+ ? await this.getSavedFilters(savedFilterCode)
1914
+ : [];
1915
+
1916
+ const allFilters = [
1917
+ ...quickFilter,
1918
+ ...attributeFilter,
1919
+ ...savedFilters,
1920
+ ].filter(f => f.filter_value !== '' && f.filter_value != null);
1921
+
1922
+ // Step 4: Build JSONB where clauses
1923
+ const baseWhere = this.buildJsonbConditions(allFilters, attributeMetaMap);
1924
+
1925
+ // Step 5: Add organization/level filter
1926
+ const orgId = loggedInUser.organization_id;
1927
+ const levelType = loggedInUser.level_type;
1928
+ const levelId = loggedInUser.level_id;
1929
+
1930
+ if (entity_type !== 'ORGP' && orgId && levelType && levelId) {
1931
+ baseWhere.push({
1932
+ query: `json_data->>'${entity_type}__organization_id' = :orgId
1933
+ AND json_data->>'${entity_type}__level_type' = :levelType
1934
+ AND json_data->>'${entity_type}__level_id' = :levelId`,
1935
+ params: {
1936
+ orgId: String(orgId),
1937
+ levelType,
1938
+ levelId: String(levelId)
1939
+ }
1940
+ });
1941
+ }
1942
+
1943
+ // Step 6: Handle queryParams filters
1944
+ if (queryParams) {
1945
+ Object.entries(queryParams).forEach(([key, value]) => {
1946
+ if (!value) return;
1947
+
1948
+ const flatKey = `${entity_type}__${key}`;
1949
+ baseWhere.push({
1950
+ query: `json_data->>'${flatKey}' = :qp_${key}`,
1951
+ params: { [`qp_${key}`]: String(value) }
1952
+ });
1953
+ });
1954
+ }
1955
+
1956
+ // Step 7: Build query on frm_entity_json
1957
+ let qb = this.entityManager
1958
+ .getRepository(EntityJson)
1959
+ .createQueryBuilder('ej')
1960
+ .select('ej.json_data')
1961
+ .where('ej.entity_type = :entityType', { entityType: entity_type });
1962
+
1963
+ // Apply all where clauses
1964
+ baseWhere.forEach(clause => {
1965
+ qb.andWhere(clause.query, clause.params);
1966
+ });
1967
+
1968
+ // Step 8: Handle tabs (if provided)
1969
+ let filteredTabs = [];
1970
+ if (tabs?.columnName) {
1971
+ const tabFlatKey = attributeMetaMap[tabs.columnName]?.flat_json_key
1972
+ || `${entity_type}__${tabs.columnName}`;
1973
+
1974
+ filteredTabs = await this.getJsonbTabCounts(
1975
+ entity_type,
1976
+ tabFlatKey,
1977
+ baseWhere
1978
+ );
1979
+
1980
+ // Apply tab filter if not "all"
1981
+ if (tabs.value && tabs.value.toLowerCase() !== 'all') {
1982
+ qb.andWhere(`json_data->>'${tabFlatKey}' = :tabValue`, {
1983
+ tabValue: tabs.value.toLowerCase()
1984
+ });
1985
+ }
1986
+ }
1987
+
1988
+ // Step 9: Apply sorting
1989
+ if (sortby?.length > 0) {
1990
+ this.applyJsonbSorting(qb, sortby, attributeMetaMap);
1991
+ } else {
1992
+ // Default sort by created_at
1993
+ qb.orderBy(`(json_data->>'${entity_type}__created_at')::bigint`, 'DESC');
1994
+ }
1995
+
1996
+ // Step 10: Get total count (before pagination)
1997
+ const countQb = qb.clone();
1998
+ const totalCount = await countQb.getCount();
1999
+
2000
+ // Step 11: Apply pagination
2001
+ const actualPage = page > 0 ? page : 1;
2002
+ const actualSize = size > 0 ? size : 10;
2003
+ qb.skip((actualPage - 1) * actualSize).take(actualSize);
2004
+
2005
+ // Step 12: Execute query
2006
+ const startTime = Date.now();
2007
+ const results = await qb.getRawMany();
2008
+ const queryTime = Date.now() - startTime;
2009
+
2010
+ await this.loggingService.log(
2011
+ 'info',
2012
+ 'FlatjsonFilterService',
2013
+ 'applyFlatjsonFilter',
2014
+ `Query executed in ${queryTime}ms, returned ${results.length} records`
2015
+ );
2016
+
2017
+ // Step 13: Extract json_data from results
2018
+ const entity_list = results.map(r => r.ej_json_data);
2019
+
2020
+ // Step 14: Resolve data (if needed - optional)
2021
+ const resolvedEntityList = await Promise.all(
2022
+ entity_list.map(row =>
2023
+ this.resolverService.getResolvedData(loggedInUser, row, entity_type)
2024
+ )
2025
+ );
2026
+
2027
+ // Step 15: Return formatted response
2028
+ return {
2029
+ success: true,
2030
+ data: {
2031
+ entity_tabs: filteredTabs,
2032
+ entity_list: resolvedEntityList,
2033
+ pagination: {
2034
+ total: totalCount,
2035
+ page: actualPage,
2036
+ size: actualSize,
2037
+ totalPages: Math.ceil(totalCount / actualSize),
2038
+ hasNextPage: actualPage * actualSize < totalCount,
2039
+ hasPreviousPage: actualPage > 1,
2040
+ },
2041
+ },
2042
+ performance: {
2043
+ query_time_ms: queryTime,
2044
+ using_flatjson: true,
2045
+ }
2046
+ };
2047
+ }
2048
+
2049
+ /**
2050
+ * Build all JSONB conditions from filters
2051
+ */
2052
+ private buildJsonbConditions(
2053
+ filters: FilterCondition[],
2054
+ attributeMetaMap: Record<string, any>
2055
+ ): JsonbCondition[] {
2056
+ return filters
2057
+ .map(f => this.buildJsonbCondition(f, attributeMetaMap[f.filter_attribute]))
2058
+ .filter((c): c is JsonbCondition => c !== null);
2059
+ }
2060
+
2061
+ /**
2062
+ * Get saved filters by code
2063
+ */
2064
+ private async getSavedFilters(code: string): Promise<FilterCondition[]> {
2065
+ // Inject SavedFilterService if not already
2066
+ // Return saved filter conditions
2067
+ // This mirrors FilterService.getSavedFilters()
2068
+ return [];
2069
+ }
2070
+ ```
2071
+
2072
+ ### Testing Requirements
2073
+ - Test with no filters (should return all entities of type)
2074
+ - Test with text filters
2075
+ - Test with number filters
2076
+ - Test with date filters
2077
+ - Test with pagination
2078
+ - Test with sorting
2079
+ - Test with tabs
2080
+ - Test performance (log query execution time)
2081
+ - Compare results with FilterService.applyFilter() for same input
2082
+
2083
+ ### Acceptance Criteria
2084
+ - [ ] Method implemented and working end-to-end
2085
+ - [ ] Queries frm_entity_json table only
2086
+ - [ ] Uses GIN index (verify with EXPLAIN ANALYZE)
2087
+ - [ ] Returns same structure as FilterService
2088
+ - [ ] Pagination works correctly
2089
+ - [ ] Sorting works on any flatjson field
2090
+ - [ ] Performance logged
2091
+ - [ ] Integration test passes
2092
+
2093
+ ---
2094
+
2095
+ ## Task 2.8: Tab Aggregation on JSONB
2096
+ **Duration: 2 hours | Files: flatjson-filter.service.ts**
2097
+
2098
+ ### Objective
2099
+ Implement tab count aggregation directly on JSONB data.
2100
+
2101
+ ### Requirements
2102
+
2103
+ **Implement `getJsonbTabCounts()` method:**
2104
+ ```typescript
2105
+ private async getJsonbTabCounts(
2106
+ entity_type: string,
2107
+ flatJsonKey: string,
2108
+ whereClauses: JsonbCondition[]
2109
+ ): Promise<Array<{ tab_value: string; tab_value_count: number }>> {
2110
+ // Build base query
2111
+ let qb = this.entityManager
2112
+ .getRepository(EntityJson)
2113
+ .createQueryBuilder('ej')
2114
+ .select(`json_data->>'${flatJsonKey}'`, 'tab_value')
2115
+ .addSelect('COUNT(*)', 'tab_value_count')
2116
+ .where('ej.entity_type = :entityType', { entityType: entity_type })
2117
+ .groupBy(`json_data->>'${flatJsonKey}'`);
2118
+
2119
+ // Apply all where clauses
2120
+ whereClauses.forEach(clause => {
2121
+ qb.andWhere(clause.query, clause.params);
2122
+ });
2123
+
2124
+ // Execute query
2125
+ const rows = await qb.getRawMany();
2126
+
2127
+ // Calculate total
2128
+ const total = rows.reduce((sum, r) => sum + parseInt(r.tab_value_count, 10), 0);
2129
+
2130
+ // Return with "All" tab
2131
+ return [
2132
+ { tab_value: 'All', tab_value_count: total },
2133
+ ...rows.map(r => ({
2134
+ tab_value: r.tab_value ?? 'BLANK',
2135
+ tab_value_count: parseInt(r.tab_value_count, 10)
2136
+ }))
2137
+ ];
2138
+ }
2139
+ ```
2140
+
2141
+ ### Testing Requirements
2142
+ ```typescript
2143
+ describe('FlatjsonFilterService - Tab Aggregation', () => {
2144
+ it('should aggregate counts by tab attribute', async () => {
2145
+ const tabs = await service['getJsonbTabCounts'](
2146
+ 'LEAD',
2147
+ 'LEAD__status',
2148
+ []
2149
+ );
2150
+
2151
+ expect(tabs).toContainEqual(
2152
+ expect.objectContaining({ tab_value: 'All' })
2153
+ );
2154
+ expect(tabs.length).toBeGreaterThan(1);
2155
+ });
2156
+
2157
+ it('should apply filters before aggregation', async () => {
2158
+ const whereClauses = [{
2159
+ query: "json_data->>'LEAD__city' = :city",
2160
+ params: { city: 'mumbai' }
2161
+ }];
2162
+
2163
+ const tabs = await service['getJsonbTabCounts'](
2164
+ 'LEAD',
2165
+ 'LEAD__status',
2166
+ whereClauses
2167
+ );
2168
+
2169
+ // Should only count entities in Mumbai
2170
+ expect(tabs.find(t => t.tab_value === 'All').tab_value_count).toBeLessThanOrEqual(/* total entities */);
2171
+ });
2172
+ });
2173
+ ```
2174
+
2175
+ ### Acceptance Criteria
2176
+ - [ ] Aggregates counts by tab attribute
2177
+ - [ ] Includes "All" tab with total count
2178
+ - [ ] Handles NULL values as "BLANK"
2179
+ - [ ] Applies base filters before grouping
2180
+ - [ ] Performance acceptable (<500ms for 100k records)
2181
+ - [ ] Unit tests pass
2182
+
2183
+ ---
2184
+
2185
+ ## Task 2.9: Sorting on JSONB Fields
2186
+ **Duration: 1 hour | Files: flatjson-filter.service.ts**
2187
+
2188
+ ### Objective
2189
+ Implement sorting on flatjson fields with proper type casting.
2190
+
2191
+ ### Requirements
2192
+
2193
+ **Implement `applyJsonbSorting()` method:**
2194
+ ```typescript
2195
+ private applyJsonbSorting(
2196
+ qb: SelectQueryBuilder<EntityJson>,
2197
+ sortby: SortConfig[],
2198
+ attributeMetaMap: Record<string, any>
2199
+ ): void {
2200
+ sortby.forEach(({ sortColum, sortType }) => {
2201
+ if (!sortColum) return;
2202
+
2203
+ const meta = attributeMetaMap[sortColum];
2204
+ if (!meta) {
2205
+ console.warn(`No metadata found for sort column: ${sortColum}`);
2206
+ return;
2207
+ }
2208
+
2209
+ const flatKey = meta.flat_json_key || `${meta.mapped_entity_type}__${sortColum}`;
2210
+ const direction = sortType?.toUpperCase() === 'DSC' ? 'DESC' : 'ASC';
2211
+
2212
+ // Apply type casting based on data_type
2213
+ let sortExpression: string;
2214
+
2215
+ switch (meta.data_type) {
2216
+ case 'text':
2217
+ sortExpression = `json_data->>'${flatKey}'`;
2218
+ break;
2219
+
2220
+ case 'number':
2221
+ case 'year':
2222
+ sortExpression = `(json_data->>'${flatKey}')::numeric`;
2223
+ break;
2224
+
2225
+ case 'date':
2226
+ sortExpression = `(json_data->>'${flatKey}')::bigint`;
2227
+ break;
2228
+
2229
+ default:
2230
+ sortExpression = `json_data->>'${flatKey}'`;
2231
+ }
2232
+
2233
+ qb.addOrderBy(sortExpression, direction);
2234
+ });
2235
+ }
2236
+ ```
2237
+
2238
+ ### Testing Requirements
2239
+ ```typescript
2240
+ describe('FlatjsonFilterService - Sorting', () => {
2241
+ it('should sort text fields alphabetically', async () => {
2242
+ const qb = /* mock query builder */;
2243
+ service['applyJsonbSorting'](qb, [{ sortColum: 'name', sortType: 'ASC' }], attributeMetaMap);
2244
+
2245
+ expect(qb.addOrderBy).toHaveBeenCalledWith(
2246
+ expect.stringContaining("json_data->>'LEAD__name'"),
2247
+ 'ASC'
2248
+ );
2249
+ });
2250
+
2251
+ it('should cast number fields for sorting', async () => {
2252
+ const qb = /* mock query builder */;
2253
+ service['applyJsonbSorting'](qb, [{ sortColum: 'age', sortType: 'DESC' }], attributeMetaMap);
2254
+
2255
+ expect(qb.addOrderBy).toHaveBeenCalledWith(
2256
+ expect.stringContaining('::numeric'),
2257
+ 'DESC'
2258
+ );
2259
+ });
2260
+
2261
+ it('should cast date fields as bigint for sorting', async () => {
2262
+ const qb = /* mock query builder */;
2263
+ service['applyJsonbSorting'](qb, [{ sortColum: 'created_at', sortType: 'DESC' }], attributeMetaMap);
2264
+
2265
+ expect(qb.addOrderBy).toHaveBeenCalledWith(
2266
+ expect.stringContaining('::bigint'),
2267
+ 'DESC'
2268
+ );
2269
+ });
2270
+ });
2271
+ ```
2272
+
2273
+ ### Acceptance Criteria
2274
+ - [ ] Handles multiple sort columns
2275
+ - [ ] Proper type casting based on data_type
2276
+ - [ ] ASC/DESC support
2277
+ - [ ] Works with GIN index
2278
+ - [ ] Unit tests pass
2279
+
2280
+ ---
2281
+
2282
+ ## Task 2.10: Sub-Entity Filter Handling
2283
+ **Duration: 2 hours | Files: flatjson-filter.service.ts**
2284
+
2285
+ ### Objective
2286
+ Handle filters on sub-entities (1:M relationships not in flatjson).
2287
+
2288
+ ### Strategy
2289
+ Since LinkedAttributes already map custom 1:M data into flatjson, most sub-entity filtering should work directly. However, for truly dynamic 1:M queries not covered by LinkedAttributes, implement a hybrid approach.
2290
+
2291
+ ### Requirements
2292
+
2293
+ **Add method: `applyFlatjsonFilterWrapper()`**
2294
+ ```typescript
2295
+ /**
2296
+ * Wrapper that handles sub-entity filters before querying flatjson
2297
+ * Similar to FilterService.applyFilterWrapper()
2298
+ */
2299
+ async applyFlatjsonFilterWrapper(dto: FilterRequestDto): Promise<any> {
2300
+ const { entity_type, quickFilter = [], savedFilterCode, attributeFilter = [] } = dto;
2301
+
2302
+ // Step 1: Get saved filters
2303
+ const savedFilters = savedFilterCode
2304
+ ? await this.getSavedFilters(savedFilterCode)
2305
+ : [];
2306
+
2307
+ const allFilters = [...quickFilter, ...attributeFilter, ...savedFilters];
2308
+
2309
+ // Step 2: Group filters by entity_type
2310
+ const grouped = allFilters.reduce((acc, f) => {
2311
+ const filterEntityType = f.filter_entity_type || entity_type;
2312
+ if (!acc[filterEntityType]) acc[filterEntityType] = [];
2313
+ acc[filterEntityType].push(f);
2314
+ return acc;
2315
+ }, {} as Record<string, FilterCondition[]>);
2316
+
2317
+ // Step 3: Check if there are sub-entity filters
2318
+ const subEntityTypes = Object.keys(grouped).filter(t => t !== entity_type);
2319
+
2320
+ if (subEntityTypes.length === 0) {
2321
+ // No sub-entity filters, just call main method
2322
+ return await this.applyFlatjsonFilter(dto);
2323
+ }
2324
+
2325
+ // Step 4: For each sub-entity, check if it's mapped in flatjson
2326
+ let intersectionIds: number[] | null = null;
2327
+
2328
+ for (const subEntityType of subEntityTypes) {
2329
+ const subFilters = grouped[subEntityType];
2330
+
2331
+ // Check if this sub-entity data exists in flatjson
2332
+ // (i.e., there are LinkedAttributes for it)
2333
+ const linkedAttrs = await this.dataSource
2334
+ .getRepository(LinkedAttributes)
2335
+ .find({
2336
+ where: {
2337
+ mapped_entity_type: entity_type,
2338
+ applicable_entity_type: subEntityType,
2339
+ }
2340
+ });
2341
+
2342
+ if (linkedAttrs.length > 0) {
2343
+ // Sub-entity data is in flatjson, can filter directly
2344
+ // Add to main filters
2345
+ grouped[entity_type].push(...subFilters);
2346
+ } else {
2347
+ // Sub-entity data NOT in flatjson, need to query traditional way
2348
+ // This mirrors FilterService.applyFilterWrapper() logic
2349
+
2350
+ // Query sub-entity table using traditional FilterService
2351
+ const subDto = {
2352
+ ...dto,
2353
+ entity_type: subEntityType,
2354
+ quickFilter: subFilters,
2355
+ savedFilterCode: null,
2356
+ attributeFilter: [],
2357
+ };
2358
+
2359
+ const subResult = await this.filterService.applyFilter(subDto);
2360
+ const subEntityIds = subResult.data.entity_list.map(row => row.id);
2361
+
2362
+ if (!subEntityIds.length) {
2363
+ // No matching sub-entities, return empty
2364
+ return {
2365
+ success: true,
2366
+ data: { entity_tabs: [], entity_list: [], pagination: {} }
2367
+ };
2368
+ }
2369
+
2370
+ // Map sub-entity IDs to main entity IDs using EntityRelationService
2371
+ const relatedIds = await this.entityRelationService.getRelatedEntityIds(
2372
+ entity_type,
2373
+ subEntityType,
2374
+ subEntityIds,
2375
+ dto.loggedInUser.organization_id
2376
+ );
2377
+
2378
+ // Apply intersection
2379
+ intersectionIds = intersectionIds === null
2380
+ ? relatedIds
2381
+ : intersectionIds.filter(id => relatedIds.includes(id));
2382
+
2383
+ if (intersectionIds.length === 0) {
2384
+ return {
2385
+ success: true,
2386
+ data: { entity_tabs: [], entity_list: [], pagination: {} }
2387
+ };
2388
+ }
2389
+ }
2390
+ }
2391
+
2392
+ // Step 5: Call main filter with ID intersection
2393
+ const mainDto = {
2394
+ ...dto,
2395
+ quickFilter: grouped[entity_type] || [],
2396
+ savedFilterCode: null,
2397
+ attributeFilter: [],
2398
+ };
2399
+
2400
+ if (intersectionIds && intersectionIds.length > 0) {
2401
+ // Add ID filter to main query
2402
+ mainDto.quickFilter.push({
2403
+ filter_entity_type: entity_type,
2404
+ filter_attribute: 'id',
2405
+ filter_operator: 'in',
2406
+ filter_value: intersectionIds,
2407
+ });
2408
+ }
2409
+
2410
+ return await this.applyFlatjsonFilter(mainDto);
2411
+ }
2412
+ ```
2413
+
2414
+ ### Dependencies
2415
+ Inject these services if not already:
2416
+ ```typescript
2417
+ @Inject() private readonly filterService: FilterService,
2418
+ @Inject() private readonly entityRelationService: EntityRelationService,
2419
+ ```
2420
+
2421
+ ### Testing Requirements
2422
+ - Test with sub-entity filters that ARE in flatjson (via LinkedAttributes)
2423
+ - Test with sub-entity filters that are NOT in flatjson
2424
+ - Test with mixed filters (main + sub-entity)
2425
+ - Verify correct results match FilterService behavior
2426
+
2427
+ ### Acceptance Criteria
2428
+ - [ ] LinkedAttributes data filterable directly in flatjson
2429
+ - [ ] Hybrid approach for unmapped sub-entities works
2430
+ - [ ] Correct results returned
2431
+ - [ ] Performance acceptable
2432
+ - [ ] Unit tests pass
2433
+
2434
+ ---
2435
+
2436
+ ## Task 2.11: Integration & Routing
2437
+ **Duration: 2 hours | Files: filter.controller.ts, entity-master.entity.ts**
2438
+
2439
+ ### Objective
2440
+ Add routing logic to switch between traditional and flatjson filtering.
2441
+
2442
+ ### Requirements
2443
+
2444
+ **Option 1: Feature Flag in Entity Master (Recommended)**
2445
+
2446
+ 1. **Add migration for feature flag:**
2447
+ ```typescript
2448
+ // Migration: AddUseFlatjsonFiltering
2449
+ public async up(queryRunner: QueryRunner): Promise<void> {
2450
+ await queryRunner.query(`
2451
+ ALTER TABLE frm_entity_master
2452
+ ADD COLUMN IF NOT EXISTS use_flatjson_filtering BOOLEAN DEFAULT FALSE;
2453
+ `);
2454
+ }
2455
+
2456
+ public async down(queryRunner: QueryRunner): Promise<void> {
2457
+ await queryRunner.query(`
2458
+ ALTER TABLE frm_entity_master
2459
+ DROP COLUMN IF EXISTS use_flatjson_filtering;
2460
+ `);
2461
+ }
2462
+ ```
2463
+
2464
+ 2. **Update entity: `src/module/meta/entity/entity-master.entity.ts`**
2465
+ ```typescript
2466
+ @Column({ type: 'boolean', default: false })
2467
+ use_flatjson_filtering: boolean;
2468
+ ```
2469
+
2470
+ 3. **Update controller: `src/module/filter/controller/filter.controller.ts`**
2471
+ ```typescript
2472
+ import { FlatjsonFilterService } from '../service/flatjson-filter.service';
2473
+
2474
+ @Controller('filter')
2475
+ export class FilterController {
2476
+ constructor(
2477
+ private readonly filterService: FilterService,
2478
+ private readonly flatjsonFilterService: FlatjsonFilterService,
2479
+ private readonly entityMasterService: EntityMasterService,
2480
+ ) {}
2481
+
2482
+ @Post('/apply')
2483
+ async applyFilter(@Body() dto: FilterRequestDto, @Req() req: any) {
2484
+ dto.loggedInUser = req.loggedInUser;
2485
+
2486
+ // Check if entity uses flatjson filtering
2487
+ const entityMeta = await this.entityMasterService.getEntityData(
2488
+ dto.entity_type,
2489
+ dto.loggedInUser
2490
+ );
2491
+
2492
+ if (entityMeta?.use_flatjson_filtering) {
2493
+ // Use new flatjson filtering
2494
+ return await this.flatjsonFilterService.applyFlatjsonFilterWrapper(dto);
2495
+ }
2496
+
2497
+ // Use traditional filtering (backward compatible)
2498
+ return await this.filterService.applyFilterWrapper(dto);
2499
+ }
2500
+ }
2501
+ ```
2502
+
2503
+ **Option 2: Separate Endpoint**
2504
+
2505
+ Add new endpoint:
2506
+ ```typescript
2507
+ @Post('/apply-flatjson')
2508
+ async applyFlatjsonFilter(@Body() dto: FilterRequestDto, @Req() req: any) {
2509
+ dto.loggedInUser = req.loggedInUser;
2510
+ return await this.flatjsonFilterService.applyFlatjsonFilterWrapper(dto);
2511
+ }
2512
+ ```
2513
+
2514
+ **Option 3: Query Parameter**
2515
+ ```typescript
2516
+ @Post('/apply')
2517
+ async applyFilter(
2518
+ @Body() dto: FilterRequestDto,
2519
+ @Query('use_flatjson') useFlatjson: boolean,
2520
+ @Req() req: any
2521
+ ) {
2522
+ dto.loggedInUser = req.loggedInUser;
2523
+
2524
+ if (useFlatjson) {
2525
+ return await this.flatjsonFilterService.applyFlatjsonFilterWrapper(dto);
2526
+ }
2527
+
2528
+ return await this.filterService.applyFilterWrapper(dto);
2529
+ }
2530
+ ```
2531
+
2532
+ ### Testing Requirements
2533
+ - Test switching between implementations
2534
+ - Verify both return same results for same input
2535
+ - Test feature flag toggle
2536
+ - Verify backward compatibility
2537
+
2538
+ ### Acceptance Criteria
2539
+ - [ ] Routing logic implemented
2540
+ - [ ] Feature flag works (or query param)
2541
+ - [ ] No breaking changes to existing API
2542
+ - [ ] Both services work independently
2543
+ - [ ] Easy to switch between implementations
2544
+ - [ ] Integration tests pass
2545
+
2546
+ ---
2547
+
2548
+ # PHASE 3: TESTING & OPTIMIZATION
2549
+ **Priority: IMPORTANT | Duration: 5 hours**
2550
+
2551
+ ## Task 3.1: Unit Tests
2552
+ **Duration: 2 hours | Files: *.spec.ts**
2553
+
2554
+ ### Objective
2555
+ Ensure all new code has comprehensive unit test coverage.
2556
+
2557
+ ### Requirements
2558
+
2559
+ 1. **Test Coverage Target: 80%+**
2560
+
2561
+ 2. **Files to Test:**
2562
+ - `linked_attributes.service.spec.ts` (Phase 1 tests)
2563
+ - `flatjson-filter.service.spec.ts` (Phase 2 tests)
2564
+
2565
+ 3. **Test Suites:**
2566
+
2567
+ **For LinkedAttributesService:**
2568
+ - Sequence generation
2569
+ - Attribute key generation
2570
+ - Validation logic
2571
+ - Filter creation
2572
+ - Smart create
2573
+ - Backfill
2574
+
2575
+ **For FlatjsonFilterService:**
2576
+ - Text condition building
2577
+ - Number condition building
2578
+ - Date condition building
2579
+ - Select condition building
2580
+ - Multiselect condition building
2581
+ - Main filter method
2582
+ - Tab aggregation
2583
+ - Sorting
2584
+
2585
+ 4. **Run tests:**
2586
+ ```bash
2587
+ npm test -- --coverage
2588
+ ```
2589
+
2590
+ ### Acceptance Criteria
2591
+ - [ ] All unit tests pass
2592
+ - [ ] Code coverage > 80%
2593
+ - [ ] No skipped tests
2594
+ - [ ] Fast execution (< 30 seconds total)
2595
+
2596
+ ---
2597
+
2598
+ ## Task 3.2: Integration Tests
2599
+ **Duration: 2 hours | Files: *.integration.spec.ts**
2600
+
2601
+ ### Objective
2602
+ Test end-to-end flows with real database.
2603
+
2604
+ ### Requirements
2605
+
2606
+ 1. **Create integration test files:**
2607
+ - `linked_attributes.integration.spec.ts`
2608
+ - `flatjson-filter.integration.spec.ts`
2609
+
2610
+ 2. **Test Scenarios:**
2611
+
2612
+ **LinkedAttributes Integration:**
2613
+ ```typescript
2614
+ describe('LinkedAttributes E2E', () => {
2615
+ it('should create linked attribute and update all entities', async () => {
2616
+ // 1. Create test entities (3 LEADs with family members)
2617
+ // 2. Create linked attribute for father_name
2618
+ // 3. Verify flatjson updated for all entities
2619
+ // 4. Query with filter on father_name
2620
+ // 5. Verify results correct
2621
+ });
2622
+ });
2623
+ ```
2624
+
2625
+ **FlatjsonFilter Integration:**
2626
+ ```typescript
2627
+ describe('FlatjsonFilter E2E', () => {
2628
+ it('should filter entities using flatjson', async () => {
2629
+ // 1. Create test entities with various attributes
2630
+ // 2. Apply filters using FlatjsonFilterService
2631
+ // 3. Apply same filters using traditional FilterService
2632
+ // 4. Compare results (should be identical)
2633
+ });
2634
+
2635
+ it('should handle complex multi-condition filters', async () => {
2636
+ // Test with 5+ filter conditions
2637
+ });
2638
+
2639
+ it('should handle pagination correctly', async () => {
2640
+ // Create 50 test entities
2641
+ // Query with page=2, size=10
2642
+ // Verify correct records returned
2643
+ });
2644
+ });
2645
+ ```
2646
+
2647
+ 3. **Setup/Teardown:**
2648
+ ```typescript
2649
+ beforeAll(async () => {
2650
+ // Setup test database
2651
+ // Create test entities
2652
+ });
2653
+
2654
+ afterAll(async () => {
2655
+ // Cleanup test data
2656
+ });
2657
+ ```
2658
+
2659
+ ### Acceptance Criteria
2660
+ - [ ] All integration tests pass
2661
+ - [ ] Tests use real database (test schema)
2662
+ - [ ] Proper setup/teardown
2663
+ - [ ] Tests are idempotent (can run multiple times)
2664
+
2665
+ ---
2666
+
2667
+ ## Task 3.3: Performance Testing
2668
+ **Duration: 1 hour | Files: performance.spec.ts**
2669
+
2670
+ ### Objective
2671
+ Benchmark flatjson filtering vs. traditional filtering.
2672
+
2673
+ ### Requirements
2674
+
2675
+ 1. **Create performance test file:**
2676
+ ```typescript
2677
+ // src/module/filter/performance.spec.ts
2678
+
2679
+ describe('Filtering Performance Benchmark', () => {
2680
+ const sizes = [100, 1000, 10000, 100000];
2681
+
2682
+ for (const size of sizes) {
2683
+ it(`should compare performance with ${size} records`, async () => {
2684
+ // Setup: Create ${size} test entities
2685
+
2686
+ const dto = {
2687
+ entity_type: 'LEAD',
2688
+ quickFilter: [
2689
+ { filter_attribute: 'city', filter_operator: 'equal', filter_value: 'mumbai' },
2690
+ { filter_attribute: 'age', filter_operator: 'greater_than', filter_value: 18 },
2691
+ ],
2692
+ page: 1,
2693
+ size: 10,
2694
+ loggedInUser: testUser,
2695
+ };
2696
+
2697
+ // Test traditional filter
2698
+ const start1 = Date.now();
2699
+ const result1 = await filterService.applyFilter(dto);
2700
+ const time1 = Date.now() - start1;
2701
+
2702
+ // Test flatjson filter
2703
+ const start2 = Date.now();
2704
+ const result2 = await flatjsonFilterService.applyFlatjsonFilter(dto);
2705
+ const time2 = Date.now() - start2;
2706
+
2707
+ console.log(`\nDataset: ${size} records`);
2708
+ console.log(`Traditional: ${time1}ms`);
2709
+ console.log(`Flatjson: ${time2}ms`);
2710
+ console.log(`Speedup: ${(time1 / time2).toFixed(2)}x`);
2711
+
2712
+ // Verify GIN index usage
2713
+ const explainQuery = `EXPLAIN ANALYZE ${/* flatjson query */}`;
2714
+ const plan = await entityManager.query(explainQuery);
2715
+ expect(plan).toContain('Bitmap Index Scan');
2716
+ });
2717
+ }
2718
+ });
2719
+
2720
+ 2. **Run benchmarks:**
2721
+ ```bash
2722
+ npm test -- performance.spec.ts
2723
+ ```
2724
+
2725
+ 3. **Expected Results:**
2726
+ - 100 records: Similar performance
2727
+ - 1,000 records: 1.5-2x speedup
2728
+ - 10,000 records: 3-5x speedup
2729
+ - 100,000 records: 5-10x speedup
2730
+
2731
+ ### Acceptance Criteria
2732
+ - [ ] Benchmarks complete
2733
+ - [ ] Flatjson faster for datasets > 10k
2734
+ - [ ] GIN index verified as used
2735
+ - [ ] Query times logged
2736
+ - [ ] Performance regression tests added
2737
+
2738
+ ---
2739
+
2740
+ # COMPLETION CHECKLIST
2741
+
2742
+ ## Phase 1: Linked Attribute Automation
2743
+ - [ ] Task 1.1: Sequence & Key Generation
2744
+ - [ ] Task 1.2: Validation Engine
2745
+ - [ ] Task 1.3: Auto SavedFilter Creation
2746
+ - [ ] Task 1.4: Smart Create Method
2747
+ - [ ] Task 1.5: Backfill Mechanism
2748
+ - [ ] Task 1.6: Update CRUD & Controller
2749
+ - [ ] Task 1.7: Testing & Edge Cases
2750
+
2751
+ ## Phase 2: Flatjson Filtering Service
2752
+ - [ ] Task 2.1: Database Index Creation
2753
+ - [ ] Task 2.2: Service Skeleton & Infrastructure
2754
+ - [ ] Task 2.3: Text Query Builders
2755
+ - [ ] Task 2.4: Number Query Builders
2756
+ - [ ] Task 2.5: Date Query Builders
2757
+ - [ ] Task 2.6: Select/Multiselect Query Builders
2758
+ - [ ] Task 2.7: Main Filter Method
2759
+ - [ ] Task 2.8: Tab Aggregation
2760
+ - [ ] Task 2.9: Sorting
2761
+ - [ ] Task 2.10: Sub-Entity Handling
2762
+ - [ ] Task 2.11: Integration & Routing
2763
+
2764
+ ## Phase 3: Testing & Optimization
2765
+ - [ ] Task 3.1: Unit Tests (80% coverage)
2766
+ - [ ] Task 3.2: Integration Tests
2767
+ - [ ] Task 3.3: Performance Benchmarks
2768
+
2769
+ ## Documentation
2770
+ - [ ] API documentation updated (Swagger)
2771
+ - [ ] README updated with new features
2772
+ - [ ] Migration guide created
2773
+ - [ ] Performance comparison report
2774
+
2775
+ ## Deployment
2776
+ - [ ] All tests passing
2777
+ - [ ] Migrations tested
2778
+ - [ ] Staging deployment successful
2779
+ - [ ] Production deployment plan ready
2780
+
2781
+ ---
2782
+
2783
+ # EXECUTION INSTRUCTIONS FOR CLAUDE CODE
2784
+
2785
+ When you start working on this project:
2786
+
2787
+ 1. **Read this entire document first** to understand the full scope
2788
+ 2. **Start with Phase 1, Task 1.1** - don't skip ahead
2789
+ 3. **After each task:**
2790
+ - Run tests to verify
2791
+ - Commit code with meaningful message
2792
+ - Update this checklist
2793
+ - Wait for confirmation before proceeding
2794
+ 4. **If you encounter issues:**
2795
+ - Log the issue clearly
2796
+ - Suggest solutions
2797
+ - Wait for guidance
2798
+ 5. **Always:**
2799
+ - Follow existing code patterns
2800
+ - Add comprehensive comments
2801
+ - Write tests for new code
2802
+ - Log important operations
2803
+
2804
+ **Begin with Task 1.1 when ready!**