0
Fork 0
mirror of https://github.com/logto-io/logto.git synced 2024-12-30 20:33:54 -05:00

refactor(connector): fix app boot

This commit is contained in:
Darcy Ye 2022-08-18 15:56:46 +08:00
parent 05ae6e532e
commit b076ba4bc9
No known key found for this signature in database
GPG key ID: B46F4C07EDEFC610
24 changed files with 177 additions and 75 deletions

View file

@ -14,6 +14,8 @@ import { assert } from '@silverhand/essentials';
import { defaultMetadata } from './constant';
import { mockMailConfigGuard, MockMailConfig } from './types';
export { defaultMetadata } from './constant';
export default class MockMailConnector extends EmailConnector<MockMailConfig> {
constructor(getConnectorConfig: GetConnectorConfig) {
super(getConnectorConfig);

View file

@ -14,6 +14,8 @@ import { assert } from '@silverhand/essentials';
import { defaultMetadata } from './constant';
import { mockSmsConfigGuard, MockSmsConfig } from './types';
export { defaultMetadata } from './constant';
export default class MockSmsConnector extends SmsConnector<MockSmsConfig> {
constructor(getConnectorConfig: GetConnectorConfig) {
super(getConnectorConfig);

View file

@ -14,6 +14,8 @@ import { z } from 'zod';
import { defaultMetadata } from './constant';
import { mockSocialConfigGuard, MockSocialConfig } from './types';
export { defaultMetadata } from './constant';
export default class MockSocialConnector extends SocialConnector<MockSocialConfig> {
constructor(getConnectorConfig: GetConnectorConfig) {
super(getConnectorConfig);

View file

@ -19,6 +19,8 @@ import {
PublicParameters,
} from './types';
export { defaultMetadata } from './constant';
export default class SendGridMailConnector extends EmailConnector<SendGridMailConfig> {
constructor(getConnectorConfig: GetConnectorConfig) {
super(getConnectorConfig);

View file

@ -13,6 +13,8 @@ import SMTPTransport from 'nodemailer/lib/smtp-transport';
import { defaultMetadata } from './constant';
import { ContextType, smtpConfigGuard, SmtpConfig } from './types';
export { defaultMetadata } from './constant';
export default class SmtpConnector extends EmailConnector<SmtpConfig> {
constructor(getConnectorConfig: GetConnectorConfig) {
super(getConnectorConfig);

View file

@ -12,6 +12,8 @@ import got, { HTTPError } from 'got';
import { defaultMetadata, endpoint } from './constant';
import { twilioSmsConfigGuard, TwilioSmsConfig, PublicParameters } from './types';
export { defaultMetadata } from './constant';
export default class TwilioSmsConnector extends SmsConnector<TwilioSmsConfig> {
constructor(getConnectorConfig: GetConnectorConfig) {
super(getConnectorConfig);

View file

@ -36,6 +36,8 @@ import {
AuthResponse,
} from './types';
export { defaultMetadata } from './constant';
export default class WechatNativeConnector extends SocialConnector<WechatNativeConfig> {
constructor(getConnectorConfig: GetConnectorConfig) {
super(getConnectorConfig);

View file

@ -37,6 +37,8 @@ import {
AuthResponse,
} from './types';
export { defaultMetadata } from './constant';
export default class WechatConnector extends SocialConnector<WechatConfig> {
constructor(getConnectorConfig: GetConnectorConfig) {
super(getConnectorConfig);

View file

@ -28,10 +28,10 @@
"@logto/connector-facebook": "^1.0.0-beta.5",
"@logto/connector-github": "^1.0.0-beta.5",
"@logto/connector-google": "^1.0.0-beta.5",
"@logto/connector-schemas": "^1.0.0-beta.5",
"@logto/connector-sendgrid-email": "^1.0.0-beta.5",
"@logto/connector-smtp": "^1.0.0-beta.5",
"@logto/connector-twilio-sms": "^1.0.0-beta.5",
"@logto/connector-types": "^1.0.0-beta.5",
"@logto/connector-wechat-native": "^1.0.0-beta.5",
"@logto/connector-wechat-web": "^1.0.0-beta.5",
"@logto/phrases": "^1.0.0-beta.5",

View file

@ -1,4 +1,4 @@
import { ConnectorPlatform } from '@logto/connector-types';
import { ConnectorPlatform } from '@logto/connector-schemas';
import { Connector, ConnectorMetadata, ConnectorType } from '@logto/schemas';
export const mockMetadata: ConnectorMetadata = {

View file

@ -1,4 +1,4 @@
import { ConnectorPlatform } from '@logto/connector-types';
import { ConnectorPlatform } from '@logto/connector-schemas';
import { Connector } from '@logto/schemas';
import {

View file

@ -1,7 +1,8 @@
import { existsSync, readFileSync } from 'fs';
import path from 'path';
import { ConnectorInstance, SocialConnectorInstance } from '@logto/connector-types';
import { GetConnectorConfig } from '@logto/connector-schemas';
import { Connector } from '@logto/schemas/lib/db-entries';
import resolvePackagePath from 'resolve-package-path';
import envSet from '@/env-set';
@ -9,17 +10,30 @@ import RequestError from '@/errors/RequestError';
import { findAllConnectors, insertConnector } from '@/queries/connector';
import { defaultConnectorPackages } from './consts';
import { ConnectorType } from './types';
import { ConnectorInstance, SocialConnectorInstance, ConnectorType } from './types';
import { getConnectorConfig } from './utilities';
// eslint-disable-next-line @silverhand/fp/no-let
let cachedConnectors: ConnectorInstance[] | undefined;
let cachedConnectorInstances: ConnectorInstance[] | undefined;
const loadConnectors = async () => {
if (cachedConnectors) {
return cachedConnectors;
export const getConnectorInstances = async (): Promise<ConnectorInstance[]> => {
const connectors = await findAllConnectors();
const connectorMap = new Map(connectors.map((connector) => [connector.id, connector]));
if (cachedConnectorInstances) {
return cachedConnectorInstances.map((connectorInstance) => {
const { id } = connectorInstance.metadata;
const connector = connectorMap.get(id);
if (!connector) {
throw new RequestError({ code: 'entity.not_found', id, status: 404 });
}
// eslint-disable-next-line @silverhand/fp/no-mutation
connectorInstance.connector = connector;
return connectorInstance;
});
}
const {
values: { additionalConnectorPackages },
} = envSet;
@ -27,12 +41,30 @@ const loadConnectors = async () => {
const connectorPackages = [...defaultConnectorPackages, ...additionalConnectorPackages];
// eslint-disable-next-line @silverhand/fp/no-mutation
cachedConnectors = await Promise.all(
cachedConnectorInstances = await Promise.all(
connectorPackages.map(async (packageName) => {
// eslint-disable-next-line @typescript-eslint/no-unsafe-assignment
const { default: Builder } = await import(packageName);
// eslint-disable-next-line @typescript-eslint/no-unsafe-call, @typescript-eslint/no-unsafe-assignment
const instance: ConnectorInstance = new Builder(getConnectorConfig);
const { default: Builder, defaultMetadata: metadata } = await import(packageName);
class InstanceBuilder extends Builder {
public connector: Connector;
constructor(getConnectorConfig: GetConnectorConfig, connector: Connector) {
// eslint-disable-next-line @typescript-eslint/no-unsafe-call
super(getConnectorConfig);
this.connector = connector;
}
}
const connector = connectorMap.get(metadata.id);
if (!connector) {
throw new RequestError({ code: 'entity.not_found', id: metadata.id, status: 404 });
}
// eslint-disable-next-line no-restricted-syntax
const instance: ConnectorInstance = new InstanceBuilder(
getConnectorConfig,
connector
) as ConnectorInstance;
// eslint-disable-next-line unicorn/prefer-module
const packagePath = resolvePackagePath(packageName, __dirname);
@ -86,29 +118,29 @@ const loadConnectors = async () => {
})
);
return cachedConnectors;
return cachedConnectorInstances;
};
export const getConnectorInstances = async (): Promise<ConnectorInstance[]> => {
const connectors = await findAllConnectors();
const connectorMap = new Map(connectors.map((connector) => [connector.id, connector]));
// Export const getConnectorInstances = async (): Promise<ConnectorInstance[]> => {
// const connectors = await findAllConnectors();
// const connectorMap = new Map(connectors.map((connector) => [connector.id, connector]));
const allConnectors = await loadConnectors();
// const allConnectors = await loadConnectors();
return allConnectors.map((element) => {
const { id } = element.metadata;
const connector = connectorMap.get(id);
// return allConnectors.map((element) => {
// const { id } = element.metadata;
// const connector = connectorMap.get(id);
if (!connector) {
throw new RequestError({ code: 'entity.not_found', id, status: 404 });
}
// if (!connector) {
// throw new RequestError({ code: 'entity.not_found', id, status: 404 });
// }
// eslint-disable-next-line @silverhand/fp/no-mutation
element.connector = connector;
// // eslint-disable-next-line @silverhand/fp/no-mutation
// element.connector = connector;
return element;
});
};
// return element;
// });
// };
export const getConnectorInstanceById = async (id: string): Promise<ConnectorInstance> => {
const connectorInstances = await getConnectorInstances();
@ -150,8 +182,8 @@ export const getSocialConnectorInstanceById = async (
export const initConnectors = async () => {
const connectors = await findAllConnectors();
const existingConnectors = new Map(connectors.map((connector) => [connector.id, connector]));
const allConnectors = await loadConnectors();
const newConnectors = allConnectors.filter(({ metadata: { id } }) => {
const allConnectorInstances = await getConnectorInstances();
const newConnectors = allConnectorInstances.filter(({ metadata: { id } }) => {
const connector = existingConnectors.get(id);
if (!connector) {

View file

@ -1,8 +1,17 @@
import { PasscodeType } from '@logto/schemas';
import {
BaseConnector,
SmsConnector,
EmailConnector,
SocialConnector,
ConnectorType,
ConnectorError,
ConnectorErrorCodes,
} from '@logto/connector-schemas';
import { Connector, PasscodeType } from '@logto/schemas';
import { z } from 'zod';
export { ConnectorType } from '@logto/schemas';
export type { ConnectorMetadata } from '@logto/schemas';
export { ConnectorType } from '@logto/connector-schemas';
export type { ConnectorMetadata } from '@logto/connector-schemas';
export type TemplateType = PasscodeType | 'Test';
@ -15,3 +24,43 @@ export const socialUserInfoGuard = z.object({
});
export type SocialUserInfo = z.infer<typeof socialUserInfoGuard>;
export class SmsConnectorInstance<T = unknown> extends SmsConnector<T> {
public connector!: Connector;
}
export class EmailConnectorInstance<T = unknown> extends EmailConnector<T> {
public connector!: Connector;
}
export class SocialConnectorInstance<T = unknown> extends SocialConnector<T> {
public connector!: Connector;
}
export class ConnectorInstance<T = unknown> extends BaseConnector<T> {
public connector!: Connector;
}
export const isSmsConnectorInstance = (
instance: unknown
): asserts instance is SmsConnectorInstance => {
if (!(instance instanceof ConnectorInstance && instance.metadata.type === ConnectorType.SMS)) {
throw new ConnectorError(ConnectorErrorCodes.General);
}
};
export const isEmailConnectorInstance = (
instance: unknown
): asserts instance is EmailConnectorInstance => {
if (!(instance instanceof ConnectorInstance && instance.metadata.type === ConnectorType.Email)) {
throw new ConnectorError(ConnectorErrorCodes.General);
}
};
export const isSocialConnectorInstance = (
instance: unknown
): asserts instance is SocialConnectorInstance => {
if (!(instance instanceof ConnectorInstance && instance.metadata.type === ConnectorType.Social)) {
throw new ConnectorError(ConnectorErrorCodes.General);
}
};

View file

@ -1,8 +1,8 @@
import { ConnectorType } from '@logto/connector-types';
import { Passcode, PasscodeType } from '@logto/schemas';
import { ConnectorMetadata, ConnectorType, ValidateConfig } from '@logto/connector-schemas';
import { Connector, Passcode, PasscodeType } from '@logto/schemas';
import { mockConnector, mockMetadata } from '@/__mocks__';
import { getConnectorInstances } from '@/connectors';
// Import { getConnectorInstances } from '@/connectors';
import RequestError from '@/errors/RequestError';
import {
consumePasscode,
@ -22,8 +22,22 @@ import {
verifyPasscode,
} from './passcode';
type ConnectorInstance = {
connector: Connector;
metadata: ConnectorMetadata;
validateConfig?: ValidateConfig<unknown>;
sendMessage?: unknown;
};
const getConnectorInstancesPlaceHolder = jest.fn() as jest.MockedFunction<
() => Promise<ConnectorInstance[]>
>;
jest.mock('@/queries/passcode');
jest.mock('@/connectors');
jest.mock('@/connectors', () => ({
...jest.requireActual('@/queries/connector'),
getConnectorInstances: async () => getConnectorInstancesPlaceHolder(),
}));
const mockedFindUnconsumedPasscodesByJtiAndType =
findUnconsumedPasscodesByJtiAndType as jest.MockedFunction<
@ -37,9 +51,9 @@ const mockedDeletePasscodesByIds = deletePasscodesByIds as jest.MockedFunction<
typeof deletePasscodesByIds
>;
const mockedInsertPasscode = insertPasscode as jest.MockedFunction<typeof insertPasscode>;
const mockedGetConnectorInstances = getConnectorInstances as jest.MockedFunction<
typeof getConnectorInstances
>;
// Const mockedGetConnectorInstances = getConnectorInstances as jest.MockedFunction<
// typeof getConnectorInstances
// >;
const mockedConsumePasscode = consumePasscode as jest.MockedFunction<typeof consumePasscode>;
const mockedIncreasePasscodeTryCount = increasePasscodeTryCount as jest.MockedFunction<
typeof increasePasscodeTryCount
@ -126,8 +140,7 @@ describe('sendPasscode', () => {
it('should throw error when email or sms connector can not be found', async () => {
const sendMessage = jest.fn();
const validateConfig = jest.fn();
const getConfig = jest.fn();
mockedGetConnectorInstances.mockResolvedValueOnce([
getConnectorInstancesPlaceHolder.mockResolvedValueOnce([
{
connector: {
...mockConnector,
@ -140,7 +153,6 @@ describe('sendPasscode', () => {
},
sendMessage,
validateConfig,
getConfig,
},
]);
const passcode: Passcode = {
@ -165,8 +177,7 @@ describe('sendPasscode', () => {
it('should call sendPasscode with params matching', async () => {
const sendMessage = jest.fn();
const validateConfig = jest.fn();
const getConfig = jest.fn();
mockedGetConnectorInstances.mockResolvedValueOnce([
getConnectorInstancesPlaceHolder.mockResolvedValueOnce([
{
connector: {
...mockConnector,
@ -179,7 +190,6 @@ describe('sendPasscode', () => {
},
sendMessage,
validateConfig,
getConfig,
},
{
connector: {
@ -193,7 +203,6 @@ describe('sendPasscode', () => {
},
sendMessage,
validateConfig,
getConfig,
},
]);
const passcode: Passcode = {

View file

@ -1,9 +1,8 @@
import { EmailConnectorInstance, SmsConnectorInstance } from '@logto/connector-types';
import { Passcode, PasscodeType } from '@logto/schemas';
import { customAlphabet, nanoid } from 'nanoid';
import { getConnectorInstances } from '@/connectors';
import { ConnectorType } from '@/connectors/types';
import { ConnectorType, EmailConnectorInstance, SmsConnectorInstance } from '@/connectors/types';
import RequestError from '@/errors/RequestError';
import {
consumePasscode,

View file

@ -1,4 +1,3 @@
import { ConnectorInstance } from '@logto/connector-types';
import { BrandingStyle, SignInMethodState, ConnectorType } from '@logto/schemas';
import {
@ -8,6 +7,7 @@ import {
mockBranding,
mockSignInMethods,
} from '@/__mocks__';
import { ConnectorInstance } from '@/connectors/types';
import RequestError from '@/errors/RequestError';
import {
isEnabled,

View file

@ -1,4 +1,3 @@
import { ConnectorInstance } from '@logto/connector-types';
import {
Branding,
BrandingStyle,
@ -8,7 +7,7 @@ import {
} from '@logto/schemas';
import { Optional } from '@silverhand/essentials';
import { ConnectorType } from '@/connectors/types';
import { ConnectorInstance, ConnectorType } from '@/connectors/types';
import RequestError from '@/errors/RequestError';
import assertThat from '@/utils/assert-that';

View file

@ -1,4 +1,4 @@
import { ConnectorError, ConnectorErrorCodes } from '@logto/connector-types';
import { ConnectorError, ConnectorErrorCodes } from '@logto/connector-schemas';
import RequestError from '@/errors/RequestError';
import { createContextWithRouteParameters } from '@/utils/test-utils';

View file

@ -1,4 +1,4 @@
import { ConnectorError, ConnectorErrorCodes } from '@logto/connector-types';
import { ConnectorError, ConnectorErrorCodes } from '@logto/connector-schemas';
import { conditional } from '@silverhand/essentials';
import { Middleware } from 'koa';
import { z } from 'zod';

View file

@ -1,8 +1,4 @@
import {
ValidateConfig,
EmailConnectorInstance,
SmsConnectorInstance,
} from '@logto/connector-types';
import { ValidateConfig } from '@logto/connector-schemas';
import { Connector, ConnectorType } from '@logto/schemas';
import { mockConnectorInstanceList, mockMetadata, mockConnector } from '@/__mocks__';
@ -16,7 +12,7 @@ import connectorRoutes from './connector';
type ConnectorInstance = {
connector: Connector;
metadata: ConnectorMetadata;
validateConfig?: ValidateConfig;
validateConfig?: ValidateConfig<unknown>;
sendMessage?: unknown;
};
@ -111,12 +107,13 @@ describe('connector route', () => {
...mockMetadata,
type: ConnectorType.SMS,
};
const mockedSmsConnectorInstance: SmsConnectorInstance = {
const mockedSmsConnectorInstance = {
connector: mockConnector,
metadata: mockedMetadata,
validateConfig: jest.fn(),
getConfig: jest.fn(),
sendMessage: jest.fn(),
sendMessageBy: jest.fn(),
sendTestMessage: jest.fn(),
};
getConnectorInstancesPlaceHolder.mockResolvedValueOnce([mockedSmsConnectorInstance]);
@ -132,12 +129,13 @@ describe('connector route', () => {
});
it('should get email connector and send test message', async () => {
const mockedEmailConnector: EmailConnectorInstance = {
const mockedEmailConnector = {
connector: mockConnector,
metadata: mockMetadata,
validateConfig: jest.fn(),
getConfig: jest.fn(),
sendMessage: jest.fn(),
sendMessageBy: jest.fn(),
sendTestMessage: jest.fn(),
};
getConnectorInstancesPlaceHolder.mockResolvedValueOnce([mockedEmailConnector]);

View file

@ -1,13 +1,13 @@
import {
ConnectorInstance,
EmailConnectorInstance,
SmsConnectorInstance,
} from '@logto/connector-types';
import { arbitraryObjectGuard, ConnectorDto, Connectors, ConnectorType } from '@logto/schemas';
import { emailRegEx, phoneRegEx } from '@logto/shared';
import { object, string } from 'zod';
import { getConnectorInstances, getConnectorInstanceById } from '@/connectors';
import {
ConnectorInstance,
EmailConnectorInstance,
SmsConnectorInstance,
} from '@/connectors/types';
import RequestError from '@/errors/RequestError';
import koaGuard from '@/middleware/koa-guard';
import { updateConnector } from '@/queries/connector';

View file

@ -1,4 +1,4 @@
import { ConnectorError, ConnectorErrorCodes, ValidateConfig } from '@logto/connector-types';
import { ConnectorError, ConnectorErrorCodes, ValidateConfig } from '@logto/connector-schemas';
import { Connector, ConnectorType } from '@logto/schemas';
import { mockConnectorInstanceList, mockMetadata, mockConnector } from '@/__mocks__';
@ -13,7 +13,7 @@ import connectorRoutes from './connector';
type ConnectorInstance = {
connector: Connector;
metadata: ConnectorMetadata;
validateConfig?: ValidateConfig;
validateConfig?: ValidateConfig<unknown>;
sendMessage?: unknown;
};
@ -38,7 +38,7 @@ const getConnectorInstanceByIdPlaceHolder = jest.fn(async (connectorId: string)
sendMessage: sendMessagePlaceHolder,
};
});
const validateConfigPlaceHolder = jest.fn() as jest.MockedFunction<ValidateConfig>;
const validateConfigPlaceHolder = jest.fn() as jest.MockedFunction<ValidateConfig<unknown>>;
const sendMessagePlaceHolder = jest.fn();
jest.mock('@/queries/connector', () => ({

View file

@ -1,4 +1,4 @@
import { ConnectorMetadata } from '@logto/connector-types';
import { ConnectorMetadata } from '@logto/connector-schemas';
import { SignInMode } from '@logto/schemas';
import {
adminConsoleApplicationId,

View file

@ -936,10 +936,10 @@ importers:
'@logto/connector-facebook': ^1.0.0-beta.5
'@logto/connector-github': ^1.0.0-beta.5
'@logto/connector-google': ^1.0.0-beta.5
'@logto/connector-schemas': ^1.0.0-beta.5
'@logto/connector-sendgrid-email': ^1.0.0-beta.5
'@logto/connector-smtp': ^1.0.0-beta.5
'@logto/connector-twilio-sms': ^1.0.0-beta.5
'@logto/connector-types': ^1.0.0-beta.5
'@logto/connector-wechat-native': ^1.0.0-beta.5
'@logto/connector-wechat-web': ^1.0.0-beta.5
'@logto/phrases': ^1.0.0-beta.5
@ -1021,10 +1021,10 @@ importers:
'@logto/connector-facebook': link:../connector-facebook
'@logto/connector-github': link:../connector-github
'@logto/connector-google': link:../connector-google
'@logto/connector-schemas': link:../connector-schemas
'@logto/connector-sendgrid-email': link:../connector-sendgrid-mail
'@logto/connector-smtp': link:../connector-smtp
'@logto/connector-twilio-sms': link:../connector-twilio-sms
'@logto/connector-types': link:../connector-types
'@logto/connector-wechat-native': link:../connector-wechat-native
'@logto/connector-wechat-web': link:../connector-wechat-web
'@logto/phrases': link:../phrases