Skip to main content
Glama
date-utils.tsβ€’16.3 kB
/** * Date utility functions for working with Attio date filters * Provides functions for handling date ranges, relative dates, and date formatting. */ import { RelativeDate, RelativeDateUnit, DateRange, DateRangePreset, } from '../types/attio.js'; import { isRelativeDate, parseRelativeDate, normalizeDate, } from './date-parser.js'; /** * Converts a relative date (e.g., "last 7 days") to an absolute ISO date string * * @param relativeDate - The relative date configuration * @returns ISO date string representation * @throws Error when validation fails */ export function resolveRelativeDate(relativeDate: RelativeDate): string { // Validate required properties if (!relativeDate) { throw new Error('RelativeDate object is required'); } if (!relativeDate.unit) { throw new Error( 'RelativeDate must specify a unit (day, week, month, quarter, year)' ); } if (relativeDate.value === undefined || relativeDate.value === null) { throw new Error('RelativeDate must specify a numeric value'); } if (!relativeDate.direction) { throw new Error('RelativeDate must specify a direction (past or future)'); } if ( relativeDate.direction !== 'past' && relativeDate.direction !== 'future' ) { throw new Error('RelativeDate direction must be either "past" or "future"'); } // Value should be a positive number if ( typeof relativeDate.value !== 'number' || isNaN(relativeDate.value) || relativeDate.value < 0 ) { throw new Error('RelativeDate value must be a positive number'); } const now = new Date(); const resultDate = new Date(now); // Determine the operation based on direction const operation = relativeDate.direction === 'past' ? -1 : 1; const value = relativeDate.value * operation; // Apply the operation based on unit switch (relativeDate.unit) { case RelativeDateUnit.DAY: resultDate.setDate(now.getDate() + value); break; case RelativeDateUnit.WEEK: resultDate.setDate(now.getDate() + value * 7); break; case RelativeDateUnit.MONTH: resultDate.setMonth(now.getMonth() + value); break; case RelativeDateUnit.QUARTER: resultDate.setMonth(now.getMonth() + value * 3); break; case RelativeDateUnit.YEAR: resultDate.setFullYear(now.getFullYear() + value); break; default: throw new Error(`Unsupported relative date unit: ${relativeDate.unit}`); } return resultDate.toISOString(); } /** * Creates a date range from a preset string (today, yesterday, this_week, etc.) * * @param preset - Preset identifier string * @returns Object with start and end dates as ISO strings * @throws Error for invalid preset values */ export function createDateRangeFromPreset(preset: string): { start: string; end: string; } { // Validate preset if (!preset || typeof preset !== 'string') { throw new Error('Date preset must be a non-empty string'); } const normalizedPreset = preset.toLowerCase().trim(); // Check if preset is a valid DateRangePreset value const isValidPreset = Object.values(DateRangePreset).includes( normalizedPreset as DateRangePreset ); if (!isValidPreset) { throw new Error( `Unsupported date preset: "${preset}". ` + `Valid presets are: ${Object.values(DateRangePreset).join(', ')}` ); } const now = new Date(); const today = new Date(now.getFullYear(), now.getMonth(), now.getDate()); let start = new Date(today); let end = new Date(today); // Set end to end of today by default end.setHours(23, 59, 59, 999); switch (normalizedPreset) { case DateRangePreset.TODAY: // start is already set to beginning of today break; case DateRangePreset.YESTERDAY: start.setDate(today.getDate() - 1); end.setDate(today.getDate() - 1); break; case DateRangePreset.THIS_WEEK: { // Set start to beginning of current week (Sunday) const dayOfWeek = today.getDay(); // 0 = Sunday, 6 = Saturday start.setDate(today.getDate() - dayOfWeek); break; } case DateRangePreset.LAST_WEEK: { // Set start to beginning of last week const lastWeekDay = today.getDay(); start.setDate(today.getDate() - lastWeekDay - 7); end.setDate(today.getDate() - lastWeekDay - 1); break; } case DateRangePreset.THIS_MONTH: // Set start to beginning of current month start.setDate(1); break; case DateRangePreset.LAST_MONTH: // Set start to beginning of last month start.setMonth(today.getMonth() - 1); start.setDate(1); end = new Date(today.getFullYear(), today.getMonth(), 0, 23, 59, 59, 999); break; case DateRangePreset.THIS_QUARTER: { // Set start to beginning of current quarter const currentQuarter = Math.floor(today.getMonth() / 3); start.setMonth(currentQuarter * 3); start.setDate(1); break; } case DateRangePreset.LAST_QUARTER: { // Set start to beginning of last quarter const lastQuarter = Math.floor(today.getMonth() / 3) - 1; const lastQuarterYear = lastQuarter < 0 ? today.getFullYear() - 1 : today.getFullYear(); const normalizedLastQuarter = lastQuarter < 0 ? 3 : lastQuarter; start = new Date(lastQuarterYear, normalizedLastQuarter * 3, 1); end = new Date( lastQuarterYear, (normalizedLastQuarter + 1) * 3, 0, 23, 59, 59, 999 ); break; } case DateRangePreset.THIS_YEAR: // Set start to beginning of current year start.setMonth(0); start.setDate(1); break; case DateRangePreset.LAST_YEAR: // Set start to beginning of last year start.setFullYear(today.getFullYear() - 1); start.setMonth(0); start.setDate(1); end = new Date(today.getFullYear() - 1, 11, 31, 23, 59, 59, 999); break; default: // This shouldn't happen due to earlier validation, but included for type safety throw new Error(`Unsupported date preset: ${preset}`); } return { start: start.toISOString(), end: end.toISOString(), }; } /** * Validates and resolves a date range to absolute ISO date strings * Handles both relative and absolute date specifications * * @param dateRange - The date range specification * @returns Object with resolved start and end dates as ISO strings * @throws Error when date range validation fails */ export function resolveDateRange(dateRange: DateRange): { start?: string; end?: string; } { // Validate date range if (!dateRange) { throw new Error('DateRange object is required'); } // A date range must have at least one of: preset, start, or end if (!dateRange.preset && !dateRange.start && !dateRange.end) { throw new Error( 'DateRange must specify at least one of: preset, start, or end' ); } const result: { start?: string; end?: string } = {}; // Handle preset if specified if (dateRange.preset) { try { // If preset is specified along with start/end, warn but continue with preset if (dateRange.start || dateRange.end) { console.warn( 'DateRange contains both preset and start/end specifications. ' + 'Using preset and ignoring explicit start/end values.' ); } // First try to parse as a relative date expression (e.g., "last 7 days", "this month") if (isRelativeDate(dateRange.preset)) { return parseRelativeDate(dateRange.preset); } // Otherwise use the standard preset resolution const presetRange = createDateRangeFromPreset(dateRange.preset); return presetRange; } catch (error: unknown) { // Throw a more descriptive error const errorMessage = error instanceof Error ? error.message : String(error); throw new Error(`Failed to resolve date range preset: ${errorMessage}`); } } // Handle start date if specified if (dateRange.start) { try { if (typeof dateRange.start === 'string') { // First try to parse as a relative date expression (e.g., "last 7 days") if (isRelativeDate(dateRange.start)) { const relativeRange = parseRelativeDate(dateRange.start); result.start = relativeRange.start; } // Then check if it's a valid ISO date string else if (isValidISODateString(dateRange.start)) { result.start = dateRange.start; } // Otherwise try to normalize it as a natural language date else { const normalized = normalizeDate(dateRange.start); if (normalized) { result.start = normalized; } else { throw new Error(`Unable to parse date: ${dateRange.start}`); } } } else { // Relative date object result.start = resolveRelativeDate(dateRange.start); } } catch (error: unknown) { const errorMessage = error instanceof Error ? error.message : String(error); throw new Error(`Failed to resolve start date: ${errorMessage}`); } } // Handle end date if specified if (dateRange.end) { try { if (typeof dateRange.end === 'string') { // First try to parse as a relative date expression (e.g., "last 7 days") if (isRelativeDate(dateRange.end)) { const relativeRange = parseRelativeDate(dateRange.end); result.end = relativeRange.end; } // Then check if it's a valid ISO date string else if (isValidISODateString(dateRange.end)) { result.end = dateRange.end; } // Otherwise try to normalize it as a natural language date else { const normalized = normalizeDate(dateRange.end); if (normalized) { result.end = normalized; } else { throw new Error(`Unable to parse date: ${dateRange.end}`); } } } else { // Relative date object result.end = resolveRelativeDate(dateRange.end); } } catch (error: unknown) { const errorMessage = error instanceof Error ? error.message : String(error); throw new Error(`Failed to resolve end date: ${errorMessage}`); } } // Validate that if both dates are provided, start is before end if (result.start && result.end) { const startDate = new Date(result.start); const endDate = new Date(result.end); if (startDate > endDate) { throw new Error( `Invalid date range: start date (${result.start}) ` + `must be before or equal to end date (${result.end})` ); } } return result; } /** * Helper function to check if a string is a valid ISO date string * * @param dateString - The string to validate * @returns True if the string is a valid ISO date, false otherwise */ export function isValidISODateString(dateString: string): boolean { // Accept both full ISO format and date-only format // Full ISO: 2025-08-01T00:00:00.000Z // Date only: 2025-08-01 if ( !/^\d{4}-\d{2}-\d{2}T\d{2}:\d{2}:\d{2}(\.\d{3})?Z$/.test(dateString) && !/^\d{4}-\d{2}-\d{2}$/.test(dateString) ) { return false; } // Check if it's a valid date const date = new Date(dateString); return !isNaN(date.getTime()); } /** * Creates a date range for a specific time period (last X days, weeks, etc.) * * @param value - The number of units (e.g., 7 for "last 7 days") * @param unit - The time unit (day, week, month, etc.) * @returns Object with start and end dates as ISO strings */ export function createRelativeDateRange( value: number, unit: RelativeDateUnit ): { start: string; end: string } { const now = new Date(); const startDate = new Date(now); // Configure start date based on unit and value switch (unit) { case RelativeDateUnit.DAY: startDate.setDate(now.getDate() - value); break; case RelativeDateUnit.WEEK: startDate.setDate(now.getDate() - value * 7); break; case RelativeDateUnit.MONTH: startDate.setMonth(now.getMonth() - value); break; case RelativeDateUnit.QUARTER: startDate.setMonth(now.getMonth() - value * 3); break; case RelativeDateUnit.YEAR: startDate.setFullYear(now.getFullYear() - value); break; default: throw new Error(`Unsupported relative date unit: ${unit}`); } return { start: startDate.toISOString(), end: now.toISOString(), }; } /** * Creates a formatted date string to display to users * * @param dateString - ISO date string * @param format - Optional format specification ('short', 'long', etc.) * @returns Formatted date string */ export function formatDate( dateString: string, format: 'short' | 'long' | 'relative' = 'short' ): string { const date = new Date(dateString); switch (format) { case 'short': return date.toLocaleDateString(); case 'long': return date.toLocaleDateString(undefined, { weekday: 'long', year: 'numeric', month: 'long', day: 'numeric', }); case 'relative': { const now = new Date(); const diffMs = now.getTime() - date.getTime(); const diffDays = Math.floor(diffMs / (1000 * 60 * 60 * 24)); if (diffDays === 0) return 'Today'; if (diffDays === 1) return 'Yesterday'; if (diffDays < 7) return `${diffDays} days ago`; if (diffDays < 30) return `${Math.floor(diffDays / 7)} weeks ago`; if (diffDays < 365) return `${Math.floor(diffDays / 30)} months ago`; return `${Math.floor(diffDays / 365)} years ago`; } default: return date.toISOString(); } } /** * Validates and creates a date range object for API requests * Ensures dates are properly formatted and handles missing values gracefully * * @param startDate - Optional start date (ISO string or empty) * @param endDate - Optional end date (ISO string or empty) * @returns Validated date range object or throws error * @throws Error when dates are invalid or illogical */ export function validateAndCreateDateRange( startDate?: string, endDate?: string ): { start?: string; end?: string } | null { // If both dates are missing or empty, return null (no date filtering) if ( (!startDate || startDate.trim() === '') && (!endDate || endDate.trim() === '') ) { return null; } const result: { start?: string; end?: string } = {}; // Validate and process start date if (startDate && startDate.trim() !== '') { const trimmedStart = startDate.trim(); if (!isValidISODateString(trimmedStart)) { throw new Error( `Invalid start date format. Expected ISO 8601 format (YYYY-MM-DDTHH:mm:ss.sssZ), got: "${trimmedStart}"` ); } result.start = trimmedStart; } // Validate and process end date if (endDate && endDate.trim() !== '') { const trimmedEnd = endDate.trim(); if (!isValidISODateString(trimmedEnd)) { throw new Error( `Invalid end date format. Expected ISO 8601 format (YYYY-MM-DDTHH:mm:ss.sssZ), got: "${trimmedEnd}"` ); } result.end = trimmedEnd; } // If only one date is provided, it's still valid for open-ended ranges if ((result.start && !result.end) || (!result.start && result.end)) { createScopedLogger('utils.date-utils', 'parseDateRange').info( 'Creating open-ended date range', { start: result.start, end: result.end } ); } // Validate logical consistency if both dates are provided if (result.start && result.end) { const startDate = new Date(result.start); const endDate = new Date(result.end); if (startDate > endDate) { throw new Error( `Invalid date range: start date (${result.start}) must be before or equal to end date (${result.end})` ); } // Warn if the date range is suspiciously large const diffMs = endDate.getTime() - startDate.getTime(); const diffYears = diffMs / (1000 * 60 * 60 * 24 * 365); if (diffYears > 10) { console.warn( `Large date range detected: ${diffYears.toFixed( 1 )} years. This may impact performance.` ); } } return result; } import { createScopedLogger } from './logger.js';

Latest Blog Posts

MCP directory API

We provide all the information about MCP servers via our MCP API.

curl -X GET 'https://glama.ai/api/mcp/v1/servers/kesslerio/attio-mcp-server'

If you have feedback or need assistance with the MCP directory API, please join our Discord server