mirror of
https://github.com/logto-io/logto.git
synced 2024-12-30 20:33:54 -05:00
chore: add comments and refactor
This commit is contained in:
parent
4a64d267b6
commit
1548e0732f
10 changed files with 83 additions and 46 deletions
5
.changeset/spicy-pears-serve.md
Normal file
5
.changeset/spicy-pears-serve.md
Normal file
|
@ -0,0 +1,5 @@
|
||||||
|
---
|
||||||
|
"@logto/core": minor
|
||||||
|
---
|
||||||
|
|
||||||
|
implement a central cache store to cache well-known with Redis implementation
|
|
@ -68,7 +68,6 @@
|
||||||
"lru-cache": "^8.0.0",
|
"lru-cache": "^8.0.0",
|
||||||
"nanoid": "^4.0.0",
|
"nanoid": "^4.0.0",
|
||||||
"oidc-provider": "^8.0.0",
|
"oidc-provider": "^8.0.0",
|
||||||
"p-memoize": "^7.1.1",
|
|
||||||
"p-retry": "^5.1.2",
|
"p-retry": "^5.1.2",
|
||||||
"pg-protocol": "^1.6.0",
|
"pg-protocol": "^1.6.0",
|
||||||
"redis": "^4.6.5",
|
"redis": "^4.6.5",
|
||||||
|
|
|
@ -17,12 +17,25 @@ const defaultCacheKey = '#';
|
||||||
|
|
||||||
export type WellKnownCacheType = keyof WellKnownMap;
|
export type WellKnownCacheType = keyof WellKnownMap;
|
||||||
|
|
||||||
type CacheKeyConfig<Args extends unknown[], Type = WellKnownCacheType> =
|
/**
|
||||||
| [Type]
|
* The array tuple to determine how cache will be built.
|
||||||
| [Type, (...args: Args) => string];
|
*
|
||||||
|
* - If only `Type` is given, the cache key should be resolved as `${valueof Type}:#`.
|
||||||
|
* - If both parameters are given, the cache key will be built dynamically by executing
|
||||||
|
* the second element (which is a function) by passing current calling arguments:
|
||||||
|
* `${valueof Type}:${valueof CacheKey(...args)}`.
|
||||||
|
*
|
||||||
|
* @template Args The function arguments for the cache key builder to resolve.
|
||||||
|
* @template Type The {@link WellKnownCacheType cache type}.
|
||||||
|
*/
|
||||||
|
type CacheKeyConfig<
|
||||||
|
Args extends unknown[],
|
||||||
|
Type = WellKnownCacheType,
|
||||||
|
CacheKey = (...args: Args) => string
|
||||||
|
> = [Type] | [Type, CacheKey];
|
||||||
|
|
||||||
// Cannot use generic type here, but direct type works.
|
// Cannot use generic type here, but direct type works.
|
||||||
// See https://github.com/microsoft/TypeScript/issues/27808#issuecomment-1207161877
|
// See [this issue](https://github.com/microsoft/TypeScript/issues/27808#issuecomment-1207161877) for details.
|
||||||
// WARN: You should carefully check key and return type mapping since the implementation signature doesn't do this.
|
// WARN: You should carefully check key and return type mapping since the implementation signature doesn't do this.
|
||||||
function getValueGuard<Type extends WellKnownCacheType>(type: Type): ZodType<WellKnownMap[Type]>;
|
function getValueGuard<Type extends WellKnownCacheType>(type: Type): ZodType<WellKnownMap[Type]>;
|
||||||
|
|
||||||
|
@ -46,9 +59,27 @@ function getValueGuard(type: WellKnownCacheType): ZodType<WellKnownMap[typeof ty
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* A reusable cache for well-known data. The name "well-known" has no direct relation to the `.well-known` routes,
|
||||||
|
* but indicates the data to store should be publicly viewable. You should never store any data that is protected
|
||||||
|
* by any authentication method.
|
||||||
|
*
|
||||||
|
* For better code maintainability, we recommend to use the cache for database queries only unless you have a strong
|
||||||
|
* reason.
|
||||||
|
*
|
||||||
|
* @see {@link getValueGuard} For how data will be guarded while getting from the cache.
|
||||||
|
*/
|
||||||
export class WellKnownCache {
|
export class WellKnownCache {
|
||||||
|
/**
|
||||||
|
* @param tenantId The tenant ID this cache is intended for.
|
||||||
|
* @param cacheStore The storage to use as the cache.
|
||||||
|
*/
|
||||||
constructor(public tenantId: string, protected cacheStore: CacheStore) {}
|
constructor(public tenantId: string, protected cacheStore: CacheStore) {}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Get value from the inner cache store for the given type and key.
|
||||||
|
* Note: Format errors will be silently caught and result an `undefined` return.
|
||||||
|
*/
|
||||||
async get<Type extends WellKnownCacheType>(
|
async get<Type extends WellKnownCacheType>(
|
||||||
type: Type,
|
type: Type,
|
||||||
key: string
|
key: string
|
||||||
|
@ -58,6 +89,10 @@ export class WellKnownCache {
|
||||||
return trySafe(() => getValueGuard(type).parse(JSON.parse(data ?? '')));
|
return trySafe(() => getValueGuard(type).parse(JSON.parse(data ?? '')));
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Set value to the inner cache store for the given type and key.
|
||||||
|
* The given value will be stringify without format validation before storing into the cache.
|
||||||
|
*/
|
||||||
async set<Type extends WellKnownCacheType>(
|
async set<Type extends WellKnownCacheType>(
|
||||||
type: Type,
|
type: Type,
|
||||||
key: string,
|
key: string,
|
||||||
|
@ -66,10 +101,18 @@ export class WellKnownCache {
|
||||||
return this.cacheStore.set(this.cacheKey(type, key), JSON.stringify(value));
|
return this.cacheStore.set(this.cacheKey(type, key), JSON.stringify(value));
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/** Delete value from the inner cache store for the given type and key. */
|
||||||
async delete(type: WellKnownCacheType, key: string) {
|
async delete(type: WellKnownCacheType, key: string) {
|
||||||
return this.cacheStore.delete(this.cacheKey(type, key));
|
return this.cacheStore.delete(this.cacheKey(type, key));
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Create a wrapper of the given function, which invalidates a set of keys in cache
|
||||||
|
* after the function runs successfully.
|
||||||
|
*
|
||||||
|
* @param run The function to wrap.
|
||||||
|
* @param types An array of {@link CacheKeyConfig}.
|
||||||
|
*/
|
||||||
mutate<Args extends unknown[], Return>(
|
mutate<Args extends unknown[], Return>(
|
||||||
run: (...args: Args) => Promise<Return>,
|
run: (...args: Args) => Promise<Return>,
|
||||||
...types: Array<CacheKeyConfig<Args>>
|
...types: Array<CacheKeyConfig<Args>>
|
||||||
|
@ -95,6 +138,13 @@ export class WellKnownCache {
|
||||||
return mutated;
|
return mutated;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* [Memoize](https://en.wikipedia.org/wiki/Memoization) a function and cache the result. The function execution
|
||||||
|
* will be also cached, which means there will be only one execution at a time.
|
||||||
|
*
|
||||||
|
* @param run The function to memoize.
|
||||||
|
* @param config The object to determine how cache key will be built. See {@link CacheKeyConfig} for details.
|
||||||
|
*/
|
||||||
memoize<Type extends WellKnownCacheType, Args extends unknown[]>(
|
memoize<Type extends WellKnownCacheType, Args extends unknown[]>(
|
||||||
run: (...args: Args) => Promise<Readonly<WellKnownMap[Type]>>,
|
run: (...args: Args) => Promise<Readonly<WellKnownMap[Type]>>,
|
||||||
[type, cacheKey]: CacheKeyConfig<Args, Type>
|
[type, cacheKey]: CacheKeyConfig<Args, Type>
|
||||||
|
|
|
@ -53,11 +53,9 @@ export const createSignInExperienceLibrary = (
|
||||||
});
|
});
|
||||||
};
|
};
|
||||||
|
|
||||||
const getSignInExperience = findDefaultSignInExperience;
|
|
||||||
|
|
||||||
const getFullSignInExperience = async (): Promise<FullSignInExperience> => {
|
const getFullSignInExperience = async (): Promise<FullSignInExperience> => {
|
||||||
const [signInExperience, logtoConnectors] = await Promise.all([
|
const [signInExperience, logtoConnectors] = await Promise.all([
|
||||||
getSignInExperience(),
|
findDefaultSignInExperience(),
|
||||||
getLogtoConnectors(),
|
getLogtoConnectors(),
|
||||||
]);
|
]);
|
||||||
|
|
||||||
|
@ -89,7 +87,6 @@ export const createSignInExperienceLibrary = (
|
||||||
return {
|
return {
|
||||||
validateLanguageInfo,
|
validateLanguageInfo,
|
||||||
removeUnavailableSocialConnectorTargets,
|
removeUnavailableSocialConnectorTargets,
|
||||||
getSignInExperience,
|
|
||||||
getFullSignInExperience,
|
getFullSignInExperience,
|
||||||
};
|
};
|
||||||
};
|
};
|
||||||
|
|
|
@ -99,7 +99,11 @@ const baseProviderMock = {
|
||||||
|
|
||||||
const tenantContext = new MockTenant(
|
const tenantContext = new MockTenant(
|
||||||
createMockProvider(jest.fn().mockResolvedValue(baseProviderMock)),
|
createMockProvider(jest.fn().mockResolvedValue(baseProviderMock)),
|
||||||
undefined,
|
{
|
||||||
|
signInExperiences: {
|
||||||
|
findDefaultSignInExperience: jest.fn().mockResolvedValue(mockSignInExperience),
|
||||||
|
},
|
||||||
|
},
|
||||||
{
|
{
|
||||||
getLogtoConnectorById: async (connectorId: string) => {
|
getLogtoConnectorById: async (connectorId: string) => {
|
||||||
const connector = await getLogtoConnectorByIdHelper(connectorId);
|
const connector = await getLogtoConnectorByIdHelper(connectorId);
|
||||||
|
@ -114,11 +118,6 @@ const tenantContext = new MockTenant(
|
||||||
// @ts-expect-error
|
// @ts-expect-error
|
||||||
return connector as LogtoConnector;
|
return connector as LogtoConnector;
|
||||||
},
|
},
|
||||||
},
|
|
||||||
{
|
|
||||||
signInExperiences: {
|
|
||||||
getSignInExperience: jest.fn().mockResolvedValue(mockSignInExperience),
|
|
||||||
},
|
|
||||||
}
|
}
|
||||||
);
|
);
|
||||||
|
|
||||||
|
|
|
@ -50,7 +50,7 @@ export type RouterContext<T> = T extends Router<unknown, infer Context> ? Contex
|
||||||
export default function interactionRoutes<T extends AnonymousRouter>(
|
export default function interactionRoutes<T extends AnonymousRouter>(
|
||||||
...[anonymousRouter, tenant]: RouterInitArgs<T>
|
...[anonymousRouter, tenant]: RouterInitArgs<T>
|
||||||
) {
|
) {
|
||||||
const { provider, queries, libraries, id: tenantId } = tenant;
|
const { provider, queries, libraries } = tenant;
|
||||||
const router =
|
const router =
|
||||||
// @ts-expect-error for good koa types
|
// @ts-expect-error for good koa types
|
||||||
// eslint-disable-next-line no-restricted-syntax
|
// eslint-disable-next-line no-restricted-syntax
|
||||||
|
@ -69,7 +69,7 @@ export default function interactionRoutes<T extends AnonymousRouter>(
|
||||||
profile: profileGuard.optional(),
|
profile: profileGuard.optional(),
|
||||||
}),
|
}),
|
||||||
}),
|
}),
|
||||||
koaInteractionSie(libraries.signInExperiences),
|
koaInteractionSie(queries),
|
||||||
async (ctx, next) => {
|
async (ctx, next) => {
|
||||||
const { event, identifier, profile } = ctx.guard.body;
|
const { event, identifier, profile } = ctx.guard.body;
|
||||||
const { signInExperience, createLog } = ctx;
|
const { signInExperience, createLog } = ctx;
|
||||||
|
@ -119,7 +119,7 @@ export default function interactionRoutes<T extends AnonymousRouter>(
|
||||||
router.put(
|
router.put(
|
||||||
`${interactionPrefix}/event`,
|
`${interactionPrefix}/event`,
|
||||||
koaGuard({ body: z.object({ event: eventGuard }) }),
|
koaGuard({ body: z.object({ event: eventGuard }) }),
|
||||||
koaInteractionSie(libraries.signInExperiences),
|
koaInteractionSie(queries),
|
||||||
async (ctx, next) => {
|
async (ctx, next) => {
|
||||||
const { event } = ctx.guard.body;
|
const { event } = ctx.guard.body;
|
||||||
const { signInExperience, interactionDetails, createLog } = ctx;
|
const { signInExperience, interactionDetails, createLog } = ctx;
|
||||||
|
@ -157,7 +157,7 @@ export default function interactionRoutes<T extends AnonymousRouter>(
|
||||||
koaGuard({
|
koaGuard({
|
||||||
body: identifierPayloadGuard,
|
body: identifierPayloadGuard,
|
||||||
}),
|
}),
|
||||||
koaInteractionSie(libraries.signInExperiences),
|
koaInteractionSie(queries),
|
||||||
async (ctx, next) => {
|
async (ctx, next) => {
|
||||||
const identifierPayload = ctx.guard.body;
|
const identifierPayload = ctx.guard.body;
|
||||||
const { signInExperience, interactionDetails, createLog } = ctx;
|
const { signInExperience, interactionDetails, createLog } = ctx;
|
||||||
|
@ -194,7 +194,7 @@ export default function interactionRoutes<T extends AnonymousRouter>(
|
||||||
koaGuard({
|
koaGuard({
|
||||||
body: profileGuard,
|
body: profileGuard,
|
||||||
}),
|
}),
|
||||||
koaInteractionSie(libraries.signInExperiences),
|
koaInteractionSie(queries),
|
||||||
async (ctx, next) => {
|
async (ctx, next) => {
|
||||||
const profilePayload = ctx.guard.body;
|
const profilePayload = ctx.guard.body;
|
||||||
const { signInExperience, interactionDetails, createLog } = ctx;
|
const { signInExperience, interactionDetails, createLog } = ctx;
|
||||||
|
@ -231,7 +231,7 @@ export default function interactionRoutes<T extends AnonymousRouter>(
|
||||||
koaGuard({
|
koaGuard({
|
||||||
body: profileGuard,
|
body: profileGuard,
|
||||||
}),
|
}),
|
||||||
koaInteractionSie(libraries.signInExperiences),
|
koaInteractionSie(queries),
|
||||||
async (ctx, next) => {
|
async (ctx, next) => {
|
||||||
const profilePayload = ctx.guard.body;
|
const profilePayload = ctx.guard.body;
|
||||||
const { signInExperience, interactionDetails, createLog } = ctx;
|
const { signInExperience, interactionDetails, createLog } = ctx;
|
||||||
|
@ -284,7 +284,7 @@ export default function interactionRoutes<T extends AnonymousRouter>(
|
||||||
// Submit Interaction
|
// Submit Interaction
|
||||||
router.post(
|
router.post(
|
||||||
`${interactionPrefix}/submit`,
|
`${interactionPrefix}/submit`,
|
||||||
koaInteractionSie(libraries.signInExperiences),
|
koaInteractionSie(queries),
|
||||||
koaInteractionHooks(tenant),
|
koaInteractionHooks(tenant),
|
||||||
async (ctx, next) => {
|
async (ctx, next) => {
|
||||||
const { interactionDetails, createLog } = ctx;
|
const { interactionDetails, createLog } = ctx;
|
||||||
|
|
|
@ -1,7 +1,7 @@
|
||||||
import type { SignInExperience } from '@logto/schemas';
|
import type { SignInExperience } from '@logto/schemas';
|
||||||
import type { MiddlewareType } from 'koa';
|
import type { MiddlewareType } from 'koa';
|
||||||
|
|
||||||
import type { SignInExperienceLibrary } from '#src/libraries/sign-in-experience/index.js';
|
import type Queries from '#src/tenants/Queries.js';
|
||||||
|
|
||||||
import type { WithInteractionDetailsContext } from './koa-interaction-details.js';
|
import type { WithInteractionDetailsContext } from './koa-interaction-details.js';
|
||||||
|
|
||||||
|
@ -10,14 +10,10 @@ export type WithInteractionSieContext<ContextT> = WithInteractionDetailsContext<
|
||||||
};
|
};
|
||||||
|
|
||||||
export default function koaInteractionSie<StateT, ContextT, ResponseT>({
|
export default function koaInteractionSie<StateT, ContextT, ResponseT>({
|
||||||
getSignInExperience,
|
signInExperiences: { findDefaultSignInExperience },
|
||||||
}: SignInExperienceLibrary): MiddlewareType<
|
}: Queries): MiddlewareType<StateT, WithInteractionSieContext<ContextT>, ResponseT> {
|
||||||
StateT,
|
|
||||||
WithInteractionSieContext<ContextT>,
|
|
||||||
ResponseT
|
|
||||||
> {
|
|
||||||
return async (ctx, next) => {
|
return async (ctx, next) => {
|
||||||
const signInExperience = await getSignInExperience();
|
const signInExperience = await findDefaultSignInExperience();
|
||||||
|
|
||||||
ctx.signInExperience = signInExperience;
|
ctx.signInExperience = signInExperience;
|
||||||
|
|
||||||
|
|
|
@ -15,10 +15,13 @@ export default function wellKnownRoutes<T extends AnonymousRouter>(
|
||||||
...[router, { libraries, queries, id: tenantId }]: RouterInitArgs<T>
|
...[router, { libraries, queries, id: tenantId }]: RouterInitArgs<T>
|
||||||
) {
|
) {
|
||||||
const {
|
const {
|
||||||
signInExperiences: { getSignInExperience, getFullSignInExperience },
|
signInExperiences: { getFullSignInExperience },
|
||||||
phrases: { getPhrases },
|
phrases: { getPhrases },
|
||||||
} = libraries;
|
} = libraries;
|
||||||
const { findAllCustomLanguageTags } = queries.customPhrases;
|
const {
|
||||||
|
customPhrases: { findAllCustomLanguageTags },
|
||||||
|
signInExperiences: { findDefaultSignInExperience },
|
||||||
|
} = queries;
|
||||||
|
|
||||||
if (tenantId === adminTenantId) {
|
if (tenantId === adminTenantId) {
|
||||||
router.get('/.well-known/endpoints/:tenantId', async (ctx, next) => {
|
router.get('/.well-known/endpoints/:tenantId', async (ctx, next) => {
|
||||||
|
@ -60,7 +63,7 @@ export default function wellKnownRoutes<T extends AnonymousRouter>(
|
||||||
|
|
||||||
const {
|
const {
|
||||||
languageInfo: { autoDetect, fallbackLanguage },
|
languageInfo: { autoDetect, fallbackLanguage },
|
||||||
} = await getSignInExperience();
|
} = await findDefaultSignInExperience();
|
||||||
|
|
||||||
const acceptableLanguages = conditionalArray<string | string[]>(
|
const acceptableLanguages = conditionalArray<string | string[]>(
|
||||||
lng,
|
lng,
|
||||||
|
|
|
@ -4,7 +4,6 @@ import { logtoConsoleUrl, logtoUrl } from '#src/constants.js';
|
||||||
|
|
||||||
const api = got.extend({
|
const api = got.extend({
|
||||||
prefixUrl: new URL('/api', logtoUrl),
|
prefixUrl: new URL('/api', logtoUrl),
|
||||||
headers: { 'cache-control': 'no-cache' },
|
|
||||||
});
|
});
|
||||||
|
|
||||||
export default api;
|
export default api;
|
||||||
|
@ -18,7 +17,6 @@ export const authedAdminApi = api.extend({
|
||||||
|
|
||||||
export const adminTenantApi = got.extend({
|
export const adminTenantApi = got.extend({
|
||||||
prefixUrl: new URL('/api', logtoConsoleUrl),
|
prefixUrl: new URL('/api', logtoConsoleUrl),
|
||||||
headers: { 'cache-control': 'no-cache' },
|
|
||||||
});
|
});
|
||||||
|
|
||||||
export const authedAdminTenantApi = adminTenantApi.extend({
|
export const authedAdminTenantApi = adminTenantApi.extend({
|
||||||
|
|
|
@ -3131,9 +3131,6 @@ importers:
|
||||||
oidc-provider:
|
oidc-provider:
|
||||||
specifier: ^8.0.0
|
specifier: ^8.0.0
|
||||||
version: 8.0.0
|
version: 8.0.0
|
||||||
p-memoize:
|
|
||||||
specifier: ^7.1.1
|
|
||||||
version: 7.1.1
|
|
||||||
p-retry:
|
p-retry:
|
||||||
specifier: ^5.1.2
|
specifier: ^5.1.2
|
||||||
version: 5.1.2
|
version: 5.1.2
|
||||||
|
@ -14867,6 +14864,7 @@ packages:
|
||||||
/mimic-fn@4.0.0:
|
/mimic-fn@4.0.0:
|
||||||
resolution: {integrity: sha512-vqiC06CuhBTUdZH+RYl8sFrL096vA45Ok5ISO6sE/Mr1jRbGH4Csnhi8f3wKVl7x8mO4Au7Ir9D3Oyv1VYMFJw==}
|
resolution: {integrity: sha512-vqiC06CuhBTUdZH+RYl8sFrL096vA45Ok5ISO6sE/Mr1jRbGH4Csnhi8f3wKVl7x8mO4Au7Ir9D3Oyv1VYMFJw==}
|
||||||
engines: {node: '>=12'}
|
engines: {node: '>=12'}
|
||||||
|
dev: true
|
||||||
|
|
||||||
/mimic-response@3.1.0:
|
/mimic-response@3.1.0:
|
||||||
resolution: {integrity: sha512-z0yWI+4FDrrweS8Zmt4Ej5HdJmky15+L2e6Wgn3+iK5fWzb6T3fhNFq2+MeTRb064c6Wr4N/wv0DzQTjNzHNGQ==}
|
resolution: {integrity: sha512-z0yWI+4FDrrweS8Zmt4Ej5HdJmky15+L2e6Wgn3+iK5fWzb6T3fhNFq2+MeTRb064c6Wr4N/wv0DzQTjNzHNGQ==}
|
||||||
|
@ -15492,14 +15490,6 @@ packages:
|
||||||
aggregate-error: 3.1.0
|
aggregate-error: 3.1.0
|
||||||
dev: true
|
dev: true
|
||||||
|
|
||||||
/p-memoize@7.1.1:
|
|
||||||
resolution: {integrity: sha512-DZ/bONJILHkQ721hSr/E9wMz5Am/OTJ9P6LhLFo2Tu+jL8044tgc9LwHO8g4PiaYePnlVVRAJcKmgy8J9MVFrA==}
|
|
||||||
engines: {node: '>=14.16'}
|
|
||||||
dependencies:
|
|
||||||
mimic-fn: 4.0.0
|
|
||||||
type-fest: 3.5.2
|
|
||||||
dev: false
|
|
||||||
|
|
||||||
/p-queue@7.3.4:
|
/p-queue@7.3.4:
|
||||||
resolution: {integrity: sha512-esox8CWt0j9EZECFvkFl2WNPat8LN4t7WWeXq73D9ha0V96qPRufApZi4ZhPwXAln1uVVal429HVVKPa2X0yQg==}
|
resolution: {integrity: sha512-esox8CWt0j9EZECFvkFl2WNPat8LN4t7WWeXq73D9ha0V96qPRufApZi4ZhPwXAln1uVVal429HVVKPa2X0yQg==}
|
||||||
engines: {node: '>=12'}
|
engines: {node: '>=12'}
|
||||||
|
|
Loading…
Reference in a new issue