Просмотр исходного кода

Merge branch 'support/typescript-go' into support/omit-jest

Yuki Takei 2 месяцев назад
Родитель
Сommit
0cc379a17b

+ 0 - 2
apps/app/src/client/components/Me/AccessTokenForm.tsx

@@ -16,8 +16,6 @@ type AccessTokenFormProps = {
 type FormInputs = {
   expiredAt: string;
   description: string;
-  // biome-ignore lint/suspicious/noTsIgnore: Suppress auto fix by lefthook
-  // @ts-ignore - Scope type causes "Type instantiation is excessively deep" with tsgo
   scopes: Scope[];
 };
 

+ 0 - 2
apps/app/src/client/util/scope-util.ts

@@ -1,8 +1,6 @@
 import { ALL_SIGN, type Scope } from '@growi/core/dist/interfaces';
 
 // Data structure for the final merged scopes
-// biome-ignore lint/suspicious/noTsIgnore: Suppress auto fix by lefthook
-// @ts-ignore - Scope type causes "Type instantiation is excessively deep" with tsgo
 interface ScopeMap {
   [key: string]: Scope | ScopeMap;
 }

+ 0 - 2
apps/app/src/interfaces/access-token.ts

@@ -3,8 +3,6 @@ import type { Scope } from '@growi/core/dist/interfaces';
 export type IAccessTokenInfo = {
   expiredAt: Date;
   description: string;
-  // biome-ignore lint/suspicious/noTsIgnore: Suppress auto fix by lefthook
-  // @ts-ignore - Scope type causes "Type instantiation is excessively deep" with tsgo
   scopes: Scope[];
 };
 

+ 0 - 2
apps/app/src/server/middlewares/access-token-parser/access-token.ts

@@ -12,8 +12,6 @@ const logger = loggerFactory(
   'growi:middleware:access-token-parser:access-token',
 );
 
-// biome-ignore lint/suspicious/noTsIgnore: Suppress auto fix by lefthook
-// @ts-ignore - Scope type causes "Type instantiation is excessively deep" with tsgo
 export const parserForAccessToken = (scopes: Scope[]) => {
   return async (req: AccessTokenParserReq, res: Response): Promise<void> => {
     // Extract token from Authorization header first

+ 0 - 2
apps/app/src/server/middlewares/access-token-parser/index.ts

@@ -12,8 +12,6 @@ const logger = loggerFactory('growi:middleware:access-token-parser');
 
 export type { AccessTokenParser, AccessTokenParserReq };
 
-// biome-ignore lint/suspicious/noTsIgnore: Suppress auto fix by lefthook
-// @ts-ignore - Scope type causes "Type instantiation is excessively deep" with tsgo
 export const accessTokenParser: AccessTokenParser = (scopes, opts) => {
   return async (req, res, next): Promise<void> => {
     if (scopes == null || scopes.length === 0) {

+ 0 - 2
apps/app/src/server/models/access-token.ts

@@ -19,8 +19,6 @@ type GenerateTokenResult = {
   token: string;
   _id: Types.ObjectId;
   expiredAt: Date;
-  // biome-ignore lint/suspicious/noTsIgnore: Suppress auto fix by lefthook
-  // @ts-ignore - Scope type causes "Type instantiation is excessively deep" with tsgo
   scopes?: Scope[];
   description?: string;
 };

+ 0 - 2
apps/app/src/server/util/scope-utils.ts

@@ -5,8 +5,6 @@ import {
   type Scope,
 } from '@growi/core/dist/interfaces';
 
-// biome-ignore lint/suspicious/noTsIgnore: Suppress auto fix by lefthook
-// @ts-ignore - Scope type causes "Type instantiation is excessively deep" with tsgo
 export const isValidScope = (scope: Scope): boolean => {
   const scopeParts = scope
     .split(':')

+ 90 - 0
packages/core/src/interfaces/scope.spec.ts

@@ -0,0 +1,90 @@
+import { SCOPE, type Scope } from './scope';
+
+/**
+ * Helper to extract all scope strings from the SCOPE constant
+ */
+function extractAllScopeStrings(obj: unknown, result: string[] = []): string[] {
+  if (typeof obj === 'string') {
+    result.push(obj);
+  } else if (typeof obj === 'object' && obj !== null) {
+    for (const value of Object.values(obj)) {
+      extractAllScopeStrings(value, result);
+    }
+  }
+  return result;
+}
+
+describe('Scope type', () => {
+  it('should include all runtime scope values in the Scope type', () => {
+    const allRuntimeScopes = extractAllScopeStrings(SCOPE);
+
+    // This test verifies type safety - if a scope is missing from the Scope type,
+    // TypeScript will fail to compile when we try to assign it to a Scope variable
+    const typedScopes: Scope[] = allRuntimeScopes as Scope[];
+
+    expect(typedScopes.length).toBeGreaterThan(0);
+  });
+
+  it('should have the expected scope structure', () => {
+    // Verify SCOPE.READ exists
+    expect(SCOPE.READ).toBeDefined();
+    expect(SCOPE.WRITE).toBeDefined();
+
+    // Verify admin scopes
+    expect(SCOPE.READ.ADMIN).toBeDefined();
+    expect(SCOPE.READ.ADMIN.TOP).toBe('read:admin:top');
+    expect(SCOPE.READ.ADMIN.PLUGIN).toBe('read:admin:plugin');
+    expect(SCOPE.READ.ADMIN.ALL).toBe('read:admin:*');
+
+    // Verify user_settings scopes
+    expect(SCOPE.READ.USER_SETTINGS).toBeDefined();
+    expect(SCOPE.READ.USER_SETTINGS.INFO).toBe('read:user_settings:info');
+    expect(SCOPE.READ.USER_SETTINGS.API.API_TOKEN).toBe(
+      'read:user_settings:api:api_token',
+    );
+    expect(SCOPE.READ.USER_SETTINGS.API.ACCESS_TOKEN).toBe(
+      'read:user_settings:api:access_token',
+    );
+    expect(SCOPE.READ.USER_SETTINGS.API.ALL).toBe('read:user_settings:api:*');
+
+    // Verify features scopes
+    expect(SCOPE.READ.FEATURES).toBeDefined();
+    expect(SCOPE.READ.FEATURES.PAGE).toBe('read:features:page');
+    expect(SCOPE.READ.FEATURES.AI_ASSISTANT).toBe('read:features:ai_assistant');
+
+    // Verify write scopes
+    expect(SCOPE.WRITE.ADMIN.TOP).toBe('write:admin:top');
+    expect(SCOPE.WRITE.FEATURES.PAGE).toBe('write:features:page');
+  });
+
+  it('should have consistent scope count', () => {
+    const allRuntimeScopes = extractAllScopeStrings(SCOPE);
+
+    // Expected count based on the SCOPE_SEED structure:
+    // Admin: 17 leaf scopes + 1 wildcard = 18
+    // User Settings: 6 leaf + 2 nested (api) + 2 wildcards = 10
+    // Features: 6 leaf scopes + 1 wildcard = 7
+    // Total per action: 35
+    // Total: 35 * 2 (read/write) = 70
+    // But some wildcards are at category level, so actual count may vary
+
+    // Just ensure we have a reasonable number of scopes
+    expect(allRuntimeScopes.length).toBeGreaterThanOrEqual(60);
+    expect(allRuntimeScopes.length).toBeLessThanOrEqual(100);
+  });
+
+  it('should allow valid scope strings to be assigned to Scope type', () => {
+    // These assignments should compile without error
+    const readAdminTop: Scope = 'read:admin:top';
+    const writeAdminPlugin: Scope = 'write:admin:plugin';
+    const readUserSettingsApiToken: Scope = 'read:user_settings:api:api_token';
+    const readAdminWildcard: Scope = 'read:admin:*';
+    const readWildcard: Scope = 'read:*';
+
+    expect(readAdminTop).toBe('read:admin:top');
+    expect(writeAdminPlugin).toBe('write:admin:plugin');
+    expect(readUserSettingsApiToken).toBe('read:user_settings:api:api_token');
+    expect(readAdminWildcard).toBe('read:admin:*');
+    expect(readWildcard).toBe('read:*');
+  });
+});

+ 111 - 20
packages/core/src/interfaces/scope.ts

@@ -2,9 +2,10 @@
 // When you need to set different permissions for Admin and User
 // on specific endpoints (like /me), use SCOPE rather than modifying SCOPE_SEED.
 
-// If you want to add a new scope, you only need to add a new key to the SCOPE_SEED object.
-
-// Change translation file contents (accesstoken_scopes_desc) when scope structure is modified
+// If you want to add a new scope:
+// 1. Add a new key to the SCOPE_SEED object below
+// 2. Add the corresponding scope strings to the Scope type union at the bottom of this file
+// 3. Change translation file contents (accesstoken_scopes_desc) when scope structure is modified
 
 const SCOPE_SEED_ADMIN = {
   admin: {
@@ -71,23 +72,113 @@ const SCOPE_SEED_WITH_ACTION = Object.values(ACTION).reduce(
   {} as Record<ACTION_TYPE, typeof SCOPE_SEED>,
 );
 
-type FlattenObject<T> = {
-  [K in keyof T]: T[K] extends object
-    ? keyof T[K] extends never
-      ? K
-      : // biome-ignore lint/suspicious/noTsIgnore: Suppress auto fix by lefthook
-        // @ts-ignore - Scope type causes "Type instantiation is excessively deep" with tsgo
-        `${K & string}:${FlattenObject<T[K]> & string}`
-    : K;
-}[keyof T];
-
-type AddAllToScope<S extends string> = S extends `${infer X}:${infer Y}`
-  ? `${X}:${typeof ALL_SIGN}` | `${X}:${AddAllToScope<Y>}` | S
-  : S;
-
-type ScopeOnly = FlattenObject<typeof SCOPE_SEED_WITH_ACTION>;
-type ScopeWithAll = AddAllToScope<ScopeOnly>;
-export type Scope = ScopeOnly | ScopeWithAll;
+// ============================================================================
+// SCOPE LITERAL TYPE
+// ============================================================================
+// This type is explicitly defined to avoid TS2589 "Type instantiation is
+// excessively deep" errors that occur with recursive type definitions.
+//
+// IMPORTANT: When modifying SCOPE_SEED above, update this type union accordingly.
+// The scope strings follow the pattern: {action}:{category}[:{subcategory}[:{item}]]
+// Wildcard scopes use '*' at any level: {action}:{category}:*
+// ============================================================================
+
+// Read scopes - Admin
+type ReadAdminScope =
+  | 'read:admin:top'
+  | 'read:admin:app'
+  | 'read:admin:security'
+  | 'read:admin:markdown'
+  | 'read:admin:customize'
+  | 'read:admin:import_data'
+  | 'read:admin:export_data'
+  | 'read:admin:data_transfer'
+  | 'read:admin:external_notification'
+  | 'read:admin:slack_integration'
+  | 'read:admin:legacy_slack_integration'
+  | 'read:admin:user_management'
+  | 'read:admin:user_group_management'
+  | 'read:admin:audit_log'
+  | 'read:admin:plugin'
+  | 'read:admin:ai_integration'
+  | 'read:admin:full_text_search'
+  | 'read:admin:*';
+
+// Read scopes - User Settings
+type ReadUserSettingsScope =
+  | 'read:user_settings:info'
+  | 'read:user_settings:external_account'
+  | 'read:user_settings:password'
+  | 'read:user_settings:api:api_token'
+  | 'read:user_settings:api:access_token'
+  | 'read:user_settings:api:*'
+  | 'read:user_settings:in_app_notification'
+  | 'read:user_settings:other'
+  | 'read:user_settings:*';
+
+// Read scopes - Features
+type ReadFeaturesScope =
+  | 'read:features:ai_assistant'
+  | 'read:features:page'
+  | 'read:features:share_link'
+  | 'read:features:bookmark'
+  | 'read:features:attachment'
+  | 'read:features:page_bulk_export'
+  | 'read:features:*';
+
+// Write scopes - Admin
+type WriteAdminScope =
+  | 'write:admin:top'
+  | 'write:admin:app'
+  | 'write:admin:security'
+  | 'write:admin:markdown'
+  | 'write:admin:customize'
+  | 'write:admin:import_data'
+  | 'write:admin:export_data'
+  | 'write:admin:data_transfer'
+  | 'write:admin:external_notification'
+  | 'write:admin:slack_integration'
+  | 'write:admin:legacy_slack_integration'
+  | 'write:admin:user_management'
+  | 'write:admin:user_group_management'
+  | 'write:admin:audit_log'
+  | 'write:admin:plugin'
+  | 'write:admin:ai_integration'
+  | 'write:admin:full_text_search'
+  | 'write:admin:*';
+
+// Write scopes - User Settings
+type WriteUserSettingsScope =
+  | 'write:user_settings:info'
+  | 'write:user_settings:external_account'
+  | 'write:user_settings:password'
+  | 'write:user_settings:api:api_token'
+  | 'write:user_settings:api:access_token'
+  | 'write:user_settings:api:*'
+  | 'write:user_settings:in_app_notification'
+  | 'write:user_settings:other'
+  | 'write:user_settings:*';
+
+// Write scopes - Features
+type WriteFeaturesScope =
+  | 'write:features:ai_assistant'
+  | 'write:features:page'
+  | 'write:features:share_link'
+  | 'write:features:bookmark'
+  | 'write:features:attachment'
+  | 'write:features:page_bulk_export'
+  | 'write:features:*';
+
+// Combined Scope type - all valid scope strings
+export type Scope =
+  | ReadAdminScope
+  | ReadUserSettingsScope
+  | ReadFeaturesScope
+  | WriteAdminScope
+  | WriteUserSettingsScope
+  | WriteFeaturesScope
+  | 'read:*'
+  | 'write:*';
 
 // ScopeConstants type definition
 type ScopeConstantLeaf = Scope;

+ 0 - 2
packages/core/src/interfaces/server/access-token-parser.ts

@@ -15,8 +15,6 @@ export interface AccessTokenParserReq extends Request {
 }
 
 export type AccessTokenParser = (
-  // biome-ignore lint/suspicious/noTsIgnore: Suppress auto fix by lefthook
-  // @ts-ignore - Scope type causes "Type instantiation is excessively deep" with tsgo
   scopes?: Scope[],
   opts?: { acceptLegacy: boolean },
 ) => RequestHandler;

+ 0 - 2
packages/remark-attachment-refs/src/server/routes/refs.ts

@@ -93,8 +93,6 @@ export const routesFactory = (crowi): Promise<Router> => {
    */
   router.get(
     '/ref',
-    // biome-ignore lint/suspicious/noTsIgnore: Suppress auto fix by lefthook
-    // @ts-ignore - Scope type causes "Type instantiation is excessively deep" with tsgo
     accessTokenParser([SCOPE.READ.FEATURES.PAGE], { acceptLegacy: true }),
     // loginRequired,
     async (req: RequestWithUser, res) => {

+ 0 - 2
packages/remark-lsx/src/server/index.ts

@@ -60,8 +60,6 @@ const middleware = (crowi: any, app: any): void => {
 
   app.get(
     '/_api/lsx',
-    // biome-ignore lint/suspicious/noTsIgnore: Suppress auto fix by lefthook
-    // @ts-ignore - Scope type causes "Type instantiation is excessively deep" with tsgo
     accessTokenParser([SCOPE.READ.FEATURES.PAGE], { acceptLegacy: true }),
     loginRequired,
     lsxValidator,