using System.Text;
using System.Text.Json.Nodes;
using Microsoft.Extensions.Primitives;
using Npgsql;
namespace NpgsqlRest;
public class NpgsqlRestParameter : NpgsqlParameter
{
public int Ordinal { get; private set; }
public string ConvertedName { get; internal set; }
public string ActualName { get; internal set; }
///
/// The original PostgreSQL parameter name as defined in the function/source.
/// Never changed by annotations. Used by RoutineSourceParameterFormatter for SQL generation.
///
public string OriginalName { get; init; }
///
/// Virtual parameters exist for HTTP matching and claim mapping but are NOT bound
/// to the PostgreSQL command. Created by @define_param annotation on SQL file endpoints.
///
public bool IsVirtual { get; init; }
public TypeDescriptor TypeDescriptor { get; set; }
///
/// For HTTP custom type composite parameters, the field names in order.
/// Used to build the composite text value without depending on CompositeTypeCache.
///
public string[]? CompositeFieldNames { get; set; }
public ParamType ParamType { get; set; } = default!;
public StringValues? QueryStringValues { get; set; } = null;
public JsonNode? JsonBodyNode { get; set; } = null;
public NpgsqlRestParameter? HashOf { get; set; } = null;
///
/// The original string representation of the parameter value as received from the request
/// (query string or JSON body). Used for cache key generation to ensure consistency.
///
public string? OriginalStringValue { get; set; } = null;
public bool IsUploadMetadata { get; set; } = false;
///
/// Explicit default value set by annotation (e.g., @param my_param default null).
/// null = no explicit default (use existing HasDefault/PostgreSQL behavior).
/// DBNull.Value = default is SQL NULL.
/// Any other value = the default value as a string to be bound.
///
public object? DefaultValue { get; set; } = null;
public bool IsIpAddress { get; set; } = false;
public string? UserClaim { get; set; } = null;
public bool IsUserClaims { get; set; } = false;
public bool IsFromUserClaims => UserClaim is not null || IsUserClaims is true;
public NpgsqlRestParameter(
int ordinal,
string convertedName,
string actualName,
TypeDescriptor typeDescriptor)
{
Ordinal = ordinal;
ConvertedName = convertedName;
OriginalName = actualName;
ActualName = actualName;
TypeDescriptor = typeDescriptor;
NpgsqlDbType = typeDescriptor.ActualDbType;
if (actualName is not null &&
Options.AuthenticationOptions.ParameterNameClaimsMapping.TryGetValue(actualName, out var claimName))
{
UserClaim = claimName;
}
if (actualName is not null &&
string.Equals(Options.AuthenticationOptions.IpAddressParameterName, actualName, StringComparison.OrdinalIgnoreCase))
{
IsIpAddress = true;
}
if (actualName is not null &&
string.Equals(Options.AuthenticationOptions.ClaimsJsonParameterName, actualName, StringComparison.OrdinalIgnoreCase))
{
IsUserClaims = true;
}
if (Options.UploadOptions.UseDefaultUploadMetadataParameter is true)
{
if (string.Equals(Options.UploadOptions.DefaultUploadMetadataParameterName, actualName, StringComparison.OrdinalIgnoreCase))
{
IsUploadMetadata = true;
}
}
}
private const char CacheKeySeparator = '\x1F'; // Unit Separator - non-printable ASCII character
// Null/DBNull marker, delimited by the Unit Separator (the same byte used between params). Avoids
// \x00, which is hostile across backends (Redis keys, HybridCache key validation, log collectors).
// Collision-free: a real value can never contain \x1F, so it can never produce this marker.
private const string CacheKeyNull = "\x1FNULL\x1F";
internal string GetCacheStringValue()
{
if (Value is null || Value == DBNull.Value)
{
return CacheKeyNull;
}
// Prefer original string value from request for consistency
if (OriginalStringValue is not null)
{
return OriginalStringValue;
}
// Fallback for internally-set values (user claims, IP address, etc.)
if (TypeDescriptor.IsArray)
{
// Arrays can be stored as List