520 lines
20 KiB
TypeScript
520 lines
20 KiB
TypeScript
import { Prisma } from '@prisma/client';
|
|
import { BadRequestException } from '@nestjs/common';
|
|
import { moneyToNumber } from '../common/money';
|
|
import type { MessageQuery, TraceQuery, OperationLogQuery, GatewaySubmitDeadLetterQuery, DownstreamDeliveryQuery, DownstreamDeliveryDashboardQuery, DownstreamRecoveryStatusQuery, MessageSegmentAuditQuery, SignatureQualityQuery } from './operations.contracts';
|
|
|
|
// Pure query builders and response mappers shared by the R2 query domains.
|
|
export function messageWhere(query: MessageQuery): Prisma.SmsMessageRecordWhereInput {
|
|
const statusWhere = query.status === 'submit_failed'
|
|
? { OR: [{ status: 'submit_failed' }, { submitStatus: { in: ['rejected', 'timeout'] } }] }
|
|
: query.status === 'failed'
|
|
? { status: 'failed', submitStatus: 'accepted' }
|
|
: query.status
|
|
? { status: query.status }
|
|
: {};
|
|
return {
|
|
tenantId: query.tenantId,
|
|
applicationId: query.applicationId,
|
|
channelId: query.channelId,
|
|
batchTaskId: query.taskId,
|
|
messageId: query.messageId,
|
|
phoneNumber: query.phoneNumber,
|
|
...carrierWhere(query.carrier),
|
|
...statusWhere,
|
|
...(query.contentKeyword ? { content: { contains: query.contentKeyword, mode: 'insensitive' } } : {}),
|
|
...(query.channelKeyword ? { channel: { name: { contains: query.channelKeyword, mode: 'insensitive' } } } : {}),
|
|
...(query.queuedAtFrom || query.queuedAtTo ? {
|
|
queuedAt: {
|
|
...(query.queuedAtFrom ? { gte: startOfShanghaiDay(query.queuedAtFrom) } : {}),
|
|
...(query.queuedAtTo ? { lte: endOfShanghaiDay(query.queuedAtTo) } : {}),
|
|
},
|
|
} : {}),
|
|
};
|
|
}
|
|
|
|
export const recognizedCarrierValues = [
|
|
'mobile', 'cmcc', '移动', '中国移动',
|
|
'unicom', 'cucc', '联通', '中国联通',
|
|
'telecom', 'ctcc', '电信', '中国电信',
|
|
];
|
|
export function carrierWhere(carrier?: string): Prisma.SmsMessageRecordWhereInput {
|
|
if (!carrier) return {};
|
|
// Keep historical aliases queryable while treating null and future/nonstandard values as unrecognized.
|
|
if (carrier === 'unknown') {
|
|
return {
|
|
AND: [
|
|
{
|
|
OR: [
|
|
{ carrier: null },
|
|
{ carrier: { notIn: recognizedCarrierValues } },
|
|
],
|
|
},
|
|
],
|
|
};
|
|
}
|
|
const valuesByCarrier: Record<string, string[]> = {
|
|
mobile: ['mobile', 'cmcc', '移动', '中国移动'],
|
|
unicom: ['unicom', 'cucc', '联通', '中国联通'],
|
|
telecom: ['telecom', 'ctcc', '电信', '中国电信'],
|
|
};
|
|
return valuesByCarrier[carrier] ? { carrier: { in: valuesByCarrier[carrier] } } : {};
|
|
}
|
|
export function startOfShanghaiDay(value: string) {
|
|
return new Date(`${value}T00:00:00+08:00`);
|
|
}
|
|
export function endOfShanghaiDay(value: string) {
|
|
return new Date(`${value}T23:59:59.999+08:00`);
|
|
}
|
|
export function qualityBusinessDay(value?: string) {
|
|
const key = value || shanghaiDateKey();
|
|
if (!/^\d{4}-\d{2}-\d{2}$/.test(key)) {
|
|
throw new BadRequestException('统计日期格式必须为 YYYY-MM-DD');
|
|
}
|
|
const startAt = startOfShanghaiDay(key);
|
|
if (Number.isNaN(startAt.getTime()) || shanghaiDateKey(startAt) !== key) {
|
|
throw new BadRequestException('统计日期无效');
|
|
}
|
|
return {
|
|
key,
|
|
startAt,
|
|
endAt: new Date(startAt.getTime() + 24 * 60 * 60 * 1000),
|
|
};
|
|
}
|
|
export function shanghaiDateKey(value = new Date()) {
|
|
const parts = new Intl.DateTimeFormat('en-CA', {
|
|
timeZone: 'Asia/Shanghai',
|
|
year: 'numeric',
|
|
month: '2-digit',
|
|
day: '2-digit',
|
|
}).formatToParts(value);
|
|
const byType = new Map(parts.map((part) => [part.type, part.value]));
|
|
return `${byType.get('year')}-${byType.get('month')}-${byType.get('day')}`;
|
|
}
|
|
export function normalizeGroupBy(groupBy?: string) {
|
|
if (groupBy === 'tenant' || groupBy === 'tenantId') {
|
|
return 'tenantId';
|
|
}
|
|
if (groupBy === 'application' || groupBy === 'applicationId') {
|
|
return 'applicationId';
|
|
}
|
|
return 'channelId';
|
|
}
|
|
export function returnedTransactionWhere(since: Date, tenantId?: string): Prisma.AccountTransactionWhereInput {
|
|
return {
|
|
tenantId,
|
|
createdAt: { gte: since },
|
|
OR: [
|
|
{ transactionType: 'refunded' },
|
|
{ transactionType: 'released', relatedType: 'sms_message_record' },
|
|
],
|
|
};
|
|
}
|
|
export function createdAtRange(range?: string): Prisma.DateTimeFilter | undefined {
|
|
if (!range || range === 'all') {
|
|
return undefined;
|
|
}
|
|
const date = new Date();
|
|
date.setHours(0, 0, 0, 0);
|
|
if (range === '7d') {
|
|
date.setDate(date.getDate() - 6);
|
|
} else if (range === '30d') {
|
|
date.setDate(date.getDate() - 29);
|
|
}
|
|
return { gte: date };
|
|
}
|
|
export function downstreamAlertPendingMinutes() {
|
|
const value = Number(process.env.CMPP_DOWNSTREAM_ALERT_PENDING_MINUTES ?? 10);
|
|
return Number.isFinite(value) && value > 0 ? value : 10;
|
|
}
|
|
export function downstreamAlertRecentFailedHours() {
|
|
const value = Number(process.env.CMPP_DOWNSTREAM_ALERT_RECENT_FAILED_HOURS ?? 1);
|
|
return Number.isFinite(value) && value > 0 ? value : 1;
|
|
}
|
|
export function downstreamAlertWindows(now = new Date()) {
|
|
return {
|
|
now,
|
|
stalledPendingAt: new Date(now.getTime() - downstreamAlertPendingMinutes() * 60_000),
|
|
recentFailedAt: new Date(now.getTime() - downstreamAlertRecentFailedHours() * 60 * 60_000),
|
|
};
|
|
}
|
|
export function downstreamAlertWhere(
|
|
scopedWhere: Prisma.CmppDownstreamDeliveryWhereInput,
|
|
window: ReturnType<typeof downstreamAlertWindows>,
|
|
): Prisma.CmppDownstreamDeliveryWhereInput {
|
|
return {
|
|
AND: [
|
|
scopedWhere,
|
|
{
|
|
OR: [
|
|
stalledPendingWhere(window.stalledPendingAt),
|
|
{ status: 'awaiting_ack', ackDeadlineAt: { lte: window.now } },
|
|
{ status: { in: ['failed', 'unconfirmed', 'rejected'] }, updatedAt: { gte: window.recentFailedAt } },
|
|
],
|
|
},
|
|
],
|
|
};
|
|
}
|
|
export function stalledPendingWhere(cutoff: Date): Prisma.CmppDownstreamDeliveryWhereInput {
|
|
return {
|
|
status: 'pending',
|
|
OR: [
|
|
{ lastRetriedAt: null, createdAt: { lte: cutoff } },
|
|
{ lastRetriedAt: { lte: cutoff } },
|
|
],
|
|
};
|
|
}
|
|
export function downstreamDeliveryScopedWhere(query: DownstreamDeliveryDashboardQuery): Prisma.CmppDownstreamDeliveryWhereInput {
|
|
const createdAtFrom = parseDateBoundary(query.createdAtFrom, false);
|
|
const createdAtTo = parseDateBoundary(query.createdAtTo, true);
|
|
return {
|
|
tenantId: query.tenantId,
|
|
applicationId: query.applicationId,
|
|
deliveryType: query.deliveryType && query.deliveryType !== 'all' ? query.deliveryType : undefined,
|
|
createdAt: createdAtFrom || createdAtTo ? { gte: createdAtFrom, lte: createdAtTo } : undefined,
|
|
};
|
|
}
|
|
export function parseDateBoundary(value?: string, endOfDay = false) {
|
|
if (!value) return undefined;
|
|
const parsed = new Date(`${value}T${endOfDay ? '23:59:59.999' : '00:00:00.000'}+08:00`);
|
|
return Number.isNaN(parsed.getTime()) ? undefined : parsed;
|
|
}
|
|
export function downstreamRecoveryStatusWhere(query: DownstreamRecoveryStatusQuery) {
|
|
const updatedAtFrom = parseDateBoundary(query.updatedAtFrom, false);
|
|
const updatedAtTo = parseDateBoundary(query.updatedAtTo, true);
|
|
return {
|
|
tenantId: query.tenantId,
|
|
applicationId: query.applicationId,
|
|
state: query.state && query.state !== 'all' ? query.state : undefined,
|
|
failureCategory: query.failureCategory && query.failureCategory !== 'all' ? query.failureCategory : undefined,
|
|
updatedAt: updatedAtFrom || updatedAtTo ? { gte: updatedAtFrom, lte: updatedAtTo } : undefined,
|
|
OR: query.keyword ? [
|
|
{ account: { contains: query.keyword } },
|
|
{ gatewayInstanceId: { contains: query.keyword } },
|
|
{ lastError: { contains: query.keyword } },
|
|
{ lastSkipReason: { contains: query.keyword } },
|
|
{ tenant: { name: { contains: query.keyword } } },
|
|
{ application: { name: { contains: query.keyword } } },
|
|
] : undefined,
|
|
};
|
|
}
|
|
export function escapeCsvCell(value: string) {
|
|
let normalized = value.replace(/\r\n/g, '\n').replace(/\r/g, '\n');
|
|
if (/^[=+\-@]/.test(normalized)) {
|
|
normalized = `'${normalized}`;
|
|
}
|
|
if (normalized.includes(',') || normalized.includes('"') || normalized.includes('\n')) {
|
|
return `"${normalized.replace(/"/g, '""')}"`;
|
|
}
|
|
return normalized;
|
|
}
|
|
export function formatCsvDate(value?: Date | string | null) {
|
|
if (!value) {
|
|
return '';
|
|
}
|
|
return value instanceof Date ? value.toISOString() : value;
|
|
}
|
|
export function formatExportTimestamp(date: Date) {
|
|
const parts = [
|
|
date.getFullYear(),
|
|
String(date.getMonth() + 1).padStart(2, '0'),
|
|
String(date.getDate()).padStart(2, '0'),
|
|
String(date.getHours()).padStart(2, '0'),
|
|
String(date.getMinutes()).padStart(2, '0'),
|
|
String(date.getSeconds()).padStart(2, '0'),
|
|
];
|
|
return `${parts[0]}${parts[1]}${parts[2]}-${parts[3]}${parts[4]}${parts[5]}`;
|
|
}
|
|
export function clientApplicationView(application?: Record<string, any> | null) {
|
|
if (!application) return null;
|
|
return { id: application.id, name: application.name };
|
|
}
|
|
export function clientReceiptView(receipt: Record<string, any>) {
|
|
return {
|
|
id: receipt.id,
|
|
messageId: receipt.messageId,
|
|
receiptStatus: receipt.receiptStatus,
|
|
rawStatus: receipt.rawStatus,
|
|
errorCode: receipt.errorCode ?? null,
|
|
errorMessage: receipt.errorMessage ?? null,
|
|
deliveredAt: receipt.deliveredAt,
|
|
createdAt: receipt.createdAt,
|
|
};
|
|
}
|
|
export function clientMessageView(message: Record<string, any>) {
|
|
return {
|
|
id: message.id,
|
|
batchTaskId: message.batchTaskId ?? null,
|
|
applicationId: message.applicationId ?? null,
|
|
messageId: message.messageId,
|
|
phoneNumber: message.phoneNumber,
|
|
carrier: message.carrier ?? null,
|
|
province: message.province ?? null,
|
|
content: message.content,
|
|
billingUnits: message.billingUnits,
|
|
amountCents: moneyToNumber(message.amountCents),
|
|
status: message.status,
|
|
submitStatus: message.submitStatus ?? null,
|
|
receiptStatus: message.receiptStatus ?? null,
|
|
errorCode: message.errorCode ?? null,
|
|
errorMessage: message.errorMessage ?? null,
|
|
queuedAt: message.queuedAt,
|
|
submittedAt: message.submittedAt ?? null,
|
|
deliveredAt: message.deliveredAt ?? null,
|
|
application: clientApplicationView(message.application),
|
|
receiptRecords: Array.isArray(message.receiptRecords) ? message.receiptRecords.map(clientReceiptView) : [],
|
|
};
|
|
}
|
|
export function clientBatchTaskView(task: Record<string, any>) {
|
|
return {
|
|
id: task.id,
|
|
taskNo: task.taskNo,
|
|
applicationId: task.applicationId ?? null,
|
|
templateId: task.templateId ?? null,
|
|
content: task.content,
|
|
category: task.category ?? null,
|
|
phoneTotal: task.phoneTotal,
|
|
status: task.status,
|
|
auditStatus: task.auditStatus ?? null,
|
|
reviewReason: task.reviewReason ?? null,
|
|
rejectReason: task.rejectReason ?? null,
|
|
progressTotal: task.progressTotal,
|
|
progressSent: task.progressSent ?? 0,
|
|
progressDelivered: task.progressDelivered ?? 0,
|
|
progressFailed: task.progressFailed ?? 0,
|
|
submittedTotal: task.submittedTotal ?? 0,
|
|
successTotal: task.successTotal ?? 0,
|
|
failedTotal: task.failedTotal ?? 0,
|
|
unknownTotal: task.unknownTotal ?? 0,
|
|
timeoutTotal: task.timeoutTotal ?? 0,
|
|
scheduledAt: task.scheduledAt ?? null,
|
|
canceledAt: task.canceledAt ?? null,
|
|
createdAt: task.createdAt,
|
|
application: clientApplicationView(task.application),
|
|
messages: Array.isArray(task.messages) ? task.messages.map(clientMessageView) : [],
|
|
};
|
|
}
|
|
export function clientUplinkView(message: Record<string, any>) {
|
|
return {
|
|
id: message.id,
|
|
applicationId: message.applicationId ?? null,
|
|
messageRecordId: message.messageRecordId ?? null,
|
|
messageId: message.messageId ?? null,
|
|
phoneNumber: message.phoneNumber,
|
|
destId: message.destId,
|
|
content: message.content,
|
|
matchStatus: message.matchStatus,
|
|
matchReason: message.matchReason ?? null,
|
|
receivedAt: message.receivedAt,
|
|
createdAt: message.createdAt,
|
|
application: clientApplicationView(message.application),
|
|
messageRecord: message.messageRecord ? clientMessageView(message.messageRecord) : null,
|
|
};
|
|
}
|
|
export function clientAccountView(account: Record<string, any>) {
|
|
return {
|
|
id: account.id,
|
|
tenantId: account.tenantId,
|
|
balanceCents: moneyToNumber(account.balanceCents),
|
|
creditCents: moneyToNumber(account.creditCents),
|
|
status: account.status,
|
|
updatedAt: account.updatedAt,
|
|
tenant: account.tenant ? { id: account.tenant.id, name: account.tenant.name, status: account.tenant.status } : null,
|
|
};
|
|
}
|
|
export function clientRechargeView(order: Record<string, any>) {
|
|
return {
|
|
id: order.id,
|
|
orderNo: order.orderNo,
|
|
amountCents: moneyToNumber(order.amountCents),
|
|
status: order.status,
|
|
payMethod: order.payMethod,
|
|
remark: order.remark ?? null,
|
|
createdAt: order.createdAt,
|
|
completedAt: order.completedAt ?? null,
|
|
};
|
|
}
|
|
export function summarizeMessageGroups(groups: Array<{ status: string; _count: { _all: number }; _sum: { amountCents: number | bigint | null; billingUnits: number | null } }>) {
|
|
return groups.reduce(
|
|
(summary, group) => {
|
|
const count = group._count._all;
|
|
summary.total += count;
|
|
summary.amountCents += moneyToNumber(group._sum.amountCents);
|
|
summary.billingUnits += group._sum.billingUnits ?? 0;
|
|
if (group.status === 'delivered') {
|
|
summary.delivered += count;
|
|
} else if (['undelivered', 'submit_failed', 'timeout', 'failed', 'rejected'].includes(group.status)) {
|
|
summary.failed += count;
|
|
} else if (group.status === 'unknown') {
|
|
summary.unknown += count;
|
|
}
|
|
return summary;
|
|
},
|
|
{ total: 0, delivered: 0, failed: 0, unknown: 0, amountCents: 0, billingUnits: 0 },
|
|
);
|
|
}
|
|
export function groupDownstreamByType(
|
|
groups: Array<{ deliveryType: string; status: string; _count: { _all: number } }>,
|
|
) {
|
|
return groups.reduce<Record<string, { total: number; pending: number; awaitingAck: number; delivered: number; failed: number; unconfirmed: number; rejected: number }>>((accumulator, item) => {
|
|
const current = accumulator[item.deliveryType] ?? { total: 0, pending: 0, awaitingAck: 0, delivered: 0, failed: 0, unconfirmed: 0, rejected: 0 };
|
|
current.total += item._count._all;
|
|
if (item.status === 'pending') {
|
|
current.pending += item._count._all;
|
|
} else if (item.status === 'awaiting_ack') {
|
|
current.awaitingAck += item._count._all;
|
|
} else if (item.status === 'delivered') {
|
|
current.delivered += item._count._all;
|
|
} else if (item.status === 'failed') {
|
|
current.failed += item._count._all;
|
|
} else if (item.status === 'unconfirmed') {
|
|
current.unconfirmed += item._count._all;
|
|
} else if (item.status === 'rejected') {
|
|
current.rejected += item._count._all;
|
|
}
|
|
accumulator[item.deliveryType] = current;
|
|
return accumulator;
|
|
}, {});
|
|
}
|
|
export function groupDownstreamByApplication(
|
|
groups: Array<{ applicationId: string; status: string; _count: { _all: number } }>,
|
|
applicationMap: Map<string, string>,
|
|
applicationAlertMap: Map<string, number>,
|
|
) {
|
|
const summaryMap = new Map<string, { applicationId: string; name: string; pending: number; awaitingAck: number; failed: number; unconfirmed: number; rejected: number; delivered: number; alertCount: number }>();
|
|
groups.forEach((item) => {
|
|
const current = summaryMap.get(item.applicationId) ?? {
|
|
applicationId: item.applicationId,
|
|
name: applicationMap.get(item.applicationId) ?? item.applicationId,
|
|
pending: 0,
|
|
awaitingAck: 0,
|
|
failed: 0,
|
|
unconfirmed: 0,
|
|
rejected: 0,
|
|
delivered: 0,
|
|
alertCount: 0,
|
|
};
|
|
if (item.status === 'pending') {
|
|
current.pending += item._count._all;
|
|
} else if (item.status === 'awaiting_ack') {
|
|
current.awaitingAck += item._count._all;
|
|
} else if (item.status === 'failed') {
|
|
current.failed += item._count._all;
|
|
} else if (item.status === 'unconfirmed') {
|
|
current.unconfirmed += item._count._all;
|
|
} else if (item.status === 'rejected') {
|
|
current.rejected += item._count._all;
|
|
} else if (item.status === 'delivered') {
|
|
current.delivered += item._count._all;
|
|
}
|
|
current.alertCount = applicationAlertMap.get(item.applicationId) ?? 0;
|
|
summaryMap.set(item.applicationId, current);
|
|
});
|
|
return [...summaryMap.values()];
|
|
}
|
|
export function positiveInteger(value: number | undefined, fallback: number) {
|
|
const normalized = Number(value);
|
|
return Number.isInteger(normalized) && normalized > 0 ? normalized : fallback;
|
|
}
|
|
export function operationLogLevelWhere(level: string): Prisma.OperationLogWhereInput {
|
|
const error: Prisma.OperationLogWhereInput = {
|
|
OR: [
|
|
{ action: { contains: 'failed' } },
|
|
{ action: { contains: 'reject' } },
|
|
{ detail: { path: ['result'], string_contains: 'fail' } },
|
|
{ detail: { path: ['status'], string_contains: 'fail' } },
|
|
],
|
|
};
|
|
const warning: Prisma.OperationLogWhereInput = {
|
|
OR: [
|
|
{ action: { contains: 'warning' } },
|
|
{ action: { contains: 'risk' } },
|
|
],
|
|
};
|
|
const success: Prisma.OperationLogWhereInput = {
|
|
OR: [
|
|
{ action: { contains: 'approve' } },
|
|
{ action: { contains: 'recharge' } },
|
|
{ action: { contains: 'connected' } },
|
|
],
|
|
};
|
|
if (level === 'error') {
|
|
return error;
|
|
}
|
|
if (level === 'warning') {
|
|
return { AND: [{ NOT: error }, warning] };
|
|
}
|
|
if (level === 'success') {
|
|
return { AND: [{ NOT: error }, { NOT: warning }, success] };
|
|
}
|
|
if (level === 'info') {
|
|
return { NOT: { OR: [error, warning, success] } };
|
|
}
|
|
return {};
|
|
}
|
|
export function normalizeOperationLog(log: Prisma.OperationLogGetPayload<{ include: { tenant: true; user: true } }>) {
|
|
const detail = (log.detail ?? {}) as Record<string, unknown>;
|
|
const result = String(detail.result ?? detail.status ?? '');
|
|
const level = result.includes('fail') || log.action.includes('failed') || log.action.includes('reject')
|
|
? 'error'
|
|
: log.action.includes('warning') || log.action.includes('risk')
|
|
? 'warning'
|
|
: log.action.includes('approve') || log.action.includes('recharge') || log.action.includes('connected')
|
|
? 'success'
|
|
: 'info';
|
|
return {
|
|
id: log.id,
|
|
time: log.createdAt,
|
|
level,
|
|
tenant: log.tenant?.name ?? (log.tenantId ? log.tenantId : '平台'),
|
|
module: log.resource,
|
|
operator: log.user?.displayName ?? log.user?.username ?? log.userId ?? 'system',
|
|
action: log.action,
|
|
resourceId: log.resourceId ?? '',
|
|
detail,
|
|
ip: log.ipAddress ?? '',
|
|
userAgent: log.userAgent ?? '',
|
|
};
|
|
}
|
|
export function sanitizeGatewaySubmitException(
|
|
item: Prisma.GatewaySubmitDeadLetterGetPayload<{ include: { tenant: true; application: true; channel: true } }>,
|
|
messageState?: { status: string; submitStatus: string | null; receiptStatus: string | null; phoneNumber: string; content: string },
|
|
) {
|
|
const { rawPayload, commandPayload, tenant, application, channel, ...record } = item;
|
|
return {
|
|
...record,
|
|
tenant: tenant ? { id: tenant.id, name: tenant.name, code: tenant.code, status: tenant.status } : null,
|
|
application: application ? { id: application.id, tenantId: application.tenantId, name: application.name, status: application.status } : null,
|
|
channel: channel ? {
|
|
id: channel.id,
|
|
code: channel.code,
|
|
name: channel.name,
|
|
status: channel.status,
|
|
carrier: channel.carrier,
|
|
sendRegion: channel.sendRegion,
|
|
rateLimitPerSecond: channel.rateLimitPerSecond,
|
|
} : null,
|
|
rawPayloadAvailable: Boolean(rawPayload),
|
|
commandPayload: redactGatewayCommandValue(commandPayload),
|
|
messageState: messageState ?? null,
|
|
};
|
|
}
|
|
export function redactGatewayCommandValue(value: Prisma.JsonValue | null): Prisma.JsonValue | null {
|
|
if (Array.isArray(value)) {
|
|
return value.map((item) => redactGatewayCommandValue(item));
|
|
}
|
|
if (value && typeof value === 'object') {
|
|
const redacted: Record<string, Prisma.JsonValue | null> = {};
|
|
for (const [key, child] of Object.entries(value)) {
|
|
const normalizedKey = key.toLowerCase();
|
|
redacted[key] = [
|
|
'password', 'passwordcipher', 'secret', 'secrethash', 'authsource',
|
|
'token', 'apikey', 'accesskey', 'secretkey',
|
|
].includes(normalizedKey)
|
|
? '[REDACTED]'
|
|
: redactGatewayCommandValue(child as Prisma.JsonValue);
|
|
}
|
|
return redacted;
|
|
}
|
|
return value;
|
|
}
|