this repo has no description
1// builders
2
3import {StoreBuilder, StoreUpdater} from './builder.store'
4import type {
5 DatabaseDef,
6 ExtantKey,
7 Increment,
8 IndexesConstraint,
9 MigrationDef,
10 MigrationFn,
11 RowConstraint,
12 SchemaConstraint,
13 Simplify,
14 StoreDef,
15 UnusedKey,
16} from './schema'
17
18// Helper: merge row type changes into schema, preserving indexes for existing stores
19type MergeRowChanges<
20 Current extends SchemaConstraint,
21 Changes extends Record<string, RowConstraint>
22> = {
23 [K in keyof Current | keyof Changes]:
24 K extends keyof Changes
25 ? K extends keyof Current
26 // Existing store: update row, preserve indexes
27 ? StoreDef<K & string, Changes[K], Changes[K], Current[K]['__indexes']>
28 // New store: no indexes
29 : StoreDef<K & string, Changes[K], Changes[K], IndexesConstraint>
30 // Unchanged store
31 : K extends keyof Current ? Current[K] : never
32}
33
34export class DatabaseBuilder<
35 Name extends string,
36 Version extends number = 0,
37 Schema extends SchemaConstraint = {},
38> {
39 #name: Name
40 #stores: Record<string, StoreDef<string, any, any, any>>
41 #migrations: MigrationDef[]
42
43 constructor(name: Name) {
44 this.#name = name
45 this.#stores = {}
46 this.#migrations = []
47 }
48
49 create<SName extends string, Builder extends StoreBuilder<any, any, any>>(
50 name: UnusedKey<SName, Schema>,
51 callback: (builder: StoreBuilder<{}>) => Builder
52 ) {
53 if (this.#stores[name]) throw new Error('cannot add store ${name} - already exists')
54
55 const builder = callback(new StoreBuilder())
56 const store = builder.build(name)
57
58 this.#stores[name] = store
59 this.#migrations.push(['create', name, store])
60
61 type FinalStore = Builder extends StoreBuilder<infer R, infer D, infer I>
62 ? StoreDef<SName, R, D, I>
63 : never
64 return this as unknown as DatabaseBuilder<
65 Name,
66 Increment<Version>,
67 Simplify<Schema & {[K in SName]: FinalStore}>
68 >
69 }
70
71 modify<SName extends string, Builder extends StoreUpdater<any, any, any, any>>(
72 name: ExtantKey<SName, Schema>,
73 callback: (b: StoreUpdater<
74 SName,
75 Schema[SName]['__row'],
76 Schema[SName]['__doc'],
77 Schema[SName]['__indexes']
78 >) => Builder
79 ) {
80 if (this.#stores[name] === undefined) throw new Error('cannot update store ${name} - does not exist')
81
82 const extant = this.#stores[name satisfies string]
83 type InputUpdater = StoreUpdater<
84 SName,
85 Schema[SName]['__row'],
86 Schema[SName]['__doc'],
87 Schema[SName]['__indexes']
88 >
89 const builder = new StoreUpdater(extant as any) as InputUpdater
90 const store = callback(builder).build()
91
92 this.#stores[name] = store
93 this.#migrations.push(['update', name, store])
94
95 type FinalStore = Builder extends StoreUpdater<any, infer R, infer D, infer I>
96 ? StoreDef<SName, R, D, I>
97 : never
98
99 return this as unknown as DatabaseBuilder<
100 Name,
101 Increment<Version>,
102 Simplify<Omit<Schema, SName> & {[K in SName]: FinalStore}>
103 >
104 }
105
106 drop<SName extends string>(name: ExtantKey<SName, Schema>) {
107 if (this.#stores[name] === undefined) throw new Error('cannot remove store ${name} - does not exist')
108
109 this.#migrations.push(['drop', name, this.#stores[name]])
110 delete this.#stores[name]
111
112 return this as unknown as DatabaseBuilder<Name, Increment<Version>, Simplify<Omit<Schema, SName>>>
113 }
114
115 migrate<
116 Write extends Record<string, RowConstraint>,
117 NextSchema extends SchemaConstraint = Simplify<MergeRowChanges<Schema, Write>>,
118 >(fn: MigrationFn<Schema, NextSchema>) {
119 this.#migrations.push(fn)
120
121 return this as unknown as DatabaseBuilder<Name, Increment<Version>, NextSchema>
122 }
123
124 build(): DatabaseDef<Name, Version, Schema> {
125 return {
126 name: this.#name,
127 version: this.#migrations.length as Version,
128 migrations: this.#migrations,
129 stores: this.#stores,
130
131 __schema: undefined as unknown as Schema,
132 }
133 }
134}