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:
parent
973846eaed
commit
f0f9bec107
5 changed files with 112 additions and 80 deletions
|
@ -3,7 +3,7 @@ import type { Connector } from '@logto/schemas';
|
|||
import { ConnectorType } from '@logto/schemas';
|
||||
import { any } from 'zod';
|
||||
|
||||
import type { VirtualConnector, LogtoConnector } from '#src/connectors/types.js';
|
||||
import type { LogtoConnector, ConnectorFactory } from '#src/connectors/types.js';
|
||||
|
||||
import {
|
||||
mockConnector0,
|
||||
|
@ -43,10 +43,11 @@ export const mockLogtoConnector = {
|
|||
configGuard: any(),
|
||||
};
|
||||
|
||||
export const mockVirtualConnector: VirtualConnector = {
|
||||
export const mockConnectorFactory: ConnectorFactory = {
|
||||
metadata: mockMetadata,
|
||||
type: ConnectorType.Social,
|
||||
...mockLogtoConnector,
|
||||
path: 'random_path',
|
||||
createConnector: jest.fn(),
|
||||
};
|
||||
|
||||
export const mockConnectorList: Connector[] = [
|
||||
|
|
|
@ -14,16 +14,17 @@ import { findAllConnectors, insertConnector } from '#src/queries/connector.js';
|
|||
|
||||
import { defaultConnectorMethods } from './consts.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';
|
||||
|
||||
const currentDirname = path.dirname(fileURLToPath(metaUrl));
|
||||
// eslint-disable-next-line @silverhand/fp/no-let
|
||||
let cachedVirtualConnectors: VirtualConnector[] | undefined;
|
||||
|
||||
export const loadVirtualConnectors = async () => {
|
||||
if (cachedVirtualConnectors) {
|
||||
return cachedVirtualConnectors;
|
||||
// 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);
|
||||
|
@ -35,7 +36,7 @@ export const loadVirtualConnectors = async () => {
|
|||
|
||||
const connectorPackages = await getConnectorPackagesFromDirectory(directory);
|
||||
|
||||
const connectors = await Promise.all(
|
||||
const connectorFactories = await Promise.all(
|
||||
connectorPackages.map(async ({ path: packagePath, name }) => {
|
||||
try {
|
||||
// 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 });
|
||||
validateConnectorModule(rawConnector);
|
||||
|
||||
const connector: VirtualConnector = {
|
||||
...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'
|
||||
),
|
||||
},
|
||||
validateConfig: (config: unknown) => {
|
||||
validateConfig(config, rawConnector.configGuard);
|
||||
},
|
||||
return {
|
||||
metadata: rawConnector.metadata,
|
||||
type: rawConnector.type,
|
||||
createConnector,
|
||||
path: packagePath + '/lib/index.js',
|
||||
};
|
||||
|
||||
return connector;
|
||||
} catch (error: unknown) {
|
||||
if (error instanceof Error) {
|
||||
console.log(
|
||||
|
@ -89,34 +74,72 @@ export const loadVirtualConnectors = async () => {
|
|||
);
|
||||
|
||||
// eslint-disable-next-line @silverhand/fp/no-mutation
|
||||
cachedVirtualConnectors = connectors.filter(
|
||||
(connector): connector is VirtualConnector => connector !== undefined
|
||||
cachedConnectorFactories = connectorFactories.filter(
|
||||
(connectorFactory): connectorFactory is ConnectorFactory => connectorFactory !== undefined
|
||||
);
|
||||
|
||||
return cachedVirtualConnectors;
|
||||
return cachedConnectorFactories;
|
||||
};
|
||||
|
||||
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
|
||||
.map((connector) => {
|
||||
const { metadata, connectorId } = connector;
|
||||
const virtualConnector = virtualConnectors.find(({ metadata: { id } }) => id === connectorId);
|
||||
const connectorFactories = await loadConnectorFactories();
|
||||
const connectorFactory = connectorFactories.find(
|
||||
({ metadata }) => metadata.id === connectorId
|
||||
);
|
||||
|
||||
if (!virtualConnector) {
|
||||
if (!connectorFactory) {
|
||||
return;
|
||||
}
|
||||
|
||||
return {
|
||||
...virtualConnector,
|
||||
metadata: { ...virtualConnector.metadata, ...metadata },
|
||||
dbEntry: connector,
|
||||
};
|
||||
const { createConnector, path: packagePath } = connectorFactory;
|
||||
|
||||
try {
|
||||
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> => {
|
||||
|
@ -136,8 +159,10 @@ export const getLogtoConnectorById = async (id: string): Promise<LogtoConnector>
|
|||
|
||||
export const initConnectors = async () => {
|
||||
const connectors = await findAllConnectors();
|
||||
const existingConnectors = new Map(connectors.map((connector) => [connector.id, connector]));
|
||||
const allConnectors = await loadVirtualConnectors();
|
||||
const existingConnectors = new Map(
|
||||
connectors.map((connector) => [connector.connectorId, connector])
|
||||
);
|
||||
const allConnectors = await loadConnectorFactories();
|
||||
const newConnectors = allConnectors.filter(({ metadata: { id } }) => {
|
||||
const connector = existingConnectors.get(id);
|
||||
|
||||
|
|
|
@ -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 { z } from 'zod';
|
||||
|
||||
|
@ -19,13 +19,17 @@ export type SocialUserInfo = z.infer<typeof socialUserInfoGuard>;
|
|||
/**
|
||||
* Dynamic loaded connector type.
|
||||
*/
|
||||
export type VirtualConnector<T extends AllConnector = AllConnector> = T & {
|
||||
validateConfig: (config: unknown) => void;
|
||||
export type ConnectorFactory<T extends AllConnector = AllConnector> = Pick<
|
||||
T,
|
||||
'type' | 'metadata'
|
||||
> & {
|
||||
createConnector: CreateConnector<AllConnector>;
|
||||
path: string;
|
||||
};
|
||||
|
||||
/**
|
||||
* The connector type with full context.
|
||||
*/
|
||||
export type LogtoConnector<T extends AllConnector = AllConnector> = VirtualConnector<T> & {
|
||||
dbEntry: Connector;
|
||||
};
|
||||
export type LogtoConnector<T extends AllConnector = AllConnector> = T & {
|
||||
validateConfig: (config: unknown) => void;
|
||||
} & { dbEntry: Connector };
|
||||
|
|
|
@ -6,11 +6,11 @@ import { any } from 'zod';
|
|||
import {
|
||||
mockMetadata,
|
||||
mockConnector,
|
||||
mockVirtualConnector,
|
||||
mockConnectorFactory,
|
||||
mockLogtoConnectorList,
|
||||
} from '#src/__mocks__/index.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 { countConnectorByConnectorId, deleteConnectorById } from '#src/queries/connector.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';
|
||||
|
||||
const loadVirtualConnectorsPlaceHolder = jest.fn() as jest.MockedFunction<
|
||||
() => Promise<VirtualConnector[]>
|
||||
const loadConnectorFactoriesPlaceHolder = jest.fn() as jest.MockedFunction<
|
||||
() => Promise<ConnectorFactory[]>
|
||||
>;
|
||||
const getLogtoConnectorsPlaceHolder = jest.fn() as jest.MockedFunction<
|
||||
() => Promise<LogtoConnector[]>
|
||||
|
@ -32,7 +32,7 @@ jest.mock('#src/queries/connector.js', () => ({
|
|||
}));
|
||||
|
||||
jest.mock('#src/connectors/index.js', () => ({
|
||||
loadVirtualConnectors: async () => loadVirtualConnectorsPlaceHolder(),
|
||||
loadConnectorFactories: async () => loadConnectorFactoriesPlaceHolder(),
|
||||
getLogtoConnectors: async () => getLogtoConnectorsPlaceHolder(),
|
||||
getLogtoConnectorById: async (connectorId: string) => {
|
||||
const connectors = await getLogtoConnectorsPlaceHolder();
|
||||
|
@ -112,10 +112,10 @@ describe('connector route', () => {
|
|||
});
|
||||
|
||||
it('should post a new connector record', async () => {
|
||||
loadVirtualConnectorsPlaceHolder.mockResolvedValueOnce([
|
||||
loadConnectorFactoriesPlaceHolder.mockResolvedValueOnce([
|
||||
{
|
||||
...mockVirtualConnector,
|
||||
metadata: { ...mockVirtualConnector.metadata, id: 'connectorId' },
|
||||
...mockConnectorFactory,
|
||||
metadata: { ...mockConnectorFactory.metadata, id: 'connectorId' },
|
||||
},
|
||||
]);
|
||||
mockedCountConnectorByConnectorId.mockResolvedValueOnce({ count: 0 });
|
||||
|
@ -135,11 +135,11 @@ describe('connector route', () => {
|
|||
expect(response).toHaveProperty('statusCode', 200);
|
||||
});
|
||||
|
||||
it('throws when virtual connector not found', async () => {
|
||||
loadVirtualConnectorsPlaceHolder.mockResolvedValueOnce([
|
||||
it('throws when connector factory not found', async () => {
|
||||
loadConnectorFactoriesPlaceHolder.mockResolvedValueOnce([
|
||||
{
|
||||
...mockVirtualConnector,
|
||||
metadata: { ...mockVirtualConnector.metadata, id: 'connectorId' },
|
||||
...mockConnectorFactory,
|
||||
metadata: { ...mockConnectorFactory.metadata, id: 'connectorId' },
|
||||
},
|
||||
]);
|
||||
mockedCountConnectorByConnectorId.mockResolvedValueOnce({ count: 0 });
|
||||
|
@ -150,11 +150,11 @@ describe('connector route', () => {
|
|||
expect(response).toHaveProperty('statusCode', 422);
|
||||
});
|
||||
|
||||
it('should post a new record when add more than 1 instance with virtual connector', async () => {
|
||||
loadVirtualConnectorsPlaceHolder.mockResolvedValueOnce([
|
||||
it('should post a new record when add more than 1 instance with connector factory', async () => {
|
||||
loadConnectorFactoriesPlaceHolder.mockResolvedValueOnce([
|
||||
{
|
||||
...mockVirtualConnector,
|
||||
metadata: { ...mockVirtualConnector.metadata, id: 'id0', isStandard: true },
|
||||
...mockConnectorFactory,
|
||||
metadata: { ...mockConnectorFactory.metadata, id: 'id0', isStandard: true },
|
||||
},
|
||||
]);
|
||||
mockedCountConnectorByConnectorId.mockResolvedValueOnce({ count: 1 });
|
||||
|
@ -174,11 +174,11 @@ describe('connector route', () => {
|
|||
expect(response).toHaveProperty('statusCode', 200);
|
||||
});
|
||||
|
||||
it('throws when add more than 1 instance with non-virtual connector', async () => {
|
||||
loadVirtualConnectorsPlaceHolder.mockResolvedValueOnce([
|
||||
it('throws when add more than 1 instance with non-connector factory', async () => {
|
||||
loadConnectorFactoriesPlaceHolder.mockResolvedValueOnce([
|
||||
{
|
||||
...mockVirtualConnector,
|
||||
metadata: { ...mockVirtualConnector.metadata, id: 'id0' },
|
||||
...mockConnectorFactory,
|
||||
metadata: { ...mockConnectorFactory.metadata, id: 'id0' },
|
||||
},
|
||||
]);
|
||||
mockedCountConnectorByConnectorId.mockResolvedValueOnce({ count: 1 });
|
||||
|
|
|
@ -8,7 +8,7 @@ import { object, string } from 'zod';
|
|||
import {
|
||||
getLogtoConnectorById,
|
||||
getLogtoConnectors,
|
||||
loadVirtualConnectors,
|
||||
loadConnectorFactories,
|
||||
} from '#src/connectors/index.js';
|
||||
import type { LogtoConnector } from '#src/connectors/types.js';
|
||||
import RequestError from '#src/errors/RequestError/index.js';
|
||||
|
@ -101,10 +101,12 @@ export default function connectorRoutes<T extends AuthedRouter>(router: T) {
|
|||
body,
|
||||
} = ctx.guard;
|
||||
|
||||
const virtualConnectors = await loadVirtualConnectors();
|
||||
const virtualConnector = virtualConnectors.find(({ metadata: { id } }) => id === connectorId);
|
||||
const connectorFactories = await loadConnectorFactories();
|
||||
const connectorFactory = connectorFactories.find(
|
||||
({ metadata: { id } }) => id === connectorId
|
||||
);
|
||||
|
||||
if (!virtualConnector) {
|
||||
if (!connectorFactory) {
|
||||
throw new RequestError({
|
||||
code: 'connector.not_found_with_connector_id',
|
||||
status: 422,
|
||||
|
@ -113,7 +115,7 @@ export default function connectorRoutes<T extends AuthedRouter>(router: T) {
|
|||
|
||||
const { count } = await countConnectorByConnectorId(connectorId);
|
||||
assertThat(
|
||||
count === 0 || virtualConnector.metadata.isStandard === true,
|
||||
count === 0 || connectorFactory.metadata.isStandard === true,
|
||||
new RequestError({
|
||||
code: 'connector.multiple_instances_not_supported',
|
||||
status: 422,
|
||||
|
|
Loading…
Add table
Reference in a new issue