Skip to content

Commit 70907d2

Browse files
committed
fix(api): Audit and fix schema to reflect real db
I went through a copy of the prod db and found lots of optional fields that aren't marked as such, old documents missing fields, etc. Introduces an IPostInput type that accounts for the mongoose schema doing defaults and fixes, while also allowing IPost to reflect the real db type. Groundwork for a proper DTO structure.
1 parent 6d8eced commit 70907d2

8 files changed

Lines changed: 265 additions & 81 deletions

File tree

apps/miiverse-api/package.json

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -26,7 +26,7 @@
2626
"express-subdomain": "^1.0.5",
2727
"fs-extra": "^9.0.0",
2828
"moment": "^2.24.0",
29-
"mongoose": "^6.10.1",
29+
"mongoose": "^8.15.1",
3030
"multer": "^1.4.5-lts.1",
3131
"nice-grpc": "^2.1.12",
3232
"node-snowflake": "0.0.1",

apps/miiverse-api/src/database.ts

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -11,7 +11,7 @@ import type { HydratedConversationDocument } from '@/types/mongoose/conversation
1111
import type { HydratedContentDocument } from '@/types/mongoose/content';
1212
import type { HydratedSettingsDocument } from '@/types/mongoose/settings';
1313
import type { HydratedEndpointDocument } from '@/types/mongoose/endpoint';
14-
import type { HydratedPostDocument, IPost } from '@/types/mongoose/post';
14+
import type { HydratedPostDocument, IPostInput } from '@/types/mongoose/post';
1515
import type { HydratedCommunityDocument } from '@/types/mongoose/community';
1616

1717
let connection: mongoose.Connection;
@@ -97,7 +97,7 @@ export async function getPostReplies(postID: string, limit: number): Promise<Hyd
9797
}).limit(limit);
9898
}
9999

