muya 2.4.93 → 2.4.94
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/esm/sqlite/__tests__/where.test.js +1 -0
- package/esm/sqlite/table/where.js +1 -1
- package/package.json +1 -1
- package/src/sqlite/__tests__/where.test.ts +234 -0
- package/src/sqlite/table/table.types.ts +8 -2
- package/src/sqlite/table/where.ts +11 -8
- package/types/sqlite/table/table.types.d.ts +6 -2
- package/types/sqlite/table/where.d.ts +7 -5
|
@@ -0,0 +1 @@
|
|
|
1
|
+
import{createTable as n}from"../table";import{bunMemoryBackend as c}from"../table/bun-backend";describe("where clauses",()=>{const a=c();it("should handle where where array of conditions",async()=>{const e=await n({backend:a,tableName:"TestTableNestedOptional",key:"id",indexes:["content"]});await e.set({id:"1",content:"The quick brown fox"}),await e.set({id:"2",content:"The jumps over the lazy dog"}),await e.set({id:"3"});const t=[];for await(const i of e.search({where:{content:{like:["The%"]}}}))t.push(i);expect(t.length).toBe(2),expect(t[0].id).toBe("1")}),it("should create nested index for optional nested fields",async()=>{const e=await n({backend:a,tableName:"TestTableNestedOptional",key:"id",indexes:["fts:info.content"]});await e.set({id:"1",info:{content:"The quick brown fox"}}),await e.set({id:"2",info:{content:"jumps over the lazy dog"}}),await e.set({id:"3"});const t=[];for await(const s of e.search({where:{info:{content:{like:["The%"]}}}}))t.push(s);expect(t.length).toBe(1),expect(t[0].id).toBe("1");const i=[];for await(const s of e.search({where:{OR:[{info:{content:{like:["The%"]}}},{info:{content:{like:["jumps%"]}}}]}}))i.push(s);expect(i.length).toBe(2);const o=[];for await(const s of e.search({where:{info:{content:"nonexistent"}}}))o.push(s);expect(o.length).toBe(0)}),it("should handle FTS queries",async()=>{const e=await n({backend:a,tableName:"TestTableFts",key:"id",indexes:["fts:content"]});await e.set({id:"1",content:"The quick brown fox"}),await e.set({id:"2",content:"Jumps over the lazy dog"}),await e.set({id:"3",content:"Another document"});const t=[];for await(const i of e.search({where:{content:{fts:"quick"}}}))t.push(i);expect(t.length).toBe(1),expect(t[0].id).toBe("1")}),it("should handle nested where conditions",async()=>{const e=await n({backend:a,tableName:"TestTableNested",key:"id",indexes:["info.content"]});await e.set({id:"1",info:{content:"Nested quick brown fox"}}),await e.set({id:"2",info:{content:"Nested jumps over the lazy dog"}});const t=[];for await(const i of e.search({where:{info:{content:{like:"Nested%"}}}}))t.push(i);expect(t.length).toBe(2)}),it("should handle complex operators",async()=>{const e=await n({backend:a,tableName:"TestTableComplex",key:"id",indexes:["value"]});await e.set({id:"1",value:10}),await e.set({id:"2",value:20}),await e.set({id:"3",value:30});const t=[];for await(const i of e.search({where:{value:{gt:15,lt:25}},sortBy:"value"}))t.push(i);expect(t.length).toBe(1),expect(t[0].id).toBe("2")}),it("should handle NOT conditions",async()=>{const e=await n({backend:a,tableName:"TestTableNot",key:"id",indexes:["value"]});await e.set({id:"1",value:"apple"}),await e.set({id:"2",value:"banana"}),await e.set({id:"3",value:"cherry"});const t=[];for await(const i of e.search({where:{NOT:{value:"banana"}}}))t.push(i);expect(t.length).toBe(2),expect(t.map(i=>i.value)).toEqual(["apple","cherry"])}),it("should handle AND conditions",async()=>{const e=await n({backend:a,tableName:"TestTableAnd",key:"id",indexes:["category","price"]});await e.set({id:"1",category:"fruit",price:10}),await e.set({id:"2",category:"fruit",price:20}),await e.set({id:"3",category:"vegetable",price:15});const t=[];for await(const i of e.search({where:{AND:[{category:"fruit"},{price:{lt:15}}]}}))t.push(i);expect(t.length).toBe(1),expect(t[0].id).toBe("1")}),it("should handle OR conditions",async()=>{const e=await n({backend:a,tableName:"TestTableOr",key:"id",indexes:["type"]});await e.set({id:"1",type:"A"}),await e.set({id:"2",type:"B"}),await e.set({id:"3",type:"C"});const t=[];for await(const i of e.search({where:{OR:[{type:"A"},{type:"C"}]}}))t.push(i);expect(t.length).toBe(2),expect(t.map(i=>i.type)).toEqual(["A","C"])}),it("should handle nested AND/OR/NOT conditions",async()=>{const e=await n({backend:a,tableName:"TestTableNestedLogic",key:"id",indexes:["category","price"]});await e.set({id:"1",category:"fruit",price:10}),await e.set({id:"2",category:"fruit",price:20}),await e.set({id:"3",category:"fruit",price:15});const t={AND:[{category:{is:"fruit"}},{OR:[{price:{lt:15}},{price:{is:15}}]}]},i=[];for await(const s of e.search({}))i.push(s);expect(i.length).toBe(3);const o=[];for await(const s of e.search({where:t}))o.push(s);expect(o.length).toBe(2)})});
|
|
@@ -1 +1 @@
|
|
|
1
|
-
function
|
|
1
|
+
function $(n){return typeof n=="string"?`'${n.replaceAll("'","''")}'`:typeof n=="number"?n.toString():typeof n=="boolean"?n?"1":"0":`'${String(n).replaceAll("'","''")}'`}function A(n,r,t){const o=t?`${t}.`:"";return n==="KEY"?`"${o}key"`:typeof r=="string"?`CAST(json_extract(${o}data, '$.${n}') AS TEXT)`:typeof r=="number"?`CAST(json_extract(${o}data, '$.${n}') AS NUMERIC)`:typeof r=="boolean"?`CAST(json_extract(${o}data, '$.${n}') AS INTEGER)`:`json_extract(${o}data, '$.${n}')`}const p=new Set(["is","isNot","gt","gte","lt","lte","in","notIn","like","fts"]);function R(n,r=""){const t={};for(const[o,c]of Object.entries(n)){if(o==="AND"||o==="OR"||o==="NOT"){t[o]=c;continue}const a=r?`${r}.${o}`:o;c&&typeof c=="object"&&!Array.isArray(c)&&!Object.keys(c).some(i=>p.has(i))?Object.assign(t,R(c,a)):t[a]=c}return t}function T(n,r,t){if(!n||typeof n!="object")return"";if(n.AND){const i=Array.isArray(n.AND)?n.AND.map(u=>T(u,r,t)).filter(Boolean):[];return i.length>0?`(${i.join(" AND ")})`:""}if(n.OR){const i=Array.isArray(n.OR)?n.OR.map(u=>T(u,r,t)).filter(Boolean):[];return i.length>0?`(${i.join(" OR ")})`:""}if(n.NOT){const i=T(n.NOT,r,t);return i?`(NOT ${i})`:""}const o=R(n);let c="",a=!1;for(const[i,u]of Object.entries(o)){if(u==null)continue;let d;typeof u!="object"||Array.isArray(u)?d=Array.isArray(u)?{in:u}:{is:u}:d=u;for(const s of Object.keys(d)){const l=d[s];if(l==null)continue;const y=Array.isArray(l)?l:[l];if(y.length!==0){if(s==="fts"){if(!t)throw new Error("FTS requires tableName for JOIN reference");const e=y.map(f=>`EXISTS (SELECT 1 FROM ${t}_fts f WHERE f.rowid = ${r??t}.rowid AND ${t}_fts MATCH ${$(f)})`).join(" AND ");c+=(a?" AND ":"")+e,a=!0;continue}if(s==="is"||s==="isNot"||s==="in"||s==="notIn"){const e=A(i,y[0],r),f=y.map($).join(","),g=s==="is"?y.length>1?`${e} IN (${f})`:`${e} = ${$(y[0])}`:s==="isNot"?y.length>1?`${e} NOT IN (${f})`:`${e} <> ${$(y[0])}`:s==="in"?`${e} IN (${f})`:`${e} NOT IN (${f})`;c+=(a?" AND ":"")+g,a=!0;continue}for(const e of y){const f=A(i,e,r),g=s==="gt"?`${f} > ${$(e)}`:s==="gte"?`${f} >= ${$(e)}`:s==="lt"?`${f} < ${$(e)}`:s==="lte"?`${f} <= ${$(e)}`:`${f} LIKE ${$(e)}`;c+=(a?" AND ":"")+g,a=!0}}}}return a?`(${c})`:""}function k(n,r){if(!n)return"";const t=T(n,void 0,r);return t?`WHERE ${t}`:""}export{T as getWhere,k as getWhereQuery};
|
package/package.json
CHANGED
|
@@ -0,0 +1,234 @@
|
|
|
1
|
+
import { createTable } from '../table'
|
|
2
|
+
import { bunMemoryBackend } from '../table/bun-backend'
|
|
3
|
+
import { type Where } from '../table/where'
|
|
4
|
+
|
|
5
|
+
describe('where clauses', () => {
|
|
6
|
+
const backend = bunMemoryBackend()
|
|
7
|
+
|
|
8
|
+
it('should handle where where array of conditions', async () => {
|
|
9
|
+
const tableNestedOptional = await createTable<{ id: string; content?: string }>({
|
|
10
|
+
backend,
|
|
11
|
+
tableName: 'TestTableNestedOptional',
|
|
12
|
+
key: 'id',
|
|
13
|
+
indexes: ['content'],
|
|
14
|
+
})
|
|
15
|
+
|
|
16
|
+
await tableNestedOptional.set({ id: '1', content: 'The quick brown fox' })
|
|
17
|
+
await tableNestedOptional.set({ id: '2', content: 'The jumps over the lazy dog' })
|
|
18
|
+
await tableNestedOptional.set({ id: '3' }) // No `content` field
|
|
19
|
+
|
|
20
|
+
const results: { id: string; content?: string }[] = []
|
|
21
|
+
for await (const doc of tableNestedOptional.search({
|
|
22
|
+
where: { content: { like: ['The%'] } },
|
|
23
|
+
})) {
|
|
24
|
+
results.push(doc)
|
|
25
|
+
}
|
|
26
|
+
expect(results.length).toBe(2)
|
|
27
|
+
expect(results[0].id).toBe('1')
|
|
28
|
+
})
|
|
29
|
+
it('should create nested index for optional nested fields', async () => {
|
|
30
|
+
type TestDoc = { id: string; info?: { content?: string } }
|
|
31
|
+
const tableNestedOptional = await createTable<TestDoc>({
|
|
32
|
+
backend,
|
|
33
|
+
tableName: 'TestTableNestedOptional',
|
|
34
|
+
key: 'id',
|
|
35
|
+
indexes: ['fts:info.content'],
|
|
36
|
+
})
|
|
37
|
+
|
|
38
|
+
await tableNestedOptional.set({ id: '1', info: { content: 'The quick brown fox' } })
|
|
39
|
+
await tableNestedOptional.set({ id: '2', info: { content: 'jumps over the lazy dog' } })
|
|
40
|
+
await tableNestedOptional.set({ id: '3' }) // No `info` field
|
|
41
|
+
|
|
42
|
+
const results: TestDoc[] = []
|
|
43
|
+
for await (const doc of tableNestedOptional.search({
|
|
44
|
+
where: { info: { content: { like: ['The%'] } } },
|
|
45
|
+
})) {
|
|
46
|
+
results.push(doc)
|
|
47
|
+
}
|
|
48
|
+
expect(results.length).toBe(1)
|
|
49
|
+
expect(results[0].id).toBe('1')
|
|
50
|
+
|
|
51
|
+
const results2: TestDoc[] = []
|
|
52
|
+
for await (const doc of tableNestedOptional.search({
|
|
53
|
+
where: { OR: [{ info: { content: { like: ['The%'] } } }, { info: { content: { like: ['jumps%'] } } }] },
|
|
54
|
+
})) {
|
|
55
|
+
results2.push(doc)
|
|
56
|
+
}
|
|
57
|
+
expect(results2.length).toBe(2)
|
|
58
|
+
|
|
59
|
+
const results3: TestDoc[] = []
|
|
60
|
+
for await (const doc of tableNestedOptional.search({
|
|
61
|
+
where: { info: { content: 'nonexistent' } },
|
|
62
|
+
})) {
|
|
63
|
+
results3.push(doc)
|
|
64
|
+
}
|
|
65
|
+
expect(results3.length).toBe(0)
|
|
66
|
+
})
|
|
67
|
+
it('should handle FTS queries', async () => {
|
|
68
|
+
const tableFts = await createTable<{ id: string; content: string }>({
|
|
69
|
+
backend,
|
|
70
|
+
tableName: 'TestTableFts',
|
|
71
|
+
key: 'id',
|
|
72
|
+
indexes: ['fts:content'],
|
|
73
|
+
})
|
|
74
|
+
|
|
75
|
+
await tableFts.set({ id: '1', content: 'The quick brown fox' })
|
|
76
|
+
await tableFts.set({ id: '2', content: 'Jumps over the lazy dog' })
|
|
77
|
+
await tableFts.set({ id: '3', content: 'Another document' })
|
|
78
|
+
|
|
79
|
+
const results: { id: string; content: string }[] = []
|
|
80
|
+
for await (const doc of tableFts.search({
|
|
81
|
+
where: { content: { fts: 'quick' } },
|
|
82
|
+
})) {
|
|
83
|
+
results.push(doc)
|
|
84
|
+
}
|
|
85
|
+
expect(results.length).toBe(1)
|
|
86
|
+
expect(results[0].id).toBe('1')
|
|
87
|
+
})
|
|
88
|
+
|
|
89
|
+
it('should handle nested where conditions', async () => {
|
|
90
|
+
const tableNested = await createTable<{ id: string; info: { content: string } }>({
|
|
91
|
+
backend,
|
|
92
|
+
tableName: 'TestTableNested',
|
|
93
|
+
key: 'id',
|
|
94
|
+
indexes: ['info.content'],
|
|
95
|
+
})
|
|
96
|
+
|
|
97
|
+
await tableNested.set({ id: '1', info: { content: 'Nested quick brown fox' } })
|
|
98
|
+
await tableNested.set({ id: '2', info: { content: 'Nested jumps over the lazy dog' } })
|
|
99
|
+
|
|
100
|
+
const results: { id: string; info: { content: string } }[] = []
|
|
101
|
+
for await (const doc of tableNested.search({
|
|
102
|
+
where: { info: { content: { like: 'Nested%' } } },
|
|
103
|
+
})) {
|
|
104
|
+
results.push(doc)
|
|
105
|
+
}
|
|
106
|
+
expect(results.length).toBe(2)
|
|
107
|
+
})
|
|
108
|
+
|
|
109
|
+
it('should handle complex operators', async () => {
|
|
110
|
+
const tableComplex = await createTable<{ id: string; value: number }>({
|
|
111
|
+
backend,
|
|
112
|
+
tableName: 'TestTableComplex',
|
|
113
|
+
key: 'id',
|
|
114
|
+
indexes: ['value'],
|
|
115
|
+
})
|
|
116
|
+
|
|
117
|
+
await tableComplex.set({ id: '1', value: 10 })
|
|
118
|
+
await tableComplex.set({ id: '2', value: 20 })
|
|
119
|
+
await tableComplex.set({ id: '3', value: 30 })
|
|
120
|
+
|
|
121
|
+
const results: { id: string; value: number }[] = []
|
|
122
|
+
for await (const doc of tableComplex.search({
|
|
123
|
+
where: { value: { gt: 15, lt: 25 } },
|
|
124
|
+
sortBy: 'value',
|
|
125
|
+
})) {
|
|
126
|
+
results.push(doc)
|
|
127
|
+
}
|
|
128
|
+
expect(results.length).toBe(1)
|
|
129
|
+
expect(results[0].id).toBe('2')
|
|
130
|
+
})
|
|
131
|
+
|
|
132
|
+
it('should handle NOT conditions', async () => {
|
|
133
|
+
const tableNot = await createTable<{ id: string; value: string }>({
|
|
134
|
+
backend,
|
|
135
|
+
tableName: 'TestTableNot',
|
|
136
|
+
key: 'id',
|
|
137
|
+
indexes: ['value'],
|
|
138
|
+
})
|
|
139
|
+
|
|
140
|
+
await tableNot.set({ id: '1', value: 'apple' })
|
|
141
|
+
await tableNot.set({ id: '2', value: 'banana' })
|
|
142
|
+
await tableNot.set({ id: '3', value: 'cherry' })
|
|
143
|
+
|
|
144
|
+
const results: { id: string; value: string }[] = []
|
|
145
|
+
for await (const doc of tableNot.search({
|
|
146
|
+
where: { NOT: { value: 'banana' } },
|
|
147
|
+
})) {
|
|
148
|
+
results.push(doc)
|
|
149
|
+
}
|
|
150
|
+
expect(results.length).toBe(2)
|
|
151
|
+
expect(results.map((doc) => doc.value)).toEqual(['apple', 'cherry'])
|
|
152
|
+
})
|
|
153
|
+
|
|
154
|
+
it('should handle AND conditions', async () => {
|
|
155
|
+
const tableAnd = await createTable<{ id: string; category: string; price: number }>({
|
|
156
|
+
backend,
|
|
157
|
+
tableName: 'TestTableAnd',
|
|
158
|
+
key: 'id',
|
|
159
|
+
indexes: ['category', 'price'],
|
|
160
|
+
})
|
|
161
|
+
|
|
162
|
+
await tableAnd.set({ id: '1', category: 'fruit', price: 10 })
|
|
163
|
+
await tableAnd.set({ id: '2', category: 'fruit', price: 20 })
|
|
164
|
+
await tableAnd.set({ id: '3', category: 'vegetable', price: 15 })
|
|
165
|
+
|
|
166
|
+
const results: { id: string; category: string; price: number }[] = []
|
|
167
|
+
for await (const doc of tableAnd.search({
|
|
168
|
+
where: { AND: [{ category: 'fruit' }, { price: { lt: 15 } }] },
|
|
169
|
+
})) {
|
|
170
|
+
results.push(doc)
|
|
171
|
+
}
|
|
172
|
+
expect(results.length).toBe(1)
|
|
173
|
+
expect(results[0].id).toBe('1')
|
|
174
|
+
})
|
|
175
|
+
|
|
176
|
+
it('should handle OR conditions', async () => {
|
|
177
|
+
const tableOr = await createTable<{ id: string; type: string }>({
|
|
178
|
+
backend,
|
|
179
|
+
tableName: 'TestTableOr',
|
|
180
|
+
key: 'id',
|
|
181
|
+
indexes: ['type'],
|
|
182
|
+
})
|
|
183
|
+
|
|
184
|
+
await tableOr.set({ id: '1', type: 'A' })
|
|
185
|
+
await tableOr.set({ id: '2', type: 'B' })
|
|
186
|
+
await tableOr.set({ id: '3', type: 'C' })
|
|
187
|
+
|
|
188
|
+
const results: { id: string; type: string }[] = []
|
|
189
|
+
for await (const doc of tableOr.search({
|
|
190
|
+
where: { OR: [{ type: 'A' }, { type: 'C' }] },
|
|
191
|
+
})) {
|
|
192
|
+
results.push(doc)
|
|
193
|
+
}
|
|
194
|
+
expect(results.length).toBe(2)
|
|
195
|
+
expect(results.map((doc) => doc.type)).toEqual(['A', 'C'])
|
|
196
|
+
})
|
|
197
|
+
|
|
198
|
+
it('should handle nested AND/OR/NOT conditions', async () => {
|
|
199
|
+
const tableNestedLogic = await createTable<{ id: string; category: string; price: number }>({
|
|
200
|
+
backend,
|
|
201
|
+
tableName: 'TestTableNestedLogic',
|
|
202
|
+
key: 'id',
|
|
203
|
+
indexes: ['category', 'price'],
|
|
204
|
+
})
|
|
205
|
+
|
|
206
|
+
await tableNestedLogic.set({ id: '1', category: 'fruit', price: 10 })
|
|
207
|
+
await tableNestedLogic.set({ id: '2', category: 'fruit', price: 20 })
|
|
208
|
+
await tableNestedLogic.set({ id: '3', category: 'fruit', price: 15 })
|
|
209
|
+
|
|
210
|
+
const whereClause: Where<{ category: string; price: number }> = {
|
|
211
|
+
AND: [
|
|
212
|
+
{ category: { is: 'fruit' } },
|
|
213
|
+
{
|
|
214
|
+
OR: [{ price: { lt: 15 } }, { price: { is: 15 } }],
|
|
215
|
+
},
|
|
216
|
+
],
|
|
217
|
+
}
|
|
218
|
+
|
|
219
|
+
const allData: { id: string; category: string; price: number }[] = []
|
|
220
|
+
for await (const doc of tableNestedLogic.search({})) {
|
|
221
|
+
allData.push(doc)
|
|
222
|
+
}
|
|
223
|
+
expect(allData.length).toBe(3)
|
|
224
|
+
|
|
225
|
+
const results: { id: string; category: string; price: number }[] = []
|
|
226
|
+
for await (const doc of tableNestedLogic.search({
|
|
227
|
+
where: whereClause,
|
|
228
|
+
})) {
|
|
229
|
+
results.push(doc)
|
|
230
|
+
}
|
|
231
|
+
|
|
232
|
+
expect(results.length).toBe(2)
|
|
233
|
+
})
|
|
234
|
+
})
|
|
@@ -20,14 +20,16 @@ export type DotPrefix<T extends string> = T extends '' ? '' : `.${T}`
|
|
|
20
20
|
|
|
21
21
|
type Previous = [never, 0, 1, 2, 3, 4, 5]
|
|
22
22
|
|
|
23
|
-
|
|
23
|
+
type DotPathRaw<T, D extends number = 5> = [D] extends [never]
|
|
24
24
|
? never
|
|
25
25
|
: T extends object
|
|
26
26
|
? {
|
|
27
|
-
[K in Extract<keyof T, string>]: T[K] extends object ? K | `${K}.${
|
|
27
|
+
[K in Extract<keyof T, string>]: T[K] extends object ? K | `${K}.${DotPathRaw<T[K], Previous[D]>}` : K
|
|
28
28
|
}[Extract<keyof T, string>]
|
|
29
29
|
: never
|
|
30
30
|
|
|
31
|
+
export type DotPath<T> = DotPathRaw<MakeAllFieldAsRequired<T>>
|
|
32
|
+
|
|
31
33
|
// Built-in FTS5 tokenizers
|
|
32
34
|
export type FtsTokenizer =
|
|
33
35
|
| 'porter' // English stemming
|
|
@@ -81,3 +83,7 @@ export interface Table<Document extends DocType> extends DbNotGeneric {
|
|
|
81
83
|
readonly deleteBy: (where: Where<Document>) => Promise<MutationResult[]>
|
|
82
84
|
readonly clear: () => Promise<void>
|
|
83
85
|
}
|
|
86
|
+
|
|
87
|
+
export type MakeAllFieldAsRequired<T> = {
|
|
88
|
+
[K in keyof T]-?: T[K] extends object ? MakeAllFieldAsRequired<T[K]> : T[K]
|
|
89
|
+
}
|
|
@@ -4,6 +4,8 @@
|
|
|
4
4
|
/* eslint-disable sonarjs/no-nested-conditional */
|
|
5
5
|
/* eslint-disable sonarjs/cognitive-complexity */
|
|
6
6
|
|
|
7
|
+
import type { MakeAllFieldAsRequired } from './table.types'
|
|
8
|
+
|
|
7
9
|
// -------------------------------------------------------------
|
|
8
10
|
// Condition operators for each field
|
|
9
11
|
// -------------------------------------------------------------
|
|
@@ -20,21 +22,22 @@ interface Condition<T> {
|
|
|
20
22
|
readonly fts?: string | string[] // 🔥 NEW
|
|
21
23
|
}
|
|
22
24
|
|
|
23
|
-
|
|
24
|
-
// Where type: recursive object with operators or nested fields
|
|
25
|
-
// -------------------------------------------------------------
|
|
26
|
-
export type Where<T extends Record<string, unknown>> =
|
|
25
|
+
type WhereRaw<T extends Record<string, unknown>> =
|
|
27
26
|
| {
|
|
28
27
|
[K in keyof T]?: T[K] extends Record<string, unknown>
|
|
29
|
-
?
|
|
28
|
+
? WhereRaw<T[K]> // nested object
|
|
30
29
|
: Condition<T[K]> | T[K] | T[K][]
|
|
31
30
|
}
|
|
32
31
|
| {
|
|
33
|
-
readonly AND?: Array<
|
|
34
|
-
readonly OR?: Array<
|
|
35
|
-
readonly NOT?:
|
|
32
|
+
readonly AND?: Array<WhereRaw<T>>
|
|
33
|
+
readonly OR?: Array<WhereRaw<T>>
|
|
34
|
+
readonly NOT?: WhereRaw<T>
|
|
36
35
|
}
|
|
37
36
|
|
|
37
|
+
// -------------------------------------------------------------
|
|
38
|
+
// Where type: recursive object with operators or nested fields
|
|
39
|
+
// -------------------------------------------------------------
|
|
40
|
+
export type Where<T extends Record<string, unknown>> = WhereRaw<MakeAllFieldAsRequired<T>>
|
|
38
41
|
/**
|
|
39
42
|
* Inline a value for SQL query, with proper escaping for strings
|
|
40
43
|
* @param value The value to inline
|
|
@@ -15,9 +15,10 @@ export interface SqlSeachOptions<Document extends DocType> {
|
|
|
15
15
|
}
|
|
16
16
|
export type DotPrefix<T extends string> = T extends '' ? '' : `.${T}`;
|
|
17
17
|
type Previous = [never, 0, 1, 2, 3, 4, 5];
|
|
18
|
-
|
|
19
|
-
[K in Extract<keyof T, string>]: T[K] extends object ? K | `${K}.${
|
|
18
|
+
type DotPathRaw<T, D extends number = 5> = [D] extends [never] ? never : T extends object ? {
|
|
19
|
+
[K in Extract<keyof T, string>]: T[K] extends object ? K | `${K}.${DotPathRaw<T[K], Previous[D]>}` : K;
|
|
20
20
|
}[Extract<keyof T, string>] : never;
|
|
21
|
+
export type DotPath<T> = DotPathRaw<MakeAllFieldAsRequired<T>>;
|
|
21
22
|
export type FtsTokenizer = 'porter' | 'simple' | 'icu' | 'unicode61' | FtsTokenizerOptions;
|
|
22
23
|
export interface FtsType<Document extends DocType> {
|
|
23
24
|
readonly type: 'fts';
|
|
@@ -59,4 +60,7 @@ export interface Table<Document extends DocType> extends DbNotGeneric {
|
|
|
59
60
|
readonly deleteBy: (where: Where<Document>) => Promise<MutationResult[]>;
|
|
60
61
|
readonly clear: () => Promise<void>;
|
|
61
62
|
}
|
|
63
|
+
export type MakeAllFieldAsRequired<T> = {
|
|
64
|
+
[K in keyof T]-?: T[K] extends object ? MakeAllFieldAsRequired<T[K]> : T[K];
|
|
65
|
+
};
|
|
62
66
|
export {};
|
|
@@ -1,3 +1,4 @@
|
|
|
1
|
+
import type { MakeAllFieldAsRequired } from './table.types';
|
|
1
2
|
interface Condition<T> {
|
|
2
3
|
readonly is?: T | T[];
|
|
3
4
|
readonly isNot?: T | T[];
|
|
@@ -10,13 +11,14 @@ interface Condition<T> {
|
|
|
10
11
|
readonly like?: T | T[];
|
|
11
12
|
readonly fts?: string | string[];
|
|
12
13
|
}
|
|
13
|
-
|
|
14
|
-
[K in keyof T]?: T[K] extends Record<string, unknown> ?
|
|
14
|
+
type WhereRaw<T extends Record<string, unknown>> = {
|
|
15
|
+
[K in keyof T]?: T[K] extends Record<string, unknown> ? WhereRaw<T[K]> : Condition<T[K]> | T[K] | T[K][];
|
|
15
16
|
} | {
|
|
16
|
-
readonly AND?: Array<
|
|
17
|
-
readonly OR?: Array<
|
|
18
|
-
readonly NOT?:
|
|
17
|
+
readonly AND?: Array<WhereRaw<T>>;
|
|
18
|
+
readonly OR?: Array<WhereRaw<T>>;
|
|
19
|
+
readonly NOT?: WhereRaw<T>;
|
|
19
20
|
};
|
|
21
|
+
export type Where<T extends Record<string, unknown>> = WhereRaw<MakeAllFieldAsRequired<T>>;
|
|
20
22
|
/**
|
|
21
23
|
* Write SQL WHERE clause from a Where object
|
|
22
24
|
* @param where The Where object defining the conditions
|