Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
8 changes: 2 additions & 6 deletions .env.example
Original file line number Diff line number Diff line change
Expand Up @@ -5,7 +5,7 @@
# Global Settings
NODE_ENV=development
JWT_SECRET="your_common_jwt_secret_key"
DATABASE_URL="postgresql://username:password@host:port/database"
DATABASE_URL="postgresql://app_runtime_login:password@host:port/database"

# Frontend Configuration (Next.js)
FRONTEND_URL="http://localhost:3000"
Expand All @@ -15,17 +15,13 @@ NEXT_PUBLIC_MEMBER_API_URL="http://localhost:8788"
# Community API Configuration (Hono / Wrangler)
COMMUNITY_URL="http://localhost:8787"
BETTER_AUTH_SECRET="a_very_long_and_secure_random_string_32_chars"
SUPABASE_ID="your_supabase_id"
SUPABASE_SECRET_KEY="your_supabase_secret_key"
DISCORD_TOKEN="your_discord_token"
DISCORD_GUILD_ID="your_discord_guild_id"

# Member (backend) API Configuration (Hono / Wrangler)
DEV_USER_ID="replace_with_an_existing_better_auth_user_id"
GITHUB_CLIENT_ID="your_github_client_id"
GITHUB_CLIENT_SECRET="your_github_client_secret"
DISCORD_CLIENT_ID="your_discord_client_id"
DISCORD_CLIENT_SECRET="your_discord_client_secret"

# Wrangler Hyperdrive Local Connection Emulation String (Secure, ignored by git)
CLOUDFLARE_HYPERDRIVE_LOCAL_CONNECTION_STRING_HYPERDRIVE="postgresql://postgres:your_password@db.your_project_id.supabase.co:5432/postgres"
CLOUDFLARE_HYPERDRIVE_LOCAL_CONNECTION_STRING_HYPERDRIVE="postgresql://app_runtime_login:your_password@your_postgres_host:5432/postgres"
24 changes: 24 additions & 0 deletions .github/workflows/ci.yml
Original file line number Diff line number Diff line change
Expand Up @@ -15,6 +15,30 @@ concurrency:
cancel-in-progress: true

jobs:
database:
runs-on: ubuntu-latest
steps:
- uses: actions/checkout@v7
- uses: supabase/setup-cli@v1
with:
# Pinned: resolving "latest" queries the GitHub API unauthenticated,
# which rate-limits as soon as a few pull requests build at once.
version: 2.101.0
# Replays every migration into a fresh database, so a migration that only
# works against an already-populated schema fails here instead of in a
# maintenance window.
- name: Start the local database
run: supabase db start
# The RLS boundary and the directory allowlist are enforced in SQL, so the
# pgTAP suites under supabase/tests are the only thing that proves them.
- name: Run pgTAP suites
run: supabase test db
- name: Stop the local database
if: always()
# Never turn a teardown problem into the reported failure.
continue-on-error: true
run: supabase stop --no-backup

member:
runs-on: ubuntu-latest
steps:
Expand Down
18 changes: 10 additions & 8 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -39,21 +39,23 @@ cd ../frontend && npm install
cd ..
```

### 2. 環境変数(`.env`)の設定
リポジトリのルートディレクトリに存在する `.env` ファイル(Git管理外)を作成・編集します。テンプレートとして `.env.example` をコピーして作成してください
### 2. 環境変数の設定
各Workerには必要な秘密だけを渡します。ルートの `.env` をWorkerへ丸ごとコピーしないでください

```bash
cp .env.example .env
cp community/.dev.vars.example community/.dev.vars
cp member/.dev.vars.example member/.dev.vars
```

**【重要】Hyperdrive ローカルエミュレーション設定**
ローカル開発時(`wrangler dev`)にも、Wrangler がローカルでデータベース接続プール(Hyperdrive)をエミュレートするため、以下の環境変数をルートの `.env` に設定する必要があります
ローカル開発時(`wrangler dev`)は、Hyperdriveの接続先をWranglerプロセスの環境変数で上書きします。`.dev.vars` の通常のWorker bindingとは別なので、APIを起動する各シェルで設定してください

```ini
# Wrangler Hyperdrive Local Connection Emulation String
CLOUDFLARE_HYPERDRIVE_LOCAL_CONNECTION_STRING_HYPERDRIVE="postgresql://username:password@host:5432/database"
```bash
export CLOUDFLARE_HYPERDRIVE_LOCAL_CONNECTION_STRING_HYPERDRIVE="postgresql://app_runtime_login:password@host:5432/database"
```
*※ローカル開発でご自身の PostgreSQL(Docker 等)を使用する場合は、上記のアドレスをローカルDBのものに書き換えてください。*

