From 9dc0ea32c025374c1f4b823c7141a9330b7cf3b3 Mon Sep 17 00:00:00 2001 From: Darcy Ye <darcyye@silverhand.io> Date: Mon, 24 Jan 2022 14:40:15 +0800 Subject: [PATCH] feat(connector): connector queries and APIs (#178) * 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 --- .../core/src/connectors/aliyun-dm/index.ts | 7 +- packages/core/src/connectors/github/index.ts | 6 +- packages/core/src/connectors/index.ts | 33 ++++++-- packages/core/src/connectors/types.ts | 8 +- .../core/src/connectors/utilities/index.ts | 14 ++-- packages/core/src/queries/connector.ts | 14 +++- packages/core/src/routes/connector.ts | 84 +++++++++++++++++++ packages/core/src/routes/init.ts | 2 + packages/schemas/src/db-entries/connector.ts | 7 +- .../schemas/src/db-entries/custom-types.ts | 5 -- .../schemas/src/foundations/jsonb-types.ts | 2 +- packages/schemas/tables/connectors.sql | 5 +- 12 files changed, 142 insertions(+), 45 deletions(-) create mode 100644 packages/core/src/routes/connector.ts diff --git a/packages/core/src/connectors/aliyun-dm/index.ts b/packages/core/src/connectors/aliyun-dm/index.ts index 31793a181..b74e16822 100644 --- a/packages/core/src/connectors/aliyun-dm/index.ts +++ b/packages/core/src/connectors/aliyun-dm/index.ts @@ -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) { diff --git a/packages/core/src/connectors/github/index.ts b/packages/core/src/connectors/github/index.ts index fbd5a83d7..ab67fb50c 100644 --- a/packages/core/src/connectors/github/index.ts +++ b/packages/core/src/connectors/github/index.ts @@ -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, diff --git a/packages/core/src/connectors/index.ts b/packages/core/src/connectors/index.ts index 06340d80c..85ce1170a 100644 --- a/packages/core/src/connectors/index.ts +++ b/packages/core/src/connectors/index.ts @@ -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, }); }) ); diff --git a/packages/core/src/connectors/types.ts b/packages/core/src/connectors/types.ts index 8dd3e42d0..ee65308d3 100644 --- a/packages/core/src/connectors/types.ts +++ b/packages/core/src/connectors/types.ts @@ -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; } diff --git a/packages/core/src/connectors/utilities/index.ts b/packages/core/src/connectors/utilities/index.ts index 270b5550a..65d068412 100644 --- a/packages/core/src/connectors/utilities/index.ts +++ b/packages/core/src/connectors/utilities/index.ts @@ -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 }, }); }; diff --git a/packages/core/src/queries/connector.ts b/packages/core/src/queries/connector.ts index 211554beb..2ee8bfc5d 100644 --- a/packages/core/src/queries/connector.ts +++ b/packages/core/src/queries/connector.ts @@ -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, { diff --git a/packages/core/src/routes/connector.ts b/packages/core/src/routes/connector.ts new file mode 100644 index 000000000..127865730 --- /dev/null +++ b/packages/core/src/routes/connector.ts @@ -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(); + } + ); +} diff --git a/packages/core/src/routes/init.ts b/packages/core/src/routes/init.ts index 918d0c77f..f3b22cc5a 100644 --- a/packages/core/src/routes/init.ts +++ b/packages/core/src/routes/init.ts @@ -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]; diff --git a/packages/schemas/src/db-entries/connector.ts b/packages/schemas/src/db-entries/connector.ts index cadf6e525..8f12de9f7 100644 --- a/packages/schemas/src/db-entries/connector.ts +++ b/packages/schemas/src/db-entries/connector.ts @@ -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, }); diff --git a/packages/schemas/src/db-entries/custom-types.ts b/packages/schemas/src/db-entries/custom-types.ts index 95edfc3bb..6cba93da5 100644 --- a/packages/schemas/src/db-entries/custom-types.ts +++ b/packages/schemas/src/db-entries/custom-types.ts @@ -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', diff --git a/packages/schemas/src/foundations/jsonb-types.ts b/packages/schemas/src/foundations/jsonb-types.ts index 7f84b184f..c6e26c938 100644 --- a/packages/schemas/src/foundations/jsonb-types.ts +++ b/packages/schemas/src/foundations/jsonb-types.ts @@ -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>; diff --git a/packages/schemas/tables/connectors.sql b/packages/schemas/tables/connectors.sql index 8b76cee46..80d0374a6 100644 --- a/packages/schemas/tables/connectors.sql +++ b/packages/schemas/tables/connectors.sql @@ -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) );