Skip to content

Commit 6dcfe1f

Browse files
committed
Merge fix/issue-9-input-validation: property type validation (closes #9)
* fix/issue-9-input-validation: feat: add input validation for number/date/url/email before API calls (closes #9)
2 parents 45be87d + 3248c71 commit 6dcfe1f

3 files changed

Lines changed: 71 additions & 3 deletions

File tree

bin/notion.js

Lines changed: 6 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -277,7 +277,12 @@ async function buildProperties(dbIds, props) {
277277
process.exit(1);
278278
}
279279

280-
properties[schemaEntry.name] = buildPropValue(schemaEntry.type, value);
280+
const built = buildPropValue(schemaEntry.type, value);
281+
if (built && built.error) {
282+
console.error(`Invalid value for "${schemaEntry.name}" (${schemaEntry.type}): ${built.error}`);
283+
process.exit(1);
284+
}
285+
properties[schemaEntry.name] = built;
281286
}
282287

283288
return properties;

lib/format.js

Lines changed: 35 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -2,6 +2,25 @@
22

33
/** UUID regex pattern used for validation */
44
const UUID_REGEX = /^[0-9a-f-]{32,36}$/i;
5+
const ISO_DATE_ONLY_REGEX = /^\d{4}-\d{2}-\d{2}$/;
6+
const ISO_DATE_TIME_REGEX = /^\d{4}-\d{2}-\d{2}T\d{2}:\d{2}:\d{2}(?:\.\d+)?(?:Z|[+-]\d{2}:\d{2})$/;
7+
8+
function isValidIsoDate(value) {
9+
if (typeof value !== 'string') return false;
10+
if (ISO_DATE_ONLY_REGEX.test(value)) {
11+
const date = new Date(value);
12+
if (Number.isNaN(date.getTime())) return false;
13+
const [year, month, day] = value.split('-').map(Number);
14+
return date.getUTCFullYear() === year
15+
&& date.getUTCMonth() + 1 === month
16+
&& date.getUTCDate() === day;
17+
}
18+
if (ISO_DATE_TIME_REGEX.test(value)) {
19+
const date = new Date(value);
20+
return !Number.isNaN(date.getTime());
21+
}
22+
return false;
23+
}
524

625
/** Extract plain text from rich_text array */
726
function richTextToPlain(rt) {
@@ -86,19 +105,33 @@ function buildPropValue(type, value) {
86105
return { title: [{ text: { content: value } }] };
87106
case 'rich_text':
88107
return { rich_text: [{ text: { content: value } }] };
89-
case 'number':
90-
return { number: Number(value) };
108+
case 'number': {
109+
const num = Number(value);
110+
if (Number.isNaN(num)) {
111+
return { error: `Invalid number value: "${value}"` };
112+
}
113+
return { number: num };
114+
}
91115
case 'select':
92116
return { select: { name: value } };
93117
case 'multi_select':
94118
return { multi_select: value.split(',').map(v => ({ name: v.trim() })) };
95119
case 'date':
120+
if (!isValidIsoDate(value)) {
121+
return { error: `Invalid date value: "${value}" (expected YYYY-MM-DD or full ISO 8601)` };
122+
}
96123
return { date: { start: value } };
97124
case 'checkbox':
98125
return { checkbox: value === 'true' || value === '1' || value === 'yes' };
99126
case 'url':
127+
if (!value.startsWith('http://') && !value.startsWith('https://')) {
128+
return { error: `Invalid URL value: "${value}" (expected http:// or https://)` };
129+
}
100130
return { url: value };
101131
case 'email':
132+
if (!value.includes('@')) {
133+
return { error: `Invalid email value: "${value}" (expected "@" in address)` };
134+
}
102135
return { email: value };
103136
case 'phone_number':
104137
return { phone_number: value };

test/unit.test.js

Lines changed: 30 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -284,6 +284,12 @@ describe('buildPropValue', () => {
284284
assert.deepEqual(buildPropValue('number', '3.14'), { number: 3.14 });
285285
});
286286

287+
it('rejects invalid number property', () => {
288+
assert.deepEqual(buildPropValue('number', 'abc'), {
289+
error: 'Invalid number value: "abc"',
290+
});
291+
});
292+
287293
it('builds select property', () => {
288294
assert.deepEqual(buildPropValue('select', 'Option A'), {
289295
select: { name: 'Option A' },
@@ -308,6 +314,18 @@ describe('buildPropValue', () => {
308314
});
309315
});
310316

317+
it('builds date property with full ISO date-time', () => {
318+
assert.deepEqual(buildPropValue('date', '2024-01-15T10:30:00Z'), {
319+
date: { start: '2024-01-15T10:30:00Z' },
320+
});
321+
});
322+
323+
it('rejects invalid date property', () => {
324+
assert.deepEqual(buildPropValue('date', '2024-13-01'), {
325+
error: 'Invalid date value: "2024-13-01" (expected YYYY-MM-DD or full ISO 8601)',
326+
});
327+
});
328+
311329
it('builds checkbox property — true values', () => {
312330
assert.deepEqual(buildPropValue('checkbox', 'true'), { checkbox: true });
313331
assert.deepEqual(buildPropValue('checkbox', '1'), { checkbox: true });
@@ -327,12 +345,24 @@ describe('buildPropValue', () => {
327345
});
328346
});
329347

348+
it('rejects invalid url property', () => {
349+
assert.deepEqual(buildPropValue('url', 'example.com'), {
350+
error: 'Invalid URL value: "example.com" (expected http:// or https://)',
351+
});
352+
});
353+
330354
it('builds email property', () => {
331355
assert.deepEqual(buildPropValue('email', 'user@test.com'), {
332356
email: 'user@test.com',
333357
});
334358
});
335359

360+
it('rejects invalid email property', () => {
361+
assert.deepEqual(buildPropValue('email', 'user.test.com'), {
362+
error: 'Invalid email value: "user.test.com" (expected "@" in address)',
363+
});
364+
});
365+
336366
it('builds phone_number property', () => {
337367
assert.deepEqual(buildPropValue('phone_number', '+1234567890'), {
338368
phone_number: '+1234567890',

0 commit comments

Comments
 (0)