Clean up model nullability

This commit is contained in:
Alexander Skvortsov 2021-11-21 19:11:18 -05:00
parent 25934833b8
commit 3c9c67d726
8 changed files with 72 additions and 59 deletions

View File

@ -310,6 +310,8 @@ export default abstract class Model {
* relationship exists; undefined if the relationship exists but the model
* has not been loaded; or the model if it has been loaded.
*/
static hasOne<M extends Model>(name: string): () => M | false;
static hasOne<M extends Model | null>(name: string): () => M | null | false;
static hasOne<M extends Model>(name: string): () => M | false {
return function (this: Model) {
if (this.data.relationships) {
@ -358,8 +360,12 @@ export default abstract class Model {
/**
* Transform the given value into a Date object.
*/
static transformDate(value: string | null): Date | null {
return value ? new Date(value) : null;
static transformDate(value: string): Date;
static transformDate(value: string | null): Date | null;
static transformDate(value: string | undefined): Date | undefined;
static transformDate(value: string | null | undefined): Date | null | undefined;
static transformDate(value: string | null | undefined): Date | null | undefined {
return value != null ? new Date(value) : value;
}
/**

View File

@ -24,8 +24,8 @@ export default function avatar(user: User, attrs: ComponentAttrs = {}): Mithril.
// uploaded image, or the first letter of their username if they haven't
// uploaded one.
if (user) {
const username: string = user.displayName() || '?';
const avatarUrl: string = user.avatarUrl();
const username = user.displayName() || '?';
const avatarUrl = user.avatarUrl();
if (hasTitle) attrs.title = attrs.title || username;

View File

@ -16,46 +16,46 @@ export default class Discussion extends Model {
}
createdAt() {
return Model.attribute<Date | null, string | null>('createdAt', Model.transformDate).call(this);
return Model.attribute<Date | undefined, string | undefined>('createdAt', Model.transformDate).call(this);
}
user() {
return Model.hasOne<User>('user').call(this);
return Model.hasOne<User | null>('user').call(this);
}
firstPost() {
return Model.hasOne<Post>('firstPost').call(this);
return Model.hasOne<Post | null>('firstPost').call(this);
}
lastPostedAt() {
return Model.attribute<Date | null, string | null>('lastPostedAt', Model.transformDate).call(this);
return Model.attribute('lastPostedAt', Model.transformDate).call(this);
}
lastPostedUser() {
return Model.hasOne<User>('lastPostedUser').call(this);
return Model.hasOne<User | null>('lastPostedUser').call(this);
}
lastPost() {
return Model.hasOne<Post>('lastPost').call(this);
return Model.hasOne<Post | null>('lastPost').call(this);
}
lastPostNumber() {
return Model.attribute<number | null>('lastPostNumber').call(this);
return Model.attribute<number | null | undefined>('lastPostNumber').call(this);
}
commentCount() {
return Model.attribute<number | null>('commentCount').call(this);
return Model.attribute<number | undefined>('commentCount').call(this);
}
replyCount() {
return computed<number, this>('commentCount', (commentCount) => Math.max(0, (commentCount as number) - 1)).call(this);
return computed<Number, this>('commentCount', (commentCount) => Math.max(0, (commentCount as number ?? 0) - 1)).call(this);
}
posts() {
return Model.hasMany<Post>('posts').call(this);
}
mostRelevantPost() {
return Model.hasOne<Post>('mostRelevantPost').call(this);
return Model.hasOne<Post | null>('mostRelevantPost').call(this);
}
lastReadAt() {
return Model.attribute<Date | null, string | null>('lastReadAt', Model.transformDate).call(this);
return Model.attribute('lastReadAt', Model.transformDate).call(this);
}
lastReadPostNumber() {
return Model.attribute<number | null>('lastReadPostNumber').call(this);
return Model.attribute<number | null | undefined>('lastReadPostNumber').call(this);
}
isUnread() {
return computed<boolean, this>('unreadCount', (unreadCount) => !!unreadCount).call(this);
@ -65,26 +65,26 @@ export default class Discussion extends Model {
}
hiddenAt() {
return Model.attribute<Date | null, string | null>('hiddenAt', Model.transformDate).call(this);
return Model.attribute('hiddenAt', Model.transformDate).call(this);
}
hiddenUser() {
return Model.hasOne<User>('hiddenUser').call(this);
return Model.hasOne<User | null>('hiddenUser').call(this);
}
isHidden() {
return computed<boolean, this>('hiddenAt', (hiddenAt) => !!hiddenAt).call(this);
}
canReply() {
return Model.attribute<boolean | null>('canReply').call(this);
return Model.attribute<boolean | undefined>('canReply').call(this);
}
canRename() {
return Model.attribute<boolean | null>('canRename').call(this);
return Model.attribute<boolean | undefined>('canRename').call(this);
}
canHide() {
return Model.attribute<boolean | null>('canHide').call(this);
return Model.attribute<boolean | undefined>('canHide').call(this);
}
canDelete() {
return Model.attribute<boolean | null>('canDelete').call(this);
return Model.attribute<boolean | undefined>('canDelete').call(this);
}
/**

View File

@ -13,10 +13,10 @@ export default class Group extends Model {
}
color() {
return Model.attribute<string>('color').call(this);
return Model.attribute<string | null>('color').call(this);
}
icon() {
return Model.attribute<string>('icon').call(this);
return Model.attribute<string | null>('icon').call(this);
}
isHidden() {

View File

@ -9,7 +9,7 @@ export default class Notification extends Model {
return Model.attribute<string>('content').call(this);
}
createdAt() {
return Model.attribute<Date | null, string | null>('createdAt', Model.transformDate).call(this);
return Model.attribute<Date, string>('createdAt', Model.transformDate).call(this);
}
isRead() {
@ -20,9 +20,9 @@ export default class Notification extends Model {
return Model.hasOne<User>('user').call(this);
}
fromUser() {
return Model.hasOne<User>('fromUser').call(this);
return Model.hasOne<User | null>('fromUser').call(this);
}
subject() {
return Model.hasOne('subject').call(this);
return Model.hasOne<Model | null>('subject').call(this);
}
}

View File

@ -13,55 +13,61 @@ export default class Post extends Model {
}
createdAt() {
return Model.attribute<Date | null, string>('createdAt', Model.transformDate).call(this);
return Model.attribute<Date, string>('createdAt', Model.transformDate).call(this);
}
user() {
return Model.hasOne<User>('user').call(this);
}
contentType() {
return Model.attribute<string>('contentType').call(this);
return Model.attribute<string | null>('contentType').call(this);
}
content() {
return Model.attribute<string>('content').call(this);
return Model.attribute<string | null | undefined>('content').call(this);
}
contentHtml() {
return Model.attribute<string>('contentHtml').call(this);
return Model.attribute<string | null | undefined>('contentHtml').call(this);
}
renderFailed() {
return Model.attribute<boolean>('renderFailed').call(this);
return Model.attribute<boolean | undefined>('renderFailed').call(this);
}
contentPlain() {
return computed<string>('contentHtml', getPlainContent as (content: unknown) => string).call(this);
return computed<string | null | undefined>('contentHtml', (content) => {
if (typeof content === 'string') {
return getPlainContent(content);
}
return content as (null | undefined);
}).call(this);
}
editedAt() {
return Model.attribute<Date | null, string>('editedAt', Model.transformDate).call(this);
return Model.attribute('editedAt', Model.transformDate).call(this);
}
editedUser() {
return Model.hasOne<User>('editedUser').call(this);
return Model.hasOne<User | null>('editedUser').call(this);
}
isEdited() {
return computed<boolean>('editedAt', (editedAt) => !!editedAt).call(this);
}
hiddenAt() {
return Model.attribute<Date | null, string>('hiddenAt', Model.transformDate).call(this);
return Model.attribute('hiddenAt', Model.transformDate).call(this);
}
hiddenUser() {
return Model.hasOne<User>('hiddenUser').call(this);
return Model.hasOne<User | null>('hiddenUser').call(this);
}
isHidden() {
return computed<boolean>('hiddenAt', (hiddenAt) => !!hiddenAt).call(this);
}
canEdit() {
return Model.attribute<boolean>('canEdit').call(this);
return Model.attribute<boolean | undefined>('canEdit').call(this);
}
canHide() {
return Model.attribute<boolean>('canHide').call(this);
return Model.attribute<boolean | undefined>('canHide').call(this);
}
canDelete() {
return Model.attribute<boolean>('canDelete').call(this);
return Model.attribute<boolean | undefined>('canDelete').call(this);
}
}

View File

@ -6,6 +6,7 @@ import ItemList from '../utils/ItemList';
import computed from '../utils/computed';
import GroupBadge from '../components/GroupBadge';
import Mithril from 'mithril';
import Group from './Group';
export default class User extends Model {
username() {
@ -19,65 +20,65 @@ export default class User extends Model {
}
email() {
return Model.attribute<string | null>('email').call(this);
return Model.attribute<string | undefined>('email').call(this);
}
isEmailConfirmed() {
return Model.attribute<boolean | null>('isEmailConfirmed').call(this);
return Model.attribute<boolean | undefined>('isEmailConfirmed').call(this);
}
password() {
return Model.attribute<string | null>('password').call(this);
return Model.attribute<string | undefined>('password').call(this);
}
avatarUrl() {
return Model.attribute<string>('avatarUrl').call(this);
return Model.attribute<string | null>('avatarUrl').call(this);
}
preferences() {
return Model.attribute<Record<string, any> | null>('preferences').call(this);
return Model.attribute<Record<string, any> | null | undefined>('preferences').call(this);
}
groups() {
return Model.hasMany('groups').call(this);
return Model.hasMany<Group>('groups').call(this);
}
joinTime() {
return Model.attribute<Date | null, string | null>('joinTime', Model.transformDate).call(this);
return Model.attribute('joinTime', Model.transformDate).call(this);
}
lastSeenAt() {
return Model.attribute<Date | null, string | null>('lastSeenAt', Model.transformDate).call(this);
return Model.attribute('lastSeenAt', Model.transformDate).call(this);
}
markedAllAsReadAt() {
return Model.attribute<Date | null, string | null>('markedAllAsReadAt', Model.transformDate).call(this);
return Model.attribute('markedAllAsReadAt', Model.transformDate).call(this);
}
unreadNotificationCount() {
return Model.attribute<number | null>('unreadNotificationCount').call(this);
return Model.attribute<number | undefined>('unreadNotificationCount').call(this);
}
newNotificationCount() {
return Model.attribute<number | null>('newNotificationCount').call(this);
return Model.attribute<number | undefined>('newNotificationCount').call(this);
}
discussionCount() {
return Model.attribute<number | null>('discussionCount').call(this);
return Model.attribute<number | undefined>('discussionCount').call(this);
}
commentCount() {
return Model.attribute<number | null>('commentCount').call(this);
return Model.attribute<number | undefined>('commentCount').call(this);
}
canEdit() {
return Model.attribute<boolean | null>('canEdit').call(this);
return Model.attribute<boolean | undefined>('canEdit').call(this);
}
canEditCredentials() {
return Model.attribute<boolean | null>('canEditCredentials').call(this);
return Model.attribute<boolean | undefined>('canEditCredentials').call(this);
}
canEditGroups() {
return Model.attribute<boolean | null>('canEditGroups').call(this);
return Model.attribute<boolean | undefined>('canEditGroups').call(this);
}
canDelete() {
return Model.attribute<boolean | null>('canDelete').call(this);
return Model.attribute<boolean | undefined>('canDelete').call(this);
}
color() {
@ -148,7 +149,7 @@ export default class User extends Model {
m.redraw();
};
image.crossOrigin = 'anonymous';
image.src = this.avatarUrl();
image.src = this.avatarUrl() ?? '';
}
/**

View File

@ -40,7 +40,7 @@ export default class DiscussionsSearchSource implements SearchSource {
<li className="DiscussionSearchResult" data-index={'discussions' + discussion.id()}>
<Link href={app.route.discussion(discussion, mostRelevantPost && mostRelevantPost.number())}>
<div className="DiscussionSearchResult-title">{highlight(discussion.title(), query)}</div>
{mostRelevantPost ? <div className="DiscussionSearchResult-excerpt">{highlight(mostRelevantPost.contentPlain(), query, 100)}</div> : ''}
{mostRelevantPost ? <div className="DiscussionSearchResult-excerpt">{highlight(mostRelevantPost.contentPlain() ?? '', query, 100)}</div> : ''}
</Link>
</li>
);