v3: the backend is documented for deno doc, one way in every file, and the docs build and publish themselves — comments were in three shapes: file headers as // lines, as an untagged /** */ (which deno doc drops, only an @module block counts) or missing; 43 exports described by a // deno doc can't see, two JSDoc blocks sitting on the wrong declaration and one duplicated; endpoints commented in 5 of 18 route files, three ways, on router.get() calls no doc tool reads. The rule, applied to every non-test module of api/, migrations included: each opens with a /** … @module */ block; every export has JSDoc, and so does every field of an exported interface; a comment describing a declaration is /** */, // only notes code; functions open with a verb, everything else with a noun phrase; parameters are named in the prose, {@link} only for symbols in scope; whatever throws to its caller says so with @throws (for an APIException, the statuses and when); a router's JSDoc lists its endpoints, method and path, who may call it, what it does. deno doc --lint goes from 528 errors to none in api/ (shared/ keeps its 385; it is next). The lint also wanted explicit types on config.ts's settings and a few return types, JsonLdResult exported, and the routers typed Router. deno task doc builds the HTML reference into docs/ from every module of api/ and shared/ but the tests, deno task doc:lint lints it; the index page is the module doc of a root mod.ts (deno.json's exports), which introduces the backend, shared/ and the frontend to come and links every module, and mod.test.ts fails when one is missing. The docs workflow builds it on every push to main that touches them and publishes it to the gh-pages branch, like publish-assistant's. Two defects found along the way are fixed: a private playlist's cover was served to anyone who had its id or slug, and now needs its owner's media token, as private dump files do (playlistImageUrl adds it for private playlists only, so public cover URLs stay the same for everyone); og.ts and static.ts each filled index.html's placeholders, and now share lib/index-html.ts, so the OG middleware picks up a rebuilt index.html instead of keeping the first it read. The README states the documentation rule. 205 backend and 628 frontend tests pass.
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
This commit is contained in:
@@ -1,13 +1,31 @@
|
||||
/**
|
||||
* The payloads of the tokens the server signs, and their guards.
|
||||
*
|
||||
* @module
|
||||
*/
|
||||
|
||||
import { isRole, type Role } from "../../shared/user.ts";
|
||||
|
||||
/** What a session token carries. */
|
||||
export interface AuthPayload {
|
||||
/** The user's id. */
|
||||
userId: string;
|
||||
/** The user's name when the token was issued. */
|
||||
username: string;
|
||||
/**
|
||||
* The user's role; the live one replaces it once the token is authenticated.
|
||||
*/
|
||||
role: Role;
|
||||
/** When the token expires, in seconds since the epoch. */
|
||||
exp: number;
|
||||
/** When the token was issued, in seconds since the epoch. */
|
||||
iat?: number;
|
||||
}
|
||||
|
||||
/**
|
||||
* Checks whether `obj` is an {@link AuthPayload}, and not a purpose-scoped
|
||||
* token's payload.
|
||||
*/
|
||||
export function isAuthPayload(obj: unknown): obj is AuthPayload {
|
||||
return !!obj &&
|
||||
typeof obj === "object" &&
|
||||
@@ -19,12 +37,17 @@ export function isAuthPayload(obj: unknown): obj is AuthPayload {
|
||||
"exp" in obj && typeof obj.exp === "number";
|
||||
}
|
||||
|
||||
/** What a password reset token carries. */
|
||||
export interface PasswordResetPayload {
|
||||
/** The token's purpose, which makes it a reset token. */
|
||||
purpose: "password-reset";
|
||||
/** The id of the user whose password it resets. */
|
||||
userId: string;
|
||||
/** When the token expires, in seconds since the epoch. */
|
||||
exp: number;
|
||||
}
|
||||
|
||||
/** Checks whether `obj` is a {@link PasswordResetPayload}. */
|
||||
export function isPasswordResetPayload(
|
||||
obj: unknown,
|
||||
): obj is PasswordResetPayload {
|
||||
|
||||
@@ -1,16 +1,32 @@
|
||||
/**
|
||||
* The `categories` table: its row type, its guard, and its mapper to the API's
|
||||
* {@link Category}.
|
||||
*
|
||||
* @module
|
||||
*/
|
||||
|
||||
import type { SQLOutputValue } from "node:sqlite";
|
||||
import type { Category } from "../../shared/category.ts";
|
||||
|
||||
/** A row of the `categories` table. */
|
||||
export interface CategoryRow {
|
||||
/** The category's id. */
|
||||
id: string;
|
||||
/** The category's URL slug, at the site root. */
|
||||
slug: string;
|
||||
/** The category's display name. */
|
||||
name: string;
|
||||
/** The category's place in the list, ascending. */
|
||||
position: number;
|
||||
/** When the category was created. */
|
||||
created_at: string;
|
||||
/** When the category was last changed, if ever. */
|
||||
updated_at: string | null;
|
||||
/** Any other column the query selects. */
|
||||
[key: string]: SQLOutputValue;
|
||||
}
|
||||
|
||||
/** Checks whether `obj` is a {@link CategoryRow}. */
|
||||
export function isCategoryRow(obj: unknown): obj is CategoryRow {
|
||||
return !!obj && typeof obj === "object" &&
|
||||
"id" in obj && typeof obj.id === "string" &&
|
||||
@@ -22,6 +38,7 @@ export function isCategoryRow(obj: unknown): obj is CategoryRow {
|
||||
(typeof obj.updated_at === "string" || obj.updated_at === null);
|
||||
}
|
||||
|
||||
/** Converts a row to the API's {@link Category}. */
|
||||
export function categoryRowToApi(row: CategoryRow): Category {
|
||||
return {
|
||||
id: row.id,
|
||||
|
||||
@@ -1,23 +1,52 @@
|
||||
/**
|
||||
* The `chat_messages` table: its row type, its guard, and its mapper to the
|
||||
* API's {@link ChatMessage}.
|
||||
*
|
||||
* @module
|
||||
*/
|
||||
|
||||
import type { SQLOutputValue } from "node:sqlite";
|
||||
import type { ChatMessage } from "../../shared/chat.ts";
|
||||
|
||||
/**
|
||||
* A row of the `chat_messages` table, joined with its author and, when it is
|
||||
* a reply, the replied-to message and its author.
|
||||
*/
|
||||
export interface ChatMessageRow {
|
||||
/** The message's id. */
|
||||
id: string;
|
||||
/** The author's id. */
|
||||
user_id: string;
|
||||
/** The message, in Markdown. */
|
||||
body: string;
|
||||
/** When the message was sent. */
|
||||
created_at: string;
|
||||
/** When the message was last edited, if ever. */
|
||||
updated_at: string | null;
|
||||
/** The author's name (joined). */
|
||||
author_username: string;
|
||||
/** The author's avatar type, null without one (joined). */
|
||||
author_avatar_mime: string | null;
|
||||
// Reply columns: reply_to_id is the stored target; the author/body preview are
|
||||
// LEFT-JOINed from the target and go null when there's no reply (or it was
|
||||
// since deleted).
|
||||
/**
|
||||
* The id of the message this replies to, kept even once that message is
|
||||
* deleted.
|
||||
*/
|
||||
reply_to_id: string | null;
|
||||
/**
|
||||
* The replied-to message's author (joined); null when there's no reply or it
|
||||
* was since deleted.
|
||||
*/
|
||||
reply_to_author: string | null;
|
||||
/**
|
||||
* The start of the replied-to message (joined); null when there's no reply or
|
||||
* it was since deleted.
|
||||
*/
|
||||
reply_to_body: string | null;
|
||||
/** Any other column the query selects. */
|
||||
[key: string]: SQLOutputValue;
|
||||
}
|
||||
|
||||
/** Checks whether `obj` is a {@link ChatMessageRow}. */
|
||||
export function isChatMessageRow(obj: unknown): obj is ChatMessageRow {
|
||||
return !!obj && typeof obj === "object" &&
|
||||
"id" in obj && typeof obj.id === "string" &&
|
||||
@@ -38,6 +67,7 @@ export function isChatMessageRow(obj: unknown): obj is ChatMessageRow {
|
||||
(typeof obj.reply_to_body === "string" || obj.reply_to_body === null);
|
||||
}
|
||||
|
||||
/** Converts a row to the API's {@link ChatMessage}. */
|
||||
export function chatMessageRowToApi(row: ChatMessageRow): ChatMessage {
|
||||
return {
|
||||
id: row.id,
|
||||
|
||||
@@ -1,21 +1,45 @@
|
||||
/**
|
||||
* The `comments` table: its row type, its guard, and its mapper to the API's
|
||||
* {@link Comment}.
|
||||
*
|
||||
* @module
|
||||
*/
|
||||
|
||||
import type { SQLOutputValue } from "node:sqlite";
|
||||
import type { Comment } from "../../shared/comment.ts";
|
||||
|
||||
/** A row of the `comments` table, joined with its author. */
|
||||
export interface CommentRow {
|
||||
/** The comment's id. */
|
||||
id: string;
|
||||
/** The id of the dump it is posted under. */
|
||||
dump_id: string;
|
||||
/** The author's id. */
|
||||
user_id: string;
|
||||
/** The id of the comment it replies to; null at the top level. */
|
||||
parent_id: string | null;
|
||||
/** The comment, in Markdown. */
|
||||
body: string;
|
||||
/** When the comment was posted. */
|
||||
created_at: string;
|
||||
/** When the comment was last edited, if ever. */
|
||||
updated_at: string | null;
|
||||
/**
|
||||
* 1 once deleted: the body is emptied but the row stays, so its replies
|
||||
* keep their place.
|
||||
*/
|
||||
deleted: number;
|
||||
/** How many users like the comment. */
|
||||
like_count: number;
|
||||
/** The author's name (joined). */
|
||||
author_username: string;
|
||||
/** The author's avatar type, null without one (joined). */
|
||||
author_avatar_mime: string | null;
|
||||
/** Any other column the query selects. */
|
||||
[key: string]: SQLOutputValue;
|
||||
}
|
||||
|
||||
/** Checks whether `obj` is a {@link CommentRow}. */
|
||||
export function isCommentRow(
|
||||
obj: unknown,
|
||||
): obj is CommentRow {
|
||||
@@ -37,6 +61,7 @@ export function isCommentRow(
|
||||
obj.author_avatar_mime === null);
|
||||
}
|
||||
|
||||
/** Converts a row to the API's {@link Comment}. */
|
||||
export function commentRowToApi(row: CommentRow): Comment {
|
||||
return {
|
||||
id: row.id,
|
||||
|
||||
@@ -1,41 +1,74 @@
|
||||
/**
|
||||
* The `dumps` table: its row type, its guard, the column lists services select
|
||||
* it with, and its mappers to and from the API's {@link Dump}.
|
||||
*
|
||||
* @module
|
||||
*/
|
||||
|
||||
import type { SQLOutputValue } from "node:sqlite";
|
||||
import type { Dump, RichContent } from "../../shared/dump.ts";
|
||||
|
||||
/** A row of the `dumps` table, with its live comment count. */
|
||||
export interface DumpRow {
|
||||
/** The dump's id. */
|
||||
id: string;
|
||||
/** `url` for a link, `file` for an upload. */
|
||||
kind: string;
|
||||
/** The dump's title. */
|
||||
title: string;
|
||||
/** The poster's description, in Markdown. */
|
||||
comment: string | null;
|
||||
/** The poster's id. */
|
||||
user_id: string;
|
||||
/** When the dump was posted. */
|
||||
created_at: string;
|
||||
/** When the dump was last edited, if ever. */
|
||||
updated_at: string | null;
|
||||
/** The dump's URL slug. */
|
||||
slug: string | null;
|
||||
/** The link exactly as submitted, for a URL dump. */
|
||||
url: string | null;
|
||||
/** The link's preview, as {@link RichContent} JSON. */
|
||||
rich_content: string | null;
|
||||
/** The uploaded file's name, for a file dump. */
|
||||
file_name: string | null;
|
||||
/** The uploaded file's type, for a file dump. */
|
||||
file_mime: string | null;
|
||||
/** The uploaded file's size in bytes, for a file dump. */
|
||||
file_size: number | null;
|
||||
/** How many users upvoted the dump. */
|
||||
vote_count: number;
|
||||
/** How many comments it has, not counting deleted ones (computed). */
|
||||
comment_count: number;
|
||||
/** 1 when only the poster sees the dump. */
|
||||
is_private: number;
|
||||
/** The uploaded thumbnail's type, null without one. */
|
||||
custom_thumbnail_mime: string | null;
|
||||
[key: string]: SQLOutputValue; // Index signature
|
||||
/** Any other column the query selects. */
|
||||
[key: string]: SQLOutputValue;
|
||||
}
|
||||
|
||||
// Shared `dumps` column list — keep in sync with DumpRow/isDumpRow.
|
||||
// Services import these instead of repeating the column list themselves.
|
||||
/**
|
||||
* The stored columns of {@link DumpRow} — keep in sync with it and
|
||||
* {@link isDumpRow}. Services select with these instead of repeating the
|
||||
* column list themselves.
|
||||
*/
|
||||
export const DUMP_COLUMNS =
|
||||
"id, kind, title, slug, comment, user_id, created_at, updated_at, url, rich_content, file_name, file_mime, file_size, vote_count, is_private, custom_thumbnail_mime";
|
||||
|
||||
/** {@link DUMP_COLUMNS}, for a query aliasing `dumps` as `d`. */
|
||||
export const DUMP_COLUMNS_ALIASED =
|
||||
"d.id, d.kind, d.title, d.slug, d.comment, d.user_id, d.created_at, d.updated_at, d.url, d.rich_content, d.file_name, d.file_mime, d.file_size, d.vote_count, d.is_private, d.custom_thumbnail_mime";
|
||||
|
||||
export const DUMP_SELECT_COLUMNS = `${DUMP_COLUMNS},
|
||||
/** {@link DUMP_COLUMNS} and the computed `comment_count`: a full {@link DumpRow}. */
|
||||
export const DUMP_SELECT_COLUMNS: string = `${DUMP_COLUMNS},
|
||||
(SELECT COUNT(*) FROM comments WHERE dump_id = dumps.id AND deleted = 0) as comment_count`;
|
||||
|
||||
export const DUMP_SELECT_COLUMNS_ALIASED = `${DUMP_COLUMNS_ALIASED},
|
||||
/** {@link DUMP_SELECT_COLUMNS}, for a query aliasing `dumps` as `d`. */
|
||||
export const DUMP_SELECT_COLUMNS_ALIASED: string = `${DUMP_COLUMNS_ALIASED},
|
||||
(SELECT COUNT(*) FROM comments WHERE dump_id = d.id AND deleted = 0) as comment_count`;
|
||||
|
||||
/** Checks whether `obj` is a {@link DumpRow}. */
|
||||
export function isDumpRow(obj: unknown): obj is DumpRow {
|
||||
return !!obj &&
|
||||
typeof obj === "object" &&
|
||||
@@ -66,6 +99,7 @@ export function isDumpRow(obj: unknown): obj is DumpRow {
|
||||
obj.custom_thumbnail_mime === null);
|
||||
}
|
||||
|
||||
/** Converts a row to the API's {@link Dump}. */
|
||||
export function dumpRowToApi(row: DumpRow): Dump {
|
||||
return {
|
||||
id: row.id,
|
||||
@@ -90,6 +124,7 @@ export function dumpRowToApi(row: DumpRow): Dump {
|
||||
};
|
||||
}
|
||||
|
||||
/** Converts a {@link Dump} back to a row. */
|
||||
export function dumpApiToRow(dump: Dump): DumpRow {
|
||||
return {
|
||||
id: dump.id,
|
||||
|
||||
@@ -1,14 +1,31 @@
|
||||
/**
|
||||
* The `follows` table: its row type and its guard.
|
||||
*
|
||||
* @module
|
||||
*/
|
||||
|
||||
import type { SQLOutputValue } from "node:sqlite";
|
||||
|
||||
/**
|
||||
* A row of the `follows` table: one user following either another user or a
|
||||
* playlist.
|
||||
*/
|
||||
export interface FollowRow {
|
||||
/** The follow's id. */
|
||||
id: string;
|
||||
/** The id of the user following. */
|
||||
follower_id: string;
|
||||
/** The id of the user followed, when it is a user. */
|
||||
followed_user_id: string | null;
|
||||
/** The id of the playlist followed, when it is a playlist. */
|
||||
followed_playlist_id: string | null;
|
||||
/** When the follow began. */
|
||||
created_at: string;
|
||||
/** Any other column the query selects. */
|
||||
[key: string]: SQLOutputValue;
|
||||
}
|
||||
|
||||
/** Checks whether `obj` is a {@link FollowRow}. */
|
||||
export function isFollowRow(
|
||||
obj: unknown,
|
||||
): obj is FollowRow {
|
||||
|
||||
@@ -1,13 +1,26 @@
|
||||
/**
|
||||
* The `invites` table: its row type and its guard.
|
||||
*
|
||||
* @module
|
||||
*/
|
||||
|
||||
import type { SQLOutputValue } from "node:sqlite";
|
||||
|
||||
/** A row of the `invites` table. */
|
||||
export interface InviteRow {
|
||||
/** The invite's token, which registering redeems. */
|
||||
token: string;
|
||||
/** The id of the user who made the invite. */
|
||||
inviter_id: string;
|
||||
/** When the invite was redeemed; null while unused. */
|
||||
used_at: string | null;
|
||||
/** When the invite was made. */
|
||||
created_at: string;
|
||||
/** Any other column the query selects. */
|
||||
[key: string]: SQLOutputValue;
|
||||
}
|
||||
|
||||
/** Checks whether `obj` is a {@link InviteRow}. */
|
||||
export function isInviteRow(
|
||||
obj: unknown,
|
||||
): obj is InviteRow {
|
||||
|
||||
@@ -1,3 +1,10 @@
|
||||
/**
|
||||
* The `notifications` table: its row type, its guard, and its mapper to the
|
||||
* API's {@link Notification}.
|
||||
*
|
||||
* @module
|
||||
*/
|
||||
|
||||
import type { SQLOutputValue } from "node:sqlite";
|
||||
import type {
|
||||
Notification,
|
||||
@@ -5,17 +12,30 @@ import type {
|
||||
NotificationType,
|
||||
} from "../../shared/notification.ts";
|
||||
|
||||
/** A row of the `notifications` table. */
|
||||
export interface NotificationRow {
|
||||
/** The notification's id. */
|
||||
id: string;
|
||||
/** The id of the user notified. */
|
||||
user_id: string;
|
||||
/** The notification's {@link NotificationType}. */
|
||||
type: string;
|
||||
/** The notification's {@link NotificationData}, as JSON. */
|
||||
data: string;
|
||||
/** 1 once the user has read it. */
|
||||
read: number;
|
||||
/** When the notification was made. */
|
||||
created_at: string;
|
||||
/**
|
||||
* What caused the notification: a second one with the same
|
||||
* `(user_id, source_key)` isn't stored. Null when duplicates are allowed.
|
||||
*/
|
||||
source_key: string | null;
|
||||
/** Any other column the query selects. */
|
||||
[key: string]: SQLOutputValue;
|
||||
}
|
||||
|
||||
/** Checks whether `obj` is a {@link NotificationRow}. */
|
||||
export function isNotificationRow(
|
||||
obj: unknown,
|
||||
): obj is NotificationRow {
|
||||
@@ -30,6 +50,7 @@ export function isNotificationRow(
|
||||
(typeof obj.source_key === "string" || obj.source_key === null);
|
||||
}
|
||||
|
||||
/** Converts a row to the API's {@link Notification}. */
|
||||
export function notificationRowToApi(row: NotificationRow): Notification {
|
||||
return {
|
||||
id: row.id,
|
||||
|
||||
@@ -1,13 +1,26 @@
|
||||
/**
|
||||
* The `password_reset_tokens` table: its row type and its guard.
|
||||
*
|
||||
* @module
|
||||
*/
|
||||
|
||||
import type { SQLOutputValue } from "node:sqlite";
|
||||
|
||||
/** A row of the `password_reset_tokens` table. */
|
||||
export interface PasswordResetTokenRow {
|
||||
/** The reset token. */
|
||||
token: string;
|
||||
/** The id of the user whose password it resets. */
|
||||
user_id: string;
|
||||
/** When the token stops working. */
|
||||
expires_at: string;
|
||||
/** When the token was used; null while unused. */
|
||||
used_at: string | null;
|
||||
/** Any other column the query selects. */
|
||||
[key: string]: SQLOutputValue;
|
||||
}
|
||||
|
||||
/** Checks whether `obj` is a {@link PasswordResetTokenRow}. */
|
||||
export function isPasswordResetTokenRow(
|
||||
obj: unknown,
|
||||
): obj is PasswordResetTokenRow {
|
||||
|
||||
@@ -1,19 +1,41 @@
|
||||
/**
|
||||
* The `playlists` table: its row type, its guard, and its mapper to the API's
|
||||
* {@link Playlist}.
|
||||
*
|
||||
* @module
|
||||
*/
|
||||
|
||||
import type { SQLOutputValue } from "node:sqlite";
|
||||
import type { Playlist } from "../../shared/playlist.ts";
|
||||
|
||||
/**
|
||||
* A row of the `playlists` table. Queries may add `dump_count` and
|
||||
* `owner_username`, which {@link playlistRowToApi} carries over.
|
||||
*/
|
||||
export interface PlaylistRow {
|
||||
/** The playlist's id. */
|
||||
id: string;
|
||||
/** The owner's id. */
|
||||
user_id: string;
|
||||
/** The playlist's title. */
|
||||
title: string;
|
||||
/** The playlist's URL slug. */
|
||||
slug: string | null;
|
||||
/** The owner's description, in Markdown. */
|
||||
description: string | null;
|
||||
/** 1 when anyone may see the playlist, 0 when only its owner does. */
|
||||
is_public: number;
|
||||
/** When the playlist was created. */
|
||||
created_at: string;
|
||||
/** When the playlist was last changed, if ever. */
|
||||
updated_at: string | null;
|
||||
/** The cover image's type, null without one. */
|
||||
image_mime: string | null;
|
||||
/** Any other column the query selects. */
|
||||
[key: string]: SQLOutputValue;
|
||||
}
|
||||
|
||||
/** Checks whether `obj` is a {@link PlaylistRow}. */
|
||||
export function isPlaylistRow(
|
||||
obj: unknown,
|
||||
): obj is PlaylistRow {
|
||||
@@ -32,6 +54,7 @@ export function isPlaylistRow(
|
||||
(typeof obj.image_mime === "string" || obj.image_mime === null);
|
||||
}
|
||||
|
||||
/** Converts a row to the API's {@link Playlist}. */
|
||||
export function playlistRowToApi(row: PlaylistRow): Playlist {
|
||||
return {
|
||||
id: row.id,
|
||||
|
||||
@@ -1,3 +1,10 @@
|
||||
/**
|
||||
* The `users` table: its row type, its guard, its mappers to and from the
|
||||
* stored {@link User}, and the views of a user the API sends.
|
||||
*
|
||||
* @module
|
||||
*/
|
||||
|
||||
import type { SQLOutputValue } from "node:sqlite";
|
||||
import {
|
||||
isRole,
|
||||
@@ -8,35 +15,54 @@ import {
|
||||
|
||||
/** A user as stored: what they see of themselves, plus the password hash. */
|
||||
export interface User extends SelfUser {
|
||||
/** The scrypt hash of the password, as `hash.salt`. */
|
||||
passwordHash: string;
|
||||
}
|
||||
|
||||
/** Returns what anyone may see of `user`: no hash, no email. */
|
||||
export function toPublicUser(
|
||||
{ passwordHash: _, email: _email, ...user }: User,
|
||||
): PublicUser {
|
||||
return user;
|
||||
}
|
||||
|
||||
/** Returns what `user` sees of themselves: everything but the hash. */
|
||||
export function toSelfUser({ passwordHash: _, ...user }: User): SelfUser {
|
||||
return user;
|
||||
}
|
||||
|
||||
/** A row of the `users` table. */
|
||||
export interface UserRow {
|
||||
/** The user's id. */
|
||||
id: string;
|
||||
/** The user's name, unique. */
|
||||
username: string;
|
||||
/** The scrypt hash of the password, as `hash.salt`. */
|
||||
password_hash: string;
|
||||
/** The user's role. */
|
||||
role: Role;
|
||||
/** When the user registered. */
|
||||
created_at: string;
|
||||
/** When the profile was last changed, if ever. */
|
||||
updated_at: string | null;
|
||||
/** The avatar's type, null without one. */
|
||||
avatar_mime: string | null;
|
||||
/** The user's profile text, in Markdown. */
|
||||
description: string | null;
|
||||
/** The id of the user whose invite they registered with. */
|
||||
invited_by: string | null;
|
||||
// Present only when joined: LEFT JOIN users i ON i.id = u.invited_by
|
||||
/**
|
||||
* The inviter's name; present only when the query joins it (`LEFT JOIN users
|
||||
* i ON i.id = u.invited_by`).
|
||||
*/
|
||||
invited_by_username: string | null;
|
||||
/** The user's email address. */
|
||||
email: string;
|
||||
[key: string]: SQLOutputValue; // Index signature
|
||||
/** Any other column the query selects. */
|
||||
[key: string]: SQLOutputValue;
|
||||
}
|
||||
|
||||
/** Checks whether `obj` is a {@link UserRow}. */
|
||||
export function isUserRow(obj: unknown): obj is UserRow {
|
||||
return !!obj &&
|
||||
typeof obj === "object" &&
|
||||
@@ -56,6 +82,7 @@ export function isUserRow(obj: unknown): obj is UserRow {
|
||||
"email" in obj && typeof obj.email === "string";
|
||||
}
|
||||
|
||||
/** Converts a row to the stored {@link User}. */
|
||||
export function userRowToApi(row: UserRow): User {
|
||||
return {
|
||||
id: row.id,
|
||||
@@ -73,6 +100,7 @@ export function userRowToApi(row: UserRow): User {
|
||||
};
|
||||
}
|
||||
|
||||
/** Converts a {@link User} back to a row, without the join-only columns. */
|
||||
export function userApiToRow(user: User): UserRow {
|
||||
return {
|
||||
id: user.id,
|
||||
|
||||
Reference in New Issue
Block a user