From 1aaa111c9aa9cb4c70d68f64b83cd9739fcfc534 Mon Sep 17 00:00:00 2001 From: Sai Asish Y Date: Sun, 18 Oct 2026 12:13:43 -0700 Subject: [PATCH 1/2] fix(web): reject a record array cell that is missing its closing parenthesis Signed-off-by: Sai Asish Y --- apps/web/src/utils/__tests__/upload.test.ts | 7 +++++++ apps/web/src/utils/upload.ts | 2 +- 2 files changed, 8 insertions(+), 1 deletion(-) diff --git a/apps/web/src/utils/__tests__/upload.test.ts b/apps/web/src/utils/__tests__/upload.test.ts index 46fdc942a..26c5f72ee 100644 --- a/apps/web/src/utils/__tests__/upload.test.ts +++ b/apps/web/src/utils/__tests__/upload.test.ts @@ -536,6 +536,13 @@ describe('Zod3', () => { it('should leave out a field the record does not mention', async () => { await expect(processItems('RECORD_ARRAY(name:item1)')).resolves.toMatchObject([{ items: [{ name: 'item1' }] }]); }); + + it('should reject a record array missing its closing parenthesis, rather than dropping its last character', async () => { + await expectRejection( + processItems('RECORD_ARRAY(name:item1,quantity:12'), + "Syntax error in RECORD_ARRAY declaration: RECORD_ARRAY(name:item1,quantity:12 at column name: 'items' and row number '1'" + ); + }); }); }); }); diff --git a/apps/web/src/utils/upload.ts b/apps/web/src/utils/upload.ts index 56240b564..f08dd7291 100644 --- a/apps/web/src/utils/upload.ts +++ b/apps/web/src/utils/upload.ts @@ -141,7 +141,7 @@ function extractRecordArrayEntry(entry: string): { [key: string]: string }[] { entry = entry.trim(); const opening = 'RECORD_ARRAY('; const closing = ')'; - if (!entry.startsWith(opening) && !entry.endsWith(closing)) { + if (!entry.startsWith(opening) || !entry.endsWith(closing)) { throw new UploadError({ en: `Syntax error in RECORD_ARRAY declaration: ${entry}`, fr: `Erreur de syntaxe dans la déclaration RECORD_ARRAY : ${entry}` From 2ffc557309309c1eea4b179a675702c4ae67befd Mon Sep 17 00:00:00 2001 From: Sai Asish Y Date: Sun, 18 Oct 2026 12:13:45 -0700 Subject: [PATCH 2/2] fix(web): split each record array field on its first colon only Signed-off-by: Sai Asish Y --- apps/web/src/utils/__tests__/upload.test.ts | 6 ++++++ apps/web/src/utils/upload.ts | 8 +++++--- 2 files changed, 11 insertions(+), 3 deletions(-) diff --git a/apps/web/src/utils/__tests__/upload.test.ts b/apps/web/src/utils/__tests__/upload.test.ts index 26c5f72ee..869463600 100644 --- a/apps/web/src/utils/__tests__/upload.test.ts +++ b/apps/web/src/utils/__tests__/upload.test.ts @@ -543,6 +543,12 @@ describe('Zod3', () => { "Syntax error in RECORD_ARRAY declaration: RECORD_ARRAY(name:item1,quantity:12 at column name: 'items' and row number '1'" ); }); + + it('should keep a colon inside a record array value, splitting each field on its first colon only', async () => { + await expect(processItems('RECORD_ARRAY(name:10:30,quantity:1;)')).resolves.toMatchObject([ + { items: [{ name: '10:30', quantity: 1 }] } + ]); + }); }); }); }); diff --git a/apps/web/src/utils/upload.ts b/apps/web/src/utils/upload.ts index f08dd7291..6e3d1f714 100644 --- a/apps/web/src/utils/upload.ts +++ b/apps/web/src/utils/upload.ts @@ -165,15 +165,17 @@ function extractRecordArrayEntry(entry: string): { [key: string]: string }[] { } const records: { [key: string]: string } = {}; recordsList.forEach((rawRecord, i) => { - const [recordKey, recordValue] = rawRecord.split(':').map((s) => s.trim()); - if (!recordKey) { + const separatorIndex = rawRecord.indexOf(':'); + const recordKey = rawRecord.slice(0, separatorIndex).trim(); + const recordValue = rawRecord.slice(separatorIndex + 1).trim(); + if (separatorIndex === -1 || !recordKey) { throw new UploadError({ en: `Malformed record at index ${i}`, fr: `Enregistrement mal formé à l'index ${i}` }); } //recordValue can be empty if the value is optional - records[recordKey] = recordValue!; + records[recordKey] = recordValue; }); return records; });