`app_runtime_login` はDB運用runbookで作る非superuser・非`BYPASSRLS`の専用ロールです。アプリケーションから`postgres`/owner資格情報を使わないでください。

### 3. 各プロジェクトのローカル起動
それぞれのディレクトリで開発サーバーを起動します。
Expand Down Expand Up @@ -120,6 +122,6 @@ npm run deploy

## 🛠️ 開発規約 (Development Conventions)

* **Database migrations**: 適用履歴の正本は `/supabase/migrations` です。`supabase migration new <name>` で作成し、`/share/drizzle/schema.ts` を同じPRで同期してください。Drizzleの生成SQLはレビュー補助であり、直接適用しません。詳細は [`docs/database-migrations.md`](docs/database-migrations.md) を参照してください。
* **PostgreSQL & Drizzle**: 実行時は Supabase SDK/Data API を使わず、Workerでは Cloudflare Hyperdrive のプールを介し、ローカルでは `DATABASE_URL` を使って `pg` + Drizzle で PostgreSQL へ接続します。スキーマ定義は `/share/drizzle/schema.ts` で共有し、変更時は PostgreSQL migration と同じPRで同期してください。現在のDB適用履歴とポータビリティ方針は [`docs/database-migrations.md`](docs/database-migrations.md) を参照してください。
* **OpenAPI 仕様の遵守**: API のルートやバリデーションスキーマの定義には `@hono/zod-openapi` を厳格に使用し、コードと Swagger UI(`/ui` エンドポイント)が常に同期するように実装してください。
* **型安全の確保**: バックエンドとフロントエンド間の API 通信は、Hono Client (`hc`) を用いて `frontend/src/lib/client.ts` でクライアント化され、エンドツーエンドでの型安全性が保証されています。
14 changes: 10 additions & 4 deletions community/.dev.vars.example
Original file line number Diff line number Diff line change
@@ -1,14 +1,20 @@
# Database
DATABASE_URL=postgresql://user:password@host:port/dbname
DATABASE_URL=postgresql://app_runtime_login:password@host:port/dbname

# Better Auth Configuration
BETTER_AUTH_SECRET=your_secure_random_32_char_string
JWT_SECRET=use_a_different_secure_random_32_char_string
BETTER_AUTH_URL=http://localhost:8787
COMMUNITY_URL=http://localhost:8787
FRONTEND_URL=http://localhost:3000

# Social Providers
GITHUB_CLIENT_ID=your_github_client_id
GITHUB_CLIENT_SECRET=your_github_client_secret
# Social Provider (Discord is the only login provider)
DISCORD_CLIENT_ID=your_discord_client_id
DISCORD_CLIENT_SECRET=your_discord_client_secret

# Discord guild verification
DISCORD_TOKEN=your_discord_bot_token
DISCORD_GUILD_ID=your_guild_id

# Optional database-backed local identity
DEV_USER_ID=your_local_user_id
7 changes: 3 additions & 4 deletions community/.env.example
Original file line number Diff line number Diff line change
@@ -1,15 +1,14 @@
# Database
DATABASE_URL=postgresql://user:password@host:port/dbname
DATABASE_URL=postgresql://app_runtime_login:password@host:port/dbname

# Better Auth Configuration
BETTER_AUTH_SECRET=your_secure_random_32_char_string
JWT_SECRET=use_a_different_secure_random_32_char_string
BETTER_AUTH_URL=http://localhost:8787 # The base URL of your auth server
COMMUNITY_URL=http://localhost:8787
FRONTEND_URL=http://localhost:3000

# Social Providers
GITHUB_CLIENT_ID=your_github_client_id
GITHUB_CLIENT_SECRET=your_github_client_secret
# Social Provider (Discord is the only login provider)
DISCORD_CLIENT_ID=your_discord_client_id
DISCORD_CLIENT_SECRET=your_discord_client_secret

