-
-
Notifications
You must be signed in to change notification settings - Fork 26
Refactored Account to improve types & reduce boilerplate #566
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
Changes from all commits
File filter
Filter by extension
Conversations
Jump to
Diff view
Diff view
There are no files selected for viewing
| Original file line number | Diff line number | Diff line change | ||||||||||||||||||||||||||||||||||||||||||||||||
|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|
| @@ -1,161 +1,91 @@ | ||||||||||||||||||||||||||||||||||||||||||||||||||
| import { randomUUID } from 'node:crypto'; | ||||||||||||||||||||||||||||||||||||||||||||||||||
| import { BaseEntity } from '../core/base.entity'; | ||||||||||||||||||||||||||||||||||||||||||||||||||
| import { type CreatePostType, PostType } from '../post/post.entity'; | ||||||||||||||||||||||||||||||||||||||||||||||||||
| import type { Site } from '../site/site.service'; | ||||||||||||||||||||||||||||||||||||||||||||||||||
|
|
||||||||||||||||||||||||||||||||||||||||||||||||||
| export type PersistedAccount = Account & { id: number }; | ||||||||||||||||||||||||||||||||||||||||||||||||||
| export interface Account { | ||||||||||||||||||||||||||||||||||||||||||||||||||
| readonly id: number; | ||||||||||||||||||||||||||||||||||||||||||||||||||
| readonly uuid: string; | ||||||||||||||||||||||||||||||||||||||||||||||||||
| readonly username: string; | ||||||||||||||||||||||||||||||||||||||||||||||||||
| readonly name: string | null; | ||||||||||||||||||||||||||||||||||||||||||||||||||
| readonly bio: string | null; | ||||||||||||||||||||||||||||||||||||||||||||||||||
| readonly url: URL; | ||||||||||||||||||||||||||||||||||||||||||||||||||
| readonly avatarUrl: URL | null; | ||||||||||||||||||||||||||||||||||||||||||||||||||
| readonly bannerImageUrl: URL | null; | ||||||||||||||||||||||||||||||||||||||||||||||||||
| readonly apId: URL; | ||||||||||||||||||||||||||||||||||||||||||||||||||
| readonly apFollowers: URL | null; | ||||||||||||||||||||||||||||||||||||||||||||||||||
| readonly isInternal: boolean; | ||||||||||||||||||||||||||||||||||||||||||||||||||
| /** | ||||||||||||||||||||||||||||||||||||||||||||||||||
| * Returns a new Account instance which needs to be saved. | ||||||||||||||||||||||||||||||||||||||||||||||||||
| */ | ||||||||||||||||||||||||||||||||||||||||||||||||||
| updateProfile(params: ProfileUpdateParams): Account; | ||||||||||||||||||||||||||||||||||||||||||||||||||
| /** | ||||||||||||||||||||||||||||||||||||||||||||||||||
| * @deprecated | ||||||||||||||||||||||||||||||||||||||||||||||||||
|
Collaborator
Author
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. This code is weird and shouldn't really live here, or at least not be exposed like this - why does an Account have a |
||||||||||||||||||||||||||||||||||||||||||||||||||
| */ | ||||||||||||||||||||||||||||||||||||||||||||||||||
| getApIdForPost(post: { type: CreatePostType; uuid: string }): URL; | ||||||||||||||||||||||||||||||||||||||||||||||||||
| } | ||||||||||||||||||||||||||||||||||||||||||||||||||
|
|
||||||||||||||||||||||||||||||||||||||||||||||||||
| export interface AccountData { | ||||||||||||||||||||||||||||||||||||||||||||||||||
| id: number; | ||||||||||||||||||||||||||||||||||||||||||||||||||
| uuid: string | null; | ||||||||||||||||||||||||||||||||||||||||||||||||||
| export interface AccountDraft { | ||||||||||||||||||||||||||||||||||||||||||||||||||
|
Member
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. What about this just being
Collaborator
Author
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. Yeah so - my thinking here is that these two types at the top of the file serve as documentation, and the duplication is preferred because it's easier to read right away. What do you think about that?
Member
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. Ah ok, yeh i'm fine with that, i guess it's unlikely they are going to come out of sync without us knowing |
||||||||||||||||||||||||||||||||||||||||||||||||||
| uuid: string; | ||||||||||||||||||||||||||||||||||||||||||||||||||
| username: string; | ||||||||||||||||||||||||||||||||||||||||||||||||||
| name: string | null; | ||||||||||||||||||||||||||||||||||||||||||||||||||
| bio: string | null; | ||||||||||||||||||||||||||||||||||||||||||||||||||
| url: URL; | ||||||||||||||||||||||||||||||||||||||||||||||||||
| avatarUrl: URL | null; | ||||||||||||||||||||||||||||||||||||||||||||||||||
| bannerImageUrl: URL | null; | ||||||||||||||||||||||||||||||||||||||||||||||||||
| site: Site | null; | ||||||||||||||||||||||||||||||||||||||||||||||||||
| apId: URL | null; | ||||||||||||||||||||||||||||||||||||||||||||||||||
| url: URL | null; | ||||||||||||||||||||||||||||||||||||||||||||||||||
| apId: URL; | ||||||||||||||||||||||||||||||||||||||||||||||||||
| apFollowers: URL | null; | ||||||||||||||||||||||||||||||||||||||||||||||||||
| isInternal: boolean; | ||||||||||||||||||||||||||||||||||||||||||||||||||
| } | ||||||||||||||||||||||||||||||||||||||||||||||||||
|
|
||||||||||||||||||||||||||||||||||||||||||||||||||
| export type AccountSite = { | ||||||||||||||||||||||||||||||||||||||||||||||||||
| id: number; | ||||||||||||||||||||||||||||||||||||||||||||||||||
| host: string; | ||||||||||||||||||||||||||||||||||||||||||||||||||
| }; | ||||||||||||||||||||||||||||||||||||||||||||||||||
|
|
||||||||||||||||||||||||||||||||||||||||||||||||||
| export interface ProfileUpdateParams { | ||||||||||||||||||||||||||||||||||||||||||||||||||
| name?: string | null; | ||||||||||||||||||||||||||||||||||||||||||||||||||
| bio?: string | null; | ||||||||||||||||||||||||||||||||||||||||||||||||||
| username?: string; | ||||||||||||||||||||||||||||||||||||||||||||||||||
| avatarUrl?: URL | null; | ||||||||||||||||||||||||||||||||||||||||||||||||||
| bannerImageUrl?: URL | null; | ||||||||||||||||||||||||||||||||||||||||||||||||||
| } | ||||||||||||||||||||||||||||||||||||||||||||||||||
|
|
||||||||||||||||||||||||||||||||||||||||||||||||||
| export class Account extends BaseEntity { | ||||||||||||||||||||||||||||||||||||||||||||||||||
| public readonly uuid: string; | ||||||||||||||||||||||||||||||||||||||||||||||||||
| public readonly url: URL; | ||||||||||||||||||||||||||||||||||||||||||||||||||
| public readonly apId: URL; | ||||||||||||||||||||||||||||||||||||||||||||||||||
| public readonly apFollowers: URL; | ||||||||||||||||||||||||||||||||||||||||||||||||||
|
|
||||||||||||||||||||||||||||||||||||||||||||||||||
| private _name: string | null; | ||||||||||||||||||||||||||||||||||||||||||||||||||
| private _bio: string | null; | ||||||||||||||||||||||||||||||||||||||||||||||||||
| private _username: string; | ||||||||||||||||||||||||||||||||||||||||||||||||||
| private _avatarUrl: URL | null; | ||||||||||||||||||||||||||||||||||||||||||||||||||
| private _bannerImageUrl: URL | null; | ||||||||||||||||||||||||||||||||||||||||||||||||||
|
|
||||||||||||||||||||||||||||||||||||||||||||||||||
| export class AccountEntity implements Account { | ||||||||||||||||||||||||||||||||||||||||||||||||||
| constructor( | ||||||||||||||||||||||||||||||||||||||||||||||||||
| public readonly id: number | null, | ||||||||||||||||||||||||||||||||||||||||||||||||||
| uuid: string | null, | ||||||||||||||||||||||||||||||||||||||||||||||||||
| username: string, | ||||||||||||||||||||||||||||||||||||||||||||||||||
| name: string | null, | ||||||||||||||||||||||||||||||||||||||||||||||||||
| bio: string | null, | ||||||||||||||||||||||||||||||||||||||||||||||||||
| avatarUrl: URL | null, | ||||||||||||||||||||||||||||||||||||||||||||||||||
| bannerImageUrl: URL | null, | ||||||||||||||||||||||||||||||||||||||||||||||||||
| private readonly site: AccountSite | null, | ||||||||||||||||||||||||||||||||||||||||||||||||||
| apId: URL | null, | ||||||||||||||||||||||||||||||||||||||||||||||||||
| url: URL | null, | ||||||||||||||||||||||||||||||||||||||||||||||||||
| apFollowers: URL | null, | ||||||||||||||||||||||||||||||||||||||||||||||||||
| ) { | ||||||||||||||||||||||||||||||||||||||||||||||||||
| super(id); | ||||||||||||||||||||||||||||||||||||||||||||||||||
|
|
||||||||||||||||||||||||||||||||||||||||||||||||||
| this._name = name; | ||||||||||||||||||||||||||||||||||||||||||||||||||
| this._bio = bio; | ||||||||||||||||||||||||||||||||||||||||||||||||||
| this._username = username; | ||||||||||||||||||||||||||||||||||||||||||||||||||
| this._avatarUrl = avatarUrl; | ||||||||||||||||||||||||||||||||||||||||||||||||||
| this._bannerImageUrl = bannerImageUrl; | ||||||||||||||||||||||||||||||||||||||||||||||||||
|
|
||||||||||||||||||||||||||||||||||||||||||||||||||
| if (uuid === null) { | ||||||||||||||||||||||||||||||||||||||||||||||||||
| this.uuid = randomUUID(); | ||||||||||||||||||||||||||||||||||||||||||||||||||
| } else { | ||||||||||||||||||||||||||||||||||||||||||||||||||
| this.uuid = uuid; | ||||||||||||||||||||||||||||||||||||||||||||||||||
| } | ||||||||||||||||||||||||||||||||||||||||||||||||||
| if (apId === null) { | ||||||||||||||||||||||||||||||||||||||||||||||||||
| this.apId = this.getApId(); | ||||||||||||||||||||||||||||||||||||||||||||||||||
| } else { | ||||||||||||||||||||||||||||||||||||||||||||||||||
| this.apId = apId; | ||||||||||||||||||||||||||||||||||||||||||||||||||
| } | ||||||||||||||||||||||||||||||||||||||||||||||||||
| if (apFollowers === null) { | ||||||||||||||||||||||||||||||||||||||||||||||||||
| this.apFollowers = this.getApFollowers(); | ||||||||||||||||||||||||||||||||||||||||||||||||||
| } else { | ||||||||||||||||||||||||||||||||||||||||||||||||||
| this.apFollowers = apFollowers; | ||||||||||||||||||||||||||||||||||||||||||||||||||
| } | ||||||||||||||||||||||||||||||||||||||||||||||||||
| if (url === null) { | ||||||||||||||||||||||||||||||||||||||||||||||||||
| this.url = this.apId; | ||||||||||||||||||||||||||||||||||||||||||||||||||
| } else { | ||||||||||||||||||||||||||||||||||||||||||||||||||
| this.url = url; | ||||||||||||||||||||||||||||||||||||||||||||||||||
| } | ||||||||||||||||||||||||||||||||||||||||||||||||||
| } | ||||||||||||||||||||||||||||||||||||||||||||||||||
|
|
||||||||||||||||||||||||||||||||||||||||||||||||||
| get name(): string | null { | ||||||||||||||||||||||||||||||||||||||||||||||||||
| return this._name; | ||||||||||||||||||||||||||||||||||||||||||||||||||
| } | ||||||||||||||||||||||||||||||||||||||||||||||||||
|
|
||||||||||||||||||||||||||||||||||||||||||||||||||
| get bio(): string | null { | ||||||||||||||||||||||||||||||||||||||||||||||||||
| return this._bio; | ||||||||||||||||||||||||||||||||||||||||||||||||||
| } | ||||||||||||||||||||||||||||||||||||||||||||||||||
|
|
||||||||||||||||||||||||||||||||||||||||||||||||||
| get username(): string { | ||||||||||||||||||||||||||||||||||||||||||||||||||
| return this._username; | ||||||||||||||||||||||||||||||||||||||||||||||||||
| } | ||||||||||||||||||||||||||||||||||||||||||||||||||
|
|
||||||||||||||||||||||||||||||||||||||||||||||||||
| get avatarUrl(): URL | null { | ||||||||||||||||||||||||||||||||||||||||||||||||||
| return this._avatarUrl; | ||||||||||||||||||||||||||||||||||||||||||||||||||
| } | ||||||||||||||||||||||||||||||||||||||||||||||||||
|
|
||||||||||||||||||||||||||||||||||||||||||||||||||
| get bannerImageUrl(): URL | null { | ||||||||||||||||||||||||||||||||||||||||||||||||||
| return this._bannerImageUrl; | ||||||||||||||||||||||||||||||||||||||||||||||||||
| } | ||||||||||||||||||||||||||||||||||||||||||||||||||
|
|
||||||||||||||||||||||||||||||||||||||||||||||||||
| updateProfile(params: ProfileUpdateParams): void { | ||||||||||||||||||||||||||||||||||||||||||||||||||
| if (params.name !== undefined) { | ||||||||||||||||||||||||||||||||||||||||||||||||||
| this._name = params.name; | ||||||||||||||||||||||||||||||||||||||||||||||||||
| } | ||||||||||||||||||||||||||||||||||||||||||||||||||
|
|
||||||||||||||||||||||||||||||||||||||||||||||||||
| if (params.bio !== undefined) { | ||||||||||||||||||||||||||||||||||||||||||||||||||
| this._bio = params.bio; | ||||||||||||||||||||||||||||||||||||||||||||||||||
| } | ||||||||||||||||||||||||||||||||||||||||||||||||||
|
|
||||||||||||||||||||||||||||||||||||||||||||||||||
| if (params.username !== undefined) { | ||||||||||||||||||||||||||||||||||||||||||||||||||
| this._username = params.username; | ||||||||||||||||||||||||||||||||||||||||||||||||||
| } | ||||||||||||||||||||||||||||||||||||||||||||||||||
|
|
||||||||||||||||||||||||||||||||||||||||||||||||||
| if (params.avatarUrl !== undefined) { | ||||||||||||||||||||||||||||||||||||||||||||||||||
| this._avatarUrl = params.avatarUrl; | ||||||||||||||||||||||||||||||||||||||||||||||||||
| } | ||||||||||||||||||||||||||||||||||||||||||||||||||
|
|
||||||||||||||||||||||||||||||||||||||||||||||||||
| if (params.bannerImageUrl !== undefined) { | ||||||||||||||||||||||||||||||||||||||||||||||||||
| this._bannerImageUrl = params.bannerImageUrl; | ||||||||||||||||||||||||||||||||||||||||||||||||||
| } | ||||||||||||||||||||||||||||||||||||||||||||||||||
| } | ||||||||||||||||||||||||||||||||||||||||||||||||||
|
|
||||||||||||||||||||||||||||||||||||||||||||||||||
| get isInternal() { | ||||||||||||||||||||||||||||||||||||||||||||||||||
| return this.site !== null; | ||||||||||||||||||||||||||||||||||||||||||||||||||
| } | ||||||||||||||||||||||||||||||||||||||||||||||||||
|
|
||||||||||||||||||||||||||||||||||||||||||||||||||
| getApId() { | ||||||||||||||||||||||||||||||||||||||||||||||||||
| if (!this.isInternal) { | ||||||||||||||||||||||||||||||||||||||||||||||||||
| throw new Error('Cannot get AP ID for External Accounts'); | ||||||||||||||||||||||||||||||||||||||||||||||||||
| } | ||||||||||||||||||||||||||||||||||||||||||||||||||
|
|
||||||||||||||||||||||||||||||||||||||||||||||||||
| return new URL( | ||||||||||||||||||||||||||||||||||||||||||||||||||
| '.ghost/activitypub/users/index', | ||||||||||||||||||||||||||||||||||||||||||||||||||
| `${Account.protocol}://${this.site!.host}`, | ||||||||||||||||||||||||||||||||||||||||||||||||||
| public readonly id: number, | ||||||||||||||||||||||||||||||||||||||||||||||||||
| public readonly uuid: string, | ||||||||||||||||||||||||||||||||||||||||||||||||||
| public readonly username: string, | ||||||||||||||||||||||||||||||||||||||||||||||||||
| public readonly name: string | null, | ||||||||||||||||||||||||||||||||||||||||||||||||||
| public readonly bio: string | null, | ||||||||||||||||||||||||||||||||||||||||||||||||||
| public readonly url: URL, | ||||||||||||||||||||||||||||||||||||||||||||||||||
| public readonly avatarUrl: URL | null, | ||||||||||||||||||||||||||||||||||||||||||||||||||
| public readonly bannerImageUrl: URL | null, | ||||||||||||||||||||||||||||||||||||||||||||||||||
| public readonly apId: URL, | ||||||||||||||||||||||||||||||||||||||||||||||||||
| public readonly apFollowers: URL | null, | ||||||||||||||||||||||||||||||||||||||||||||||||||
| public readonly isInternal: boolean, | ||||||||||||||||||||||||||||||||||||||||||||||||||
| ) {} | ||||||||||||||||||||||||||||||||||||||||||||||||||
|
|
||||||||||||||||||||||||||||||||||||||||||||||||||
| static create(data: Data<Account>) { | ||||||||||||||||||||||||||||||||||||||||||||||||||
| return new AccountEntity( | ||||||||||||||||||||||||||||||||||||||||||||||||||
| data.id, | ||||||||||||||||||||||||||||||||||||||||||||||||||
| data.uuid, | ||||||||||||||||||||||||||||||||||||||||||||||||||
| data.username, | ||||||||||||||||||||||||||||||||||||||||||||||||||
| data.name, | ||||||||||||||||||||||||||||||||||||||||||||||||||
| data.bio, | ||||||||||||||||||||||||||||||||||||||||||||||||||
| data.url, | ||||||||||||||||||||||||||||||||||||||||||||||||||
| data.avatarUrl, | ||||||||||||||||||||||||||||||||||||||||||||||||||
| data.bannerImageUrl, | ||||||||||||||||||||||||||||||||||||||||||||||||||
| data.apId, | ||||||||||||||||||||||||||||||||||||||||||||||||||
| data.apFollowers, | ||||||||||||||||||||||||||||||||||||||||||||||||||
| data.isInternal, | ||||||||||||||||||||||||||||||||||||||||||||||||||
| ); | ||||||||||||||||||||||||||||||||||||||||||||||||||
| } | ||||||||||||||||||||||||||||||||||||||||||||||||||
|
|
||||||||||||||||||||||||||||||||||||||||||||||||||
| getApFollowers() { | ||||||||||||||||||||||||||||||||||||||||||||||||||
| if (!this.isInternal) { | ||||||||||||||||||||||||||||||||||||||||||||||||||
| throw new Error('Cannot get AP Followers for External Accounts'); | ||||||||||||||||||||||||||||||||||||||||||||||||||
| } | ||||||||||||||||||||||||||||||||||||||||||||||||||
|
|
||||||||||||||||||||||||||||||||||||||||||||||||||
| return new URL( | ||||||||||||||||||||||||||||||||||||||||||||||||||
| '.ghost/activitypub/followers/index', | ||||||||||||||||||||||||||||||||||||||||||||||||||
| `${Account.protocol}://${this.site!.host}`, | ||||||||||||||||||||||||||||||||||||||||||||||||||
| ); | ||||||||||||||||||||||||||||||||||||||||||||||||||
| static draft(from: AccountDraftData): AccountDraft { | ||||||||||||||||||||||||||||||||||||||||||||||||||
| const uuid = randomUUID(); | ||||||||||||||||||||||||||||||||||||||||||||||||||
| const apId = !from.isInternal | ||||||||||||||||||||||||||||||||||||||||||||||||||
| ? from.apId | ||||||||||||||||||||||||||||||||||||||||||||||||||
| : new URL('/.ghost/activitypub/users/index', from.host); | ||||||||||||||||||||||||||||||||||||||||||||||||||
| const apFollowers = !from.isInternal | ||||||||||||||||||||||||||||||||||||||||||||||||||
| ? from.apFollowers | ||||||||||||||||||||||||||||||||||||||||||||||||||
| : new URL('/.ghost/activitypub/followers/index', from.host); | ||||||||||||||||||||||||||||||||||||||||||||||||||
| const url = from.url || apId; | ||||||||||||||||||||||||||||||||||||||||||||||||||
| return { | ||||||||||||||||||||||||||||||||||||||||||||||||||
| ...from, | ||||||||||||||||||||||||||||||||||||||||||||||||||
| uuid, | ||||||||||||||||||||||||||||||||||||||||||||||||||
| url, | ||||||||||||||||||||||||||||||||||||||||||||||||||
| apId, | ||||||||||||||||||||||||||||||||||||||||||||||||||
| apFollowers, | ||||||||||||||||||||||||||||||||||||||||||||||||||
| }; | ||||||||||||||||||||||||||||||||||||||||||||||||||
| } | ||||||||||||||||||||||||||||||||||||||||||||||||||
|
|
||||||||||||||||||||||||||||||||||||||||||||||||||
| getApIdForPost(post: { type: CreatePostType; uuid: string }) { | ||||||||||||||||||||||||||||||||||||||||||||||||||
| getApIdForPost(post: { type: CreatePostType; uuid: string }): URL { | ||||||||||||||||||||||||||||||||||||||||||||||||||
| if (!this.isInternal) { | ||||||||||||||||||||||||||||||||||||||||||||||||||
| throw new Error('Cannot get AP ID for External Accounts'); | ||||||||||||||||||||||||||||||||||||||||||||||||||
| } | ||||||||||||||||||||||||||||||||||||||||||||||||||
|
|
@@ -174,28 +104,65 @@ export class Account extends BaseEntity { | |||||||||||||||||||||||||||||||||||||||||||||||||
| } | ||||||||||||||||||||||||||||||||||||||||||||||||||
| } | ||||||||||||||||||||||||||||||||||||||||||||||||||
|
|
||||||||||||||||||||||||||||||||||||||||||||||||||
| return new URL( | ||||||||||||||||||||||||||||||||||||||||||||||||||
| `.ghost/activitypub/${type}/${post.uuid}`, | ||||||||||||||||||||||||||||||||||||||||||||||||||
| `${Account.protocol}://${this.site!.host}`, | ||||||||||||||||||||||||||||||||||||||||||||||||||
| ); | ||||||||||||||||||||||||||||||||||||||||||||||||||
| return new URL(`/.ghost/activitypub/${type}/${post.uuid}`, this.apId); | ||||||||||||||||||||||||||||||||||||||||||||||||||
| } | ||||||||||||||||||||||||||||||||||||||||||||||||||
|
|
||||||||||||||||||||||||||||||||||||||||||||||||||
| private static protocol: 'http' | 'https' = | ||||||||||||||||||||||||||||||||||||||||||||||||||
| process.env.NODE_ENV === 'testing' ? 'http' : 'https'; | ||||||||||||||||||||||||||||||||||||||||||||||||||
|
|
||||||||||||||||||||||||||||||||||||||||||||||||||
| static createFromData(data: AccountData) { | ||||||||||||||||||||||||||||||||||||||||||||||||||
| return new Account( | ||||||||||||||||||||||||||||||||||||||||||||||||||
| data.id, | ||||||||||||||||||||||||||||||||||||||||||||||||||
| data.uuid, | ||||||||||||||||||||||||||||||||||||||||||||||||||
| data.username, | ||||||||||||||||||||||||||||||||||||||||||||||||||
| data.name, | ||||||||||||||||||||||||||||||||||||||||||||||||||
| data.bio, | ||||||||||||||||||||||||||||||||||||||||||||||||||
| data.avatarUrl, | ||||||||||||||||||||||||||||||||||||||||||||||||||
| data.bannerImageUrl, | ||||||||||||||||||||||||||||||||||||||||||||||||||
| data.site, | ||||||||||||||||||||||||||||||||||||||||||||||||||
| data.apId, | ||||||||||||||||||||||||||||||||||||||||||||||||||
| data.url, | ||||||||||||||||||||||||||||||||||||||||||||||||||
| data.apFollowers, | ||||||||||||||||||||||||||||||||||||||||||||||||||
| ); | ||||||||||||||||||||||||||||||||||||||||||||||||||
| updateProfile(params: ProfileUpdateParams) { | ||||||||||||||||||||||||||||||||||||||||||||||||||
| type P = ProfileUpdateParams; | ||||||||||||||||||||||||||||||||||||||||||||||||||
| const get = <K extends keyof P>(prop: K): P[K] => | ||||||||||||||||||||||||||||||||||||||||||||||||||
|
Collaborator
Author
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. I just wanted to avoid the repetition of username: params.username === undefined ? this.username : params.username,But I can put it back if it's preferred
Member
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. Nah, this is a cool utility, maybe we have generic
Collaborator
Author
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. I did try to pull it out - but unfortunately it does't work well when you pull it out, because you need to pass in the
Member
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. Ah cool, let leave it in |
||||||||||||||||||||||||||||||||||||||||||||||||||
| params[prop] === undefined ? this[prop] : params[prop]; | ||||||||||||||||||||||||||||||||||||||||||||||||||
|
|
||||||||||||||||||||||||||||||||||||||||||||||||||
| return AccountEntity.create({ | ||||||||||||||||||||||||||||||||||||||||||||||||||
| ...this, | ||||||||||||||||||||||||||||||||||||||||||||||||||
| username: get('username'), | ||||||||||||||||||||||||||||||||||||||||||||||||||
| name: get('name'), | ||||||||||||||||||||||||||||||||||||||||||||||||||
| bio: get('bio'), | ||||||||||||||||||||||||||||||||||||||||||||||||||
| avatarUrl: get('avatarUrl'), | ||||||||||||||||||||||||||||||||||||||||||||||||||
| bannerImageUrl: get('bannerImageUrl'), | ||||||||||||||||||||||||||||||||||||||||||||||||||
| }); | ||||||||||||||||||||||||||||||||||||||||||||||||||
| } | ||||||||||||||||||||||||||||||||||||||||||||||||||
|
Comment on lines
+110
to
123
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. 🛠️ Refactor suggestion
A safer, fully-typed alternative: - type P = ProfileUpdateParams;
- const get = <K extends keyof P>(prop: K): P[K] =>
- params[prop] === undefined ? this[prop] : params[prop];
-
- return AccountEntity.create({
- ...this,
- username: get('username'),
- name: get('name'),
- bio: get('bio'),
- avatarUrl: get('avatarUrl'),
- bannerImageUrl: get('bannerImageUrl'),
- });
+ return AccountEntity.create({
+ ...this,
+ username: params.username ?? this.username,
+ name: params.name ?? this.name,
+ bio: params.bio ?? this.bio,
+ avatarUrl: params.avatarUrl ?? this.avatarUrl,
+ bannerImageUrl: params.bannerImageUrl ?? this.bannerImageUrl,
+ });This keeps the implementation concise, avoids the helper closure, and preserves correct types. 📝 Committable suggestion
Suggested change
Collaborator
Author
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. I don't think this is true, we explicitly do not return There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. @allouis You're absolutely right. Looking closer at your implementation, the runtime behavior is perfectly safe: const get = <K extends keyof P>(prop: K): P[K] =>
params[prop] === undefined ? this[prop] : params[prop];This correctly falls back to The only subtlety is that TypeScript's type system declares the return type as Thanks for the clarification and pushing back on my suggestion! ✏️ Learnings added
|
||||||||||||||||||||||||||||||||||||||||||||||||||
| } | ||||||||||||||||||||||||||||||||||||||||||||||||||
|
|
||||||||||||||||||||||||||||||||||||||||||||||||||
| type ProfileUpdateParams = { | ||||||||||||||||||||||||||||||||||||||||||||||||||
| name?: string | null; | ||||||||||||||||||||||||||||||||||||||||||||||||||
| bio?: string | null; | ||||||||||||||||||||||||||||||||||||||||||||||||||
| username?: string; | ||||||||||||||||||||||||||||||||||||||||||||||||||
| avatarUrl?: URL | null; | ||||||||||||||||||||||||||||||||||||||||||||||||||
| bannerImageUrl?: URL | null; | ||||||||||||||||||||||||||||||||||||||||||||||||||
| }; | ||||||||||||||||||||||||||||||||||||||||||||||||||
|
|
||||||||||||||||||||||||||||||||||||||||||||||||||
| /** | ||||||||||||||||||||||||||||||||||||||||||||||||||
| * Internal accounts require a `host` so we can calculate the ActivityPub URLs | ||||||||||||||||||||||||||||||||||||||||||||||||||
| */ | ||||||||||||||||||||||||||||||||||||||||||||||||||
| type InternalAccountDraftData = { | ||||||||||||||||||||||||||||||||||||||||||||||||||
| isInternal: true; | ||||||||||||||||||||||||||||||||||||||||||||||||||
| host: URL; | ||||||||||||||||||||||||||||||||||||||||||||||||||
| username: string; | ||||||||||||||||||||||||||||||||||||||||||||||||||
| name: string; | ||||||||||||||||||||||||||||||||||||||||||||||||||
| bio: string | null; | ||||||||||||||||||||||||||||||||||||||||||||||||||
| url: URL | null; | ||||||||||||||||||||||||||||||||||||||||||||||||||
| avatarUrl: URL | null; | ||||||||||||||||||||||||||||||||||||||||||||||||||
| bannerImageUrl: URL | null; | ||||||||||||||||||||||||||||||||||||||||||||||||||
| }; | ||||||||||||||||||||||||||||||||||||||||||||||||||
|
|
||||||||||||||||||||||||||||||||||||||||||||||||||
| /** | ||||||||||||||||||||||||||||||||||||||||||||||||||
| * External accounts require the ActivityPub URLs to be passed in | ||||||||||||||||||||||||||||||||||||||||||||||||||
| */ | ||||||||||||||||||||||||||||||||||||||||||||||||||
| type ExternalAccountDraftData = { | ||||||||||||||||||||||||||||||||||||||||||||||||||
| isInternal: false; | ||||||||||||||||||||||||||||||||||||||||||||||||||
| username: string; | ||||||||||||||||||||||||||||||||||||||||||||||||||
| name: string; | ||||||||||||||||||||||||||||||||||||||||||||||||||
| bio: string | null; | ||||||||||||||||||||||||||||||||||||||||||||||||||
| url: URL | null; | ||||||||||||||||||||||||||||||||||||||||||||||||||
| avatarUrl: URL | null; | ||||||||||||||||||||||||||||||||||||||||||||||||||
| bannerImageUrl: URL | null; | ||||||||||||||||||||||||||||||||||||||||||||||||||
| apId: URL; | ||||||||||||||||||||||||||||||||||||||||||||||||||
| apFollowers: URL | null; | ||||||||||||||||||||||||||||||||||||||||||||||||||
| }; | ||||||||||||||||||||||||||||||||||||||||||||||||||
|
|
||||||||||||||||||||||||||||||||||||||||||||||||||
| type AccountDraftData = InternalAccountDraftData | ExternalAccountDraftData; | ||||||||||||||||||||||||||||||||||||||||||||||||||
|
|
||||||||||||||||||||||||||||||||||||||||||||||||||
| type Data<T> = { | ||||||||||||||||||||||||||||||||||||||||||||||||||
|
Collaborator
Author
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. Yeah I can probably put this somewhere else, but where? |
||||||||||||||||||||||||||||||||||||||||||||||||||
| // biome-ignore lint/suspicious/noExplicitAny: These anys are internal and don't leak to our code | ||||||||||||||||||||||||||||||||||||||||||||||||||
| [K in keyof T as T[K] extends (...args: any[]) => any ? never : K]: T[K]; | ||||||||||||||||||||||||||||||||||||||||||||||||||
| }; | ||||||||||||||||||||||||||||||||||||||||||||||||||
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.
This is nullable now
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.
Should this be
nullthough? When would an account never have a followers URL?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.
Great question, I don't know - but it's nullable in the DB, so I think we should fix that all together.