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,142 +0,0 @@
|
|
|
1
|
-
export type ChatUser = {
|
|
2
|
-
id: string;
|
|
3
|
-
username: string;
|
|
4
|
-
displayName: string | null;
|
|
5
|
-
createdAt: string;
|
|
6
|
-
updatedAt: string;
|
|
7
|
-
};
|
|
8
|
-
|
|
9
|
-
export type ChatConversation = {
|
|
10
|
-
id: string;
|
|
11
|
-
title: string | null;
|
|
12
|
-
createdByUserId: string;
|
|
13
|
-
participants: ChatUser[];
|
|
14
|
-
createdAt: string;
|
|
15
|
-
updatedAt: string;
|
|
16
|
-
};
|
|
17
|
-
|
|
18
|
-
export type ChatMessage = {
|
|
19
|
-
id: string;
|
|
20
|
-
conversationId: string;
|
|
21
|
-
userId: string;
|
|
22
|
-
body: string;
|
|
23
|
-
editedAt: string | null;
|
|
24
|
-
createdAt: string;
|
|
25
|
-
updatedAt: string;
|
|
26
|
-
attachments: ChatMessageAttachment[];
|
|
27
|
-
};
|
|
28
|
-
|
|
29
|
-
export type ChatMessageAttachment = {
|
|
30
|
-
id: string;
|
|
31
|
-
filename: string;
|
|
32
|
-
contentType: string;
|
|
33
|
-
byteSize: number;
|
|
34
|
-
status: "pending" | "ready" | "deleted";
|
|
35
|
-
publicUrl: string;
|
|
36
|
-
};
|
|
37
|
-
|
|
38
|
-
export type ChatAttachment = {
|
|
39
|
-
id: string;
|
|
40
|
-
conversationId: string;
|
|
41
|
-
messageId: string | null;
|
|
42
|
-
uploadedByUserId: string;
|
|
43
|
-
storageBucket: string;
|
|
44
|
-
storageKey: string;
|
|
45
|
-
contentType: string;
|
|
46
|
-
byteSize: number;
|
|
47
|
-
filename: string;
|
|
48
|
-
status: "pending" | "ready" | "deleted";
|
|
49
|
-
publicUrl: string;
|
|
50
|
-
createdAt: string;
|
|
51
|
-
updatedAt: string;
|
|
52
|
-
};
|
|
53
|
-
|
|
54
|
-
export type WebhookEventRecord = {
|
|
55
|
-
id: string;
|
|
56
|
-
provider: string;
|
|
57
|
-
externalEventId: string;
|
|
58
|
-
eventType: string;
|
|
59
|
-
signatureValid: boolean;
|
|
60
|
-
status: "received" | "processed" | "failed";
|
|
61
|
-
payloadJson: string;
|
|
62
|
-
receivedAt: string;
|
|
63
|
-
processedAt: string | null;
|
|
64
|
-
};
|
|
65
|
-
|
|
66
|
-
export type CreateUserInput = {
|
|
67
|
-
username: string;
|
|
68
|
-
displayName?: string | null;
|
|
69
|
-
};
|
|
70
|
-
|
|
71
|
-
export type CreateConversationInput = {
|
|
72
|
-
createdByUserId: string;
|
|
73
|
-
title?: string | null;
|
|
74
|
-
participantUserIds?: string[];
|
|
75
|
-
};
|
|
76
|
-
|
|
77
|
-
export type UpdateConversationInput = {
|
|
78
|
-
title?: string | null;
|
|
79
|
-
};
|
|
80
|
-
|
|
81
|
-
export type CreateMessageInput = {
|
|
82
|
-
userId: string;
|
|
83
|
-
body: string;
|
|
84
|
-
};
|
|
85
|
-
|
|
86
|
-
export type ListMessagesInput = {
|
|
87
|
-
cursor?: string | null;
|
|
88
|
-
limit?: number | null;
|
|
89
|
-
};
|
|
90
|
-
|
|
91
|
-
export type UpdateMessageInput = {
|
|
92
|
-
body: string;
|
|
93
|
-
};
|
|
94
|
-
|
|
95
|
-
export type CreateAttachmentUploadInput = {
|
|
96
|
-
conversationId: string;
|
|
97
|
-
uploadedByUserId: string;
|
|
98
|
-
filename: string;
|
|
99
|
-
contentType: string;
|
|
100
|
-
byteSize: number;
|
|
101
|
-
};
|
|
102
|
-
|
|
103
|
-
export type FinalizeAttachmentInput = {
|
|
104
|
-
messageId?: string | null;
|
|
105
|
-
};
|
|
106
|
-
|
|
107
|
-
export type AttachmentUploadTarget = {
|
|
108
|
-
method: "PUT";
|
|
109
|
-
url: string;
|
|
110
|
-
headers: Record<string, string>;
|
|
111
|
-
};
|
|
112
|
-
|
|
113
|
-
export type CreateAttachmentUploadResult = {
|
|
114
|
-
attachment: ChatAttachment;
|
|
115
|
-
upload: AttachmentUploadTarget;
|
|
116
|
-
};
|
|
117
|
-
|
|
118
|
-
export type AttachmentObjectMetadata = {
|
|
119
|
-
bucket: string;
|
|
120
|
-
key: string;
|
|
121
|
-
contentType: string;
|
|
122
|
-
byteSize: number;
|
|
123
|
-
publicUrl: string;
|
|
124
|
-
};
|
|
125
|
-
|
|
126
|
-
export type NormalizedWebhookEvent = {
|
|
127
|
-
provider: string;
|
|
128
|
-
externalEventId: string;
|
|
129
|
-
eventType: string;
|
|
130
|
-
signatureValid: boolean;
|
|
131
|
-
payloadJson: string;
|
|
132
|
-
};
|
|
133
|
-
|
|
134
|
-
export type WebhookProcessResult = {
|
|
135
|
-
event: WebhookEventRecord;
|
|
136
|
-
duplicate: boolean;
|
|
137
|
-
};
|
|
138
|
-
|
|
139
|
-
export type ListMessagesResult = {
|
|
140
|
-
messages: ChatMessage[];
|
|
141
|
-
nextCursor?: string;
|
|
142
|
-
};
|
|
@@ -1,72 +0,0 @@
|
|
|
1
|
-
import { Storage } from "@google-cloud/storage";
|
|
2
|
-
import type { AttachmentObjectMetadata, AttachmentUploadTarget } from "./chat/types";
|
|
3
|
-
|
|
4
|
-
export type AttachmentStorage = {
|
|
5
|
-
createSignedUpload(input: {
|
|
6
|
-
attachmentId: string;
|
|
7
|
-
conversationId: string;
|
|
8
|
-
filename: string;
|
|
9
|
-
contentType: string;
|
|
10
|
-
}): Promise<{ bucket: string; key: string; upload: AttachmentUploadTarget; publicUrl: string }>;
|
|
11
|
-
getObjectMetadata(input: { bucket: string; key: string }): Promise<AttachmentObjectMetadata>;
|
|
12
|
-
};
|
|
13
|
-
|
|
14
|
-
export class GcsAttachmentStorage implements AttachmentStorage {
|
|
15
|
-
constructor(
|
|
16
|
-
private readonly bucketName = requireAttachmentBucket(),
|
|
17
|
-
private readonly publicBaseUrl = Bun.env.ATTACHMENT_PUBLIC_BASE_URL?.trim() || `https://storage.googleapis.com/${requireAttachmentBucket()}`
|
|
18
|
-
) {}
|
|
19
|
-
|
|
20
|
-
async createSignedUpload(input: { attachmentId: string; conversationId: string; filename: string; contentType: string }) {
|
|
21
|
-
const storage = new Storage();
|
|
22
|
-
const bucket = storage.bucket(this.bucketName);
|
|
23
|
-
const key = `attachments/${input.conversationId}/${input.attachmentId}/${sanitizeFilename(input.filename)}`;
|
|
24
|
-
const file = bucket.file(key);
|
|
25
|
-
const [url] = await file.getSignedUrl({
|
|
26
|
-
action: "write",
|
|
27
|
-
version: "v4",
|
|
28
|
-
expires: Date.now() + 15 * 60 * 1000,
|
|
29
|
-
contentType: input.contentType,
|
|
30
|
-
});
|
|
31
|
-
return {
|
|
32
|
-
bucket: this.bucketName,
|
|
33
|
-
key,
|
|
34
|
-
upload: {
|
|
35
|
-
method: "PUT" as const,
|
|
36
|
-
url,
|
|
37
|
-
headers: {
|
|
38
|
-
"Content-Type": input.contentType,
|
|
39
|
-
},
|
|
40
|
-
},
|
|
41
|
-
publicUrl: `${this.publicBaseUrl.replace(/\/+$/g, "")}/${key}`,
|
|
42
|
-
};
|
|
43
|
-
}
|
|
44
|
-
|
|
45
|
-
async getObjectMetadata(input: { bucket: string; key: string }): Promise<AttachmentObjectMetadata> {
|
|
46
|
-
const storage = new Storage();
|
|
47
|
-
const [metadata] = await storage.bucket(input.bucket).file(input.key).getMetadata();
|
|
48
|
-
return {
|
|
49
|
-
bucket: input.bucket,
|
|
50
|
-
key: input.key,
|
|
51
|
-
contentType: String(metadata.contentType ?? ""),
|
|
52
|
-
byteSize: Number(metadata.size ?? 0),
|
|
53
|
-
publicUrl: `${this.publicBaseUrl.replace(/\/+$/g, "")}/${input.key}`,
|
|
54
|
-
};
|
|
55
|
-
}
|
|
56
|
-
}
|
|
57
|
-
|
|
58
|
-
export function createAttachmentStorage() {
|
|
59
|
-
return new GcsAttachmentStorage();
|
|
60
|
-
}
|
|
61
|
-
|
|
62
|
-
export function requireAttachmentBucket() {
|
|
63
|
-
const bucket = Bun.env.ATTACHMENT_BUCKET?.trim();
|
|
64
|
-
if (!bucket) {
|
|
65
|
-
throw new Error("ATTACHMENT_BUCKET is required");
|
|
66
|
-
}
|
|
67
|
-
return bucket;
|
|
68
|
-
}
|
|
69
|
-
|
|
70
|
-
function sanitizeFilename(filename: string) {
|
|
71
|
-
return filename.trim().replace(/[^a-zA-Z0-9._-]+/g, "-") || "upload.bin";
|
|
72
|
-
}
|
|
@@ -1,35 +0,0 @@
|
|
|
1
|
-
import type { NormalizedWebhookEvent } from "./chat/types";
|
|
2
|
-
|
|
3
|
-
export type WebhookAdapter = {
|
|
4
|
-
normalize(provider: string, headers: Headers, rawBody: string): Promise<NormalizedWebhookEvent>;
|
|
5
|
-
};
|
|
6
|
-
|
|
7
|
-
export class GenericJsonWebhookAdapter implements WebhookAdapter {
|
|
8
|
-
async normalize(provider: string, headers: Headers, rawBody: string): Promise<NormalizedWebhookEvent> {
|
|
9
|
-
const payload = parseJson(rawBody);
|
|
10
|
-
const secret = Bun.env[`WEBHOOK_${provider.toUpperCase()}_SECRET`]?.trim();
|
|
11
|
-
const incomingSecret = headers.get("x-webhook-secret")?.trim() ?? "";
|
|
12
|
-
const externalEventId = String(payload.id ?? headers.get("x-event-id") ?? crypto.randomUUID());
|
|
13
|
-
const eventType = String(payload.type ?? headers.get("x-event-type") ?? "generic.event");
|
|
14
|
-
|
|
15
|
-
return {
|
|
16
|
-
provider,
|
|
17
|
-
externalEventId,
|
|
18
|
-
eventType,
|
|
19
|
-
signatureValid: secret ? incomingSecret === secret : true,
|
|
20
|
-
payloadJson: rawBody,
|
|
21
|
-
};
|
|
22
|
-
}
|
|
23
|
-
}
|
|
24
|
-
|
|
25
|
-
export function createWebhookAdapter() {
|
|
26
|
-
return new GenericJsonWebhookAdapter();
|
|
27
|
-
}
|
|
28
|
-
|
|
29
|
-
function parseJson(rawBody: string) {
|
|
30
|
-
try {
|
|
31
|
-
return JSON.parse(rawBody) as Record<string, unknown>;
|
|
32
|
-
} catch {
|
|
33
|
-
return {};
|
|
34
|
-
}
|
|
35
|
-
}
|
|
@@ -1,182 +0,0 @@
|
|
|
1
|
-
import { afterEach, beforeEach, expect, test } from "bun:test";
|
|
2
|
-
import { SQL } from "bun";
|
|
3
|
-
import { createDb } from "../src/db/client";
|
|
4
|
-
import { ChatRepository } from "../src/db/repository";
|
|
5
|
-
import { DefaultChatService } from "../src/chat/service";
|
|
6
|
-
import { createRpcService } from "../src/index";
|
|
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 rpc = createRpcService(new DefaultChatService(new ChatRepository(createDb(databaseUrl)), storage, new NoopWebhookAdapter()));
|
|
41
|
-
|
|
42
|
-
const user = (await rpc.createUser!({ username: "alice", displayName: "Alice" } as any, undefined as never)).user!;
|
|
43
|
-
const conversation = (
|
|
44
|
-
await rpc.createConversation!({
|
|
45
|
-
createdByUserId: user.id!,
|
|
46
|
-
title: "General",
|
|
47
|
-
participantUserIds: [user.id!],
|
|
48
|
-
} as any, undefined as never)
|
|
49
|
-
).conversation!;
|
|
50
|
-
|
|
51
|
-
const messageIds: string[] = [];
|
|
52
|
-
for (let index = 1; index <= 55; index += 1) {
|
|
53
|
-
const response = await rpc.createMessage!({
|
|
54
|
-
conversationId: conversation.id!,
|
|
55
|
-
userId: user.id!,
|
|
56
|
-
body: `message-${index}`,
|
|
57
|
-
} as any, undefined as never);
|
|
58
|
-
messageIds.push(response.message!.id!);
|
|
59
|
-
}
|
|
60
|
-
await rewriteMessageTimestamps(messageIds);
|
|
61
|
-
|
|
62
|
-
const uploadResult = await rpc.createAttachmentUpload!({
|
|
63
|
-
conversationId: conversation.id!,
|
|
64
|
-
userId: user.id!,
|
|
65
|
-
filename: "photo.png",
|
|
66
|
-
contentType: "image/png",
|
|
67
|
-
byteSize: BigInt(1234),
|
|
68
|
-
} as any, undefined as never);
|
|
69
|
-
storage.setObjectMetadata(uploadResult.attachment!.publicUrl!, {
|
|
70
|
-
contentType: "image/png",
|
|
71
|
-
byteSize: 1234,
|
|
72
|
-
publicUrl: uploadResult.attachment!.publicUrl!,
|
|
73
|
-
});
|
|
74
|
-
await rpc.finalizeAttachment!({
|
|
75
|
-
attachmentId: uploadResult.attachment!.id!,
|
|
76
|
-
messageId: messageIds[54]!,
|
|
77
|
-
} as any, undefined as never);
|
|
78
|
-
|
|
79
|
-
const firstPage = await rpc.listMessages!({
|
|
80
|
-
conversationId: conversation.id!,
|
|
81
|
-
} as any, undefined as never);
|
|
82
|
-
expect(firstPage.messages!).toHaveLength(50);
|
|
83
|
-
expect(firstPage.messages![0]?.body).toBe("message-55");
|
|
84
|
-
expect(firstPage.messages![49]?.body).toBe("message-6");
|
|
85
|
-
expect(firstPage.nextCursor).toBeString();
|
|
86
|
-
expect(firstPage.messages![0]?.attachments).toEqual([
|
|
87
|
-
{
|
|
88
|
-
id: uploadResult.attachment!.id!,
|
|
89
|
-
filename: "photo.png",
|
|
90
|
-
contentType: "image/png",
|
|
91
|
-
byteSize: BigInt(1234),
|
|
92
|
-
status: "ready",
|
|
93
|
-
publicUrl: uploadResult.attachment!.publicUrl!,
|
|
94
|
-
},
|
|
95
|
-
]);
|
|
96
|
-
|
|
97
|
-
const secondPage = await rpc.listMessages!({
|
|
98
|
-
conversationId: conversation.id!,
|
|
99
|
-
cursor: firstPage.nextCursor,
|
|
100
|
-
} as any, undefined as never);
|
|
101
|
-
expect(secondPage.messages!.map((message) => message.body)).toEqual([
|
|
102
|
-
"message-5",
|
|
103
|
-
"message-4",
|
|
104
|
-
"message-3",
|
|
105
|
-
"message-2",
|
|
106
|
-
"message-1",
|
|
107
|
-
]);
|
|
108
|
-
expect(secondPage.nextCursor).toBe("");
|
|
109
|
-
});
|
|
110
|
-
|
|
111
|
-
async function rewriteMessageTimestamps(messageIds: string[]) {
|
|
112
|
-
if (!sql) {
|
|
113
|
-
throw new Error("sql client not initialized");
|
|
114
|
-
}
|
|
115
|
-
|
|
116
|
-
const baseTime = Date.parse("2026-01-01T00:00:00.000Z");
|
|
117
|
-
for (const [index, messageId] of messageIds.entries()) {
|
|
118
|
-
const createdAt = new Date(baseTime + (index + 1) * 1000).toISOString();
|
|
119
|
-
await sql`
|
|
120
|
-
update messages
|
|
121
|
-
set created_at = ${createdAt}::timestamptz,
|
|
122
|
-
updated_at = ${createdAt}::timestamptz
|
|
123
|
-
where id = ${messageId}
|
|
124
|
-
`;
|
|
125
|
-
}
|
|
126
|
-
}
|
|
127
|
-
|
|
128
|
-
class FakeAttachmentStorage implements AttachmentStorage {
|
|
129
|
-
private readonly metadata = new Map<string, AttachmentObjectMetadata>();
|
|
130
|
-
|
|
131
|
-
async createSignedUpload(input: {
|
|
132
|
-
attachmentId: string;
|
|
133
|
-
conversationId: string;
|
|
134
|
-
filename: string;
|
|
135
|
-
contentType: string;
|
|
136
|
-
}): Promise<{ bucket: string; key: string; upload: AttachmentUploadTarget; publicUrl: string }> {
|
|
137
|
-
const bucket = "test-bucket";
|
|
138
|
-
const key = `attachments/${input.conversationId}/${input.attachmentId}/${input.filename}`;
|
|
139
|
-
const publicUrl = `https://storage.test/${key}`;
|
|
140
|
-
return {
|
|
141
|
-
bucket,
|
|
142
|
-
key,
|
|
143
|
-
upload: {
|
|
144
|
-
method: "PUT",
|
|
145
|
-
url: `https://uploads.test/${key}`,
|
|
146
|
-
headers: { "Content-Type": input.contentType },
|
|
147
|
-
},
|
|
148
|
-
publicUrl,
|
|
149
|
-
};
|
|
150
|
-
}
|
|
151
|
-
|
|
152
|
-
async getObjectMetadata(input: { bucket: string; key: string }) {
|
|
153
|
-
const metadata = this.metadata.get(`${input.bucket}/${input.key}`);
|
|
154
|
-
if (!metadata) {
|
|
155
|
-
throw new Error(`missing metadata for ${input.bucket}/${input.key}`);
|
|
156
|
-
}
|
|
157
|
-
return metadata;
|
|
158
|
-
}
|
|
159
|
-
|
|
160
|
-
setObjectMetadata(publicUrl: string, input: Omit<AttachmentObjectMetadata, "bucket" | "key">) {
|
|
161
|
-
const [, key = ""] = publicUrl.split("https://storage.test/");
|
|
162
|
-
this.metadata.set(`test-bucket/${key}`, {
|
|
163
|
-
bucket: "test-bucket",
|
|
164
|
-
key,
|
|
165
|
-
contentType: input.contentType,
|
|
166
|
-
byteSize: input.byteSize,
|
|
167
|
-
publicUrl: input.publicUrl,
|
|
168
|
-
});
|
|
169
|
-
}
|
|
170
|
-
}
|
|
171
|
-
|
|
172
|
-
class NoopWebhookAdapter {
|
|
173
|
-
async normalize() {
|
|
174
|
-
return {
|
|
175
|
-
provider: "generic",
|
|
176
|
-
externalEventId: "evt_test",
|
|
177
|
-
eventType: "generic.event",
|
|
178
|
-
signatureValid: true,
|
|
179
|
-
payloadJson: "{}",
|
|
180
|
-
};
|
|
181
|
-
}
|
|
182
|
-
}
|