100-
export async function getDuplicatePosts(pid: number, post: IPost): Promise<HydratedPostDocument | null> {
100+
export async function getDuplicatePosts(pid: number, post: IPostInput): Promise<HydratedPostDocument | null> {
101101
verifyConnected();
102102

103103
return Post.findOne({

apps/miiverse-api/src/models/post.ts

Lines changed: 49 additions & 42 deletions
Original file line numberDiff line numberDiff line change
@@ -7,29 +7,28 @@ import type { HydratedCommunityDocument } from '@/types/mongoose/community';
77
import type { PostToJSONOptions } from '@/types/mongoose/post-to-json-options';
88
import type { PostData, PostPainting, PostScreenshot, PostTopicTag } from '@/types/miiverse/post';
99

10+
/* Constraints here (default, required etc.) apply to new documents being added
11+
* See IPost for expected shape of query results
12+
* If you add default: or required:, please also update IPost and IPostInput!
13+
*/
1014
const PostSchema = new Schema<IPost, PostModel, IPostMethods>({
11-
id: String,
12-
title_id: String,
13-
screen_name: String,
14-
body: String,
15-
app_data: String,
16-
painting: String,
17-
screenshot: String,
18-
screenshot_length: Number,
19-
search_key: {
20-
type: [String],
21-
default: undefined
22-
},
23-
topic_tag: {
24-
type: String,
25-
default: undefined
26-
},
27-
community_id: {
28-
type: String,
29-
default: undefined
30-
},
31-
created_at: Date,
32-
feeling_id: Number,
15+
id: { type: String, required: true },
16+
title_id: { type: String },
17+
screen_name: { type: String, required: true },
18+
body: { type: String, required: true },
19+
app_data: { type: String },
20+
21+
painting: { type: String },
22+
screenshot: { type: String },
23+
screenshot_length: { type: Number },
24+
25+
search_key: { type: [String] },
26+
topic_tag: { type: String },
27+
28+
community_id: { type: String, required: true },
29+
created_at: { type: Date, required: true },
30+
feeling_id: { type: Number },
31+
3332
is_autopost: {
3433
type: Number,
3534
default: 0
@@ -46,6 +45,7 @@ const PostSchema = new Schema<IPost, PostModel, IPostMethods>({
4645
type: Number,
4746
default: 0
4847
},
48+
4949
empathy_count: {
5050
type: Number,
5151
default: 0,
@@ -59,12 +59,15 @@ const PostSchema = new Schema<IPost, PostModel, IPostMethods>({
5959
type: Number,
6060
default: 1
6161
},
62-
mii: String,
63-
mii_face_url: String,
64-
pid: Number,
65-
platform_id: Number,
66-
region_id: Number,
67-
parent: String,
62+
63+
mii: { type: String, required: true },
64+
mii_face_url: { type: String, required: true },
65+
66+
pid: { type: Number, required: true },
67+
platform_id: { type: Number },
68+
region_id: { type: Number },
69+
parent: { type: String },
70+
6871
reply_count: {
6972
type: Number,
7073
default: 0
@@ -73,17 +76,21 @@ const PostSchema = new Schema<IPost, PostModel, IPostMethods>({
7376
type: Boolean,
7477
default: false
7578
},
79+
7680
message_to_pid: {
7781
type: String,
7882
default: null
7983
},
84+
8085
removed: {
8186
type: Boolean,
8287
default: false
8388
},
84-
removed_reason: String,
85-
yeahs: [Number],
86-
number: Number
89+
removed_reason: { type: String },
90+
removed_by: { type: Number },
91+
removed_at: { type: Date },
92+
93+
yeahs: { type: [Number], default: [] }
8794
}, {
8895
id: false // * Disables the .id() getter used by Mongoose in TypeScript. Needed to have our own .id field
8996
});
@@ -114,12 +121,12 @@ PostSchema.method<HydratedPostDocument>('cleanedMiiData', function cleanedMiiDat
114121
return this.mii.replace(/[^A-Za-z0-9+/=]/g, '').replace(/[\n\r]+/gm, '').trim();
115122
});
116123

117-
PostSchema.method<HydratedPostDocument>('cleanedPainting', function cleanedPainting(): string {
118-
return this.painting.replace(/[\n\r]+/gm, '').trim();
124+
PostSchema.method<HydratedPostDocument>('cleanedPainting', function cleanedPainting(): string | undefined {
125+
return this.painting?.replace(/[\n\r]+/gm, '').trim();
119126
});
120127

121-
PostSchema.method<HydratedPostDocument>('cleanedAppData', function cleanedAppData(): string {
122-
return this.app_data.replace(/[^A-Za-z0-9+/=]/g, '').replace(/[\n\r]+/gm, '').trim();
128+
PostSchema.method<HydratedPostDocument>('cleanedAppData', function cleanedAppData(): string | undefined {
129+
return this.app_data?.replace(/[^A-Za-z0-9+/=]/g, '').replace(/[\n\r]+/gm, '').trim();
123130
});
124131

125132
PostSchema.method<HydratedPostDocument>('formatPainting', function formatPainting(): PostPainting | undefined {
@@ -146,7 +153,7 @@ PostSchema.method<HydratedPostDocument>('formatTopicTag', function formatTopicTa
146153
if (this.topic_tag?.trim()) {
147154
return {
148155
name: this.topic_tag,
149-
title_id: this.title_id
156+
title_id: this.title_id ?? ''
150157
};
151158
}
152159
});
@@ -158,26 +165,26 @@ PostSchema.method<HydratedPostDocument>('json', function json(options: PostToJSO
158165
community_id: this.community_id, // TODO - This sucks
159166
country_id: this.country_id,
160167
created_at: moment(this.created_at).format('YYYY-MM-DD HH:MM:SS'),
161-
feeling_id: this.feeling_id,
168+
feeling_id: this.feeling_id ?? 0,
162169
id: this.id,
163170
is_autopost: this.is_autopost ? 1 : 0,
164171
is_community_private_autopost: this.is_community_private_autopost ? 1 : 0,
165172
is_spoiler: this.is_spoiler ? 1 : 0,
166173
is_app_jumpable: this.is_app_jumpable ? 1 : 0,
167-
empathy_count: this.empathy_count || 0,
174+
empathy_count: this.empathy_count,
168175
language_id: this.language_id,
169176
mii: undefined, // * Conditionally set later
170177
mii_face_url: undefined, // * Conditionally set later
171178
number: 0,
172179
painting: this.formatPainting(),
173180
pid: this.pid,
174-
platform_id: this.platform_id,
175-
region_id: this.region_id,
176-
reply_count: this.reply_count || 0,
181+
platform_id: this.platform_id ?? 1,
182+
region_id: this.region_id ?? 0,
183+
reply_count: this.reply_count,
177184
screen_name: this.screen_name,
178185
screenshot: this.formatScreenshot(),
179186
topic_tag: undefined, // * Conditionally set later
180-
title_id: this.title_id
187+
title_id: this.title_id ?? ''
181188
};
182189

183190
if (options.app_data) {

apps/miiverse-api/src/services/api/routes/communities.ts

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -298,7 +298,7 @@ router.post('/', multer().none(), async function (request: express.Request, resp
298298
return badRequest(response, ApiErrorCode.CREATE_TOO_MANY_FAVORITES, 403);
299299
}
300300

301-
const communitiesCount = await Community.count();
301+
const communitiesCount = await Community.countDocuments();
302302
const communityID = (parseInt(parentCommunity.community_id) + (5000 * communitiesCount)); // Change this to auto increment
303303
const community = await Community.create({
304304
platform_id: 0, // WiiU

apps/miiverse-api/src/services/api/routes/friend_messages.ts

Lines changed: 4 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -245,20 +245,20 @@ router.get('/', async function (request: express.Request, response: express.Resp
245245
is_app_jumpable: message.is_app_jumpable,
246246
empathy_added: message.empathy_count,
247247
language_id: message.language_id,
248-
message_to_pid: message.message_to_pid,
248+
message_to_pid: message.message_to_pid ?? undefined,
249249
mii: message.mii,
250250
mii_face_url: message.mii_face_url,
251-
number: message.number || 0,
251+
number: 0,
252252
pid: message.pid,
253253
platform_id: message.platform_id || 0,
254254
region_id: message.region_id || 0,
255255
reply_count: message.reply_count,
256256
screen_name: message.screen_name,
257257
topic_tag: {
258-
name: message.topic_tag,
258+
name: message.topic_tag ?? '',
259259
title_id: 0
260260
},
261-
title_id: message.title_id
261+
title_id: message.title_id ?? ''
262262
}
263263
});
264264
}

apps/miiverse-api/src/services/api/routes/posts.ts

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -22,7 +22,7 @@ import { Community } from '@/models/community';
2222
import { config } from '@/config';
2323
import { ApiErrorCode, badRequest, serverError } from '@/errors';
2424
import type { PostRepliesResult } from '@/types/miiverse/post';
25-
import type { HydratedPostDocument } from '@/types/mongoose/post';
25+
import type { HydratedPostDocument, IPostInput } from '@/types/mongoose/post';
2626

2727
const newPostSchema = z.object({
2828
community_id: z.string().optional(),
@@ -312,7 +312,7 @@ async function newPost(request: express.Request, response: express.Response): Pr
312312
searchKey = [searchKey];
313313
}
314314

315-
const document = {
315+
const document: IPostInput = {
316316
id: '', // * This gets changed when saving the document for the first time
317317
title_id: request.paramPack.title_id,
318318
community_id: community.olive_community_id,

apps/miiverse-api/src/types/mongoose/post.ts

Lines changed: 56 additions & 24 deletions
Original file line numberDiff line numberDiff line change
@@ -3,41 +3,73 @@ import type { HydratedCommunityDocument } from '@/types/mongoose/community';
33
import type { PostToJSONOptions } from '@/types/mongoose/post-to-json-options';
44
import type { PostData, PostPainting, PostScreenshot, PostTopicTag } from '@/types/miiverse/post';
55

6+
/* This type needs to reflect "reality" as it is in the DB
7+
* Thus, all the optionals, since some legacy documents are missing many fields
8+
*/
69
export interface IPost {
710
id: string;
8-
title_id: string;
11+
title_id?: string; // u64
912
screen_name: string;
1013
body: string;
11-
app_data: string;
12-
painting: string;
13-
screenshot: string;
14-
screenshot_length: number;
15-
search_key: string[];
16-
topic_tag: string;
17-
community_id: string;
14+
app_data?: string; // nintendo base64
15+
16+
painting?: string; // base64, can be empty or undefined
17+
screenshot?: string; // url fragment (leading /), can be empty or undefined
18+
screenshot_length?: number;
19+
20+
search_key?: string[]; // can be empty or undefined
21+
topic_tag?: string;
22+
23+
community_id: string; // actually a number
1824
created_at: Date;
19-
feeling_id: number;
20-
is_autopost: number;
21-
is_community_private_autopost?: number;
22-
is_spoiler: number;
23-
is_app_jumpable: number;
24-
empathy_count?: number;
25+
feeling_id?: number; // missing on PMs
26+
27+
is_autopost: number; // 0 | 1
28+
is_community_private_autopost: number; // 0 | 1
29+
is_spoiler: number; // 0 | 1
30+
is_app_jumpable: number; // 0 | 1
31+
32+
empathy_count: number;
2533
country_id: number;
2634
language_id: number;
27-
mii: string;
28-
mii_face_url: string;
35+
36+
mii: string; // nintendo base64
37+
mii_face_url: string; // fully qualified (usually cdn. or r2-cdn.)
38+
2939
pid: number;
30-
platform_id: number;
31-
region_id: number;
32-
parent: string;
33-
reply_count?: number;
40+
platform_id?: number; // missing on PMs
41+
region_id?: number; // missing on PMs
42+
parent?: string | null; // both undef and null exist in db
43+
44+
reply_count: number;
3445
verified: boolean;
35-
message_to_pid?: string;
46+
47+
message_to_pid: string | null;
48+
3649
removed: boolean;
3750
removed_reason?: string;
38-
yeahs?: Types.Array<number>;
39-
number?: number;
51+
removed_by?: number;
52+
removed_at?: Date;
53+
54+
yeahs: Types.Array<number>;
4055
}
56+
// Fields that have "default: " in the Mongoose schema should also be listed here to make them optional
57+
// on input but not output
58+
// We really need an ORM
59+
type PostDefaultedFields =
60+
'is_autopost' |
61+
'is_community_private_autopost' |
62+
'is_spoiler' |
63+
'is_app_jumpable' |
64+
'empathy_count' |
65+
'country_id' |
66+
'language_id' |
67+
'reply_count' |
68+
'verified' |
69+
'message_to_pid' |
70+
'removed' |
71+
'yeahs';
72+
export type IPostInput = Omit<IPost, PostDefaultedFields> & Partial<Pick<IPost, PostDefaultedFields>>;
4173

4274
export interface IPostMethods {
4375
del(reason: string): Promise<void>;
@@ -52,6 +84,6 @@ export interface IPostMethods {
5284
json(options: PostToJSONOptions, community?: HydratedCommunityDocument): PostData;
5385
}
5486

55-
export type PostModel = Model<IPost, object, IPostMethods>;
87+
export type PostModel = Model<IPost, {}, IPostMethods>;
5688

5789
export type HydratedPostDocument = HydratedDocument<IPost, IPostMethods>;

0 commit comments

Comments
 (0)