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:
parent
f8f34f0e87
commit
6477c6deef
10 changed files with 176 additions and 26 deletions
12
.changeset/clever-lemons-yawn.md
Normal file
12
.changeset/clever-lemons-yawn.md
Normal 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`
|
|
@ -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 = {
|
||||
|
|
|
@ -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."
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
|
@ -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();
|
||||
}
|
||||
);
|
||||
}
|
|
@ -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);
|
||||
}
|
||||
|
|
|
@ -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>>();
|
||||
};
|
||||
|
|
|
@ -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');
|
||||
});
|
||||
});
|
|
@ -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;
|
|
@ -30,6 +30,7 @@ export const buildDemoAppDataForTenant = (tenantId: string): Application => ({
|
|||
protectedAppMetadata: null,
|
||||
isThirdParty: false,
|
||||
createdAt: 0,
|
||||
customData: {},
|
||||
});
|
||||
|
||||
export const createDefaultAdminConsoleApplication = (): Readonly<CreateApplication> =>
|
||||
|
|
|
@ -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)
|
||||
|
|
Loading…
Reference in a new issue