@cuboapp/database 1.0.6 → 1.0.7
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/package.json +13 -3
- package/src/db/index.ts +186 -4
- package/src/db/utils.ts +73 -50
- package/.prettierrc +0 -8
- package/tsconfig.build.json +0 -4
- package/tsconfig.json +0 -21
package/package.json
CHANGED
|
@@ -1,17 +1,27 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@cuboapp/database",
|
|
3
|
-
"version": "1.0.
|
|
3
|
+
"version": "1.0.7",
|
|
4
4
|
"description": "Database methods",
|
|
5
5
|
"main": "src/index.ts",
|
|
6
|
-
"
|
|
6
|
+
"files": [
|
|
7
|
+
"src"
|
|
8
|
+
],
|
|
9
|
+
"repository": {
|
|
10
|
+
"type": "git",
|
|
11
|
+
"url": "git@cuboapp.gitlab.yandexcloud.net:cubo/database.git"
|
|
12
|
+
},
|
|
7
13
|
"author": "CuboSoft",
|
|
8
14
|
"license": "MIT",
|
|
9
15
|
"type": "module",
|
|
10
16
|
"publishConfig": {
|
|
11
17
|
"access": "public"
|
|
12
18
|
},
|
|
19
|
+
"scripts": {
|
|
20
|
+
"clean": "rm -rf dist",
|
|
21
|
+
"build": "npm run clean && tsc -p tsconfig.build.json"
|
|
22
|
+
},
|
|
13
23
|
"dependencies": {
|
|
14
|
-
"sequelize": "^6.37.
|
|
24
|
+
"sequelize": "^6.37.8"
|
|
15
25
|
},
|
|
16
26
|
"devDependencies": {
|
|
17
27
|
"@types/node": "^24.0.0",
|
package/src/db/index.ts
CHANGED
|
@@ -1,14 +1,14 @@
|
|
|
1
1
|
import { Options, QueryTypes, Sequelize, Transaction } from 'sequelize'
|
|
2
2
|
|
|
3
3
|
import { DatabaseOptions } from './types'
|
|
4
|
-
import { dbPrepareInsertQueryString, dbPrepareUpdateQueryString } from './utils'
|
|
4
|
+
import { dbPrepareBatchInsertQueryString, dbPrepareInsertQueryString, dbPrepareUpdateQueryString } from './utils'
|
|
5
5
|
|
|
6
6
|
export * from 'sequelize'
|
|
7
7
|
export * from './types'
|
|
8
8
|
export * from './utils'
|
|
9
9
|
|
|
10
10
|
export class Database {
|
|
11
|
-
public connection
|
|
11
|
+
public connection!: Sequelize
|
|
12
12
|
|
|
13
13
|
constructor(
|
|
14
14
|
private connectOptions: Options,
|
|
@@ -90,7 +90,7 @@ export class Database {
|
|
|
90
90
|
return []
|
|
91
91
|
}
|
|
92
92
|
|
|
93
|
-
let pk = []
|
|
93
|
+
let pk: string[] = []
|
|
94
94
|
|
|
95
95
|
switch (this.connectOptions.dialect) {
|
|
96
96
|
case 'postgres':
|
|
@@ -101,7 +101,7 @@ export class Database {
|
|
|
101
101
|
break
|
|
102
102
|
}
|
|
103
103
|
|
|
104
|
-
return pkType === 'number' ? pk.map((r) => +r) : pk
|
|
104
|
+
return (pkType === 'number' ? pk.map((r) => +r) : pk) as T[]
|
|
105
105
|
})
|
|
106
106
|
}
|
|
107
107
|
|
|
@@ -160,5 +160,187 @@ export class Database {
|
|
|
160
160
|
return result as unknown as T[]
|
|
161
161
|
}
|
|
162
162
|
}
|
|
163
|
+
|
|
164
|
+
return []
|
|
165
|
+
}
|
|
166
|
+
|
|
167
|
+
/**
|
|
168
|
+
* Inserts many rows in a single statement. The column set is the union of keys
|
|
169
|
+
* across all rows; columns absent from a given row are written as `null`.
|
|
170
|
+
*
|
|
171
|
+
* For postgres every primary key is returned (via `RETURNING`). MySQL only
|
|
172
|
+
* surfaces the first auto-increment id of the batch, so the returned array
|
|
173
|
+
* holds that single id.
|
|
174
|
+
*/
|
|
175
|
+
public async createMany<T extends number | string = number>(
|
|
176
|
+
table: string,
|
|
177
|
+
arInsert: any[],
|
|
178
|
+
opts?: { log?: boolean; transaction?: Transaction; pk_key?: string; pk_type?: 'string' | 'number' }
|
|
179
|
+
): Promise<T[]> {
|
|
180
|
+
if (!Array.isArray(arInsert) || arInsert.length === 0) {
|
|
181
|
+
throw 'QUERY: db createMany insert array is empty'
|
|
182
|
+
}
|
|
183
|
+
|
|
184
|
+
const rows = arInsert.filter((row) => row && Object.keys(row).length > 0)
|
|
185
|
+
|
|
186
|
+
if (rows.length === 0) {
|
|
187
|
+
throw 'QUERY: db createMany insert array is empty'
|
|
188
|
+
}
|
|
189
|
+
|
|
190
|
+
const { keys, values, replacements } = dbPrepareBatchInsertQueryString(rows)
|
|
191
|
+
|
|
192
|
+
const pkKey = opts?.pk_key ?? 'id'
|
|
193
|
+
const pkType = opts?.pk_type ?? 'number'
|
|
194
|
+
|
|
195
|
+
let sql = ''
|
|
196
|
+
|
|
197
|
+
switch (this.connectOptions.dialect) {
|
|
198
|
+
case 'postgres':
|
|
199
|
+
if (!!pkKey) {
|
|
200
|
+
sql = `insert into ${table} (${keys}) values ${values} returning ${pkKey}`
|
|
201
|
+
} else {
|
|
202
|
+
sql = `insert into ${table} (${keys}) values ${values}`
|
|
203
|
+
}
|
|
204
|
+
break
|
|
205
|
+
case 'mysql':
|
|
206
|
+
sql = `insert into ${table} (${keys}) values ${values}`
|
|
207
|
+
break
|
|
208
|
+
default:
|
|
209
|
+
throw 'QUERY: db createMany unsupported dialect'
|
|
210
|
+
}
|
|
211
|
+
|
|
212
|
+
if (opts?.log) {
|
|
213
|
+
console.warn(`CREATE MANY QUERY: ${sql} [${JSON.stringify(replacements)}]`)
|
|
214
|
+
}
|
|
215
|
+
|
|
216
|
+
return this.connection
|
|
217
|
+
.query(sql, {
|
|
218
|
+
type: QueryTypes.INSERT,
|
|
219
|
+
replacements,
|
|
220
|
+
transaction: opts?.transaction
|
|
221
|
+
})
|
|
222
|
+
.then((res: any) => {
|
|
223
|
+
if (!pkKey) {
|
|
224
|
+
return []
|
|
225
|
+
}
|
|
226
|
+
|
|
227
|
+
let pk: string[] = []
|
|
228
|
+
|
|
229
|
+
switch (this.connectOptions.dialect) {
|
|
230
|
+
case 'postgres':
|
|
231
|
+
pk = (res?.[0] || []).map((r: any) => r[pkKey]) as string[]
|
|
232
|
+
break
|
|
233
|
+
case 'mysql':
|
|
234
|
+
pk = (res?.[0] != null ? [res[0]] : []) as string[]
|
|
235
|
+
break
|
|
236
|
+
}
|
|
237
|
+
|
|
238
|
+
return (pkType === 'number' ? pk.map((r) => +r) : pk) as T[]
|
|
239
|
+
})
|
|
240
|
+
}
|
|
241
|
+
|
|
242
|
+
/**
|
|
243
|
+
* Applies a set of updates atomically. Each item carries its own `where` clause,
|
|
244
|
+
* its `where` replacements and the columns to set, so different rows can receive
|
|
245
|
+
* different values. When no transaction is supplied a managed one is created so
|
|
246
|
+
* the whole batch commits or rolls back together. Returns the flattened primary
|
|
247
|
+
* keys of every affected row.
|
|
248
|
+
*/
|
|
249
|
+
public async updateMany<T = number | string>(
|
|
250
|
+
table: string,
|
|
251
|
+
arUpdate: Array<{ where: string; replacements?: Record<string, any>; values: Record<string, any> }>,
|
|
252
|
+
opts?: { log?: boolean; transaction?: Transaction; pk_key?: string; pk_type?: 'string' | 'number' }
|
|
253
|
+
): Promise<T[]> {
|
|
254
|
+
if (!Array.isArray(arUpdate) || arUpdate.length === 0) {
|
|
255
|
+
throw 'QUERY: db updateMany update array is empty'
|
|
256
|
+
}
|
|
257
|
+
|
|
258
|
+
const run = async (transaction: Transaction): Promise<T[]> => {
|
|
259
|
+
const ids: T[] = []
|
|
260
|
+
|
|
261
|
+
for (const item of arUpdate) {
|
|
262
|
+
if (!item?.values || Object.keys(item.values).length === 0) {
|
|
263
|
+
continue
|
|
264
|
+
}
|
|
265
|
+
|
|
266
|
+
const res = await this.update<T>(table, item.where, item.replacements ?? {}, item.values, {
|
|
267
|
+
...opts,
|
|
268
|
+
transaction
|
|
269
|
+
})
|
|
270
|
+
|
|
271
|
+
if (res?.length) {
|
|
272
|
+
ids.push(...res)
|
|
273
|
+
}
|
|
274
|
+
}
|
|
275
|
+
|
|
276
|
+
return ids
|
|
277
|
+
}
|
|
278
|
+
|
|
279
|
+
if (opts?.transaction) {
|
|
280
|
+
return run(opts.transaction)
|
|
281
|
+
}
|
|
282
|
+
|
|
283
|
+
return this.connection.transaction((transaction) => run(transaction))
|
|
284
|
+
}
|
|
285
|
+
|
|
286
|
+
/**
|
|
287
|
+
* Deletes many rows in a single statement, matched by a list of primary keys
|
|
288
|
+
* (`where <pk_key> in (...)`). For postgres the keys actually removed are
|
|
289
|
+
* returned (via `RETURNING`); MySQL has no `RETURNING`, so it resolves to an
|
|
290
|
+
* empty array.
|
|
291
|
+
*/
|
|
292
|
+
public async deleteMany<T extends number | string = number>(
|
|
293
|
+
table: string,
|
|
294
|
+
ids: Array<number | string>,
|
|
295
|
+
opts?: { log?: boolean; transaction?: Transaction; pk_key?: string; pk_type?: 'string' | 'number' }
|
|
296
|
+
): Promise<T[]> {
|
|
297
|
+
if (!Array.isArray(ids) || ids.length === 0) {
|
|
298
|
+
throw 'QUERY: db deleteMany id array is empty'
|
|
299
|
+
}
|
|
300
|
+
|
|
301
|
+
const pkKey = opts?.pk_key ?? 'id'
|
|
302
|
+
const pkType = opts?.pk_type ?? 'number'
|
|
303
|
+
|
|
304
|
+
const replacements: Record<string, any> = {}
|
|
305
|
+
|
|
306
|
+
const placeholders = ids.map((id, index) => {
|
|
307
|
+
const name = `${pkKey}_${index}`
|
|
308
|
+
replacements[name] = id
|
|
309
|
+
return `:${name}`
|
|
310
|
+
})
|
|
311
|
+
|
|
312
|
+
let sql = ''
|
|
313
|
+
let queryType: QueryTypes
|
|
314
|
+
|
|
315
|
+
switch (this.connectOptions.dialect) {
|
|
316
|
+
case 'postgres':
|
|
317
|
+
sql = `delete from ${table} where ${pkKey} in (${placeholders.join(', ')}) returning ${pkKey}`
|
|
318
|
+
queryType = QueryTypes.SELECT
|
|
319
|
+
break
|
|
320
|
+
case 'mysql':
|
|
321
|
+
sql = `delete from ${table} where ${pkKey} in (${placeholders.join(', ')})`
|
|
322
|
+
queryType = QueryTypes.DELETE
|
|
323
|
+
break
|
|
324
|
+
default:
|
|
325
|
+
throw 'QUERY: db deleteMany unsupported dialect'
|
|
326
|
+
}
|
|
327
|
+
|
|
328
|
+
if (opts?.log) {
|
|
329
|
+
console.warn(`DELETE MANY QUERY: ${sql} [${JSON.stringify(replacements)}]`)
|
|
330
|
+
}
|
|
331
|
+
|
|
332
|
+
const result: any = await this.connection.query(sql, {
|
|
333
|
+
type: queryType,
|
|
334
|
+
replacements,
|
|
335
|
+
transaction: opts?.transaction
|
|
336
|
+
})
|
|
337
|
+
|
|
338
|
+
if (this.connectOptions.dialect !== 'postgres') {
|
|
339
|
+
return []
|
|
340
|
+
}
|
|
341
|
+
|
|
342
|
+
const pk = (result || []).map((r: any) => r[pkKey]) as string[]
|
|
343
|
+
|
|
344
|
+
return (pkType === 'number' ? pk.map((r) => +r) : pk) as T[]
|
|
163
345
|
}
|
|
164
346
|
}
|
package/src/db/utils.ts
CHANGED
|
@@ -3,36 +3,49 @@ import { literal, Options, QueryTypes, Sequelize, Transaction } from 'sequelize'
|
|
|
3
3
|
import { Database } from '.'
|
|
4
4
|
import { DatabaseOptions } from './types'
|
|
5
5
|
|
|
6
|
+
/**
|
|
7
|
+
* Normalizes a JS value into something safe to pass to Sequelize as a replacement:
|
|
8
|
+
* empty strings / NaN / nullish objects collapse to `null`, Buffers become raw
|
|
9
|
+
* literals, and plain arrays/objects are JSON-stringified. Shared by every
|
|
10
|
+
* insert/update query builder so the coercion rules stay in one place.
|
|
11
|
+
*/
|
|
12
|
+
export function dbNormalizeValue(value: any) {
|
|
13
|
+
if (typeof value === 'string') {
|
|
14
|
+
return value ? value : null
|
|
15
|
+
}
|
|
16
|
+
|
|
17
|
+
if (typeof value === 'number') {
|
|
18
|
+
return !isNaN(value) ? value : null
|
|
19
|
+
}
|
|
20
|
+
|
|
21
|
+
if (typeof value === 'boolean') {
|
|
22
|
+
return value
|
|
23
|
+
}
|
|
24
|
+
|
|
25
|
+
if (Array.isArray(value) || typeof value === 'object') {
|
|
26
|
+
if (Buffer.isBuffer(value)) {
|
|
27
|
+
return literal(value.toString('utf-8'))
|
|
28
|
+
}
|
|
29
|
+
|
|
30
|
+
if (['Literal', 'Date'].includes(value?.constructor?.name)) {
|
|
31
|
+
return value
|
|
32
|
+
}
|
|
33
|
+
|
|
34
|
+
return value ? JSON.stringify(value) : null
|
|
35
|
+
}
|
|
36
|
+
|
|
37
|
+
return null
|
|
38
|
+
}
|
|
39
|
+
|
|
6
40
|
export function dbPrepareUpdateQueryString(obj: Record<string, any>, prefix = '') {
|
|
7
41
|
const keys: string[] = []
|
|
8
42
|
const replacements: Record<string, any> = {}
|
|
9
43
|
|
|
10
44
|
for (const key in obj) {
|
|
11
|
-
const
|
|
12
|
-
|
|
13
|
-
keys.push(`${key + (prefix ? '_' + prefix : '')} = :${key + (prefix ? '_' + prefix : '')}`)
|
|
14
|
-
|
|
15
|
-
let val = null
|
|
16
|
-
|
|
17
|
-
if (typeof value === 'string') {
|
|
18
|
-
val = !!value ? value : null
|
|
19
|
-
} else if (typeof value === 'number') {
|
|
20
|
-
val = !isNaN(value) ? value : null
|
|
21
|
-
} else if (typeof value === 'boolean') {
|
|
22
|
-
val = value
|
|
23
|
-
} else if (Array.isArray(value) || typeof value === 'object') {
|
|
24
|
-
if (Buffer.isBuffer(value)) {
|
|
25
|
-
val = literal(value.toString('utf-8'))
|
|
26
|
-
} else {
|
|
27
|
-
if (['Literal', 'Date'].includes(value?.constructor?.name)) {
|
|
28
|
-
val = value
|
|
29
|
-
} else {
|
|
30
|
-
val = value ? JSON.stringify(value) : null
|
|
31
|
-
}
|
|
32
|
-
}
|
|
33
|
-
}
|
|
45
|
+
const name = key + (prefix ? '_' + prefix : '')
|
|
34
46
|
|
|
35
|
-
|
|
47
|
+
keys.push(`${name} = :${name}`)
|
|
48
|
+
replacements[name] = dbNormalizeValue(obj[key])
|
|
36
49
|
}
|
|
37
50
|
|
|
38
51
|
return { query: keys.join(', '), replacements }
|
|
@@ -43,38 +56,48 @@ export function dbPrepareInsertQueryString(obj: Record<string, any>, prefix = ''
|
|
|
43
56
|
const replacements: Record<string, any> = {}
|
|
44
57
|
|
|
45
58
|
for (const key in obj) {
|
|
46
|
-
const
|
|
47
|
-
|
|
48
|
-
values.push(`:${key + (prefix ? '_' + prefix : '')}`)
|
|
49
|
-
|
|
50
|
-
let val = null
|
|
51
|
-
|
|
52
|
-
// console.log(key, typeof value, value, value?.constructor?.name, value?.name)
|
|
53
|
-
|
|
54
|
-
if (typeof value === 'string') {
|
|
55
|
-
val = !!value ? value : null
|
|
56
|
-
} else if (typeof value === 'number') {
|
|
57
|
-
val = !isNaN(value) ? value : null
|
|
58
|
-
} else if (typeof value === 'boolean') {
|
|
59
|
-
val = value
|
|
60
|
-
} else if (Array.isArray(value) || typeof value === 'object') {
|
|
61
|
-
if (Buffer.isBuffer(value)) {
|
|
62
|
-
val = literal(value.toString('utf-8'))
|
|
63
|
-
} else {
|
|
64
|
-
if (['Literal', 'Date'].includes(value?.constructor?.name)) {
|
|
65
|
-
val = value
|
|
66
|
-
} else {
|
|
67
|
-
val = value ? JSON.stringify(value) : null
|
|
68
|
-
}
|
|
69
|
-
}
|
|
70
|
-
}
|
|
59
|
+
const name = key + (prefix ? '_' + prefix : '')
|
|
71
60
|
|
|
72
|
-
|
|
61
|
+
values.push(`:${name}`)
|
|
62
|
+
replacements[name] = dbNormalizeValue(obj[key])
|
|
73
63
|
}
|
|
74
64
|
|
|
75
65
|
return { keys: Object.keys(obj).join(', '), values: values.join(','), replacements }
|
|
76
66
|
}
|
|
77
67
|
|
|
68
|
+
/**
|
|
69
|
+
* Builds the column list and a multi-row `VALUES (...), (...)` clause for a batch
|
|
70
|
+
* insert. The column set is the union of keys across every row (first-seen order);
|
|
71
|
+
* columns missing from a given row are inserted as `null`. Each placeholder is
|
|
72
|
+
* suffixed with the row index (e.g. `:name_0`, `:name_1`) so replacements never
|
|
73
|
+
* collide.
|
|
74
|
+
*/
|
|
75
|
+
export function dbPrepareBatchInsertQueryString(rows: Record<string, any>[]) {
|
|
76
|
+
const columns: string[] = []
|
|
77
|
+
|
|
78
|
+
for (const row of rows) {
|
|
79
|
+
for (const key in row) {
|
|
80
|
+
if (!columns.includes(key)) {
|
|
81
|
+
columns.push(key)
|
|
82
|
+
}
|
|
83
|
+
}
|
|
84
|
+
}
|
|
85
|
+
|
|
86
|
+
const replacements: Record<string, any> = {}
|
|
87
|
+
|
|
88
|
+
const values = rows.map((row, index) => {
|
|
89
|
+
const placeholders = columns.map((column) => {
|
|
90
|
+
const name = `${column}_${index}`
|
|
91
|
+
replacements[name] = dbNormalizeValue(row[column])
|
|
92
|
+
return `:${name}`
|
|
93
|
+
})
|
|
94
|
+
|
|
95
|
+
return `(${placeholders.join(', ')})`
|
|
96
|
+
})
|
|
97
|
+
|
|
98
|
+
return { keys: columns.join(', '), values: values.join(', '), replacements }
|
|
99
|
+
}
|
|
100
|
+
|
|
78
101
|
export async function dbCreate<T = number>(
|
|
79
102
|
db: Sequelize,
|
|
80
103
|
table: string,
|
package/.prettierrc
DELETED
package/tsconfig.build.json
DELETED
package/tsconfig.json
DELETED
|
@@ -1,21 +0,0 @@
|
|
|
1
|
-
{
|
|
2
|
-
"compilerOptions": {
|
|
3
|
-
"module": "commonjs",
|
|
4
|
-
"declaration": true,
|
|
5
|
-
"removeComments": true,
|
|
6
|
-
"emitDecoratorMetadata": true,
|
|
7
|
-
"experimentalDecorators": true,
|
|
8
|
-
"allowSyntheticDefaultImports": true,
|
|
9
|
-
"target": "ES2021",
|
|
10
|
-
"sourceMap": true,
|
|
11
|
-
"outDir": "./dist",
|
|
12
|
-
"baseUrl": "./",
|
|
13
|
-
"incremental": true,
|
|
14
|
-
"skipLibCheck": true,
|
|
15
|
-
"paths": {
|
|
16
|
-
"@/*": ["src/*"]
|
|
17
|
-
}
|
|
18
|
-
},
|
|
19
|
-
"include": ["src/**/*"],
|
|
20
|
-
"exclude": ["node_modules", "dist"]
|
|
21
|
-
}
|