@things-factory/organization 10.1.218 → 10.1.223

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
@@ -0,0 +1,29 @@
1
+ import { EntityManager } from 'typeorm';
2
+ import { Role } from '@things-factory/auth-base';
3
+ import { Domain } from '@things-factory/shell';
4
+ import { type AudienceMember, type AudienceTarget } from './audience';
5
+ /**
6
+ * The roles of the domain - the ones it inherits from its parent included, as its privileges are
7
+ * (User.getPrivilegesByDomain); `holder` keeps those one person holds.
8
+ */
9
+ export declare function domainRolesOf(domain: Partial<Domain>, holder?: string, tx?: EntityManager): Promise<Role[]>;
10
+ /** Each department of the domain and the one above it. */
11
+ export declare function departmentTreeOf(domain: Partial<Domain>, tx?: EntityManager): Promise<Map<string, string | null>>;
12
+ /**
13
+ * One person as an audience sees them, with the departments of their own active records (`ownDepartments`, what they
14
+ * may address as theirs) apart from the ones above those.
15
+ */
16
+ export declare function audienceMemberOf(domain: Partial<Domain>, user: {
17
+ id?: string;
18
+ }, tx?: EntityManager): Promise<AudienceMember & {
19
+ ownDepartments: {
20
+ id: string;
21
+ name: string;
22
+ }[];
23
+ }>;
24
+ /**
25
+ * The people an audience names: those who may be given work in the domain (its active members and owners -
26
+ * getDomainAssignees), narrowed to the targets. A department names the active employees of it and of every department
27
+ * under it; a role, whoever holds it.
28
+ */
29
+ export declare function audienceUserIds(domain: Partial<Domain>, targets: readonly AudienceTarget[], tx?: EntityManager): Promise<string[]>;
@@ -0,0 +1,84 @@
1
+ "use strict";
2
+ Object.defineProperty(exports, "__esModule", { value: true });
3
+ exports.domainRolesOf = domainRolesOf;
4
+ exports.departmentTreeOf = departmentTreeOf;
5
+ exports.audienceMemberOf = audienceMemberOf;
6
+ exports.audienceUserIds = audienceUserIds;
7
+ /*
8
+ * The facts an audience is asked about, read from the domain (ADR-0163 decision 2) - who one person is to an audience,
9
+ * and who the people of an audience are. The rule itself is audience.ts.
10
+ */
11
+ const typeorm_1 = require("typeorm");
12
+ const auth_base_1 = require("@things-factory/auth-base");
13
+ const shell_1 = require("@things-factory/shell");
14
+ const department_1 = require("../department/department");
15
+ const employee_1 = require("../employee/employee");
16
+ const audience_1 = require("./audience");
17
+ /**
18
+ * The roles of the domain - the ones it inherits from its parent included, as its privileges are
19
+ * (User.getPrivilegesByDomain); `holder` keeps those one person holds.
20
+ */
21
+ async function domainRolesOf(domain, holder, tx) {
22
+ const qb = (0, shell_1.getRepository)(auth_base_1.Role, tx).createQueryBuilder('ROLE');
23
+ if (holder)
24
+ qb.innerJoin('ROLE.users', 'HOLDER', 'HOLDER.id = :holder', { holder });
25
+ (0, shell_1.applyDomainInheritanceFilter)(qb, 'ROLE', domain, shell_1.InheritedValueType.Include);
26
+ return await qb.orderBy('ROLE.name').getMany();
27
+ }
28
+ /** Each department of the domain and the one above it. */
29
+ async function departmentTreeOf(domain, tx) {
30
+ const rows = await (0, shell_1.getRepository)(department_1.Department, tx).find({ where: { domain: { id: domain.id } }, relations: ['parent'] });
31
+ return new Map(rows.map(one => [one.id, one.parent?.id ?? null]));
32
+ }
33
+ /**
34
+ * One person as an audience sees them, with the departments of their own active records (`ownDepartments`, what they
35
+ * may address as theirs) apart from the ones above those.
36
+ */
37
+ async function audienceMemberOf(domain, user, tx) {
38
+ if (!domain?.id || !user?.id)
39
+ return { userId: user?.id, domainId: domain?.id, departmentIds: [], roleIds: [], ownDepartments: [] };
40
+ const records = await (0, shell_1.getRepository)(employee_1.Employee, tx).find({ where: { domain: { id: domain.id }, user: { id: user.id }, active: true }, relations: ['department'] });
41
+ const own = (0, audience_1.departmentsOfEmployees)(records);
42
+ const departmentIds = own.length ? (0, audience_1.withAncestors)(own, await departmentTreeOf(domain, tx)) : [];
43
+ const roleIds = (await domainRolesOf(domain, user.id, tx)).map(one => one.id);
44
+ return {
45
+ userId: user.id,
46
+ domainId: domain.id,
47
+ departmentIds,
48
+ roleIds,
49
+ ownDepartments: own.map(id => ({ id, name: records.find(one => one.department?.id === id)?.department?.name ?? id }))
50
+ };
51
+ }
52
+ /**
53
+ * The people an audience names: those who may be given work in the domain (its active members and owners -
54
+ * getDomainAssignees), narrowed to the targets. A department names the active employees of it and of every department
55
+ * under it; a role, whoever holds it.
56
+ */
57
+ async function audienceUserIds(domain, targets, tx) {
58
+ if (!domain?.id || !targets?.length)
59
+ return [];
60
+ const assignees = new Set((await (0, auth_base_1.getDomainAssignees)(domain, tx)).map(one => one.id));
61
+ if (targets.some(one => one.scope === 'DOMAIN' && one.targetId === domain.id))
62
+ return [...assignees];
63
+ const named = new Set();
64
+ for (const one of targets)
65
+ if (one.scope === 'USER')
66
+ named.add(one.targetId);
67
+ const departments = targets.filter(one => one.scope === 'DEPARTMENT').map(one => one.targetId);
68
+ if (departments.length) {
69
+ const ids = (0, audience_1.withDescendants)(departments, await departmentTreeOf(domain, tx));
70
+ const employees = await (0, shell_1.getRepository)(employee_1.Employee, tx).find({ where: { domain: { id: domain.id }, department: { id: (0, typeorm_1.In)(ids) }, active: true }, relations: ['user'] });
71
+ for (const one of employees)
72
+ if (one.user?.id)
73
+ named.add(one.user.id);
74
+ }
75
+ const roles = targets.filter(one => one.scope === 'ROLE').map(one => one.targetId);
76
+ if (roles.length) {
77
+ const holders = await (0, shell_1.getRepository)(auth_base_1.Role, tx).createQueryBuilder('ROLE').innerJoinAndSelect('ROLE.users', 'HOLDER').where('ROLE.id IN (:...roles)', { roles }).getMany();
78
+ for (const role of holders)
79
+ for (const holder of role.users ?? [])
80
+ named.add(holder.id);
81
+ }
82
+ return [...named].filter(id => assignees.has(id));
83
+ }
84
+ //# sourceMappingURL=audience-member.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"audience-member.js","sourceRoot":"","sources":["../../../server/service/audience/audience-member.ts"],"names":[],"mappings":"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","sourcesContent":["/*\n * The facts an audience is asked about, read from the domain (ADR-0163 decision 2) - who one person is to an audience,\n * and who the people of an audience are. The rule itself is audience.ts.\n */\nimport { EntityManager, In } from 'typeorm'\n\nimport { getDomainAssignees, Role } from '@things-factory/auth-base'\nimport { applyDomainInheritanceFilter, Domain, getRepository, InheritedValueType } from '@things-factory/shell'\n\nimport { Department } from '../department/department'\nimport { Employee } from '../employee/employee'\nimport { type AudienceMember, type AudienceTarget, departmentsOfEmployees, withAncestors, withDescendants } from './audience'\n\n/**\n * The roles of the domain - the ones it inherits from its parent included, as its privileges are\n * (User.getPrivilegesByDomain); `holder` keeps those one person holds.\n */\nexport async function domainRolesOf(domain: Partial<Domain>, holder?: string, tx?: EntityManager): Promise<Role[]> {\n const qb = getRepository(Role, tx).createQueryBuilder('ROLE')\n if (holder) qb.innerJoin('ROLE.users', 'HOLDER', 'HOLDER.id = :holder', { holder })\n applyDomainInheritanceFilter(qb, 'ROLE', domain as any, InheritedValueType.Include)\n return await qb.orderBy('ROLE.name').getMany()\n}\n\n/** Each department of the domain and the one above it. */\nexport async function departmentTreeOf(domain: Partial<Domain>, tx?: EntityManager): Promise<Map<string, string | null>> {\n const rows = await getRepository(Department, tx).find({ where: { domain: { id: domain.id } } as any, relations: ['parent'] })\n return new Map(rows.map(one => [one.id, one.parent?.id ?? null]))\n}\n\n/**\n * One person as an audience sees them, with the departments of their own active records (`ownDepartments`, what they\n * may address as theirs) apart from the ones above those.\n */\nexport async function audienceMemberOf(\n domain: Partial<Domain>,\n user: { id?: string },\n tx?: EntityManager\n): Promise<AudienceMember & { ownDepartments: { id: string; name: string }[] }> {\n if (!domain?.id || !user?.id) return { userId: user?.id, domainId: domain?.id, departmentIds: [], roleIds: [], ownDepartments: [] }\n const records = await getRepository(Employee, tx).find({ where: { domain: { id: domain.id }, user: { id: user.id }, active: true } as any, relations: ['department'] })\n const own = departmentsOfEmployees(records)\n const departmentIds = own.length ? withAncestors(own, await departmentTreeOf(domain, tx)) : []\n const roleIds = (await domainRolesOf(domain, user.id, tx)).map(one => one.id)\n return {\n userId: user.id,\n domainId: domain.id,\n departmentIds,\n roleIds,\n ownDepartments: own.map(id => ({ id, name: records.find(one => one.department?.id === id)?.department?.name ?? id }))\n }\n}\n\n/**\n * The people an audience names: those who may be given work in the domain (its active members and owners -\n * getDomainAssignees), narrowed to the targets. A department names the active employees of it and of every department\n * under it; a role, whoever holds it.\n */\nexport async function audienceUserIds(domain: Partial<Domain>, targets: readonly AudienceTarget[], tx?: EntityManager): Promise<string[]> {\n if (!domain?.id || !targets?.length) return []\n const assignees = new Set((await getDomainAssignees(domain as any, tx)).map(one => one.id))\n if (targets.some(one => one.scope === 'DOMAIN' && one.targetId === domain.id)) return [...assignees]\n\n const named = new Set<string>()\n for (const one of targets) if (one.scope === 'USER') named.add(one.targetId)\n\n const departments = targets.filter(one => one.scope === 'DEPARTMENT').map(one => one.targetId)\n if (departments.length) {\n const ids = withDescendants(departments, await departmentTreeOf(domain, tx))\n const employees = await getRepository(Employee, tx).find({ where: { domain: { id: domain.id }, department: { id: In(ids) }, active: true } as any, relations: ['user'] })\n for (const one of employees) if (one.user?.id) named.add(one.user.id)\n }\n\n const roles = targets.filter(one => one.scope === 'ROLE').map(one => one.targetId)\n if (roles.length) {\n const holders = await getRepository(Role, tx).createQueryBuilder('ROLE').innerJoinAndSelect('ROLE.users', 'HOLDER').where('ROLE.id IN (:...roles)', { roles }).getMany()\n for (const role of holders) for (const holder of role.users ?? []) named.add(holder.id)\n }\n\n return [...named].filter(id => assignees.has(id))\n}\n"]}
@@ -0,0 +1,43 @@
1
+ export declare const AUDIENCE_SCOPES: readonly ["DOMAIN", "DEPARTMENT", "ROLE", "USER"];
2
+ export type AudienceScope = (typeof AUDIENCE_SCOPES)[number];
3
+ export interface AudienceTarget {
4
+ scope: AudienceScope | string;
5
+ targetId: string;
6
+ }
7
+ /** One person as an audience sees them. */
8
+ export interface AudienceMember {
9
+ userId: string;
10
+ domainId: string;
11
+ /**
12
+ * The departments the person stands in: those of their active employee records and every department above them - a
13
+ * department target reaches the people of the departments under it.
14
+ */
15
+ departmentIds: readonly string[];
16
+ /** The roles the person holds in the domain, the inherited ones included. */
17
+ roleIds: readonly string[];
18
+ }
19
+ /** Whether any of `targets` names `member`. */
20
+ export declare function audienceReaches(targets: readonly AudienceTarget[], member: AudienceMember): boolean;
21
+ /**
22
+ * The departments of a person's employee records that are active (`active = true`; an unset `active` is not taken, as
23
+ * the assistant's people read takes only `active eq true`). A record without a department adds none.
24
+ */
25
+ export declare function departmentsOfEmployees(records: readonly {
26
+ active?: boolean | null;
27
+ department?: {
28
+ id?: string | null;
29
+ } | null;
30
+ }[] | null | undefined): string[];
31
+ /** `departmentIds` and every department above them. A loop in the tree ends the walk instead of hanging it. */
32
+ export declare function withAncestors(departmentIds: readonly string[], parentOf: ReadonlyMap<string, string | null | undefined>): string[];
33
+ /** `departmentIds` and every department under them. */
34
+ export declare function withDescendants(departmentIds: readonly string[], parentOf: ReadonlyMap<string, string | null | undefined>): string[];
35
+ /**
36
+ * Why a target may not be given, or nothing - a known scope with a target; the domain scope names the giver's own
37
+ * domain, a department one of the domain's departments, a role one of the domain's roles.
38
+ */
39
+ export declare function audienceTargetRefusal(target: AudienceTarget, domain: {
40
+ domainId: string;
41
+ departmentIds: readonly string[];
42
+ roleIds: readonly string[];
43
+ }): string | undefined;
@@ -0,0 +1,81 @@
1
+ "use strict";
2
+ /*
3
+ * Whom a thing is addressed to in a domain - the one rule of an audience (ADR-0163 decision 2, ADR-0160 decision 7).
4
+ *
5
+ * An audience is a list of targets: the whole domain, a department (and every department under it), a role (the ones
6
+ * the domain inherits included) or a person. A dashboard card shared and an announcement addressed ask the same
7
+ * question, so they ask it here (the user, 2026-10-26: one rule, a department reaches its sub-departments).
8
+ *
9
+ * Pure - the rule is tested without a database. audience-member.ts reads the facts it is given.
10
+ */
11
+ Object.defineProperty(exports, "__esModule", { value: true });
12
+ exports.AUDIENCE_SCOPES = void 0;
13
+ exports.audienceReaches = audienceReaches;
14
+ exports.departmentsOfEmployees = departmentsOfEmployees;
15
+ exports.withAncestors = withAncestors;
16
+ exports.withDescendants = withDescendants;
17
+ exports.audienceTargetRefusal = audienceTargetRefusal;
18
+ exports.AUDIENCE_SCOPES = ['DOMAIN', 'DEPARTMENT', 'ROLE', 'USER'];
19
+ /** Whether any of `targets` names `member`. */
20
+ function audienceReaches(targets, member) {
21
+ if (!member?.userId || !member.domainId)
22
+ return false;
23
+ return (targets ?? []).some(target => (target.scope === 'DOMAIN' && target.targetId === member.domainId) ||
24
+ (target.scope === 'USER' && target.targetId === member.userId) ||
25
+ (target.scope === 'DEPARTMENT' && member.departmentIds.includes(target.targetId)) ||
26
+ (target.scope === 'ROLE' && member.roleIds.includes(target.targetId)));
27
+ }
28
+ /**
29
+ * The departments of a person's employee records that are active (`active = true`; an unset `active` is not taken, as
30
+ * the assistant's people read takes only `active eq true`). A record without a department adds none.
31
+ */
32
+ function departmentsOfEmployees(records) {
33
+ return [...new Set((records ?? []).filter(one => one?.active === true && one.department?.id).map(one => one.department.id))];
34
+ }
35
+ /** `departmentIds` and every department above them. A loop in the tree ends the walk instead of hanging it. */
36
+ function withAncestors(departmentIds, parentOf) {
37
+ const out = new Set();
38
+ for (const start of departmentIds) {
39
+ let id = start;
40
+ while (id && !out.has(id)) {
41
+ out.add(id);
42
+ id = parentOf.get(id);
43
+ }
44
+ }
45
+ return [...out];
46
+ }
47
+ /** `departmentIds` and every department under them. */
48
+ function withDescendants(departmentIds, parentOf) {
49
+ const children = new Map();
50
+ for (const [id, parent] of parentOf)
51
+ if (parent)
52
+ children.set(parent, [...(children.get(parent) ?? []), id]);
53
+ const out = new Set();
54
+ const walk = (id) => {
55
+ if (out.has(id))
56
+ return;
57
+ out.add(id);
58
+ for (const child of children.get(id) ?? [])
59
+ walk(child);
60
+ };
61
+ departmentIds.forEach(walk);
62
+ return [...out];
63
+ }
64
+ /**
65
+ * Why a target may not be given, or nothing - a known scope with a target; the domain scope names the giver's own
66
+ * domain, a department one of the domain's departments, a role one of the domain's roles.
67
+ */
68
+ function audienceTargetRefusal(target, domain) {
69
+ if (!exports.AUDIENCE_SCOPES.includes(target?.scope))
70
+ return 'unknown-scope';
71
+ if (!target.targetId)
72
+ return 'no-target';
73
+ if (target.scope === 'DOMAIN' && target.targetId !== domain.domainId)
74
+ return 'another-domain';
75
+ if (target.scope === 'DEPARTMENT' && !domain.departmentIds.includes(target.targetId))
76
+ return 'unknown-department';
77
+ if (target.scope === 'ROLE' && !domain.roleIds.includes(target.targetId))
78
+ return 'unknown-role';
79
+ return undefined;
80
+ }
81
+ //# sourceMappingURL=audience.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"audience.js","sourceRoot":"","sources":["../../../server/service/audience/audience.ts"],"names":[],"mappings":";AAAA;;;;;;;;GAQG;;;AAyBH,0CASC;AAMD,wDAEC;AAGD,sCAUC;AAGD,0CAWC;AAMD,sDAUC;AAnFY,QAAA,eAAe,GAAG,CAAC,QAAQ,EAAE,YAAY,EAAE,MAAM,EAAE,MAAM,CAAU,CAAA;AAsBhF,+CAA+C;AAC/C,SAAgB,eAAe,CAAC,OAAkC,EAAE,MAAsB;IACxF,IAAI,CAAC,MAAM,EAAE,MAAM,IAAI,CAAC,MAAM,CAAC,QAAQ;QAAE,OAAO,KAAK,CAAA;IACrD,OAAO,CAAC,OAAO,IAAI,EAAE,CAAC,CAAC,IAAI,CACzB,MAAM,CAAC,EAAE,CACP,CAAC,MAAM,CAAC,KAAK,KAAK,QAAQ,IAAI,MAAM,CAAC,QAAQ,KAAK,MAAM,CAAC,QAAQ,CAAC;QAClE,CAAC,MAAM,CAAC,KAAK,KAAK,MAAM,IAAI,MAAM,CAAC,QAAQ,KAAK,MAAM,CAAC,MAAM,CAAC;QAC9D,CAAC,MAAM,CAAC,KAAK,KAAK,YAAY,IAAI,MAAM,CAAC,aAAa,CAAC,QAAQ,CAAC,MAAM,CAAC,QAAQ,CAAC,CAAC;QACjF,CAAC,MAAM,CAAC,KAAK,KAAK,MAAM,IAAI,MAAM,CAAC,OAAO,CAAC,QAAQ,CAAC,MAAM,CAAC,QAAQ,CAAC,CAAC,CACxE,CAAA;AACH,CAAC;AAED;;;GAGG;AACH,SAAgB,sBAAsB,CAAC,OAA8G;IACnJ,OAAO,CAAC,GAAG,IAAI,GAAG,CAAC,CAAC,OAAO,IAAI,EAAE,CAAC,CAAC,MAAM,CAAC,GAAG,CAAC,EAAE,CAAC,GAAG,EAAE,MAAM,KAAK,IAAI,IAAI,GAAG,CAAC,UAAU,EAAE,EAAE,CAAC,CAAC,GAAG,CAAC,GAAG,CAAC,EAAE,CAAC,GAAG,CAAC,UAAW,CAAC,EAAG,CAAC,CAAC,CAAC,CAAA;AAChI,CAAC;AAED,+GAA+G;AAC/G,SAAgB,aAAa,CAAC,aAAgC,EAAE,QAAwD;IACtH,MAAM,GAAG,GAAG,IAAI,GAAG,EAAU,CAAA;IAC7B,KAAK,MAAM,KAAK,IAAI,aAAa,EAAE,CAAC;QAClC,IAAI,EAAE,GAA8B,KAAK,CAAA;QACzC,OAAO,EAAE,IAAI,CAAC,GAAG,CAAC,GAAG,CAAC,EAAE,CAAC,EAAE,CAAC;YAC1B,GAAG,CAAC,GAAG,CAAC,EAAE,CAAC,CAAA;YACX,EAAE,GAAG,QAAQ,CAAC,GAAG,CAAC,EAAE,CAAC,CAAA;QACvB,CAAC;IACH,CAAC;IACD,OAAO,CAAC,GAAG,GAAG,CAAC,CAAA;AACjB,CAAC;AAED,uDAAuD;AACvD,SAAgB,eAAe,CAAC,aAAgC,EAAE,QAAwD;IACxH,MAAM,QAAQ,GAAG,IAAI,GAAG,EAAoB,CAAA;IAC5C,KAAK,MAAM,CAAC,EAAE,EAAE,MAAM,CAAC,IAAI,QAAQ;QAAE,IAAI,MAAM;YAAE,QAAQ,CAAC,GAAG,CAAC,MAAM,EAAE,CAAC,GAAG,CAAC,QAAQ,CAAC,GAAG,CAAC,MAAM,CAAC,IAAI,EAAE,CAAC,EAAE,EAAE,CAAC,CAAC,CAAA;IAC5G,MAAM,GAAG,GAAG,IAAI,GAAG,EAAU,CAAA;IAC7B,MAAM,IAAI,GAAG,CAAC,EAAU,EAAE,EAAE;QAC1B,IAAI,GAAG,CAAC,GAAG,CAAC,EAAE,CAAC;YAAE,OAAM;QACvB,GAAG,CAAC,GAAG,CAAC,EAAE,CAAC,CAAA;QACX,KAAK,MAAM,KAAK,IAAI,QAAQ,CAAC,GAAG,CAAC,EAAE,CAAC,IAAI,EAAE;YAAE,IAAI,CAAC,KAAK,CAAC,CAAA;IACzD,CAAC,CAAA;IACD,aAAa,CAAC,OAAO,CAAC,IAAI,CAAC,CAAA;IAC3B,OAAO,CAAC,GAAG,GAAG,CAAC,CAAA;AACjB,CAAC;AAED;;;GAGG;AACH,SAAgB,qBAAqB,CACnC,MAAsB,EACtB,MAA0F;IAE1F,IAAI,CAAE,uBAAqC,CAAC,QAAQ,CAAC,MAAM,EAAE,KAAK,CAAC;QAAE,OAAO,eAAe,CAAA;IAC3F,IAAI,CAAC,MAAM,CAAC,QAAQ;QAAE,OAAO,WAAW,CAAA;IACxC,IAAI,MAAM,CAAC,KAAK,KAAK,QAAQ,IAAI,MAAM,CAAC,QAAQ,KAAK,MAAM,CAAC,QAAQ;QAAE,OAAO,gBAAgB,CAAA;IAC7F,IAAI,MAAM,CAAC,KAAK,KAAK,YAAY,IAAI,CAAC,MAAM,CAAC,aAAa,CAAC,QAAQ,CAAC,MAAM,CAAC,QAAQ,CAAC;QAAE,OAAO,oBAAoB,CAAA;IACjH,IAAI,MAAM,CAAC,KAAK,KAAK,MAAM,IAAI,CAAC,MAAM,CAAC,OAAO,CAAC,QAAQ,CAAC,MAAM,CAAC,QAAQ,CAAC;QAAE,OAAO,cAAc,CAAA;IAC/F,OAAO,SAAS,CAAA;AAClB,CAAC","sourcesContent":["/*\n * Whom a thing is addressed to in a domain - the one rule of an audience (ADR-0163 decision 2, ADR-0160 decision 7).\n *\n * An audience is a list of targets: the whole domain, a department (and every department under it), a role (the ones\n * the domain inherits included) or a person. A dashboard card shared and an announcement addressed ask the same\n * question, so they ask it here (the user, 2026-10-26: one rule, a department reaches its sub-departments).\n *\n * Pure - the rule is tested without a database. audience-member.ts reads the facts it is given.\n */\n\nexport const AUDIENCE_SCOPES = ['DOMAIN', 'DEPARTMENT', 'ROLE', 'USER'] as const\n\nexport type AudienceScope = (typeof AUDIENCE_SCOPES)[number]\n\nexport interface AudienceTarget {\n scope: AudienceScope | string\n targetId: string\n}\n\n/** One person as an audience sees them. */\nexport interface AudienceMember {\n userId: string\n domainId: string\n /**\n * The departments the person stands in: those of their active employee records and every department above them - a\n * department target reaches the people of the departments under it.\n */\n departmentIds: readonly string[]\n /** The roles the person holds in the domain, the inherited ones included. */\n roleIds: readonly string[]\n}\n\n/** Whether any of `targets` names `member`. */\nexport function audienceReaches(targets: readonly AudienceTarget[], member: AudienceMember): boolean {\n if (!member?.userId || !member.domainId) return false\n return (targets ?? []).some(\n target =>\n (target.scope === 'DOMAIN' && target.targetId === member.domainId) ||\n (target.scope === 'USER' && target.targetId === member.userId) ||\n (target.scope === 'DEPARTMENT' && member.departmentIds.includes(target.targetId)) ||\n (target.scope === 'ROLE' && member.roleIds.includes(target.targetId))\n )\n}\n\n/**\n * The departments of a person's employee records that are active (`active = true`; an unset `active` is not taken, as\n * the assistant's people read takes only `active eq true`). A record without a department adds none.\n */\nexport function departmentsOfEmployees(records: readonly { active?: boolean | null; department?: { id?: string | null } | null }[] | null | undefined): string[] {\n return [...new Set((records ?? []).filter(one => one?.active === true && one.department?.id).map(one => one.department!.id!))]\n}\n\n/** `departmentIds` and every department above them. A loop in the tree ends the walk instead of hanging it. */\nexport function withAncestors(departmentIds: readonly string[], parentOf: ReadonlyMap<string, string | null | undefined>): string[] {\n const out = new Set<string>()\n for (const start of departmentIds) {\n let id: string | null | undefined = start\n while (id && !out.has(id)) {\n out.add(id)\n id = parentOf.get(id)\n }\n }\n return [...out]\n}\n\n/** `departmentIds` and every department under them. */\nexport function withDescendants(departmentIds: readonly string[], parentOf: ReadonlyMap<string, string | null | undefined>): string[] {\n const children = new Map<string, string[]>()\n for (const [id, parent] of parentOf) if (parent) children.set(parent, [...(children.get(parent) ?? []), id])\n const out = new Set<string>()\n const walk = (id: string) => {\n if (out.has(id)) return\n out.add(id)\n for (const child of children.get(id) ?? []) walk(child)\n }\n departmentIds.forEach(walk)\n return [...out]\n}\n\n/**\n * Why a target may not be given, or nothing - a known scope with a target; the domain scope names the giver's own\n * domain, a department one of the domain's departments, a role one of the domain's roles.\n */\nexport function audienceTargetRefusal(\n target: AudienceTarget,\n domain: { domainId: string; departmentIds: readonly string[]; roleIds: readonly string[] }\n): string | undefined {\n if (!(AUDIENCE_SCOPES as readonly string[]).includes(target?.scope)) return 'unknown-scope'\n if (!target.targetId) return 'no-target'\n if (target.scope === 'DOMAIN' && target.targetId !== domain.domainId) return 'another-domain'\n if (target.scope === 'DEPARTMENT' && !domain.departmentIds.includes(target.targetId)) return 'unknown-department'\n if (target.scope === 'ROLE' && !domain.roleIds.includes(target.targetId)) return 'unknown-role'\n return undefined\n}\n"]}
@@ -1,6 +1,8 @@
1
1
  export * from './approval-line/approval-line';
