Skip to content

Commit edb4001

Browse files
authored
Implement Partial Errors for FDC (#8821)
1 parent 5718838 commit edb4001

File tree

11 files changed

+152
-99
lines changed

11 files changed

+152
-99
lines changed

.changeset/sharp-nails-glow.md

+5
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,5 @@
1+
---
2+
"@firebase/data-connect": patch
3+
---
4+
5+
Expose partial errors to the user.

common/api-review/data-connect.api.md

+31-2
Original file line numberDiff line numberDiff line change
@@ -52,6 +52,35 @@ export class DataConnect {
5252
setInitialized(): void;
5353
}
5454

55+
// @public
56+
export class DataConnectError extends FirebaseError {
57+
}
58+
59+
// @public (undocumented)
60+
export type DataConnectErrorCode = 'other' | 'already-initialized' | 'not-initialized' | 'not-supported' | 'invalid-argument' | 'partial-error' | 'unauthorized';
61+
62+
// @public
63+
export class DataConnectOperationError extends DataConnectError {
64+
/* Excluded from this release type: name */
65+
readonly response: DataConnectOperationFailureResponse;
66+
}
67+
68+
// @public (undocumented)
69+
export interface DataConnectOperationFailureResponse {
70+
// (undocumented)
71+
readonly data?: Record<string, unknown> | null;
72+
// (undocumented)
73+
readonly errors: DataConnectOperationFailureResponseErrorInfo[];
74+
}
75+
76+
// @public (undocumented)
77+
export interface DataConnectOperationFailureResponseErrorInfo {
78+
// (undocumented)
79+
readonly message: string;
80+
// (undocumented)
81+
readonly path: Array<string | number>;
82+
}
83+
5584
// @public
5685
export interface DataConnectOptions extends ConnectorConfig {
5786
// (undocumented)
@@ -67,7 +96,7 @@ export interface DataConnectResult<Data, Variables> extends OpResult<Data> {
6796
// @public
6897
export interface DataConnectSubscription<Data, Variables> {
6998
// (undocumented)
70-
errCallback?: (e?: FirebaseError) => void;
99+
errCallback?: (e?: DataConnectError) => void;
71100
// (undocumented)
72101
unsubscribe: () => void;
73102
// (undocumented)
@@ -118,7 +147,7 @@ export interface MutationResult<Data, Variables> extends DataConnectResult<Data,
118147
export type OnCompleteSubscription = () => void;
119148

120149
// @public
121-
export type OnErrorSubscription = (err?: FirebaseError) => void;
150+
export type OnErrorSubscription = (err?: DataConnectError) => void;
122151

123152
// @public
124153
export type OnResultSubscription<Data, Variables> = (res: QueryResult<Data, Variables>) => void;

packages/data-connect/src/api/index.ts

+7
Original file line numberDiff line numberDiff line change
@@ -22,3 +22,10 @@ export * from './Mutation';
2222
export * from './query';
2323
export { setLogLevel } from '../logger';
2424
export { validateArgs } from '../util/validateArgs';
25+
export {
26+
DataConnectErrorCode,
27+
DataConnectError,
28+
DataConnectOperationError,
29+
DataConnectOperationFailureResponse,
30+
DataConnectOperationFailureResponseErrorInfo
31+
} from '../core/error';

packages/data-connect/src/core/error.ts

+53-16
Original file line numberDiff line numberDiff line change
@@ -40,25 +40,62 @@ export const Code = {
4040

4141
/** An error returned by a DataConnect operation. */
4242
export class DataConnectError extends FirebaseError {
43-
/** The stack of the error. */
44-
readonly stack?: string;
43+
/** @internal */
44+
readonly name: string = 'DataConnectError';
4545

4646
/** @hideconstructor */
47-
constructor(
48-
/**
49-
* The backend error code associated with this error.
50-
*/
51-
readonly code: DataConnectErrorCode,
52-
/**
53-
* A custom error description.
54-
*/
55-
readonly message: string
56-
) {
47+
constructor(code: Code, message: string) {
5748
super(code, message);
5849

59-
// HACK: We write a toString property directly because Error is not a real
60-
// class and so inheritance does not work correctly. We could alternatively
61-
// do the same "back-door inheritance" trick that FirebaseError does.
62-
this.toString = () => `${this.name}: [code=${this.code}]: ${this.message}`;
50+
// Ensure the instanceof operator works as expected on subclasses of Error.
51+
// See https://developer.mozilla.org/en-US/docs/Web/JavaScript/Reference/Global_Objects/Error#custom_error_types
52+
// and https://www.typescriptlang.org/docs/handbook/release-notes/typescript-2-2.html#support-for-newtarget
53+
Object.setPrototypeOf(this, DataConnectError.prototype);
6354
}
55+
56+
/** @internal */
57+
toString(): string {
58+
return `${this.name}[code=${this.code}]: ${this.message}`;
59+
}
60+
}
61+
62+
/** An error returned by a DataConnect operation. */
63+
export class DataConnectOperationError extends DataConnectError {
64+
/** @internal */
65+
readonly name: string = 'DataConnectOperationError';
66+
67+
/** The response received from the backend. */
68+
readonly response: DataConnectOperationFailureResponse;
69+
70+
/** @hideconstructor */
71+
constructor(message: string, response: DataConnectOperationFailureResponse) {
72+
super(Code.PARTIAL_ERROR, message);
73+
this.response = response;
74+
}
75+
}
76+
77+
export interface DataConnectOperationFailureResponse {
78+
// The "data" provided by the backend in the response message.
79+
//
80+
// Will be `undefined` if no "data" was provided in the response message.
81+
// Otherwise, will be `null` if `null` was explicitly specified as the "data"
82+
// in the response message. Otherwise, will be the value of the "data"
83+
// specified as the "data" in the response message
84+
readonly data?: Record<string, unknown> | null;
85+
86+
// The list of errors provided by the backend in the response message.
87+
readonly errors: DataConnectOperationFailureResponseErrorInfo[];
88+
}
89+
90+
// Information about the error, as provided in the response from the backend.
91+
// See https://spec.graphql.org/draft/#sec-Errors
92+
export interface DataConnectOperationFailureResponseErrorInfo {
93+
// The error message.
94+
readonly message: string;
95+
96+
// The path of the field in the response data to which this error relates.
97+
// String values in this array refer to field names. Numeric values in this
98+
// array always satisfy `Number.isInteger()` and refer to the index in an
99+
// array.
100+
readonly path: Array<string | number>;
64101
}

packages/data-connect/src/network/fetch.ts

+14-3
Original file line numberDiff line numberDiff line change
@@ -15,7 +15,12 @@
1515
* limitations under the License.
1616
*/
1717

18-
import { Code, DataConnectError } from '../core/error';
18+
import {
19+
Code,
20+
DataConnectError,
21+
DataConnectOperationError,
22+
DataConnectOperationFailureResponse
23+
} from '../core/error';
1924
import { SDK_VERSION } from '../core/version';
2025
import { logDebug, logError } from '../logger';
2126

@@ -108,8 +113,14 @@ export function dcFetch<T, U>(
108113
.then(res => {
109114
if (res.errors && res.errors.length) {
110115
const stringified = JSON.stringify(res.errors);
111-
logError('DataConnect error while performing request: ' + stringified);
112-
throw new DataConnectError(Code.OTHER, stringified);
116+
const response: DataConnectOperationFailureResponse = {
117+
errors: res.errors,
118+
data: res.data
119+
};
120+
throw new DataConnectOperationError(
121+
'DataConnect error while performing request: ' + stringified,
122+
response
123+
);
113124
}
114125
return res;
115126
});
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,6 @@
11
connectorId: "tests"
22
authMode: "PUBLIC"
33
generate:
4-
javascriptSdk:
5-
outputDir: "./gen/web"
6-
package: "@test-app/tests"
4+
javascriptSdk:
5+
outputDir: "./gen/web"
6+
package: "@test-app/tests"

packages/data-connect/test/emulatorSeeder.ts

-68
This file was deleted.

packages/data-connect/test/mutations.gql

-6
This file was deleted.

packages/data-connect/test/unit/fetch.test.ts

+34
Original file line numberDiff line numberDiff line change
@@ -85,6 +85,40 @@ describe('fetch', () => {
8585
)
8686
).to.eventually.be.rejectedWith(JSON.stringify(json));
8787
});
88+
it('should throw a stringified message when the server responds with an error without a message property in the body', async () => {
89+
const json = {
90+
'data': { 'abc': 'def' },
91+
'errors': [
92+
{
93+
'message':
94+
'SQL query error: pq: duplicate key value violates unique constraint movie_pkey',
95+
'locations': [],
96+
'path': ['the_matrix'],
97+
'extensions': null
98+
}
99+
]
100+
};
101+
mockFetch(json, false);
102+
await expect(
103+
dcFetch(
104+
'http://localhost',
105+
{
106+
name: 'n',
107+
operationName: 'n',
108+
variables: {}
109+
},
110+
{} as AbortController,
111+
null,
112+
null,
113+
null,
114+
false,
115+
CallerSdkTypeEnum.Base
116+
)
117+
).to.eventually.be.rejected.then(error => {
118+
expect(error.response.data).to.eq(json.data);
119+
expect(error.response.errors).to.eq(json.errors);
120+
});
121+
});
88122
it('should assign different values to custom headers based on the _callerSdkType argument (_isUsingGen is false)', async () => {
89123
const json = {
90124
code: 200,

scripts/emulator-testing/emulators/dataconnect-emulator.ts

+1-1
Original file line numberDiff line numberDiff line change
@@ -18,7 +18,7 @@
1818
import { platform } from 'os';
1919
import { Emulator } from './emulator';
2020

21-
const DATACONNECT_EMULATOR_VERSION = '1.7.5';
21+
const DATACONNECT_EMULATOR_VERSION = '1.9.2';
2222

2323
export class DataConnectEmulator extends Emulator {
2424
constructor(port = 9399) {

scripts/emulator-testing/emulators/emulator.ts

+4
Original file line numberDiff line numberDiff line change
@@ -146,6 +146,7 @@ export abstract class Emulator {
146146
if (this.isDataConnect) {
147147
const dataConnectConfigDir = this.findDataConnectConfigDir();
148148
promise = spawn(this.binaryPath, [
149+
'--logtostderr',
149150
'--v=2',
150151
'dev',
151152
`--listen=127.0.0.1:${this.port},[::1]:${this.port}`,
@@ -155,6 +156,9 @@ export abstract class Emulator {
155156
promise.childProcess.stderr?.on('data', res =>
156157
console.log(res.toString())
157158
);
159+
promise.childProcess.stderr?.on('error', res =>
160+
console.log(res.toString())
161+
);
158162
} else {
159163
promise = spawn(
160164
'java',

0 commit comments

Comments
 (0)