rez_core 6.5.58 → 6.5.61
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.
- package/dist/app.module.js +3 -5
- package/dist/app.module.js.map +1 -1
- package/dist/core.module.js +38 -63
- package/dist/core.module.js.map +1 -1
- package/dist/migrations/1732612800000-AddEntityJsonGinIndex.d.ts +6 -0
- package/dist/migrations/1732612800000-AddEntityJsonGinIndex.js +32 -0
- package/dist/migrations/1732612800000-AddEntityJsonGinIndex.js.map +1 -0
- package/dist/module/auth/strategies/jwt.strategy.d.ts +1 -2
- package/dist/module/auth/strategies/jwt.strategy.js +2 -3
- package/dist/module/auth/strategies/jwt.strategy.js.map +1 -1
- package/dist/module/dashboard/dashboard.module.js +1 -1
- package/dist/module/dashboard/dashboard.module.js.map +1 -1
- package/dist/module/dashboard/service/dashboard.service.js +2 -1
- package/dist/module/dashboard/service/dashboard.service.js.map +1 -1
- package/dist/module/enterprise/controller/organization.controller.d.ts +4 -12
- package/dist/module/enterprise/controller/organization.controller.js +8 -64
- package/dist/module/enterprise/controller/organization.controller.js.map +1 -1
- package/dist/module/enterprise/enterprise.module.js +15 -10
- package/dist/module/enterprise/enterprise.module.js.map +1 -1
- package/dist/module/enterprise/entity/enterprise.entity.d.ts +3 -1
- package/dist/module/enterprise/entity/enterprise.entity.js +12 -4
- package/dist/module/enterprise/entity/enterprise.entity.js.map +1 -1
- package/dist/module/enterprise/entity/organization-app-mapping.entity.d.ts +1 -6
- package/dist/module/enterprise/entity/organization-app-mapping.entity.js +4 -21
- package/dist/module/enterprise/entity/organization-app-mapping.entity.js.map +1 -1
- package/dist/module/enterprise/entity/organization.entity.d.ts +17 -3
- package/dist/module/enterprise/entity/organization.entity.js +73 -11
- package/dist/module/enterprise/entity/organization.entity.js.map +1 -1
- package/dist/module/enterprise/repository/enterprise.repository.d.ts +2 -4
- package/dist/module/enterprise/repository/enterprise.repository.js +4 -19
- package/dist/module/enterprise/repository/enterprise.repository.js.map +1 -1
- package/dist/module/enterprise/service/brand.service.d.ts +3 -0
- package/dist/module/enterprise/service/brand.service.js +17 -0
- package/dist/module/enterprise/service/brand.service.js.map +1 -1
- package/dist/module/enterprise/service/enterprise.service.d.ts +2 -2
- package/dist/module/enterprise/service/enterprise.service.js +4 -6
- package/dist/module/enterprise/service/enterprise.service.js.map +1 -1
- package/dist/module/enterprise/service/organization.service.d.ts +6 -6
- package/dist/module/enterprise/service/organization.service.js +27 -109
- package/dist/module/enterprise/service/organization.service.js.map +1 -1
- package/dist/module/entity_json/controller/entity_json.controller.d.ts +2 -9
- package/dist/module/entity_json/controller/entity_json.controller.js.map +1 -1
- package/dist/module/entity_json/entity/entityJson.entity.d.ts +2 -1
- package/dist/module/entity_json/entity/entityJson.entity.js +5 -1
- package/dist/module/entity_json/entity/entityJson.entity.js.map +1 -1
- package/dist/module/entity_json/entity_json.module.js +7 -2
- package/dist/module/entity_json/entity_json.module.js.map +1 -1
- package/dist/module/entity_json/service/entity_json.service.d.ts +2 -10
- package/dist/module/entity_json/service/entity_json.service.js +101 -25
- package/dist/module/entity_json/service/entity_json.service.js.map +1 -1
- package/dist/module/filter/controller/filter.controller.d.ts +12 -0
- package/dist/module/filter/controller/filter.controller.js +1 -1
- package/dist/module/filter/controller/filter.controller.js.map +1 -1
- package/dist/module/filter/filter.module.js +11 -2
- package/dist/module/filter/filter.module.js.map +1 -1
- package/dist/module/filter/service/filter.service.d.ts +38 -2
- package/dist/module/filter/service/filter.service.js +43 -50
- package/dist/module/filter/service/filter.service.js.map +1 -1
- package/dist/module/filter/service/flatjson-filter.service.d.ts +32 -0
- package/dist/module/filter/service/flatjson-filter.service.js +632 -0
- package/dist/module/filter/service/flatjson-filter.service.js.map +1 -0
- package/dist/module/filter/service/saved-filter.service.d.ts +3 -2
- package/dist/module/filter/service/saved-filter.service.js +14 -18
- package/dist/module/filter/service/saved-filter.service.js.map +1 -1
- package/dist/module/integration/service/integration.service.d.ts +1 -0
- package/dist/module/integration/service/integration.service.js +2 -1
- package/dist/module/integration/service/integration.service.js.map +1 -1
- package/dist/module/integration/service/wrapper.service.js +1 -0
- package/dist/module/integration/service/wrapper.service.js.map +1 -1
- package/dist/module/layout/controller/layout.controller.d.ts +3 -1
- package/dist/module/layout/controller/layout.controller.js +7 -3
- package/dist/module/layout/controller/layout.controller.js.map +1 -1
- package/dist/module/layout/entity/header-section.entity.d.ts +2 -0
- package/dist/module/layout/entity/header-section.entity.js +8 -0
- package/dist/module/layout/entity/header-section.entity.js.map +1 -1
- package/dist/module/layout/layout.module.js +2 -1
- package/dist/module/layout/layout.module.js.map +1 -1
- package/dist/module/layout/repository/header-section.repository.d.ts +1 -0
- package/dist/module/layout/repository/header-section.repository.js +5 -0
- package/dist/module/layout/repository/header-section.repository.js.map +1 -1
- package/dist/module/layout/service/header-section.service.d.ts +1 -1
- package/dist/module/layout/service/header-section.service.js +1 -1
- package/dist/module/layout/service/header-section.service.js.map +1 -1
- package/dist/module/linked_attributes/controller/linked_attributes.controller.d.ts +41 -0
- package/dist/module/linked_attributes/controller/linked_attributes.controller.js +90 -0
- package/dist/module/linked_attributes/controller/linked_attributes.controller.js.map +1 -1
- package/dist/module/linked_attributes/dto/create-linked-attribute-smart.dto.d.ts +13 -0
- package/dist/module/linked_attributes/dto/create-linked-attribute-smart.dto.js +64 -0
- package/dist/module/linked_attributes/dto/create-linked-attribute-smart.dto.js.map +1 -0
- package/dist/module/linked_attributes/linked_attributes.module.js +8 -1
- package/dist/module/linked_attributes/linked_attributes.module.js.map +1 -1
- package/dist/module/linked_attributes/service/linked_attributes.service.d.ts +65 -1
- package/dist/module/linked_attributes/service/linked_attributes.service.js +287 -2
- package/dist/module/linked_attributes/service/linked_attributes.service.js.map +1 -1
- package/dist/module/listmaster/service/list-master.service.js +8 -1
- package/dist/module/listmaster/service/list-master.service.js.map +1 -1
- package/dist/module/meta/controller/app-master.controller.js.map +1 -0
- package/dist/module/meta/controller/attribute-master.controller.d.ts +3 -0
- package/dist/module/meta/controller/attribute-master.controller.js +12 -0
- package/dist/module/meta/controller/attribute-master.controller.js.map +1 -1
- package/dist/module/meta/controller/meta.controller.d.ts +6 -1
- package/dist/module/meta/controller/meta.controller.js +19 -1
- package/dist/module/meta/controller/meta.controller.js.map +1 -1
- package/dist/module/meta/entity/app-master.entity.d.ts +13 -0
- package/dist/module/{app_master → meta}/entity/app-master.entity.js +12 -30
- package/dist/module/meta/entity/app-master.entity.js.map +1 -0
- package/dist/module/meta/entity/entity-master.entity.d.ts +1 -0
- package/dist/module/meta/entity/entity-master.entity.js +8 -1
- package/dist/module/meta/entity/entity-master.entity.js.map +1 -1
- package/dist/module/meta/entity.module.js +14 -3
- package/dist/module/meta/entity.module.js.map +1 -1
- package/dist/module/{app_master → meta}/repository/app-master.repository.d.ts +2 -2
- package/dist/module/{app_master → meta}/repository/app-master.repository.js +4 -4
- package/dist/module/meta/repository/app-master.repository.js.map +1 -0
- package/dist/module/meta/service/app-master.service.js.map +1 -0
- package/dist/module/meta/service/attribute-master.service.d.ts +6 -1
- package/dist/module/meta/service/attribute-master.service.js +22 -2
- package/dist/module/meta/service/attribute-master.service.js.map +1 -1
- package/dist/module/meta/service/entity-master.service.js +1 -0
- package/dist/module/meta/service/entity-master.service.js.map +1 -1
- package/dist/module/meta/service/entity-relation.service.d.ts +4 -3
- package/dist/module/meta/service/entity-relation.service.js +10 -4
- package/dist/module/meta/service/entity-relation.service.js.map +1 -1
- package/dist/module/meta/service/entity-service-impl.service.d.ts +1 -1
- package/dist/module/meta/service/entity-service-impl.service.js +14 -10
- package/dist/module/meta/service/entity-service-impl.service.js.map +1 -1
- package/dist/module/meta/service/entity-table.service.d.ts +5 -4
- package/dist/module/meta/service/entity-table.service.js +45 -24
- package/dist/module/meta/service/entity-table.service.js.map +1 -1
- package/dist/module/meta/service/populate-meta.service.d.ts +13 -0
- package/dist/module/{enterprise → meta}/service/populate-meta.service.js +8 -2
- package/dist/module/meta/service/populate-meta.service.js.map +1 -0
- package/dist/module/meta/service/resolver.service.d.ts +1 -1
- package/dist/module/meta/service/resolver.service.js +6 -3
- package/dist/module/meta/service/resolver.service.js.map +1 -1
- package/dist/module/module/controller/module-access.controller.d.ts +4 -3
- package/dist/module/module/controller/module-access.controller.js +8 -13
- package/dist/module/module/controller/module-access.controller.js.map +1 -1
- package/dist/module/module/entity/menu.entity.d.ts +3 -6
- package/dist/module/module/entity/menu.entity.js +10 -19
- package/dist/module/module/entity/menu.entity.js.map +1 -1
- package/dist/module/module/entity/module-access.entity.d.ts +1 -15
- package/dist/module/module/entity/module-access.entity.js +3 -49
- package/dist/module/module/entity/module-access.entity.js.map +1 -1
- package/dist/module/module/entity/module-action.entity.d.ts +2 -4
- package/dist/module/module/entity/module-action.entity.js +6 -11
- package/dist/module/module/entity/module-action.entity.js.map +1 -1
- package/dist/module/module/entity/module.entity.d.ts +5 -3
- package/dist/module/module/entity/module.entity.js +18 -8
- package/dist/module/module/entity/module.entity.js.map +1 -1
- package/dist/module/module/module.module.d.ts +1 -1
- package/dist/module/module/module.module.js +7 -5
- package/dist/module/module/module.module.js.map +1 -1
- package/dist/module/module/repository/menu.repository.d.ts +3 -3
- package/dist/module/module/repository/menu.repository.js +27 -38
- package/dist/module/module/repository/menu.repository.js.map +1 -1
- package/dist/module/module/repository/module-access.repository.d.ts +6 -6
- package/dist/module/module/repository/module-access.repository.js +50 -100
- package/dist/module/module/repository/module-access.repository.js.map +1 -1
- package/dist/module/module/service/menu.service.d.ts +4 -2
- package/dist/module/module/service/menu.service.js +10 -7
- package/dist/module/module/service/menu.service.js.map +1 -1
- package/dist/module/module/service/module-access.service.d.ts +10 -7
- package/dist/module/module/service/module-access.service.js +24 -22
- package/dist/module/module/service/module-access.service.js.map +1 -1
- package/dist/module/notification/entity/notification.entity.d.ts +2 -17
- package/dist/module/notification/entity/notification.entity.js +2 -68
- package/dist/module/notification/entity/notification.entity.js.map +1 -1
- package/dist/module/notification/notification.module.js +4 -3
- package/dist/module/notification/notification.module.js.map +1 -1
- package/dist/module/notification/service/email.service.d.ts +0 -1
- package/dist/module/notification/service/email.service.js +0 -14
- package/dist/module/notification/service/email.service.js.map +1 -1
- package/dist/module/notification/service/notification.service.d.ts +3 -1
- package/dist/module/notification/service/notification.service.js +5 -2
- package/dist/module/notification/service/notification.service.js.map +1 -1
- package/dist/module/notification/service/otp.service.d.ts +2 -2
- package/dist/module/notification/service/otp.service.js +5 -4
- package/dist/module/notification/service/otp.service.js.map +1 -1
- package/dist/module/user/controller/login.controller.d.ts +3 -1
- package/dist/module/user/controller/login.controller.js +6 -2
- package/dist/module/user/controller/login.controller.js.map +1 -1
- package/dist/module/user/controller/user.controller.d.ts +2 -0
- package/dist/module/user/controller/user.controller.js +13 -0
- package/dist/module/user/controller/user.controller.js.map +1 -1
- package/dist/module/user/dto/create-user.dto.d.ts +3 -6
- package/dist/module/user/dto/create-user.dto.js +11 -17
- package/dist/module/user/dto/create-user.dto.js.map +1 -1
- package/dist/module/user/entity/role.entity.d.ts +6 -18
- package/dist/module/user/entity/role.entity.js +19 -64
- package/dist/module/user/entity/role.entity.js.map +1 -1
- package/dist/module/user/entity/user-role-mapping.entity.d.ts +0 -10
- package/dist/module/user/entity/user-role-mapping.entity.js +1 -33
- package/dist/module/user/entity/user-role-mapping.entity.js.map +1 -1
- package/dist/module/user/entity/user-session.entity.d.ts +2 -0
- package/dist/module/user/entity/user-session.entity.js +20 -2
- package/dist/module/user/entity/user-session.entity.js.map +1 -1
- package/dist/module/user/entity/user.entity.d.ts +5 -17
- package/dist/module/user/entity/user.entity.js +15 -61
- package/dist/module/user/entity/user.entity.js.map +1 -1
- package/dist/module/user/repository/role.repository.d.ts +2 -7
- package/dist/module/user/repository/role.repository.js +8 -23
- package/dist/module/user/repository/role.repository.js.map +1 -1
- package/dist/module/user/repository/user-role-mapping.repository.d.ts +0 -1
- package/dist/module/user/repository/user-role-mapping.repository.js +0 -3
- package/dist/module/user/repository/user-role-mapping.repository.js.map +1 -1
- package/dist/module/user/repository/user.repository.d.ts +2 -5
- package/dist/module/user/repository/user.repository.js +7 -26
- package/dist/module/user/repository/user.repository.js.map +1 -1
- package/dist/module/user/repository/userSession.repository.d.ts +0 -1
- package/dist/module/user/repository/userSession.repository.js +0 -3
- package/dist/module/user/repository/userSession.repository.js.map +1 -1
- package/dist/module/user/service/login.service.d.ts +5 -3
- package/dist/module/user/service/login.service.js +42 -43
- package/dist/module/user/service/login.service.js.map +1 -1
- package/dist/module/user/service/role.service.d.ts +11 -24
- package/dist/module/user/service/role.service.js +40 -54
- package/dist/module/user/service/role.service.js.map +1 -1
- package/dist/module/user/service/user-role-mapping.service.d.ts +0 -2
- package/dist/module/user/service/user-role-mapping.service.js +0 -6
- package/dist/module/user/service/user-role-mapping.service.js.map +1 -1
- package/dist/module/user/service/user-session.service.d.ts +4 -3
- package/dist/module/user/service/user-session.service.js +11 -10
- package/dist/module/user/service/user-session.service.js.map +1 -1
- package/dist/module/user/service/user.service.d.ts +22 -33
- package/dist/module/user/service/user.service.js +58 -66
- package/dist/module/user/service/user.service.js.map +1 -1
- package/dist/module/user/user.module.js +7 -2
- package/dist/module/user/user.module.js.map +1 -1
- package/dist/module/workflow/repository/action-data.repository.d.ts +1 -1
- package/dist/module/workflow/repository/action-data.repository.js +8 -6
- package/dist/module/workflow/repository/action-data.repository.js.map +1 -1
- package/dist/module/workflow/repository/action.repository.d.ts +1 -1
- package/dist/module/workflow/repository/action.repository.js +10 -10
- package/dist/module/workflow/repository/action.repository.js.map +1 -1
- package/dist/module/workflow/repository/form-master.repository.d.ts +1 -1
- package/dist/module/workflow/repository/form-master.repository.js +2 -2
- package/dist/module/workflow/repository/form-master.repository.js.map +1 -1
- package/dist/module/workflow/service/action-data.service.js +2 -1
- package/dist/module/workflow/service/action-data.service.js.map +1 -1
- package/dist/module/workflow/service/action.service.js +2 -2
- package/dist/module/workflow/service/action.service.js.map +1 -1
- package/dist/module/workflow/service/comm-template.service.js +2 -0
- package/dist/module/workflow/service/comm-template.service.js.map +1 -1
- package/dist/module/workflow/service/entity-modification.service.js +1 -0
- package/dist/module/workflow/service/entity-modification.service.js.map +1 -1
- package/dist/module/workflow/service/form-master.service.js +2 -2
- package/dist/module/workflow/service/form-master.service.js.map +1 -1
- package/dist/module/workflow/service/populate-workflow.service.d.ts +1 -1
- package/dist/module/workflow/service/populate-workflow.service.js +1 -1
- package/dist/module/workflow/service/populate-workflow.service.js.map +1 -1
- package/dist/module/workflow/service/task.service.js +3 -0
- package/dist/module/workflow/service/task.service.js.map +1 -1
- package/dist/module/workflow/service/workflow-meta.service.js +7 -2
- package/dist/module/workflow/service/workflow-meta.service.js.map +1 -1
- package/dist/module/workflow/service/workflow.service.js +2 -2
- package/dist/module/workflow/service/workflow.service.js.map +1 -1
- package/dist/module/workflow/workflow.module.js +0 -2
- package/dist/module/workflow/workflow.module.js.map +1 -1
- package/dist/module/workflow-automation/service/workflow-automation.service.js +6 -11
- package/dist/module/workflow-automation/service/workflow-automation.service.js.map +1 -1
- package/dist/module/workflow-automation/workflow-automation.module.js +1 -3
- package/dist/module/workflow-automation/workflow-automation.module.js.map +1 -1
- package/dist/module/workflow-schedule/service/workflow-schedule.service.js +2 -0
- package/dist/module/workflow-schedule/service/workflow-schedule.service.js.map +1 -1
- package/dist/table.config.d.ts +5 -3
- package/dist/table.config.js +3 -3
- package/dist/table.config.js.map +1 -1
- package/dist/tsconfig.build.tsbuildinfo +1 -1
- package/package.json +2 -2
- package/src/app.module.ts +5 -7
- package/src/core.module.ts +44 -58
- package/src/migrations/1732612800000-AddEntityJsonGinIndex.ts +41 -0
- package/src/module/auth/strategies/jwt.strategy.ts +2 -4
- package/src/module/dashboard/dashboard.module.ts +3 -3
- package/src/module/dashboard/service/dashboard.service.ts +2 -1
- package/src/module/enterprise/controller/organization.controller.ts +4 -60
- package/src/module/enterprise/enterprise.module.ts +18 -16
- package/src/module/enterprise/entity/enterprise.entity.ts +11 -5
- package/src/module/enterprise/entity/organization-app-mapping.entity.ts +4 -18
- package/src/module/enterprise/entity/organization.entity.ts +58 -11
- package/src/module/enterprise/repository/enterprise.repository.ts +4 -26
- package/src/module/enterprise/service/brand.service.ts +5 -75
- package/src/module/enterprise/service/enterprise.service.ts +4 -12
- package/src/module/enterprise/service/organization.service.ts +28 -151
- package/src/module/entity_json/controller/entity_json.controller.ts +13 -0
- package/src/module/entity_json/docs/FlatJson_Filterin_System.md +2804 -0
- package/src/module/entity_json/entity/entityJson.entity.ts +4 -1
- package/src/module/entity_json/entity_json.module.ts +9 -5
- package/src/module/entity_json/service/entity_json.service.ts +237 -51
- package/src/module/filter/controller/filter.controller.ts +1 -3
- package/src/module/filter/filter.module.ts +12 -3
- package/src/module/filter/service/filter.service.ts +130 -73
- package/src/module/filter/service/flatjson-filter.service.ts +903 -0
- package/src/module/filter/service/saved-filter.service.ts +16 -26
- package/src/module/filter/test/flatjson-filter.service.spec.ts +415 -0
- package/src/module/integration/service/integration.service.ts +6 -2
- package/src/module/integration/service/wrapper.service.ts +1 -0
- package/src/module/layout/controller/layout.controller.ts +8 -1
- package/src/module/layout/entity/header-section.entity.ts +6 -0
- package/src/module/layout/layout.module.ts +1 -1
- package/src/module/layout/repository/header-section.repository.ts +6 -0
- package/src/module/layout/service/header-section.service.ts +1 -1
- package/src/module/linked_attributes/controller/linked_attributes.controller.ts +100 -0
- package/src/module/linked_attributes/dto/create-linked-attribute-smart.dto.ts +54 -0
- package/src/module/linked_attributes/linked_attributes.module.ts +9 -2
- package/src/module/linked_attributes/service/linked_attributes.service.ts +578 -3
- package/src/module/linked_attributes/test/linked-attributes.service.spec.ts +244 -0
- package/src/module/listmaster/service/list-master.service.ts +9 -1
- package/src/module/meta/controller/attribute-master.controller.ts +12 -0
- package/src/module/meta/controller/meta.controller.ts +25 -3
- package/src/module/{app_master → meta}/entity/app-master.entity.ts +9 -22
- package/src/module/meta/entity/entity-master.entity.ts +9 -3
- package/src/module/meta/entity.module.ts +20 -6
- package/src/module/{app_master → meta}/repository/app-master.repository.ts +3 -3
- package/src/module/meta/service/attribute-master.service.ts +31 -1
- package/src/module/meta/service/entity-master.service.ts +1 -0
- package/src/module/meta/service/entity-relation.service.ts +10 -6
- package/src/module/meta/service/entity-service-impl.service.ts +14 -19
- package/src/module/meta/service/entity-table.service.ts +82 -68
- package/src/module/meta/service/entity.service.ts +0 -1
- package/src/module/{enterprise → meta}/service/populate-meta.service.ts +5 -2
- package/src/module/meta/service/resolver.service.ts +4 -0
- package/src/module/module/controller/module-access.controller.ts +9 -14
- package/src/module/module/entity/menu.entity.ts +10 -18
- package/src/module/module/entity/module-access.entity.ts +3 -40
- package/src/module/module/entity/module-action.entity.ts +6 -10
- package/src/module/module/entity/module.entity.ts +14 -7
- package/src/module/module/module.module.ts +3 -2
- package/src/module/module/repository/menu.repository.ts +29 -43
- package/src/module/module/repository/module-access.repository.ts +62 -110
- package/src/module/module/service/menu.service.ts +9 -7
- package/src/module/module/service/module-access.service.ts +34 -22
- package/src/module/notification/entity/notification.entity.ts +3 -53
- package/src/module/notification/notification.module.ts +5 -6
- package/src/module/notification/service/email.service.ts +1 -16
- package/src/module/notification/service/notification.service.ts +1 -0
- package/src/module/notification/service/otp.service.ts +5 -16
- package/src/module/user/controller/login.controller.ts +8 -7
- package/src/module/user/controller/user.controller.ts +9 -0
- package/src/module/user/dto/create-user.dto.ts +6 -19
- package/src/module/user/entity/role.entity.ts +16 -59
- package/src/module/user/entity/user-role-mapping.entity.ts +3 -29
- package/src/module/user/entity/user-session.entity.ts +19 -3
- package/src/module/user/entity/user.entity.ts +13 -48
- package/src/module/user/repository/role.repository.ts +12 -32
- package/src/module/user/repository/user-role-mapping.repository.ts +1 -5
- package/src/module/user/repository/user.repository.ts +9 -36
- package/src/module/user/repository/userSession.repository.ts +1 -5
- package/src/module/user/service/login.service.ts +51 -47
- package/src/module/user/service/role.service.ts +63 -64
- package/src/module/user/service/user-role-mapping.service.ts +1 -23
- package/src/module/user/service/user-session.service.ts +11 -14
- package/src/module/user/service/user.service.ts +95 -76
- package/src/module/user/user.module.ts +5 -4
- package/src/module/workflow/repository/action-data.repository.ts +8 -6
- package/src/module/workflow/repository/action.repository.ts +11 -11
- package/src/module/workflow/repository/form-master.repository.ts +2 -2
- package/src/module/workflow/service/action-data.service.ts +2 -3
- package/src/module/workflow/service/action.service.ts +2 -2
- package/src/module/workflow/service/comm-template.service.ts +2 -0
- package/src/module/workflow/service/entity-modification.service.ts +1 -0
- package/src/module/workflow/service/form-master.service.ts +2 -2
- package/src/module/workflow/service/populate-workflow.service.ts +1 -1
- package/src/module/workflow/service/task.service.ts +3 -0
- package/src/module/workflow/service/workflow-meta.service.ts +7 -2
- package/src/module/workflow/service/workflow.service.ts +2 -2
- package/src/module/workflow/workflow.module.ts +0 -2
- package/src/module/workflow-automation/service/workflow-automation.service.ts +7 -19
- package/src/module/workflow-automation/workflow-automation.module.ts +3 -4
- package/src/module/workflow-schedule/service/workflow-schedule.service.ts +2 -0
- package/src/resources/dev.properties.yaml +2 -2
- package/src/table.config.ts +3 -3
- package/.claude/settings.local.json +0 -26
- package/.idea/250218_nodejs_core.iml +0 -9
- package/.idea/codeStyles/Project.xml +0 -59
- package/.idea/codeStyles/codeStyleConfig.xml +0 -5
- package/.idea/copilot.data.migration.agent.xml +0 -6
- package/.idea/copilot.data.migration.ask.xml +0 -6
- package/.idea/copilot.data.migration.ask2agent.xml +0 -6
- package/.idea/copilot.data.migration.edit.xml +0 -6
- package/.idea/inspectionProfiles/Project_Default.xml +0 -6
- package/.idea/misc.xml +0 -6
- package/.idea/modules.xml +0 -8
- package/.idea/prettier.xml +0 -6
- package/.idea/vcs.xml +0 -6
- package/dist/constant/status.constant.d.ts +0 -4
- package/dist/constant/status.constant.js +0 -9
- package/dist/constant/status.constant.js.map +0 -1
- package/dist/module/app_master/app-master.module.d.ts +0 -2
- package/dist/module/app_master/app-master.module.js +0 -28
- package/dist/module/app_master/app-master.module.js.map +0 -1
- package/dist/module/app_master/controller/app-master.controller.js.map +0 -1
- package/dist/module/app_master/entity/app-master.entity.d.ts +0 -17
- package/dist/module/app_master/entity/app-master.entity.js.map +0 -1
- package/dist/module/app_master/repository/app-master.repository.js.map +0 -1
- package/dist/module/app_master/service/app-master.service.js.map +0 -1
- package/dist/module/enterprise/controller/enterprise.controller.d.ts +0 -12
- package/dist/module/enterprise/controller/enterprise.controller.js +0 -57
- package/dist/module/enterprise/controller/enterprise.controller.js.map +0 -1
- package/dist/module/enterprise/controller/meta.controller.d.ts +0 -9
- package/dist/module/enterprise/controller/meta.controller.js +0 -43
- package/dist/module/enterprise/controller/meta.controller.js.map +0 -1
- package/dist/module/enterprise/service/brand-profile.service.d.ts +0 -0
- package/dist/module/enterprise/service/brand-profile.service.js +0 -1
- package/dist/module/enterprise/service/brand-profile.service.js.map +0 -1
- package/dist/module/enterprise/service/populate-meta.service.d.ts +0 -9
- package/dist/module/enterprise/service/populate-meta.service.js.map +0 -1
- package/dist/module/enterprise/service/school.service.d.ts +0 -0
- package/dist/module/enterprise/service/school.service.js +0 -1
- package/dist/module/enterprise/service/school.service.js.map +0 -1
- package/dist/module/preference_master/entity/preference.entity.d.ts +0 -9
- package/dist/module/preference_master/entity/preference.entity.js +0 -48
- package/dist/module/preference_master/entity/preference.entity.js.map +0 -1
- package/dist/module/preference_master/preference.service.d.ts +0 -8
- package/dist/module/preference_master/preference.service.js +0 -31
- package/dist/module/preference_master/preference.service.js.map +0 -1
- package/dist/module/preference_master/repo/preference.repository.d.ts +0 -8
- package/dist/module/preference_master/repo/preference.repository.js +0 -48
- package/dist/module/preference_master/repo/preference.repository.js.map +0 -1
- package/server.log +0 -850
- package/src/constant/status.constant.ts +0 -4
- package/src/module/app_master/app-master.module.ts +0 -15
- package/src/module/enterprise/controller/enterprise.controller.ts +0 -40
- package/src/module/enterprise/controller/meta.controller.ts +0 -23
- package/src/module/enterprise/service/brand-profile.service.ts +0 -10
- package/src/module/enterprise/service/school.service.ts +0 -5
- package/src/module/preference_master/entity/preference.entity.ts +0 -25
- package/src/module/preference_master/preference.service.ts +0 -27
- package/src/module/preference_master/repo/preference.repository.ts +0 -36
- /package/dist/module/{app_master → meta}/controller/app-master.controller.d.ts +0 -0
- /package/dist/module/{app_master → meta}/controller/app-master.controller.js +0 -0
- /package/dist/module/{app_master → meta}/service/app-master.service.d.ts +0 -0
- /package/dist/module/{app_master → meta}/service/app-master.service.js +0 -0
- /package/src/module/{app_master → meta}/controller/app-master.controller.ts +0 -0
- /package/src/module/{app_master → meta}/service/app-master.service.ts +0 -0
|
@@ -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!**
|