@aurabase/aurabase-js 0.3.0

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 (344) hide show
  1. package/.changeset/config.json +11 -0
  2. package/CHANGELOG.md +30 -0
  3. package/LICENSE +21 -0
  4. package/README.md +833 -0
  5. package/package.json +90 -0
  6. package/packages/ai/LICENSE +21 -0
  7. package/packages/ai/README.md +106 -0
  8. package/packages/ai/dist/index.cjs +229 -0
  9. package/packages/ai/dist/index.d.cts +139 -0
  10. package/packages/ai/dist/index.d.ts +139 -0
  11. package/packages/ai/dist/index.js +226 -0
  12. package/packages/ai/node_modules/.bin/esbuild +19 -0
  13. package/packages/ai/node_modules/.bin/tsc +22 -0
  14. package/packages/ai/node_modules/.bin/tsserver +22 -0
  15. package/packages/ai/node_modules/.bin/tsup +22 -0
  16. package/packages/ai/node_modules/.bin/tsup-node +22 -0
  17. package/packages/ai/node_modules/.bin/vite +22 -0
  18. package/packages/ai/node_modules/.bin/vitest +22 -0
  19. package/packages/ai/node_modules/.vite/vitest/da39a3ee5e6b4b0d3255bfef95601890afd80709/results.json +1 -0
  20. package/packages/ai/package.json +61 -0
  21. package/packages/ai/src/AiService.ts +226 -0
  22. package/packages/ai/src/index.ts +18 -0
  23. package/packages/ai/src/sse.ts +141 -0
  24. package/packages/ai/src/types.ts +30 -0
  25. package/packages/ai/tests/chat-stream.test.ts +151 -0
  26. package/packages/ai/tests/rag-namespaces.test.ts +54 -0
  27. package/packages/ai/tests/sse.test.ts +69 -0
  28. package/packages/ai/tsconfig.json +5 -0
  29. package/packages/ai/tsup.config.ts +11 -0
  30. package/packages/aurabase/LICENSE +21 -0
  31. package/packages/aurabase/README.md +64 -0
  32. package/packages/aurabase/dist/AuraClient-DAtDbfPB.d.cts +77 -0
  33. package/packages/aurabase/dist/AuraClient-DAtDbfPB.d.ts +77 -0
  34. package/packages/aurabase/dist/index.cjs +237 -0
  35. package/packages/aurabase/dist/index.d.cts +46 -0
  36. package/packages/aurabase/dist/index.d.ts +46 -0
  37. package/packages/aurabase/dist/index.js +177 -0
  38. package/packages/aurabase/dist/ssr.cjs +224 -0
  39. package/packages/aurabase/dist/ssr.d.cts +65 -0
  40. package/packages/aurabase/dist/ssr.d.ts +65 -0
  41. package/packages/aurabase/dist/ssr.js +214 -0
  42. package/packages/aurabase/node_modules/.bin/esbuild +19 -0
  43. package/packages/aurabase/node_modules/.bin/tsc +22 -0
  44. package/packages/aurabase/node_modules/.bin/tsserver +22 -0
  45. package/packages/aurabase/node_modules/.bin/tsup +22 -0
  46. package/packages/aurabase/node_modules/.bin/tsup-node +22 -0
  47. package/packages/aurabase/node_modules/.bin/vite +22 -0
  48. package/packages/aurabase/node_modules/.bin/vitest +22 -0
  49. package/packages/aurabase/node_modules/.vite/vitest/da39a3ee5e6b4b0d3255bfef95601890afd80709/results.json +1 -0
  50. package/packages/aurabase/package.json +80 -0
  51. package/packages/aurabase/src/AuraClient.ts +237 -0
  52. package/packages/aurabase/src/index.ts +152 -0
  53. package/packages/aurabase/src/ssr.ts +156 -0
  54. package/packages/aurabase/tests/ai-streaming-rag-e2e.test.ts +82 -0
  55. package/packages/aurabase/tests/ai.test.ts +111 -0
  56. package/packages/aurabase/tests/auth-admin.test.ts +515 -0
  57. package/packages/aurabase/tests/auth-e2e.test.ts +135 -0
  58. package/packages/aurabase/tests/auth-hardening.test.ts +206 -0
  59. package/packages/aurabase/tests/auth.test.ts +947 -0
  60. package/packages/aurabase/tests/database.test.ts +230 -0
  61. package/packages/aurabase/tests/db-types-e2e.test.ts +217 -0
  62. package/packages/aurabase/tests/dispose.test.ts +23 -0
  63. package/packages/aurabase/tests/foreign-token.e2e.test.ts +129 -0
  64. package/packages/aurabase/tests/functions.test.ts +107 -0
  65. package/packages/aurabase/tests/integration.test.ts +413 -0
  66. package/packages/aurabase/tests/live-production-validation.js +241 -0
  67. package/packages/aurabase/tests/live-sdk-production-validation.js +293 -0
  68. package/packages/aurabase/tests/mocks.ts +139 -0
  69. package/packages/aurabase/tests/notifications.test.ts +314 -0
  70. package/packages/aurabase/tests/observability-backup.test.ts +186 -0
  71. package/packages/aurabase/tests/observability-hook.test.ts +130 -0
  72. package/packages/aurabase/tests/production-robustness.test.ts +268 -0
  73. package/packages/aurabase/tests/public-api-surface.test.ts +53 -0
  74. package/packages/aurabase/tests/query-builder.test.ts +329 -0
  75. package/packages/aurabase/tests/readme-smoke-e2e.test.ts +231 -0
  76. package/packages/aurabase/tests/realtime-filter-e2e.test.ts +344 -0
  77. package/packages/aurabase/tests/refresh-locks.test.ts +210 -0
  78. package/packages/aurabase/tests/retry-option.test.ts +17 -0
  79. package/packages/aurabase/tests/rpc-e2e.test.ts +254 -0
  80. package/packages/aurabase/tests/rs256-local-e2e.test.ts +114 -0
  81. package/packages/aurabase/tests/rt-alias-public-e2e.test.ts +235 -0
  82. package/packages/aurabase/tests/rt-http-broadcast-e2e.test.ts +124 -0
  83. package/packages/aurabase/tests/service-role-guard.test.ts +99 -0
  84. package/packages/aurabase/tests/ssr.test.ts +91 -0
  85. package/packages/aurabase/tests/storage-backend-e2e.test.ts +481 -0
  86. package/packages/aurabase/tests/storage-sdk-parity-e2e.test.ts +104 -0
  87. package/packages/aurabase/tests/storage-transform-e2e.test.ts +684 -0
  88. package/packages/aurabase/tests/storage.test.ts +55 -0
  89. package/packages/aurabase/tests/types/client-generic.test-d.ts +79 -0
  90. package/packages/aurabase/tests/types/docs-snippets.test-d.ts +571 -0
  91. package/packages/aurabase/tests/types/error-code.test-d.ts +53 -0
  92. package/packages/aurabase/tsconfig.json +5 -0
  93. package/packages/aurabase/tsconfig.test-d.json +5 -0
  94. package/packages/aurabase/tsup.config.ts +11 -0
  95. package/packages/aurabase/vitest.config.ts +16 -0
  96. package/packages/aurabase/vitest.e2e.config.ts +9 -0
  97. package/packages/auth/LICENSE +21 -0
  98. package/packages/auth/README.md +215 -0
  99. package/packages/auth/dist/index.cjs +1175 -0
  100. package/packages/auth/dist/index.d.cts +397 -0
  101. package/packages/auth/dist/index.d.ts +397 -0
  102. package/packages/auth/dist/index.js +1170 -0
  103. package/packages/auth/node_modules/.bin/esbuild +19 -0
  104. package/packages/auth/node_modules/.bin/tsc +22 -0
  105. package/packages/auth/node_modules/.bin/tsserver +22 -0
  106. package/packages/auth/node_modules/.bin/tsup +22 -0
  107. package/packages/auth/node_modules/.bin/tsup-node +22 -0
  108. package/packages/auth/node_modules/.bin/vite +22 -0
  109. package/packages/auth/node_modules/.bin/vitest +22 -0
  110. package/packages/auth/node_modules/.vite/vitest/da39a3ee5e6b4b0d3255bfef95601890afd80709/results.json +1 -0
  111. package/packages/auth/package.json +61 -0
  112. package/packages/auth/src/AuthAdminService.ts +149 -0
  113. package/packages/auth/src/AuthService.ts +1166 -0
  114. package/packages/auth/src/SessionManager.ts +92 -0
  115. package/packages/auth/src/index.ts +17 -0
  116. package/packages/auth/src/jwks.ts +337 -0
  117. package/packages/auth/src/pkce.ts +27 -0
  118. package/packages/auth/src/types.ts +89 -0
  119. package/packages/auth/tests/auth-lifecycle.test.ts +149 -0
  120. package/packages/auth/tests/detect-session.test.ts +361 -0
  121. package/packages/auth/tests/getclaims.test.ts +780 -0
  122. package/packages/auth/tests/is-mfa-challenge.test.ts +27 -0
  123. package/packages/auth/tests/password-recovery.test.ts +124 -0
  124. package/packages/auth/tests/pkce.test.ts +192 -0
  125. package/packages/auth/tests/session-manager.test.ts +103 -0
  126. package/packages/auth/tests/set-session.test.ts +139 -0
  127. package/packages/auth/tests/verify-otp.test.ts +153 -0
  128. package/packages/auth/tsconfig.json +5 -0
  129. package/packages/auth/tsup.config.ts +11 -0
  130. package/packages/core/LICENSE +21 -0
  131. package/packages/core/README.md +132 -0
  132. package/packages/core/dist/index.cjs +695 -0
  133. package/packages/core/dist/index.d.cts +1137 -0
  134. package/packages/core/dist/index.d.ts +1137 -0
  135. package/packages/core/dist/index.js +673 -0
  136. package/packages/core/node_modules/.bin/esbuild +22 -0
  137. package/packages/core/node_modules/.bin/tsc +22 -0
  138. package/packages/core/node_modules/.bin/tsserver +22 -0
  139. package/packages/core/node_modules/.bin/tsup +22 -0
  140. package/packages/core/node_modules/.bin/tsup-node +22 -0
  141. package/packages/core/node_modules/.bin/vite +22 -0
  142. package/packages/core/node_modules/.bin/vitest +22 -0
  143. package/packages/core/node_modules/.vite/vitest/da39a3ee5e6b4b0d3255bfef95601890afd80709/results.json +1 -0
  144. package/packages/core/package.json +57 -0
  145. package/packages/core/src/AuraError.ts +89 -0
  146. package/packages/core/src/BaseService.ts +44 -0
  147. package/packages/core/src/client.ts +34 -0
  148. package/packages/core/src/cookie-utils.ts +42 -0
  149. package/packages/core/src/fetch.ts +435 -0
  150. package/packages/core/src/index.ts +22 -0
  151. package/packages/core/src/retry.ts +63 -0
  152. package/packages/core/src/serialize.ts +6 -0
  153. package/packages/core/src/stores/BaseAuthStore.ts +164 -0
  154. package/packages/core/src/stores/LocalAuthStore.ts +199 -0
  155. package/packages/core/src/types.ts +1042 -0
  156. package/packages/core/tests/auth-store.test.ts +300 -0
  157. package/packages/core/tests/error-code.test.ts +61 -0
  158. package/packages/core/tests/error.test.ts +24 -0
  159. package/packages/core/tests/fetch.test.ts +286 -0
  160. package/packages/core/tests/mocks.ts +139 -0
  161. package/packages/core/tests/observability.test.ts +86 -0
  162. package/packages/core/tests/refresh-hardening.test.ts +164 -0
  163. package/packages/core/tests/refresh-observability.test.ts +111 -0
  164. package/packages/core/tests/retry.test.ts +106 -0
  165. package/packages/core/tests/serialize.test.ts +12 -0
  166. package/packages/core/tests/timeout.test.ts +79 -0
  167. package/packages/core/tsconfig.json +5 -0
  168. package/packages/core/tsup.config.ts +11 -0
  169. package/packages/db/LICENSE +21 -0
  170. package/packages/db/README.md +152 -0
  171. package/packages/db/VENDORED.md +93 -0
  172. package/packages/db/dist/index.cjs +544 -0
  173. package/packages/db/dist/index.d.cts +1111 -0
  174. package/packages/db/dist/index.d.ts +1111 -0
  175. package/packages/db/dist/index.js +541 -0
  176. package/packages/db/node_modules/.bin/esbuild +19 -0
  177. package/packages/db/node_modules/.bin/tsc +22 -0
  178. package/packages/db/node_modules/.bin/tsserver +22 -0
  179. package/packages/db/node_modules/.bin/tsup +22 -0
  180. package/packages/db/node_modules/.bin/tsup-node +22 -0
  181. package/packages/db/node_modules/.bin/vite +22 -0
  182. package/packages/db/node_modules/.bin/vitest +22 -0
  183. package/packages/db/node_modules/.vite/vitest/da39a3ee5e6b4b0d3255bfef95601890afd80709/results.json +1 -0
  184. package/packages/db/package.json +62 -0
  185. package/packages/db/src/DatabaseService.ts +170 -0
  186. package/packages/db/src/QueryBuilder.ts +830 -0
  187. package/packages/db/src/index.ts +2 -0
  188. package/packages/db/src/pgrst-types/common/common.ts +86 -0
  189. package/packages/db/src/pgrst-types/common/rpc.ts +151 -0
  190. package/packages/db/src/pgrst-types/feature-flags.ts +20 -0
  191. package/packages/db/src/pgrst-types/types.ts +134 -0
  192. package/packages/db/src/select-query-parser/parser.ts +479 -0
  193. package/packages/db/src/select-query-parser/result.ts +553 -0
  194. package/packages/db/src/select-query-parser/types.ts +132 -0
  195. package/packages/db/src/select-query-parser/utils.ts +707 -0
  196. package/packages/db/tests/filters.test.ts +81 -0
  197. package/packages/db/tests/mutations-filtered.e2e.test.ts +132 -0
  198. package/packages/db/tests/mutations-filtered.test.ts +188 -0
  199. package/packages/db/tests/order.test.ts +45 -0
  200. package/packages/db/tests/rpc.test.ts +41 -0
  201. package/packages/db/tests/scale/build-large-schema.ts +94 -0
  202. package/packages/db/tests/scale/scale-compile.test.ts +167 -0
  203. package/packages/db/tests/schema.test.ts +77 -0
  204. package/packages/db/tests/single-accept.e2e.test.ts +123 -0
  205. package/packages/db/tests/single.test.ts +211 -0
  206. package/packages/db/tests/types/filters.test-d.ts +52 -0
  207. package/packages/db/tests/types/fixtures.ts +40 -0
  208. package/packages/db/tests/types/from-views.test-d.ts +18 -0
  209. package/packages/db/tests/types/mutations.test-d.ts +23 -0
  210. package/packages/db/tests/types/rpc.test-d.ts +60 -0
  211. package/packages/db/tests/types/schema-shape.test-d.ts +8 -0
  212. package/packages/db/tests/types/select-inference.test-d.ts +38 -0
  213. package/packages/db/tests/vendored/fixtures.ts +83 -0
  214. package/packages/db/tests/vendored/get-result.test-d.ts +75 -0
  215. package/packages/db/tsconfig.json +5 -0
  216. package/packages/db/tsconfig.test-d.json +5 -0
  217. package/packages/db/tsup.config.ts +11 -0
  218. package/packages/db/vitest.config.ts +8 -0
  219. package/packages/functions/LICENSE +21 -0
  220. package/packages/functions/README.md +96 -0
  221. package/packages/functions/dist/index.cjs +173 -0
  222. package/packages/functions/dist/index.d.cts +81 -0
  223. package/packages/functions/dist/index.d.ts +81 -0
  224. package/packages/functions/dist/index.js +168 -0
  225. package/packages/functions/node_modules/.bin/esbuild +19 -0
  226. package/packages/functions/node_modules/.bin/tsc +22 -0
  227. package/packages/functions/node_modules/.bin/tsserver +22 -0
  228. package/packages/functions/node_modules/.bin/tsup +22 -0
  229. package/packages/functions/node_modules/.bin/tsup-node +22 -0
  230. package/packages/functions/node_modules/.bin/vite +22 -0
  231. package/packages/functions/node_modules/.bin/vitest +22 -0
  232. package/packages/functions/node_modules/.vite/vitest/da39a3ee5e6b4b0d3255bfef95601890afd80709/results.json +1 -0
  233. package/packages/functions/package.json +60 -0
  234. package/packages/functions/src/FunctionsService.ts +188 -0
  235. package/packages/functions/src/errors.ts +70 -0
  236. package/packages/functions/src/index.ts +6 -0
  237. package/packages/functions/tests/invoke.test.ts +97 -0
  238. package/packages/functions/tests/response-type-errors.test.ts +135 -0
  239. package/packages/functions/tsconfig.json +5 -0
  240. package/packages/functions/tsup.config.ts +11 -0
  241. package/packages/gen/LICENSE +21 -0
  242. package/packages/gen/README.md +84 -0
  243. package/packages/gen/dist/cli.cjs +334 -0
  244. package/packages/gen/dist/cli.d.cts +1 -0
  245. package/packages/gen/dist/cli.d.ts +1 -0
  246. package/packages/gen/dist/cli.js +332 -0
  247. package/packages/gen/dist/index.cjs +257 -0
  248. package/packages/gen/dist/index.d.cts +12 -0
  249. package/packages/gen/dist/index.d.ts +12 -0
  250. package/packages/gen/dist/index.js +255 -0
  251. package/packages/gen/node_modules/.bin/esbuild +19 -0
  252. package/packages/gen/node_modules/.bin/tsc +22 -0
  253. package/packages/gen/node_modules/.bin/tsserver +22 -0
  254. package/packages/gen/node_modules/.bin/tsup +22 -0
  255. package/packages/gen/node_modules/.bin/tsup-node +22 -0
  256. package/packages/gen/node_modules/.bin/vite +22 -0
  257. package/packages/gen/node_modules/.bin/vitest +22 -0
  258. package/packages/gen/node_modules/.vite/vitest/da39a3ee5e6b4b0d3255bfef95601890afd80709/results.json +1 -0
  259. package/packages/gen/package.json +66 -0
  260. package/packages/gen/src/cli.ts +67 -0
  261. package/packages/gen/src/generate.ts +0 -0
  262. package/packages/gen/src/index.ts +2 -0
  263. package/packages/gen/tests/compile.test.ts +213 -0
  264. package/packages/gen/tests/e2e.test.ts +130 -0
  265. package/packages/gen/tests/generate.test.ts +396 -0
  266. package/packages/gen/tsconfig.json +5 -0
  267. package/packages/gen/tsup.config.ts +11 -0
  268. package/packages/notifications/LICENSE +21 -0
  269. package/packages/notifications/README.md +134 -0
  270. package/packages/notifications/dist/index.cjs +128 -0
  271. package/packages/notifications/dist/index.d.cts +81 -0
  272. package/packages/notifications/dist/index.d.ts +81 -0
  273. package/packages/notifications/dist/index.js +125 -0
  274. package/packages/notifications/node_modules/.bin/esbuild +19 -0
  275. package/packages/notifications/node_modules/.bin/tsc +22 -0
  276. package/packages/notifications/node_modules/.bin/tsserver +22 -0
  277. package/packages/notifications/node_modules/.bin/tsup +22 -0
  278. package/packages/notifications/node_modules/.bin/tsup-node +22 -0
  279. package/packages/notifications/node_modules/.bin/vite +22 -0
  280. package/packages/notifications/node_modules/.bin/vitest +22 -0
  281. package/packages/notifications/package.json +60 -0
  282. package/packages/notifications/src/NotificationsService.ts +230 -0
  283. package/packages/notifications/src/index.ts +19 -0
  284. package/packages/notifications/tsconfig.json +5 -0
  285. package/packages/notifications/tsup.config.ts +11 -0
  286. package/packages/realtime/LICENSE +21 -0
  287. package/packages/realtime/README.md +183 -0
  288. package/packages/realtime/dist/index.cjs +977 -0
  289. package/packages/realtime/dist/index.d.cts +259 -0
  290. package/packages/realtime/dist/index.d.ts +259 -0
  291. package/packages/realtime/dist/index.js +973 -0
  292. package/packages/realtime/node_modules/.bin/esbuild +19 -0
  293. package/packages/realtime/node_modules/.bin/tsc +22 -0
  294. package/packages/realtime/node_modules/.bin/tsserver +22 -0
  295. package/packages/realtime/node_modules/.bin/tsup +22 -0
  296. package/packages/realtime/node_modules/.bin/tsup-node +22 -0
  297. package/packages/realtime/node_modules/.bin/vite +22 -0
  298. package/packages/realtime/node_modules/.bin/vitest +22 -0
  299. package/packages/realtime/node_modules/.vite/vitest/da39a3ee5e6b4b0d3255bfef95601890afd80709/results.json +1 -0
  300. package/packages/realtime/package.json +60 -0
  301. package/packages/realtime/src/AuraChannel.ts +485 -0
  302. package/packages/realtime/src/HeartbeatManager.ts +73 -0
  303. package/packages/realtime/src/RealtimeService.ts +505 -0
  304. package/packages/realtime/src/columnFilter.ts +301 -0
  305. package/packages/realtime/src/index.ts +4 -0
  306. package/packages/realtime/tests/ack-routing.test.ts +81 -0
  307. package/packages/realtime/tests/column-filter.test.ts +144 -0
  308. package/packages/realtime/tests/dispatch.test.ts +191 -0
  309. package/packages/realtime/tests/heartbeat.test.ts +67 -0
  310. package/packages/realtime/tests/http-broadcast.test.ts +68 -0
  311. package/packages/realtime/tests/realtime-token.test.ts +43 -0
  312. package/packages/realtime/tests/reconnect-race.test.ts +149 -0
  313. package/packages/realtime/tests/ref-id.test.ts +34 -0
  314. package/packages/realtime/tests/set-auth.test.ts +70 -0
  315. package/packages/realtime/tsconfig.json +5 -0
  316. package/packages/realtime/tsup.config.ts +11 -0
  317. package/packages/storage/LICENSE +21 -0
  318. package/packages/storage/README.md +141 -0
  319. package/packages/storage/dist/index.cjs +333 -0
  320. package/packages/storage/dist/index.d.cts +185 -0
  321. package/packages/storage/dist/index.d.ts +185 -0
  322. package/packages/storage/dist/index.js +329 -0
  323. package/packages/storage/node_modules/.bin/esbuild +19 -0
  324. package/packages/storage/node_modules/.bin/tsc +22 -0
  325. package/packages/storage/node_modules/.bin/tsserver +22 -0
  326. package/packages/storage/node_modules/.bin/tsup +22 -0
  327. package/packages/storage/node_modules/.bin/tsup-node +22 -0
  328. package/packages/storage/node_modules/.bin/vite +22 -0
  329. package/packages/storage/node_modules/.bin/vitest +22 -0
  330. package/packages/storage/node_modules/.vite/vitest/da39a3ee5e6b4b0d3255bfef95601890afd80709/results.json +1 -0
  331. package/packages/storage/package.json +60 -0
  332. package/packages/storage/src/DownloadBuilder.ts +60 -0
  333. package/packages/storage/src/StorageService.ts +323 -0
  334. package/packages/storage/src/index.ts +2 -0
  335. package/packages/storage/src/path.ts +35 -0
  336. package/packages/storage/src/transform.ts +15 -0
  337. package/packages/storage/tests/download.test.ts +64 -0
  338. package/packages/storage/tests/parity.test.ts +154 -0
  339. package/packages/storage/tests/path.test.ts +87 -0
  340. package/packages/storage/tests/transform.test.ts +79 -0
  341. package/packages/storage/tsconfig.json +5 -0
  342. package/packages/storage/tsup.config.ts +11 -0
  343. package/pnpm-workspace.yaml +24 -0
  344. package/tsconfig.base.json +15 -0
