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,133 +1,73 @@
|
|
|
1
|
-
import { connectNodeAdapter } from "@connectrpc/connect-node";
|
|
2
1
|
import { Code, ConnectError } from "@connectrpc/connect";
|
|
2
|
+
import { connectNodeAdapter } from "@connectrpc/connect-node";
|
|
3
3
|
import type { ServiceImpl } from "@connectrpc/connect";
|
|
4
|
-
import { createServer } from "node:
|
|
5
|
-
import {
|
|
6
|
-
import {
|
|
7
|
-
|
|
8
|
-
|
|
4
|
+
import { createServer } from "node:http";
|
|
5
|
+
import { WaitlistService as WaitlistRpcService } from "../gen/protos/waitlist/v1/waitlist_pb.js";
|
|
6
|
+
import { withServiceAuth } from "./auth";
|
|
7
|
+
import { AppError, createDefaultWaitlistService, type WaitlistService } from "./waitlist/service";
|
|
8
|
+
import { startTemporalWorker } from "./temporal/worker";
|
|
9
|
+
import type { WaitlistEntry, WaitlistTrigger } from "./waitlist/types";
|
|
10
|
+
|
|
11
|
+
type RpcService = ServiceImpl<typeof WaitlistRpcService>;
|
|
9
12
|
type FallbackHandler = NonNullable<Parameters<typeof connectNodeAdapter>[0]["fallback"]>;
|
|
10
13
|
|
|
11
|
-
export function createRpcService(service:
|
|
14
|
+
export function createRpcService(service: WaitlistService): Partial<RpcService> {
|
|
12
15
|
return {
|
|
13
|
-
async
|
|
14
|
-
const
|
|
15
|
-
|
|
16
|
-
|
|
16
|
+
async joinWaitlist(request) {
|
|
17
|
+
const result = await service.joinWaitlist({
|
|
18
|
+
email: request.email,
|
|
19
|
+
name: request.name || null,
|
|
20
|
+
company: request.company || null,
|
|
21
|
+
source: request.source || null,
|
|
17
22
|
});
|
|
18
|
-
return {
|
|
23
|
+
return { entry: toRpcEntry(result.entry), created: result.created };
|
|
19
24
|
},
|
|
20
|
-
async
|
|
21
|
-
return {
|
|
22
|
-
},
|
|
23
|
-
async getUserByUsername(request) {
|
|
24
|
-
return { user: toRpcUser(await service.getUserByUsername(request.username)) };
|
|
25
|
-
},
|
|
26
|
-
async createConversation(request) {
|
|
27
|
-
return {
|
|
28
|
-
conversation: toRpcConversation(
|
|
29
|
-
await service.createConversation({
|
|
30
|
-
createdByUserId: request.createdByUserId,
|
|
31
|
-
title: request.title || null,
|
|
32
|
-
participantUserIds: request.participantUserIds,
|
|
33
|
-
})
|
|
34
|
-
),
|
|
35
|
-
};
|
|
25
|
+
async getWaitlistEntry(request) {
|
|
26
|
+
return { entry: toRpcEntry(await service.getWaitlistEntry(request.entryId)) };
|
|
36
27
|
},
|
|
37
|
-
async
|
|
38
|
-
return {
|
|
28
|
+
async getWaitlistEntryByEmail(request) {
|
|
29
|
+
return { entry: toRpcEntry(await service.getWaitlistEntryByEmail(request.email)) };
|
|
39
30
|
},
|
|
40
|
-
async
|
|
41
|
-
|
|
42
|
-
|
|
43
|
-
await service.updateConversation(request.conversationId, { title: request.title || null })
|
|
44
|
-
),
|
|
45
|
-
};
|
|
46
|
-
},
|
|
47
|
-
async deleteConversation(request) {
|
|
48
|
-
await service.deleteConversation(request.conversationId);
|
|
49
|
-
return {};
|
|
50
|
-
},
|
|
51
|
-
async addConversationParticipant(request) {
|
|
52
|
-
return {
|
|
53
|
-
conversation: toRpcConversation(
|
|
54
|
-
await service.addParticipant(request.conversationId, request.userId)
|
|
55
|
-
),
|
|
56
|
-
};
|
|
57
|
-
},
|
|
58
|
-
async removeConversationParticipant(request) {
|
|
59
|
-
await service.removeParticipant(request.conversationId, request.userId);
|
|
60
|
-
return {};
|
|
61
|
-
},
|
|
62
|
-
async listMessages(request) {
|
|
63
|
-
const result = await service.listMessages(request.conversationId, {
|
|
64
|
-
cursor: request.cursor || null,
|
|
31
|
+
async listWaitlistEntries(request) {
|
|
32
|
+
const entries = await service.listWaitlistEntries({
|
|
33
|
+
status: request.status || null,
|
|
65
34
|
limit: request.limit || null,
|
|
66
35
|
});
|
|
67
|
-
return {
|
|
68
|
-
messages: result.messages.map(toRpcMessage),
|
|
69
|
-
nextCursor: result.nextCursor ?? "",
|
|
70
|
-
};
|
|
36
|
+
return { entries: entries.map(toRpcEntry) };
|
|
71
37
|
},
|
|
72
|
-
async
|
|
38
|
+
async updateWaitlistEntry(request) {
|
|
73
39
|
return {
|
|
74
|
-
|
|
75
|
-
await service.
|
|
76
|
-
|
|
77
|
-
|
|
40
|
+
entry: toRpcEntry(
|
|
41
|
+
await service.updateWaitlistEntry({
|
|
42
|
+
entryId: request.entryId,
|
|
43
|
+
status: request.status,
|
|
78
44
|
})
|
|
79
45
|
),
|
|
80
46
|
};
|
|
81
47
|
},
|
|
82
|
-
async
|
|
48
|
+
async exportWaitlistEntries(request) {
|
|
83
49
|
return {
|
|
84
|
-
|
|
85
|
-
|
|
86
|
-
|
|
50
|
+
csv: await service.exportWaitlistEntries({
|
|
51
|
+
status: request.status || null,
|
|
52
|
+
limit: request.limit || null,
|
|
53
|
+
}),
|
|
87
54
|
};
|
|
88
55
|
},
|
|
89
|
-
async
|
|
90
|
-
await service.
|
|
91
|
-
|
|
92
|
-
|
|
93
|
-
|
|
94
|
-
|
|
95
|
-
|
|
96
|
-
uploadedByUserId: request.userId,
|
|
97
|
-
filename: request.filename,
|
|
98
|
-
contentType: request.contentType,
|
|
99
|
-
byteSize: Number(request.byteSize),
|
|
100
|
-
});
|
|
101
|
-
return {
|
|
102
|
-
attachment: toRpcAttachment(result.attachment),
|
|
103
|
-
upload: {
|
|
104
|
-
method: result.upload.method,
|
|
105
|
-
url: result.upload.url,
|
|
106
|
-
headers: result.upload.headers,
|
|
107
|
-
},
|
|
108
|
-
};
|
|
109
|
-
},
|
|
110
|
-
async finalizeAttachment(request) {
|
|
111
|
-
return {
|
|
112
|
-
attachment: toRpcAttachment(
|
|
113
|
-
await service.finalizeAttachment(request.attachmentId, { messageId: request.messageId || null })
|
|
114
|
-
),
|
|
115
|
-
};
|
|
116
|
-
},
|
|
117
|
-
async getAttachment(request) {
|
|
118
|
-
return { attachment: toRpcAttachment(await service.getAttachment(request.attachmentId)) };
|
|
119
|
-
},
|
|
120
|
-
async deleteAttachment(request) {
|
|
121
|
-
await service.deleteAttachment(request.attachmentId);
|
|
122
|
-
return {};
|
|
56
|
+
async recordTrigger(request) {
|
|
57
|
+
const trigger = (await service.recordTrigger({
|
|
58
|
+
type: request.type,
|
|
59
|
+
entryId: request.entryId || null,
|
|
60
|
+
payloadJson: request.payloadJson || "{}",
|
|
61
|
+
})) as WaitlistTrigger;
|
|
62
|
+
return { trigger: toRpcTrigger(trigger) };
|
|
123
63
|
},
|
|
124
64
|
};
|
|
125
65
|
}
|
|
126
66
|
|
|
127
|
-
export function createHandler(service:
|
|
67
|
+
export function createHandler(service: WaitlistService) {
|
|
128
68
|
return connectNodeAdapter({
|
|
129
69
|
routes: (router) => {
|
|
130
|
-
router.service(
|
|
70
|
+
router.service(WaitlistRpcService, createRpcService(service));
|
|
131
71
|
},
|
|
132
72
|
fallback: (async (request: Parameters<FallbackHandler>[0], response: Parameters<FallbackHandler>[1]) => {
|
|
133
73
|
const url = new URL(request.url ?? "/", "http://localhost");
|
|
@@ -141,7 +81,7 @@ export function createHandler(service: ChatService) {
|
|
|
141
81
|
if (path === "/") {
|
|
142
82
|
respondJson(response, 200, {
|
|
143
83
|
service: "{{SERVICE_NAME}}",
|
|
144
|
-
domain: "
|
|
84
|
+
domain: "waitlist",
|
|
145
85
|
apiOrigin: "https://api.{{SERVICE_NAME}}.anmho.com",
|
|
146
86
|
});
|
|
147
87
|
return;
|
|
@@ -156,8 +96,11 @@ export function createHandler(service: ChatService) {
|
|
|
156
96
|
try {
|
|
157
97
|
const provider = path.split("/").filter(Boolean)[1] ?? "generic";
|
|
158
98
|
const rawBody = await readRawBody(request);
|
|
159
|
-
const
|
|
160
|
-
|
|
99
|
+
const trigger = await service.recordTrigger({
|
|
100
|
+
type: `webhook.${provider}`,
|
|
101
|
+
payloadJson: JSON.stringify({ headers: request.headers, rawBody }),
|
|
102
|
+
});
|
|
103
|
+
respondJson(response, 202, { trigger });
|
|
161
104
|
} catch (error) {
|
|
162
105
|
respondAppError(response, error);
|
|
163
106
|
}
|
|
@@ -177,9 +120,9 @@ export function createHandler(service: ChatService) {
|
|
|
177
120
|
|
|
178
121
|
export function createIntrospectionDocument() {
|
|
179
122
|
return {
|
|
180
|
-
service:
|
|
181
|
-
file:
|
|
182
|
-
methods:
|
|
123
|
+
service: WaitlistRpcService.typeName,
|
|
124
|
+
file: WaitlistRpcService.file.proto.name,
|
|
125
|
+
methods: WaitlistRpcService.methods.map((method) => ({
|
|
183
126
|
name: method.name,
|
|
184
127
|
localName: method.localName,
|
|
185
128
|
kind: method.methodKind,
|
|
@@ -197,62 +140,28 @@ export function isLocalRpcIntrospectionEnabled() {
|
|
|
197
140
|
return !Bun.env.K_SERVICE && Bun.env.NODE_ENV !== "production";
|
|
198
141
|
}
|
|
199
142
|
|
|
200
|
-
function
|
|
201
|
-
return {
|
|
202
|
-
id: user.id,
|
|
203
|
-
username: user.username,
|
|
204
|
-
displayName: user.displayName ?? "",
|
|
205
|
-
createdAt: user.createdAt,
|
|
206
|
-
updatedAt: user.updatedAt,
|
|
207
|
-
};
|
|
208
|
-
}
|
|
209
|
-
|
|
210
|
-
function toRpcConversation(conversation: Awaited<ReturnType<ChatService["getConversation"]>>) {
|
|
143
|
+
function toRpcEntry(entry: WaitlistEntry) {
|
|
211
144
|
return {
|
|
212
|
-
id:
|
|
213
|
-
|
|
214
|
-
|
|
215
|
-
|
|
216
|
-
|
|
217
|
-
|
|
145
|
+
id: entry.id,
|
|
146
|
+
email: entry.email,
|
|
147
|
+
name: entry.name ?? "",
|
|
148
|
+
company: entry.company ?? "",
|
|
149
|
+
source: entry.source ?? "",
|
|
150
|
+
status: entry.status,
|
|
151
|
+
createdAt: entry.createdAt,
|
|
152
|
+
updatedAt: entry.updatedAt,
|
|
218
153
|
};
|
|
219
154
|
}
|
|
220
155
|
|
|
221
|
-
function
|
|
156
|
+
function toRpcTrigger(trigger: WaitlistTrigger) {
|
|
222
157
|
return {
|
|
223
|
-
id:
|
|
224
|
-
|
|
225
|
-
|
|
226
|
-
|
|
227
|
-
|
|
228
|
-
createdAt:
|
|
229
|
-
|
|
230
|
-
attachments: message.attachments.map((attachment) => ({
|
|
231
|
-
id: attachment.id,
|
|
232
|
-
filename: attachment.filename,
|
|
233
|
-
contentType: attachment.contentType,
|
|
234
|
-
byteSize: BigInt(attachment.byteSize),
|
|
235
|
-
status: attachment.status,
|
|
236
|
-
publicUrl: attachment.publicUrl,
|
|
237
|
-
})),
|
|
238
|
-
};
|
|
239
|
-
}
|
|
240
|
-
|
|
241
|
-
function toRpcAttachment(attachment: Awaited<ReturnType<ChatService["getAttachment"]>>) {
|
|
242
|
-
return {
|
|
243
|
-
id: attachment.id,
|
|
244
|
-
conversationId: attachment.conversationId,
|
|
245
|
-
messageId: attachment.messageId ?? "",
|
|
246
|
-
uploadedByUserId: attachment.uploadedByUserId,
|
|
247
|
-
storageBucket: attachment.storageBucket,
|
|
248
|
-
storageKey: attachment.storageKey,
|
|
249
|
-
contentType: attachment.contentType,
|
|
250
|
-
byteSize: BigInt(attachment.byteSize),
|
|
251
|
-
filename: attachment.filename,
|
|
252
|
-
status: attachment.status,
|
|
253
|
-
publicUrl: attachment.publicUrl,
|
|
254
|
-
createdAt: attachment.createdAt,
|
|
255
|
-
updatedAt: attachment.updatedAt,
|
|
158
|
+
id: trigger.id,
|
|
159
|
+
type: trigger.type,
|
|
160
|
+
entryId: trigger.entryId ?? "",
|
|
161
|
+
status: trigger.status,
|
|
162
|
+
payloadJson: trigger.payloadJson,
|
|
163
|
+
createdAt: trigger.createdAt,
|
|
164
|
+
processedAt: trigger.processedAt ?? "",
|
|
256
165
|
};
|
|
257
166
|
}
|
|
258
167
|
|
|
@@ -271,7 +180,7 @@ function respondAppError(response: Parameters<FallbackHandler>[1], error: unknow
|
|
|
271
180
|
respondJson(response, 500, { error: error.message, code: error.code });
|
|
272
181
|
return;
|
|
273
182
|
}
|
|
274
|
-
respondJson(response, 500, { error: error instanceof Error ? error.message : String(error) });
|
|
183
|
+
respondJson(response, 500, { error: error instanceof Error ? error.message : String(error), code: Code[Code.Internal] });
|
|
275
184
|
}
|
|
276
185
|
|
|
277
186
|
function readRawBody(request: Parameters<FallbackHandler>[0]) {
|
|
@@ -283,22 +192,13 @@ function readRawBody(request: Parameters<FallbackHandler>[0]) {
|
|
|
283
192
|
});
|
|
284
193
|
}
|
|
285
194
|
|
|
286
|
-
|
|
287
|
-
const
|
|
288
|
-
|
|
289
|
-
|
|
290
|
-
for (const item of value) {
|
|
291
|
-
headers.append(key, item);
|
|
292
|
-
}
|
|
293
|
-
} else if (value) {
|
|
294
|
-
headers.set(key, value);
|
|
295
|
-
}
|
|
195
|
+
if (import.meta.main) {
|
|
196
|
+
const temporalWorker = await startTemporalWorker();
|
|
197
|
+
if (temporalWorker) {
|
|
198
|
+
console.log(`Temporal worker polling ${temporalWorker.taskQueue}`);
|
|
296
199
|
}
|
|
297
|
-
return headers;
|
|
298
|
-
}
|
|
299
200
|
|
|
300
|
-
if (import.meta.main) {
|
|
301
201
|
const port = Number(Bun.env.PORT ?? 8080);
|
|
302
|
-
const server = createServer(createHandler(
|
|
202
|
+
const server = createServer(withServiceAuth(createHandler(createDefaultWaitlistService())));
|
|
303
203
|
server.listen(port);
|
|
304
204
|
}
|
|
@@ -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,172 @@
|
|
|
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
|
+
payloadJson: JSON.stringify({ 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
|
+
payloadJson: normalizePayloadJson(input.payloadJson),
|
|
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
|
+
}
|
|
167
|
+
|
|
168
|
+
function normalizePayloadJson(value: string | null | undefined) {
|
|
169
|
+
const payload = value?.trim() || "{}";
|
|
170
|
+
JSON.parse(payload);
|
|
171
|
+
return payload;
|
|
172
|
+
}
|
|
@@ -0,0 +1,45 @@
|
|
|
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
|
+
payloadJson: string;
|
|
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 WaitlistEntryStatus = "joined" | "invited" | "converted" | "archived";
|
|
30
|
+
|
|
31
|
+
export type ListWaitlistEntriesInput = {
|
|
32
|
+
status?: string | null;
|
|
33
|
+
limit?: number | null;
|
|
34
|
+
};
|
|
35
|
+
|
|
36
|
+
export type UpdateWaitlistEntryInput = {
|
|
37
|
+
entryId: string;
|
|
38
|
+
status: string;
|
|
39
|
+
};
|
|
40
|
+
|
|
41
|
+
export type RecordTriggerInput = {
|
|
42
|
+
type: string;
|
|
43
|
+
entryId?: string | null;
|
|
44
|
+
payloadJson?: string | null;
|
|
45
|
+
};
|
|
@@ -1,12 +1,12 @@
|
|
|
1
1
|
import { expect, test } from "bun:test";
|
|
2
2
|
import { createIntrospectionDocument, isLocalRpcIntrospectionEnabled } from "../src/index";
|
|
3
3
|
|
|
4
|
-
test("local introspection document exposes
|
|
4
|
+
test("local introspection document exposes waitlist service and methods", () => {
|
|
5
5
|
const document = createIntrospectionDocument();
|
|
6
6
|
|
|
7
|
-
expect(document.service).toBe("
|
|
8
|
-
expect(document.methods.map((method) => method.name)).toContain("
|
|
9
|
-
expect(document.methods.map((method) => method.name)).toContain("
|
|
7
|
+
expect(document.service).toBe("waitlist.v1.WaitlistService");
|
|
8
|
+
expect(document.methods.map((method) => method.name)).toContain("JoinWaitlist");
|
|
9
|
+
expect(document.methods.map((method) => method.name)).toContain("RecordTrigger");
|
|
10
10
|
});
|
|
11
11
|
|
|
12
12
|
test("local introspection defaults to enabled outside Cloud Run", () => {
|