1/**
2 * Typed object-store descriptors for the browser IndexedDB integration.
3 *
4 * An {@link IndexedDbTable} is the schema-backed description of one IndexedDB
5 * object store. It carries the store name, row schema, key path, index key
6 * paths, auto-increment mode, and transaction durability used by database
7 * versions, migrations, and typed queries. The `make` constructor also derives
8 * the read, array, and auto-increment write schemas used by the query builder.
9 *
10 * @see {@link make} for constructing table descriptors.
11 *
12 * @since 4.0.0
13 */
14import { type Pipeable, pipeArguments } from "effect/Pipeable"
15import * as Schema from "effect/Schema"
16import * as Struct from "effect/Struct"
17import type { NoInfer } from "effect/Types"
18import * as IndexedDb from "./IndexedDb.ts"
19import type * as IndexedDbQueryBuilder from "./IndexedDbQueryBuilder.ts"
20
21const TypeId = "~@effect/platform-browser/IndexedDbTable"
22
23/**
24 * Typed IndexedDB table definition containing its name, schema, key path, indexes, auto-increment setting, and transaction durability.
25 *
26 * @category models
27 * @since 4.0.0
28 */
29export interface IndexedDbTable<
30 out Name extends string,
31 out TableSchema extends AnySchemaStruct,
32 out Indexes extends Record<
33 string,
34 IndexedDbQueryBuilder.KeyPath<TableSchema>
35 >,
36 out KeyPath extends Readonly<IDBValidKey | undefined>,
37 out AutoIncrement extends boolean
38> extends Pipeable {
39 new(_: never): {}
40 readonly [TypeId]: typeof TypeId
41 readonly tableName: Name
42 readonly tableSchema: TableSchema
43 readonly readSchema: Schema.Top
44 readonly autoincrementSchema: Schema.Top
45 readonly arraySchema: Schema.Top
46 readonly keyPath: KeyPath
47 readonly indexes: Indexes
48 readonly autoIncrement: AutoIncrement
49 readonly durability: IDBTransactionDurability
50}
51
52/**
53 * Schema constraint for table schemas that expose struct fields.
54 *
55 * @category models
56 * @since 4.0.0
57 */
58export type AnySchemaStruct = Schema.Top & {
59 readonly fields: Schema.Struct.Fields
60}
61
62/**
63 * Type-erased shape of an `IndexedDbTable` used when table type parameters are not needed.
64 *
65 * @category models
66 * @since 4.0.0
67 */
68export interface Any {
69 readonly [TypeId]: typeof TypeId
70 readonly keyPath: any
71 readonly tableName: string
72 readonly tableSchema: Schema.Top
73 readonly readSchema: Schema.Top
74 readonly autoincrementSchema: Schema.Top
75 readonly arraySchema: Schema.Top
76 readonly autoIncrement: boolean
77 readonly indexes: any
78}
79
80/**
81 * Type-erased `IndexedDbTable` retaining the table interface properties with broad type parameters.
82 *
83 * @category models
84 * @since 4.0.0
85 */
86export type AnyWithProps = IndexedDbTable<
87 string,
88 AnySchemaStruct,
89 any,
90 any,
91 boolean
92>
93
94/**
95 * Extracts the table name type from an `IndexedDbTable`.
96 *
97 * @category utility types
98 * @since 4.0.0
99 */
100export type TableName<Table extends Any> = Table["tableName"]
101/**
102 * Extracts the key-path type from an `IndexedDbTable`.
103 *
104 * @category utility types
105 * @since 4.0.0
106 */
107export type KeyPath<Table extends Any> = Table["keyPath"]
108
109/**
110 * Extracts the auto-increment flag type from an `IndexedDbTable`.
111 *
112 * @category utility types
113 * @since 4.0.0
114 */
115export type AutoIncrement<Table extends Any> = Table["autoIncrement"]
116
117/**
118 * Extracts the schema type from an `IndexedDbTable`.
119 *
120 * @category utility types
121 * @since 4.0.0
122 */
123export type TableSchema<Table extends Any> = Table["tableSchema"]
124/**
125 * Extracts the decoding or encoding service requirements needed by an `IndexedDbTable` schema.
126 *
127 * @category utility types
128 * @since 4.0.0
129 */
130export type Context<Table extends Any> =
131 | Table["tableSchema"]["DecodingServices"]
132 | Table["tableSchema"]["EncodingServices"]
133
134/**
135 * Extracts the encoded row type from an `IndexedDbTable` schema.
136 *
137 * @category utility types
138 * @since 4.0.0
139 */
140export type Encoded<Table extends Any> = Table["tableSchema"]["Encoded"]
141
142/**
143 * Extracts the index definition map from an `IndexedDbTable`.
144 *
145 * @category utility types
146 * @since 4.0.0
147 */
148export type Indexes<Table extends Any> = Table["indexes"]
149
150/**
151 * Selects the table with the given name from a union of `IndexedDbTable` types.
152 *
153 * @category utility types
154 * @since 4.0.0
155 */
156export type WithName<Table extends Any, TableName extends string> = Extract<
157 Table,
158 { readonly tableName: TableName }
159>
160
161const Proto = {
162 [TypeId]: TypeId,
163 pipe() {
164 return pipeArguments(this, arguments)
165 }
166}
167
168/**
169 * Creates a typed IndexedDB table definition from its name, schema, optional key path, indexes, auto-increment flag, and durability.
170 *
171 * **When to use**
172 *
173 * Use to define a typed object-store descriptor for inclusion in an
174 * `IndexedDbVersion` and for migration or query APIs.
175 *
176 * **Details**
177 *
178 * `autoIncrement` defaults to `false` and `durability` defaults to `"relaxed"`.
179 * Tables without a key path get a read schema that includes an out-of-line
180 * `key`, while auto-increment tables use a write schema where the generated key
181 * may be omitted.
182 *
183 * **Gotchas**
184 *
185 * Tables without a key path cannot define a `key` field in their row schema.
186 * Key paths and index paths must point to encoded fields whose values are valid
187 * IndexedDB keys, and declared indexes still need to be created during
188 * database migrations.
189 *
190 * @see `IndexedDbVersion.make` for grouping table definitions into a schema version
191 *
192 * @category constructors
193 * @since 4.0.0
194 */
195export const make = <
196 const Name extends string,
197 TableSchema extends AnySchemaStruct,
198 const Indexes extends Record<
199 string,
200 IndexedDbQueryBuilder.KeyPath<TableSchema>
201 >,
202 const KeyPath extends
203 | (AutoIncrement extends true ? IndexedDbQueryBuilder.KeyPathNumber<NoInfer<TableSchema>>
204 : IndexedDbQueryBuilder.KeyPath<NoInfer<TableSchema>>)
205 | undefined = undefined,
206 const AutoIncrement extends boolean = false
207>(options: {
208 readonly name: Name
209 readonly schema: [KeyPath] extends [undefined]
210 ? "key" extends keyof TableSchema["fields"] ? "Cannot have a 'key' field when keyPath is undefined"
211 : TableSchema
212 : TableSchema
213 readonly keyPath?: KeyPath
214 readonly indexes?: Indexes | undefined
215 readonly autoIncrement?: IsValidAutoIncrementKeyPath<
216 TableSchema,
217 KeyPath
218 > extends true ? AutoIncrement | undefined
219 : never
220 readonly durability?: IDBTransactionDurability | undefined
221}): IndexedDbTable<
222 Name,
223 TableSchema,
224 Indexes,
225 Extract<KeyPath, Readonly<IDBValidKey | undefined>>,
226 AutoIncrement
227> => {
228 // oxlint-disable-next-line typescript/no-extraneous-class
229 class Table {}
230 Object.assign(Table, Proto)
231 const readSchema = options.keyPath === undefined
232 ? Schema.Struct({
233 ...(options.schema as Schema.Struct<{}>).fields,
234 key: IndexedDb.IDBValidKey
235 })
236 : options.schema
237 ;(Table as any).tableName = options.name
238 ;(Table as any).tableSchema = options.schema
239 ;(Table as any).readSchema = readSchema
240 ;(Table as any).arraySchema = Schema.Array(readSchema as any)
241 ;(Table as any).autoincrementSchema = options.autoIncrement
242 ? Schema.Struct(Struct.omit((options.schema as Schema.Struct<{}>).fields, [options.keyPath!] as any))
243 : options.schema
244 ;(Table as any).keyPath = options.keyPath
245 ;(Table as any).indexes = options.indexes
246 ;(Table as any).autoIncrement = options.autoIncrement === true
247 ;(Table as any).durability = options.durability ?? "relaxed"
248 return Table as any
249}
250
251// -----------------------------------------------------------------------------
252// internal
253// -----------------------------------------------------------------------------
254
255type IsValidAutoIncrementKeyPath<
256 TableSchema extends AnySchemaStruct,
257 KeyPath
258> = KeyPath extends keyof TableSchema["Encoded"] ? TableSchema["Encoded"][KeyPath] extends number ? true
259 : false
260 : false