mirror of
https://github.com/logto-io/logto.git
synced 2025-03-24 22:41:28 -05:00
refactor: cache well-known data
This commit is contained in:
parent
70709044cc
commit
dd91ebddfa
51 changed files with 356 additions and 216 deletions
|
@ -47,7 +47,7 @@
|
|||
"@logto/core-kit": "workspace:*",
|
||||
"@logto/schemas": "workspace:*",
|
||||
"@logto/shared": "workspace:*",
|
||||
"@silverhand/essentials": "2.4.1",
|
||||
"@silverhand/essentials": "^2.4.1",
|
||||
"chalk": "^5.0.0",
|
||||
"decamelize": "^6.0.0",
|
||||
"dotenv": "^16.0.0",
|
||||
|
|
|
@ -28,7 +28,7 @@
|
|||
"@logto/core-kit": "workspace:*",
|
||||
"@logto/schemas": "workspace:*",
|
||||
"@logto/shared": "workspace:*",
|
||||
"@silverhand/essentials": "2.4.1",
|
||||
"@silverhand/essentials": "^2.4.1",
|
||||
"@withtyped/postgres": "^0.8.1",
|
||||
"@withtyped/server": "^0.8.1",
|
||||
"accepts": "^1.3.8",
|
||||
|
|
|
@ -35,7 +35,7 @@
|
|||
"@parcel/transformer-svg-react": "2.8.3",
|
||||
"@silverhand/eslint-config": "2.0.1",
|
||||
"@silverhand/eslint-config-react": "2.0.1",
|
||||
"@silverhand/essentials": "2.4.1",
|
||||
"@silverhand/essentials": "^2.4.1",
|
||||
"@silverhand/ts-config": "2.0.3",
|
||||
"@silverhand/ts-config-react": "2.0.3",
|
||||
"@tsconfig/docusaurus": "^1.0.5",
|
||||
|
|
|
@ -35,7 +35,7 @@
|
|||
"@logto/phrases-ui": "workspace:*",
|
||||
"@logto/schemas": "workspace:*",
|
||||
"@logto/shared": "workspace:*",
|
||||
"@silverhand/essentials": "2.4.1",
|
||||
"@silverhand/essentials": "^2.4.1",
|
||||
"aws-sdk": "^2.1329.0",
|
||||
"chalk": "^5.0.0",
|
||||
"clean-deep": "^3.4.0",
|
||||
|
@ -51,6 +51,7 @@
|
|||
"iconv-lite": "0.6.3",
|
||||
"jose": "^4.11.0",
|
||||
"js-yaml": "^4.1.0",
|
||||
"keyv": "^4.5.2",
|
||||
"koa": "^2.13.1",
|
||||
"koa-body": "^5.0.0",
|
||||
"koa-compose": "^4.1.0",
|
||||
|
@ -63,6 +64,7 @@
|
|||
"lru-cache": "^7.14.1",
|
||||
"nanoid": "^4.0.0",
|
||||
"oidc-provider": "^8.0.0",
|
||||
"p-memoize": "^7.1.1",
|
||||
"p-retry": "^5.1.2",
|
||||
"pg-protocol": "^1.6.0",
|
||||
"roarr": "^7.11.0",
|
||||
|
|
30
packages/core/src/caches/well-known.ts
Normal file
30
packages/core/src/caches/well-known.ts
Normal file
|
@ -0,0 +1,30 @@
|
|||
import Keyv from 'keyv';
|
||||
import type { AnyAsyncFunction } from 'p-memoize';
|
||||
import pMemoize from 'p-memoize';
|
||||
|
||||
const cacheKeys = Object.freeze(['sie', 'sie-full', 'phrases', 'lng-tags'] as const);
|
||||
|
||||
/** Well-known data type key for cache. */
|
||||
export type WellKnownCacheKey = (typeof cacheKeys)[number];
|
||||
|
||||
// Not sure if we need guard value for `.has()` and `.get()`,
|
||||
// trust cache value for now.
|
||||
const wellKnownCache = new Keyv({ ttl: 300_000 /* 5 minutes */ });
|
||||
|
||||
/**
|
||||
* Use for centralized well-known data caching.
|
||||
*
|
||||
* WARN: You should store only well-known (public) data since it's a central cache.
|
||||
*/
|
||||
export const useWellKnownCache = <FunctionToMemoize extends AnyAsyncFunction>(
|
||||
tenantId: string,
|
||||
key: WellKnownCacheKey,
|
||||
run: FunctionToMemoize
|
||||
) =>
|
||||
pMemoize(run, {
|
||||
cacheKey: () => `${tenantId}:${key}`,
|
||||
cache: wellKnownCache,
|
||||
});
|
||||
|
||||
export const invalidateWellKnownCache = async (tenantId: string) =>
|
||||
wellKnownCache.delete(cacheKeys.map((key) => `${tenantId}:${key}` as const));
|
|
@ -11,6 +11,7 @@ import {
|
|||
zhCnTag,
|
||||
zhHkTag,
|
||||
} from '#src/__mocks__/custom-phrase.js';
|
||||
import { invalidateWellKnownCache } from '#src/caches/well-known.js';
|
||||
import RequestError from '#src/errors/RequestError/index.js';
|
||||
import { MockQueries } from '#src/test-utils/tenant.js';
|
||||
|
||||
|
@ -41,12 +42,15 @@ const findCustomPhraseByLanguageTag = jest.fn(async (languageTag: string) => {
|
|||
return mockCustomPhrase;
|
||||
});
|
||||
|
||||
const tenantId = 'mock_id';
|
||||
const { createPhraseLibrary } = await import('#src/libraries/phrase.js');
|
||||
const { getPhrases } = createPhraseLibrary(
|
||||
new MockQueries({ customPhrases: { findCustomPhraseByLanguageTag } })
|
||||
new MockQueries({ customPhrases: { findCustomPhraseByLanguageTag } }),
|
||||
tenantId
|
||||
);
|
||||
|
||||
afterEach(() => {
|
||||
afterEach(async () => {
|
||||
await invalidateWellKnownCache(tenantId);
|
||||
jest.clearAllMocks();
|
||||
});
|
||||
|
||||
|
|
|
@ -4,12 +4,16 @@ import type { CustomPhrase } from '@logto/schemas';
|
|||
import cleanDeep from 'clean-deep';
|
||||
import deepmerge from 'deepmerge';
|
||||
|
||||
import { useWellKnownCache } from '#src/caches/well-known.js';
|
||||
import type Queries from '#src/tenants/Queries.js';
|
||||
|
||||
export const createPhraseLibrary = (queries: Queries) => {
|
||||
const { findCustomPhraseByLanguageTag } = queries.customPhrases;
|
||||
export const createPhraseLibrary = (queries: Queries, tenantId: string) => {
|
||||
const { findCustomPhraseByLanguageTag, findAllCustomLanguageTags } = queries.customPhrases;
|
||||
|
||||
const getPhrases = async (supportedLanguage: string, customLanguages: string[]) => {
|
||||
const _getPhrases = async (
|
||||
supportedLanguage: string,
|
||||
customLanguages: string[]
|
||||
): Promise<LocalePhrase> => {
|
||||
if (!isBuiltInLanguageTag(supportedLanguage)) {
|
||||
return deepmerge<LocalePhrase, CustomPhrase>(
|
||||
resource.en,
|
||||
|
@ -27,5 +31,18 @@ export const createPhraseLibrary = (queries: Queries) => {
|
|||
);
|
||||
};
|
||||
|
||||
return { getPhrases };
|
||||
const getPhrases = useWellKnownCache(tenantId, 'phrases', _getPhrases);
|
||||
|
||||
const getAllCustomLanguageTags = useWellKnownCache(
|
||||
tenantId,
|
||||
'lng-tags',
|
||||
findAllCustomLanguageTags
|
||||
);
|
||||
|
||||
return {
|
||||
/** NOTE: This function is cached by the first parameter. */
|
||||
getPhrases,
|
||||
/** NOTE: This function is cached. */
|
||||
getAllCustomLanguageTags,
|
||||
};
|
||||
};
|
||||
|
|
|
@ -42,7 +42,7 @@ const getLogtoConnectors = jest.spyOn(connectorLibrary, 'getLogtoConnectors');
|
|||
|
||||
const { createSignInExperienceLibrary } = await import('./index.js');
|
||||
const { validateLanguageInfo, removeUnavailableSocialConnectorTargets } =
|
||||
createSignInExperienceLibrary(queries, connectorLibrary);
|
||||
createSignInExperienceLibrary(queries, connectorLibrary, 'mock_id');
|
||||
|
||||
beforeEach(() => {
|
||||
jest.clearAllMocks();
|
||||
|
|
|
@ -1,8 +1,11 @@
|
|||
import { connectorMetadataGuard } from '@logto/connector-kit';
|
||||
import { builtInLanguages } from '@logto/phrases-ui';
|
||||
import type { LanguageInfo, SignInExperience } from '@logto/schemas';
|
||||
import { ConnectorType } from '@logto/schemas';
|
||||
import type { ConnectorMetadata, LanguageInfo, SignInExperience } from '@logto/schemas';
|
||||
import { SignInExperiences, ConnectorType } from '@logto/schemas';
|
||||
import { deduplicate } from '@silverhand/essentials';
|
||||
import { z } from 'zod';
|
||||
|
||||
import { useWellKnownCache } from '#src/caches/well-known.js';
|
||||
import RequestError from '#src/errors/RequestError/index.js';
|
||||
import type { ConnectorLibrary } from '#src/libraries/connector.js';
|
||||
import type Queries from '#src/tenants/Queries.js';
|
||||
|
@ -15,12 +18,12 @@ export type SignInExperienceLibrary = ReturnType<typeof createSignInExperienceLi
|
|||
|
||||
export const createSignInExperienceLibrary = (
|
||||
queries: Queries,
|
||||
connectorLibrary: ConnectorLibrary
|
||||
{ getLogtoConnectors }: ConnectorLibrary,
|
||||
tenantId: string
|
||||
) => {
|
||||
const {
|
||||
customPhrases: { findAllCustomLanguageTags },
|
||||
signInExperiences: { findDefaultSignInExperience, updateDefaultSignInExperience },
|
||||
users: { hasActiveUsers },
|
||||
} = queries;
|
||||
|
||||
const validateLanguageInfo = async (languageInfo: LanguageInfo) => {
|
||||
|
@ -36,7 +39,7 @@ export const createSignInExperienceLibrary = (
|
|||
};
|
||||
|
||||
const removeUnavailableSocialConnectorTargets = async () => {
|
||||
const connectors = await connectorLibrary.getLogtoConnectors();
|
||||
const connectors = await getLogtoConnectors();
|
||||
const availableSocialConnectorTargets = deduplicate(
|
||||
connectors
|
||||
.filter(({ type }) => type === ConnectorType.Social)
|
||||
|
@ -52,11 +55,65 @@ export const createSignInExperienceLibrary = (
|
|||
});
|
||||
};
|
||||
|
||||
const getSignInExperience = async (): Promise<SignInExperience> => findDefaultSignInExperience();
|
||||
const getSignInExperience = useWellKnownCache(tenantId, 'sie', findDefaultSignInExperience);
|
||||
|
||||
const _getFullSignInExperience = async (): Promise<FullSignInExperience> => {
|
||||
const [signInExperience, logtoConnectors] = await Promise.all([
|
||||
getSignInExperience(),
|
||||
getLogtoConnectors(),
|
||||
]);
|
||||
|
||||
const forgotPassword = {
|
||||
phone: logtoConnectors.some(({ type }) => type === ConnectorType.Sms),
|
||||
email: logtoConnectors.some(({ type }) => type === ConnectorType.Email),
|
||||
};
|
||||
|
||||
const socialConnectors = signInExperience.socialSignInConnectorTargets.reduce<
|
||||
Array<ConnectorMetadata & { id: string }>
|
||||
>((previous, connectorTarget) => {
|
||||
const connectors = logtoConnectors.filter(
|
||||
({ metadata: { target } }) => target === connectorTarget
|
||||
);
|
||||
|
||||
return [
|
||||
...previous,
|
||||
...connectors.map(({ metadata, dbEntry: { id } }) => ({ ...metadata, id })),
|
||||
];
|
||||
}, []);
|
||||
|
||||
return {
|
||||
...signInExperience,
|
||||
socialConnectors,
|
||||
forgotPassword,
|
||||
};
|
||||
};
|
||||
|
||||
const getFullSignInExperience = useWellKnownCache(tenantId, 'sie-full', _getFullSignInExperience);
|
||||
|
||||
return {
|
||||
validateLanguageInfo,
|
||||
removeUnavailableSocialConnectorTargets,
|
||||
/** NOTE: This function is cached. */
|
||||
getSignInExperience,
|
||||
/** NOTE: This function is cached. */
|
||||
getFullSignInExperience,
|
||||
};
|
||||
};
|
||||
|
||||
export type ForgotPassword = {
|
||||
phone: boolean;
|
||||
email: boolean;
|
||||
};
|
||||
|
||||
export type ConnectorMetadataWithId = ConnectorMetadata & { id: string };
|
||||
|
||||
export type FullSignInExperience = SignInExperience & {
|
||||
socialConnectors: ConnectorMetadataWithId[];
|
||||
forgotPassword: ForgotPassword;
|
||||
};
|
||||
|
||||
export const guardFullSignInExperience: z.ZodType<FullSignInExperience> =
|
||||
SignInExperiences.guard.extend({
|
||||
socialConnectors: connectorMetadataGuard.extend({ id: z.string() }).array(),
|
||||
forgotPassword: z.object({ phone: z.boolean(), email: z.boolean() }),
|
||||
});
|
||||
|
|
|
@ -58,6 +58,10 @@ export const createCustomPhraseQueries = (pool: CommonQueryMethods) => {
|
|||
};
|
||||
|
||||
return {
|
||||
/**
|
||||
* NOTE: Use `getAllCustomLanguageTags()` from phrase library
|
||||
* if possible since that function leverages cache.
|
||||
*/
|
||||
findAllCustomLanguageTags,
|
||||
findAllCustomPhrases,
|
||||
findCustomPhraseByLanguageTag,
|
||||
|
|
|
@ -16,5 +16,12 @@ export const createSignInExperienceQueries = (pool: CommonQueryMethods) => {
|
|||
const findDefaultSignInExperience = async () =>
|
||||
buildFindEntityByIdWithPool(pool)(SignInExperiences)(id);
|
||||
|
||||
return { updateDefaultSignInExperience, findDefaultSignInExperience };
|
||||
return {
|
||||
updateDefaultSignInExperience,
|
||||
/**
|
||||
* NOTE: Use `getSignInExperience()` from sign-in experience library
|
||||
* if possible since that function leverages cache.
|
||||
*/
|
||||
findDefaultSignInExperience,
|
||||
};
|
||||
};
|
||||
|
|
|
@ -20,13 +20,11 @@ export default function socialRoutes<T extends AuthedMeRouter>(
|
|||
...[router, tenant]: RouterInitArgs<T>
|
||||
) {
|
||||
const {
|
||||
libraries: {
|
||||
connectors: { getLogtoConnectors, getLogtoConnectorById },
|
||||
},
|
||||
queries: {
|
||||
users: { findUserById, updateUserById, deleteUserIdentity, hasUserWithIdentity },
|
||||
signInExperiences: { findDefaultSignInExperience },
|
||||
},
|
||||
connectors: { getLogtoConnectors, getLogtoConnectorById },
|
||||
} = tenant;
|
||||
|
||||
router.get('/social/connectors', async (ctx, next) => {
|
||||
|
|
|
@ -105,7 +105,9 @@ const usersLibraries = {
|
|||
const adminUserRoutes = await pickDefault(import('./admin-user.js'));
|
||||
|
||||
describe('adminUserRoutes', () => {
|
||||
const tenantContext = new MockTenant(undefined, mockedQueries, { users: usersLibraries });
|
||||
const tenantContext = new MockTenant(undefined, mockedQueries, undefined, {
|
||||
users: usersLibraries,
|
||||
});
|
||||
const userRequest = createRequester({ authedRoutes: adminUserRoutes, tenantContext });
|
||||
|
||||
afterEach(() => {
|
||||
|
|
|
@ -62,6 +62,7 @@ const usersLibraries = {
|
|||
const tenantContext = new MockTenant(
|
||||
createMockProvider(jest.fn().mockResolvedValue(baseProviderMock)),
|
||||
undefined,
|
||||
undefined,
|
||||
{ users: usersLibraries, socials: socialsLibraries }
|
||||
);
|
||||
const { createRequester } = await import('#src/utils/test-utils.js');
|
||||
|
|
|
@ -76,24 +76,24 @@ const tenantContext = new MockTenant(
|
|||
undefined,
|
||||
{ connectors: connectorQueries },
|
||||
{
|
||||
signInExperiences: { removeUnavailableSocialConnectorTargets },
|
||||
connectors: {
|
||||
getLogtoConnectors,
|
||||
getLogtoConnectorById: async (connectorId: string) => {
|
||||
const connectors = await getLogtoConnectors();
|
||||
const connector = connectors.find(({ dbEntry }) => dbEntry.id === connectorId);
|
||||
assertThat(
|
||||
connector,
|
||||
new RequestError({
|
||||
code: 'entity.not_found',
|
||||
connectorId,
|
||||
status: 404,
|
||||
})
|
||||
);
|
||||
getLogtoConnectors,
|
||||
getLogtoConnectorById: async (connectorId: string) => {
|
||||
const connectors = await getLogtoConnectors();
|
||||
const connector = connectors.find(({ dbEntry }) => dbEntry.id === connectorId);
|
||||
assertThat(
|
||||
connector,
|
||||
new RequestError({
|
||||
code: 'entity.not_found',
|
||||
connectorId,
|
||||
status: 404,
|
||||
})
|
||||
);
|
||||
|
||||
return connector;
|
||||
},
|
||||
return connector;
|
||||
},
|
||||
},
|
||||
{
|
||||
signInExperiences: { removeUnavailableSocialConnectorTargets },
|
||||
}
|
||||
);
|
||||
|
||||
|
|
|
@ -21,7 +21,7 @@ import type { AuthedRouter, RouterInitArgs } from './types.js';
|
|||
const generateConnectorId = buildIdGenerator(12);
|
||||
|
||||
export default function connectorRoutes<T extends AuthedRouter>(
|
||||
...[router, { queries, libraries }]: RouterInitArgs<T>
|
||||
...[router, { queries, connectors, libraries }]: RouterInitArgs<T>
|
||||
) {
|
||||
const {
|
||||
findConnectorById,
|
||||
|
@ -31,8 +31,8 @@ export default function connectorRoutes<T extends AuthedRouter>(
|
|||
insertConnector,
|
||||
updateConnector,
|
||||
} = queries.connectors;
|
||||
const { getLogtoConnectorById, getLogtoConnectors } = connectors;
|
||||
const {
|
||||
connectors: { getLogtoConnectorById, getLogtoConnectors },
|
||||
signInExperiences: { removeUnavailableSocialConnectorTargets },
|
||||
} = libraries;
|
||||
|
||||
|
|
|
@ -44,10 +44,10 @@ const tenantContext = new MockTenant(
|
|||
undefined,
|
||||
{ connectors: { updateConnector } },
|
||||
{
|
||||
connectors: {
|
||||
getLogtoConnectors,
|
||||
getLogtoConnectorById,
|
||||
},
|
||||
getLogtoConnectors,
|
||||
getLogtoConnectorById,
|
||||
},
|
||||
{
|
||||
signInExperiences: {
|
||||
// eslint-disable-next-line @typescript-eslint/no-empty-function
|
||||
removeUnavailableSocialConnectorTargets: async () => {},
|
||||
|
|
|
@ -56,7 +56,8 @@ describe('submit action', () => {
|
|||
const tenant = new MockTenant(
|
||||
undefined,
|
||||
{ users: userQueries, signInExperiences: { updateDefaultSignInExperience: jest.fn() } },
|
||||
{ users: userLibraries, connectors: { getLogtoConnectorById } }
|
||||
{ getLogtoConnectorById },
|
||||
{ users: userLibraries }
|
||||
);
|
||||
const ctx = {
|
||||
...createContextWithRouteParameters(),
|
||||
|
|
|
@ -149,7 +149,7 @@ const parseUserProfile = async (
|
|||
export default async function submitInteraction(
|
||||
interaction: VerifiedInteractionResult,
|
||||
ctx: WithInteractionDetailsContext,
|
||||
{ provider, libraries, queries }: TenantContext,
|
||||
{ provider, libraries, connectors, queries }: TenantContext,
|
||||
log?: LogEntry
|
||||
) {
|
||||
const { hasActiveUsers, findUserById, updateUserById } = queries.users;
|
||||
|
@ -157,7 +157,6 @@ export default async function submitInteraction(
|
|||
|
||||
const {
|
||||
users: { generateUserId, insertUser },
|
||||
connectors,
|
||||
} = libraries;
|
||||
const { event, profile } = interaction;
|
||||
|
||||
|
|
|
@ -101,21 +101,21 @@ const tenantContext = new MockTenant(
|
|||
createMockProvider(jest.fn().mockResolvedValue(baseProviderMock)),
|
||||
undefined,
|
||||
{
|
||||
connectors: {
|
||||
getLogtoConnectorById: async (connectorId: string) => {
|
||||
const connector = await getLogtoConnectorByIdHelper(connectorId);
|
||||
getLogtoConnectorById: async (connectorId: string) => {
|
||||
const connector = await getLogtoConnectorByIdHelper(connectorId);
|
||||
|
||||
if (connector.type !== ConnectorType.Social) {
|
||||
throw new RequestError({
|
||||
code: 'entity.not_found',
|
||||
status: 404,
|
||||
});
|
||||
}
|
||||
if (connector.type !== ConnectorType.Social) {
|
||||
throw new RequestError({
|
||||
code: 'entity.not_found',
|
||||
status: 404,
|
||||
});
|
||||
}
|
||||
|
||||
// @ts-expect-error
|
||||
return connector as LogtoConnector;
|
||||
},
|
||||
// @ts-expect-error
|
||||
return connector as LogtoConnector;
|
||||
},
|
||||
},
|
||||
{
|
||||
signInExperiences: {
|
||||
getSignInExperience: jest.fn().mockResolvedValue(mockSignInExperience),
|
||||
},
|
||||
|
|
|
@ -18,7 +18,7 @@ const tenantContext = new MockTenant(
|
|||
{
|
||||
users: queries,
|
||||
},
|
||||
{ connectors: { getLogtoConnectorById } }
|
||||
{ getLogtoConnectorById }
|
||||
);
|
||||
|
||||
const findUserByIdentifier = await pickDefault(import('./find-user-by-identifier.js'));
|
||||
|
|
|
@ -3,12 +3,12 @@ import type TenantContext from '#src/tenants/TenantContext.js';
|
|||
import type { UserIdentity } from '../types/index.js';
|
||||
|
||||
export default async function findUserByIdentifier(
|
||||
{ queries, libraries }: TenantContext,
|
||||
{ queries, connectors }: TenantContext,
|
||||
identity: UserIdentity
|
||||
) {
|
||||
const { findUserByEmail, findUserByUsername, findUserByPhone, findUserByIdentity } =
|
||||
queries.users;
|
||||
const { getLogtoConnectorById } = libraries.connectors;
|
||||
const { getLogtoConnectorById } = connectors;
|
||||
|
||||
if ('username' in identity) {
|
||||
return findUserByUsername(identity.username);
|
||||
|
|
|
@ -11,7 +11,9 @@ const { mockEsm } = createMockUtils(jest);
|
|||
|
||||
const getUserInfoByAuthCode = jest.fn().mockResolvedValue({ id: 'foo' });
|
||||
|
||||
const tenant = new MockTenant(undefined, undefined, { socials: { getUserInfoByAuthCode } });
|
||||
const tenant = new MockTenant(undefined, undefined, undefined, {
|
||||
socials: { getUserInfoByAuthCode },
|
||||
});
|
||||
|
||||
mockEsm('#src/libraries/connector.js', () => ({
|
||||
getLogtoConnectorById: jest.fn().mockResolvedValue({
|
||||
|
|
|
@ -14,12 +14,10 @@ import type { SocialAuthorizationUrlPayload } from '../types/index.js';
|
|||
|
||||
export const createSocialAuthorizationUrl = async (
|
||||
ctx: WithLogContext,
|
||||
{ provider, libraries }: TenantContext,
|
||||
{ provider, connectors }: TenantContext,
|
||||
payload: SocialAuthorizationUrlPayload
|
||||
) => {
|
||||
const {
|
||||
connectors: { getLogtoConnectorById },
|
||||
} = libraries;
|
||||
const { getLogtoConnectorById } = connectors;
|
||||
|
||||
const { connectorId, state, redirectUri } = payload;
|
||||
assertThat(state && redirectUri, 'session.insufficient_info');
|
||||
|
|
|
@ -21,11 +21,7 @@ const getLogtoConnectorById = jest.fn().mockResolvedValue({
|
|||
metadata: { target: 'logto' },
|
||||
});
|
||||
|
||||
const tenantContext = new MockTenant(
|
||||
undefined,
|
||||
{ users: userQueries },
|
||||
{ connectors: { getLogtoConnectorById } }
|
||||
);
|
||||
const tenantContext = new MockTenant(undefined, { users: userQueries }, { getLogtoConnectorById });
|
||||
const verifyProfile = await pickDefault(import('./profile-verification.js'));
|
||||
|
||||
const identifiers: Identifier[] = [
|
||||
|
|
|
@ -19,11 +19,9 @@ const tenantContext = new MockTenant(
|
|||
},
|
||||
},
|
||||
{
|
||||
connectors: {
|
||||
getLogtoConnectorById: jest.fn().mockResolvedValue({
|
||||
metadata: { target: 'logto' },
|
||||
}),
|
||||
},
|
||||
getLogtoConnectorById: jest.fn().mockResolvedValue({
|
||||
metadata: { target: 'logto' },
|
||||
}),
|
||||
}
|
||||
);
|
||||
const verifyProfile = await pickDefault(import('./profile-verification.js'));
|
||||
|
|
|
@ -58,7 +58,7 @@ const verifyProfileIdentifiers = (
|
|||
};
|
||||
|
||||
const verifyProfileNotRegisteredByOtherUserAccount = async (
|
||||
{ queries, libraries }: TenantContext,
|
||||
{ queries, connectors }: TenantContext,
|
||||
{ username, email, phone, connectorId }: Profile,
|
||||
identifiers: Identifier[] = []
|
||||
) => {
|
||||
|
@ -97,7 +97,7 @@ const verifyProfileNotRegisteredByOtherUserAccount = async (
|
|||
if (connectorId) {
|
||||
const {
|
||||
metadata: { target },
|
||||
} = await libraries.connectors.getLogtoConnectorById(connectorId);
|
||||
} = await connectors.getLogtoConnectorById(connectorId);
|
||||
|
||||
const socialIdentifier = identifiers.find(
|
||||
(identifier): identifier is SocialIdentifier => identifier.key === 'social'
|
||||
|
|
|
@ -11,11 +11,9 @@ const { mockEsmDefault } = createMockUtils(jest);
|
|||
|
||||
const findUserByIdentifier = mockEsmDefault('../utils/find-user-by-identifier.js', () => jest.fn());
|
||||
|
||||
const tenant = new MockTenant(
|
||||
undefined,
|
||||
{},
|
||||
{ socials: { findSocialRelatedUser: jest.fn().mockResolvedValue(null) } }
|
||||
);
|
||||
const tenant = new MockTenant(undefined, undefined, undefined, {
|
||||
socials: { findSocialRelatedUser: jest.fn().mockResolvedValue(null) },
|
||||
});
|
||||
|
||||
const verifyUserAccount = await pickDefault(import('./user-identity-verification.js'));
|
||||
|
||||
|
|
|
@ -52,7 +52,7 @@ mockEsm('@logto/core-kit', () => ({
|
|||
buildIdGenerator: () => () => 'randomId',
|
||||
}));
|
||||
|
||||
const tenantContext = new MockTenant(undefined, { scopes, resources }, libraries);
|
||||
const tenantContext = new MockTenant(undefined, { scopes, resources }, undefined, libraries);
|
||||
|
||||
const resourceRoutes = await pickDefault(import('./resource.js'));
|
||||
|
||||
|
|
|
@ -20,6 +20,7 @@ const tenantContext = new MockTenant(
|
|||
}),
|
||||
},
|
||||
},
|
||||
undefined,
|
||||
{
|
||||
signInExperiences: {
|
||||
validateLanguageInfo,
|
||||
|
|
|
@ -1,6 +1,9 @@
|
|||
import type { SignInExperience, CreateSignInExperience } from '@logto/schemas';
|
||||
import { pickDefault, createMockUtils } from '@logto/shared/esm';
|
||||
|
||||
import { MockTenant } from '#src/test-utils/tenant.js';
|
||||
import { createRequester } from '#src/utils/test-utils.js';
|
||||
|
||||
import {
|
||||
mockFacebookConnector,
|
||||
mockGithubConnector,
|
||||
|
@ -17,8 +20,6 @@ import {
|
|||
mockPrivacyPolicyUrl,
|
||||
mockDemoSocialConnector,
|
||||
} from '#src/__mocks__/index.js';
|
||||
import { MockTenant } from '#src/test-utils/tenant.js';
|
||||
import { createRequester } from '#src/utils/test-utils.js';
|
||||
|
||||
const { jest } = import.meta;
|
||||
const { mockEsmWithActual } = createMockUtils(jest);
|
||||
|
@ -56,15 +57,9 @@ const mockDeleteConnectorById = jest.fn();
|
|||
|
||||
const tenantContext = new MockTenant(
|
||||
undefined,
|
||||
{
|
||||
signInExperiences,
|
||||
customPhrases: { findAllCustomLanguageTags: async () => [] },
|
||||
connectors: { deleteConnectorById: mockDeleteConnectorById },
|
||||
},
|
||||
{
|
||||
signInExperiences: { validateLanguageInfo },
|
||||
connectors: { getLogtoConnectors: mockGetLogtoConnectors },
|
||||
}
|
||||
{ signInExperiences, customPhrases: { findAllCustomLanguageTags: async () => [] } },
|
||||
{ getLogtoConnectors: async () => logtoConnectors },
|
||||
{ signInExperiences: { validateLanguageInfo } }
|
||||
);
|
||||
|
||||
const signInExperiencesRoutes = await pickDefault(import('./index.js'));
|
||||
|
|
|
@ -8,14 +8,14 @@ import koaGuard from '#src/middleware/koa-guard.js';
|
|||
import type { AuthedRouter, RouterInitArgs } from '../types.js';
|
||||
|
||||
export default function signInExperiencesRoutes<T extends AuthedRouter>(
|
||||
...[router, { queries, libraries }]: RouterInitArgs<T>
|
||||
...[router, { queries, libraries, connectors }]: RouterInitArgs<T>
|
||||
) {
|
||||
const { findDefaultSignInExperience, updateDefaultSignInExperience } = queries.signInExperiences;
|
||||
const { deleteConnectorById } = queries.connectors;
|
||||
const {
|
||||
signInExperiences: { validateLanguageInfo },
|
||||
connectors: { getLogtoConnectors },
|
||||
} = libraries;
|
||||
const { getLogtoConnectors } = connectors;
|
||||
|
||||
/**
|
||||
* As we only support single signInExperience settings for V1
|
||||
|
|
|
@ -23,13 +23,9 @@ const passcodeQueries = await mockEsmWithActual('#src/queries/passcode.js', () =
|
|||
const verificationCodeRoutes = await pickDefault(import('./verification-code.js'));
|
||||
|
||||
describe('Generic verification code flow triggered by management API', () => {
|
||||
const tenantContext = new MockTenant(
|
||||
undefined,
|
||||
{ passcodes: passcodeQueries },
|
||||
{
|
||||
passcodes: passcodeLibraries,
|
||||
}
|
||||
);
|
||||
const tenantContext = new MockTenant(undefined, { passcodes: passcodeQueries }, undefined, {
|
||||
passcodes: passcodeLibraries,
|
||||
});
|
||||
const verificationCodeRequest = createRequester({
|
||||
authedRoutes: verificationCodeRoutes,
|
||||
tenantContext,
|
||||
|
|
|
@ -4,6 +4,7 @@ import { pickDefault } from '@logto/shared/esm';
|
|||
|
||||
import { trTrTag, zhCnTag, zhHkTag } from '#src/__mocks__/custom-phrase.js';
|
||||
import { mockSignInExperience } from '#src/__mocks__/index.js';
|
||||
import { invalidateWellKnownCache } from '#src/caches/well-known.js';
|
||||
import { MockTenant } from '#src/test-utils/tenant.js';
|
||||
import { createRequester } from '#src/utils/test-utils.js';
|
||||
|
||||
|
@ -29,6 +30,7 @@ const tenantContext = new MockTenant(
|
|||
customPhrases: { findAllCustomLanguageTags: async () => [trTrTag, zhCnTag] },
|
||||
signInExperiences: { findDefaultSignInExperience },
|
||||
},
|
||||
undefined,
|
||||
{ phrases: { getPhrases: jest.fn().mockResolvedValue(en) } }
|
||||
);
|
||||
|
||||
|
@ -39,7 +41,8 @@ const phraseRequest = createRequester({
|
|||
tenantContext,
|
||||
});
|
||||
|
||||
afterEach(() => {
|
||||
afterEach(async () => {
|
||||
await invalidateWellKnownCache(tenantContext.id);
|
||||
jest.clearAllMocks();
|
||||
});
|
||||
|
||||
|
|
|
@ -4,6 +4,7 @@ import { pickDefault, createMockUtils } from '@logto/shared/esm';
|
|||
|
||||
import { zhCnTag } from '#src/__mocks__/custom-phrase.js';
|
||||
import { mockSignInExperience } from '#src/__mocks__/index.js';
|
||||
import { invalidateWellKnownCache } from '#src/caches/well-known.js';
|
||||
import Queries from '#src/tenants/Queries.js';
|
||||
import { createMockProvider } from '#src/test-utils/oidc-provider.js';
|
||||
import { MockTenant } from '#src/test-utils/tenant.js';
|
||||
|
@ -46,6 +47,7 @@ const getPhrases = jest.fn(async () => zhCN);
|
|||
const tenantContext = new MockTenant(
|
||||
createMockProvider(),
|
||||
{ customPhrases, signInExperiences: { findDefaultSignInExperience } },
|
||||
undefined,
|
||||
{ phrases: { getPhrases } }
|
||||
);
|
||||
|
||||
|
@ -57,11 +59,12 @@ const phraseRequest = createRequester({
|
|||
tenantContext,
|
||||
});
|
||||
|
||||
describe('when the application is not admin-console', () => {
|
||||
afterEach(() => {
|
||||
jest.clearAllMocks();
|
||||
});
|
||||
afterEach(async () => {
|
||||
await invalidateWellKnownCache(tenantContext.id);
|
||||
jest.clearAllMocks();
|
||||
});
|
||||
|
||||
describe('when the application is not admin-console', () => {
|
||||
it('should call findDefaultSignInExperience', async () => {
|
||||
await expect(phraseRequest.get('/.well-known/phrases')).resolves.toHaveProperty('status', 200);
|
||||
expect(findDefaultSignInExperience).toBeCalledTimes(1);
|
||||
|
@ -123,4 +126,16 @@ describe('when the application is not admin-console', () => {
|
|||
);
|
||||
expect(getPhrases).toBeCalledWith('fr', [customizedLanguage]);
|
||||
});
|
||||
|
||||
it('should use cache for continuous requests', async () => {
|
||||
const [response1, response2, response3] = await Promise.all([
|
||||
phraseRequest.get('/.well-known/phrases'),
|
||||
phraseRequest.get('/.well-known/phrases'),
|
||||
phraseRequest.get('/.well-known/phrases'),
|
||||
]);
|
||||
expect(findDefaultSignInExperience).toHaveBeenCalledTimes(1);
|
||||
expect(findAllCustomLanguageTags).toHaveBeenCalledTimes(1);
|
||||
expect(response1.body).toStrictEqual(response2.body);
|
||||
expect(response1.body).toStrictEqual(response3.body);
|
||||
});
|
||||
});
|
||||
|
|
|
@ -10,6 +10,7 @@ import {
|
|||
mockWechatConnector,
|
||||
mockWechatNativeConnector,
|
||||
} from '#src/__mocks__/index.js';
|
||||
import { invalidateWellKnownCache } from '#src/caches/well-known.js';
|
||||
|
||||
const { jest } = import.meta;
|
||||
const { mockEsm } = createMockUtils(jest);
|
||||
|
@ -32,34 +33,36 @@ const { createMockProvider } = await import('#src/test-utils/oidc-provider.js');
|
|||
const { MockTenant } = await import('#src/test-utils/tenant.js');
|
||||
const { createRequester } = await import('#src/utils/test-utils.js');
|
||||
|
||||
const provider = createMockProvider();
|
||||
const getLogtoConnectors = jest.fn(async () => {
|
||||
return [
|
||||
mockAliyunDmConnector,
|
||||
mockAliyunSmsConnector,
|
||||
mockFacebookConnector,
|
||||
mockGithubConnector,
|
||||
mockGoogleConnector,
|
||||
mockWechatConnector,
|
||||
mockWechatNativeConnector,
|
||||
];
|
||||
});
|
||||
const tenantContext = new MockTenant(
|
||||
provider,
|
||||
{
|
||||
signInExperiences: sieQueries,
|
||||
users: { hasActiveUsers: jest.fn().mockResolvedValue(true) },
|
||||
},
|
||||
{ getLogtoConnectors }
|
||||
);
|
||||
|
||||
describe('GET /.well-known/sign-in-exp', () => {
|
||||
afterEach(() => {
|
||||
afterEach(async () => {
|
||||
await invalidateWellKnownCache(tenantContext.id);
|
||||
jest.clearAllMocks();
|
||||
});
|
||||
|
||||
const provider = createMockProvider();
|
||||
const sessionRequest = createRequester({
|
||||
anonymousRoutes: wellKnownRoutes,
|
||||
tenantContext: new MockTenant(
|
||||
provider,
|
||||
{
|
||||
signInExperiences: sieQueries,
|
||||
users: { hasActiveUsers: jest.fn().mockResolvedValue(true) },
|
||||
},
|
||||
{
|
||||
connectors: {
|
||||
getLogtoConnectors: jest.fn(async () => [
|
||||
mockAliyunDmConnector,
|
||||
mockAliyunSmsConnector,
|
||||
mockFacebookConnector,
|
||||
mockGithubConnector,
|
||||
mockGoogleConnector,
|
||||
mockWechatConnector,
|
||||
mockWechatNativeConnector,
|
||||
]),
|
||||
},
|
||||
}
|
||||
),
|
||||
tenantContext,
|
||||
middlewares: [
|
||||
async (ctx, next) => {
|
||||
ctx.addLogContext = jest.fn();
|
||||
|
@ -96,4 +99,16 @@ describe('GET /.well-known/sign-in-exp', () => {
|
|||
],
|
||||
});
|
||||
});
|
||||
|
||||
it('should use cache for continuous requests', async () => {
|
||||
const [response1, response2, response3] = await Promise.all([
|
||||
sessionRequest.get('/.well-known/sign-in-exp'),
|
||||
sessionRequest.get('/.well-known/sign-in-exp'),
|
||||
sessionRequest.get('/.well-known/sign-in-exp'),
|
||||
]);
|
||||
expect(findDefaultSignInExperience).toHaveBeenCalledTimes(1);
|
||||
expect(getLogtoConnectors).toHaveBeenCalledTimes(1);
|
||||
expect(response1.body).toStrictEqual(response2.body);
|
||||
expect(response2.body).toStrictEqual(response3.body);
|
||||
});
|
||||
});
|
||||
|
|
|
@ -1,27 +1,22 @@
|
|||
import type { ConnectorMetadata } from '@logto/connector-kit';
|
||||
import { ConnectorType } from '@logto/connector-kit';
|
||||
import { isBuiltInLanguageTag } from '@logto/phrases-ui';
|
||||
import { adminTenantId } from '@logto/schemas';
|
||||
import { object, string } from 'zod';
|
||||
import { conditionalArray } from '@silverhand/essentials';
|
||||
import { z } from 'zod';
|
||||
|
||||
import { EnvSet, getTenantEndpoint } from '#src/env-set/index.js';
|
||||
import RequestError from '#src/errors/RequestError/index.js';
|
||||
import detectLanguage from '#src/i18n/detect-language.js';
|
||||
import { guardFullSignInExperience } from '#src/libraries/sign-in-experience/index.js';
|
||||
import koaGuard from '#src/middleware/koa-guard.js';
|
||||
|
||||
import type { AnonymousRouter, RouterInitArgs } from './types.js';
|
||||
|
||||
export default function wellKnownRoutes<T extends AnonymousRouter>(
|
||||
...[router, { queries, libraries, id }]: RouterInitArgs<T>
|
||||
...[router, { libraries, id }]: RouterInitArgs<T>
|
||||
) {
|
||||
const {
|
||||
customPhrases: { findAllCustomLanguageTags },
|
||||
signInExperiences: { findDefaultSignInExperience },
|
||||
} = queries;
|
||||
const {
|
||||
signInExperiences: { getSignInExperience },
|
||||
connectors: { getLogtoConnectors },
|
||||
phrases: { getPhrases },
|
||||
signInExperiences: { getSignInExperience, getFullSignInExperience },
|
||||
phrases: { getPhrases, getAllCustomLanguageTags },
|
||||
} = libraries;
|
||||
|
||||
if (id === adminTenantId) {
|
||||
|
@ -38,45 +33,24 @@ export default function wellKnownRoutes<T extends AnonymousRouter>(
|
|||
});
|
||||
}
|
||||
|
||||
router.get('/.well-known/sign-in-exp', async (ctx, next) => {
|
||||
const [signInExperience, logtoConnectors] = await Promise.all([
|
||||
getSignInExperience(),
|
||||
getLogtoConnectors(),
|
||||
]);
|
||||
router.get(
|
||||
'/.well-known/sign-in-exp',
|
||||
koaGuard({ response: guardFullSignInExperience, status: 200 }),
|
||||
async (ctx, next) => {
|
||||
ctx.body = await getFullSignInExperience();
|
||||
|
||||
const forgotPassword = {
|
||||
phone: logtoConnectors.some(({ type }) => type === ConnectorType.Sms),
|
||||
email: logtoConnectors.some(({ type }) => type === ConnectorType.Email),
|
||||
};
|
||||
|
||||
const socialConnectors = signInExperience.socialSignInConnectorTargets.reduce<
|
||||
Array<ConnectorMetadata & { id: string }>
|
||||
>((previous, connectorTarget) => {
|
||||
const connectors = logtoConnectors.filter(
|
||||
({ metadata: { target } }) => target === connectorTarget
|
||||
);
|
||||
|
||||
return [
|
||||
...previous,
|
||||
...connectors.map(({ metadata, dbEntry: { id } }) => ({ ...metadata, id })),
|
||||
];
|
||||
}, []);
|
||||
|
||||
ctx.body = {
|
||||
...signInExperience,
|
||||
socialConnectors,
|
||||
forgotPassword,
|
||||
};
|
||||
|
||||
return next();
|
||||
});
|
||||
return next();
|
||||
}
|
||||
);
|
||||
|
||||
router.get(
|
||||
'/.well-known/phrases',
|
||||
koaGuard({
|
||||
query: object({
|
||||
lng: string().optional(),
|
||||
query: z.object({
|
||||
lng: z.string().optional(),
|
||||
}),
|
||||
response: z.record(z.string().or(z.record(z.unknown()))),
|
||||
status: 200,
|
||||
}),
|
||||
async (ctx, next) => {
|
||||
const {
|
||||
|
@ -85,12 +59,14 @@ export default function wellKnownRoutes<T extends AnonymousRouter>(
|
|||
|
||||
const {
|
||||
languageInfo: { autoDetect, fallbackLanguage },
|
||||
} = await findDefaultSignInExperience();
|
||||
} = await getSignInExperience();
|
||||
|
||||
const targetLanguage = lng ? [lng] : [];
|
||||
const detectedLanguages = autoDetect ? detectLanguage(ctx) : [];
|
||||
const acceptableLanguages = [...targetLanguage, ...detectedLanguages, fallbackLanguage];
|
||||
const customLanguages = await findAllCustomLanguageTags();
|
||||
const acceptableLanguages = conditionalArray<string | string[]>(
|
||||
lng,
|
||||
autoDetect && detectLanguage(ctx),
|
||||
fallbackLanguage
|
||||
);
|
||||
const customLanguages = await getAllCustomLanguageTags();
|
||||
const language =
|
||||
acceptableLanguages.find(
|
||||
(tag) => isBuiltInLanguageTag(tag) || customLanguages.includes(tag)
|
||||
|
|
|
@ -1,5 +1,5 @@
|
|||
import { createApplicationLibrary } from '#src/libraries/application.js';
|
||||
import { createConnectorLibrary } from '#src/libraries/connector.js';
|
||||
import type { ConnectorLibrary } from '#src/libraries/connector.js';
|
||||
import { createHookLibrary } from '#src/libraries/hook.js';
|
||||
import { createPasscodeLibrary } from '#src/libraries/passcode.js';
|
||||
import { createPhraseLibrary } from '#src/libraries/phrase.js';
|
||||
|
@ -12,10 +12,9 @@ import { createVerificationStatusLibrary } from '#src/libraries/verification-sta
|
|||
import type Queries from './Queries.js';
|
||||
|
||||
export default class Libraries {
|
||||
connectors = createConnectorLibrary(this.queries);
|
||||
users = createUserLibrary(this.queries);
|
||||
signInExperiences = createSignInExperienceLibrary(this.queries, this.connectors);
|
||||
phrases = createPhraseLibrary(this.queries);
|
||||
signInExperiences = createSignInExperienceLibrary(this.queries, this.connectors, this.tenantId);
|
||||
phrases = createPhraseLibrary(this.queries, this.tenantId);
|
||||
resources = createResourceLibrary(this.queries);
|
||||
hooks = createHookLibrary(this.queries);
|
||||
socials = createSocialLibrary(this.queries, this.connectors);
|
||||
|
@ -23,5 +22,10 @@ export default class Libraries {
|
|||
applications = createApplicationLibrary(this.queries);
|
||||
verificationStatuses = createVerificationStatusLibrary(this.queries);
|
||||
|
||||
constructor(private readonly queries: Queries) {}
|
||||
constructor(
|
||||
public readonly tenantId: string,
|
||||
private readonly queries: Queries,
|
||||
// Explicitly passing connector library to eliminate dependency issue
|
||||
private readonly connectors: ConnectorLibrary
|
||||
) {}
|
||||
}
|
||||
|
|
|
@ -8,6 +8,7 @@ import mount from 'koa-mount';
|
|||
import type Provider from 'oidc-provider';
|
||||
|
||||
import { AdminApps, EnvSet, UserApps } from '#src/env-set/index.js';
|
||||
import { createConnectorLibrary } from '#src/libraries/connector.js';
|
||||
import koaConnectorErrorHandler from '#src/middleware/koa-connector-error-handler.js';
|
||||
import koaConsoleRedirectProxy from '#src/middleware/koa-console-redirect-proxy.js';
|
||||
import koaErrorHandler from '#src/middleware/koa-error-handler.js';
|
||||
|
@ -38,15 +39,18 @@ export default class Tenant implements TenantContext {
|
|||
#onRequestEmpty?: () => Promise<void>;
|
||||
|
||||
public readonly provider: Provider;
|
||||
public readonly queries: Queries;
|
||||
public readonly libraries: Libraries;
|
||||
public readonly run: MiddlewareType;
|
||||
|
||||
private readonly app: Koa;
|
||||
|
||||
private constructor(public readonly envSet: EnvSet, public readonly id: string) {
|
||||
const queries = new Queries(envSet.pool);
|
||||
const libraries = new Libraries(queries);
|
||||
// eslint-disable-next-line max-params
|
||||
private constructor(
|
||||
public readonly envSet: EnvSet,
|
||||
public readonly id: string,
|
||||
public readonly queries = new Queries(envSet.pool),
|
||||
public readonly connectors = createConnectorLibrary(queries),
|
||||
public readonly libraries = new Libraries(id, queries, connectors)
|
||||
) {
|
||||
const isAdminTenant = id === adminTenantId;
|
||||
const mountedApps = [
|
||||
...Object.values(UserApps),
|
||||
|
@ -54,8 +58,6 @@ export default class Tenant implements TenantContext {
|
|||
];
|
||||
|
||||
this.envSet = envSet;
|
||||
this.queries = queries;
|
||||
this.libraries = libraries;
|
||||
|
||||
// Init app
|
||||
const app = new Koa();
|
||||
|
@ -76,6 +78,7 @@ export default class Tenant implements TenantContext {
|
|||
id,
|
||||
provider,
|
||||
queries,
|
||||
connectors,
|
||||
libraries,
|
||||
envSet,
|
||||
};
|
||||
|
|
|
@ -1,6 +1,7 @@
|
|||
import type Provider from 'oidc-provider';
|
||||
|
||||
import type { EnvSet } from '#src/env-set/index.js';
|
||||
import type { ConnectorLibrary } from '#src/libraries/connector.js';
|
||||
|
||||
import type Libraries from './Libraries.js';
|
||||
import type Queries from './Queries.js';
|
||||
|
@ -10,5 +11,6 @@ export default abstract class TenantContext {
|
|||
public abstract readonly envSet: EnvSet;
|
||||
public abstract readonly provider: Provider;
|
||||
public abstract readonly queries: Queries;
|
||||
public abstract readonly connectors: ConnectorLibrary;
|
||||
public abstract readonly libraries: Libraries;
|
||||
}
|
||||
|
|
|
@ -1,5 +1,7 @@
|
|||
import { createMockPool, createMockQueryResult } from 'slonik';
|
||||
|
||||
import type { ConnectorLibrary } from '#src/libraries/connector.js';
|
||||
import { createConnectorLibrary } from '#src/libraries/connector.js';
|
||||
import Libraries from '#src/tenants/Libraries.js';
|
||||
import Queries from '#src/tenants/Queries.js';
|
||||
import type TenantContext from '#src/tenants/TenantContext.js';
|
||||
|
@ -46,15 +48,18 @@ export class MockTenant implements TenantContext {
|
|||
public id = 'mock_id';
|
||||
public envSet = mockEnvSet;
|
||||
public queries: Queries;
|
||||
public connectors: ConnectorLibrary;
|
||||
public libraries: Libraries;
|
||||
|
||||
constructor(
|
||||
public provider = createMockProvider(),
|
||||
queriesOverride?: Partial2<Queries>,
|
||||
connectorsOverride?: Partial<ConnectorLibrary>,
|
||||
librariesOverride?: Partial2<Libraries>
|
||||
) {
|
||||
this.queries = new MockQueries(queriesOverride);
|
||||
this.libraries = new Libraries(this.queries);
|
||||
this.connectors = { ...createConnectorLibrary(this.queries), ...connectorsOverride };
|
||||
this.libraries = new Libraries(this.id, this.queries, this.connectors);
|
||||
this.setPartial('libraries', librariesOverride);
|
||||
}
|
||||
|
||||
|
|
|
@ -28,7 +28,7 @@
|
|||
"@logto/schemas": "workspace:*",
|
||||
"@peculiar/webcrypto": "^1.3.3",
|
||||
"@silverhand/eslint-config": "2.0.1",
|
||||
"@silverhand/essentials": "2.4.1",
|
||||
"@silverhand/essentials": "^2.4.1",
|
||||
"@silverhand/ts-config": "2.0.3",
|
||||
"@types/expect-puppeteer": "^5.0.3",
|
||||
"@types/jest": "^29.4.0",
|
||||
|
|
|
@ -34,7 +34,7 @@
|
|||
},
|
||||
"dependencies": {
|
||||
"@logto/language-kit": "workspace:*",
|
||||
"@silverhand/essentials": "2.4.1",
|
||||
"@silverhand/essentials": "^2.4.1",
|
||||
"zod": "^3.20.2"
|
||||
},
|
||||
"devDependencies": {
|
||||
|
|
|
@ -34,7 +34,7 @@
|
|||
},
|
||||
"dependencies": {
|
||||
"@logto/language-kit": "workspace:*",
|
||||
"@silverhand/essentials": "2.4.1",
|
||||
"@silverhand/essentials": "^2.4.1",
|
||||
"zod": "^3.20.2"
|
||||
},
|
||||
"devDependencies": {
|
||||
|
|
|
@ -41,7 +41,7 @@
|
|||
},
|
||||
"devDependencies": {
|
||||
"@silverhand/eslint-config": "2.0.1",
|
||||
"@silverhand/essentials": "2.4.1",
|
||||
"@silverhand/essentials": "^2.4.1",
|
||||
"@silverhand/ts-config": "2.0.3",
|
||||
"@types/inquirer": "^9.0.0",
|
||||
"@types/jest": "^29.4.0",
|
||||
|
|
|
@ -56,7 +56,7 @@
|
|||
"dependencies": {
|
||||
"@logto/core-kit": "workspace:*",
|
||||
"@logto/schemas": "workspace:*",
|
||||
"@silverhand/essentials": "2.4.1",
|
||||
"@silverhand/essentials": "^2.4.1",
|
||||
"chalk": "^5.0.0",
|
||||
"find-up": "^6.3.0",
|
||||
"nanoid": "^4.0.0",
|
||||
|
|
|
@ -33,7 +33,7 @@
|
|||
},
|
||||
"dependencies": {
|
||||
"@logto/language-kit": "workspace:*",
|
||||
"@silverhand/essentials": "2.4.1"
|
||||
"@silverhand/essentials": "^2.4.1"
|
||||
},
|
||||
"optionalDependencies": {
|
||||
"zod": "^3.20.2"
|
||||
|
|
|
@ -136,7 +136,7 @@ const connectorConfigFormItemGuard = z.discriminatedUnion('type', [
|
|||
|
||||
export type ConnectorConfigFormItem = z.infer<typeof connectorConfigFormItemGuard>;
|
||||
|
||||
const connectorMetadataGuard = z.object({
|
||||
export const connectorMetadataGuard = z.object({
|
||||
id: z.string(),
|
||||
target: z.string(),
|
||||
platform: z.nativeEnum(ConnectorPlatform).nullable(),
|
||||
|
|
|
@ -50,7 +50,7 @@
|
|||
"@jest/types": "^29.0.3",
|
||||
"@silverhand/eslint-config": "2.0.1",
|
||||
"@silverhand/eslint-config-react": "2.0.1",
|
||||
"@silverhand/essentials": "2.4.1",
|
||||
"@silverhand/essentials": "^2.4.1",
|
||||
"@silverhand/ts-config": "2.0.3",
|
||||
"@types/color": "^3.0.3",
|
||||
"@types/jest": "^29.4.0",
|
||||
|
|
|
@ -33,7 +33,7 @@
|
|||
"@react-spring/web": "^9.6.1",
|
||||
"@silverhand/eslint-config": "2.0.1",
|
||||
"@silverhand/eslint-config-react": "2.0.1",
|
||||
"@silverhand/essentials": "2.4.1",
|
||||
"@silverhand/essentials": "^2.4.1",
|
||||
"@silverhand/jest-config": "1.2.2",
|
||||
"@silverhand/ts-config": "2.0.3",
|
||||
"@silverhand/ts-config-react": "2.0.3",
|
||||
|
|
37
pnpm-lock.yaml
generated
37
pnpm-lock.yaml
generated
|
@ -32,7 +32,7 @@ importers:
|
|||
'@logto/schemas': workspace:*
|
||||
'@logto/shared': workspace:*
|
||||
'@silverhand/eslint-config': 2.0.1
|
||||
'@silverhand/essentials': 2.4.1
|
||||
'@silverhand/essentials': ^2.4.1
|
||||
'@silverhand/ts-config': 2.0.3
|
||||
'@types/inquirer': ^9.0.0
|
||||
'@types/jest': ^29.4.0
|
||||
|
@ -116,7 +116,7 @@ importers:
|
|||
'@logto/schemas': workspace:*
|
||||
'@logto/shared': workspace:*
|
||||
'@silverhand/eslint-config': 2.0.1
|
||||
'@silverhand/essentials': 2.4.1
|
||||
'@silverhand/essentials': ^2.4.1
|
||||
'@silverhand/jest-config': ^2.0.1
|
||||
'@silverhand/ts-config': 2.0.3
|
||||
'@types/accepts': ^1.3.5
|
||||
|
@ -196,7 +196,7 @@ importers:
|
|||
'@parcel/transformer-svg-react': 2.8.3
|
||||
'@silverhand/eslint-config': 2.0.1
|
||||
'@silverhand/eslint-config-react': 2.0.1
|
||||
'@silverhand/essentials': 2.4.1
|
||||
'@silverhand/essentials': ^2.4.1
|
||||
'@silverhand/ts-config': 2.0.3
|
||||
'@silverhand/ts-config-react': 2.0.3
|
||||
'@tsconfig/docusaurus': ^1.0.5
|
||||
|
@ -346,7 +346,7 @@ importers:
|
|||
'@logto/schemas': workspace:*
|
||||
'@logto/shared': workspace:*
|
||||
'@silverhand/eslint-config': 2.0.1
|
||||
'@silverhand/essentials': 2.4.1
|
||||
'@silverhand/essentials': ^2.4.1
|
||||
'@silverhand/ts-config': 2.0.3
|
||||
'@types/debug': ^4.1.7
|
||||
'@types/etag': ^1.8.1
|
||||
|
@ -385,6 +385,7 @@ importers:
|
|||
jest-matcher-specific-error: ^1.0.0
|
||||
jose: ^4.11.0
|
||||
js-yaml: ^4.1.0
|
||||
keyv: ^4.5.2
|
||||
koa: ^2.13.1
|
||||
koa-body: ^5.0.0
|
||||
koa-compose: ^4.1.0
|
||||
|
@ -401,6 +402,7 @@ importers:
|
|||
nodemon: ^2.0.19
|
||||
oidc-provider: ^8.0.0
|
||||
openapi-types: ^12.0.0
|
||||
p-memoize: ^7.1.1
|
||||
p-retry: ^5.1.2
|
||||
pg-protocol: ^1.6.0
|
||||
prettier: ^2.8.2
|
||||
|
@ -442,6 +444,7 @@ importers:
|
|||
iconv-lite: 0.6.3
|
||||
jose: 4.11.0
|
||||
js-yaml: 4.1.0
|
||||
keyv: 4.5.2
|
||||
koa: 2.13.4
|
||||
koa-body: 5.0.0
|
||||
koa-compose: 4.1.0
|
||||
|
@ -454,6 +457,7 @@ importers:
|
|||
lru-cache: 7.14.1
|
||||
nanoid: 4.0.0
|
||||
oidc-provider: 8.0.0
|
||||
p-memoize: 7.1.1
|
||||
p-retry: 5.1.2
|
||||
pg-protocol: 1.6.0
|
||||
roarr: 7.11.0
|
||||
|
@ -573,7 +577,7 @@ importers:
|
|||
'@logto/schemas': workspace:*
|
||||
'@peculiar/webcrypto': ^1.3.3
|
||||
'@silverhand/eslint-config': 2.0.1
|
||||
'@silverhand/essentials': 2.4.1
|
||||
'@silverhand/essentials': ^2.4.1
|
||||
'@silverhand/ts-config': 2.0.3
|
||||
'@types/expect-puppeteer': ^5.0.3
|
||||
'@types/jest': ^29.4.0
|
||||
|
@ -625,7 +629,7 @@ importers:
|
|||
specifiers:
|
||||
'@logto/language-kit': workspace:*
|
||||
'@silverhand/eslint-config': 2.0.1
|
||||
'@silverhand/essentials': 2.4.1
|
||||
'@silverhand/essentials': ^2.4.1
|
||||
'@silverhand/ts-config': 2.0.3
|
||||
eslint: ^8.34.0
|
||||
lint-staged: ^13.0.0
|
||||
|
@ -648,7 +652,7 @@ importers:
|
|||
specifiers:
|
||||
'@logto/language-kit': workspace:*
|
||||
'@silverhand/eslint-config': 2.0.1
|
||||
'@silverhand/essentials': 2.4.1
|
||||
'@silverhand/essentials': ^2.4.1
|
||||
'@silverhand/ts-config': 2.0.3
|
||||
buffer: ^5.7.1
|
||||
eslint: ^8.34.0
|
||||
|
@ -677,7 +681,7 @@ importers:
|
|||
'@logto/phrases': workspace:*
|
||||
'@logto/phrases-ui': workspace:*
|
||||
'@silverhand/eslint-config': 2.0.1
|
||||
'@silverhand/essentials': 2.4.1
|
||||
'@silverhand/essentials': ^2.4.1
|
||||
'@silverhand/ts-config': 2.0.3
|
||||
'@types/inquirer': ^9.0.0
|
||||
'@types/jest': ^29.4.0
|
||||
|
@ -730,7 +734,7 @@ importers:
|
|||
'@logto/core-kit': workspace:*
|
||||
'@logto/schemas': workspace:*
|
||||
'@silverhand/eslint-config': 2.0.1
|
||||
'@silverhand/essentials': 2.4.1
|
||||
'@silverhand/essentials': ^2.4.1
|
||||
'@silverhand/ts-config': 2.0.3
|
||||
'@types/jest': ^29.4.0
|
||||
'@types/node': ^18.11.18
|
||||
|
@ -767,7 +771,7 @@ importers:
|
|||
specifiers:
|
||||
'@logto/language-kit': workspace:*
|
||||
'@silverhand/eslint-config': 2.0.1
|
||||
'@silverhand/essentials': 2.4.1
|
||||
'@silverhand/essentials': ^2.4.1
|
||||
'@silverhand/ts-config': 2.0.3
|
||||
'@types/node': ^18.11.18
|
||||
eslint: ^8.34.0
|
||||
|
@ -797,7 +801,7 @@ importers:
|
|||
'@logto/language-kit': workspace:*
|
||||
'@silverhand/eslint-config': 2.0.1
|
||||
'@silverhand/eslint-config-react': 2.0.1
|
||||
'@silverhand/essentials': 2.4.1
|
||||
'@silverhand/essentials': ^2.4.1
|
||||
'@silverhand/ts-config': 2.0.3
|
||||
'@types/color': ^3.0.3
|
||||
'@types/jest': ^29.4.0
|
||||
|
@ -886,7 +890,7 @@ importers:
|
|||
'@react-spring/web': ^9.6.1
|
||||
'@silverhand/eslint-config': 2.0.1
|
||||
'@silverhand/eslint-config-react': 2.0.1
|
||||
'@silverhand/essentials': 2.4.1
|
||||
'@silverhand/essentials': ^2.4.1
|
||||
'@silverhand/jest-config': 1.2.2
|
||||
'@silverhand/ts-config': 2.0.3
|
||||
'@silverhand/ts-config-react': 2.0.3
|
||||
|
@ -10638,7 +10642,6 @@ 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==}
|
||||
|
@ -11233,6 +11236,14 @@ 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-retry/5.1.2:
|
||||
resolution: {integrity: sha512-couX95waDu98NfNZV+i/iLt+fdVxmI7CbrrdC2uDWfPdUAApyxT4wmDlyOtR5KtTDmkDO0zDScDjDou9YHhd9g==}
|
||||
engines: {node: ^12.20.0 || ^14.13.1 || >=16.0.0}
|
||||
|
|
Loading…
Add table
Reference in a new issue