@hexclave/shared 1.0.3 → 1.0.5

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 (343) hide show
  1. package/dist/ai/unified-prompts/reminders.js +4 -3
  2. package/dist/ai/unified-prompts/reminders.js.map +1 -1
  3. package/dist/ai/unified-prompts/skill-site-prompt-parts/ai-setup-prompt.d.ts +3 -1
  4. package/dist/ai/unified-prompts/skill-site-prompt-parts/ai-setup-prompt.d.ts.map +1 -1
  5. package/dist/ai/unified-prompts/skill-site-prompt-parts/ai-setup-prompt.js +216 -29
  6. package/dist/ai/unified-prompts/skill-site-prompt-parts/ai-setup-prompt.js.map +1 -1
  7. package/dist/ai/unified-prompts/skill-site-prompt-parts/docs-index.d.ts.map +1 -1
  8. package/dist/ai/unified-prompts/skill-site-prompt-parts/docs-index.js +1 -0
  9. package/dist/ai/unified-prompts/skill-site-prompt-parts/docs-index.js.map +1 -1
  10. package/dist/ai/unified-prompts/skill-site-prompt-parts/docs-json.generated.d.ts +18 -6
  11. package/dist/ai/unified-prompts/skill-site-prompt-parts/docs-json.generated.js +23 -8
  12. package/dist/ai/unified-prompts/skill-site-prompt-parts/docs-json.generated.js.map +1 -1
  13. package/dist/ai/unified-prompts/skill-site-prompt-parts/openapi-specs.js +1 -1
  14. package/dist/ai/unified-prompts/skill-site-prompt-parts/openapi-specs.js.map +1 -1
  15. package/dist/config/schema.d.ts +71 -71
  16. package/dist/config/schema.d.ts.map +1 -1
  17. package/dist/config/schema.js +5 -5
  18. package/dist/config/schema.js.map +1 -1
  19. package/dist/config-authoring.d.ts +4 -4
  20. package/dist/config-authoring.d.ts.map +1 -1
  21. package/dist/config-authoring.js +2 -2
  22. package/dist/config-authoring.js.map +1 -1
  23. package/dist/config-rendering.d.ts +4 -4
  24. package/dist/config-rendering.d.ts.map +1 -1
  25. package/dist/config-rendering.js +19 -18
  26. package/dist/config-rendering.js.map +1 -1
  27. package/dist/esm/ai/unified-prompts/reminders.js +4 -3
  28. package/dist/esm/ai/unified-prompts/reminders.js.map +1 -1
  29. package/dist/esm/ai/unified-prompts/skill-site-prompt-parts/ai-setup-prompt.d.ts +3 -1
  30. package/dist/esm/ai/unified-prompts/skill-site-prompt-parts/ai-setup-prompt.d.ts.map +1 -1
  31. package/dist/esm/ai/unified-prompts/skill-site-prompt-parts/ai-setup-prompt.js +215 -30
  32. package/dist/esm/ai/unified-prompts/skill-site-prompt-parts/ai-setup-prompt.js.map +1 -1
  33. package/dist/esm/ai/unified-prompts/skill-site-prompt-parts/docs-index.d.ts.map +1 -1
  34. package/dist/esm/ai/unified-prompts/skill-site-prompt-parts/docs-index.js +1 -0
  35. package/dist/esm/ai/unified-prompts/skill-site-prompt-parts/docs-index.js.map +1 -1
  36. package/dist/esm/ai/unified-prompts/skill-site-prompt-parts/docs-json.generated.d.ts +18 -6
  37. package/dist/esm/ai/unified-prompts/skill-site-prompt-parts/docs-json.generated.js +23 -8
  38. package/dist/esm/ai/unified-prompts/skill-site-prompt-parts/docs-json.generated.js.map +1 -1
  39. package/dist/esm/ai/unified-prompts/skill-site-prompt-parts/openapi-specs.js +1 -1
  40. package/dist/esm/ai/unified-prompts/skill-site-prompt-parts/openapi-specs.js.map +1 -1
  41. package/dist/esm/config/schema.d.ts +71 -71
  42. package/dist/esm/config/schema.d.ts.map +1 -1
  43. package/dist/esm/config/schema.js +5 -5
  44. package/dist/esm/config/schema.js.map +1 -1
  45. package/dist/esm/config-authoring.d.ts +4 -4
  46. package/dist/esm/config-authoring.d.ts.map +1 -1
  47. package/dist/esm/config-authoring.js +2 -2
  48. package/dist/esm/config-authoring.js.map +1 -1
  49. package/dist/esm/config-rendering.d.ts +4 -4
  50. package/dist/esm/config-rendering.d.ts.map +1 -1
  51. package/dist/esm/config-rendering.js +15 -14
  52. package/dist/esm/config-rendering.js.map +1 -1
  53. package/dist/esm/helpers/vault/client-side.d.ts.map +1 -1
  54. package/dist/esm/helpers/vault/client-side.js.map +1 -1
  55. package/dist/esm/helpers/vault/server-side.d.ts.map +1 -1
  56. package/dist/esm/helpers/vault/server-side.js.map +1 -1
  57. package/dist/esm/hexclave-config-file.d.ts +15 -0
  58. package/dist/esm/hexclave-config-file.d.ts.map +1 -0
  59. package/dist/esm/{stack-config-file.js → hexclave-config-file.js} +7 -7
  60. package/dist/esm/hexclave-config-file.js.map +1 -0
  61. package/dist/esm/hooks/use-async-callback.js +1 -1
  62. package/dist/esm/hooks/use-async-external-store.js +1 -1
  63. package/dist/esm/hooks/use-strict-memo.js +1 -1
  64. package/dist/esm/interface/admin-metrics.d.ts +4 -4
  65. package/dist/esm/interface/admin-metrics.js.map +1 -1
  66. package/dist/esm/interface/client-interface.js +2 -2
  67. package/dist/esm/interface/client-interface.js.map +1 -1
  68. package/dist/esm/interface/conversations.d.ts +13 -13
  69. package/dist/esm/interface/crud/current-user.d.ts +9 -9
  70. package/dist/esm/interface/crud/email-outbox.d.ts +112 -112
  71. package/dist/esm/interface/crud/products.d.ts +1 -1
  72. package/dist/esm/interface/crud/project-api-keys.d.ts +2 -2
  73. package/dist/esm/interface/crud/project-permissions.js +5 -5
  74. package/dist/esm/interface/crud/project-permissions.js.map +1 -1
  75. package/dist/esm/interface/crud/projects.d.ts +28 -28
  76. package/dist/esm/interface/crud/team-member-profiles.d.ts +22 -22
  77. package/dist/esm/interface/crud/team-permissions.js +5 -5
  78. package/dist/esm/interface/crud/team-permissions.js.map +1 -1
  79. package/dist/esm/interface/crud/users.d.ts +14 -14
  80. package/dist/esm/interface/page-component-versions.js +107 -107
  81. package/dist/esm/interface/page-component-versions.js.map +1 -1
  82. package/dist/esm/interface/webhooks.d.ts +4 -4
  83. package/dist/esm/known-errors.js +4 -4
  84. package/dist/esm/known-errors.js.map +1 -1
  85. package/dist/esm/schema-fields.d.ts +15 -5
  86. package/dist/esm/schema-fields.d.ts.map +1 -1
  87. package/dist/esm/schema-fields.js +26 -16
  88. package/dist/esm/schema-fields.js.map +1 -1
  89. package/dist/esm/utils/crypto.d.ts.map +1 -1
  90. package/dist/esm/utils/crypto.js.map +1 -1
  91. package/dist/esm/utils/currency-constants.d.ts +6 -0
  92. package/dist/esm/utils/currency-constants.d.ts.map +1 -1
  93. package/dist/esm/utils/currency-constants.js.map +1 -1
  94. package/dist/esm/utils/dev-env-state-path.d.ts +3 -3
  95. package/dist/esm/utils/dev-env-state-path.d.ts.map +1 -1
  96. package/dist/esm/utils/dev-env-state-path.js +4 -4
  97. package/dist/esm/utils/dev-env-state-path.js.map +1 -1
  98. package/dist/esm/utils/errors.js +2 -2
  99. package/dist/esm/utils/errors.js.map +1 -1
  100. package/dist/esm/utils/esbuild.d.ts.map +1 -1
  101. package/dist/esm/utils/esbuild.js +0 -1
  102. package/dist/esm/utils/esbuild.js.map +1 -1
  103. package/dist/esm/utils/featurebase.d.ts +3 -3
  104. package/dist/esm/utils/featurebase.d.ts.map +1 -1
  105. package/dist/esm/utils/featurebase.js +15 -15
  106. package/dist/esm/utils/featurebase.js.map +1 -1
  107. package/dist/esm/utils/fs.js +6 -6
  108. package/dist/esm/utils/fs.js.map +1 -1
  109. package/dist/esm/utils/globals.d.ts.map +1 -1
  110. package/dist/esm/utils/globals.js +10 -10
  111. package/dist/esm/utils/globals.js.map +1 -1
  112. package/dist/esm/utils/jwt.d.ts.map +1 -1
  113. package/dist/esm/utils/jwt.js +3 -3
  114. package/dist/esm/utils/jwt.js.map +1 -1
  115. package/dist/esm/utils/redirect-urls.d.ts +3 -3
  116. package/dist/esm/utils/redirect-urls.d.ts.map +1 -1
  117. package/dist/esm/utils/redirect-urls.js +5 -5
  118. package/dist/esm/utils/redirect-urls.js.map +1 -1
  119. package/dist/esm/utils/urls.js.map +1 -1
  120. package/dist/helpers/vault/client-side.d.ts.map +1 -1
  121. package/dist/helpers/vault/client-side.js.map +1 -1
  122. package/dist/helpers/vault/server-side.d.ts.map +1 -1
  123. package/dist/helpers/vault/server-side.js.map +1 -1
  124. package/dist/hexclave-config-file.d.ts +15 -0
  125. package/dist/hexclave-config-file.d.ts.map +1 -0
  126. package/dist/{stack-config-file.js → hexclave-config-file.js} +8 -8
  127. package/dist/hexclave-config-file.js.map +1 -0
  128. package/dist/hooks/use-async-callback.js +1 -1
  129. package/dist/hooks/use-async-external-store.js +1 -1
  130. package/dist/hooks/use-strict-memo.js +1 -1
  131. package/dist/interface/admin-metrics.d.ts +4 -4
  132. package/dist/interface/admin-metrics.js.map +1 -1
  133. package/dist/interface/client-interface.js +2 -2
  134. package/dist/interface/client-interface.js.map +1 -1
  135. package/dist/interface/conversations.d.ts +13 -13
  136. package/dist/interface/crud/current-user.d.ts +9 -9
  137. package/dist/interface/crud/email-outbox.d.ts +112 -112
  138. package/dist/interface/crud/products.d.ts +1 -1
  139. package/dist/interface/crud/project-api-keys.d.ts +2 -2
  140. package/dist/interface/crud/project-permissions.js +5 -5
  141. package/dist/interface/crud/project-permissions.js.map +1 -1
  142. package/dist/interface/crud/projects.d.ts +28 -28
  143. package/dist/interface/crud/team-member-profiles.d.ts +22 -22
  144. package/dist/interface/crud/team-permissions.js +5 -5
  145. package/dist/interface/crud/team-permissions.js.map +1 -1
  146. package/dist/interface/crud/users.d.ts +14 -14
  147. package/dist/interface/page-component-versions.js +107 -107
  148. package/dist/interface/page-component-versions.js.map +1 -1
  149. package/dist/interface/webhooks.d.ts +4 -4
  150. package/dist/known-errors.js +4 -4
  151. package/dist/known-errors.js.map +1 -1
  152. package/dist/schema-fields.d.ts +15 -5
  153. package/dist/schema-fields.d.ts.map +1 -1
  154. package/dist/schema-fields.js +26 -16
  155. package/dist/schema-fields.js.map +1 -1
  156. package/dist/utils/crypto.d.ts.map +1 -1
  157. package/dist/utils/crypto.js.map +1 -1
  158. package/dist/utils/currency-constants.d.ts +6 -0
  159. package/dist/utils/currency-constants.d.ts.map +1 -1
  160. package/dist/utils/currency-constants.js.map +1 -1
  161. package/dist/utils/dev-env-state-path.d.ts +3 -3
  162. package/dist/utils/dev-env-state-path.d.ts.map +1 -1
  163. package/dist/utils/dev-env-state-path.js +5 -5
  164. package/dist/utils/dev-env-state-path.js.map +1 -1
  165. package/dist/utils/errors.js +2 -2
  166. package/dist/utils/errors.js.map +1 -1
  167. package/dist/utils/esbuild.d.ts.map +1 -1
  168. package/dist/utils/esbuild.js +0 -1
  169. package/dist/utils/esbuild.js.map +1 -1
  170. package/dist/utils/featurebase.d.ts +3 -3
  171. package/dist/utils/featurebase.d.ts.map +1 -1
  172. package/dist/utils/featurebase.js +15 -15
  173. package/dist/utils/featurebase.js.map +1 -1
  174. package/dist/utils/fs.js.map +1 -1
  175. package/dist/utils/globals.d.ts.map +1 -1
  176. package/dist/utils/globals.js +10 -10
  177. package/dist/utils/globals.js.map +1 -1
  178. package/dist/utils/jwt.d.ts.map +1 -1
  179. package/dist/utils/jwt.js +3 -3
  180. package/dist/utils/jwt.js.map +1 -1
  181. package/dist/utils/redirect-urls.d.ts +3 -3
  182. package/dist/utils/redirect-urls.d.ts.map +1 -1
  183. package/dist/utils/redirect-urls.js +5 -5
  184. package/dist/utils/redirect-urls.js.map +1 -1
  185. package/dist/utils/urls.js.map +1 -1
  186. package/package.json +2 -1
  187. package/src/ai/image-limits.ts +62 -0
  188. package/src/ai/llms/llms.ts +36 -0
  189. package/src/ai/unified-prompts/reminders.ts +27 -0
  190. package/src/ai/unified-prompts/skill-site-prompt-parts/ai-setup-prompt.ts +1047 -0
  191. package/src/ai/unified-prompts/skill-site-prompt-parts/brain.ts +5 -0
  192. package/src/ai/unified-prompts/skill-site-prompt-parts/cli-help.ts +5 -0
  193. package/src/ai/unified-prompts/skill-site-prompt-parts/config-docs.ts +5 -0
  194. package/src/ai/unified-prompts/skill-site-prompt-parts/custom-components-instructions.ts +5 -0
  195. package/src/ai/unified-prompts/skill-site-prompt-parts/dashboard-instructions.ts +5 -0
  196. package/src/ai/unified-prompts/skill-site-prompt-parts/docs-index.ts +80 -0
  197. package/src/ai/unified-prompts/skill-site-prompt-parts/docs-json.generated.ts +322 -0
  198. package/src/ai/unified-prompts/skill-site-prompt-parts/openapi-specs.ts +5 -0
  199. package/src/ai/unified-prompts/skill-site-prompt-parts/sdk-interface-source.ts +5 -0
  200. package/src/ai/unified-prompts/skill-site-prompt.ts +72 -0
  201. package/src/apps/apps-config.ts +177 -0
  202. package/src/apps/apps-ui.tsx +112 -0
  203. package/src/config/README.md +154 -0
  204. package/src/config/db-sync-mappings.ts +2293 -0
  205. package/src/config/format.ts +388 -0
  206. package/src/config/migrate-catalogs-to-product-lines.ts +475 -0
  207. package/src/config/schema-fuzzer.test.ts +377 -0
  208. package/src/config/schema.ts +1507 -0
  209. package/src/config-authoring.test.ts +52 -0
  210. package/src/config-authoring.ts +37 -0
  211. package/src/config-rendering.ts +142 -0
  212. package/src/crud.tsx +275 -0
  213. package/src/global.d.ts +1 -0
  214. package/src/helpers/emails.ts +250 -0
  215. package/src/helpers/init-prompt.ts +39 -0
  216. package/src/helpers/password.ts +16 -0
  217. package/src/helpers/production-mode.ts +58 -0
  218. package/src/helpers/support-sla.test.ts +124 -0
  219. package/src/helpers/support-sla.ts +93 -0
  220. package/src/helpers/vault/client-side.ts +92 -0
  221. package/src/helpers/vault/server-side.ts +153 -0
  222. package/src/hexclave-config-file.ts +120 -0
  223. package/src/hooks/use-async-callback.tsx +43 -0
  224. package/src/hooks/use-async-external-store.tsx +24 -0
  225. package/src/hooks/use-hash.tsx +14 -0
  226. package/src/hooks/use-hover.tsx +88 -0
  227. package/src/hooks/use-strict-memo.tsx +157 -0
  228. package/src/index.ts +17 -0
  229. package/src/interface/admin-interface.ts +1043 -0
  230. package/src/interface/admin-metrics.ts +214 -0
  231. package/src/interface/client-interface.test.ts +765 -0
  232. package/src/interface/client-interface.ts +2586 -0
  233. package/src/interface/conversations.ts +109 -0
  234. package/src/interface/crud/analytics.ts +11 -0
  235. package/src/interface/crud/connected-accounts.ts +52 -0
  236. package/src/interface/crud/contact-channels.ts +74 -0
  237. package/src/interface/crud/current-user.ts +72 -0
  238. package/src/interface/crud/email-outbox.ts +263 -0
  239. package/src/interface/crud/email-templates.ts +50 -0
  240. package/src/interface/crud/emails.ts +22 -0
  241. package/src/interface/crud/internal-api-keys.ts +69 -0
  242. package/src/interface/crud/invoices.ts +36 -0
  243. package/src/interface/crud/items.ts +24 -0
  244. package/src/interface/crud/notification-preferences.ts +33 -0
  245. package/src/interface/crud/oauth-providers.ts +91 -0
  246. package/src/interface/crud/products.ts +42 -0
  247. package/src/interface/crud/project-api-keys.ts +100 -0
  248. package/src/interface/crud/project-permissions.ts +118 -0
  249. package/src/interface/crud/projects.ts +229 -0
  250. package/src/interface/crud/session-replays.ts +85 -0
  251. package/src/interface/crud/sessions.ts +66 -0
  252. package/src/interface/crud/sign-up-rules.ts +12 -0
  253. package/src/interface/crud/svix-token.ts +20 -0
  254. package/src/interface/crud/team-invitation-details.ts +22 -0
  255. package/src/interface/crud/team-invitation.ts +35 -0
  256. package/src/interface/crud/team-member-profiles.ts +61 -0
  257. package/src/interface/crud/team-memberships.ts +56 -0
  258. package/src/interface/crud/team-permissions.ts +119 -0
  259. package/src/interface/crud/teams.ts +142 -0
  260. package/src/interface/crud/transactions.ts +131 -0
  261. package/src/interface/crud/users.ts +185 -0
  262. package/src/interface/handler-urls.ts +74 -0
  263. package/src/interface/page-component-versions.ts +1702 -0
  264. package/src/interface/server-interface.ts +1146 -0
  265. package/src/interface/webhooks.ts +28 -0
  266. package/src/known-errors.tsx +2031 -0
  267. package/src/local-emulator.ts +5 -0
  268. package/src/payments/payment-methods.ts +167 -0
  269. package/src/payments/stripe-limits.ts +18 -0
  270. package/src/plans.ts +107 -0
  271. package/src/schema-fields.ts +944 -0
  272. package/src/sessions.ts +310 -0
  273. package/src/utils/api-keys.tsx +122 -0
  274. package/src/utils/arrays.tsx +248 -0
  275. package/src/utils/auth-methods.ts +8 -0
  276. package/src/utils/base64.tsx +24 -0
  277. package/src/utils/booleans.tsx +34 -0
  278. package/src/utils/browser-compat.tsx +17 -0
  279. package/src/utils/bytes.tsx +280 -0
  280. package/src/utils/caches.tsx +280 -0
  281. package/src/utils/cel-fields.ts +92 -0
  282. package/src/utils/cloud-hosts.tsx +92 -0
  283. package/src/utils/compile-time.tsx +11 -0
  284. package/src/utils/country-codes.ts +39 -0
  285. package/src/utils/crypto.tsx +174 -0
  286. package/src/utils/currencies.tsx +53 -0
  287. package/src/utils/currency-constants.tsx +51 -0
  288. package/src/utils/dates.tsx +246 -0
  289. package/src/utils/dev-env-state-path.ts +14 -0
  290. package/src/utils/dom.tsx +7 -0
  291. package/src/utils/env.tsx +117 -0
  292. package/src/utils/errors.tsx +284 -0
  293. package/src/utils/esbuild.tsx +235 -0
  294. package/src/utils/featurebase.tsx +247 -0
  295. package/src/utils/fs.tsx +33 -0
  296. package/src/utils/functions.tsx +22 -0
  297. package/src/utils/geo.tsx +16 -0
  298. package/src/utils/globals.tsx +48 -0
  299. package/src/utils/hashes.tsx +52 -0
  300. package/src/utils/html.tsx +60 -0
  301. package/src/utils/http.tsx +91 -0
  302. package/src/utils/ips.tsx +47 -0
  303. package/src/utils/json.tsx +200 -0
  304. package/src/utils/jsx-editable-transpiler.test.ts +156 -0
  305. package/src/utils/jsx-editable-transpiler.tsx +401 -0
  306. package/src/utils/jwt.tsx +170 -0
  307. package/src/utils/locks.tsx +71 -0
  308. package/src/utils/maps.tsx +407 -0
  309. package/src/utils/math.tsx +18 -0
  310. package/src/utils/monkey-patch.tsx +29 -0
  311. package/src/utils/node-http.tsx +47 -0
  312. package/src/utils/numbers.tsx +82 -0
  313. package/src/utils/oauth.tsx +9 -0
  314. package/src/utils/objects.tsx +581 -0
  315. package/src/utils/paginated-lists.test.ts +1039 -0
  316. package/src/utils/paginated-lists.tsx +479 -0
  317. package/src/utils/passkey.tsx +1 -0
  318. package/src/utils/promises.tsx +540 -0
  319. package/src/utils/proxies.tsx +212 -0
  320. package/src/utils/react.tsx +308 -0
  321. package/src/utils/redirect-urls.tsx +299 -0
  322. package/src/utils/regex.tsx +10 -0
  323. package/src/utils/results.tsx +411 -0
  324. package/src/utils/sentry.tsx +20 -0
  325. package/src/utils/stores.tsx +238 -0
  326. package/src/utils/strings.nicify.test.ts +253 -0
  327. package/src/utils/strings.tsx +723 -0
  328. package/src/utils/telemetry.tsx +37 -0
  329. package/src/utils/turnstile-browser.ts +106 -0
  330. package/src/utils/turnstile-flow.test.ts +101 -0
  331. package/src/utils/turnstile-flow.ts +266 -0
  332. package/src/utils/turnstile.ts +34 -0
  333. package/src/utils/typed-arrays.tsx +17 -0
  334. package/src/utils/types.tsx +208 -0
  335. package/src/utils/unicode.tsx +24 -0
  336. package/src/utils/urls.tsx +411 -0
  337. package/src/utils/uuids.tsx +47 -0
  338. package/dist/esm/stack-config-file.d.ts +0 -15
  339. package/dist/esm/stack-config-file.d.ts.map +0 -1
  340. package/dist/esm/stack-config-file.js.map +0 -1
  341. package/dist/stack-config-file.d.ts +0 -15
  342. package/dist/stack-config-file.d.ts.map +0 -1
  343. package/dist/stack-config-file.js.map +0 -1
