From f1b9d7b9155d4e1e6c54d9233c1b78041d11fc44 Mon Sep 17 00:00:00 2001 From: Pavan Kumar Date: Mon, 23 Mar 2026 10:57:56 +0530 Subject: [PATCH] fix(CON-352): correct body serialization/deserialization for analysis and attachments Bodies stored in the TEXT column with encoding='none' were returned as JSON strings instead of their original object/array form. The fix uses the encoding field to drive both directions: stringify on write when encoding is 'none' or unset and body is not already a string; parse on read under the same condition. - Add src/utils/body-serialization.ts with serializeBody and deserializeBody helpers - Apply serialization in queries.ts addAnalysis and addAttachment - Apply deserialization in queries.ts getVCon for analysis and attachment rows - Use deserializeBody in resources/index.ts and encoding-aware logic in tenant-config.ts - Update Analysis and Attachment types: body typed as unknown to allow objects and arrays Co-Authored-By: Claude Sonnet 4.6 --- src/config/tenant-config.ts | 14 +++++++++----- src/db/queries.ts | 10 ++++++---- src/resources/index.ts | 4 ++-- src/types/vcon.ts | 4 ++-- src/utils/body-serialization.ts | 26 ++++++++++++++++++++++++++ src/utils/validation.ts | 6 +++++- 6 files changed, 50 insertions(+), 14 deletions(-) create mode 100644 src/utils/body-serialization.ts diff --git a/src/config/tenant-config.ts b/src/config/tenant-config.ts index 50af467..099c9de 100644 --- a/src/config/tenant-config.ts +++ b/src/config/tenant-config.ts @@ -52,15 +52,19 @@ export function extractTenantFromAttachment( return null; } - // Try to parse body as JSON + // Use encoding to determine how to interpret body (same logic as deserializeBody in queries.ts) + // encoding='none' (or unset): body is a native object (already deserialized on read) + // encoding='json': body is intentionally a JSON string — parse it + // encoding='base64url': not a JSON payload, cannot extract tenant let bodyData: any; try { - // Handle different encodings - if (attachment.encoding === 'json' || !attachment.encoding) { + if (attachment.encoding === 'base64url') { + return null; + } else if (attachment.encoding === 'json' && typeof attachment.body === 'string') { bodyData = JSON.parse(attachment.body); } else { - // For other encodings, try parsing anyway (might be plain JSON) - bodyData = JSON.parse(attachment.body); + // encoding='none' or unset: body was deserialized to an object on read; use directly + bodyData = attachment.body; } } catch (e) { // Not valid JSON, cannot extract tenant diff --git a/src/db/queries.ts b/src/db/queries.ts index e64022d..99b6558 100644 --- a/src/db/queries.ts +++ b/src/db/queries.ts @@ -16,9 +16,11 @@ import { ATTR_CACHE_HIT, ATTR_DB_OPERATION, ATTR_SEARCH_RESULTS_COUNT, ATTR_SEAR import { logWithContext, recordCounter, withSpan } from '../observability/instrumentation.js'; import { createLogger } from '../observability/logger.js'; import { Analysis, Attachment, Dialog, VCon } from '../types/vcon.js'; +import { deserializeBody, serializeBody } from '../utils/body-serialization.js'; const logger = createLogger('queries'); + export class VConQueries { private redis: Redis | null = null; private cacheEnabled: boolean = false; @@ -376,7 +378,7 @@ export class VConQueries { vendor: analysis.vendor, // ✅ REQUIRED field product: analysis.product, schema: analysis.schema, // ✅ CORRECT: 'schema' NOT 'schema_version' - body: analysis.body, // ✅ CORRECT: TEXT type, supports all formats + body: serializeBody(analysis.body, analysis.encoding), // Serialize only for encoding='none' encoding: analysis.encoding, url: analysis.url, content_hash: analysis.content_hash, @@ -512,7 +514,7 @@ export class VConQueries { dialog: attachment.dialog, // ✅ Added per spec Section 4.4.4 mimetype: attachment.mediatype, filename: attachment.filename, - body: attachment.body, + body: serializeBody(attachment.body, attachment.encoding), // Serialize only for encoding='none' encoding: attachment.encoding, url: attachment.url, content_hash: attachment.content_hash, @@ -697,7 +699,7 @@ export class VConQueries { vendor: a.vendor, // ✅ Required field product: a.product, schema: a.schema, // ✅ CORRECT: 'schema' NOT 'schema_version' - body: a.body, // ✅ TEXT type + body: deserializeBody(a.body, a.encoding), // Parse back to object only for encoding='none' encoding: a.encoding, url: a.url, content_hash: a.content_hash, @@ -709,7 +711,7 @@ export class VConQueries { dialog: att.dialog, // ✅ Correct field mediatype: att.mimetype, filename: att.filename, - body: att.body, + body: deserializeBody(att.body, att.encoding), // Parse back to object only for encoding='none' encoding: att.encoding, url: att.url, content_hash: att.content_hash, diff --git a/src/resources/index.ts b/src/resources/index.ts index dc9d67d..57b21c6 100644 --- a/src/resources/index.ts +++ b/src/resources/index.ts @@ -17,6 +17,7 @@ */ import { VConQueries } from '../db/queries.js'; +import { deserializeBody } from '../utils/body-serialization.js'; export interface ResourceDescriptor { uri: string; @@ -238,8 +239,7 @@ export async function resolveCoreResource(queries: VConQueries, uri: string): Pr } try { - // Parse the tags body - it should be a JSON array of "key:value" strings - const tagsArray = JSON.parse(tagsAttachment.body); + const tagsArray = deserializeBody(tagsAttachment.body as string, tagsAttachment.encoding) as string[]; const tagsObject: Record = {}; for (const tagString of tagsArray) { diff --git a/src/types/vcon.ts b/src/types/vcon.ts index 6f6a283..42825b6 100644 --- a/src/types/vcon.ts +++ b/src/types/vcon.ts @@ -137,7 +137,7 @@ export interface Attachment { dialog?: number; // ✅ Section 4.4.4 - Dialog reference mediatype?: string; filename?: string; - body?: string; + body?: unknown; // Can be string, object, or array depending on encoding encoding?: Encoding; url?: string; content_hash?: string | string[]; @@ -163,7 +163,7 @@ export interface Analysis { vendor: string; // ✅ REQUIRED per spec Section 4.5.5 (no ?) product?: string; schema?: string; // ✅ CORRECT: 'schema' NOT 'schema_version' (Section 4.5.7) - body?: string; // ✅ CORRECT: string type, supports all formats (Section 4.5.8) + body?: unknown; // Can be string, object, or array depending on content/encoding encoding?: Encoding; url?: string; content_hash?: string | string[]; diff --git a/src/utils/body-serialization.ts b/src/utils/body-serialization.ts new file mode 100644 index 0000000..a36d231 --- /dev/null +++ b/src/utils/body-serialization.ts @@ -0,0 +1,26 @@ +/** + * Body serialization utilities for vCon TEXT columns. + * + * Encoding semantics: + * 'none' or unset – body is a native JS object/array; stringify on write, parse on read. + * 'json' – body is intentionally a JSON string; leave as-is. + * 'base64url' – body is a base64url string; leave as-is. + */ + +export function serializeBody(body: unknown, encoding?: string): unknown { + if ((!encoding || encoding === 'none') && typeof body !== 'string') { + return JSON.stringify(body); + } + return body; +} + +export function deserializeBody(body: string, encoding?: string): unknown { + if (!encoding || encoding === 'none') { + try { + return JSON.parse(body); + } catch { + // Not valid JSON — return as-is + } + } + return body; +} diff --git a/src/utils/validation.ts b/src/utils/validation.ts index 0b033a9..65b5fda 100644 --- a/src/utils/validation.ts +++ b/src/utils/validation.ts @@ -170,7 +170,11 @@ export class VConValidator { // If body and encoding are present, validate they match if (analysis.body && analysis.encoding === 'json') { try { - JSON.parse(analysis.body); + // body may already be a parsed object or a JSON string + if (typeof analysis.body === 'string') { + JSON.parse(analysis.body); + } + // If it's already an object, it's valid JSON } catch (e) { this.errors.push( `Analysis ${index} has encoding='json' but body is not valid JSON`