Skip to content

Commit 3d58597

Browse files
feat: add phase 1 chat persistence schema
Co-authored-by: multica-agent <github@multica.ai>
1 parent 87d5af3 commit 3d58597

5 files changed

Lines changed: 152 additions & 4 deletions

File tree

package.json

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -15,6 +15,7 @@
1515
"test": "playwright test",
1616
"format": "biome check --write",
1717
"check": "run-p 'check:*'",
18+
"check:schema-contract": "bun scripts/check_schema_contract.ts",
1819
"check:tsc": "tsc",
1920
"check:eslint": "eslint src",
2021
"check:script": "uv run basedpyright scripts",

schema.prisma

Lines changed: 27 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -18,16 +18,41 @@ model user {
1818
}
1919

2020
model friend {
21-
id String @id
2221
owner_id String
22+
id String
2323
name String
2424
avatar Bytes?
2525
bio String
26+
27+
@@id([owner_id, id])
2628
}
2729

2830
model chat_message {
29-
timestamp DateTime @id @default(now())
31+
id String @id
3032
owner_id String
3133
chat_user_id String
34+
sender_id String
3235
content String
36+
status String @default("sent")
37+
created_at DateTime @default(now())
38+
updated_at DateTime @default(now()) @updatedAt
39+
retry_count Int @default(0)
40+
last_error String?
41+
42+
@@index([owner_id, chat_user_id, created_at])
43+
@@index([owner_id, status])
44+
}
45+
46+
model friend_request {
47+
owner_id String
48+
remote_id String
49+
name String
50+
avatar Bytes?
51+
bio String
52+
status String @default("pending")
53+
created_at DateTime @default(now())
54+
responded_at DateTime?
55+
56+
@@id([owner_id, remote_id])
57+
@@index([owner_id, status, created_at])
3358
}

scripts/check_schema_contract.ts

Lines changed: 88 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,88 @@
1+
import { readFileSync } from "node:fs";
2+
3+
const schema = readFileSync("schema.prisma", "utf8");
4+
const endpointTypes = readFileSync("src/lib/endpoint/types.ts", "utf8");
5+
6+
function modelBody(name: string) {
7+
const match = schema.match(
8+
new RegExp(`model\\s+${name}\\s+\\{([\\s\\S]*?)\\n\\}`),
9+
);
10+
if (!match) throw new Error(`Missing Prisma model: ${name}`);
11+
return match[1];
12+
}
13+
14+
function expectMatch(source: string, pattern: RegExp, message: string) {
15+
if (!pattern.test(source)) throw new Error(message);
16+
}
17+
18+
const friend = modelBody("friend");
19+
expectMatch(
20+
friend,
21+
/^\s*@@id\(\[owner_id,\s*id\]\)/m,
22+
"friend must be keyed by owner_id + id",
23+
);
24+
if (/^\s*id\s+String\s+@id\b/m.test(friend)) {
25+
throw new Error("friend.id must not be a global primary key");
26+
}
27+
28+
const chatMessage = modelBody("chat_message");
29+
for (const [field, pattern] of [
30+
["id", /^\s*id\s+String\s+@id\b/m],
31+
["owner_id", /^\s*owner_id\s+String\b/m],
32+
["chat_user_id", /^\s*chat_user_id\s+String\b/m],
33+
["sender_id", /^\s*sender_id\s+String\b/m],
34+
["content", /^\s*content\s+String\b/m],
35+
["status", /^\s*status\s+String\b/m],
36+
["created_at", /^\s*created_at\s+DateTime\b/m],
37+
["updated_at", /^\s*updated_at\s+DateTime\b/m],
38+
["retry_count", /^\s*retry_count\s+Int\b/m],
39+
["last_error", /^\s*last_error\s+String\?/m],
40+
] as const) {
41+
expectMatch(chatMessage, pattern, `chat_message is missing ${field}`);
42+
}
43+
44+
const friendRequest = modelBody("friend_request");
45+
for (const [field, pattern] of [
46+
["owner_id", /^\s*owner_id\s+String\b/m],
47+
["remote_id", /^\s*remote_id\s+String\b/m],
48+
["name", /^\s*name\s+String\b/m],
49+
["avatar", /^\s*avatar\s+Bytes\?/m],
50+
["bio", /^\s*bio\s+String\b/m],
51+
["status", /^\s*status\s+String\b/m],
52+
["created_at", /^\s*created_at\s+DateTime\b/m],
53+
["responded_at", /^\s*responded_at\s+DateTime\?/m],
54+
] as const) {
55+
expectMatch(friendRequest, pattern, `friend_request is missing ${field}`);
56+
}
57+
expectMatch(
58+
friendRequest,
59+
/^\s*@@id\(\[owner_id,\s*remote_id\]\)/m,
60+
"friend_request must be keyed by owner_id + remote_id",
61+
);
62+
63+
expectMatch(endpointTypes, /id:\s*string/, "Message type must include id");
64+
expectMatch(
65+
endpointTypes,
66+
/sender_id:\s*string/,
67+
"Message type must include sender_id",
68+
);
69+
expectMatch(
70+
endpointTypes,
71+
/status:\s*MessageStatus/,
72+
"Message type must expose MessageStatus",
73+
);
74+
expectMatch(
75+
endpointTypes,
76+
/created_at:\s*string/,
77+
"Message type must expose sortable created_at",
78+
);
79+
expectMatch(
80+
endpointTypes,
81+
/retry_count:\s*number/,
82+
"Message type must include retry_count",
83+
);
84+
expectMatch(
85+
endpointTypes,
86+
/last_error\?:\s*string/,
87+
"Message type must include optional last_error",
88+
);

src/components/ui/chat_bar.tsx

Lines changed: 13 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -17,9 +17,21 @@ export default function ChatBar(props: { chat_user: User }) {
1717
if (!u) throw new Error("用户不存在");
1818
return await main_store.sqlite.query<Message>(
1919
QueryBuilder.selectFrom("chat_message")
20-
.select(["chat_user_id", "timestamp", "content"])
20+
.select([
21+
"id",
22+
"owner_id",
23+
"chat_user_id",
24+
"sender_id",
25+
"content",
26+
"status",
27+
"created_at",
28+
"updated_at",
29+
"retry_count",
30+
"last_error",
31+
])
2132
.where("owner_id", "=", u.id)
2233
.where("chat_user_id", "=", props.chat_user.id)
34+
.orderBy("created_at", "asc")
2335
.compile(),
2436
);
2537
});

src/lib/endpoint/types.ts

Lines changed: 23 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -13,8 +13,30 @@ export type RelayConfig = {
1313
quic_port: number;
1414
};
1515

16+
export type MessageStatus = "sending" | "sent" | "failed" | "received";
17+
1618
export type Message = {
19+
id: string;
20+
owner_id: string;
21+
chat_user_id: string;
1722
sender_id: string;
18-
timestamp: number;
1923
content: string;
24+
status: MessageStatus;
25+
created_at: string;
26+
updated_at: string;
27+
retry_count: number;
28+
last_error?: string | null;
29+
};
30+
31+
export type FriendRequestStatus = "pending" | "accepted" | "rejected";
32+
33+
export type FriendRequest = {
34+
owner_id: string;
35+
remote_id: string;
36+
name: string;
37+
avatar?: Uint8Array;
38+
bio: string;
39+
status: FriendRequestStatus;
40+
created_at: string;
41+
responded_at?: string | null;
2042
};

0 commit comments

Comments
 (0)