0
Fork 0
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:
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", "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",

View file

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

View file

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

View file

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

View file

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

View file

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

View file

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

View file

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

View file

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