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

feat(connector): connector queries and APIs ()

* feat(connector): connector queries and APIs

* chore(connectors): remove type from DB schema design and fix code accordingly

* chore(connectors): put connector as ConnectorInstance's property

* chore(connector): put connector as optional property of ConnectorInstance
This commit is contained in:
Darcy Ye 2022-01-24 14:40:15 +08:00 committed by GitHub
parent b9aec921c2
commit 9dc0ea32c0
No known key found for this signature in database
GPG key ID: 4AEE18F83AFDEB23
12 changed files with 142 additions and 45 deletions
packages
core/src
schemas

View file

@ -1,4 +1,3 @@
import { ConnectorType } from '@logto/schemas';
import { z } from 'zod';
import {
@ -7,6 +6,7 @@ import {
ConnectorMetadata,
EmailSendMessageFunction,
ValidateConfig,
ConnectorType,
} from '../types';
import { getConnectorConfig } from '../utilities';
import { singleSendMail } from './single-send-mail';
@ -54,10 +54,7 @@ const configGuard = z.object({
export type AliyunDmConfig = z.infer<typeof configGuard>;
export const sendMessage: EmailSendMessageFunction = async (address, type, data) => {
const config: AliyunDmConfig = await getConnectorConfig<AliyunDmConfig>(
metadata.id,
metadata.type
);
const config = await getConnectorConfig<AliyunDmConfig>(metadata.id);
const template = config.templates.find((template) => template.type === type);
if (!template) {

View file

@ -1,4 +1,3 @@
import { ConnectorType } from '@logto/schemas';
import got from 'got';
import { stringify } from 'query-string';
import { z } from 'zod';
@ -10,6 +9,7 @@ import {
GetAuthorizationUri,
ValidateConfig,
GetUserInfo,
ConnectorType,
} from '../types';
import { getConnectorConfig } from '../utilities';
import { authorizationEndpoint, accessTokenEndpoint, scope, userInfoEndpoint } from './constant';
@ -47,7 +47,7 @@ export const validateConfig: ValidateConfig = async (config: unknown) => {
};
export const getAuthorizationUri: GetAuthorizationUri = async (redirectUri, state) => {
const config = await getConnectorConfig<GithubConfig>(metadata.id, metadata.type);
const config = await getConnectorConfig<GithubConfig>(metadata.id);
return `${authorizationEndpoint}?${stringify({
client_id: config.clientId,
redirect_uri: redirectUri,
@ -64,7 +64,7 @@ export const getAccessToken: GetAccessToken = async (code) => {
};
const { clientId: client_id, clientSecret: client_secret } =
await getConnectorConfig<GithubConfig>(metadata.id, metadata.type);
await getConnectorConfig<GithubConfig>(metadata.id);
const { access_token: accessToken } = await got
.post({
url: accessTokenEndpoint,

View file

@ -1,25 +1,44 @@
import { findConnectorByIdAndType, insertConnector } from '@/queries/connector';
import RequestError from '@/errors/RequestError';
import { findConnectorById, insertConnector } from '@/queries/connector';
import * as AliyunDM from './aliyun-dm';
import { ConnectorInstance } from './types';
const connectors: ConnectorInstance[] = [AliyunDM];
const allConnectors: ConnectorInstance[] = [AliyunDM];
export const getConnectorById = (id: string): ConnectorInstance | null => {
return connectors.find((connector) => connector.metadata.id === id) ?? null;
export const getConnectorInstances = async (): Promise<ConnectorInstance[]> => {
return Promise.all(
allConnectors.map(async (element) => {
const connector = await findConnectorById(element.metadata.id);
return { connector, ...element };
})
);
};
export const getConnectorInstanceById = async (id: string): Promise<ConnectorInstance> => {
const found = allConnectors.find((element) => element.metadata.id === id);
if (!found) {
throw new RequestError({
code: 'entity.not_found',
id,
status: 404,
});
}
const connector = await findConnectorById(id);
return { connector, ...found };
};
export const initConnectors = async () => {
await Promise.all(
connectors.map(async ({ metadata: { id, type } }) => {
const record = await findConnectorByIdAndType(id, type);
allConnectors.map(async ({ metadata: { id } }) => {
const record = await findConnectorById(id);
if (record) {
return;
}
await insertConnector({
id,
type,
});
})
);

View file

@ -1,6 +1,11 @@
import { Languages } from '@logto/phrases';
import { ConnectorConfig, ConnectorType } from '@logto/schemas';
import { ConnectorConfig, Connector } from '@logto/schemas';
export enum ConnectorType {
SMS = 'SMS',
Email = 'Email',
Social = 'Social',
}
export interface ConnectorMetadata {
id: string;
type: ConnectorType;
@ -13,6 +18,7 @@ export interface ConnectorMetadata {
export type ConnectorInstance = EmailConector | SocialConector;
export interface BaseConnector {
connector?: Connector;
metadata: ConnectorMetadata;
validateConfig: ValidateConfig;
}

View file

@ -1,13 +1,10 @@
import { ConnectorConfig, ConnectorType } from '@logto/schemas';
import { ConnectorConfig } from '@logto/schemas';
import RequestError from '@/errors/RequestError';
import { findConnectorByIdAndType, updateConnector } from '@/queries/connector';
import { findConnectorById, updateConnector } from '@/queries/connector';
export const getConnectorConfig = async <T extends ConnectorConfig>(
id: string,
type: ConnectorType
): Promise<T> => {
const connector = await findConnectorByIdAndType(id, type);
export const getConnectorConfig = async <T extends ConnectorConfig>(id: string): Promise<T> => {
const connector = await findConnectorById(id);
if (!connector) {
throw new RequestError({
code: 'entity.not_exists_with_id',
@ -22,11 +19,10 @@ export const getConnectorConfig = async <T extends ConnectorConfig>(
export const updateConnectorConfig = async <T extends ConnectorConfig>(
id: string,
type: ConnectorType,
config: T
): Promise<void> => {
await updateConnector({
where: { id, type },
where: { id },
set: { config },
});
};

View file

@ -1,4 +1,4 @@
import { Connector, CreateConnector, Connectors, ConnectorType } from '@logto/schemas';
import { Connector, CreateConnector, Connectors } from '@logto/schemas';
import { sql } from 'slonik';
import { buildInsertInto } from '@/database/insert-into';
@ -8,11 +8,17 @@ import { convertToIdentifiers } from '@/database/utils';
const { table, fields } = convertToIdentifiers(Connectors);
export const findConnectorByIdAndType = async (id: string, type: ConnectorType) =>
pool.maybeOne<Connector>(sql`
export const findAllConnectors = async () =>
pool.many<Connector>(sql`
select ${sql.join(Object.values(fields), sql`, `)}
from ${table}
where ${fields.id}=${id} and ${fields.type}=${type}
`);
export const findConnectorById = async (id: string) =>
pool.one<Connector>(sql`
select ${sql.join(Object.values(fields), sql`, `)}
from ${table}
where ${fields.id}=${id}
`);
export const insertConnector = buildInsertInto<CreateConnector, Connector>(pool, Connectors, {

View file

@ -0,0 +1,84 @@
import { Connectors } from '@logto/schemas';
import { object, string } from 'zod';
import { getConnectorInstances, getConnectorInstanceById } from '@/connectors';
import { ConnectorInstance } from '@/connectors/types';
import koaGuard from '@/middleware/koa-guard';
import { findConnectorById, updateConnector } from '@/queries/connector';
import { AuthedRouter } from './types';
const transpileConnectorInstance = ({ connector, metadata }: ConnectorInstance) => ({
...connector,
metadata,
});
export default function connectorRoutes<T extends AuthedRouter>(router: T) {
router.get('/connectors', async (ctx, next) => {
const connectorInstances = await getConnectorInstances();
ctx.body = connectorInstances.map((connectorInstance) => {
return transpileConnectorInstance(connectorInstance);
});
return next();
});
router.get(
'/connectors/:id',
koaGuard({ params: object({ id: string().min(1) }) }),
async (ctx, next) => {
const {
params: { id },
} = ctx.guard;
const connectorInstance = await getConnectorInstanceById(id);
ctx.body = transpileConnectorInstance(connectorInstance);
return next();
}
);
router.patch(
'/connectors/:id/enabled',
koaGuard({
params: object({ id: string().min(1) }),
body: Connectors.createGuard.pick({ enabled: true }),
}),
async (ctx, next) => {
const {
params: { id },
body: { enabled },
} = ctx.guard;
await findConnectorById(id);
await updateConnector({ set: { enabled }, where: { id } });
ctx.body = { enabled };
return next();
}
);
router.patch(
'/connectors/:id',
koaGuard({
params: object({ id: string().min(1) }),
body: Connectors.createGuard
.omit({ id: true, type: true, enabled: true, createdAt: true })
.partial(),
}),
async (ctx, next) => {
const {
params: { id },
body,
} = ctx.guard;
const connectorInstance = await getConnectorInstanceById(id);
if (body.config) {
await connectorInstance.validateConfig(body.config);
}
await updateConnector({ set: body, where: { id } });
ctx.body = transpileConnectorInstance(await getConnectorInstanceById(id));
return next();
}
);
}

View file

@ -5,6 +5,7 @@ import { Provider } from 'oidc-provider';
import koaAuth from '@/middleware/koa-auth';
import applicationRoutes from '@/routes/application';
import connectorRoutes from '@/routes/connector';
import resourceRoutes from '@/routes/resource';
import sessionRoutes from '@/routes/session';
import settingRoutes from '@/routes/setting';
@ -26,6 +27,7 @@ const createRouters = (provider: Provider) => {
router.use(koaAuth());
applicationRoutes(router);
settingRoutes(router);
connectorRoutes(router);
resourceRoutes(router);
return [anonymousRouter, router];

View file

@ -3,12 +3,10 @@
import { z } from 'zod';
import { ConnectorConfig, connectorConfigGuard, GeneratedSchema, Guard } from '../foundations';
import { ConnectorType } from './custom-types';
export type CreateConnector = {
id: string;
enabled?: boolean;
type: ConnectorType;
config?: ConnectorConfig;
createdAt?: number;
};
@ -16,7 +14,6 @@ export type CreateConnector = {
export type Connector = {
id: string;
enabled: boolean;
type: ConnectorType;
config: ConnectorConfig;
createdAt: number;
};
@ -24,7 +21,6 @@ export type Connector = {
const createGuard: Guard<CreateConnector> = z.object({
id: z.string(),
enabled: z.boolean().optional(),
type: z.nativeEnum(ConnectorType),
config: connectorConfigGuard.optional(),
createdAt: z.number().optional(),
});
@ -35,10 +31,9 @@ export const Connectors: GeneratedSchema<CreateConnector> = Object.freeze({
fields: {
id: 'id',
enabled: 'enabled',
type: 'type',
config: 'config',
createdAt: 'created_at',
},
fieldKeys: ['id', 'enabled', 'type', 'config', 'createdAt'],
fieldKeys: ['id', 'enabled', 'config', 'createdAt'],
createGuard,
});

View file

@ -5,11 +5,6 @@ export enum ApplicationType {
SPA = 'SPA',
Traditional = 'Traditional',
}
export enum ConnectorType {
SMS = 'SMS',
Email = 'Email',
Social = 'Social',
}
export enum UserLogType {
SignInUsernameAndPassword = 'SignInUsernameAndPassword',
ExchangeAccessToken = 'ExchangeAccessToken',

View file

@ -45,7 +45,7 @@ export const userLogPayloadGuard = z.object({
export type UserLogPayload = z.infer<typeof userLogPayloadGuard>;
// TODO: support empty shape of object
export const connectorConfigGuard = z.object({});
export const connectorConfigGuard = z.object({}).catchall(z.unknown());
export type ConnectorConfig = z.infer<typeof connectorConfigGuard>;

View file

@ -1,10 +1,7 @@
create type connector_type as enum ('SMS', 'Email', 'Social');
create table connectors (
id varchar(128) not null,
enabled boolean not null default TRUE,
type connector_type not null,
config jsonb /* @use ConnectorConfig */ not null default '{}'::jsonb,
created_at timestamptz not null default(now()),
primary key (id, type)
primary key (id)
);