649 lines
20 KiB
TypeScript
649 lines
20 KiB
TypeScript
import { PermissionKey, SystemRole } from "@capakraken/shared";
|
|
import { describe, expect, it, vi } from "vitest";
|
|
import { clientRouter } from "../router/client.js";
|
|
import { countryRouter } from "../router/country.js";
|
|
import { managementLevelRouter } from "../router/management-level.js";
|
|
import { orgUnitRouter } from "../router/org-unit.js";
|
|
import { utilizationCategoryRouter } from "../router/utilization-category.js";
|
|
import { createCallerFactory } from "../trpc.js";
|
|
|
|
function createProtectedContext(
|
|
db: Record<string, unknown>,
|
|
overrides: { granted?: PermissionKey[]; denied?: PermissionKey[] } | null = null,
|
|
) {
|
|
return {
|
|
session: {
|
|
user: { email: "user@example.com", name: "User", image: null },
|
|
expires: "2099-01-01T00:00:00.000Z",
|
|
},
|
|
db: db as never,
|
|
dbUser: {
|
|
id: "user_1",
|
|
systemRole: SystemRole.USER,
|
|
permissionOverrides: overrides,
|
|
},
|
|
};
|
|
}
|
|
|
|
describe("master-data router authorization", () => {
|
|
it("keeps country lists available to authenticated users as safe lookup data", async () => {
|
|
const findMany = vi.fn().mockResolvedValue([
|
|
{
|
|
id: "country_de",
|
|
code: "DE",
|
|
name: "Germany",
|
|
isActive: true,
|
|
dailyWorkingHours: 8,
|
|
metroCities: [{ id: "city_ber", name: "Berlin", countryId: "country_de" }],
|
|
},
|
|
]);
|
|
const caller = createCallerFactory(countryRouter)(createProtectedContext({
|
|
country: {
|
|
findMany,
|
|
},
|
|
}));
|
|
|
|
const result = await caller.list({ isActive: true });
|
|
|
|
expect(result).toHaveLength(1);
|
|
expect(findMany).toHaveBeenCalledWith(expect.objectContaining({
|
|
where: { isActive: true },
|
|
include: { metroCities: { orderBy: { name: "asc" } } },
|
|
orderBy: { name: "asc" },
|
|
}));
|
|
});
|
|
|
|
it("keeps minimal country lookups available to authenticated users", async () => {
|
|
const findFirst = vi.fn().mockResolvedValue({
|
|
id: "country_de",
|
|
code: "DE",
|
|
name: "Germany",
|
|
isActive: true,
|
|
dailyWorkingHours: 8,
|
|
});
|
|
const caller = createCallerFactory(countryRouter)(createProtectedContext({
|
|
country: {
|
|
findUnique: vi.fn().mockResolvedValue(null),
|
|
findFirst,
|
|
},
|
|
}));
|
|
|
|
const result = await caller.resolveByIdentifier({ identifier: "de" });
|
|
|
|
expect(result).toEqual({
|
|
id: "country_de",
|
|
code: "DE",
|
|
name: "Germany",
|
|
isActive: true,
|
|
dailyWorkingHours: 8,
|
|
});
|
|
expect(findFirst).toHaveBeenCalled();
|
|
});
|
|
|
|
it("keeps metro-city lookups available to authenticated users", async () => {
|
|
const findUnique = vi.fn().mockResolvedValue({
|
|
id: "city_muc",
|
|
name: "Munich",
|
|
countryId: "country_de",
|
|
});
|
|
const caller = createCallerFactory(countryRouter)(createProtectedContext({
|
|
metroCity: {
|
|
findUnique,
|
|
},
|
|
}));
|
|
|
|
const result = await caller.getCityById({ id: "city_muc" });
|
|
|
|
expect(result).toEqual({
|
|
id: "city_muc",
|
|
name: "Munich",
|
|
countryId: "country_de",
|
|
});
|
|
expect(findUnique).toHaveBeenCalledWith({
|
|
where: { id: "city_muc" },
|
|
select: { id: true, name: true, countryId: true },
|
|
});
|
|
});
|
|
|
|
it("requires resource overview access for detailed country reads with resource counts", async () => {
|
|
const caller = createCallerFactory(countryRouter)(createProtectedContext({
|
|
country: {
|
|
findFirst: vi.fn(),
|
|
findUnique: vi.fn(),
|
|
},
|
|
}));
|
|
|
|
await expect(caller.getByIdentifier({ identifier: "DE" })).rejects.toMatchObject({
|
|
code: "FORBIDDEN",
|
|
message: "Resource overview access required",
|
|
});
|
|
await expect(caller.getById({ id: "country_de" })).rejects.toMatchObject({
|
|
code: "FORBIDDEN",
|
|
message: "Resource overview access required",
|
|
});
|
|
});
|
|
|
|
it("allows detailed country reads for users with resource overview access", async () => {
|
|
const findFirst = vi.fn().mockResolvedValue({
|
|
id: "country_de",
|
|
code: "DE",
|
|
name: "Germany",
|
|
isActive: true,
|
|
dailyWorkingHours: 8,
|
|
scheduleRules: null,
|
|
metroCities: [{ id: "city_muc", name: "Munich", countryId: "country_de" }],
|
|
_count: { resources: 12 },
|
|
});
|
|
const caller = createCallerFactory(countryRouter)(createProtectedContext({
|
|
country: {
|
|
findUnique: vi.fn().mockResolvedValue(null),
|
|
findFirst,
|
|
},
|
|
}, {
|
|
granted: [PermissionKey.VIEW_ALL_RESOURCES],
|
|
}));
|
|
|
|
const result = await caller.getByIdentifier({ identifier: "DE" });
|
|
|
|
expect(result._count.resources).toBe(12);
|
|
expect(findFirst).toHaveBeenCalledWith(expect.objectContaining({
|
|
include: expect.objectContaining({
|
|
metroCities: expect.any(Object),
|
|
_count: expect.any(Object),
|
|
}),
|
|
}));
|
|
});
|
|
|
|
it("allows detailed country reads for users with manage-resources access", async () => {
|
|
const findUnique = vi.fn().mockResolvedValue({
|
|
id: "country_de",
|
|
code: "DE",
|
|
name: "Germany",
|
|
isActive: true,
|
|
dailyWorkingHours: 8,
|
|
scheduleRules: null,
|
|
metroCities: [],
|
|
_count: { resources: 4 },
|
|
});
|
|
const caller = createCallerFactory(countryRouter)(createProtectedContext({
|
|
country: {
|
|
findUnique,
|
|
},
|
|
}, {
|
|
granted: [PermissionKey.MANAGE_RESOURCES],
|
|
}));
|
|
|
|
const result = await caller.getById({ id: "country_de" });
|
|
|
|
expect(result._count.resources).toBe(4);
|
|
expect(findUnique).toHaveBeenCalled();
|
|
});
|
|
|
|
it("keeps minimal org-unit lookups available to authenticated users", async () => {
|
|
const findFirst = vi.fn()
|
|
.mockResolvedValueOnce(null)
|
|
.mockResolvedValueOnce({
|
|
id: "ou_1",
|
|
name: "Delivery",
|
|
shortName: "DEL",
|
|
level: 5,
|
|
isActive: true,
|
|
});
|
|
const caller = createCallerFactory(orgUnitRouter)(createProtectedContext({
|
|
orgUnit: {
|
|
findUnique: vi.fn().mockResolvedValue(null),
|
|
findFirst,
|
|
},
|
|
}));
|
|
|
|
const result = await caller.resolveByIdentifier({ identifier: "DEL" });
|
|
|
|
expect(result).toEqual({
|
|
id: "ou_1",
|
|
name: "Delivery",
|
|
shortName: "DEL",
|
|
level: 5,
|
|
isActive: true,
|
|
});
|
|
});
|
|
|
|
it("requires resource overview access for org-unit list and tree reads", async () => {
|
|
const findMany = vi.fn();
|
|
const caller = createCallerFactory(orgUnitRouter)(createProtectedContext({
|
|
orgUnit: {
|
|
findMany,
|
|
},
|
|
}));
|
|
|
|
await expect(caller.list({ level: 5, isActive: true })).rejects.toMatchObject({
|
|
code: "FORBIDDEN",
|
|
message: "Resource overview access required",
|
|
});
|
|
await expect(caller.getTree({ isActive: true })).rejects.toMatchObject({
|
|
code: "FORBIDDEN",
|
|
message: "Resource overview access required",
|
|
});
|
|
|
|
expect(findMany).not.toHaveBeenCalled();
|
|
});
|
|
|
|
it("requires resource overview access for detailed org-unit reads with staffing counts", async () => {
|
|
const caller = createCallerFactory(orgUnitRouter)(createProtectedContext({
|
|
orgUnit: {
|
|
findFirst: vi.fn(),
|
|
findUnique: vi.fn(),
|
|
},
|
|
}));
|
|
|
|
await expect(caller.getByIdentifier({ identifier: "Delivery" })).rejects.toMatchObject({
|
|
code: "FORBIDDEN",
|
|
message: "Resource overview access required",
|
|
});
|
|
await expect(caller.getById({ id: "ou_1" })).rejects.toMatchObject({
|
|
code: "FORBIDDEN",
|
|
message: "Resource overview access required",
|
|
});
|
|
});
|
|
|
|
it("allows detailed org-unit reads for users with resource overview access", async () => {
|
|
const findFirst = vi.fn().mockResolvedValue({
|
|
id: "ou_1",
|
|
name: "Delivery",
|
|
shortName: "DEL",
|
|
level: 5,
|
|
parentId: null,
|
|
sortOrder: 10,
|
|
isActive: true,
|
|
_count: { resources: 7 },
|
|
});
|
|
const caller = createCallerFactory(orgUnitRouter)(createProtectedContext({
|
|
orgUnit: {
|
|
findUnique: vi.fn().mockResolvedValue(null),
|
|
findFirst,
|
|
},
|
|
}, {
|
|
granted: [PermissionKey.VIEW_ALL_RESOURCES],
|
|
}));
|
|
|
|
const result = await caller.getByIdentifier({ identifier: "Delivery" });
|
|
|
|
expect(result._count.resources).toBe(7);
|
|
expect(findFirst).toHaveBeenCalledWith(expect.objectContaining({
|
|
include: { _count: { select: { resources: true } } },
|
|
}));
|
|
});
|
|
|
|
it("allows org-unit list and tree reads for users with resource overview access", async () => {
|
|
const listFindMany = vi.fn().mockResolvedValue([
|
|
{
|
|
id: "ou_1",
|
|
name: "Delivery",
|
|
shortName: "DEL",
|
|
level: 5,
|
|
parentId: null,
|
|
sortOrder: 10,
|
|
isActive: true,
|
|
createdAt: new Date("2026-03-01T00:00:00.000Z"),
|
|
updatedAt: new Date("2026-03-01T00:00:00.000Z"),
|
|
},
|
|
]);
|
|
const treeFindMany = vi.fn().mockResolvedValue([
|
|
{
|
|
id: "ou_1",
|
|
name: "Delivery",
|
|
shortName: "DEL",
|
|
level: 5,
|
|
parentId: null,
|
|
sortOrder: 10,
|
|
isActive: true,
|
|
createdAt: new Date("2026-03-01T00:00:00.000Z"),
|
|
updatedAt: new Date("2026-03-01T00:00:00.000Z"),
|
|
},
|
|
]);
|
|
const caller = createCallerFactory(orgUnitRouter)(createProtectedContext({
|
|
orgUnit: {
|
|
findMany: vi.fn()
|
|
.mockImplementationOnce(listFindMany)
|
|
.mockImplementationOnce(treeFindMany),
|
|
},
|
|
}, {
|
|
granted: [PermissionKey.MANAGE_RESOURCES],
|
|
}));
|
|
|
|
const listResult = await caller.list({ level: 5, isActive: true });
|
|
const treeResult = await caller.getTree({ isActive: true });
|
|
|
|
expect(listResult).toHaveLength(1);
|
|
expect(treeResult).toEqual([
|
|
expect.objectContaining({
|
|
id: "ou_1",
|
|
name: "Delivery",
|
|
children: [],
|
|
}),
|
|
]);
|
|
expect(listFindMany).toHaveBeenCalledWith({
|
|
where: { level: 5, isActive: true },
|
|
orderBy: [{ level: "asc" }, { sortOrder: "asc" }, { name: "asc" }],
|
|
});
|
|
expect(treeFindMany).toHaveBeenCalledWith({
|
|
where: { isActive: true },
|
|
orderBy: [{ sortOrder: "asc" }, { name: "asc" }],
|
|
});
|
|
});
|
|
|
|
it("keeps minimal client lookups available to authenticated users", async () => {
|
|
const findUnique = vi.fn()
|
|
.mockResolvedValueOnce(null)
|
|
.mockResolvedValueOnce({
|
|
id: "client_1",
|
|
name: "Acme Studios",
|
|
code: "ACME",
|
|
parentId: null,
|
|
isActive: true,
|
|
});
|
|
const caller = createCallerFactory(clientRouter)(createProtectedContext({
|
|
client: {
|
|
findUnique,
|
|
findFirst: vi.fn(),
|
|
},
|
|
}));
|
|
|
|
const result = await caller.resolveByIdentifier({ identifier: "ACME" });
|
|
|
|
expect(result).toEqual({
|
|
id: "client_1",
|
|
name: "Acme Studios",
|
|
code: "ACME",
|
|
parentId: null,
|
|
isActive: true,
|
|
});
|
|
});
|
|
|
|
it("requires planning read access for client list and tree reads", async () => {
|
|
const findMany = vi.fn();
|
|
const caller = createCallerFactory(clientRouter)(createProtectedContext({
|
|
client: {
|
|
findMany,
|
|
},
|
|
}));
|
|
|
|
await expect(caller.list({ isActive: true })).rejects.toMatchObject({
|
|
code: "FORBIDDEN",
|
|
message: "Planning read access required",
|
|
});
|
|
await expect(caller.getTree({ isActive: true })).rejects.toMatchObject({
|
|
code: "FORBIDDEN",
|
|
message: "Planning read access required",
|
|
});
|
|
|
|
expect(findMany).not.toHaveBeenCalled();
|
|
});
|
|
|
|
it("requires planning read access for detailed client reads with project counts", async () => {
|
|
const caller = createCallerFactory(clientRouter)(createProtectedContext({
|
|
client: {
|
|
findFirst: vi.fn(),
|
|
findUnique: vi.fn(),
|
|
},
|
|
}));
|
|
|
|
await expect(caller.getByIdentifier({ identifier: "Acme" })).rejects.toMatchObject({
|
|
code: "FORBIDDEN",
|
|
message: "Planning read access required",
|
|
});
|
|
await expect(caller.getById({ id: "client_1" })).rejects.toMatchObject({
|
|
code: "FORBIDDEN",
|
|
message: "Planning read access required",
|
|
});
|
|
});
|
|
|
|
it("allows client list, tree, and detail reads for users with planning access", async () => {
|
|
const listFindMany = vi.fn().mockResolvedValue([
|
|
{
|
|
id: "client_1",
|
|
name: "Acme Studios",
|
|
code: "ACME",
|
|
parentId: null,
|
|
isActive: true,
|
|
sortOrder: 10,
|
|
tags: ["enterprise"],
|
|
createdAt: new Date("2026-03-01T00:00:00.000Z"),
|
|
updatedAt: new Date("2026-03-01T00:00:00.000Z"),
|
|
_count: { children: 1, projects: 4 },
|
|
},
|
|
]);
|
|
const treeFindMany = vi.fn().mockResolvedValue([
|
|
{
|
|
id: "client_1",
|
|
name: "Acme Studios",
|
|
code: "ACME",
|
|
parentId: null,
|
|
isActive: true,
|
|
sortOrder: 10,
|
|
tags: ["enterprise"],
|
|
createdAt: new Date("2026-03-01T00:00:00.000Z"),
|
|
updatedAt: new Date("2026-03-01T00:00:00.000Z"),
|
|
},
|
|
]);
|
|
const getByIdFindUnique = vi.fn()
|
|
.mockResolvedValueOnce({
|
|
id: "client_1",
|
|
name: "Acme Studios",
|
|
code: "ACME",
|
|
parentId: null,
|
|
isActive: true,
|
|
sortOrder: 10,
|
|
tags: ["enterprise"],
|
|
createdAt: new Date("2026-03-01T00:00:00.000Z"),
|
|
updatedAt: new Date("2026-03-01T00:00:00.000Z"),
|
|
parent: null,
|
|
children: [],
|
|
_count: { children: 1, projects: 4 },
|
|
})
|
|
.mockResolvedValueOnce(null);
|
|
const getByIdentifierFindFirst = vi.fn().mockResolvedValue({
|
|
id: "client_1",
|
|
name: "Acme Studios",
|
|
code: "ACME",
|
|
parentId: null,
|
|
isActive: true,
|
|
sortOrder: 10,
|
|
tags: ["enterprise"],
|
|
createdAt: new Date("2026-03-01T00:00:00.000Z"),
|
|
updatedAt: new Date("2026-03-01T00:00:00.000Z"),
|
|
_count: { children: 1, projects: 4 },
|
|
});
|
|
const caller = createCallerFactory(clientRouter)(createProtectedContext({
|
|
client: {
|
|
findMany: vi.fn()
|
|
.mockImplementationOnce(listFindMany)
|
|
.mockImplementationOnce(treeFindMany),
|
|
findUnique: getByIdFindUnique,
|
|
findFirst: getByIdentifierFindFirst,
|
|
},
|
|
}, {
|
|
granted: [PermissionKey.VIEW_PLANNING],
|
|
}));
|
|
|
|
const listResult = await caller.list({ isActive: true, search: "Acme" });
|
|
const treeResult = await caller.getTree({ isActive: true });
|
|
const byIdResult = await caller.getById({ id: "client_1" });
|
|
const byIdentifierResult = await caller.getByIdentifier({ identifier: "Acme Studios" });
|
|
|
|
expect(listResult).toHaveLength(1);
|
|
expect(treeResult).toEqual([
|
|
expect.objectContaining({
|
|
id: "client_1",
|
|
name: "Acme Studios",
|
|
children: [],
|
|
}),
|
|
]);
|
|
expect(byIdResult._count.projects).toBe(4);
|
|
expect(byIdentifierResult._count.projects).toBe(4);
|
|
expect(listFindMany).toHaveBeenCalledWith({
|
|
where: {
|
|
isActive: true,
|
|
OR: [
|
|
{ name: { contains: "Acme", mode: "insensitive" } },
|
|
{ code: { contains: "Acme", mode: "insensitive" } },
|
|
],
|
|
},
|
|
include: { _count: { select: { children: true, projects: true } } },
|
|
orderBy: [{ sortOrder: "asc" }, { name: "asc" }],
|
|
});
|
|
expect(treeFindMany).toHaveBeenCalledWith({
|
|
where: { isActive: true },
|
|
orderBy: [{ sortOrder: "asc" }, { name: "asc" }],
|
|
});
|
|
expect(getByIdFindUnique).toHaveBeenCalledWith({
|
|
where: { id: "client_1" },
|
|
include: {
|
|
parent: true,
|
|
children: { orderBy: { sortOrder: "asc" } },
|
|
_count: { select: { projects: true, children: true } },
|
|
},
|
|
});
|
|
expect(getByIdentifierFindFirst).toHaveBeenCalledWith(expect.objectContaining({
|
|
where: { name: { equals: "Acme Studios", mode: "insensitive" } },
|
|
include: { _count: { select: { projects: true, children: true } } },
|
|
}));
|
|
});
|
|
|
|
it("requires planning read access for utilization-category reads with project counts", async () => {
|
|
const listFindMany = vi.fn();
|
|
const getByIdFindUnique = vi.fn();
|
|
const caller = createCallerFactory(utilizationCategoryRouter)(createProtectedContext({
|
|
utilizationCategory: {
|
|
findMany: listFindMany,
|
|
findUnique: getByIdFindUnique,
|
|
},
|
|
}));
|
|
|
|
await expect(caller.list({ isActive: true })).rejects.toMatchObject({
|
|
code: "FORBIDDEN",
|
|
message: "Planning read access required",
|
|
});
|
|
await expect(caller.getById({ id: "util_chargeable" })).rejects.toMatchObject({
|
|
code: "FORBIDDEN",
|
|
message: "Planning read access required",
|
|
});
|
|
|
|
expect(listFindMany).not.toHaveBeenCalled();
|
|
expect(getByIdFindUnique).not.toHaveBeenCalled();
|
|
});
|
|
|
|
it("allows utilization-category reads for users with planning access", async () => {
|
|
const listFindMany = vi.fn().mockResolvedValue([
|
|
{
|
|
id: "util_chargeable",
|
|
code: "CHARGEABLE",
|
|
name: "Chargeable",
|
|
description: "Revenue-generating project work",
|
|
sortOrder: 1,
|
|
isDefault: true,
|
|
isActive: true,
|
|
},
|
|
]);
|
|
const getByIdFindUnique = vi.fn().mockResolvedValue({
|
|
id: "util_chargeable",
|
|
code: "CHARGEABLE",
|
|
name: "Chargeable",
|
|
description: "Revenue-generating project work",
|
|
sortOrder: 1,
|
|
isDefault: true,
|
|
isActive: true,
|
|
_count: { projects: 12 },
|
|
});
|
|
const caller = createCallerFactory(utilizationCategoryRouter)(createProtectedContext({
|
|
utilizationCategory: {
|
|
findMany: listFindMany,
|
|
findUnique: getByIdFindUnique,
|
|
},
|
|
}, {
|
|
granted: [PermissionKey.VIEW_PLANNING],
|
|
}));
|
|
|
|
const listResult = await caller.list({ isActive: true });
|
|
const byIdResult = await caller.getById({ id: "util_chargeable" });
|
|
|
|
expect(listResult).toHaveLength(1);
|
|
expect(byIdResult._count.projects).toBe(12);
|
|
expect(listFindMany).toHaveBeenCalledWith({
|
|
where: { isActive: true },
|
|
orderBy: { sortOrder: "asc" },
|
|
});
|
|
expect(getByIdFindUnique).toHaveBeenCalledWith({
|
|
where: { id: "util_chargeable" },
|
|
include: { _count: { select: { projects: true } } },
|
|
});
|
|
});
|
|
|
|
it("requires planning read access for management-level reads", async () => {
|
|
const listFindMany = vi.fn();
|
|
const getByIdFindUnique = vi.fn();
|
|
const caller = createCallerFactory(managementLevelRouter)(createProtectedContext({
|
|
managementLevelGroup: {
|
|
findMany: listFindMany,
|
|
findUnique: getByIdFindUnique,
|
|
},
|
|
}));
|
|
|
|
await expect(caller.listGroups()).rejects.toMatchObject({
|
|
code: "FORBIDDEN",
|
|
message: "Planning read access required",
|
|
});
|
|
await expect(caller.getGroupById({ id: "mgmt_group_1" })).rejects.toMatchObject({
|
|
code: "FORBIDDEN",
|
|
message: "Planning read access required",
|
|
});
|
|
|
|
expect(listFindMany).not.toHaveBeenCalled();
|
|
expect(getByIdFindUnique).not.toHaveBeenCalled();
|
|
});
|
|
|
|
it("allows management-level reads for users with planning access", async () => {
|
|
const listFindMany = vi.fn().mockResolvedValue([
|
|
{
|
|
id: "mgmt_group_1",
|
|
name: "Team Leads",
|
|
targetPercentage: 0.72,
|
|
sortOrder: 10,
|
|
levels: [{ id: "mgmt_level_1", name: "Senior Team Lead" }],
|
|
},
|
|
]);
|
|
const getByIdFindUnique = vi.fn().mockResolvedValue({
|
|
id: "mgmt_group_1",
|
|
name: "Team Leads",
|
|
targetPercentage: 0.72,
|
|
sortOrder: 10,
|
|
levels: [{ id: "mgmt_level_1", name: "Senior Team Lead" }],
|
|
_count: { resources: 6 },
|
|
});
|
|
const caller = createCallerFactory(managementLevelRouter)(createProtectedContext({
|
|
managementLevelGroup: {
|
|
findMany: listFindMany,
|
|
findUnique: getByIdFindUnique,
|
|
},
|
|
}, {
|
|
granted: [PermissionKey.VIEW_PLANNING],
|
|
}));
|
|
|
|
const listResult = await caller.listGroups();
|
|
const detailResult = await caller.getGroupById({ id: "mgmt_group_1" });
|
|
|
|
expect(listResult).toHaveLength(1);
|
|
expect(detailResult._count.resources).toBe(6);
|
|
expect(listFindMany).toHaveBeenCalledWith({
|
|
include: { levels: { orderBy: { name: "asc" } } },
|
|
orderBy: { sortOrder: "asc" },
|
|
});
|
|
expect(getByIdFindUnique).toHaveBeenCalledWith({
|
|
where: { id: "mgmt_group_1" },
|
|
include: {
|
|
levels: { orderBy: { name: "asc" } },
|
|
_count: { select: { resources: true } },
|
|
},
|
|
});
|
|
});
|
|
});
|