0
Fork 0
mirror of https://github.com/logto-io/logto.git synced 2024-12-30 20:33:54 -05:00

feat(schemas): add custom data to application (#6309)

* feat(core,schemas): add application custom data

add application custom data

* test(core): add update application with new custom data test

add update application with new custom data test
This commit is contained in:
simeng-li 2024-07-25 17:55:57 +08:00 committed by GitHub
parent f8f34f0e87
commit 6477c6deef
No known key found for this signature in database
GPG key ID: B5690EEEBB952194
10 changed files with 176 additions and 26 deletions

View file

@ -0,0 +1,12 @@
---
"@logto/schemas": minor
"@logto/core": minor
---
add `custom_data` to applications
Introduce a new property `custom_data` to the `Application` schema. This property is an arbitrary object that can be used to store custom data for an application.
Added a new API to update the custom data of an application:
- `PATCH /applications/:applicationId/custom-data`

View file

@ -4,32 +4,32 @@ import type {
Application, Application,
ApplicationsRole, ApplicationsRole,
LogtoConfig, LogtoConfig,
Passcode,
OidcConfigKey, OidcConfigKey,
Passcode,
Resource, Resource,
Role, Role,
Scope, Scope,
UsersRole, UsersRole,
} from '@logto/schemas'; } from '@logto/schemas';
import { import {
RoleType,
ApplicationType, ApplicationType,
LogtoOidcConfigKey,
DomainStatus, DomainStatus,
LogtoJwtTokenKey,
internalPrefix, internalPrefix,
LogtoJwtTokenKey,
LogtoOidcConfigKey,
RoleType,
} from '@logto/schemas'; } from '@logto/schemas';
import { protectedAppSignInCallbackUrl } from '#src/constants/index.js'; import { protectedAppSignInCallbackUrl } from '#src/constants/index.js';
import { mockId } from '#src/test-utils/nanoid.js'; import { mockId } from '#src/test-utils/nanoid.js';
export * from './connector.js';
export * from './sign-in-experience.js';
export * from './cloud-connection.js'; export * from './cloud-connection.js';
export * from './user.js'; export * from './connector.js';
export * from './domain.js'; export * from './domain.js';
export * from './sso.js';
export * from './protected-app.js'; export * from './protected-app.js';
export * from './sign-in-experience.js';
export * from './sso.js';
export * from './user.js';
export const mockApplication: Application = { export const mockApplication: Application = {
tenantId: 'fake_tenant', tenantId: 'fake_tenant',
@ -50,6 +50,7 @@ export const mockApplication: Application = {
protectedAppMetadata: null, protectedAppMetadata: null,
isThirdParty: false, isThirdParty: false,
createdAt: 1_645_334_775_356, createdAt: 1_645_334_775_356,
customData: {},
}; };
export const mockProtectedApplication: Omit<Application, 'protectedAppMetadata'> & { export const mockProtectedApplication: Omit<Application, 'protectedAppMetadata'> & {
@ -78,6 +79,7 @@ export const mockProtectedApplication: Omit<Application, 'protectedAppMetadata'>
}, },
isThirdParty: false, isThirdParty: false,
createdAt: 1_645_334_775_356, createdAt: 1_645_334_775_356,
customData: {},
}; };
export const mockCustomDomain = { export const mockCustomDomain = {

View file

@ -0,0 +1,24 @@
{
"paths": {
"/api/applications/{applicationId}/custom-data": {
"patch": {
"summary": "Update application custom data",
"description": "Update the custom data of an application.",
"requestBody": {
"content": {
"application/json": {
"schema": {
"description": "An arbitrary JSON object."
}
}
}
},
"responses": {
"200": {
"description": "The updated custom data in JSON."
}
}
}
}
}
}

View file

@ -0,0 +1,33 @@
import { jsonObjectGuard } from '@logto/connector-kit';
import { z } from 'zod';
import koaGuard from '#src/middleware/koa-guard.js';
import { type ManagementApiRouter, type RouterInitArgs } from '../types.js';
export default function applicationCustomDataRoutes<T extends ManagementApiRouter>(
...[router, { queries }]: RouterInitArgs<T>
) {
router.patch(
'/applications/:applicationId/custom-data',
koaGuard({
params: z.object({ applicationId: z.string() }),
body: jsonObjectGuard,
response: jsonObjectGuard,
}),
async (ctx, next) => {
const { applicationId } = ctx.guard.params;
const patchPayload = ctx.guard.body;
const { customData } = await queries.applications.findApplicationById(applicationId);
const application = await queries.applications.updateApplicationById(applicationId, {
customData: { ...customData, ...patchPayload },
});
ctx.body = application.customData;
return next();
}
);
}

View file

@ -1,11 +1,11 @@
import type { Role } from '@logto/schemas'; import type { Role } from '@logto/schemas';
import { import {
demoAppApplicationId,
buildDemoAppDataForTenant,
InternalRole,
ApplicationType,
Applications, Applications,
ApplicationType,
buildDemoAppDataForTenant,
demoAppApplicationId,
hasSecrets, hasSecrets,
InternalRole,
} from '@logto/schemas'; } from '@logto/schemas';
import { generateStandardId, generateStandardSecret } from '@logto/shared'; import { generateStandardId, generateStandardSecret } from '@logto/shared';
import { conditional } from '@silverhand/essentials'; import { conditional } from '@silverhand/essentials';
@ -21,6 +21,7 @@ import { parseSearchParamsForSearch } from '#src/utils/search.js';
import type { ManagementApiRouter, RouterInitArgs } from '../types.js'; import type { ManagementApiRouter, RouterInitArgs } from '../types.js';
import applicationCustomDataRoutes from './application-custom-data.js';
import { generateInternalSecret } from './application-secret.js'; import { generateInternalSecret } from './application-secret.js';
import { applicationCreateGuard, applicationPatchGuard } from './types.js'; import { applicationCreateGuard, applicationPatchGuard } from './types.js';
@ -38,15 +39,14 @@ const parseIsThirdPartQueryParam = (isThirdPartyQuery: 'true' | 'false' | undefi
const applicationTypeGuard = z.nativeEnum(ApplicationType); const applicationTypeGuard = z.nativeEnum(ApplicationType);
export default function applicationRoutes<T extends ManagementApiRouter>( export default function applicationRoutes<T extends ManagementApiRouter>(
...[ ...[router, tenant]: RouterInitArgs<T>
router,
{
queries,
id: tenantId,
libraries: { quota, protectedApps },
},
]: RouterInitArgs<T>
) { ) {
const {
queries,
id: tenantId,
libraries: { quota, protectedApps },
} = tenant;
router.get( router.get(
'/applications', '/applications',
koaPagination({ isOptional: true }), koaPagination({ isOptional: true }),
@ -346,4 +346,6 @@ export default function applicationRoutes<T extends ManagementApiRouter>(
return next(); return next();
} }
); );
applicationCustomDataRoutes(router, tenant);
} }

View file

@ -1,13 +1,13 @@
import { import {
ApplicationType, ApplicationType,
type Application, type Application,
type CreateApplication,
type OidcClientMetadata,
type Role,
type ProtectedAppMetadata,
type OrganizationWithRoles,
type CreateApplicationSecret,
type ApplicationSecret, type ApplicationSecret,
type CreateApplication,
type CreateApplicationSecret,
type OidcClientMetadata,
type OrganizationWithRoles,
type ProtectedAppMetadata,
type Role,
} from '@logto/schemas'; } from '@logto/schemas';
import { conditional } from '@silverhand/essentials'; import { conditional } from '@silverhand/essentials';
@ -150,3 +150,14 @@ export const deleteApplicationSecret = async (applicationId: string, secretName:
export const deleteLegacyApplicationSecret = async (applicationId: string) => export const deleteLegacyApplicationSecret = async (applicationId: string) =>
authedAdminApi.delete(`applications/${applicationId}/legacy-secret`); authedAdminApi.delete(`applications/${applicationId}/legacy-secret`);
export const patchApplicationCustomData = async (
applicationId: string,
customData: Record<string, unknown>
) => {
return authedAdminApi
.patch(`applications/${applicationId}/custom-data`, {
json: customData,
})
.json<Record<string, unknown>>();
};

View file

@ -0,0 +1,46 @@
import { ApplicationType } from '@logto/schemas';
import {
createApplication,
deleteApplication,
getApplication,
patchApplicationCustomData,
updateApplication,
} from '#src/api/application.js';
describe('application custom data API', () => {
it('should patch application custom data successfully', async () => {
const applicationName = 'test-create-app';
const applicationType = ApplicationType.SPA;
const application = await createApplication(applicationName, applicationType);
const customData = { key: 'value' };
const result = await patchApplicationCustomData(application.id, customData);
expect(result.key).toEqual(customData.key);
const fetchedApplication = await getApplication(application.id);
expect(fetchedApplication.customData.key).toEqual(customData.key);
await deleteApplication(application.id);
});
it('should put application custom data successfully', async () => {
const applicationName = 'test-create-app';
const applicationType = ApplicationType.SPA;
const application = await createApplication(applicationName, applicationType);
const customData = { key: 'foo' };
const result = await patchApplicationCustomData(application.id, customData);
expect(result.key).toEqual(customData.key);
await updateApplication(application.id, {
customData: { key: 'bar' },
});
const fetchedApplication = await getApplication(application.id);
expect(fetchedApplication.customData.key).toEqual('bar');
});
});

View file

@ -0,0 +1,18 @@
import { sql } from '@silverhand/slonik';
import type { AlterationScript } from '../lib/types/alteration.js';
const alteration: AlterationScript = {
up: async (pool) => {
await pool.query(sql`
alter table applications add column custom_data jsonb not null default '{}'::jsonb;
`);
},
down: async (pool) => {
await pool.query(sql`
alter table applications drop column custom_data;
`);
},
};
export default alteration;

View file

@ -30,6 +30,7 @@ export const buildDemoAppDataForTenant = (tenantId: string): Application => ({
protectedAppMetadata: null, protectedAppMetadata: null,
isThirdParty: false, isThirdParty: false,
createdAt: 0, createdAt: 0,
customData: {},
}); });
export const createDefaultAdminConsoleApplication = (): Readonly<CreateApplication> => export const createDefaultAdminConsoleApplication = (): Readonly<CreateApplication> =>

View file

@ -13,6 +13,7 @@ create table applications (
oidc_client_metadata jsonb /* @use OidcClientMetadata */ not null, oidc_client_metadata jsonb /* @use OidcClientMetadata */ not null,
custom_client_metadata jsonb /* @use CustomClientMetadata */ not null default '{}'::jsonb, custom_client_metadata jsonb /* @use CustomClientMetadata */ not null default '{}'::jsonb,
protected_app_metadata jsonb /* @use ProtectedAppMetadata */, protected_app_metadata jsonb /* @use ProtectedAppMetadata */,
custom_data jsonb /* @use JsonObject */ not null default '{}'::jsonb,
is_third_party boolean not null default false, is_third_party boolean not null default false,
created_at timestamptz not null default(now()), created_at timestamptz not null default(now()),
primary key (id) primary key (id)