@cap-js/db-service 1.4.0 → 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,32 @@
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/db-service-v1.5.0...db-service-v1.5.1) (2023-12-20)
8
+
9
+
10
+ ### Fixed
11
+
12
+ * **cqn2sql:** supporting calculated elements ([#387](https://github.com/cap-js/cds-dbs/issues/387)) ([2153fb9](https://github.com/cap-js/cds-dbs/commit/2153fb9a3910cd4afa3a91918e6cf682646492b7))
13
+ * do not rely on db constraints for deep delete ([#390](https://github.com/cap-js/cds-dbs/issues/390)) ([9623af6](https://github.com/cap-js/cds-dbs/commit/9623af64db97cfe15ef07b659635850fc908f77c))
14
+
15
+
16
+ ### Performance Improvements
17
+
18
+ * HANA list placeholder ([#380](https://github.com/cap-js/cds-dbs/issues/380)) ([3eadfea](https://github.com/cap-js/cds-dbs/commit/3eadfea7b94f485030cc8bd0bd298ce088586422))
19
+
20
+ ## [1.5.0](https://github.com/cap-js/cds-dbs/compare/db-service-v1.4.0...db-service-v1.5.0) (2023-12-06)
21
+
22
+
23
+ ### Added
24
+
25
+ * Improved connection pool for HANAService ([#349](https://github.com/cap-js/cds-dbs/issues/349)) ([1c284e6](https://github.com/cap-js/cds-dbs/commit/1c284e69cccd76daad52249c0462bc62aa4d11a8))
26
+
27
+
28
+ ### Fixed
29
+
30
+ * **localized:** `ref`s in subqueries in `from` are translated ([#366](https://github.com/cap-js/cds-dbs/issues/366)) ([cfe4897](https://github.com/cap-js/cds-dbs/commit/cfe489715db0854d30b90b7f13c024e6e90be497))
31
+ * wrong odata count in filter with groupby ([#352](https://github.com/cap-js/cds-dbs/issues/352)) ([70690a1](https://github.com/cap-js/cds-dbs/commit/70690a1a13e72bfbb66f03bf315d3f2d48672bf6))
32
+
7
33
  ## [1.4.0](https://github.com/cap-js/cds-dbs/compare/db-service-v1.3.2...db-service-v1.4.0) (2023-11-20)
8
34
 
9
35
 
package/lib/SQLService.js CHANGED
@@ -153,7 +153,8 @@ class SQLService extends DatabaseService {
153
153
  }
154
154
 
155
155
  get onDELETE() {
156
- return super.onDELETE = cds.env.features.assert_integrity === 'db' ? this.onSIMPLE : deep_delete
156
+ // REVISIT: It's not yet 100 % clear under which circumstances we can rely on db constraints
157
+ return super.onDELETE = /* cds.env.features.assert_integrity === 'db' ? this.onSIMPLE : */ deep_delete
157
158
  async function deep_delete(/** @type {Request} */ req) {
158
159
  let { compositions } = req.target
159
160
  if (compositions) {
@@ -230,16 +231,15 @@ class SQLService extends DatabaseService {
230
231
  if (max === undefined || (n < max && (n || !offset))) return n + offset
231
232
  }
232
233
  // REVISIT: made uppercase count because of HANA reserved word quoting
233
- const cq = cds.ql.clone(query, {
234
- columns: [{ func: 'count', as: 'COUNT' }],
234
+ const cq = SELECT.one([{ func: 'count', as: 'COUNT' }]).from(
235
+ cds.ql.clone(query, {
235
236
  localized: false,
236
237
  expand: false,
237
- limit: 0,
238
- orderBy: 0,
239
- })
240
- const { sql, values } = this.cqn2sql(cq)
241
- const ps = await this.prepare(sql)
242
- const { count, COUNT } = await ps.get(values)
238
+ limit: undefined,
239
+ orderBy: undefined,
240
+ }),
241
+ )
242
+ const { count, COUNT } = await this.onSELECT({ query: cq })
243
243
  return count ?? COUNT
244
244
  }
245
245
 
@@ -280,12 +280,12 @@ class SQLService extends DatabaseService {
280
280
  */
281
281
  cqn2sql(query, values) {
282
282
  let q = this.cqn4sql(query)
283
- if (q.SELECT && q.elements) q.SELECT.expand = q.SELECT.expand ?? 'root'
283
+ if (q.SELECT && 'elements' in q) q.SELECT.expand ??= 'root'
284
284
 
285
- let cmd = q.cmd || Object.keys(q)[0]
286
- if (cmd in { INSERT: 1, DELETE: 1, UPSERT: 1, UPDATE: 1 } || q.STREAM?.into) {
285
+ let kind = q.kind || Object.keys(q)[0]
286
+ if (kind in { INSERT: 1, DELETE: 1, UPSERT: 1, UPDATE: 1 } || q.STREAM?.into) {
287
287
  q = resolveView(q, this.model, this) // REVISIT: before resolveView was called on flat cqn obtained from cqn4sql -> is it correct to call on original q instead?
288
- let target = q[cmd]._transitions?.[0].target
288
+ let target = q[kind]._transitions?.[0].target
289
289
  if (target) q.target = target // REVISIT: Why isn't that done in resolveView?
290
290
  }
291
291
  let cqn2sql = new this.class.CQN2SQL(this)
@@ -63,6 +63,7 @@ class DatabaseService extends cds.Service {
63
63
 
64
64
  // Acquire a pooled connection
65
65
  this.dbc = await this.acquire()
66
+ this.dbc.destroy = this.destroy.bind(this)
66
67
 
67
68
  // Begin a session...
68
69
  try {
@@ -110,8 +111,20 @@ class DatabaseService extends cds.Service {
110
111
  */
111
112
  async release() {
112
113
  if (!this.dbc) return
113
- await this.pool.release(this.dbc)
114
+ const dbc = this.dbc
114
115
  this.dbc = undefined
116
+ await this.pool.release(dbc)
117
+ }
118
+
119
+ /**
120
+ * Destroys own connection, i.e. tix.dbc, from this.pool
121
+ * This is for subclasses to intercept, if required.
122
+ */
123
+ async destroy() {
124
+ if (!this.dbc) return
125
+ const dbc = this.dbc
126
+ this.dbc = undefined
127
+ await this.pool.destroy(dbc)
115
128
  }
116
129
 
117
130
  // REVISIT: should happen automatically after a configurable time
package/lib/cqn2sql.js CHANGED
@@ -27,7 +27,7 @@ class CQN2SQLRenderer {
27
27
  this.class._init() // is a noop for subsequent calls
28
28
  }
29
29
 
30
- static _add_mixins (aspect, mixins) {
30
+ static _add_mixins(aspect, mixins) {
31
31
  const fqn = this.name + aspect
32
32
  const types = cds.builtin.types
33
33
  for (let each in mixins) {
@@ -52,7 +52,7 @@ class CQN2SQLRenderer {
52
52
  this.ReservedWords[each[0] + each.slice(1).toLowerCase()] = 1 // Order
53
53
  this.ReservedWords[each.toLowerCase()] = 1 // order
54
54
  }
55
- this._init = () => {} // makes this a noop for subsequent calls
55
+ this._init = () => { } // makes this a noop for subsequent calls
56
56
  }
57
57
 
58
58
  /**
@@ -62,14 +62,14 @@ class CQN2SQLRenderer {
62
62
  * @returns {CQN2SQLRenderer|unknown}
63
63
  */
64
64
  render(q, vars) {
65
- const cmd = q.cmd || Object.keys(q)[0] // SELECT, INSERT, ...
65
+ const kind = q.kind || Object.keys(q)[0] // SELECT, INSERT, ...
66
66
  /**
67
67
  * @type {string} the rendered SQL string
68
68
  */
69
69
  this.sql = '' // to have it as first property for debugging
70
70
  /** @type {unknown[]} */
71
71
  this.values = [] // prepare values, filled in by subroutines
72
- this[cmd]((this.cqn = q)) // actual sql rendering happens here
72
+ this[kind]((this.cqn = q)) // actual sql rendering happens here
73
73
  if (vars?.length && !this.values?.length) this.values = vars
74
74
  const sanitize_values = process.env.NODE_ENV === 'production' && cds.env.log.sanitize_values !== false
75
75
  DEBUG?.(
@@ -208,9 +208,8 @@ class CQN2SQLRenderer {
208
208
  if (limit) sql += ` LIMIT ${this.limit(limit)}`
209
209
  // Expand cannot work without an inferred query
210
210
  if (expand) {
211
- // REVISIT: Why don't we handle that as an error in SELECT_expand?
212
- if (!q.elements) cds.error`Query was not inferred and includes expand. For which the metadata is missing.`
213
- sql = this.SELECT_expand(q, sql)
211
+ if ('elements' in q) sql = this.SELECT_expand(q, sql)
212
+ else cds.error`Query was not inferred and includes expand. For which the metadata is missing.`
214
213
  }
215
214
  return (this.sql = sql)
216
215
  }
@@ -230,10 +229,12 @@ class CQN2SQLRenderer {
230
229
  * @param {string} sql
231
230
  * @returns {string} SQL
232
231
  */
233
- SELECT_expand({ SELECT, elements }, sql) {
232
+ SELECT_expand(q, sql) {
233
+ if (!('elements' in q)) return sql
234
+
235
+ const SELECT = q.SELECT
234
236
  if (!SELECT.columns) return sql
235
- if (!elements) return sql // REVISIT: Above we say this is an error condition, but here we say it's ok?
236
-
237
+
237
238
  let cols = SELECT.columns.map(x => {
238
239
  const name = this.column_name(x)
239
240
  let col = `'${name}',${this.output_converter4(x.element, this.quote(name))}`
@@ -248,7 +249,7 @@ class CQN2SQLRenderer {
248
249
  // Prevent SQLite from hitting function argument limit of 100
249
250
  let obj = ''
250
251
 
251
- if(cols.length < 50) obj = `json_object(${cols.slice(0, 50)})`
252
+ if (cols.length < 50) obj = `json_object(${cols.slice(0, 50)})`
252
253
  else {
253
254
  const chunks = []
254
255
  for (let i = 0; i < cols.length; i += 50) {
@@ -256,7 +257,7 @@ class CQN2SQLRenderer {
256
257
  }
257
258
  // REVISIT: json_merge is a user defined function, bad performance!
258
259
  obj = `json_merge(${chunks})`
259
- }
260
+ }
260
261
 
261
262
 
262
263
  return `SELECT ${SELECT.one || SELECT.expand === 'root' ? obj : `json_group_array(${obj.includes('json_merge') ? `json_insert(${obj})` : obj})`} as _json_ FROM (${sql})`
@@ -341,9 +342,9 @@ class CQN2SQLRenderer {
341
342
  return orderBy.map(
342
343
  localized
343
344
  ? c =>
344
- this.expr(c) +
345
- (c.element?.[this.class._localized] ? ' COLLATE NOCASE' : '') +
346
- (c.sort === 'desc' || c.sort === -1 ? ' DESC' : ' ASC')
345
+ this.expr(c) +
346
+ (c.element?.[this.class._localized] ? ' COLLATE NOCASE' : '') +
347
+ (c.sort === 'desc' || c.sort === -1 ? ' DESC' : ' ASC')
347
348
  : c => this.expr(c) + (c.sort === 'desc' || c.sort === -1 ? ' DESC' : ' ASC'),
348
349
  )
349
350
  }
@@ -371,12 +372,12 @@ class CQN2SQLRenderer {
371
372
  return INSERT.entries
372
373
  ? this.INSERT_entries(q)
373
374
  : INSERT.rows
374
- ? this.INSERT_rows(q)
375
- : INSERT.values
376
- ? this.INSERT_values(q)
377
- : INSERT.as
378
- ? this.INSERT_select(q)
379
- : cds.error`Missing .entries, .rows, or .values in ${q}`
375
+ ? this.INSERT_rows(q)
376
+ : INSERT.values
377
+ ? this.INSERT_values(q)
378
+ : INSERT.as
379
+ ? this.INSERT_select(q)
380
+ : cds.error`Missing .entries, .rows, or .values in ${q}`
380
381
  }
381
382
 
382
383
  /**
@@ -393,7 +394,7 @@ class CQN2SQLRenderer {
393
394
  return // REVISIT: mtx sends an insert statement without entries and no reference entity
394
395
  }
395
396
  const columns = elements
396
- ? ObjectKeys(elements).filter(c => c in elements && !elements[c].virtual && !elements[c].isAssociation)
397
+ ? ObjectKeys(elements).filter(c => c in elements && !elements[c].virtual && !elements[c].value && !elements[c].isAssociation)
397
398
  : ObjectKeys(INSERT.entries[0])
398
399
 
399
400
  /** @type {string[]} */
@@ -426,9 +427,8 @@ class CQN2SQLRenderer {
426
427
  // Include this.values for placeholders
427
428
  /** @type {unknown[][]} */
428
429
  this.entries = [[...this.values, JSON.stringify(INSERT.entries)]]
429
- return (this.sql = `INSERT INTO ${this.quote(entity)}${alias ? ' as ' + this.quote(alias) : ''} (${
430
- this.columns
431
- }) SELECT ${extraction} FROM json_each(?)`)
430
+ return (this.sql = `INSERT INTO ${this.quote(entity)}${alias ? ' as ' + this.quote(alias) : ''} (${this.columns
431
+ }) SELECT ${extraction} FROM json_each(?)`)
432
432
  }
433
433
 
434
434
  /**
@@ -442,21 +442,20 @@ class CQN2SQLRenderer {
442
442
  const alias = INSERT.into.as
443
443
  const elements = q.elements || q.target?.elements
444
444
  const columns = INSERT.columns
445
- || cds.error`Cannot insert rows without columns or elements`
445
+ || cds.error`Cannot insert rows without columns or elements`
446
446
 
447
447
  const inputConverter = this.class._convertInput
448
- const extraction = columns.map((c,i) => {
448
+ const extraction = columns.map((c, i) => {
449
449
  const extract = `value->>'$[${i}]'`
450
450
  const element = elements?.[c]
451
451
  const converter = element?.[inputConverter]
452
- return converter?.(extract,element) || extract
452
+ return converter?.(extract, element) || extract
453
453
  })
454
454
 
455
455
  this.columns = columns.map(c => this.quote(c))
456
456
  this.entries = [[JSON.stringify(INSERT.rows)]]
457
- return (this.sql = `INSERT INTO ${this.quote(entity)}${alias ? ' as ' + this.quote(alias) : ''} (${
458
- this.columns
459
- }) SELECT ${extraction} FROM json_each(?)`)
457
+ return (this.sql = `INSERT INTO ${this.quote(entity)}${alias ? ' as ' + this.quote(alias) : ''} (${this.columns
458
+ }) SELECT ${extraction} FROM json_each(?)`)
460
459
  }
461
460
 
462
461
  /**
@@ -551,9 +550,9 @@ class CQN2SQLRenderer {
551
550
  if (entity.as) sql += ` AS ${entity.as}`
552
551
 
553
552
  let columns = []
554
- if (data) _add (data, val => this.val({val}))
555
- if (_with) _add (_with, x => this.expr(x))
556
- function _add (data, sql4) {
553
+ if (data) _add(data, val => this.val({ val }))
554
+ if (_with) _add(_with, x => this.expr(x))
555
+ function _add(data, sql4) {
557
556
  for (let c in data) {
558
557
  if (!elements || (c in elements && !elements[c].virtual)) {
559
558
  columns.push({ name: c, sql: sql4(data[c]) })
@@ -601,8 +600,8 @@ class CQN2SQLRenderer {
601
600
  return STREAM.from
602
601
  ? this.STREAM_from(q)
603
602
  : STREAM.into
604
- ? this.STREAM_into(q)
605
- : cds.error`Missing .form or .into in ${q}`
603
+ ? this.STREAM_into(q)
604
+ : cds.error`Missing .form or .into in ${q}`
606
605
  }
607
606
 
608
607
  /**
@@ -667,7 +666,7 @@ class CQN2SQLRenderer {
667
666
  expr(x) {
668
667
  const wrap = x.cast ? sql => `cast(${sql} as ${this.type4(x.cast)})` : sql => sql
669
668
  if (typeof x === 'string') throw cds.error`Unsupported expr: ${x}`
670
- if ('param' in x) return wrap(this.param(x))
669
+ if (x.param) return wrap(this.param(x))
671
670
  if ('ref' in x) return wrap(this.ref(x))
672
671
  if ('val' in x) return wrap(this.val(x))
673
672
  if ('xpr' in x) return wrap(this.xpr(x))
@@ -703,15 +702,15 @@ class CQN2SQLRenderer {
703
702
  operator(x, i, xpr) {
704
703
 
705
704
  // Translate = to IS NULL for rhs operand being NULL literal
706
- if (x === '=') return xpr[i+1]?.val === null ? 'is' : '='
705
+ if (x === '=') return xpr[i + 1]?.val === null ? 'is' : '='
707
706
 
708
707
  // Translate == to IS NOT NULL for rhs operand being NULL literal, otherwise ...
709
708
  // Translate == to IS NOT DISTINCT FROM, unless both operands cannot be NULL
710
- if (x === '==') return xpr[i+1]?.val === null ? 'is' : _not_null(i-1) && _not_null(i+1) ? '=' : this.is_not_distinct_from_
709
+ if (x === '==') return xpr[i + 1]?.val === null ? 'is' : _not_null(i - 1) && _not_null(i + 1) ? '=' : this.is_not_distinct_from_
711
710
 
712
711
  // Translate != to IS NULL for rhs operand being NULL literal, otherwise...
713
712
  // Translate != to IS DISTINCT FROM, unless both operands cannot be NULL
714
- if (x === '!=') return xpr[i+1]?.val === null ? 'is not' : _not_null(i-1) && _not_null(i+1) ? '<>' : this.is_distinct_from_
713
+ if (x === '!=') return xpr[i + 1]?.val === null ? 'is not' : _not_null(i - 1) && _not_null(i + 1) ? '<>' : this.is_distinct_from_
715
714
 
716
715
  else return x
717
716
 
@@ -748,9 +747,9 @@ class CQN2SQLRenderer {
748
747
  */
749
748
  ref({ ref }) {
750
749
  switch (ref[0]) {
751
- case '$now': return this.func({ func: 'session_context', args: [{ val: '$now' }]})
750
+ case '$now': return this.func({ func: 'session_context', args: [{ val: '$now', param: false }] })
752
751
  case '$user':
753
- case '$user.id': return this.func({ func: 'session_context', args: [{ val: '$user.id' }]})
752
+ case '$user.id': return this.func({ func: 'session_context', args: [{ val: '$user.id', param: false }] })
754
753
  default: return ref.map(r => this.quote(r)).join('.')
755
754
  }
756
755
  }
@@ -760,7 +759,7 @@ class CQN2SQLRenderer {
760
759
  * @param {import('./infer/cqn').val} param0
761
760
  * @returns {string} SQL
762
761
  */
763
- val({ val }) {
762
+ val({ val, param }) {
764
763
  switch (typeof val) {
765
764
  case 'function': throw new Error('Function values not supported.')
766
765
  case 'undefined': return 'NULL'
@@ -769,13 +768,13 @@ class CQN2SQLRenderer {
769
768
  case 'object':
770
769
  if (val === null) return 'NULL'
771
770
  if (val instanceof Date) return `'${val.toISOString()}'`
772
- if (val instanceof Readable) ; // go on with default below
771
+ if (val instanceof Readable); // go on with default below
773
772
  else if (Buffer.isBuffer(val)) val = val.toString('base64')
774
773
  else if (is_regexp(val)) val = val.source
775
774
  else val = JSON.stringify(val)
776
775
  case 'string': // eslint-disable-line no-fallthrough
777
776
  }
778
- if (!this.values) return this.string(val)
777
+ if (!this.values || param === false) return this.string(val)
779
778
  else this.values.push(val)
780
779
  return '?'
781
780
  }
@@ -859,12 +858,12 @@ class CQN2SQLRenderer {
859
858
  const requiredColumns = !elements
860
859
  ? []
861
860
  : Object.keys(elements)
862
- .filter(
863
- e =>
864
- (elements[e]?.[annotation] || (!isUpdate && elements[e]?.default && !elements[e].virtual && !elements[e].isAssociation)) &&
865
- !columns.find(c => c.name === e),
866
- )
867
- .map(name => ({ name, sql: 'NULL' }))
861
+ .filter(
862
+ e =>
863
+ (elements[e]?.[annotation] || (!isUpdate && elements[e]?.default && !elements[e].virtual && !elements[e].isAssociation)) &&
864
+ !columns.find(c => c.name === e),
865
+ )
866
+ .map(name => ({ name, sql: 'NULL' }))
868
867
 
869
868
  return [...columns, ...requiredColumns].map(({ name, sql }) => {
870
869
  let element = elements?.[name] || {}
@@ -874,14 +873,13 @@ class CQN2SQLRenderer {
874
873
  if (converter && sql[0] !== '$') sql = converter(sql, element)
875
874
 
876
875
  let val = _managed[element[annotation]?.['=']]
877
- if (val) sql = `coalesce(${sql}, ${this.func({ func: 'session_context', args: [{ val }] })})`
876
+ if (val) sql = `coalesce(${sql}, ${this.func({ func: 'session_context', args: [{ val, param: false }] })})`
878
877
  else if (!isUpdate && element.default) {
879
878
  const d = element.default
880
879
  if (d.val !== undefined || d.ref?.[0] === '$now') {
881
880
  // REVISIT: d.ref is not used afterwards
882
- sql = `(CASE WHEN json_type(value,'$."${name}"') IS NULL THEN ${
883
- this.defaultValue(d.val) // REVISIT: this.defaultValue is a strange function
884
- } ELSE ${sql} END)`
881
+ sql = `(CASE WHEN json_type(value,'$."${name}"') IS NULL THEN ${this.defaultValue(d.val) // REVISIT: this.defaultValue is a strange function
882
+ } ELSE ${sql} END)`
885
883
  }
886
884
  }
887
885
 
package/lib/cqn4sql.js CHANGED
@@ -48,7 +48,7 @@ function cqn4sql(originalQuery, model = cds.context?.model || cds.model) {
48
48
  if (originalQuery.SELECT?.from.args && !originalQuery.joinTree) return inferred
49
49
 
50
50
  let transformedQuery = cds.ql.clone(inferred)
51
- const kind = inferred.cmd || Object.keys(inferred)[0]
51
+ const kind = inferred.kind || Object.keys(inferred)[0]
52
52
 
53
53
  if (inferred.INSERT || inferred.UPSERT) {
54
54
  transformedQuery = transformQueryForInsertUpsert(kind)
@@ -415,7 +415,6 @@ function cqn4sql(originalQuery, model = cds.context?.model || cds.model) {
415
415
  return transformedColumns
416
416
 
417
417
  function handleSubquery(col) {
418
- if (isLocalized(inferred.target)) col.SELECT.localized = true
419
418
  if (!col.SELECT.from.as) {
420
419
  const uniqueSubqueryAlias = inferred.joinTree.addNextAvailableTableAlias(
421
420
  getLastStringSegment(col.SELECT.from.ref[col.SELECT.from.ref.length - 1]),
@@ -830,7 +829,6 @@ function cqn4sql(originalQuery, model = cds.context?.model || cds.model) {
830
829
  one: column.$refLinks[column.$refLinks.length - 1].definition.is2one,
831
830
  },
832
831
  }
833
- if (isLocalized(inferred.target)) subquery.SELECT.localized = true
834
832
  const expanded = transformSubquery(subquery)
835
833
  const correlated = _correlate({ ...expanded, as: columnAlias }, outerAlias)
836
834
  Object.defineProperty(correlated, 'elements', { value: subquery.elements, writable: true })
@@ -946,6 +944,7 @@ function cqn4sql(originalQuery, model = cds.context?.model || cds.model) {
946
944
  outerQueries.push(inferred)
947
945
  Object.defineProperty(q, 'outerQueries', { value: outerQueries })
948
946
  }
947
+ if (isLocalized(inferred.target)) q.SELECT.localized = true
949
948
  return cqn4sql(q, model)
950
949
  }
951
950
 
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@cap-js/db-service",
3
- "version": "1.4.0",
3
+ "version": "1.5.1",
4
4
  "description": "CDS base database service",
5
5
  "homepage": "https://github.com/cap-js/cds-dbs/tree/main/db-service#cds-base-database-service",
6
6
  "repository": {