mirror of
https://github.com/logto-io/logto.git
synced 2024-12-16 20:26:19 -05:00
feat(core): add subscription cache class
refactor the well-known cache class and implement a new subscription cache
This commit is contained in:
parent
971c75ad5e
commit
e8bfb028ce
7 changed files with 288 additions and 166 deletions
177
packages/core/src/caches/base-cache.ts
Normal file
177
packages/core/src/caches/base-cache.ts
Normal file
|
@ -0,0 +1,177 @@
|
||||||
|
import { trySafe, type Optional } from '@silverhand/essentials';
|
||||||
|
import { type ZodType } from 'zod';
|
||||||
|
|
||||||
|
import { type CacheStore } from './types.js';
|
||||||
|
import { cacheConsole } from './utils.js';
|
||||||
|
|
||||||
|
type CacheKeyOf<CacheMapT extends Record<string, unknown>> = Extract<keyof CacheMapT, 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 extends string,
|
||||||
|
CacheKey = (...args: Args) => string,
|
||||||
|
> = [Type] | [Type, CacheKey];
|
||||||
|
|
||||||
|
export abstract class BaseCache<CacheMapT extends Record<string, unknown>> {
|
||||||
|
static defaultKey = '#';
|
||||||
|
|
||||||
|
/**
|
||||||
|
* For logging and debugging purposes only.
|
||||||
|
* This name will be used in the log messages.
|
||||||
|
*/
|
||||||
|
abstract name: string;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* @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: Redis connection and format errors will be silently caught and result an `undefined` return.
|
||||||
|
*/
|
||||||
|
async get<Type extends CacheKeyOf<CacheMapT>>(
|
||||||
|
type: Type,
|
||||||
|
key: string
|
||||||
|
): Promise<Optional<CacheMapT[Type]>> {
|
||||||
|
return trySafe(async () => {
|
||||||
|
const data = await this.cacheStore.get(this.cacheKey(type, key));
|
||||||
|
return this.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.
|
||||||
|
*
|
||||||
|
* @param expire The expire time in seconds. If not given, use the default expire time 30 * 60 seconds.
|
||||||
|
*/
|
||||||
|
async set<Type extends CacheKeyOf<CacheMapT>>(
|
||||||
|
type: Type,
|
||||||
|
key: string,
|
||||||
|
value: Readonly<CacheMapT[Type]>,
|
||||||
|
expire?: number
|
||||||
|
) {
|
||||||
|
return this.cacheStore.set(this.cacheKey(type, key), JSON.stringify(value), expire);
|
||||||
|
}
|
||||||
|
|
||||||
|
/** Delete value from the inner cache store for the given type and key. */
|
||||||
|
async delete(type: CacheKeyOf<CacheMapT>, 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, CacheKeyOf<CacheMapT>>>
|
||||||
|
) {
|
||||||
|
// Intended. We're going to use `this` cache inside another closure.
|
||||||
|
// eslint-disable-next-line @typescript-eslint/no-this-alias, unicorn/no-this-assignment
|
||||||
|
const kvCache = this;
|
||||||
|
|
||||||
|
const mutated = async function (this: unknown, ...args: Args): Promise<Return> {
|
||||||
|
const value = await run.apply(this, args);
|
||||||
|
|
||||||
|
// We don't leverage `finally` here since we want to ensure cache deleting
|
||||||
|
// only happens when the original function executed successfully
|
||||||
|
void Promise.all(
|
||||||
|
types.map(async ([type, cacheKey]) =>
|
||||||
|
trySafe(kvCache.delete(type, cacheKey?.(...args) ?? BaseCache.defaultKey))
|
||||||
|
)
|
||||||
|
);
|
||||||
|
|
||||||
|
return value;
|
||||||
|
};
|
||||||
|
|
||||||
|
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.
|
||||||
|
* @param getExpiresIn A function to determine how long the cache will be expired. The function will be called
|
||||||
|
* with the resolved value from the original function. The return value should be the expire time in seconds.
|
||||||
|
*/
|
||||||
|
memoize<
|
||||||
|
Type extends CacheKeyOf<CacheMapT>,
|
||||||
|
Args extends unknown[],
|
||||||
|
Value extends Readonly<CacheMapT[Type]>,
|
||||||
|
>(
|
||||||
|
run: (...args: Args) => Promise<Value>,
|
||||||
|
[type, cacheKey]: CacheKeyConfig<Args, Type>,
|
||||||
|
getExpiresIn?: (value: Value) => number
|
||||||
|
) {
|
||||||
|
const promiseCache = new Map<unknown, Promise<Readonly<CacheMapT[Type]>>>();
|
||||||
|
// Intended. We're going to use `this` cache inside another closure.
|
||||||
|
// eslint-disable-next-line @typescript-eslint/no-this-alias, unicorn/no-this-assignment
|
||||||
|
const kvCache = this;
|
||||||
|
|
||||||
|
const memoized = async function (
|
||||||
|
this: unknown,
|
||||||
|
...args: Args
|
||||||
|
): Promise<Readonly<CacheMapT[Type]>> {
|
||||||
|
const promiseKey = cacheKey?.(...args) ?? BaseCache.defaultKey;
|
||||||
|
const cachedPromise = promiseCache.get(promiseKey);
|
||||||
|
|
||||||
|
if (cachedPromise) {
|
||||||
|
return cachedPromise;
|
||||||
|
}
|
||||||
|
|
||||||
|
const promise = (async () => {
|
||||||
|
try {
|
||||||
|
// Wrap with `trySafe()` here to ignore Redis errors
|
||||||
|
const cachedValue = await trySafe(kvCache.get(type, promiseKey));
|
||||||
|
|
||||||
|
if (cachedValue) {
|
||||||
|
cacheConsole.info(`${kvCache.name} cache hit for', type, promiseKey`);
|
||||||
|
return cachedValue;
|
||||||
|
}
|
||||||
|
|
||||||
|
const value = await run.apply(this, args);
|
||||||
|
|
||||||
|
await trySafe(kvCache.set(type, promiseKey, value, getExpiresIn?.(value)));
|
||||||
|
|
||||||
|
return value;
|
||||||
|
} finally {
|
||||||
|
promiseCache.delete(promiseKey);
|
||||||
|
}
|
||||||
|
})();
|
||||||
|
|
||||||
|
promiseCache.set(promiseKey, promise);
|
||||||
|
|
||||||
|
return promise;
|
||||||
|
};
|
||||||
|
|
||||||
|
return memoized;
|
||||||
|
}
|
||||||
|
|
||||||
|
abstract getValueGuard<Type extends CacheKeyOf<CacheMapT>>(type: Type): ZodType<CacheMapT[Type]>;
|
||||||
|
|
||||||
|
protected cacheKey(type: CacheKeyOf<CacheMapT>, key: string) {
|
||||||
|
return `${this.tenantId}:${type}:${key}`;
|
||||||
|
}
|
||||||
|
}
|
32
packages/core/src/caches/tenant-subscription.ts
Normal file
32
packages/core/src/caches/tenant-subscription.ts
Normal file
|
@ -0,0 +1,32 @@
|
||||||
|
import { SubscriptionRedisCacheKey } from '@logto/schemas';
|
||||||
|
import { type ZodType } from 'zod';
|
||||||
|
|
||||||
|
import { type Subscription, subscriptionCacheGuard } from '#src/utils/subscription/types.js';
|
||||||
|
|
||||||
|
import { BaseCache } from './base-cache.js';
|
||||||
|
|
||||||
|
type SubscriptionCacheMap = {
|
||||||
|
[SubscriptionRedisCacheKey.Subscription]: Subscription;
|
||||||
|
};
|
||||||
|
|
||||||
|
type SubscriptionCacheType = keyof SubscriptionCacheMap;
|
||||||
|
|
||||||
|
function getValueGuard(type: SubscriptionCacheType): ZodType<SubscriptionCacheMap[typeof type]> {
|
||||||
|
switch (type) {
|
||||||
|
case SubscriptionRedisCacheKey.Subscription: {
|
||||||
|
return subscriptionCacheGuard;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* A local region cache for tenant subscription data.
|
||||||
|
* We use this cache to reduce the number of requests to the Cloud
|
||||||
|
* and improve the performance of subscription-related operations.
|
||||||
|
*
|
||||||
|
* TODO: Will use the cache for tenant subscription data.
|
||||||
|
*/
|
||||||
|
class TenantSubscriptionCache extends BaseCache<SubscriptionCacheMap> {
|
||||||
|
name = 'Tenant Subscription';
|
||||||
|
getValueGuard = getValueGuard;
|
||||||
|
}
|
|
@ -1,11 +1,9 @@
|
||||||
import { type SignInExperience, SignInExperiences } from '@logto/schemas';
|
import { type SignInExperience, SignInExperiences } from '@logto/schemas';
|
||||||
import { type Optional, trySafe } from '@silverhand/essentials';
|
|
||||||
import { type ZodType, z } from 'zod';
|
import { type ZodType, z } from 'zod';
|
||||||
|
|
||||||
import { type ConnectorWellKnown, connectorWellKnownGuard } from '#src/utils/connectors/types.js';
|
import { type ConnectorWellKnown, connectorWellKnownGuard } from '#src/utils/connectors/types.js';
|
||||||
|
|
||||||
import { type CacheStore } from './types.js';
|
import { BaseCache } from './base-cache.js';
|
||||||
import { cacheConsole } from './utils.js';
|
|
||||||
|
|
||||||
type WellKnownMap = {
|
type WellKnownMap = {
|
||||||
sie: SignInExperience;
|
sie: SignInExperience;
|
||||||
|
@ -19,23 +17,6 @@ type WellKnownMap = {
|
||||||
|
|
||||||
type WellKnownCacheType = keyof WellKnownMap;
|
type WellKnownCacheType = keyof WellKnownMap;
|
||||||
|
|
||||||
/**
|
|
||||||
* 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.
|
// Cannot use generic type here, but direct type works.
|
||||||
// See [this issue](https://github.com/microsoft/TypeScript/issues/27808#issuecomment-1207161877) for details.
|
// 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.
|
||||||
|
@ -75,147 +56,7 @@ function getValueGuard(type: WellKnownCacheType): ZodType<WellKnownMap[typeof ty
|
||||||
*
|
*
|
||||||
* @see {@link getValueGuard} For how data will be guarded while getting from the cache.
|
* @see {@link getValueGuard} For how data will be guarded while getting from the cache.
|
||||||
*/
|
*/
|
||||||
export class WellKnownCache {
|
export class WellKnownCache extends BaseCache<WellKnownMap> {
|
||||||
static defaultKey = '#';
|
name = 'Well-known';
|
||||||
|
getValueGuard = getValueGuard;
|
||||||
/**
|
|
||||||
* @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: Redis connection and format errors will be silently caught and result an `undefined` return.
|
|
||||||
*/
|
|
||||||
async get<Type extends WellKnownCacheType>(
|
|
||||||
type: Type,
|
|
||||||
key: string
|
|
||||||
): Promise<Optional<WellKnownMap[Type]>> {
|
|
||||||
return trySafe(async () => {
|
|
||||||
const data = await this.cacheStore.get(this.cacheKey(type, key));
|
|
||||||
return 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.
|
|
||||||
*
|
|
||||||
* @param expire The expire time in seconds. If not given, use the default expire time 30 * 60 seconds.
|
|
||||||
*/
|
|
||||||
async set<Type extends WellKnownCacheType>(
|
|
||||||
type: Type,
|
|
||||||
key: string,
|
|
||||||
value: Readonly<WellKnownMap[Type]>,
|
|
||||||
expire?: number
|
|
||||||
) {
|
|
||||||
return this.cacheStore.set(this.cacheKey(type, key), JSON.stringify(value), expire);
|
|
||||||
}
|
|
||||||
|
|
||||||
/** 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>>
|
|
||||||
) {
|
|
||||||
// Intended. We're going to use `this` cache inside another closure.
|
|
||||||
// eslint-disable-next-line @typescript-eslint/no-this-alias, unicorn/no-this-assignment
|
|
||||||
const kvCache = this;
|
|
||||||
|
|
||||||
const mutated = async function (this: unknown, ...args: Args): Promise<Return> {
|
|
||||||
const value = await run.apply(this, args);
|
|
||||||
|
|
||||||
// We don't leverage `finally` here since we want to ensure cache deleting
|
|
||||||
// only happens when the original function executed successfully
|
|
||||||
void Promise.all(
|
|
||||||
types.map(async ([type, cacheKey]) =>
|
|
||||||
trySafe(kvCache.delete(type, cacheKey?.(...args) ?? WellKnownCache.defaultKey))
|
|
||||||
)
|
|
||||||
);
|
|
||||||
|
|
||||||
return value;
|
|
||||||
};
|
|
||||||
|
|
||||||
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.
|
|
||||||
* @param getExpiresIn A function to determine how long the cache will be expired. The function will be called
|
|
||||||
* with the resolved value from the original function. The return value should be the expire time in seconds.
|
|
||||||
*/
|
|
||||||
memoize<
|
|
||||||
Type extends WellKnownCacheType,
|
|
||||||
Args extends unknown[],
|
|
||||||
Value extends Readonly<WellKnownMap[Type]>,
|
|
||||||
>(
|
|
||||||
run: (...args: Args) => Promise<Value>,
|
|
||||||
[type, cacheKey]: CacheKeyConfig<Args, Type>,
|
|
||||||
getExpiresIn?: (value: Value) => number
|
|
||||||
) {
|
|
||||||
const promiseCache = new Map<unknown, Promise<Readonly<WellKnownMap[Type]>>>();
|
|
||||||
// Intended. We're going to use `this` cache inside another closure.
|
|
||||||
// eslint-disable-next-line @typescript-eslint/no-this-alias, unicorn/no-this-assignment
|
|
||||||
const kvCache = this;
|
|
||||||
|
|
||||||
const memoized = async function (
|
|
||||||
this: unknown,
|
|
||||||
...args: Args
|
|
||||||
): Promise<Readonly<WellKnownMap[Type]>> {
|
|
||||||
const promiseKey = cacheKey?.(...args) ?? WellKnownCache.defaultKey;
|
|
||||||
const cachedPromise = promiseCache.get(promiseKey);
|
|
||||||
|
|
||||||
if (cachedPromise) {
|
|
||||||
return cachedPromise;
|
|
||||||
}
|
|
||||||
|
|
||||||
const promise = (async () => {
|
|
||||||
try {
|
|
||||||
// Wrap with `trySafe()` here to ignore Redis errors
|
|
||||||
const cachedValue = await trySafe(kvCache.get(type, promiseKey));
|
|
||||||
|
|
||||||
if (cachedValue) {
|
|
||||||
cacheConsole.info('Well-known cache hit for', type, promiseKey);
|
|
||||||
return cachedValue;
|
|
||||||
}
|
|
||||||
|
|
||||||
const value = await run.apply(this, args);
|
|
||||||
|
|
||||||
await trySafe(kvCache.set(type, promiseKey, value, getExpiresIn?.(value)));
|
|
||||||
|
|
||||||
return value;
|
|
||||||
} finally {
|
|
||||||
promiseCache.delete(promiseKey);
|
|
||||||
}
|
|
||||||
})();
|
|
||||||
|
|
||||||
promiseCache.set(promiseKey, promise);
|
|
||||||
|
|
||||||
return promise;
|
|
||||||
};
|
|
||||||
|
|
||||||
return memoized;
|
|
||||||
}
|
|
||||||
|
|
||||||
protected cacheKey(type: WellKnownCacheType, key: string) {
|
|
||||||
return `${this.tenantId}:${type}:${key}`;
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
|
|
@ -1,10 +1,10 @@
|
||||||
|
import { type ToZodObject } from '@logto/connector-kit';
|
||||||
import {
|
import {
|
||||||
type VerificationIdentifier,
|
type VerificationIdentifier,
|
||||||
VerificationType,
|
VerificationType,
|
||||||
type User,
|
type User,
|
||||||
verificationIdentifierGuard,
|
verificationIdentifierGuard,
|
||||||
} from '@logto/schemas';
|
} from '@logto/schemas';
|
||||||
import { type ToZodObject } from '@logto/schemas/lib/utils/zod.js';
|
|
||||||
import { generateStandardId } from '@logto/shared';
|
import { generateStandardId } from '@logto/shared';
|
||||||
import { z } from 'zod';
|
import { z } from 'zod';
|
||||||
|
|
||||||
|
|
|
@ -1,4 +1,4 @@
|
||||||
import { type ToZodObject } from '@logto/schemas/lib/utils/zod.js';
|
import { type ToZodObject } from '@logto/connector-kit';
|
||||||
import { z } from 'zod';
|
import { z } from 'zod';
|
||||||
|
|
||||||
import { extendedSocialUserInfoGuard, type ExtendedSocialUserInfo } from './saml.js';
|
import { extendedSocialUserInfoGuard, type ExtendedSocialUserInfo } from './saml.js';
|
||||||
|
|
|
@ -1,6 +1,7 @@
|
||||||
import type router from '@logto/cloud/routes';
|
import type router from '@logto/cloud/routes';
|
||||||
|
import { type ToZodObject } from '@logto/connector-kit';
|
||||||
import { type RouterRoutes } from '@withtyped/client';
|
import { type RouterRoutes } from '@withtyped/client';
|
||||||
import { type z, type ZodType } from 'zod';
|
import { z, type ZodType } from 'zod';
|
||||||
|
|
||||||
type GetRoutes = RouterRoutes<typeof router>['get'];
|
type GetRoutes = RouterRoutes<typeof router>['get'];
|
||||||
type PostRoutes = RouterRoutes<typeof router>['post'];
|
type PostRoutes = RouterRoutes<typeof router>['post'];
|
||||||
|
@ -56,3 +57,64 @@ export const allReportSubscriptionUpdatesUsageKeys = Object.freeze([
|
||||||
'enterpriseSsoLimit',
|
'enterpriseSsoLimit',
|
||||||
'hooksLimit',
|
'hooksLimit',
|
||||||
]) satisfies readonly ReportSubscriptionUpdatesUsageKey[];
|
]) satisfies readonly ReportSubscriptionUpdatesUsageKey[];
|
||||||
|
|
||||||
|
const subscriptionStatusGuard = z.enum([
|
||||||
|
'incomplete',
|
||||||
|
'incomplete_expired',
|
||||||
|
'trialing',
|
||||||
|
'active',
|
||||||
|
'past_due',
|
||||||
|
'canceled',
|
||||||
|
'unpaid',
|
||||||
|
'paused',
|
||||||
|
]);
|
||||||
|
|
||||||
|
const upcomingInvoiceGuard = z.object({
|
||||||
|
subtotal: z.number(),
|
||||||
|
subtotalExcludingTax: z.number().nullable(),
|
||||||
|
total: z.number(),
|
||||||
|
totalExcludingTax: z.number().nullable(),
|
||||||
|
}) satisfies ToZodObject<Subscription['upcomingInvoice']>;
|
||||||
|
|
||||||
|
const logtoSkuQuotaGuard = z.object({
|
||||||
|
mauLimit: z.number().nullable(),
|
||||||
|
applicationsLimit: z.number().nullable(),
|
||||||
|
thirdPartyApplicationsLimit: z.number().nullable(),
|
||||||
|
scopesPerResourceLimit: z.number().nullable(),
|
||||||
|
socialConnectorsLimit: z.number().nullable(),
|
||||||
|
userRolesLimit: z.number().nullable(),
|
||||||
|
machineToMachineRolesLimit: z.number().nullable(),
|
||||||
|
scopesPerRoleLimit: z.number().nullable(),
|
||||||
|
hooksLimit: z.number().nullable(),
|
||||||
|
auditLogsRetentionDays: z.number().nullable(),
|
||||||
|
customJwtEnabled: z.boolean(),
|
||||||
|
subjectTokenEnabled: z.boolean(),
|
||||||
|
bringYourUiEnabled: z.boolean(),
|
||||||
|
tokenLimit: z.number().nullable(),
|
||||||
|
machineToMachineLimit: z.number().nullable(),
|
||||||
|
resourcesLimit: z.number().nullable(),
|
||||||
|
enterpriseSsoLimit: z.number().nullable(),
|
||||||
|
tenantMembersLimit: z.number().nullable(),
|
||||||
|
mfaEnabled: z.boolean(),
|
||||||
|
organizationsEnabled: z.boolean(),
|
||||||
|
organizationsLimit: z.number().nullable(),
|
||||||
|
idpInitiatedSsoEnabled: z.boolean(),
|
||||||
|
}) satisfies ToZodObject<SubscriptionQuota>;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Redis cache guard for the subscription data returned from the Cloud API `/api/tenants/my/subscription`.
|
||||||
|
* Logto core does not have access to the zod guard of the subscription data in Cloud,
|
||||||
|
* so we need to manually define the guard here,
|
||||||
|
* it should be kept in sync with the Cloud API response.
|
||||||
|
*/
|
||||||
|
export const subscriptionCacheGuard = z.object({
|
||||||
|
id: z.string().optional(),
|
||||||
|
planId: z.string(),
|
||||||
|
currentPeriodStart: z.string(),
|
||||||
|
currentPeriodEnd: z.string(),
|
||||||
|
isEnterprisePlan: z.boolean(),
|
||||||
|
isAddOnAvailable: z.boolean(),
|
||||||
|
status: subscriptionStatusGuard,
|
||||||
|
upcomingInvoice: upcomingInvoiceGuard.nullable().optional(),
|
||||||
|
quota: logtoSkuQuotaGuard,
|
||||||
|
}) satisfies ToZodObject<Subscription>;
|
||||||
|
|
|
@ -23,3 +23,13 @@ export enum ReservedPlanId {
|
||||||
*/
|
*/
|
||||||
Pro202411 = 'pro-202411',
|
Pro202411 = 'pro-202411',
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Tenant subscription related Redis cache keys.
|
||||||
|
*
|
||||||
|
* We use Redis to cache the tenant subscription data to reduce the number of requests to the Cloud.
|
||||||
|
* Both @logto/core and @logto/cloud will need to access the cache, so we define the cache keys here as the SSOT.
|
||||||
|
*/
|
||||||
|
export enum SubscriptionRedisCacheKey {
|
||||||
|
Subscription = 'subscription',
|
||||||
|
}
|
||||||
|
|
Loading…
Reference in a new issue