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:
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,
|
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 = {
|
||||||
|
|
|
@ -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 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);
|
||||||
}
|
}
|
||||||
|
|
|
@ -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>>();
|
||||||
|
};
|
||||||
|
|
|
@ -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,
|
protectedAppMetadata: null,
|
||||||
isThirdParty: false,
|
isThirdParty: false,
|
||||||
createdAt: 0,
|
createdAt: 0,
|
||||||
|
customData: {},
|
||||||
});
|
});
|
||||||
|
|
||||||
export const createDefaultAdminConsoleApplication = (): Readonly<CreateApplication> =>
|
export const createDefaultAdminConsoleApplication = (): Readonly<CreateApplication> =>
|
||||||
|
|
|
@ -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)
|
||||||
|
|
Loading…
Reference in a new issue