create-svc 0.1.10 → 0.1.12

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (171) hide show
  1. package/README.md +51 -47
  2. package/index.ts +2 -2
  3. package/package.json +10 -9
  4. package/src/cli.test.ts +28 -10
  5. package/src/cli.ts +196 -33
  6. package/src/git-bootstrap.test.ts +40 -0
  7. package/src/git-bootstrap.ts +110 -0
  8. package/src/naming.test.ts +1 -0
  9. package/src/naming.ts +23 -0
  10. package/src/post-scaffold.test.ts +19 -0
  11. package/src/post-scaffold.ts +17 -4
  12. package/src/profiles.ts +2 -5
  13. package/src/scaffold.test.ts +232 -41
  14. package/src/scaffold.ts +81 -36
  15. package/src/service.test.ts +30 -0
  16. package/src/service.ts +65 -0
  17. package/src/vault.test.ts +61 -1
  18. package/src/vault.ts +77 -15
  19. package/templates/shared/.github/workflows/ci.yml +2 -1
  20. package/templates/shared/.github/workflows/deploy.yml +2 -0
  21. package/templates/shared/README.md +124 -47
  22. package/templates/shared/grafana/alerts.yaml +54 -0
  23. package/templates/shared/grafana/waitlist-dashboard.json +63 -0
  24. package/templates/shared/scripts/authctl.ts +231 -0
  25. package/templates/shared/scripts/cloudrun/bootstrap.ts +14 -5
  26. package/templates/shared/scripts/cloudrun/cleanup.ts +64 -4
  27. package/templates/shared/scripts/cloudrun/cli.ts +329 -7
  28. package/templates/shared/scripts/cloudrun/config.ts +11 -4
  29. package/templates/shared/scripts/cloudrun/deploy.ts +0 -4
  30. package/templates/shared/scripts/cloudrun/lib.ts +174 -41
  31. package/templates/shared/scripts/cloudrun/neon.ts +45 -0
  32. package/templates/shared/scripts/dev.ts +22 -0
  33. package/templates/shared/scripts/ensure-local-db.ts +3 -0
  34. package/templates/shared/scripts/local-docker.ts +63 -0
  35. package/templates/shared/scripts/local-env.ts +27 -0
  36. package/templates/shared/scripts/seed.ts +73 -0
  37. package/templates/shared/scripts/wait-for-db.ts +32 -0
  38. package/templates/shared/service.config.ts +59 -0
  39. package/templates/shared/service.yaml +24 -44
  40. package/templates/targets/workers/.github/workflows/ci.yml +19 -0
  41. package/templates/targets/workers/.github/workflows/deploy.yml +19 -0
  42. package/templates/targets/workers/Makefile +33 -0
  43. package/templates/targets/workers/README.md +75 -0
  44. package/templates/targets/workers/package.json +35 -0
  45. package/templates/targets/workers/scripts/workers/cli.ts +402 -0
  46. package/templates/targets/workers/src/auth.ts +178 -0
  47. package/templates/targets/workers/src/index.ts +198 -0
  48. package/templates/targets/workers/src/storage.ts +370 -0
  49. package/templates/targets/workers/test/app.test.ts +108 -0
  50. package/templates/targets/workers/tsconfig.json +11 -0
  51. package/templates/targets/workers/wrangler.toml +24 -0
  52. package/templates/variants/bun-connectrpc/Makefile +14 -8
  53. package/templates/variants/bun-connectrpc/gen/protos/waitlist/v1/waitlist_pb.ts +424 -0
  54. package/templates/variants/bun-connectrpc/migrations/0000_init.sql +12 -55
  55. package/templates/variants/bun-connectrpc/package.json +12 -5
  56. package/templates/variants/bun-connectrpc/protos/waitlist/v1/waitlist.proto +91 -0
  57. package/templates/variants/bun-connectrpc/scripts/codegen.ts +1 -1
  58. package/templates/variants/bun-connectrpc/scripts/migrate.ts +4 -1
  59. package/templates/variants/bun-connectrpc/src/auth.ts +200 -0
  60. package/templates/variants/bun-connectrpc/src/db/repository.ts +67 -420
  61. package/templates/variants/bun-connectrpc/src/db/schema.ts +15 -64
  62. package/templates/variants/bun-connectrpc/src/index.ts +76 -176
  63. package/templates/variants/bun-connectrpc/src/temporal/activities.ts +14 -0
  64. package/templates/variants/bun-connectrpc/src/temporal/worker.ts +38 -0
  65. package/templates/variants/bun-connectrpc/src/temporal/workflows.ts +10 -0
  66. package/templates/variants/bun-connectrpc/src/waitlist/service.ts +172 -0
  67. package/templates/variants/bun-connectrpc/src/waitlist/types.ts +45 -0
  68. package/templates/variants/bun-connectrpc/test/app.test.ts +4 -4
  69. package/templates/variants/bun-connectrpc/test/waitlist.integration.test.ts +71 -0
  70. package/templates/variants/bun-hono/Makefile +14 -8
  71. package/templates/variants/bun-hono/migrations/0000_init.sql +12 -55
  72. package/templates/variants/bun-hono/package.json +12 -5
  73. package/templates/variants/bun-hono/scripts/migrate.ts +4 -1
  74. package/templates/variants/bun-hono/src/auth.ts +181 -0
  75. package/templates/variants/bun-hono/src/db/repository.ts +68 -421
  76. package/templates/variants/bun-hono/src/db/schema.ts +15 -64
  77. package/templates/variants/bun-hono/src/index.ts +65 -180
  78. package/templates/variants/bun-hono/src/temporal/activities.ts +14 -0
  79. package/templates/variants/bun-hono/src/temporal/worker.ts +38 -0
  80. package/templates/variants/bun-hono/src/temporal/workflows.ts +10 -0
  81. package/templates/variants/bun-hono/src/waitlist/service.ts +166 -0
  82. package/templates/variants/bun-hono/src/waitlist/types.ts +50 -0
  83. package/templates/variants/bun-hono/test/app.test.ts +72 -41
  84. package/templates/variants/bun-hono/test/waitlist.integration.test.ts +102 -0
  85. package/templates/variants/go-chi/Makefile +27 -11
  86. package/templates/variants/go-chi/atlas.hcl +8 -0
  87. package/templates/variants/go-chi/cmd/server/main.go +21 -10
  88. package/templates/variants/go-chi/go.mod +1 -3
  89. package/templates/variants/go-chi/internal/app/service.go +202 -685
  90. package/templates/variants/go-chi/internal/auth/middleware.go +289 -0
  91. package/templates/variants/go-chi/internal/auth/middleware_test.go +38 -0
  92. package/templates/variants/go-chi/internal/config/config.go +27 -11
  93. package/templates/variants/go-chi/internal/httpapi/routes.go +78 -157
  94. package/templates/variants/go-chi/internal/httpapi/waitlist_integration_test.go +199 -0
  95. package/templates/variants/go-chi/internal/temporal/activities.go +27 -0
  96. package/templates/variants/go-chi/internal/temporal/worker.go +42 -0
  97. package/templates/variants/go-chi/internal/temporal/workflows.go +18 -0
  98. package/templates/variants/go-chi/migrations/0000_init.sql +12 -55
  99. package/templates/variants/go-chi/migrations/atlas.sum +2 -0
  100. package/templates/variants/go-chi/package.json +7 -1
  101. package/templates/variants/go-connectrpc/Makefile +26 -9
  102. package/templates/variants/go-connectrpc/atlas.hcl +8 -0
  103. package/templates/variants/go-connectrpc/buf.gen.yaml +2 -2
  104. package/templates/variants/go-connectrpc/cmd/server/main.go +23 -12
  105. package/templates/variants/go-connectrpc/gen/waitlist/v1/waitlist.pb.go +960 -0
  106. package/templates/variants/go-connectrpc/gen/waitlist/v1/waitlistv1connect/waitlist.connect.go +283 -0
  107. package/templates/variants/go-connectrpc/go.mod +1 -1
  108. package/templates/variants/go-connectrpc/internal/app/service.go +202 -685
  109. package/templates/variants/go-connectrpc/internal/auth/middleware.go +289 -0
  110. package/templates/variants/go-connectrpc/internal/auth/middleware_test.go +38 -0
  111. package/templates/variants/go-connectrpc/internal/config/config.go +27 -11
  112. package/templates/variants/go-connectrpc/internal/connectapi/handler.go +78 -201
  113. package/templates/variants/go-connectrpc/internal/connectapi/waitlist_integration_test.go +122 -0
  114. package/templates/variants/go-connectrpc/internal/httpapi/routes.go +147 -9
  115. package/templates/variants/go-connectrpc/internal/temporal/activities.go +27 -0
  116. package/templates/variants/go-connectrpc/internal/temporal/worker.go +42 -0
  117. package/templates/variants/go-connectrpc/internal/temporal/workflows.go +18 -0
  118. package/templates/variants/go-connectrpc/migrations/0000_init.sql +12 -55
  119. package/templates/variants/go-connectrpc/migrations/atlas.sum +2 -0
  120. package/templates/variants/go-connectrpc/package.json +7 -1
  121. package/templates/variants/go-connectrpc/protos/waitlist/v1/waitlist.proto +93 -0
  122. package/templates/root/.github/workflows/buf-publish.yml +0 -19
  123. package/templates/root/.github/workflows/ci.yml +0 -26
  124. package/templates/root/.github/workflows/deploy.yml +0 -22
  125. package/templates/root/Dockerfile +0 -23
  126. package/templates/root/README.md +0 -69
  127. package/templates/root/buf.gen.yaml +0 -10
  128. package/templates/root/buf.yaml +0 -9
  129. package/templates/root/cmd/server/main.go +0 -44
  130. package/templates/root/gen/dns/v1/dns.pb.go +0 -623
  131. package/templates/root/gen/dns/v1/dnsv1connect/dns.connect.go +0 -192
  132. package/templates/root/go.mod +0 -10
  133. package/templates/root/internal/app/service.go +0 -152
  134. package/templates/root/internal/app/token_source.go +0 -50
  135. package/templates/root/internal/cloudflare/client.go +0 -160
  136. package/templates/root/internal/config/config.go +0 -55
  137. package/templates/root/internal/connectapi/handler.go +0 -79
  138. package/templates/root/internal/httpapi/routes.go +0 -93
  139. package/templates/root/internal/vault/client.go +0 -148
  140. package/templates/root/package.json +0 -12
  141. package/templates/root/protos/dns/v1/dns.proto +0 -58
  142. package/templates/root/scripts/cloudrun/bootstrap.ts +0 -65
  143. package/templates/root/scripts/cloudrun/config.ts +0 -50
  144. package/templates/root/scripts/cloudrun/deploy.ts +0 -41
  145. package/templates/root/scripts/cloudrun/lib.ts +0 -244
  146. package/templates/root/service.yaml +0 -50
  147. package/templates/root/test/go.test.ts +0 -19
  148. package/templates/shared/scripts/cloudrun/integrations.ts +0 -111
  149. package/templates/variants/bun-connectrpc/gen/protos/chat/v1/chat_pb.ts +0 -1078
  150. package/templates/variants/bun-connectrpc/protos/chat/v1/chat.proto +0 -228
  151. package/templates/variants/bun-connectrpc/src/chat/service.ts +0 -384
  152. package/templates/variants/bun-connectrpc/src/chat/types.ts +0 -142
  153. package/templates/variants/bun-connectrpc/src/storage.ts +0 -72
  154. package/templates/variants/bun-connectrpc/src/webhooks.ts +0 -35
  155. package/templates/variants/bun-connectrpc/test/list-messages.integration.test.ts +0 -182
  156. package/templates/variants/bun-hono/src/chat/service.ts +0 -384
  157. package/templates/variants/bun-hono/src/chat/types.ts +0 -142
  158. package/templates/variants/bun-hono/src/storage.ts +0 -72
  159. package/templates/variants/bun-hono/src/webhooks.ts +0 -35
  160. package/templates/variants/bun-hono/test/list-messages.integration.test.ts +0 -256
  161. package/templates/variants/go-chi/buf.gen.yaml +0 -12
  162. package/templates/variants/go-chi/buf.yaml +0 -9
  163. package/templates/variants/go-chi/cmd/migrate/main.go +0 -101
  164. package/templates/variants/go-chi/internal/httpapi/list_messages_integration_test.go +0 -298
  165. package/templates/variants/go-chi/protos/chat/v1/chat.proto +0 -219
  166. package/templates/variants/go-connectrpc/cmd/migrate/main.go +0 -101
  167. package/templates/variants/go-connectrpc/gen/chat/v1/chat.pb.go +0 -2512
  168. package/templates/variants/go-connectrpc/gen/chat/v1/chatv1connect/chat.connect.go +0 -571
  169. package/templates/variants/go-connectrpc/internal/connectapi/list_messages_integration_test.go +0 -216
  170. package/templates/variants/go-connectrpc/protos/chat/v1/chat.proto +0 -232
  171. /package/bin/{create-svc.mjs → service.mjs} +0 -0
