voluptasmollitia
Version:
Monorepo for the Firebase JavaScript SDK
247 lines (225 loc) • 7.38 kB
text/typescript
/**
* @license
* Copyright 2020 Google LLC
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/
import { Auth } from '../../model/public_types';
import { FirebaseError } from '@firebase/util';
import { AuthInternal } from '../../model/auth';
import {
_DEFAULT_AUTH_ERROR_FACTORY,
AuthErrorCode,
AuthErrorParams
} from '../errors';
import { _logError } from './log';
type AuthErrorListParams<K> = K extends keyof AuthErrorParams
? [AuthErrorParams[K]]
: [];
type LessAppName<K extends AuthErrorCode> = Omit<AuthErrorParams[K], 'appName'>;
/**
* Unconditionally fails, throwing a developer facing INTERNAL_ERROR
*
* @example
* ```javascript
* fail(auth, AuthErrorCode.MFA_REQUIRED); // Error: the MFA_REQUIRED error needs more params than appName
* fail(auth, AuthErrorCode.MFA_REQUIRED, {serverResponse}); // Compiles
* fail(AuthErrorCode.INTERNAL_ERROR); // Compiles; internal error does not need appName
* fail(AuthErrorCode.USER_DELETED); // Error: USER_DELETED requires app name
* fail(auth, AuthErrorCode.USER_DELETED); // Compiles; USER_DELETED _only_ needs app name
* ```
*
* @param appName App name for tagging the error
* @throws FirebaseError
*/
export function _fail<K extends AuthErrorCode>(
code: K,
...data: {} extends AuthErrorParams[K]
? [AuthErrorParams[K]?]
: [AuthErrorParams[K]]
): never;
export function _fail<K extends AuthErrorCode>(
auth: Auth,
code: K,
...data: {} extends LessAppName<K> ? [LessAppName<K>?] : [LessAppName<K>]
): never;
export function _fail<K extends AuthErrorCode>(
authOrCode: Auth | K,
...rest: unknown[]
): never {
throw createErrorInternal(authOrCode, ...rest);
}
export function _createError<K extends AuthErrorCode>(
code: K,
...data: {} extends AuthErrorParams[K]
? [AuthErrorParams[K]?]
: [AuthErrorParams[K]]
): FirebaseError;
export function _createError<K extends AuthErrorCode>(
auth: Auth,
code: K,
...data: {} extends LessAppName<K> ? [LessAppName<K>?] : [LessAppName<K>]
): FirebaseError;
export function _createError<K extends AuthErrorCode>(
authOrCode: Auth | K,
...rest: unknown[]
): FirebaseError {
return createErrorInternal(authOrCode, ...rest);
}
function createErrorInternal<K extends AuthErrorCode>(
authOrCode: Auth | K,
...rest: unknown[]
): FirebaseError {
if (typeof authOrCode !== 'string') {
const code = rest[0] as K;
const fullParams = [...rest.slice(1)] as AuthErrorListParams<K>;
if (fullParams[0]) {
fullParams[0].appName = authOrCode.name;
}
return (authOrCode as AuthInternal)._errorFactory.create(
code,
...fullParams
);
}
return _DEFAULT_AUTH_ERROR_FACTORY.create(
authOrCode,
...(rest as AuthErrorListParams<K>)
);
}
export function _assert<K extends AuthErrorCode>(
assertion: unknown,
code: K,
...data: {} extends AuthErrorParams[K]
? [AuthErrorParams[K]?]
: [AuthErrorParams[K]]
): asserts assertion;
export function _assert<K extends AuthErrorCode>(
assertion: unknown,
auth: Auth,
code: K,
...data: {} extends LessAppName<K> ? [LessAppName<K>?] : [LessAppName<K>]
): asserts assertion;
export function _assert<K extends AuthErrorCode>(
assertion: unknown,
authOrCode: Auth | K,
...rest: unknown[]
): asserts assertion {
if (!assertion) {
throw createErrorInternal(authOrCode, ...rest);
}
}
// We really do want to accept literally any function type here
// eslint-disable-next-line @typescript-eslint/ban-types
type TypeExpectation = Function | string | MapType;
interface MapType extends Record<string, TypeExpectation | Optional> {}
class Optional {
constructor(readonly type: TypeExpectation) {}
}
export function opt(type: TypeExpectation): Optional {
return new Optional(type);
}
/**
* Asserts the runtime types of arguments. The 'expected' field can be one of
* a class, a string (representing a "typeof" call), or a record map of name
* to type. Furthermore, the opt() function can be used to mark a field as
* optional. For example:
*
* function foo(auth: Auth, profile: {displayName?: string}, update = false) {
* assertTypes(arguments, [AuthImpl, {displayName: opt('string')}, opt('boolean')]);
* }
*
* opt() can be used for any type:
* function foo(auth?: Auth) {
* assertTypes(arguments, [opt(AuthImpl)]);
* }
*
* The string types can be or'd together, and you can use "null" as well (note
* that typeof null === 'object'; this is an edge case). For example:
*
* function foo(profile: {displayName?: string | null}) {
* assertTypes(arguments, [{displayName: opt('string|null')}]);
* }
*
* @param args
* @param expected
*/
export function assertTypes(
args: Omit<IArguments, 'callee'>,
...expected: Array<TypeExpectation | Optional>
): void {
if (args.length > expected.length) {
_fail(AuthErrorCode.ARGUMENT_ERROR, {});
}
for (let i = 0; i < expected.length; i++) {
let expect = expected[i];
const arg = args[i];
if (expect instanceof Optional) {
// If the arg is undefined, then it matches "optional" and we can move to
// the next arg
if (typeof arg === 'undefined') {
continue;
}
expect = expect.type;
}
if (typeof expect === 'string') {
// Handle the edge case for null because typeof null === 'object'
if (expect.includes('null') && arg === null) {
continue;
}
const required = expect.split('|');
_assert(required.includes(typeof arg), AuthErrorCode.ARGUMENT_ERROR, {});
} else if (typeof expect === 'object') {
// Recursively check record arguments
const record = arg as Record<string, unknown>;
const map = expect as MapType;
const keys = Object.keys(expect);
assertTypes(
keys.map(k => record[k]),
...keys.map(k => map[k])
);
} else {
_assert(arg instanceof expect, AuthErrorCode.ARGUMENT_ERROR, {});
}
}
}
/**
* Unconditionally fails, throwing an internal error with the given message.
*
* @param failure type of failure encountered
* @throws Error
*/
export function debugFail(failure: string): never {
// Log the failure in addition to throw an exception, just in case the
// exception is swallowed.
const message = `INTERNAL ASSERTION FAILED: ` + failure;
_logError(message);
// NOTE: We don't use FirebaseError here because these are internal failures
// that cannot be handled by the user. (Also it would create a circular
// dependency between the error and assert modules which doesn't work.)
throw new Error(message);
}
/**
* Fails if the given assertion condition is false, throwing an Error with the
* given message if it did.
*
* @param assertion
* @param message
*/
export function debugAssert(
assertion: unknown,
message: string
): asserts assertion {
if (!assertion) {
debugFail(message);
}
}