Sharing
The sharing status of a board and the sharing groups through which a teacher shares boards with chosen peers, with the SharingGroupsService.
The sharing module models who can see a teacher's boards beyond the teacher. A board's Sharing value says whether it is private, public or shared, and a sharing group is the set of peer accounts a teacher shares with. Each account owns at most one sharing group and can be a member of many. The teacher app's sharing settings, the API, the admin import and the bulk claim all go through the service.
Import from @workspace/training/domain/sharing and @workspace/training/application/sharing.
Domain
Sharing
| Export | Definition |
|---|---|
SHARING | { PRIVATE: "private", PUBLIC: "public", SHARED: "shared" } |
SHARING_VALUES | ["private", "public", "shared"] |
Sharing | Union of the three literals, carried by boards |
const status: Sharing = SHARING.SHARED; // => "shared"
SHARING_VALUES; // => ["private", "public", "shared"]
export const isShared = status === SHARING.SHARED;Sharing group
Prop
Type
Prop
Type
Prop
Type
SharingGroupMembership is the view from the member's side: the groups an account was added to, with the owner's display fields.
The module has no pure functions.
Application
SharingGroupsService
createSharingGroupsService(deps: SharingGroupsServiceDeps): SharingGroupsService
| Dependency | Type | Role |
|---|---|---|
sharingGroups | SharingGroupsRepository | Group writes. |
sharingGroupsRead | SharingGroupsReadRepository | Group reads. |
members | SharingGroupMembersRepository | The member links. |
entitlements | EntitlementsGate | The plan check before addMember. |
Owner-scoped methods use ctx.userId. The id-scoped ones that read or change a group (update, upsert with an id, getMembers, getMemberCount, addMember, removeMember) belong to the group's owner: anyone else, a member included, gets AccessDeniedError before anything is read, and a missing group gets the same answer. The ServiceContext comes last.
| Method | Signature | Description |
|---|---|---|
getByOwner | (ctx) => Promise<SharingGroup | null> | The caller's own group. |
create | (name: string | undefined, ctx) => Promise<SharingGroup> | Owned by ctx.userId. |
update | (groupId: string, updates: UpdateSharingGroupDTO, ctx) => Promise<SharingGroup> | Owner only. description is accepted and not persisted. |
upsert | (groupId: string | null, updates: UpdateSharingGroupDTO, ctx) => Promise<SharingGroup> | Updates when groupId is given (owner only), otherwise creates for the caller with updates.name. |
getMembers | (sharingGroupId: string, ctx) => Promise<Account[]> | Owner only: full accounts of the members, emails included. |
getMemberCount | (sharingGroupId: string, ctx) => Promise<number> | Owner only. |
addMember | (sharingGroupId: string, userIdToAdd: string, ctx) => Promise<void> | Owner only. Checks the ownership, then the members per sharing group limit, then adds the link. Adding an existing member is a no-op. |
ensureWithMembers | (ownerId: string, name: string, memberIds: readonly string[], ctx) => Promise<EnsureWithMembersResult> | Idempotent: reaches the owner's group named name, creating it when missing, and unions memberIds into it. ownerId is explicit because the admin and import paths act on another owner's group. Returns { id, createdNow, added }. |
removeMember | (sharingGroupId: string, userIdToRemove: string, ctx) => Promise<void> | Owner only. |
leaveSharingGroup | (sharingGroupId: string, ctx) => Promise<void> | The caller leaves. Throws AccessDeniedError when the caller owns that group: owners delete, they do not leave. |
getMemberships | (ctx) => Promise<SharingGroupMembership[]> | The groups the caller is a member of, newest first. |
AccountSharingGroupsService is SharingGroupsService without ensureWithMembers. The API types its sharingGroupsService with it, so a request on behalf of an account cannot reach the back-office method that acts for another owner.
// Adding a member checks the group exists and is the caller's own first.
export function shareWith(
repositories: Omit<SharingGroupsServiceDeps, "entitlements">,
groupId: string,
userIdToAdd: string,
ctx: ServiceContext
): Promise<void> {
const sharingGroups = createSharingGroupsService({
...repositories,
entitlements: allowAllEntitlements,
});
return sharingGroups.addMember(groupId, userIdToAdd, ctx);
}export async function leave(
repositories: Omit<SharingGroupsServiceDeps, "entitlements">,
sharingGroupId: string,
ctx: ServiceContext
): Promise<"left" | "owner"> {
const sharingGroups = createSharingGroupsService({
...repositories,
entitlements: allowAllEntitlements,
});
try {
await sharingGroups.leaveSharingGroup(sharingGroupId, ctx);
return "left";
} catch (error) {
if (isAppError(error) && error.code === "ACCESS_DENIED") {
return "owner"; // owners delete their group instead of leaving it
}
throw error;
}
}export function sharedWithMe(
repositories: Omit<SharingGroupsServiceDeps, "entitlements">,
ctx: ServiceContext
) {
const sharingGroups = createSharingGroupsService({
...repositories,
entitlements: allowAllEntitlements,
});
return sharingGroups.getMemberships(ctx); // => SharingGroupMembership[]
}DTOs
The DTOs are the wire shapes of the API routes; the service takes their fields as positional arguments.
Prop
Type
Prop
Type
Prop
Type
Ports
The groups bind the canonical surface of Queries; their members are a junction with a repository of its own.
| Export | Binds | Notes |
|---|---|---|
SharingGroupWhere | WhereShape | { id?, ownerId?, name? }, each an Equality<string>. name is the stored text, "" for a group without one. |
SharingGroupQuery, SharingGroupQueryParams | QueryObject | empty(), withId, byOwner, byName, withPagination, withFilters. No sorting. |
SharingGroupCriteria | Criteria<SharingGroupWhere> | byId, byIds, byOwner, where; instance and. |
SharingGroupProjections | Projection<T> | entity(): the SharingGroup. |
SharingGroupAggregations | Aggregation<T> | count(criteria?). |
SharingGroupPatch | TPatch | { name?: string }. |
PersistableSharingGroupWrite | TPersistableWrite | { ownerId: string; name: string | null }. A null name is stored as an empty string. |
SharingGroupsReadRepository | ReadRepository<SharingGroupQuery> | getById throws NotFoundError(SHARING_GROUP_NOT_FOUND) when the group is missing or hidden. |
SharingGroupsRepository | WriteRepository<SharingGroupCriteria, SharingGroupPatch, PersistableSharingGroupWrite> | Plus writeOne(input) => Promise<SharingGroup>. update always bumps updatedAt, so an empty patch still reports the group it matched. |
SharingGroupMembersRepository
The sharing_group_members junction. A link is unique per group and user, and deleting a group cascades to its links.
| Export | Notes |
|---|---|
SharingGroupMembersWhere | { sharingGroupId?, userId? }, each an Equality<string>. |
SharingGroupMembersQuery | empty(), byGroup, byUser, withFilters, and asViewer(userId): the viewer sees their own untrusted avatar in the memberships projection. |
SharingGroupMembersCriteria | byGroup, byUser, byGroupAndUser, where; instance and. |
SharingGroupMembersProjections | links(): SharingGroupMemberLink, oldest first. accounts(): the member Accounts. memberships(): SharingGroupMembership, newest first. |
SharingGroupMembersAggregations | count(criteria?). |
| Method | Signature | Description |
|---|---|---|
findMany | <T>(query: SharingGroupMembersQuery, projection: Projection<T>) => Promise<T[]> | Projected read of the links. |
aggregate | <T>(aggregation: Aggregation<T>) => Promise<T> | Link counts. |
writeMany | (links: readonly SharingGroupMemberLink[]) => Promise<{ added: number }> | Set union: an existing link is kept and not counted. |
delete | (criteria: SharingGroupMembersCriteria) => Promise<{ deletedCount: number }> | Filtered delete. |
Errors
| Error | Code | Thrown by |
|---|---|---|
AccessDeniedError | ACCESS_DENIED | update, upsert, getMembers, getMemberCount, addMember and removeMember when the caller does not own the group or the group is missing; leaveSharingGroup by the group's owner. |
NotFoundError | SHARING_GROUP_NOT_FOUND | update when the owned group disappears between the check and the write. |
Test doubles: createFakeSharingGroupsRepository and createFakeSharingGroupMembersRepository in Testing. Test data: makeSharingGroup and makeSharingGroupMembership in Factories.