Skip to content

Commit 68b7e13

Browse files
authored
Release v1.55.0 (#1298)
- feat: GatherSG trigger and create case action - fix: throw http 400 when multipart request is invalid - fix: reduce chunk size based on columns - fix: cannot remove last multirow - fix: only throw multipart error if error
2 parents 0bc70a9 + 2849ea7 commit 68b7e13

File tree

47 files changed

+1409
-117
lines changed

Some content is hidden

Large Commits have some content hidden by default. Use the searchbox below for content that may be hidden.

47 files changed

+1409
-117
lines changed

ecs/env.json

Lines changed: 5 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -257,6 +257,10 @@
257257
{
258258
"name": "SSO_DISCOVERY_URL",
259259
"valueFrom": "plumber-<ENVIRONMENT>-sso-discovery-url"
260+
},
261+
{
262+
"name": "GATHERSG_PUBLIC_KEY",
263+
"valueFrom": "plumber-gathersg-public-key"
260264
}
261265
]
262-
}
266+
}

package-lock.json

Lines changed: 3 additions & 3 deletions
Some generated files are not rendered by default. Learn more about customizing how changed files appear on GitHub.

packages/backend/.env-example

Lines changed: 4 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -66,4 +66,7 @@ M365_EXCEL_INTERVAL_BETWEEN_ACTIONS_MS=1000
6666
# SSO LOGIN
6767
SSO_CLIENT_ID=...
6868
SSO_CLIENT_SECRET=...
69-
SSO_DISCOVERY_URL=...
69+
SSO_DISCOVERY_URL=...
70+
71+
# GATHERSG
72+
GATHERSG_PUBLIC_KEY=gathersg-public-key

packages/backend/package.json

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -111,5 +111,5 @@
111111
"tsconfig-paths": "^4.2.0",
112112
"type-fest": "4.10.3"
113113
},
114-
"version": "1.54.5"
114+
"version": "1.55.0"
115115
}
Lines changed: 112 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,112 @@
1+
import { describe, expect, it } from 'vitest'
2+
import type { SafeParseError, ZodIssue } from 'zod'
3+
4+
import { encryptionKeySchema } from '../../../triggers/new-instant-workflow/schema'
5+
6+
describe('encryptionKeySchema', () => {
7+
it('accepts a valid key', () => {
8+
const valid = 'Abcdefghij1$'
9+
const parsed = encryptionKeySchema.safeParse(valid)
10+
expect(parsed.success).toBe(true)
11+
})
12+
13+
it('rejects keys shorter than 12 chars', () => {
14+
const result = encryptionKeySchema.safeParse('Abcdef1$A')
15+
expect(result.success).toBe(false)
16+
if (!result.success) {
17+
const { issues } = (result as unknown as SafeParseError<string>).error
18+
expect(
19+
issues.some((i: ZodIssue) => i.message.includes('be at least 12')),
20+
).toBe(true)
21+
}
22+
})
23+
24+
it('rejects keys longer than 20 chars', () => {
25+
const result = encryptionKeySchema.safeParse('Abcdefghijklmnopqr1$S')
26+
expect(result.success).toBe(false)
27+
if (!result.success) {
28+
const { issues } = (result as unknown as SafeParseError<string>).error
29+
expect(
30+
issues.some((i: ZodIssue) => i.message.includes('be at most 20')),
31+
).toBe(true)
32+
}
33+
})
34+
35+
it('requires at least one number', () => {
36+
const result = encryptionKeySchema.safeParse('Abcdefghijk$')
37+
expect(result.success).toBe(false)
38+
if (!result.success) {
39+
const { issues } = (result as unknown as SafeParseError<string>).error
40+
expect(
41+
issues.some((i: ZodIssue) =>
42+
i.message.includes('contain at least 1 number'),
43+
),
44+
).toBe(true)
45+
}
46+
})
47+
48+
it('requires at least one uppercase letter', () => {
49+
const result = encryptionKeySchema.safeParse('abcdefghi1$z')
50+
expect(result.success).toBe(false)
51+
if (!result.success) {
52+
const { issues } = (result as unknown as SafeParseError<string>).error
53+
expect(
54+
issues.some((i: ZodIssue) =>
55+
i.message.includes('contain at least 1 uppercase letter'),
56+
),
57+
).toBe(true)
58+
}
59+
})
60+
61+
it('requires at least one special character', () => {
62+
const result = encryptionKeySchema.safeParse('Abcdefghij12')
63+
expect(result.success).toBe(false)
64+
if (!result.success) {
65+
const { issues } = (result as unknown as SafeParseError<string>).error
66+
expect(
67+
issues.some((i: ZodIssue) =>
68+
i.message.includes('contain at least 1 special character'),
69+
),
70+
).toBe(true)
71+
}
72+
})
73+
74+
it('rejects leading whitespace', () => {
75+
const result = encryptionKeySchema.safeParse(' Abcdefghij1$')
76+
expect(result.success).toBe(false)
77+
if (!result.success) {
78+
const { issues } = (result as unknown as SafeParseError<string>).error
79+
expect(
80+
issues.some((i: ZodIssue) =>
81+
i.message.includes('not have leading or trailing whitespace'),
82+
),
83+
).toBe(true)
84+
}
85+
})
86+
87+
it('rejects trailing whitespace', () => {
88+
const result = encryptionKeySchema.safeParse('Abcdefghij1$ ')
89+
expect(result.success).toBe(false)
90+
if (!result.success) {
91+
const { issues } = (result as unknown as SafeParseError<string>).error
92+
expect(
93+
issues.some((i: ZodIssue) =>
94+
i.message.includes('not have leading or trailing whitespace'),
95+
),
96+
).toBe(true)
97+
}
98+
})
99+
100+
it('rejects both leading and trailing whitespace', () => {
101+
const result = encryptionKeySchema.safeParse(' Abcdefghij1$ ')
102+
expect(result.success).toBe(false)
103+
if (!result.success) {
104+
const { issues } = (result as unknown as SafeParseError<string>).error
105+
expect(
106+
issues.some((i: ZodIssue) =>
107+
i.message.includes('not have leading or trailing whitespace'),
108+
),
109+
).toBe(true)
110+
}
111+
})
112+
})

