-
Notifications
You must be signed in to change notification settings - Fork 3
chore: pass timezone through headers #351
New issue
Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.
By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.
Already on GitHub? Sign in to your account
base: main
Are you sure you want to change the base?
Changes from 1 commit
File filter
Filter by extension
Conversations
Jump to
Diff view
Diff view
There are no files selected for viewing
| Original file line number | Diff line number | Diff line change | ||||
|---|---|---|---|---|---|---|
| @@ -1,9 +1,9 @@ | ||||||
| import { IMainContext } from '../../core-types'; | ||||||
| import { extractUserFromHeader } from '../headers'; | ||||||
| import { getSubdomain } from '../utils'; | ||||||
| import { ExpressContextFunctionArgument } from '@apollo/server/dist/esm/express4'; | ||||||
| import { Request as ApiRequest, Response as ApiResponse } from 'express'; | ||||||
| import { nanoid } from 'nanoid'; | ||||||
| import { IMainContext } from '../../core-types'; | ||||||
| import { extractUserFromHeader, getTimezone } from '../headers'; | ||||||
| import { getSubdomain } from '../utils'; | ||||||
|
|
||||||
| export const generateApolloContext = | ||||||
| <TContext>( | ||||||
|
|
@@ -22,6 +22,7 @@ export const generateApolloContext = | |||||
| return {}; | ||||||
| } | ||||||
| const user: any = extractUserFromHeader(req.headers); | ||||||
| const timezone: any = getTimezone(req.headers); | ||||||
|
||||||
| const timezone: any = getTimezone(req.headers); | |
| const timezone: string = getTimezone(req.headers); |
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -1,4 +1,5 @@ | ||
| export * from './sanitize'; | ||
| export * from './user'; | ||
| export * from './get-hostname'; | ||
| export * from './sanitize'; | ||
| export * from './subdomain'; | ||
| export * from './timezone'; | ||
| export * from './user'; |
| Original file line number | Diff line number | Diff line change | ||||||||||||||||||||||||||||||
|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|
| @@ -0,0 +1,5 @@ | ||||||||||||||||||||||||||||||||
| import { IncomingHttpHeaders } from 'http'; | ||||||||||||||||||||||||||||||||
|
|
||||||||||||||||||||||||||||||||
| export const getTimezone = (req: IncomingHttpHeaders) => { | ||||||||||||||||||||||||||||||||
| return req['x-timezone'] || req['timezone'] || 'UTC'; | ||||||||||||||||||||||||||||||||
|
||||||||||||||||||||||||||||||||
| }; | ||||||||||||||||||||||||||||||||
|
||||||||||||||||||||||||||||||||
| export const getTimezone = (req: IncomingHttpHeaders) => { | |
| return req['x-timezone'] || req['timezone'] || 'UTC'; | |
| }; | |
| export const getTimezone = (headers: IncomingHttpHeaders): string => { | |
| const raw = headers['x-timezone'] ?? headers['timezone']; | |
| const value = Array.isArray(raw) ? raw[0] : raw; | |
| if (typeof value !== 'string') return 'UTC'; | |
| try { | |
| // Intl throws RangeError for invalid time zones | |
| new Intl.DateTimeFormat('en-US', { timeZone: value }).format(); | |
| return value; | |
| } catch { | |
| return 'UTC'; | |
| } | |
| }; |
🤖 Prompt for AI Agents
In backend/erxes-api-shared/src/utils/headers/timezone.ts around lines 3 to 5,
the current getter returns raw header values which may be string | string[] |
undefined and may contain invalid timezones; update it to normalize and
validate: if header is an array, pick the first element; coerce to string, trim
and uppercase/lowercase as appropriate; validate the candidate timezone using a
reliable IANA check (e.g. attempt to construct an Intl.DateTimeFormat with the
timeZone option and catch RangeError) and only return it when valid; otherwise
fall back to 'UTC'. Ensure the function returns a string and handles undefined
safely.
| Original file line number | Diff line number | Diff line change | ||||||||||||||||||||||||||||||||||||||||||||||
|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|
| @@ -1,6 +1,7 @@ | ||||||||||||||||||||||||||||||||||||||||||||||||
| import { fillMissingDays } from '@/project/utils/charUtils'; | ||||||||||||||||||||||||||||||||||||||||||||||||
| import { STATUS_TYPES } from '@/status/constants/types'; | ||||||||||||||||||||||||||||||||||||||||||||||||
| import { differenceInCalendarDays, startOfDay } from 'date-fns'; | ||||||||||||||||||||||||||||||||||||||||||||||||
| import { differenceInCalendarDays } from 'date-fns'; | ||||||||||||||||||||||||||||||||||||||||||||||||
| import { toZonedTime } from 'date-fns-tz'; | ||||||||||||||||||||||||||||||||||||||||||||||||
| import { Types } from 'mongoose'; | ||||||||||||||||||||||||||||||||||||||||||||||||
| import { IModels } from '~/connectionResolvers'; | ||||||||||||||||||||||||||||||||||||||||||||||||
|
|
||||||||||||||||||||||||||||||||||||||||||||||||
|
|
@@ -115,6 +116,7 @@ export const getCycleProgressChart = async ( | |||||||||||||||||||||||||||||||||||||||||||||||
| cycleId: string, | ||||||||||||||||||||||||||||||||||||||||||||||||
| assigneeId: string | undefined, | ||||||||||||||||||||||||||||||||||||||||||||||||
| models: IModels, | ||||||||||||||||||||||||||||||||||||||||||||||||
| timezone: string, | ||||||||||||||||||||||||||||||||||||||||||||||||
| ) => { | ||||||||||||||||||||||||||||||||||||||||||||||||
|
Comment on lines
+119
to
120
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. 🧩 Analysis chainProvide a safe default for the timezone parameter. Guard callers that haven’t been migrated by defaulting to 'UTC'. -export const getCycleProgressChart = async (
+export const getCycleProgressChart = async (
cycleId: string,
assigneeId: string | undefined,
models: IModels,
- timezone: string,
+ timezone: string = 'UTC',
) => {Run this to find any remaining call sites not passing a TZ: 🏁 Script executed: #!/bin/bash
rg -n --type=ts 'getCycleProgressChart\(' -C2Length of output: 2668 Default timezone parameter to 'UTC' in getCycleProgressChart Default timezone to 'UTC' so callers that haven't been migrated don't break — resolver already passes timezone, but there is at least one internal call that omits it. -export const getCycleProgressChart = async (
+export const getCycleProgressChart = async (
cycleId: string,
assigneeId: string | undefined,
models: IModels,
- timezone: string,
+ timezone: string = 'UTC',
) => {Remaining caller without TZ: backend/plugins/operation_api/src/modules/cycle/db/models/Cycle.ts:124 (const chartData = await getCycleProgressChart(_id, undefined, models);) 📝 Committable suggestion
Suggested change
🤖 Prompt for AI Agents |
||||||||||||||||||||||||||||||||||||||||||||||||
| const filter: { cycleId: Types.ObjectId; assigneeId?: string } = { | ||||||||||||||||||||||||||||||||||||||||||||||||
| cycleId: new Types.ObjectId(cycleId), | ||||||||||||||||||||||||||||||||||||||||||||||||
|
|
@@ -177,10 +179,10 @@ export const getCycleProgressChart = async ( | |||||||||||||||||||||||||||||||||||||||||||||||
| { | ||||||||||||||||||||||||||||||||||||||||||||||||
| $addFields: { | ||||||||||||||||||||||||||||||||||||||||||||||||
| dayDate: { | ||||||||||||||||||||||||||||||||||||||||||||||||
| $dateFromParts: { | ||||||||||||||||||||||||||||||||||||||||||||||||
| year: { $year: '$statusChangedDate' }, | ||||||||||||||||||||||||||||||||||||||||||||||||
| month: { $month: '$statusChangedDate' }, | ||||||||||||||||||||||||||||||||||||||||||||||||
| day: { $dayOfMonth: '$statusChangedDate' }, | ||||||||||||||||||||||||||||||||||||||||||||||||
| $dateToString: { | ||||||||||||||||||||||||||||||||||||||||||||||||
| format: '%Y-%m-%d', | ||||||||||||||||||||||||||||||||||||||||||||||||
| date: '$statusChangedDate', | ||||||||||||||||||||||||||||||||||||||||||||||||
| timezone, | ||||||||||||||||||||||||||||||||||||||||||||||||
| }, | ||||||||||||||||||||||||||||||||||||||||||||||||
| }, | ||||||||||||||||||||||||||||||||||||||||||||||||
| isStarted: { $eq: ['$statusType', STATUS_TYPES.STARTED] }, | ||||||||||||||||||||||||||||||||||||||||||||||||
|
|
@@ -210,7 +212,7 @@ export const getCycleProgressChart = async ( | |||||||||||||||||||||||||||||||||||||||||||||||
| { | ||||||||||||||||||||||||||||||||||||||||||||||||
| $project: { | ||||||||||||||||||||||||||||||||||||||||||||||||
| _id: 0, | ||||||||||||||||||||||||||||||||||||||||||||||||
| date: { $dateToString: { format: '%Y-%m-%d', date: '$_id' } }, | ||||||||||||||||||||||||||||||||||||||||||||||||
| date: '$_id', | ||||||||||||||||||||||||||||||||||||||||||||||||
| started: 1, | ||||||||||||||||||||||||||||||||||||||||||||||||
| completed: 1, | ||||||||||||||||||||||||||||||||||||||||||||||||
| }, | ||||||||||||||||||||||||||||||||||||||||||||||||
|
|
@@ -238,15 +240,16 @@ export const getCycleProgressChart = async ( | |||||||||||||||||||||||||||||||||||||||||||||||
| chartData: [], | ||||||||||||||||||||||||||||||||||||||||||||||||
| }; | ||||||||||||||||||||||||||||||||||||||||||||||||
|
|
||||||||||||||||||||||||||||||||||||||||||||||||
| const start = startOfDay(new Date(cycle.startDate)); | ||||||||||||||||||||||||||||||||||||||||||||||||
| const end = startOfDay(new Date(cycle.endDate)); | ||||||||||||||||||||||||||||||||||||||||||||||||
| const start = toZonedTime(cycle.startDate, timezone); | ||||||||||||||||||||||||||||||||||||||||||||||||
| const end = toZonedTime(cycle.endDate, timezone); | ||||||||||||||||||||||||||||||||||||||||||||||||
|
|
||||||||||||||||||||||||||||||||||||||||||||||||
| const days = differenceInCalendarDays(end, start) + 1; | ||||||||||||||||||||||||||||||||||||||||||||||||
|
|
||||||||||||||||||||||||||||||||||||||||||||||||
| chartData.chartData = fillMissingDays( | ||||||||||||||||||||||||||||||||||||||||||||||||
| chartDataAggregation, | ||||||||||||||||||||||||||||||||||||||||||||||||
| cycle.startDate, | ||||||||||||||||||||||||||||||||||||||||||||||||
| start, | ||||||||||||||||||||||||||||||||||||||||||||||||
| days, | ||||||||||||||||||||||||||||||||||||||||||||||||
| timezone, | ||||||||||||||||||||||||||||||||||||||||||||||||
| ); | ||||||||||||||||||||||||||||||||||||||||||||||||
|
||||||||||||||||||||||||||||||||||||||||||||||||
| const start = toZonedTime(cycle.startDate, timezone); | |
| const end = toZonedTime(cycle.endDate, timezone); | |
| const days = differenceInCalendarDays(end, start) + 1; | |
| chartData.chartData = fillMissingDays( | |
| chartDataAggregation, | |
| cycle.startDate, | |
| start, | |
| days, | |
| timezone, | |
| ); | |
| const startZoned = toZonedTime(cycle.startDate, timezone); | |
| const endZoned = toZonedTime(cycle.endDate, timezone); | |
| const days = differenceInCalendarDays(endZoned, startZoned) + 1; | |
| chartData.chartData = fillMissingDays( | |
| chartDataAggregation, | |
| cycle.startDate, // unzoned base; formatting happens in TZ inside fillMissingDays | |
| days, | |
| timezone, | |
| ); |
🤖 Prompt for AI Agents
In backend/plugins/operation_api/src/modules/cycle/utils.ts around lines 243 to
253, the code first converts cycle.startDate and cycle.endDate to the timezone
with toZonedTime and then passes those zoned dates into fillMissingDays which
also applies timezone conversion, causing dates to be shifted twice; instead
pass the unzoned base start (use the original cycle.startDate) into
fillMissingDays while keeping the days count computed from the already-zoned
start/end, so compute startZoned = toZonedTime(...), endZoned =
toZonedTime(...), days = differenceInCalendarDays(endZoned, startZoned) + 1, and
call fillMissingDays with the original unzoned start date and the computed days
and timezone.
| Original file line number | Diff line number | Diff line change | ||||||||||||||||||||||||||||||||||||||||||||||||||
|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|
|
|
@@ -35,6 +35,7 @@ const authLink = setContext((_, { headers }) => { | |||||||||||||||||||||||||||||||||||||||||||||||||||
| headers: { | ||||||||||||||||||||||||||||||||||||||||||||||||||||
| ...headers, | ||||||||||||||||||||||||||||||||||||||||||||||||||||
| sessioncode: sessionStorage.getItem('sessioncode') || '', | ||||||||||||||||||||||||||||||||||||||||||||||||||||
| 'x-timezone': Intl.DateTimeFormat().resolvedOptions().timeZone, | ||||||||||||||||||||||||||||||||||||||||||||||||||||
| }, | ||||||||||||||||||||||||||||||||||||||||||||||||||||
| }; | ||||||||||||||||||||||||||||||||||||||||||||||||||||
|
Comment on lines
35
to
40
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. suggestion: Client timezone detection may not be reliable in all environments. Consider adding fallback logic or validation for environments where Intl.DateTimeFormat().resolvedOptions().timeZone is unavailable or inaccurate.
Suggested change
|
||||||||||||||||||||||||||||||||||||||||||||||||||||
| }); | ||||||||||||||||||||||||||||||||||||||||||||||||||||
|
|
||||||||||||||||||||||||||||||||||||||||||||||||||||
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
🛠️ Refactor suggestion
🧩 Analysis chain
Type timezone strictly; avoid any
getTimezone returns a string after validation; carry that type into context.
Please verify IContext['requestInfo'] includes timezone: string.
Also applies to: 25-26, 41-44
🏁 Script executed:
Length of output: 873
🏁 Script executed:
Length of output: 7065
🏁 Script executed:
Length of output: 6772
🏁 Script executed:
Length of output: 1595
Enforce timezone as string across context
getTimezone can return string|string[]; coerce it to a string, declare its return type, and propagate strict typing into the context and core types.
🤖 Prompt for AI Agents