Skip to content

Commit f557676

Browse files
authored
PLU-309: [TILES-ATOMIC-INCREMENT-4] backend implementation for increment/decrement row value (#835)
### TL;DR Add backend implementation for 'add' and 'subtract' operations for Tiles update row action ### What changed? - Modified `patchTableRow` to handle mathematical operations - added tests - error handling for invalid numeric operations - Updated column metadata for tiles action to maintain column ordering - Fixed variable sorting logic for null order values ### How to test? 1. Create a tile with a few columns and values that are both numbers and strings 2. Test updating cells using: - normal updates (set as) - adding - subtracting - mixed 3. Verify error handling by: - Attempting math operations on non-numeric values (both original values and operands) 4. Check that columns maintain their order in the UI ### Regression test 1. Check that existing tiles update rows are still working
1 parent 090f403 commit f557676

File tree

9 files changed

+253
-60
lines changed

9 files changed

+253
-60
lines changed

packages/backend/src/apps/tiles/actions/update-row/index.ts

Lines changed: 58 additions & 22 deletions
Original file line numberDiff line numberDiff line change
@@ -1,8 +1,11 @@
11
import { IRawAction } from '@plumber/types'
22

33
import StepError from '@/errors/step'
4-
import { stripInvalidKeys } from '@/models/dynamodb/helpers'
5-
import { patchTableRow } from '@/models/dynamodb/table-row'
4+
import {
5+
autoMarshallNumberStrings,
6+
stripInvalidKeys,
7+
} from '@/models/dynamodb/helpers'
8+
import { PatchRowInput, patchTableRow } from '@/models/dynamodb/table-row'
69
import TableCollaborator from '@/models/table-collaborators'
710
import TableColumnMetadata from '@/models/table-column-metadata'
811

@@ -117,7 +120,7 @@ const action: IRawAction = {
117120
const { tableId, rowId, rowData } = $.step.parameters as {
118121
tableId: string
119122
rowId: string
120-
rowData: { columnId: string; cellValue: string }[]
123+
rowData: { columnId: string; cellValue: string; operator?: string }[]
121124
}
122125

123126
if (!tableId) {
@@ -150,20 +153,48 @@ const action: IRawAction = {
150153
return
151154
}
152155

156+
function assertNumber(value: string): void {
157+
if (typeof autoMarshallNumberStrings(value) !== 'number') {
158+
throw new StepError(
159+
'Unable to update row',
160+
'The value to add or subtract by must be a number.',
161+
$.step.position,
162+
$.app.name,
163+
)
164+
}
165+
}
166+
153167
const patchData = {
154-
...rowData.reduce((acc, { columnId, cellValue }) => {
155-
// Check that the column still exists
156-
if (columnIds.includes(columnId)) {
157-
acc[columnId] = cellValue
158-
}
159-
return acc
160-
}, {} as Record<string, string>),
168+
...rowData.reduce(
169+
(acc, { columnId, cellValue, operator }) => {
170+
// Check that the column still exists
171+
if (columnIds.includes(columnId)) {
172+
switch (operator) {
173+
case 'add':
174+
assertNumber(cellValue)
175+
acc.add[columnId] = cellValue
176+
break
177+
case 'subtract':
178+
assertNumber(cellValue)
179+
acc.subtract[columnId] = cellValue
180+
break
181+
// we default to set, since old operators will be undefined
182+
default:
183+
acc.set[columnId] = cellValue
184+
break
185+
}
186+
}
187+
return acc
188+
},
189+
{ set: {}, add: {}, subtract: {} } as PatchRowInput['patchData'],
190+
),
161191
}
192+
162193
try {
163194
const updatedRow = await patchTableRow({
164195
tableId,
165196
rowId,
166-
data: patchData,
197+
patchData,
167198
})
168199

169200
const updatedRowData = stripInvalidKeys({
@@ -179,17 +210,22 @@ const action: IRawAction = {
179210
} satisfies UpdateRowOutput,
180211
})
181212
} catch (e) {
182-
if (
183-
e instanceof Error &&
184-
e.message.includes('The conditional request failed')
185-
) {
186-
// This means the corresponding row does not exist
187-
$.setActionItem({
188-
raw: {
189-
updated: false,
190-
} satisfies UpdateRowOutput,
191-
})
192-
return
213+
if (e instanceof Error) {
214+
if (e.message.includes('The conditional request failed')) {
215+
// This means the corresponding row does not exist
216+
$.setActionItem({
217+
raw: {
218+
updated: false,
219+
} satisfies UpdateRowOutput,
220+
})
221+
return
222+
}
223+
throw new StepError(
224+
'Failed to update row',
225+
e.message,
226+
$.step.position,
227+
$.app.name,
228+
)
193229
}
194230
throw e
195231
}

packages/backend/src/apps/tiles/common/column-name-metadata.ts

Lines changed: 2 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -17,11 +17,12 @@ export async function generateColumnNameMetadata(
1717

1818
const columns = await TableColumnMetadata.query()
1919
.findByIds(columnIds)
20-
.select('id', 'name')
20+
.select('id', 'name', 'position')
2121

2222
columns.forEach((column) => {
2323
columnMetadata[column.id] = {
2424
label: column.name,
25+
order: column.position,
2526
}
2627
})
2728
return { [parentKey]: columnMetadata }

packages/backend/src/models/dynamodb/__tests__/table-row/functions.itest.ts

Lines changed: 116 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -390,12 +390,124 @@ describe('dynamodb table row functions', () => {
390390
const updatedRow = await patchTableRow({
391391
tableId: dummyTable.id,
392392
rowId: row.rowId,
393-
data: newData,
393+
patchData: {
394+
set: newData,
395+
},
394396
})
395397

396398
expect(updatedRow.data).toEqual({ ...data, ...newData })
397399
})
398400

401+
it('should set, add or subtract values for the row', async () => {
402+
const data = {
403+
[dummyColumnIds[0]]: 10,
404+
[dummyColumnIds[1]]: 20,
405+
[dummyColumnIds[2]]: 30,
406+
}
407+
const row = await createTableRow({ tableId: dummyTable.id, data })
408+
const updatedRow = await patchTableRow({
409+
tableId: dummyTable.id,
410+
rowId: row.rowId,
411+
patchData: {
412+
set: {
413+
[dummyColumnIds[0]]: '1',
414+
},
415+
add: {
416+
[dummyColumnIds[1]]: '2',
417+
},
418+
subtract: {
419+
[dummyColumnIds[2]]: '3',
420+
},
421+
},
422+
})
423+
424+
const expectedData = {
425+
[dummyColumnIds[0]]: 1,
426+
[dummyColumnIds[1]]: 22,
427+
[dummyColumnIds[2]]: 27,
428+
}
429+
430+
expect(updatedRow.data).toEqual(expectedData)
431+
})
432+
433+
it('should throw a step error if operand is not a number', async () => {
434+
const data = {
435+
[dummyColumnIds[0]]: 10,
436+
[dummyColumnIds[1]]: 20,
437+
[dummyColumnIds[2]]: 30,
438+
}
439+
const row = await createTableRow({ tableId: dummyTable.id, data })
440+
441+
await expect(
442+
patchTableRow({
443+
tableId: dummyTable.id,
444+
rowId: row.rowId,
445+
patchData: {
446+
set: {
447+
[dummyColumnIds[0]]: '1',
448+
},
449+
add: {
450+
[dummyColumnIds[1]]: 'add',
451+
},
452+
subtract: {
453+
[dummyColumnIds[2]]: '3',
454+
},
455+
},
456+
}),
457+
).rejects.toThrow(Error)
458+
})
459+
460+
it('should throw a generic error if original value is not a number', async () => {
461+
const data = {
462+
[dummyColumnIds[0]]: 10,
463+
[dummyColumnIds[1]]: 20,
464+
[dummyColumnIds[2]]: 'string',
465+
}
466+
const row = await createTableRow({ tableId: dummyTable.id, data })
467+
468+
await expect(
469+
patchTableRow({
470+
tableId: dummyTable.id,
471+
rowId: row.rowId,
472+
patchData: {
473+
set: {
474+
[dummyColumnIds[0]]: '1',
475+
},
476+
add: {
477+
[dummyColumnIds[1]]: '2re',
478+
},
479+
subtract: {
480+
[dummyColumnIds[2]]: '3',
481+
},
482+
},
483+
}),
484+
).rejects.toThrow(Error)
485+
})
486+
487+
it('should work fine if only add/subtract is provided', async () => {
488+
const data = {
489+
[dummyColumnIds[0]]: 10,
490+
[dummyColumnIds[1]]: 20,
491+
[dummyColumnIds[2]]: 30,
492+
}
493+
const row = await createTableRow({ tableId: dummyTable.id, data })
494+
495+
const updatedRow = await patchTableRow({
496+
tableId: dummyTable.id,
497+
rowId: row.rowId,
498+
patchData: {
499+
subtract: {
500+
[dummyColumnIds[2]]: '3',
501+
},
502+
},
503+
})
504+
expect(updatedRow.data).toEqual({
505+
[dummyColumnIds[0]]: 10,
506+
[dummyColumnIds[1]]: 20,
507+
[dummyColumnIds[2]]: 27,
508+
})
509+
})
510+
399511
it('should not change if no columns are provided', async () => {
400512
const data = generateMockTableRowData({
401513
columnIds: dummyColumnIds,
@@ -404,7 +516,7 @@ describe('dynamodb table row functions', () => {
404516
const updatedRow = await patchTableRow({
405517
tableId: dummyTable.id,
406518
rowId: row.rowId,
407-
data: {},
519+
patchData: {},
408520
})
409521

410522
expect(updatedRow.data).toEqual(data)
@@ -418,7 +530,7 @@ describe('dynamodb table row functions', () => {
418530
const updatedRow = await patchTableRow({
419531
tableId: dummyTable.id,
420532
rowId: row.rowId,
421-
data: { [dummyColumnIds[0]]: '123' },
533+
patchData: { set: { [dummyColumnIds[0]]: '123' } },
422534
})
423535
expect(updatedRow.data).toEqual({ ...data, [dummyColumnIds[0]]: 123 })
424536
})
@@ -431,7 +543,7 @@ describe('dynamodb table row functions', () => {
431543
const updatedRow = await patchTableRow({
432544
tableId: dummyTable.id,
433545
rowId: row.rowId,
434-
data: { [dummyColumnIds[0]]: '123' },
546+
patchData: { [dummyColumnIds[0]]: '123' },
435547
})
436548
expect(updatedRow.updatedAt).toBeGreaterThan(row.updatedAt)
437549
})

packages/backend/src/models/dynamodb/helpers.ts

Lines changed: 14 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -27,6 +27,16 @@ export function handleDynamoDBError(error: unknown): never {
2727
delayType: 'step',
2828
})
2929
}
30+
if (
31+
message.includes(
32+
'An operand in the update expression has an incorrect data type',
33+
) ||
34+
message.includes('Incorrect operand type for operator or function')
35+
) {
36+
throw new Error(
37+
'You can only add or subtract numbers, please check your values.',
38+
)
39+
}
3040
throw new Error('DynamoDB Internal Error: ' + message)
3141
}
3242
if (error.code < 6000) {
@@ -89,11 +99,11 @@ export function stripInvalidKeys({
8999
columnIds: string[]
90100
data: Record<string, string | number>
91101
}) {
92-
const validKeys = new Set(columnIds)
93102
const strippedData: Record<string, string | number> = {}
94-
for (const [key, value] of Object.entries(data)) {
95-
if (validKeys.has(key)) {
96-
strippedData[key] = value
103+
// Iterate through columnIds to maintain order
104+
for (const columnId of columnIds) {
105+
if (columnId in data) {
106+
strippedData[columnId] = data[columnId]
97107
}
98108
}
99109
return strippedData

packages/backend/src/models/dynamodb/table-row/functions.ts

Lines changed: 31 additions & 16 deletions
Original file line numberDiff line numberDiff line change
@@ -10,6 +10,7 @@ import {
1010
CreateRowInput,
1111
CreateRowsInput,
1212
DeleteRowsInput,
13+
PatchRowInput,
1314
TableRowFilter,
1415
TableRowFilterOperator,
1516
TableRowItem,
@@ -227,26 +228,40 @@ export const updateTableRow = async ({
227228
export const patchTableRow = async ({
228229
rowId,
229230
tableId,
230-
data: patchData,
231-
}: UpdateRowInput): Promise<TableRowItem> => {
231+
patchData,
232+
}: PatchRowInput): Promise<TableRowItem> => {
232233
try {
233-
const res = await TableRow.patch({
234+
const patchOperation = TableRow.patch({
234235
tableId,
235236
rowId,
237+
}).data(({ data }, { set, add, subtract }) => {
238+
// Handle set operations
239+
Object.entries(patchData.set || {}).forEach(
240+
([key, value]: [string, string]) => {
241+
set(data[key], value ? autoMarshallNumberStrings(value) : '')
242+
},
243+
)
244+
245+
// Handle add operations
246+
Object.entries(patchData.add || {}).forEach(
247+
([key, value]: [string, string]) => {
248+
add(data[key], autoMarshallNumberStrings(value))
249+
},
250+
)
251+
252+
// Handle subtract operations
253+
Object.entries(patchData.subtract || {}).forEach(
254+
([key, value]: [string, string]) => {
255+
subtract(data[key], autoMarshallNumberStrings(value))
256+
},
257+
)
236258
})
237-
.data(({ data }, { set }) => {
238-
for (const key in patchData) {
239-
set(
240-
data[key],
241-
patchData[key] ? autoMarshallNumberStrings(patchData[key]) : '',
242-
)
243-
}
244-
})
245-
.go({
246-
ignoreOwnership: true,
247-
// Return the new row data
248-
response: 'all_new',
249-
})
259+
260+
const res = await patchOperation.go({
261+
ignoreOwnership: true,
262+
response: 'all_new',
263+
})
264+
250265
return res.data
251266
} catch (e: unknown) {
252267
handleDynamoDBError(e)

packages/backend/src/models/dynamodb/table-row/types.ts

Lines changed: 9 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -8,7 +8,15 @@ export type CreateRowsInput = {
88
tableId: string
99
dataArray: Array<TableRowItem['data']>
1010
}
11-
export type UpdateRowInput = Pick<TableRowItem, 'tableId' | 'data' | 'rowId'>
11+
export type UpdateRowInput = Pick<TableRowItem, 'tableId' | 'rowId' | 'data'>
12+
13+
export type PatchRowInput = Pick<TableRowItem, 'tableId' | 'rowId'> & {
14+
patchData: {
15+
set?: TableRowItem['data']
16+
add?: TableRowItem['data']
17+
subtract?: TableRowItem['data']
18+
}
19+
}
1220
export interface DeleteRowsInput {
1321
tableId: string
1422
rowIds: string[]

0 commit comments

Comments
 (0)