@cap-js/postgres 1.4.1 → 1.5.1

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/CHANGELOG.md CHANGED
@@ -4,6 +4,27 @@
4
4
  - The format is based on [Keep a Changelog](http://keepachangelog.com/).
5
5
  - This project adheres to [Semantic Versioning](http://semver.org/).
6
6
 
7
+ ## [1.5.1](https://github.com/cap-js/cds-dbs/compare/postgres-v1.5.0...postgres-v1.5.1) (2024-02-16)
8
+
9
+
10
+ ### Fixed
11
+
12
+ * **`sqlite`:** Retain Error object for unique constraint violation ([#446](https://github.com/cap-js/cds-dbs/issues/446)) ([d27ee79](https://github.com/cap-js/cds-dbs/commit/d27ee79b4c4eea8522bf5dd2a288638f54029567))
13
+
14
+ ## [1.5.0](https://github.com/cap-js/cds-dbs/compare/postgres-v1.4.1...postgres-v1.5.0) (2024-02-02)
15
+
16
+
17
+ ### Added
18
+
19
+ * SELECT returns LargeBinaries as streams unless feature flag "stream_compat" is set ([#251](https://github.com/cap-js/cds-dbs/issues/251)) ([8165a4a](https://github.com/cap-js/cds-dbs/commit/8165a4a3f6bb21c970668c8873f9d9c662b43780))
20
+ * Support Readable Streams inside INSERT.entries ([#343](https://github.com/cap-js/cds-dbs/issues/343)) ([f6faf89](https://github.com/cap-js/cds-dbs/commit/f6faf8955b7888479c66f1727ade65b382611c2f))
21
+
22
+
23
+ ### Fixed
24
+
25
+ * switch Postgres from json to jsonb ([#402](https://github.com/cap-js/cds-dbs/issues/402)) ([c98a964](https://github.com/cap-js/cds-dbs/commit/c98a964a34232267aece337dc6f6bedf03e9891a))
26
+ * UPSERT for @cap-js/hana for entities with multiple keys ([#418](https://github.com/cap-js/cds-dbs/issues/418)) ([9bbac6e](https://github.com/cap-js/cds-dbs/commit/9bbac6ebbbddfa2f620833ce195eedeb0a79f43e))
27
+
7
28
  ## [1.4.1](https://github.com/cap-js/cds-dbs/compare/postgres-v1.4.0...postgres-v1.4.1) (2023-11-24)
8
29
 
9
30
 
@@ -13,6 +13,7 @@ class PostgresService extends SQLService {
13
13
  cds.options.dialect = 'postgres'
14
14
  }
15
15
  this.kind = 'postgres'
16
+ this._queryCache = {}
16
17
  return super.init(...arguments)
17
18
  }
18
19
 
@@ -78,7 +79,7 @@ class PostgresService extends SQLService {
78
79
  }
79
80
 
80
81
  return Promise.all([
81
- (await this.prepare(`SELECT set_config(key::text,$1->>key,false) FROM json_each($1);`)).run([
82
+ (await this.prepare(`SELECT set_config(key::text,$1->>key,false) FROM jsonb_each($1);`)).run([
82
83
  JSON.stringify(env),
83
84
  ]),
84
85
  ...(this.options?.credentials?.schema
@@ -135,12 +136,13 @@ GROUP BY k
135
136
  }
136
137
 
137
138
  prepare(sql) {
138
- const query = {
139
+ // Track queries name for postgres referencing prepare statements
140
+ // sha1 as it needs to be less then 63 character
141
+ const sha = crypto.createHash('sha1').update(sql).digest('hex')
142
+ const query = this._queryCache[sha] = this._queryCache[sha] || {
139
143
  _streams: 0,
140
144
  text: sql,
141
- // Track queries name for postgres referencing prepare statements
142
- // sha1 as it needs to be less then 63 characters
143
- name: crypto.createHash('sha1').update(sql).digest('hex'),
145
+ name: sha,
144
146
  }
145
147
  return {
146
148
  run: async values => {
@@ -194,7 +196,7 @@ GROUP BY k
194
196
  values.forEach((value, i) => {
195
197
  if (value instanceof Readable) {
196
198
  const streamID = query._streams++
197
- const isBinary = value.type === 'binary'
199
+ const isBinary = value.type !== 'json'
198
200
  const paramStream = new ParameterStream(query.name, streamID)
199
201
  if (isBinary) value.setEncoding('base64')
200
202
  value.pipe(paramStream)
@@ -281,14 +283,55 @@ GROUP BY k
281
283
  return super.onPlainSQL(req, next)
282
284
  }
283
285
 
286
+ async onSELECT({ query, data }) {
287
+ // workaround for chunking odata streaming
288
+ if (query.SELECT?.columns?.find(col => col.as === '$mediaContentType')) {
289
+ const columns = query.SELECT.columns
290
+ const index = columns.findIndex(col => query.elements[col.ref?.[col.ref.length - 1]].type === 'cds.LargeBinary')
291
+ const binary = columns[index]
292
+ // SELECT without binary column
293
+ columns.splice(index, 1)
294
+ const { sql, values } = this.cqn2sql(query, data)
295
+ let ps = this.prepare(sql)
296
+ let res = await ps.all(values)
297
+ if (res.length === 0) return
298
+ res = res.map(r => (typeof r._json_ === 'string' ? JSON.parse(r._json_) : r._json_ || r))[0]
299
+ // SELECT only binary column
300
+ query.SELECT.columns = [binary]
301
+ const { sql: streamSql, values: valuesStream } = this.cqn2sql(query, data)
302
+ ps = this.prepare(streamSql)
303
+ const stream = await ps.stream(valuesStream, true)
304
+ // merge results
305
+ res[binary.as || binary.ref[binary.ref.length - 1]] = stream
306
+ return res
307
+ }
308
+ return super.onSELECT({ query, data })
309
+ }
310
+
311
+ async onINSERT(req) {
312
+ try {
313
+ return await super.onINSERT(req)
314
+ } catch (err) {
315
+ throw _not_unique(err, 'ENTITY_ALREADY_EXISTS')
316
+ }
317
+ }
318
+
319
+ async onUPDATE(req) {
320
+ try {
321
+ return await super.onUPDATE(req)
322
+ } catch (err) {
323
+ throw _not_unique(err, 'UNIQUE_CONSTRAINT_VIOLATION')
324
+ }
325
+ }
326
+
284
327
  static CQN2SQL = class CQN2Postgres extends SQLService.CQN2SQL {
285
328
  _orderBy(orderBy, localized, locale) {
286
329
  return orderBy.map(
287
330
  localized
288
331
  ? c =>
289
- this.expr(c) +
290
- (c.element?.[this.class._localized] ? ` COLLATE "${locale}"` : '') +
291
- (c.sort === 'desc' || c.sort === -1 ? ' DESC' : ' ASC')
332
+ this.expr(c) +
333
+ (c.element?.[this.class._localized] ? ` COLLATE "${locale}"` : '') +
334
+ (c.sort === 'desc' || c.sort === -1 ? ' DESC' : ' ASC')
292
335
  : c => this.expr(c) + (c.sort === 'desc' || c.sort === -1 ? ' DESC' : ' ASC'),
293
336
  )
294
337
  }
@@ -337,10 +380,9 @@ GROUP BY k
337
380
  return col
338
381
  })
339
382
  // REVISIT: Remove SELECT ${cols} by adjusting SELECT_columns
340
- let obj = `row_to_json(${queryAlias}.*)`
341
- return `SELECT ${
342
- SELECT.one || SELECT.expand === 'root' ? obj : `coalesce(json_agg(${obj}),'[]'::json)`
343
- } as _json_ FROM (SELECT ${cols} FROM (${sql}) as ${queryAlias}) as ${queryAlias}`
383
+ let obj = `to_jsonb(${queryAlias}.*)`
384
+ return `SELECT ${SELECT.one || SELECT.expand === 'root' ? obj : `coalesce(jsonb_agg (${obj}),'[]'::jsonb)`
385
+ } as _json_ FROM (SELECT ${cols} FROM (${sql}) as ${queryAlias}) as ${queryAlias}`
344
386
  }
345
387
 
346
388
  doubleQuote(name) {
@@ -355,8 +397,8 @@ GROUP BY k
355
397
  // Adjusts json path expressions to be postgres specific
356
398
  .replace(/->>'\$(?:(?:\."(.*?)")|(?:\[(\d*)\]))'/g, (a, b, c) => (b ? `->>'${b}'` : `->>${c}`))
357
399
  // Adjusts json function to be postgres specific
358
- .replace('json_each(?)', 'json_array_elements($1::JSON)')
359
- .replace(/json_type\((\w+),'\$\."(\w+)"'\)/g, (_a, b, c) => `json_typeof(${b}->'${c}')`))
400
+ .replace('json_each(?)', 'jsonb_array_elements($1::jsonb)')
401
+ .replace(/json_type\((\w+),'\$\."(\w+)"'\)/g, (_a, b, c) => `jsonb_typeof(${b}->'${c}')`))
360
402
  }
361
403
 
362
404
  param({ ref }) {
@@ -428,8 +470,9 @@ GROUP BY k
428
470
  Timestamp: e => `to_char(${e}, 'YYYY-MM-DD"T"HH24:MI:SS.FF3"Z"')`,
429
471
  UTCDateTime: e => `to_char(${e}, 'YYYY-MM-DD"T"HH24:MI:SS"Z"')`,
430
472
  UTCTimestamp: e => `to_char(${e}, 'YYYY-MM-DD"T"HH24:MI:SS.FF3"Z"')`,
431
- struct: e => `json(${e})`,
432
- array: e => `json(${e})`,
473
+ Association: e => `jsonb(${e})`,
474
+ struct: e => `jsonb(${e})`,
475
+ array: e => `jsonb(${e})`,
433
476
  }
434
477
  }
435
478
 
@@ -457,6 +500,7 @@ GROUP BY k
457
500
  DROP USER IF EXISTS "${creds.user}";
458
501
  CREATE GROUP "${creds.usergroup}";
459
502
  CREATE USER "${creds.user}" WITH CREATEROLE IN GROUP "${creds.usergroup}" PASSWORD '${creds.user}';
503
+ GRANT "${creds.usergroup}" TO "${creds.user}" WITH ADMIN OPTION;
460
504
  `)
461
505
  await this.exec(`CREATE DATABASE "${creds.database}" OWNER="${creds.user}" TEMPLATE=template0`)
462
506
  } catch (e) {
@@ -523,14 +567,14 @@ class QueryStream extends Query {
523
567
  this.stream = new Readable({
524
568
  read: this.rows
525
569
  ? () => {
526
- this.stream.pause()
527
- // Request more rows
528
- this.connection.execute({
529
- portal: this.portal,
530
- rows: this.rows,
531
- })
532
- this.connection.flush()
533
- }
570
+ this.stream.pause()
571
+ // Request more rows
572
+ this.connection.execute({
573
+ portal: this.portal,
574
+ rows: this.rows,
575
+ })
576
+ this.connection.flush()
577
+ }
534
578
  : () => {},
535
579
  })
536
580
  this.push = this.stream.push.bind(this.stream)
@@ -711,4 +755,14 @@ class ParameterStream extends Writable {
711
755
  }
712
756
  }
713
757
 
758
+ function _not_unique(err, code) {
759
+ if (err.code === '23505')
760
+ return Object.assign(err, {
761
+ originalMessage: err.message, // FIXME: required because of next line
762
+ message: code, // FIXME: misusing message as code
763
+ code: 400, // FIXME: misusing code as (http) status
764
+ })
765
+ return err
766
+ }
767
+
714
768
  module.exports = PostgresService
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@cap-js/postgres",
3
- "version": "1.4.1",
3
+ "version": "1.5.1",
4
4
  "description": "CDS database service for Postgres",
5
5
  "homepage": "https://github.com/cap-js/cds-dbs/tree/main/postgres#cds-database-service-for-postgres",
6
6
  "repository": {
@@ -31,11 +31,11 @@
31
31
  "start": "docker-compose -f pg-stack.yml up -d"
32
32
  },
33
33
  "dependencies": {
34
- "@cap-js/db-service": "^1.3.1",
34
+ "@cap-js/db-service": "^1.6.0",
35
35
  "pg": "^8"
36
36
  },
37
37
  "peerDependencies": {
38
- "@sap/cds": ">=7",
38
+ "@sap/cds": ">=7.6",
39
39
  "@sap/cds-dk": ">=7"
40
40
  },
41
41
  "peerDependenciesMeta": {