@iss-ai/easy-web-store 0.0.1 → 0.0.2
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/README.md +214 -182
- package/lib/index.cjs.js +1 -1
- package/lib/index.esm.js +1 -1
- package/lib/index.min.js +1 -1
- package/lib/index.umd.js +1 -1
- package/lib/src/easy/EasyCookiesStore.d.ts +124 -0
- package/lib/src/easy/{DexieStore.d.ts → EasyDexieStore.d.ts} +25 -26
- package/lib/src/easy/EasyIndexedDb.d.ts +2 -0
- package/lib/src/easy/EasyLocalStorage.d.ts +105 -0
- package/lib/src/easy/EasyMemoryStore.d.ts +91 -0
- package/lib/src/easy/EasySessionStorage.d.ts +105 -0
- package/lib/src/index.d.ts +6 -1
- package/lib/src/interface/types.d.ts +18 -0
- package/lib/src/utils/Utils.d.ts +5 -0
- package/package.json +5 -1
package/README.md
CHANGED
|
@@ -1,15 +1,17 @@
|
|
|
1
1
|
# @iss-ai/easy-web-store
|
|
2
2
|
|
|
3
|
-
一个简单高效的 Web
|
|
3
|
+
一个简单高效的 Web 应用数据存储库,提供多种存储方式(IndexedDB、localStorage、sessionStorage、Cookies、内存),支持完整的 CRUD 操作和丰富的查询条件。
|
|
4
4
|
|
|
5
5
|
## ✨ 特性
|
|
6
6
|
|
|
7
7
|
- 🚀 **简单易用**:直观的 API 设计,快速上手
|
|
8
|
-
- 💾
|
|
8
|
+
- 💾 **多种存储方式**:支持 IndexedDB、localStorage、sessionStorage、Cookies、内存存储
|
|
9
9
|
- 🔧 **TypeScript 支持**:完整的类型定义
|
|
10
|
-
- 📦
|
|
10
|
+
- 📦 **轻量级**:零依赖(IndexedDB 版本仅依赖 Dexie.js)
|
|
11
11
|
- 🌐 **跨浏览器**:支持现代浏览器(Last 2 versions, > 5%, not IE <= 9)
|
|
12
12
|
- 📄 **完整 CRUD**:提供保存、查询、更新、删除等完整数据操作
|
|
13
|
+
- 🔍 **丰富查询**:支持多种查询操作符($eq, $neq, $gt, $lt, $in, $like, $regexp 等)
|
|
14
|
+
- 📃 **分页支持**:内置分页功能
|
|
13
15
|
|
|
14
16
|
## 📦 安装
|
|
15
17
|
|
|
@@ -32,280 +34,310 @@ pnpm add @iss-ai/easy-web-store
|
|
|
32
34
|
import { EasyDexieStore } from '@iss-ai/easy-web-store';
|
|
33
35
|
|
|
34
36
|
// 定义数据类型
|
|
35
|
-
interface
|
|
37
|
+
interface User {
|
|
36
38
|
id?: string | number;
|
|
37
39
|
name: string;
|
|
38
|
-
|
|
39
|
-
|
|
40
|
+
email: string;
|
|
41
|
+
age: number;
|
|
40
42
|
createTime?: Date;
|
|
41
43
|
updateTime?: Date;
|
|
42
44
|
}
|
|
43
45
|
|
|
44
|
-
//
|
|
45
|
-
const
|
|
46
|
+
// 创建数据存储实例(IndexedDB)
|
|
47
|
+
const userStore = new EasyDexieStore<User>('users', 'my-app-db');
|
|
46
48
|
|
|
47
49
|
// 保存数据
|
|
48
|
-
await
|
|
49
|
-
name: '
|
|
50
|
-
|
|
51
|
-
|
|
50
|
+
const user = await userStore.save({
|
|
51
|
+
name: 'John',
|
|
52
|
+
email: 'john@example.com',
|
|
53
|
+
age: 25
|
|
52
54
|
});
|
|
53
55
|
|
|
54
56
|
// 查询数据
|
|
55
|
-
const
|
|
56
|
-
where: {
|
|
57
|
-
sort: {
|
|
57
|
+
const users = await userStore.getList({
|
|
58
|
+
where: { age: { $gt: 18 } },
|
|
59
|
+
sort: { createTime: -1 }
|
|
58
60
|
});
|
|
59
61
|
|
|
60
|
-
//
|
|
61
|
-
await
|
|
62
|
+
// 更新数据(更新所有符合条件的记录)
|
|
63
|
+
await userStore.update({ age: 26 }, { name: 'John' });
|
|
62
64
|
|
|
63
65
|
// 删除数据
|
|
64
|
-
await
|
|
66
|
+
await userStore.delete({ name: 'John' });
|
|
65
67
|
```
|
|
66
68
|
|
|
67
|
-
|
|
68
|
-
|
|
69
|
-
### EasyDexieStore 构造函数
|
|
69
|
+
### 使用不同的存储方式
|
|
70
70
|
|
|
71
71
|
```typescript
|
|
72
|
-
|
|
73
|
-
|
|
72
|
+
import {
|
|
73
|
+
EasyDexieStore, // IndexedDB(推荐,容量大,支持复杂查询)
|
|
74
|
+
EasyMemoryStore, // 内存存储(应用重启后数据丢失)
|
|
75
|
+
EasyLocalStorage, // localStorage(持久化,约 5MB 容量)
|
|
76
|
+
EasySessionStorage, // sessionStorage(页面关闭后数据丢失)
|
|
77
|
+
EasyCookiesStore // Cookies(小数据量,支持过期时间)
|
|
78
|
+
} from '@iss-ai/easy-web-store';
|
|
74
79
|
|
|
75
|
-
|
|
76
|
-
|
|
77
|
-
| tableName | string | 'store' | 数据表名称 |
|
|
78
|
-
| dbName | string | 'easy-store' | 数据库名称 |
|
|
79
|
-
| tableSchema | string | undefined | 自定义表结构 |
|
|
80
|
+
// 内存存储 - 适合临时数据
|
|
81
|
+
const memoryStore = new EasyMemoryStore<User>('temp-users');
|
|
80
82
|
|
|
81
|
-
|
|
83
|
+
// localStorage - 适合持久化数据
|
|
84
|
+
const localStore = new EasyLocalStorage<User>('users');
|
|
82
85
|
|
|
83
|
-
|
|
86
|
+
// sessionStorage - 适合会话数据
|
|
87
|
+
const sessionStore = new EasySessionStorage<User>('session-users');
|
|
84
88
|
|
|
85
|
-
|
|
86
|
-
|
|
89
|
+
// Cookies - 适合小数据量、需要与服务端交互的场景
|
|
90
|
+
const cookiesStore = new EasyCookiesStore<User>('cookie-users');
|
|
87
91
|
```
|
|
88
92
|
|
|
89
|
-
|
|
90
|
-
- **更新数据**:指定已有数据的 `id` 字段
|
|
91
|
-
- 自动处理 `createTime` 和 `updateTime`
|
|
93
|
+
## 📖 API 文档
|
|
92
94
|
|
|
93
|
-
|
|
95
|
+
### 通用接口
|
|
94
96
|
|
|
95
|
-
|
|
96
|
-
async saveList(dataList: Array<Omit<T, 'id'> | T>): Promise<T[]>
|
|
97
|
-
```
|
|
97
|
+
所有存储类都实现 `IEasyStore<T>` 接口,提供相同的方法:
|
|
98
98
|
|
|
99
|
-
|
|
99
|
+
| 方法 | 描述 | 返回值 |
|
|
100
|
+
|------|------|--------|
|
|
101
|
+
| `save(data)` | 保存或更新单条数据 | `Promise<T>` |
|
|
102
|
+
| `saveList(dataList)` | 批量保存数据 | `Promise<T[]>` |
|
|
103
|
+
| `getList(condition?)` | 获取数据列表 | `Promise<T[]>` |
|
|
104
|
+
| `getInfo(condition)` | 获取单条数据 | `Promise<T \| null>` |
|
|
105
|
+
| `update(data, condition)` | 更新符合条件的数据 | `Promise<T \| null>` |
|
|
106
|
+
| `delete(condition)` | 删除符合条件的数据 | `Promise<number>` |
|
|
107
|
+
| `count(condition)` | 统计符合条件的数量 | `Promise<number>` |
|
|
108
|
+
| `getPage(condition)` | 分页获取数据 | `Promise<IPage<T>>` |
|
|
109
|
+
| `clear()` | 清空所有数据 | `Promise<void>` |
|
|
100
110
|
|
|
101
|
-
|
|
111
|
+
### 查询条件
|
|
102
112
|
|
|
103
113
|
```typescript
|
|
104
|
-
|
|
114
|
+
interface IQueryCondition<T> {
|
|
115
|
+
where?: Partial<T>; // 查询条件
|
|
116
|
+
sort?: Record<string, 1 | -1>; // 排序规则
|
|
117
|
+
page?: number; // 页码(从 1 开始)
|
|
118
|
+
pageSize?: number; // 每页数量
|
|
119
|
+
fields?: Partial<Record<keyof T, 1>>; // 字段选择
|
|
120
|
+
}
|
|
105
121
|
```
|
|
106
122
|
|
|
107
|
-
|
|
123
|
+
### 查询操作符
|
|
108
124
|
|
|
109
|
-
|
|
110
|
-
|
|
111
|
-
|
|
112
|
-
|
|
113
|
-
|
|
114
|
-
|
|
115
|
-
|
|
125
|
+
| 操作符 | 说明 | 示例 |
|
|
126
|
+
|--------|------|------|
|
|
127
|
+
| `$eq` | 等于 | `{ age: { $eq: 18 } }` |
|
|
128
|
+
| `$neq` | 不等于 | `{ age: { $neq: 18 } }` |
|
|
129
|
+
| `$gt` | 大于 | `{ age: { $gt: 18 } }` |
|
|
130
|
+
| `$gte` | 大于等于 | `{ age: { $gte: 18 } }` |
|
|
131
|
+
| `$lt` | 小于 | `{ age: { $lt: 18 } }` |
|
|
132
|
+
| `$lte` | 小于等于 | `{ age: { $lte: 18 } }` |
|
|
133
|
+
| `$in` | 在数组中 | `{ age: { $in: [18, 25, 30] } }` |
|
|
134
|
+
| `$nin` | 不在数组中 | `{ age: { $nin: [18, 25] } }` |
|
|
135
|
+
| `$like` | 包含字符串 | `{ name: { $like: 'John' } }` |
|
|
136
|
+
| `$likeIgnoreCase` | 包含字符串(忽略大小写) | `{ name: { $likeIgnoreCase: 'john' } }` |
|
|
137
|
+
| `$startsWith` | 以指定字符串开头 | `{ name: { $startsWith: 'J' } }` |
|
|
138
|
+
| `$startsWithIgnoreCase` | 以指定字符串开头(忽略大小写) | `{ name: { $startsWithIgnoreCase: 'j' } }` |
|
|
139
|
+
| `$regexp` | 正则表达式匹配 | `{ email: { $regexp: /^[a-z]+@/ } }` |
|
|
140
|
+
|
|
141
|
+
### 各类存储特点
|
|
142
|
+
|
|
143
|
+
#### EasyDexieStore (IndexedDB)
|
|
144
|
+
|
|
145
|
+
- **容量**:大容量(通常 50MB+)
|
|
146
|
+
- **持久化**:是
|
|
147
|
+
- **查询能力**:支持完整查询操作符
|
|
148
|
+
- **适用场景**:大量数据存储、复杂查询
|
|
116
149
|
|
|
117
150
|
```typescript
|
|
118
|
-
|
|
119
|
-
const all = await store.getList();
|
|
120
|
-
|
|
121
|
-
// 条件查询
|
|
122
|
-
const filtered = await store.getList({
|
|
123
|
-
where: { category: '电子产品' }
|
|
124
|
-
});
|
|
125
|
-
|
|
126
|
-
// 排序查询
|
|
127
|
-
const sorted = await store.getList({
|
|
128
|
-
sort: { price: -1 } // -1 降序,1 升序
|
|
129
|
-
});
|
|
130
|
-
|
|
131
|
-
// 复杂条件
|
|
132
|
-
const complex = await store.getList({
|
|
133
|
-
where: {
|
|
134
|
-
price: { $gt: 1000, $lt: 10000 }
|
|
135
|
-
}
|
|
136
|
-
});
|
|
151
|
+
const store = new EasyDexieStore<User>('users', 'my-db', 'id,createTime,updateTime,name,email');
|
|
137
152
|
```
|
|
138
153
|
|
|
139
|
-
####
|
|
154
|
+
#### EasyMemoryStore (内存)
|
|
155
|
+
|
|
156
|
+
- **容量**:受限于浏览器内存
|
|
157
|
+
- **持久化**:否(应用重启后丢失)
|
|
158
|
+
- **查询能力**:支持完整查询操作符
|
|
159
|
+
- **适用场景**:临时数据、缓存
|
|
140
160
|
|
|
141
161
|
```typescript
|
|
142
|
-
|
|
162
|
+
const store = new EasyMemoryStore<User>('temp-data');
|
|
143
163
|
```
|
|
144
164
|
|
|
145
|
-
|
|
165
|
+
#### EasyLocalStorage (localStorage)
|
|
146
166
|
|
|
147
|
-
|
|
167
|
+
- **容量**:约 5MB
|
|
168
|
+
- **持久化**:是
|
|
169
|
+
- **查询能力**:支持完整查询操作符
|
|
170
|
+
- **适用场景**:中等数据量、需要持久化
|
|
148
171
|
|
|
149
172
|
```typescript
|
|
150
|
-
|
|
173
|
+
const store = new EasyLocalStorage<User>('user-preferences');
|
|
151
174
|
```
|
|
152
175
|
|
|
153
|
-
|
|
176
|
+
#### EasySessionStorage (sessionStorage)
|
|
154
177
|
|
|
155
|
-
|
|
178
|
+
- **容量**:约 5MB
|
|
179
|
+
- **持久化**:否(页面关闭后丢失)
|
|
180
|
+
- **查询能力**:支持完整查询操作符
|
|
181
|
+
- **适用场景**:会话数据、临时表单
|
|
156
182
|
|
|
157
183
|
```typescript
|
|
158
|
-
|
|
184
|
+
const store = new EasySessionStorage<User>('session-data');
|
|
159
185
|
```
|
|
160
186
|
|
|
161
|
-
|
|
187
|
+
#### EasyCookiesStore (Cookies)
|
|
162
188
|
|
|
163
|
-
|
|
189
|
+
- **容量**:约 4KB
|
|
190
|
+
- **持久化**:可设置过期时间
|
|
191
|
+
- **查询能力**:支持完整查询操作符
|
|
192
|
+
- **适用场景**:小数据量、需要与服务端共享
|
|
164
193
|
|
|
165
194
|
```typescript
|
|
166
|
-
|
|
195
|
+
const store = new EasyCookiesStore<User>('user-settings');
|
|
167
196
|
```
|
|
168
197
|
|
|
169
|
-
|
|
198
|
+
## 📝 使用示例
|
|
170
199
|
|
|
171
|
-
|
|
200
|
+
### 完整 CRUD 示例
|
|
172
201
|
|
|
173
202
|
```typescript
|
|
174
|
-
|
|
175
|
-
```
|
|
176
|
-
|
|
177
|
-
返回分页结果:
|
|
203
|
+
import { EasyDexieStore } from '@iss-ai/easy-web-store';
|
|
178
204
|
|
|
179
|
-
|
|
180
|
-
|
|
181
|
-
|
|
182
|
-
|
|
183
|
-
|
|
184
|
-
|
|
185
|
-
|
|
205
|
+
interface Product {
|
|
206
|
+
id?: string;
|
|
207
|
+
name: string;
|
|
208
|
+
price: number;
|
|
209
|
+
category: string;
|
|
210
|
+
tags?: string[];
|
|
211
|
+
createTime?: Date;
|
|
212
|
+
updateTime?: Date;
|
|
186
213
|
}
|
|
187
|
-
```
|
|
188
214
|
|
|
189
|
-
|
|
215
|
+
const productStore = new EasyDexieStore<Product>('products', 'shop-db');
|
|
190
216
|
|
|
191
|
-
|
|
192
|
-
|
|
193
|
-
|
|
217
|
+
// 创建
|
|
218
|
+
await productStore.save({
|
|
219
|
+
name: 'iPhone 15',
|
|
220
|
+
price: 6999,
|
|
221
|
+
category: 'electronics',
|
|
222
|
+
tags: ['apple', 'smartphone']
|
|
223
|
+
});
|
|
194
224
|
|
|
195
|
-
|
|
225
|
+
// 批量创建
|
|
226
|
+
await productStore.saveList([
|
|
227
|
+
{ name: 'MacBook Pro', price: 14999, category: 'electronics' },
|
|
228
|
+
{ name: 'AirPods', price: 1299, category: 'electronics' }
|
|
229
|
+
]);
|
|
196
230
|
|
|
197
|
-
|
|
231
|
+
// 查询 - 简单条件
|
|
232
|
+
const electronics = await productStore.getList({
|
|
233
|
+
where: { category: 'electronics' }
|
|
234
|
+
});
|
|
198
235
|
|
|
199
|
-
|
|
200
|
-
|
|
201
|
-
|
|
236
|
+
// 查询 - 复杂条件
|
|
237
|
+
const expensiveProducts = await productStore.getList({
|
|
238
|
+
where: {
|
|
239
|
+
price: { $gt: 5000 },
|
|
240
|
+
name: { $likeIgnoreCase: 'iphone' }
|
|
241
|
+
},
|
|
242
|
+
sort: { price: -1 }
|
|
243
|
+
});
|
|
202
244
|
|
|
203
|
-
|
|
245
|
+
// 查询 - 正则表达式
|
|
246
|
+
const appleProducts = await productStore.getList({
|
|
247
|
+
where: { name: { $regexp: /apple/i } }
|
|
248
|
+
});
|
|
204
249
|
|
|
205
|
-
|
|
250
|
+
// 分页查询
|
|
251
|
+
const page1 = await productStore.getPage({
|
|
252
|
+
where: { category: 'electronics' },
|
|
253
|
+
page: 1,
|
|
254
|
+
pageSize: 10,
|
|
255
|
+
sort: { createTime: -1 }
|
|
256
|
+
});
|
|
206
257
|
|
|
207
|
-
|
|
208
|
-
|
|
209
|
-
|
|
210
|
-
|
|
211
|
-
|
|
212
|
-
| `$lt` | 小于 | `{ price: { $lt: 100 } }` |
|
|
213
|
-
| `$lte` | 小于等于 | `{ price: { $lte: 100 } }` |
|
|
214
|
-
| `$in` | 在数组中 | `{ category: { $in: ['A', 'B'] } }` |
|
|
215
|
-
| `$nin` | 不在数组中 | `{ category: { $nin: ['C'] } }` |
|
|
216
|
-
| `$like` | 包含字符串 | `{ name: { $like: 'phone' } }` |
|
|
258
|
+
// 更新
|
|
259
|
+
await productStore.update(
|
|
260
|
+
{ price: 6799 },
|
|
261
|
+
{ name: 'iPhone 15' }
|
|
262
|
+
);
|
|
217
263
|
|
|
218
|
-
|
|
264
|
+
// 统计
|
|
265
|
+
const count = await productStore.count({ category: 'electronics' });
|
|
219
266
|
|
|
220
|
-
|
|
267
|
+
// 删除
|
|
268
|
+
await productStore.delete({ category: 'electronics' });
|
|
221
269
|
|
|
222
|
-
|
|
270
|
+
// 清空
|
|
271
|
+
await productStore.clear();
|
|
272
|
+
```
|
|
273
|
+
|
|
274
|
+
### 响应式数据同步示例
|
|
223
275
|
|
|
224
276
|
```typescript
|
|
225
|
-
|
|
226
|
-
|
|
277
|
+
import { EasyLocalStorage } from '@iss-ai/easy-web-store';
|
|
278
|
+
|
|
279
|
+
interface Todo {
|
|
280
|
+
id?: string;
|
|
281
|
+
title: string;
|
|
282
|
+
completed: boolean;
|
|
227
283
|
createTime?: Date;
|
|
228
284
|
updateTime?: Date;
|
|
229
285
|
}
|
|
230
|
-
```
|
|
231
286
|
|
|
232
|
-
|
|
287
|
+
class TodoStore {
|
|
288
|
+
private store: EasyLocalStorage<Todo>;
|
|
289
|
+
private listeners: Set<() => void> = new Set();
|
|
233
290
|
|
|
234
|
-
|
|
291
|
+
constructor() {
|
|
292
|
+
this.store = new EasyLocalStorage<Todo>('todos');
|
|
293
|
+
}
|
|
235
294
|
|
|
236
|
-
|
|
237
|
-
|
|
238
|
-
|
|
239
|
-
|
|
240
|
-
|
|
241
|
-
|
|
242
|
-
|
|
243
|
-
|
|
244
|
-
|
|
295
|
+
// 订阅数据变化
|
|
296
|
+
subscribe(listener: () => void) {
|
|
297
|
+
this.listeners.add(listener);
|
|
298
|
+
return () => this.listeners.delete(listener);
|
|
299
|
+
}
|
|
300
|
+
|
|
301
|
+
// 通知所有订阅者
|
|
302
|
+
private notify() {
|
|
303
|
+
this.listeners.forEach(listener => listener());
|
|
304
|
+
}
|
|
245
305
|
|
|
246
|
-
|
|
306
|
+
async addTodo(title: string) {
|
|
307
|
+
await this.store.save({ title, completed: false });
|
|
308
|
+
this.notify();
|
|
309
|
+
}
|
|
247
310
|
|
|
248
|
-
|
|
311
|
+
async toggleTodo(id: string) {
|
|
312
|
+
const todo = await this.store.getInfo({ id });
|
|
313
|
+
if (todo) {
|
|
314
|
+
await this.store.update({ completed: !todo.completed }, { id });
|
|
315
|
+
this.notify();
|
|
316
|
+
}
|
|
317
|
+
}
|
|
249
318
|
|
|
250
|
-
|
|
251
|
-
|
|
252
|
-
|
|
253
|
-
total: number; // 总记录数
|
|
254
|
-
page: number; // 当前页码
|
|
255
|
-
size: number; // 每页数量
|
|
256
|
-
totalPages: number; // 总页数
|
|
319
|
+
async getTodos() {
|
|
320
|
+
return await this.store.getList({ sort: { createTime: -1 } });
|
|
321
|
+
}
|
|
257
322
|
}
|
|
258
323
|
```
|
|
259
324
|
|
|
260
|
-
##
|
|
261
|
-
|
|
262
|
-
### 环境要求
|
|
263
|
-
|
|
264
|
-
- Node.js >= 18.0.0
|
|
265
|
-
- pnpm (推荐) 或 npm/yarn
|
|
266
|
-
|
|
267
|
-
### 本地开发
|
|
325
|
+
## 🔧 开发
|
|
268
326
|
|
|
269
327
|
```bash
|
|
270
|
-
# 克隆仓库
|
|
271
|
-
git clone https://github.com/your-repo/web-easy-store.git
|
|
272
|
-
|
|
273
|
-
# 进入目录
|
|
274
|
-
cd web-easy-store
|
|
275
|
-
|
|
276
328
|
# 安装依赖
|
|
277
|
-
|
|
329
|
+
npm install
|
|
278
330
|
|
|
279
331
|
# 构建
|
|
280
|
-
|
|
281
|
-
|
|
282
|
-
# 运行测试
|
|
283
|
-
pnpm test
|
|
284
|
-
```
|
|
332
|
+
npm run build
|
|
285
333
|
|
|
286
|
-
|
|
334
|
+
# 测试
|
|
335
|
+
npm test
|
|
287
336
|
|
|
337
|
+
# 类型检查
|
|
338
|
+
npm run tsc
|
|
288
339
|
```
|
|
289
|
-
lib/
|
|
290
|
-
├── index.cjs.js # CommonJS 模块
|
|
291
|
-
├── index.esm.js # ES 模块
|
|
292
|
-
├── index.umd.js # UMD 模块
|
|
293
|
-
├── index.min.js # 压缩版
|
|
294
|
-
└── index.d.ts # TypeScript 类型定义
|
|
295
|
-
```
|
|
296
|
-
|
|
297
|
-
## 📝 示例
|
|
298
|
-
|
|
299
|
-
查看 [`example/`](example/) 目录获取更多使用示例。
|
|
300
340
|
|
|
301
341
|
## 📄 许可证
|
|
302
342
|
|
|
303
|
-
MIT License
|
|
304
|
-
|
|
305
|
-
## 🤝 贡献
|
|
306
|
-
|
|
307
|
-
欢迎提交 Issue 和 Pull Request!
|
|
308
|
-
|
|
309
|
-
---
|
|
310
|
-
|
|
311
|
-
Built with ❤️ using TypeScript and Dexie.js
|
|
343
|
+
MIT License
|