muya 2.2.2 → 2.2.4

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.
@@ -1 +1 @@
1
- import{bunMemoryBackend as c}from"../table/bun-backend";import{createTable as r}from"../table/table";describe("table",()=>{let n=c(),e;beforeEach(async()=>{n=c(),e=await r({backend:n,tableName:"TestTable",key:"name"})}),it("should set and get items",async()=>{const a=await e.set({name:"Alice",age:30,city:"Paris"});expect(a.key).toBe("Alice"),expect(a.op).toBe("insert");const i=await e.get("Alice");expect(i).toEqual({name:"Alice",age:30,city:"Paris"});const t=await e.set({name:"Alice",age:31,city:"Paris"});expect(t.key).toBe("Alice"),expect(t.op).toBe("update");const s=await e.get("Alice");expect(s).toEqual({name:"Alice",age:31,city:"Paris"})}),it("should count items and count with where",async()=>{await e.set({name:"Alice",age:30,city:"Paris"}),await e.set({name:"Bob",age:25,city:"London"}),expect(await e.count()).toBe(2),expect(await e.count({where:{city:"Paris"}})).toBe(1)}),it("should search with ordering, limit and offset",async()=>{const a=[{name:"Alice",age:30,city:"Paris"},{name:"Bob",age:25,city:"London"},{name:"Carol",age:35,city:"Berlin"}];for(const o of a)await e.set(o);const i=[];for await(const o of e.search({sortBy:"age",order:"asc"}))i.push(o);expect(i.map(o=>o.name)).toEqual(["Bob","Alice","Carol"]);const t=[];for await(const o of e.search({sortBy:"age",order:"asc",limit:2}))t.push(o);expect(t.map(o=>o.name)).toEqual(["Bob","Alice"]);const s=[];for await(const o of e.search({sortBy:"age",order:"asc",offset:1,limit:2}))s.push(o);expect(s.map(o=>o.name)).toEqual(["Alice","Carol"])}),it("should deleteBy where clause",async()=>{await e.set({name:"Dave",age:40,city:"NY"}),await e.set({name:"Eve",age:45,city:"NY"}),await e.set({name:"Frank",age:50,city:"LA"}),expect(await e.count()).toBe(3),await e.deleteBy({city:"NY"}),expect(await e.count()).toBe(1),expect(await e.get("Frank")).toEqual({name:"Frank",age:50,city:"LA"}),expect(await e.get("Dave")).toBeUndefined()}),it("should use selector in get and search",async()=>{await e.set({name:"Gary",age:60,city:"SF"});const a=await e.get("Gary",({age:t})=>t);expect(a).toBe(60);const i=[];for await(const t of e.search({select:({city:s})=>s}))i.push(t);expect(i).toEqual(["SF"])}),it("should delete items by key",async()=>{await e.set({name:"Helen",age:28,city:"Rome"}),expect(await e.get("Helen")).toBeDefined(),await e.delete("Helen"),expect(await e.get("Helen")).toBeUndefined()}),it("should test search with 1000 items",async()=>{const a=[];for(let t=0;t<1e3;t++)a.push({name:`Person${t}`,age:Math.floor(Math.random()*100),city:"City"+t%10});for(const t of a)await e.set(t);const i=[];for await(const t of e.search({sortBy:"age",order:"asc",limit:100}))i.push(t);expect(i.length).toBe(100)}),it("should handle operations on an empty table",async()=>{expect(await e.count()).toBe(0),expect(await e.get("NonExistent")).toBeUndefined();const a=[];for await(const i of e.search({sortBy:"age",order:"asc"}))a.push(i);expect(a.length).toBe(0)}),it("should handle duplicate keys gracefully",async()=>{await e.set({name:"Alice",age:30,city:"Paris"}),await e.set({name:"Alice",age:35,city:"Berlin"});const a=await e.get("Alice");expect(a).toEqual({name:"Alice",age:35,city:"Berlin"})}),it("should handle edge cases in selectors",async()=>{await e.set({name:"Charlie",age:40,city:"NY"});const a=await e.get("Charlie",()=>null);expect(a).toBeNull();const i=await e.get("Charlie",()=>{});expect(i).toBeUndefined()})});
1
+ import{bunMemoryBackend as r}from"../table/bun-backend";import{createTable as l}from"../table/table";describe("table",()=>{let c=r(),e,s;beforeEach(async()=>{c=r(),e=await l({backend:c,tableName:"TestTable",key:"name"}),s=await l({backend:c,tableName:"TestTableNested",key:"info.name",indexes:["info.age","info.city"]})}),it("should set and get items",async()=>{const a=await e.set({name:"Alice",age:30,city:"Paris"});expect(a.key).toBe("Alice"),expect(a.op).toBe("insert");const o=await e.get("Alice");expect(o).toEqual({name:"Alice",age:30,city:"Paris"});const t=await e.set({name:"Alice",age:31,city:"Paris"});expect(t.key).toBe("Alice"),expect(t.op).toBe("update");const i=await e.get("Alice");expect(i).toEqual({name:"Alice",age:31,city:"Paris"})}),it("should set and get nested key",async()=>{const a=await s.set({info:{name:"Bob",age:25,city:"London"}});expect(a.key).toBe("Bob"),expect(a.op).toBe("insert");const o=await s.get("Bob");expect(o).toEqual({info:{name:"Bob",age:25,city:"London"}});const t=await s.set({info:{name:"Bob",age:26,city:"London"}});expect(t.key).toBe("Bob"),expect(t.op).toBe("update");const i=await s.get("Bob");expect(i).toEqual({info:{name:"Bob",age:26,city:"London"}});const n=[];for await(const d of s.search({where:{info:{city:{like:"London"}}}}))n.push(d);expect(n.length).toBe(1)}),it("should count items and count with where",async()=>{await e.set({name:"Alice",age:30,city:"Paris"}),await e.set({name:"Bob",age:25,city:"London"}),expect(await e.count()).toBe(2),expect(await e.count({where:{city:"Paris"}})).toBe(1)}),it("should search with ordering, limit and offset",async()=>{const a=[{name:"Alice",age:30,city:"Paris"},{name:"Bob",age:25,city:"London"},{name:"Carol",age:35,city:"Berlin"}];for(const n of a)await e.set(n);const o=[];for await(const n of e.search({sortBy:"age",order:"asc"}))o.push(n);expect(o.map(n=>n.name)).toEqual(["Bob","Alice","Carol"]);const t=[];for await(const n of e.search({sortBy:"age",order:"asc",limit:2}))t.push(n);expect(t.map(n=>n.name)).toEqual(["Bob","Alice"]);const i=[];for await(const n of e.search({sortBy:"age",order:"asc",offset:1,limit:2}))i.push(n);expect(i.map(n=>n.name)).toEqual(["Alice","Carol"])}),it("should deleteBy where clause",async()=>{await e.set({name:"Dave",age:40,city:"NY"}),await e.set({name:"Eve",age:45,city:"NY"}),await e.set({name:"Frank",age:50,city:"LA"}),expect(await e.count()).toBe(3),await e.deleteBy({city:"NY"}),expect(await e.count()).toBe(1),expect(await e.get("Frank")).toEqual({name:"Frank",age:50,city:"LA"}),expect(await e.get("Dave")).toBeUndefined()}),it("should use selector in get and search",async()=>{await e.set({name:"Gary",age:60,city:"SF"});const a=await e.get("Gary",({age:t})=>t);expect(a).toBe(60);const o=[];for await(const t of e.search({select:({city:i})=>i}))o.push(t);expect(o).toEqual(["SF"])}),it("should delete items by key",async()=>{await e.set({name:"Helen",age:28,city:"Rome"}),expect(await e.get("Helen")).toBeDefined(),await e.delete("Helen"),expect(await e.get("Helen")).toBeUndefined()}),it("should test search with 1000 items",async()=>{const a=[];for(let t=0;t<1e3;t++)a.push({name:`Person${t}`,age:Math.floor(Math.random()*100),city:"City"+t%10});for(const t of a)await e.set(t);const o=[];for await(const t of e.search({sortBy:"age",order:"asc",limit:100}))o.push(t);expect(o.length).toBe(100)}),it("should handle operations on an empty table",async()=>{expect(await e.count()).toBe(0),expect(await e.get("NonExistent")).toBeUndefined();const a=[];for await(const o of e.search({sortBy:"age",order:"asc"}))a.push(o);expect(a.length).toBe(0)}),it("should handle duplicate keys gracefully",async()=>{await e.set({name:"Alice",age:30,city:"Paris"}),await e.set({name:"Alice",age:35,city:"Berlin"});const a=await e.get("Alice");expect(a).toEqual({name:"Alice",age:35,city:"Berlin"})}),it("should handle edge cases in selectors",async()=>{await e.set({name:"Charlie",age:40,city:"NY"});const a=await e.get("Charlie",()=>null);expect(a).toBeNull();const o=await e.get("Charlie",()=>{});expect(o).toBeUndefined()})});
@@ -1 +1 @@
1
- import{createScheduler as P}from"../scheduler";import{shallow as O}from"../utils/shallow";import{selectSql as R}from"./select-sql";import{createTable as M,DEFAULT_STEP_SIZE as v}from"./table/table";const f=P();let B=0;function C(){return B++}function N(g){const k=C();function l(e){return`state-${k}-search-${e}`}let m;async function o(){if(!m){const{backend:e,...n}=g,t=await e;m=await M({...n,backend:t})}return m}const c=new Map,i=new Map,h=new Map;async function b(e,n){const t=h.get(e),{options:a={}}=n,{stepSize:s=v}=a;if(!t)return!1;const r=[];for(let u=0;u<s;u++){const y=await t.next();if(y.done){h.delete(e);break}r.push(y.value.document),n.keys.add(String(y.value.rowId))}return r.length===0||O(n.items,r)?!1:(n.items=[...n.items,...r],!0)}function p(e){const n=i.get(e);n&&n()}async function x(e){const n=await o(),t=c.get(e);if(!t)return;const{options:a}=t,s=n.search({...a,select:(r,{rowId:u})=>({document:r,rowId:u})});h.set(e,s),t.keys=new Set,t.items=[],await b(e,t)}async function S(e){await x(e),p(e)}function T(e){const{key:n,op:t}=e,a=new Set;for(const[s,{keys:r}]of c)switch(t){case"delete":case"update":{r.has(String(n))&&a.add(s);break}case"insert":{a.add(s);break}}return a}async function d(e){const n=new Set;for(const t of e){const a=T(t);for(const s of a)n.add(s)}for(const t of n){const a=l(t);f.schedule(a,{searchId:t})}}const D=new Set;function w(e,n){c.has(e)||(c.set(e,{items:[],options:n,keys:new Set}),n&&S(e));const t=c.get(e);return n&&(t.options=n),t}const I={async set(e){const t=await(await o()).set(e);return await d([t]),t},async batchSet(e){const t=await(await o()).batchSet(e);return await d(t),t},async delete(e){const t=await(await o()).delete(e);return t&&await d([t]),t},async deleteBy(e){const t=await(await o()).deleteBy(e);return await d(t),t},async get(e,n){return(await o()).get(e,n)},async*search(e={}){const n=await o();for await(const t of n.search(e))yield t},async count(e){return await(await o()).count(e)},updateSearchOptions(e,n){const t=w(e,n);t.options=n;const a=l(e);f.schedule(a,{searchId:e})},subscribe(e,n){const t=l(e),a=f.add(t,{onScheduleDone(){S(e)}});return D.add(a),i.has(e)||i.set(e,n),()=>{i.delete(e),a(),c.delete(e)}},getSnapshot(e){return w(e).items},refresh:S,destroy(){for(const e of D)e();c.clear(),i.clear()},async next(e){const n=c.get(e);if(n){const t=await b(e,n);return t&&p(e),t}return!1},select(e){return R(I,e)}};return I}export{N as createSqliteState};
1
+ import{createScheduler as P}from"../scheduler";import{shallow as O}from"../utils/shallow";import{selectSql as R}from"./select-sql";import{createTable as v,DEFAULT_STEP_SIZE as M}from"./table/table";const f=P();let C=0;function B(){return C++}function L(g){const k=B();function l(e){return`state-${k}-search-${e}`}let m;async function o(){if(!m){const{backend:e,...n}=g,t=e instanceof Promise?await e:e;m=await v({backend:t,...n})}return m}const c=new Map,i=new Map,h=new Map;async function p(e,n){const t=h.get(e),{options:a={}}=n,{stepSize:s=M}=a;if(!t)return!1;const r=[];for(let u=0;u<s;u++){const y=await t.next();if(y.done){h.delete(e);break}r.push(y.value.document),n.keys.add(String(y.value.rowId))}return r.length===0||O(n.items,r)?!1:(n.items=[...n.items,...r],!0)}function b(e){const n=i.get(e);n&&n()}async function x(e){const n=await o(),t=c.get(e);if(!t)return;const{options:a}=t,s=n.search({...a,select:(r,{rowId:u})=>({document:r,rowId:u})});h.set(e,s),t.keys=new Set,t.items=[],await p(e,t)}async function S(e){await x(e),b(e)}function T(e){const{key:n,op:t}=e,a=new Set;for(const[s,{keys:r}]of c)switch(t){case"delete":case"update":{r.has(String(n))&&a.add(s);break}case"insert":{a.add(s);break}}return a}async function d(e){const n=new Set;for(const t of e){const a=T(t);for(const s of a)n.add(s)}for(const t of n){const a=l(t);f.schedule(a,{searchId:t})}}const D=new Set;function w(e,n){c.has(e)||(c.set(e,{items:[],options:n,keys:new Set}),n&&S(e));const t=c.get(e);return n&&(t.options=n),t}const I={async set(e){const t=await(await o()).set(e);return await d([t]),t},async batchSet(e){const t=await(await o()).batchSet(e);return await d(t),t},async delete(e){const t=await(await o()).delete(e);return t&&await d([t]),t},async deleteBy(e){const t=await(await o()).deleteBy(e);return await d(t),t},async get(e,n){return(await o()).get(e,n)},async*search(e={}){const n=await o();for await(const t of n.search(e))yield t},async count(e){return await(await o()).count(e)},updateSearchOptions(e,n){const t=w(e,n);t.options=n;const a=l(e);f.schedule(a,{searchId:e})},subscribe(e,n){const t=l(e),a=f.add(t,{onScheduleDone(){S(e)}});return D.add(a),i.has(e)||i.set(e,n),()=>{i.delete(e),a(),c.delete(e)}},getSnapshot(e){return w(e).items},refresh:S,destroy(){for(const e of D)e();c.clear(),i.clear()},async next(e){const n=c.get(e);if(n){const t=await p(e,n);return t&&b(e),t}return!1},select(e){return R(I,e)}};return I}export{L as createSqliteState};
@@ -1,10 +1,11 @@
1
- import{getWhereQuery as S}from"./where";const p=500,g=100;async function x(A){const{backend:o,tableName:s,indexes:O,key:m,disablePragmaOptimization:R}=A,d=m!==void 0;R||(await o.execute("PRAGMA journal_mode=WAL;"),await o.execute("PRAGMA synchronous=NORMAL;"),await o.execute("PRAGMA temp_store=MEMORY;"),await o.execute("PRAGMA cache_size=-20000;")),d?await o.execute(`
2
- CREATE TABLE IF NOT EXISTS ${s} (
1
+ import{getWhereQuery as h}from"./where";const O=500,N=100;function D(l){return"$."+l}function I(l,r){if(!(!l||!r))return r.split(".").reduce((o,y)=>{if(typeof o=="object"&&o!==null&&y in o)return o[y]},l)}async function M(l){const{backend:r,tableName:o,indexes:y,key:S,disablePragmaOptimization:k}=l,d=S!==void 0;k||(await r.execute("PRAGMA journal_mode=WAL;"),await r.execute("PRAGMA synchronous=NORMAL;"),await r.execute("PRAGMA temp_store=MEMORY;"),await r.execute("PRAGMA cache_size=-20000;")),d?await r.execute(`
2
+ CREATE TABLE IF NOT EXISTS ${o} (
3
3
  key TEXT PRIMARY KEY,
4
4
  data TEXT NOT NULL
5
5
  );
6
- `):await o.execute(`
7
- CREATE TABLE IF NOT EXISTS ${s} (
6
+ `):await r.execute(`
7
+ CREATE TABLE IF NOT EXISTS ${o} (
8
8
  data TEXT NOT NULL
9
9
  );
10
- `);for(const n of O??[]){const r=String(n);await o.execute(`CREATE INDEX IF NOT EXISTS idx_${s}_${r} ON ${s} (json_extract(data, '$.${r}'));`)}function k(n){return d?n[m]:void 0}async function D(n){return(await n.select("SELECT changes() AS c"))[0]?.c??0}const f={backend:o,async set(n,r){const e=r??o,a=JSON.stringify(n);if(d){const t=k(n);if(t==null)throw new Error(`Document is missing the configured key "${String(m)}". Provide it or create the table without "key".`);if(await e.execute(`UPDATE ${s} SET data = ? WHERE key = ?`,[a,t]),await D(e)===1)return{key:t,op:"update"};try{return await e.execute(`INSERT INTO ${s} (key, data) VALUES (?, ?)`,[t,a]),{key:t,op:"insert"}}catch{return await e.execute(`UPDATE ${s} SET data = ? WHERE key = ?`,[a,t]),{key:t,op:"update"}}}await e.execute(`INSERT INTO ${s} (data) VALUES (?)`,[a]);const c=(await e.select("SELECT last_insert_rowid() AS id"))[0]?.id;if(typeof c!="number")throw new Error("Failed to retrieve last_insert_rowid()");return{key:c,op:"insert"}},async get(n,r=(e,a)=>e){const e=d?"key = ?":"rowid = ?",a=await o.select(`SELECT rowid, data FROM ${s} WHERE ${e}`,[n]);if(a.length===0)return;const[i]=a,{data:c,rowid:u}=i,t=JSON.parse(c);return r(t,{rowid:u})},async delete(n){const r=d?"key = ?":"rowid = ?";if(await o.execute(`DELETE FROM ${s} WHERE ${r}`,[n]),((await o.select("SELECT changes() AS c"))[0]?.c??0)>0)return{key:n,op:"delete"}},async*search(n={}){const{sortBy:r,order:e="asc",limit:a,offset:i=0,where:c,select:u=(y,T)=>y,stepSize:t=g}=n;let l=`SELECT rowid, data FROM ${s}`;c&&(l+=" "+S(c));let E=0,h=i;for(;;){let y=l;r?y+=` ORDER BY json_extract(data, '$.${String(r)}') COLLATE NOCASE ${e.toUpperCase()}`:y+=d?` ORDER BY key COLLATE NOCASE ${e.toUpperCase()}`:` ORDER BY rowid ${e.toUpperCase()}`;const T=a?Math.min(t,a-E):t;y+=` LIMIT ${T} OFFSET ${h}`;const w=await o.select(y);if(w.length===0)break;for(const{rowid:$,data:b}of w){if(a&&E>=a)return;const L=JSON.parse(b);yield u(L,{rowId:$}),E++}if(w.length<T||a&&E>=a)break;h+=w.length}},async count(n={}){const{where:r}=n;let e=`SELECT COUNT(*) as count FROM ${s}`;return r&&(e+=" "+S(r)),(await o.select(e))[0]?.count??0},async deleteBy(n){const r=S(n),e=d?"key":"rowid",a=[];return await o.transaction(async i=>{const c=await i.select(`SELECT ${e} AS k, rowid FROM ${s} ${r}`);if(c.length===0)return;const u=c.map(t=>t.k);for(let t=0;t<u.length;t+=p){const l=u.slice(t,t+p),E=l.map(()=>"?").join(",");await i.execute(`DELETE FROM ${s} WHERE ${e} IN (${E})`,l)}for(const t of u)a.push({key:t,op:"delete"})}),a},async batchSet(n){const r=[];return await o.transaction(async e=>{for(const a of n){const i=await f.set(a,e);r.push(i)}}),r}};return f}export{g as DEFAULT_STEP_SIZE,x as createTable};
10
+ `);for(const t of y??[]){const a=String(t);await r.execute(`CREATE INDEX IF NOT EXISTS idx_${o}_${a.replaceAll(/\W/g,"_")}
11
+ ON ${o} (json_extract(data, '${D(a)}'));`)}function $(t){if(d)return I(t,String(S))}async function g(t){return(await t.select("SELECT changes() AS c"))[0]?.c??0}const p={backend:r,async set(t,a){const e=a??r,n=JSON.stringify(t);if(d){const s=$(t);if(s==null)throw new Error(`Document is missing the configured key "${String(S)}". Provide it or create the table without "key".`);if(await e.execute(`UPDATE ${o} SET data = ? WHERE key = ?`,[n,s]),await g(e)===1)return{key:s,op:"update"};try{return await e.execute(`INSERT INTO ${o} (key, data) VALUES (?, ?)`,[s,n]),{key:s,op:"insert"}}catch{return await e.execute(`UPDATE ${o} SET data = ? WHERE key = ?`,[n,s]),{key:s,op:"update"}}}await e.execute(`INSERT INTO ${o} (data) VALUES (?)`,[n]);const u=(await e.select("SELECT last_insert_rowid() AS id"))[0]?.id;if(typeof u!="number")throw new Error("Failed to retrieve last_insert_rowid()");return{key:u,op:"insert"}},async get(t,a=e=>e){const e=d?"key = ?":"rowid = ?",n=await r.select(`SELECT rowid, data FROM ${o} WHERE ${e}`,[t]);if(n.length===0)return;const{data:i,rowid:u}=n[0],s=JSON.parse(i);return a(s,{rowId:u})},async delete(t){const a=d?"key = ?":"rowid = ?";if(await r.execute(`DELETE FROM ${o} WHERE ${a}`,[t]),((await r.select("SELECT changes() AS c"))[0]?.c??0)>0)return{key:t,op:"delete"}},async*search(t={}){const{sortBy:a,order:e="asc",limit:n,offset:i=0,where:u,select:s=E=>E,stepSize:c=N}=t,w=h(u),f=`SELECT rowid, data FROM ${o} ${w}`;let T=0,A=i;for(;;){let E=f;a?E+=` ORDER BY json_extract(data, '${D(String(a))}') COLLATE NOCASE ${e.toUpperCase()}`:E+=d?` ORDER BY key COLLATE NOCASE ${e.toUpperCase()}`:` ORDER BY rowid ${e.toUpperCase()}`;const R=n?Math.min(c,n-T):c;E+=` LIMIT ${R} OFFSET ${A}`;const m=await r.select(E);if(m.length===0)break;for(const{rowid:b,data:L}of m){if(n&&T>=n)return;const x=JSON.parse(L);yield s(x,{rowId:b}),T++}if(m.length<R||n&&T>=n)break;A+=m.length}},async count(t={}){const a=h(t.where),e=`SELECT COUNT(*) as count FROM ${o} ${a}`;return(await r.select(e))[0]?.count??0},async deleteBy(t){const a=h(t),e=d?"key":"rowid",n=[];return await r.transaction(async i=>{const u=await i.select(`SELECT ${e} AS k FROM ${o} ${a}`);if(u.length===0)return;const s=u.map(c=>c.k);for(let c=0;c<s.length;c+=O){const w=s.slice(c,c+O),f=w.map(()=>"?").join(",");await i.execute(`DELETE FROM ${o} WHERE ${e} IN (${f})`,w)}for(const c of s)n.push({key:c,op:"delete"})}),n},async batchSet(t){const a=[];return await r.transaction(async e=>{for(const n of t){const i=await p.set(n,e);a.push(i)}}),a}};return p}export{N as DEFAULT_STEP_SIZE,M as createTable,I as getByPath};
@@ -1 +1 @@
1
- function y(n){return typeof n=="string"?`'${n.split("'").join("''")}'`:typeof n=="number"?n.toString():typeof n=="boolean"?n?"1":"0":`'${String(n).split("'").join("''")}'`}function $(n,c,i){const e=i?`${i}.`:"";return n==="KEY"?`"${e}key"`:typeof c=="string"?`CAST(json_extract(${e}data, '$.${n}') AS TEXT)`:typeof c=="boolean"?`CAST(json_extract(${e}data, '$.${n}') AS INTEGER)`:typeof c=="number"?`CAST(json_extract(${e}data, '$.${n}') AS NUMERIC)`:`json_extract(${e}data, '$.${n}')`}const N=new Set(["is","isNot","gt","gte","lt","lte","in","notIn","like"]);function m(n){return n===void 0}function l(n,c){if(!n||typeof n!="object")return"";if(!m(n.AND)){const t=n.AND;if(Array.isArray(t)&&t.length>0){let o="",u=!1;for(const a of t){const f=l(a,c);f&&(u&&(o+=" AND "),o+=f,u=!0)}return u?`(${o})`:""}return""}if(!m(n.OR)){const t=n.OR;if(Array.isArray(t)&&t.length>0){let o="",u=!1;for(const a of t){const f=l(a,c);f&&(u&&(o+=" OR "),o+=f,u=!0)}return u?`(${o})`:""}return""}if(!m(n.NOT)){const t=n.NOT;if(t&&typeof t=="object"){const o=l(t,c);return o?`(NOT ${o})`:""}return""}let i="",e=!1;for(const t in n){if(t==="AND"||t==="OR"||t==="NOT")continue;const o=n[t];if(o==null)continue;let u;typeof o!="object"||Array.isArray(o)?u=Array.isArray(o)?{in:o}:{is:o}:u=o;for(const a of Object.keys(u)){if(!N.has(a))continue;const f=u[a];if(f==null)continue;const s=Array.isArray(f)?f:[f];if(s.length!==0){if(a==="is"||a==="isNot"||a==="in"||a==="notIn"){const[D]=s,r=$(t,D,c);let d="";if(s.length>1)for(const[A,g]of s.entries())A>0&&(d+=","),d+=y(g);switch(a){case"is":{i+=s.length>1?(e?" AND ":"")+`${r} IN (${d})`:(e?" AND ":"")+`${r} = ${y(s[0])}`;break}case"isNot":{i+=s.length>1?(e?" AND ":"")+`${r} NOT IN (${d})`:(e?" AND ":"")+`${r} <> ${y(s[0])}`;break}case"in":{i+=s.length>1?(e?" AND ":"")+`${r} IN (${d})`:(e?" AND ":"")+`${r} IN (${y(s[0])})`;break}case"notIn":{i+=s.length>1?(e?" AND ":"")+`${r} NOT IN (${d})`:(e?" AND ":"")+`${r} NOT IN (${y(s[0])})`;break}}e=!0;continue}for(const D of s){const r=$(t,D,c);switch(a){case"gt":{i+=(e?" AND ":"")+`${r} > ${y(D)}`;break}case"gte":{i+=(e?" AND ":"")+`${r} >= ${y(D)}`;break}case"lt":{i+=(e?" AND ":"")+`${r} < ${y(D)}`;break}case"lte":{i+=(e?" AND ":"")+`${r} <= ${y(D)}`;break}case"like":{i+=(e?" AND ":"")+`${r} LIKE ${y(D)}`;break}}e=!0}}}}return e?`(${i})`:""}function k(n){const c=l(n);return c?`WHERE ${c}`:""}export{l as getWhere,k as getWhereQuery};
1
+ function l(n){return typeof n=="string"?`'${n.replaceAll("'","''")}'`:typeof n=="number"?n.toString():typeof n=="boolean"?n?"1":"0":`'${String(n).replaceAll("'","''")}'`}function g(n,e,u){const t=u?`${u}.`:"";return n==="KEY"?`"${t}key"`:typeof e=="string"?`CAST(json_extract(${t}data, '$.${n}') AS TEXT)`:typeof e=="number"?`CAST(json_extract(${t}data, '$.${n}') AS NUMERIC)`:typeof e=="boolean"?`CAST(json_extract(${t}data, '$.${n}') AS INTEGER)`:`json_extract(${t}data, '$.${n}')`}const A=new Set(["is","isNot","gt","gte","lt","lte","in","notIn","like"]);function N(n,e=""){const u={};for(const[t,o]of Object.entries(n)){if(t==="AND"||t==="OR"||t==="NOT"){u[t]=o;continue}const r=e?`${e}.${t}`:t;o&&typeof o=="object"&&!Array.isArray(o)&&!Object.keys(o).some(i=>A.has(i))?Object.assign(u,N(o,r)):u[r]=o}return u}function T(n,e){if(!n||typeof n!="object")return"";if(n.AND){const r=Array.isArray(n.AND)?n.AND.map(i=>T(i,e)).filter(Boolean):[];return r.length>0?`(${r.join(" AND ")})`:""}if(n.OR){const r=Array.isArray(n.OR)?n.OR.map(i=>T(i,e)).filter(Boolean):[];return r.length>0?`(${r.join(" OR ")})`:""}if(n.NOT){const r=T(n.NOT,e);return r?`(NOT ${r})`:""}const u=N(n);let t="",o=!1;for(const[r,i]of Object.entries(u)){if(i==null)continue;let y;typeof i!="object"||Array.isArray(i)?y=Array.isArray(i)?{in:i}:{is:i}:y=i;for(const s of Object.keys(y)){if(!A.has(s))continue;const $=y[s];if($==null)continue;const f=Array.isArray($)?$:[$];if(f.length!==0){if(s==="is"||s==="isNot"||s==="in"||s==="notIn"){const c=g(r,f[0],e),a=f.map(l).join(","),d=s==="is"?f.length>1?`${c} IN (${a})`:`${c} = ${l(f[0])}`:s==="isNot"?f.length>1?`${c} NOT IN (${a})`:`${c} <> ${l(f[0])}`:s==="in"?`${c} IN (${a})`:`${c} NOT IN (${a})`;t+=(o?" AND ":"")+d,o=!0;continue}for(const c of f){const a=g(r,c,e),d=s==="gt"?`${a} > ${l(c)}`:s==="gte"?`${a} >= ${l(c)}`:s==="lt"?`${a} < ${l(c)}`:s==="lte"?`${a} <= ${l(c)}`:`${a} LIKE ${l(c)}`;t+=(o?" AND ":"")+d,o=!0}}}}return o?`(${t})`:""}function p(n){if(!n)return"";const e=T(n);return e?`WHERE ${e}`:""}export{T as getWhere,p as getWhereQuery};
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "muya",
3
- "version": "2.2.2",
3
+ "version": "2.2.4",
4
4
  "author": "samuel.gjabel@gmail.com",
5
5
  "repository": "https://github.com/samuelgjabel/muya",
6
6
  "main": "cjs/index.js",
@@ -12,10 +12,20 @@ interface Person {
12
12
  age: number
13
13
  city: string
14
14
  }
15
+
16
+ interface PersonNested {
17
+ info: {
18
+ name: string
19
+ age: number
20
+ city: string
21
+ }
22
+ }
15
23
  describe('table', () => {
16
24
  let backend = bunMemoryBackend()
17
25
  let table: ReturnType<typeof createTable<Person>> extends Promise<infer T> ? T : never
18
26
 
27
+ let tableNested: ReturnType<typeof createTable<PersonNested>> extends Promise<infer T> ? T : never
28
+
19
29
  beforeEach(async () => {
20
30
  backend = bunMemoryBackend()
21
31
  table = await createTable<Person>({
@@ -23,6 +33,12 @@ describe('table', () => {
23
33
  tableName: 'TestTable',
24
34
  key: 'name',
25
35
  })
36
+ tableNested = await createTable<PersonNested>({
37
+ backend,
38
+ tableName: 'TestTableNested',
39
+ key: 'info.name',
40
+ indexes: ['info.age', 'info.city'],
41
+ })
26
42
  })
27
43
 
28
44
  it('should set and get items', async () => {
@@ -39,6 +55,29 @@ describe('table', () => {
39
55
  expect(updatedResult).toEqual({ name: 'Alice', age: 31, city: 'Paris' })
40
56
  })
41
57
 
58
+ it('should set and get nested key', async () => {
59
+ const mutation = await tableNested.set({ info: { name: 'Bob', age: 25, city: 'London' } })
60
+ expect(mutation.key).toBe('Bob')
61
+ expect(mutation.op).toBe('insert')
62
+ const result = await tableNested.get('Bob')
63
+ expect(result).toEqual({ info: { name: 'Bob', age: 25, city: 'London' } })
64
+
65
+ const updateMutation = await tableNested.set({ info: { name: 'Bob', age: 26, city: 'London' } })
66
+ expect(updateMutation.key).toBe('Bob')
67
+ expect(updateMutation.op).toBe('update')
68
+ const updatedResult = await tableNested.get('Bob')
69
+ expect(updatedResult).toEqual({ info: { name: 'Bob', age: 26, city: 'London' } })
70
+
71
+ // nested where query
72
+ const results: PersonNested[] = []
73
+ for await (const person of tableNested.search({
74
+ where: { info: { city: { like: 'London' } } },
75
+ })) {
76
+ results.push(person)
77
+ }
78
+ expect(results.length).toBe(1)
79
+ })
80
+
42
81
  it('should count items and count with where', async () => {
43
82
  await table.set({ name: 'Alice', age: 30, city: 'Paris' })
44
83
  await table.set({ name: 'Bob', age: 25, city: 'London' })
@@ -0,0 +1,280 @@
1
+ import { act, renderHook } from '@testing-library/react-hooks'
2
+ import { createSqliteState } from '../create-sqlite'
3
+ import { useSqliteValue } from '../use-sqlite'
4
+ import { waitFor } from '@testing-library/react'
5
+ import { bunMemoryBackend } from '../table/bun-backend'
6
+ import { Suspense, useState } from 'react'
7
+ import { DEFAULT_STEP_SIZE } from '../table/table'
8
+
9
+ const backend = bunMemoryBackend()
10
+ interface Person {
11
+ id: string
12
+ name: string
13
+ age: number
14
+ }
15
+
16
+ function Wrapper({ children }: Readonly<{ children: React.ReactNode }>) {
17
+ return <Suspense fallback={<div>Loading...</div>}>{children}</Suspense>
18
+ }
19
+ describe('use-sqlite-state', () => {
20
+ it('should get basic value states', async () => {
21
+ const sql = createSqliteState<Person>({ backend, tableName: 'State1', key: 'id' })
22
+ let reRenders = 0
23
+ const { result } = renderHook(
24
+ () => {
25
+ reRenders++
26
+ const aha = useSqliteValue(sql, {}, [])
27
+ return aha
28
+ },
29
+ { wrapper: Wrapper },
30
+ )
31
+
32
+ expect(reRenders).toBe(1)
33
+
34
+ act(() => {
35
+ sql.set({ id: '1', name: 'Alice', age: 30 })
36
+ })
37
+ await waitFor(() => {
38
+ expect(result.current[0]).toEqual([{ id: '1', name: 'Alice', age: 30 }])
39
+ expect(reRenders).toBe(3)
40
+ })
41
+
42
+ // act(() => {
43
+ // sql.set({ id: '1', name: 'Alice2', age: 30 })
44
+ // })
45
+ // await waitFor(() => {
46
+ // expect(result.current[0]).toEqual([{ id: '1', name: 'Alice2', age: 30 }])
47
+ // expect(reRenders).toBe(4)
48
+ // })
49
+
50
+ // // delete item
51
+ // act(() => {
52
+ // sql.delete('1')
53
+ // })
54
+ // await waitFor(() => {
55
+ // expect(result.current[0]).toEqual([])
56
+ // expect(reRenders).toBe(5)
57
+ // })
58
+
59
+ // // add two items
60
+ // act(() => {
61
+ // sql.set({ id: '1', name: 'Alice', age: 30 })
62
+ // sql.set({ id: '2', name: 'Bob', age: 25 })
63
+ // })
64
+ // await waitFor(() => {
65
+ // expect(result.current[0].length).toBe(2)
66
+ // expect(reRenders).toBe(6)
67
+ // })
68
+ })
69
+
70
+ it('should use where clause changed via state', async () => {
71
+ const sql = createSqliteState<Person>({ backend, tableName: 'State2', key: 'id' })
72
+ await sql.batchSet([
73
+ { id: '1', name: 'Alice', age: 30 },
74
+ { id: '2', name: 'Bob', age: 25 },
75
+ { id: '3', name: 'Carol', age: 40 },
76
+ ])
77
+ let reRenders = 0
78
+ const { result } = renderHook(() => {
79
+ reRenders++
80
+ const [minAge, setMinAge] = useState(20)
81
+ return [useSqliteValue(sql, { where: { age: { gt: minAge } }, sorBy: 'age' }, [minAge]), setMinAge] as const
82
+ })
83
+
84
+ await waitFor(() => {
85
+ expect(result.current[0][0].map((p) => p.name)).toEqual(['Alice', 'Bob', 'Carol'])
86
+ expect(reRenders).toBe(2)
87
+ })
88
+
89
+ // // change minAge to 29
90
+ act(() => {
91
+ result.current[1](29)
92
+ })
93
+ await waitFor(() => {
94
+ expect(result.current[0][0].map((p) => p.name)).toEqual(['Alice', 'Carol'])
95
+ expect(reRenders).toBe(4)
96
+ })
97
+ })
98
+
99
+ it('should support like in where clause and update results', async () => {
100
+ const sql = createSqliteState<Person>({ backend, tableName: 'State3', key: 'id' })
101
+ await sql.batchSet([
102
+ { id: '1', name: 'Alice', age: 30 },
103
+ { id: '2', name: 'Alicia', age: 25 },
104
+ { id: '3', name: 'Bob', age: 40 },
105
+ ])
106
+ let reRenders = 0
107
+ const { result, rerender } = renderHook(
108
+ ({ like }) => {
109
+ reRenders++
110
+ return useSqliteValue(sql, { where: { name: { like } } }, [like])
111
+ },
112
+ { initialProps: { like: '%Ali%' } },
113
+ )
114
+ await waitFor(() => {
115
+ expect(result.current[0].map((p) => p.name)).toEqual(['Alice', 'Alicia'])
116
+ })
117
+ act(() => {
118
+ rerender({ like: '%Bob%' })
119
+ })
120
+ await waitFor(() => {
121
+ expect(result.current[0].map((p) => p.name)).toEqual(['Bob'])
122
+ })
123
+ expect(reRenders).toBeGreaterThanOrEqual(2)
124
+ })
125
+
126
+ it('should update results when changing order and limit options', async () => {
127
+ const sql = createSqliteState<Person>({ backend, tableName: 'State4', key: 'id' })
128
+ await sql.batchSet([
129
+ { id: '1', name: 'Alice', age: 30 },
130
+ { id: '2', name: 'Bob', age: 25 },
131
+ { id: '3', name: 'Carol', age: 40 },
132
+ ])
133
+ const { result, rerender } = renderHook(
134
+ ({ order, limit }) => useSqliteValue(sql, { sorBy: 'age', order, limit }, [order, limit]),
135
+ { initialProps: { order: 'asc' as 'asc' | 'desc', limit: 2 } },
136
+ )
137
+ await waitFor(() => {
138
+ expect(result.current[0].map((p) => p.name)).toEqual(['Alice', 'Bob'])
139
+ })
140
+ act(() => {
141
+ rerender({ order: 'desc', limit: 2 })
142
+ })
143
+ await waitFor(() => {
144
+ expect(result.current[0].map((p) => p.name)).toEqual(['Alice', 'Bob'])
145
+ })
146
+ act(() => {
147
+ rerender({ order: 'desc', limit: 1 })
148
+ })
149
+ await waitFor(() => {
150
+ expect(result.current[0].map((p) => p.name)).toEqual(['Carol'])
151
+ })
152
+ })
153
+
154
+ it('should support actions.next and actions.refresh', async () => {
155
+ const sql = createSqliteState<Person>({ backend, tableName: 'State5', key: 'id' })
156
+ await sql.batchSet([
157
+ { id: '1', name: 'Alice', age: 30 },
158
+ { id: '2', name: 'Bob', age: 25 },
159
+ ])
160
+ const { result } = renderHook(() => useSqliteValue(sql, {}, []))
161
+ // actions.next and actions.refresh should be functions
162
+ await waitFor(() => {
163
+ expect(typeof result.current[1].next).toBe('function')
164
+ expect(typeof result.current[1].reset).toBe('function')
165
+ expect(result.current[1].reset()).resolves.toBeUndefined()
166
+ expect(result.current[1].next()).resolves.toBeFalsy()
167
+ })
168
+ })
169
+ it('should handle thousands of records', async () => {
170
+ const sql = createSqliteState<Person>({ backend, tableName: 'State6', key: 'id' })
171
+ const people: Person[] = []
172
+ const ITEMS_COUNT = 1000
173
+ for (let index = 1; index <= ITEMS_COUNT; index++) {
174
+ people.push({ id: index.toString(), name: `Person${index}`, age: 20 + (index % 50) })
175
+ }
176
+ await sql.batchSet(people)
177
+ const { result } = renderHook(() => useSqliteValue(sql, {}, []))
178
+ await waitFor(() => {
179
+ expect(result.current[0].length).toBe(DEFAULT_STEP_SIZE)
180
+ })
181
+
182
+ // loop until we have all ITEMS_COUNT items
183
+ for (let index = 0; index < ITEMS_COUNT / DEFAULT_STEP_SIZE; index++) {
184
+ act(() => {
185
+ result.current[1].next()
186
+ })
187
+ await waitFor(() => {
188
+ expect(result.current[0].length).toBe(Math.min(DEFAULT_STEP_SIZE * (index + 2), ITEMS_COUNT))
189
+ })
190
+ }
191
+
192
+ act(() => {
193
+ result.current[1].reset()
194
+ })
195
+ await waitFor(() => {
196
+ expect(result.current[0].length).toBe(DEFAULT_STEP_SIZE)
197
+ })
198
+ })
199
+
200
+ it('should handle thousands of records with single update', async () => {
201
+ const sql = createSqliteState<Person>({ backend, tableName: 'State6', key: 'id' })
202
+ const people: Person[] = []
203
+ const ITEMS_COUNT = 10_000
204
+ const stepSize = 5000
205
+ for (let index = 1; index <= ITEMS_COUNT; index++) {
206
+ people.push({ id: index.toString(), name: `Person${index}`, age: 20 + (index % 50) })
207
+ }
208
+ await sql.batchSet(people)
209
+ let reRenders = 0
210
+ const { result } = renderHook(() => {
211
+ reRenders++
212
+ return useSqliteValue(sql, { stepSize }, [])
213
+ })
214
+ await waitFor(() => {
215
+ expect(reRenders).toBe(2)
216
+ expect(result.current[0].length).toBe(stepSize)
217
+ })
218
+
219
+ act(() => {
220
+ for (let index = 0; index < ITEMS_COUNT / stepSize; index++) {
221
+ result.current[1].next()
222
+ }
223
+ })
224
+
225
+ await waitFor(() => {
226
+ expect(reRenders).toBe(4)
227
+ expect(result.current[0].length).toBe(ITEMS_COUNT)
228
+ })
229
+
230
+ act(() => {
231
+ result.current[1].reset()
232
+ })
233
+ await waitFor(() => {
234
+ expect(reRenders).toBe(5)
235
+ expect(result.current[0].length).toBe(stepSize)
236
+ })
237
+ })
238
+ it('should change ordering', async () => {
239
+ const sql = createSqliteState<Person>({ backend, tableName: 'State7', key: 'id', indexes: ['age'] })
240
+ const people: Person[] = []
241
+ for (let index = 1; index <= 100; index++) {
242
+ people.push({ id: index.toString(), name: `Person${index}`, age: 20 + (index % 50) })
243
+ }
244
+ await sql.batchSet(people)
245
+ const { result, rerender } = renderHook(({ order }) => useSqliteValue(sql, { sorBy: 'age', order }, [order]), {
246
+ initialProps: { order: 'asc' as 'asc' | 'desc' },
247
+ })
248
+ await waitFor(() => {
249
+ expect(result.current[0][0].age).toBe(21)
250
+ })
251
+ act(() => {
252
+ rerender({ order: 'desc' })
253
+ })
254
+ await waitFor(() => {
255
+ expect(result.current[0][0].age).toBe(69)
256
+ })
257
+ })
258
+
259
+ it('should support selector in options', async () => {
260
+ const sql = createSqliteState<Person>({ backend, tableName: 'State8', key: 'id' })
261
+ await sql.batchSet([
262
+ { id: '1', name: 'Alice', age: 30 },
263
+ { id: '2', name: 'Bob', age: 25 },
264
+ { id: '3', name: 'Carol', age: 40 },
265
+ ])
266
+ const { result } = renderHook(() =>
267
+ useSqliteValue(
268
+ sql,
269
+ {
270
+ sorBy: 'age',
271
+ select: (d) => d.name,
272
+ },
273
+ [],
274
+ ),
275
+ )
276
+ await waitFor(() => {
277
+ expect(result.current[0]).toEqual(['Alice', 'Bob', 'Carol'])
278
+ })
279
+ })
280
+ })
@@ -17,6 +17,10 @@ function getStateId() {
17
17
  return stateId++
18
18
  }
19
19
 
20
+ export interface CreateSqliteOptions<Document extends DocType> extends Omit<DbOptions<Document>, 'backend'> {
21
+ readonly backend: Backend | Promise<Backend>
22
+ }
23
+
20
24
  export interface SyncTable<Document extends DocType> {
21
25
  // readonly registerSearch: <Selected = Document>(searchId: SearchId, options: SearchOptions<Document, Selected>) => () => void
22
26
  readonly updateSearchOptions: <Selected = Document>(searchId: SearchId, options: SearchOptions<Document, Selected>) => void
@@ -46,12 +50,7 @@ interface DataItems<Document extends DocType> {
46
50
  options?: SearchOptions<Document, unknown>
47
51
  }
48
52
 
49
- export interface ReactDbOptions<Document extends DocType> extends Omit<DbOptions<Document>, 'backend'> {
50
- readonly backend: Backend | Promise<Backend>
51
- }
52
- export function createSqliteState<Document extends DocType>(options: ReactDbOptions<Document>): SyncTable<Document> {
53
- // const table = await createTable<Document>(options)
54
-
53
+ export function createSqliteState<Document extends DocType>(options: CreateSqliteOptions<Document>): SyncTable<Document> {
55
54
  const id = getStateId()
56
55
  function getScheduleId(searchId: SearchId) {
57
56
  return `state-${id}-search-${searchId}`
@@ -61,8 +60,8 @@ export function createSqliteState<Document extends DocType>(options: ReactDbOpti
61
60
  async function getTable() {
62
61
  if (!cachedTable) {
63
62
  const { backend, ...rest } = options
64
- const awaitedBackend = await backend
65
- cachedTable = await createTable<Document>({ ...rest, backend: awaitedBackend })
63
+ const resolvedBackend = backend instanceof Promise ? await backend : backend
64
+ cachedTable = await createTable<Document>({ backend: resolvedBackend, ...rest })
66
65
  }
67
66
  return cachedTable
68
67
  }
@@ -1,19 +1,37 @@
1
- // table.ts
1
+ /* eslint-disable prefer-destructuring */
2
2
  /* eslint-disable sonarjs/different-types-comparison */
3
3
  /* eslint-disable sonarjs/cognitive-complexity */
4
4
  /* eslint-disable @typescript-eslint/no-shadow */
5
5
  /* eslint-disable no-shadow */
6
+
6
7
  import type { Table, DbOptions, DocType, Key, SearchOptions, MutationResult } from './table.types'
7
- import { getWhereQuery, type Where } from './where'
8
+ import type { Where } from './where'
9
+ import { getWhereQuery } from './where'
8
10
 
9
11
  const DELETE_IN_CHUNK = 500 // keep well below SQLite's default 999 parameter limit
10
12
  export const DEFAULT_STEP_SIZE = 100
13
+
14
+ // --- Helpers for JSON dot paths ---
15
+ function toJsonPath(dot: string) {
16
+ return '$.' + dot
17
+ }
18
+
19
+ export function getByPath<T extends object>(object: T, path: string): unknown {
20
+ if (!object || !path) return undefined
21
+ // eslint-disable-next-line unicorn/no-array-reduce
22
+ return path.split('.').reduce<unknown>((accumulator, key) => {
23
+ if (typeof accumulator === 'object' && accumulator !== null && key in (accumulator as Record<string, unknown>)) {
24
+ return (accumulator as Record<string, unknown>)[key]
25
+ }
26
+ return
27
+ }, object)
28
+ }
29
+
11
30
  export async function createTable<Document extends DocType>(options: DbOptions<Document>): Promise<Table<Document>> {
12
31
  const { backend, tableName, indexes, key, disablePragmaOptimization } = options
13
32
  const hasUserKey = key !== undefined
14
33
 
15
34
  // --- Apply performance PRAGMAs unless explicitly disabled ---
16
- // These significantly speed up write-heavy workloads on SQLite.
17
35
  if (!disablePragmaOptimization) {
18
36
  await backend.execute(`PRAGMA journal_mode=WAL;`)
19
37
  await backend.execute(`PRAGMA synchronous=NORMAL;`)
@@ -37,14 +55,18 @@ export async function createTable<Document extends DocType>(options: DbOptions<D
37
55
  `)
38
56
  }
39
57
 
40
- // JSON expression indexes for fields under data
58
+ // JSON expression indexes
41
59
  for (const index of indexes ?? []) {
42
60
  const idx = String(index)
43
- await backend.execute(`CREATE INDEX IF NOT EXISTS idx_${tableName}_${idx} ON ${tableName} (json_extract(data, '$.${idx}'));`)
61
+ await backend.execute(
62
+ `CREATE INDEX IF NOT EXISTS idx_${tableName}_${idx.replaceAll(/\W/g, '_')}
63
+ ON ${tableName} (json_extract(data, '${toJsonPath(idx)}'));`,
64
+ )
44
65
  }
45
66
 
46
67
  function getKeyFromDocument(document: Document): Key | undefined {
47
- return hasUserKey ? (document[key as keyof Document] as unknown as Key | undefined) : undefined
68
+ if (!hasUserKey) return undefined
69
+ return getByPath(document, String(key)) as Key | undefined
48
70
  }
49
71
 
50
72
  async function getChanges(conn: typeof backend): Promise<number> {
@@ -87,14 +109,13 @@ export async function createTable<Document extends DocType>(options: DbOptions<D
87
109
  const rows = await db.select<Array<{ id: number }>>(`SELECT last_insert_rowid() AS id`)
88
110
  const rowid = rows[0]?.id
89
111
  if (typeof rowid !== 'number') throw new Error('Failed to retrieve last_insert_rowid()')
90
- const result: MutationResult = { key: rowid, op: 'insert' }
91
- return result
112
+ return { key: rowid, op: 'insert' }
92
113
  },
93
114
 
94
- // --- FIXED: include rowid ---
95
115
  async get<Selected = Document>(
96
116
  keyValue: Key,
97
- selector: (document: Document, meta: { rowid: number }) => Selected = (d, _m) => d as unknown as Selected,
117
+ // keep meta available and consistent with search() { rowId }
118
+ selector: (document: Document, meta: { rowId: number }) => Selected = (d) => d as unknown as Selected,
98
119
  ) {
99
120
  const whereKey = hasUserKey ? `key = ?` : `rowid = ?`
100
121
  const result = await backend.select<Array<{ data: string; rowid: number }>>(
@@ -102,23 +123,19 @@ export async function createTable<Document extends DocType>(options: DbOptions<D
102
123
  [keyValue],
103
124
  )
104
125
  if (result.length === 0) return
105
- const [item] = result
106
- const { data, rowid } = item
126
+ const { data, rowid } = result[0]
107
127
  const document = JSON.parse(data) as Document
108
- return selector(document, { rowid }) as Selected
128
+ return selector(document, { rowId: rowid }) as Selected
109
129
  },
110
130
 
111
131
  async delete(keyValue: Key) {
112
132
  const whereKey = hasUserKey ? `key = ?` : `rowid = ?`
113
133
  await backend.execute(`DELETE FROM ${tableName} WHERE ${whereKey}`, [keyValue])
114
134
  const changed = await backend.select<Array<{ c: number }>>(`SELECT changes() AS c`)
115
- if ((changed[0]?.c ?? 0) > 0) {
116
- return { key: keyValue, op: 'delete' }
117
- }
135
+ if ((changed[0]?.c ?? 0) > 0) return { key: keyValue, op: 'delete' }
118
136
  return
119
137
  },
120
138
 
121
- // --- FIXED: include rowid in search ---
122
139
  async *search<Selected = Document>(options: SearchOptions<Document, Selected> = {}): AsyncIterableIterator<Selected> {
123
140
  const {
124
141
  sortBy,
@@ -126,12 +143,12 @@ export async function createTable<Document extends DocType>(options: DbOptions<D
126
143
  limit,
127
144
  offset = 0,
128
145
  where,
129
- select = (document, _meta) => document as unknown as Selected,
146
+ select = (document) => document as unknown as Selected,
130
147
  stepSize = DEFAULT_STEP_SIZE,
131
148
  } = options
132
149
 
133
- let baseQuery = `SELECT rowid, data FROM ${tableName}`
134
- if (where) baseQuery += ' ' + getWhereQuery(where)
150
+ const whereSql = getWhereQuery<Document>(where)
151
+ const baseQuery = `SELECT rowid, data FROM ${tableName} ${whereSql}`
135
152
 
136
153
  let yielded = 0
137
154
  let currentOffset = offset
@@ -139,7 +156,7 @@ export async function createTable<Document extends DocType>(options: DbOptions<D
139
156
  let query = baseQuery
140
157
 
141
158
  if (sortBy) {
142
- query += ` ORDER BY json_extract(data, '$.${String(sortBy)}') COLLATE NOCASE ${order.toUpperCase()}`
159
+ query += ` ORDER BY json_extract(data, '${toJsonPath(String(sortBy))}') COLLATE NOCASE ${order.toUpperCase()}`
143
160
  } else {
144
161
  query += hasUserKey ? ` ORDER BY key COLLATE NOCASE ${order.toUpperCase()}` : ` ORDER BY rowid ${order.toUpperCase()}`
145
162
  }
@@ -163,28 +180,26 @@ export async function createTable<Document extends DocType>(options: DbOptions<D
163
180
  },
164
181
 
165
182
  async count(options: { where?: Where<Document> } = {}) {
166
- const { where } = options
167
- let query = `SELECT COUNT(*) as count FROM ${tableName}`
168
- if (where) query += ' ' + getWhereQuery(where)
183
+ const whereSql = getWhereQuery<Document>(options.where)
184
+ const query = `SELECT COUNT(*) as count FROM ${tableName} ${whereSql}`
169
185
  const result = await backend.select<Array<{ count: number }>>(query)
170
186
  return result[0]?.count ?? 0
171
187
  },
172
188
 
173
189
  async deleteBy(where: Where<Document>) {
174
- const whereQuery = getWhereQuery(where)
190
+ const whereSql = getWhereQuery<Document>(where)
175
191
  const keyCol = hasUserKey ? 'key' : 'rowid'
176
-
177
192
  const results: MutationResult[] = []
193
+
178
194
  await backend.transaction(async (tx) => {
179
- const rows = await tx.select<Array<{ k: Key }>>(`SELECT ${keyCol} AS k, rowid FROM ${tableName} ${whereQuery}`)
195
+ const rows = await tx.select<Array<{ k: Key }>>(`SELECT ${keyCol} AS k FROM ${tableName} ${whereSql}`)
180
196
  if (rows.length === 0) return
181
197
 
182
198
  const allKeys = rows.map((r) => r.k)
183
-
184
199
  for (let index = 0; index < allKeys.length; index += DELETE_IN_CHUNK) {
185
200
  const chunk = allKeys.slice(index, index + DELETE_IN_CHUNK)
186
201
  const placeholders = chunk.map(() => '?').join(',')
187
- await tx.execute(`DELETE FROM ${tableName} WHERE ${keyCol} IN (${placeholders})`, chunk as unknown as unknown[])
202
+ await tx.execute(`DELETE FROM ${tableName} WHERE ${keyCol} IN (${placeholders})`, chunk as unknown[])
188
203
  }
189
204
 
190
205
  for (const k of allKeys) results.push({ key: k, op: 'delete' })
@@ -6,36 +6,41 @@ import type { Where } from './where'
6
6
  export type DocType = { [key: string]: any }
7
7
  export type KeyTypeAvailable = 'string' | 'number'
8
8
 
9
+ // Expand all nested keys into dot-paths
10
+ export type DotPrefix<T extends string> = T extends '' ? '' : `.${T}`
11
+
12
+ type Previous = [never, 0, 1, 2, 3, 4, 5]
13
+
14
+ export type DotPath<T, D extends number = 5> = [D] extends [never]
15
+ ? never
16
+ : T extends object
17
+ ? {
18
+ [K in Extract<keyof T, string>]: T[K] extends object ? K | `${K}.${DotPath<T[K], Previous[D]>}` : K
19
+ }[Extract<keyof T, string>]
20
+ : never
21
+ // Replace keyof Document with DotPath<Document>
9
22
  export interface DbOptions<Document extends DocType> {
10
- readonly sorBy?: keyof Document
11
- readonly order?: 'asc' | 'desc'
12
23
  readonly tableName: string
13
- readonly indexes?: Array<keyof Document>
24
+ readonly indexes?: Array<DotPath<Document>>
14
25
  readonly backend: Backend
15
- /**
16
- * Optional key. If omitted, the table uses implicit SQLite ROWID as the key.
17
- */
18
- readonly key?: keyof Document
26
+ readonly key?: DotPath<Document>
19
27
  readonly disablePragmaOptimization?: boolean
20
28
  }
21
29
 
22
- interface DbNotGeneric {
23
- readonly backend: Backend
24
- }
25
-
26
30
  export interface SearchOptions<Document extends DocType, Selected = Document> {
27
- readonly sortBy?: keyof Document
31
+ readonly sortBy?: DotPath<Document>
28
32
  readonly order?: 'asc' | 'desc'
29
33
  readonly limit?: number
30
34
  readonly offset?: number
31
35
  readonly where?: Where<Document>
32
36
  readonly stepSize?: number
33
- /**
34
- * Naive projection. Prefer specialized queries for heavy fan-out graphs.
35
- */
36
37
  readonly select?: (document: Document, meta: { rowId: number }) => Selected
37
38
  }
38
39
 
40
+ interface DbNotGeneric {
41
+ readonly backend: Backend
42
+ }
43
+
39
44
  export type Key = string | number
40
45
 
41
46
  export type MutationOp = 'insert' | 'update' | 'delete'
@@ -1,267 +1,182 @@
1
+ /* eslint-disable unicorn/no-array-callback-reference */
2
+ /* eslint-disable unicorn/no-nested-ternary */
3
+ /* eslint-disable no-nested-ternary */
1
4
  /* eslint-disable sonarjs/no-nested-conditional */
2
5
  /* eslint-disable sonarjs/cognitive-complexity */
6
+
3
7
  // -------------------------------------------------------------
4
- // Simplified `Where` type: each field may be a `Condition`
5
- // *or* directly a `Document[K]`/`Document[K][]`, shorthand for "is"/"in".
6
- // We also allow the special literal "KEY" to filter by the primary‐key column.
8
+ // Condition operators for each field
7
9
  // -------------------------------------------------------------
8
-
9
- export interface Field {
10
- readonly table: string
11
- readonly field: string
10
+ interface Condition<T> {
11
+ readonly is?: T | T[]
12
+ readonly isNot?: T | T[]
13
+ readonly gt?: T
14
+ readonly gte?: T
15
+ readonly lt?: T
16
+ readonly lte?: T
17
+ readonly in?: T[]
18
+ readonly notIn?: T[]
19
+ readonly like?: T | T[]
12
20
  }
13
21
 
14
- interface Condition<Document extends Record<string, unknown>, K extends keyof Document = keyof Document> {
15
- readonly is?: Document[K] | Array<Document[K]>
16
- readonly isNot?: Document[K] | Array<Document[K]>
17
- readonly gt?: Document[K] | Array<Document[K]>
18
- readonly gte?: Document[K] | Array<Document[K]>
19
- readonly lt?: Document[K] | Array<Document[K]>
20
- readonly lte?: Document[K] | Array<Document[K]>
21
- readonly in?: Document[K] | Array<Document[K]>
22
- readonly notIn?: Document[K] | Array<Document[K]>
23
- readonly like?: Document[K] | Array<Document[K]>
24
- }
25
-
26
- /**
27
- * We extend `keyof Document` by the special literal "KEY".
28
- * That means users can write `{ KEY: ... }` in addition to `{ someField: ... }`.
29
- *
30
- * - If K extends keyof Document, then primitive values must match Document[K].
31
- * - If K === "KEY", then primitive values are treated as strings/Array<string>.
32
- */
33
- export type Where<Document extends Record<string, unknown>> =
22
+ // -------------------------------------------------------------
23
+ // Where type: recursive object with operators or nested fields
24
+ // -------------------------------------------------------------
25
+ export type Where<T extends Record<string, unknown>> =
34
26
  | {
35
- [K in keyof Document | 'KEY']?:
36
- | Condition<Document, K extends keyof Document ? K : keyof Document>
37
- | (K extends keyof Document ? Document[K] : string)
38
- | (K extends keyof Document ? Array<Document[K]> : string[])
27
+ [K in keyof T]?: T[K] extends Record<string, unknown>
28
+ ? Where<T[K]> // nested object
29
+ : Condition<T[K]> | T[K] | T[K][]
39
30
  }
40
31
  | {
41
- readonly AND?: Array<Where<Document>>
42
- readonly OR?: Array<Where<Document>>
43
- readonly NOT?: Where<Document>
32
+ readonly AND?: Array<Where<T>>
33
+ readonly OR?: Array<Where<T>>
34
+ readonly NOT?: Where<T>
44
35
  }
45
36
 
46
37
  // -------------------------------------------------------------
47
- // A tiny helper to escape/inline a single primitive into SQL.
38
+ // Tiny helpers
48
39
  // -------------------------------------------------------------
49
40
  function inlineValue(value: unknown): string {
50
- if (typeof value === 'string') {
51
- return `'${(value as string).split("'").join("''")}'`
52
- }
53
- if (typeof value === 'number') {
54
- return (value as number).toString()
55
- }
56
- if (typeof value === 'boolean') {
57
- return (value as boolean) ? '1' : '0'
58
- }
59
- return `'${String(value).split("'").join("''")}'`
41
+ if (typeof value === 'string') return `'${value.replaceAll("'", "''")}'`
42
+ if (typeof value === 'number') return value.toString()
43
+ if (typeof value === 'boolean') return value ? '1' : '0'
44
+ return `'${String(value).replaceAll("'", "''")}'`
60
45
  }
61
46
 
62
- // -------------------------------------------------------------
63
- // Build the expression for a given field.
64
- // If field === "KEY", refer directly to the primary‐key column (`key`).
65
- // Otherwise, extract from JSON `data`.
66
- // -------------------------------------------------------------
67
47
  function getFieldExpr(field: string, value: unknown, tableAlias?: string): string {
68
48
  const prefix = tableAlias ? `${tableAlias}.` : ''
69
49
  if (field === 'KEY') {
70
- // Use double‐quotes around key to avoid conflicts with reserved words
71
50
  return `"${prefix}key"`
72
51
  }
73
-
74
- // Otherwise, treat as JSON field under "data"
75
- if (typeof value === 'string') {
76
- return `CAST(json_extract(${prefix}data, '$.${field}') AS TEXT)`
77
- }
78
- if (typeof value === 'boolean') {
79
- return `CAST(json_extract(${prefix}data, '$.${field}') AS INTEGER)`
80
- }
81
- if (typeof value === 'number') {
82
- return `CAST(json_extract(${prefix}data, '$.${field}') AS NUMERIC)`
83
- }
52
+ if (typeof value === 'string') return `CAST(json_extract(${prefix}data, '$.${field}') AS TEXT)`
53
+ if (typeof value === 'number') return `CAST(json_extract(${prefix}data, '$.${field}') AS NUMERIC)`
54
+ if (typeof value === 'boolean') return `CAST(json_extract(${prefix}data, '$.${field}') AS INTEGER)`
84
55
  return `json_extract(${prefix}data, '$.${field}')`
85
56
  }
86
57
 
58
+ const OPS_SET: ReadonlySet<string> = new Set(['is', 'isNot', 'gt', 'gte', 'lt', 'lte', 'in', 'notIn', 'like'])
59
+
87
60
  // -------------------------------------------------------------
88
- // Valid operators set (for quick membership checks).
61
+ // Flatten nested objects to dot-paths
89
62
  // -------------------------------------------------------------
90
- const OPS_SET: ReadonlySet<string> = new Set(['is', 'isNot', 'gt', 'gte', 'lt', 'lte', 'in', 'notIn', 'like'])
63
+ function flattenWhere(object: Record<string, unknown>, prefix = ''): Record<string, unknown> {
64
+ const result: Record<string, unknown> = {}
91
65
 
92
- function isUndefined(value: unknown): value is undefined {
93
- return value === undefined
66
+ for (const [k, v] of Object.entries(object)) {
67
+ if (k === 'AND' || k === 'OR' || k === 'NOT') {
68
+ result[k] = v
69
+ continue
70
+ }
71
+
72
+ const path = prefix ? `${prefix}.${k}` : k
73
+
74
+ if (v && typeof v === 'object' && !Array.isArray(v) && !Object.keys(v).some((kk) => OPS_SET.has(kk))) {
75
+ Object.assign(result, flattenWhere(v as Record<string, unknown>, path))
76
+ } else {
77
+ result[path] = v
78
+ }
79
+ }
80
+
81
+ return result
94
82
  }
95
83
 
96
84
  // -------------------------------------------------------------
97
- // Main recursive parser: turn a `Where<Document>` into a SQL clause
98
- // (without the leading "WHERE").
85
+ // Main recursive builder
99
86
  // -------------------------------------------------------------
100
- export function getWhere<Document extends Record<string, unknown>>(where: Where<Document>, tableAlias?: string): string {
101
- if (!where || typeof where !== 'object') {
102
- return ''
103
- }
87
+ export function getWhere<T extends Record<string, unknown>>(where: Where<T>, tableAlias?: string): string {
88
+ if (!where || typeof where !== 'object') return ''
104
89
 
105
- // ----- Logical branches: AND / OR / NOT -----
106
- if (!isUndefined(where.AND)) {
107
- const array = where.AND as Array<Where<Document>>
108
- if (Array.isArray(array) && array.length > 0) {
109
- let combined = ''
110
- let firstAdded = false
111
- for (const sub of array) {
112
- const clause = getWhere(sub, tableAlias)
113
- if (!clause) continue
114
- if (firstAdded) combined += ' AND '
115
- combined += clause
116
- firstAdded = true
117
- }
118
- return firstAdded ? `(${combined})` : ''
119
- }
120
- return ''
90
+ // ----- Logical branches -----
91
+ if (where.AND) {
92
+ const clauses = Array.isArray(where.AND) ? where.AND.map((w) => getWhere(w, tableAlias)).filter(Boolean) : []
93
+ return clauses.length > 0 ? `(${clauses.join(' AND ')})` : ''
121
94
  }
122
95
 
123
- if (!isUndefined(where.OR)) {
124
- const array = where.OR as Array<Where<Document>>
125
- if (Array.isArray(array) && array.length > 0) {
126
- let combined = ''
127
- let firstAdded = false
128
- for (const sub of array) {
129
- const clause = getWhere(sub, tableAlias)
130
- if (!clause) continue
131
- if (firstAdded) combined += ' OR '
132
- combined += clause
133
- firstAdded = true
134
- }
135
- return firstAdded ? `(${combined})` : ''
136
- }
137
- return ''
96
+ if (where.OR) {
97
+ const clauses = Array.isArray(where.OR) ? where.OR.map((w) => getWhere(w, tableAlias)).filter(Boolean) : []
98
+ return clauses.length > 0 ? `(${clauses.join(' OR ')})` : ''
138
99
  }
139
100
 
140
- if (!isUndefined(where.NOT)) {
141
- const sub = where.NOT as Where<Document>
142
- if (sub && typeof sub === 'object') {
143
- const clause = getWhere(sub, tableAlias)
144
- return clause ? `(NOT ${clause})` : ''
145
- }
146
- return ''
101
+ if (where.NOT) {
102
+ const clause = getWhere(where.NOT, tableAlias)
103
+ return clause ? `(NOT ${clause})` : ''
147
104
  }
148
105
 
149
- // ----- Field‐based conditions: default is AND across fields -----
106
+ // ----- Field conditions -----
107
+ const flat = flattenWhere(where as Record<string, unknown>)
150
108
  let fieldClauses = ''
151
- let anyFieldClause = false
109
+ let anyField = false
152
110
 
153
- for (const key in where as Record<string, unknown>) {
154
- if (key === 'AND' || key === 'OR' || key === 'NOT') continue
155
-
156
- const rawValue = (where as Record<string, unknown>)[key]
111
+ for (const [key, rawValue] of Object.entries(flat)) {
157
112
  if (rawValue == null) continue
158
113
 
159
- // If the user provided a primitive or an array, coerce it to a Condition:
160
- // - single primitive → { is: rawVal }
161
- // - array → { in: rawVal }
162
- let cond: Condition<Document, typeof key>
114
+ // coerce primitive/array into Condition
115
+ let cond: Condition<unknown>
163
116
  if (typeof rawValue !== 'object' || Array.isArray(rawValue)) {
164
- cond = Array.isArray(rawValue) ? { in: rawValue } : ({ is: rawValue } as Condition<Document, typeof key>)
117
+ cond = Array.isArray(rawValue) ? { in: rawValue } : { is: rawValue }
165
118
  } else {
166
- cond = rawValue as Condition<Document, typeof key>
119
+ cond = rawValue as Condition<unknown>
167
120
  }
168
121
 
169
- // Iterate only over real operator keys that exist on this `cond`
170
- for (const opKey of Object.keys(cond) as Array<keyof typeof cond>) {
171
- if (!OPS_SET.has(opKey as string)) continue
172
- const rawOpValue = cond[opKey]
173
- if (rawOpValue == null) continue
122
+ for (const opKey of Object.keys(cond)) {
123
+ if (!OPS_SET.has(opKey)) continue
124
+ const opValue = cond[opKey as keyof Condition<unknown>]
125
+ if (opValue == null) continue
174
126
 
175
- // Always treat it as an array for uniformity:
176
- const array = Array.isArray(rawOpValue) ? (rawOpValue as unknown[]) : [rawOpValue]
177
- if (array.length === 0) continue
127
+ const values = Array.isArray(opValue) ? opValue : [opValue]
128
+ if (values.length === 0) continue
178
129
 
179
- // Handle `is` / `isNot` / `in` / `notIn`
130
+ // is / isNot / in / notIn
180
131
  if (opKey === 'is' || opKey === 'isNot' || opKey === 'in' || opKey === 'notIn') {
181
- const [firstValue] = array
182
- const fieldExpr = getFieldExpr(key, firstValue, tableAlias)
183
-
184
- // Build comma‐separated list without using `.map()`
185
- let inList = ''
186
- if (array.length > 1) {
187
- for (const [index, elt] of array.entries()) {
188
- if (index > 0) inList += ','
189
- inList += inlineValue(elt)
190
- }
191
- }
192
-
193
- switch (opKey) {
194
- case 'is': {
195
- fieldClauses +=
196
- array.length > 1
197
- ? (anyFieldClause ? ' AND ' : '') + `${fieldExpr} IN (${inList})`
198
- : (anyFieldClause ? ' AND ' : '') + `${fieldExpr} = ${inlineValue(array[0])}`
199
- break
200
- }
201
- case 'isNot': {
202
- fieldClauses +=
203
- array.length > 1
204
- ? (anyFieldClause ? ' AND ' : '') + `${fieldExpr} NOT IN (${inList})`
205
- : (anyFieldClause ? ' AND ' : '') + `${fieldExpr} <> ${inlineValue(array[0])}`
206
- break
207
- }
208
- case 'in': {
209
- fieldClauses +=
210
- array.length > 1
211
- ? (anyFieldClause ? ' AND ' : '') + `${fieldExpr} IN (${inList})`
212
- : (anyFieldClause ? ' AND ' : '') + `${fieldExpr} IN (${inlineValue(array[0])})`
213
- break
214
- }
215
- case 'notIn': {
216
- fieldClauses +=
217
- array.length > 1
218
- ? (anyFieldClause ? ' AND ' : '') + `${fieldExpr} NOT IN (${inList})`
219
- : (anyFieldClause ? ' AND ' : '') + `${fieldExpr} NOT IN (${inlineValue(array[0])})`
220
- break
221
- }
222
- }
223
-
224
- anyFieldClause = true
132
+ const fieldExpr = getFieldExpr(key, values[0], tableAlias)
133
+ const inList = values.map(inlineValue).join(',')
134
+
135
+ const clause =
136
+ opKey === 'is'
137
+ ? values.length > 1
138
+ ? `${fieldExpr} IN (${inList})`
139
+ : `${fieldExpr} = ${inlineValue(values[0])}`
140
+ : opKey === 'isNot'
141
+ ? values.length > 1
142
+ ? `${fieldExpr} NOT IN (${inList})`
143
+ : `${fieldExpr} <> ${inlineValue(values[0])}`
144
+ : opKey === 'in'
145
+ ? `${fieldExpr} IN (${inList})`
146
+ : `${fieldExpr} NOT IN (${inList})`
147
+
148
+ fieldClauses += (anyField ? ' AND ' : '') + clause
149
+ anyField = true
225
150
  continue
226
151
  }
227
152
 
228
- // Handle comparisons: gt, gte, lt, lte, like
229
- for (const v of array) {
153
+ // gt / gte / lt / lte / like
154
+ for (const v of values) {
230
155
  const fieldExpr = getFieldExpr(key, v, tableAlias)
231
- switch (opKey) {
232
- case 'gt': {
233
- fieldClauses += (anyFieldClause ? ' AND ' : '') + `${fieldExpr} > ${inlineValue(v)}`
234
- break
235
- }
236
- case 'gte': {
237
- fieldClauses += (anyFieldClause ? ' AND ' : '') + `${fieldExpr} >= ${inlineValue(v)}`
238
- break
239
- }
240
- case 'lt': {
241
- fieldClauses += (anyFieldClause ? ' AND ' : '') + `${fieldExpr} < ${inlineValue(v)}`
242
- break
243
- }
244
- case 'lte': {
245
- fieldClauses += (anyFieldClause ? ' AND ' : '') + `${fieldExpr} <= ${inlineValue(v)}`
246
- break
247
- }
248
- case 'like': {
249
- fieldClauses += (anyFieldClause ? ' AND ' : '') + `${fieldExpr} LIKE ${inlineValue(v)}`
250
- break
251
- }
252
- }
253
- anyFieldClause = true
156
+ const clause =
157
+ opKey === 'gt'
158
+ ? `${fieldExpr} > ${inlineValue(v)}`
159
+ : opKey === 'gte'
160
+ ? `${fieldExpr} >= ${inlineValue(v)}`
161
+ : opKey === 'lt'
162
+ ? `${fieldExpr} < ${inlineValue(v)}`
163
+ : opKey === 'lte'
164
+ ? `${fieldExpr} <= ${inlineValue(v)}`
165
+ : `${fieldExpr} LIKE ${inlineValue(v)}`
166
+ fieldClauses += (anyField ? ' AND ' : '') + clause
167
+ anyField = true
254
168
  }
255
169
  }
256
170
  }
257
171
 
258
- return anyFieldClause ? `(${fieldClauses})` : ''
172
+ return anyField ? `(${fieldClauses})` : ''
259
173
  }
260
174
 
261
175
  // -------------------------------------------------------------
262
- // Wrap `parse(...)` in "WHERE (…)". If empty, return "".
176
+ // Public wrapper: adds "WHERE" if needed
263
177
  // -------------------------------------------------------------
264
- export function getWhereQuery<Document extends Record<string, unknown>>(where: Where<Document>): string {
178
+ export function getWhereQuery<T extends Record<string, unknown>>(where?: Where<T>): string {
179
+ if (!where) return ''
265
180
  const clause = getWhere(where)
266
181
  return clause ? `WHERE ${clause}` : ''
267
182
  }
@@ -3,6 +3,9 @@ import type { Backend } from './table';
3
3
  import type { DbOptions, DocType, Key, MutationResult, SearchOptions } from './table/table.types';
4
4
  import type { Where } from './table/where';
5
5
  type SearchId = string;
6
+ export interface CreateSqliteOptions<Document extends DocType> extends Omit<DbOptions<Document>, 'backend'> {
7
+ readonly backend: Backend | Promise<Backend>;
8
+ }
6
9
  export interface SyncTable<Document extends DocType> {
7
10
  readonly updateSearchOptions: <Selected = Document>(searchId: SearchId, options: SearchOptions<Document, Selected>) => void;
8
11
  readonly subscribe: (searchId: SearchId, listener: () => void) => () => void;
@@ -21,8 +24,5 @@ export interface SyncTable<Document extends DocType> {
21
24
  readonly next: (searchId: SearchId) => Promise<boolean>;
22
25
  readonly select: <Params extends unknown[]>(compute: (...args: Params) => SearchOptions<Document>) => CreateState<Document, Params>;
23
26
  }
24
- export interface ReactDbOptions<Document extends DocType> extends Omit<DbOptions<Document>, 'backend'> {
25
- readonly backend: Backend | Promise<Backend>;
26
- }
27
- export declare function createSqliteState<Document extends DocType>(options: ReactDbOptions<Document>): SyncTable<Document>;
27
+ export declare function createSqliteState<Document extends DocType>(options: CreateSqliteOptions<Document>): SyncTable<Document>;
28
28
  export {};
@@ -1,3 +1,4 @@
1
1
  import type { Table, DbOptions, DocType } from './table.types';
2
2
  export declare const DEFAULT_STEP_SIZE = 100;
3
+ export declare function getByPath<T extends object>(object: T, path: string): unknown;
3
4
  export declare function createTable<Document extends DocType>(options: DbOptions<Document>): Promise<Table<Document>>;
@@ -4,35 +4,32 @@ export type DocType = {
4
4
  [key: string]: any;
5
5
  };
6
6
  export type KeyTypeAvailable = 'string' | 'number';
7
+ export type DotPrefix<T extends string> = T extends '' ? '' : `.${T}`;
8
+ type Previous = [never, 0, 1, 2, 3, 4, 5];
9
+ export type DotPath<T, D extends number = 5> = [D] extends [never] ? never : T extends object ? {
10
+ [K in Extract<keyof T, string>]: T[K] extends object ? K | `${K}.${DotPath<T[K], Previous[D]>}` : K;
11
+ }[Extract<keyof T, string>] : never;
7
12
  export interface DbOptions<Document extends DocType> {
8
- readonly sorBy?: keyof Document;
9
- readonly order?: 'asc' | 'desc';
10
13
  readonly tableName: string;
11
- readonly indexes?: Array<keyof Document>;
14
+ readonly indexes?: Array<DotPath<Document>>;
12
15
  readonly backend: Backend;
13
- /**
14
- * Optional key. If omitted, the table uses implicit SQLite ROWID as the key.
15
- */
16
- readonly key?: keyof Document;
16
+ readonly key?: DotPath<Document>;
17
17
  readonly disablePragmaOptimization?: boolean;
18
18
  }
19
- interface DbNotGeneric {
20
- readonly backend: Backend;
21
- }
22
19
  export interface SearchOptions<Document extends DocType, Selected = Document> {
23
- readonly sortBy?: keyof Document;
20
+ readonly sortBy?: DotPath<Document>;
24
21
  readonly order?: 'asc' | 'desc';
25
22
  readonly limit?: number;
26
23
  readonly offset?: number;
27
24
  readonly where?: Where<Document>;
28
25
  readonly stepSize?: number;
29
- /**
30
- * Naive projection. Prefer specialized queries for heavy fan-out graphs.
31
- */
32
26
  readonly select?: (document: Document, meta: {
33
27
  rowId: number;
34
28
  }) => Selected;
35
29
  }
30
+ interface DbNotGeneric {
31
+ readonly backend: Backend;
32
+ }
36
33
  export type Key = string | number;
37
34
  export type MutationOp = 'insert' | 'update' | 'delete';
38
35
  export interface MutationResult {
@@ -1,32 +1,21 @@
1
- export interface Field {
2
- readonly table: string;
3
- readonly field: string;
1
+ interface Condition<T> {
2
+ readonly is?: T | T[];
3
+ readonly isNot?: T | T[];
4
+ readonly gt?: T;
5
+ readonly gte?: T;
6
+ readonly lt?: T;
7
+ readonly lte?: T;
8
+ readonly in?: T[];
9
+ readonly notIn?: T[];
10
+ readonly like?: T | T[];
4
11
  }
5
- interface Condition<Document extends Record<string, unknown>, K extends keyof Document = keyof Document> {
6
- readonly is?: Document[K] | Array<Document[K]>;
7
- readonly isNot?: Document[K] | Array<Document[K]>;
8
- readonly gt?: Document[K] | Array<Document[K]>;
9
- readonly gte?: Document[K] | Array<Document[K]>;
10
- readonly lt?: Document[K] | Array<Document[K]>;
11
- readonly lte?: Document[K] | Array<Document[K]>;
12
- readonly in?: Document[K] | Array<Document[K]>;
13
- readonly notIn?: Document[K] | Array<Document[K]>;
14
- readonly like?: Document[K] | Array<Document[K]>;
15
- }
16
- /**
17
- * We extend `keyof Document` by the special literal "KEY".
18
- * That means users can write `{ KEY: ... }` in addition to `{ someField: ... }`.
19
- *
20
- * - If K extends keyof Document, then primitive values must match Document[K].
21
- * - If K === "KEY", then primitive values are treated as strings/Array<string>.
22
- */
23
- export type Where<Document extends Record<string, unknown>> = {
24
- [K in keyof Document | 'KEY']?: Condition<Document, K extends keyof Document ? K : keyof Document> | (K extends keyof Document ? Document[K] : string) | (K extends keyof Document ? Array<Document[K]> : string[]);
12
+ export type Where<T extends Record<string, unknown>> = {
13
+ [K in keyof T]?: T[K] extends Record<string, unknown> ? Where<T[K]> : Condition<T[K]> | T[K] | T[K][];
25
14
  } | {
26
- readonly AND?: Array<Where<Document>>;
27
- readonly OR?: Array<Where<Document>>;
28
- readonly NOT?: Where<Document>;
15
+ readonly AND?: Array<Where<T>>;
16
+ readonly OR?: Array<Where<T>>;
17
+ readonly NOT?: Where<T>;
29
18
  };
30
- export declare function getWhere<Document extends Record<string, unknown>>(where: Where<Document>, tableAlias?: string): string;
31
- export declare function getWhereQuery<Document extends Record<string, unknown>>(where: Where<Document>): string;
19
+ export declare function getWhere<T extends Record<string, unknown>>(where: Where<T>, tableAlias?: string): string;
20
+ export declare function getWhereQuery<T extends Record<string, unknown>>(where?: Where<T>): string;
32
21
  export {};