1import { IndexedDb, IndexedDbDatabase, IndexedDbTable, IndexedDbVersion } from "@effect/platform-browser"
2import { afterEach, assert, describe, it } from "@effect/vitest"
3import {
4 Array,
5 Cause,
6 Context,
7 DateTime,
8 Effect,
9 Fiber,
10 Layer,
11 Schema,
12 SchemaGetter,
13 SchemaIssue,
14 Stream
15} from "effect"
16import { IDBKeyRange, indexedDB } from "fake-indexeddb"
17
18const databaseName = "db"
19
20const layerFakeIndexedDb = Layer.succeed(
21 IndexedDb.IndexedDb,
22 IndexedDb.make({ indexedDB, IDBKeyRange })
23)
24
25const provideDb = (database: IndexedDbDatabase.Any) =>
26 Effect.provide(
27 database.layer(databaseName).pipe(Layer.provide(layerFakeIndexedDb))
28 )
29
30afterEach(() => {
31 indexedDB.deleteDatabase(databaseName)
32})
33
34class Table1 extends IndexedDbTable.make({
35 name: "todo",
36 schema: Schema.Struct({
37 id: Schema.Number,
38 title: Schema.String,
39 count: Schema.Number,
40 completed: Schema.Boolean
41 }),
42 keyPath: "id",
43 indexes: { titleIndex: "title", countIndex: "count", titleCount: ["title", "count"] }
44}) {}
45
46class User extends Schema.Class<User>("User")({
47 id: Schema.Number,
48 name: Schema.String,
49 email: Schema.String,
50 createdAt: Schema.DateTimeUtcFromMillis
51}) {}
52
53class ProductSchema extends Schema.Class<ProductSchema>("ProductSchema")({
54 key: IndexedDb.AutoIncrement,
55 name: Schema.String,
56 price: Schema.Number
57}) {}
58
59class VerifyContext extends Context.Service<
60 VerifyContext,
61 { readonly maxLength: number }
62>()("VerifyContext") {}
63
64const VerifyId = Schema.String.pipe(
65 Schema.decodeTo(Schema.String, {
66 encode: SchemaGetter.transformEffect((s) =>
67 Effect.gen(function*() {
68 const { maxLength } = yield* VerifyContext
69 if (s.length > maxLength) {
70 return yield* Effect.fail(
71 new SchemaIssue.InvalidValue({
72 message: "Max length exceeded"
73 })
74 )
75 }
76 return s
77 })
78 ),
79 decode: SchemaGetter.String()
80 })
81)
82
83const Table2 = IndexedDbTable.make({
84 name: "user",
85 schema: User,
86 keyPath: "id"
87})
88
89const Table3 = IndexedDbTable.make({
90 name: "product",
91 schema: ProductSchema,
92 keyPath: "price"
93})
94
95const Table4 = IndexedDbTable.make({
96 name: "price",
97 schema: Schema.Struct({
98 id: IndexedDb.AutoIncrement,
99 amount: Schema.Number
100 }),
101 keyPath: "id",
102 autoIncrement: true
103})
104
105const Table5 = IndexedDbTable.make({
106 name: "person",
107 schema: Schema.Struct({
108 firstName: Schema.String,
109 lastName: Schema.String,
110 age: Schema.Number
111 }),
112 keyPath: ["firstName", "lastName"]
113})
114
115const Table6 = IndexedDbTable.make({
116 name: "user-verify",
117 schema: Schema.Struct({ id: VerifyId }),
118 keyPath: "id"
119})
120
121const Table7 = IndexedDbTable.make({
122 name: "no-keypath",
123 schema: Schema.Struct({ username: Schema.String, index: Schema.Number }),
124 indexes: { usernameIndex: "username" }
125})
126
127class V1 extends IndexedDbVersion.make(
128 Table1,
129 Table2,
130 Table3,
131 Table4,
132 Table5,
133 Table6,
134 Table7
135) {}
136
137describe("IndexedDbQueryBuilder", { concurrent: false }, () => {
138 describe("select", () => {
139 it.effect("select", () => {
140 class Db extends IndexedDbDatabase.make(
141 V1,
142 Effect.fn(function*(api) {
143 yield* api.createObjectStore("todo")
144 yield* api.createIndex("todo", "titleIndex")
145 yield* api.createIndex("todo", "countIndex")
146 yield* api
147 .from("todo")
148 .insert({ id: 1, title: "test", count: 1, completed: false })
149 })
150 ) {}
151
152 return Effect.gen(function*() {
153 const api = yield* Db.getQueryBuilder
154 const from = api.from("todo")
155 const select = from.select()
156 const data = yield* select
157
158 assert.equal(from.table.tableName, "todo")
159 assert.equal(select.index, undefined)
160 assert.deepStrictEqual(select.from, from)
161 assert.deepStrictEqual(select.from.IDBKeyRange, IDBKeyRange)
162 assert.equal(data.length, 1)
163 assert.deepStrictEqual(data, [
164 { id: 1, title: "test", count: 1, completed: false }
165 ])
166 }).pipe(provideDb(Db))
167 })
168
169 it.effect("select with index", () => {
170 class Db extends IndexedDbDatabase.make(
171 V1,
172 Effect.fn(function*(api) {
173 yield* api.createObjectStore("todo")
174 yield* api.createIndex("todo", "titleIndex")
175 yield* api.createIndex("todo", "countIndex")
176 yield* api
177 .from("todo")
178 .insert({ id: 2, title: "test2", count: 2, completed: false })
179 })
180 ) {}
181
182 return Effect.gen(function*() {
183 const api = yield* Db
184 const from = api.from("todo")
185 const select = from.select("titleIndex")
186 const data = yield* select
187
188 assert.equal(from.table.tableName, "todo")
189 assert.equal(select.index, "titleIndex")
190 assert.deepStrictEqual(select.from, from)
191 assert.deepStrictEqual(select.from.IDBKeyRange, IDBKeyRange)
192 assert.equal(data.length, 1)
193 assert.deepStrictEqual(data, [
194 { id: 2, title: "test2", count: 2, completed: false }
195 ])
196 }).pipe(provideDb(Db))
197 })
198
199 it.effect("select equals", () => {
200 class Db extends IndexedDbDatabase.make(
201 V1,
202 Effect.fn(function*(api) {
203 yield* api.createObjectStore("todo")
204 yield* api.createIndex("todo", "titleIndex")
205 yield* api.createIndex("todo", "countIndex")
206 yield* api.from("todo").insertAll([
207 { id: 1, title: "test1", count: 1, completed: false },
208 { id: 2, title: "test2", count: 2, completed: false },
209 { id: 3, title: "test3", count: 3, completed: false }
210 ])
211 })
212 ) {}
213
214 return Effect.gen(function*() {
215 {
216 const api = yield* Db
217 const from = api.from("todo")
218 const select = from.select()
219 const equals = select.equals(2)
220 const data = yield* equals
221
222 assert.equal(from.table.tableName, "todo")
223 assert.equal(equals.index, undefined)
224 assert.deepStrictEqual(equals.from, from)
225 assert.deepStrictEqual(equals.from.IDBKeyRange, IDBKeyRange)
226 assert.equal(equals.only, 2)
227 assert.equal(data.length, 1)
228 assert.deepStrictEqual(data, [
229 { id: 2, title: "test2", count: 2, completed: false }
230 ])
231 }
232 }).pipe(provideDb(Db))
233 })
234
235 it.effect("select equals in no keypath table", () => {
236 class Db extends IndexedDbDatabase.make(
237 V1,
238 Effect.fn(function*(api) {
239 yield* api.createObjectStore("no-keypath")
240 yield* api
241 .from("no-keypath")
242 .insert({ username: "user", index: 1, key: 5 })
243 })
244 ) {}
245
246 return Effect.gen(function*() {
247 const api = yield* Db
248 const data = yield* api.from("no-keypath").select().equals(5)
249
250 assert.deepStrictEqual(data, [
251 { username: "user", index: 1, key: 5 }
252 ])
253 }).pipe(provideDb(Db))
254 })
255
256 it.effect("select first preserves the primary key in no keypath table", () => {
257 class Db extends IndexedDbDatabase.make(
258 V1,
259 Effect.fn(function*(api) {
260 yield* api.createObjectStore("no-keypath")
261 yield* api.createIndex("no-keypath", "usernameIndex")
262 yield* api.from("no-keypath").insertAll([
263 { key: 7, username: "user", index: 1 },
264 { key: 9, username: "user", index: 2 }
265 ])
266 })
267 ) {}
268
269 return Effect.gen(function*() {
270 const api = yield* Db
271 const data = yield* api.from("no-keypath").select("usernameIndex").equals("user").first()
272
273 assert.deepStrictEqual(data, { key: 7, username: "user", index: 1 })
274 }).pipe(provideDb(Db))
275 })
276
277 it.effect("select preserves primary keys in no keypath table", () => {
278 class Db extends IndexedDbDatabase.make(
279 V1,
280 Effect.fn(function*(api) {
281 yield* api.createObjectStore("no-keypath")
282 yield* api.createIndex("no-keypath", "usernameIndex")
283 yield* api.from("no-keypath").insertAll([
284 { key: 7, username: "user", index: 1 },
285 { key: 9, username: "user", index: 2 }
286 ])
287 })
288 ) {}
289
290 return Effect.gen(function*() {
291 const api = yield* Db
292 const data = yield* api.from("no-keypath").select("usernameIndex").equals("user")
293
294 assert.deepStrictEqual(data, [
295 { key: 7, username: "user", index: 1 },
296 { key: 9, username: "user", index: 2 }
297 ])
298 }).pipe(provideDb(Db))
299 })
300
301 it.effect("select equals with index", () => {
302 class Db extends IndexedDbDatabase.make(
303 V1,
304 Effect.fn(function*(api) {
305 yield* api.createObjectStore("todo")
306 yield* api.createIndex("todo", "titleIndex")
307 yield* api.createIndex("todo", "countIndex")
308 yield* api.from("todo").insertAll([
309 { id: 1, title: "test1", count: 1, completed: false },
310 { id: 2, title: "test2", count: 2, completed: false },
311 { id: 3, title: "test3", count: 3, completed: false }
312 ])
313 })
314 ) {}
315
316 return Effect.gen(function*() {
317 {
318 const api = yield* Db
319 const from = api.from("todo")
320 const select = from.select("titleIndex")
321 const equals = select.equals("test3")
322 const data = yield* equals
323
324 assert.equal(from.table.tableName, "todo")
325 assert.equal(equals.index, "titleIndex")
326 assert.deepStrictEqual(equals.from, from)
327 assert.deepStrictEqual(equals.from.IDBKeyRange, IDBKeyRange)
328 assert.equal(equals.only, "test3")
329 assert.equal(data.length, 1)
330 assert.deepStrictEqual(data, [
331 { id: 3, title: "test3", count: 3, completed: false }
332 ])
333 }
334 }).pipe(provideDb(Db))
335 })
336
337 it.effect("select gte", () => {
338 class Db extends IndexedDbDatabase.make(
339 V1,
340 Effect.fn(function*(api) {
341 yield* api.createObjectStore("todo")
342 yield* api.createIndex("todo", "titleIndex")
343 yield* api.createIndex("todo", "countIndex")
344 yield* api.from("todo").insertAll([
345 { id: 1, title: "test1", count: 1, completed: false },
346 { id: 2, title: "test2", count: 2, completed: false },
347 { id: 3, title: "test3", count: 3, completed: false }
348 ])
349 })
350 ) {}
351
352 return Effect.gen(function*() {
353 const api = yield* Db.getQueryBuilder
354 const data = yield* api.from("todo").select().gte(2)
355
356 assert.equal(data.length, 2)
357 assert.deepStrictEqual(data, [
358 { id: 2, title: "test2", count: 2, completed: false },
359 { id: 3, title: "test3", count: 3, completed: false }
360 ])
361 }).pipe(provideDb(Db))
362 })
363
364 it.effect("select gte with index", () => {
365 class Db extends IndexedDbDatabase.make(
366 V1,
367 Effect.fn(function*(api) {
368 yield* api.createObjectStore("todo")
369 yield* api.createIndex("todo", "titleIndex")
370 yield* api.createIndex("todo", "countIndex")
371 yield* api.from("todo").insertAll([
372 { id: 1, title: "test1", count: 1, completed: false },
373 { id: 2, title: "test2", count: 2, completed: false },
374 { id: 3, title: "test3", count: 3, completed: false }
375 ])
376 })
377 ) {}
378
379 return Effect.gen(function*() {
380 const api = yield* Db.getQueryBuilder
381 const data = yield* api.from("todo").select("countIndex").gte(3)
382
383 assert.equal(data.length, 1)
384 assert.deepStrictEqual(data, [
385 { id: 3, title: "test3", count: 3, completed: false }
386 ])
387 }).pipe(provideDb(Db))
388 })
389
390 it.effect("select lte", () => {
391 class Db extends IndexedDbDatabase.make(
392 V1,
393 Effect.fn(function*(api) {
394 yield* api.createObjectStore("todo")
395 yield* api.createIndex("todo", "titleIndex")
396 yield* api.createIndex("todo", "countIndex")
397 yield* api.from("todo").insertAll([
398 { id: 1, title: "test1", count: 1, completed: false },
399 { id: 2, title: "test2", count: 2, completed: false },
400 { id: 3, title: "test3", count: 3, completed: false }
401 ])
402 })
403 ) {}
404
405 return Effect.gen(function*() {
406 {
407 const api = yield* Db.getQueryBuilder
408 const data = yield* api.from("todo").select().lte(2)
409
410 assert.equal(data.length, 2)
411 assert.deepStrictEqual(data, [
412 { id: 1, title: "test1", count: 1, completed: false },
413 { id: 2, title: "test2", count: 2, completed: false }
414 ])
415 }
416 }).pipe(provideDb(Db))
417 })
418
419 it.effect("select gt", () => {
420 class Db extends IndexedDbDatabase.make(
421 V1,
422 Effect.fn(function*(api) {
423 yield* api.createObjectStore("todo")
424 yield* api.createIndex("todo", "titleIndex")
425 yield* api.createIndex("todo", "countIndex")
426 yield* api.from("todo").insertAll([
427 { id: 1, title: "test1", count: 1, completed: false },
428 { id: 2, title: "test2", count: 2, completed: false },
429 { id: 3, title: "test3", count: 3, completed: false }
430 ])
431 })
432 ) {}
433
434 return Effect.gen(function*() {
435 const api = yield* Db.getQueryBuilder
436 const data = yield* api.from("todo").select().gt(2)
437
438 assert.equal(data.length, 1)
439 assert.deepStrictEqual(data, [
440 { id: 3, title: "test3", count: 3, completed: false }
441 ])
442 }).pipe(provideDb(Db))
443 })
444
445 it.effect("select lt", () => {
446 class Db extends IndexedDbDatabase.make(
447 V1,
448 Effect.fn(function*(api) {
449 yield* api.createObjectStore("todo")
450 yield* api.createIndex("todo", "titleIndex")
451 yield* api.createIndex("todo", "countIndex")
452 yield* api.from("todo").insertAll([
453 { id: 1, title: "test1", count: 1, completed: false },
454 { id: 2, title: "test2", count: 2, completed: false },
455 { id: 3, title: "test3", count: 3, completed: false }
456 ])
457 })
458 ) {}
459
460 return Effect.gen(function*() {
461 const api = yield* Db.getQueryBuilder
462 const data = yield* api.from("todo").select().lt(2)
463
464 assert.equal(data.length, 1)
465 assert.deepStrictEqual(data, [
466 { id: 1, title: "test1", count: 1, completed: false }
467 ])
468 }).pipe(provideDb(Db))
469 })
470
471 it.effect("select between", () => {
472 class Db extends IndexedDbDatabase.make(
473 V1,
474 Effect.fn(function*(api) {
475 yield* api.createObjectStore("todo")
476 yield* api.createIndex("todo", "titleIndex")
477 yield* api.createIndex("todo", "countIndex")
478 yield* api.from("todo").insertAll([
479 { id: 1, title: "test1", count: 1, completed: false },
480 { id: 2, title: "test2", count: 2, completed: false },
481 { id: 3, title: "test3", count: 3, completed: false },
482 { id: 4, title: "test4", count: 4, completed: false },
483 { id: 5, title: "test5", count: 5, completed: false }
484 ])
485 })
486 ) {}
487
488 return Effect.gen(function*() {
489 const api = yield* Db.getQueryBuilder
490 const data = yield* api.from("todo").select().between(2, 3)
491
492 assert.equal(data.length, 2)
493 assert.deepStrictEqual(data, [
494 { id: 2, title: "test2", count: 2, completed: false },
495 { id: 3, title: "test3", count: 3, completed: false }
496 ])
497 }).pipe(provideDb(Db))
498 })
499
500 it.effect("select between with exclude", () => {
501 class Db extends IndexedDbDatabase.make(
502 V1,
503 Effect.fn(function*(api) {
504 yield* api.createObjectStore("todo")
505 yield* api.createIndex("todo", "titleIndex")
506 yield* api.createIndex("todo", "countIndex")
507 yield* api.from("todo").insertAll([
508 { id: 1, title: "test1", count: 1, completed: false },
509 { id: 2, title: "test2", count: 2, completed: false },
510 { id: 3, title: "test3", count: 3, completed: false },
511 { id: 4, title: "test4", count: 4, completed: false },
512 { id: 5, title: "test5", count: 5, completed: false }
513 ])
514 })
515 ) {}
516
517 return Effect.gen(function*() {
518 const api = yield* Db.getQueryBuilder
519 const data = yield* api.from("todo").select().between(2, 4, {
520 excludeLowerBound: true,
521 excludeUpperBound: true
522 })
523
524 assert.equal(data.length, 1)
525 assert.deepStrictEqual(data, [
526 { id: 3, title: "test3", count: 3, completed: false }
527 ])
528 }).pipe(provideDb(Db))
529 })
530
531 it.effect("select limit", () => {
532 class Db extends IndexedDbDatabase.make(
533 V1,
534 Effect.fn(function*(api) {
535 yield* api.createObjectStore("todo")
536 yield* api.createIndex("todo", "titleIndex")
537 yield* api.createIndex("todo", "countIndex")
538 yield* api.from("todo").insertAll([
539 { id: 1, title: "test1", count: 1, completed: false },
540 { id: 2, title: "test2", count: 2, completed: false },
541 { id: 3, title: "test3", count: 3, completed: false },
542 { id: 4, title: "test4", count: 4, completed: false },
543 { id: 5, title: "test5", count: 5, completed: false }
544 ])
545 })
546 ) {}
547
548 return Effect.gen(function*() {
549 const api = yield* Db.getQueryBuilder
550 const data = yield* api.from("todo").select().limit(2)
551
552 assert.equal(data.length, 2)
553 assert.deepStrictEqual(data, [
554 { id: 1, title: "test1", count: 1, completed: false },
555 { id: 2, title: "test2", count: 2, completed: false }
556 ])
557 }).pipe(provideDb(Db))
558 })
559
560 it.effect("select offset", () => {
561 class Db extends IndexedDbDatabase.make(
562 V1,
563 Effect.fn(function*(api) {
564 yield* api.createObjectStore("todo")
565 yield* api.createIndex("todo", "titleIndex")
566 yield* api.createIndex("todo", "countIndex")
567 yield* api.from("todo").insertAll([
568 { id: 1, title: "test1", count: 1, completed: false },
569 { id: 2, title: "test2", count: 2, completed: false },
570 { id: 3, title: "test3", count: 3, completed: false },
571 { id: 4, title: "test4", count: 4, completed: false },
572 { id: 5, title: "test5", count: 5, completed: false }
573 ])
574 })
575 ) {}
576
577 return Effect.gen(function*() {
578 const api = yield* Db.getQueryBuilder
579 const data = yield* api.from("todo").select().limit(2).offset(2)
580
581 assert.equal(data.length, 2)
582 assert.deepStrictEqual(data, [
583 { id: 3, title: "test3", count: 3, completed: false },
584 { id: 4, title: "test4", count: 4, completed: false }
585 ])
586 }).pipe(provideDb(Db))
587 })
588
589 it.effect("select limit with filters", () => {
590 class Db extends IndexedDbDatabase.make(
591 V1,
592 Effect.fn(function*(api) {
593 yield* api.createObjectStore("todo")
594 yield* api.createIndex("todo", "titleIndex")
595 yield* api.createIndex("todo", "countIndex")
596 yield* api.from("todo").insertAll([
597 { id: 1, title: "test1", count: 1, completed: false },
598 { id: 2, title: "test2", count: 2, completed: false },
599 { id: 3, title: "test3", count: 3, completed: false },
600 { id: 4, title: "test4", count: 4, completed: false },
601 { id: 5, title: "test5", count: 5, completed: false }
602 ])
603 })
604 ) {}
605
606 return Effect.gen(function*() {
607 const api = yield* Db.getQueryBuilder
608 const data = yield* api
609 .from("todo")
610 .select("countIndex")
611 .gte(2)
612 .limit(2)
613
614 assert.equal(data.length, 2)
615 assert.deepStrictEqual(data, [
616 { id: 2, title: "test2", count: 2, completed: false },
617 { id: 3, title: "test3", count: 3, completed: false }
618 ])
619 }).pipe(provideDb(Db))
620 })
621
622 it.effect("select first", () => {
623 class Db extends IndexedDbDatabase.make(
624 V1,
625 Effect.fn(function*(api) {
626 yield* api.createObjectStore("todo")
627 yield* api.createIndex("todo", "titleIndex")
628 yield* api.createIndex("todo", "countIndex")
629 yield* api.from("todo").insertAll([
630 { id: 1, title: "test1", count: 1, completed: false },
631 { id: 2, title: "test2", count: 2, completed: false },
632 { id: 3, title: "test3", count: 3, completed: false }
633 ])
634 })
635 ) {}
636
637 return Effect.gen(function*() {
638 const api = yield* Db.getQueryBuilder
639 const data = yield* api.from("todo").select().first()
640
641 assert.deepStrictEqual(data, {
642 id: 1,
643 title: "test1",
644 count: 1,
645 completed: false
646 })
647 }).pipe(provideDb(Db))
648 })
649
650 it.effect("select first with filters", () => {
651 class Db extends IndexedDbDatabase.make(
652 V1,
653 Effect.fn(function*(api) {
654 yield* api.createObjectStore("todo")
655 yield* api.createIndex("todo", "titleIndex")
656 yield* api.createIndex("todo", "countIndex")
657 yield* api.from("todo").insertAll([
658 { id: 1, title: "test1", count: 1, completed: false },
659 { id: 2, title: "test2", count: 2, completed: false },
660 { id: 3, title: "test3", count: 3, completed: false }
661 ])
662 })
663 ) {}
664
665 return Effect.gen(function*() {
666 const api = yield* Db.getQueryBuilder
667 const data = yield* api
668 .from("todo")
669 .select("titleIndex")
670 .equals("test2")
671 .first()
672
673 assert.deepStrictEqual(data, {
674 id: 2,
675 title: "test2",
676 count: 2,
677 completed: false
678 })
679 }).pipe(provideDb(Db))
680 })
681 })
682
683 describe("modify", () => {
684 it.effect("insert", () => {
685 class Db extends IndexedDbDatabase.make(
686 V1,
687 Effect.fn(function*(api) {
688 yield* api.createObjectStore("todo")
689 yield* api.createIndex("todo", "titleIndex")
690 yield* api.createIndex("todo", "countIndex")
691 })
692 ) {}
693
694 return Effect.gen(function*() {
695 const api = yield* Db.getQueryBuilder
696 const addedKey = yield* api
697 .from("todo")
698 .insert({ id: 10, title: "insert1", count: 10, completed: true })
699 const data = yield* api.from("todo").select()
700
701 assert.equal(addedKey, 10)
702 assert.equal(data.length, 1)
703 assert.deepStrictEqual(data, [
704 { id: 10, title: "insert1", count: 10, completed: true }
705 ])
706 }).pipe(provideDb(Db))
707 })
708
709 it.effect("insert schema with context", () => {
710 class Db extends IndexedDbDatabase.make(
711 V1,
712 Effect.fn(function*(api) {
713 yield* api.createObjectStore("user-verify")
714 })
715 ) {}
716
717 return Effect.gen(function*() {
718 const api = yield* Db.getQueryBuilder
719 const addedKey = yield* api.from("user-verify").insert({ id: "abc" })
720 const data = yield* api.from("user-verify").select()
721
722 assert.equal(addedKey, "abc")
723 assert.equal(data.length, 1)
724 assert.deepStrictEqual(data, [{ id: "abc" }])
725 }).pipe(
726 provideDb(Db),
727 Effect.provideService(
728 VerifyContext,
729 VerifyContext.of({ maxLength: 4 })
730 )
731 )
732 })
733
734 it.effect("insert with manual key required", () => {
735 class Db extends IndexedDbDatabase.make(
736 V1,
737 Effect.fn(function*(api) {
738 yield* api.createObjectStore("todo")
739 yield* api.createObjectStore("user")
740 yield* api.createIndex("todo", "titleIndex")
741 yield* api.createIndex("todo", "countIndex")
742 })
743 ) {}
744
745 return Effect.gen(function*() {
746 const api = yield* Db.getQueryBuilder
747 const createdAt = DateTime.nowUnsafe()
748 const addedKey = yield* api.from("user").insert({
749 id: 10,
750 name: "insert1",
751 email: "insert1@example.com",
752 createdAt
753 })
754 const addedAllKeys = yield* api.from("user").insertAll([
755 {
756 id: 11,
757 name: "insert2",
758 email: "insert2@example.com",
759 createdAt
760 },
761 {
762 id: 12,
763 name: "insert3",
764 email: "insert3@example.com",
765 createdAt
766 }
767 ])
768 const data = yield* api.from("user").select()
769
770 assert.equal(addedKey, 10)
771 assert.deepStrictEqual(addedAllKeys, [11, 12])
772 assert.equal(data.length, 3)
773 assert.deepStrictEqual(data, [
774 new User({
775 id: 10,
776 name: "insert1",
777 email: "insert1@example.com",
778 createdAt
779 }),
780 new User({
781 id: 11,
782 name: "insert2",
783 email: "insert2@example.com",
784 createdAt
785 }),
786 new User({
787 id: 12,
788 name: "insert3",
789 email: "insert3@example.com",
790 createdAt
791 })
792 ])
793 }).pipe(provideDb(Db))
794 })
795
796 it.effect("insert with manual multiple keys required", () => {
797 class Db extends IndexedDbDatabase.make(
798 V1,
799 Effect.fn(function*(api) {
800 yield* api.createObjectStore("todo")
801 yield* api.createObjectStore("user")
802 yield* api.createObjectStore("person")
803 yield* api.createIndex("todo", "titleIndex")
804 yield* api.createIndex("todo", "countIndex")
805 })
806 ) {}
807
808 return Effect.gen(function*() {
809 const api = yield* Db.getQueryBuilder
810 const addedKey = yield* api.from("person").insert({
811 firstName: "John",
812 lastName: "Doe",
813 age: 30
814 })
815 const data = yield* api.from("person").select()
816
817 assert.deepStrictEqual(addedKey, ["John", "Doe"])
818 assert.equal(data.length, 1)
819 assert.deepStrictEqual(data, [
820 { firstName: "John", lastName: "Doe", age: 30 }
821 ])
822 }).pipe(provideDb(Db))
823 })
824
825 it.effect("insert with manual key optional", () => {
826 class Db extends IndexedDbDatabase.make(
827 V1,
828 Effect.fn(function*(api) {
829 yield* api.createObjectStore("todo")
830 yield* api.createObjectStore("user")
831 yield* api.createObjectStore("product")
832 yield* api.createIndex("todo", "titleIndex")
833 yield* api.createIndex("todo", "countIndex")
834 })
835 ) {}
836
837 return Effect.gen(function*() {
838 const api = yield* Db.getQueryBuilder
839 const addedKey = yield* api.from("product").insert({
840 key: 10,
841 name: "insert1",
842 price: 10
843 })
844 const data = yield* api.from("product").select()
845
846 assert.equal(addedKey, 10)
847 assert.equal(data.length, 1)
848 assert.deepStrictEqual(data, [new ProductSchema({ key: 10, name: "insert1", price: 10 })])
849 }).pipe(provideDb(Db))
850 })
851
852 it.effect("insert with auto-increment", () => {
853 class Db extends IndexedDbDatabase.make(
854 V1,
855 Effect.fn(function*(api) {
856 yield* api.createObjectStore("todo")
857 yield* api.createObjectStore("user")
858 yield* api.createObjectStore("product")
859 yield* api.createObjectStore("price")
860 yield* api.createIndex("todo", "titleIndex")
861 yield* api.createIndex("todo", "countIndex")
862 })
863 ) {}
864
865 return Effect.gen(function*() {
866 const api = yield* Db.getQueryBuilder
867 const addedKey = yield* api
868 .from("price")
869 .insertAll([{ amount: 10 }, { amount: 20, id: 10 }, { amount: 30 }])
870 const data = yield* api.from("price").select()
871
872 assert.deepStrictEqual(addedKey, [1, 10, 11])
873 assert.equal(data.length, 3)
874 assert.deepStrictEqual(data, [
875 { id: 1, amount: 10 },
876 { id: 10, amount: 20 },
877 { id: 11, amount: 30 }
878 ])
879 }).pipe(provideDb(Db))
880 })
881
882 it.effect("insert with auto-increment and get first", () => {
883 class Db extends IndexedDbDatabase.make(
884 V1,
885 Effect.fn(function*(api) {
886 yield* api.createObjectStore("todo")
887 yield* api.createObjectStore("user")
888 yield* api.createObjectStore("product")
889 yield* api.createObjectStore("price")
890 yield* api.createIndex("todo", "titleIndex")
891 yield* api.createIndex("todo", "countIndex")
892 })
893 ) {}
894
895 return Effect.gen(function*() {
896 const api = yield* Db.getQueryBuilder
897 const addedKey = yield* api.from("price").insert({ amount: 10 })
898 const data = yield* api.from("price").select().first()
899
900 assert.equal(addedKey, 1)
901 assert.deepStrictEqual(data, { id: 1, amount: 10 })
902 }).pipe(provideDb(Db))
903 })
904
905 it.effect(
906 "insert with auto-increment schema and different key path",
907 () => {
908 class Db extends IndexedDbDatabase.make(
909 V1,
910 Effect.fn(function*(api) {
911 yield* api.createObjectStore("product")
912 })
913 ) {}
914
915 return Effect.gen(function*() {
916 const api = yield* Db.getQueryBuilder
917 const addedKey = yield* api
918 .from("product")
919 .insert({ name: "insert1", price: 12, key: 10 })
920 const data = yield* api.from("product").select()
921
922 assert.equal(addedKey, 12)
923 assert.equal(data.length, 1)
924 assert.deepStrictEqual(data, [
925 new ProductSchema({ key: 10, name: "insert1", price: 12 })
926 ])
927 }).pipe(provideDb(Db))
928 }
929 )
930
931 it.effect("insert in no keypath table with manual key", () => {
932 class Db extends IndexedDbDatabase.make(
933 V1,
934 Effect.fn(function*(api) {
935 yield* api.createObjectStore("no-keypath")
936 })
937 ) {}
938
939 return Effect.gen(function*() {
940 const api = yield* Db.getQueryBuilder
941 const addedKey = yield* api
942 .from("no-keypath")
943 .insert({ username: "insert1", index: 12, key: "key" })
944 const data = yield* api.from("no-keypath").select()
945
946 assert.equal(addedKey, "key")
947 assert.equal(data.length, 1)
948 assert.deepStrictEqual(data, [
949 { username: "insert1", index: 12, key: "key" }
950 ])
951 }).pipe(provideDb(Db))
952 })
953
954 it.effect("insertAll in no keypath table with manual key", () => {
955 class Db extends IndexedDbDatabase.make(
956 V1,
957 Effect.fn(function*(api) {
958 yield* api.createObjectStore("no-keypath")
959 })
960 ) {}
961
962 return Effect.gen(function*() {
963 const api = yield* Db.getQueryBuilder
964 const addedKey = yield* api.from("no-keypath").insertAll([
965 { username: "insert1", index: 12, key: "key" },
966 {
967 username: "insert2",
968 index: 13,
969 key: "key2"
970 }
971 ])
972 const data = yield* api.from("no-keypath").select()
973
974 assert.deepStrictEqual(addedKey, ["key", "key2"])
975 assert.equal(data.length, 2)
976 assert.deepStrictEqual(data, [
977 { username: "insert1", index: 12, key: "key" },
978 {
979 username: "insert2",
980 index: 13,
981 key: "key2"
982 }
983 ])
984 }).pipe(provideDb(Db))
985 })
986
987 it.effect("insertAll in no keypath table and get first", () => {
988 class Db extends IndexedDbDatabase.make(
989 V1,
990 Effect.fn(function*(api) {
991 yield* api.createObjectStore("no-keypath")
992 })
993 ) {}
994
995 return Effect.gen(function*() {
996 const api = yield* Db.getQueryBuilder
997 const addedKey = yield* api.from("no-keypath").insertAll([
998 { username: "insert1", index: 12, key: "key" },
999 {
1000 username: "insert2",
1001 index: 13,
1002 key: "key2"
1003 }
1004 ])
1005 const data = yield* api.from("no-keypath").select().first()
1006
1007 assert.deepStrictEqual(addedKey, ["key", "key2"])
1008 assert.deepStrictEqual(data, {
1009 username: "insert1",
1010 index: 12,
1011 key: "key"
1012 })
1013 }).pipe(provideDb(Db))
1014 })
1015
1016 it.effect("insertAll in no keypath table and get limit", () => {
1017 class Db extends IndexedDbDatabase.make(
1018 V1,
1019 Effect.fn(function*(api) {
1020 yield* api.createObjectStore("no-keypath")
1021 })
1022 ) {}
1023
1024 return Effect.gen(function*() {
1025 const api = yield* Db.getQueryBuilder
1026 const addedKey = yield* api.from("no-keypath").insertAll([
1027 { username: "insert1", index: 12, key: "key" },
1028 {
1029 username: "insert2",
1030 index: 13,
1031 key: "key2"
1032 }
1033 ])
1034 const data = yield* api.from("no-keypath").select().limit(1)
1035
1036 assert.deepStrictEqual(addedKey, ["key", "key2"])
1037 assert.deepStrictEqual(data, [
1038 { username: "insert1", index: 12, key: "key" }
1039 ])
1040 }).pipe(provideDb(Db))
1041 })
1042
1043 it.effect(
1044 "insertAll in no keypath table with multiple types of keys",
1045 () => {
1046 class Db extends IndexedDbDatabase.make(
1047 V1,
1048 Effect.fn(function*(api) {
1049 yield* api.createObjectStore("no-keypath")
1050 })
1051 ) {}
1052
1053 return Effect.gen(function*() {
1054 const api = yield* Db.getQueryBuilder
1055 const date = new Date()
1056 const arrayBuffer = new ArrayBuffer(10)
1057 const addedKey = yield* api.from("no-keypath").insertAll([
1058 { username: "insert1", index: 12, key: "key" },
1059 {
1060 username: "insert2",
1061 index: 13,
1062 key: 14
1063 },
1064 {
1065 username: "insert3",
1066 index: 14,
1067 key: date
1068 },
1069 {
1070 username: "insert4",
1071 index: 15,
1072 key: arrayBuffer
1073 },
1074 {
1075 username: "insert5",
1076 index: 16,
1077 key: [1, "key", date, arrayBuffer]
1078 }
1079 ])
1080 const data = yield* api.from("no-keypath").select()
1081
1082 // Returned data order not guaranteed
1083 data.sort((a, b) => a.index - b.index)
1084
1085 assert.deepStrictEqual(addedKey, [
1086 "key",
1087 14,
1088 date,
1089 arrayBuffer,
1090 [1, "key", date, arrayBuffer]
1091 ])
1092 assert.deepStrictEqual(data, [
1093 { username: "insert1", index: 12, key: "key" },
1094 {
1095 username: "insert2",
1096 index: 13,
1097 key: 14
1098 },
1099 { username: "insert3", index: 14, key: date },
1100 { username: "insert4", index: 15, key: arrayBuffer },
1101 {
1102 username: "insert5",
1103 index: 16,
1104 key: [1, "key", date, arrayBuffer]
1105 }
1106 ])
1107 }).pipe(provideDb(Db))
1108 }
1109 )
1110
1111 it.effect("upsert", () => {
1112 class Db extends IndexedDbDatabase.make(
1113 V1,
1114 Effect.fn(function*(api) {
1115 yield* api.createObjectStore("todo")
1116 yield* api.createIndex("todo", "titleIndex")
1117 yield* api.createIndex("todo", "countIndex")
1118 yield* api
1119 .from("todo")
1120 .insert({ id: 10, title: "insert1", count: 10, completed: true })
1121 })
1122 ) {}
1123
1124 return Effect.gen(function*() {
1125 const api = yield* Db.getQueryBuilder
1126 const addedKey = yield* api
1127 .from("todo")
1128 .upsert({ id: 10, title: "update1", count: -10, completed: false })
1129 const data = yield* api.from("todo").select()
1130
1131 assert.equal(addedKey, 10)
1132 assert.equal(data.length, 1)
1133 assert.deepStrictEqual(data, [
1134 { id: 10, title: "update1", count: -10, completed: false }
1135 ])
1136 }).pipe(provideDb(Db))
1137 })
1138
1139 it.effect("delete", () => {
1140 class Db extends IndexedDbDatabase.make(
1141 V1,
1142 Effect.fn(function*(api) {
1143 yield* api.createObjectStore("todo")
1144 yield* api.createIndex("todo", "titleIndex")
1145 yield* api.createIndex("todo", "countIndex")
1146 yield* api.from("todo").insertAll([
1147 { id: 10, title: "insert1", count: 10, completed: true },
1148 { id: 11, title: "insert2", count: 11, completed: true }
1149 ])
1150 })
1151 ) {}
1152
1153 return Effect.gen(function*() {
1154 const api = yield* Db.getQueryBuilder
1155 yield* api.from("todo").delete().equals(10)
1156 const data = yield* api.from("todo").select()
1157
1158 assert.equal(data.length, 1)
1159 assert.deepStrictEqual(data, [
1160 { id: 11, title: "insert2", count: 11, completed: true }
1161 ])
1162 }).pipe(provideDb(Db))
1163 })
1164
1165 it.effect("delete with limit", () => {
1166 class Db extends IndexedDbDatabase.make(
1167 V1,
1168 Effect.fn(function*(api) {
1169 yield* api.createObjectStore("todo")
1170 yield* api.createIndex("todo", "titleIndex")
1171 yield* api.createIndex("todo", "countIndex")
1172 yield* api.from("todo").insertAll([
1173 { id: 10, title: "insert1", count: 10, completed: true },
1174 { id: 11, title: "insert2", count: 11, completed: true },
1175 { id: 12, title: "insert3", count: 12, completed: true }
1176 ])
1177 })
1178 ) {}
1179
1180 return Effect.gen(function*() {
1181 const api = yield* Db.getQueryBuilder
1182 yield* api.from("todo").delete().limit(2)
1183 const data = yield* api.from("todo").select()
1184
1185 assert.equal(data.length, 1)
1186 assert.deepStrictEqual(data, [
1187 { id: 12, title: "insert3", count: 12, completed: true }
1188 ])
1189 }).pipe(provideDb(Db))
1190 })
1191
1192 it.effect("clear", () => {
1193 class Db extends IndexedDbDatabase.make(
1194 V1,
1195 Effect.fn(function*(api) {
1196 yield* api.createObjectStore("todo")
1197 yield* api.createIndex("todo", "titleIndex")
1198 yield* api.createIndex("todo", "countIndex")
1199 yield* api.from("todo").insertAll([
1200 { id: 10, title: "insert1", count: 10, completed: true },
1201 { id: 11, title: "insert2", count: 11, completed: true }
1202 ])
1203 })
1204 ) {}
1205
1206 return Effect.gen(function*() {
1207 const api = yield* Db.getQueryBuilder
1208 yield* api.from("todo").clear
1209 const data = yield* api.from("todo").select()
1210
1211 assert.equal(data.length, 0)
1212 }).pipe(provideDb(Db))
1213 })
1214 })
1215
1216 describe("modify all", () => {
1217 it.effect("insertAll", () => {
1218 class Db extends IndexedDbDatabase.make(
1219 V1,
1220 Effect.fn(function*(api) {
1221 yield* api.createObjectStore("todo")
1222 yield* api.createIndex("todo", "titleIndex")
1223 yield* api.createIndex("todo", "countIndex")
1224 })
1225 ) {}
1226
1227 return Effect.gen(function*() {
1228 const api = yield* Db.getQueryBuilder
1229 const addedKeys = yield* api.from("todo").insertAll([
1230 { id: 10, title: "insert1", count: 10, completed: true },
1231 { id: 11, title: "insert2", count: 11, completed: true }
1232 ])
1233 const data = yield* api.from("todo").select()
1234
1235 assert.deepStrictEqual(addedKeys, [10, 11])
1236 assert.equal(data.length, 2)
1237 assert.deepStrictEqual(data, [
1238 { id: 10, title: "insert1", count: 10, completed: true },
1239 { id: 11, title: "insert2", count: 11, completed: true }
1240 ])
1241 }).pipe(provideDb(Db))
1242 })
1243
1244 it.effect("upsertAll", () => {
1245 class Db extends IndexedDbDatabase.make(
1246 V1,
1247 Effect.fn(function*(api) {
1248 yield* api.createObjectStore("todo")
1249 yield* api.createIndex("todo", "titleIndex")
1250 yield* api.createIndex("todo", "countIndex")
1251 yield* api.from("todo").insertAll([
1252 { id: 10, title: "insert1", count: 10, completed: true },
1253 { id: 11, title: "insert2", count: 11, completed: true }
1254 ])
1255 })
1256 ) {}
1257
1258 return Effect.gen(function*() {
1259 const api = yield* Db.getQueryBuilder
1260 const addedKeys = yield* api.from("todo").upsertAll([
1261 { id: 10, title: "update1", count: -10, completed: false },
1262 { id: 11, title: "update2", count: -11, completed: false }
1263 ])
1264 const data = yield* api.from("todo").select()
1265
1266 assert.deepStrictEqual(addedKeys, [10, 11])
1267 assert.equal(data.length, 2)
1268 assert.deepStrictEqual(data, [
1269 { id: 10, title: "update1", count: -10, completed: false },
1270 { id: 11, title: "update2", count: -11, completed: false }
1271 ])
1272 }).pipe(provideDb(Db))
1273 })
1274
1275 it.effect("upsertAll withTransaction", () => {
1276 class Db extends IndexedDbDatabase.make(
1277 V1,
1278 Effect.fn(function*(api) {
1279 yield* api.createObjectStore("todo")
1280 yield* api.createIndex("todo", "titleIndex")
1281 yield* api.createIndex("todo", "countIndex")
1282 yield* api.from("todo").insertAll([
1283 { id: 10, title: "insert1", count: 10, completed: true },
1284 { id: 11, title: "insert2", count: 11, completed: true }
1285 ])
1286 })
1287 ) {}
1288
1289 return Effect.gen(function*() {
1290 const api = yield* Db.getQueryBuilder
1291 const addedKeys = yield* api.withTransaction({ tables: ["todo"], mode: "readwrite" })(
1292 Effect.gen(function*() {
1293 return yield* api.from("todo").upsertAll([
1294 { id: 10, title: "update1", count: -10, completed: false },
1295 { id: 11, title: "update2", count: -11, completed: false }
1296 ]).invalidate()
1297 })
1298 )
1299 const data = yield* api.from("todo").select()
1300
1301 assert.deepStrictEqual(addedKeys, [10, 11])
1302 assert.equal(data.length, 2)
1303 assert.deepStrictEqual(data, [
1304 { id: 10, title: "update1", count: -10, completed: false },
1305 { id: 11, title: "update2", count: -11, completed: false }
1306 ])
1307 }).pipe(provideDb(Db))
1308 })
1309
1310 it.effect("upsertAll same key", () => {
1311 class Db extends IndexedDbDatabase.make(
1312 V1,
1313 Effect.fn(function*(api) {
1314 yield* api.createObjectStore("todo")
1315 yield* api.createIndex("todo", "titleIndex")
1316 yield* api.createIndex("todo", "countIndex")
1317 yield* api
1318 .from("todo")
1319 .insert({ id: 10, title: "insert1", count: 10, completed: true })
1320 })
1321 ) {}
1322
1323 return Effect.gen(function*() {
1324 const api = yield* Db.getQueryBuilder
1325 const addedKeys = yield* api.from("todo").upsertAll([
1326 { id: 10, title: "update1", count: -10, completed: false },
1327 { id: 10, title: "update2", count: -11, completed: false }
1328 ])
1329 const data = yield* api.from("todo").select()
1330
1331 assert.deepStrictEqual(addedKeys, [10, 10])
1332 assert.equal(data.length, 1)
1333 assert.deepStrictEqual(data, [
1334 { id: 10, title: "update2", count: -11, completed: false }
1335 ])
1336 }).pipe(provideDb(Db))
1337 })
1338
1339 it.effect("clearAll", () => {
1340 class Db extends IndexedDbDatabase.make(
1341 V1,
1342 Effect.fn(function*(api) {
1343 yield* api.createObjectStore("todo")
1344 yield* api.createIndex("todo", "titleIndex")
1345 yield* api.createIndex("todo", "countIndex")
1346 yield* api
1347 .from("todo")
1348 .insert({ id: 10, title: "insert1", count: 10, completed: true })
1349 })
1350 ) {}
1351
1352 return Effect.gen(function*() {
1353 const api = yield* Db.getQueryBuilder
1354 yield* api.clearAll
1355 const data = yield* api.from("todo").select()
1356
1357 assert.equal(data.length, 0)
1358 }).pipe(provideDb(Db))
1359 })
1360 })
1361
1362 it.effect("count", () => {
1363 class Db extends IndexedDbDatabase.make(
1364 V1,
1365 Effect.fn(function*(api) {
1366 yield* api.createObjectStore("todo")
1367 yield* api.createIndex("todo", "titleIndex")
1368 yield* api.createIndex("todo", "countIndex")
1369 yield* api.from("todo").insertAll([
1370 { id: 1, title: "test1", count: 1, completed: false },
1371 { id: 2, title: "test2", count: 2, completed: false },
1372 { id: 3, title: "test3", count: 3, completed: false }
1373 ])
1374 })
1375 ) {}
1376
1377 return Effect.gen(function*() {
1378 const api = yield* Db.getQueryBuilder
1379 const data = yield* api.from("todo").count()
1380
1381 assert.equal(data, 3)
1382 }).pipe(provideDb(Db))
1383 })
1384
1385 it.effect("count with filters", () => {
1386 class Db extends IndexedDbDatabase.make(
1387 V1,
1388 Effect.fn(function*(api) {
1389 yield* api.createObjectStore("todo")
1390 yield* api.createIndex("todo", "titleIndex")
1391 yield* api.createIndex("todo", "countIndex")
1392 yield* api.from("todo").insertAll([
1393 { id: 1, title: "test1", count: 1, completed: false },
1394 { id: 2, title: "test2", count: 2, completed: false },
1395 { id: 3, title: "test3", count: 3, completed: false }
1396 ])
1397 })
1398 ) {}
1399
1400 return Effect.gen(function*() {
1401 const api = yield* Db
1402 const data = yield* api.from("todo").count("titleIndex").equals("test2")
1403
1404 assert.equal(data, 1)
1405 }).pipe(provideDb(Db))
1406 })
1407
1408 it.effect("filter", () => {
1409 class Db extends IndexedDbDatabase.make(
1410 V1,
1411 Effect.fn(function*(api) {
1412 yield* api.createObjectStore("todo")
1413 yield* api.createIndex("todo", "titleIndex")
1414 yield* api.createIndex("todo", "countIndex")
1415 yield* api.from("todo").insertAll([
1416 { id: 1, title: "test1", count: 1, completed: false },
1417 { id: 2, title: "test2", count: 2, completed: false },
1418 { id: 3, title: "test3", count: 3, completed: false }
1419 ])
1420 })
1421 ) {}
1422
1423 return Effect.gen(function*() {
1424 const api = yield* Db
1425 const data = yield* api.from("todo").select().filter((v) => v.count === 2)
1426
1427 assert.deepStrictEqual(data, [{
1428 id: 2,
1429 title: "test2",
1430 count: 2,
1431 completed: false
1432 }])
1433 }).pipe(provideDb(Db))
1434 })
1435
1436 it.effect("stream", () => {
1437 class Db extends IndexedDbDatabase.make(
1438 V1,
1439 Effect.fn(function*(api) {
1440 yield* api.createObjectStore("todo")
1441 yield* api.createIndex("todo", "titleIndex")
1442 yield* api.createIndex("todo", "countIndex")
1443 yield* api.from("todo").insertAll(
1444 Array.makeBy(1000, (i) => ({ id: i + 1, title: `test${i}`, count: i, completed: false }))
1445 )
1446 })
1447 ) {}
1448
1449 return Effect.gen(function*() {
1450 const api = yield* Db
1451 const data = yield* api.from("todo").select().stream().pipe(
1452 Stream.runCollect
1453 )
1454 assert.equal(data.length, 1000)
1455
1456 const data2 = yield* api.from("todo").select().limit(10).stream().pipe(
1457 Stream.runCollect
1458 )
1459 assert.equal(data2.length, 10)
1460 assert.equal(data2[0].id, 1)
1461
1462 const data3 = yield* api.from("todo").select().limit(10).offset(50).stream().pipe(
1463 Stream.runCollect
1464 )
1465 assert.equal(data3.length, 10)
1466 assert.equal(data3[0].id, 51)
1467
1468 const data4 = yield* api.from("todo").select().limit(3).stream({ chunkSize: 2 }).pipe(
1469 Stream.runCollect
1470 )
1471 assert.deepStrictEqual(data4.map((row) => row.id), [1, 2, 3])
1472 }).pipe(provideDb(Db))
1473 })
1474
1475 it.effect("reactive", () => {
1476 class Db extends IndexedDbDatabase.make(
1477 V1,
1478 Effect.fn(function*(api) {
1479 yield* api.createObjectStore("todo")
1480 yield* api.createIndex("todo", "titleIndex")
1481 yield* api.createIndex("todo", "countIndex")
1482 yield* api.from("todo").insertAll([
1483 { id: 1, title: "test1", count: 1, completed: false },
1484 { id: 2, title: "test2", count: 2, completed: false },
1485 { id: 3, title: "test3", count: 3, completed: false }
1486 ])
1487 })
1488 ) {}
1489
1490 return Effect.gen(function*() {
1491 const api = yield* Db
1492 const data = yield* api.from("todo").select().reactive().pipe(
1493 Stream.take(2),
1494 Stream.runCollect,
1495 Effect.forkChild({ startImmediately: true })
1496 )
1497 yield* api.from("todo").insert({ id: 4, title: "test4", count: 4, completed: false }).invalidate()
1498 yield* Fiber.join(data)
1499 }).pipe(provideDb(Db))
1500 })
1501
1502 it.effect("compound query", () => {
1503 class Db extends IndexedDbDatabase.make(
1504 V1,
1505 Effect.fn(function*(api) {
1506 yield* api.createObjectStore("todo")
1507 yield* api.createIndex("todo", "titleIndex")
1508 yield* api.createIndex("todo", "countIndex")
1509 yield* api.createIndex("todo", "titleCount")
1510 yield* api.from("todo").insertAll([
1511 { id: 1, title: "test1", count: 1, completed: false },
1512 { id: 2, title: "test2", count: 2, completed: false },
1513 { id: 3, title: "test2", count: 3, completed: false },
1514 { id: 4, title: "test3", count: 4, completed: false }
1515 ])
1516 })
1517 ) {}
1518
1519 return Effect.gen(function*() {
1520 const api = yield* Db
1521 const data = yield* api.from("todo").select("titleCount").between(["test2"], ["test2", []])
1522 assert.deepStrictEqual(data, [
1523 { id: 2, title: "test2", count: 2, completed: false },
1524 { id: 3, title: "test2", count: 3, completed: false }
1525 ])
1526
1527 const data2 = yield* api.from("todo").select("titleCount").equals(["test2", 3]).first()
1528 assert.deepStrictEqual(data2, { id: 3, title: "test2", count: 3, completed: false })
1529 }).pipe(provideDb(Db))
1530 })
1531
1532 it.effect("rolls back withTransaction writes when the effect fails", () => {
1533 class Db extends IndexedDbDatabase.make(V1, (api) => api.createObjectStore("todo")) {}
1534
1535 return Effect.gen(function*() {
1536 const api = yield* Db
1537 yield* Effect.result(
1538 api.withTransaction({ tables: ["todo"], mode: "readwrite" })(
1539 Effect.andThen(
1540 api.from("todo").insert({ id: 1, title: "committed", count: 1, completed: false }),
1541 Effect.fail("rollback")
1542 )
1543 )
1544 )
1545
1546 assert.deepStrictEqual(yield* api.from("todo").select(), [])
1547 }).pipe(provideDb(Db))
1548 })
1549
1550 it.effect("applies reverse before a select limit", () => {
1551 class Db extends IndexedDbDatabase.make(
1552 V1,
1553 Effect.fn(function*(api) {
1554 yield* api.createObjectStore("todo")
1555 yield* api.from("todo").insertAll([
1556 { id: 1, title: "one", count: 1, completed: false },
1557 { id: 2, title: "two", count: 2, completed: false },
1558 { id: 3, title: "three", count: 3, completed: false }
1559 ])
1560 })
1561 ) {}
1562
1563 return Effect.gen(function*() {
1564 const api = yield* Db
1565 const rows = yield* api.from("todo").select().reverse().limit(2)
1566 assert.deepStrictEqual(rows.map((row) => row.id), [3, 2])
1567 }).pipe(provideDb(Db))
1568 })
1569
1570 it.effect("honors an indexed range when delete has a limit", () => {
1571 class Db extends IndexedDbDatabase.make(
1572 V1,
1573 Effect.fn(function*(api) {
1574 yield* api.createObjectStore("todo")
1575 yield* api.createIndex("todo", "titleIndex")
1576 yield* api.from("todo").insertAll([
1577 { id: 1, title: "keep", count: 1, completed: false },
1578 { id: 2, title: "delete", count: 2, completed: false }
1579 ])
1580 })
1581 ) {}
1582
1583 return Effect.gen(function*() {
1584 const api = yield* Db
1585 yield* api.from("todo").delete("titleIndex").equals("delete").limit(1)
1586 const rows = yield* api.from("todo").select()
1587 assert.deepStrictEqual(rows.map((row) => row.id), [1])
1588 }).pipe(provideDb(Db))
1589 })
1590
1591 it.effect("can consume the same paged select stream twice", () => {
1592 class Db extends IndexedDbDatabase.make(
1593 V1,
1594 Effect.fn(function*(api) {
1595 yield* api.createObjectStore("todo")
1596 yield* api.from("todo").insertAll([
1597 { id: 1, title: "one", count: 1, completed: false },
1598 { id: 2, title: "two", count: 2, completed: false },
1599 { id: 3, title: "three", count: 3, completed: false }
1600 ])
1601 })
1602 ) {}
1603
1604 return Effect.gen(function*() {
1605 const api = yield* Db
1606 const stream = api.from("todo").select().stream({ chunkSize: 2 })
1607 const first = yield* Stream.runCollect(stream)
1608 const second = yield* Stream.runCollect(stream)
1609 assert.deepStrictEqual(second.map((row) => row.id), first.map((row) => row.id))
1610 }).pipe(provideDb(Db))
1611 })
1612
1613 it.effect("reports NoSuchElementError for an empty ranged first query", () => {
1614 class Db extends IndexedDbDatabase.make(V1, (api) => api.createObjectStore("todo")) {}
1615
1616 return Effect.gen(function*() {
1617 const api = yield* Db
1618 const error = yield* Effect.flip(api.from("todo").select().equals(1).first())
1619 assert.instanceOf(error, Cause.NoSuchElementError)
1620 }).pipe(provideDb(Db))
1621 })
1622})