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,228 +0,0 @@
|
|
|
1
|
-
syntax = "proto3";
|
|
2
|
-
|
|
3
|
-
package chat.v1;
|
|
4
|
-
|
|
5
|
-
option go_package = "{{MODULE_PATH}}/gen/chat/v1;chatv1";
|
|
6
|
-
|
|
7
|
-
service ChatService {
|
|
8
|
-
rpc CreateUser(CreateUserRequest) returns (CreateUserResponse) {}
|
|
9
|
-
rpc GetUser(GetUserRequest) returns (GetUserResponse) {}
|
|
10
|
-
rpc GetUserByUsername(GetUserByUsernameRequest) returns (GetUserResponse) {}
|
|
11
|
-
rpc CreateConversation(CreateConversationRequest) returns (CreateConversationResponse) {}
|
|
12
|
-
rpc GetConversation(GetConversationRequest) returns (GetConversationResponse) {}
|
|
13
|
-
rpc UpdateConversation(UpdateConversationRequest) returns (UpdateConversationResponse) {}
|
|
14
|
-
rpc DeleteConversation(DeleteConversationRequest) returns (DeleteConversationResponse) {}
|
|
15
|
-
rpc AddConversationParticipant(AddConversationParticipantRequest) returns (AddConversationParticipantResponse) {}
|
|
16
|
-
rpc RemoveConversationParticipant(RemoveConversationParticipantRequest) returns (RemoveConversationParticipantResponse) {}
|
|
17
|
-
rpc ListMessages(ListMessagesRequest) returns (ListMessagesResponse) {}
|
|
18
|
-
rpc CreateMessage(CreateMessageRequest) returns (CreateMessageResponse) {}
|
|
19
|
-
rpc UpdateMessage(UpdateMessageRequest) returns (UpdateMessageResponse) {}
|
|
20
|
-
rpc DeleteMessage(DeleteMessageRequest) returns (DeleteMessageResponse) {}
|
|
21
|
-
rpc CreateAttachmentUpload(CreateAttachmentUploadRequest) returns (CreateAttachmentUploadResponse) {}
|
|
22
|
-
rpc FinalizeAttachment(FinalizeAttachmentRequest) returns (FinalizeAttachmentResponse) {}
|
|
23
|
-
rpc GetAttachment(GetAttachmentRequest) returns (GetAttachmentResponse) {}
|
|
24
|
-
rpc DeleteAttachment(DeleteAttachmentRequest) returns (DeleteAttachmentResponse) {}
|
|
25
|
-
}
|
|
26
|
-
|
|
27
|
-
message User {
|
|
28
|
-
string id = 1;
|
|
29
|
-
string username = 2;
|
|
30
|
-
string display_name = 3;
|
|
31
|
-
string created_at = 4;
|
|
32
|
-
string updated_at = 5;
|
|
33
|
-
}
|
|
34
|
-
|
|
35
|
-
message Conversation {
|
|
36
|
-
string id = 1;
|
|
37
|
-
string title = 2;
|
|
38
|
-
string created_by_user_id = 3;
|
|
39
|
-
repeated User participants = 4;
|
|
40
|
-
string created_at = 5;
|
|
41
|
-
string updated_at = 6;
|
|
42
|
-
}
|
|
43
|
-
|
|
44
|
-
message Message {
|
|
45
|
-
string id = 1;
|
|
46
|
-
string conversation_id = 2;
|
|
47
|
-
string user_id = 3;
|
|
48
|
-
string body = 4;
|
|
49
|
-
string edited_at = 5;
|
|
50
|
-
string created_at = 6;
|
|
51
|
-
string updated_at = 7;
|
|
52
|
-
repeated MessageAttachment attachments = 8;
|
|
53
|
-
}
|
|
54
|
-
|
|
55
|
-
message MessageAttachment {
|
|
56
|
-
string id = 1;
|
|
57
|
-
string filename = 2;
|
|
58
|
-
string content_type = 3;
|
|
59
|
-
int64 byte_size = 4;
|
|
60
|
-
string status = 5;
|
|
61
|
-
string public_url = 6;
|
|
62
|
-
}
|
|
63
|
-
|
|
64
|
-
message Attachment {
|
|
65
|
-
string id = 1;
|
|
66
|
-
string conversation_id = 2;
|
|
67
|
-
string message_id = 3;
|
|
68
|
-
string uploaded_by_user_id = 4;
|
|
69
|
-
string storage_bucket = 5;
|
|
70
|
-
string storage_key = 6;
|
|
71
|
-
string content_type = 7;
|
|
72
|
-
int64 byte_size = 8;
|
|
73
|
-
string filename = 9;
|
|
74
|
-
string status = 10;
|
|
75
|
-
string public_url = 11;
|
|
76
|
-
string created_at = 12;
|
|
77
|
-
string updated_at = 13;
|
|
78
|
-
}
|
|
79
|
-
|
|
80
|
-
message UploadTarget {
|
|
81
|
-
string method = 1;
|
|
82
|
-
string url = 2;
|
|
83
|
-
map<string, string> headers = 3;
|
|
84
|
-
}
|
|
85
|
-
|
|
86
|
-
message CreateUserRequest {
|
|
87
|
-
string username = 1;
|
|
88
|
-
string display_name = 2;
|
|
89
|
-
}
|
|
90
|
-
|
|
91
|
-
message CreateUserResponse {
|
|
92
|
-
User user = 1;
|
|
93
|
-
}
|
|
94
|
-
|
|
95
|
-
message GetUserRequest {
|
|
96
|
-
string user_id = 1;
|
|
97
|
-
}
|
|
98
|
-
|
|
99
|
-
message GetUserByUsernameRequest {
|
|
100
|
-
string username = 1;
|
|
101
|
-
}
|
|
102
|
-
|
|
103
|
-
message GetUserResponse {
|
|
104
|
-
User user = 1;
|
|
105
|
-
}
|
|
106
|
-
|
|
107
|
-
message CreateConversationRequest {
|
|
108
|
-
string created_by_user_id = 1;
|
|
109
|
-
string title = 2;
|
|
110
|
-
repeated string participant_user_ids = 3;
|
|
111
|
-
}
|
|
112
|
-
|
|
113
|
-
message CreateConversationResponse {
|
|
114
|
-
Conversation conversation = 1;
|
|
115
|
-
}
|
|
116
|
-
|
|
117
|
-
message GetConversationRequest {
|
|
118
|
-
string conversation_id = 1;
|
|
119
|
-
}
|
|
120
|
-
|
|
121
|
-
message GetConversationResponse {
|
|
122
|
-
Conversation conversation = 1;
|
|
123
|
-
}
|
|
124
|
-
|
|
125
|
-
message UpdateConversationRequest {
|
|
126
|
-
string conversation_id = 1;
|
|
127
|
-
string title = 2;
|
|
128
|
-
}
|
|
129
|
-
|
|
130
|
-
message UpdateConversationResponse {
|
|
131
|
-
Conversation conversation = 1;
|
|
132
|
-
}
|
|
133
|
-
|
|
134
|
-
message DeleteConversationRequest {
|
|
135
|
-
string conversation_id = 1;
|
|
136
|
-
}
|
|
137
|
-
|
|
138
|
-
message DeleteConversationResponse {}
|
|
139
|
-
|
|
140
|
-
message AddConversationParticipantRequest {
|
|
141
|
-
string conversation_id = 1;
|
|
142
|
-
string user_id = 2;
|
|
143
|
-
}
|
|
144
|
-
|
|
145
|
-
message AddConversationParticipantResponse {
|
|
146
|
-
Conversation conversation = 1;
|
|
147
|
-
}
|
|
148
|
-
|
|
149
|
-
message RemoveConversationParticipantRequest {
|
|
150
|
-
string conversation_id = 1;
|
|
151
|
-
string user_id = 2;
|
|
152
|
-
}
|
|
153
|
-
|
|
154
|
-
message RemoveConversationParticipantResponse {}
|
|
155
|
-
|
|
156
|
-
message ListMessagesRequest {
|
|
157
|
-
string conversation_id = 1;
|
|
158
|
-
string cursor = 2;
|
|
159
|
-
int32 limit = 3;
|
|
160
|
-
}
|
|
161
|
-
|
|
162
|
-
message ListMessagesResponse {
|
|
163
|
-
repeated Message messages = 1;
|
|
164
|
-
string next_cursor = 2;
|
|
165
|
-
}
|
|
166
|
-
|
|
167
|
-
message CreateMessageRequest {
|
|
168
|
-
string conversation_id = 1;
|
|
169
|
-
string user_id = 2;
|
|
170
|
-
string body = 3;
|
|
171
|
-
}
|
|
172
|
-
|
|
173
|
-
message CreateMessageResponse {
|
|
174
|
-
Message message = 1;
|
|
175
|
-
}
|
|
176
|
-
|
|
177
|
-
message UpdateMessageRequest {
|
|
178
|
-
string conversation_id = 1;
|
|
179
|
-
string message_id = 2;
|
|
180
|
-
string body = 3;
|
|
181
|
-
}
|
|
182
|
-
|
|
183
|
-
message UpdateMessageResponse {
|
|
184
|
-
Message message = 1;
|
|
185
|
-
}
|
|
186
|
-
|
|
187
|
-
message DeleteMessageRequest {
|
|
188
|
-
string conversation_id = 1;
|
|
189
|
-
string message_id = 2;
|
|
190
|
-
}
|
|
191
|
-
|
|
192
|
-
message DeleteMessageResponse {}
|
|
193
|
-
|
|
194
|
-
message CreateAttachmentUploadRequest {
|
|
195
|
-
string conversation_id = 1;
|
|
196
|
-
string user_id = 2;
|
|
197
|
-
string filename = 3;
|
|
198
|
-
string content_type = 4;
|
|
199
|
-
int64 byte_size = 5;
|
|
200
|
-
}
|
|
201
|
-
|
|
202
|
-
message CreateAttachmentUploadResponse {
|
|
203
|
-
Attachment attachment = 1;
|
|
204
|
-
UploadTarget upload = 2;
|
|
205
|
-
}
|
|
206
|
-
|
|
207
|
-
message FinalizeAttachmentRequest {
|
|
208
|
-
string attachment_id = 1;
|
|
209
|
-
string message_id = 2;
|
|
210
|
-
}
|
|
211
|
-
|
|
212
|
-
message FinalizeAttachmentResponse {
|
|
213
|
-
Attachment attachment = 1;
|
|
214
|
-
}
|
|
215
|
-
|
|
216
|
-
message GetAttachmentRequest {
|
|
217
|
-
string attachment_id = 1;
|
|
218
|
-
}
|
|
219
|
-
|
|
220
|
-
message GetAttachmentResponse {
|
|
221
|
-
Attachment attachment = 1;
|
|
222
|
-
}
|
|
223
|
-
|
|
224
|
-
message DeleteAttachmentRequest {
|
|
225
|
-
string attachment_id = 1;
|
|
226
|
-
}
|
|
227
|
-
|
|
228
|
-
message DeleteAttachmentResponse {}
|
|
@@ -1,384 +0,0 @@
|
|
|
1
|
-
import type {
|
|
2
|
-
AttachmentObjectMetadata,
|
|
3
|
-
ChatAttachment,
|
|
4
|
-
ChatConversation,
|
|
5
|
-
ChatMessage,
|
|
6
|
-
ChatUser,
|
|
7
|
-
CreateAttachmentUploadInput,
|
|
8
|
-
CreateAttachmentUploadResult,
|
|
9
|
-
CreateConversationInput,
|
|
10
|
-
ListMessagesInput,
|
|
11
|
-
ListMessagesResult,
|
|
12
|
-
CreateMessageInput,
|
|
13
|
-
CreateUserInput,
|
|
14
|
-
FinalizeAttachmentInput,
|
|
15
|
-
UpdateConversationInput,
|
|
16
|
-
UpdateMessageInput,
|
|
17
|
-
WebhookProcessResult,
|
|
18
|
-
} from "./types";
|
|
19
|
-
import { createDb } from "../db/client";
|
|
20
|
-
import { ChatRepository } from "../db/repository";
|
|
21
|
-
import { createAttachmentStorage, type AttachmentStorage } from "../storage";
|
|
22
|
-
import { createWebhookAdapter, type WebhookAdapter } from "../webhooks";
|
|
23
|
-
|
|
24
|
-
export class AppError extends Error {
|
|
25
|
-
constructor(
|
|
26
|
-
readonly status: number,
|
|
27
|
-
readonly code: string,
|
|
28
|
-
message: string
|
|
29
|
-
) {
|
|
30
|
-
super(message);
|
|
31
|
-
}
|
|
32
|
-
}
|
|
33
|
-
|
|
34
|
-
export type ChatService = {
|
|
35
|
-
createUser(input: CreateUserInput): Promise<ChatUser>;
|
|
36
|
-
getUser(userId: string): Promise<ChatUser>;
|
|
37
|
-
getUserByUsername(username: string): Promise<ChatUser>;
|
|
38
|
-
createConversation(input: CreateConversationInput): Promise<ChatConversation>;
|
|
39
|
-
getConversation(conversationId: string): Promise<ChatConversation>;
|
|
40
|
-
updateConversation(conversationId: string, input: UpdateConversationInput): Promise<ChatConversation>;
|
|
41
|
-
deleteConversation(conversationId: string): Promise<void>;
|
|
42
|
-
addParticipant(conversationId: string, userId: string): Promise<ChatConversation>;
|
|
43
|
-
removeParticipant(conversationId: string, userId: string): Promise<void>;
|
|
44
|
-
listMessages(conversationId: string, input?: ListMessagesInput): Promise<ListMessagesResult>;
|
|
45
|
-
createMessage(conversationId: string, input: CreateMessageInput): Promise<ChatMessage>;
|
|
46
|
-
updateMessage(conversationId: string, messageId: string, input: UpdateMessageInput): Promise<ChatMessage>;
|
|
47
|
-
deleteMessage(conversationId: string, messageId: string): Promise<void>;
|
|
48
|
-
createAttachmentUpload(input: CreateAttachmentUploadInput): Promise<CreateAttachmentUploadResult>;
|
|
49
|
-
finalizeAttachment(attachmentId: string, input: FinalizeAttachmentInput): Promise<ChatAttachment>;
|
|
50
|
-
getAttachment(attachmentId: string): Promise<ChatAttachment>;
|
|
51
|
-
deleteAttachment(attachmentId: string): Promise<void>;
|
|
52
|
-
processWebhook(provider: string, headers: Headers, rawBody: string): Promise<WebhookProcessResult>;
|
|
53
|
-
};
|
|
54
|
-
|
|
55
|
-
export class DefaultChatService implements ChatService {
|
|
56
|
-
constructor(
|
|
57
|
-
private readonly repository: ChatRepository,
|
|
58
|
-
private readonly storage: AttachmentStorage,
|
|
59
|
-
private readonly webhookAdapter: WebhookAdapter
|
|
60
|
-
) {}
|
|
61
|
-
|
|
62
|
-
async createUser(input: CreateUserInput) {
|
|
63
|
-
const username = input.username.trim().toLowerCase();
|
|
64
|
-
if (!username) {
|
|
65
|
-
throw new AppError(400, "invalid_username", "username is required");
|
|
66
|
-
}
|
|
67
|
-
|
|
68
|
-
const existing = await this.repository.getUserByUsername(username);
|
|
69
|
-
if (existing) {
|
|
70
|
-
throw new AppError(409, "username_taken", `username ${username} already exists`);
|
|
71
|
-
}
|
|
72
|
-
|
|
73
|
-
return this.repository.createUser({
|
|
74
|
-
id: crypto.randomUUID(),
|
|
75
|
-
username,
|
|
76
|
-
displayName: normalizeNullableText(input.displayName),
|
|
77
|
-
});
|
|
78
|
-
}
|
|
79
|
-
|
|
80
|
-
async getUser(userId: string) {
|
|
81
|
-
return this.requireUser(userId);
|
|
82
|
-
}
|
|
83
|
-
|
|
84
|
-
async getUserByUsername(username: string) {
|
|
85
|
-
const normalized = username.trim().toLowerCase();
|
|
86
|
-
if (!normalized) {
|
|
87
|
-
throw new AppError(400, "invalid_username", "username is required");
|
|
88
|
-
}
|
|
89
|
-
|
|
90
|
-
const user = await this.repository.getUserByUsername(normalized);
|
|
91
|
-
if (!user) {
|
|
92
|
-
throw new AppError(404, "user_not_found", `user ${normalized} not found`);
|
|
93
|
-
}
|
|
94
|
-
return user;
|
|
95
|
-
}
|
|
96
|
-
|
|
97
|
-
async createConversation(input: CreateConversationInput) {
|
|
98
|
-
const createdBy = await this.requireUser(input.createdByUserId);
|
|
99
|
-
const participantUserIds = new Set([createdBy.id, ...(input.participantUserIds ?? []).map((value) => value.trim()).filter(Boolean)]);
|
|
100
|
-
await this.requireUsers(Array.from(participantUserIds));
|
|
101
|
-
|
|
102
|
-
return this.repository.createConversation({
|
|
103
|
-
id: crypto.randomUUID(),
|
|
104
|
-
createdByUserId: createdBy.id,
|
|
105
|
-
title: normalizeNullableText(input.title),
|
|
106
|
-
participantUserIds: Array.from(participantUserIds),
|
|
107
|
-
});
|
|
108
|
-
}
|
|
109
|
-
|
|
110
|
-
async getConversation(conversationId: string) {
|
|
111
|
-
return this.requireConversation(conversationId);
|
|
112
|
-
}
|
|
113
|
-
|
|
114
|
-
async updateConversation(conversationId: string, input: UpdateConversationInput) {
|
|
115
|
-
await this.requireConversation(conversationId);
|
|
116
|
-
return this.repository.updateConversation(conversationId, normalizeNullableText(input.title));
|
|
117
|
-
}
|
|
118
|
-
|
|
119
|
-
async deleteConversation(conversationId: string) {
|
|
120
|
-
await this.requireConversation(conversationId);
|
|
121
|
-
await this.repository.softDeleteConversation(conversationId);
|
|
122
|
-
}
|
|
123
|
-
|
|
124
|
-
async addParticipant(conversationId: string, userId: string) {
|
|
125
|
-
await this.requireConversation(conversationId);
|
|
126
|
-
const user = await this.requireUser(userId);
|
|
127
|
-
await this.repository.addParticipant(conversationId, user.id);
|
|
128
|
-
return this.requireConversation(conversationId);
|
|
129
|
-
}
|
|
130
|
-
|
|
131
|
-
async removeParticipant(conversationId: string, userId: string) {
|
|
132
|
-
const conversation = await this.requireConversation(conversationId);
|
|
133
|
-
const isParticipant = conversation.participants.some((participant) => participant.id === userId);
|
|
134
|
-
if (!isParticipant) {
|
|
135
|
-
throw new AppError(404, "participant_not_found", `user ${userId} is not a participant`);
|
|
136
|
-
}
|
|
137
|
-
await this.repository.removeParticipant(conversationId, userId);
|
|
138
|
-
}
|
|
139
|
-
|
|
140
|
-
async listMessages(conversationId: string, input: ListMessagesInput = {}) {
|
|
141
|
-
await this.requireConversation(conversationId);
|
|
142
|
-
const limit = normalizeMessagePageSize(input.limit);
|
|
143
|
-
const cursor = parseMessageCursor(input.cursor);
|
|
144
|
-
const result = await this.repository.listMessages(conversationId, { limit, cursor });
|
|
145
|
-
return {
|
|
146
|
-
messages: result.messages,
|
|
147
|
-
nextCursor: result.hasMore ? encodeMessageCursor(result.messages[result.messages.length - 1]!) : undefined,
|
|
148
|
-
};
|
|
149
|
-
}
|
|
150
|
-
|
|
151
|
-
async createMessage(conversationId: string, input: CreateMessageInput) {
|
|
152
|
-
const conversation = await this.requireConversation(conversationId);
|
|
153
|
-
const user = await this.requireUser(input.userId);
|
|
154
|
-
if (!conversation.participants.some((participant) => participant.id === user.id)) {
|
|
155
|
-
throw new AppError(409, "not_a_participant", `user ${user.id} is not a participant`);
|
|
156
|
-
}
|
|
157
|
-
|
|
158
|
-
const body = input.body.trim();
|
|
159
|
-
if (!body) {
|
|
160
|
-
throw new AppError(400, "invalid_body", "message body is required");
|
|
161
|
-
}
|
|
162
|
-
|
|
163
|
-
return this.repository.createMessage({
|
|
164
|
-
id: crypto.randomUUID(),
|
|
165
|
-
conversationId,
|
|
166
|
-
userId: user.id,
|
|
167
|
-
body,
|
|
168
|
-
});
|
|
169
|
-
}
|
|
170
|
-
|
|
171
|
-
async updateMessage(conversationId: string, messageId: string, input: UpdateMessageInput) {
|
|
172
|
-
await this.requireConversation(conversationId);
|
|
173
|
-
const existing = await this.repository.getMessageById(messageId);
|
|
174
|
-
if (!existing || existing.conversationId !== conversationId) {
|
|
175
|
-
throw new AppError(404, "message_not_found", `message ${messageId} not found`);
|
|
176
|
-
}
|
|
177
|
-
|
|
178
|
-
const body = input.body.trim();
|
|
179
|
-
if (!body) {
|
|
180
|
-
throw new AppError(400, "invalid_body", "message body is required");
|
|
181
|
-
}
|
|
182
|
-
|
|
183
|
-
return this.repository.updateMessage(messageId, body);
|
|
184
|
-
}
|
|
185
|
-
|
|
186
|
-
async deleteMessage(conversationId: string, messageId: string) {
|
|
187
|
-
await this.requireConversation(conversationId);
|
|
188
|
-
const existing = await this.repository.getMessageById(messageId);
|
|
189
|
-
if (!existing || existing.conversationId !== conversationId) {
|
|
190
|
-
throw new AppError(404, "message_not_found", `message ${messageId} not found`);
|
|
191
|
-
}
|
|
192
|
-
await this.repository.softDeleteMessage(messageId);
|
|
193
|
-
}
|
|
194
|
-
|
|
195
|
-
async createAttachmentUpload(input: CreateAttachmentUploadInput) {
|
|
196
|
-
if (!input.contentType.startsWith("image/")) {
|
|
197
|
-
throw new AppError(400, "invalid_content_type", "only image uploads are supported");
|
|
198
|
-
}
|
|
199
|
-
await this.requireConversation(input.conversationId);
|
|
200
|
-
const user = await this.requireUser(input.uploadedByUserId);
|
|
201
|
-
if (input.byteSize <= 0) {
|
|
202
|
-
throw new AppError(400, "invalid_byte_size", "byte_size must be positive");
|
|
203
|
-
}
|
|
204
|
-
|
|
205
|
-
const attachmentId = crypto.randomUUID();
|
|
206
|
-
const uploadTarget = await this.storage.createSignedUpload({
|
|
207
|
-
attachmentId,
|
|
208
|
-
conversationId: input.conversationId,
|
|
209
|
-
filename: input.filename,
|
|
210
|
-
contentType: input.contentType,
|
|
211
|
-
});
|
|
212
|
-
|
|
213
|
-
const attachment = await this.repository.createAttachment({
|
|
214
|
-
id: attachmentId,
|
|
215
|
-
conversationId: input.conversationId,
|
|
216
|
-
uploadedByUserId: user.id,
|
|
217
|
-
storageBucket: uploadTarget.bucket,
|
|
218
|
-
storageKey: uploadTarget.key,
|
|
219
|
-
contentType: input.contentType,
|
|
220
|
-
byteSize: input.byteSize,
|
|
221
|
-
filename: input.filename,
|
|
222
|
-
});
|
|
223
|
-
|
|
224
|
-
return {
|
|
225
|
-
attachment: { ...attachment, publicUrl: uploadTarget.publicUrl },
|
|
226
|
-
upload: uploadTarget.upload,
|
|
227
|
-
};
|
|
228
|
-
}
|
|
229
|
-
|
|
230
|
-
async finalizeAttachment(attachmentId: string, input: FinalizeAttachmentInput) {
|
|
231
|
-
const attachment = await this.requireAttachment(attachmentId);
|
|
232
|
-
let messageId: string | null = null;
|
|
233
|
-
if (input.messageId) {
|
|
234
|
-
const message = await this.repository.getMessageById(input.messageId);
|
|
235
|
-
if (!message || message.conversationId !== attachment.conversationId) {
|
|
236
|
-
throw new AppError(404, "message_not_found", `message ${input.messageId} not found`);
|
|
237
|
-
}
|
|
238
|
-
messageId = message.id;
|
|
239
|
-
}
|
|
240
|
-
|
|
241
|
-
const metadata = await this.storage.getObjectMetadata({
|
|
242
|
-
bucket: attachment.storageBucket,
|
|
243
|
-
key: attachment.storageKey,
|
|
244
|
-
});
|
|
245
|
-
this.validateAttachmentMetadata(attachment, metadata);
|
|
246
|
-
|
|
247
|
-
const finalized = await this.repository.finalizeAttachment(attachmentId, {
|
|
248
|
-
messageId,
|
|
249
|
-
contentType: metadata.contentType,
|
|
250
|
-
byteSize: metadata.byteSize,
|
|
251
|
-
});
|
|
252
|
-
return { ...finalized, publicUrl: metadata.publicUrl };
|
|
253
|
-
}
|
|
254
|
-
|
|
255
|
-
async getAttachment(attachmentId: string) {
|
|
256
|
-
return this.requireAttachment(attachmentId);
|
|
257
|
-
}
|
|
258
|
-
|
|
259
|
-
async deleteAttachment(attachmentId: string) {
|
|
260
|
-
await this.requireAttachment(attachmentId);
|
|
261
|
-
await this.repository.softDeleteAttachment(attachmentId);
|
|
262
|
-
}
|
|
263
|
-
|
|
264
|
-
async processWebhook(provider: string, headers: Headers, rawBody: string): Promise<WebhookProcessResult> {
|
|
265
|
-
const event = await this.webhookAdapter.normalize(provider, headers, rawBody);
|
|
266
|
-
const existing = await this.repository.getWebhookEvent(event.provider, event.externalEventId);
|
|
267
|
-
if (existing) {
|
|
268
|
-
return { event: existing, duplicate: true };
|
|
269
|
-
}
|
|
270
|
-
|
|
271
|
-
const created = await this.repository.createWebhookEvent(event);
|
|
272
|
-
const status = event.signatureValid ? "processed" : "failed";
|
|
273
|
-
const updated = await this.repository.markWebhookEventProcessed(created.id, status);
|
|
274
|
-
return { event: updated, duplicate: false };
|
|
275
|
-
}
|
|
276
|
-
|
|
277
|
-
private async requireUser(userId: string) {
|
|
278
|
-
const user = await this.repository.getUserById(userId);
|
|
279
|
-
if (!user) {
|
|
280
|
-
throw new AppError(404, "user_not_found", `user ${userId} not found`);
|
|
281
|
-
}
|
|
282
|
-
return user;
|
|
283
|
-
}
|
|
284
|
-
|
|
285
|
-
private async requireUsers(userIds: string[]) {
|
|
286
|
-
const users = await this.repository.listUsersByIds(userIds);
|
|
287
|
-
if (users.length !== userIds.length) {
|
|
288
|
-
throw new AppError(404, "user_not_found", "one or more users do not exist");
|
|
289
|
-
}
|
|
290
|
-
return users;
|
|
291
|
-
}
|
|
292
|
-
|
|
293
|
-
private async requireConversation(conversationId: string) {
|
|
294
|
-
const conversation = await this.repository.getConversationById(conversationId);
|
|
295
|
-
if (!conversation) {
|
|
296
|
-
throw new AppError(404, "conversation_not_found", `conversation ${conversationId} not found`);
|
|
297
|
-
}
|
|
298
|
-
return conversation;
|
|
299
|
-
}
|
|
300
|
-
|
|
301
|
-
private async requireAttachment(attachmentId: string) {
|
|
302
|
-
const attachment = await this.repository.getAttachmentById(attachmentId);
|
|
303
|
-
if (!attachment) {
|
|
304
|
-
throw new AppError(404, "attachment_not_found", `attachment ${attachmentId} not found`);
|
|
305
|
-
}
|
|
306
|
-
const metadata = await this.storage.getObjectMetadata({
|
|
307
|
-
bucket: attachment.storageBucket,
|
|
308
|
-
key: attachment.storageKey,
|
|
309
|
-
}).catch(() => null);
|
|
310
|
-
return {
|
|
311
|
-
...attachment,
|
|
312
|
-
publicUrl: metadata?.publicUrl ?? attachment.publicUrl,
|
|
313
|
-
};
|
|
314
|
-
}
|
|
315
|
-
|
|
316
|
-
private validateAttachmentMetadata(attachment: ChatAttachment, metadata: AttachmentObjectMetadata) {
|
|
317
|
-
if (metadata.contentType !== attachment.contentType) {
|
|
318
|
-
throw new AppError(409, "content_type_mismatch", "uploaded object content_type does not match pending attachment");
|
|
319
|
-
}
|
|
320
|
-
if (metadata.byteSize !== attachment.byteSize) {
|
|
321
|
-
throw new AppError(409, "byte_size_mismatch", "uploaded object byte_size does not match pending attachment");
|
|
322
|
-
}
|
|
323
|
-
}
|
|
324
|
-
}
|
|
325
|
-
|
|
326
|
-
export function createDefaultChatService() {
|
|
327
|
-
const db = createDb();
|
|
328
|
-
return new DefaultChatService(new ChatRepository(db), createAttachmentStorage(), createWebhookAdapter());
|
|
329
|
-
}
|
|
330
|
-
|
|
331
|
-
function normalizeNullableText(value: string | null | undefined) {
|
|
332
|
-
if (value === undefined) {
|
|
333
|
-
return undefined;
|
|
334
|
-
}
|
|
335
|
-
if (value === null) {
|
|
336
|
-
return null;
|
|
337
|
-
}
|
|
338
|
-
const trimmed = value.trim();
|
|
339
|
-
return trimmed || null;
|
|
340
|
-
}
|
|
341
|
-
|
|
342
|
-
const DEFAULT_MESSAGE_PAGE_SIZE = 50;
|
|
343
|
-
const MAX_MESSAGE_PAGE_SIZE = 100;
|
|
344
|
-
|
|
345
|
-
function normalizeMessagePageSize(limit: number | null | undefined) {
|
|
346
|
-
if (limit == null) {
|
|
347
|
-
return DEFAULT_MESSAGE_PAGE_SIZE;
|
|
348
|
-
}
|
|
349
|
-
if (!Number.isInteger(limit) || limit <= 0) {
|
|
350
|
-
throw new AppError(400, "invalid_limit", "limit must be a positive integer");
|
|
351
|
-
}
|
|
352
|
-
if (limit > MAX_MESSAGE_PAGE_SIZE) {
|
|
353
|
-
throw new AppError(400, "invalid_limit", `limit must be at most ${MAX_MESSAGE_PAGE_SIZE}`);
|
|
354
|
-
}
|
|
355
|
-
return limit;
|
|
356
|
-
}
|
|
357
|
-
|
|
358
|
-
type MessageCursor = {
|
|
359
|
-
createdAt: string;
|
|
360
|
-
id: string;
|
|
361
|
-
};
|
|
362
|
-
|
|
363
|
-
function parseMessageCursor(cursor: string | null | undefined): MessageCursor | undefined {
|
|
364
|
-
if (!cursor) {
|
|
365
|
-
return undefined;
|
|
366
|
-
}
|
|
367
|
-
|
|
368
|
-
try {
|
|
369
|
-
const value = JSON.parse(Buffer.from(cursor, "base64url").toString("utf8")) as Partial<MessageCursor>;
|
|
370
|
-
if (typeof value.createdAt !== "string" || typeof value.id !== "string" || !value.createdAt || !value.id) {
|
|
371
|
-
throw new Error("invalid cursor payload");
|
|
372
|
-
}
|
|
373
|
-
return {
|
|
374
|
-
createdAt: value.createdAt,
|
|
375
|
-
id: value.id,
|
|
376
|
-
};
|
|
377
|
-
} catch {
|
|
378
|
-
throw new AppError(400, "invalid_cursor", "cursor is invalid");
|
|
379
|
-
}
|
|
380
|
-
}
|
|
381
|
-
|
|
382
|
-
function encodeMessageCursor(message: { createdAt: string; id: string }) {
|
|
383
|
-
return Buffer.from(JSON.stringify({ createdAt: message.createdAt, id: message.id }), "utf8").toString("base64url");
|
|
384
|
-
}
|