@adtrackify/at-service-common 3.19.25 → 4.0.1

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (761) hide show
  1. package/dist/cjs/__tests__/clients/acuity-client.spec.d.ts +1 -1
  2. package/dist/cjs/__tests__/clients/acuity-client.spec.js +43 -43
  3. package/dist/cjs/__tests__/clients/cross-platform-compression.spec.d.ts +1 -1
  4. package/dist/cjs/__tests__/clients/cross-platform-compression.spec.js +354 -354
  5. package/dist/cjs/__tests__/clients/dynamodb-client.spec.d.ts +1 -1
  6. package/dist/cjs/__tests__/clients/dynamodb-client.spec.js +194 -194
  7. package/dist/cjs/__tests__/clients/sqs-bundled-client.spec.d.ts +1 -1
  8. package/dist/cjs/__tests__/clients/sqs-bundled-client.spec.js +931 -931
  9. package/dist/cjs/__tests__/clients/sqs-bundling-contracts.spec.d.ts +1 -1
  10. package/dist/cjs/__tests__/clients/sqs-bundling-contracts.spec.js +563 -563
  11. package/dist/cjs/__tests__/clients/sqs-client.spec.d.ts +1 -1
  12. package/dist/cjs/__tests__/clients/sqs-client.spec.js +191 -191
  13. package/dist/cjs/__tests__/clients/sqs-unbundle.spec.d.ts +1 -1
  14. package/dist/cjs/__tests__/clients/sqs-unbundle.spec.js +1357 -1357
  15. package/dist/cjs/__tests__/db/contact-enrichments-db-service.spec.d.ts +1 -1
  16. package/dist/cjs/__tests__/db/contact-enrichments-db-service.spec.js +68 -68
  17. package/dist/cjs/__tests__/db/destinations-db-service.spec.d.ts +1 -1
  18. package/dist/cjs/__tests__/db/destinations-db-service.spec.js +125 -125
  19. package/dist/cjs/__tests__/db/shared-read-db-services.spec.d.ts +1 -1
  20. package/dist/cjs/__tests__/db/shared-read-db-services.spec.js +89 -89
  21. package/dist/cjs/__tests__/db/shopify-app-installs-db-service.spec.d.ts +1 -1
  22. package/dist/cjs/__tests__/db/shopify-app-installs-db-service.spec.js +104 -104
  23. package/dist/cjs/__tests__/db/subscriptions-db-service.spec.d.ts +1 -1
  24. package/dist/cjs/__tests__/db/subscriptions-db-service.spec.js +95 -95
  25. package/dist/cjs/__tests__/db/user-accounts-db-service.spec.d.ts +1 -1
  26. package/dist/cjs/__tests__/db/user-accounts-db-service.spec.js +76 -76
  27. package/dist/cjs/__tests__/helpers/account-users-helper.spec.d.ts +1 -1
  28. package/dist/cjs/__tests__/helpers/account-users-helper.spec.js +220 -220
  29. package/dist/cjs/__tests__/helpers/acuity-helper.spec.d.ts +1 -1
  30. package/dist/cjs/__tests__/helpers/acuity-helper.spec.js +69 -69
  31. package/dist/cjs/__tests__/helpers/api-key-auth-helper.spec.d.ts +1 -1
  32. package/dist/cjs/__tests__/helpers/api-key-auth-helper.spec.js +82 -82
  33. package/dist/cjs/__tests__/identity-cache/identity-cache-db-service.spec.d.ts +1 -1
  34. package/dist/cjs/__tests__/identity-cache/identity-cache-db-service.spec.js +676 -674
  35. package/dist/cjs/__tests__/identity-cache/identity-cache-db-service.spec.js.map +1 -1
  36. package/dist/cjs/__tests__/identity-cache/identity-cache-dynamodb-service.spec.d.ts +1 -1
  37. package/dist/cjs/__tests__/identity-cache/identity-cache-dynamodb-service.spec.js +1140 -1140
  38. package/dist/cjs/__tests__/{db/products-db-service.spec.d.ts → identity-cache/identity-cache-tier-routing.spec.d.ts} +1 -1
  39. package/dist/cjs/__tests__/identity-cache/identity-cache-tier-routing.spec.js +852 -0
  40. package/dist/cjs/__tests__/identity-cache/identity-cache-tier-routing.spec.js.map +1 -0
  41. package/dist/cjs/__tests__/identity-cache/trait-merging-and-staleness.spec.d.ts +1 -1
  42. package/dist/cjs/__tests__/identity-cache/trait-merging-and-staleness.spec.js +1060 -588
  43. package/dist/cjs/__tests__/identity-cache/trait-merging-and-staleness.spec.js.map +1 -1
  44. package/dist/cjs/__tests__/{services/cost/cost-calculator-service.spec.d.ts → identity-cache/volatile-traits-optimization.spec.d.ts} +1 -1
  45. package/dist/cjs/__tests__/identity-cache/volatile-traits-optimization.spec.js +819 -0
  46. package/dist/cjs/__tests__/identity-cache/volatile-traits-optimization.spec.js.map +1 -0
  47. package/dist/cjs/__tests__/integration/sqs-bundling-roundtrip.spec.d.ts +1 -1
  48. package/dist/cjs/__tests__/integration/sqs-bundling-roundtrip.spec.js +584 -584
  49. package/dist/cjs/__tests__/libs/compress-decompress.spec.d.ts +1 -1
  50. package/dist/cjs/__tests__/libs/compress-decompress.spec.js +16 -16
  51. package/dist/cjs/__tests__/libs/contacts.spec.d.ts +1 -1
  52. package/dist/cjs/__tests__/libs/contacts.spec.js +294 -294
  53. package/dist/cjs/__tests__/libs/currency.spec.d.ts +1 -1
  54. package/dist/cjs/__tests__/libs/currency.spec.js +220 -220
  55. package/dist/cjs/__tests__/libs/dates.spec.d.ts +1 -1
  56. package/dist/cjs/__tests__/libs/dates.spec.js +130 -130
  57. package/dist/cjs/__tests__/libs/domain.spec.d.ts +1 -1
  58. package/dist/cjs/__tests__/libs/domain.spec.js +107 -107
  59. package/dist/cjs/__tests__/libs/numbers.spec.d.ts +1 -1
  60. package/dist/cjs/__tests__/libs/numbers.spec.js +261 -261
  61. package/dist/cjs/__tests__/s3-client/s3-client.spec.d.ts +1 -1
  62. package/dist/cjs/__tests__/s3-client/s3-client.spec.js +33 -33
  63. package/dist/cjs/__tests__/services/acuity-api-service.spec.d.ts +1 -1
  64. package/dist/cjs/__tests__/services/acuity-api-service.spec.js +71 -71
  65. package/dist/cjs/__tests__/services/email-verification/contact-email-verification-service.spec.d.ts +1 -1
  66. package/dist/cjs/__tests__/services/email-verification/contact-email-verification-service.spec.js +93 -93
  67. package/dist/cjs/__tests__/services/email-verification/email-verification-service.spec.d.ts +1 -1
  68. package/dist/cjs/__tests__/services/email-verification/email-verification-service.spec.js +57 -57
  69. package/dist/cjs/__tests__/shopify/shopify-graphql-transformer.spec.d.ts +1 -1
  70. package/dist/cjs/__tests__/shopify/shopify-graphql-transformer.spec.js +35 -35
  71. package/dist/cjs/__tests__/unit/libs/api-router/public-api-router.spec.d.ts +1 -1
  72. package/dist/cjs/__tests__/unit/libs/api-router/public-api-router.spec.js +181 -181
  73. package/dist/cjs/__tests__/unit/libs/api-router/route-matcher.spec.d.ts +1 -1
  74. package/dist/cjs/__tests__/unit/libs/api-router/route-matcher.spec.js +69 -69
  75. package/dist/cjs/__tests__/utils/custom-measure-formula-utils.spec.d.ts +1 -1
  76. package/dist/cjs/__tests__/utils/custom-measure-formula-utils.spec.js +139 -139
  77. package/dist/cjs/clients/generic/cognito-client.d.ts +23 -23
  78. package/dist/cjs/clients/generic/cognito-client.js +209 -209
  79. package/dist/cjs/clients/generic/dynamodb-client.d.ts +20 -20
  80. package/dist/cjs/clients/generic/dynamodb-client.js +235 -235
  81. package/dist/cjs/clients/generic/eventbridge-client.d.ts +14 -14
  82. package/dist/cjs/clients/generic/eventbridge-client.js +51 -51
  83. package/dist/cjs/clients/generic/http-client.d.ts +14 -14
  84. package/dist/cjs/clients/generic/http-client.js +61 -61
  85. package/dist/cjs/clients/generic/index.d.ts +13 -13
  86. package/dist/cjs/clients/generic/index.js +29 -29
  87. package/dist/cjs/clients/generic/lambda-invoke-client.d.ts +10 -10
  88. package/dist/cjs/clients/generic/lambda-invoke-client.js +39 -39
  89. package/dist/cjs/clients/generic/location-client.d.ts +8 -8
  90. package/dist/cjs/clients/generic/location-client.js +31 -31
  91. package/dist/cjs/clients/generic/redis-client.d.ts +33 -33
  92. package/dist/cjs/clients/generic/redis-client.js +191 -191
  93. package/dist/cjs/clients/generic/s3-client.d.ts +23 -23
  94. package/dist/cjs/clients/generic/s3-client.js +216 -216
  95. package/dist/cjs/clients/generic/singlestore-db-client.d.ts +14 -14
  96. package/dist/cjs/clients/generic/singlestore-db-client.js +67 -67
  97. package/dist/cjs/clients/generic/sqs-bundled-client.d.ts +15 -15
  98. package/dist/cjs/clients/generic/sqs-bundled-client.js +311 -311
  99. package/dist/cjs/clients/generic/sqs-bundled-client.types.d.ts +53 -53
  100. package/dist/cjs/clients/generic/sqs-bundled-client.types.js +17 -17
  101. package/dist/cjs/clients/generic/sqs-client.d.ts +53 -53
  102. package/dist/cjs/clients/generic/sqs-client.js +285 -285
  103. package/dist/cjs/clients/generic/sqs-unbundle.d.ts +32 -32
  104. package/dist/cjs/clients/generic/sqs-unbundle.js +144 -144
  105. package/dist/cjs/clients/index.d.ts +3 -3
  106. package/dist/cjs/clients/index.js +19 -19
  107. package/dist/cjs/clients/internal-api/accounts-client.d.ts +91 -91
  108. package/dist/cjs/clients/internal-api/accounts-client.js +129 -129
  109. package/dist/cjs/clients/internal-api/cache-lambda-client.d.ts +26 -26
  110. package/dist/cjs/clients/internal-api/cache-lambda-client.js +89 -89
  111. package/dist/cjs/clients/internal-api/db-management-client.d.ts +18 -18
  112. package/dist/cjs/clients/internal-api/db-management-client.js +36 -36
  113. package/dist/cjs/clients/internal-api/destinations-client.d.ts +34 -34
  114. package/dist/cjs/clients/internal-api/destinations-client.js +79 -79
  115. package/dist/cjs/clients/internal-api/event-collector-client.d.ts +20 -20
  116. package/dist/cjs/clients/internal-api/event-collector-client.js +36 -36
  117. package/dist/cjs/clients/internal-api/identity-client.d.ts +31 -31
  118. package/dist/cjs/clients/internal-api/identity-client.js +91 -91
  119. package/dist/cjs/clients/internal-api/index.d.ts +9 -9
  120. package/dist/cjs/clients/internal-api/index.js +25 -25
  121. package/dist/cjs/clients/internal-api/shopify-app-install-client.d.ts +37 -37
  122. package/dist/cjs/clients/internal-api/shopify-app-install-client.js +81 -81
  123. package/dist/cjs/clients/internal-api/subscriptions-client.d.ts +26 -26
  124. package/dist/cjs/clients/internal-api/subscriptions-client.js +77 -77
  125. package/dist/cjs/clients/internal-api/users-auth-client.d.ts +35 -35
  126. package/dist/cjs/clients/internal-api/users-auth-client.js +110 -110
  127. package/dist/cjs/clients/third-party/acuity-client.d.ts +10 -10
  128. package/dist/cjs/clients/third-party/acuity-client.js +40 -40
  129. package/dist/cjs/clients/third-party/emailable-client.d.ts +7 -7
  130. package/dist/cjs/clients/third-party/emailable-client.js +25 -25
  131. package/dist/cjs/clients/third-party/exchange-rate-api-client.d.ts +17 -17
  132. package/dist/cjs/clients/third-party/exchange-rate-api-client.js +19 -19
  133. package/dist/cjs/clients/third-party/index.d.ts +5 -5
  134. package/dist/cjs/clients/third-party/index.js +21 -21
  135. package/dist/cjs/clients/third-party/loops-client.d.ts +10 -10
  136. package/dist/cjs/clients/third-party/loops-client.js +30 -30
  137. package/dist/cjs/clients/third-party/shopify/graphql-order-queries.d.ts +25 -25
  138. package/dist/cjs/clients/third-party/shopify/graphql-order-queries.js +4 -4
  139. package/dist/cjs/clients/third-party/shopify/graphql-product-queries.d.ts +2 -2
  140. package/dist/cjs/clients/third-party/shopify/graphql-product-queries.js +5 -5
  141. package/dist/cjs/clients/third-party/shopify/shopify-graphql-client.d.ts +10 -10
  142. package/dist/cjs/clients/third-party/shopify/shopify-graphql-client.js +161 -161
  143. package/dist/cjs/clients/third-party/shopify-client.d.ts +29 -29
  144. package/dist/cjs/clients/third-party/shopify-client.js +146 -146
  145. package/dist/cjs/constants/index.d.ts +1 -1
  146. package/dist/cjs/constants/index.js +17 -17
  147. package/dist/cjs/constants/sqs.d.ts +20 -20
  148. package/dist/cjs/constants/sqs.js +26 -26
  149. package/dist/cjs/helpers/account-users-helper.d.ts +2 -2
  150. package/dist/cjs/helpers/account-users-helper.js +22 -22
  151. package/dist/cjs/helpers/acuity-helper.d.ts +4 -4
  152. package/dist/cjs/helpers/acuity-helper.js +56 -56
  153. package/dist/cjs/helpers/api-key-auth-helper.d.ts +9 -9
  154. package/dist/cjs/helpers/api-key-auth-helper.js +40 -40
  155. package/dist/cjs/helpers/api-key-authorizer-helper.d.ts +36 -36
  156. package/dist/cjs/helpers/api-key-authorizer-helper.js +87 -87
  157. package/dist/cjs/helpers/identity-cache-helper.d.ts +30 -21
  158. package/dist/cjs/helpers/identity-cache-helper.js +253 -157
  159. package/dist/cjs/helpers/identity-cache-helper.js.map +1 -1
  160. package/dist/cjs/helpers/index.d.ts +10 -10
  161. package/dist/cjs/helpers/index.js +26 -26
  162. package/dist/cjs/helpers/input-validation-helper.d.ts +3 -3
  163. package/dist/cjs/helpers/input-validation-helper.js +22 -22
  164. package/dist/cjs/helpers/logging-helper.d.ts +16 -16
  165. package/dist/cjs/helpers/logging-helper.js +84 -84
  166. package/dist/cjs/helpers/response-helper.d.ts +18 -18
  167. package/dist/cjs/helpers/response-helper.js +43 -43
  168. package/dist/cjs/helpers/shopify-helper.d.ts +9 -9
  169. package/dist/cjs/helpers/shopify-helper.js +26 -26
  170. package/dist/cjs/helpers/sqs-utils.d.ts +6 -6
  171. package/dist/cjs/helpers/sqs-utils.js +14 -14
  172. package/dist/cjs/index.d.ts +7 -7
  173. package/dist/cjs/index.js +23 -23
  174. package/dist/cjs/libs/api-router/index.d.ts +2 -2
  175. package/dist/cjs/libs/api-router/index.js +18 -18
  176. package/dist/cjs/libs/api-router/public-api-router.d.ts +3 -3
  177. package/dist/cjs/libs/api-router/public-api-router.js +36 -36
  178. package/dist/cjs/libs/api-router/route-matcher.d.ts +21 -21
  179. package/dist/cjs/libs/api-router/route-matcher.js +36 -36
  180. package/dist/cjs/libs/click-id-parser.d.ts +23 -23
  181. package/dist/cjs/libs/click-id-parser.js +49 -49
  182. package/dist/cjs/libs/compression.d.ts +2 -2
  183. package/dist/cjs/libs/compression.js +33 -33
  184. package/dist/cjs/libs/contacts.d.ts +7 -7
  185. package/dist/cjs/libs/contacts.js +152 -152
  186. package/dist/cjs/libs/cookie.d.ts +17 -17
  187. package/dist/cjs/libs/cookie.js +76 -76
  188. package/dist/cjs/libs/crypto.d.ts +4 -4
  189. package/dist/cjs/libs/crypto.js +25 -25
  190. package/dist/cjs/libs/csv.d.ts +2 -2
  191. package/dist/cjs/libs/csv.js +35 -35
  192. package/dist/cjs/libs/currency.d.ts +1 -1
  193. package/dist/cjs/libs/currency.js +29 -29
  194. package/dist/cjs/libs/dates.d.ts +12 -12
  195. package/dist/cjs/libs/dates.js +96 -96
  196. package/dist/cjs/libs/domain.d.ts +2 -2
  197. package/dist/cjs/libs/domain.js +38 -38
  198. package/dist/cjs/libs/emails.d.ts +8 -8
  199. package/dist/cjs/libs/emails.js +154 -154
  200. package/dist/cjs/libs/http-error.d.ts +21 -21
  201. package/dist/cjs/libs/http-error.js +63 -63
  202. package/dist/cjs/libs/http-status-codes.d.ts +58 -58
  203. package/dist/cjs/libs/http-status-codes.js +62 -62
  204. package/dist/cjs/libs/index.d.ts +19 -19
  205. package/dist/cjs/libs/index.js +35 -35
  206. package/dist/cjs/libs/numbers.d.ts +1 -1
  207. package/dist/cjs/libs/numbers.js +15 -15
  208. package/dist/cjs/libs/referrer-parser/index.d.ts +2 -2
  209. package/dist/cjs/libs/referrer-parser/index.js +18 -18
  210. package/dist/cjs/libs/referrer-parser/referrer-data.d.ts +9 -9
  211. package/dist/cjs/libs/referrer-parser/referrer-data.js +3307 -3307
  212. package/dist/cjs/libs/referrer-parser/referrer-parser-util.d.ts +20 -20
  213. package/dist/cjs/libs/referrer-parser/referrer-parser-util.js +131 -131
  214. package/dist/cjs/libs/strings.d.ts +3 -3
  215. package/dist/cjs/libs/strings.js +46 -46
  216. package/dist/cjs/libs/traits.d.ts +6 -6
  217. package/dist/cjs/libs/traits.js +65 -65
  218. package/dist/cjs/libs/url.d.ts +1 -1
  219. package/dist/cjs/libs/url.js +13 -13
  220. package/dist/cjs/services/acuity-api-service.d.ts +9 -9
  221. package/dist/cjs/services/acuity-api-service.js +73 -73
  222. package/dist/cjs/services/cache/generic-cached-object.d.ts +5 -5
  223. package/dist/cjs/services/cache/generic-cached-object.js +2 -2
  224. package/dist/cjs/services/cache/index.d.ts +1 -1
  225. package/dist/cjs/services/cache/index.js +17 -17
  226. package/dist/cjs/services/cache/product-cache-service.d.ts +21 -21
  227. package/dist/cjs/services/cache/product-cache-service.js +76 -76
  228. package/dist/cjs/services/currency-exchange-rate-lookup-service.d.ts +11 -12
  229. package/dist/cjs/services/currency-exchange-rate-lookup-service.js +66 -94
  230. package/dist/cjs/services/currency-exchange-rate-lookup-service.js.map +1 -1
  231. package/dist/cjs/services/db/accounts-db-service.d.ts +9 -9
  232. package/dist/cjs/services/db/accounts-db-service.js +33 -33
  233. package/dist/cjs/services/db/api-keys-db-service.d.ts +10 -10
  234. package/dist/cjs/services/db/api-keys-db-service.js +36 -36
  235. package/dist/cjs/services/db/contact-enrichments-db-service.d.ts +15 -15
  236. package/dist/cjs/services/db/contact-enrichments-db-service.js +94 -94
  237. package/dist/cjs/services/db/currency-exchange-rates-db-service.d.ts +21 -21
  238. package/dist/cjs/services/db/currency-exchange-rates-db-service.js +39 -39
  239. package/dist/cjs/services/db/custom-measures-db-service.d.ts +14 -14
  240. package/dist/cjs/services/db/custom-measures-db-service.js +48 -48
  241. package/dist/cjs/services/db/destinations-db-service.d.ts +13 -13
  242. package/dist/cjs/services/db/destinations-db-service.js +74 -74
  243. package/dist/cjs/services/db/identity-cache-db-service.d.ts +28 -28
  244. package/dist/cjs/services/db/identity-cache-db-service.js +320 -320
  245. package/dist/cjs/services/db/identity-cache-dynamodb-service.d.ts +44 -38
  246. package/dist/cjs/services/db/identity-cache-dynamodb-service.js +734 -439
  247. package/dist/cjs/services/db/identity-cache-dynamodb-service.js.map +1 -1
  248. package/dist/cjs/services/db/index.d.ts +17 -19
  249. package/dist/cjs/services/db/index.js +33 -35
  250. package/dist/cjs/services/db/index.js.map +1 -1
  251. package/dist/cjs/services/db/log-events-db-service.d.ts +11 -11
  252. package/dist/cjs/services/db/log-events-db-service.js +181 -181
  253. package/dist/cjs/services/db/pixels-db-service.d.ts +8 -8
  254. package/dist/cjs/services/db/pixels-db-service.js +35 -35
  255. package/dist/cjs/services/db/purchasable-contacts-db-service.d.ts +9 -9
  256. package/dist/cjs/services/db/purchasable-contacts-db-service.js +43 -43
  257. package/dist/cjs/services/db/purchased-contacts/index.d.ts +2 -2
  258. package/dist/cjs/services/db/purchased-contacts/index.js +18 -18
  259. package/dist/cjs/services/db/purchased-contacts/purchased-contacts-db-service.d.ts +18 -18
  260. package/dist/cjs/services/db/purchased-contacts/purchased-contacts-db-service.js +152 -152
  261. package/dist/cjs/services/db/purchased-contacts/types.d.ts +11 -11
  262. package/dist/cjs/services/db/purchased-contacts/types.js +2 -2
  263. package/dist/cjs/services/db/shopify-app-installs-db-service.d.ts +10 -10
  264. package/dist/cjs/services/db/shopify-app-installs-db-service.js +52 -52
  265. package/dist/cjs/services/db/shopify-products-cache-db-service.d.ts +16 -16
  266. package/dist/cjs/services/db/shopify-products-cache-db-service.js +73 -73
  267. package/dist/cjs/services/db/subscriptions-db-service.d.ts +11 -11
  268. package/dist/cjs/services/db/subscriptions-db-service.js +38 -38
  269. package/dist/cjs/services/db/tracking-events-db-service.d.ts +21 -21
  270. package/dist/cjs/services/db/tracking-events-db-service.js +188 -188
  271. package/dist/cjs/services/db/user-accounts-db-service.d.ts +7 -7
  272. package/dist/cjs/services/db/user-accounts-db-service.js +17 -17
  273. package/dist/cjs/services/email-verification/contact-email-verification-service.d.ts +7 -7
  274. package/dist/cjs/services/email-verification/contact-email-verification-service.js +101 -101
  275. package/dist/cjs/services/email-verification/email-verification-service.d.ts +19 -19
  276. package/dist/cjs/services/email-verification/email-verification-service.js +131 -131
  277. package/dist/cjs/services/email-verification/index.d.ts +2 -2
  278. package/dist/cjs/services/email-verification/index.js +18 -18
  279. package/dist/cjs/services/eventbridge-integration-service.d.ts +9 -9
  280. package/dist/cjs/services/eventbridge-integration-service.js +28 -28
  281. package/dist/cjs/services/events/index.d.ts +3 -3
  282. package/dist/cjs/services/events/index.js +19 -19
  283. package/dist/cjs/services/events/log-event-service.d.ts +19 -19
  284. package/dist/cjs/services/events/log-event-service.js +77 -77
  285. package/dist/cjs/services/events/metric-event-service.d.ts +9 -9
  286. package/dist/cjs/services/events/metric-event-service.js +49 -49
  287. package/dist/cjs/services/events/tracking-event-sqs-service.d.ts +8 -8
  288. package/dist/cjs/services/events/tracking-event-sqs-service.js +34 -34
  289. package/dist/cjs/services/generic-cache-service.d.ts +7 -7
  290. package/dist/cjs/services/generic-cache-service.js +33 -33
  291. package/dist/cjs/services/index.d.ts +10 -11
  292. package/dist/cjs/services/index.js +26 -27
  293. package/dist/cjs/services/index.js.map +1 -1
  294. package/dist/cjs/services/ipdata-lookup-service.d.ts +20 -20
  295. package/dist/cjs/services/ipdata-lookup-service.js +112 -112
  296. package/dist/cjs/services/shopify/index.d.ts +2 -2
  297. package/dist/cjs/services/shopify/index.js +18 -18
  298. package/dist/cjs/services/shopify/products/index.d.ts +1 -1
  299. package/dist/cjs/services/shopify/products/index.js +17 -17
  300. package/dist/cjs/services/shopify/products/shopify-products-serviceV2.d.ts +17 -17
  301. package/dist/cjs/services/shopify/products/shopify-products-serviceV2.js +112 -112
  302. package/dist/cjs/services/shopify/shopify-graphql-transformer.d.ts +8 -8
  303. package/dist/cjs/services/shopify/shopify-graphql-transformer.js +141 -141
  304. package/dist/cjs/types/acuity-types.d.ts +74 -74
  305. package/dist/cjs/types/acuity-types.js +2 -2
  306. package/dist/cjs/types/api-response.d.ts +6 -6
  307. package/dist/cjs/types/api-response.js +2 -2
  308. package/dist/cjs/types/index.d.ts +4 -4
  309. package/dist/cjs/types/index.js +33 -33
  310. package/dist/cjs/types/internal-events/event-detail-types.d.ts +20 -20
  311. package/dist/cjs/types/internal-events/event-detail-types.js +27 -27
  312. package/dist/cjs/types/internal-events/index.d.ts +1 -1
  313. package/dist/cjs/types/internal-events/index.js +17 -17
  314. package/dist/cjs/types/shopify-graphql-types/admin.generated.d.ts +123 -123
  315. package/dist/cjs/types/shopify-graphql-types/admin.generated.js +2 -2
  316. package/dist/cjs/types/shopify-graphql-types/admin.types.d.ts +26289 -26289
  317. package/dist/cjs/types/shopify-graphql-types/admin.types.js +5311 -5311
  318. package/dist/cjs/types/shopify-graphql-types/index.d.ts +2 -2
  319. package/dist/cjs/types/shopify-graphql-types/index.js +18 -18
  320. package/dist/cjs/types/shopify-rest-types.d.ts +767 -767
  321. package/dist/cjs/types/shopify-rest-types.js +2 -2
  322. package/dist/cjs/utils/compression.d.ts +36 -36
  323. package/dist/cjs/utils/compression.js +198 -198
  324. package/dist/cjs/utils/custom-measure-formula-utils.d.ts +6 -6
  325. package/dist/cjs/utils/custom-measure-formula-utils.js +209 -209
  326. package/dist/cjs/utils/index.d.ts +4 -4
  327. package/dist/cjs/utils/index.js +20 -20
  328. package/dist/cjs/utils/retry-envelope.d.ts +12 -12
  329. package/dist/cjs/utils/retry-envelope.js +28 -28
  330. package/dist/cjs/utils/size.d.ts +2 -2
  331. package/dist/cjs/utils/size.js +49 -49
  332. package/dist/esm/__tests__/clients/acuity-client.spec.d.ts +1 -1
  333. package/dist/esm/__tests__/clients/acuity-client.spec.js +41 -41
  334. package/dist/esm/__tests__/clients/cross-platform-compression.spec.d.ts +1 -1
  335. package/dist/esm/__tests__/clients/cross-platform-compression.spec.js +329 -329
  336. package/dist/esm/__tests__/clients/dynamodb-client.spec.d.ts +1 -1
  337. package/dist/esm/__tests__/clients/dynamodb-client.spec.js +192 -192
  338. package/dist/esm/__tests__/clients/sqs-bundled-client.spec.d.ts +1 -1
  339. package/dist/esm/__tests__/clients/sqs-bundled-client.spec.js +906 -906
  340. package/dist/esm/__tests__/clients/sqs-bundling-contracts.spec.d.ts +1 -1
  341. package/dist/esm/__tests__/clients/sqs-bundling-contracts.spec.js +538 -538
  342. package/dist/esm/__tests__/clients/sqs-client.spec.d.ts +1 -1
  343. package/dist/esm/__tests__/clients/sqs-client.spec.js +189 -189
  344. package/dist/esm/__tests__/clients/sqs-unbundle.spec.d.ts +1 -1
  345. package/dist/esm/__tests__/clients/sqs-unbundle.spec.js +1355 -1355
  346. package/dist/esm/__tests__/db/contact-enrichments-db-service.spec.d.ts +1 -1
  347. package/dist/esm/__tests__/db/contact-enrichments-db-service.spec.js +66 -66
  348. package/dist/esm/__tests__/db/destinations-db-service.spec.d.ts +1 -1
  349. package/dist/esm/__tests__/db/destinations-db-service.spec.js +123 -123
  350. package/dist/esm/__tests__/db/shared-read-db-services.spec.d.ts +1 -1
  351. package/dist/esm/__tests__/db/shared-read-db-services.spec.js +87 -87
  352. package/dist/esm/__tests__/db/shopify-app-installs-db-service.spec.d.ts +1 -1
  353. package/dist/esm/__tests__/db/shopify-app-installs-db-service.spec.js +102 -102
  354. package/dist/esm/__tests__/db/subscriptions-db-service.spec.d.ts +1 -1
  355. package/dist/esm/__tests__/db/subscriptions-db-service.spec.js +93 -93
  356. package/dist/esm/__tests__/db/user-accounts-db-service.spec.d.ts +1 -1
  357. package/dist/esm/__tests__/db/user-accounts-db-service.spec.js +74 -74
  358. package/dist/esm/__tests__/helpers/account-users-helper.spec.d.ts +1 -1
  359. package/dist/esm/__tests__/helpers/account-users-helper.spec.js +218 -218
  360. package/dist/esm/__tests__/helpers/acuity-helper.spec.d.ts +1 -1
  361. package/dist/esm/__tests__/helpers/acuity-helper.spec.js +67 -67
  362. package/dist/esm/__tests__/helpers/api-key-auth-helper.spec.d.ts +1 -1
  363. package/dist/esm/__tests__/helpers/api-key-auth-helper.spec.js +80 -80
  364. package/dist/esm/__tests__/identity-cache/identity-cache-db-service.spec.d.ts +1 -1
  365. package/dist/esm/__tests__/identity-cache/identity-cache-db-service.spec.js +674 -672
  366. package/dist/esm/__tests__/identity-cache/identity-cache-db-service.spec.js.map +1 -1
  367. package/dist/esm/__tests__/identity-cache/identity-cache-dynamodb-service.spec.d.ts +1 -1
  368. package/dist/esm/__tests__/identity-cache/identity-cache-dynamodb-service.spec.js +1138 -1138
  369. package/dist/{cjs/__tests__/services/cost/cost-currency-service.spec.d.ts → esm/__tests__/identity-cache/identity-cache-tier-routing.spec.d.ts} +1 -1
  370. package/dist/esm/__tests__/identity-cache/identity-cache-tier-routing.spec.js +850 -0
  371. package/dist/esm/__tests__/identity-cache/identity-cache-tier-routing.spec.js.map +1 -0
  372. package/dist/esm/__tests__/identity-cache/trait-merging-and-staleness.spec.d.ts +1 -1
  373. package/dist/esm/__tests__/identity-cache/trait-merging-and-staleness.spec.js +1058 -586
  374. package/dist/esm/__tests__/identity-cache/trait-merging-and-staleness.spec.js.map +1 -1
  375. package/dist/{cjs/__tests__/services/cost/cost-calculation-types.spec.d.ts → esm/__tests__/identity-cache/volatile-traits-optimization.spec.d.ts} +1 -1
  376. package/dist/esm/__tests__/identity-cache/volatile-traits-optimization.spec.js +817 -0
  377. package/dist/esm/__tests__/identity-cache/volatile-traits-optimization.spec.js.map +1 -0
  378. package/dist/esm/__tests__/integration/sqs-bundling-roundtrip.spec.d.ts +1 -1
  379. package/dist/esm/__tests__/integration/sqs-bundling-roundtrip.spec.js +582 -582
  380. package/dist/esm/__tests__/libs/compress-decompress.spec.d.ts +1 -1
  381. package/dist/esm/__tests__/libs/compress-decompress.spec.js +14 -14
  382. package/dist/esm/__tests__/libs/contacts.spec.d.ts +1 -1
  383. package/dist/esm/__tests__/libs/contacts.spec.js +292 -292
  384. package/dist/esm/__tests__/libs/currency.spec.d.ts +1 -1
  385. package/dist/esm/__tests__/libs/currency.spec.js +218 -218
  386. package/dist/esm/__tests__/libs/dates.spec.d.ts +1 -1
  387. package/dist/esm/__tests__/libs/dates.spec.js +128 -128
  388. package/dist/esm/__tests__/libs/domain.spec.d.ts +1 -1
  389. package/dist/esm/__tests__/libs/domain.spec.js +105 -105
  390. package/dist/esm/__tests__/libs/numbers.spec.d.ts +1 -1
  391. package/dist/esm/__tests__/libs/numbers.spec.js +259 -259
  392. package/dist/esm/__tests__/s3-client/s3-client.spec.d.ts +1 -1
  393. package/dist/esm/__tests__/s3-client/s3-client.spec.js +31 -31
  394. package/dist/esm/__tests__/services/acuity-api-service.spec.d.ts +1 -1
  395. package/dist/esm/__tests__/services/acuity-api-service.spec.js +69 -69
  396. package/dist/esm/__tests__/services/email-verification/contact-email-verification-service.spec.d.ts +1 -1
  397. package/dist/esm/__tests__/services/email-verification/contact-email-verification-service.spec.js +91 -91
  398. package/dist/esm/__tests__/services/email-verification/email-verification-service.spec.d.ts +1 -1
  399. package/dist/esm/__tests__/services/email-verification/email-verification-service.spec.js +55 -55
  400. package/dist/esm/__tests__/shopify/shopify-graphql-transformer.spec.d.ts +1 -1
  401. package/dist/esm/__tests__/shopify/shopify-graphql-transformer.spec.js +33 -33
  402. package/dist/esm/__tests__/unit/libs/api-router/public-api-router.spec.d.ts +1 -1
  403. package/dist/esm/__tests__/unit/libs/api-router/public-api-router.spec.js +156 -156
  404. package/dist/esm/__tests__/unit/libs/api-router/route-matcher.spec.d.ts +1 -1
  405. package/dist/esm/__tests__/unit/libs/api-router/route-matcher.spec.js +67 -67
  406. package/dist/esm/__tests__/utils/custom-measure-formula-utils.spec.d.ts +1 -1
  407. package/dist/esm/__tests__/utils/custom-measure-formula-utils.spec.js +137 -137
  408. package/dist/esm/clients/generic/cognito-client.d.ts +23 -23
  409. package/dist/esm/clients/generic/cognito-client.js +204 -204
  410. package/dist/esm/clients/generic/dynamodb-client.d.ts +20 -20
  411. package/dist/esm/clients/generic/dynamodb-client.js +231 -231
  412. package/dist/esm/clients/generic/eventbridge-client.d.ts +14 -14
  413. package/dist/esm/clients/generic/eventbridge-client.js +47 -47
  414. package/dist/esm/clients/generic/http-client.d.ts +14 -14
  415. package/dist/esm/clients/generic/http-client.js +53 -53
  416. package/dist/esm/clients/generic/index.d.ts +13 -13
  417. package/dist/esm/clients/generic/index.js +13 -13
  418. package/dist/esm/clients/generic/lambda-invoke-client.d.ts +10 -10
  419. package/dist/esm/clients/generic/lambda-invoke-client.js +35 -35
  420. package/dist/esm/clients/generic/location-client.d.ts +8 -8
  421. package/dist/esm/clients/generic/location-client.js +27 -27
  422. package/dist/esm/clients/generic/redis-client.d.ts +33 -33
  423. package/dist/esm/clients/generic/redis-client.js +184 -184
  424. package/dist/esm/clients/generic/s3-client.d.ts +23 -23
  425. package/dist/esm/clients/generic/s3-client.js +209 -209
  426. package/dist/esm/clients/generic/singlestore-db-client.d.ts +14 -14
  427. package/dist/esm/clients/generic/singlestore-db-client.js +40 -40
  428. package/dist/esm/clients/generic/sqs-bundled-client.d.ts +15 -15
  429. package/dist/esm/clients/generic/sqs-bundled-client.js +307 -307
  430. package/dist/esm/clients/generic/sqs-bundled-client.types.d.ts +53 -53
  431. package/dist/esm/clients/generic/sqs-bundled-client.types.js +14 -14
  432. package/dist/esm/clients/generic/sqs-client.d.ts +53 -53
  433. package/dist/esm/clients/generic/sqs-client.js +281 -281
  434. package/dist/esm/clients/generic/sqs-unbundle.d.ts +32 -32
  435. package/dist/esm/clients/generic/sqs-unbundle.js +137 -137
  436. package/dist/esm/clients/index.d.ts +3 -3
  437. package/dist/esm/clients/index.js +3 -3
  438. package/dist/esm/clients/internal-api/accounts-client.d.ts +91 -91
  439. package/dist/esm/clients/internal-api/accounts-client.js +125 -125
  440. package/dist/esm/clients/internal-api/cache-lambda-client.d.ts +26 -26
  441. package/dist/esm/clients/internal-api/cache-lambda-client.js +85 -85
  442. package/dist/esm/clients/internal-api/db-management-client.d.ts +18 -18
  443. package/dist/esm/clients/internal-api/db-management-client.js +32 -32
  444. package/dist/esm/clients/internal-api/destinations-client.d.ts +34 -34
  445. package/dist/esm/clients/internal-api/destinations-client.js +75 -75
  446. package/dist/esm/clients/internal-api/event-collector-client.d.ts +20 -20
  447. package/dist/esm/clients/internal-api/event-collector-client.js +32 -32
  448. package/dist/esm/clients/internal-api/identity-client.d.ts +31 -31
  449. package/dist/esm/clients/internal-api/identity-client.js +87 -87
  450. package/dist/esm/clients/internal-api/index.d.ts +9 -9
  451. package/dist/esm/clients/internal-api/index.js +9 -9
  452. package/dist/esm/clients/internal-api/shopify-app-install-client.d.ts +37 -37
  453. package/dist/esm/clients/internal-api/shopify-app-install-client.js +77 -77
  454. package/dist/esm/clients/internal-api/subscriptions-client.d.ts +26 -26
  455. package/dist/esm/clients/internal-api/subscriptions-client.js +73 -73
  456. package/dist/esm/clients/internal-api/users-auth-client.d.ts +35 -35
  457. package/dist/esm/clients/internal-api/users-auth-client.js +106 -106
  458. package/dist/esm/clients/third-party/acuity-client.d.ts +10 -10
  459. package/dist/esm/clients/third-party/acuity-client.js +36 -36
  460. package/dist/esm/clients/third-party/emailable-client.d.ts +7 -7
  461. package/dist/esm/clients/third-party/emailable-client.js +21 -21
  462. package/dist/esm/clients/third-party/exchange-rate-api-client.d.ts +17 -17
  463. package/dist/esm/clients/third-party/exchange-rate-api-client.js +15 -15
  464. package/dist/esm/clients/third-party/index.d.ts +5 -5
  465. package/dist/esm/clients/third-party/index.js +5 -5
  466. package/dist/esm/clients/third-party/loops-client.d.ts +10 -10
  467. package/dist/esm/clients/third-party/loops-client.js +26 -26
  468. package/dist/esm/clients/third-party/shopify/graphql-order-queries.d.ts +25 -25
  469. package/dist/esm/clients/third-party/shopify/graphql-order-queries.js +1 -1
  470. package/dist/esm/clients/third-party/shopify/graphql-product-queries.d.ts +2 -2
  471. package/dist/esm/clients/third-party/shopify/graphql-product-queries.js +2 -2
  472. package/dist/esm/clients/third-party/shopify/shopify-graphql-client.d.ts +10 -10
  473. package/dist/esm/clients/third-party/shopify/shopify-graphql-client.js +157 -157
  474. package/dist/esm/clients/third-party/shopify-client.d.ts +29 -29
  475. package/dist/esm/clients/third-party/shopify-client.js +142 -142
  476. package/dist/esm/constants/index.d.ts +1 -1
  477. package/dist/esm/constants/index.js +1 -1
  478. package/dist/esm/constants/sqs.d.ts +20 -20
  479. package/dist/esm/constants/sqs.js +22 -22
  480. package/dist/esm/helpers/account-users-helper.d.ts +2 -2
  481. package/dist/esm/helpers/account-users-helper.js +18 -18
  482. package/dist/esm/helpers/acuity-helper.d.ts +4 -4
  483. package/dist/esm/helpers/acuity-helper.js +51 -51
  484. package/dist/esm/helpers/api-key-auth-helper.d.ts +9 -9
  485. package/dist/esm/helpers/api-key-auth-helper.js +35 -35
  486. package/dist/esm/helpers/api-key-authorizer-helper.d.ts +36 -36
  487. package/dist/esm/helpers/api-key-authorizer-helper.js +83 -83
  488. package/dist/esm/helpers/identity-cache-helper.d.ts +30 -21
  489. package/dist/esm/helpers/identity-cache-helper.js +248 -152
  490. package/dist/esm/helpers/identity-cache-helper.js.map +1 -1
  491. package/dist/esm/helpers/index.d.ts +10 -10
  492. package/dist/esm/helpers/index.js +10 -10
  493. package/dist/esm/helpers/input-validation-helper.d.ts +3 -3
  494. package/dist/esm/helpers/input-validation-helper.js +18 -18
  495. package/dist/esm/helpers/logging-helper.d.ts +16 -16
  496. package/dist/esm/helpers/logging-helper.js +56 -56
  497. package/dist/esm/helpers/response-helper.d.ts +18 -18
  498. package/dist/esm/helpers/response-helper.js +37 -37
  499. package/dist/esm/helpers/shopify-helper.d.ts +9 -9
  500. package/dist/esm/helpers/shopify-helper.js +21 -21
  501. package/dist/esm/helpers/sqs-utils.d.ts +6 -6
  502. package/dist/esm/helpers/sqs-utils.js +9 -9
  503. package/dist/esm/index.d.ts +7 -7
  504. package/dist/esm/index.js +7 -7
  505. package/dist/esm/libs/api-router/index.d.ts +2 -2
  506. package/dist/esm/libs/api-router/index.js +2 -2
  507. package/dist/esm/libs/api-router/public-api-router.d.ts +3 -3
  508. package/dist/esm/libs/api-router/public-api-router.js +32 -32
  509. package/dist/esm/libs/api-router/route-matcher.d.ts +21 -21
  510. package/dist/esm/libs/api-router/route-matcher.js +30 -30
  511. package/dist/esm/libs/click-id-parser.d.ts +23 -23
  512. package/dist/esm/libs/click-id-parser.js +45 -45
  513. package/dist/esm/libs/compression.d.ts +2 -2
  514. package/dist/esm/libs/compression.js +25 -25
  515. package/dist/esm/libs/contacts.d.ts +7 -7
  516. package/dist/esm/libs/contacts.js +143 -143
  517. package/dist/esm/libs/cookie.d.ts +17 -17
  518. package/dist/esm/libs/cookie.js +70 -70
  519. package/dist/esm/libs/crypto.d.ts +4 -4
  520. package/dist/esm/libs/crypto.js +15 -15
  521. package/dist/esm/libs/csv.d.ts +2 -2
  522. package/dist/esm/libs/csv.js +30 -30
  523. package/dist/esm/libs/currency.d.ts +1 -1
  524. package/dist/esm/libs/currency.js +22 -22
  525. package/dist/esm/libs/dates.d.ts +12 -12
  526. package/dist/esm/libs/dates.js +83 -83
  527. package/dist/esm/libs/domain.d.ts +2 -2
  528. package/dist/esm/libs/domain.js +33 -33
  529. package/dist/esm/libs/emails.d.ts +8 -8
  530. package/dist/esm/libs/emails.js +146 -146
  531. package/dist/esm/libs/http-error.d.ts +21 -21
  532. package/dist/esm/libs/http-error.js +59 -59
  533. package/dist/esm/libs/http-status-codes.d.ts +58 -58
  534. package/dist/esm/libs/http-status-codes.js +59 -59
  535. package/dist/esm/libs/index.d.ts +19 -19
  536. package/dist/esm/libs/index.js +19 -19
  537. package/dist/esm/libs/numbers.d.ts +1 -1
  538. package/dist/esm/libs/numbers.js +11 -11
  539. package/dist/esm/libs/referrer-parser/index.d.ts +2 -2
  540. package/dist/esm/libs/referrer-parser/index.js +2 -2
  541. package/dist/esm/libs/referrer-parser/referrer-data.d.ts +9 -9
  542. package/dist/esm/libs/referrer-parser/referrer-data.js +3304 -3304
  543. package/dist/esm/libs/referrer-parser/referrer-parser-util.d.ts +20 -20
  544. package/dist/esm/libs/referrer-parser/referrer-parser-util.js +124 -124
  545. package/dist/esm/libs/strings.d.ts +3 -3
  546. package/dist/esm/libs/strings.js +40 -40
  547. package/dist/esm/libs/traits.d.ts +6 -6
  548. package/dist/esm/libs/traits.js +54 -54
  549. package/dist/esm/libs/url.d.ts +1 -1
  550. package/dist/esm/libs/url.js +9 -9
  551. package/dist/esm/services/acuity-api-service.d.ts +9 -9
  552. package/dist/esm/services/acuity-api-service.js +69 -69
  553. package/dist/esm/services/cache/generic-cached-object.d.ts +5 -5
  554. package/dist/esm/services/cache/generic-cached-object.js +1 -1
  555. package/dist/esm/services/cache/index.d.ts +1 -1
  556. package/dist/esm/services/cache/index.js +1 -1
  557. package/dist/esm/services/cache/product-cache-service.d.ts +21 -21
  558. package/dist/esm/services/cache/product-cache-service.js +68 -68
  559. package/dist/esm/services/currency-exchange-rate-lookup-service.d.ts +11 -12
  560. package/dist/esm/services/currency-exchange-rate-lookup-service.js +62 -90
  561. package/dist/esm/services/currency-exchange-rate-lookup-service.js.map +1 -1
  562. package/dist/esm/services/db/accounts-db-service.d.ts +9 -9
  563. package/dist/esm/services/db/accounts-db-service.js +29 -29
  564. package/dist/esm/services/db/api-keys-db-service.d.ts +10 -10
  565. package/dist/esm/services/db/api-keys-db-service.js +32 -32
  566. package/dist/esm/services/db/contact-enrichments-db-service.d.ts +15 -15
  567. package/dist/esm/services/db/contact-enrichments-db-service.js +90 -90
  568. package/dist/esm/services/db/currency-exchange-rates-db-service.d.ts +21 -21
  569. package/dist/esm/services/db/currency-exchange-rates-db-service.js +35 -35
  570. package/dist/esm/services/db/custom-measures-db-service.d.ts +14 -14
  571. package/dist/esm/services/db/custom-measures-db-service.js +44 -44
  572. package/dist/esm/services/db/destinations-db-service.d.ts +13 -13
  573. package/dist/esm/services/db/destinations-db-service.js +70 -70
  574. package/dist/esm/services/db/identity-cache-db-service.d.ts +28 -28
  575. package/dist/esm/services/db/identity-cache-db-service.js +313 -313
  576. package/dist/esm/services/db/identity-cache-dynamodb-service.d.ts +44 -38
  577. package/dist/esm/services/db/identity-cache-dynamodb-service.js +727 -432
  578. package/dist/esm/services/db/identity-cache-dynamodb-service.js.map +1 -1
  579. package/dist/esm/services/db/index.d.ts +17 -19
  580. package/dist/esm/services/db/index.js +17 -19
  581. package/dist/esm/services/db/index.js.map +1 -1
  582. package/dist/esm/services/db/log-events-db-service.d.ts +11 -11
  583. package/dist/esm/services/db/log-events-db-service.js +177 -177
  584. package/dist/esm/services/db/pixels-db-service.d.ts +8 -8
  585. package/dist/esm/services/db/pixels-db-service.js +31 -31
  586. package/dist/esm/services/db/purchasable-contacts-db-service.d.ts +9 -9
  587. package/dist/esm/services/db/purchasable-contacts-db-service.js +39 -39
  588. package/dist/esm/services/db/purchased-contacts/index.d.ts +2 -2
  589. package/dist/esm/services/db/purchased-contacts/index.js +2 -2
  590. package/dist/esm/services/db/purchased-contacts/purchased-contacts-db-service.d.ts +18 -18
  591. package/dist/esm/services/db/purchased-contacts/purchased-contacts-db-service.js +148 -148
  592. package/dist/esm/services/db/purchased-contacts/types.d.ts +11 -11
  593. package/dist/esm/services/db/purchased-contacts/types.js +1 -1
  594. package/dist/esm/services/db/shopify-app-installs-db-service.d.ts +10 -10
  595. package/dist/esm/services/db/shopify-app-installs-db-service.js +48 -48
  596. package/dist/esm/services/db/shopify-products-cache-db-service.d.ts +16 -16
  597. package/dist/esm/services/db/shopify-products-cache-db-service.js +66 -66
  598. package/dist/esm/services/db/subscriptions-db-service.d.ts +11 -11
  599. package/dist/esm/services/db/subscriptions-db-service.js +34 -34
  600. package/dist/esm/services/db/tracking-events-db-service.d.ts +21 -21
  601. package/dist/esm/services/db/tracking-events-db-service.js +184 -184
  602. package/dist/esm/services/db/user-accounts-db-service.d.ts +7 -7
  603. package/dist/esm/services/db/user-accounts-db-service.js +13 -13
  604. package/dist/esm/services/email-verification/contact-email-verification-service.d.ts +7 -7
  605. package/dist/esm/services/email-verification/contact-email-verification-service.js +97 -97
  606. package/dist/esm/services/email-verification/email-verification-service.d.ts +19 -19
  607. package/dist/esm/services/email-verification/email-verification-service.js +127 -127
  608. package/dist/esm/services/email-verification/index.d.ts +2 -2
  609. package/dist/esm/services/email-verification/index.js +2 -2
  610. package/dist/esm/services/eventbridge-integration-service.d.ts +9 -9
  611. package/dist/esm/services/eventbridge-integration-service.js +24 -24
  612. package/dist/esm/services/events/index.d.ts +3 -3
  613. package/dist/esm/services/events/index.js +3 -3
  614. package/dist/esm/services/events/log-event-service.d.ts +19 -19
  615. package/dist/esm/services/events/log-event-service.js +73 -73
  616. package/dist/esm/services/events/metric-event-service.d.ts +9 -9
  617. package/dist/esm/services/events/metric-event-service.js +45 -45
  618. package/dist/esm/services/events/tracking-event-sqs-service.d.ts +8 -8
  619. package/dist/esm/services/events/tracking-event-sqs-service.js +30 -30
  620. package/dist/esm/services/generic-cache-service.d.ts +7 -7
  621. package/dist/esm/services/generic-cache-service.js +29 -29
  622. package/dist/esm/services/index.d.ts +10 -11
  623. package/dist/esm/services/index.js +10 -11
  624. package/dist/esm/services/index.js.map +1 -1
  625. package/dist/esm/services/ipdata-lookup-service.d.ts +20 -20
  626. package/dist/esm/services/ipdata-lookup-service.js +108 -108
  627. package/dist/esm/services/shopify/index.d.ts +2 -2
  628. package/dist/esm/services/shopify/index.js +2 -2
  629. package/dist/esm/services/shopify/products/index.d.ts +1 -1
  630. package/dist/esm/services/shopify/products/index.js +1 -1
  631. package/dist/esm/services/shopify/products/shopify-products-serviceV2.d.ts +17 -17
  632. package/dist/esm/services/shopify/products/shopify-products-serviceV2.js +108 -108
  633. package/dist/esm/services/shopify/shopify-graphql-transformer.d.ts +8 -8
  634. package/dist/esm/services/shopify/shopify-graphql-transformer.js +138 -138
  635. package/dist/esm/types/acuity-types.d.ts +74 -74
  636. package/dist/esm/types/acuity-types.js +1 -1
  637. package/dist/esm/types/api-response.d.ts +6 -6
  638. package/dist/esm/types/api-response.js +1 -1
  639. package/dist/esm/types/index.d.ts +4 -4
  640. package/dist/esm/types/index.js +4 -4
  641. package/dist/esm/types/internal-events/event-detail-types.d.ts +20 -20
  642. package/dist/esm/types/internal-events/event-detail-types.js +24 -24
  643. package/dist/esm/types/internal-events/index.d.ts +1 -1
  644. package/dist/esm/types/internal-events/index.js +1 -1
  645. package/dist/esm/types/shopify-graphql-types/admin.generated.d.ts +123 -123
  646. package/dist/esm/types/shopify-graphql-types/admin.generated.js +1 -1
  647. package/dist/esm/types/shopify-graphql-types/admin.types.d.ts +26289 -26289
  648. package/dist/esm/types/shopify-graphql-types/admin.types.js +5299 -5299
  649. package/dist/esm/types/shopify-graphql-types/index.d.ts +2 -2
  650. package/dist/esm/types/shopify-graphql-types/index.js +2 -2
  651. package/dist/esm/types/shopify-rest-types.d.ts +767 -767
  652. package/dist/esm/types/shopify-rest-types.js +1 -1
  653. package/dist/esm/utils/compression.d.ts +36 -36
  654. package/dist/esm/utils/compression.js +187 -187
  655. package/dist/esm/utils/custom-measure-formula-utils.d.ts +6 -6
  656. package/dist/esm/utils/custom-measure-formula-utils.js +201 -201
  657. package/dist/esm/utils/index.d.ts +4 -4
  658. package/dist/esm/utils/index.js +4 -4
  659. package/dist/esm/utils/retry-envelope.d.ts +12 -12
  660. package/dist/esm/utils/retry-envelope.js +22 -22
  661. package/dist/esm/utils/size.d.ts +2 -2
  662. package/dist/esm/utils/size.js +44 -44
  663. package/package.json +134 -134
  664. package/dist/cjs/__tests__/db/products-db-service.spec.js +0 -90
  665. package/dist/cjs/__tests__/db/products-db-service.spec.js.map +0 -1
  666. package/dist/cjs/__tests__/services/cost/cost-calculation-types.spec.js +0 -24
  667. package/dist/cjs/__tests__/services/cost/cost-calculation-types.spec.js.map +0 -1
  668. package/dist/cjs/__tests__/services/cost/cost-calculator-service.spec.js +0 -3320
  669. package/dist/cjs/__tests__/services/cost/cost-calculator-service.spec.js.map +0 -1
  670. package/dist/cjs/__tests__/services/cost/cost-currency-service.spec.js +0 -115
  671. package/dist/cjs/__tests__/services/cost/cost-currency-service.spec.js.map +0 -1
  672. package/dist/cjs/__tests__/services/cost/cost-filter-service.spec.d.ts +0 -1
  673. package/dist/cjs/__tests__/services/cost/cost-filter-service.spec.js +0 -469
  674. package/dist/cjs/__tests__/services/cost/cost-filter-service.spec.js.map +0 -1
  675. package/dist/cjs/__tests__/services/cost/order-cost/order-cost-resolution-service.spec.d.ts +0 -1
  676. package/dist/cjs/__tests__/services/cost/order-cost/order-cost-resolution-service.spec.js +0 -207
  677. package/dist/cjs/__tests__/services/cost/order-cost/order-cost-resolution-service.spec.js.map +0 -1
  678. package/dist/cjs/__tests__/services/currency-exchange-rate-lookup-service.spec.d.ts +0 -1
  679. package/dist/cjs/__tests__/services/currency-exchange-rate-lookup-service.spec.js +0 -35
  680. package/dist/cjs/__tests__/services/currency-exchange-rate-lookup-service.spec.js.map +0 -1
  681. package/dist/cjs/services/cost/cost-calculation-types.d.ts +0 -69
  682. package/dist/cjs/services/cost/cost-calculation-types.js +0 -20
  683. package/dist/cjs/services/cost/cost-calculation-types.js.map +0 -1
  684. package/dist/cjs/services/cost/cost-calculator-service.d.ts +0 -24
  685. package/dist/cjs/services/cost/cost-calculator-service.js +0 -457
  686. package/dist/cjs/services/cost/cost-calculator-service.js.map +0 -1
  687. package/dist/cjs/services/cost/cost-currency-service.d.ts +0 -6
  688. package/dist/cjs/services/cost/cost-currency-service.js +0 -88
  689. package/dist/cjs/services/cost/cost-currency-service.js.map +0 -1
  690. package/dist/cjs/services/cost/cost-filter-service.d.ts +0 -10
  691. package/dist/cjs/services/cost/cost-filter-service.js +0 -122
  692. package/dist/cjs/services/cost/cost-filter-service.js.map +0 -1
  693. package/dist/cjs/services/cost/index.d.ts +0 -5
  694. package/dist/cjs/services/cost/index.js +0 -22
  695. package/dist/cjs/services/cost/index.js.map +0 -1
  696. package/dist/cjs/services/cost/order-cost/index.d.ts +0 -2
  697. package/dist/cjs/services/cost/order-cost/index.js +0 -19
  698. package/dist/cjs/services/cost/order-cost/index.js.map +0 -1
  699. package/dist/cjs/services/cost/order-cost/order-cost-resolution-service.d.ts +0 -23
  700. package/dist/cjs/services/cost/order-cost/order-cost-resolution-service.js +0 -362
  701. package/dist/cjs/services/cost/order-cost/order-cost-resolution-service.js.map +0 -1
  702. package/dist/cjs/services/cost/order-cost/order-cost-resolution-types.d.ts +0 -37
  703. package/dist/cjs/services/cost/order-cost/order-cost-resolution-types.js +0 -3
  704. package/dist/cjs/services/cost/order-cost/order-cost-resolution-types.js.map +0 -1
  705. package/dist/cjs/services/db/products-db-service-types.d.ts +0 -10
  706. package/dist/cjs/services/db/products-db-service-types.js +0 -3
  707. package/dist/cjs/services/db/products-db-service-types.js.map +0 -1
  708. package/dist/cjs/services/db/products-db-service.d.ts +0 -19
  709. package/dist/cjs/services/db/products-db-service.js +0 -282
  710. package/dist/cjs/services/db/products-db-service.js.map +0 -1
  711. package/dist/esm/__tests__/db/products-db-service.spec.d.ts +0 -1
  712. package/dist/esm/__tests__/db/products-db-service.spec.js +0 -88
  713. package/dist/esm/__tests__/db/products-db-service.spec.js.map +0 -1
  714. package/dist/esm/__tests__/services/cost/cost-calculation-types.spec.d.ts +0 -1
  715. package/dist/esm/__tests__/services/cost/cost-calculation-types.spec.js +0 -22
  716. package/dist/esm/__tests__/services/cost/cost-calculation-types.spec.js.map +0 -1
  717. package/dist/esm/__tests__/services/cost/cost-calculator-service.spec.d.ts +0 -1
  718. package/dist/esm/__tests__/services/cost/cost-calculator-service.spec.js +0 -3318
  719. package/dist/esm/__tests__/services/cost/cost-calculator-service.spec.js.map +0 -1
  720. package/dist/esm/__tests__/services/cost/cost-currency-service.spec.d.ts +0 -1
  721. package/dist/esm/__tests__/services/cost/cost-currency-service.spec.js +0 -113
  722. package/dist/esm/__tests__/services/cost/cost-currency-service.spec.js.map +0 -1
  723. package/dist/esm/__tests__/services/cost/cost-filter-service.spec.d.ts +0 -1
  724. package/dist/esm/__tests__/services/cost/cost-filter-service.spec.js +0 -467
  725. package/dist/esm/__tests__/services/cost/cost-filter-service.spec.js.map +0 -1
  726. package/dist/esm/__tests__/services/cost/order-cost/order-cost-resolution-service.spec.d.ts +0 -1
  727. package/dist/esm/__tests__/services/cost/order-cost/order-cost-resolution-service.spec.js +0 -205
  728. package/dist/esm/__tests__/services/cost/order-cost/order-cost-resolution-service.spec.js.map +0 -1
  729. package/dist/esm/__tests__/services/currency-exchange-rate-lookup-service.spec.d.ts +0 -1
  730. package/dist/esm/__tests__/services/currency-exchange-rate-lookup-service.spec.js +0 -33
  731. package/dist/esm/__tests__/services/currency-exchange-rate-lookup-service.spec.js.map +0 -1
  732. package/dist/esm/services/cost/cost-calculation-types.d.ts +0 -69
  733. package/dist/esm/services/cost/cost-calculation-types.js +0 -16
  734. package/dist/esm/services/cost/cost-calculation-types.js.map +0 -1
  735. package/dist/esm/services/cost/cost-calculator-service.d.ts +0 -24
  736. package/dist/esm/services/cost/cost-calculator-service.js +0 -451
  737. package/dist/esm/services/cost/cost-calculator-service.js.map +0 -1
  738. package/dist/esm/services/cost/cost-currency-service.d.ts +0 -6
  739. package/dist/esm/services/cost/cost-currency-service.js +0 -85
  740. package/dist/esm/services/cost/cost-currency-service.js.map +0 -1
  741. package/dist/esm/services/cost/cost-filter-service.d.ts +0 -10
  742. package/dist/esm/services/cost/cost-filter-service.js +0 -119
  743. package/dist/esm/services/cost/cost-filter-service.js.map +0 -1
  744. package/dist/esm/services/cost/index.d.ts +0 -5
  745. package/dist/esm/services/cost/index.js +0 -6
  746. package/dist/esm/services/cost/index.js.map +0 -1
  747. package/dist/esm/services/cost/order-cost/index.d.ts +0 -2
  748. package/dist/esm/services/cost/order-cost/index.js +0 -3
  749. package/dist/esm/services/cost/order-cost/index.js.map +0 -1
  750. package/dist/esm/services/cost/order-cost/order-cost-resolution-service.d.ts +0 -23
  751. package/dist/esm/services/cost/order-cost/order-cost-resolution-service.js +0 -356
  752. package/dist/esm/services/cost/order-cost/order-cost-resolution-service.js.map +0 -1
  753. package/dist/esm/services/cost/order-cost/order-cost-resolution-types.d.ts +0 -37
  754. package/dist/esm/services/cost/order-cost/order-cost-resolution-types.js +0 -2
  755. package/dist/esm/services/cost/order-cost/order-cost-resolution-types.js.map +0 -1
  756. package/dist/esm/services/db/products-db-service-types.d.ts +0 -10
  757. package/dist/esm/services/db/products-db-service-types.js +0 -2
  758. package/dist/esm/services/db/products-db-service-types.js.map +0 -1
  759. package/dist/esm/services/db/products-db-service.d.ts +0 -19
  760. package/dist/esm/services/db/products-db-service.js +0 -278
  761. package/dist/esm/services/db/products-db-service.js.map +0 -1
