Skip to content

Commit 0a6643f

Browse files
Merge pull request #36 from NYCU-SDC/feat/CORE-123-add-form-workflow-api
Add Form Workflow
2 parents 904cf18 + 8d65d19 commit 0a6643f

4 files changed

Lines changed: 202 additions & 5 deletions

File tree

main.tsp

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -8,6 +8,7 @@ import "./service/forms.tsp";
88
import "./service/responses.tsp";
99
import "./service/inbox.tsp";
1010
import "./service/slug.tsp";
11+
import "./service/form_workflow.tsp";
1112

1213
using Http;
1314
using Versioning;

service/form_workflow.tsp

Lines changed: 187 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,187 @@
1+
import "@typespec/http";
2+
import "@typespec/openapi3";
3+
import "./forms.tsp";
4+
5+
using Http;
6+
using OpenAPI;
7+
using CoreSystem.Forms;
8+
9+
@tag("Form Workflow")
10+
namespace CoreSystem.FormWorkflow {
11+
@doc("Node type enum for workflow nodes")
12+
enum NodeType {
13+
@doc("Section node - represents a form section/page")
14+
section: "SECTION",
15+
16+
@doc("End node - represents the end of the workflow (thank you page and redirect)")
17+
end: "END",
18+
19+
@doc("Start node - represents the workflow entry point")
20+
start: "START",
21+
22+
@doc("Condition node - represents a condition check node")
23+
condition: "CONDITION",
24+
}
25+
26+
enum CreateNodeRequestType {
27+
@doc("Create a section node - represents a form section")
28+
section: "SECTION",
29+
30+
@doc("Create a condition node - represents a condition check node")
31+
condition: "CONDITION",
32+
}
33+
34+
@doc("Data source for condition rules")
35+
enum ConditionSource {
36+
@doc("Source from choice answer")
37+
choice: "CHOICE",
38+
39+
@doc("Source from non-choice answer")
40+
nonChoice: "NON_CHOICE",
41+
}
42+
43+
@doc("Condition rule for condition nodes")
44+
model ConditionRule {
45+
@doc("Source of data for the condition")
46+
@example(ConditionSource.nonChoice)
47+
source: ConditionSource;
48+
49+
@doc("The question ID to check against")
50+
@example("9a843aa0-8451-4e3b-b6a0-8c0e994e9040")
51+
question: uuid;
52+
53+
@doc("Regex pattern to match against. If source is NON_CHOICE, the regex is matched against the answer value. If source is CHOICE, the answer is an array of selected choice option IDs, and this regex is matched against each element; the condition passes if any element matches (e.g. use an exact-match UUID regex like ^<uuid>$).")
54+
@format("regex")
55+
@example("^([6-9]|[1-9][0-9]+)$") // non-choice source
56+
@example("^f421cfc2-2b84-4da9-9629-011af609935a$") // choice source (any selected option ID equals this UUID)
57+
pattern: string;
58+
}
59+
60+
@doc("Workflow node structure with linked list connections")
61+
model NodeStructure {
62+
@doc("Unique identifier for the node.")
63+
id: uuid;
64+
65+
@doc("Type of the node")
66+
@example(NodeType.start)
67+
type: NodeType;
68+
69+
@doc("The label for the node")
70+
@example("開始表單")
71+
label: string;
72+
}
73+
74+
@doc("Request body for creating a new node for a workflow")
75+
@example(#{ type: CreateNodeRequestType.section })
76+
model CreateNodeRequest {
77+
@doc("Type of the node")
78+
@example(CreateNodeRequestType.section)
79+
type: CreateNodeRequestType;
80+
}
81+
82+
@doc("Workflow node request model with linked list connections")
83+
model NodeRequest {
84+
@doc("Unique identifier for the node.")
85+
id: uuid;
86+
87+
@doc("The label for the node")
88+
@example("開始表單")
89+
label: string;
90+
91+
@doc("The condition rule for the node, only for condition nodes")
92+
conditionRule?: ConditionRule;
93+
94+
@doc("Next node id for sequential flow (for start and section nodes)")
95+
next?: uuid;
96+
97+
@doc("Next node id when condition is true (for condition nodes)")
98+
nextTrue?: uuid;
99+
100+
@doc("Next node id when condition is false (for condition nodes)")
101+
nextFalse?: uuid;
102+
}
103+
104+
@doc("Workflow node response model with linked list connections")
105+
model NodeResponse {
106+
...NodeStructure;
107+
108+
@doc("The condition rule for the node, only for condition nodes")
109+
conditionRule: ConditionRule;
110+
111+
@doc("Next node id for sequential flow (for start and section nodes)")
112+
next: uuid;
113+
114+
@doc("Next node id when condition is true (for condition nodes)")
115+
nextTrue: uuid;
116+
117+
@doc("Next node id when condition is false (for condition nodes)")
118+
nextFalse: uuid;
119+
}
120+
121+
// API Endpoints
122+
123+
@doc("Get workflow for a form")
124+
@route("/forms/{id}/workflow")
125+
@get
126+
op getWorkflow(@path id: uuid): {
127+
@statusCode statusCode: 200;
128+
@body response: NodeResponse[];
129+
} | {
130+
@statusCode statusCode: 404;
131+
@body error: NotFound;
132+
};
133+
134+
@doc("Update workflow for a form")
135+
@route("/forms/{id}/workflow")
136+
@put
137+
op updateWorkflow(@path id: uuid, @body request: NodeRequest[]): {
138+
@statusCode statusCode: 200;
139+
@body response: NodeResponse[];
140+
} | {
141+
@statusCode statusCode: 404;
142+
@body error: NotFound;
143+
} | {
144+
@statusCode statusCode: 400;
145+
@body error: ProblemDetail;
146+
};
147+
148+
@doc("Activate workflow for a form")
149+
@route("/forms/{id}/workflow/activate")
150+
@post
151+
op activateWorkflow(@path id: uuid, @body request: NodeRequest[]): {
152+
@statusCode statusCode: 200;
153+
} | {
154+
@statusCode statusCode: 404;
155+
@body error: NotFound;
156+
} | {
157+
@statusCode statusCode: 400;
158+
@body error: ProblemDetail;
159+
};
160+
161+
@doc("Create a new node for a workflow")
162+
@route("/forms/{formId}/workflow/nodes")
163+
@post
164+
op createNode(@path formId: uuid, @body request: CreateNodeRequest): {
165+
@statusCode statusCode: 201;
166+
@body response: NodeStructure;
167+
} | {
168+
@statusCode statusCode: 404;
169+
@body error: NotFound;
170+
} | {
171+
@statusCode statusCode: 400;
172+
@body error: ProblemDetail;
173+
};
174+
175+
@doc("Delete a node for a workflow")
176+
@route("/forms/{formId}/workflow/nodes/{nodeId}")
177+
@delete
178+
op deleteNode(@path formId: uuid, @path nodeId: uuid): {
179+
@statusCode statusCode: 204;
180+
} | {
181+
@statusCode statusCode: 404;
182+
@body error: NotFound;
183+
} | {
184+
@statusCode statusCode: 400;
185+
@body error: ProblemDetail;
186+
};
187+
}

service/forms.tsp

Lines changed: 12 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -410,6 +410,18 @@ namespace CoreSystem.Forms {
410410
@doc("Upload a cover image for a form. The frontend should send images in WebP format with max-width/height: 1800px and quality 80%.")
411411
@route("/forms/{id}/cover")
412412
@post
413+
op submitSection(
414+
@path formId: uuid,
415+
@path responseId: uuid,
416+
@path sectionId: uuid,
417+
): {
418+
@statusCode statusCode: 200;
419+
@body response: NextSectionResponse;
420+
} | {
421+
@statusCode statusCode: 400;
422+
@body error: ProblemDetail;
423+
};
424+
413425
op uploadFormCoverImage(
414426
@path id: uuid,
415427
@header("content-type") contentType: "multipart/form-data",

service/responses.tsp

Lines changed: 2 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -197,11 +197,8 @@ namespace CoreSystem.Responses {
197197

198198
@doc("Update answers for the response.")
199199
@route("/responses/{responseId}/answers")
200-
@patch(#{implicitOptionality: true})
201-
op updateFormResponse(
202-
@path responseId: uuid,
203-
@body req: AnswersRequest
204-
): {
200+
@patch(#{ implicitOptionality: true })
201+
op updateFormResponse(@path responseId: uuid, @body req: UpdateRequest): {
205202
@statusCode statusCode: 200;
206203
};
207204

0 commit comments

Comments
 (0)