Combine data-access and contact list facets with OR so each selected dimension grants visibility.

Co-authored-by: Cursor <cursoragent@cursor.com>
This commit is contained in:
2026-11-12 20:15:21 +03:30
co-authored by Cursor
parent b00465439e
commit a1ac677b3a
5 changed files with 139 additions and 104 deletions
+108 -95
View File
@@ -105,27 +105,33 @@ export function resolveAllSectionAccess(input: {
return out;
}
/** ابعاد پرشدهٔ یک قانون با OR؛ خالی = null */
function unionFacets<T>(facets: T[]): T | { OR: T[] } | null {
if (!facets.length) return null;
return facets.length === 1 ? facets[0]! : { OR: facets };
}
export function buildContactRuleWhere(
rule: DataAccessRule,
ctx: { userId: string; teamPeerIds: string[] | null },
): Prisma.PersonWhereInput | null {
const and: Prisma.PersonWhereInput[] = [];
const ids = ownerIdList(rule, ctx);
const facets: Prisma.PersonWhereInput[] = [];
if (rule.owners?.length) {
if (!ids.length) return null;
and.push(ids.length === 1 ? { ownerId: ids[0]! } : { ownerId: { in: ids } });
const ids = ownerIdList(rule, ctx);
if (ids.length) {
facets.push(ids.length === 1 ? { ownerId: ids[0]! } : { ownerId: { in: ids } });
}
}
// groupIds دیگر فیلتر دسترسی نیست؛ دید گروه فقط از مسئول‌گروه (personViaGroupAssignee)
if (rule.sources?.length) {
and.push({
facets.push({
OR: [{ importSource: { in: rule.sources } }, { sourceRef: { in: rule.sources } }],
});
}
if (rule.moods?.length) {
and.push({ mood: { in: rule.moods } });
facets.push({ mood: { in: rule.moods } });
}
if (!and.length) return null;
return and.length === 1 ? and[0]! : { AND: and };
return unionFacets(facets);
}
function orParts<T>(parts: T[]): T | { OR: T[] } | { id: string } {
@@ -177,26 +183,26 @@ export function customersCompanyWhere(
if (!sectionHasDataAccessLimits(section)) return {};
const parts: Prisma.CompanyWhereInput[] = [];
for (const rule of section.rules ?? []) {
const and: Prisma.CompanyWhereInput[] = [];
// حال مشتری روی شرکت معنا ندارد؛ فقط مالک/منبع
if (rule.moods?.length && !rule.owners?.length && !rule.sources?.length) continue;
const facets: Prisma.CompanyWhereInput[] = [];
if (rule.owners?.length) {
const ids = ownerIdList(rule, {
userId: actor.userId,
teamPeerIds: actor.teamPeerIds ?? null,
});
if (!ids.length) continue;
and.push(ids.length === 1 ? { ownerId: ids[0]! } : { ownerId: { in: ids } });
if (ids.length) {
facets.push(ids.length === 1 ? { ownerId: ids[0]! } : { ownerId: { in: ids } });
}
}
// groupIds دیگر فیلتر دسترسی نیست؛ دید گروه فقط از مسئول‌گروه (companyViaGroupAssignee)
if (rule.sources?.length) {
and.push({
facets.push({
OR: [{ importSource: { in: rule.sources } }, { sourceRef: { in: rule.sources } }],
});
}
if (rule.moods?.length && !rule.owners?.length && !rule.sources?.length) {
continue;
}
if (!and.length) continue;
parts.push(and.length === 1 ? and[0]! : { AND: and });
const w = unionFacets(facets);
if (w) parts.push(w);
}
parts.push(companyViaGroupAssignee(actor.userId));
return orParts(parts);
@@ -210,27 +216,28 @@ export function leadsDeepWhere(
if (!sectionHasDataAccessLimits(section)) return {};
const parts: Prisma.LeadWhereInput[] = [];
for (const rule of section.rules ?? []) {
const and: Prisma.LeadWhereInput[] = [];
const facets: Prisma.LeadWhereInput[] = [];
if (rule.owners?.length) {
const ids = ownerIdList(rule, {
userId: actor.userId,
teamPeerIds: actor.teamPeerIds ?? null,
});
if (!ids.length) continue;
and.push(ids.length === 1 ? { assigneeId: ids[0]! } : { assigneeId: { in: ids } });
if (ids.length) {
facets.push(ids.length === 1 ? { assigneeId: ids[0]! } : { assigneeId: { in: ids } });
}
}
if (rule.stageIds?.length) {
and.push({ stageId: { in: rule.stageIds } });
facets.push({ stageId: { in: rule.stageIds } });
}
if (rule.sources?.length) {
and.push({
facets.push({
OR: rule.sources.map((s) => ({
source: { equals: s, mode: 'insensitive' as const },
})),
});
}
if (!and.length) continue;
parts.push(and.length === 1 ? and[0]! : { AND: and });
const w = unionFacets(facets);
if (w) parts.push(w);
}
return orParts(parts);
}
@@ -243,28 +250,29 @@ export function dealsDeepWhere(
if (!sectionHasDataAccessLimits(section)) return {};
const parts: Prisma.DealWhereInput[] = [];
for (const rule of section.rules ?? []) {
const and: Prisma.DealWhereInput[] = [];
const facets: Prisma.DealWhereInput[] = [];
if (rule.owners?.length) {
const ids = ownerIdList(rule, {
userId: actor.userId,
teamPeerIds: actor.teamPeerIds ?? null,
});
if (!ids.length) continue;
and.push({
OR: [
{ ownerId: { in: ids } },
{ assignees: { some: { id: { in: ids } } } },
],
});
if (ids.length) {
facets.push({
OR: [
{ ownerId: { in: ids } },
{ assignees: { some: { id: { in: ids } } } },
],
});
}
}
if (rule.pipelineIds?.length) {
and.push({ pipelineId: { in: rule.pipelineIds } });
facets.push({ pipelineId: { in: rule.pipelineIds } });
}
if (rule.stageIds?.length) {
and.push({ stageId: { in: rule.stageIds } });
facets.push({ stageId: { in: rule.stageIds } });
}
if (!and.length) continue;
parts.push(and.length === 1 ? and[0]! : { AND: and });
const w = unionFacets(facets);
if (w) parts.push(w);
}
return orParts(parts);
}
@@ -277,32 +285,33 @@ export function activitiesDeepWhere(
if (!sectionHasDataAccessLimits(section)) return {};
const parts: Prisma.ActivityWhereInput[] = [];
for (const rule of section.rules ?? []) {
const and: Prisma.ActivityWhereInput[] = [];
const facets: Prisma.ActivityWhereInput[] = [];
if (rule.owners?.length) {
const ids = ownerIdList(rule, {
userId: actor.userId,
teamPeerIds: actor.teamPeerIds ?? null,
});
if (!ids.length) continue;
and.push({
OR: [
{ assigneeId: { in: ids } },
{ assignees: { some: { id: { in: ids } } } },
{ participantIds: { hasSome: ids } },
],
});
if (ids.length) {
facets.push({
OR: [
{ assigneeId: { in: ids } },
{ assignees: { some: { id: { in: ids } } } },
{ participantIds: { hasSome: ids } },
],
});
}
}
if (rule.boardIds?.length) {
and.push({ boardId: { in: rule.boardIds } });
facets.push({ boardId: { in: rule.boardIds } });
}
if (rule.stageIds?.length) {
and.push({ stageId: { in: rule.stageIds } });
facets.push({ stageId: { in: rule.stageIds } });
}
if (rule.typeIds?.length) {
and.push({ typeDefId: { in: rule.typeIds } });
facets.push({ typeDefId: { in: rule.typeIds } });
}
if (!and.length) continue;
parts.push(and.length === 1 ? and[0]! : { AND: and });
const w = unionFacets(facets);
if (w) parts.push(w);
}
return orParts(parts);
}
@@ -321,34 +330,35 @@ export function notesDeepWhere(
if (!sectionHasDataAccessLimits(section)) return personalGate;
const parts: Prisma.NoteWhereInput[] = [];
for (const rule of section.rules ?? []) {
const and: Prisma.NoteWhereInput[] = [];
const facets: Prisma.NoteWhereInput[] = [];
if (rule.owners?.length) {
const ids = ownerIdList(rule, {
userId: actor.userId,
teamPeerIds: actor.teamPeerIds ?? null,
});
if (!ids.length) continue;
and.push({
OR: [
{ authorId: { in: ids } },
{
deal: {
OR: [
{ ownerId: { in: ids } },
{ assignees: { some: { id: { in: ids } } } },
],
if (ids.length) {
facets.push({
OR: [
{ authorId: { in: ids } },
{
deal: {
OR: [
{ ownerId: { in: ids } },
{ assignees: { some: { id: { in: ids } } } },
],
},
},
},
{ person: { ownerId: { in: ids } } },
{ company: { ownerId: { in: ids } } },
],
});
{ person: { ownerId: { in: ids } } },
{ company: { ownerId: { in: ids } } },
],
});
}
}
if (rule.sources?.length) {
and.push({ noteType: { in: rule.sources } });
facets.push({ noteType: { in: rule.sources } });
}
if (!and.length) continue;
parts.push(and.length === 1 ? and[0]! : { AND: and });
const w = unionFacets(facets);
if (w) parts.push(w);
}
return {
AND: [personalGate, orParts(parts)],
@@ -363,26 +373,27 @@ export function ticketsDeepWhere(
if (!sectionHasDataAccessLimits(section)) return {};
const parts: Prisma.TicketWhereInput[] = [];
for (const rule of section.rules ?? []) {
const and: Prisma.TicketWhereInput[] = [];
const facets: Prisma.TicketWhereInput[] = [];
if (rule.owners?.length) {
const ids = ownerIdList(rule, {
userId: actor.userId,
teamPeerIds: actor.teamPeerIds ?? null,
});
if (!ids.length) continue;
and.push({
OR: [{ assigneeId: { in: ids } }, { reporterId: { in: ids } }],
});
if (ids.length) {
facets.push({
OR: [{ assigneeId: { in: ids } }, { reporterId: { in: ids } }],
});
}
}
if (rule.sources?.length) {
and.push({
facets.push({
status: {
in: rule.sources as Array<'open' | 'in_progress' | 'waiting' | 'resolved' | 'closed'>,
},
});
}
if (!and.length) continue;
parts.push(and.length === 1 ? and[0]! : { AND: and });
const w = unionFacets(facets);
if (w) parts.push(w);
}
return orParts(parts);
}
@@ -395,28 +406,29 @@ export function projectsDeepWhere(
if (!sectionHasDataAccessLimits(section)) return {};
const parts: Prisma.ProjectCardWhereInput[] = [];
for (const rule of section.rules ?? []) {
const and: Prisma.ProjectCardWhereInput[] = [];
const facets: Prisma.ProjectCardWhereInput[] = [];
if (rule.owners?.length) {
const ids = ownerIdList(rule, {
userId: actor.userId,
teamPeerIds: actor.teamPeerIds ?? null,
});
if (!ids.length) continue;
and.push({
OR: [
{ ownerId: { in: ids } },
{ assignees: { some: { id: { in: ids } } } },
],
});
if (ids.length) {
facets.push({
OR: [
{ ownerId: { in: ids } },
{ assignees: { some: { id: { in: ids } } } },
],
});
}
}
if (rule.boardIds?.length) {
and.push({ boardId: { in: rule.boardIds } });
facets.push({ boardId: { in: rule.boardIds } });
}
if (rule.stageIds?.length) {
and.push({ stageId: { in: rule.stageIds } });
facets.push({ stageId: { in: rule.stageIds } });
}
if (!and.length) continue;
parts.push(and.length === 1 ? and[0]! : { AND: and });
const w = unionFacets(facets);
if (w) parts.push(w);
}
return orParts(parts);
}
@@ -429,20 +441,21 @@ export function routinesDeepWhere(
if (!sectionHasDataAccessLimits(section)) return {};
const parts: Prisma.RoutineTaskWhereInput[] = [];
for (const rule of section.rules ?? []) {
const and: Prisma.RoutineTaskWhereInput[] = [];
const facets: Prisma.RoutineTaskWhereInput[] = [];
if (rule.owners?.length) {
const ids = ownerIdList(rule, {
userId: actor.userId,
teamPeerIds: actor.teamPeerIds ?? null,
});
if (!ids.length) continue;
and.push(ids.length === 1 ? { assigneeId: ids[0]! } : { assigneeId: { in: ids } });
if (ids.length) {
facets.push(ids.length === 1 ? { assigneeId: ids[0]! } : { assigneeId: { in: ids } });
}
}
if (rule.typeIds?.length) {
and.push({ categoryId: { in: rule.typeIds } });
facets.push({ categoryId: { in: rule.typeIds } });
}
if (!and.length) continue;
parts.push(and.length === 1 ? and[0]! : { AND: and });
const w = unionFacets(facets);
if (w) parts.push(w);
}
return orParts(parts);
}
+13 -3
View File
@@ -219,15 +219,25 @@ export class PeopleService implements OnModuleInit {
}
const disc = discFilter(opts?.disc);
if (disc) and.push(disc);
if (groupIds.length) and.push({ groupLinks: { some: { groupId: { in: groupIds } } } });
// گروه / منبع / حال: اتحاد (هر بُعد کافی است)؛ داخل هر بُعد چند مقدار با in = OR
const facets: Prisma.PersonWhereInput[] = [];
if (groupIds.length) {
facets.push({ groupLinks: { some: { groupId: { in: groupIds } } } });
}
if (moods.length) {
facets.push({ mood: { in: moods } });
}
if (sources.length) {
facets.push({ importSource: { in: sources } });
}
if (facets.length === 1) and.push(facets[0]!);
else if (facets.length > 1) and.push({ OR: facets });
return {
AND: [
{
organizationId: actor.organizationId,
...customersPersonWhere(actor),
},
...(moods.length ? [{ mood: { in: moods } }] : []),
...(sources.length ? [{ importSource: { in: sources } }] : []),
...and,
],
};
@@ -126,6 +126,9 @@ function apply() {
<strong>{{ hasSort ? 'فیلتر و مرتب‌سازی' : 'فیلترها' }}</strong>
<button type="button" class="link" :disabled="!draftCount" @click="clearAll">پاک کردن همه</button>
</header>
<p v-if="isPeople" class="facet-hint">
گروه، منبع و وضعیت با هم جمع می‌شوند؛ هر کدام کافی است (بدون تکرار).
</p>
<div class="panel-body">
<section v-if="hasSort" class="sec">
@@ -264,6 +267,15 @@ function apply() {
border-bottom: 1px solid var(--line);
}
.facet-hint {
margin: 0;
padding: 0.35rem 0.9rem 0.55rem;
font-size: var(--fs-sm);
color: #64748b;
line-height: 1.45;
border-bottom: 1px solid var(--line);
}
.panel-foot {
border-top: 1px solid var(--line);
justify-content: flex-end;
+4 -4
View File
@@ -449,11 +449,11 @@ const filterSummary = computed(() => {
<template v-if="mode === 'rules'">
<p class="hint">
فقط رکوردهایی دیده می‌شوند که با
<strong>همهٔ فیلترهای پرشده</strong>
جور باشند. در هر فهرست، چند انتخاب یعنی هر کدام کافی است. خالی یعنی بدون محدودیت.
فیلترهای پرشده با هم
<strong>جمع می‌شوند</strong>
؛ هر بُعد کافی است (بدون تکرار). در هر فهرست هم چند انتخاب یعنی اتحاد. خالی یعنی بدون محدودیت آن بُعد.
<template v-if="isCustomers">
اعضای گروه‌هایی که این کاربر مسئولشان است جداگانه به همین محدوده اضافه می‌شوند (بدون تکرار).
اعضای گروه‌هایی که این کاربر مسئولشان است جداگانه به همین محدوده اضافه می‌شوند.
</template>
</p>
<p v-if="hadMultipleRules" class="hint warn">
+2 -2
View File
@@ -646,8 +646,8 @@ export const DATA_ACCESS_LEGACY_SCOPE: Record<DataAccessSection, DataScope | nul
};
/**
* یک قانون: فیلدهای پرشده با AND.
* قوانین متعدد در SectionDataAccess با OR ترکیب می‌شوند.
* یک قانون: فیلدهای پرشده با OR (هر بُعد کافی است).
* قوانین متعدد در SectionDataAccess هم با OR ترکیب می‌شوند.
* ابعاد بلااستفاده برای یک بخش نادیده گرفته می‌شوند.
*/
export type DataAccessRule = {