Skip to content

Commit 1c82ce2

Browse files
committed
feat(api): add endpoint to suggest streamers with validation
1 parent e2cab66 commit 1c82ce2

23 files changed

Lines changed: 1357 additions & 23 deletions

apps/backend/src/api/index.ts

Lines changed: 67 additions & 20 deletions
Original file line numberDiff line numberDiff line change
@@ -1,5 +1,5 @@
11
import { zValidator } from '@hono/zod-validator';
2-
import { desc, ilike, isNotNull, or } from 'drizzle-orm';
2+
import { and, desc, eq, ilike, isNotNull, or, type SQL } from 'drizzle-orm';
33
import { Hono } from 'hono';
44
import { z } from 'zod';
55
import cache from '@/lib/cache/default';
@@ -34,6 +34,26 @@ const api = new Hono<ApiContext>()
3434
});
3535
}
3636

37+
// Build the where condition with approved filter
38+
const conditions: SQL[] = [eq(streamer.approved, true)];
39+
40+
if (search && platform) {
41+
const searchCondition =
42+
platform === 'twitch'
43+
? ilike(streamer.twitchUsername, `${search}`)
44+
: ilike(streamer.kickUsername, `${search}`);
45+
conditions.push(searchCondition);
46+
} else if (search) {
47+
conditions.push(
48+
// biome-ignore lint/style/noNonNullAssertion: DO NOT CARE
49+
or(
50+
ilike(streamer.name, `${search}`),
51+
ilike(streamer.twitchUsername, `${search}`),
52+
ilike(streamer.kickUsername, `${search}`)
53+
)!
54+
);
55+
}
56+
3757
const streamers = await db.query.streamer.findMany({
3858
columns: {
3959
id: true,
@@ -47,18 +67,7 @@ const api = new Hono<ApiContext>()
4767
category: true,
4868
title: true,
4969
},
50-
where:
51-
search && platform
52-
? platform === 'twitch'
53-
? ilike(streamer.twitchUsername, `${search}`)
54-
: ilike(streamer.kickUsername, `${search}`)
55-
: search
56-
? or(
57-
ilike(streamer.name, `${search}`),
58-
ilike(streamer.twitchUsername, `${search}`),
59-
ilike(streamer.kickUsername, `${search}`)
60-
)
61-
: undefined,
70+
where: and(...conditions),
6271
orderBy: [desc(streamer.isLive), desc(streamer.viewerCount)],
6372
});
6473

@@ -79,20 +88,58 @@ const api = new Hono<ApiContext>()
7988
});
8089
}
8190

82-
// Return streamers that have both a twitch and kick username
91+
// Return streamers that have both a twitch and kick username AND are approved
8392
const streamers = await db.query.streamer.findMany({
84-
where: (streamer, { and }) =>
85-
and(
86-
isNotNull(streamer.twitchUsername),
87-
isNotNull(streamer.kickUsername)
88-
),
93+
where: and(
94+
eq(streamer.approved, true),
95+
isNotNull(streamer.twitchUsername),
96+
isNotNull(streamer.kickUsername)
97+
),
8998
});
9099

91100
await cache.set('streamers-multi', JSON.stringify(streamers));
92101

93102
return c.json({
94103
streamers,
95104
});
96-
});
105+
})
106+
.post(
107+
'/streamers/suggest',
108+
zValidator(
109+
'json',
110+
z.object({
111+
name: z.string(),
112+
twitchUsername: z.string(),
113+
kickUsername: z.string(),
114+
})
115+
),
116+
async (c) => {
117+
const { name, twitchUsername, kickUsername } = await c.req.json();
118+
119+
try {
120+
await db
121+
.insert(streamer)
122+
.values({
123+
name: name.trim().toLowerCase(),
124+
twitchUsername: twitchUsername.trim().toLowerCase(),
125+
kickUsername: kickUsername.trim().toLowerCase(),
126+
approved: false,
127+
})
128+
.onConflictDoNothing();
129+
130+
return c.json({
131+
message: 'Streamer suggested',
132+
});
133+
} catch (error) {
134+
logger.withError(error).error('Failed to suggest streamer');
135+
return c.json(
136+
{
137+
message: 'Failed to suggest streamer',
138+
},
139+
500
140+
);
141+
}
142+
}
143+
);
97144

