Improve membership flow, play library, and mobile tactics board
This commit is contained in:
@@ -18,6 +18,7 @@ function iso(value) { return value instanceof Date ? value.toISOString() : new D
|
||||
function safeText(value, fallback = '') { return String(value ?? '').trim() || fallback; }
|
||||
function publicUser(row) { return { id: row.id, email: row.email, displayName: row.displayName, status: row.status, isOperator: Boolean(row.isOperator) }; }
|
||||
function publicTeam(row) { return { id: row.id, name: row.name, ownerUserId: row.ownerUserId, role: row.role, createdAt: iso(row.createdAt), updatedAt: iso(row.updatedAt) }; }
|
||||
function publicJoinRequest(row) { return { id: row.id, teamId: row.teamId, userId: row.userId, status: row.status, role: row.role || null, createdAt: iso(row.createdAt), updatedAt: iso(row.updatedAt) }; }
|
||||
function errorPayload(code, message) { return { error: { code, message } }; }
|
||||
function fail(reply, status, code, message) { return reply.code(status).send(errorPayload(code, message)); }
|
||||
function parsePositiveId(value) { return typeof value === 'string' && /^[A-Za-z0-9][A-Za-z0-9._:-]{0,127}$/.test(value); }
|
||||
@@ -159,6 +160,76 @@ export async function buildServer(options = {}) {
|
||||
return reply.code(201).send({ team: publicTeam({ id, name, ownerUserId: request.user.id, role: 'owner', createdAt, updatedAt: createdAt }) });
|
||||
});
|
||||
|
||||
app.get('/api/teams/search', { preHandler: requireUser }, async (request) => {
|
||||
const query = safeText(request.query?.q).slice(0, 60);
|
||||
if (query.length < 2) return { teams: [] };
|
||||
const escaped = query.replace(/[.*+?^${}()|[\]\\]/g, '\\$&');
|
||||
const rows = await store.teams.find({ name: { $regex: escaped, $options: 'i' } }, { projection: { _id: 0, id: 1, name: 1 } }).sort({ name: 1 }).limit(20).toArray();
|
||||
return { teams: rows.slice(0, 20).map((team) => ({ id: team.id, name: team.name })) };
|
||||
});
|
||||
|
||||
app.get('/api/team-requests', { preHandler: requireUser }, async (request) => {
|
||||
const rows = await store.joinRequests.find({ userId: request.user.id }).sort({ updatedAt: -1 }).toArray();
|
||||
const teams = await store.teams.find({ id: { $in: rows.map((row) => row.teamId) } }, { projection: { _id: 0, id: 1, name: 1 } }).toArray();
|
||||
const names = new Map(teams.map((team) => [team.id, team.name]));
|
||||
return { requests: rows.map((row) => ({ ...publicJoinRequest(row), teamName: names.get(row.teamId) || '삭제된 팀' })) };
|
||||
});
|
||||
|
||||
app.post('/api/teams/:teamId/join-requests', { preHandler: requireUser, schema: { body: bodySchema({}, []) } }, async (request, reply) => {
|
||||
if (!parsePositiveId(request.params.teamId)) return fail(reply, 400, 'invalid_team', '팀 식별자가 올바르지 않습니다');
|
||||
const team = await store.teams.findOne({ id: request.params.teamId }); if (!team) return fail(reply, 404, 'team_not_found', '팀을 찾을 수 없습니다');
|
||||
if (await store.teamMembers.findOne({ teamId: team.id, userId: request.user.id })) return fail(reply, 409, 'already_member', '이미 팀에 속해 있습니다');
|
||||
const existing = await store.joinRequests.findOne({ teamId: team.id, userId: request.user.id });
|
||||
if (existing) {
|
||||
if (existing.status === 'pending' || existing.status === 'approved') return { request: publicJoinRequest(existing), existing: true };
|
||||
const at = nowDate();
|
||||
await store.joinRequests.updateOne({ id: existing.id, teamId: team.id, userId: request.user.id }, { $set: { status: 'pending', role: null, updatedAt: at } });
|
||||
return { request: publicJoinRequest({ ...existing, status: 'pending', role: null, updatedAt: at }), existing: true };
|
||||
}
|
||||
const at = nowDate(); const row = { id: randomUUID(), teamId: team.id, userId: request.user.id, status: 'pending', role: null, createdAt: at, updatedAt: at };
|
||||
try { await store.joinRequests.insertOne(row); } catch (error) { if (error?.code === 11000) { const current = await store.joinRequests.findOne({ teamId: team.id, userId: request.user.id }); return { request: publicJoinRequest(current), existing: true }; } throw error; }
|
||||
return reply.code(201).send({ request: publicJoinRequest(row), existing: false });
|
||||
});
|
||||
|
||||
app.get('/api/teams/:teamId/members', { preHandler: requireTeam('owner') }, async (request) => {
|
||||
const members = await store.teamMembers.find({ teamId: request.params.teamId }).toArray();
|
||||
const users = await store.users.find({ id: { $in: members.map((member) => member.userId) } }).toArray();
|
||||
const byId = new Map(users.map((user) => [user.id, user]));
|
||||
return { members: members.map((member) => ({ user: publicUser(byId.get(member.userId) || { id: member.userId, email: '', displayName: member.userId, status: 'unknown', isOperator: false }), role: member.role, createdAt: iso(member.createdAt) })) };
|
||||
});
|
||||
|
||||
app.get('/api/teams/:teamId/join-requests', { preHandler: requireTeam('owner') }, async (request) => {
|
||||
const rows = await store.joinRequests.find({ teamId: request.params.teamId, status: 'pending' }).sort({ createdAt: 1 }).toArray();
|
||||
const users = await store.users.find({ id: { $in: rows.map((row) => row.userId) } }).toArray(); const byId = new Map(users.map((user) => [user.id, user]));
|
||||
return { requests: rows.map((row) => ({ ...publicJoinRequest(row), user: publicUser(byId.get(row.userId) || { id: row.userId, email: '', displayName: row.userId, status: 'unknown', isOperator: false }) })) };
|
||||
});
|
||||
|
||||
app.post('/api/teams/:teamId/join-requests/:requestId/approve', { preHandler: requireTeam('owner'), schema: { body: bodySchema({ role: { type: 'string', enum: ['viewer', 'editor'] } }, []) } }, async (request, reply) => {
|
||||
if (request.team.ownerUserId !== request.user.id) return fail(reply, 403, 'team_forbidden', '팀 소유자만 가입 요청을 승인할 수 있습니다');
|
||||
const requestedRole = request.body?.role || 'viewer'; const requestRow = await store.joinRequests.findOne({ id: request.params.requestId, teamId: request.params.teamId });
|
||||
if (requestRow?.status === 'approved') {
|
||||
const existingMember = await store.teamMembers.findOne({ teamId: request.params.teamId, userId: requestRow.userId });
|
||||
if (existingMember) return { ok: true, existing: true, member: { user: publicUser(await store.users.findOne({ id: requestRow.userId }) || { id: requestRow.userId, email: '', displayName: requestRow.userId, status: 'unknown', isOperator: false }), role: existingMember.role } };
|
||||
}
|
||||
if (requestRow?.status !== 'pending') return fail(reply, 404, 'request_not_found', '가입 요청을 찾을 수 없습니다');
|
||||
const applicant = await store.users.findOne({ id: requestRow.userId, status: 'approved' });
|
||||
if (!applicant) return fail(reply, 409, 'applicant_not_approved', '승인된 계정만 팀에 추가할 수 있습니다');
|
||||
const at = nowDate();
|
||||
let memberRole = requestedRole;
|
||||
try { await store.teamMembers.insertOne({ teamId: request.params.teamId, userId: requestRow.userId, role: requestedRole, createdAt: at }); }
|
||||
catch (error) { if (error?.code !== 11000) throw error; const existing = await store.teamMembers.findOne({ teamId: request.params.teamId, userId: requestRow.userId }); memberRole = existing?.role || requestedRole; }
|
||||
await store.joinRequests.updateOne({ id: requestRow.id, status: 'pending' }, { $set: { status: 'approved', role: memberRole, updatedAt: at } });
|
||||
return { ok: true, member: { user: publicUser(applicant), role: memberRole } };
|
||||
});
|
||||
|
||||
app.patch('/api/teams/:teamId/members/:userId', { preHandler: requireTeam('owner'), schema: { body: bodySchema({ role: { type: 'string', enum: ['viewer', 'editor'] } }, ['role']) } }, async (request, reply) => {
|
||||
if (request.team.ownerUserId !== request.user.id) return fail(reply, 403, 'team_forbidden', '팀 소유자만 멤버 권한을 변경할 수 있습니다');
|
||||
const userId = request.params.userId; if (userId === request.user.id) return fail(reply, 400, 'owner_immutable', '소유자 권한은 변경할 수 없습니다');
|
||||
const member = await store.teamMembers.findOne({ teamId: request.params.teamId, userId }); if (!member) return fail(reply, 404, 'member_not_found', '팀 멤버를 찾을 수 없습니다');
|
||||
if (member.role === 'owner') return fail(reply, 400, 'owner_immutable', '소유자 권한은 변경할 수 없습니다');
|
||||
await store.teamMembers.updateOne({ teamId: request.params.teamId, userId }, { $set: { role: request.body.role, updatedAt: nowDate() } }); return { ok: true, userId, role: request.body.role };
|
||||
});
|
||||
|
||||
app.get('/api/teams/:teamId', { preHandler: requireTeam('viewer') }, async (request) => ({ team: publicTeam(request.team) }));
|
||||
|
||||
app.get('/api/teams/:teamId/plays', { preHandler: requireTeam('viewer') }, async (request) => {
|
||||
|
||||
+51
-4
@@ -14,6 +14,7 @@ function matches(document, filter) {
|
||||
if ('$gt' in expected && !(actual > expected.$gt)) return false;
|
||||
if ('$lte' in expected && !(actual <= expected.$lte)) return false;
|
||||
if ('$in' in expected && !expected.$in.includes(actual)) return false;
|
||||
if ('$regex' in expected && !(new RegExp(expected.$regex, expected.$options || '')).test(String(actual ?? ''))) return false;
|
||||
return true;
|
||||
}
|
||||
return actual === expected;
|
||||
@@ -26,7 +27,7 @@ function project(document, projection = {}) {
|
||||
if (projection._id === 0) delete result._id;
|
||||
return result;
|
||||
}
|
||||
function collection() {
|
||||
function collection(uniqueKeys = []) {
|
||||
const documents = [];
|
||||
return {
|
||||
documents,
|
||||
@@ -34,16 +35,16 @@ function collection() {
|
||||
async findOne(filter, options) { const document = documents.find((item) => matches(item, filter)); return document ? project(document, options?.projection) : null; },
|
||||
find(filter, options) {
|
||||
let result = documents.filter((item) => matches(item, filter)).map((item) => project(item, options?.projection));
|
||||
return { sort(spec) { result.sort((left, right) => { for (const [key, direction] of Object.entries(spec)) { const a = left[key]; const b = right[key]; if (a === b) continue; return (a > b ? 1 : -1) * direction; } return 0; }); return this; }, toArray: async () => result };
|
||||
return { sort(spec) { result.sort((left, right) => { for (const [key, direction] of Object.entries(spec)) { const a = left[key]; const b = right[key]; if (a === b) continue; return (a > b ? 1 : -1) * direction; } return 0; }); return this; }, limit(count) { result = result.slice(0, count); return this; }, toArray: async () => result };
|
||||
},
|
||||
async insertOne(document) { if (documents.some((item) => item.email && item.email === document.email)) { const error = new Error('duplicate'); error.code = 11000; throw error; } documents.push(clone(document)); return { acknowledged: true }; },
|
||||
async insertOne(document) { if (documents.some((item) => uniqueKeys.some((keys) => keys.every((key) => item[key] !== undefined && item[key] === document[key])))) { const error = new Error('duplicate'); error.code = 11000; throw error; } documents.push(clone(document)); return { acknowledged: true }; },
|
||||
async updateOne(filter, update) { const document = documents.find((item) => matches(item, filter)); if (!document) return { modifiedCount: 0 }; Object.assign(document, clone(update.$set || {})); return { modifiedCount: 1 }; },
|
||||
async findOneAndUpdate(filter, update, options) { let document = documents.find((item) => matches(item, filter)); let upserted = false; if (!document && options?.upsert) { document = { ...clone(filter), ...clone(update.$setOnInsert || {}) }; documents.push(document); upserted = true; } if (!document) return null; Object.assign(document, clone(update.$set || {})); const value = clone(document); return options?.includeResultMetadata ? { value, lastErrorObject: upserted ? { upserted: document.id } : {} } : value; },
|
||||
async deleteOne(filter) { const index = documents.findIndex((item) => matches(item, filter)); if (index < 0) return { deletedCount: 0 }; documents.splice(index, 1); return { deletedCount: 1 }; },
|
||||
async deleteMany(filter) { let deletedCount = 0; for (let index = documents.length - 1; index >= 0; index -= 1) if (matches(documents[index], filter)) { documents.splice(index, 1); deletedCount += 1; } return { deletedCount }; },
|
||||
};
|
||||
}
|
||||
function memoryStore() { return { users: collection(), sessions: collection(), teams: collection(), teamMembers: collection(), plays: collection(), async close() {} }; }
|
||||
function memoryStore() { return { users: collection([['email']]), sessions: collection([['tokenHash']]), teams: collection(), teamMembers: collection([['teamId', 'userId']]), joinRequests: collection([['teamId', 'userId']]), plays: collection([['teamId', 'id']]), async close() {} }; }
|
||||
function testConfig() { return { mongodb: { uri: 'mongodb://172.16.0.7:27017', db: 'basket_utils' }, server: { host: '0.0.0.0', port: 3000, mode: 'development', allowedOrigins: ['http://localhost:5173', 'http://127.0.0.1:5173', 'http://192.168.5.10:5173'], secureCookie: false }, bootstrap: { password: '' }, qa: { db: 'basket_utils_qa', password: '' } }; }
|
||||
async function seedUser(store, { id, email, displayName, isOperator = false, status = 'approved', password = 'correct horse battery staple' }) {
|
||||
const at = new Date(); await store.users.insertOne({ id, email, displayName, passwordHash: await hashPassword(password), status, isOperator, createdAt: at, updatedAt: at }); return { id, email, password };
|
||||
@@ -87,6 +88,52 @@ describe('Fastify Mongo contract', () => {
|
||||
expect((await app.inject({ method: 'POST', url: '/api/admin/users/viewer/suspend', headers: { cookie: operator.cookie }, payload: {} })).statusCode).toBe(200); expect((await app.inject({ method: 'GET', url: '/api/teams', headers: { cookie: viewer.cookie } })).statusCode).toBe(401);
|
||||
});
|
||||
|
||||
it('completes registration, operator approval, join approval, and role changes', async () => {
|
||||
const store = memoryStore(); await seedUser(store, { id: 'owner', email: 'owner@example.com', displayName: 'Owner' }); await seedUser(store, { id: 'operator', email: 'operator@example.com', displayName: 'Operator', isOperator: true });
|
||||
app = await buildServer({ config: testConfig(), db: store, serveStatic: false, secureCookie: false });
|
||||
const registered = await app.inject({ method: 'POST', url: '/api/auth/register', payload: { email: 'new@example.com', password: 'password123', displayName: 'New User' } }); expect(registered.statusCode).toBe(201); expect(registered.json().user.status).toBe('pending');
|
||||
const operator = await login(app, 'operator@example.com', 'correct horse battery staple'); const userId = registered.json().user.id;
|
||||
expect((await app.inject({ method: 'POST', url: `/api/admin/users/${userId}/approve`, headers: { cookie: operator.cookie }, payload: {} })).statusCode).toBe(200);
|
||||
const owner = await login(app, 'owner@example.com', 'correct horse battery staple'); const team = (await app.inject({ method: 'POST', url: '/api/teams', headers: { cookie: owner.cookie }, payload: { name: 'North [A]' } })).json().team;
|
||||
const applicant = await login(app, 'new@example.com', 'password123'); const search = await app.inject({ method: 'GET', url: '/api/teams/search?q=North%20%5BA%5D', headers: { cookie: applicant.cookie } }); expect(search.statusCode).toBe(200); expect(search.json().teams).toEqual([{ id: team.id, name: team.name }]);
|
||||
const request = await app.inject({ method: 'POST', url: `/api/teams/${team.id}/join-requests`, headers: { cookie: applicant.cookie }, payload: {} }); expect(request.statusCode).toBe(201); const requestId = request.json().request.id;
|
||||
const repeatedRequest = await app.inject({ method: 'POST', url: `/api/teams/${team.id}/join-requests`, headers: { cookie: applicant.cookie }, payload: {} }); expect(repeatedRequest.statusCode).toBe(200); expect(repeatedRequest.json().request.id).toBe(requestId);
|
||||
const ownPendingRequests = (await app.inject({ method: 'GET', url: '/api/team-requests', headers: { cookie: applicant.cookie } })).json().requests; expect(ownPendingRequests[0].status).toBe('pending'); expect(ownPendingRequests[0].teamName).toBe('North [A]');
|
||||
expect((await app.inject({ method: 'GET', url: `/api/teams/${team.id}/join-requests`, headers: { cookie: owner.cookie } })).json().requests).toHaveLength(1);
|
||||
const approved = await app.inject({ method: 'POST', url: `/api/teams/${team.id}/join-requests/${requestId}/approve`, headers: { cookie: owner.cookie }, payload: { role: 'editor' } }); expect(approved.statusCode).toBe(200); expect(approved.json().member.role).toBe('editor');
|
||||
const repeatedApproval = await app.inject({ method: 'POST', url: `/api/teams/${team.id}/join-requests/${requestId}/approve`, headers: { cookie: owner.cookie }, payload: { role: 'viewer' } }); expect(repeatedApproval.statusCode).toBe(200); expect(repeatedApproval.json().member.role).toBe('editor'); expect(store.joinRequests.documents.find((row) => row.id === requestId).role).toBe('editor');
|
||||
expect((await app.inject({ method: 'PATCH', url: `/api/teams/${team.id}/members/${userId}`, headers: { cookie: applicant.cookie }, payload: { role: 'editor' } })).statusCode).toBe(403);
|
||||
const editorPlay = createInitialPlay('Editor play'); expect((await app.inject({ method: 'PUT', url: `/api/teams/${team.id}/plays/${editorPlay.id}`, headers: { cookie: applicant.cookie }, payload: { play: editorPlay } })).statusCode).toBe(201);
|
||||
expect((await app.inject({ method: 'PATCH', url: `/api/teams/${team.id}/members/${userId}`, headers: { cookie: owner.cookie }, payload: { role: 'viewer' } })).statusCode).toBe(200);
|
||||
expect((await app.inject({ method: 'GET', url: '/api/teams', headers: { cookie: applicant.cookie } })).json().teams.map((entry) => entry.id)).toContain(team.id);
|
||||
expect((await app.inject({ method: 'GET', url: `/api/teams/${team.id}/members`, headers: { cookie: owner.cookie } })).json().members.map((entry) => entry.user.id)).toContain(userId);
|
||||
const ownApprovedRequests = (await app.inject({ method: 'GET', url: '/api/team-requests', headers: { cookie: applicant.cookie } })).json().requests; expect(ownApprovedRequests[0].status).toBe('approved'); expect(ownApprovedRequests[0].teamName).toBe('North [A]');
|
||||
expect((await app.inject({ method: 'PUT', url: `/api/teams/${team.id}/plays/${editorPlay.id}`, headers: { cookie: applicant.cookie }, payload: { play: editorPlay } })).statusCode).toBe(403);
|
||||
expect((await app.inject({ method: 'PATCH', url: `/api/teams/${team.id}/members/${userId}`, headers: { cookie: owner.cookie }, payload: { role: 'owner' } })).statusCode).toBe(400);
|
||||
expect((await app.inject({ method: 'PATCH', url: `/api/teams/${team.id}/members/${userId}`, headers: { cookie: owner.cookie }, payload: { role: 'bad' } })).statusCode).toBe(400);
|
||||
});
|
||||
|
||||
it('keeps teams isolated and refuses suspended applicants and nonmembers', async () => {
|
||||
const store = memoryStore(); await seedUser(store, { id: 'owner', email: 'owner@example.com', displayName: 'Owner' }); await seedUser(store, { id: 'applicant', email: 'applicant@example.com', displayName: 'Applicant' }); await seedUser(store, { id: 'viewer', email: 'viewer@example.com', displayName: 'Viewer' });
|
||||
app = await buildServer({ config: testConfig(), db: store, serveStatic: false, secureCookie: false }); const owner = await login(app, 'owner@example.com', 'correct horse battery staple'); const applicant = await login(app, 'applicant@example.com', 'correct horse battery staple'); const viewer = await login(app, 'viewer@example.com', 'correct horse battery staple');
|
||||
const first = (await app.inject({ method: 'POST', url: '/api/teams', headers: { cookie: owner.cookie }, payload: { name: 'First Team' } })).json().team; const second = (await app.inject({ method: 'POST', url: '/api/teams', headers: { cookie: owner.cookie }, payload: { name: 'Second Team' } })).json().team;
|
||||
const request = await app.inject({ method: 'POST', url: `/api/teams/${first.id}/join-requests`, headers: { cookie: applicant.cookie }, payload: {} }); const requestId = request.json().request.id;
|
||||
await store.users.updateOne({ id: 'applicant' }, { $set: { status: 'suspended' } }); expect((await app.inject({ method: 'POST', url: `/api/teams/${first.id}/join-requests/${requestId}/approve`, headers: { cookie: owner.cookie }, payload: {} })).statusCode).toBe(409);
|
||||
expect((await app.inject({ method: 'GET', url: `/api/teams/${first.id}`, headers: { cookie: viewer.cookie } })).statusCode).toBe(404);
|
||||
expect((await app.inject({ method: 'PATCH', url: `/api/teams/${first.id}/members/owner`, headers: { cookie: owner.cookie }, payload: { role: 'viewer' } })).statusCode).toBe(400);
|
||||
expect((await app.inject({ method: 'GET', url: `/api/teams/${second.id}/members`, headers: { cookie: viewer.cookie } })).statusCode).toBe(404);
|
||||
});
|
||||
|
||||
it('caps team search and preserves the inserted role on concurrent approval', async () => {
|
||||
const store = memoryStore(); await seedUser(store, { id: 'owner', email: 'owner@example.com', displayName: 'Owner' }); await seedUser(store, { id: 'applicant', email: 'applicant@example.com', displayName: 'Applicant' });
|
||||
app = await buildServer({ config: testConfig(), db: store, serveStatic: false, secureCookie: false }); const owner = await login(app, 'owner@example.com', 'correct horse battery staple'); const applicant = await login(app, 'applicant@example.com', 'correct horse battery staple');
|
||||
const teams = []; for (let index = 0; index < 21; index += 1) teams.push((await app.inject({ method: 'POST', url: '/api/teams', headers: { cookie: owner.cookie }, payload: { name: `Cap Team ${String(index).padStart(2, '0')}` } })).json().team);
|
||||
expect((await app.inject({ method: 'GET', url: '/api/teams/search?q=Cap', headers: { cookie: applicant.cookie } })).json().teams).toHaveLength(20);
|
||||
const request = await app.inject({ method: 'POST', url: `/api/teams/${teams[0].id}/join-requests`, headers: { cookie: applicant.cookie }, payload: {} }); const requestId = request.json().request.id;
|
||||
const approvals = await Promise.all(['viewer', 'editor'].map((role) => app.inject({ method: 'POST', url: `/api/teams/${teams[0].id}/join-requests/${requestId}/approve`, headers: { cookie: owner.cookie }, payload: { role } })));
|
||||
expect(approvals.every((response) => response.statusCode === 200)).toBe(true); const member = store.teamMembers.documents.find((row) => row.teamId === teams[0].id && row.userId === 'applicant'); expect(['viewer', 'editor']).toContain(member.role); expect(store.joinRequests.documents.find((row) => row.id === requestId).role).toBe(member.role);
|
||||
});
|
||||
|
||||
it('does not expose config files through the static handler', async () => {
|
||||
const staticRoot = await mkdtemp(path.join(os.tmpdir(), 'basket-utils-static-')); temporaryRoots.push(staticRoot);
|
||||
await writeFile(path.join(staticRoot, 'index.html'), 'safe'); await writeFile(path.join(staticRoot, '.config.json'), '{"bootstrap":{"password":"static-secret"}}');
|
||||
|
||||
+5
-1
@@ -15,6 +15,7 @@ export async function createMongoStore(options = {}) {
|
||||
const sessions = database.collection('sessions');
|
||||
const teams = database.collection('teams');
|
||||
const teamMembers = database.collection('team_members');
|
||||
const joinRequests = database.collection('team_join_requests');
|
||||
const plays = database.collection('plays');
|
||||
try {
|
||||
await Promise.all([
|
||||
@@ -23,6 +24,9 @@ export async function createMongoStore(options = {}) {
|
||||
sessions.createIndex({ expiresAt: 1 }, { expireAfterSeconds: 0 }),
|
||||
teamMembers.createIndex({ teamId: 1, userId: 1 }, { unique: true }),
|
||||
teamMembers.createIndex({ userId: 1 }),
|
||||
joinRequests.createIndex({ teamId: 1, userId: 1 }, { unique: true }),
|
||||
joinRequests.createIndex({ teamId: 1, status: 1, createdAt: 1 }),
|
||||
joinRequests.createIndex({ userId: 1, updatedAt: -1 }),
|
||||
plays.createIndex({ teamId: 1, id: 1 }, { unique: true }),
|
||||
plays.createIndex({ teamId: 1, updatedAt: -1 }),
|
||||
]);
|
||||
@@ -31,7 +35,7 @@ export async function createMongoStore(options = {}) {
|
||||
throw error;
|
||||
}
|
||||
return {
|
||||
client, database, users, sessions, teams, teamMembers, plays,
|
||||
client, database, users, sessions, teams, teamMembers, joinRequests, plays,
|
||||
async close() { if (!options.client) await client.close(); },
|
||||
};
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user