@platformatic/sql-mapper 0.6.1 → 0.7.0

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/lib/entity.js CHANGED
@@ -116,6 +116,24 @@ function createMapper (defaultDb, sql, log, table, fields, primaryKey, relations
116
116
  }
117
117
  }
118
118
 
119
+ async function updateMany (args) {
120
+ const db = args.tx || defaultDb
121
+ const fieldsToRetrieve = computeFields(args.fields).map((f) => sql.ident(f))
122
+ if (args.input === undefined) {
123
+ throw new Error('Input not provided.')
124
+ }
125
+ const input = fixInput(args.input)
126
+ let now
127
+ if (autoTimestamp && fields.updated_at) {
128
+ now = new Date()
129
+ input.updated_at = now
130
+ }
131
+ const criteria = computeCriteria(args)
132
+
133
+ const res = await queries.updateMany(db, sql, table, criteria, input, fieldsToRetrieve)
134
+ return res.map(fixOutput)
135
+ }
136
+
119
137
  function computeFields (fields) {
120
138
  if (!fields) {
121
139
  return Object.values(inputToFieldMap)
@@ -264,7 +282,8 @@ function createMapper (defaultDb, sql, log, table, fields, primaryKey, relations
264
282
  count,
265
283
  insert,
266
284
  save,
267
- delete: _delete
285
+ delete: _delete,
286
+ updateMany
268
287
  }
269
288
  }
270
289
 
@@ -276,12 +295,30 @@ async function buildEntity (db, sql, log, table, queries, autoTimestamp, ignore)
276
295
  sqlType: column.udt_name,
277
296
  isNullable: column.is_nullable === 'YES'
278
297
  }
298
+
299
+ // To get enum values in mysql and mariadb
300
+ /* istanbul ignore next */
301
+ if (column.udt_name === 'enum') {
302
+ acc[column.column_name].enum = column.column_type.match(/'(.+?)'/g).map(enumValue => enumValue.slice(1, enumValue.length - 1))
303
+ }
304
+
279
305
  if (autoTimestamp && (column.column_name === 'updated_at' || column.column_name === 'inserted_at')) {
280
306
  acc[column.column_name].autoTimestamp = true
281
307
  }
282
308
  return acc
283
309
  }, {})
284
-
310
+ // To get enum values in pg
311
+ /* istanbul ignore next */
312
+ if (db.isPg) {
313
+ const enums = await queries.listEnumValues(db, sql, table)
314
+ for (const enumValue of enums) {
315
+ if (!fields[enumValue.column_name].enum) {
316
+ fields[enumValue.column_name].enum = [enumValue.enumlabel]
317
+ } else {
318
+ fields[enumValue.column_name].enum.push(enumValue.enumlabel)
319
+ }
320
+ }
321
+ }
285
322
  const currentRelations = []
286
323
 
287
324
  const constraintsList = await queries.listConstraints(db, sql, table)
