|
1 | 1 | /** |
2 | 2 | * @file lib/crud.js |
3 | | - * @description Implements generic CRUD execution logic per entity. |
| 3 | + * @description Implements generic CRUD execution logic per entity with reduced complexity and parameter grouping. |
4 | 4 | */ |
5 | 5 |
|
6 | 6 | import http from 'k6/http'; |
7 | | -import { check } from 'k6'; |
8 | 7 | import { ENV } from './env.js'; |
9 | | -import { secureRandomInt } from './utils.js'; |
| 8 | +import { recordTrendAndCheck, secureRandomInt } from './utils.js'; |
10 | 9 |
|
11 | 10 | /** |
12 | | - * Performs a single CRUD operation based on allowedOps from ENV. |
13 | | - * |
14 | | - * @param {object} params |
15 | | - * @param {string[]} params.vuIds - IDs accessible to this VU |
16 | | - * @param {string[]} params.hotIds - Hot IDs for read-heavy or update-heavy ops |
17 | | - * @param {string[]} params.coolIds - Cool IDs to skip for certain ops |
18 | | - * @param {string} params.entity - Entity name (e.g., users, items) |
19 | | - * @param {Function} params.generateFn - Function to generate new entity payloads |
20 | | - * @param {Record<string, import('k6/metrics').Trend>} params.trends - Trend metrics for the entity |
21 | | - * @param {string[]} params.allowedOps - Allowed operations (e.g., list,read,create,update,delete) |
22 | | - * @returns {void} |
| 11 | + * Helper: Selects an entity ID based on hot/cool/available sets. |
| 12 | + * @param {string[]} hotIds |
| 13 | + * @param {string[]} vuIds |
| 14 | + * @param {string[]} coolIds |
| 15 | + * @param {boolean} preferHot |
| 16 | + * @returns {string|null} |
23 | 17 | */ |
24 | | -export function performCrudAction({ |
25 | | - vuIds, |
26 | | - hotIds, |
27 | | - coolIds, |
28 | | - entity, |
29 | | - generateFn, |
30 | | - trends, |
31 | | - allowedOps |
32 | | -}) { |
33 | | - // Choose random operation based on allowedOps |
34 | | - const op = allowedOps[secureRandomInt(0, allowedOps.length)]; |
35 | | - const baseUrl = `${ENV.BASE_URL}/${entity}`; |
| 18 | +function selectTargetId(hotIds, vuIds, coolIds, preferHot = true) { |
| 19 | + const pool = preferHot && hotIds.length ? hotIds : vuIds; |
| 20 | + if (!pool.length) return null; |
36 | 21 |
|
| 22 | + for (let i = 0; i < 3; i++) { // try a few times |
| 23 | + const id = pool[secureRandomInt(0, pool.length)]; |
| 24 | + if (!coolIds.includes(id)) return id; |
| 25 | + } |
| 26 | + |
| 27 | + // fallback to any id |
| 28 | + // return vuIds.length ? vuIds[secureRandomInt(0, vuIds.length)] : null; |
| 29 | + return null; |
| 30 | +} |
| 31 | + |
| 32 | +/** |
| 33 | + * Executes a CRUD HTTP request for the given operation. |
| 34 | + * Reduced parameters by grouping logically related args into objects. |
| 35 | + * |
| 36 | + * @param {string} op |
| 37 | + * @param {string} baseUrl |
| 38 | + * @param {string} entity |
| 39 | + * @param {{ generateFn: Function, trends: Record<string, import('k6/metrics').Trend>, contentType: string }} context |
| 40 | + * @param {{ vuIds: string[], hotIds: string[], coolIds: string[] }} idSets |
| 41 | + */ |
| 42 | +function executeCrudOp(op, baseUrl, entity, context, idSets) { |
37 | 43 | switch (op) { |
38 | 44 | case 'list': { |
39 | | - const res = http.get(`${baseUrl}`); |
40 | | - trends.list?.add(res.timings.duration); |
41 | | - check(res, { [`${entity} LIST success`]: r => r.status === 200 }); |
| 45 | + executeList(baseUrl, entity, context); |
42 | 46 | break; |
43 | 47 | } |
44 | | - |
45 | 48 | case 'read': { |
46 | | - const id = hotIds.length |
47 | | - ? hotIds[secureRandomInt(0, hotIds.length)] |
48 | | - : vuIds[secureRandomInt(0, vuIds.length)]; |
49 | | - if (!id || coolIds.includes(id)) return; |
50 | | - const res = http.get(`${baseUrl}/${id}`); |
51 | | - trends.read?.add(res.timings.duration); |
52 | | - check(res, { [`${entity} READ success`]: r => r.status === 200 }); |
| 49 | + executeRead(baseUrl, entity, context, idSets); |
53 | 50 | break; |
54 | 51 | } |
55 | | - |
56 | 52 | case 'create': { |
57 | | - const obj = generateFn(secureRandomInt(0, 1000000)); |
58 | | - const res = http.post(baseUrl, JSON.stringify(obj), { |
59 | | - headers: { 'Content-Type': 'application/json' } |
60 | | - }); |
61 | | - trends.create?.add(res.timings.duration); |
62 | | - check(res, { [`${entity} CREATE success`]: r => r.status === 201 }); |
63 | | - if (res.status === 201) { |
64 | | - try { |
65 | | - const parsed = JSON.parse(res.body); |
66 | | - if (parsed?.id) vuIds.push(parsed.id); |
67 | | - } catch { } |
68 | | - } |
| 53 | + executeCreate(baseUrl, entity, context, idSets); |
69 | 54 | break; |
70 | 55 | } |
71 | | - |
72 | 56 | case 'update': { |
73 | | - const id = hotIds.length |
74 | | - ? hotIds[secureRandomInt(0, hotIds.length)] |
75 | | - : vuIds[secureRandomInt(0, vuIds.length)]; |
76 | | - if (!id || coolIds.includes(id)) return; |
77 | | - const obj = generateFn(id); |
78 | | - const res = http.put(`${baseUrl}/${id}`, JSON.stringify(obj), { |
79 | | - headers: { 'Content-Type': 'application/json' } |
80 | | - }); |
81 | | - trends.update?.add(res.timings.duration); |
82 | | - check(res, { [`${entity} UPDATE success`]: r => r.status === 200 }); |
| 57 | + executeUpdate(baseUrl, entity, context, idSets); |
83 | 58 | break; |
84 | 59 | } |
85 | | - |
86 | 60 | case 'delete': { |
87 | | - const id = coolIds[secureRandomInt(0, coolIds.length)]; |
88 | | - if (!id) return; |
89 | | - const res = http.del(`${baseUrl}/${id}`); |
90 | | - trends.delete?.add(res.timings.duration); |
91 | | - check(res, { [`${entity} DELETE success`]: r => [200, 204].includes(r.status) }); |
| 61 | + executeDelete(baseUrl, entity, context, idSets); |
92 | 62 | break; |
93 | 63 | } |
94 | | - |
95 | 64 | default: |
96 | 65 | console.warn(`[WARN] Unsupported CRUD operation: ${op}`); |
97 | 66 | } |
98 | 67 | } |
| 68 | + |
| 69 | +/** |
| 70 | + * Executes a CRUD HTTP request for the given operation. |
| 71 | + * Reduced parameters by grouping logically related args into objects. |
| 72 | + * |
| 73 | + * @param {string} baseUrl |
| 74 | + * @param {string} entity |
| 75 | + * @param {{ generateFn: Function, trends: Record<string, import('k6/metrics').Trend>, contentType: string }} context |
| 76 | + */ |
| 77 | +function executeList(baseUrl, entity, context) { |
| 78 | + const { trends } = context; |
| 79 | + |
| 80 | + const res = http.get(baseUrl); |
| 81 | + recordTrendAndCheck(res, entity, "list", trends.list, 200); |
| 82 | +} |
| 83 | + |
| 84 | +/** |
| 85 | + * Executes a CRUD HTTP request for the given operation. |
| 86 | + * Reduced parameters by grouping logically related args into objects. |
| 87 | + * |
| 88 | + * @param {string} baseUrl |
| 89 | + * @param {string} entity |
| 90 | + * @param {{ generateFn: Function, trends: Record<string, import('k6/metrics').Trend>, contentType: string }} context |
| 91 | + * @param {{ vuIds: string[], hotIds: string[], coolIds: string[] }} idSets |
| 92 | + */ |
| 93 | +function executeRead(baseUrl, entity, context, idSets) { |
| 94 | + const { trends } = context; |
| 95 | + const { vuIds, hotIds, coolIds } = idSets; |
| 96 | + |
| 97 | + const id = selectTargetId(hotIds, vuIds, coolIds, true); |
| 98 | + if (!id) { |
| 99 | + console.log("Skipping read no id"); |
| 100 | + return; |
| 101 | + } |
| 102 | + const res = http.get(`${baseUrl}/${id}`); |
| 103 | + recordTrendAndCheck(res, entity, "read", trends.read, 200); |
| 104 | +} |
| 105 | + |
| 106 | +/** |
| 107 | + * Executes a CRUD HTTP request for the given operation. |
| 108 | + * Reduced parameters by grouping logically related args into objects. |
| 109 | + * |
| 110 | + * @param {string} baseUrl |
| 111 | + * @param {string} entity |
| 112 | + * @param {{ generateFn: Function, trends: Record<string, import('k6/metrics').Trend>, contentType: string }} context |
| 113 | + * @param {{ vuIds: string[], hotIds: string[], coolIds: string[] }} idSets |
| 114 | + */ |
| 115 | +function executeCreate(baseUrl, entity, context, idSets) { |
| 116 | + const { generateFn, trends, contentType = 'json' } = context; |
| 117 | + const { vuIds } = idSets; |
| 118 | + |
| 119 | + const obj = generateFn(secureRandomInt(0, 1000000)); |
| 120 | + |
| 121 | + // choose encoding based on contentType |
| 122 | + let body, headers; |
| 123 | + if (contentType === 'form') { |
| 124 | + body = encodeFormData(obj); |
| 125 | + headers = { 'Content-Type': 'application/x-www-form-urlencoded' }; |
| 126 | + } else { |
| 127 | + body = JSON.stringify(obj); |
| 128 | + headers = { 'Content-Type': 'application/json' }; |
| 129 | + } |
| 130 | + |
| 131 | + const res = http.post(baseUrl, body, { headers }); |
| 132 | + recordTrendAndCheck(res, entity, "create", trends.create, [200, 201, 202]); |
| 133 | + |
| 134 | + if (res.status === 201) { |
| 135 | + try { |
| 136 | + const parsed = JSON.parse(res.body); |
| 137 | + if (parsed?.id) vuIds.push(parsed.id); |
| 138 | + } catch { |
| 139 | + // ignore malformed response |
| 140 | + } |
| 141 | + } |
| 142 | +} |
| 143 | + |
| 144 | +/** |
| 145 | + * Executes a CRUD HTTP request for the given operation. |
| 146 | + * Reduced parameters by grouping logically related args into objects. |
| 147 | + * |
| 148 | + * @param {string} baseUrl |
| 149 | + * @param {string} entity |
| 150 | + * @param {{ generateFn: Function, trends: Record<string, import('k6/metrics').Trend>, contentType: string }} context |
| 151 | + * @param {{ vuIds: string[], hotIds: string[], coolIds: string[] }} idSets |
| 152 | + */ |
| 153 | +function executeUpdate(baseUrl, entity, context, idSets) { |
| 154 | + const { generateFn, trends, contentType = 'json' } = context; |
| 155 | + const { vuIds, hotIds, coolIds } = idSets; |
| 156 | + |
| 157 | + const id = selectTargetId(hotIds, vuIds, coolIds, false); |
| 158 | + if (!id) { |
| 159 | + console.log("Skipping update no id"); |
| 160 | + return; |
| 161 | + } |
| 162 | + const obj = generateFn(id); |
| 163 | + |
| 164 | + // choose encoding based on contentType |
| 165 | + let body, headers; |
| 166 | + if (contentType === 'form') { |
| 167 | + body = encodeFormData(obj); |
| 168 | + headers = { 'Content-Type': 'application/x-www-form-urlencoded' }; |
| 169 | + } else { |
| 170 | + body = JSON.stringify(obj); |
| 171 | + headers = { 'Content-Type': 'application/json' }; |
| 172 | + } |
| 173 | + |
| 174 | + const res = http.put(`${baseUrl}/${id}`, body, { headers }); |
| 175 | + recordTrendAndCheck(res, entity, "update", trends.update, [200, 202]); |
| 176 | +} |
| 177 | + |
| 178 | +/** |
| 179 | + * Executes a CRUD HTTP request for the given operation. |
| 180 | + * Reduced parameters by grouping logically related args into objects. |
| 181 | + * |
| 182 | + * @param {string} baseUrl |
| 183 | + * @param {string} entity |
| 184 | + * @param {{ generateFn: Function, trends: Record<string, import('k6/metrics').Trend>, contentType: string }} context |
| 185 | + * @param {{ vuIds: string[], hotIds: string[], coolIds: string[] }} idSets |
| 186 | + */ |
| 187 | +function executeDelete(baseUrl, entity, context, idSets) { |
| 188 | + const { trends } = context; |
| 189 | + const { coolIds } = idSets; |
| 190 | + |
| 191 | + const id = coolIds[secureRandomInt(0, coolIds.length)]; |
| 192 | + if (!id) { |
| 193 | + console.log("Skipping delete no id"); |
| 194 | + return; |
| 195 | + } |
| 196 | + const res = http.del(`${baseUrl}/${id}`); |
| 197 | + recordTrendAndCheck(res, entity, "delete", trends.delete, [200, 202, 204]); |
| 198 | +} |
| 199 | + |
| 200 | +/** |
| 201 | + * Performs a single CRUD operation based on allowedOps from ENV. |
| 202 | + * |
| 203 | + * @param {object} params |
| 204 | + * @param {string[]} params.vuIds |
| 205 | + * @param {string[]} params.hotIds |
| 206 | + * @param {string[]} params.coolIds |
| 207 | + * @param {string} params.entity |
| 208 | + * @param {Function} params.generateFn |
| 209 | + * @param {Record<string, import('k6/metrics').Trend>} params.trends |
| 210 | + * @param {string[]} params.allowedOps |
| 211 | + * @param {bool} params.async |
| 212 | + * @returns {void} |
| 213 | + */ |
| 214 | +export function performCrudAction({ |
| 215 | + vuIds, |
| 216 | + hotIds, |
| 217 | + coolIds, |
| 218 | + entity, |
| 219 | + generateFn, |
| 220 | + trends, |
| 221 | + allowedOps, |
| 222 | + contentType = 'json' // can be 'json' or 'form' |
| 223 | +}) { |
| 224 | + const op = allowedOps[secureRandomInt(0, allowedOps.length)]; |
| 225 | + const baseUrl = `${ENV.BASE_URL}/${entity}`; |
| 226 | + |
| 227 | + const idSets = { vuIds, hotIds, coolIds }; |
| 228 | + const context = { generateFn, trends, contentType }; |
| 229 | + |
| 230 | + executeCrudOp(op, baseUrl, entity, context, idSets); |
| 231 | +} |
0 commit comments