bknd
Version:
Lightweight Firebase/Supabase alternative built to run anywhere — incl. Next.js, React Router, Astro, Cloudflare, Bun, Node, AWS Lambda & more.
3 lines • 16.1 kB
JavaScript
"use client";
import {$console,omitKeys,encodeSearch,objectTransform,isPlainObject}from'bknd/utils';import {decode}from'hono/jwt';import {createContext,useContext,useMemo,useState}from'react';import {jsx}from'react/jsx-runtime';import z,{useSWRConfig,mutate}from'swr';import G from'swr/infinite';import {useApi,useInvalidate,useClientContext}from'bknd/client';var C=(i,e)=>"number"=="string"?[1,"1","true"].includes(i):false;function D(){try{return C(0)}catch{return false}}var l=class{constructor(e={},t){this._options=e;this.fetcher=t??fetch;}fetcher;getDefaultOptions(){return {}}get options(){return {host:"http://localhost",token:void 0,...this.getDefaultOptions(),...this._options}}key(){return this.options.basepath??""}getUrl(e){let t=this.options.basepath??"";return this.options.host+(t+"/"+e).replace(/\/{2,}/g,"/").replace(/\/$/,"")}request(e,t,n){let r=n?.method??"GET",a=Array.isArray(e)?e.join("/"):e,s=this.getUrl(a);t instanceof URLSearchParams?s+="?"+t.toString():typeof t=="object"&&Object.keys(t).length>0&&(s+="?"+encodeSearch(t));let o=new Headers(this.options.headers??{});for(let[u,y]of Object.entries(n?.headers??{}))o.set(u,y);o.has("Accept")||o.set("Accept","application/json"),this.options.token&&this.options.token_transport==="header"&&o.set("Authorization",`Bearer ${this.options.token}`);let d=n?.body;if(n&&"body"in n&&["POST","PATCH","PUT"].includes(r)){let u=o.get("Content-Type")??void 0;(!u||u.startsWith("application/json")||isPlainObject(d))&&(d=JSON.stringify(n.body),o.set("Content-Type","application/json"));}let p=new Request(s,{...n,credentials:this.options.credentials,method:r,body:d,headers:o});return new c(p,{fetcher:this.fetcher,verbose:this.options.verbose})}get(e,t,n){return this.request(e,t,{...n,method:"GET"})}post(e,t,n){return this.request(e,void 0,{...n,body:t,method:"POST"})}patch(e,t,n){return this.request(e,void 0,{...n,body:t,method:"PATCH"})}put(e,t,n){return this.request(e,void 0,{...n,body:t,method:"PUT"})}delete(e,t,n){return this.request(e,void 0,{...n,body:t,method:"DELETE"})}};function _(i,e,t){let n=typeof t<"u"?t:e,r=["raw","body","ok","status","res","data","toJSON"];return typeof n!="object"&&(n={}),new Proxy(n,{get(a,s,o){return s==="raw"||s==="res"?i:s==="body"?e:s==="data"?t:s==="ok"?i.ok:s==="status"?i.status:s==="toJSON"?()=>a:Reflect.get(a,s,o)},has(a,s){return r.includes(s)?true:Reflect.has(a,s)},ownKeys(a){return Array.from(new Set([...Reflect.ownKeys(a),...r]))},getOwnPropertyDescriptor(a,s){return r.includes(s)?{configurable:true,enumerable:true,value:Reflect.get({raw:i,body:e,ok:i.ok,status:i.status},s)}:Reflect.getOwnPropertyDescriptor(a,s)}})}var c=class i{constructor(e,t,n){this.request=e;this.options=t;this.refineData=n;}[Symbol.toStringTag];get verbose(){return this.options?.verbose??false}refine(e){return new i(this.request,this.options,e)}async execute(){D()&&await new Promise(s=>setTimeout(s,200));let e=this.options?.fetcher??fetch;this.verbose&&$console.debug("[FetchPromise] Request",{method:this.request.method,url:this.request.url});let t=await e(this.request);this.verbose&&$console.debug("[FetchPromise] Response",{res:t,ok:t.ok,status:t.status});let n,r,a=t.headers.get("Content-Type")??"";if(a.startsWith("application/json")?(n=await t.json(),typeof n=="object"&&(r="data"in n?n.data:n)):a.startsWith("text")?n=await t.text():n=t.body,this.refineData)try{r=this.refineData(r);}catch(s){console.warn("[FetchPromise] Error in refineData",s),r=void 0;}return _(t,n,r)}then(e,t){return this.execute().then(e,t)}catch(e){return this.then(void 0,e)}finally(e){return this.then(t=>(e?.(),t),t=>{throw e?.(),t})}path(){return new URL(this.request.url).pathname}key(e){let t=new URL(this.request.url);return e?.search!==false?this.path()+t.search:this.path()}keyArray(e){let t=new URL(this.request.url),n=this.path().split("/");return (e?.search!==false?[...n,t.searchParams.toString()]:n).filter(Boolean)}toString(){return this.key({search:true})}toJSON(){return {url:this.request.url,method:this.request.method}}};var A=class extends l{getDefaultOptions(){return {basepath:"/api/auth",credentials:"include"}}async login(e,t){let n=await this.post([e,"login"],t);return n.ok&&n.body.token&&await this.options.onTokenUpdate?.(n.body.token,true),n}async register(e,t){let n=await this.post([e,"register"],t);return n.ok&&n.body.token&&await this.options.onTokenUpdate?.(n.body.token,true),n}async actionSchema(e,t){return this.get([e,"actions",t,"schema.json"])}async action(e,t,n){return this.post([e,"actions",t],n)}async loginWithPassword(e){return this.login("password",e)}async registerWithPassword(e){return this.register("password",e)}me(){return this.get(["me"])}strategies(){return this.get(["strategies"])}async logout(){return this.get(["logout"],void 0,{headers:{Accept:"application/json"}}).then(()=>this.options.onTokenUpdate?.(void 0,true))}};var k=class extends l{getDefaultOptions(){return {basepath:"/api/data",queryLengthLimit:1e3,defaultQuery:{limit:10}}}requireObjectSet(e,t){if(!e||typeof e!="object"||Object.keys(e).length===0)throw new Error(t??"object is required")}readOne(e,t,n={}){return this.get(["entity",e,t],n)}readOneBy(e,t={}){return this.readMany(e,{...t,limit:1,offset:0}).refine(n=>n[0])}readMany(e,t={}){let n=t??this.options.defaultQuery,r=this.get(["entity",e],n);return r.request.url.length<=this.options.queryLengthLimit?r:this.post(["entity",e,"query"],n)}readManyByReference(e,t,n,r={}){return this.get(["entity",e,t,n],r??this.options.defaultQuery)}createOne(e,t){return this.post(["entity",e],t)}createMany(e,t){if(!t||!Array.isArray(t)||t.length===0)throw new Error("input is required");return this.post(["entity",e],t)}updateOne(e,t,n){if(!t)throw new Error("ID is required");return this.patch(["entity",e,t],n)}updateMany(e,t,n){return this.requireObjectSet(t),this.patch(["entity",e],{update:n,where:t})}deleteOne(e,t){if(!t)throw new Error("ID is required");return this.delete(["entity",e,t])}deleteMany(e,t){return this.requireObjectSet(t),this.delete(["entity",e],t)}count(e,t={}){return this.post(["entity",e,"fn","count"],t)}exists(e,t={}){return this.post(["entity",e,"fn","exists"],t)}};var x=class extends l{getDefaultOptions(){return {basepath:"/api/media",upload_fetcher:fetch,init:{}}}listFiles(){return this.get(["files"])}getFile(e){return this.get(["file",e],void 0,{headers:{Accept:"*/*"}})}async getFileStream(e){let{res:t}=await this.getFile(e);if(!t.ok||!t.body)throw new Error("Failed to fetch file");return t.body}async download(e){let{res:t}=await this.getFile(e);if(!t.ok||!t.body)throw new Error("Failed to fetch file");return await t.blob()}getFileUploadUrl(e){return e?this.getUrl(`/upload/${e.path}`):this.getUrl("/upload")}getEntityUploadUrl(e,t,n){return this.getUrl(`/entity/${e}/${t}/${n}`)}getUploadHeaders(){return this.options.token_transport==="header"&&this.options.token?new Headers({Authorization:`Bearer ${this.options.token}`}):new Headers}uploadFile(e,t){let n={"Content-Type":"application/octet-stream",...t?._init?.headers||{}},r=t?.filename||"";try{typeof e.type<"u"&&(n["Content-Type"]=e.type),t?.filename||(r=e.name);}catch{}r&&r.length>0&&r.includes("/")&&(r=r.split("/").pop()||"");let a={...this.options.init,...t?._init||{},headers:n};if(t?.path)return this.request(t.path,t?.query,{...a,body:e,method:"POST"});if(!r||r.length===0)throw new Error("Invalid filename");return this.request(t?.path??["upload",r],t?.query,{...a,body:e,method:"POST"})}async upload(e,t={}){if(e instanceof Request||typeof e=="string"){let r=await(t.fetcher??this.options.upload_fetcher)(e);if(!r.ok||!r.body)throw new Error("Failed to fetch file");return this.uploadFile(r.body,t)}else if(e instanceof Response){if(!e.body)throw new Error("Invalid response");return this.uploadFile(e.body,{...t??{},_init:{...t._init??{},headers:{...t._init?.headers??{},"Content-Type":e.headers.get("Content-Type")||"application/octet-stream"}}})}return this.uploadFile(e,t)}async uploadToEntity(e,t,n,r,a){let s=a?.overwrite!==void 0?{overwrite:a.overwrite}:void 0;return this.upload(r,{...a,path:["entity",e,t,n],query:s})}deleteFile(e){return this.delete(["file",e])}};var E=class extends l{getDefaultOptions(){return {basepath:"/api/system"}}readConfig(){return this.get("config")}readSchema(e){return this.get("schema",{config:e?.config?1:0,secrets:e?.secrets?1:0,fresh:e?.fresh?1:0})}setConfig(e,t,n){return this.post(["config","set",e].join("/")+`?force=${n?1:0}`,t)}addConfig(e,t,n){return this.post(["config","add",e,t],n)}patchConfig(e,t,n){return this.patch(["config","patch",e,t],n)}overwriteConfig(e,t,n){return this.put(["config","overwrite",e,t],n)}removeConfig(e,t){return this.delete(["config","remove",e,t])}permissions(){return this.get("permissions")}};var h=class{constructor(e={}){this.options=e;this.verified=e.verified===true,"request"in e&&e.request?(this.options.host=e.host??new URL(e.request.url).origin,this.options.headers=e.headers??e.request.headers,this.extractToken()):"token"in e&&e.token?(this.token_transport="header",this.updateToken(e.token,{trigger:false})):"user"in e&&e.user?(this.token_transport="none",this.user=e.user,this.verified=e.verified!==false):this.extractToken(),this.buildApis();}token;user;verified=false;token_transport="header";system;data;auth;media;get fetcher(){return this.options.fetcher??fetch}get baseUrl(){return this.options.host??"http://localhost"}get tokenKey(){return this.options.key??"auth"}extractToken(){if(this.verified=false,this.options.headers){let e=N(this.options.headers.get("cookie"),"auth");if(e){this.token_transport="cookie",this.updateToken(e);return}let t=this.options.headers.get("authorization")?.replace("Bearer ","");if(t){this.token_transport="header",this.updateToken(t);return}}else this.storage&&this.storage.getItem(this.tokenKey).then(e=>{this.token_transport="header",this.updateToken(e?String(e):void 0,{verified:true,trigger:false});});}get storage(){let e=this.options.storage;return new Proxy({},{get(t,n){return (...r)=>{let a=e?e[n](...r):void 0;return a instanceof Promise?a:{then:s=>s(a)}}}})}updateToken(e,t){this.token=e,this.verified=t?.verified===true,e?this.user=omitKeys(decode(e).payload,["iat","iss","exp"]):this.user=void 0;let n=()=>{t?.trigger!==false&&this.options.onAuthStateChange?.(this.getAuthState());};if(this.storage){let r=this.tokenKey;e?this.storage.setItem(r,e).then(n):this.storage.removeItem(r).then(n);}else t?.trigger!==false&&n();t?.rebuild&&this.buildApis();}markAuthVerified(e){return this.verified=e,this.options.onAuthStateChange?.(this.getAuthState()),this}isAuthVerified(){return this.verified}getAuthState(){return {token:this.token,user:this.user,verified:this.verified}}isAuthenticated(){let{token:e,user:t}=this.getAuthState();return !!e&&!!t}async getVerifiedAuthState(){return await this.verifyAuth(),this.getAuthState()}async verifyAuth(){try{let{ok:e,data:t}=await this.auth.me(),n=t?.user;if(!e||!n)throw new Error;this.user=n;}catch{this.updateToken(void 0);}finally{this.markAuthVerified(true);}}getUser(){return this.user||null}getParams(){return Object.freeze({host:this.baseUrl,token:this.token,headers:this.options.headers,token_transport:this.token_transport,verbose:this.options.verbose,credentials:this.options.credentials})}buildApis(){let e=this.getParams(),t=this.options.fetcher;this.system=new E(e,t),this.data=new k({...e,...this.options.data},t),this.auth=new A({...e,...this.options.auth,onTokenUpdate:(n,r)=>{this.updateToken(n,{rebuild:true,verified:r,trigger:true}),this.options.auth?.onTokenUpdate?.(n);}},t),this.media=new x({...e,...this.options.media},t);}};function N(i,e){if(!i)return null;for(let t of i.split("; ")){let[n,r]=t.split("=");if(n===e&&r)return decodeURIComponent(r)}return null}var w=createContext(void 0),J=({children:i,host:e,baseUrl:t=e,api:n,...r})=>{let a=v(),s=T(),o=t??s?.baseUrl??"",d;if(a&&(d=a.user),!o)try{o=window.location.origin;}catch{}let p={user:d,...r,host:o},u=useMemo(()=>n??new h({...p,verbose:D(),onAuthStateChange:f=>{r.onAuthStateChange?.(f),(!y?.token||f.token!==y?.token)&&g(f);}}),[n,JSON.stringify(p)]),[y,g]=useState(u.getAuthState());return jsx(w.Provider,{value:{baseUrl:u.baseUrl,api:u,authState:y},children:i})},R=i=>{let e=useContext(w);if(!e?.api||i&&i.length>0&&i!==e.baseUrl)return console.info("creating new api",{host:i}),new h({host:i??""});if(!e)throw new Error("useApi must be used within a ClientProvider");return e.api},T=()=>useContext(w),$=()=>T()?.baseUrl;function v(){let i={logout_route:"/api/auth/logout",admin_basepath:""};return typeof window<"u"&&window.__BKND__?{...i,...window.__BKND__}:i}var _e=(i,e)=>{let t=R(),n=i(t),r=e?.refine??(d=>d),a=()=>n.execute().then(r),s=n.key();return {...z(e?.enabled===false?null:s,a,e),promise:n,key:s,api:t}},je=(i,e)=>{let[t,n]=useState(false),r=R(),a=p=>i(r,p),s=e?.refine??(p=>p),o=G((p,u)=>p>0&&u&&u.length<(e?.pageSize??0)?(n(true),null):a(p).request.url,p=>new c(new Request(p),{fetcher:r.fetcher},s).execute(),{revalidateFirstPage:false}),d=o.data?[].concat(...o.data):[];return {...o,_data:o.data,data:d,endReached:t,promise:a(o.size),key:a(o.size).key(),api:r}},Le=i=>{let e=useSWRConfig().mutate,t=R();return async n=>{let r="";return typeof n=="string"?r=n:typeof n=="function"&&(r=n(t).key()),i?.exact?e(r):e(a=>typeof a=="string"&&a.startsWith(r))}},B=new Map,Ne=i=>(e,t,n)=>{if(typeof e=="string"){if(B.has(e))return i(e,t,{...n,revalidateOnMount:false});let r=i(e,t,n);return r.data&&B.set(e,true),r}return i(e,t,n)};var m=class extends Error{constructor(t,n){let r=n;"error"in t&&(r=t.error,n&&(r=`${n}: ${r}`));super(r??"UseEntityApiError");this.response=t;}},te=(i,e)=>{let t=useApi().data;return {create:async n=>{let r=await t.createOne(i,n);if(!r.ok)throw new m(r,`Failed to create entity "${i}"`);return r},read:async n=>{let r=e?await t.readOne(i,e,n):await t.readMany(i,n);if(!r.ok)throw new m(r,`Failed to read entity "${i}"`);return r},update:async(n,r=e)=>{if(!r)throw new Error("id is required");let a=await t.updateOne(i,r,n);if(!a.ok)throw new m(a,`Failed to update entity "${i}"`);return a},_delete:async(n=e)=>{if(!n)throw new Error("id is required");let r=await t.deleteOne(i,n);if(!r.ok)throw new m(r,`Failed to delete entity "${i}"`);return r}}};function S(i,e,t,n){return "/"+[...i.options?.basepath?.split("/")??[],e,...t?[t]:[]].filter(Boolean).join("/")+(n?"?"+encodeSearch(n):"")}var ne=(i,e,t,n)=>{let r=useApi().data,a=S(r,i,e,t),{read:s,...o}=te(i,e),d=()=>s(t??{}),p=z(n?.enabled===false?null:a,d,{revalidateOnFocus:false,keepPreviousData:true,...n}),u=async g=>{let f=S(r,i,g);return mutate(b=>typeof b=="string"&&b.startsWith(f),void 0,{revalidate:true})},y=objectTransform(o,g=>async(...f)=>{let b=await g(...f);return n?.revalidateOnMutate!==false&&await u(),b});return {...p,...y,mutate:u,mutateRaw:p.mutate,api:r,key:a}};async function F(i,e,t,n){function r(s,o){return typeof s<"u"&&typeof o<"u"&&"id"in s&&s.id===t?{...s,...o}:s}let a=S(i,e);return mutate(s=>typeof s=="string"&&s.startsWith(a),async s=>{if(!(typeof s>"u"))return Array.isArray(s)?s.map(o=>r(o,n)):r(s,n)},{revalidate:false})}var $e=(i,e,t)=>{let{data:n,...r}=ne(i,e,void 0,{...t,enabled:false});return {...r,mutate:e?s=>F(r.api,i,e,s):(s,o)=>F(r.api,i,s,o)}};var ae=i=>{let e=useApi(i?.baseUrl),t=useInvalidate(),{authState:n}=useClientContext(),r=n?.verified??false;async function a(u){return (await e.auth.login("password",u)).data}async function s(u){return (await e.auth.register("password",u)).data}function o(u){e.updateToken(u);}async function d(){await e.auth.logout(),await t();}async function p(){await e.verifyAuth(),await t();}return {data:n,user:n?.user,token:n?.token,verified:r,login:a,register:s,logout:d,setToken:o,verify:p,local:!!e.options.storage}};
export{h as Api,J as ClientProvider,c as FetchPromise,m as UseEntityApiError,S as makeKey,Ne as mountOnce,F as mutateEntityCache,R as useApi,je as useApiInfiniteQuery,_e as useApiQuery,ae as useAuth,$ as useBaseUrl,v as useBkndWindowContext,T as useClientContext,te as useEntity,$e as useEntityMutate,ne as useEntityQuery,Le as useInvalidate};