@playfast/reform-db-sqlite 1.0.2 → 1.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/README.md +1 -3
- package/package.json +24 -24
- package/src/engine.ts +2 -1
- package/src/sqlite.bun.spec.ts +88 -109
package/README.md
CHANGED
|
@@ -44,9 +44,7 @@ const DataLayer = QueriesAndProcedures.pipe(
|
|
|
44
44
|
)
|
|
45
45
|
|
|
46
46
|
// Ephemeral in-memory — the default for tests / proofs:
|
|
47
|
-
const TestLayer = QueriesAndProcedures.pipe(
|
|
48
|
-
Layer.provideMerge(Sqlite.memory({ migrations })),
|
|
49
|
-
)
|
|
47
|
+
const TestLayer = QueriesAndProcedures.pipe(Layer.provideMerge(Sqlite.memory({ migrations })))
|
|
50
48
|
```
|
|
51
49
|
|
|
52
50
|
Both layers build the `Db` service **synchronously** (bun:sqlite opens on a background
|
package/package.json
CHANGED
|
@@ -1,37 +1,39 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@playfast/reform-db-sqlite",
|
|
3
|
-
"
|
|
4
|
-
"version": "1.0.2",
|
|
5
|
-
"type": "module",
|
|
3
|
+
"version": "1.1.0",
|
|
6
4
|
"description": "bun:sqlite engine for @playfast/reform-db — a Db driver backed by Bun's built-in SQLite, with emulated reactivity and serialize()/deserialize() snapshots.",
|
|
7
5
|
"keywords": [
|
|
8
|
-
"reform",
|
|
9
|
-
"effect",
|
|
10
|
-
"sqlite",
|
|
11
6
|
"bun",
|
|
12
|
-
"
|
|
7
|
+
"effect",
|
|
8
|
+
"local-first",
|
|
13
9
|
"reactive",
|
|
14
|
-
"
|
|
10
|
+
"reform",
|
|
11
|
+
"sql",
|
|
12
|
+
"sqlite"
|
|
15
13
|
],
|
|
14
|
+
"bugs": {
|
|
15
|
+
"url": "https://github.com/playfast/reform/issues"
|
|
16
|
+
},
|
|
16
17
|
"license": "MIT",
|
|
17
18
|
"repository": {
|
|
18
19
|
"type": "git",
|
|
19
20
|
"url": "https://github.com/playfast/reform.git",
|
|
20
21
|
"directory": "packages/reform-db-sqlite"
|
|
21
22
|
},
|
|
22
|
-
"
|
|
23
|
-
"
|
|
24
|
-
|
|
23
|
+
"files": [
|
|
24
|
+
"src",
|
|
25
|
+
"README.md"
|
|
26
|
+
],
|
|
27
|
+
"type": "module",
|
|
25
28
|
"sideEffects": false,
|
|
26
29
|
"exports": {
|
|
27
30
|
"./package.json": "./package.json",
|
|
28
31
|
".": "./src/index.ts",
|
|
29
32
|
"./*": "./src/*.ts"
|
|
30
33
|
},
|
|
31
|
-
"
|
|
32
|
-
"
|
|
33
|
-
|
|
34
|
-
],
|
|
34
|
+
"publishConfig": {
|
|
35
|
+
"access": "public"
|
|
36
|
+
},
|
|
35
37
|
"scripts": {
|
|
36
38
|
"clean": "rm -rf dist .tsbuildinfo",
|
|
37
39
|
"check": "tsc --noEmit",
|
|
@@ -40,16 +42,14 @@
|
|
|
40
42
|
"lint": "oxlint src",
|
|
41
43
|
"lint:fix": "oxlint --fix src"
|
|
42
44
|
},
|
|
43
|
-
"peerDependencies": {
|
|
44
|
-
"effect": "*",
|
|
45
|
-
"kysely": "*",
|
|
46
|
-
"@playfast/reform": "*",
|
|
47
|
-
"@playfast/reform-db": "*"
|
|
48
|
-
},
|
|
49
45
|
"devDependencies": {
|
|
50
46
|
"@types/bun": "^1.3.14"
|
|
51
47
|
},
|
|
52
|
-
"
|
|
53
|
-
"
|
|
54
|
-
|
|
48
|
+
"peerDependencies": {
|
|
49
|
+
"@playfast/reform": "*",
|
|
50
|
+
"@playfast/reform-db": "*",
|
|
51
|
+
"effect": "*",
|
|
52
|
+
"kysely": "*"
|
|
53
|
+
},
|
|
54
|
+
"playbook": "./playbook"
|
|
55
55
|
}
|
package/src/engine.ts
CHANGED
|
@@ -44,7 +44,8 @@ const openSqlite = Effect.fn('openSqlite')(function* (
|
|
|
44
44
|
): Effect.fn.Return<Database, DbError> {
|
|
45
45
|
const { Database } = yield* Effect.tryPromise({
|
|
46
46
|
try: () => import('bun:sqlite'),
|
|
47
|
-
catch: (cause) =>
|
|
47
|
+
catch: (cause) =>
|
|
48
|
+
new DbError({ reason: 'bun:sqlite unavailable (Bun runtime required)', cause }),
|
|
48
49
|
})
|
|
49
50
|
// deserialize wants bytes sync; Blob is already in memory — read async then open sync.
|
|
50
51
|
const seed = yield* Option.match(Option.fromNullable(options.loadDataDir), {
|
package/src/sqlite.bun.spec.ts
CHANGED
|
@@ -7,14 +7,14 @@ import {
|
|
|
7
7
|
DbTable,
|
|
8
8
|
Migration,
|
|
9
9
|
type QueryData,
|
|
10
|
-
} from
|
|
11
|
-
import { expect, it } from
|
|
12
|
-
import { Context, Effect, Exit, Layer, Schedule, Schema, Scope } from
|
|
13
|
-
import { Sqlite } from
|
|
10
|
+
} from '@playfast/reform-db'
|
|
11
|
+
import { expect, it } from '@effect/vitest'
|
|
12
|
+
import { Context, Effect, Exit, Layer, Schedule, Schema, Scope } from 'effect'
|
|
13
|
+
import { Sqlite } from './index'
|
|
14
14
|
|
|
15
|
-
const Meta = Schema.Struct({ note: Schema.String })
|
|
15
|
+
const Meta = Schema.Struct({ note: Schema.String })
|
|
16
16
|
|
|
17
|
-
const Items = DbTable.make(
|
|
17
|
+
const Items = DbTable.make('items', {
|
|
18
18
|
columns: {
|
|
19
19
|
id: DbColumn.text({ primaryKey: true }),
|
|
20
20
|
label: DbColumn.text(),
|
|
@@ -22,130 +22,109 @@ const Items = DbTable.make("items", {
|
|
|
22
22
|
done: DbColumn.boolean(),
|
|
23
23
|
meta: DbColumn.json(Meta),
|
|
24
24
|
},
|
|
25
|
-
primaryKey:
|
|
26
|
-
})
|
|
25
|
+
primaryKey: 'id',
|
|
26
|
+
})
|
|
27
27
|
|
|
28
|
-
const AppDb = DbSchema.make(
|
|
29
|
-
const migrations = Migration.fromSchema(AppDb, Migration.sqlite)
|
|
28
|
+
const AppDb = DbSchema.make('app', { tables: [Items] })
|
|
29
|
+
const migrations = Migration.fromSchema(AppDb, Migration.sqlite)
|
|
30
30
|
|
|
31
|
-
const ItemsQuery = DbQuery.make(
|
|
31
|
+
const ItemsQuery = DbQuery.make('items', { output: Items.Row })
|
|
32
32
|
|
|
33
33
|
type Item = {
|
|
34
|
-
readonly id: string
|
|
35
|
-
readonly label: string
|
|
36
|
-
readonly rank: number
|
|
37
|
-
readonly done: boolean
|
|
38
|
-
readonly meta: { readonly note: string }
|
|
39
|
-
}
|
|
34
|
+
readonly id: string
|
|
35
|
+
readonly label: string
|
|
36
|
+
readonly rank: number
|
|
37
|
+
readonly done: boolean
|
|
38
|
+
readonly meta: { readonly note: string }
|
|
39
|
+
}
|
|
40
40
|
|
|
41
41
|
const rowsOf = (data: QueryData<Item>): ReadonlyArray<Item> =>
|
|
42
|
-
data._tag ===
|
|
42
|
+
data._tag === 'Success' ? data.value : []
|
|
43
43
|
|
|
44
44
|
const waitUntil = (predicate: () => boolean) =>
|
|
45
45
|
Effect.sync(predicate).pipe(
|
|
46
46
|
Effect.repeat({
|
|
47
47
|
until: (ready) => ready,
|
|
48
|
-
schedule: Schedule.spaced(
|
|
48
|
+
schedule: Schedule.spaced('20 millis'),
|
|
49
49
|
}),
|
|
50
|
-
Effect.timeout(
|
|
51
|
-
)
|
|
50
|
+
Effect.timeout('5 seconds'),
|
|
51
|
+
)
|
|
52
52
|
|
|
53
53
|
const liveLayer = (dataLayer: Layer.Layer<Db>) =>
|
|
54
54
|
DbQuery.live(ItemsQuery, {
|
|
55
55
|
schema: AppDb,
|
|
56
56
|
inputs: [],
|
|
57
|
-
build: (_inputs, db) => db.selectFrom(
|
|
58
|
-
}).pipe(Layer.provideMerge(dataLayer))
|
|
59
|
-
|
|
60
|
-
it.live(
|
|
61
|
-
|
|
62
|
-
|
|
63
|
-
|
|
64
|
-
|
|
65
|
-
|
|
66
|
-
|
|
67
|
-
|
|
68
|
-
|
|
69
|
-
|
|
70
|
-
|
|
71
|
-
|
|
72
|
-
|
|
73
|
-
|
|
74
|
-
|
|
75
|
-
|
|
76
|
-
|
|
77
|
-
|
|
78
|
-
|
|
79
|
-
|
|
80
|
-
|
|
81
|
-
|
|
82
|
-
|
|
83
|
-
|
|
84
|
-
|
|
85
|
-
|
|
86
|
-
|
|
87
|
-
|
|
88
|
-
|
|
89
|
-
|
|
90
|
-
|
|
91
|
-
|
|
92
|
-
|
|
93
|
-
|
|
94
|
-
|
|
95
|
-
|
|
96
|
-
|
|
97
|
-
|
|
98
|
-
|
|
99
|
-
|
|
100
|
-
|
|
101
|
-
|
|
102
|
-
|
|
103
|
-
|
|
104
|
-
|
|
105
|
-
|
|
106
|
-
|
|
107
|
-
|
|
108
|
-
|
|
109
|
-
|
|
110
|
-
|
|
111
|
-
|
|
112
|
-
|
|
113
|
-
|
|
114
|
-
).pipe(Effect.provideService(Db, driver));
|
|
115
|
-
return yield* driver.dump();
|
|
116
|
-
}),
|
|
117
|
-
);
|
|
118
|
-
|
|
119
|
-
yield* Effect.scoped(
|
|
120
|
-
Effect.gen(function* () {
|
|
121
|
-
const store = yield* ItemsQuery.store;
|
|
122
|
-
yield* waitUntil(() => rowsOf(store.getSnapshot()).length === 1);
|
|
123
|
-
const row = rowsOf(store.getSnapshot())[0];
|
|
124
|
-
expect(row?.label).toBe("first");
|
|
125
|
-
expect(row?.done).toBe(true);
|
|
126
|
-
expect(row?.meta.note).toBe("one");
|
|
127
|
-
}).pipe(
|
|
128
|
-
Effect.provide(
|
|
129
|
-
liveLayer(Sqlite.memory({ migrations, loadDataDir: dump })),
|
|
130
|
-
),
|
|
131
|
-
),
|
|
132
|
-
);
|
|
133
|
-
}),
|
|
134
|
-
);
|
|
57
|
+
build: (_inputs, db) => db.selectFrom('items').selectAll().orderBy('rank'),
|
|
58
|
+
}).pipe(Layer.provideMerge(dataLayer))
|
|
59
|
+
|
|
60
|
+
it.live('DbQuery reflects writes (emulated reactivity), decoding boolean + json columns', () => {
|
|
61
|
+
const program = Effect.gen(function* () {
|
|
62
|
+
const store = yield* ItemsQuery.store
|
|
63
|
+
|
|
64
|
+
yield* waitUntil(() => store.getSnapshot()._tag === 'Success')
|
|
65
|
+
expect(rowsOf(store.getSnapshot())).toEqual([])
|
|
66
|
+
|
|
67
|
+
const insert = (id: string, label: string, rank: number, done: boolean, note: string) =>
|
|
68
|
+
Db.exec(AppDb.kysely.insertInto('items').values({ id, label, rank, done, meta: { note } }))
|
|
69
|
+
|
|
70
|
+
yield* insert('b', 'second', 2, false, 'two')
|
|
71
|
+
yield* insert('a', 'first', 1, true, 'one')
|
|
72
|
+
|
|
73
|
+
yield* waitUntil(() => rowsOf(store.getSnapshot()).length === 2)
|
|
74
|
+
const rows = rowsOf(store.getSnapshot())
|
|
75
|
+
expect(rows.map((row) => row.label)).toEqual(['first', 'second'])
|
|
76
|
+
expect(rows.map((row) => row.done)).toEqual([true, false])
|
|
77
|
+
expect(rows.map((row) => row.meta.note)).toEqual(['one', 'two'])
|
|
78
|
+
}).pipe(Effect.provide(liveLayer(Sqlite.memory({ migrations }))))
|
|
79
|
+
|
|
80
|
+
return program
|
|
81
|
+
})
|
|
82
|
+
|
|
83
|
+
it.scopedLive('dump() → loadDataDir round-trips rows into a fresh in-memory db', () =>
|
|
84
|
+
Effect.gen(function* () {
|
|
85
|
+
const dump = yield* Effect.scoped(
|
|
86
|
+
Effect.gen(function* () {
|
|
87
|
+
const context = yield* Layer.build(Sqlite.memory({ migrations }))
|
|
88
|
+
const driver = Context.get(context, Db)
|
|
89
|
+
yield* Db.exec(
|
|
90
|
+
AppDb.kysely.insertInto('items').values({
|
|
91
|
+
id: 'a',
|
|
92
|
+
label: 'first',
|
|
93
|
+
rank: 1,
|
|
94
|
+
done: true,
|
|
95
|
+
meta: { note: 'one' },
|
|
96
|
+
}),
|
|
97
|
+
).pipe(Effect.provideService(Db, driver))
|
|
98
|
+
return yield* driver.dump()
|
|
99
|
+
}),
|
|
100
|
+
)
|
|
101
|
+
|
|
102
|
+
yield* Effect.scoped(
|
|
103
|
+
Effect.gen(function* () {
|
|
104
|
+
const store = yield* ItemsQuery.store
|
|
105
|
+
yield* waitUntil(() => rowsOf(store.getSnapshot()).length === 1)
|
|
106
|
+
const row = rowsOf(store.getSnapshot())[0]
|
|
107
|
+
expect(row?.label).toBe('first')
|
|
108
|
+
expect(row?.done).toBe(true)
|
|
109
|
+
expect(row?.meta.note).toBe('one')
|
|
110
|
+
}).pipe(Effect.provide(liveLayer(Sqlite.memory({ migrations, loadDataDir: dump })))),
|
|
111
|
+
)
|
|
112
|
+
}),
|
|
113
|
+
)
|
|
135
114
|
|
|
136
115
|
it.scopedLive(
|
|
137
|
-
|
|
116
|
+
'Sqlite.memory layer is sync-providable: store starts loading, then resolves to Success',
|
|
138
117
|
() =>
|
|
139
118
|
Effect.gen(function* () {
|
|
140
|
-
const layer = liveLayer(Sqlite.memory({ migrations }))
|
|
119
|
+
const layer = liveLayer(Sqlite.memory({ migrations }))
|
|
141
120
|
|
|
142
121
|
// Sync build (opens on background fiber) — must not throw "Fiber cannot be resolved synchronously".
|
|
143
|
-
const scope = Effect.runSync(Scope.make())
|
|
144
|
-
const context = Effect.runSync(Layer.buildWithScope(layer, scope))
|
|
145
|
-
yield* Effect.addFinalizer(() => Scope.close(scope, Exit.void))
|
|
146
|
-
const store = Context.get(context, ItemsQuery.store)
|
|
147
|
-
expect(store.getSnapshot()._tag).toBe(
|
|
148
|
-
yield* waitUntil(() => store.getSnapshot()._tag ===
|
|
149
|
-
expect(rowsOf(store.getSnapshot())).toEqual([])
|
|
122
|
+
const scope = Effect.runSync(Scope.make())
|
|
123
|
+
const context = Effect.runSync(Layer.buildWithScope(layer, scope))
|
|
124
|
+
yield* Effect.addFinalizer(() => Scope.close(scope, Exit.void))
|
|
125
|
+
const store = Context.get(context, ItemsQuery.store)
|
|
126
|
+
expect(store.getSnapshot()._tag).toBe('Loading')
|
|
127
|
+
yield* waitUntil(() => store.getSnapshot()._tag === 'Success')
|
|
128
|
+
expect(rowsOf(store.getSnapshot())).toEqual([])
|
|
150
129
|
}),
|
|
151
|
-
)
|
|
130
|
+
)
|