|
54 | 54 | * }
|
55 | 55 | * }
|
56 | 56 | */
|
57 |
| -export type ErrorList<T> = { [code: string]: string }; |
| 57 | + |
| 58 | +export type ErrorMap<ErrorCode extends string> = { |
| 59 | + readonly [K in ErrorCode]: string |
| 60 | +}; |
58 | 61 |
|
59 | 62 | const ERROR_NAME = 'FirebaseError';
|
60 | 63 |
|
61 | 64 | export interface StringLike {
|
62 |
| - toString: () => string; |
| 65 | + toString(): string; |
63 | 66 | }
|
64 | 67 |
|
65 |
| -let captureStackTrace: (obj: Object, fn?: Function) => void = (Error as any) |
66 |
| - .captureStackTrace; |
67 |
| - |
68 |
| -// Export for faking in tests |
69 |
| -export function patchCapture(captureFake?: any): any { |
70 |
| - let result: any = captureStackTrace; |
71 |
| - captureStackTrace = captureFake; |
72 |
| - return result; |
| 68 | +export interface ErrorData { |
| 69 | + [key: string]: StringLike | undefined; |
73 | 70 | }
|
74 | 71 |
|
75 |
| -export interface FirebaseError { |
76 |
| - // Unique code for error - format is service/error-code-string |
77 |
| - code: string; |
| 72 | +export interface FirebaseError extends Error, ErrorData { |
| 73 | + // Unique code for error - format is service/error-code-string. |
| 74 | + readonly code: string; |
78 | 75 |
|
79 | 76 | // Developer-friendly error message.
|
80 |
| - message: string; |
| 77 | + readonly message: string; |
81 | 78 |
|
82 |
| - // Always 'FirebaseError' |
83 |
| - name: string; |
| 79 | + // Always 'FirebaseError'. |
| 80 | + readonly name: typeof ERROR_NAME; |
84 | 81 |
|
85 |
| - // Where available - stack backtrace in a string |
86 |
| - stack: string; |
| 82 | + // Where available - stack backtrace in a string. |
| 83 | + readonly stack?: string; |
87 | 84 | }
|
88 | 85 |
|
89 |
| -export class FirebaseError implements FirebaseError { |
90 |
| - public stack: string; |
91 |
| - public name: string; |
92 |
| - |
93 |
| - constructor(public code: string, public message: string) { |
94 |
| - let stack: string; |
95 |
| - // We want the stack value, if implemented by Error |
96 |
| - if (captureStackTrace) { |
97 |
| - // Patches this.stack, omitted calls above ErrorFactory#create |
98 |
| - captureStackTrace(this, ErrorFactory.prototype.create); |
99 |
| - } else { |
100 |
| - try { |
101 |
| - // In case of IE11, stack will be set only after error is raised. |
102 |
| - // https://docs.microsoft.com/en-us/scripting/javascript/reference/stack-property-error-javascript |
103 |
| - throw Error.apply(this, arguments); |
104 |
| - } catch (err) { |
105 |
| - this.name = ERROR_NAME; |
106 |
| - // Make non-enumerable getter for the property. |
107 |
| - Object.defineProperty(this, 'stack', { |
108 |
| - get: function() { |
109 |
| - return err.stack; |
110 |
| - } |
111 |
| - }); |
112 |
| - } |
113 |
| - } |
114 |
| - } |
115 |
| -} |
116 |
| - |
117 |
| -// Back-door inheritance |
118 |
| -FirebaseError.prototype = Object.create(Error.prototype) as FirebaseError; |
119 |
| -FirebaseError.prototype.constructor = FirebaseError; |
120 |
| -(FirebaseError.prototype as any).name = ERROR_NAME; |
| 86 | +// Based on code from: |
| 87 | +// https://developer.mozilla.org/en-US/docs/Web/JavaScript/Reference/Global_Objects/Error#Custom_Error_Types |
| 88 | +export class FirebaseError extends Error { |
| 89 | + readonly name = ERROR_NAME; |
121 | 90 |
|
122 |
| -export class ErrorFactory<T extends string> { |
123 |
| - // Matches {$name}, by default. |
124 |
| - public pattern = /\{\$([^}]+)}/g; |
| 91 | + constructor(readonly code: string, message: string) { |
| 92 | + super(message); |
125 | 93 |
|
126 |
| - constructor( |
127 |
| - private service: string, |
128 |
| - private serviceName: string, |
129 |
| - private errors: ErrorList<T> |
130 |
| - ) { |
131 |
| - // empty |
132 |
| - } |
| 94 | + // Fix For ES5 |
| 95 | + // https://github.com/Microsoft/TypeScript-wiki/blob/master/Breaking-Changes.md#extending-built-ins-like-error-array-and-map-may-no-longer-work |
| 96 | + Object.setPrototypeOf(this, FirebaseError.prototype); |
133 | 97 |
|
134 |
| - create(code: T, data?: { [prop: string]: StringLike }): FirebaseError { |
135 |
| - if (data === undefined) { |
136 |
| - data = {}; |
| 98 | + // Maintains proper stack trace for where our error was thrown. |
| 99 | + // Only available on V8. |
| 100 | + if (Error.captureStackTrace) { |
| 101 | + Error.captureStackTrace(this, ErrorFactory.prototype.create); |
137 | 102 | }
|
| 103 | + } |
| 104 | +} |
138 | 105 |
|
139 |
| - let template = this.errors[code as string]; |
| 106 | +export class ErrorFactory<ErrorCode extends string> { |
| 107 | + constructor( |
| 108 | + private readonly service: string, |
| 109 | + private readonly serviceName: string, |
| 110 | + private readonly errors: ErrorMap<ErrorCode> |
| 111 | + ) {} |
140 | 112 |
|
141 |
| - let fullCode = this.service + '/' + code; |
142 |
| - let message: string; |
| 113 | + create(code: ErrorCode, data: ErrorData = {}): FirebaseError { |
| 114 | + const fullCode = `${this.service}/${code}`; |
| 115 | + const template = this.errors[code]; |
143 | 116 |
|
144 |
| - if (template === undefined) { |
145 |
| - message = 'Error'; |
146 |
| - } else { |
147 |
| - message = template.replace(this.pattern, (match, key) => { |
148 |
| - let value = data![key]; |
149 |
| - return value !== undefined ? value.toString() : '<' + key + '?>'; |
150 |
| - }); |
151 |
| - } |
| 117 | + const message = template ? replaceTemplate(template, data) : 'Error'; |
152 | 118 |
|
153 | 119 | // Service: Error message (service/code).
|
154 |
| - message = this.serviceName + ': ' + message + ' (' + fullCode + ').'; |
155 |
| - let err = new FirebaseError(fullCode, message); |
156 |
| - |
157 |
| - // Populate the Error object with message parts for programmatic |
158 |
| - // accesses (e.g., e.file). |
159 |
| - for (let prop in data) { |
160 |
| - if (!data.hasOwnProperty(prop) || prop.slice(-1) === '_') { |
161 |
| - continue; |
| 120 | + const fullMessage = `${this.serviceName}: ${message} (${fullCode}).`; |
| 121 | + |
| 122 | + // Keys with an underscore at the end of their name are not included in |
| 123 | + // error.data for some reason. |
| 124 | + const error = new FirebaseError(fullCode, fullMessage); |
| 125 | + // TODO: Replace with Object.entries when lib is updated to es2017. |
| 126 | + for (const key of Object.keys(data)) { |
| 127 | + if (key.slice(-1) !== '_') { |
| 128 | + if (key in error) { |
| 129 | + console.warn( |
| 130 | + `Overwriting FirebaseError base field "${key}" can cause unexpected behavior.` |
| 131 | + ); |
| 132 | + } |
| 133 | + error[key] = data[key]; |
162 | 134 | }
|
163 |
| - (err as any)[prop] = data[prop]; |
164 | 135 | }
|
165 |
| - |
166 |
| - return err; |
| 136 | + return error; |
167 | 137 | }
|
168 | 138 | }
|
| 139 | + |
| 140 | +function replaceTemplate(template: string, data: ErrorData): string { |
| 141 | + return template.replace(PATTERN, (_, key) => { |
| 142 | + const value = data[key]; |
| 143 | + return value != null ? value.toString() : `<${key}?>`; |
| 144 | + }); |
| 145 | +} |
| 146 | + |
| 147 | +const PATTERN = /\{\$([^}]+)}/g; |
0 commit comments