Expand Down
2 changes: 1 addition & 1 deletion community/package.json
Original file line number Diff line number Diff line change
Expand Up @@ -5,7 +5,7 @@
"node": ">=22.0.0"
},
"scripts": {
"dev": "cp ../.env .dev.vars && wrangler dev",
"dev": "wrangler dev",
"deploy": "wrangler deploy --minify",
"test": "tsx --test \"src/**/*.test.ts\"",
"test:discord:live": "RUN_DISCORD_LIVE_TESTS=1 tsx --test src/lib/community/discord/live-send.test.ts",
Expand Down
24 changes: 24 additions & 0 deletions community/src/api_v0/message/reactions.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -63,12 +63,20 @@ const linkedUsers: LinkedReactionUser[] = [
email: 'taro-account@example.com',
memberId: 'member-1',
memberName: '山田 太郎',
memberStatus: 'active',
displayGrade: 'B2',
studentId: 'S001',
studentEmail: 'taro@example.edu',
emergencyContact: '090-0000-0000',
insurance: true,
someAllergy: false,
allergyDetails: null,
skills: ['TypeScript'],
interests: ['Robotics'],
currentActivities: 'Robot controller',
bio: 'Embedded developer',
discordNickname: 'たろう',
discordRoles: ['Member', 'Developer'],
},
];

