From a47671eb945599dfa1be2f2755a44dc4e2d1fd8c Mon Sep 17 00:00:00 2001 From: Gao Sun Date: Mon, 9 Jan 2023 19:33:06 +0800 Subject: [PATCH] refactor(core): reorg connector files --- packages/core/src/__mocks__/connector.ts | 2 +- packages/core/src/connectors/index.ts | 142 ------------------ packages/core/src/connectors/meta-url.ts | 4 - .../src/connectors/utilities/index.test.ts | 35 ----- packages/core/src/index.ts | 2 +- packages/core/src/libraries/connector.test.ts | 69 ++++----- packages/core/src/libraries/connector.ts | 101 ++++++++++--- packages/core/src/libraries/passcode.test.ts | 4 +- packages/core/src/libraries/passcode.ts | 6 +- .../sign-in-experience/index.test.ts | 2 +- .../src/libraries/sign-in-experience/index.ts | 2 +- .../libraries/sign-in-experience/sign-in.ts | 2 +- .../libraries/sign-in-experience/sign-up.ts | 2 +- packages/core/src/libraries/social.ts | 2 +- packages/core/src/routes/connector.test.ts | 12 +- packages/core/src/routes/connector.ts | 11 +- .../core/src/routes/connector.update.test.ts | 4 +- .../actions/submit-interaction.test.ts | 2 +- .../interaction/actions/submit-interaction.ts | 2 +- .../core/src/routes/interaction/index.test.ts | 2 +- .../utils/find-user-by-identifier.test.ts | 2 +- .../utils/find-user-by-identifier.ts | 2 +- .../utils/social-verification.test.ts | 2 +- .../interaction/utils/social-verification.ts | 2 +- ...le-verification-profile-registered.test.ts | 2 +- ...-verification-protected-identifier.test.ts | 2 +- .../verifications/profile-verification.ts | 2 +- packages/core/src/routes/profile.test.ts | 2 +- packages/core/src/routes/profile.ts | 2 +- .../routes/session/social.bind-social.test.ts | 4 +- .../core/src/routes/session/social.test.ts | 4 +- packages/core/src/routes/session/social.ts | 2 +- .../sign-in-experience/guard.branding.test.ts | 2 +- .../sign-in-experience/guard.color.test.ts | 2 +- .../routes/sign-in-experience/guard.test.ts | 2 +- .../routes/sign-in-experience/index.test.ts | 2 +- .../src/routes/sign-in-experience/index.ts | 2 +- packages/core/src/routes/well-known.test.ts | 2 +- packages/core/src/routes/well-known.ts | 2 +- .../core/src/{ => utils}/connectors/consts.ts | 2 +- .../core/src/utils/connectors/factories.ts | 69 +++++++++ .../utilities => utils/connectors}/index.ts | 13 -- .../utilities => utils/connectors}/loader.ts | 0 .../src/utils/connectors/platform.test.ts | 46 ++++++ .../core/src/utils/connectors/platform.ts | 27 ++++ .../core/src/{ => utils}/connectors/types.ts | 0 46 files changed, 302 insertions(+), 305 deletions(-) delete mode 100644 packages/core/src/connectors/index.ts delete mode 100644 packages/core/src/connectors/meta-url.ts delete mode 100644 packages/core/src/connectors/utilities/index.test.ts rename packages/core/src/{ => utils}/connectors/consts.ts (89%) create mode 100644 packages/core/src/utils/connectors/factories.ts rename packages/core/src/{connectors/utilities => utils/connectors}/index.ts (77%) rename packages/core/src/{connectors/utilities => utils/connectors}/loader.ts (100%) create mode 100644 packages/core/src/utils/connectors/platform.test.ts create mode 100644 packages/core/src/utils/connectors/platform.ts rename packages/core/src/{ => utils}/connectors/types.ts (100%) diff --git a/packages/core/src/__mocks__/connector.ts b/packages/core/src/__mocks__/connector.ts index db3db5267..63db4132d 100644 --- a/packages/core/src/__mocks__/connector.ts +++ b/packages/core/src/__mocks__/connector.ts @@ -3,7 +3,7 @@ import type { Connector } from '@logto/schemas'; import { ConnectorType } from '@logto/schemas'; import { any } from 'zod'; -import type { LogtoConnector, ConnectorFactory } from '#src/connectors/types.js'; +import type { LogtoConnector, ConnectorFactory } from '#src/utils/connectors/types.js'; import { mockConnector0, diff --git a/packages/core/src/connectors/index.ts b/packages/core/src/connectors/index.ts deleted file mode 100644 index e5fbe25b0..000000000 --- a/packages/core/src/connectors/index.ts +++ /dev/null @@ -1,142 +0,0 @@ -import { existsSync } from 'fs'; -import { fileURLToPath } from 'node:url'; -import path from 'path'; - -import { connectorDirectory } from '@logto/cli/lib/constants.js'; -import { getConnectorPackagesFromDirectory } from '@logto/cli/lib/utilities.js'; -import type { AllConnector } from '@logto/connector-kit'; -import { validateConfig } from '@logto/connector-kit'; -import { findPackage } from '@logto/shared'; -import chalk from 'chalk'; - -import RequestError from '#src/errors/RequestError/index.js'; -import { findAllConnectors } from '#src/queries/connector.js'; - -import { defaultConnectorMethods } from './consts.js'; -import { metaUrl } from './meta-url.js'; -import type { ConnectorFactory, LogtoConnector } from './types.js'; -import { getConnectorConfig, parseMetadata, validateConnectorModule } from './utilities/index.js'; -import { loadConnector } from './utilities/loader.js'; - -const currentDirname = path.dirname(fileURLToPath(metaUrl)); - -// eslint-disable-next-line @silverhand/fp/no-let -let cachedConnectorFactories: ConnectorFactory[] | undefined; - -export const loadConnectorFactories = async () => { - if (cachedConnectorFactories) { - return cachedConnectorFactories; - } - - const coreDirectory = await findPackage(currentDirname); - const directory = coreDirectory && path.join(coreDirectory, connectorDirectory); - - if (!directory || !existsSync(directory)) { - return []; - } - - const connectorPackages = await getConnectorPackagesFromDirectory(directory); - - const connectorFactories = await Promise.all( - connectorPackages.map(async ({ path: packagePath, name }) => { - try { - const createConnector = await loadConnector(packagePath); - const rawConnector = await createConnector({ getConfig: getConnectorConfig }); - validateConnectorModule(rawConnector); - - return { - metadata: await parseMetadata(rawConnector.metadata, packagePath), - type: rawConnector.type, - createConnector, - path: packagePath, - }; - } catch (error: unknown) { - if (error instanceof Error) { - console.log( - `${chalk.red( - `[load-connector] skip ${chalk.bold(name)} due to error: ${error.message}` - )}` - ); - - return; - } - - throw error; - } - }) - ); - - // eslint-disable-next-line @silverhand/fp/no-mutation - cachedConnectorFactories = connectorFactories.filter( - (connectorFactory): connectorFactory is ConnectorFactory => connectorFactory !== undefined - ); - - return cachedConnectorFactories; -}; - -export const getLogtoConnectors = async (): Promise => { - const databaseConnectors = await findAllConnectors(); - - const logtoConnectors = await Promise.all( - databaseConnectors.map(async (databaseConnector) => { - const { id, metadata, connectorId } = databaseConnector; - - const connectorFactories = await loadConnectorFactories(); - const connectorFactory = connectorFactories.find( - ({ metadata }) => metadata.id === connectorId - ); - - if (!connectorFactory) { - return; - } - - const { createConnector, path: packagePath } = connectorFactory; - - try { - const rawConnector = await createConnector({ - getConfig: async () => { - return getConnectorConfig(id); - }, - }); - validateConnectorModule(rawConnector); - const rawMetadata = await parseMetadata(rawConnector.metadata, packagePath); - - const connector: AllConnector = { - ...defaultConnectorMethods, - ...rawConnector, - metadata: { - ...rawMetadata, - ...metadata, - }, - }; - - return { - ...connector, - validateConfig: (config: unknown) => { - validateConfig(config, rawConnector.configGuard); - }, - dbEntry: databaseConnector, - }; - } catch {} - }) - ); - - return logtoConnectors.filter( - (logtoConnector): logtoConnector is LogtoConnector => logtoConnector !== undefined - ); -}; - -export const getLogtoConnectorById = async (id: string): Promise => { - const connectors = await getLogtoConnectors(); - const pickedConnector = connectors.find(({ dbEntry }) => dbEntry.id === id); - - if (!pickedConnector) { - throw new RequestError({ - code: 'entity.not_found', - id, - status: 404, - }); - } - - return pickedConnector; -}; diff --git a/packages/core/src/connectors/meta-url.ts b/packages/core/src/connectors/meta-url.ts deleted file mode 100644 index 740bd697b..000000000 --- a/packages/core/src/connectors/meta-url.ts +++ /dev/null @@ -1,4 +0,0 @@ -// Have to define this in a separate file since Jest sticks with CJS -// We need to mock this before running tests -// https://github.com/facebook/jest/issues/12952 -export const metaUrl = import.meta.url; diff --git a/packages/core/src/connectors/utilities/index.test.ts b/packages/core/src/connectors/utilities/index.test.ts deleted file mode 100644 index 811beb79e..000000000 --- a/packages/core/src/connectors/utilities/index.test.ts +++ /dev/null @@ -1,35 +0,0 @@ -import type { Connector } from '@logto/schemas'; -import { createMockUtils } from '@logto/shared/esm'; - -import RequestError from '#src/errors/RequestError/index.js'; - -const { jest } = import.meta; -const { mockEsmWithActual } = createMockUtils(jest); - -const connectors: Connector[] = [ - { - id: 'id', - config: { foo: 'bar' }, - createdAt: 0, - syncProfile: false, - connectorId: 'id', - metadata: {}, - }, -]; - -await mockEsmWithActual('#src/queries/connector.js', () => ({ - findAllConnectors: jest.fn(async () => connectors), -})); - -const { getConnectorConfig } = await import('./index.js'); - -it('getConnectorConfig() should return right config', async () => { - const config = await getConnectorConfig('id'); - expect(config).toMatchObject({ foo: 'bar' }); -}); - -it('getConnectorConfig() should throw error if connector not found', async () => { - await expect(getConnectorConfig('not-found')).rejects.toMatchError( - new RequestError({ code: 'entity.not_found', id: 'not-found', status: 404 }) - ); -}); diff --git a/packages/core/src/index.ts b/packages/core/src/index.ts index ad51c7755..5f7030994 100644 --- a/packages/core/src/index.ts +++ b/packages/core/src/index.ts @@ -11,7 +11,7 @@ dotenv.config({ path: await findUp('.env', {}) }); const { default: envSet } = await import('./env-set/index.js'); await envSet.load(); -const { loadConnectorFactories } = await import('./connectors/index.js'); +const { loadConnectorFactories } = await import('./utils/connectors/factories.js'); try { const app = new Koa({ diff --git a/packages/core/src/libraries/connector.test.ts b/packages/core/src/libraries/connector.test.ts index 5380ed222..bb37982e9 100644 --- a/packages/core/src/libraries/connector.test.ts +++ b/packages/core/src/libraries/connector.test.ts @@ -1,46 +1,35 @@ -import { ConnectorType } from '@logto/schemas'; +import type { Connector } from '@logto/schemas'; +import { createMockUtils } from '@logto/shared/esm'; -import { - mockMetadata0, - mockMetadata1, - mockConnector0, - mockConnector1, - mockLogtoConnector, - mockLogtoConnectorList, -} from '#src/__mocks__/index.js'; import RequestError from '#src/errors/RequestError/index.js'; -import { checkSocialConnectorTargetAndPlatformUniqueness } from './connector.js'; +const { jest } = import.meta; +const { mockEsmWithActual } = createMockUtils(jest); -describe('check social connector target and platform uniqueness', () => { - it('throws if more than one same-platform social connectors sharing the same `target`', () => { - const mockConnectors = [ - { - dbEntry: mockConnector0, - metadata: { ...mockMetadata0, target: 'target' }, - type: ConnectorType.Social, - ...mockLogtoConnector, - }, - { - dbEntry: mockConnector1, - metadata: { ...mockMetadata1, target: 'target' }, - type: ConnectorType.Social, - ...mockLogtoConnector, - }, - ]; - expect(() => { - checkSocialConnectorTargetAndPlatformUniqueness(mockConnectors); - }).toMatchError( - new RequestError({ - code: 'connector.multiple_target_with_same_platform', - status: 400, - }) - ); - }); +const connectors: Connector[] = [ + { + id: 'id', + config: { foo: 'bar' }, + createdAt: 0, + syncProfile: false, + connectorId: 'id', + metadata: {}, + }, +]; - it('should not throw when no multiple connectors sharing same target and platform', () => { - expect(() => { - checkSocialConnectorTargetAndPlatformUniqueness(mockLogtoConnectorList); - }).not.toThrow(); - }); +await mockEsmWithActual('#src/queries/connector.js', () => ({ + findAllConnectors: jest.fn(async () => connectors), +})); + +const { getConnectorConfig } = await import('./connector.js'); + +it('getConnectorConfig() should return right config', async () => { + const config = await getConnectorConfig('id'); + expect(config).toMatchObject({ foo: 'bar' }); +}); + +it('getConnectorConfig() should throw error if connector not found', async () => { + await expect(getConnectorConfig('not-found')).rejects.toMatchError( + new RequestError({ code: 'entity.not_found', id: 'not-found', status: 404 }) + ); }); diff --git a/packages/core/src/libraries/connector.ts b/packages/core/src/libraries/connector.ts index 7c614b872..0737967c9 100644 --- a/packages/core/src/libraries/connector.ts +++ b/packages/core/src/libraries/connector.ts @@ -1,27 +1,86 @@ -import { ConnectorType } from '@logto/schemas'; +import type { AllConnector } from '@logto/connector-kit'; +import { validateConfig } from '@logto/connector-kit'; -import type { LogtoConnector } from '#src/connectors/types.js'; +import RequestError from '#src/errors/RequestError/index.js'; +import { findAllConnectors } from '#src/queries/connector.js'; import assertThat from '#src/utils/assert-that.js'; +import { defaultConnectorMethods } from '#src/utils/connectors/consts.js'; +import { loadConnectorFactories } from '#src/utils/connectors/factories.js'; +import { validateConnectorModule, parseMetadata } from '#src/utils/connectors/index.js'; +import type { LogtoConnector } from '#src/utils/connectors/types.js'; -export const checkSocialConnectorTargetAndPlatformUniqueness = (connectors: LogtoConnector[]) => { - const targetAndPlatformObjectsInUse = connectors - .filter(({ type }) => type === ConnectorType.Social) - .map(({ metadata: { target, platform } }) => ({ - target, - platform, - })); +export const getConnectorConfig = async (id: string): Promise => { + const connectors = await findAllConnectors(); + const connector = connectors.find((connector) => connector.id === id); - const targetAndPlatformSet = new Set(); + assertThat(connector, new RequestError({ code: 'entity.not_found', id, status: 404 })); - for (const targetAndPlatformObject of targetAndPlatformObjectsInUse) { - const { target, platform } = targetAndPlatformObject; - - if (platform === null) { - continue; - } - - const element = JSON.stringify([target, platform]); - assertThat(!targetAndPlatformSet.has(element), 'connector.multiple_target_with_same_platform'); - targetAndPlatformSet.add(element); - } + return connector.config; +}; + +export const getLogtoConnectors = async (): Promise => { + const databaseConnectors = await findAllConnectors(); + + const logtoConnectors = await Promise.all( + databaseConnectors.map(async (databaseConnector) => { + const { id, metadata, connectorId } = databaseConnector; + + const connectorFactories = await loadConnectorFactories(); + const connectorFactory = connectorFactories.find( + ({ metadata }) => metadata.id === connectorId + ); + + if (!connectorFactory) { + return; + } + + const { createConnector, path: packagePath } = connectorFactory; + + try { + const rawConnector = await createConnector({ + getConfig: async () => { + return getConnectorConfig(id); + }, + }); + validateConnectorModule(rawConnector); + const rawMetadata = await parseMetadata(rawConnector.metadata, packagePath); + + const connector: AllConnector = { + ...defaultConnectorMethods, + ...rawConnector, + metadata: { + ...rawMetadata, + ...metadata, + }, + }; + + return { + ...connector, + validateConfig: (config: unknown) => { + validateConfig(config, rawConnector.configGuard); + }, + dbEntry: databaseConnector, + }; + } catch {} + }) + ); + + return logtoConnectors.filter( + (logtoConnector): logtoConnector is LogtoConnector => logtoConnector !== undefined + ); +}; + +export const getLogtoConnectorById = async (id: string): Promise => { + const connectors = await getLogtoConnectors(); + const pickedConnector = connectors.find(({ dbEntry }) => dbEntry.id === id); + + if (!pickedConnector) { + throw new RequestError({ + code: 'entity.not_found', + id, + status: 404, + }); + } + + return pickedConnector; }; diff --git a/packages/core/src/libraries/passcode.test.ts b/packages/core/src/libraries/passcode.test.ts index 196440716..e51bdeccd 100644 --- a/packages/core/src/libraries/passcode.test.ts +++ b/packages/core/src/libraries/passcode.test.ts @@ -4,8 +4,8 @@ import { createMockUtils } from '@logto/shared/esm'; import { any } from 'zod'; import { mockConnector, mockMetadata } from '#src/__mocks__/index.js'; -import { defaultConnectorMethods } from '#src/connectors/consts.js'; import RequestError from '#src/errors/RequestError/index.js'; +import { defaultConnectorMethods } from '#src/utils/connectors/consts.js'; const { jest } = import.meta; const { mockEsm } = createMockUtils(jest); @@ -26,7 +26,7 @@ const { increasePasscodeTryCount: jest.fn(), })); -const { getLogtoConnectors } = mockEsm('#src/connectors/index.js', () => ({ +const { getLogtoConnectors } = mockEsm('#src/libraries/connector.js', () => ({ getLogtoConnectors: jest.fn(), })); diff --git a/packages/core/src/libraries/passcode.ts b/packages/core/src/libraries/passcode.ts index b2c4bb528..5800a337a 100644 --- a/packages/core/src/libraries/passcode.ts +++ b/packages/core/src/libraries/passcode.ts @@ -7,10 +7,8 @@ import { import type { Passcode } from '@logto/schemas'; import { customAlphabet, nanoid } from 'nanoid'; -import { getLogtoConnectors } from '#src/connectors/index.js'; -import type { LogtoConnector } from '#src/connectors/types.js'; -import { ConnectorType } from '#src/connectors/types.js'; import RequestError from '#src/errors/RequestError/index.js'; +import { getLogtoConnectors } from '#src/libraries/connector.js'; import { consumePasscode, deletePasscodesByIds, @@ -20,6 +18,8 @@ import { insertPasscode, } from '#src/queries/passcode.js'; import assertThat from '#src/utils/assert-that.js'; +import { ConnectorType } from '#src/utils/connectors/types.js'; +import type { LogtoConnector } from '#src/utils/connectors/types.js'; export const passcodeLength = 6; const randomCode = customAlphabet('1234567890', passcodeLength); diff --git a/packages/core/src/libraries/sign-in-experience/index.test.ts b/packages/core/src/libraries/sign-in-experience/index.test.ts index 83853d758..e29c12df5 100644 --- a/packages/core/src/libraries/sign-in-experience/index.test.ts +++ b/packages/core/src/libraries/sign-in-experience/index.test.ts @@ -23,7 +23,7 @@ const customPhrases = { }; const { findAllCustomLanguageTags } = customPhrases; -const { getLogtoConnectors } = mockEsm('#src/connectors.js', () => ({ +const { getLogtoConnectors } = mockEsm('#src/libraries/connector.js', () => ({ getLogtoConnectors: jest.fn(), })); diff --git a/packages/core/src/libraries/sign-in-experience/index.ts b/packages/core/src/libraries/sign-in-experience/index.ts index 81c4e1dbe..d1cc95ddd 100644 --- a/packages/core/src/libraries/sign-in-experience/index.ts +++ b/packages/core/src/libraries/sign-in-experience/index.ts @@ -11,8 +11,8 @@ import { import { deduplicate } from '@silverhand/essentials'; import i18next from 'i18next'; -import { getLogtoConnectors } from '#src/connectors/index.js'; import RequestError from '#src/errors/RequestError/index.js'; +import { getLogtoConnectors } from '#src/libraries/connector.js'; import type Queries from '#src/tenants/Queries.js'; import assertThat from '#src/utils/assert-that.js'; diff --git a/packages/core/src/libraries/sign-in-experience/sign-in.ts b/packages/core/src/libraries/sign-in-experience/sign-in.ts index 5803e11da..e083e1963 100644 --- a/packages/core/src/libraries/sign-in-experience/sign-in.ts +++ b/packages/core/src/libraries/sign-in-experience/sign-in.ts @@ -1,9 +1,9 @@ import type { SignIn, SignUp } from '@logto/schemas'; import { ConnectorType, SignInIdentifier } from '@logto/schemas'; -import type { LogtoConnector } from '#src/connectors/types.js'; import RequestError from '#src/errors/RequestError/index.js'; import assertThat from '#src/utils/assert-that.js'; +import type { LogtoConnector } from '#src/utils/connectors/types.js'; export const validateSignIn = ( signIn: SignIn, diff --git a/packages/core/src/libraries/sign-in-experience/sign-up.ts b/packages/core/src/libraries/sign-in-experience/sign-up.ts index 45c3ff49e..a5b105a2a 100644 --- a/packages/core/src/libraries/sign-in-experience/sign-up.ts +++ b/packages/core/src/libraries/sign-in-experience/sign-up.ts @@ -1,9 +1,9 @@ import type { SignUp } from '@logto/schemas'; import { SignInIdentifier, ConnectorType } from '@logto/schemas'; -import type { LogtoConnector } from '#src/connectors/types.js'; import RequestError from '#src/errors/RequestError/index.js'; import assertThat from '#src/utils/assert-that.js'; +import type { LogtoConnector } from '#src/utils/connectors/types.js'; export const validateSignUp = (signUp: SignUp, enabledConnectors: LogtoConnector[]) => { for (const identifier of signUp.identifiers) { diff --git a/packages/core/src/libraries/social.ts b/packages/core/src/libraries/social.ts index bfdbed2d7..dd04f277a 100644 --- a/packages/core/src/libraries/social.ts +++ b/packages/core/src/libraries/social.ts @@ -6,8 +6,8 @@ import type { Nullable } from '@silverhand/essentials'; import type { InteractionResults } from 'oidc-provider'; import { z } from 'zod'; -import { getLogtoConnectorById } from '#src/connectors/index.js'; import RequestError from '#src/errors/RequestError/index.js'; +import { getLogtoConnectorById } from '#src/libraries/connector.js'; import { findUserByEmail, findUserByPhone } from '#src/queries/user.js'; import assertThat from '#src/utils/assert-that.js'; diff --git a/packages/core/src/routes/connector.test.ts b/packages/core/src/routes/connector.test.ts index 57cce6f05..f74c64e64 100644 --- a/packages/core/src/routes/connector.test.ts +++ b/packages/core/src/routes/connector.test.ts @@ -16,16 +16,16 @@ import { mockLogtoConnectorList, mockLogtoConnector, } from '#src/__mocks__/index.js'; -import { defaultConnectorMethods } from '#src/connectors/consts.js'; -import type { LogtoConnector } from '#src/connectors/types.js'; import RequestError from '#src/errors/RequestError/index.js'; import assertThat from '#src/utils/assert-that.js'; +import { defaultConnectorMethods } from '#src/utils/connectors/consts.js'; +import type { LogtoConnector } from '#src/utils/connectors/types.js'; import { createRequester } from '#src/utils/test-utils.js'; const { jest } = import.meta; const { mockEsm, mockEsmWithActual } = createMockUtils(jest); -mockEsm('#src/libraries/connector.js', () => ({ +mockEsm('#src/utils/connectors/platform.js', () => ({ checkSocialConnectorTargetAndPlatformUniqueness: jest.fn(), })); @@ -52,8 +52,12 @@ const { // eslint-disable-next-line @typescript-eslint/ban-types const getLogtoConnectors = jest.fn, []>(); -const { loadConnectorFactories } = mockEsm('#src/connectors/index.js', () => ({ + +const { loadConnectorFactories } = mockEsm('#src/utils/connectors/factories.js', () => ({ loadConnectorFactories: jest.fn(), +})); + +mockEsm('#src/libraries/connector.js', () => ({ getLogtoConnectors, getLogtoConnectorById: async (connectorId: string) => { const connectors = await getLogtoConnectors(); diff --git a/packages/core/src/routes/connector.ts b/packages/core/src/routes/connector.ts index bc02178db..3f690a3ca 100644 --- a/packages/core/src/routes/connector.ts +++ b/packages/core/src/routes/connector.ts @@ -5,14 +5,8 @@ import { arbitraryObjectGuard, Connectors, ConnectorType } from '@logto/schemas' import cleanDeep from 'clean-deep'; import { object, string } from 'zod'; -import { - getLogtoConnectorById, - getLogtoConnectors, - loadConnectorFactories, -} from '#src/connectors/index.js'; -import type { LogtoConnector } from '#src/connectors/types.js'; import RequestError from '#src/errors/RequestError/index.js'; -import { checkSocialConnectorTargetAndPlatformUniqueness } from '#src/libraries/connector.js'; +import { getLogtoConnectorById, getLogtoConnectors } from '#src/libraries/connector.js'; import { removeUnavailableSocialConnectorTargets } from '#src/libraries/sign-in-experience/index.js'; import koaGuard from '#src/middleware/koa-guard.js'; import { @@ -24,6 +18,9 @@ import { updateConnector, } from '#src/queries/connector.js'; import assertThat from '#src/utils/assert-that.js'; +import { loadConnectorFactories } from '#src/utils/connectors/factories.js'; +import { checkSocialConnectorTargetAndPlatformUniqueness } from '#src/utils/connectors/platform.js'; +import type { LogtoConnector } from '#src/utils/connectors/types.js'; import type { AuthedRouter, RouterInitArgs } from './types.js'; diff --git a/packages/core/src/routes/connector.update.test.ts b/packages/core/src/routes/connector.update.test.ts index 168d865fb..65b5ae612 100644 --- a/packages/core/src/routes/connector.update.test.ts +++ b/packages/core/src/routes/connector.update.test.ts @@ -8,9 +8,9 @@ import { mockLogtoConnectorList, mockLogtoConnector, } from '#src/__mocks__/index.js'; -import type { LogtoConnector } from '#src/connectors/types.js'; import RequestError from '#src/errors/RequestError/index.js'; import assertThat from '#src/utils/assert-that.js'; +import type { LogtoConnector } from '#src/utils/connectors/types.js'; import { createRequester } from '#src/utils/test-utils.js'; const { jest } = import.meta; @@ -42,7 +42,7 @@ const { updateConnector } = await mockEsmWithActual('#src/queries/connector.js', updateConnector: jest.fn(), })); -await mockEsmWithActual('#src/connectors.js', () => ({ +await mockEsmWithActual('#src/libraries/connector.js', () => ({ getLogtoConnectors, getLogtoConnectorById, })); diff --git a/packages/core/src/routes/interaction/actions/submit-interaction.test.ts b/packages/core/src/routes/interaction/actions/submit-interaction.test.ts index 2b670eace..abc795ee6 100644 --- a/packages/core/src/routes/interaction/actions/submit-interaction.test.ts +++ b/packages/core/src/routes/interaction/actions/submit-interaction.test.ts @@ -16,7 +16,7 @@ import type { const { jest } = import.meta; const { mockEsm } = createMockUtils(jest); -const { getLogtoConnectorById } = mockEsm('#src/connectors/index.js', () => ({ +const { getLogtoConnectorById } = mockEsm('#src/libraries/connector.js', () => ({ getLogtoConnectorById: jest .fn() .mockResolvedValue({ metadata: { target: 'logto' }, dbEntry: { syncProfile: true } }), diff --git a/packages/core/src/routes/interaction/actions/submit-interaction.ts b/packages/core/src/routes/interaction/actions/submit-interaction.ts index 1c6eb1fc3..1951b7fbb 100644 --- a/packages/core/src/routes/interaction/actions/submit-interaction.ts +++ b/packages/core/src/routes/interaction/actions/submit-interaction.ts @@ -2,7 +2,7 @@ import type { User, Profile } from '@logto/schemas'; import { InteractionEvent, UserRole, adminConsoleApplicationId } from '@logto/schemas'; import { conditional } from '@silverhand/essentials'; -import { getLogtoConnectorById } from '#src/connectors/index.js'; +import { getLogtoConnectorById } from '#src/libraries/connector.js'; import { assignInteractionResults } from '#src/libraries/session.js'; import { encryptUserPassword } from '#src/libraries/user.js'; import type TenantContext from '#src/tenants/TenantContext.js'; diff --git a/packages/core/src/routes/interaction/index.test.ts b/packages/core/src/routes/interaction/index.test.ts index 9437c251f..d2800e963 100644 --- a/packages/core/src/routes/interaction/index.test.ts +++ b/packages/core/src/routes/interaction/index.test.ts @@ -31,7 +31,7 @@ const getLogtoConnectorByIdHelper = jest.fn(async (connectorId: string) => { }; }); -await mockEsmWithActual('#src/connectors/index.js', () => ({ +await mockEsmWithActual('#src/libraries/connector.js', () => ({ getLogtoConnectorById: jest.fn(async (connectorId: string) => { const connector = await getLogtoConnectorByIdHelper(connectorId); diff --git a/packages/core/src/routes/interaction/utils/find-user-by-identifier.test.ts b/packages/core/src/routes/interaction/utils/find-user-by-identifier.test.ts index 63d94516a..9b5781aaa 100644 --- a/packages/core/src/routes/interaction/utils/find-user-by-identifier.test.ts +++ b/packages/core/src/routes/interaction/utils/find-user-by-identifier.test.ts @@ -12,7 +12,7 @@ const queries = { mockEsm('#src/queries/user.js', () => queries); -const { getLogtoConnectorById } = mockEsm('#src/connectors/index.js', () => ({ +const { getLogtoConnectorById } = mockEsm('#src/libraries/connector.js', () => ({ getLogtoConnectorById: jest.fn().mockResolvedValue({ metadata: { target: 'logto' } }), })); diff --git a/packages/core/src/routes/interaction/utils/find-user-by-identifier.ts b/packages/core/src/routes/interaction/utils/find-user-by-identifier.ts index 2399cb312..28ca7b69c 100644 --- a/packages/core/src/routes/interaction/utils/find-user-by-identifier.ts +++ b/packages/core/src/routes/interaction/utils/find-user-by-identifier.ts @@ -1,4 +1,4 @@ -import { getLogtoConnectorById } from '#src/connectors/index.js'; +import { getLogtoConnectorById } from '#src/libraries/connector.js'; import { findUserByEmail, findUserByUsername, diff --git a/packages/core/src/routes/interaction/utils/social-verification.test.ts b/packages/core/src/routes/interaction/utils/social-verification.test.ts index 208bccd24..1bf750835 100644 --- a/packages/core/src/routes/interaction/utils/social-verification.test.ts +++ b/packages/core/src/routes/interaction/utils/social-verification.test.ts @@ -13,7 +13,7 @@ const { getUserInfoByAuthCode } = mockEsm('#src/libraries/social.js', () => ({ getUserInfoByAuthCode: jest.fn().mockResolvedValue({ id: 'foo' }), })); -mockEsm('#src/connectors.js', () => ({ +mockEsm('#src/libraries/connector.js', () => ({ getLogtoConnectorById: jest.fn().mockResolvedValue({ metadata: { id: 'social', diff --git a/packages/core/src/routes/interaction/utils/social-verification.ts b/packages/core/src/routes/interaction/utils/social-verification.ts index 6bc03331a..bab12886a 100644 --- a/packages/core/src/routes/interaction/utils/social-verification.ts +++ b/packages/core/src/routes/interaction/utils/social-verification.ts @@ -3,7 +3,7 @@ import type { SocialConnectorPayload } from '@logto/schemas'; import { ConnectorType } from '@logto/schemas'; import type Provider from 'oidc-provider'; -import { getLogtoConnectorById } from '#src/connectors/index.js'; +import { getLogtoConnectorById } from '#src/libraries/connector.js'; import { getUserInfoByAuthCode } from '#src/libraries/social.js'; import type { WithLogContext } from '#src/middleware/koa-audit-log.js'; import { diff --git a/packages/core/src/routes/interaction/verifications/profile-verification-profile-registered.test.ts b/packages/core/src/routes/interaction/verifications/profile-verification-profile-registered.test.ts index 270468131..6ad7cbd35 100644 --- a/packages/core/src/routes/interaction/verifications/profile-verification-profile-registered.test.ts +++ b/packages/core/src/routes/interaction/verifications/profile-verification-profile-registered.test.ts @@ -17,7 +17,7 @@ const { hasUser, hasUserWithEmail, hasUserWithPhone, hasUserWithIdentity } = hasUserWithIdentity: jest.fn().mockResolvedValue(false), })); -mockEsm('#src/connectors/index.js', () => ({ +mockEsm('#src/libraries/connector.js', () => ({ getLogtoConnectorById: jest.fn().mockResolvedValue({ metadata: { target: 'logto' }, }), diff --git a/packages/core/src/routes/interaction/verifications/profile-verification-protected-identifier.test.ts b/packages/core/src/routes/interaction/verifications/profile-verification-protected-identifier.test.ts index e4e337f74..742739617 100644 --- a/packages/core/src/routes/interaction/verifications/profile-verification-protected-identifier.test.ts +++ b/packages/core/src/routes/interaction/verifications/profile-verification-protected-identifier.test.ts @@ -15,7 +15,7 @@ await mockEsmWithActual('#src/queries/user.js', () => ({ hasUserWithIdentity: jest.fn().mockResolvedValue(false), })); -mockEsm('#src/connectors/index.js', () => ({ +mockEsm('#src/libraries/connector.js', () => ({ getLogtoConnectorById: jest.fn().mockResolvedValue({ metadata: { target: 'logto' }, }), diff --git a/packages/core/src/routes/interaction/verifications/profile-verification.ts b/packages/core/src/routes/interaction/verifications/profile-verification.ts index 8b14c24e5..3cc05ca76 100644 --- a/packages/core/src/routes/interaction/verifications/profile-verification.ts +++ b/packages/core/src/routes/interaction/verifications/profile-verification.ts @@ -2,8 +2,8 @@ import type { Profile, User } from '@logto/schemas'; import { InteractionEvent } from '@logto/schemas'; import { argon2Verify } from 'hash-wasm'; -import { getLogtoConnectorById } from '#src/connectors/index.js'; import RequestError from '#src/errors/RequestError/index.js'; +import { getLogtoConnectorById } from '#src/libraries/connector.js'; import { findUserById, hasUser, diff --git a/packages/core/src/routes/profile.test.ts b/packages/core/src/routes/profile.test.ts index 8f0dec8ec..7714e6421 100644 --- a/packages/core/src/routes/profile.test.ts +++ b/packages/core/src/routes/profile.test.ts @@ -27,7 +27,7 @@ const getLogtoConnectorById = jest.fn(async () => ({ getAuthorizationUri: jest.fn(async () => ''), })); -mockEsm('#src/connectors/index.js', () => ({ +mockEsm('#src/libraries/connector.js', () => ({ getLogtoConnectors: mockLogtoConnectorList, getLogtoConnectorById, })); diff --git a/packages/core/src/routes/profile.ts b/packages/core/src/routes/profile.ts index c9a409d95..f29df0fba 100644 --- a/packages/core/src/routes/profile.ts +++ b/packages/core/src/routes/profile.ts @@ -4,8 +4,8 @@ import { has, pick } from '@silverhand/essentials'; import { argon2Verify } from 'hash-wasm'; import { object, string, unknown } from 'zod'; -import { getLogtoConnectorById } from '#src/connectors/index.js'; import RequestError from '#src/errors/RequestError/index.js'; +import { getLogtoConnectorById } from '#src/libraries/connector.js'; import { checkSessionHealth } from '#src/libraries/session.js'; import { getUserInfoByAuthCode } from '#src/libraries/social.js'; import { encryptUserPassword } from '#src/libraries/user.js'; diff --git a/packages/core/src/routes/session/social.bind-social.test.ts b/packages/core/src/routes/session/social.bind-social.test.ts index d5c0282c8..e9756918c 100644 --- a/packages/core/src/routes/session/social.bind-social.test.ts +++ b/packages/core/src/routes/session/social.bind-social.test.ts @@ -3,8 +3,8 @@ import type { User } from '@logto/schemas'; import Provider from 'oidc-provider'; import { mockLogtoConnectorList, mockSignInExperience, mockUser } from '#src/__mocks__/index.js'; -import { getLogtoConnectorById } from '#src/connectors/index.js'; import RequestError from '#src/errors/RequestError/index.js'; +import { getLogtoConnectorById } from '#src/libraries/connector.js'; import { createRequester } from '#src/utils/test-utils.js'; import socialRoutes, { registerRoute } from './social.js'; @@ -83,7 +83,7 @@ const getLogtoConnectorByIdHelper = jest.fn(async (connectorId: string) => { }; }); -jest.mock('#src/connectors.js', () => ({ +jest.mock('#src/libraries/connector.js', () => ({ getLogtoConnectors: jest.fn(async () => mockLogtoConnectorList), getLogtoConnectorById: jest.fn(async (connectorId: string) => { const connector = await getLogtoConnectorByIdHelper(connectorId); diff --git a/packages/core/src/routes/session/social.test.ts b/packages/core/src/routes/session/social.test.ts index 97a0b7987..362a654ae 100644 --- a/packages/core/src/routes/session/social.test.ts +++ b/packages/core/src/routes/session/social.test.ts @@ -4,8 +4,8 @@ import type { User } from '@logto/schemas'; import Provider from 'oidc-provider'; import { mockLogtoConnectorList, mockSignInExperience, mockUser } from '#src/__mocks__/index.js'; -import { getLogtoConnectorById } from '#src/connectors/index.js'; import RequestError from '#src/errors/RequestError/index.js'; +import { getLogtoConnectorById } from '#src/libraries/connector.js'; import { createRequester } from '#src/utils/test-utils.js'; import socialRoutes, { signInRoute } from './social.js'; @@ -90,7 +90,7 @@ const getLogtoConnectorByIdHelper = jest.fn(async (connectorId: string) => { }; }); -jest.mock('#src/connectors.js', () => ({ +jest.mock('#src/libraries/connector.js', () => ({ getLogtoConnectors: jest.fn(async () => mockLogtoConnectorList), getLogtoConnectorById: jest.fn(async (connectorId: string) => { const connector = await getLogtoConnectorByIdHelper(connectorId); diff --git a/packages/core/src/routes/session/social.ts b/packages/core/src/routes/session/social.ts index 6dd497ee5..d9569ca76 100644 --- a/packages/core/src/routes/session/social.ts +++ b/packages/core/src/routes/session/social.ts @@ -5,8 +5,8 @@ import { conditional, pick } from '@silverhand/essentials'; import type Provider from 'oidc-provider'; import { object, string, unknown } from 'zod'; -import { getLogtoConnectorById } from '#src/connectors/index.js'; import RequestError from '#src/errors/RequestError/index.js'; +import { getLogtoConnectorById } from '#src/libraries/connector.js'; import { assignInteractionResults, getApplicationIdFromInteraction, diff --git a/packages/core/src/routes/sign-in-experience/guard.branding.test.ts b/packages/core/src/routes/sign-in-experience/guard.branding.test.ts index 07580b2dd..2e006f136 100644 --- a/packages/core/src/routes/sign-in-experience/guard.branding.test.ts +++ b/packages/core/src/routes/sign-in-experience/guard.branding.test.ts @@ -8,7 +8,7 @@ import { createRequester } from '#src/utils/test-utils.js'; const { mockEsm } = createMockUtils(import.meta.jest); -mockEsm('#src/connectors.js', () => ({ +mockEsm('#src/libraries/connector.js', () => ({ getLogtoConnectors: async () => [], })); diff --git a/packages/core/src/routes/sign-in-experience/guard.color.test.ts b/packages/core/src/routes/sign-in-experience/guard.color.test.ts index d8aebc447..658662121 100644 --- a/packages/core/src/routes/sign-in-experience/guard.color.test.ts +++ b/packages/core/src/routes/sign-in-experience/guard.color.test.ts @@ -7,7 +7,7 @@ import { createRequester } from '#src/utils/test-utils.js'; const { mockEsm } = createMockUtils(import.meta.jest); -mockEsm('#src/connectors.js', () => ({ +mockEsm('#src/libraries/connector.js', () => ({ getLogtoConnectors: async () => [], })); diff --git a/packages/core/src/routes/sign-in-experience/guard.test.ts b/packages/core/src/routes/sign-in-experience/guard.test.ts index 9b1aeb5e8..62e58ad4d 100644 --- a/packages/core/src/routes/sign-in-experience/guard.test.ts +++ b/packages/core/src/routes/sign-in-experience/guard.test.ts @@ -15,7 +15,7 @@ import { MockTenant } from '#src/test-utils/tenant.js'; const { jest } = import.meta; const { mockEsm } = createMockUtils(jest); -mockEsm('#src/connectors.js', () => ({ +mockEsm('#src/libraries/connector.js', () => ({ getLogtoConnectors: jest.fn(async () => [ mockAliyunDmConnector, mockAliyunSmsConnector, diff --git a/packages/core/src/routes/sign-in-experience/index.test.ts b/packages/core/src/routes/sign-in-experience/index.test.ts index 4d7b0b0e1..c7fe0875c 100644 --- a/packages/core/src/routes/sign-in-experience/index.test.ts +++ b/packages/core/src/routes/sign-in-experience/index.test.ts @@ -29,7 +29,7 @@ const logtoConnectors = [ mockAliyunSmsConnector, ]; -await mockEsmWithActual('#src/connectors.js', () => ({ +await mockEsmWithActual('#src/libraries/connector.js', () => ({ getLogtoConnectors: async () => logtoConnectors, })); diff --git a/packages/core/src/routes/sign-in-experience/index.ts b/packages/core/src/routes/sign-in-experience/index.ts index e493aa00c..217f33032 100644 --- a/packages/core/src/routes/sign-in-experience/index.ts +++ b/packages/core/src/routes/sign-in-experience/index.ts @@ -1,7 +1,7 @@ import { ConnectorType, SignInExperiences } from '@logto/schemas'; import { literal, object, string } from 'zod'; -import { getLogtoConnectors } from '#src/connectors/index.js'; +import { getLogtoConnectors } from '#src/libraries/connector.js'; import { validateBranding, validateSignUp, diff --git a/packages/core/src/routes/well-known.test.ts b/packages/core/src/routes/well-known.test.ts index 854bb9808..2069a8882 100644 --- a/packages/core/src/routes/well-known.test.ts +++ b/packages/core/src/routes/well-known.test.ts @@ -31,7 +31,7 @@ const sieQueries = { }; const { findDefaultSignInExperience } = sieQueries; -mockEsm('#src/connectors.js', () => ({ +mockEsm('#src/libraries/connector.js', () => ({ getLogtoConnectors: jest.fn(async () => [ mockAliyunDmConnector, mockAliyunSmsConnector, diff --git a/packages/core/src/routes/well-known.ts b/packages/core/src/routes/well-known.ts index 178e91d70..bf68becdf 100644 --- a/packages/core/src/routes/well-known.ts +++ b/packages/core/src/routes/well-known.ts @@ -3,7 +3,7 @@ import { ConnectorType } from '@logto/connector-kit'; import { adminConsoleApplicationId } from '@logto/schemas'; import etag from 'etag'; -import { getLogtoConnectors } from '#src/connectors/index.js'; +import { getLogtoConnectors } from '#src/libraries/connector.js'; import { getApplicationIdFromInteraction } from '#src/libraries/session.js'; import type { AnonymousRouter, RouterInitArgs } from './types.js'; diff --git a/packages/core/src/connectors/consts.ts b/packages/core/src/utils/connectors/consts.ts similarity index 89% rename from packages/core/src/connectors/consts.ts rename to packages/core/src/utils/connectors/consts.ts index 36d457e77..8a9172817 100644 --- a/packages/core/src/connectors/consts.ts +++ b/packages/core/src/utils/connectors/consts.ts @@ -1,6 +1,6 @@ import { ConnectorError, ConnectorErrorCodes } from '@logto/connector-kit'; -const notImplemented = () => { +export const notImplemented = () => { throw new ConnectorError(ConnectorErrorCodes.NotImplemented); }; diff --git a/packages/core/src/utils/connectors/factories.ts b/packages/core/src/utils/connectors/factories.ts new file mode 100644 index 000000000..68ba81baf --- /dev/null +++ b/packages/core/src/utils/connectors/factories.ts @@ -0,0 +1,69 @@ +import { existsSync } from 'fs'; +import { fileURLToPath } from 'node:url'; +import path from 'path'; + +import { connectorDirectory } from '@logto/cli/lib/constants.js'; +import { getConnectorPackagesFromDirectory } from '@logto/cli/lib/utilities.js'; +import { findPackage } from '@logto/shared'; +import chalk from 'chalk'; + +import type { ConnectorFactory } from '#src/utils/connectors/types.js'; + +import { notImplemented } from './consts.js'; +import { parseMetadata, validateConnectorModule } from './index.js'; +import { loadConnector } from './loader.js'; + +// eslint-disable-next-line @silverhand/fp/no-let +let cachedConnectorFactories: ConnectorFactory[] | undefined; + +export const loadConnectorFactories = async () => { + if (cachedConnectorFactories) { + return cachedConnectorFactories; + } + + const currentDirname = path.dirname(fileURLToPath(import.meta.url)); + const coreDirectory = await findPackage(currentDirname); + const directory = coreDirectory && path.join(coreDirectory, connectorDirectory); + + if (!directory || !existsSync(directory)) { + return []; + } + + const connectorPackages = await getConnectorPackagesFromDirectory(directory); + + const connectorFactories = await Promise.all( + connectorPackages.map(async ({ path: packagePath, name }) => { + try { + const createConnector = await loadConnector(packagePath); + const rawConnector = await createConnector({ getConfig: notImplemented }); + validateConnectorModule(rawConnector); + + return { + metadata: await parseMetadata(rawConnector.metadata, packagePath), + type: rawConnector.type, + createConnector, + path: packagePath, + }; + } catch (error: unknown) { + if (error instanceof Error) { + console.log( + `${chalk.red( + `[load-connector] skip ${chalk.bold(name)} due to error: ${error.message}` + )}` + ); + + return; + } + + throw error; + } + }) + ); + + // eslint-disable-next-line @silverhand/fp/no-mutation + cachedConnectorFactories = connectorFactories.filter( + (connectorFactory): connectorFactory is ConnectorFactory => connectorFactory !== undefined + ); + + return cachedConnectorFactories; +}; diff --git a/packages/core/src/connectors/utilities/index.ts b/packages/core/src/utils/connectors/index.ts similarity index 77% rename from packages/core/src/connectors/utilities/index.ts rename to packages/core/src/utils/connectors/index.ts index f8675baca..4771b3d46 100644 --- a/packages/core/src/connectors/utilities/index.ts +++ b/packages/core/src/utils/connectors/index.ts @@ -5,19 +5,6 @@ import path from 'path'; import type { AllConnector, BaseConnector } from '@logto/connector-kit'; import { ConnectorError, ConnectorErrorCodes, ConnectorType } from '@logto/connector-kit'; -import RequestError from '#src/errors/RequestError/index.js'; -import { findAllConnectors } from '#src/queries/connector.js'; -import assertThat from '#src/utils/assert-that.js'; - -export const getConnectorConfig = async (id: string): Promise => { - const connectors = await findAllConnectors(); - const connector = connectors.find((connector) => connector.id === id); - - assertThat(connector, new RequestError({ code: 'entity.not_found', id, status: 404 })); - - return connector.config; -}; - export function validateConnectorModule( connector: Partial> ): asserts connector is BaseConnector { diff --git a/packages/core/src/connectors/utilities/loader.ts b/packages/core/src/utils/connectors/loader.ts similarity index 100% rename from packages/core/src/connectors/utilities/loader.ts rename to packages/core/src/utils/connectors/loader.ts diff --git a/packages/core/src/utils/connectors/platform.test.ts b/packages/core/src/utils/connectors/platform.test.ts new file mode 100644 index 000000000..ce0601c2f --- /dev/null +++ b/packages/core/src/utils/connectors/platform.test.ts @@ -0,0 +1,46 @@ +import { ConnectorType } from '@logto/schemas'; + +import { + mockMetadata0, + mockMetadata1, + mockConnector0, + mockConnector1, + mockLogtoConnector, + mockLogtoConnectorList, +} from '#src/__mocks__/index.js'; +import RequestError from '#src/errors/RequestError/index.js'; + +import { checkSocialConnectorTargetAndPlatformUniqueness } from './platform.js'; + +describe('check social connector target and platform uniqueness', () => { + it('throws if more than one same-platform social connectors sharing the same `target`', () => { + const mockConnectors = [ + { + dbEntry: mockConnector0, + metadata: { ...mockMetadata0, target: 'target' }, + type: ConnectorType.Social, + ...mockLogtoConnector, + }, + { + dbEntry: mockConnector1, + metadata: { ...mockMetadata1, target: 'target' }, + type: ConnectorType.Social, + ...mockLogtoConnector, + }, + ]; + expect(() => { + checkSocialConnectorTargetAndPlatformUniqueness(mockConnectors); + }).toMatchError( + new RequestError({ + code: 'connector.multiple_target_with_same_platform', + status: 400, + }) + ); + }); + + it('should not throw when no multiple connectors sharing same target and platform', () => { + expect(() => { + checkSocialConnectorTargetAndPlatformUniqueness(mockLogtoConnectorList); + }).not.toThrow(); + }); +}); diff --git a/packages/core/src/utils/connectors/platform.ts b/packages/core/src/utils/connectors/platform.ts new file mode 100644 index 000000000..b7bb6893f --- /dev/null +++ b/packages/core/src/utils/connectors/platform.ts @@ -0,0 +1,27 @@ +import { ConnectorType } from '@logto/schemas'; + +import assertThat from '#src/utils/assert-that.js'; +import type { LogtoConnector } from '#src/utils/connectors/types.js'; + +export const checkSocialConnectorTargetAndPlatformUniqueness = (connectors: LogtoConnector[]) => { + const targetAndPlatformObjectsInUse = connectors + .filter(({ type }) => type === ConnectorType.Social) + .map(({ metadata: { target, platform } }) => ({ + target, + platform, + })); + + const targetAndPlatformSet = new Set(); + + for (const targetAndPlatformObject of targetAndPlatformObjectsInUse) { + const { target, platform } = targetAndPlatformObject; + + if (platform === null) { + continue; + } + + const element = JSON.stringify([target, platform]); + assertThat(!targetAndPlatformSet.has(element), 'connector.multiple_target_with_same_platform'); + targetAndPlatformSet.add(element); + } +}; diff --git a/packages/core/src/connectors/types.ts b/packages/core/src/utils/connectors/types.ts similarity index 100% rename from packages/core/src/connectors/types.ts rename to packages/core/src/utils/connectors/types.ts