feat: 플레이 감사 장수 도시 상세와 현재 예약 조회 구현
This commit is contained in:
@@ -0,0 +1,138 @@
|
||||
import { z } from 'zod';
|
||||
import { auditProcedure, findAuditMonth, readAudit, readAuditWorld, zAuditMonth } from './shared.js';
|
||||
import {
|
||||
citySelect,
|
||||
generalSelect,
|
||||
projectCurrentCity,
|
||||
projectCurrentGeneral,
|
||||
zAuditCityData,
|
||||
zAuditGeneralData,
|
||||
} from './projection.js';
|
||||
|
||||
const identity = z.object({ id: z.number(), name: z.string() });
|
||||
const zDetail = z.object({ id: z.number().int().nonnegative(), at: zAuditMonth.optional() }).strict();
|
||||
|
||||
export const generalDetail = auditProcedure.input(zDetail).query(({ ctx, input }) =>
|
||||
readAudit(ctx, async (tx) => {
|
||||
const world = await readAuditWorld(tx);
|
||||
if (input.at) {
|
||||
const sample = await findAuditMonth(tx, world, input.at);
|
||||
const row = sample
|
||||
? await tx.playAuditGeneral.findUnique({
|
||||
where: { sampleId_generalId: { sampleId: sample.id, generalId: input.id } },
|
||||
select: { data: true },
|
||||
})
|
||||
: null;
|
||||
const general = row ? zAuditGeneralData.parse(row.data) : null;
|
||||
const nation =
|
||||
general && sample
|
||||
? await tx.playAuditNation.findUnique({
|
||||
where: { sampleId_nationId: { sampleId: sample.id, nationId: general.nationId } },
|
||||
select: { data: true },
|
||||
})
|
||||
: null;
|
||||
const city =
|
||||
general && sample
|
||||
? await tx.playAuditCity.findUnique({
|
||||
where: { sampleId_cityId: { sampleId: sample.id, cityId: general.cityId } },
|
||||
select: { data: true },
|
||||
})
|
||||
: null;
|
||||
return {
|
||||
...world,
|
||||
sample,
|
||||
collected: Boolean(sample),
|
||||
general,
|
||||
nation: nation ? identity.parse(nation.data) : null,
|
||||
city: city ? identity.parse(city.data) : null,
|
||||
};
|
||||
}
|
||||
const row = await tx.general.findUnique({ where: { id: input.id }, select: generalSelect });
|
||||
const general = row ? projectCurrentGeneral(row) : null;
|
||||
const nation = general
|
||||
? await tx.nation.findUnique({ where: { id: general.nationId }, select: { id: true, name: true } })
|
||||
: null;
|
||||
const city = general
|
||||
? await tx.city.findUnique({ where: { id: general.cityId }, select: { id: true, name: true } })
|
||||
: null;
|
||||
return { ...world, sample: null, collected: true, general, nation, city };
|
||||
})
|
||||
);
|
||||
|
||||
export const cityDetail = auditProcedure.input(zDetail).query(({ ctx, input }) =>
|
||||
readAudit(ctx, async (tx) => {
|
||||
const world = await readAuditWorld(tx);
|
||||
if (input.at) {
|
||||
const sample = await findAuditMonth(tx, world, input.at);
|
||||
const row = sample
|
||||
? await tx.playAuditCity.findUnique({
|
||||
where: { sampleId_cityId: { sampleId: sample.id, cityId: input.id } },
|
||||
select: { data: true },
|
||||
})
|
||||
: null;
|
||||
const city = row ? zAuditCityData.parse(row.data) : null;
|
||||
const nation =
|
||||
city && sample
|
||||
? await tx.playAuditNation.findUnique({
|
||||
where: { sampleId_nationId: { sampleId: sample.id, nationId: city.nationId } },
|
||||
select: { data: true },
|
||||
})
|
||||
: null;
|
||||
return {
|
||||
...world,
|
||||
sample,
|
||||
collected: Boolean(sample),
|
||||
city,
|
||||
nation: nation ? identity.parse(nation.data) : null,
|
||||
};
|
||||
}
|
||||
const row = await tx.city.findUnique({ where: { id: input.id }, select: citySelect });
|
||||
const city = row ? projectCurrentCity(row) : null;
|
||||
const nation = city
|
||||
? await tx.nation.findUnique({ where: { id: city.nationId }, select: { id: true, name: true } })
|
||||
: null;
|
||||
return { ...world, sample: null, collected: true, city, nation };
|
||||
})
|
||||
);
|
||||
|
||||
// 현재 예약은 과거 표본과 분리한다. 잘못된 slot도 숨기지 않고 페이지로 조회한다.
|
||||
export const generalTurns = auditProcedure
|
||||
.input(
|
||||
z
|
||||
.object({
|
||||
generalId: z.number().int().nonnegative(),
|
||||
cursor: z.number().int().optional(),
|
||||
limit: z.number().int().min(1).max(200).default(50),
|
||||
})
|
||||
.strict()
|
||||
)
|
||||
.query(({ ctx, input }) =>
|
||||
readAudit(ctx, async (tx) => {
|
||||
const world = await readAuditWorld(tx);
|
||||
const general = await tx.general.findUnique({ where: { id: input.generalId }, select: { id: true } });
|
||||
const rows = general
|
||||
? await tx.generalTurn.findMany({
|
||||
where: {
|
||||
generalId: input.generalId,
|
||||
turnIdx: input.cursor === undefined ? undefined : { gt: input.cursor },
|
||||
},
|
||||
orderBy: { turnIdx: 'asc' },
|
||||
take: input.limit + 1,
|
||||
select: { turnIdx: true, actionCode: true, arg: true },
|
||||
})
|
||||
: [];
|
||||
return {
|
||||
...world,
|
||||
currentOnly: true,
|
||||
generalExists: Boolean(general),
|
||||
items: rows
|
||||
.slice(0, input.limit)
|
||||
.map((row) => ({
|
||||
turnIdx: row.turnIdx,
|
||||
actionCode: row.actionCode,
|
||||
argumentJson: JSON.stringify(row.arg),
|
||||
})),
|
||||
nextCursor: rows.length > input.limit ? rows[input.limit - 1]!.turnIdx : null,
|
||||
};
|
||||
})
|
||||
);
|
||||
@@ -1,4 +1,5 @@
|
||||
import { nationSeries, zAuditNation } from './nationSeries.js';
|
||||
import { cityDetail, generalDetail, generalTurns } from './details.js';
|
||||
import { z } from 'zod';
|
||||
import { canReadPlayAuditAccounts } from '@sammo-ts/common';
|
||||
import { router } from '../../trpc.js';
|
||||
@@ -21,6 +22,9 @@ import {
|
||||
} from './projection.js';
|
||||
|
||||
export const playAuditRouter = router({
|
||||
cityDetail,
|
||||
generalDetail,
|
||||
generalTurns,
|
||||
nationSeries,
|
||||
nationSnapshot: auditProcedure
|
||||
.input(z.object({ nationId: z.number().int().nonnegative(), at: zAuditMonth }).strict())
|
||||
|
||||
@@ -2197,23 +2197,90 @@ integration('game API security over HTTP transport', () => {
|
||||
kind: 'MONTH_END',
|
||||
settlementsComplete: true,
|
||||
hash: 'http-fixture',
|
||||
cities: {
|
||||
create: {
|
||||
cityId: 99123,
|
||||
nationId: ownerNationId,
|
||||
data: {
|
||||
id: 99123,
|
||||
name: '과거도시',
|
||||
nationId: ownerNationId,
|
||||
level: 4,
|
||||
state: 0,
|
||||
population: 100,
|
||||
populationMax: 200,
|
||||
agriculture: 10,
|
||||
agricultureMax: 20,
|
||||
commerce: 10,
|
||||
commerceMax: 20,
|
||||
security: 10,
|
||||
securityMax: 20,
|
||||
wall: 10,
|
||||
wallMax: 20,
|
||||
defence: 10,
|
||||
defenceMax: 20,
|
||||
supplyState: 1,
|
||||
frontState: 0,
|
||||
trust: 80,
|
||||
},
|
||||
},
|
||||
},
|
||||
generals: {
|
||||
create: {
|
||||
generalId,
|
||||
nationId: current.nationId,
|
||||
cityId: current.cityId,
|
||||
cityId: 99123,
|
||||
npcState: current.npcState,
|
||||
data: { ...past, name: '과거이름', hiddenSecret: 'must-not-expose' },
|
||||
data: { ...past, cityId: 99123, name: '과거이름', hiddenSecret: 'must-not-expose' },
|
||||
},
|
||||
},
|
||||
},
|
||||
});
|
||||
const admin = await token([`admin.playAudit.read:${profileName}`]);
|
||||
const beforeInputs = await db.inputEvent.count();
|
||||
expect((await get('generalDetail', admin, { id: generalId })).body).toMatchObject({
|
||||
result: { data: { collected: true, general: { id: generalId, name: current.name } } },
|
||||
});
|
||||
expect(
|
||||
(await get('generalDetail', admin, { id: generalId, at: { year: 190, month: 1 } })).body
|
||||
).toMatchObject({
|
||||
result: {
|
||||
data: { collected: true, general: { name: '과거이름' }, city: { id: 99123, name: '과거도시' } },
|
||||
},
|
||||
});
|
||||
expect(
|
||||
(await get('generalDetail', admin, { id: generalId, at: { year: 190, month: 2 } })).body
|
||||
).toMatchObject({ result: { data: { collected: false, general: null } } });
|
||||
expect((await get('generalDetail', await token(['admin']), { id: generalId })).status).toBe(403);
|
||||
expect((await get('cityDetail', admin, { id: 99123, at: { year: 190, month: 1 } })).body).toMatchObject({
|
||||
result: { data: { collected: true, city: { id: 99123, name: '과거도시', population: 100 } } },
|
||||
});
|
||||
await db.generalTurn.createMany({
|
||||
data: [9001, 9002].map((turnIdx) => ({
|
||||
generalId,
|
||||
turnIdx,
|
||||
actionCode: '휴식',
|
||||
arg: { fixture: turnIdx },
|
||||
})),
|
||||
});
|
||||
expect((await get('generalTurns', admin, { generalId, cursor: 9000, limit: 1 })).body).toMatchObject({
|
||||
result: {
|
||||
data: {
|
||||
currentOnly: true,
|
||||
items: [{ turnIdx: 9001, actionCode: '휴식', argumentJson: '{"fixture":9001}' }],
|
||||
nextCursor: 9001,
|
||||
},
|
||||
},
|
||||
});
|
||||
expect((await get('generalTurns', admin, { generalId, cursor: 9001, limit: 1 })).body).toMatchObject({
|
||||
result: { data: { items: [{ turnIdx: 9002 }], nextCursor: null } },
|
||||
});
|
||||
expect((await get('generalTurns', admin, { generalId, at: { year: 190, month: 1 } })).status).toBe(400);
|
||||
expect((await get('generalTurns', admin, { generalId, limit: 201 })).status).toBe(400);
|
||||
expect((await get('capabilities')).status).toBe(401);
|
||||
for (const roles of [['user'], ['admin'], ['admin.audit.read'], ['admin.playAudit.read:other:default']]) {
|
||||
expect((await get('capabilities', await token(roles))).status).toBe(403);
|
||||
}
|
||||
const admin = await token([`admin.playAudit.read:${profileName}`]);
|
||||
expect(await db.general.findUnique({ where: { userId: auditUserId } })).toBeNull();
|
||||
expect((await get('capabilities', admin)).body).toMatchObject({
|
||||
result: { data: { read: true, accounts: false } },
|
||||
@@ -2446,6 +2513,7 @@ integration('game API security over HTTP transport', () => {
|
||||
await expect.poll(async () => (await get('capabilities', admin)).status).toBe(401);
|
||||
} finally {
|
||||
await db.playAuditMonth.deleteMany({ where: { serverId: seasonId } });
|
||||
await db.generalTurn.deleteMany({ where: { generalId, turnIdx: { in: [9001, 9002] } } });
|
||||
await db.nation.deleteMany({ where: { id: { in: [99121, 99122] } } });
|
||||
await db.worldState.update({
|
||||
where: { id: fixtureWorldId },
|
||||
|
||||
Reference in New Issue
Block a user