Expand Down Expand Up @@ -100,12 +108,20 @@ test('buildMessageReactionSummary includes reaction users, linked personal infor
email: 'taro-account@example.com',
memberId: 'member-1',
memberName: '山田 太郎',
memberStatus: 'active',
displayGrade: 'B2',
studentId: 'S001',
studentEmail: 'taro@example.edu',
emergencyContact: '090-0000-0000',
insurance: true,
someAllergy: false,
allergyDetails: null,
skills: ['TypeScript'],
interests: ['Robotics'],
currentActivities: 'Robot controller',
bio: 'Embedded developer',
discordNickname: 'たろう',
discordRoles: ['Member', 'Developer'],
reactions: ['✅'],
});
assert.deepEqual(summary.reactions[0].users[1], {
Expand All @@ -118,12 +134,20 @@ test('buildMessageReactionSummary includes reaction users, linked personal infor
email: null,
memberId: null,
memberName: null,
memberStatus: null,
displayGrade: null,
studentId: null,
studentEmail: null,
emergencyContact: null,
insurance: null,
someAllergy: null,
allergyDetails: null,
skills: [],
interests: [],
currentActivities: null,
bio: null,
discordNickname: null,
discordRoles: [],
reactions: ['✅'],
});
assert.deepEqual(summary.members.find(member => member.discordUserId === '423456789012345678')?.reactions, ['✅', '🍱']);
Expand Down
33 changes: 28 additions & 5 deletions community/src/api_v0/message/reactions.ts
Original file line number Diff line number Diff line change
@@ -1,4 +1,5 @@
import type { DiscordMessage, DiscordReactionUser } from "../../lib/community/type";
import type { MemberStatus } from "../../../../share/drizzle/schema";

export type EventMessageRecord = {
id: string;
Expand All @@ -11,19 +12,27 @@ export type EventMessageRecord = {
};

export type LinkedReactionUser = {
discordUserId: string | null;
discordUserId: string;
userId: string;
userName: string;
displayName: string | null;
email: string;
memberId: string | null;
memberName: string | null;
memberStatus: MemberStatus | null;
displayName: string | null;
displayGrade: string | null;
studentId: string | null;
studentEmail: string | null;
emergencyContact: string | null;
insurance: boolean | null;
someAllergy: boolean | null;
allergyDetails: string | null;
skills: string[] | null;
interests: string[] | null;
currentActivities: string | null;
bio: string | null;
discordNickname: string | null;
discordRoles: string[] | null;
};

export type ReactionUsersByEmoji = {
Expand All @@ -42,12 +51,20 @@ export type ReactionMember = {
email: string | null;
memberId: string | null;
memberName: string | null;
memberStatus: MemberStatus | null;
displayGrade: string | null;
studentId: string | null;
studentEmail: string | null;
emergencyContact: string | null;
insurance: boolean | null;
someAllergy: boolean | null;
allergyDetails: string | null;
skills: string[];
interests: string[];
currentActivities: string | null;
bio: string | null;
discordNickname: string | null;
discordRoles: string[];
reactions: string[];
};

Expand All @@ -64,9 +81,7 @@ export const buildMessageReactionSummary = (
linkedUsers: LinkedReactionUser[],
) => {
const linkedUserMap = new Map(
linkedUsers
.filter((user) => user.discordUserId)
.map((user) => [user.discordUserId, user])
linkedUsers.map((user) => [user.discordUserId, user])
);
const memberMap = new Map<string, ReactionMember>();

Expand All @@ -83,12 +98,20 @@ export const buildMessageReactionSummary = (
email: linkedUser?.email ?? null,
memberId: linkedUser?.memberId ?? null,
memberName: linkedUser?.memberName ?? null,
memberStatus: linkedUser?.memberStatus ?? null,
displayGrade: linkedUser?.displayGrade ?? null,
studentId: linkedUser?.studentId ?? null,
studentEmail: linkedUser?.studentEmail ?? null,
emergencyContact: linkedUser?.emergencyContact ?? null,
insurance: linkedUser?.insurance ?? null,
someAllergy: linkedUser?.someAllergy ?? null,
allergyDetails: linkedUser?.allergyDetails ?? null,
skills: linkedUser?.skills ?? [],
interests: linkedUser?.interests ?? [],
currentActivities: linkedUser?.currentActivities ?? null,
bio: linkedUser?.bio ?? null,
discordNickname: linkedUser?.discordNickname ?? null,
discordRoles: linkedUser?.discordRoles ?? [],
reactions: [reaction.emoji],
};

Expand Down
8 changes: 8 additions & 0 deletions community/src/api_v0/message/schema.ts
Original file line number Diff line number Diff line change
Expand Up @@ -49,12 +49,20 @@ export const reactionMemberSchema = z.object({
email: z.string().nullable(),
memberId: z.string().nullable(),
memberName: z.string().nullable(),
memberStatus: z.enum(["pending", "active", "rejected", "withdrawn"]).nullable(),
displayGrade: z.string().nullable(),
studentId: z.string().nullable(),
studentEmail: z.string().nullable(),
emergencyContact: z.string().nullable(),
insurance: z.boolean().nullable(),
someAllergy: z.boolean().nullable(),
allergyDetails: z.string().nullable(),
skills: z.array(z.string()),
interests: z.array(z.string()),
currentActivities: z.string().nullable(),
bio: z.string().nullable(),
discordNickname: z.string().nullable(),
discordRoles: z.array(z.string()),
reactions: z.array(z.string()),
}).openapi("ReactionMember")

Expand Down
20 changes: 18 additions & 2 deletions community/src/api_v0/message/service.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -36,7 +36,7 @@ const createContext = (overrides: {
const community = overrides.community ?? {
sendMessage: async () => ({ messageId: '323456789012345678' }),
};
const db = overrides.db ?? {
const transactionDatabase = overrides.db ?? {
insert: () => ({
values: (values: unknown) => {
valuesCalls.push(values);
Expand All @@ -52,6 +52,17 @@ const createContext = (overrides: {
},
}),
};
let openTransactions = 0;
const db = {
transaction: async (operation: (tx: unknown) => Promise<unknown>) => {
openTransactions += 1;
try {
return await operation(transactionDatabase);
} finally {
openTransactions -= 1;
}
},
};

return {
c: {
Expand All @@ -72,19 +83,24 @@ const createContext = (overrides: {
} as any,
jsonCalls,
valuesCalls,
isTransactionOpen: () => openTransactions > 0,
};
};

test('createMessageService sends an event notification to Discord and saves the event message', async () => {
const sendMessageCalls: unknown[] = [];
const { c, valuesCalls } = createContext({
let isTransactionOpen = () => false;
const testContext = createContext({
community: {
sendMessage: async (input: unknown) => {
assert.equal(isTransactionOpen(), false);
sendMessageCalls.push(input);
return { messageId: '323456789012345678' };
},
},
});
const { c, valuesCalls } = testContext;
isTransactionOpen = testContext.isTransactionOpen;

const response = await (createMessageService as any)(c);

Expand Down
Loading
Loading