@sap/cds 9.4.5 → 9.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.
Files changed (55) hide show
  1. package/CHANGELOG.md +71 -1
  2. package/_i18n/messages_en_US_saptrc.properties +1 -1
  3. package/common.cds +5 -2
  4. package/lib/compile/cds-compile.js +1 -0
  5. package/lib/compile/for/assert.js +64 -0
  6. package/lib/compile/for/flows.js +194 -58
  7. package/lib/compile/for/lean_drafts.js +75 -7
  8. package/lib/compile/parse.js +1 -1
  9. package/lib/compile/to/csn.js +6 -2
  10. package/lib/compile/to/edm.js +1 -1
  11. package/lib/compile/to/yaml.js +8 -1
  12. package/lib/dbs/cds-deploy.js +2 -2
  13. package/lib/env/cds-env.js +14 -4
  14. package/lib/env/defaults.js +6 -1
  15. package/lib/i18n/localize.js +1 -1
  16. package/lib/index.js +7 -7
  17. package/lib/req/event.js +4 -0
  18. package/lib/req/validate.js +3 -0
  19. package/lib/srv/cds.Service.js +2 -1
  20. package/lib/srv/middlewares/auth/ias-auth.js +5 -7
  21. package/lib/srv/middlewares/auth/index.js +1 -1
  22. package/lib/srv/protocols/index.js +7 -6
  23. package/lib/srv/srv-handlers.js +7 -0
  24. package/libx/_runtime/common/Service.js +5 -1
  25. package/libx/_runtime/common/constants/events.js +1 -0
  26. package/libx/_runtime/common/generic/assert.js +220 -0
  27. package/libx/_runtime/common/generic/flows.js +168 -108
  28. package/libx/_runtime/common/utils/cqn.js +0 -24
  29. package/libx/_runtime/common/utils/normalizeTimestamp.js +2 -2
  30. package/libx/_runtime/common/utils/resolveView.js +8 -2
  31. package/libx/_runtime/common/utils/templateProcessor.js +10 -1
  32. package/libx/_runtime/common/utils/templateProcessorPathSerializer.js +21 -9
  33. package/libx/_runtime/fiori/lean-draft.js +511 -379
  34. package/libx/_runtime/messaging/enterprise-messaging-utils/registerEndpoints.js +39 -35
  35. package/libx/_runtime/messaging/enterprise-messaging.js +2 -2
  36. package/libx/_runtime/remote/Service.js +4 -5
  37. package/libx/_runtime/ucl/Service.js +111 -15
  38. package/libx/common/utils/streaming.js +1 -1
  39. package/libx/odata/middleware/batch.js +8 -6
  40. package/libx/odata/middleware/create.js +2 -2
  41. package/libx/odata/middleware/delete.js +2 -2
  42. package/libx/odata/middleware/metadata.js +18 -11
  43. package/libx/odata/middleware/read.js +2 -2
  44. package/libx/odata/middleware/service-document.js +1 -1
  45. package/libx/odata/middleware/update.js +1 -1
  46. package/libx/odata/parse/afterburner.js +24 -25
  47. package/libx/odata/parse/cqn2odata.js +2 -6
  48. package/libx/odata/parse/grammar.peggy +90 -12
  49. package/libx/odata/parse/parser.js +1 -1
  50. package/libx/odata/utils/index.js +2 -2
  51. package/libx/odata/utils/readAfterWrite.js +2 -0
  52. package/libx/queue/TaskRunner.js +26 -1
  53. package/libx/queue/index.js +11 -1
  54. package/package.json +1 -1
  55. package/srv/ucl-service.cds +2 -0
@@ -5,12 +5,10 @@ const { getFrom } = require('../../../../lib/compile/for/flows')
5
5
  const FLOW_STATUS = '@flow.status'
6
6
  const FROM = '@from'
7
7
  const TO = '@to'
8
- // backwards compat
9
- const FLOW_FROM = '@flow.from'
10
- const FLOW_TO = '@flow.to'
11
-
12
8
  const FLOW_PREVIOUS = '$flow.previous'
13
9
 
