0
Fork 0
mirror of https://github.com/logto-io/logto.git synced 2025-03-17 22:31:28 -05:00

test: add connector-mock-sms and connector-mock-email for integration tests (#1668)

This commit is contained in:
IceHe.life 2022-07-26 16:12:34 +08:00 committed by GitHub
parent fe97a657e2
commit f7bc349e03
No known key found for this signature in database
GPG key ID: 4AEE18F83AFDEB23
25 changed files with 574 additions and 31 deletions

View file

@ -7,7 +7,7 @@ on:
- 'push-action/**'
pull_request:
concurrency:
concurrency:
group: integration-test-${{ github.head_ref || github.run_id }}
cancel-in-progress: true
@ -23,20 +23,22 @@ jobs:
- name: Package
run: ./package.sh
env:
INTEGRATION_TEST: true
- uses: actions/upload-artifact@v3
with:
name: integration-test-${{ github.sha }}
path: /tmp/logto.tar.gz
retention-days: 3
run-logto:
needs: package
strategy:
matrix:
os: [ubuntu-latest, macos-12]
runs-on: ${{ matrix.os }}
steps:
@ -93,6 +95,7 @@ jobs:
INTEGRATION_TEST: true
NODE_ENV: production
DB_URL_DEFAULT: postgres://postgres:postgres@localhost:5432
ADDITIONAL_CONNECTOR_PACKAGES: '@logto/connector-mock-sms,@logto/connector-mock-email'
- name: Sleep for 5 seconds
run: sleep 5

View file

@ -7,7 +7,16 @@ echo Prune dependencies
rm -rf node_modules packages/*/node_modules
echo Install production dependencies
NODE_ENV=production pnpm i
if [[ $INTEGRATION_TEST =~ ^(true|1)$ ]]; then
echo Install the mock connectors for integration tests only
cd packages/core
pnpm link @logto/connector-mock-sms
pnpm link @logto/connector-mock-email
cd -
NODE_ENV=production pnpm i --no-frozen-lockfile
else
NODE_ENV=production pnpm i
fi
echo Prune files
rm -rf \

View file

@ -0,0 +1,3 @@
# Mock mail connector
For integration tests only.

View file

@ -0,0 +1,25 @@
{
"apiKey": "<api-key>",
"fromEmail": "<noreply@logto.test.io>",
"fromName": "<OPTIONAL-logto>",
"templates": [
{
"usageType": "SignIn",
"type": "text/plain",
"subject": "Logto SignIn Template",
"content": "This is for sign-in purposes only. Your passcode is {{code}}."
},
{
"usageType": "Register",
"type": "text/plain",
"subject": "Logto Register Template",
"content": "This is for registering purposes only. Your passcode is {{code}}."
},
{
"usageType": "Test",
"type": "text/plain",
"subject": "Logto Test Template",
"content": "This is for testing purposes only. Your passcode is {{code}}."
}
]
}

View file

@ -0,0 +1,7 @@
<svg width="24" height="24" viewBox="0 0 24 24" fill="none" xmlns="http://www.w3.org/2000/svg">
<path d="M15.3215 15.3384H8.66016V21.9997H15.3215V15.3384Z" fill="#99E1F4"/>
<path d="M8.66134 8.66138H2V15.3384H8.66134V8.66138Z" fill="#99E1F4"/>
<path d="M21.9992 8.66138H15.3379V15.3384H21.9992V8.66138Z" fill="#00B2E3"/>
<path d="M15.3215 2H8.66016V8.66135H15.3215V2Z" fill="#00B2E3"/>
<path d="M8.66016 8.66138V15.3384H15.3372V8.66138H8.66016Z" fill="#009DD9"/>
</svg>

After

Width:  |  Height:  |  Size: 470 B

View file

@ -0,0 +1,47 @@
{
"name": "@logto/connector-mock-email",
"version": "1.0.0-beta.1",
"description": "Mock Email Service connector implementation for integration tests only.",
"main": "./lib/index.js",
"exports": "./lib/index.js",
"author": "Silverhand Inc. <contact@silverhand.io>",
"license": "MPL-2.0",
"private": true,
"files": [
"lib",
"docs",
"logo.svg",
"README.md"
],
"scripts": {
"precommit": "lint-staged",
"build": "rm -rf lib/ && tsc -p tsconfig.build.json",
"lint": "eslint --ext .ts src",
"lint:report": "pnpm lint --format json --output-file report.json",
"dev": "rm -rf lib/ && tsc-watch -p tsconfig.build.json --preserveWatchOutput --onSuccess \"node ./lib/index.js\"",
"prepack": "pnpm build"
},
"dependencies": {
"@logto/connector-types": "^1.0.0-beta.1",
"@logto/shared": "^1.0.0-beta.1",
"@silverhand/essentials": "^1.1.6",
"zod": "^3.14.3"
},
"devDependencies": {
"@silverhand/eslint-config": "^0.17.0",
"@silverhand/ts-config": "^0.17.0",
"@types/node": "^16.3.1",
"eslint": "^8.19.0",
"lint-staged": "^13.0.0",
"prettier": "^2.3.2",
"tsc-watch": "^5.0.0",
"typescript": "^4.6.2"
},
"engines": {
"node": "^16.0.0"
},
"eslintConfig": {
"extends": "@silverhand"
},
"prettier": "@silverhand/eslint-config/.prettierrc"
}

View file

@ -0,0 +1,20 @@
import { ConnectorType, ConnectorMetadata } from '@logto/connector-types';
export const defaultMetadata: ConnectorMetadata = {
id: 'mock-email-service',
target: 'mock-mail',
type: ConnectorType.Email,
platform: null,
name: {
en: 'Mock Mail Service',
'zh-CN': 'Mock 邮件服务',
},
logo: './logo.svg',
logoDark: null,
description: {
en: 'The description of Mock SMS Service.',
'zh-CN': 'Mock 邮件服务的描述。',
},
readme: './README.md',
configTemplate: './docs/config-template.json',
};

View file

@ -0,0 +1,76 @@
import {
ConnectorError,
ConnectorErrorCodes,
ConnectorMetadata,
Connector,
EmailSendMessageFunction,
EmailSendTestMessageFunction,
EmailConnectorInstance,
GetConnectorConfig,
EmailMessageTypes,
} from '@logto/connector-types';
import { assert } from '@silverhand/essentials';
import { defaultMetadata } from './constant';
import { mockMailConfigGuard, MockMailConfig } from './types';
export default class MockMailConnector implements EmailConnectorInstance<MockMailConfig> {
public metadata: ConnectorMetadata = defaultMetadata;
private _connector?: Connector;
public get connector() {
if (!this._connector) {
throw new ConnectorError(ConnectorErrorCodes.General);
}
return this._connector;
}
public set connector(input: Connector) {
this._connector = input;
}
constructor(public readonly getConfig: GetConnectorConfig) {}
public validateConfig(config: unknown): asserts config is MockMailConfig {
const result = mockMailConfigGuard.safeParse(config);
if (!result.success) {
throw new ConnectorError(ConnectorErrorCodes.InvalidConfig, result.error);
}
}
public sendMessage: EmailSendMessageFunction = async (address, type, data) => {
const config = await this.getConfig(this.metadata.id);
this.validateConfig(config);
return this.sendMessageBy(config, address, type, data);
};
public sendTestMessage: EmailSendTestMessageFunction = async (config, address, type, data) => {
this.validateConfig(config);
return this.sendMessageBy(config, address, type, data);
};
private readonly sendMessageBy = async (
config: MockMailConfig,
address: string,
type: keyof EmailMessageTypes,
data: EmailMessageTypes[typeof type]
) => {
const { templates } = config;
const template = templates.find((template) => template.usageType === type);
assert(
template,
new ConnectorError(
ConnectorErrorCodes.TemplateNotFound,
`Template not found for type: ${type}`
)
);
return { address, data };
};
}

View file

@ -0,0 +1,22 @@
import { z } from 'zod';
export enum ContextType {
Text = 'text/plain',
Html = 'text/html',
}
const templateGuard = z.object({
usageType: z.string(),
type: z.nativeEnum(ContextType),
subject: z.string(),
content: z.string(), // With variable {{code}}, support HTML
});
export const mockMailConfigGuard = z.object({
apiKey: z.string(),
fromEmail: z.string(),
fromName: z.string().optional(),
templates: z.array(templateGuard),
});
export type MockMailConfig = z.infer<typeof mockMailConfigGuard>;

View file

@ -0,0 +1,10 @@
{
"extends": "@silverhand/ts-config/tsconfig.base",
"compilerOptions": {
"outDir": "lib",
"baseUrl": ".",
"paths": {
"@/*": ["src/*"]
}
}
}

View file

@ -0,0 +1,4 @@
{
"extends": "./tsconfig.base",
"include": ["src"]
}

View file

@ -0,0 +1,7 @@
{
"extends": "./tsconfig.base",
"compilerOptions": {
"types": ["node"]
},
"include": ["src"]
}

View file

@ -0,0 +1,3 @@
# Mock short message service connector
For integration tests only.

View file

@ -0,0 +1,19 @@
{
"accountSID": "<account-sid>",
"authToken": "<auth-token>",
"fromMessagingServiceSID": "<from-messaging-service-sid>",
"templates": [
{
"usageType": "SignIn",
"content": "This is for sign-in purposes only. Your passcode is {{code}}."
},
{
"usageType": "Register",
"content": "This is for registering purposes only. Your passcode is {{code}}."
},
{
"usageType": "Test",
"content": "This is for testing purposes only. Your passcode is {{code}}."
}
]
}

View file

@ -0,0 +1,3 @@
<svg width="24" height="24" viewBox="0 0 24 24" fill="none" xmlns="http://www.w3.org/2000/svg">
<path fill-rule="evenodd" clip-rule="evenodd" d="M11.9854 2C6.48023 2 2 6.48677 2 11.9999C2 17.5131 6.48023 21.9999 11.9854 21.9999C17.5198 22.0292 22 17.5424 22 11.9999C22 6.45745 17.5198 2 11.9854 2ZM11.9854 19.3899C7.94436 19.3899 4.63543 16.0761 4.63543 11.9999C4.63543 7.95304 7.94436 4.63928 11.9854 4.63928C16.0556 4.63928 19.3646 7.95304 19.3646 11.9999C19.3646 16.0761 16.0556 19.3899 11.9854 19.3899ZM14.4749 11.5894C15.6231 11.5894 16.554 10.6572 16.554 9.50731C16.554 8.3574 15.6231 7.42521 14.4749 7.42521C13.3267 7.42521 12.3958 8.3574 12.3958 9.50731C12.3958 10.6572 13.3267 11.5894 14.4749 11.5894ZM16.554 14.4926C16.554 15.6425 15.6231 16.5747 14.4749 16.5747C13.3267 16.5747 12.3958 15.6425 12.3958 14.4926C12.3958 13.3427 13.3267 12.4105 14.4749 12.4105C15.6231 12.4105 16.554 13.3427 16.554 14.4926ZM9.49703 16.5747C10.6453 16.5747 11.5761 15.6425 11.5761 14.4926C11.5761 13.3427 10.6453 12.4105 9.49703 12.4105C8.3488 12.4105 7.41797 13.3427 7.41797 14.4926C7.41797 15.6425 8.3488 16.5747 9.49703 16.5747ZM11.5761 9.50731C11.5761 10.6572 10.6453 11.5894 9.49703 11.5894C8.3488 11.5894 7.41797 10.6572 7.41797 9.50731C7.41797 8.3574 8.3488 7.42521 9.49703 7.42521C10.6453 7.42521 11.5761 8.3574 11.5761 9.50731Z" style="fill: rgb(70, 72, 148);"/>
</svg>

After

Width:  |  Height:  |  Size: 1.3 KiB

View file

@ -0,0 +1,47 @@
{
"name": "@logto/connector-mock-sms",
"version": "1.0.0-beta.1",
"description": "Mock SMS connector implementation for integration tests only.",
"main": "./lib/index.js",
"exports": "./lib/index.js",
"author": "Silverhand Inc. <contact@silverhand.io>",
"license": "MPL-2.0",
"private": true,
"files": [
"lib",
"docs",
"logo.svg",
"README.md"
],
"scripts": {
"precommit": "lint-staged",
"build": "rm -rf lib/ && tsc -p tsconfig.build.json",
"lint": "eslint --ext .ts src",
"lint:report": "pnpm lint --format json --output-file report.json",
"dev": "rm -rf lib/ && tsc-watch -p tsconfig.build.json --preserveWatchOutput --onSuccess \"node ./lib/index.js\"",
"prepack": "pnpm build"
},
"dependencies": {
"@logto/connector-types": "^1.0.0-beta.1",
"@logto/shared": "^1.0.0-beta.1",
"@silverhand/essentials": "^1.1.6",
"zod": "^3.14.3"
},
"devDependencies": {
"@silverhand/eslint-config": "^0.17.0",
"@silverhand/ts-config": "^0.17.0",
"@types/node": "^16.3.1",
"eslint": "^8.19.0",
"lint-staged": "^13.0.0",
"prettier": "^2.3.2",
"tsc-watch": "^5.0.0",
"typescript": "^4.6.2"
},
"engines": {
"node": "^16.0.0"
},
"eslintConfig": {
"extends": "@silverhand"
},
"prettier": "@silverhand/eslint-config/.prettierrc"
}

View file

@ -0,0 +1,20 @@
import { ConnectorType, ConnectorMetadata } from '@logto/connector-types';
export const defaultMetadata: ConnectorMetadata = {
id: 'mock-short-message-service',
target: 'mock-sms',
type: ConnectorType.SMS,
platform: null,
name: {
en: 'Mock SMS Service',
'zh-CN': 'Mock 短信服务',
},
logo: './logo.svg',
logoDark: null,
description: {
en: 'The description of Mock SMS Service.',
'zh-CN': 'Mock 短信服务的描述。',
},
readme: './README.md',
configTemplate: './docs/config-template.json',
};

View file

@ -0,0 +1,76 @@
import {
ConnectorError,
ConnectorErrorCodes,
ConnectorMetadata,
Connector,
SmsSendMessageFunction,
SmsSendTestMessageFunction,
SmsConnectorInstance,
GetConnectorConfig,
SmsMessageTypes,
} from '@logto/connector-types';
import { assert } from '@silverhand/essentials';
import { defaultMetadata } from './constant';
import { mockSmsConfigGuard, MockSmsConfig } from './types';
export default class MockSmsConnector implements SmsConnectorInstance<MockSmsConfig> {
public metadata: ConnectorMetadata = defaultMetadata;
private _connector?: Connector;
public get connector() {
if (!this._connector) {
throw new ConnectorError(ConnectorErrorCodes.General);
}
return this._connector;
}
public set connector(input: Connector) {
this._connector = input;
}
constructor(public readonly getConfig: GetConnectorConfig) {}
public validateConfig(config: unknown): asserts config is MockSmsConfig {
const result = mockSmsConfigGuard.safeParse(config);
if (!result.success) {
throw new ConnectorError(ConnectorErrorCodes.InvalidConfig, result.error);
}
}
public sendMessage: SmsSendMessageFunction = async (phone, type, data) => {
const config = await this.getConfig(this.metadata.id);
this.validateConfig(config);
return this.sendMessageBy(config, phone, type, data);
};
public sendTestMessage: SmsSendTestMessageFunction = async (config, phone, type, data) => {
this.validateConfig(config);
return this.sendMessageBy(config, phone, type, data);
};
private readonly sendMessageBy = async (
config: MockSmsConfig,
phone: string,
type: keyof SmsMessageTypes,
data: SmsMessageTypes[typeof type]
) => {
const { templates } = config;
const template = templates.find((template) => template.usageType === type);
assert(
template,
new ConnectorError(
ConnectorErrorCodes.TemplateNotFound,
`Template not found for type: ${type}`
)
);
return { phone, data };
};
}

View file

@ -0,0 +1,15 @@
import { z } from 'zod';
const templateGuard = z.object({
usageType: z.string(),
content: z.string(),
});
export const mockSmsConfigGuard = z.object({
accountSID: z.string(),
authToken: z.string(),
fromMessagingServiceSID: z.string(),
templates: z.array(templateGuard),
});
export type MockSmsConfig = z.infer<typeof mockSmsConfigGuard>;

View file

@ -0,0 +1,10 @@
{
"extends": "@silverhand/ts-config/tsconfig.base",
"compilerOptions": {
"outDir": "lib",
"baseUrl": ".",
"paths": {
"@/*": ["src/*"]
}
}
}

View file

@ -0,0 +1,4 @@
{
"extends": "./tsconfig.base",
"include": ["src"]
}

View file

@ -0,0 +1,7 @@
{
"extends": "./tsconfig.base",
"compilerOptions": {
"types": ["node"]
},
"include": ["src"]
}

View file

@ -98,3 +98,51 @@ export const sendgridEmailConnectorConfig = {
},
],
};
export const mockSmsConnectorId = 'mock-short-message-service';
export const mockSmsConnectorConfig = {
accountSID: 'account-sid-value',
authToken: 'auth-token-value',
fromMessagingServiceSID: 'from-messaging-service-sid-value',
templates: [
{
content: 'This is for sign-in purposes only. Your passcode is {{code}}.',
usageType: 'SignIn',
},
{
content: 'This is for registering purposes only. Your passcode is {{code}}.',
usageType: 'Register',
},
{
content: 'This is for testing purposes only. Your passcode is {{code}}.',
usageType: 'Test',
},
],
};
export const mockEmailConnectorId = 'mock-email-service';
export const mockEmailConnectorConfig = {
apiKey: 'api-key-value',
fromEmail: 'noreply@logto.test.io',
fromName: 'from-name-value',
templates: [
{
usageType: 'SignIn',
type: 'text/plain',
subject: 'Logto SignIn Template',
content: 'This is for sign-in purposes only. Your passcode is {{code}}.',
},
{
usageType: 'Register',
type: 'text/plain',
subject: 'Logto Register Template',
content: 'This is for registering purposes only. Your passcode is {{code}}.',
},
{
usageType: 'Test',
type: 'text/plain',
subject: 'Logto Test Template',
content: 'This is for testing purposes only. Your passcode is {{code}}.',
},
],
};

View file

@ -13,12 +13,12 @@ import {
facebookConnectorConfig,
aliyunSmsConnectorId,
aliyunSmsConnectorConfig,
twilioSmsConnectorId,
twilioSmsConnectorConfig,
aliyunEmailConnectorId,
aliyunEmailConnectorConfig,
sendgridEmailConnectorId,
sendgridEmailConnectorConfig,
mockSmsConnectorId,
mockSmsConnectorConfig,
mockEmailConnectorId,
mockEmailConnectorConfig,
} from '@/connectors-mock';
test('connector flow', async () => {
@ -62,13 +62,13 @@ test('connector flow', async () => {
/*
* Change to another SMS connector
*/
const updatedTwilioSmsConnector = await updateConnectorConfig(
twilioSmsConnectorId,
twilioSmsConnectorConfig
const updatedMockSmsConnector = await updateConnectorConfig(
mockSmsConnectorId,
mockSmsConnectorConfig
);
expect(updatedTwilioSmsConnector.config).toEqual(twilioSmsConnectorConfig);
const enabledTwilioSmsConnector = await enableConnector(twilioSmsConnectorId);
expect(enabledTwilioSmsConnector.enabled).toBeTruthy();
expect(updatedMockSmsConnector.config).toEqual(mockSmsConnectorConfig);
const enabledMockSmsConnector = await enableConnector(mockSmsConnectorId);
expect(enabledMockSmsConnector.enabled).toBeTruthy();
// There should be exactly one enabled SMS connector after changing to another SMS connector.
const connectorsAfterChangingSmsConnector = await listConnectors();
@ -76,7 +76,7 @@ test('connector flow', async () => {
(connector) => connector.type === ConnectorType.SMS && connector.enabled
);
expect(enabledSmsConnectors.length).toEqual(1);
expect(enabledSmsConnectors[0]?.id).toEqual(twilioSmsConnectorId);
expect(enabledSmsConnectors[0]?.id).toEqual(mockSmsConnectorId);
/*
* Set up an email connector
@ -92,13 +92,13 @@ test('connector flow', async () => {
/*
* Change to another email connector
*/
const updatedSendgridEmailConnector = await updateConnectorConfig(
sendgridEmailConnectorId,
sendgridEmailConnectorConfig
const updatedMockEmailConnector = await updateConnectorConfig(
mockEmailConnectorId,
mockEmailConnectorConfig
);
expect(updatedSendgridEmailConnector.config).toEqual(sendgridEmailConnectorConfig);
const enabledSendgridEmailConnector = await enableConnector(sendgridEmailConnectorId);
expect(enabledSendgridEmailConnector.enabled).toBeTruthy();
expect(updatedMockEmailConnector.config).toEqual(mockEmailConnectorConfig);
const enabledMockEmailConnector = await enableConnector(mockEmailConnectorId);
expect(enabledMockEmailConnector.enabled).toBeTruthy();
// There should be exactly one enabled email connector after changing to another email connector.
const connectorsAfterChangingEmailConnector = await listConnectors();
@ -106,14 +106,14 @@ test('connector flow', async () => {
(connector) => connector.type === ConnectorType.Email && connector.enabled
);
expect(enabledEmailConnector.length).toEqual(1);
expect(enabledEmailConnector[0]?.id).toEqual(sendgridEmailConnectorId);
expect(enabledEmailConnector[0]?.id).toEqual(mockEmailConnectorId);
/*
* It should update the connector config successfully when it is valid; otherwise, it should fail.
* We will test updating to the invalid connector config, that is the case not covered above.
*/
await expect(
updateConnectorConfig(aliyunEmailConnectorId, sendgridEmailConnectorConfig)
updateConnectorConfig(aliyunEmailConnectorId, mockEmailConnectorConfig)
).rejects.toThrow(HTTPError);
// To confirm the failed updating request above did not modify the original config,
// we check: the Aliyun email connector config should stay the same.
@ -126,10 +126,10 @@ test('connector flow', async () => {
* We have not provided the API to delete a connector for now.
* Deleting a connector using Admin Console means disabling a connector using Management API.
*/
const disabledSendgridEmailConnector = await disableConnector(sendgridEmailConnectorId);
expect(disabledSendgridEmailConnector.enabled).toBeFalsy();
const sendgridEmailConnector = await getConnector(sendgridEmailConnectorId);
expect(sendgridEmailConnector.enabled).toBeFalsy();
const disabledMockEmailConnector = await disableConnector(mockEmailConnectorId);
expect(disabledMockEmailConnector.enabled).toBeFalsy();
const mockEmailConnector = await getConnector(mockEmailConnectorId);
expect(mockEmailConnector.enabled).toBeFalsy();
/**
* List connectors after manually setting up connectors.
@ -148,8 +148,8 @@ test('connector flow', async () => {
enabled: false,
}),
expect.objectContaining({
id: twilioSmsConnectorId,
config: twilioSmsConnectorConfig,
id: mockSmsConnectorId,
config: mockSmsConnectorConfig,
enabled: true,
}),
expect.objectContaining({
@ -158,8 +158,8 @@ test('connector flow', async () => {
enabled: false,
}),
expect.objectContaining({
id: sendgridEmailConnectorId,
config: sendgridEmailConnectorConfig,
id: mockEmailConnectorId,
config: mockEmailConnectorConfig,
enabled: false,
}),
])

58
pnpm-lock.yaml generated
View file

@ -412,6 +412,64 @@ importers:
tsc-watch: 5.0.3_typescript@4.6.3
typescript: 4.6.3
packages/connector-mock-mail:
specifiers:
'@logto/connector-types': ^1.0.0-beta.1
'@logto/shared': ^1.0.0-beta.1
'@silverhand/eslint-config': ^0.17.0
'@silverhand/essentials': ^1.1.6
'@silverhand/ts-config': ^0.17.0
'@types/node': ^16.3.1
eslint: ^8.19.0
lint-staged: ^13.0.0
prettier: ^2.3.2
tsc-watch: ^5.0.0
typescript: ^4.6.2
zod: ^3.14.3
dependencies:
'@logto/connector-types': link:../connector-types
'@logto/shared': link:../shared
'@silverhand/essentials': 1.1.7
zod: 3.14.3
devDependencies:
'@silverhand/eslint-config': 0.17.0_zheml67kdyttrjblcc5k6h6tmu
'@silverhand/ts-config': 0.17.0_typescript@4.7.2
'@types/node': 16.11.12
eslint: 8.19.0
lint-staged: 13.0.0
prettier: 2.5.1
tsc-watch: 5.0.3_typescript@4.7.2
typescript: 4.7.2
packages/connector-mock-sms:
specifiers:
'@logto/connector-types': ^1.0.0-beta.1
'@logto/shared': ^1.0.0-beta.1
'@silverhand/eslint-config': ^0.17.0
'@silverhand/essentials': ^1.1.6
'@silverhand/ts-config': ^0.17.0
'@types/node': ^16.3.1
eslint: ^8.19.0
lint-staged: ^13.0.0
prettier: ^2.3.2
tsc-watch: ^5.0.0
typescript: ^4.6.2
zod: ^3.14.3
dependencies:
'@logto/connector-types': link:../connector-types
'@logto/shared': link:../shared
'@silverhand/essentials': 1.1.7
zod: 3.14.3
devDependencies:
'@silverhand/eslint-config': 0.17.0_zheml67kdyttrjblcc5k6h6tmu
'@silverhand/ts-config': 0.17.0_typescript@4.7.2
'@types/node': 16.11.12
eslint: 8.19.0
lint-staged: 13.0.0
prettier: 2.5.1
tsc-watch: 5.0.3_typescript@4.7.2
typescript: 4.7.2
packages/connector-sendgrid-mail:
specifiers:
'@jest/types': ^27.5.1