Skip to content
Open
Show file tree
Hide file tree
Changes from 2 commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
8 changes: 5 additions & 3 deletions backend/erxes-api-shared/src/utils/apollo/utils.ts
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';

Comment on lines +4 to 7
Copy link

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.

-    const user: any = extractUserFromHeader(req.headers);
-    const timezone: any = getTimezone(req.headers);
+    const user: any = extractUserFromHeader(req.headers);
+    const timezone: string = getTimezone(req.headers);

Please verify IContext['requestInfo'] includes timezone: string.

Also applies to: 25-26, 41-44


🏁 Script executed:

#!/bin/bash
rg -nP -C2 '\binterface\s+IContext\b' --typescript
rg -nP -C2 'requestInfo\s*:\s*{[^}]*}' --typescript

Length of output: 873


🏁 Script executed:

#!/bin/bash
set -e
# Display first part of the utils.ts file to confirm context usage
sed -n '1,200p' backend/erxes-api-shared/src/utils/apollo/utils.ts
# Search for interface definitions of IContext or IMainContext in .ts files
rg -nP '\binterface\s+(IContext|IMainContext)\b' -g '*.ts' -C2
# Locate IMainContext definition in core-types directory
rg -nP 'export\s+interface\s+IMainContext' -g 'backend/erxes-api-shared/src/core-types/**/*.ts'

Length of output: 7065


🏁 Script executed:

#!/bin/bash
set -e
echo "== core-types/common.ts =="
sed -n '1,240p' backend/erxes-api-shared/src/core-types/common.ts || true

echo
echo "== utils/headers.ts =="
sed -n '1,240p' backend/erxes-api-shared/src/utils/headers.ts || true

echo
echo "== rg getTimezone matches =="
rg -n "getTimezone" -S || true

echo
echo "== rg extractUserFromHeader matches =="
rg -n "extractUserFromHeader" -S || true

echo
echo "== rg requestInfo matches =="
rg -n "requestInfo" -S || true

echo
echo "== gateway/connectionResolver.ts =="
sed -n '1,240p' backend/gateway/src/connectionResolver.ts || true

Length of output: 6772


🏁 Script executed:

#!/bin/bash
set -e
echo "== headers directory =="
ls -la backend/erxes-api-shared/src/utils/headers || true

echo
echo "== timezone.ts =="
sed -n '1,240p' backend/erxes-api-shared/src/utils/headers/timezone.ts || true

echo
echo "== user.ts =="
sed -n '1,240p' backend/erxes-api-shared/src/utils/headers/user.ts || true

echo
echo "== headers index (if present) =="
sed -n '1,240p' backend/erxes-api-shared/src/utils/headers/index.ts || true

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.

  • backend/erxes-api-shared/src/utils/headers/timezone.ts — change to return string (handle Array by taking first element) and add a return type.
  • backend/erxes-api-shared/src/utils/apollo/utils.ts — change the local type:
-    const timezone: any = getTimezone(req.headers);
+    const timezone: string = getTimezone(req.headers);
  • backend/erxes-api-shared/src/core-types/common.ts — replace requestInfo: any with a typed shape that includes timezone: string (and the used fields like secure, cookies, headers).

Committable suggestion skipped: line range outside the PR's diff.

🤖 Prompt for AI Agents
In backend/erxes-api-shared/src/utils/apollo/utils.ts around lines 4 to 7,
ensure timezone is treated as a string throughout: change
backend/erxes-api-shared/src/utils/headers/timezone.ts to explicitly return
string (if getTimezone receives an array, take the first element) and add the
return type annotation; update backend/erxes-api-shared/src/core-types/common.ts
to replace requestInfo: any with a concrete typed shape that includes timezone:
string plus the used fields (secure, cookies, headers); then update utils.ts to
call the newly typed getTimezone (or coerce its result with String(...) / take
first element) and propagate the strict timezone:string into the
IMainContext/requestInfo usage so all types align.

export const generateApolloContext =
<TContext>(
Expand All @@ -22,6 +22,7 @@ export const generateApolloContext =
return {};
}
const user: any = extractUserFromHeader(req.headers);
const timezone: any = getTimezone(req.headers);
Copy link

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Use an explicit type (string) for the timezone instead of 'any'.

Suggested change
const timezone: any = getTimezone(req.headers);
const timezone: string = getTimezone(req.headers);


const subdomain = getSubdomain(req);

Expand All @@ -39,6 +40,7 @@ export const generateApolloContext =
requestInfo: {
secure: req.secure,
cookies: req.cookies,
timezone,
},
};

Expand Down
5 changes: 3 additions & 2 deletions backend/erxes-api-shared/src/utils/headers/index.ts
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';
5 changes: 5 additions & 0 deletions backend/erxes-api-shared/src/utils/headers/timezone.ts
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';
Copy link

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

suggestion: Consider normalizing timezone header casing and value.

Direct string key access may miss headers with different casing. Normalize header keys to lowercase or use a header parsing library. Also, validate that the timezone value is a valid IANA timezone string to avoid errors.

};
Copy link

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

🛠️ Refactor suggestion

Sanitize, normalize, and validate timezone header

IncomingHttpHeaders values can be string | string[] | undefined. Also validate IANA timezones to avoid Mongo $dateToString failures on bad input.

-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';
+  }
+};
📝 Committable suggestion

‼️ IMPORTANT
Carefully review the code before committing. Ensure that it accurately replaces the highlighted code, contains no missing lines, and has no issues with indentation. Thoroughly test & benchmark the code to ensure it meets the requirements.

