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:
parent
ecbf028f4e
commit
a47671eb94
46 changed files with 302 additions and 305 deletions
|
@ -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,
|
||||
|
|
|
@ -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;
|
||||
};
|
|
@ -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;
|
|
@ -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 })
|
||||
);
|
||||
});
|
|
@ -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({
|
||||
|
|
|
@ -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 })
|
||||
);
|
||||
});
|
||||
|
|
|
@ -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;
|
||||
};
|
||||
|
|
|
@ -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(),
|
||||
}));
|
||||
|
||||
|
|
|
@ -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);
|
||||
|
|
|
@ -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(),
|
||||
}));
|
||||
|
||||
|
|
|
@ -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';
|
||||
|
||||
|
|
|
@ -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,
|
||||
|
|
|
@ -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) {
|
||||
|
|
|
@ -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';
|
||||
|
||||
|
|
|
@ -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();
|
||||
|
|
|
@ -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';
|
||||
|
||||
|
|
|
@ -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,
|
||||
}));
|
||||
|
|
|
@ -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 } }),
|
||||
|
|
|
@ -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';
|
||||
|
|
|
@ -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);
|
||||
|
||||
|
|
|
@ -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' } }),
|
||||
}));
|
||||
|
||||
|
|
|
@ -1,4 +1,4 @@
|
|||
import { getLogtoConnectorById } from '#src/connectors/index.js';
|
||||
import { getLogtoConnectorById } from '#src/libraries/connector.js';
|
||||
import {
|
||||
findUserByEmail,
|
||||
findUserByUsername,
|
||||
|
|
|
@ -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',
|
||||
|
|
|
@ -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 {
|
||||
|
|
|
@ -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' },
|
||||
}),
|
||||
|
|
|
@ -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' },
|
||||
}),
|
||||
|
|
|
@ -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,
|
||||
|
|
|
@ -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,
|
||||
}));
|
||||
|
|
|
@ -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';
|
||||
|
|
|
@ -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);
|
||||
|
|
|
@ -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);
|
||||
|
|
|
@ -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,
|
||||
|
|
|
@ -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 () => [],
|
||||
}));
|
||||
|
||||
|
|
|
@ -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 () => [],
|
||||
}));
|
||||
|
||||
|
|
|
@ -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,
|
||||
|
|
|
@ -29,7 +29,7 @@ const logtoConnectors = [
|
|||
mockAliyunSmsConnector,
|
||||
];
|
||||
|
||||
await mockEsmWithActual('#src/connectors.js', () => ({
|
||||
await mockEsmWithActual('#src/libraries/connector.js', () => ({
|
||||
getLogtoConnectors: async () => logtoConnectors,
|
||||
}));
|
||||
|
||||
|
|
|
@ -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,
|
||||
|
|
|
@ -31,7 +31,7 @@ const sieQueries = {
|
|||
};
|
||||
const { findDefaultSignInExperience } = sieQueries;
|
||||
|
||||
mockEsm('#src/connectors.js', () => ({
|
||||
mockEsm('#src/libraries/connector.js', () => ({
|
||||
getLogtoConnectors: jest.fn(async () => [
|
||||
mockAliyunDmConnector,
|
||||
mockAliyunSmsConnector,
|
||||
|
|
|
@ -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';
|
||||
|
|
|
@ -1,6 +1,6 @@
|
|||
import { ConnectorError, ConnectorErrorCodes } from '@logto/connector-kit';
|
||||
|
||||
const notImplemented = () => {
|
||||
export const notImplemented = () => {
|
||||
throw new ConnectorError(ConnectorErrorCodes.NotImplemented);
|
||||
};
|
||||
|
69
packages/core/src/utils/connectors/factories.ts
Normal file
69
packages/core/src/utils/connectors/factories.ts
Normal 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;
|
||||
};
|
|
@ -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> {
|
46
packages/core/src/utils/connectors/platform.test.ts
Normal file
46
packages/core/src/utils/connectors/platform.test.ts
Normal 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();
|
||||
});
|
||||
});
|
27
packages/core/src/utils/connectors/platform.ts
Normal file
27
packages/core/src/utils/connectors/platform.ts
Normal 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);
|
||||
}
|
||||
};
|
Loading…
Add table
Reference in a new issue