@powersync/service-core 0.0.2

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 (412) hide show
  1. package/.probes/.gitkeep +0 -0
  2. package/CHANGELOG.md +13 -0
  3. package/LICENSE +67 -0
  4. package/README.md +3 -0
  5. package/dist/api/api-index.d.ts +2 -0
  6. package/dist/api/api-index.js +3 -0
  7. package/dist/api/api-index.js.map +1 -0
  8. package/dist/api/diagnostics.d.ts +21 -0
  9. package/dist/api/diagnostics.js +183 -0
  10. package/dist/api/diagnostics.js.map +1 -0
  11. package/dist/api/schema.d.ts +5 -0
  12. package/dist/api/schema.js +88 -0
  13. package/dist/api/schema.js.map +1 -0
  14. package/dist/auth/CachedKeyCollector.d.ts +46 -0
  15. package/dist/auth/CachedKeyCollector.js +116 -0
  16. package/dist/auth/CachedKeyCollector.js.map +1 -0
  17. package/dist/auth/CompoundKeyCollector.d.ts +8 -0
  18. package/dist/auth/CompoundKeyCollector.js +23 -0
  19. package/dist/auth/CompoundKeyCollector.js.map +1 -0
  20. package/dist/auth/JwtPayload.d.ts +10 -0
  21. package/dist/auth/JwtPayload.js +2 -0
  22. package/dist/auth/JwtPayload.js.map +1 -0
  23. package/dist/auth/KeyCollector.d.ts +24 -0
  24. package/dist/auth/KeyCollector.js +2 -0
  25. package/dist/auth/KeyCollector.js.map +1 -0
  26. package/dist/auth/KeySpec.d.ts +26 -0
  27. package/dist/auth/KeySpec.js +49 -0
  28. package/dist/auth/KeySpec.js.map +1 -0
  29. package/dist/auth/KeyStore.d.ts +39 -0
  30. package/dist/auth/KeyStore.js +131 -0
  31. package/dist/auth/KeyStore.js.map +1 -0
  32. package/dist/auth/LeakyBucket.d.ts +39 -0
  33. package/dist/auth/LeakyBucket.js +57 -0
  34. package/dist/auth/LeakyBucket.js.map +1 -0
  35. package/dist/auth/RemoteJWKSCollector.d.ts +24 -0
  36. package/dist/auth/RemoteJWKSCollector.js +106 -0
  37. package/dist/auth/RemoteJWKSCollector.js.map +1 -0
  38. package/dist/auth/StaticKeyCollector.d.ts +14 -0
  39. package/dist/auth/StaticKeyCollector.js +19 -0
  40. package/dist/auth/StaticKeyCollector.js.map +1 -0
  41. package/dist/auth/SupabaseKeyCollector.d.ts +22 -0
  42. package/dist/auth/SupabaseKeyCollector.js +61 -0
  43. package/dist/auth/SupabaseKeyCollector.js.map +1 -0
  44. package/dist/auth/auth-index.d.ts +10 -0
  45. package/dist/auth/auth-index.js +11 -0
  46. package/dist/auth/auth-index.js.map +1 -0
  47. package/dist/db/db-index.d.ts +1 -0
  48. package/dist/db/db-index.js +2 -0
  49. package/dist/db/db-index.js.map +1 -0
  50. package/dist/db/mongo.d.ts +29 -0
  51. package/dist/db/mongo.js +65 -0
  52. package/dist/db/mongo.js.map +1 -0
  53. package/dist/entry/cli-entry.d.ts +15 -0
  54. package/dist/entry/cli-entry.js +36 -0
  55. package/dist/entry/cli-entry.js.map +1 -0
  56. package/dist/entry/commands/config-command.d.ts +10 -0
  57. package/dist/entry/commands/config-command.js +21 -0
  58. package/dist/entry/commands/config-command.js.map +1 -0
  59. package/dist/entry/commands/migrate-action.d.ts +2 -0
  60. package/dist/entry/commands/migrate-action.js +18 -0
  61. package/dist/entry/commands/migrate-action.js.map +1 -0
  62. package/dist/entry/commands/start-action.d.ts +3 -0
  63. package/dist/entry/commands/start-action.js +15 -0
  64. package/dist/entry/commands/start-action.js.map +1 -0
  65. package/dist/entry/commands/teardown-action.d.ts +2 -0
  66. package/dist/entry/commands/teardown-action.js +17 -0
  67. package/dist/entry/commands/teardown-action.js.map +1 -0
  68. package/dist/entry/entry-index.d.ts +5 -0
  69. package/dist/entry/entry-index.js +6 -0
  70. package/dist/entry/entry-index.js.map +1 -0
  71. package/dist/index.d.ts +24 -0
  72. package/dist/index.js +26 -0
  73. package/dist/index.js.map +1 -0
  74. package/dist/metrics/metrics.d.ts +16 -0
  75. package/dist/metrics/metrics.js +139 -0
  76. package/dist/metrics/metrics.js.map +1 -0
  77. package/dist/migrations/db/migrations/1684951997326-init.d.ts +3 -0
  78. package/dist/migrations/db/migrations/1684951997326-init.js +31 -0
  79. package/dist/migrations/db/migrations/1684951997326-init.js.map +1 -0
  80. package/dist/migrations/db/migrations/1688556755264-initial-sync-rules.d.ts +2 -0
  81. package/dist/migrations/db/migrations/1688556755264-initial-sync-rules.js +5 -0
  82. package/dist/migrations/db/migrations/1688556755264-initial-sync-rules.js.map +1 -0
  83. package/dist/migrations/db/migrations/1702295701188-sync-rule-state.d.ts +3 -0
  84. package/dist/migrations/db/migrations/1702295701188-sync-rule-state.js +54 -0
  85. package/dist/migrations/db/migrations/1702295701188-sync-rule-state.js.map +1 -0
  86. package/dist/migrations/db/migrations/1711543888062-write-checkpoint-index.d.ts +3 -0
  87. package/dist/migrations/db/migrations/1711543888062-write-checkpoint-index.js +27 -0
  88. package/dist/migrations/db/migrations/1711543888062-write-checkpoint-index.js.map +1 -0
  89. package/dist/migrations/db/store.d.ts +3 -0
  90. package/dist/migrations/db/store.js +10 -0
  91. package/dist/migrations/db/store.js.map +1 -0
  92. package/dist/migrations/migrations.d.ts +10 -0
  93. package/dist/migrations/migrations.js +94 -0
  94. package/dist/migrations/migrations.js.map +1 -0
  95. package/dist/replication/ErrorRateLimiter.d.ts +17 -0
  96. package/dist/replication/ErrorRateLimiter.js +42 -0
  97. package/dist/replication/ErrorRateLimiter.js.map +1 -0
  98. package/dist/replication/PgRelation.d.ts +16 -0
  99. package/dist/replication/PgRelation.js +26 -0
  100. package/dist/replication/PgRelation.js.map +1 -0
  101. package/dist/replication/WalConnection.d.ts +34 -0
  102. package/dist/replication/WalConnection.js +190 -0
  103. package/dist/replication/WalConnection.js.map +1 -0
  104. package/dist/replication/WalStream.d.ts +58 -0
  105. package/dist/replication/WalStream.js +517 -0
  106. package/dist/replication/WalStream.js.map +1 -0
  107. package/dist/replication/WalStreamManager.d.ts +30 -0
  108. package/dist/replication/WalStreamManager.js +199 -0
  109. package/dist/replication/WalStreamManager.js.map +1 -0
  110. package/dist/replication/WalStreamRunner.d.ts +38 -0
  111. package/dist/replication/WalStreamRunner.js +155 -0
  112. package/dist/replication/WalStreamRunner.js.map +1 -0
  113. package/dist/replication/replication-index.d.ts +7 -0
  114. package/dist/replication/replication-index.js +8 -0
  115. package/dist/replication/replication-index.js.map +1 -0
  116. package/dist/replication/util.d.ts +9 -0
  117. package/dist/replication/util.js +62 -0
  118. package/dist/replication/util.js.map +1 -0
  119. package/dist/routes/admin.d.ts +7 -0
  120. package/dist/routes/admin.js +192 -0
  121. package/dist/routes/admin.js.map +1 -0
  122. package/dist/routes/auth.d.ts +58 -0
  123. package/dist/routes/auth.js +182 -0
  124. package/dist/routes/auth.js.map +1 -0
  125. package/dist/routes/checkpointing.d.ts +3 -0
  126. package/dist/routes/checkpointing.js +30 -0
  127. package/dist/routes/checkpointing.js.map +1 -0
  128. package/dist/routes/dev.d.ts +6 -0
  129. package/dist/routes/dev.js +163 -0
  130. package/dist/routes/dev.js.map +1 -0
  131. package/dist/routes/route-generators.d.ts +15 -0
  132. package/dist/routes/route-generators.js +32 -0
  133. package/dist/routes/route-generators.js.map +1 -0
  134. package/dist/routes/router-socket.d.ts +10 -0
  135. package/dist/routes/router-socket.js +5 -0
  136. package/dist/routes/router-socket.js.map +1 -0
  137. package/dist/routes/router.d.ts +13 -0
  138. package/dist/routes/router.js +2 -0
  139. package/dist/routes/router.js.map +1 -0
  140. package/dist/routes/routes-index.d.ts +4 -0
  141. package/dist/routes/routes-index.js +5 -0
  142. package/dist/routes/routes-index.js.map +1 -0
  143. package/dist/routes/socket-route.d.ts +2 -0
  144. package/dist/routes/socket-route.js +119 -0
  145. package/dist/routes/socket-route.js.map +1 -0
  146. package/dist/routes/sync-rules.d.ts +6 -0
  147. package/dist/routes/sync-rules.js +182 -0
  148. package/dist/routes/sync-rules.js.map +1 -0
  149. package/dist/routes/sync-stream.d.ts +5 -0
  150. package/dist/routes/sync-stream.js +74 -0
  151. package/dist/routes/sync-stream.js.map +1 -0
  152. package/dist/runner/teardown.d.ts +2 -0
  153. package/dist/runner/teardown.js +79 -0
  154. package/dist/runner/teardown.js.map +1 -0
  155. package/dist/storage/BucketStorage.d.ts +298 -0
  156. package/dist/storage/BucketStorage.js +25 -0
  157. package/dist/storage/BucketStorage.js.map +1 -0
  158. package/dist/storage/MongoBucketStorage.d.ts +51 -0
  159. package/dist/storage/MongoBucketStorage.js +388 -0
  160. package/dist/storage/MongoBucketStorage.js.map +1 -0
  161. package/dist/storage/SourceTable.d.ts +39 -0
  162. package/dist/storage/SourceTable.js +50 -0
  163. package/dist/storage/SourceTable.js.map +1 -0
  164. package/dist/storage/mongo/MongoBucketBatch.d.ts +48 -0
  165. package/dist/storage/mongo/MongoBucketBatch.js +584 -0
  166. package/dist/storage/mongo/MongoBucketBatch.js.map +1 -0
  167. package/dist/storage/mongo/MongoIdSequence.d.ts +12 -0
  168. package/dist/storage/mongo/MongoIdSequence.js +21 -0
  169. package/dist/storage/mongo/MongoIdSequence.js.map +1 -0
  170. package/dist/storage/mongo/MongoPersistedSyncRules.d.ts +9 -0
  171. package/dist/storage/mongo/MongoPersistedSyncRules.js +9 -0
  172. package/dist/storage/mongo/MongoPersistedSyncRules.js.map +1 -0
  173. package/dist/storage/mongo/MongoPersistedSyncRulesContent.d.ts +20 -0
  174. package/dist/storage/mongo/MongoPersistedSyncRulesContent.js +26 -0
  175. package/dist/storage/mongo/MongoPersistedSyncRulesContent.js.map +1 -0
  176. package/dist/storage/mongo/MongoSyncBucketStorage.d.ts +27 -0
  177. package/dist/storage/mongo/MongoSyncBucketStorage.js +379 -0
  178. package/dist/storage/mongo/MongoSyncBucketStorage.js.map +1 -0
  179. package/dist/storage/mongo/MongoSyncRulesLock.d.ts +16 -0
  180. package/dist/storage/mongo/MongoSyncRulesLock.js +65 -0
  181. package/dist/storage/mongo/MongoSyncRulesLock.js.map +1 -0
  182. package/dist/storage/mongo/OperationBatch.d.ts +26 -0
  183. package/dist/storage/mongo/OperationBatch.js +101 -0
  184. package/dist/storage/mongo/OperationBatch.js.map +1 -0
  185. package/dist/storage/mongo/PersistedBatch.d.ts +42 -0
  186. package/dist/storage/mongo/PersistedBatch.js +200 -0
  187. package/dist/storage/mongo/PersistedBatch.js.map +1 -0
  188. package/dist/storage/mongo/db.d.ts +23 -0
  189. package/dist/storage/mongo/db.js +34 -0
  190. package/dist/storage/mongo/db.js.map +1 -0
  191. package/dist/storage/mongo/models.d.ts +137 -0
  192. package/dist/storage/mongo/models.js +27 -0
  193. package/dist/storage/mongo/models.js.map +1 -0
  194. package/dist/storage/mongo/util.d.ts +26 -0
  195. package/dist/storage/mongo/util.js +81 -0
  196. package/dist/storage/mongo/util.js.map +1 -0
  197. package/dist/storage/storage-index.d.ts +14 -0
  198. package/dist/storage/storage-index.js +15 -0
  199. package/dist/storage/storage-index.js.map +1 -0
  200. package/dist/sync/BroadcastIterable.d.ts +38 -0
  201. package/dist/sync/BroadcastIterable.js +153 -0
  202. package/dist/sync/BroadcastIterable.js.map +1 -0
  203. package/dist/sync/LastValueSink.d.ts +25 -0
  204. package/dist/sync/LastValueSink.js +84 -0
  205. package/dist/sync/LastValueSink.js.map +1 -0
  206. package/dist/sync/merge.d.ts +39 -0
  207. package/dist/sync/merge.js +175 -0
  208. package/dist/sync/merge.js.map +1 -0
  209. package/dist/sync/safeRace.d.ts +1 -0
  210. package/dist/sync/safeRace.js +91 -0
  211. package/dist/sync/safeRace.js.map +1 -0
  212. package/dist/sync/sync-index.d.ts +6 -0
  213. package/dist/sync/sync-index.js +7 -0
  214. package/dist/sync/sync-index.js.map +1 -0
  215. package/dist/sync/sync.d.ts +18 -0
  216. package/dist/sync/sync.js +248 -0
  217. package/dist/sync/sync.js.map +1 -0
  218. package/dist/sync/util.d.ts +26 -0
  219. package/dist/sync/util.js +73 -0
  220. package/dist/sync/util.js.map +1 -0
  221. package/dist/system/CorePowerSyncSystem.d.ts +18 -0
  222. package/dist/system/CorePowerSyncSystem.js +28 -0
  223. package/dist/system/CorePowerSyncSystem.js.map +1 -0
  224. package/dist/util/Mutex.d.ts +47 -0
  225. package/dist/util/Mutex.js +132 -0
  226. package/dist/util/Mutex.js.map +1 -0
  227. package/dist/util/PgManager.d.ts +24 -0
  228. package/dist/util/PgManager.js +55 -0
  229. package/dist/util/PgManager.js.map +1 -0
  230. package/dist/util/alerting.d.ts +4 -0
  231. package/dist/util/alerting.js +14 -0
  232. package/dist/util/alerting.js.map +1 -0
  233. package/dist/util/config/collectors/config-collector.d.ts +29 -0
  234. package/dist/util/config/collectors/config-collector.js +116 -0
  235. package/dist/util/config/collectors/config-collector.js.map +1 -0
  236. package/dist/util/config/collectors/impl/base64-config-collector.d.ts +6 -0
  237. package/dist/util/config/collectors/impl/base64-config-collector.js +15 -0
  238. package/dist/util/config/collectors/impl/base64-config-collector.js.map +1 -0
  239. package/dist/util/config/collectors/impl/fallback-config-collector.d.ts +11 -0
  240. package/dist/util/config/collectors/impl/fallback-config-collector.js +19 -0
  241. package/dist/util/config/collectors/impl/fallback-config-collector.js.map +1 -0
  242. package/dist/util/config/collectors/impl/filesystem-config-collector.d.ts +6 -0
  243. package/dist/util/config/collectors/impl/filesystem-config-collector.js +35 -0
  244. package/dist/util/config/collectors/impl/filesystem-config-collector.js.map +1 -0
  245. package/dist/util/config/compound-config-collector.d.ts +32 -0
  246. package/dist/util/config/compound-config-collector.js +126 -0
  247. package/dist/util/config/compound-config-collector.js.map +1 -0
  248. package/dist/util/config/sync-rules/impl/base64-sync-rules-collector.d.ts +7 -0
  249. package/dist/util/config/sync-rules/impl/base64-sync-rules-collector.js +17 -0
  250. package/dist/util/config/sync-rules/impl/base64-sync-rules-collector.js.map +1 -0
  251. package/dist/util/config/sync-rules/impl/filesystem-sync-rules-collector.d.ts +7 -0
  252. package/dist/util/config/sync-rules/impl/filesystem-sync-rules-collector.js +21 -0
  253. package/dist/util/config/sync-rules/impl/filesystem-sync-rules-collector.js.map +1 -0
  254. package/dist/util/config/sync-rules/impl/inline-sync-rules-collector.d.ts +7 -0
  255. package/dist/util/config/sync-rules/impl/inline-sync-rules-collector.js +17 -0
  256. package/dist/util/config/sync-rules/impl/inline-sync-rules-collector.js.map +1 -0
  257. package/dist/util/config/sync-rules/sync-collector.d.ts +6 -0
  258. package/dist/util/config/sync-rules/sync-collector.js +3 -0
  259. package/dist/util/config/sync-rules/sync-collector.js.map +1 -0
  260. package/dist/util/config/types.d.ts +53 -0
  261. package/dist/util/config/types.js +7 -0
  262. package/dist/util/config/types.js.map +1 -0
  263. package/dist/util/config.d.ts +7 -0
  264. package/dist/util/config.js +35 -0
  265. package/dist/util/config.js.map +1 -0
  266. package/dist/util/env.d.ts +10 -0
  267. package/dist/util/env.js +25 -0
  268. package/dist/util/env.js.map +1 -0
  269. package/dist/util/memory-tracking.d.ts +7 -0
  270. package/dist/util/memory-tracking.js +58 -0
  271. package/dist/util/memory-tracking.js.map +1 -0
  272. package/dist/util/migration_lib.d.ts +11 -0
  273. package/dist/util/migration_lib.js +64 -0
  274. package/dist/util/migration_lib.js.map +1 -0
  275. package/dist/util/pgwire_utils.d.ts +24 -0
  276. package/dist/util/pgwire_utils.js +117 -0
  277. package/dist/util/pgwire_utils.js.map +1 -0
  278. package/dist/util/populate_test_data.d.ts +8 -0
  279. package/dist/util/populate_test_data.js +65 -0
  280. package/dist/util/populate_test_data.js.map +1 -0
  281. package/dist/util/protocol-types.d.ts +178 -0
  282. package/dist/util/protocol-types.js +38 -0
  283. package/dist/util/protocol-types.js.map +1 -0
  284. package/dist/util/secs.d.ts +2 -0
  285. package/dist/util/secs.js +49 -0
  286. package/dist/util/secs.js.map +1 -0
  287. package/dist/util/util-index.d.ts +22 -0
  288. package/dist/util/util-index.js +23 -0
  289. package/dist/util/util-index.js.map +1 -0
  290. package/dist/util/utils.d.ts +14 -0
  291. package/dist/util/utils.js +75 -0
  292. package/dist/util/utils.js.map +1 -0
  293. package/package.json +55 -0
  294. package/src/api/api-index.ts +2 -0
  295. package/src/api/diagnostics.ts +221 -0
  296. package/src/api/schema.ts +99 -0
  297. package/src/auth/CachedKeyCollector.ts +132 -0
  298. package/src/auth/CompoundKeyCollector.ts +33 -0
  299. package/src/auth/JwtPayload.ts +11 -0
  300. package/src/auth/KeyCollector.ts +27 -0
  301. package/src/auth/KeySpec.ts +67 -0
  302. package/src/auth/KeyStore.ts +156 -0
  303. package/src/auth/LeakyBucket.ts +66 -0
  304. package/src/auth/RemoteJWKSCollector.ts +130 -0
  305. package/src/auth/StaticKeyCollector.ts +21 -0
  306. package/src/auth/SupabaseKeyCollector.ts +67 -0
  307. package/src/auth/auth-index.ts +10 -0
  308. package/src/db/db-index.ts +1 -0
  309. package/src/db/mongo.ts +72 -0
  310. package/src/entry/cli-entry.ts +41 -0
  311. package/src/entry/commands/config-command.ts +36 -0
  312. package/src/entry/commands/migrate-action.ts +25 -0
  313. package/src/entry/commands/start-action.ts +24 -0
  314. package/src/entry/commands/teardown-action.ts +23 -0
  315. package/src/entry/entry-index.ts +5 -0
  316. package/src/index.ts +37 -0
  317. package/src/metrics/metrics.ts +169 -0
  318. package/src/migrations/db/migrations/1684951997326-init.ts +33 -0
  319. package/src/migrations/db/migrations/1688556755264-initial-sync-rules.ts +5 -0
  320. package/src/migrations/db/migrations/1702295701188-sync-rule-state.ts +99 -0
  321. package/src/migrations/db/migrations/1711543888062-write-checkpoint-index.ts +32 -0
  322. package/src/migrations/db/store.ts +11 -0
  323. package/src/migrations/migrations.ts +122 -0
  324. package/src/replication/ErrorRateLimiter.ts +49 -0
  325. package/src/replication/PgRelation.ts +42 -0
  326. package/src/replication/WalConnection.ts +227 -0
  327. package/src/replication/WalStream.ts +626 -0
  328. package/src/replication/WalStreamManager.ts +214 -0
  329. package/src/replication/WalStreamRunner.ts +180 -0
  330. package/src/replication/replication-index.ts +7 -0
  331. package/src/replication/util.ts +76 -0
  332. package/src/routes/admin.ts +229 -0
  333. package/src/routes/auth.ts +209 -0
  334. package/src/routes/checkpointing.ts +38 -0
  335. package/src/routes/dev.ts +194 -0
  336. package/src/routes/route-generators.ts +39 -0
  337. package/src/routes/router-socket.ts +13 -0
  338. package/src/routes/router.ts +17 -0
  339. package/src/routes/routes-index.ts +5 -0
  340. package/src/routes/socket-route.ts +131 -0
  341. package/src/routes/sync-rules.ts +210 -0
  342. package/src/routes/sync-stream.ts +92 -0
  343. package/src/runner/teardown.ts +91 -0
  344. package/src/storage/BucketStorage.ts +386 -0
  345. package/src/storage/MongoBucketStorage.ts +493 -0
  346. package/src/storage/SourceTable.ts +60 -0
  347. package/src/storage/mongo/MongoBucketBatch.ts +756 -0
  348. package/src/storage/mongo/MongoIdSequence.ts +24 -0
  349. package/src/storage/mongo/MongoPersistedSyncRules.ts +16 -0
  350. package/src/storage/mongo/MongoPersistedSyncRulesContent.ts +47 -0
  351. package/src/storage/mongo/MongoSyncBucketStorage.ts +517 -0
  352. package/src/storage/mongo/MongoSyncRulesLock.ts +81 -0
  353. package/src/storage/mongo/OperationBatch.ts +115 -0
  354. package/src/storage/mongo/PersistedBatch.ts +245 -0
  355. package/src/storage/mongo/db.ts +69 -0
  356. package/src/storage/mongo/models.ts +157 -0
  357. package/src/storage/mongo/util.ts +88 -0
  358. package/src/storage/storage-index.ts +15 -0
  359. package/src/sync/BroadcastIterable.ts +161 -0
  360. package/src/sync/LastValueSink.ts +100 -0
  361. package/src/sync/merge.ts +200 -0
  362. package/src/sync/safeRace.ts +99 -0
  363. package/src/sync/sync-index.ts +6 -0
  364. package/src/sync/sync.ts +312 -0
  365. package/src/sync/util.ts +98 -0
  366. package/src/system/CorePowerSyncSystem.ts +43 -0
  367. package/src/util/Mutex.ts +159 -0
  368. package/src/util/PgManager.ts +64 -0
  369. package/src/util/alerting.ts +17 -0
  370. package/src/util/config/collectors/config-collector.ts +141 -0
  371. package/src/util/config/collectors/impl/base64-config-collector.ts +18 -0
  372. package/src/util/config/collectors/impl/fallback-config-collector.ts +22 -0
  373. package/src/util/config/collectors/impl/filesystem-config-collector.ts +41 -0
  374. package/src/util/config/compound-config-collector.ts +171 -0
  375. package/src/util/config/sync-rules/impl/base64-sync-rules-collector.ts +21 -0
  376. package/src/util/config/sync-rules/impl/filesystem-sync-rules-collector.ts +26 -0
  377. package/src/util/config/sync-rules/impl/inline-sync-rules-collector.ts +21 -0
  378. package/src/util/config/sync-rules/sync-collector.ts +8 -0
  379. package/src/util/config/types.ts +60 -0
  380. package/src/util/config.ts +39 -0
  381. package/src/util/env.ts +28 -0
  382. package/src/util/memory-tracking.ts +67 -0
  383. package/src/util/migration_lib.ts +79 -0
  384. package/src/util/pgwire_utils.ts +139 -0
  385. package/src/util/populate_test_data.ts +78 -0
  386. package/src/util/protocol-types.ts +223 -0
  387. package/src/util/secs.ts +54 -0
  388. package/src/util/util-index.ts +25 -0
  389. package/src/util/utils.ts +102 -0
  390. package/test/src/__snapshots__/pg_test.test.ts.snap +256 -0
  391. package/test/src/__snapshots__/sync.test.ts.snap +235 -0
  392. package/test/src/auth.test.ts +340 -0
  393. package/test/src/broadcast_iterable.test.ts +156 -0
  394. package/test/src/data_storage.test.ts +1176 -0
  395. package/test/src/env.ts +8 -0
  396. package/test/src/large_batch.test.ts +194 -0
  397. package/test/src/merge_iterable.test.ts +355 -0
  398. package/test/src/pg_test.test.ts +432 -0
  399. package/test/src/schema_changes.test.ts +545 -0
  400. package/test/src/slow_tests.test.ts +257 -0
  401. package/test/src/sql_functions.test.ts +254 -0
  402. package/test/src/sql_operators.test.ts +132 -0
  403. package/test/src/sync.test.ts +293 -0
  404. package/test/src/sync_rules.test.ts +1051 -0
  405. package/test/src/util.ts +67 -0
  406. package/test/src/validation.test.ts +63 -0
  407. package/test/src/wal_stream.test.ts +310 -0
  408. package/test/src/wal_stream_utils.ts +147 -0
  409. package/test/tsconfig.json +20 -0
  410. package/tsconfig.json +20 -0
  411. package/tsconfig.tsbuildinfo +1 -0
  412. package/vitest.config.ts +11 -0
