import { getExistingManifestContext, type ExistingManifestContextOptions } from './existing-manifest-context'; import type { ManifestComponentDeletion, ManifestScope } from './manifest-builder'; import { Group, type GroupConfig, type GroupCoreRqlRule } from './group'; /** Saved-group creation only; Composer does not support updating sharing grants. */ export interface SavedSuperGroupSharingGrant { shouldNotify: boolean; groups: readonly { group: Group; grantType: 'VIEWER' | 'COLLABORATOR' }[]; } export interface SavedSuperGroupProps { apiName: string; group: Group; spokeOwnerHandle?: string; aliasAccessType?: 'Open' | 'Restricted'; /** Installed with a new saved group; Composer rejects changes to an existing sharing grant. */ sharingGrant?: SavedSuperGroupSharingGrant; } function validateApiName(apiName: string): void { if (typeof apiName !== 'string' || !apiName.endsWith('__c')) { throw new Error('SavedSuperGroup apiName must end with "__c".'); } } /** Saved identity for a reusable SUPERGROUP configuration. */ export class SavedSuperGroup { static readonly componentType = 'SUPERGROUP' as const; static toDeletionIdentifier(apiName: string): ManifestComponentDeletion { return { type: SavedSuperGroup.componentType, api_name: apiName, }; } /** Fresh content is reusable; hydrated content remains bound to this saved group's identity. */ readonly group: Group; private readonly _apiName: string; private readonly _spokeOwnerHandle: string | undefined; private readonly _aliasAccessType: 'Open' | 'Restricted' | undefined; private readonly _sharingGrant: SavedSuperGroupSharingGrant | undefined; private _loadedFromExisting = false; constructor(scope: ManifestScope, props: SavedSuperGroupProps) { validateApiName(props.apiName); if ( typeof props.group !== 'object' || props.group === null || typeof props.group._toDict !== 'function' ) { throw new Error('SavedSuperGroup group must be a Group.'); } this.group = props.group; this._apiName = props.apiName; this._spokeOwnerHandle = props.spokeOwnerHandle; this._aliasAccessType = props.aliasAccessType; this._sharingGrant = props.sharingGrant === undefined ? undefined : { shouldNotify: props.sharingGrant.shouldNotify, groups: props.sharingGrant.groups.map(({ group, grantType }) => ({ group, grantType })), }; scope._register(this); } static loadFromExisting(apiName: string, options: ExistingManifestContextOptions = {}): SavedSuperGroup { return getExistingManifestContext(options).loadSavedSuperGroup(apiName); } /** @internal Hydrates a saved alias from existing manifest wire JSON. */ static _fromExistingComponent(scope: ManifestScope, component: Record): SavedSuperGroup { if (component['type'] !== SavedSuperGroup.componentType) { throw new Error(`SavedSuperGroup component type must be "${SavedSuperGroup.componentType}".`); } if (component['group_type'] !== 'ALIAS') { throw new Error('SavedSuperGroup group_type must be "ALIAS".'); } const apiName = component['api_name']; if (typeof apiName !== 'string') { throw new Error('SUPERGROUP component field "api_name" must be a string.'); } validateApiName(apiName); const savedSuperGroup = new SavedSuperGroup(scope, { apiName, group: Group._fromExistingConfig(component, { savedApiName: apiName }), }); savedSuperGroup._loadedFromExisting = true; return savedSuperGroup; } getApiName(): string { return this._apiName; } getName(): string { return this.group.getName(); } setName(name: string): this { this.group.setName(name); return this; } setDescription(description: string | null): this { this.group.setDescription(description); return this; } setCoreRqlRule(rule: GroupCoreRqlRule | null): this { this.group.setCoreRqlRule(rule); return this; } getReferencedGroups(): SavedSuperGroup[] { return [ ...this.group.getReferencedGroups(), ...(this._sharingGrant?.groups.flatMap(({ group }) => group.getReferencedGroups()) ?? []), ]; } toDict(): GroupConfig { const config = this.group._toDict({ savedApiName: this._apiName }); if (this._loadedFromExisting) { config['api_name'] = this._apiName; return config; } if (this._sharingGrant !== undefined) { if (typeof this._sharingGrant.shouldNotify !== 'boolean') { throw new TypeError('Sharing grant notifications must be explicitly enabled or disabled.'); } const owner = this._spokeOwnerHandle ?? 'GROUP_MANAGER'; const grantTypes = new Set(); const groups = this._sharingGrant.groups.map(({ group, grantType }) => { if ((grantType !== 'VIEWER' && grantType !== 'COLLABORATOR') || grantTypes.has(grantType)) { throw new TypeError( 'Saved-group permission groups require distinct VIEWER or COLLABORATOR grants.', ); } grantTypes.add(grantType); if (group == null || typeof group._toDict !== 'function') { throw new TypeError('Sharing permissions require inline Group values.'); } const permission = group._toDict({ meObjId: `SUPERGROUP:${this._apiName}:sharing_grant:${grantType}`, spokeOwnerHandle: owner, }); if (permission['group_type'] === 'ALIAS' || permission['sharing_grant']) { throw new TypeError('Sharing permission groups cannot be saved groups or contain sharing grants.'); } return { ...permission, grant_type: grantType }; }); config['sharing_grant'] = { type: 'SHARING_GRANT', spoke_owner_handle: owner, groups: groups.length === 0 ? null : groups, config: 'ALIAS', should_notify: this._sharingGrant.shouldNotify, }; } return { ...config, api_name: this._apiName, identity_format_version: 2, group_type: 'ALIAS', spoke_owner_handle: this._spokeOwnerHandle ?? 'GROUP_MANAGER', alias_access_type: this._aliasAccessType ?? null, }; } }