@@ -11,7 +11,7 @@ async function listTables (db, sql) {
11
11
 
12
12
  async function listColumns (db, sql, table) {
13
13
  const res = await db.query(sql`
14
- SELECT column_name as column_name, data_type as udt_name, is_nullable as is_nullable
14
+ SELECT column_name as column_name, data_type as udt_name, is_nullable as is_nullable, column_type as column_type
15
15
  FROM information_schema.columns
16
16
  WHERE table_name = ${table}
17
17
  AND table_schema = (SELECT DATABASE())
@@ -54,9 +54,41 @@ async function updateOne (db, sql, table, input, primaryKey, fieldsToRetrieve) {
54
54
  return res[0]
55
55
  }
56
56
 
57
+ async function updateMany (db, sql, table, criteria, input, fieldsToRetrieve) {
58
+ const pairs = Object.keys(input).map((key) => {
59
+ const value = input[key]
60
+ return sql`${sql.ident(key)} = ${value}`
61
+ })
62
+
63
+ const selectIds = sql`
64
+ SELECT id
65
+ FROM ${sql.ident(table)}
66
+ WHERE ${sql.join(criteria, sql` AND `)}
67
+ `
68
+ const resp = await db.query(selectIds)
69
+ const ids = resp.map(({ id }) => id)
70
+
71
+ const update = sql`
72
+ UPDATE ${sql.ident(table)}
73
+ SET ${sql.join(pairs, sql`, `)}
74
+ WHERE ${sql.join(criteria, sql` AND `)}
75
+ `
76
+
77
+ await db.query(update)
78
+
79
+ const select = sql`
80
+ SELECT ${sql.join(fieldsToRetrieve, sql`, `)}
81
+ FROM ${sql.ident(table)}
82
+ WHERE id IN (${ids});
83
+ `
84
+ const res = await db.query(select)
85
+ return res
86
+ }
87
+
57
88
  module.exports = {
58
89
  listTables,
59
90
  listColumns,
60
91
  listConstraints,
61
- updateOne
92
+ updateOne,
93
+ updateMany
62
94
  }
package/lib/queries/pg.js CHANGED
@@ -77,3 +77,17 @@ async function updateOne (db, sql, table, input, primaryKey, fieldsToRetrieve) {
77
77
  }
78
78
 
79
79
  module.exports.updateOne = updateOne
80
+
81
+ module.exports.updateMany = shared.updateMany
82
+
83
+ async function listEnumValues (db, sql, table) {
84
+ return (await db.query(sql`
85
+ SELECT udt_name, enumlabel, column_name
86
+ FROM pg_enum e
87
+ JOIN pg_type t ON e.enumtypid = t.oid
88
+ JOIN information_schema.columns c on c.udt_name = t.typname
89
+ WHERE table_name = ${table};
90
+ `))
91
+ }
92
+
93
+ module.exports.listEnumValues = listEnumValues
@@ -92,9 +92,25 @@ function insertPrep (inputs, inputToFieldMap, fields, sql) {
92
92
  return { keys, values }
93
93
  }
94
94
 
95
+ async function updateMany (db, sql, table, criteria, input, fieldsToRetrieve) {
96
+ const pairs = Object.keys(input).map((key) => {
97
+ const value = input[key]
98
+ return sql`${sql.ident(key)} = ${value}`
99
+ })
100
+ const update = sql`
101
+ UPDATE ${sql.ident(table)}
102
+ SET ${sql.join(pairs, sql`, `)}
103
+ WHERE ${sql.join(criteria, sql` AND `)}
104
+ RETURNING ${sql.join(fieldsToRetrieve, sql`, `)}
105
+ `
106
+ const res = await db.query(update)
107
+ return res
108
+ }
109
+
95
110
  module.exports = {
96
111
  insertOne,
97
112
  insertPrep,
98
113
  deleteAll,
99
- insertMany
114
+ insertMany,
115
+ updateMany
100
116
  }
@@ -1,6 +1,7 @@
1
1
  'use strict'
2
2
 
3
3
  const { randomUUID } = require('crypto')
4
+ const shared = require('./shared')
4
5
 
5
6
  async function listTables (db, sql) {
6
7
  const tables = await db.query(sql`
@@ -167,3 +168,5 @@ async function deleteAll (db, sql, table, criteria, fieldsToRetrieve) {
167
168
  }
168
169
 
169
170
  module.exports.deleteAll = deleteAll
171
+
172
+ module.exports.updateMany = shared.updateMany
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@platformatic/sql-mapper",
3
- "version": "0.6.1",
3
+ "version": "0.7.0",
4
4
  "description": "A data mapper utility for SQL databases",
5
5
  "main": "mapper.js",
6
6
  "repository": {
@@ -2,7 +2,7 @@
2
2
 
3
3
  const { test } = require('tap')
4
4
 
5
- const { clear, connInfo, isSQLite, isMysql } = require('./helper')
5
+ const { clear, connInfo, isSQLite, isMysql, isPg } = require('./helper')
6
6
  const { connect } = require('..')
7
7
  const fakeLogger = {
8
8
  trace: () => {},
@@ -604,3 +604,37 @@ test('include all fields', async ({ pass, teardown, same, equal }) => {
604
604
  }])
605
605
  }
606
606
  })
607
+
608
+ test('include possible values of enum columns', { skip: isSQLite }, async ({ same, teardown }) => {
609
+ async function onDatabaseLoad (db, sql) {
610
+ await clear(db, sql)
611
+ teardown(() => db.dispose())
612
+
613
+ if (isPg) {
614
+ await db.query(sql`
615
+ CREATE TYPE pagetype as enum ('blank', 'non-blank');
616
+ CREATE TABLE pages (
617
+ id INTEGER PRIMARY KEY,
618
+ title VARCHAR(42),
619
+ type pagetype
620
+ );`)
621
+ } else {
622
+ await db.query(sql`CREATE TABLE pages (
623
+ id INTEGER PRIMARY KEY,
624
+ title VARCHAR(42),
625
+ type ENUM ('blank', 'non-blank')
626
+ );
627
+ `)
628
+ }
629
+ }
630
+ const mapper = await connect({
631
+ connectionString: connInfo.connectionString,
632
+ log: fakeLogger,
633
+ onDatabaseLoad,
634
+ ignore: {},
635
+ hooks: {}
636
+ })
637
+ const pageEntity = mapper.entities.page
638
+ const typeField = pageEntity.fields.type
639
+ same(typeField.enum, ['blank', 'non-blank'])
640
+ })
package/test/helper.js CHANGED
@@ -63,4 +63,9 @@ module.exports.clear = async function (db, sql) {
63
63
  await db.query(sql`DROP TABLE versions`)
64
64
  } catch {
65
65
  }
66
+
67
+ try {
68
+ await db.query(sql`DROP TYPE pagetype`)
69
+ } catch {
70
+ }
66
71
  }
