better-supabase 0.5.1 → 0.6.1
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/README.md +187 -46
- package/dist/access-sql-B0dF-SoF.js +181 -0
- package/dist/{actor-DaJjqD5R.d.ts → actor-CQTlc1Hh.d.ts} +1 -1
- package/dist/adapter-CHnPkCPd.js +1160 -0
- package/dist/agents-CVHGd7It.d.ts +94 -0
- package/dist/ai-cache-CBWs1W0k.js +80 -0
- package/dist/ai-cache-DaDu_G5L.d.ts +64 -0
- package/dist/ai-chat-7amvXjmQ.d.ts +590 -0
- package/dist/ai-chat-B24UI962.js +743 -0
- package/dist/ai-files-CSPNswUY.js +319 -0
- package/dist/ai-sdk/agents/index.d.ts +74 -0
- package/dist/ai-sdk/agents/index.js +159 -0
- package/dist/ai-sdk/batches/index.d.ts +74 -0
- package/dist/ai-sdk/batches/index.js +180 -0
- package/dist/ai-sdk/cache/index.d.ts +38 -0
- package/dist/ai-sdk/cache/index.js +127 -0
- package/dist/ai-sdk/chat/index.d.ts +2 -0
- package/dist/ai-sdk/chat/index.js +246 -0
- package/dist/ai-sdk/embeddings/index.d.ts +3 -0
- package/dist/ai-sdk/embeddings/index.js +2 -0
- package/dist/ai-sdk/files/index.d.ts +61 -0
- package/dist/ai-sdk/files/index.js +111 -0
- package/dist/ai-sdk/index.d.ts +145 -0
- package/dist/ai-sdk/index.js +174 -0
- package/dist/ai-sdk/mcp/index.d.ts +102 -0
- package/dist/ai-sdk/mcp/index.js +362 -0
- package/dist/ai-sdk/memory/index.d.ts +67 -0
- package/dist/ai-sdk/memory/index.js +175 -0
- package/dist/ai-sdk/react/index.d.ts +2 -0
- package/dist/ai-sdk/react/index.js +3 -0
- package/dist/ai-sdk/react/server.d.ts +6 -0
- package/dist/ai-sdk/react/server.js +7 -0
- package/dist/ai-sdk/workflow/index.d.ts +249 -0
- package/dist/ai-sdk/workflow/index.js +575 -0
- package/dist/ai-sdk/workflow/react/index.d.ts +20 -0
- package/dist/ai-sdk/workflow/react/index.js +66 -0
- package/dist/ai-sdk/workflow/react/server.d.ts +10 -0
- package/dist/ai-sdk/workflow/react/server.js +12 -0
- package/dist/ai-tasks-Cd1HMFvB.d.ts +131 -0
- package/dist/announcements-CiyOs-xP.js +107 -0
- package/dist/announcements-ZRGnScWI.d.ts +72 -0
- package/dist/api-keys-rCGtKaZA.d.ts +156 -0
- package/dist/assistant-DwdpDH1q.d.ts +147 -0
- package/dist/astro/index.d.ts +86 -0
- package/dist/astro/index.js +93 -0
- package/dist/attributes-5dYul0u0.js +13 -0
- package/dist/auth-link-BjFPqwLz.d.ts +97 -0
- package/dist/auth-link-DrDX9KOT.js +114 -0
- package/dist/authorization-BFCqamMs.d.ts +172 -0
- package/dist/before-route-CnR0C0Jr.js +30 -0
- package/dist/bind-Bu_ZNa3W.d.ts +62 -0
- package/dist/bind-l4kAvYSv.js +97 -0
- package/dist/block-events-CrT8r7Fe.js +76 -0
- package/dist/block-fields-GFkxl4dP.js +49 -0
- package/dist/block-fields-SJ6fEf3o.d.ts +31 -0
- package/dist/block-helpers-CKtbFcRv.d.ts +37 -0
- package/dist/block-helpers-DWHxHsEQ.js +190 -0
- package/dist/block-hooks-BmjMyXTa.js +114 -0
- package/dist/block-hooks-plbZ0AbJ.d.ts +107 -0
- package/dist/{kit-transport-DJlHQUBU.js → block-transport-BLr-ZgLc.js} +11 -7
- package/dist/block-transport-DvaoPuZ9.d.ts +48 -0
- package/dist/blocks/agents/index.d.ts +3 -0
- package/dist/blocks/agents/index.js +123 -0
- package/dist/blocks/ai-cache/index.d.ts +3 -0
- package/dist/blocks/ai-cache/index.js +3 -0
- package/dist/blocks/ai-chat/index.d.ts +31 -0
- package/dist/blocks/ai-chat/index.js +5 -0
- package/dist/blocks/ai-chat/react/index.d.ts +77 -0
- package/dist/blocks/ai-chat/react/index.js +387 -0
- package/dist/blocks/ai-chat/react/server.d.ts +16 -0
- package/dist/blocks/ai-chat/react/server.js +14 -0
- package/dist/blocks/ai-files/index.d.ts +14 -0
- package/dist/blocks/ai-files/index.js +3 -0
- package/dist/blocks/ai-providers/index.d.ts +7 -0
- package/dist/blocks/ai-providers/index.js +211 -0
- package/dist/blocks/ai-tasks/index.d.ts +3 -0
- package/dist/blocks/ai-tasks/index.js +203 -0
- package/dist/blocks/announcements/index.d.ts +3 -0
- package/dist/blocks/announcements/index.js +3 -0
- package/dist/blocks/announcements/react/index.d.ts +31 -0
- package/dist/blocks/announcements/react/index.js +86 -0
- package/dist/blocks/announcements/react/server.d.ts +6 -0
- package/dist/blocks/announcements/react/server.js +7 -0
- package/dist/blocks/api-keys/index.d.ts +35 -0
- package/dist/blocks/api-keys/index.js +334 -0
- package/dist/blocks/attachments/index.d.ts +224 -0
- package/dist/blocks/attachments/index.js +234 -0
- package/dist/blocks/audit/index.d.ts +366 -0
- package/dist/blocks/audit/index.js +714 -0
- package/dist/blocks/billing/index.d.ts +265 -0
- package/dist/blocks/billing/index.js +400 -0
- package/dist/blocks/comments/index.d.ts +200 -0
- package/dist/blocks/comments/index.js +207 -0
- package/dist/blocks/connectors/index.d.ts +3 -0
- package/dist/blocks/connectors/index.js +164 -0
- package/dist/blocks/data-lifecycle/index.d.ts +259 -0
- package/dist/blocks/data-lifecycle/index.js +358 -0
- package/dist/blocks/entitlements/index.d.ts +14 -0
- package/dist/blocks/entitlements/index.js +25 -0
- package/dist/blocks/flags/index.d.ts +226 -0
- package/dist/blocks/flags/index.js +282 -0
- package/dist/blocks/inbox/index.d.ts +6 -0
- package/dist/blocks/inbox/index.js +499 -0
- package/dist/blocks/inbox/react/index.d.ts +2 -0
- package/dist/blocks/inbox/react/index.js +225 -0
- package/dist/blocks/inbox/react/server.d.ts +10 -0
- package/dist/blocks/inbox/react/server.js +12 -0
- package/dist/blocks/index.d.ts +65 -0
- package/dist/blocks/index.js +77 -0
- package/dist/blocks/jobs/index.d.ts +297 -0
- package/dist/blocks/jobs/index.js +1051 -0
- package/dist/blocks/knowledge/index.d.ts +3 -0
- package/dist/blocks/knowledge/index.js +3 -0
- package/dist/blocks/memory/index.d.ts +8 -0
- package/dist/blocks/memory/index.js +289 -0
- package/dist/blocks/notifications/index.d.ts +297 -0
- package/dist/blocks/notifications/index.js +425 -0
- package/dist/blocks/notifications/react/index.d.ts +44 -0
- package/dist/blocks/notifications/react/index.js +67 -0
- package/dist/blocks/notifications/react/server.d.ts +6 -0
- package/dist/blocks/notifications/react/server.js +7 -0
- package/dist/blocks/onboarding/index.d.ts +3 -0
- package/dist/blocks/onboarding/index.js +64 -0
- package/dist/blocks/onboarding/react/index.d.ts +28 -0
- package/dist/blocks/onboarding/react/index.js +78 -0
- package/dist/blocks/onboarding/react/server.d.ts +7 -0
- package/dist/blocks/onboarding/react/server.js +7 -0
- package/dist/blocks/organizations/index.d.ts +241 -0
- package/dist/blocks/organizations/index.js +400 -0
- package/dist/blocks/outbox/index.d.ts +3 -0
- package/dist/blocks/outbox/index.js +266 -0
- package/dist/blocks/profiles/index.d.ts +40 -0
- package/dist/blocks/profiles/index.js +28 -0
- package/dist/blocks/push/index.d.ts +185 -0
- package/dist/blocks/push/index.js +258 -0
- package/dist/blocks/settings/index.d.ts +89 -0
- package/dist/blocks/settings/index.js +104 -0
- package/dist/blocks/sso/index.d.ts +198 -0
- package/dist/blocks/sso/index.js +1297 -0
- package/dist/blocks/usage/index.d.ts +4 -0
- package/dist/blocks/usage/index.js +233 -0
- package/dist/blocks/waitlist/index.d.ts +117 -0
- package/dist/blocks/waitlist/index.js +132 -0
- package/dist/blocks/webhooks/index.d.ts +304 -0
- package/dist/{webhooks → blocks/webhooks}/index.js +468 -148
- package/dist/blocks/workflow-builder/index.d.ts +21 -0
- package/dist/blocks/workflow-builder/index.js +5 -0
- package/dist/blocks/workflow-builder/react/index.d.ts +47 -0
- package/dist/blocks/workflow-builder/react/index.js +119 -0
- package/dist/blocks/workflow-builder/react/server.d.ts +8 -0
- package/dist/blocks/workflow-builder/react/server.js +11 -0
- package/dist/blocks/workflows/index.d.ts +3 -0
- package/dist/blocks/workflows/index.js +3 -0
- package/dist/blocks/workflows/react/index.d.ts +47 -0
- package/dist/blocks/workflows/react/index.js +137 -0
- package/dist/blocks/workflows/react/server.d.ts +8 -0
- package/dist/blocks/workflows/react/server.js +11 -0
- package/dist/{index-SaTsEfGP.d.ts → bucket-Cl46E3jS.d.ts} +134 -91
- package/dist/{simplify-_0oUKqCd.js → build-BXIzE6Xv.js} +222 -124
- package/dist/{channel-FQW-CnGR.d.ts → channel-_2Mx1Oyu.d.ts} +2 -2
- package/dist/channels-CkgQ_X60.js +114 -0
- package/dist/chat-sdk/index.d.ts +303 -0
- package/dist/chat-sdk/index.js +733 -0
- package/dist/chat-sdk/react/index.d.ts +2 -0
- package/dist/chat-sdk/react/index.js +3 -0
- package/dist/chat-sdk/react/server.d.ts +3 -0
- package/dist/chat-sdk/react/server.js +2 -0
- package/dist/claims-DDBQT6BK.js +61 -0
- package/dist/{claims-BAf6c7is.js → claims-PqzCxaTD.js} +39 -2
- package/dist/cli/bin.js +1 -1
- package/dist/cli/{codemod-DqV7612m.js → codemod-Bl_tQpHn.js} +105 -2
- package/dist/cli/config-B9HkL8nF.js +2 -0
- package/dist/cli/config-DU7EJImt.js +49 -0
- package/dist/cli/config-NiPxAmUF.js +1077 -0
- package/dist/cli/{dist-xn4wlboc.js → dist-DRAVaE6w.js} +111 -1
- package/dist/cli/{doctor-C_A-KEAz.js → doctor-BMqBgM3X.js} +991 -317
- package/dist/cli/{env-DmKv_G4A.js → env-CJLbUVPB.js} +1 -1
- package/dist/cli/{env-jIFUADLY.js → env-qXgyMV6r.js} +2 -2
- package/dist/cli/fresh-import-BeYf9I3x.js +40 -0
- package/dist/cli/gen-4JlOwgWG.js +2 -0
- package/dist/cli/gen-DeuDgPxN.js +3100 -0
- package/dist/cli/index.d.ts +44 -5
- package/dist/cli/index.js +7 -7
- package/dist/cli/{init-DMa6M7hV.js → init-C7Xd0Kda.js} +242 -27
- package/dist/cli/{introspect-BKlaIu_M.js → introspect-CSnMJ94U.js} +5 -5
- package/dist/cli/{io-k41_z-jT.js → io-DI1mTdSA.js} +12 -3
- package/dist/cli/{keys-ByKHnvP9.js → keys-BGCnnb-s.js} +8 -6
- package/dist/cli/{ohash-D7bG_EOb.js → ohash-BjQY1cBb.js} +1 -1
- package/dist/cli/{openapi-Cp5Fyta-.js → openapi-BlRJ18s3.js} +2 -2
- package/dist/cli/{project-B_GeXyHh.js → project-DZRaBaY7.js} +11 -6
- package/dist/cli/read-sets-CETOSFx4.js +223 -0
- package/dist/cli/{run-Bo_NLT5n.js → run-C8tnlHSb.js} +33 -23
- package/dist/cli/{seed-CUlqWh-8.js → seed-DQzqT7LP.js} +8 -6
- package/dist/cli/{skills-LJJIhLFR.js → skills-QQs4q68J.js} +3 -2
- package/dist/cli/sql-D0GUtUPp.js +536 -0
- package/dist/cli/{config-CPVpXP5I.js → supabase-toml-B-bIukQF.js} +72 -942
- package/dist/cli/typegen-version-DeFbtzYI.js +8 -0
- package/dist/cli/{version-BCtlgDQx.js → version-C_4JwyiM.js} +1 -1
- package/dist/client/index.d.ts +3 -2
- package/dist/client/index.js +11 -71
- package/dist/client/native/index.d.ts +228 -0
- package/dist/client/native/index.js +361 -0
- package/dist/client-CI0Z2zWX.js +277 -0
- package/dist/cloud-event-BnmD5fb9.js +46 -0
- package/dist/cloud-event-C1JCc1bH.d.ts +50 -0
- package/dist/{compiler-BXH_aZ7N.d.ts → compiler-DY_wkj4R.d.ts} +2 -2
- package/dist/compose-BIBNgAnq.js +53 -0
- package/dist/composite-CLqs9-OA.js +130 -0
- package/dist/composite-pSfeJNce.d.ts +146 -0
- package/dist/config/index.d.ts +6 -4
- package/dist/config/index.js +5 -5
- package/dist/{config-Bv7gvfzC.d.ts → config-D44Y7PAv.d.ts} +406 -214
- package/dist/{config-DreERlKo.js → config-Dn7F5k9A.js} +62 -14
- package/dist/connector-Dv8kK0MO.d.ts +107 -0
- package/dist/connectors-hkLCwyEz.d.ts +120 -0
- package/dist/context-BCrWe7ow.d.ts +237 -0
- package/dist/credentials/index.d.ts +31 -0
- package/dist/credentials/index.js +168 -0
- package/dist/cron-DVPUg5k3.js +145 -0
- package/dist/csv-CPAzEmB6.js +25 -0
- package/dist/db-exception-CnWcYPxL.js +15 -0
- package/dist/{define-cNKzqwL_.js → define-B-TVjC25.js} +784 -227
- package/dist/{define-Bp_BZa5h.d.ts → define-CJJkkr1-.d.ts} +353 -61
- package/dist/edge/index.d.ts +69 -11
- package/dist/edge/index.js +191 -75
- package/dist/elysia/index.d.ts +59 -0
- package/dist/elysia/index.js +62 -0
- package/dist/embeddings-4ZvFzjsU.js +127 -0
- package/dist/embeddings-C9KGQnXt.d.ts +68 -0
- package/dist/{refresh-CFPX0Qft.d.ts → ended-jLfvw8h-.d.ts} +20 -2
- package/dist/{entitlements-BiJZ4t49.d.ts → entitlements-DHrY2UDZ.d.ts} +16 -7
- package/dist/entitlements-DRl6ZJUm.js +24 -0
- package/dist/envelope-DqnYGRw7.js +47 -0
- package/dist/{errors-DtwXa6b8.js → errors-B11Duf73.js} +82 -13
- package/dist/errors-DLjJHPLK.js +44 -0
- package/dist/escape-like-DHfx6jRr.js +7 -0
- package/dist/escape-like-ImTK0ps_.d.ts +15 -0
- package/dist/eve/index.d.ts +220 -0
- package/dist/eve/index.js +504 -0
- package/dist/eve-gT2Fpjut.d.ts +151 -0
- package/dist/events/index.d.ts +4 -3
- package/dist/events/index.js +9 -50
- package/dist/events-DWDhx77e.d.ts +549 -0
- package/dist/executor-Bq28caBo.js +718 -0
- package/dist/executor-C-R4pucm.js +83 -0
- package/dist/executor-CaWigTyR.d.ts +148 -0
- package/dist/executor-D14tGopJ.d.ts +36 -0
- package/dist/{executor-Cghdstwu.d.ts → executor-DbvExCFC.d.ts} +7 -1
- package/dist/{sql-Coh6n4cC.js → executor-De1Of3m3.js} +210 -11
- package/dist/executor-DvsBbNBS.d.ts +261 -0
- package/dist/expo/index.d.ts +79 -0
- package/dist/expo/index.js +129 -0
- package/dist/expo-sqlite/index.d.ts +58 -0
- package/dist/expo-sqlite/index.js +104 -0
- package/dist/field-errors-BFOa7WhB.d.ts +99 -0
- package/dist/field-errors-VF1yb8yI.js +13 -0
- package/dist/framework-CdPDPpFs.d.ts +25 -0
- package/dist/framework-xjTBoyBB.js +51 -0
- package/dist/gateway-C1ssRHnf.d.ts +130 -0
- package/dist/graph-BGyLLFLX.js +265 -0
- package/dist/h3/index.d.ts +59 -0
- package/dist/h3/index.js +55 -0
- package/dist/h3/v1/index.d.ts +63 -0
- package/dist/h3/v1/index.js +49 -0
- package/dist/history-BZih6SFP.js +64 -0
- package/dist/hono/index.d.ts +56 -7
- package/dist/hono/index.js +97 -45
- package/dist/{react-C_s7kiNz.js → hooks-1BTCJsiw.js} +104 -133
- package/dist/http-Cv4T29xh.js +40 -0
- package/dist/http-DdYadv-o.js +126 -0
- package/dist/image-url-pIVlO6R1.js +41 -0
- package/dist/{impersonation-BgSHAYLf.js → impersonation-DcqaTBHx.js} +21 -6
- package/dist/inbox-i7E1ppo6.d.ts +384 -0
- package/dist/index-ACiNPN4Z.d.ts +74 -0
- package/dist/{index-DgILJRXj.d.ts → index-Bepi-Hgg.d.ts} +91 -28
- package/dist/index-BkWT_uBO.d.ts +51 -0
- package/dist/index-BpFYMsi0.d.ts +53 -0
- package/dist/{index-C7f8cOBM.d.ts → index-Bxih3l0u.d.ts} +2 -2
- package/dist/index-CaxrMD1A.d.ts +1 -0
- package/dist/index-ClmjK_o-.d.ts +55 -0
- package/dist/index-CtuakVRc.d.ts +267 -0
- package/dist/index-D48jVS-N.d.ts +246 -0
- package/dist/index-DDrYaUcq.d.ts +138 -0
- package/dist/index-DQh4VCc9.d.ts +98 -0
- package/dist/{live-D9W2M5aO.d.ts → index-DunNRdnT.d.ts} +136 -38
- package/dist/index.d.ts +65 -18
- package/dist/index.js +42 -14
- package/dist/invalidate-BBqhpxyB.d.ts +19 -0
- package/dist/json-CEnpTHlZ.js +16 -0
- package/dist/json-schema-CvER4kgT.js +502 -0
- package/dist/keys-4MnkWTTh.js +35 -0
- package/dist/kit-B5tid-q5.d.ts +60 -0
- package/dist/kit-Fm-Fv7q5.js +78 -0
- package/dist/knowledge-BXBbT0Tr.d.ts +176 -0
- package/dist/knowledge-CDmiMLew.js +255 -0
- package/dist/lint/index.d.ts +1 -1
- package/dist/lint/index.js +37 -0
- package/dist/list/index.d.ts +3 -2
- package/dist/list/index.js +1 -444
- package/dist/{index-fag8VJZ2.d.ts → list-query-BRt-zDF7.d.ts} +20 -12
- package/dist/list-query-gl808ac1.js +459 -0
- package/dist/{live-CdJBOc4f.js → live-CTAZQkwl.js} +99 -14
- package/dist/logger-DWVYUmwi.d.ts +18 -0
- package/dist/mcp/index.d.ts +38 -11
- package/dist/mcp/index.js +82 -46
- package/dist/mcp/sdk/index.d.ts +105 -0
- package/dist/mcp/sdk/index.js +126 -0
- package/dist/message-CaKlqdsX.js +219 -0
- package/dist/message-CfDoR0Y6.d.ts +94 -0
- package/dist/messages-B9-5887d.js +715 -0
- package/dist/messages-DYP0sUTe.d.ts +59 -0
- package/dist/mfa-Daak8ukG.d.ts +54 -0
- package/dist/modules-DpPwzC7E.d.ts +192 -0
- package/dist/modules-dxrVK3ID.js +228 -0
- package/dist/nestjs/index.d.ts +68 -0
- package/dist/nestjs/index.js +164 -0
- package/dist/next/client/index.d.ts +35 -0
- package/dist/next/client/index.js +36 -0
- package/dist/next/image/index.js +3 -24
- package/dist/next/index.d.ts +150 -51
- package/dist/next/index.js +261 -107
- package/dist/node/index.d.ts +4 -0
- package/dist/node/index.js +210 -0
- package/dist/node-headers-AO06VhqH.d.ts +24 -0
- package/dist/node-headers-C71zNQHe.js +55 -0
- package/dist/nuxt/index.d.ts +60 -0
- package/dist/nuxt/index.js +64 -0
- package/dist/onboarding-BZGHYJks.d.ts +62 -0
- package/dist/openapi/index.d.ts +1 -1
- package/dist/openapi/index.js +3 -3
- package/dist/orpc/index.d.ts +70 -11
- package/dist/orpc/index.js +73 -24
- package/dist/otel/index.d.ts +7 -7
- package/dist/otel/index.js +87 -57
- package/dist/outbox-Cq7pPYvl.d.ts +161 -0
- package/dist/{plugin-o144f322.d.ts → plugin-FGY8UhYm.d.ts} +30 -10
- package/dist/plugin-tfXW1-qO.js +45 -0
- package/dist/plugins/actor/index.d.ts +2 -2
- package/dist/plugins/actor/index.js +3 -3
- package/dist/plugins/rules/index.d.ts +11 -4
- package/dist/plugins/rules/index.js +40 -13
- package/dist/plugins/soft-delete/index.d.ts +5 -4
- package/dist/plugins/soft-delete/index.js +10 -5
- package/dist/plugins/tenant/index.d.ts +2 -2
- package/dist/plugins/tenant/index.js +18 -11
- package/dist/plugins/timestamps/index.d.ts +5 -3
- package/dist/plugins/timestamps/index.js +6 -4
- package/dist/plugins/validation/index.d.ts +2 -2
- package/dist/plugins/validation/index.js +10 -6
- package/dist/{pool-C9voM4Kw.d.ts → pool-CAu2nLrL.d.ts} +2 -2
- package/dist/postgres/index.d.ts +4 -4
- package/dist/postgres/index.js +1 -2
- package/dist/{postgrest-CruYKAvV.d.ts → postgrest-BLVdEnfe.d.ts} +13 -3
- package/dist/powersync/index.d.ts +43 -0
- package/dist/powersync/index.js +199 -0
- package/dist/powersync/react/index.d.ts +87 -0
- package/dist/powersync/react/index.js +108 -0
- package/dist/{problem-BW1KhzSG.js → problem-BreOl8b8.js} +14 -3
- package/dist/{problem-Blxe8S31.d.ts → problem-DD8LZFjc.d.ts} +21 -3
- package/dist/provider-BH5UWZJ6.d.ts +113 -0
- package/dist/provider-Di3sRutW.js +57 -0
- package/dist/query/index.d.ts +4 -2
- package/dist/query/index.js +4 -3
- package/dist/{query-CCbo44lG.js → query-BQ-NaZon.js} +141 -24
- package/dist/{queue-BYUCjr59.d.ts → queue-CLJurhdy.d.ts} +258 -83
- package/dist/react/index.d.ts +11 -7
- package/dist/react/index.js +7 -3
- package/dist/react/native/index.d.ts +116 -0
- package/dist/react/native/index.js +130 -0
- package/dist/react/server.d.ts +28 -10
- package/dist/react/server.js +22 -5
- package/dist/react/session.d.ts +2 -2
- package/dist/react-4ZqkP8da.js +79 -0
- package/dist/react-F80mF4-t.js +409 -0
- package/dist/react-router/index.d.ts +83 -0
- package/dist/react-router/index.js +89 -0
- package/dist/react-topic-seAWjL3_.js +38 -0
- package/dist/{read-set-DQFt1N_Y.js → read-set-DuKWC9Mj.js} +67 -25
- package/dist/realtime/index.d.ts +4 -5
- package/dist/realtime/index.js +144 -82
- package/dist/refusal-CeJWxmkH.js +61 -0
- package/dist/refusal-D1rgbzNq.d.ts +15 -0
- package/dist/replicas-CwrYp5Zj.js +94 -0
- package/dist/{view-Bn6HNKUe.js → resolve-D_IcnvFd.js} +248 -240
- package/dist/{impersonation-BmePsuxz.d.ts → resolve-KW8nCeL6.d.ts} +29 -69
- package/dist/{resource-mqfJW4YF.js → resource-BsvzCMlX.js} +4 -4
- package/dist/{resource-DNRFP7KR.d.ts → resource-D6Uc7Sk7.d.ts} +5 -5
- package/dist/respond-BUgsXNgk.js +91 -0
- package/dist/respond-DoBijysA.d.ts +93 -0
- package/dist/result-D7y2fT87.d.ts +282 -0
- package/dist/{result-BKiliPY7.js → result-DzUbqgdW.js} +27 -2
- package/dist/run-state-CPo-1X2B.d.ts +12 -0
- package/dist/run-state-CgBKzYMW.js +13 -0
- package/dist/search-CUWCRYKL.js +9 -0
- package/dist/server/index.d.ts +143 -22
- package/dist/server/index.js +46 -14
- package/dist/{respond-BR1fJWOB.d.ts → server-C6DWVEVG.d.ts} +237 -85
- package/dist/session-ChiXDZdZ.d.ts +72 -0
- package/dist/shared-CBxosKK3.js +75 -0
- package/dist/{shared-D1dRUrAg.js → shared-CxWmcyqB.js} +2 -1
- package/dist/shared-DDSjEGDR.d.ts +11 -0
- package/dist/sign-in-BCV5S2WZ.d.ts +49 -0
- package/dist/sign-in-Cb1mwv6X.js +86 -0
- package/dist/{signers-Cc6r98SW.d.ts → signers-4y62_VIi.d.ts} +13 -9
- package/dist/simplify-DDO0j0LI.js +83 -0
- package/dist/solid/index.d.ts +131 -0
- package/dist/solid/index.js +344 -0
- package/dist/solid-start/index.d.ts +75 -0
- package/dist/solid-start/index.js +58 -0
- package/dist/{spec-pins-C3oc-Tdp.js → spec-pins--j6mTok4.js} +8 -1
- package/dist/{spec-pins-CqorWDnO.d.ts → spec-pins-C89s6DyW.d.ts} +7 -0
- package/dist/sql/index.d.ts +313 -212
- package/dist/sql/index.js +34924 -6609
- package/dist/ssr/index.d.ts +6 -5
- package/dist/ssr/index.js +3 -3
- package/dist/{standard-D7r6dMUu.js → standard-BK0cqJ7y.js} +4 -3
- package/dist/{standard-J4YgMOLm.d.ts → standard-CYsm7xpT.d.ts} +1 -1
- package/dist/stats-D_AD9GGu.js +1123 -0
- package/dist/storage/index.d.ts +5 -5
- package/dist/storage/index.js +1027 -368
- package/dist/store-BdH5ruDx.js +66 -0
- package/dist/store-CGjgWu5n.d.ts +68 -0
- package/dist/streams/index.d.ts +76 -0
- package/dist/streams/index.js +77 -0
- package/dist/streams/redis/index.d.ts +52 -0
- package/dist/streams/redis/index.js +185 -0
- package/dist/stripe-BMj5LtsT.js +38 -0
- package/dist/stripe-D04h-QBT.d.ts +136 -0
- package/dist/{support-view-B8SlToQX.d.ts → support-view-33Oi07TT.d.ts} +1 -1
- package/dist/{support-wHnK6zp-.js → support-zIa1URaN.js} +32 -11
- package/dist/svelte/index.d.ts +114 -0
- package/dist/svelte/index.js +344 -0
- package/dist/sveltekit/index.d.ts +116 -0
- package/dist/sveltekit/index.js +106 -0
- package/dist/tags-B99_U9Pp.js +39 -0
- package/dist/tags-Cx-LnIQP.d.ts +30 -0
- package/dist/tanstack-db/index.d.ts +71 -0
- package/dist/tanstack-db/index.js +72 -0
- package/dist/tanstack-start/index.d.ts +75 -0
- package/dist/tanstack-start/index.js +95 -0
- package/dist/tee-CoEppCER.js +101 -0
- package/dist/{template-TxrGI5jR.js → template--jDkl3UP.js} +34 -13
- package/dist/template-CwO38Do8.d.ts +6 -0
- package/dist/temporal-BJGitm-h.js +79 -0
- package/dist/{temporal-required-CAt4Dc9Z.js → temporal-required-BVvW7SYJ.js} +3 -2
- package/dist/tenant-I1TEZ_BX.js +27 -0
- package/dist/tenant-icPeuUBN.d.ts +17 -0
- package/dist/testing/index.d.ts +315 -41
- package/dist/testing/index.js +1046 -79
- package/dist/timing-CyTQPubY.js +92 -0
- package/dist/ttl-Hr34gRu8.js +12 -0
- package/dist/{types-Dnf_wRDz.d.ts → types-6BdhkmZe.d.ts} +111 -45
- package/dist/types-BI-0Sl2v.d.ts +359 -0
- package/dist/types-C05bxW-W.d.ts +220 -0
- package/dist/types-DGs5L0VN.d.ts +192 -0
- package/dist/types-TaiEeBGA.d.ts +156 -0
- package/dist/usage-CrvrL2vw.d.ts +177 -0
- package/dist/user-change-XzvgafU4.js +130 -0
- package/dist/vault-CSO7ss5C.d.ts +50 -0
- package/dist/vercel-connect/index.d.ts +47 -0
- package/dist/vercel-connect/index.js +119 -0
- package/dist/{verify-Ce49ETPz.js → verify-DL41hbvY.js} +85 -13
- package/dist/verify-DPuWngD4.d.ts +172 -0
- package/dist/{version-DOYTYwHP.js → version-CQt77F3T.js} +1 -1
- package/dist/view-BeFcRV6G.js +99 -0
- package/dist/{view-CbgObAeb.d.ts → view-DbxLNu8L.d.ts} +22 -5
- package/dist/vue/index.d.ts +141 -0
- package/dist/vue/index.js +362 -0
- package/dist/watch-DI34G86j.d.ts +60 -0
- package/dist/watch-Xuxrvxq-.js +122 -0
- package/dist/{wire-D2dT86vi.js → wire-DtZQ4sdE.js} +15 -5
- package/dist/workflow-builder-DJ1oHn8g.js +398 -0
- package/dist/workflow-sdk/builder/index.d.ts +85 -0
- package/dist/workflow-sdk/builder/index.js +250 -0
- package/dist/workflow-sdk/index.d.ts +14 -0
- package/dist/workflow-sdk/index.js +3 -0
- package/dist/workflow-sdk/world/index.d.ts +107 -0
- package/dist/workflow-sdk/world/index.js +702 -0
- package/dist/workflow-sdk-B3yLD6Br.d.ts +98 -0
- package/dist/workflow-sdk-C2WJ_G2O.js +173 -0
- package/dist/workflow-sdk-world-D-prir6O.js +1397 -0
- package/dist/workflows-B0j9WKPu.d.ts +185 -0
- package/dist/workflows-wu1WWBn7.js +216 -0
- package/package.json +461 -20
- package/schemas/ai-message-v1.json +258 -0
- package/schemas/config-v1.json +1074 -140
- package/schemas/doctor-report-v1.json +20 -1
- package/schemas/snapshot-v2.json +8 -0
- package/skills/better-supabase/SKILL.md +41 -12
- package/skills/better-supabase/references/frontend.md +96 -0
- package/skills/better-supabase/references/plugins.md +10 -6
- package/skills/better-supabase/references/troubleshooting.md +2 -1
- package/skills/better-supabase-api/SKILL.md +76 -22
- package/skills/better-supabase-api/references/adapters.md +207 -0
- package/skills/better-supabase-api/references/ai.md +125 -0
- package/skills/better-supabase-api/references/expo.md +65 -0
- package/skills/better-supabase-api/references/powersync.md +35 -0
- package/skills/better-supabase-api/references/streams-credentials.md +105 -0
- package/skills/better-supabase-auth/SKILL.md +47 -11
- package/skills/better-supabase-auth/references/authorization-providers.md +51 -0
- package/skills/better-supabase-testing/SKILL.md +3 -1
- package/dist/cli/config-dtoekM0v.js +0 -2
- package/dist/cli/gen-CIdSQZad.js +0 -2
- package/dist/cli/gen-CjElD1Vw.js +0 -2241
- package/dist/cli/read-sets-VQcPgxU9.js +0 -436
- package/dist/cli/sql-CVx-1O8G.js +0 -373
- package/dist/entitlements-CTEBm7IB.js +0 -20
- package/dist/events-aIO7vsIk.d.ts +0 -230
- package/dist/executor-BunPOO-k.d.ts +0 -402
- package/dist/executor-XycESQPH.js +0 -94
- package/dist/index-CWMDdEr4.d.ts +0 -50
- package/dist/index-DOESKG27.d.ts +0 -100
- package/dist/index-DmXSxbLi.d.ts +0 -62
- package/dist/index-Dp2VDwog.d.ts +0 -161
- package/dist/jobs/index.d.ts +0 -226
- package/dist/jobs/index.js +0 -1007
- package/dist/json-schema-m1fiwKgN.js +0 -208
- package/dist/kit-events-CTFDfSwb.js +0 -60
- package/dist/kit-transport-B82k1djG.d.ts +0 -33
- package/dist/kits-CGLLrNPd.js +0 -157
- package/dist/mfa-Cu96oJdw.d.ts +0 -30
- package/dist/notifications/index.d.ts +0 -162
- package/dist/notifications/index.js +0 -264
- package/dist/orgs/index.d.ts +0 -132
- package/dist/orgs/index.js +0 -245
- package/dist/permdock-sql-C2Inc6Xc.d.ts +0 -29
- package/dist/permdock-sql-DgOfeixi.js +0 -51
- package/dist/plugin-DwARMuQp.js +0 -59
- package/dist/server-B1mZ7CgQ.js +0 -660
- package/dist/stats-Bqob3WqT.js +0 -592
- package/dist/support-CEPwlw2G.d.ts +0 -89
- package/dist/template-mbvlysOo.d.ts +0 -6
- package/dist/temporal-CVlIV2DZ.js +0 -40
- package/dist/user-change-CHFEUro1.js +0 -49
- package/dist/webhooks/index.d.ts +0 -285
- package/skills/better-supabase-auth/references/permdock.md +0 -122
package/dist/testing/index.js
CHANGED
|
@@ -1,14 +1,24 @@
|
|
|
1
|
-
import {
|
|
2
|
-
import {
|
|
3
|
-
import {
|
|
4
|
-
import {
|
|
5
|
-
import {
|
|
1
|
+
import { t as DbException } from "../db-exception-CnWcYPxL.js";
|
|
2
|
+
import { n as dbError } from "../errors-B11Duf73.js";
|
|
3
|
+
import { d as temporalText } from "../temporal-BJGitm-h.js";
|
|
4
|
+
import { t as temporal } from "../temporal-required-BVvW7SYJ.js";
|
|
5
|
+
import { a as ok } from "../result-DzUbqgdW.js";
|
|
6
|
+
import { t as BetterSupabase } from "../define-B-TVjC25.js";
|
|
6
7
|
import { a as toBase64Url, r as fromBase64, t as base64ToText } from "../base64-D2gsioCY.js";
|
|
7
|
-
import { a as resolveConfig } from "../kits-CGLLrNPd.js";
|
|
8
8
|
import { t as isList } from "../guards-BnzDvFrz.js";
|
|
9
|
+
import { a as resolveConfig } from "../modules-dxrVK3ID.js";
|
|
9
10
|
import { ENV_VARIABLES } from "../env/index.js";
|
|
10
|
-
import { n as postgresExecutor } from "../executor-
|
|
11
|
+
import { n as postgresExecutor } from "../executor-De1Of3m3.js";
|
|
12
|
+
import "../replicas-CwrYp5Zj.js";
|
|
13
|
+
import { a as tenantCredentialRef } from "../provider-Di3sRutW.js";
|
|
14
|
+
import { n as parseDbStats } from "../timing-CyTQPubY.js";
|
|
15
|
+
import { t as rawError } from "../block-transport-BLr-ZgLc.js";
|
|
16
|
+
import { a as isScopeName, c as templateFunctions, l as templatePlaceholders, n as TEMPLATE_PLACEHOLDERS, s as scopeProblems, u as templateProblems } from "../access-sql-B0dF-SoF.js";
|
|
11
17
|
import { toCloudEvents } from "../events/index.js";
|
|
18
|
+
import { timestamps } from "../plugins/timestamps/index.js";
|
|
19
|
+
import { softDelete } from "../plugins/soft-delete/index.js";
|
|
20
|
+
import { tenant } from "../plugins/tenant/index.js";
|
|
21
|
+
import { actor } from "../plugins/actor/index.js";
|
|
12
22
|
import { createClient } from "@supabase/supabase-js";
|
|
13
23
|
import { existsSync } from "node:fs";
|
|
14
24
|
import { readFile } from "node:fs/promises";
|
|
@@ -235,6 +245,11 @@ function isPrefetch(response) {
|
|
|
235
245
|
const headers = response.request().headers();
|
|
236
246
|
return headers["next-router-prefetch"] !== void 0 || headers["purpose"] === "prefetch" || (headers["sec-purpose"]?.includes("prefetch") ?? false);
|
|
237
247
|
}
|
|
248
|
+
function sleep$1(ms) {
|
|
249
|
+
return new Promise((done) => {
|
|
250
|
+
setTimeout(done, ms);
|
|
251
|
+
});
|
|
252
|
+
}
|
|
238
253
|
function settle(promise, ms) {
|
|
239
254
|
let timer;
|
|
240
255
|
return Promise.race([promise, new Promise((done) => {
|
|
@@ -243,32 +258,74 @@ function settle(promise, ms) {
|
|
|
243
258
|
clearTimeout(timer);
|
|
244
259
|
});
|
|
245
260
|
}
|
|
246
|
-
|
|
247
|
-
|
|
248
|
-
|
|
249
|
-
|
|
250
|
-
|
|
251
|
-
|
|
261
|
+
function expectResponseBudget(response, expectation) {
|
|
262
|
+
const header = expectation.header ?? "x-bs-db-calls";
|
|
263
|
+
const stats = parseDbStats(response.headers.get(header));
|
|
264
|
+
if (!stats) throw new Error(`expectDbBudget: the response has no ${header} header. Add withDbStats() to the entries.`);
|
|
265
|
+
if (expectation.maxCalls !== void 0 && stats.calls > expectation.maxCalls || expectation.maxWaves !== void 0 && stats.waves > expectation.maxWaves) throw new Error(`expectDbBudget: ${stats.calls} calls in ${stats.waves} waves, over the budget of ${expectation.maxCalls ?? "∞"} calls and ${expectation.maxWaves ?? "∞"} waves`);
|
|
266
|
+
return stats;
|
|
267
|
+
}
|
|
268
|
+
function expectDbBudget(target, expectation) {
|
|
269
|
+
if (target instanceof Response) return expectResponseBudget(target, expectation);
|
|
270
|
+
return expectPageBudget(target, expectation);
|
|
271
|
+
}
|
|
272
|
+
async function expectPageBudget(page, expectation) {
|
|
252
273
|
const seen = [];
|
|
274
|
+
let lastSeen = 0;
|
|
253
275
|
const listener = (response) => {
|
|
254
276
|
const headers = response.headers();
|
|
255
277
|
const id = headers["x-bs-request-id"];
|
|
256
278
|
const stats = headers["x-bs-stats"];
|
|
257
279
|
if (!id || !stats || !expectation.prefetches && isPrefetch(response)) return;
|
|
280
|
+
lastSeen = Date.now();
|
|
258
281
|
seen.push({
|
|
259
282
|
response,
|
|
260
283
|
id,
|
|
261
284
|
url: new URL(stats, response.url()).href
|
|
262
285
|
});
|
|
263
286
|
};
|
|
287
|
+
const settleMs = expectation.settleMs ?? 500;
|
|
288
|
+
const timeoutMs = expectation.timeoutMs ?? 1e4;
|
|
289
|
+
/** Waits until no response has arrived for `settleMs`, at most `timeoutMs`. */
|
|
290
|
+
const quiet = async () => {
|
|
291
|
+
const start = Date.now();
|
|
292
|
+
lastSeen = Math.max(lastSeen, start);
|
|
293
|
+
for (;;) {
|
|
294
|
+
const left = lastSeen + settleMs - Date.now();
|
|
295
|
+
if (left <= 0 || Date.now() - start >= timeoutMs) return;
|
|
296
|
+
await sleep$1(left);
|
|
297
|
+
}
|
|
298
|
+
};
|
|
299
|
+
const failed = /* @__PURE__ */ new WeakSet();
|
|
300
|
+
const aborts = /* @__PURE__ */ new Map();
|
|
301
|
+
const onFailed = (request) => {
|
|
302
|
+
failed.add(request);
|
|
303
|
+
aborts.get(request)?.();
|
|
304
|
+
};
|
|
305
|
+
const ended = (response) => {
|
|
306
|
+
const request = response.request();
|
|
307
|
+
if (failed.has(request)) return Promise.resolve();
|
|
308
|
+
return Promise.race([response.finished(), new Promise((done) => {
|
|
309
|
+
aborts.set(request, done);
|
|
310
|
+
})]);
|
|
311
|
+
};
|
|
264
312
|
page.on("response", listener);
|
|
313
|
+
page.on("requestfailed", onFailed);
|
|
265
314
|
try {
|
|
266
|
-
|
|
315
|
+
try {
|
|
316
|
+
await (expectation.during ? expectation.during() : page.reload());
|
|
317
|
+
await quiet();
|
|
318
|
+
} finally {
|
|
319
|
+
page.off("response", listener);
|
|
320
|
+
}
|
|
321
|
+
if (seen.length === 0) {
|
|
322
|
+
if (expectation.requireRequest === false) return [];
|
|
323
|
+
throw new Error("expectDbBudget: no response carried x-bs-request-id. Enable createNext(betterSupabase, { debug: { enabled: true } }) and run the proxy.");
|
|
324
|
+
}
|
|
325
|
+
await Promise.all(seen.map(({ response }) => settle(ended(response), timeoutMs)));
|
|
267
326
|
} finally {
|
|
268
|
-
page.off("
|
|
327
|
+
page.off("requestfailed", onFailed);
|
|
269
328
|
}
|
|
270
|
-
if (seen.length === 0) throw new Error("expectDbBudget: no response carried x-bs-request-id. Enable createNext(betterSupabase, { debug: { enabled: true } }) and run the proxy.");
|
|
271
|
-
await Promise.all(seen.map(({ response }) => settle(response.finished(), expectation.timeoutMs ?? 1e4)));
|
|
272
329
|
const renders = [];
|
|
273
330
|
for (const { response, id, url } of seen) {
|
|
274
331
|
const reply = await page.request.get(url);
|
|
@@ -287,6 +344,60 @@ async function expectDbBudget(page, expectation) {
|
|
|
287
344
|
return renders;
|
|
288
345
|
}
|
|
289
346
|
//#endregion
|
|
347
|
+
//#region src/testing/instant.ts
|
|
348
|
+
/**
|
|
349
|
+
* `@next/playwright` is an optional peer: loading it on demand keeps it out
|
|
350
|
+
* of apps that never call `expectInstant`, and a missing install returns
|
|
351
|
+
* `undefined` so the caller can say how to add it.
|
|
352
|
+
*/
|
|
353
|
+
async function loadInstant() {
|
|
354
|
+
const specifier = "@next/playwright";
|
|
355
|
+
try {
|
|
356
|
+
return (await import(specifier)).instant;
|
|
357
|
+
} catch {
|
|
358
|
+
return;
|
|
359
|
+
}
|
|
360
|
+
}
|
|
361
|
+
async function assertUnderLock(expectation) {
|
|
362
|
+
const timeout = expectation.timeoutMs ?? 5e3;
|
|
363
|
+
for (const locator of expectation.visible) try {
|
|
364
|
+
await locator.waitFor({
|
|
365
|
+
state: "visible",
|
|
366
|
+
timeout
|
|
367
|
+
});
|
|
368
|
+
} catch (error) {
|
|
369
|
+
throw new Error(`expectInstant: ${String(locator)} was not visible under the instant() lock. It is not in the prefetched UI.`, { cause: error });
|
|
370
|
+
}
|
|
371
|
+
for (const locator of expectation.absent ?? []) {
|
|
372
|
+
const count = await locator.count();
|
|
373
|
+
if (count > 0) throw new Error(`expectInstant: ${String(locator)} matched ${count} elements under the instant() lock. It should stream in after the navigation.`);
|
|
374
|
+
}
|
|
375
|
+
}
|
|
376
|
+
/**
|
|
377
|
+
* Runs `during` inside `@next/playwright`'s `instant()`, checks what the
|
|
378
|
+
* prefetched UI shows while the lock holds, and with `maxCalls` or
|
|
379
|
+
* `maxWaves` checks the navigation's database budget like `expectDbBudget`.
|
|
380
|
+
* Needs a `next build` with the testing API exposed.
|
|
381
|
+
*/
|
|
382
|
+
async function expectInstant(page, expectation) {
|
|
383
|
+
const instant = await loadInstant();
|
|
384
|
+
if (!instant) throw new Error("expectInstant: install @next/playwright as a dev dependency (pnpm add -D @next/playwright).");
|
|
385
|
+
const run = () => instant(page, async () => {
|
|
386
|
+
await expectation.during();
|
|
387
|
+
await assertUnderLock(expectation);
|
|
388
|
+
}, expectation.baseURL === void 0 ? void 0 : { baseURL: expectation.baseURL });
|
|
389
|
+
if (expectation.maxCalls === void 0 && expectation.maxWaves === void 0) {
|
|
390
|
+
await run();
|
|
391
|
+
return [];
|
|
392
|
+
}
|
|
393
|
+
return expectDbBudget(page, {
|
|
394
|
+
during: run,
|
|
395
|
+
...expectation.settleMs === void 0 ? {} : { settleMs: expectation.settleMs },
|
|
396
|
+
...expectation.maxCalls === void 0 ? {} : { maxCalls: expectation.maxCalls },
|
|
397
|
+
...expectation.maxWaves === void 0 ? {} : { maxWaves: expectation.maxWaves }
|
|
398
|
+
});
|
|
399
|
+
}
|
|
400
|
+
//#endregion
|
|
290
401
|
//#region src/testing/conformance.ts
|
|
291
402
|
/** Thrown by a conformance kit when any check fails. Lists every failure, not just the first. */
|
|
292
403
|
var ConformanceError = class extends Error {
|
|
@@ -367,7 +478,8 @@ const hasName = (subject) => ["has a name", () => {
|
|
|
367
478
|
/**
|
|
368
479
|
* Proves an `Executor` meets the contract: rows keyed by the selection's
|
|
369
480
|
* aliases, counts, `aborted` errors, failures returned as `Result`s rather
|
|
370
|
-
* than thrown, and (with `create`) a write round trip
|
|
481
|
+
* than thrown, and (with `create`) a write round trip and a `deleteMany`
|
|
482
|
+
* that `maxAffected` refuses without deleting anything.
|
|
371
483
|
*
|
|
372
484
|
* ```ts
|
|
373
485
|
* it('conforms', () => testExecutor(myExecutor, { betterSupabase, table: 'tags', create: { name: 'x' } }));
|
|
@@ -475,6 +587,32 @@ function testExecutor(executor, options) {
|
|
|
475
587
|
expect(removed.ok, `delete failed: ${JSON.stringify(removed.error)}`);
|
|
476
588
|
const again = await repository["findFirst"]({ where: { [primary]: row[primary] } });
|
|
477
589
|
expect(again.ok && again.data === null, "the deleted row is still readable");
|
|
590
|
+
}],
|
|
591
|
+
create && ["refuses a write past maxAffected and writes nothing", async () => {
|
|
592
|
+
const repository = betterSupabase.connect(executor)[table];
|
|
593
|
+
const [primary] = betterSupabase.meta.tables[table].primaryKey;
|
|
594
|
+
expect(primary, "write checks need a table with a primary key");
|
|
595
|
+
const created = await repository["create"](create);
|
|
596
|
+
expect(created.ok, `create failed: ${JSON.stringify(created.error)}`);
|
|
597
|
+
const id = created.data[primary];
|
|
598
|
+
try {
|
|
599
|
+
const refused = await repository["deleteMany"]({
|
|
600
|
+
where: { [primary]: id },
|
|
601
|
+
maxAffected: 0
|
|
602
|
+
});
|
|
603
|
+
expect(!refused.ok, "deleteMany past maxAffected must fail");
|
|
604
|
+
const kind = refused.error?.kind;
|
|
605
|
+
expect(kind === "max_affected", `expected kind "max_affected", got "${String(kind)}"`);
|
|
606
|
+
const kept = await repository["findFirst"]({ where: { [primary]: id } });
|
|
607
|
+
expect(kept.ok && kept.data !== null, "a refused deleteMany must not delete the row");
|
|
608
|
+
const allowed = await repository["deleteMany"]({
|
|
609
|
+
where: { [primary]: id },
|
|
610
|
+
maxAffected: 1
|
|
611
|
+
});
|
|
612
|
+
expect(allowed.ok, `deleteMany within maxAffected failed: ${JSON.stringify(allowed.error)}`);
|
|
613
|
+
} finally {
|
|
614
|
+
await repository["delete"](id);
|
|
615
|
+
}
|
|
478
616
|
}]
|
|
479
617
|
]);
|
|
480
618
|
}
|
|
@@ -496,7 +634,7 @@ function testCacheAdapter(adapter, options = {}) {
|
|
|
496
634
|
table,
|
|
497
635
|
tables: [table],
|
|
498
636
|
ids: ["1", "a,b"],
|
|
499
|
-
tenant: "
|
|
637
|
+
tenant: "organization_1"
|
|
500
638
|
})],
|
|
501
639
|
["accepts repeated and concurrent calls", async () => {
|
|
502
640
|
await invalidate({
|
|
@@ -534,7 +672,7 @@ function testEventSink(sink, options = {}) {
|
|
|
534
672
|
id: "2",
|
|
535
673
|
name: "Globex"
|
|
536
674
|
}],
|
|
537
|
-
context: { tenant: "
|
|
675
|
+
context: { tenant: "organization_1" }
|
|
538
676
|
}, { source: "/better-supabase/conformance" });
|
|
539
677
|
return conform("EventSink", [
|
|
540
678
|
["accepts an empty batch", () => sink.send(deepFreeze([]))],
|
|
@@ -610,6 +748,19 @@ function testQueueBackend(backend, options) {
|
|
|
610
748
|
expect(await backend.fail(second, "boom", 0) === "dead", "fail at max_attempts must return dead");
|
|
611
749
|
expect(await claimOne() === void 0, "a dead message must not be claimed again");
|
|
612
750
|
}],
|
|
751
|
+
backend.stats && backend.listDead && ["counts and lists dead letters", async () => {
|
|
752
|
+
expect(((await backend.stats?.(queue))?.dead ?? 0) >= 1, "stats must count the dead letter");
|
|
753
|
+
const [dead] = await backend.listDead?.(queue, 10, void 0) ?? [];
|
|
754
|
+
expect(dead, "listDead must return the dead letter");
|
|
755
|
+
expect(same(dead.message?.payload, { n: 3 }), "listDead must return the newest dead letter first");
|
|
756
|
+
if (backend.retryDead) {
|
|
757
|
+
expect(await backend.retryDead(queue, [Number(dead.id)], 10) === 1, "retryDead must enqueue the dead letter again");
|
|
758
|
+
const job = await claimOne();
|
|
759
|
+
expect(job, "the retried job must be claimable");
|
|
760
|
+
expect(same(job.payload, { n: 3 }), "the retried job must keep its payload");
|
|
761
|
+
await backend.complete(job);
|
|
762
|
+
}
|
|
763
|
+
}],
|
|
613
764
|
(options.dedupe ?? backend.leases) && ["returns the first id for a repeated dedupe key", async () => {
|
|
614
765
|
expect(await backend.send(queue, { n: 4 }, 0, 3, "same") === await backend.send(queue, { n: 4 }, 0, 3, "same"), "a waiting dedupe key must return its id");
|
|
615
766
|
const job = await claimOne();
|
|
@@ -722,65 +873,11 @@ function testAuthResolver(resolver, options) {
|
|
|
722
873
|
}]
|
|
723
874
|
]);
|
|
724
875
|
}
|
|
725
|
-
function relativeImport(from, to) {
|
|
726
|
-
const fromParts = from.split("/").slice(0, -1);
|
|
727
|
-
const toParts = to.split("/");
|
|
728
|
-
let common = 0;
|
|
729
|
-
while (common < fromParts.length && fromParts[common] === toParts[common]) common += 1;
|
|
730
|
-
const path = [...Array.from({ length: fromParts.length - common }, () => ".."), ...toParts.slice(common)].join("/");
|
|
731
|
-
return path.startsWith(".") ? path : `./${path}`;
|
|
732
|
-
}
|
|
733
|
-
const EMPTY_INTROSPECTION = {
|
|
734
|
-
version: 1,
|
|
735
|
-
schemas: [],
|
|
736
|
-
tables: [],
|
|
737
|
-
foreignTables: [],
|
|
738
|
-
views: [],
|
|
739
|
-
materializedViews: [],
|
|
740
|
-
columns: [],
|
|
741
|
-
primaryKeys: [],
|
|
742
|
-
relationships: [],
|
|
743
|
-
functions: [],
|
|
744
|
-
types: []
|
|
745
|
-
};
|
|
746
|
-
/** Proves a `Generator` writes relative, unique paths, is deterministic and doesn't mutate its input. */
|
|
747
|
-
function testGenerator(generator, options) {
|
|
748
|
-
const config = options.config ?? resolveConfig({}, "/project");
|
|
749
|
-
const input = () => ({
|
|
750
|
-
meta: frozenCopy(options.meta),
|
|
751
|
-
introspection: frozenCopy(options.introspection ?? EMPTY_INTROSPECTION),
|
|
752
|
-
extras: frozenCopy(options.extras ?? {
|
|
753
|
-
tables: [],
|
|
754
|
-
buckets: [],
|
|
755
|
-
realtime: []
|
|
756
|
-
}),
|
|
757
|
-
config,
|
|
758
|
-
output: `${config.root}/${config.output}`,
|
|
759
|
-
importPath: (from, to) => relativeImport(from, to)
|
|
760
|
-
});
|
|
761
|
-
return conform(`Generator "${generator.name}"`, [
|
|
762
|
-
hasName(generator),
|
|
763
|
-
["writes files inside the project", async () => {
|
|
764
|
-
const files = await generator.generate(input());
|
|
765
|
-
expect(isList(files), "generate() must return an array");
|
|
766
|
-
for (const file of files) {
|
|
767
|
-
expect(typeof file.path === "string" && typeof file.contents === "string", "files need a path and contents");
|
|
768
|
-
expect(!file.path.startsWith("/") && !/^[A-Za-z]:/.test(file.path), `${file.path} must be relative`);
|
|
769
|
-
expect(!file.path.split(/[\\/]/).includes(".."), `${file.path} must stay inside the project`);
|
|
770
|
-
}
|
|
771
|
-
const paths = files.map((file) => file.path);
|
|
772
|
-
expect(new Set(paths).size === paths.length, "paths must be unique");
|
|
773
|
-
}],
|
|
774
|
-
["is deterministic and does not mutate its input", async () => {
|
|
775
|
-
const first = await generator.generate(input());
|
|
776
|
-
const second = await generator.generate(input());
|
|
777
|
-
expect(same(first, second), "two runs on the same input differ");
|
|
778
|
-
}]
|
|
779
|
-
]);
|
|
780
|
-
}
|
|
781
876
|
/**
|
|
782
|
-
* Proves a `Plugin` targets API v1, installs cleanly
|
|
783
|
-
*
|
|
877
|
+
* Proves a `Plugin` targets API v1, installs cleanly on its own and next to
|
|
878
|
+
* the first-party plugins in either order, adds repository methods without
|
|
879
|
+
* replacing base ones, keeps `context`, `transformQuery` and
|
|
880
|
+
* `beforeMutation` pure and deterministic, and keeps executors'
|
|
784
881
|
* results intact when it wraps them.
|
|
785
882
|
*/
|
|
786
883
|
function testPlugin(plugin, options) {
|
|
@@ -804,6 +901,41 @@ function testPlugin(plugin, options) {
|
|
|
804
901
|
const db = betterSupabase.use(plugin).connect(recorder([]), context);
|
|
805
902
|
for (const key of Object.keys(betterSupabase.meta.tables)) expect(db[key], `db.${key} is missing`);
|
|
806
903
|
}],
|
|
904
|
+
["declares a known hook order", () => {
|
|
905
|
+
const enforce = plugin.enforce;
|
|
906
|
+
expect(enforce === void 0 || enforce === "first" || enforce === "pre" || enforce === "post", `enforce is ${String(enforce)}; use "first", "pre", "post" or leave it out`);
|
|
907
|
+
}],
|
|
908
|
+
["installs next to the first-party plugins in either order", () => {
|
|
909
|
+
const others = [
|
|
910
|
+
timestamps(),
|
|
911
|
+
softDelete(),
|
|
912
|
+
tenant(),
|
|
913
|
+
actor()
|
|
914
|
+
].filter((other) => other.name !== plugin.name);
|
|
915
|
+
let before = betterSupabase;
|
|
916
|
+
for (const other of others) before = before.use(other);
|
|
917
|
+
let after = betterSupabase.use(plugin);
|
|
918
|
+
for (const other of others) after = after.use(other);
|
|
919
|
+
for (const installed of [before.use(plugin), after]) {
|
|
920
|
+
const db = installed.connect(recorder([]), context);
|
|
921
|
+
for (const key of Object.keys(betterSupabase.meta.tables)) expect(db[key], `db.${key} is missing`);
|
|
922
|
+
}
|
|
923
|
+
}],
|
|
924
|
+
plugin.repository && ["repository adds functions and keeps the base methods", () => {
|
|
925
|
+
const db = betterSupabase.connect(recorder([]), context);
|
|
926
|
+
for (const [key, table] of Object.entries(betterSupabase.meta.tables)) {
|
|
927
|
+
const base = db[key];
|
|
928
|
+
const added = plugin.repository({
|
|
929
|
+
table,
|
|
930
|
+
base
|
|
931
|
+
});
|
|
932
|
+
if (added === void 0) continue;
|
|
933
|
+
for (const [name, method] of Object.entries(added)) {
|
|
934
|
+
expect(typeof method === "function", `${key}.${name} is not a function`);
|
|
935
|
+
expect(!(name in base), `${key}.${name} replaces a base method`);
|
|
936
|
+
}
|
|
937
|
+
}
|
|
938
|
+
}],
|
|
807
939
|
plugin.transformQuery && ["transformQuery is pure and deterministic", async () => {
|
|
808
940
|
const op = await captureOp(betterSupabase, (db) => db[table]["findMany"]({ limit: 1 }));
|
|
809
941
|
const first = plugin.transformQuery(frozenCopy(op), hook(op));
|
|
@@ -848,6 +980,13 @@ function testPlugin(plugin, options) {
|
|
|
848
980
|
const result = await wrapped.execute(op, { errorMappers: [] });
|
|
849
981
|
expect(result.ok && same(result.data, data), "the wrapped executor changed the result");
|
|
850
982
|
}],
|
|
983
|
+
plugin.context && ["context is pure and deterministic", () => {
|
|
984
|
+
const args = { schema: betterSupabase.meta };
|
|
985
|
+
const first = plugin.context(frozenCopy(context), args);
|
|
986
|
+
const second = plugin.context(frozenCopy(context), args);
|
|
987
|
+
expect(typeof first === "object" && first !== null, "must return a context object");
|
|
988
|
+
expect(same(first, second), "two calls with the same input differ");
|
|
989
|
+
}],
|
|
851
990
|
plugin.mapError && ["mapError returns a DbError or undefined", () => {
|
|
852
991
|
const fallback = dbError("unexpected", "internal error");
|
|
853
992
|
const mapped = plugin.mapError({
|
|
@@ -1211,6 +1350,471 @@ function defineSeed(betterSupabase, fixtures) {
|
|
|
1211
1350
|
};
|
|
1212
1351
|
}
|
|
1213
1352
|
//#endregion
|
|
1353
|
+
//#region src/testing/adapter.ts
|
|
1354
|
+
const MODULE_SERVER = {
|
|
1355
|
+
env: {
|
|
1356
|
+
url: "http://127.0.0.1:54321",
|
|
1357
|
+
publishableKey: "sb_publishable_kit",
|
|
1358
|
+
jwksUrl: new URL("http://127.0.0.1:54321/auth/v1/.well-known/jwks.json")
|
|
1359
|
+
},
|
|
1360
|
+
readUrl: "http://127.0.0.1:54329",
|
|
1361
|
+
prefetchJwks: false,
|
|
1362
|
+
exposeErrors: false
|
|
1363
|
+
};
|
|
1364
|
+
/**
|
|
1365
|
+
* Proves a framework adapter refuses callers its guard rejects, answers
|
|
1366
|
+
* `DbError`s with their status, hides unexpected errors, applies the
|
|
1367
|
+
* context's cookies and hands pending event sends to `waitUntil`, the way
|
|
1368
|
+
* `handle()` from `better-supabase/server` does.
|
|
1369
|
+
*/
|
|
1370
|
+
function testAdapter(name, options) {
|
|
1371
|
+
const { events } = options.betterSupabase;
|
|
1372
|
+
const call = async (run, allow = ["anon"], waitUntil = () => void 0) => options.serve(MODULE_SERVER, run, {
|
|
1373
|
+
allow,
|
|
1374
|
+
waitUntil
|
|
1375
|
+
})(new Request("http://127.0.0.1/module"));
|
|
1376
|
+
const errorStatus = (expected) => options.errorsInBody ? 200 : expected;
|
|
1377
|
+
return conform(`Adapter "${name}"`, [
|
|
1378
|
+
["refuses a caller the guard rejects", async () => {
|
|
1379
|
+
let ran = false;
|
|
1380
|
+
const response = await call(() => {
|
|
1381
|
+
ran = true;
|
|
1382
|
+
}, ["user"]);
|
|
1383
|
+
expect(response.status === 401, `expected 401 for an anonymous caller, got ${response.status}`);
|
|
1384
|
+
expect(!ran, "the handler ran for a refused caller");
|
|
1385
|
+
}],
|
|
1386
|
+
["runs the handler for an allowed caller", async () => {
|
|
1387
|
+
const response = await call(() => ({ module: "ran" }));
|
|
1388
|
+
expect(response.status === 200, `expected 200, got ${response.status}`);
|
|
1389
|
+
const body = await response.text();
|
|
1390
|
+
expect(body.includes("ran"), `the handler's data is missing: ${body}`);
|
|
1391
|
+
}],
|
|
1392
|
+
["answers a DbError with its status", async () => {
|
|
1393
|
+
const response = await call(() => {
|
|
1394
|
+
throw new DbException(dbError("not_found", "module: no such row"));
|
|
1395
|
+
});
|
|
1396
|
+
expect(response.status === errorStatus(404), `expected ${errorStatus(404)}, got ${response.status}`);
|
|
1397
|
+
const body = await response.text();
|
|
1398
|
+
expect(body.includes("module: no such row"), `the error message is missing: ${body}`);
|
|
1399
|
+
}],
|
|
1400
|
+
["hides the message of an unexpected error", async () => {
|
|
1401
|
+
const response = await call(() => {
|
|
1402
|
+
throw new Error("module-internal-detail");
|
|
1403
|
+
});
|
|
1404
|
+
expect(response.status === errorStatus(500), `expected ${errorStatus(500)}, got ${response.status}`);
|
|
1405
|
+
expect(!(await response.text()).includes("module-internal-detail"), "the response leaks the error message");
|
|
1406
|
+
}],
|
|
1407
|
+
options.cookies !== false && ["applies the context's cookies", async () => {
|
|
1408
|
+
let replica = false;
|
|
1409
|
+
const response = await call((ctx) => {
|
|
1410
|
+
replica = ctx.replica !== void 0;
|
|
1411
|
+
ctx.replica?.pin();
|
|
1412
|
+
return { pinned: true };
|
|
1413
|
+
});
|
|
1414
|
+
expect(replica, "ctx.replica is unset: pass `server` to the adapter");
|
|
1415
|
+
expect(response.headers.getSetCookie().some((cookie) => cookie.startsWith(`bs-primary-until=`)), `a write must send bs-primary-until; apply ctx.apply() or ctx.cookies()`);
|
|
1416
|
+
}],
|
|
1417
|
+
["hands pending event sends to waitUntil", async () => {
|
|
1418
|
+
const { promise: send, resolve } = Promise.withResolvers();
|
|
1419
|
+
const waited = [];
|
|
1420
|
+
await call(() => {
|
|
1421
|
+
events.track(send);
|
|
1422
|
+
return { sent: true };
|
|
1423
|
+
}, ["anon"], (promise) => {
|
|
1424
|
+
waited.push(promise);
|
|
1425
|
+
});
|
|
1426
|
+
resolve();
|
|
1427
|
+
expect(waited.length > 0, "waitUntil was not called while an event send was pending");
|
|
1428
|
+
await Promise.all(waited);
|
|
1429
|
+
}]
|
|
1430
|
+
]);
|
|
1431
|
+
}
|
|
1432
|
+
//#endregion
|
|
1433
|
+
//#region src/testing/transport-middleware.ts
|
|
1434
|
+
const REQUEST = {
|
|
1435
|
+
schema: "better_supabase",
|
|
1436
|
+
fn: "list_my_organizations",
|
|
1437
|
+
args: {
|
|
1438
|
+
organization: "organization_1",
|
|
1439
|
+
filters: { status: "open" }
|
|
1440
|
+
}
|
|
1441
|
+
};
|
|
1442
|
+
/**
|
|
1443
|
+
* Proves a `BlockTransportMiddleware` meets the contract: it declares API 1
|
|
1444
|
+
* and a name, calls `next` at most once per call, leaves the request it got
|
|
1445
|
+
* unchanged, resolves when `next` does, and rejects with the database error
|
|
1446
|
+
* `next` rejected with, so blocks still map it to a `DbError`.
|
|
1447
|
+
*
|
|
1448
|
+
* ```ts
|
|
1449
|
+
* it('conforms', () => testBlockTransportMiddleware(statementTimeout('5s')));
|
|
1450
|
+
* ```
|
|
1451
|
+
*/
|
|
1452
|
+
function testBlockTransportMiddleware(middleware) {
|
|
1453
|
+
return conform(`BlockTransportMiddleware "${middleware.name}"`, [
|
|
1454
|
+
hasName(middleware),
|
|
1455
|
+
["declares apiVersion 1", () => {
|
|
1456
|
+
const version = middleware.apiVersion;
|
|
1457
|
+
expect(version === 1, "apiVersion must be 1");
|
|
1458
|
+
}],
|
|
1459
|
+
["calls next at most once and resolves with a value", async () => {
|
|
1460
|
+
let calls = 0;
|
|
1461
|
+
await middleware.call(frozenCopy(REQUEST), (request) => {
|
|
1462
|
+
calls += 1;
|
|
1463
|
+
expect(typeof request.fn === "string" && typeof request.schema === "string", "next got a request without a schema and function");
|
|
1464
|
+
return Promise.resolve([{ id: "organization_1" }]);
|
|
1465
|
+
});
|
|
1466
|
+
expect(calls <= 1, `next was called ${String(calls)} times`);
|
|
1467
|
+
}],
|
|
1468
|
+
["keeps the database error next rejects with", async () => {
|
|
1469
|
+
const denied = Object.assign(/* @__PURE__ */ new Error("permission denied"), {
|
|
1470
|
+
code: "42501",
|
|
1471
|
+
hint: "ORGANIZATION_FORBIDDEN"
|
|
1472
|
+
});
|
|
1473
|
+
let rejected;
|
|
1474
|
+
try {
|
|
1475
|
+
await middleware.call(frozenCopy(REQUEST), () => Promise.reject(denied));
|
|
1476
|
+
} catch (cause) {
|
|
1477
|
+
rejected = cause;
|
|
1478
|
+
}
|
|
1479
|
+
expect(rejected !== void 0, "resolved although next rejected");
|
|
1480
|
+
const raw = rawError(rejected);
|
|
1481
|
+
expect(raw?.code === "42501" && raw.hint === "ORGANIZATION_FORBIDDEN", "the rejection lost the database error's code or hint");
|
|
1482
|
+
}]
|
|
1483
|
+
]);
|
|
1484
|
+
}
|
|
1485
|
+
//#endregion
|
|
1486
|
+
//#region src/testing/authorization-provider.ts
|
|
1487
|
+
function isPlainJson(value) {
|
|
1488
|
+
if (value === null || typeof value === "string" || typeof value === "boolean") return true;
|
|
1489
|
+
if (typeof value === "number") return Number.isFinite(value);
|
|
1490
|
+
if (Array.isArray(value)) return value.every(isPlainJson);
|
|
1491
|
+
if (typeof value !== "object") return false;
|
|
1492
|
+
const prototype = Object.getPrototypeOf(value);
|
|
1493
|
+
return (prototype === Object.prototype || prototype === null) && Object.values(value).every((entry) => entry === void 0 || isPlainJson(entry));
|
|
1494
|
+
}
|
|
1495
|
+
const QUALIFIED = /^[a-z_][\w$]*\.[a-z_][\w${}]*$/i;
|
|
1496
|
+
const TABLE = /^[a-z_][\w$]*\.[a-z_][\w$]*$/i;
|
|
1497
|
+
const COLUMN = /^[a-z_][\w$]*\.[a-z_][\w$]*\.[a-z_][\w$]*$/i;
|
|
1498
|
+
const IDENTIFIER = /^[a-z_][\w$]*$/i;
|
|
1499
|
+
function expectRow(where, row) {
|
|
1500
|
+
expect(TABLE.test(row.table), `${where}.table "${row.table}" is not schema.table`);
|
|
1501
|
+
expect(IDENTIFIER.test(row.id), `${where}.id "${row.id}" is not a column`);
|
|
1502
|
+
expect(row.disabledAt !== void 0 || row.status !== void 0, `${where} sets neither disabledAt nor status`);
|
|
1503
|
+
for (const column of [row.disabledAt, row.status]) if (column !== void 0) expect(IDENTIFIER.test(column), `${where} column "${column}" is not a column`);
|
|
1504
|
+
expect(row.status === void 0 === (row.active === void 0), `${where} needs status and active together`);
|
|
1505
|
+
}
|
|
1506
|
+
/**
|
|
1507
|
+
* Proves an `AuthorizationProvider` targets API v1, is plain JSON, names
|
|
1508
|
+
* valid scopes with a tenant scope that has an id type, writes templates
|
|
1509
|
+
* that are safe to inline and use the placeholders they need, lists every
|
|
1510
|
+
* function its templates call in `requires`, and keeps its token hook,
|
|
1511
|
+
* suspension rows, role sources and deciding columns well formed.
|
|
1512
|
+
*/
|
|
1513
|
+
function testAuthorizationProvider(provider) {
|
|
1514
|
+
const scopes = new Set(provider.scopes.map((scope) => scope.name));
|
|
1515
|
+
const templates = Object.entries(provider.functions).filter((entry) => typeof entry[1] === "string");
|
|
1516
|
+
const calls = templates.some(([, template]) => (template.includes("{scope}") ? [...scopes] : [void 0]).some((scope) => templateFunctions(template, scope).length > 0));
|
|
1517
|
+
return conform(`AuthorizationProvider "${provider.name}"`, [
|
|
1518
|
+
hasName(provider),
|
|
1519
|
+
["targets authorization API v1", () => {
|
|
1520
|
+
expect(provider.apiVersion === 1, `apiVersion is ${String(provider.apiVersion)}`);
|
|
1521
|
+
}],
|
|
1522
|
+
["is plain JSON", () => {
|
|
1523
|
+
expect(isPlainJson(provider), "it doesn't survive a JSON round trip");
|
|
1524
|
+
}],
|
|
1525
|
+
["names valid, unique scopes", () => {
|
|
1526
|
+
expect(scopes.size > 0, "scopes is empty");
|
|
1527
|
+
expect(scopes.size === provider.scopes.length, "a scope name repeats");
|
|
1528
|
+
for (const scope of provider.scopes) expect(isScopeName(scope.name), `scope "${scope.name}" is not lower snake case`);
|
|
1529
|
+
const problems = scopeProblems(provider);
|
|
1530
|
+
expect(problems.length === 0, problems.join("; "));
|
|
1531
|
+
}],
|
|
1532
|
+
["templates use only their placeholders", () => {
|
|
1533
|
+
for (const [name, template] of templates) {
|
|
1534
|
+
const allowed = TEMPLATE_PLACEHOLDERS[name];
|
|
1535
|
+
expect(allowed, `functions.${name} is not a known template`);
|
|
1536
|
+
for (const placeholder of templatePlaceholders(template)) expect(allowed.includes(placeholder), `functions.${name} uses {${placeholder}}; it takes ${allowed.map((key) => `{${key}}`).join(", ")}`);
|
|
1537
|
+
const needed = allowed.filter((key) => key === "permission" || key === "user" || key === "tenant" || key === "role" || key === "scope" && scopes.size > 1 && name.startsWith("idsWith"));
|
|
1538
|
+
for (const key of needed) expect(template.includes(`{${key}}`), key === "scope" ? `functions.${name} doesn't use {scope}, so it answers the same for every scope` : `functions.${name} doesn't use {${key}}`);
|
|
1539
|
+
}
|
|
1540
|
+
}],
|
|
1541
|
+
["templates are safe to inline", () => {
|
|
1542
|
+
for (const [name, template] of templates) {
|
|
1543
|
+
const problems = templateProblems(template);
|
|
1544
|
+
expect(problems.length === 0, `functions.${name} ${problems.join(", ")}`);
|
|
1545
|
+
}
|
|
1546
|
+
}],
|
|
1547
|
+
["requires every function the templates call", () => {
|
|
1548
|
+
expect(!calls || provider.requires !== void 0, "the templates call functions, but requires is unset");
|
|
1549
|
+
const required = new Set((provider.requires ?? []).map((entry) => entry.function));
|
|
1550
|
+
for (const entry of provider.requires ?? []) expect(QUALIFIED.test(entry.function), `requires "${entry.function}" is not schema.name`);
|
|
1551
|
+
for (const [name, template] of templates) for (const scope of template.includes("{scope}") ? scopes : [void 0]) for (const fn of templateFunctions(template, scope)) expect(required.has(fn), `functions.${name} calls ${fn}, which requires doesn't list`);
|
|
1552
|
+
}],
|
|
1553
|
+
provider.permissions && ["lists each permission once, at known scopes", () => {
|
|
1554
|
+
const keys = /* @__PURE__ */ new Set();
|
|
1555
|
+
for (const permission of provider.permissions) {
|
|
1556
|
+
expect(permission.key !== "", "a permission key is empty");
|
|
1557
|
+
expect(!keys.has(permission.key), `permission "${permission.key}" repeats`);
|
|
1558
|
+
keys.add(permission.key);
|
|
1559
|
+
for (const scope of permission.scopes ?? []) expect(scopes.has(scope), `permission "${permission.key}" names unknown scope "${scope}"`);
|
|
1560
|
+
}
|
|
1561
|
+
}],
|
|
1562
|
+
provider.memberships && ["memberships name known scopes", () => {
|
|
1563
|
+
for (const membership of provider.memberships) {
|
|
1564
|
+
expect(TABLE.test(membership.table), `membership table "${membership.table}" is not schema.table`);
|
|
1565
|
+
if ("value" in membership.scope) expect(scopes.has(membership.scope.value), `${membership.table} names unknown scope "${membership.scope.value}"`);
|
|
1566
|
+
}
|
|
1567
|
+
}],
|
|
1568
|
+
provider.suspension && ["suspension rows name a table, an id and a state column", () => {
|
|
1569
|
+
const { user, tenant } = provider.suspension;
|
|
1570
|
+
if (user) expectRow("suspension.user", user);
|
|
1571
|
+
if (tenant) expectRow("suspension.tenant", tenant);
|
|
1572
|
+
}],
|
|
1573
|
+
provider.roleSources && ["role sources name schema.table and columns", () => {
|
|
1574
|
+
for (const source of provider.roleSources) {
|
|
1575
|
+
expect(TABLE.test(source.table), `roleSources table "${source.table}" is not schema.table`);
|
|
1576
|
+
expect(TABLE.test(source.role.through.table), `roleSources ${source.table} reads roles from "${source.role.through.table}", which is not schema.table`);
|
|
1577
|
+
for (const column of [
|
|
1578
|
+
source.role.column,
|
|
1579
|
+
source.role.through.id,
|
|
1580
|
+
source.role.through.column
|
|
1581
|
+
]) expect(IDENTIFIER.test(column), `roleSources ${source.table} names "${column}", which is not a column`);
|
|
1582
|
+
}
|
|
1583
|
+
}],
|
|
1584
|
+
provider.decidingColumns && ["deciding columns are schema.table.column", () => {
|
|
1585
|
+
for (const column of provider.decidingColumns) expect(COLUMN.test(column), `decidingColumns "${column}" is not schema.table.column`);
|
|
1586
|
+
}],
|
|
1587
|
+
provider.tokenHook && ["names its token hook and the claims it writes", () => {
|
|
1588
|
+
const hook = provider.tokenHook;
|
|
1589
|
+
expect(QUALIFIED.test(hook.function), `tokenHook.function "${hook.function}" is not schema.name`);
|
|
1590
|
+
expect(hook.ownedClaims.length > 0, "tokenHook.ownedClaims is empty");
|
|
1591
|
+
const owned = new Set(hook.ownedClaims);
|
|
1592
|
+
const written = new Set(owned);
|
|
1593
|
+
for (const claim of hook.registeredClaims ?? []) {
|
|
1594
|
+
expect(QUALIFIED.test(claim.function), `tokenHook claim "${claim.name}" calls "${claim.function}", which is not schema.name`);
|
|
1595
|
+
expect(!owned.has(claim.name), `tokenHook registers "${claim.name}", which ownedClaims also lists: a claim the hook owns has no second writer`);
|
|
1596
|
+
written.add(claim.name);
|
|
1597
|
+
}
|
|
1598
|
+
if (hook.budget !== void 0) {
|
|
1599
|
+
expect(Number.isInteger(hook.budget.bytes) && hook.budget.bytes > 0, "tokenHook.budget.bytes must be a positive integer");
|
|
1600
|
+
for (const claim of hook.budget.claims) expect(written.has(claim), `tokenHook.budget counts "${claim}", which neither ownedClaims nor registeredClaims lists`);
|
|
1601
|
+
}
|
|
1602
|
+
}]
|
|
1603
|
+
]);
|
|
1604
|
+
}
|
|
1605
|
+
//#endregion
|
|
1606
|
+
//#region src/testing/generator.ts
|
|
1607
|
+
function relativeImport(from, to) {
|
|
1608
|
+
const fromParts = from.split("/").slice(0, -1);
|
|
1609
|
+
const toParts = to.split("/");
|
|
1610
|
+
let common = 0;
|
|
1611
|
+
while (common < fromParts.length && fromParts[common] === toParts[common]) common += 1;
|
|
1612
|
+
const path = [...Array.from({ length: fromParts.length - common }, () => ".."), ...toParts.slice(common)].join("/");
|
|
1613
|
+
return path.startsWith(".") ? path : `./${path}`;
|
|
1614
|
+
}
|
|
1615
|
+
/** A `GeneratorModel` from schema metadata, for generators tested without a database. */
|
|
1616
|
+
function modelFromMeta(meta) {
|
|
1617
|
+
return {
|
|
1618
|
+
tables: Object.values(meta.tables).map((table) => ({
|
|
1619
|
+
key: table.key,
|
|
1620
|
+
schema: table.schema,
|
|
1621
|
+
name: table.name,
|
|
1622
|
+
casing: meta.casing,
|
|
1623
|
+
columns: Object.entries(table.columns).map(([app, column]) => ({
|
|
1624
|
+
app,
|
|
1625
|
+
db: column.db,
|
|
1626
|
+
tsType: column.enum ? column.enum.map((value) => JSON.stringify(value)).join(" | ") : "unknown",
|
|
1627
|
+
nullable: column.nullable,
|
|
1628
|
+
optional: column.nullable || column.hasDefault || column.generated === true,
|
|
1629
|
+
readonly: column.generated === true,
|
|
1630
|
+
values: column.enum,
|
|
1631
|
+
json: column.json === true
|
|
1632
|
+
}))
|
|
1633
|
+
})),
|
|
1634
|
+
enums: Object.entries(meta.enums).map(([name, values]) => ({
|
|
1635
|
+
schema: "public",
|
|
1636
|
+
name,
|
|
1637
|
+
values
|
|
1638
|
+
}))
|
|
1639
|
+
};
|
|
1640
|
+
}
|
|
1641
|
+
const EMPTY_INTROSPECTION = {
|
|
1642
|
+
version: 1,
|
|
1643
|
+
schemas: [],
|
|
1644
|
+
tables: [],
|
|
1645
|
+
foreignTables: [],
|
|
1646
|
+
views: [],
|
|
1647
|
+
materializedViews: [],
|
|
1648
|
+
columns: [],
|
|
1649
|
+
primaryKeys: [],
|
|
1650
|
+
relationships: [],
|
|
1651
|
+
functions: [],
|
|
1652
|
+
types: []
|
|
1653
|
+
};
|
|
1654
|
+
/** Proves a `Generator` writes relative, unique paths, is deterministic and doesn't mutate its input. */
|
|
1655
|
+
function testGenerator(generator, options) {
|
|
1656
|
+
const config = options.config ?? resolveConfig({}, "/project");
|
|
1657
|
+
const input = () => ({
|
|
1658
|
+
meta: frozenCopy(options.meta),
|
|
1659
|
+
introspection: frozenCopy(options.introspection ?? EMPTY_INTROSPECTION),
|
|
1660
|
+
extras: frozenCopy(options.extras ?? {
|
|
1661
|
+
tables: [],
|
|
1662
|
+
buckets: [],
|
|
1663
|
+
realtime: []
|
|
1664
|
+
}),
|
|
1665
|
+
config,
|
|
1666
|
+
output: `${config.root}/${config.output}`,
|
|
1667
|
+
importPath: (from, to) => relativeImport(from, to),
|
|
1668
|
+
model: frozenCopy(options.model ?? modelFromMeta(options.meta))
|
|
1669
|
+
});
|
|
1670
|
+
return conform(`Generator "${generator.name}"`, [
|
|
1671
|
+
hasName(generator),
|
|
1672
|
+
["targets generator API 1", () => {
|
|
1673
|
+
const version = generator.apiVersion;
|
|
1674
|
+
expect(version === void 0 || version === 1, `apiVersion is ${String(version)}; this better-supabase supports 1`);
|
|
1675
|
+
}],
|
|
1676
|
+
["writes files inside the project", async () => {
|
|
1677
|
+
const files = await generator.generate(input());
|
|
1678
|
+
expect(isList(files), "generate() must return an array");
|
|
1679
|
+
for (const file of files) {
|
|
1680
|
+
expect(typeof file.path === "string" && typeof file.contents === "string", "files need a path and contents");
|
|
1681
|
+
expect(!file.path.startsWith("/") && !/^[A-Za-z]:/.test(file.path), `${file.path} must be relative`);
|
|
1682
|
+
expect(!file.path.split(/[\\/]/).includes(".."), `${file.path} must stay inside the project`);
|
|
1683
|
+
}
|
|
1684
|
+
const paths = files.map((file) => file.path);
|
|
1685
|
+
expect(new Set(paths).size === paths.length, "paths must be unique");
|
|
1686
|
+
}],
|
|
1687
|
+
["is deterministic and does not mutate its input", async () => {
|
|
1688
|
+
const first = await generator.generate(input());
|
|
1689
|
+
const second = await generator.generate(input());
|
|
1690
|
+
expect(same(first, second), "two runs on the same input differ");
|
|
1691
|
+
}]
|
|
1692
|
+
]);
|
|
1693
|
+
}
|
|
1694
|
+
//#endregion
|
|
1695
|
+
//#region src/testing/injectables.ts
|
|
1696
|
+
const hasApiVersion = (subject) => ["has apiVersion 1", () => {
|
|
1697
|
+
const version = subject.apiVersion;
|
|
1698
|
+
expect(version === 1, `apiVersion is ${String(version)}, not 1`);
|
|
1699
|
+
}];
|
|
1700
|
+
const rejects = async (run) => {
|
|
1701
|
+
try {
|
|
1702
|
+
await run();
|
|
1703
|
+
return false;
|
|
1704
|
+
} catch {
|
|
1705
|
+
return true;
|
|
1706
|
+
}
|
|
1707
|
+
};
|
|
1708
|
+
/**
|
|
1709
|
+
* Proves an `Embedder` for the knowledge and memory blocks names its model,
|
|
1710
|
+
* embeds an empty list to nothing, and returns one finite vector per value,
|
|
1711
|
+
* all of the same length on every call. It calls the real model.
|
|
1712
|
+
*/
|
|
1713
|
+
function testEmbedder(embedder, options = {}) {
|
|
1714
|
+
const values = options.values ?? ["The first text.", "A second text."];
|
|
1715
|
+
return conform(`Embedder "${embedder.model}"`, [
|
|
1716
|
+
hasApiVersion(embedder),
|
|
1717
|
+
["names its model", () => {
|
|
1718
|
+
expect(typeof embedder.model === "string" && embedder.model !== "", "model must be a non-empty string");
|
|
1719
|
+
}],
|
|
1720
|
+
["embeds nothing to nothing", async () => {
|
|
1721
|
+
const vectors = await embedder.embed([]);
|
|
1722
|
+
expect(vectors.length === 0, `got ${vectors.length} vectors`);
|
|
1723
|
+
}],
|
|
1724
|
+
["returns one finite vector per value, of one length", async () => {
|
|
1725
|
+
const first = await embedder.embed(values);
|
|
1726
|
+
expect(first.length === values.length, `got ${first.length} vectors for ${values.length} values`);
|
|
1727
|
+
const size = first[0]?.length ?? 0;
|
|
1728
|
+
expect(size > 0, "vectors must not be empty");
|
|
1729
|
+
expect(first.every((vector) => vector.length === size && vector.every(Number.isFinite)), "vectors must share one length and hold finite numbers");
|
|
1730
|
+
const [again] = await embedder.embed(values.slice(0, 1));
|
|
1731
|
+
expect(again?.length === size, "a second call changed the length");
|
|
1732
|
+
}]
|
|
1733
|
+
]);
|
|
1734
|
+
}
|
|
1735
|
+
/**
|
|
1736
|
+
* Proves an `AiTaskRunner` resolves with nothing or a chat id and stops when
|
|
1737
|
+
* its signal is already aborted. It runs `options.task` for real.
|
|
1738
|
+
*/
|
|
1739
|
+
function testAiTaskRunner(runner, options) {
|
|
1740
|
+
const { task, run } = options;
|
|
1741
|
+
return conform("AiTaskRunner", [
|
|
1742
|
+
hasApiVersion(runner),
|
|
1743
|
+
["resolves with nothing or a chat id", async () => {
|
|
1744
|
+
const result = await runner(task, run, new AbortController().signal);
|
|
1745
|
+
expect(result === void 0 || typeof result === "object" && result !== null && (!("chatId" in result) || result.chatId === void 0 || typeof result.chatId === "string"), "must resolve with undefined or { chatId?: string }");
|
|
1746
|
+
}],
|
|
1747
|
+
["stops on an aborted signal", async () => {
|
|
1748
|
+
expect(await rejects(() => runner(task, run, AbortSignal.abort())), "resolved although the signal was aborted");
|
|
1749
|
+
}]
|
|
1750
|
+
]);
|
|
1751
|
+
}
|
|
1752
|
+
/**
|
|
1753
|
+
* Proves a `GraphCompiler` returns a JSON value (stored with the published
|
|
1754
|
+
* version), compiles the same graph to the same value and leaves the graph
|
|
1755
|
+
* unchanged.
|
|
1756
|
+
*/
|
|
1757
|
+
function testGraphCompiler(compile, options) {
|
|
1758
|
+
const { graph } = options;
|
|
1759
|
+
return conform("GraphCompiler", [
|
|
1760
|
+
hasApiVersion(compile),
|
|
1761
|
+
["returns a JSON value", async () => {
|
|
1762
|
+
const compiled = await compile(graph);
|
|
1763
|
+
if (compiled === void 0) return;
|
|
1764
|
+
const json = JSON.stringify(compiled);
|
|
1765
|
+
expect(same(JSON.parse(json), compiled), "the compiled form must survive a JSON round trip");
|
|
1766
|
+
}],
|
|
1767
|
+
["compiles one graph to one value, without changing it", async () => {
|
|
1768
|
+
const before = structuredClone(graph);
|
|
1769
|
+
const first = await compile(graph);
|
|
1770
|
+
const second = await compile(graph);
|
|
1771
|
+
expect(same(first, second), "two compiles of one graph differ");
|
|
1772
|
+
expect(same(graph, before), "compile changed the graph");
|
|
1773
|
+
}]
|
|
1774
|
+
]);
|
|
1775
|
+
}
|
|
1776
|
+
/**
|
|
1777
|
+
* Proves a `BuilderStarter` returns a run id and returns the same id when
|
|
1778
|
+
* the same idempotency key starts again. It starts real runs.
|
|
1779
|
+
*/
|
|
1780
|
+
function testBuilderStarter(start, options) {
|
|
1781
|
+
const { call } = options;
|
|
1782
|
+
return conform("BuilderStarter", [hasApiVersion(start), ["returns one run id per idempotency key", async () => {
|
|
1783
|
+
const first = await start(call);
|
|
1784
|
+
expect(typeof first === "string" && first !== "", "must resolve with a non-empty run id");
|
|
1785
|
+
expect(await start(call) === first, "a repeated idempotency key started a new run");
|
|
1786
|
+
}]]);
|
|
1787
|
+
}
|
|
1788
|
+
/**
|
|
1789
|
+
* Proves an `EveDocumentBackend` reads a missing document as `null`, creates
|
|
1790
|
+
* one with `expectedVersion: null`, refuses a stale version and moves the
|
|
1791
|
+
* version on every write. It writes `options.key` on the real store.
|
|
1792
|
+
*/
|
|
1793
|
+
function testEveDocumentBackend(backend, options = {}) {
|
|
1794
|
+
const key = options.key ?? `conformance/${crypto.randomUUID()}`;
|
|
1795
|
+
return conform("EveDocumentBackend", [hasApiVersion(backend), ["creates, reads and updates a document by version", async () => {
|
|
1796
|
+
expect(await backend.read({ key }) === null, "a missing document must read as null");
|
|
1797
|
+
const created = await backend.write({
|
|
1798
|
+
key,
|
|
1799
|
+
content: "one",
|
|
1800
|
+
expectedVersion: null
|
|
1801
|
+
});
|
|
1802
|
+
const read = await backend.read({ key });
|
|
1803
|
+
expect(read?.content === "one" && read.version === created.version, "read must return the written content and version");
|
|
1804
|
+
expect((await backend.write({
|
|
1805
|
+
key,
|
|
1806
|
+
content: "two",
|
|
1807
|
+
expectedVersion: created.version
|
|
1808
|
+
})).version !== created.version, "a write must change the version");
|
|
1809
|
+
expect(await rejects(() => backend.write({
|
|
1810
|
+
key,
|
|
1811
|
+
content: "three",
|
|
1812
|
+
expectedVersion: created.version
|
|
1813
|
+
})), "a write with a stale version must throw");
|
|
1814
|
+
expect((await backend.read({ key }))?.content === "two", "a refused write changed the document");
|
|
1815
|
+
}]]);
|
|
1816
|
+
}
|
|
1817
|
+
//#endregion
|
|
1214
1818
|
//#region src/testing/notification-channel.ts
|
|
1215
1819
|
const STATUSES = /* @__PURE__ */ new Set(["sent", "skipped"]);
|
|
1216
1820
|
function sample(channel, email) {
|
|
@@ -1383,4 +1987,367 @@ function testWebhookSecretStore(store, options) {
|
|
|
1383
1987
|
]);
|
|
1384
1988
|
}
|
|
1385
1989
|
//#endregion
|
|
1386
|
-
|
|
1990
|
+
//#region src/testing/chat-state.ts
|
|
1991
|
+
const sleep = (ms) => new Promise((resolve) => {
|
|
1992
|
+
setTimeout(resolve, ms);
|
|
1993
|
+
});
|
|
1994
|
+
/**
|
|
1995
|
+
* Runs the Chat SDK `StateAdapter` contract against `state`: subscriptions,
|
|
1996
|
+
* one lock holder at a time with token-checked release and extension, lock
|
|
1997
|
+
* and cache TTLs, `setIfNotExists`, list trimming, and a per-thread queue
|
|
1998
|
+
* that keeps the newest entries, reports its depth and drops expired ones.
|
|
1999
|
+
*/
|
|
2000
|
+
function testChatState(state, options) {
|
|
2001
|
+
const fresh = options.id ?? (() => `conformance-${crypto.randomUUID()}`);
|
|
2002
|
+
const ttl = options.ttlMs ?? 300;
|
|
2003
|
+
const entry = (thread, text, expiresIn = 6e4) => ({
|
|
2004
|
+
enqueuedAt: Date.now(),
|
|
2005
|
+
expiresAt: Date.now() + expiresIn,
|
|
2006
|
+
message: options.message(`${thread}-${text}`, text)
|
|
2007
|
+
});
|
|
2008
|
+
return conform("StateAdapter", [
|
|
2009
|
+
["subscribes and unsubscribes a thread", async () => {
|
|
2010
|
+
const thread = fresh();
|
|
2011
|
+
expect(!await state.isSubscribed(thread), "new thread is subscribed");
|
|
2012
|
+
await state.subscribe(thread);
|
|
2013
|
+
await state.subscribe(thread);
|
|
2014
|
+
expect(await state.isSubscribed(thread), "subscribe did not stick");
|
|
2015
|
+
await state.unsubscribe(thread);
|
|
2016
|
+
expect(!await state.isSubscribed(thread), "unsubscribe did not stick");
|
|
2017
|
+
}],
|
|
2018
|
+
["gives the lock to one holder at a time", async () => {
|
|
2019
|
+
const thread = fresh();
|
|
2020
|
+
const [a, b] = await Promise.all([state.acquireLock(thread, 1e4), state.acquireLock(thread, 1e4)]);
|
|
2021
|
+
expect(a === null !== (b === null), "both or neither got the lock");
|
|
2022
|
+
const held = a ?? b;
|
|
2023
|
+
expect(held !== null, "no lock");
|
|
2024
|
+
expect(held.threadId === thread, "lock names another thread");
|
|
2025
|
+
expect(held.expiresAt > Date.now(), "lock is already expired");
|
|
2026
|
+
await state.releaseLock({
|
|
2027
|
+
...held,
|
|
2028
|
+
token: "not-the-token"
|
|
2029
|
+
});
|
|
2030
|
+
expect(await state.acquireLock(thread, 1e4) === null, "a wrong token released the lock");
|
|
2031
|
+
expect(await state.extendLock(held, 2e4), "extendLock failed");
|
|
2032
|
+
await state.releaseLock(held);
|
|
2033
|
+
expect(await state.acquireLock(thread, 1e4) !== null, "released lock was not free");
|
|
2034
|
+
await state.forceReleaseLock(thread);
|
|
2035
|
+
expect(await state.acquireLock(thread, 1e4) !== null, "forceReleaseLock left the lock held");
|
|
2036
|
+
}],
|
|
2037
|
+
["lets an expired lock go", async () => {
|
|
2038
|
+
const thread = fresh();
|
|
2039
|
+
const lock = await state.acquireLock(thread, ttl);
|
|
2040
|
+
expect(lock !== null, "no lock");
|
|
2041
|
+
await sleep(ttl + 100);
|
|
2042
|
+
expect(!await state.extendLock(lock, 1e4), "extended an expired lock");
|
|
2043
|
+
expect(await state.acquireLock(thread, 1e4) !== null, "expired lock is still held");
|
|
2044
|
+
}],
|
|
2045
|
+
["stores values with a TTL", async () => {
|
|
2046
|
+
const key = fresh();
|
|
2047
|
+
expect(await state.get(key) === null, "missing key is not null");
|
|
2048
|
+
await state.set(key, {
|
|
2049
|
+
n: 1,
|
|
2050
|
+
list: ["a"]
|
|
2051
|
+
});
|
|
2052
|
+
const stored = await state.get(key);
|
|
2053
|
+
expect(stored?.n === 1 && stored.list[0] === "a", "value changed");
|
|
2054
|
+
await state.set(key, "text");
|
|
2055
|
+
expect(await state.get(key) === "text", "a string value changed");
|
|
2056
|
+
await state.delete(key);
|
|
2057
|
+
expect(await state.get(key) === null, "delete left the value");
|
|
2058
|
+
await state.set(key, 1, ttl);
|
|
2059
|
+
await sleep(ttl + 100);
|
|
2060
|
+
expect(await state.get(key) === null, "value outlived its TTL");
|
|
2061
|
+
}],
|
|
2062
|
+
["sets a key only when it is missing", async () => {
|
|
2063
|
+
const key = fresh();
|
|
2064
|
+
expect(await state.setIfNotExists(key, "first"), "first set failed");
|
|
2065
|
+
expect(!await state.setIfNotExists(key, "second"), "second set won");
|
|
2066
|
+
expect(await state.get(key) === "first", "value was replaced");
|
|
2067
|
+
const short = fresh();
|
|
2068
|
+
await state.setIfNotExists(short, "old", ttl);
|
|
2069
|
+
await sleep(ttl + 100);
|
|
2070
|
+
expect(await state.setIfNotExists(short, "new"), "an expired key blocked the set");
|
|
2071
|
+
}],
|
|
2072
|
+
["appends to a list and keeps the newest maxLength", async () => {
|
|
2073
|
+
const key = fresh();
|
|
2074
|
+
for (const value of [
|
|
2075
|
+
"a",
|
|
2076
|
+
"b",
|
|
2077
|
+
"c",
|
|
2078
|
+
"d"
|
|
2079
|
+
]) await state.appendToList(key, value, { maxLength: 3 });
|
|
2080
|
+
const list = await state.getList(key);
|
|
2081
|
+
expect(list.join() === "b,c,d", `list is ${list.join()}`);
|
|
2082
|
+
expect((await state.getList(fresh())).length === 0, "missing list is not empty");
|
|
2083
|
+
}],
|
|
2084
|
+
["queues per thread, keeps the newest maxSize and reports the depth", async () => {
|
|
2085
|
+
const thread = fresh();
|
|
2086
|
+
expect(await state.queueDepth(thread) === 0, "new queue is not empty");
|
|
2087
|
+
expect(await state.dequeue(thread) === null, "empty queue dequeued");
|
|
2088
|
+
let depth = 0;
|
|
2089
|
+
for (const text of [
|
|
2090
|
+
"one",
|
|
2091
|
+
"two",
|
|
2092
|
+
"three"
|
|
2093
|
+
]) depth = await state.enqueue(thread, entry(thread, text), 2);
|
|
2094
|
+
expect(depth === 2, `depth after three with maxSize 2 is ${String(depth)}`);
|
|
2095
|
+
expect(await state.queueDepth(thread) === 2, "queueDepth disagrees");
|
|
2096
|
+
const first = await state.dequeue(thread);
|
|
2097
|
+
expect(first?.message.text === "two", "dequeue did not return the oldest kept entry");
|
|
2098
|
+
expect(first.message.id === `${thread}-two`, "message id changed");
|
|
2099
|
+
expect(await state.queueDepth(thread) === 1, "dequeue did not remove it");
|
|
2100
|
+
expect(await state.queueDepth(fresh()) === 0, "another thread shares the queue");
|
|
2101
|
+
}],
|
|
2102
|
+
["drops expired queue entries", async () => {
|
|
2103
|
+
const thread = fresh();
|
|
2104
|
+
await state.enqueue(thread, entry(thread, "stale", ttl), 10);
|
|
2105
|
+
await sleep(ttl + 100);
|
|
2106
|
+
expect(await state.queueDepth(thread) === 0, "an expired entry counts");
|
|
2107
|
+
expect(await state.dequeue(thread) === null, "an expired entry dequeued");
|
|
2108
|
+
}]
|
|
2109
|
+
]);
|
|
2110
|
+
}
|
|
2111
|
+
//#endregion
|
|
2112
|
+
//#region src/testing/streams.ts
|
|
2113
|
+
async function collect(stream, timeoutMs) {
|
|
2114
|
+
const chunks = [];
|
|
2115
|
+
const reader = stream.getReader();
|
|
2116
|
+
let timer;
|
|
2117
|
+
const timeout = new Promise((_, reject) => {
|
|
2118
|
+
timer = setTimeout(() => {
|
|
2119
|
+
reject(/* @__PURE__ */ new Error(`read did not end within ${String(timeoutMs)} ms`));
|
|
2120
|
+
}, timeoutMs);
|
|
2121
|
+
});
|
|
2122
|
+
try {
|
|
2123
|
+
for (;;) {
|
|
2124
|
+
const { done, value } = await Promise.race([reader.read(), timeout]);
|
|
2125
|
+
if (done) return chunks;
|
|
2126
|
+
chunks.push(value);
|
|
2127
|
+
}
|
|
2128
|
+
} finally {
|
|
2129
|
+
clearTimeout(timer);
|
|
2130
|
+
await reader.cancel().catch(() => void 0);
|
|
2131
|
+
}
|
|
2132
|
+
}
|
|
2133
|
+
const hintOf = (result) => result.ok ? void 0 : result.error.hint;
|
|
2134
|
+
/**
|
|
2135
|
+
* Runs the `StreamStore` contract against `store`: the API version, open
|
|
2136
|
+
* once, idempotent appends that refuse gaps, reads from any index that end
|
|
2137
|
+
* when the stream closes, a live read that sees later appends, cancel seen by
|
|
2138
|
+
* the writer, a closed stream that refuses appends, and a missing stream.
|
|
2139
|
+
*/
|
|
2140
|
+
function testStreamStore(store, options = {}) {
|
|
2141
|
+
const fresh = options.id ?? (() => `conformance-${crypto.randomUUID()}`);
|
|
2142
|
+
const timeoutMs = options.timeoutMs ?? 5e3;
|
|
2143
|
+
return conform(`StreamStore "${store.name}"`, [
|
|
2144
|
+
["has apiVersion 1 and a name", () => {
|
|
2145
|
+
const version = store.apiVersion;
|
|
2146
|
+
expect(version === 1, `apiVersion is ${String(version)}`);
|
|
2147
|
+
expect(store.name.length > 0, "name is empty");
|
|
2148
|
+
}],
|
|
2149
|
+
["opens a stream once", async () => {
|
|
2150
|
+
const id = fresh();
|
|
2151
|
+
expect(await store.open(id).orThrow(), "first open was not new");
|
|
2152
|
+
expect(!await store.open(id).orThrow(), "second open was new");
|
|
2153
|
+
}],
|
|
2154
|
+
["appends idempotently and refuses a gap", async () => {
|
|
2155
|
+
const id = fresh();
|
|
2156
|
+
await store.open(id).orThrow();
|
|
2157
|
+
const first = await store.append(id, 0, ["a", "b"]).orThrow();
|
|
2158
|
+
expect(first.next === 2, `next after two chunks is ${String(first.next)}`);
|
|
2159
|
+
const retried = await store.append(id, 0, [
|
|
2160
|
+
"a",
|
|
2161
|
+
"b",
|
|
2162
|
+
"c"
|
|
2163
|
+
]).orThrow();
|
|
2164
|
+
expect(retried.next === 3, `next after a retried batch is ${String(retried.next)}`);
|
|
2165
|
+
const gap = await store.append(id, 5, ["x"]);
|
|
2166
|
+
expect(hintOf(gap) === "STREAM_GAP", `a gap gave ${JSON.stringify(gap)}`);
|
|
2167
|
+
await store.close(id).orThrow();
|
|
2168
|
+
const chunks = await collect(store.read(id), timeoutMs);
|
|
2169
|
+
expect(chunks.join("") === "abc", `read ${JSON.stringify(chunks)} after a retried batch`);
|
|
2170
|
+
}],
|
|
2171
|
+
["reads from an index and ends once closed", async () => {
|
|
2172
|
+
const id = fresh();
|
|
2173
|
+
await store.open(id).orThrow();
|
|
2174
|
+
await store.append(id, 0, [
|
|
2175
|
+
"0",
|
|
2176
|
+
"1",
|
|
2177
|
+
"2",
|
|
2178
|
+
"3"
|
|
2179
|
+
]).orThrow();
|
|
2180
|
+
expect(await store.close(id).orThrow(), "close returned false");
|
|
2181
|
+
expect(!await store.close(id).orThrow(), "a second close returned true");
|
|
2182
|
+
const tail = await collect(store.read(id, 2), timeoutMs);
|
|
2183
|
+
expect(tail.join(",") === "2,3", `read from 2 gave ${JSON.stringify(tail)}`);
|
|
2184
|
+
const status = await store.status(id).orThrow();
|
|
2185
|
+
expect(status?.next === 4 && status.closed && !status.cancelled, `status is ${JSON.stringify(status)}`);
|
|
2186
|
+
}],
|
|
2187
|
+
["a live read sees later appends", async () => {
|
|
2188
|
+
const id = fresh();
|
|
2189
|
+
await store.open(id).orThrow();
|
|
2190
|
+
const reading = collect(store.read(id), timeoutMs);
|
|
2191
|
+
await store.append(id, 0, ["x"]).orThrow();
|
|
2192
|
+
await store.append(id, 1, ["y"]).orThrow();
|
|
2193
|
+
await store.close(id).orThrow();
|
|
2194
|
+
const chunks = await reading;
|
|
2195
|
+
expect(chunks.join("") === "xy", `live read gave ${JSON.stringify(chunks)}`);
|
|
2196
|
+
}],
|
|
2197
|
+
["tells the writer about a cancel", async () => {
|
|
2198
|
+
const id = fresh();
|
|
2199
|
+
await store.open(id).orThrow();
|
|
2200
|
+
expect(!await store.isCancelled(id).orThrow(), "a new stream is cancelled");
|
|
2201
|
+
expect(await store.cancel(id).orThrow(), "cancel returned false");
|
|
2202
|
+
expect(await store.isCancelled(id).orThrow(), "isCancelled is false after cancel");
|
|
2203
|
+
expect((await store.append(id, 0, ["late"]).orThrow()).cancelled, "append did not report the cancel");
|
|
2204
|
+
await store.close(id).orThrow();
|
|
2205
|
+
expect(!await store.cancel(id).orThrow(), "cancel of a closed stream returned true");
|
|
2206
|
+
}],
|
|
2207
|
+
["refuses appends to a closed stream", async () => {
|
|
2208
|
+
const id = fresh();
|
|
2209
|
+
await store.open(id).orThrow();
|
|
2210
|
+
await store.close(id).orThrow();
|
|
2211
|
+
const closed = await store.append(id, 0, ["x"]);
|
|
2212
|
+
expect(hintOf(closed) === "STREAM_CLOSED", `append gave ${JSON.stringify(closed)}`);
|
|
2213
|
+
}],
|
|
2214
|
+
["treats a missing stream as empty", async () => {
|
|
2215
|
+
const id = fresh();
|
|
2216
|
+
expect(await store.status(id).orThrow() === void 0, "status of a missing stream");
|
|
2217
|
+
const chunks = await collect(store.read(id), timeoutMs);
|
|
2218
|
+
expect(chunks.length === 0, `read a missing stream as ${JSON.stringify(chunks)}`);
|
|
2219
|
+
const appended = await store.append(id, 0, ["x"]);
|
|
2220
|
+
expect(hintOf(appended) === "STREAM_NOT_FOUND", `append gave ${JSON.stringify(appended)}`);
|
|
2221
|
+
}],
|
|
2222
|
+
["purges with a count", async () => {
|
|
2223
|
+
const purged = await store.purge().orThrow();
|
|
2224
|
+
expect(Number.isInteger(purged) && purged >= 0, `purge returned ${String(purged)}`);
|
|
2225
|
+
}]
|
|
2226
|
+
]);
|
|
2227
|
+
}
|
|
2228
|
+
//#endregion
|
|
2229
|
+
//#region src/testing/credentials.ts
|
|
2230
|
+
const APP = { type: "app" };
|
|
2231
|
+
const kindOf = (result) => result.ok ? void 0 : result.error.kind;
|
|
2232
|
+
/**
|
|
2233
|
+
* Runs the `CredentialProvider` contract against `provider`: the API version,
|
|
2234
|
+
* an unknown ref, a stored token with its headers, a new value seen after it
|
|
2235
|
+
* is stored, separate credentials per user and per tenant (`ref` with a
|
|
2236
|
+
* `tenant`, which `seed` must store too), revoke (per user, as the
|
|
2237
|
+
* organization purger revokes connector grants), and inbound checks that
|
|
2238
|
+
* accept a signed request and refuse a tampered one.
|
|
2239
|
+
*/
|
|
2240
|
+
function testCredentialProvider(provider, options) {
|
|
2241
|
+
const value = () => `token-${crypto.randomUUID()}`;
|
|
2242
|
+
const { ref, seed, userRef, inbound } = options;
|
|
2243
|
+
return conform(`CredentialProvider "${provider.name}"`, [
|
|
2244
|
+
["has apiVersion 1 and a name", () => {
|
|
2245
|
+
const version = provider.apiVersion;
|
|
2246
|
+
expect(version === 1, `apiVersion is ${String(version)}`);
|
|
2247
|
+
expect(provider.name.length > 0, "name is empty");
|
|
2248
|
+
}],
|
|
2249
|
+
["refuses a ref for another provider", async () => {
|
|
2250
|
+
const result = await provider.getToken({ provider: "conformance-unknown" }, { subject: APP });
|
|
2251
|
+
expect(kindOf(result) === "invalid_input", `got ${kindOf(result) ?? "a token"}`);
|
|
2252
|
+
}],
|
|
2253
|
+
["returns a stored token with headers", async () => {
|
|
2254
|
+
const token = value();
|
|
2255
|
+
await seed(ref, APP, token);
|
|
2256
|
+
const got = await provider.getToken(ref, { subject: APP }).orThrow();
|
|
2257
|
+
expect(got.token === token, "returned another token");
|
|
2258
|
+
expect(Object.values(got.headers).some((header) => header.includes(token)), "no header carries the token");
|
|
2259
|
+
}],
|
|
2260
|
+
["sees a new value once it is stored", async () => {
|
|
2261
|
+
await seed(ref, APP, value());
|
|
2262
|
+
await provider.getToken(ref, { subject: APP }).orThrow();
|
|
2263
|
+
const next = value();
|
|
2264
|
+
await seed(ref, APP, next);
|
|
2265
|
+
expect((await provider.getToken(ref, { subject: APP }).orThrow()).token === next, "returned the previous value");
|
|
2266
|
+
}],
|
|
2267
|
+
provider.set !== void 0 && ["stores a value through set", async () => {
|
|
2268
|
+
const set = provider.set?.bind(provider);
|
|
2269
|
+
expect(set !== void 0, "no set");
|
|
2270
|
+
const next = value();
|
|
2271
|
+
await set(ref, next, { subject: APP }).orThrow();
|
|
2272
|
+
expect((await provider.getToken(ref, { subject: APP }).orThrow()).token === next, "getToken missed the value set stored");
|
|
2273
|
+
}],
|
|
2274
|
+
userRef !== void 0 && ["keeps a credential per user", async () => {
|
|
2275
|
+
const alice = {
|
|
2276
|
+
type: "user",
|
|
2277
|
+
id: crypto.randomUUID()
|
|
2278
|
+
};
|
|
2279
|
+
const bob = {
|
|
2280
|
+
type: "user",
|
|
2281
|
+
id: crypto.randomUUID()
|
|
2282
|
+
};
|
|
2283
|
+
const [forAlice, forBob] = [value(), value()];
|
|
2284
|
+
await seed(userRef, alice, forAlice);
|
|
2285
|
+
await seed(userRef, bob, forBob);
|
|
2286
|
+
const a = await provider.getToken(userRef, { subject: alice }).orThrow();
|
|
2287
|
+
const b = await provider.getToken(userRef, { subject: bob }).orThrow();
|
|
2288
|
+
expect(a.token === forAlice && b.token === forBob, "users share a credential");
|
|
2289
|
+
expect(provider.capabilities(userRef).userSubjects, "capabilities do not report user subjects");
|
|
2290
|
+
expect(!(await provider.getToken(userRef, { subject: APP })).ok, "the app read a per-user credential");
|
|
2291
|
+
}],
|
|
2292
|
+
userRef !== void 0 && ["revokes one user's credential only", async () => {
|
|
2293
|
+
const alice = {
|
|
2294
|
+
type: "user",
|
|
2295
|
+
id: crypto.randomUUID()
|
|
2296
|
+
};
|
|
2297
|
+
const bob = {
|
|
2298
|
+
type: "user",
|
|
2299
|
+
id: crypto.randomUUID()
|
|
2300
|
+
};
|
|
2301
|
+
const forBob = value();
|
|
2302
|
+
await seed(userRef, alice, value());
|
|
2303
|
+
await seed(userRef, bob, forBob);
|
|
2304
|
+
expect(await provider.revoke(userRef, { subject: alice }).orThrow(), "revoke found nothing for the user");
|
|
2305
|
+
const after = await provider.getToken(userRef, { subject: alice });
|
|
2306
|
+
expect(kindOf(after) === "not_found", `after revoke got ${kindOf(after) ?? "a token"}`);
|
|
2307
|
+
expect((await provider.getToken(userRef, { subject: bob }).orThrow()).token === forBob, "revoking a user's credential removed another user's");
|
|
2308
|
+
await provider.revoke(userRef, { subject: bob }).orThrow();
|
|
2309
|
+
}],
|
|
2310
|
+
["keeps a tenant's credential inside its namespace", async () => {
|
|
2311
|
+
const forA = tenantCredentialRef(crypto.randomUUID(), ref);
|
|
2312
|
+
const forB = tenantCredentialRef(crypto.randomUUID(), ref);
|
|
2313
|
+
const [app, a, b] = [
|
|
2314
|
+
value(),
|
|
2315
|
+
value(),
|
|
2316
|
+
value()
|
|
2317
|
+
];
|
|
2318
|
+
await seed(ref, APP, app);
|
|
2319
|
+
await seed(forA, APP, a);
|
|
2320
|
+
await seed(forB, APP, b);
|
|
2321
|
+
const tokenOf = async (of) => (await provider.getToken(of, { subject: APP }).orThrow()).token;
|
|
2322
|
+
expect(await tokenOf(ref) === app && await tokenOf(forA) === a && await tokenOf(forB) === b, "a tenant's ref resolved another tenant's or the app's credential");
|
|
2323
|
+
expect(await provider.revoke(forA, { subject: APP }).orThrow(), "revoke found nothing in the tenant's namespace");
|
|
2324
|
+
const after = await provider.getToken(forA, { subject: APP });
|
|
2325
|
+
expect(kindOf(after) === "not_found", `after revoke got ${kindOf(after) ?? "a token"}`);
|
|
2326
|
+
expect(await tokenOf(forB) === b && await tokenOf(ref) === app, "revoking a tenant's ref removed another tenant's or the app's credential");
|
|
2327
|
+
await provider.revoke(forB, { subject: APP }).orThrow();
|
|
2328
|
+
}],
|
|
2329
|
+
["revokes a credential", async () => {
|
|
2330
|
+
await seed(ref, APP, value());
|
|
2331
|
+
expect(await provider.revoke(ref, { subject: APP }).orThrow(), "revoke found nothing");
|
|
2332
|
+
const after = await provider.getToken(ref, { subject: APP });
|
|
2333
|
+
expect(kindOf(after) === "not_found", `after revoke got ${kindOf(after) ?? "a token"}`);
|
|
2334
|
+
expect(!await provider.revoke(ref, { subject: APP }).orThrow(), "a second revoke found a credential");
|
|
2335
|
+
}],
|
|
2336
|
+
inbound !== void 0 && ["verifies inbound requests", async () => {
|
|
2337
|
+
const verify = provider.verifyInbound?.bind(provider);
|
|
2338
|
+
expect(verify !== void 0, "no verifyInbound");
|
|
2339
|
+
const secret = `whsec_${btoa(crypto.randomUUID())}`;
|
|
2340
|
+
await seed(inbound.ref, APP, secret);
|
|
2341
|
+
const body = JSON.stringify({ hello: "world" });
|
|
2342
|
+
const signed = await inbound.sign(body, secret);
|
|
2343
|
+
expect(await verify(signed, inbound.ref).orThrow(), "refused a signed request");
|
|
2344
|
+
expect(!await verify(new Request(signed.url, {
|
|
2345
|
+
method: "POST",
|
|
2346
|
+
headers: signed.headers,
|
|
2347
|
+
body: `${body} `
|
|
2348
|
+
}), inbound.ref).orThrow(), "accepted a tampered request");
|
|
2349
|
+
}]
|
|
2350
|
+
]);
|
|
2351
|
+
}
|
|
2352
|
+
//#endregion
|
|
2353
|
+
export { ConformanceError, LOCAL_JWT_SECRET, asUser, createTestSigner, defineSeed, expectDbBudget, expectInstant, expectTenantIsolation, isSeed, localAuth, localSigningKey, signLocalJwt, signTestJwt, signTestJwtWithKey, supabaseClaimFixtures, testAdapter, testAiTaskRunner, testAuthResolver, testAuthorizationProvider, testBlockTransportMiddleware, testBuilderStarter, testCacheAdapter, testChatState, testCredentialProvider, testEmbedder, testEveDocumentBackend, testEventSink, testExecutor, testGenerator, testGraphCompiler, testNotificationChannel, testPlugin, testQueueBackend, testStreamStore, testSupportSessionStore, testWebhookSecretStore, testWebhookSigner, testWebhookTransport };
|