Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
55 changes: 49 additions & 6 deletions src/commands/profiles.ts
Original file line number Diff line number Diff line change
Expand Up @@ -504,13 +504,33 @@ profiles.command('search', {
// ── Update profile (merge identity properties) ──

export interface UpdateProfileOptions {
properties: string
properties?: string
unset?: string
}

export function buildUpdateProfileBody(options: UpdateProfileOptions) {
const body = parseJsonObject(options.properties, '--properties')
const body: Record<string, unknown> = options.properties
? parseJsonObject(options.properties, '--properties')
: {}
if (options.unset) {
const keys = options.unset
.split(',')
.map((key) => key.trim())
.filter((key) => key.length > 0)
if (keys.length === 0) {
throw new Error('--unset must list at least one property key')
}
for (const key of keys) {
if (key === 'user_id') {
throw new Error(
'user_id cannot be unset (it participates in identity stitching)',
)
}
body[key] = null
}
}
if (Object.keys(body).length === 0) {
throw new Error('--properties must contain at least one key')
throw new Error('provide --properties and/or --unset with at least one key')
}
return body
}
Expand All @@ -535,8 +555,15 @@ profiles.command('update', {
options: z.object({
properties: z
.string()
.optional()
.describe(
'JSON object of properties to merge; a null value unsets (deletes) that property. Allowed keys: user_id, display_name, email, farcaster, discord, twitter, telegram, instagram, website, github, linkedin, facebook, tiktok, youtube, reddit, avatar, description, location, ens, lens, basenames, linea',
),
unset: z
.string()
.optional()
.describe(
'JSON object of properties to merge. Allowed keys: user_id, display_name, email, farcaster, discord, twitter, telegram, instagram, website, github, linkedin, facebook, tiktok, youtube, reddit, avatar, description, location, ens, lens, basenames, linea',
'Comma-separated property keys to unset (delete), e.g. "email,twitter". Shorthand for null values in --properties. user_id cannot be unset.',
),
}),
examples: [
Expand All @@ -552,8 +579,18 @@ profiles.command('update', {
options: { properties: '{"email":"alice@example.com"}' },
description: 'Set just the email',
},
{
args: { address: '0xd8dA6BF26964aF9D7eEd9e03E53415D37aA96045' },
options: { unset: 'email,twitter' },
description: 'Delete the email and Twitter properties',
},
{
args: { address: '0xd8dA6BF26964aF9D7eEd9e03E53415D37aA96045' },
options: { properties: '{"display_name":"alice.eth"}', unset: 'email' },
description: 'Set a new display name and delete the email in one call',
},
],
hint: 'Requires profiles:write scope on your API key. Only the listed keys are accepted; unknown keys are rejected.',
hint: 'Requires profiles:write scope on your API key. Only the listed keys are accepted; unknown keys are rejected. Deleting a property hides any globally-enriched fallback value too; user_id cannot be unset.',
run({ args, options }) {
return updateProfileRun(args.address, options)
},
Expand Down Expand Up @@ -599,7 +636,7 @@ profilesProperties.command('batch', {
rows: z
.string()
.describe(
'JSON array of flat {address,...properties} objects. ENS names are not resolved in batch requests.',
'JSON array of flat {address,...properties} objects; a null value unsets (deletes) that property (user_id cannot be unset). ENS names are not resolved in batch requests.',
),
}),
examples: [
Expand All @@ -609,6 +646,12 @@ profilesProperties.command('batch', {
},
description: 'Batch set display names and emails',
},
{
options: {
rows: '[{"address":"0xd8dA6BF26964aF9D7eEd9e03E53415D37aA96045","email":null}]',
},
description: 'Batch delete emails (null unsets a property)',
},
],
hint: 'Requires profiles:write scope on your API key. Unknown keys are ignored by the API; invalid rows are quarantined.',
run({ options }) {
Expand Down
40 changes: 40 additions & 0 deletions test/commands/profiles.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -2,6 +2,7 @@ import { expect } from 'chai';
import {
batchCreateProfileLabelsRun,
batchUpdateProfilesRun,
buildUpdateProfileBody,
createProfileLabelRun,
deleteProfileLabelRun,
getProfileRun,
Expand Down Expand Up @@ -83,6 +84,45 @@ describe('commands/profiles', function () {
updateProfileRun(KNOWN_ADDRESS, { properties: '{}' }),
).to.throw(/at least one key/);
});

it('throws when neither --properties nor --unset is provided', function () {
expect(() => updateProfileRun(KNOWN_ADDRESS, {})).to.throw(
/properties.*unset/,
);
});

it('throws when --unset is only commas/whitespace', function () {
expect(() =>
updateProfileRun(KNOWN_ADDRESS, { unset: ' , ,' }),
).to.throw(/--unset/);
});

it('throws when --unset includes user_id', function () {
expect(() =>
updateProfileRun(KNOWN_ADDRESS, { unset: 'email,user_id' }),
).to.throw(/user_id cannot be unset/);
});

it('maps --unset keys to null values in the request body', function () {
expect(
buildUpdateProfileBody({ unset: 'email, twitter' }),
).to.deep.equal({ email: null, twitter: null });
});

it('merges --properties values with --unset nulls (unset wins on overlap)', function () {
expect(
buildUpdateProfileBody({
properties: '{"display_name":"alice.eth","email":"a@x.co"}',
unset: 'email',
}),
).to.deep.equal({ display_name: 'alice.eth', email: null });
});

it('passes explicit null values in --properties through unchanged', function () {
expect(
buildUpdateProfileBody({ properties: '{"email":null}' }),
).to.deep.equal({ email: null });
});
});

describe('createProfileLabelRun() — local validation', function () {
Expand Down