packages/backend/src/apps/gathersg/__tests__/actions/update-case.test.ts

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -216,7 +216,7 @@ describe('update case', () => {
216216
mocks.httpPatch.mockRejectedValueOnce(httpError)
217217

218218
await expect(updateCaseAction.run($)).rejects.toThrowError(
219-
'Check that you have entered the correct value type for the following fields: age, score',
219+
'Check that you have provided values for required fields and entered the correct value type (e.g., numbers, strings, etc.) for: age, score',
220220
)
221221
})
222222

Lines changed: 29 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,29 @@
1+
import { IDataOutMetadata, IExecutionStep } from '@plumber/types'
2+
3+
import { responseSchema } from './schema'
4+
5+
async function getDataOutMetadata(
6+
step: IExecutionStep,
7+
): Promise<IDataOutMetadata> {
8+
const { dataOut: rawDataOut } = step
9+
if (!rawDataOut) {
10+
return null
11+
}
12+
13+
responseSchema.parse(rawDataOut)
14+
15+
const defaultMetadata = {
16+
data: {
17+
uuid: {
18+
label: 'Case UUID',
19+
},
20+
caseRef: {
21+
label: 'Case ref',
22+
},
23+
},
24+
}
25+
26+
return defaultMetadata
27+
}
28+
29+
export default getDataOutMetadata
Lines changed: 184 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,184 @@
1+
import { IRawAction } from '@plumber/types'
2+
3+
import { ZodError } from 'zod'
4+
import { fromZodError } from 'zod-validation-error'
5+
6+
import HttpError from '@/errors/http'
7+
import StepError, { GenericSolution } from '@/errors/step'
8+
import logger from '@/helpers/logger'
9+
import { ensureZodEnumValue } from '@/helpers/zod-utils'
10+
11+
import { fetchCaseFields } from '../../common/fetch-case-fields'
12+
import throwGatherSGStepError from '../../common/throw-errors'
13+
14+
import getDataOutMetadata from './get-data-out-metadata'
15+
import { fieldTypeEnum, requestSchema, responseSchema } from './schema'
16+
17+
const action: IRawAction = {
18+
name: 'Create case',
19+
key: 'createCase',
20+
description: 'Create a case',
21+
arguments: [
22+
{
23+
label: 'Case type',
24+
key: 'caseType',
25+
type: 'dropdown' as const,
26+
description: 'Select the type of the case you want to create',
27+
required: true,
28+
variables: false,
29+
showOptionValue: false,
30+
source: {
31+
type: 'query' as const,
32+
name: 'getDynamicData' as const,
33+
arguments: [
34+
{
35+
name: 'key',
36+
value: 'getCaseTypes',
37+
},
38+
],
39+
},
40+
},
41+
{
42+
label: 'Case status',
43+
key: 'caseStatus',
44+
type: 'dropdown' as const,
45+
description: 'Select the status you want to update the case to.',
46+
required: false,
47+
variables: true,
48+
showOptionValue: false,
49+
hiddenIf: {
50+
fieldKey: 'caseType',
51+
op: 'is_empty',
52+
},
53+
source: {
54+
type: 'query' as const,
55+
name: 'getDynamicData' as const,
56+
arguments: [{ name: 'key', value: 'getCaseStatuses' }],
57+
},
58+
},
59+
{
60+
label: 'Case fields',
61+
key: 'caseFields',
62+
type: 'multirow-multicol' as const,
63+
required: true,
64+
description:
65+
'Specify values for each field you want to update in your case. Note that fields that require an array of objects as a value are not supported yet.',
66+
hiddenIf: {
67+
fieldKey: 'caseType',
68+
op: 'is_empty',
69+
},
70+
subFields: [
71+
{
72+
placeholder: 'Field',
73+
key: 'field',
74+
type: 'dropdown' as const,
75+
showOptionValue: false,
76+
required: true,
77+
variables: false,
78+
allowArbitrary: true,
79+
source: {
80+
type: 'query' as const,
81+
name: 'getDynamicData' as const,
82+
arguments: [
83+
{
84+
name: 'key',
85+
value: 'getCaseFields',
86+
},
87+
{
88+
name: 'parameters.caseType',
89+
value: '{parameters.caseType}',
90+
},
91+
],
92+
},
93+
customStyle: { flex: 2 },
94+
},
95+
{
96+
placeholder: 'Field type',
97+
key: 'fieldType',
98+
type: 'dropdown' as const,
99+
showOptionValue: false,
100+
required: true,
101+
value: 'string',
102+
variables: false,
103+
options: [
104+
{
105+
label: 'Text',
106+
value: ensureZodEnumValue(fieldTypeEnum, 'string'),
107+
},
108+
{
109+
label: 'Number',
110+
value: ensureZodEnumValue(fieldTypeEnum, 'number'),
111+
},
112+
{
113+
label: 'Null',
114+
value: ensureZodEnumValue(fieldTypeEnum, 'null'),
115+
},
116+
],
117+
customStyle: { flex: 1, maxWidth: 140 },
118+
},
119+
{
120+
placeholder: 'Value',
121+
key: 'value',
122+
type: 'string' as const,
123+
required: true,
124+
variables: true,
125+
hiddenIf: {
126+
fieldKey: 'fieldType',
127+
op: 'equals',
128+
fieldValue: 'null',
129+
},
130+
customStyle: { flex: 3, minWidth: 0, maxWidth: '60%' },
131+
},
132+
],
133+
},
134+
],
135+
136+
getDataOutMetadata,
137+
138+
async run($) {
139+
try {
140+
const parameters = requestSchema.parse($.step.parameters)
141+
const { caseType: caseTypeUuid, status, fields } = parameters
142+
143+
// get the case type name from the case type uuid
144+
const { caseTypeName } = await fetchCaseFields({ $, caseTypeUuid })
145+
146+
const payload = {
147+
...(status && { status }),
148+
fields,
149+
type: caseTypeName,
150+
}
151+
152+
const rawResponse = await $.http.post('/cases', payload)
153+
const { data } = responseSchema.parse(rawResponse.data)
154+
155+
$.setActionItem({
156+
raw: {
157+
data,
158+
},
159+
})
160+
} catch (error) {
161+
logger.error('Failed to create case on GatherSG:', error)
162+
if (error instanceof ZodError) {
163+
const firstError = fromZodError(error).details[0]
164+
throw new StepError(
165+
`${firstError.path[0]}: ${firstError.message}`,
166+
GenericSolution.ReconfigureInvalidField,
167+
$.step.position,
168+
$.app.name,
169+
)
170+
}
171+
if (error instanceof HttpError) {
172+
throwGatherSGStepError({ $, error })
173+
}
174+
throw new StepError(
175+
`An error occurred: '${error.message}'`,
176+
'Please check that you have configured your step correctly',
177+
$.step.position,
178+
$.app.name,
179+
)
180+
}
181+
},
182+
}
183+
184+
export default action

0 commit comments

Comments
 (0)