Skip to content

Commit 6f360a8

Browse files
committed
Merge branch 'main' of github.com:izadoesdev/mono
2 parents f5b81f3 + a50ba20 commit 6f360a8

27 files changed

Lines changed: 1268 additions & 358 deletions

apps/api2/biome.json

Lines changed: 30 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,30 @@
1+
{
2+
"$schema": "https://biomejs.dev/schemas/1.9.4/schema.json",
3+
"vcs": {
4+
"enabled": false,
5+
"clientKind": "git",
6+
"useIgnoreFile": false
7+
},
8+
"files": {
9+
"ignoreUnknown": false,
10+
"ignore": []
11+
},
12+
"formatter": {
13+
"enabled": true,
14+
"indentStyle": "tab"
15+
},
16+
"organizeImports": {
17+
"enabled": true
18+
},
19+
"linter": {
20+
"enabled": true,
21+
"rules": {
22+
"recommended": true
23+
}
24+
},
25+
"javascript": {
26+
"formatter": {
27+
"quoteStyle": "double"
28+
}
29+
}
30+
}

apps/api2/package.json

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -13,6 +13,7 @@
1313
"typescript": "^5"
1414
},
1515
"dependencies": {
16+
"dayjs": "^1.11.13",
1617
"elysia": "^1.3.5"
1718
}
1819
}
Lines changed: 57 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,57 @@
1+
import { clickHouse, clix, tables } from '@databuddy/db';
2+
import type { User } from '@databuddy/auth';
3+
import type { WebsiteType } from '../types';
4+
import type { TimezoneInfo } from '../lib/timezone';
5+
import { Elysia, t } from 'elysia';
6+
7+
type QueryContext = {
8+
user: User;
9+
website: WebsiteType;
10+
timezoneInfo: TimezoneInfo;
11+
body: {
12+
name: string;
13+
params?: Record<string, any>;
14+
};
15+
};
16+
17+
// Define individual query functions
18+
const queries = {
19+
'page-views': async (websiteId: string, params?: Record<string, any>) => {
20+
const query = tables
21+
.events(clickHouse)
22+
.select(['count() as count'], 'replace')
23+
.where('client_id', '=', websiteId)
24+
.where('event_name', '=', 'screen_view');
25+
return query.execute();
26+
},
27+
// ... other queries can be added here
28+
};
29+
30+
export const executeQuery = async (context: QueryContext) => {
31+
const { website, body } = context;
32+
const { name, params } = body;
33+
34+
if (!website?.id) {
35+
throw new Error('Website not found');
36+
}
37+
38+
const queryFunction = queries[name as keyof typeof queries];
39+
40+
if (!queryFunction) {
41+
throw new Error(`Query not found: ${name}`);
42+
}
43+
44+
try {
45+
const results = await queryFunction(website.id, params);
46+
return {
47+
success: true,
48+
data: results,
49+
};
50+
} catch (error) {
51+
console.error(`Error executing query: ${name}`, {
52+
error,
53+
website_id: website.id,
54+
});
55+
throw new Error(`Error executing query: ${name}`);
56+
}
57+
};

apps/api2/src/index.ts

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -1,12 +1,12 @@
11
import { Elysia } from "elysia";
2-
import domainsRouter from "./routes/v1/domains";
2+
import v1Router from "./routes/v1";
33

44
const app = new Elysia();
55

66
app.get('/', () => {
77
return 'Hello World';
88
})
9-
.use(domainsRouter)
9+
.use(v1Router)
1010

