@headless-lms/core 0.2.0 → 0.4.0
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.
- package/dist/assets/events.d.ts +42 -24
- package/dist/assets/events.js +1 -1
- package/dist/assets/events.js.map +1 -1
- package/dist/assets/file-urls.d.ts +20 -0
- package/dist/assets/file-urls.js +25 -0
- package/dist/assets/file-urls.js.map +1 -0
- package/dist/assets/index.d.ts +6 -4
- package/dist/assets/index.js +4 -2
- package/dist/assets/jobs.d.ts +15 -0
- package/dist/assets/jobs.js +30 -0
- package/dist/assets/jobs.js.map +1 -0
- package/dist/assets/model.d.ts +2 -2
- package/dist/assets/ports.d.ts +46 -15
- package/dist/assets/service.d.ts +38 -7
- package/dist/assets/service.js +287 -38
- package/dist/assets/service.js.map +1 -1
- package/dist/automations/actions.d.ts +13 -24
- package/dist/automations/actions.js +106 -42
- package/dist/automations/actions.js.map +1 -1
- package/dist/automations/catalog.js +226 -3
- package/dist/automations/catalog.js.map +1 -1
- package/dist/automations/community-derivations.d.ts +14 -0
- package/dist/automations/community-derivations.js +439 -0
- package/dist/automations/community-derivations.js.map +1 -0
- package/dist/automations/community-mail.d.ts +98 -0
- package/dist/automations/community-mail.js +70 -0
- package/dist/automations/community-mail.js.map +1 -0
- package/dist/automations/events.d.ts +18 -0
- package/dist/automations/index.d.ts +13 -4
- package/dist/automations/index.js +10 -2
- package/dist/automations/model.d.ts +10 -3
- package/dist/automations/model.js +15 -2
- package/dist/automations/model.js.map +1 -1
- package/dist/automations/notifications/debounce.d.ts +25 -0
- package/dist/automations/notifications/debounce.js +93 -0
- package/dist/automations/notifications/debounce.js.map +1 -0
- package/dist/automations/notifications/defaults.d.ts +8 -0
- package/dist/automations/notifications/defaults.js +316 -0
- package/dist/automations/notifications/defaults.js.map +1 -0
- package/dist/automations/notifications/delivery.d.ts +28 -0
- package/dist/automations/notifications/delivery.js +88 -0
- package/dist/automations/notifications/delivery.js.map +1 -0
- package/dist/automations/notifications/processor.d.ts +64 -0
- package/dist/automations/notifications/processor.js +270 -0
- package/dist/automations/notifications/processor.js.map +1 -0
- package/dist/automations/notifications/rules/content.d.ts +6 -0
- package/dist/automations/notifications/rules/content.js +39 -0
- package/dist/automations/notifications/rules/content.js.map +1 -0
- package/dist/automations/notifications/rules/discussion.d.ts +7 -0
- package/dist/automations/notifications/rules/discussion.js +678 -0
- package/dist/automations/notifications/rules/discussion.js.map +1 -0
- package/dist/automations/notifications/rules/entitlements.d.ts +6 -0
- package/dist/automations/notifications/rules/entitlements.js +249 -0
- package/dist/automations/notifications/rules/entitlements.js.map +1 -0
- package/dist/automations/notifications/rules/index.d.ts +6 -0
- package/dist/automations/notifications/rules/index.js +15 -0
- package/dist/automations/notifications/rules/index.js.map +1 -0
- package/dist/automations/notifications/rules/organizations.d.ts +6 -0
- package/dist/automations/notifications/rules/organizations.js +28 -0
- package/dist/automations/notifications/rules/organizations.js.map +1 -0
- package/dist/automations/notifications/rules/recipients.d.ts +32 -0
- package/dist/automations/notifications/rules/recipients.js +99 -0
- package/dist/automations/notifications/rules/recipients.js.map +1 -0
- package/dist/automations/notifications/service.d.ts +27 -0
- package/dist/automations/notifications/service.js +45 -0
- package/dist/automations/notifications/service.js.map +1 -0
- package/dist/automations/notifications/types.d.ts +7 -0
- package/dist/automations/notifications/types.js +473 -0
- package/dist/automations/notifications/types.js.map +1 -0
- package/dist/automations/ports.d.ts +121 -3
- package/dist/automations/schedule.d.ts +6 -0
- package/dist/automations/schedule.js +41 -0
- package/dist/automations/schedule.js.map +1 -0
- package/dist/automations/service.d.ts +20 -4
- package/dist/automations/service.js +246 -45
- package/dist/automations/service.js.map +1 -1
- package/dist/content/events.d.ts +712 -50
- package/dist/content/events.js +55 -1
- package/dist/content/events.js.map +1 -1
- package/dist/content/index.d.ts +6 -4
- package/dist/content/index.js +3 -1
- package/dist/content/model.d.ts +2 -2
- package/dist/content/notification-categories.d.ts +36 -0
- package/dist/content/notification-categories.js +64 -0
- package/dist/content/notification-categories.js.map +1 -0
- package/dist/content/ports.d.ts +90 -18
- package/dist/content/service.d.ts +57 -18
- package/dist/content/service.js +520 -25
- package/dist/content/service.js.map +1 -1
- package/dist/content/titles.d.ts +22 -0
- package/dist/content/titles.js +69 -0
- package/dist/content/titles.js.map +1 -0
- package/dist/content/types.d.ts +2 -2
- package/dist/discussion/authors.d.ts +10 -0
- package/dist/discussion/authors.js +23 -0
- package/dist/discussion/authors.js.map +1 -0
- package/dist/discussion/chats.d.ts +109 -0
- package/dist/discussion/chats.js +831 -0
- package/dist/discussion/chats.js.map +1 -0
- package/dist/discussion/digests.d.ts +24 -0
- package/dist/discussion/digests.js +104 -0
- package/dist/discussion/digests.js.map +1 -0
- package/dist/discussion/events.d.ts +785 -56
- package/dist/discussion/events.js +165 -5
- package/dist/discussion/events.js.map +1 -1
- package/dist/discussion/index.d.ts +12 -3
- package/dist/discussion/index.js +9 -1
- package/dist/discussion/jobs.d.ts +16 -0
- package/dist/discussion/jobs.js +40 -0
- package/dist/discussion/jobs.js.map +1 -0
- package/dist/discussion/mentions.d.ts +11 -0
- package/dist/discussion/mentions.js +35 -0
- package/dist/discussion/mentions.js.map +1 -0
- package/dist/discussion/ports.d.ts +372 -28
- package/dist/discussion/ports.js +6 -1
- package/dist/discussion/ports.js.map +1 -0
- package/dist/discussion/posts.d.ts +113 -0
- package/dist/discussion/posts.js +775 -0
- package/dist/discussion/posts.js.map +1 -0
- package/dist/discussion/service.d.ts +42 -11
- package/dist/discussion/service.js +364 -85
- package/dist/discussion/service.js.map +1 -1
- package/dist/discussion/subscriptions.d.ts +37 -0
- package/dist/discussion/subscriptions.js +99 -0
- package/dist/discussion/subscriptions.js.map +1 -0
- package/dist/discussion/titles.d.ts +26 -0
- package/dist/discussion/titles.js +84 -0
- package/dist/discussion/titles.js.map +1 -0
- package/dist/entitlements/events.d.ts +178 -11
- package/dist/entitlements/events.js +28 -1
- package/dist/entitlements/events.js.map +1 -1
- package/dist/entitlements/index.d.ts +6 -4
- package/dist/entitlements/index.js +3 -1
- package/dist/entitlements/jobs.d.ts +12 -0
- package/dist/entitlements/jobs.js +18 -0
- package/dist/entitlements/jobs.js.map +1 -0
- package/dist/entitlements/model.d.ts +3 -2
- package/dist/entitlements/ports.d.ts +128 -11
- package/dist/entitlements/rules.d.ts +12 -0
- package/dist/entitlements/rules.js +17 -0
- package/dist/entitlements/rules.js.map +1 -0
- package/dist/entitlements/service.d.ts +57 -3
- package/dist/entitlements/service.js +492 -2
- package/dist/entitlements/service.js.map +1 -1
- package/dist/identity/access.d.ts +6 -0
- package/dist/identity/access.js +7 -0
- package/dist/identity/access.js.map +1 -0
- package/dist/identity/devices.d.ts +30 -0
- package/dist/identity/devices.js +107 -0
- package/dist/identity/devices.js.map +1 -0
- package/dist/identity/events.d.ts +2 -2
- package/dist/identity/index.d.ts +7 -4
- package/dist/identity/index.js +4 -1
- package/dist/identity/model.d.ts +2 -1
- package/dist/identity/notification-settings.d.ts +28 -0
- package/dist/identity/notification-settings.js +113 -0
- package/dist/identity/notification-settings.js.map +1 -0
- package/dist/identity/ports.d.ts +97 -3
- package/dist/identity/service.d.ts +14 -3
- package/dist/identity/service.js +62 -5
- package/dist/identity/service.js.map +1 -1
- package/dist/integrations/events.d.ts +3 -3
- package/dist/integrations/model.js +1 -1
- package/dist/integrations/model.js.map +1 -1
- package/dist/integrations/service.js +2 -2
- package/dist/integrations/service.js.map +1 -1
- package/dist/organizations/access-scopes.d.ts +19 -0
- package/dist/organizations/access-scopes.js +89 -0
- package/dist/organizations/access-scopes.js.map +1 -0
- package/dist/organizations/events.d.ts +75 -33
- package/dist/organizations/events.js +6 -0
- package/dist/organizations/events.js.map +1 -1
- package/dist/organizations/index.d.ts +8 -6
- package/dist/organizations/index.js +5 -3
- package/dist/organizations/members.d.ts +8 -5
- package/dist/organizations/members.js.map +1 -1
- package/dist/organizations/notification-categories.d.ts +32 -0
- package/dist/organizations/notification-categories.js +51 -0
- package/dist/organizations/notification-categories.js.map +1 -0
- package/dist/organizations/ports.d.ts +17 -11
- package/dist/organizations/roles.d.ts +6 -10
- package/dist/organizations/roles.js +19 -7
- package/dist/organizations/roles.js.map +1 -1
- package/dist/organizations/service.d.ts +22 -11
- package/dist/organizations/service.js +164 -47
- package/dist/organizations/service.js.map +1 -1
- package/dist/organizations/types.d.ts +1 -10
- package/dist/progress/events.d.ts +14 -14
- package/dist/progress/index.d.ts +2 -2
- package/dist/progress/ports.d.ts +6 -1
- package/dist/progress/service.d.ts +3 -1
- package/dist/progress/service.js +6 -2
- package/dist/progress/service.js.map +1 -1
- package/dist/reporting/admin/index.d.ts +4 -0
- package/dist/reporting/admin/index.js +2 -0
- package/dist/reporting/admin/model.d.ts +108 -0
- package/dist/reporting/admin/ports.d.ts +27 -0
- package/dist/reporting/admin/service.d.ts +22 -0
- package/dist/reporting/admin/service.js +38 -0
- package/dist/reporting/admin/service.js.map +1 -0
- package/dist/reporting/member/index.d.ts +5 -0
- package/dist/reporting/member/index.js +2 -0
- package/dist/reporting/member/model.d.ts +157 -0
- package/dist/reporting/member/ports.d.ts +48 -0
- package/dist/reporting/member/service.d.ts +92 -0
- package/dist/reporting/member/service.js +364 -0
- package/dist/reporting/member/service.js.map +1 -0
- package/dist/shared/actor.d.ts +9 -0
- package/dist/shared/errors.d.ts +19 -1
- package/dist/shared/errors.js +27 -1
- package/dist/shared/errors.js.map +1 -1
- package/dist/shared/id.d.ts +14 -0
- package/dist/shared/id.js +15 -1
- package/dist/shared/id.js.map +1 -1
- package/dist/shared/mailer.d.ts +15 -2
- package/dist/shared/mailer.js +32 -5
- package/dist/shared/mailer.js.map +1 -1
- package/dist/shared/network.d.ts +10 -0
- package/dist/shared/network.js +211 -0
- package/dist/shared/network.js.map +1 -0
- package/dist/shared/notification-categories.d.ts +16 -0
- package/dist/shared/notification-categories.js +94 -0
- package/dist/shared/notification-categories.js.map +1 -0
- package/dist/shared/notification-settings.d.ts +14 -0
- package/dist/shared/notification-settings.js +61 -0
- package/dist/shared/notification-settings.js.map +1 -0
- package/dist/shared/ports.d.ts +48 -2
- package/dist/shared/settings.d.ts +2 -1
- package/dist/shared/settings.js +1 -0
- package/dist/shared/settings.js.map +1 -1
- package/dist/types/assets.d.ts +2 -2
- package/dist/types/automations.d.ts +15 -1
- package/dist/types/content.d.ts +2 -2
- package/dist/types/discussion.d.ts +11 -3
- package/dist/types/email-templates.d.ts +216 -1
- package/dist/types/entitlements.d.ts +2 -2
- package/dist/types/identity.d.ts +48 -2
- package/dist/types/index.d.ts +9 -8
- package/dist/types/notifications.d.ts +157 -0
- package/dist/types/ports.d.ts +6 -0
- package/dist/types/schemas/assets.d.ts +65 -37
- package/dist/types/schemas/assets.js +17 -12
- package/dist/types/schemas/assets.js.map +1 -1
- package/dist/types/schemas/automations.d.ts +9 -1
- package/dist/types/schemas/automations.js +13 -6
- package/dist/types/schemas/automations.js.map +1 -1
- package/dist/types/schemas/content.d.ts +396 -3
- package/dist/types/schemas/content.js +161 -3
- package/dist/types/schemas/content.js.map +1 -1
- package/dist/types/schemas/discussion.d.ts +443 -3
- package/dist/types/schemas/discussion.js +233 -4
- package/dist/types/schemas/discussion.js.map +1 -1
- package/dist/types/schemas/entitlements.d.ts +36 -2
- package/dist/types/schemas/entitlements.js +23 -1
- package/dist/types/schemas/entitlements.js.map +1 -1
- package/dist/types/schemas/identity.d.ts +27 -1
- package/dist/types/schemas/identity.js +14 -2
- package/dist/types/schemas/identity.js.map +1 -1
- package/dist/types/schemas/index.d.ts +9 -8
- package/dist/types/schemas/index.js +9 -8
- package/dist/types/schemas/notifications.d.ts +95 -0
- package/dist/types/schemas/notifications.js +56 -0
- package/dist/types/schemas/notifications.js.map +1 -0
- package/dist/types/schemas/organizations.d.ts +8 -0
- package/dist/types/schemas/organizations.js +2 -0
- package/dist/types/schemas/organizations.js.map +1 -1
- package/dist/types/schemas/progress.d.ts +3 -3
- package/dist/types/schemas/shared.d.ts +3 -1
- package/dist/types/schemas/shared.js +5 -1
- package/dist/types/schemas/shared.js.map +1 -1
- package/package.json +2 -2
- package/dist/reporting/courses/index.d.ts +0 -4
- package/dist/reporting/courses/index.js +0 -2
- package/dist/reporting/courses/model.d.ts +0 -24
- package/dist/reporting/courses/ports.d.ts +0 -13
- package/dist/reporting/courses/service.d.ts +0 -19
- package/dist/reporting/courses/service.js +0 -20
- package/dist/reporting/courses/service.js.map +0 -1
- package/dist/reporting/dashboard/index.d.ts +0 -4
- package/dist/reporting/dashboard/index.js +0 -2
- package/dist/reporting/dashboard/model.d.ts +0 -14
- package/dist/reporting/dashboard/ports.d.ts +0 -13
- package/dist/reporting/dashboard/service.d.ts +0 -19
- package/dist/reporting/dashboard/service.js +0 -20
- package/dist/reporting/dashboard/service.js.map +0 -1
- package/dist/reporting/learn/index.d.ts +0 -5
- package/dist/reporting/learn/index.js +0 -2
- package/dist/reporting/learn/model.d.ts +0 -23
- package/dist/reporting/learn/ports.d.ts +0 -43
- package/dist/reporting/learn/service.d.ts +0 -49
- package/dist/reporting/learn/service.js +0 -116
- package/dist/reporting/learn/service.js.map +0 -1
- package/dist/reporting/students/index.d.ts +0 -4
- package/dist/reporting/students/index.js +0 -2
- package/dist/reporting/students/model.d.ts +0 -51
- package/dist/reporting/students/model.js +0 -1
- package/dist/reporting/students/ports.d.ts +0 -15
- package/dist/reporting/students/ports.js +0 -1
- package/dist/reporting/students/service.d.ts +0 -20
- package/dist/reporting/students/service.js +0 -23
- package/dist/reporting/students/service.js.map +0 -1
- /package/dist/reporting/{courses → admin}/model.js +0 -0
- /package/dist/reporting/{courses → admin}/ports.js +0 -0
- /package/dist/reporting/{dashboard → member}/model.js +0 -0
- /package/dist/reporting/{dashboard → member}/ports.js +0 -0
- /package/dist/{reporting/learn/model.js → shared/actor.js} +0 -0
- /package/dist/{reporting/learn/ports.js → types/notifications.js} +0 -0
|
@@ -1,26 +1,80 @@
|
|
|
1
1
|
import { Page } from "../types/shared.js";
|
|
2
2
|
import { Logger } from "../types/ports.js";
|
|
3
|
-
import {
|
|
3
|
+
import { ContentType, Space } from "../types/schemas/content.js";
|
|
4
|
+
import { Entitlement, EntitlementsQuery, GrantEntitlementInput, SpaceMember } from "../types/schemas/entitlements.js";
|
|
4
5
|
import "../shared/ports.js";
|
|
6
|
+
import { Actor } from "../shared/actor.js";
|
|
7
|
+
import { CommunityReader } from "../content/ports.js";
|
|
8
|
+
import "../content/index.js";
|
|
5
9
|
import "./model.js";
|
|
6
|
-
import { EntitlementsRepository, EntitlementsService, EntitlementsUnitOfWork } from "./ports.js";
|
|
10
|
+
import { EntitlementsRepository, EntitlementsService, EntitlementsUnitOfWork, GrantTarget, OpenableSpace } from "./ports.js";
|
|
7
11
|
//#region src/entitlements/service.d.ts
|
|
8
12
|
type EntitlementsServiceParams = {
|
|
9
13
|
/** Read-only access (list) — runs outside any transaction. */
|
|
10
14
|
repo: EntitlementsRepository;
|
|
15
|
+
content: CommunityReader;
|
|
11
16
|
/** Atomic write scope: tx-bound repo + outbox appender. */
|
|
12
17
|
uow: EntitlementsUnitOfWork;
|
|
13
18
|
logger?: Logger;
|
|
14
19
|
};
|
|
15
20
|
declare class EntitlementsServiceImpl implements EntitlementsService {
|
|
16
21
|
private readonly repo;
|
|
22
|
+
private readonly content;
|
|
17
23
|
private readonly uow;
|
|
18
24
|
private readonly logger;
|
|
19
25
|
constructor(params: EntitlementsServiceParams);
|
|
20
26
|
list(orgId: string, query: EntitlementsQuery): Promise<Page<Entitlement>>;
|
|
21
27
|
grant(orgId: string, input: GrantEntitlementInput): Promise<Entitlement>;
|
|
28
|
+
ensureGrant(orgId: string, input: GrantEntitlementInput): Promise<Entitlement>;
|
|
22
29
|
setStatus(orgId: string, id: string, status: 'active' | 'revoked'): Promise<Entitlement | null>;
|
|
23
|
-
|
|
30
|
+
hasAccess(orgId: string, orgUserId: string, contentId: string): Promise<boolean>;
|
|
31
|
+
listAccessibleContent(orgId: string, orgUserId: string, type?: ContentType): Promise<string[]>;
|
|
32
|
+
isCommunityMember(orgId: string, actor: Actor, communityId: string): Promise<boolean>;
|
|
33
|
+
canOpenSpace(orgId: string, actor: Actor, spaceId: string): Promise<boolean>;
|
|
34
|
+
canOpen(orgId: string, actor: Actor, space: OpenableSpace): Promise<boolean>;
|
|
35
|
+
spaceStandings(orgId: string, actor: Actor, communityId: string): Promise<Record<string, SpaceMember['status']>>;
|
|
36
|
+
openSpacesFor(orgId: string, actor: Actor, communityId: string): Promise<Space[]>;
|
|
37
|
+
openSpaceIds(orgId: string, actor: Actor, communityId: string): Promise<string[]>;
|
|
38
|
+
communityMembersAmong(orgId: string, communityId: string, orgUserIds: string[]): Promise<string[]>;
|
|
39
|
+
spaceOpenersAmong(orgId: string, spaceId: string, orgUserIds: string[]): Promise<string[]>;
|
|
40
|
+
spaceOpeners(orgId: string, spaceId: string): Promise<string[]>;
|
|
41
|
+
communitiesOpenedBy(orgId: string, grant: GrantTarget): Promise<string[]>;
|
|
42
|
+
communityModeratorIds(orgId: string, communityId: string): Promise<string[]>;
|
|
43
|
+
communityIdsOpenedFor(orgId: string, orgUserId: string): Promise<string[]>;
|
|
44
|
+
openCommunityIds(orgId: string, actor: Actor): Promise<string[]>;
|
|
45
|
+
communityMemberIds(orgId: string, communityId: string): Promise<string[]>;
|
|
46
|
+
contentHolderIds(orgId: string, contentId: string): Promise<string[]>;
|
|
47
|
+
countActiveSpaceMembers(orgId: string, spaceId: string): Promise<number>;
|
|
48
|
+
requestJoin(orgId: string, actor: Actor, communityId: string, spaceId: string): Promise<SpaceMember>;
|
|
49
|
+
invite(orgId: string, actor: Actor, communityId: string, spaceId: string, orgUserId: string): Promise<SpaceMember>;
|
|
50
|
+
addSpaceMember(orgId: string, actor: Actor, communityId: string, spaceId: string, orgUserId: string): Promise<SpaceMember>;
|
|
51
|
+
accept(orgId: string, actor: Actor, communityId: string, spaceId: string): Promise<SpaceMember>;
|
|
52
|
+
approve(orgId: string, actor: Actor, communityId: string, spaceId: string, orgUserId: string): Promise<SpaceMember>;
|
|
53
|
+
decline(orgId: string, actor: Actor, communityId: string, spaceId: string, orgUserId: string): Promise<SpaceMember>;
|
|
54
|
+
refuse(orgId: string, actor: Actor, communityId: string, spaceId: string): Promise<SpaceMember>;
|
|
55
|
+
leave(orgId: string, actor: Actor, communityId: string, spaceId: string): Promise<SpaceMember>;
|
|
56
|
+
remove(orgId: string, actor: Actor, communityId: string, spaceId: string, orgUserId: string): Promise<SpaceMember>;
|
|
57
|
+
listSpaceMembers(orgId: string, actor: Actor, communityId: string, spaceId: string, query: {
|
|
58
|
+
page: number;
|
|
59
|
+
pageSize: number;
|
|
60
|
+
}): Promise<Page<SpaceMember>>;
|
|
61
|
+
expireDueGrants(now: Date): Promise<void>;
|
|
62
|
+
private expireDueIn;
|
|
63
|
+
private requireSpaceIn;
|
|
64
|
+
private requireVisibleSpaceIn;
|
|
65
|
+
private assertHasMembers;
|
|
66
|
+
private assertRule;
|
|
67
|
+
private assertHolder;
|
|
68
|
+
private assertGrant;
|
|
69
|
+
private assertHolds;
|
|
70
|
+
private assertCanManage;
|
|
71
|
+
private accessOf;
|
|
72
|
+
private createMember;
|
|
73
|
+
private insertMember;
|
|
74
|
+
private activate;
|
|
75
|
+
private deleteMember;
|
|
76
|
+
private lockSpace;
|
|
77
|
+
private assertRoom;
|
|
24
78
|
}
|
|
25
79
|
//#endregion
|
|
26
80
|
export { EntitlementsServiceImpl, EntitlementsServiceParams };
|
|
@@ -1,12 +1,19 @@
|
|
|
1
|
+
import { ConflictError, ForbiddenError, NotFoundError } from "../shared/errors.js";
|
|
1
2
|
import { noopLogger } from "../shared/logger.js";
|
|
3
|
+
import { canModerate, isStaffRole } from "../organizations/roles.js";
|
|
2
4
|
import { entitlementEvents } from "./events.js";
|
|
5
|
+
import { laterExpiry, opensSpace, seesSpace } from "./rules.js";
|
|
3
6
|
//#region src/entitlements/service.ts
|
|
7
|
+
const ENDING_SOON_MS = 10080 * 60 * 1e3;
|
|
8
|
+
const NOTICE_BATCH_SIZE = 100;
|
|
4
9
|
var EntitlementsServiceImpl = class {
|
|
5
10
|
repo;
|
|
11
|
+
content;
|
|
6
12
|
uow;
|
|
7
13
|
logger;
|
|
8
14
|
constructor(params) {
|
|
9
15
|
this.repo = params.repo;
|
|
16
|
+
this.content = params.content;
|
|
10
17
|
this.uow = params.uow;
|
|
11
18
|
this.logger = params.logger ?? noopLogger;
|
|
12
19
|
}
|
|
@@ -31,6 +38,50 @@ var EntitlementsServiceImpl = class {
|
|
|
31
38
|
});
|
|
32
39
|
return entitlement;
|
|
33
40
|
}
|
|
41
|
+
async ensureGrant(orgId, input) {
|
|
42
|
+
const { entitlement, change } = await this.uow.run(async ({ entitlements, outbox }) => {
|
|
43
|
+
const created = await entitlements.insertIfAbsent(orgId, input);
|
|
44
|
+
if (created) {
|
|
45
|
+
await outbox.append([entitlementEvents.entitlementCreated.make({
|
|
46
|
+
orgId,
|
|
47
|
+
data: created
|
|
48
|
+
})]);
|
|
49
|
+
return {
|
|
50
|
+
entitlement: created,
|
|
51
|
+
change: "created"
|
|
52
|
+
};
|
|
53
|
+
}
|
|
54
|
+
const existing = await entitlements.lockGrant(orgId, input);
|
|
55
|
+
if (!existing) throw new ConflictError("The grant changed while it was being given");
|
|
56
|
+
if (existing.status === "revoked") return {
|
|
57
|
+
entitlement: existing,
|
|
58
|
+
change: null
|
|
59
|
+
};
|
|
60
|
+
const expiresAt = laterExpiry(existing.expiresAt, input.expiresAt ?? null);
|
|
61
|
+
if (expiresAt?.getTime() === existing.expiresAt?.getTime()) return {
|
|
62
|
+
entitlement: existing,
|
|
63
|
+
change: null
|
|
64
|
+
};
|
|
65
|
+
const updated = await entitlements.setExpiresAt(orgId, existing.id, expiresAt);
|
|
66
|
+
if (!updated) throw new NotFoundError("Entitlement", existing.id);
|
|
67
|
+
await outbox.append([entitlementEvents.entitlementUpdated.make({
|
|
68
|
+
orgId,
|
|
69
|
+
data: updated
|
|
70
|
+
})]);
|
|
71
|
+
return {
|
|
72
|
+
entitlement: updated,
|
|
73
|
+
change: "updated"
|
|
74
|
+
};
|
|
75
|
+
});
|
|
76
|
+
if (change) this.logger.info(change === "created" ? "entitlement granted" : "entitlement extended", {
|
|
77
|
+
orgId,
|
|
78
|
+
entitlementId: entitlement.id,
|
|
79
|
+
orgUserId: entitlement.orgUserId,
|
|
80
|
+
bundleId: entitlement.bundleId,
|
|
81
|
+
contentId: entitlement.contentId
|
|
82
|
+
});
|
|
83
|
+
return entitlement;
|
|
84
|
+
}
|
|
34
85
|
async setStatus(orgId, id, status) {
|
|
35
86
|
const entitlement = await this.uow.run(async ({ entitlements, outbox }) => {
|
|
36
87
|
const updated = await entitlements.setStatus(orgId, id, status);
|
|
@@ -51,10 +102,449 @@ var EntitlementsServiceImpl = class {
|
|
|
51
102
|
});
|
|
52
103
|
return entitlement;
|
|
53
104
|
}
|
|
54
|
-
|
|
55
|
-
return this.repo.
|
|
105
|
+
hasAccess(orgId, orgUserId, contentId) {
|
|
106
|
+
return this.repo.hasAccess(orgId, orgUserId, contentId);
|
|
107
|
+
}
|
|
108
|
+
listAccessibleContent(orgId, orgUserId, type) {
|
|
109
|
+
return this.repo.listAccessibleContent(orgId, orgUserId, type);
|
|
110
|
+
}
|
|
111
|
+
async isCommunityMember(orgId, actor, communityId) {
|
|
112
|
+
return (await this.accessOf(orgId, actor.id, communityId, null))?.member === true;
|
|
113
|
+
}
|
|
114
|
+
async canOpenSpace(orgId, actor, spaceId) {
|
|
115
|
+
const space = await this.content.getSpace(orgId, spaceId);
|
|
116
|
+
if (!space) return false;
|
|
117
|
+
return this.canOpen(orgId, actor, space);
|
|
118
|
+
}
|
|
119
|
+
async canOpen(orgId, actor, space) {
|
|
120
|
+
const facts = await this.accessOf(orgId, actor.id, space.communityId, space.id);
|
|
121
|
+
if (!facts) return false;
|
|
122
|
+
return opensSpace(facts.role, facts.member, space.accessRule, facts.activeInSpace);
|
|
123
|
+
}
|
|
124
|
+
async spaceStandings(orgId, actor, communityId) {
|
|
125
|
+
const rows = await this.repo.listSpaceMembersFor(orgId, actor.id, communityId);
|
|
126
|
+
return Object.fromEntries(rows.map((row) => [row.spaceId, row.status]));
|
|
127
|
+
}
|
|
128
|
+
async openSpacesFor(orgId, actor, communityId) {
|
|
129
|
+
const facts = await this.accessOf(orgId, actor.id, communityId, null);
|
|
130
|
+
if (!facts?.member) return [];
|
|
131
|
+
const [spaces, activeIds] = await Promise.all([this.content.listSpaces(orgId, communityId), this.repo.activeSpaceIds(orgId, actor.id, communityId)]);
|
|
132
|
+
const active = new Set(activeIds);
|
|
133
|
+
return spaces.filter((space) => opensSpace(facts.role, facts.member, space.accessRule, active.has(space.id)));
|
|
134
|
+
}
|
|
135
|
+
async openSpaceIds(orgId, actor, communityId) {
|
|
136
|
+
return (await this.openSpacesFor(orgId, actor, communityId)).map((space) => space.id);
|
|
137
|
+
}
|
|
138
|
+
async communityMembersAmong(orgId, communityId, orgUserIds) {
|
|
139
|
+
if (orgUserIds.length === 0) return [];
|
|
140
|
+
return (await this.repo.accessAmong(orgId, communityId, null, orgUserIds)).filter((fact) => fact.member).map((fact) => fact.orgUserId);
|
|
141
|
+
}
|
|
142
|
+
async spaceOpenersAmong(orgId, spaceId, orgUserIds) {
|
|
143
|
+
if (orgUserIds.length === 0) return [];
|
|
144
|
+
const space = await this.content.getSpace(orgId, spaceId);
|
|
145
|
+
if (!space) return [];
|
|
146
|
+
return (await this.repo.accessAmong(orgId, space.communityId, space.id, orgUserIds)).filter((fact) => opensSpace(fact.role, fact.member, space.accessRule, fact.activeInSpace)).map((fact) => fact.orgUserId);
|
|
147
|
+
}
|
|
148
|
+
async spaceOpeners(orgId, spaceId) {
|
|
149
|
+
const space = await this.content.getSpace(orgId, spaceId);
|
|
150
|
+
if (!space) return [];
|
|
151
|
+
const members = await this.repo.communityMemberIds(orgId, space.communityId);
|
|
152
|
+
return this.spaceOpenersAmong(orgId, spaceId, members);
|
|
153
|
+
}
|
|
154
|
+
communitiesOpenedBy(orgId, grant) {
|
|
155
|
+
return this.repo.communitiesOpenedBy(orgId, grant);
|
|
156
|
+
}
|
|
157
|
+
communityModeratorIds(orgId, communityId) {
|
|
158
|
+
return this.repo.communityModeratorIds(orgId, communityId);
|
|
159
|
+
}
|
|
160
|
+
communityIdsOpenedFor(orgId, orgUserId) {
|
|
161
|
+
return this.repo.communityIdsOpenedFor(orgId, orgUserId);
|
|
162
|
+
}
|
|
163
|
+
openCommunityIds(orgId, actor) {
|
|
164
|
+
return this.repo.openCommunityIds(orgId, actor.id);
|
|
165
|
+
}
|
|
166
|
+
communityMemberIds(orgId, communityId) {
|
|
167
|
+
return this.repo.communityMemberIds(orgId, communityId);
|
|
168
|
+
}
|
|
169
|
+
contentHolderIds(orgId, contentId) {
|
|
170
|
+
return this.repo.contentHolderIds(orgId, contentId);
|
|
171
|
+
}
|
|
172
|
+
countActiveSpaceMembers(orgId, spaceId) {
|
|
173
|
+
return this.repo.countActiveSpaceMembers(orgId, spaceId);
|
|
174
|
+
}
|
|
175
|
+
async requestJoin(orgId, actor, communityId, spaceId) {
|
|
176
|
+
const space = await this.requireVisibleSpaceIn(orgId, actor, communityId, spaceId);
|
|
177
|
+
this.assertRule(space.accessRule, "by_request");
|
|
178
|
+
if (canModerate(actor.role)) throw new ForbiddenError("Staff do not request a place in a space");
|
|
179
|
+
await this.assertGrant(orgId, actor, actor.id, space.communityId);
|
|
180
|
+
const created = await this.createMember(orgId, actor, space, actor.id, "requested", "request");
|
|
181
|
+
this.logger.info("space member created", {
|
|
182
|
+
orgId,
|
|
183
|
+
spaceId,
|
|
184
|
+
orgUserId: actor.id,
|
|
185
|
+
status: created.status,
|
|
186
|
+
actorId: actor.id
|
|
187
|
+
});
|
|
188
|
+
return created;
|
|
189
|
+
}
|
|
190
|
+
async invite(orgId, actor, communityId, spaceId, orgUserId) {
|
|
191
|
+
const space = await this.requireVisibleSpaceIn(orgId, actor, communityId, spaceId);
|
|
192
|
+
this.assertRule(space.accessRule, "by_invite");
|
|
193
|
+
await this.assertCanManage(orgId, actor, space.communityId);
|
|
194
|
+
await this.assertHolds(orgId, actor, orgUserId, space.communityId);
|
|
195
|
+
const created = await this.createMember(orgId, actor, space, orgUserId, "invited", "invite");
|
|
196
|
+
this.logger.info("space member created", {
|
|
197
|
+
orgId,
|
|
198
|
+
spaceId,
|
|
199
|
+
orgUserId,
|
|
200
|
+
status: created.status,
|
|
201
|
+
actorId: actor.id
|
|
202
|
+
});
|
|
203
|
+
return created;
|
|
204
|
+
}
|
|
205
|
+
async addSpaceMember(orgId, actor, communityId, spaceId, orgUserId) {
|
|
206
|
+
const space = await this.requireVisibleSpaceIn(orgId, actor, communityId, spaceId);
|
|
207
|
+
this.assertHasMembers(space.accessRule);
|
|
208
|
+
await this.assertCanManage(orgId, actor, space.communityId);
|
|
209
|
+
await this.assertHolds(orgId, actor, orgUserId, space.communityId);
|
|
210
|
+
const created = await this.uow.run(async (scope) => {
|
|
211
|
+
const locked = await this.lockSpace(scope.spaces, orgId, spaceId);
|
|
212
|
+
this.assertHasMembers(locked.accessRule);
|
|
213
|
+
return this.insertMember(scope.entitlements, scope.outbox, orgId, actor, {
|
|
214
|
+
communityId: space.communityId,
|
|
215
|
+
spaceId,
|
|
216
|
+
orgUserId,
|
|
217
|
+
status: "active",
|
|
218
|
+
source: "manual"
|
|
219
|
+
}, locked);
|
|
220
|
+
});
|
|
221
|
+
this.logger.info("space member created", {
|
|
222
|
+
orgId,
|
|
223
|
+
spaceId,
|
|
224
|
+
orgUserId,
|
|
225
|
+
status: created.status,
|
|
226
|
+
actorId: actor.id
|
|
227
|
+
});
|
|
228
|
+
return created;
|
|
229
|
+
}
|
|
230
|
+
async accept(orgId, actor, communityId, spaceId) {
|
|
231
|
+
this.assertHolder(actor);
|
|
232
|
+
const space = await this.requireVisibleSpaceIn(orgId, actor, communityId, spaceId);
|
|
233
|
+
this.assertRule(space.accessRule, "by_invite");
|
|
234
|
+
await this.assertGrant(orgId, actor, actor.id, space.communityId);
|
|
235
|
+
return this.activate(orgId, actor, spaceId, actor.id, "by_invite", "invited", () => ownRowMissing("invited"));
|
|
236
|
+
}
|
|
237
|
+
async approve(orgId, actor, communityId, spaceId, orgUserId) {
|
|
238
|
+
const space = await this.requireVisibleSpaceIn(orgId, actor, communityId, spaceId);
|
|
239
|
+
this.assertRule(space.accessRule, "by_request");
|
|
240
|
+
await this.assertCanManage(orgId, actor, space.communityId);
|
|
241
|
+
await this.assertGrant(orgId, actor, orgUserId, space.communityId);
|
|
242
|
+
return this.activate(orgId, actor, spaceId, orgUserId, "by_request", "requested", () => new NotFoundError("Space member", orgUserId));
|
|
243
|
+
}
|
|
244
|
+
async decline(orgId, actor, communityId, spaceId, orgUserId) {
|
|
245
|
+
const space = await this.requireVisibleSpaceIn(orgId, actor, communityId, spaceId);
|
|
246
|
+
await this.assertCanManage(orgId, actor, space.communityId);
|
|
247
|
+
return this.deleteMember(orgId, actor, spaceId, orgUserId, "requested", () => new NotFoundError("Space member", orgUserId));
|
|
248
|
+
}
|
|
249
|
+
async refuse(orgId, actor, communityId, spaceId) {
|
|
250
|
+
const space = await this.requireVisibleSpaceIn(orgId, actor, communityId, spaceId);
|
|
251
|
+
this.assertHolder(actor);
|
|
252
|
+
await this.assertGrant(orgId, actor, actor.id, space.communityId);
|
|
253
|
+
return this.deleteMember(orgId, actor, spaceId, actor.id, "invited", () => ownRowMissing("invited"));
|
|
254
|
+
}
|
|
255
|
+
async leave(orgId, actor, communityId, spaceId) {
|
|
256
|
+
const space = await this.requireVisibleSpaceIn(orgId, actor, communityId, spaceId);
|
|
257
|
+
this.assertHolder(actor);
|
|
258
|
+
await this.assertGrant(orgId, actor, actor.id, space.communityId);
|
|
259
|
+
return this.deleteMember(orgId, actor, spaceId, actor.id, "active", () => ownRowMissing("active"));
|
|
260
|
+
}
|
|
261
|
+
async remove(orgId, actor, communityId, spaceId, orgUserId) {
|
|
262
|
+
const space = await this.requireVisibleSpaceIn(orgId, actor, communityId, spaceId);
|
|
263
|
+
this.assertHasMembers(space.accessRule);
|
|
264
|
+
await this.assertCanManage(orgId, actor, space.communityId);
|
|
265
|
+
const { updated, previousStatus } = await this.uow.run(async ({ entitlements, spaces, outbox }) => {
|
|
266
|
+
const locked = await this.lockSpace(spaces, orgId, spaceId);
|
|
267
|
+
this.assertHasMembers(locked.accessRule);
|
|
268
|
+
const row = await entitlements.lockSpaceMember(orgId, spaceId, orgUserId);
|
|
269
|
+
if (!row) {
|
|
270
|
+
this.logger.warn("space member remove rejected — not found", {
|
|
271
|
+
orgId,
|
|
272
|
+
spaceId,
|
|
273
|
+
orgUserId,
|
|
274
|
+
actorId: actor.id
|
|
275
|
+
});
|
|
276
|
+
throw new NotFoundError("Space member", orgUserId);
|
|
277
|
+
}
|
|
278
|
+
if (row.status === "revoked") throw new ConflictError("This person is already removed from the space");
|
|
279
|
+
const next = await entitlements.updateSpaceMemberStatus(orgId, row.id, "revoked");
|
|
280
|
+
if (!next) throw new NotFoundError("Space member", row.id);
|
|
281
|
+
await outbox.append([entitlementEvents.spaceMemberUpdated.make({
|
|
282
|
+
orgId,
|
|
283
|
+
data: next,
|
|
284
|
+
metadata: {
|
|
285
|
+
actorOrgUserId: actor.id,
|
|
286
|
+
previousStatus: row.status
|
|
287
|
+
}
|
|
288
|
+
})]);
|
|
289
|
+
return {
|
|
290
|
+
updated: next,
|
|
291
|
+
previousStatus: row.status
|
|
292
|
+
};
|
|
293
|
+
});
|
|
294
|
+
this.logger.info("space member removed", {
|
|
295
|
+
orgId,
|
|
296
|
+
spaceId,
|
|
297
|
+
orgUserId,
|
|
298
|
+
previousStatus,
|
|
299
|
+
actorId: actor.id
|
|
300
|
+
});
|
|
301
|
+
return updated;
|
|
302
|
+
}
|
|
303
|
+
async listSpaceMembers(orgId, actor, communityId, spaceId, query) {
|
|
304
|
+
const space = await this.requireVisibleSpaceIn(orgId, actor, communityId, spaceId);
|
|
305
|
+
if (!await this.canOpen(orgId, actor, space)) throw new NotFoundError("Space", spaceId);
|
|
306
|
+
return this.repo.listSpaceMembers(orgId, spaceId, {
|
|
307
|
+
...query,
|
|
308
|
+
...canModerate(actor.role) ? {} : { statuses: ["active"] }
|
|
309
|
+
});
|
|
310
|
+
}
|
|
311
|
+
async expireDueGrants(now) {
|
|
312
|
+
const orgIds = await this.repo.orgIdsWithDueNotices(now, new Date(now.getTime() + ENDING_SOON_MS));
|
|
313
|
+
let failed = 0;
|
|
314
|
+
for (const orgId of orgIds) try {
|
|
315
|
+
await this.expireDueIn(orgId, now);
|
|
316
|
+
} catch (err) {
|
|
317
|
+
failed += 1;
|
|
318
|
+
this.logger.error("due grants not expired", {
|
|
319
|
+
orgId,
|
|
320
|
+
err
|
|
321
|
+
});
|
|
322
|
+
}
|
|
323
|
+
if (failed > 0) throw new Error(`due grants of ${failed} orgs could not be expired`);
|
|
324
|
+
}
|
|
325
|
+
async expireDueIn(orgId, now) {
|
|
326
|
+
const until = new Date(now.getTime() + ENDING_SOON_MS);
|
|
327
|
+
let appended = 0;
|
|
328
|
+
let afterId;
|
|
329
|
+
for (;;) {
|
|
330
|
+
const batch = await this.uow.run(async ({ entitlements, outbox }) => {
|
|
331
|
+
const events = [];
|
|
332
|
+
const grants = await entitlements.listGrantsExpiredBy(orgId, now, NOTICE_BATCH_SIZE, afterId);
|
|
333
|
+
for (const grant of grants) {
|
|
334
|
+
if (!grant.expiresAt) continue;
|
|
335
|
+
if (await entitlements.claimNotice(orgId, {
|
|
336
|
+
kind: "expired",
|
|
337
|
+
entitlementId: grant.id,
|
|
338
|
+
expiresAt: grant.expiresAt
|
|
339
|
+
}, now)) events.push(entitlementEvents.entitlementExpired.make({
|
|
340
|
+
orgId,
|
|
341
|
+
data: grant
|
|
342
|
+
}));
|
|
343
|
+
}
|
|
344
|
+
if (events.length > 0) await outbox.append(events);
|
|
345
|
+
return {
|
|
346
|
+
events: events.length,
|
|
347
|
+
last: grants.at(-1)?.id,
|
|
348
|
+
size: grants.length
|
|
349
|
+
};
|
|
350
|
+
});
|
|
351
|
+
appended += batch.events;
|
|
352
|
+
if (batch.size < NOTICE_BATCH_SIZE || batch.last === void 0) break;
|
|
353
|
+
afterId = batch.last;
|
|
354
|
+
}
|
|
355
|
+
let afterKey;
|
|
356
|
+
for (;;) {
|
|
357
|
+
const batch = await this.uow.run(async ({ entitlements, outbox }) => {
|
|
358
|
+
const events = [];
|
|
359
|
+
const ending = await entitlements.listGrantsEndingBetween(orgId, now, until, NOTICE_BATCH_SIZE, afterKey);
|
|
360
|
+
for (const { grant, communityId } of ending) {
|
|
361
|
+
if (!grant.expiresAt) continue;
|
|
362
|
+
if (await entitlements.claimNotice(orgId, {
|
|
363
|
+
kind: "ending_soon",
|
|
364
|
+
entitlementId: grant.id,
|
|
365
|
+
communityId,
|
|
366
|
+
expiresAt: grant.expiresAt
|
|
367
|
+
}, now)) events.push(entitlementEvents.entitlementEndingSoon.make({
|
|
368
|
+
orgId,
|
|
369
|
+
data: {
|
|
370
|
+
...grant,
|
|
371
|
+
communityId
|
|
372
|
+
}
|
|
373
|
+
}));
|
|
374
|
+
}
|
|
375
|
+
if (events.length > 0) await outbox.append(events);
|
|
376
|
+
const last = ending.at(-1);
|
|
377
|
+
return {
|
|
378
|
+
events: events.length,
|
|
379
|
+
last: last && {
|
|
380
|
+
entitlementId: last.grant.id,
|
|
381
|
+
communityId: last.communityId
|
|
382
|
+
},
|
|
383
|
+
size: ending.length
|
|
384
|
+
};
|
|
385
|
+
});
|
|
386
|
+
appended += batch.events;
|
|
387
|
+
if (batch.size < NOTICE_BATCH_SIZE || batch.last === void 0) break;
|
|
388
|
+
afterKey = batch.last;
|
|
389
|
+
}
|
|
390
|
+
if (appended > 0) this.logger.info("due grants expired", {
|
|
391
|
+
orgId,
|
|
392
|
+
events: appended
|
|
393
|
+
});
|
|
394
|
+
}
|
|
395
|
+
async requireSpaceIn(orgId, communityId, spaceId) {
|
|
396
|
+
const space = await this.content.getSpace(orgId, spaceId);
|
|
397
|
+
if (!space || space.communityId !== communityId) {
|
|
398
|
+
this.logger.warn("space members rejected — space not found", {
|
|
399
|
+
orgId,
|
|
400
|
+
communityId,
|
|
401
|
+
spaceId
|
|
402
|
+
});
|
|
403
|
+
throw new NotFoundError("Space", spaceId);
|
|
404
|
+
}
|
|
405
|
+
return space;
|
|
406
|
+
}
|
|
407
|
+
async requireVisibleSpaceIn(orgId, actor, communityId, spaceId) {
|
|
408
|
+
const space = await this.requireSpaceIn(orgId, communityId, spaceId);
|
|
409
|
+
if (seesSpace(actor.role, space.accessRule, void 0)) return space;
|
|
410
|
+
const row = await this.repo.findSpaceMember(orgId, spaceId, actor.id);
|
|
411
|
+
if (!seesSpace(actor.role, space.accessRule, row?.status)) throw new NotFoundError("Space", spaceId);
|
|
412
|
+
return space;
|
|
413
|
+
}
|
|
414
|
+
assertHasMembers(accessRule) {
|
|
415
|
+
if (accessRule === "all_members") throw new ConflictError("An all-members space has no space members");
|
|
416
|
+
}
|
|
417
|
+
assertRule(accessRule, expected) {
|
|
418
|
+
this.assertHasMembers(accessRule);
|
|
419
|
+
if (accessRule !== expected) throw new ConflictError(accessRule === "by_invite" ? "This space is by invitation" : "This space is by request");
|
|
420
|
+
}
|
|
421
|
+
assertHolder(actor) {
|
|
422
|
+
if (isStaffRole(actor.role)) throw new ForbiddenError("Staff do not hold a place in a space");
|
|
423
|
+
}
|
|
424
|
+
async assertGrant(orgId, actor, orgUserId, communityId) {
|
|
425
|
+
if (!await this.repo.hasCommunityAccess(orgId, orgUserId, communityId)) throw new ForbiddenError(orgUserId === actor.id ? "You are not a member of this community" : "That person is not a member of this community");
|
|
426
|
+
}
|
|
427
|
+
async assertHolds(orgId, actor, orgUserId, communityId) {
|
|
428
|
+
const facts = await this.accessOf(orgId, orgUserId, communityId, null);
|
|
429
|
+
if (facts && isStaffRole(facts.role)) throw new ForbiddenError("Staff do not hold a place in a space");
|
|
430
|
+
await this.assertGrant(orgId, actor, orgUserId, communityId);
|
|
431
|
+
}
|
|
432
|
+
async assertCanManage(orgId, actor, communityId) {
|
|
433
|
+
const facts = await this.accessOf(orgId, actor.id, communityId, null);
|
|
434
|
+
if (facts?.member && canModerate(facts.role)) return;
|
|
435
|
+
throw new ForbiddenError("You cannot manage members of this space");
|
|
436
|
+
}
|
|
437
|
+
async accessOf(orgId, orgUserId, communityId, spaceId) {
|
|
438
|
+
const [facts] = await this.repo.accessAmong(orgId, communityId, spaceId, [orgUserId]);
|
|
439
|
+
return facts ?? null;
|
|
440
|
+
}
|
|
441
|
+
async createMember(orgId, actor, space, orgUserId, status, source) {
|
|
442
|
+
return this.uow.run(async ({ entitlements, outbox }) => this.insertMember(entitlements, outbox, orgId, actor, {
|
|
443
|
+
communityId: space.communityId,
|
|
444
|
+
spaceId: space.id,
|
|
445
|
+
orgUserId,
|
|
446
|
+
status,
|
|
447
|
+
source
|
|
448
|
+
}));
|
|
449
|
+
}
|
|
450
|
+
async insertMember(entitlements, outbox, orgId, actor, input, room) {
|
|
451
|
+
const existing = await entitlements.lockSpaceMember(orgId, input.spaceId, input.orgUserId);
|
|
452
|
+
if (existing && existing.status !== "revoked") throw new ConflictError(input.orgUserId === actor.id ? "You already have a place in this space" : "That person already has a place in this space");
|
|
453
|
+
if (room) await this.assertRoom(entitlements, orgId, room);
|
|
454
|
+
if (existing) await entitlements.deleteSpaceMember(orgId, existing.id);
|
|
455
|
+
const created = await entitlements.insertSpaceMember(orgId, input);
|
|
456
|
+
await outbox.append([entitlementEvents.spaceMemberCreated.make({
|
|
457
|
+
orgId,
|
|
458
|
+
data: created,
|
|
459
|
+
metadata: { actorOrgUserId: actor.id }
|
|
460
|
+
})]);
|
|
461
|
+
return created;
|
|
462
|
+
}
|
|
463
|
+
async activate(orgId, actor, spaceId, orgUserId, rule, from, missing) {
|
|
464
|
+
const updated = await this.uow.run(async ({ entitlements, spaces, outbox }) => {
|
|
465
|
+
const locked = await this.lockSpace(spaces, orgId, spaceId);
|
|
466
|
+
this.assertRule(locked.accessRule, rule);
|
|
467
|
+
const row = await entitlements.lockSpaceMember(orgId, spaceId, orgUserId);
|
|
468
|
+
if (row?.status === "active") throw alreadyPlaced(actor, orgUserId);
|
|
469
|
+
if (!row || row.status !== from) {
|
|
470
|
+
this.logger.warn("space member activate rejected — not found", {
|
|
471
|
+
orgId,
|
|
472
|
+
spaceId,
|
|
473
|
+
orgUserId,
|
|
474
|
+
actorId: actor.id
|
|
475
|
+
});
|
|
476
|
+
throw missing();
|
|
477
|
+
}
|
|
478
|
+
await this.assertRoom(entitlements, orgId, locked);
|
|
479
|
+
const next = await entitlements.updateSpaceMemberStatus(orgId, row.id, "active");
|
|
480
|
+
if (!next) throw missing();
|
|
481
|
+
await outbox.append([entitlementEvents.spaceMemberUpdated.make({
|
|
482
|
+
orgId,
|
|
483
|
+
data: next,
|
|
484
|
+
metadata: {
|
|
485
|
+
actorOrgUserId: actor.id,
|
|
486
|
+
previousStatus: from
|
|
487
|
+
}
|
|
488
|
+
})]);
|
|
489
|
+
return next;
|
|
490
|
+
});
|
|
491
|
+
this.logger.info("space member activated", {
|
|
492
|
+
orgId,
|
|
493
|
+
spaceId,
|
|
494
|
+
orgUserId,
|
|
495
|
+
actorId: actor.id
|
|
496
|
+
});
|
|
497
|
+
return updated;
|
|
498
|
+
}
|
|
499
|
+
async deleteMember(orgId, actor, spaceId, orgUserId, from, missing) {
|
|
500
|
+
const row = await this.uow.run(async ({ entitlements, outbox }) => {
|
|
501
|
+
const current = await entitlements.lockSpaceMember(orgId, spaceId, orgUserId);
|
|
502
|
+
if (from !== "active" && current?.status === "active") throw alreadyPlaced(actor, orgUserId);
|
|
503
|
+
if (!current || current.status !== from) {
|
|
504
|
+
this.logger.warn("space member delete rejected — not found", {
|
|
505
|
+
orgId,
|
|
506
|
+
spaceId,
|
|
507
|
+
orgUserId,
|
|
508
|
+
actorId: actor.id
|
|
509
|
+
});
|
|
510
|
+
throw missing();
|
|
511
|
+
}
|
|
512
|
+
await entitlements.deleteSpaceMember(orgId, current.id);
|
|
513
|
+
await outbox.append([entitlementEvents.spaceMemberDeleted.make({
|
|
514
|
+
orgId,
|
|
515
|
+
data: current,
|
|
516
|
+
metadata: {
|
|
517
|
+
actorOrgUserId: actor.id,
|
|
518
|
+
previousStatus: from
|
|
519
|
+
}
|
|
520
|
+
})]);
|
|
521
|
+
return current;
|
|
522
|
+
});
|
|
523
|
+
this.logger.info("space member deleted", {
|
|
524
|
+
orgId,
|
|
525
|
+
spaceId,
|
|
526
|
+
orgUserId,
|
|
527
|
+
previousStatus: from,
|
|
528
|
+
actorId: actor.id
|
|
529
|
+
});
|
|
530
|
+
return row;
|
|
531
|
+
}
|
|
532
|
+
async lockSpace(spaces, orgId, spaceId) {
|
|
533
|
+
const locked = await spaces.lockSpace(orgId, spaceId);
|
|
534
|
+
if (!locked) throw new NotFoundError("Space", spaceId);
|
|
535
|
+
return locked;
|
|
536
|
+
}
|
|
537
|
+
async assertRoom(entitlements, orgId, space) {
|
|
538
|
+
if (space.capacity === null) return;
|
|
539
|
+
if (await entitlements.countActiveSpaceMembers(orgId, space.id) >= space.capacity) throw new ConflictError("This space is full");
|
|
56
540
|
}
|
|
57
541
|
};
|
|
542
|
+
function alreadyPlaced(actor, orgUserId) {
|
|
543
|
+
return new ConflictError(orgUserId === actor.id ? "You already have a place in this space" : "That person already has a place in this space");
|
|
544
|
+
}
|
|
545
|
+
function ownRowMissing(status) {
|
|
546
|
+
return new ConflictError(status === "invited" ? "You do not have an invitation to this space" : "You are not a member of this space");
|
|
547
|
+
}
|
|
58
548
|
//#endregion
|
|
59
549
|
export { EntitlementsServiceImpl };
|
|
60
550
|
|