Skip to content
Open
Show file tree
Hide file tree
Changes from all 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: 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';
11 changes: 11 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,11 @@
import { IncomingHttpHeaders } from 'http';

export const getTimezone = (req: IncomingHttpHeaders): string => {
const timezone = req['x-timezone'] || req['timezone'] || 'UTC';

if (Array.isArray(timezone)) {
return timezone[0];
}

return timezone;
};
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
24 changes: 11 additions & 13 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,16 +240,12 @@ 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,
days,
);
chartData.chartData = fillMissingDays(chartDataAggregation, start, days);

Comment on lines +243 to 249
Copy link

Choose a reason for hiding this comment

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

⚠️ Potential issue

Avoid double TZ conversion and DST drift; compute days on zoned midnights and pass an unzoned base into fillMissingDays (TZ-aware).

  • Normalize start/end to midnight in the target TZ to prevent off‑by‑one around DST.
  • If fillMissingDays formats dates in TZ, pass the original (unzoned) base plus the timezone; otherwise you risk double conversion or server-TZ formatting.
-import { differenceInCalendarDays } from 'date-fns';
+import { differenceInCalendarDays, startOfDay } from 'date-fns';
 import { toZonedTime } from 'date-fns-tz';
@@
-  const start = toZonedTime(cycle.startDate, timezone);
-  const end = toZonedTime(cycle.endDate, timezone);
-
-  const days = differenceInCalendarDays(end, start) + 1;
-
-  chartData.chartData = fillMissingDays(chartDataAggregation, start, days);
+  const startZoned = startOfDay(toZonedTime(cycle.startDate, timezone));
+  const endZoned = startOfDay(toZonedTime(cycle.endDate, timezone));
+
+  const days = differenceInCalendarDays(endZoned, startZoned) + 1;
+
+  // Pass unzoned base and timezone to avoid double-shifting inside fillMissingDays
+  chartData.chartData = fillMissingDays(
+    chartDataAggregation,
+    cycle.startDate,
+    days,
+    timezone,
+  );

If fillMissingDays isn’t TZ-aware yet, update it like below (outside this file):

// backend/plugins/operation_api/src/modules/project/utils/charUtils.ts
import { addDays } from 'date-fns';
import { formatInTimeZone } from 'date-fns-tz';

export function fillMissingDays(
  data: { date: string; started: number; completed: number }[],
  baseDate: Date,
  totalDays = 7,
  timezone = 'UTC',
) {
  const filled: typeof data = [];
  const index = new Map(data.map(d => [d.date, d]));
  for (let i = 0; i < totalDays; i++) {
    const date = addDays(baseDate, i);
    const key = formatInTimeZone(date, timezone, 'yyyy-MM-dd');
    const item = index.get(key) ?? filled[filled.length - 1] ?? { started: 0, completed: 0, date: key };
    filled.push(item.date === key ? item : { date: key, started: item.started, completed: item.completed });
  }
  return filled;
}

return chartData;
};
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