2
2
  export * from './department/department';
3
3
  export * from './employee/employee';
4
+ export * from './audience/audience';
5
+ export * from './audience/audience-member';
4
6
  export declare const entities: (typeof import("./department/department").Department | typeof import("./employee/employee").Employee | typeof import("./approval-line/approval-line").ApprovalLine | typeof import("./department/department-history").DepartmentHistory | typeof import("./employee/employee-history").EmployeeHistory)[];
5
7
  export declare const subscribers: (typeof import("./department/event-subscriber").DepartmentHistoryEntitySubscriber | typeof import("./employee/event-subscriber").EmployeeHistoryEntitySubscriber)[];
6
8
  export declare const schema: {
@@ -6,6 +6,8 @@ const tslib_1 = require("tslib");
6
6
  tslib_1.__exportStar(require("./approval-line/approval-line"), exports);
7
7
  tslib_1.__exportStar(require("./department/department"), exports);
8
8
  tslib_1.__exportStar(require("./employee/employee"), exports);
9
+ tslib_1.__exportStar(require("./audience/audience"), exports);
10
+ tslib_1.__exportStar(require("./audience/audience-member"), exports);
9
11
  /* IMPORT ENTITIES AND RESOLVERS */
10
12
  const approval_line_1 = require("./approval-line");
11
13
  const department_1 = require("./department");
@@ -1 +1 @@
1
- {"version":3,"file":"index.js","sourceRoot":"","sources":["../../server/service/index.ts"],"names":[],"mappings":";;;;AAAA,yBAAyB;AACzB,wEAA6C;AAC7C,kEAAuC;AACvC,8DAAmC;AAEnC,mCAAmC;AACnC,mDAAsG;AACtG,6CAIqB;AACrB,yCAImB;AAEN,QAAA,QAAQ,GAAG;IACtB,cAAc;IACd,GAAG,wBAAoB;IACvB,GAAG,qBAAkB;IACrB,GAAG,mBAAgB;CACpB,CAAA;AAEY,QAAA,WAAW,GAAG;IACzB,iBAAiB;IACjB,GAAG,wBAAqB;IACxB,GAAG,sBAAmB;CACvB,CAAA;AAEY,QAAA,MAAM,GAAG;IACpB,eAAe,EAAE;QACf,sBAAsB;QACtB,GAAG,yBAAqB;QACxB,GAAG,sBAAmB;QACtB,GAAG,oBAAiB;KACrB;CACF,CAAA","sourcesContent":["/* EXPORT ENTITY TYPES */\nexport * from './approval-line/approval-line'\nexport * from './department/department'\nexport * from './employee/employee'\n\n/* IMPORT ENTITIES AND RESOLVERS */\nimport { entities as ApprovalLineEntities, resolvers as ApprovalLineResolvers } from './approval-line'\nimport {\n entities as DepartmentEntities,\n resolvers as DepartmentResolvers,\n subscribers as DepartmentSubscribers\n} from './department'\nimport {\n entities as EmployeeEntities,\n resolvers as EmployeeResolvers,\n subscribers as EmployeeSubscribers\n} from './employee'\n\nexport const entities = [\n /* ENTITIES */\n ...ApprovalLineEntities,\n ...DepartmentEntities,\n ...EmployeeEntities\n]\n\nexport const subscribers = [\n /* SUBSCRIBERS */\n ...DepartmentSubscribers,\n ...EmployeeSubscribers\n]\n\nexport const schema = {\n resolverClasses: [\n /* RESOLVER CLASSES */\n ...ApprovalLineResolvers,\n ...DepartmentResolvers,\n ...EmployeeResolvers\n ]\n}\n"]}
1
+ {"version":3,"file":"index.js","sourceRoot":"","sources":["../../server/service/index.ts"],"names":[],"mappings":";;;;AAAA,yBAAyB;AACzB,wEAA6C;AAC7C,kEAAuC;AACvC,8DAAmC;AACnC,8DAAmC;AACnC,qEAA0C;AAE1C,mCAAmC;AACnC,mDAAsG;AACtG,6CAIqB;AACrB,yCAImB;AAEN,QAAA,QAAQ,GAAG;IACtB,cAAc;IACd,GAAG,wBAAoB;IACvB,GAAG,qBAAkB;IACrB,GAAG,mBAAgB;CACpB,CAAA;AAEY,QAAA,WAAW,GAAG;IACzB,iBAAiB;IACjB,GAAG,wBAAqB;IACxB,GAAG,sBAAmB;CACvB,CAAA;AAEY,QAAA,MAAM,GAAG;IACpB,eAAe,EAAE;QACf,sBAAsB;QACtB,GAAG,yBAAqB;QACxB,GAAG,sBAAmB;QACtB,GAAG,oBAAiB;KACrB;CACF,CAAA","sourcesContent":["/* EXPORT ENTITY TYPES */\nexport * from './approval-line/approval-line'\nexport * from './department/department'\nexport * from './employee/employee'\nexport * from './audience/audience'\nexport * from './audience/audience-member'\n\n/* IMPORT ENTITIES AND RESOLVERS */\nimport { entities as ApprovalLineEntities, resolvers as ApprovalLineResolvers } from './approval-line'\nimport {\n entities as DepartmentEntities,\n resolvers as DepartmentResolvers,\n subscribers as DepartmentSubscribers\n} from './department'\nimport {\n entities as EmployeeEntities,\n resolvers as EmployeeResolvers,\n subscribers as EmployeeSubscribers\n} from './employee'\n\nexport const entities = [\n /* ENTITIES */\n ...ApprovalLineEntities,\n ...DepartmentEntities,\n ...EmployeeEntities\n]\n\nexport const subscribers = [\n /* SUBSCRIBERS */\n ...DepartmentSubscribers,\n ...EmployeeSubscribers\n]\n\nexport const schema = {\n resolverClasses: [\n /* RESOLVER CLASSES */\n ...ApprovalLineResolvers,\n ...DepartmentResolvers,\n ...EmployeeResolvers\n ]\n}\n"]}