@@ -1,1139 +1,1139 @@
1
- const mockInvokeFunction = jest.fn();
2
- jest.mock('../../clients/index.js', () => ({
3
- DynamoDbClient: {
4
- safeGet: jest.fn(),
5
- safePut: jest.fn(),
6
- safeDelete: jest.fn(),
7
- safeBatchGet: jest.fn(),
8
- safeBatchWrite: jest.fn(),
9
- safeQueryByGSI: jest.fn(),
10
- batchWrite: jest.fn(),
11
- },
12
- LambdaInvokeClient: jest.fn().mockImplementation(() => ({
13
- invokeFunction: mockInvokeFunction,
14
- })),
15
- }));
16
- jest.mock('../../helpers/index.js', () => ({
17
- Logger: {
18
- debug: jest.fn(),
19
- info: jest.fn(),
20
- warn: jest.fn(),
21
- error: jest.fn(),
22
- },
23
- }));
24
- import { IdentityCacheDynamoDbService, } from '../../services/db/identity-cache-dynamodb-service.js';
25
- import { DynamoDbClient } from '../../clients/index.js';
26
- const mockedDynamoDbClient = DynamoDbClient;
27
- describe('IdentityCacheDynamoDbService', () => {
28
- beforeEach(() => {
29
- jest.clearAllMocks();
30
- mockedDynamoDbClient.safeGet.mockReset();
31
- mockedDynamoDbClient.safePut.mockReset();
32
- mockedDynamoDbClient.safeDelete.mockReset();
33
- mockedDynamoDbClient.safeBatchGet.mockReset();
34
- mockedDynamoDbClient.safeBatchWrite.mockReset();
35
- mockedDynamoDbClient.safeQueryByGSI.mockReset();
36
- mockedDynamoDbClient.batchWrite.mockReset();
37
- mockInvokeFunction.mockReset();
38
- IdentityCacheDynamoDbService.lambdaInvokeClient = undefined;
39
- });
40
- describe('getIdentityWithCaching', () => {
41
- const pixelId = 'pixel123';
42
- const lambdaArn = 'arn:aws:lambda:us-east-1:123456789:function:identity-private';
43
- describe('cache hit (fresh cache)', () => {
44
- it('should return cached identity without invoking Neptune Lambda', async () => {
45
- const incomingIdentity = {
46
- identityId: 'identity456',
47
- traits: { emails: ['test@email.com'] },
48
- };
49
- const cachedResponse = {
50
- pk: `identity#${pixelId}#identity456`,
51
- pixelId,
52
- identityId: 'identity456',
53
- response: {
54
- identityId: 'identity456',
55
- traits: { emails: ['test@email.com'] },
56
- },
57
- updatedAt: new Date().toISOString(),
58
- };
59
- mockedDynamoDbClient.safeGet.mockResolvedValueOnce(cachedResponse);
60
- const result = await IdentityCacheDynamoDbService.getIdentityWithCaching(pixelId, incomingIdentity, lambdaArn);
61
- expect(result).toBeDefined();
62
- expect(result?.identityId).toBe('identity456');
63
- expect(mockInvokeFunction).not.toHaveBeenCalled();
64
- });
65
- });
66
- describe('cache miss', () => {
67
- it('should invoke Neptune Lambda and return resolved identity', async () => {
68
- const incomingIdentity = {
69
- identityId: 'identity456',
70
- traits: { emails: ['test@email.com'] },
71
- };
72
- mockedDynamoDbClient.safeGet.mockResolvedValueOnce(null);
73
- mockInvokeFunction.mockResolvedValueOnce({
74
- statusCode: 200,
75
- body: JSON.stringify({
76
- identity: {
77
- identityId: 'resolved-identity',
78
- traits: { emails: ['test@email.com', 'resolved@email.com'] },
79
- },
80
- }),
81
- });
82
- const result = await IdentityCacheDynamoDbService.getIdentityWithCaching(pixelId, incomingIdentity, lambdaArn);
83
- expect(mockInvokeFunction).toHaveBeenCalledWith(lambdaArn, {
84
- pixelId,
85
- context: { identity: incomingIdentity },
86
- });
87
- expect(result?.identityId).toBe('resolved-identity');
88
- expect(mockedDynamoDbClient.safeBatchWrite).toHaveBeenCalled();
89
- });
90
- it('should return incomingIdentity when Neptune Lambda fails to resolve', async () => {
91
- const incomingIdentity = {
92
- identityId: 'identity456',
93
- traits: { emails: ['test@email.com'] },
94
- };
95
- mockedDynamoDbClient.safeGet.mockResolvedValueOnce(null);
96
- mockInvokeFunction.mockResolvedValueOnce({
97
- statusCode: 200,
98
- body: JSON.stringify({ identity: undefined }),
99
- });
100
- const result = await IdentityCacheDynamoDbService.getIdentityWithCaching(pixelId, incomingIdentity, lambdaArn);
101
- expect(result).toEqual(incomingIdentity);
102
- });
103
- });
104
- describe('cache stale', () => {
105
- it('should invoke Neptune Lambda when cache is stale', async () => {
106
- const incomingIdentity = {
107
- identityId: 'identity456',
108
- traits: { emails: ['old@email.com', 'new@email.com'] },
109
- };
110
- const cachedResponse = {
111
- pk: `identity#${pixelId}#identity456`,
112
- pixelId,
113
- identityId: 'identity456',
114
- response: {
115
- identityId: 'identity456',
116
- traits: { emails: ['old@email.com'] },
117
- },
118
- updatedAt: new Date().toISOString(),
119
- };
120
- mockedDynamoDbClient.safeGet.mockResolvedValueOnce(cachedResponse);
121
- mockInvokeFunction.mockResolvedValueOnce({
122
- statusCode: 200,
123
- body: JSON.stringify({
124
- identity: {
125
- identityId: 'identity456',
126
- traits: { emails: ['old@email.com', 'new@email.com'] },
127
- },
128
- }),
129
- });
130
- const result = await IdentityCacheDynamoDbService.getIdentityWithCaching(pixelId, incomingIdentity, lambdaArn);
131
- expect(mockInvokeFunction).toHaveBeenCalled();
132
- expect(result?.identityId).toBe('identity456');
133
- expect(result?.traits?.emails).toContain('new@email.com');
134
- });
135
- it('should use merged cache identity when Neptune fails', async () => {
136
- const incomingIdentity = {
137
- identityId: 'identity456',
138
- traits: { emails: ['old@email.com', 'new@email.com'] },
139
- };
140
- const cachedResponse = {
141
- pk: `identity#${pixelId}#identity456`,
142
- pixelId,
143
- identityId: 'identity456',
144
- response: {
145
- identityId: 'identity456',
146
- traits: { emails: ['old@email.com'] },
147
- },
148
- updatedAt: new Date().toISOString(),
149
- };
150
- mockedDynamoDbClient.safeGet.mockResolvedValueOnce(cachedResponse);
151
- mockInvokeFunction.mockResolvedValueOnce({
152
- statusCode: 500,
153
- body: 'Internal Server Error',
154
- });
155
- const result = await IdentityCacheDynamoDbService.getIdentityWithCaching(pixelId, incomingIdentity, lambdaArn);
156
- expect(result).toBeDefined();
157
- expect(result?.identityId).toBe('identity456');
158
- });
159
- });
160
- describe('Neptune Lambda invocation', () => {
161
- it('should write-back Neptune response to cache on success', async () => {
162
- const incomingIdentity = {
163
- traits: { emails: ['test@email.com'] },
164
- };
165
- mockedDynamoDbClient.safeGet.mockResolvedValueOnce(null);
166
- mockInvokeFunction.mockResolvedValueOnce({
167
- statusCode: 200,
168
- body: JSON.stringify({
169
- identity: {
170
- identityId: 'neptune-resolved-id',
171
- traits: { emails: ['test@email.com'] },
172
- },
173
- }),
174
- });
175
- await IdentityCacheDynamoDbService.getIdentityWithCaching(pixelId, incomingIdentity, lambdaArn);
176
- expect(mockedDynamoDbClient.safeBatchWrite).toHaveBeenCalled();
177
- const writtenItems = mockedDynamoDbClient.safeBatchWrite.mock.calls[0][1];
178
- const identityPks = writtenItems.map((item) => item.pk);
179
- expect(identityPks).toContain(`identity#${pixelId}#neptune-resolved-id`);
180
- });
181
- it('should not write to cache when Neptune returns undefined', async () => {
182
- const incomingIdentity = {
183
- traits: { emails: ['test@email.com'] },
184
- };
185
- mockedDynamoDbClient.safeGet.mockResolvedValueOnce(null);
186
- mockInvokeFunction.mockResolvedValueOnce({
187
- statusCode: 200,
188
- body: JSON.stringify({ identity: undefined }),
189
- });
190
- await IdentityCacheDynamoDbService.getIdentityWithCaching(pixelId, incomingIdentity, lambdaArn);
191
- expect(mockedDynamoDbClient.safeBatchWrite).not.toHaveBeenCalled();
192
- });
193
- });
194
- describe('error handling (fail-open)', () => {
195
- it('should return undefined when pixelId is missing', async () => {
196
- const result = await IdentityCacheDynamoDbService.getIdentityWithCaching(null, { identityId: 'test-id' }, lambdaArn);
197
- expect(result).toBeUndefined();
198
- expect(mockInvokeFunction).not.toHaveBeenCalled();
199
- });
200
- it('should return undefined when incomingIdentity is missing', async () => {
201
- const result = await IdentityCacheDynamoDbService.getIdentityWithCaching(pixelId, null, lambdaArn);
202
- expect(result).toBeUndefined();
203
- expect(mockInvokeFunction).not.toHaveBeenCalled();
204
- });
205
- it('should return undefined when Neptune Lambda throws', async () => {
206
- const incomingIdentity = {
207
- identityId: 'identity456',
208
- traits: { emails: ['test@email.com'] },
209
- };
210
- mockedDynamoDbClient.safeGet.mockResolvedValueOnce(null);
211
- mockInvokeFunction.mockRejectedValueOnce(new Error('Lambda invocation failed'));
212
- const result = await IdentityCacheDynamoDbService.getIdentityWithCaching(pixelId, incomingIdentity, lambdaArn);
213
- expect(result).toEqual(incomingIdentity);
214
- });
215
- it('should not throw when cache write fails after Neptune success', async () => {
216
- const incomingIdentity = {
217
- traits: { emails: ['test@email.com'] },
218
- };
219
- mockedDynamoDbClient.safeGet.mockResolvedValueOnce(null);
220
- mockInvokeFunction.mockResolvedValueOnce({
221
- statusCode: 200,
222
- body: JSON.stringify({
223
- identity: {
224
- identityId: 'neptune-resolved-id',
225
- traits: { emails: ['test@email.com'] },
226
- },
227
- }),
228
- });
229
- mockedDynamoDbClient.safeBatchWrite.mockRejectedValueOnce(new Error('DynamoDB write failed'));
230
- const result = await IdentityCacheDynamoDbService.getIdentityWithCaching(pixelId, incomingIdentity, lambdaArn);
231
- expect(result?.identityId).toBe('neptune-resolved-id');
232
- });
233
- });
234
- });
235
- describe('Key Builders', () => {
236
- describe('buildIdentityPk', () => {
237
- it('should build correct pk for identity lookup', async () => {
238
- const pixelId = 'pixel123';
239
- const identityId = 'identity456';
240
- mockedDynamoDbClient.safeGet.mockResolvedValueOnce(null);
241
- await IdentityCacheDynamoDbService.getIdentityFromCache(pixelId, { identityId });
242
- expect(mockedDynamoDbClient.safeGet).toHaveBeenCalledWith(expect.any(String), 'pk', `identity#${pixelId}#${identityId}`);
243
- });
244
- });
245
- describe('buildEmailPk', () => {
246
- it('should build correct pk for email lookup (lowercase)', async () => {
247
- const pixelId = 'pixel123';
248
- const email = 'Test@Email.COM';
249
- mockedDynamoDbClient.safeGet.mockResolvedValueOnce(null);
250
- await IdentityCacheDynamoDbService.getIdentityFromCache(pixelId, {
251
- traits: { emails: [email] },
252
- });
253
- expect(mockedDynamoDbClient.safeGet).toHaveBeenCalledWith(expect.any(String), 'pk', `email#${pixelId}#test@email.com`);
254
- });
255
- });
256
- describe('buildUserIdPk (no longer used for lookup)', () => {
257
- it('should NOT lookup by userId (optimization: email-only secondary lookup)', async () => {
258
- const pixelId = 'pixel123';
259
- const userId = ' user789 ';
260
- mockedDynamoDbClient.safeGet.mockResolvedValueOnce(null);
261
- await IdentityCacheDynamoDbService.getIdentityFromCache(pixelId, {
262
- traits: { userIds: [userId] },
263
- });
264
- expect(mockedDynamoDbClient.safeBatchGet).not.toHaveBeenCalled();
265
- });
266
- });
267
- });
268
- describe('getIdentityFromCache', () => {
269
- const pixelId = 'pixel123';
270
- describe('with identityId (primary lookup)', () => {
271
- it('should return cache hit when identity found', async () => {
272
- const identityId = 'identity456';
273
- const cachedResponse = {
274
- pk: `identity#${pixelId}#${identityId}`,
275
- pixelId,
276
- identityId,
277
- response: {
278
- identityId,
279
- traits: { emails: ['test@email.com'] },
280
- },
281
- updatedAt: new Date().toISOString(),
282
- };
283
- mockedDynamoDbClient.safeGet.mockResolvedValueOnce(cachedResponse);
284
- const result = await IdentityCacheDynamoDbService.getIdentityFromCache(pixelId, {
285
- identityId,
286
- traits: { emails: ['test@email.com'] },
287
- });
288
- expect(result.resolvedIdentity).toBeDefined();
289
- expect(result.resolvedIdentity?.identityId).toBe(identityId);
290
- expect(result.isCacheStale).toBe(false);
291
- });
292
- it('should return cache miss when identity not found', async () => {
293
- mockedDynamoDbClient.safeGet.mockResolvedValueOnce(null);
294
- const result = await IdentityCacheDynamoDbService.getIdentityFromCache(pixelId, {
295
- identityId: 'unknown-id',
296
- });
297
- expect(result.resolvedIdentity).toBeUndefined();
298
- expect(result.isCacheStale).toBe(true);
299
- });
300
- it('should NOT fallback to secondary keys when identityId lookup misses', async () => {
301
- mockedDynamoDbClient.safeGet.mockResolvedValueOnce(null);
302
- await IdentityCacheDynamoDbService.getIdentityFromCache(pixelId, {
303
- identityId: 'unknown-id',
304
- traits: { emails: ['test@email.com'] },
305
- });
306
- expect(mockedDynamoDbClient.safeBatchGet).not.toHaveBeenCalled();
307
- });
308
- it('should detect stale cache when new traits present', async () => {
309
- const identityId = 'identity456';
310
- const cachedResponse = {
311
- pk: `identity#${pixelId}#${identityId}`,
312
- pixelId,
313
- identityId,
314
- response: {
315
- identityId,
316
- traits: { emails: ['old@email.com'] },
317
- },
318
- updatedAt: new Date().toISOString(),
319
- };
320
- mockedDynamoDbClient.safeGet.mockResolvedValueOnce(cachedResponse);
321
- const result = await IdentityCacheDynamoDbService.getIdentityFromCache(pixelId, {
322
- identityId,
323
- traits: { emails: ['old@email.com', 'new@email.com'] },
324
- });
325
- expect(result.resolvedIdentity).toBeDefined();
326
- expect(result.isCacheStale).toBe(true);
327
- });
328
- });
329
- describe('without identityId (secondary lookup)', () => {
330
- it('should use single get for email lookup (pointer-based)', async () => {
331
- const pointerRecord = {
332
- pk: `email#${pixelId}#test@email.com`,
333
- pixelId,
334
- identityId: 'resolved-id',
335
- gsi1pk: 'resolved-id',
336
- updatedAt: new Date().toISOString(),
337
- };
338
- mockedDynamoDbClient.safeGet.mockResolvedValueOnce(pointerRecord);
339
- const result = await IdentityCacheDynamoDbService.getIdentityFromCache(pixelId, {
340
- traits: { emails: ['test@email.com'], userIds: ['user123'] },
341
- });
342
- expect(mockedDynamoDbClient.safeGet).toHaveBeenCalledWith(expect.any(String), 'pk', `email#${pixelId}#test@email.com`);
343
- expect(mockedDynamoDbClient.safeBatchGet).not.toHaveBeenCalled();
344
- expect(result.resolvedIdentity).toBeDefined();
345
- expect(result.resolvedIdentity?.traits?.emails).toContain('test@email.com');
346
- expect(result.isCacheStale).toBe(true);
347
- });
348
- it('should return discovered identityId with stale flag for Neptune resolution', async () => {
349
- const pointerRecord = {
350
- pk: `email#${pixelId}#test@email.com`,
351
- pixelId,
352
- identityId: 'discovered-id',
353
- gsi1pk: 'discovered-id',
354
- updatedAt: new Date().toISOString(),
355
- };
356
- mockedDynamoDbClient.safeGet.mockResolvedValueOnce(pointerRecord);
357
- const result = await IdentityCacheDynamoDbService.getIdentityFromCache(pixelId, {
358
- traits: { emails: ['test@email.com'], userIds: ['user123'] },
359
- });
360
- expect(result.resolvedIdentity).toBeDefined();
361
- expect(result.resolvedIdentity?.identityId).toBe('discovered-id');
362
- expect(result.isCacheStale).toBe(true);
363
- });
364
- it('should return cache miss when no secondary keys match', async () => {
365
- mockedDynamoDbClient.safeGet.mockResolvedValueOnce(null);
366
- const result = await IdentityCacheDynamoDbService.getIdentityFromCache(pixelId, {
367
- traits: { emails: ['unknown@email.com'] },
368
- });
369
- expect(result.resolvedIdentity).toBeUndefined();
370
- expect(result.isCacheStale).toBe(true);
371
- });
372
- it('should return cache miss when no emails to lookup', async () => {
373
- const result = await IdentityCacheDynamoDbService.getIdentityFromCache(pixelId, {
374
- traits: { userIds: ['user123'] },
375
- });
376
- expect(result.resolvedIdentity).toBeUndefined();
377
- expect(result.isCacheStale).toBe(true);
378
- expect(mockedDynamoDbClient.safeGet).not.toHaveBeenCalled();
379
- });
380
- it('should return cache miss when empty traits', async () => {
381
- const result = await IdentityCacheDynamoDbService.getIdentityFromCache(pixelId, {
382
- traits: {},
383
- });
384
- expect(result.resolvedIdentity).toBeUndefined();
385
- expect(result.isCacheStale).toBe(true);
386
- });
387
- });
388
- describe('error handling (fail-open)', () => {
389
- it('should return cache miss on safeGet error', async () => {
390
- mockedDynamoDbClient.safeGet.mockRejectedValueOnce(new Error('DynamoDB error'));
391
- const result = await IdentityCacheDynamoDbService.getIdentityFromCache(pixelId, {
392
- identityId: 'test-id',
393
- });
394
- expect(result.resolvedIdentity).toBeUndefined();
395
- expect(result.isCacheStale).toBe(true);
396
- });
397
- it('should return cache miss on safeGet error for email lookup', async () => {
398
- mockedDynamoDbClient.safeGet.mockRejectedValueOnce(new Error('DynamoDB error'));
399
- const result = await IdentityCacheDynamoDbService.getIdentityFromCache(pixelId, {
400
- traits: { emails: ['test@email.com'] },
401
- });
402
- expect(result.resolvedIdentity).toBeUndefined();
403
- expect(result.isCacheStale).toBe(true);
404
- });
405
- it('should return cache miss for null pixelId', async () => {
406
- const result = await IdentityCacheDynamoDbService.getIdentityFromCache(null, {
407
- identityId: 'test-id',
408
- });
409
- expect(result.resolvedIdentity).toBeUndefined();
410
- expect(result.isCacheStale).toBe(true);
411
- });
412
- it('should return cache miss for null incomingIdentity', async () => {
413
- const result = await IdentityCacheDynamoDbService.getIdentityFromCache(pixelId, null);
414
- expect(result.resolvedIdentity).toBeUndefined();
415
- expect(result.isCacheStale).toBe(true);
416
- });
417
- });
418
- });
419
- describe('updateIdentityCache', () => {
420
- const pixelId = 'pixel123';
421
- it('should write identity with email pointer (no full blob on secondary keys)', async () => {
422
- const identity = {
423
- identityId: 'identity456',
424
- traits: {
425
- emails: ['test@email.com', 'other@email.com'],
426
- userIds: ['user1', 'user2'],
427
- },
428
- };
429
- await IdentityCacheDynamoDbService.updateIdentityCache(pixelId, identity);
430
- expect(mockedDynamoDbClient.safeBatchWrite).toHaveBeenCalledTimes(1);
431
- const writtenItems = mockedDynamoDbClient.safeBatchWrite.mock.calls[0][1];
432
- expect(writtenItems).toHaveLength(3);
433
- const pks = writtenItems.map((item) => item.pk);
434
- expect(pks).toContain(`identity#${pixelId}#identity456`);
435
- expect(pks).toContain(`email#${pixelId}#test@email.com`);
436
- expect(pks).toContain(`email#${pixelId}#other@email.com`);
437
- expect(pks).not.toContain(`user_id#${pixelId}#user1`);
438
- expect(pks).not.toContain(`user_id#${pixelId}#user2`);
439
- const identityItem = writtenItems.find((item) => item.pk.startsWith('identity#'));
440
- expect(identityItem).toBeDefined();
441
- expect(identityItem.response).toBeDefined();
442
- expect(identityItem.response.identityId).toBe('identity456');
443
- const emailItem = writtenItems.find((item) => item.pk.startsWith('email#'));
444
- expect(emailItem).toBeDefined();
445
- expect(emailItem.response).toBeUndefined();
446
- expect(emailItem.identityId).toBe('identity456');
447
- });
448
- it('should not throw on safeBatchWrite error (fail-open)', async () => {
449
- mockedDynamoDbClient.safeBatchWrite.mockRejectedValueOnce(new Error('DynamoDB error'));
450
- const identity = {
451
- identityId: 'identity456',
452
- traits: { emails: ['test@email.com'] },
453
- };
454
- await expect(IdentityCacheDynamoDbService.updateIdentityCache(pixelId, identity)).resolves.toBeUndefined();
455
- });
456
- it('should return early for missing identityId', async () => {
457
- await IdentityCacheDynamoDbService.updateIdentityCache(pixelId, {});
458
- expect(mockedDynamoDbClient.safeBatchWrite).not.toHaveBeenCalled();
459
- });
460
- it('should return early for missing pixelId', async () => {
461
- await IdentityCacheDynamoDbService.updateIdentityCache(null, {
462
- identityId: 'test-id',
463
- });
464
- expect(mockedDynamoDbClient.safeBatchWrite).not.toHaveBeenCalled();
465
- });
466
- it('should handle identity with no traits', async () => {
467
- const identity = {
468
- identityId: 'identity456',
469
- };
470
- await IdentityCacheDynamoDbService.updateIdentityCache(pixelId, identity);
471
- const writtenItems = mockedDynamoDbClient.safeBatchWrite.mock.calls[0][1];
472
- expect(writtenItems).toHaveLength(1);
473
- });
474
- it('should include gsi1pk for reverse lookups', async () => {
475
- const identity = {
476
- identityId: 'identity456',
477
- traits: { emails: ['test@email.com'] },
478
- };
479
- await IdentityCacheDynamoDbService.updateIdentityCache(pixelId, identity);
480
- const writtenItems = mockedDynamoDbClient.safeBatchWrite.mock.calls[0][1];
481
- for (const item of writtenItems) {
482
- expect(item.gsi1pk).toBe('identity456');
483
- }
484
- });
485
- });
486
- describe('getIdentityMap', () => {
487
- it('should return identity map when found', async () => {
488
- const mockMap = {
489
- pk: 'identity_map#pixel123#identity456',
490
- pixelId: 'pixel123',
491
- identityId: 'identity456',
492
- linkedIdentities: ['identity789'],
493
- updatedAt: new Date().toISOString(),
494
- };
495
- mockedDynamoDbClient.safeGet.mockResolvedValueOnce(mockMap);
496
- const result = await IdentityCacheDynamoDbService.getIdentityMap('pixel123', 'identity456');
497
- expect(result).toEqual(mockMap);
498
- expect(mockedDynamoDbClient.safeGet).toHaveBeenCalledWith(expect.any(String), 'pk', 'identity_map#pixel123#identity456');
499
- });
500
- it('should return undefined when not found', async () => {
501
- mockedDynamoDbClient.safeGet.mockResolvedValueOnce(null);
502
- const result = await IdentityCacheDynamoDbService.getIdentityMap('pixel123', 'identity456');
503
- expect(result).toBeUndefined();
504
- });
505
- it('should return undefined for missing params (fail-open)', async () => {
506
- const result = await IdentityCacheDynamoDbService.getIdentityMap('', '');
507
- expect(result).toBeUndefined();
508
- expect(mockedDynamoDbClient.safeGet).not.toHaveBeenCalled();
509
- });
510
- it('should return undefined when only pixelId is missing', async () => {
511
- const result = await IdentityCacheDynamoDbService.getIdentityMap('', 'identity456');
512
- expect(result).toBeUndefined();
513
- expect(mockedDynamoDbClient.safeGet).not.toHaveBeenCalled();
514
- });
515
- it('should return undefined when only identityId is missing', async () => {
516
- const result = await IdentityCacheDynamoDbService.getIdentityMap('pixel123', '');
517
- expect(result).toBeUndefined();
518
- expect(mockedDynamoDbClient.safeGet).not.toHaveBeenCalled();
519
- });
520
- it('should return undefined on safeGet error (fail-open)', async () => {
521
- mockedDynamoDbClient.safeGet.mockRejectedValueOnce(new Error('DynamoDB error'));
522
- const result = await IdentityCacheDynamoDbService.getIdentityMap('pixel123', 'identity456');
523
- expect(result).toBeUndefined();
524
- });
525
- });
526
- describe('getForcePurgeFlag', () => {
527
- it('should return true when flag exists', async () => {
528
- mockedDynamoDbClient.safeGet.mockResolvedValueOnce({
529
- pk: 'force_purge#pixel123#identity456',
530
- });
531
- const result = await IdentityCacheDynamoDbService.getForcePurgeFlag('pixel123', 'identity456');
532
- expect(result).toBe(true);
533
- });
534
- it('should return false when flag not found', async () => {
535
- mockedDynamoDbClient.safeGet.mockResolvedValueOnce(null);
536
- const result = await IdentityCacheDynamoDbService.getForcePurgeFlag('pixel123', 'identity456');
537
- expect(result).toBe(false);
538
- });
539
- it('should return false on error (fail-open)', async () => {
540
- mockedDynamoDbClient.safeGet.mockRejectedValueOnce(new Error('DynamoDB error'));
541
- const result = await IdentityCacheDynamoDbService.getForcePurgeFlag('pixel123', 'identity456');
542
- expect(result).toBe(false);
543
- });
544
- it('should return false for missing params', async () => {
545
- const result = await IdentityCacheDynamoDbService.getForcePurgeFlag('', '');
546
- expect(result).toBe(false);
547
- expect(mockedDynamoDbClient.safeGet).not.toHaveBeenCalled();
548
- });
549
- });
550
- describe('setForcePurgeFlag', () => {
551
- it('should set flag with TTL', async () => {
552
- await IdentityCacheDynamoDbService.setForcePurgeFlag('pixel123', 'identity456');
553
- expect(mockedDynamoDbClient.safePut).toHaveBeenCalledTimes(1);
554
- const putItem = mockedDynamoDbClient.safePut.mock.calls[0][1];
555
- expect(putItem.pk).toBe('force_purge#pixel123#identity456');
556
- expect(putItem.pixelId).toBe('pixel123');
557
- expect(putItem.identityId).toBe('identity456');
558
- expect(putItem.ttl).toBeGreaterThan(0);
559
- });
560
- it('should accept custom TTL', async () => {
561
- await IdentityCacheDynamoDbService.setForcePurgeFlag('pixel123', 'identity456', 3600);
562
- const putItem = mockedDynamoDbClient.safePut.mock.calls[0][1];
563
- expect(putItem.ttl).toBeGreaterThan(0);
564
- });
565
- it('should include createdAt timestamp', async () => {
566
- const before = new Date().toISOString();
567
- await IdentityCacheDynamoDbService.setForcePurgeFlag('pixel123', 'identity456');
568
- const after = new Date().toISOString();
569
- const putItem = mockedDynamoDbClient.safePut.mock.calls[0][1];
570
- expect(putItem.createdAt).toBeDefined();
571
- expect(putItem.createdAt >= before).toBe(true);
572
- expect(putItem.createdAt <= after).toBe(true);
573
- });
574
- it('should return early when pixelId is missing', async () => {
575
- await IdentityCacheDynamoDbService.setForcePurgeFlag('', 'identity456');
576
- expect(mockedDynamoDbClient.safePut).not.toHaveBeenCalled();
577
- });
578
- it('should return early when identityId is missing', async () => {
579
- await IdentityCacheDynamoDbService.setForcePurgeFlag('pixel123', '');
580
- expect(mockedDynamoDbClient.safePut).not.toHaveBeenCalled();
581
- });
582
- it('should not throw on error (fail-open)', async () => {
583
- mockedDynamoDbClient.safePut.mockRejectedValueOnce(new Error('DynamoDB error'));
584
- await expect(IdentityCacheDynamoDbService.setForcePurgeFlag('pixel123', 'identity456')).resolves.toBeUndefined();
585
- });
586
- });
587
- describe('putIdentityMap', () => {
588
- it('should write identity map', async () => {
589
- await IdentityCacheDynamoDbService.putIdentityMap('pixel123', 'identity456', ['linked1', 'linked2']);
590
- expect(mockedDynamoDbClient.safePut).toHaveBeenCalledTimes(1);
591
- const putItem = mockedDynamoDbClient.safePut.mock.calls[0][1];
592
- expect(putItem.pk).toBe('identity_map#pixel123#identity456');
593
- expect(putItem.linkedIdentities).toEqual(['linked1', 'linked2']);
594
- });
595
- it('should return early for missing params', async () => {
596
- await IdentityCacheDynamoDbService.putIdentityMap('', '', []);
597
- expect(mockedDynamoDbClient.safePut).not.toHaveBeenCalled();
598
- });
599
- it('should return early when only pixelId is missing', async () => {
600
- await IdentityCacheDynamoDbService.putIdentityMap('', 'identity456', ['linked1']);
601
- expect(mockedDynamoDbClient.safePut).not.toHaveBeenCalled();
602
- });
603
- it('should return early when only identityId is missing', async () => {
604
- await IdentityCacheDynamoDbService.putIdentityMap('pixel123', '', ['linked1']);
605
- expect(mockedDynamoDbClient.safePut).not.toHaveBeenCalled();
606
- });
607
- it('should include updatedAt timestamp', async () => {
608
- const before = new Date().toISOString();
609
- await IdentityCacheDynamoDbService.putIdentityMap('pixel123', 'identity456', ['linked1']);
610
- const after = new Date().toISOString();
611
- const putItem = mockedDynamoDbClient.safePut.mock.calls[0][1];
612
- expect(putItem.updatedAt).toBeDefined();
613
- expect(putItem.updatedAt >= before).toBe(true);
614
- expect(putItem.updatedAt <= after).toBe(true);
615
- });
616
- it('should not throw on safePut error (fail-open)', async () => {
617
- mockedDynamoDbClient.safePut.mockRejectedValueOnce(new Error('DynamoDB error'));
618
- await expect(IdentityCacheDynamoDbService.putIdentityMap('pixel123', 'identity456', ['linked1'])).resolves.toBeUndefined();
619
- });
620
- });
621
- describe('deleteIdentityMap', () => {
622
- it('should delete identity map', async () => {
623
- await IdentityCacheDynamoDbService.deleteIdentityMap('pixel123', 'identity456');
624
- expect(mockedDynamoDbClient.safeDelete).toHaveBeenCalledWith(expect.any(String), 'pk', 'identity_map#pixel123#identity456');
625
- });
626
- it('should return early for missing params', async () => {
627
- await IdentityCacheDynamoDbService.deleteIdentityMap('', '');
628
- expect(mockedDynamoDbClient.safeDelete).not.toHaveBeenCalled();
629
- });
630
- it('should return early when only pixelId is missing', async () => {
631
- await IdentityCacheDynamoDbService.deleteIdentityMap('', 'identity456');
632
- expect(mockedDynamoDbClient.safeDelete).not.toHaveBeenCalled();
633
- });
634
- it('should return early when only identityId is missing', async () => {
635
- await IdentityCacheDynamoDbService.deleteIdentityMap('pixel123', '');
636
- expect(mockedDynamoDbClient.safeDelete).not.toHaveBeenCalled();
637
- });
638
- it('should not throw on safeDelete error (fail-open)', async () => {
639
- mockedDynamoDbClient.safeDelete.mockRejectedValueOnce(new Error('DynamoDB error'));
640
- await expect(IdentityCacheDynamoDbService.deleteIdentityMap('pixel123', 'identity456')).resolves.toBeUndefined();
641
- });
642
- });
643
- describe('deleteForcePurgeFlag', () => {
644
- it('should delete force purge flag', async () => {
645
- await IdentityCacheDynamoDbService.deleteForcePurgeFlag('pixel123', 'identity456');
646
- expect(mockedDynamoDbClient.safeDelete).toHaveBeenCalledWith(expect.any(String), 'pk', 'force_purge#pixel123#identity456');
647
- });
648
- it('should return early for missing params', async () => {
649
- await IdentityCacheDynamoDbService.deleteForcePurgeFlag('', '');
650
- expect(mockedDynamoDbClient.safeDelete).not.toHaveBeenCalled();
651
- });
652
- it('should return early when only pixelId is missing', async () => {
653
- await IdentityCacheDynamoDbService.deleteForcePurgeFlag('', 'identity456');
654
- expect(mockedDynamoDbClient.safeDelete).not.toHaveBeenCalled();
655
- });
656
- it('should return early when only identityId is missing', async () => {
657
- await IdentityCacheDynamoDbService.deleteForcePurgeFlag('pixel123', '');
658
- expect(mockedDynamoDbClient.safeDelete).not.toHaveBeenCalled();
659
- });
660
- it('should not throw on safeDelete error (fail-open)', async () => {
661
- mockedDynamoDbClient.safeDelete.mockRejectedValueOnce(new Error('DynamoDB error'));
662
- await expect(IdentityCacheDynamoDbService.deleteForcePurgeFlag('pixel123', 'identity456')).resolves.toBeUndefined();
663
- });
664
- });
665
- describe('deleteIdentityCache', () => {
666
- const pixelId = 'pixel123';
667
- it('should delete all related cache items', async () => {
668
- mockedDynamoDbClient.safeQueryByGSI.mockResolvedValue([
669
- { pk: `identity#${pixelId}#identity456`, pixelId },
670
- { pk: `email#${pixelId}#test@email.com`, pixelId },
671
- ]);
672
- mockedDynamoDbClient.batchWrite.mockResolvedValue({ $metadata: {} });
673
- const incomingIdentity = {
674
- identityId: 'identity456',
675
- traits: { emails: ['test@email.com'] },
676
- };
677
- const resolvedIdentity = {
678
- identityId: 'identity456',
679
- traits: { emails: ['test@email.com', 'other@email.com'] },
680
- };
681
- await IdentityCacheDynamoDbService.deleteIdentityCache(pixelId, incomingIdentity, resolvedIdentity);
682
- expect(mockedDynamoDbClient.safeQueryByGSI).toHaveBeenCalled();
683
- expect(mockedDynamoDbClient.batchWrite).toHaveBeenCalled();
684
- });
685
- it('should return early for missing pixelId', async () => {
686
- await IdentityCacheDynamoDbService.deleteIdentityCache(null, {}, {});
687
- expect(mockedDynamoDbClient.safeQueryByGSI).not.toHaveBeenCalled();
688
- });
689
- it('should handle different identityIds in incoming vs resolved', async () => {
690
- mockedDynamoDbClient.safeQueryByGSI.mockResolvedValue([{ pk: `identity#${pixelId}#incoming-id`, pixelId }]);
691
- mockedDynamoDbClient.batchWrite.mockResolvedValue({ $metadata: {} });
692
- const incomingIdentity = {
693
- identityId: 'incoming-id',
694
- traits: { emails: ['test@email.com'] },
695
- };
696
- const resolvedIdentity = {
697
- identityId: 'resolved-id',
698
- traits: { emails: ['test@email.com'] },
699
- };
700
- await IdentityCacheDynamoDbService.deleteIdentityCache(pixelId, incomingIdentity, resolvedIdentity);
701
- expect(mockedDynamoDbClient.safeQueryByGSI).toHaveBeenCalledTimes(2);
702
- });
703
- it('should not throw on batchWrite error (fail-open)', async () => {
704
- mockedDynamoDbClient.safeQueryByGSI.mockResolvedValue([{ pk: `identity#${pixelId}#identity456`, pixelId }]);
705
- mockedDynamoDbClient.batchWrite.mockRejectedValueOnce(new Error('DynamoDB error'));
706
- await expect(IdentityCacheDynamoDbService.deleteIdentityCache(pixelId, { identityId: 'identity456' }, { identityId: 'identity456' })).resolves.toBeUndefined();
707
- });
708
- it('should deduplicate keys when same items returned from GSI query', async () => {
709
- const duplicateItem = { pk: `identity#${pixelId}#identity456`, pixelId };
710
- mockedDynamoDbClient.safeQueryByGSI.mockResolvedValue([duplicateItem, duplicateItem]);
711
- mockedDynamoDbClient.batchWrite.mockResolvedValue({ $metadata: {} });
712
- await IdentityCacheDynamoDbService.deleteIdentityCache(pixelId, { identityId: 'identity456' }, { identityId: 'identity456' });
713
- expect(mockedDynamoDbClient.batchWrite).toHaveBeenCalled();
714
- const batchWriteCall = mockedDynamoDbClient.batchWrite.mock.calls[0];
715
- expect(batchWriteCall).toBeDefined();
716
- });
717
- });
718
- describe('merge and staleness logic', () => {
719
- const pixelId = 'pixel123';
720
- it('should merge incoming traits with cached traits', async () => {
721
- const cachedResponse = {
722
- pk: `identity#${pixelId}#identity456`,
723
- pixelId,
724
- identityId: 'identity456',
725
- response: {
726
- identityId: 'identity456',
727
- traits: {
728
- emails: ['cached@email.com'],
729
- userIds: ['cached-user'],
730
- },
731
- },
732
- updatedAt: new Date().toISOString(),
733
- };
734
- mockedDynamoDbClient.safeGet.mockResolvedValueOnce(cachedResponse);
735
- const result = await IdentityCacheDynamoDbService.getIdentityFromCache(pixelId, {
736
- identityId: 'identity456',
737
- traits: {
738
- emails: ['incoming@email.com'],
739
- phones: ['+1234567890'],
740
- },
741
- });
742
- expect(result.resolvedIdentity?.traits?.emails).toContain('incoming@email.com');
743
- expect(result.resolvedIdentity?.traits?.emails).toContain('cached@email.com');
744
- expect(result.resolvedIdentity?.traits?.userIds).toContain('cached-user');
745
- expect(result.resolvedIdentity?.traits?.phones).toContain('+1234567890');
746
- });
747
- it('should mark cache stale when new data present', async () => {
748
- const cachedResponse = {
749
- pk: `identity#${pixelId}#identity456`,
750
- pixelId,
751
- identityId: 'identity456',
752
- response: {
753
- identityId: 'identity456',
754
- traits: { emails: ['cached@email.com'] },
755
- },
756
- updatedAt: new Date().toISOString(),
757
- };
758
- mockedDynamoDbClient.safeGet.mockResolvedValueOnce(cachedResponse);
759
- const result = await IdentityCacheDynamoDbService.getIdentityFromCache(pixelId, {
760
- identityId: 'identity456',
761
- traits: {
762
- emails: ['cached@email.com', 'new@email.com'],
763
- },
764
- });
765
- expect(result.isCacheStale).toBe(true);
766
- });
767
- it('should not mark cache stale when incoming is subset of cached', async () => {
768
- const cachedResponse = {
769
- pk: `identity#${pixelId}#identity456`,
770
- pixelId,
771
- identityId: 'identity456',
772
- response: {
773
- identityId: 'identity456',
774
- traits: {
775
- emails: ['a@email.com', 'b@email.com'],
776
- userIds: ['user1', 'user2'],
777
- },
778
- },
779
- updatedAt: new Date().toISOString(),
780
- };
781
- mockedDynamoDbClient.safeGet.mockResolvedValueOnce(cachedResponse);
782
- const result = await IdentityCacheDynamoDbService.getIdentityFromCache(pixelId, {
783
- identityId: 'identity456',
784
- traits: {
785
- emails: ['a@email.com'],
786
- },
787
- });
788
- expect(result.isCacheStale).toBe(false);
789
- });
790
- });
791
- describe('multiple emails pointer writes', () => {
792
- const pixelId = 'pixel123';
793
- it('should write ALL emails as pointer records (not just first)', async () => {
794
- const identity = {
795
- identityId: 'identity456',
796
- traits: {
797
- emails: ['first@email.com', 'second@email.com', 'third@email.com', 'fourth@email.com'],
798
- },
799
- };
800
- await IdentityCacheDynamoDbService.updateIdentityCache(pixelId, identity);
801
- const writtenItems = mockedDynamoDbClient.safeBatchWrite.mock.calls[0][1];
802
- expect(writtenItems).toHaveLength(5);
803
- const emailPks = writtenItems
804
- .filter((item) => item.pk.startsWith('email#'))
805
- .map((item) => item.pk);
806
- expect(emailPks).toContain(`email#${pixelId}#first@email.com`);
807
- expect(emailPks).toContain(`email#${pixelId}#second@email.com`);
808
- expect(emailPks).toContain(`email#${pixelId}#third@email.com`);
809
- expect(emailPks).toContain(`email#${pixelId}#fourth@email.com`);
810
- });
811
- it('should skip null/undefined emails in array', async () => {
812
- const identity = {
813
- identityId: 'identity456',
814
- traits: {
815
- emails: ['valid@email.com', null, undefined, '', 'another@email.com'],
816
- },
817
- };
818
- await IdentityCacheDynamoDbService.updateIdentityCache(pixelId, identity);
819
- const writtenItems = mockedDynamoDbClient.safeBatchWrite.mock.calls[0][1];
820
- const emailItems = writtenItems.filter((item) => item.pk.startsWith('email#'));
821
- expect(emailItems).toHaveLength(2);
822
- expect(emailItems.map((e) => e.pk)).toContain(`email#${pixelId}#valid@email.com`);
823
- expect(emailItems.map((e) => e.pk)).toContain(`email#${pixelId}#another@email.com`);
824
- });
825
- it('should handle empty emails array', async () => {
826
- const identity = {
827
- identityId: 'identity456',
828
- traits: { emails: [] },
829
- };
830
- await IdentityCacheDynamoDbService.updateIdentityCache(pixelId, identity);
831
- const writtenItems = mockedDynamoDbClient.safeBatchWrite.mock.calls[0][1];
832
- expect(writtenItems).toHaveLength(1);
833
- expect(writtenItems[0].pk).toBe(`identity#${pixelId}#identity456`);
834
- });
835
- it('should handle email normalization (lowercase)', async () => {
836
- const identity = {
837
- identityId: 'identity456',
838
- traits: {
839
- emails: ['UPPERCASE@EMAIL.COM', 'MixedCase@Email.Com'],
840
- },
841
- };
842
- await IdentityCacheDynamoDbService.updateIdentityCache(pixelId, identity);
843
- const writtenItems = mockedDynamoDbClient.safeBatchWrite.mock.calls[0][1];
844
- const emailPks = writtenItems
845
- .filter((item) => item.pk.startsWith('email#'))
846
- .map((item) => item.pk);
847
- expect(emailPks).toContain(`email#${pixelId}#uppercase@email.com`);
848
- expect(emailPks).toContain(`email#${pixelId}#mixedcase@email.com`);
849
- });
850
- });
851
- describe('null/undefined traits edge cases', () => {
852
- const pixelId = 'pixel123';
853
- it('should handle null traits object', async () => {
854
- const identity = {
855
- identityId: 'identity456',
856
- traits: null,
857
- };
858
- await IdentityCacheDynamoDbService.updateIdentityCache(pixelId, identity);
859
- const writtenItems = mockedDynamoDbClient.safeBatchWrite.mock.calls[0][1];
860
- expect(writtenItems).toHaveLength(1);
861
- });
862
- it('should handle undefined traits object', async () => {
863
- const identity = {
864
- identityId: 'identity456',
865
- traits: undefined,
866
- };
867
- await IdentityCacheDynamoDbService.updateIdentityCache(pixelId, identity);
868
- const writtenItems = mockedDynamoDbClient.safeBatchWrite.mock.calls[0][1];
869
- expect(writtenItems).toHaveLength(1);
870
- });
871
- it('should return cache miss for identity with null traits in lookup', async () => {
872
- const result = await IdentityCacheDynamoDbService.getIdentityFromCache(pixelId, {
873
- traits: null,
874
- });
875
- expect(result.resolvedIdentity).toBeUndefined();
876
- expect(result.isCacheStale).toBe(true);
877
- expect(mockedDynamoDbClient.safeGet).not.toHaveBeenCalled();
878
- });
879
- it('should handle identity with undefined emails in traits', async () => {
880
- const result = await IdentityCacheDynamoDbService.getIdentityFromCache(pixelId, {
881
- traits: { userIds: ['user123'] },
882
- });
883
- expect(result.resolvedIdentity).toBeUndefined();
884
- expect(result.isCacheStale).toBe(true);
885
- });
886
- });
887
- describe('backward compatibility (old full-blob format)', () => {
888
- const pixelId = 'pixel123';
889
- it('should read old format email record with full response blob', async () => {
890
- const oldFormatEmailRecord = {
891
- pk: `email#${pixelId}#test@email.com`,
892
- pixelId,
893
- identityId: 'discovered-id',
894
- gsi1pk: 'discovered-id',
895
- response: {
896
- identityId: 'discovered-id',
897
- traits: { emails: ['test@email.com', 'other@email.com'] },
898
- },
899
- updatedAt: new Date().toISOString(),
900
- };
901
- mockedDynamoDbClient.safeGet.mockResolvedValueOnce(oldFormatEmailRecord);
902
- const result = await IdentityCacheDynamoDbService.getIdentityFromCache(pixelId, {
903
- traits: { emails: ['test@email.com'] },
904
- });
905
- expect(result.resolvedIdentity).toBeDefined();
906
- expect(result.resolvedIdentity?.identityId).toBe('discovered-id');
907
- expect(result.isCacheStale).toBe(true);
908
- });
909
- it('should read new format pointer record (no response blob)', async () => {
910
- const newFormatPointerRecord = {
911
- pk: `email#${pixelId}#test@email.com`,
912
- pixelId,
913
- identityId: 'discovered-id',
914
- gsi1pk: 'discovered-id',
915
- updatedAt: new Date().toISOString(),
916
- };
917
- mockedDynamoDbClient.safeGet.mockResolvedValueOnce(newFormatPointerRecord);
918
- const result = await IdentityCacheDynamoDbService.getIdentityFromCache(pixelId, {
919
- traits: { emails: ['test@email.com'] },
920
- });
921
- expect(result.resolvedIdentity).toBeDefined();
922
- expect(result.resolvedIdentity?.identityId).toBe('discovered-id');
923
- expect(result.isCacheStale).toBe(true);
924
- });
925
- it('should trigger Neptune resolution after finding pointer record', async () => {
926
- const lambdaArn = 'arn:aws:lambda:us-east-1:123456789:function:identity-private';
927
- const pointerRecord = {
928
- pk: `email#${pixelId}#test@email.com`,
929
- pixelId,
930
- identityId: 'discovered-id',
931
- gsi1pk: 'discovered-id',
932
- updatedAt: new Date().toISOString(),
933
- };
934
- mockedDynamoDbClient.safeGet.mockResolvedValueOnce(pointerRecord);
935
- mockInvokeFunction.mockResolvedValueOnce({
936
- statusCode: 200,
937
- body: JSON.stringify({
938
- identity: {
939
- identityId: 'discovered-id',
940
- traits: { emails: ['test@email.com', 'resolved@email.com'] },
941
- },
942
- }),
943
- });
944
- const result = await IdentityCacheDynamoDbService.getIdentityWithCaching(pixelId, { traits: { emails: ['test@email.com'] } }, lambdaArn);
945
- expect(mockInvokeFunction).toHaveBeenCalledWith(lambdaArn, expect.objectContaining({
946
- pixelId,
947
- context: {
948
- identity: expect.objectContaining({
949
- identityId: 'discovered-id',
950
- }),
951
- },
952
- }));
953
- expect(result?.identityId).toBe('discovered-id');
954
- });
955
- });
956
- describe('deleteIdentityCache with multiple email pointers', () => {
957
- const pixelId = 'pixel123';
958
- it('should delete all email pointers from both incoming and resolved identities', async () => {
959
- mockedDynamoDbClient.safeQueryByGSI.mockResolvedValue([
960
- { pk: `identity#${pixelId}#identity456`, pixelId },
961
- ]);
962
- mockedDynamoDbClient.batchWrite.mockResolvedValue({ $metadata: {} });
963
- const incomingIdentity = {
964
- identityId: 'identity456',
965
- traits: { emails: ['incoming1@email.com', 'incoming2@email.com'] },
966
- };
967
- const resolvedIdentity = {
968
- identityId: 'identity456',
969
- traits: { emails: ['resolved1@email.com', 'resolved2@email.com', 'resolved3@email.com'] },
970
- };
971
- await IdentityCacheDynamoDbService.deleteIdentityCache(pixelId, incomingIdentity, resolvedIdentity);
972
- expect(mockedDynamoDbClient.batchWrite).toHaveBeenCalled();
973
- const batchWriteCall = mockedDynamoDbClient.batchWrite.mock.calls[0][0];
974
- const deleteRequests = batchWriteCall.RequestItems[Object.keys(batchWriteCall.RequestItems)[0]];
975
- const deletedPks = deleteRequests.map((req) => req.DeleteRequest.Key.pk);
976
- expect(deletedPks).toContain(`identity#${pixelId}#identity456`);
977
- expect(deletedPks).toContain(`email#${pixelId}#incoming1@email.com`);
978
- expect(deletedPks).toContain(`email#${pixelId}#incoming2@email.com`);
979
- expect(deletedPks).toContain(`email#${pixelId}#resolved1@email.com`);
980
- expect(deletedPks).toContain(`email#${pixelId}#resolved2@email.com`);
981
- expect(deletedPks).toContain(`email#${pixelId}#resolved3@email.com`);
982
- });
983
- it('should handle empty emails in delete', async () => {
984
- mockedDynamoDbClient.safeQueryByGSI.mockResolvedValue([
985
- { pk: `identity#${pixelId}#identity456`, pixelId },
986
- ]);
987
- mockedDynamoDbClient.batchWrite.mockResolvedValue({ $metadata: {} });
988
- await IdentityCacheDynamoDbService.deleteIdentityCache(pixelId, { identityId: 'identity456', traits: { emails: [] } }, { identityId: 'identity456' });
989
- expect(mockedDynamoDbClient.batchWrite).toHaveBeenCalled();
990
- });
991
- it('should handle missing traits in delete', async () => {
992
- mockedDynamoDbClient.safeQueryByGSI.mockResolvedValue([
993
- { pk: `identity#${pixelId}#identity456`, pixelId },
994
- ]);
995
- mockedDynamoDbClient.batchWrite.mockResolvedValue({ $metadata: {} });
996
- await IdentityCacheDynamoDbService.deleteIdentityCache(pixelId, { identityId: 'identity456' }, { identityId: 'identity456' });
997
- expect(mockedDynamoDbClient.batchWrite).toHaveBeenCalled();
998
- });
999
- it('should not delete items from other pixelIds', async () => {
1000
- const otherPixelId = 'other-pixel';
1001
- mockedDynamoDbClient.safeQueryByGSI.mockResolvedValue([
1002
- { pk: `identity#${pixelId}#identity456`, pixelId },
1003
- { pk: `identity#${otherPixelId}#identity456`, pixelId: otherPixelId },
1004
- ]);
1005
- mockedDynamoDbClient.batchWrite.mockResolvedValue({ $metadata: {} });
1006
- await IdentityCacheDynamoDbService.deleteIdentityCache(pixelId, { identityId: 'identity456' }, { identityId: 'identity456' });
1007
- const batchWriteCall = mockedDynamoDbClient.batchWrite.mock.calls[0][0];
1008
- const deleteRequests = batchWriteCall.RequestItems[Object.keys(batchWriteCall.RequestItems)[0]];
1009
- const deletedPks = deleteRequests.map((req) => req.DeleteRequest.Key.pk);
1010
- expect(deletedPks).toContain(`identity#${pixelId}#identity456`);
1011
- expect(deletedPks).not.toContain(`identity#${otherPixelId}#identity456`);
1012
- });
1013
- });
1014
- describe('GSI1 query for reverse lookups', () => {
1015
- const pixelId = 'pixel123';
1016
- it('should find all records by identityId via GSI1', async () => {
1017
- const identityId = 'identity456';
1018
- const mockResults = [
1019
- { pk: `identity#${pixelId}#${identityId}`, pixelId, identityId },
1020
- { pk: `email#${pixelId}#email1@test.com`, pixelId, identityId },
1021
- { pk: `email#${pixelId}#email2@test.com`, pixelId, identityId },
1022
- ];
1023
- mockedDynamoDbClient.safeQueryByGSI.mockResolvedValue(mockResults);
1024
- mockedDynamoDbClient.batchWrite.mockResolvedValue({ $metadata: {} });
1025
- await IdentityCacheDynamoDbService.deleteIdentityCache(pixelId, { identityId }, { identityId });
1026
- expect(mockedDynamoDbClient.safeQueryByGSI).toHaveBeenCalledWith(expect.any(String), expect.any(String), 'gsi1pk', identityId);
1027
- });
1028
- it('should handle GSI1 query returning empty results', async () => {
1029
- mockedDynamoDbClient.safeQueryByGSI.mockResolvedValue([]);
1030
- await IdentityCacheDynamoDbService.deleteIdentityCache(pixelId, { identityId: 'unknown-id' }, { identityId: 'unknown-id' });
1031
- expect(mockedDynamoDbClient.batchWrite).not.toHaveBeenCalled();
1032
- });
1033
- it('should handle GSI1 query error gracefully (fail-open)', async () => {
1034
- mockedDynamoDbClient.safeQueryByGSI.mockRejectedValueOnce(new Error('GSI query failed'));
1035
- await expect(IdentityCacheDynamoDbService.deleteIdentityCache(pixelId, { identityId: 'identity456' }, { identityId: 'identity456' })).resolves.toBeUndefined();
1036
- });
1037
- });
1038
- describe('email pointer lookups', () => {
1039
- const pixelId = 'pixel123';
1040
- it('should only lookup first email (optimization)', async () => {
1041
- mockedDynamoDbClient.safeGet.mockResolvedValueOnce(null);
1042
- await IdentityCacheDynamoDbService.getIdentityFromCache(pixelId, {
1043
- traits: { emails: ['first@email.com', 'second@email.com', 'third@email.com'] },
1044
- });
1045
- expect(mockedDynamoDbClient.safeGet).toHaveBeenCalledTimes(1);
1046
- expect(mockedDynamoDbClient.safeGet).toHaveBeenCalledWith(expect.any(String), 'pk', `email#${pixelId}#first@email.com`);
1047
- });
1048
- it('should handle whitespace in email during lookup', async () => {
1049
- mockedDynamoDbClient.safeGet.mockResolvedValueOnce(null);
1050
- await IdentityCacheDynamoDbService.getIdentityFromCache(pixelId, {
1051
- traits: { emails: [' test@email.com '] },
1052
- });
1053
- expect(mockedDynamoDbClient.safeGet).toHaveBeenCalled();
1054
- });
1055
- });
1056
- describe('fail-open behavior comprehensive', () => {
1057
- const pixelId = 'pixel123';
1058
- const lambdaArn = 'arn:aws:lambda:us-east-1:123456789:function:identity-private';
1059
- it('should return incoming identity when Neptune throws (fail-open fallback)', async () => {
1060
- const incomingIdentity = {
1061
- identityId: 'identity456',
1062
- traits: { emails: ['test@email.com'] },
1063
- };
1064
- mockedDynamoDbClient.safeGet.mockRejectedValueOnce(new Error('DynamoDB down'));
1065
- mockInvokeFunction.mockRejectedValueOnce(new Error('Lambda down'));
1066
- const result = await IdentityCacheDynamoDbService.getIdentityWithCaching(pixelId, incomingIdentity, lambdaArn);
1067
- expect(result).toEqual(incomingIdentity);
1068
- });
1069
- it('should continue when cache write fails after Neptune success', async () => {
1070
- mockedDynamoDbClient.safeGet.mockResolvedValueOnce(null);
1071
- mockInvokeFunction.mockResolvedValueOnce({
1072
- statusCode: 200,
1073
- body: JSON.stringify({
1074
- identity: { identityId: 'resolved-id', traits: { emails: ['test@email.com'] } },
1075
- }),
1076
- });
1077
- mockedDynamoDbClient.safeBatchWrite.mockRejectedValueOnce(new Error('Write failed'));
1078
- const result = await IdentityCacheDynamoDbService.getIdentityWithCaching(pixelId, { traits: { emails: ['test@email.com'] } }, lambdaArn);
1079
- expect(result?.identityId).toBe('resolved-id');
1080
- });
1081
- it('should return incoming identity when Neptune returns non-200', async () => {
1082
- const incomingIdentity = {
1083
- identityId: 'incoming-id',
1084
- traits: { emails: ['test@email.com'] },
1085
- };
1086
- mockedDynamoDbClient.safeGet.mockResolvedValueOnce(null);
1087
- mockInvokeFunction.mockResolvedValueOnce({
1088
- statusCode: 500,
1089
- body: 'Internal Server Error',
1090
- });
1091
- const result = await IdentityCacheDynamoDbService.getIdentityWithCaching(pixelId, incomingIdentity, lambdaArn);
1092
- expect(result).toEqual(incomingIdentity);
1093
- });
1094
- it('should return incoming identity when Neptune returns malformed JSON (fail-open)', async () => {
1095
- const incomingIdentity = {
1096
- identityId: 'test-id',
1097
- traits: { emails: ['test@email.com'] },
1098
- };
1099
- mockedDynamoDbClient.safeGet.mockResolvedValueOnce(null);
1100
- mockInvokeFunction.mockResolvedValueOnce({
1101
- statusCode: 200,
1102
- body: 'not valid json',
1103
- });
1104
- const result = await IdentityCacheDynamoDbService.getIdentityWithCaching(pixelId, incomingIdentity, lambdaArn);
1105
- expect(result).toEqual(incomingIdentity);
1106
- });
1107
- it('should return undefined only when both inputs are invalid', async () => {
1108
- const result = await IdentityCacheDynamoDbService.getIdentityWithCaching(null, null, lambdaArn);
1109
- expect(result).toBeUndefined();
1110
- });
1111
- });
1112
- describe('gsi1pk attribute on all records', () => {
1113
- const pixelId = 'pixel123';
1114
- it('should include gsi1pk on identity record', async () => {
1115
- const identity = {
1116
- identityId: 'identity456',
1117
- traits: { emails: ['test@email.com'] },
1118
- };
1119
- await IdentityCacheDynamoDbService.updateIdentityCache(pixelId, identity);
1120
- const writtenItems = mockedDynamoDbClient.safeBatchWrite.mock.calls[0][1];
1121
- const identityItem = writtenItems.find((item) => item.pk.startsWith('identity#'));
1122
- expect(identityItem).toBeDefined();
1123
- expect(identityItem.gsi1pk).toBe('identity456');
1124
- });
1125
- it('should include gsi1pk on all email pointer records', async () => {
1126
- const identity = {
1127
- identityId: 'identity456',
1128
- traits: { emails: ['email1@test.com', 'email2@test.com'] },
1129
- };
1130
- await IdentityCacheDynamoDbService.updateIdentityCache(pixelId, identity);
1131
- const writtenItems = mockedDynamoDbClient.safeBatchWrite.mock.calls[0][1];
1132
- const emailItems = writtenItems.filter((item) => item.pk.startsWith('email#'));
1133
- for (const emailItem of emailItems) {
1134
- expect(emailItem.gsi1pk).toBe('identity456');
1135
- }
1136
- });
1137
- });
1138
- });
1
+ const mockInvokeFunction = jest.fn();
2
+ jest.mock('../../clients/index.js', () => ({
3
+ DynamoDbClient: {
4
+ safeGet: jest.fn(),
5
+ safePut: jest.fn(),
6
+ safeDelete: jest.fn(),
7
+ safeBatchGet: jest.fn(),
8
+ safeBatchWrite: jest.fn(),
9
+ safeQueryByGSI: jest.fn(),
10
+ batchWrite: jest.fn(),
11
+ },
12
+ LambdaInvokeClient: jest.fn().mockImplementation(() => ({
13
+ invokeFunction: mockInvokeFunction,
14
+ })),
15
+ }));
16
+ jest.mock('../../helpers/index.js', () => ({
17
+ Logger: {
18
+ debug: jest.fn(),
19
+ info: jest.fn(),
20
+ warn: jest.fn(),
21
+ error: jest.fn(),
22
+ },
23
+ }));
24
+ import { IdentityCacheDynamoDbService, } from '../../services/db/identity-cache-dynamodb-service.js';
25
+ import { DynamoDbClient } from '../../clients/index.js';
26
+ const mockedDynamoDbClient = DynamoDbClient;
27
+ describe('IdentityCacheDynamoDbService', () => {
28
+ beforeEach(() => {
29
+ jest.clearAllMocks();
30
+ mockedDynamoDbClient.safeGet.mockReset();
31
+ mockedDynamoDbClient.safePut.mockReset();
32
+ mockedDynamoDbClient.safeDelete.mockReset();
33
+ mockedDynamoDbClient.safeBatchGet.mockReset();
34
+ mockedDynamoDbClient.safeBatchWrite.mockReset();
35
+ mockedDynamoDbClient.safeQueryByGSI.mockReset();
36
+ mockedDynamoDbClient.batchWrite.mockReset();
37
+ mockInvokeFunction.mockReset();
38
+ IdentityCacheDynamoDbService.lambdaInvokeClient = undefined;
39
+ });
40
+ describe('getIdentityWithCaching', () => {
41
+ const pixelId = 'pixel123';
42
+ const lambdaArn = 'arn:aws:lambda:us-east-1:123456789:function:identity-private';
43
+ describe('cache hit (fresh cache)', () => {
44
+ it('should return cached identity without invoking Neptune Lambda', async () => {
45
+ const incomingIdentity = {
46
+ identityId: 'identity456',
47
+ traits: { emails: ['test@email.com'] },
48
+ };
49
+ const cachedResponse = {
50
+ pk: `identity#${pixelId}#identity456`,
51
+ pixelId,
52
+ identityId: 'identity456',
53
+ response: {
54
+ identityId: 'identity456',
55
+ traits: { emails: ['test@email.com'] },
56
+ },
57
+ updatedAt: new Date().toISOString(),
58
+ };
59
+ mockedDynamoDbClient.safeGet.mockResolvedValueOnce(cachedResponse);
60
+ const result = await IdentityCacheDynamoDbService.getIdentityWithCaching(pixelId, incomingIdentity, lambdaArn);
61
+ expect(result).toBeDefined();
62
+ expect(result?.identityId).toBe('identity456');
63
+ expect(mockInvokeFunction).not.toHaveBeenCalled();
64
+ });
65
+ });
66
+ describe('cache miss', () => {
67
+ it('should invoke Neptune Lambda and return resolved identity', async () => {
68
+ const incomingIdentity = {
69
+ identityId: 'identity456',
70
+ traits: { emails: ['test@email.com'] },
71
+ };
72
+ mockedDynamoDbClient.safeGet.mockResolvedValueOnce(null);
73
+ mockInvokeFunction.mockResolvedValueOnce({
74
+ statusCode: 200,
75
+ body: JSON.stringify({
76
+ identity: {
77
+ identityId: 'resolved-identity',
78
+ traits: { emails: ['test@email.com', 'resolved@email.com'] },
79
+ },
80
+ }),
81
+ });
82
+ const result = await IdentityCacheDynamoDbService.getIdentityWithCaching(pixelId, incomingIdentity, lambdaArn);
83
+ expect(mockInvokeFunction).toHaveBeenCalledWith(lambdaArn, {
84
+ pixelId,
85
+ context: { identity: incomingIdentity },
86
+ });
87
+ expect(result?.identityId).toBe('resolved-identity');
88
+ expect(mockedDynamoDbClient.safeBatchWrite).toHaveBeenCalled();
89
+ });
90
+ it('should return incomingIdentity when Neptune Lambda fails to resolve', async () => {
91
+ const incomingIdentity = {
92
+ identityId: 'identity456',
93
+ traits: { emails: ['test@email.com'] },
94
+ };
95
+ mockedDynamoDbClient.safeGet.mockResolvedValueOnce(null);
96
+ mockInvokeFunction.mockResolvedValueOnce({
97
+ statusCode: 200,
98
+ body: JSON.stringify({ identity: undefined }),
99
+ });
100
+ const result = await IdentityCacheDynamoDbService.getIdentityWithCaching(pixelId, incomingIdentity, lambdaArn);
101
+ expect(result).toEqual(incomingIdentity);
102
+ });
103
+ });
104
+ describe('cache stale', () => {
105
+ it('should invoke Neptune Lambda when cache is stale', async () => {
106
+ const incomingIdentity = {
107
+ identityId: 'identity456',
108
+ traits: { emails: ['old@email.com', 'new@email.com'] },
109
+ };
110
+ const cachedResponse = {
111
+ pk: `identity#${pixelId}#identity456`,
112
+ pixelId,
113
+ identityId: 'identity456',
114
+ response: {
115
+ identityId: 'identity456',
116
+ traits: { emails: ['old@email.com'] },
117
+ },
118
+ updatedAt: new Date().toISOString(),
119
+ };
120
+ mockedDynamoDbClient.safeGet.mockResolvedValueOnce(cachedResponse);
121
+ mockInvokeFunction.mockResolvedValueOnce({
122
+ statusCode: 200,
123
+ body: JSON.stringify({
124
+ identity: {
125
+ identityId: 'identity456',
126
+ traits: { emails: ['old@email.com', 'new@email.com'] },
127
+ },
128
+ }),
129
+ });
130
+ const result = await IdentityCacheDynamoDbService.getIdentityWithCaching(pixelId, incomingIdentity, lambdaArn);
131
+ expect(mockInvokeFunction).toHaveBeenCalled();
132
+ expect(result?.identityId).toBe('identity456');
133
+ expect(result?.traits?.emails).toContain('new@email.com');
134
+ });
135
+ it('should use merged cache identity when Neptune fails', async () => {
136
+ const incomingIdentity = {
137
+ identityId: 'identity456',
138
+ traits: { emails: ['old@email.com', 'new@email.com'] },
139
+ };
140
+ const cachedResponse = {
141
+ pk: `identity#${pixelId}#identity456`,
142
+ pixelId,
143
+ identityId: 'identity456',
144
+ response: {
145
+ identityId: 'identity456',
146
+ traits: { emails: ['old@email.com'] },
147
+ },
148
+ updatedAt: new Date().toISOString(),
149
+ };
150
+ mockedDynamoDbClient.safeGet.mockResolvedValueOnce(cachedResponse);
151
+ mockInvokeFunction.mockResolvedValueOnce({
152
+ statusCode: 500,
153
+ body: 'Internal Server Error',
154
+ });
155
+ const result = await IdentityCacheDynamoDbService.getIdentityWithCaching(pixelId, incomingIdentity, lambdaArn);
156
+ expect(result).toBeDefined();
157
+ expect(result?.identityId).toBe('identity456');
158
+ });
159
+ });
160
+ describe('Neptune Lambda invocation', () => {
161
+ it('should write-back Neptune response to cache on success', async () => {
162
+ const incomingIdentity = {
163
+ traits: { emails: ['test@email.com'] },
164
+ };
165
+ mockedDynamoDbClient.safeGet.mockResolvedValueOnce(null);
166
+ mockInvokeFunction.mockResolvedValueOnce({
167
+ statusCode: 200,
168
+ body: JSON.stringify({
169
+ identity: {
170
+ identityId: 'neptune-resolved-id',
171
+ traits: { emails: ['test@email.com'] },
172
+ },
173
+ }),
174
+ });
175
+ await IdentityCacheDynamoDbService.getIdentityWithCaching(pixelId, incomingIdentity, lambdaArn);
176
+ expect(mockedDynamoDbClient.safeBatchWrite).toHaveBeenCalled();
177
+ const writtenItems = mockedDynamoDbClient.safeBatchWrite.mock.calls[0][1];
178
+ const identityPks = writtenItems.map((item) => item.pk);
179
+ expect(identityPks).toContain(`identity#${pixelId}#neptune-resolved-id`);
180
+ });
181
+ it('should not write to cache when Neptune returns undefined', async () => {
182
+ const incomingIdentity = {
183
+ traits: { emails: ['test@email.com'] },
184
+ };
185
+ mockedDynamoDbClient.safeGet.mockResolvedValueOnce(null);
186
+ mockInvokeFunction.mockResolvedValueOnce({
187
+ statusCode: 200,
188
+ body: JSON.stringify({ identity: undefined }),
189
+ });
190
+ await IdentityCacheDynamoDbService.getIdentityWithCaching(pixelId, incomingIdentity, lambdaArn);
191
+ expect(mockedDynamoDbClient.safeBatchWrite).not.toHaveBeenCalled();
192
+ });
193
+ });
194
+ describe('error handling (fail-open)', () => {
195
+ it('should return undefined when pixelId is missing', async () => {
196
+ const result = await IdentityCacheDynamoDbService.getIdentityWithCaching(null, { identityId: 'test-id' }, lambdaArn);
197
+ expect(result).toBeUndefined();
198
+ expect(mockInvokeFunction).not.toHaveBeenCalled();
199
+ });
200
+ it('should return undefined when incomingIdentity is missing', async () => {
201
+ const result = await IdentityCacheDynamoDbService.getIdentityWithCaching(pixelId, null, lambdaArn);
202
+ expect(result).toBeUndefined();
203
+ expect(mockInvokeFunction).not.toHaveBeenCalled();
204
+ });
205
+ it('should return undefined when Neptune Lambda throws', async () => {
206
+ const incomingIdentity = {
207
+ identityId: 'identity456',
208
+ traits: { emails: ['test@email.com'] },
209
+ };
210
+ mockedDynamoDbClient.safeGet.mockResolvedValueOnce(null);
211
+ mockInvokeFunction.mockRejectedValueOnce(new Error('Lambda invocation failed'));
212
+ const result = await IdentityCacheDynamoDbService.getIdentityWithCaching(pixelId, incomingIdentity, lambdaArn);
213
+ expect(result).toEqual(incomingIdentity);
214
+ });
215
+ it('should not throw when cache write fails after Neptune success', async () => {
216
+ const incomingIdentity = {
217
+ traits: { emails: ['test@email.com'] },
218
+ };
219
+ mockedDynamoDbClient.safeGet.mockResolvedValueOnce(null);
220
+ mockInvokeFunction.mockResolvedValueOnce({
221
+ statusCode: 200,
222
+ body: JSON.stringify({
223
+ identity: {
224
+ identityId: 'neptune-resolved-id',
225
+ traits: { emails: ['test@email.com'] },
226
+ },
227
+ }),
228
+ });
229
+ mockedDynamoDbClient.safeBatchWrite.mockRejectedValueOnce(new Error('DynamoDB write failed'));
230
+ const result = await IdentityCacheDynamoDbService.getIdentityWithCaching(pixelId, incomingIdentity, lambdaArn);
231
+ expect(result?.identityId).toBe('neptune-resolved-id');
232
+ });
233
+ });
234
+ });
235
+ describe('Key Builders', () => {
236
+ describe('buildIdentityPk', () => {
237
+ it('should build correct pk for identity lookup', async () => {
238
+ const pixelId = 'pixel123';
239
+ const identityId = 'identity456';
240
+ mockedDynamoDbClient.safeGet.mockResolvedValueOnce(null);
241
+ await IdentityCacheDynamoDbService.getIdentityFromCache(pixelId, { identityId });
242
+ expect(mockedDynamoDbClient.safeGet).toHaveBeenCalledWith(expect.any(String), 'pk', `identity#${pixelId}#${identityId}`);
243
+ });
244
+ });
245
+ describe('buildEmailPk', () => {
246
+ it('should build correct pk for email lookup (lowercase)', async () => {
247
+ const pixelId = 'pixel123';
248
+ const email = 'Test@Email.COM';
249
+ mockedDynamoDbClient.safeGet.mockResolvedValueOnce(null);
250
+ await IdentityCacheDynamoDbService.getIdentityFromCache(pixelId, {
251
+ traits: { emails: [email] },
252
+ });
253
+ expect(mockedDynamoDbClient.safeGet).toHaveBeenCalledWith(expect.any(String), 'pk', `email#${pixelId}#test@email.com`);
254
+ });
255
+ });
256
+ describe('buildUserIdPk (no longer used for lookup)', () => {
257
+ it('should NOT lookup by userId (optimization: email-only secondary lookup)', async () => {
258
+ const pixelId = 'pixel123';
259
+ const userId = ' user789 ';
260
+ mockedDynamoDbClient.safeGet.mockResolvedValueOnce(null);
261
+ await IdentityCacheDynamoDbService.getIdentityFromCache(pixelId, {
262
+ traits: { userIds: [userId] },
263
+ });
264
+ expect(mockedDynamoDbClient.safeBatchGet).not.toHaveBeenCalled();
265
+ });
266
+ });
267
+ });
268
+ describe('getIdentityFromCache', () => {
269
+ const pixelId = 'pixel123';
270
+ describe('with identityId (primary lookup)', () => {
271
+ it('should return cache hit when identity found', async () => {
272
+ const identityId = 'identity456';
273
+ const cachedResponse = {
274
+ pk: `identity#${pixelId}#${identityId}`,
275
+ pixelId,
276
+ identityId,
277
+ response: {
278
+ identityId,
279
+ traits: { emails: ['test@email.com'] },
280
+ },
281
+ updatedAt: new Date().toISOString(),
282
+ };
283
+ mockedDynamoDbClient.safeGet.mockResolvedValueOnce(cachedResponse);
284
+ const result = await IdentityCacheDynamoDbService.getIdentityFromCache(pixelId, {
285
+ identityId,
286
+ traits: { emails: ['test@email.com'] },
287
+ });
288
+ expect(result.resolvedIdentity).toBeDefined();
289
+ expect(result.resolvedIdentity?.identityId).toBe(identityId);
290
+ expect(result.isCacheStale).toBe(false);
291
+ });
292
+ it('should return cache miss when identity not found', async () => {
293
+ mockedDynamoDbClient.safeGet.mockResolvedValueOnce(null);
294
+ const result = await IdentityCacheDynamoDbService.getIdentityFromCache(pixelId, {
295
+ identityId: 'unknown-id',
296
+ });
297
+ expect(result.resolvedIdentity).toBeUndefined();
298
+ expect(result.isCacheStale).toBe(true);
299
+ });
300
+ it('should NOT fallback to secondary keys when identityId lookup misses', async () => {
301
+ mockedDynamoDbClient.safeGet.mockResolvedValueOnce(null);
302
+ await IdentityCacheDynamoDbService.getIdentityFromCache(pixelId, {
303
+ identityId: 'unknown-id',
304
+ traits: { emails: ['test@email.com'] },
305
+ });
306
+ expect(mockedDynamoDbClient.safeBatchGet).not.toHaveBeenCalled();
307
+ });
308
+ it('should detect stale cache when new traits present', async () => {
309
+ const identityId = 'identity456';
310
+ const cachedResponse = {
311
+ pk: `identity#${pixelId}#${identityId}`,
312
+ pixelId,
313
+ identityId,
314
+ response: {
315
+ identityId,
316
+ traits: { emails: ['old@email.com'] },
317
+ },
318
+ updatedAt: new Date().toISOString(),
319
+ };
320
+ mockedDynamoDbClient.safeGet.mockResolvedValueOnce(cachedResponse);
321
+ const result = await IdentityCacheDynamoDbService.getIdentityFromCache(pixelId, {
322
+ identityId,
323
+ traits: { emails: ['old@email.com', 'new@email.com'] },
324
+ });
325
+ expect(result.resolvedIdentity).toBeDefined();
326
+ expect(result.isCacheStale).toBe(true);
327
+ });
328
+ });
329
+ describe('without identityId (secondary lookup)', () => {
330
+ it('should use single get for email lookup (pointer-based)', async () => {
331
+ const pointerRecord = {
332
+ pk: `email#${pixelId}#test@email.com`,
333
+ pixelId,
334
+ identityId: 'resolved-id',
335
+ gsi1pk: 'resolved-id',
336
+ updatedAt: new Date().toISOString(),
337
+ };
338
+ mockedDynamoDbClient.safeGet.mockResolvedValueOnce(pointerRecord);
339
+ const result = await IdentityCacheDynamoDbService.getIdentityFromCache(pixelId, {
340
+ traits: { emails: ['test@email.com'], userIds: ['user123'] },
341
+ });
342
+ expect(mockedDynamoDbClient.safeGet).toHaveBeenCalledWith(expect.any(String), 'pk', `email#${pixelId}#test@email.com`);
343
+ expect(mockedDynamoDbClient.safeBatchGet).not.toHaveBeenCalled();
344
+ expect(result.resolvedIdentity).toBeDefined();
345
+ expect(result.resolvedIdentity?.traits?.emails).toContain('test@email.com');
346
+ expect(result.isCacheStale).toBe(true);
347
+ });
348
+ it('should return discovered identityId with stale flag for Neptune resolution', async () => {
349
+ const pointerRecord = {
350
+ pk: `email#${pixelId}#test@email.com`,
351
+ pixelId,
352
+ identityId: 'discovered-id',
353
+ gsi1pk: 'discovered-id',
354
+ updatedAt: new Date().toISOString(),
355
+ };
356
+ mockedDynamoDbClient.safeGet.mockResolvedValueOnce(pointerRecord);
357
+ const result = await IdentityCacheDynamoDbService.getIdentityFromCache(pixelId, {
358
+ traits: { emails: ['test@email.com'], userIds: ['user123'] },
359
+ });
360
+ expect(result.resolvedIdentity).toBeDefined();
361
+ expect(result.resolvedIdentity?.identityId).toBe('discovered-id');
362
+ expect(result.isCacheStale).toBe(true);
363
+ });
364
+ it('should return cache miss when no secondary keys match', async () => {
365
+ mockedDynamoDbClient.safeGet.mockResolvedValueOnce(null);
366
+ const result = await IdentityCacheDynamoDbService.getIdentityFromCache(pixelId, {
367
+ traits: { emails: ['unknown@email.com'] },
368
+ });
369
+ expect(result.resolvedIdentity).toBeUndefined();
370
+ expect(result.isCacheStale).toBe(true);
371
+ });
372
+ it('should return cache miss when no emails to lookup', async () => {
373
+ const result = await IdentityCacheDynamoDbService.getIdentityFromCache(pixelId, {
374
+ traits: { userIds: ['user123'] },
375
+ });
376
+ expect(result.resolvedIdentity).toBeUndefined();
377
+ expect(result.isCacheStale).toBe(true);
378
+ expect(mockedDynamoDbClient.safeGet).not.toHaveBeenCalled();
379
+ });
380
+ it('should return cache miss when empty traits', async () => {
381
+ const result = await IdentityCacheDynamoDbService.getIdentityFromCache(pixelId, {
382
+ traits: {},
383
+ });
384
+ expect(result.resolvedIdentity).toBeUndefined();
385
+ expect(result.isCacheStale).toBe(true);
386
+ });
387
+ });
388
+ describe('error handling (fail-open)', () => {
389
+ it('should return cache miss on safeGet error', async () => {
390
+ mockedDynamoDbClient.safeGet.mockRejectedValueOnce(new Error('DynamoDB error'));
391
+ const result = await IdentityCacheDynamoDbService.getIdentityFromCache(pixelId, {
392
+ identityId: 'test-id',
393
+ });
394
+ expect(result.resolvedIdentity).toBeUndefined();
395
+ expect(result.isCacheStale).toBe(true);
396
+ });
397
+ it('should return cache miss on safeGet error for email lookup', async () => {
398
+ mockedDynamoDbClient.safeGet.mockRejectedValueOnce(new Error('DynamoDB error'));
399
+ const result = await IdentityCacheDynamoDbService.getIdentityFromCache(pixelId, {
400
+ traits: { emails: ['test@email.com'] },
401
+ });
402
+ expect(result.resolvedIdentity).toBeUndefined();
403
+ expect(result.isCacheStale).toBe(true);
404
+ });
405
+ it('should return cache miss for null pixelId', async () => {
406
+ const result = await IdentityCacheDynamoDbService.getIdentityFromCache(null, {
407
+ identityId: 'test-id',
408
+ });
409
+ expect(result.resolvedIdentity).toBeUndefined();
410
+ expect(result.isCacheStale).toBe(true);
411
+ });
412
+ it('should return cache miss for null incomingIdentity', async () => {
413
+ const result = await IdentityCacheDynamoDbService.getIdentityFromCache(pixelId, null);
414
+ expect(result.resolvedIdentity).toBeUndefined();
415
+ expect(result.isCacheStale).toBe(true);
416
+ });
417
+ });
418
+ });
419
+ describe('updateIdentityCache', () => {
420
+ const pixelId = 'pixel123';
421
+ it('should write identity with email pointer (no full blob on secondary keys)', async () => {
422
+ const identity = {
423
+ identityId: 'identity456',
424
+ traits: {
425
+ emails: ['test@email.com', 'other@email.com'],
426
+ userIds: ['user1', 'user2'],
427
+ },
428
+ };
429
+ await IdentityCacheDynamoDbService.updateIdentityCache(pixelId, identity);
430
+ expect(mockedDynamoDbClient.safeBatchWrite).toHaveBeenCalledTimes(1);
431
+ const writtenItems = mockedDynamoDbClient.safeBatchWrite.mock.calls[0][1];
432
+ expect(writtenItems).toHaveLength(3);
433
+ const pks = writtenItems.map((item) => item.pk);
434
+ expect(pks).toContain(`identity#${pixelId}#identity456`);
435
+ expect(pks).toContain(`email#${pixelId}#test@email.com`);
436
+ expect(pks).toContain(`email#${pixelId}#other@email.com`);
437
+ expect(pks).not.toContain(`user_id#${pixelId}#user1`);
438
+ expect(pks).not.toContain(`user_id#${pixelId}#user2`);
439
+ const identityItem = writtenItems.find((item) => item.pk.startsWith('identity#'));
440
+ expect(identityItem).toBeDefined();
441
+ expect(identityItem.response).toBeDefined();
442
+ expect(identityItem.response.identityId).toBe('identity456');
443
+ const emailItem = writtenItems.find((item) => item.pk.startsWith('email#'));
444
+ expect(emailItem).toBeDefined();
445
+ expect(emailItem.response).toBeUndefined();
446
+ expect(emailItem.identityId).toBe('identity456');
447
+ });
448
+ it('should not throw on safeBatchWrite error (fail-open)', async () => {
449
+ mockedDynamoDbClient.safeBatchWrite.mockRejectedValueOnce(new Error('DynamoDB error'));
450
+ const identity = {
451
+ identityId: 'identity456',
452
+ traits: { emails: ['test@email.com'] },
453
+ };
454
+ await expect(IdentityCacheDynamoDbService.updateIdentityCache(pixelId, identity)).resolves.toBeUndefined();
455
+ });
456
+ it('should return early for missing identityId', async () => {
457
+ await IdentityCacheDynamoDbService.updateIdentityCache(pixelId, {});
458
+ expect(mockedDynamoDbClient.safeBatchWrite).not.toHaveBeenCalled();
459
+ });
460
+ it('should return early for missing pixelId', async () => {
461
+ await IdentityCacheDynamoDbService.updateIdentityCache(null, {
462
+ identityId: 'test-id',
463
+ });
464
+ expect(mockedDynamoDbClient.safeBatchWrite).not.toHaveBeenCalled();
465
+ });
466
+ it('should handle identity with no traits', async () => {
467
+ const identity = {
468
+ identityId: 'identity456',
469
+ };
470
+ await IdentityCacheDynamoDbService.updateIdentityCache(pixelId, identity);
471
+ const writtenItems = mockedDynamoDbClient.safeBatchWrite.mock.calls[0][1];
472
+ expect(writtenItems).toHaveLength(1);
473
+ });
474
+ it('should include gsi1pk for reverse lookups', async () => {
475
+ const identity = {
476
+ identityId: 'identity456',
477
+ traits: { emails: ['test@email.com'] },
478
+ };
479
+ await IdentityCacheDynamoDbService.updateIdentityCache(pixelId, identity);
480
+ const writtenItems = mockedDynamoDbClient.safeBatchWrite.mock.calls[0][1];
481
+ for (const item of writtenItems) {
482
+ expect(item.gsi1pk).toBe('identity456');
483
+ }
484
+ });
485
+ });
486
+ describe('getIdentityMap', () => {
487
+ it('should return identity map when found', async () => {
488
+ const mockMap = {
489
+ pk: 'identity_map#pixel123#identity456',
490
+ pixelId: 'pixel123',
491
+ identityId: 'identity456',
492
+ linkedIdentities: ['identity789'],
493
+ updatedAt: new Date().toISOString(),
494
+ };
495
+ mockedDynamoDbClient.safeGet.mockResolvedValueOnce(mockMap);
496
+ const result = await IdentityCacheDynamoDbService.getIdentityMap('pixel123', 'identity456');
497
+ expect(result).toEqual(mockMap);
498
+ expect(mockedDynamoDbClient.safeGet).toHaveBeenCalledWith(expect.any(String), 'pk', 'identity_map#pixel123#identity456');
499
+ });
500
+ it('should return undefined when not found', async () => {
501
+ mockedDynamoDbClient.safeGet.mockResolvedValueOnce(null);
502
+ const result = await IdentityCacheDynamoDbService.getIdentityMap('pixel123', 'identity456');
503
+ expect(result).toBeUndefined();
504
+ });
505
+ it('should return undefined for missing params (fail-open)', async () => {
506
+ const result = await IdentityCacheDynamoDbService.getIdentityMap('', '');
507
+ expect(result).toBeUndefined();
508
+ expect(mockedDynamoDbClient.safeGet).not.toHaveBeenCalled();
509
+ });
510
+ it('should return undefined when only pixelId is missing', async () => {
511
+ const result = await IdentityCacheDynamoDbService.getIdentityMap('', 'identity456');
512
+ expect(result).toBeUndefined();
513
+ expect(mockedDynamoDbClient.safeGet).not.toHaveBeenCalled();
514
+ });
515
+ it('should return undefined when only identityId is missing', async () => {
516
+ const result = await IdentityCacheDynamoDbService.getIdentityMap('pixel123', '');
517
+ expect(result).toBeUndefined();
518
+ expect(mockedDynamoDbClient.safeGet).not.toHaveBeenCalled();
519
+ });
520
+ it('should return undefined on safeGet error (fail-open)', async () => {
521
+ mockedDynamoDbClient.safeGet.mockRejectedValueOnce(new Error('DynamoDB error'));
522
+ const result = await IdentityCacheDynamoDbService.getIdentityMap('pixel123', 'identity456');
523
+ expect(result).toBeUndefined();
524
+ });
525
+ });
526
+ describe('getForcePurgeFlag', () => {
527
+ it('should return true when flag exists', async () => {
528
+ mockedDynamoDbClient.safeGet.mockResolvedValueOnce({
529
+ pk: 'force_purge#pixel123#identity456',
530
+ });
531
+ const result = await IdentityCacheDynamoDbService.getForcePurgeFlag('pixel123', 'identity456');
532
+ expect(result).toBe(true);
533
+ });
534
+ it('should return false when flag not found', async () => {
535
+ mockedDynamoDbClient.safeGet.mockResolvedValueOnce(null);
536
+ const result = await IdentityCacheDynamoDbService.getForcePurgeFlag('pixel123', 'identity456');
537
+ expect(result).toBe(false);
538
+ });
539
+ it('should return false on error (fail-open)', async () => {
540
+ mockedDynamoDbClient.safeGet.mockRejectedValueOnce(new Error('DynamoDB error'));
541
+ const result = await IdentityCacheDynamoDbService.getForcePurgeFlag('pixel123', 'identity456');
542
+ expect(result).toBe(false);
543
+ });
544
+ it('should return false for missing params', async () => {
545
+ const result = await IdentityCacheDynamoDbService.getForcePurgeFlag('', '');
546
+ expect(result).toBe(false);
547
+ expect(mockedDynamoDbClient.safeGet).not.toHaveBeenCalled();
548
+ });
549
+ });
550
+ describe('setForcePurgeFlag', () => {
551
+ it('should set flag with TTL', async () => {
552
+ await IdentityCacheDynamoDbService.setForcePurgeFlag('pixel123', 'identity456');
553
+ expect(mockedDynamoDbClient.safePut).toHaveBeenCalledTimes(1);
554
+ const putItem = mockedDynamoDbClient.safePut.mock.calls[0][1];
555
+ expect(putItem.pk).toBe('force_purge#pixel123#identity456');
556
+ expect(putItem.pixelId).toBe('pixel123');
557
+ expect(putItem.identityId).toBe('identity456');
558
+ expect(putItem.ttl).toBeGreaterThan(0);
559
+ });
560
+ it('should accept custom TTL', async () => {
561
+ await IdentityCacheDynamoDbService.setForcePurgeFlag('pixel123', 'identity456', 3600);
562
+ const putItem = mockedDynamoDbClient.safePut.mock.calls[0][1];
563
+ expect(putItem.ttl).toBeGreaterThan(0);
564
+ });
565
+ it('should include createdAt timestamp', async () => {
566
+ const before = new Date().toISOString();
567
+ await IdentityCacheDynamoDbService.setForcePurgeFlag('pixel123', 'identity456');
568
+ const after = new Date().toISOString();
569
+ const putItem = mockedDynamoDbClient.safePut.mock.calls[0][1];
570
+ expect(putItem.createdAt).toBeDefined();
571
+ expect(putItem.createdAt >= before).toBe(true);
572
+ expect(putItem.createdAt <= after).toBe(true);
573
+ });
574
+ it('should return early when pixelId is missing', async () => {
575
+ await IdentityCacheDynamoDbService.setForcePurgeFlag('', 'identity456');
576
+ expect(mockedDynamoDbClient.safePut).not.toHaveBeenCalled();
577
+ });
578
+ it('should return early when identityId is missing', async () => {
579
+ await IdentityCacheDynamoDbService.setForcePurgeFlag('pixel123', '');
580
+ expect(mockedDynamoDbClient.safePut).not.toHaveBeenCalled();
581
+ });
582
+ it('should not throw on error (fail-open)', async () => {
583
+ mockedDynamoDbClient.safePut.mockRejectedValueOnce(new Error('DynamoDB error'));
584
+ await expect(IdentityCacheDynamoDbService.setForcePurgeFlag('pixel123', 'identity456')).resolves.toBeUndefined();
585
+ });
586
+ });
587
+ describe('putIdentityMap', () => {
588
+ it('should write identity map', async () => {
589
+ await IdentityCacheDynamoDbService.putIdentityMap('pixel123', 'identity456', ['linked1', 'linked2']);
590
+ expect(mockedDynamoDbClient.safePut).toHaveBeenCalledTimes(1);
591
+ const putItem = mockedDynamoDbClient.safePut.mock.calls[0][1];
592
+ expect(putItem.pk).toBe('identity_map#pixel123#identity456');
593
+ expect(putItem.linkedIdentities).toEqual(['linked1', 'linked2']);
594
+ });
595
+ it('should return early for missing params', async () => {
596
+ await IdentityCacheDynamoDbService.putIdentityMap('', '', []);
597
+ expect(mockedDynamoDbClient.safePut).not.toHaveBeenCalled();
598
+ });
599
+ it('should return early when only pixelId is missing', async () => {
600
+ await IdentityCacheDynamoDbService.putIdentityMap('', 'identity456', ['linked1']);
601
+ expect(mockedDynamoDbClient.safePut).not.toHaveBeenCalled();
602
+ });
603
+ it('should return early when only identityId is missing', async () => {
604
+ await IdentityCacheDynamoDbService.putIdentityMap('pixel123', '', ['linked1']);
605
+ expect(mockedDynamoDbClient.safePut).not.toHaveBeenCalled();
606
+ });
607
+ it('should include updatedAt timestamp', async () => {
608
+ const before = new Date().toISOString();
609
+ await IdentityCacheDynamoDbService.putIdentityMap('pixel123', 'identity456', ['linked1']);
610
+ const after = new Date().toISOString();
611
+ const putItem = mockedDynamoDbClient.safePut.mock.calls[0][1];
612
+ expect(putItem.updatedAt).toBeDefined();
613
+ expect(putItem.updatedAt >= before).toBe(true);
614
+ expect(putItem.updatedAt <= after).toBe(true);
615
+ });
616
+ it('should not throw on safePut error (fail-open)', async () => {
617
+ mockedDynamoDbClient.safePut.mockRejectedValueOnce(new Error('DynamoDB error'));
618
+ await expect(IdentityCacheDynamoDbService.putIdentityMap('pixel123', 'identity456', ['linked1'])).resolves.toBeUndefined();
619
+ });
620
+ });
621
+ describe('deleteIdentityMap', () => {
622
+ it('should delete identity map', async () => {
623
+ await IdentityCacheDynamoDbService.deleteIdentityMap('pixel123', 'identity456');
624
+ expect(mockedDynamoDbClient.safeDelete).toHaveBeenCalledWith(expect.any(String), 'pk', 'identity_map#pixel123#identity456');
625
+ });
626
+ it('should return early for missing params', async () => {
627
+ await IdentityCacheDynamoDbService.deleteIdentityMap('', '');
628
+ expect(mockedDynamoDbClient.safeDelete).not.toHaveBeenCalled();
629
+ });
630
+ it('should return early when only pixelId is missing', async () => {
631
+ await IdentityCacheDynamoDbService.deleteIdentityMap('', 'identity456');
632
+ expect(mockedDynamoDbClient.safeDelete).not.toHaveBeenCalled();
633
+ });
634
+ it('should return early when only identityId is missing', async () => {
635
+ await IdentityCacheDynamoDbService.deleteIdentityMap('pixel123', '');
636
+ expect(mockedDynamoDbClient.safeDelete).not.toHaveBeenCalled();
637
+ });
638
+ it('should not throw on safeDelete error (fail-open)', async () => {
639
+ mockedDynamoDbClient.safeDelete.mockRejectedValueOnce(new Error('DynamoDB error'));
640
+ await expect(IdentityCacheDynamoDbService.deleteIdentityMap('pixel123', 'identity456')).resolves.toBeUndefined();
641
+ });
642
+ });
643
+ describe('deleteForcePurgeFlag', () => {
644
+ it('should delete force purge flag', async () => {
645
+ await IdentityCacheDynamoDbService.deleteForcePurgeFlag('pixel123', 'identity456');
646
+ expect(mockedDynamoDbClient.safeDelete).toHaveBeenCalledWith(expect.any(String), 'pk', 'force_purge#pixel123#identity456');
647
+ });
648
+ it('should return early for missing params', async () => {
649
+ await IdentityCacheDynamoDbService.deleteForcePurgeFlag('', '');
650
+ expect(mockedDynamoDbClient.safeDelete).not.toHaveBeenCalled();
651
+ });
652
+ it('should return early when only pixelId is missing', async () => {
653
+ await IdentityCacheDynamoDbService.deleteForcePurgeFlag('', 'identity456');
654
+ expect(mockedDynamoDbClient.safeDelete).not.toHaveBeenCalled();
655
+ });
656
+ it('should return early when only identityId is missing', async () => {
657
+ await IdentityCacheDynamoDbService.deleteForcePurgeFlag('pixel123', '');
658
+ expect(mockedDynamoDbClient.safeDelete).not.toHaveBeenCalled();
659
+ });
660
+ it('should not throw on safeDelete error (fail-open)', async () => {
661
+ mockedDynamoDbClient.safeDelete.mockRejectedValueOnce(new Error('DynamoDB error'));
662
+ await expect(IdentityCacheDynamoDbService.deleteForcePurgeFlag('pixel123', 'identity456')).resolves.toBeUndefined();
663
+ });
664
+ });
665
+ describe('deleteIdentityCache', () => {
666
+ const pixelId = 'pixel123';
667
+ it('should delete all related cache items', async () => {
668
+ mockedDynamoDbClient.safeQueryByGSI.mockResolvedValue([
669
+ { pk: `identity#${pixelId}#identity456`, pixelId },
670
+ { pk: `email#${pixelId}#test@email.com`, pixelId },
671
+ ]);
672
+ mockedDynamoDbClient.batchWrite.mockResolvedValue({ $metadata: {} });
673
+ const incomingIdentity = {
674
+ identityId: 'identity456',
675
+ traits: { emails: ['test@email.com'] },
676
+ };
677
+ const resolvedIdentity = {
678
+ identityId: 'identity456',
679
+ traits: { emails: ['test@email.com', 'other@email.com'] },
680
+ };
681
+ await IdentityCacheDynamoDbService.deleteIdentityCache(pixelId, incomingIdentity, resolvedIdentity);
682
+ expect(mockedDynamoDbClient.safeQueryByGSI).toHaveBeenCalled();
683
+ expect(mockedDynamoDbClient.batchWrite).toHaveBeenCalled();
684
+ });
685
+ it('should return early for missing pixelId', async () => {
686
+ await IdentityCacheDynamoDbService.deleteIdentityCache(null, {}, {});
687
+ expect(mockedDynamoDbClient.safeQueryByGSI).not.toHaveBeenCalled();
688
+ });
689
+ it('should handle different identityIds in incoming vs resolved', async () => {
690
+ mockedDynamoDbClient.safeQueryByGSI.mockResolvedValue([{ pk: `identity#${pixelId}#incoming-id`, pixelId }]);
691
+ mockedDynamoDbClient.batchWrite.mockResolvedValue({ $metadata: {} });
692
+ const incomingIdentity = {
693
+ identityId: 'incoming-id',
694
+ traits: { emails: ['test@email.com'] },
695
+ };
696
+ const resolvedIdentity = {
697
+ identityId: 'resolved-id',
698
+ traits: { emails: ['test@email.com'] },
699
+ };
700
+ await IdentityCacheDynamoDbService.deleteIdentityCache(pixelId, incomingIdentity, resolvedIdentity);
701
+ expect(mockedDynamoDbClient.safeQueryByGSI).toHaveBeenCalledTimes(2);
702
+ });
703
+ it('should not throw on batchWrite error (fail-open)', async () => {
704
+ mockedDynamoDbClient.safeQueryByGSI.mockResolvedValue([{ pk: `identity#${pixelId}#identity456`, pixelId }]);
705
+ mockedDynamoDbClient.batchWrite.mockRejectedValueOnce(new Error('DynamoDB error'));
706
+ await expect(IdentityCacheDynamoDbService.deleteIdentityCache(pixelId, { identityId: 'identity456' }, { identityId: 'identity456' })).resolves.toBeUndefined();
707
+ });
708
+ it('should deduplicate keys when same items returned from GSI query', async () => {
709
+ const duplicateItem = { pk: `identity#${pixelId}#identity456`, pixelId };
710
+ mockedDynamoDbClient.safeQueryByGSI.mockResolvedValue([duplicateItem, duplicateItem]);
711
+ mockedDynamoDbClient.batchWrite.mockResolvedValue({ $metadata: {} });
712
+ await IdentityCacheDynamoDbService.deleteIdentityCache(pixelId, { identityId: 'identity456' }, { identityId: 'identity456' });
713
+ expect(mockedDynamoDbClient.batchWrite).toHaveBeenCalled();
714
+ const batchWriteCall = mockedDynamoDbClient.batchWrite.mock.calls[0];
715
+ expect(batchWriteCall).toBeDefined();
716
+ });
717
+ });
718
+ describe('merge and staleness logic', () => {
719
+ const pixelId = 'pixel123';
720
+ it('should merge incoming traits with cached traits', async () => {
721
+ const cachedResponse = {
722
+ pk: `identity#${pixelId}#identity456`,
723
+ pixelId,
724
+ identityId: 'identity456',
725
+ response: {
726
+ identityId: 'identity456',
727
+ traits: {
728
+ emails: ['cached@email.com'],
729
+ userIds: ['cached-user'],
730
+ },
731
+ },
732
+ updatedAt: new Date().toISOString(),
733
+ };
734
+ mockedDynamoDbClient.safeGet.mockResolvedValueOnce(cachedResponse);
735
+ const result = await IdentityCacheDynamoDbService.getIdentityFromCache(pixelId, {
736
+ identityId: 'identity456',
737
+ traits: {
738
+ emails: ['incoming@email.com'],
739
+ phones: ['+1234567890'],
740
+ },
741
+ });
742
+ expect(result.resolvedIdentity?.traits?.emails).toContain('incoming@email.com');
743
+ expect(result.resolvedIdentity?.traits?.emails).toContain('cached@email.com');
744
+ expect(result.resolvedIdentity?.traits?.userIds).toContain('cached-user');
745
+ expect(result.resolvedIdentity?.traits?.phones).toContain('+1234567890');
746
+ });
747
+ it('should mark cache stale when new data present', async () => {
748
+ const cachedResponse = {
749
+ pk: `identity#${pixelId}#identity456`,
750
+ pixelId,
751
+ identityId: 'identity456',
752
+ response: {
753
+ identityId: 'identity456',
754
+ traits: { emails: ['cached@email.com'] },
755
+ },
756
+ updatedAt: new Date().toISOString(),
757
+ };
758
+ mockedDynamoDbClient.safeGet.mockResolvedValueOnce(cachedResponse);
759
+ const result = await IdentityCacheDynamoDbService.getIdentityFromCache(pixelId, {
760
+ identityId: 'identity456',
761
+ traits: {
762
+ emails: ['cached@email.com', 'new@email.com'],
763
+ },
764
+ });
765
+ expect(result.isCacheStale).toBe(true);
766
+ });
767
+ it('should not mark cache stale when incoming is subset of cached', async () => {
768
+ const cachedResponse = {
769
+ pk: `identity#${pixelId}#identity456`,
770
+ pixelId,
771
+ identityId: 'identity456',
772
+ response: {
773
+ identityId: 'identity456',
774
+ traits: {
775
+ emails: ['a@email.com', 'b@email.com'],
776
+ userIds: ['user1', 'user2'],
777
+ },
778
+ },
779
+ updatedAt: new Date().toISOString(),
780
+ };
781
+ mockedDynamoDbClient.safeGet.mockResolvedValueOnce(cachedResponse);
782
+ const result = await IdentityCacheDynamoDbService.getIdentityFromCache(pixelId, {
783
+ identityId: 'identity456',
784
+ traits: {
785
+ emails: ['a@email.com'],
786
+ },
787
+ });
788
+ expect(result.isCacheStale).toBe(false);
789
+ });
790
+ });
791
+ describe('multiple emails pointer writes', () => {
792
+ const pixelId = 'pixel123';
793
+ it('should write ALL emails as pointer records (not just first)', async () => {
794
+ const identity = {
795
+ identityId: 'identity456',
796
+ traits: {
797
+ emails: ['first@email.com', 'second@email.com', 'third@email.com', 'fourth@email.com'],
798
+ },
799
+ };
800
+ await IdentityCacheDynamoDbService.updateIdentityCache(pixelId, identity);
801
+ const writtenItems = mockedDynamoDbClient.safeBatchWrite.mock.calls[0][1];
802
+ expect(writtenItems).toHaveLength(5);
803
+ const emailPks = writtenItems
804
+ .filter((item) => item.pk.startsWith('email#'))
805
+ .map((item) => item.pk);
806
+ expect(emailPks).toContain(`email#${pixelId}#first@email.com`);
807
+ expect(emailPks).toContain(`email#${pixelId}#second@email.com`);
808
+ expect(emailPks).toContain(`email#${pixelId}#third@email.com`);
809
+ expect(emailPks).toContain(`email#${pixelId}#fourth@email.com`);
810
+ });
811
+ it('should skip null/undefined emails in array', async () => {
812
+ const identity = {
813
+ identityId: 'identity456',
814
+ traits: {
815
+ emails: ['valid@email.com', null, undefined, '', 'another@email.com'],
816
+ },
817
+ };
818
+ await IdentityCacheDynamoDbService.updateIdentityCache(pixelId, identity);
819
+ const writtenItems = mockedDynamoDbClient.safeBatchWrite.mock.calls[0][1];
820
+ const emailItems = writtenItems.filter((item) => item.pk.startsWith('email#'));
821
+ expect(emailItems).toHaveLength(2);
822
+ expect(emailItems.map((e) => e.pk)).toContain(`email#${pixelId}#valid@email.com`);
823
+ expect(emailItems.map((e) => e.pk)).toContain(`email#${pixelId}#another@email.com`);
824
+ });
825
+ it('should handle empty emails array', async () => {
826
+ const identity = {
827
+ identityId: 'identity456',
828
+ traits: { emails: [] },
829
+ };
830
+ await IdentityCacheDynamoDbService.updateIdentityCache(pixelId, identity);
831
+ const writtenItems = mockedDynamoDbClient.safeBatchWrite.mock.calls[0][1];
832
+ expect(writtenItems).toHaveLength(1);
833
+ expect(writtenItems[0].pk).toBe(`identity#${pixelId}#identity456`);
834
+ });
835
+ it('should handle email normalization (lowercase)', async () => {
836
+ const identity = {
837
+ identityId: 'identity456',
838
+ traits: {
839
+ emails: ['UPPERCASE@EMAIL.COM', 'MixedCase@Email.Com'],
840
+ },
841
+ };
842
+ await IdentityCacheDynamoDbService.updateIdentityCache(pixelId, identity);
843
+ const writtenItems = mockedDynamoDbClient.safeBatchWrite.mock.calls[0][1];
844
+ const emailPks = writtenItems
845
+ .filter((item) => item.pk.startsWith('email#'))
846
+ .map((item) => item.pk);
847
+ expect(emailPks).toContain(`email#${pixelId}#uppercase@email.com`);
848
+ expect(emailPks).toContain(`email#${pixelId}#mixedcase@email.com`);
849
+ });
850
+ });
851
+ describe('null/undefined traits edge cases', () => {
852
+ const pixelId = 'pixel123';
853
+ it('should handle null traits object', async () => {
854
+ const identity = {
855
+ identityId: 'identity456',
856
+ traits: null,
857
+ };
858
+ await IdentityCacheDynamoDbService.updateIdentityCache(pixelId, identity);
859
+ const writtenItems = mockedDynamoDbClient.safeBatchWrite.mock.calls[0][1];
860
+ expect(writtenItems).toHaveLength(1);
861
+ });
862
+ it('should handle undefined traits object', async () => {
863
+ const identity = {
864
+ identityId: 'identity456',
865
+ traits: undefined,
866
+ };
867
+ await IdentityCacheDynamoDbService.updateIdentityCache(pixelId, identity);
868
+ const writtenItems = mockedDynamoDbClient.safeBatchWrite.mock.calls[0][1];
869
+ expect(writtenItems).toHaveLength(1);
870
+ });
871
+ it('should return cache miss for identity with null traits in lookup', async () => {
872
+ const result = await IdentityCacheDynamoDbService.getIdentityFromCache(pixelId, {
873
+ traits: null,
874
+ });
875
+ expect(result.resolvedIdentity).toBeUndefined();
876
+ expect(result.isCacheStale).toBe(true);
877
+ expect(mockedDynamoDbClient.safeGet).not.toHaveBeenCalled();
878
+ });
879
+ it('should handle identity with undefined emails in traits', async () => {
880
+ const result = await IdentityCacheDynamoDbService.getIdentityFromCache(pixelId, {
881
+ traits: { userIds: ['user123'] },
882
+ });
883
+ expect(result.resolvedIdentity).toBeUndefined();
884
+ expect(result.isCacheStale).toBe(true);
885
+ });
886
+ });
887
+ describe('backward compatibility (old full-blob format)', () => {
888
+ const pixelId = 'pixel123';
889
+ it('should read old format email record with full response blob', async () => {
890
+ const oldFormatEmailRecord = {
891
+ pk: `email#${pixelId}#test@email.com`,
892
+ pixelId,
893
+ identityId: 'discovered-id',
894
+ gsi1pk: 'discovered-id',
895
+ response: {
896
+ identityId: 'discovered-id',
897
+ traits: { emails: ['test@email.com', 'other@email.com'] },
898
+ },
899
+ updatedAt: new Date().toISOString(),
900
+ };
901
+ mockedDynamoDbClient.safeGet.mockResolvedValueOnce(oldFormatEmailRecord);
902
+ const result = await IdentityCacheDynamoDbService.getIdentityFromCache(pixelId, {
903
+ traits: { emails: ['test@email.com'] },
904
+ });
905
+ expect(result.resolvedIdentity).toBeDefined();
906
+ expect(result.resolvedIdentity?.identityId).toBe('discovered-id');
907
+ expect(result.isCacheStale).toBe(true);
908
+ });
909
+ it('should read new format pointer record (no response blob)', async () => {
910
+ const newFormatPointerRecord = {
911
+ pk: `email#${pixelId}#test@email.com`,
912
+ pixelId,
913
+ identityId: 'discovered-id',
914
+ gsi1pk: 'discovered-id',
915
+ updatedAt: new Date().toISOString(),
916
+ };
917
+ mockedDynamoDbClient.safeGet.mockResolvedValueOnce(newFormatPointerRecord);
918
+ const result = await IdentityCacheDynamoDbService.getIdentityFromCache(pixelId, {
919
+ traits: { emails: ['test@email.com'] },
920
+ });
921
+ expect(result.resolvedIdentity).toBeDefined();
922
+ expect(result.resolvedIdentity?.identityId).toBe('discovered-id');
923
+ expect(result.isCacheStale).toBe(true);
924
+ });
925
+ it('should trigger Neptune resolution after finding pointer record', async () => {
926
+ const lambdaArn = 'arn:aws:lambda:us-east-1:123456789:function:identity-private';
927
+ const pointerRecord = {
928
+ pk: `email#${pixelId}#test@email.com`,
929
+ pixelId,
930
+ identityId: 'discovered-id',
931
+ gsi1pk: 'discovered-id',
932
+ updatedAt: new Date().toISOString(),
933
+ };
934
+ mockedDynamoDbClient.safeGet.mockResolvedValueOnce(pointerRecord);
935
+ mockInvokeFunction.mockResolvedValueOnce({
936
+ statusCode: 200,
937
+ body: JSON.stringify({
938
+ identity: {
939
+ identityId: 'discovered-id',
940
+ traits: { emails: ['test@email.com', 'resolved@email.com'] },
941
+ },
942
+ }),
943
+ });
944
+ const result = await IdentityCacheDynamoDbService.getIdentityWithCaching(pixelId, { traits: { emails: ['test@email.com'] } }, lambdaArn);
945
+ expect(mockInvokeFunction).toHaveBeenCalledWith(lambdaArn, expect.objectContaining({
946
+ pixelId,
947
+ context: {
948
+ identity: expect.objectContaining({
949
+ identityId: 'discovered-id',
950
+ }),
951
+ },
952
+ }));
953
+ expect(result?.identityId).toBe('discovered-id');
954
+ });
955
+ });
956
+ describe('deleteIdentityCache with multiple email pointers', () => {
957
+ const pixelId = 'pixel123';
958
+ it('should delete all email pointers from both incoming and resolved identities', async () => {
959
+ mockedDynamoDbClient.safeQueryByGSI.mockResolvedValue([
960
+ { pk: `identity#${pixelId}#identity456`, pixelId },
961
+ ]);
962
+ mockedDynamoDbClient.batchWrite.mockResolvedValue({ $metadata: {} });
963
+ const incomingIdentity = {
964
+ identityId: 'identity456',
965
+ traits: { emails: ['incoming1@email.com', 'incoming2@email.com'] },
966
+ };
967
+ const resolvedIdentity = {
968
+ identityId: 'identity456',
969
+ traits: { emails: ['resolved1@email.com', 'resolved2@email.com', 'resolved3@email.com'] },
970
+ };
971
+ await IdentityCacheDynamoDbService.deleteIdentityCache(pixelId, incomingIdentity, resolvedIdentity);
972
+ expect(mockedDynamoDbClient.batchWrite).toHaveBeenCalled();
973
+ const batchWriteCall = mockedDynamoDbClient.batchWrite.mock.calls[0][0];
974
+ const deleteRequests = batchWriteCall.RequestItems[Object.keys(batchWriteCall.RequestItems)[0]];
975
+ const deletedPks = deleteRequests.map((req) => req.DeleteRequest.Key.pk);
976
+ expect(deletedPks).toContain(`identity#${pixelId}#identity456`);
977
+ expect(deletedPks).toContain(`email#${pixelId}#incoming1@email.com`);
978
+ expect(deletedPks).toContain(`email#${pixelId}#incoming2@email.com`);
979
+ expect(deletedPks).toContain(`email#${pixelId}#resolved1@email.com`);
980
+ expect(deletedPks).toContain(`email#${pixelId}#resolved2@email.com`);
981
+ expect(deletedPks).toContain(`email#${pixelId}#resolved3@email.com`);
982
+ });
983
+ it('should handle empty emails in delete', async () => {
984
+ mockedDynamoDbClient.safeQueryByGSI.mockResolvedValue([
985
+ { pk: `identity#${pixelId}#identity456`, pixelId },
986
+ ]);
987
+ mockedDynamoDbClient.batchWrite.mockResolvedValue({ $metadata: {} });
988
+ await IdentityCacheDynamoDbService.deleteIdentityCache(pixelId, { identityId: 'identity456', traits: { emails: [] } }, { identityId: 'identity456' });
989
+ expect(mockedDynamoDbClient.batchWrite).toHaveBeenCalled();
990
+ });
991
+ it('should handle missing traits in delete', async () => {
992
+ mockedDynamoDbClient.safeQueryByGSI.mockResolvedValue([
993
+ { pk: `identity#${pixelId}#identity456`, pixelId },
994
+ ]);
995
+ mockedDynamoDbClient.batchWrite.mockResolvedValue({ $metadata: {} });
996
+ await IdentityCacheDynamoDbService.deleteIdentityCache(pixelId, { identityId: 'identity456' }, { identityId: 'identity456' });
997
+ expect(mockedDynamoDbClient.batchWrite).toHaveBeenCalled();
998
+ });
999
+ it('should not delete items from other pixelIds', async () => {
1000
+ const otherPixelId = 'other-pixel';
1001
+ mockedDynamoDbClient.safeQueryByGSI.mockResolvedValue([
1002
+ { pk: `identity#${pixelId}#identity456`, pixelId },
1003
+ { pk: `identity#${otherPixelId}#identity456`, pixelId: otherPixelId },
1004
+ ]);
1005
+ mockedDynamoDbClient.batchWrite.mockResolvedValue({ $metadata: {} });
1006
+ await IdentityCacheDynamoDbService.deleteIdentityCache(pixelId, { identityId: 'identity456' }, { identityId: 'identity456' });
1007
+ const batchWriteCall = mockedDynamoDbClient.batchWrite.mock.calls[0][0];
1008
+ const deleteRequests = batchWriteCall.RequestItems[Object.keys(batchWriteCall.RequestItems)[0]];
1009
+ const deletedPks = deleteRequests.map((req) => req.DeleteRequest.Key.pk);
1010
+ expect(deletedPks).toContain(`identity#${pixelId}#identity456`);
1011
+ expect(deletedPks).not.toContain(`identity#${otherPixelId}#identity456`);
1012
+ });
1013
+ });
1014
+ describe('GSI1 query for reverse lookups', () => {
1015
+ const pixelId = 'pixel123';
1016
+ it('should find all records by identityId via GSI1', async () => {
1017
+ const identityId = 'identity456';
1018
+ const mockResults = [
1019
+ { pk: `identity#${pixelId}#${identityId}`, pixelId, identityId },
1020
+ { pk: `email#${pixelId}#email1@test.com`, pixelId, identityId },
1021
+ { pk: `email#${pixelId}#email2@test.com`, pixelId, identityId },
1022
+ ];
1023
+ mockedDynamoDbClient.safeQueryByGSI.mockResolvedValue(mockResults);
1024
+ mockedDynamoDbClient.batchWrite.mockResolvedValue({ $metadata: {} });
1025
+ await IdentityCacheDynamoDbService.deleteIdentityCache(pixelId, { identityId }, { identityId });
1026
+ expect(mockedDynamoDbClient.safeQueryByGSI).toHaveBeenCalledWith(expect.any(String), expect.any(String), 'gsi1pk', identityId);
1027
+ });
1028
+ it('should handle GSI1 query returning empty results', async () => {
1029
+ mockedDynamoDbClient.safeQueryByGSI.mockResolvedValue([]);
1030
+ await IdentityCacheDynamoDbService.deleteIdentityCache(pixelId, { identityId: 'unknown-id' }, { identityId: 'unknown-id' });
1031
+ expect(mockedDynamoDbClient.batchWrite).not.toHaveBeenCalled();
1032
+ });
1033
+ it('should handle GSI1 query error gracefully (fail-open)', async () => {
1034
+ mockedDynamoDbClient.safeQueryByGSI.mockRejectedValueOnce(new Error('GSI query failed'));
1035
+ await expect(IdentityCacheDynamoDbService.deleteIdentityCache(pixelId, { identityId: 'identity456' }, { identityId: 'identity456' })).resolves.toBeUndefined();
1036
+ });
1037
+ });
1038
+ describe('email pointer lookups', () => {
1039
+ const pixelId = 'pixel123';
1040
+ it('should only lookup first email (optimization)', async () => {
1041
+ mockedDynamoDbClient.safeGet.mockResolvedValueOnce(null);
1042
+ await IdentityCacheDynamoDbService.getIdentityFromCache(pixelId, {
1043
+ traits: { emails: ['first@email.com', 'second@email.com', 'third@email.com'] },
1044
+ });
1045
+ expect(mockedDynamoDbClient.safeGet).toHaveBeenCalledTimes(1);
1046
+ expect(mockedDynamoDbClient.safeGet).toHaveBeenCalledWith(expect.any(String), 'pk', `email#${pixelId}#first@email.com`);
1047
+ });
1048
+ it('should handle whitespace in email during lookup', async () => {
1049
+ mockedDynamoDbClient.safeGet.mockResolvedValueOnce(null);
1050
+ await IdentityCacheDynamoDbService.getIdentityFromCache(pixelId, {
1051
+ traits: { emails: [' test@email.com '] },
1052
+ });
1053
+ expect(mockedDynamoDbClient.safeGet).toHaveBeenCalled();
1054
+ });
1055
+ });
1056
+ describe('fail-open behavior comprehensive', () => {
1057
+ const pixelId = 'pixel123';
1058
+ const lambdaArn = 'arn:aws:lambda:us-east-1:123456789:function:identity-private';
1059
+ it('should return incoming identity when Neptune throws (fail-open fallback)', async () => {
1060
+ const incomingIdentity = {
1061
+ identityId: 'identity456',
1062
+ traits: { emails: ['test@email.com'] },
1063
+ };
1064
+ mockedDynamoDbClient.safeGet.mockRejectedValueOnce(new Error('DynamoDB down'));
1065
+ mockInvokeFunction.mockRejectedValueOnce(new Error('Lambda down'));
1066
+ const result = await IdentityCacheDynamoDbService.getIdentityWithCaching(pixelId, incomingIdentity, lambdaArn);
1067
+ expect(result).toEqual(incomingIdentity);
1068
+ });
1069
+ it('should continue when cache write fails after Neptune success', async () => {
1070
+ mockedDynamoDbClient.safeGet.mockResolvedValueOnce(null);
1071
+ mockInvokeFunction.mockResolvedValueOnce({
1072
+ statusCode: 200,
1073
+ body: JSON.stringify({
1074
+ identity: { identityId: 'resolved-id', traits: { emails: ['test@email.com'] } },
1075
+ }),
1076
+ });
1077
+ mockedDynamoDbClient.safeBatchWrite.mockRejectedValueOnce(new Error('Write failed'));
1078
+ const result = await IdentityCacheDynamoDbService.getIdentityWithCaching(pixelId, { traits: { emails: ['test@email.com'] } }, lambdaArn);
1079
+ expect(result?.identityId).toBe('resolved-id');
1080
+ });
1081
+ it('should return incoming identity when Neptune returns non-200', async () => {
1082
+ const incomingIdentity = {
1083
+ identityId: 'incoming-id',
1084
+ traits: { emails: ['test@email.com'] },
1085
+ };
1086
+ mockedDynamoDbClient.safeGet.mockResolvedValueOnce(null);
1087
+ mockInvokeFunction.mockResolvedValueOnce({
1088
+ statusCode: 500,
1089
+ body: 'Internal Server Error',
1090
+ });
1091
+ const result = await IdentityCacheDynamoDbService.getIdentityWithCaching(pixelId, incomingIdentity, lambdaArn);
1092
+ expect(result).toEqual(incomingIdentity);
1093
+ });
1094
+ it('should return incoming identity when Neptune returns malformed JSON (fail-open)', async () => {
1095
+ const incomingIdentity = {
1096
+ identityId: 'test-id',
1097
+ traits: { emails: ['test@email.com'] },
1098
+ };
1099
+ mockedDynamoDbClient.safeGet.mockResolvedValueOnce(null);
1100
+ mockInvokeFunction.mockResolvedValueOnce({
1101
+ statusCode: 200,
1102
+ body: 'not valid json',
1103
+ });
1104
+ const result = await IdentityCacheDynamoDbService.getIdentityWithCaching(pixelId, incomingIdentity, lambdaArn);
1105
+ expect(result).toEqual(incomingIdentity);
1106
+ });
1107
+ it('should return undefined only when both inputs are invalid', async () => {
1108
+ const result = await IdentityCacheDynamoDbService.getIdentityWithCaching(null, null, lambdaArn);
1109
+ expect(result).toBeUndefined();
1110
+ });
1111
+ });
1112
+ describe('gsi1pk attribute on all records', () => {
1113
+ const pixelId = 'pixel123';
1114
+ it('should include gsi1pk on identity record', async () => {
1115
+ const identity = {
1116
+ identityId: 'identity456',
1117
+ traits: { emails: ['test@email.com'] },
1118
+ };
1119
+ await IdentityCacheDynamoDbService.updateIdentityCache(pixelId, identity);
1120
+ const writtenItems = mockedDynamoDbClient.safeBatchWrite.mock.calls[0][1];
1121
+ const identityItem = writtenItems.find((item) => item.pk.startsWith('identity#'));
1122
+ expect(identityItem).toBeDefined();
1123
+ expect(identityItem.gsi1pk).toBe('identity456');
1124
+ });
1125
+ it('should include gsi1pk on all email pointer records', async () => {
1126
+ const identity = {
1127
+ identityId: 'identity456',
1128
+ traits: { emails: ['email1@test.com', 'email2@test.com'] },
1129
+ };
1130
+ await IdentityCacheDynamoDbService.updateIdentityCache(pixelId, identity);
1131
+ const writtenItems = mockedDynamoDbClient.safeBatchWrite.mock.calls[0][1];
1132
+ const emailItems = writtenItems.filter((item) => item.pk.startsWith('email#'));
1133
+ for (const emailItem of emailItems) {
1134
+ expect(emailItem.gsi1pk).toBe('identity456');
1135
+ }
1136
+ });
1137
+ });
1138
+ });
1139
1139
  //# sourceMappingURL=identity-cache-dynamodb-service.spec.js.map