create-svc 0.1.10 → 0.1.11

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 (168) hide show
  1. package/README.md +46 -43
  2. package/bin/create-service.mjs +2 -0
  3. package/package.json +12 -9
  4. package/src/cli.test.ts +28 -10
  5. package/src/cli.ts +195 -30
  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 +231 -40
  14. package/src/scaffold.ts +84 -29
  15. package/src/vault.test.ts +61 -1
  16. package/src/vault.ts +77 -15
  17. package/templates/shared/.github/workflows/ci.yml +2 -1
  18. package/templates/shared/.github/workflows/deploy.yml +2 -0
  19. package/templates/shared/README.md +124 -47
  20. package/templates/shared/grafana/alerts.yaml +54 -0
  21. package/templates/shared/grafana/waitlist-dashboard.json +63 -0
  22. package/templates/shared/scripts/authctl.ts +231 -0
  23. package/templates/shared/scripts/cloudrun/bootstrap.ts +14 -5
  24. package/templates/shared/scripts/cloudrun/cleanup.ts +64 -4
  25. package/templates/shared/scripts/cloudrun/cli.ts +324 -7
  26. package/templates/shared/scripts/cloudrun/config.ts +11 -4
  27. package/templates/shared/scripts/cloudrun/deploy.ts +0 -4
  28. package/templates/shared/scripts/cloudrun/lib.ts +174 -41
  29. package/templates/shared/scripts/cloudrun/neon.ts +45 -0
  30. package/templates/shared/scripts/dev.ts +22 -0
  31. package/templates/shared/scripts/ensure-local-db.ts +3 -0
  32. package/templates/shared/scripts/local-docker.ts +63 -0
  33. package/templates/shared/scripts/local-env.ts +27 -0
  34. package/templates/shared/scripts/seed.ts +73 -0
  35. package/templates/shared/scripts/wait-for-db.ts +32 -0
  36. package/templates/shared/service.config.ts +59 -0
  37. package/templates/shared/service.yaml +24 -44
  38. package/templates/targets/workers/.github/workflows/ci.yml +19 -0
  39. package/templates/targets/workers/.github/workflows/deploy.yml +19 -0
  40. package/templates/targets/workers/Makefile +33 -0
  41. package/templates/targets/workers/README.md +75 -0
  42. package/templates/targets/workers/package.json +35 -0
  43. package/templates/targets/workers/scripts/workers/cli.ts +397 -0
  44. package/templates/targets/workers/src/auth.ts +178 -0
  45. package/templates/targets/workers/src/index.ts +198 -0
  46. package/templates/targets/workers/src/storage.ts +370 -0
  47. package/templates/targets/workers/test/app.test.ts +108 -0
  48. package/templates/targets/workers/tsconfig.json +11 -0
  49. package/templates/targets/workers/wrangler.toml +24 -0
  50. package/templates/variants/bun-connectrpc/Makefile +14 -8
  51. package/templates/variants/bun-connectrpc/gen/protos/waitlist/v1/waitlist_pb.ts +424 -0
  52. package/templates/variants/bun-connectrpc/migrations/0000_init.sql +12 -55
  53. package/templates/variants/bun-connectrpc/package.json +12 -5
  54. package/templates/variants/bun-connectrpc/protos/waitlist/v1/waitlist.proto +91 -0
  55. package/templates/variants/bun-connectrpc/scripts/codegen.ts +1 -1
  56. package/templates/variants/bun-connectrpc/scripts/migrate.ts +4 -1
  57. package/templates/variants/bun-connectrpc/src/auth.ts +200 -0
  58. package/templates/variants/bun-connectrpc/src/db/repository.ts +67 -420
  59. package/templates/variants/bun-connectrpc/src/db/schema.ts +15 -64
  60. package/templates/variants/bun-connectrpc/src/index.ts +76 -176
  61. package/templates/variants/bun-connectrpc/src/temporal/activities.ts +14 -0
  62. package/templates/variants/bun-connectrpc/src/temporal/worker.ts +38 -0
  63. package/templates/variants/bun-connectrpc/src/temporal/workflows.ts +10 -0
  64. package/templates/variants/bun-connectrpc/src/waitlist/service.ts +172 -0
  65. package/templates/variants/bun-connectrpc/src/waitlist/types.ts +45 -0
  66. package/templates/variants/bun-connectrpc/test/app.test.ts +4 -4
  67. package/templates/variants/bun-connectrpc/test/waitlist.integration.test.ts +71 -0
  68. package/templates/variants/bun-hono/Makefile +14 -8
  69. package/templates/variants/bun-hono/migrations/0000_init.sql +12 -55
  70. package/templates/variants/bun-hono/package.json +12 -5
  71. package/templates/variants/bun-hono/scripts/migrate.ts +4 -1
  72. package/templates/variants/bun-hono/src/auth.ts +181 -0
  73. package/templates/variants/bun-hono/src/db/repository.ts +68 -421
  74. package/templates/variants/bun-hono/src/db/schema.ts +15 -64
  75. package/templates/variants/bun-hono/src/index.ts +65 -180
  76. package/templates/variants/bun-hono/src/temporal/activities.ts +14 -0
  77. package/templates/variants/bun-hono/src/temporal/worker.ts +38 -0
  78. package/templates/variants/bun-hono/src/temporal/workflows.ts +10 -0
  79. package/templates/variants/bun-hono/src/waitlist/service.ts +166 -0
  80. package/templates/variants/bun-hono/src/waitlist/types.ts +50 -0
  81. package/templates/variants/bun-hono/test/app.test.ts +72 -41
  82. package/templates/variants/bun-hono/test/waitlist.integration.test.ts +102 -0
  83. package/templates/variants/go-chi/Makefile +27 -11
  84. package/templates/variants/go-chi/atlas.hcl +8 -0
  85. package/templates/variants/go-chi/cmd/server/main.go +21 -10
  86. package/templates/variants/go-chi/go.mod +1 -3
  87. package/templates/variants/go-chi/internal/app/service.go +202 -685
  88. package/templates/variants/go-chi/internal/auth/middleware.go +289 -0
  89. package/templates/variants/go-chi/internal/auth/middleware_test.go +38 -0
  90. package/templates/variants/go-chi/internal/config/config.go +27 -11
  91. package/templates/variants/go-chi/internal/httpapi/routes.go +78 -157
  92. package/templates/variants/go-chi/internal/httpapi/waitlist_integration_test.go +199 -0
  93. package/templates/variants/go-chi/internal/temporal/activities.go +27 -0
  94. package/templates/variants/go-chi/internal/temporal/worker.go +42 -0
  95. package/templates/variants/go-chi/internal/temporal/workflows.go +18 -0
  96. package/templates/variants/go-chi/migrations/0000_init.sql +12 -55
  97. package/templates/variants/go-chi/migrations/atlas.sum +2 -0
  98. package/templates/variants/go-chi/package.json +7 -1
  99. package/templates/variants/go-connectrpc/Makefile +26 -9
  100. package/templates/variants/go-connectrpc/atlas.hcl +8 -0
  101. package/templates/variants/go-connectrpc/buf.gen.yaml +2 -2
  102. package/templates/variants/go-connectrpc/cmd/server/main.go +23 -12
  103. package/templates/variants/go-connectrpc/gen/waitlist/v1/waitlist.pb.go +960 -0
  104. package/templates/variants/go-connectrpc/gen/waitlist/v1/waitlistv1connect/waitlist.connect.go +283 -0
  105. package/templates/variants/go-connectrpc/go.mod +1 -1
  106. package/templates/variants/go-connectrpc/internal/app/service.go +202 -685
  107. package/templates/variants/go-connectrpc/internal/auth/middleware.go +289 -0
  108. package/templates/variants/go-connectrpc/internal/auth/middleware_test.go +38 -0
  109. package/templates/variants/go-connectrpc/internal/config/config.go +27 -11
  110. package/templates/variants/go-connectrpc/internal/connectapi/handler.go +78 -201
  111. package/templates/variants/go-connectrpc/internal/connectapi/waitlist_integration_test.go +122 -0
  112. package/templates/variants/go-connectrpc/internal/httpapi/routes.go +147 -9
  113. package/templates/variants/go-connectrpc/internal/temporal/activities.go +27 -0
  114. package/templates/variants/go-connectrpc/internal/temporal/worker.go +42 -0
  115. package/templates/variants/go-connectrpc/internal/temporal/workflows.go +18 -0
  116. package/templates/variants/go-connectrpc/migrations/0000_init.sql +12 -55
  117. package/templates/variants/go-connectrpc/migrations/atlas.sum +2 -0
  118. package/templates/variants/go-connectrpc/package.json +7 -1
  119. package/templates/variants/go-connectrpc/protos/waitlist/v1/waitlist.proto +93 -0
  120. package/templates/root/.github/workflows/buf-publish.yml +0 -19
  121. package/templates/root/.github/workflows/ci.yml +0 -26
  122. package/templates/root/.github/workflows/deploy.yml +0 -22
  123. package/templates/root/Dockerfile +0 -23
  124. package/templates/root/README.md +0 -69
  125. package/templates/root/buf.gen.yaml +0 -10
  126. package/templates/root/buf.yaml +0 -9
  127. package/templates/root/cmd/server/main.go +0 -44
  128. package/templates/root/gen/dns/v1/dns.pb.go +0 -623
  129. package/templates/root/gen/dns/v1/dnsv1connect/dns.connect.go +0 -192
  130. package/templates/root/go.mod +0 -10
  131. package/templates/root/internal/app/service.go +0 -152
  132. package/templates/root/internal/app/token_source.go +0 -50
  133. package/templates/root/internal/cloudflare/client.go +0 -160
  134. package/templates/root/internal/config/config.go +0 -55
  135. package/templates/root/internal/connectapi/handler.go +0 -79
  136. package/templates/root/internal/httpapi/routes.go +0 -93
  137. package/templates/root/internal/vault/client.go +0 -148
  138. package/templates/root/package.json +0 -12
  139. package/templates/root/protos/dns/v1/dns.proto +0 -58
  140. package/templates/root/scripts/cloudrun/bootstrap.ts +0 -65
  141. package/templates/root/scripts/cloudrun/config.ts +0 -50
  142. package/templates/root/scripts/cloudrun/deploy.ts +0 -41
  143. package/templates/root/scripts/cloudrun/lib.ts +0 -244
  144. package/templates/root/service.yaml +0 -50
  145. package/templates/root/test/go.test.ts +0 -19
  146. package/templates/shared/scripts/cloudrun/integrations.ts +0 -111
  147. package/templates/variants/bun-connectrpc/gen/protos/chat/v1/chat_pb.ts +0 -1078
  148. package/templates/variants/bun-connectrpc/protos/chat/v1/chat.proto +0 -228
  149. package/templates/variants/bun-connectrpc/src/chat/service.ts +0 -384
  150. package/templates/variants/bun-connectrpc/src/chat/types.ts +0 -142
  151. package/templates/variants/bun-connectrpc/src/storage.ts +0 -72
  152. package/templates/variants/bun-connectrpc/src/webhooks.ts +0 -35
  153. package/templates/variants/bun-connectrpc/test/list-messages.integration.test.ts +0 -182
  154. package/templates/variants/bun-hono/src/chat/service.ts +0 -384
  155. package/templates/variants/bun-hono/src/chat/types.ts +0 -142
  156. package/templates/variants/bun-hono/src/storage.ts +0 -72
  157. package/templates/variants/bun-hono/src/webhooks.ts +0 -35
  158. package/templates/variants/bun-hono/test/list-messages.integration.test.ts +0 -256
  159. package/templates/variants/go-chi/buf.gen.yaml +0 -12
  160. package/templates/variants/go-chi/buf.yaml +0 -9
  161. package/templates/variants/go-chi/cmd/migrate/main.go +0 -101
  162. package/templates/variants/go-chi/internal/httpapi/list_messages_integration_test.go +0 -298
  163. package/templates/variants/go-chi/protos/chat/v1/chat.proto +0 -219
  164. package/templates/variants/go-connectrpc/cmd/migrate/main.go +0 -101
  165. package/templates/variants/go-connectrpc/gen/chat/v1/chat.pb.go +0 -2512
  166. package/templates/variants/go-connectrpc/gen/chat/v1/chatv1connect/chat.connect.go +0 -571
  167. package/templates/variants/go-connectrpc/internal/connectapi/list_messages_integration_test.go +0 -216
  168. package/templates/variants/go-connectrpc/protos/chat/v1/chat.proto +0 -232
