Skip to content

Commit 3b78711

Browse files
committed
feat: add scalar types utility functions and corresponding tests
1 parent b155177 commit 3b78711

File tree

2 files changed

+232
-0
lines changed

2 files changed

+232
-0
lines changed
Lines changed: 89 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,89 @@
1+
import { randomUUID } from 'node:crypto';
2+
3+
/**
4+
* ID - A unique identifier for an object. This scalar is serialized like a String
5+
* but isn't meant to be human-readable.
6+
*/
7+
export const makeId = () => randomUUID();
8+
9+
/**
10+
* AWSTimestamp - An integer value representing the number of seconds
11+
* before or after 1970-01-01-T00:00Z.
12+
*/
13+
export const awsTimestamp = () => Math.floor(Date.now() / 1000);
14+
15+
/**
16+
* AWSDate - An extended ISO 8601 date string in the format YYYY-MM-DD.
17+
*
18+
* @param timezoneOffset - Timezone offset in hours, defaults to 0
19+
*/
20+
export const awsDate = (timezoneOffset = 0) =>
21+
formattedTime(new Date(), '%Y-%m-%d', timezoneOffset);
22+
23+
/**
24+
* AWSTime - An extended ISO 8601 time string in the format hh:mm:ss.sss.
25+
*
26+
* @param timezoneOffset - Timezone offset in hours, defaults to 0
27+
*/
28+
export const awsTime = (timezoneOffset = 0) =>
29+
formattedTime(new Date(), '%H:%M:%S.%f', timezoneOffset);
30+
31+
/**
32+
* AWSDateTime - An extended ISO 8601 date and time string in the format
33+
* YYYY-MM-DDThh:mm:ss.sssZ.
34+
*
35+
* @param timezoneOffset - Timezone offset in hours, defaults to 0
36+
*/
37+
export const awsDateTime = (timezoneOffset = 0) =>
38+
formattedTime(new Date(), '%Y-%m-%dT%H:%M:%S.%f', timezoneOffset);
39+
40+
/**
41+
* String formatted time with optional timezone offset
42+
*
43+
* @param now - Current Date object with zero timezone offset
44+
* @param format - Date format function to apply before adding timezone offset
45+
* @param timezoneOffset - Timezone offset in hours, defaults to 0
46+
*/
47+
const formattedTime = (
48+
now: Date,
49+
format: string,
50+
timezoneOffset: number
51+
): string => {
52+
if (timezoneOffset < -12 || timezoneOffset > 14) {
53+
// Reference: https://en.wikipedia.org/wiki/List_of_UTC_offsets
54+
throw new RangeError(
55+
'timezoneOffset must be between -12 and +14 (inclusive)'
56+
);
57+
}
58+
const adjustedDate = new Date(
59+
now.getTime() + timezoneOffset * 60 * 60 * 1000
60+
);
61+
62+
const formattedDateParts: Record<string, string> = {
63+
'%Y': adjustedDate.getUTCFullYear().toString(),
64+
'%m': (adjustedDate.getUTCMonth() + 1).toString().padStart(2, '0'),
65+
'%d': adjustedDate.getUTCDate().toString().padStart(2, '0'),
66+
'%H': adjustedDate.getUTCHours().toString().padStart(2, '0'),
67+
'%M': adjustedDate.getUTCMinutes().toString().padStart(2, '0'),
68+
'%S': adjustedDate.getUTCSeconds().toString().padStart(2, '0'),
69+
'.%f': `.${adjustedDate.getUTCMilliseconds().toString().padStart(3, '0')}`,
70+
};
71+
72+
const dateTimeStr = format.replace(
73+
/%Y|%m|%d|%H|%M|%S|\.%f/g,
74+
(match) => formattedDateParts[match]
75+
);
76+
77+
let postfix: string;
78+
if (timezoneOffset === 0) {
79+
postfix = 'Z';
80+
} else {
81+
const sign = timezoneOffset > 0 ? '+' : '-';
82+
const absOffset = Math.abs(timezoneOffset);
83+
const hours = Math.floor(absOffset);
84+
const minutes = Math.floor((absOffset - hours) * 60);
85+
postfix = `${sign}${String(hours).padStart(2, '0')}:${String(minutes).padStart(2, '0')}:00`;
86+
}
87+
88+
return `${dateTimeStr}${postfix}`;
89+
};
Lines changed: 143 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,143 @@
1+
import { afterAll, beforeAll, describe, expect, it, vi } from 'vitest';
2+
import {
3+
awsDate,
4+
awsDateTime,
5+
awsTime,
6+
awsTimestamp,
7+
makeId,
8+
} from '../../../src/appsync-graphql/scalarTypesUtils.js';
9+
10+
const mockDate = new Date('2025-06-15T10:30:45.123Z');
11+
describe('Scalar Types Utils', () => {
12+
beforeAll(() => {
13+
vi.useFakeTimers().setSystemTime(mockDate);
14+
});
15+
16+
afterAll(() => {
17+
vi.useRealTimers();
18+
});
19+
describe('makeId', () => {
20+
it('should generate a valid UUID', () => {
21+
const id = makeId();
22+
// UUID v4 format: xxxxxxxx-xxxx-4xxx-yxxx-xxxxxxxxxxxx
23+
const uuidRegex =
24+
/^[0-9a-f]{8}-[0-9a-f]{4}-4[0-9a-f]{3}-[89ab][0-9a-f]{3}-[0-9a-f]{12}$/i;
25+
expect(id).toMatch(uuidRegex);
26+
});
27+
28+
it('should generate unique IDs', () => {
29+
const id1 = makeId();
30+
const id2 = makeId();
31+
expect(id1).not.toBe(id2);
32+
});
33+
});
34+
35+
describe('awsDate', () => {
36+
it('should return a date in YYYY-MM-DD format with Z timezone', () => {
37+
const result = awsDate();
38+
expect(result).toBe('2025-06-15Z');
39+
});
40+
41+
it('should handle positive timezone offset', () => {
42+
const result = awsDate(5);
43+
expect(result).toBe('2025-06-15+05:00:00');
44+
});
45+
46+
it('should handle negative timezone offset', () => {
47+
const result = awsDate(-8);
48+
expect(result).toBe('2025-06-15-08:00:00');
49+
});
50+
51+
it('should handle date change with timezone offset', () => {
52+
const result = awsDate(-11);
53+
expect(result).toBe('2025-06-14-11:00:00');
54+
});
55+
56+
it('should handle fractional timezone offset', () => {
57+
const result = awsDate(5.5);
58+
expect(result).toBe('2025-06-15+05:30:00');
59+
});
60+
61+
it('should handle negative fractional timezone offset', () => {
62+
const result = awsDate(-9.5);
63+
expect(result).toBe('2025-06-15-09:30:00');
64+
});
65+
66+
it('should throw RangeError for invalid timezone offset', () => {
67+
expect(() => awsDate(15)).toThrow(RangeError);
68+
expect(() => awsDate(-13)).toThrow(RangeError);
69+
});
70+
});
71+
72+
describe('awsTime', () => {
73+
it('should return a time in HH:MM:SS.sss format with Z timezone', () => {
74+
const result = awsTime();
75+
expect(result).toBe('10:30:45.123Z');
76+
});
77+
78+
it('should handle positive timezone offset', () => {
79+
const result = awsTime(3);
80+
expect(result).toBe('13:30:45.123+03:00:00');
81+
});
82+
83+
it('should handle negative timezone offset', () => {
84+
const result = awsTime(-5);
85+
expect(result).toBe('05:30:45.123-05:00:00');
86+
});
87+
88+
it('should handle fractional timezone offset', () => {
89+
const result = awsTime(5.5);
90+
expect(result).toBe('16:00:45.123+05:30:00');
91+
});
92+
93+
it('should throw RangeError for invalid timezone offset', () => {
94+
expect(() => awsTime(15)).toThrow(RangeError);
95+
expect(() => awsTime(-13)).toThrow(RangeError);
96+
});
97+
});
98+
99+
describe('awsDateTime', () => {
100+
it('should return a datetime in ISO 8601 format with Z timezone', () => {
101+
const result = awsDateTime();
102+
expect(result).toBe('2025-06-15T10:30:45.123Z');
103+
});
104+
105+
it('should handle positive timezone offset', () => {
106+
const result = awsDateTime(2);
107+
expect(result).toBe('2025-06-15T12:30:45.123+02:00:00');
108+
});
109+
110+
it('should handle negative timezone offset', () => {
111+
const result = awsDateTime(-7);
112+
expect(result).toBe('2025-06-15T03:30:45.123-07:00:00');
113+
});
114+
115+
it('should handle date/time change with timezone offset', () => {
116+
const result = awsDateTime(-11);
117+
expect(result).toBe('2025-06-14T23:30:45.123-11:00:00');
118+
});
119+
120+
it('should handle fractional timezone offset', () => {
121+
const result = awsDateTime(5.5);
122+
expect(result).toBe('2025-06-15T16:00:45.123+05:30:00');
123+
});
124+
125+
it('should handle negative fractional timezone offset', () => {
126+
const result = awsDateTime(-9.5);
127+
expect(result).toBe('2025-06-15T01:00:45.123-09:30:00');
128+
});
129+
130+
it('should throw RangeError for invalid timezone offset', () => {
131+
expect(() => awsDateTime(15)).toThrow(RangeError);
132+
expect(() => awsDateTime(-13)).toThrow(RangeError);
133+
});
134+
});
135+
136+
describe('awsTimestamp', () => {
137+
it('should return current time as Unix timestamp in seconds', () => {
138+
const result = awsTimestamp();
139+
const expected = Math.floor(mockDate.getTime() / 1000);
140+
expect(result).toBe(expected);
141+
});
142+
});
143+
});

0 commit comments

Comments
 (0)