diff --git a/packages/core/src/connectors/types.ts b/packages/core/src/connectors/types.ts index dd3144178..83df65523 100644 --- a/packages/core/src/connectors/types.ts +++ b/packages/core/src/connectors/types.ts @@ -1,5 +1,5 @@ import { Languages } from '@logto/phrases'; -import { ConnectorConfig, Connector, PasscodeType } from '@logto/schemas'; +import { ArbitraryObject, Connector, PasscodeType } from '@logto/schemas'; import { z } from 'zod'; export enum ConnectorType { @@ -92,7 +92,7 @@ export class ConnectorError extends Error { } } -export type ValidateConfig = ( +export type ValidateConfig = ( config: T ) => Promise; diff --git a/packages/core/src/connectors/utilities/index.ts b/packages/core/src/connectors/utilities/index.ts index 758ce8cc4..5088c7e46 100644 --- a/packages/core/src/connectors/utilities/index.ts +++ b/packages/core/src/connectors/utilities/index.ts @@ -1,9 +1,9 @@ -import { ConnectorConfig } from '@logto/schemas'; +import { ArbitraryObject } from '@logto/schemas'; import RequestError from '@/errors/RequestError'; import { findConnectorById, updateConnector } from '@/queries/connector'; -export const getConnectorConfig = async (id: string): Promise => { +export const getConnectorConfig = async (id: string): Promise => { const connector = await findConnectorById(id); // FIXME: @@ -20,7 +20,7 @@ export const getConnectorConfig = async (id: string): return connector.config as T; }; -export const updateConnectorConfig = async ( +export const updateConnectorConfig = async ( id: string, config: T ): Promise => { diff --git a/packages/core/src/routes/admin-user.test.ts b/packages/core/src/routes/admin-user.test.ts index 2a16c4eb4..105c7ef0a 100644 --- a/packages/core/src/routes/admin-user.test.ts +++ b/packages/core/src/routes/admin-user.test.ts @@ -308,11 +308,27 @@ describe('adminUserRoutes', () => { } }); await expect( - userRequest.patch(`/users/${notExistedUserId}/roleNames`).send({ roleNames: ['admin'] }) + userRequest.patch(`/users/${notExistedUserId}/custom-data`).send({ customData: { level: 1 } }) ).resolves.toHaveProperty('status', 500); expect(updateUserById).not.toHaveBeenCalled(); }); + it('PATCH /users/:userId/custom-data should throw if customData is not an object', async () => { + await expect( + userRequest.patch(`/users/foo/custom-data`).send({ customData: 123_456 }) + ).resolves.toHaveProperty('status', 400); + + await expect( + userRequest.patch(`/users/foo/custom-data`).send({ customData: ['customDataContent'] }) + ).resolves.toHaveProperty('status', 400); + + await expect( + userRequest.patch(`/users/foo/custom-data`).send({ customData: 'customDataContent' }) + ).resolves.toHaveProperty('status', 400); + + expect(updateUserById).not.toHaveBeenCalled(); + }); + it('DELETE /users/:userId/custom-data', async () => { const response = await userRequest.delete('/users/foo/custom-data'); expect(findUserById).toHaveBeenCalledTimes(1); diff --git a/packages/core/src/routes/admin-user.ts b/packages/core/src/routes/admin-user.ts index 5e26a6d7a..0405b2416 100644 --- a/packages/core/src/routes/admin-user.ts +++ b/packages/core/src/routes/admin-user.ts @@ -1,4 +1,4 @@ -import { customDataGuard, userInfoSelectFields } from '@logto/schemas'; +import { arbitraryObjectGuard, userInfoSelectFields } from '@logto/schemas'; import pick from 'lodash.pick'; import { InvalidInputError } from 'slonik'; import { object, string } from 'zod'; @@ -218,7 +218,7 @@ export default function adminUserRoutes(router: T) { '/users/:userId/custom-data', koaGuard({ params: object({ userId: string() }), - body: object({ customData: customDataGuard }), + body: object({ customData: arbitraryObjectGuard }), }), async (ctx, next) => { const { diff --git a/packages/schemas/src/db-entries/connector.ts b/packages/schemas/src/db-entries/connector.ts index 8f12de9f7..d853732c9 100644 --- a/packages/schemas/src/db-entries/connector.ts +++ b/packages/schemas/src/db-entries/connector.ts @@ -2,26 +2,26 @@ import { z } from 'zod'; -import { ConnectorConfig, connectorConfigGuard, GeneratedSchema, Guard } from '../foundations'; +import { ArbitraryObject, arbitraryObjectGuard, GeneratedSchema, Guard } from '../foundations'; export type CreateConnector = { id: string; enabled?: boolean; - config?: ConnectorConfig; + config?: ArbitraryObject; createdAt?: number; }; export type Connector = { id: string; enabled: boolean; - config: ConnectorConfig; + config: ArbitraryObject; createdAt: number; }; const createGuard: Guard = z.object({ id: z.string(), enabled: z.boolean().optional(), - config: connectorConfigGuard.optional(), + config: arbitraryObjectGuard.optional(), createdAt: z.number().optional(), }); diff --git a/packages/schemas/src/db-entries/user.ts b/packages/schemas/src/db-entries/user.ts index 8ab9a7a30..1db8a7b98 100644 --- a/packages/schemas/src/db-entries/user.ts +++ b/packages/schemas/src/db-entries/user.ts @@ -7,8 +7,8 @@ import { roleNamesGuard, Identities, identitiesGuard, - CustomData, - customDataGuard, + ArbitraryObject, + arbitraryObjectGuard, GeneratedSchema, Guard, } from '../foundations'; @@ -26,7 +26,7 @@ export type CreateUser = { avatar?: string | null; roleNames?: RoleNames; identities?: Identities; - customData?: CustomData; + customData?: ArbitraryObject; }; export type User = { @@ -41,7 +41,7 @@ export type User = { avatar: string | null; roleNames: RoleNames; identities: Identities; - customData: CustomData; + customData: ArbitraryObject; }; const createGuard: Guard = z.object({ @@ -56,7 +56,7 @@ const createGuard: Guard = z.object({ avatar: z.string().nullable().optional(), roleNames: roleNamesGuard.optional(), identities: identitiesGuard.optional(), - customData: customDataGuard.optional(), + customData: arbitraryObjectGuard.optional(), }); export const Users: GeneratedSchema = Object.freeze({ diff --git a/packages/schemas/src/foundations/jsonb-types.ts b/packages/schemas/src/foundations/jsonb-types.ts index b4dc946a7..3247c2ea3 100644 --- a/packages/schemas/src/foundations/jsonb-types.ts +++ b/packages/schemas/src/foundations/jsonb-types.ts @@ -54,11 +54,6 @@ export const identitiesGuard = z.record(identityGuard); export type Identity = z.infer; export type Identities = z.infer; -// TODO: LOG-1553 support empty shape of object -export const customDataGuard = z.object({}).catchall(z.unknown()); - -export type CustomData = z.infer; - /** * User Logs */ @@ -73,15 +68,6 @@ export const userLogPayloadGuard = z.object({ export type UserLogPayload = z.infer; -/** - * Connectors - */ - -// TODO: support empty shape of object -export const connectorConfigGuard = z.object({}).catchall(z.unknown()); - -export type ConnectorConfig = z.infer; - /** * Settings */ @@ -148,3 +134,11 @@ export const signInMethodSettingsGuard = z.object({ }); export type SignInMethodSettings = z.infer; + +/** + * Commonly Used + */ + +export const arbitraryObjectGuard = z.object({}).catchall(z.unknown()); + +export type ArbitraryObject = z.infer; diff --git a/packages/schemas/tables/connectors.sql b/packages/schemas/tables/connectors.sql index 80d0374a6..cc54e6ffc 100644 --- a/packages/schemas/tables/connectors.sql +++ b/packages/schemas/tables/connectors.sql @@ -1,7 +1,7 @@ create table connectors ( id varchar(128) not null, enabled boolean not null default TRUE, - config jsonb /* @use ConnectorConfig */ not null default '{}'::jsonb, + config jsonb /* @use ArbitraryObject */ not null default '{}'::jsonb, created_at timestamptz not null default(now()), primary key (id) ); diff --git a/packages/schemas/tables/users.sql b/packages/schemas/tables/users.sql index 949f70f87..23600fc6e 100644 --- a/packages/schemas/tables/users.sql +++ b/packages/schemas/tables/users.sql @@ -12,6 +12,6 @@ create table users ( avatar varchar(256), role_names jsonb /* @use RoleNames */ not null default '[]'::jsonb, identities jsonb /* @use Identities */ not null default '{}'::jsonb, - custom_data jsonb /* @use CustomData */ not null default '{}'::jsonb, + custom_data jsonb /* @use ArbitraryObject */ not null default '{}'::jsonb, primary key (id) );