@@ -0,0 +1,318 @@
1
+ 'use strict'
2
+
3
+ const { test } = require('tap')
4
+ const { connect } = require('..')
5
+ const { clear, connInfo, isSQLite, isMysql } = require('./helper')
6
+ const { setTimeout } = require('timers/promises')
7
+ const fakeLogger = {
8
+ trace: () => {},
9
+ error: () => {}
10
+ }
11
+
12
+ test('updateMany successful', async ({ pass, teardown, same }) => {
13
+ const mapper = await connect({
14
+ ...connInfo,
15
+ log: fakeLogger,
16
+ async onDatabaseLoad (db, sql) {
17
+ teardown(() => db.dispose())
18
+ pass('onDatabaseLoad called')
19
+
20
+ await clear(db, sql)
21
+
22
+ if (isSQLite) {
23
+ await db.query(sql`CREATE TABLE posts (
24
+ id INTEGER PRIMARY KEY,
25
+ title VARCHAR(42),
26
+ long_text TEXT,
27
+ counter INTEGER
28
+ );`)
29
+ } else {
30
+ await db.query(sql`CREATE TABLE posts (
31
+ id SERIAL PRIMARY KEY,
32
+ title VARCHAR(42),
33
+ long_text TEXT,
34
+ counter INTEGER
35
+ );`)
36
+ }
37
+ }
38
+ })
39
+
40
+ const entity = mapper.entities.post
41
+
42
+ const posts = [{
43
+ title: 'Dog',
44
+ longText: 'Foo',
45
+ counter: 10
46
+ }, {
47
+ title: 'Cat',
48
+ longText: 'Bar',
49
+ counter: 20
50
+ }, {
51
+ title: 'Mouse',
52
+ longText: 'Baz',
53
+ counter: 30
54
+ }, {
55
+ title: 'Duck',
56
+ longText: 'A duck tale',
57
+ counter: 40
58
+ }]
59
+
60
+ await entity.insert({
61
+ inputs: posts
62
+ })
63
+
64
+ await entity.updateMany({
65
+ where: {
66
+ counter: {
67
+ gte: 30
68
+ }
69
+ },
70
+ input: {
71
+ title: 'Updated title'
72
+ }
73
+ })
74
+
75
+ const updatedPosts = await entity.find({})
76
+
77
+ same(updatedPosts, [{
78
+ id: '1',
79
+ title: 'Dog',
80
+ longText: 'Foo',
81
+ counter: 10
82
+ }, {
83
+ id: '2',
84
+ title: 'Cat',
85
+ longText: 'Bar',
86
+ counter: 20
87
+ }, {
88
+ id: '3',
89
+ title: 'Updated title',
90
+ longText: 'Baz',
91
+ counter: 30
92
+ }, {
93
+ id: '4',
94
+ title: 'Updated title',
95
+ longText: 'A duck tale',
96
+ counter: 40
97
+ }])
98
+ })
99
+
100
+ test('updateMany will return the updated values', async ({ pass, teardown, same }) => {
101
+ const mapper = await connect({
102
+ ...connInfo,
103
+ log: fakeLogger,
104
+ async onDatabaseLoad (db, sql) {
105
+ teardown(() => db.dispose())
106
+ pass('onDatabaseLoad called')
107
+
108
+ await clear(db, sql)
109
+
110
+ if (isSQLite) {
111
+ await db.query(sql`CREATE TABLE posts (
112
+ id INTEGER PRIMARY KEY,
113
+ title VARCHAR(42),
114
+ long_text TEXT,
115
+ counter INTEGER
116
+ );`)
117
+ } else {
118
+ await db.query(sql`CREATE TABLE posts (
119
+ id SERIAL PRIMARY KEY,
120
+ title VARCHAR(42),
121
+ long_text TEXT,
122
+ counter INTEGER
123
+ );`)
124
+ }
125
+ }
126
+ })
127
+
128
+ const entity = mapper.entities.post
129
+
130
+ const posts = [{
131
+ title: 'Dog',
132
+ longText: 'Foo',
133
+ counter: 10
134
+ }, {
135
+ title: 'Cat',
136
+ longText: 'Bar',
137
+ counter: 20
138
+ }, {
139
+ title: 'Mouse',
140
+ longText: 'Baz',
141
+ counter: 30
142
+ }, {
143
+ title: 'Duck',
144
+ longText: 'A duck tale',
145
+ counter: 40
146
+ }]
147
+
148
+ await entity.insert({
149
+ inputs: posts
150
+ })
151
+
152
+ const updatedPosts = await entity.updateMany({
153
+ where: {
154
+ counter: {
155
+ gte: 30
156
+ }
157
+ },
158
+ input: {
159
+ title: 'Updated title'
160
+ },
161
+ fields: ['id', 'counter']
162
+ })
163
+
164
+ same(updatedPosts, [{
165
+ id: '3',
166
+ counter: 30
167
+ }, {
168
+ id: '4',
169
+ counter: 40
170
+ }])
171
+ })
172
+
173
+ test('updateMany missing input', async ({ pass, teardown, rejects }) => {
174
+ const mapper = await connect({
175
+ ...connInfo,
176
+ log: fakeLogger,
177
+ async onDatabaseLoad (db, sql) {
178
+ teardown(() => db.dispose())
179
+ pass('onDatabaseLoad called')
180
+
181
+ await clear(db, sql)
182
+
183
+ if (isSQLite) {
184
+ await db.query(sql`CREATE TABLE posts (
185
+ id INTEGER PRIMARY KEY,
186
+ title VARCHAR(42),
187
+ long_text TEXT,
188
+ counter INTEGER
189
+ );`)
190
+ } else {
191
+ await db.query(sql`CREATE TABLE posts (
192
+ id SERIAL PRIMARY KEY,
193
+ title VARCHAR(42),
194
+ long_text TEXT,
195
+ counter INTEGER
196
+ );`)
197
+ }
198
+ }
199
+ })
200
+
201
+ const entity = mapper.entities.post
202
+
203
+ const posts = [{
204
+ title: 'Dog',
205
+ longText: 'Foo',
206
+ counter: 10
207
+ }, {
208
+ title: 'Cat',
209
+ longText: 'Bar',
210
+ counter: 20
211
+ }, {
212
+ title: 'Mouse',
213
+ longText: 'Baz',
214
+ counter: 30
215
+ }, {
216
+ title: 'Duck',
217
+ longText: 'A duck tale',
218
+ counter: 40
219
+ }]
220
+
221
+ await entity.insert({
222
+ inputs: posts
223
+ })
224
+
225
+ rejects(entity.updateMany({
226
+ where: {
227
+ counter: {
228
+ gte: 30
229
+ }
230
+ }
231
+ }), new Error('Input not provided.'))
232
+ })
233
+
234
+ test('updateMany successful and update updated_at', async ({ pass, teardown, same, notSame }) => {
235
+ const mapper = await connect({
236
+ ...connInfo,
237
+ autoTimestamp: true,
238
+ log: fakeLogger,
239
+ async onDatabaseLoad (db, sql) {
240
+ teardown(() => db.dispose())
241
+ pass('onDatabaseLoad called')
242
+
243
+ await clear(db, sql)
244
+
245
+ if (isSQLite) {
246
+ await db.query(sql`CREATE TABLE posts (
247
+ id INTEGER PRIMARY KEY,
248
+ title VARCHAR(42),
249
+ long_text TEXT,
250
+ counter INTEGER,
251
+ inserted_at TIMESTAMP,
252
+ updated_at TIMESTAMP
253
+ );`)
254
+ } else if (isMysql) {
255
+ await db.query(sql`CREATE TABLE posts (
256
+ id SERIAL PRIMARY KEY,
257
+ title VARCHAR(42),
258
+ long_text TEXT,
259
+ counter INTEGER,
260
+ inserted_at TIMESTAMP NULL DEFAULT NULL,
261
+ updated_at TIMESTAMP NULL DEFAULT NULL
262
+ );`)
263
+ } else {
264
+ await db.query(sql`CREATE TABLE posts (
265
+ id SERIAL PRIMARY KEY,
266
+ title VARCHAR(42),
267
+ long_text TEXT,
268
+ counter INTEGER,
269
+ inserted_at TIMESTAMP,
270
+ updated_at TIMESTAMP
271
+ );`)
272
+ }
273
+ }
274
+ })
275
+
276
+ const entity = mapper.entities.post
277
+
278
+ const posts = [{
279
+ title: 'Dog',
280
+ longText: 'Foo',
281
+ counter: 10
282
+ }, {
283
+ title: 'Cat',
284
+ longText: 'Bar',
285
+ counter: 20
286
+ }, {
287
+ title: 'Mouse',
288
+ longText: 'Baz',
289
+ counter: 30
290
+ }, {
291
+ title: 'Duck',
292
+ longText: 'A duck tale',
293
+ counter: 40
294
+ }]
295
+
296
+ await entity.insert({
297
+ inputs: posts
298
+ })
299
+ const createdPost3 = (await entity.find({ where: { id: { eq: '3' } } }))[0]
300
+
301
+ await setTimeout(1000) // await 1s
302
+
303
+ await entity.updateMany({
304
+ where: {
305
+ counter: {
306
+ gte: 30
307
+ }
308
+ },
309
+ input: {
310
+ title: 'Updated title'
311
+ }
312
+ })
313
+
314
+ const updatedPost3 = (await entity.find({ where: { id: { eq: '3' } } }))[0]
315
+ same(updatedPost3.title, 'Updated title')
316
+ same(createdPost3.insertedAt, updatedPost3.insertedAt)
317
+ notSame(createdPost3.updatedAt, updatedPost3.updatedAt)
318
+ })