Suggested change
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,7 +1,7 @@
import { cursorPaginate } from 'erxes-api-shared/utils';
import { IContext } from '~/connectionResolvers';
import { ICycleDocument } from '@/cycle/types';
import { STATUS_TYPES } from '@/status/constants/types';
import { cursorPaginate } from 'erxes-api-shared/utils';
import { IContext } from '~/connectionResolvers';

import {
getCycleProgressByMember,
Expand Down Expand Up @@ -80,9 +80,9 @@ export const cycleQueries = {
getCycleProgressChart: async (
_parent: undefined,
{ _id, assigneeId },
{ models }: IContext,
{ models, requestInfo: { timezone } }: IContext,
) => {
return getCycleProgressChart(_id, assigneeId, models);
return getCycleProgressChart(_id, assigneeId, models, timezone);
},

getCycleProgressByMember: async (
Expand Down
21 changes: 12 additions & 9 deletions backend/plugins/operation_api/src/modules/cycle/utils.ts
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';

Expand Down Expand Up @@ -115,6 +116,7 @@ export const getCycleProgressChart = async (
cycleId: string,
assigneeId: string | undefined,
models: IModels,
timezone: string,
) => {
Comment on lines +119 to 120
Copy link

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

⚠️ Potential issue

🧩 Analysis chain

Provide 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\(' -C2

Length 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

‼️ IMPORTANT
Carefully review the code before committing. Ensure that it accurately replaces the highlighted code, contains no missing lines, and has no issues with indentation. Thoroughly test & benchmark the code to ensure it meets the requirements.

Suggested change
timezone: string,
) => {
export const getCycleProgressChart = async (
cycleId: string,
assigneeId: string | undefined,
models: IModels,
timezone: string = 'UTC',
) => {
🤖 Prompt for AI Agents
In backend/plugins/operation_api/src/modules/cycle/utils.ts around lines 119 to
120, the getCycleProgressChart function's timezone parameter is currently
required which breaks internal callers that omit it; update the function
signature to default timezone to 'UTC' (e.g., timezone = 'UTC') so callers that
don't pass a timezone continue to behave correctly, and ensure any internal
calls relying on undefined still work without modification.

const filter: { cycleId: Types.ObjectId; assigneeId?: string } = {
cycleId: new Types.ObjectId(cycleId),
Expand Down Expand Up @@ -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] },
Expand Down Expand Up @@ -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,
},
Expand Down Expand Up @@ -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,
);
Copy link

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

⚠️ Potential issue

Fix double timezone conversion when filling gaps

You zone start/end, then fillMissingDays zones again, shifting dates twice. Pass an unzoned base into fillMissingDays and keep days computed from zoned bounds.

-  const start = toZonedTime(cycle.startDate, timezone);
-  const end = toZonedTime(cycle.endDate, timezone);
-
-  const days = differenceInCalendarDays(end, start) + 1;
-
-  chartData.chartData = fillMissingDays(
-    chartDataAggregation,
-    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,
+  );
📝 Committable suggestion

‼️ IMPORTANT
Carefully review the code before committing. Ensure that it accurately replaces the highlighted code, contains no missing lines, and has no issues with indentation. Thoroughly test & benchmark the code to ensure it meets the requirements.

Suggested change
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.


return chartData;
Expand Down
Original file line number Diff line number Diff line change
@@ -1,4 +1,5 @@
import { addDays, differenceInCalendarDays, format } from 'date-fns';
import { toZonedTime } from 'date-fns-tz';

export const fillUntilTargetDate = (
data: { date: string; started: number; completed: number }[],
Expand Down Expand Up @@ -26,14 +27,15 @@ export const fillMissingDays = (
data: { date: string; started: number; completed: number }[],
baseDate: Date,
totalDays = 7,
timezone: string,
) => {
const filledData: { date: string; started: number; completed: number }[] = [];

const mapDateToData = new Map(data.map((item) => [item.date, item]));

for (let i = 0; i < totalDays; i++) {
const date = addDays(baseDate, i);
const key = format(date, 'yyyy-MM-dd');
const key = format(toZonedTime(date, timezone), 'yyyy-MM-dd');
const item = mapDateToData.get(key);

if (item) {
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -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
Copy link

Choose a reason for hiding this comment

The 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
headers: {
...headers,
sessioncode: sessionStorage.getItem('sessioncode') || '',
'x-timezone': Intl.DateTimeFormat().resolvedOptions().timeZone,
},
};
// Helper to safely get timezone, fallback to 'UTC'
function getClientTimezone(): string {
try {
const tz = Intl?.DateTimeFormat?.().resolvedOptions?.().timeZone;
if (typeof tz === 'string' && tz.length > 0) {
return tz;
}
} catch (e) {
// Ignore errors and fallback
}
return 'UTC';
}
headers: {
...headers,
sessioncode: sessionStorage.getItem('sessioncode') || '',
'x-timezone': getClientTimezone(),
},
};

});
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -107,6 +107,7 @@ export const CycleProgressChart = ({
dot={false}
connectNulls={true}
strokeLinecap="round"
activeDot={{ fill: STATUS_COLORS.started, r: 4 }}
/>
<Area
dataKey="completed"
Expand All @@ -117,6 +118,7 @@ export const CycleProgressChart = ({
dot={false}
connectNulls={true}
strokeLinecap="round"
activeDot={{ fill: STATUS_COLORS.completed, r: 4 }}
/>
</AreaChart>
</ChartContainer>
Expand Down