Skip to content

Commit

Permalink
chore(backend,nextjs,clerk-sdk-node): Drop legacy return response in …
Browse files Browse the repository at this point in the history
…BAPI responses
  • Loading branch information
dimkl committed Nov 13, 2023
1 parent 9be55f9 commit bbdb0cd
Show file tree
Hide file tree
Showing 14 changed files with 149 additions and 121 deletions.
7 changes: 7 additions & 0 deletions .changeset/mighty-pugs-knock.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,7 @@
---
'@clerk/clerk-sdk-node': major
'@clerk/backend': major
'@clerk/nextjs': major
---

Change the response payload of Backend API requests to return `{ data, errors }` instead of return the data and throwing on error response.
64 changes: 29 additions & 35 deletions packages/backend/src/api/factory.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -4,6 +4,7 @@ import sinon from 'sinon';
import emailJson from '../fixtures/responses/email.json';
import userJson from '../fixtures/responses/user.json';
import runtime from '../runtime';
import { assertErrorResponse, assertResponse } from '../util/assertResponse';
import { jsonError, jsonNotOk, jsonOk } from '../util/mockFetch';
import { createBackendApiClient } from './factory';

Expand All @@ -26,20 +27,17 @@ export default (QUnit: QUnit) => {
fakeFetch = sinon.stub(runtime, 'fetch');
fakeFetch.onCall(0).returns(jsonOk(userJson));

const payload = await apiClient.users.getUser('user_deadbeef');
const response = await apiClient.users.getUser('user_deadbeef');

if (!payload) {
assert.false(true, 'This assertion should never fail. We need to check for payload to make TS happy.');
return;
}
assertResponse(assert, response);
const { data: payload } = response;

assert.equal(payload.firstName, 'John');
assert.equal(payload.lastName, 'Doe');
assert.equal(payload.emailAddresses[0].emailAddress, '[email protected]');
assert.equal(payload.phoneNumbers[0].phoneNumber, '+311-555-2368');
assert.equal(payload.externalAccounts[0].emailAddress, '[email protected]');
assert.equal(payload.publicMetadata.zodiac_sign, 'leo');
// assert.equal(payload.errors, null);

assert.ok(
fakeFetch.calledOnceWith('https://api.clerk.test/v1/users/user_deadbeef', {
Expand All @@ -57,20 +55,16 @@ export default (QUnit: QUnit) => {
fakeFetch = sinon.stub(runtime, 'fetch');
fakeFetch.onCall(0).returns(jsonOk([userJson]));

const payload = await apiClient.users.getUserList({ offset: 2, limit: 5 });

if (!payload) {
assert.false(true, 'This assertion should never fail. We need to check for payload to make TS happy.');
return;
}
const response = await apiClient.users.getUserList({ offset: 2, limit: 5 });
assertResponse(assert, response);
const { data: payload } = response;

assert.equal(payload[0].firstName, 'John');
assert.equal(payload[0].lastName, 'Doe');
assert.equal(payload[0].emailAddresses[0].emailAddress, '[email protected]');
assert.equal(payload[0].phoneNumbers[0].phoneNumber, '+311-555-2368');
assert.equal(payload[0].externalAccounts[0].emailAddress, '[email protected]');
assert.equal(payload[0].publicMetadata.zodiac_sign, 'leo');
// assert.equal(payload.errors, null);

assert.ok(
fakeFetch.calledOnceWith('https://api.clerk.test/v1/users?offset=2&limit=5', {
Expand All @@ -96,12 +90,10 @@ export default (QUnit: QUnit) => {
};
const requestBody =
'{"from_email_name":"foobar123","email_address_id":"[email protected]","body":"this is a test","subject":"this is a test"}';
const payload = await apiClient.emails.createEmail(body);
const response = await apiClient.emails.createEmail(body);
assertResponse(assert, response);
const { data: payload } = response;

if (!payload) {
assert.false(true, 'This assertion should never fail. We need to check for payload to make TS happy.');
return;
}
assert.equal(JSON.stringify(payload.data), '{}');
assert.equal(payload.id, 'ema_2PHa2N3bS7D6NPPQ5mpHEg0waZQ');

Expand All @@ -120,15 +112,19 @@ export default (QUnit: QUnit) => {

test('executes a successful backend API request to create a new resource', async assert => {
fakeFetch = sinon.stub(runtime, 'fetch');
fakeFetch.onCall(0).returns(jsonOk([userJson]));
fakeFetch.onCall(0).returns(jsonOk(userJson));

await apiClient.users.createUser({
const response = await apiClient.users.createUser({
firstName: 'John',
lastName: 'Doe',
publicMetadata: {
star_sign: 'Leon',
},
});
assertResponse(assert, response);
const { data: payload } = response;

assert.equal(payload.firstName, 'John');

assert.ok(
fakeFetch.calledOnceWith('https://api.clerk.test/v1/users', {
Expand All @@ -155,14 +151,13 @@ export default (QUnit: QUnit) => {
fakeFetch = sinon.stub(runtime, 'fetch');
fakeFetch.onCall(0).returns(jsonNotOk({ errors: [mockErrorPayload], clerk_trace_id: traceId }));

try {
await apiClient.users.getUser('user_deadbeef');
} catch (e: any) {
assert.equal(e.clerkTraceId, traceId);
assert.equal(e.clerkError, true);
assert.equal(e.status, 422);
assert.equal(e.errors[0].code, 'whatever_error');
}
const response = await apiClient.users.getUser('user_deadbeef');
assertErrorResponse(assert, response);

assert.equal(response.clerkTraceId, traceId);
assert.equal(response.status, 422);
assert.equal(response.statusText, '422');
assert.equal(response.errors[0].code, 'whatever_error');

assert.ok(
fakeFetch.calledOnceWith('https://api.clerk.test/v1/users/user_deadbeef', {
Expand All @@ -180,13 +175,12 @@ export default (QUnit: QUnit) => {
fakeFetch = sinon.stub(runtime, 'fetch');
fakeFetch.onCall(0).returns(jsonError({ errors: [] }));

try {
await apiClient.users.getUser('user_deadbeef');
} catch (e: any) {
assert.equal(e.clerkError, true);
assert.equal(e.status, 500);
assert.equal(e.clerkTraceId, 'mock_cf_ray');
}
const response = await apiClient.users.getUser('user_deadbeef');
assertErrorResponse(assert, response);

assert.equal(response.status, 500);
assert.equal(response.statusText, '500');
assert.equal(response.clerkTraceId, 'mock_cf_ray');

assert.ok(
fakeFetch.calledOnceWith('https://api.clerk.test/v1/users/user_deadbeef', {
Expand Down
41 changes: 11 additions & 30 deletions packages/backend/src/api/request.ts
Original file line number Diff line number Diff line change
Expand Up @@ -38,35 +38,14 @@ export type ClerkBackendApiResponse<T> =
data: null;
errors: ClerkAPIError[];
clerkTraceId?: string;
status?: number;
statusText?: string;
};

export type RequestFunction = ReturnType<typeof buildRequest>;
type LegacyRequestFunction = <T>(requestOptions: ClerkBackendApiRequestOptions) => Promise<T>;

/**
* Switching to the { data, errors } format is a breaking change, so we will skip it for now
* until we release v5 of the related SDKs.
* This HOF wraps the request helper and transforms the new return to the legacy return.
* TODO: Simply remove this wrapper and the ClerkAPIResponseError before the v5 release.
*/
const withLegacyReturn =
(cb: any): LegacyRequestFunction =>
async (...args) => {
// @ts-ignore
const { data, errors, status, statusText, clerkTraceId } = await cb<T>(...args);
if (errors === null) {
return data;
} else {
throw new ClerkAPIResponseError(statusText || '', {
data: errors,
status: status || '',
clerkTraceId,
});
}
};

export function buildRequest(options: CreateBackendApiOptions) {
const request = async <T>(requestOptions: ClerkBackendApiRequestOptions): Promise<ClerkBackendApiResponse<T>> => {
return async <T>(requestOptions: ClerkBackendApiRequestOptions): Promise<ClerkBackendApiResponse<T>> => {
const { secretKey, apiUrl = API_URL, apiVersion = API_VERSION, userAgent = USER_AGENT } = options;
const { path, method, queryParams, headerParams, bodyParams, formData } = requestOptions;

Expand Down Expand Up @@ -96,7 +75,7 @@ export function buildRequest(options: CreateBackendApiOptions) {
...headerParams,
};

let res: Response | undefined = undefined;
let res: Response | undefined;
try {
if (formData) {
res = await runtime.fetch(finalUrl.href, {
Expand Down Expand Up @@ -124,7 +103,13 @@ export function buildRequest(options: CreateBackendApiOptions) {
const data = await (isJSONResponse ? res.json() : res.text());

if (!res.ok) {
throw data;
return {
data: null,
errors: data?.errors || data,
status: res?.status,
statusText: res?.statusText,
clerkTraceId: getTraceId(data, res?.headers),
};
}

return {
Expand All @@ -148,16 +133,12 @@ export function buildRequest(options: CreateBackendApiOptions) {
return {
data: null,
errors: parseErrors(err),
// TODO: To be removed with withLegacyReturn
// @ts-expect-error
status: res?.status,
statusText: res?.statusText,
clerkTraceId: getTraceId(err, res?.headers),
};
}
};

return withLegacyReturn(request);
}

// Returns either clerk_trace_id if present in response json, otherwise defaults to CF-Ray header
Expand Down
9 changes: 3 additions & 6 deletions packages/backend/src/tokens/authStatus.ts
Original file line number Diff line number Diff line change
Expand Up @@ -115,12 +115,9 @@ export async function signedIn<T>(options: T, sessionClaims: JwtPayload): Promis
loadOrganization && orgId ? organizations.getOrganization({ organizationId: orgId }) : Promise.resolve(undefined),
]);

const session = sessionResp;
const user = userResp;
const organization = organizationResp;
// const session = sessionResp && !sessionResp.errors ? sessionResp.data : undefined;
// const user = userResp && !userResp.errors ? userResp.data : undefined;
// const organization = organizationResp && !organizationResp.errors ? organizationResp.data : undefined;
const session = sessionResp && !sessionResp.errors ? sessionResp.data : undefined;
const user = userResp && !userResp.errors ? userResp.data : undefined;
const organization = organizationResp && !organizationResp.errors ? organizationResp.data : undefined;

const authObject = signedInAuthObject(
sessionClaims,
Expand Down
9 changes: 9 additions & 0 deletions packages/backend/src/util/assertResponse.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,9 @@
type ApiResponse<T> = { data: T | null; errors: null | any[] };
type SuccessApiResponse<T> = { data: T; errors: null };
type ErrorApiResponse = { data: null; errors: any[]; clerkTraceId: string; status: number; statusText: string };
export function assertResponse<T>(assert: Assert, resp: ApiResponse<T>): asserts resp is SuccessApiResponse<T> {
assert.equal(resp.errors, null);
}
export function assertErrorResponse<T>(assert: Assert, resp: ApiResponse<T>): asserts resp is ErrorApiResponse {
assert.notEqual(resp.errors, null);
}
5 changes: 4 additions & 1 deletion packages/backend/src/util/mockFetch.ts
Original file line number Diff line number Diff line change
Expand Up @@ -5,6 +5,7 @@ export function jsonOk(body: unknown, status = 200) {
const mockResponse = {
ok: true,
status,
statusText: status.toString(),
headers: { get: mockHeadersGet },
json() {
return Promise.resolve(body);
Expand All @@ -19,6 +20,7 @@ export function jsonNotOk(body: unknown) {
const mockResponse = {
ok: false,
status: 422,
statusText: 422,
headers: { get: mockHeadersGet },
json() {
return Promise.resolve(body);
Expand All @@ -32,7 +34,8 @@ export function jsonError(body: unknown, status = 500) {
// Mock response object that satisfies the window.Response interface
const mockResponse = {
ok: false,
status: status,
status,
statusText: status.toString(),
headers: { get: mockHeadersGet },
json() {
return Promise.resolve(body);
Expand Down
8 changes: 6 additions & 2 deletions packages/nextjs/src/app-router/server/currentUser.ts
Original file line number Diff line number Diff line change
Expand Up @@ -3,8 +3,12 @@ import type { User } from '@clerk/backend';
import { clerkClient } from '../../server/clerkClient';
import { auth } from './auth';

// eslint-disable-next-line @typescript-eslint/require-await
export async function currentUser(): Promise<User | null> {
const { userId } = auth();
return userId ? clerkClient.users.getUser(userId) : null;
if (!userId) return null;

const { data, errors } = await clerkClient.users.getUser(userId);
if (errors) return null;

return data;
}
Original file line number Diff line number Diff line change
Expand Up @@ -21,8 +21,12 @@ app.use(clerk.expressWithAuth());
app.get('/', async (req: WithAuthProp<Request>, res: Response) => {
const { userId, debug } = req.auth;
console.log(debug());
const user = userId ? await clerk.users.getUser(userId) : null;
res.json({ auth: req.auth, user });
if (!userId) return res.json({ auth: req.auth, user: null });

const { data, errors } = await clerk.users.getUser(userId);
if (errors) return res.json({ auth: req.auth, user: null });

return res.json({ auth: req.auth, user: data });;
});

// @ts-ignore
Expand Down
21 changes: 16 additions & 5 deletions packages/sdk-node/examples/node/src/organizations.ts
Original file line number Diff line number Diff line change
@@ -1,18 +1,29 @@
import { organizations, users } from '@clerk/clerk-sdk-node';

console.log('Get user to create organization');
const [creator] = await users.getUserList();
const { data, errors } = await users.getUserList();
if (errors) {
throw new Error(errors);
}

const creator = data[0];

console.log('Create organization');
const organization = await organizations.createOrganization({
const { data: organization } = await organizations.createOrganization({
name: 'test-organization',
createdBy: creator.id,
});
console.log(organization);

console.log('Update organization metadata');
const updatedOrganizationMetadata =
await organizations.updateOrganizationMetadata(organization.id, {
const { data: updatedOrganizationMetadata, errors: uomErrors } = await organizations.updateOrganizationMetadata(
organization.id,
{
publicMetadata: { test: 1 },
});
},
);
if (uomErrors) {
throw new Error(uomErrors);
}

console.log(updatedOrganizationMetadata);
28 changes: 10 additions & 18 deletions packages/sdk-node/examples/node/src/sessions.ts
Original file line number Diff line number Diff line change
Expand Up @@ -9,33 +9,25 @@ const sessionIdtoRevoke = process.env.SESSION_ID_TO_REVOKE || '';
const sessionToken = process.env.SESSION_TOKEN || '';

console.log('Get session list');
const sessionList = await sessions.getSessionList();
const { data: sessionList } = await sessions.getSessionList();
console.log(sessionList);

console.log('Get session list filtered by userId');
const filteredSessions1 = await sessions.getSessionList({ userId });
const { data: filteredSessions1 } = await sessions.getSessionList({ userId });
console.log(filteredSessions1);

console.log('Get session list filtered by clientId');
const filteredSessions2 = await sessions.getSessionList({ clientId });
const { data: filteredSessions2 } = await sessions.getSessionList({ clientId });
console.log(filteredSessions2);

console.log('Get single session');
const session = await sessions.getSession(sessionId);
const { data: session } = await sessions.getSession(sessionId);
console.log(session);

try {
console.log('Revoke session');
const revokedSession = await sessions.revokeSession(sessionIdtoRevoke);
console.log(revokedSession);
} catch (error) {
console.log(error);
}
console.log('Revoke session');
const { data: revokedSession } = await sessions.revokeSession(sessionIdtoRevoke);
console.log(revokedSession);

try {
console.log('Verify session');
const verifiedSession = await sessions.verifySession(sessionId, sessionToken);
console.log(verifiedSession);
} catch (error) {
console.log(error);
}
console.log('Verify session');
const { data: verifiedSession } = await sessions.verifySession(sessionId, sessionToken);
console.log(verifiedSession);
Loading

0 comments on commit bbdb0cd

Please sign in to comment.