create-svc 0.1.10 → 0.1.12

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (171) hide show
  1. package/README.md +51 -47
  2. package/index.ts +2 -2
  3. package/package.json +10 -9
  4. package/src/cli.test.ts +28 -10
  5. package/src/cli.ts +196 -33
  6. package/src/git-bootstrap.test.ts +40 -0
  7. package/src/git-bootstrap.ts +110 -0
  8. package/src/naming.test.ts +1 -0
  9. package/src/naming.ts +23 -0
  10. package/src/post-scaffold.test.ts +19 -0
  11. package/src/post-scaffold.ts +17 -4
  12. package/src/profiles.ts +2 -5
  13. package/src/scaffold.test.ts +232 -41
  14. package/src/scaffold.ts +81 -36
  15. package/src/service.test.ts +30 -0
  16. package/src/service.ts +65 -0
  17. package/src/vault.test.ts +61 -1
  18. package/src/vault.ts +77 -15
  19. package/templates/shared/.github/workflows/ci.yml +2 -1
  20. package/templates/shared/.github/workflows/deploy.yml +2 -0
  21. package/templates/shared/README.md +124 -47
  22. package/templates/shared/grafana/alerts.yaml +54 -0
  23. package/templates/shared/grafana/waitlist-dashboard.json +63 -0
  24. package/templates/shared/scripts/authctl.ts +231 -0
  25. package/templates/shared/scripts/cloudrun/bootstrap.ts +14 -5
  26. package/templates/shared/scripts/cloudrun/cleanup.ts +64 -4
  27. package/templates/shared/scripts/cloudrun/cli.ts +329 -7
  28. package/templates/shared/scripts/cloudrun/config.ts +11 -4
  29. package/templates/shared/scripts/cloudrun/deploy.ts +0 -4
  30. package/templates/shared/scripts/cloudrun/lib.ts +174 -41
  31. package/templates/shared/scripts/cloudrun/neon.ts +45 -0
  32. package/templates/shared/scripts/dev.ts +22 -0
  33. package/templates/shared/scripts/ensure-local-db.ts +3 -0
  34. package/templates/shared/scripts/local-docker.ts +63 -0
  35. package/templates/shared/scripts/local-env.ts +27 -0
  36. package/templates/shared/scripts/seed.ts +73 -0
  37. package/templates/shared/scripts/wait-for-db.ts +32 -0
  38. package/templates/shared/service.config.ts +59 -0
  39. package/templates/shared/service.yaml +24 -44
  40. package/templates/targets/workers/.github/workflows/ci.yml +19 -0
  41. package/templates/targets/workers/.github/workflows/deploy.yml +19 -0
  42. package/templates/targets/workers/Makefile +33 -0
  43. package/templates/targets/workers/README.md +75 -0
  44. package/templates/targets/workers/package.json +35 -0
  45. package/templates/targets/workers/scripts/workers/cli.ts +402 -0
  46. package/templates/targets/workers/src/auth.ts +178 -0
  47. package/templates/targets/workers/src/index.ts +198 -0
  48. package/templates/targets/workers/src/storage.ts +370 -0
  49. package/templates/targets/workers/test/app.test.ts +108 -0
  50. package/templates/targets/workers/tsconfig.json +11 -0
  51. package/templates/targets/workers/wrangler.toml +24 -0
  52. package/templates/variants/bun-connectrpc/Makefile +14 -8
  53. package/templates/variants/bun-connectrpc/gen/protos/waitlist/v1/waitlist_pb.ts +424 -0
  54. package/templates/variants/bun-connectrpc/migrations/0000_init.sql +12 -55
  55. package/templates/variants/bun-connectrpc/package.json +12 -5
  56. package/templates/variants/bun-connectrpc/protos/waitlist/v1/waitlist.proto +91 -0
  57. package/templates/variants/bun-connectrpc/scripts/codegen.ts +1 -1
  58. package/templates/variants/bun-connectrpc/scripts/migrate.ts +4 -1
  59. package/templates/variants/bun-connectrpc/src/auth.ts +200 -0
  60. package/templates/variants/bun-connectrpc/src/db/repository.ts +67 -420
  61. package/templates/variants/bun-connectrpc/src/db/schema.ts +15 -64
  62. package/templates/variants/bun-connectrpc/src/index.ts +76 -176
  63. package/templates/variants/bun-connectrpc/src/temporal/activities.ts +14 -0
  64. package/templates/variants/bun-connectrpc/src/temporal/worker.ts +38 -0
  65. package/templates/variants/bun-connectrpc/src/temporal/workflows.ts +10 -0
  66. package/templates/variants/bun-connectrpc/src/waitlist/service.ts +172 -0
  67. package/templates/variants/bun-connectrpc/src/waitlist/types.ts +45 -0
  68. package/templates/variants/bun-connectrpc/test/app.test.ts +4 -4
  69. package/templates/variants/bun-connectrpc/test/waitlist.integration.test.ts +71 -0
  70. package/templates/variants/bun-hono/Makefile +14 -8
  71. package/templates/variants/bun-hono/migrations/0000_init.sql +12 -55
  72. package/templates/variants/bun-hono/package.json +12 -5
  73. package/templates/variants/bun-hono/scripts/migrate.ts +4 -1
  74. package/templates/variants/bun-hono/src/auth.ts +181 -0
  75. package/templates/variants/bun-hono/src/db/repository.ts +68 -421
  76. package/templates/variants/bun-hono/src/db/schema.ts +15 -64
  77. package/templates/variants/bun-hono/src/index.ts +65 -180
  78. package/templates/variants/bun-hono/src/temporal/activities.ts +14 -0
  79. package/templates/variants/bun-hono/src/temporal/worker.ts +38 -0
  80. package/templates/variants/bun-hono/src/temporal/workflows.ts +10 -0
  81. package/templates/variants/bun-hono/src/waitlist/service.ts +166 -0
  82. package/templates/variants/bun-hono/src/waitlist/types.ts +50 -0
  83. package/templates/variants/bun-hono/test/app.test.ts +72 -41
  84. package/templates/variants/bun-hono/test/waitlist.integration.test.ts +102 -0
  85. package/templates/variants/go-chi/Makefile +27 -11
  86. package/templates/variants/go-chi/atlas.hcl +8 -0
  87. package/templates/variants/go-chi/cmd/server/main.go +21 -10
  88. package/templates/variants/go-chi/go.mod +1 -3
  89. package/templates/variants/go-chi/internal/app/service.go +202 -685
  90. package/templates/variants/go-chi/internal/auth/middleware.go +289 -0
  91. package/templates/variants/go-chi/internal/auth/middleware_test.go +38 -0
  92. package/templates/variants/go-chi/internal/config/config.go +27 -11
  93. package/templates/variants/go-chi/internal/httpapi/routes.go +78 -157
  94. package/templates/variants/go-chi/internal/httpapi/waitlist_integration_test.go +199 -0
  95. package/templates/variants/go-chi/internal/temporal/activities.go +27 -0
  96. package/templates/variants/go-chi/internal/temporal/worker.go +42 -0
  97. package/templates/variants/go-chi/internal/temporal/workflows.go +18 -0
  98. package/templates/variants/go-chi/migrations/0000_init.sql +12 -55
  99. package/templates/variants/go-chi/migrations/atlas.sum +2 -0
  100. package/templates/variants/go-chi/package.json +7 -1
  101. package/templates/variants/go-connectrpc/Makefile +26 -9
  102. package/templates/variants/go-connectrpc/atlas.hcl +8 -0
  103. package/templates/variants/go-connectrpc/buf.gen.yaml +2 -2
  104. package/templates/variants/go-connectrpc/cmd/server/main.go +23 -12
  105. package/templates/variants/go-connectrpc/gen/waitlist/v1/waitlist.pb.go +960 -0
  106. package/templates/variants/go-connectrpc/gen/waitlist/v1/waitlistv1connect/waitlist.connect.go +283 -0
  107. package/templates/variants/go-connectrpc/go.mod +1 -1
  108. package/templates/variants/go-connectrpc/internal/app/service.go +202 -685
  109. package/templates/variants/go-connectrpc/internal/auth/middleware.go +289 -0
  110. package/templates/variants/go-connectrpc/internal/auth/middleware_test.go +38 -0
  111. package/templates/variants/go-connectrpc/internal/config/config.go +27 -11
  112. package/templates/variants/go-connectrpc/internal/connectapi/handler.go +78 -201
  113. package/templates/variants/go-connectrpc/internal/connectapi/waitlist_integration_test.go +122 -0
  114. package/templates/variants/go-connectrpc/internal/httpapi/routes.go +147 -9
  115. package/templates/variants/go-connectrpc/internal/temporal/activities.go +27 -0
  116. package/templates/variants/go-connectrpc/internal/temporal/worker.go +42 -0
  117. package/templates/variants/go-connectrpc/internal/temporal/workflows.go +18 -0
  118. package/templates/variants/go-connectrpc/migrations/0000_init.sql +12 -55
  119. package/templates/variants/go-connectrpc/migrations/atlas.sum +2 -0
  120. package/templates/variants/go-connectrpc/package.json +7 -1
  121. package/templates/variants/go-connectrpc/protos/waitlist/v1/waitlist.proto +93 -0
  122. package/templates/root/.github/workflows/buf-publish.yml +0 -19
  123. package/templates/root/.github/workflows/ci.yml +0 -26
  124. package/templates/root/.github/workflows/deploy.yml +0 -22
  125. package/templates/root/Dockerfile +0 -23
  126. package/templates/root/README.md +0 -69
  127. package/templates/root/buf.gen.yaml +0 -10
  128. package/templates/root/buf.yaml +0 -9
  129. package/templates/root/cmd/server/main.go +0 -44
  130. package/templates/root/gen/dns/v1/dns.pb.go +0 -623
  131. package/templates/root/gen/dns/v1/dnsv1connect/dns.connect.go +0 -192
  132. package/templates/root/go.mod +0 -10
  133. package/templates/root/internal/app/service.go +0 -152
  134. package/templates/root/internal/app/token_source.go +0 -50
  135. package/templates/root/internal/cloudflare/client.go +0 -160
  136. package/templates/root/internal/config/config.go +0 -55
  137. package/templates/root/internal/connectapi/handler.go +0 -79
  138. package/templates/root/internal/httpapi/routes.go +0 -93
  139. package/templates/root/internal/vault/client.go +0 -148
  140. package/templates/root/package.json +0 -12
  141. package/templates/root/protos/dns/v1/dns.proto +0 -58
  142. package/templates/root/scripts/cloudrun/bootstrap.ts +0 -65
  143. package/templates/root/scripts/cloudrun/config.ts +0 -50
  144. package/templates/root/scripts/cloudrun/deploy.ts +0 -41
  145. package/templates/root/scripts/cloudrun/lib.ts +0 -244
  146. package/templates/root/service.yaml +0 -50
  147. package/templates/root/test/go.test.ts +0 -19
  148. package/templates/shared/scripts/cloudrun/integrations.ts +0 -111
  149. package/templates/variants/bun-connectrpc/gen/protos/chat/v1/chat_pb.ts +0 -1078
  150. package/templates/variants/bun-connectrpc/protos/chat/v1/chat.proto +0 -228
  151. package/templates/variants/bun-connectrpc/src/chat/service.ts +0 -384
  152. package/templates/variants/bun-connectrpc/src/chat/types.ts +0 -142
  153. package/templates/variants/bun-connectrpc/src/storage.ts +0 -72
  154. package/templates/variants/bun-connectrpc/src/webhooks.ts +0 -35
  155. package/templates/variants/bun-connectrpc/test/list-messages.integration.test.ts +0 -182
  156. package/templates/variants/bun-hono/src/chat/service.ts +0 -384
  157. package/templates/variants/bun-hono/src/chat/types.ts +0 -142
  158. package/templates/variants/bun-hono/src/storage.ts +0 -72
  159. package/templates/variants/bun-hono/src/webhooks.ts +0 -35
  160. package/templates/variants/bun-hono/test/list-messages.integration.test.ts +0 -256
  161. package/templates/variants/go-chi/buf.gen.yaml +0 -12
  162. package/templates/variants/go-chi/buf.yaml +0 -9
  163. package/templates/variants/go-chi/cmd/migrate/main.go +0 -101
  164. package/templates/variants/go-chi/internal/httpapi/list_messages_integration_test.go +0 -298
  165. package/templates/variants/go-chi/protos/chat/v1/chat.proto +0 -219
  166. package/templates/variants/go-connectrpc/cmd/migrate/main.go +0 -101
  167. package/templates/variants/go-connectrpc/gen/chat/v1/chat.pb.go +0 -2512
  168. package/templates/variants/go-connectrpc/gen/chat/v1/chatv1connect/chat.connect.go +0 -571
  169. package/templates/variants/go-connectrpc/internal/connectapi/list_messages_integration_test.go +0 -216
  170. package/templates/variants/go-connectrpc/protos/chat/v1/chat.proto +0 -232
  171. /package/bin/{create-svc.mjs → service.mjs} +0 -0
@@ -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
+ }