10
+ const $transitions_ = Symbol.for('transitions_')
11
+
14
12
  function buildAllowedCondition(action, statusElementName, statusEnum) {
15
13
  const fromList = getFrom(action)
16
14
  const conditions = fromList.map(from => {
@@ -20,154 +18,216 @@ function buildAllowedCondition(action, statusElementName, statusEnum) {
20
18
  return `(${conditions.join(' OR ')})`
21
19
  }
22
20
 
23
- async function isCurrentStatusInFrom(req, action, statusElementName, statusEnum) {
21
+ async function isCurrentStatusInFrom(subject, action, statusElementName, statusEnum) {
24
22
  const cond = buildAllowedCondition(action, statusElementName, statusEnum)
25
23
  const parsedXpr = cds.parse.expr(cond)
26
- const dbEntity = await SELECT.one.from(req.subject).where(parsedXpr)
24
+ const dbEntity = await SELECT.one.from(subject).where(parsedXpr)
27
25
  return dbEntity !== undefined
28
26
  }
29
27
 
30
- async function checkStatus(req, action, statusElementName, statusEnum) {
31
- const allowed = await isCurrentStatusInFrom(req, action, statusElementName, statusEnum)
28
+ async function checkStatus(subject, action, statusElementName, statusEnum) {
29
+ const allowed = await isCurrentStatusInFrom(subject, action, statusElementName, statusEnum)
32
30
  if (!allowed) {
33
31
  const from = getFrom(action)
34
32
  const fromValues = JSON.stringify(from.flatMap(el => Object.values(el)))
35
33
  cds.error({
36
- code: 409,
34
+ status: 409,
37
35
  message: from.length > 1 ? 'INVALID_FLOW_TRANSITION_MULTI' : 'INVALID_FLOW_TRANSITION_SINGLE',
38
36
  args: [action.name, statusElementName, fromValues]
39
37
  })
40
38
  }
41
39
  }
42
40
 
43
- const buildUpKeys = parentKeys => {
41
+ // REVISIT: what about renamed keys?
42
+ const buildUpKeys = async (entity, data, subject) => {
43
+ const parentKeys = Object.keys(entity.keys).filter(k => k !== 'IsActiveEntity')
44
+ // REVISIT: when do we not hava all keys?
45
+ const keyValues =
46
+ data && parentKeys.every(key => key in data) ? data : await SELECT.one.from(subject).columns(parentKeys)
44
47
  const upKeys = {}
45
- for (const key in parentKeys) {
46
- upKeys[`up__${key}`] = parentKeys[key]
48
+ for (let i = 0; i < parentKeys.length; i++) {
49
+ upKeys[`up__${parentKeys[i]}`] = keyValues[parentKeys[i]]
47
50
  }
48
51
  return upKeys
49
52
  }
50
53
 
51
- const updateFlowHistory = async (req, toValue, upKeys, changes, isPrevious) => {
52
- if (cds.env.features.flows_history_stack && isPrevious) {
53
- await DELETE.from(req.target.compositions['transitions_'].target).where({
54
- timestamp: changes[changes.length - 1].timestamp
55
- })
54
+ const resolveTo = (action, statusEnum) => {
55
+ let to = action[TO]
56
+ to = to['#'] ? (statusEnum[to['#']].val ?? statusEnum[to['#']]['$path'].at(-1)) : to
57
+ return to
58
+ }
59
+
60
+ const handleTransition = async (entity, data, subject, to) => {
61
+ const isPrevious = to['='] === FLOW_PREVIOUS
62
+ if (isPrevious) {
63
+ const upKeys = await buildUpKeys(entity, data, subject)
64
+ const previous = await SELECT.one
65
+ .from(entity[$transitions_].target)
66
+ .where({ ...upKeys })
67
+ .orderBy('timestamp desc')
68
+ .limit(1, 1)
69
+ if (!previous)
70
+ cds.error({ status: 409, message: 'No change has been made yet, cannot transition to previous status.' })
71
+ to = previous.status
72
+ }
73
+ return to
74
+ }
75
+
76
+ const getStatusInfo = statusElement => {
77
+ let statusEnum, statusElementName
78
+ if (statusElement.enum) {
79
+ statusEnum = statusElement.enum
80
+ statusElementName = statusElement.name
81
+ } else if (statusElement?._target?.elements['code']) {
82
+ statusEnum = statusElement._target.elements['code'].enum
83
+ statusElementName = statusElement.name + '_code'
56
84
  } else {
57
- await INSERT.into(req.target.compositions['transitions_'].target).entries({
58
- ...upKeys,
59
- status: toValue
85
+ cds.error({
86
+ status: 409,
87
+ message: `Status element in ${statusElement.parent.name} must be an enum or target an entity with an enum named "code"`
60
88
  })
61
89
  }
90
+ return { statusEnum, statusElementName }
62
91
  }
63
92
 
64
- const buildToKey = (action, statusEnum) => {
65
- const to = action[TO] ?? action[FLOW_TO]
66
- const toKey = to['#'] ? (statusEnum[to['#']].val ?? statusEnum[to['#']]['$path'].at(-1)) : to
67
- return toKey
93
+ const from_factory = (entity, action, { statusElementName, statusEnum }) => {
94
+ async function handle_flow_from(req) {
95
+ const subject = cds.clone(req.subject)
96
+ if (entity.name.endsWith('.drafts')) subject.ref[0].id = entity.name
97
+
98
+ await checkStatus(subject, action, statusElementName, statusEnum)
99
+ }
100
+ handle_flow_from._initial = true
101
+ return handle_flow_from
68
102
  }
69
103
 
70
- const handleStatusTransitionWithHistory = async (req, statusElementName, toKey, service) => {
71
- let upKeys, changes
72
- upKeys = buildUpKeys(req.params[0])
73
- changes = await SELECT.from(req.target.compositions['transitions_'].target)
74
- .where({ ...upKeys })
75
- .orderBy('timestamp asc')
76
- const isPrevious = toKey['='] === FLOW_PREVIOUS
77
- if (isPrevious) {
78
- if (changes.length <= 1)
79
- return cds.error({ code: 409, message: 'No change has been made yet, cannot transition to previous status.' })
80
- toKey = changes[changes.length - 2].status
104
+ const to___factory = (entity, action, { statusElementName, statusEnum }) => {
105
+ return async function handle_flow_to(req, next) {
106
+ const res = await next()
107
+
108
+ let subject = cds.clone(req.subject)
109
+ if (entity.name.endsWith('.drafts')) subject.ref[0].id = entity.name
110
+
111
+ // REVISIT: this only happens on CREATE, where req.subject is a collection
112
+ // -> could be avoided if setting the status would be done via req.data
113
+ if (!subject.ref[0].id) {
114
+ const keys = Object.keys(entity.keys).reduce((acc, cur) => {
115
+ acc[cur] = res[cur]
116
+ return acc
117
+ }, {})
118
+ subject = SELECT.from(entity.name, keys).SELECT.from
119
+ }
120
+
121
+ let to = resolveTo(action, statusEnum)
122
+
123
+ if (Object.prototype.hasOwnProperty.call(entity, $transitions_)) {
124
+ to = await handleTransition(entity, req.data, subject, to)
125
+ }
126
+
127
+ await UPDATE(subject).with({ [statusElementName]: to })
128
+
129
+ // REVISIT: for stack, we now need to delete the last to transitions
130
+ if (cds.env.features.flows_history_stack && resolveTo(action, statusEnum)['='] === FLOW_PREVIOUS) {
131
+ const upKeys = await buildUpKeys(entity, req.data, req.subject)
132
+ const timestamps = SELECT('timestamp')
133
+ .from(entity[$transitions_].target)
134
+ .where({ ...upKeys })
135
+ .orderBy('timestamp desc')
136
+ .limit(2)
137
+ await DELETE.from(entity[$transitions_].target)
138
+ .where({ ...upKeys })
139
+ .where(`timestamp in`, timestamps)
140
+ }
141
+
142
+ return res
81
143
  }
82
- await service.run(UPDATE(req.subject).with({ [statusElementName]: toKey }))
83
- await updateFlowHistory(req, toKey, upKeys, changes, isPrevious)
84
144
  }
85
145
 
86
146
  /**
87
147
  * handler registration
88
148
  */
89
149
  module.exports = cds.service.impl(function () {
90
- const entry = []
91
- const exit = []
150
+ const b4 = []
151
+ const on = []
152
+ const after = []
92
153
 
93
154
  for (const entity of this.entities) {
94
155
  if (!entity.actions || !entity.elements) continue
95
156
 
96
- const fromActions = []
97
- const toActions = []
98
- for (const action of entity.actions) {
99
- if (action[FROM] || action[FLOW_FROM]) fromActions.push(action)
100
- if (action[TO] || action[FLOW_TO]) toActions.push(action)
101
- }
102
- if (fromActions.length === 0 && toActions.length === 0) continue
103
-
104
- let statusElement = Object.values(entity.elements).find(el => el[FLOW_STATUS])
105
- if (!statusElement) {
106
- cds.error({
107
- code: 409,
108
- message: `Entity ${entity.name} does not have a status element, but its actions have registered @flow annotations.`
157
+ const statusElement = Object.values(entity.elements).find(el => el[FLOW_STATUS])
158
+ if (!statusElement) continue
159
+
160
+ const statusInfo = getStatusInfo(statusElement)
161
+
162
+ // determine and cache target for transitions recording, if any
163
+ let base = entity
164
+ while (base.__proto__.kind === 'entity') base = base.__proto__
165
+ if (base.compositions?.transitions_) {
166
+ entity[$transitions_] = base.compositions.transitions_
167
+ // track changes on db level
168
+ cds.connect.to('db').then(db => {
169
+ db.after(['CREATE', 'UPDATE', 'UPSERT'], entity, async (res, req) => {
170
+ if ((res.affectedRows ?? res) !== 1) return
171
+ if (!(statusInfo.statusElementName in req.data)) return
172
+ const status = req.data[statusInfo.statusElementName]
173
+ const upKeys = await buildUpKeys(entity, req.data, req.subject)
174
+ const last = await SELECT.one.from(entity[$transitions_].target).orderBy('timestamp desc').where(upKeys)
175
+ if (last?.status !== status) await UPSERT.into(entity[$transitions_].target).entries({ ...upKeys, status })
176
+ })
109
177
  })
110
178
  }
111
179
 
112
- let statusEnum, statusElementName
113
- if (statusElement.enum) {
114
- statusEnum = statusElement.enum
115
- statusElementName = statusElement.name
116
- } else if (statusElement?._target?.elements['code']) {
117
- statusEnum = statusElement._target.elements['code'].enum
118
- statusElementName = statusElement.name + '_code'
119
- } else {
120
- cds.error({
121
- code: 409,
122
- message: `Status element in entity ${entity.name} is not an enum and does not have a valid target with code enum.`
123
- })
180
+ // register handlers
181
+ for (const action of entity.actions) {
182
+ const to__ = action[TO]
183
+ const from = action[FROM]
184
+
185
+ // REVISIT: for CRUD and Draft, we could set status in before handlers (on db level) to save roundtrips
186
+ switch (action.name) {
187
+ // CRUD
188
+ case 'CREATE':
189
+ if (to__) on.push([action.name, entity, to___factory(entity, action, statusInfo)])
190
+ break
191
+ case 'READ':
192
+ // nothing to do
193
+ break
194
+ case 'UPDATE':
195
+ if (from) b4.push([action.name, entity, from_factory(entity, action, statusInfo)])
196
+ if (to__) on.push([action.name, entity, to___factory(entity, action, statusInfo)])
197
+ break
198
+ case 'DELETE':
199
+ if (from) b4.push([action.name, entity, from_factory(entity, action, statusInfo)])
200
+ break
201
+ // Draft
202
+ case 'NEW':
203
+ if (to__) on.push(['CREATE', entity.drafts, to___factory(entity.drafts, action, statusInfo)])
204
+ break
205
+ case 'PATCH':
206
+ if (from) b4.push(['UPDATE', entity.drafts, from_factory(entity.drafts, action, statusInfo)])
207
+ if (to__) on.push(['UPDATE', entity.drafts, to___factory(entity.drafts, action, statusInfo)])
208
+ break
209
+ case 'SAVE':
210
+ if (from) b4.push([action.name, entity.drafts, from_factory(entity.drafts, action, statusInfo)])
211
+ if (to__) on.push([action.name, entity.drafts, to___factory(entity, action, statusInfo)])
212
+ break
213
+ case 'EDIT':
214
+ if (from) b4.push([action.name, entity, from_factory(entity, action, statusInfo)])
215
+ if (to__) on.push([action.name, entity, to___factory(entity.drafts, action, statusInfo)])
216
+ break
217
+ case 'DISCARD':
218
+ if (from) b4.push([action.name, entity.drafts, from_factory(entity.drafts, action, statusInfo)])
219
+ break
220
+ // custom actions
221
+ default:
222
+ if (from) b4.push([action.name, entity, from_factory(entity, action, statusInfo)])
223
+ if (to__) on.push([action.name, entity, to___factory(entity, action, statusInfo)])
224
+ }
124
225
  }
125
-
126
- entry.push({ events: fromActions, entity, statusElementName, statusEnum })
127
- exit.push({ events: toActions, entity, statusElementName, statusEnum })
128
226
  }
129
227
 
130
228
  this.prepend(function () {
131
- for (const each of entry) {
132
- this.before(
133
- each.events,
134
- each.entity,
135
- Object.assign(
136
- async function handle_entry_state(req) {
137
- const action = req.target.actions[req.event]
138
- await checkStatus(req, action, each.statusElementName, each.statusEnum)
139
- },
140
- { _initial: true }
141
- )
142
- )
143
- }
144
-
145
- for (const each of exit) {
146
- async function handle_after_create(res, req) {
147
- const parentKeys = Object.keys(req.target.keys)
148
- const entry = {}
149
- for (let i = 0; i < parentKeys.length; i++) {
150
- entry[`up__${parentKeys[i]}`] = req.data[parentKeys[i]]
151
- }
152
- await INSERT.into(req.target.compositions['transitions_'].target).entries({
153
- ...entry,
154
- status: res[each.statusElementName]
155
- })
156
- }
157
- if ('transitions_' in (each.entity.compositions ?? {})) this.after('CREATE', each.entity, handle_after_create)
158
-
159
- async function handle_exit_state(req, next) {
160
- const res = await next()
161
- const action = req.target.actions[req.event]
162
- let toKey = buildToKey(action, each.statusEnum)
163
- if ('transitions_' in (req.target.compositions ?? {})) {
164
- await handleStatusTransitionWithHistory(req, each.statusElementName, toKey, this)
165
- } else {
166
- await this.run(UPDATE(req.subject).with({ [each.statusElementName]: toKey }))
167
- }
168
- return res
169
- }
170
- this.on(each.events, each.entity, handle_exit_state)
171
- }
229
+ for (const each of b4) this.before(...each)
230
+ for (const each of on) this.on(...each)
231
+ for (const each of after) this.after(...each)
172
232
  })
173
233
  })
@@ -2,28 +2,6 @@ const cds = require('../../cds')
2
2
  const { SELECT } = cds.ql
3
3
  const { setEntityContained } = require('./csn')
4
4
 
5
- const getEntityNameFromDeleteCQN = cqn => {
6
- let from
7
- if (cqn && cqn.DELETE && cqn.DELETE.from) {
8
- if (typeof cqn.DELETE.from === 'string') {
9
- from = cqn.DELETE.from
10
- } else if (cqn.DELETE.from.name) {
11
- from = cqn.DELETE.from.name
12
- } else if (cqn.DELETE.from.ref && cqn.DELETE.from.ref.length === 1) {
13
- from = cqn.DELETE.from.ref[0]
14
- }
15
- }
16
- return from
17
- }
18
-
19
- const getEntityNameFromUpdateCQN = cqn => {
20
- return (
21
- (cqn.UPDATE.entity.ref && cqn.UPDATE.entity.ref[0] && (cqn.UPDATE.entity.ref[0].id || cqn.UPDATE.entity.ref[0])) ||
22
- cqn.UPDATE.entity.name ||
23
- cqn.UPDATE.entity
24
- )
25
- }
26
-
27
5
  // scope: simple wheres à la "[{ ref: ['foo'] }, '=', { val: 'bar' }, 'and', ... ]"
28
6
  function where2obj(where, target = null, data = {}) {
29
7
  for (let i = 0; i < where.length; ) {
@@ -89,8 +67,6 @@ const resolveFromSelect = query => {
89
67
  }
90
68
 
91
69
  module.exports = {
92
- getEntityNameFromDeleteCQN,
93
- getEntityNameFromUpdateCQN,
94
70
  where2obj,
95
71
  targetFromPath,
96
72
  resolveFromSelect
@@ -11,7 +11,7 @@ module.exports = value => {
11
11
  if (typeof value === 'number') value = new Date(value).toISOString()
12
12
  if (typeof value !== 'string') {
13
13
  const msg = `Value "${value}" is not a valid Timestamp`
14
- throw Object.assign(new Error(msg), { statusCode: 400 })
14
+ cds.error({ status: 400, message: msg })
15
15
  }
16
16
 
17
17
  const decimalPointIndex = _lengthIfNotFoundIndex(value.lastIndexOf('.'), value.length)
@@ -22,7 +22,7 @@ module.exports = value => {
22
22
  let dt = new Date(value.slice(0, dateEndIndex) + tz)
23
23
  if (isNaN(dt)) {
24
24
  const msg = `Value "${value}" is not a valid Timestamp`
25
- throw Object.assign(new Error(msg), { statusCode: 400 })
25
+ cds.error({ status: 400, message: msg })
26
26
  }
27
27
  const dateNoMillisNoTZ = dt.toISOString().slice(0, 19)
28
28
  const normalizedFractionalDigits = value
@@ -473,8 +473,14 @@ const _newDelete = (query, transitions, options) => {
473
473
  : targetName
474
474
 
475
475
  if (newDelete.where) {
476
- const from = typeof query.DELETE.from === 'string' ? query.DELETE.from : query.DELETE.from.ref[0]
477
- newDelete.where = _newWhere(newDelete.where, targetTransition, from, query.DELETE.from.as, undefined, options)
476
+ newDelete.where = _newWhere(
477
+ newDelete.where,
478
+ targetTransition,
479
+ query.DELETE.from.ref[0],
480
+ query.DELETE.from.as,
481
+ undefined,
482
+ options
483
+ )
478
484
  }
479
485
 
480
486
  return newDelete
@@ -45,14 +45,23 @@ const _processComplex = (processFn, row, template, key, pathOptions) => {
45
45
  if (rows.length === 0) return
46
46
  const keyNames = pathOptions.includeKeyValues && _getTargetKeyNames(template.target)
47
47
 
48
+ let rowIndex = -1
48
49
  for (const row of rows) {
50
+ rowIndex++
49
51
  if (row == null) continue
50
52
  const args = { processFn, data: row, template, isRoot: false, pathOptions }
51
53
 
52
54
  let pathSegmentInfo
53
55
  if (pathOptions.includeKeyValues) {
54
56
  pathOptions.rowUUIDGenerator?.(keyNames, row, template)
55
- pathSegmentInfo = { key, keyNames, row, elements: template.target.elements, draftKeys: pathOptions.draftKeys }
57
+ pathSegmentInfo = {
58
+ key,
59
+ keyNames,
60
+ row,
61
+ rowIndex,
62
+ elements: template.target.elements,
63
+ draftKeys: pathOptions.draftKeys
64
+ }
56
65
  }
57
66
 
58
67
  if (pathOptions.pathSegmentsInfo) pathOptions.pathSegmentsInfo.push(pathSegmentInfo || key)
@@ -1,24 +1,30 @@
1
- const segmentSerializer = pathSegmentInfo => {
1
+ const segmentKeySerializer = pathSegmentInfo => {
2
2
  const { key: tKey, row, elements, draftKeys } = pathSegmentInfo
3
3
  let keyNames = pathSegmentInfo.keyNames
4
4
 
5
5
  const keyValuePairs = keyNames
6
6
  .map(key => {
7
- let quote
7
+ const keyValue = row[key] ?? draftKeys?.[key]
8
+ if (keyValue == null) return
9
+
10
+ let formattedValue
8
11
 
9
12
  switch (elements[key].type) {
10
13
  case 'cds.String':
11
- quote = "'"
14
+ formattedValue = `'${keyValue}'`
15
+ break
16
+
17
+ case 'cds.Binary':
18
+ if (Buffer.isBuffer(keyValue)) formattedValue = keyValue.toString('base64')
19
+ formattedValue = `binary'${formattedValue}'`
12
20
  break
13
21
 
14
22
  default:
15
- quote = ''
23
+ formattedValue = keyValue
16
24
  break
17
25
  }
18
26
 
19
- const keyValue = row[key] ?? draftKeys?.[key]
20
- if (keyValue == null) return
21
- return `${key}=${quote}${keyValue}${quote}`
27
+ return `${key}=${formattedValue}`
22
28
  })
23
29
  .filter(c => c)
24
30
 
@@ -27,10 +33,16 @@ const segmentSerializer = pathSegmentInfo => {
27
33
  return pathSegment
28
34
  }
29
35
 
30
- const templatePathSerializer = (elementName, pathSegmentsInfo) => {
36
+ const segmentIndexSerializer = pathSegmentInfo => {
37
+ const { key: tKey, rowIndex } = pathSegmentInfo
38
+ return `${tKey}[${rowIndex}]`
39
+ }
40
+
41
+ const templatePathSerializer = (elementName, pathSegmentsInfo, serializeWithIndices = false) => {
31
42
  const pathSegments = pathSegmentsInfo.map(pathSegmentInfo => {
32
43
  if (typeof pathSegmentInfo === 'string') return pathSegmentInfo
33
- return segmentSerializer(pathSegmentInfo)
44
+ if (serializeWithIndices) return segmentIndexSerializer(pathSegmentInfo)
45
+ return segmentKeySerializer(pathSegmentInfo)
34
46
  })
35
47
  const path = `${pathSegments.join('/')}${pathSegments.length ? '/' : ''}${elementName}`
36
48
  return path