create-svc 0.1.9 → 0.1.10
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 +130 -11
- package/package.json +9 -4
- package/src/cli.test.ts +29 -8
- package/src/cli.ts +103 -70
- package/src/naming.test.ts +4 -2
- package/src/naming.ts +9 -1
- package/src/neon.ts +10 -8
- package/src/post-scaffold.ts +7 -28
- package/src/profiles.ts +28 -0
- package/src/scaffold.test.ts +126 -15
- package/src/scaffold.ts +94 -23
- package/src/vault.test.ts +33 -9
- package/src/vault.ts +4 -3
- package/templates/shared/README.md +135 -24
- package/templates/shared/docker-compose.yml +19 -0
- package/templates/shared/scripts/cloudrun/bootstrap.ts +15 -42
- package/templates/shared/scripts/cloudrun/cleanup.ts +17 -31
- package/templates/shared/scripts/cloudrun/config.ts +14 -19
- package/templates/shared/scripts/cloudrun/deploy.ts +19 -10
- package/templates/shared/scripts/cloudrun/integrations.ts +111 -0
- package/templates/shared/scripts/cloudrun/lib.ts +88 -112
- package/templates/shared/scripts/cloudrun/neon.ts +82 -13
- package/templates/shared/service.yaml +44 -1
- package/templates/variants/bun-connectrpc/Dockerfile +1 -0
- package/templates/variants/bun-connectrpc/Makefile +4 -1
- package/templates/variants/bun-connectrpc/gen/protos/chat/v1/chat_pb.ts +1078 -0
- package/templates/variants/bun-connectrpc/migrations/0000_init.sql +63 -0
- package/templates/variants/bun-connectrpc/package.json +17 -0
- package/templates/variants/bun-connectrpc/protos/chat/v1/chat.proto +228 -0
- package/templates/variants/bun-connectrpc/scripts/codegen.ts +31 -1
- package/templates/variants/bun-connectrpc/scripts/migrate.ts +46 -0
- package/templates/variants/bun-connectrpc/src/chat/service.ts +384 -0
- package/templates/variants/bun-connectrpc/src/chat/types.ts +142 -0
- package/templates/variants/bun-connectrpc/src/db/client.ts +15 -0
- package/templates/variants/bun-connectrpc/src/db/repository.ts +479 -0
- package/templates/variants/bun-connectrpc/src/db/schema.ts +75 -0
- package/templates/variants/bun-connectrpc/src/index.ts +294 -22
- package/templates/variants/bun-connectrpc/src/storage.ts +72 -0
- package/templates/variants/bun-connectrpc/src/webhooks.ts +35 -0
- package/templates/variants/bun-connectrpc/test/app.test.ts +14 -13
- package/templates/variants/bun-connectrpc/test/list-messages.integration.test.ts +182 -0
- package/templates/variants/bun-connectrpc/tsconfig.json +2 -1
- package/templates/variants/bun-hono/Makefile +4 -1
- package/templates/variants/bun-hono/migrations/0000_init.sql +63 -0
- package/templates/variants/bun-hono/package.json +13 -0
- package/templates/variants/bun-hono/scripts/migrate.ts +46 -0
- package/templates/variants/bun-hono/src/chat/service.ts +384 -0
- package/templates/variants/bun-hono/src/chat/types.ts +142 -0
- package/templates/variants/bun-hono/src/db/client.ts +15 -0
- package/templates/variants/bun-hono/src/db/repository.ts +479 -0
- package/templates/variants/bun-hono/src/db/schema.ts +75 -0
- package/templates/variants/bun-hono/src/index.ts +254 -8
- package/templates/variants/bun-hono/src/storage.ts +72 -0
- package/templates/variants/bun-hono/src/webhooks.ts +35 -0
- package/templates/variants/bun-hono/test/app.test.ts +60 -6
- package/templates/variants/bun-hono/test/list-messages.integration.test.ts +256 -0
- package/templates/variants/bun-hono/tsconfig.json +1 -0
- package/templates/variants/go-chi/Makefile +6 -2
- package/templates/variants/go-chi/buf.gen.yaml +2 -0
- package/templates/variants/go-chi/cmd/migrate/main.go +101 -0
- package/templates/variants/go-chi/cmd/server/main.go +16 -15
- package/templates/variants/go-chi/go.mod +3 -0
- package/templates/variants/go-chi/internal/app/service.go +763 -71
- package/templates/variants/go-chi/internal/config/config.go +22 -7
- package/templates/variants/go-chi/internal/httpapi/list_messages_integration_test.go +298 -0
- package/templates/variants/go-chi/internal/httpapi/routes.go +245 -43
- package/templates/variants/go-chi/migrations/0000_init.sql +63 -0
- package/templates/variants/go-chi/protos/chat/v1/chat.proto +219 -0
- package/templates/variants/go-chi/test/go.test.ts +4 -1
- package/templates/variants/go-connectrpc/Makefile +6 -2
- package/templates/variants/go-connectrpc/buf.gen.yaml +2 -0
- package/templates/variants/go-connectrpc/cmd/migrate/main.go +101 -0
- package/templates/variants/go-connectrpc/cmd/server/main.go +35 -11
- package/templates/variants/go-connectrpc/gen/chat/v1/chat.pb.go +2512 -0
- package/templates/variants/go-connectrpc/gen/chat/v1/chatv1connect/chat.connect.go +571 -0
- package/templates/variants/go-connectrpc/go.mod +4 -0
- package/templates/variants/go-connectrpc/internal/app/service.go +763 -71
- package/templates/variants/go-connectrpc/internal/config/config.go +22 -7
- package/templates/variants/go-connectrpc/internal/connectapi/handler.go +254 -42
- package/templates/variants/go-connectrpc/internal/connectapi/list_messages_integration_test.go +216 -0
- package/templates/variants/go-connectrpc/internal/httpapi/routes.go +41 -56
- package/templates/variants/go-connectrpc/migrations/0000_init.sql +63 -0
- package/templates/variants/go-connectrpc/protos/chat/v1/chat.proto +232 -0
- package/templates/shared/.env.example +0 -10
- package/templates/variants/go-chi/gen/dns/v1/dns.pb.go +0 -623
- package/templates/variants/go-chi/gen/dns/v1/dnsv1connect/dns.connect.go +0 -192
- package/templates/variants/go-chi/internal/connectapi/handler.go +0 -79
- package/templates/variants/go-chi/protos/dns/v1/dns.proto +0 -58
- package/templates/variants/go-connectrpc/gen/dns/v1/dns.pb.go +0 -623
- package/templates/variants/go-connectrpc/gen/dns/v1/dnsv1connect/dns.connect.go +0 -192
- package/templates/variants/go-connectrpc/protos/dns/v1/dns.proto +0 -58
|
@@ -0,0 +1,256 @@
|
|
|
1
|
+
import { afterEach, beforeEach, expect, test } from "bun:test";
|
|
2
|
+
import { SQL } from "bun";
|
|
3
|
+
import { createApp } from "../src/index";
|
|
4
|
+
import { DefaultChatService } from "../src/chat/service";
|
|
5
|
+
import { ChatRepository } from "../src/db/repository";
|
|
6
|
+
import { createDb } from "../src/db/client";
|
|
7
|
+
import type { AttachmentObjectMetadata, AttachmentUploadTarget } from "../src/chat/types";
|
|
8
|
+
import type { AttachmentStorage } from "../src/storage";
|
|
9
|
+
|
|
10
|
+
const databaseUrl = Bun.env.DATABASE_URL?.trim();
|
|
11
|
+
const integrationTest = databaseUrl ? test : test.skip;
|
|
12
|
+
|
|
13
|
+
let sql: SQL | null = null;
|
|
14
|
+
|
|
15
|
+
beforeEach(async () => {
|
|
16
|
+
if (!databaseUrl) {
|
|
17
|
+
return;
|
|
18
|
+
}
|
|
19
|
+
sql = new SQL(databaseUrl);
|
|
20
|
+
await sql.unsafe(`
|
|
21
|
+
truncate table
|
|
22
|
+
webhook_events,
|
|
23
|
+
attachments,
|
|
24
|
+
messages,
|
|
25
|
+
conversation_participants,
|
|
26
|
+
conversations,
|
|
27
|
+
users
|
|
28
|
+
restart identity cascade
|
|
29
|
+
`);
|
|
30
|
+
});
|
|
31
|
+
|
|
32
|
+
afterEach(async () => {
|
|
33
|
+
await sql?.end();
|
|
34
|
+
sql = null;
|
|
35
|
+
});
|
|
36
|
+
|
|
37
|
+
integrationTest("list messages returns newest-first pages with attachment metadata", async () => {
|
|
38
|
+
Bun.env.ATTACHMENT_PUBLIC_BASE_URL = "https://storage.test";
|
|
39
|
+
const storage = new FakeAttachmentStorage();
|
|
40
|
+
const app = createApp(new DefaultChatService(new ChatRepository(createDb(databaseUrl)), storage, new NoopWebhookAdapter()));
|
|
41
|
+
|
|
42
|
+
const user = await createUser(app);
|
|
43
|
+
const conversation = await createConversation(app, user.id);
|
|
44
|
+
const messageIds: string[] = [];
|
|
45
|
+
|
|
46
|
+
for (let index = 1; index <= 55; index += 1) {
|
|
47
|
+
const message = await createMessage(app, conversation.id, user.id, `message-${index}`);
|
|
48
|
+
messageIds.push(message.id);
|
|
49
|
+
}
|
|
50
|
+
|
|
51
|
+
await rewriteMessageTimestamps(messageIds);
|
|
52
|
+
|
|
53
|
+
const uploadResult = await requestJson(app, "/v1/attachments/uploads", {
|
|
54
|
+
method: "POST",
|
|
55
|
+
body: {
|
|
56
|
+
conversation_id: conversation.id,
|
|
57
|
+
user_id: user.id,
|
|
58
|
+
filename: "photo.png",
|
|
59
|
+
content_type: "image/png",
|
|
60
|
+
byte_size: 1234,
|
|
61
|
+
},
|
|
62
|
+
expectedStatus: 201,
|
|
63
|
+
});
|
|
64
|
+
const attachment = uploadResult.result.attachment as {
|
|
65
|
+
id: string;
|
|
66
|
+
publicUrl: string;
|
|
67
|
+
filename: string;
|
|
68
|
+
contentType: string;
|
|
69
|
+
byteSize: number;
|
|
70
|
+
status: string;
|
|
71
|
+
};
|
|
72
|
+
|
|
73
|
+
storage.setObjectMetadataFromUrl(uploadResult.result.attachment.publicUrl, {
|
|
74
|
+
contentType: "image/png",
|
|
75
|
+
byteSize: 1234,
|
|
76
|
+
publicUrl: uploadResult.result.attachment.publicUrl,
|
|
77
|
+
});
|
|
78
|
+
|
|
79
|
+
await requestJson(app, `/v1/attachments/${attachment.id}/finalize`, {
|
|
80
|
+
method: "POST",
|
|
81
|
+
body: {
|
|
82
|
+
message_id: messageIds[54],
|
|
83
|
+
},
|
|
84
|
+
});
|
|
85
|
+
|
|
86
|
+
const firstPage = await requestJson(app, `/v1/conversations/${conversation.id}/messages`);
|
|
87
|
+
expect(firstPage.messages).toHaveLength(50);
|
|
88
|
+
expect(firstPage.messages[0].body).toBe("message-55");
|
|
89
|
+
expect(firstPage.messages[49].body).toBe("message-6");
|
|
90
|
+
expect(firstPage.next_cursor).toBeString();
|
|
91
|
+
expect(firstPage.messages[0].attachments).toEqual([
|
|
92
|
+
{
|
|
93
|
+
id: attachment.id,
|
|
94
|
+
filename: "photo.png",
|
|
95
|
+
content_type: "image/png",
|
|
96
|
+
byte_size: 1234,
|
|
97
|
+
status: "ready",
|
|
98
|
+
public_url: attachment.publicUrl,
|
|
99
|
+
},
|
|
100
|
+
]);
|
|
101
|
+
|
|
102
|
+
const secondPage = await requestJson(
|
|
103
|
+
app,
|
|
104
|
+
`/v1/conversations/${conversation.id}/messages?cursor=${encodeURIComponent(firstPage.next_cursor)}`
|
|
105
|
+
);
|
|
106
|
+
expect(secondPage.messages.map((message: { body: string }) => message.body)).toEqual([
|
|
107
|
+
"message-5",
|
|
108
|
+
"message-4",
|
|
109
|
+
"message-3",
|
|
110
|
+
"message-2",
|
|
111
|
+
"message-1",
|
|
112
|
+
]);
|
|
113
|
+
expect(secondPage.next_cursor).toBeUndefined();
|
|
114
|
+
|
|
115
|
+
const invalidLimit = await app.request(`/v1/conversations/${conversation.id}/messages?limit=0`);
|
|
116
|
+
expect(invalidLimit.status).toBe(400);
|
|
117
|
+
expect(await invalidLimit.json()).toEqual({
|
|
118
|
+
error: "limit must be a positive integer",
|
|
119
|
+
code: "invalid_limit",
|
|
120
|
+
});
|
|
121
|
+
|
|
122
|
+
const tooLargeLimit = await app.request(`/v1/conversations/${conversation.id}/messages?limit=101`);
|
|
123
|
+
expect(tooLargeLimit.status).toBe(400);
|
|
124
|
+
expect(await tooLargeLimit.json()).toEqual({
|
|
125
|
+
error: "limit must be at most 100",
|
|
126
|
+
code: "invalid_limit",
|
|
127
|
+
});
|
|
128
|
+
});
|
|
129
|
+
|
|
130
|
+
async function createUser(app: ReturnType<typeof createApp>) {
|
|
131
|
+
const response = await requestJson(app, "/v1/users", {
|
|
132
|
+
method: "POST",
|
|
133
|
+
body: {
|
|
134
|
+
username: "alice",
|
|
135
|
+
display_name: "Alice",
|
|
136
|
+
},
|
|
137
|
+
expectedStatus: 201,
|
|
138
|
+
});
|
|
139
|
+
return response.user as { id: string };
|
|
140
|
+
}
|
|
141
|
+
|
|
142
|
+
async function createConversation(app: ReturnType<typeof createApp>, createdByUserId: string) {
|
|
143
|
+
const response = await requestJson(app, "/v1/conversations", {
|
|
144
|
+
method: "POST",
|
|
145
|
+
body: {
|
|
146
|
+
created_by_user_id: createdByUserId,
|
|
147
|
+
title: "General",
|
|
148
|
+
participant_user_ids: [createdByUserId],
|
|
149
|
+
},
|
|
150
|
+
expectedStatus: 201,
|
|
151
|
+
});
|
|
152
|
+
return response.conversation as { id: string };
|
|
153
|
+
}
|
|
154
|
+
|
|
155
|
+
async function createMessage(app: ReturnType<typeof createApp>, conversationId: string, userId: string, body: string) {
|
|
156
|
+
const response = await requestJson(app, `/v1/conversations/${conversationId}/messages`, {
|
|
157
|
+
method: "POST",
|
|
158
|
+
body: {
|
|
159
|
+
user_id: userId,
|
|
160
|
+
body,
|
|
161
|
+
},
|
|
162
|
+
expectedStatus: 201,
|
|
163
|
+
});
|
|
164
|
+
return response.message as { id: string };
|
|
165
|
+
}
|
|
166
|
+
|
|
167
|
+
async function requestJson(
|
|
168
|
+
app: ReturnType<typeof createApp>,
|
|
169
|
+
path: string,
|
|
170
|
+
input: {
|
|
171
|
+
method?: string;
|
|
172
|
+
body?: unknown;
|
|
173
|
+
expectedStatus?: number;
|
|
174
|
+
} = {}
|
|
175
|
+
) {
|
|
176
|
+
const response = await app.request(path, {
|
|
177
|
+
method: input.method ?? "GET",
|
|
178
|
+
headers: input.body ? { "Content-Type": "application/json" } : undefined,
|
|
179
|
+
body: input.body ? JSON.stringify(input.body) : undefined,
|
|
180
|
+
});
|
|
181
|
+
expect(response.status).toBe(input.expectedStatus ?? 200);
|
|
182
|
+
return response.json();
|
|
183
|
+
}
|
|
184
|
+
|
|
185
|
+
async function rewriteMessageTimestamps(messageIds: string[]) {
|
|
186
|
+
if (!sql) {
|
|
187
|
+
throw new Error("sql client not initialized");
|
|
188
|
+
}
|
|
189
|
+
|
|
190
|
+
const baseTime = Date.parse("2026-01-01T00:00:00.000Z");
|
|
191
|
+
for (const [index, messageId] of messageIds.entries()) {
|
|
192
|
+
const createdAt = new Date(baseTime + (index + 1) * 1000).toISOString();
|
|
193
|
+
await sql`
|
|
194
|
+
update messages
|
|
195
|
+
set created_at = ${createdAt}::timestamptz,
|
|
196
|
+
updated_at = ${createdAt}::timestamptz
|
|
197
|
+
where id = ${messageId}
|
|
198
|
+
`;
|
|
199
|
+
}
|
|
200
|
+
}
|
|
201
|
+
|
|
202
|
+
class FakeAttachmentStorage implements AttachmentStorage {
|
|
203
|
+
private readonly metadata = new Map<string, AttachmentObjectMetadata>();
|
|
204
|
+
|
|
205
|
+
async createSignedUpload(input: {
|
|
206
|
+
attachmentId: string;
|
|
207
|
+
conversationId: string;
|
|
208
|
+
filename: string;
|
|
209
|
+
contentType: string;
|
|
210
|
+
}): Promise<{ bucket: string; key: string; upload: AttachmentUploadTarget; publicUrl: string }> {
|
|
211
|
+
const bucket = "test-bucket";
|
|
212
|
+
const key = `attachments/${input.conversationId}/${input.attachmentId}/${input.filename}`;
|
|
213
|
+
const publicUrl = `https://storage.test/${key}`;
|
|
214
|
+
return {
|
|
215
|
+
bucket,
|
|
216
|
+
key,
|
|
217
|
+
upload: {
|
|
218
|
+
method: "PUT",
|
|
219
|
+
url: `https://uploads.test/${key}`,
|
|
220
|
+
headers: { "Content-Type": input.contentType },
|
|
221
|
+
},
|
|
222
|
+
publicUrl,
|
|
223
|
+
};
|
|
224
|
+
}
|
|
225
|
+
|
|
226
|
+
async getObjectMetadata(input: { bucket: string; key: string }) {
|
|
227
|
+
const metadata = this.metadata.get(`${input.bucket}/${input.key}`);
|
|
228
|
+
if (!metadata) {
|
|
229
|
+
throw new Error(`missing metadata for ${input.bucket}/${input.key}`);
|
|
230
|
+
}
|
|
231
|
+
return metadata;
|
|
232
|
+
}
|
|
233
|
+
|
|
234
|
+
setObjectMetadataFromUrl(publicUrl: string, input: Omit<AttachmentObjectMetadata, "bucket" | "key">) {
|
|
235
|
+
const [, bucketAndKey = ""] = publicUrl.split("https://storage.test/");
|
|
236
|
+
this.metadata.set(`test-bucket/${bucketAndKey}`, {
|
|
237
|
+
bucket: "test-bucket",
|
|
238
|
+
key: bucketAndKey,
|
|
239
|
+
contentType: input.contentType,
|
|
240
|
+
byteSize: input.byteSize,
|
|
241
|
+
publicUrl: input.publicUrl,
|
|
242
|
+
});
|
|
243
|
+
}
|
|
244
|
+
}
|
|
245
|
+
|
|
246
|
+
class NoopWebhookAdapter {
|
|
247
|
+
async normalize() {
|
|
248
|
+
return {
|
|
249
|
+
provider: "generic",
|
|
250
|
+
externalEventId: "evt_test",
|
|
251
|
+
eventType: "generic.event",
|
|
252
|
+
signatureValid: true,
|
|
253
|
+
payloadJson: "{}",
|
|
254
|
+
};
|
|
255
|
+
}
|
|
256
|
+
}
|
|
@@ -1,9 +1,13 @@
|
|
|
1
|
-
.PHONY: dev gen lint test bootstrap deploy cleanup
|
|
1
|
+
.PHONY: dev migrate gen lint test bootstrap deploy cleanup
|
|
2
2
|
|
|
3
3
|
CLOUDRUN := npx --no-install svc-cloudrun
|
|
4
|
+
WITH_ENV := set -a; [ ! -f .env.local ] || . ./.env.local; set +a;
|
|
4
5
|
|
|
5
6
|
dev:
|
|
6
|
-
go run ./cmd/server
|
|
7
|
+
@$(WITH_ENV) go run ./cmd/server
|
|
8
|
+
|
|
9
|
+
migrate:
|
|
10
|
+
@$(WITH_ENV) go run ./cmd/migrate
|
|
7
11
|
|
|
8
12
|
gen:
|
|
9
13
|
buf generate
|
|
@@ -0,0 +1,101 @@
|
|
|
1
|
+
package main
|
|
2
|
+
|
|
3
|
+
import (
|
|
4
|
+
"context"
|
|
5
|
+
"log"
|
|
6
|
+
"os"
|
|
7
|
+
"sort"
|
|
8
|
+
"strings"
|
|
9
|
+
"time"
|
|
10
|
+
|
|
11
|
+
_ "github.com/jackc/pgx/v5/stdlib"
|
|
12
|
+
"github.com/jmoiron/sqlx"
|
|
13
|
+
|
|
14
|
+
"{{MODULE_PATH}}/internal/app"
|
|
15
|
+
"{{MODULE_PATH}}/internal/config"
|
|
16
|
+
)
|
|
17
|
+
|
|
18
|
+
func main() {
|
|
19
|
+
cfg, err := config.Load()
|
|
20
|
+
if err != nil {
|
|
21
|
+
log.Fatal(err)
|
|
22
|
+
}
|
|
23
|
+
|
|
24
|
+
db, err := app.OpenDatabase(context.Background(), cfg.DatabaseURL)
|
|
25
|
+
if err != nil {
|
|
26
|
+
db, err = waitForDatabase(cfg.DatabaseURL, 30*time.Second)
|
|
27
|
+
if err != nil {
|
|
28
|
+
log.Fatal(err)
|
|
29
|
+
}
|
|
30
|
+
}
|
|
31
|
+
|
|
32
|
+
if _, err := db.ExecContext(context.Background(), `
|
|
33
|
+
create table if not exists schema_migrations (
|
|
34
|
+
version text primary key,
|
|
35
|
+
applied_at timestamptz not null default now()
|
|
36
|
+
)`); err != nil {
|
|
37
|
+
log.Fatal(err)
|
|
38
|
+
}
|
|
39
|
+
|
|
40
|
+
entries, err := os.ReadDir("migrations")
|
|
41
|
+
if err != nil {
|
|
42
|
+
log.Fatal(err)
|
|
43
|
+
}
|
|
44
|
+
|
|
45
|
+
versions := make([]string, 0, len(entries))
|
|
46
|
+
for _, entry := range entries {
|
|
47
|
+
if entry.IsDir() || !strings.HasSuffix(entry.Name(), ".sql") {
|
|
48
|
+
continue
|
|
49
|
+
}
|
|
50
|
+
versions = append(versions, entry.Name())
|
|
51
|
+
}
|
|
52
|
+
sort.Strings(versions)
|
|
53
|
+
|
|
54
|
+
for _, version := range versions {
|
|
55
|
+
var count int
|
|
56
|
+
if err := db.GetContext(context.Background(), &count, `select count(*) from schema_migrations where version = $1`, version); err != nil {
|
|
57
|
+
log.Fatal(err)
|
|
58
|
+
}
|
|
59
|
+
if count > 0 {
|
|
60
|
+
continue
|
|
61
|
+
}
|
|
62
|
+
|
|
63
|
+
sqlBytes, err := os.ReadFile("migrations/" + version)
|
|
64
|
+
if err != nil {
|
|
65
|
+
log.Fatal(err)
|
|
66
|
+
}
|
|
67
|
+
|
|
68
|
+
tx, err := db.BeginTxx(context.Background(), nil)
|
|
69
|
+
if err != nil {
|
|
70
|
+
log.Fatal(err)
|
|
71
|
+
}
|
|
72
|
+
if _, err := tx.ExecContext(context.Background(), string(sqlBytes)); err != nil {
|
|
73
|
+
_ = tx.Rollback()
|
|
74
|
+
log.Fatal(err)
|
|
75
|
+
}
|
|
76
|
+
if _, err := tx.ExecContext(context.Background(), `insert into schema_migrations (version) values ($1)`, version); err != nil {
|
|
77
|
+
_ = tx.Rollback()
|
|
78
|
+
log.Fatal(err)
|
|
79
|
+
}
|
|
80
|
+
if err := tx.Commit(); err != nil {
|
|
81
|
+
log.Fatal(err)
|
|
82
|
+
}
|
|
83
|
+
log.Printf("applied migration %s", version)
|
|
84
|
+
}
|
|
85
|
+
}
|
|
86
|
+
|
|
87
|
+
func waitForDatabase(databaseURL string, timeout time.Duration) (*sqlx.DB, error) {
|
|
88
|
+
deadline := time.Now().Add(timeout)
|
|
89
|
+
var lastErr error
|
|
90
|
+
|
|
91
|
+
for time.Now().Before(deadline) {
|
|
92
|
+
db, err := app.OpenDatabase(context.Background(), databaseURL)
|
|
93
|
+
if err == nil {
|
|
94
|
+
return db, nil
|
|
95
|
+
}
|
|
96
|
+
lastErr = err
|
|
97
|
+
time.Sleep(time.Second)
|
|
98
|
+
}
|
|
99
|
+
|
|
100
|
+
return nil, lastErr
|
|
101
|
+
}
|
|
@@ -6,13 +6,14 @@ import (
|
|
|
6
6
|
"net/http"
|
|
7
7
|
"time"
|
|
8
8
|
|
|
9
|
+
"cloud.google.com/go/storage"
|
|
9
10
|
"github.com/go-chi/chi/v5"
|
|
11
|
+
_ "github.com/jackc/pgx/v5/stdlib"
|
|
10
12
|
"golang.org/x/net/http2"
|
|
11
13
|
"golang.org/x/net/http2/h2c"
|
|
12
14
|
|
|
13
15
|
"{{MODULE_PATH}}/internal/app"
|
|
14
16
|
"{{MODULE_PATH}}/internal/config"
|
|
15
|
-
"{{MODULE_PATH}}/internal/connectapi"
|
|
16
17
|
"{{MODULE_PATH}}/internal/httpapi"
|
|
17
18
|
)
|
|
18
19
|
|
|
@@ -22,25 +23,25 @@ func main() {
|
|
|
22
23
|
log.Fatal(err)
|
|
23
24
|
}
|
|
24
25
|
|
|
25
|
-
|
|
26
|
-
if
|
|
27
|
-
|
|
28
|
-
|
|
29
|
-
|
|
30
|
-
|
|
31
|
-
|
|
32
|
-
|
|
33
|
-
}); err != nil {
|
|
34
|
-
log.Fatal(err)
|
|
35
|
-
}
|
|
26
|
+
db, err := app.OpenDatabase(context.Background(), cfg.DatabaseURL)
|
|
27
|
+
if err != nil {
|
|
28
|
+
log.Fatal(err)
|
|
29
|
+
}
|
|
30
|
+
|
|
31
|
+
storageClient, err := storage.NewClient(context.Background())
|
|
32
|
+
if err != nil {
|
|
33
|
+
log.Fatal(err)
|
|
36
34
|
}
|
|
37
35
|
|
|
36
|
+
service := app.NewChatService(
|
|
37
|
+
db,
|
|
38
|
+
app.NewGCSStorage(cfg.AttachmentBucket, cfg.AttachmentPublicBaseURL, storageClient),
|
|
39
|
+
app.GenericWebhookAdapter{},
|
|
40
|
+
)
|
|
41
|
+
|
|
38
42
|
router := chi.NewRouter()
|
|
39
43
|
httpapi.RegisterRoutes(router, service)
|
|
40
44
|
|
|
41
|
-
connectPath, connectHandler := connectapi.NewHandler(service)
|
|
42
|
-
router.Mount(connectPath, connectHandler)
|
|
43
|
-
|
|
44
45
|
server := &http.Server{
|
|
45
46
|
Addr: ":" + cfg.Port,
|
|
46
47
|
ReadHeaderTimeout: 10 * time.Second,
|
|
@@ -3,8 +3,11 @@ module {{MODULE_PATH}}
|
|
|
3
3
|
go 1.25.4
|
|
4
4
|
|
|
5
5
|
require (
|
|
6
|
+
cloud.google.com/go/storage v1.53.0
|
|
6
7
|
connectrpc.com/connect v1.19.1
|
|
7
8
|
github.com/go-chi/chi/v5 v5.2.2
|
|
9
|
+
github.com/jackc/pgx/v5 v5.7.5
|
|
10
|
+
github.com/jmoiron/sqlx v1.4.0
|
|
8
11
|
golang.org/x/net v0.42.0
|
|
9
12
|
google.golang.org/protobuf v1.36.10
|
|
10
13
|
)
|