• Home
  • Features
  • Pricing
  • Docs
  • Announcements
  • Sign In

opengovsg / FormSG / 14314964150

07 Apr 2025 05:09PM UTC coverage: 72.116% (-0.3%) from 72.421%
14314964150

push

github

GitHub
Merge pull request #8296 from opengovsg/release-al2

2878 of 4861 branches covered (59.21%)

Branch coverage included in aggregate %.

10348 of 13479 relevant lines covered (76.77%)

48.09 hits per line

Source File
Press 'n' to go to next uncovered line, 'b' for previous

27.87
/src/app/modules/form/admin-form/admin-form.assistance.controller.ts
1
import { celebrate, Joi, Segments } from 'celebrate'
7✔
2
import { AuthedSessionData } from 'express-session'
3
import { StatusCodes } from 'http-status-codes'
7✔
4

5
import {
7✔
6
  featureFlags,
7
  MFB_VISION_MAX_IMAGES_COUNT,
8
} from '../../../../../shared/constants'
9
import { createLoggerWithLabel } from '../../../config/logger'
7✔
10
import { createReqMeta } from '../../../utils/request'
7✔
11
import * as AuthService from '../../auth/auth.service'
7✔
12
import { ControllerHandler } from '../../core/core.types'
13
import * as UserService from '../../user/user.service'
7✔
14

15
import {
7✔
16
  createFormFieldsUsingTextPrompt,
17
  createFormFieldsUsingVisionPrompt,
18
} from './admin-form.assistance.service'
19
import { PermissionLevel } from './admin-form.types'
7✔
20
import { mapRouteError, verifyUserBetaflag } from './admin-form.utils'
7✔
21

22
const logger = createLoggerWithLabel(module)
7✔
23

24
const handleTextPromptValidator = celebrate({
7✔
25
  [Segments.PARAMS]: {
26
    formId: Joi.string()
27
      .required()
28
      .pattern(/^[a-fA-F0-9]{24}$/)
29
      .message('Your form ID is invalid.'),
30
  },
31
  [Segments.BODY]: {
32
    prompt: Joi.string().required().max(500),
33
  },
34
})
35

36
interface ITextPrompt {
37
  prompt: string
38
}
39

40
const _handleTextPrompt: ControllerHandler<
41
  { formId: string },
42
  { message: string; createdFieldIds?: string[] },
43
  ITextPrompt
44
> = async (req, res) => {
7✔
45
  const { formId } = req.params
×
46
  const sessionUserId = (req.session as AuthedSessionData).user._id
×
47

48
  const gb = req.growthbook
×
49

50
  if (!gb?.isOn(featureFlags.mfb)) {
×
51
    return res.status(StatusCodes.FORBIDDEN).json({
×
52
      message: 'This feature is currently unavailable.',
53
    })
54
  }
55

56
  // Step 1: Retrieve currently logged in user.
57
  return UserService.getPopulatedUserById(sessionUserId)
×
58
    .andThen((user) =>
59
      // Step 2: Retrieve form with write permission check.
60
      AuthService.getFormAfterPermissionChecks({
×
61
        user,
62
        formId,
63
        level: PermissionLevel.Write,
64
      }).map((form) => ({ user, form })),
×
65
    )
66
    .map(({ user, form }) => {
67
      logger.info({
×
68
        message: 'Generating form fields using text prompt',
69
        meta: {
70
          action: '_handleTextPrompt',
71
          ...createReqMeta(req),
72
          userId: sessionUserId,
73
          userEmail: user.email,
74
          formId,
75
          promptLength: req.body.prompt.length,
76
        },
77
      })
78
      return form
×
79
    }) // Step 3: Create form fields using text prompt.
80
    .andThen((form) =>
81
      createFormFieldsUsingTextPrompt({
×
82
        form,
83
        userPrompt: req.body.prompt,
84
      }),
85
    )
86
    .map((createdFieldIds) =>
87
      res.status(StatusCodes.OK).json({
×
88
        message: 'Created form fields using text prompt successfully.',
89
        createdFieldIds: createdFieldIds.map((field) => field._id.toString()),
×
90
      }),
91
    )
92
    .mapErr((error) => {
93
      logger.error({
×
94
        message: 'Error occurred creating form fields using text prompt.',
95
        meta: {
96
          action: '_handleTextPrompt',
97
          ...createReqMeta(req),
98
          userId: sessionUserId,
99
          formId,
100
          userPrompt: req.body.prompt,
101
        },
102
        error,
103
      })
104
      const { errorMessage, statusCode } = mapRouteError(error)
×
105
      return res.status(statusCode).json({ message: errorMessage })
×
106
    })
107
}
108

