Skip to content
This commit does not belong to any branch on this repository, and may belong to a fork outside of the repository.

Commit d11b047

Browse files
committedNov 24, 2024·
feat: typesafe nested object and array types
1 parent 007eeeb commit d11b047

File tree

7 files changed

+125
-46
lines changed

7 files changed

+125
-46
lines changed
 

‎packages/idb-orm/src/builders/base.ts

+2-2
Original file line numberDiff line numberDiff line change
@@ -1,5 +1,5 @@
11
import type { DatabaseSchema, Operation } from '../types'
2-
import { selectFields, uuid } from '../utils'
2+
import { isPrimaryKeyField, selectFields, uuid } from '../utils'
33

44
export abstract class BaseQueryBuilder<
55
TSchema extends DatabaseSchema,
@@ -105,7 +105,7 @@ export abstract class BaseQueryBuilder<
105105

106106
private ensurePrimaryKey(data: any): void {
107107
const pkField = Object.entries(this.tableSchema).find(
108-
([_, field]) => field.primaryKey,
108+
([_, field]) => isPrimaryKeyField(field) && field.primaryKey,
109109
)
110110

111111
if (pkField) {

‎packages/idb-orm/src/orm.ts

+4-3
Original file line numberDiff line numberDiff line change
@@ -1,5 +1,6 @@
1-
import type { DatabaseResults, DatabaseSchema } from './types'
1+
import type { DatabaseResults, DatabaseSchema, PrimitiveFieldSchema } from './types'
22
import { QueryBuilder } from './builders/query'
3+
import { isPrimaryKeyField } from './utils'
34

45
export class IdbOrm<TSchema extends DatabaseSchema> {
56
public dbName: string
@@ -35,7 +36,7 @@ export class IdbOrm<TSchema extends DatabaseSchema> {
3536
Object.entries(this.schema).forEach(([tableName, tableSchema]) => {
3637
if (!db.objectStoreNames.contains(tableName)) {
3738
const pkField = Object.entries(tableSchema).find(
38-
([_, field]) => field.primaryKey,
39+
([_, field]) => isPrimaryKeyField(field) && field.primaryKey,
3940
)
4041

4142
if (!pkField)
@@ -44,7 +45,7 @@ export class IdbOrm<TSchema extends DatabaseSchema> {
4445
const [keyPath, field] = pkField
4546
db.createObjectStore(tableName, {
4647
keyPath,
47-
autoIncrement: field.autoIncrement ?? false,
48+
autoIncrement: (field as PrimitiveFieldSchema<any>).autoIncrement ?? false,
4849
})
4950
}
5051
})

‎packages/idb-orm/src/types.ts

+58-31
Original file line numberDiff line numberDiff line change
@@ -1,27 +1,66 @@
1-
export type Field = 'string' | 'number' | 'boolean' | 'object' | 'array'
1+
export type PrimitiveField = 'string' | 'number' | 'boolean'
2+
3+
// Schema field definitions for nested structures
4+
export interface ArrayFieldSchema<T = any> {
5+
type: 'array'
6+
items: SchemaField
7+
required?: boolean
8+
default?: T[] | (() => T[])
9+
}
10+
11+
export interface ObjectFieldSchema<T = any> {
12+
type: 'object'
13+
props: { [K in keyof T]: SchemaField }
14+
required?: boolean
15+
default?: T | (() => T)
16+
}
17+
18+
export interface PrimitiveFieldSchema<T extends PrimitiveField> {
19+
type: T
20+
required?: boolean
21+
default?: TypeMap[T] | (() => TypeMap[T])
22+
primaryKey?: boolean
23+
autoIncrement?: boolean
24+
}
25+
26+
export type SchemaField =
27+
| PrimitiveFieldSchema<PrimitiveField>
28+
| ArrayFieldSchema
29+
| ObjectFieldSchema
230

331
export interface TypeMap {
432
string: string
533
number: number
634
boolean: boolean
7-
object: object
8-
array: any[]
935
}
1036

11-
export type SchemaField = {
12-
[T in Field]: {
13-
type: T
14-
required?: boolean
15-
default?: TypeMap[T] | (() => TypeMap[T])
16-
primaryKey?: boolean
17-
autoIncrement?: boolean
18-
}
19-
}[Field]
37+
// Helper type to infer the actual TypeScript type from a schema field
38+
export type InferField<T extends SchemaField> = T extends PrimitiveFieldSchema<infer P>
39+
? TypeMap[P]
40+
: T extends ArrayFieldSchema
41+
? InferField<T['items']>[]
42+
: T extends ObjectFieldSchema
43+
? { [K in keyof T['props']]: InferField<T['props'][K]> }
44+
: never
2045

2146
export interface TableSchema {
2247
[key: string]: SchemaField
2348
}
2449

50+
export type InferSchemaType<T extends TableSchema> = {
51+
[K in keyof T as T[K] extends { primaryKey: true }
52+
? K
53+
: T[K] extends { required: true } | { default: any }
54+
? K
55+
: never]: InferField<T[K]>
56+
} & {
57+
[K in keyof T as T[K] extends { primaryKey: true }
58+
? never
59+
: T[K] extends { required: true } | { default: any }
60+
? never
61+
: K]?: InferField<T[K]>
62+
}
63+
2564
export type PrimaryKeyField<T extends TableSchema> = {
2665
[K in keyof T]: T[K] extends { primaryKey: true } ? K : never
2766
}[keyof T]
@@ -47,30 +86,18 @@ export interface FilterCondition {
4786
value: any
4887
}
4988

50-
export type InferValue<T extends SchemaField> = TypeMap[T['type']]
89+
export type InferValue<T extends SchemaField> = T extends PrimitiveFieldSchema<infer P>
90+
? TypeMap[P]
91+
: T extends ArrayFieldSchema
92+
? InferField<T['items']>[]
93+
: T extends ObjectFieldSchema
94+
? { [K in keyof T['props']]: InferField<T['props'][K]> }
95+
: never
5196

5297
export type FieldType<T extends TableSchema, K extends keyof T> = InferValue<T[K]>
5398

54-
export type InferSchemaType<T extends TableSchema> = {
55-
[K in keyof T as T[K] extends { primaryKey: true }
56-
? K
57-
: T[K] extends { required: true } | { default: any }
58-
? K
59-
: never]: InferValue<T[K]>
60-
} & {
61-
[K in keyof T as T[K] extends { primaryKey: true }
62-
? never
63-
: T[K] extends { required: true } | { default: any }
64-
? never
65-
: K]?: InferValue<T[K]>
66-
}
67-
6899
type HasDefault<T extends SchemaField> = T extends { default: any } ? true : false
69100

70-
// type DefaultKeys<T extends TableSchema> = {
71-
// [K in keyof T]: HasDefault<T[K]> extends true ? K : never
72-
// }[keyof T]
73-
74101
type RequiredKeys<T extends TableSchema> = {
75102
[K in keyof T]: T[K] extends { required: true }
76103
? HasDefault<T[K]> extends true ? never : K

‎packages/idb-orm/src/utils.ts

+6
Original file line numberDiff line numberDiff line change
@@ -1,3 +1,9 @@
1+
import type { PrimitiveFieldSchema } from './types'
2+
3+
export function isPrimaryKeyField(field: any): field is PrimitiveFieldSchema<any> {
4+
return 'primaryKey' in field
5+
}
6+
17
export function selectFields<T extends object, K extends keyof T>(
28
obj: T,
39
fields: K[],

‎packages/idb-orm/test/advanced.test.ts

+14-2
Original file line numberDiff line numberDiff line change
@@ -8,8 +8,20 @@ const advancedSchema = {
88
price: { type: 'number', required: true },
99
category: { type: 'string', required: true },
1010
inStock: { type: 'boolean', default: false },
11-
tags: { type: 'array', default: [] },
12-
metadata: { type: 'object', default: {} },
11+
tags: {
12+
type: 'array',
13+
items: { type: 'string' },
14+
default: [],
15+
},
16+
metadata: {
17+
type: 'object',
18+
props: {
19+
edition: { type: 'string' },
20+
serialNumber: { type: 'string' },
21+
test: { type: 'boolean', required: false },
22+
},
23+
default: {},
24+
},
1325
},
1426
orders: {
1527
id: { type: 'number', primaryKey: true, autoIncrement: true },

‎packages/idb-orm/test/nested.test.ts

+37-7
Original file line numberDiff line numberDiff line change
@@ -7,25 +7,55 @@ const nestedSchema = {
77
name: { type: 'string', required: true },
88
metadata: {
99
type: 'object',
10+
props: {
11+
createdBy: { type: 'string' },
12+
version: { type: 'number' },
13+
updated: { type: 'boolean' },
14+
config: {
15+
type: 'object',
16+
props: {
17+
setting1: { type: 'boolean' },
18+
setting2: { type: 'boolean' },
19+
},
20+
},
21+
nested: {
22+
type: 'object',
23+
props: {
24+
deep: {
25+
type: 'object',
26+
props: {
27+
value: { type: 'boolean' },
28+
},
29+
},
30+
},
31+
},
32+
},
1033
default: {},
1134
},
1235
tags: {
1336
type: 'array',
37+
items: { type: 'string' },
1438
default: [],
1539
},
1640
attributes: {
1741
type: 'object',
18-
default: {
19-
color: 'default',
20-
size: 'medium',
42+
props: {
43+
color: { type: 'string' },
44+
size: { type: 'string' },
45+
material: { type: 'string' },
2146
},
47+
default: { color: 'default', size: 'medium' },
2248
},
2349
variants: {
2450
type: 'array',
25-
default: () => [{
26-
name: 'default',
27-
stock: 0,
28-
}],
51+
items: {
52+
type: 'object',
53+
props: {
54+
name: { type: 'string' },
55+
stock: { type: 'number' },
56+
},
57+
},
58+
default: () => [{ name: 'default', stock: 0 }],
2959
},
3060
},
3161
} satisfies DatabaseSchema

‎packages/idb-orm/test/type.test.ts

+4-1
Original file line numberDiff line numberDiff line change
@@ -8,7 +8,10 @@ const schema = {
88
name: { type: 'string', required: true },
99
email: { type: 'string' },
1010
age: { type: 'number', required: true },
11-
meta: { type: 'object' },
11+
meta: {
12+
type: 'object',
13+
14+
},
1215
},
1316
posts: {
1417
id: { type: 'number', primaryKey: true, autoIncrement: true },

0 commit comments

Comments
 (0)
Please sign in to comment.