better-supabase 0.5.0 → 0.6.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/README.md +299 -41
- package/dist/access-sql-B0dF-SoF.js +181 -0
- package/dist/actor-CQTlc1Hh.d.ts +43 -0
- package/dist/adapter-Bc1o3zyy.js +1106 -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 +210 -0
- package/dist/blocks/attachments/index.js +222 -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 +251 -0
- package/dist/blocks/data-lifecycle/index.js +353 -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-L7Z8mYTx.js → doctor-B4JOm_ox.js} +992 -316
- 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-BGLb_zou.js +3100 -0
- package/dist/cli/gen-DAH8FM2W.js +2 -0
- package/dist/cli/index.d.ts +44 -5
- package/dist/cli/index.js +7 -7
- package/dist/cli/{init-Saf5-R9T.js → init-BIIwclPg.js} +242 -27
- package/dist/cli/{introspect--H7oGmcd.js → introspect-DNUTuNvb.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-BKR2W_79.js → run-DNWxHynX.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-BDJuO0UR.js → version-hr88OZG0.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-Bt1YK3Ke.d.ts +146 -0
- package/dist/composite-DhE66P5u.js +130 -0
- package/dist/config/index.d.ts +6 -4
- package/dist/config/index.js +5 -5
- package/dist/{config-T_ejwyd3.d.ts → config-D44Y7PAv.d.ts} +406 -205
- 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-BTrQ_ZTp.d.ts → ended-jLfvw8h-.d.ts} +20 -2
- package/dist/{entitlements-DUxU4oHe.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-CONDyPYx.js +51 -0
- package/dist/framework-DuVb4Xa8.d.ts +25 -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-DcqaTBHx.js +148 -0
- 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-CusDgPaD.d.ts → mfa-Daak8ukG.d.ts} +8 -2
- 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-Bq64ki8-.js → resolve-D_IcnvFd.js} +239 -289
- package/dist/{resolve-C15TJq77.d.ts → resolve-KW8nCeL6.d.ts} +29 -45
- package/dist/{resource-CRxxwkQq.js → resource-BsvzCMlX.js} +4 -4
- package/dist/{resource-D8iqWMr-.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 -21
- package/dist/server/index.js +46 -14
- package/dist/{respond-D2OQl62-.d.ts → server-CeVeeXwz.d.ts} +225 -80
- 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 +330 -194
- package/dist/sql/index.js +34901 -6489
- 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-Dk6vdmEw.js → support-BivNqSpK.js} +35 -12
- package/dist/{support-view-Da5peKFg.d.ts → support-view-33Oi07TT.d.ts} +1 -1
- 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 +343 -37
- package/dist/testing/index.js +1186 -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-DM_Roffb.js → version-Bvv6aLjZ.js} +1 -1
- package/dist/view-BeFcRV6G.js +99 -0
- package/dist/{view-BEf0diC5.d.ts → view-DbxLNu8L.d.ts} +35 -32
- 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 +470 -29
- package/schemas/ai-message-v1.json +258 -0
- package/schemas/config-v1.json +1074 -140
- package/schemas/doctor-report-v1.json +21 -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 +62 -18
- 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-BJc9vdyu.js +0 -2
- package/dist/cli/gen-C2osRcr5.js +0 -2204
- package/dist/cli/read-sets-BfMQcKy8.js +0 -289
- package/dist/cli/sql-DzPbfngx.js +0 -348
- 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/impersonation-VfaGc4Q_.js +0 -41
- package/dist/index-BGdgBPdf.d.ts +0 -161
- package/dist/index-DOESKG27.d.ts +0 -100
- package/dist/index-DmXSxbLi.d.ts +0 -62
- package/dist/index-Dxn2a5cw.d.ts +0 -50
- 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/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-Csx2sRjg.js +0 -655
- package/dist/stats-Bqob3WqT.js +0 -592
- package/dist/support-DT-OhSxD.d.ts +0 -88
- 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 -100
package/dist/storage/index.js
CHANGED
|
@@ -1,31 +1,717 @@
|
|
|
1
|
-
import {
|
|
2
|
-
import {
|
|
3
|
-
import {
|
|
4
|
-
import {
|
|
5
|
-
import {
|
|
6
|
-
import {
|
|
7
|
-
import {
|
|
8
|
-
import { n as
|
|
1
|
+
import { t as DbException } from "../db-exception-CnWcYPxL.js";
|
|
2
|
+
import { n as dbError } from "../errors-B11Duf73.js";
|
|
3
|
+
import { c as optionalTemporal } from "../temporal-BJGitm-h.js";
|
|
4
|
+
import { n as temporalMissing } from "../temporal-required-BVvW7SYJ.js";
|
|
5
|
+
import { a as ok, r as err, s as toDbError, t as AsyncResult } from "../result-DzUbqgdW.js";
|
|
6
|
+
import { a as spansAllTenants } from "../plugin-tfXW1-qO.js";
|
|
7
|
+
import { a as tenantClaimPaths, o as tenantFrom, s as tenantPathsFor } from "../claims-PqzCxaTD.js";
|
|
8
|
+
import { n as ttlSeconds, t as TTL } from "../ttl-Hr34gRu8.js";
|
|
9
|
+
import { i as sqlString, n as slug, r as sqlIdent, t as parseTemplate } from "../template--jDkl3UP.js";
|
|
10
|
+
import { t as storageImageUrl } from "../image-url-pIVlO6R1.js";
|
|
11
|
+
import { t as fromStorageError } from "../errors-DLjJHPLK.js";
|
|
12
|
+
import { r as accessCheck } from "../access-sql-B0dF-SoF.js";
|
|
13
|
+
//#region src/storage/versioning.ts
|
|
14
|
+
const MAX_DAYS = 2147483647;
|
|
15
|
+
function wholeIn(value, min, max) {
|
|
16
|
+
return Number.isInteger(value) && value >= min && value <= max;
|
|
17
|
+
}
|
|
18
|
+
/** Checks a lifecycle policy and fills Storage's defaults; throws a `TypeError` for one Storage would refuse. */
|
|
19
|
+
function lifecyclePayload(id, lifecycle) {
|
|
20
|
+
const rules = lifecycle.rules;
|
|
21
|
+
if (rules.length === 0 || rules.length > 1e3) throw new TypeError(`defineBucket("${id}"): lifecycle needs 1 to 1000 rules`);
|
|
22
|
+
const ids = rules.flatMap((rule) => rule.id === void 0 ? [] : [rule.id]);
|
|
23
|
+
if (new Set(ids).size !== ids.length) throw new TypeError(`defineBucket("${id}"): lifecycle rule ids repeat`);
|
|
24
|
+
return { rules: rules.map((rule) => {
|
|
25
|
+
const { noncurrentDays, newerNoncurrentVersions } = rule.noncurrentVersionExpiration;
|
|
26
|
+
if (!wholeIn(noncurrentDays, 1, MAX_DAYS)) throw new TypeError(`defineBucket("${id}"): noncurrentDays must be a whole number of days, at least 1`);
|
|
27
|
+
if (newerNoncurrentVersions !== void 0 && !wholeIn(newerNoncurrentVersions, 1, 100)) throw new TypeError(`defineBucket("${id}"): newerNoncurrentVersions must be 1 to 100`);
|
|
28
|
+
return {
|
|
29
|
+
...rule.id === void 0 ? {} : { id: rule.id },
|
|
30
|
+
status: rule.status ?? "Enabled",
|
|
31
|
+
filter: {},
|
|
32
|
+
noncurrentVersionExpiration: {
|
|
33
|
+
noncurrentDays,
|
|
34
|
+
...newerNoncurrentVersions === void 0 ? {} : { newerNoncurrentVersions }
|
|
35
|
+
}
|
|
36
|
+
};
|
|
37
|
+
}) };
|
|
38
|
+
}
|
|
39
|
+
function ruleKey(rule) {
|
|
40
|
+
if (typeof rule !== "object" || rule === null) return JSON.stringify(rule);
|
|
41
|
+
const entry = rule;
|
|
42
|
+
const expiration = entry.noncurrentVersionExpiration ?? {};
|
|
43
|
+
return JSON.stringify([
|
|
44
|
+
entry.status ?? "Enabled",
|
|
45
|
+
expiration.noncurrentDays,
|
|
46
|
+
expiration.newerNoncurrentVersions ?? null
|
|
47
|
+
]);
|
|
48
|
+
}
|
|
49
|
+
/**
|
|
50
|
+
* Whether a stored `lifecycle_configuration` matches the expected payload.
|
|
51
|
+
* Generated rule ids are ignored; the rules compare as a set.
|
|
52
|
+
*/
|
|
53
|
+
function sameLifecycle(expected, actual) {
|
|
54
|
+
const stored = typeof actual === "object" && actual !== null && "rules" in actual && Array.isArray(actual.rules) ? actual.rules : [];
|
|
55
|
+
const want = (expected?.rules ?? []).map(ruleKey).sort();
|
|
56
|
+
const have = stored.map(ruleKey).sort();
|
|
57
|
+
return want.length === have.length && want.every((key, i) => key === have[i]);
|
|
58
|
+
}
|
|
59
|
+
/** Storage codes for a lifecycle that is absent or a feature the project doesn't have. */
|
|
60
|
+
function lifecycleAbsent(error) {
|
|
61
|
+
return error.code === "NoSuchLifecycleConfiguration" || error.kind === "unsupported";
|
|
62
|
+
}
|
|
63
|
+
function toVersion(item) {
|
|
64
|
+
if (!item.version) return void 0;
|
|
65
|
+
const metadata = item.metadata ?? {};
|
|
66
|
+
const size = metadata["size"];
|
|
67
|
+
const type = metadata["mimetype"];
|
|
68
|
+
return {
|
|
69
|
+
versionId: item.version,
|
|
70
|
+
current: !item.archived_at,
|
|
71
|
+
deleteMarker: item.is_delete_marker === true,
|
|
72
|
+
...typeof size === "number" ? { size } : {},
|
|
73
|
+
...typeof type === "string" ? { contentType: type } : {},
|
|
74
|
+
createdAt: item.created_at ?? null,
|
|
75
|
+
archivedAt: item.archived_at ?? null
|
|
76
|
+
};
|
|
77
|
+
}
|
|
78
|
+
//#endregion
|
|
79
|
+
//#region src/storage/apply.ts
|
|
80
|
+
/**
|
|
81
|
+
* Creates or updates a bucket through the Storage API, then sets or removes
|
|
82
|
+
* its lifecycle policy. Versioning and lifecycle live behind the API, so
|
|
83
|
+
* `bucket.sql()` can't write them.
|
|
84
|
+
*/
|
|
85
|
+
function applyBucket(settings, client) {
|
|
86
|
+
const storage = client.storage;
|
|
87
|
+
const fail = (raw) => err(fromStorageError(raw, settings.id));
|
|
88
|
+
return AsyncResult.from(async () => {
|
|
89
|
+
try {
|
|
90
|
+
const limits = {
|
|
91
|
+
public: settings.public,
|
|
92
|
+
fileSizeLimit: settings.fileSizeLimit ?? null,
|
|
93
|
+
allowedMimeTypes: settings.allowedMimeTypes ? [...settings.allowedMimeTypes] : null
|
|
94
|
+
};
|
|
95
|
+
const found = await storage.getBucket(settings.id);
|
|
96
|
+
const missing = found.error !== null && fromStorageError(found.error).kind === "not_found";
|
|
97
|
+
if (found.error && !missing) return fail(found.error);
|
|
98
|
+
if (missing) {
|
|
99
|
+
const created = await storage.createBucket(settings.id, {
|
|
100
|
+
...limits,
|
|
101
|
+
...settings.versioning ? { versioningStatus: "ENABLED" } : {}
|
|
102
|
+
});
|
|
103
|
+
if (created.error) return fail(created.error);
|
|
104
|
+
} else {
|
|
105
|
+
const current = found.data?.versioning_status;
|
|
106
|
+
const versioningStatus = settings.versioning ? "ENABLED" : current === "ENABLED" ? "SUSPENDED" : void 0;
|
|
107
|
+
const updated = await storage.updateBucket(settings.id, {
|
|
108
|
+
...limits,
|
|
109
|
+
...versioningStatus ? { versioningStatus } : {}
|
|
110
|
+
});
|
|
111
|
+
if (updated.error) return fail(updated.error);
|
|
112
|
+
}
|
|
113
|
+
if (settings.lifecycle) {
|
|
114
|
+
const set = await storage.updateBucketLifecycle(settings.id, { rules: settings.lifecycle.rules.map((rule) => ({ ...rule })) });
|
|
115
|
+
if (set.error) return fail(set.error);
|
|
116
|
+
} else if (!missing) {
|
|
117
|
+
const removed = await storage.deleteBucketLifecycle(settings.id);
|
|
118
|
+
if (removed.error && !lifecycleAbsent(fromStorageError(removed.error))) return fail(removed.error);
|
|
119
|
+
}
|
|
120
|
+
return ok({ created: missing });
|
|
121
|
+
} catch (cause) {
|
|
122
|
+
return fail(cause);
|
|
123
|
+
}
|
|
124
|
+
});
|
|
125
|
+
}
|
|
126
|
+
//#endregion
|
|
127
|
+
//#region src/storage/layouts.ts
|
|
128
|
+
const SAFE_SEGMENT = /^[\w!\-.*'() &$@=;:+,?]+$/;
|
|
129
|
+
function validateSegment(_name, value) {
|
|
130
|
+
if (value === "." || value === "..") return "is a relative path";
|
|
131
|
+
if (!SAFE_SEGMENT.test(value)) return "contains characters Storage does not allow";
|
|
132
|
+
}
|
|
133
|
+
const keysOf = (values) => Object.keys(values).filter((key) => values[key] !== void 0);
|
|
134
|
+
/** The longest folder, segment by segment, that holds every one of `folders`. */
|
|
135
|
+
function commonFolder(folders) {
|
|
136
|
+
const split = folders.map((folder) => folder ? folder.split("/") : []);
|
|
137
|
+
const shared = [];
|
|
138
|
+
for (const [index, segment] of (split[0] ?? []).entries()) {
|
|
139
|
+
if (split.some((parts) => parts[index] !== segment)) break;
|
|
140
|
+
shared.push(segment);
|
|
141
|
+
}
|
|
142
|
+
return shared.join("/");
|
|
143
|
+
}
|
|
144
|
+
function pathLayouts(id, path, sqlString) {
|
|
145
|
+
const sources = typeof path === "string" ? [path] : path;
|
|
146
|
+
if (sources.length === 0) throw new TypeError(`defineBucket(${id}): path needs a template`);
|
|
147
|
+
const layouts = sources.map((source) => parseTemplate(source, "/", validateSegment, { rest: true }));
|
|
148
|
+
const layoutFor = (values) => {
|
|
149
|
+
if (layouts.length === 1) return layouts[0];
|
|
150
|
+
const keys = keysOf(values);
|
|
151
|
+
const layout = layouts.find((candidate) => candidate.params.length === keys.length && candidate.params.every((name) => keys.includes(name)));
|
|
152
|
+
if (layout) return layout;
|
|
153
|
+
throw new DbException(dbError("invalid_input", `No path template of bucket "${id}" takes exactly {${keys.join(", ")}}`));
|
|
154
|
+
};
|
|
155
|
+
const patterns = layouts.map((layout) => `name ~ ${sqlString(layout.sqlPattern)}`);
|
|
156
|
+
return {
|
|
157
|
+
sources,
|
|
158
|
+
params: [...new Set(layouts.flatMap((layout) => layout.params))],
|
|
159
|
+
build(values) {
|
|
160
|
+
const filled = values;
|
|
161
|
+
return layoutFor(values).build(filled);
|
|
162
|
+
},
|
|
163
|
+
match(value) {
|
|
164
|
+
for (const layout of layouts) {
|
|
165
|
+
const values = layout.match(value);
|
|
166
|
+
if (values) return values;
|
|
167
|
+
}
|
|
168
|
+
return null;
|
|
169
|
+
},
|
|
170
|
+
prefix(values) {
|
|
171
|
+
const given = keysOf(values);
|
|
172
|
+
const fitting = layouts.filter((layout) => given.every((key) => layout.params.includes(key)));
|
|
173
|
+
return commonFolder((fitting.length > 0 ? fitting : layouts).map((layout) => layout.prefix(values, layout.rest === void 0 ? layout.segments - 1 : layout.segments)));
|
|
174
|
+
},
|
|
175
|
+
segmentOf(param, kind) {
|
|
176
|
+
const indexes = layouts.map((layout) => {
|
|
177
|
+
const index = layout.segmentOf(param);
|
|
178
|
+
if (index === void 0) throw new TypeError(`defineBucket: a ${kind} policy needs {${param}} as a whole path segment in "${layout.source}"`);
|
|
179
|
+
return index;
|
|
180
|
+
});
|
|
181
|
+
if (new Set(indexes).size > 1) throw new TypeError(`defineBucket: a ${kind} policy needs {${param}} at the same segment in every path of "${id}"`);
|
|
182
|
+
return indexes[0];
|
|
183
|
+
},
|
|
184
|
+
requireParam(param, label) {
|
|
185
|
+
for (const layout of layouts) if (!layout.params.includes(param)) throw new TypeError(`defineBucket: ${label} {${param}} is not in "${layout.source}"`);
|
|
186
|
+
},
|
|
187
|
+
sqlMatch: patterns.length === 1 ? patterns[0] : `(${patterns.join(" or ")})`
|
|
188
|
+
};
|
|
189
|
+
}
|
|
190
|
+
/** Whether matched path values hold every value of `within`; a rest value may sit under it. */
|
|
191
|
+
function inScope(values, within) {
|
|
192
|
+
if (!values) return false;
|
|
193
|
+
return Object.entries(within).every(([key, expected]) => {
|
|
194
|
+
if (expected === void 0) return true;
|
|
195
|
+
const actual = String(values[key]);
|
|
196
|
+
return actual === String(expected) || actual.startsWith(`${String(expected)}/`);
|
|
197
|
+
});
|
|
198
|
+
}
|
|
199
|
+
//#endregion
|
|
200
|
+
//#region src/storage/progress.ts
|
|
201
|
+
/** The runtime's `XMLHttpRequest`, or `undefined` (Node, workers). */
|
|
202
|
+
function xhrConstructor() {
|
|
203
|
+
const runtime = globalThis;
|
|
204
|
+
return typeof runtime.XMLHttpRequest === "function" ? runtime.XMLHttpRequest : void 0;
|
|
205
|
+
}
|
|
206
|
+
/** Bodies `XMLHttpRequest.send` takes; streams go through storage-js instead. */
|
|
207
|
+
function sendable(body) {
|
|
208
|
+
return !(typeof ReadableStream !== "undefined" && body instanceof ReadableStream);
|
|
209
|
+
}
|
|
210
|
+
/**
|
|
211
|
+
* PUTs `body` to a signed upload URL with upload progress events. Resolves
|
|
212
|
+
* with `undefined` on success or the failure to map.
|
|
213
|
+
*/
|
|
214
|
+
function xhrUpload(Request, url, body, options) {
|
|
215
|
+
return new Promise((resolve) => {
|
|
216
|
+
const xhr = new Request();
|
|
217
|
+
const onAbort = () => {
|
|
218
|
+
xhr.abort();
|
|
219
|
+
};
|
|
220
|
+
const done = (failure) => {
|
|
221
|
+
options.signal?.removeEventListener("abort", onAbort);
|
|
222
|
+
resolve(failure);
|
|
223
|
+
};
|
|
224
|
+
xhr.open("PUT", url);
|
|
225
|
+
xhr.setRequestHeader("x-upsert", String(options.upsert));
|
|
226
|
+
xhr.setRequestHeader("cache-control", `max-age=${options.cacheControl ?? "3600"}`);
|
|
227
|
+
const type = options.contentType ?? (typeof Blob !== "undefined" && body instanceof Blob && body.type ? body.type : void 0);
|
|
228
|
+
if (type) xhr.setRequestHeader("content-type", type);
|
|
229
|
+
xhr.upload.onprogress = (event) => {
|
|
230
|
+
if (event.lengthComputable && event.total > 0) options.onProgress(Math.min(1, event.loaded / event.total));
|
|
231
|
+
};
|
|
232
|
+
xhr.onload = () => {
|
|
233
|
+
if (xhr.status >= 200 && xhr.status < 300) {
|
|
234
|
+
options.onProgress(1);
|
|
235
|
+
done(void 0);
|
|
236
|
+
return;
|
|
237
|
+
}
|
|
238
|
+
done(parseFailure(xhr.status, xhr.responseText));
|
|
239
|
+
};
|
|
240
|
+
xhr.onerror = () => {
|
|
241
|
+
done({
|
|
242
|
+
message: "Upload failed",
|
|
243
|
+
statusCode: "0",
|
|
244
|
+
name: "StorageUnknownError"
|
|
245
|
+
});
|
|
246
|
+
};
|
|
247
|
+
xhr.onabort = () => {
|
|
248
|
+
done({
|
|
249
|
+
message: "The upload was aborted",
|
|
250
|
+
statusCode: "0",
|
|
251
|
+
name: "AbortError"
|
|
252
|
+
});
|
|
253
|
+
};
|
|
254
|
+
options.signal?.addEventListener("abort", onAbort, { once: true });
|
|
255
|
+
xhr.send(body);
|
|
256
|
+
});
|
|
257
|
+
}
|
|
258
|
+
function parseFailure(status, text) {
|
|
259
|
+
let message = `Upload failed with status ${String(status)}`;
|
|
260
|
+
let statusCode = String(status);
|
|
261
|
+
try {
|
|
262
|
+
const parsed = JSON.parse(text);
|
|
263
|
+
if (typeof parsed === "object" && parsed !== null) {
|
|
264
|
+
const fields = parsed;
|
|
265
|
+
if (typeof fields.message === "string") message = fields.message;
|
|
266
|
+
if (typeof fields.statusCode === "string") statusCode = fields.statusCode;
|
|
267
|
+
}
|
|
268
|
+
} catch {}
|
|
269
|
+
return {
|
|
270
|
+
message,
|
|
271
|
+
statusCode
|
|
272
|
+
};
|
|
273
|
+
}
|
|
274
|
+
//#endregion
|
|
275
|
+
//#region src/storage/connect.ts
|
|
276
|
+
/** Folder listings in flight at once while `list` walks a tree. */
|
|
277
|
+
const LIST_CONCURRENCY = 4;
|
|
278
|
+
/** Signed URLs kept per connected bucket; the oldest goes first. */
|
|
279
|
+
const SIGNED_CACHE_SIZE = 500;
|
|
280
|
+
/** Runs at most `limit` of the calls passed to the returned function at once. */
|
|
281
|
+
function pool(limit) {
|
|
282
|
+
let active = 0;
|
|
283
|
+
const waiting = [];
|
|
284
|
+
return async (fn) => {
|
|
285
|
+
if (active < limit) active++;
|
|
286
|
+
else await new Promise((resolve) => {
|
|
287
|
+
waiting.push(resolve);
|
|
288
|
+
});
|
|
289
|
+
try {
|
|
290
|
+
return await fn();
|
|
291
|
+
} finally {
|
|
292
|
+
const next = waiting.shift();
|
|
293
|
+
if (next) next();
|
|
294
|
+
else active--;
|
|
295
|
+
}
|
|
296
|
+
};
|
|
297
|
+
}
|
|
298
|
+
function bodyInfo(body, contentType) {
|
|
299
|
+
const info = {};
|
|
300
|
+
if (typeof Blob !== "undefined" && body instanceof Blob) {
|
|
301
|
+
info.size = body.size;
|
|
302
|
+
if (body.type) info.type = body.type;
|
|
303
|
+
} else if (body instanceof ArrayBuffer || ArrayBuffer.isView(body)) info.size = body.byteLength;
|
|
304
|
+
if (contentType) info.type = contentType;
|
|
305
|
+
return info;
|
|
306
|
+
}
|
|
307
|
+
/** Rejects with the signal's reason when it aborts before `promise` settles. */
|
|
308
|
+
async function abortable(promise, signal) {
|
|
309
|
+
if (!signal) return promise;
|
|
310
|
+
signal.throwIfAborted();
|
|
311
|
+
let abort = () => {};
|
|
312
|
+
const aborted = new Promise((_, reject) => {
|
|
313
|
+
abort = () => {
|
|
314
|
+
const reason = signal.reason;
|
|
315
|
+
reject(reason instanceof Error ? reason : new Error(String(reason)));
|
|
316
|
+
};
|
|
317
|
+
signal.addEventListener("abort", abort, { once: true });
|
|
318
|
+
});
|
|
319
|
+
try {
|
|
320
|
+
return await Promise.race([promise, aborted]);
|
|
321
|
+
} finally {
|
|
322
|
+
signal.removeEventListener("abort", abort);
|
|
323
|
+
}
|
|
324
|
+
}
|
|
325
|
+
function isErrorResult(value) {
|
|
326
|
+
return typeof value === "object" && value !== null && value.ok === false && "error" in value;
|
|
327
|
+
}
|
|
328
|
+
function connectBucket(bucket, client, resolveTarget, connectOptions, guard) {
|
|
329
|
+
const resolve = (target) => {
|
|
330
|
+
const path = resolveTarget(target);
|
|
331
|
+
return guard ? guard.path(path) : path;
|
|
332
|
+
};
|
|
333
|
+
const scopedWithin = (within) => guard ? {
|
|
334
|
+
...within,
|
|
335
|
+
[guard.param]: guard.within(within)
|
|
336
|
+
} : within ?? {};
|
|
337
|
+
const api = () => client.storage.from(bucket.id);
|
|
338
|
+
const signed = connectOptions.cacheSignedUrls ? /* @__PURE__ */ new Map() : void 0;
|
|
339
|
+
/** Drops cached signed URLs for `paths`; keys start with the JSON-quoted path. */
|
|
340
|
+
const evict = (paths) => {
|
|
341
|
+
if (!signed || signed.size === 0) return;
|
|
342
|
+
const prefixes = paths.map((path) => `[${JSON.stringify(path)},`);
|
|
343
|
+
for (const key of signed.keys()) if (prefixes.some((prefix) => key.startsWith(prefix))) signed.delete(key);
|
|
344
|
+
};
|
|
345
|
+
const run = (fn) => AsyncResult.from(async () => {
|
|
346
|
+
try {
|
|
347
|
+
const { data, error } = await fn();
|
|
348
|
+
return error || data === null ? err(fromStorageError(error, bucket.id)) : ok(data);
|
|
349
|
+
} catch (cause) {
|
|
350
|
+
return err(fromStorageError(cause, bucket.id));
|
|
351
|
+
}
|
|
352
|
+
});
|
|
353
|
+
const fileOptions = (options, contentType) => ({
|
|
354
|
+
...contentType ? { contentType } : {},
|
|
355
|
+
...options?.cacheControl ? { cacheControl: options.cacheControl } : {},
|
|
356
|
+
...options?.metadata ? { metadata: { ...options.metadata } } : {},
|
|
357
|
+
upsert: options?.upsert ?? false
|
|
358
|
+
});
|
|
359
|
+
const download = (value) => value === void 0 ? {} : { download: value };
|
|
360
|
+
const urlExtras = (options) => ({
|
|
361
|
+
...download(options?.download),
|
|
362
|
+
...options?.transform ? { transform: { ...options.transform } } : {},
|
|
363
|
+
...options?.cacheNonce ? { cacheNonce: options.cacheNonce } : {},
|
|
364
|
+
...options?.versionId ? { versionId: options.versionId } : {}
|
|
365
|
+
});
|
|
366
|
+
const upload = (target, body, options) => AsyncResult.from(async () => {
|
|
367
|
+
options?.signal?.throwIfAborted();
|
|
368
|
+
const path = resolve(target);
|
|
369
|
+
const contentType = options?.contentType;
|
|
370
|
+
const problem = bucket.check(bodyInfo(body, contentType));
|
|
371
|
+
if (problem) return err(problem);
|
|
372
|
+
const Request = options?.onProgress ? xhrConstructor() : void 0;
|
|
373
|
+
if (options?.onProgress && Request && !options.metadata && sendable(body)) {
|
|
374
|
+
const reserved = await run(() => api().createSignedUploadUrl(path, options.upsert ? { upsert: true } : void 0));
|
|
375
|
+
if (!reserved.ok) return reserved;
|
|
376
|
+
const failure = await xhrUpload(Request, reserved.data.signedUrl, body, {
|
|
377
|
+
contentType,
|
|
378
|
+
cacheControl: options.cacheControl,
|
|
379
|
+
upsert: options.upsert ?? false,
|
|
380
|
+
signal: options.signal,
|
|
381
|
+
onProgress: options.onProgress
|
|
382
|
+
});
|
|
383
|
+
if (failure) return err(fromStorageError(failure, bucket.id));
|
|
384
|
+
evict([path]);
|
|
385
|
+
return ok({ path });
|
|
386
|
+
}
|
|
387
|
+
return run(() => abortable(api().upload(path, body, fileOptions(options, contentType)), options?.signal)).map(() => {
|
|
388
|
+
evict([path]);
|
|
389
|
+
options?.onProgress?.(1);
|
|
390
|
+
return { path };
|
|
391
|
+
});
|
|
392
|
+
});
|
|
393
|
+
const remove = (targets) => AsyncResult.from(async () => {
|
|
394
|
+
const paths = targets.map(resolve);
|
|
395
|
+
if (paths.length === 0) return ok([]);
|
|
396
|
+
return run(() => api().remove(paths)).map(() => {
|
|
397
|
+
evict(paths);
|
|
398
|
+
return paths;
|
|
399
|
+
});
|
|
400
|
+
});
|
|
401
|
+
const transfer = (method) => (from, to, options) => AsyncResult.from(async () => {
|
|
402
|
+
const [source, path] = [resolve(from), resolve(to)];
|
|
403
|
+
return run(() => api()[method](source, path, options?.versionId ? { sourceVersionId: options.versionId } : void 0)).map(() => {
|
|
404
|
+
evict(method === "move" ? [source, path] : [path]);
|
|
405
|
+
return { path };
|
|
406
|
+
});
|
|
407
|
+
});
|
|
408
|
+
const versions = (target, options) => AsyncResult.from(async () => {
|
|
409
|
+
const path = resolve(target);
|
|
410
|
+
const out = [];
|
|
411
|
+
for (let cursor;;) {
|
|
412
|
+
options?.signal?.throwIfAborted();
|
|
413
|
+
const { data, error } = await api().listV2({
|
|
414
|
+
prefix: path,
|
|
415
|
+
exactMatch: true,
|
|
416
|
+
noncurrentVersions: "include",
|
|
417
|
+
deleteMarkers: "include",
|
|
418
|
+
sortBy: {
|
|
419
|
+
column: "created_at",
|
|
420
|
+
order: "desc"
|
|
421
|
+
},
|
|
422
|
+
...cursor ? { cursor } : {}
|
|
423
|
+
}, options?.signal ? { signal: options.signal } : {});
|
|
424
|
+
if (error) return err(fromStorageError(error, bucket.id));
|
|
425
|
+
for (const item of data.objects) {
|
|
426
|
+
const version = toVersion(item);
|
|
427
|
+
if (version) out.push(version);
|
|
428
|
+
}
|
|
429
|
+
if (!data.hasNext || !data.nextCursor) return ok(out);
|
|
430
|
+
cursor = data.nextCursor;
|
|
431
|
+
}
|
|
432
|
+
});
|
|
433
|
+
const toObject = (path, item) => {
|
|
434
|
+
const metadata = item.metadata ?? {};
|
|
435
|
+
return {
|
|
436
|
+
path,
|
|
437
|
+
...typeof metadata.size === "number" ? { size: metadata.size } : {},
|
|
438
|
+
...metadata.mimetype ? { contentType: metadata.mimetype } : {},
|
|
439
|
+
createdAt: item.created_at,
|
|
440
|
+
updatedAt: item.updated_at
|
|
441
|
+
};
|
|
442
|
+
};
|
|
443
|
+
const PAGE = 1e3;
|
|
444
|
+
const page = async (folder, offset, signal) => {
|
|
445
|
+
signal?.throwIfAborted();
|
|
446
|
+
const { data, error } = await api().list(folder, {
|
|
447
|
+
limit: PAGE,
|
|
448
|
+
offset,
|
|
449
|
+
sortBy: {
|
|
450
|
+
column: "name",
|
|
451
|
+
order: "asc"
|
|
452
|
+
}
|
|
453
|
+
}, signal ? { signal } : {});
|
|
454
|
+
if (error) throw new DbException(fromStorageError(error, bucket.id));
|
|
455
|
+
return data;
|
|
456
|
+
};
|
|
457
|
+
const walk = async (folder, signal, request) => {
|
|
458
|
+
const parts = [];
|
|
459
|
+
for (let offset = 0;; offset += PAGE) {
|
|
460
|
+
const data = await request(() => page(folder, offset, signal));
|
|
461
|
+
for (const item of data) {
|
|
462
|
+
const path = folder ? `${folder}/${item.name}` : item.name;
|
|
463
|
+
if (item.id === null) {
|
|
464
|
+
const nested = walk(path, signal, request);
|
|
465
|
+
nested.catch(() => void 0);
|
|
466
|
+
parts.push(nested);
|
|
467
|
+
} else parts.push(toObject(path, item));
|
|
468
|
+
}
|
|
469
|
+
if (data.length < PAGE) break;
|
|
470
|
+
}
|
|
471
|
+
const out = [];
|
|
472
|
+
for (const part of parts) if (part instanceof Promise) for (const each of await part) out.push(each);
|
|
473
|
+
else out.push(part);
|
|
474
|
+
return out;
|
|
475
|
+
};
|
|
476
|
+
/** Yields objects one listing page at a time, depth first, in `list` order. */
|
|
477
|
+
async function* stream(folder, signal) {
|
|
478
|
+
for (let offset = 0;; offset += PAGE) {
|
|
479
|
+
const data = await page(folder, offset, signal);
|
|
480
|
+
for (const item of data) {
|
|
481
|
+
const path = folder ? `${folder}/${item.name}` : item.name;
|
|
482
|
+
if (item.id === null) yield* stream(path, signal);
|
|
483
|
+
else yield toObject(path, item);
|
|
484
|
+
}
|
|
485
|
+
if (data.length < PAGE) return;
|
|
486
|
+
}
|
|
487
|
+
}
|
|
488
|
+
const list = (within, options) => AsyncResult.from(async () => ok(await walk(bucket.prefix(scopedWithin(within)), options?.signal, pool(LIST_CONCURRENCY)))).mapError((error) => ({
|
|
489
|
+
...error,
|
|
490
|
+
table: bucket.id
|
|
491
|
+
}));
|
|
492
|
+
const signedUrl = (target, options) => AsyncResult.from(async () => {
|
|
493
|
+
const path = resolve(target);
|
|
494
|
+
const ttl = ttlSeconds(options?.ttl);
|
|
495
|
+
const key = signed ? JSON.stringify([
|
|
496
|
+
path,
|
|
497
|
+
ttl,
|
|
498
|
+
options?.transform,
|
|
499
|
+
options?.download,
|
|
500
|
+
options?.cacheNonce,
|
|
501
|
+
options?.versionId
|
|
502
|
+
]) : "";
|
|
503
|
+
const cached = signed?.get(key);
|
|
504
|
+
if (cached) {
|
|
505
|
+
if (cached.until > Date.now()) return ok(cached.url);
|
|
506
|
+
signed?.delete(key);
|
|
507
|
+
}
|
|
508
|
+
const result = await run(() => api().createSignedUrl(path, ttl, urlExtras(options))).map((data) => data.signedUrl);
|
|
509
|
+
if (signed && result.ok) {
|
|
510
|
+
const margin = Math.min(60, ttl / 10);
|
|
511
|
+
if (signed.size >= SIGNED_CACHE_SIZE) {
|
|
512
|
+
const oldest = signed.keys().next();
|
|
513
|
+
if (!oldest.done) signed.delete(oldest.value);
|
|
514
|
+
}
|
|
515
|
+
signed.set(key, {
|
|
516
|
+
url: result.data,
|
|
517
|
+
until: Date.now() + (ttl - margin) * 1e3
|
|
518
|
+
});
|
|
519
|
+
}
|
|
520
|
+
return result;
|
|
521
|
+
});
|
|
522
|
+
return {
|
|
523
|
+
bucket,
|
|
524
|
+
path: (target) => {
|
|
525
|
+
try {
|
|
526
|
+
return ok(resolve(target));
|
|
527
|
+
} catch (cause) {
|
|
528
|
+
return err(toDbError(cause));
|
|
529
|
+
}
|
|
530
|
+
},
|
|
531
|
+
upload,
|
|
532
|
+
download: (target, options) => AsyncResult.from(async () => {
|
|
533
|
+
const path = resolve(target);
|
|
534
|
+
return run(() => api().download(path, options?.versionId ? { versionId: options.versionId } : {}, options?.signal ? { signal: options.signal } : {}));
|
|
535
|
+
}),
|
|
536
|
+
exists: (target) => AsyncResult.from(async () => {
|
|
537
|
+
const path = resolve(target);
|
|
538
|
+
let found;
|
|
539
|
+
try {
|
|
540
|
+
found = await api().exists(path);
|
|
541
|
+
} catch (cause) {
|
|
542
|
+
return err(fromStorageError(cause, bucket.id));
|
|
543
|
+
}
|
|
544
|
+
if (found.data && !found.error) return ok(true);
|
|
545
|
+
if (!found.error) return ok(false);
|
|
546
|
+
const error = fromStorageError(found.error, bucket.id);
|
|
547
|
+
return error.status === 400 || error.status === 404 ? ok(false) : err(error);
|
|
548
|
+
}),
|
|
549
|
+
remove,
|
|
550
|
+
copy: transfer("copy"),
|
|
551
|
+
move: transfer("move"),
|
|
552
|
+
versions,
|
|
553
|
+
removeVersions: (target, versionIds) => AsyncResult.from(async () => {
|
|
554
|
+
const path = resolve(target);
|
|
555
|
+
if (versionIds.length === 0) return ok([]);
|
|
556
|
+
return run(() => api().remove(versionIds.map((versionId) => ({
|
|
557
|
+
path,
|
|
558
|
+
versionId
|
|
559
|
+
})))).map(() => {
|
|
560
|
+
evict([path]);
|
|
561
|
+
return versionIds;
|
|
562
|
+
});
|
|
563
|
+
}),
|
|
564
|
+
purgeCache: (target, options) => AsyncResult.from(async () => {
|
|
565
|
+
const path = resolve(target);
|
|
566
|
+
return run(() => api().purgeCache(path, options?.transformations ? { transformations: true } : {}, options?.signal ? { signal: options.signal } : {})).map(() => void 0);
|
|
567
|
+
}),
|
|
568
|
+
list,
|
|
569
|
+
signedUrl,
|
|
570
|
+
signedUrls: (targets, options) => AsyncResult.from(async () => {
|
|
571
|
+
const paths = targets.map(resolve);
|
|
572
|
+
if (paths.length === 0) return ok([]);
|
|
573
|
+
return run(() => api().createSignedUrls(paths, ttlSeconds(options?.ttl), {
|
|
574
|
+
...download(options?.download),
|
|
575
|
+
...options?.cacheNonce ? { cacheNonce: options.cacheNonce } : {}
|
|
576
|
+
})).andThen((data) => {
|
|
577
|
+
const failed = data.find((entry) => entry.error != null || !entry.signedUrl);
|
|
578
|
+
if (failed) return Promise.resolve(err(dbError("not_found", failed.error ?? `No URL for ${String(failed.path)}`, { table: bucket.id })));
|
|
579
|
+
return Promise.resolve(ok(data.map((entry) => entry.signedUrl)));
|
|
580
|
+
});
|
|
581
|
+
}),
|
|
582
|
+
publicUrl(target, options) {
|
|
583
|
+
try {
|
|
584
|
+
return ok(api().getPublicUrl(resolve(target), urlExtras(options)).data.publicUrl);
|
|
585
|
+
} catch (cause) {
|
|
586
|
+
return err(toDbError(cause));
|
|
587
|
+
}
|
|
588
|
+
},
|
|
589
|
+
renderUrl(target, transform, options) {
|
|
590
|
+
if (!bucket.public) return signedUrl(target, {
|
|
591
|
+
...options,
|
|
592
|
+
transform
|
|
593
|
+
});
|
|
594
|
+
return AsyncResult.from(() => Promise.resolve(ok(api().getPublicUrl(resolve(target), urlExtras({
|
|
595
|
+
...options,
|
|
596
|
+
transform
|
|
597
|
+
})).data.publicUrl)));
|
|
598
|
+
},
|
|
599
|
+
replace: (target, body, options = {}) => AsyncResult.from(async () => {
|
|
600
|
+
const path = resolve(target);
|
|
601
|
+
const previous = options.previous == null ? null : resolve(options.previous);
|
|
602
|
+
const same = previous === path;
|
|
603
|
+
const uploaded = await upload(path, body, {
|
|
604
|
+
...options,
|
|
605
|
+
upsert: same || (options.upsert ?? false)
|
|
606
|
+
});
|
|
607
|
+
if (!uploaded.ok) return uploaded;
|
|
608
|
+
const undo = async (error) => {
|
|
609
|
+
if (!same) await remove([path]);
|
|
610
|
+
return err(error);
|
|
611
|
+
};
|
|
612
|
+
try {
|
|
613
|
+
options.signal?.throwIfAborted();
|
|
614
|
+
const outcome = await options.commit?.(path);
|
|
615
|
+
if (isErrorResult(outcome)) return await undo(outcome.error);
|
|
616
|
+
} catch (cause) {
|
|
617
|
+
return undo(fromStorageError(cause, bucket.id));
|
|
618
|
+
}
|
|
619
|
+
if (!previous || same) return ok({
|
|
620
|
+
path,
|
|
621
|
+
removed: null
|
|
622
|
+
});
|
|
623
|
+
const removed = await run(() => api().remove([previous]));
|
|
624
|
+
if (removed.ok) evict([previous]);
|
|
625
|
+
return ok(removed.ok ? {
|
|
626
|
+
path,
|
|
627
|
+
removed: previous
|
|
628
|
+
} : {
|
|
629
|
+
path,
|
|
630
|
+
removed: null,
|
|
631
|
+
cleanup: removed.error
|
|
632
|
+
});
|
|
633
|
+
}),
|
|
634
|
+
reserve: (target, options) => AsyncResult.from(async () => {
|
|
635
|
+
const path = resolve(target);
|
|
636
|
+
return run(() => api().createSignedUploadUrl(path, options?.upsert ? { upsert: true } : void 0)).map((data) => ({
|
|
637
|
+
path: data.path,
|
|
638
|
+
token: data.token,
|
|
639
|
+
signedUrl: data.signedUrl
|
|
640
|
+
}));
|
|
641
|
+
}),
|
|
642
|
+
uploadReserved: (reservation, body, options) => AsyncResult.from(async () => {
|
|
643
|
+
options?.signal?.throwIfAborted();
|
|
644
|
+
const path = resolve(reservation.path);
|
|
645
|
+
const problem = bucket.check(bodyInfo(body, options?.contentType));
|
|
646
|
+
if (problem) return err(problem);
|
|
647
|
+
return run(() => api().uploadToSignedUrl(path, reservation.token, body, fileOptions(options, options?.contentType))).map(() => {
|
|
648
|
+
evict([path]);
|
|
649
|
+
return { path };
|
|
650
|
+
});
|
|
651
|
+
}),
|
|
652
|
+
sweep: (options) => AsyncResult.from(async () => {
|
|
653
|
+
const namespace = optionalTemporal();
|
|
654
|
+
if (namespace === void 0) return err(temporalMissing());
|
|
655
|
+
const now = options.now?.() ?? namespace.Now.instant();
|
|
656
|
+
const cutoff = options.olderThan instanceof namespace.Instant ? options.olderThan.epochMilliseconds : now.epochMilliseconds - options.olderThan.total("milliseconds");
|
|
657
|
+
const scope = scopedWithin(options.within);
|
|
658
|
+
const size = options.batchSize ?? 100;
|
|
659
|
+
const orphans = [];
|
|
660
|
+
const removed = [];
|
|
661
|
+
let scanned = 0;
|
|
662
|
+
let batch = [];
|
|
663
|
+
const check = async () => {
|
|
664
|
+
options.signal?.throwIfAborted();
|
|
665
|
+
const keep = new Set(await options.referenced(batch));
|
|
666
|
+
for (const path of batch) if (!keep.has(path)) orphans.push(path);
|
|
667
|
+
batch = [];
|
|
668
|
+
};
|
|
669
|
+
try {
|
|
670
|
+
for await (const object of stream(bucket.prefix(scope), options.signal)) {
|
|
671
|
+
scanned++;
|
|
672
|
+
if (!inScope(bucket.match(object.path), scope)) continue;
|
|
673
|
+
const created = Date.parse(object.createdAt ?? object.updatedAt ?? "");
|
|
674
|
+
if (!Number.isFinite(created) || created >= cutoff) continue;
|
|
675
|
+
batch.push(object.path);
|
|
676
|
+
if (batch.length >= size) await check();
|
|
677
|
+
}
|
|
678
|
+
if (batch.length > 0) await check();
|
|
679
|
+
} catch (cause) {
|
|
680
|
+
return err({
|
|
681
|
+
...toDbError(cause),
|
|
682
|
+
table: bucket.id
|
|
683
|
+
});
|
|
684
|
+
}
|
|
685
|
+
for (let index = 0; index < orphans.length && !options.dryRun;) {
|
|
686
|
+
options.signal?.throwIfAborted();
|
|
687
|
+
const chunk = orphans.slice(index, index + size);
|
|
688
|
+
const result = await run(() => api().remove(chunk));
|
|
689
|
+
if (!result.ok) return result;
|
|
690
|
+
evict(chunk);
|
|
691
|
+
removed.push(...chunk);
|
|
692
|
+
index += size;
|
|
693
|
+
}
|
|
694
|
+
return ok({
|
|
695
|
+
scanned,
|
|
696
|
+
orphans,
|
|
697
|
+
removed
|
|
698
|
+
});
|
|
699
|
+
})
|
|
700
|
+
};
|
|
701
|
+
}
|
|
702
|
+
//#endregion
|
|
9
703
|
//#region src/storage/policy.ts
|
|
10
704
|
/**
|
|
11
|
-
* The checks of
|
|
12
|
-
*
|
|
13
|
-
* path has none.
|
|
705
|
+
* The checks of an access policy, or `undefined` for a named policy.
|
|
706
|
+
* `segmentOf` returns the tenant segment, or throws when the path has none.
|
|
14
707
|
*/
|
|
15
708
|
function policyChecks(config, policy, segmentOf) {
|
|
16
709
|
if (typeof policy === "string") return void 0;
|
|
17
|
-
|
|
18
|
-
|
|
19
|
-
|
|
20
|
-
|
|
21
|
-
|
|
22
|
-
check = (key) => permdockCheck(where, policy, key, id, config.catalog);
|
|
23
|
-
keys = policy.permdock;
|
|
24
|
-
} else {
|
|
25
|
-
const segment = policy.segment ?? (policy.scope === "platform" ? void 0 : segmentOf("permission"));
|
|
26
|
-
check = (key) => segment === void 0 ? `(select better_supabase.is_platform(${sqlString(key)}))` : `split_part(name, '/', ${String(segment)}) in (select t::text from better_supabase.tenant_ids_with(${sqlString(key)}) t)`;
|
|
27
|
-
keys = policy.access;
|
|
28
|
-
}
|
|
710
|
+
const where = `defineBucket(${config.id})`;
|
|
711
|
+
const segment = policy.segment ?? (policy.scope === "platform" ? void 0 : segmentOf("permission"));
|
|
712
|
+
const id = segment === void 0 || policy.scope === "platform" ? void 0 : `split_part(name, '/', ${String(segment)})`;
|
|
713
|
+
const check = (key) => accessCheck(where, policy, key, id);
|
|
714
|
+
const keys = policy.access;
|
|
29
715
|
return {
|
|
30
716
|
read: check(keys.read),
|
|
31
717
|
...keys.list === void 0 ? {} : { list: check(keys.list) },
|
|
@@ -37,16 +723,8 @@ function policyChecks(config, policy, segmentOf) {
|
|
|
37
723
|
//#region src/storage/tenant-scope.ts
|
|
38
724
|
const forbidden = (bucket, message) => new DbException(dbError("forbidden", message, { table: bucket }));
|
|
39
725
|
function resolveTenant(options, claim) {
|
|
40
|
-
const { context } = options;
|
|
41
726
|
if (options.tenant !== void 0) return options.tenant;
|
|
42
|
-
|
|
43
|
-
const tenant = tenantOf(context);
|
|
44
|
-
if (tenant !== void 0) return tenant;
|
|
45
|
-
const paths = typeof claim === "string" ? [claim] : claim ?? tenantClaimPaths();
|
|
46
|
-
for (const path of paths) {
|
|
47
|
-
const value = claimAt(context.claims, path);
|
|
48
|
-
if (value !== void 0) return value;
|
|
49
|
-
}
|
|
727
|
+
return options.context && tenantFrom(options.context, tenantPathsFor(claim));
|
|
50
728
|
}
|
|
51
729
|
/**
|
|
52
730
|
* The guard for a client of bucket `bucket` whose template holds the tenant
|
|
@@ -84,12 +762,6 @@ const LIST_OPERATIONS = [
|
|
|
84
762
|
"object.list_v2",
|
|
85
763
|
"s3.object.list"
|
|
86
764
|
];
|
|
87
|
-
const TTL = {
|
|
88
|
-
minute: 60,
|
|
89
|
-
hour: 3600,
|
|
90
|
-
day: 86400,
|
|
91
|
-
week: 604800
|
|
92
|
-
};
|
|
93
765
|
const SIZE_UNITS = {
|
|
94
766
|
b: 1,
|
|
95
767
|
kb: 1e3,
|
|
@@ -103,13 +775,8 @@ function parseSize(value) {
|
|
|
103
775
|
if (typeof value === "number") return value;
|
|
104
776
|
const match = /^\s*(\d+(?:\.\d+)?)\s*([a-z]*)\s*$/i.exec(value);
|
|
105
777
|
const unit = SIZE_UNITS[(match?.[2] || "b").toLowerCase()];
|
|
106
|
-
if (!match || unit === void 0) throw new TypeError(`Invalid size "${value}"`);
|
|
107
|
-
return Math.round(Number(match[1]) * unit);
|
|
108
|
-
}
|
|
109
|
-
const SAFE_SEGMENT = /^[\w!\-.*'() &$@=;:+,?]+$/;
|
|
110
|
-
function validateSegment(_name, value) {
|
|
111
|
-
if (value === "." || value === "..") return "is a relative path";
|
|
112
|
-
if (!SAFE_SEGMENT.test(value)) return "contains characters Storage does not allow";
|
|
778
|
+
if (!match || unit === void 0) throw new TypeError(`Invalid size "${value}"`);
|
|
779
|
+
return Math.round(Number(match[1]) * unit);
|
|
113
780
|
}
|
|
114
781
|
function claimSql(claim) {
|
|
115
782
|
const expressions = (typeof claim === "string" ? [claim] : claim).map((path) => {
|
|
@@ -119,6 +786,16 @@ function claimSql(claim) {
|
|
|
119
786
|
});
|
|
120
787
|
return expressions.length === 1 ? expressions[0] : `coalesce(${expressions.join(", ")})`;
|
|
121
788
|
}
|
|
789
|
+
/**
|
|
790
|
+
* Path segment `index` equals `value`. A first segment also bounds `name` to
|
|
791
|
+
* the `value/` prefix in the "C" collation, the storage name indexes', so
|
|
792
|
+
* Postgres range-scans one tenant's objects instead of the whole bucket;
|
|
793
|
+
* `like` can't, because its pattern only exists at run time.
|
|
794
|
+
*/
|
|
795
|
+
function segmentCheck(index, value) {
|
|
796
|
+
const equal = `split_part(name, '/', ${String(index)}) = ${value}`;
|
|
797
|
+
return index === 1 ? `name collate "C" >= (${value} || '/') and name collate "C" < (${value} || '0') and ${equal}` : equal;
|
|
798
|
+
}
|
|
122
799
|
function mimeAllowed(allowed, type) {
|
|
123
800
|
const base = type.split(";")[0].trim().toLowerCase();
|
|
124
801
|
return allowed.some((entry) => {
|
|
@@ -134,43 +811,6 @@ function sizeLabel(bytes) {
|
|
|
134
811
|
]) if (bytes >= size && bytes % size === 0) return `${String(bytes / size)}${unit}`;
|
|
135
812
|
return `${String(bytes)}B`;
|
|
136
813
|
}
|
|
137
|
-
/** Maps a Storage error (`StorageApiError`, fetch failures) to a `DbError`. */
|
|
138
|
-
function fromStorageError(raw, table) {
|
|
139
|
-
if (isDbError(raw)) return raw;
|
|
140
|
-
if (raw instanceof DbException) return raw.error;
|
|
141
|
-
if (typeof raw !== "object" || raw === null) return toDbError(raw);
|
|
142
|
-
const failure = raw;
|
|
143
|
-
if (failure.name === "AbortError") return toDbError(raw);
|
|
144
|
-
const message = failure.message ?? "Storage request failed";
|
|
145
|
-
const code = failure.code ?? failure.statusCode;
|
|
146
|
-
const base = {
|
|
147
|
-
...code ? { code } : {},
|
|
148
|
-
...table ? { table } : {}
|
|
149
|
-
};
|
|
150
|
-
const statusCode = Number(failure.statusCode);
|
|
151
|
-
const status = Number.isFinite(statusCode) && statusCode >= 400 ? statusCode : failure.status ?? 0;
|
|
152
|
-
if (/row-level security|unauthorized to|AccessDenied/i.test(`${message} ${code ?? ""}`)) return dbError("forbidden", message, base);
|
|
153
|
-
if (/already exists|duplicate/i.test(`${message} ${code ?? ""}`)) return dbError("conflict", message, base);
|
|
154
|
-
if (failure.name === "StorageUnknownError" && status === 0) return dbError("network", message, base);
|
|
155
|
-
switch (status) {
|
|
156
|
-
case 400: return dbError("invalid_request", message, base);
|
|
157
|
-
case 401: return dbError("unauthorized", message, base);
|
|
158
|
-
case 403: return dbError("forbidden", message, base);
|
|
159
|
-
case 404: return dbError("not_found", message, base);
|
|
160
|
-
case 409: return dbError("conflict", message, base);
|
|
161
|
-
case 413:
|
|
162
|
-
case 415: return dbError("invalid_input", message, {
|
|
163
|
-
...base,
|
|
164
|
-
status
|
|
165
|
-
});
|
|
166
|
-
case 408:
|
|
167
|
-
case 429: return dbError("network", message, {
|
|
168
|
-
...base,
|
|
169
|
-
status
|
|
170
|
-
});
|
|
171
|
-
default: return status >= 500 ? dbError("network", message, base) : dbError("unexpected", message, base);
|
|
172
|
-
}
|
|
173
|
-
}
|
|
174
814
|
/**
|
|
175
815
|
* A typed Storage bucket: path builders from a template, limits, generated
|
|
176
816
|
* SQL policies and `config.toml`, and helpers for the multi-step work apps
|
|
@@ -179,7 +819,7 @@ function fromStorageError(raw, table) {
|
|
|
179
819
|
* ```ts
|
|
180
820
|
* export const logos = defineBucket({
|
|
181
821
|
* id: 'customer-logos',
|
|
182
|
-
* path: '{
|
|
822
|
+
* path: '{organizationId}/{customerId}/logo/{version}.webp',
|
|
183
823
|
* policy: 'tenant',
|
|
184
824
|
* fileSizeLimit: '5MiB',
|
|
185
825
|
* allowedMimeTypes: ['image/webp'],
|
|
@@ -187,41 +827,32 @@ function fromStorageError(raw, table) {
|
|
|
187
827
|
* ```
|
|
188
828
|
*/
|
|
189
829
|
function defineBucket(config) {
|
|
190
|
-
const
|
|
830
|
+
const layouts = pathLayouts(config.id, config.path, sqlString);
|
|
191
831
|
const policy = config.policy ?? "none";
|
|
192
832
|
const fileSizeLimit = config.fileSizeLimit === void 0 ? void 0 : parseSize(config.fileSizeLimit);
|
|
193
833
|
const allowedMimeTypes = config.allowedMimeTypes;
|
|
194
|
-
|
|
195
|
-
|
|
196
|
-
|
|
197
|
-
|
|
198
|
-
|
|
199
|
-
|
|
200
|
-
if (tenantParam !== void 0 && !template.params.includes(tenantParam)) throw new TypeError(`defineBucket: tenant.param {${tenantParam}} is not in "${config.path}"`);
|
|
834
|
+
const versioning = config.versioning ?? false;
|
|
835
|
+
if (config.lifecycle && !versioning) throw new TypeError(`defineBucket("${config.id}"): lifecycle expires noncurrent versions, so it needs versioning: true`);
|
|
836
|
+
const lifecycle = config.lifecycle ? lifecyclePayload(config.id, config.lifecycle) : void 0;
|
|
837
|
+
const segmentFor = (param, kind) => layouts.segmentOf(param, kind);
|
|
838
|
+
const tenantParam = config.tenant ? config.tenant.param ?? "organizationId" : void 0;
|
|
839
|
+
if (tenantParam !== void 0) layouts.requireParam(tenantParam, "tenant.param");
|
|
201
840
|
const tenantClaim = config.tenant?.claim;
|
|
202
|
-
const mode = typeof policy === "string" ? policy : "
|
|
841
|
+
const mode = typeof policy === "string" ? policy : "access";
|
|
203
842
|
const accessCheck = (() => {
|
|
204
843
|
switch (mode) {
|
|
205
|
-
case "tenant": {
|
|
206
|
-
|
|
207
|
-
const expression = config.tenant?.sql ?? claimSql(config.tenant?.claim ?? tenantClaimPaths());
|
|
208
|
-
return `split_part(name, '/', ${String(index)}) = (${expression})`;
|
|
209
|
-
}
|
|
210
|
-
case "owner": {
|
|
211
|
-
const index = segmentFor(config.owner?.param ?? "userId", "owner");
|
|
212
|
-
return `split_part(name, '/', ${String(index)}) = (select auth.uid())::text`;
|
|
213
|
-
}
|
|
844
|
+
case "tenant": return segmentCheck(segmentFor(config.tenant?.param ?? "organizationId", "tenant"), `(${config.tenant?.sql ?? claimSql(config.tenant?.claim ?? tenantClaimPaths())})`);
|
|
845
|
+
case "owner": return segmentCheck(segmentFor(config.owner?.param ?? "userId", "owner"), "(select auth.uid())::text");
|
|
214
846
|
case "public":
|
|
215
847
|
case "none":
|
|
216
|
-
case "permdock":
|
|
217
848
|
case "access": return;
|
|
218
849
|
default: throw new TypeError(`defineBucket: unknown policy "${String(mode)}"`);
|
|
219
850
|
}
|
|
220
851
|
})();
|
|
221
|
-
const
|
|
852
|
+
const permission = policyChecks(config, policy, (kind) => segmentFor(config.tenant?.param ?? "organizationId", kind));
|
|
222
853
|
const resolve = (target) => {
|
|
223
|
-
if (typeof target !== "string") return
|
|
224
|
-
if (!
|
|
854
|
+
if (typeof target !== "string") return layouts.build(target);
|
|
855
|
+
if (!layouts.match(target)) throw new DbException(dbError("invalid_input", `Path "${target}" does not match "${layouts.sources.join("\" or \"")}"`));
|
|
225
856
|
return target;
|
|
226
857
|
};
|
|
227
858
|
const check = (file) => {
|
|
@@ -230,23 +861,32 @@ function defineBucket(config) {
|
|
|
230
861
|
};
|
|
231
862
|
const bucket = {
|
|
232
863
|
id: config.id,
|
|
233
|
-
template:
|
|
234
|
-
|
|
235
|
-
|
|
864
|
+
template: layouts.sources[0],
|
|
865
|
+
templates: layouts.sources,
|
|
866
|
+
params: layouts.params,
|
|
867
|
+
owner: policy === "owner" ? config.owner?.param ?? "userId" : layouts.params.includes("userId") ? "userId" : void 0,
|
|
236
868
|
tenant: tenantParam,
|
|
237
869
|
public: config.public ?? false,
|
|
238
870
|
policy,
|
|
239
871
|
fileSizeLimit,
|
|
240
872
|
allowedMimeTypes,
|
|
241
|
-
|
|
242
|
-
|
|
243
|
-
|
|
873
|
+
versioning,
|
|
874
|
+
lifecycle: config.lifecycle,
|
|
875
|
+
path: (values) => {
|
|
876
|
+
try {
|
|
877
|
+
return ok(layouts.build(values));
|
|
878
|
+
} catch (cause) {
|
|
879
|
+
return err(toDbError(cause));
|
|
880
|
+
}
|
|
881
|
+
},
|
|
882
|
+
match: (path) => layouts.match(path),
|
|
883
|
+
prefix: (values = {}) => layouts.prefix(values),
|
|
244
884
|
check,
|
|
245
885
|
sql() {
|
|
246
886
|
const id = sqlString(config.id);
|
|
247
887
|
const name = slug(config.id);
|
|
248
888
|
const lines = [
|
|
249
|
-
`-- better-supabase: bucket ${config.id} (${
|
|
889
|
+
`-- better-supabase: bucket ${config.id} (${layouts.sources.join(", ")})`,
|
|
250
890
|
"insert into storage.buckets (id, name, public, file_size_limit, allowed_mime_types)",
|
|
251
891
|
`values (${id}, ${id}, ${String(bucket.public)}, ${fileSizeLimit === void 0 ? "null" : String(fileSizeLimit)}, ${allowedMimeTypes ? `array[${allowedMimeTypes.map(sqlString).join(", ")}]` : "null"})`,
|
|
252
892
|
"on conflict (id) do update set public = excluded.public, file_size_limit = excluded.file_size_limit,",
|
|
@@ -254,7 +894,7 @@ function defineBucket(config) {
|
|
|
254
894
|
];
|
|
255
895
|
const policies = [];
|
|
256
896
|
const inBucket = `bucket_id = ${id}`;
|
|
257
|
-
const pathMatch =
|
|
897
|
+
const pathMatch = layouts.sqlMatch;
|
|
258
898
|
if (accessCheck) {
|
|
259
899
|
const using = `${inBucket} and ${accessCheck}`;
|
|
260
900
|
const write = `${using} and ${pathMatch}`;
|
|
@@ -291,29 +931,29 @@ function defineBucket(config) {
|
|
|
291
931
|
inBucket,
|
|
292
932
|
void 0
|
|
293
933
|
]);
|
|
294
|
-
else if (
|
|
934
|
+
else if (permission) {
|
|
295
935
|
const listing = `storage.allow_any_operation(array[${LIST_OPERATIONS.map(sqlString).join(", ")}])`;
|
|
296
|
-
if (
|
|
936
|
+
if (permission.list) policies.push([
|
|
297
937
|
"select",
|
|
298
938
|
"select",
|
|
299
939
|
"authenticated",
|
|
300
|
-
`${inBucket} and not ${listing} and ${
|
|
940
|
+
`${inBucket} and not ${listing} and ${permission.read}`,
|
|
301
941
|
void 0
|
|
302
942
|
], [
|
|
303
943
|
"list",
|
|
304
944
|
"select",
|
|
305
945
|
"authenticated",
|
|
306
|
-
`${inBucket} and ${listing} and ${
|
|
946
|
+
`${inBucket} and ${listing} and ${permission.list}`,
|
|
307
947
|
void 0
|
|
308
948
|
]);
|
|
309
949
|
else policies.push([
|
|
310
950
|
"select",
|
|
311
951
|
"select",
|
|
312
952
|
"authenticated",
|
|
313
|
-
`${inBucket} and ${
|
|
953
|
+
`${inBucket} and ${permission.read}`,
|
|
314
954
|
void 0
|
|
315
955
|
]);
|
|
316
|
-
const write = `${inBucket} and ${
|
|
956
|
+
const write = `${inBucket} and ${permission.write}`;
|
|
317
957
|
policies.push([
|
|
318
958
|
"insert",
|
|
319
959
|
"insert",
|
|
@@ -330,7 +970,7 @@ function defineBucket(config) {
|
|
|
330
970
|
"delete",
|
|
331
971
|
"delete",
|
|
332
972
|
"authenticated",
|
|
333
|
-
`${inBucket} and ${
|
|
973
|
+
`${inBucket} and ${permission.delete}`,
|
|
334
974
|
void 0
|
|
335
975
|
]);
|
|
336
976
|
}
|
|
@@ -338,7 +978,7 @@ function defineBucket(config) {
|
|
|
338
978
|
const policyName = sqlIdent(`bs_${name}_${suffix}`);
|
|
339
979
|
lines.push("", `drop policy if exists ${policyName} on storage.objects;`, `create policy ${policyName} on storage.objects for ${command} to ${roles}${using ? `\n using (${using})` : ""}${withCheck ? `\n with check (${withCheck})` : ""};`);
|
|
340
980
|
}
|
|
341
|
-
if (
|
|
981
|
+
if (permission && !permission.list) lines.push("", `drop policy if exists ${sqlIdent(`bs_${name}_list`)} on storage.objects;`);
|
|
342
982
|
return `${lines.join("\n")}\n`;
|
|
343
983
|
},
|
|
344
984
|
toml() {
|
|
@@ -374,257 +1014,88 @@ function defineBucket(config) {
|
|
|
374
1014
|
actual: actual.allowedMimeTypes,
|
|
375
1015
|
message: `Bucket "${config.id}" allowed MIME types differ`
|
|
376
1016
|
});
|
|
1017
|
+
if (actual.versioning !== void 0 && actual.versioning !== null && actual.versioning === "ENABLED" !== versioning) issues.push({
|
|
1018
|
+
field: "versioning",
|
|
1019
|
+
expected: versioning ? "ENABLED" : "DISABLED or SUSPENDED",
|
|
1020
|
+
actual: actual.versioning,
|
|
1021
|
+
message: `Bucket "${config.id}" versioning is ${actual.versioning}`
|
|
1022
|
+
});
|
|
1023
|
+
if (actual.lifecycle !== void 0 && !sameLifecycle(lifecycle, actual.lifecycle)) issues.push({
|
|
1024
|
+
field: "lifecycle",
|
|
1025
|
+
expected: lifecycle ?? null,
|
|
1026
|
+
actual: actual.lifecycle,
|
|
1027
|
+
message: `Bucket "${config.id}" lifecycle policy differs`
|
|
1028
|
+
});
|
|
377
1029
|
return issues;
|
|
378
1030
|
},
|
|
1031
|
+
apply: (client) => applyBucket({
|
|
1032
|
+
id: config.id,
|
|
1033
|
+
public: bucket.public,
|
|
1034
|
+
fileSizeLimit,
|
|
1035
|
+
allowedMimeTypes,
|
|
1036
|
+
versioning,
|
|
1037
|
+
lifecycle
|
|
1038
|
+
}, client),
|
|
379
1039
|
connect: (client, options = {}) => connectBucket(bucket, client, resolve, options, tenantGuard(bucket.id, tenantParam, tenantClaim, (path) => bucket.match(path), options))
|
|
380
1040
|
};
|
|
381
1041
|
return bucket;
|
|
382
1042
|
}
|
|
383
|
-
|
|
384
|
-
|
|
385
|
-
|
|
386
|
-
|
|
387
|
-
|
|
388
|
-
|
|
389
|
-
|
|
390
|
-
|
|
391
|
-
|
|
392
|
-
|
|
393
|
-
|
|
394
|
-
|
|
395
|
-
|
|
396
|
-
|
|
397
|
-
}
|
|
398
|
-
|
|
399
|
-
|
|
400
|
-
|
|
401
|
-
|
|
1043
|
+
//#endregion
|
|
1044
|
+
//#region src/storage/registry.ts
|
|
1045
|
+
/**
|
|
1046
|
+
* A registry of bucket definitions by id, for rows that store a bucket id
|
|
1047
|
+
* next to an object path (files, attachments, sources). An id no bucket
|
|
1048
|
+
* has, and a path outside the bucket's templates, are errors, never a
|
|
1049
|
+
* fallback bucket.
|
|
1050
|
+
*
|
|
1051
|
+
* ```ts
|
|
1052
|
+
* export const buckets = defineBuckets({ files, attachments, logos });
|
|
1053
|
+
*
|
|
1054
|
+
* const ref = buckets.connectStored(supabase, {
|
|
1055
|
+
* bucket: row.bucketId,
|
|
1056
|
+
* path: row.path,
|
|
1057
|
+
* }, { context: db.$context });
|
|
1058
|
+
* if (ref.ok) await ref.data.storage.signedUrl(ref.data.path);
|
|
1059
|
+
* ```
|
|
1060
|
+
*/
|
|
1061
|
+
function defineBuckets(buckets) {
|
|
1062
|
+
const byId = /* @__PURE__ */ new Map();
|
|
1063
|
+
for (const [name, bucket] of Object.entries(buckets)) {
|
|
1064
|
+
if (byId.get(bucket.id) !== void 0) throw new TypeError(`defineBuckets: "${name}" reuses the bucket id "${bucket.id}"`);
|
|
1065
|
+
byId.set(bucket.id, bucket);
|
|
1066
|
+
}
|
|
1067
|
+
const lookup = (id) => {
|
|
1068
|
+
const bucket = byId.get(id);
|
|
1069
|
+
return bucket === void 0 ? err(dbError("invalid_input", `Bucket "${id}" is not registered`)) : ok(bucket);
|
|
402
1070
|
};
|
|
403
|
-
const
|
|
404
|
-
|
|
405
|
-
|
|
406
|
-
|
|
407
|
-
|
|
408
|
-
|
|
409
|
-
|
|
410
|
-
|
|
411
|
-
|
|
412
|
-
return error || data === null ? err(fromStorageError(error, bucket.id)) : ok(data);
|
|
413
|
-
} catch (cause) {
|
|
414
|
-
return err(fromStorageError(cause, bucket.id));
|
|
415
|
-
}
|
|
416
|
-
});
|
|
417
|
-
const fileOptions = (options, contentType) => ({
|
|
418
|
-
...contentType ? { contentType } : {},
|
|
419
|
-
...options?.cacheControl ? { cacheControl: options.cacheControl } : {},
|
|
420
|
-
...options?.metadata ? { metadata: { ...options.metadata } } : {},
|
|
421
|
-
upsert: options?.upsert ?? false
|
|
422
|
-
});
|
|
423
|
-
const download = (value) => value === void 0 ? {} : { download: value };
|
|
424
|
-
const upload = (target, body, options) => AsyncResult.from(async () => {
|
|
425
|
-
options?.signal?.throwIfAborted();
|
|
426
|
-
const path = resolve(target);
|
|
427
|
-
const contentType = options?.contentType;
|
|
428
|
-
const problem = bucket.check(bodyInfo(body, contentType));
|
|
429
|
-
if (problem) return err(problem);
|
|
430
|
-
return run(() => api().upload(path, body, fileOptions(options, contentType))).map(() => ({ path }));
|
|
431
|
-
});
|
|
432
|
-
const remove = (targets) => AsyncResult.from(async () => {
|
|
433
|
-
const paths = targets.map(resolve);
|
|
434
|
-
if (paths.length === 0) return ok([]);
|
|
435
|
-
return run(() => api().remove(paths)).map(() => paths);
|
|
436
|
-
});
|
|
437
|
-
const walk = async (folder, signal, out) => {
|
|
438
|
-
const limit = 1e3;
|
|
439
|
-
for (let offset = 0;; offset += limit) {
|
|
440
|
-
signal?.throwIfAborted();
|
|
441
|
-
const { data, error } = await api().list(folder, {
|
|
442
|
-
limit,
|
|
443
|
-
offset,
|
|
444
|
-
sortBy: {
|
|
445
|
-
column: "name",
|
|
446
|
-
order: "asc"
|
|
447
|
-
}
|
|
448
|
-
}, signal ? { signal } : {});
|
|
449
|
-
if (error) throw new DbException(fromStorageError(error, bucket.id));
|
|
450
|
-
for (const item of data) {
|
|
451
|
-
const path = folder ? `${folder}/${item.name}` : item.name;
|
|
452
|
-
if (item.id === null) await walk(path, signal, out);
|
|
453
|
-
else {
|
|
454
|
-
const metadata = item.metadata ?? {};
|
|
455
|
-
out.push({
|
|
456
|
-
path,
|
|
457
|
-
...typeof metadata.size === "number" ? { size: metadata.size } : {},
|
|
458
|
-
...metadata.mimetype ? { contentType: metadata.mimetype } : {},
|
|
459
|
-
createdAt: item.created_at,
|
|
460
|
-
updatedAt: item.updated_at
|
|
461
|
-
});
|
|
462
|
-
}
|
|
463
|
-
}
|
|
464
|
-
if (data.length < limit) return;
|
|
465
|
-
}
|
|
1071
|
+
const resolve = (ref) => {
|
|
1072
|
+
const found = lookup(ref.bucket);
|
|
1073
|
+
if (!found.ok) return found;
|
|
1074
|
+
const bucket = found.data;
|
|
1075
|
+
if (bucket.match(ref.path) === null) return err(dbError("invalid_input", `Path "${ref.path}" does not match "${bucket.templates.join("\" or \"")}"`, { table: bucket.id }));
|
|
1076
|
+
return ok({
|
|
1077
|
+
bucket,
|
|
1078
|
+
path: ref.path
|
|
1079
|
+
});
|
|
466
1080
|
};
|
|
467
|
-
const list = (within, options) => AsyncResult.from(async () => {
|
|
468
|
-
const out = [];
|
|
469
|
-
await walk(bucket.prefix(scopedWithin(within)), options?.signal, out);
|
|
470
|
-
return ok(out);
|
|
471
|
-
}).mapError((error) => ({
|
|
472
|
-
...error,
|
|
473
|
-
table: bucket.id
|
|
474
|
-
}));
|
|
475
|
-
const signedUrl = (target, options) => AsyncResult.from(async () => {
|
|
476
|
-
const path = resolve(target);
|
|
477
|
-
const ttl = ttlSeconds(options?.ttl);
|
|
478
|
-
const key = signed ? JSON.stringify([
|
|
479
|
-
path,
|
|
480
|
-
ttl,
|
|
481
|
-
options?.transform,
|
|
482
|
-
options?.download
|
|
483
|
-
]) : "";
|
|
484
|
-
const cached = signed?.get(key);
|
|
485
|
-
if (cached && cached.until > Date.now()) return ok(cached.url);
|
|
486
|
-
const result = await run(() => api().createSignedUrl(path, ttl, {
|
|
487
|
-
...download(options?.download),
|
|
488
|
-
...options?.transform ? { transform: { ...options.transform } } : {}
|
|
489
|
-
})).map((data) => data.signedUrl);
|
|
490
|
-
if (signed && result.ok) {
|
|
491
|
-
const margin = Math.min(60, ttl / 10);
|
|
492
|
-
signed.set(key, {
|
|
493
|
-
url: result.data,
|
|
494
|
-
until: Date.now() + (ttl - margin) * 1e3
|
|
495
|
-
});
|
|
496
|
-
}
|
|
497
|
-
return result;
|
|
498
|
-
});
|
|
499
1081
|
return {
|
|
500
|
-
|
|
501
|
-
|
|
502
|
-
|
|
503
|
-
|
|
504
|
-
|
|
505
|
-
|
|
506
|
-
|
|
507
|
-
|
|
508
|
-
|
|
509
|
-
|
|
510
|
-
const path =
|
|
511
|
-
return
|
|
512
|
-
|
|
513
|
-
|
|
514
|
-
|
|
515
|
-
|
|
516
|
-
|
|
517
|
-
return found.error ? err(fromStorageError(found.error, bucket.id)) : ok(true);
|
|
518
|
-
}),
|
|
519
|
-
remove,
|
|
520
|
-
list,
|
|
521
|
-
signedUrl,
|
|
522
|
-
signedUrls: (targets, options) => AsyncResult.from(async () => {
|
|
523
|
-
const paths = targets.map(resolve);
|
|
524
|
-
if (paths.length === 0) return ok([]);
|
|
525
|
-
return run(() => api().createSignedUrls(paths, ttlSeconds(options?.ttl), download(options?.download))).andThen((data) => {
|
|
526
|
-
const failed = data.find((entry) => entry.error != null || !entry.signedUrl);
|
|
527
|
-
if (failed) return Promise.resolve(err(dbError("not_found", failed.error ?? `No URL for ${String(failed.path)}`, { table: bucket.id })));
|
|
528
|
-
return Promise.resolve(ok(data.map((entry) => entry.signedUrl)));
|
|
529
|
-
});
|
|
530
|
-
}),
|
|
531
|
-
publicUrl(target, options) {
|
|
532
|
-
const path = resolve(target);
|
|
533
|
-
return api().getPublicUrl(path, {
|
|
534
|
-
...download(options?.download),
|
|
535
|
-
...options?.transform ? { transform: { ...options.transform } } : {}
|
|
536
|
-
}).data.publicUrl;
|
|
537
|
-
},
|
|
538
|
-
renderUrl(target, transform, options) {
|
|
539
|
-
if (!bucket.public) return signedUrl(target, {
|
|
540
|
-
...options,
|
|
541
|
-
transform
|
|
542
|
-
});
|
|
543
|
-
return AsyncResult.from(() => Promise.resolve(ok(api().getPublicUrl(resolve(target), {
|
|
544
|
-
...download(options?.download),
|
|
545
|
-
transform: { ...transform }
|
|
546
|
-
}).data.publicUrl)));
|
|
547
|
-
},
|
|
548
|
-
replace: (target, body, options = {}) => AsyncResult.from(async () => {
|
|
549
|
-
const path = resolve(target);
|
|
550
|
-
const previous = options.previous ?? null;
|
|
551
|
-
const same = previous === path;
|
|
552
|
-
const uploaded = await upload(path, body, {
|
|
553
|
-
...options,
|
|
554
|
-
upsert: same || (options.upsert ?? false)
|
|
555
|
-
});
|
|
556
|
-
if (!uploaded.ok) return uploaded;
|
|
557
|
-
const undo = async (error) => {
|
|
558
|
-
if (!same) await remove([path]);
|
|
559
|
-
return err(error);
|
|
560
|
-
};
|
|
561
|
-
try {
|
|
562
|
-
options.signal?.throwIfAborted();
|
|
563
|
-
const outcome = await options.commit?.(path);
|
|
564
|
-
if (isErrorResult(outcome)) return await undo(outcome.error);
|
|
565
|
-
} catch (cause) {
|
|
566
|
-
return undo(fromStorageError(cause, bucket.id));
|
|
567
|
-
}
|
|
568
|
-
if (!previous || same) return ok({
|
|
569
|
-
path,
|
|
570
|
-
removed: null
|
|
571
|
-
});
|
|
572
|
-
const removed = await run(() => api().remove([previous]));
|
|
573
|
-
return ok(removed.ok ? {
|
|
574
|
-
path,
|
|
575
|
-
removed: previous
|
|
576
|
-
} : {
|
|
577
|
-
path,
|
|
578
|
-
removed: null,
|
|
579
|
-
cleanup: removed.error
|
|
580
|
-
});
|
|
581
|
-
}),
|
|
582
|
-
reserve: (target, options) => AsyncResult.from(async () => {
|
|
583
|
-
const path = resolve(target);
|
|
584
|
-
return run(() => api().createSignedUploadUrl(path, options?.upsert ? { upsert: true } : void 0)).map((data) => ({
|
|
585
|
-
path: data.path,
|
|
586
|
-
token: data.token,
|
|
587
|
-
signedUrl: data.signedUrl
|
|
588
|
-
}));
|
|
589
|
-
}),
|
|
590
|
-
uploadReserved: (reservation, body, options) => AsyncResult.from(async () => {
|
|
591
|
-
options?.signal?.throwIfAborted();
|
|
592
|
-
const path = resolve(reservation.path);
|
|
593
|
-
const problem = bucket.check(bodyInfo(body, options?.contentType));
|
|
594
|
-
if (problem) return err(problem);
|
|
595
|
-
return run(() => api().uploadToSignedUrl(path, reservation.token, body, fileOptions(options, options?.contentType))).map(() => ({ path }));
|
|
596
|
-
}),
|
|
597
|
-
sweep: (options) => AsyncResult.from(async () => {
|
|
598
|
-
const namespace = optionalTemporal();
|
|
599
|
-
if (namespace === void 0) return err(temporalMissing());
|
|
600
|
-
const now = options.now?.() ?? namespace.Now.instant();
|
|
601
|
-
const cutoff = options.olderThan instanceof namespace.Instant ? options.olderThan.epochMilliseconds : now.epochMilliseconds - options.olderThan.total("milliseconds");
|
|
602
|
-
const found = await list(options.within, options.signal ? { signal: options.signal } : void 0);
|
|
603
|
-
if (!found.ok) return found;
|
|
604
|
-
const candidates = found.data.filter((object) => bucket.match(object.path) !== null).filter((object) => {
|
|
605
|
-
const created = Date.parse(object.createdAt ?? object.updatedAt ?? "");
|
|
606
|
-
return Number.isFinite(created) && created < cutoff;
|
|
607
|
-
}).map((object) => object.path);
|
|
608
|
-
const size = options.batchSize ?? 100;
|
|
609
|
-
const orphans = [];
|
|
610
|
-
const removed = [];
|
|
611
|
-
for (let index = 0; index < candidates.length; index += size) {
|
|
612
|
-
options.signal?.throwIfAborted();
|
|
613
|
-
const batch = candidates.slice(index, index + size);
|
|
614
|
-
const keep = new Set(await options.referenced(batch));
|
|
615
|
-
const unreferenced = batch.filter((path) => !keep.has(path));
|
|
616
|
-
orphans.push(...unreferenced);
|
|
617
|
-
if (options.dryRun || unreferenced.length === 0) continue;
|
|
618
|
-
const result = await run(() => api().remove(unreferenced));
|
|
619
|
-
if (!result.ok) return result;
|
|
620
|
-
removed.push(...unreferenced);
|
|
621
|
-
}
|
|
622
|
-
return ok({
|
|
623
|
-
scanned: found.data.length,
|
|
624
|
-
orphans,
|
|
625
|
-
removed
|
|
626
|
-
});
|
|
627
|
-
})
|
|
1082
|
+
buckets,
|
|
1083
|
+
ids: [...byId.keys()],
|
|
1084
|
+
has: (id) => byId.has(id),
|
|
1085
|
+
byId: lookup,
|
|
1086
|
+
resolve,
|
|
1087
|
+
connectStored(client, ref, options) {
|
|
1088
|
+
const resolved = resolve(ref);
|
|
1089
|
+
if (!resolved.ok) return resolved;
|
|
1090
|
+
const { bucket } = resolved.data;
|
|
1091
|
+
const storage = bucket.connect(client, options);
|
|
1092
|
+
const path = storage.path(ref.path);
|
|
1093
|
+
return path.ok ? ok({
|
|
1094
|
+
bucket,
|
|
1095
|
+
storage,
|
|
1096
|
+
path: path.data
|
|
1097
|
+
}) : path;
|
|
1098
|
+
}
|
|
628
1099
|
};
|
|
629
1100
|
}
|
|
630
1101
|
//#endregion
|
|
@@ -638,7 +1109,7 @@ const IMAGE_TYPES = [
|
|
|
638
1109
|
"image/avif"
|
|
639
1110
|
];
|
|
640
1111
|
const AVATAR_PATH = "{userId}/avatar-{version}.{ext}";
|
|
641
|
-
const LOGO_PATH = "{
|
|
1112
|
+
const LOGO_PATH = "{organizationId}/logo-{version}.{ext}";
|
|
642
1113
|
/**
|
|
643
1114
|
* A bucket for profile pictures: `{userId}/avatar-{version}.{ext}`, public,
|
|
644
1115
|
* 2 MiB of images, written only by their owner (the `owner` policy).
|
|
@@ -654,13 +1125,13 @@ function avatarBucket(options = {}) {
|
|
|
654
1125
|
});
|
|
655
1126
|
}
|
|
656
1127
|
/**
|
|
657
|
-
* A bucket for organization logos: `{
|
|
1128
|
+
* A bucket for organization logos: `{organizationId}/logo-{version}.{ext}`, public,
|
|
658
1129
|
* 2 MiB of images, written by members with the update permission through
|
|
659
1130
|
* the access contract (`tenant_ids_with`), so it follows the app's roles.
|
|
660
1131
|
*/
|
|
661
|
-
function
|
|
1132
|
+
function organizationLogoBucket(options = {}) {
|
|
662
1133
|
const permission = options.permission ?? "organization.update";
|
|
663
|
-
const tenant = options.tenant === false ? void 0 : options.tenant ?? { param: "
|
|
1134
|
+
const tenant = options.tenant === false ? void 0 : options.tenant ?? { param: "organizationId" };
|
|
664
1135
|
return defineBucket({
|
|
665
1136
|
id: options.id ?? "organization-logos",
|
|
666
1137
|
path: options.path ?? LOGO_PATH,
|
|
@@ -675,4 +1146,192 @@ function orgLogoBucket(options = {}) {
|
|
|
675
1146
|
});
|
|
676
1147
|
}
|
|
677
1148
|
//#endregion
|
|
678
|
-
|
|
1149
|
+
//#region src/storage/analytics-bucket.ts
|
|
1150
|
+
/**
|
|
1151
|
+
* A Storage analytics bucket: Apache Iceberg tables that query engines read
|
|
1152
|
+
* through the Iceberg REST catalog.
|
|
1153
|
+
*
|
|
1154
|
+
* ```ts
|
|
1155
|
+
* export const events = defineAnalyticsBucket({ id: "events" });
|
|
1156
|
+
* await events.connect(admin).apply().orThrow();
|
|
1157
|
+
* ```
|
|
1158
|
+
*/
|
|
1159
|
+
function defineAnalyticsBucket(config) {
|
|
1160
|
+
return {
|
|
1161
|
+
id: config.id,
|
|
1162
|
+
connect: (client) => ({
|
|
1163
|
+
apply: () => AsyncResult.from(async () => {
|
|
1164
|
+
const analytics = client.storage.analytics;
|
|
1165
|
+
try {
|
|
1166
|
+
const listed = await analytics.listBuckets({ search: config.id });
|
|
1167
|
+
if (listed.error) return err(fromStorageError(listed.error, config.id));
|
|
1168
|
+
if (listed.data.some((bucket) => bucket.name === config.id)) return ok({ created: false });
|
|
1169
|
+
const made = await analytics.createBucket(config.id);
|
|
1170
|
+
if (made.error) return err(fromStorageError(made.error, config.id));
|
|
1171
|
+
return ok({ created: true });
|
|
1172
|
+
} catch (cause) {
|
|
1173
|
+
return err(fromStorageError(cause, config.id));
|
|
1174
|
+
}
|
|
1175
|
+
}),
|
|
1176
|
+
catalog: () => client.storage.analytics.from(config.id)
|
|
1177
|
+
})
|
|
1178
|
+
};
|
|
1179
|
+
}
|
|
1180
|
+
//#endregion
|
|
1181
|
+
//#region src/storage/vector-bucket.ts
|
|
1182
|
+
const PUT_BATCH = 500;
|
|
1183
|
+
const GET_BATCH = 100;
|
|
1184
|
+
function chunks(items, size) {
|
|
1185
|
+
const out = [];
|
|
1186
|
+
for (let index = 0; index < items.length; index += size) out.push(items.slice(index, index + size));
|
|
1187
|
+
return out;
|
|
1188
|
+
}
|
|
1189
|
+
/** Batches in flight at once for one `put`, `get` or `remove`. */
|
|
1190
|
+
const CHUNK_CONCURRENCY = 4;
|
|
1191
|
+
/** Runs `run` per item over a few lanes; stops starting items after the first error. */
|
|
1192
|
+
async function inLanes(items, run) {
|
|
1193
|
+
const out = [];
|
|
1194
|
+
let next = 0;
|
|
1195
|
+
let failed;
|
|
1196
|
+
const lane = async () => {
|
|
1197
|
+
while (!failed && next < items.length) {
|
|
1198
|
+
const index = next++;
|
|
1199
|
+
const result = await run(items[index]);
|
|
1200
|
+
if (!result.ok) {
|
|
1201
|
+
failed ??= result;
|
|
1202
|
+
return;
|
|
1203
|
+
}
|
|
1204
|
+
out[index] = result.data;
|
|
1205
|
+
}
|
|
1206
|
+
};
|
|
1207
|
+
await Promise.all(Array.from({ length: Math.min(CHUNK_CONCURRENCY, items.length) }, lane));
|
|
1208
|
+
return failed ?? ok(out);
|
|
1209
|
+
}
|
|
1210
|
+
const metricOf = (config) => config.distanceMetric ?? "cosine";
|
|
1211
|
+
/**
|
|
1212
|
+
* A Storage vector bucket with typed indexes. Storage keeps the vectors
|
|
1213
|
+
* outside Postgres; use the `vector-search` module for pgvector in a table.
|
|
1214
|
+
*
|
|
1215
|
+
* ```ts
|
|
1216
|
+
* export const embeddings = defineVectorBucket({
|
|
1217
|
+
* id: "embeddings",
|
|
1218
|
+
* indexes: { documents: { dimension: 1536 } },
|
|
1219
|
+
* });
|
|
1220
|
+
* const docs = embeddings.connect(admin).index("documents");
|
|
1221
|
+
* await docs.put([{ key: doc.id, vector, metadata: { organizationId } }]);
|
|
1222
|
+
* ```
|
|
1223
|
+
*/
|
|
1224
|
+
function defineVectorBucket(config) {
|
|
1225
|
+
for (const [name, index] of Object.entries(config.indexes)) if (!Number.isInteger(index.dimension) || index.dimension < 1) throw new TypeError(`defineVectorBucket("${config.id}"): index "${name}" needs a whole, positive dimension`);
|
|
1226
|
+
return {
|
|
1227
|
+
id: config.id,
|
|
1228
|
+
indexes: config.indexes,
|
|
1229
|
+
connect: (client) => connectVectorBucket(config, client)
|
|
1230
|
+
};
|
|
1231
|
+
}
|
|
1232
|
+
function connectVectorBucket(config, client) {
|
|
1233
|
+
const vectors = () => client.storage.vectors;
|
|
1234
|
+
const scope = () => vectors().from(config.id);
|
|
1235
|
+
const failure = (raw) => fromStorageError(raw, config.id);
|
|
1236
|
+
const attempt = (fn) => AsyncResult.from(async () => {
|
|
1237
|
+
try {
|
|
1238
|
+
return await fn();
|
|
1239
|
+
} catch (cause) {
|
|
1240
|
+
return err(failure(cause));
|
|
1241
|
+
}
|
|
1242
|
+
});
|
|
1243
|
+
return {
|
|
1244
|
+
apply: () => attempt(async () => {
|
|
1245
|
+
const created = [];
|
|
1246
|
+
const found = await vectors().getBucket(config.id);
|
|
1247
|
+
if (found.error) {
|
|
1248
|
+
if (failure(found.error).kind !== "not_found") return err(failure(found.error));
|
|
1249
|
+
const made = await vectors().createBucket(config.id);
|
|
1250
|
+
if (made.error) return err(failure(made.error));
|
|
1251
|
+
}
|
|
1252
|
+
const entries = Object.entries(config.indexes);
|
|
1253
|
+
for (const [name, index] of entries) {
|
|
1254
|
+
const existing = await scope().getIndex(name);
|
|
1255
|
+
if (existing.data) {
|
|
1256
|
+
const actual = existing.data.index;
|
|
1257
|
+
if (actual.dimension !== index.dimension || actual.distanceMetric !== metricOf(index)) return err(dbError("conflict", `Vector index "${name}" has dimension ${String(actual.dimension)} and metric ${actual.distanceMetric}, not ${String(index.dimension)} and ${metricOf(index)}`, { table: config.id }));
|
|
1258
|
+
continue;
|
|
1259
|
+
}
|
|
1260
|
+
if (failure(existing.error).kind !== "not_found") return err(failure(existing.error));
|
|
1261
|
+
const made = await scope().createIndex({
|
|
1262
|
+
indexName: name,
|
|
1263
|
+
dataType: "float32",
|
|
1264
|
+
dimension: index.dimension,
|
|
1265
|
+
distanceMetric: metricOf(index),
|
|
1266
|
+
...index.nonFilterableMetadataKeys ? { metadataConfiguration: { nonFilterableMetadataKeys: [...index.nonFilterableMetadataKeys] } } : {}
|
|
1267
|
+
});
|
|
1268
|
+
if (made.error) return err(failure(made.error));
|
|
1269
|
+
created.push(name);
|
|
1270
|
+
}
|
|
1271
|
+
return ok({ created });
|
|
1272
|
+
}),
|
|
1273
|
+
index(name) {
|
|
1274
|
+
const settings = config.indexes[name];
|
|
1275
|
+
const api = () => scope().index(name);
|
|
1276
|
+
const checkDimension = (vector, key) => vector.length === settings.dimension ? void 0 : dbError("invalid_input", `${key === void 0 ? "The query vector" : `Vector "${key}"`} has ${String(vector.length)} values; index "${name}" takes ${String(settings.dimension)}`, { table: config.id });
|
|
1277
|
+
const hit = (match) => ({
|
|
1278
|
+
key: match.key,
|
|
1279
|
+
...match.distance === void 0 ? {} : { distance: match.distance },
|
|
1280
|
+
...match.metadata === void 0 ? {} : { metadata: match.metadata },
|
|
1281
|
+
...match.data ? { vector: match.data.float32 } : {}
|
|
1282
|
+
});
|
|
1283
|
+
return {
|
|
1284
|
+
name,
|
|
1285
|
+
dimension: settings.dimension,
|
|
1286
|
+
put: (records) => attempt(async () => {
|
|
1287
|
+
for (const record of records) {
|
|
1288
|
+
const problem = checkDimension(record.vector, record.key);
|
|
1289
|
+
if (problem) return err(problem);
|
|
1290
|
+
}
|
|
1291
|
+
const put = await inLanes(chunks(records, PUT_BATCH), async (batch) => {
|
|
1292
|
+
const { error } = await api().putVectors({ vectors: batch.map((record) => ({
|
|
1293
|
+
key: record.key,
|
|
1294
|
+
data: { float32: [...record.vector] },
|
|
1295
|
+
...record.metadata ? { metadata: { ...record.metadata } } : {}
|
|
1296
|
+
})) });
|
|
1297
|
+
return error ? err(failure(error)) : ok(batch.length);
|
|
1298
|
+
});
|
|
1299
|
+
return put.ok ? ok(records.length) : put;
|
|
1300
|
+
}),
|
|
1301
|
+
query: (vector, options = {}) => attempt(async () => {
|
|
1302
|
+
const problem = checkDimension(vector);
|
|
1303
|
+
if (problem) return err(problem);
|
|
1304
|
+
const found = await api().queryVectors({
|
|
1305
|
+
queryVector: { float32: [...vector] },
|
|
1306
|
+
topK: options.topK ?? 10,
|
|
1307
|
+
returnDistance: true,
|
|
1308
|
+
returnMetadata: options.metadata ?? true,
|
|
1309
|
+
...options.filter ? { filter: { ...options.filter } } : {}
|
|
1310
|
+
});
|
|
1311
|
+
if (found.error) return err(failure(found.error));
|
|
1312
|
+
return ok(found.data.vectors.map(hit));
|
|
1313
|
+
}),
|
|
1314
|
+
get: (keys, options = {}) => attempt(async () => {
|
|
1315
|
+
const found = await inLanes(chunks(keys, GET_BATCH), async (batch) => {
|
|
1316
|
+
const { data, error } = await api().getVectors({
|
|
1317
|
+
keys: batch,
|
|
1318
|
+
returnMetadata: true,
|
|
1319
|
+
returnData: options.vector ?? false
|
|
1320
|
+
});
|
|
1321
|
+
return error ? err(failure(error)) : ok(data.vectors.map(hit));
|
|
1322
|
+
});
|
|
1323
|
+
return found.ok ? ok(found.data.flat()) : found;
|
|
1324
|
+
}),
|
|
1325
|
+
remove: (keys) => attempt(async () => {
|
|
1326
|
+
const removed = await inLanes(chunks(keys, PUT_BATCH), async (batch) => {
|
|
1327
|
+
const { error } = await api().deleteVectors({ keys: batch });
|
|
1328
|
+
return error ? err(failure(error)) : ok(batch.length);
|
|
1329
|
+
});
|
|
1330
|
+
return removed.ok ? ok(keys) : removed;
|
|
1331
|
+
})
|
|
1332
|
+
};
|
|
1333
|
+
}
|
|
1334
|
+
};
|
|
1335
|
+
}
|
|
1336
|
+
//#endregion
|
|
1337
|
+
export { IMAGE_TYPES, TTL, avatarBucket, defineAnalyticsBucket, defineBucket, defineBuckets, defineVectorBucket, fromStorageError, organizationLogoBucket, parseSize, storageImageUrl };
|