@open-mercato/core 0.5.1-develop.2691.d8a0934b37 → 0.5.1-develop.2699.f8b50c8046
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/modules/api_keys/data/entities.js +1 -1
- package/dist/modules/api_keys/data/entities.js.map +1 -1
- package/dist/modules/api_keys/services/apiKeyService.js +5 -5
- package/dist/modules/api_keys/services/apiKeyService.js.map +2 -2
- package/dist/modules/attachments/api/library/[id]/route.js +1 -1
- package/dist/modules/attachments/api/library/[id]/route.js.map +2 -2
- package/dist/modules/attachments/api/library/route.js +7 -9
- package/dist/modules/attachments/api/library/route.js.map +2 -2
- package/dist/modules/attachments/api/partitions/route.js +3 -3
- package/dist/modules/attachments/api/partitions/route.js.map +2 -2
- package/dist/modules/attachments/api/route.js +6 -5
- package/dist/modules/attachments/api/route.js.map +2 -2
- package/dist/modules/attachments/api/transfer/route.js +1 -1
- package/dist/modules/attachments/api/transfer/route.js.map +2 -2
- package/dist/modules/attachments/data/entities.js +2 -1
- package/dist/modules/attachments/data/entities.js.map +2 -2
- package/dist/modules/attachments/lib/ocrQueue.js +1 -1
- package/dist/modules/attachments/lib/ocrQueue.js.map +2 -2
- package/dist/modules/audit_logs/api/audit-logs/actions/export/route.js.map +2 -2
- package/dist/modules/audit_logs/api/audit-logs/actions/route.js.map +2 -2
- package/dist/modules/audit_logs/data/entities.js +1 -1
- package/dist/modules/audit_logs/data/entities.js.map +1 -1
- package/dist/modules/audit_logs/services/actionLogService.js +77 -70
- package/dist/modules/audit_logs/services/actionLogService.js.map +2 -2
- package/dist/modules/auth/api/roles/acl/route.js +1 -1
- package/dist/modules/auth/api/roles/acl/route.js.map +2 -2
- package/dist/modules/auth/api/users/acl/route.js +2 -2
- package/dist/modules/auth/api/users/acl/route.js.map +2 -2
- package/dist/modules/auth/api/users/resend-invite/route.js +1 -1
- package/dist/modules/auth/api/users/resend-invite/route.js.map +2 -2
- package/dist/modules/auth/cli.js +12 -6
- package/dist/modules/auth/cli.js.map +2 -2
- package/dist/modules/auth/commands/users.js +1 -1
- package/dist/modules/auth/commands/users.js.map +2 -2
- package/dist/modules/auth/data/entities.js +1 -1
- package/dist/modules/auth/data/entities.js.map +2 -2
- package/dist/modules/auth/lib/setup-app.js +3 -3
- package/dist/modules/auth/lib/setup-app.js.map +2 -2
- package/dist/modules/auth/services/authService.js +2 -2
- package/dist/modules/auth/services/authService.js.map +2 -2
- package/dist/modules/business_rules/api/rules/route.js +3 -3
- package/dist/modules/business_rules/api/rules/route.js.map +2 -2
- package/dist/modules/business_rules/api/sets/[id]/members/route.js +7 -4
- package/dist/modules/business_rules/api/sets/[id]/members/route.js.map +2 -2
- package/dist/modules/business_rules/api/sets/route.js +3 -3
- package/dist/modules/business_rules/api/sets/route.js.map +2 -2
- package/dist/modules/business_rules/cli.js +1 -1
- package/dist/modules/business_rules/cli.js.map +2 -2
- package/dist/modules/business_rules/data/entities.js +2 -9
- package/dist/modules/business_rules/data/entities.js.map +2 -2
- package/dist/modules/business_rules/lib/rule-engine.js +1 -1
- package/dist/modules/business_rules/lib/rule-engine.js.map +2 -2
- package/dist/modules/catalog/api/option-schemas/route.js +0 -1
- package/dist/modules/catalog/api/option-schemas/route.js.map +2 -2
- package/dist/modules/catalog/data/entities.js +2 -11
- package/dist/modules/catalog/data/entities.js.map +2 -2
- package/dist/modules/configs/data/entities.js +2 -1
- package/dist/modules/configs/data/entities.js.map +2 -2
- package/dist/modules/currencies/commands/fetch-configs.js +3 -3
- package/dist/modules/currencies/commands/fetch-configs.js.map +2 -2
- package/dist/modules/currencies/data/entities.js +1 -1
- package/dist/modules/currencies/data/entities.js.map +2 -2
- package/dist/modules/customer_accounts/api/signup.js +1 -1
- package/dist/modules/customer_accounts/api/signup.js.map +2 -2
- package/dist/modules/customer_accounts/data/entities.js +1 -1
- package/dist/modules/customer_accounts/data/entities.js.map +2 -2
- package/dist/modules/customer_accounts/services/customerInvitationService.js +1 -1
- package/dist/modules/customer_accounts/services/customerInvitationService.js.map +2 -2
- package/dist/modules/customer_accounts/services/customerSessionService.js +1 -1
- package/dist/modules/customer_accounts/services/customerSessionService.js.map +2 -2
- package/dist/modules/customer_accounts/services/customerTokenService.js +12 -7
- package/dist/modules/customer_accounts/services/customerTokenService.js.map +2 -2
- package/dist/modules/customers/api/interactions/conflicts/route.js +19 -17
- package/dist/modules/customers/api/interactions/conflicts/route.js.map +2 -2
- package/dist/modules/customers/api/interactions/counts/route.js +7 -6
- package/dist/modules/customers/api/interactions/counts/route.js.map +2 -2
- package/dist/modules/customers/api/interactions/route.js +28 -42
- package/dist/modules/customers/api/interactions/route.js.map +2 -2
- package/dist/modules/customers/api/utils.js +29 -24
- package/dist/modules/customers/api/utils.js.map +2 -2
- package/dist/modules/customers/cli.js +45 -40
- package/dist/modules/customers/cli.js.map +2 -2
- package/dist/modules/customers/commands/dictionaries.js +1 -1
- package/dist/modules/customers/commands/dictionaries.js.map +2 -2
- package/dist/modules/customers/commands/tags.js +1 -1
- package/dist/modules/customers/commands/tags.js.map +2 -2
- package/dist/modules/customers/data/entities.js +2 -12
- package/dist/modules/customers/data/entities.js.map +2 -2
- package/dist/modules/customers/lib/interactionProjection.js +18 -15
- package/dist/modules/customers/lib/interactionProjection.js.map +2 -2
- package/dist/modules/customers/lib/personCompanyLinkTable.js +6 -8
- package/dist/modules/customers/lib/personCompanyLinkTable.js.map +2 -2
- package/dist/modules/dashboards/api/roles/widgets/route.js +1 -1
- package/dist/modules/dashboards/api/roles/widgets/route.js.map +2 -2
- package/dist/modules/dashboards/api/users/widgets/route.js +1 -1
- package/dist/modules/dashboards/api/users/widgets/route.js.map +2 -2
- package/dist/modules/dashboards/data/entities.js +1 -1
- package/dist/modules/dashboards/data/entities.js.map +1 -1
- package/dist/modules/data_sync/api/mappings/route.js +1 -1
- package/dist/modules/data_sync/api/mappings/route.js.map +2 -2
- package/dist/modules/data_sync/data/entities.js +2 -1
- package/dist/modules/data_sync/data/entities.js.map +2 -2
- package/dist/modules/data_sync/lib/id-mapping.js +1 -1
- package/dist/modules/data_sync/lib/id-mapping.js.map +2 -2
- package/dist/modules/data_sync/lib/sync-run-service.js +1 -1
- package/dist/modules/data_sync/lib/sync-run-service.js.map +2 -2
- package/dist/modules/dictionaries/commands/factory.js +1 -1
- package/dist/modules/dictionaries/commands/factory.js.map +2 -2
- package/dist/modules/dictionaries/data/entities.js +2 -9
- package/dist/modules/dictionaries/data/entities.js.map +2 -2
- package/dist/modules/directory/commands/organizations.js +4 -4
- package/dist/modules/directory/commands/organizations.js.map +2 -2
- package/dist/modules/directory/data/entities.js +2 -1
- package/dist/modules/directory/data/entities.js.map +2 -2
- package/dist/modules/entities/api/definitions.js +2 -2
- package/dist/modules/entities/api/definitions.js.map +2 -2
- package/dist/modules/entities/api/encryption.js +2 -2
- package/dist/modules/entities/api/encryption.js.map +2 -2
- package/dist/modules/entities/api/relations/options.js +2 -2
- package/dist/modules/entities/api/relations/options.js.map +2 -2
- package/dist/modules/entities/cli.js +4 -4
- package/dist/modules/entities/cli.js.map +2 -2
- package/dist/modules/entities/data/entities.js +1 -1
- package/dist/modules/entities/data/entities.js.map +2 -2
- package/dist/modules/entities/lib/field-definitions.js +2 -2
- package/dist/modules/entities/lib/field-definitions.js.map +2 -2
- package/dist/modules/entities/lib/register.js +1 -1
- package/dist/modules/entities/lib/register.js.map +2 -2
- package/dist/modules/feature_toggles/data/entities.js +2 -9
- package/dist/modules/feature_toggles/data/entities.js.map +2 -2
- package/dist/modules/inbox_ops/api/proposals/counts/route.js +3 -6
- package/dist/modules/inbox_ops/api/proposals/counts/route.js.map +2 -2
- package/dist/modules/inbox_ops/data/entities.js +2 -8
- package/dist/modules/inbox_ops/data/entities.js.map +2 -2
- package/dist/modules/inbox_ops/lib/messagesIntegration.js +6 -6
- package/dist/modules/inbox_ops/lib/messagesIntegration.js.map +2 -2
- package/dist/modules/integrations/data/entities.js +2 -1
- package/dist/modules/integrations/data/entities.js.map +2 -2
- package/dist/modules/integrations/lib/credentials-service.js +1 -1
- package/dist/modules/integrations/lib/credentials-service.js.map +2 -2
- package/dist/modules/integrations/lib/log-service.js +1 -1
- package/dist/modules/integrations/lib/log-service.js.map +2 -2
- package/dist/modules/integrations/lib/state-service.js +1 -1
- package/dist/modules/integrations/lib/state-service.js.map +2 -2
- package/dist/modules/messages/api/route.js +90 -93
- package/dist/modules/messages/api/route.js.map +2 -2
- package/dist/modules/messages/api/unread-count/route.js +8 -7
- package/dist/modules/messages/api/unread-count/route.js.map +2 -2
- package/dist/modules/messages/commands/confirmations.js +1 -1
- package/dist/modules/messages/commands/confirmations.js.map +2 -2
- package/dist/modules/messages/commands/messages.js +3 -3
- package/dist/modules/messages/commands/messages.js.map +2 -2
- package/dist/modules/messages/data/entities.js +2 -1
- package/dist/modules/messages/data/entities.js.map +2 -2
- package/dist/modules/messages/lib/email-sender.js +1 -1
- package/dist/modules/messages/lib/email-sender.js.map +2 -2
- package/dist/modules/messages/lib/searchLookup.js +8 -8
- package/dist/modules/messages/lib/searchLookup.js.map +2 -2
- package/dist/modules/messages/lib/tokenConsumption.js +9 -4
- package/dist/modules/messages/lib/tokenConsumption.js.map +2 -2
- package/dist/modules/notifications/data/entities.js +2 -1
- package/dist/modules/notifications/data/entities.js.map +2 -2
- package/dist/modules/notifications/lib/notificationRecipients.js +15 -5
- package/dist/modules/notifications/lib/notificationRecipients.js.map +2 -2
- package/dist/modules/notifications/lib/notificationService.js +39 -34
- package/dist/modules/notifications/lib/notificationService.js.map +2 -2
- package/dist/modules/notifications/workers/create-notification.worker.js +14 -13
- package/dist/modules/notifications/workers/create-notification.worker.js.map +2 -2
- package/dist/modules/payment_gateways/api/transactions/route.js +2 -2
- package/dist/modules/payment_gateways/api/transactions/route.js.map +2 -2
- package/dist/modules/payment_gateways/data/entities.js +2 -1
- package/dist/modules/payment_gateways/data/entities.js.map +2 -2
- package/dist/modules/payment_gateways/lib/gateway-service.js +1 -1
- package/dist/modules/payment_gateways/lib/gateway-service.js.map +2 -2
- package/dist/modules/payment_gateways/lib/webhook-utils.js +2 -2
- package/dist/modules/payment_gateways/lib/webhook-utils.js.map +2 -2
- package/dist/modules/perspectives/data/entities.js +1 -1
- package/dist/modules/perspectives/data/entities.js.map +2 -2
- package/dist/modules/planner/data/entities.js +1 -1
- package/dist/modules/planner/data/entities.js.map +2 -2
- package/dist/modules/progress/data/entities.js +2 -1
- package/dist/modules/progress/data/entities.js.map +2 -2
- package/dist/modules/progress/lib/progressServiceImpl.js +1 -1
- package/dist/modules/progress/lib/progressServiceImpl.js.map +2 -2
- package/dist/modules/query_index/api/status.js +66 -57
- package/dist/modules/query_index/api/status.js.map +2 -2
- package/dist/modules/query_index/cli.js +39 -24
- package/dist/modules/query_index/cli.js.map +2 -2
- package/dist/modules/query_index/data/entities.js +1 -1
- package/dist/modules/query_index/data/entities.js.map +2 -2
- package/dist/modules/query_index/di.js +25 -13
- package/dist/modules/query_index/di.js.map +2 -2
- package/dist/modules/query_index/lib/batch.js +31 -33
- package/dist/modules/query_index/lib/batch.js.map +2 -2
- package/dist/modules/query_index/lib/coverage.js +63 -50
- package/dist/modules/query_index/lib/coverage.js.map +2 -2
- package/dist/modules/query_index/lib/engine.js +592 -588
- package/dist/modules/query_index/lib/engine.js.map +2 -2
- package/dist/modules/query_index/lib/indexer.js +74 -47
- package/dist/modules/query_index/lib/indexer.js.map +2 -2
- package/dist/modules/query_index/lib/jobs.js +37 -24
- package/dist/modules/query_index/lib/jobs.js.map +2 -2
- package/dist/modules/query_index/lib/purge.js +19 -11
- package/dist/modules/query_index/lib/purge.js.map +2 -2
- package/dist/modules/query_index/lib/reindexer.js +47 -44
- package/dist/modules/query_index/lib/reindexer.js.map +2 -2
- package/dist/modules/query_index/lib/search-tokens.js +47 -25
- package/dist/modules/query_index/lib/search-tokens.js.map +2 -2
- package/dist/modules/query_index/lib/stale.js +14 -12
- package/dist/modules/query_index/lib/stale.js.map +2 -2
- package/dist/modules/query_index/lib/subscriber-scope.js +2 -2
- package/dist/modules/query_index/lib/subscriber-scope.js.map +2 -2
- package/dist/modules/query_index/subscribers/delete_one.js +3 -2
- package/dist/modules/query_index/subscribers/delete_one.js.map +2 -2
- package/dist/modules/resources/commands/tag-assignments.js +1 -1
- package/dist/modules/resources/commands/tag-assignments.js.map +2 -2
- package/dist/modules/resources/commands/tags.js +1 -1
- package/dist/modules/resources/commands/tags.js.map +2 -2
- package/dist/modules/resources/data/entities.js +2 -1
- package/dist/modules/resources/data/entities.js.map +2 -2
- package/dist/modules/sales/commands/documentAddresses.js +2 -2
- package/dist/modules/sales/commands/documentAddresses.js.map +2 -2
- package/dist/modules/sales/commands/notes.js.map +2 -2
- package/dist/modules/sales/commands/tags.js +1 -1
- package/dist/modules/sales/commands/tags.js.map +2 -2
- package/dist/modules/sales/data/enrichers.js +9 -8
- package/dist/modules/sales/data/enrichers.js.map +2 -2
- package/dist/modules/sales/data/entities.js +2 -11
- package/dist/modules/sales/data/entities.js.map +2 -2
- package/dist/modules/shipping_carriers/data/entities.js +2 -1
- package/dist/modules/shipping_carriers/data/entities.js.map +2 -2
- package/dist/modules/shipping_carriers/lib/shipping-service.js +1 -1
- package/dist/modules/shipping_carriers/lib/shipping-service.js.map +2 -2
- package/dist/modules/shipping_carriers/lib/webhook-utils.js +2 -2
- package/dist/modules/shipping_carriers/lib/webhook-utils.js.map +2 -2
- package/dist/modules/staff/data/entities.js +1 -1
- package/dist/modules/staff/data/entities.js.map +2 -2
- package/dist/modules/translations/api/[entityType]/[entityId]/route.js +3 -5
- package/dist/modules/translations/api/[entityType]/[entityId]/route.js.map +2 -2
- package/dist/modules/translations/api/context.js +2 -2
- package/dist/modules/translations/api/context.js.map +2 -2
- package/dist/modules/translations/commands/translations.js +46 -39
- package/dist/modules/translations/commands/translations.js.map +2 -2
- package/dist/modules/translations/components/TranslationManager.js +19 -10
- package/dist/modules/translations/components/TranslationManager.js.map +2 -2
- package/dist/modules/translations/data/entities.js +1 -1
- package/dist/modules/translations/data/entities.js.map +2 -2
- package/dist/modules/translations/lib/apply.js +4 -4
- package/dist/modules/translations/lib/apply.js.map +2 -2
- package/dist/modules/translations/lib/batch.js +3 -2
- package/dist/modules/translations/lib/batch.js.map +2 -2
- package/dist/modules/translations/subscribers/cleanup.js +3 -5
- package/dist/modules/translations/subscribers/cleanup.js.map +2 -2
- package/dist/modules/workflows/api/definitions/route.js +1 -1
- package/dist/modules/workflows/api/definitions/route.js.map +2 -2
- package/dist/modules/workflows/cli.js +5 -5
- package/dist/modules/workflows/cli.js.map +2 -2
- package/dist/modules/workflows/data/entities.js +2 -1
- package/dist/modules/workflows/data/entities.js.map +2 -2
- package/dist/modules/workflows/lib/event-logger.js +2 -2
- package/dist/modules/workflows/lib/event-logger.js.map +2 -2
- package/dist/modules/workflows/lib/seeds.js +16 -1
- package/dist/modules/workflows/lib/seeds.js.map +2 -2
- package/dist/modules/workflows/lib/step-handler.js +3 -3
- package/dist/modules/workflows/lib/step-handler.js.map +2 -2
- package/dist/modules/workflows/lib/task-handler.js +1 -1
- package/dist/modules/workflows/lib/task-handler.js.map +2 -2
- package/dist/modules/workflows/lib/transition-handler.js +1 -1
- package/dist/modules/workflows/lib/transition-handler.js.map +2 -2
- package/dist/modules/workflows/lib/workflow-executor.js +2 -2
- package/dist/modules/workflows/lib/workflow-executor.js.map +2 -2
- package/jest.config.cjs +4 -2
- package/package.json +3 -3
- package/src/modules/api_keys/data/entities.ts +1 -1
- package/src/modules/api_keys/services/apiKeyService.ts +5 -5
- package/src/modules/attachments/api/library/[id]/route.ts +1 -1
- package/src/modules/attachments/api/library/route.ts +10 -12
- package/src/modules/attachments/api/partitions/route.ts +3 -3
- package/src/modules/attachments/api/route.ts +10 -8
- package/src/modules/attachments/api/transfer/route.ts +1 -1
- package/src/modules/attachments/data/entities.ts +2 -1
- package/src/modules/attachments/lib/ocrQueue.ts +1 -1
- package/src/modules/audit_logs/api/audit-logs/actions/export/route.ts +4 -4
- package/src/modules/audit_logs/api/audit-logs/actions/route.ts +4 -4
- package/src/modules/audit_logs/data/entities.ts +1 -1
- package/src/modules/audit_logs/services/actionLogService.ts +96 -87
- package/src/modules/auth/api/roles/acl/route.ts +1 -1
- package/src/modules/auth/api/users/acl/route.ts +2 -2
- package/src/modules/auth/api/users/resend-invite/route.ts +1 -1
- package/src/modules/auth/cli.ts +46 -40
- package/src/modules/auth/commands/users.ts +1 -1
- package/src/modules/auth/data/entities.ts +1 -1
- package/src/modules/auth/lib/setup-app.ts +3 -3
- package/src/modules/auth/services/authService.ts +2 -2
- package/src/modules/business_rules/api/rules/route.ts +3 -3
- package/src/modules/business_rules/api/sets/[id]/members/route.ts +7 -4
- package/src/modules/business_rules/api/sets/route.ts +3 -3
- package/src/modules/business_rules/cli.ts +1 -1
- package/src/modules/business_rules/data/entities.ts +2 -9
- package/src/modules/business_rules/lib/rule-engine.ts +1 -1
- package/src/modules/catalog/api/option-schemas/route.ts +0 -1
- package/src/modules/catalog/data/entities.ts +2 -11
- package/src/modules/configs/data/entities.ts +2 -1
- package/src/modules/currencies/commands/fetch-configs.ts +3 -3
- package/src/modules/currencies/data/entities.ts +1 -1
- package/src/modules/customer_accounts/api/signup.ts +1 -1
- package/src/modules/customer_accounts/data/entities.ts +1 -1
- package/src/modules/customer_accounts/services/customerInvitationService.ts +1 -1
- package/src/modules/customer_accounts/services/customerSessionService.ts +1 -1
- package/src/modules/customer_accounts/services/customerTokenService.ts +26 -15
- package/src/modules/customers/api/interactions/conflicts/route.ts +26 -23
- package/src/modules/customers/api/interactions/counts/route.ts +13 -11
- package/src/modules/customers/api/interactions/route.ts +32 -44
- package/src/modules/customers/api/utils.ts +45 -37
- package/src/modules/customers/cli.ts +88 -67
- package/src/modules/customers/commands/dictionaries.ts +1 -1
- package/src/modules/customers/commands/tags.ts +1 -1
- package/src/modules/customers/data/entities.ts +2 -12
- package/src/modules/customers/lib/interactionProjection.ts +36 -25
- package/src/modules/customers/lib/personCompanyLinkTable.ts +13 -18
- package/src/modules/dashboards/api/roles/widgets/route.ts +1 -1
- package/src/modules/dashboards/api/users/widgets/route.ts +1 -1
- package/src/modules/dashboards/data/entities.ts +1 -1
- package/src/modules/data_sync/api/mappings/route.ts +1 -1
- package/src/modules/data_sync/data/entities.ts +2 -1
- package/src/modules/data_sync/lib/id-mapping.ts +1 -1
- package/src/modules/data_sync/lib/sync-run-service.ts +1 -1
- package/src/modules/dictionaries/commands/factory.ts +1 -1
- package/src/modules/dictionaries/data/entities.ts +2 -9
- package/src/modules/directory/commands/organizations.ts +4 -4
- package/src/modules/directory/data/entities.ts +2 -1
- package/src/modules/entities/api/definitions.ts +2 -2
- package/src/modules/entities/api/encryption.ts +2 -2
- package/src/modules/entities/api/relations/options.ts +8 -3
- package/src/modules/entities/cli.ts +4 -4
- package/src/modules/entities/data/entities.ts +1 -1
- package/src/modules/entities/lib/field-definitions.ts +2 -2
- package/src/modules/entities/lib/register.ts +1 -1
- package/src/modules/feature_toggles/data/entities.ts +2 -9
- package/src/modules/inbox_ops/api/proposals/counts/route.ts +10 -10
- package/src/modules/inbox_ops/data/entities.ts +2 -8
- package/src/modules/inbox_ops/lib/messagesIntegration.ts +12 -11
- package/src/modules/integrations/data/entities.ts +2 -1
- package/src/modules/integrations/lib/credentials-service.ts +1 -1
- package/src/modules/integrations/lib/log-service.ts +1 -1
- package/src/modules/integrations/lib/state-service.ts +1 -1
- package/src/modules/messages/api/route.ts +134 -123
- package/src/modules/messages/api/unread-count/route.ts +19 -16
- package/src/modules/messages/commands/confirmations.ts +1 -1
- package/src/modules/messages/commands/messages.ts +3 -3
- package/src/modules/messages/data/entities.ts +2 -1
- package/src/modules/messages/lib/email-sender.ts +1 -1
- package/src/modules/messages/lib/searchLookup.ts +16 -13
- package/src/modules/messages/lib/tokenConsumption.ts +16 -8
- package/src/modules/notifications/data/entities.ts +2 -1
- package/src/modules/notifications/lib/notificationRecipients.ts +42 -26
- package/src/modules/notifications/lib/notificationService.ts +53 -42
- package/src/modules/notifications/workers/create-notification.worker.ts +20 -17
- package/src/modules/payment_gateways/api/transactions/route.ts +2 -2
- package/src/modules/payment_gateways/data/entities.ts +2 -1
- package/src/modules/payment_gateways/lib/gateway-service.ts +1 -1
- package/src/modules/payment_gateways/lib/webhook-utils.ts +2 -2
- package/src/modules/perspectives/data/entities.ts +1 -1
- package/src/modules/planner/data/entities.ts +1 -1
- package/src/modules/progress/data/entities.ts +2 -1
- package/src/modules/progress/lib/progressServiceImpl.ts +1 -1
- package/src/modules/query_index/api/status.ts +85 -71
- package/src/modules/query_index/cli.ts +51 -31
- package/src/modules/query_index/data/entities.ts +1 -1
- package/src/modules/query_index/di.ts +41 -16
- package/src/modules/query_index/lib/batch.ts +68 -55
- package/src/modules/query_index/lib/coverage.ts +115 -88
- package/src/modules/query_index/lib/engine.ts +1036 -1096
- package/src/modules/query_index/lib/indexer.ts +115 -79
- package/src/modules/query_index/lib/jobs.ts +51 -31
- package/src/modules/query_index/lib/purge.ts +25 -19
- package/src/modules/query_index/lib/reindexer.ts +97 -84
- package/src/modules/query_index/lib/search-tokens.ts +67 -36
- package/src/modules/query_index/lib/stale.ts +14 -17
- package/src/modules/query_index/lib/subscriber-scope.ts +6 -5
- package/src/modules/query_index/subscribers/delete_one.ts +9 -6
- package/src/modules/resources/commands/tag-assignments.ts +1 -1
- package/src/modules/resources/commands/tags.ts +1 -1
- package/src/modules/resources/data/entities.ts +2 -1
- package/src/modules/sales/commands/documentAddresses.ts +2 -2
- package/src/modules/sales/commands/notes.ts +1 -1
- package/src/modules/sales/commands/tags.ts +1 -1
- package/src/modules/sales/data/enrichers.ts +17 -13
- package/src/modules/sales/data/entities.ts +2 -11
- package/src/modules/shipping_carriers/data/entities.ts +2 -1
- package/src/modules/shipping_carriers/lib/shipping-service.ts +1 -1
- package/src/modules/shipping_carriers/lib/webhook-utils.ts +2 -2
- package/src/modules/staff/data/entities.ts +1 -1
- package/src/modules/translations/api/[entityType]/[entityId]/route.ts +14 -11
- package/src/modules/translations/api/context.ts +4 -4
- package/src/modules/translations/commands/translations.ts +116 -81
- package/src/modules/translations/components/TranslationManager.tsx +23 -14
- package/src/modules/translations/data/entities.ts +1 -1
- package/src/modules/translations/i18n/de.json +1 -0
- package/src/modules/translations/i18n/en.json +1 -0
- package/src/modules/translations/i18n/es.json +1 -0
- package/src/modules/translations/i18n/pl.json +1 -0
- package/src/modules/translations/lib/apply.ts +6 -6
- package/src/modules/translations/lib/batch.ts +9 -7
- package/src/modules/translations/subscribers/cleanup.ts +10 -11
- package/src/modules/workflows/api/definitions/route.ts +1 -1
- package/src/modules/workflows/cli.ts +5 -5
- package/src/modules/workflows/data/entities.ts +2 -1
- package/src/modules/workflows/lib/event-logger.ts +2 -2
- package/src/modules/workflows/lib/seeds.ts +16 -1
- package/src/modules/workflows/lib/step-handler.ts +3 -3
- package/src/modules/workflows/lib/task-handler.ts +1 -1
- package/src/modules/workflows/lib/transition-handler.ts +1 -1
- package/src/modules/workflows/lib/workflow-executor.ts +2 -2
|
@@ -1,5 +1,6 @@
|
|
|
1
1
|
import { SortDir } from "@open-mercato/shared/lib/query/types";
|
|
2
2
|
import { resolveEntityTableName } from "@open-mercato/shared/lib/query/engine";
|
|
3
|
+
import { sql } from "kysely";
|
|
3
4
|
import { readCoverageSnapshot, refreshCoverageSnapshot } from "./coverage.js";
|
|
4
5
|
import { createProfiler, shouldEnableProfiler } from "@open-mercato/shared/lib/profiler";
|
|
5
6
|
import { decryptIndexDocCustomFields } from "@open-mercato/shared/lib/encryption/indexDoc";
|
|
@@ -82,6 +83,11 @@ class HybridQueryEngine {
|
|
|
82
83
|
return null;
|
|
83
84
|
}
|
|
84
85
|
}
|
|
86
|
+
getDb() {
|
|
87
|
+
const emAny = this.em;
|
|
88
|
+
if (typeof emAny.getKysely === "function") return emAny.getKysely();
|
|
89
|
+
throw new Error("HybridQueryEngine requires an EntityManager exposing getKysely() (MikroORM v7)");
|
|
90
|
+
}
|
|
85
91
|
async query(entity, opts = {}) {
|
|
86
92
|
const ext = opts.extensions;
|
|
87
93
|
let hybridExtCtx = null;
|
|
@@ -148,8 +154,8 @@ class HybridQueryEngine {
|
|
|
148
154
|
throw err;
|
|
149
155
|
}
|
|
150
156
|
}
|
|
151
|
-
const
|
|
152
|
-
profiler.mark("query:
|
|
157
|
+
const db = this.getDb();
|
|
158
|
+
profiler.mark("query:db_ready");
|
|
153
159
|
const baseTable = resolveEntityTableName(this.em, entity);
|
|
154
160
|
profiler.mark("query:base_table_resolved");
|
|
155
161
|
const searchConfig = resolveSearchConfig();
|
|
@@ -261,10 +267,11 @@ class HybridQueryEngine {
|
|
|
261
267
|
}
|
|
262
268
|
}
|
|
263
269
|
const qualify = (col) => `b.${col}`;
|
|
264
|
-
|
|
265
|
-
const
|
|
266
|
-
const
|
|
267
|
-
|
|
270
|
+
const columns = await this.getBaseColumnsForEntity(entity);
|
|
271
|
+
const hasOrganizationColumn = await this.columnExists(baseTable, "organization_id");
|
|
272
|
+
const hasTenantColumn = await this.columnExists(baseTable, "tenant_id");
|
|
273
|
+
const hasDeletedColumn = await this.columnExists(baseTable, "deleted_at");
|
|
274
|
+
if (!opts.tenantId) throw new Error("QueryEngine: tenantId is required");
|
|
268
275
|
const resolvedJoinsConfig = resolveJoins(
|
|
269
276
|
baseTable,
|
|
270
277
|
[...opts.joins ?? [], ...buildFilterableCustomFieldJoins(opts.customFieldSources)],
|
|
@@ -280,76 +287,22 @@ class HybridQueryEngine {
|
|
|
280
287
|
aliasTables.set(join.alias, join.table);
|
|
281
288
|
}
|
|
282
289
|
const { baseFilters, joinFilters } = partitionFilters(baseTable, normalizedFilters, joinMap);
|
|
283
|
-
if (!opts.tenantId) throw new Error("QueryEngine: tenantId is required");
|
|
284
|
-
const hasOrganizationColumn = await this.columnExists(baseTable, "organization_id");
|
|
285
|
-
const hasTenantColumn = await this.columnExists(baseTable, "tenant_id");
|
|
286
|
-
const hasDeletedColumn = await this.columnExists(baseTable, "deleted_at");
|
|
287
290
|
const searchRuntimeBase = {
|
|
288
291
|
enabled: false,
|
|
289
292
|
config: searchConfig,
|
|
290
293
|
organizationScope: orgScope,
|
|
291
294
|
tenantId: opts.tenantId ?? null
|
|
292
295
|
};
|
|
293
|
-
if (orgScope && hasOrganizationColumn) {
|
|
294
|
-
builder = this.applyOrganizationScope(builder, qualify("organization_id"), orgScope);
|
|
295
|
-
if (optimizedCountBuilder) optimizedCountBuilder = this.applyOrganizationScope(optimizedCountBuilder, qualify("organization_id"), orgScope);
|
|
296
|
-
}
|
|
297
|
-
if (hasTenantColumn) {
|
|
298
|
-
builder = builder.where(qualify("tenant_id"), opts.tenantId);
|
|
299
|
-
if (optimizedCountBuilder) optimizedCountBuilder = optimizedCountBuilder.where(qualify("tenant_id"), opts.tenantId);
|
|
300
|
-
}
|
|
301
|
-
if (!opts.withDeleted && hasDeletedColumn) {
|
|
302
|
-
builder = builder.whereNull(qualify("deleted_at"));
|
|
303
|
-
if (optimizedCountBuilder) optimizedCountBuilder = optimizedCountBuilder.whereNull(qualify("deleted_at"));
|
|
304
|
-
}
|
|
305
|
-
const baseJoinParts = [];
|
|
306
|
-
baseJoinParts.push(`ei.entity_type = ${knex.raw("?", [entity]).toString()}`);
|
|
307
|
-
baseJoinParts.push(`ei.entity_id = (${qualify("id")}::text)`);
|
|
308
|
-
if (hasOrganizationColumn) {
|
|
309
|
-
baseJoinParts.push(`ei.organization_id = ${qualify("organization_id")}`);
|
|
310
|
-
baseJoinParts.push("ei.organization_id is not null");
|
|
311
|
-
}
|
|
312
|
-
if (hasTenantColumn) {
|
|
313
|
-
baseJoinParts.push(`ei.tenant_id = ${qualify("tenant_id")}`);
|
|
314
|
-
baseJoinParts.push("ei.tenant_id is not null");
|
|
315
|
-
}
|
|
316
|
-
if (!opts.withDeleted) baseJoinParts.push(`ei.deleted_at is null`);
|
|
317
|
-
builder = builder.leftJoin({ ei: "entity_indexes" }, knex.raw(baseJoinParts.join(" AND ")));
|
|
318
|
-
const columns = await this.getBaseColumnsForEntity(entity);
|
|
319
296
|
const indexSources = [{ alias: "ei", entityId: entity, recordIdColumn: "b.id" }];
|
|
297
|
+
let preparedCfSources = [];
|
|
320
298
|
const shouldAttachCustomSources = Array.isArray(opts.customFieldSources) && opts.customFieldSources.length > 0 && (wantsCf || searchEnabled);
|
|
321
299
|
if (shouldAttachCustomSources) {
|
|
322
|
-
|
|
323
|
-
|
|
324
|
-
for (const source of prepared.sources) {
|
|
325
|
-
const fragments = [];
|
|
326
|
-
fragments.push(`${source.indexAlias}.entity_type = ${knex.raw("?", [source.entityId]).toString()}`);
|
|
327
|
-
fragments.push(`${source.indexAlias}.entity_id = (${knex.raw("??::text", [`${source.alias}.${source.recordIdColumn}`]).toString()})`);
|
|
328
|
-
const orgExpr = source.organizationField ? knex.raw("??", [`${source.alias}.${source.organizationField}`]).toString() : columns.has("organization_id") ? qualify("organization_id") : null;
|
|
329
|
-
if (orgExpr) {
|
|
330
|
-
fragments.push(`${source.indexAlias}.organization_id = ${orgExpr}`);
|
|
331
|
-
fragments.push(`${source.indexAlias}.organization_id is not null`);
|
|
332
|
-
}
|
|
333
|
-
const tenantExpr = source.tenantField ? knex.raw("??", [`${source.alias}.${source.tenantField}`]).toString() : columns.has("tenant_id") ? qualify("tenant_id") : null;
|
|
334
|
-
if (tenantExpr) {
|
|
335
|
-
fragments.push(`${source.indexAlias}.tenant_id = ${tenantExpr}`);
|
|
336
|
-
fragments.push(`${source.indexAlias}.tenant_id is not null`);
|
|
337
|
-
}
|
|
338
|
-
if (!opts.withDeleted) fragments.push(`${source.indexAlias}.deleted_at is null`);
|
|
339
|
-
builder = builder.leftJoin({ [source.indexAlias]: "entity_indexes" }, knex.raw(fragments.join(" AND ")));
|
|
300
|
+
preparedCfSources = this.prepareCustomFieldSources(opts.customFieldSources ?? []);
|
|
301
|
+
for (const source of preparedCfSources) {
|
|
340
302
|
indexSources.push({ alias: source.indexAlias, entityId: source.entityId, recordIdColumn: `${source.alias}.${source.recordIdColumn}` });
|
|
341
303
|
}
|
|
342
304
|
}
|
|
343
|
-
|
|
344
|
-
this.debug("query:index-sources", {
|
|
345
|
-
entity,
|
|
346
|
-
sources: indexSources.map((src) => ({ alias: src.alias, entity: src.entityId }))
|
|
347
|
-
});
|
|
348
|
-
}
|
|
349
|
-
const searchSources = indexSources.map((src) => ({
|
|
350
|
-
entity: String(src.entityId),
|
|
351
|
-
recordIdColumn: src.recordIdColumn
|
|
352
|
-
})).filter((src) => src.recordIdColumn && src.entity);
|
|
305
|
+
const searchSources = indexSources.map((src) => ({ entity: String(src.entityId), recordIdColumn: src.recordIdColumn })).filter((src) => src.recordIdColumn && src.entity);
|
|
353
306
|
const hasSearchTokens = searchEnabled && searchSources.length ? await this.searchSourcesHaveTokens(searchSources, opts.tenantId ?? null, orgScope) : false;
|
|
354
307
|
const searchRuntime = { ...searchRuntimeBase, searchSources, enabled: searchEnabled && hasSearchTokens };
|
|
355
308
|
const joinSearchAvailability = /* @__PURE__ */ new Map();
|
|
@@ -372,24 +325,18 @@ class HybridQueryEngine {
|
|
|
372
325
|
blocklistedFields: searchConfig.blocklistedFields
|
|
373
326
|
}
|
|
374
327
|
});
|
|
375
|
-
if (!searchEnabled) {
|
|
376
|
-
|
|
377
|
-
|
|
378
|
-
|
|
379
|
-
|
|
380
|
-
|
|
381
|
-
|
|
382
|
-
|
|
383
|
-
searchSources
|
|
384
|
-
});
|
|
385
|
-
}
|
|
328
|
+
if (!searchEnabled) this.logSearchDebug("search:disabled", { entity, baseTable });
|
|
329
|
+
else if (!hasSearchTokens) this.logSearchDebug("search:no-search-tokens", {
|
|
330
|
+
entity,
|
|
331
|
+
baseTable,
|
|
332
|
+
tenantId: opts.tenantId ?? null,
|
|
333
|
+
organizationScope: orgScope,
|
|
334
|
+
searchSources
|
|
335
|
+
});
|
|
386
336
|
}
|
|
387
337
|
const hasNonBaseSearchSource = searchSources.some(
|
|
388
338
|
(src) => src.entity !== String(entity) || src.recordIdColumn !== "b.id"
|
|
389
339
|
);
|
|
390
|
-
if (hasNonBaseSearchSource) {
|
|
391
|
-
optimizedCountBuilder = null;
|
|
392
|
-
}
|
|
393
340
|
if (!partialIndexWarning && Array.isArray(opts.customFieldSources) && opts.customFieldSources.length > 0 && this.isForcePartialIndexEnabled()) {
|
|
394
341
|
const seen = /* @__PURE__ */ new Set([entity]);
|
|
395
342
|
for (const source of opts.customFieldSources) {
|
|
@@ -444,14 +391,7 @@ class HybridQueryEngine {
|
|
|
444
391
|
const globalGap = globalBase > 0 && globalIndexed < globalBase || globalIndexed > globalBase;
|
|
445
392
|
if (globalGap) {
|
|
446
393
|
console.warn("[HybridQueryEngine] Partial index coverage detected at global scope; forcing query index usage due to FORCE_QUERY_INDEX_ON_PARTIAL_INDEXES:", { entity, baseCount: globalBase, indexedCount: globalIndexed, scope: "global" });
|
|
447
|
-
if (debugEnabled) {
|
|
448
|
-
this.debug("query:partial-coverage:forced", {
|
|
449
|
-
entity,
|
|
450
|
-
baseCount: globalBase,
|
|
451
|
-
indexedCount: globalIndexed,
|
|
452
|
-
scope: "global"
|
|
453
|
-
});
|
|
454
|
-
}
|
|
394
|
+
if (debugEnabled) this.debug("query:partial-coverage:forced", { entity, baseCount: globalBase, indexedCount: globalIndexed, scope: "global" });
|
|
455
395
|
partialIndexWarning = {
|
|
456
396
|
entity,
|
|
457
397
|
entityLabel: this.resolveEntityLabel(entity),
|
|
@@ -462,12 +402,7 @@ class HybridQueryEngine {
|
|
|
462
402
|
}
|
|
463
403
|
}
|
|
464
404
|
} catch (err) {
|
|
465
|
-
if (debugEnabled) {
|
|
466
|
-
this.debug("query:partial-coverage:global-check-failed", {
|
|
467
|
-
entity,
|
|
468
|
-
error: err instanceof Error ? err.message : err
|
|
469
|
-
});
|
|
470
|
-
}
|
|
405
|
+
if (debugEnabled) this.debug("query:partial-coverage:global-check-failed", { entity, error: err instanceof Error ? err.message : err });
|
|
471
406
|
}
|
|
472
407
|
}
|
|
473
408
|
const resolveBaseColumn = (field) => {
|
|
@@ -475,38 +410,81 @@ class HybridQueryEngine {
|
|
|
475
410
|
if (field === "organization_id" && columns.has("id")) return "id";
|
|
476
411
|
return null;
|
|
477
412
|
};
|
|
478
|
-
|
|
479
|
-
|
|
480
|
-
|
|
481
|
-
|
|
482
|
-
|
|
483
|
-
|
|
484
|
-
|
|
485
|
-
|
|
486
|
-
|
|
487
|
-
|
|
488
|
-
|
|
489
|
-
|
|
490
|
-
|
|
491
|
-
|
|
492
|
-
|
|
493
|
-
|
|
494
|
-
|
|
495
|
-
|
|
496
|
-
|
|
497
|
-
|
|
498
|
-
"ei",
|
|
499
|
-
|
|
500
|
-
|
|
413
|
+
const applyBaseScope = (q) => {
|
|
414
|
+
let next = q;
|
|
415
|
+
if (orgScope && hasOrganizationColumn) {
|
|
416
|
+
next = this.applyOrganizationScope(next, qualify("organization_id"), orgScope);
|
|
417
|
+
}
|
|
418
|
+
if (hasTenantColumn) {
|
|
419
|
+
next = next.where(qualify("tenant_id"), "=", opts.tenantId);
|
|
420
|
+
}
|
|
421
|
+
if (!opts.withDeleted && hasDeletedColumn) {
|
|
422
|
+
next = next.where(qualify("deleted_at"), "is", null);
|
|
423
|
+
}
|
|
424
|
+
return next;
|
|
425
|
+
};
|
|
426
|
+
const applyEntityIndexesJoin = (q) => {
|
|
427
|
+
return q.leftJoin("entity_indexes as ei", (jb) => {
|
|
428
|
+
let jc = jb.on("ei.entity_type", "=", String(entity)).onRef("ei.entity_id", "=", sql`(${sql.ref(qualify("id"))}::text)`);
|
|
429
|
+
if (hasOrganizationColumn) {
|
|
430
|
+
jc = jc.onRef("ei.organization_id", "=", qualify("organization_id")).on("ei.organization_id", "is not", null);
|
|
431
|
+
}
|
|
432
|
+
if (hasTenantColumn) {
|
|
433
|
+
jc = jc.onRef("ei.tenant_id", "=", qualify("tenant_id")).on("ei.tenant_id", "is not", null);
|
|
434
|
+
}
|
|
435
|
+
if (!opts.withDeleted) {
|
|
436
|
+
jc = jc.on("ei.deleted_at", "is", null);
|
|
437
|
+
}
|
|
438
|
+
return jc;
|
|
439
|
+
});
|
|
440
|
+
};
|
|
441
|
+
const applyCustomFieldSourceJoins = (q) => {
|
|
442
|
+
let next = q;
|
|
443
|
+
for (const source of preparedCfSources) {
|
|
444
|
+
const join = (opts.customFieldSources ?? []).find((s) => s && (s.alias ?? void 0) === source.alias)?.join;
|
|
445
|
+
if (!join) continue;
|
|
446
|
+
const joinType = (join.type ?? "left") === "inner" ? "innerJoin" : "leftJoin";
|
|
447
|
+
next = next[joinType](`${source.table} as ${source.alias}`, (jb) => jb.onRef(`${source.alias}.${join.toField}`, "=", qualify(join.fromField)));
|
|
448
|
+
next = next.leftJoin(`entity_indexes as ${source.indexAlias}`, (jb) => {
|
|
449
|
+
let jc = jb.on(`${source.indexAlias}.entity_type`, "=", String(source.entityId)).onRef(`${source.indexAlias}.entity_id`, "=", sql`(${sql.ref(`${source.alias}.${source.recordIdColumn}`)}::text)`);
|
|
450
|
+
const orgRef = source.organizationField ? `${source.alias}.${source.organizationField}` : columns.has("organization_id") ? qualify("organization_id") : null;
|
|
451
|
+
if (orgRef) {
|
|
452
|
+
jc = jc.onRef(`${source.indexAlias}.organization_id`, "=", orgRef).on(`${source.indexAlias}.organization_id`, "is not", null);
|
|
453
|
+
}
|
|
454
|
+
const tenantRef = source.tenantField ? `${source.alias}.${source.tenantField}` : columns.has("tenant_id") ? qualify("tenant_id") : null;
|
|
455
|
+
if (tenantRef) {
|
|
456
|
+
jc = jc.onRef(`${source.indexAlias}.tenant_id`, "=", tenantRef).on(`${source.indexAlias}.tenant_id`, "is not", null);
|
|
457
|
+
}
|
|
458
|
+
if (!opts.withDeleted) jc = jc.on(`${source.indexAlias}.deleted_at`, "is", null);
|
|
459
|
+
return jc;
|
|
460
|
+
});
|
|
461
|
+
}
|
|
462
|
+
return next;
|
|
463
|
+
};
|
|
464
|
+
const applyCfFilters = (q) => {
|
|
465
|
+
let next = q;
|
|
466
|
+
for (const filter of cfFilters) {
|
|
467
|
+
next = this.applyCfFilterAcrossSources(
|
|
468
|
+
next,
|
|
469
|
+
filter.field,
|
|
501
470
|
filter.op,
|
|
502
471
|
filter.value,
|
|
503
|
-
|
|
472
|
+
indexSources,
|
|
504
473
|
searchRuntime
|
|
505
474
|
);
|
|
506
|
-
|
|
507
|
-
|
|
508
|
-
|
|
509
|
-
|
|
475
|
+
}
|
|
476
|
+
return next;
|
|
477
|
+
};
|
|
478
|
+
const regularBaseFilters = baseFilters.filter((filter) => !filter.orGroup);
|
|
479
|
+
const orGroupFilters = baseFilters.filter((filter) => filter.orGroup);
|
|
480
|
+
const applyRegularBaseFilters = (q) => {
|
|
481
|
+
let next = q;
|
|
482
|
+
for (const filter of regularBaseFilters) {
|
|
483
|
+
const fieldName = String(filter.field);
|
|
484
|
+
const baseField = resolveBaseColumn(fieldName);
|
|
485
|
+
if (!baseField) {
|
|
486
|
+
next = this.applyIndexDocFilterFromAlias(
|
|
487
|
+
next,
|
|
510
488
|
"ei",
|
|
511
489
|
entity,
|
|
512
490
|
fieldName,
|
|
@@ -515,29 +493,20 @@ class HybridQueryEngine {
|
|
|
515
493
|
"b.id",
|
|
516
494
|
searchRuntime
|
|
517
495
|
);
|
|
496
|
+
continue;
|
|
518
497
|
}
|
|
519
|
-
|
|
520
|
-
|
|
521
|
-
const column = qualify(baseField);
|
|
522
|
-
builder = this.applyColumnFilter(builder, column, filter, {
|
|
523
|
-
...searchRuntime,
|
|
524
|
-
knex,
|
|
525
|
-
entity,
|
|
526
|
-
field: fieldName,
|
|
527
|
-
recordIdColumn: "b.id"
|
|
528
|
-
});
|
|
529
|
-
if (optimizedCountBuilder) {
|
|
530
|
-
optimizedCountBuilder = this.applyColumnFilter(optimizedCountBuilder, column, filter, {
|
|
498
|
+
const column = qualify(baseField);
|
|
499
|
+
next = this.applyColumnFilter(next, column, filter, {
|
|
531
500
|
...searchRuntime,
|
|
532
|
-
knex,
|
|
533
501
|
entity,
|
|
534
502
|
field: fieldName,
|
|
535
503
|
recordIdColumn: "b.id"
|
|
536
504
|
});
|
|
537
505
|
}
|
|
538
|
-
|
|
539
|
-
|
|
540
|
-
|
|
506
|
+
return next;
|
|
507
|
+
};
|
|
508
|
+
const applyOrGroupedBaseFilters = (q) => {
|
|
509
|
+
if (orGroupFilters.length === 0) return q;
|
|
541
510
|
const groups = /* @__PURE__ */ new Map();
|
|
542
511
|
for (const filter of orGroupFilters) {
|
|
543
512
|
if (!filter.orGroup) continue;
|
|
@@ -545,99 +514,61 @@ class HybridQueryEngine {
|
|
|
545
514
|
existing.push(filter);
|
|
546
515
|
groups.set(filter.orGroup, existing);
|
|
547
516
|
}
|
|
548
|
-
let next =
|
|
517
|
+
let next = q;
|
|
549
518
|
for (const [, groupFilters] of groups) {
|
|
550
519
|
if (!groupFilters.length) continue;
|
|
551
|
-
next = next.where((
|
|
552
|
-
groupFilters.
|
|
553
|
-
|
|
554
|
-
const baseField = resolveBaseColumn(fieldName);
|
|
555
|
-
const applyCondition = (conditionBuilder) => {
|
|
556
|
-
if (!baseField) {
|
|
557
|
-
this.applyIndexDocFilterFromAlias(
|
|
558
|
-
knex,
|
|
559
|
-
conditionBuilder,
|
|
560
|
-
"ei",
|
|
561
|
-
entity,
|
|
562
|
-
fieldName,
|
|
563
|
-
filter.op,
|
|
564
|
-
filter.value,
|
|
565
|
-
"b.id",
|
|
566
|
-
searchRuntime
|
|
567
|
-
);
|
|
568
|
-
return;
|
|
569
|
-
}
|
|
570
|
-
this.applyColumnFilter(conditionBuilder, qualify(baseField), filter, {
|
|
571
|
-
...searchRuntime,
|
|
572
|
-
knex,
|
|
573
|
-
entity,
|
|
574
|
-
field: fieldName,
|
|
575
|
-
recordIdColumn: "b.id"
|
|
576
|
-
});
|
|
577
|
-
};
|
|
578
|
-
if (index === 0) {
|
|
579
|
-
applyCondition(groupBuilder);
|
|
580
|
-
return;
|
|
581
|
-
}
|
|
582
|
-
groupBuilder.orWhere((conditionBuilder) => {
|
|
583
|
-
applyCondition(conditionBuilder);
|
|
584
|
-
});
|
|
585
|
-
});
|
|
586
|
-
});
|
|
520
|
+
next = next.where((eb) => eb.or(
|
|
521
|
+
groupFilters.map((filter) => this.buildBaseFilterExpression(eb, filter, resolveBaseColumn, qualify, entity, searchRuntime))
|
|
522
|
+
));
|
|
587
523
|
}
|
|
588
524
|
return next;
|
|
589
525
|
};
|
|
590
|
-
builder = applyOrGroupedBaseFilters(builder) ?? builder;
|
|
591
|
-
optimizedCountBuilder = applyOrGroupedBaseFilters(optimizedCountBuilder);
|
|
592
526
|
const applyAliasScopes = async (target, aliasName) => {
|
|
527
|
+
let next = target;
|
|
593
528
|
const tableName = aliasTables.get(aliasName);
|
|
594
|
-
if (!tableName) return;
|
|
529
|
+
if (!tableName) return next;
|
|
595
530
|
if (orgScope && await this.columnExists(tableName, "organization_id")) {
|
|
596
|
-
this.applyOrganizationScope(
|
|
531
|
+
next = this.applyOrganizationScope(next, `${aliasName}.organization_id`, orgScope);
|
|
597
532
|
}
|
|
598
533
|
if (opts.tenantId && await this.columnExists(tableName, "tenant_id")) {
|
|
599
|
-
|
|
534
|
+
next = next.where(`${aliasName}.tenant_id`, "=", opts.tenantId);
|
|
600
535
|
}
|
|
601
536
|
if (!opts.withDeleted && await this.columnExists(tableName, "deleted_at")) {
|
|
602
|
-
|
|
537
|
+
next = next.where(`${aliasName}.deleted_at`, "is", null);
|
|
603
538
|
}
|
|
539
|
+
return next;
|
|
604
540
|
};
|
|
605
|
-
const
|
|
541
|
+
const applyJoinFilterOpFn = (target, column, op, value) => {
|
|
606
542
|
switch (op) {
|
|
607
543
|
case "eq":
|
|
608
|
-
target.where(column, value);
|
|
609
|
-
break;
|
|
544
|
+
return target.where(column, "=", value);
|
|
610
545
|
case "ne":
|
|
611
|
-
target.
|
|
612
|
-
break;
|
|
546
|
+
return target.where(column, "!=", value);
|
|
613
547
|
case "gt":
|
|
548
|
+
return target.where(column, ">", value);
|
|
614
549
|
case "gte":
|
|
550
|
+
return target.where(column, ">=", value);
|
|
615
551
|
case "lt":
|
|
616
|
-
|
|
617
|
-
|
|
618
|
-
target.where(column,
|
|
619
|
-
break;
|
|
620
|
-
}
|
|
552
|
+
return target.where(column, "<", value);
|
|
553
|
+
case "lte":
|
|
554
|
+
return target.where(column, "<=", value);
|
|
621
555
|
case "in":
|
|
622
|
-
target.
|
|
623
|
-
break;
|
|
556
|
+
return target.where(column, "in", this.toArray(value));
|
|
624
557
|
case "nin":
|
|
625
|
-
target.
|
|
626
|
-
break;
|
|
558
|
+
return target.where(column, "not in", this.toArray(value));
|
|
627
559
|
case "like":
|
|
628
|
-
target.where(column, "like", value);
|
|
629
|
-
break;
|
|
560
|
+
return target.where(column, "like", value);
|
|
630
561
|
case "ilike":
|
|
631
|
-
target.where(column, "ilike", value);
|
|
632
|
-
break;
|
|
562
|
+
return target.where(column, "ilike", value);
|
|
633
563
|
case "exists":
|
|
634
|
-
value ? target.
|
|
635
|
-
|
|
564
|
+
return value ? target.where(column, "is not", null) : target.where(column, "is", null);
|
|
565
|
+
default:
|
|
566
|
+
return target;
|
|
636
567
|
}
|
|
637
568
|
};
|
|
638
569
|
const applyJoinSearchFilterOp = async (target, filter, _qualified, join) => {
|
|
639
570
|
if (!searchEnabled || !join.entityId) return false;
|
|
640
|
-
if (!["
|
|
571
|
+
if (!["like", "ilike"].includes(filter.op)) return false;
|
|
641
572
|
if (typeof filter.value !== "string" || filter.value.trim().length === 0) return false;
|
|
642
573
|
let searchAvailable = joinSearchAvailability.get(join.entityId);
|
|
643
574
|
if (searchAvailable === void 0) {
|
|
@@ -648,7 +579,6 @@ class HybridQueryEngine {
|
|
|
648
579
|
const tokens = tokenizeText(String(filter.value), searchConfig);
|
|
649
580
|
if (!tokens.hashes.length) return false;
|
|
650
581
|
return this.applySearchTokens(target, {
|
|
651
|
-
knex,
|
|
652
582
|
entity: String(join.entityId),
|
|
653
583
|
field: filter.column,
|
|
654
584
|
hashes: tokens.hashes,
|
|
@@ -657,43 +587,40 @@ class HybridQueryEngine {
|
|
|
657
587
|
organizationScope: orgScope
|
|
658
588
|
});
|
|
659
589
|
};
|
|
660
|
-
|
|
661
|
-
|
|
662
|
-
|
|
663
|
-
|
|
664
|
-
|
|
665
|
-
|
|
666
|
-
|
|
667
|
-
|
|
668
|
-
|
|
669
|
-
applyFilterOp: (target, column, op, value) => applyJoinFilterOp(target, column, op, value),
|
|
670
|
-
applyJoinFilterOp: (target, filter, qualified, join) => applyJoinSearchFilterOp(target, filter, qualified, join),
|
|
671
|
-
columnExists: (tbl, column) => this.columnExists(tbl, column)
|
|
672
|
-
});
|
|
673
|
-
if (optimizedCountBuilder) {
|
|
674
|
-
await applyJoinFilters({
|
|
675
|
-
knex,
|
|
590
|
+
const applyQueryShape = async (q) => {
|
|
591
|
+
let next = applyBaseScope(q);
|
|
592
|
+
next = applyEntityIndexesJoin(next);
|
|
593
|
+
next = applyCustomFieldSourceJoins(next);
|
|
594
|
+
next = applyCfFilters(next);
|
|
595
|
+
next = applyRegularBaseFilters(next);
|
|
596
|
+
next = applyOrGroupedBaseFilters(next);
|
|
597
|
+
next = await applyJoinFilters({
|
|
598
|
+
db,
|
|
676
599
|
baseTable,
|
|
677
|
-
builder:
|
|
600
|
+
builder: next,
|
|
678
601
|
joinMap,
|
|
679
602
|
joinFilters,
|
|
680
603
|
aliasTables,
|
|
681
604
|
qualifyBase: (column) => qualify(column),
|
|
682
|
-
applyAliasScope: (target, alias) => applyAliasScopes(target, alias),
|
|
683
|
-
applyFilterOp: (target, column, op, value) =>
|
|
684
|
-
applyJoinFilterOp: (target, filter, qualified, join) =>
|
|
605
|
+
applyAliasScope: async (target, alias) => applyAliasScopes(target, alias),
|
|
606
|
+
applyFilterOp: (target, column, op, value) => applyJoinFilterOpFn(target, column, op, value),
|
|
607
|
+
applyJoinFilterOp: async (target, filter, qualified, join) => {
|
|
608
|
+
const applied = await applyJoinSearchFilterOp(target, filter, qualified, join);
|
|
609
|
+
return { applied, builder: target };
|
|
610
|
+
},
|
|
685
611
|
columnExists: (tbl, column) => this.columnExists(tbl, column)
|
|
686
612
|
});
|
|
687
|
-
|
|
613
|
+
return next;
|
|
614
|
+
};
|
|
615
|
+
const hasCustomFieldFilters = cfFilters.length > 0;
|
|
616
|
+
const canOptimizeCount = !hasCustomFieldFilters && !hasNonBaseSearchSource;
|
|
688
617
|
const selectFieldSet = new Set(opts.fields && opts.fields.length ? opts.fields.map(String) : Array.from(columns.keys()));
|
|
689
618
|
if (opts.includeCustomFields === true) {
|
|
690
619
|
const entityIds = Array.from(new Set(indexSources.map((src) => String(src.entityId))));
|
|
691
620
|
try {
|
|
692
621
|
const resolvedKeys = await this.resolveAvailableCustomFieldKeys(entityIds, opts.tenantId ?? null);
|
|
693
622
|
resolvedKeys.forEach((key) => selectFieldSet.add(`cf:${key}`));
|
|
694
|
-
if (this.isDebugVerbosity()) {
|
|
695
|
-
this.debug("query:cf:resolved-keys", { entity, keys: resolvedKeys });
|
|
696
|
-
}
|
|
623
|
+
if (this.isDebugVerbosity()) this.debug("query:cf:resolved-keys", { entity, keys: resolvedKeys });
|
|
697
624
|
} catch (err) {
|
|
698
625
|
console.warn("[HybridQueryEngine] Failed to resolve custom field keys for", entity, err);
|
|
699
626
|
}
|
|
@@ -701,74 +628,107 @@ class HybridQueryEngine {
|
|
|
701
628
|
opts.includeCustomFields.map((key) => String(key)).forEach((key) => selectFieldSet.add(`cf:${key}`));
|
|
702
629
|
}
|
|
703
630
|
const selectFields = Array.from(selectFieldSet);
|
|
704
|
-
|
|
705
|
-
|
|
706
|
-
|
|
707
|
-
const
|
|
708
|
-
|
|
709
|
-
|
|
710
|
-
|
|
711
|
-
|
|
712
|
-
|
|
631
|
+
const applySelection = (q) => {
|
|
632
|
+
let next = q;
|
|
633
|
+
for (const field of selectFields) {
|
|
634
|
+
const fieldName = String(field);
|
|
635
|
+
if (fieldName.startsWith("cf:")) {
|
|
636
|
+
const alias = this.sanitize(fieldName);
|
|
637
|
+
const jsonExpr = this.buildCfJsonExprSql(fieldName, indexSources);
|
|
638
|
+
const exprRaw = jsonExpr ?? sql`NULL::jsonb`;
|
|
639
|
+
next = next.select(exprRaw.as(alias));
|
|
640
|
+
} else if (columns.has(fieldName)) {
|
|
641
|
+
next = next.select(`${qualify(fieldName)} as ${fieldName}`);
|
|
642
|
+
}
|
|
713
643
|
}
|
|
714
|
-
|
|
715
|
-
|
|
716
|
-
|
|
717
|
-
|
|
718
|
-
|
|
719
|
-
|
|
720
|
-
|
|
721
|
-
|
|
644
|
+
return next;
|
|
645
|
+
};
|
|
646
|
+
const applySort = (q) => {
|
|
647
|
+
let next = q;
|
|
648
|
+
for (const s of opts.sort || []) {
|
|
649
|
+
const fieldName = String(s.field);
|
|
650
|
+
if (fieldName.startsWith("cf:")) {
|
|
651
|
+
const textExpr = this.buildCfTextExprSql(fieldName, indexSources);
|
|
652
|
+
if (textExpr) {
|
|
653
|
+
const direction = sql.raw(String(s.dir ?? SortDir.Asc));
|
|
654
|
+
next = next.orderBy(sql`${textExpr} ${direction}`);
|
|
655
|
+
}
|
|
656
|
+
} else {
|
|
657
|
+
const baseField = resolveBaseColumn(fieldName);
|
|
658
|
+
if (!baseField) continue;
|
|
659
|
+
next = next.orderBy(qualify(baseField), s.dir ?? SortDir.Asc);
|
|
722
660
|
}
|
|
723
|
-
} else {
|
|
724
|
-
const baseField = resolveBaseColumn(fieldName);
|
|
725
|
-
if (!baseField) continue;
|
|
726
|
-
builder = builder.orderBy(qualify(baseField), sort.dir ?? SortDir.Asc);
|
|
727
661
|
}
|
|
728
|
-
|
|
662
|
+
return next;
|
|
663
|
+
};
|
|
729
664
|
const page = opts.page?.page ?? 1;
|
|
730
665
|
const pageSize = opts.page?.pageSize ?? 20;
|
|
731
666
|
const sqlDebugEnabled = this.isSqlDebugEnabled();
|
|
732
667
|
let total;
|
|
733
|
-
if (
|
|
734
|
-
const
|
|
735
|
-
|
|
668
|
+
if (canOptimizeCount) {
|
|
669
|
+
const optimizedRoot = db.selectFrom(`${baseTable} as b`);
|
|
670
|
+
let countCore = applyBaseScope(optimizedRoot);
|
|
671
|
+
countCore = applyRegularBaseFilters(countCore);
|
|
672
|
+
countCore = applyOrGroupedBaseFilters(countCore);
|
|
673
|
+
countCore = await applyJoinFilters({
|
|
674
|
+
db,
|
|
675
|
+
baseTable,
|
|
676
|
+
builder: countCore,
|
|
677
|
+
joinMap,
|
|
678
|
+
joinFilters,
|
|
679
|
+
aliasTables,
|
|
680
|
+
qualifyBase: (column) => qualify(column),
|
|
681
|
+
applyAliasScope: async (target, alias) => applyAliasScopes(target, alias),
|
|
682
|
+
applyFilterOp: (target, column, op, value) => applyJoinFilterOpFn(target, column, op, value),
|
|
683
|
+
applyJoinFilterOp: async (target, filter, qualified, join) => {
|
|
684
|
+
const applied = await applyJoinSearchFilterOp(target, filter, qualified, join);
|
|
685
|
+
return { applied, builder: target };
|
|
686
|
+
},
|
|
687
|
+
columnExists: (tbl, column) => this.columnExists(tbl, column)
|
|
688
|
+
});
|
|
689
|
+
const sub = countCore.select(sql.ref(qualify("id")).as("id")).groupBy(qualify("id")).as("sq");
|
|
690
|
+
const countQuery = db.selectFrom(sub).select(sql`count(*)`.as("count"));
|
|
736
691
|
if (debugEnabled && sqlDebugEnabled) {
|
|
737
|
-
const
|
|
738
|
-
this.debug("query:sql:count", { entity, sql, bindings });
|
|
692
|
+
const compiled = countQuery.compile();
|
|
693
|
+
this.debug("query:sql:count", { entity, sql: compiled.sql, bindings: compiled.parameters });
|
|
739
694
|
}
|
|
740
695
|
const countRow = await this.captureSqlTiming(
|
|
741
696
|
"query:sql:count",
|
|
742
697
|
entity,
|
|
743
|
-
() => countQuery.
|
|
698
|
+
() => countQuery.executeTakeFirst(),
|
|
744
699
|
{ optimized: true },
|
|
745
700
|
profiler
|
|
746
701
|
);
|
|
747
702
|
total = this.parseCount(countRow);
|
|
748
703
|
} else {
|
|
749
|
-
const
|
|
704
|
+
const countRoot = db.selectFrom(`${baseTable} as b`);
|
|
705
|
+
const countBuilder = (await applyQueryShape(countRoot)).select(sql`count(distinct ${sql.ref(qualify("id"))})`.as("count"));
|
|
750
706
|
if (debugEnabled && sqlDebugEnabled) {
|
|
751
|
-
const
|
|
752
|
-
this.debug("query:sql:count", { entity, sql, bindings });
|
|
707
|
+
const compiled = countBuilder.compile();
|
|
708
|
+
this.debug("query:sql:count", { entity, sql: compiled.sql, bindings: compiled.parameters });
|
|
753
709
|
}
|
|
754
710
|
const countRow = await this.captureSqlTiming(
|
|
755
711
|
"query:sql:count",
|
|
756
712
|
entity,
|
|
757
|
-
() => countBuilder.
|
|
713
|
+
() => countBuilder.executeTakeFirst(),
|
|
758
714
|
{ optimized: false },
|
|
759
715
|
profiler
|
|
760
716
|
);
|
|
761
717
|
total = this.parseCount(countRow);
|
|
762
718
|
}
|
|
763
|
-
const
|
|
719
|
+
const dataRoot = db.selectFrom(`${baseTable} as b`);
|
|
720
|
+
let dataBuilder = await applyQueryShape(dataRoot);
|
|
721
|
+
dataBuilder = applySelection(dataBuilder);
|
|
722
|
+
dataBuilder = applySort(dataBuilder);
|
|
723
|
+
dataBuilder = dataBuilder.limit(pageSize).offset((page - 1) * pageSize);
|
|
764
724
|
if (debugEnabled && sqlDebugEnabled) {
|
|
765
|
-
const
|
|
766
|
-
this.debug("query:sql:data", { entity, sql, bindings, page, pageSize });
|
|
725
|
+
const compiled = dataBuilder.compile();
|
|
726
|
+
this.debug("query:sql:data", { entity, sql: compiled.sql, bindings: compiled.parameters, page, pageSize });
|
|
767
727
|
}
|
|
768
728
|
const itemsRaw = await this.captureSqlTiming(
|
|
769
729
|
"query:sql:data",
|
|
770
730
|
entity,
|
|
771
|
-
() => dataBuilder,
|
|
731
|
+
() => dataBuilder.execute(),
|
|
772
732
|
{ page, pageSize },
|
|
773
733
|
profiler
|
|
774
734
|
);
|
|
@@ -816,9 +776,7 @@ class HybridQueryEngine {
|
|
|
816
776
|
}
|
|
817
777
|
const typedItems = items;
|
|
818
778
|
let result = { items: typedItems, page, pageSize, total };
|
|
819
|
-
if (partialIndexWarning) {
|
|
820
|
-
result.meta = { partialIndexWarning };
|
|
821
|
-
}
|
|
779
|
+
if (partialIndexWarning) result.meta = { partialIndexWarning };
|
|
822
780
|
result = await applyAfterExtensions(result);
|
|
823
781
|
finishProfile({
|
|
824
782
|
result: "ok",
|
|
@@ -834,30 +792,15 @@ class HybridQueryEngine {
|
|
|
834
792
|
throw err;
|
|
835
793
|
}
|
|
836
794
|
}
|
|
837
|
-
|
|
838
|
-
const connection = this.em.getConnection();
|
|
839
|
-
const withKnex = connection;
|
|
840
|
-
if (typeof withKnex.getKnex === "function") {
|
|
841
|
-
return withKnex.getKnex();
|
|
842
|
-
}
|
|
843
|
-
throw new Error("HybridQueryEngine requires a SQL connection that exposes getKnex()");
|
|
844
|
-
}
|
|
845
|
-
prepareCustomFieldSources(knex, builder, sources, qualify) {
|
|
846
|
-
let current = builder;
|
|
795
|
+
prepareCustomFieldSources(sources) {
|
|
847
796
|
const prepared = [];
|
|
848
797
|
sources.forEach((source, index) => {
|
|
849
798
|
if (!source) return;
|
|
850
799
|
const joinTable = source.table ?? resolveEntityTableName(this.em, source.entityId);
|
|
851
800
|
const alias = source.alias ?? `cfs_${index}`;
|
|
852
|
-
|
|
853
|
-
if (!join) {
|
|
801
|
+
if (!source.join) {
|
|
854
802
|
throw new Error(`QueryEngine: customFieldSources entry for ${String(source.entityId)} requires a join configuration`);
|
|
855
803
|
}
|
|
856
|
-
const joinArgs = { [alias]: joinTable };
|
|
857
|
-
const joinCallback = function() {
|
|
858
|
-
this.on(`${alias}.${join.toField}`, "=", qualify(join.fromField));
|
|
859
|
-
};
|
|
860
|
-
current = (join.type ?? "left") === "inner" ? current.join(joinArgs, joinCallback) : current.leftJoin(joinArgs, joinCallback);
|
|
861
804
|
prepared.push({
|
|
862
805
|
alias,
|
|
863
806
|
indexAlias: `ei_${alias}`,
|
|
@@ -868,17 +811,26 @@ class HybridQueryEngine {
|
|
|
868
811
|
table: joinTable
|
|
869
812
|
});
|
|
870
813
|
});
|
|
871
|
-
return
|
|
814
|
+
return prepared;
|
|
872
815
|
}
|
|
873
816
|
async isCustomEntity(entity) {
|
|
874
817
|
try {
|
|
875
|
-
const
|
|
876
|
-
const row = await
|
|
818
|
+
const db = this.getDb();
|
|
819
|
+
const row = await db.selectFrom("custom_entities").select("id").where("entity_id", "=", entity).where("is_active", "=", true).executeTakeFirst();
|
|
877
820
|
return !!row;
|
|
878
821
|
} catch {
|
|
879
822
|
return false;
|
|
880
823
|
}
|
|
881
824
|
}
|
|
825
|
+
/**
|
|
826
|
+
* Adds a WHERE EXISTS / OR WHERE EXISTS subquery that matches
|
|
827
|
+
* `search_tokens` for the supplied (entity, field) against the
|
|
828
|
+
* provided record id column.
|
|
829
|
+
*
|
|
830
|
+
* Returns true when the sub-query was applied (i.e. tokens were
|
|
831
|
+
* non-empty). Caller is responsible for the calling context
|
|
832
|
+
* (direct where vs. inside `eb.or([...])`).
|
|
833
|
+
*/
|
|
882
834
|
applySearchTokens(q, opts) {
|
|
883
835
|
if (!opts.hashes.length) {
|
|
884
836
|
this.logSearchDebug("search:skip-no-hashes", {
|
|
@@ -890,8 +842,6 @@ class HybridQueryEngine {
|
|
|
890
842
|
return false;
|
|
891
843
|
}
|
|
892
844
|
const alias = `st_${this.searchAliasSeq++}`;
|
|
893
|
-
const combineWith = opts.combineWith === "or" ? "orWhereExists" : "whereExists";
|
|
894
|
-
const engine = this;
|
|
895
845
|
this.logSearchDebug("search:apply-search-tokens", {
|
|
896
846
|
entity: opts.entity,
|
|
897
847
|
field: opts.field,
|
|
@@ -901,63 +851,67 @@ class HybridQueryEngine {
|
|
|
901
851
|
organizationScope: opts.organizationScope,
|
|
902
852
|
combineWith: opts.combineWith ?? "and"
|
|
903
853
|
});
|
|
904
|
-
|
|
905
|
-
|
|
854
|
+
const engine = this;
|
|
855
|
+
const buildSub = (eb) => {
|
|
856
|
+
let sub = eb.selectFrom(`search_tokens as ${alias}`).select(sql`1`.as("one")).where(`${alias}.entity_type`, "=", opts.entity).where(`${alias}.field`, "=", opts.field).where(sql`${sql.ref(`${alias}.entity_id`)} = ${sql.ref(opts.recordIdColumn)}::text`).where(`${alias}.token_hash`, "in", opts.hashes).groupBy([`${alias}.entity_id`, `${alias}.field`]).having(sql`count(distinct ${sql.ref(`${alias}.token_hash`)}) >= ${opts.hashes.length}`);
|
|
906
857
|
if (opts.tenantId !== void 0) {
|
|
907
|
-
|
|
858
|
+
sub = sub.where(sql`${sql.ref(`${alias}.tenant_id`)} is not distinct from ${opts.tenantId ?? null}`);
|
|
908
859
|
}
|
|
909
860
|
if (opts.organizationScope) {
|
|
910
|
-
engine.applyOrganizationScope(
|
|
861
|
+
sub = engine.applyOrganizationScope(sub, `${alias}.organization_id`, opts.organizationScope);
|
|
911
862
|
}
|
|
912
|
-
|
|
863
|
+
return sub;
|
|
864
|
+
};
|
|
865
|
+
if (opts.combineWith === "or") {
|
|
866
|
+
;
|
|
867
|
+
q.__pendingOrExists = buildSub(q);
|
|
868
|
+
return true;
|
|
869
|
+
}
|
|
870
|
+
;
|
|
871
|
+
q.__applied = true;
|
|
872
|
+
const built = buildSub(q);
|
|
873
|
+
if (typeof q.where === "function") {
|
|
874
|
+
;
|
|
875
|
+
q = q.where((eb) => eb.exists(built));
|
|
876
|
+
}
|
|
913
877
|
return true;
|
|
914
878
|
}
|
|
915
|
-
|
|
879
|
+
/** SQL fragment for `cf:<key>` (or legacy bare key) as JSON across a single alias. */
|
|
880
|
+
jsonbSqlAlias(alias, key) {
|
|
916
881
|
if (key.startsWith("cf:")) {
|
|
917
882
|
const bare = key.slice(3);
|
|
918
|
-
return
|
|
883
|
+
return sql`coalesce(${sql.ref(alias + ".doc")} -> ${key}, ${sql.ref(alias + ".doc")} -> ${bare})`;
|
|
919
884
|
}
|
|
920
|
-
return
|
|
885
|
+
return sql`${sql.ref(alias + ".doc")} -> ${key}`;
|
|
921
886
|
}
|
|
922
|
-
|
|
887
|
+
/** SQL fragment for `cf:<key>` (or legacy bare key) as text across a single alias. */
|
|
888
|
+
cfTextExprAlias(alias, key) {
|
|
923
889
|
if (key.startsWith("cf:")) {
|
|
924
890
|
const bare = key.slice(3);
|
|
925
|
-
return
|
|
891
|
+
return sql`coalesce((${sql.ref(alias + ".doc")} ->> ${key}), (${sql.ref(alias + ".doc")} ->> ${bare}))`;
|
|
926
892
|
}
|
|
927
|
-
return
|
|
893
|
+
return sql`(${sql.ref(alias + ".doc")} ->> ${key})`;
|
|
894
|
+
}
|
|
895
|
+
/** Build JSON/text SQL expressions across multiple index alias sources (coalesce over them). */
|
|
896
|
+
buildCfJsonExprSql(key, sources) {
|
|
897
|
+
if (!sources.length) return null;
|
|
898
|
+
const parts = sources.map((src) => this.jsonbSqlAlias(src.alias, key));
|
|
899
|
+
if (parts.length === 1) return parts[0];
|
|
900
|
+
return sql`coalesce(${sql.join(parts, sql`, `)})`;
|
|
928
901
|
}
|
|
929
|
-
|
|
930
|
-
if (!sources.length) return
|
|
931
|
-
const
|
|
932
|
-
|
|
933
|
-
|
|
934
|
-
const textSql = textFragments.length === 1 ? textFragments[0] : `coalesce(${textFragments.join(", ")})`;
|
|
935
|
-
return { jsonSql, textSql };
|
|
902
|
+
buildCfTextExprSql(key, sources) {
|
|
903
|
+
if (!sources.length) return null;
|
|
904
|
+
const parts = sources.map((src) => this.cfTextExprAlias(src.alias, key));
|
|
905
|
+
if (parts.length === 1) return parts[0];
|
|
906
|
+
return sql`coalesce(${sql.join(parts, sql`, `)})`;
|
|
936
907
|
}
|
|
937
|
-
applyCfFilterAcrossSources(
|
|
908
|
+
applyCfFilterAcrossSources(builder, key, op, value, sources, search) {
|
|
938
909
|
if (!sources.length) return builder;
|
|
939
910
|
if ((op === "like" || op === "ilike") && search?.enabled && typeof value === "string") {
|
|
940
911
|
const tokens = tokenizeText(String(value), search.config);
|
|
941
912
|
const hashes = tokens.hashes;
|
|
942
913
|
if (hashes.length) {
|
|
943
|
-
|
|
944
|
-
if (sources.length) {
|
|
945
|
-
builder = builder.where((qb) => {
|
|
946
|
-
sources.forEach((source, idx) => {
|
|
947
|
-
const ok = this.applySearchTokens(qb, {
|
|
948
|
-
knex,
|
|
949
|
-
entity: source.entityId,
|
|
950
|
-
field: key,
|
|
951
|
-
hashes,
|
|
952
|
-
recordIdColumn: `${source.alias}.entity_id`,
|
|
953
|
-
tenantId: search.tenantId ?? null,
|
|
954
|
-
organizationScope: search.organizationScope ?? null,
|
|
955
|
-
combineWith: idx === 0 ? "and" : "or"
|
|
956
|
-
});
|
|
957
|
-
if (ok) applied = true;
|
|
958
|
-
});
|
|
959
|
-
});
|
|
960
|
-
}
|
|
914
|
+
const applied = this.applyMultiSourceSearchExists(builder, sources, key, hashes, search);
|
|
961
915
|
this.logSearchDebug("search:cf-filter-across", {
|
|
962
916
|
entity: sources.map((src) => src.entityId),
|
|
963
917
|
field: key,
|
|
@@ -967,7 +921,7 @@ class HybridQueryEngine {
|
|
|
967
921
|
tenantId: search.tenantId ?? null,
|
|
968
922
|
organizationScope: search.organizationScope
|
|
969
923
|
});
|
|
970
|
-
if (applied) return builder;
|
|
924
|
+
if (applied.builder !== builder) return applied.builder;
|
|
971
925
|
} else {
|
|
972
926
|
this.logSearchDebug("search:cf-skip-empty-hashes", {
|
|
973
927
|
entity: sources.map((src) => src.entityId),
|
|
@@ -977,193 +931,282 @@ class HybridQueryEngine {
|
|
|
977
931
|
}
|
|
978
932
|
return builder;
|
|
979
933
|
}
|
|
980
|
-
const
|
|
981
|
-
|
|
982
|
-
|
|
983
|
-
const arrContains = (val) =>
|
|
934
|
+
const textExpr = this.buildCfTextExprSql(key, sources);
|
|
935
|
+
const jsonExpr = this.buildCfJsonExprSql(key, sources);
|
|
936
|
+
if (!textExpr || !jsonExpr) return builder;
|
|
937
|
+
const arrContains = (val) => sql`${jsonExpr} @> ${JSON.stringify([val])}::jsonb`;
|
|
984
938
|
switch (op) {
|
|
985
939
|
case "eq":
|
|
986
|
-
return builder.where((
|
|
987
|
-
|
|
988
|
-
|
|
989
|
-
|
|
940
|
+
return builder.where((eb) => eb.or([
|
|
941
|
+
sql`${textExpr} = ${value}`,
|
|
942
|
+
arrContains(value)
|
|
943
|
+
]));
|
|
990
944
|
case "ne":
|
|
991
|
-
return builder.
|
|
945
|
+
return builder.where(sql`${textExpr} <> ${value}`);
|
|
992
946
|
case "in": {
|
|
993
947
|
const values = this.toArray(value);
|
|
994
|
-
return builder.where((
|
|
995
|
-
values.
|
|
996
|
-
|
|
997
|
-
|
|
998
|
-
|
|
999
|
-
|
|
948
|
+
return builder.where((eb) => eb.or(
|
|
949
|
+
values.flatMap((val) => [
|
|
950
|
+
sql`${textExpr} = ${val}`,
|
|
951
|
+
arrContains(val)
|
|
952
|
+
])
|
|
953
|
+
));
|
|
1000
954
|
}
|
|
1001
955
|
case "nin": {
|
|
1002
956
|
const values = this.toArray(value);
|
|
1003
|
-
return builder.
|
|
957
|
+
return builder.where(sql`${textExpr} not in (${sql.join(values.map((v) => sql`${v}`), sql`, `)})`);
|
|
1004
958
|
}
|
|
1005
959
|
case "like":
|
|
1006
|
-
return builder.where(textExpr
|
|
960
|
+
return builder.where(sql`${textExpr} like ${value}`);
|
|
1007
961
|
case "ilike":
|
|
1008
|
-
return builder.where(textExpr
|
|
962
|
+
return builder.where(sql`${textExpr} ilike ${value}`);
|
|
1009
963
|
case "exists":
|
|
1010
|
-
return value ? builder.
|
|
964
|
+
return value ? builder.where(sql`${textExpr} is not null`) : builder.where(sql`${textExpr} is null`);
|
|
1011
965
|
case "gt":
|
|
1012
966
|
case "gte":
|
|
1013
967
|
case "lt":
|
|
1014
968
|
case "lte": {
|
|
1015
|
-
const operator = op === "gt" ? ">" : op === "gte" ? ">=" : op === "lt" ? "<" : "<=";
|
|
1016
|
-
return builder.where(textExpr
|
|
969
|
+
const operator = sql.raw(op === "gt" ? ">" : op === "gte" ? ">=" : op === "lt" ? "<" : "<=");
|
|
970
|
+
return builder.where(sql`${textExpr} ${operator} ${value}`);
|
|
1017
971
|
}
|
|
1018
972
|
default:
|
|
1019
973
|
return builder;
|
|
1020
974
|
}
|
|
1021
975
|
}
|
|
1022
|
-
|
|
1023
|
-
|
|
1024
|
-
|
|
1025
|
-
const
|
|
976
|
+
/** Apply a search-token EXISTS subquery across multiple sources (OR-joined). */
|
|
977
|
+
applyMultiSourceSearchExists(builder, sources, key, hashes, search) {
|
|
978
|
+
if (!sources.length || !hashes.length) return { builder, applied: false };
|
|
979
|
+
const next = builder.where((eb) => eb.or(
|
|
980
|
+
sources.map(
|
|
981
|
+
(source) => eb.exists(this.buildSearchTokensSub(eb, {
|
|
982
|
+
entity: String(source.entityId),
|
|
983
|
+
field: key,
|
|
984
|
+
hashes,
|
|
985
|
+
recordIdColumn: `${source.alias}.entity_id`,
|
|
986
|
+
tenantId: search.tenantId ?? null,
|
|
987
|
+
organizationScope: search.organizationScope ?? null
|
|
988
|
+
}))
|
|
989
|
+
)
|
|
990
|
+
));
|
|
991
|
+
return { builder: next, applied: true };
|
|
992
|
+
}
|
|
993
|
+
/** Construct a search-token EXISTS subquery using the given ExpressionBuilder. */
|
|
994
|
+
buildSearchTokensSub(eb, opts) {
|
|
995
|
+
const alias = `st_${this.searchAliasSeq++}`;
|
|
996
|
+
let sub = eb.selectFrom(`search_tokens as ${alias}`).select(sql`1`.as("one")).where(`${alias}.entity_type`, "=", opts.entity).where(`${alias}.field`, "=", opts.field).where(sql`${sql.ref(`${alias}.entity_id`)} = ${sql.ref(opts.recordIdColumn)}::text`).where(`${alias}.token_hash`, "in", opts.hashes).groupBy([`${alias}.entity_id`, `${alias}.field`]).having(sql`count(distinct ${sql.ref(`${alias}.token_hash`)}) >= ${opts.hashes.length}`);
|
|
997
|
+
if (opts.tenantId !== void 0) {
|
|
998
|
+
sub = sub.where(sql`${sql.ref(`${alias}.tenant_id`)} is not distinct from ${opts.tenantId ?? null}`);
|
|
999
|
+
}
|
|
1000
|
+
if (opts.organizationScope) {
|
|
1001
|
+
sub = this.applyOrganizationScope(sub, `${alias}.organization_id`, opts.organizationScope);
|
|
1002
|
+
}
|
|
1003
|
+
return sub;
|
|
1004
|
+
}
|
|
1005
|
+
applyCfFilterFromAlias(q, alias, entityType, key, op, value, search) {
|
|
1006
|
+
const textExpr = this.cfTextExprAlias(alias, key);
|
|
1007
|
+
const arrExpr = sql`(${sql.ref(alias + ".doc")} -> ${key})`;
|
|
1008
|
+
const arrContains = (val) => sql`${arrExpr} @> ${JSON.stringify([val])}::jsonb`;
|
|
1026
1009
|
if ((op === "like" || op === "ilike") && search?.enabled && typeof value === "string") {
|
|
1027
1010
|
const tokens = tokenizeText(String(value), search.config);
|
|
1028
1011
|
const hashes = tokens.hashes;
|
|
1029
1012
|
if (hashes.length) {
|
|
1030
|
-
const applied = this.
|
|
1031
|
-
knex,
|
|
1013
|
+
const applied = q.where((eb) => eb.exists(this.buildSearchTokensSub(eb, {
|
|
1032
1014
|
entity: entityType,
|
|
1033
1015
|
field: key,
|
|
1034
1016
|
hashes,
|
|
1035
1017
|
recordIdColumn: `${alias}.entity_id`,
|
|
1036
1018
|
tenantId: search.tenantId ?? null,
|
|
1037
1019
|
organizationScope: search.organizationScope ?? null
|
|
1038
|
-
});
|
|
1020
|
+
})));
|
|
1039
1021
|
this.logSearchDebug("search:cf-filter", {
|
|
1040
1022
|
entity: entityType,
|
|
1041
1023
|
field: key,
|
|
1042
1024
|
tokens: tokens.tokens,
|
|
1043
1025
|
hashes,
|
|
1044
|
-
applied,
|
|
1026
|
+
applied: true,
|
|
1045
1027
|
tenantId: search.tenantId ?? null,
|
|
1046
1028
|
organizationScope: search.organizationScope
|
|
1047
1029
|
});
|
|
1048
|
-
|
|
1030
|
+
return applied;
|
|
1049
1031
|
} else {
|
|
1050
|
-
this.logSearchDebug("search:cf-skip-empty-hashes", {
|
|
1051
|
-
entity: entityType,
|
|
1052
|
-
field: key,
|
|
1053
|
-
value
|
|
1054
|
-
});
|
|
1032
|
+
this.logSearchDebug("search:cf-skip-empty-hashes", { entity: entityType, field: key, value });
|
|
1055
1033
|
}
|
|
1056
1034
|
return q;
|
|
1057
1035
|
}
|
|
1058
1036
|
switch (op) {
|
|
1059
1037
|
case "eq":
|
|
1060
|
-
return q.where((
|
|
1061
|
-
|
|
1062
|
-
|
|
1063
|
-
|
|
1038
|
+
return q.where((eb) => eb.or([
|
|
1039
|
+
sql`${textExpr} = ${value}`,
|
|
1040
|
+
arrContains(value)
|
|
1041
|
+
]));
|
|
1064
1042
|
case "ne":
|
|
1065
|
-
return q.
|
|
1043
|
+
return q.where(sql`${textExpr} <> ${value}`);
|
|
1066
1044
|
case "in": {
|
|
1067
1045
|
const vals = this.toArray(value);
|
|
1068
|
-
return q.where((
|
|
1069
|
-
vals.
|
|
1070
|
-
|
|
1071
|
-
|
|
1072
|
-
|
|
1073
|
-
|
|
1046
|
+
return q.where((eb) => eb.or(
|
|
1047
|
+
vals.flatMap((val) => [
|
|
1048
|
+
sql`${textExpr} = ${val}`,
|
|
1049
|
+
arrContains(val)
|
|
1050
|
+
])
|
|
1051
|
+
));
|
|
1074
1052
|
}
|
|
1075
1053
|
case "nin": {
|
|
1076
1054
|
const vals = this.toArray(value);
|
|
1077
|
-
return q.
|
|
1055
|
+
return q.where(sql`${textExpr} not in (${sql.join(vals.map((v) => sql`${v}`), sql`, `)})`);
|
|
1078
1056
|
}
|
|
1079
1057
|
case "like":
|
|
1080
|
-
return q.where(
|
|
1058
|
+
return q.where(sql`${textExpr} like ${value}`);
|
|
1081
1059
|
case "ilike":
|
|
1082
|
-
return q.where(
|
|
1060
|
+
return q.where(sql`${textExpr} ilike ${value}`);
|
|
1083
1061
|
case "exists":
|
|
1084
|
-
return value ? q.
|
|
1062
|
+
return value ? q.where(sql`${textExpr} is not null`) : q.where(sql`${textExpr} is null`);
|
|
1085
1063
|
case "gt":
|
|
1086
1064
|
case "gte":
|
|
1087
1065
|
case "lt":
|
|
1088
1066
|
case "lte": {
|
|
1089
|
-
const operator = op === "gt" ? ">" : op === "gte" ? ">=" : op === "lt" ? "<" : "<=";
|
|
1090
|
-
return q.where(
|
|
1067
|
+
const operator = sql.raw(op === "gt" ? ">" : op === "gte" ? ">=" : op === "lt" ? "<" : "<=");
|
|
1068
|
+
return q.where(sql`${textExpr} ${operator} ${value}`);
|
|
1091
1069
|
}
|
|
1092
1070
|
default:
|
|
1093
1071
|
return q;
|
|
1094
1072
|
}
|
|
1095
1073
|
}
|
|
1096
|
-
applyIndexDocFilterFromAlias(
|
|
1097
|
-
const
|
|
1074
|
+
applyIndexDocFilterFromAlias(q, alias, entityType, key, op, value, recordIdColumn, search) {
|
|
1075
|
+
const textExpr = sql`(${sql.ref(alias + ".doc")} ->> ${key})`;
|
|
1098
1076
|
if ((op === "like" || op === "ilike") && search?.enabled && typeof value === "string") {
|
|
1099
1077
|
const tokens = tokenizeText(String(value), search.config);
|
|
1100
1078
|
const hashes = tokens.hashes;
|
|
1101
1079
|
if (hashes.length) {
|
|
1102
|
-
const applied = this.
|
|
1103
|
-
knex,
|
|
1080
|
+
const applied = q.where((eb) => eb.exists(this.buildSearchTokensSub(eb, {
|
|
1104
1081
|
entity: entityType,
|
|
1105
1082
|
field: key,
|
|
1106
1083
|
hashes,
|
|
1107
1084
|
recordIdColumn,
|
|
1108
1085
|
tenantId: search.tenantId ?? null,
|
|
1109
1086
|
organizationScope: search.organizationScope ?? null
|
|
1110
|
-
});
|
|
1087
|
+
})));
|
|
1111
1088
|
this.logSearchDebug("search:index-doc-filter", {
|
|
1112
1089
|
entity: entityType,
|
|
1113
1090
|
field: key,
|
|
1114
1091
|
tokens: tokens.tokens,
|
|
1115
1092
|
hashes,
|
|
1116
|
-
applied,
|
|
1093
|
+
applied: true,
|
|
1117
1094
|
tenantId: search.tenantId ?? null,
|
|
1118
1095
|
organizationScope: search.organizationScope
|
|
1119
1096
|
});
|
|
1120
|
-
|
|
1097
|
+
return applied;
|
|
1121
1098
|
} else {
|
|
1122
|
-
this.logSearchDebug("search:index-doc-skip-empty-hashes", {
|
|
1123
|
-
entity: entityType,
|
|
1124
|
-
field: key,
|
|
1125
|
-
value
|
|
1126
|
-
});
|
|
1099
|
+
this.logSearchDebug("search:index-doc-skip-empty-hashes", { entity: entityType, field: key, value });
|
|
1127
1100
|
}
|
|
1128
1101
|
return q;
|
|
1129
1102
|
}
|
|
1130
1103
|
switch (op) {
|
|
1131
1104
|
case "eq":
|
|
1132
|
-
return q.where(
|
|
1105
|
+
return q.where(sql`${textExpr} = ${value}`);
|
|
1133
1106
|
case "ne":
|
|
1134
|
-
return q.where(
|
|
1107
|
+
return q.where(sql`${textExpr} <> ${value}`);
|
|
1108
|
+
case "in": {
|
|
1109
|
+
const vals = this.toArray(value);
|
|
1110
|
+
return q.where(sql`${textExpr} in (${sql.join(vals.map((v) => sql`${v}`), sql`, `)})`);
|
|
1111
|
+
}
|
|
1112
|
+
case "nin": {
|
|
1113
|
+
const vals = this.toArray(value);
|
|
1114
|
+
return q.where(sql`${textExpr} not in (${sql.join(vals.map((v) => sql`${v}`), sql`, `)})`);
|
|
1115
|
+
}
|
|
1116
|
+
case "like":
|
|
1117
|
+
return q.where(sql`${textExpr} like ${value}`);
|
|
1118
|
+
case "ilike":
|
|
1119
|
+
return q.where(sql`${textExpr} ilike ${value}`);
|
|
1120
|
+
case "exists":
|
|
1121
|
+
return value ? q.where(sql`${textExpr} is not null`) : q.where(sql`${textExpr} is null`);
|
|
1122
|
+
case "gt":
|
|
1123
|
+
case "gte":
|
|
1124
|
+
case "lt":
|
|
1125
|
+
case "lte": {
|
|
1126
|
+
const operator = sql.raw(op === "gt" ? ">" : op === "gte" ? ">=" : op === "lt" ? "<" : "<=");
|
|
1127
|
+
return q.where(sql`${textExpr} ${operator} ${value}`);
|
|
1128
|
+
}
|
|
1129
|
+
default:
|
|
1130
|
+
return q;
|
|
1131
|
+
}
|
|
1132
|
+
}
|
|
1133
|
+
/**
|
|
1134
|
+
* Build a single OR-group base filter expression as a Kysely predicate
|
|
1135
|
+
* (no side effects on the outer builder).
|
|
1136
|
+
*/
|
|
1137
|
+
buildBaseFilterExpression(eb, filter, resolveBaseColumn, qualify, entity, searchRuntime) {
|
|
1138
|
+
const fieldName = String(filter.field);
|
|
1139
|
+
const baseField = resolveBaseColumn(fieldName);
|
|
1140
|
+
if (!baseField) {
|
|
1141
|
+
return this.buildIndexDocFilterExpression(eb, "ei", entity, fieldName, filter.op, filter.value, "b.id", searchRuntime);
|
|
1142
|
+
}
|
|
1143
|
+
return this.buildColumnFilterExpression(eb, qualify(baseField), filter.op, filter.value);
|
|
1144
|
+
}
|
|
1145
|
+
buildColumnFilterExpression(eb, column, op, value) {
|
|
1146
|
+
switch (op) {
|
|
1147
|
+
case "eq":
|
|
1148
|
+
return eb(column, "=", value);
|
|
1149
|
+
case "ne":
|
|
1150
|
+
return eb(column, "!=", value);
|
|
1151
|
+
case "gt":
|
|
1152
|
+
return eb(column, ">", value);
|
|
1153
|
+
case "gte":
|
|
1154
|
+
return eb(column, ">=", value);
|
|
1155
|
+
case "lt":
|
|
1156
|
+
return eb(column, "<", value);
|
|
1157
|
+
case "lte":
|
|
1158
|
+
return eb(column, "<=", value);
|
|
1135
1159
|
case "in":
|
|
1136
|
-
return
|
|
1160
|
+
return eb(column, "in", this.toArray(value));
|
|
1137
1161
|
case "nin":
|
|
1138
|
-
return
|
|
1162
|
+
return eb(column, "not in", this.toArray(value));
|
|
1139
1163
|
case "like":
|
|
1140
|
-
return
|
|
1164
|
+
return eb(column, "like", value);
|
|
1141
1165
|
case "ilike":
|
|
1142
|
-
return
|
|
1166
|
+
return eb(column, "ilike", value);
|
|
1143
1167
|
case "exists":
|
|
1144
|
-
return value ?
|
|
1168
|
+
return eb(column, value ? "is not" : "is", null);
|
|
1169
|
+
default:
|
|
1170
|
+
return sql`true`;
|
|
1171
|
+
}
|
|
1172
|
+
}
|
|
1173
|
+
buildIndexDocFilterExpression(eb, alias, _entity, key, op, value, _recordIdColumn, _search) {
|
|
1174
|
+
const textExpr = sql`(${sql.ref(alias + ".doc")} ->> ${key})`;
|
|
1175
|
+
switch (op) {
|
|
1176
|
+
case "eq":
|
|
1177
|
+
return sql`${textExpr} = ${value}`;
|
|
1178
|
+
case "ne":
|
|
1179
|
+
return sql`${textExpr} <> ${value}`;
|
|
1145
1180
|
case "gt":
|
|
1146
1181
|
case "gte":
|
|
1147
1182
|
case "lt":
|
|
1148
1183
|
case "lte": {
|
|
1149
|
-
const operator = op === "gt" ? ">" : op === "gte" ? ">=" : op === "lt" ? "<" : "<=";
|
|
1150
|
-
return
|
|
1184
|
+
const operator = sql.raw(op === "gt" ? ">" : op === "gte" ? ">=" : op === "lt" ? "<" : "<=");
|
|
1185
|
+
return sql`${textExpr} ${operator} ${value}`;
|
|
1186
|
+
}
|
|
1187
|
+
case "like":
|
|
1188
|
+
return sql`${textExpr} like ${value}`;
|
|
1189
|
+
case "ilike":
|
|
1190
|
+
return sql`${textExpr} ilike ${value}`;
|
|
1191
|
+
case "in": {
|
|
1192
|
+
const vals = this.toArray(value);
|
|
1193
|
+
return sql`${textExpr} in (${sql.join(vals.map((v) => sql`${v}`), sql`, `)})`;
|
|
1151
1194
|
}
|
|
1195
|
+
case "nin": {
|
|
1196
|
+
const vals = this.toArray(value);
|
|
1197
|
+
return sql`${textExpr} not in (${sql.join(vals.map((v) => sql`${v}`), sql`, `)})`;
|
|
1198
|
+
}
|
|
1199
|
+
case "exists":
|
|
1200
|
+
return value ? sql`${textExpr} is not null` : sql`${textExpr} is null`;
|
|
1152
1201
|
default:
|
|
1153
|
-
return
|
|
1202
|
+
return sql`true`;
|
|
1154
1203
|
}
|
|
1155
1204
|
}
|
|
1156
1205
|
async queryCustomEntity(entity, opts = {}) {
|
|
1157
|
-
const
|
|
1206
|
+
const db = this.getDb();
|
|
1158
1207
|
const alias = "ce";
|
|
1159
|
-
let q = knex({ [alias]: "custom_entities_storage" }).where(`${alias}.entity_type`, entity);
|
|
1160
1208
|
const orgScope = this.resolveOrganizationScope(opts);
|
|
1161
1209
|
if (!opts.tenantId) throw new Error("QueryEngine: tenantId is required");
|
|
1162
|
-
q = q.andWhere(`${alias}.tenant_id`, opts.tenantId);
|
|
1163
|
-
if (orgScope) {
|
|
1164
|
-
q = this.applyOrganizationScope(q, `${alias}.organization_id`, orgScope);
|
|
1165
|
-
}
|
|
1166
|
-
if (!opts.withDeleted) q = q.whereNull(`${alias}.deleted_at`);
|
|
1167
1210
|
const searchConfig = resolveSearchConfig();
|
|
1168
1211
|
const searchEnabled = searchConfig.enabled && await this.tableExists("search_tokens");
|
|
1169
1212
|
const hasSearchTokens = searchEnabled ? await this.hasSearchTokens(entity, opts.tenantId ?? null, orgScope) : false;
|
|
@@ -1174,31 +1217,37 @@ class HybridQueryEngine {
|
|
|
1174
1217
|
tenantId: opts.tenantId ?? null
|
|
1175
1218
|
};
|
|
1176
1219
|
const normalizedFilters = normalizeFilters(opts.filters);
|
|
1177
|
-
|
|
1178
|
-
|
|
1179
|
-
|
|
1180
|
-
|
|
1220
|
+
const applyScope = (q) => {
|
|
1221
|
+
let next = q.where(`${alias}.entity_type`, "=", entity).where(`${alias}.tenant_id`, "=", opts.tenantId);
|
|
1222
|
+
if (orgScope) {
|
|
1223
|
+
next = this.applyOrganizationScope(next, `${alias}.organization_id`, orgScope);
|
|
1181
1224
|
}
|
|
1182
|
-
|
|
1183
|
-
|
|
1184
|
-
|
|
1225
|
+
if (!opts.withDeleted) next = next.where(`${alias}.deleted_at`, "is", null);
|
|
1226
|
+
for (const filter of normalizedFilters) {
|
|
1227
|
+
if (filter.field.startsWith("cf:")) {
|
|
1228
|
+
next = this.applyCfFilterFromAlias(next, alias, entity, filter.field, filter.op, filter.value, searchRuntime);
|
|
1229
|
+
continue;
|
|
1230
|
+
}
|
|
1231
|
+
const column = this.resolveCustomEntityColumn(alias, String(filter.field));
|
|
1232
|
+
if (column) {
|
|
1233
|
+
next = this.applyColumnFilter(next, column, filter, {
|
|
1234
|
+
...searchRuntime,
|
|
1235
|
+
entity,
|
|
1236
|
+
field: String(filter.field),
|
|
1237
|
+
recordIdColumn: `${alias}.entity_id`
|
|
1238
|
+
});
|
|
1239
|
+
continue;
|
|
1240
|
+
}
|
|
1241
|
+
const docExpr = sql`(${sql.ref(alias + ".doc")} ->> ${String(filter.field)})`;
|
|
1242
|
+
next = this.applyColumnFilter(next, docExpr, filter, {
|
|
1185
1243
|
...searchRuntime,
|
|
1186
|
-
knex,
|
|
1187
1244
|
entity,
|
|
1188
1245
|
field: String(filter.field),
|
|
1189
1246
|
recordIdColumn: `${alias}.entity_id`
|
|
1190
1247
|
});
|
|
1191
|
-
continue;
|
|
1192
1248
|
}
|
|
1193
|
-
|
|
1194
|
-
|
|
1195
|
-
...searchRuntime,
|
|
1196
|
-
knex,
|
|
1197
|
-
entity,
|
|
1198
|
-
field: String(filter.field),
|
|
1199
|
-
recordIdColumn: `${alias}.entity_id`
|
|
1200
|
-
});
|
|
1201
|
-
}
|
|
1249
|
+
return next;
|
|
1250
|
+
};
|
|
1202
1251
|
const cfKeys = /* @__PURE__ */ new Set();
|
|
1203
1252
|
for (const f of opts.fields || []) {
|
|
1204
1253
|
if (typeof f === "string" && f.startsWith("cf:")) cfKeys.add(f.slice(3));
|
|
@@ -1210,90 +1259,87 @@ class HybridQueryEngine {
|
|
|
1210
1259
|
}
|
|
1211
1260
|
if (opts.includeCustomFields === true) {
|
|
1212
1261
|
try {
|
|
1213
|
-
const rows = await
|
|
1214
|
-
qb.andWhere({ tenant_id: opts.tenantId });
|
|
1215
|
-
});
|
|
1262
|
+
const rows = await db.selectFrom("custom_field_defs").select("key").where("entity_id", "=", entity).where("is_active", "=", true).where("tenant_id", "=", opts.tenantId).execute();
|
|
1216
1263
|
for (const row of rows) {
|
|
1217
1264
|
const key = row.key;
|
|
1218
|
-
if (typeof key === "string")
|
|
1219
|
-
|
|
1220
|
-
} else if (key != null) {
|
|
1221
|
-
cfKeys.add(String(key));
|
|
1222
|
-
}
|
|
1265
|
+
if (typeof key === "string") cfKeys.add(key);
|
|
1266
|
+
else if (key != null) cfKeys.add(String(key));
|
|
1223
1267
|
}
|
|
1224
1268
|
} catch {
|
|
1225
1269
|
}
|
|
1226
1270
|
} else if (Array.isArray(opts.includeCustomFields)) {
|
|
1227
1271
|
for (const k of opts.includeCustomFields) cfKeys.add(k);
|
|
1228
1272
|
}
|
|
1229
|
-
const
|
|
1230
|
-
|
|
1231
|
-
const
|
|
1232
|
-
|
|
1233
|
-
const
|
|
1234
|
-
|
|
1235
|
-
|
|
1236
|
-
|
|
1237
|
-
|
|
1238
|
-
|
|
1239
|
-
|
|
1240
|
-
|
|
1241
|
-
|
|
1242
|
-
|
|
1273
|
+
const applySelection = (q) => {
|
|
1274
|
+
let next = q;
|
|
1275
|
+
const requested = opts.fields && opts.fields.length ? opts.fields : ["id"];
|
|
1276
|
+
for (const field of requested) {
|
|
1277
|
+
const f = String(field);
|
|
1278
|
+
if (f.startsWith("cf:")) {
|
|
1279
|
+
const aliasName = this.sanitize(f);
|
|
1280
|
+
next = next.select(this.jsonbSqlAlias(alias, f).as(aliasName));
|
|
1281
|
+
} else if (f === "id") {
|
|
1282
|
+
next = next.select(`${alias}.entity_id as id`);
|
|
1283
|
+
} else if (f === "created_at" || f === "updated_at" || f === "deleted_at") {
|
|
1284
|
+
next = next.select(`${alias}.${f} as ${f}`);
|
|
1285
|
+
} else {
|
|
1286
|
+
const expr = sql`(${sql.ref(alias + ".doc")} ->> ${f})`;
|
|
1287
|
+
next = next.select(expr.as(f));
|
|
1288
|
+
}
|
|
1243
1289
|
}
|
|
1244
|
-
|
|
1245
|
-
const cfSelectedAliases = [];
|
|
1246
|
-
for (const key of cfKeys) {
|
|
1247
|
-
const aliasName = this.sanitize(`cf:${key}`);
|
|
1248
|
-
const expr = this.jsonbRawAlias(knex, alias, `cf:${key}`);
|
|
1249
|
-
q = q.select({ [aliasName]: expr });
|
|
1250
|
-
cfSelectedAliases.push(aliasName);
|
|
1251
|
-
}
|
|
1252
|
-
for (const s of opts.sort || []) {
|
|
1253
|
-
if (s.field.startsWith("cf:")) {
|
|
1254
|
-
const key = s.field.slice(3);
|
|
1290
|
+
for (const key of cfKeys) {
|
|
1255
1291
|
const aliasName = this.sanitize(`cf:${key}`);
|
|
1256
|
-
|
|
1257
|
-
|
|
1258
|
-
|
|
1259
|
-
|
|
1292
|
+
next = next.select(this.jsonbSqlAlias(alias, `cf:${key}`).as(aliasName));
|
|
1293
|
+
}
|
|
1294
|
+
return next;
|
|
1295
|
+
};
|
|
1296
|
+
const applySort = (q) => {
|
|
1297
|
+
let next = q;
|
|
1298
|
+
for (const s of opts.sort || []) {
|
|
1299
|
+
if (s.field.startsWith("cf:")) {
|
|
1300
|
+
const key = s.field.slice(3);
|
|
1301
|
+
const aliasName = this.sanitize(`cf:${key}`);
|
|
1302
|
+
next = next.orderBy(aliasName, s.dir ?? SortDir.Asc);
|
|
1303
|
+
} else if (s.field === "id") {
|
|
1304
|
+
next = next.orderBy(`${alias}.entity_id`, s.dir ?? SortDir.Asc);
|
|
1305
|
+
} else if (s.field === "created_at" || s.field === "updated_at" || s.field === "deleted_at") {
|
|
1306
|
+
next = next.orderBy(`${alias}.${s.field}`, s.dir ?? SortDir.Asc);
|
|
1307
|
+
} else {
|
|
1308
|
+
const direction = sql.raw(String(s.dir ?? SortDir.Asc));
|
|
1309
|
+
next = next.orderBy(sql`(${sql.ref(alias + ".doc")} ->> ${s.field}) ${direction}`);
|
|
1260
1310
|
}
|
|
1261
|
-
q = q.orderBy(aliasName, s.dir ?? SortDir.Asc);
|
|
1262
|
-
} else if (s.field === "id") {
|
|
1263
|
-
q = q.orderBy(`${alias}.entity_id`, s.dir ?? SortDir.Asc);
|
|
1264
|
-
} else if (s.field === "created_at" || s.field === "updated_at" || s.field === "deleted_at") {
|
|
1265
|
-
q = q.orderBy(`${alias}.${s.field}`, s.dir ?? SortDir.Asc);
|
|
1266
|
-
} else {
|
|
1267
|
-
const direction = s.dir ?? SortDir.Asc;
|
|
1268
|
-
q = q.orderByRaw(`(${alias}.doc ->> ?) ${direction}`, [s.field]);
|
|
1269
1311
|
}
|
|
1270
|
-
|
|
1312
|
+
return next;
|
|
1313
|
+
};
|
|
1271
1314
|
const page = opts.page?.page ?? 1;
|
|
1272
1315
|
const pageSize = opts.page?.pageSize ?? 20;
|
|
1273
|
-
const
|
|
1274
|
-
|
|
1275
|
-
|
|
1276
|
-
const countRow = await countClone.countDistinct(`${alias}.entity_id as count`).first();
|
|
1316
|
+
const root = db.selectFrom(`custom_entities_storage as ${alias}`);
|
|
1317
|
+
const countQuery = applyScope(root).select(sql`count(distinct ${sql.ref(`${alias}.entity_id`)})`.as("count"));
|
|
1318
|
+
const countRow = await countQuery.executeTakeFirst();
|
|
1277
1319
|
const total = this.parseCount(countRow);
|
|
1278
|
-
|
|
1320
|
+
let dataQuery = applyScope(db.selectFrom(`custom_entities_storage as ${alias}`));
|
|
1321
|
+
dataQuery = applySelection(dataQuery);
|
|
1322
|
+
dataQuery = applySort(dataQuery);
|
|
1323
|
+
dataQuery = dataQuery.limit(pageSize).offset((page - 1) * pageSize);
|
|
1324
|
+
const items = await dataQuery.execute();
|
|
1279
1325
|
return { items, page, pageSize, total };
|
|
1280
1326
|
}
|
|
1281
1327
|
async tableExists(table) {
|
|
1282
|
-
const
|
|
1283
|
-
const exists = await
|
|
1328
|
+
const db = this.getDb();
|
|
1329
|
+
const exists = await db.selectFrom("information_schema.tables").select(sql`1`.as("one")).where("table_name", "=", table).executeTakeFirst();
|
|
1284
1330
|
return !!exists;
|
|
1285
1331
|
}
|
|
1286
1332
|
async hasSearchTokens(entity, tenantId, orgScope) {
|
|
1287
1333
|
try {
|
|
1288
|
-
const
|
|
1289
|
-
|
|
1334
|
+
const db = this.getDb();
|
|
1335
|
+
let query = db.selectFrom("search_tokens").select(sql`1`.as("one")).where("entity_type", "=", entity);
|
|
1290
1336
|
if (tenantId !== void 0) {
|
|
1291
|
-
query.
|
|
1337
|
+
query = query.where(sql`tenant_id is not distinct from ${tenantId}`);
|
|
1292
1338
|
}
|
|
1293
1339
|
if (orgScope) {
|
|
1294
|
-
this.applyOrganizationScope(query, "search_tokens.organization_id", orgScope);
|
|
1340
|
+
query = this.applyOrganizationScope(query, "search_tokens.organization_id", orgScope);
|
|
1295
1341
|
}
|
|
1296
|
-
const row = await query.
|
|
1342
|
+
const row = await query.limit(1).executeTakeFirst();
|
|
1297
1343
|
return !!row;
|
|
1298
1344
|
} catch (err) {
|
|
1299
1345
|
this.logSearchDebug("search:has-tokens-error", {
|
|
@@ -1324,17 +1370,14 @@ class HybridQueryEngine {
|
|
|
1324
1370
|
const cacheKey = this.customFieldKeysCacheKey(entityIds, tenantId);
|
|
1325
1371
|
const now = Date.now();
|
|
1326
1372
|
const cached = this.customFieldKeysCache.get(cacheKey);
|
|
1327
|
-
if (cached && cached.expiresAt > now)
|
|
1328
|
-
|
|
1329
|
-
|
|
1330
|
-
|
|
1331
|
-
|
|
1332
|
-
|
|
1333
|
-
inner.where({ tenant_id: tenantId }).orWhereNull("tenant_id");
|
|
1334
|
-
});
|
|
1335
|
-
});
|
|
1373
|
+
if (cached && cached.expiresAt > now) return cached.value.slice();
|
|
1374
|
+
const db = this.getDb();
|
|
1375
|
+
const rows = await db.selectFrom("custom_field_defs").select("key").where("entity_id", "in", entityIds).where("is_active", "=", true).where((eb) => eb.or([
|
|
1376
|
+
eb("tenant_id", "=", tenantId),
|
|
1377
|
+
eb("tenant_id", "is", null)
|
|
1378
|
+
])).execute();
|
|
1336
1379
|
const keys = /* @__PURE__ */ new Set();
|
|
1337
|
-
for (const row of rows
|
|
1380
|
+
for (const row of rows) {
|
|
1338
1381
|
const key = row.key;
|
|
1339
1382
|
if (typeof key === "string" && key.trim().length) keys.add(key.trim());
|
|
1340
1383
|
else if (key != null) keys.add(String(key));
|
|
@@ -1376,27 +1419,24 @@ class HybridQueryEngine {
|
|
|
1376
1419
|
return entity;
|
|
1377
1420
|
}
|
|
1378
1421
|
async indexAnyRows(entity) {
|
|
1379
|
-
const
|
|
1380
|
-
const coverage = await
|
|
1422
|
+
const db = this.getDb();
|
|
1423
|
+
const coverage = await db.selectFrom("entity_index_coverage").select(sql`1`.as("one")).where("entity_type", "=", entity).where("indexed_count", ">", 0).executeTakeFirst();
|
|
1381
1424
|
if (coverage) return true;
|
|
1382
|
-
const exists = await
|
|
1425
|
+
const exists = await db.selectFrom("entity_indexes").select("entity_id").where("entity_type", "=", entity).executeTakeFirst();
|
|
1383
1426
|
return !!exists;
|
|
1384
1427
|
}
|
|
1385
1428
|
async getStoredCoverageSnapshot(entity, tenantId, organizationId, withDeleted) {
|
|
1386
1429
|
try {
|
|
1387
1430
|
if (!this.isCoverageOptimizationEnabled()) {
|
|
1388
|
-
await refreshCoverageSnapshot(
|
|
1389
|
-
|
|
1390
|
-
|
|
1391
|
-
|
|
1392
|
-
|
|
1393
|
-
|
|
1394
|
-
withDeleted
|
|
1395
|
-
}
|
|
1396
|
-
);
|
|
1431
|
+
await refreshCoverageSnapshot(this.em, {
|
|
1432
|
+
entityType: entity,
|
|
1433
|
+
tenantId,
|
|
1434
|
+
organizationId,
|
|
1435
|
+
withDeleted
|
|
1436
|
+
});
|
|
1397
1437
|
}
|
|
1398
|
-
const
|
|
1399
|
-
const row = await readCoverageSnapshot(
|
|
1438
|
+
const db = this.getDb();
|
|
1439
|
+
const row = await readCoverageSnapshot(db, {
|
|
1400
1440
|
entityType: entity,
|
|
1401
1441
|
tenantId,
|
|
1402
1442
|
organizationId,
|
|
@@ -1427,13 +1467,7 @@ class HybridQueryEngine {
|
|
|
1427
1467
|
organizationId: organizationIdOverride ?? opts.organizationId ?? null,
|
|
1428
1468
|
force: false
|
|
1429
1469
|
};
|
|
1430
|
-
const context = stats ? {
|
|
1431
|
-
entity,
|
|
1432
|
-
tenantId: payload.tenantId,
|
|
1433
|
-
organizationId: payload.organizationId,
|
|
1434
|
-
baseCount: stats.baseCount,
|
|
1435
|
-
indexedCount: stats.indexedCount
|
|
1436
|
-
} : { entity, tenantId: payload.tenantId, organizationId: payload.organizationId };
|
|
1470
|
+
const context = stats ? { entity, tenantId: payload.tenantId, organizationId: payload.organizationId, baseCount: stats.baseCount, indexedCount: stats.indexedCount } : { entity, tenantId: payload.tenantId, organizationId: payload.organizationId };
|
|
1437
1471
|
void Promise.resolve().then(async () => {
|
|
1438
1472
|
try {
|
|
1439
1473
|
await bus.emitEvent("query_index.reindex", payload, { persistent: true });
|
|
@@ -1449,12 +1483,7 @@ class HybridQueryEngine {
|
|
|
1449
1483
|
scheduleCoverageRefresh(entity, tenantId, organizationId, withDeleted) {
|
|
1450
1484
|
const bus = this.resolveEventBus();
|
|
1451
1485
|
if (!bus) return;
|
|
1452
|
-
const key = [
|
|
1453
|
-
entity,
|
|
1454
|
-
tenantId ?? "__tenant__",
|
|
1455
|
-
organizationId ?? "__org__",
|
|
1456
|
-
withDeleted ? "1" : "0"
|
|
1457
|
-
].join("|");
|
|
1486
|
+
const key = [entity, tenantId ?? "__tenant__", organizationId ?? "__org__", withDeleted ? "1" : "0"].join("|");
|
|
1458
1487
|
if (this.pendingCoverageRefreshKeys.has(key)) return;
|
|
1459
1488
|
this.pendingCoverageRefreshKeys.add(key);
|
|
1460
1489
|
void Promise.resolve().then(async () => {
|
|
@@ -1526,17 +1555,17 @@ class HybridQueryEngine {
|
|
|
1526
1555
|
if (cached === true) return true;
|
|
1527
1556
|
this.columnCache.delete(key);
|
|
1528
1557
|
}
|
|
1529
|
-
const
|
|
1530
|
-
const exists = await
|
|
1558
|
+
const db = this.getDb();
|
|
1559
|
+
const exists = await db.selectFrom("information_schema.columns").select(sql`1`.as("one")).where("table_name", "=", table).where("column_name", "=", column).executeTakeFirst();
|
|
1531
1560
|
const present = !!exists;
|
|
1532
1561
|
if (present) this.columnCache.set(key, true);
|
|
1533
1562
|
else this.columnCache.delete(key);
|
|
1534
1563
|
return present;
|
|
1535
1564
|
}
|
|
1536
1565
|
async getBaseColumnsForEntity(entity) {
|
|
1537
|
-
const
|
|
1566
|
+
const db = this.getDb();
|
|
1538
1567
|
const table = resolveEntityTableName(this.em, entity);
|
|
1539
|
-
const rows = await
|
|
1568
|
+
const rows = await db.selectFrom("information_schema.columns").select(["column_name", "data_type"]).where("table_name", "=", table).execute();
|
|
1540
1569
|
const map = /* @__PURE__ */ new Map();
|
|
1541
1570
|
for (const r of rows) map.set(r.column_name, r.data_type);
|
|
1542
1571
|
return map;
|
|
@@ -1568,20 +1597,14 @@ class HybridQueryEngine {
|
|
|
1568
1597
|
}
|
|
1569
1598
|
applyOrganizationScope(q, column, scope) {
|
|
1570
1599
|
if (scope.ids.length === 0 && !scope.includeNull) {
|
|
1571
|
-
return q.
|
|
1600
|
+
return q.where(sql`1 = 0`);
|
|
1572
1601
|
}
|
|
1573
|
-
return q.where((
|
|
1574
|
-
|
|
1575
|
-
if (scope.ids.length > 0)
|
|
1576
|
-
|
|
1577
|
-
|
|
1578
|
-
|
|
1579
|
-
if (scope.includeNull) {
|
|
1580
|
-
if (applied) builder.orWhereNull(column);
|
|
1581
|
-
else builder.whereNull(column);
|
|
1582
|
-
} else if (!applied) {
|
|
1583
|
-
builder.whereRaw("1 = 0");
|
|
1584
|
-
}
|
|
1602
|
+
return q.where((eb) => {
|
|
1603
|
+
const parts = [];
|
|
1604
|
+
if (scope.ids.length > 0) parts.push(eb(column, "in", scope.ids));
|
|
1605
|
+
if (scope.includeNull) parts.push(eb(column, "is", null));
|
|
1606
|
+
if (parts.length === 1) return parts[0];
|
|
1607
|
+
return eb.or(parts);
|
|
1585
1608
|
});
|
|
1586
1609
|
}
|
|
1587
1610
|
normalizeFilters(filters) {
|
|
@@ -1647,12 +1670,8 @@ class HybridQueryEngine {
|
|
|
1647
1670
|
return s.replace(/[^a-zA-Z0-9_]/g, "_");
|
|
1648
1671
|
}
|
|
1649
1672
|
toArray(value) {
|
|
1650
|
-
if (Array.isArray(value))
|
|
1651
|
-
|
|
1652
|
-
}
|
|
1653
|
-
if (value === void 0) {
|
|
1654
|
-
return [];
|
|
1655
|
-
}
|
|
1673
|
+
if (Array.isArray(value)) return value;
|
|
1674
|
+
if (value === void 0) return [];
|
|
1656
1675
|
return [value];
|
|
1657
1676
|
}
|
|
1658
1677
|
parseCount(row) {
|
|
@@ -1663,6 +1682,7 @@ class HybridQueryEngine {
|
|
|
1663
1682
|
const parsed = Number(value);
|
|
1664
1683
|
return Number.isNaN(parsed) ? 0 : parsed;
|
|
1665
1684
|
}
|
|
1685
|
+
if (typeof value === "bigint") return Number(value);
|
|
1666
1686
|
}
|
|
1667
1687
|
return 0;
|
|
1668
1688
|
}
|
|
@@ -1680,35 +1700,30 @@ class HybridQueryEngine {
|
|
|
1680
1700
|
const hashes = tokens.hashes;
|
|
1681
1701
|
if (hashes.length) {
|
|
1682
1702
|
const sources = (search.searchSources && search.searchSources.length ? search.searchSources : [{ entity: search.entity, recordIdColumn: search.recordIdColumn ?? "" }]).filter((src) => src.recordIdColumn && src.entity);
|
|
1683
|
-
let applied = false;
|
|
1684
1703
|
if (sources.length) {
|
|
1685
|
-
|
|
1686
|
-
|
|
1687
|
-
|
|
1688
|
-
|
|
1689
|
-
|
|
1690
|
-
|
|
1691
|
-
|
|
1692
|
-
|
|
1693
|
-
|
|
1694
|
-
|
|
1695
|
-
|
|
1696
|
-
|
|
1697
|
-
|
|
1698
|
-
|
|
1704
|
+
const engine = this;
|
|
1705
|
+
q = q.where((eb) => eb.or(
|
|
1706
|
+
sources.map((src) => eb.exists(engine.buildSearchTokensSub(eb, {
|
|
1707
|
+
entity: src.entity,
|
|
1708
|
+
field: search.field,
|
|
1709
|
+
hashes,
|
|
1710
|
+
recordIdColumn: src.recordIdColumn,
|
|
1711
|
+
tenantId: search.tenantId ?? null,
|
|
1712
|
+
organizationScope: search.organizationScope ?? null
|
|
1713
|
+
})))
|
|
1714
|
+
));
|
|
1715
|
+
this.logSearchDebug("search:filter", {
|
|
1716
|
+
entity: search.entity,
|
|
1717
|
+
field: search.field,
|
|
1718
|
+
tokens: tokens.tokens,
|
|
1719
|
+
hashes,
|
|
1720
|
+
applied: true,
|
|
1721
|
+
tenantId: search.tenantId ?? null,
|
|
1722
|
+
organizationScope: search.organizationScope,
|
|
1723
|
+
sources: sources.map((src) => ({ entity: src.entity, recordIdColumn: src.recordIdColumn }))
|
|
1699
1724
|
});
|
|
1725
|
+
return q;
|
|
1700
1726
|
}
|
|
1701
|
-
this.logSearchDebug("search:filter", {
|
|
1702
|
-
entity: search.entity,
|
|
1703
|
-
field: search.field,
|
|
1704
|
-
tokens: tokens.tokens,
|
|
1705
|
-
hashes,
|
|
1706
|
-
applied,
|
|
1707
|
-
tenantId: search.tenantId ?? null,
|
|
1708
|
-
organizationScope: search.organizationScope,
|
|
1709
|
-
sources: sources.map((src) => ({ entity: src.entity, recordIdColumn: src.recordIdColumn }))
|
|
1710
|
-
});
|
|
1711
|
-
if (applied) return q;
|
|
1712
1727
|
} else {
|
|
1713
1728
|
this.logSearchDebug("search:skip-empty-hashes", {
|
|
1714
1729
|
entity: search.entity,
|
|
@@ -1721,9 +1736,9 @@ class HybridQueryEngine {
|
|
|
1721
1736
|
const col = column;
|
|
1722
1737
|
switch (filter.op) {
|
|
1723
1738
|
case "eq":
|
|
1724
|
-
return q.where(col, filter.value);
|
|
1739
|
+
return q.where(col, "=", filter.value);
|
|
1725
1740
|
case "ne":
|
|
1726
|
-
return q.
|
|
1741
|
+
return q.where(col, "!=", filter.value);
|
|
1727
1742
|
case "gt":
|
|
1728
1743
|
case "gte":
|
|
1729
1744
|
case "lt":
|
|
@@ -1731,20 +1746,16 @@ class HybridQueryEngine {
|
|
|
1731
1746
|
const operator = filter.op === "gt" ? ">" : filter.op === "gte" ? ">=" : filter.op === "lt" ? "<" : "<=";
|
|
1732
1747
|
return q.where(col, operator, filter.value);
|
|
1733
1748
|
}
|
|
1734
|
-
case "in":
|
|
1735
|
-
|
|
1736
|
-
|
|
1737
|
-
|
|
1738
|
-
case "nin": {
|
|
1739
|
-
const values = this.toArray(filter.value);
|
|
1740
|
-
return q.whereNotIn(col, values);
|
|
1741
|
-
}
|
|
1749
|
+
case "in":
|
|
1750
|
+
return q.where(col, "in", this.toArray(filter.value));
|
|
1751
|
+
case "nin":
|
|
1752
|
+
return q.where(col, "not in", this.toArray(filter.value));
|
|
1742
1753
|
case "like":
|
|
1743
1754
|
return q.where(col, "like", filter.value);
|
|
1744
1755
|
case "ilike":
|
|
1745
1756
|
return q.where(col, "ilike", filter.value);
|
|
1746
1757
|
case "exists":
|
|
1747
|
-
return filter.value ? q.
|
|
1758
|
+
return filter.value ? q.where(col, "is not", null) : q.where(col, "is", null);
|
|
1748
1759
|
default:
|
|
1749
1760
|
return q;
|
|
1750
1761
|
}
|
|
@@ -1785,9 +1796,7 @@ class HybridQueryEngine {
|
|
|
1785
1796
|
const baseCount = snapshot.baseCount;
|
|
1786
1797
|
const indexCount = snapshot.indexedCount;
|
|
1787
1798
|
const hasGap = baseCount > 0 && indexCount < baseCount;
|
|
1788
|
-
if (hasGap || indexCount > baseCount) {
|
|
1789
|
-
return { stats: snapshot, scope: "scoped" };
|
|
1790
|
-
}
|
|
1799
|
+
if (hasGap || indexCount > baseCount) return { stats: snapshot, scope: "scoped" };
|
|
1791
1800
|
return null;
|
|
1792
1801
|
}
|
|
1793
1802
|
// Backward-compatible hook for tests that mock coverage stats
|
|
@@ -1798,18 +1807,13 @@ class HybridQueryEngine {
|
|
|
1798
1807
|
async captureSqlTiming(label, entity, execute, extra, profiler) {
|
|
1799
1808
|
const shouldDebug = this.isSqlDebugEnabled() && this.isDebugVerbosity();
|
|
1800
1809
|
const shouldProfile = profiler?.enabled === true;
|
|
1801
|
-
if (!shouldDebug && !shouldProfile)
|
|
1802
|
-
return Promise.resolve(execute());
|
|
1803
|
-
}
|
|
1810
|
+
if (!shouldDebug && !shouldProfile) return Promise.resolve(execute());
|
|
1804
1811
|
const startedAt = process.hrtime.bigint();
|
|
1805
1812
|
try {
|
|
1806
1813
|
return await Promise.resolve(execute());
|
|
1807
1814
|
} finally {
|
|
1808
1815
|
const elapsedMs = Number(process.hrtime.bigint() - startedAt) / 1e6;
|
|
1809
|
-
const context = {
|
|
1810
|
-
entity,
|
|
1811
|
-
durationMs: Math.round(elapsedMs * 1e3) / 1e3
|
|
1812
|
-
};
|
|
1816
|
+
const context = { entity, durationMs: Math.round(elapsedMs * 1e3) / 1e3 };
|
|
1813
1817
|
if (extra) Object.assign(context, extra);
|
|
1814
1818
|
if (shouldProfile) profiler.record(label, context.durationMs, extra);
|
|
1815
1819
|
if (shouldDebug) this.debug(`${label}:timing`, context);
|