1 | |
2 |
|
3 | function authHeader(auth: RT.Servicenow) { |
4 | return auth.token |
5 | ? `Bearer ${auth.token}` |
6 | : `Basic ${btoa(`${auth.username}:${auth.password}`)}` |
7 | } |
8 |
|
9 | |
10 | function value(field: any) { |
11 | return field != null && typeof field === "object" ? field.value : field |
12 | } |
13 |
|
14 | function label(field: any) { |
15 | return field != null && typeof field === "object" |
16 | ? field.display_value |
17 | : field |
18 | } |
19 |
|
20 | |
21 | * Update Change Request |
22 | * Partially update a change request by sys_id through the Change Management API (PATCH /api/sn_chg_rest/change/{sys_id}, documented in the Yokohama and Brazil API references; needs the itil, change_manager or admin role). Optionally move it to a target state: use a state value from Get change next states (e.g. -1 for Implement). Throws with ServiceNow's message when the update is rejected, and also when ServiceNow answers 200 but leaves the state unchanged (transition not allowed, or the field was ignored). |
23 | */ |
24 | export async function main( |
25 | auth: RT.Servicenow, |
26 | sys_id: string, |
27 | state: string | undefined, |
28 | work_notes: string | undefined, |
29 | fields: { [key: string]: any } | undefined |
30 | ) { |
31 | const url = new URL( |
32 | `${auth.instance_url}/api/sn_chg_rest/change/${encodeURIComponent(sys_id)}` |
33 | ) |
34 | const headers = { |
35 | Authorization: authHeader(auth), |
36 | Accept: "application/json", |
37 | } |
38 |
|
39 | |
40 | let previousState: any |
41 | let alreadyInState = false |
42 | if (state) { |
43 | const current = await fetch(url, { headers }) |
44 | if (!current.ok) { |
45 | throw new Error(`${current.status} ${await current.text()}`) |
46 | } |
47 | const { result } = (await current.json()) as { result: any } |
48 | previousState = value(result.state) |
49 | alreadyInState = |
50 | String(state) === String(previousState) || |
51 | String(state).toLowerCase() === String(label(result.state)).toLowerCase() |
52 | } |
53 |
|
54 | const body: { [key: string]: any } = { ...(fields ?? {}) } |
55 | if (work_notes) body.work_notes = work_notes |
56 | if (state) body.state = state |
57 |
|
58 | const response = await fetch(url, { |
59 | method: "PATCH", |
60 | headers: { ...headers, "Content-Type": "application/json" }, |
61 | body: JSON.stringify(body), |
62 | }) |
63 |
|
64 | if (!response.ok) { |
65 | throw new Error(`${response.status} ${await response.text()}`) |
66 | } |
67 |
|
68 | const { result } = (await response.json()) as { result: any } |
69 | const ignoredFields: string[] = result.__meta?.ignoredFields ?? [] |
70 |
|
71 | if (state) { |
72 | const newState = value(result.state) |
73 | if ( |
74 | ignoredFields.includes("state") || |
75 | (String(newState) === String(previousState) && !alreadyInState) |
76 | ) { |
77 | throw new Error( |
78 | `ServiceNow did not move change ${value(result.number)} to state ${state}: it is still in ${label(result.state)} (${newState}). Check the allowed transitions with Get change next states.` |
79 | ) |
80 | } |
81 | } |
82 |
|
83 | return { |
84 | sys_id: value(result.sys_id), |
85 | number: value(result.number), |
86 | state: value(result.state), |
87 | state_label: label(result.state), |
88 | ignored_fields: ignoredFields, |
89 | record: result, |
90 | } |
91 | } |
92 |
|