1635 lines
70 KiB
C
1635 lines
70 KiB
C
#include "trainlog/web_programs.h"
|
|
|
|
#include <math.h>
|
|
#include <stdint.h>
|
|
#include <stdio.h>
|
|
#include <stdlib.h>
|
|
#include <string.h>
|
|
#include <time.h>
|
|
|
|
#include <openssl/evp.h>
|
|
#include <sqlite3.h>
|
|
#include <yyjson.h>
|
|
|
|
#include "database_internal.h"
|
|
#include "trainlog/id.h"
|
|
#include "trainlog/web_sessions.h"
|
|
|
|
#define PROGRAM_PAGE_MAX 64U
|
|
|
|
static bool timestamp_now(char output[32]) {
|
|
time_t now = time(NULL);
|
|
struct tm utc;
|
|
|
|
return now != (time_t)-1 && gmtime_r(&now, &utc) != NULL &&
|
|
strftime(output, 32U, "%Y-%m-%dT%H:%M:%SZ", &utc) > 0U;
|
|
}
|
|
|
|
static TrainlogStatus
|
|
write_document(yyjson_mut_doc *document, char **output_json, size_t *output_size) {
|
|
yyjson_write_err error;
|
|
char *json = yyjson_mut_write_opts(document, YYJSON_WRITE_NOFLAG, NULL, output_size, &error);
|
|
|
|
yyjson_mut_doc_free(document);
|
|
if (json == NULL) {
|
|
return TRAINLOG_STATUS_SYSTEM_ERROR;
|
|
}
|
|
*output_json = json;
|
|
return TRAINLOG_STATUS_OK;
|
|
}
|
|
|
|
static bool
|
|
object_has_exact_keys(yyjson_val *object, const char *const *expected, size_t expected_count) {
|
|
yyjson_obj_iter iterator;
|
|
yyjson_val *key;
|
|
size_t seen = 0U;
|
|
|
|
if (!yyjson_is_obj(object) || yyjson_obj_size(object) != expected_count) {
|
|
return false;
|
|
}
|
|
iterator = yyjson_obj_iter_with(object);
|
|
while ((key = yyjson_obj_iter_next(&iterator)) != NULL) {
|
|
const char *name = yyjson_get_str(key);
|
|
size_t index;
|
|
bool found = false;
|
|
|
|
for (index = 0U; index < expected_count; ++index) {
|
|
if (name != NULL && strcmp(name, expected[index]) == 0) {
|
|
found = true;
|
|
break;
|
|
}
|
|
}
|
|
if (!found) {
|
|
return false;
|
|
}
|
|
++seen;
|
|
}
|
|
return seen == expected_count;
|
|
}
|
|
|
|
static bool no_duplicate_keys(yyjson_val *value) {
|
|
if (yyjson_is_obj(value)) {
|
|
yyjson_obj_iter outer = yyjson_obj_iter_with(value);
|
|
yyjson_val *key;
|
|
|
|
while ((key = yyjson_obj_iter_next(&outer)) != NULL) {
|
|
yyjson_obj_iter inner = yyjson_obj_iter_with(value);
|
|
yyjson_val *other;
|
|
size_t matches = 0U;
|
|
|
|
while ((other = yyjson_obj_iter_next(&inner)) != NULL) {
|
|
if (strcmp(yyjson_get_str(key), yyjson_get_str(other)) == 0) {
|
|
++matches;
|
|
}
|
|
}
|
|
if (matches != 1U || !no_duplicate_keys(yyjson_obj_iter_get_val(key))) {
|
|
return false;
|
|
}
|
|
}
|
|
} else if (yyjson_is_arr(value)) {
|
|
size_t index;
|
|
for (index = 0U; index < yyjson_arr_size(value); ++index) {
|
|
if (!no_duplicate_keys(yyjson_arr_get(value, index))) {
|
|
return false;
|
|
}
|
|
}
|
|
}
|
|
return true;
|
|
}
|
|
|
|
static bool bounded_string(yyjson_val *value, size_t maximum, bool nullable) {
|
|
size_t length;
|
|
if (nullable && yyjson_is_null(value)) {
|
|
return true;
|
|
}
|
|
if (!yyjson_is_str(value)) {
|
|
return false;
|
|
}
|
|
length = yyjson_get_len(value);
|
|
return length > 0U && length <= maximum;
|
|
}
|
|
|
|
static bool nullable_date(yyjson_val *value) {
|
|
const char *date;
|
|
size_t index;
|
|
static const size_t HYPHENS[] = {4U, 7U};
|
|
|
|
if (yyjson_is_null(value)) {
|
|
return true;
|
|
}
|
|
if (!yyjson_is_str(value) || yyjson_get_len(value) != 10U) {
|
|
return false;
|
|
}
|
|
date = yyjson_get_str(value);
|
|
for (index = 0U; index < 10U; ++index) {
|
|
if ((index == HYPHENS[0] || index == HYPHENS[1]) ? date[index] != '-'
|
|
: date[index] < '0' || date[index] > '9') {
|
|
return false;
|
|
}
|
|
}
|
|
return true;
|
|
}
|
|
|
|
static bool digest_hex(const char *body, size_t body_size, char output[65]) {
|
|
unsigned char digest[EVP_MAX_MD_SIZE];
|
|
unsigned int digest_size = 0U;
|
|
size_t index;
|
|
|
|
if (EVP_Digest(body, body_size, digest, &digest_size, EVP_sha256(), NULL) != 1 ||
|
|
digest_size != 32U) {
|
|
return false;
|
|
}
|
|
for (index = 0U; index < digest_size; ++index) {
|
|
(void)snprintf(output + (index * 2U), 3U, "%02x", digest[index]);
|
|
}
|
|
output[64] = '\0';
|
|
return true;
|
|
}
|
|
|
|
static bool add_column_text(yyjson_mut_doc *document,
|
|
yyjson_mut_val *object,
|
|
const char *key,
|
|
sqlite3_stmt *statement,
|
|
int column) {
|
|
if (sqlite3_column_type(statement, column) == SQLITE_NULL) {
|
|
return yyjson_mut_obj_add_null(document, object, key);
|
|
}
|
|
return sqlite3_column_type(statement, column) == SQLITE_TEXT &&
|
|
yyjson_mut_obj_add_strcpy(
|
|
document, object, key, (const char *)sqlite3_column_text(statement, column));
|
|
}
|
|
|
|
static bool add_nullable_integer(yyjson_mut_doc *document,
|
|
yyjson_mut_val *object,
|
|
const char *key,
|
|
sqlite3_stmt *statement,
|
|
int column) {
|
|
if (sqlite3_column_type(statement, column) == SQLITE_NULL) {
|
|
return yyjson_mut_obj_add_null(document, object, key);
|
|
}
|
|
return yyjson_mut_obj_add_sint(document, object, key, sqlite3_column_int64(statement, column));
|
|
}
|
|
|
|
static bool add_nullable_real(yyjson_mut_doc *document,
|
|
yyjson_mut_val *object,
|
|
const char *key,
|
|
sqlite3_stmt *statement,
|
|
int column) {
|
|
if (sqlite3_column_type(statement, column) == SQLITE_NULL) {
|
|
return yyjson_mut_obj_add_null(document, object, key);
|
|
}
|
|
return yyjson_mut_obj_add_real(document, object, key, sqlite3_column_double(statement, column));
|
|
}
|
|
|
|
static bool
|
|
add_program_list_item(yyjson_mut_doc *document, yyjson_mut_val *items, sqlite3_stmt *statement) {
|
|
yyjson_mut_val *item = yyjson_mut_obj(document);
|
|
|
|
if (item == NULL) {
|
|
return false;
|
|
}
|
|
return add_column_text(document, item, "program_id", statement, 0) &&
|
|
add_column_text(document, item, "title", statement, 1) &&
|
|
add_column_text(document, item, "state", statement, 2) &&
|
|
add_column_text(document, item, "start_date", statement, 3) &&
|
|
add_column_text(document, item, "end_date", statement, 4) &&
|
|
yyjson_mut_obj_add_sint(
|
|
document, item, "session_count", sqlite3_column_int64(statement, 5)) &&
|
|
add_column_text(document, item, "provenance", statement, 6) &&
|
|
add_column_text(document, item, "updated_at", statement, 7) &&
|
|
add_column_text(document, item, "imported_at", statement, 8) &&
|
|
add_column_text(document, item, "revision_id", statement, 9) &&
|
|
yyjson_mut_obj_add_sint(
|
|
document, item, "preparation_count", sqlite3_column_int64(statement, 10)) &&
|
|
yyjson_mut_obj_add_strcpy(document,
|
|
item,
|
|
"usage",
|
|
sqlite3_column_int64(statement, 10) > 0 ? "used" : "unused") &&
|
|
yyjson_mut_arr_add_val(items, item);
|
|
}
|
|
|
|
TrainlogStatus trainlog_web_programs_list_json(TrainlogDatabase *database,
|
|
const char *search,
|
|
const char *state,
|
|
size_t offset,
|
|
size_t limit,
|
|
char **output_json,
|
|
size_t *output_size) {
|
|
static const char SQL[] =
|
|
"SELECT p.program_id,p.title,p.state,p.start_date,p.end_date,"
|
|
"COUNT(DISTINCT s.program_session_id),p.source_format,p.updated_at,p.created_at,"
|
|
"p.revision_id,COUNT(DISTINCT sp.preparation_id) "
|
|
"FROM programs p "
|
|
"LEFT JOIN program_sessions s ON s.program_id=p.program_id "
|
|
"LEFT JOIN session_preparations sp ON sp.source_program_id=p.program_id "
|
|
"WHERE p.deleted_at IS NULL AND "
|
|
"(?1='' OR p.title LIKE '%'||?1||'%' COLLATE NOCASE) "
|
|
"AND (?2='' OR p.state=?2) GROUP BY p.program_id ORDER BY CASE p.state WHEN 'active' "
|
|
"THEN 0 ELSE 1 END,p.title COLLATE NOCASE,p.program_id LIMIT ?3 OFFSET ?4";
|
|
sqlite3_stmt *statement = NULL;
|
|
yyjson_mut_doc *document;
|
|
yyjson_mut_val *root;
|
|
yyjson_mut_val *items;
|
|
size_t count = 0U;
|
|
int step;
|
|
|
|
if (database == NULL || output_json == NULL || output_size == NULL || limit == 0U ||
|
|
limit > PROGRAM_PAGE_MAX || offset > INT64_MAX ||
|
|
(state != NULL && state[0] != '\0' && strcmp(state, "active") != 0 &&
|
|
strcmp(state, "archived") != 0)) {
|
|
return TRAINLOG_STATUS_INVALID_ARGUMENT;
|
|
}
|
|
*output_json = NULL;
|
|
*output_size = 0U;
|
|
document = yyjson_mut_doc_new(NULL);
|
|
root = document == NULL ? NULL : yyjson_mut_obj(document);
|
|
items = document == NULL ? NULL : yyjson_mut_arr(document);
|
|
if (document == NULL || root == NULL || items == NULL) {
|
|
yyjson_mut_doc_free(document);
|
|
return TRAINLOG_STATUS_SYSTEM_ERROR;
|
|
}
|
|
if (sqlite3_prepare_v2(database->connection, SQL, -1, &statement, NULL) != SQLITE_OK) {
|
|
yyjson_mut_doc_free(document);
|
|
return TRAINLOG_STATUS_DATABASE_ERROR;
|
|
}
|
|
if (sqlite3_bind_text(statement, 1, search == NULL ? "" : search, -1, SQLITE_TRANSIENT) !=
|
|
SQLITE_OK ||
|
|
sqlite3_bind_text(statement, 2, state == NULL ? "" : state, -1, SQLITE_TRANSIENT) !=
|
|
SQLITE_OK ||
|
|
sqlite3_bind_int64(statement, 3, (sqlite3_int64)(limit + 1U)) != SQLITE_OK ||
|
|
sqlite3_bind_int64(statement, 4, (sqlite3_int64)offset) != SQLITE_OK) {
|
|
(void)sqlite3_finalize(statement);
|
|
yyjson_mut_doc_free(document);
|
|
return TRAINLOG_STATUS_DATABASE_ERROR;
|
|
}
|
|
yyjson_mut_doc_set_root(document, root);
|
|
(void)yyjson_mut_obj_add_uint(document, root, "api_version", 1U);
|
|
(void)yyjson_mut_obj_add_uint(document, root, "offset", offset);
|
|
(void)yyjson_mut_obj_add_val(document, root, "items", items);
|
|
while ((step = sqlite3_step(statement)) == SQLITE_ROW) {
|
|
if (count == limit) {
|
|
++count;
|
|
break;
|
|
}
|
|
if (!add_program_list_item(document, items, statement)) {
|
|
(void)sqlite3_finalize(statement);
|
|
yyjson_mut_doc_free(document);
|
|
return TRAINLOG_STATUS_DATABASE_ERROR;
|
|
}
|
|
++count;
|
|
}
|
|
if ((step != SQLITE_DONE && count <= limit) || sqlite3_finalize(statement) != SQLITE_OK) {
|
|
yyjson_mut_doc_free(document);
|
|
return TRAINLOG_STATUS_DATABASE_ERROR;
|
|
}
|
|
(void)yyjson_mut_obj_add_bool(document, root, "more", count > limit);
|
|
(void)yyjson_mut_obj_add_uint(document, root, "next_offset", offset + limit);
|
|
return write_document(document, output_json, output_size);
|
|
}
|
|
|
|
static bool
|
|
add_program_entry_json(yyjson_mut_doc *document, yyjson_mut_val *entries, sqlite3_stmt *statement) {
|
|
yyjson_mut_val *entry = yyjson_mut_obj(document);
|
|
|
|
if (entry == NULL) {
|
|
return false;
|
|
}
|
|
return add_column_text(document, entry, "entry_id", statement, 0) &&
|
|
yyjson_mut_obj_add_sint(
|
|
document, entry, "position", sqlite3_column_int64(statement, 1)) &&
|
|
add_column_text(document, entry, "exercise_id", statement, 2) &&
|
|
add_column_text(document, entry, "equipment_id", statement, 3) &&
|
|
add_column_text(document, entry, "recording_mode", statement, 4) &&
|
|
add_column_text(document, entry, "tracking_mode", statement, 5) &&
|
|
yyjson_mut_obj_add_sint(
|
|
document, entry, "data_fields", sqlite3_column_int64(statement, 6)) &&
|
|
add_column_text(document, entry, "load_mode", statement, 7) &&
|
|
yyjson_mut_obj_add_sint(
|
|
document, entry, "rest_seconds", sqlite3_column_int64(statement, 8)) &&
|
|
add_nullable_integer(document, entry, "target_sets", statement, 9) &&
|
|
add_nullable_integer(document, entry, "target_reps", statement, 10) &&
|
|
add_nullable_integer(document, entry, "target_duration_seconds", statement, 11) &&
|
|
add_nullable_real(document, entry, "target_weight_kg", statement, 12) &&
|
|
add_column_text(document, entry, "notes", statement, 13) &&
|
|
yyjson_mut_arr_add_val(entries, entry);
|
|
}
|
|
|
|
static TrainlogStatus add_program_entries(TrainlogDatabase *database,
|
|
const char *session_id,
|
|
yyjson_mut_doc *document,
|
|
yyjson_mut_val *entries) {
|
|
static const char SQL[] =
|
|
"SELECT entry_id,position,exercise_id,equipment_id,recording_mode,tracking_mode,"
|
|
"data_fields,load_mode,rest_seconds,target_sets,target_reps,target_duration_seconds,"
|
|
"target_weight_kg,notes "
|
|
"FROM program_session_entries "
|
|
"WHERE program_session_id=?1 "
|
|
"ORDER BY position,entry_id";
|
|
sqlite3_stmt *statement = NULL;
|
|
int step;
|
|
|
|
if (sqlite3_prepare_v2(database->connection, SQL, -1, &statement, NULL) != SQLITE_OK) {
|
|
return TRAINLOG_STATUS_DATABASE_ERROR;
|
|
}
|
|
if (sqlite3_bind_text(statement, 1, session_id, -1, SQLITE_TRANSIENT) != SQLITE_OK) {
|
|
(void)sqlite3_finalize(statement);
|
|
return TRAINLOG_STATUS_DATABASE_ERROR;
|
|
}
|
|
while ((step = sqlite3_step(statement)) == SQLITE_ROW) {
|
|
if (!add_program_entry_json(document, entries, statement)) {
|
|
(void)sqlite3_finalize(statement);
|
|
return TRAINLOG_STATUS_DATABASE_ERROR;
|
|
}
|
|
}
|
|
if (step != SQLITE_DONE || sqlite3_finalize(statement) != SQLITE_OK) {
|
|
return TRAINLOG_STATUS_DATABASE_ERROR;
|
|
}
|
|
return TRAINLOG_STATUS_OK;
|
|
}
|
|
|
|
static TrainlogStatus add_program_session_json(TrainlogDatabase *database,
|
|
yyjson_mut_doc *document,
|
|
yyjson_mut_val *sessions,
|
|
sqlite3_stmt *statement) {
|
|
yyjson_mut_val *session = yyjson_mut_obj(document);
|
|
yyjson_mut_val *entries = yyjson_mut_arr(document);
|
|
const char *session_id = (const char *)sqlite3_column_text(statement, 0);
|
|
TrainlogStatus status;
|
|
|
|
if (session == NULL || entries == NULL || session_id == NULL) {
|
|
return TRAINLOG_STATUS_DATABASE_ERROR;
|
|
}
|
|
if (!add_column_text(document, session, "program_session_id", statement, 0) ||
|
|
!yyjson_mut_obj_add_sint(
|
|
document, session, "position", sqlite3_column_int64(statement, 1)) ||
|
|
!add_column_text(document, session, "title", statement, 2) ||
|
|
!add_column_text(document, session, "session_type", statement, 3) ||
|
|
!add_column_text(document, session, "planned_for", statement, 4) ||
|
|
!add_column_text(document, session, "note", statement, 5) ||
|
|
!add_column_text(document, session, "execution_state", statement, 6) ||
|
|
!add_column_text(document, session, "execution_session_id", statement, 7) ||
|
|
!yyjson_mut_obj_add_val(document, session, "occurrences", entries) ||
|
|
!yyjson_mut_arr_add_val(sessions, session)) {
|
|
return TRAINLOG_STATUS_DATABASE_ERROR;
|
|
}
|
|
status = add_program_entries(database, session_id, document, entries);
|
|
return status;
|
|
}
|
|
|
|
static TrainlogStatus add_program_sessions(TrainlogDatabase *database,
|
|
const char *program_id,
|
|
yyjson_mut_doc *document,
|
|
yyjson_mut_val *sessions) {
|
|
static const char SESSION_SQL[] =
|
|
"SELECT ps.program_session_id,ps.position,ps.title,ps.session_type,ps.planned_for,"
|
|
"ps.note,CASE WHEN pe.state='completed' THEN 'completed' "
|
|
"WHEN pe.state='in_progress' THEN 'in_progress' "
|
|
"WHEN EXISTS(SELECT 1 FROM session_preparations sp WHERE "
|
|
"sp.source_program_session_id=ps.program_session_id AND sp.withdrawn_at IS NULL) "
|
|
"THEN 'prepared' ELSE 'todo' END,pe.session_id "
|
|
"FROM program_sessions ps LEFT JOIN program_session_executions pe "
|
|
"ON pe.program_session_id=ps.program_session_id "
|
|
"WHERE ps.program_id=?1 ORDER BY ps.position,ps.program_session_id";
|
|
sqlite3_stmt *session_statement = NULL;
|
|
int step;
|
|
|
|
if (sqlite3_prepare_v2(database->connection, SESSION_SQL, -1, &session_statement, NULL) !=
|
|
SQLITE_OK) {
|
|
return TRAINLOG_STATUS_DATABASE_ERROR;
|
|
}
|
|
if (sqlite3_bind_text(session_statement, 1, program_id, -1, SQLITE_TRANSIENT) != SQLITE_OK) {
|
|
(void)sqlite3_finalize(session_statement);
|
|
return TRAINLOG_STATUS_DATABASE_ERROR;
|
|
}
|
|
while ((step = sqlite3_step(session_statement)) == SQLITE_ROW) {
|
|
TrainlogStatus status =
|
|
add_program_session_json(database, document, sessions, session_statement);
|
|
if (status != TRAINLOG_STATUS_OK) {
|
|
(void)sqlite3_finalize(session_statement);
|
|
return status;
|
|
}
|
|
}
|
|
if (step != SQLITE_DONE || sqlite3_finalize(session_statement) != SQLITE_OK) {
|
|
return TRAINLOG_STATUS_DATABASE_ERROR;
|
|
}
|
|
return TRAINLOG_STATUS_OK;
|
|
}
|
|
|
|
TrainlogStatus trainlog_web_programs_detail_json(TrainlogDatabase *database,
|
|
const char *program_id,
|
|
char **output_json,
|
|
size_t *output_size) {
|
|
sqlite3_stmt *statement = NULL;
|
|
yyjson_mut_doc *document;
|
|
yyjson_mut_val *root;
|
|
yyjson_mut_val *sessions;
|
|
TrainlogStatus status;
|
|
|
|
if (database == NULL || program_id == NULL || program_id[0] == '\0' || output_json == NULL ||
|
|
output_size == NULL) {
|
|
return TRAINLOG_STATUS_INVALID_ARGUMENT;
|
|
}
|
|
*output_json = NULL;
|
|
*output_size = 0U;
|
|
document = yyjson_mut_doc_new(NULL);
|
|
root = document == NULL ? NULL : yyjson_mut_obj(document);
|
|
sessions = document == NULL ? NULL : yyjson_mut_arr(document);
|
|
if (document == NULL || root == NULL || sessions == NULL) {
|
|
yyjson_mut_doc_free(document);
|
|
return TRAINLOG_STATUS_SYSTEM_ERROR;
|
|
}
|
|
if (sqlite3_prepare_v2(
|
|
database->connection,
|
|
"SELECT program_id,title,note,state,start_date,end_date,created_at,updated_at,"
|
|
"revision_id,source_format,source_version,source_payload_sha256,deleted_at "
|
|
"FROM programs WHERE program_id=?1",
|
|
-1,
|
|
&statement,
|
|
NULL) != SQLITE_OK) {
|
|
yyjson_mut_doc_free(document);
|
|
return TRAINLOG_STATUS_DATABASE_ERROR;
|
|
}
|
|
if (sqlite3_bind_text(statement, 1, program_id, -1, SQLITE_TRANSIENT) != SQLITE_OK) {
|
|
(void)sqlite3_finalize(statement);
|
|
yyjson_mut_doc_free(document);
|
|
return TRAINLOG_STATUS_DATABASE_ERROR;
|
|
}
|
|
if (sqlite3_step(statement) != SQLITE_ROW) {
|
|
(void)sqlite3_finalize(statement);
|
|
yyjson_mut_doc_free(document);
|
|
return TRAINLOG_STATUS_NOT_FOUND;
|
|
}
|
|
if (sqlite3_column_type(statement, 12) != SQLITE_NULL) {
|
|
(void)sqlite3_finalize(statement);
|
|
yyjson_mut_doc_free(document);
|
|
return TRAINLOG_STATUS_CONFLICT;
|
|
}
|
|
yyjson_mut_doc_set_root(document, root);
|
|
(void)yyjson_mut_obj_add_uint(document, root, "api_version", 1U);
|
|
for (int column = 0; column < 12; ++column) {
|
|
static const char *const KEYS[] = {"program_id",
|
|
"title",
|
|
"note",
|
|
"state",
|
|
"start_date",
|
|
"end_date",
|
|
"created_at",
|
|
"updated_at",
|
|
"revision_id",
|
|
"source_format",
|
|
"source_version",
|
|
"source_payload_sha256"};
|
|
if (column == 10) {
|
|
(void)yyjson_mut_obj_add_sint(
|
|
document, root, KEYS[column], sqlite3_column_int64(statement, column));
|
|
} else {
|
|
(void)add_column_text(document, root, KEYS[column], statement, column);
|
|
}
|
|
}
|
|
(void)sqlite3_finalize(statement);
|
|
(void)yyjson_mut_obj_add_val(document, root, "sessions", sessions);
|
|
status = add_program_sessions(database, program_id, document, sessions);
|
|
if (status != TRAINLOG_STATUS_OK) {
|
|
yyjson_mut_doc_free(document);
|
|
return status;
|
|
}
|
|
return write_document(document, output_json, output_size);
|
|
}
|
|
|
|
static bool equipment_is_known(TrainlogDatabase *database, yyjson_val *equipment) {
|
|
sqlite3_stmt *statement = NULL;
|
|
int result;
|
|
|
|
if (yyjson_is_null(equipment)) {
|
|
return true;
|
|
}
|
|
result = sqlite3_prepare_v2(
|
|
database->connection,
|
|
"SELECT 1 FROM custom_equipment e WHERE e.equipment_id=?1 AND NOT EXISTS("
|
|
"SELECT 1 FROM sync_causal_state c WHERE c.target_kind='custom_equipment' "
|
|
"AND c.target_id=e.equipment_id AND c.deleted=1)",
|
|
-1,
|
|
&statement,
|
|
NULL);
|
|
if (result == SQLITE_OK) {
|
|
result = sqlite3_bind_text(statement, 1, yyjson_get_str(equipment), -1, SQLITE_TRANSIENT);
|
|
}
|
|
if (result == SQLITE_OK) {
|
|
result = sqlite3_step(statement);
|
|
}
|
|
(void)sqlite3_finalize(statement);
|
|
return result == SQLITE_ROW;
|
|
}
|
|
|
|
static bool valid_target_weight(const char *load_mode, yyjson_val *weight) {
|
|
double target_weight;
|
|
|
|
if (strcmp(load_mode, "none") == 0) {
|
|
return yyjson_is_null(weight);
|
|
}
|
|
if (!yyjson_is_num(weight)) {
|
|
return false;
|
|
}
|
|
target_weight = yyjson_get_num(weight);
|
|
return isfinite(target_weight) && target_weight > 0.0 && target_weight <= 2000.0;
|
|
}
|
|
|
|
static bool valid_occurrence(TrainlogDatabase *database, yyjson_val *occurrence) {
|
|
static const char *const KEYS[] = {"entry_id",
|
|
"exercise_id",
|
|
"equipment_id",
|
|
"load_mode",
|
|
"rest_seconds",
|
|
"target_sets",
|
|
"target_reps",
|
|
"target_duration_seconds",
|
|
"target_weight_kg",
|
|
"notes"};
|
|
sqlite3_stmt *statement = NULL;
|
|
const char *exercise_id;
|
|
const char *load_mode;
|
|
char recording_mode[16];
|
|
char tracking_mode[16];
|
|
int prepare_result;
|
|
int bind_result;
|
|
int step_result;
|
|
yyjson_val *equipment;
|
|
yyjson_val *weight;
|
|
|
|
if (!object_has_exact_keys(occurrence, KEYS, sizeof(KEYS) / sizeof(KEYS[0])) ||
|
|
!bounded_string(yyjson_obj_get(occurrence, "entry_id"), 128U, false) ||
|
|
!bounded_string(yyjson_obj_get(occurrence, "exercise_id"), 128U, false) ||
|
|
!(yyjson_is_null(yyjson_obj_get(occurrence, "equipment_id")) ||
|
|
bounded_string(yyjson_obj_get(occurrence, "equipment_id"), 128U, false)) ||
|
|
!bounded_string(yyjson_obj_get(occurrence, "load_mode"), 16U, false) ||
|
|
!yyjson_is_int(yyjson_obj_get(occurrence, "rest_seconds")) ||
|
|
yyjson_get_sint(yyjson_obj_get(occurrence, "rest_seconds")) < 0 ||
|
|
yyjson_get_sint(yyjson_obj_get(occurrence, "rest_seconds")) > 86400 ||
|
|
!(yyjson_is_null(yyjson_obj_get(occurrence, "notes")) ||
|
|
bounded_string(yyjson_obj_get(occurrence, "notes"), 4000U, false))) {
|
|
return false;
|
|
}
|
|
exercise_id = yyjson_get_str(yyjson_obj_get(occurrence, "exercise_id"));
|
|
load_mode = yyjson_get_str(yyjson_obj_get(occurrence, "load_mode"));
|
|
if (strcmp(load_mode, "none") != 0 && strcmp(load_mode, "external") != 0 &&
|
|
strcmp(load_mode, "assistance") != 0) {
|
|
return false;
|
|
}
|
|
equipment = yyjson_obj_get(occurrence, "equipment_id");
|
|
weight = yyjson_obj_get(occurrence, "target_weight_kg");
|
|
if (!equipment_is_known(database, equipment) || !valid_target_weight(load_mode, weight)) {
|
|
return false;
|
|
}
|
|
prepare_result =
|
|
sqlite3_prepare_v2(database->connection,
|
|
"SELECT recording_mode,tracking_mode "
|
|
"FROM exercises "
|
|
"WHERE exercise_id=?1 "
|
|
"AND NOT EXISTS(SELECT 1 FROM sync_causal_state c "
|
|
"WHERE c.target_kind='exercise' AND c.target_id=?1 AND c.deleted=1)",
|
|
-1,
|
|
&statement,
|
|
NULL);
|
|
if (prepare_result != SQLITE_OK) {
|
|
return false;
|
|
}
|
|
bind_result = sqlite3_bind_text(statement, 1, exercise_id, -1, SQLITE_TRANSIENT);
|
|
if (bind_result != SQLITE_OK) {
|
|
(void)sqlite3_finalize(statement);
|
|
return false;
|
|
}
|
|
step_result = sqlite3_step(statement);
|
|
if (step_result != SQLITE_ROW || sqlite3_column_text(statement, 0) == NULL ||
|
|
sqlite3_column_text(statement, 1) == NULL) {
|
|
(void)sqlite3_finalize(statement);
|
|
return false;
|
|
}
|
|
(void)snprintf(recording_mode,
|
|
sizeof(recording_mode),
|
|
"%s",
|
|
(const char *)sqlite3_column_text(statement, 0));
|
|
(void)snprintf(tracking_mode,
|
|
sizeof(tracking_mode),
|
|
"%s",
|
|
(const char *)sqlite3_column_text(statement, 1));
|
|
(void)sqlite3_finalize(statement);
|
|
if (strcmp(recording_mode, "sets") == 0) {
|
|
yyjson_val *target_sets = yyjson_obj_get(occurrence, "target_sets");
|
|
if (!yyjson_is_int(target_sets) || yyjson_get_sint(target_sets) < 1 ||
|
|
yyjson_get_sint(target_sets) > 99) {
|
|
return false;
|
|
}
|
|
} else if (!yyjson_is_null(yyjson_obj_get(occurrence, "target_sets"))) {
|
|
return false;
|
|
}
|
|
if (strcmp(tracking_mode, "reps") == 0) {
|
|
yyjson_val *target_reps = yyjson_obj_get(occurrence, "target_reps");
|
|
return yyjson_is_int(target_reps) && yyjson_get_sint(target_reps) >= 1 &&
|
|
yyjson_get_sint(target_reps) <= 10000 &&
|
|
yyjson_is_null(yyjson_obj_get(occurrence, "target_duration_seconds"));
|
|
}
|
|
return yyjson_is_int(yyjson_obj_get(occurrence, "target_duration_seconds")) &&
|
|
yyjson_get_sint(yyjson_obj_get(occurrence, "target_duration_seconds")) >= 1 &&
|
|
yyjson_get_sint(yyjson_obj_get(occurrence, "target_duration_seconds")) <= 604800 &&
|
|
yyjson_is_null(yyjson_obj_get(occurrence, "target_reps"));
|
|
}
|
|
|
|
static bool
|
|
validate_program(TrainlogDatabase *database, yyjson_val *root, yyjson_val **program_out) {
|
|
static const char *const ROOT_KEYS[] = {"format", "version", "program"};
|
|
static const char *const PROGRAM_KEYS[] = {
|
|
"program_id", "title", "note", "state", "start_date", "end_date", "sessions"};
|
|
static const char *const SESSION_KEYS[] = {
|
|
"program_session_id", "title", "session_type", "planned_for", "note", "occurrences"};
|
|
yyjson_val *program;
|
|
yyjson_val *sessions;
|
|
size_t session_index;
|
|
|
|
if (!no_duplicate_keys(root) || !object_has_exact_keys(root, ROOT_KEYS, 3U) ||
|
|
!yyjson_equals_str(yyjson_obj_get(root, "format"), "trainlog-program") ||
|
|
!yyjson_is_int(yyjson_obj_get(root, "version")) ||
|
|
yyjson_get_sint(yyjson_obj_get(root, "version")) != 1) {
|
|
return false;
|
|
}
|
|
program = yyjson_obj_get(root, "program");
|
|
if (!object_has_exact_keys(program, PROGRAM_KEYS, 7U) ||
|
|
!bounded_string(yyjson_obj_get(program, "program_id"), 128U, false) ||
|
|
!bounded_string(yyjson_obj_get(program, "title"), 200U, false) ||
|
|
!(yyjson_is_null(yyjson_obj_get(program, "note")) ||
|
|
bounded_string(yyjson_obj_get(program, "note"), 4000U, false)) ||
|
|
!(yyjson_equals_str(yyjson_obj_get(program, "state"), "active") ||
|
|
yyjson_equals_str(yyjson_obj_get(program, "state"), "archived")) ||
|
|
!nullable_date(yyjson_obj_get(program, "start_date")) ||
|
|
!nullable_date(yyjson_obj_get(program, "end_date"))) {
|
|
return false;
|
|
}
|
|
sessions = yyjson_obj_get(program, "sessions");
|
|
if (!yyjson_is_arr(sessions) || yyjson_arr_size(sessions) == 0U ||
|
|
yyjson_arr_size(sessions) > TRAINLOG_WEB_PROGRAM_SESSIONS_MAX) {
|
|
return false;
|
|
}
|
|
for (session_index = 0U; session_index < yyjson_arr_size(sessions); ++session_index) {
|
|
yyjson_val *session = yyjson_arr_get(sessions, session_index);
|
|
yyjson_val *occurrences;
|
|
size_t occurrence_index;
|
|
if (!object_has_exact_keys(session, SESSION_KEYS, 6U) ||
|
|
!bounded_string(yyjson_obj_get(session, "program_session_id"), 128U, false) ||
|
|
!bounded_string(yyjson_obj_get(session, "title"), 200U, false) ||
|
|
!(yyjson_equals_str(yyjson_obj_get(session, "session_type"), "training") ||
|
|
yyjson_equals_str(yyjson_obj_get(session, "session_type"), "max_test")) ||
|
|
!nullable_date(yyjson_obj_get(session, "planned_for")) ||
|
|
!(yyjson_is_null(yyjson_obj_get(session, "note")) ||
|
|
bounded_string(yyjson_obj_get(session, "note"), 4000U, false))) {
|
|
return false;
|
|
}
|
|
occurrences = yyjson_obj_get(session, "occurrences");
|
|
if (!yyjson_is_arr(occurrences) || yyjson_arr_size(occurrences) == 0U ||
|
|
yyjson_arr_size(occurrences) > TRAINLOG_WEB_PROGRAM_OCCURRENCES_MAX) {
|
|
return false;
|
|
}
|
|
for (occurrence_index = 0U; occurrence_index < yyjson_arr_size(occurrences);
|
|
++occurrence_index) {
|
|
if (!valid_occurrence(database, yyjson_arr_get(occurrences, occurrence_index))) {
|
|
return false;
|
|
}
|
|
}
|
|
}
|
|
*program_out = program;
|
|
return true;
|
|
}
|
|
|
|
static int bind_json_text(sqlite3_stmt *statement, int index, yyjson_val *value) {
|
|
return yyjson_is_null(value)
|
|
? sqlite3_bind_null(statement, index)
|
|
: sqlite3_bind_text(statement, index, yyjson_get_str(value), -1, SQLITE_TRANSIENT);
|
|
}
|
|
|
|
static TrainlogStatus program_insert_header(TrainlogDatabase *database,
|
|
yyjson_val *program,
|
|
const char *digest,
|
|
const char *now,
|
|
const char *revision) {
|
|
static const char SQL[] =
|
|
"INSERT INTO programs(program_id,title,note,state,start_date,end_date,created_at,"
|
|
"updated_at,revision_id,source_format,source_version,source_payload_sha256) "
|
|
"VALUES(?1,?2,?3,?4,?5,?6,?7,?7,?8,'trainlog-program',1,?9)";
|
|
sqlite3_stmt *statement = NULL;
|
|
int result;
|
|
|
|
result = sqlite3_prepare_v2(database->connection, SQL, -1, &statement, NULL);
|
|
if (result != SQLITE_OK) {
|
|
return TRAINLOG_STATUS_DATABASE_ERROR;
|
|
}
|
|
result = sqlite3_bind_text(
|
|
statement, 1, yyjson_get_str(yyjson_obj_get(program, "program_id")), -1, SQLITE_TRANSIENT);
|
|
if (result == SQLITE_OK) {
|
|
result = sqlite3_bind_text(
|
|
statement, 2, yyjson_get_str(yyjson_obj_get(program, "title")), -1, SQLITE_TRANSIENT);
|
|
}
|
|
if (result == SQLITE_OK) {
|
|
result = bind_json_text(statement, 3, yyjson_obj_get(program, "note"));
|
|
}
|
|
if (result == SQLITE_OK) {
|
|
result = sqlite3_bind_text(
|
|
statement, 4, yyjson_get_str(yyjson_obj_get(program, "state")), -1, SQLITE_TRANSIENT);
|
|
}
|
|
if (result == SQLITE_OK) {
|
|
result = bind_json_text(statement, 5, yyjson_obj_get(program, "start_date"));
|
|
}
|
|
if (result == SQLITE_OK) {
|
|
result = bind_json_text(statement, 6, yyjson_obj_get(program, "end_date"));
|
|
}
|
|
if (result == SQLITE_OK) {
|
|
result = sqlite3_bind_text(statement, 7, now, -1, SQLITE_TRANSIENT);
|
|
}
|
|
if (result == SQLITE_OK) {
|
|
result = sqlite3_bind_text(statement, 8, revision, -1, SQLITE_TRANSIENT);
|
|
}
|
|
if (result == SQLITE_OK) {
|
|
result = sqlite3_bind_text(statement, 9, digest, -1, SQLITE_TRANSIENT);
|
|
}
|
|
if (result == SQLITE_OK) {
|
|
result = sqlite3_step(statement) == SQLITE_DONE ? SQLITE_OK : SQLITE_ERROR;
|
|
}
|
|
if (sqlite3_finalize(statement) != SQLITE_OK) {
|
|
result = SQLITE_ERROR;
|
|
}
|
|
return result == SQLITE_OK ? TRAINLOG_STATUS_OK : TRAINLOG_STATUS_DATABASE_ERROR;
|
|
}
|
|
|
|
typedef struct ProgramExerciseProfile {
|
|
char recording_mode[16];
|
|
char tracking_mode[16];
|
|
int64_t data_fields;
|
|
} ProgramExerciseProfile;
|
|
|
|
static TrainlogStatus program_load_profile(TrainlogDatabase *database,
|
|
const char *exercise_id,
|
|
ProgramExerciseProfile *profile) {
|
|
sqlite3_stmt *statement = NULL;
|
|
int result;
|
|
|
|
result = sqlite3_prepare_v2(
|
|
database->connection,
|
|
"SELECT recording_mode,tracking_mode,data_fields FROM exercises WHERE exercise_id=?1",
|
|
-1,
|
|
&statement,
|
|
NULL);
|
|
if (result == SQLITE_OK) {
|
|
result = sqlite3_bind_text(statement, 1, exercise_id, -1, SQLITE_TRANSIENT);
|
|
}
|
|
if (result == SQLITE_OK && sqlite3_step(statement) == SQLITE_ROW) {
|
|
(void)snprintf(profile->recording_mode,
|
|
sizeof(profile->recording_mode),
|
|
"%s",
|
|
(const char *)sqlite3_column_text(statement, 0));
|
|
(void)snprintf(profile->tracking_mode,
|
|
sizeof(profile->tracking_mode),
|
|
"%s",
|
|
(const char *)sqlite3_column_text(statement, 1));
|
|
profile->data_fields = sqlite3_column_int64(statement, 2);
|
|
} else {
|
|
result = SQLITE_ERROR;
|
|
}
|
|
(void)sqlite3_finalize(statement);
|
|
return result == SQLITE_OK ? TRAINLOG_STATUS_OK : TRAINLOG_STATUS_DATABASE_ERROR;
|
|
}
|
|
|
|
static int
|
|
bind_optional_number(sqlite3_stmt *statement, int parameter, yyjson_val *value, bool real_value) {
|
|
if (yyjson_is_null(value)) {
|
|
return sqlite3_bind_null(statement, parameter);
|
|
}
|
|
if (real_value) {
|
|
return sqlite3_bind_double(statement, parameter, yyjson_get_real(value));
|
|
}
|
|
return sqlite3_bind_int64(statement, parameter, yyjson_get_sint(value));
|
|
}
|
|
|
|
static TrainlogStatus program_insert_occurrence(TrainlogDatabase *database,
|
|
const char *session_id,
|
|
yyjson_val *occurrence,
|
|
size_t position) {
|
|
static const char SQL[] = "INSERT INTO program_session_entries "
|
|
"VALUES(?1,?2,?3,?4,?5,?6,?7,?8,?9,?10,?11,?12,?13,?14,?15)";
|
|
sqlite3_stmt *statement = NULL;
|
|
ProgramExerciseProfile profile;
|
|
const char *exercise_id = yyjson_get_str(yyjson_obj_get(occurrence, "exercise_id"));
|
|
int result;
|
|
|
|
if (program_load_profile(database, exercise_id, &profile) != TRAINLOG_STATUS_OK) {
|
|
return TRAINLOG_STATUS_DATABASE_ERROR;
|
|
}
|
|
result = sqlite3_prepare_v2(database->connection, SQL, -1, &statement, NULL);
|
|
if (result == SQLITE_OK) {
|
|
result = sqlite3_bind_text(statement, 1, session_id, -1, SQLITE_TRANSIENT);
|
|
}
|
|
if (result == SQLITE_OK) {
|
|
result = sqlite3_bind_text(statement,
|
|
2,
|
|
yyjson_get_str(yyjson_obj_get(occurrence, "entry_id")),
|
|
-1,
|
|
SQLITE_TRANSIENT);
|
|
}
|
|
if (result == SQLITE_OK) {
|
|
result = sqlite3_bind_int64(statement, 3, (sqlite3_int64)position);
|
|
}
|
|
if (result == SQLITE_OK) {
|
|
result = sqlite3_bind_text(statement, 4, exercise_id, -1, SQLITE_TRANSIENT);
|
|
}
|
|
if (result == SQLITE_OK) {
|
|
result = bind_json_text(statement, 5, yyjson_obj_get(occurrence, "equipment_id"));
|
|
}
|
|
if (result == SQLITE_OK) {
|
|
result = sqlite3_bind_text(statement, 6, profile.recording_mode, -1, SQLITE_TRANSIENT);
|
|
}
|
|
if (result == SQLITE_OK) {
|
|
result = sqlite3_bind_text(statement, 7, profile.tracking_mode, -1, SQLITE_TRANSIENT);
|
|
}
|
|
if (result == SQLITE_OK) {
|
|
result = sqlite3_bind_int64(statement, 8, profile.data_fields);
|
|
}
|
|
if (result == SQLITE_OK) {
|
|
result = sqlite3_bind_text(statement,
|
|
9,
|
|
yyjson_get_str(yyjson_obj_get(occurrence, "load_mode")),
|
|
-1,
|
|
SQLITE_TRANSIENT);
|
|
}
|
|
if (result == SQLITE_OK) {
|
|
result = sqlite3_bind_int64(
|
|
statement, 10, yyjson_get_sint(yyjson_obj_get(occurrence, "rest_seconds")));
|
|
}
|
|
if (result == SQLITE_OK) {
|
|
result =
|
|
bind_optional_number(statement, 11, yyjson_obj_get(occurrence, "target_sets"), false);
|
|
}
|
|
if (result == SQLITE_OK) {
|
|
result =
|
|
bind_optional_number(statement, 12, yyjson_obj_get(occurrence, "target_reps"), false);
|
|
}
|
|
if (result == SQLITE_OK) {
|
|
result = bind_optional_number(
|
|
statement, 13, yyjson_obj_get(occurrence, "target_duration_seconds"), false);
|
|
}
|
|
if (result == SQLITE_OK) {
|
|
result = bind_optional_number(
|
|
statement, 14, yyjson_obj_get(occurrence, "target_weight_kg"), true);
|
|
}
|
|
if (result == SQLITE_OK) {
|
|
result = bind_json_text(statement, 15, yyjson_obj_get(occurrence, "notes"));
|
|
}
|
|
if (result == SQLITE_OK) {
|
|
result = sqlite3_step(statement) == SQLITE_DONE ? SQLITE_OK : SQLITE_ERROR;
|
|
}
|
|
if (sqlite3_finalize(statement) != SQLITE_OK) {
|
|
result = SQLITE_ERROR;
|
|
}
|
|
return result == SQLITE_OK ? TRAINLOG_STATUS_OK : TRAINLOG_STATUS_DATABASE_ERROR;
|
|
}
|
|
|
|
static TrainlogStatus program_insert_session(TrainlogDatabase *database,
|
|
const char *program_id,
|
|
yyjson_val *session,
|
|
size_t position) {
|
|
sqlite3_stmt *statement = NULL;
|
|
yyjson_val *occurrences = yyjson_obj_get(session, "occurrences");
|
|
const char *session_id = yyjson_get_str(yyjson_obj_get(session, "program_session_id"));
|
|
size_t occurrence_index;
|
|
int result;
|
|
|
|
result = sqlite3_prepare_v2(database->connection,
|
|
"INSERT INTO program_sessions VALUES(?1,?2,?3,?4,?5,?6,?7)",
|
|
-1,
|
|
&statement,
|
|
NULL);
|
|
if (result == SQLITE_OK) {
|
|
result = sqlite3_bind_text(statement, 1, session_id, -1, SQLITE_TRANSIENT);
|
|
}
|
|
if (result == SQLITE_OK) {
|
|
result = sqlite3_bind_text(statement, 2, program_id, -1, SQLITE_TRANSIENT);
|
|
}
|
|
if (result == SQLITE_OK) {
|
|
result = sqlite3_bind_int64(statement, 3, (sqlite3_int64)position);
|
|
}
|
|
if (result == SQLITE_OK) {
|
|
result = sqlite3_bind_text(
|
|
statement, 4, yyjson_get_str(yyjson_obj_get(session, "title")), -1, SQLITE_TRANSIENT);
|
|
}
|
|
if (result == SQLITE_OK) {
|
|
result = sqlite3_bind_text(statement,
|
|
5,
|
|
yyjson_get_str(yyjson_obj_get(session, "session_type")),
|
|
-1,
|
|
SQLITE_TRANSIENT);
|
|
}
|
|
if (result == SQLITE_OK) {
|
|
result = bind_json_text(statement, 6, yyjson_obj_get(session, "planned_for"));
|
|
}
|
|
if (result == SQLITE_OK) {
|
|
result = bind_json_text(statement, 7, yyjson_obj_get(session, "note"));
|
|
}
|
|
if (result == SQLITE_OK) {
|
|
result = sqlite3_step(statement) == SQLITE_DONE ? SQLITE_OK : SQLITE_ERROR;
|
|
}
|
|
if (sqlite3_finalize(statement) != SQLITE_OK) {
|
|
result = SQLITE_ERROR;
|
|
}
|
|
if (result != SQLITE_OK) {
|
|
return TRAINLOG_STATUS_DATABASE_ERROR;
|
|
}
|
|
|
|
for (occurrence_index = 0U; occurrence_index < yyjson_arr_size(occurrences);
|
|
++occurrence_index) {
|
|
TrainlogStatus status = program_insert_occurrence(
|
|
database, session_id, yyjson_arr_get(occurrences, occurrence_index), occurrence_index);
|
|
if (status != TRAINLOG_STATUS_OK) {
|
|
return status;
|
|
}
|
|
}
|
|
return TRAINLOG_STATUS_OK;
|
|
}
|
|
|
|
static TrainlogStatus insert_program(TrainlogDatabase *database,
|
|
yyjson_val *program,
|
|
const char *digest,
|
|
const char *now,
|
|
const char *revision) {
|
|
yyjson_val *sessions = yyjson_obj_get(program, "sessions");
|
|
const char *program_id = yyjson_get_str(yyjson_obj_get(program, "program_id"));
|
|
size_t session_index;
|
|
TrainlogStatus status = program_insert_header(database, program, digest, now, revision);
|
|
|
|
if (status != TRAINLOG_STATUS_OK) {
|
|
return status;
|
|
}
|
|
for (session_index = 0U; session_index < yyjson_arr_size(sessions); ++session_index) {
|
|
status = program_insert_session(
|
|
database, program_id, yyjson_arr_get(sessions, session_index), session_index);
|
|
if (status != TRAINLOG_STATUS_OK) {
|
|
return status;
|
|
}
|
|
}
|
|
return TRAINLOG_STATUS_OK;
|
|
}
|
|
|
|
typedef enum ExistingProgramResult {
|
|
EXISTING_PROGRAM_NONE = 0,
|
|
EXISTING_PROGRAM_IDENTICAL,
|
|
EXISTING_PROGRAM_CONFLICT,
|
|
EXISTING_PROGRAM_ERROR
|
|
} ExistingProgramResult;
|
|
|
|
static ExistingProgramResult program_lookup_existing_import(TrainlogDatabase *database,
|
|
const char *program_id,
|
|
const char *digest) {
|
|
sqlite3_stmt *statement = NULL;
|
|
ExistingProgramResult outcome = EXISTING_PROGRAM_ERROR;
|
|
int result;
|
|
|
|
result = sqlite3_prepare_v2(database->connection,
|
|
"SELECT source_payload_sha256 FROM programs WHERE program_id=?1",
|
|
-1,
|
|
&statement,
|
|
NULL);
|
|
if (result != SQLITE_OK) {
|
|
return EXISTING_PROGRAM_ERROR;
|
|
}
|
|
result = sqlite3_bind_text(statement, 1, program_id, -1, SQLITE_TRANSIENT);
|
|
if (result != SQLITE_OK) {
|
|
(void)sqlite3_finalize(statement);
|
|
return EXISTING_PROGRAM_ERROR;
|
|
}
|
|
result = sqlite3_step(statement);
|
|
if (result == SQLITE_DONE) {
|
|
outcome = EXISTING_PROGRAM_NONE;
|
|
} else if (result == SQLITE_ROW) {
|
|
const char *known_digest = (const char *)sqlite3_column_text(statement, 0);
|
|
outcome = known_digest != NULL && strcmp(known_digest, digest) == 0
|
|
? EXISTING_PROGRAM_IDENTICAL
|
|
: EXISTING_PROGRAM_CONFLICT;
|
|
}
|
|
(void)sqlite3_finalize(statement);
|
|
return outcome;
|
|
}
|
|
|
|
static TrainlogStatus program_persist_import(TrainlogDatabase *database,
|
|
yyjson_val *program,
|
|
const char *digest,
|
|
const char *now,
|
|
const char *revision) {
|
|
TrainlogStatus status;
|
|
|
|
if (sqlite3_exec(database->connection, "BEGIN IMMEDIATE", NULL, NULL, NULL) != SQLITE_OK) {
|
|
return TRAINLOG_STATUS_DATABASE_ERROR;
|
|
}
|
|
status = insert_program(database, program, digest, now, revision);
|
|
if (status == TRAINLOG_STATUS_OK &&
|
|
sqlite3_exec(database->connection, "COMMIT", NULL, NULL, NULL) == SQLITE_OK) {
|
|
return TRAINLOG_STATUS_OK;
|
|
}
|
|
(void)sqlite3_exec(database->connection, "ROLLBACK", NULL, NULL, NULL);
|
|
return status == TRAINLOG_STATUS_OK ? TRAINLOG_STATUS_DATABASE_ERROR : status;
|
|
}
|
|
|
|
static TrainlogStatus program_build_import_response(yyjson_val *program,
|
|
const char *digest,
|
|
bool imported,
|
|
char **output_json,
|
|
size_t *output_size) {
|
|
yyjson_mut_doc *response = yyjson_mut_doc_new(NULL);
|
|
yyjson_mut_val *root = response == NULL ? NULL : yyjson_mut_obj(response);
|
|
yyjson_mut_val *warnings = response == NULL ? NULL : yyjson_mut_arr(response);
|
|
yyjson_val *start_date = yyjson_obj_get(program, "start_date");
|
|
yyjson_val *end_date = yyjson_obj_get(program, "end_date");
|
|
|
|
if (response == NULL || root == NULL || warnings == NULL) {
|
|
yyjson_mut_doc_free(response);
|
|
return TRAINLOG_STATUS_SYSTEM_ERROR;
|
|
}
|
|
yyjson_mut_doc_set_root(response, root);
|
|
(void)yyjson_mut_obj_add_uint(response, root, "api_version", 1U);
|
|
(void)yyjson_mut_obj_add_strcpy(
|
|
response, root, "program_id", yyjson_get_str(yyjson_obj_get(program, "program_id")));
|
|
(void)yyjson_mut_obj_add_strcpy(
|
|
response, root, "title", yyjson_get_str(yyjson_obj_get(program, "title")));
|
|
if (yyjson_is_null(start_date)) {
|
|
(void)yyjson_mut_obj_add_null(response, root, "start_date");
|
|
} else {
|
|
(void)yyjson_mut_obj_add_strcpy(response, root, "start_date", yyjson_get_str(start_date));
|
|
}
|
|
if (yyjson_is_null(end_date)) {
|
|
(void)yyjson_mut_obj_add_null(response, root, "end_date");
|
|
} else {
|
|
(void)yyjson_mut_obj_add_strcpy(response, root, "end_date", yyjson_get_str(end_date));
|
|
}
|
|
(void)yyjson_mut_obj_add_strcpy(response, root, "payload_sha256", digest);
|
|
(void)yyjson_mut_obj_add_uint(
|
|
response, root, "session_count", yyjson_arr_size(yyjson_obj_get(program, "sessions")));
|
|
(void)yyjson_mut_obj_add_uint(response, root, "unknown_exercise_count", 0U);
|
|
(void)yyjson_mut_obj_add_val(response, root, "warnings", warnings);
|
|
(void)yyjson_mut_obj_add_bool(response, root, "imported", imported);
|
|
return write_document(response, output_json, output_size);
|
|
}
|
|
|
|
TrainlogStatus trainlog_web_programs_import_json(TrainlogDatabase *database,
|
|
const char *body,
|
|
size_t body_size,
|
|
bool commit,
|
|
char **output_json,
|
|
size_t *output_size) {
|
|
yyjson_doc *input;
|
|
yyjson_val *root;
|
|
yyjson_val *program = NULL;
|
|
char digest[65];
|
|
char now[32];
|
|
char revision[80];
|
|
const char *program_id;
|
|
ExistingProgramResult existing;
|
|
TrainlogStatus status = TRAINLOG_STATUS_OK;
|
|
bool imported = false;
|
|
if (database == NULL || body == NULL || body_size == 0U ||
|
|
body_size > TRAINLOG_WEB_PROGRAM_BYTES_MAX || output_json == NULL || output_size == NULL ||
|
|
!digest_hex(body, body_size, digest) || !timestamp_now(now) ||
|
|
trainlog_id_generate("pgr", revision, sizeof(revision)) != TRAINLOG_STATUS_OK) {
|
|
return TRAINLOG_STATUS_INVALID_ARGUMENT;
|
|
}
|
|
*output_json = NULL;
|
|
*output_size = 0U;
|
|
input = yyjson_read(body, body_size, YYJSON_READ_NOFLAG);
|
|
root = input == NULL ? NULL : yyjson_doc_get_root(input);
|
|
if (!validate_program(database, root, &program)) {
|
|
yyjson_doc_free(input);
|
|
return TRAINLOG_STATUS_INVALID_ARGUMENT;
|
|
}
|
|
program_id = yyjson_get_str(yyjson_obj_get(program, "program_id"));
|
|
existing = program_lookup_existing_import(database, program_id, digest);
|
|
if (existing == EXISTING_PROGRAM_ERROR) {
|
|
status = TRAINLOG_STATUS_DATABASE_ERROR;
|
|
} else if (existing == EXISTING_PROGRAM_CONFLICT) {
|
|
status = TRAINLOG_STATUS_CONFLICT;
|
|
} else if (commit && existing == EXISTING_PROGRAM_NONE) {
|
|
status = program_persist_import(database, program, digest, now, revision);
|
|
imported = status == TRAINLOG_STATUS_OK;
|
|
}
|
|
if (status == TRAINLOG_STATUS_OK) {
|
|
status = program_build_import_response(program, digest, imported, output_json, output_size);
|
|
}
|
|
yyjson_doc_free(input);
|
|
return status;
|
|
}
|
|
|
|
static TrainlogStatus program_replay_archive(TrainlogDatabase *database,
|
|
const char *request_id,
|
|
char **output_json,
|
|
size_t *output_size,
|
|
bool *found) {
|
|
sqlite3_stmt *statement = NULL;
|
|
int result;
|
|
|
|
*found = false;
|
|
result = sqlite3_prepare_v2(database->connection,
|
|
"SELECT response_json FROM program_requests WHERE request_id=?1",
|
|
-1,
|
|
&statement,
|
|
NULL);
|
|
if (result != SQLITE_OK) {
|
|
return TRAINLOG_STATUS_DATABASE_ERROR;
|
|
}
|
|
result = sqlite3_bind_text(statement, 1, request_id, -1, SQLITE_TRANSIENT);
|
|
if (result == SQLITE_OK) {
|
|
result = sqlite3_step(statement);
|
|
}
|
|
if (result == SQLITE_ROW) {
|
|
const char *saved = (const char *)sqlite3_column_text(statement, 0);
|
|
*output_size = saved == NULL ? 0U : strlen(saved);
|
|
*output_json = saved == NULL ? NULL : strdup(saved);
|
|
*found = true;
|
|
if (*output_json == NULL) {
|
|
result = SQLITE_NOMEM;
|
|
} else {
|
|
result = SQLITE_OK;
|
|
}
|
|
} else if (result == SQLITE_DONE) {
|
|
result = SQLITE_OK;
|
|
}
|
|
(void)sqlite3_finalize(statement);
|
|
return result == SQLITE_OK ? TRAINLOG_STATUS_OK : TRAINLOG_STATUS_DATABASE_ERROR;
|
|
}
|
|
|
|
static TrainlogStatus program_apply_archive(TrainlogDatabase *database,
|
|
const char *program_id,
|
|
const char *expected_revision,
|
|
const char *revision,
|
|
const char *now) {
|
|
sqlite3_stmt *statement = NULL;
|
|
int result;
|
|
|
|
result = sqlite3_prepare_v2(database->connection,
|
|
"UPDATE programs SET state='archived',updated_at=?1,revision_id=?2 "
|
|
"WHERE program_id=?3 AND revision_id=?4 AND state='active' AND "
|
|
"deleted_at IS NULL",
|
|
-1,
|
|
&statement,
|
|
NULL);
|
|
if (result == SQLITE_OK) {
|
|
result = sqlite3_bind_text(statement, 1, now, -1, SQLITE_TRANSIENT);
|
|
}
|
|
if (result == SQLITE_OK) {
|
|
result = sqlite3_bind_text(statement, 2, revision, -1, SQLITE_TRANSIENT);
|
|
}
|
|
if (result == SQLITE_OK) {
|
|
result = sqlite3_bind_text(statement, 3, program_id, -1, SQLITE_TRANSIENT);
|
|
}
|
|
if (result == SQLITE_OK) {
|
|
result = sqlite3_bind_text(statement, 4, expected_revision, -1, SQLITE_TRANSIENT);
|
|
}
|
|
if (result == SQLITE_OK) {
|
|
result = sqlite3_step(statement) == SQLITE_DONE ? SQLITE_OK : SQLITE_ERROR;
|
|
}
|
|
if (result == SQLITE_OK && sqlite3_changes(database->connection) != 1) {
|
|
result = SQLITE_CONSTRAINT;
|
|
}
|
|
(void)sqlite3_finalize(statement);
|
|
return result == SQLITE_OK ? TRAINLOG_STATUS_OK : TRAINLOG_STATUS_CONFLICT;
|
|
}
|
|
|
|
static TrainlogStatus program_build_archive_response(const char *program_id,
|
|
const char *revision,
|
|
char **output_json,
|
|
size_t *output_size) {
|
|
yyjson_mut_doc *response = yyjson_mut_doc_new(NULL);
|
|
yyjson_mut_val *root = response == NULL ? NULL : yyjson_mut_obj(response);
|
|
if (response == NULL || root == NULL) {
|
|
yyjson_mut_doc_free(response);
|
|
return TRAINLOG_STATUS_SYSTEM_ERROR;
|
|
}
|
|
yyjson_mut_doc_set_root(response, root);
|
|
(void)yyjson_mut_obj_add_uint(response, root, "api_version", 1U);
|
|
(void)yyjson_mut_obj_add_strcpy(response, root, "program_id", program_id);
|
|
(void)yyjson_mut_obj_add_strcpy(response, root, "state", "archived");
|
|
(void)yyjson_mut_obj_add_strcpy(response, root, "revision_id", revision);
|
|
return write_document(response, output_json, output_size);
|
|
}
|
|
|
|
static TrainlogStatus program_store_archive_replay(TrainlogDatabase *database,
|
|
const char *request_id,
|
|
const char *program_id,
|
|
const char *response_json,
|
|
const char *now) {
|
|
sqlite3_stmt *statement = NULL;
|
|
int result = sqlite3_prepare_v2(database->connection,
|
|
"INSERT INTO program_requests("
|
|
"request_id,command,program_id,response_json,created_at) "
|
|
"VALUES(?1,'archive',?2,?3,?4)",
|
|
-1,
|
|
&statement,
|
|
NULL);
|
|
if (result == SQLITE_OK) {
|
|
result = sqlite3_bind_text(statement, 1, request_id, -1, SQLITE_TRANSIENT);
|
|
}
|
|
if (result == SQLITE_OK) {
|
|
result = sqlite3_bind_text(statement, 2, program_id, -1, SQLITE_TRANSIENT);
|
|
}
|
|
if (result == SQLITE_OK) {
|
|
result = sqlite3_bind_text(statement, 3, response_json, -1, SQLITE_TRANSIENT);
|
|
}
|
|
if (result == SQLITE_OK) {
|
|
result = sqlite3_bind_text(statement, 4, now, -1, SQLITE_TRANSIENT);
|
|
}
|
|
if (result == SQLITE_OK) {
|
|
result = sqlite3_step(statement) == SQLITE_DONE ? SQLITE_OK : SQLITE_ERROR;
|
|
}
|
|
(void)sqlite3_finalize(statement);
|
|
return result == SQLITE_OK ? TRAINLOG_STATUS_OK : TRAINLOG_STATUS_DATABASE_ERROR;
|
|
}
|
|
|
|
TrainlogStatus trainlog_web_programs_archive_json(TrainlogDatabase *database,
|
|
const char *program_id,
|
|
const char *expected_revision,
|
|
const char *request_id,
|
|
char **output_json,
|
|
size_t *output_size) {
|
|
char revision[80];
|
|
char now[32];
|
|
TrainlogStatus status;
|
|
bool replayed = false;
|
|
if (database == NULL || program_id == NULL || expected_revision == NULL || request_id == NULL ||
|
|
request_id[0] == '\0' || output_json == NULL || output_size == NULL ||
|
|
!timestamp_now(now) ||
|
|
trainlog_id_generate("pgr", revision, sizeof(revision)) != TRAINLOG_STATUS_OK) {
|
|
return TRAINLOG_STATUS_INVALID_ARGUMENT;
|
|
}
|
|
*output_json = NULL;
|
|
*output_size = 0U;
|
|
if (sqlite3_exec(database->connection, "BEGIN IMMEDIATE", NULL, NULL, NULL) != SQLITE_OK) {
|
|
return TRAINLOG_STATUS_DATABASE_ERROR;
|
|
}
|
|
status = program_replay_archive(database, request_id, output_json, output_size, &replayed);
|
|
if (status == TRAINLOG_STATUS_OK && replayed) {
|
|
(void)sqlite3_exec(database->connection, "COMMIT", NULL, NULL, NULL);
|
|
return TRAINLOG_STATUS_OK;
|
|
}
|
|
if (status != TRAINLOG_STATUS_OK) {
|
|
(void)sqlite3_exec(database->connection, "ROLLBACK", NULL, NULL, NULL);
|
|
return status;
|
|
}
|
|
status = program_apply_archive(database, program_id, expected_revision, revision, now);
|
|
if (status == TRAINLOG_STATUS_OK) {
|
|
status = program_build_archive_response(program_id, revision, output_json, output_size);
|
|
}
|
|
if (status == TRAINLOG_STATUS_OK) {
|
|
status = program_store_archive_replay(database, request_id, program_id, *output_json, now);
|
|
}
|
|
if (status != TRAINLOG_STATUS_OK ||
|
|
sqlite3_exec(database->connection, "COMMIT", NULL, NULL, NULL) != SQLITE_OK) {
|
|
(void)sqlite3_exec(database->connection, "ROLLBACK", NULL, NULL, NULL);
|
|
free(*output_json);
|
|
*output_json = NULL;
|
|
*output_size = 0U;
|
|
return status == TRAINLOG_STATUS_OK ? TRAINLOG_STATUS_DATABASE_ERROR : status;
|
|
}
|
|
return TRAINLOG_STATUS_OK;
|
|
}
|
|
|
|
typedef enum ProgramDeleteReplay {
|
|
PROGRAM_DELETE_REPLAY_NONE = 0,
|
|
PROGRAM_DELETE_REPLAY_EXACT,
|
|
PROGRAM_DELETE_REPLAY_CONFLICT,
|
|
PROGRAM_DELETE_REPLAY_ERROR
|
|
} ProgramDeleteReplay;
|
|
|
|
static ProgramDeleteReplay program_replay_delete(TrainlogDatabase *database,
|
|
const char *program_id,
|
|
const char *expected_revision,
|
|
const char *request_id,
|
|
char **output_json,
|
|
size_t *output_size) {
|
|
sqlite3_stmt *statement = NULL;
|
|
ProgramDeleteReplay replay = PROGRAM_DELETE_REPLAY_ERROR;
|
|
int result;
|
|
|
|
result = sqlite3_prepare_v2(database->connection,
|
|
"SELECT program_id,expected_revision,response_json FROM "
|
|
"program_deletions WHERE request_id=?1",
|
|
-1,
|
|
&statement,
|
|
NULL);
|
|
if (result == SQLITE_OK) {
|
|
result = sqlite3_bind_text(statement, 1, request_id, -1, SQLITE_TRANSIENT);
|
|
}
|
|
if (result == SQLITE_OK) {
|
|
result = sqlite3_step(statement);
|
|
}
|
|
if (result == SQLITE_ROW) {
|
|
const char *known_program = (const char *)sqlite3_column_text(statement, 0);
|
|
const char *known_revision = (const char *)sqlite3_column_text(statement, 1);
|
|
const char *saved = (const char *)sqlite3_column_text(statement, 2);
|
|
|
|
if (known_program == NULL || known_revision == NULL || saved == NULL) {
|
|
replay = PROGRAM_DELETE_REPLAY_ERROR;
|
|
} else if (strcmp(known_program, program_id) != 0 ||
|
|
strcmp(known_revision, expected_revision) != 0) {
|
|
replay = PROGRAM_DELETE_REPLAY_CONFLICT;
|
|
} else {
|
|
*output_size = strlen(saved);
|
|
*output_json = strdup(saved);
|
|
replay =
|
|
*output_json == NULL ? PROGRAM_DELETE_REPLAY_ERROR : PROGRAM_DELETE_REPLAY_EXACT;
|
|
}
|
|
} else if (result == SQLITE_DONE) {
|
|
replay = PROGRAM_DELETE_REPLAY_NONE;
|
|
}
|
|
(void)sqlite3_finalize(statement);
|
|
if (replay != PROGRAM_DELETE_REPLAY_NONE) {
|
|
return replay;
|
|
}
|
|
|
|
result = sqlite3_prepare_v2(database->connection,
|
|
"SELECT 1 FROM program_requests WHERE request_id=?1",
|
|
-1,
|
|
&statement,
|
|
NULL);
|
|
if (result == SQLITE_OK) {
|
|
result = sqlite3_bind_text(statement, 1, request_id, -1, SQLITE_TRANSIENT);
|
|
}
|
|
if (result == SQLITE_OK) {
|
|
result = sqlite3_step(statement);
|
|
}
|
|
(void)sqlite3_finalize(statement);
|
|
if (result == SQLITE_ROW) {
|
|
return PROGRAM_DELETE_REPLAY_CONFLICT;
|
|
}
|
|
return result == SQLITE_DONE ? PROGRAM_DELETE_REPLAY_NONE : PROGRAM_DELETE_REPLAY_ERROR;
|
|
}
|
|
|
|
static TrainlogStatus program_build_delete_response(const char *program_id,
|
|
const char *revision,
|
|
const char *deleted_at,
|
|
char **output_json,
|
|
size_t *output_size) {
|
|
yyjson_mut_doc *response = yyjson_mut_doc_new(NULL);
|
|
yyjson_mut_val *root = response == NULL ? NULL : yyjson_mut_obj(response);
|
|
|
|
if (response == NULL || root == NULL) {
|
|
yyjson_mut_doc_free(response);
|
|
return TRAINLOG_STATUS_SYSTEM_ERROR;
|
|
}
|
|
yyjson_mut_doc_set_root(response, root);
|
|
(void)yyjson_mut_obj_add_uint(response, root, "api_version", 1U);
|
|
(void)yyjson_mut_obj_add_strcpy(response, root, "program_id", program_id);
|
|
(void)yyjson_mut_obj_add_strcpy(response, root, "deleted_at", deleted_at);
|
|
(void)yyjson_mut_obj_add_strcpy(response, root, "revision_id", revision);
|
|
return write_document(response, output_json, output_size);
|
|
}
|
|
|
|
TrainlogStatus trainlog_web_programs_delete_json(TrainlogDatabase *database,
|
|
const char *program_id,
|
|
const char *expected_revision,
|
|
const char *request_id,
|
|
char **output_json,
|
|
size_t *output_size) {
|
|
sqlite3_stmt *statement = NULL;
|
|
ProgramDeleteReplay replay;
|
|
char revision[80];
|
|
char now[32];
|
|
TrainlogStatus status = TRAINLOG_STATUS_OK;
|
|
int result;
|
|
|
|
if (database == NULL || program_id == NULL || program_id[0] == '\0' ||
|
|
expected_revision == NULL || expected_revision[0] == '\0' || request_id == NULL ||
|
|
request_id[0] == '\0' || strlen(request_id) > 128U || output_json == NULL ||
|
|
output_size == NULL || !timestamp_now(now) ||
|
|
trainlog_id_generate("pgr", revision, sizeof(revision)) != TRAINLOG_STATUS_OK) {
|
|
return TRAINLOG_STATUS_INVALID_ARGUMENT;
|
|
}
|
|
*output_json = NULL;
|
|
*output_size = 0U;
|
|
if (sqlite3_exec(database->connection, "BEGIN IMMEDIATE", NULL, NULL, NULL) != SQLITE_OK) {
|
|
return TRAINLOG_STATUS_DATABASE_ERROR;
|
|
}
|
|
replay = program_replay_delete(
|
|
database, program_id, expected_revision, request_id, output_json, output_size);
|
|
if (replay == PROGRAM_DELETE_REPLAY_EXACT) {
|
|
(void)sqlite3_exec(database->connection, "COMMIT", NULL, NULL, NULL);
|
|
return TRAINLOG_STATUS_OK;
|
|
}
|
|
if (replay == PROGRAM_DELETE_REPLAY_CONFLICT) {
|
|
status = TRAINLOG_STATUS_CONFLICT;
|
|
} else if (replay == PROGRAM_DELETE_REPLAY_ERROR) {
|
|
status = TRAINLOG_STATUS_DATABASE_ERROR;
|
|
}
|
|
if (status == TRAINLOG_STATUS_OK) {
|
|
result =
|
|
sqlite3_prepare_v2(database->connection,
|
|
"UPDATE programs SET deleted_at=?1,updated_at=?1,revision_id=?2 "
|
|
"WHERE program_id=?3 AND revision_id=?4 AND deleted_at IS NULL",
|
|
-1,
|
|
&statement,
|
|
NULL);
|
|
if (result == SQLITE_OK) {
|
|
result = sqlite3_bind_text(statement, 1, now, -1, SQLITE_TRANSIENT);
|
|
}
|
|
if (result == SQLITE_OK) {
|
|
result = sqlite3_bind_text(statement, 2, revision, -1, SQLITE_TRANSIENT);
|
|
}
|
|
if (result == SQLITE_OK) {
|
|
result = sqlite3_bind_text(statement, 3, program_id, -1, SQLITE_TRANSIENT);
|
|
}
|
|
if (result == SQLITE_OK) {
|
|
result = sqlite3_bind_text(statement, 4, expected_revision, -1, SQLITE_TRANSIENT);
|
|
}
|
|
if (result == SQLITE_OK) {
|
|
result = sqlite3_step(statement) == SQLITE_DONE ? SQLITE_OK : SQLITE_ERROR;
|
|
}
|
|
if (result == SQLITE_OK && sqlite3_changes(database->connection) != 1) {
|
|
status = TRAINLOG_STATUS_CONFLICT;
|
|
} else if (result != SQLITE_OK) {
|
|
status = TRAINLOG_STATUS_DATABASE_ERROR;
|
|
}
|
|
(void)sqlite3_finalize(statement);
|
|
statement = NULL;
|
|
}
|
|
if (status == TRAINLOG_STATUS_OK) {
|
|
status = program_build_delete_response(program_id, revision, now, output_json, output_size);
|
|
}
|
|
if (status == TRAINLOG_STATUS_OK) {
|
|
result = sqlite3_prepare_v2(
|
|
database->connection,
|
|
"INSERT INTO program_deletions("
|
|
"program_id,request_id,expected_revision,deleted_revision,deleted_at,response_json) "
|
|
"VALUES(?1,?2,?3,?4,?5,?6)",
|
|
-1,
|
|
&statement,
|
|
NULL);
|
|
if (result == SQLITE_OK) {
|
|
result = sqlite3_bind_text(statement, 1, program_id, -1, SQLITE_TRANSIENT);
|
|
}
|
|
if (result == SQLITE_OK) {
|
|
result = sqlite3_bind_text(statement, 2, request_id, -1, SQLITE_TRANSIENT);
|
|
}
|
|
if (result == SQLITE_OK) {
|
|
result = sqlite3_bind_text(statement, 3, expected_revision, -1, SQLITE_TRANSIENT);
|
|
}
|
|
if (result == SQLITE_OK) {
|
|
result = sqlite3_bind_text(statement, 4, revision, -1, SQLITE_TRANSIENT);
|
|
}
|
|
if (result == SQLITE_OK) {
|
|
result = sqlite3_bind_text(statement, 5, now, -1, SQLITE_TRANSIENT);
|
|
}
|
|
if (result == SQLITE_OK) {
|
|
result = sqlite3_bind_text(statement, 6, *output_json, -1, SQLITE_TRANSIENT);
|
|
}
|
|
if (result == SQLITE_OK) {
|
|
result = sqlite3_step(statement) == SQLITE_DONE ? SQLITE_OK : SQLITE_ERROR;
|
|
}
|
|
if (result != SQLITE_OK) {
|
|
status = TRAINLOG_STATUS_DATABASE_ERROR;
|
|
}
|
|
(void)sqlite3_finalize(statement);
|
|
}
|
|
if (status != TRAINLOG_STATUS_OK ||
|
|
sqlite3_exec(database->connection, "COMMIT", NULL, NULL, NULL) != SQLITE_OK) {
|
|
(void)sqlite3_exec(database->connection, "ROLLBACK", NULL, NULL, NULL);
|
|
free(*output_json);
|
|
*output_json = NULL;
|
|
*output_size = 0U;
|
|
return status == TRAINLOG_STATUS_OK ? TRAINLOG_STATUS_DATABASE_ERROR : status;
|
|
}
|
|
return TRAINLOG_STATUS_OK;
|
|
}
|
|
|
|
static yyjson_val *program_find_session(yyjson_val *root, const char *program_session_id) {
|
|
yyjson_val *sessions = yyjson_obj_get(root, "sessions");
|
|
size_t index;
|
|
|
|
if (!yyjson_is_arr(sessions)) {
|
|
return NULL;
|
|
}
|
|
for (index = 0U; index < yyjson_arr_size(sessions); ++index) {
|
|
yyjson_val *candidate = yyjson_arr_get(sessions, index);
|
|
if (yyjson_equals_str(yyjson_obj_get(candidate, "program_session_id"),
|
|
program_session_id)) {
|
|
return candidate;
|
|
}
|
|
}
|
|
return NULL;
|
|
}
|
|
|
|
static yyjson_mut_val *program_copy_preparation_occurrences(yyjson_mut_doc *document,
|
|
yyjson_val *session) {
|
|
static const char *const KEYS[] = {"exercise_id",
|
|
"equipment_id",
|
|
"load_mode",
|
|
"rest_seconds",
|
|
"target_sets",
|
|
"target_reps",
|
|
"target_duration_seconds",
|
|
"target_weight_kg",
|
|
"notes"};
|
|
yyjson_val *source = yyjson_obj_get(session, "occurrences");
|
|
yyjson_mut_val *destination = yyjson_mut_arr(document);
|
|
size_t occurrence_index;
|
|
|
|
if (!yyjson_is_arr(source) || destination == NULL) {
|
|
return NULL;
|
|
}
|
|
for (occurrence_index = 0U; occurrence_index < yyjson_arr_size(source); ++occurrence_index) {
|
|
yyjson_val *source_occurrence = yyjson_arr_get(source, occurrence_index);
|
|
yyjson_mut_val *destination_occurrence = yyjson_mut_obj(document);
|
|
size_t key_index;
|
|
|
|
if (destination_occurrence == NULL) {
|
|
return NULL;
|
|
}
|
|
for (key_index = 0U; key_index < sizeof(KEYS) / sizeof(KEYS[0]); ++key_index) {
|
|
yyjson_mut_val *value =
|
|
yyjson_val_mut_copy(document, yyjson_obj_get(source_occurrence, KEYS[key_index]));
|
|
if (value == NULL ||
|
|
!yyjson_mut_obj_add_val(document, destination_occurrence, KEYS[key_index], value)) {
|
|
return NULL;
|
|
}
|
|
}
|
|
if (!yyjson_mut_arr_add_val(destination, destination_occurrence)) {
|
|
return NULL;
|
|
}
|
|
}
|
|
return destination;
|
|
}
|
|
|
|
static char *program_build_preparation_body(yyjson_val *session,
|
|
const char *program_id,
|
|
const char *program_session_id,
|
|
size_t *body_size) {
|
|
yyjson_mut_doc *command = yyjson_mut_doc_new(NULL);
|
|
yyjson_mut_val *root = command == NULL ? NULL : yyjson_mut_obj(command);
|
|
yyjson_mut_val *occurrences =
|
|
command == NULL ? NULL : program_copy_preparation_occurrences(command, session);
|
|
char *body;
|
|
|
|
if (command == NULL || root == NULL || occurrences == NULL) {
|
|
yyjson_mut_doc_free(command);
|
|
return NULL;
|
|
}
|
|
yyjson_mut_doc_set_root(command, root);
|
|
(void)yyjson_mut_obj_add_strcpy(
|
|
command, root, "title", yyjson_get_str(yyjson_obj_get(session, "title")));
|
|
(void)yyjson_mut_obj_add_strcpy(
|
|
command, root, "session_type", yyjson_get_str(yyjson_obj_get(session, "session_type")));
|
|
(void)yyjson_mut_obj_add_val(
|
|
command,
|
|
root,
|
|
"planned_for",
|
|
yyjson_val_mut_copy(command, yyjson_obj_get(session, "planned_for")));
|
|
(void)yyjson_mut_obj_add_val(
|
|
command, root, "notes", yyjson_val_mut_copy(command, yyjson_obj_get(session, "note")));
|
|
(void)yyjson_mut_obj_add_strcpy(command, root, "editing_state", "draft");
|
|
(void)yyjson_mut_obj_add_val(command, root, "occurrences", occurrences);
|
|
(void)yyjson_mut_obj_add_strcpy(command, root, "source_program_id", program_id);
|
|
(void)yyjson_mut_obj_add_strcpy(command, root, "source_program_session_id", program_session_id);
|
|
body = yyjson_mut_write(command, YYJSON_WRITE_NOFLAG, body_size);
|
|
yyjson_mut_doc_free(command);
|
|
return body;
|
|
}
|
|
|
|
TrainlogStatus trainlog_web_programs_prepare_json(TrainlogDatabase *database,
|
|
const char *program_id,
|
|
const char *program_session_id,
|
|
const char *request_id,
|
|
char **output_json,
|
|
size_t *output_size) {
|
|
char *detail = NULL;
|
|
size_t detail_size = 0U;
|
|
yyjson_doc *document;
|
|
yyjson_val *root;
|
|
yyjson_val *session;
|
|
char *body = NULL;
|
|
size_t body_size = 0U;
|
|
TrainlogStatus status;
|
|
|
|
status = trainlog_web_programs_detail_json(database, program_id, &detail, &detail_size);
|
|
if (status != TRAINLOG_STATUS_OK) {
|
|
return status;
|
|
}
|
|
document = yyjson_read(detail, detail_size, YYJSON_READ_NOFLAG);
|
|
free(detail);
|
|
root = document == NULL ? NULL : yyjson_doc_get_root(document);
|
|
if (root == NULL || !yyjson_equals_str(yyjson_obj_get(root, "state"), "active")) {
|
|
yyjson_doc_free(document);
|
|
return TRAINLOG_STATUS_CONFLICT;
|
|
}
|
|
session = root == NULL ? NULL : program_find_session(root, program_session_id);
|
|
if (session == NULL) {
|
|
yyjson_doc_free(document);
|
|
return TRAINLOG_STATUS_NOT_FOUND;
|
|
}
|
|
body = program_build_preparation_body(session, program_id, program_session_id, &body_size);
|
|
yyjson_doc_free(document);
|
|
if (body == NULL) {
|
|
return TRAINLOG_STATUS_SYSTEM_ERROR;
|
|
}
|
|
status = trainlog_web_sessions_save_json(
|
|
database, NULL, "", request_id, body, body_size, output_json, output_size);
|
|
free(body);
|
|
return status;
|
|
}
|