Skip to content

Commit 8e5c881

Browse files
dimklnikosdouvlis
andauthored
fix(backend,nextjs): Revert { data, errors } return values to v4 format for clerkClient methods and jwt/token helpers (clerk#2633)
* fix(backend): Make signJwt consistent by returning error instead of throwing * fix(backend): Revert `{ data, errors }` return value from api client This change will make all the BackendAPI methods to either return the value or throw error instead of returning `{ data, errors }`. We also (temporarily) added the `totalCount` property when we expect an array of resources to allow passing the `total_count` of paginated responses from the response body to the return value. * fix(backend): Revert `{ data, errors }` return value from jwt/token helpers The following helpers were reverted to return their `data` or throw an error. Internally we will keep the `{ data, errors }` format but added a compatibility layer to avoid exposing this breaking change: - `import { verifyToken } from '@clerk/backend'` - `import { signJwt, hasValidSignature, decodeJwt, verifyJwt } from '@clerk/backend/jwt'` * fix(nextjs): Apply changes related to adding legacy return value compat layer * fix(clerk-sdk-node): Update examples related to backend legacy return value compat layer * chore(repo): Add changeset * feat(backend): Paginated responses now return { data, totalCount } instead of an array --------- Co-authored-by: Nikos Douvlis <nikosdouvlis@gmail.com>
1 parent 24de669 commit 8e5c881

File tree

25 files changed

+173
-193
lines changed

25 files changed

+173
-193
lines changed

.changeset/gentle-radios-shout.md

Lines changed: 16 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,16 @@
1+
---
2+
'@clerk/clerk-sdk-node': patch
3+
'@clerk/backend': major
4+
'@clerk/nextjs': patch
5+
---
6+
7+
The following paginated APIs now return `{ data, totalCount }` instead of simple arrays, in order to make building paginated UIs easier:
8+
- `clerkClient.users.getOrganizationMembershipList(...)`
9+
- `clerkClient.organization.getOrganizationList(...)`
10+
- `clerkClient.organization.getOrganizationInvitationList(...)`
11+
12+
Revert changing the `{ data, errors }` return value of the following helpers to throw the `errors` or return the `data` (keep v4 format):
13+
14+
- `import { verifyToken } from '@clerk/backend'`
15+
- `import { signJwt, hasValidSignature, decodeJwt, verifyJwt } from '@clerk/backend/jwt'`
16+
- BAPI `clerkClient` methods eg (`clerkClient.users.getUserList(...)`)

integration/cleanup/cleanup.setup.ts

Lines changed: 1 addition & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -19,17 +19,12 @@ setup('cleanup instances ', async () => {
1919
for (const entry of entries) {
2020
console.log(`Cleanup for ${entry!.secretKey.replace(/(sk_test_)(.+)(...)/, '$1***$3')}`);
2121
const clerkClient = createClerkClient({ secretKey: entry!.secretKey, apiUrl: entry?.apiUrl });
22-
const { data: users, errors } = await clerkClient.users.getUserList({
22+
const users = await clerkClient.users.getUserList({
2323
orderBy: '-created_at',
2424
query: 'clerkcookie',
2525
limit: 100,
2626
});
2727

28-
if (errors) {
29-
console.log(errors);
30-
return;
31-
}
32-
3328
const batches = batchElements(skipUsersThatWereCreatedToday(users), 5);
3429
for (const batch of batches) {
3530
console.log(`Starting batch...`);

integration/testUtils/usersService.ts

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -39,7 +39,7 @@ export const createUserService = (clerkClient: ReturnType<typeof Clerk>) => {
3939
},
4040
createFakeOrganization: async (userId: string) => {
4141
const name = faker.animal.dog();
42-
const { data: organization } = await clerkClient.organizations.createOrganization({
42+
const organization = await clerkClient.organizations.createOrganization({
4343
name: faker.animal.dog(),
4444
createdBy: userId,
4545
});

integration/tests/protect.test.ts

Lines changed: 2 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -15,11 +15,10 @@ testAgainstRunningApps({ withEnv: [appConfigs.envs.withCustomRoles] })('authoriz
1515
test.beforeAll(async () => {
1616
const m = createTestUtils({ app });
1717
fakeAdmin = m.services.users.createFakeUser();
18-
const { data: admin } = await m.services.users.createBapiUser(fakeAdmin);
18+
const admin = await m.services.users.createBapiUser(fakeAdmin);
1919
fakeOrganization = await m.services.users.createFakeOrganization(admin.id);
2020
fakeViewer = m.services.users.createFakeUser();
21-
const { data: viewer } = await m.services.users.createBapiUser(fakeViewer);
22-
21+
const viewer = await m.services.users.createBapiUser(fakeViewer);
2322
await m.services.clerk.organizations.createOrganizationMembership({
2423
organizationId: fakeOrganization.organization.id,
2524
role: 'org:viewer' as OrganizationMembershipRole,

packages/backend/src/api/__tests__/factory.test.ts

Lines changed: 33 additions & 67 deletions
Original file line numberDiff line numberDiff line change
@@ -4,14 +4,7 @@ import sinon from 'sinon';
44
// @ts-ignore
55
import userJson from '../../fixtures/user.json';
66
import runtime from '../../runtime';
7-
import {
8-
assertErrorResponse,
9-
assertResponse,
10-
jsonError,
11-
jsonNotOk,
12-
jsonOk,
13-
jsonPaginatedOk,
14-
} from '../../util/testUtils';
7+
import { jsonError, jsonNotOk, jsonOk, jsonPaginatedOk } from '../../util/testUtils';
158
import { createBackendApiClient } from '../factory';
169

1710
export default (QUnit: QUnit) => {
@@ -35,16 +28,12 @@ export default (QUnit: QUnit) => {
3528

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

38-
assertResponse(assert, response);
39-
const { data: payload, totalCount } = response;
40-
41-
assert.equal(payload.firstName, 'John');
42-
assert.equal(payload.lastName, 'Doe');
43-
assert.equal(payload.emailAddresses[0].emailAddress, 'john.doe@clerk.test');
44-
assert.equal(payload.phoneNumbers[0].phoneNumber, '+311-555-2368');
45-
assert.equal(payload.externalAccounts[0].emailAddress, 'john.doe@clerk.test');
46-
assert.equal(payload.publicMetadata.zodiac_sign, 'leo');
47-
assert.equal(totalCount, undefined);
31+
assert.equal(response.firstName, 'John');
32+
assert.equal(response.lastName, 'Doe');
33+
assert.equal(response.emailAddresses[0].emailAddress, 'john.doe@clerk.test');
34+
assert.equal(response.phoneNumbers[0].phoneNumber, '+311-555-2368');
35+
assert.equal(response.externalAccounts[0].emailAddress, 'john.doe@clerk.test');
36+
assert.equal(response.publicMetadata.zodiac_sign, 'leo');
4837

4938
assert.ok(
5039
fakeFetch.calledOnceWith('https://api.clerk.test/v1/users/user_deadbeef', {
@@ -63,16 +52,13 @@ export default (QUnit: QUnit) => {
6352
fakeFetch.onCall(0).returns(jsonOk([userJson]));
6453

6554
const response = await apiClient.users.getUserList({ offset: 2, limit: 5 });
66-
assertResponse(assert, response);
67-
const { data: payload, totalCount } = response;
6855

69-
assert.equal(payload[0].firstName, 'John');
70-
assert.equal(payload[0].lastName, 'Doe');
71-
assert.equal(payload[0].emailAddresses[0].emailAddress, 'john.doe@clerk.test');
72-
assert.equal(payload[0].phoneNumbers[0].phoneNumber, '+311-555-2368');
73-
assert.equal(payload[0].externalAccounts[0].emailAddress, 'john.doe@clerk.test');
74-
assert.equal(payload[0].publicMetadata.zodiac_sign, 'leo');
75-
assert.equal(totalCount, 1);
56+
assert.equal(response[0].firstName, 'John');
57+
assert.equal(response[0].lastName, 'Doe');
58+
assert.equal(response[0].emailAddresses[0].emailAddress, 'john.doe@clerk.test');
59+
assert.equal(response[0].phoneNumbers[0].phoneNumber, '+311-555-2368');
60+
assert.equal(response[0].externalAccounts[0].emailAddress, 'john.doe@clerk.test');
61+
assert.equal(response[0].publicMetadata.zodiac_sign, 'leo');
7662

7763
assert.ok(
7864
fakeFetch.calledOnceWith('https://api.clerk.test/v1/users?offset=2&limit=5', {
@@ -88,32 +74,18 @@ export default (QUnit: QUnit) => {
8874

8975
test('executes a successful backend API request for a paginated response', async assert => {
9076
fakeFetch = sinon.stub(runtime, 'fetch');
91-
fakeFetch.onCall(0).returns(jsonPaginatedOk([userJson], 3));
77+
fakeFetch.onCall(0).returns(jsonPaginatedOk([{ id: '1' }], 3));
9278

93-
const response = await apiClient.users.getUserList({ offset: 2, limit: 5 });
94-
assertResponse(assert, response);
95-
const { data: payload, totalCount } = response;
96-
97-
assert.equal(payload[0].firstName, 'John');
98-
assert.equal(payload[0].lastName, 'Doe');
99-
assert.equal(payload[0].emailAddresses[0].emailAddress, 'john.doe@clerk.test');
100-
assert.equal(payload[0].phoneNumbers[0].phoneNumber, '+311-555-2368');
101-
assert.equal(payload[0].externalAccounts[0].emailAddress, 'john.doe@clerk.test');
102-
assert.equal(payload[0].publicMetadata.zodiac_sign, 'leo');
79+
const { data: response, totalCount } = await apiClient.users.getOrganizationMembershipList({
80+
offset: 2,
81+
limit: 5,
82+
userId: 'user_123',
83+
});
84+
85+
assert.equal(response[0].id, '1');
10386
// payload.length is different from response total_count to check that totalCount use the total_count from response
104-
assert.equal(payload.length, 1);
10587
assert.equal(totalCount, 3);
106-
107-
assert.ok(
108-
fakeFetch.calledOnceWith('https://api.clerk.test/v1/users?offset=2&limit=5', {
109-
method: 'GET',
110-
headers: {
111-
Authorization: 'Bearer deadbeef',
112-
'Content-Type': 'application/json',
113-
'User-Agent': '@clerk/backend@0.0.0-test',
114-
},
115-
}),
116-
);
88+
assert.equal(response.length, 1);
11789
});
11890

11991
test('executes a successful backend API request to create a new resource', async assert => {
@@ -127,10 +99,8 @@ export default (QUnit: QUnit) => {
12799
star_sign: 'Leon',
128100
},
129101
});
130-
assertResponse(assert, response);
131-
const { data: payload } = response;
132102

133-
assert.equal(payload.firstName, 'John');
103+
assert.equal(response.firstName, 'John');
134104

135105
assert.ok(
136106
fakeFetch.calledOnceWith('https://api.clerk.test/v1/users', {
@@ -162,16 +132,14 @@ export default (QUnit: QUnit) => {
162132
fakeFetch = sinon.stub(runtime, 'fetch');
163133
fakeFetch.onCall(0).returns(jsonNotOk({ errors: [mockErrorPayload], clerk_trace_id: traceId }));
164134

165-
const response = await apiClient.users.getUser('user_deadbeef');
166-
assertErrorResponse(assert, response);
135+
const errResponse = await apiClient.users.getUser('user_deadbeef').catch(err => err);
167136

168-
assert.equal(response.clerkTraceId, traceId);
169-
assert.equal(response.status, 422);
170-
assert.equal(response.statusText, '422');
171-
assert.equal(response.errors[0].code, 'whatever_error');
172-
assert.equal(response.errors[0].message, 'whatever error');
173-
assert.equal(response.errors[0].longMessage, 'some long message');
174-
assert.equal(response.errors[0].meta.paramName, 'some param');
137+
assert.equal(errResponse.clerkTraceId, traceId);
138+
assert.equal(errResponse.status, 422);
139+
assert.equal(errResponse.errors[0].code, 'whatever_error');
140+
assert.equal(errResponse.errors[0].message, 'whatever error');
141+
assert.equal(errResponse.errors[0].longMessage, 'some long message');
142+
assert.equal(errResponse.errors[0].meta.paramName, 'some param');
175143

176144
assert.ok(
177145
fakeFetch.calledOnceWith('https://api.clerk.test/v1/users/user_deadbeef', {
@@ -189,12 +157,10 @@ export default (QUnit: QUnit) => {
189157
fakeFetch = sinon.stub(runtime, 'fetch');
190158
fakeFetch.onCall(0).returns(jsonError({ errors: [] }));
191159

192-
const response = await apiClient.users.getUser('user_deadbeef');
193-
assertErrorResponse(assert, response);
160+
const errResponse = await apiClient.users.getUser('user_deadbeef').catch(err => err);
194161

195-
assert.equal(response.status, 500);
196-
assert.equal(response.statusText, '500');
197-
assert.equal(response.clerkTraceId, 'mock_cf_ray');
162+
assert.equal(errResponse.status, 500);
163+
assert.equal(errResponse.clerkTraceId, 'mock_cf_ray');
198164

199165
assert.ok(
200166
fakeFetch.calledOnceWith('https://api.clerk.test/v1/users/user_deadbeef', {

packages/backend/src/api/endpoints/OrganizationApi.ts

Lines changed: 3 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -8,6 +8,7 @@ import type {
88
OrganizationInvitationStatus,
99
OrganizationMembership,
1010
} from '../resources';
11+
import type { PaginatedResourceResponse } from '../resources/Deserializer';
1112
import type { OrganizationMembershipRole } from '../resources/Enums';
1213
import { AbstractAPI } from './AbstractApi';
1314

@@ -95,7 +96,7 @@ type RevokeOrganizationInvitationParams = {
9596

9697
export class OrganizationAPI extends AbstractAPI {
9798
public async getOrganizationList(params?: GetOrganizationListParams) {
98-
return this.request<Organization[]>({
99+
return this.request<PaginatedResourceResponse<Organization[]>>({
99100
method: 'GET',
100101
path: basePath,
101102
queryParams: params,
@@ -234,7 +235,7 @@ export class OrganizationAPI extends AbstractAPI {
234235
const { organizationId, status, limit, offset } = params;
235236
this.requireId(organizationId);
236237

237-
return this.request<OrganizationInvitation[]>({
238+
return this.request<PaginatedResourceResponse<OrganizationInvitation[]>>({
238239
method: 'GET',
239240
path: joinPaths(basePath, organizationId, 'invitations'),
240241
queryParams: { status, limit, offset },

packages/backend/src/api/endpoints/UserApi.ts

Lines changed: 2 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -3,6 +3,7 @@ import type { ClerkPaginationRequest, OAuthProvider } from '@clerk/types';
33
import runtime from '../../runtime';
44
import { joinPaths } from '../../util/path';
55
import type { OauthAccessToken, OrganizationMembership, User } from '../resources';
6+
import type { PaginatedResourceResponse } from '../resources/Deserializer';
67
import { AbstractAPI } from './AbstractApi';
78

89
const basePath = '/users';
@@ -199,7 +200,7 @@ export class UserAPI extends AbstractAPI {
199200
const { userId, limit, offset } = params;
200201
this.requireId(userId);
201202

202-
return this.request<OrganizationMembership[]>({
203+
return this.request<PaginatedResourceResponse<OrganizationMembership[]>>({
203204
method: 'GET',
204205
path: joinPaths(basePath, userId, 'organization_memberships'),
205206
queryParams: { limit, offset },

packages/backend/src/api/request.ts

Lines changed: 32 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -1,4 +1,4 @@
1-
import { parseError } from '@clerk/shared/error';
1+
import { ClerkAPIResponseError, parseError } from '@clerk/shared/error';
22
import type { ClerkAPIError, ClerkAPIErrorJSON } from '@clerk/types';
33
import snakecaseKeys from 'snakecase-keys';
44

@@ -55,7 +55,7 @@ type BuildRequestOptions = {
5555
userAgent?: string;
5656
};
5757
export function buildRequest(options: BuildRequestOptions) {
58-
return async <T>(requestOptions: ClerkBackendApiRequestOptions): Promise<ClerkBackendApiResponse<T>> => {
58+
const requestFn = async <T>(requestOptions: ClerkBackendApiRequestOptions): Promise<ClerkBackendApiResponse<T>> => {
5959
const { secretKey, apiUrl = API_URL, apiVersion = API_VERSION, userAgent = USER_AGENT } = options;
6060
const { path, method, queryParams, headerParams, bodyParams, formData } = requestOptions;
6161

@@ -149,6 +149,8 @@ export function buildRequest(options: BuildRequestOptions) {
149149
};
150150
}
151151
};
152+
153+
return withLegacyRequestReturn(requestFn);
152154
}
153155

154156
// Returns either clerk_trace_id if present in response json, otherwise defaults to CF-Ray header
@@ -169,3 +171,31 @@ function parseErrors(data: unknown): ClerkAPIError[] {
169171
}
170172
return [];
171173
}
174+
175+
type LegacyRequestFunction = <T>(requestOptions: ClerkBackendApiRequestOptions) => Promise<T>;
176+
177+
// TODO(dimkl): Will be probably be dropped in next major version
178+
function withLegacyRequestReturn(cb: any): LegacyRequestFunction {
179+
return async (...args) => {
180+
// @ts-ignore
181+
const { data, errors, totalCount, status, statusText, clerkTraceId } = await cb<T>(...args);
182+
if (errors) {
183+
// instead of passing `data: errors`, we have set the `error.errors` because
184+
// the errors returned from callback is already parsed and passing them as `data`
185+
// will not be able to assign them to the instance
186+
const error = new ClerkAPIResponseError(statusText || '', {
187+
data: [],
188+
status,
189+
clerkTraceId,
190+
});
191+
error.errors = errors;
192+
throw error;
193+
}
194+
195+
if (typeof totalCount !== 'undefined') {
196+
return { data, totalCount };
197+
}
198+
199+
return data;
200+
};
201+
}

packages/backend/src/api/resources/Deserializer.ts

Lines changed: 5 additions & 8 deletions
Original file line numberDiff line numberDiff line change
@@ -24,19 +24,16 @@ type ResourceResponse<T> = {
2424
data: T;
2525
};
2626

27-
type PaginatedResponse<T> = {
28-
data: T;
29-
totalCount?: number;
27+
export type PaginatedResourceResponse<T> = ResourceResponse<T> & {
28+
totalCount: number;
3029
};
3130

32-
export function deserialize<U = any>(payload: unknown): PaginatedResponse<U> | ResourceResponse<U> {
31+
export function deserialize<U = any>(payload: unknown): PaginatedResourceResponse<U> | ResourceResponse<U> {
3332
let data, totalCount: number | undefined;
3433

3534
if (Array.isArray(payload)) {
36-
data = payload.map(item => jsonToObject(item)) as U;
37-
totalCount = payload.length;
38-
39-
return { data, totalCount };
35+
const data = payload.map(item => jsonToObject(item)) as U;
36+
return { data };
4037
} else if (isPaginated(payload)) {
4138
data = payload.data.map(item => jsonToObject(item)) as U;
4239
totalCount = payload.total_count;

packages/backend/src/index.ts

Lines changed: 4 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -4,12 +4,15 @@ import type { SDKMetadata } from '@clerk/types';
44

55
import type { ApiClient, CreateBackendApiOptions } from './api';
66
import { createBackendApiClient } from './api';
7+
import { withLegacyReturn } from './jwt/legacyReturn';
78
import type { CreateAuthenticateRequestOptions } from './tokens/factory';
89
import { createAuthenticateRequest } from './tokens/factory';
10+
import { verifyToken as _verifyToken } from './tokens/verify';
911

1012
export type { Organization, Session, User, WebhookEvent, WebhookEventType } from './api/resources';
1113
export type { VerifyTokenOptions } from './tokens/verify';
12-
export { verifyToken } from './tokens/verify';
14+
15+
export const verifyToken = withLegacyReturn(_verifyToken);
1316

1417
export type ClerkOptions = CreateBackendApiOptions &
1518
Partial<

0 commit comments

Comments
 (0)