Pharos 0.7.23
Modern Web Framework & Web App Hosting Service.
Loading...
Searching...
No Matches
native_dynamic_business_logic.c
Go to the documentation of this file.
1/**
2 * @file
3 * @brief Executes framework-owned declarative action contracts for dynamic apps.
4 */
5
7
10#include "native_fs.h"
12#include "native_support.h"
13
14#include <stdio.h>
15#include <stdlib.h>
16#include <string.h>
17#include <time.h>
18
19#ifdef _WIN32
20#include <winsock2.h>
21#include <ws2tcpip.h>
22#else
23#include <arpa/inet.h>
24#include <errno.h>
25#include <netdb.h>
26#include <netinet/in.h>
27#include <sys/socket.h>
28#include <sys/time.h>
29#include <unistd.h>
30#endif
31
32static char *request_body_bytes_dup_logic_local(const HttpRequest *request, size_t *out_size) {
33 char *body_text = NULL;
34 if (out_size != NULL) {
35 *out_size = 0;
36 }
37 if (request == NULL) {
38 return strdup("");
39 }
40 if (request->body != NULL) {
41 body_text = strdup(request->body);
42 } else if (request->body_file_path != NULL && request->body_file_path[0] != '\0') {
43 body_text = read_file_text(request->body_file_path);
44 }
45 if (body_text == NULL) {
46 body_text = strdup("");
47 }
48 if (out_size != NULL && body_text != NULL) {
49 *out_size = strlen(body_text);
50 }
51 return body_text;
52}
53
54static char *request_form_value_dup_logic_local(const HttpRequest *request, const char *field_name) {
55 if (field_name == NULL || field_name[0] == '\0') {
56 return NULL;
57 }
58 if (request != NULL && request->is_multipart) {
59 size_t body_len = 0;
60 char *body_bytes = request_body_bytes_dup_logic_local(request, &body_len);
61 char *value = NULL;
62 if (body_bytes != NULL) {
63 value = native_dynamic_multipart_extract_text((const unsigned char *)body_bytes, body_len, request->content_type, field_name);
64 free(body_bytes);
65 }
66 return value;
67 }
68 {
69 char *form_body = request_body_bytes_dup_logic_local(request, NULL);
70 char *value = native_dynamic_form_value(form_body != NULL ? form_body : "", field_name);
71 free(form_body);
72 return value;
73 }
74}
75
77 NativeConnection *connection,
78 int status,
79 const char *status_text,
80 const char *error_kind,
81 const char *message
82) {
83 char body[2048];
84 int written = snprintf(
85 body,
86 sizeof(body),
87 "{\"error\":\"%s\",\"message\":\"%s\"}",
88 error_kind != NULL ? error_kind : "error",
89 message != NULL ? message : "An error occurred"
90 );
91 if (written < 0 || (size_t)written >= sizeof(body)) {
92 native_send_text_response(connection, 500, "Internal Server Error", "response formatting error\n");
93 return;
94 }
96 connection,
97 "HTTP/1.1 %d %s\r\n"
98 "Content-Type: application/json\r\n"
99 "Cache-Control: no-store\r\n"
100 "Content-Length: %zu\r\n"
101 "Connection: close\r\n"
102 "\r\n"
103 "%s",
104 status,
105 status_text != NULL ? status_text : "Error",
106 strlen(body),
107 body
108 );
109}
110
111static int now_iso_utc_logic_local(char out[20]) {
112 time_t now = time(NULL);
113 struct tm tm_value;
114 if (out == NULL) {
115 return 0;
116 }
117#ifdef _WIN32
118 if (gmtime_s(&tm_value, &now) != 0) {
119 return 0;
120 }
121#else
122 if (gmtime_r(&now, &tm_value) == NULL) {
123 return 0;
124 }
125#endif
126 return strftime(out, 20, "%Y-%m-%d %H:%M:%S", &tm_value) == 19;
127}
128
130 if (app == NULL || app->app_root == NULL || app->app_root[0] == '\0') {
131 return NULL;
132 }
133 return path_join(app->app_root, "pharos.app.json");
134}
135
136static char *fixture_path_dup_logic_local(const AppRuntime *app, const char *fixture_key) {
137 char *manifest_path = NULL;
138 yyjson_doc *doc = NULL;
139 yyjson_val *root = NULL;
140 yyjson_val *fixtures = NULL;
141 char *relative = NULL;
142 char *resolved = NULL;
143 if (app == NULL || fixture_key == NULL || fixture_key[0] == '\0') {
144 return NULL;
145 }
146 manifest_path = app_manifest_path_dup_logic_local(app);
147 if (manifest_path == NULL) {
148 return NULL;
149 }
150 doc = native_json_doc_load_file(manifest_path);
151 free(manifest_path);
152 if (doc == NULL) {
153 return NULL;
154 }
155 root = yyjson_doc_get_root(doc);
156 fixtures = root != NULL ? native_json_obj_get(root, "fixtures") : NULL;
157 if (fixtures != NULL) {
158 relative = native_json_obj_get_string_dup(fixtures, fixture_key);
159 }
160 if (relative != NULL && relative[0] != '\0') {
161 if (relative[0] == '/') {
162 resolved = relative;
163 relative = NULL;
164 } else {
165 resolved = path_join(app->app_root, relative);
166 }
167 }
168 free(relative);
170 return resolved;
171}
172
173static yyjson_doc *load_fixture_doc_logic_local(const AppRuntime *app, const char *fixture_key) {
174 char *path = fixture_path_dup_logic_local(app, fixture_key);
175 yyjson_doc *doc = NULL;
176 if (path != NULL) {
177 doc = native_json_doc_load_file(path);
178 }
179 free(path);
180 return doc;
181}
182
183static yyjson_val *find_action_contract_logic_local(yyjson_val *root, const char *action_id) {
184 yyjson_val *actions = root != NULL ? native_json_obj_get(root, "actions") : NULL;
185 yyjson_arr_iter iter;
186 yyjson_val *item = NULL;
187 if (actions == NULL || action_id == NULL || !yyjson_is_arr(actions)) {
188 return NULL;
189 }
190 if (yyjson_arr_iter_init(actions, &iter)) {
191 while ((item = yyjson_arr_iter_next(&iter)) != NULL) {
192 if (yyjson_is_obj(item) && yyjson_equals_str(native_json_obj_get(item, "action_id"), action_id)) {
193 return item;
194 }
195 }
196 }
197 return NULL;
198}
199
200static yyjson_val *find_workflow_transition_logic_local(yyjson_val *root, const char *action_id, yyjson_val **workflow_out) {
201 yyjson_val *workflows = root != NULL ? native_json_obj_get(root, "workflows") : NULL;
202 yyjson_arr_iter workflow_iter;
203 yyjson_val *workflow = NULL;
204 if (workflow_out != NULL) {
205 *workflow_out = NULL;
206 }
207 if (workflows == NULL || action_id == NULL || !yyjson_is_arr(workflows)) {
208 return NULL;
209 }
210 if (yyjson_arr_iter_init(workflows, &workflow_iter)) {
211 while ((workflow = yyjson_arr_iter_next(&workflow_iter)) != NULL) {
212 yyjson_val *transitions = native_json_obj_get(workflow, "transitions");
213 yyjson_arr_iter transition_iter;
214 yyjson_val *transition = NULL;
215 if (transitions == NULL || !yyjson_is_arr(transitions)) {
216 continue;
217 }
218 if (yyjson_arr_iter_init(transitions, &transition_iter)) {
219 while ((transition = yyjson_arr_iter_next(&transition_iter)) != NULL) {
220 if (yyjson_is_obj(transition) && yyjson_equals_str(native_json_obj_get(transition, "action_id"), action_id)) {
221 if (workflow_out != NULL) {
222 *workflow_out = workflow;
223 }
224 return transition;
225 }
226 }
227 }
228 }
229 }
230 return NULL;
231}
232
233typedef struct {
234 char *field_id;
235 char *value;
237
242
256
262
263typedef struct {
264 char *scheme;
265 char *host;
266 int port;
267 char *path;
269
276
278 if (context == NULL) {
279 return;
280 }
281 free(context->anchor_resource_id);
282 free(context->last_created_resource_id);
283 free(context->last_touched_resource_id);
284 free(context->overflow_policy);
285 memset(context, 0, sizeof(*context));
286}
287
289 if (url == NULL) {
290 return;
291 }
292 free(url->scheme);
293 free(url->host);
294 free(url->path);
295 memset(url, 0, sizeof(*url));
296}
297
299 if (result == NULL) {
300 return;
301 }
302 free(result->error_message);
303 free(result->response_body);
304 memset(result, 0, sizeof(*result));
305}
306
307static int now_plus_seconds_iso_utc_logic_local(long offset_seconds, char out[20]) {
308 time_t now = time(NULL);
309 struct tm tm_value;
310 if (out == NULL) {
311 return 0;
312 }
313 now += offset_seconds;
314#ifdef _WIN32
315 if (gmtime_s(&tm_value, &now) != 0) {
316 return 0;
317 }
318#else
319 if (gmtime_r(&now, &tm_value) == NULL) {
320 return 0;
321 }
322#endif
323 return strftime(out, 20, "%Y-%m-%d %H:%M:%S", &tm_value) == 19;
324}
325
326static int replace_owned_string_logic_local(char **slot, const char *value) {
327 char *copy = NULL;
328 if (slot == NULL) {
329 return 0;
330 }
331 if (value != NULL) {
332 copy = strdup(value);
333 if (copy == NULL) {
334 return 0;
335 }
336 }
337 free(*slot);
338 *slot = copy;
339 return 1;
340}
341
342static int note_created_record_logic_local(ActionExecutionContextLogicLocal *context, long record_id, const char *resource_id) {
343 if (context == NULL || record_id <= 0) {
344 return 0;
345 }
346 if (context->anchor_record_id <= 0) {
347 context->anchor_record_id = record_id;
348 if (resource_id != NULL && resource_id[0] != '\0' && !replace_owned_string_logic_local(&context->anchor_resource_id, resource_id)) {
349 return 0;
350 }
351 }
352 context->last_created_record_id = record_id;
353 context->last_touched_record_id = record_id;
354 context->created_record_count++;
355 if (resource_id != NULL && resource_id[0] != '\0') {
358 return 0;
359 }
360 }
361 return 1;
362}
363
364static int note_updated_record_logic_local(ActionExecutionContextLogicLocal *context, long record_id, const char *resource_id) {
365 if (context == NULL || record_id <= 0) {
366 return 0;
367 }
368 context->last_touched_record_id = record_id;
369 context->updated_record_count++;
370 if (resource_id != NULL && resource_id[0] != '\0' && !replace_owned_string_logic_local(&context->last_touched_resource_id, resource_id)) {
371 return 0;
372 }
373 return 1;
374}
375
378 long matched_count,
379 long applied_count,
380 const char *overflow_policy
381) {
382 if (context == NULL) {
383 return 0;
384 }
385 context->matched_record_count += matched_count > 0 ? matched_count : 0;
386 if (matched_count > applied_count) {
387 context->skipped_record_count += matched_count - applied_count;
388 }
389 if (overflow_policy != NULL && overflow_policy[0] != '\0'
390 && !replace_owned_string_logic_local(&context->overflow_policy, overflow_policy)) {
391 return 0;
392 }
393 return 1;
394}
395
397 size_t index = 0;
398 if (map == NULL) {
399 return;
400 }
401 for (index = 0; index < map->count; index++) {
402 free(map->items[index].field_id);
403 free(map->items[index].value);
404 }
405 free(map->items);
406 map->items = NULL;
407 map->count = 0;
408}
409
410static char *input_field_value_dup_logic_local(const InputFieldMapLogicLocal *map, const char *field_id) {
411 size_t index = 0;
412 if (map == NULL || field_id == NULL) {
413 return NULL;
414 }
415 for (index = 0; index < map->count; index++) {
416 if (map->items[index].field_id != NULL && streq(map->items[index].field_id, field_id)) {
417 return map->items[index].value != NULL ? strdup(map->items[index].value) : NULL;
418 }
419 }
420 return NULL;
421}
422
423static char *action_presentation_string_dup_logic_local(yyjson_val *action_contract, const char *field_name) {
424 yyjson_val *presentation = action_contract != NULL ? native_json_obj_get(action_contract, "presentation") : NULL;
425 if (presentation == NULL || field_name == NULL || field_name[0] == '\0') {
426 return NULL;
427 }
428 return native_json_obj_get_string_dup(presentation, field_name);
429}
430
431static char *workflow_presentation_string_dup_logic_local(yyjson_val *transition, const char *field_name) {
432 yyjson_val *presentation = transition != NULL ? native_json_obj_get(transition, "presentation") : NULL;
433 if (presentation == NULL || field_name == NULL || field_name[0] == '\0') {
434 return NULL;
435 }
436 return native_json_obj_get_string_dup(presentation, field_name);
437}
438
440 const HttpRequest *request,
441 yyjson_val *action_contract,
443 char **error_message_out
444) {
445 yyjson_val *input_contract = native_json_obj_get(action_contract, "input_contract");
446 yyjson_val *fields = input_contract != NULL ? native_json_obj_get(input_contract, "fields") : NULL;
447 yyjson_arr_iter iter;
448 yyjson_val *item = NULL;
449 size_t count = 0;
450 size_t index = 0;
451 if (error_message_out != NULL) {
452 *error_message_out = NULL;
453 }
454 if (map == NULL) {
455 return 0;
456 }
457 memset(map, 0, sizeof(*map));
458 if (fields == NULL || !yyjson_is_arr(fields)) {
459 return 1;
460 }
461 count = yyjson_arr_size(fields);
462 if (count == 0) {
463 return 1;
464 }
465 map->items = (InputFieldValueLogicLocal *)calloc(count, sizeof(InputFieldValueLogicLocal));
466 if (map->items == NULL) {
467 return 0;
468 }
469 map->count = count;
470 if (yyjson_arr_iter_init(fields, &iter)) {
471 while ((item = yyjson_arr_iter_next(&iter)) != NULL) {
472 char *field_id = native_json_obj_get_string_dup(item, "field_id");
473 char *type = native_json_obj_get_string_dup(item, "type");
474 int required = yyjson_get_bool(native_json_obj_get(item, "required"));
475 char *value = NULL;
476 if (field_id == NULL || field_id[0] == '\0') {
477 free(field_id);
478 free(type);
480 return 0;
481 }
482 value = request_form_value_dup_logic_local(request, field_id);
483 if (required && (value == NULL || value[0] == '\0')) {
484 if (error_message_out != NULL) {
485 size_t needed = strlen(field_id) + 64;
486 *error_message_out = (char *)malloc(needed);
487 if (*error_message_out != NULL) {
488 snprintf(*error_message_out, needed, "Missing required field: %s.", field_id);
489 }
490 }
491 free(field_id);
492 free(type);
493 free(value);
495 return 1;
496 }
497 if (type != NULL && streq(type, "integer") && value != NULL && value[0] != '\0') {
498 char *end = NULL;
499 (void)strtol(value, &end, 10);
500 if (end == NULL || *end != '\0') {
501 if (error_message_out != NULL) {
502 size_t needed = strlen(field_id) + 64;
503 *error_message_out = (char *)malloc(needed);
504 if (*error_message_out != NULL) {
505 snprintf(*error_message_out, needed, "Invalid integer field: %s.", field_id);
506 }
507 }
508 free(field_id);
509 free(type);
510 free(value);
512 return 1;
513 }
514 }
515 map->items[index].field_id = field_id;
516 map->items[index].value = value != NULL ? value : strdup("");
517 free(type);
518 index++;
519 }
520 }
521 return 1;
522}
523
525 yyjson_val *value_contract,
526 const InputFieldMapLogicLocal *inputs,
528 const char *list_item_text,
529 yyjson_val *source_record_context
530) {
531 char *source = NULL;
532 char buffer[64];
533 char *result = NULL;
534 if (value_contract == NULL) {
535 return NULL;
536 }
537 if (yyjson_is_str(value_contract)) {
538 return strdup(yyjson_get_str(value_contract));
539 }
540 if (yyjson_is_num(value_contract)) {
541 snprintf(buffer, sizeof(buffer), "%lld", (long long)yyjson_get_sint(value_contract));
542 return strdup(buffer);
543 }
544 if (yyjson_is_bool(value_contract)) {
545 return strdup(yyjson_get_bool(value_contract) ? "true" : "false");
546 }
547 if (!yyjson_is_obj(value_contract)) {
548 return NULL;
549 }
550 source = native_json_obj_get_string_dup(value_contract, "source");
551 if (source != NULL && streq(source, "input")) {
552 char *field_name = native_json_obj_get_string_dup(value_contract, "field");
553 result = input_field_value_dup_logic_local(inputs, field_name);
554 free(field_name);
555 } else if (source != NULL && streq(source, "literal")) {
556 yyjson_val *literal = native_json_obj_get(value_contract, "value");
557 if (literal == NULL || yyjson_is_null(literal)) {
558 result = strdup("");
559 } else if (yyjson_is_str(literal)) {
560 result = strdup(yyjson_get_str(literal));
561 } else if (yyjson_is_num(literal)) {
562 snprintf(buffer, sizeof(buffer), "%lld", (long long)yyjson_get_sint(literal));
563 result = strdup(buffer);
564 } else if (yyjson_is_bool(literal)) {
565 result = strdup(yyjson_get_bool(literal) ? "true" : "false");
566 }
567 } else if (source != NULL && streq(source, "list_item")) {
568 result = strdup(list_item_text != NULL ? list_item_text : "");
569 } else if (source != NULL && streq(source, "context_anchor_created_id")) {
570 snprintf(buffer, sizeof(buffer), "%ld", context != NULL ? context->anchor_record_id : 0L);
571 result = strdup(buffer);
572 } else if (source != NULL && streq(source, "context_last_created_id")) {
573 snprintf(buffer, sizeof(buffer), "%ld", context != NULL ? context->last_created_record_id : 0L);
574 result = strdup(buffer);
575 } else if (source != NULL && streq(source, "context_last_touched_id")) {
576 snprintf(buffer, sizeof(buffer), "%ld", context != NULL ? context->last_touched_record_id : 0L);
577 result = strdup(buffer);
578 } else if (source != NULL && streq(source, "source_record_field")) {
579 char *field_name = native_json_obj_get_string_dup(value_contract, "field");
580 yyjson_val *current = source_record_context != NULL ? native_json_obj_get(source_record_context, field_name) : NULL;
581 if (current == NULL || yyjson_is_null(current)) {
582 result = strdup("");
583 } else if (yyjson_is_str(current)) {
584 result = strdup(yyjson_get_str(current));
585 } else if (yyjson_is_num(current)) {
586 snprintf(buffer, sizeof(buffer), "%lld", (long long)yyjson_get_sint(current));
587 result = strdup(buffer);
588 } else if (yyjson_is_bool(current)) {
589 result = strdup(yyjson_get_bool(current) ? "true" : "false");
590 }
591 free(field_name);
592 }
593 free(source);
594 return result;
595}
596
598 yyjson_mut_doc *doc,
599 yyjson_val *value_contract,
600 const InputFieldMapLogicLocal *inputs,
601 long next_id,
602 yyjson_val *record_context,
604 const char *list_item_text,
605 yyjson_val *source_record_context
606) {
607 char *source = NULL;
608 yyjson_mut_val *result = NULL;
609 if (doc == NULL || value_contract == NULL || !yyjson_is_obj(value_contract)) {
610 return NULL;
611 }
612 source = native_json_obj_get_string_dup(value_contract, "source");
613 if (source != NULL && streq(source, "input")) {
614 char *field_name = native_json_obj_get_string_dup(value_contract, "field");
615 char *value = input_field_value_dup_logic_local(inputs, field_name);
616 result = yyjson_mut_strcpy(doc, value != NULL ? value : "");
617 free(field_name);
618 free(value);
619 } else if (source != NULL && streq(source, "next_id")) {
620 result = yyjson_mut_int(doc, next_id);
621 } else if (source != NULL && streq(source, "literal")) {
622 yyjson_val *literal = native_json_obj_get(value_contract, "value");
623 if (literal == NULL || yyjson_is_null(literal)) {
624 result = yyjson_mut_null(doc);
625 } else if (yyjson_is_bool(literal)) {
626 result = yyjson_mut_bool(doc, yyjson_get_bool(literal));
627 } else if (yyjson_is_num(literal)) {
628 result = yyjson_mut_sint(doc, yyjson_get_sint(literal));
629 } else if (yyjson_is_str(literal)) {
630 result = yyjson_mut_strcpy(doc, yyjson_get_str(literal));
631 }
632 } else if (source != NULL && streq(source, "toggle_boolean")) {
633 char *field_name = native_json_obj_get_string_dup(value_contract, "field");
634 yyjson_val *current = record_context != NULL ? native_json_obj_get(record_context, field_name) : NULL;
635 result = yyjson_mut_bool(doc, current != NULL ? !yyjson_get_bool(current) : 1);
636 free(field_name);
637 } else if (source != NULL && streq(source, "record_field")) {
638 char *field_name = native_json_obj_get_string_dup(value_contract, "field");
639 yyjson_val *current = record_context != NULL ? native_json_obj_get(record_context, field_name) : NULL;
640 if (current == NULL || yyjson_is_null(current)) {
641 result = yyjson_mut_null(doc);
642 } else if (yyjson_is_bool(current)) {
643 result = yyjson_mut_bool(doc, yyjson_get_bool(current));
644 } else if (yyjson_is_num(current)) {
645 result = yyjson_mut_sint(doc, yyjson_get_sint(current));
646 } else if (yyjson_is_str(current)) {
647 result = yyjson_mut_strcpy(doc, yyjson_get_str(current));
648 }
649 free(field_name);
650 } else if (source != NULL && streq(source, "context_anchor_created_id")) {
651 result = yyjson_mut_int(doc, context != NULL ? context->anchor_record_id : 0);
652 } else if (source != NULL && streq(source, "context_last_created_id")) {
653 result = yyjson_mut_int(doc, context != NULL ? context->last_created_record_id : 0);
654 } else if (source != NULL && streq(source, "context_last_touched_id")) {
655 result = yyjson_mut_int(doc, context != NULL ? context->last_touched_record_id : 0);
656 } else if (source != NULL && streq(source, "list_item")) {
657 result = yyjson_mut_strcpy(doc, list_item_text != NULL ? list_item_text : "");
658 } else if (source != NULL && streq(source, "source_record_field")) {
659 char *field_name = native_json_obj_get_string_dup(value_contract, "field");
660 yyjson_val *current = source_record_context != NULL ? native_json_obj_get(source_record_context, field_name) : NULL;
661 if (current == NULL || yyjson_is_null(current)) {
662 result = yyjson_mut_null(doc);
663 } else if (yyjson_is_bool(current)) {
664 result = yyjson_mut_bool(doc, yyjson_get_bool(current));
665 } else if (yyjson_is_num(current)) {
666 result = yyjson_mut_sint(doc, yyjson_get_sint(current));
667 } else if (yyjson_is_str(current)) {
668 result = yyjson_mut_strcpy(doc, yyjson_get_str(current));
669 }
670 free(field_name);
671 }
672 free(source);
673 return result;
674}
675
676static int append_list_item_logic_local(char ***items_out, size_t *count_out, const char *start, size_t length) {
677 char *item = NULL;
678 char **grown = NULL;
679 if (items_out == NULL || count_out == NULL) {
680 return 0;
681 }
682 item = dup_range(start, length);
683 if (item == NULL) {
684 return 0;
685 }
686 trim_in_place(item);
687 if (item[0] == '\0') {
688 free(item);
689 return 1;
690 }
691 grown = (char **)realloc(*items_out, sizeof(char *) * (*count_out + 1));
692 if (grown == NULL) {
693 free(item);
694 return 0;
695 }
696 *items_out = grown;
697 (*items_out)[*count_out] = item;
698 *count_out = *count_out + 1;
699 return 1;
700}
701
702static void free_split_items_logic_local(char **items, size_t count) {
703 size_t index = 0;
704 for (index = 0; index < count; index++) {
705 free(items[index]);
706 }
707 free(items);
708}
709
710static int split_input_list_logic_local(const char *raw_value, const char *delimiter, char ***items_out, size_t *count_out) {
711 const char *cursor = NULL;
712 const char *next = NULL;
713 size_t delimiter_len = 0;
714 if (items_out == NULL || count_out == NULL) {
715 return 0;
716 }
717 *items_out = NULL;
718 *count_out = 0;
719 if (raw_value == NULL) {
720 return 1;
721 }
722 delimiter = (delimiter != NULL && delimiter[0] != '\0') ? delimiter : "\n";
723 delimiter_len = strlen(delimiter);
724 if (delimiter_len == 0) {
725 delimiter = "\n";
726 delimiter_len = 1;
727 }
728 cursor = raw_value;
729 while (1) {
730 next = strstr(cursor, delimiter);
731 if (next == NULL) {
732 return append_list_item_logic_local(items_out, count_out, cursor, strlen(cursor));
733 }
734 if (!append_list_item_logic_local(items_out, count_out, cursor, (size_t)(next - cursor))) {
735 return 0;
736 }
737 cursor = next + delimiter_len;
738 }
739}
740
741static int set_mut_object_field_logic_local(yyjson_mut_doc *doc, yyjson_mut_val *obj, const char *field_name, yyjson_mut_val *value) {
742 if (doc == NULL || obj == NULL || field_name == NULL || value == NULL || !yyjson_mut_is_obj(obj)) {
743 return 0;
744 }
745 yyjson_mut_obj_remove_key(obj, field_name);
746 return yyjson_mut_obj_put(obj, yyjson_mut_strcpy(doc, field_name), value);
747}
748
749static char *scoped_resource_id_dup_logic_local(const char *collection_name, const char *field_name, const char *expected_text) {
750 size_t needed = 0;
751 char *resource_id = NULL;
752 if (collection_name == NULL || collection_name[0] == '\0') {
753 return NULL;
754 }
755 if (field_name == NULL || field_name[0] == '\0' || expected_text == NULL || expected_text[0] == '\0') {
756 needed = strlen(collection_name) + 8;
757 resource_id = (char *)malloc(needed);
758 if (resource_id != NULL) {
759 snprintf(resource_id, needed, "%s:scope", collection_name);
760 }
761 return resource_id;
762 }
763 needed = strlen(collection_name) + strlen(field_name) + strlen(expected_text) + 16;
764 resource_id = (char *)malloc(needed);
765 if (resource_id != NULL) {
766 snprintf(resource_id, needed, "%s:%s:%s", collection_name, field_name, expected_text);
767 }
768 return resource_id;
769}
770
771static int record_field_matches_text_value_logic_local(yyjson_val *record, const char *field_name, const char *expected_text) {
772 yyjson_val *field = NULL;
773 char buffer[64];
774 if (record == NULL || field_name == NULL || field_name[0] == '\0' || expected_text == NULL) {
775 return 0;
776 }
777 field = native_json_obj_get(record, field_name);
778 if (field == NULL || yyjson_is_null(field)) {
779 return 0;
780 }
781 if (yyjson_is_str(field)) {
782 return streq(yyjson_get_str(field), expected_text);
783 }
784 if (yyjson_is_num(field)) {
785 snprintf(buffer, sizeof(buffer), "%lld", (long long)yyjson_get_sint(field));
786 return streq(buffer, expected_text);
787 }
788 if (yyjson_is_bool(field)) {
789 return (yyjson_get_bool(field) && (streq(expected_text, "true") || streq(expected_text, "1")))
790 || (!yyjson_get_bool(field) && (streq(expected_text, "false") || streq(expected_text, "0")));
791 }
792 return 0;
793}
794
795static int record_field_matches_text_logic_local(yyjson_mut_val *record, const char *field_name, const char *expected_text) {
796 return record_field_matches_text_value_logic_local((yyjson_val *)record, field_name, expected_text);
797}
798
800 yyjson_val *scope_match,
801 const InputFieldMapLogicLocal *inputs,
803) {
804 char *from_input = NULL;
805 char *result = NULL;
806 if (scope_match == NULL || !yyjson_is_obj(scope_match)) {
807 return NULL;
808 }
809 from_input = native_json_obj_get_string_dup(scope_match, "from_input");
810 if (from_input != NULL && from_input[0] != '\0') {
811 result = input_field_value_dup_logic_local(inputs, from_input);
812 } else {
813 yyjson_val *literal = native_json_obj_get(scope_match, "literal");
814 result = string_value_from_contract_logic_local(literal, inputs, context, NULL, NULL);
815 }
816 free(from_input);
817 return result;
818}
819
821 yyjson_val *collection,
822 const char *scope_field,
823 const char *expected_text
824) {
825 size_t index = 0;
826 size_t max = 0;
827 yyjson_val *record = NULL;
828 long matched_count = 0;
829 if (collection == NULL || !yyjson_is_arr(collection)
830 || scope_field == NULL || scope_field[0] == '\0'
831 || expected_text == NULL) {
832 return 0;
833 }
834 yyjson_arr_foreach(collection, index, max, record) {
835 if (yyjson_is_obj(record)
836 && record_field_matches_text_value_logic_local(record, scope_field, expected_text)) {
837 matched_count++;
838 }
839 }
840 return matched_count;
841}
842
844 yyjson_mut_val *collection,
845 const char *scope_field,
846 const char *expected_text
847) {
848 return matching_record_count_value_logic_local((yyjson_val *)collection, scope_field, expected_text);
849}
850
852 yyjson_mut_doc *mut_doc,
853 yyjson_mut_val *mut_root,
854 yyjson_val *primitive,
855 const InputFieldMapLogicLocal *inputs,
857 long next_id,
858 long *created_record_id_out,
859 char **resource_id_out
860) {
861 char *collection_name = native_json_obj_get_string_dup(primitive, "collection");
862 yyjson_val *record_contract = native_json_obj_get(primitive, "record_contract");
863 yyjson_mut_val *collection = NULL;
864 yyjson_mut_val *record = NULL;
865 yyjson_obj_iter iter;
866 yyjson_val *field_contract = NULL;
867 yyjson_val *key = NULL;
868 if (created_record_id_out != NULL) {
869 *created_record_id_out = 0;
870 }
871 if (resource_id_out != NULL) {
872 *resource_id_out = NULL;
873 }
874 if (collection_name == NULL || record_contract == NULL || !yyjson_is_obj(record_contract)) {
875 free(collection_name);
876 return 0;
877 }
878 collection = yyjson_mut_obj_get(mut_root, collection_name);
879 if (collection == NULL) {
880 collection = yyjson_mut_arr(mut_doc);
881 if (collection == NULL || !yyjson_mut_obj_put(mut_root, yyjson_mut_strcpy(mut_doc, collection_name), collection)) {
882 free(collection_name);
883 return 0;
884 }
885 }
886 if (!yyjson_mut_is_arr(collection)) {
887 free(collection_name);
888 return 0;
889 }
890 record = yyjson_mut_obj(mut_doc);
891 if (record == NULL) {
892 free(collection_name);
893 return 0;
894 }
895 if (yyjson_obj_iter_init(record_contract, &iter)) {
896 while ((key = yyjson_obj_iter_next(&iter)) != NULL) {
897 const char *field_name = yyjson_get_str(key);
898 field_contract = yyjson_obj_iter_get_val(key);
899 if (field_name == NULL || field_contract == NULL) {
900 free(collection_name);
901 return 0;
902 }
903 {
904 yyjson_mut_val *value = field_value_from_contract_logic_local(mut_doc, field_contract, inputs, next_id, NULL, context, NULL, NULL);
905 if (value == NULL || !set_mut_object_field_logic_local(mut_doc, record, field_name, value)) {
906 free(collection_name);
907 return 0;
908 }
909 }
910 }
911 }
912 if (!yyjson_mut_arr_append(collection, record)) {
913 free(collection_name);
914 return 0;
915 }
916 if (created_record_id_out != NULL) {
917 yyjson_mut_val *id_value = yyjson_mut_obj_get(record, "id");
918 *created_record_id_out = (id_value != NULL && yyjson_is_num((yyjson_val *)id_value)) ? (long)yyjson_get_sint((yyjson_val *)id_value) : 0;
919 }
920 if (resource_id_out != NULL && created_record_id_out != NULL && *created_record_id_out > 0) {
921 size_t needed = strlen(collection_name) + 32;
922 *resource_id_out = (char *)malloc(needed);
923 if (*resource_id_out != NULL) {
924 snprintf(*resource_id_out, needed, "%s:%ld", collection_name, *created_record_id_out);
925 }
926 }
927 free(collection_name);
928 return 1;
929}
930
931static yyjson_mut_val *find_mut_record_by_long_field_logic_local(yyjson_mut_val *array, const char *field_name, long match_value) {
932 size_t index = 0;
933 size_t max = 0;
934 yyjson_mut_val *item = NULL;
935 if (array == NULL || field_name == NULL || !yyjson_mut_is_arr(array)) {
936 return NULL;
937 }
938 yyjson_mut_arr_foreach(array, index, max, item) {
939 yyjson_mut_val *field = yyjson_mut_obj_get(item, field_name);
940 if (yyjson_mut_is_obj(item) && field != NULL && yyjson_is_num((yyjson_val *)field) && yyjson_get_sint((yyjson_val *)field) == match_value) {
941 return item;
942 }
943 }
944 return NULL;
945}
946
948 yyjson_mut_doc *mut_doc,
949 yyjson_mut_val *mut_root,
950 yyjson_val *primitive,
951 const InputFieldMapLogicLocal *inputs,
953 long next_id,
954 long *created_count_out,
955 char **resource_id_out
956) {
957 char *collection_name = native_json_obj_get_string_dup(primitive, "collection");
958 yyjson_val *list_source = native_json_obj_get(primitive, "list_source");
959 yyjson_val *record_contract = native_json_obj_get(primitive, "record_contract");
960 char *field_name = list_source != NULL ? native_json_obj_get_string_dup(list_source, "from_input") : NULL;
961 char *delimiter = list_source != NULL ? native_json_obj_get_string_dup(list_source, "delimiter") : NULL;
962 char *raw_list = input_field_value_dup_logic_local(inputs, field_name);
963 char **items = NULL;
964 size_t item_count = 0;
965 size_t index = 0;
966 yyjson_mut_val *collection = NULL;
967 if (created_count_out != NULL) {
968 *created_count_out = 0;
969 }
970 if (resource_id_out != NULL) {
971 *resource_id_out = NULL;
972 }
973 if (collection_name == NULL || list_source == NULL || record_contract == NULL || !yyjson_is_obj(record_contract)) {
974 free(collection_name);
975 free(field_name);
976 free(delimiter);
977 free(raw_list);
978 return 0;
979 }
980 if (!split_input_list_logic_local(raw_list != NULL ? raw_list : "", delimiter, &items, &item_count) || item_count == 0) {
981 free(collection_name);
982 free(field_name);
983 free(delimiter);
984 free(raw_list);
985 free_split_items_logic_local(items, item_count);
986 return 0;
987 }
988 collection = yyjson_mut_obj_get(mut_root, collection_name);
989 if (collection == NULL) {
990 collection = yyjson_mut_arr(mut_doc);
991 if (collection == NULL || !yyjson_mut_obj_put(mut_root, yyjson_mut_strcpy(mut_doc, collection_name), collection)) {
992 free(collection_name);
993 free(field_name);
994 free(delimiter);
995 free(raw_list);
996 free_split_items_logic_local(items, item_count);
997 return 0;
998 }
999 }
1000 if (!yyjson_mut_is_arr(collection)) {
1001 free(collection_name);
1002 free(field_name);
1003 free(delimiter);
1004 free(raw_list);
1005 free_split_items_logic_local(items, item_count);
1006 return 0;
1007 }
1008 for (index = 0; index < item_count; index++) {
1009 yyjson_mut_val *record = yyjson_mut_obj(mut_doc);
1010 yyjson_obj_iter iter;
1011 yyjson_val *field_contract = NULL;
1012 yyjson_val *key = NULL;
1013 long record_id = 0;
1014 char *item_resource_id = NULL;
1015 if (record == NULL) {
1016 free(collection_name);
1017 free(field_name);
1018 free(delimiter);
1019 free(raw_list);
1020 free_split_items_logic_local(items, item_count);
1021 return 0;
1022 }
1023 if (yyjson_obj_iter_init(record_contract, &iter)) {
1024 while ((key = yyjson_obj_iter_next(&iter)) != NULL) {
1025 const char *record_field_name = yyjson_get_str(key);
1026 field_contract = yyjson_obj_iter_get_val(key);
1027 yyjson_mut_val *value = NULL;
1028 if (record_field_name == NULL || field_contract == NULL) {
1029 free(collection_name);
1030 free(field_name);
1031 free(delimiter);
1032 free(raw_list);
1033 free_split_items_logic_local(items, item_count);
1034 return 0;
1035 }
1036 value = field_value_from_contract_logic_local(mut_doc, field_contract, inputs, next_id + (long)index, NULL, context, items[index], NULL);
1037 if (value == NULL || !set_mut_object_field_logic_local(mut_doc, record, record_field_name, value)) {
1038 free(collection_name);
1039 free(field_name);
1040 free(delimiter);
1041 free(raw_list);
1042 free_split_items_logic_local(items, item_count);
1043 return 0;
1044 }
1045 }
1046 }
1047 if (!yyjson_mut_arr_append(collection, record)) {
1048 free(collection_name);
1049 free(field_name);
1050 free(delimiter);
1051 free(raw_list);
1052 free_split_items_logic_local(items, item_count);
1053 return 0;
1054 }
1055 {
1056 yyjson_mut_val *id_value = yyjson_mut_obj_get(record, "id");
1057 record_id = (id_value != NULL && yyjson_is_num((yyjson_val *)id_value)) ? (long)yyjson_get_sint((yyjson_val *)id_value) : 0;
1058 }
1059 if (record_id > 0) {
1060 size_t needed = strlen(collection_name) + 32;
1061 item_resource_id = (char *)malloc(needed);
1062 if (item_resource_id == NULL) {
1063 free(collection_name);
1064 free(field_name);
1065 free(delimiter);
1066 free(raw_list);
1067 free_split_items_logic_local(items, item_count);
1068 return 0;
1069 }
1070 snprintf(item_resource_id, needed, "%s:%ld", collection_name, record_id);
1071 if (!note_created_record_logic_local(context, record_id, item_resource_id)) {
1072 free(item_resource_id);
1073 free(collection_name);
1074 free(field_name);
1075 free(delimiter);
1076 free(raw_list);
1077 free_split_items_logic_local(items, item_count);
1078 return 0;
1079 }
1080 if (resource_id_out != NULL) {
1081 free(*resource_id_out);
1082 *resource_id_out = item_resource_id;
1083 item_resource_id = NULL;
1084 }
1085 free(item_resource_id);
1086 }
1087 }
1088 if (created_count_out != NULL) {
1089 *created_count_out = (long)item_count;
1090 }
1091 free(collection_name);
1092 free(field_name);
1093 free(delimiter);
1094 free(raw_list);
1095 free_split_items_logic_local(items, item_count);
1096 return 1;
1097}
1098
1100 size_t index = 0;
1101 size_t max = 0;
1102 yyjson_mut_val *record = NULL;
1103 if (collection == NULL || !yyjson_mut_is_arr(collection) || record_id <= 0) {
1104 return NULL;
1105 }
1106 yyjson_mut_arr_foreach(collection, index, max, record) {
1107 yyjson_mut_val *id_value = NULL;
1108 if (!yyjson_mut_is_obj(record)) {
1109 continue;
1110 }
1111 id_value = yyjson_mut_obj_get(record, "id");
1112 if (id_value != NULL && yyjson_is_num((yyjson_val *)id_value)
1113 && (long)yyjson_get_sint((yyjson_val *)id_value) == record_id) {
1114 return record;
1115 }
1116 }
1117 return NULL;
1118}
1119
1121 yyjson_mut_doc *mut_doc,
1122 yyjson_mut_val *mut_root,
1123 yyjson_val *source_root,
1124 yyjson_val *primitive,
1125 const InputFieldMapLogicLocal *inputs,
1127 long next_id,
1128 long *created_count_out,
1129 char **resource_id_out
1130) {
1131 char *collection_name = native_json_obj_get_string_dup(primitive, "collection");
1132 char *source_collection_name = native_json_obj_get_string_dup(primitive, "source_collection");
1133 yyjson_val *scope_match = native_json_obj_get(primitive, "scope_match");
1134 yyjson_val *record_contract = native_json_obj_get(primitive, "record_contract");
1135 char *scope_field = scope_match != NULL ? native_json_obj_get_string_dup(scope_match, "field") : NULL;
1136 char *expected_text = scope_match_expected_text_dup_logic_local(scope_match, inputs, context);
1137 char *overflow_policy = native_json_obj_get_string_dup(primitive, "overflow_policy");
1138 long max_records = native_json_obj_get_long_default(primitive, "max_records", 100, NULL);
1139 yyjson_mut_val *target_collection = NULL;
1140 yyjson_val *source_collection = NULL;
1141 size_t index = 0;
1142 size_t max = 0;
1143 yyjson_val *source_record = NULL;
1144 long matched_count = 0;
1145 long created_count = 0;
1146 if (created_count_out != NULL) {
1147 *created_count_out = 0;
1148 }
1149 if (resource_id_out != NULL) {
1150 *resource_id_out = NULL;
1151 }
1152 if (source_collection_name == NULL || source_collection_name[0] == '\0') {
1153 free(source_collection_name);
1154 source_collection_name = collection_name != NULL ? strdup(collection_name) : NULL;
1155 }
1156 if (overflow_policy == NULL || overflow_policy[0] == '\0') {
1157 free(overflow_policy);
1158 overflow_policy = strdup("truncate");
1159 }
1160 if (collection_name == NULL || source_collection_name == NULL || scope_field == NULL || expected_text == NULL
1161 || record_contract == NULL || !yyjson_is_obj(record_contract) || overflow_policy == NULL) {
1162 free(collection_name);
1163 free(source_collection_name);
1164 free(scope_field);
1165 free(expected_text);
1166 free(overflow_policy);
1167 return 0;
1168 }
1169 target_collection = yyjson_mut_obj_get(mut_root, collection_name);
1170 if (target_collection == NULL) {
1171 target_collection = yyjson_mut_arr(mut_doc);
1172 if (target_collection == NULL || !yyjson_mut_obj_put(mut_root, yyjson_mut_strcpy(mut_doc, collection_name), target_collection)) {
1173 free(collection_name);
1174 free(source_collection_name);
1175 free(scope_field);
1176 free(expected_text);
1177 free(overflow_policy);
1178 return 0;
1179 }
1180 }
1181 source_collection = source_root != NULL ? native_json_obj_get(source_root, source_collection_name) : NULL;
1182 if (!yyjson_mut_is_arr(target_collection) || source_collection == NULL || !yyjson_is_arr(source_collection)) {
1183 free(collection_name);
1184 free(source_collection_name);
1185 free(scope_field);
1186 free(expected_text);
1187 free(overflow_policy);
1188 return -1;
1189 }
1190 matched_count = matching_record_count_value_logic_local(source_collection, scope_field, expected_text);
1191 if (matched_count <= 0) {
1192 free(collection_name);
1193 free(source_collection_name);
1194 free(scope_field);
1195 free(expected_text);
1196 free(overflow_policy);
1197 return -1;
1198 }
1199 if (streq(overflow_policy, "fail") && matched_count > max_records) {
1200 free(collection_name);
1201 free(source_collection_name);
1202 free(scope_field);
1203 free(expected_text);
1204 free(overflow_policy);
1205 return -2;
1206 }
1207 yyjson_arr_foreach(source_collection, index, max, source_record) {
1208 yyjson_mut_val *record = NULL;
1209 yyjson_obj_iter iter;
1210 yyjson_val *key = NULL;
1211 yyjson_val *field_contract = NULL;
1212 yyjson_mut_val *id_value = NULL;
1213 long record_id = 0;
1214 char *record_resource_id = NULL;
1215 if (created_count >= max_records) {
1216 break;
1217 }
1218 if (!yyjson_is_obj(source_record)
1219 || !record_field_matches_text_value_logic_local(source_record, scope_field, expected_text)) {
1220 continue;
1221 }
1222 record = yyjson_mut_obj(mut_doc);
1223 if (record == NULL) {
1224 free(collection_name);
1225 free(source_collection_name);
1226 free(scope_field);
1227 free(expected_text);
1228 free(overflow_policy);
1229 return 0;
1230 }
1231 if (yyjson_obj_iter_init(record_contract, &iter)) {
1232 while ((key = yyjson_obj_iter_next(&iter)) != NULL) {
1233 const char *record_field_name = yyjson_get_str(key);
1234 yyjson_mut_val *value = NULL;
1235 field_contract = yyjson_obj_iter_get_val(key);
1236 if (record_field_name == NULL || field_contract == NULL) {
1237 free(collection_name);
1238 free(source_collection_name);
1239 free(scope_field);
1240 free(expected_text);
1241 free(overflow_policy);
1242 return 0;
1243 }
1244 value = field_value_from_contract_logic_local(mut_doc, field_contract, inputs, next_id + created_count, NULL, context, NULL, source_record);
1245 if (value == NULL || !set_mut_object_field_logic_local(mut_doc, record, record_field_name, value)) {
1246 free(collection_name);
1247 free(source_collection_name);
1248 free(scope_field);
1249 free(expected_text);
1250 free(overflow_policy);
1251 return 0;
1252 }
1253 }
1254 }
1255 if (!yyjson_mut_arr_append(target_collection, record)) {
1256 free(collection_name);
1257 free(source_collection_name);
1258 free(scope_field);
1259 free(expected_text);
1260 free(overflow_policy);
1261 return 0;
1262 }
1263 id_value = yyjson_mut_obj_get(record, "id");
1264 record_id = (id_value != NULL && yyjson_is_num((yyjson_val *)id_value)) ? (long)yyjson_get_sint((yyjson_val *)id_value) : 0;
1265 if (record_id > 0) {
1266 size_t needed = strlen(collection_name) + 32;
1267 record_resource_id = (char *)malloc(needed);
1268 if (record_resource_id == NULL) {
1269 free(collection_name);
1270 free(source_collection_name);
1271 free(scope_field);
1272 free(expected_text);
1273 free(overflow_policy);
1274 return 0;
1275 }
1276 snprintf(record_resource_id, needed, "%s:%ld", collection_name, record_id);
1277 if (!note_created_record_logic_local(context, record_id, record_resource_id)) {
1278 free(record_resource_id);
1279 free(collection_name);
1280 free(source_collection_name);
1281 free(scope_field);
1282 free(expected_text);
1283 free(overflow_policy);
1284 return 0;
1285 }
1286 if (resource_id_out != NULL) {
1287 free(*resource_id_out);
1288 *resource_id_out = record_resource_id;
1289 record_resource_id = NULL;
1290 }
1291 free(record_resource_id);
1292 }
1293 created_count++;
1294 }
1295 if (created_count == 0) {
1296 free(collection_name);
1297 free(source_collection_name);
1298 free(scope_field);
1299 free(expected_text);
1300 free(overflow_policy);
1301 return -1;
1302 }
1303 if (context != NULL && !note_bounded_scope_logic_local(context, matched_count, created_count, overflow_policy)) {
1304 free(collection_name);
1305 free(source_collection_name);
1306 free(scope_field);
1307 free(expected_text);
1308 free(overflow_policy);
1309 return 0;
1310 }
1311 if (resource_id_out != NULL && *resource_id_out == NULL) {
1312 *resource_id_out = scoped_resource_id_dup_logic_local(source_collection_name, scope_field, expected_text);
1313 }
1314 if (created_count_out != NULL) {
1315 *created_count_out = created_count;
1316 }
1317 free(collection_name);
1318 free(source_collection_name);
1319 free(scope_field);
1320 free(expected_text);
1321 free(overflow_policy);
1322 return 1;
1323}
1324
1326 yyjson_mut_doc *mut_doc,
1327 yyjson_mut_val *mut_root,
1328 yyjson_val *primitive,
1329 const InputFieldMapLogicLocal *inputs,
1331 char **resource_id_out,
1332 char **from_state_out,
1333 char **to_state_out
1334) {
1335 char *collection_name = native_json_obj_get_string_dup(primitive, "collection");
1336 yyjson_val *match = native_json_obj_get(primitive, "match");
1337 yyjson_val *updates = native_json_obj_get(primitive, "updates");
1338 char *match_field = match != NULL ? native_json_obj_get_string_dup(match, "field") : NULL;
1339 char *from_input = match != NULL ? native_json_obj_get_string_dup(match, "from_input") : NULL;
1340 char *input_value = input_field_value_dup_logic_local(inputs, from_input);
1341 long match_id = input_value != NULL ? strtol(input_value, NULL, 10) : 0;
1342 yyjson_mut_val *collection = NULL;
1343 yyjson_mut_val *record = NULL;
1344 yyjson_obj_iter iter;
1345 yyjson_val *key = NULL;
1346 yyjson_val *field_contract = NULL;
1347 if (resource_id_out != NULL) {
1348 *resource_id_out = NULL;
1349 }
1350 if (from_state_out != NULL) {
1351 *from_state_out = NULL;
1352 }
1353 if (to_state_out != NULL) {
1354 *to_state_out = NULL;
1355 }
1356 if (collection_name == NULL || match_field == NULL || from_input == NULL || updates == NULL || !yyjson_is_obj(updates) || match_id <= 0) {
1357 free(collection_name);
1358 free(match_field);
1359 free(from_input);
1360 free(input_value);
1361 return 0;
1362 }
1363 collection = yyjson_mut_obj_get(mut_root, collection_name);
1364 record = find_mut_record_by_long_field_logic_local(collection, match_field, match_id);
1365 if (record == NULL) {
1366 free(collection_name);
1367 free(match_field);
1368 free(from_input);
1369 free(input_value);
1370 return -1;
1371 }
1372 if (from_state_out != NULL) {
1373 yyjson_mut_val *current = yyjson_mut_obj_get(record, "workflow_state");
1374 if (current != NULL && yyjson_is_str((yyjson_val *)current)) {
1375 *from_state_out = strdup(yyjson_get_str((yyjson_val *)current));
1376 }
1377 }
1378 if (yyjson_obj_iter_init(updates, &iter)) {
1379 while ((key = yyjson_obj_iter_next(&iter)) != NULL) {
1380 const char *field_name = yyjson_get_str(key);
1381 field_contract = yyjson_obj_iter_get_val(key);
1382 if (field_name == NULL || field_contract == NULL) {
1383 free(collection_name);
1384 free(match_field);
1385 free(from_input);
1386 free(input_value);
1387 return 0;
1388 }
1389 {
1390 yyjson_mut_val *value = field_value_from_contract_logic_local(mut_doc, field_contract, inputs, 0, (yyjson_val *)record, context, NULL, NULL);
1391 if (value == NULL || !set_mut_object_field_logic_local(mut_doc, record, field_name, value)) {
1392 free(collection_name);
1393 free(match_field);
1394 free(from_input);
1395 free(input_value);
1396 return 0;
1397 }
1398 if (to_state_out != NULL && streq(field_name, "workflow_state") && yyjson_mut_is_str(value)) {
1399 free(*to_state_out);
1400 *to_state_out = strdup(yyjson_get_str((yyjson_val *)value));
1401 }
1402 }
1403 }
1404 }
1405 if (resource_id_out != NULL) {
1406 size_t needed = strlen(collection_name) + 32;
1407 *resource_id_out = (char *)malloc(needed);
1408 if (*resource_id_out != NULL) {
1409 snprintf(*resource_id_out, needed, "%s:%ld", collection_name, match_id);
1410 }
1411 }
1412 if (context != NULL && !note_updated_record_logic_local(context, match_id, resource_id_out != NULL ? *resource_id_out : NULL)) {
1413 free(collection_name);
1414 free(match_field);
1415 free(from_input);
1416 free(input_value);
1417 return 0;
1418 }
1419 free(collection_name);
1420 free(match_field);
1421 free(from_input);
1422 free(input_value);
1423 return 1;
1424}
1425
1427 yyjson_mut_doc *mut_doc,
1428 yyjson_mut_val *mut_root,
1429 yyjson_val *source_root,
1430 yyjson_val *primitive,
1431 const InputFieldMapLogicLocal *inputs,
1433 char **resource_id_out
1434) {
1435 char *collection_name = native_json_obj_get_string_dup(primitive, "collection");
1436 yyjson_val *scope_match = native_json_obj_get(primitive, "scope_match");
1437 yyjson_val *value_contract = native_json_obj_get(primitive, "value_contract");
1438 char *scope_field = scope_match != NULL ? native_json_obj_get_string_dup(scope_match, "field") : NULL;
1439 char *expected_text = scope_match_expected_text_dup_logic_local(scope_match, inputs, context);
1440 char *target_field = native_json_obj_get_string_dup(primitive, "target_field");
1441 char *overflow_policy = native_json_obj_get_string_dup(primitive, "overflow_policy");
1442 long max_records = native_json_obj_get_long_default(primitive, "max_records", 100, NULL);
1443 yyjson_mut_val *collection = NULL;
1444 yyjson_val *source_collection = NULL;
1445 size_t index = 0;
1446 size_t max = 0;
1447 yyjson_val *source_record = NULL;
1448 long updated_count = 0;
1449 long matched_count = 0;
1450 if (resource_id_out != NULL) {
1451 *resource_id_out = NULL;
1452 }
1453 if (overflow_policy == NULL || overflow_policy[0] == '\0') {
1454 free(overflow_policy);
1455 overflow_policy = strdup("truncate");
1456 }
1457 if (collection_name == NULL || scope_field == NULL || target_field == NULL || value_contract == NULL || expected_text == NULL || overflow_policy == NULL) {
1458 free(collection_name);
1459 free(scope_field);
1460 free(expected_text);
1461 free(target_field);
1462 free(overflow_policy);
1463 return 0;
1464 }
1465 collection = yyjson_mut_obj_get(mut_root, collection_name);
1466 source_collection = source_root != NULL ? native_json_obj_get(source_root, collection_name) : NULL;
1467 if (collection == NULL || !yyjson_mut_is_arr(collection)
1468 || source_collection == NULL || !yyjson_is_arr(source_collection)) {
1469 free(collection_name);
1470 free(scope_field);
1471 free(expected_text);
1472 free(target_field);
1473 free(overflow_policy);
1474 return -1;
1475 }
1476 matched_count = matching_record_count_value_logic_local(source_collection, scope_field, expected_text);
1477 if (matched_count <= 0) {
1478 free(collection_name);
1479 free(scope_field);
1480 free(expected_text);
1481 free(target_field);
1482 free(overflow_policy);
1483 return -1;
1484 }
1485 if (streq(overflow_policy, "fail") && matched_count > max_records) {
1486 free(collection_name);
1487 free(scope_field);
1488 free(expected_text);
1489 free(target_field);
1490 free(overflow_policy);
1491 return -2;
1492 }
1493 yyjson_arr_foreach(source_collection, index, max, source_record) {
1494 yyjson_val *source_id_value = NULL;
1495 yyjson_mut_val *record = NULL;
1496 long record_id = 0;
1497 char *record_resource_id = NULL;
1498 yyjson_mut_val *value = NULL;
1499 if (updated_count >= max_records) {
1500 break;
1501 }
1502 if (!yyjson_is_obj(source_record)
1503 || !record_field_matches_text_value_logic_local(source_record, scope_field, expected_text)) {
1504 continue;
1505 }
1506 source_id_value = native_json_obj_get(source_record, "id");
1507 record_id = (source_id_value != NULL && yyjson_is_num(source_id_value)) ? (long)yyjson_get_sint(source_id_value) : 0;
1508 record = find_mut_record_by_id_logic_local(collection, record_id);
1509 if (record == NULL) {
1510 free(collection_name);
1511 free(scope_field);
1512 free(expected_text);
1513 free(target_field);
1514 free(overflow_policy);
1515 return 0;
1516 }
1517 value = field_value_from_contract_logic_local(mut_doc, value_contract, inputs, 0, source_record, context, NULL, NULL);
1518 if (value == NULL || !set_mut_object_field_logic_local(mut_doc, record, target_field, value)) {
1519 free(collection_name);
1520 free(scope_field);
1521 free(expected_text);
1522 free(target_field);
1523 free(overflow_policy);
1524 return 0;
1525 }
1526 if (record_id > 0) {
1527 size_t needed = strlen(collection_name) + 32;
1528 record_resource_id = (char *)malloc(needed);
1529 if (record_resource_id == NULL) {
1530 free(collection_name);
1531 free(scope_field);
1532 free(expected_text);
1533 free(target_field);
1534 free(overflow_policy);
1535 return 0;
1536 }
1537 snprintf(record_resource_id, needed, "%s:%ld", collection_name, record_id);
1538 if (context != NULL && !note_updated_record_logic_local(context, record_id, record_resource_id)) {
1539 free(record_resource_id);
1540 free(collection_name);
1541 free(scope_field);
1542 free(expected_text);
1543 free(target_field);
1544 free(overflow_policy);
1545 return 0;
1546 }
1547 free(record_resource_id);
1548 }
1549 updated_count++;
1550 }
1551 if (updated_count == 0) {
1552 free(collection_name);
1553 free(scope_field);
1554 free(expected_text);
1555 free(target_field);
1556 free(overflow_policy);
1557 return -1;
1558 }
1559 if (context != NULL
1560 && !note_bounded_scope_logic_local(context, matched_count, updated_count, overflow_policy)) {
1561 free(collection_name);
1562 free(scope_field);
1563 free(expected_text);
1564 free(target_field);
1565 free(overflow_policy);
1566 return 0;
1567 }
1568 if (resource_id_out != NULL) {
1569 *resource_id_out = scoped_resource_id_dup_logic_local(collection_name, scope_field, expected_text);
1570 }
1571 free(collection_name);
1572 free(scope_field);
1573 free(expected_text);
1574 free(target_field);
1575 free(overflow_policy);
1576 return 1;
1577}
1578
1580 yyjson_mut_doc *mut_doc,
1581 yyjson_mut_val *mut_root,
1582 yyjson_val *primitive,
1583 const InputFieldMapLogicLocal *inputs,
1585 char **resource_id_out,
1586 char **from_state_out,
1587 char **to_state_out
1588) {
1589 char *collection_name = native_json_obj_get_string_dup(primitive, "collection");
1590 yyjson_val *scope_match = native_json_obj_get(primitive, "scope_match");
1591 yyjson_val *updates = native_json_obj_get(primitive, "updates");
1592 char *scope_field = scope_match != NULL ? native_json_obj_get_string_dup(scope_match, "field") : NULL;
1593 char *expected_text = scope_match_expected_text_dup_logic_local(scope_match, inputs, context);
1594 char *overflow_policy = native_json_obj_get_string_dup(primitive, "overflow_policy");
1595 long max_records = native_json_obj_get_long_default(primitive, "max_records", 100, NULL);
1596 yyjson_mut_val *collection = NULL;
1597 size_t index = 0;
1598 size_t max = 0;
1599 yyjson_mut_val *record = NULL;
1600 long updated_count = 0;
1601 long matched_count = 0;
1602 if (resource_id_out != NULL) {
1603 *resource_id_out = NULL;
1604 }
1605 if (from_state_out != NULL) {
1606 *from_state_out = NULL;
1607 }
1608 if (to_state_out != NULL) {
1609 *to_state_out = NULL;
1610 }
1611 if (overflow_policy == NULL || overflow_policy[0] == '\0') {
1612 free(overflow_policy);
1613 overflow_policy = strdup("truncate");
1614 }
1615 if (collection_name == NULL || scope_field == NULL || expected_text == NULL || updates == NULL || !yyjson_is_obj(updates) || overflow_policy == NULL) {
1616 free(collection_name);
1617 free(scope_field);
1618 free(expected_text);
1619 free(overflow_policy);
1620 return 0;
1621 }
1622 collection = yyjson_mut_obj_get(mut_root, collection_name);
1623 if (collection == NULL || !yyjson_mut_is_arr(collection)) {
1624 free(collection_name);
1625 free(scope_field);
1626 free(expected_text);
1627 free(overflow_policy);
1628 return -1;
1629 }
1630 matched_count = matching_record_count_logic_local(collection, scope_field, expected_text);
1631 if (matched_count <= 0) {
1632 free(collection_name);
1633 free(scope_field);
1634 free(expected_text);
1635 free(overflow_policy);
1636 return -1;
1637 }
1638 if (streq(overflow_policy, "fail") && matched_count > max_records) {
1639 free(collection_name);
1640 free(scope_field);
1641 free(expected_text);
1642 free(overflow_policy);
1643 return -2;
1644 }
1645 yyjson_mut_arr_foreach(collection, index, max, record) {
1646 yyjson_obj_iter iter;
1647 yyjson_val *key = NULL;
1648 yyjson_val *field_contract = NULL;
1649 yyjson_mut_val *id_value = NULL;
1650 long record_id = 0;
1651 char *record_resource_id = NULL;
1652 if (updated_count >= max_records) {
1653 break;
1654 }
1655 if (!yyjson_mut_is_obj(record) || !record_field_matches_text_logic_local(record, scope_field, expected_text)) {
1656 continue;
1657 }
1658 if (from_state_out != NULL && *from_state_out == NULL) {
1659 yyjson_mut_val *current = yyjson_mut_obj_get(record, "workflow_state");
1660 if (current != NULL && yyjson_is_str((yyjson_val *)current)) {
1661 *from_state_out = strdup(yyjson_get_str((yyjson_val *)current));
1662 }
1663 }
1664 if (yyjson_obj_iter_init(updates, &iter)) {
1665 while ((key = yyjson_obj_iter_next(&iter)) != NULL) {
1666 const char *field_name = yyjson_get_str(key);
1667 field_contract = yyjson_obj_iter_get_val(key);
1668 yyjson_mut_val *value = NULL;
1669 if (field_name == NULL || field_contract == NULL) {
1670 free(collection_name);
1671 free(scope_field);
1672 free(expected_text);
1673 free(overflow_policy);
1674 return 0;
1675 }
1676 value = field_value_from_contract_logic_local(mut_doc, field_contract, inputs, 0, (yyjson_val *)record, context, NULL, NULL);
1677 if (value == NULL || !set_mut_object_field_logic_local(mut_doc, record, field_name, value)) {
1678 free(collection_name);
1679 free(scope_field);
1680 free(expected_text);
1681 free(overflow_policy);
1682 return 0;
1683 }
1684 if (to_state_out != NULL && streq(field_name, "workflow_state") && yyjson_mut_is_str(value)) {
1685 free(*to_state_out);
1686 *to_state_out = strdup(yyjson_get_str((yyjson_val *)value));
1687 }
1688 }
1689 }
1690 id_value = yyjson_mut_obj_get(record, "id");
1691 record_id = (id_value != NULL && yyjson_is_num((yyjson_val *)id_value)) ? (long)yyjson_get_sint((yyjson_val *)id_value) : 0;
1692 if (record_id > 0) {
1693 size_t needed = strlen(collection_name) + 32;
1694 record_resource_id = (char *)malloc(needed);
1695 if (record_resource_id == NULL) {
1696 free(collection_name);
1697 free(scope_field);
1698 free(expected_text);
1699 free(overflow_policy);
1700 return 0;
1701 }
1702 snprintf(record_resource_id, needed, "%s:%ld", collection_name, record_id);
1703 if (context != NULL && !note_updated_record_logic_local(context, record_id, record_resource_id)) {
1704 free(record_resource_id);
1705 free(collection_name);
1706 free(scope_field);
1707 free(expected_text);
1708 free(overflow_policy);
1709 return 0;
1710 }
1711 free(record_resource_id);
1712 }
1713 updated_count++;
1714 }
1715 if (updated_count == 0) {
1716 free(collection_name);
1717 free(scope_field);
1718 free(expected_text);
1719 free(overflow_policy);
1720 return -1;
1721 }
1722 if (context != NULL
1723 && !note_bounded_scope_logic_local(context, matched_count, updated_count, overflow_policy)) {
1724 free(collection_name);
1725 free(scope_field);
1726 free(expected_text);
1727 free(overflow_policy);
1728 return 0;
1729 }
1730 if (resource_id_out != NULL) {
1731 *resource_id_out = scoped_resource_id_dup_logic_local(collection_name, scope_field, expected_text);
1732 }
1733 free(collection_name);
1734 free(scope_field);
1735 free(expected_text);
1736 free(overflow_policy);
1737 return 1;
1738}
1739
1741 yyjson_mut_doc *mut_doc,
1742 yyjson_mut_val *mut_root,
1743 yyjson_val *primitive,
1744 const InputFieldMapLogicLocal *inputs,
1745 const ActionExecutionContextLogicLocal *context,
1746 long next_id
1747) {
1748 yyjson_val *fields = native_json_obj_get(primitive, "fields");
1749 yyjson_obj_iter iter;
1750 yyjson_val *key = NULL;
1751 yyjson_val *field_contract = NULL;
1752 if (fields == NULL || !yyjson_is_obj(fields)) {
1753 return 0;
1754 }
1755 if (yyjson_obj_iter_init(fields, &iter)) {
1756 while ((key = yyjson_obj_iter_next(&iter)) != NULL) {
1757 const char *field_name = yyjson_get_str(key);
1758 field_contract = yyjson_obj_iter_get_val(key);
1759 if (field_name == NULL || field_contract == NULL) {
1760 return 0;
1761 }
1762 {
1763 yyjson_mut_val *value = field_value_from_contract_logic_local(mut_doc, field_contract, inputs, next_id, NULL, context, NULL, NULL);
1764 if (value == NULL || !set_mut_object_field_logic_local(mut_doc, mut_root, field_name, value)) {
1765 return 0;
1766 }
1767 }
1768 }
1769 }
1770 return 1;
1771}
1772
1773static long next_section_id_logic_local(yyjson_mut_val *root, const char *section_name) {
1774 yyjson_mut_val *array = root != NULL ? yyjson_mut_obj_get(root, section_name) : NULL;
1775 size_t index = 0;
1776 size_t max = 0;
1777 yyjson_mut_val *item = NULL;
1778 long max_id = 0;
1779 if (array == NULL || !yyjson_mut_is_arr(array)) {
1780 return 1;
1781 }
1782 yyjson_mut_arr_foreach(array, index, max, item) {
1783 yyjson_mut_val *id_value = yyjson_mut_obj_get(item, "id");
1784 if (id_value != NULL && yyjson_is_num((yyjson_val *)id_value) && yyjson_get_sint((yyjson_val *)id_value) > max_id) {
1785 max_id = (long)yyjson_get_sint((yyjson_val *)id_value);
1786 }
1787 }
1788 return max_id + 1;
1789}
1790
1792 yyjson_mut_val *array = root != NULL ? yyjson_mut_obj_get(root, field_name) : NULL;
1793 if (array != NULL && yyjson_mut_is_arr(array)) {
1794 return array;
1795 }
1796 array = yyjson_mut_arr(doc);
1797 if (array == NULL || !yyjson_mut_obj_put(root, yyjson_mut_strcpy(doc, field_name), array)) {
1798 return NULL;
1799 }
1800 return array;
1801}
1802
1803static int append_mut_object_to_array_logic_local(yyjson_mut_doc *doc, yyjson_mut_val *root, const char *section_name, yyjson_mut_val *obj) {
1804 yyjson_mut_val *array = ensure_mut_array_field_logic_local(doc, root, section_name);
1805 if (array == NULL || obj == NULL) {
1806 return 0;
1807 }
1808 return yyjson_mut_arr_append(array, obj);
1809}
1810
1811static yyjson_val *find_integration_adapter_logic_local(yyjson_val *root, const char *adapter_id) {
1812 yyjson_val *adapters = root != NULL ? native_json_obj_get(root, "adapters") : NULL;
1813 yyjson_arr_iter iter;
1814 yyjson_val *item = NULL;
1815 if (adapters == NULL || adapter_id == NULL || adapter_id[0] == '\0' || !yyjson_is_arr(adapters)) {
1816 return NULL;
1817 }
1818 if (yyjson_arr_iter_init(adapters, &iter)) {
1819 while ((item = yyjson_arr_iter_next(&iter)) != NULL) {
1820 if (yyjson_is_obj(item) && yyjson_equals_str(native_json_obj_get(item, "adapter_id"), adapter_id)) {
1821 return item;
1822 }
1823 }
1824 }
1825 return NULL;
1826}
1827
1829 char *target_url = NULL;
1830 char *target_url_env = NULL;
1831 if (adapter == NULL || !yyjson_is_obj(adapter)) {
1832 return NULL;
1833 }
1834 target_url = native_json_obj_get_string_dup(adapter, "target_url");
1835 if (target_url != NULL && target_url[0] != '\0') {
1836 return target_url;
1837 }
1838 free(target_url);
1839 target_url_env = native_json_obj_get_string_dup(adapter, "target_url_env");
1840 if (target_url_env != NULL && target_url_env[0] != '\0') {
1841 const char *env_value = getenv(target_url_env);
1842 free(target_url_env);
1843 if (env_value != NULL && env_value[0] != '\0') {
1844 return strdup(env_value);
1845 }
1846 return NULL;
1847 }
1848 free(target_url_env);
1849 return NULL;
1850}
1851
1852static int parse_http_url_logic_local(const char *url, ParsedHttpUrlLogicLocal *parsed) {
1853 const char *remainder = NULL;
1854 const char *slash = NULL;
1855 const char *colon = NULL;
1856 size_t host_len = 0;
1857 if (parsed == NULL) {
1858 return 0;
1859 }
1860 memset(parsed, 0, sizeof(*parsed));
1861 if (url == NULL || strncmp(url, "http://", 7) != 0) {
1862 return 0;
1863 }
1864 parsed->scheme = strdup("http");
1865 remainder = url + 7;
1866 slash = strchr(remainder, '/');
1867 if (slash == NULL) {
1868 slash = remainder + strlen(remainder);
1869 parsed->path = strdup("/");
1870 } else {
1871 parsed->path = strdup(slash[0] != '\0' ? slash : "/");
1872 }
1873 colon = NULL;
1874 {
1875 const char *cursor = remainder;
1876 while (cursor < slash) {
1877 if (*cursor == ':') {
1878 colon = cursor;
1879 }
1880 cursor++;
1881 }
1882 }
1883 if (colon != NULL) {
1884 host_len = (size_t)(colon - remainder);
1885 parsed->port = atoi(colon + 1);
1886 } else {
1887 host_len = (size_t)(slash - remainder);
1888 parsed->port = 80;
1889 }
1890 parsed->host = dup_range(remainder, host_len);
1891 if (parsed->scheme == NULL || parsed->host == NULL || parsed->path == NULL || parsed->host[0] == '\0' || parsed->port <= 0) {
1893 return 0;
1894 }
1895 return 1;
1896}
1897
1898static int append_bytes_logic_local(char **buffer, size_t *length, size_t *capacity, const char *chunk, size_t chunk_len) {
1899 char *grown = NULL;
1900 size_t needed = 0;
1901 if (buffer == NULL || length == NULL || capacity == NULL || chunk == NULL) {
1902 return 0;
1903 }
1904 needed = *length + chunk_len + 1;
1905 if (needed > *capacity) {
1906 size_t next_capacity = *capacity == 0 ? 1024 : *capacity;
1907 while (next_capacity < needed) {
1908 next_capacity *= 2;
1909 }
1910 grown = (char *)realloc(*buffer, next_capacity);
1911 if (grown == NULL) {
1912 return 0;
1913 }
1914 *buffer = grown;
1915 *capacity = next_capacity;
1916 }
1917 memcpy(*buffer + *length, chunk, chunk_len);
1918 *length += chunk_len;
1919 (*buffer)[*length] = '\0';
1920 return 1;
1921}
1922
1924 const char *target_url,
1925 const char *content_type,
1926 const char *payload_json,
1928) {
1930 struct addrinfo hints;
1931 struct addrinfo *records = NULL;
1932 struct addrinfo *current = NULL;
1933 char port_text[16];
1934 char *request = NULL;
1935 size_t request_length = 0;
1936 int fd = -1;
1937 int connected = 0;
1938 char response_chunk[4096];
1939 char *response_text = NULL;
1940 size_t response_length = 0;
1941 size_t response_capacity = 0;
1942 if (result == NULL) {
1943 return 0;
1944 }
1945 memset(result, 0, sizeof(*result));
1946 if (!parse_http_url_logic_local(target_url, &parsed)) {
1947 result->error_message = strdup("unsupported_target_url");
1948 return 0;
1949 }
1950 snprintf(port_text, sizeof(port_text), "%d", parsed.port);
1951 memset(&hints, 0, sizeof(hints));
1952 hints.ai_family = AF_UNSPEC;
1953 hints.ai_socktype = SOCK_STREAM;
1954 if (getaddrinfo(parsed.host, port_text, &hints, &records) != 0) {
1956 result->error_message = strdup("resolve_failed");
1957 return 0;
1958 }
1959 for (current = records; current != NULL; current = current->ai_next) {
1960 fd = socket(current->ai_family, current->ai_socktype, current->ai_protocol);
1961 if (fd < 0) {
1962 continue;
1963 }
1964#ifndef _WIN32
1965 {
1966 struct timeval timeout;
1967 timeout.tv_sec = 10;
1968 timeout.tv_usec = 0;
1969 setsockopt(fd, SOL_SOCKET, SO_RCVTIMEO, &timeout, sizeof(timeout));
1970 setsockopt(fd, SOL_SOCKET, SO_SNDTIMEO, &timeout, sizeof(timeout));
1971 }
1972#endif
1973 if (connect(fd, current->ai_addr, current->ai_addrlen) == 0) {
1974 connected = 1;
1975 break;
1976 }
1977#ifndef _WIN32
1978 close(fd);
1979#else
1980 closesocket(fd);
1981#endif
1982 fd = -1;
1983 }
1984 freeaddrinfo(records);
1985 if (!connected || fd < 0) {
1987 result->error_message = strdup("connect_failed");
1988 return 0;
1989 }
1990 {
1991 size_t needed = strlen(parsed.path) + strlen(parsed.host) + strlen(content_type != NULL ? content_type : "application/json")
1992 + strlen(payload_json != NULL ? payload_json : "{}") + 256;
1993 request = (char *)malloc(needed);
1994 if (request == NULL) {
1995#ifndef _WIN32
1996 close(fd);
1997#else
1998 closesocket(fd);
1999#endif
2001 result->error_message = strdup("allocation_failed");
2002 return 0;
2003 }
2004 request_length = (size_t)snprintf(
2005 request,
2006 needed,
2007 "POST %s HTTP/1.1\r\nHost: %s\r\nContent-Type: %s\r\nContent-Length: %zu\r\nConnection: close\r\n\r\n%s",
2008 parsed.path,
2009 parsed.host,
2010 content_type != NULL ? content_type : "application/json",
2011 strlen(payload_json != NULL ? payload_json : "{}"),
2012 payload_json != NULL ? payload_json : "{}"
2013 );
2014 }
2015 if (request == NULL) {
2016#ifndef _WIN32
2017 close(fd);
2018#else
2019 closesocket(fd);
2020#endif
2022 result->error_message = strdup("request_build_failed");
2023 return 0;
2024 }
2025 {
2026 size_t sent = 0;
2027 while (sent < request_length) {
2028 ssize_t wrote = send(fd, request + sent, request_length - sent, 0);
2029 if (wrote <= 0) {
2030 free(request);
2031#ifndef _WIN32
2032 close(fd);
2033#else
2034 closesocket(fd);
2035#endif
2037 free(response_text);
2038 result->error_message = strdup("send_failed");
2039 return 0;
2040 }
2041 sent += (size_t)wrote;
2042 }
2043 }
2044 free(request);
2045 while (1) {
2046 ssize_t read_count = recv(fd, response_chunk, sizeof(response_chunk), 0);
2047 if (read_count == 0) {
2048 break;
2049 }
2050 if (read_count < 0) {
2051 free(response_text);
2052#ifndef _WIN32
2053 close(fd);
2054#else
2055 closesocket(fd);
2056#endif
2058 result->error_message = strdup("recv_failed");
2059 return 0;
2060 }
2061 if (!append_bytes_logic_local(&response_text, &response_length, &response_capacity, response_chunk, (size_t)read_count)) {
2062 free(response_text);
2063#ifndef _WIN32
2064 close(fd);
2065#else
2066 closesocket(fd);
2067#endif
2069 result->error_message = strdup("response_buffer_failed");
2070 return 0;
2071 }
2072 }
2073#ifndef _WIN32
2074 close(fd);
2075#else
2076 closesocket(fd);
2077#endif
2079 if (response_text != NULL) {
2080 char *body_start = strstr(response_text, "\r\n\r\n");
2081 char *status_line_end = strstr(response_text, "\r\n");
2082 if (status_line_end != NULL) {
2083 char *first_space = strchr(response_text, ' ');
2084 if (first_space != NULL) {
2085 result->http_status = strtol(first_space + 1, NULL, 10);
2086 }
2087 }
2088 if (body_start != NULL) {
2089 result->response_body = strdup(body_start + 4);
2090 } else {
2091 result->response_body = strdup("");
2092 }
2093 }
2094 result->ok = result->http_status >= 200 && result->http_status < 300;
2095 if (!result->ok && result->error_message == NULL) {
2096 char buffer[64];
2097 snprintf(buffer, sizeof(buffer), "http_%ld", result->http_status);
2098 result->error_message = strdup(buffer);
2099 }
2100 free(response_text);
2101 return result->ok ? 1 : 0;
2102}
2103
2105 const char *action_id,
2106 const char *adapter_id,
2107 const char *resource_id,
2108 const char *replay_trace_id,
2109 const char *created_at
2110) {
2112 yyjson_mut_val *root = yyjson_mut_obj(doc);
2113 char *result = NULL;
2114 if (doc == NULL || root == NULL) {
2116 return NULL;
2117 }
2118 if (!yyjson_mut_obj_add_strcpy(doc, root, "action_id", action_id != NULL ? action_id : "")
2119 || !yyjson_mut_obj_add_strcpy(doc, root, "adapter_id", adapter_id != NULL ? adapter_id : "")
2120 || !yyjson_mut_obj_add_strcpy(doc, root, "resource_id", resource_id != NULL ? resource_id : "")
2121 || !yyjson_mut_obj_add_strcpy(doc, root, "replay_trace_id", replay_trace_id != NULL ? replay_trace_id : "")
2122 || !yyjson_mut_obj_add_strcpy(doc, root, "created_at", created_at != NULL ? created_at : "")) {
2124 return NULL;
2125 }
2126 yyjson_mut_doc_set_root(doc, root);
2127 result = yyjson_mut_write(doc, YYJSON_WRITE_NOFLAG, NULL);
2129 return result;
2130}
2131
2133 yyjson_mut_doc *mut_doc,
2134 yyjson_mut_val *mut_root,
2135 long job_execution_id,
2136 const char *execution_status,
2137 const char *payload_json,
2138 const char *created_at,
2139 const char *error_message
2140) {
2141 yyjson_mut_val *attempts = ensure_mut_array_field_logic_local(mut_doc, mut_root, "job_execution_attempts");
2142 yyjson_mut_val *attempt = yyjson_mut_obj(mut_doc);
2143 long attempt_id = next_section_id_logic_local(mut_root, "job_execution_attempts");
2144 yyjson_doc *payload_doc = payload_json != NULL ? native_json_doc_load_text(payload_json) : NULL;
2145 yyjson_val *payload_root = payload_doc != NULL ? yyjson_doc_get_root(payload_doc) : NULL;
2146 int ok = attempts != NULL && attempt != NULL
2147 && yyjson_mut_obj_add_int(mut_doc, attempt, "id", attempt_id)
2148 && yyjson_mut_obj_add_int(mut_doc, attempt, "job_execution_id", job_execution_id)
2149 && yyjson_mut_obj_add_int(mut_doc, attempt, "attempt_number", 1)
2150 && yyjson_mut_obj_add_strcpy(mut_doc, attempt, "execution_status", execution_status != NULL ? execution_status : "succeeded")
2151 && yyjson_mut_obj_add_strcpy(mut_doc, attempt, "error_message", error_message != NULL ? error_message : "")
2152 && yyjson_mut_obj_add_strcpy(mut_doc, attempt, "started_at", created_at != NULL ? created_at : "")
2153 && yyjson_mut_obj_add_strcpy(mut_doc, attempt, "finished_at", created_at != NULL ? created_at : "")
2154 && (payload_root == NULL || yyjson_mut_obj_put(attempt, yyjson_mut_strcpy(mut_doc, "payload_json"), yyjson_val_mut_copy(mut_doc, payload_root)))
2155 && yyjson_mut_arr_append(attempts, attempt);
2156 native_json_doc_free(payload_doc);
2157 return ok;
2158}
2159
2161 yyjson_mut_doc *mut_doc,
2162 yyjson_mut_val *mut_root,
2163 yyjson_mut_val *delivery,
2164 const char *created_at,
2165 const char *payload_json
2166) {
2167 yyjson_mut_val *attempts = ensure_mut_array_field_logic_local(mut_doc, mut_root, "integration_delivery_attempts");
2168 yyjson_mut_val *attempt = yyjson_mut_obj(mut_doc);
2169 long attempt_id = next_section_id_logic_local(mut_root, "integration_delivery_attempts");
2170 char next_attempt_at[20];
2171 long delivery_id = 0;
2172 long attempt_count = 0;
2173 long max_attempts = 0;
2174 long backoff_seconds = 0;
2175 char *target_url = NULL;
2176 char *delivery_status = NULL;
2177 char *error_message = NULL;
2179 memset(&result, 0, sizeof(result));
2180 if (delivery == NULL || attempts == NULL || attempt == NULL) {
2181 return 0;
2182 }
2183 delivery_id = (long)yyjson_get_sint((yyjson_val *)yyjson_mut_obj_get(delivery, "id"));
2184 attempt_count = native_json_obj_get_long_default((yyjson_val *)delivery, "attempt_count", 0, NULL) + 1;
2185 max_attempts = native_json_obj_get_long_default((yyjson_val *)delivery, "max_attempts", 1, NULL);
2186 backoff_seconds = native_json_obj_get_long_default((yyjson_val *)delivery, "backoff_seconds", 5, NULL);
2187 target_url = native_json_obj_get_string_dup((yyjson_val *)delivery, "target_url");
2188 if (target_url != NULL && outbound_http_post_logic_local(target_url, "application/json", payload_json, &result)) {
2189 delivery_status = strdup("succeeded");
2190 } else if (attempt_count < max_attempts) {
2191 delivery_status = strdup("retryable");
2192 } else {
2193 delivery_status = strdup("failed");
2194 }
2195 error_message = result.error_message != NULL ? strdup(result.error_message) : strdup("");
2196 if (!now_plus_seconds_iso_utc_logic_local(backoff_seconds, next_attempt_at)) {
2197 snprintf(next_attempt_at, sizeof(next_attempt_at), "%s", created_at != NULL ? created_at : "");
2198 }
2199 if (delivery_status == NULL || error_message == NULL
2200 || !yyjson_mut_obj_add_int(mut_doc, attempt, "id", attempt_id)
2201 || !yyjson_mut_obj_add_int(mut_doc, attempt, "delivery_id", delivery_id)
2202 || !yyjson_mut_obj_add_strcpy(mut_doc, attempt, "attempt_status", result.ok ? "succeeded" : "failed")
2203 || !yyjson_mut_obj_add_int(mut_doc, attempt, "http_status", result.http_status)
2204 || !yyjson_mut_obj_add_strcpy(mut_doc, attempt, "error_message", error_message)
2205 || !yyjson_mut_obj_add_strcpy(mut_doc, attempt, "response_body", result.response_body != NULL ? result.response_body : "")
2206 || !yyjson_mut_obj_add_strcpy(mut_doc, attempt, "started_at", created_at != NULL ? created_at : "")
2207 || !yyjson_mut_obj_add_strcpy(mut_doc, attempt, "finished_at", created_at != NULL ? created_at : "")
2208 || !yyjson_mut_arr_append(attempts, attempt)
2209 || !set_mut_object_field_logic_local(mut_doc, delivery, "delivery_status", yyjson_mut_strcpy(mut_doc, delivery_status))
2210 || !set_mut_object_field_logic_local(mut_doc, delivery, "attempt_count", yyjson_mut_int(mut_doc, attempt_count))
2211 || !set_mut_object_field_logic_local(mut_doc, delivery, "last_http_status", yyjson_mut_int(mut_doc, result.http_status))
2212 || !set_mut_object_field_logic_local(mut_doc, delivery, "last_error", yyjson_mut_strcpy(mut_doc, error_message))
2213 || !set_mut_object_field_logic_local(mut_doc, delivery, "updated_at", yyjson_mut_strcpy(mut_doc, created_at != NULL ? created_at : ""))
2214 || !set_mut_object_field_logic_local(mut_doc, delivery, "next_attempt_at", yyjson_mut_strcpy(mut_doc, streq(delivery_status, "retryable") ? next_attempt_at : ""))
2215 || !set_mut_object_field_logic_local(mut_doc, delivery, "completed_at", yyjson_mut_strcpy(mut_doc, streq(delivery_status, "succeeded") ? (created_at != NULL ? created_at : "") : ""))) {
2216 free(target_url);
2217 free(delivery_status);
2218 free(error_message);
2220 return 0;
2221 }
2222 free(target_url);
2223 free(delivery_status);
2224 free(error_message);
2226 return 1;
2227}
2228
2230 yyjson_mut_doc *mut_doc,
2231 yyjson_mut_val *mut_root,
2232 const char *created_at,
2233 const char *payload_json
2234) {
2235 yyjson_mut_val *deliveries = ensure_mut_array_field_logic_local(mut_doc, mut_root, "integration_deliveries");
2236 size_t index = 0;
2237 size_t max = 0;
2238 yyjson_mut_val *delivery = NULL;
2239 if (deliveries == NULL) {
2240 return 0;
2241 }
2242 yyjson_mut_arr_foreach(deliveries, index, max, delivery) {
2243 char *status = native_json_obj_get_string_dup((yyjson_val *)delivery, "delivery_status");
2244 char *next_attempt_at = native_json_obj_get_string_dup((yyjson_val *)delivery, "next_attempt_at");
2245 int due = 0;
2246 if (status != NULL && (streq(status, "queued") || streq(status, "retryable"))) {
2247 if (next_attempt_at == NULL || next_attempt_at[0] == '\0' || created_at == NULL || strcmp(next_attempt_at, created_at) <= 0) {
2248 due = 1;
2249 }
2250 }
2251 free(status);
2252 free(next_attempt_at);
2253 if (!due) {
2254 continue;
2255 }
2256 if (!process_webhook_delivery_logic_local(mut_doc, mut_root, delivery, created_at, payload_json)) {
2257 return 0;
2258 }
2259 }
2260 return 1;
2261}
2262
2264 yyjson_mut_doc *mut_doc,
2265 yyjson_mut_val *mut_root,
2266 yyjson_val *action_contract,
2267 yyjson_val *workflow,
2268 yyjson_val *transition,
2269 const ActionExecutionContextLogicLocal *context,
2270 const ActionEffectCountsLogicLocal *effect_counts,
2271 const char *resource_id,
2272 const char *from_state,
2273 const char *to_state,
2274 const char *created_at,
2275 const char *policy_gate,
2276 const char *success_message,
2277 const char *operator_summary,
2278 const char *transition_label,
2279 const char *from_state_label,
2280 const char *to_state_label,
2281 long workflow_execution_id,
2282 long created_record_id,
2283 char **report_json_out,
2284 char **replay_trace_id_out
2285) {
2286 yyjson_mut_val *action_reports = NULL;
2287 yyjson_mut_val *report = NULL;
2288 char *action_id = native_json_obj_get_string_dup(action_contract, "action_id");
2289 char *workflow_id = workflow != NULL ? native_json_obj_get_string_dup(workflow, "workflow_id") : NULL;
2290 char *transition_id = transition != NULL ? native_json_obj_get_string_dup(transition, "transition_id") : NULL;
2291 char *event_name = transition != NULL ? native_json_obj_get_string_dup(transition, "emits_event") : NULL;
2292 char replay_trace_buffer[256];
2293 if (report_json_out != NULL) {
2294 *report_json_out = NULL;
2295 }
2296 if (replay_trace_id_out != NULL) {
2297 *replay_trace_id_out = NULL;
2298 }
2299 action_reports = ensure_mut_array_field_logic_local(mut_doc, mut_root, "action_reports");
2300 report = yyjson_mut_obj(mut_doc);
2301 if (action_reports == NULL || report == NULL || action_id == NULL) {
2302 free(action_id);
2303 free(workflow_id);
2304 free(transition_id);
2305 free(event_name);
2306 return 0;
2307 }
2308 snprintf(replay_trace_buffer, sizeof(replay_trace_buffer), "trace:%s:%s:%ld", action_id, resource_id != NULL ? resource_id : "resource", workflow_execution_id);
2309 if (replay_trace_id_out != NULL) {
2310 *replay_trace_id_out = strdup(replay_trace_buffer);
2311 }
2312 if (!yyjson_mut_obj_add_strcpy(mut_doc, report, "report_id", "pharos-action-execution-v2")
2313 || !yyjson_mut_obj_add_strcpy(mut_doc, report, "action_id", action_id)
2314 || !yyjson_mut_obj_add_strcpy(mut_doc, report, "policy_gate", policy_gate != NULL ? policy_gate : "public")
2315 || !yyjson_mut_obj_add_strcpy(mut_doc, report, "resource_id", resource_id != NULL ? resource_id : "")
2316 || !yyjson_mut_obj_add_strcpy(mut_doc, report, "from_state", from_state != NULL ? from_state : "none")
2317 || !yyjson_mut_obj_add_strcpy(mut_doc, report, "to_state", to_state != NULL ? to_state : "applied")
2318 || !yyjson_mut_obj_add_int(mut_doc, report, "workflow_execution_id", workflow_execution_id)
2319 || !yyjson_mut_obj_add_int(mut_doc, report, "created_record_id", created_record_id)
2320 || !yyjson_mut_obj_add_strcpy(mut_doc, report, "created_at", created_at != NULL ? created_at : "")
2321 || !yyjson_mut_obj_add_strcpy(mut_doc, report, "replay_trace_id", replay_trace_buffer)
2322 || !yyjson_mut_obj_add_strcpy(mut_doc, report, "workflow_id", workflow_id != NULL ? workflow_id : "")
2323 || !yyjson_mut_obj_add_strcpy(mut_doc, report, "transition_id", transition_id != NULL ? transition_id : "")
2324 || !yyjson_mut_obj_add_strcpy(mut_doc, report, "event_name", event_name != NULL ? event_name : "")
2325 || !yyjson_mut_obj_add_int(mut_doc, report, "anchor_record_id", context != NULL ? context->anchor_record_id : 0)
2326 || !yyjson_mut_obj_add_int(mut_doc, report, "last_created_record_id", context != NULL ? context->last_created_record_id : 0)
2327 || !yyjson_mut_obj_add_int(mut_doc, report, "last_touched_record_id", context != NULL ? context->last_touched_record_id : 0)
2328 || !yyjson_mut_obj_add_int(mut_doc, report, "created_record_count", context != NULL ? context->created_record_count : 0)
2329 || !yyjson_mut_obj_add_int(mut_doc, report, "updated_record_count", context != NULL ? context->updated_record_count : 0)
2330 || !yyjson_mut_obj_add_int(mut_doc, report, "matched_record_count", context != NULL ? context->matched_record_count : 0)
2331 || !yyjson_mut_obj_add_int(mut_doc, report, "skipped_record_count", context != NULL ? context->skipped_record_count : 0)
2332 || !yyjson_mut_obj_add_strcpy(mut_doc, report, "overflow_policy", context != NULL && context->overflow_policy != NULL ? context->overflow_policy : "")
2333 || !yyjson_mut_obj_add_strcpy(mut_doc, report, "operator_message", success_message != NULL ? success_message : "")
2334 || !yyjson_mut_obj_add_strcpy(mut_doc, report, "operator_summary", operator_summary != NULL ? operator_summary : "")
2335 || !yyjson_mut_obj_add_strcpy(mut_doc, report, "transition_label", transition_label != NULL ? transition_label : "")
2336 || !yyjson_mut_obj_add_strcpy(mut_doc, report, "from_state_label", from_state_label != NULL ? from_state_label : (from_state != NULL ? from_state : "none"))
2337 || !yyjson_mut_obj_add_strcpy(mut_doc, report, "to_state_label", to_state_label != NULL ? to_state_label : (to_state != NULL ? to_state : "applied"))
2338 || !yyjson_mut_obj_add_int(mut_doc, report, "queued_job_count", effect_counts != NULL ? effect_counts->job_count : 0)
2339 || !yyjson_mut_obj_add_int(mut_doc, report, "queued_webhook_count", effect_counts != NULL ? effect_counts->webhook_count : 0)
2340 || !yyjson_mut_obj_add_int(mut_doc, report, "queued_notification_count", effect_counts != NULL ? effect_counts->notification_count : 0)
2341 || !yyjson_mut_arr_append(action_reports, report)) {
2342 free(action_id);
2343 free(workflow_id);
2344 free(transition_id);
2345 free(event_name);
2346 return 0;
2347 }
2348 {
2349 yyjson_mut_val *summary = yyjson_mut_obj(mut_doc);
2350 if (summary == NULL
2351 || !yyjson_mut_obj_add_strcpy(mut_doc, summary, "action_id", action_id)
2352 || !yyjson_mut_obj_add_strcpy(mut_doc, summary, "policy_gate", policy_gate != NULL ? policy_gate : "public")
2353 || !yyjson_mut_obj_add_strcpy(mut_doc, summary, "resource_id", resource_id != NULL ? resource_id : "")
2354 || !yyjson_mut_obj_add_strcpy(mut_doc, summary, "from_state", from_state != NULL ? from_state : "none")
2355 || !yyjson_mut_obj_add_strcpy(mut_doc, summary, "to_state", to_state != NULL ? to_state : "applied")
2356 || !yyjson_mut_obj_add_int(mut_doc, summary, "workflow_execution_id", workflow_execution_id)
2357 || !yyjson_mut_obj_add_int(mut_doc, summary, "created_record_id", created_record_id)
2358 || !yyjson_mut_obj_add_strcpy(mut_doc, summary, "created_at", created_at != NULL ? created_at : "")
2359 || !yyjson_mut_obj_add_strcpy(mut_doc, summary, "replay_trace_id", replay_trace_buffer)
2360 || !yyjson_mut_obj_add_strcpy(mut_doc, summary, "workflow_id", workflow_id != NULL ? workflow_id : "")
2361 || !yyjson_mut_obj_add_strcpy(mut_doc, summary, "transition_id", transition_id != NULL ? transition_id : "")
2362 || !yyjson_mut_obj_add_strcpy(mut_doc, summary, "event_name", event_name != NULL ? event_name : "")
2363 || !yyjson_mut_obj_add_int(mut_doc, summary, "anchor_record_id", context != NULL ? context->anchor_record_id : 0)
2364 || !yyjson_mut_obj_add_int(mut_doc, summary, "last_created_record_id", context != NULL ? context->last_created_record_id : 0)
2365 || !yyjson_mut_obj_add_int(mut_doc, summary, "last_touched_record_id", context != NULL ? context->last_touched_record_id : 0)
2366 || !yyjson_mut_obj_add_int(mut_doc, summary, "created_record_count", context != NULL ? context->created_record_count : 0)
2367 || !yyjson_mut_obj_add_int(mut_doc, summary, "updated_record_count", context != NULL ? context->updated_record_count : 0)
2368 || !yyjson_mut_obj_add_int(mut_doc, summary, "matched_record_count", context != NULL ? context->matched_record_count : 0)
2369 || !yyjson_mut_obj_add_int(mut_doc, summary, "skipped_record_count", context != NULL ? context->skipped_record_count : 0)
2370 || !yyjson_mut_obj_add_strcpy(mut_doc, summary, "overflow_policy", context != NULL && context->overflow_policy != NULL ? context->overflow_policy : "")
2371 || !yyjson_mut_obj_add_strcpy(mut_doc, summary, "operator_message", success_message != NULL ? success_message : "")
2372 || !yyjson_mut_obj_add_strcpy(mut_doc, summary, "operator_summary", operator_summary != NULL ? operator_summary : "")
2373 || !yyjson_mut_obj_add_strcpy(mut_doc, summary, "transition_label", transition_label != NULL ? transition_label : "")
2374 || !yyjson_mut_obj_add_strcpy(mut_doc, summary, "from_state_label", from_state_label != NULL ? from_state_label : (from_state != NULL ? from_state : "none"))
2375 || !yyjson_mut_obj_add_strcpy(mut_doc, summary, "to_state_label", to_state_label != NULL ? to_state_label : (to_state != NULL ? to_state : "applied"))
2376 || !yyjson_mut_obj_add_int(mut_doc, summary, "queued_job_count", effect_counts != NULL ? effect_counts->job_count : 0)
2377 || !yyjson_mut_obj_add_int(mut_doc, summary, "queued_webhook_count", effect_counts != NULL ? effect_counts->webhook_count : 0)
2378 || !yyjson_mut_obj_add_int(mut_doc, summary, "queued_notification_count", effect_counts != NULL ? effect_counts->notification_count : 0)
2379 || !set_mut_object_field_logic_local(mut_doc, mut_root, "last_action_report", summary)) {
2380 free(action_id);
2381 free(workflow_id);
2382 free(transition_id);
2383 free(event_name);
2384 return 0;
2385 }
2386 if (report_json_out != NULL) {
2387 size_t needed = strlen(action_id) + strlen(policy_gate != NULL ? policy_gate : "public")
2388 + strlen(resource_id != NULL ? resource_id : "") + strlen(from_state != NULL ? from_state : "none")
2389 + strlen(to_state != NULL ? to_state : "applied") + strlen(replay_trace_buffer)
2390 + strlen(workflow_id != NULL ? workflow_id : "") + strlen(transition_id != NULL ? transition_id : "")
2391 + strlen(event_name != NULL ? event_name : "") + strlen(created_at != NULL ? created_at : "")
2392 + strlen(success_message != NULL ? success_message : "") + strlen(operator_summary != NULL ? operator_summary : "")
2393 + strlen(transition_label != NULL ? transition_label : "")
2394 + strlen(from_state_label != NULL ? from_state_label : (from_state != NULL ? from_state : "none"))
2395 + strlen(to_state_label != NULL ? to_state_label : (to_state != NULL ? to_state : "applied")) + 896;
2396 *report_json_out = (char *)malloc(needed);
2397 if (*report_json_out == NULL) {
2398 free(action_id);
2399 free(workflow_id);
2400 free(transition_id);
2401 free(event_name);
2402 return 0;
2403 }
2404 snprintf(
2405 *report_json_out,
2406 needed,
2407 "{\"action_id\":\"%s\",\"policy_gate\":\"%s\",\"resource_id\":\"%s\",\"from_state\":\"%s\",\"to_state\":\"%s\",\"workflow_execution_id\":%ld,\"created_record_id\":%ld,\"created_at\":\"%s\",\"replay_trace_id\":\"%s\",\"workflow_id\":\"%s\",\"transition_id\":\"%s\",\"event_name\":\"%s\",\"anchor_record_id\":%ld,\"last_created_record_id\":%ld,\"last_touched_record_id\":%ld,\"created_record_count\":%ld,\"updated_record_count\":%ld,\"matched_record_count\":%ld,\"skipped_record_count\":%ld,\"overflow_policy\":\"%s\",\"operator_message\":\"%s\",\"operator_summary\":\"%s\",\"transition_label\":\"%s\",\"from_state_label\":\"%s\",\"to_state_label\":\"%s\",\"queued_job_count\":%ld,\"queued_webhook_count\":%ld,\"queued_notification_count\":%ld}",
2408 action_id,
2409 policy_gate != NULL ? policy_gate : "public",
2410 resource_id != NULL ? resource_id : "",
2411 from_state != NULL ? from_state : "none",
2412 to_state != NULL ? to_state : "applied",
2413 workflow_execution_id,
2414 created_record_id,
2415 created_at != NULL ? created_at : "",
2416 replay_trace_buffer,
2417 workflow_id != NULL ? workflow_id : "",
2418 transition_id != NULL ? transition_id : "",
2419 event_name != NULL ? event_name : "",
2420 context != NULL ? context->anchor_record_id : 0,
2421 context != NULL ? context->last_created_record_id : 0,
2422 context != NULL ? context->last_touched_record_id : 0,
2423 context != NULL ? context->created_record_count : 0,
2424 context != NULL ? context->updated_record_count : 0,
2425 context != NULL ? context->matched_record_count : 0,
2426 context != NULL ? context->skipped_record_count : 0,
2427 context != NULL && context->overflow_policy != NULL ? context->overflow_policy : "",
2428 success_message != NULL ? success_message : "",
2429 operator_summary != NULL ? operator_summary : "",
2430 transition_label != NULL ? transition_label : "",
2431 from_state_label != NULL ? from_state_label : (from_state != NULL ? from_state : "none"),
2432 to_state_label != NULL ? to_state_label : (to_state != NULL ? to_state : "applied"),
2433 effect_counts != NULL ? effect_counts->job_count : 0,
2434 effect_counts != NULL ? effect_counts->webhook_count : 0,
2435 effect_counts != NULL ? effect_counts->notification_count : 0
2436 );
2437 }
2438 }
2439 free(action_id);
2440 free(workflow_id);
2441 free(transition_id);
2442 free(event_name);
2443 return 1;
2444}
2445
2447 const AppRuntime *app,
2448 yyjson_mut_doc *mut_doc,
2449 yyjson_mut_val *mut_root,
2450 yyjson_val *action_contract,
2451 yyjson_val *workflow,
2452 yyjson_val *transition,
2453 const InputFieldMapLogicLocal *inputs,
2454 const ActionExecutionContextLogicLocal *context,
2455 const char *policy_gate,
2456 const char *resource_id,
2457 const char *from_state,
2458 const char *to_state,
2459 const char *created_at,
2460 const char *replay_trace_id,
2461 long *workflow_execution_id_out,
2462 ActionEffectCountsLogicLocal *effect_counts_out
2463) {
2464 yyjson_val *effect_hooks = native_json_obj_get(action_contract, "effect_hooks");
2465 yyjson_val *jobs = effect_hooks != NULL ? native_json_obj_get(effect_hooks, "scheduled_jobs") : NULL;
2466 yyjson_val *webhooks = effect_hooks != NULL ? native_json_obj_get(effect_hooks, "webhook_dispatch") : NULL;
2467 yyjson_val *notifications = effect_hooks != NULL ? native_json_obj_get(effect_hooks, "notifications") : NULL;
2468 yyjson_mut_val *workflow_executions = ensure_mut_array_field_logic_local(mut_doc, mut_root, "workflow_executions");
2469 yyjson_mut_val *workflow_events = ensure_mut_array_field_logic_local(mut_doc, mut_root, "workflow_execution_events");
2470 yyjson_mut_val *job_executions = ensure_mut_array_field_logic_local(mut_doc, mut_root, "job_executions");
2471 yyjson_mut_val *job_execution_attempts = ensure_mut_array_field_logic_local(mut_doc, mut_root, "job_execution_attempts");
2472 yyjson_mut_val *webhook_receipts = ensure_mut_array_field_logic_local(mut_doc, mut_root, "webhook_receipts");
2473 yyjson_mut_val *notification_deliveries = ensure_mut_array_field_logic_local(mut_doc, mut_root, "notification_deliveries");
2474 yyjson_mut_val *integration_deliveries = ensure_mut_array_field_logic_local(mut_doc, mut_root, "integration_deliveries");
2475 yyjson_mut_val *integration_delivery_attempts = ensure_mut_array_field_logic_local(mut_doc, mut_root, "integration_delivery_attempts");
2476 yyjson_mut_val *execution = yyjson_mut_obj(mut_doc);
2477 yyjson_mut_val *event = yyjson_mut_obj(mut_doc);
2478 yyjson_arr_iter iter;
2479 yyjson_val *item = NULL;
2480 yyjson_doc *integration_doc = load_fixture_doc_logic_local(app, "integration_fixture");
2481 yyjson_val *integration_root = integration_doc != NULL ? yyjson_doc_get_root(integration_doc) : NULL;
2482 long workflow_execution_id = next_section_id_logic_local(mut_root, "workflow_executions");
2483 long workflow_event_id = next_section_id_logic_local(mut_root, "workflow_execution_events");
2484 long job_id = next_section_id_logic_local(mut_root, "job_executions");
2485 long webhook_id = next_section_id_logic_local(mut_root, "webhook_receipts");
2486 long notification_id = next_section_id_logic_local(mut_root, "notification_deliveries");
2487 long queued_jobs = jobs != NULL && yyjson_is_arr(jobs) ? (long)yyjson_arr_size(jobs) : 0;
2488 long queued_webhooks = webhooks != NULL && yyjson_is_arr(webhooks) ? (long)yyjson_arr_size(webhooks) : 0;
2489 long queued_notifications = notifications != NULL && yyjson_is_arr(notifications) ? (long)yyjson_arr_size(notifications) : 0;
2490 char *workflow_id = workflow != NULL ? native_json_obj_get_string_dup(workflow, "workflow_id") : NULL;
2491 char *transition_id = transition != NULL ? native_json_obj_get_string_dup(transition, "transition_id") : NULL;
2492 char *event_name = transition != NULL ? native_json_obj_get_string_dup(transition, "emits_event") : NULL;
2493 (void)job_execution_attempts;
2494 (void)integration_delivery_attempts;
2495 if (workflow_execution_id_out != NULL) {
2496 *workflow_execution_id_out = 0;
2497 }
2498 if (effect_counts_out != NULL) {
2499 memset(effect_counts_out, 0, sizeof(*effect_counts_out));
2500 }
2501 if (workflow_executions == NULL || workflow_events == NULL || job_executions == NULL || job_execution_attempts == NULL || webhook_receipts == NULL || notification_deliveries == NULL || integration_deliveries == NULL || integration_delivery_attempts == NULL || execution == NULL || event == NULL) {
2502 free(workflow_id);
2503 free(transition_id);
2504 free(event_name);
2505 native_json_doc_free(integration_doc);
2506 return 0;
2507 }
2508 if (!yyjson_mut_obj_add_int(mut_doc, execution, "id", workflow_execution_id)
2509 || !yyjson_mut_obj_add_strcpy(mut_doc, execution, "workflow_id", workflow_id != NULL ? workflow_id : "framework_action")
2510 || !yyjson_mut_obj_add_strcpy(mut_doc, execution, "transition_id", transition_id != NULL ? transition_id : "action_applied")
2511 || !yyjson_mut_obj_add_strcpy(mut_doc, execution, "subject_id", "anonymous_or_session")
2512 || !yyjson_mut_obj_add_strcpy(mut_doc, execution, "resource_id", resource_id != NULL ? resource_id : "")
2513 || !yyjson_mut_obj_add_strcpy(mut_doc, execution, "from_state", from_state != NULL ? from_state : "none")
2514 || !yyjson_mut_obj_add_strcpy(mut_doc, execution, "to_state", to_state != NULL ? to_state : "applied")
2515 || !yyjson_mut_obj_add_strcpy(mut_doc, execution, "idempotency_key", replay_trace_id != NULL ? replay_trace_id : "")
2516 || !yyjson_mut_obj_add_strcpy(mut_doc, execution, "replay_trace_id", replay_trace_id != NULL ? replay_trace_id : "")
2517 || !yyjson_mut_obj_add_strcpy(mut_doc, execution, "policy_gate", policy_gate != NULL ? policy_gate : "public")
2518 || !yyjson_mut_obj_add_strcpy(mut_doc, execution, "execution_status", "applied")
2519 || !yyjson_mut_obj_add_int(mut_doc, execution, "executed_by_user_id", 0)
2520 || !yyjson_mut_obj_add_int(mut_doc, execution, "queued_job_count", queued_jobs)
2521 || !yyjson_mut_obj_add_int(mut_doc, execution, "queued_webhook_count", queued_webhooks)
2522 || !yyjson_mut_obj_add_int(mut_doc, execution, "queued_notification_count", queued_notifications)
2523 || !yyjson_mut_obj_add_strcpy(mut_doc, execution, "created_at", created_at != NULL ? created_at : "")
2524 || !yyjson_mut_obj_add_strcpy(mut_doc, execution, "completed_at", created_at != NULL ? created_at : "")
2525 || !yyjson_mut_arr_append(workflow_executions, execution)) {
2526 free(workflow_id);
2527 free(transition_id);
2528 free(event_name);
2529 return 0;
2530 }
2531 if (!yyjson_mut_obj_add_int(mut_doc, event, "id", workflow_event_id)
2532 || !yyjson_mut_obj_add_int(mut_doc, event, "workflow_execution_id", workflow_execution_id)
2533 || !yyjson_mut_obj_add_strcpy(mut_doc, event, "event_kind", "transition_applied")
2534 || !yyjson_mut_obj_add_strcpy(mut_doc, event, "event_key", transition_id != NULL ? transition_id : "action_applied")
2535 || !yyjson_mut_obj_add_strcpy(mut_doc, event, "created_at", created_at != NULL ? created_at : "")) {
2536 free(workflow_id);
2537 free(transition_id);
2538 free(event_name);
2539 return 0;
2540 }
2541 {
2542 yyjson_mut_val *payload = yyjson_mut_obj(mut_doc);
2543 if (payload == NULL
2544 || !yyjson_mut_obj_add_strcpy(mut_doc, payload, "workflow_id", workflow_id != NULL ? workflow_id : "framework_action")
2545 || !yyjson_mut_obj_add_strcpy(mut_doc, payload, "transition_id", transition_id != NULL ? transition_id : "action_applied")
2546 || !yyjson_mut_obj_add_strcpy(mut_doc, payload, "resource_id", resource_id != NULL ? resource_id : "")
2547 || !yyjson_mut_obj_add_strcpy(mut_doc, payload, "from_state", from_state != NULL ? from_state : "none")
2548 || !yyjson_mut_obj_add_strcpy(mut_doc, payload, "to_state", to_state != NULL ? to_state : "applied")
2549 || !yyjson_mut_obj_add_strcpy(mut_doc, payload, "replay_trace_id", replay_trace_id != NULL ? replay_trace_id : "")
2550 || !yyjson_mut_obj_put(event, yyjson_mut_strcpy(mut_doc, "payload_json"), payload)
2551 || !yyjson_mut_arr_append(workflow_events, event)) {
2552 free(workflow_id);
2553 free(transition_id);
2554 free(event_name);
2555 return 0;
2556 }
2557 }
2558 if (event_name != NULL && event_name[0] != '\0') {
2559 yyjson_mut_val *emitted = yyjson_mut_obj(mut_doc);
2560 yyjson_mut_val *payload = yyjson_mut_obj(mut_doc);
2561 if (emitted == NULL || payload == NULL
2562 || !yyjson_mut_obj_add_int(mut_doc, emitted, "id", workflow_event_id + 1)
2563 || !yyjson_mut_obj_add_int(mut_doc, emitted, "workflow_execution_id", workflow_execution_id)
2564 || !yyjson_mut_obj_add_strcpy(mut_doc, emitted, "event_kind", "event_emitted")
2565 || !yyjson_mut_obj_add_strcpy(mut_doc, emitted, "event_key", event_name)
2566 || !yyjson_mut_obj_add_strcpy(mut_doc, emitted, "created_at", created_at != NULL ? created_at : "")
2567 || !yyjson_mut_obj_add_strcpy(mut_doc, payload, "event", event_name)
2568 || !yyjson_mut_obj_add_strcpy(mut_doc, payload, "resource_id", resource_id != NULL ? resource_id : "")
2569 || !yyjson_mut_obj_add_strcpy(mut_doc, payload, "replay_trace_id", replay_trace_id != NULL ? replay_trace_id : "")
2570 || !yyjson_mut_obj_put(emitted, yyjson_mut_strcpy(mut_doc, "payload_json"), payload)
2571 || !yyjson_mut_arr_append(workflow_events, emitted)) {
2572 free(workflow_id);
2573 free(transition_id);
2574 free(event_name);
2575 return 0;
2576 }
2577 workflow_event_id++;
2578 }
2579 if (jobs != NULL && yyjson_is_arr(jobs) && yyjson_arr_iter_init(jobs, &iter)) {
2580 while ((item = yyjson_arr_iter_next(&iter)) != NULL) {
2581 const char *job_name = yyjson_is_str(item) ? yyjson_get_str(item) : "job";
2582 yyjson_mut_val *job = yyjson_mut_obj(mut_doc);
2583 yyjson_mut_val *job_event = yyjson_mut_obj(mut_doc);
2584 yyjson_mut_val *job_payload = yyjson_mut_obj(mut_doc);
2585 if (job == NULL || job_event == NULL || job_payload == NULL
2586 || !yyjson_mut_obj_add_int(mut_doc, job, "id", job_id)
2587 || !yyjson_mut_obj_add_int(mut_doc, job, "workflow_execution_id", workflow_execution_id)
2588 || !yyjson_mut_obj_add_strcpy(mut_doc, job, "queue_name", "default")
2589 || !yyjson_mut_obj_add_strcpy(mut_doc, job, "job_id", job_name)
2590 || !yyjson_mut_obj_add_strcpy(mut_doc, job, "handler_id", job_name)
2591 || !yyjson_mut_obj_add_strcpy(mut_doc, job, "execution_status", "succeeded")
2592 || !yyjson_mut_obj_add_strcpy(mut_doc, job, "idempotency_key", replay_trace_id != NULL ? replay_trace_id : "")
2593 || !yyjson_mut_obj_add_strcpy(mut_doc, job, "scheduled_for", created_at != NULL ? created_at : "")
2594 || !yyjson_mut_obj_add_strcpy(mut_doc, job, "available_at", created_at != NULL ? created_at : "")
2595 || !yyjson_mut_obj_add_strcpy(mut_doc, job, "claimed_at", created_at != NULL ? created_at : "")
2596 || !yyjson_mut_obj_add_strcpy(mut_doc, job, "lease_expires_at", created_at != NULL ? created_at : "")
2597 || !yyjson_mut_obj_add_strcpy(mut_doc, job, "completed_at", created_at != NULL ? created_at : "")
2598 || !yyjson_mut_obj_add_strcpy(mut_doc, job, "failed_at", "")
2599 || !yyjson_mut_obj_add_int(mut_doc, job, "attempt_count", 1)
2600 || !yyjson_mut_obj_add_int(mut_doc, job, "max_attempts", 3)
2601 || !yyjson_mut_obj_add_int(mut_doc, job, "backoff_seconds", 5)
2602 || !yyjson_mut_obj_add_strcpy(mut_doc, job, "replay_trace_id", replay_trace_id != NULL ? replay_trace_id : "")
2603 || !yyjson_mut_obj_add_strcpy(mut_doc, job, "last_error", "")
2604 || !yyjson_mut_obj_add_strcpy(mut_doc, job, "created_at", created_at != NULL ? created_at : "")
2605 || !yyjson_mut_arr_append(job_executions, job)
2606 || !append_job_attempt_logic_local(mut_doc, mut_root, job_id, "succeeded", "{}", created_at, "")
2607 || !yyjson_mut_obj_add_int(mut_doc, job_event, "id", workflow_event_id + 1)
2608 || !yyjson_mut_obj_add_int(mut_doc, job_event, "workflow_execution_id", workflow_execution_id)
2609 || !yyjson_mut_obj_add_strcpy(mut_doc, job_event, "event_kind", "job_queued")
2610 || !yyjson_mut_obj_add_strcpy(mut_doc, job_event, "event_key", job_name)
2611 || !yyjson_mut_obj_add_strcpy(mut_doc, job_event, "created_at", created_at != NULL ? created_at : "")
2612 || !yyjson_mut_obj_add_strcpy(mut_doc, job_payload, "job_id", job_name)
2613 || !yyjson_mut_obj_add_strcpy(mut_doc, job_payload, "resource_id", resource_id != NULL ? resource_id : "")
2614 || !yyjson_mut_obj_add_strcpy(mut_doc, job_payload, "replay_trace_id", replay_trace_id != NULL ? replay_trace_id : "")
2615 || !yyjson_mut_obj_add_int(mut_doc, job_payload, "max_attempts", 3)
2616 || !yyjson_mut_obj_add_int(mut_doc, job_payload, "backoff_seconds", 5)
2617 || !yyjson_mut_obj_put(job_event, yyjson_mut_strcpy(mut_doc, "payload_json"), job_payload)
2618 || !yyjson_mut_arr_append(workflow_events, job_event)) {
2619 free(workflow_id);
2620 free(transition_id);
2621 free(event_name);
2622 return 0;
2623 }
2624 if (effect_counts_out != NULL) {
2625 effect_counts_out->job_count++;
2626 }
2627 job_id++;
2628 workflow_event_id++;
2629 }
2630 }
2631 if (webhooks != NULL && yyjson_is_arr(webhooks) && yyjson_arr_iter_init(webhooks, &iter)) {
2632 while ((item = yyjson_arr_iter_next(&iter)) != NULL) {
2633 const char *adapter_id = yyjson_is_str(item) ? yyjson_get_str(item) : "webhook";
2634 yyjson_mut_val *receipt = yyjson_mut_obj(mut_doc);
2635 yyjson_val *adapter = find_integration_adapter_logic_local(integration_root, adapter_id);
2636 char *target_url = integration_adapter_target_url_dup_logic_local(adapter);
2637 char *payload_json = effect_payload_json_dup_logic_local(yyjson_get_str(native_json_obj_get(action_contract, "action_id")), adapter_id, resource_id, replay_trace_id, created_at);
2638 long delivery_id = next_section_id_logic_local(mut_root, "integration_deliveries");
2639 yyjson_mut_val *integration_delivery = yyjson_mut_obj(mut_doc);
2640 if (receipt == NULL || integration_delivery == NULL || payload_json == NULL
2641 || !yyjson_mut_obj_add_int(mut_doc, receipt, "id", webhook_id)
2642 || !yyjson_mut_obj_add_strcpy(mut_doc, receipt, "adapter_id", adapter_id)
2643 || !yyjson_mut_obj_add_strcpy(mut_doc, receipt, "delivery_status", "queued")
2644 || !yyjson_mut_obj_add_strcpy(mut_doc, receipt, "resource_id", resource_id != NULL ? resource_id : "")
2645 || !yyjson_mut_obj_add_strcpy(mut_doc, receipt, "replay_trace_id", replay_trace_id != NULL ? replay_trace_id : "")
2646 || !yyjson_mut_obj_add_strcpy(mut_doc, receipt, "created_at", created_at != NULL ? created_at : "")
2647 || !yyjson_mut_arr_append(webhook_receipts, receipt)
2648 || !yyjson_mut_obj_add_int(mut_doc, integration_delivery, "id", delivery_id)
2649 || !yyjson_mut_obj_add_strcpy(mut_doc, integration_delivery, "adapter_id", adapter_id)
2650 || !yyjson_mut_obj_add_strcpy(mut_doc, integration_delivery, "target_url", target_url != NULL ? target_url : "")
2651 || !yyjson_mut_obj_add_strcpy(mut_doc, integration_delivery, "resource_id", resource_id != NULL ? resource_id : "")
2652 || !yyjson_mut_obj_add_strcpy(mut_doc, integration_delivery, "delivery_status", "queued")
2653 || !yyjson_mut_obj_add_int(mut_doc, integration_delivery, "attempt_count", 0)
2654 || !yyjson_mut_obj_add_int(mut_doc, integration_delivery, "max_attempts", adapter != NULL ? native_json_obj_get_long_default(adapter, "max_attempts", 3, NULL) : 3)
2655 || !yyjson_mut_obj_add_int(mut_doc, integration_delivery, "backoff_seconds", adapter != NULL ? native_json_obj_get_long_default(adapter, "backoff_seconds", 5, NULL) : 5)
2656 || !yyjson_mut_obj_add_int(mut_doc, integration_delivery, "timeout_seconds", adapter != NULL ? native_json_obj_get_long_default(adapter, "timeout_seconds", 10, NULL) : 10)
2657 || !yyjson_mut_obj_add_strcpy(mut_doc, integration_delivery, "last_error", "")
2658 || !yyjson_mut_obj_add_int(mut_doc, integration_delivery, "last_http_status", 0)
2659 || !yyjson_mut_obj_add_strcpy(mut_doc, integration_delivery, "next_attempt_at", created_at != NULL ? created_at : "")
2660 || !yyjson_mut_obj_add_strcpy(mut_doc, integration_delivery, "created_at", created_at != NULL ? created_at : "")
2661 || !yyjson_mut_obj_add_strcpy(mut_doc, integration_delivery, "updated_at", created_at != NULL ? created_at : "")
2662 || !yyjson_mut_obj_add_strcpy(mut_doc, integration_delivery, "completed_at", "")
2663 || !yyjson_mut_arr_append(integration_deliveries, integration_delivery)) {
2664 free(target_url);
2665 free(payload_json);
2666 free(workflow_id);
2667 free(transition_id);
2668 free(event_name);
2669 native_json_doc_free(integration_doc);
2670 return 0;
2671 }
2672 free(target_url);
2673 free(payload_json);
2674 if (effect_counts_out != NULL) {
2675 effect_counts_out->webhook_count++;
2676 }
2677 webhook_id++;
2678 }
2679 }
2680 if (notifications != NULL && yyjson_is_arr(notifications) && yyjson_arr_iter_init(notifications, &iter)) {
2681 while ((item = yyjson_arr_iter_next(&iter)) != NULL) {
2682 char *adapter_id = NULL;
2683 char *channel = NULL;
2684 char *recipient = NULL;
2685 char *template_id = NULL;
2686 yyjson_mut_val *delivery = yyjson_mut_obj(mut_doc);
2687 yyjson_mut_val *notification_event = yyjson_mut_obj(mut_doc);
2688 yyjson_mut_val *notification_payload = yyjson_mut_obj(mut_doc);
2689 if (yyjson_is_str(item)) {
2690 adapter_id = strdup(yyjson_get_str(item));
2691 } else if (yyjson_is_obj(item)) {
2692 adapter_id = native_json_obj_get_string_dup(item, "adapter_id");
2693 channel = native_json_obj_get_string_dup(item, "channel");
2694 recipient = string_value_from_contract_logic_local(native_json_obj_get(item, "recipient"), inputs, context, NULL, NULL);
2695 template_id = string_value_from_contract_logic_local(native_json_obj_get(item, "template_id"), inputs, context, NULL, NULL);
2696 }
2697 if (adapter_id == NULL) {
2698 adapter_id = strdup("notification");
2699 }
2700 if (channel == NULL) {
2701 channel = strdup("generic");
2702 }
2703 if (recipient == NULL) {
2704 recipient = strdup("");
2705 }
2706 if (template_id == NULL) {
2707 template_id = strdup("");
2708 }
2709 if (delivery == NULL || notification_event == NULL || notification_payload == NULL
2710 || adapter_id == NULL || channel == NULL || recipient == NULL || template_id == NULL
2711 || !yyjson_mut_obj_add_int(mut_doc, delivery, "id", notification_id)
2712 || !yyjson_mut_obj_add_int(mut_doc, delivery, "workflow_execution_id", workflow_execution_id)
2713 || !yyjson_mut_obj_add_strcpy(mut_doc, delivery, "adapter_id", adapter_id)
2714 || !yyjson_mut_obj_add_strcpy(mut_doc, delivery, "channel", channel)
2715 || !yyjson_mut_obj_add_strcpy(mut_doc, delivery, "recipient", recipient)
2716 || !yyjson_mut_obj_add_strcpy(mut_doc, delivery, "template_id", template_id)
2717 || !yyjson_mut_obj_add_strcpy(mut_doc, delivery, "delivery_status", "succeeded")
2718 || !yyjson_mut_obj_add_strcpy(mut_doc, delivery, "resource_id", resource_id != NULL ? resource_id : "")
2719 || !yyjson_mut_obj_add_strcpy(mut_doc, delivery, "replay_trace_id", replay_trace_id != NULL ? replay_trace_id : "")
2720 || !yyjson_mut_obj_add_strcpy(mut_doc, delivery, "created_at", created_at != NULL ? created_at : "")
2721 || !yyjson_mut_obj_add_strcpy(mut_doc, delivery, "delivered_at", created_at != NULL ? created_at : "")
2722 || !yyjson_mut_arr_append(notification_deliveries, delivery)
2723 || !yyjson_mut_obj_add_int(mut_doc, notification_event, "id", workflow_event_id + 1)
2724 || !yyjson_mut_obj_add_int(mut_doc, notification_event, "workflow_execution_id", workflow_execution_id)
2725 || !yyjson_mut_obj_add_strcpy(mut_doc, notification_event, "event_kind", "notification_delivered")
2726 || !yyjson_mut_obj_add_strcpy(mut_doc, notification_event, "event_key", adapter_id)
2727 || !yyjson_mut_obj_add_strcpy(mut_doc, notification_event, "created_at", created_at != NULL ? created_at : "")
2728 || !yyjson_mut_obj_add_strcpy(mut_doc, notification_payload, "adapter_id", adapter_id)
2729 || !yyjson_mut_obj_add_strcpy(mut_doc, notification_payload, "channel", channel)
2730 || !yyjson_mut_obj_add_strcpy(mut_doc, notification_payload, "recipient", recipient)
2731 || !yyjson_mut_obj_add_strcpy(mut_doc, notification_payload, "template_id", template_id)
2732 || !yyjson_mut_obj_add_strcpy(mut_doc, notification_payload, "resource_id", resource_id != NULL ? resource_id : "")
2733 || !yyjson_mut_obj_add_strcpy(mut_doc, notification_payload, "replay_trace_id", replay_trace_id != NULL ? replay_trace_id : "")
2734 || !yyjson_mut_obj_put(notification_event, yyjson_mut_strcpy(mut_doc, "payload_json"), notification_payload)
2735 || !yyjson_mut_arr_append(workflow_events, notification_event)) {
2736 free(adapter_id);
2737 free(channel);
2738 free(recipient);
2739 free(template_id);
2740 free(workflow_id);
2741 free(transition_id);
2742 free(event_name);
2743 return 0;
2744 }
2745 if (effect_counts_out != NULL) {
2746 effect_counts_out->notification_count++;
2747 }
2748 free(adapter_id);
2749 free(channel);
2750 free(recipient);
2751 free(template_id);
2752 notification_id++;
2753 workflow_event_id++;
2754 }
2755 }
2756 if (workflow_execution_id_out != NULL) {
2757 *workflow_execution_id_out = workflow_execution_id;
2758 }
2759 free(workflow_id);
2760 free(transition_id);
2761 free(event_name);
2762 native_json_doc_free(integration_doc);
2763 return 1;
2764}
2765
2767 yyjson_mut_doc *mut_doc,
2768 yyjson_mut_val *mut_root,
2769 yyjson_val *transition,
2770 const char *resource_id,
2771 const char *created_at,
2772 const char *replay_trace_id,
2773 const char *report_json,
2774 const char *from_state,
2775 const char *to_state
2776) {
2777 yyjson_mut_val *audit_log = ensure_mut_array_field_logic_local(mut_doc, mut_root, "audit_log");
2778 yyjson_mut_val *audit = yyjson_mut_obj(mut_doc);
2779 yyjson_mut_val *payload = yyjson_mut_obj(mut_doc);
2780 char *event_name = transition != NULL ? native_json_obj_get_string_dup(transition, "emits_event") : NULL;
2781 if (audit_log == NULL || audit == NULL || payload == NULL) {
2782 free(event_name);
2783 return 0;
2784 }
2785 if (!yyjson_mut_obj_add_strcpy(mut_doc, audit, "event", event_name != NULL ? event_name : "action_applied")
2786 || !yyjson_mut_obj_add_strcpy(mut_doc, audit, "created_at", created_at != NULL ? created_at : "")
2787 || !yyjson_mut_obj_add_strcpy(mut_doc, payload, "resource_id", resource_id != NULL ? resource_id : "")
2788 || !yyjson_mut_obj_add_strcpy(mut_doc, payload, "replay_trace_id", replay_trace_id != NULL ? replay_trace_id : "")
2789 || !yyjson_mut_obj_add_strcpy(mut_doc, payload, "from_state", from_state != NULL ? from_state : "none")
2790 || !yyjson_mut_obj_add_strcpy(mut_doc, payload, "to_state", to_state != NULL ? to_state : "applied")) {
2791 free(event_name);
2792 return 0;
2793 }
2794 if (report_json != NULL && report_json[0] != '\0') {
2795 yyjson_doc *report_doc = native_json_doc_load_text(report_json);
2796 yyjson_val *report_root = report_doc != NULL ? yyjson_doc_get_root(report_doc) : NULL;
2797 if (report_root != NULL && !yyjson_mut_obj_put(payload, yyjson_mut_strcpy(mut_doc, "action_report"), yyjson_val_mut_copy(mut_doc, report_root))) {
2798 native_json_doc_free(report_doc);
2799 free(event_name);
2800 return 0;
2801 }
2802 native_json_doc_free(report_doc);
2803 }
2804 if (!yyjson_mut_obj_put(audit, yyjson_mut_strcpy(mut_doc, "payload_json"), payload)
2805 || !yyjson_mut_arr_append(audit_log, audit)) {
2806 free(event_name);
2807 return 0;
2808 }
2809 free(event_name);
2810 return 1;
2811}
2812
2814 char *text = NULL;
2815 int code = 69;
2816 if (app == NULL || mut_doc == NULL) {
2817 return 69;
2818 }
2819 text = yyjson_mut_write(mut_doc, YYJSON_WRITE_NOFLAG, NULL);
2820 if (text == NULL) {
2821 return 69;
2822 }
2823 code = write_text_file(app->state_path, text);
2824 free(text);
2825 return code == 0 ? 0 : 69;
2826}
2827
2828/**
2829 * @brief Resolves a minimal HTTP reason phrase for framework-owned responses.
2830 *
2831 * @param status HTTP status code.
2832 * @return Static reason phrase string.
2833 */
2834static const char *http_reason_phrase_logic_local(int status) {
2835 switch (status) {
2836 case 200:
2837 return "OK";
2838 case 201:
2839 return "Created";
2840 case 202:
2841 return "Accepted";
2842 case 204:
2843 return "No Content";
2844 case 303:
2845 return "See Other";
2846 case 400:
2847 return "Bad Request";
2848 case 404:
2849 return "Not Found";
2850 case 422:
2851 return "Unprocessable Entity";
2852 default:
2853 return "OK";
2854 }
2855}
2856
2857/**
2858 * @brief Sends a declarative redirect response contract.
2859 *
2860 * @param connection Connection receiving the response.
2861 * @param app Finalized app runtime metadata.
2862 * @param response_contract Declarative response contract object.
2863 * @return HTTP status code written to the connection.
2864 */
2865static int send_redirect_logic_local(NativeConnection *connection, const AppRuntime *app, yyjson_val *response_contract) {
2866 char *location = response_contract != NULL ? native_json_obj_get_string_dup(response_contract, "location") : NULL;
2867 const char *base_path = app != NULL && app->base_path != NULL ? app->base_path : "";
2868 int status = 303;
2869 if (location == NULL || location[0] == '\0') {
2870 location = strdup("/");
2871 }
2873 connection,
2874 "HTTP/1.1 %d See Other\r\n"
2875 "Location: %s%s\r\n"
2876 "Cache-Control: no-store\r\n"
2877 "Content-Length: 0\r\n"
2878 "Connection: close\r\n"
2879 "\r\n",
2880 status,
2881 base_path,
2882 location != NULL ? location : "/"
2883 );
2884 free(location);
2885 return status;
2886}
2887
2888/**
2889 * @brief Sends a declarative JSON action-result response contract.
2890 *
2891 * The response is framework-owned and exposes a stable generic envelope with
2892 * action metadata, the persisted action report, and optional post-action state.
2893 *
2894 * @param connection Connection receiving the response.
2895 * @param response_contract Declarative response contract object.
2896 * @param action_id Executed action identifier.
2897 * @param resource_id Touched or created resource identifier.
2898 * @param workflow_execution_id Persisted workflow execution identifier.
2899 * @param report_json Serialized framework action report JSON.
2900 * @param mut_doc Mutated state document after the action applied.
2901 * @return HTTP status code written to the connection, or 500 on formatting failure.
2902 */
2904 NativeConnection *connection,
2905 yyjson_val *response_contract,
2906 const char *action_id,
2907 const char *resource_id,
2908 long workflow_execution_id,
2909 const char *report_json,
2910 yyjson_mut_doc *mut_doc
2911) {
2912 yyjson_mut_doc *response_doc = NULL;
2913 yyjson_mut_val *response_root = NULL;
2914 yyjson_doc *report_doc = NULL;
2915 yyjson_val *report_root = NULL;
2916 yyjson_doc *state_doc = NULL;
2917 yyjson_val *state_root = NULL;
2918 char *body = NULL;
2919 int include_state = yyjson_get_bool(response_contract != NULL ? native_json_obj_get(response_contract, "include_state") : NULL);
2920 int status = (int)native_json_obj_get_long_default(response_contract, "status", 200, NULL);
2921 const char *reason = http_reason_phrase_logic_local(status);
2922 size_t body_length = 0;
2923 if (connection == NULL || report_json == NULL || mut_doc == NULL) {
2924 return 500;
2925 }
2926 response_doc = yyjson_mut_doc_new(NULL);
2927 response_root = response_doc != NULL ? yyjson_mut_obj(response_doc) : NULL;
2928 report_doc = native_json_doc_load_text(report_json);
2929 report_root = report_doc != NULL ? yyjson_doc_get_root(report_doc) : NULL;
2930 if (response_doc != NULL && response_root != NULL) {
2931 yyjson_mut_doc_set_root(response_doc, response_root);
2932 }
2933 if (response_doc == NULL || response_root == NULL || report_root == NULL
2934 || !yyjson_mut_obj_add_bool(response_doc, response_root, "ok", 1)
2935 || !yyjson_mut_obj_add_strcpy(response_doc, response_root, "action_id", action_id != NULL ? action_id : "")
2936 || !yyjson_mut_obj_add_strcpy(response_doc, response_root, "resource_id", resource_id != NULL ? resource_id : "")
2937 || !yyjson_mut_obj_add_int(response_doc, response_root, "workflow_execution_id", workflow_execution_id)
2938 || !yyjson_mut_obj_put(response_root, yyjson_mut_strcpy(response_doc, "report"), yyjson_val_mut_copy(response_doc, report_root))) {
2939 native_json_doc_free(report_doc);
2940 yyjson_mut_doc_free(response_doc);
2941 native_send_text_response(connection, 500, "Internal Server Error", "response formatting error\n");
2942 return 500;
2943 }
2944 if (include_state) {
2945 char *state_text = yyjson_mut_write(mut_doc, YYJSON_WRITE_NOFLAG, NULL);
2946 if (state_text == NULL) {
2947 native_json_doc_free(report_doc);
2948 yyjson_mut_doc_free(response_doc);
2949 native_send_text_response(connection, 500, "Internal Server Error", "response formatting error\n");
2950 return 500;
2951 }
2952 state_doc = native_json_doc_load_text(state_text);
2953 free(state_text);
2954 state_root = state_doc != NULL ? yyjson_doc_get_root(state_doc) : NULL;
2955 if (state_root == NULL || !yyjson_mut_obj_put(response_root, yyjson_mut_strcpy(response_doc, "state"), yyjson_val_mut_copy(response_doc, state_root))) {
2956 native_json_doc_free(state_doc);
2957 native_json_doc_free(report_doc);
2958 yyjson_mut_doc_free(response_doc);
2959 native_send_text_response(connection, 500, "Internal Server Error", "response formatting error\n");
2960 return 500;
2961 }
2962 }
2963 body = yyjson_mut_write(response_doc, YYJSON_WRITE_NOFLAG, NULL);
2964 body_length = body != NULL ? strlen(body) : 0;
2965 if (body == NULL) {
2966 native_json_doc_free(state_doc);
2967 native_json_doc_free(report_doc);
2968 yyjson_mut_doc_free(response_doc);
2969 native_send_text_response(connection, 500, "Internal Server Error", "response formatting error\n");
2970 return 500;
2971 }
2973 connection,
2974 "HTTP/1.1 %d %s\r\n"
2975 "Content-Type: application/json\r\n"
2976 "Cache-Control: no-store\r\n"
2977 "Content-Length: %zu\r\n"
2978 "Connection: close\r\n"
2979 "\r\n"
2980 "%s",
2981 status,
2982 reason,
2983 body_length,
2984 body
2985 );
2986 free(body);
2987 native_json_doc_free(state_doc);
2988 native_json_doc_free(report_doc);
2989 yyjson_mut_doc_free(response_doc);
2990 return status;
2991}
2992
2993/**
2994 * @brief Sends the configured declarative response contract for an action.
2995 *
2996 * @param connection Connection receiving the response.
2997 * @param app Finalized app runtime metadata.
2998 * @param response_contract Declarative response contract object.
2999 * @param action_id Executed action identifier.
3000 * @param resource_id Touched or created resource identifier.
3001 * @param workflow_execution_id Persisted workflow execution identifier.
3002 * @param report_json Serialized framework action report JSON.
3003 * @param mut_doc Mutated state document after the action applied.
3004 * @return HTTP status code written to the connection.
3005 */
3007 NativeConnection *connection,
3008 const AppRuntime *app,
3009 yyjson_val *response_contract,
3010 const char *action_id,
3011 const char *resource_id,
3012 long workflow_execution_id,
3013 const char *report_json,
3014 yyjson_mut_doc *mut_doc
3015) {
3016 char *response_kind = response_contract != NULL ? native_json_obj_get_string_dup(response_contract, "response_kind") : NULL;
3017 int status = 0;
3018 if (response_kind != NULL && streq(response_kind, "json")) {
3019 status = send_json_action_result_logic_local(connection, response_contract, action_id, resource_id, workflow_execution_id, report_json, mut_doc);
3020 } else {
3021 status = send_redirect_logic_local(connection, app, response_contract);
3022 }
3023 free(response_kind);
3024 return status;
3025}
3026
3028 const AppRuntime *app,
3029 const char *action_id,
3030 char **submit_label_out,
3031 char **summary_out
3032) {
3033 yyjson_doc *action_doc = load_fixture_doc_logic_local(app, "action_fixture");
3034 yyjson_val *action_root = action_doc != NULL ? yyjson_doc_get_root(action_doc) : NULL;
3035 yyjson_val *action_contract = find_action_contract_logic_local(action_root, action_id);
3036 yyjson_val *presentation = action_contract != NULL ? native_json_obj_get(action_contract, "presentation") : NULL;
3037 if (submit_label_out != NULL) {
3038 *submit_label_out = presentation != NULL ? native_json_obj_get_string_dup(presentation, "submit_label") : NULL;
3039 }
3040 if (summary_out != NULL) {
3041 *summary_out = presentation != NULL ? native_json_obj_get_string_dup(presentation, "summary") : NULL;
3042 }
3043 native_json_doc_free(action_doc);
3044 return action_contract != NULL ? 1 : 0;
3045}
3046
3048 NativeConnection *connection,
3049 const AppRuntime *app,
3050 const HttpRequest *request,
3051 const char *action_id
3052) {
3053 yyjson_doc *action_doc = NULL;
3054 yyjson_doc *workflow_doc = NULL;
3055 yyjson_doc *state_doc = NULL;
3056 yyjson_val *action_root = NULL;
3057 yyjson_val *workflow_root = NULL;
3058 yyjson_val *state_root = NULL;
3059 yyjson_val *action_contract = NULL;
3060 yyjson_val *workflow = NULL;
3061 yyjson_val *transition = NULL;
3062 yyjson_mut_doc *mut_doc = NULL;
3063 yyjson_mut_val *mut_root = NULL;
3064 InputFieldMapLogicLocal inputs = {0};
3066 ActionEffectCountsLogicLocal effect_counts = {0};
3067 char *input_error = NULL;
3068 char *policy_gate = NULL;
3069 char *success_message = NULL;
3070 char *failure_message = NULL;
3071 char *operator_summary = NULL;
3072 char *transition_label = NULL;
3073 char *from_state_label = NULL;
3074 char *to_state_label = NULL;
3075 char *created_at = NULL;
3076 char *resource_id = NULL;
3077 char *from_state = NULL;
3078 char *to_state = NULL;
3079 char *report_json = NULL;
3080 char *replay_trace_id = NULL;
3081 yyjson_val *mutation_primitives = NULL;
3082 yyjson_arr_iter primitive_iter;
3083 yyjson_val *primitive = NULL;
3084 long next_id = 1;
3085 long created_record_id = 0;
3086 long workflow_execution_id = 0;
3087 int status = 0;
3088 char created_at_buffer[20];
3089 action_doc = load_fixture_doc_logic_local(app, "action_fixture");
3090 workflow_doc = load_fixture_doc_logic_local(app, "workflow_fixture");
3091 if (action_doc == NULL) {
3092 return 0;
3093 }
3094 action_root = yyjson_doc_get_root(action_doc);
3095 workflow_root = workflow_doc != NULL ? yyjson_doc_get_root(workflow_doc) : NULL;
3096 action_contract = find_action_contract_logic_local(action_root, action_id);
3097 if (action_contract == NULL) {
3098 native_json_doc_free(action_doc);
3099 native_json_doc_free(workflow_doc);
3100 return 0;
3101 }
3102 transition = find_workflow_transition_logic_local(workflow_root, action_id, &workflow);
3103 policy_gate = native_json_obj_get_string_dup(action_contract, "policy_gate");
3104 success_message = action_presentation_string_dup_logic_local(action_contract, "success_message");
3105 failure_message = action_presentation_string_dup_logic_local(action_contract, "failure_message");
3106 operator_summary = action_presentation_string_dup_logic_local(action_contract, "operator_summary");
3107 transition_label = workflow_presentation_string_dup_logic_local(transition, "transition_label");
3108 from_state_label = workflow_presentation_string_dup_logic_local(transition, "from_label");
3109 to_state_label = workflow_presentation_string_dup_logic_local(transition, "to_label");
3110 if (!load_action_inputs_logic_local(request, action_contract, &inputs, &input_error)) {
3111 status = 500;
3112 write_json_error_logic_local(connection, 500, "Internal Server Error", "action_input_error", "Failed to load action inputs.");
3113 goto cleanup;
3114 }
3115 if (input_error != NULL) {
3116 status = 422;
3118 connection,
3119 422,
3120 "Unprocessable Entity",
3121 "invalid_action_input",
3122 failure_message != NULL && failure_message[0] != '\0' ? failure_message : input_error
3123 );
3124 goto cleanup;
3125 }
3126 state_doc = native_json_doc_load_file(app->state_path);
3127 state_root = state_doc != NULL ? yyjson_doc_get_root(state_doc) : NULL;
3128 if (state_root == NULL || !yyjson_is_obj(state_root)) {
3129 status = 500;
3130 write_json_error_logic_local(connection, 500, "Internal Server Error", "state_unavailable", "Declarative action state is unavailable.");
3131 goto cleanup;
3132 }
3133 mut_doc = yyjson_doc_mut_copy(state_doc, NULL);
3134 mut_root = yyjson_mut_doc_get_root(mut_doc);
3135 if (mut_doc == NULL || mut_root == NULL || !yyjson_mut_is_obj(mut_root) || !now_iso_utc_logic_local(created_at_buffer)) {
3136 status = 500;
3137 write_json_error_logic_local(connection, 500, "Internal Server Error", "action_runtime_error", "Declarative action runtime initialization failed.");
3138 goto cleanup;
3139 }
3140 created_at = strdup(created_at_buffer);
3141 next_id = native_json_obj_get_long_default(state_root, "next_id", 1, NULL);
3142 mutation_primitives = native_json_obj_get(action_contract, "mutation_primitives");
3143 if (mutation_primitives == NULL || !yyjson_is_arr(mutation_primitives)) {
3144 status = 500;
3145 write_json_error_logic_local(connection, 500, "Internal Server Error", "missing_mutation_primitives", "Declarative action is missing mutation primitives.");
3146 goto cleanup;
3147 }
3148 if (yyjson_arr_iter_init(mutation_primitives, &primitive_iter)) {
3149 while ((primitive = yyjson_arr_iter_next(&primitive_iter)) != NULL) {
3150 char *kind = native_json_obj_get_string_dup(primitive, "primitive_kind");
3151 char *primitive_resource_id = NULL;
3152 int primitive_code = 0;
3153 long created_count = 0;
3154 if (kind != NULL && streq(kind, "append_record")) {
3155 primitive_code = append_record_primitive_logic_local(mut_doc, mut_root, primitive, &inputs, &context, next_id, &created_record_id, &primitive_resource_id);
3156 if (primitive_code == 1) {
3157 if (created_record_id > 0 && !note_created_record_logic_local(&context, created_record_id, primitive_resource_id)) {
3158 free(primitive_resource_id);
3159 free(kind);
3160 status = 500;
3161 write_json_error_logic_local(connection, 500, "Internal Server Error", "action_runtime_error", "Failed to record created action context.");
3162 goto cleanup;
3163 }
3164 next_id++;
3165 if (!set_mut_object_field_logic_local(mut_doc, mut_root, "next_id", yyjson_mut_int(mut_doc, next_id))) {
3166 free(primitive_resource_id);
3167 free(kind);
3168 status = 500;
3169 write_json_error_logic_local(connection, 500, "Internal Server Error", "action_runtime_error", "Failed to advance next_id.");
3170 goto cleanup;
3171 }
3172 if (from_state == NULL) {
3173 from_state = strdup("none");
3174 }
3175 if (to_state == NULL) {
3176 to_state = strdup("created");
3177 }
3178 }
3179 } else if (kind != NULL && streq(kind, "append_records_from_input_list")) {
3180 primitive_code = append_records_from_input_list_primitive_logic_local(mut_doc, mut_root, primitive, &inputs, &context, next_id, &created_count, &primitive_resource_id);
3181 if (primitive_code == 1 && created_count > 0) {
3182 next_id += created_count;
3183 if (!set_mut_object_field_logic_local(mut_doc, mut_root, "next_id", yyjson_mut_int(mut_doc, next_id))) {
3184 free(primitive_resource_id);
3185 free(kind);
3186 status = 500;
3187 write_json_error_logic_local(connection, 500, "Internal Server Error", "action_runtime_error", "Failed to advance next_id.");
3188 goto cleanup;
3189 }
3190 if (from_state == NULL) {
3191 from_state = strdup("none");
3192 }
3193 if (to_state == NULL) {
3194 to_state = strdup("created");
3195 }
3196 }
3197 } else if (kind != NULL && streq(kind, "append_records_from_related_collection")) {
3198 primitive_code = append_records_from_related_collection_primitive_logic_local(mut_doc, mut_root, state_root, primitive, &inputs, &context, next_id, &created_count, &primitive_resource_id);
3199 if (primitive_code == 1 && created_count > 0) {
3200 next_id += created_count;
3201 if (!set_mut_object_field_logic_local(mut_doc, mut_root, "next_id", yyjson_mut_int(mut_doc, next_id))) {
3202 free(primitive_resource_id);
3203 free(kind);
3204 status = 500;
3205 write_json_error_logic_local(connection, 500, "Internal Server Error", "action_runtime_error", "Failed to advance next_id.");
3206 goto cleanup;
3207 }
3208 if (from_state == NULL) {
3209 from_state = strdup("none");
3210 }
3211 if (to_state == NULL) {
3212 to_state = strdup("created");
3213 }
3214 }
3215 } else if (kind != NULL && streq(kind, "update_record")) {
3216 primitive_code = update_record_primitive_logic_local(mut_doc, mut_root, primitive, &inputs, &context, &primitive_resource_id, &from_state, &to_state);
3217 } else if (kind != NULL && streq(kind, "scoped_reassignment")) {
3218 primitive_code = scoped_reassignment_primitive_logic_local(mut_doc, mut_root, state_root, primitive, &inputs, &context, &primitive_resource_id);
3219 } else if (kind != NULL && streq(kind, "bulk_update")) {
3220 primitive_code = bulk_update_primitive_logic_local(mut_doc, mut_root, primitive, &inputs, &context, &primitive_resource_id, &from_state, &to_state);
3221 } else if (kind != NULL && streq(kind, "set_root_fields")) {
3222 primitive_code = set_root_fields_primitive_logic_local(mut_doc, mut_root, primitive, &inputs, &context, next_id);
3223 }
3224 if (primitive_resource_id != NULL) {
3225 if (resource_id == NULL) {
3226 resource_id = primitive_resource_id;
3227 primitive_resource_id = NULL;
3228 }
3229 free(primitive_resource_id);
3230 }
3231 free(kind);
3232 if (primitive_code == -2) {
3233 status = 422;
3235 connection,
3236 422,
3237 "Unprocessable Entity",
3238 "action_scope_overflow",
3239 failure_message != NULL && failure_message[0] != '\0'
3240 ? failure_message
3241 : "Matching scope exceeded the configured action bound."
3242 );
3243 goto cleanup;
3244 }
3245 if (primitive_code == -1) {
3246 status = 404;
3248 connection,
3249 404,
3250 "Not Found",
3251 "action_target_not_found",
3252 failure_message != NULL && failure_message[0] != '\0'
3253 ? failure_message
3254 : "Action target record not found."
3255 );
3256 goto cleanup;
3257 }
3258 if (primitive_code == 0) {
3259 status = 500;
3260 write_json_error_logic_local(connection, 500, "Internal Server Error", "action_runtime_error", "Declarative action primitive failed.");
3261 goto cleanup;
3262 }
3263 }
3264 }
3265 if (transition != NULL && context.created_record_count > 0
3266 && from_state != NULL && to_state != NULL
3267 && streq(from_state, "none") && streq(to_state, "created")) {
3268 char *transition_from_state = native_json_obj_get_string_dup(transition, "from_state");
3269 char *transition_to_state = native_json_obj_get_string_dup(transition, "to_state");
3270 if (transition_from_state != NULL && transition_from_state[0] != '\0') {
3271 free(from_state);
3272 from_state = transition_from_state;
3273 transition_from_state = NULL;
3274 }
3275 if (transition_to_state != NULL && transition_to_state[0] != '\0') {
3276 free(to_state);
3277 to_state = transition_to_state;
3278 transition_to_state = NULL;
3279 }
3280 free(transition_from_state);
3281 free(transition_to_state);
3282 }
3283 if (from_state == NULL && transition != NULL) {
3284 from_state = native_json_obj_get_string_dup(transition, "from_state");
3285 }
3286 if (to_state == NULL && transition != NULL) {
3287 to_state = native_json_obj_get_string_dup(transition, "to_state");
3288 }
3289 if (from_state == NULL) {
3290 from_state = strdup("none");
3291 }
3292 if (to_state == NULL) {
3293 to_state = strdup("applied");
3294 }
3295 if (created_record_id <= 0) {
3296 created_record_id = context.anchor_record_id > 0 ? context.anchor_record_id : context.last_created_record_id;
3297 }
3298 if (resource_id == NULL && context.anchor_resource_id != NULL) {
3299 resource_id = strdup(context.anchor_resource_id);
3300 }
3301 if (resource_id == NULL && context.last_touched_resource_id != NULL) {
3302 resource_id = strdup(context.last_touched_resource_id);
3303 }
3304 if (resource_id == NULL && created_record_id > 0) {
3305 size_t needed = 32;
3306 resource_id = (char *)malloc(needed);
3307 if (resource_id != NULL) {
3308 snprintf(resource_id, needed, "record:%ld", created_record_id);
3309 }
3310 }
3311 if (resource_id == NULL) {
3312 resource_id = strdup("resource:unknown");
3313 }
3314 if (!append_workflow_and_effects_logic_local(app, mut_doc, mut_root, action_contract, workflow, transition, &inputs, &context, policy_gate, resource_id, from_state, to_state, created_at, NULL, &workflow_execution_id, &effect_counts)) {
3315 status = 500;
3316 write_json_error_logic_local(connection, 500, "Internal Server Error", "workflow_recording_failed", "Failed to record workflow execution.");
3317 goto cleanup;
3318 }
3320 mut_doc,
3321 mut_root,
3322 action_contract,
3323 workflow,
3324 transition,
3325 &context,
3326 &effect_counts,
3327 resource_id,
3328 from_state,
3329 to_state,
3330 created_at,
3331 policy_gate,
3332 success_message,
3333 operator_summary,
3334 transition_label,
3335 from_state_label,
3336 to_state_label,
3337 workflow_execution_id,
3338 created_record_id,
3339 &report_json,
3340 &replay_trace_id)) {
3341 status = 500;
3342 write_json_error_logic_local(connection, 500, "Internal Server Error", "action_reporting_failed", "Failed to record action report.");
3343 goto cleanup;
3344 }
3345 if (!append_audit_event_logic_local(mut_doc, mut_root, transition, resource_id, created_at, replay_trace_id, report_json, from_state, to_state)) {
3346 status = 500;
3347 write_json_error_logic_local(connection, 500, "Internal Server Error", "audit_recording_failed", "Failed to append audit event.");
3348 goto cleanup;
3349 }
3350 if (persist_mut_doc_logic_local(app, mut_doc) != 0) {
3351 status = 500;
3352 write_json_error_logic_local(connection, 500, "Internal Server Error", "state_persist_failed", "Failed to persist declarative action state.");
3353 goto cleanup;
3354 }
3356 connection,
3357 app,
3358 native_json_obj_get(action_contract, "response_contract"),
3359 action_id,
3360 resource_id,
3361 workflow_execution_id,
3362 report_json,
3363 mut_doc
3364 );
3365
3366cleanup:
3367 native_json_doc_free(action_doc);
3368 native_json_doc_free(workflow_doc);
3369 native_json_doc_free(state_doc);
3370 yyjson_mut_doc_free(mut_doc);
3372 free(input_error);
3373 free(policy_gate);
3374 free(success_message);
3375 free(failure_message);
3376 free(operator_summary);
3377 free(transition_label);
3378 free(from_state_label);
3379 free(to_state_label);
3380 free(created_at);
3381 free(resource_id);
3382 free(from_state);
3383 free(to_state);
3384 free(report_json);
3385 free(replay_trace_id);
3387 return status;
3388}
static yyjson_val * find_integration_adapter_logic_local(yyjson_val *root, const char *adapter_id)
static yyjson_val * find_action_contract_logic_local(yyjson_val *root, const char *action_id)
static int parse_http_url_logic_local(const char *url, ParsedHttpUrlLogicLocal *parsed)
static yyjson_mut_val * find_mut_record_by_id_logic_local(yyjson_mut_val *collection, long record_id)
static yyjson_doc * load_fixture_doc_logic_local(const AppRuntime *app, const char *fixture_key)
static long matching_record_count_logic_local(yyjson_mut_val *collection, const char *scope_field, const char *expected_text)
static int set_root_fields_primitive_logic_local(yyjson_mut_doc *mut_doc, yyjson_mut_val *mut_root, yyjson_val *primitive, const InputFieldMapLogicLocal *inputs, const ActionExecutionContextLogicLocal *context, long next_id)
static int append_records_from_related_collection_primitive_logic_local(yyjson_mut_doc *mut_doc, yyjson_mut_val *mut_root, yyjson_val *source_root, yyjson_val *primitive, const InputFieldMapLogicLocal *inputs, ActionExecutionContextLogicLocal *context, long next_id, long *created_count_out, char **resource_id_out)
static char * action_presentation_string_dup_logic_local(yyjson_val *action_contract, const char *field_name)
static int bulk_update_primitive_logic_local(yyjson_mut_doc *mut_doc, yyjson_mut_val *mut_root, yyjson_val *primitive, const InputFieldMapLogicLocal *inputs, ActionExecutionContextLogicLocal *context, char **resource_id_out, char **from_state_out, char **to_state_out)
static char * scope_match_expected_text_dup_logic_local(yyjson_val *scope_match, const InputFieldMapLogicLocal *inputs, const ActionExecutionContextLogicLocal *context)
static int process_webhook_delivery_logic_local(yyjson_mut_doc *mut_doc, yyjson_mut_val *mut_root, yyjson_mut_val *delivery, const char *created_at, const char *payload_json)
static int process_due_integration_deliveries_logic_local(yyjson_mut_doc *mut_doc, yyjson_mut_val *mut_root, const char *created_at, const char *payload_json)
static int note_updated_record_logic_local(ActionExecutionContextLogicLocal *context, long record_id, const char *resource_id)
static char * string_value_from_contract_logic_local(yyjson_val *value_contract, const InputFieldMapLogicLocal *inputs, const ActionExecutionContextLogicLocal *context, const char *list_item_text, yyjson_val *source_record_context)
static int load_action_inputs_logic_local(const HttpRequest *request, yyjson_val *action_contract, InputFieldMapLogicLocal *map, char **error_message_out)
static int append_action_report_logic_local(yyjson_mut_doc *mut_doc, yyjson_mut_val *mut_root, yyjson_val *action_contract, yyjson_val *workflow, yyjson_val *transition, const ActionExecutionContextLogicLocal *context, const ActionEffectCountsLogicLocal *effect_counts, const char *resource_id, const char *from_state, const char *to_state, const char *created_at, const char *policy_gate, const char *success_message, const char *operator_summary, const char *transition_label, const char *from_state_label, const char *to_state_label, long workflow_execution_id, long created_record_id, char **report_json_out, char **replay_trace_id_out)
static yyjson_mut_val * find_mut_record_by_long_field_logic_local(yyjson_mut_val *array, const char *field_name, long match_value)
static int send_action_response_logic_local(NativeConnection *connection, const AppRuntime *app, yyjson_val *response_contract, const char *action_id, const char *resource_id, long workflow_execution_id, const char *report_json, yyjson_mut_doc *mut_doc)
Sends the configured declarative response contract for an action.
static int append_mut_object_to_array_logic_local(yyjson_mut_doc *doc, yyjson_mut_val *root, const char *section_name, yyjson_mut_val *obj)
static int note_bounded_scope_logic_local(ActionExecutionContextLogicLocal *context, long matched_count, long applied_count, const char *overflow_policy)
static long next_section_id_logic_local(yyjson_mut_val *root, const char *section_name)
static int send_redirect_logic_local(NativeConnection *connection, const AppRuntime *app, yyjson_val *response_contract)
Sends a declarative redirect response contract.
static char * integration_adapter_target_url_dup_logic_local(yyjson_val *adapter)
static long matching_record_count_value_logic_local(yyjson_val *collection, const char *scope_field, const char *expected_text)
static int update_record_primitive_logic_local(yyjson_mut_doc *mut_doc, yyjson_mut_val *mut_root, yyjson_val *primitive, const InputFieldMapLogicLocal *inputs, ActionExecutionContextLogicLocal *context, char **resource_id_out, char **from_state_out, char **to_state_out)
int native_dynamic_execute_declarative_action(NativeConnection *connection, const AppRuntime *app, const HttpRequest *request, const char *action_id)
Executes one declarative dynamic-app action contract against JSON state.
static void free_outbound_http_result_logic_local(OutboundHttpResultLogicLocal *result)
static yyjson_mut_val * ensure_mut_array_field_logic_local(yyjson_mut_doc *doc, yyjson_mut_val *root, const char *field_name)
int native_dynamic_action_presentation_dup(const AppRuntime *app, const char *action_id, char **submit_label_out, char **summary_out)
Loads declarative presentation overrides for one action contract.
static char * scoped_resource_id_dup_logic_local(const char *collection_name, const char *field_name, const char *expected_text)
static int append_record_primitive_logic_local(yyjson_mut_doc *mut_doc, yyjson_mut_val *mut_root, yyjson_val *primitive, const InputFieldMapLogicLocal *inputs, const ActionExecutionContextLogicLocal *context, long next_id, long *created_record_id_out, char **resource_id_out)
static int append_records_from_input_list_primitive_logic_local(yyjson_mut_doc *mut_doc, yyjson_mut_val *mut_root, yyjson_val *primitive, const InputFieldMapLogicLocal *inputs, ActionExecutionContextLogicLocal *context, long next_id, long *created_count_out, char **resource_id_out)
static char * input_field_value_dup_logic_local(const InputFieldMapLogicLocal *map, const char *field_id)
static int append_audit_event_logic_local(yyjson_mut_doc *mut_doc, yyjson_mut_val *mut_root, yyjson_val *transition, const char *resource_id, const char *created_at, const char *replay_trace_id, const char *report_json, const char *from_state, const char *to_state)
static void free_input_field_map_logic_local(InputFieldMapLogicLocal *map)
static char * app_manifest_path_dup_logic_local(const AppRuntime *app)
static char * request_form_value_dup_logic_local(const HttpRequest *request, const char *field_name)
static int persist_mut_doc_logic_local(const AppRuntime *app, yyjson_mut_doc *mut_doc)
static void free_action_execution_context_logic_local(ActionExecutionContextLogicLocal *context)
static int append_list_item_logic_local(char ***items_out, size_t *count_out, const char *start, size_t length)
static int append_bytes_logic_local(char **buffer, size_t *length, size_t *capacity, const char *chunk, size_t chunk_len)
static char * fixture_path_dup_logic_local(const AppRuntime *app, const char *fixture_key)
static int outbound_http_post_logic_local(const char *target_url, const char *content_type, const char *payload_json, OutboundHttpResultLogicLocal *result)
static int set_mut_object_field_logic_local(yyjson_mut_doc *doc, yyjson_mut_val *obj, const char *field_name, yyjson_mut_val *value)
static int record_field_matches_text_logic_local(yyjson_mut_val *record, const char *field_name, const char *expected_text)
static void free_parsed_http_url_logic_local(ParsedHttpUrlLogicLocal *url)
static int append_job_attempt_logic_local(yyjson_mut_doc *mut_doc, yyjson_mut_val *mut_root, long job_execution_id, const char *execution_status, const char *payload_json, const char *created_at, const char *error_message)
static int now_iso_utc_logic_local(char out[20])
static int split_input_list_logic_local(const char *raw_value, const char *delimiter, char ***items_out, size_t *count_out)
static const char * http_reason_phrase_logic_local(int status)
Resolves a minimal HTTP reason phrase for framework-owned responses.
static int send_json_action_result_logic_local(NativeConnection *connection, yyjson_val *response_contract, const char *action_id, const char *resource_id, long workflow_execution_id, const char *report_json, yyjson_mut_doc *mut_doc)
Sends a declarative JSON action-result response contract.
static void free_split_items_logic_local(char **items, size_t count)
static int replace_owned_string_logic_local(char **slot, const char *value)
static char * workflow_presentation_string_dup_logic_local(yyjson_val *transition, const char *field_name)
static int now_plus_seconds_iso_utc_logic_local(long offset_seconds, char out[20])
static int note_created_record_logic_local(ActionExecutionContextLogicLocal *context, long record_id, const char *resource_id)
static int append_workflow_and_effects_logic_local(const AppRuntime *app, yyjson_mut_doc *mut_doc, yyjson_mut_val *mut_root, yyjson_val *action_contract, yyjson_val *workflow, yyjson_val *transition, const InputFieldMapLogicLocal *inputs, const ActionExecutionContextLogicLocal *context, const char *policy_gate, const char *resource_id, const char *from_state, const char *to_state, const char *created_at, const char *replay_trace_id, long *workflow_execution_id_out, ActionEffectCountsLogicLocal *effect_counts_out)
static yyjson_val * find_workflow_transition_logic_local(yyjson_val *root, const char *action_id, yyjson_val **workflow_out)
static char * request_body_bytes_dup_logic_local(const HttpRequest *request, size_t *out_size)
static char * effect_payload_json_dup_logic_local(const char *action_id, const char *adapter_id, const char *resource_id, const char *replay_trace_id, const char *created_at)
static yyjson_mut_val * field_value_from_contract_logic_local(yyjson_mut_doc *doc, yyjson_val *value_contract, const InputFieldMapLogicLocal *inputs, long next_id, yyjson_val *record_context, const ActionExecutionContextLogicLocal *context, const char *list_item_text, yyjson_val *source_record_context)
static void write_json_error_logic_local(NativeConnection *connection, int status, const char *status_text, const char *error_kind, const char *message)
static int scoped_reassignment_primitive_logic_local(yyjson_mut_doc *mut_doc, yyjson_mut_val *mut_root, yyjson_val *source_root, yyjson_val *primitive, const InputFieldMapLogicLocal *inputs, ActionExecutionContextLogicLocal *context, char **resource_id_out)
static int record_field_matches_text_value_logic_local(yyjson_val *record, const char *field_name, const char *expected_text)
Shared declarative action, workflow, and effect helpers for dynamic-app business-logic scaffolding.
char * native_dynamic_multipart_extract_text(const unsigned char *body, size_t body_len, const char *content_type, const char *field_name)
Extracts a text value from a multipart form field.
Pharos-owned multipart form-data parsing for in-process dynamic handling.
char * native_dynamic_form_value(const char *body, const char *key)
Extracts a value from a URL-encoded form body.
Request field extraction helpers for in-process dynamic handling.
int write_text_file(const char *path, const char *content)
Writes a text buffer to a file path.
Definition native_fs.c:66
Declares native filesystem helper routines.
void native_send_text_response(NativeConnection *connection, int status, const char *status_text, const char *body)
Sends a plain-text HTTP response with explicit content length.
int native_connection_printf(NativeConnection *connection, const char *format,...)
Writes formatted bytes to a native connection.
Declares native HTTP response writing helpers.
char * path_join(const char *left, const char *right)
Joins two path segments using the native path separator.
void trim_in_place(char *text)
Trims leading and trailing ASCII whitespace from a string in place.
char * dup_range(const char *start, size_t len)
Duplicates a byte range into a newly allocated NUL-terminated string.
int streq(const char *a, const char *b)
Tests whether two strings are exactly equal.
char * read_file_text(const char *path)
Reads an entire file into a newly allocated text buffer.
Declares shared low-level support helpers used across the Pharos native runtime.
yyjson_doc * native_json_doc_load_file(const char *path)
Parses a JSON file into an owned yyjson document.
long native_json_obj_get_long_default(yyjson_val *obj, const char *key, long default_value, int *found_out)
Reads a numeric object member or returns a fallback value.
void native_json_doc_free(yyjson_doc *doc)
Releases a document returned by the native yyjson helpers.
yyjson_doc * native_json_doc_load_text(const char *text)
Parses JSON text into an owned yyjson document.
char * native_json_obj_get_string_dup(yyjson_val *obj, const char *key)
Duplicates a string-valued key from a JSON object.
yyjson_val * native_json_obj_get(yyjson_val *obj, const char *key)
Looks up a key from a JSON object value.
Loaded runtime metadata for one Pharos app artifact.
Parsed HTTP request fields extracted from a native connection.
InputFieldValueLogicLocal * items
Accepted client connection and its resolved transport metadata.
void yyjson_mut_doc_free(yyjson_mut_doc *doc)
Definition yyjson.c:2653
yyjson_mut_doc * yyjson_mut_doc_new(const yyjson_alc *alc)
Definition yyjson.c:2663
yyjson_mut_doc * yyjson_doc_mut_copy(const yyjson_doc *doc, const yyjson_alc *alc)
Definition yyjson.c:2678
yyjson_mut_val * yyjson_val_mut_copy(yyjson_mut_doc *m_doc, const yyjson_val *i_vals)
Definition yyjson.c:2714
yyjson_api_inline yyjson_mut_val * yyjson_mut_obj_remove_key(yyjson_mut_val *obj, const char *key)
Definition yyjson.h:7316
yyjson_api_inline bool yyjson_is_str(const yyjson_val *val)
Definition yyjson.h:5390
yyjson_api_inline bool yyjson_is_obj(const yyjson_val *val)
Definition yyjson.h:5398
#define yyjson_mut_arr_foreach(arr, idx, max, val)
Definition yyjson.h:2987
yyjson_api_inline yyjson_mut_val * yyjson_mut_obj_get(const yyjson_mut_val *obj, const char *key)
Definition yyjson.h:6970
yyjson_api_inline yyjson_val * yyjson_obj_iter_get_val(yyjson_val *key)
Definition yyjson.h:5751
yyjson_api_inline int64_t yyjson_get_sint(const yyjson_val *val)
Definition yyjson.h:5453
yyjson_api_inline bool yyjson_mut_obj_add_int(yyjson_mut_doc *doc, yyjson_mut_val *obj, const char *key, int64_t val)
Definition yyjson.h:7425
yyjson_api_inline yyjson_mut_val * yyjson_mut_bool(yyjson_mut_doc *doc, bool val)
Definition yyjson.h:6236
yyjson_api_inline yyjson_val * yyjson_obj_iter_next(yyjson_obj_iter *iter)
Definition yyjson.h:5741
#define yyjson_arr_foreach(arr, idx, max, val)
Definition yyjson.h:2247
yyjson_api_inline yyjson_val * yyjson_doc_get_root(const yyjson_doc *doc)
Definition yyjson.h:5323
yyjson_api_inline bool yyjson_is_arr(const yyjson_val *val)
Definition yyjson.h:5394
yyjson_api_inline yyjson_mut_val * yyjson_mut_obj(yyjson_mut_doc *doc)
Definition yyjson.h:7086
yyjson_api_inline yyjson_mut_val * yyjson_mut_null(yyjson_mut_doc *doc)
Definition yyjson.h:6224
yyjson_api_inline bool yyjson_obj_iter_init(const yyjson_val *obj, yyjson_obj_iter *iter)
Definition yyjson.h:5718
yyjson_api_inline char * yyjson_mut_write(const yyjson_mut_doc *doc, yyjson_write_flag flg, size_t *len)
Definition yyjson.h:1604
yyjson_api_inline void yyjson_mut_doc_set_root(yyjson_mut_doc *doc, yyjson_mut_val *root)
Definition yyjson.h:5919
yyjson_api_inline yyjson_mut_val * yyjson_mut_int(yyjson_mut_doc *doc, int64_t num)
Definition yyjson.h:6251
yyjson_api_inline yyjson_mut_val * yyjson_mut_strcpy(yyjson_mut_doc *doc, const char *str)
Definition yyjson.h:6282
yyjson_api_inline bool yyjson_equals_str(const yyjson_val *val, const char *str)
Definition yyjson.h:5477
yyjson_api_inline bool yyjson_mut_is_obj(const yyjson_mut_val *val)
Definition yyjson.h:5978
yyjson_api_inline yyjson_mut_val * yyjson_mut_doc_get_root(yyjson_mut_doc *doc)
Definition yyjson.h:5915
yyjson_api_inline yyjson_val * yyjson_arr_iter_next(yyjson_arr_iter *iter)
Definition yyjson.h:5672
yyjson_api_inline bool yyjson_is_num(const yyjson_val *val)
Definition yyjson.h:5386
yyjson_api_inline bool yyjson_is_null(const yyjson_val *val)
Definition yyjson.h:5354
yyjson_api_inline bool yyjson_mut_obj_add_bool(yyjson_mut_doc *doc, yyjson_mut_val *obj, const char *key, bool val)
Definition yyjson.h:7404
yyjson_api_inline bool yyjson_mut_obj_add_strcpy(yyjson_mut_doc *doc, yyjson_mut_val *obj, const char *key, const char *val)
Definition yyjson.h:7479
yyjson_api_inline size_t yyjson_arr_size(const yyjson_val *arr)
Definition yyjson.h:5599
yyjson_api_inline bool yyjson_mut_obj_put(yyjson_mut_val *obj, yyjson_mut_val *key, yyjson_mut_val *val)
Definition yyjson.h:7259
yyjson_api_inline bool yyjson_mut_is_str(const yyjson_mut_val *val)
Definition yyjson.h:5970
yyjson_api_inline yyjson_mut_val * yyjson_mut_sint(yyjson_mut_doc *doc, int64_t num)
Definition yyjson.h:6246
static const yyjson_write_flag YYJSON_WRITE_NOFLAG
Definition yyjson.h:1245
yyjson_api_inline const char * yyjson_get_str(const yyjson_val *val)
Definition yyjson.h:5469
yyjson_api_inline bool yyjson_mut_is_arr(const yyjson_mut_val *val)
Definition yyjson.h:5974
yyjson_api_inline yyjson_mut_val * yyjson_mut_arr(yyjson_mut_doc *doc)
Definition yyjson.h:6410
yyjson_api_inline bool yyjson_mut_arr_append(yyjson_mut_val *arr, yyjson_mut_val *val)
Definition yyjson.h:6621
yyjson_api_inline bool yyjson_arr_iter_init(const yyjson_val *arr, yyjson_arr_iter *iter)
Definition yyjson.h:5650
yyjson_api_inline bool yyjson_get_bool(const yyjson_val *val)
Definition yyjson.h:5445
yyjson_api_inline bool yyjson_is_bool(const yyjson_val *val)
Definition yyjson.h:5366