109
export const handleTextPrompt = [
7✔
110
  handleTextPromptValidator,
111
  _handleTextPrompt,
112
] as ControllerHandler[]
113

114
const handleVisionPromptValidator = celebrate({
7✔
115
  [Segments.PARAMS]: {
116
    formId: Joi.string()
117
      .required()
118
      .pattern(/^[a-fA-F0-9]{24}$/)
119
      .message('Your form ID is invalid.'),
120
  },
121
  [Segments.BODY]: {
122
    imageDataUrls: Joi.array()
123
      .items(Joi.string())
124
      .required()
125
      .min(1)
126
      .max(MFB_VISION_MAX_IMAGES_COUNT),
127
  },
128
})
129

130
interface IVisionPrompt {
131
  imageDataUrls: string[]
132
}
133

134
const _handleVisionPrompt: ControllerHandler<
135
  { formId: string },
136
  { message: string; createdFieldIds?: string[] },
137
  IVisionPrompt
138
> = async (req, res) => {
7✔
139
  const { formId } = req.params
×
140
  const sessionUserId = (req.session as AuthedSessionData).user._id
×
141
  const { imageDataUrls } = req.body
×
142
  const gb = req.growthbook
×
143

144
  if (!gb?.isOn(featureFlags.mfbVision)) {
×
145
    return res.status(StatusCodes.FORBIDDEN).json({
×
146
      message: 'This feature is currently unavailable.',
147
    })
148
  }
149

150
  // Step 1: Retrieve currently logged in user.
151
  return (
×
152
    UserService.getPopulatedUserById(sessionUserId)
153
      .andThen((user) => {
154
        return verifyUserBetaflag(user, 'mfbVision')
×
155
      })
156
      .andThen((user) =>
157
        // Step 2: Retrieve form with write permission check.
158
        AuthService.getFormAfterPermissionChecks({
×
159
          user,
160
          formId,
161
          level: PermissionLevel.Write,
162
        }).map((form) => ({ user, form })),
×
163
      )
164
      .map(({ user, form }) => {
165
        logger.info({
×
166
          message: 'Generating form fields using vision prompt',
167
          meta: {
168
            action: '_handleVisionPrompt',
169
            ...createReqMeta(req),
170
            userId: sessionUserId,
171
            userEmail: user.email,
172
            formId,
173
            numImagesInPrompt: imageDataUrls.length,
174
          },
175
        })
176
        return { form }
×
177
      })
178
      // Step 3: Create form fields using text prompt.
179
      .andThen(({ form }) =>
180
        createFormFieldsUsingVisionPrompt({
×
181
          form,
182
          imageDataUrls,
183
        }),
184
      )
185
      .map((createdFieldIds) =>
186
        res.status(StatusCodes.OK).json({
×
187
          message: 'Created form fields using vision prompt successfully.',
188
          createdFieldIds: createdFieldIds.map((field) => field._id.toString()),
×
189
        }),
190
      )
191
      .mapErr((error) => {
192
        logger.error({
×
193
          message: 'Error occurred creating form fields using vision prompt.',
194
          meta: {
195
            action: '_handleVisionPrompt',
196
            ...createReqMeta(req),
197
            userId: sessionUserId,
198
            formId,
199
            numImagesInPrompt: req.body.imageDataUrls.length,
200
          },
201
          error,
202
        })
203
        const { errorMessage, statusCode } = mapRouteError(error)
×
204
        return res.status(statusCode).json({ message: errorMessage })
×
205
      })
206
  )
207
}
208

209
export const handleVisionPrompt = [
7✔
210
  handleVisionPromptValidator,
211
  _handleVisionPrompt,
212
]
STATUS · Troubleshooting · Open an Issue · Sales · Support · CAREERS · ENTERPRISE · START FREE · SCHEDULE DEMO
ANNOUNCEMENTS · TWITTER · TOS & SLA · Supported CI Services · What's a CI service? · Automated Testing

© 2026 Coveralls, Inc