@alexmitchell/melondb 0.0.0-stage → 0.1.0
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/LICENSE +21 -0
- package/README.md +485 -2
- package/lib/cleanRow.d.ts +11 -0
- package/lib/cleanRow.d.ts.map +1 -0
- package/lib/cleanRow.js +20 -0
- package/lib/cleanRow.js.map +1 -0
- package/lib/createAdapter.d.ts +5 -0
- package/lib/createAdapter.d.ts.map +1 -0
- package/lib/createAdapter.js +13 -0
- package/lib/createAdapter.js.map +1 -0
- package/lib/createAdapter.web.d.ts +4 -0
- package/lib/createAdapter.web.d.ts.map +1 -0
- package/lib/createAdapter.web.js +17 -0
- package/lib/createAdapter.web.js.map +1 -0
- package/lib/createMelon.d.ts +37 -0
- package/lib/createMelon.d.ts.map +1 -0
- package/lib/createMelon.js +227 -0
- package/lib/createMelon.js.map +1 -0
- package/lib/createWatermelon.d.ts +16 -0
- package/lib/createWatermelon.d.ts.map +1 -0
- package/lib/createWatermelon.js +26 -0
- package/lib/createWatermelon.js.map +1 -0
- package/lib/index.d.ts +9 -0
- package/lib/index.d.ts.map +1 -0
- package/lib/index.js +14 -0
- package/lib/index.js.map +1 -0
- package/lib/sanitizeCircular.d.ts +6 -0
- package/lib/sanitizeCircular.d.ts.map +1 -0
- package/lib/sanitizeCircular.js +68 -0
- package/lib/sanitizeCircular.js.map +1 -0
- package/package.json +56 -4
- package/src/cleanRow.ts +22 -0
- package/src/createAdapter.ts +7 -0
- package/src/createAdapter.web.ts +12 -0
- package/src/createMelon.ts +298 -0
- package/src/createWatermelon.ts +42 -0
- package/src/index.ts +12 -0
- package/src/sanitizeCircular.ts +76 -0
package/LICENSE
ADDED
|
@@ -0,0 +1,21 @@
|
|
|
1
|
+
MIT License
|
|
2
|
+
|
|
3
|
+
Copyright (c) 2026 Alzen
|
|
4
|
+
|
|
5
|
+
Permission is hereby granted, free of charge, to any person obtaining a copy
|
|
6
|
+
of this software and associated documentation files (the "Software"), to deal
|
|
7
|
+
in the Software without restriction, including without limitation the rights
|
|
8
|
+
to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
|
|
9
|
+
copies of the Software, and to permit persons to whom the Software is
|
|
10
|
+
furnished to do so, subject to the following conditions:
|
|
11
|
+
|
|
12
|
+
The above copyright notice and this permission notice shall be included in all
|
|
13
|
+
copies or substantial portions of the Software.
|
|
14
|
+
|
|
15
|
+
THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
|
|
16
|
+
IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
|
|
17
|
+
FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
|
|
18
|
+
AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
|
|
19
|
+
LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
|
|
20
|
+
OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
|
|
21
|
+
SOFTWARE.
|
package/README.md
CHANGED
|
@@ -1,3 +1,486 @@
|
|
|
1
|
-
#
|
|
1
|
+
# 🍈 MelonDB
|
|
2
2
|
|
|
3
|
-
|
|
3
|
+
Typed helpers for [WatermelonDB](https://github.com/Nozbe/WatermelonDB).
|
|
4
|
+
|
|
5
|
+
MelonDB is WatermelonDB but more DRY. WatermelonDB is excellent for building offline-first React Native apps. **MelonDB** improves the developer experience by wrapping common operations in a small, typed API you configure once per app, reducing boilerplate and making simple mistakes easier to catch.
|
|
6
|
+
|
|
7
|
+
---
|
|
8
|
+
|
|
9
|
+
## Install
|
|
10
|
+
|
|
11
|
+
```bash
|
|
12
|
+
npm install @alexmitchell/melondb @nozbe/watermelondb
|
|
13
|
+
```
|
|
14
|
+
|
|
15
|
+
```bash
|
|
16
|
+
yarn add @alexmitchell/melondb @nozbe/watermelondb
|
|
17
|
+
```
|
|
18
|
+
|
|
19
|
+
`@nozbe/watermelondb` (>= 0.27) and `react` (>= 18) are **peer dependencies**: MelonDB uses your app's copy rather than bundling its own. If you've already set up WatermelonDB you have both.
|
|
20
|
+
|
|
21
|
+
---
|
|
22
|
+
|
|
23
|
+
## Quick start
|
|
24
|
+
|
|
25
|
+
Suppose you have some note data to insert:
|
|
26
|
+
|
|
27
|
+
```ts
|
|
28
|
+
const draft = {
|
|
29
|
+
title: "Hello",
|
|
30
|
+
body: "World",
|
|
31
|
+
// ...
|
|
32
|
+
};
|
|
33
|
+
```
|
|
34
|
+
|
|
35
|
+
With **WatermelonDB**, creating the row looks like this:
|
|
36
|
+
|
|
37
|
+
```ts
|
|
38
|
+
const noteId = await database.write(async () => {
|
|
39
|
+
const note = await database.get<NoteModel>("notes").create((record) => {
|
|
40
|
+
record.title = draft.title;
|
|
41
|
+
record.body = draft.body;
|
|
42
|
+
// ...
|
|
43
|
+
});
|
|
44
|
+
return note.id;
|
|
45
|
+
});
|
|
46
|
+
```
|
|
47
|
+
|
|
48
|
+
WatermelonDB requires each field to be assigned manually. TypeScript can catch a wrong type when it is assigned to a typed model property, but it does not check `draft` as a complete `notes` payload at the database call. A missing assignment is easy to overlook, and the `"notes"` string is not tied to the `NoteModel` type argument.
|
|
49
|
+
|
|
50
|
+
Once [configured](#setup), **MelonDB** reduces the same operation to:
|
|
51
|
+
|
|
52
|
+
```ts
|
|
53
|
+
const noteId = await melon.insert("notes", draft);
|
|
54
|
+
```
|
|
55
|
+
|
|
56
|
+
MelonDB uses `"notes"` to infer `TableData["notes"]`, then checks `draft` against it. A missing required property or a value with the wrong type—such as `body: 123`—causes a TypeScript error at the `insert` call. An unknown table name is rejected too.
|
|
57
|
+
|
|
58
|
+
---
|
|
59
|
+
|
|
60
|
+
## Setup
|
|
61
|
+
|
|
62
|
+
### Define your tables
|
|
63
|
+
|
|
64
|
+
You still define each model and schema the WatermelonDB way. MelonDB only replaces the repetitive database access layer.
|
|
65
|
+
|
|
66
|
+
```ts
|
|
67
|
+
import { Model, tableSchema } from "@nozbe/watermelondb";
|
|
68
|
+
import { text, readonly, date } from "@nozbe/watermelondb/decorators";
|
|
69
|
+
|
|
70
|
+
class NoteModel extends Model {
|
|
71
|
+
static table = "notes";
|
|
72
|
+
|
|
73
|
+
@text("title") title!: string;
|
|
74
|
+
@text("body") body!: string;
|
|
75
|
+
@readonly @date("created_at") createdAt!: Date;
|
|
76
|
+
}
|
|
77
|
+
|
|
78
|
+
type Note = Omit<NoteModel, keyof Model>;
|
|
79
|
+
|
|
80
|
+
const noteSchema = tableSchema({
|
|
81
|
+
name: "notes",
|
|
82
|
+
columns: [
|
|
83
|
+
{ name: "title", type: "string" },
|
|
84
|
+
{ name: "body", type: "string" },
|
|
85
|
+
{ name: "created_at", type: "number" },
|
|
86
|
+
],
|
|
87
|
+
});
|
|
88
|
+
```
|
|
89
|
+
|
|
90
|
+
### Initialize MelonDB
|
|
91
|
+
|
|
92
|
+
Then register your tables once in the app-wide configuration:
|
|
93
|
+
|
|
94
|
+
```ts
|
|
95
|
+
import { createMelon } from "@alexmitchell/melondb";
|
|
96
|
+
|
|
97
|
+
// Register every table in these app-wide definitions.
|
|
98
|
+
|
|
99
|
+
type TableModels = {
|
|
100
|
+
notes: NoteModel;
|
|
101
|
+
// ...
|
|
102
|
+
};
|
|
103
|
+
|
|
104
|
+
type TableData = {
|
|
105
|
+
notes: Note;
|
|
106
|
+
// ...
|
|
107
|
+
};
|
|
108
|
+
|
|
109
|
+
const tables = [
|
|
110
|
+
noteSchema,
|
|
111
|
+
// ...
|
|
112
|
+
];
|
|
113
|
+
|
|
114
|
+
const modelClasses = [
|
|
115
|
+
NoteModel,
|
|
116
|
+
// ...
|
|
117
|
+
];
|
|
118
|
+
|
|
119
|
+
export const melon = createMelon<TableModels, TableData>({
|
|
120
|
+
version: 1,
|
|
121
|
+
tables,
|
|
122
|
+
modelClasses,
|
|
123
|
+
migrations: [],
|
|
124
|
+
});
|
|
125
|
+
```
|
|
126
|
+
|
|
127
|
+
That gives you a typed `melon` instance plus the underlying `melon.db` if you need raw WatermelonDB APIs.
|
|
128
|
+
|
|
129
|
+
---
|
|
130
|
+
|
|
131
|
+
## More examples
|
|
132
|
+
|
|
133
|
+
### Batch create
|
|
134
|
+
|
|
135
|
+
Suppose you have several notes to insert:
|
|
136
|
+
|
|
137
|
+
```ts
|
|
138
|
+
const rows = [
|
|
139
|
+
{ title: "One", body: "..." },
|
|
140
|
+
{ title: "Two", body: "..." },
|
|
141
|
+
// ...
|
|
142
|
+
];
|
|
143
|
+
```
|
|
144
|
+
|
|
145
|
+
**WatermelonDB**
|
|
146
|
+
|
|
147
|
+
```ts
|
|
148
|
+
await database.write(async () => {
|
|
149
|
+
const collection = database.get<NoteModel>("notes");
|
|
150
|
+
const prepared = rows.map((row) =>
|
|
151
|
+
collection.prepareCreate((record) => {
|
|
152
|
+
record.title = row.title;
|
|
153
|
+
record.body = row.body;
|
|
154
|
+
}),
|
|
155
|
+
);
|
|
156
|
+
await database.batch(prepared);
|
|
157
|
+
});
|
|
158
|
+
```
|
|
159
|
+
|
|
160
|
+
**MelonDB**
|
|
161
|
+
|
|
162
|
+
```ts
|
|
163
|
+
await melon.insertBatch("notes", rows);
|
|
164
|
+
```
|
|
165
|
+
|
|
166
|
+
---
|
|
167
|
+
|
|
168
|
+
### Fetch one row by id
|
|
169
|
+
|
|
170
|
+
WatermelonDB's `find` rejects if the row does not exist, so returning `null` instead requires handling that error yourself.
|
|
171
|
+
|
|
172
|
+
**WatermelonDB**
|
|
173
|
+
|
|
174
|
+
```ts
|
|
175
|
+
let note: NoteModel | null = null;
|
|
176
|
+
try {
|
|
177
|
+
note = await database.get<NoteModel>("notes").find(id);
|
|
178
|
+
} catch {
|
|
179
|
+
note = null;
|
|
180
|
+
}
|
|
181
|
+
```
|
|
182
|
+
|
|
183
|
+
**MelonDB**
|
|
184
|
+
|
|
185
|
+
```ts
|
|
186
|
+
const note = await melon.getRowById("notes", id); // NoteModel | null
|
|
187
|
+
```
|
|
188
|
+
|
|
189
|
+
MelonDB handles the missing-row case and returns `null`. It also infers `NoteModel` from the `"notes"` table name, so the string and model type cannot drift apart.
|
|
190
|
+
|
|
191
|
+
---
|
|
192
|
+
|
|
193
|
+
### Update one field
|
|
194
|
+
|
|
195
|
+
Updating a single column on a single row is a common operation. Suppose you want to rename a note:
|
|
196
|
+
|
|
197
|
+
```ts
|
|
198
|
+
const newTitle = "Renamed";
|
|
199
|
+
```
|
|
200
|
+
|
|
201
|
+
**WatermelonDB**
|
|
202
|
+
|
|
203
|
+
```ts
|
|
204
|
+
await database.write(async () => {
|
|
205
|
+
await note.update((row) => {
|
|
206
|
+
row.title = newTitle;
|
|
207
|
+
// easy to forget a convention like updatedDate
|
|
208
|
+
});
|
|
209
|
+
});
|
|
210
|
+
```
|
|
211
|
+
|
|
212
|
+
**MelonDB**
|
|
213
|
+
|
|
214
|
+
```ts
|
|
215
|
+
await melon.updateColumn(note, "title", newTitle);
|
|
216
|
+
```
|
|
217
|
+
|
|
218
|
+
`updateColumn` ties the property name to that property's value type. Because `title` is a string, TypeScript would flag `newTitle` if it were a number. If the model has an `updatedDate` property, MelonDB also sets it to `new Date().toISOString()` automatically.
|
|
219
|
+
|
|
220
|
+
For several fields:
|
|
221
|
+
|
|
222
|
+
```ts
|
|
223
|
+
await melon.updateColumns(note, [
|
|
224
|
+
{ property: "title", value: "Renamed" },
|
|
225
|
+
{ property: "body", value: "New body" },
|
|
226
|
+
]);
|
|
227
|
+
```
|
|
228
|
+
|
|
229
|
+
Or a full updater when you need custom logic:
|
|
230
|
+
|
|
231
|
+
```ts
|
|
232
|
+
await melon.updateRow(note, (row) => {
|
|
233
|
+
row.title = row.title.trim();
|
|
234
|
+
});
|
|
235
|
+
```
|
|
236
|
+
|
|
237
|
+
---
|
|
238
|
+
|
|
239
|
+
### Soft-delete
|
|
240
|
+
|
|
241
|
+
WatermelonDB deletes model instances rather than IDs. If all you have is `noteId`, you must fetch the row before you can delete it:
|
|
242
|
+
|
|
243
|
+
**WatermelonDB**
|
|
244
|
+
|
|
245
|
+
```ts
|
|
246
|
+
const note = await database.get<NoteModel>("notes").find(noteId);
|
|
247
|
+
|
|
248
|
+
await database.write(async () => {
|
|
249
|
+
await note.markAsDeleted();
|
|
250
|
+
});
|
|
251
|
+
```
|
|
252
|
+
|
|
253
|
+
**MelonDB**
|
|
254
|
+
|
|
255
|
+
```ts
|
|
256
|
+
await melon.deleteRowById("notes", noteId);
|
|
257
|
+
```
|
|
258
|
+
|
|
259
|
+
`deleteRowById` performs the lookup and soft-delete for you. If you already have the model instance, use `melon.deleteRow(note)` instead.
|
|
260
|
+
|
|
261
|
+
---
|
|
262
|
+
|
|
263
|
+
### Typed collections
|
|
264
|
+
|
|
265
|
+
**WatermelonDB**
|
|
266
|
+
|
|
267
|
+
```ts
|
|
268
|
+
const notes = database.get<NoteModel>("notes");
|
|
269
|
+
// table name is a free string; generics are easy to drift
|
|
270
|
+
```
|
|
271
|
+
|
|
272
|
+
**MelonDB**
|
|
273
|
+
|
|
274
|
+
```ts
|
|
275
|
+
const notes = melon.get("notes");
|
|
276
|
+
// Collection<NoteModel> — table name is keyed by TableModels
|
|
277
|
+
```
|
|
278
|
+
|
|
279
|
+
---
|
|
280
|
+
|
|
281
|
+
### Observable components (`withObservables`)
|
|
282
|
+
|
|
283
|
+
WatermelonDB's `withObservables` HOC is powerful but awkward to type when you also have query props and extra component props.
|
|
284
|
+
|
|
285
|
+
**WatermelonDB** (common pattern: cast and hope)
|
|
286
|
+
|
|
287
|
+
```ts
|
|
288
|
+
export default withObservables(["noteId"], ({ noteId }) => ({
|
|
289
|
+
note: database.get("notes").findAndObserve(noteId),
|
|
290
|
+
}))(NoteBody) as React.ComponentType<{ noteId: string }>;
|
|
291
|
+
```
|
|
292
|
+
|
|
293
|
+
**MelonDB**
|
|
294
|
+
|
|
295
|
+
```ts
|
|
296
|
+
type Injected = { note: NoteModel };
|
|
297
|
+
type Query = { noteId: string };
|
|
298
|
+
type Props = { showTimestamp: boolean };
|
|
299
|
+
|
|
300
|
+
const enhancer = withObservables(["noteId"], ({ noteId }: Query) => ({
|
|
301
|
+
note: melon.get("notes").findAndObserve(noteId),
|
|
302
|
+
}));
|
|
303
|
+
|
|
304
|
+
export default melon.enhance<Injected, Query, Props>(enhancer, NoteBody);
|
|
305
|
+
// ComponentType<{ noteId: string; showTimestamp: boolean }>
|
|
306
|
+
```
|
|
307
|
+
|
|
308
|
+
`enhance` is a thin typing helper: it wires `Injected` (from the HOC), `Query` (inputs that drive observables), and optional extra `Props` without fighting the HOC generics by hand.
|
|
309
|
+
|
|
310
|
+
---
|
|
311
|
+
|
|
312
|
+
### Safe database reset
|
|
313
|
+
|
|
314
|
+
Calling `unsafeResetDatabase` while React components still observe the DB can crash (see [WatermelonDB #102](https://github.com/Nozbe/WatermelonDB/issues/102)).
|
|
315
|
+
|
|
316
|
+
**WatermelonDB**
|
|
317
|
+
|
|
318
|
+
```ts
|
|
319
|
+
// You must coordinate unmounting observers yourself, then:
|
|
320
|
+
await database.write(async () => {
|
|
321
|
+
await database.unsafeResetDatabase();
|
|
322
|
+
});
|
|
323
|
+
```
|
|
324
|
+
|
|
325
|
+
**MelonDB**
|
|
326
|
+
|
|
327
|
+
```ts
|
|
328
|
+
// Gate UI on availability:
|
|
329
|
+
const available = useSyncExternalStore(
|
|
330
|
+
melon.subscribeDatabaseAvailable,
|
|
331
|
+
melon.getDatabaseAvailable,
|
|
332
|
+
melon.getDatabaseAvailable,
|
|
333
|
+
);
|
|
334
|
+
|
|
335
|
+
if (!available) return null;
|
|
336
|
+
|
|
337
|
+
// Later, after a schema bump or logout:
|
|
338
|
+
await melon.resetDatabase();
|
|
339
|
+
```
|
|
340
|
+
|
|
341
|
+
`resetDatabase` sets availability to `false`, waits for React to unmount gated observers, resets, then sets availability back to `true`.
|
|
342
|
+
|
|
343
|
+
---
|
|
344
|
+
|
|
345
|
+
## Platform adapters
|
|
346
|
+
|
|
347
|
+
`createMelon` / `createWatermelon` pick an adapter automatically:
|
|
348
|
+
|
|
349
|
+
| Platform | Adapter |
|
|
350
|
+
| ------------- | ------------------------------------------- |
|
|
351
|
+
| iOS / Android | SQLite (`createAdapter.ts`) |
|
|
352
|
+
| Web | LokiJS + IndexedDB (`createAdapter.web.ts`) |
|
|
353
|
+
|
|
354
|
+
Metro and webpack resolve the `.web.ts` file on web. You do not need to branch this yourself.
|
|
355
|
+
|
|
356
|
+
---
|
|
357
|
+
|
|
358
|
+
## IDs
|
|
359
|
+
|
|
360
|
+
By default, MelonDB leaves WatermelonDB's own random id generator in place. Pass `idGenerator` to replace it globally:
|
|
361
|
+
|
|
362
|
+
```ts
|
|
363
|
+
createMelon({
|
|
364
|
+
// ...schema
|
|
365
|
+
idGenerator: () => myCustomId(),
|
|
366
|
+
});
|
|
367
|
+
```
|
|
368
|
+
|
|
369
|
+
See [Tips → UUID v7 ids](#uuid-v7-ids) for a common choice.
|
|
370
|
+
|
|
371
|
+
---
|
|
372
|
+
|
|
373
|
+
## Logging
|
|
374
|
+
|
|
375
|
+
On by default. MelonDB logs to the console (prefixed `[🍈 MelonDB]`) when `getRowById` fails and when `resetDatabase` completes. To turn that off:
|
|
376
|
+
|
|
377
|
+
```ts
|
|
378
|
+
createMelon({
|
|
379
|
+
// ...schema
|
|
380
|
+
logging: false,
|
|
381
|
+
});
|
|
382
|
+
```
|
|
383
|
+
|
|
384
|
+
`logRowCount` and `logAllRows` always print, since calling them is an explicit request for output.
|
|
385
|
+
|
|
386
|
+
---
|
|
387
|
+
|
|
388
|
+
## Exporting plain rows
|
|
389
|
+
|
|
390
|
+
When you need JSON without WatermelonDB meta (`_status`, `_changed`):
|
|
391
|
+
|
|
392
|
+
```ts
|
|
393
|
+
import { cleanRow } from "@alexmitchell/melondb";
|
|
394
|
+
|
|
395
|
+
const plain = cleanRow()(note);
|
|
396
|
+
// or merge extra fields:
|
|
397
|
+
const withUser = cleanRow({ user_id: userId })(note);
|
|
398
|
+
```
|
|
399
|
+
|
|
400
|
+
For debug logging of model instances (circular refs):
|
|
401
|
+
|
|
402
|
+
```ts
|
|
403
|
+
import { sanitizeCircular } from "@alexmitchell/melondb";
|
|
404
|
+
|
|
405
|
+
console.log(JSON.stringify(sanitizeCircular(note)));
|
|
406
|
+
```
|
|
407
|
+
|
|
408
|
+
---
|
|
409
|
+
|
|
410
|
+
## API summary
|
|
411
|
+
|
|
412
|
+
| Method | Purpose |
|
|
413
|
+
| --------------------------------------- | --------------------------- |
|
|
414
|
+
| `melon.db` | Raw WatermelonDB `Database` |
|
|
415
|
+
| `melon.get(table)` | Typed `Collection` |
|
|
416
|
+
| `melon.getRowById(table, id)` | Fetch one or `null` |
|
|
417
|
+
| `melon.insert(table, data)` | Create, return id |
|
|
418
|
+
| `melon.insertBatch(table, rows)` | Batch create |
|
|
419
|
+
| `melon.updateRow(model, fn)` | Update with updater |
|
|
420
|
+
| `melon.updateColumn(model, key, value)` | Single-field update |
|
|
421
|
+
| `melon.updateColumns(model, cols)` | Multi-field update |
|
|
422
|
+
| `melon.deleteRow(model)` | Soft-delete |
|
|
423
|
+
| `melon.deleteRowById(table, id)` | Soft-delete by id |
|
|
424
|
+
| `melon.deleteBatch(models)` | Soft-delete many |
|
|
425
|
+
| `melon.deleteTable(table)` | Soft-delete all rows |
|
|
426
|
+
| `melon.emptyTable(table)` | Permanent destroy all |
|
|
427
|
+
| `melon.getRowCount(table)` | Count |
|
|
428
|
+
| `melon.logRowCount` / `logAllRows` | Debug helpers |
|
|
429
|
+
| `melon.enhance(hoc, Component)` | Typed `withObservables` |
|
|
430
|
+
| `melon.resetDatabase()` | Safe reset |
|
|
431
|
+
| `melon.getDatabaseAvailable()` | Current availability flag |
|
|
432
|
+
| `melon.subscribeDatabaseAvailable(fn)` | Subscribe to that flag |
|
|
433
|
+
|
|
434
|
+
Also exported: `createWatermelon`, `createAdapter`, `cleanRow`, `sanitizeCircular`.
|
|
435
|
+
|
|
436
|
+
---
|
|
437
|
+
|
|
438
|
+
## Type parameters
|
|
439
|
+
|
|
440
|
+
```ts
|
|
441
|
+
createMelon<TableModels, TableData>({ ... })
|
|
442
|
+
```
|
|
443
|
+
|
|
444
|
+
- `**TableModels**`: map table name → Model class instance type
|
|
445
|
+
- `**TableData**`: map table name → plain object shape for `insert` / `insertBatch`
|
|
446
|
+
|
|
447
|
+
Keep those two maps in sync with your schema and you get end-to-end typing for table names and insert payloads.
|
|
448
|
+
|
|
449
|
+
---
|
|
450
|
+
|
|
451
|
+
## What this is not
|
|
452
|
+
|
|
453
|
+
- Not a sync engine. No pull/push, no backend protocol.
|
|
454
|
+
- Not a replacement for WatermelonDB models, schemas, migrations, or queries.
|
|
455
|
+
- Not a full ORM. Prefer `melon.get(table).query(...)` when you need WatermelonDB's query API.
|
|
456
|
+
|
|
457
|
+
---
|
|
458
|
+
|
|
459
|
+
## Tips
|
|
460
|
+
|
|
461
|
+
### UUID v7 ids
|
|
462
|
+
|
|
463
|
+
WatermelonDB's default ids are short random strings. If you sync with a backend that uses UUID primary keys, time-ordered UUID v7 ids are a good fit: they sort by creation time and index well in Postgres.
|
|
464
|
+
|
|
465
|
+
```bash
|
|
466
|
+
npm install uuid react-native-get-random-values
|
|
467
|
+
```
|
|
468
|
+
|
|
469
|
+
```ts
|
|
470
|
+
import "react-native-get-random-values"; // must run before uuid, polyfills crypto.getRandomValues on React Native
|
|
471
|
+
import { v7 as uuidv7 } from "uuid";
|
|
472
|
+
import { createMelon } from "@alexmitchell/melondb";
|
|
473
|
+
|
|
474
|
+
export const melon = createMelon<TableModels, TableData>({
|
|
475
|
+
// ...schema
|
|
476
|
+
idGenerator: () => uuidv7(),
|
|
477
|
+
});
|
|
478
|
+
```
|
|
479
|
+
|
|
480
|
+
On web you can skip `react-native-get-random-values`, because browsers already provide `crypto.getRandomValues`.
|
|
481
|
+
|
|
482
|
+
---
|
|
483
|
+
|
|
484
|
+
## License
|
|
485
|
+
|
|
486
|
+
MIT
|
|
@@ -0,0 +1,11 @@
|
|
|
1
|
+
export type DirtyRaw = {
|
|
2
|
+
[key: string]: any;
|
|
3
|
+
};
|
|
4
|
+
/**
|
|
5
|
+
* Build a mapper that strips WatermelonDB meta fields (`_status`, `_changed`)
|
|
6
|
+
* from a model's `_raw` and optionally merges extra fields.
|
|
7
|
+
*
|
|
8
|
+
* Useful when exporting rows to JSON, PDF, or a remote API.
|
|
9
|
+
*/
|
|
10
|
+
export declare const cleanRow: (changes?: DirtyRaw) => (row?: DirtyRaw) => DirtyRaw | undefined;
|
|
11
|
+
//# sourceMappingURL=cleanRow.d.ts.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"cleanRow.d.ts","sourceRoot":"","sources":["../src/cleanRow.ts"],"names":[],"mappings":"AAAA,MAAM,MAAM,QAAQ,GAAG;IAErB,CAAC,GAAG,EAAE,MAAM,GAAG,GAAG,CAAA;CACnB,CAAA;AAED;;;;;GAKG;AACH,eAAO,MAAM,QAAQ,GAAI,UAAS,QAAa,MACrC,MAAM,QAAQ,KAAG,QAAQ,GAAG,SASrC,CAAA"}
|
package/lib/cleanRow.js
ADDED
|
@@ -0,0 +1,20 @@
|
|
|
1
|
+
"use strict";
|
|
2
|
+
Object.defineProperty(exports, "__esModule", { value: true });
|
|
3
|
+
exports.cleanRow = void 0;
|
|
4
|
+
/**
|
|
5
|
+
* Build a mapper that strips WatermelonDB meta fields (`_status`, `_changed`)
|
|
6
|
+
* from a model's `_raw` and optionally merges extra fields.
|
|
7
|
+
*
|
|
8
|
+
* Useful when exporting rows to JSON, PDF, or a remote API.
|
|
9
|
+
*/
|
|
10
|
+
const cleanRow = (changes = {}) => {
|
|
11
|
+
return (row) => {
|
|
12
|
+
if (!row) {
|
|
13
|
+
return undefined;
|
|
14
|
+
}
|
|
15
|
+
const { _status, _changed, ...rest } = row._raw;
|
|
16
|
+
return { ...rest, ...changes };
|
|
17
|
+
};
|
|
18
|
+
};
|
|
19
|
+
exports.cleanRow = cleanRow;
|
|
20
|
+
//# sourceMappingURL=cleanRow.js.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"cleanRow.js","sourceRoot":"","sources":["../src/cleanRow.ts"],"names":[],"mappings":";;;AAKA;;;;;GAKG;AACI,MAAM,QAAQ,GAAG,CAAC,UAAoB,EAAE,EAAE,EAAE;IACjD,OAAO,CAAC,GAAc,EAAwB,EAAE;QAC9C,IAAI,CAAC,GAAG,EAAE,CAAC;YACT,OAAO,SAAS,CAAA;QAClB,CAAC;QAED,MAAM,EAAE,OAAO,EAAE,QAAQ,EAAE,GAAG,IAAI,EAAE,GAAG,GAAG,CAAC,IAAI,CAAA;QAE/C,OAAO,EAAE,GAAG,IAAI,EAAE,GAAG,OAAO,EAAE,CAAA;IAChC,CAAC,CAAA;AACH,CAAC,CAAA;AAVY,QAAA,QAAQ,YAUpB"}
|
|
@@ -0,0 +1,5 @@
|
|
|
1
|
+
import SQLiteAdapter from "@nozbe/watermelondb/adapters/sqlite";
|
|
2
|
+
import { SQLiteAdapterOptions } from "@nozbe/watermelondb/adapters/sqlite/type";
|
|
3
|
+
/** Native (iOS / Android) SQLite adapter. */
|
|
4
|
+
export declare const createAdapter: (options: SQLiteAdapterOptions) => SQLiteAdapter;
|
|
5
|
+
//# sourceMappingURL=createAdapter.d.ts.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"createAdapter.d.ts","sourceRoot":"","sources":["../src/createAdapter.ts"],"names":[],"mappings":"AAAA,OAAO,aAAa,MAAM,qCAAqC,CAAC;AAChE,OAAO,EAAE,oBAAoB,EAAE,MAAM,0CAA0C,CAAC;AAEhF,6CAA6C;AAC7C,eAAO,MAAM,aAAa,GAAI,SAAS,oBAAoB,kBAE1D,CAAC"}
|
|
@@ -0,0 +1,13 @@
|
|
|
1
|
+
"use strict";
|
|
2
|
+
var __importDefault = (this && this.__importDefault) || function (mod) {
|
|
3
|
+
return (mod && mod.__esModule) ? mod : { "default": mod };
|
|
4
|
+
};
|
|
5
|
+
Object.defineProperty(exports, "__esModule", { value: true });
|
|
6
|
+
exports.createAdapter = void 0;
|
|
7
|
+
const sqlite_1 = __importDefault(require("@nozbe/watermelondb/adapters/sqlite"));
|
|
8
|
+
/** Native (iOS / Android) SQLite adapter. */
|
|
9
|
+
const createAdapter = (options) => {
|
|
10
|
+
return new sqlite_1.default(options);
|
|
11
|
+
};
|
|
12
|
+
exports.createAdapter = createAdapter;
|
|
13
|
+
//# sourceMappingURL=createAdapter.js.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"createAdapter.js","sourceRoot":"","sources":["../src/createAdapter.ts"],"names":[],"mappings":";;;;;;AAAA,iFAAgE;AAGhE,6CAA6C;AACtC,MAAM,aAAa,GAAG,CAAC,OAA6B,EAAE,EAAE;IAC7D,OAAO,IAAI,gBAAa,CAAC,OAAO,CAAC,CAAC;AACpC,CAAC,CAAC;AAFW,QAAA,aAAa,iBAExB"}
|
|
@@ -0,0 +1,4 @@
|
|
|
1
|
+
import LokiJSAdapter, { LokiAdapterOptions } from '@nozbe/watermelondb/adapters/lokijs';
|
|
2
|
+
/** Web adapter (LokiJS + IndexedDB). Metro/webpack resolve this via `.web.ts`. */
|
|
3
|
+
export declare const createAdapter: (options: LokiAdapterOptions) => LokiJSAdapter;
|
|
4
|
+
//# sourceMappingURL=createAdapter.web.d.ts.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"createAdapter.web.d.ts","sourceRoot":"","sources":["../src/createAdapter.web.ts"],"names":[],"mappings":"AAAA,OAAO,aAAa,EAAE,EACpB,kBAAkB,EACnB,MAAM,qCAAqC,CAAA;AAE5C,kFAAkF;AAClF,eAAO,MAAM,aAAa,GAAI,SAAS,kBAAkB,kBAMxD,CAAA"}
|
|
@@ -0,0 +1,17 @@
|
|
|
1
|
+
"use strict";
|
|
2
|
+
var __importDefault = (this && this.__importDefault) || function (mod) {
|
|
3
|
+
return (mod && mod.__esModule) ? mod : { "default": mod };
|
|
4
|
+
};
|
|
5
|
+
Object.defineProperty(exports, "__esModule", { value: true });
|
|
6
|
+
exports.createAdapter = void 0;
|
|
7
|
+
const lokijs_1 = __importDefault(require("@nozbe/watermelondb/adapters/lokijs"));
|
|
8
|
+
/** Web adapter (LokiJS + IndexedDB). Metro/webpack resolve this via `.web.ts`. */
|
|
9
|
+
const createAdapter = (options) => {
|
|
10
|
+
return new lokijs_1.default({
|
|
11
|
+
useWebWorker: false,
|
|
12
|
+
useIncrementalIndexedDB: true,
|
|
13
|
+
...options,
|
|
14
|
+
});
|
|
15
|
+
};
|
|
16
|
+
exports.createAdapter = createAdapter;
|
|
17
|
+
//# sourceMappingURL=createAdapter.web.js.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"createAdapter.web.js","sourceRoot":"","sources":["../src/createAdapter.web.ts"],"names":[],"mappings":";;;;;;AAAA,iFAE4C;AAE5C,kFAAkF;AAC3E,MAAM,aAAa,GAAG,CAAC,OAA2B,EAAE,EAAE;IAC3D,OAAO,IAAI,gBAAa,CAAC;QACvB,YAAY,EAAE,KAAK;QACnB,uBAAuB,EAAE,IAAI;QAC7B,GAAG,OAAO;KACX,CAAC,CAAA;AACJ,CAAC,CAAA;AANY,QAAA,aAAa,iBAMzB"}
|
|
@@ -0,0 +1,37 @@
|
|
|
1
|
+
import { ComponentType } from "react";
|
|
2
|
+
import { Collection, Model } from "@nozbe/watermelondb";
|
|
3
|
+
import { WatermelonParams } from "./createWatermelon";
|
|
4
|
+
export type Melon = ReturnType<typeof createMelon>;
|
|
5
|
+
export type MelonParams = WatermelonParams & {
|
|
6
|
+
/** Console logging for failed lookups and database resets. Defaults to `true`. */
|
|
7
|
+
logging?: boolean;
|
|
8
|
+
/** Replaces WatermelonDB's global id generator. Defaults to WatermelonDB's own generator. */
|
|
9
|
+
idGenerator?: () => string;
|
|
10
|
+
};
|
|
11
|
+
export declare const createMelon: <TableModels extends Record<string, Model>, TableData extends Record<keyof TableModels, unknown>>(params: MelonParams) => {
|
|
12
|
+
/** Underlying WatermelonDB `Database` instance. */
|
|
13
|
+
db: import("@nozbe/watermelondb").Database;
|
|
14
|
+
enhance: <Injected, Query = unknown, Props = unknown>(enhancer: (Component: ComponentType<Injected & Props>) => ComponentType<Query & Props>, Component: ComponentType<Injected & Props>) => ComponentType<Query & Props>;
|
|
15
|
+
get: <T extends Extract<keyof TableModels, string>>(tableName: T) => Collection<TableModels[T]>;
|
|
16
|
+
getRowById: <T extends Extract<keyof TableModels, string>>(tableName: T, id: string) => Promise<TableModels[T] | null>;
|
|
17
|
+
insert: <T extends Extract<keyof TableModels, string>>(tableName: T, data: TableData[T]) => Promise<string>;
|
|
18
|
+
insertBatch: <T extends Extract<keyof TableModels, string>>(tableName: T, data: TableData[T][]) => Promise<void>;
|
|
19
|
+
updateRow: <T extends Extract<keyof TableModels, string>>(model: TableModels[T], updater: (row: TableModels[T]) => void) => Promise<TableModels[T]>;
|
|
20
|
+
updateColumn: <M extends Model, P extends keyof M, V extends M[P]>(model: M, property: P, value: V) => Promise<M>;
|
|
21
|
+
updateColumns: <M extends Model, P_1 extends keyof M, V_1 extends M[P_1]>(model: M, columns: {
|
|
22
|
+
property: P_1;
|
|
23
|
+
value: V_1;
|
|
24
|
+
}[]) => Promise<M>;
|
|
25
|
+
deleteRow: <T extends Model>(row: T) => Promise<void>;
|
|
26
|
+
deleteRowById: <T extends Extract<keyof TableModels, string>>(tableName: T, id: string | undefined) => Promise<void>;
|
|
27
|
+
deleteBatch: <T extends Model>(rows: T[]) => Promise<void>;
|
|
28
|
+
deleteTable: <T extends Extract<keyof TableModels, string>>(tableName: T) => Promise<void>;
|
|
29
|
+
emptyTable: <T extends Extract<keyof TableModels, string>>(tableName: T, batchSize?: number) => Promise<void>;
|
|
30
|
+
getRowCount: (tableName: Extract<keyof TableModels, string>) => Promise<number>;
|
|
31
|
+
logRowCount: (tableName: Extract<keyof TableModels, string>) => Promise<void>;
|
|
32
|
+
logAllRows: (tableName: Extract<keyof TableModels, string>) => Promise<void>;
|
|
33
|
+
resetDatabase: () => Promise<void>;
|
|
34
|
+
getDatabaseAvailable: () => boolean;
|
|
35
|
+
subscribeDatabaseAvailable: (listener: (available: boolean) => void) => () => void;
|
|
36
|
+
};
|
|
37
|
+
//# sourceMappingURL=createMelon.d.ts.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"createMelon.d.ts","sourceRoot":"","sources":["../src/createMelon.ts"],"names":[],"mappings":"AAAA,OAAO,EAAE,aAAa,EAAE,MAAM,OAAO,CAAC;AACtC,OAAO,EAAE,UAAU,EAAE,KAAK,EAAK,MAAM,qBAAqB,CAAC;AAE3D,OAAO,EAAoB,gBAAgB,EAAE,MAAM,oBAAoB,CAAC;AAGxE,MAAM,MAAM,KAAK,GAAG,UAAU,CAAC,OAAO,WAAW,CAAC,CAAC;AAEnD,MAAM,MAAM,WAAW,GAAG,gBAAgB,GAAG;IAC3C,kFAAkF;IAClF,OAAO,CAAC,EAAE,OAAO,CAAC;IAClB,6FAA6F;IAC7F,WAAW,CAAC,EAAE,MAAM,MAAM,CAAC;CAC5B,CAAC;AAEF,eAAO,MAAM,WAAW,GACtB,WAAW,SAAS,MAAM,CAAC,MAAM,EAAE,KAAK,CAAC,EACzC,SAAS,SAAS,MAAM,CAAC,MAAM,WAAW,EAAE,OAAO,CAAC,EAEpD,QAAQ,WAAW;IAgQjB,mDAAmD;;cA9OpC,QAAQ,EAAE,KAAK,YAAY,KAAK,sBACrC,CACR,SAAS,EAAE,aAAa,CAAC,QAAQ,GAAG,KAAK,CAAC,KACvC,aAAa,CAAC,KAAK,GAAG,KAAK,CAAC,aACtB,aAAa,CAAC,QAAQ,GAAG,KAAK,CAAC,KACzC,aAAa,CAAC,KAAK,GAAG,KAAK,CAAC;UAIlB,CAAC,wDAAgC,CAAC,KACE,UAAU,CAAC,WAAW,CAAC,CAAC,CAAC,CAAC;iBAGjD,CAAC,wDAAgC,CAAC,MAAM,MAAM;aAYlD,CAAC,wDACV,CAAC,QACN,SAAS,CAAC,CAAC,CAAC,KACjB,OAAO,CAAC,MAAM,CAAC;kBAUS,CAAC,wDACf,CAAC,QACN,SAAS,CAAC,CAAC,CAAC,EAAE;gBAaG,CAAC,oDACjB,WAAW,CAAC,CAAC,CAAC,WACZ,CAAC,GAAG,EAAE,WAAW,CAAC,CAAC,CAAC,KAAK,IAAI;mBASlB,CAAC,SAAS,KAAK,EAAE,CAAC,SAAS,MAAM,CAAC,EAAE,CAAC,SAAS,CAAC,CAAC,CAAC,CAAC,SAC/D,CAAC,YACE,CAAC,SACJ,CAAC;oBAgBa,CAAC,SAAS,KAAK,cAAY,MAAM,CAAC,cAAY,CAAC,CAAC,GAAC,CAAC,SAChE,CAAC,WACC;QAAE,QAAQ,EAAE,GAAC,CAAC;QAAC,KAAK,EAAE,GAAC,CAAA;KAAE,EAAE;gBAcb,CAAC,SAAS,KAAK,OAAO,CAAC;oBAMnB,CAAC,wDACjB,CAAC,MACR,MAAM,GAAG,SAAS;kBAYG,CAAC,SAAS,KAAK,QAAQ,CAAC,EAAE;kBAe1B,CAAC,wDAAgC,CAAC;iBAkBnC,CAAC,wDACd,CAAC;;;;;;2CA0CF,CAAC,SAAS,EAAE,OAAO,KAAK,IAAI;CAiEzC,CAAC"}
|