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.
- package/README.md +51 -47
- package/index.ts +2 -2
- package/package.json +10 -9
- package/src/cli.test.ts +28 -10
- package/src/cli.ts +196 -33
- package/src/git-bootstrap.test.ts +40 -0
- package/src/git-bootstrap.ts +110 -0
- package/src/naming.test.ts +1 -0
- package/src/naming.ts +23 -0
- package/src/post-scaffold.test.ts +19 -0
- package/src/post-scaffold.ts +17 -4
- package/src/profiles.ts +2 -5
- package/src/scaffold.test.ts +232 -41
- package/src/scaffold.ts +81 -36
- package/src/service.test.ts +30 -0
- package/src/service.ts +65 -0
- package/src/vault.test.ts +61 -1
- package/src/vault.ts +77 -15
- package/templates/shared/.github/workflows/ci.yml +2 -1
- package/templates/shared/.github/workflows/deploy.yml +2 -0
- package/templates/shared/README.md +124 -47
- package/templates/shared/grafana/alerts.yaml +54 -0
- package/templates/shared/grafana/waitlist-dashboard.json +63 -0
- package/templates/shared/scripts/authctl.ts +231 -0
- package/templates/shared/scripts/cloudrun/bootstrap.ts +14 -5
- package/templates/shared/scripts/cloudrun/cleanup.ts +64 -4
- package/templates/shared/scripts/cloudrun/cli.ts +329 -7
- package/templates/shared/scripts/cloudrun/config.ts +11 -4
- package/templates/shared/scripts/cloudrun/deploy.ts +0 -4
- package/templates/shared/scripts/cloudrun/lib.ts +174 -41
- package/templates/shared/scripts/cloudrun/neon.ts +45 -0
- package/templates/shared/scripts/dev.ts +22 -0
- package/templates/shared/scripts/ensure-local-db.ts +3 -0
- package/templates/shared/scripts/local-docker.ts +63 -0
- package/templates/shared/scripts/local-env.ts +27 -0
- package/templates/shared/scripts/seed.ts +73 -0
- package/templates/shared/scripts/wait-for-db.ts +32 -0
- package/templates/shared/service.config.ts +59 -0
- package/templates/shared/service.yaml +24 -44
- package/templates/targets/workers/.github/workflows/ci.yml +19 -0
- package/templates/targets/workers/.github/workflows/deploy.yml +19 -0
- package/templates/targets/workers/Makefile +33 -0
- package/templates/targets/workers/README.md +75 -0
- package/templates/targets/workers/package.json +35 -0
- package/templates/targets/workers/scripts/workers/cli.ts +402 -0
- package/templates/targets/workers/src/auth.ts +178 -0
- package/templates/targets/workers/src/index.ts +198 -0
- package/templates/targets/workers/src/storage.ts +370 -0
- package/templates/targets/workers/test/app.test.ts +108 -0
- package/templates/targets/workers/tsconfig.json +11 -0
- package/templates/targets/workers/wrangler.toml +24 -0
- package/templates/variants/bun-connectrpc/Makefile +14 -8
- package/templates/variants/bun-connectrpc/gen/protos/waitlist/v1/waitlist_pb.ts +424 -0
- package/templates/variants/bun-connectrpc/migrations/0000_init.sql +12 -55
- package/templates/variants/bun-connectrpc/package.json +12 -5
- package/templates/variants/bun-connectrpc/protos/waitlist/v1/waitlist.proto +91 -0
- package/templates/variants/bun-connectrpc/scripts/codegen.ts +1 -1
- package/templates/variants/bun-connectrpc/scripts/migrate.ts +4 -1
- package/templates/variants/bun-connectrpc/src/auth.ts +200 -0
- package/templates/variants/bun-connectrpc/src/db/repository.ts +67 -420
- package/templates/variants/bun-connectrpc/src/db/schema.ts +15 -64
- package/templates/variants/bun-connectrpc/src/index.ts +76 -176
- package/templates/variants/bun-connectrpc/src/temporal/activities.ts +14 -0
- package/templates/variants/bun-connectrpc/src/temporal/worker.ts +38 -0
- package/templates/variants/bun-connectrpc/src/temporal/workflows.ts +10 -0
- package/templates/variants/bun-connectrpc/src/waitlist/service.ts +172 -0
- package/templates/variants/bun-connectrpc/src/waitlist/types.ts +45 -0
- package/templates/variants/bun-connectrpc/test/app.test.ts +4 -4
- package/templates/variants/bun-connectrpc/test/waitlist.integration.test.ts +71 -0
- package/templates/variants/bun-hono/Makefile +14 -8
- package/templates/variants/bun-hono/migrations/0000_init.sql +12 -55
- package/templates/variants/bun-hono/package.json +12 -5
- package/templates/variants/bun-hono/scripts/migrate.ts +4 -1
- package/templates/variants/bun-hono/src/auth.ts +181 -0
- package/templates/variants/bun-hono/src/db/repository.ts +68 -421
- package/templates/variants/bun-hono/src/db/schema.ts +15 -64
- package/templates/variants/bun-hono/src/index.ts +65 -180
- package/templates/variants/bun-hono/src/temporal/activities.ts +14 -0
- package/templates/variants/bun-hono/src/temporal/worker.ts +38 -0
- package/templates/variants/bun-hono/src/temporal/workflows.ts +10 -0
- package/templates/variants/bun-hono/src/waitlist/service.ts +166 -0
- package/templates/variants/bun-hono/src/waitlist/types.ts +50 -0
- package/templates/variants/bun-hono/test/app.test.ts +72 -41
- package/templates/variants/bun-hono/test/waitlist.integration.test.ts +102 -0
- package/templates/variants/go-chi/Makefile +27 -11
- package/templates/variants/go-chi/atlas.hcl +8 -0
- package/templates/variants/go-chi/cmd/server/main.go +21 -10
- package/templates/variants/go-chi/go.mod +1 -3
- package/templates/variants/go-chi/internal/app/service.go +202 -685
- package/templates/variants/go-chi/internal/auth/middleware.go +289 -0
- package/templates/variants/go-chi/internal/auth/middleware_test.go +38 -0
- package/templates/variants/go-chi/internal/config/config.go +27 -11
- package/templates/variants/go-chi/internal/httpapi/routes.go +78 -157
- package/templates/variants/go-chi/internal/httpapi/waitlist_integration_test.go +199 -0
- package/templates/variants/go-chi/internal/temporal/activities.go +27 -0
- package/templates/variants/go-chi/internal/temporal/worker.go +42 -0
- package/templates/variants/go-chi/internal/temporal/workflows.go +18 -0
- package/templates/variants/go-chi/migrations/0000_init.sql +12 -55
- package/templates/variants/go-chi/migrations/atlas.sum +2 -0
- package/templates/variants/go-chi/package.json +7 -1
- package/templates/variants/go-connectrpc/Makefile +26 -9
- package/templates/variants/go-connectrpc/atlas.hcl +8 -0
- package/templates/variants/go-connectrpc/buf.gen.yaml +2 -2
- package/templates/variants/go-connectrpc/cmd/server/main.go +23 -12
- package/templates/variants/go-connectrpc/gen/waitlist/v1/waitlist.pb.go +960 -0
- package/templates/variants/go-connectrpc/gen/waitlist/v1/waitlistv1connect/waitlist.connect.go +283 -0
- package/templates/variants/go-connectrpc/go.mod +1 -1
- package/templates/variants/go-connectrpc/internal/app/service.go +202 -685
- package/templates/variants/go-connectrpc/internal/auth/middleware.go +289 -0
- package/templates/variants/go-connectrpc/internal/auth/middleware_test.go +38 -0
- package/templates/variants/go-connectrpc/internal/config/config.go +27 -11
- package/templates/variants/go-connectrpc/internal/connectapi/handler.go +78 -201
- package/templates/variants/go-connectrpc/internal/connectapi/waitlist_integration_test.go +122 -0
- package/templates/variants/go-connectrpc/internal/httpapi/routes.go +147 -9
- package/templates/variants/go-connectrpc/internal/temporal/activities.go +27 -0
- package/templates/variants/go-connectrpc/internal/temporal/worker.go +42 -0
- package/templates/variants/go-connectrpc/internal/temporal/workflows.go +18 -0
- package/templates/variants/go-connectrpc/migrations/0000_init.sql +12 -55
- package/templates/variants/go-connectrpc/migrations/atlas.sum +2 -0
- package/templates/variants/go-connectrpc/package.json +7 -1
- package/templates/variants/go-connectrpc/protos/waitlist/v1/waitlist.proto +93 -0
- package/templates/root/.github/workflows/buf-publish.yml +0 -19
- package/templates/root/.github/workflows/ci.yml +0 -26
- package/templates/root/.github/workflows/deploy.yml +0 -22
- package/templates/root/Dockerfile +0 -23
- package/templates/root/README.md +0 -69
- package/templates/root/buf.gen.yaml +0 -10
- package/templates/root/buf.yaml +0 -9
- package/templates/root/cmd/server/main.go +0 -44
- package/templates/root/gen/dns/v1/dns.pb.go +0 -623
- package/templates/root/gen/dns/v1/dnsv1connect/dns.connect.go +0 -192
- package/templates/root/go.mod +0 -10
- package/templates/root/internal/app/service.go +0 -152
- package/templates/root/internal/app/token_source.go +0 -50
- package/templates/root/internal/cloudflare/client.go +0 -160
- package/templates/root/internal/config/config.go +0 -55
- package/templates/root/internal/connectapi/handler.go +0 -79
- package/templates/root/internal/httpapi/routes.go +0 -93
- package/templates/root/internal/vault/client.go +0 -148
- package/templates/root/package.json +0 -12
- package/templates/root/protos/dns/v1/dns.proto +0 -58
- package/templates/root/scripts/cloudrun/bootstrap.ts +0 -65
- package/templates/root/scripts/cloudrun/config.ts +0 -50
- package/templates/root/scripts/cloudrun/deploy.ts +0 -41
- package/templates/root/scripts/cloudrun/lib.ts +0 -244
- package/templates/root/service.yaml +0 -50
- package/templates/root/test/go.test.ts +0 -19
- package/templates/shared/scripts/cloudrun/integrations.ts +0 -111
- package/templates/variants/bun-connectrpc/gen/protos/chat/v1/chat_pb.ts +0 -1078
- package/templates/variants/bun-connectrpc/protos/chat/v1/chat.proto +0 -228
- package/templates/variants/bun-connectrpc/src/chat/service.ts +0 -384
- package/templates/variants/bun-connectrpc/src/chat/types.ts +0 -142
- package/templates/variants/bun-connectrpc/src/storage.ts +0 -72
- package/templates/variants/bun-connectrpc/src/webhooks.ts +0 -35
- package/templates/variants/bun-connectrpc/test/list-messages.integration.test.ts +0 -182
- package/templates/variants/bun-hono/src/chat/service.ts +0 -384
- package/templates/variants/bun-hono/src/chat/types.ts +0 -142
- package/templates/variants/bun-hono/src/storage.ts +0 -72
- package/templates/variants/bun-hono/src/webhooks.ts +0 -35
- package/templates/variants/bun-hono/test/list-messages.integration.test.ts +0 -256
- package/templates/variants/go-chi/buf.gen.yaml +0 -12
- package/templates/variants/go-chi/buf.yaml +0 -9
- package/templates/variants/go-chi/cmd/migrate/main.go +0 -101
- package/templates/variants/go-chi/internal/httpapi/list_messages_integration_test.go +0 -298
- package/templates/variants/go-chi/protos/chat/v1/chat.proto +0 -219
- package/templates/variants/go-connectrpc/cmd/migrate/main.go +0 -101
- package/templates/variants/go-connectrpc/gen/chat/v1/chat.pb.go +0 -2512
- package/templates/variants/go-connectrpc/gen/chat/v1/chatv1connect/chat.connect.go +0 -571
- package/templates/variants/go-connectrpc/internal/connectapi/list_messages_integration_test.go +0 -216
- package/templates/variants/go-connectrpc/protos/chat/v1/chat.proto +0 -232
- /package/bin/{create-svc.mjs → service.mjs} +0 -0
|
@@ -1,7 +1,9 @@
|
|
|
1
1
|
import { Hono } from "hono";
|
|
2
|
-
import {
|
|
2
|
+
import { authMiddleware } from "./auth";
|
|
3
|
+
import { AppError, createDefaultWaitlistService, type WaitlistService } from "./waitlist/service";
|
|
4
|
+
import { startTemporalWorker } from "./temporal/worker";
|
|
3
5
|
|
|
4
|
-
export function createApp(service:
|
|
6
|
+
export function createApp(service: WaitlistService) {
|
|
5
7
|
const app = new Hono();
|
|
6
8
|
|
|
7
9
|
app.get("/healthz", (context) => context.json({ status: "ok" }));
|
|
@@ -9,71 +11,50 @@ export function createApp(service: ChatService) {
|
|
|
9
11
|
app.get("/", (context) =>
|
|
10
12
|
context.json({
|
|
11
13
|
service: "{{SERVICE_NAME}}",
|
|
12
|
-
domain: "
|
|
14
|
+
domain: "waitlist",
|
|
13
15
|
apiOrigin: "https://api.{{SERVICE_NAME}}.anmho.com",
|
|
14
16
|
})
|
|
15
17
|
);
|
|
16
18
|
|
|
17
|
-
app.
|
|
19
|
+
app.use("/v1/*", authMiddleware());
|
|
20
|
+
|
|
21
|
+
app.post("/v1/waitlist", async (context) => {
|
|
18
22
|
try {
|
|
19
23
|
const body = await context.req.json();
|
|
20
|
-
const
|
|
21
|
-
|
|
22
|
-
|
|
24
|
+
const result = await service.joinWaitlist({
|
|
25
|
+
email: String(body.email ?? ""),
|
|
26
|
+
name: body.name ?? null,
|
|
27
|
+
company: body.company ?? null,
|
|
28
|
+
source: body.source ?? null,
|
|
23
29
|
});
|
|
24
|
-
return context.json(
|
|
30
|
+
return context.json(result, result.created ? 201 : 200);
|
|
25
31
|
} catch (error) {
|
|
26
32
|
return writeError(context, error);
|
|
27
33
|
}
|
|
28
34
|
});
|
|
29
35
|
|
|
30
|
-
app.get("/v1/
|
|
36
|
+
app.get("/v1/waitlist", async (context) => {
|
|
31
37
|
try {
|
|
32
|
-
return context.json({
|
|
38
|
+
return context.json({ entry: await service.getWaitlistEntryByEmail(context.req.query("email") ?? "") });
|
|
33
39
|
} catch (error) {
|
|
34
40
|
return writeError(context, error);
|
|
35
41
|
}
|
|
36
42
|
});
|
|
37
43
|
|
|
38
|
-
app.get("/v1/
|
|
44
|
+
app.get("/v1/waitlist/:entryId", async (context) => {
|
|
39
45
|
try {
|
|
40
|
-
|
|
41
|
-
return context.json({ user: await service.getUserByUsername(username) });
|
|
46
|
+
return context.json({ entry: await service.getWaitlistEntry(context.req.param("entryId")) });
|
|
42
47
|
} catch (error) {
|
|
43
48
|
return writeError(context, error);
|
|
44
49
|
}
|
|
45
50
|
});
|
|
46
51
|
|
|
47
|
-
app.
|
|
52
|
+
app.get("/v1/admin/waitlist", async (context) => {
|
|
48
53
|
try {
|
|
49
|
-
const body = await context.req.json();
|
|
50
|
-
const conversation = await service.createConversation({
|
|
51
|
-
createdByUserId: String(body.created_by_user_id ?? body.createdByUserId ?? ""),
|
|
52
|
-
title: body.title ?? null,
|
|
53
|
-
participantUserIds: Array.isArray(body.participant_user_ids ?? body.participantUserIds)
|
|
54
|
-
? (body.participant_user_ids ?? body.participantUserIds).map(String)
|
|
55
|
-
: [],
|
|
56
|
-
});
|
|
57
|
-
return context.json({ conversation }, 201);
|
|
58
|
-
} catch (error) {
|
|
59
|
-
return writeError(context, error);
|
|
60
|
-
}
|
|
61
|
-
});
|
|
62
|
-
|
|
63
|
-
app.get("/v1/conversations/:conversationId", async (context) => {
|
|
64
|
-
try {
|
|
65
|
-
return context.json({ conversation: await service.getConversation(context.req.param("conversationId")) });
|
|
66
|
-
} catch (error) {
|
|
67
|
-
return writeError(context, error);
|
|
68
|
-
}
|
|
69
|
-
});
|
|
70
|
-
|
|
71
|
-
app.patch("/v1/conversations/:conversationId", async (context) => {
|
|
72
|
-
try {
|
|
73
|
-
const body = await context.req.json();
|
|
74
54
|
return context.json({
|
|
75
|
-
|
|
76
|
-
|
|
55
|
+
entries: await service.listWaitlistEntries({
|
|
56
|
+
status: context.req.query("status"),
|
|
57
|
+
limit: parseOptionalNumber(context.req.query("limit")),
|
|
77
58
|
}),
|
|
78
59
|
});
|
|
79
60
|
} catch (error) {
|
|
@@ -81,159 +62,46 @@ export function createApp(service: ChatService) {
|
|
|
81
62
|
}
|
|
82
63
|
});
|
|
83
64
|
|
|
84
|
-
app.
|
|
85
|
-
try {
|
|
86
|
-
await service.deleteConversation(context.req.param("conversationId"));
|
|
87
|
-
return context.body(null, 204);
|
|
88
|
-
} catch (error) {
|
|
89
|
-
return writeError(context, error);
|
|
90
|
-
}
|
|
91
|
-
});
|
|
92
|
-
|
|
93
|
-
app.post("/v1/conversations/:conversationId/participants", async (context) => {
|
|
65
|
+
app.patch("/v1/admin/waitlist/:entryId", async (context) => {
|
|
94
66
|
try {
|
|
95
67
|
const body = await context.req.json();
|
|
96
|
-
return context.json(
|
|
97
|
-
{
|
|
98
|
-
conversation: await service.addParticipant(
|
|
99
|
-
context.req.param("conversationId"),
|
|
100
|
-
String(body.user_id ?? body.userId ?? "")
|
|
101
|
-
),
|
|
102
|
-
},
|
|
103
|
-
201
|
|
104
|
-
);
|
|
105
|
-
} catch (error) {
|
|
106
|
-
return writeError(context, error);
|
|
107
|
-
}
|
|
108
|
-
});
|
|
109
|
-
|
|
110
|
-
app.delete("/v1/conversations/:conversationId/participants/:userId", async (context) => {
|
|
111
|
-
try {
|
|
112
|
-
await service.removeParticipant(context.req.param("conversationId"), context.req.param("userId"));
|
|
113
|
-
return context.body(null, 204);
|
|
114
|
-
} catch (error) {
|
|
115
|
-
return writeError(context, error);
|
|
116
|
-
}
|
|
117
|
-
});
|
|
118
|
-
|
|
119
|
-
app.get("/v1/conversations/:conversationId/messages", async (context) => {
|
|
120
|
-
try {
|
|
121
|
-
const limit = context.req.query("limit");
|
|
122
|
-
const result = await service.listMessages(context.req.param("conversationId"), {
|
|
123
|
-
cursor: context.req.query("cursor") ?? undefined,
|
|
124
|
-
limit: limit == null ? undefined : Number(limit),
|
|
125
|
-
});
|
|
126
68
|
return context.json({
|
|
127
|
-
|
|
128
|
-
|
|
129
|
-
|
|
130
|
-
|
|
131
|
-
body: message.body,
|
|
132
|
-
edited_at: message.editedAt,
|
|
133
|
-
created_at: message.createdAt,
|
|
134
|
-
updated_at: message.updatedAt,
|
|
135
|
-
attachments: message.attachments.map((attachment) => ({
|
|
136
|
-
id: attachment.id,
|
|
137
|
-
filename: attachment.filename,
|
|
138
|
-
content_type: attachment.contentType,
|
|
139
|
-
byte_size: attachment.byteSize,
|
|
140
|
-
status: attachment.status,
|
|
141
|
-
public_url: attachment.publicUrl,
|
|
142
|
-
})),
|
|
143
|
-
})),
|
|
144
|
-
...(result.nextCursor ? { next_cursor: result.nextCursor } : {}),
|
|
69
|
+
entry: await service.updateWaitlistEntry({
|
|
70
|
+
entryId: context.req.param("entryId"),
|
|
71
|
+
status: String(body.status ?? ""),
|
|
72
|
+
}),
|
|
145
73
|
});
|
|
146
74
|
} catch (error) {
|
|
147
75
|
return writeError(context, error);
|
|
148
76
|
}
|
|
149
77
|
});
|
|
150
78
|
|
|
151
|
-
app.
|
|
152
|
-
try {
|
|
153
|
-
const body = await context.req.json();
|
|
154
|
-
return context.json(
|
|
155
|
-
{
|
|
156
|
-
message: await service.createMessage(context.req.param("conversationId"), {
|
|
157
|
-
userId: String(body.user_id ?? body.userId ?? ""),
|
|
158
|
-
body: String(body.body ?? ""),
|
|
159
|
-
}),
|
|
160
|
-
},
|
|
161
|
-
201
|
|
162
|
-
);
|
|
163
|
-
} catch (error) {
|
|
164
|
-
return writeError(context, error);
|
|
165
|
-
}
|
|
166
|
-
});
|
|
167
|
-
|
|
168
|
-
app.patch("/v1/conversations/:conversationId/messages/:messageId", async (context) => {
|
|
79
|
+
app.get("/v1/admin/waitlist/export", async (context) => {
|
|
169
80
|
try {
|
|
170
|
-
const
|
|
171
|
-
|
|
172
|
-
|
|
173
|
-
context.req.param("conversationId"),
|
|
174
|
-
context.req.param("messageId"),
|
|
175
|
-
{ body: String(body.body ?? "") }
|
|
176
|
-
),
|
|
81
|
+
const csv = await service.exportWaitlistEntries({
|
|
82
|
+
status: context.req.query("status"),
|
|
83
|
+
limit: parseOptionalNumber(context.req.query("limit")),
|
|
177
84
|
});
|
|
178
|
-
|
|
179
|
-
|
|
180
|
-
|
|
181
|
-
|
|
182
|
-
|
|
183
|
-
app.delete("/v1/conversations/:conversationId/messages/:messageId", async (context) => {
|
|
184
|
-
try {
|
|
185
|
-
await service.deleteMessage(context.req.param("conversationId"), context.req.param("messageId"));
|
|
186
|
-
return context.body(null, 204);
|
|
187
|
-
} catch (error) {
|
|
188
|
-
return writeError(context, error);
|
|
189
|
-
}
|
|
190
|
-
});
|
|
191
|
-
|
|
192
|
-
app.post("/v1/attachments/uploads", async (context) => {
|
|
193
|
-
try {
|
|
194
|
-
const body = await context.req.json();
|
|
195
|
-
return context.json(
|
|
196
|
-
{
|
|
197
|
-
result: await service.createAttachmentUpload({
|
|
198
|
-
conversationId: String(body.conversation_id ?? body.conversationId ?? ""),
|
|
199
|
-
uploadedByUserId: String(body.user_id ?? body.userId ?? ""),
|
|
200
|
-
filename: String(body.filename ?? ""),
|
|
201
|
-
contentType: String(body.content_type ?? body.contentType ?? ""),
|
|
202
|
-
byteSize: Number(body.byte_size ?? body.byteSize ?? 0),
|
|
203
|
-
}),
|
|
85
|
+
return new Response(csv, {
|
|
86
|
+
headers: {
|
|
87
|
+
"Content-Type": "text/csv; charset=utf-8",
|
|
88
|
+
"Content-Disposition": 'attachment; filename="waitlist.csv"',
|
|
204
89
|
},
|
|
205
|
-
|
|
206
|
-
);
|
|
90
|
+
});
|
|
207
91
|
} catch (error) {
|
|
208
92
|
return writeError(context, error);
|
|
209
93
|
}
|
|
210
94
|
});
|
|
211
95
|
|
|
212
|
-
app.post("/v1/
|
|
96
|
+
app.post("/v1/triggers/waitlist", async (context) => {
|
|
213
97
|
try {
|
|
214
98
|
const body = await context.req.json().catch(() => ({}));
|
|
215
|
-
|
|
216
|
-
|
|
217
|
-
|
|
218
|
-
|
|
99
|
+
const trigger = await service.recordTrigger({
|
|
100
|
+
type: String(body.type ?? "manual"),
|
|
101
|
+
entryId: body.entry_id ?? body.entryId ?? null,
|
|
102
|
+
payload: body,
|
|
219
103
|
});
|
|
220
|
-
|
|
221
|
-
return writeError(context, error);
|
|
222
|
-
}
|
|
223
|
-
});
|
|
224
|
-
|
|
225
|
-
app.get("/v1/attachments/:attachmentId", async (context) => {
|
|
226
|
-
try {
|
|
227
|
-
return context.json({ attachment: await service.getAttachment(context.req.param("attachmentId")) });
|
|
228
|
-
} catch (error) {
|
|
229
|
-
return writeError(context, error);
|
|
230
|
-
}
|
|
231
|
-
});
|
|
232
|
-
|
|
233
|
-
app.delete("/v1/attachments/:attachmentId", async (context) => {
|
|
234
|
-
try {
|
|
235
|
-
await service.deleteAttachment(context.req.param("attachmentId"));
|
|
236
|
-
return context.body(null, 204);
|
|
104
|
+
return context.json({ trigger }, 202);
|
|
237
105
|
} catch (error) {
|
|
238
106
|
return writeError(context, error);
|
|
239
107
|
}
|
|
@@ -242,8 +110,15 @@ export function createApp(service: ChatService) {
|
|
|
242
110
|
app.post("/webhooks/:provider", async (context) => {
|
|
243
111
|
try {
|
|
244
112
|
const rawBody = await context.req.text();
|
|
245
|
-
const
|
|
246
|
-
|
|
113
|
+
const trigger = await service.recordTrigger({
|
|
114
|
+
type: `webhook.${context.req.param("provider")}`,
|
|
115
|
+
entryId: null,
|
|
116
|
+
payload: {
|
|
117
|
+
headers: Object.fromEntries(context.req.raw.headers),
|
|
118
|
+
rawBody,
|
|
119
|
+
},
|
|
120
|
+
});
|
|
121
|
+
return context.json({ trigger }, 202);
|
|
247
122
|
} catch (error) {
|
|
248
123
|
return writeError(context, error);
|
|
249
124
|
}
|
|
@@ -258,13 +133,23 @@ function writeError(context: any, error: unknown) {
|
|
|
258
133
|
if (error instanceof AppError) {
|
|
259
134
|
return context.json({ error: error.message, code: error.code }, error.status);
|
|
260
135
|
}
|
|
261
|
-
|
|
136
|
+
|
|
137
|
+
console.error(error);
|
|
138
|
+
return context.json({ error: "internal server error", code: "internal" }, 500);
|
|
139
|
+
}
|
|
140
|
+
|
|
141
|
+
function parseOptionalNumber(value: string | undefined) {
|
|
142
|
+
return value ? Number(value) : null;
|
|
262
143
|
}
|
|
263
144
|
|
|
264
145
|
if (import.meta.main) {
|
|
265
|
-
const
|
|
146
|
+
const temporalWorker = await startTemporalWorker();
|
|
147
|
+
if (temporalWorker) {
|
|
148
|
+
console.log(`Temporal worker polling ${temporalWorker.taskQueue}`);
|
|
149
|
+
}
|
|
150
|
+
|
|
266
151
|
Bun.serve({
|
|
267
|
-
port: Number(Bun.env.PORT ??
|
|
268
|
-
fetch:
|
|
152
|
+
port: Number(Bun.env.PORT ?? 3000),
|
|
153
|
+
fetch: createApp(createDefaultWaitlistService()).fetch,
|
|
269
154
|
});
|
|
270
155
|
}
|
|
@@ -0,0 +1,14 @@
|
|
|
1
|
+
export type WaitlistFollowUpInput = {
|
|
2
|
+
triggerId?: string;
|
|
3
|
+
email?: string;
|
|
4
|
+
type: string;
|
|
5
|
+
};
|
|
6
|
+
|
|
7
|
+
export async function recordWaitlistFollowUp(input: WaitlistFollowUpInput) {
|
|
8
|
+
return {
|
|
9
|
+
status: "queued",
|
|
10
|
+
triggerId: input.triggerId ?? null,
|
|
11
|
+
email: input.email ?? null,
|
|
12
|
+
type: input.type,
|
|
13
|
+
};
|
|
14
|
+
}
|
|
@@ -0,0 +1,38 @@
|
|
|
1
|
+
import { NativeConnection, Worker } from "@temporalio/worker";
|
|
2
|
+
import * as activities from "./activities";
|
|
3
|
+
|
|
4
|
+
export function temporalWorkerEnabled() {
|
|
5
|
+
return (Bun.env.TEMPORAL_ENABLED ?? "").trim().toLowerCase() === "true";
|
|
6
|
+
}
|
|
7
|
+
|
|
8
|
+
export async function startTemporalWorker() {
|
|
9
|
+
if (!temporalWorkerEnabled()) {
|
|
10
|
+
return undefined;
|
|
11
|
+
}
|
|
12
|
+
|
|
13
|
+
const address = Bun.env.TEMPORAL_ADDRESS || "localhost:7233";
|
|
14
|
+
const namespace = Bun.env.TEMPORAL_NAMESPACE || "default";
|
|
15
|
+
const taskQueue = Bun.env.TEMPORAL_TASK_QUEUE || "{{SERVICE_NAME}}";
|
|
16
|
+
const apiKey = Bun.env.TEMPORAL_API_KEY?.trim();
|
|
17
|
+
const connection = await NativeConnection.connect({
|
|
18
|
+
address,
|
|
19
|
+
...(apiKey ? { apiKey } : {}),
|
|
20
|
+
});
|
|
21
|
+
const worker = await Worker.create({
|
|
22
|
+
connection,
|
|
23
|
+
namespace,
|
|
24
|
+
taskQueue,
|
|
25
|
+
workflowsPath: new URL("./workflows.ts", import.meta.url).pathname,
|
|
26
|
+
activities,
|
|
27
|
+
});
|
|
28
|
+
const running = worker.run();
|
|
29
|
+
|
|
30
|
+
return {
|
|
31
|
+
taskQueue,
|
|
32
|
+
async shutdown() {
|
|
33
|
+
worker.shutdown();
|
|
34
|
+
await running.catch(() => undefined);
|
|
35
|
+
await connection.close();
|
|
36
|
+
},
|
|
37
|
+
};
|
|
38
|
+
}
|
|
@@ -0,0 +1,10 @@
|
|
|
1
|
+
import { proxyActivities } from "@temporalio/workflow";
|
|
2
|
+
import type * as activities from "./activities";
|
|
3
|
+
|
|
4
|
+
const { recordWaitlistFollowUp } = proxyActivities<typeof activities>({
|
|
5
|
+
startToCloseTimeout: "1 minute",
|
|
6
|
+
});
|
|
7
|
+
|
|
8
|
+
export async function waitlistFollowUpWorkflow(input: activities.WaitlistFollowUpInput) {
|
|
9
|
+
return await recordWaitlistFollowUp(input);
|
|
10
|
+
}
|
|
@@ -0,0 +1,166 @@
|
|
|
1
|
+
import { createDb } from "../db/client";
|
|
2
|
+
import { WaitlistRepository } from "../db/repository";
|
|
3
|
+
import type {
|
|
4
|
+
JoinWaitlistInput,
|
|
5
|
+
ListWaitlistEntriesInput,
|
|
6
|
+
RecordTriggerInput,
|
|
7
|
+
UpdateWaitlistEntryInput,
|
|
8
|
+
WaitlistEntry,
|
|
9
|
+
WaitlistEntryStatus,
|
|
10
|
+
} from "./types";
|
|
11
|
+
|
|
12
|
+
export class AppError extends Error {
|
|
13
|
+
constructor(
|
|
14
|
+
readonly status: number,
|
|
15
|
+
readonly code: string,
|
|
16
|
+
message: string
|
|
17
|
+
) {
|
|
18
|
+
super(message);
|
|
19
|
+
}
|
|
20
|
+
}
|
|
21
|
+
|
|
22
|
+
export type WaitlistService = {
|
|
23
|
+
joinWaitlist(input: JoinWaitlistInput): Promise<{ entry: WaitlistEntry; created: boolean }>;
|
|
24
|
+
getWaitlistEntry(entryId: string): Promise<WaitlistEntry>;
|
|
25
|
+
getWaitlistEntryByEmail(email: string): Promise<WaitlistEntry>;
|
|
26
|
+
listWaitlistEntries(input?: ListWaitlistEntriesInput): Promise<WaitlistEntry[]>;
|
|
27
|
+
updateWaitlistEntry(input: UpdateWaitlistEntryInput): Promise<WaitlistEntry>;
|
|
28
|
+
exportWaitlistEntries(input?: ListWaitlistEntriesInput): Promise<string>;
|
|
29
|
+
recordTrigger(input: RecordTriggerInput): Promise<unknown>;
|
|
30
|
+
};
|
|
31
|
+
|
|
32
|
+
export class DefaultWaitlistService implements WaitlistService {
|
|
33
|
+
constructor(private readonly repository: WaitlistRepository) {}
|
|
34
|
+
|
|
35
|
+
async joinWaitlist(input: JoinWaitlistInput) {
|
|
36
|
+
const email = normalizeEmail(input.email);
|
|
37
|
+
const existing = await this.repository.getEntryByEmail(email);
|
|
38
|
+
if (existing) {
|
|
39
|
+
return { entry: existing, created: false };
|
|
40
|
+
}
|
|
41
|
+
|
|
42
|
+
const entry = await this.repository.createEntry({
|
|
43
|
+
id: crypto.randomUUID(),
|
|
44
|
+
email,
|
|
45
|
+
name: normalizeNullableText(input.name),
|
|
46
|
+
company: normalizeNullableText(input.company),
|
|
47
|
+
source: normalizeNullableText(input.source),
|
|
48
|
+
});
|
|
49
|
+
|
|
50
|
+
await this.repository.createTrigger({
|
|
51
|
+
id: crypto.randomUUID(),
|
|
52
|
+
type: "waitlist.joined",
|
|
53
|
+
entryId: entry.id,
|
|
54
|
+
payload: { email: entry.email, source: entry.source },
|
|
55
|
+
});
|
|
56
|
+
|
|
57
|
+
return { entry, created: true };
|
|
58
|
+
}
|
|
59
|
+
|
|
60
|
+
async getWaitlistEntry(entryId: string) {
|
|
61
|
+
const entry = await this.repository.getEntryById(entryId.trim());
|
|
62
|
+
if (!entry) {
|
|
63
|
+
throw new AppError(404, "entry_not_found", `waitlist entry ${entryId} not found`);
|
|
64
|
+
}
|
|
65
|
+
return entry;
|
|
66
|
+
}
|
|
67
|
+
|
|
68
|
+
async getWaitlistEntryByEmail(email: string) {
|
|
69
|
+
const entry = await this.repository.getEntryByEmail(normalizeEmail(email));
|
|
70
|
+
if (!entry) {
|
|
71
|
+
throw new AppError(404, "entry_not_found", `waitlist entry for ${email} not found`);
|
|
72
|
+
}
|
|
73
|
+
return entry;
|
|
74
|
+
}
|
|
75
|
+
|
|
76
|
+
async listWaitlistEntries(input: ListWaitlistEntriesInput = {}) {
|
|
77
|
+
return this.repository.listEntries({
|
|
78
|
+
status: input.status ? normalizeStatus(input.status) : null,
|
|
79
|
+
limit: input.limit,
|
|
80
|
+
});
|
|
81
|
+
}
|
|
82
|
+
|
|
83
|
+
async updateWaitlistEntry(input: UpdateWaitlistEntryInput) {
|
|
84
|
+
const entryId = input.entryId.trim();
|
|
85
|
+
if (!entryId) {
|
|
86
|
+
throw new AppError(400, "invalid_entry_id", "entry id is required");
|
|
87
|
+
}
|
|
88
|
+
const entry = await this.repository.updateEntryStatus(entryId, normalizeStatus(input.status));
|
|
89
|
+
if (!entry) {
|
|
90
|
+
throw new AppError(404, "entry_not_found", `waitlist entry ${input.entryId} not found`);
|
|
91
|
+
}
|
|
92
|
+
return entry;
|
|
93
|
+
}
|
|
94
|
+
|
|
95
|
+
async exportWaitlistEntries(input: ListWaitlistEntriesInput = {}) {
|
|
96
|
+
return entriesToCsv(await this.listWaitlistEntries(input));
|
|
97
|
+
}
|
|
98
|
+
|
|
99
|
+
async recordTrigger(input: RecordTriggerInput) {
|
|
100
|
+
const type = input.type.trim();
|
|
101
|
+
if (!type) {
|
|
102
|
+
throw new AppError(400, "invalid_trigger_type", "trigger type is required");
|
|
103
|
+
}
|
|
104
|
+
|
|
105
|
+
if (input.entryId) {
|
|
106
|
+
await this.getWaitlistEntry(input.entryId);
|
|
107
|
+
}
|
|
108
|
+
|
|
109
|
+
return this.repository.createTrigger({
|
|
110
|
+
id: crypto.randomUUID(),
|
|
111
|
+
type,
|
|
112
|
+
entryId: input.entryId?.trim() || null,
|
|
113
|
+
payload: input.payload ?? {},
|
|
114
|
+
});
|
|
115
|
+
}
|
|
116
|
+
}
|
|
117
|
+
|
|
118
|
+
export function createDefaultWaitlistService() {
|
|
119
|
+
return new DefaultWaitlistService(new WaitlistRepository(createDb()));
|
|
120
|
+
}
|
|
121
|
+
|
|
122
|
+
function normalizeEmail(value: string) {
|
|
123
|
+
const email = value.trim().toLowerCase();
|
|
124
|
+
if (!email || !/^[^@\s]+@[^@\s]+\.[^@\s]+$/.test(email)) {
|
|
125
|
+
throw new AppError(400, "invalid_email", "valid email is required");
|
|
126
|
+
}
|
|
127
|
+
return email;
|
|
128
|
+
}
|
|
129
|
+
|
|
130
|
+
function normalizeNullableText(value: string | null | undefined) {
|
|
131
|
+
const normalized = value?.trim();
|
|
132
|
+
return normalized ? normalized : null;
|
|
133
|
+
}
|
|
134
|
+
|
|
135
|
+
function normalizeStatus(value: string): WaitlistEntryStatus {
|
|
136
|
+
const status = value.trim().toLowerCase();
|
|
137
|
+
if (status === "joined" || status === "invited" || status === "converted" || status === "archived") {
|
|
138
|
+
return status;
|
|
139
|
+
}
|
|
140
|
+
throw new AppError(400, "invalid_status", "status must be one of joined, invited, converted, archived");
|
|
141
|
+
}
|
|
142
|
+
|
|
143
|
+
function entriesToCsv(entries: WaitlistEntry[]) {
|
|
144
|
+
const headers = ["id", "email", "name", "company", "source", "status", "created_at", "updated_at"];
|
|
145
|
+
return [
|
|
146
|
+
headers.join(","),
|
|
147
|
+
...entries.map((entry) =>
|
|
148
|
+
[
|
|
149
|
+
entry.id,
|
|
150
|
+
entry.email,
|
|
151
|
+
entry.name ?? "",
|
|
152
|
+
entry.company ?? "",
|
|
153
|
+
entry.source ?? "",
|
|
154
|
+
entry.status,
|
|
155
|
+
entry.createdAt,
|
|
156
|
+
entry.updatedAt,
|
|
157
|
+
]
|
|
158
|
+
.map(csvCell)
|
|
159
|
+
.join(",")
|
|
160
|
+
),
|
|
161
|
+
].join("\n");
|
|
162
|
+
}
|
|
163
|
+
|
|
164
|
+
function csvCell(value: string) {
|
|
165
|
+
return `"${value.replaceAll('"', '""')}"`;
|
|
166
|
+
}
|
|
@@ -0,0 +1,50 @@
|
|
|
1
|
+
export type WaitlistEntry = {
|
|
2
|
+
id: string;
|
|
3
|
+
email: string;
|
|
4
|
+
name: string | null;
|
|
5
|
+
company: string | null;
|
|
6
|
+
source: string | null;
|
|
7
|
+
status: WaitlistEntryStatus;
|
|
8
|
+
createdAt: string;
|
|
9
|
+
updatedAt: string;
|
|
10
|
+
};
|
|
11
|
+
|
|
12
|
+
export type WaitlistTrigger = {
|
|
13
|
+
id: string;
|
|
14
|
+
type: string;
|
|
15
|
+
entryId: string | null;
|
|
16
|
+
status: "queued" | "processed" | "failed";
|
|
17
|
+
payload: unknown;
|
|
18
|
+
createdAt: string;
|
|
19
|
+
processedAt: string | null;
|
|
20
|
+
};
|
|
21
|
+
|
|
22
|
+
export type JoinWaitlistInput = {
|
|
23
|
+
email: string;
|
|
24
|
+
name?: string | null;
|
|
25
|
+
company?: string | null;
|
|
26
|
+
source?: string | null;
|
|
27
|
+
};
|
|
28
|
+
|
|
29
|
+
export type JoinWaitlistResult = {
|
|
30
|
+
entry: WaitlistEntry;
|
|
31
|
+
created: boolean;
|
|
32
|
+
};
|
|
33
|
+
|
|
34
|
+
export type WaitlistEntryStatus = "joined" | "invited" | "converted" | "archived";
|
|
35
|
+
|
|
36
|
+
export type ListWaitlistEntriesInput = {
|
|
37
|
+
status?: string | null;
|
|
38
|
+
limit?: number | null;
|
|
39
|
+
};
|
|
40
|
+
|
|
41
|
+
export type UpdateWaitlistEntryInput = {
|
|
42
|
+
entryId: string;
|
|
43
|
+
status: string;
|
|
44
|
+
};
|
|
45
|
+
|
|
46
|
+
export type RecordTriggerInput = {
|
|
47
|
+
type: string;
|
|
48
|
+
entryId?: string | null;
|
|
49
|
+
payload?: unknown;
|
|
50
|
+
};
|