@@ -0,0 +1,1051 @@
1
+ import {
2
+ DEFAULT_SCHEMA,
3
+ DEFAULT_TAG,
4
+ DartSchemaGenerator,
5
+ ExpressionType,
6
+ JsSchemaGenerator,
7
+ SourceTableInterface,
8
+ SqlDataQuery,
9
+ SqlParameterQuery,
10
+ SqlSyncRules,
11
+ StaticSchema,
12
+ normalizeTokenParameters
13
+ } from '@powersync/service-sync-rules';
14
+ import { describe, expect, test } from 'vitest';
15
+ import { SourceTable } from '../../src/storage/SourceTable.js';
16
+
17
+ class TestSourceTable implements SourceTableInterface {
18
+ readonly connectionTag = DEFAULT_TAG;
19
+ readonly schema = DEFAULT_SCHEMA;
20
+
21
+ constructor(public readonly table: string) {}
22
+ }
23
+
24
+ const ASSETS = new TestSourceTable('assets');
25
+ const USERS = new TestSourceTable('users');
26
+
27
+ describe('sync rules', () => {
28
+ test('parse empty sync rules', () => {
29
+ const rules = SqlSyncRules.fromYaml('bucket_definitions: {}');
30
+ expect(rules.bucket_descriptors).toEqual([]);
31
+ });
32
+
33
+ test('parse global sync rules', () => {
34
+ const rules = SqlSyncRules.fromYaml(`
35
+ bucket_definitions:
36
+ mybucket:
37
+ data:
38
+ - SELECT id, description FROM assets
39
+ `);
40
+ const bucket = rules.bucket_descriptors[0];
41
+ expect(bucket.name).toEqual('mybucket');
42
+ expect(bucket.bucket_parameters).toEqual([]);
43
+ const dataQuery = bucket.data_queries[0];
44
+ expect(dataQuery.bucket_parameters).toEqual([]);
45
+ expect(dataQuery.columnOutputNames()).toEqual(['id', 'description']);
46
+ expect(rules.evaluateRow({ sourceTable: ASSETS, record: { id: 'asset1', description: 'test' } })).toEqual([
47
+ {
48
+ ruleId: '1',
49
+ table: 'assets',
50
+ id: 'asset1',
51
+ data: {
52
+ id: 'asset1',
53
+ description: 'test'
54
+ },
55
+ bucket: 'mybucket[]'
56
+ }
57
+ ]);
58
+ expect(rules.getStaticBucketIds({ token_parameters: {}, user_parameters: {} })).toEqual(['mybucket[]']);
59
+ });
60
+
61
+ test('parse global sync rules with filter', () => {
62
+ const rules = SqlSyncRules.fromYaml(`
63
+ bucket_definitions:
64
+ mybucket:
65
+ parameters: SELECT WHERE token_parameters.is_admin
66
+ data: []
67
+ `);
68
+ const bucket = rules.bucket_descriptors[0];
69
+ expect(bucket.bucket_parameters).toEqual([]);
70
+ const param_query = bucket.global_parameter_queries[0];
71
+
72
+ expect(param_query.filter!.filter({ token_parameters: { is_admin: 1n } })).toEqual([{}]);
73
+ expect(param_query.filter!.filter({ token_parameters: { is_admin: 0n } })).toEqual([]);
74
+ expect(rules.getStaticBucketIds(normalizeTokenParameters({ is_admin: true }))).toEqual(['mybucket[]']);
75
+ expect(rules.getStaticBucketIds(normalizeTokenParameters({ is_admin: false }))).toEqual([]);
76
+ expect(rules.getStaticBucketIds(normalizeTokenParameters({}))).toEqual([]);
77
+ });
78
+
79
+ test('parse global sync rules with table filter', () => {
80
+ const rules = SqlSyncRules.fromYaml(`
81
+ bucket_definitions:
82
+ mybucket:
83
+ parameters: SELECT FROM users WHERE users.id = token_parameters.user_id AND users.is_admin
84
+ data: []
85
+ `);
86
+ const bucket = rules.bucket_descriptors[0];
87
+ expect(bucket.bucket_parameters).toEqual([]);
88
+ const param_query = bucket.parameter_queries[0];
89
+ expect(param_query.bucket_parameters).toEqual([]);
90
+ expect(rules.evaluateParameterRow(USERS, { id: 'user1', is_admin: 1 })).toEqual([
91
+ {
92
+ bucket_parameters: [{}],
93
+ lookup: ['mybucket', '1', 'user1']
94
+ }
95
+ ]);
96
+ expect(rules.evaluateParameterRow(USERS, { id: 'user1', is_admin: 0 })).toEqual([]);
97
+ });
98
+
99
+ test('parse bucket with parameters', () => {
100
+ const rules = SqlSyncRules.fromYaml(`
101
+ bucket_definitions:
102
+ mybucket:
103
+ parameters: SELECT token_parameters.user_id
104
+ data:
105
+ - SELECT id, description FROM assets WHERE assets.user_id = bucket.user_id AND NOT assets.archived
106
+ `);
107
+ const bucket = rules.bucket_descriptors[0];
108
+ expect(bucket.bucket_parameters).toEqual(['user_id']);
109
+ const param_query = bucket.global_parameter_queries[0];
110
+ expect(param_query.bucket_parameters).toEqual(['user_id']);
111
+ expect(rules.getStaticBucketIds(normalizeTokenParameters({ user_id: 'user1' }))).toEqual(['mybucket["user1"]']);
112
+
113
+ const data_query = bucket.data_queries[0];
114
+ expect(data_query.bucket_parameters).toEqual(['user_id']);
115
+ expect(
116
+ rules.evaluateRow({
117
+ sourceTable: ASSETS,
118
+ record: { id: 'asset1', description: 'test', user_id: 'user1' }
119
+ })
120
+ ).toEqual([
121
+ {
122
+ ruleId: '1',
123
+ bucket: 'mybucket["user1"]',
124
+ id: 'asset1',
125
+ data: {
126
+ id: 'asset1',
127
+ description: 'test'
128
+ },
129
+ table: 'assets'
130
+ }
131
+ ]);
132
+ expect(
133
+ rules.evaluateRow({
134
+ sourceTable: ASSETS,
135
+ record: { id: 'asset1', description: 'test', user_id: 'user1', archived: 1 }
136
+ })
137
+ ).toEqual([]);
138
+ });
139
+
140
+ test('parse bucket with parameters and OR condition', () => {
141
+ const rules = SqlSyncRules.fromYaml(`
142
+ bucket_definitions:
143
+ mybucket:
144
+ parameters: SELECT token_parameters.user_id
145
+ data:
146
+ - SELECT id, description FROM assets WHERE assets.user_id = bucket.user_id OR assets.owner_id = bucket.user_id
147
+ `);
148
+ const bucket = rules.bucket_descriptors[0];
149
+ expect(bucket.bucket_parameters).toEqual(['user_id']);
150
+ const param_query = bucket.global_parameter_queries[0];
151
+ expect(param_query.bucket_parameters).toEqual(['user_id']);
152
+ expect(rules.getStaticBucketIds(normalizeTokenParameters({ user_id: 'user1' }))).toEqual(['mybucket["user1"]']);
153
+
154
+ const data_query = bucket.data_queries[0];
155
+ expect(data_query.bucket_parameters).toEqual(['user_id']);
156
+ expect(
157
+ rules.evaluateRow({
158
+ sourceTable: ASSETS,
159
+ record: { id: 'asset1', description: 'test', user_id: 'user1' }
160
+ })
161
+ ).toEqual([
162
+ {
163
+ ruleId: '1',
164
+ bucket: 'mybucket["user1"]',
165
+ id: 'asset1',
166
+ data: {
167
+ id: 'asset1',
168
+ description: 'test'
169
+ },
170
+ table: 'assets'
171
+ }
172
+ ]);
173
+ expect(
174
+ rules.evaluateRow({
175
+ sourceTable: ASSETS,
176
+ record: { id: 'asset1', description: 'test', owner_id: 'user1' }
177
+ })
178
+ ).toEqual([
179
+ {
180
+ ruleId: '1',
181
+ bucket: 'mybucket["user1"]',
182
+ id: 'asset1',
183
+ data: {
184
+ id: 'asset1',
185
+ description: 'test'
186
+ },
187
+ table: 'assets'
188
+ }
189
+ ]);
190
+ });
191
+
192
+ test('parse bucket with parameters and invalid OR condition', () => {
193
+ expect(() => {
194
+ const rules = SqlSyncRules.fromYaml(`
195
+ bucket_definitions:
196
+ mybucket:
197
+ parameters: SELECT token_parameters.user_id
198
+ data:
199
+ - SELECT id, description FROM assets WHERE assets.user_id = bucket.user_id AND (assets.user_id = bucket.foo OR assets.other_id = bucket.bar)
200
+ `);
201
+ }).toThrowError(/must use the same parameters/);
202
+ });
203
+
204
+ test('reject unsupported queries', () => {
205
+ expect(
206
+ SqlSyncRules.validate(`
207
+ bucket_definitions:
208
+ mybucket:
209
+ parameters: SELECT token_parameters.user_id LIMIT 1
210
+ data: []
211
+ `)
212
+ ).toMatchObject([{ message: 'LIMIT is not supported' }]);
213
+
214
+ expect(
215
+ SqlSyncRules.validate(`
216
+ bucket_definitions:
217
+ mybucket:
218
+ data:
219
+ - SELECT DISTINCT id, description FROM assets
220
+ `)
221
+ ).toMatchObject([{ message: 'DISTINCT is not supported' }]);
222
+
223
+ expect(
224
+ SqlSyncRules.validate(`
225
+ bucket_definitions:
226
+ mybucket:
227
+ parameters: SELECT token_parameters.user_id OFFSET 10
228
+ data: []
229
+ `)
230
+ ).toMatchObject([{ message: 'LIMIT is not supported' }]);
231
+
232
+ expect(() => {
233
+ const rules = SqlSyncRules.fromYaml(`
234
+ bucket_definitions:
235
+ mybucket:
236
+ parameters: SELECT token_parameters.user_id FOR UPDATE SKIP LOCKED
237
+ data: []
238
+ `);
239
+ }).toThrowError(/SKIP is not supported/);
240
+
241
+ expect(() => {
242
+ const rules = SqlSyncRules.fromYaml(`
243
+ bucket_definitions:
244
+ mybucket:
245
+ parameters: SELECT token_parameters.user_id FOR UPDATE
246
+ data: []
247
+ `);
248
+ }).toThrowError(/FOR is not supported/);
249
+
250
+ expect(() => {
251
+ const rules = SqlSyncRules.fromYaml(`
252
+ bucket_definitions:
253
+ mybucket:
254
+ data:
255
+ - SELECT id, description FROM assets ORDER BY id
256
+ `);
257
+ }).toThrowError(/ORDER BY is not supported/);
258
+ });
259
+
260
+ test('transforming things', () => {
261
+ const rules = SqlSyncRules.fromYaml(`
262
+ bucket_definitions:
263
+ mybucket:
264
+ parameters: SELECT upper(token_parameters.user_id) AS user_id
265
+ data:
266
+ - SELECT id, upper(description) AS description_upper FROM assets WHERE upper(assets.user_id) = bucket.user_id AND NOT assets.archived
267
+ `);
268
+ const bucket = rules.bucket_descriptors[0];
269
+ expect(bucket.bucket_parameters).toEqual(['user_id']);
270
+ expect(rules.getStaticBucketIds(normalizeTokenParameters({ user_id: 'user1' }))).toEqual(['mybucket["USER1"]']);
271
+
272
+ expect(
273
+ rules.evaluateRow({
274
+ sourceTable: ASSETS,
275
+ record: { id: 'asset1', description: 'test', user_id: 'user1' }
276
+ })
277
+ ).toEqual([
278
+ {
279
+ ruleId: '1',
280
+ bucket: 'mybucket["USER1"]',
281
+ id: 'asset1',
282
+ data: {
283
+ id: 'asset1',
284
+ description_upper: 'TEST'
285
+ },
286
+ table: 'assets'
287
+ }
288
+ ]);
289
+ });
290
+
291
+ test('transforming things with upper-case functions', () => {
292
+ // Testing that we can use different case for the function names
293
+ const rules = SqlSyncRules.fromYaml(`
294
+ bucket_definitions:
295
+ mybucket:
296
+ parameters: SELECT UPPER(token_parameters.user_id) AS user_id
297
+ data:
298
+ - SELECT id, UPPER(description) AS description_upper FROM assets WHERE UPPER(assets.user_id) = bucket.user_id AND NOT assets.archived
299
+ `);
300
+ const bucket = rules.bucket_descriptors[0];
301
+ expect(bucket.bucket_parameters).toEqual(['user_id']);
302
+ expect(rules.getStaticBucketIds(normalizeTokenParameters({ user_id: 'user1' }))).toEqual(['mybucket["USER1"]']);
303
+
304
+ expect(
305
+ rules.evaluateRow({
306
+ sourceTable: ASSETS,
307
+ record: { id: 'asset1', description: 'test', user_id: 'user1' }
308
+ })
309
+ ).toEqual([
310
+ {
311
+ ruleId: '1',
312
+ bucket: 'mybucket["USER1"]',
313
+ id: 'asset1',
314
+ data: {
315
+ id: 'asset1',
316
+ description_upper: 'TEST'
317
+ },
318
+ table: 'assets'
319
+ }
320
+ ]);
321
+ });
322
+
323
+ test('transforming json', () => {
324
+ const rules = SqlSyncRules.fromYaml(`
325
+ bucket_definitions:
326
+ mybucket:
327
+ data:
328
+ - SELECT id, data ->> 'count' AS count, data -> 'bool' AS bool1, data ->> 'bool' AS bool2, 'true' ->> '$' as bool3, json_extract(data, '$.bool') AS bool4 FROM assets
329
+ `);
330
+ expect(
331
+ rules.evaluateRow({
332
+ sourceTable: ASSETS,
333
+ record: { id: 'asset1', data: JSON.stringify({ count: 5, bool: true }) }
334
+ })
335
+ ).toEqual([
336
+ {
337
+ ruleId: '1',
338
+ bucket: 'mybucket[]',
339
+ id: 'asset1',
340
+ data: {
341
+ id: 'asset1',
342
+ count: 5n,
343
+ bool1: 'true',
344
+ bool2: 1n,
345
+ bool3: 1n,
346
+ bool4: 1n
347
+ },
348
+ table: 'assets'
349
+ }
350
+ ]);
351
+ });
352
+
353
+ test('IN json', () => {
354
+ const rules = SqlSyncRules.fromYaml(`
355
+ bucket_definitions:
356
+ mybucket:
357
+ parameters: SELECT token_parameters.region_id
358
+ data:
359
+ - SELECT id, description FROM assets WHERE bucket.region_id IN assets.region_ids
360
+ `);
361
+
362
+ expect(
363
+ rules.evaluateRow({
364
+ sourceTable: ASSETS,
365
+ record: {
366
+ id: 'asset1',
367
+ description: 'test',
368
+ region_ids: JSON.stringify(['region1', 'region2'])
369
+ }
370
+ })
371
+ ).toEqual([
372
+ {
373
+ ruleId: '1',
374
+ bucket: 'mybucket["region1"]',
375
+ id: 'asset1',
376
+ data: {
377
+ id: 'asset1',
378
+ description: 'test'
379
+ },
380
+ table: 'assets'
381
+ },
382
+ {
383
+ ruleId: '1',
384
+ bucket: 'mybucket["region2"]',
385
+ id: 'asset1',
386
+ data: {
387
+ id: 'asset1',
388
+ description: 'test'
389
+ },
390
+ table: 'assets'
391
+ }
392
+ ]);
393
+ });
394
+
395
+ test('direct boolean param', () => {
396
+ const rules = SqlSyncRules.fromYaml(`
397
+ bucket_definitions:
398
+ mybucket:
399
+ parameters: SELECT token_parameters.is_admin
400
+ data:
401
+ - SELECT id, description, role, 'admin' as rule FROM assets WHERE bucket.is_admin
402
+ - SELECT id, description, role, 'normal' as rule FROM assets WHERE (bucket.is_admin OR bucket.is_admin = false) AND assets.role != 'admin'
403
+ `);
404
+
405
+ expect(
406
+ rules.evaluateRow({ sourceTable: ASSETS, record: { id: 'asset1', description: 'test', role: 'admin' } })
407
+ ).toEqual([
408
+ {
409
+ ruleId: '1',
410
+ bucket: 'mybucket[1]',
411
+ id: 'asset1',
412
+ data: {
413
+ id: 'asset1',
414
+ description: 'test',
415
+ role: 'admin',
416
+ rule: 'admin'
417
+ },
418
+ table: 'assets'
419
+ }
420
+ ]);
421
+
422
+ // TODO: Deduplicate somewhere
423
+ expect(
424
+ rules.evaluateRow({ sourceTable: ASSETS, record: { id: 'asset2', description: 'test', role: 'normal' } })
425
+ ).toEqual([
426
+ {
427
+ ruleId: '1',
428
+ bucket: 'mybucket[1]',
429
+ id: 'asset2',
430
+ data: {
431
+ id: 'asset2',
432
+ description: 'test',
433
+ role: 'normal',
434
+ rule: 'admin'
435
+ },
436
+ table: 'assets'
437
+ },
438
+ {
439
+ ruleId: '2',
440
+ bucket: 'mybucket[1]',
441
+ id: 'asset2',
442
+ data: {
443
+ id: 'asset2',
444
+ description: 'test',
445
+ role: 'normal',
446
+ rule: 'normal'
447
+ },
448
+ table: 'assets'
449
+ },
450
+ {
451
+ ruleId: '2',
452
+ bucket: 'mybucket[0]',
453
+ id: 'asset2',
454
+ data: {
455
+ id: 'asset2',
456
+ description: 'test',
457
+ role: 'normal',
458
+ rule: 'normal'
459
+ },
460
+ table: 'assets'
461
+ }
462
+ ]);
463
+
464
+ expect(rules.getStaticBucketIds(normalizeTokenParameters({ is_admin: true }))).toEqual(['mybucket[1]']);
465
+ });
466
+
467
+ test('token_parameters IN query', function () {
468
+ const sql = 'SELECT id as group_id FROM groups WHERE token_parameters.user_id IN groups.user_ids';
469
+ const query = SqlParameterQuery.fromSql('mybucket', sql) as SqlParameterQuery;
470
+ query.id = '1';
471
+ expect(query.evaluateParameterRow({ id: 'group1', user_ids: JSON.stringify(['user1', 'user2']) })).toEqual([
472
+ {
473
+ lookup: ['mybucket', '1', 'user1'],
474
+ bucket_parameters: [
475
+ {
476
+ group_id: 'group1'
477
+ }
478
+ ]
479
+ },
480
+ {
481
+ lookup: ['mybucket', '1', 'user2'],
482
+ bucket_parameters: [
483
+ {
484
+ group_id: 'group1'
485
+ }
486
+ ]
487
+ }
488
+ ]);
489
+ expect(
490
+ query.getLookups(
491
+ normalizeTokenParameters({
492
+ user_id: 'user1'
493
+ })
494
+ )
495
+ ).toEqual([['mybucket', '1', 'user1']]);
496
+ });
497
+
498
+ test('IN token_parameters query', function () {
499
+ const sql = 'SELECT id as region_id FROM regions WHERE name IN token_parameters.region_names';
500
+ const query = SqlParameterQuery.fromSql('mybucket', sql) as SqlParameterQuery;
501
+ query.id = '1';
502
+ expect(query.evaluateParameterRow({ id: 'region1', name: 'colorado' })).toEqual([
503
+ {
504
+ lookup: ['mybucket', '1', 'colorado'],
505
+ bucket_parameters: [
506
+ {
507
+ region_id: 'region1'
508
+ }
509
+ ]
510
+ }
511
+ ]);
512
+ expect(
513
+ query.getLookups(
514
+ normalizeTokenParameters({
515
+ region_names: JSON.stringify(['colorado', 'texas'])
516
+ })
517
+ )
518
+ ).toEqual([
519
+ ['mybucket', '1', 'colorado'],
520
+ ['mybucket', '1', 'texas']
521
+ ]);
522
+ });
523
+
524
+ test('some math', () => {
525
+ const rules = SqlSyncRules.fromYaml(`
526
+ bucket_definitions:
527
+ mybucket:
528
+ data:
529
+ - SELECT id, (5 / 2) AS int, (5 / 2.0) AS float, (CAST(5 AS real) / 2) AS float2 FROM assets
530
+ `);
531
+
532
+ expect(rules.evaluateRow({ sourceTable: ASSETS, record: { id: 'asset1' } })).toEqual([
533
+ {
534
+ ruleId: '1',
535
+ bucket: 'mybucket[]',
536
+ id: 'asset1',
537
+ data: {
538
+ id: 'asset1',
539
+ int: 2n,
540
+ float: 2.5,
541
+ float2: 2.5
542
+ },
543
+ table: 'assets'
544
+ }
545
+ ]);
546
+ });
547
+
548
+ test('bucket with static numeric parameters', () => {
549
+ const rules = SqlSyncRules.fromYaml(`
550
+ bucket_definitions:
551
+ mybucket:
552
+ parameters: SELECT token_parameters.int1, token_parameters.float1, token_parameters.float2
553
+ data:
554
+ - SELECT id FROM assets WHERE assets.int1 = bucket.int1 AND assets.float1 = bucket.float1 AND assets.float2 = bucket.float2
555
+ `);
556
+ expect(rules.getStaticBucketIds(normalizeTokenParameters({ int1: 314, float1: 3.14, float2: 314 }))).toEqual([
557
+ 'mybucket[314,3.14,314]'
558
+ ]);
559
+
560
+ expect(
561
+ rules.evaluateRow({ sourceTable: ASSETS, record: { id: 'asset1', int1: 314n, float1: 3.14, float2: 314 } })
562
+ ).toEqual([
563
+ {
564
+ ruleId: '1',
565
+ bucket: 'mybucket[314,3.14,314]',
566
+ id: 'asset1',
567
+ data: {
568
+ id: 'asset1'
569
+ },
570
+ table: 'assets'
571
+ }
572
+ ]);
573
+ });
574
+
575
+ test('bucket with queried numeric parameters', () => {
576
+ const sql =
577
+ 'SELECT users.int1, users.float1, users.float2 FROM users WHERE users.int1 = token_parameters.int1 AND users.float1 = token_parameters.float1 AND users.float2 = token_parameters.float2';
578
+ const query = SqlParameterQuery.fromSql('mybucket', sql) as SqlParameterQuery;
579
+ query.id = '1';
580
+ // Note: We don't need to worry about numeric vs decimal types in the lookup - JSONB handles normalization for us.
581
+ expect(query.evaluateParameterRow({ int1: 314n, float1: 3.14, float2: 314 })).toEqual([
582
+ {
583
+ lookup: ['mybucket', '1', 314n, 3.14, 314],
584
+
585
+ bucket_parameters: [{ int1: 314n, float1: 3.14, float2: 314 }]
586
+ }
587
+ ]);
588
+
589
+ // Similarly, we don't need to worry about the types here.
590
+ // This test just checks the current behavior.
591
+ expect(query.getLookups(normalizeTokenParameters({ int1: 314n, float1: 3.14, float2: 314 }))).toEqual([
592
+ ['mybucket', '1', 314n, 3.14, 314n]
593
+ ]);
594
+
595
+ // We _do_ need to care about the bucket string representation.
596
+ expect(query.resolveBucketIds([{ int1: 314, float1: 3.14, float2: 314 }], normalizeTokenParameters({}))).toEqual([
597
+ 'mybucket[314,3.14,314]'
598
+ ]);
599
+
600
+ expect(query.resolveBucketIds([{ int1: 314n, float1: 3.14, float2: 314 }], normalizeTokenParameters({}))).toEqual([
601
+ 'mybucket[314,3.14,314]'
602
+ ]);
603
+ });
604
+
605
+ test('parameter query with token filter (1)', () => {
606
+ // Also supported: token_parameters.is_admin = true
607
+ // Not supported: token_parameters.is_admin != false
608
+ // Support could be added later.
609
+ const sql = 'SELECT FROM users WHERE users.id = token_parameters.user_id AND token_parameters.is_admin';
610
+ const query = SqlParameterQuery.fromSql('mybucket', sql) as SqlParameterQuery;
611
+ query.id = '1';
612
+
613
+ expect(query.evaluateParameterRow({ id: 'user1' })).toEqual([
614
+ {
615
+ lookup: ['mybucket', '1', 'user1', 1n],
616
+ bucket_parameters: [{}]
617
+ }
618
+ ]);
619
+
620
+ expect(query.getLookups(normalizeTokenParameters({ user_id: 'user1', is_admin: true }))).toEqual([
621
+ ['mybucket', '1', 'user1', 1n]
622
+ ]);
623
+ // Would not match any actual lookups
624
+ expect(query.getLookups(normalizeTokenParameters({ user_id: 'user1', is_admin: false }))).toEqual([
625
+ ['mybucket', '1', 'user1', 0n]
626
+ ]);
627
+ });
628
+
629
+ test('parameter query with token filter (2)', () => {
630
+ const sql =
631
+ 'SELECT users.id AS user_id, token_parameters.is_admin as is_admin FROM users WHERE users.id = token_parameters.user_id AND token_parameters.is_admin';
632
+ const query = SqlParameterQuery.fromSql('mybucket', sql) as SqlParameterQuery;
633
+ query.id = '1';
634
+
635
+ expect(query.evaluateParameterRow({ id: 'user1' })).toEqual([
636
+ {
637
+ lookup: ['mybucket', '1', 'user1', 1n],
638
+
639
+ bucket_parameters: [{ user_id: 'user1' }]
640
+ }
641
+ ]);
642
+
643
+ expect(query.getLookups(normalizeTokenParameters({ user_id: 'user1', is_admin: true }))).toEqual([
644
+ ['mybucket', '1', 'user1', 1n]
645
+ ]);
646
+
647
+ expect(
648
+ query.resolveBucketIds([{ user_id: 'user1' }], normalizeTokenParameters({ user_id: 'user1', is_admin: true }))
649
+ ).toEqual(['mybucket["user1",1]']);
650
+ });
651
+
652
+ test('custom table and id', () => {
653
+ const rules = SqlSyncRules.fromYaml(`
654
+ bucket_definitions:
655
+ mybucket:
656
+ data:
657
+ - SELECT client_id AS id, description FROM assets_123 as assets WHERE assets.archived = false
658
+ - SELECT other_id AS id, description FROM assets_123 as assets
659
+ `);
660
+
661
+ expect(
662
+ rules.evaluateRow({
663
+ sourceTable: new TestSourceTable('assets_123'),
664
+ record: { client_id: 'asset1', description: 'test', archived: 0n, other_id: 'other1' }
665
+ })
666
+ ).toEqual([
667
+ {
668
+ ruleId: '1',
669
+ bucket: 'mybucket[]',
670
+ id: 'asset1',
671
+ data: {
672
+ id: 'asset1',
673
+ description: 'test'
674
+ },
675
+ table: 'assets'
676
+ },
677
+ {
678
+ ruleId: '2',
679
+ bucket: 'mybucket[]',
680
+ id: 'other1',
681
+ data: {
682
+ id: 'other1',
683
+ description: 'test'
684
+ },
685
+ table: 'assets'
686
+ }
687
+ ]);
688
+ });
689
+
690
+ test('wildcard table', () => {
691
+ const rules = SqlSyncRules.fromYaml(`
692
+ bucket_definitions:
693
+ mybucket:
694
+ data:
695
+ - SELECT client_id AS id, description, _table_suffix as suffix, * FROM "assets_%" as assets WHERE assets.archived = false AND _table_suffix > '100'
696
+ `);
697
+
698
+ expect(
699
+ rules.evaluateRow({
700
+ sourceTable: new TestSourceTable('assets_123'),
701
+ record: { client_id: 'asset1', description: 'test', archived: 0n, other_id: 'other1' }
702
+ })
703
+ ).toEqual([
704
+ {
705
+ ruleId: '1',
706
+ bucket: 'mybucket[]',
707
+ id: 'asset1',
708
+ data: {
709
+ id: 'asset1',
710
+ description: 'test',
711
+ suffix: '123',
712
+ archived: 0n,
713
+ client_id: 'asset1',
714
+ other_id: 'other1'
715
+ },
716
+ table: 'assets'
717
+ }
718
+ ]);
719
+ });
720
+
721
+ test('wildcard without alias', () => {
722
+ const rules = SqlSyncRules.fromYaml(`
723
+ bucket_definitions:
724
+ mybucket:
725
+ data:
726
+ - SELECT *, _table_suffix as suffix, * FROM "%" WHERE archived = false
727
+ `);
728
+
729
+ expect(
730
+ rules.evaluateRow({
731
+ sourceTable: ASSETS,
732
+ record: { id: 'asset1', description: 'test', archived: 0n }
733
+ })
734
+ ).toEqual([
735
+ {
736
+ ruleId: '1',
737
+ bucket: 'mybucket[]',
738
+ id: 'asset1',
739
+ data: {
740
+ id: 'asset1',
741
+ description: 'test',
742
+ suffix: 'assets',
743
+ archived: 0n
744
+ },
745
+ table: 'assets'
746
+ }
747
+ ]);
748
+ });
749
+
750
+ test('should filter schemas', () => {
751
+ const rules = SqlSyncRules.fromYaml(`
752
+ bucket_definitions:
753
+ mybucket:
754
+ data:
755
+ - SELECT id FROM "assets" # Yes
756
+ - SELECT id FROM "public"."assets" # yes
757
+ - SELECT id FROM "default.public"."assets" # yes
758
+ - SELECT id FROM "other"."assets" # no
759
+ - SELECT id FROM "other.public"."assets" # no
760
+ `);
761
+
762
+ expect(
763
+ rules.evaluateRow({
764
+ sourceTable: ASSETS,
765
+ record: { id: 'asset1' }
766
+ })
767
+ ).toEqual([
768
+ {
769
+ ruleId: '1',
770
+ bucket: 'mybucket[]',
771
+ id: 'asset1',
772
+ data: {
773
+ id: 'asset1'
774
+ },
775
+ table: 'assets'
776
+ },
777
+ {
778
+ ruleId: '2',
779
+ bucket: 'mybucket[]',
780
+ id: 'asset1',
781
+ data: {
782
+ id: 'asset1'
783
+ },
784
+ table: 'assets'
785
+ },
786
+ {
787
+ ruleId: '3',
788
+ bucket: 'mybucket[]',
789
+ id: 'asset1',
790
+ data: {
791
+ id: 'asset1'
792
+ },
793
+ table: 'assets'
794
+ }
795
+ ]);
796
+ });
797
+
798
+ test('case-sensitive parameter queries (1)', () => {
799
+ const sql = 'SELECT users."userId" AS user_id FROM users WHERE users."userId" = token_parameters.user_id';
800
+ const query = SqlParameterQuery.fromSql('mybucket', sql) as SqlParameterQuery;
801
+ query.id = '1';
802
+
803
+ expect(query.evaluateParameterRow({ userId: 'user1' })).toEqual([
804
+ {
805
+ lookup: ['mybucket', '1', 'user1'],
806
+
807
+ bucket_parameters: [{ user_id: 'user1' }]
808
+ }
809
+ ]);
810
+ });
811
+
812
+ test('case-sensitive parameter queries (2)', () => {
813
+ // Note: This documents current behavior.
814
+ // This may change in the future - we should check against expected behavior for
815
+ // Postgres and/or SQLite.
816
+ const sql = 'SELECT users.userId AS user_id FROM users WHERE users.userId = token_parameters.user_id';
817
+ const query = SqlParameterQuery.fromSql('mybucket', sql) as SqlParameterQuery;
818
+ query.id = '1';
819
+
820
+ expect(query.evaluateParameterRow({ userId: 'user1' })).toEqual([]);
821
+ expect(query.evaluateParameterRow({ userid: 'user1' })).toEqual([
822
+ {
823
+ lookup: ['mybucket', '1', 'user1'],
824
+
825
+ bucket_parameters: [{ user_id: 'user1' }]
826
+ }
827
+ ]);
828
+ });
829
+
830
+ test('dynamic global parameter query', () => {
831
+ const sql = "SELECT workspaces.id AS workspace_id FROM workspaces WHERE visibility = 'public'";
832
+ const query = SqlParameterQuery.fromSql('mybucket', sql) as SqlParameterQuery;
833
+ query.id = '1';
834
+
835
+ expect(query.evaluateParameterRow({ id: 'workspace1', visibility: 'public' })).toEqual([
836
+ {
837
+ lookup: ['mybucket', '1'],
838
+
839
+ bucket_parameters: [{ workspace_id: 'workspace1' }]
840
+ }
841
+ ]);
842
+
843
+ expect(query.evaluateParameterRow({ id: 'workspace1', visibility: 'private' })).toEqual([]);
844
+ });
845
+
846
+ test('invalid OR in parameter queries', () => {
847
+ // Supporting this case is more tricky. We can do this by effectively denormalizing the OR clause
848
+ // into separate queries, but it's a significant change. For now, developers should do that manually.
849
+ const sql =
850
+ "SELECT workspaces.id AS workspace_id FROM workspaces WHERE workspaces.user_id = token_parameters.user_id OR visibility = 'public'";
851
+ const query = SqlParameterQuery.fromSql('mybucket', sql) as SqlParameterQuery;
852
+ expect(query.errors[0].message).toMatch(/must use the same parameters/);
853
+ });
854
+
855
+ test('types', () => {
856
+ const schema = new StaticSchema([
857
+ {
858
+ tag: SourceTable.DEFAULT_TAG,
859
+ schemas: [
860
+ {
861
+ name: SourceTable.DEFAULT_SCHEMA,
862
+ tables: [
863
+ {
864
+ name: 'assets',
865
+ columns: [
866
+ { name: 'id', pg_type: 'uuid' },
867
+ { name: 'name', pg_type: 'text' },
868
+ { name: 'count', pg_type: 'int4' },
869
+ { name: 'owner_id', pg_type: 'uuid' }
870
+ ]
871
+ }
872
+ ]
873
+ }
874
+ ]
875
+ }
876
+ ]);
877
+
878
+ const q1 = SqlDataQuery.fromSql('q1', ['user_id'], `SELECT * FROM assets WHERE owner_id = bucket.user_id`);
879
+ expect(q1.getColumnOutputs(schema)).toEqual([
880
+ {
881
+ name: 'assets',
882
+ columns: [
883
+ { name: 'id', type: ExpressionType.TEXT },
884
+ { name: 'name', type: ExpressionType.TEXT },
885
+ { name: 'count', type: ExpressionType.INTEGER },
886
+ { name: 'owner_id', type: ExpressionType.TEXT }
887
+ ]
888
+ }
889
+ ]);
890
+
891
+ const q2 = SqlDataQuery.fromSql(
892
+ 'q1',
893
+ ['user_id'],
894
+ `
895
+ SELECT id :: integer as id,
896
+ upper(name) as name_upper,
897
+ hex('test') as hex,
898
+ count + 2 as count2,
899
+ count * 3.0 as count3,
900
+ count * '4' as count4,
901
+ name ->> '$.attr' as json_value,
902
+ ifnull(name, 2.0) as maybe_name
903
+ FROM assets WHERE owner_id = bucket.user_id`
904
+ );
905
+ expect(q2.getColumnOutputs(schema)).toEqual([
906
+ {
907
+ name: 'assets',
908
+ columns: [
909
+ { name: 'id', type: ExpressionType.INTEGER },
910
+ { name: 'name_upper', type: ExpressionType.TEXT },
911
+ { name: 'hex', type: ExpressionType.TEXT },
912
+ { name: 'count2', type: ExpressionType.INTEGER },
913
+ { name: 'count3', type: ExpressionType.REAL },
914
+ { name: 'count4', type: ExpressionType.NUMERIC },
915
+ { name: 'json_value', type: ExpressionType.ANY_JSON },
916
+ { name: 'maybe_name', type: ExpressionType.TEXT.or(ExpressionType.REAL) }
917
+ ]
918
+ }
919
+ ]);
920
+ });
921
+
922
+ test('validate columns', () => {
923
+ const schema = new StaticSchema([
924
+ {
925
+ tag: SourceTable.DEFAULT_TAG,
926
+ schemas: [
927
+ {
928
+ name: SourceTable.DEFAULT_SCHEMA,
929
+ tables: [
930
+ {
931
+ name: 'assets',
932
+ columns: [
933
+ { name: 'id', pg_type: 'uuid' },
934
+ { name: 'name', pg_type: 'text' },
935
+ { name: 'count', pg_type: 'int4' },
936
+ { name: 'owner_id', pg_type: 'uuid' }
937
+ ]
938
+ }
939
+ ]
940
+ }
941
+ ]
942
+ }
943
+ ]);
944
+ const q1 = SqlDataQuery.fromSql(
945
+ 'q1',
946
+ ['user_id'],
947
+ 'SELECT id, name, count FROM assets WHERE owner_id = bucket.user_id',
948
+ schema
949
+ );
950
+ expect(q1.errors).toEqual([]);
951
+
952
+ const q2 = SqlDataQuery.fromSql(
953
+ 'q2',
954
+ ['user_id'],
955
+ 'SELECT id, upper(description) as d FROM assets WHERE other_id = bucket.user_id',
956
+ schema
957
+ );
958
+ expect(q2.errors).toMatchObject([
959
+ {
960
+ message: `Column not found: other_id`,
961
+ type: 'warning'
962
+ },
963
+ {
964
+ message: `Column not found: description`,
965
+ type: 'warning'
966
+ }
967
+ ]);
968
+
969
+ const q3 = SqlDataQuery.fromSql(
970
+ 'q3',
971
+ ['user_id'],
972
+ 'SELECT id, description, * FROM nope WHERE other_id = bucket.user_id',
973
+ schema
974
+ );
975
+ expect(q3.errors).toMatchObject([
976
+ {
977
+ message: `Table public.nope not found`,
978
+ type: 'warning'
979
+ }
980
+ ]);
981
+ });
982
+
983
+ test('schema generation', () => {
984
+ const schema = new StaticSchema([
985
+ {
986
+ tag: SourceTable.DEFAULT_TAG,
987
+ schemas: [
988
+ {
989
+ name: SourceTable.DEFAULT_SCHEMA,
990
+ tables: [
991
+ {
992
+ name: 'assets',
993
+ columns: [
994
+ { name: 'id', pg_type: 'uuid' },
995
+ { name: 'name', pg_type: 'text' },
996
+ { name: 'count', pg_type: 'int4' },
997
+ { name: 'owner_id', pg_type: 'uuid' }
998
+ ]
999
+ }
1000
+ ]
1001
+ }
1002
+ ]
1003
+ }
1004
+ ]);
1005
+
1006
+ const rules = SqlSyncRules.fromYaml(`
1007
+ bucket_definitions:
1008
+ mybucket:
1009
+ data:
1010
+ - SELECT * FROM assets as assets1
1011
+ - SELECT id, name, count FROM assets as assets2
1012
+ - SELECT id, owner_id as other_id, foo FROM assets as ASSETS2
1013
+ `);
1014
+
1015
+ expect(new DartSchemaGenerator().generate(rules, schema)).toEqual(`Schema([
1016
+ Table('assets1', [
1017
+ Column.text('name'),
1018
+ Column.integer('count'),
1019
+ Column.text('owner_id')
1020
+ ]),
1021
+ Table('assets2', [
1022
+ Column.text('name'),
1023
+ Column.integer('count'),
1024
+ Column.text('other_id'),
1025
+ Column.text('foo')
1026
+ ])
1027
+ ]);
1028
+ `);
1029
+
1030
+ expect(new JsSchemaGenerator().generate(rules, schema)).toEqual(`new Schema([
1031
+ new Table({
1032
+ name: 'assets1',
1033
+ columns: [
1034
+ new Column({ name: 'name', type: ColumnType.TEXT }),
1035
+ new Column({ name: 'count', type: ColumnType.INTEGER }),
1036
+ new Column({ name: 'owner_id', type: ColumnType.TEXT })
1037
+ ]
1038
+ }),
1039
+ new Table({
1040
+ name: 'assets2',
1041
+ columns: [
1042
+ new Column({ name: 'name', type: ColumnType.TEXT }),
1043
+ new Column({ name: 'count', type: ColumnType.INTEGER }),
1044
+ new Column({ name: 'other_id', type: ColumnType.TEXT }),
1045
+ new Column({ name: 'foo', type: ColumnType.TEXT })
1046
+ ]
1047
+ })
1048
+ ])
1049
+ `);
1050
+ });
1051
+ });