0
Fork 0
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:
Gao Sun 2023-04-07 14:36:18 +08:00
parent 4a64d267b6
commit 1548e0732f
No known key found for this signature in database
GPG key ID: 13EBE123E4773688
10 changed files with 83 additions and 46 deletions

View file

@ -0,0 +1,5 @@
---
"@logto/core": minor
---
implement a central cache store to cache well-known with Redis implementation

View file

@ -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",

View file

@ -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>

View file

@ -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,
};
};

View file

@ -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),
},
}
);

View file

@ -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;

View file

@ -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;

View file

@ -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,

View file

@ -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({

View file

@ -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'}