98145
export default api;

apps/backend/src/lib/db/schema.ts

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -50,6 +50,7 @@ export const streamer = pgTable(
5050
viewerCount: integer('viewer_count').default(0),
5151
category: text('category'),
5252
title: text('title'),
53+
approved: boolean('approved').default(false),
5354
...timestamps,
5455
},
5556
(table) => [
Lines changed: 31 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,31 @@
1+
CREATE TYPE "public"."stream_platform" AS ENUM('twitch', 'kick');--> statement-breakpoint
2+
CREATE TABLE "config" (
3+
"id" text PRIMARY KEY NOT NULL,
4+
"key" text NOT NULL,
5+
"value" text NOT NULL,
6+
"created_at" timestamp DEFAULT now(),
7+
"updated_at" timestamp DEFAULT now(),
8+
CONSTRAINT "config_key_unique" UNIQUE("key")
9+
);
10+
--> statement-breakpoint
11+
CREATE TABLE "streamer" (
12+
"id" text PRIMARY KEY NOT NULL,
13+
"twitch_id" text,
14+
"kick_id" text,
15+
"twitch_username" text,
16+
"kick_username" text,
17+
"name" text NOT NULL,
18+
"avatar_url" text,
19+
"is_live" boolean DEFAULT false,
20+
"live_platform" "stream_platform",
21+
"viewer_count" integer DEFAULT 0,
22+
"category" text,
23+
"title" text,
24+
"created_at" timestamp DEFAULT now(),
25+
"updated_at" timestamp DEFAULT now(),
26+
CONSTRAINT "streamer_twitch_id_unique" UNIQUE("twitch_id"),
27+
CONSTRAINT "streamer_kick_id_unique" UNIQUE("kick_id"),
28+
CONSTRAINT "streamer_twitch_username_unique" UNIQUE("twitch_username"),
29+
CONSTRAINT "streamer_kick_username_unique" UNIQUE("kick_username"),
30+
CONSTRAINT "at_least_one_username" CHECK ("streamer"."twitch_username" IS NOT NULL OR "streamer"."kick_username" IS NOT NULL)
31+
);
Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1 @@
1+
ALTER TABLE "streamer" ADD COLUMN "approved" boolean DEFAULT false;
Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1 @@
1+
ALTER TABLE "streamer" ALTER COLUMN "approved" SET DEFAULT false;
Lines changed: 217 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,217 @@
1+
{
2+
"id": "80bf0bf7-43a7-45b5-955f-3562948b9583",
3+
"prevId": "00000000-0000-0000-0000-000000000000",
4+
"version": "7",
5+
"dialect": "postgresql",
6+
"tables": {
7+
"public.config": {
8+
"name": "config",
9+
"schema": "",
10+
"columns": {
11+
"id": {
12+
"name": "id",
13+
"type": "text",
14+
"primaryKey": true,
15+
"notNull": true
16+
},
17+
"key": {
18+
"name": "key",
19+
"type": "text",
20+
"primaryKey": false,
21+
"notNull": true
22+
},
23+
"value": {
24+
"name": "value",
25+
"type": "text",
26+
"primaryKey": false,
27+
"notNull": true
28+
},
29+
"created_at": {
30+
"name": "created_at",
31+
"type": "timestamp",
32+
"primaryKey": false,
33+
"notNull": false,
34+
"default": "now()"
35+
},
36+
"updated_at": {
37+
"name": "updated_at",
38+
"type": "timestamp",
39+
"primaryKey": false,
40+
"notNull": false,
41+
"default": "now()"
42+
}
43+
},
44+
"indexes": {},
45+
"foreignKeys": {},
46+
"compositePrimaryKeys": {},
47+
"uniqueConstraints": {
48+
"config_key_unique": {
49+
"name": "config_key_unique",
50+
"nullsNotDistinct": false,
51+
"columns": [
52+
"key"
53+
]
54+
}
55+
},
56+
"policies": {},
57+
"checkConstraints": {},
58+
"isRLSEnabled": false
59+
},
60+
"public.streamer": {
61+
"name": "streamer",
62+
"schema": "",
63+
"columns": {
64+
"id": {
65+
"name": "id",
66+
"type": "text",
67+
"primaryKey": true,
68+
"notNull": true
69+
},
70+
"twitch_id": {
71+
"name": "twitch_id",
72+
"type": "text",
73+
"primaryKey": false,
74+
"notNull": false
75+
},
76+
"kick_id": {
77+
"name": "kick_id",
78+
"type": "text",
79+
"primaryKey": false,
80+
"notNull": false
81+
},
82+
"twitch_username": {
83+
"name": "twitch_username",
84+
"type": "text",
85+
"primaryKey": false,
86+
"notNull": false
87+
},
88+
"kick_username": {
89+
"name": "kick_username",
90+
"type": "text",
91+
"primaryKey": false,
92+
"notNull": false
93+
},
94+
"name": {
95+
"name": "name",
96+
"type": "text",
97+
"primaryKey": false,
98+
"notNull": true
99+
},
100+
"avatar_url": {
101+
"name": "avatar_url",
102+
"type": "text",
103+
"primaryKey": false,
104+
"notNull": false
105+
},
106+
"is_live": {
107+
"name": "is_live",
108+
"type": "boolean",
109+
"primaryKey": false,
110+
"notNull": false,
111+
"default": false
112+
},
113+
"live_platform": {
114+
"name": "live_platform",
115+
"type": "stream_platform",
116+
"typeSchema": "public",
117+
"primaryKey": false,
118+
"notNull": false
119+
},
120+
"viewer_count": {
121+
"name": "viewer_count",
122+
"type": "integer",
123+
"primaryKey": false,
124+
"notNull": false,
125+
"default": 0
126+
},
127+
"category": {
128+
"name": "category",
129+
"type": "text",
130+
"primaryKey": false,
131+
"notNull": false
132+
},
133+
"title": {
134+
"name": "title",
135+
"type": "text",
136+
"primaryKey": false,
137+
"notNull": false
138+
},
139+
"created_at": {
140+
"name": "created_at",
141+
"type": "timestamp",
142+
"primaryKey": false,
143+
"notNull": false,
144+
"default": "now()"
145+
},
146+
"updated_at": {
147+
"name": "updated_at",
148+
"type": "timestamp",
149+
"primaryKey": false,
150+
"notNull": false,
151+
"default": "now()"
152+
}
153+
},
154+
"indexes": {},
155+
"foreignKeys": {},
156+
"compositePrimaryKeys": {},
157+
"uniqueConstraints": {
158+
"streamer_twitch_id_unique": {
159+
"name": "streamer_twitch_id_unique",
160+
"nullsNotDistinct": false,
161+
"columns": [
162+
"twitch_id"
163+
]
164+
},
165+
"streamer_kick_id_unique": {
166+
"name": "streamer_kick_id_unique",
167+
"nullsNotDistinct": false,
168+
"columns": [
169+
"kick_id"
170+
]
171+
},
172+
"streamer_twitch_username_unique": {
173+
"name": "streamer_twitch_username_unique",
174+
"nullsNotDistinct": false,
175+
"columns": [
176+
"twitch_username"
177+
]
178+
},
179+
"streamer_kick_username_unique": {
180+
"name": "streamer_kick_username_unique",
181+
"nullsNotDistinct": false,
182+
"columns": [
183+
"kick_username"
184+
]
185+
}
186+
},
187+
"policies": {},
188+
"checkConstraints": {
189+
"at_least_one_username": {
190+
"name": "at_least_one_username",
191+
"value": "\"streamer\".\"twitch_username\" IS NOT NULL OR \"streamer\".\"kick_username\" IS NOT NULL"
192+
}
193+
},
194+
"isRLSEnabled": false
195+
}
196+
},
197+
"enums": {
198+
"public.stream_platform": {
199+
"name": "stream_platform",
200+
"schema": "public",
201+
"values": [
202+
"twitch",
203+
"kick"
204+
]
205+
}
206+
},
207+
"schemas": {},
208+
"sequences": {},
209+
"roles": {},
210+
"policies": {},
211+
"views": {},
212+
"_meta": {
213+
"columns": {},
214+
"schemas": {},
215+
"tables": {}
216+
}
217+
}

0 commit comments

Comments
 (0)