0
Fork 0
mirror of https://github.com/logto-io/logto.git synced 2025-02-17 22:04:19 -05:00

feat: update virtual connector loading to getConfig by id (#2508)

This commit is contained in:
Darcy Ye 2022-11-23 14:11:13 +08:00 committed by GitHub
parent 973846eaed
commit f0f9bec107
No known key found for this signature in database
GPG key ID: 4AEE18F83AFDEB23
5 changed files with 112 additions and 80 deletions

View file

@ -3,7 +3,7 @@ import type { Connector } from '@logto/schemas';
import { ConnectorType } from '@logto/schemas'; import { ConnectorType } from '@logto/schemas';
import { any } from 'zod'; import { any } from 'zod';
import type { VirtualConnector, LogtoConnector } from '#src/connectors/types.js'; import type { LogtoConnector, ConnectorFactory } from '#src/connectors/types.js';
import { import {
mockConnector0, mockConnector0,
@ -43,10 +43,11 @@ export const mockLogtoConnector = {
configGuard: any(), configGuard: any(),
}; };
export const mockVirtualConnector: VirtualConnector = { export const mockConnectorFactory: ConnectorFactory = {
metadata: mockMetadata, metadata: mockMetadata,
type: ConnectorType.Social, type: ConnectorType.Social,
...mockLogtoConnector, path: 'random_path',
createConnector: jest.fn(),
}; };
export const mockConnectorList: Connector[] = [ export const mockConnectorList: Connector[] = [

View file

@ -14,16 +14,17 @@ import { findAllConnectors, insertConnector } from '#src/queries/connector.js';
import { defaultConnectorMethods } from './consts.js'; import { defaultConnectorMethods } from './consts.js';
import { metaUrl } from './meta-url.js'; import { metaUrl } from './meta-url.js';
import type { VirtualConnector, LogtoConnector } from './types.js'; import type { ConnectorFactory, LogtoConnector } from './types.js';
import { getConnectorConfig, readUrl, validateConnectorModule } from './utilities/index.js'; import { getConnectorConfig, readUrl, validateConnectorModule } from './utilities/index.js';
const currentDirname = path.dirname(fileURLToPath(metaUrl)); const currentDirname = path.dirname(fileURLToPath(metaUrl));
// eslint-disable-next-line @silverhand/fp/no-let
let cachedVirtualConnectors: VirtualConnector[] | undefined;
export const loadVirtualConnectors = async () => { // eslint-disable-next-line @silverhand/fp/no-let
if (cachedVirtualConnectors) { let cachedConnectorFactories: ConnectorFactory[] | undefined;
return cachedVirtualConnectors;
export const loadConnectorFactories = async () => {
if (cachedConnectorFactories) {
return cachedConnectorFactories;
} }
const coreDirectory = await findPackage(currentDirname); const coreDirectory = await findPackage(currentDirname);
@ -35,7 +36,7 @@ export const loadVirtualConnectors = async () => {
const connectorPackages = await getConnectorPackagesFromDirectory(directory); const connectorPackages = await getConnectorPackagesFromDirectory(directory);
const connectors = await Promise.all( const connectorFactories = await Promise.all(
connectorPackages.map(async ({ path: packagePath, name }) => { connectorPackages.map(async ({ path: packagePath, name }) => {
try { try {
// TODO: fix type and remove `/lib/index.js` suffix once we upgrade all connectors to ESM // TODO: fix type and remove `/lib/index.js` suffix once we upgrade all connectors to ESM
@ -50,28 +51,12 @@ export const loadVirtualConnectors = async () => {
const rawConnector = await createConnector({ getConfig: getConnectorConfig }); const rawConnector = await createConnector({ getConfig: getConnectorConfig });
validateConnectorModule(rawConnector); validateConnectorModule(rawConnector);
const connector: VirtualConnector = { return {
...defaultConnectorMethods, metadata: rawConnector.metadata,
...rawConnector, type: rawConnector.type,
metadata: { createConnector,
...rawConnector.metadata, path: packagePath + '/lib/index.js',
logo: await readUrl(rawConnector.metadata.logo, packagePath, 'svg'),
logoDark:
rawConnector.metadata.logoDark &&
(await readUrl(rawConnector.metadata.logoDark, packagePath, 'svg')),
readme: await readUrl(rawConnector.metadata.readme, packagePath, 'text'),
configTemplate: await readUrl(
rawConnector.metadata.configTemplate,
packagePath,
'text'
),
},
validateConfig: (config: unknown) => {
validateConfig(config, rawConnector.configGuard);
},
}; };
return connector;
} catch (error: unknown) { } catch (error: unknown) {
if (error instanceof Error) { if (error instanceof Error) {
console.log( console.log(
@ -89,34 +74,72 @@ export const loadVirtualConnectors = async () => {
); );
// eslint-disable-next-line @silverhand/fp/no-mutation // eslint-disable-next-line @silverhand/fp/no-mutation
cachedVirtualConnectors = connectors.filter( cachedConnectorFactories = connectorFactories.filter(
(connector): connector is VirtualConnector => connector !== undefined (connectorFactory): connectorFactory is ConnectorFactory => connectorFactory !== undefined
); );
return cachedVirtualConnectors; return cachedConnectorFactories;
}; };
export const getLogtoConnectors = async (): Promise<LogtoConnector[]> => { export const getLogtoConnectors = async (): Promise<LogtoConnector[]> => {
const connectors = await findAllConnectors(); const databaseConnectors = await findAllConnectors();
const virtualConnectors = await loadVirtualConnectors(); const logtoConnectors = await Promise.all(
databaseConnectors.map(async (databaseConnector) => {
const { id, metadata, connectorId } = databaseConnector;
return connectors const connectorFactories = await loadConnectorFactories();
.map((connector) => { const connectorFactory = connectorFactories.find(
const { metadata, connectorId } = connector; ({ metadata }) => metadata.id === connectorId
const virtualConnector = virtualConnectors.find(({ metadata: { id } }) => id === connectorId); );
if (!virtualConnector) { if (!connectorFactory) {
return; return;
} }
return { const { createConnector, path: packagePath } = connectorFactory;
...virtualConnector,
metadata: { ...virtualConnector.metadata, ...metadata }, try {
dbEntry: connector, const rawConnector = await createConnector({
}; getConfig: async () => {
return getConnectorConfig(id);
},
});
validateConnectorModule(rawConnector);
const connector: AllConnector = {
...defaultConnectorMethods,
...rawConnector,
metadata: {
...rawConnector.metadata,
logo: await readUrl(rawConnector.metadata.logo, packagePath, 'svg'),
logoDark:
rawConnector.metadata.logoDark &&
(await readUrl(rawConnector.metadata.logoDark, packagePath, 'svg')),
readme: await readUrl(rawConnector.metadata.readme, packagePath, 'text'),
configTemplate: await readUrl(
rawConnector.metadata.configTemplate,
packagePath,
'text'
),
...metadata,
},
};
return {
...connector,
validateConfig: (config: unknown) => {
validateConfig(config, rawConnector.configGuard);
},
dbEntry: databaseConnector,
};
} catch {}
}) })
.filter((connector): connector is LogtoConnector => connector !== undefined); );
return logtoConnectors.filter(
(logtoConnector): logtoConnector is LogtoConnector => logtoConnector !== undefined
);
}; };
export const getLogtoConnectorById = async (id: string): Promise<LogtoConnector> => { export const getLogtoConnectorById = async (id: string): Promise<LogtoConnector> => {
@ -136,8 +159,10 @@ export const getLogtoConnectorById = async (id: string): Promise<LogtoConnector>
export const initConnectors = async () => { export const initConnectors = async () => {
const connectors = await findAllConnectors(); const connectors = await findAllConnectors();
const existingConnectors = new Map(connectors.map((connector) => [connector.id, connector])); const existingConnectors = new Map(
const allConnectors = await loadVirtualConnectors(); connectors.map((connector) => [connector.connectorId, connector])
);
const allConnectors = await loadConnectorFactories();
const newConnectors = allConnectors.filter(({ metadata: { id } }) => { const newConnectors = allConnectors.filter(({ metadata: { id } }) => {
const connector = existingConnectors.get(id); const connector = existingConnectors.get(id);

View file

@ -1,4 +1,4 @@
import type { AllConnector } from '@logto/connector-kit'; import type { AllConnector, CreateConnector } from '@logto/connector-kit';
import type { Connector, PasscodeType } from '@logto/schemas'; import type { Connector, PasscodeType } from '@logto/schemas';
import { z } from 'zod'; import { z } from 'zod';
@ -19,13 +19,17 @@ export type SocialUserInfo = z.infer<typeof socialUserInfoGuard>;
/** /**
* Dynamic loaded connector type. * Dynamic loaded connector type.
*/ */
export type VirtualConnector<T extends AllConnector = AllConnector> = T & { export type ConnectorFactory<T extends AllConnector = AllConnector> = Pick<
validateConfig: (config: unknown) => void; T,
'type' | 'metadata'
> & {
createConnector: CreateConnector<AllConnector>;
path: string;
}; };
/** /**
* The connector type with full context. * The connector type with full context.
*/ */
export type LogtoConnector<T extends AllConnector = AllConnector> = VirtualConnector<T> & { export type LogtoConnector<T extends AllConnector = AllConnector> = T & {
dbEntry: Connector; validateConfig: (config: unknown) => void;
}; } & { dbEntry: Connector };

View file

@ -6,11 +6,11 @@ import { any } from 'zod';
import { import {
mockMetadata, mockMetadata,
mockConnector, mockConnector,
mockVirtualConnector, mockConnectorFactory,
mockLogtoConnectorList, mockLogtoConnectorList,
} from '#src/__mocks__/index.js'; } from '#src/__mocks__/index.js';
import { defaultConnectorMethods } from '#src/connectors/consts.js'; import { defaultConnectorMethods } from '#src/connectors/consts.js';
import type { VirtualConnector, LogtoConnector } from '#src/connectors/types.js'; import type { ConnectorFactory, LogtoConnector } from '#src/connectors/types.js';
import RequestError from '#src/errors/RequestError/index.js'; import RequestError from '#src/errors/RequestError/index.js';
import { countConnectorByConnectorId, deleteConnectorById } from '#src/queries/connector.js'; import { countConnectorByConnectorId, deleteConnectorById } from '#src/queries/connector.js';
import assertThat from '#src/utils/assert-that.js'; import assertThat from '#src/utils/assert-that.js';
@ -18,8 +18,8 @@ import { createRequester } from '#src/utils/test-utils.js';
import connectorRoutes from './connector.js'; import connectorRoutes from './connector.js';
const loadVirtualConnectorsPlaceHolder = jest.fn() as jest.MockedFunction< const loadConnectorFactoriesPlaceHolder = jest.fn() as jest.MockedFunction<
() => Promise<VirtualConnector[]> () => Promise<ConnectorFactory[]>
>; >;
const getLogtoConnectorsPlaceHolder = jest.fn() as jest.MockedFunction< const getLogtoConnectorsPlaceHolder = jest.fn() as jest.MockedFunction<
() => Promise<LogtoConnector[]> () => Promise<LogtoConnector[]>
@ -32,7 +32,7 @@ jest.mock('#src/queries/connector.js', () => ({
})); }));
jest.mock('#src/connectors/index.js', () => ({ jest.mock('#src/connectors/index.js', () => ({
loadVirtualConnectors: async () => loadVirtualConnectorsPlaceHolder(), loadConnectorFactories: async () => loadConnectorFactoriesPlaceHolder(),
getLogtoConnectors: async () => getLogtoConnectorsPlaceHolder(), getLogtoConnectors: async () => getLogtoConnectorsPlaceHolder(),
getLogtoConnectorById: async (connectorId: string) => { getLogtoConnectorById: async (connectorId: string) => {
const connectors = await getLogtoConnectorsPlaceHolder(); const connectors = await getLogtoConnectorsPlaceHolder();
@ -112,10 +112,10 @@ describe('connector route', () => {
}); });
it('should post a new connector record', async () => { it('should post a new connector record', async () => {
loadVirtualConnectorsPlaceHolder.mockResolvedValueOnce([ loadConnectorFactoriesPlaceHolder.mockResolvedValueOnce([
{ {
...mockVirtualConnector, ...mockConnectorFactory,
metadata: { ...mockVirtualConnector.metadata, id: 'connectorId' }, metadata: { ...mockConnectorFactory.metadata, id: 'connectorId' },
}, },
]); ]);
mockedCountConnectorByConnectorId.mockResolvedValueOnce({ count: 0 }); mockedCountConnectorByConnectorId.mockResolvedValueOnce({ count: 0 });
@ -135,11 +135,11 @@ describe('connector route', () => {
expect(response).toHaveProperty('statusCode', 200); expect(response).toHaveProperty('statusCode', 200);
}); });
it('throws when virtual connector not found', async () => { it('throws when connector factory not found', async () => {
loadVirtualConnectorsPlaceHolder.mockResolvedValueOnce([ loadConnectorFactoriesPlaceHolder.mockResolvedValueOnce([
{ {
...mockVirtualConnector, ...mockConnectorFactory,
metadata: { ...mockVirtualConnector.metadata, id: 'connectorId' }, metadata: { ...mockConnectorFactory.metadata, id: 'connectorId' },
}, },
]); ]);
mockedCountConnectorByConnectorId.mockResolvedValueOnce({ count: 0 }); mockedCountConnectorByConnectorId.mockResolvedValueOnce({ count: 0 });
@ -150,11 +150,11 @@ describe('connector route', () => {
expect(response).toHaveProperty('statusCode', 422); expect(response).toHaveProperty('statusCode', 422);
}); });
it('should post a new record when add more than 1 instance with virtual connector', async () => { it('should post a new record when add more than 1 instance with connector factory', async () => {
loadVirtualConnectorsPlaceHolder.mockResolvedValueOnce([ loadConnectorFactoriesPlaceHolder.mockResolvedValueOnce([
{ {
...mockVirtualConnector, ...mockConnectorFactory,
metadata: { ...mockVirtualConnector.metadata, id: 'id0', isStandard: true }, metadata: { ...mockConnectorFactory.metadata, id: 'id0', isStandard: true },
}, },
]); ]);
mockedCountConnectorByConnectorId.mockResolvedValueOnce({ count: 1 }); mockedCountConnectorByConnectorId.mockResolvedValueOnce({ count: 1 });
@ -174,11 +174,11 @@ describe('connector route', () => {
expect(response).toHaveProperty('statusCode', 200); expect(response).toHaveProperty('statusCode', 200);
}); });
it('throws when add more than 1 instance with non-virtual connector', async () => { it('throws when add more than 1 instance with non-connector factory', async () => {
loadVirtualConnectorsPlaceHolder.mockResolvedValueOnce([ loadConnectorFactoriesPlaceHolder.mockResolvedValueOnce([
{ {
...mockVirtualConnector, ...mockConnectorFactory,
metadata: { ...mockVirtualConnector.metadata, id: 'id0' }, metadata: { ...mockConnectorFactory.metadata, id: 'id0' },
}, },
]); ]);
mockedCountConnectorByConnectorId.mockResolvedValueOnce({ count: 1 }); mockedCountConnectorByConnectorId.mockResolvedValueOnce({ count: 1 });

View file

@ -8,7 +8,7 @@ import { object, string } from 'zod';
import { import {
getLogtoConnectorById, getLogtoConnectorById,
getLogtoConnectors, getLogtoConnectors,
loadVirtualConnectors, loadConnectorFactories,
} from '#src/connectors/index.js'; } from '#src/connectors/index.js';
import type { LogtoConnector } from '#src/connectors/types.js'; import type { LogtoConnector } from '#src/connectors/types.js';
import RequestError from '#src/errors/RequestError/index.js'; import RequestError from '#src/errors/RequestError/index.js';
@ -101,10 +101,12 @@ export default function connectorRoutes<T extends AuthedRouter>(router: T) {
body, body,
} = ctx.guard; } = ctx.guard;
const virtualConnectors = await loadVirtualConnectors(); const connectorFactories = await loadConnectorFactories();
const virtualConnector = virtualConnectors.find(({ metadata: { id } }) => id === connectorId); const connectorFactory = connectorFactories.find(
({ metadata: { id } }) => id === connectorId
);
if (!virtualConnector) { if (!connectorFactory) {
throw new RequestError({ throw new RequestError({
code: 'connector.not_found_with_connector_id', code: 'connector.not_found_with_connector_id',
status: 422, status: 422,
@ -113,7 +115,7 @@ export default function connectorRoutes<T extends AuthedRouter>(router: T) {
const { count } = await countConnectorByConnectorId(connectorId); const { count } = await countConnectorByConnectorId(connectorId);
assertThat( assertThat(
count === 0 || virtualConnector.metadata.isStandard === true, count === 0 || connectorFactory.metadata.isStandard === true,
new RequestError({ new RequestError({
code: 'connector.multiple_instances_not_supported', code: 'connector.multiple_instances_not_supported',
status: 422, status: 422,