Skip to content
This repository was archived by the owner on Jan 14, 2023. It is now read-only.
Draft
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
2 changes: 1 addition & 1 deletion .vscode/launch.json
Original file line number Diff line number Diff line change
Expand Up @@ -18,7 +18,7 @@
"type": "node",
"request": "launch",
"name": "Launch with Debugger",
"program": "${workspaceFolder}/dist/packages/server/main.js",
"program": "${workspaceFolder}/dist/apps/api/main.js",
"smartStep": true,
}
]
Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,68 @@
declare module 'mongoose-fuzzy-searching' {
import { Document, DocumentQuery, HookAsyncCallback, HookSyncCallback, Model, Schema } from 'mongoose'

export type FuzzyFieldStringOptions<T> = (keyof T & string)[];
Copy link
Collaborator

@pingzing pingzing Sep 12, 2021

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

This basically means "the only fields allowed on this type are the fields already defined on T, as strings".


export interface FuzzyFieldOptions<T> {
Copy link
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

The docs and defaults from this come from the package's docs.

/**
* Collection key name. If unspecified, defualts to **null**.
*/
name?: string | null;

/**
* N-grams min size. If unspecified, defaults to **2**.
*/
minSize?: number;

/**
* Denotes the significance of the field relative to the other indexed fields in terms of the text search score.
* If unspecified, defaults to **1**.
*/
weight?: number;

/**
* Only return ngrams from start of word. (It gives more precise results).
* If unspecified, defaults to **false**.
*/
prefixOnly?: boolean;

/**
* Remove special characters from N-grams.
* If unspecified, defaults to **true**.
*/
escapeSpecialCharacters?: boolean;

/**
* Defines which attributes on this object to be used for fuzzy searching.
* If unspecified, defaults to **null**.
*/
keys: FuzzyFieldStringOptions<T>;
}

export interface MongooseFuzzyOptions<T> {
/**
* Defines the fields to fuzzy search. Can either be an array of strings
* (in which case defaults will be used), or an array of objects,
* that define the options for each field.
*/
fields: FuzzyFieldStringOptions<T> | FuzzyFieldOptions<T>;
middlewares?: {
preSave?: HookSyncCallback<T> | HookAsyncCallback<T>;
preInsertMany?: HookSyncCallback<T> | HookAsyncCallback<T>;
preUpdate?: HookSyncCallback<T> | HookAsyncCallback<T>;
preUpdateOne?: HookSyncCallback<T> | HookAsyncCallback<T>;
preFindOneAndUpdate?: HookSyncCallback<T> | HookAsyncCallback<T>;
preUpdateMany?: HookSyncCallback<T> | HookAsyncCallback<T>;
}
}

export interface MongooseFuzzyModel<T extends Document, QueryHelpers = Record<string, unknown>>
extends Model<T, QueryHelpers> {
fuzzySearch(
search: string,
callBack?: (err: any, data: Model<T, QueryHelpers>[]) => void
): DocumentQuery<T[], T, QueryHelpers>
}

export default function registerFuzzySearch<T>(schema: Schema<T>, options: MongooseFuzzyOptions<T>): void
Copy link
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

this needs to be defined as a default export because of how the original JS package exposes its registration function (i.e. as an unnamed function in the module.exports dictionary)

}
3 changes: 2 additions & 1 deletion libs/api/database/src/lib/content/schemas/content.schema.ts
Original file line number Diff line number Diff line change
Expand Up @@ -17,6 +17,7 @@ export class ContentDocument extends Document implements ContentModel {
})
readonly author: string | Pseudonym;

// Indexes by title as text in index file
@Prop({ trim: true, required: true })
title: string;

