-
Notifications
You must be signed in to change notification settings - Fork 283
/
Copy pathcamel-case-plugin.ts
173 lines (158 loc) · 4.65 KB
/
camel-case-plugin.ts
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
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
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
import { QueryResult } from '../../driver/database-connection.js'
import { RootOperationNode } from '../../query-compiler/query-compiler.js'
import {
isArrayBufferOrView,
isBuffer,
isDate,
isObject,
} from '../../util/object-utils.js'
import { UnknownRow } from '../../util/type-utils.js'
import {
KyselyPlugin,
PluginTransformQueryArgs,
PluginTransformResultArgs,
} from '../kysely-plugin.js'
import { SnakeCaseTransformer } from './camel-case-transformer.js'
import {
createCamelCaseMapper,
createSnakeCaseMapper,
StringMapper,
} from './camel-case.js'
export interface CamelCasePluginOptions {
/**
* If true, camelCase is transformed into upper case SNAKE_CASE.
* For example `fooBar => FOO_BAR` and `FOO_BAR => fooBar`
*
* Defaults to false.
*/
upperCase?: boolean
/**
* If true, an underscore is added before each digit when converting
* camelCase to snake_case. For example `foo12Bar => foo_12_bar` and
* `foo_12_bar => foo12Bar`
*
* Defaults to false.
*/
underscoreBeforeDigits?: boolean
/**
* If true, an underscore is added between consecutive upper case
* letters when converting from camelCase to snake_case. For example
* `fooBAR => foo_b_a_r` and `foo_b_a_r => fooBAR`.
*
* Defaults to false.
*/
underscoreBetweenUppercaseLetters?: boolean
}
/**
* A plugin that converts snake_case identifiers in the database into
* camelCase in the javascript side.
*
* For example let's assume we have a table called `person_table`
* with columns `first_name` and `last_name` in the database. When
* using `CamelCasePlugin` we would setup Kysely like this:
*
* ```ts
* interface Person {
* firstName: string
* lastName: string
* }
*
* interface Database {
* personTable: Person
* }
*
* const db = new Kysely<Database>({
* dialect: new PostgresDialect({
* database: 'kysely_test',
* host: 'localhost',
* }),
* plugins: [
* new CamelCasePlugin()
* ]
* })
*
* const person = await db.selectFrom('personTable')
* .where('firstName', '=', 'Arnold')
* .select(['firstName', 'lastName'])
* .executeTakeFirst()
*
* // generated sql:
* // select first_name, last_name from person_table where first_name = $1
*
* if (person) {
* console.log(person.firstName)
* }
* ```
*
* As you can see from the example, __everything__ needs to be defined
* in camelCase in the typescript code: the table names, the columns,
* schemas, __everything__. When using the `CamelCasePlugin` Kysely
* works as if the database was defined in camelCase.
*
* There are various options you can give to the plugin to modify
* the way identifiers are converted. See {@link CamelCasePluginOptions}.
* If those options are not enough, you can override this plugin's
* `snakeCase` and `camelCase` methods to make the conversion exactly
* the way you like:
*
* ```ts
* class MyCamelCasePlugin extends CamelCasePlugin {
* protected override snakeCase(str: string): string {
* return mySnakeCase(str)
* }
*
* protected override camelCase(str: string): string {
* return myCamelCase(str)
* }
* }
* ```
*/
export class CamelCasePlugin implements KyselyPlugin {
readonly #camelCase: StringMapper
readonly #snakeCase: StringMapper
readonly #snakeCaseTransformer: SnakeCaseTransformer
constructor(opt: CamelCasePluginOptions = {}) {
this.#camelCase = createCamelCaseMapper(opt)
this.#snakeCase = createSnakeCaseMapper(opt)
this.#snakeCaseTransformer = new SnakeCaseTransformer(
this.snakeCase.bind(this)
)
}
transformQuery(args: PluginTransformQueryArgs): RootOperationNode {
return this.#snakeCaseTransformer.transformNode(args.node)
}
async transformResult(
args: PluginTransformResultArgs
): Promise<QueryResult<UnknownRow>> {
if (args.result.rows && Array.isArray(args.result.rows)) {
return {
...args.result,
rows: args.result.rows.map((row) => this.mapRow(row)),
}
}
return args.result
}
protected mapRow(row: UnknownRow): UnknownRow {
return Object.keys(row).reduce<UnknownRow>((obj, key) => {
let value = row[key]
if (Array.isArray(value)) {
value = value.map((it) => (canMap(it) ? this.mapRow(it) : it))
} else if (canMap(value)) {
value = this.mapRow(value)
}
obj[this.camelCase(key)] = value
return obj
}, {})
}
protected snakeCase(str: string): string {
return this.#snakeCase(str)
}
protected camelCase(str: string): string {
return this.#camelCase(str)
}
}
function canMap(obj: unknown): obj is Record<string, unknown> {
return (
isObject(obj) && !isDate(obj) && !isBuffer(obj) && !isArrayBufferOrView(obj)
)
}