1111
app.listen(3500)
1212
.onStart(() => {

apps/api2/src/lib/timezone.ts

Lines changed: 59 additions & 128 deletions
Original file line numberDiff line numberDiff line change
@@ -2,175 +2,106 @@
22
* Timezone utilities for handling user timezone detection and date conversions
33
*/
44

5-
export interface TimezoneInfo {
5+
import dayjs from 'dayjs';
6+
import utc from 'dayjs/plugin/utc';
7+
import timezone from 'dayjs/plugin/timezone';
8+
9+
dayjs.extend(utc);
10+
dayjs.extend(timezone);
11+
12+
export type TimezoneInfo = {
613
timezone: string;
7-
detected: boolean;
8-
source: 'explicit' | 'header' | 'cloudflare' | 'accept-language' | 'default';
9-
}
14+
offset: number;
15+
};
1016

1117
/**
12-
* Detect user timezone from various sources
18+
* Validate timezone string
1319
*/
14-
export function detectUserTimezone(headers: Headers, explicitTimezone?: string): TimezoneInfo {
15-
// Use explicit timezone if provided and valid
16-
if (explicitTimezone) {
17-
try {
18-
Intl.DateTimeFormat(undefined, { timeZone: explicitTimezone });
19-
return {
20-
timezone: explicitTimezone,
21-
detected: true,
22-
source: 'explicit'
23-
};
24-
} catch {
25-
// Invalid timezone, fall through to header detection
26-
}
20+
export function isValidTimezone(tz: string): boolean {
21+
try {
22+
// dayjs.tz throws an error for invalid timezones
23+
dayjs().tz(tz);
24+
return true;
25+
} catch (e) {
26+
return false;
2727
}
28+
}
2829

29-
// Try to get timezone from various headers
30-
const timezoneHeader = headers.get('x-timezone') ||
31-
headers.get('timezone') ||
32-
headers.get('x-user-timezone');
33-
34-
if (timezoneHeader) {
35-
try {
36-
// Validate timezone
37-
Intl.DateTimeFormat(undefined, { timeZone: timezoneHeader });
38-
return {
39-
timezone: timezoneHeader,
40-
detected: true,
41-
source: 'header'
42-
};
43-
} catch {
44-
// Invalid timezone, fall through to other methods
45-
}
46-
}
30+
/**
31+
* Detect user timezone from various sources
32+
*/
33+
export function detectUserTimezone(
34+
headers: Headers,
35+
explicitTimezone?: string,
36+
): TimezoneInfo {
37+
const timezonesToCheck = [
38+
explicitTimezone,
39+
headers.get('x-vercel-ip-timezone'),
40+
headers.get('cf-timezone'),
41+
];
4742

48-
// Try to infer from CloudFlare headers (if using CloudFlare)
49-
const cfTimezone = headers.get('cf-timezone');
50-
if (cfTimezone) {
51-
try {
52-
Intl.DateTimeFormat(undefined, { timeZone: cfTimezone });
43+
for (const tz of timezonesToCheck) {
44+
if (tz && isValidTimezone(tz)) {
5345
return {
54-
timezone: cfTimezone,
55-
detected: true,
56-
source: 'cloudflare'
46+
timezone: tz,
47+
offset: dayjs().tz(tz).utcOffset(),
5748
};
58-
} catch {
59-
// Invalid timezone
6049
}
6150
}
6251

63-
// Try to extract from Accept-Language header
64-
const acceptLanguage = headers.get('accept-language');
65-
if (acceptLanguage) {
66-
// Look for timezone info in accept-language (some browsers include it)
67-
const timezoneMatch = acceptLanguage.match(/timezone=([^,;]+)/i);
68-
if (timezoneMatch) {
69-
try {
70-
Intl.DateTimeFormat(undefined, { timeZone: timezoneMatch[1] });
71-
return {
72-
timezone: timezoneMatch[1],
73-
detected: true,
74-
source: 'accept-language'
75-
};
76-
} catch {
77-
// Invalid timezone
78-
}
79-
}
80-
}
81-
82-
// Default to UTC if no timezone detected
8352
return {
8453
timezone: 'UTC',
85-
detected: false,
86-
source: 'default'
54+
offset: 0,
8755
};
8856
}
8957

9058
/**
91-
* Convert date to user's timezone
59+
* Convert date string to a string in the user's timezone
9260
*/
9361
export function convertToUserTimezone(date: string, timezone: string): string {
94-
try {
95-
const utcDate = new Date(`${date}T00:00:00Z`);
96-
const userDate = new Date(utcDate.toLocaleString('en-US', { timeZone: timezone }));
97-
return userDate.toISOString().split('T')[0];
98-
} catch {
99-
// If timezone conversion fails, return original date
62+
if (!isValidTimezone(timezone)) {
10063
return date;
10164
}
65+
return dayjs(date).tz(timezone).format('YYYY-MM-DD');
10266
}
10367

10468
/**
105-
* Adjust date range for user timezone to ensure we capture all relevant data
69+
* Adjust a date range to fully encompass the range in a given timezone, returning the new range in UTC.
10670
*/
10771
export function adjustDateRangeForTimezone(
108-
startDate: string,
109-
endDate: string,
110-
timezone: string
111-
): { startDate: string, endDate: string } {
112-
if (timezone === 'UTC') {
72+
startDate: string,
73+
endDate: string,
74+
timezone: string,
75+
): { startDate: string; endDate: string } {
76+
if (!isValidTimezone(timezone) || timezone === 'UTC') {
11377
return { startDate, endDate };
11478
}
11579

116-
try {
117-
// Convert start date to user timezone (might need to go back a day)
118-
const startUTC = new Date(`${startDate}T00:00:00Z`);
119-
const startInUserTZ = new Date(startUTC.toLocaleString('en-US', { timeZone: timezone }));
120-
121-
// Convert end date to user timezone (might need to go forward a day)
122-
const endUTC = new Date(`${endDate}T23:59:59Z`);
123-
const endInUserTZ = new Date(endUTC.toLocaleString('en-US', { timeZone: timezone }));
124-
125-
// Adjust the range to ensure we capture all data for the user's timezone
126-
const adjustedStart = new Date(startInUserTZ.getTime() - 24 * 60 * 60 * 1000); // Go back 1 day
127-
const adjustedEnd = new Date(endInUserTZ.getTime() + 24 * 60 * 60 * 1000); // Go forward 1 day
80+
const start = dayjs.tz(startDate, timezone).startOf('day').utc().format('YYYY-MM-DD');
81+
const end = dayjs.tz(endDate, timezone).endOf('day').utc().format('YYYY-MM-DD');
12882

129-
return {
130-
startDate: adjustedStart.toISOString().split('T')[0],
131-
endDate: adjustedEnd.toISOString().split('T')[0]
132-
};
133-
} catch {
134-
// If timezone conversion fails, return original dates
135-
return { startDate, endDate };
136-
}
83+
return { startDate: start, endDate: end };
13784
}
13885

13986
/**
140-
* Validate timezone string
141-
*/
142-
export function isValidTimezone(timezone: string): boolean {
143-
try {
144-
Intl.DateTimeFormat(undefined, { timeZone: timezone });
145-
return true;
146-
} catch {
147-
return false;
148-
}
149-
}
150-
151-
/**
152-
* Get current date in user's timezone
87+
* Get the current date as a string in the given timezone.
15388
*/
15489
export function getCurrentDateInTimezone(timezone: string): string {
155-
try {
156-
const now = new Date();
157-
const userDate = new Date(now.toLocaleString('en-US', { timeZone: timezone }));
158-
return userDate.toISOString().split('T')[0];
159-
} catch {
160-
// If timezone conversion fails, return UTC date
161-
return new Date().toISOString().split('T')[0];
90+
if (!isValidTimezone(timezone)) {
91+
return dayjs().utc().format('YYYY-MM-DD');
16292
}
93+
return dayjs().tz(timezone).format('YYYY-MM-DD');
16394
}
16495

16596
/**
166-
* Convert timestamp to user's timezone
97+
* Convert a UNIX timestamp to a Date object in the given timezone.
16798
*/
168-
export function convertTimestampToUserTimezone(timestamp: number, timezone: string): Date {
169-
try {
170-
const utcDate = new Date(timestamp);
171-
return new Date(utcDate.toLocaleString('en-US', { timeZone: timezone }));
172-
} catch {
173-
// If timezone conversion fails, return original date
99+
export function convertTimestampToUserTimezone(
100+
timestamp: number,
101+
timezone: string,
102+
): Date {
103+
if (!isValidTimezone(timezone)) {
174104
return new Date(timestamp);
175105
}
106+
return dayjs(timestamp).tz(timezone).toDate();
176107
}

apps/api2/src/middleware/website.ts

Lines changed: 14 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -2,13 +2,14 @@ import { Elysia } from "elysia";
22
import { db, eq, and, websites, projects } from "@databuddy/db";
33
import { cacheable } from "@databuddy/redis";
44
import type { User } from "../lib/auth";
5-
import { WebsiteType } from "../types";
5+
import type { WebsiteType } from "../types";
66

77
export const getWebsiteById = cacheable(
8-
async (id: string): Promise<WebsiteType> => {
9-
return db.query.websites.findFirst({
8+
async (id: string): Promise<WebsiteType | null> => {
9+
const website = await db.query.websites.findFirst({
1010
where: eq(websites.id, id)
1111
});
12+
return website ?? null;
1213
},
1314
{
1415
expireInSec: 300,
@@ -65,6 +66,9 @@ export const websiteMiddleware = (options: WebsiteAuthOptions = { required: fals
6566
const websiteId = request.headers.get('X-Website-Id') || new URL(request.url).searchParams.get('website_id') || new URL(request.url).searchParams.get('websiteId');
6667

6768
if (!websiteId) {
69+
if (options.required) {
70+
throw new Error("Website ID is required");
71+
}
6872
return { website: null };
6973
}
7074

@@ -76,6 +80,13 @@ export const websiteMiddleware = (options: WebsiteAuthOptions = { required: fals
7680

7781
const website = await getWebsiteById(websiteId);
7882

83+
if (!website) {
84+
if (options.required) {
85+
throw new Error("Website not found");
86+
}
87+
return { website: null };
88+
}
89+
7990
return { website };
8091
})
8192
.onBeforeHandle(({ website, set }) => {

0 commit comments

Comments
 (0)