Skip to content

Commit f216499

Browse files
✨ (dayjs-plugin): add date utilities to dayjs as plugin (#653)
* ✨ (dayjs-plugin): add date utilities to dayjs as plugin * ✨ (dayjs-plugin): add TZ var to workflow
1 parent f76636e commit f216499

File tree

10 files changed

+370
-64
lines changed

10 files changed

+370
-64
lines changed

.github/workflows/test.yml

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -21,5 +21,7 @@ jobs:
2121
yarn install --frozen-lockfile --network-timeout 300000
2222
2323
- name: Run tests
24+
env:
25+
TZ: ${{ vars.TZ }}
2426
run: |
2527
yarn test

jest.config.js

Lines changed: 0 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -102,9 +102,6 @@ module.exports = {
102102
testEnvironment: "node",
103103
testMatch: ["<rootDir>/packages/core/**/?(*.)+(spec|test).[tj]s?(x)"],
104104
setupFiles: ["<rootDir>/packages/core/src/__tests__/core.test.init.ts"],
105-
setupFilesAfterEnv: [
106-
"<rootDir>/packages/core/src/__tests__/core.test.start.ts",
107-
],
108105
},
109106
{
110107
displayName: "web",
@@ -134,7 +131,6 @@ module.exports = {
134131
"<rootDir>/packages/web/src/__tests__/web.test.init.js",
135132
],
136133
setupFilesAfterEnv: [
137-
"<rootDir>/packages/core/src/__tests__/core.test.start.ts",
138134
"<rootDir>/packages/web/src/__tests__/web.test.start.js",
139135
],
140136
testEnvironment: "jsdom",
@@ -170,7 +166,6 @@ module.exports = {
170166
setupFilesAfterEnv: [
171167
// backend init intentionally here to accommodate @shelf/mongodb preset
172168
"<rootDir>/packages/backend/src/__tests__/backend.test.init.ts",
173-
"<rootDir>/packages/core/src/__tests__/core.test.start.ts",
174169
"<rootDir>/packages/backend/src/__tests__/backend.test.start.ts",
175170
],
176171
testMatch: ["<rootDir>/packages/backend/**/?(*.)+(spec|test).[tj]s?(x)"],

packages/backend/.env.example

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -16,7 +16,7 @@ BASEURL=http://localhost:3000/api
1616
CORS=http://localhost:3000,http://localhost:9080,https://app.yourdomain.com
1717
LOG_LEVEL=debug # options: error, warn, info, http, verbose, debug, silly
1818
NODE_ENV=development # options: test, development, staging, production
19-
TZ="UTC" # set a timezone for this process - prefer UTC
19+
TZ="Etc/UTC" # set the timezone for this process to UTC - required
2020
PORT=3000 # Node.js server
2121
# Unique tokens for auth
2222
# These defaults are fine for development, but

packages/backend/src/app.ts

Lines changed: 0 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -2,10 +2,6 @@
22
import { logger } from "./init"; //must be first import
33

44
import { ENV } from "@backend/common/constants/env.constants";
5-
import dayjs from "@core/util/date/dayjs";
6-
7-
dayjs.tz.setDefault(ENV.TZ);
8-
95
import mongoService from "@backend/common/services/mongo.service";
106
import { initExpressServer } from "@backend/servers/express/express.server";
117
import { initNgrokServer } from "@backend/servers/ngrok/ngrok.server";

packages/backend/src/common/constants/env.constants.ts

Lines changed: 8 additions & 8 deletions
Original file line numberDiff line numberDiff line change
@@ -26,7 +26,7 @@ const EnvSchema = z
2626
EMAILER_USER_TAG_ID: z.string().nonempty().optional(),
2727
MONGO_URI: z.string().nonempty(),
2828
NODE_ENV: z.nativeEnum(NodeEnv),
29-
TZ: z.string().nonempty().default("UTC"),
29+
TZ: z.enum(["Etc/UTC", "UTC"]),
3030
ORIGINS_ALLOWED: z.array(z.string().nonempty()).default([]),
3131
PORT: z.string().nonempty().default(PORT_DEFAULT_BACKEND.toString()),
3232
SUPERTOKENS_URI: z.string().nonempty(),
@@ -50,9 +50,7 @@ const EnvSchema = z
5050
});
5151
});
5252

