-
Notifications
You must be signed in to change notification settings - Fork 452
feat(backend, clerk-js): Deprecate metadata updates in user update methods #8587
New issue
Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.
By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.
Already on GitHub? Sign in to your account
Open
brunol95
wants to merge
10
commits into
main
Choose a base branch
from
bruno/user-5312-deprecate-update-user-metadata-fields
base: main
Could not load branches
Branch not found: {{ refName }}
Loading
Could not load tags
Nothing to show
Loading
Are you sure you want to change the base?
Some commits from the old base branch may be removed from the timeline,
and old review comments may become outdated.
Open
Changes from all commits
Commits
Show all changes
10 commits
Select commit
Hold shift + click to select a range
16fdd04
deprecate metadata updates via clerkClient.users.updateUser and user.…
brunol95 bf5b092
update api version in test
brunol95 aebfef5
user.update refresh user object on metadata only updates
brunol95 926d666
fix: isPlainObject rejects Date,Map, class interfaces
brunol95 c4ccc16
chore: fix changeset formatting
wobsoriano 34c0df9
Merge branch 'main' into bruno/user-5312-deprecate-update-user-metada…
wobsoriano 34c3fc8
address lint
brunol95 e0df27e
Merge branch 'bruno/user-5312-deprecate-update-user-metadata-fields' …
brunol95 4cf3a28
change supported version to latest for testing
brunol95 d63df08
remove unused import
brunol95 File filter
Filter by extension
Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
There are no files selected for viewing
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -0,0 +1,23 @@ | ||
| --- | ||
| '@clerk/backend': minor | ||
| --- | ||
|
|
||
| Add `clerkClient.users.replaceUserMetadata(userId, params)` for replacing a user's metadata fields in full. | ||
|
|
||
| Use `replaceUserMetadata` when the provided metadata should become the complete value for that metadata field: | ||
|
|
||
| ```ts | ||
| await clerkClient.users.replaceUserMetadata(userId, { | ||
| publicMetadata: { plan: 'pro' }, | ||
| }); | ||
| ``` | ||
|
|
||
| Use `clerkClient.users.updateUserMetadata(userId, params)` when you want to partially update metadata with deep-merge semantics: | ||
|
|
||
| ```ts | ||
| await clerkClient.users.updateUserMetadata(userId, { | ||
| publicMetadata: { onboardingComplete: true }, | ||
| }); | ||
| ``` | ||
|
|
||
| The `publicMetadata`, `privateMetadata`, and `unsafeMetadata` parameters on `clerkClient.users.updateUser()` are now deprecated. They continue to work, but new code should use `updateUserMetadata()` for partial updates or `replaceUserMetadata()` for full replacement. |
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -0,0 +1,24 @@ | ||
| --- | ||
| '@clerk/clerk-js': patch | ||
| '@clerk/shared': patch | ||
| --- | ||
|
|
||
| Deprecate passing `unsafeMetadata` to `user.update()`. | ||
|
|
||
| Use `user.updateMetadata()` when you want to partially update unsafe metadata with deep-merge semantics: | ||
|
|
||
| ```ts | ||
| await user.updateMetadata({ | ||
| unsafeMetadata: { onboardingComplete: true }, | ||
| }); | ||
| ``` | ||
|
|
||
| `user.update({ unsafeMetadata })` continues to work for now and preserves its existing full-replacement behavior: | ||
|
|
||
| ```ts | ||
| await user.update({ | ||
| unsafeMetadata: { theme: 'dark' }, | ||
| }); | ||
| ``` | ||
|
|
||
| New code should prefer `user.updateMetadata({ unsafeMetadata })` for metadata-only updates. |
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -0,0 +1,179 @@ | ||
| import { http, HttpResponse } from 'msw'; | ||
| import { describe, expect, it, vi } from 'vitest'; | ||
|
|
||
| import { server, validateHeaders } from '../../mock-server'; | ||
| import { createBackendApiClient } from '../factory'; | ||
|
|
||
| describe('UserAPI', () => { | ||
| const apiClient = createBackendApiClient({ | ||
| apiUrl: 'https://api.clerk.test', | ||
| secretKey: 'deadbeef', | ||
| }); | ||
|
|
||
| const mockUserResponse = { | ||
| object: 'user', | ||
| id: 'user_123', | ||
| public_metadata: {}, | ||
| private_metadata: {}, | ||
| unsafe_metadata: {}, | ||
| }; | ||
|
|
||
| describe('updateUser', () => { | ||
| it('calls PATCH /users/{id} when no metadata fields are provided', async () => { | ||
| const patchHandler = vi.fn(async ({ request }: { request: Request }) => { | ||
| const body = await request.json(); | ||
| expect(body).toEqual({ first_name: 'Jane' }); | ||
| return HttpResponse.json(mockUserResponse); | ||
| }); | ||
|
|
||
| server.use(http.patch('https://api.clerk.test/v1/users/user_123', validateHeaders(patchHandler))); | ||
|
|
||
| const response = await apiClient.users.updateUser('user_123', { firstName: 'Jane' }); | ||
|
|
||
| expect(patchHandler).toHaveBeenCalledTimes(1); | ||
| expect(response.id).toBe('user_123'); | ||
| }); | ||
|
|
||
| it('routes metadata to PUT /users/{id}/metadata when only metadata is provided', async () => { | ||
| const patchHandler = vi.fn(() => HttpResponse.json(mockUserResponse)); | ||
| const putHandler = vi.fn(async ({ request }: { request: Request }) => { | ||
| const body = await request.json(); | ||
| expect(body).toEqual({ | ||
| public_metadata: { foo: 'bar' }, | ||
| }); | ||
| return HttpResponse.json({ | ||
| ...mockUserResponse, | ||
| public_metadata: { foo: 'bar' }, | ||
| }); | ||
| }); | ||
|
|
||
| server.use( | ||
| http.patch('https://api.clerk.test/v1/users/user_123', validateHeaders(patchHandler)), | ||
| http.put('https://api.clerk.test/v1/users/user_123/metadata', validateHeaders(putHandler)), | ||
| ); | ||
|
|
||
| const response = await apiClient.users.updateUser('user_123', { | ||
| publicMetadata: { foo: 'bar' }, | ||
| }); | ||
|
|
||
| expect(patchHandler).not.toHaveBeenCalled(); | ||
| expect(putHandler).toHaveBeenCalledTimes(1); | ||
| expect(response.publicMetadata).toEqual({ foo: 'bar' }); | ||
| }); | ||
|
|
||
| it('splits mixed calls: PATCH for non-metadata, then PUT for metadata', async () => { | ||
| const calls: string[] = []; | ||
|
|
||
| const patchHandler = vi.fn(async ({ request }: { request: Request }) => { | ||
| calls.push('patch'); | ||
| const body = await request.json(); | ||
| expect(body).toEqual({ first_name: 'Jane' }); | ||
| return HttpResponse.json(mockUserResponse); | ||
| }); | ||
|
|
||
| const putHandler = vi.fn(async ({ request }: { request: Request }) => { | ||
| calls.push('put'); | ||
| const body = await request.json(); | ||
| expect(body).toEqual({ | ||
| public_metadata: { plan: 'pro' }, | ||
| private_metadata: { invoice: 'inv_1' }, | ||
| }); | ||
| return HttpResponse.json({ | ||
| ...mockUserResponse, | ||
| first_name: 'Jane', | ||
| public_metadata: { plan: 'pro' }, | ||
| private_metadata: { invoice: 'inv_1' }, | ||
| }); | ||
| }); | ||
|
|
||
| server.use( | ||
| http.patch('https://api.clerk.test/v1/users/user_123', validateHeaders(patchHandler)), | ||
| http.put('https://api.clerk.test/v1/users/user_123/metadata', validateHeaders(putHandler)), | ||
| ); | ||
|
|
||
| const response = await apiClient.users.updateUser('user_123', { | ||
| firstName: 'Jane', | ||
| publicMetadata: { plan: 'pro' }, | ||
| privateMetadata: { invoice: 'inv_1' }, | ||
| }); | ||
|
|
||
| expect(patchHandler).toHaveBeenCalledTimes(1); | ||
| expect(putHandler).toHaveBeenCalledTimes(1); | ||
| // PATCH must run before PUT so the user state from PUT is the latest. | ||
| expect(calls).toEqual(['patch', 'put']); | ||
| expect(response.firstName).toBe('Jane'); | ||
| expect(response.publicMetadata).toEqual({ plan: 'pro' }); | ||
| }); | ||
|
|
||
| it('passes only metadata fields that were explicitly provided to PUT', async () => { | ||
| const putHandler = vi.fn(async ({ request }: { request: Request }) => { | ||
| const body = (await request.json()) as Record<string, unknown>; | ||
| // Only unsafe_metadata was provided. The other two should be undefined, | ||
| // which serializes to "field omitted" on the wire — leaving those | ||
| // columns untouched server-side. | ||
| expect(body.unsafe_metadata).toEqual({ device: 'mobile' }); | ||
| expect(body).not.toHaveProperty('public_metadata'); | ||
| expect(body).not.toHaveProperty('private_metadata'); | ||
| return HttpResponse.json({ | ||
| ...mockUserResponse, | ||
| unsafe_metadata: { device: 'mobile' }, | ||
| }); | ||
| }); | ||
|
|
||
| server.use(http.put('https://api.clerk.test/v1/users/user_123/metadata', validateHeaders(putHandler))); | ||
|
|
||
| await apiClient.users.updateUser('user_123', { | ||
| unsafeMetadata: { device: 'mobile' }, | ||
| }); | ||
|
|
||
| expect(putHandler).toHaveBeenCalledTimes(1); | ||
| }); | ||
| }); | ||
|
|
||
| describe('updateUserMetadata', () => { | ||
| it('still hits PATCH /users/{id}/metadata (unchanged)', async () => { | ||
| const patchHandler = vi.fn(async ({ request }: { request: Request }) => { | ||
| const body = await request.json(); | ||
| expect(body).toEqual({ | ||
| public_metadata: { merge: true }, | ||
| }); | ||
| return HttpResponse.json({ | ||
| ...mockUserResponse, | ||
| public_metadata: { merge: true }, | ||
| }); | ||
| }); | ||
|
|
||
| server.use(http.patch('https://api.clerk.test/v1/users/user_123/metadata', validateHeaders(patchHandler))); | ||
|
|
||
| await apiClient.users.updateUserMetadata('user_123', { | ||
| publicMetadata: { merge: true }, | ||
| }); | ||
|
|
||
| expect(patchHandler).toHaveBeenCalledTimes(1); | ||
| }); | ||
| }); | ||
|
|
||
| describe('replaceUserMetadata', () => { | ||
| it('hits PUT /users/{id}/metadata', async () => { | ||
| const putHandler = vi.fn(async ({ request }: { request: Request }) => { | ||
| const body = await request.json(); | ||
| expect(body).toEqual({ | ||
| public_metadata: { replaced: true }, | ||
| }); | ||
| return HttpResponse.json({ | ||
| ...mockUserResponse, | ||
| public_metadata: { replaced: true }, | ||
| }); | ||
| }); | ||
|
|
||
| server.use(http.put('https://api.clerk.test/v1/users/user_123/metadata', validateHeaders(putHandler))); | ||
|
|
||
| const response = await apiClient.users.replaceUserMetadata('user_123', { | ||
| publicMetadata: { replaced: true }, | ||
| }); | ||
|
|
||
| expect(putHandler).toHaveBeenCalledTimes(1); | ||
| expect(response.publicMetadata).toEqual({ replaced: true }); | ||
| }); | ||
| }); | ||
| }); |
Oops, something went wrong.
Oops, something went wrong.
Add this suggestion to a batch that can be applied as a single commit.
This suggestion is invalid because no changes were made to the code.
Suggestions cannot be applied while the pull request is closed.
Suggestions cannot be applied while viewing a subset of changes.
Only one suggestion per line can be applied in a batch.
Add this suggestion to a batch that can be applied as a single commit.
Applying suggestions on deleted lines is not supported.
You must change the existing code in this line in order to create a valid suggestion.
Outdated suggestions cannot be applied.
This suggestion has been applied or marked resolved.
Suggestions cannot be applied from pending reviews.
Suggestions cannot be applied on multi-line comments.
Suggestions cannot be applied while the pull request is queued to merge.
Suggestion cannot be applied right now. Please check back later.
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Let's split this into separate changesets so each package changelog stays focused 👍🏼
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
done