muya 2.5.1 → 2.5.3
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__/create-sqlite.test.js +1 -1
- package/esm/sqlite/create-sqlite.js +1 -1
- package/esm/sqlite/table/table.js +11 -6
- package/esm/sqlite/use-sqlite.js +1 -1
- package/package.json +1 -1
- package/src/sqlite/__tests__/create-sqlite.test.ts +183 -0
- package/src/sqlite/__tests__/use-sqlite.more.test.tsx +6 -6
- package/src/sqlite/__tests__/use-sqlite.test.tsx +510 -25
- package/src/sqlite/create-sqlite.ts +20 -1
- package/src/sqlite/table/table.ts +29 -1
- package/src/sqlite/table/table.types.ts +25 -0
- package/src/sqlite/use-sqlite.ts +40 -5
- package/types/sqlite/create-sqlite.d.ts +2 -1
- package/types/sqlite/table/table.types.d.ts +13 -0
- package/types/sqlite/use-sqlite.d.ts +8 -1
|
@@ -2,7 +2,18 @@ import { STATE_SCHEDULER } from '../create'
|
|
|
2
2
|
import { getId } from '../utils/id'
|
|
3
3
|
import type { Backend } from './table'
|
|
4
4
|
import { createTable } from './table/table'
|
|
5
|
-
import type {
|
|
5
|
+
import type {
|
|
6
|
+
DbOptions,
|
|
7
|
+
DocType,
|
|
8
|
+
DotPath,
|
|
9
|
+
GetFieldType,
|
|
10
|
+
GroupByOptions,
|
|
11
|
+
GroupByResult,
|
|
12
|
+
Key,
|
|
13
|
+
MutationResult,
|
|
14
|
+
SearchOptions,
|
|
15
|
+
Table,
|
|
16
|
+
} from './table/table.types'
|
|
6
17
|
import type { Where } from './table/where'
|
|
7
18
|
|
|
8
19
|
export interface CreateSqliteOptions<Document extends DocType> extends Omit<DbOptions<Document>, 'backend'> {
|
|
@@ -26,6 +37,10 @@ export interface SyncTable<Document extends DocType> {
|
|
|
26
37
|
readonly count: (options?: { where?: Where<Document> }) => Promise<number>
|
|
27
38
|
readonly deleteBy: (where: Where<Document>) => Promise<MutationResult<Document>[]>
|
|
28
39
|
readonly clear: () => Promise<void>
|
|
40
|
+
readonly groupBy: <Field extends DotPath<Document>>(
|
|
41
|
+
field: Field,
|
|
42
|
+
options?: GroupByOptions<Document>,
|
|
43
|
+
) => Promise<Array<GroupByResult<GetFieldType<Document, Field>>>>
|
|
29
44
|
}
|
|
30
45
|
|
|
31
46
|
/**
|
|
@@ -139,6 +154,10 @@ export function createSqliteState<Document extends DocType>(options: CreateSqlit
|
|
|
139
154
|
const table = await getTable()
|
|
140
155
|
return await table.count(countOptions)
|
|
141
156
|
},
|
|
157
|
+
async groupBy(field, groupByOptions) {
|
|
158
|
+
const table = await getTable()
|
|
159
|
+
return await table.groupBy(field, groupByOptions)
|
|
160
|
+
},
|
|
142
161
|
}
|
|
143
162
|
|
|
144
163
|
return state
|
|
@@ -4,7 +4,18 @@
|
|
|
4
4
|
/* eslint-disable @typescript-eslint/no-shadow */
|
|
5
5
|
/* eslint-disable no-shadow */
|
|
6
6
|
import type { Backend } from './backend'
|
|
7
|
-
import type {
|
|
7
|
+
import type {
|
|
8
|
+
Table,
|
|
9
|
+
DbOptions,
|
|
10
|
+
DocType,
|
|
11
|
+
Key,
|
|
12
|
+
SearchOptions,
|
|
13
|
+
MutationResult,
|
|
14
|
+
GroupByResult,
|
|
15
|
+
GroupByOptions,
|
|
16
|
+
DotPath,
|
|
17
|
+
GetFieldType,
|
|
18
|
+
} from './table.types'
|
|
8
19
|
import { unicodeTokenizer, type FtsTokenizerOptions } from './tokenizer'
|
|
9
20
|
import type { Where } from './where'
|
|
10
21
|
import { getWhereQuery } from './where'
|
|
@@ -299,6 +310,23 @@ export async function createTable<Document extends DocType>(options: DbOptions<D
|
|
|
299
310
|
await backend.execute(`DELETE FROM ${tableName}`)
|
|
300
311
|
},
|
|
301
312
|
|
|
313
|
+
async groupBy<Field extends DotPath<Document>>(
|
|
314
|
+
field: Field,
|
|
315
|
+
options: GroupByOptions<Document> = {},
|
|
316
|
+
): Promise<Array<GroupByResult<GetFieldType<Document, Field>>>> {
|
|
317
|
+
const whereSql = getWhereQuery<Document>(options.where, tableName)
|
|
318
|
+
const jsonPath = toJsonPath(String(field))
|
|
319
|
+
const query = `
|
|
320
|
+
SELECT json_extract(data, '${jsonPath}') AS groupKey, COUNT(*) AS count
|
|
321
|
+
FROM ${tableName}
|
|
322
|
+
${whereSql}
|
|
323
|
+
GROUP BY groupKey
|
|
324
|
+
`
|
|
325
|
+
type FieldType = GetFieldType<Document, Field>
|
|
326
|
+
const results = await backend.select<Array<{ groupKey: FieldType; count: number }>>(query)
|
|
327
|
+
return results.map((row) => ({ key: row.groupKey, count: row.count }))
|
|
328
|
+
},
|
|
329
|
+
|
|
302
330
|
async batchSet(documents: Document[]) {
|
|
303
331
|
const mutations: MutationResult<Document>[] = []
|
|
304
332
|
await backend.transaction(async (tx) => {
|
|
@@ -30,6 +30,18 @@ type DotPathRaw<T, D extends number = 5> = [D] extends [never]
|
|
|
30
30
|
|
|
31
31
|
export type DotPath<T> = DotPathRaw<MakeAllFieldAsRequired<T>>
|
|
32
32
|
|
|
33
|
+
/**
|
|
34
|
+
* Extract the value type at a given dot path
|
|
35
|
+
* e.g., GetFieldType<{ user: { name: string } }, 'user.name'> = string
|
|
36
|
+
*/
|
|
37
|
+
export type GetFieldType<T, Path extends string> = Path extends `${infer First}.${infer Rest}`
|
|
38
|
+
? First extends keyof T
|
|
39
|
+
? GetFieldType<T[First], Rest>
|
|
40
|
+
: never
|
|
41
|
+
: Path extends keyof T
|
|
42
|
+
? T[Path]
|
|
43
|
+
: never
|
|
44
|
+
|
|
33
45
|
// Built-in FTS5 tokenizers
|
|
34
46
|
export type FtsTokenizer =
|
|
35
47
|
| 'porter' // English stemming
|
|
@@ -86,6 +98,15 @@ interface MutationResultUpdateInsert<T> extends MutationResultBase<T> {
|
|
|
86
98
|
|
|
87
99
|
export type MutationResult<T> = MutationResultDelete<T> | MutationResultUpdateInsert<T>
|
|
88
100
|
|
|
101
|
+
export interface GroupByResult<K> {
|
|
102
|
+
readonly key: K
|
|
103
|
+
readonly count: number
|
|
104
|
+
}
|
|
105
|
+
|
|
106
|
+
export interface GroupByOptions<Document extends DocType> {
|
|
107
|
+
readonly where?: Where<Document>
|
|
108
|
+
}
|
|
109
|
+
|
|
89
110
|
export interface Table<Document extends DocType> extends DbNotGeneric {
|
|
90
111
|
readonly set: (document: Document, backendOverride?: Backend) => Promise<MutationResult<Document>>
|
|
91
112
|
readonly batchSet: (documents: Document[]) => Promise<MutationResult<Document>[]>
|
|
@@ -97,6 +118,10 @@ export interface Table<Document extends DocType> extends DbNotGeneric {
|
|
|
97
118
|
readonly count: (options?: { where?: Where<Document> }) => Promise<number>
|
|
98
119
|
readonly deleteBy: (where: Where<Document>) => Promise<MutationResult<Document>[]>
|
|
99
120
|
readonly clear: () => Promise<void>
|
|
121
|
+
readonly groupBy: <Field extends DotPath<Document>>(
|
|
122
|
+
field: Field,
|
|
123
|
+
options?: GroupByOptions<Document>,
|
|
124
|
+
) => Promise<Array<GroupByResult<GetFieldType<Document, Field>>>>
|
|
100
125
|
}
|
|
101
126
|
|
|
102
127
|
export type MakeAllFieldAsRequired<T> = {
|
package/src/sqlite/use-sqlite.ts
CHANGED
|
@@ -1,11 +1,25 @@
|
|
|
1
1
|
/* eslint-disable sonarjs/cognitive-complexity */
|
|
2
|
-
import { useCallback, useLayoutEffect, useReducer, useRef, type DependencyList } from 'react'
|
|
2
|
+
import { useCallback, useLayoutEffect, useReducer, useRef, useState, type DependencyList } from 'react'
|
|
3
3
|
import type { SyncTable } from './create-sqlite'
|
|
4
4
|
import type { DocType, Key, SqlSeachOptions } from './table/table.types'
|
|
5
5
|
import { DEFAULT_PAGE_SIZE } from './table'
|
|
6
6
|
import { shallow } from '../utils/shallow'
|
|
7
7
|
const MAX_ITERATIONS = 10_000
|
|
8
8
|
|
|
9
|
+
/**
|
|
10
|
+
* Shallow compare two dependency arrays
|
|
11
|
+
* @param previousDeps Previous deps array
|
|
12
|
+
* @param nextDeps Next deps array
|
|
13
|
+
* @returns True if arrays have same length and all items are strictly equal
|
|
14
|
+
*/
|
|
15
|
+
function shallowEqualDeps(previousDeps: DependencyList, nextDeps: DependencyList): boolean {
|
|
16
|
+
if (previousDeps.length !== nextDeps.length) return false
|
|
17
|
+
for (const [index, previousDep] of previousDeps.entries()) {
|
|
18
|
+
if (!Object.is(previousDep, nextDeps[index])) return false
|
|
19
|
+
}
|
|
20
|
+
return true
|
|
21
|
+
}
|
|
22
|
+
|
|
9
23
|
export interface SqLiteActions {
|
|
10
24
|
/**
|
|
11
25
|
* Load the next page of results and return if isDone to show more results.
|
|
@@ -17,8 +31,15 @@ export interface SqLiteActions {
|
|
|
17
31
|
* @returns void
|
|
18
32
|
*/
|
|
19
33
|
readonly reset: () => Promise<void>
|
|
34
|
+
/**
|
|
35
|
+
* Map of document keys to their index in the results array.
|
|
36
|
+
*/
|
|
20
37
|
readonly keysIndex: Map<Key, number>
|
|
21
|
-
|
|
38
|
+
/**
|
|
39
|
+
* True when deps changed but fresh data hasn't loaded yet.
|
|
40
|
+
* Use this to show stale/dimmed UI while new results are loading.
|
|
41
|
+
*/
|
|
42
|
+
readonly isStale: boolean
|
|
22
43
|
}
|
|
23
44
|
|
|
24
45
|
export interface UseSearchOptions<Document extends DocType, Selected = Document> extends SqlSeachOptions<Document> {
|
|
@@ -48,6 +69,11 @@ export function useSqliteValue<Document extends DocType, Selected = Document>(
|
|
|
48
69
|
const keysIndex = useRef(new Map<Key, number>())
|
|
49
70
|
const iteratorRef = useRef<AsyncIterableIterator<{ doc: Document; meta: { key: Key } }> | null>(null)
|
|
50
71
|
|
|
72
|
+
// Track "settled" deps - the deps value when data last finished loading
|
|
73
|
+
// isStale is derived: true when current deps differ from settled deps
|
|
74
|
+
const [settledDeps, setSettledDeps] = useState<DependencyList | null>(null)
|
|
75
|
+
const isStale = settledDeps === null || !shallowEqualDeps(settledDeps, deps)
|
|
76
|
+
|
|
51
77
|
const updateIterator = useCallback(() => {
|
|
52
78
|
// eslint-disable-next-line sonarjs/no-unused-vars
|
|
53
79
|
const { select: _ignore, ...resetOptions } = options
|
|
@@ -198,17 +224,26 @@ export function useSqliteValue<Document extends DocType, Selected = Document>(
|
|
|
198
224
|
}, [state])
|
|
199
225
|
|
|
200
226
|
useLayoutEffect(() => {
|
|
227
|
+
// Capture current deps for this effect invocation
|
|
228
|
+
const currentDeps = deps
|
|
201
229
|
resetDataAndUpdateIterator()
|
|
202
|
-
nextPage()
|
|
230
|
+
nextPage().then(() => {
|
|
231
|
+
// Mark these deps as settled when data finishes loading
|
|
232
|
+
setSettledDeps(currentDeps)
|
|
233
|
+
})
|
|
203
234
|
// eslint-disable-next-line react-hooks/exhaustive-deps
|
|
204
235
|
}, deps)
|
|
205
236
|
|
|
206
237
|
const resetCb = useCallback(async () => {
|
|
238
|
+
// Set settledDeps to null to make isStale=true during reset
|
|
239
|
+
setSettledDeps(null)
|
|
207
240
|
resetDataAndUpdateIterator()
|
|
208
241
|
await nextPage()
|
|
209
|
-
|
|
242
|
+
// After data loads, mark current deps as settled
|
|
243
|
+
setSettledDeps(deps)
|
|
244
|
+
}, [nextPage, resetDataAndUpdateIterator, deps])
|
|
210
245
|
|
|
211
|
-
return [itemsRef.current, { nextPage, reset: resetCb, keysIndex: keysIndex.current }] as [
|
|
246
|
+
return [itemsRef.current, { nextPage, reset: resetCb, keysIndex: keysIndex.current, isStale }] as [
|
|
212
247
|
(undefined extends Selected ? Document[] : Selected[]) | null,
|
|
213
248
|
SqLiteActions,
|
|
214
249
|
]
|
|
@@ -1,5 +1,5 @@
|
|
|
1
1
|
import type { Backend } from './table';
|
|
2
|
-
import type { DbOptions, DocType, Key, MutationResult, SearchOptions } from './table/table.types';
|
|
2
|
+
import type { DbOptions, DocType, DotPath, GetFieldType, GroupByOptions, GroupByResult, Key, MutationResult, SearchOptions } from './table/table.types';
|
|
3
3
|
import type { Where } from './table/where';
|
|
4
4
|
export interface CreateSqliteOptions<Document extends DocType> extends Omit<DbOptions<Document>, 'backend'> {
|
|
5
5
|
readonly backend: Backend | Promise<Backend>;
|
|
@@ -21,6 +21,7 @@ export interface SyncTable<Document extends DocType> {
|
|
|
21
21
|
}) => Promise<number>;
|
|
22
22
|
readonly deleteBy: (where: Where<Document>) => Promise<MutationResult<Document>[]>;
|
|
23
23
|
readonly clear: () => Promise<void>;
|
|
24
|
+
readonly groupBy: <Field extends DotPath<Document>>(field: Field, options?: GroupByOptions<Document>) => Promise<Array<GroupByResult<GetFieldType<Document, Field>>>>;
|
|
24
25
|
}
|
|
25
26
|
/**
|
|
26
27
|
* Create a SyncTable that wraps a Table and provides reactive capabilities
|
|
@@ -19,6 +19,11 @@ type DotPathRaw<T, D extends number = 5> = [D] extends [never] ? never : T exten
|
|
|
19
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
21
|
export type DotPath<T> = DotPathRaw<MakeAllFieldAsRequired<T>>;
|
|
22
|
+
/**
|
|
23
|
+
* Extract the value type at a given dot path
|
|
24
|
+
* e.g., GetFieldType<{ user: { name: string } }, 'user.name'> = string
|
|
25
|
+
*/
|
|
26
|
+
export type GetFieldType<T, Path extends string> = Path extends `${infer First}.${infer Rest}` ? First extends keyof T ? GetFieldType<T[First], Rest> : never : Path extends keyof T ? T[Path] : never;
|
|
22
27
|
export type FtsTokenizer = 'porter' | 'simple' | 'icu' | 'unicode61' | FtsTokenizerOptions;
|
|
23
28
|
export interface FtsType<Document extends DocType> {
|
|
24
29
|
readonly type: 'fts';
|
|
@@ -59,6 +64,13 @@ interface MutationResultUpdateInsert<T> extends MutationResultBase<T> {
|
|
|
59
64
|
document: T;
|
|
60
65
|
}
|
|
61
66
|
export type MutationResult<T> = MutationResultDelete<T> | MutationResultUpdateInsert<T>;
|
|
67
|
+
export interface GroupByResult<K> {
|
|
68
|
+
readonly key: K;
|
|
69
|
+
readonly count: number;
|
|
70
|
+
}
|
|
71
|
+
export interface GroupByOptions<Document extends DocType> {
|
|
72
|
+
readonly where?: Where<Document>;
|
|
73
|
+
}
|
|
62
74
|
export interface Table<Document extends DocType> extends DbNotGeneric {
|
|
63
75
|
readonly set: (document: Document, backendOverride?: Backend) => Promise<MutationResult<Document>>;
|
|
64
76
|
readonly batchSet: (documents: Document[]) => Promise<MutationResult<Document>[]>;
|
|
@@ -71,6 +83,7 @@ export interface Table<Document extends DocType> extends DbNotGeneric {
|
|
|
71
83
|
}) => Promise<number>;
|
|
72
84
|
readonly deleteBy: (where: Where<Document>) => Promise<MutationResult<Document>[]>;
|
|
73
85
|
readonly clear: () => Promise<void>;
|
|
86
|
+
readonly groupBy: <Field extends DotPath<Document>>(field: Field, options?: GroupByOptions<Document>) => Promise<Array<GroupByResult<GetFieldType<Document, Field>>>>;
|
|
74
87
|
}
|
|
75
88
|
export type MakeAllFieldAsRequired<T> = {
|
|
76
89
|
[K in keyof T]-?: T[K] extends object ? MakeAllFieldAsRequired<T[K]> : T[K];
|
|
@@ -12,8 +12,15 @@ export interface SqLiteActions {
|
|
|
12
12
|
* @returns void
|
|
13
13
|
*/
|
|
14
14
|
readonly reset: () => Promise<void>;
|
|
15
|
+
/**
|
|
16
|
+
* Map of document keys to their index in the results array.
|
|
17
|
+
*/
|
|
15
18
|
readonly keysIndex: Map<Key, number>;
|
|
16
|
-
|
|
19
|
+
/**
|
|
20
|
+
* True when deps changed but fresh data hasn't loaded yet.
|
|
21
|
+
* Use this to show stale/dimmed UI while new results are loading.
|
|
22
|
+
*/
|
|
23
|
+
readonly isStale: boolean;
|
|
17
24
|
}
|
|
18
25
|
export interface UseSearchOptions<Document extends DocType, Selected = Document> extends SqlSeachOptions<Document> {
|
|
19
26
|
/**
|