diff --git a/packages/connectors/connector-mock-email-alternative/src/index.ts b/packages/connectors/connector-mock-email-alternative/src/index.ts index 03f97d142..07f52cd40 100644 --- a/packages/connectors/connector-mock-email-alternative/src/index.ts +++ b/packages/connectors/connector-mock-email-alternative/src/index.ts @@ -26,7 +26,7 @@ const sendMessage = const config = inputConfig ?? (await getConfig(defaultMetadata.id)); validateConfig(config, mockMailConfigGuard); const { templates } = config; - const template = templates.find((template) => template.usageType === type); + const template = templates?.find((template) => template.usageType === type); assert( template, diff --git a/packages/connectors/connector-mock-email-alternative/src/types.ts b/packages/connectors/connector-mock-email-alternative/src/types.ts index c72ac0d4c..b4401c7fa 100644 --- a/packages/connectors/connector-mock-email-alternative/src/types.ts +++ b/packages/connectors/connector-mock-email-alternative/src/types.ts @@ -12,11 +12,13 @@ const templateGuard = z.object({ 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 const mockMailConfigGuard = z + .object({ + apiKey: z.string(), + fromEmail: z.string(), + fromName: z.string().optional(), + templates: z.array(templateGuard), + }) + .partial(); export type MockMailConfig = z.infer; diff --git a/packages/core/src/database/update-where.test.ts b/packages/core/src/database/update-where.test.ts index 0943fe456..4cfbdb379 100644 --- a/packages/core/src/database/update-where.test.ts +++ b/packages/core/src/database/update-where.test.ts @@ -69,12 +69,20 @@ describe('buildUpdateWhere()', () => { ).resolves.toStrictEqual({ id: 'foo', customClientMetadata: '{"idTokenTtl":3600}' }); }); - it('throws an error when `undefined` found in values', async () => { + it('should skip the keys whose value is `undefined`', async () => { + const user: CreateUser = { + id: 'foo', + username: '456', + }; const pool = createTestPool( - 'update "users"\nset "username"=$1\nwhere "id"=$2 and "username"=$3' + 'update "users"\nset "username"=$1\nwhere "id"=$2 and "username"=$3\nreturning *', + (_, [username, id]) => ({ + id: String(id), + username: String(username), + }) ); - const updateWhere = buildUpdateWhereWithPool(pool)(Users); + const updateWhere = buildUpdateWhereWithPool(pool)(Users, true); await expect( updateWhere({ @@ -82,7 +90,7 @@ describe('buildUpdateWhere()', () => { where: { id: 'foo', username: '456' }, jsonbMode: 'merge', }) - ).rejects.toMatchError(new Error(`Cannot convert id to primitive`)); + ).resolves.toStrictEqual({ ...user, username: '123' }); }); it('throws `entity.not_exists_with_id` error with `undefined` when `returning` is true', async () => { diff --git a/packages/core/src/database/update-where.ts b/packages/core/src/database/update-where.ts index 6d824a1dc..8f3ddfc0a 100644 --- a/packages/core/src/database/update-where.ts +++ b/packages/core/src/database/update-where.ts @@ -32,7 +32,7 @@ export const buildUpdateWhereWithPool = const connectKeyValueWithEqualSign = (data: Partial, jsonbMode: 'replace' | 'merge') => Object.entries(data) .map(([key, value]) => { - if (!isKeyOfSchema(key)) { + if (!isKeyOfSchema(key) || value === undefined) { return; } diff --git a/packages/core/src/routes/connector/index.patch.test.ts b/packages/core/src/routes/connector/index.patch.test.ts index abdd39ccc..5f202bcc9 100644 --- a/packages/core/src/routes/connector/index.patch.test.ts +++ b/packages/core/src/routes/connector/index.patch.test.ts @@ -179,6 +179,34 @@ describe('connector data routes', () => { ); }); + it('successfully reset connector config', async () => { + getLogtoConnectors.mockResolvedValue([ + { + dbEntry: mockConnector, + metadata: { ...mockMetadata, isStandard: true }, + type: ConnectorType.Social, + ...mockLogtoConnector, + }, + ]); + updateConnector.mockResolvedValueOnce({ + ...mockConnector, + config: {}, + }); + const response = await connectorRequest.patch('/connectors/id').send({ + config: {}, + }); + expect(response).toHaveProperty('statusCode', 200); + expect(updateConnector).toHaveBeenCalledWith( + expect.objectContaining({ + where: { id: 'id' }, + set: { + config: {}, + }, + jsonbMode: 'replace', + }) + ); + }); + it('successfully updates connector config and metadata', async () => { getLogtoConnectors.mockResolvedValue([ { @@ -235,9 +263,6 @@ describe('connector data routes', () => { ...mockConnector, metadata: { target: 'connector', - name: { en: '' }, - logo: '', - logoDark: '', }, }); const response = await connectorRequest.patch('/connectors/id').send({ diff --git a/packages/core/src/routes/connector/index.ts b/packages/core/src/routes/connector/index.ts index 5c846b130..a219e6cc3 100644 --- a/packages/core/src/routes/connector/index.ts +++ b/packages/core/src/routes/connector/index.ts @@ -5,8 +5,10 @@ import { Connectors, ConnectorType, connectorResponseGuard, + type JsonObject, } from '@logto/schemas'; import { buildIdGenerator } from '@logto/shared'; +import { conditional } from '@silverhand/essentials'; import cleanDeep from 'clean-deep'; import { string, object } from 'zod'; @@ -308,7 +310,17 @@ export default function connectorRoutes( } await updateConnector({ - set: cleanDeep({ config, metadata, syncProfile }), + set: { + /** + * `JsonObject` has all non-undefined values, and `cleanDeep` method with default settings + * drops all keys with undefined values, the return type of `Partial` is still `JsonObject`. + * The type inference failed to infer this, manually assign type `JsonObject`. + */ + // eslint-disable-next-line no-restricted-syntax + config: conditional(config && (cleanDeep(config) as JsonObject)), + metadata: conditional(metadata && cleanDeep(metadata)), + syncProfile, + }, where: { id }, jsonbMode: 'replace', }); diff --git a/packages/integration-tests/src/tests/api/connector.test.ts b/packages/integration-tests/src/tests/api/connector.test.ts index b790183ac..e20863320 100644 --- a/packages/integration-tests/src/tests/api/connector.test.ts +++ b/packages/integration-tests/src/tests/api/connector.test.ts @@ -115,6 +115,10 @@ test('connector set-up flow', async () => { currentConnectors.find((connector) => connector.connectorId === mockAlternativeEmailConnectorId) ?.config ).toEqual(mockAlternativeEmailConnectorConfig); + // Can reset the connector config to empty object `{}` (when it is valid). + await expect(updateConnectorConfig(id, { config: {} })).resolves.not.toThrow(); + const alternativeEmailConnector = await getConnector(id); + expect(alternativeEmailConnector.config).toEqual({}); /* * Delete (i.e. disable) a connector