0
Fork 0
mirror of https://github.com/logto-io/logto.git synced 2024-12-16 20:26:19 -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,
ApplicationsRole,
LogtoConfig,
Passcode,
OidcConfigKey,
Passcode,
Resource,
Role,
Scope,
UsersRole,
} from '@logto/schemas';
import {
RoleType,
ApplicationType,
LogtoOidcConfigKey,
DomainStatus,
LogtoJwtTokenKey,
internalPrefix,
LogtoJwtTokenKey,
LogtoOidcConfigKey,
RoleType,
} from '@logto/schemas';
import { protectedAppSignInCallbackUrl } from '#src/constants/index.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 './user.js';
export * from './connector.js';
export * from './domain.js';
export * from './sso.js';
export * from './protected-app.js';
export * from './sign-in-experience.js';
export * from './sso.js';
export * from './user.js';
export const mockApplication: Application = {
tenantId: 'fake_tenant',
@ -50,6 +50,7 @@ export const mockApplication: Application = {
protectedAppMetadata: null,
isThirdParty: false,
createdAt: 1_645_334_775_356,
customData: {},
};
export const mockProtectedApplication: Omit<Application, 'protectedAppMetadata'> & {
@ -78,6 +79,7 @@ export const mockProtectedApplication: Omit<Application, 'protectedAppMetadata'>
},
isThirdParty: false,
createdAt: 1_645_334_775_356,
customData: {},
};
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 {
demoAppApplicationId,
buildDemoAppDataForTenant,
InternalRole,
ApplicationType,
Applications,
ApplicationType,
buildDemoAppDataForTenant,
demoAppApplicationId,
hasSecrets,
InternalRole,
} from '@logto/schemas';
import { generateStandardId, generateStandardSecret } from '@logto/shared';
import { conditional } from '@silverhand/essentials';
@ -21,6 +21,7 @@ import { parseSearchParamsForSearch } from '#src/utils/search.js';
import type { ManagementApiRouter, RouterInitArgs } from '../types.js';
import applicationCustomDataRoutes from './application-custom-data.js';
import { generateInternalSecret } from './application-secret.js';
import { applicationCreateGuard, applicationPatchGuard } from './types.js';
@ -38,15 +39,14 @@ const parseIsThirdPartQueryParam = (isThirdPartyQuery: 'true' | 'false' | undefi
const applicationTypeGuard = z.nativeEnum(ApplicationType);
export default function applicationRoutes<T extends ManagementApiRouter>(
...[
router,
{
queries,
id: tenantId,
libraries: { quota, protectedApps },
},
]: RouterInitArgs<T>
...[router, tenant]: RouterInitArgs<T>
) {
const {
queries,
id: tenantId,
libraries: { quota, protectedApps },
} = tenant;
router.get(
'/applications',
koaPagination({ isOptional: true }),
@ -346,4 +346,6 @@ export default function applicationRoutes<T extends ManagementApiRouter>(
return next();
}
);
applicationCustomDataRoutes(router, tenant);
}

View file

@ -1,13 +1,13 @@
import {
ApplicationType,
type Application,
type CreateApplication,
type OidcClientMetadata,
type Role,
type ProtectedAppMetadata,
type OrganizationWithRoles,
type CreateApplicationSecret,
type ApplicationSecret,
type CreateApplication,
type CreateApplicationSecret,
type OidcClientMetadata,
type OrganizationWithRoles,
type ProtectedAppMetadata,
type Role,
} from '@logto/schemas';
import { conditional } from '@silverhand/essentials';
@ -150,3 +150,14 @@ export const deleteApplicationSecret = async (applicationId: string, secretName:
export const deleteLegacyApplicationSecret = async (applicationId: string) =>
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,
isThirdParty: false,
createdAt: 0,
customData: {},
});
export const createDefaultAdminConsoleApplication = (): Readonly<CreateApplication> =>

View file

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