@@ -2,214 +2,114 @@ package connectapi
2
2
 
3
3
  import (
4
4
  "context"
5
+ "encoding/json"
6
+ "errors"
5
7
  "net/http"
6
8
 
7
9
  "connectrpc.com/connect"
8
10
 
9
- chatv1 "{{MODULE_PATH}}/gen/chat/v1"
10
- chatv1connect "{{MODULE_PATH}}/gen/chat/v1/chatv1connect"
11
+ waitlistv1 "{{MODULE_PATH}}/gen/waitlist/v1"
12
+ waitlistv1connect "{{MODULE_PATH}}/gen/waitlist/v1/waitlistv1connect"
11
13
  "{{MODULE_PATH}}/internal/app"
12
14
  )
13
15
 
14
16
  type Handler struct {
15
- service *app.ChatService
17
+ service *app.WaitlistService
16
18
  }
17
19
 
18
- func NewHandler(service *app.ChatService) (string, http.Handler) {
19
- return chatv1connect.NewChatServiceHandler(&Handler{service: service})
20
+ func NewHandler(service *app.WaitlistService) (string, http.Handler) {
21
+ return waitlistv1connect.NewWaitlistServiceHandler(&Handler{service: service})
20
22
  }
21
23
 
22
- func (h *Handler) CreateUser(ctx context.Context, request *connect.Request[chatv1.CreateUserRequest]) (*connect.Response[chatv1.CreateUserResponse], error) {
23
- user, err := h.service.CreateUser(ctx, app.CreateUserInput{
24
- Username: request.Msg.GetUsername(),
25
- DisplayName: request.Msg.GetDisplayName(),
24
+ func (h *Handler) JoinWaitlist(ctx context.Context, request *connect.Request[waitlistv1.JoinWaitlistRequest]) (*connect.Response[waitlistv1.JoinWaitlistResponse], error) {
25
+ result, err := h.service.JoinWaitlist(ctx, app.JoinWaitlistInput{
26
+ Email: request.Msg.GetEmail(),
27
+ Name: request.Msg.GetName(),
28
+ Company: request.Msg.GetCompany(),
29
+ Source: request.Msg.GetSource(),
26
30
  })
27
31
  if err != nil {
28
32
  return nil, toConnectError(err)
29
33
  }
30
- return connect.NewResponse(&chatv1.CreateUserResponse{User: toProtoUser(user)}), nil
31
- }
32
-
33
- func (h *Handler) GetUser(ctx context.Context, request *connect.Request[chatv1.GetUserRequest]) (*connect.Response[chatv1.GetUserResponse], error) {
34
- user, err := h.service.GetUser(ctx, request.Msg.GetUserId())
35
- if err != nil {
36
- return nil, toConnectError(err)
37
- }
38
- return connect.NewResponse(&chatv1.GetUserResponse{User: toProtoUser(user)}), nil
39
- }
40
-
41
- func (h *Handler) GetUserByUsername(ctx context.Context, request *connect.Request[chatv1.GetUserByUsernameRequest]) (*connect.Response[chatv1.GetUserByUsernameResponse], error) {
42
- user, err := h.service.GetUserByUsername(ctx, request.Msg.GetUsername())
43
- if err != nil {
44
- return nil, toConnectError(err)
45
- }
46
- return connect.NewResponse(&chatv1.GetUserByUsernameResponse{User: toProtoUser(user)}), nil
47
- }
48
-
49
- func (h *Handler) CreateConversation(ctx context.Context, request *connect.Request[chatv1.CreateConversationRequest]) (*connect.Response[chatv1.CreateConversationResponse], error) {
50
- conversation, err := h.service.CreateConversation(ctx, app.CreateConversationInput{
51
- CreatedByUserID: request.Msg.GetCreatedByUserId(),
52
- Title: request.Msg.GetTitle(),
53
- ParticipantUserIDs: request.Msg.GetParticipantUserIds(),
54
- })
55
- if err != nil {
56
- return nil, toConnectError(err)
57
- }
58
- return connect.NewResponse(&chatv1.CreateConversationResponse{Conversation: toProtoConversation(conversation)}), nil
34
+ return connect.NewResponse(&waitlistv1.JoinWaitlistResponse{
35
+ Entry: toProtoEntry(result.Entry),
36
+ Created: result.Created,
37
+ }), nil
59
38
  }
60
39
 
61
- func (h *Handler) GetConversation(ctx context.Context, request *connect.Request[chatv1.GetConversationRequest]) (*connect.Response[chatv1.GetConversationResponse], error) {
62
- conversation, err := h.service.GetConversation(ctx, request.Msg.GetConversationId())
40
+ func (h *Handler) GetWaitlistEntry(ctx context.Context, request *connect.Request[waitlistv1.GetWaitlistEntryRequest]) (*connect.Response[waitlistv1.GetWaitlistEntryResponse], error) {
41
+ entry, err := h.service.GetEntry(ctx, request.Msg.GetEntryId())
63
42
  if err != nil {
64
43
  return nil, toConnectError(err)
65
44
  }
66
- return connect.NewResponse(&chatv1.GetConversationResponse{Conversation: toProtoConversation(conversation)}), nil
45
+ return connect.NewResponse(&waitlistv1.GetWaitlistEntryResponse{Entry: toProtoEntry(entry)}), nil
67
46
  }
68
47
 
69
- func (h *Handler) UpdateConversation(ctx context.Context, request *connect.Request[chatv1.UpdateConversationRequest]) (*connect.Response[chatv1.UpdateConversationResponse], error) {
70
- conversation, err := h.service.UpdateConversation(ctx, request.Msg.GetConversationId(), app.UpdateConversationInput{
71
- Title: request.Msg.GetTitle(),
72
- })
48
+ func (h *Handler) GetWaitlistEntryByEmail(ctx context.Context, request *connect.Request[waitlistv1.GetWaitlistEntryByEmailRequest]) (*connect.Response[waitlistv1.GetWaitlistEntryResponse], error) {
49
+ entry, err := h.service.GetEntryByEmail(ctx, request.Msg.GetEmail())
73
50
  if err != nil {
74
51
  return nil, toConnectError(err)
75
52
  }
76
- return connect.NewResponse(&chatv1.UpdateConversationResponse{Conversation: toProtoConversation(conversation)}), nil
77
- }
78
-
79
- func (h *Handler) DeleteConversation(ctx context.Context, request *connect.Request[chatv1.DeleteConversationRequest]) (*connect.Response[chatv1.DeleteConversationResponse], error) {
80
- if err := h.service.DeleteConversation(ctx, request.Msg.GetConversationId()); err != nil {
81
- return nil, toConnectError(err)
82
- }
83
- return connect.NewResponse(&chatv1.DeleteConversationResponse{}), nil
53
+ return connect.NewResponse(&waitlistv1.GetWaitlistEntryResponse{Entry: toProtoEntry(entry)}), nil
84
54
  }
85
55
 
86
- func (h *Handler) AddConversationParticipant(ctx context.Context, request *connect.Request[chatv1.AddConversationParticipantRequest]) (*connect.Response[chatv1.AddConversationParticipantResponse], error) {
87
- conversation, err := h.service.AddParticipant(ctx, request.Msg.GetConversationId(), request.Msg.GetUserId())
88
- if err != nil {
89
- return nil, toConnectError(err)
90
- }
91
- return connect.NewResponse(&chatv1.AddConversationParticipantResponse{Conversation: toProtoConversation(conversation)}), nil
92
- }
93
-
94
- func (h *Handler) RemoveConversationParticipant(ctx context.Context, request *connect.Request[chatv1.RemoveConversationParticipantRequest]) (*connect.Response[chatv1.RemoveConversationParticipantResponse], error) {
95
- if err := h.service.RemoveParticipant(ctx, request.Msg.GetConversationId(), request.Msg.GetUserId()); err != nil {
96
- return nil, toConnectError(err)
97
- }
98
- return connect.NewResponse(&chatv1.RemoveConversationParticipantResponse{}), nil
99
- }
100
-
101
- func (h *Handler) ListMessages(ctx context.Context, request *connect.Request[chatv1.ListMessagesRequest]) (*connect.Response[chatv1.ListMessagesResponse], error) {
102
- result, err := h.service.ListMessages(ctx, request.Msg.GetConversationId(), app.ListMessagesInput{
103
- Cursor: request.Msg.GetCursor(),
56
+ func (h *Handler) ListWaitlistEntries(ctx context.Context, request *connect.Request[waitlistv1.ListWaitlistEntriesRequest]) (*connect.Response[waitlistv1.ListWaitlistEntriesResponse], error) {
57
+ entries, err := h.service.ListEntries(ctx, app.ListWaitlistEntriesInput{
58
+ Status: request.Msg.GetStatus(),
104
59
  Limit: int(request.Msg.GetLimit()),
105
60
  })
106
61
  if err != nil {
107
62
  return nil, toConnectError(err)
108
63
  }
109
- items := make([]*chatv1.Message, 0, len(result.Messages))
110
- for _, message := range result.Messages {
111
- items = append(items, toProtoMessage(message))
112
- }
113
- return connect.NewResponse(&chatv1.ListMessagesResponse{
114
- Messages: items,
115
- NextCursor: result.NextCursor,
116
- }), nil
117
- }
118
-
119
- func (h *Handler) CreateMessage(ctx context.Context, request *connect.Request[chatv1.CreateMessageRequest]) (*connect.Response[chatv1.CreateMessageResponse], error) {
120
- message, err := h.service.CreateMessage(ctx, request.Msg.GetConversationId(), app.CreateMessageInput{
121
- UserID: request.Msg.GetUserId(),
122
- Body: request.Msg.GetBody(),
123
- })
124
- if err != nil {
125
- return nil, toConnectError(err)
64
+ response := &waitlistv1.ListWaitlistEntriesResponse{Entries: make([]*waitlistv1.WaitlistEntry, 0, len(entries))}
65
+ for _, entry := range entries {
66
+ response.Entries = append(response.Entries, toProtoEntry(entry))
126
67
  }
127
- return connect.NewResponse(&chatv1.CreateMessageResponse{Message: toProtoMessage(message)}), nil
68
+ return connect.NewResponse(response), nil
128
69
  }
129
70
 
130
- func (h *Handler) UpdateMessage(ctx context.Context, request *connect.Request[chatv1.UpdateMessageRequest]) (*connect.Response[chatv1.UpdateMessageResponse], error) {
131
- message, err := h.service.UpdateMessage(ctx, request.Msg.GetConversationId(), request.Msg.GetMessageId(), app.UpdateMessageInput{
132
- Body: request.Msg.GetBody(),
71
+ func (h *Handler) UpdateWaitlistEntry(ctx context.Context, request *connect.Request[waitlistv1.UpdateWaitlistEntryRequest]) (*connect.Response[waitlistv1.GetWaitlistEntryResponse], error) {
72
+ entry, err := h.service.UpdateEntry(ctx, app.UpdateWaitlistEntryInput{
73
+ EntryID: request.Msg.GetEntryId(),
74
+ Status: request.Msg.GetStatus(),
133
75
  })
134
76
  if err != nil {
135
77
  return nil, toConnectError(err)
136
78
  }
137
- return connect.NewResponse(&chatv1.UpdateMessageResponse{Message: toProtoMessage(message)}), nil
79
+ return connect.NewResponse(&waitlistv1.GetWaitlistEntryResponse{Entry: toProtoEntry(entry)}), nil
138
80
  }
139
81
 
140
- func (h *Handler) DeleteMessage(ctx context.Context, request *connect.Request[chatv1.DeleteMessageRequest]) (*connect.Response[chatv1.DeleteMessageResponse], error) {
141
- if err := h.service.DeleteMessage(ctx, request.Msg.GetConversationId(), request.Msg.GetMessageId()); err != nil {
142
- return nil, toConnectError(err)
143
- }
144
- return connect.NewResponse(&chatv1.DeleteMessageResponse{}), nil
145
- }
146
-
147
- func (h *Handler) CreateAttachmentUpload(ctx context.Context, request *connect.Request[chatv1.CreateAttachmentUploadRequest]) (*connect.Response[chatv1.CreateAttachmentUploadResponse], error) {
148
- result, err := h.service.CreateAttachmentUpload(ctx, app.CreateAttachmentUploadInput{
149
- ConversationID: request.Msg.GetConversationId(),
150
- UploadedByUserID: request.Msg.GetUserId(),
151
- Filename: request.Msg.GetFilename(),
152
- ContentType: request.Msg.GetContentType(),
153
- ByteSize: request.Msg.GetByteSize(),
82
+ func (h *Handler) ExportWaitlistEntries(ctx context.Context, request *connect.Request[waitlistv1.ExportWaitlistEntriesRequest]) (*connect.Response[waitlistv1.ExportWaitlistEntriesResponse], error) {
83
+ csv, err := h.service.ExportEntries(ctx, app.ListWaitlistEntriesInput{
84
+ Status: request.Msg.GetStatus(),
85
+ Limit: int(request.Msg.GetLimit()),
154
86
  })
155
87
  if err != nil {
156
88
  return nil, toConnectError(err)
157
89
  }
158
- return connect.NewResponse(&chatv1.CreateAttachmentUploadResponse{
159
- Attachment: toProtoAttachment(result.Attachment),
160
- Upload: &chatv1.UploadTarget{
161
- Method: result.Upload.Method,
162
- Url: result.Upload.URL,
163
- Headers: result.Upload.Headers,
164
- },
165
- }), nil
90
+ return connect.NewResponse(&waitlistv1.ExportWaitlistEntriesResponse{Csv: csv}), nil
166
91
  }
167
92
 
168
- func (h *Handler) FinalizeAttachment(ctx context.Context, request *connect.Request[chatv1.FinalizeAttachmentRequest]) (*connect.Response[chatv1.FinalizeAttachmentResponse], error) {
169
- attachment, err := h.service.FinalizeAttachment(ctx, request.Msg.GetAttachmentId(), app.FinalizeAttachmentInput{
170
- MessageID: request.Msg.GetMessageId(),
93
+ func (h *Handler) RecordTrigger(ctx context.Context, request *connect.Request[waitlistv1.RecordTriggerRequest]) (*connect.Response[waitlistv1.RecordTriggerResponse], error) {
94
+ trigger, err := h.service.RecordTrigger(ctx, app.RecordTriggerInput{
95
+ Type: request.Msg.GetType(),
96
+ EntryID: request.Msg.GetEntryId(),
97
+ Payload: jsonPayload(request.Msg.GetPayloadJson()),
171
98
  })
172
99
  if err != nil {
173
100
  return nil, toConnectError(err)
174
101
  }
175
- return connect.NewResponse(&chatv1.FinalizeAttachmentResponse{Attachment: toProtoAttachment(attachment)}), nil
176
- }
177
-
178
- func (h *Handler) GetAttachment(ctx context.Context, request *connect.Request[chatv1.GetAttachmentRequest]) (*connect.Response[chatv1.GetAttachmentResponse], error) {
179
- attachment, err := h.service.GetAttachment(ctx, request.Msg.GetAttachmentId())
180
- if err != nil {
181
- return nil, toConnectError(err)
182
- }
183
- return connect.NewResponse(&chatv1.GetAttachmentResponse{Attachment: toProtoAttachment(attachment)}), nil
184
- }
185
-
186
- func (h *Handler) DeleteAttachment(ctx context.Context, request *connect.Request[chatv1.DeleteAttachmentRequest]) (*connect.Response[chatv1.DeleteAttachmentResponse], error) {
187
- if err := h.service.DeleteAttachment(ctx, request.Msg.GetAttachmentId()); err != nil {
188
- return nil, toConnectError(err)
189
- }
190
- return connect.NewResponse(&chatv1.DeleteAttachmentResponse{}), nil
102
+ return connect.NewResponse(&waitlistv1.RecordTriggerResponse{Trigger: toProtoTrigger(trigger)}), nil
191
103
  }
192
104
 
193
105
  func toConnectError(err error) error {
194
106
  var appErr *app.AppError
195
- if ok := asAppError(err, &appErr); ok {
107
+ if errors.As(err, &appErr) {
196
108
  return connect.NewError(statusCodeToConnectCode(appErr.Status), err)
197
109
  }
198
110
  return connect.NewError(connect.CodeInternal, err)
199
111
  }
200
112
 
201
- func asAppError(err error, target **app.AppError) bool {
202
- if err == nil {
203
- return false
204
- }
205
- appErr, ok := err.(*app.AppError)
206
- if ok {
207
- *target = appErr
208
- return true
209
- }
210
- return false
211
- }
212
-
213
113
  func statusCodeToConnectCode(status int) connect.Code {
214
114
  switch status {
215
115
  case 400:
@@ -223,69 +123,46 @@ func statusCodeToConnectCode(status int) connect.Code {
223
123
  }
224
124
  }
225
125
 
226
- func toProtoUser(user app.User) *chatv1.User {
227
- return &chatv1.User{
228
- Id: user.ID,
229
- Username: user.Username,
230
- DisplayName: user.DisplayName,
231
- CreatedAt: user.CreatedAt,
232
- UpdatedAt: user.UpdatedAt,
126
+ func toProtoEntry(entry app.WaitlistEntry) *waitlistv1.WaitlistEntry {
127
+ return &waitlistv1.WaitlistEntry{
128
+ Id: entry.ID,
129
+ Email: entry.Email,
130
+ Name: entry.Name,
131
+ Company: entry.Company,
132
+ Source: entry.Source,
133
+ Status: entry.Status,
134
+ CreatedAt: entry.CreatedAt,
135
+ UpdatedAt: entry.UpdatedAt,
233
136
  }
234
137
  }
235
138
 
236
- func toProtoConversation(conversation app.Conversation) *chatv1.Conversation {
237
- participants := make([]*chatv1.User, 0, len(conversation.Participants))
238
- for _, participant := range conversation.Participants {
239
- participants = append(participants, toProtoUser(participant))
240
- }
241
- return &chatv1.Conversation{
242
- Id: conversation.ID,
243
- Title: conversation.Title,
244
- CreatedByUserId: conversation.CreatedByUserID,
245
- Participants: participants,
246
- CreatedAt: conversation.CreatedAt,
247
- UpdatedAt: conversation.UpdatedAt,
139
+ func toProtoTrigger(trigger app.WaitlistTrigger) *waitlistv1.WaitlistTrigger {
140
+ return &waitlistv1.WaitlistTrigger{
141
+ Id: trigger.ID,
142
+ Type: trigger.Type,
143
+ EntryId: trigger.EntryID,
144
+ Status: trigger.Status,
145
+ PayloadJson: payloadToJSON(trigger.Payload),
146
+ CreatedAt: trigger.CreatedAt,
147
+ ProcessedAt: trigger.ProcessedAt,
248
148
  }
249
149
  }
250
150
 
251
- func toProtoMessage(message app.Message) *chatv1.Message {
252
- attachments := make([]*chatv1.MessageAttachment, 0, len(message.Attachments))
253
- for _, attachment := range message.Attachments {
254
- attachments = append(attachments, &chatv1.MessageAttachment{
255
- Id: attachment.ID,
256
- Filename: attachment.Filename,
257
- ContentType: attachment.ContentType,
258
- ByteSize: attachment.ByteSize,
259
- Status: attachment.Status,
260
- PublicUrl: attachment.PublicURL,
261
- })
151
+ func jsonPayload(value string) any {
152
+ if value == "" {
153
+ return map[string]any{}
262
154
  }
263
- return &chatv1.Message{
264
- Id: message.ID,
265
- ConversationId: message.ConversationID,
266
- UserId: message.UserID,
267
- Body: message.Body,
268
- EditedAt: message.EditedAt,
269
- CreatedAt: message.CreatedAt,
270
- UpdatedAt: message.UpdatedAt,
271
- Attachments: attachments,
155
+ var payload any
156
+ if err := json.Unmarshal([]byte(value), &payload); err != nil {
157
+ return map[string]any{}
272
158
  }
159
+ return payload
273
160
  }
274
161
 
275
- func toProtoAttachment(attachment app.Attachment) *chatv1.Attachment {
276
- return &chatv1.Attachment{
277
- Id: attachment.ID,
278
- ConversationId: attachment.ConversationID,
279
- MessageId: attachment.MessageID,
280
- UploadedByUserId: attachment.UploadedByUserID,
281
- StorageBucket: attachment.StorageBucket,
282
- StorageKey: attachment.StorageKey,
283
- ContentType: attachment.ContentType,
284
- ByteSize: attachment.ByteSize,
285
- Filename: attachment.Filename,
286
- Status: attachment.Status,
287
- PublicUrl: attachment.PublicURL,
288
- CreatedAt: attachment.CreatedAt,
289
- UpdatedAt: attachment.UpdatedAt,
162
+ func payloadToJSON(value any) string {
163
+ encoded, err := json.Marshal(value)
164
+ if err != nil {
165
+ return "{}"
290
166
  }
167
+ return string(encoded)
291
168
  }
@@ -0,0 +1,122 @@
1
+ package connectapi
2
+
3
+ import (
4
+ "context"
5
+ "net/http"
6
+ "net/http/httptest"
7
+ "os"
8
+ "strings"
9
+ "testing"
10
+
11
+ "connectrpc.com/connect"
12
+ _ "github.com/jackc/pgx/v5/stdlib"
13
+
14
+ waitlistv1 "{{MODULE_PATH}}/gen/waitlist/v1"
15
+ waitlistv1connect "{{MODULE_PATH}}/gen/waitlist/v1/waitlistv1connect"
16
+ "{{MODULE_PATH}}/internal/app"
17
+ )
18
+
19
+ func TestWaitlistRPCJoinIsIdempotentAndRecordsTriggers(t *testing.T) {
20
+ databaseURL := strings.TrimSpace(os.Getenv("DATABASE_URL"))
21
+ if databaseURL == "" {
22
+ t.Skip("DATABASE_URL is required for integration test")
23
+ }
24
+
25
+ db, err := app.OpenDatabase(context.Background(), databaseURL)
26
+ if err != nil {
27
+ t.Fatalf("open database: %v", err)
28
+ }
29
+ t.Cleanup(func() { _ = db.Close() })
30
+
31
+ if _, err := db.ExecContext(context.Background(), `
32
+ truncate table
33
+ waitlist_triggers,
34
+ waitlist_entries
35
+ restart identity cascade`); err != nil {
36
+ t.Fatalf("truncate tables: %v", err)
37
+ }
38
+
39
+ service := app.NewWaitlistService(db)
40
+ path, handler := NewHandler(service)
41
+ mux := http.NewServeMux()
42
+ mux.Handle(path, handler)
43
+ server := httptest.NewServer(mux)
44
+ t.Cleanup(server.Close)
45
+
46
+ client := waitlistv1connect.NewWaitlistServiceClient(http.DefaultClient, server.URL)
47
+
48
+ first, err := client.JoinWaitlist(context.Background(), connect.NewRequest(&waitlistv1.JoinWaitlistRequest{
49
+ Email: "Founder@Example.com",
50
+ Name: "Founder",
51
+ Company: "Example Co",
52
+ Source: "homepage",
53
+ }))
54
+ if err != nil {
55
+ t.Fatalf("join waitlist: %v", err)
56
+ }
57
+ if !first.Msg.GetCreated() {
58
+ t.Fatal("expected first join to create entry")
59
+ }
60
+ if first.Msg.GetEntry().GetEmail() != "founder@example.com" {
61
+ t.Fatalf("expected normalized email, got %s", first.Msg.GetEntry().GetEmail())
62
+ }
63
+
64
+ second, err := client.JoinWaitlist(context.Background(), connect.NewRequest(&waitlistv1.JoinWaitlistRequest{
65
+ Email: "founder@example.com",
66
+ }))
67
+ if err != nil {
68
+ t.Fatalf("join waitlist again: %v", err)
69
+ }
70
+ if second.Msg.GetCreated() {
71
+ t.Fatal("expected second join to be idempotent")
72
+ }
73
+ if second.Msg.GetEntry().GetId() != first.Msg.GetEntry().GetId() {
74
+ t.Fatalf("expected same entry id, got %s and %s", first.Msg.GetEntry().GetId(), second.Msg.GetEntry().GetId())
75
+ }
76
+
77
+ trigger, err := client.RecordTrigger(context.Background(), connect.NewRequest(&waitlistv1.RecordTriggerRequest{
78
+ Type: "cron.digest",
79
+ EntryId: first.Msg.GetEntry().GetId(),
80
+ PayloadJson: "{}",
81
+ }))
82
+ if err != nil {
83
+ t.Fatalf("record trigger: %v", err)
84
+ }
85
+ if trigger.Msg.GetTrigger().GetType() != "cron.digest" {
86
+ t.Fatalf("expected cron.digest trigger, got %s", trigger.Msg.GetTrigger().GetType())
87
+ }
88
+ if trigger.Msg.GetTrigger().GetEntryId() != first.Msg.GetEntry().GetId() {
89
+ t.Fatalf("expected entry id %s, got %s", first.Msg.GetEntry().GetId(), trigger.Msg.GetTrigger().GetEntryId())
90
+ }
91
+
92
+ updated, err := client.UpdateWaitlistEntry(context.Background(), connect.NewRequest(&waitlistv1.UpdateWaitlistEntryRequest{
93
+ EntryId: first.Msg.GetEntry().GetId(),
94
+ Status: "invited",
95
+ }))
96
+ if err != nil {
97
+ t.Fatalf("update waitlist entry: %v", err)
98
+ }
99
+ if updated.Msg.GetEntry().GetStatus() != "invited" {
100
+ t.Fatalf("expected invited status, got %s", updated.Msg.GetEntry().GetStatus())
101
+ }
102
+
103
+ list, err := client.ListWaitlistEntries(context.Background(), connect.NewRequest(&waitlistv1.ListWaitlistEntriesRequest{
104
+ Status: "invited",
105
+ }))
106
+ if err != nil {
107
+ t.Fatalf("list waitlist entries: %v", err)
108
+ }
109
+ if len(list.Msg.GetEntries()) != 1 || list.Msg.GetEntries()[0].GetId() != first.Msg.GetEntry().GetId() {
110
+ t.Fatalf("expected one invited entry %s, got %+v", first.Msg.GetEntry().GetId(), list.Msg.GetEntries())
111
+ }
112
+
113
+ exported, err := client.ExportWaitlistEntries(context.Background(), connect.NewRequest(&waitlistv1.ExportWaitlistEntriesRequest{
114
+ Status: "invited",
115
+ }))
116
+ if err != nil {
117
+ t.Fatalf("export waitlist entries: %v", err)
118
+ }
119
+ if !strings.Contains(exported.Msg.GetCsv(), "founder@example.com") {
120
+ t.Fatalf("expected exported csv to contain email, got %s", exported.Msg.GetCsv())
121
+ }
122
+ }