@@ -1,479 +1,126 @@
1
- import { and, asc, desc, eq, inArray, isNull, lt, or } from "drizzle-orm";
1
+ import { desc, eq } from "drizzle-orm";
2
2
  import type { createDb } from "./client";
3
- import { attachments, conversationParticipants, conversations, messages, users, webhookEvents } from "./schema";
4
- import type {
5
- ChatAttachment,
6
- ChatConversation,
7
- ChatMessage,
8
- ChatMessageAttachment,
9
- ChatUser,
10
- NormalizedWebhookEvent,
11
- WebhookEventRecord,
12
- } from "../chat/types";
3
+ import { waitlistEntries, waitlistTriggers } from "./schema";
4
+ import type { WaitlistEntry, WaitlistTrigger } from "../waitlist/types";
13
5
 
14
6
  type Database = ReturnType<typeof createDb>;
7
+ type WaitlistEntryRow = typeof waitlistEntries.$inferSelect;
15
8
 
16
- type CreateUserRecord = {
9
+ type CreateEntryRecord = {
17
10
  id: string;
18
- username: string;
19
- displayName: string | null | undefined;
11
+ email: string;
12
+ name: string | null;
13
+ company: string | null;
14
+ source: string | null;
20
15
  };
21
16
 
22
- type CreateConversationRecord = {
17
+ type CreateTriggerRecord = {
23
18
  id: string;
24
- title: string | null | undefined;
25
- createdByUserId: string;
26
- participantUserIds: string[];
19
+ type: string;
20
+ entryId: string | null;
21
+ payloadJson: string;
27
22
  };
28
23
 
29
- type CreateMessageRecord = {
30
- id: string;
31
- conversationId: string;
32
- userId: string;
33
- body: string;
34
- };
35
-
36
- type CreateAttachmentRecord = {
37
- id: string;
38
- conversationId: string;
39
- uploadedByUserId: string;
40
- storageBucket: string;
41
- storageKey: string;
42
- contentType: string;
43
- byteSize: number;
44
- filename: string;
45
- };
46
-
47
- type ListMessagesCursor = {
48
- createdAt: string;
49
- id: string;
50
- };
51
-
52
- type ListMessagesPage = {
53
- messages: ChatMessage[];
54
- hasMore: boolean;
55
- };
56
-
57
- export class ChatRepository {
24
+ export class WaitlistRepository {
58
25
  constructor(private readonly db: Database) {}
59
26
 
60
- async createUser(input: CreateUserRecord): Promise<ChatUser> {
61
- const now = new Date();
62
- const [row] = await this.db
63
- .insert(users)
64
- .values({
65
- id: input.id,
66
- username: input.username,
67
- displayName: input.displayName ?? null,
68
- createdAt: now,
69
- updatedAt: now,
70
- })
71
- .returning();
72
- return toChatUser(row);
73
- }
74
-
75
- async getUserById(userId: string): Promise<ChatUser | null> {
76
- const [row] = await this.db.select().from(users).where(eq(users.id, userId)).limit(1);
77
- return row ? toChatUser(row) : null;
78
- }
79
-
80
- async getUserByUsername(username: string): Promise<ChatUser | null> {
81
- const [row] = await this.db.select().from(users).where(eq(users.username, username)).limit(1);
82
- return row ? toChatUser(row) : null;
83
- }
84
-
85
- async listUsersByIds(userIds: string[]): Promise<ChatUser[]> {
86
- if (userIds.length === 0) {
87
- return [];
88
- }
89
- const rows = await this.db.select().from(users).where(inArray(users.id, userIds));
90
- return rows.map(toChatUser);
91
- }
92
-
93
- async createConversation(input: CreateConversationRecord): Promise<ChatConversation> {
94
- const now = new Date();
95
- await this.db.transaction(async (tx) => {
96
- await tx.insert(conversations).values({
97
- id: input.id,
98
- title: input.title ?? null,
99
- createdByUserId: input.createdByUserId,
100
- createdAt: now,
101
- updatedAt: now,
102
- });
103
-
104
- await tx.insert(conversationParticipants).values(
105
- input.participantUserIds.map((userId) => ({
106
- conversationId: input.id,
107
- userId,
108
- joinedAt: now,
109
- }))
110
- );
111
- });
112
-
113
- return this.getConversationByIdOrThrow(input.id);
114
- }
115
-
116
- async getConversationById(conversationId: string): Promise<ChatConversation | null> {
117
- const [conversation] = await this.db
118
- .select()
119
- .from(conversations)
120
- .where(and(eq(conversations.id, conversationId), isNull(conversations.deletedAt)))
121
- .limit(1);
122
-
123
- if (!conversation) {
124
- return null;
125
- }
126
-
127
- const participantRows = await this.db
128
- .select({
129
- id: users.id,
130
- username: users.username,
131
- displayName: users.displayName,
132
- createdAt: users.createdAt,
133
- updatedAt: users.updatedAt,
134
- })
135
- .from(conversationParticipants)
136
- .innerJoin(users, eq(conversationParticipants.userId, users.id))
137
- .where(eq(conversationParticipants.conversationId, conversationId))
138
- .orderBy(asc(users.username));
139
-
140
- return {
141
- id: conversation.id,
142
- title: conversation.title,
143
- createdByUserId: conversation.createdByUserId,
144
- participants: participantRows.map(toChatUser),
145
- createdAt: conversation.createdAt.toISOString(),
146
- updatedAt: conversation.updatedAt.toISOString(),
147
- };
148
- }
149
-
150
- async updateConversation(conversationId: string, title: string | null | undefined): Promise<ChatConversation> {
151
- await this.db
152
- .update(conversations)
153
- .set({
154
- title: title ?? null,
155
- updatedAt: new Date(),
156
- })
157
- .where(eq(conversations.id, conversationId));
158
-
159
- return this.getConversationByIdOrThrow(conversationId);
160
- }
161
-
162
- async softDeleteConversation(conversationId: string) {
163
- const now = new Date();
164
- await this.db
165
- .update(conversations)
166
- .set({
167
- deletedAt: now,
168
- updatedAt: now,
169
- })
170
- .where(eq(conversations.id, conversationId));
171
- }
172
-
173
- async addParticipant(conversationId: string, userId: string) {
174
- const [existing] = await this.db
175
- .select()
176
- .from(conversationParticipants)
177
- .where(and(eq(conversationParticipants.conversationId, conversationId), eq(conversationParticipants.userId, userId)))
178
- .limit(1);
179
-
180
- if (!existing) {
181
- await this.db.insert(conversationParticipants).values({
182
- conversationId,
183
- userId,
184
- joinedAt: new Date(),
185
- });
186
- }
187
- }
188
-
189
- async removeParticipant(conversationId: string, userId: string) {
190
- await this.db
191
- .delete(conversationParticipants)
192
- .where(and(eq(conversationParticipants.conversationId, conversationId), eq(conversationParticipants.userId, userId)));
193
- }
194
-
195
- async listMessages(
196
- conversationId: string,
197
- input: {
198
- limit: number;
199
- cursor?: ListMessagesCursor;
200
- }
201
- ): Promise<ListMessagesPage> {
202
- const predicate = input.cursor
203
- ? and(
204
- eq(messages.conversationId, conversationId),
205
- isNull(messages.deletedAt),
206
- or(
207
- lt(messages.createdAt, new Date(input.cursor.createdAt)),
208
- and(eq(messages.createdAt, new Date(input.cursor.createdAt)), lt(messages.id, input.cursor.id))
209
- )
210
- )
211
- : and(eq(messages.conversationId, conversationId), isNull(messages.deletedAt));
212
-
213
- const rows = await this.db
214
- .select()
215
- .from(messages)
216
- .where(predicate)
217
- .orderBy(desc(messages.createdAt), desc(messages.id))
218
- .limit(input.limit + 1);
219
-
220
- const hasMore = rows.length > input.limit;
221
- const pageRows = hasMore ? rows.slice(0, input.limit) : rows;
222
- const attachmentMap = await this.listReadyAttachmentsByMessageIds(pageRows.map((row) => row.id));
223
-
224
- return {
225
- messages: pageRows.map((row) => toChatMessage(row, attachmentMap.get(row.id) ?? [])),
226
- hasMore,
227
- };
228
- }
229
-
230
- async createMessage(input: CreateMessageRecord): Promise<ChatMessage> {
27
+ async createEntry(input: CreateEntryRecord): Promise<WaitlistEntry> {
231
28
  const now = new Date();
232
29
  const [row] = await this.db
233
- .insert(messages)
30
+ .insert(waitlistEntries)
234
31
  .values({
235
32
  id: input.id,
236
- conversationId: input.conversationId,
237
- userId: input.userId,
238
- body: input.body,
33
+ email: input.email,
34
+ name: input.name,
35
+ company: input.company,
36
+ source: input.source,
37
+ status: "joined",
239
38
  createdAt: now,
240
39
  updatedAt: now,
241
40
  })
242
41
  .returning();
243
- return toChatMessage(row, []);
244
- }
245
-
246
- async getMessageById(messageId: string): Promise<ChatMessage | null> {
247
- const [row] = await this.db.select().from(messages).where(and(eq(messages.id, messageId), isNull(messages.deletedAt))).limit(1);
248
- return row ? toChatMessage(row, []) : null;
249
- }
250
-
251
- async updateMessage(messageId: string, body: string): Promise<ChatMessage> {
252
- const now = new Date();
253
- const [row] = await this.db
254
- .update(messages)
255
- .set({
256
- body,
257
- editedAt: now,
258
- updatedAt: now,
259
- })
260
- .where(eq(messages.id, messageId))
261
- .returning();
262
- return toChatMessage(row, []);
42
+ return toWaitlistEntry(row);
263
43
  }
264
44
 
265
- async softDeleteMessage(messageId: string) {
266
- const now = new Date();
267
- await this.db
268
- .update(messages)
269
- .set({
270
- deletedAt: now,
271
- updatedAt: now,
272
- })
273
- .where(eq(messages.id, messageId));
45
+ async getEntryById(entryId: string): Promise<WaitlistEntry | null> {
46
+ const [row] = await this.db.select().from(waitlistEntries).where(eq(waitlistEntries.id, entryId)).limit(1);
47
+ return row ? toWaitlistEntry(row) : null;
274
48
  }
275
49
 
276
- async createAttachment(input: CreateAttachmentRecord): Promise<ChatAttachment> {
277
- const now = new Date();
278
- const [row] = await this.db
279
- .insert(attachments)
280
- .values({
281
- id: input.id,
282
- conversationId: input.conversationId,
283
- uploadedByUserId: input.uploadedByUserId,
284
- storageBucket: input.storageBucket,
285
- storageKey: input.storageKey,
286
- contentType: input.contentType,
287
- byteSize: input.byteSize,
288
- filename: input.filename,
289
- status: "pending",
290
- createdAt: now,
291
- updatedAt: now,
292
- })
293
- .returning();
294
- return toChatAttachment(row);
50
+ async getEntryByEmail(email: string): Promise<WaitlistEntry | null> {
51
+ const [row] = await this.db.select().from(waitlistEntries).where(eq(waitlistEntries.email, email)).limit(1);
52
+ return row ? toWaitlistEntry(row) : null;
295
53
  }
296
54
 
297
- async getAttachmentById(attachmentId: string): Promise<ChatAttachment | null> {
298
- const [row] = await this.db
299
- .select()
300
- .from(attachments)
301
- .where(and(eq(attachments.id, attachmentId), isNull(attachments.deletedAt)))
302
- .limit(1);
303
- return row ? toChatAttachment(row) : null;
55
+ async listEntries(input: { status?: string | null; limit?: number | null } = {}): Promise<WaitlistEntry[]> {
56
+ const limit = clampLimit(input.limit);
57
+ const rows = input.status
58
+ ? await this.db
59
+ .select()
60
+ .from(waitlistEntries)
61
+ .where(eq(waitlistEntries.status, input.status as WaitlistEntryRow["status"]))
62
+ .orderBy(desc(waitlistEntries.createdAt))
63
+ .limit(limit)
64
+ : await this.db.select().from(waitlistEntries).orderBy(desc(waitlistEntries.createdAt)).limit(limit);
65
+ return rows.map(toWaitlistEntry);
304
66
  }
305
67
 
306
- async finalizeAttachment(attachmentId: string, input: { messageId: string | null; contentType: string; byteSize: number }) {
68
+ async updateEntryStatus(entryId: string, status: WaitlistEntryRow["status"]): Promise<WaitlistEntry | null> {
307
69
  const [row] = await this.db
308
- .update(attachments)
70
+ .update(waitlistEntries)
309
71
  .set({
310
- messageId: input.messageId,
311
- contentType: input.contentType,
312
- byteSize: input.byteSize,
313
- status: "ready",
72
+ status,
314
73
  updatedAt: new Date(),
315
74
  })
316
- .where(eq(attachments.id, attachmentId))
75
+ .where(eq(waitlistEntries.id, entryId))
317
76
  .returning();
318
- return toChatAttachment(row);
319
- }
320
-
321
- async softDeleteAttachment(attachmentId: string) {
322
- const now = new Date();
323
- await this.db
324
- .update(attachments)
325
- .set({
326
- deletedAt: now,
327
- status: "deleted",
328
- updatedAt: now,
329
- })
330
- .where(eq(attachments.id, attachmentId));
331
- }
332
-
333
- async getWebhookEvent(provider: string, externalEventId: string): Promise<WebhookEventRecord | null> {
334
- const [row] = await this.db
335
- .select()
336
- .from(webhookEvents)
337
- .where(and(eq(webhookEvents.provider, provider), eq(webhookEvents.externalEventId, externalEventId)))
338
- .limit(1);
339
- return row ? toWebhookEvent(row) : null;
77
+ return row ? toWaitlistEntry(row) : null;
340
78
  }
341
79
 
342
- async createWebhookEvent(event: NormalizedWebhookEvent): Promise<WebhookEventRecord> {
80
+ async createTrigger(input: CreateTriggerRecord): Promise<WaitlistTrigger> {
343
81
  const [row] = await this.db
344
- .insert(webhookEvents)
82
+ .insert(waitlistTriggers)
345
83
  .values({
346
- id: crypto.randomUUID(),
347
- provider: event.provider,
348
- externalEventId: event.externalEventId,
349
- eventType: event.eventType,
350
- signatureValid: event.signatureValid ? "true" : "false",
351
- status: "received",
352
- payloadJson: event.payloadJson,
353
- receivedAt: new Date(),
354
- })
355
- .returning();
356
- return toWebhookEvent(row);
357
- }
358
-
359
- async markWebhookEventProcessed(id: string, status: "processed" | "failed") {
360
- const [row] = await this.db
361
- .update(webhookEvents)
362
- .set({
363
- status,
364
- processedAt: new Date(),
84
+ id: input.id,
85
+ type: input.type,
86
+ entryId: input.entryId,
87
+ status: "queued",
88
+ payloadJson: input.payloadJson,
89
+ createdAt: new Date(),
365
90
  })
366
- .where(eq(webhookEvents.id, id))
367
91
  .returning();
368
- return toWebhookEvent(row);
369
- }
370
-
371
- private async getConversationByIdOrThrow(conversationId: string) {
372
- const conversation = await this.getConversationById(conversationId);
373
- if (!conversation) {
374
- throw new Error(`conversation ${conversationId} not found`);
375
- }
376
- return conversation;
377
- }
378
-
379
- private async listReadyAttachmentsByMessageIds(messageIds: string[]) {
380
- if (messageIds.length === 0) {
381
- return new Map<string, ChatMessageAttachment[]>();
382
- }
383
-
384
- const rows = await this.db
385
- .select()
386
- .from(attachments)
387
- .where(
388
- and(
389
- inArray(attachments.messageId, messageIds),
390
- eq(attachments.status, "ready"),
391
- isNull(attachments.deletedAt)
392
- )
393
- )
394
- .orderBy(asc(attachments.createdAt), asc(attachments.id));
395
-
396
- const map = new Map<string, ChatMessageAttachment[]>();
397
- for (const row of rows) {
398
- if (!row.messageId) {
399
- continue;
400
- }
401
- const bucket = map.get(row.messageId) ?? [];
402
- bucket.push(toChatMessageAttachment(row));
403
- map.set(row.messageId, bucket);
404
- }
405
- return map;
92
+ return toWaitlistTrigger(row);
406
93
  }
407
94
  }
408
95
 
409
- function toChatUser(row: typeof users.$inferSelect): ChatUser {
410
- return {
411
- id: row.id,
412
- username: row.username,
413
- displayName: row.displayName ?? null,
414
- createdAt: row.createdAt.toISOString(),
415
- updatedAt: row.updatedAt.toISOString(),
416
- };
417
- }
418
-
419
- function toChatMessage(row: typeof messages.$inferSelect, messageAttachments: ChatMessageAttachment[]): ChatMessage {
420
- return {
421
- id: row.id,
422
- conversationId: row.conversationId,
423
- userId: row.userId,
424
- body: row.body,
425
- editedAt: row.editedAt?.toISOString() ?? null,
426
- createdAt: row.createdAt.toISOString(),
427
- updatedAt: row.updatedAt.toISOString(),
428
- attachments: messageAttachments,
429
- };
430
- }
431
-
432
- function toChatMessageAttachment(row: typeof attachments.$inferSelect): ChatMessageAttachment {
433
- return {
434
- id: row.id,
435
- filename: row.filename,
436
- contentType: row.contentType,
437
- byteSize: row.byteSize,
438
- status: row.status,
439
- publicUrl: buildAttachmentPublicUrl(row.storageBucket, row.storageKey),
440
- };
441
- }
442
-
443
- function buildAttachmentPublicUrl(bucket: string, key: string) {
444
- const configuredBase = Bun.env.ATTACHMENT_PUBLIC_BASE_URL?.trim();
445
- const base = configuredBase || `https://storage.googleapis.com/${bucket}`;
446
- return `${base.replace(/\/+$/g, "")}/${key}`;
96
+ function clampLimit(value: number | null | undefined) {
97
+ if (!value || !Number.isFinite(value)) {
98
+ return 100;
99
+ }
100
+ return Math.min(Math.max(Math.trunc(value), 1), 500);
447
101
  }
448
102
 
449
- function toChatAttachment(row: typeof attachments.$inferSelect): ChatAttachment {
103
+ function toWaitlistEntry(row: WaitlistEntryRow): WaitlistEntry {
450
104
  return {
451
105
  id: row.id,
452
- conversationId: row.conversationId,
453
- messageId: row.messageId ?? null,
454
- uploadedByUserId: row.uploadedByUserId,
455
- storageBucket: row.storageBucket,
456
- storageKey: row.storageKey,
457
- contentType: row.contentType,
458
- byteSize: row.byteSize,
459
- filename: row.filename,
106
+ email: row.email,
107
+ name: row.name,
108
+ company: row.company,
109
+ source: row.source,
460
110
  status: row.status,
461
- publicUrl: "",
462
111
  createdAt: row.createdAt.toISOString(),
463
112
  updatedAt: row.updatedAt.toISOString(),
464
113
  };
465
114
  }
466
115
 
467
- function toWebhookEvent(row: typeof webhookEvents.$inferSelect): WebhookEventRecord {
116
+ function toWaitlistTrigger(row: typeof waitlistTriggers.$inferSelect): WaitlistTrigger {
468
117
  return {
469
118
  id: row.id,
470
- provider: row.provider,
471
- externalEventId: row.externalEventId,
472
- eventType: row.eventType,
473
- signatureValid: row.signatureValid === "true",
119
+ type: row.type,
120
+ entryId: row.entryId,
474
121
  status: row.status,
475
122
  payloadJson: row.payloadJson,
476
- receivedAt: row.receivedAt.toISOString(),
123
+ createdAt: row.createdAt.toISOString(),
477
124
  processedAt: row.processedAt?.toISOString() ?? null,
478
125
  };
479
126
  }
@@ -1,75 +1,26 @@
1
- import { integer, pgTable, primaryKey, text, timestamp, uniqueIndex } from "drizzle-orm/pg-core";
1
+ import { pgTable, text, timestamp, uniqueIndex } from "drizzle-orm/pg-core";
2
2
 
3
- export const users = pgTable(
4
- "users",
3
+ export const waitlistEntries = pgTable(
4
+ "waitlist_entries",
5
5
  {
6
6
  id: text("id").primaryKey(),
7
- username: text("username").notNull(),
8
- displayName: text("display_name"),
7
+ email: text("email").notNull(),
8
+ name: text("name"),
9
+ company: text("company"),
10
+ source: text("source"),
11
+ status: text("status").$type<"joined" | "invited" | "converted" | "archived">().notNull().default("joined"),
9
12
  createdAt: timestamp("created_at", { withTimezone: true, mode: "date" }).notNull().defaultNow(),
10
13
  updatedAt: timestamp("updated_at", { withTimezone: true, mode: "date" }).notNull().defaultNow(),
11
14
  },
12
- (table) => [uniqueIndex("users_username_key").on(table.username)]
15
+ (table) => [uniqueIndex("waitlist_entries_email_key").on(table.email)]
13
16
  );
14
17
 
15
- export const conversations = pgTable("conversations", {
18
+ export const waitlistTriggers = pgTable("waitlist_triggers", {
16
19
  id: text("id").primaryKey(),
17
- title: text("title"),
18
- createdByUserId: text("created_by_user_id").notNull().references(() => users.id),
19
- deletedAt: timestamp("deleted_at", { withTimezone: true, mode: "date" }),
20
+ type: text("type").notNull(),
21
+ entryId: text("entry_id").references(() => waitlistEntries.id),
22
+ status: text("status").$type<"queued" | "processed" | "failed">().notNull().default("queued"),
23
+ payloadJson: text("payload_json").notNull(),
20
24
  createdAt: timestamp("created_at", { withTimezone: true, mode: "date" }).notNull().defaultNow(),
21
- updatedAt: timestamp("updated_at", { withTimezone: true, mode: "date" }).notNull().defaultNow(),
25
+ processedAt: timestamp("processed_at", { withTimezone: true, mode: "date" }),
22
26
  });
23
-
24
- export const conversationParticipants = pgTable(
25
- "conversation_participants",
26
- {
27
- conversationId: text("conversation_id").notNull().references(() => conversations.id),
28
- userId: text("user_id").notNull().references(() => users.id),
29
- joinedAt: timestamp("joined_at", { withTimezone: true, mode: "date" }).notNull().defaultNow(),
30
- },
31
- (table) => [primaryKey({ columns: [table.conversationId, table.userId] })]
32
- );
33
-
34
- export const messages = pgTable("messages", {
35
- id: text("id").primaryKey(),
36
- conversationId: text("conversation_id").notNull().references(() => conversations.id),
37
- userId: text("user_id").notNull().references(() => users.id),
38
- body: text("body").notNull(),
39
- editedAt: timestamp("edited_at", { withTimezone: true, mode: "date" }),
40
- deletedAt: timestamp("deleted_at", { withTimezone: true, mode: "date" }),
41
- createdAt: timestamp("created_at", { withTimezone: true, mode: "date" }).notNull().defaultNow(),
42
- updatedAt: timestamp("updated_at", { withTimezone: true, mode: "date" }).notNull().defaultNow(),
43
- });
44
-
45
- export const attachments = pgTable("attachments", {
46
- id: text("id").primaryKey(),
47
- conversationId: text("conversation_id").notNull().references(() => conversations.id),
48
- messageId: text("message_id").references(() => messages.id),
49
- uploadedByUserId: text("uploaded_by_user_id").notNull().references(() => users.id),
50
- storageBucket: text("storage_bucket").notNull(),
51
- storageKey: text("storage_key").notNull(),
52
- contentType: text("content_type").notNull(),
53
- byteSize: integer("byte_size").notNull(),
54
- filename: text("filename").notNull(),
55
- status: text("status").$type<"pending" | "ready" | "deleted">().notNull(),
56
- deletedAt: timestamp("deleted_at", { withTimezone: true, mode: "date" }),
57
- createdAt: timestamp("created_at", { withTimezone: true, mode: "date" }).notNull().defaultNow(),
58
- updatedAt: timestamp("updated_at", { withTimezone: true, mode: "date" }).notNull().defaultNow(),
59
- });
60
-
61
- export const webhookEvents = pgTable(
62
- "webhook_events",
63
- {
64
- id: text("id").primaryKey(),
65
- provider: text("provider").notNull(),
66
- externalEventId: text("external_event_id").notNull(),
67
- eventType: text("event_type").notNull(),
68
- signatureValid: text("signature_valid").notNull(),
69
- status: text("status").$type<"received" | "processed" | "failed">().notNull(),
70
- payloadJson: text("payload_json").notNull(),
71
- receivedAt: timestamp("received_at", { withTimezone: true, mode: "date" }).notNull().defaultNow(),
72
- processedAt: timestamp("processed_at", { withTimezone: true, mode: "date" }),
73
- },
74
- (table) => [uniqueIndex("webhook_events_provider_external_event_key").on(table.provider, table.externalEventId)]
75
- );