0
Fork 0
mirror of https://github.com/logto-io/logto.git synced 2025-02-10 21:58:23 -05:00

refactor(core): reorg connector files

This commit is contained in:
Gao Sun 2023-01-09 19:33:06 +08:00
parent ecbf028f4e
commit a47671eb94
No known key found for this signature in database
GPG key ID: 13EBE123E4773688
46 changed files with 302 additions and 305 deletions

View file

@ -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,

View file

@ -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<LogtoConnector[]> => {
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<LogtoConnector> => {
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;
};

View file

@ -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;

View file

@ -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 })
);
});

View file

@ -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({

View file

@ -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 })
);
});

View file

@ -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<unknown> => {
const connectors = await findAllConnectors();
const connector = connectors.find((connector) => connector.id === id);
const targetAndPlatformSet = new Set<string>();
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<LogtoConnector[]> => {
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<LogtoConnector> => {
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;
};

View file

@ -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(),
}));

View file

@ -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);

View file

@ -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(),
}));

View file

@ -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';

View file

@ -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,

View file

@ -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) {

View file

@ -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';

View file

@ -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<Promise<LogtoConnector[]>, []>();
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();

View file

@ -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';

View file

@ -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,
}));

View file

@ -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 } }),

View file

@ -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';

View file

@ -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);

View file

@ -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' } }),
}));

View file

@ -1,4 +1,4 @@
import { getLogtoConnectorById } from '#src/connectors/index.js';
import { getLogtoConnectorById } from '#src/libraries/connector.js';
import {
findUserByEmail,
findUserByUsername,

View file

@ -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',

View file

@ -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 {

View file

@ -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' },
}),

View file

@ -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' },
}),

View file

@ -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,

View file

@ -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,
}));

View file

@ -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';

View file

@ -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);

View file

@ -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);

View file

@ -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,

View file

@ -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 () => [],
}));

View file

@ -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 () => [],
}));

View file

@ -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,

View file

@ -29,7 +29,7 @@ const logtoConnectors = [
mockAliyunSmsConnector,
];
await mockEsmWithActual('#src/connectors.js', () => ({
await mockEsmWithActual('#src/libraries/connector.js', () => ({
getLogtoConnectors: async () => logtoConnectors,
}));

View file

@ -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,

View file

@ -31,7 +31,7 @@ const sieQueries = {
};
const { findDefaultSignInExperience } = sieQueries;
mockEsm('#src/connectors.js', () => ({
mockEsm('#src/libraries/connector.js', () => ({
getLogtoConnectors: jest.fn(async () => [
mockAliyunDmConnector,
mockAliyunSmsConnector,

View file

@ -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';

View file

@ -1,6 +1,6 @@
import { ConnectorError, ConnectorErrorCodes } from '@logto/connector-kit';
const notImplemented = () => {
export const notImplemented = () => {
throw new ConnectorError(ConnectorErrorCodes.NotImplemented);
};

View file

@ -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;
};

View file

@ -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<unknown> => {
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<BaseConnector<ConnectorType>>
): asserts connector is BaseConnector<ConnectorType> {

View file

@ -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();
});
});

View file

@ -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<string>();
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);
}
};