53-
type Env = z.infer<typeof EnvSchema>;
54-
55-
export const ENV = {
53+
const processEnv = {
5654
BASEURL: process.env["BASEURL"],
5755
CHANNEL_EXPIRATION_MIN: process.env["CHANNEL_EXPIRATION_MIN"],
5856
CLIENT_ID: process.env["CLIENT_ID"],
@@ -73,12 +71,14 @@ export const ENV = {
7371
TOKEN_COMPASS_SYNC: process.env["TOKEN_COMPASS_SYNC"],
7472
NGROK_AUTHTOKEN: process.env["NGROK_AUTHTOKEN"],
7573
NGROK_DOMAIN: process.env["NGROK_DOMAIN"],
76-
} as Env;
74+
};
7775

78-
const parsedEnv = EnvSchema.safeParse(ENV);
76+
const { success, error, data } = EnvSchema.safeParse(processEnv);
7977

80-
if (!parsedEnv.success) {
78+
if (!success) {
8179
logger.error(`Exiting because a critical env value is missing or invalid:`);
82-
console.error(parsedEnv.error.issues);
80+
console.error(error.issues);
8381
process.exit(1);
8482
}
83+
84+
export const ENV = data!;
Lines changed: 0 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,3 +1,2 @@
11
process.env["LOG_LEVEL"] = "debug";
22
process.env["NODE_ENV"] = "test";
3-
process.env["TZ"] = "UTC";

packages/core/src/__tests__/core.test.start.ts

Lines changed: 0 additions & 3 deletions
This file was deleted.
Lines changed: 164 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,164 @@
1+
import dayjs from "@core/util/date/dayjs";
2+
3+
describe("next", () => {
4+
it("adds returns the next time unit for a dayjs date", () => {
5+
const date = dayjs("2026-01-08T00:58:08.000Z");
6+
7+
expect(date.next("millisecond").toISOString()).toEqual(
8+
"2026-01-08T00:58:08.001Z",
9+
);
10+
11+
expect(date.next("second").toISOString()).toEqual(
12+
"2026-01-08T00:58:09.000Z",
13+
);
14+
15+
expect(date.next("minute").toISOString()).toEqual(
16+
"2026-01-08T00:59:08.000Z",
17+
);
18+
19+
expect(date.next("hour").toISOString()).toEqual("2026-01-08T01:58:08.000Z");
20+
21+
expect(date.next("day").toISOString()).toEqual("2026-01-09T00:58:08.000Z");
22+
23+
expect(date.next("week").toISOString()).toEqual("2026-01-15T00:58:08.000Z");
24+
25+
expect(date.next("month").toISOString()).toEqual(
26+
"2026-02-08T00:58:08.000Z",
27+
);
28+
29+
expect(date.next("year").toISOString()).toEqual("2027-01-08T00:58:08.000Z");
30+
});
31+
});
32+
33+
describe("startOfNextWeek", () => {
34+
it("returns the start of the next week", () => {
35+
[
36+
"2026-07-26T00:00:00.000Z",
37+
"2026-07-27T00:00:00.000Z",
38+
"2026-07-28T00:00:00.000Z",
39+
"2026-07-29T00:00:00.000Z",
40+
"2026-07-30T00:00:00.000Z",
41+
"2026-07-31T00:00:00.000Z",
42+
"2026-08-01T00:00:00.000Z",
43+
].forEach((day) =>
44+
expect(dayjs(day).startOfNextWeek().toISOString()).toEqual(
45+
"2026-08-02T00:00:00.000Z",
46+
),
47+
);
48+
});
49+
});
50+
51+
describe("startOfNextMonth", () => {
52+
it("returns the start of the next month", () => {
53+
for (let index = -5; index < 20; index++) {
54+
const month = dayjs.monthStrFromZeroIndex(index);
55+
const intMonth = dayjs.monthFromZeroIndex(index);
56+
const nextMonth = dayjs.monthStrFromZeroIndex(intMonth);
57+
const date = dayjs(
58+
`2026-${month}-08T00:58:08.000Z`,
59+
dayjs.DateFormat.RFC3339_OFFSET,
60+
);
61+
const isDecember = date.month() === 11;
62+
63+
if (isDecember) break;
64+
65+
expect(date.startOfNextMonth().toISOString()).toEqual(
66+
`2026-${nextMonth}-01T00:00:00.000Z`,
67+
);
68+
}
69+
});
70+
});
71+
72+
describe("weekMonthRange", () => {
73+
it("returns a range representing the start of the week and month", () => {
74+
expect(dayjs("2026-07-25T00:00:00.000Z").weekMonthRange()).toEqual(
75+
expect.objectContaining({
76+
week: expect.objectContaining({
77+
startDate: dayjs("2026-07-19T00:00:00.000Z").toYearMonthDayString(),
78+
endDate: dayjs("2026-07-25T00:00:00.000Z").toYearMonthDayString(),
79+
}),
80+
month: expect.objectContaining({
81+
startDate: dayjs("2026-07-01T00:00:00.000Z").toYearMonthDayString(),
82+
endDate: dayjs("2026-07-31T00:00:00.000Z").toYearMonthDayString(),
83+
}),
84+
}),
85+
);
86+
});
87+
});
88+
89+
describe("toRFC5545String", () => {
90+
it("converts dayjs date to RFC5545 iCalendar format", () => {
91+
const rfc5545 = dayjs("2025-12-07T15:59:33.000Z").toRFC5545String();
92+
93+
expect(rfc5545).toEqual("20251207T155933Z");
94+
});
95+
});
96+
97+
describe("toRFC3339String", () => {
98+
it("converts dayjs date to RFC3339 format", () => {
99+
const rfc3339 = dayjs("2025-12-07T15:59:33.000Z").toRFC3339String();
100+
101+
expect(rfc3339).toEqual("2025-12-07T15:59:33Z");
102+
});
103+
});
104+
105+
describe("toRFC3339OffsetString", () => {
106+
it("converts dayjs date to RFC3339_OFFSET format", () => {
107+
const rfc3339Offset = dayjs(
108+
"2025-12-07T15:59:33.000Z",
109+
).toRFC3339OffsetString();
110+
111+
expect(rfc3339Offset).toEqual(
112+
dayjs("2025-12-07T15:59:33+00:00").toRFC3339OffsetString(),
113+
);
114+
});
115+
});
116+
117+
describe("toYearMonthDayString", () => {
118+
it("converts dayjs date to RFC3339 format", () => {
119+
const yearMonthDate = dayjs(
120+
"2025-12-07T15:59:33.000Z",
121+
).toYearMonthDayString();
122+
123+
expect(yearMonthDate).toEqual("2025-12-07");
124+
});
125+
});
126+
127+
describe("monthFromZeroIndex", () => {
128+
it("it returns the zero indexed month from an index", () => {
129+
for (let index = -5; index < 20; index++) {
130+
const validIndex = index >= 0 && index <= 11;
131+
const month = dayjs.monthFromZeroIndex(index);
132+
133+
if (validIndex) expect(month).toStrictEqual(index + 1);
134+
135+
expect(month).toBeGreaterThanOrEqual(1);
136+
expect(month).toBeLessThanOrEqual(12);
137+
}
138+
});
139+
});
140+
141+
describe("monthStrFromZeroIndex", () => {
142+
it("it returns the zero indexed month string from an index", () => {
143+
for (let index = -5; index < 20; index++) {
144+
const validIndex = index >= 0 && index <= 11;
145+
const month = dayjs.monthStrFromZeroIndex(index);
146+
const intMonth = parseInt(month);
147+
148+
if (validIndex) expect(intMonth).toStrictEqual(index + 1);
149+
150+
expect(month).toHaveLength(2);
151+
expect(intMonth).toBeGreaterThanOrEqual(1);
152+
expect(intMonth).toBeLessThanOrEqual(12);
153+
}
154+
});
155+
});
156+
157+
describe("rruleUntilToIsoString", () => {
158+
it("converts a recurrence rule with UNTIL value to an ISO date", () => {
159+
const rrule = "RRULE:FREQ=DAILY;UNTIL=20260108T005808Z";
160+
const isoDate = dayjs.rruleUntilToIsoString(rrule);
161+
162+
expect(isoDate).toEqual(dayjs("2026-01-08T00:58:08.000Z").toISOString());
163+
});
164+
});

0 commit comments

Comments
 (0)