@kidus.dev/flowdb 1.0.0 → 1.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 +360 -7
- package/dist/index.d.ts +3 -0
- package/dist/index.d.ts.map +1 -0
- package/dist/index.js +2 -0
- package/package.json +1 -1
package/README.md
CHANGED
|
@@ -1,15 +1,368 @@
|
|
|
1
1
|
# flowdb
|
|
2
2
|
|
|
3
|
-
|
|
3
|
+
A lightweight, file-based local database for Dart and Flutter. flowdb stores data on disk as JSON, with collections for structured records, key-value stores, chunked blob storage, backups, and optional encryption. It also includes reactive state (FlowState) and a Flutter widget (FlowBuilder) for stream-driven UIs.
|
|
4
4
|
|
|
5
|
-
|
|
6
|
-
|
|
5
|
+
---
|
|
6
|
+
|
|
7
|
+
## Features
|
|
8
|
+
|
|
9
|
+
- **File-based persistence** — no native drivers or SQL; data lives under a database directory on the filesystem
|
|
10
|
+
- **Collections** — document-style records with auto-generated CUID ids, CRUD, and rich querying
|
|
11
|
+
- **Key-value stores** — simple get / set / remove storage scoped to the database
|
|
12
|
+
- **Blob storage** — large binary files stored in configurable chunks with metadata in a collection
|
|
13
|
+
- **Query builder** — filter, sort, paginate, project fields, and run bulk updates/deletes
|
|
14
|
+
- **Relations** — define one-to-one, one-to-many, and many-to-many links between collections
|
|
15
|
+
- **Backups** — snapshot the entire database directory
|
|
16
|
+
- **Optional encryption** — encrypt collection and store payloads at rest
|
|
17
|
+
- **Sync and async APIs** — most operations offer both `foo()` and `fooSync()` variants
|
|
18
|
+
- **Reactive state** — `FlowState<T>` built on RxDart BehaviorSubject
|
|
19
|
+
- **Flutter integration** — `FlowBuilder` connects FlowState streams to widgets
|
|
20
|
+
|
|
21
|
+
---
|
|
22
|
+
|
|
23
|
+
## Requirements
|
|
24
|
+
|
|
25
|
+
- Dart SDK `^3.10.7`
|
|
26
|
+
- Flutter `>=1.17.0` (only if you use `package:flowdb/flutter.dart`)
|
|
27
|
+
|
|
28
|
+
---
|
|
29
|
+
|
|
30
|
+
## Installation
|
|
31
|
+
|
|
32
|
+
Add flowdb to your `pubspec.yaml`:
|
|
33
|
+
|
|
34
|
+
```yaml
|
|
35
|
+
dependencies:
|
|
36
|
+
flowdb: ^1.0.0
|
|
37
|
+
```
|
|
38
|
+
|
|
39
|
+
For Dart-only projects, import the core library:
|
|
40
|
+
|
|
41
|
+
```dart
|
|
42
|
+
import 'package:flowdb/core.dart';
|
|
43
|
+
```
|
|
44
|
+
|
|
45
|
+
For Flutter apps that need FlowBuilder, import:
|
|
46
|
+
|
|
47
|
+
```dart
|
|
48
|
+
import 'package:flowdb/flutter.dart';
|
|
49
|
+
```
|
|
50
|
+
|
|
51
|
+
---
|
|
52
|
+
|
|
53
|
+
## Quick start
|
|
54
|
+
|
|
55
|
+
```dart
|
|
56
|
+
import 'package:flowdb/core.dart';
|
|
57
|
+
|
|
58
|
+
Future<void> main() async {
|
|
59
|
+
final db = openDatabase('my_app', path: './data/my_app');
|
|
60
|
+
|
|
61
|
+
final users = db.collection('users');
|
|
62
|
+
|
|
63
|
+
final alice = await users.add({'name': 'Alice', 'age': 30});
|
|
64
|
+
print(alice.id); // auto-generated CUID
|
|
65
|
+
|
|
66
|
+
final found = await users.where('name', eq: 'Alice').getFirst();
|
|
67
|
+
print(found?.data);
|
|
68
|
+
|
|
69
|
+
final settings = db.store('settings');
|
|
70
|
+
settings.set('theme', 'dark');
|
|
71
|
+
print(settings.get('theme'));
|
|
72
|
+
}
|
|
73
|
+
```
|
|
74
|
+
|
|
75
|
+
---
|
|
76
|
+
|
|
77
|
+
## Core concepts
|
|
78
|
+
|
|
79
|
+
### Database
|
|
80
|
+
|
|
81
|
+
Open a database with `openDatabase`. All collections, stores, blobs, and backups live under the database path.
|
|
82
|
+
|
|
83
|
+
```dart
|
|
84
|
+
final db = openDatabase(
|
|
85
|
+
'my_app',
|
|
86
|
+
path: './data/my_app', // optional; defaults to the database name
|
|
87
|
+
encrypted: false, // encrypt collection/store file contents
|
|
88
|
+
isWeb: false, // web platform hint
|
|
89
|
+
);
|
|
90
|
+
```
|
|
91
|
+
|
|
92
|
+
| Method / property | Description |
|
|
93
|
+
|------------------------|-----------------------------------------------|
|
|
94
|
+
| `collection(name)` | Get or create a collection |
|
|
95
|
+
| `store(name)` | Get or create a key-value store |
|
|
96
|
+
| `blobStorage(name)` | Get or create chunked blob storage |
|
|
97
|
+
| `relation(...)` | Define a relation between collections |
|
|
98
|
+
| `backup(name)` | Create a filesystem backup |
|
|
99
|
+
| `reset() / clear()` | Delete and recreate the database directory |
|
|
100
|
+
| `drop()` | Delete the database directory entirely |
|
|
101
|
+
| `size / sizeFormatted` | Total on-disk size |
|
|
102
|
+
|
|
103
|
+
---
|
|
104
|
+
|
|
105
|
+
### Collections and records
|
|
106
|
+
|
|
107
|
+
A collection holds records — maps of fields keyed by string, each with a stable id.
|
|
108
|
+
|
|
109
|
+
```dart
|
|
110
|
+
final posts = db.collection('posts');
|
|
111
|
+
|
|
112
|
+
// Create
|
|
113
|
+
final record = await posts.add({'title': 'Hello', 'published': true});
|
|
114
|
+
final batch = await posts.addMany([
|
|
115
|
+
{'title': 'First'},
|
|
116
|
+
{'title': 'Second'},
|
|
117
|
+
]);
|
|
118
|
+
|
|
119
|
+
// Read by id
|
|
120
|
+
final one = await posts.getById(record.id);
|
|
121
|
+
|
|
122
|
+
// Read all
|
|
123
|
+
final all = await posts.getAll();
|
|
124
|
+
final total = await posts.count;
|
|
125
|
+
```
|
|
126
|
+
|
|
127
|
+
Records support nested field access via dot notation:
|
|
128
|
+
|
|
129
|
+
```dart
|
|
130
|
+
record.set('author.name', 'Alice');
|
|
131
|
+
final authorName = record.get('author.name');
|
|
132
|
+
```
|
|
133
|
+
|
|
134
|
+
Export a collection as CSV:
|
|
135
|
+
|
|
136
|
+
```dart
|
|
137
|
+
final csv = await posts.exportAsCSV();
|
|
138
|
+
```
|
|
139
|
+
|
|
140
|
+
---
|
|
141
|
+
|
|
142
|
+
### Querying
|
|
143
|
+
|
|
144
|
+
Chain filters on `collection.where(...)`, `collection.builder`, or the query returned by `where`:
|
|
145
|
+
|
|
146
|
+
```dart
|
|
147
|
+
final results = await posts
|
|
148
|
+
.where('published', eq: true)
|
|
149
|
+
.and('views', gte: 100)
|
|
150
|
+
.orderBy('title')
|
|
151
|
+
.skip(10)
|
|
152
|
+
.limit(20)
|
|
153
|
+
.get();
|
|
154
|
+
|
|
155
|
+
final first = await posts.where('title', startsWith: 'Hello').getFirst();
|
|
156
|
+
final matching = await posts.where('tags', contains: 'dart').get();
|
|
157
|
+
```
|
|
158
|
+
|
|
159
|
+
**Comparison operators**
|
|
160
|
+
|
|
161
|
+
| Parameter | Meaning |
|
|
162
|
+
|-------------------------------------------|--------------------------------------|
|
|
163
|
+
| `eq` / `neq` | Equal / not equal |
|
|
164
|
+
| `gt` / `gte` / `lt` / `lte` | Numeric ordering |
|
|
165
|
+
| `contains` / `doesNotContain` | Substring or list membership |
|
|
166
|
+
| `startsWith` / `endsWith` | String prefix / suffix |
|
|
167
|
+
| `regexMatch` | Regular expression |
|
|
168
|
+
| `between`, `betweenStartInclusive`, etc. | Range checks |
|
|
169
|
+
| `isNull` / `isNotNull` | Null checks |
|
|
170
|
+
| `isEmpty` / `isNotEmpty` | Empty string or collection |
|
|
171
|
+
| `filter` | Custom `bool Function(dynamic value)`|
|
|
172
|
+
|
|
173
|
+
**Other query methods**
|
|
174
|
+
|
|
175
|
+
| Method | Description |
|
|
176
|
+
|-------------------|-----------------------------------------------------|
|
|
177
|
+
| `or(...)` | OR condition (after an initial where) |
|
|
178
|
+
| `pick / pickMany` | Include only selected fields |
|
|
179
|
+
| `omit / omitMany` | Exclude fields from results |
|
|
180
|
+
| `orderBy(field, desc: true)` | Sort results |
|
|
181
|
+
| `skip / limit / take` | Pagination |
|
|
182
|
+
| `include / includeMany` | Load related data (when relations configured) |
|
|
183
|
+
| `getShuffled / getRandom / getRandomMany` | Random access |
|
|
184
|
+
| `update / updateMany` | Update matching records |
|
|
185
|
+
| `delete / deleteMany` | Delete matching records |
|
|
186
|
+
| `upsert` | Update if matched, otherwise insert |
|
|
187
|
+
| `addIfAbsent` | Insert only when no match |
|
|
188
|
+
|
|
189
|
+
Sync variants mirror the async API: `getSync()`, `addSync()`, `countSync`, and so on.
|
|
190
|
+
|
|
191
|
+
---
|
|
192
|
+
|
|
193
|
+
### Key-value stores
|
|
194
|
+
|
|
195
|
+
Stores hold arbitrary JSON-serializable values under string keys.
|
|
196
|
+
|
|
197
|
+
```dart
|
|
198
|
+
final cache = db.store('cache');
|
|
199
|
+
|
|
200
|
+
cache.set('lastSync', DateTime.now().toIso8601String());
|
|
201
|
+
cache.setFrom('counter', (v) => (v as int? ?? 0) + 1, 0);
|
|
202
|
+
|
|
203
|
+
if (cache.has('lastSync')) {
|
|
204
|
+
print(cache.get('lastSync'));
|
|
205
|
+
}
|
|
206
|
+
|
|
207
|
+
final all = cache.all(); // Map of all entries
|
|
208
|
+
cache.clear();
|
|
209
|
+
cache.drop(); // delete the store from disk
|
|
210
|
+
```
|
|
211
|
+
|
|
212
|
+
---
|
|
213
|
+
|
|
214
|
+
### Blob storage
|
|
215
|
+
|
|
216
|
+
Store large binary data in chunked files. Metadata is kept in an internal collection; bytes are written in configurable chunk sizes (default 512 KB).
|
|
217
|
+
|
|
218
|
+
```dart
|
|
219
|
+
import 'dart:typed_data';
|
|
220
|
+
|
|
221
|
+
final files = db.blobStorage('files', chunksSize: 1024 * 512);
|
|
222
|
+
|
|
223
|
+
await files.add(
|
|
224
|
+
metadata: {'filename': 'photo.png', 'mime': 'image/png'},
|
|
225
|
+
bytes: Uint8List.fromList([/* ... */]),
|
|
226
|
+
);
|
|
227
|
+
|
|
228
|
+
final blob = await files.get((meta) => meta.where('filename', eq: 'photo.png'));
|
|
229
|
+
final bytes = await blob?.bytes;
|
|
230
|
+
```
|
|
231
|
+
|
|
232
|
+
---
|
|
233
|
+
|
|
234
|
+
### Relations
|
|
235
|
+
|
|
236
|
+
Define how collections link to each other. Relations are registered on the database and can be used with query `include`.
|
|
237
|
+
|
|
238
|
+
```dart
|
|
239
|
+
db
|
|
240
|
+
.relation('users', 'id', name: 'posts')
|
|
241
|
+
.hasMany('posts', 'authorId');
|
|
242
|
+
|
|
243
|
+
// one-to-one
|
|
244
|
+
db.relation('users', 'profileId', name: 'profile').hasOne('profiles', 'id');
|
|
245
|
+
|
|
246
|
+
// many-to-many
|
|
247
|
+
db.relation('users', 'id', name: 'tags').manyToMany('tags', 'id');
|
|
248
|
+
```
|
|
249
|
+
|
|
250
|
+
---
|
|
251
|
+
|
|
252
|
+
### Backups
|
|
253
|
+
|
|
254
|
+
Backups copy the database directory (excluding existing backup folders) into `backups/<name>/`.
|
|
255
|
+
|
|
256
|
+
```dart
|
|
257
|
+
final snapshot = db.backup('before-migration');
|
|
258
|
+
print(db.backups); // list of Backup instances
|
|
259
|
+
|
|
260
|
+
db.removeBackup('before-migration');
|
|
261
|
+
db.removeAllBackups();
|
|
262
|
+
```
|
|
263
|
+
|
|
264
|
+
---
|
|
265
|
+
|
|
266
|
+
### Reactive state (`FlowState`)
|
|
267
|
+
|
|
268
|
+
`FlowState<T>` wraps a seeded `BehaviorSubject` for synchronous reads and stream updates.
|
|
269
|
+
|
|
270
|
+
```dart
|
|
271
|
+
final counter = FlowState<int>(0);
|
|
272
|
+
|
|
273
|
+
counter.listen((value) => print('count: $value'));
|
|
274
|
+
counter.update(1);
|
|
275
|
+
counter.updateFrom((v) => v + 1);
|
|
276
|
+
|
|
277
|
+
final combined = FlowState.combine2(
|
|
278
|
+
counter,
|
|
279
|
+
FlowState<String>(''),
|
|
280
|
+
(count, label) => '$label: $count',
|
|
281
|
+
);
|
|
282
|
+
|
|
283
|
+
await counter.close();
|
|
7
284
|
```
|
|
8
285
|
|
|
9
|
-
|
|
286
|
+
`FlowState.combine2` through `combine9`, plus `FlowState.list`, merge multiple states into one.
|
|
287
|
+
|
|
288
|
+
---
|
|
289
|
+
|
|
290
|
+
### Flutter integration
|
|
10
291
|
|
|
11
|
-
|
|
12
|
-
|
|
292
|
+
Use `FlowBuilder` to rebuild widgets when a FlowState changes:
|
|
293
|
+
|
|
294
|
+
```dart
|
|
295
|
+
import 'package:flowdb/flutter.dart';
|
|
296
|
+
import 'package:flutter/material.dart';
|
|
297
|
+
|
|
298
|
+
class CounterView extends StatelessWidget {
|
|
299
|
+
const CounterView({super.key, required this.counter});
|
|
300
|
+
|
|
301
|
+
final FlowState<int> counter;
|
|
302
|
+
|
|
303
|
+
@override
|
|
304
|
+
Widget build(BuildContext context) {
|
|
305
|
+
return FlowBuilder<int>(
|
|
306
|
+
flow: counter,
|
|
307
|
+
builder: (value) => Text('Count: $value'),
|
|
308
|
+
onLoadingBuilder: () => const CircularProgressIndicator(),
|
|
309
|
+
onErrorBuilder: (e) => Text('Error: $e'),
|
|
310
|
+
onNoDataBuilder: () => const Text('No data'),
|
|
311
|
+
);
|
|
312
|
+
}
|
|
313
|
+
}
|
|
13
314
|
```
|
|
14
315
|
|
|
15
|
-
|
|
316
|
+
---
|
|
317
|
+
|
|
318
|
+
## Utilities
|
|
319
|
+
|
|
320
|
+
The core library also exports helpers used internally and available for app code:
|
|
321
|
+
|
|
322
|
+
| Export | Purpose |
|
|
323
|
+
|----------------|------------------------------------------------|
|
|
324
|
+
| `BGWorker` | Run a function in a separate isolate |
|
|
325
|
+
| `Json` | JSON encode/decode, nested get/update, CSV conversion |
|
|
326
|
+
| `Encryptor` | Encrypt/decrypt strings for at-rest storage |
|
|
327
|
+
| `Format` | Human-readable byte sizes |
|
|
328
|
+
| `Performance` | Timing utilities |
|
|
329
|
+
| `PrintColored` | ANSI-colored console output |
|
|
330
|
+
| `Random` | ID generation (CUID) |
|
|
331
|
+
|
|
332
|
+
---
|
|
333
|
+
|
|
334
|
+
## On-disk layout
|
|
335
|
+
|
|
336
|
+
A database directory typically looks like:
|
|
337
|
+
|
|
338
|
+
```
|
|
339
|
+
my_app/
|
|
340
|
+
├── collections/
|
|
341
|
+
│ └── users # newline-delimited JSON records
|
|
342
|
+
├── stores/
|
|
343
|
+
│ └── settings # JSON key-value file
|
|
344
|
+
├── blobs/
|
|
345
|
+
│ └── files/
|
|
346
|
+
│ ├── _metadata/ # blob metadata collection
|
|
347
|
+
│ └── chunks/ # binary chunks
|
|
348
|
+
└── backups/
|
|
349
|
+
└── snapshot_name/
|
|
350
|
+
```
|
|
351
|
+
|
|
352
|
+
---
|
|
353
|
+
|
|
354
|
+
## Sync vs async
|
|
355
|
+
|
|
356
|
+
Most collection and query methods exist in pairs:
|
|
357
|
+
|
|
358
|
+
- `add` / `addSync`
|
|
359
|
+
- `getAll` / `getAllSync`
|
|
360
|
+
- `where(...).get()` / `where(...).getSync()`
|
|
361
|
+
|
|
362
|
+
Use async methods in UI code and isolates where blocking I/O is undesirable. Use sync methods in scripts, tests, or startup paths where simplicity matters.
|
|
363
|
+
|
|
364
|
+
---
|
|
365
|
+
|
|
366
|
+
## Encryption
|
|
367
|
+
|
|
368
|
+
Pass `encrypted: true` when opening a database to encrypt collection and store file contents. Encrypted databases propagate the setting to collections and stores created through `db.collection()` and `db.store()`.
|
package/dist/index.d.ts
ADDED
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"index.d.ts","sourceRoot":"","sources":["../src/index.ts"],"names":[],"mappings":"AAAA,cAAc,0BAA0B,CAAC;AACzC,cAAc,SAAS,CAAC"}
|
package/dist/index.js
ADDED