Expand Down Expand Up @@ -73,7 +74,7 @@ export class ContentDocument extends Document implements ContentModel {
readonly kind: ContentKind;

@Prop({type: [{
type: String,
type: String,
ref: 'Tags',
autopopulate: {
select: '_id name desc parent kind createdAt updatedAt',
Expand Down
31 changes: 19 additions & 12 deletions libs/api/database/src/lib/content/schemas/index.ts
Original file line number Diff line number Diff line change
Expand Up @@ -5,10 +5,12 @@ import { ContentDocument, ContentSchema } from './content.schema';
import { RatingsSchema } from './ratings.schema';
import { ReadingHistorySchema } from './reading-history.schema';
import { SectionsDocument, SectionsSchema } from './sections.schema';
import { TagsDocument, TagsSchema } from './tags.schema';
import { TagsSchema } from './tags.schema';
import * as MongooseAutopopulate from 'mongoose-autopopulate';
import * as MongoosePaginate from 'mongoose-paginate-v2';
import { countWords, stripTags } from 'voca';
import { MongooseFuzzyOptions } from 'mongoose-fuzzy-searching';
import registerFuzzySearch from 'mongoose-fuzzy-searching';

//#region ---EXPORTS---

Expand All @@ -35,19 +37,24 @@ export async function setupContentCollection() {
// making a text index on the title field for search
schema.index({ title: 'text' });

schema.pre<ContentDocument>('save', async function (next: HookNextFunction) {
this.set('title', sanitizeHtml(this.title, sanitizeOptions));
this.set('body', sanitizeHtml(this.body, sanitizeOptions));

// this will only trigger if any creation or editing functions has modified the `desc` field,
// otherwise we'll leave it alone
if (this.isModified('desc')) {
this.set('desc', sanitizeHtml(this.desc, sanitizeOptions));
schema.plugin<MongooseFuzzyOptions<ContentDocument>>(registerFuzzySearch,{
fields: ['title'],
// Middlewares must be defined inside the fuzzy-search plugin, otherwise it will override them.
middlewares: {
preSave: async function (next: HookNextFunction) {
this.set('title', sanitizeHtml(this.title, sanitizeOptions));
this.set('body', sanitizeHtml(this.body, sanitizeOptions));

// this will only trigger if any creation or editing functions has modified the `desc` field,
// otherwise we'll leave it alone
if (this.isModified('desc')) {
this.set('desc', sanitizeHtml(this.desc, sanitizeOptions));
}

return next();
Comment on lines +44 to +54
Copy link
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Per the mongoose-fuzzy-searching docs, it overrides any pre()- hooks you have defined on a schema, so they need to be defined within the plugin's middlewares config field. For that reason, I also define it as the first plugin in the chain, to ensure those hooks run as early as possible.

}
}

return next();
});

schema.plugin(MongooseAutopopulate);
schema.plugin(MongoosePaginate);

Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -7,6 +7,7 @@ import { RatingOption } from '@dragonfish/shared/models/reading-history';
import { JwtPayload } from '@dragonfish/shared/models/auth';
import { ContentFilter, ContentKind, ContentRating, PubStatus } from '@dragonfish/shared/models/content';
import { Pseudonym } from '@dragonfish/shared/models/accounts';
import { MongooseFuzzyModel } from 'mongoose-fuzzy-searching';

/**
* ## Content Group Store
Expand All @@ -18,6 +19,7 @@ export class ContentGroupStore {
readonly NEWEST_FIRST = -1
constructor(
@InjectModel('Content') private readonly content: PaginateModel<ContentDocument>,
@InjectModel('Content') private readonly fuzzySearchableContent: MongooseFuzzyModel<ContentDocument>,
Copy link
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I was kind of surprised this worked! But it did. Nice and easy.

@InjectModel('Sections') private readonly sections: Model<SectionsDocument>,
@InjectModel('Ratings') private readonly ratings: Model<RatingsDocument>,
@InjectModel('ReadingHistory') private readonly history: Model<ReadingHistoryDocument>,
Expand Down Expand Up @@ -201,7 +203,8 @@ export class ContentGroupStore {
kind: { $in: kinds },
};
await ContentGroupStore.determineContentFilter(paginateQuery, filter);
return await this.content.paginate(paginateQuery, paginateOptions);
const fuzzySearchedContent = await this.fuzzySearchableContent.fuzzySearch(query);
Copy link
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

So we're not actually doing anything with this variable. fuzzySearch() returns an array of ContentDocuments, but this function expects to return a PaginateResult<ContentDocument>, and those are two very different things. This is where a big part of the complexity of making mongoose-paginate and mongoose-fuzzy-searching play nicely together is going to happen.

return await this.content.paginate(paginateQuery, paginateOptions);
}

/**
Expand All @@ -211,7 +214,7 @@ export class ContentGroupStore {
* @param pageNum The page of results to retrieve.
* @param maxPerPage The maximum number of results per page.
* @param filter The content filter to apply to returned results.
* @returns
* @returns
*/
public async getContentByFandomTag(
tagId: string,
Expand Down
2 changes: 2 additions & 0 deletions libs/api/database/src/lib/content/stores/content.store.ts
Original file line number Diff line number Diff line change
Expand Up @@ -25,6 +25,7 @@ import { NotificationsService, UnsubscribeResult } from '../../notifications';
import { ApprovalQueueStore } from '../../approval-queue';
import { PublishSection, SectionForm } from '@dragonfish/shared/models/sections';
import { SectionsStore } from './sections.store';
import { MongooseFuzzyModel } from 'mongoose-fuzzy-searching';

/**
* ## Content Store
Expand All @@ -36,6 +37,7 @@ export class ContentStore {
constructor(
@InjectModel('Content') private readonly content: PaginateModel<ContentDocument>,
@InjectModel('Sections') private readonly sections: Model<SectionsDocument>,
@InjectModel('Content') private readonly fuzzySearchableContent: MongooseFuzzyModel<ContentDocument>,
private readonly users: UsersStore,
private readonly blogContent: BlogsStore,
private readonly newsContent: NewsStore,
Expand Down
3 changes: 2 additions & 1 deletion libs/api/database/tsconfig.lib.json
Original file line number Diff line number Diff line change
Expand Up @@ -5,7 +5,8 @@
"outDir": "../../../dist/out-tsc",
"declaration": true,
"types": ["node"],
"target": "es6"
"target": "es6",
"typeRoots": ["../../../node_modules/@types", "src/customTypes"]
Copy link
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Now that we have some custom types, we need to tell the compiler where to find them. Since we do that, we have to tell it where to find all of them, so the node_modules types go in here too.

We could import the .d.ts files manually, but who wants to do that everywhere?

},
"exclude": ["**/*.spec.ts"],
"include": ["**/*.ts"]
Expand Down
1 change: 1 addition & 0 deletions package.json
Original file line number Diff line number Diff line change
Expand Up @@ -111,6 +111,7 @@
"moment": "^2.29.1",
"mongoose": "5.10.0",
"mongoose-autopopulate": "0.12.2",
"mongoose-fuzzy-searching": "^2.0.2",
"mongoose-paginate-v2": "1.3.9",
"mongoose-sequence": "^5.3.1",
"mongoose-unique-validator": "^2.0.3",
Expand Down
5 changes: 5 additions & 0 deletions yarn.lock
Original file line number Diff line number Diff line change
Expand Up @@ -12984,6 +12984,11 @@ mongoose-autopopulate@0.12.2:
resolved "https://registry.yarnpkg.com/mongoose-autopopulate/-/mongoose-autopopulate-0.12.2.tgz#5209c7f9934b2642d2eac1afc84d289316138a84"
integrity sha512-ZpjylV6ty0iqNjSWPgQWDMaBExnXrvCNhci9EnX49O0fOIVES1Qc7r1d+nxYBKvQutYW5UWs+fauahOTTlpjww==

mongoose-fuzzy-searching@^2.0.2:
version "2.0.2"
resolved "https://registry.yarnpkg.com/mongoose-fuzzy-searching/-/mongoose-fuzzy-searching-2.0.2.tgz#16a25b198591ab41ac00f3a9d15d3328aa7a126c"
integrity sha512-Hbmx59VWJjQJDNdoyQ8bdECaqpis0pKoliWzf5eILzFs2KO7laUJpaQWJySN9OvdQS/gM9fURMSzQVQoLeZ3DA==

mongoose-legacy-pluralize@1.0.2:
version "1.0.2"
resolved "https://registry.yarnpkg.com/mongoose-legacy-pluralize/-/mongoose-legacy-pluralize-1.0.2.tgz#3ba9f91fa507b5186d399fb40854bff18fb563e4"
Expand Down