mirror of
https://github.com/logto-io/logto.git
synced 2024-12-16 20:26:19 -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",
|
||||
"nanoid": "^4.0.0",
|
||||
"oidc-provider": "^8.0.0",
|
||||
"p-memoize": "^7.1.1",
|
||||
"p-retry": "^5.1.2",
|
||||
"pg-protocol": "^1.6.0",
|
||||
"redis": "^4.6.5",
|
||||
|
|
|
@ -17,12 +17,25 @@ const defaultCacheKey = '#';
|
|||
|
||||
export type WellKnownCacheType = keyof WellKnownMap;
|
||||
|
||||
type CacheKeyConfig<Args extends unknown[], Type = WellKnownCacheType> =
|
||||
| [Type]
|
||||
| [Type, (...args: Args) => string];
|
||||
/**
|
||||
* The array tuple to determine how cache will be built.
|
||||
*
|
||||
* - 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.
|
||||
// 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.
|
||||
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 {
|
||||
/**
|
||||
* @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) {}
|
||||
|
||||
/**
|
||||
* 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>(
|
||||
type: Type,
|
||||
key: string
|
||||
|
@ -58,6 +89,10 @@ export class WellKnownCache {
|
|||
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>(
|
||||
type: Type,
|
||||
key: string,
|
||||
|
@ -66,10 +101,18 @@ export class WellKnownCache {
|
|||
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) {
|
||||
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>(
|
||||
run: (...args: Args) => Promise<Return>,
|
||||
...types: Array<CacheKeyConfig<Args>>
|
||||
|
@ -95,6 +138,13 @@ export class WellKnownCache {
|
|||
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[]>(
|
||||
run: (...args: Args) => Promise<Readonly<WellKnownMap[Type]>>,
|
||||
[type, cacheKey]: CacheKeyConfig<Args, Type>
|
||||
|
|
|
@ -53,11 +53,9 @@ export const createSignInExperienceLibrary = (
|
|||
});
|
||||
};
|
||||
|
||||
const getSignInExperience = findDefaultSignInExperience;
|
||||
|
||||
const getFullSignInExperience = async (): Promise<FullSignInExperience> => {
|
||||
const [signInExperience, logtoConnectors] = await Promise.all([
|
||||
getSignInExperience(),
|
||||
findDefaultSignInExperience(),
|
||||
getLogtoConnectors(),
|
||||
]);
|
||||
|
||||
|
@ -89,7 +87,6 @@ export const createSignInExperienceLibrary = (
|
|||
return {
|
||||
validateLanguageInfo,
|
||||
removeUnavailableSocialConnectorTargets,
|
||||
getSignInExperience,
|
||||
getFullSignInExperience,
|
||||
};
|
||||
};
|
||||
|
|
|
@ -99,7 +99,11 @@ const baseProviderMock = {
|
|||
|
||||
const tenantContext = new MockTenant(
|
||||
createMockProvider(jest.fn().mockResolvedValue(baseProviderMock)),
|
||||
undefined,
|
||||
{
|
||||
signInExperiences: {
|
||||
findDefaultSignInExperience: jest.fn().mockResolvedValue(mockSignInExperience),
|
||||
},
|
||||
},
|
||||
{
|
||||
getLogtoConnectorById: async (connectorId: string) => {
|
||||
const connector = await getLogtoConnectorByIdHelper(connectorId);
|
||||
|
@ -114,11 +118,6 @@ const tenantContext = new MockTenant(
|
|||
// @ts-expect-error
|
||||
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>(
|
||||
...[anonymousRouter, tenant]: RouterInitArgs<T>
|
||||
) {
|
||||
const { provider, queries, libraries, id: tenantId } = tenant;
|
||||
const { provider, queries, libraries } = tenant;
|
||||
const router =
|
||||
// @ts-expect-error for good koa types
|
||||
// eslint-disable-next-line no-restricted-syntax
|
||||
|
@ -69,7 +69,7 @@ export default function interactionRoutes<T extends AnonymousRouter>(
|
|||
profile: profileGuard.optional(),
|
||||
}),
|
||||
}),
|
||||
koaInteractionSie(libraries.signInExperiences),
|
||||
koaInteractionSie(queries),
|
||||
async (ctx, next) => {
|
||||
const { event, identifier, profile } = ctx.guard.body;
|
||||
const { signInExperience, createLog } = ctx;
|
||||
|
@ -119,7 +119,7 @@ export default function interactionRoutes<T extends AnonymousRouter>(
|
|||
router.put(
|
||||
`${interactionPrefix}/event`,
|
||||
koaGuard({ body: z.object({ event: eventGuard }) }),
|
||||
koaInteractionSie(libraries.signInExperiences),
|
||||
koaInteractionSie(queries),
|
||||
async (ctx, next) => {
|
||||
const { event } = ctx.guard.body;
|
||||
const { signInExperience, interactionDetails, createLog } = ctx;
|
||||
|
@ -157,7 +157,7 @@ export default function interactionRoutes<T extends AnonymousRouter>(
|
|||
koaGuard({
|
||||
body: identifierPayloadGuard,
|
||||
}),
|
||||
koaInteractionSie(libraries.signInExperiences),
|
||||
koaInteractionSie(queries),
|
||||
async (ctx, next) => {
|
||||
const identifierPayload = ctx.guard.body;
|
||||
const { signInExperience, interactionDetails, createLog } = ctx;
|
||||
|
@ -194,7 +194,7 @@ export default function interactionRoutes<T extends AnonymousRouter>(
|
|||
koaGuard({
|
||||
body: profileGuard,
|
||||
}),
|
||||
koaInteractionSie(libraries.signInExperiences),
|
||||
koaInteractionSie(queries),
|
||||
async (ctx, next) => {
|
||||
const profilePayload = ctx.guard.body;
|
||||
const { signInExperience, interactionDetails, createLog } = ctx;
|
||||
|
@ -231,7 +231,7 @@ export default function interactionRoutes<T extends AnonymousRouter>(
|
|||
koaGuard({
|
||||
body: profileGuard,
|
||||
}),
|
||||
koaInteractionSie(libraries.signInExperiences),
|
||||
koaInteractionSie(queries),
|
||||
async (ctx, next) => {
|
||||
const profilePayload = ctx.guard.body;
|
||||
const { signInExperience, interactionDetails, createLog } = ctx;
|
||||
|
@ -284,7 +284,7 @@ export default function interactionRoutes<T extends AnonymousRouter>(
|
|||
// Submit Interaction
|
||||
router.post(
|
||||
`${interactionPrefix}/submit`,
|
||||
koaInteractionSie(libraries.signInExperiences),
|
||||
koaInteractionSie(queries),
|
||||
koaInteractionHooks(tenant),
|
||||
async (ctx, next) => {
|
||||
const { interactionDetails, createLog } = ctx;
|
||||
|
|
|
@ -1,7 +1,7 @@
|
|||
import type { SignInExperience } from '@logto/schemas';
|
||||
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';
|
||||
|
||||
|
@ -10,14 +10,10 @@ export type WithInteractionSieContext<ContextT> = WithInteractionDetailsContext<
|
|||
};
|
||||
|
||||
export default function koaInteractionSie<StateT, ContextT, ResponseT>({
|
||||
getSignInExperience,
|
||||
}: SignInExperienceLibrary): MiddlewareType<
|
||||
StateT,
|
||||
WithInteractionSieContext<ContextT>,
|
||||
ResponseT
|
||||
> {
|
||||
signInExperiences: { findDefaultSignInExperience },
|
||||
}: Queries): MiddlewareType<StateT, WithInteractionSieContext<ContextT>, ResponseT> {
|
||||
return async (ctx, next) => {
|
||||
const signInExperience = await getSignInExperience();
|
||||
const signInExperience = await findDefaultSignInExperience();
|
||||
|
||||
ctx.signInExperience = signInExperience;
|
||||
|
||||
|
|
|
@ -15,10 +15,13 @@ export default function wellKnownRoutes<T extends AnonymousRouter>(
|
|||
...[router, { libraries, queries, id: tenantId }]: RouterInitArgs<T>
|
||||
) {
|
||||
const {
|
||||
signInExperiences: { getSignInExperience, getFullSignInExperience },
|
||||
signInExperiences: { getFullSignInExperience },
|
||||
phrases: { getPhrases },
|
||||
} = libraries;
|
||||
const { findAllCustomLanguageTags } = queries.customPhrases;
|
||||
const {
|
||||
customPhrases: { findAllCustomLanguageTags },
|
||||
signInExperiences: { findDefaultSignInExperience },
|
||||
} = queries;
|
||||
|
||||
if (tenantId === adminTenantId) {
|
||||
router.get('/.well-known/endpoints/:tenantId', async (ctx, next) => {
|
||||
|
@ -60,7 +63,7 @@ export default function wellKnownRoutes<T extends AnonymousRouter>(
|
|||
|
||||
const {
|
||||
languageInfo: { autoDetect, fallbackLanguage },
|
||||
} = await getSignInExperience();
|
||||
} = await findDefaultSignInExperience();
|
||||
|
||||
const acceptableLanguages = conditionalArray<string | string[]>(
|
||||
lng,
|
||||
|
|
|
@ -4,7 +4,6 @@ import { logtoConsoleUrl, logtoUrl } from '#src/constants.js';
|
|||
|
||||
const api = got.extend({
|
||||
prefixUrl: new URL('/api', logtoUrl),
|
||||
headers: { 'cache-control': 'no-cache' },
|
||||
});
|
||||
|
||||
export default api;
|
||||
|
@ -18,7 +17,6 @@ export const authedAdminApi = api.extend({
|
|||
|
||||
export const adminTenantApi = got.extend({
|
||||
prefixUrl: new URL('/api', logtoConsoleUrl),
|
||||
headers: { 'cache-control': 'no-cache' },
|
||||
});
|
||||
|
||||
export const authedAdminTenantApi = adminTenantApi.extend({
|
||||
|
|
|
@ -3131,9 +3131,6 @@ importers:
|
|||
oidc-provider:
|
||||
specifier: ^8.0.0
|
||||
version: 8.0.0
|
||||
p-memoize:
|
||||
specifier: ^7.1.1
|
||||
version: 7.1.1
|
||||
p-retry:
|
||||
specifier: ^5.1.2
|
||||
version: 5.1.2
|
||||
|
@ -14867,6 +14864,7 @@ packages:
|
|||
/mimic-fn@4.0.0:
|
||||
resolution: {integrity: sha512-vqiC06CuhBTUdZH+RYl8sFrL096vA45Ok5ISO6sE/Mr1jRbGH4Csnhi8f3wKVl7x8mO4Au7Ir9D3Oyv1VYMFJw==}
|
||||
engines: {node: '>=12'}
|
||||
dev: true
|
||||
|
||||
/mimic-response@3.1.0:
|
||||
resolution: {integrity: sha512-z0yWI+4FDrrweS8Zmt4Ej5HdJmky15+L2e6Wgn3+iK5fWzb6T3fhNFq2+MeTRb064c6Wr4N/wv0DzQTjNzHNGQ==}
|
||||
|
@ -15492,14 +15490,6 @@ packages:
|
|||
aggregate-error: 3.1.0
|
||||
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:
|
||||
resolution: {integrity: sha512-esox8CWt0j9EZECFvkFl2WNPat8LN4t7WWeXq73D9ha0V96qPRufApZi4ZhPwXAln1uVVal429HVVKPa2X0yQg==}
|
||||
engines: {node: '>=12'}
|
||||
|
|
Loading…
Reference in a new issue