scope-util.ts 3.9 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153
  1. import { ALL_SIGN, type Scope } from '@growi/core/dist/interfaces';
  2. // Data structure for the final merged scopes
  3. interface ScopeMap {
  4. [key: string]: Scope | ScopeMap;
  5. }
  6. // Input object with arbitrary action keys (e.g., READ, WRITE)
  7. type ScopesInput = Record<string, any>;
  8. function parseSubScope(
  9. parentKey: string,
  10. subObjForActions: Record<string, any>,
  11. actions: string[],
  12. ): ScopeMap {
  13. const result: ScopeMap = {};
  14. for (const action of actions) {
  15. if (typeof subObjForActions[action] === 'string') {
  16. result[`${action.toLowerCase()}:${parentKey.toLowerCase()}`] =
  17. subObjForActions[action];
  18. subObjForActions[action] = undefined;
  19. }
  20. }
  21. const childKeys = new Set<string>();
  22. for (const action of actions) {
  23. const obj = subObjForActions[action];
  24. if (obj && typeof obj === 'object') {
  25. Object.keys(obj).forEach((k) => {
  26. childKeys.add(k);
  27. });
  28. }
  29. }
  30. for (const ck of childKeys) {
  31. if (ck === 'ALL') {
  32. for (const action of actions) {
  33. const val = subObjForActions[action]?.[ck];
  34. if (typeof val === 'string') {
  35. result[`${action.toLowerCase()}:${parentKey.toLowerCase()}:all`] =
  36. val as Scope;
  37. }
  38. }
  39. continue;
  40. }
  41. const newKey = `${parentKey}:${ck}`;
  42. const childSubObj: Record<string, any> = {};
  43. for (const action of actions) {
  44. childSubObj[action] = subObjForActions[action]?.[ck];
  45. }
  46. result[newKey] = parseSubScope(newKey, childSubObj, actions);
  47. }
  48. return result;
  49. }
  50. export function parseScopes({
  51. scopes,
  52. isAdmin = false,
  53. }: {
  54. scopes: ScopesInput;
  55. isAdmin?: boolean;
  56. }): ScopeMap {
  57. const actions = Object.keys(scopes);
  58. const topKeys = new Set<string>();
  59. // Collect all top-level keys (e.g., ALL, ADMIN, USER) across all actions
  60. for (const action of actions) {
  61. Object.keys(scopes[action] || {}).forEach((k) => {
  62. topKeys.add(k);
  63. });
  64. }
  65. const result: ScopeMap = {};
  66. for (const key of topKeys) {
  67. // Skip 'ADMIN' key if isAdmin is true
  68. if (!isAdmin && (key === 'ADMIN' || key === 'ALL')) {
  69. continue;
  70. }
  71. if (key === 'ALL') {
  72. const allObj: ScopeMap = {};
  73. for (const action of actions) {
  74. const val = scopes[action]?.[key];
  75. if (typeof val === 'string') {
  76. allObj[`${action.toLowerCase()}:all`] = val as Scope;
  77. }
  78. }
  79. result.ALL = allObj;
  80. } else {
  81. const subObjForActions: Record<string, any> = {};
  82. for (const action of actions) {
  83. subObjForActions[action] = scopes[action]?.[key];
  84. }
  85. result[key] = parseSubScope(key, subObjForActions, actions);
  86. }
  87. }
  88. return result;
  89. }
  90. /**
  91. * Determines which scopes should be disabled based on wildcard selections
  92. */
  93. export function getDisabledScopes(
  94. selectedScopes: Scope[],
  95. availableScopes: string[],
  96. ): Set<Scope> {
  97. const disabledSet = new Set<Scope>();
  98. // If no selected scopes, return empty set
  99. if (!selectedScopes || selectedScopes.length === 0) {
  100. return disabledSet;
  101. }
  102. selectedScopes.forEach((scope) => {
  103. // Check if the scope is in the form `xxx:*`
  104. if (scope.endsWith(`:${ALL_SIGN}`)) {
  105. // Convert something like `read:*` into the prefix `read:`
  106. const prefix = scope.replace(`:${ALL_SIGN}`, ':');
  107. // Disable all scopes that start with the prefix (but are not the selected scope itself)
  108. availableScopes.forEach((s: Scope) => {
  109. if (s.startsWith(prefix) && s !== scope) {
  110. disabledSet.add(s);
  111. }
  112. });
  113. }
  114. });
  115. return disabledSet;
  116. }
  117. /**
  118. * Extracts all scope strings from a nested ScopeMap object
  119. */
  120. export function extractScopes(obj: Record<string, any>): string[] {
  121. let result: string[] = [];
  122. Object.values(obj).forEach((value) => {
  123. if (typeof value === 'string') {
  124. result.push(value);
  125. } else if (typeof value === 'object' && !Array.isArray(value)) {
  126. result = result.concat(extractScopes(value));
  127. }
  128. });
  129. return result;
  130. }