using System.Text.Json; using MongoDB.Bson; namespace FozFc.Api.Application.Queries; /// /// Translates the Firestore-style constraint array the frontend sends (JSON in /// the q query param) into a Mongo . /// /// Example: [{"type":"where","field":"status","op":"==","value":"scheduled"}, /// {"type":"orderBy","field":"date","direction":"asc"},{"type":"limit","count":5}] /// public static class QueryTranslator { public static QuerySpec Parse(string? rawQuery) { if (string.IsNullOrWhiteSpace(rawQuery)) return QuerySpec.Empty; JsonElement root; try { using var doc = JsonDocument.Parse(rawQuery); root = doc.RootElement.Clone(); } catch (JsonException) { return QuerySpec.Empty; } if (root.ValueKind != JsonValueKind.Array) return QuerySpec.Empty; var filter = new BsonDocument(); var sort = new BsonDocument(); var limit = 0; foreach (var constraint in root.EnumerateArray()) { if (constraint.ValueKind != JsonValueKind.Object) continue; var type = GetString(constraint, "type"); switch (type) { case "where" when constraint.TryGetProperty("field", out var fieldEl): ApplyWhere(filter, fieldEl.GetString()!, GetString(constraint, "op", "=="), constraint.TryGetProperty("value", out var v) ? ToBson(v) : BsonNull.Value); break; case "orderBy" when constraint.TryGetProperty("field", out var orderField): sort[orderField.GetString()!] = GetString(constraint, "direction", "desc") == "asc" ? 1 : -1; break; case "limit" when constraint.TryGetProperty("count", out var count): limit = count.TryGetInt32(out var n) ? n : 0; break; } } return new QuerySpec(filter, sort, limit); } private static void ApplyWhere(BsonDocument filter, string field, string op, BsonValue value) { BsonValue clause = op switch { "==" => value, "!=" => new BsonDocument("$ne", value), ">" => new BsonDocument("$gt", value), ">=" => new BsonDocument("$gte", value), "<" => new BsonDocument("$lt", value), "<=" => new BsonDocument("$lte", value), "in" => new BsonDocument("$in", value as BsonArray ?? new BsonArray { value }), "array-contains" => value, // scalar match against an array field _ => value, }; // Merge range clauses on the same field (e.g. >= AND <). if (filter.Contains(field) && filter[field] is BsonDocument existing && clause is BsonDocument extra) { existing.Merge(extra); } else { filter[field] = clause; } } private static string GetString(JsonElement element, string property, string fallback = "") => element.TryGetProperty(property, out var value) && value.ValueKind == JsonValueKind.String ? value.GetString() ?? fallback : fallback; private static BsonValue ToBson(JsonElement element) => element.ValueKind switch { JsonValueKind.String => new BsonString(element.GetString()), JsonValueKind.Number => element.TryGetInt64(out var l) ? new BsonInt64(l) : new BsonDouble(element.GetDouble()), JsonValueKind.True => BsonBoolean.True, JsonValueKind.False => BsonBoolean.False, JsonValueKind.Null => BsonNull.Value, JsonValueKind.Array => new BsonArray(element.EnumerateArray().Select(ToBson)), _ => BsonNull.Value, }; }