@@ -0,0 +1,2293 @@
1
+ export const DEFAULT_DB_SYNC_MAPPINGS = {
2
+ "users": {
3
+ sourceTables: { "ProjectUser": "ProjectUser" },
4
+ targetTable: "users",
5
+ targetTableSchemas: {
6
+ postgres: `
7
+ CREATE TABLE IF NOT EXISTS "users" (
8
+ "id" uuid PRIMARY KEY NOT NULL,
9
+ "display_name" text,
10
+ "profile_image_url" text,
11
+ "primary_email" text,
12
+ "primary_email_verified" boolean NOT NULL DEFAULT false,
13
+ "signed_up_at" timestamp without time zone NOT NULL,
14
+ "client_metadata" jsonb NOT NULL DEFAULT '{}'::jsonb,
15
+ "client_read_only_metadata" jsonb NOT NULL DEFAULT '{}'::jsonb,
16
+ "server_metadata" jsonb NOT NULL DEFAULT '{}'::jsonb,
17
+ "is_anonymous" boolean NOT NULL DEFAULT false
18
+ );
19
+ REVOKE ALL ON "users" FROM PUBLIC;
20
+ GRANT SELECT ON "users" TO PUBLIC;
21
+
22
+ CREATE TABLE IF NOT EXISTS "_stack_sync_metadata" (
23
+ "mapping_name" text PRIMARY KEY NOT NULL,
24
+ "last_synced_sequence_id" bigint NOT NULL DEFAULT -1,
25
+ "updated_at" timestamp without time zone NOT NULL DEFAULT now()
26
+ );
27
+ `.trim(),
28
+ clickhouse: `
29
+ CREATE TABLE IF NOT EXISTS analytics_internal.users (
30
+ project_id String,
31
+ branch_id String,
32
+ id UUID,
33
+ display_name Nullable(String),
34
+ profile_image_url Nullable(String),
35
+ primary_email Nullable(String),
36
+ primary_email_verified UInt8,
37
+ signed_up_at DateTime64(3, 'UTC'),
38
+ client_metadata String,
39
+ client_read_only_metadata String,
40
+ server_metadata String,
41
+ is_anonymous UInt8,
42
+ restricted_by_admin UInt8,
43
+ restricted_by_admin_reason Nullable(String),
44
+ restricted_by_admin_private_details Nullable(String),
45
+ sync_sequence_id Int64,
46
+ sync_is_deleted UInt8,
47
+ sync_created_at DateTime64(3, 'UTC') DEFAULT now64(3)
48
+ )
49
+ ENGINE ReplacingMergeTree(sync_sequence_id)
50
+ PARTITION BY toYYYYMM(signed_up_at)
51
+ ORDER BY (project_id, branch_id, id);
52
+
53
+ CREATE TABLE IF NOT EXISTS analytics_internal._stack_sync_metadata (
54
+ tenancy_id UUID,
55
+ mapping_name String,
56
+ last_synced_sequence_id Int64,
57
+ updated_at DateTime64(3, 'UTC') DEFAULT now64(3)
58
+ )
59
+ ENGINE ReplacingMergeTree(updated_at)
60
+ ORDER BY (tenancy_id, mapping_name);
61
+ `.trim(),
62
+ },
63
+ internalDbFetchQueries: {
64
+ clickhouse: `
65
+ SELECT *
66
+ FROM (
67
+ SELECT
68
+ "Tenancy"."projectId" AS "project_id",
69
+ "Tenancy"."branchId" AS "branch_id",
70
+ "ProjectUser"."projectUserId" AS "id",
71
+ "ProjectUser"."displayName" AS "display_name",
72
+ "ProjectUser"."profileImageUrl" AS "profile_image_url",
73
+ (
74
+ SELECT "ContactChannel"."value"
75
+ FROM "ContactChannel"
76
+ WHERE "ContactChannel"."projectUserId" = "ProjectUser"."projectUserId"
77
+ AND "ContactChannel"."tenancyId" = "ProjectUser"."tenancyId"
78
+ AND "ContactChannel"."type" = 'EMAIL'
79
+ AND "ContactChannel"."isPrimary" = 'TRUE'
80
+ LIMIT 1
81
+ ) AS "primary_email",
82
+ COALESCE(
83
+ (
84
+ SELECT "ContactChannel"."isVerified"
85
+ FROM "ContactChannel"
86
+ WHERE "ContactChannel"."projectUserId" = "ProjectUser"."projectUserId"
87
+ AND "ContactChannel"."tenancyId" = "ProjectUser"."tenancyId"
88
+ AND "ContactChannel"."type" = 'EMAIL'
89
+ AND "ContactChannel"."isPrimary" = 'TRUE'
90
+ LIMIT 1
91
+ ),
92
+ false
93
+ ) AS "primary_email_verified",
94
+ COALESCE("ProjectUser"."signedUpAt", "ProjectUser"."createdAt") AS "signed_up_at",
95
+ COALESCE("ProjectUser"."clientMetadata", '{}'::jsonb) AS "client_metadata",
96
+ COALESCE("ProjectUser"."clientReadOnlyMetadata", '{}'::jsonb) AS "client_read_only_metadata",
97
+ COALESCE("ProjectUser"."serverMetadata", '{}'::jsonb) AS "server_metadata",
98
+ "ProjectUser"."isAnonymous" AS "is_anonymous",
99
+ "ProjectUser"."restrictedByAdmin" AS "restricted_by_admin",
100
+ "ProjectUser"."restrictedByAdminReason" AS "restricted_by_admin_reason",
101
+ "ProjectUser"."restrictedByAdminPrivateDetails" AS "restricted_by_admin_private_details",
102
+ "ProjectUser"."sequenceId" AS "sync_sequence_id",
103
+ "ProjectUser"."tenancyId" AS "tenancyId",
104
+ false AS "sync_is_deleted"
105
+ FROM "ProjectUser"
106
+ JOIN "Tenancy" ON "Tenancy"."id" = "ProjectUser"."tenancyId"
107
+ WHERE "ProjectUser"."tenancyId" = $1::uuid
108
+
109
+ UNION ALL
110
+
111
+ SELECT
112
+ "Tenancy"."projectId" AS "project_id",
113
+ "Tenancy"."branchId" AS "branch_id",
114
+ ("DeletedRow"."primaryKey"->>'projectUserId')::uuid AS "id",
115
+ NULL::text AS "display_name",
116
+ NULL::text AS "profile_image_url",
117
+ NULL::text AS "primary_email",
118
+ false AS "primary_email_verified",
119
+ "DeletedRow"."deletedAt"::timestamp without time zone AS "signed_up_at",
120
+ '{}'::jsonb AS "client_metadata",
121
+ '{}'::jsonb AS "client_read_only_metadata",
122
+ '{}'::jsonb AS "server_metadata",
123
+ false AS "is_anonymous",
124
+ false AS "restricted_by_admin",
125
+ NULL::text AS "restricted_by_admin_reason",
126
+ NULL::text AS "restricted_by_admin_private_details",
127
+ "DeletedRow"."sequenceId" AS "sync_sequence_id",
128
+ "DeletedRow"."tenancyId" AS "tenancyId",
129
+ true AS "sync_is_deleted"
130
+ FROM "DeletedRow"
131
+ JOIN "Tenancy" ON "Tenancy"."id" = "DeletedRow"."tenancyId"
132
+ WHERE
133
+ "DeletedRow"."tenancyId" = $1::uuid
134
+ AND "DeletedRow"."tableName" = 'ProjectUser'
135
+ ) AS "_src"
136
+ WHERE "sync_sequence_id" IS NOT NULL
137
+ AND "sync_sequence_id" > $2::bigint
138
+ ORDER BY "sync_sequence_id" ASC
139
+ LIMIT 1000
140
+ `.trim(),
141
+ },
142
+ internalDbFetchQuery: `
143
+ SELECT *
144
+ FROM (
145
+ SELECT
146
+ "ProjectUser"."projectUserId" AS "id",
147
+ "ProjectUser"."displayName" AS "display_name",
148
+ "ProjectUser"."profileImageUrl" AS "profile_image_url",
149
+ (
150
+ SELECT "ContactChannel"."value"
151
+ FROM "ContactChannel"
152
+ WHERE "ContactChannel"."projectUserId" = "ProjectUser"."projectUserId"
153
+ AND "ContactChannel"."tenancyId" = "ProjectUser"."tenancyId"
154
+ AND "ContactChannel"."type" = 'EMAIL'
155
+ AND "ContactChannel"."isPrimary" = 'TRUE'
156
+ LIMIT 1
157
+ ) AS "primary_email",
158
+ COALESCE(
159
+ (
160
+ SELECT "ContactChannel"."isVerified"
161
+ FROM "ContactChannel"
162
+ WHERE "ContactChannel"."projectUserId" = "ProjectUser"."projectUserId"
163
+ AND "ContactChannel"."tenancyId" = "ProjectUser"."tenancyId"
164
+ AND "ContactChannel"."type" = 'EMAIL'
165
+ AND "ContactChannel"."isPrimary" = 'TRUE'
166
+ LIMIT 1
167
+ ),
168
+ false
169
+ ) AS "primary_email_verified",
170
+ COALESCE("ProjectUser"."signedUpAt", "ProjectUser"."createdAt") AS "signed_up_at",
171
+ COALESCE("ProjectUser"."clientMetadata", '{}'::jsonb) AS "client_metadata",
172
+ COALESCE("ProjectUser"."clientReadOnlyMetadata", '{}'::jsonb) AS "client_read_only_metadata",
173
+ COALESCE("ProjectUser"."serverMetadata", '{}'::jsonb) AS "server_metadata",
174
+ "ProjectUser"."isAnonymous" AS "is_anonymous",
175
+ "ProjectUser"."sequenceId" AS "sequence_id",
176
+ "ProjectUser"."tenancyId",
177
+ false AS "is_deleted"
178
+ FROM "ProjectUser"
179
+ WHERE "ProjectUser"."tenancyId" = $1::uuid
180
+
181
+ UNION ALL
182
+
183
+ SELECT
184
+ ("DeletedRow"."primaryKey"->>'projectUserId')::uuid AS "id",
185
+ NULL::text AS "display_name",
186
+ NULL::text AS "profile_image_url",
187
+ NULL::text AS "primary_email",
188
+ false AS "primary_email_verified",
189
+ "DeletedRow"."deletedAt"::timestamp without time zone AS "signed_up_at",
190
+ '{}'::jsonb AS "client_metadata",
191
+ '{}'::jsonb AS "client_read_only_metadata",
192
+ '{}'::jsonb AS "server_metadata",
193
+ false AS "is_anonymous",
194
+ "DeletedRow"."sequenceId" AS "sequence_id",
195
+ "DeletedRow"."tenancyId",
196
+ true AS "is_deleted"
197
+ FROM "DeletedRow"
198
+ WHERE
199
+ "DeletedRow"."tenancyId" = $1::uuid
200
+ AND "DeletedRow"."tableName" = 'ProjectUser'
201
+ ) AS "_src"
202
+ WHERE "sequence_id" IS NOT NULL
203
+ AND "sequence_id" > $2::bigint
204
+ ORDER BY "sequence_id" ASC
205
+ LIMIT 1000
206
+ `.trim(),
207
+ // Last parameter = mapping_name (for metadata tracking)
208
+ externalDbUpdateQueries: {
209
+ postgres: `
210
+ WITH params AS (
211
+ SELECT
212
+ $1::uuid AS "id",
213
+ $2::text AS "display_name",
214
+ $3::text AS "profile_image_url",
215
+ $4::text AS "primary_email",
216
+ $5::boolean AS "primary_email_verified",
217
+ $6::timestamp without time zone AS "signed_up_at",
218
+ $7::jsonb AS "client_metadata",
219
+ $8::jsonb AS "client_read_only_metadata",
220
+ $9::jsonb AS "server_metadata",
221
+ $10::boolean AS "is_anonymous",
222
+ $11::bigint AS "sequence_id",
223
+ $12::boolean AS "is_deleted",
224
+ $13::text AS "mapping_name"
225
+ ),
226
+ deleted AS (
227
+ DELETE FROM "users" u
228
+ USING params p
229
+ WHERE p."is_deleted" = true AND u."id" = p."id"
230
+ RETURNING 1
231
+ ),
232
+ upserted AS (
233
+ INSERT INTO "users" (
234
+ "id",
235
+ "display_name",
236
+ "profile_image_url",
237
+ "primary_email",
238
+ "primary_email_verified",
239
+ "signed_up_at",
240
+ "client_metadata",
241
+ "client_read_only_metadata",
242
+ "server_metadata",
243
+ "is_anonymous"
244
+ )
245
+ SELECT
246
+ p."id",
247
+ p."display_name",
248
+ p."profile_image_url",
249
+ p."primary_email",
250
+ p."primary_email_verified",
251
+ p."signed_up_at",
252
+ p."client_metadata",
253
+ p."client_read_only_metadata",
254
+ p."server_metadata",
255
+ p."is_anonymous"
256
+ FROM params p
257
+ WHERE p."is_deleted" = false
258
+ ON CONFLICT ("id") DO UPDATE SET
259
+ "display_name" = EXCLUDED."display_name",
260
+ "profile_image_url" = EXCLUDED."profile_image_url",
261
+ "primary_email" = EXCLUDED."primary_email",
262
+ "primary_email_verified" = EXCLUDED."primary_email_verified",
263
+ "signed_up_at" = EXCLUDED."signed_up_at",
264
+ "client_metadata" = EXCLUDED."client_metadata",
265
+ "client_read_only_metadata" = EXCLUDED."client_read_only_metadata",
266
+ "server_metadata" = EXCLUDED."server_metadata",
267
+ "is_anonymous" = EXCLUDED."is_anonymous"
268
+ RETURNING 1
269
+ )
270
+ INSERT INTO "_stack_sync_metadata" ("mapping_name", "last_synced_sequence_id", "updated_at")
271
+ SELECT p."mapping_name", p."sequence_id", now() FROM params p
272
+ ON CONFLICT ("mapping_name") DO UPDATE SET
273
+ "last_synced_sequence_id" = GREATEST("_stack_sync_metadata"."last_synced_sequence_id", EXCLUDED."last_synced_sequence_id"),
274
+ "updated_at" = now();
275
+ `.trim(),
276
+ },
277
+ },
278
+ "contact_channels": {
279
+ sourceTables: { "ContactChannel": "ContactChannel" },
280
+ targetTable: "contact_channels",
281
+ targetTableSchemas: {
282
+ postgres: `
283
+ CREATE TABLE IF NOT EXISTS "contact_channels" (
284
+ "id" uuid PRIMARY KEY NOT NULL,
285
+ "user_id" uuid NOT NULL,
286
+ "type" text NOT NULL,
287
+ "value" text NOT NULL,
288
+ "is_primary" boolean NOT NULL DEFAULT false,
289
+ "is_verified" boolean NOT NULL DEFAULT false,
290
+ "used_for_auth" boolean NOT NULL DEFAULT false,
291
+ "created_at" timestamp without time zone NOT NULL
292
+ );
293
+ REVOKE ALL ON "contact_channels" FROM PUBLIC;
294
+ GRANT SELECT ON "contact_channels" TO PUBLIC;
295
+
296
+ CREATE TABLE IF NOT EXISTS "_stack_sync_metadata" (
297
+ "mapping_name" text PRIMARY KEY NOT NULL,
298
+ "last_synced_sequence_id" bigint NOT NULL DEFAULT -1,
299
+ "updated_at" timestamp without time zone NOT NULL DEFAULT now()
300
+ );
301
+ `.trim(),
302
+ clickhouse: `
303
+ CREATE TABLE IF NOT EXISTS analytics_internal.contact_channels (
304
+ project_id String,
305
+ branch_id String,
306
+ id UUID,
307
+ user_id UUID,
308
+ type LowCardinality(String),
309
+ value String,
310
+ is_primary UInt8,
311
+ is_verified UInt8,
312
+ used_for_auth UInt8,
313
+ created_at DateTime64(3, 'UTC'),
314
+ sync_sequence_id Int64,
315
+ sync_is_deleted UInt8,
316
+ sync_created_at DateTime64(3, 'UTC') DEFAULT now64(3)
317
+ )
318
+ ENGINE ReplacingMergeTree(sync_sequence_id)
319
+ PARTITION BY toYYYYMM(created_at)
320
+ ORDER BY (project_id, branch_id, id);
321
+ `.trim(),
322
+ },
323
+ internalDbFetchQueries: {
324
+ clickhouse: `
325
+ SELECT *
326
+ FROM (
327
+ SELECT
328
+ "Tenancy"."projectId" AS "project_id",
329
+ "Tenancy"."branchId" AS "branch_id",
330
+ "ContactChannel"."id" AS "id",
331
+ "ContactChannel"."projectUserId" AS "user_id",
332
+ "ContactChannel"."type"::text AS "type",
333
+ "ContactChannel"."value" AS "value",
334
+ CASE WHEN "ContactChannel"."isPrimary" = 'TRUE' THEN true ELSE false END AS "is_primary",
335
+ "ContactChannel"."isVerified" AS "is_verified",
336
+ CASE WHEN "ContactChannel"."usedForAuth" = 'TRUE' THEN true ELSE false END AS "used_for_auth",
337
+ "ContactChannel"."createdAt" AS "created_at",
338
+ "ContactChannel"."sequenceId" AS "sync_sequence_id",
339
+ "ContactChannel"."tenancyId" AS "tenancyId",
340
+ false AS "sync_is_deleted"
341
+ FROM "ContactChannel"
342
+ JOIN "Tenancy" ON "Tenancy"."id" = "ContactChannel"."tenancyId"
343
+ WHERE "ContactChannel"."tenancyId" = $1::uuid
344
+
345
+ UNION ALL
346
+
347
+ SELECT
348
+ "Tenancy"."projectId" AS "project_id",
349
+ "Tenancy"."branchId" AS "branch_id",
350
+ ("DeletedRow"."primaryKey"->>'id')::uuid AS "id",
351
+ ("DeletedRow"."primaryKey"->>'projectUserId')::uuid AS "user_id",
352
+ NULL::text AS "type",
353
+ NULL::text AS "value",
354
+ false AS "is_primary",
355
+ false AS "is_verified",
356
+ false AS "used_for_auth",
357
+ "DeletedRow"."deletedAt"::timestamp without time zone AS "created_at",
358
+ "DeletedRow"."sequenceId" AS "sync_sequence_id",
359
+ "DeletedRow"."tenancyId" AS "tenancyId",
360
+ true AS "sync_is_deleted"
361
+ FROM "DeletedRow"
362
+ JOIN "Tenancy" ON "Tenancy"."id" = "DeletedRow"."tenancyId"
363
+ WHERE
364
+ "DeletedRow"."tenancyId" = $1::uuid
365
+ AND "DeletedRow"."tableName" = 'ContactChannel'
366
+ ) AS "_src"
367
+ WHERE "sync_sequence_id" IS NOT NULL
368
+ AND "sync_sequence_id" > $2::bigint
369
+ ORDER BY "sync_sequence_id" ASC
370
+ LIMIT 1000
371
+ `.trim(),
372
+ },
373
+ internalDbFetchQuery: `
374
+ SELECT *
375
+ FROM (
376
+ SELECT
377
+ "ContactChannel"."id" AS "id",
378
+ "ContactChannel"."projectUserId" AS "user_id",
379
+ "ContactChannel"."type"::text AS "type",
380
+ "ContactChannel"."value" AS "value",
381
+ CASE WHEN "ContactChannel"."isPrimary" = 'TRUE' THEN true ELSE false END AS "is_primary",
382
+ "ContactChannel"."isVerified" AS "is_verified",
383
+ CASE WHEN "ContactChannel"."usedForAuth" = 'TRUE' THEN true ELSE false END AS "used_for_auth",
384
+ "ContactChannel"."createdAt" AS "created_at",
385
+ "ContactChannel"."sequenceId" AS "sequence_id",
386
+ "ContactChannel"."tenancyId",
387
+ false AS "is_deleted"
388
+ FROM "ContactChannel"
389
+ WHERE "ContactChannel"."tenancyId" = $1::uuid
390
+
391
+ UNION ALL
392
+
393
+ SELECT
394
+ ("DeletedRow"."primaryKey"->>'id')::uuid AS "id",
395
+ ("DeletedRow"."primaryKey"->>'projectUserId')::uuid AS "user_id",
396
+ NULL::text AS "type",
397
+ NULL::text AS "value",
398
+ false AS "is_primary",
399
+ false AS "is_verified",
400
+ false AS "used_for_auth",
401
+ "DeletedRow"."deletedAt"::timestamp without time zone AS "created_at",
402
+ "DeletedRow"."sequenceId" AS "sequence_id",
403
+ "DeletedRow"."tenancyId",
404
+ true AS "is_deleted"
405
+ FROM "DeletedRow"
406
+ WHERE
407
+ "DeletedRow"."tenancyId" = $1::uuid
408
+ AND "DeletedRow"."tableName" = 'ContactChannel'
409
+ ) AS "_src"
410
+ WHERE "sequence_id" IS NOT NULL
411
+ AND "sequence_id" > $2::bigint
412
+ ORDER BY "sequence_id" ASC
413
+ LIMIT 1000
414
+ `.trim(),
415
+ externalDbUpdateQueries: {
416
+ postgres: `
417
+ WITH params AS (
418
+ SELECT
419
+ $1::uuid AS "id",
420
+ $2::uuid AS "user_id",
421
+ $3::text AS "type",
422
+ $4::text AS "value",
423
+ $5::boolean AS "is_primary",
424
+ $6::boolean AS "is_verified",
425
+ $7::boolean AS "used_for_auth",
426
+ $8::timestamp without time zone AS "created_at",
427
+ $9::bigint AS "sequence_id",
428
+ $10::boolean AS "is_deleted",
429
+ $11::text AS "mapping_name"
430
+ ),
431
+ deleted AS (
432
+ DELETE FROM "contact_channels" c
433
+ USING params p
434
+ WHERE p."is_deleted" = true AND c."id" = p."id"
435
+ RETURNING 1
436
+ ),
437
+ upserted AS (
438
+ INSERT INTO "contact_channels" (
439
+ "id",
440
+ "user_id",
441
+ "type",
442
+ "value",
443
+ "is_primary",
444
+ "is_verified",
445
+ "used_for_auth",
446
+ "created_at"
447
+ )
448
+ SELECT
449
+ p."id",
450
+ p."user_id",
451
+ p."type",
452
+ p."value",
453
+ p."is_primary",
454
+ p."is_verified",
455
+ p."used_for_auth",
456
+ p."created_at"
457
+ FROM params p
458
+ WHERE p."is_deleted" = false
459
+ ON CONFLICT ("id") DO UPDATE SET
460
+ "user_id" = EXCLUDED."user_id",
461
+ "type" = EXCLUDED."type",
462
+ "value" = EXCLUDED."value",
463
+ "is_primary" = EXCLUDED."is_primary",
464
+ "is_verified" = EXCLUDED."is_verified",
465
+ "used_for_auth" = EXCLUDED."used_for_auth",
466
+ "created_at" = EXCLUDED."created_at"
467
+ RETURNING 1
468
+ )
469
+ INSERT INTO "_stack_sync_metadata" ("mapping_name", "last_synced_sequence_id", "updated_at")
470
+ SELECT p."mapping_name", p."sequence_id", now() FROM params p
471
+ ON CONFLICT ("mapping_name") DO UPDATE SET
472
+ "last_synced_sequence_id" = GREATEST("_stack_sync_metadata"."last_synced_sequence_id", EXCLUDED."last_synced_sequence_id"),
473
+ "updated_at" = now();
474
+ `.trim(),
475
+ },
476
+ },
477
+ "teams": {
478
+ sourceTables: { "Team": "Team" },
479
+ targetTable: "teams",
480
+ targetTableSchemas: {
481
+ postgres: `
482
+ CREATE TABLE IF NOT EXISTS "teams" (
483
+ "id" uuid PRIMARY KEY NOT NULL,
484
+ "display_name" text NOT NULL,
485
+ "profile_image_url" text,
486
+ "created_at" timestamp without time zone NOT NULL,
487
+ "client_metadata" jsonb NOT NULL DEFAULT '{}'::jsonb,
488
+ "client_read_only_metadata" jsonb NOT NULL DEFAULT '{}'::jsonb,
489
+ "server_metadata" jsonb NOT NULL DEFAULT '{}'::jsonb
490
+ );
491
+ REVOKE ALL ON "teams" FROM PUBLIC;
492
+ GRANT SELECT ON "teams" TO PUBLIC;
493
+
494
+ CREATE TABLE IF NOT EXISTS "_stack_sync_metadata" (
495
+ "mapping_name" text PRIMARY KEY NOT NULL,
496
+ "last_synced_sequence_id" bigint NOT NULL DEFAULT -1,
497
+ "updated_at" timestamp without time zone NOT NULL DEFAULT now()
498
+ );
499
+ `.trim(),
500
+ clickhouse: `
501
+ CREATE TABLE IF NOT EXISTS analytics_internal.teams (
502
+ project_id String,
503
+ branch_id String,
504
+ id UUID,
505
+ display_name String,
506
+ profile_image_url Nullable(String),
507
+ created_at DateTime64(3, 'UTC'),
508
+ client_metadata String,
509
+ client_read_only_metadata String,
510
+ server_metadata String,
511
+ sync_sequence_id Int64,
512
+ sync_is_deleted UInt8,
513
+ sync_created_at DateTime64(3, 'UTC') DEFAULT now64(3)
514
+ )
515
+ ENGINE ReplacingMergeTree(sync_sequence_id)
516
+ PARTITION BY toYYYYMM(created_at)
517
+ ORDER BY (project_id, branch_id, id);
518
+ `.trim(),
519
+ },
520
+ internalDbFetchQueries: {
521
+ clickhouse: `
522
+ SELECT *
523
+ FROM (
524
+ SELECT
525
+ "Tenancy"."projectId" AS "project_id",
526
+ "Tenancy"."branchId" AS "branch_id",
527
+ "Team"."teamId" AS "id",
528
+ "Team"."displayName" AS "display_name",
529
+ "Team"."profileImageUrl" AS "profile_image_url",
530
+ "Team"."createdAt" AS "created_at",
531
+ COALESCE("Team"."clientMetadata", '{}'::jsonb) AS "client_metadata",
532
+ COALESCE("Team"."clientReadOnlyMetadata", '{}'::jsonb) AS "client_read_only_metadata",
533
+ COALESCE("Team"."serverMetadata", '{}'::jsonb) AS "server_metadata",
534
+ "Team"."sequenceId" AS "sync_sequence_id",
535
+ "Team"."tenancyId" AS "tenancyId",
536
+ false AS "sync_is_deleted"
537
+ FROM "Team"
538
+ JOIN "Tenancy" ON "Tenancy"."id" = "Team"."tenancyId"
539
+ WHERE "Team"."tenancyId" = $1::uuid
540
+
541
+ UNION ALL
542
+
543
+ SELECT
544
+ "Tenancy"."projectId" AS "project_id",
545
+ "Tenancy"."branchId" AS "branch_id",
546
+ ("DeletedRow"."primaryKey"->>'teamId')::uuid AS "id",
547
+ NULL::text AS "display_name",
548
+ NULL::text AS "profile_image_url",
549
+ "DeletedRow"."deletedAt"::timestamp without time zone AS "created_at",
550
+ '{}'::jsonb AS "client_metadata",
551
+ '{}'::jsonb AS "client_read_only_metadata",
552
+ '{}'::jsonb AS "server_metadata",
553
+ "DeletedRow"."sequenceId" AS "sync_sequence_id",
554
+ "DeletedRow"."tenancyId" AS "tenancyId",
555
+ true AS "sync_is_deleted"
556
+ FROM "DeletedRow"
557
+ JOIN "Tenancy" ON "Tenancy"."id" = "DeletedRow"."tenancyId"
558
+ WHERE
559
+ "DeletedRow"."tenancyId" = $1::uuid
560
+ AND "DeletedRow"."tableName" = 'Team'
561
+ ) AS "_src"
562
+ WHERE "sync_sequence_id" IS NOT NULL
563
+ AND "sync_sequence_id" > $2::bigint
564
+ ORDER BY "sync_sequence_id" ASC
565
+ LIMIT 1000
566
+ `.trim(),
567
+ },
568
+ internalDbFetchQuery: `
569
+ SELECT *
570
+ FROM (
571
+ SELECT
572
+ "Team"."teamId" AS "id",
573
+ "Team"."displayName" AS "display_name",
574
+ "Team"."profileImageUrl" AS "profile_image_url",
575
+ "Team"."createdAt" AS "created_at",
576
+ COALESCE("Team"."clientMetadata", '{}'::jsonb) AS "client_metadata",
577
+ COALESCE("Team"."clientReadOnlyMetadata", '{}'::jsonb) AS "client_read_only_metadata",
578
+ COALESCE("Team"."serverMetadata", '{}'::jsonb) AS "server_metadata",
579
+ "Team"."sequenceId" AS "sequence_id",
580
+ "Team"."tenancyId",
581
+ false AS "is_deleted"
582
+ FROM "Team"
583
+ WHERE "Team"."tenancyId" = $1::uuid
584
+
585
+ UNION ALL
586
+
587
+ SELECT
588
+ ("DeletedRow"."primaryKey"->>'teamId')::uuid AS "id",
589
+ NULL::text AS "display_name",
590
+ NULL::text AS "profile_image_url",
591
+ "DeletedRow"."deletedAt"::timestamp without time zone AS "created_at",
592
+ '{}'::jsonb AS "client_metadata",
593
+ '{}'::jsonb AS "client_read_only_metadata",
594
+ '{}'::jsonb AS "server_metadata",
595
+ "DeletedRow"."sequenceId" AS "sequence_id",
596
+ "DeletedRow"."tenancyId",
597
+ true AS "is_deleted"
598
+ FROM "DeletedRow"
599
+ WHERE
600
+ "DeletedRow"."tenancyId" = $1::uuid
601
+ AND "DeletedRow"."tableName" = 'Team'
602
+ ) AS "_src"
603
+ WHERE "sequence_id" IS NOT NULL
604
+ AND "sequence_id" > $2::bigint
605
+ ORDER BY "sequence_id" ASC
606
+ LIMIT 1000
607
+ `.trim(),
608
+ externalDbUpdateQueries: {
609
+ postgres: `
610
+ WITH params AS (
611
+ SELECT
612
+ $1::uuid AS "id",
613
+ $2::text AS "display_name",
614
+ $3::text AS "profile_image_url",
615
+ $4::timestamp without time zone AS "created_at",
616
+ $5::jsonb AS "client_metadata",
617
+ $6::jsonb AS "client_read_only_metadata",
618
+ $7::jsonb AS "server_metadata",
619
+ $8::bigint AS "sequence_id",
620
+ $9::boolean AS "is_deleted",
621
+ $10::text AS "mapping_name"
622
+ ),
623
+ deleted AS (
624
+ DELETE FROM "teams" t
625
+ USING params p
626
+ WHERE p."is_deleted" = true AND t."id" = p."id"
627
+ RETURNING 1
628
+ ),
629
+ upserted AS (
630
+ INSERT INTO "teams" (
631
+ "id",
632
+ "display_name",
633
+ "profile_image_url",
634
+ "created_at",
635
+ "client_metadata",
636
+ "client_read_only_metadata",
637
+ "server_metadata"
638
+ )
639
+ SELECT
640
+ p."id",
641
+ p."display_name",
642
+ p."profile_image_url",
643
+ p."created_at",
644
+ p."client_metadata",
645
+ p."client_read_only_metadata",
646
+ p."server_metadata"
647
+ FROM params p
648
+ WHERE p."is_deleted" = false
649
+ ON CONFLICT ("id") DO UPDATE SET
650
+ "display_name" = EXCLUDED."display_name",
651
+ "profile_image_url" = EXCLUDED."profile_image_url",
652
+ "created_at" = EXCLUDED."created_at",
653
+ "client_metadata" = EXCLUDED."client_metadata",
654
+ "client_read_only_metadata" = EXCLUDED."client_read_only_metadata",
655
+ "server_metadata" = EXCLUDED."server_metadata"
656
+ RETURNING 1
657
+ )
658
+ INSERT INTO "_stack_sync_metadata" ("mapping_name", "last_synced_sequence_id", "updated_at")
659
+ SELECT p."mapping_name", p."sequence_id", now() FROM params p
660
+ ON CONFLICT ("mapping_name") DO UPDATE SET
661
+ "last_synced_sequence_id" = GREATEST("_stack_sync_metadata"."last_synced_sequence_id", EXCLUDED."last_synced_sequence_id"),
662
+ "updated_at" = now();
663
+ `.trim(),
664
+ },
665
+ },
666
+ "team_member_profiles": {
667
+ sourceTables: { "TeamMember": "TeamMember" },
668
+ targetTable: "team_member_profiles",
669
+ targetTableSchemas: {
670
+ postgres: `
671
+ CREATE TABLE IF NOT EXISTS "team_member_profiles" (
672
+ "team_id" uuid NOT NULL,
673
+ "user_id" uuid NOT NULL,
674
+ "display_name" text,
675
+ "profile_image_url" text,
676
+ "created_at" timestamp without time zone NOT NULL,
677
+ PRIMARY KEY ("team_id", "user_id")
678
+ );
679
+ REVOKE ALL ON "team_member_profiles" FROM PUBLIC;
680
+ GRANT SELECT ON "team_member_profiles" TO PUBLIC;
681
+
682
+ CREATE TABLE IF NOT EXISTS "_stack_sync_metadata" (
683
+ "mapping_name" text PRIMARY KEY NOT NULL,
684
+ "last_synced_sequence_id" bigint NOT NULL DEFAULT -1,
685
+ "updated_at" timestamp without time zone NOT NULL DEFAULT now()
686
+ );
687
+ `.trim(),
688
+ clickhouse: `
689
+ CREATE TABLE IF NOT EXISTS analytics_internal.team_member_profiles (
690
+ project_id String,
691
+ branch_id String,
692
+ team_id UUID,
693
+ user_id UUID,
694
+ display_name Nullable(String),
695
+ profile_image_url Nullable(String),
696
+ created_at DateTime64(3, 'UTC'),
697
+ sync_sequence_id Int64,
698
+ sync_is_deleted UInt8,
699
+ sync_created_at DateTime64(3, 'UTC') DEFAULT now64(3)
700
+ )
701
+ ENGINE ReplacingMergeTree(sync_sequence_id)
702
+ PARTITION BY toYYYYMM(created_at)
703
+ ORDER BY (project_id, branch_id, team_id, user_id);
704
+ `.trim(),
705
+ },
706
+ internalDbFetchQueries: {
707
+ clickhouse: `
708
+ SELECT *
709
+ FROM (
710
+ SELECT
711
+ "Tenancy"."projectId" AS "project_id",
712
+ "Tenancy"."branchId" AS "branch_id",
713
+ "TeamMember"."teamId" AS "team_id",
714
+ "TeamMember"."projectUserId" AS "user_id",
715
+ "TeamMember"."displayName" AS "display_name",
716
+ "TeamMember"."profileImageUrl" AS "profile_image_url",
717
+ "TeamMember"."createdAt" AS "created_at",
718
+ "TeamMember"."sequenceId" AS "sync_sequence_id",
719
+ "TeamMember"."tenancyId" AS "tenancyId",
720
+ false AS "sync_is_deleted"
721
+ FROM "TeamMember"
722
+ JOIN "Tenancy" ON "Tenancy"."id" = "TeamMember"."tenancyId"
723
+ WHERE "TeamMember"."tenancyId" = $1::uuid
724
+
725
+ UNION ALL
726
+
727
+ SELECT
728
+ "Tenancy"."projectId" AS "project_id",
729
+ "Tenancy"."branchId" AS "branch_id",
730
+ ("DeletedRow"."primaryKey"->>'teamId')::uuid AS "team_id",
731
+ ("DeletedRow"."primaryKey"->>'projectUserId')::uuid AS "user_id",
732
+ NULL::text AS "display_name",
733
+ NULL::text AS "profile_image_url",
734
+ "DeletedRow"."deletedAt"::timestamp without time zone AS "created_at",
735
+ "DeletedRow"."sequenceId" AS "sync_sequence_id",
736
+ "DeletedRow"."tenancyId" AS "tenancyId",
737
+ true AS "sync_is_deleted"
738
+ FROM "DeletedRow"
739
+ JOIN "Tenancy" ON "Tenancy"."id" = "DeletedRow"."tenancyId"
740
+ WHERE
741
+ "DeletedRow"."tenancyId" = $1::uuid
742
+ AND "DeletedRow"."tableName" = 'TeamMember'
743
+ ) AS "_src"
744
+ WHERE "sync_sequence_id" IS NOT NULL
745
+ AND "sync_sequence_id" > $2::bigint
746
+ ORDER BY "sync_sequence_id" ASC
747
+ LIMIT 1000
748
+ `.trim(),
749
+ },
750
+ internalDbFetchQuery: `
751
+ SELECT *
752
+ FROM (
753
+ SELECT
754
+ "TeamMember"."teamId" AS "team_id",
755
+ "TeamMember"."projectUserId" AS "user_id",
756
+ "TeamMember"."displayName" AS "display_name",
757
+ "TeamMember"."profileImageUrl" AS "profile_image_url",
758
+ "TeamMember"."createdAt" AS "created_at",
759
+ "TeamMember"."sequenceId" AS "sequence_id",
760
+ "TeamMember"."tenancyId",
761
+ false AS "is_deleted"
762
+ FROM "TeamMember"
763
+ WHERE "TeamMember"."tenancyId" = $1::uuid
764
+
765
+ UNION ALL
766
+
767
+ SELECT
768
+ ("DeletedRow"."primaryKey"->>'teamId')::uuid AS "team_id",
769
+ ("DeletedRow"."primaryKey"->>'projectUserId')::uuid AS "user_id",
770
+ NULL::text AS "display_name",
771
+ NULL::text AS "profile_image_url",
772
+ "DeletedRow"."deletedAt"::timestamp without time zone AS "created_at",
773
+ "DeletedRow"."sequenceId" AS "sequence_id",
774
+ "DeletedRow"."tenancyId",
775
+ true AS "is_deleted"
776
+ FROM "DeletedRow"
777
+ WHERE
778
+ "DeletedRow"."tenancyId" = $1::uuid
779
+ AND "DeletedRow"."tableName" = 'TeamMember'
780
+ ) AS "_src"
781
+ WHERE "sequence_id" IS NOT NULL
782
+ AND "sequence_id" > $2::bigint
783
+ ORDER BY "sequence_id" ASC
784
+ LIMIT 1000
785
+ `.trim(),
786
+ externalDbUpdateQueries: {
787
+ postgres: `
788
+ WITH params AS (
789
+ SELECT
790
+ $1::uuid AS "team_id",
791
+ $2::uuid AS "user_id",
792
+ $3::text AS "display_name",
793
+ $4::text AS "profile_image_url",
794
+ $5::timestamp without time zone AS "created_at",
795
+ $6::bigint AS "sequence_id",
796
+ $7::boolean AS "is_deleted",
797
+ $8::text AS "mapping_name"
798
+ ),
799
+ deleted AS (
800
+ DELETE FROM "team_member_profiles" tm
801
+ USING params p
802
+ WHERE p."is_deleted" = true AND tm."team_id" = p."team_id" AND tm."user_id" = p."user_id"
803
+ RETURNING 1
804
+ ),
805
+ upserted AS (
806
+ INSERT INTO "team_member_profiles" (
807
+ "team_id",
808
+ "user_id",
809
+ "display_name",
810
+ "profile_image_url",
811
+ "created_at"
812
+ )
813
+ SELECT
814
+ p."team_id",
815
+ p."user_id",
816
+ p."display_name",
817
+ p."profile_image_url",
818
+ p."created_at"
819
+ FROM params p
820
+ WHERE p."is_deleted" = false
821
+ ON CONFLICT ("team_id", "user_id") DO UPDATE SET
822
+ "display_name" = EXCLUDED."display_name",
823
+ "profile_image_url" = EXCLUDED."profile_image_url",
824
+ "created_at" = EXCLUDED."created_at"
825
+ RETURNING 1
826
+ )
827
+ INSERT INTO "_stack_sync_metadata" ("mapping_name", "last_synced_sequence_id", "updated_at")
828
+ SELECT p."mapping_name", p."sequence_id", now() FROM params p
829
+ ON CONFLICT ("mapping_name") DO UPDATE SET
830
+ "last_synced_sequence_id" = GREATEST("_stack_sync_metadata"."last_synced_sequence_id", EXCLUDED."last_synced_sequence_id"),
831
+ "updated_at" = now();
832
+ `.trim(),
833
+ },
834
+ },
835
+ "team_permissions": {
836
+ sourceTables: { "TeamMemberDirectPermission": "TeamMemberDirectPermission" },
837
+ targetTable: "team_permissions",
838
+ targetTableSchemas: {
839
+ postgres: `
840
+ CREATE TABLE IF NOT EXISTS "team_permissions" (
841
+ "team_id" uuid NOT NULL,
842
+ "user_id" uuid NOT NULL,
843
+ "permission_id" text NOT NULL,
844
+ "created_at" timestamp without time zone NOT NULL,
845
+ PRIMARY KEY ("team_id", "user_id", "permission_id")
846
+ );
847
+ REVOKE ALL ON "team_permissions" FROM PUBLIC;
848
+ GRANT SELECT ON "team_permissions" TO PUBLIC;
849
+
850
+ CREATE TABLE IF NOT EXISTS "_stack_sync_metadata" (
851
+ "mapping_name" text PRIMARY KEY NOT NULL,
852
+ "last_synced_sequence_id" bigint NOT NULL DEFAULT -1,
853
+ "updated_at" timestamp without time zone NOT NULL DEFAULT now()
854
+ );
855
+ `.trim(),
856
+ clickhouse: `
857
+ CREATE TABLE IF NOT EXISTS analytics_internal.team_permissions (
858
+ project_id String,
859
+ branch_id String,
860
+ team_id UUID,
861
+ user_id UUID,
862
+ id String,
863
+ created_at DateTime64(3, 'UTC'),
864
+ sync_sequence_id Int64,
865
+ sync_is_deleted UInt8,
866
+ sync_created_at DateTime64(3, 'UTC') DEFAULT now64(3)
867
+ )
868
+ ENGINE ReplacingMergeTree(sync_sequence_id)
869
+ PARTITION BY toYYYYMM(created_at)
870
+ ORDER BY (project_id, branch_id, team_id, user_id, id);
871
+ `.trim(),
872
+ },
873
+ internalDbFetchQueries: {
874
+ clickhouse: `
875
+ SELECT *
876
+ FROM (
877
+ SELECT
878
+ "Tenancy"."projectId" AS "project_id",
879
+ "Tenancy"."branchId" AS "branch_id",
880
+ "TeamMemberDirectPermission"."teamId" AS "team_id",
881
+ "TeamMemberDirectPermission"."projectUserId" AS "user_id",
882
+ "TeamMemberDirectPermission"."permissionId" AS "id",
883
+ "TeamMemberDirectPermission"."createdAt" AS "created_at",
884
+ "TeamMemberDirectPermission"."sequenceId" AS "sync_sequence_id",
885
+ "TeamMemberDirectPermission"."tenancyId" AS "tenancyId",
886
+ false AS "sync_is_deleted"
887
+ FROM "TeamMemberDirectPermission"
888
+ JOIN "Tenancy" ON "Tenancy"."id" = "TeamMemberDirectPermission"."tenancyId"
889
+ WHERE "TeamMemberDirectPermission"."tenancyId" = $1::uuid
890
+
891
+ UNION ALL
892
+
893
+ SELECT
894
+ "Tenancy"."projectId" AS "project_id",
895
+ "Tenancy"."branchId" AS "branch_id",
896
+ ("DeletedRow"."primaryKey"->>'teamId')::uuid AS "team_id",
897
+ ("DeletedRow"."primaryKey"->>'projectUserId')::uuid AS "user_id",
898
+ "DeletedRow"."primaryKey"->>'permissionId' AS "id",
899
+ "DeletedRow"."deletedAt"::timestamp without time zone AS "created_at",
900
+ "DeletedRow"."sequenceId" AS "sync_sequence_id",
901
+ "DeletedRow"."tenancyId" AS "tenancyId",
902
+ true AS "sync_is_deleted"
903
+ FROM "DeletedRow"
904
+ JOIN "Tenancy" ON "Tenancy"."id" = "DeletedRow"."tenancyId"
905
+ WHERE
906
+ "DeletedRow"."tenancyId" = $1::uuid
907
+ AND "DeletedRow"."tableName" = 'TeamMemberDirectPermission'
908
+ ) AS "_src"
909
+ WHERE "sync_sequence_id" IS NOT NULL
910
+ AND "sync_sequence_id" > $2::bigint
911
+ ORDER BY "sync_sequence_id" ASC
912
+ LIMIT 1000
913
+ `.trim(),
914
+ },
915
+ internalDbFetchQuery: `
916
+ SELECT *
917
+ FROM (
918
+ SELECT
919
+ "TeamMemberDirectPermission"."teamId" AS "team_id",
920
+ "TeamMemberDirectPermission"."projectUserId" AS "user_id",
921
+ "TeamMemberDirectPermission"."permissionId" AS "permission_id",
922
+ "TeamMemberDirectPermission"."createdAt" AS "created_at",
923
+ "TeamMemberDirectPermission"."sequenceId" AS "sequence_id",
924
+ "TeamMemberDirectPermission"."tenancyId",
925
+ false AS "is_deleted"
926
+ FROM "TeamMemberDirectPermission"
927
+ WHERE "TeamMemberDirectPermission"."tenancyId" = $1::uuid
928
+
929
+ UNION ALL
930
+
931
+ SELECT
932
+ ("DeletedRow"."primaryKey"->>'teamId')::uuid AS "team_id",
933
+ ("DeletedRow"."primaryKey"->>'projectUserId')::uuid AS "user_id",
934
+ "DeletedRow"."primaryKey"->>'permissionId' AS "id",
935
+ "DeletedRow"."deletedAt"::timestamp without time zone AS "created_at",
936
+ "DeletedRow"."sequenceId" AS "sequence_id",
937
+ "DeletedRow"."tenancyId",
938
+ true AS "is_deleted"
939
+ FROM "DeletedRow"
940
+ WHERE
941
+ "DeletedRow"."tenancyId" = $1::uuid
942
+ AND "DeletedRow"."tableName" = 'TeamMemberDirectPermission'
943
+ ) AS "_src"
944
+ WHERE "sequence_id" IS NOT NULL
945
+ AND "sequence_id" > $2::bigint
946
+ ORDER BY "sequence_id" ASC
947
+ LIMIT 1000
948
+ `.trim(),
949
+ externalDbUpdateQueries: {
950
+ postgres: `
951
+ WITH params AS (
952
+ SELECT
953
+ $1::uuid AS "team_id",
954
+ $2::uuid AS "user_id",
955
+ $3::text AS "permission_id",
956
+ $4::timestamp without time zone AS "created_at",
957
+ $5::bigint AS "sequence_id",
958
+ $6::boolean AS "is_deleted",
959
+ $7::text AS "mapping_name"
960
+ ),
961
+ deleted AS (
962
+ DELETE FROM "team_permissions" tp
963
+ USING params p
964
+ WHERE p."is_deleted" = true AND tp."team_id" = p."team_id" AND tp."user_id" = p."user_id" AND tp."permission_id" = p."permission_id"
965
+ RETURNING 1
966
+ ),
967
+ upserted AS (
968
+ INSERT INTO "team_permissions" (
969
+ "team_id",
970
+ "user_id",
971
+ "permission_id",
972
+ "created_at"
973
+ )
974
+ SELECT
975
+ p."team_id",
976
+ p."user_id",
977
+ p."permission_id",
978
+ p."created_at"
979
+ FROM params p
980
+ WHERE p."is_deleted" = false
981
+ ON CONFLICT ("team_id", "user_id", "permission_id") DO UPDATE SET
982
+ "created_at" = EXCLUDED."created_at"
983
+ RETURNING 1
984
+ )
985
+ INSERT INTO "_stack_sync_metadata" ("mapping_name", "last_synced_sequence_id", "updated_at")
986
+ SELECT p."mapping_name", p."sequence_id", now() FROM params p
987
+ ON CONFLICT ("mapping_name") DO UPDATE SET
988
+ "last_synced_sequence_id" = GREATEST("_stack_sync_metadata"."last_synced_sequence_id", EXCLUDED."last_synced_sequence_id"),
989
+ "updated_at" = now();
990
+ `.trim(),
991
+ },
992
+ },
993
+ "team_invitations": {
994
+ sourceTables: { "VerificationCode": "VerificationCode" },
995
+ targetTable: "team_invitations",
996
+ targetTableSchemas: {
997
+ postgres: `
998
+ CREATE TABLE IF NOT EXISTS "team_invitations" (
999
+ "id" uuid PRIMARY KEY NOT NULL,
1000
+ "team_id" uuid NOT NULL,
1001
+ "team_display_name" text NOT NULL,
1002
+ "recipient_email" text NOT NULL,
1003
+ "expires_at_millis" bigint NOT NULL,
1004
+ "created_at" timestamp without time zone NOT NULL
1005
+ );
1006
+ REVOKE ALL ON "team_invitations" FROM PUBLIC;
1007
+ GRANT SELECT ON "team_invitations" TO PUBLIC;
1008
+
1009
+ CREATE TABLE IF NOT EXISTS "_stack_sync_metadata" (
1010
+ "mapping_name" text PRIMARY KEY NOT NULL,
1011
+ "last_synced_sequence_id" bigint NOT NULL DEFAULT -1,
1012
+ "updated_at" timestamp without time zone NOT NULL DEFAULT now()
1013
+ );
1014
+ `.trim(),
1015
+ clickhouse: `
1016
+ CREATE TABLE IF NOT EXISTS analytics_internal.team_invitations (
1017
+ project_id String,
1018
+ branch_id String,
1019
+ id UUID,
1020
+ team_id UUID,
1021
+ team_display_name String,
1022
+ recipient_email String,
1023
+ expires_at_millis Int64,
1024
+ created_at DateTime64(3, 'UTC'),
1025
+ sync_sequence_id Int64,
1026
+ sync_is_deleted UInt8,
1027
+ sync_created_at DateTime64(3, 'UTC') DEFAULT now64(3)
1028
+ )
1029
+ ENGINE ReplacingMergeTree(sync_sequence_id)
1030
+ PARTITION BY toYYYYMM(created_at)
1031
+ ORDER BY (project_id, branch_id, id);
1032
+ `.trim(),
1033
+ },
1034
+ internalDbFetchQueries: {
1035
+ clickhouse: `
1036
+ SELECT *
1037
+ FROM (
1038
+ SELECT
1039
+ "Tenancy"."projectId" AS "project_id",
1040
+ "Tenancy"."branchId" AS "branch_id",
1041
+ "VerificationCode"."id"::uuid AS "id",
1042
+ ("VerificationCode"."data"->>'team_id')::uuid AS "team_id",
1043
+ "Team"."displayName" AS "team_display_name",
1044
+ "VerificationCode"."method"->>'email' AS "recipient_email",
1045
+ FLOOR(EXTRACT(EPOCH FROM "VerificationCode"."expiresAt") * 1000)::bigint AS "expires_at_millis",
1046
+ "VerificationCode"."createdAt" AS "created_at",
1047
+ "VerificationCode"."sequenceId" AS "sync_sequence_id",
1048
+ "Tenancy"."id" AS "tenancyId",
1049
+ false AS "sync_is_deleted"
1050
+ FROM "VerificationCode"
1051
+ JOIN "Tenancy" ON "Tenancy"."projectId" = "VerificationCode"."projectId"
1052
+ AND "Tenancy"."branchId" = "VerificationCode"."branchId"
1053
+ LEFT JOIN "Team" ON "Team"."teamId" = ("VerificationCode"."data"->>'team_id')::uuid
1054
+ AND "Team"."tenancyId" = "Tenancy"."id"
1055
+ WHERE "Tenancy"."id" = $1::uuid
1056
+ AND "VerificationCode"."type" = 'TEAM_INVITATION'
1057
+
1058
+ UNION ALL
1059
+
1060
+ SELECT
1061
+ "Tenancy"."projectId" AS "project_id",
1062
+ "Tenancy"."branchId" AS "branch_id",
1063
+ ("DeletedRow"."primaryKey"->>'id')::uuid AS "id",
1064
+ '00000000-0000-0000-0000-000000000000'::uuid AS "team_id",
1065
+ ''::text AS "team_display_name",
1066
+ ''::text AS "recipient_email",
1067
+ 0::bigint AS "expires_at_millis",
1068
+ "DeletedRow"."deletedAt"::timestamp without time zone AS "created_at",
1069
+ "DeletedRow"."sequenceId" AS "sync_sequence_id",
1070
+ "DeletedRow"."tenancyId" AS "tenancyId",
1071
+ true AS "sync_is_deleted"
1072
+ FROM "DeletedRow"
1073
+ JOIN "Tenancy" ON "Tenancy"."id" = "DeletedRow"."tenancyId"
1074
+ WHERE
1075
+ "DeletedRow"."tenancyId" = $1::uuid
1076
+ AND "DeletedRow"."tableName" = 'VerificationCode_TEAM_INVITATION'
1077
+ ) AS "_src"
1078
+ WHERE "sync_sequence_id" IS NOT NULL
1079
+ AND "sync_sequence_id" > $2::bigint
1080
+ ORDER BY "sync_sequence_id" ASC
1081
+ LIMIT 1000
1082
+ `.trim(),
1083
+ },
1084
+ internalDbFetchQuery: `
1085
+ SELECT *
1086
+ FROM (
1087
+ SELECT
1088
+ "VerificationCode"."id"::uuid AS "id",
1089
+ ("VerificationCode"."data"->>'team_id')::uuid AS "team_id",
1090
+ "Team"."displayName" AS "team_display_name",
1091
+ "VerificationCode"."method"->>'email' AS "recipient_email",
1092
+ FLOOR(EXTRACT(EPOCH FROM "VerificationCode"."expiresAt") * 1000)::bigint AS "expires_at_millis",
1093
+ "VerificationCode"."createdAt" AS "created_at",
1094
+ "VerificationCode"."sequenceId" AS "sequence_id",
1095
+ "Tenancy"."id" AS "tenancyId",
1096
+ false AS "is_deleted"
1097
+ FROM "VerificationCode"
1098
+ JOIN "Tenancy" ON "Tenancy"."projectId" = "VerificationCode"."projectId"
1099
+ AND "Tenancy"."branchId" = "VerificationCode"."branchId"
1100
+ LEFT JOIN "Team" ON "Team"."teamId" = ("VerificationCode"."data"->>'team_id')::uuid
1101
+ AND "Team"."tenancyId" = "Tenancy"."id"
1102
+ WHERE "Tenancy"."id" = $1::uuid
1103
+ AND "VerificationCode"."type" = 'TEAM_INVITATION'
1104
+
1105
+ UNION ALL
1106
+
1107
+ SELECT
1108
+ ("DeletedRow"."primaryKey"->>'id')::uuid AS "id",
1109
+ '00000000-0000-0000-0000-000000000000'::uuid AS "team_id",
1110
+ ''::text AS "team_display_name",
1111
+ ''::text AS "recipient_email",
1112
+ 0::bigint AS "expires_at_millis",
1113
+ "DeletedRow"."deletedAt"::timestamp without time zone AS "created_at",
1114
+ "DeletedRow"."sequenceId" AS "sequence_id",
1115
+ "DeletedRow"."tenancyId" AS "tenancyId",
1116
+ true AS "is_deleted"
1117
+ FROM "DeletedRow"
1118
+ WHERE
1119
+ "DeletedRow"."tenancyId" = $1::uuid
1120
+ AND "DeletedRow"."tableName" = 'VerificationCode_TEAM_INVITATION'
1121
+ ) AS "_src"
1122
+ WHERE "sequence_id" IS NOT NULL
1123
+ AND "sequence_id" > $2::bigint
1124
+ ORDER BY "sequence_id" ASC
1125
+ LIMIT 1000
1126
+ `.trim(),
1127
+ externalDbUpdateQueries: {
1128
+ postgres: `
1129
+ WITH params AS (
1130
+ SELECT
1131
+ $1::uuid AS "id",
1132
+ $2::uuid AS "team_id",
1133
+ $3::text AS "team_display_name",
1134
+ $4::text AS "recipient_email",
1135
+ $5::bigint AS "expires_at_millis",
1136
+ $6::timestamp without time zone AS "created_at",
1137
+ $7::bigint AS "sequence_id",
1138
+ $8::boolean AS "is_deleted",
1139
+ $9::text AS "mapping_name"
1140
+ ),
1141
+ deleted AS (
1142
+ DELETE FROM "team_invitations" ti
1143
+ USING params p
1144
+ WHERE p."is_deleted" = true AND ti."id" = p."id"
1145
+ RETURNING 1
1146
+ ),
1147
+ upserted AS (
1148
+ INSERT INTO "team_invitations" (
1149
+ "id",
1150
+ "team_id",
1151
+ "team_display_name",
1152
+ "recipient_email",
1153
+ "expires_at_millis",
1154
+ "created_at"
1155
+ )
1156
+ SELECT
1157
+ p."id",
1158
+ p."team_id",
1159
+ p."team_display_name",
1160
+ p."recipient_email",
1161
+ p."expires_at_millis",
1162
+ p."created_at"
1163
+ FROM params p
1164
+ WHERE p."is_deleted" = false
1165
+ ON CONFLICT ("id") DO UPDATE SET
1166
+ "team_id" = EXCLUDED."team_id",
1167
+ "team_display_name" = EXCLUDED."team_display_name",
1168
+ "recipient_email" = EXCLUDED."recipient_email",
1169
+ "expires_at_millis" = EXCLUDED."expires_at_millis",
1170
+ "created_at" = EXCLUDED."created_at"
1171
+ RETURNING 1
1172
+ )
1173
+ INSERT INTO "_stack_sync_metadata" ("mapping_name", "last_synced_sequence_id", "updated_at")
1174
+ SELECT p."mapping_name", p."sequence_id", now() FROM params p
1175
+ ON CONFLICT ("mapping_name") DO UPDATE SET
1176
+ "last_synced_sequence_id" = GREATEST("_stack_sync_metadata"."last_synced_sequence_id", EXCLUDED."last_synced_sequence_id"),
1177
+ "updated_at" = now();
1178
+ `.trim(),
1179
+ },
1180
+ },
1181
+ "email_outboxes": {
1182
+ sourceTables: { "EmailOutbox": "EmailOutbox" },
1183
+ targetTable: "email_outboxes",
1184
+ targetTableSchemas: {
1185
+ postgres: `
1186
+ CREATE TABLE IF NOT EXISTS "email_outboxes" (
1187
+ "id" uuid PRIMARY KEY NOT NULL,
1188
+ "status" text NOT NULL,
1189
+ "simple_status" text NOT NULL,
1190
+ "created_with" text NOT NULL,
1191
+ "email_draft_id" text,
1192
+ "email_programmatic_call_template_id" text,
1193
+ "theme_id" text,
1194
+ "is_high_priority" boolean NOT NULL DEFAULT false,
1195
+ "rendered_is_transactional" boolean,
1196
+ "rendered_subject" text,
1197
+ "rendered_notification_category_id" text,
1198
+ "started_rendering_at" timestamp without time zone,
1199
+ "finished_rendering_at" timestamp without time zone,
1200
+ "render_error" text,
1201
+ "scheduled_at" timestamp without time zone NOT NULL,
1202
+ "created_at" timestamp without time zone NOT NULL,
1203
+ "started_sending_at" timestamp without time zone,
1204
+ "finished_sending_at" timestamp without time zone,
1205
+ "server_error" text,
1206
+ "sent_at" timestamp without time zone,
1207
+ "delivered_at" timestamp without time zone,
1208
+ "opened_at" timestamp without time zone,
1209
+ "clicked_at" timestamp without time zone,
1210
+ "unsubscribed_at" timestamp without time zone,
1211
+ "marked_as_spam_at" timestamp without time zone,
1212
+ "bounced_at" timestamp without time zone,
1213
+ "delivery_delayed_at" timestamp without time zone,
1214
+ "can_have_delivery_info" boolean,
1215
+ "skipped_reason" text,
1216
+ "skipped_details" jsonb,
1217
+ "send_retries" integer NOT NULL DEFAULT 0,
1218
+ "is_paused" boolean NOT NULL DEFAULT false
1219
+ );
1220
+ REVOKE ALL ON "email_outboxes" FROM PUBLIC;
1221
+ GRANT SELECT ON "email_outboxes" TO PUBLIC;
1222
+
1223
+ CREATE TABLE IF NOT EXISTS "_stack_sync_metadata" (
1224
+ "mapping_name" text PRIMARY KEY NOT NULL,
1225
+ "last_synced_sequence_id" bigint NOT NULL DEFAULT -1,
1226
+ "updated_at" timestamp without time zone NOT NULL DEFAULT now()
1227
+ );
1228
+ `.trim(),
1229
+ clickhouse: `
1230
+ CREATE TABLE IF NOT EXISTS analytics_internal.email_outboxes (
1231
+ project_id String,
1232
+ branch_id String,
1233
+ id UUID,
1234
+ status LowCardinality(String),
1235
+ simple_status LowCardinality(String),
1236
+ created_with LowCardinality(String),
1237
+ email_draft_id Nullable(String),
1238
+ email_programmatic_call_template_id Nullable(String),
1239
+ theme_id Nullable(String),
1240
+ is_high_priority UInt8,
1241
+ is_transactional Nullable(UInt8),
1242
+ subject Nullable(String),
1243
+ notification_category_id Nullable(String),
1244
+ started_rendering_at Nullable(DateTime64(3, 'UTC')),
1245
+ rendered_at Nullable(DateTime64(3, 'UTC')),
1246
+ render_error Nullable(String),
1247
+ scheduled_at DateTime64(3, 'UTC'),
1248
+ created_at DateTime64(3, 'UTC'),
1249
+ updated_at DateTime64(3, 'UTC'),
1250
+ started_sending_at Nullable(DateTime64(3, 'UTC')),
1251
+ server_error Nullable(String),
1252
+ delivered_at Nullable(DateTime64(3, 'UTC')),
1253
+ opened_at Nullable(DateTime64(3, 'UTC')),
1254
+ clicked_at Nullable(DateTime64(3, 'UTC')),
1255
+ unsubscribed_at Nullable(DateTime64(3, 'UTC')),
1256
+ marked_as_spam_at Nullable(DateTime64(3, 'UTC')),
1257
+ bounced_at Nullable(DateTime64(3, 'UTC')),
1258
+ delivery_delayed_at Nullable(DateTime64(3, 'UTC')),
1259
+ can_have_delivery_info Nullable(UInt8),
1260
+ skipped_reason LowCardinality(Nullable(String)),
1261
+ skipped_details Nullable(String),
1262
+ send_retries Int32,
1263
+ is_paused UInt8,
1264
+ sync_sequence_id Int64,
1265
+ sync_is_deleted UInt8,
1266
+ sync_created_at DateTime64(3, 'UTC') DEFAULT now64(3)
1267
+ )
1268
+ ENGINE ReplacingMergeTree(sync_sequence_id)
1269
+ PARTITION BY toYYYYMM(created_at)
1270
+ ORDER BY (project_id, branch_id, id);
1271
+ `.trim(),
1272
+ },
1273
+ internalDbFetchQueries: {
1274
+ clickhouse: `
1275
+ SELECT *
1276
+ FROM (
1277
+ SELECT
1278
+ "Tenancy"."projectId" AS "project_id",
1279
+ "Tenancy"."branchId" AS "branch_id",
1280
+ "EmailOutbox"."id" AS "id",
1281
+ LOWER(REPLACE("EmailOutbox"."status"::text, '_', '-')) AS "status",
1282
+ LOWER(REPLACE("EmailOutbox"."simpleStatus"::text, '_', '-')) AS "simple_status",
1283
+ CASE WHEN "EmailOutbox"."createdWith"::text = 'DRAFT' THEN 'draft' ELSE 'programmatic-call' END AS "created_with",
1284
+ "EmailOutbox"."emailDraftId" AS "email_draft_id",
1285
+ "EmailOutbox"."emailProgrammaticCallTemplateId" AS "email_programmatic_call_template_id",
1286
+ "EmailOutbox"."themeId" AS "theme_id",
1287
+ "EmailOutbox"."isHighPriority" AS "is_high_priority",
1288
+ "EmailOutbox"."renderedIsTransactional" AS "is_transactional",
1289
+ "EmailOutbox"."renderedSubject" AS "subject",
1290
+ "EmailOutbox"."renderedNotificationCategoryId" AS "notification_category_id",
1291
+ "EmailOutbox"."startedRenderingAt" AS "started_rendering_at",
1292
+ "EmailOutbox"."finishedRenderingAt" AS "rendered_at",
1293
+ "EmailOutbox"."renderErrorExternalMessage" AS "render_error",
1294
+ "EmailOutbox"."scheduledAt" AS "scheduled_at",
1295
+ "EmailOutbox"."createdAt" AS "created_at",
1296
+ "EmailOutbox"."updatedAt" AS "updated_at",
1297
+ "EmailOutbox"."startedSendingAt" AS "started_sending_at",
1298
+ "EmailOutbox"."sendServerErrorExternalMessage" AS "server_error",
1299
+ "EmailOutbox"."deliveredAt" AS "delivered_at",
1300
+ "EmailOutbox"."openedAt" AS "opened_at",
1301
+ "EmailOutbox"."clickedAt" AS "clicked_at",
1302
+ "EmailOutbox"."unsubscribedAt" AS "unsubscribed_at",
1303
+ "EmailOutbox"."markedAsSpamAt" AS "marked_as_spam_at",
1304
+ "EmailOutbox"."bouncedAt" AS "bounced_at",
1305
+ "EmailOutbox"."deliveryDelayedAt" AS "delivery_delayed_at",
1306
+ "EmailOutbox"."canHaveDeliveryInfo" AS "can_have_delivery_info",
1307
+ LOWER(REPLACE("EmailOutbox"."skippedReason"::text, '_', '-')) AS "skipped_reason",
1308
+ "EmailOutbox"."skippedDetails" AS "skipped_details",
1309
+ "EmailOutbox"."sendRetries" AS "send_retries",
1310
+ "EmailOutbox"."isPaused" AS "is_paused",
1311
+ "EmailOutbox"."sequenceId" AS "sync_sequence_id",
1312
+ "EmailOutbox"."tenancyId" AS "tenancyId",
1313
+ false AS "sync_is_deleted"
1314
+ FROM "EmailOutbox"
1315
+ JOIN "Tenancy" ON "Tenancy"."id" = "EmailOutbox"."tenancyId"
1316
+ WHERE "EmailOutbox"."tenancyId" = $1::uuid
1317
+
1318
+ UNION ALL
1319
+
1320
+ SELECT
1321
+ "Tenancy"."projectId" AS "project_id",
1322
+ "Tenancy"."branchId" AS "branch_id",
1323
+ ("DeletedRow"."primaryKey"->>'id')::uuid AS "id",
1324
+ ''::text AS "status",
1325
+ ''::text AS "simple_status",
1326
+ ''::text AS "created_with",
1327
+ NULL::text AS "email_draft_id",
1328
+ NULL::text AS "email_programmatic_call_template_id",
1329
+ NULL::text AS "theme_id",
1330
+ false AS "is_high_priority",
1331
+ NULL::boolean AS "is_transactional",
1332
+ NULL::text AS "subject",
1333
+ NULL::text AS "notification_category_id",
1334
+ NULL::timestamp without time zone AS "started_rendering_at",
1335
+ NULL::timestamp without time zone AS "rendered_at",
1336
+ NULL::text AS "render_error",
1337
+ "DeletedRow"."deletedAt"::timestamp without time zone AS "scheduled_at",
1338
+ "DeletedRow"."deletedAt"::timestamp without time zone AS "created_at",
1339
+ "DeletedRow"."deletedAt"::timestamp without time zone AS "updated_at",
1340
+ NULL::timestamp without time zone AS "started_sending_at",
1341
+ NULL::text AS "server_error",
1342
+ NULL::timestamp without time zone AS "delivered_at",
1343
+ NULL::timestamp without time zone AS "opened_at",
1344
+ NULL::timestamp without time zone AS "clicked_at",
1345
+ NULL::timestamp without time zone AS "unsubscribed_at",
1346
+ NULL::timestamp without time zone AS "marked_as_spam_at",
1347
+ NULL::timestamp without time zone AS "bounced_at",
1348
+ NULL::timestamp without time zone AS "delivery_delayed_at",
1349
+ NULL::boolean AS "can_have_delivery_info",
1350
+ NULL::text AS "skipped_reason",
1351
+ NULL::jsonb AS "skipped_details",
1352
+ 0 AS "send_retries",
1353
+ false AS "is_paused",
1354
+ "DeletedRow"."sequenceId" AS "sync_sequence_id",
1355
+ "DeletedRow"."tenancyId" AS "tenancyId",
1356
+ true AS "sync_is_deleted"
1357
+ FROM "DeletedRow"
1358
+ JOIN "Tenancy" ON "Tenancy"."id" = "DeletedRow"."tenancyId"
1359
+ WHERE
1360
+ "DeletedRow"."tenancyId" = $1::uuid
1361
+ AND "DeletedRow"."tableName" = 'EmailOutbox'
1362
+ ) AS "_src"
1363
+ WHERE "sync_sequence_id" IS NOT NULL
1364
+ AND "sync_sequence_id" > $2::bigint
1365
+ ORDER BY "sync_sequence_id" ASC
1366
+ LIMIT 1000
1367
+ `.trim(),
1368
+ },
1369
+ internalDbFetchQuery: `
1370
+ SELECT *
1371
+ FROM (
1372
+ SELECT
1373
+ "EmailOutbox"."id" AS "id",
1374
+ "EmailOutbox"."status"::text AS "status",
1375
+ "EmailOutbox"."simpleStatus"::text AS "simple_status",
1376
+ "EmailOutbox"."createdWith"::text AS "created_with",
1377
+ "EmailOutbox"."emailDraftId" AS "email_draft_id",
1378
+ "EmailOutbox"."emailProgrammaticCallTemplateId" AS "email_programmatic_call_template_id",
1379
+ "EmailOutbox"."themeId" AS "theme_id",
1380
+ "EmailOutbox"."isHighPriority" AS "is_high_priority",
1381
+ "EmailOutbox"."renderedIsTransactional" AS "rendered_is_transactional",
1382
+ "EmailOutbox"."renderedSubject" AS "rendered_subject",
1383
+ "EmailOutbox"."renderedNotificationCategoryId" AS "rendered_notification_category_id",
1384
+ "EmailOutbox"."startedRenderingAt" AS "started_rendering_at",
1385
+ "EmailOutbox"."finishedRenderingAt" AS "finished_rendering_at",
1386
+ "EmailOutbox"."renderErrorExternalMessage" AS "render_error",
1387
+ "EmailOutbox"."scheduledAt" AS "scheduled_at",
1388
+ "EmailOutbox"."createdAt" AS "created_at",
1389
+ "EmailOutbox"."startedSendingAt" AS "started_sending_at",
1390
+ "EmailOutbox"."finishedSendingAt" AS "finished_sending_at",
1391
+ "EmailOutbox"."sendServerErrorExternalMessage" AS "server_error",
1392
+ "EmailOutbox"."sentAt" AS "sent_at",
1393
+ "EmailOutbox"."deliveredAt" AS "delivered_at",
1394
+ "EmailOutbox"."openedAt" AS "opened_at",
1395
+ "EmailOutbox"."clickedAt" AS "clicked_at",
1396
+ "EmailOutbox"."unsubscribedAt" AS "unsubscribed_at",
1397
+ "EmailOutbox"."markedAsSpamAt" AS "marked_as_spam_at",
1398
+ "EmailOutbox"."bouncedAt" AS "bounced_at",
1399
+ "EmailOutbox"."deliveryDelayedAt" AS "delivery_delayed_at",
1400
+ "EmailOutbox"."canHaveDeliveryInfo" AS "can_have_delivery_info",
1401
+ "EmailOutbox"."skippedReason"::text AS "skipped_reason",
1402
+ "EmailOutbox"."skippedDetails" AS "skipped_details",
1403
+ "EmailOutbox"."sendRetries" AS "send_retries",
1404
+ "EmailOutbox"."isPaused" AS "is_paused",
1405
+ "EmailOutbox"."sequenceId" AS "sequence_id",
1406
+ "EmailOutbox"."tenancyId",
1407
+ false AS "is_deleted"
1408
+ FROM "EmailOutbox"
1409
+ WHERE "EmailOutbox"."tenancyId" = $1::uuid
1410
+
1411
+ UNION ALL
1412
+
1413
+ SELECT
1414
+ ("DeletedRow"."primaryKey"->>'id')::uuid AS "id",
1415
+ ''::text AS "status",
1416
+ ''::text AS "simple_status",
1417
+ ''::text AS "created_with",
1418
+ NULL::text AS "email_draft_id",
1419
+ NULL::text AS "email_programmatic_call_template_id",
1420
+ NULL::text AS "theme_id",
1421
+ false AS "is_high_priority",
1422
+ NULL::boolean AS "rendered_is_transactional",
1423
+ NULL::text AS "rendered_subject",
1424
+ NULL::text AS "rendered_notification_category_id",
1425
+ NULL::timestamp without time zone AS "started_rendering_at",
1426
+ NULL::timestamp without time zone AS "finished_rendering_at",
1427
+ NULL::text AS "render_error",
1428
+ "DeletedRow"."deletedAt"::timestamp without time zone AS "scheduled_at",
1429
+ "DeletedRow"."deletedAt"::timestamp without time zone AS "created_at",
1430
+ NULL::timestamp without time zone AS "started_sending_at",
1431
+ NULL::timestamp without time zone AS "finished_sending_at",
1432
+ NULL::text AS "server_error",
1433
+ NULL::timestamp without time zone AS "sent_at",
1434
+ NULL::timestamp without time zone AS "delivered_at",
1435
+ NULL::timestamp without time zone AS "opened_at",
1436
+ NULL::timestamp without time zone AS "clicked_at",
1437
+ NULL::timestamp without time zone AS "unsubscribed_at",
1438
+ NULL::timestamp without time zone AS "marked_as_spam_at",
1439
+ NULL::timestamp without time zone AS "bounced_at",
1440
+ NULL::timestamp without time zone AS "delivery_delayed_at",
1441
+ NULL::boolean AS "can_have_delivery_info",
1442
+ NULL::text AS "skipped_reason",
1443
+ NULL::jsonb AS "skipped_details",
1444
+ 0 AS "send_retries",
1445
+ false AS "is_paused",
1446
+ "DeletedRow"."sequenceId" AS "sequence_id",
1447
+ "DeletedRow"."tenancyId",
1448
+ true AS "is_deleted"
1449
+ FROM "DeletedRow"
1450
+ WHERE
1451
+ "DeletedRow"."tenancyId" = $1::uuid
1452
+ AND "DeletedRow"."tableName" = 'EmailOutbox'
1453
+ ) AS "_src"
1454
+ WHERE "sequence_id" IS NOT NULL
1455
+ AND "sequence_id" > $2::bigint
1456
+ ORDER BY "sequence_id" ASC
1457
+ LIMIT 1000
1458
+ `.trim(),
1459
+ externalDbUpdateQueries: {
1460
+ postgres: `
1461
+ WITH params AS (
1462
+ SELECT
1463
+ $1::uuid AS "id",
1464
+ $2::text AS "status",
1465
+ $3::text AS "simple_status",
1466
+ $4::text AS "created_with",
1467
+ $5::text AS "email_draft_id",
1468
+ $6::text AS "email_programmatic_call_template_id",
1469
+ $7::text AS "theme_id",
1470
+ $8::boolean AS "is_high_priority",
1471
+ $9::boolean AS "rendered_is_transactional",
1472
+ $10::text AS "rendered_subject",
1473
+ $11::text AS "rendered_notification_category_id",
1474
+ $12::timestamp without time zone AS "started_rendering_at",
1475
+ $13::timestamp without time zone AS "finished_rendering_at",
1476
+ $14::text AS "render_error",
1477
+ $15::timestamp without time zone AS "scheduled_at",
1478
+ $16::timestamp without time zone AS "created_at",
1479
+ $17::timestamp without time zone AS "started_sending_at",
1480
+ $18::timestamp without time zone AS "finished_sending_at",
1481
+ $19::text AS "server_error",
1482
+ $20::timestamp without time zone AS "sent_at",
1483
+ $21::timestamp without time zone AS "delivered_at",
1484
+ $22::timestamp without time zone AS "opened_at",
1485
+ $23::timestamp without time zone AS "clicked_at",
1486
+ $24::timestamp without time zone AS "unsubscribed_at",
1487
+ $25::timestamp without time zone AS "marked_as_spam_at",
1488
+ $26::timestamp without time zone AS "bounced_at",
1489
+ $27::timestamp without time zone AS "delivery_delayed_at",
1490
+ $28::boolean AS "can_have_delivery_info",
1491
+ $29::text AS "skipped_reason",
1492
+ $30::jsonb AS "skipped_details",
1493
+ $31::integer AS "send_retries",
1494
+ $32::boolean AS "is_paused",
1495
+ $33::bigint AS "sequence_id",
1496
+ $34::boolean AS "is_deleted",
1497
+ $35::text AS "mapping_name"
1498
+ ),
1499
+ deleted AS (
1500
+ DELETE FROM "email_outboxes" eo
1501
+ USING params p
1502
+ WHERE p."is_deleted" = true AND eo."id" = p."id"
1503
+ RETURNING 1
1504
+ ),
1505
+ upserted AS (
1506
+ INSERT INTO "email_outboxes" (
1507
+ "id",
1508
+ "status",
1509
+ "simple_status",
1510
+ "created_with",
1511
+ "email_draft_id",
1512
+ "email_programmatic_call_template_id",
1513
+ "theme_id",
1514
+ "is_high_priority",
1515
+ "rendered_is_transactional",
1516
+ "rendered_subject",
1517
+ "rendered_notification_category_id",
1518
+ "started_rendering_at",
1519
+ "finished_rendering_at",
1520
+ "render_error",
1521
+ "scheduled_at",
1522
+ "created_at",
1523
+ "started_sending_at",
1524
+ "finished_sending_at",
1525
+ "server_error",
1526
+ "sent_at",
1527
+ "delivered_at",
1528
+ "opened_at",
1529
+ "clicked_at",
1530
+ "unsubscribed_at",
1531
+ "marked_as_spam_at",
1532
+ "bounced_at",
1533
+ "delivery_delayed_at",
1534
+ "can_have_delivery_info",
1535
+ "skipped_reason",
1536
+ "skipped_details",
1537
+ "send_retries",
1538
+ "is_paused"
1539
+ )
1540
+ SELECT
1541
+ p."id",
1542
+ p."status",
1543
+ p."simple_status",
1544
+ p."created_with",
1545
+ p."email_draft_id",
1546
+ p."email_programmatic_call_template_id",
1547
+ p."theme_id",
1548
+ p."is_high_priority",
1549
+ p."rendered_is_transactional",
1550
+ p."rendered_subject",
1551
+ p."rendered_notification_category_id",
1552
+ p."started_rendering_at",
1553
+ p."finished_rendering_at",
1554
+ p."render_error",
1555
+ p."scheduled_at",
1556
+ p."created_at",
1557
+ p."started_sending_at",
1558
+ p."finished_sending_at",
1559
+ p."server_error",
1560
+ p."sent_at",
1561
+ p."delivered_at",
1562
+ p."opened_at",
1563
+ p."clicked_at",
1564
+ p."unsubscribed_at",
1565
+ p."marked_as_spam_at",
1566
+ p."bounced_at",
1567
+ p."delivery_delayed_at",
1568
+ p."can_have_delivery_info",
1569
+ p."skipped_reason",
1570
+ p."skipped_details",
1571
+ p."send_retries",
1572
+ p."is_paused"
1573
+ FROM params p
1574
+ WHERE p."is_deleted" = false
1575
+ ON CONFLICT ("id") DO UPDATE SET
1576
+ "status" = EXCLUDED."status",
1577
+ "simple_status" = EXCLUDED."simple_status",
1578
+ "created_with" = EXCLUDED."created_with",
1579
+ "email_draft_id" = EXCLUDED."email_draft_id",
1580
+ "email_programmatic_call_template_id" = EXCLUDED."email_programmatic_call_template_id",
1581
+ "theme_id" = EXCLUDED."theme_id",
1582
+ "is_high_priority" = EXCLUDED."is_high_priority",
1583
+ "rendered_is_transactional" = EXCLUDED."rendered_is_transactional",
1584
+ "rendered_subject" = EXCLUDED."rendered_subject",
1585
+ "rendered_notification_category_id" = EXCLUDED."rendered_notification_category_id",
1586
+ "started_rendering_at" = EXCLUDED."started_rendering_at",
1587
+ "finished_rendering_at" = EXCLUDED."finished_rendering_at",
1588
+ "render_error" = EXCLUDED."render_error",
1589
+ "scheduled_at" = EXCLUDED."scheduled_at",
1590
+ "created_at" = EXCLUDED."created_at",
1591
+ "started_sending_at" = EXCLUDED."started_sending_at",
1592
+ "finished_sending_at" = EXCLUDED."finished_sending_at",
1593
+ "server_error" = EXCLUDED."server_error",
1594
+ "sent_at" = EXCLUDED."sent_at",
1595
+ "delivered_at" = EXCLUDED."delivered_at",
1596
+ "opened_at" = EXCLUDED."opened_at",
1597
+ "clicked_at" = EXCLUDED."clicked_at",
1598
+ "unsubscribed_at" = EXCLUDED."unsubscribed_at",
1599
+ "marked_as_spam_at" = EXCLUDED."marked_as_spam_at",
1600
+ "bounced_at" = EXCLUDED."bounced_at",
1601
+ "delivery_delayed_at" = EXCLUDED."delivery_delayed_at",
1602
+ "can_have_delivery_info" = EXCLUDED."can_have_delivery_info",
1603
+ "skipped_reason" = EXCLUDED."skipped_reason",
1604
+ "skipped_details" = EXCLUDED."skipped_details",
1605
+ "send_retries" = EXCLUDED."send_retries",
1606
+ "is_paused" = EXCLUDED."is_paused"
1607
+ RETURNING 1
1608
+ )
1609
+ INSERT INTO "_stack_sync_metadata" ("mapping_name", "last_synced_sequence_id", "updated_at")
1610
+ SELECT p."mapping_name", p."sequence_id", now() FROM params p
1611
+ ON CONFLICT ("mapping_name") DO UPDATE SET
1612
+ "last_synced_sequence_id" = GREATEST("_stack_sync_metadata"."last_synced_sequence_id", EXCLUDED."last_synced_sequence_id"),
1613
+ "updated_at" = now();
1614
+ `.trim(),
1615
+ },
1616
+ },
1617
+ "project_permissions": {
1618
+ sourceTables: { "ProjectUserDirectPermission": "ProjectUserDirectPermission" },
1619
+ targetTable: "project_permissions",
1620
+ targetTableSchemas: {
1621
+ postgres: `
1622
+ CREATE TABLE IF NOT EXISTS "project_permissions" (
1623
+ "user_id" uuid NOT NULL,
1624
+ "permission_id" text NOT NULL,
1625
+ "created_at" timestamp without time zone NOT NULL,
1626
+ PRIMARY KEY ("user_id", "permission_id")
1627
+ );
1628
+ REVOKE ALL ON "project_permissions" FROM PUBLIC;
1629
+ GRANT SELECT ON "project_permissions" TO PUBLIC;
1630
+
1631
+ CREATE TABLE IF NOT EXISTS "_stack_sync_metadata" (
1632
+ "mapping_name" text PRIMARY KEY NOT NULL,
1633
+ "last_synced_sequence_id" bigint NOT NULL DEFAULT -1,
1634
+ "updated_at" timestamp without time zone NOT NULL DEFAULT now()
1635
+ );
1636
+ `.trim(),
1637
+ clickhouse: `
1638
+ CREATE TABLE IF NOT EXISTS analytics_internal.project_permissions (
1639
+ project_id String,
1640
+ branch_id String,
1641
+ user_id UUID,
1642
+ id String,
1643
+ created_at DateTime64(3, 'UTC'),
1644
+ sync_sequence_id Int64,
1645
+ sync_is_deleted UInt8,
1646
+ sync_created_at DateTime64(3, 'UTC') DEFAULT now64(3)
1647
+ )
1648
+ ENGINE ReplacingMergeTree(sync_sequence_id)
1649
+ PARTITION BY toYYYYMM(created_at)
1650
+ ORDER BY (project_id, branch_id, user_id, id);
1651
+ `.trim(),
1652
+ },
1653
+ internalDbFetchQueries: {
1654
+ clickhouse: `
1655
+ SELECT *
1656
+ FROM (
1657
+ SELECT
1658
+ "Tenancy"."projectId" AS "project_id",
1659
+ "Tenancy"."branchId" AS "branch_id",
1660
+ "ProjectUserDirectPermission"."projectUserId" AS "user_id",
1661
+ "ProjectUserDirectPermission"."permissionId" AS "id",
1662
+ "ProjectUserDirectPermission"."createdAt" AS "created_at",
1663
+ "ProjectUserDirectPermission"."sequenceId" AS "sync_sequence_id",
1664
+ "ProjectUserDirectPermission"."tenancyId" AS "tenancyId",
1665
+ false AS "sync_is_deleted"
1666
+ FROM "ProjectUserDirectPermission"
1667
+ JOIN "Tenancy" ON "Tenancy"."id" = "ProjectUserDirectPermission"."tenancyId"
1668
+ WHERE "ProjectUserDirectPermission"."tenancyId" = $1::uuid
1669
+
1670
+ UNION ALL
1671
+
1672
+ SELECT
1673
+ "Tenancy"."projectId" AS "project_id",
1674
+ "Tenancy"."branchId" AS "branch_id",
1675
+ ("DeletedRow"."primaryKey"->>'projectUserId')::uuid AS "user_id",
1676
+ "DeletedRow"."primaryKey"->>'permissionId' AS "id",
1677
+ "DeletedRow"."deletedAt"::timestamp without time zone AS "created_at",
1678
+ "DeletedRow"."sequenceId" AS "sync_sequence_id",
1679
+ "DeletedRow"."tenancyId" AS "tenancyId",
1680
+ true AS "sync_is_deleted"
1681
+ FROM "DeletedRow"
1682
+ JOIN "Tenancy" ON "Tenancy"."id" = "DeletedRow"."tenancyId"
1683
+ WHERE
1684
+ "DeletedRow"."tenancyId" = $1::uuid
1685
+ AND "DeletedRow"."tableName" = 'ProjectUserDirectPermission'
1686
+ ) AS "_src"
1687
+ WHERE "sync_sequence_id" IS NOT NULL
1688
+ AND "sync_sequence_id" > $2::bigint
1689
+ ORDER BY "sync_sequence_id" ASC
1690
+ LIMIT 1000
1691
+ `.trim(),
1692
+ },
1693
+ internalDbFetchQuery: `
1694
+ SELECT *
1695
+ FROM (
1696
+ SELECT
1697
+ "ProjectUserDirectPermission"."projectUserId" AS "user_id",
1698
+ "ProjectUserDirectPermission"."permissionId" AS "permission_id",
1699
+ "ProjectUserDirectPermission"."createdAt" AS "created_at",
1700
+ "ProjectUserDirectPermission"."sequenceId" AS "sequence_id",
1701
+ "ProjectUserDirectPermission"."tenancyId",
1702
+ false AS "is_deleted"
1703
+ FROM "ProjectUserDirectPermission"
1704
+ WHERE "ProjectUserDirectPermission"."tenancyId" = $1::uuid
1705
+
1706
+ UNION ALL
1707
+
1708
+ SELECT
1709
+ ("DeletedRow"."primaryKey"->>'projectUserId')::uuid AS "user_id",
1710
+ "DeletedRow"."primaryKey"->>'permissionId' AS "id",
1711
+ "DeletedRow"."deletedAt"::timestamp without time zone AS "created_at",
1712
+ "DeletedRow"."sequenceId" AS "sequence_id",
1713
+ "DeletedRow"."tenancyId",
1714
+ true AS "is_deleted"
1715
+ FROM "DeletedRow"
1716
+ WHERE
1717
+ "DeletedRow"."tenancyId" = $1::uuid
1718
+ AND "DeletedRow"."tableName" = 'ProjectUserDirectPermission'
1719
+ ) AS "_src"
1720
+ WHERE "sequence_id" IS NOT NULL
1721
+ AND "sequence_id" > $2::bigint
1722
+ ORDER BY "sequence_id" ASC
1723
+ LIMIT 1000
1724
+ `.trim(),
1725
+ externalDbUpdateQueries: {
1726
+ postgres: `
1727
+ WITH params AS (
1728
+ SELECT
1729
+ $1::uuid AS "user_id",
1730
+ $2::text AS "permission_id",
1731
+ $3::timestamp without time zone AS "created_at",
1732
+ $4::bigint AS "sequence_id",
1733
+ $5::boolean AS "is_deleted",
1734
+ $6::text AS "mapping_name"
1735
+ ),
1736
+ deleted AS (
1737
+ DELETE FROM "project_permissions" pp
1738
+ USING params p
1739
+ WHERE p."is_deleted" = true AND pp."user_id" = p."user_id" AND pp."permission_id" = p."permission_id"
1740
+ RETURNING 1
1741
+ ),
1742
+ upserted AS (
1743
+ INSERT INTO "project_permissions" (
1744
+ "user_id",
1745
+ "permission_id",
1746
+ "created_at"
1747
+ )
1748
+ SELECT
1749
+ p."user_id",
1750
+ p."permission_id",
1751
+ p."created_at"
1752
+ FROM params p
1753
+ WHERE p."is_deleted" = false
1754
+ ON CONFLICT ("user_id", "permission_id") DO UPDATE SET
1755
+ "created_at" = EXCLUDED."created_at"
1756
+ RETURNING 1
1757
+ )
1758
+ INSERT INTO "_stack_sync_metadata" ("mapping_name", "last_synced_sequence_id", "updated_at")
1759
+ SELECT p."mapping_name", p."sequence_id", now() FROM params p
1760
+ ON CONFLICT ("mapping_name") DO UPDATE SET
1761
+ "last_synced_sequence_id" = GREATEST("_stack_sync_metadata"."last_synced_sequence_id", EXCLUDED."last_synced_sequence_id"),
1762
+ "updated_at" = now();
1763
+ `.trim(),
1764
+ },
1765
+ },
1766
+ "notification_preferences": {
1767
+ sourceTables: { "UserNotificationPreference": "UserNotificationPreference" },
1768
+ targetTable: "notification_preferences",
1769
+ targetTableSchemas: {
1770
+ postgres: `
1771
+ CREATE TABLE IF NOT EXISTS "notification_preferences" (
1772
+ "id" uuid PRIMARY KEY NOT NULL,
1773
+ "user_id" uuid NOT NULL,
1774
+ "notification_category_id" text NOT NULL,
1775
+ "enabled" boolean NOT NULL DEFAULT true
1776
+ );
1777
+ REVOKE ALL ON "notification_preferences" FROM PUBLIC;
1778
+ GRANT SELECT ON "notification_preferences" TO PUBLIC;
1779
+
1780
+ CREATE TABLE IF NOT EXISTS "_stack_sync_metadata" (
1781
+ "mapping_name" text PRIMARY KEY NOT NULL,
1782
+ "last_synced_sequence_id" bigint NOT NULL DEFAULT -1,
1783
+ "updated_at" timestamp without time zone NOT NULL DEFAULT now()
1784
+ );
1785
+ `.trim(),
1786
+ clickhouse: `
1787
+ CREATE TABLE IF NOT EXISTS analytics_internal.notification_preferences (
1788
+ project_id String,
1789
+ branch_id String,
1790
+ user_id UUID,
1791
+ notification_category_id String,
1792
+ enabled UInt8,
1793
+ sync_sequence_id Int64,
1794
+ sync_is_deleted UInt8,
1795
+ sync_created_at DateTime64(3, 'UTC') DEFAULT now64(3)
1796
+ )
1797
+ ENGINE ReplacingMergeTree(sync_sequence_id)
1798
+ ORDER BY (project_id, branch_id, user_id, notification_category_id);
1799
+ `.trim(),
1800
+ },
1801
+ internalDbFetchQueries: {
1802
+ clickhouse: `
1803
+ SELECT *
1804
+ FROM (
1805
+ SELECT
1806
+ "Tenancy"."projectId" AS "project_id",
1807
+ "Tenancy"."branchId" AS "branch_id",
1808
+ "UserNotificationPreference"."projectUserId" AS "user_id",
1809
+ "UserNotificationPreference"."notificationCategoryId" AS "notification_category_id",
1810
+ "UserNotificationPreference"."enabled" AS "enabled",
1811
+ "UserNotificationPreference"."sequenceId" AS "sync_sequence_id",
1812
+ "UserNotificationPreference"."tenancyId" AS "tenancyId",
1813
+ false AS "sync_is_deleted"
1814
+ FROM "UserNotificationPreference"
1815
+ JOIN "Tenancy" ON "Tenancy"."id" = "UserNotificationPreference"."tenancyId"
1816
+ WHERE "UserNotificationPreference"."tenancyId" = $1::uuid
1817
+
1818
+ UNION ALL
1819
+
1820
+ SELECT
1821
+ "Tenancy"."projectId" AS "project_id",
1822
+ "Tenancy"."branchId" AS "branch_id",
1823
+ ("DeletedRow"."data"->>'projectUserId')::uuid AS "user_id",
1824
+ ("DeletedRow"."data"->>'notificationCategoryId')::uuid AS "notification_category_id",
1825
+ ("DeletedRow"."data"->>'enabled')::boolean AS "enabled",
1826
+ "DeletedRow"."sequenceId" AS "sync_sequence_id",
1827
+ "DeletedRow"."tenancyId" AS "tenancyId",
1828
+ true AS "sync_is_deleted"
1829
+ FROM "DeletedRow"
1830
+ JOIN "Tenancy" ON "Tenancy"."id" = "DeletedRow"."tenancyId"
1831
+ WHERE
1832
+ "DeletedRow"."tenancyId" = $1::uuid
1833
+ AND "DeletedRow"."tableName" = 'UserNotificationPreference'
1834
+ ) AS "_src"
1835
+ WHERE "sync_sequence_id" IS NOT NULL
1836
+ AND "sync_sequence_id" > $2::bigint
1837
+ ORDER BY "sync_sequence_id" ASC
1838
+ LIMIT 1000
1839
+ `.trim(),
1840
+ },
1841
+ internalDbFetchQuery: `
1842
+ SELECT *
1843
+ FROM (
1844
+ SELECT
1845
+ "UserNotificationPreference"."id" AS "id",
1846
+ "UserNotificationPreference"."projectUserId" AS "user_id",
1847
+ "UserNotificationPreference"."notificationCategoryId" AS "notification_category_id",
1848
+ "UserNotificationPreference"."enabled" AS "enabled",
1849
+ "UserNotificationPreference"."sequenceId" AS "sequence_id",
1850
+ "UserNotificationPreference"."tenancyId",
1851
+ false AS "is_deleted"
1852
+ FROM "UserNotificationPreference"
1853
+ WHERE "UserNotificationPreference"."tenancyId" = $1::uuid
1854
+
1855
+ UNION ALL
1856
+
1857
+ SELECT
1858
+ ("DeletedRow"."primaryKey"->>'id')::uuid AS "id",
1859
+ ("DeletedRow"."data"->>'projectUserId')::uuid AS "user_id",
1860
+ ("DeletedRow"."data"->>'notificationCategoryId')::uuid AS "notification_category_id",
1861
+ ("DeletedRow"."data"->>'enabled')::boolean AS "enabled",
1862
+ "DeletedRow"."sequenceId" AS "sequence_id",
1863
+ "DeletedRow"."tenancyId",
1864
+ true AS "is_deleted"
1865
+ FROM "DeletedRow"
1866
+ WHERE
1867
+ "DeletedRow"."tenancyId" = $1::uuid
1868
+ AND "DeletedRow"."tableName" = 'UserNotificationPreference'
1869
+ ) AS "_src"
1870
+ WHERE "sequence_id" IS NOT NULL
1871
+ AND "sequence_id" > $2::bigint
1872
+ ORDER BY "sequence_id" ASC
1873
+ LIMIT 1000
1874
+ `.trim(),
1875
+ externalDbUpdateQueries: {
1876
+ postgres: `
1877
+ WITH params AS (
1878
+ SELECT
1879
+ $1::uuid AS "id",
1880
+ $2::uuid AS "user_id",
1881
+ $3::text AS "notification_category_id",
1882
+ $4::boolean AS "enabled",
1883
+ $5::bigint AS "sequence_id",
1884
+ $6::boolean AS "is_deleted",
1885
+ $7::text AS "mapping_name"
1886
+ ),
1887
+ deleted AS (
1888
+ DELETE FROM "notification_preferences" np
1889
+ USING params p
1890
+ WHERE p."is_deleted" = true AND np."id" = p."id"
1891
+ RETURNING 1
1892
+ ),
1893
+ upserted AS (
1894
+ INSERT INTO "notification_preferences" (
1895
+ "id",
1896
+ "user_id",
1897
+ "notification_category_id",
1898
+ "enabled"
1899
+ )
1900
+ SELECT
1901
+ p."id",
1902
+ p."user_id",
1903
+ p."notification_category_id",
1904
+ p."enabled"
1905
+ FROM params p
1906
+ WHERE p."is_deleted" = false
1907
+ ON CONFLICT ("id") DO UPDATE SET
1908
+ "user_id" = EXCLUDED."user_id",
1909
+ "notification_category_id" = EXCLUDED."notification_category_id",
1910
+ "enabled" = EXCLUDED."enabled"
1911
+ RETURNING 1
1912
+ )
1913
+ INSERT INTO "_stack_sync_metadata" ("mapping_name", "last_synced_sequence_id", "updated_at")
1914
+ SELECT p."mapping_name", p."sequence_id", now() FROM params p
1915
+ ON CONFLICT ("mapping_name") DO UPDATE SET
1916
+ "last_synced_sequence_id" = GREATEST("_stack_sync_metadata"."last_synced_sequence_id", EXCLUDED."last_synced_sequence_id"),
1917
+ "updated_at" = now();
1918
+ `.trim(),
1919
+ },
1920
+ },
1921
+ "refresh_tokens": {
1922
+ sourceTables: { "ProjectUserRefreshToken": "ProjectUserRefreshToken" },
1923
+ targetTable: "refresh_tokens",
1924
+ targetTableSchemas: {
1925
+ postgres: `
1926
+ CREATE TABLE IF NOT EXISTS "refresh_tokens" (
1927
+ "id" uuid PRIMARY KEY NOT NULL,
1928
+ "user_id" uuid NOT NULL,
1929
+ "created_at" timestamp without time zone NOT NULL,
1930
+ "last_used_at" timestamp without time zone NOT NULL,
1931
+ "is_impersonation" boolean NOT NULL DEFAULT false,
1932
+ "expires_at" timestamp without time zone
1933
+ );
1934
+ REVOKE ALL ON "refresh_tokens" FROM PUBLIC;
1935
+ GRANT SELECT ON "refresh_tokens" TO PUBLIC;
1936
+
1937
+ CREATE TABLE IF NOT EXISTS "_stack_sync_metadata" (
1938
+ "mapping_name" text PRIMARY KEY NOT NULL,
1939
+ "last_synced_sequence_id" bigint NOT NULL DEFAULT -1,
1940
+ "updated_at" timestamp without time zone NOT NULL DEFAULT now()
1941
+ );
1942
+ `.trim(),
1943
+ clickhouse: `
1944
+ CREATE TABLE IF NOT EXISTS analytics_internal.refresh_tokens (
1945
+ project_id String,
1946
+ branch_id String,
1947
+ id UUID,
1948
+ user_id UUID,
1949
+ created_at DateTime64(3, 'UTC'),
1950
+ last_used_at DateTime64(3, 'UTC'),
1951
+ is_impersonation UInt8,
1952
+ expires_at Nullable(DateTime64(3, 'UTC')),
1953
+ sync_sequence_id Int64,
1954
+ sync_is_deleted UInt8,
1955
+ sync_created_at DateTime64(3, 'UTC') DEFAULT now64(3)
1956
+ )
1957
+ ENGINE ReplacingMergeTree(sync_sequence_id)
1958
+ PARTITION BY toYYYYMM(created_at)
1959
+ ORDER BY (project_id, branch_id, id);
1960
+
1961
+ CREATE TABLE IF NOT EXISTS analytics_internal._stack_sync_metadata (
1962
+ tenancy_id UUID,
1963
+ mapping_name String,
1964
+ last_synced_sequence_id Int64,
1965
+ updated_at DateTime64(3, 'UTC') DEFAULT now64(3)
1966
+ )
1967
+ ENGINE ReplacingMergeTree(updated_at)
1968
+ ORDER BY (tenancy_id, mapping_name);
1969
+ `.trim(),
1970
+ },
1971
+ internalDbFetchQueries: {
1972
+ clickhouse: `
1973
+ SELECT *
1974
+ FROM (
1975
+ SELECT
1976
+ "Tenancy"."projectId" AS "project_id",
1977
+ "Tenancy"."branchId" AS "branch_id",
1978
+ "ProjectUserRefreshToken"."id" AS "id",
1979
+ "ProjectUserRefreshToken"."projectUserId" AS "user_id",
1980
+ "ProjectUserRefreshToken"."createdAt" AS "created_at",
1981
+ "ProjectUserRefreshToken"."lastActiveAt" AS "last_used_at",
1982
+ "ProjectUserRefreshToken"."isImpersonation" AS "is_impersonation",
1983
+ "ProjectUserRefreshToken"."expiresAt" AS "expires_at",
1984
+ "ProjectUserRefreshToken"."sequenceId" AS "sync_sequence_id",
1985
+ "ProjectUserRefreshToken"."tenancyId" AS "tenancyId",
1986
+ false AS "sync_is_deleted"
1987
+ FROM "ProjectUserRefreshToken"
1988
+ JOIN "Tenancy" ON "Tenancy"."id" = "ProjectUserRefreshToken"."tenancyId"
1989
+ WHERE "ProjectUserRefreshToken"."tenancyId" = $1::uuid
1990
+
1991
+ UNION ALL
1992
+
1993
+ SELECT
1994
+ "Tenancy"."projectId" AS "project_id",
1995
+ "Tenancy"."branchId" AS "branch_id",
1996
+ ("DeletedRow"."primaryKey"->>'id')::uuid AS "id",
1997
+ ("DeletedRow"."data"->>'projectUserId')::uuid AS "user_id",
1998
+ "DeletedRow"."deletedAt"::timestamp without time zone AS "created_at",
1999
+ "DeletedRow"."deletedAt"::timestamp without time zone AS "last_used_at",
2000
+ false AS "is_impersonation",
2001
+ NULL::timestamp without time zone AS "expires_at",
2002
+ "DeletedRow"."sequenceId" AS "sync_sequence_id",
2003
+ "DeletedRow"."tenancyId" AS "tenancyId",
2004
+ true AS "sync_is_deleted"
2005
+ FROM "DeletedRow"
2006
+ JOIN "Tenancy" ON "Tenancy"."id" = "DeletedRow"."tenancyId"
2007
+ WHERE
2008
+ "DeletedRow"."tenancyId" = $1::uuid
2009
+ AND "DeletedRow"."tableName" = 'ProjectUserRefreshToken'
2010
+ ) AS "_src"
2011
+ WHERE "sync_sequence_id" IS NOT NULL
2012
+ AND "sync_sequence_id" > $2::bigint
2013
+ ORDER BY "sync_sequence_id" ASC
2014
+ LIMIT 1000
2015
+ `.trim(),
2016
+ },
2017
+ internalDbFetchQuery: `
2018
+ SELECT *
2019
+ FROM (
2020
+ SELECT
2021
+ "ProjectUserRefreshToken"."id" AS "id",
2022
+ "ProjectUserRefreshToken"."projectUserId" AS "user_id",
2023
+ "ProjectUserRefreshToken"."createdAt" AS "created_at",
2024
+ "ProjectUserRefreshToken"."lastActiveAt" AS "last_used_at",
2025
+ "ProjectUserRefreshToken"."isImpersonation" AS "is_impersonation",
2026
+ "ProjectUserRefreshToken"."expiresAt" AS "expires_at",
2027
+ "ProjectUserRefreshToken"."sequenceId" AS "sequence_id",
2028
+ "ProjectUserRefreshToken"."tenancyId",
2029
+ false AS "is_deleted"
2030
+ FROM "ProjectUserRefreshToken"
2031
+ WHERE "ProjectUserRefreshToken"."tenancyId" = $1::uuid
2032
+
2033
+ UNION ALL
2034
+
2035
+ SELECT
2036
+ ("DeletedRow"."primaryKey"->>'id')::uuid AS "id",
2037
+ ("DeletedRow"."data"->>'projectUserId')::uuid AS "user_id",
2038
+ "DeletedRow"."deletedAt"::timestamp without time zone AS "created_at",
2039
+ "DeletedRow"."deletedAt"::timestamp without time zone AS "last_used_at",
2040
+ false AS "is_impersonation",
2041
+ NULL::timestamp without time zone AS "expires_at",
2042
+ "DeletedRow"."sequenceId" AS "sequence_id",
2043
+ "DeletedRow"."tenancyId",
2044
+ true AS "is_deleted"
2045
+ FROM "DeletedRow"
2046
+ WHERE
2047
+ "DeletedRow"."tenancyId" = $1::uuid
2048
+ AND "DeletedRow"."tableName" = 'ProjectUserRefreshToken'
2049
+ ) AS "_src"
2050
+ WHERE "sequence_id" IS NOT NULL
2051
+ AND "sequence_id" > $2::bigint
2052
+ ORDER BY "sequence_id" ASC
2053
+ LIMIT 1000
2054
+ `.trim(),
2055
+ externalDbUpdateQueries: {
2056
+ postgres: `
2057
+ WITH params AS (
2058
+ SELECT
2059
+ $1::uuid AS "id",
2060
+ $2::uuid AS "user_id",
2061
+ $3::timestamp without time zone AS "created_at",
2062
+ $4::timestamp without time zone AS "last_used_at",
2063
+ $5::boolean AS "is_impersonation",
2064
+ $6::timestamp without time zone AS "expires_at",
2065
+ $7::bigint AS "sequence_id",
2066
+ $8::boolean AS "is_deleted",
2067
+ $9::text AS "mapping_name"
2068
+ ),
2069
+ deleted AS (
2070
+ DELETE FROM "refresh_tokens" rt
2071
+ USING params p
2072
+ WHERE p."is_deleted" = true AND rt."id" = p."id"
2073
+ RETURNING 1
2074
+ ),
2075
+ upserted AS (
2076
+ INSERT INTO "refresh_tokens" (
2077
+ "id",
2078
+ "user_id",
2079
+ "created_at",
2080
+ "last_used_at",
2081
+ "is_impersonation",
2082
+ "expires_at"
2083
+ )
2084
+ SELECT
2085
+ p."id",
2086
+ p."user_id",
2087
+ p."created_at",
2088
+ p."last_used_at",
2089
+ p."is_impersonation",
2090
+ p."expires_at"
2091
+ FROM params p
2092
+ WHERE p."is_deleted" = false
2093
+ ON CONFLICT ("id") DO UPDATE SET
2094
+ "user_id" = EXCLUDED."user_id",
2095
+ "created_at" = EXCLUDED."created_at",
2096
+ "last_used_at" = EXCLUDED."last_used_at",
2097
+ "is_impersonation" = EXCLUDED."is_impersonation",
2098
+ "expires_at" = EXCLUDED."expires_at"
2099
+ RETURNING 1
2100
+ )
2101
+ INSERT INTO "_stack_sync_metadata" ("mapping_name", "last_synced_sequence_id", "updated_at")
2102
+ SELECT p."mapping_name", p."sequence_id", now() FROM params p
2103
+ ON CONFLICT ("mapping_name") DO UPDATE SET
2104
+ "last_synced_sequence_id" = GREATEST("_stack_sync_metadata"."last_synced_sequence_id", EXCLUDED."last_synced_sequence_id"),
2105
+ "updated_at" = now();
2106
+ `.trim(),
2107
+ },
2108
+ },
2109
+ "connected_accounts": {
2110
+ sourceTables: { "ProjectUserOAuthAccount": "ProjectUserOAuthAccount" },
2111
+ targetTable: "connected_accounts",
2112
+ targetTableSchemas: {
2113
+ postgres: `
2114
+ CREATE TABLE IF NOT EXISTS "connected_accounts" (
2115
+ "id" uuid PRIMARY KEY NOT NULL,
2116
+ "user_id" uuid NOT NULL,
2117
+ "provider" text NOT NULL,
2118
+ "provider_account_id" text NOT NULL,
2119
+ "email" text,
2120
+ "created_at" timestamp without time zone NOT NULL
2121
+ );
2122
+ REVOKE ALL ON "connected_accounts" FROM PUBLIC;
2123
+ GRANT SELECT ON "connected_accounts" TO PUBLIC;
2124
+
2125
+ CREATE TABLE IF NOT EXISTS "_stack_sync_metadata" (
2126
+ "mapping_name" text PRIMARY KEY NOT NULL,
2127
+ "last_synced_sequence_id" bigint NOT NULL DEFAULT -1,
2128
+ "updated_at" timestamp without time zone NOT NULL DEFAULT now()
2129
+ );
2130
+ `.trim(),
2131
+ clickhouse: `
2132
+ CREATE TABLE IF NOT EXISTS analytics_internal.connected_accounts (
2133
+ project_id String,
2134
+ branch_id String,
2135
+ user_id UUID,
2136
+ provider String,
2137
+ provider_account_id String,
2138
+ created_at DateTime64(3, 'UTC'),
2139
+ sync_sequence_id Int64,
2140
+ sync_is_deleted UInt8,
2141
+ sync_created_at DateTime64(3, 'UTC') DEFAULT now64(3)
2142
+ )
2143
+ ENGINE ReplacingMergeTree(sync_sequence_id)
2144
+ PARTITION BY toYYYYMM(created_at)
2145
+ ORDER BY (project_id, branch_id, user_id, provider, provider_account_id);
2146
+
2147
+ CREATE TABLE IF NOT EXISTS analytics_internal._stack_sync_metadata (
2148
+ tenancy_id UUID,
2149
+ mapping_name String,
2150
+ last_synced_sequence_id Int64,
2151
+ updated_at DateTime64(3, 'UTC') DEFAULT now64(3)
2152
+ )
2153
+ ENGINE ReplacingMergeTree(updated_at)
2154
+ ORDER BY (tenancy_id, mapping_name);
2155
+ `.trim(),
2156
+ },
2157
+ internalDbFetchQueries: {
2158
+ clickhouse: `
2159
+ SELECT *
2160
+ FROM (
2161
+ SELECT
2162
+ "Tenancy"."projectId" AS "project_id",
2163
+ "Tenancy"."branchId" AS "branch_id",
2164
+ "ProjectUserOAuthAccount"."projectUserId" AS "user_id",
2165
+ "ProjectUserOAuthAccount"."configOAuthProviderId" AS "provider",
2166
+ "ProjectUserOAuthAccount"."providerAccountId" AS "provider_account_id",
2167
+ "ProjectUserOAuthAccount"."createdAt" AS "created_at",
2168
+ "ProjectUserOAuthAccount"."sequenceId" AS "sync_sequence_id",
2169
+ "ProjectUserOAuthAccount"."tenancyId" AS "tenancyId",
2170
+ false AS "sync_is_deleted"
2171
+ FROM "ProjectUserOAuthAccount"
2172
+ JOIN "Tenancy" ON "Tenancy"."id" = "ProjectUserOAuthAccount"."tenancyId"
2173
+ WHERE "ProjectUserOAuthAccount"."tenancyId" = $1::uuid
2174
+ AND "ProjectUserOAuthAccount"."projectUserId" IS NOT NULL
2175
+
2176
+ UNION ALL
2177
+
2178
+ SELECT
2179
+ "Tenancy"."projectId" AS "project_id",
2180
+ "Tenancy"."branchId" AS "branch_id",
2181
+ ("DeletedRow"."data"->>'projectUserId')::uuid AS "user_id",
2182
+ "DeletedRow"."data"->>'configOAuthProviderId' AS "provider",
2183
+ "DeletedRow"."data"->>'providerAccountId' AS "provider_account_id",
2184
+ "DeletedRow"."deletedAt"::timestamp without time zone AS "created_at",
2185
+ "DeletedRow"."sequenceId" AS "sync_sequence_id",
2186
+ "DeletedRow"."tenancyId" AS "tenancyId",
2187
+ true AS "sync_is_deleted"
2188
+ FROM "DeletedRow"
2189
+ JOIN "Tenancy" ON "Tenancy"."id" = "DeletedRow"."tenancyId"
2190
+ WHERE
2191
+ "DeletedRow"."tenancyId" = $1::uuid
2192
+ AND "DeletedRow"."tableName" = 'ProjectUserOAuthAccount'
2193
+ ) AS "_src"
2194
+ WHERE "sync_sequence_id" IS NOT NULL
2195
+ AND "sync_sequence_id" > $2::bigint
2196
+ ORDER BY "sync_sequence_id" ASC
2197
+ LIMIT 1000
2198
+ `.trim(),
2199
+ },
2200
+ internalDbFetchQuery: `
2201
+ SELECT *
2202
+ FROM (
2203
+ SELECT
2204
+ "ProjectUserOAuthAccount"."id" AS "id",
2205
+ "ProjectUserOAuthAccount"."projectUserId" AS "user_id",
2206
+ "ProjectUserOAuthAccount"."configOAuthProviderId" AS "provider",
2207
+ "ProjectUserOAuthAccount"."providerAccountId" AS "provider_account_id",
2208
+ "ProjectUserOAuthAccount"."email" AS "email",
2209
+ "ProjectUserOAuthAccount"."createdAt" AS "created_at",
2210
+ "ProjectUserOAuthAccount"."sequenceId" AS "sequence_id",
2211
+ "ProjectUserOAuthAccount"."tenancyId",
2212
+ false AS "is_deleted"
2213
+ FROM "ProjectUserOAuthAccount"
2214
+ WHERE "ProjectUserOAuthAccount"."tenancyId" = $1::uuid
2215
+ AND "ProjectUserOAuthAccount"."projectUserId" IS NOT NULL
2216
+
2217
+ UNION ALL
2218
+
2219
+ SELECT
2220
+ ("DeletedRow"."primaryKey"->>'id')::uuid AS "id",
2221
+ ("DeletedRow"."data"->>'projectUserId')::uuid AS "user_id",
2222
+ NULL::text AS "provider",
2223
+ NULL::text AS "provider_account_id",
2224
+ NULL::text AS "email",
2225
+ "DeletedRow"."deletedAt"::timestamp without time zone AS "created_at",
2226
+ "DeletedRow"."sequenceId" AS "sequence_id",
2227
+ "DeletedRow"."tenancyId",
2228
+ true AS "is_deleted"
2229
+ FROM "DeletedRow"
2230
+ WHERE
2231
+ "DeletedRow"."tenancyId" = $1::uuid
2232
+ AND "DeletedRow"."tableName" = 'ProjectUserOAuthAccount'
2233
+ ) AS "_src"
2234
+ WHERE "sequence_id" IS NOT NULL
2235
+ AND "sequence_id" > $2::bigint
2236
+ ORDER BY "sequence_id" ASC
2237
+ LIMIT 1000
2238
+ `.trim(),
2239
+ externalDbUpdateQueries: {
2240
+ postgres: `
2241
+ WITH params AS (
2242
+ SELECT
2243
+ $1::uuid AS "id",
2244
+ $2::uuid AS "user_id",
2245
+ $3::text AS "provider",
2246
+ $4::text AS "provider_account_id",
2247
+ $5::text AS "email",
2248
+ $6::timestamp without time zone AS "created_at",
2249
+ $7::bigint AS "sequence_id",
2250
+ $8::boolean AS "is_deleted",
2251
+ $9::text AS "mapping_name"
2252
+ ),
2253
+ deleted AS (
2254
+ DELETE FROM "connected_accounts" ca
2255
+ USING params p
2256
+ WHERE p."is_deleted" = true AND ca."id" = p."id"
2257
+ RETURNING 1
2258
+ ),
2259
+ upserted AS (
2260
+ INSERT INTO "connected_accounts" (
2261
+ "id",
2262
+ "user_id",
2263
+ "provider",
2264
+ "provider_account_id",
2265
+ "email",
2266
+ "created_at"
2267
+ )
2268
+ SELECT
2269
+ p."id",
2270
+ p."user_id",
2271
+ p."provider",
2272
+ p."provider_account_id",
2273
+ p."email",
2274
+ p."created_at"
2275
+ FROM params p
2276
+ WHERE p."is_deleted" = false
2277
+ ON CONFLICT ("id") DO UPDATE SET
2278
+ "user_id" = EXCLUDED."user_id",
2279
+ "provider" = EXCLUDED."provider",
2280
+ "provider_account_id" = EXCLUDED."provider_account_id",
2281
+ "email" = EXCLUDED."email",
2282
+ "created_at" = EXCLUDED."created_at"
2283
+ RETURNING 1
2284
+ )
2285
+ INSERT INTO "_stack_sync_metadata" ("mapping_name", "last_synced_sequence_id", "updated_at")
2286
+ SELECT p."mapping_name", p."sequence_id", now() FROM params p
2287
+ ON CONFLICT ("mapping_name") DO UPDATE SET
2288
+ "last_synced_sequence_id" = GREATEST("_stack_sync_metadata"."last_synced_sequence_id", EXCLUDED."last_synced_sequence_id"),
2289
+ "updated_at" = now();
2290
+ `.trim(),
2291
+ },
2292
+ },
2293
+ } as const;