@@ -0,0 +1,301 @@
1
+ /**
2
+ * Fallback client (second rideau) du filtre colonne `postgres_changes`.
3
+ *
4
+ * Le serveur `aura-realtime` est **autoritaire** : il filtre déjà les events CDC
5
+ * par colonne avant de les envoyer (`services/aura-realtime/src/protocol.rs`).
6
+ * Ce module reproduit fidèlement la même sémantique côté client, en defense-in-depth :
7
+ * il ne peut que sur-filtrer (jamais laisser passer un event que le serveur aurait dû
8
+ * exclure).
9
+ *
10
+ * Sémantique portée depuis le backend Rust (parité stricte) :
11
+ * - Opérateurs : eq, neq, lt, lte, gt, gte, in, like, ilike, is, match, imatch, isdistinct.
12
+ * - Syntaxe : `column=operator.value`, négation `column=not.operator.value`.
13
+ * - AND par virgules : `a=eq.1,b=gt.2` (les virgules internes d'un `in.(a,b,c)` ne
14
+ * séparent PAS les conditions — split respectant la profondeur des parenthèses).
15
+ * - **Fail-CLOSED** : toute condition non parsable (syntaxe invalide, opérateur inconnu,
16
+ * regex invalide, trop de valeurs `in`, pattern `match`/`imatch` trop long) => `false`,
17
+ * jamais laissé passer.
18
+ * - Champ absent du record => résultat non évaluable => exclusion ferme, **jamais
19
+ * inversée** par `not.` (sémantique SQL : `NOT (col = x)` avec `col` NULL/absent
20
+ * => NULL => exclu). Seul un résultat sémantique évaluable est inversé par `not.`.
21
+ */
22
+
23
+ type FilterOp =
24
+ | 'eq'
25
+ | 'neq'
26
+ | 'lt'
27
+ | 'lte'
28
+ | 'gt'
29
+ | 'gte'
30
+ | 'in'
31
+ | 'like'
32
+ | 'ilike'
33
+ | 'is'
34
+ | 'match'
35
+ | 'imatch'
36
+ | 'isdistinct'
37
+
38
+ const VALID_OPS: ReadonlySet<string> = new Set([
39
+ 'eq',
40
+ 'neq',
41
+ 'lt',
42
+ 'lte',
43
+ 'gt',
44
+ 'gte',
45
+ 'in',
46
+ 'like',
47
+ 'ilike',
48
+ 'is',
49
+ 'match',
50
+ 'imatch',
51
+ 'isdistinct',
52
+ ])
53
+
54
+ /** Longueur max d'un pattern `match`/`imatch` — borne anti-ReDoS (parité backend). */
55
+ const MAX_REGEX_PATTERN_LEN = 512
56
+
57
+ /** Nombre max de valeurs dans un `in.(...)` — parité backend/PostgREST. */
58
+ const MAX_IN_VALUES = 100
59
+
60
+ interface ParsedColumnFilter {
61
+ column: string
62
+ op: FilterOp
63
+ value: string
64
+ /** `true` si le filtre était préfixé par `not.` — le résultat sémantique est inversé. */
65
+ negated: boolean
66
+ }
67
+
68
+ /**
69
+ * Parse une condition unique "column=op.value" ou "column=not.op.value".
70
+ * Retourne `null` si non parsable (fail-closed — voir `matchesColumnFilter`).
71
+ */
72
+ function parseColumnFilter(filterStr: string): ParsedColumnFilter | null {
73
+ const eqIdx = filterStr.indexOf('=')
74
+ if (eqIdx === -1) return null
75
+ const column = filterStr.slice(0, eqIdx)
76
+ if (!column) return null
77
+ let rest = filterStr.slice(eqIdx + 1)
78
+
79
+ let negated = false
80
+ if (rest.startsWith('not.')) {
81
+ negated = true
82
+ rest = rest.slice(4)
83
+ }
84
+
85
+ const dotIdx = rest.indexOf('.')
86
+ if (dotIdx === -1) return null
87
+ const opStr = rest.slice(0, dotIdx)
88
+ let value = rest.slice(dotIdx + 1)
89
+
90
+ if (!VALID_OPS.has(opStr)) return null
91
+ const op = opStr as FilterOp
92
+
93
+ if (op === 'in') {
94
+ const stripped = value.replace(/^\(+/, '').replace(/\)+$/, '')
95
+ if (stripped.split(',').length > MAX_IN_VALUES) return null
96
+ value = stripped
97
+ } else if (op === 'is') {
98
+ // `is` n'accepte que null/true/false (parité PostgREST) : toute autre valeur est
99
+ // rejetée DÈS LE PARSE. Sinon `is.<garbage>` serait accepté et `matchesRaw`
100
+ // renverrait `false` (défaut) — un `not.is.<garbage>` inverserait ce `false` en
101
+ // `true` => fail-OPEN (matche tout). On refuse ce filtre en amont (parité Rust).
102
+ if (value !== 'null' && value !== 'true' && value !== 'false') return null
103
+ } else if (op === 'match' || op === 'imatch') {
104
+ if (value.length > MAX_REGEX_PATTERN_LEN) return null
105
+ // Compiler la regex DÈS LE PARSE : un pattern invalide => filtre rejeté (null).
106
+ // Sinon un `not.match.(invalide` verrait son échec de compilation transformé
107
+ // en un `true` inversé => fail-OPEN. On refuse ce filtre en amont (parité Rust).
108
+ try {
109
+ // eslint-disable-next-line no-new
110
+ new RegExp(value, op === 'imatch' ? 'i' : undefined)
111
+ } catch {
112
+ return null
113
+ }
114
+ }
115
+
116
+ return { column, op, value, negated }
117
+ }
118
+
119
+ /**
120
+ * Découpe un ensemble de conditions AND ("a=eq.1,b=in.(x,y)") en conditions
121
+ * individuelles, en respectant les virgules internes d'un `in.(...)` (profondeur
122
+ * de parenthèses). Ne fait PAS de parsing ici — juste le split structurel.
123
+ */
124
+ function splitTopLevel(s: string): string[] {
125
+ const parts: string[] = []
126
+ let depth = 0
127
+ let start = 0
128
+ for (let i = 0; i < s.length; i++) {
129
+ const c = s[i]
130
+ if (c === '(') depth += 1
131
+ else if (c === ')') depth = Math.max(0, depth - 1)
132
+ else if (c === ',' && depth === 0) {
133
+ parts.push(s.slice(start, i))
134
+ start = i + 1
135
+ }
136
+ }
137
+ parts.push(s.slice(start))
138
+ return parts
139
+ }
140
+
141
+ /**
142
+ * Découpe puis parse un ensemble de conditions AND. **Fail-closed** : retourne
143
+ * `null` dès qu'UNE condition est invalide (propage l'échec, n'ignore jamais
144
+ * silencieusement une condition malformée).
145
+ */
146
+ function parseFilterSet(s: string): ParsedColumnFilter[] | null {
147
+ const parts = splitTopLevel(s)
148
+ const out: ParsedColumnFilter[] = []
149
+ for (const part of parts) {
150
+ const cf = parseColumnFilter(part.trim())
151
+ if (!cf) return null
152
+ out.push(cf)
153
+ }
154
+ return out
155
+ }
156
+
157
+ /** Convertit une valeur JSON en sa représentation string (comparaison textuelle/numérique). */
158
+ function jsonValueToString(field: unknown): string {
159
+ if (typeof field === 'string') return field
160
+ if (typeof field === 'number' || typeof field === 'boolean') return String(field)
161
+ if (field === null) return 'null'
162
+ try {
163
+ return JSON.stringify(field) ?? ''
164
+ } catch {
165
+ return ''
166
+ }
167
+ }
168
+
169
+ /** Parse une valeur en `number` strict (rejette les chaînes vides/non-numériques). */
170
+ function parseStrictNumber(s: string): number | null {
171
+ if (s.trim() === '') return null
172
+ const n = Number(s)
173
+ return Number.isFinite(n) ? n : null
174
+ }
175
+
176
+ function compareNumeric(a: string, b: string, cmp: (a: number, b: number) => boolean): boolean {
177
+ const an = parseStrictNumber(a)
178
+ const bn = parseStrictNumber(b)
179
+ if (an === null || bn === null) return false // types incompatibles => fail-closed
180
+ return cmp(an, bn)
181
+ }
182
+
183
+ /** Convertit un motif SQL like/ilike (`%`→sous-chaîne, `_`→1 caractère) en RegExp ancrée. */
184
+ function likeToRegExp(pattern: string, caseInsensitive: boolean): RegExp | null {
185
+ let re = '^'
186
+ for (const ch of pattern) {
187
+ if (ch === '%') re += '.*'
188
+ else if (ch === '_') re += '.'
189
+ else re += ch.replace(/[.*+?^${}()|[\]\\]/g, '\\$&')
190
+ }
191
+ re += '$'
192
+ try {
193
+ return new RegExp(re, caseInsensitive ? 'i' : undefined)
194
+ } catch {
195
+ return null
196
+ }
197
+ }
198
+
199
+ /**
200
+ * Évalue le prédicat brut (sans tenir compte de `negated`).
201
+ * Retourne `null` si non évaluable (champ absent, regex invalide) — distinct
202
+ * d'un `false` qui est un « faux » sémantique (inversable par `not.`).
203
+ */
204
+ function matchesRaw(cf: ParsedColumnFilter, record: Record<string, unknown>): boolean | null {
205
+ // `is` compare la valeur JSON réelle (null/true/false) — toujours évaluable.
206
+ if (cf.op === 'is') {
207
+ const field = record[cf.column]
208
+ switch (cf.value) {
209
+ case 'null':
210
+ return field === undefined || field === null
211
+ case 'true':
212
+ return field === true
213
+ case 'false':
214
+ return field === false
215
+ default:
216
+ // Invariant garanti par `parseColumnFilter` : `is.<autre>` est rejeté au parse
217
+ // (null), ce bras est donc défensif uniquement (jamais atteint en pratique).
218
+ return false
219
+ }
220
+ }
221
+
222
+ // `isdistinct` : NULL est distinct de toute valeur non-NULL — toujours évaluable.
223
+ if (cf.op === 'isdistinct') {
224
+ const field = record[cf.column]
225
+ const fieldIsNull = field === undefined || field === null
226
+ if (fieldIsNull) return true
227
+ return jsonValueToString(field) !== cf.value
228
+ }
229
+
230
+ // Champ absent => non évaluable (null), JAMAIS un false inversable.
231
+ if (!(cf.column in record) || record[cf.column] === undefined) return null
232
+ const field = record[cf.column]
233
+ const fieldStr = jsonValueToString(field)
234
+
235
+ switch (cf.op) {
236
+ case 'eq':
237
+ return fieldStr === cf.value
238
+ case 'neq':
239
+ return fieldStr !== cf.value
240
+ case 'lt':
241
+ return compareNumeric(fieldStr, cf.value, (a, b) => a < b)
242
+ case 'lte':
243
+ return compareNumeric(fieldStr, cf.value, (a, b) => a <= b)
244
+ case 'gt':
245
+ return compareNumeric(fieldStr, cf.value, (a, b) => a > b)
246
+ case 'gte':
247
+ return compareNumeric(fieldStr, cf.value, (a, b) => a >= b)
248
+ case 'in':
249
+ return cf.value
250
+ .split(',')
251
+ .map((v) => v.trim())
252
+ .some((v) => v === fieldStr)
253
+ case 'like': {
254
+ const re = likeToRegExp(cf.value, false)
255
+ return re ? re.test(fieldStr) : null
256
+ }
257
+ case 'ilike': {
258
+ const re = likeToRegExp(cf.value, true)
259
+ return re ? re.test(fieldStr) : null
260
+ }
261
+ case 'match': {
262
+ try {
263
+ return new RegExp(cf.value).test(fieldStr)
264
+ } catch {
265
+ return null
266
+ }
267
+ }
268
+ case 'imatch': {
269
+ try {
270
+ return new RegExp(cf.value, 'i').test(fieldStr)
271
+ } catch {
272
+ return null
273
+ }
274
+ }
275
+ default:
276
+ return null
277
+ }
278
+ }
279
+
280
+ /** Applique la négation UNIQUEMENT au résultat sémantique évaluable (jamais à `null`). */
281
+ function matchesSingle(cf: ParsedColumnFilter, record: Record<string, unknown>): boolean {
282
+ const raw = matchesRaw(cf, record)
283
+ if (raw === null) return false // non évaluable => exclusion ferme, pas d'inversion
284
+ return cf.negated ? !raw : raw
285
+ }
286
+
287
+ /**
288
+ * Vérifie si `record` matche le filtre colonne `filter` (syntaxe Supabase/PostgREST) :
289
+ * `column=operator.value`, négation `not.`, conditions AND séparées par virgules.
290
+ *
291
+ * **Fail-CLOSED** : si `filter` n'est pas parsable dans son ensemble (une seule
292
+ * condition invalide suffit), retourne `false` — jamais un match par défaut.
293
+ *
294
+ * Ce helper est un second rideau client (defense-in-depth) : le serveur
295
+ * `aura-realtime` a déjà filtré l'event de façon autoritaire.
296
+ */
297
+ export function matchesColumnFilter(filter: string, record: Record<string, unknown>): boolean {
298
+ const set = parseFilterSet(filter)
299
+ if (!set) return false
300
+ return set.every((cf) => matchesSingle(cf, record))
301
+ }
@@ -0,0 +1,4 @@
1
+ export { RealtimeService } from './RealtimeService'
2
+ export type { ConnectionState } from './RealtimeService'
3
+ export { AuraChannel } from './AuraChannel'
4
+ export { HeartbeatManager } from './HeartbeatManager'
@@ -0,0 +1,81 @@
1
+ import { describe, it, expect, vi } from 'vitest'
2
+ import { RealtimeService } from '../src/RealtimeService'
3
+ import { AuraChannel } from '../src/AuraChannel'
4
+ import { BaseAuthStore } from '@aurabase/core'
5
+ import type { AuraClientLike } from '@aurabase/core'
6
+
7
+ function client(): AuraClientLike {
8
+ return {
9
+ baseUrl: 'http://x',
10
+ projectId: 'proj',
11
+ apiKey: 'k',
12
+ authStore: new BaseAuthStore(),
13
+ fetchFn: (async () => new Response('{}')) as unknown as typeof globalThis.fetch,
14
+ // eslint-disable-next-line @typescript-eslint/no-explicit-any
15
+ auth: { refreshSession: async () => ({ data: null, error: null }) as any },
16
+ }
17
+ }
18
+
19
+ function seedChannels(rt: RealtimeService, names: string[]): Map<string, AuraChannel> {
20
+ const map = (rt as unknown as { _channels: Map<string, AuraChannel> })._channels
21
+ for (const n of names) map.set(n, new AuraChannel(n, 'proj'))
22
+ return map
23
+ }
24
+
25
+ function dispatch(rt: RealtimeService, msg: unknown): void {
26
+ ;(rt as unknown as { _dispatch: (m: unknown) => void })._dispatch(msg)
27
+ }
28
+
29
+ describe('RealtimeService._dispatch — routage broadcast_ack par ref_id', () => {
30
+ it("un broadcast_ack n'est dispatché qu'au channel émetteur (extrait du préfixe ref_id)", () => {
31
+ const rt = new RealtimeService(client())
32
+ const map = seedChannels(rt, ['room-1', 'room-2'])
33
+ const spy1 = vi.spyOn(map.get('room-1')!, '_onMessage').mockImplementation(() => {})
34
+ const spy2 = vi.spyOn(map.get('room-2')!, '_onMessage').mockImplementation(() => {})
35
+
36
+ dispatch(rt, { type: 'broadcast_ack', message_id: 'm1', ref_id: 'proj:room-1:r3' })
37
+
38
+ expect(spy1).toHaveBeenCalledTimes(1)
39
+ expect(spy2).not.toHaveBeenCalled()
40
+ })
41
+
42
+ it('résout par message_id absent de ref_id ? — fallback dispatch à tous si ref_id non résoluble', () => {
43
+ const rt = new RealtimeService(client())
44
+ const map = seedChannels(rt, ['room-1', 'room-2'])
45
+ const spy1 = vi.spyOn(map.get('room-1')!, '_onMessage').mockImplementation(() => {})
46
+ const spy2 = vi.spyOn(map.get('room-2')!, '_onMessage').mockImplementation(() => {})
47
+
48
+ // Pas de ref_id (message_id brut, aucun ':r') → non résoluble → fallback à tous.
49
+ dispatch(rt, { type: 'broadcast_ack', message_id: 'uuid-sans-prefixe' })
50
+
51
+ expect(spy1).toHaveBeenCalledTimes(1)
52
+ expect(spy2).toHaveBeenCalledTimes(1)
53
+ })
54
+
55
+ it("la promesse channel.send(ack:true) se résout 'ok' sur réception de l'ACK routé", async () => {
56
+ const rt = new RealtimeService(client())
57
+ const map = (rt as unknown as { _channels: Map<string, AuraChannel> })._channels
58
+ const ch = new AuraChannel('room-1', 'proj', { broadcast: { ack: true } })
59
+ const sent: string[] = []
60
+ ch._bindSend((d) => sent.push(d))
61
+ map.set('room-1', ch)
62
+
63
+ const p = ch.send({ type: 'broadcast', event: 'ping', payload: { a: 1 } })
64
+
65
+ // Récupérer le ref_id réellement émis par le channel.
66
+ const outbound = JSON.parse(sent[0]!) as { ref_id: string }
67
+ dispatch(rt, { type: 'broadcast_ack', message_id: 'm', ref_id: outbound.ref_id })
68
+
69
+ await expect(p).resolves.toBe('ok')
70
+ })
71
+
72
+ it('nom de channel contenant ":r" — lastIndexOf isole bien le suffixe :r{n}', () => {
73
+ const rt = new RealtimeService(client())
74
+ const map = seedChannels(rt, ['weird:r-name'])
75
+ const spy = vi.spyOn(map.get('weird:r-name')!, '_onMessage').mockImplementation(() => {})
76
+
77
+ dispatch(rt, { type: 'broadcast_ack', message_id: 'm', ref_id: 'proj:weird:r-name:r7' })
78
+
79
+ expect(spy).toHaveBeenCalledTimes(1)
80
+ })
81
+ })
@@ -0,0 +1,144 @@
1
+ import { describe, it, expect } from 'vitest'
2
+ import { matchesColumnFilter } from '../src/columnFilter'
3
+
4
+ // Ces tests portent la même sémantique que le backend Rust
5
+ // (services/aura-realtime/src/protocol.rs) : eq/neq/lt/lte/gt/gte/in/like/ilike/
6
+ // is/match/imatch/isdistinct, négation `not.`, AND par virgules (hors `in.(...)`),
7
+ // fail-closed sur filtre non parsable, champ absent = exclusion ferme (jamais inversée).
8
+
9
+ describe('matchesColumnFilter — opérateurs de base', () => {
10
+ it('eq', () => {
11
+ expect(matchesColumnFilter('room_id=eq.42', { room_id: 42 })).toBe(true)
12
+ expect(matchesColumnFilter('room_id=eq.42', { room_id: 7 })).toBe(false)
13
+ })
14
+
15
+ it('neq', () => {
16
+ expect(matchesColumnFilter('status=neq.deleted', { status: 'active' })).toBe(true)
17
+ expect(matchesColumnFilter('status=neq.deleted', { status: 'deleted' })).toBe(false)
18
+ })
19
+
20
+ it('lt/lte/gt/gte numérique', () => {
21
+ expect(matchesColumnFilter('age=lt.18', { age: 10 })).toBe(true)
22
+ expect(matchesColumnFilter('age=lt.18', { age: 18 })).toBe(false)
23
+ expect(matchesColumnFilter('age=lte.18', { age: 18 })).toBe(true)
24
+ expect(matchesColumnFilter('qty=gt.5', { qty: 10 })).toBe(true)
25
+ expect(matchesColumnFilter('qty=gt.5', { qty: 5 })).toBe(false)
26
+ expect(matchesColumnFilter('score=gte.50', { score: 50 })).toBe(true)
27
+ expect(matchesColumnFilter('score=gte.50', { score: 49 })).toBe(false)
28
+ })
29
+
30
+ it('lt/gt sur types incompatibles => false (fail-closed)', () => {
31
+ expect(matchesColumnFilter('name=gt.100', { name: 'hello' })).toBe(false)
32
+ })
33
+
34
+ it('in — respecte les parenthèses et les espaces', () => {
35
+ expect(matchesColumnFilter('status=in.(a,b)', { status: 'b' })).toBe(true)
36
+ expect(matchesColumnFilter('status=in.(a,b)', { status: 'c' })).toBe(false)
37
+ expect(matchesColumnFilter('status=in.(active, pending, done)', { status: 'pending' })).toBe(true)
38
+ })
39
+
40
+ it('like / ilike', () => {
41
+ expect(matchesColumnFilter('name=like.jo%', { name: 'john' })).toBe(true)
42
+ expect(matchesColumnFilter('name=like.jo%', { name: 'alice' })).toBe(false)
43
+ expect(matchesColumnFilter('name=ilike.JO%', { name: 'john' })).toBe(true)
44
+ expect(matchesColumnFilter('name=ilike.JO%', { name: 'JOHN' })).toBe(true)
45
+ expect(matchesColumnFilter('name=ilike.JO%', { name: 'alice' })).toBe(false)
46
+ })
47
+
48
+ it('is — null/true/false sur la valeur réelle', () => {
49
+ expect(matchesColumnFilter('deleted_at=is.null', { deleted_at: null })).toBe(true)
50
+ expect(matchesColumnFilter('deleted_at=is.null', { deleted_at: '2026' })).toBe(false)
51
+ expect(matchesColumnFilter('active=is.true', { active: true })).toBe(true)
52
+ expect(matchesColumnFilter('active=is.true', { active: false })).toBe(false)
53
+ expect(matchesColumnFilter('active=is.false', { active: false })).toBe(true)
54
+ expect(matchesColumnFilter('active=is.false', { active: true })).toBe(false)
55
+ })
56
+
57
+ it('match / imatch — RegExp', () => {
58
+ expect(matchesColumnFilter('email=match.^[a-z]+@example\\.com$', { email: 'alice@example.com' })).toBe(true)
59
+ expect(matchesColumnFilter('email=match.^[a-z]+@example\\.com$', { email: 'ALICE@example.com' })).toBe(false)
60
+ expect(matchesColumnFilter('email=imatch.^[a-z]+@example\\.com$', { email: 'ALICE@example.com' })).toBe(true)
61
+ })
62
+
63
+ it('isdistinct — NULL distinct de toute valeur non-NULL', () => {
64
+ expect(matchesColumnFilter('status=isdistinct.done', { status: 'open' })).toBe(true)
65
+ expect(matchesColumnFilter('status=isdistinct.done', { status: 'done' })).toBe(false)
66
+ expect(matchesColumnFilter('status=isdistinct.done', { status: null })).toBe(true)
67
+ })
68
+ })
69
+
70
+ describe('matchesColumnFilter — négation not.', () => {
71
+ it('inverse le résultat sémantique', () => {
72
+ expect(matchesColumnFilter('status=not.eq.done', { status: 'open' })).toBe(true)
73
+ expect(matchesColumnFilter('status=not.eq.done', { status: 'done' })).toBe(false)
74
+ })
75
+
76
+ it('ne fail-open jamais sur champ absent (non évaluable => exclusion ferme)', () => {
77
+ // Sémantique SQL : NOT (col = x) avec col NULL/absent => NULL => exclu, pas inversé en true.
78
+ expect(matchesColumnFilter('status=not.eq.done', { id: 1 })).toBe(false)
79
+ })
80
+ })
81
+
82
+ describe('matchesColumnFilter — AND (virgules)', () => {
83
+ it('toutes les conditions doivent matcher', () => {
84
+ expect(matchesColumnFilter('room_id=eq.42,qty=gt.5', { room_id: 42, qty: 9 })).toBe(true)
85
+ expect(matchesColumnFilter('room_id=eq.42,qty=gt.5', { room_id: 42, qty: 1 })).toBe(false)
86
+ })
87
+
88
+ it("respecte les virgules internes d'un in.(...)", () => {
89
+ expect(matchesColumnFilter('status=in.(a,b),qty=gt.5', { status: 'b', qty: 9 })).toBe(true)
90
+ expect(matchesColumnFilter('status=in.(a,b),qty=gt.5', { status: 'c', qty: 9 })).toBe(false)
91
+ })
92
+ })
93
+
94
+ describe('matchesColumnFilter — fail-closed', () => {
95
+ it('filtre non parsable (syntaxe invalide) => false', () => {
96
+ expect(matchesColumnFilter('garbage', { a: 1 })).toBe(false)
97
+ })
98
+
99
+ it('opérateur inconnu => false', () => {
100
+ expect(matchesColumnFilter('col=unknown.val', { col: 'val' })).toBe(false)
101
+ })
102
+
103
+ it('une condition AND invalide invalide tout le set => false', () => {
104
+ expect(matchesColumnFilter('room_id=eq.42,garbage', { room_id: 42 })).toBe(false)
105
+ })
106
+
107
+ it('regex match/imatch invalide => filtre rejeté (false), jamais inversé par not.', () => {
108
+ expect(matchesColumnFilter('name=match.(unclosed', { name: 'anything' })).toBe(false)
109
+ expect(matchesColumnFilter('name=not.match.(unclosed', { name: 'anything' })).toBe(false)
110
+ })
111
+
112
+ it('pattern match trop long (>512) => rejeté', () => {
113
+ const longPattern = 'a'.repeat(513)
114
+ expect(matchesColumnFilter(`name=match.${longPattern}`, { name: 'a'.repeat(513) })).toBe(false)
115
+ })
116
+
117
+ it('in avec plus de 100 valeurs => rejeté', () => {
118
+ const values = Array.from({ length: 101 }, (_, i) => String(i + 1)).join(',')
119
+ expect(matchesColumnFilter(`id=in.(${values})`, { id: 1 })).toBe(false)
120
+ })
121
+
122
+ it('is.<valeur invalide> => rejeté au parse (pas null/true/false)', () => {
123
+ expect(matchesColumnFilter('active=is.garbage', { active: true })).toBe(false)
124
+ expect(matchesColumnFilter('active=is.garbage', { active: false })).toBe(false)
125
+ })
126
+
127
+ it('not.is.<valeur invalide> ne fail-open jamais (recoin fermé)', () => {
128
+ // Avant le fix : is.garbage était accepté au parse, matchesRaw renvoyait false,
129
+ // et `not.` inversait ce false en true => le filtre matchait tout event.
130
+ expect(matchesColumnFilter('active=not.is.garbage', { active: true })).toBe(false)
131
+ expect(matchesColumnFilter('active=not.is.garbage', { active: false })).toBe(false)
132
+ })
133
+ })
134
+
135
+ describe('matchesColumnFilter — champ absent', () => {
136
+ it('champ absent => exclusion ferme (jamais un match)', () => {
137
+ expect(matchesColumnFilter('xyz=eq.1', { other: 1 })).toBe(false)
138
+ })
139
+
140
+ it('champ null (présent) sur eq => comparaison textuelle "null", pas une exclusion', () => {
141
+ expect(matchesColumnFilter('name=eq.test', { name: null })).toBe(false)
142
+ expect(matchesColumnFilter('name=eq.null', { name: null })).toBe(true)
143
+ })
144
+ })