@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 CHANGED
@@ -1,17 +1,27 @@
1
1
  {
2
2
  "name": "@cuboapp/database",
3
- "version": "1.0.6",
3
+ "version": "1.0.7",
4
4
  "description": "Database methods",
5
5
  "main": "src/index.ts",
6
- "repository": "git@cuboapp.gitlab.yandexcloud.net:cubo/database.git",
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.7"
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: Sequelize
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 value = obj[key]
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
- replacements[key + (prefix ? '_' + prefix : '')] = val
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 value = obj[key]
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
- replacements[key + (prefix ? '_' + prefix : '')] = val
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
@@ -1,8 +0,0 @@
1
- {
2
- "trailingComma": "none",
3
- "tabWidth": 2,
4
- "semi": false,
5
- "singleQuote": true,
6
- "eslintIntegration": true,
7
- "printWidth": 140
8
- }
@@ -1,4 +0,0 @@
1
- {
2
- "extends": "./tsconfig.json",
3
- "exclude": ["node_modules", "test", "dist", "**/*spec.ts"]
4
- }
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
- }