0
Fork 0
mirror of https://github.com/logto-io/logto.git synced 2024-12-16 20:26:19 -05:00

refactor(core,toolkit): extract connector loading code to toolkit (#3400)

This commit is contained in:
wangsijie 2023-03-14 20:26:42 +08:00 committed by GitHub
parent 1d1cd46ccf
commit 2bd5f36fc3
No known key found for this signature in database
GPG key ID: 4AEE18F83AFDEB23
28 changed files with 265 additions and 243 deletions

View file

@ -43,6 +43,7 @@
"url": "https://github.com/logto-io/logto/issues"
},
"dependencies": {
"@logto/connector-kit": "workspace:1.0.0-rc.2",
"@logto/core-kit": "workspace:*",
"@logto/schemas": "workspace:*",
"@logto/shared": "workspace:*",

View file

@ -0,0 +1,53 @@
import chalk from 'chalk';
import { notImplemented } from './consts.js';
import { loadConnector } from './loader.js';
import type { ConnectorFactory, ConnectorPackage } from './types.js';
import { parseMetadata, validateConnectorModule } from './utils.js';
// eslint-disable-next-line @silverhand/fp/no-let
let cachedConnectorFactories: ConnectorFactory[] | undefined;
export const loadConnectorFactories = async (
connectorPackages: ConnectorPackage[],
ignoreVersionMismatch: boolean
) => {
if (cachedConnectorFactories) {
return cachedConnectorFactories;
}
const connectorFactories = await Promise.all(
connectorPackages.map(async ({ path: packagePath, name }) => {
try {
const createConnector = await loadConnector(packagePath, ignoreVersionMismatch);
const rawConnector = await createConnector({ getConfig: notImplemented });
validateConnectorModule(rawConnector);
return {
metadata: await parseMetadata(rawConnector.metadata, packagePath),
type: rawConnector.type,
createConnector,
path: packagePath,
};
} catch (error: unknown) {
if (error instanceof Error) {
console.log(
`${chalk.red(
`[load-connector] skip ${chalk.bold(name)} due to error: ${error.message}`
)}`
);
return;
}
throw error;
}
})
);
// eslint-disable-next-line @silverhand/fp/no-mutation
cachedConnectorFactories = connectorFactories.filter(
(connectorFactory): connectorFactory is ConnectorFactory => connectorFactory !== undefined
);
return cachedConnectorFactories;
};

View file

@ -0,0 +1,4 @@
export * from './factories.js';
export * from './types.js';
export * from './consts.js';
export * from './utils.js';

View file

@ -2,15 +2,14 @@ import path from 'path';
import type { AllConnector, CreateConnector } from '@logto/connector-kit';
import connectorKitMeta from '@logto/connector-kit/package.json' assert { type: 'json' };
import { isKeyInObject } from '@logto/shared';
import { satisfies } from 'semver';
import { EnvSet } from '#src/env-set/index.js';
import { isKeyInObject } from './utils.js';
const connectorKit = '@logto/connector-kit';
const { version: currentVersion } = connectorKitMeta;
const checkConnectorKitVersion = (dependencies: unknown) => {
const checkConnectorKitVersion = (dependencies: unknown, ignoreVersionMismatch: boolean) => {
if (isKeyInObject(dependencies, connectorKit)) {
const value = dependencies[connectorKit];
@ -21,7 +20,7 @@ const checkConnectorKitVersion = (dependencies: unknown) => {
const message = `Connector requires ${connectorKit} to be ${value}, but the version here is ${currentVersion}.`;
if (EnvSet.values.isIntegrationTest || EnvSet.values.ignoreConnectorVersionCheck) {
if (ignoreVersionMismatch) {
console.warn(`[warn] ${message}\n\nThis is highly discouraged in production.`);
return;
@ -35,7 +34,8 @@ const checkConnectorKitVersion = (dependencies: unknown) => {
};
export const loadConnector = async (
connectorPath: string
connectorPath: string,
ignoreVersionMismatch: boolean
): Promise<CreateConnector<AllConnector>> => {
const {
default: { dependencies },
@ -44,7 +44,7 @@ export const loadConnector = async (
assert: { type: 'json' },
})) as { default: Record<string, unknown> };
checkConnectorKitVersion(dependencies);
checkConnectorKitVersion(dependencies, ignoreVersionMismatch);
const loaded: unknown = await import(path.join(connectorPath, 'lib/index.js'));

View file

@ -0,0 +1,17 @@
import type { AllConnector, CreateConnector } from '@logto/connector-kit';
/**
* Dynamic loaded connector type.
*/
export type ConnectorFactory<T extends AllConnector = AllConnector> = Pick<
T,
'type' | 'metadata'
> & {
createConnector: CreateConnector<AllConnector>;
path: string;
};
export type ConnectorPackage = {
name: string;
path: string;
};

View file

@ -0,0 +1,85 @@
import { existsSync } from 'fs';
import { readFile } from 'fs/promises';
import path from 'path';
import type { AllConnector, BaseConnector, GetConnectorConfig } from '@logto/connector-kit';
import { ConnectorError, ConnectorErrorCodes, ConnectorType } from '@logto/connector-kit';
import { notImplemented } from './consts.js';
import type { ConnectorFactory } from './types.js';
export function validateConnectorModule(
connector: Partial<BaseConnector<ConnectorType>>
): asserts connector is BaseConnector<ConnectorType> {
if (!connector.metadata) {
throw new ConnectorError(ConnectorErrorCodes.InvalidMetadata);
}
if (!connector.configGuard) {
throw new ConnectorError(ConnectorErrorCodes.InvalidConfigGuard);
}
if (!connector.type || !Object.values(ConnectorType).includes(connector.type)) {
throw new ConnectorError(ConnectorErrorCodes.UnexpectedType);
}
}
export const readUrl = async (
url: string,
baseUrl: string,
type: 'text' | 'svg'
): Promise<string> => {
if (!url) {
return url;
}
if (type !== 'text' && url.startsWith('http')) {
return url;
}
if (!existsSync(path.join(baseUrl, url))) {
return url;
}
if (type === 'svg') {
const data = await readFile(path.join(baseUrl, url));
return `data:image/svg+xml;base64,${data.toString('base64')}`;
}
return readFile(path.join(baseUrl, url), 'utf8');
};
export const parseMetadata = async (
metadata: AllConnector['metadata'],
packagePath: string
): Promise<AllConnector['metadata']> => {
return {
...metadata,
logo: await readUrl(metadata.logo, packagePath, 'svg'),
logoDark: metadata.logoDark && (await readUrl(metadata.logoDark, packagePath, 'svg')),
readme: await readUrl(metadata.readme, packagePath, 'text'),
configTemplate:
metadata.configTemplate && (await readUrl(metadata.configTemplate, packagePath, 'text')),
};
};
export const buildRawConnector = async (
connectorFactory: ConnectorFactory,
getConnectorConfig?: GetConnectorConfig
) => {
const { createConnector, path: packagePath } = connectorFactory;
const rawConnector = await createConnector({
getConfig: getConnectorConfig ?? notImplemented,
});
validateConnectorModule(rawConnector);
const rawMetadata = await parseMetadata(rawConnector.metadata, packagePath);
return { rawConnector, rawMetadata };
};
export const isKeyInObject = <Key extends string>(
object: unknown,
key: Key
): object is Record<string, unknown> & Record<Key, unknown> =>
object !== null && typeof object === 'object' && key in object;

View file

@ -1,9 +1,10 @@
import type { ConnectorFactory } from '@logto/cli/lib/connector/index.js';
import { ConnectorPlatform } from '@logto/connector-kit';
import type { Connector } from '@logto/schemas';
import { ConnectorType } from '@logto/schemas';
import { any } from 'zod';
import type { LogtoConnector, ConnectorFactory } from '#src/utils/connectors/types.js';
import type { LogtoConnector } from '#src/utils/connectors/types.js';
import {
mockConnector0,

View file

@ -9,7 +9,7 @@ import SystemContext from './tenants/SystemContext.js';
dotenv.config({ path: await findUp('.env', {}) });
// Import after env has been configured
const { loadConnectorFactories } = await import('./utils/connectors/factories.js');
const { loadConnectorFactories } = await import('./utils/connectors/index.js');
const { EnvSet } = await import('./env-set/index.js');
const { default: initI18n } = await import('./i18n/init.js');
const { tenantPool, checkRowLevelSecurity } = await import('./tenants/index.js');

View file

@ -1,12 +1,11 @@
import { buildRawConnector, defaultConnectorMethods } from '@logto/cli/lib/connector/index.js';
import type { AllConnector } from '@logto/connector-kit';
import { validateConfig } from '@logto/connector-kit';
import RequestError from '#src/errors/RequestError/index.js';
import type Queries from '#src/tenants/Queries.js';
import assertThat from '#src/utils/assert-that.js';
import { defaultConnectorMethods } from '#src/utils/connectors/consts.js';
import { loadConnectorFactories } from '#src/utils/connectors/factories.js';
import { buildRawConnector } from '#src/utils/connectors/index.js';
import { loadConnectorFactories } from '#src/utils/connectors/index.js';
import type { LogtoConnector } from '#src/utils/connectors/types.js';
export type ConnectorLibrary = ReturnType<typeof createConnectorLibrary>;

View file

@ -1,3 +1,4 @@
import { defaultConnectorMethods } from '@logto/cli/lib/connector/index.js';
import { ConnectorType, VerificationCodeType } from '@logto/connector-kit';
import { Passcode } from '@logto/schemas';
import { any } from 'zod';
@ -5,7 +6,6 @@ import { any } from 'zod';
import { mockConnector, mockMetadata } from '#src/__mocks__/index.js';
import RequestError from '#src/errors/RequestError/index.js';
import { MockQueries } from '#src/test-utils/tenant.js';
import { defaultConnectorMethods } from '#src/utils/connectors/consts.js';
import {
createPasscodeLibrary,

View file

@ -1,3 +1,4 @@
import { notImplemented } from '@logto/cli/lib/connector/index.js';
import { ConnectorType } from '@logto/schemas';
import { has } from '@silverhand/essentials';
import { object, record, string, unknown } from 'zod';
@ -5,7 +6,6 @@ import { object, record, string, unknown } from 'zod';
import RequestError from '#src/errors/RequestError/index.js';
import koaGuard from '#src/middleware/koa-guard.js';
import assertThat from '#src/utils/assert-that.js';
import { notImplemented } from '#src/utils/connectors/consts.js';
import { transpileLogtoConnector } from '#src/utils/connectors/index.js';
import type { RouterInitArgs } from '../routes/types.js';

View file

@ -1,11 +1,12 @@
/* eslint-disable max-lines */
import type { EmailConnector, SmsConnector } from '@logto/connector-kit';
import { defaultConnectorMethods } from '@logto/cli/lib/connector/index.js';
import {
ConnectorError,
ConnectorErrorCodes,
ConnectorPlatform,
VerificationCodeType,
} from '@logto/connector-kit';
import type { EmailConnector, SmsConnector } from '@logto/connector-kit';
import type { Connector } from '@logto/schemas';
import { ConnectorType } from '@logto/schemas';
import { pickDefault, createMockUtils } from '@logto/shared/esm';
@ -26,7 +27,6 @@ import RequestError from '#src/errors/RequestError/index.js';
import Queries from '#src/tenants/Queries.js';
import { MockTenant } from '#src/test-utils/tenant.js';
import assertThat from '#src/utils/assert-that.js';
import { defaultConnectorMethods } from '#src/utils/connectors/consts.js';
import type { LogtoConnector } from '#src/utils/connectors/types.js';
import { createRequester } from '#src/utils/test-utils.js';
@ -57,11 +57,14 @@ const {
// eslint-disable-next-line @typescript-eslint/ban-types
const getLogtoConnectors = jest.fn<Promise<LogtoConnector[]>, []>();
const { loadConnectorFactories } = mockEsm('#src/utils/connectors/factories.js', () => ({
loadConnectorFactories: jest.fn(),
}));
const { loadConnectorFactories } = await mockEsmWithActual(
'#src/utils/connectors/index.js',
() => ({
loadConnectorFactories: jest.fn(),
})
);
const { buildRawConnector } = await mockEsmWithActual('#src/utils/connectors/index.js', () => ({
const { buildRawConnector } = await mockEsmWithActual('@logto/cli/lib/connector/index.js', () => ({
buildRawConnector: jest.fn(),
}));

View file

@ -1,3 +1,4 @@
import { buildRawConnector } from '@logto/cli/lib/connector/index.js';
import { VerificationCodeType, validateConfig } from '@logto/connector-kit';
import { emailRegEx, phoneRegEx, buildIdGenerator } from '@logto/core-kit';
import { arbitraryObjectGuard, Connectors, ConnectorType } from '@logto/schemas';
@ -7,9 +8,8 @@ import { string, object } from 'zod';
import RequestError from '#src/errors/RequestError/index.js';
import koaGuard from '#src/middleware/koa-guard.js';
import assertThat from '#src/utils/assert-that.js';
import { loadConnectorFactories } from '#src/utils/connectors/factories.js';
import {
buildRawConnector,
loadConnectorFactories,
transpileConnectorFactory,
transpileLogtoConnector,
} from '#src/utils/connectors/index.js';

View file

@ -1,91 +0,0 @@
import { existsSync } from 'fs';
import { fileURLToPath } from 'node:url';
import path from 'path';
import { connectorDirectory } from '@logto/cli/lib/constants.js';
import { getConnectorPackagesFromDirectory } from '@logto/cli/lib/utils.js';
import { findPackage } from '@logto/shared';
import { deduplicate } from '@silverhand/essentials';
import chalk from 'chalk';
import RequestError from '#src/errors/RequestError/index.js';
import type { ConnectorFactory } from '#src/utils/connectors/types.js';
import { notImplemented } from './consts.js';
import { parseMetadata, validateConnectorModule } from './index.js';
import { loadConnector } from './loader.js';
const checkDuplicateConnectorFactoriesId = (connectorFactories: ConnectorFactory[]) => {
const connectorFactoryIds = connectorFactories.map(({ metadata }) => metadata.id);
const deduplicatedConnectorFactoryIds = deduplicate(connectorFactoryIds);
if (connectorFactoryIds.length !== deduplicatedConnectorFactoryIds.length) {
const duplicatedConnectorFactoryIds = deduplicatedConnectorFactoryIds.filter(
(deduplicateId) => connectorFactoryIds.filter((id) => id === deduplicateId).length > 1
);
throw new RequestError({
code: 'connector.more_than_one_connector_factory',
status: 422,
connectorIds: duplicatedConnectorFactoryIds.map((id) => `${id}`).join(', '),
});
}
};
// eslint-disable-next-line @silverhand/fp/no-let
let cachedConnectorFactories: ConnectorFactory[] | undefined;
export const loadConnectorFactories = async () => {
if (cachedConnectorFactories) {
checkDuplicateConnectorFactoriesId(cachedConnectorFactories);
return cachedConnectorFactories;
}
const currentDirname = path.dirname(fileURLToPath(import.meta.url));
const coreDirectory = await findPackage(currentDirname);
const directory = coreDirectory && path.join(coreDirectory, connectorDirectory);
if (!directory || !existsSync(directory)) {
return [];
}
const connectorPackages = await getConnectorPackagesFromDirectory(directory);
const connectorFactories = await Promise.all(
connectorPackages.map(async ({ path: packagePath, name }) => {
try {
const createConnector = await loadConnector(packagePath);
const rawConnector = await createConnector({ getConfig: notImplemented });
validateConnectorModule(rawConnector);
return {
metadata: await parseMetadata(rawConnector.metadata, packagePath),
type: rawConnector.type,
createConnector,
path: packagePath,
};
} catch (error: unknown) {
if (error instanceof Error) {
console.log(
`${chalk.red(
`[load-connector] skip ${chalk.bold(name)} due to error: ${error.message}`
)}`
);
return;
}
throw error;
}
})
);
// eslint-disable-next-line @silverhand/fp/no-mutation
cachedConnectorFactories = connectorFactories.filter(
(connectorFactory): connectorFactory is ConnectorFactory => connectorFactory !== undefined
);
checkDuplicateConnectorFactoriesId(cachedConnectorFactories);
return cachedConnectorFactories;
};

View file

@ -1,84 +1,19 @@
import { existsSync } from 'fs';
import { readFile } from 'fs/promises';
import path from 'path';
import { fileURLToPath } from 'url';
import type { AllConnector, BaseConnector, GetConnectorConfig } from '@logto/connector-kit';
import { ConnectorError, ConnectorErrorCodes, ConnectorType } from '@logto/connector-kit';
import type { ConnectorFactory } from '@logto/cli/lib/connector/index.js';
import { loadConnectorFactories as _loadConnectorFactories } from '@logto/cli/lib/connector/index.js';
import { connectorDirectory } from '@logto/cli/lib/constants.js';
import { getConnectorPackagesFromDirectory } from '@logto/cli/lib/utils.js';
import type { ConnectorFactoryResponse, ConnectorResponse } from '@logto/schemas';
import { pick } from '@silverhand/essentials';
import { findPackage } from '@logto/shared';
import { deduplicate, pick } from '@silverhand/essentials';
import { notImplemented } from './consts.js';
import type { ConnectorFactory, LogtoConnector } from './types.js';
import { EnvSet } from '#src/env-set/index.js';
import RequestError from '#src/errors/RequestError/index.js';
export function validateConnectorModule(
connector: Partial<BaseConnector<ConnectorType>>
): asserts connector is BaseConnector<ConnectorType> {
if (!connector.metadata) {
throw new ConnectorError(ConnectorErrorCodes.InvalidMetadata);
}
if (!connector.configGuard) {
throw new ConnectorError(ConnectorErrorCodes.InvalidConfigGuard);
}
if (!connector.type || !Object.values(ConnectorType).includes(connector.type)) {
throw new ConnectorError(ConnectorErrorCodes.UnexpectedType);
}
}
export const readUrl = async (
url: string,
baseUrl: string,
type: 'text' | 'svg'
): Promise<string> => {
if (!url) {
return url;
}
if (type !== 'text' && url.startsWith('http')) {
return url;
}
if (!existsSync(path.join(baseUrl, url))) {
return url;
}
if (type === 'svg') {
const data = await readFile(path.join(baseUrl, url));
return `data:image/svg+xml;base64,${data.toString('base64')}`;
}
return readFile(path.join(baseUrl, url), 'utf8');
};
export const parseMetadata = async (
metadata: AllConnector['metadata'],
packagePath: string
): Promise<AllConnector['metadata']> => {
return {
...metadata,
logo: await readUrl(metadata.logo, packagePath, 'svg'),
logoDark: metadata.logoDark && (await readUrl(metadata.logoDark, packagePath, 'svg')),
readme: await readUrl(metadata.readme, packagePath, 'text'),
configTemplate:
metadata.configTemplate && (await readUrl(metadata.configTemplate, packagePath, 'text')),
};
};
export const buildRawConnector = async (
connectorFactory: ConnectorFactory,
getConnectorConfig?: GetConnectorConfig
) => {
const { createConnector, path: packagePath } = connectorFactory;
const rawConnector = await createConnector({
getConfig: getConnectorConfig ?? notImplemented,
});
validateConnectorModule(rawConnector);
const rawMetadata = await parseMetadata(rawConnector.metadata, packagePath);
return { rawConnector, rawMetadata };
};
import type { LogtoConnector } from './types.js';
export const transpileLogtoConnector = ({
dbEntry,
@ -98,3 +33,41 @@ export const transpileConnectorFactory = ({
}: ConnectorFactory): ConnectorFactoryResponse => {
return { type, ...metadata };
};
const checkDuplicateConnectorFactoriesId = (connectorFactories: ConnectorFactory[]) => {
const connectorFactoryIds = connectorFactories.map(({ metadata }) => metadata.id);
const deduplicatedConnectorFactoryIds = deduplicate(connectorFactoryIds);
if (connectorFactoryIds.length !== deduplicatedConnectorFactoryIds.length) {
const duplicatedConnectorFactoryIds = deduplicatedConnectorFactoryIds.filter(
(deduplicateId) => connectorFactoryIds.filter((id) => id === deduplicateId).length > 1
);
throw new RequestError({
code: 'connector.more_than_one_connector_factory',
status: 422,
connectorIds: duplicatedConnectorFactoryIds.map((id) => `${id}`).join(', '),
});
}
};
export const loadConnectorFactories = async () => {
const currentDirname = path.dirname(fileURLToPath(import.meta.url));
const cliDirectory = await findPackage(currentDirname);
const coreDirectory = cliDirectory && path.join(cliDirectory, '../core');
const directory = coreDirectory && path.join(coreDirectory, connectorDirectory);
if (!directory || !existsSync(directory)) {
return [];
}
const connectorPackages = await getConnectorPackagesFromDirectory(directory);
const connectorFactories = await _loadConnectorFactories(
connectorPackages,
EnvSet.values.isIntegrationTest || EnvSet.values.ignoreConnectorVersionCheck
);
checkDuplicateConnectorFactoriesId(connectorFactories);
return connectorFactories;
};

View file

@ -1,21 +1,10 @@
import type { AllConnector, CreateConnector, VerificationCodeType } from '@logto/connector-kit';
import type { AllConnector, VerificationCodeType } from '@logto/connector-kit';
import type { Connector } from '@logto/schemas';
export { ConnectorType } from '@logto/schemas';
export type TemplateType = VerificationCodeType;
/**
* Dynamic loaded connector type.
*/
export type ConnectorFactory<T extends AllConnector = AllConnector> = Pick<
T,
'type' | 'metadata'
> & {
createConnector: CreateConnector<AllConnector>;
path: string;
};
/**
* The connector type with full context.
*/

View file

@ -33,7 +33,6 @@
"url": "https://github.com/logto-io/logto/issues"
},
"dependencies": {
"@logto/core-kit": "workspace:*",
"@logto/language-kit": "workspace:*",
"@silverhand/essentials": "2.4.0",
"zod": "^3.20.2"

View file

@ -1,6 +1,5 @@
import { fallback } from '@logto/core-kit';
import type { LanguageTag } from '@logto/language-kit';
import { languages } from '@logto/language-kit';
import { languages, fallback } from '@logto/language-kit';
import type { NormalizeKeyPaths } from '@silverhand/essentials';
import { z } from 'zod';

View file

@ -33,7 +33,6 @@
"url": "https://github.com/logto-io/logto/issues"
},
"dependencies": {
"@logto/core-kit": "workspace:*",
"@logto/language-kit": "workspace:*",
"@silverhand/essentials": "2.4.0",
"zod": "^3.20.2"

View file

@ -1,6 +1,5 @@
import { fallback } from '@logto/core-kit';
import type { LanguageTag } from '@logto/language-kit';
import { languages } from '@logto/language-kit';
import { languages, fallback } from '@logto/language-kit';
import type { NormalizeKeyPaths } from '@silverhand/essentials';
import { z } from 'zod';

View file

@ -32,7 +32,6 @@
"prepack": "pnpm build"
},
"dependencies": {
"@logto/core-kit": "workspace:*",
"@logto/language-kit": "workspace:*",
"@silverhand/essentials": "2.4.0"
},

View file

@ -1,4 +1,3 @@
export * from './color.js';
export * from './id.js';
export * from './zod.js';
export * from './url.js';

View file

@ -1,13 +0,0 @@
import { number, ZodError } from 'zod';
import { fallback } from './zod.js';
describe('fallback', () => {
it('should fallback to default value', () => {
const schema = number();
const tolerant = schema.or(fallback(-1));
expect(() => schema.parse('foo')).toThrow(ZodError);
expect(tolerant.parse('foo')).toBe(-1);
});
});

View file

@ -1,15 +0,0 @@
import { any } from 'zod';
/**
* https://github.com/colinhacks/zod/issues/316#issuecomment-850906479
* Create a schema matches anything and returns a value. Use it with `or`:
*
* const schema = zod.number();
* const tolerant = schema.or(fallback(-1));
*
* schema.parse('foo') // => ZodError
* tolerant.parse('foo') // -1
*/
export function fallback<T>(value: T) {
return any().transform(() => value);
}

View file

@ -1,4 +1,6 @@
import { isLanguageTag, languageTagGuard } from './utility.js';
import { number, ZodError } from 'zod';
import { fallback, isLanguageTag, languageTagGuard } from './utility.js';
describe('isLanguageTag', () => {
it('should pass when input is a valid language key', () => {
@ -25,3 +27,13 @@ describe('languageTagGuard', () => {
}
});
});
describe('fallback', () => {
it('should fallback to default value', () => {
const schema = number();
const tolerant = schema.or(fallback(-1));
expect(() => schema.parse('foo')).toThrow(ZodError);
expect(tolerant.parse('foo')).toBe(-1);
});
});

View file

@ -1,4 +1,4 @@
import { z } from 'zod';
import { z, any } from 'zod';
import { languages } from './const.js';
import type { LanguageTag } from './type.js';
@ -9,3 +9,17 @@ export const isLanguageTag = (value: unknown): value is LanguageTag =>
export const languageTagGuard: z.ZodType<LanguageTag> = z
.any()
.refine((value: unknown) => isLanguageTag(value));
/**
* https://github.com/colinhacks/zod/issues/316#issuecomment-850906479
* Create a schema matches anything and returns a value. Use it with `or`:
*
* const schema = zod.number();
* const tolerant = schema.or(fallback(-1));
*
* schema.parse('foo') // => ZodError
* tolerant.parse('foo') // -1
*/
export function fallback<T>(value: T) {
return any().transform(() => value);
}

View file

@ -27,6 +27,7 @@ importers:
packages/cli:
specifiers:
'@logto/connector-kit': workspace:1.0.0-rc.2
'@logto/core-kit': workspace:*
'@logto/schemas': workspace:*
'@logto/shared': workspace:*
@ -66,6 +67,7 @@ importers:
yargs: ^17.6.0
zod: ^3.20.2
dependencies:
'@logto/connector-kit': link:../toolkit/connector-kit
'@logto/core-kit': link:../toolkit/core-kit
'@logto/schemas': link:../schemas
'@logto/shared': link:../shared
@ -619,7 +621,6 @@ importers:
packages/phrases:
specifiers:
'@logto/core-kit': workspace:*
'@logto/language-kit': workspace:*
'@silverhand/eslint-config': 2.0.1
'@silverhand/essentials': 2.4.0
@ -630,7 +631,6 @@ importers:
typescript: ^4.9.4
zod: ^3.20.2
dependencies:
'@logto/core-kit': link:../toolkit/core-kit
'@logto/language-kit': link:../toolkit/language-kit
'@silverhand/essentials': 2.4.0
zod: 3.20.2
@ -644,7 +644,6 @@ importers:
packages/phrases-ui:
specifiers:
'@logto/core-kit': workspace:*
'@logto/language-kit': workspace:*
'@silverhand/eslint-config': 2.0.1
'@silverhand/essentials': 2.4.0
@ -656,7 +655,6 @@ importers:
typescript: ^4.9.4
zod: ^3.20.2
dependencies:
'@logto/core-kit': link:../toolkit/core-kit
'@logto/language-kit': link:../toolkit/language-kit
'@silverhand/essentials': 2.4.0
zod: 3.20.2
@ -765,7 +763,6 @@ importers:
packages/toolkit/connector-kit:
specifiers:
'@logto/core-kit': workspace:*
'@logto/language-kit': workspace:*
'@silverhand/eslint-config': 2.0.1
'@silverhand/essentials': 2.4.0
@ -778,7 +775,6 @@ importers:
typescript: ^4.9.4
zod: ^3.20.2
dependencies:
'@logto/core-kit': link:../core-kit
'@logto/language-kit': link:../language-kit
'@silverhand/essentials': 2.4.0
optionalDependencies: