Skip to content

Commit

Permalink
feat(search-service): add sequelize support in search service
Browse files Browse the repository at this point in the history
add sequelize support in search service

GH-1350
  • Loading branch information
Surbhi-sharma1 committed Jan 5, 2024
1 parent 229179c commit c344faf
Show file tree
Hide file tree
Showing 11 changed files with 306 additions and 35 deletions.
16 changes: 16 additions & 0 deletions services/search-service/package.json
Original file line number Diff line number Diff line change
Expand Up @@ -11,6 +11,19 @@
"engines": {
"node": "18 || 20"
},
"exports": {
".": "./dist/index.js",
"./sequelize": {
"types": "./dist/repositories/sequelize/index.d.ts"
}
},
"typesVersions": {
"*": {
"sequelize": [
"./dist/repositories/sequelize/index.d.ts"
]
}
},
"scripts": {
"build": "npm run clean && lb-tsc",
"build:watch": "lb-tsc --watch",
Expand Down Expand Up @@ -85,6 +98,9 @@
"typescript": "~4.9.5",
"widdershins": "^4.0.1"
},
"optionalDependencies": {
"@loopback/sequelize": "^0.5.2"
},
"overrides": {
"widdershins": {
"swagger2openapi": "^7.0.8",
Expand Down
31 changes: 24 additions & 7 deletions services/search-service/src/component.ts
Original file line number Diff line number Diff line change
Expand Up @@ -44,10 +44,15 @@ import {SearchServiceBindings} from './keys';
import {SearchQuery, SearchResult} from './models';
import {RecentSearchRepository} from './repositories/recent-search.repository';
import {SearchQueryRepository} from './repositories/search-query.repository';
import {RecentSearchRepository as RecentSearchSequelizeRepository} from './repositories/sequelize/recent-search.repository';
import {SearchQueryRepository as SearchQuerySequelizeRepository} from './repositories/sequelize/search-query.repository';
import {SearchFilterProvider, SearchProvider} from './services';
import {
SearchModelProvider,
SearchProvider as SearchSequelizeProvider,
} from './services/sequelize';
import {SearchServiceConfig} from './types';
import {defineModelClass} from './utils';

export class SearchServiceComponent<T extends Model> implements Component {
constructor(
@inject(CoreBindings.APPLICATION_INSTANCE)
Expand All @@ -68,11 +73,24 @@ export class SearchServiceComponent<T extends Model> implements Component {
}

this.models = [SearchQuery];

this.providers = {
[SearchServiceBindings.SearchFunction.key]: SearchProvider,
[SearchServiceBindings.SearchFilterFunction.key]: SearchFilterProvider,
};
if (this.config?.useSequelize) {
this.providers = {
[SearchServiceBindings.SearchFunction.key]: SearchSequelizeProvider,
[SearchServiceBindings.SearchFilterFunction.key]: SearchFilterProvider,
[SearchServiceBindings.modelProvider.key]: SearchModelProvider,
};
this.repositories = [
RecentSearchSequelizeRepository,
SearchQuerySequelizeRepository,
];
} else {
this.providers = {
[SearchServiceBindings.SearchFunction.key]: SearchProvider,
[SearchServiceBindings.SearchFilterFunction.key]: SearchFilterProvider,
[SearchServiceBindings.modelProvider.key]: SearchModelProvider,
};
this.repositories = [RecentSearchRepository, SearchQueryRepository];
}

this.application
.bind(SearchServiceBindings.MySQLQueryBuilder)
Expand Down Expand Up @@ -130,7 +148,6 @@ export class SearchServiceComponent<T extends Model> implements Component {
);

this.controllers = [controllerCtor];
this.repositories = [RecentSearchRepository, SearchQueryRepository];
}

providers: ProviderMap = {};
Expand Down
4 changes: 4 additions & 0 deletions services/search-service/src/keys.ts
Original file line number Diff line number Diff line change
Expand Up @@ -5,9 +5,13 @@
import {BindingKey} from '@loopback/core';
import {SearchFilter} from '.';
import {SearchQueryBuilder} from './classes';
import {ModelProviderFn} from './services/model.provider';
import {SearchFunctionType, SearchServiceConfig} from './types';

export namespace SearchServiceBindings {
export const modelProvider = BindingKey.create<ModelProviderFn>(
'sf.search.modelprovider',
);
export const DATASOURCE_NAME = 'SearchServiceDb';
export const SearchFunction =
BindingKey.create<SearchFunctionType<unknown>>('sf.search.function');
Expand Down
2 changes: 2 additions & 0 deletions services/search-service/src/repositories/sequelize/index.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,2 @@
export * from './recent-search.repository';
export * from './search-query.repository';
Original file line number Diff line number Diff line change
@@ -0,0 +1,106 @@
// Copyright (c) 2023 Sourcefuse Technologies
//
// This software is released under the MIT License.
// https://opensource.org/licenses/MIT
import {Getter, inject} from '@loopback/core';
import {HasManyRepositoryFactory, repository} from '@loopback/repository';
import {HttpErrors} from '@loopback/rest';
import {SequelizeDataSource} from '@loopback/sequelize';
import {IAuthUserWithPermissions} from '@sourceloop/core';
import {SequelizeUserModifyCrudRepository} from '@sourceloop/core/sequelize';
import {AuthenticationBindings} from 'loopback4-authentication';
import {SearchServiceConfig} from '../..';
import {DEFAULT_RECENTS, Errors} from '../../const';
import {SearchServiceBindings} from '../../keys';
import {RecentSearch, SearchQuery} from '../../models';
import {SearchQueryRepository} from './search-query.repository';
export class RecentSearchRepository extends SequelizeUserModifyCrudRepository<
RecentSearch,
typeof RecentSearch.prototype.id
> {
public readonly params: HasManyRepositoryFactory<
SearchQuery,
typeof SearchQuery.prototype.id
>;

constructor(
@inject(`datasources.${SearchServiceBindings.DATASOURCE_NAME}`)
dataSource: SequelizeDataSource,
@repository.getter('SearchQueryRepository')
queryRepositoryGetter: Getter<SearchQueryRepository>,
@inject(SearchServiceBindings.Config)
private readonly config: SearchServiceConfig,
@inject.getter(AuthenticationBindings.CURRENT_USER)
protected readonly getCurrentUser: Getter<
IAuthUserWithPermissions | undefined
>,
) {
super(RecentSearch, dataSource, getCurrentUser);
this.params = this.createHasManyRepositoryFactoryFor(
'params',
queryRepositoryGetter,
);
this.registerInclusionResolver('params', this.params.inclusionResolver);
}

async create(query: SearchQuery, user?: IAuthUserWithPermissions) {
if (!user) {
throw new HttpErrors.BadRequest(Errors.USER_MISSING);
}

let saved = await super.findOne({
where: {
userId: user.userTenantId,
},
});

if (saved?.id) {
const prev = await this.params(saved.id).find({
order: ['created_on DESC'],
fields: {
where: false,
},
});

const recentCount =
this.config.controller?.recentCount ?? DEFAULT_RECENTS;

//to delete from recent search if already present
const prevMatched = prev
.filter(
item =>
item.match.toLocaleLowerCase() === query.match.toLocaleLowerCase(),
)
.map(item => item.id);
await this.params(saved.id).delete({
id: {
inq: prevMatched,
},
});

if (prev.length >= recentCount) {
const latestOnes = prev.slice(0, recentCount).map(item => item.id);
await this.params(saved.id).delete({
id: {
nin: latestOnes,
},
});
}
} else {
saved = await super.create({
userId: user.userTenantId,
});
}

if (saved?.id) {
await this.params(saved.id).create({
...query,
recentSearchId: saved.id,
});
} else {
throw new HttpErrors.InternalServerError(Errors.FAILED);
}

return saved;
}
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,35 @@
// Copyright (c) 2023 Sourcefuse Technologies
//
// This software is released under the MIT License.
// https://opensource.org/licenses/MIT
import {Getter, inject} from '@loopback/core';
import {AnyObject, Count, Where} from '@loopback/repository';
import {SequelizeDataSource} from '@loopback/sequelize';
import {IAuthUserWithPermissions} from '@sourceloop/core';
import {SequelizeUserModifyCrudRepository} from '@sourceloop/core/sequelize';
import {AuthenticationBindings} from 'loopback4-authentication';
import {SearchServiceConfig} from '../..';
import {SearchServiceBindings} from '../../keys';
import {SearchQuery} from '../../models';

export class SearchQueryRepository extends SequelizeUserModifyCrudRepository<
SearchQuery,
typeof SearchQuery.prototype.id
> {
constructor(
@inject(`datasources.${SearchServiceBindings.DATASOURCE_NAME}`)
dataSource: SequelizeDataSource,
@inject(SearchServiceBindings.Config)
private readonly config: SearchServiceConfig,
@inject.getter(AuthenticationBindings.CURRENT_USER)
protected readonly getCurrentUser: Getter<
IAuthUserWithPermissions | undefined
>,
) {
super(SearchQuery, dataSource, getCurrentUser);
}

deleteAll(where?: Where<SearchQuery>, options?: AnyObject): Promise<Count> {
return super.deleteAllHard(where, options);
}
}
45 changes: 45 additions & 0 deletions services/search-service/src/services/model.provider.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,45 @@
import {BindingScope, Provider, inject, injectable} from '@loopback/context';
import {SearchServiceBindings} from '../keys';
import {SearchQuery, SearchResult} from '../models';
import {SearchServiceConfig, isSearchableModel} from '../types';
export type ModelProviderFn = (
search: SearchQuery,
// eslint-disable-next-line @typescript-eslint/no-explicit-any
queryBuilder: any, // NOSONAR
// eslint-disable-next-line @typescript-eslint/no-explicit-any
) => Promise<{query: string; params: any[]}>; // NOSONAR
@injectable({scope: BindingScope.SINGLETON})
export class SearchModelProvider implements Provider<ModelProviderFn> {
constructor(
@inject(SearchServiceBindings.Config)
private readonly config: SearchServiceConfig,
) {}
value(): ModelProviderFn {
// eslint-disable-next-line @typescript-eslint/no-explicit-any
// sonarignore:start
return async (search: SearchQuery, queryBuilder: any) => {
// sonarignore:end
let models;
if (search.sources && search.sources.length > 0) {
const sources = search.sources;
models = this.config.models.filter(model => {
if (isSearchableModel(model)) {
return sources.includes(model.identifier ?? model.model.modelName);
} else {
return sources.includes(model.modelName);
}
});
} else {
models = this.config.models;
}
const type = this.config.type ?? SearchResult;

const {query, params} = await queryBuilder.build(
models,
this.config.ignoreColumns,
type,
);
return {query, params};
};
}
}
35 changes: 7 additions & 28 deletions services/search-service/src/services/search.provider.ts
Original file line number Diff line number Diff line change
Expand Up @@ -8,12 +8,8 @@ import {HttpErrors} from '@loopback/rest';
import {MySqlQueryBuilder, PsqlQueryBuilder} from '../classes';
import {CONNECTORS, Errors} from '../const';
import {SearchServiceBindings} from '../keys';
import {SearchResult} from '../models';
import {
isSearchableModel,
SearchFunctionType,
SearchServiceConfig,
} from '../types';
import {SearchFunctionType, SearchServiceConfig} from '../types';
import {ModelProviderFn} from './model.provider';

@injectable({scope: BindingScope.SINGLETON})
export class SearchProvider<T extends Model>
Expand All @@ -28,10 +24,12 @@ export class SearchProvider<T extends Model>
private readonly mySQLBuilder: typeof MySqlQueryBuilder,
@inject(SearchServiceBindings.PostgreSQLQueryBuilder)
private readonly psqlBuilder: typeof PsqlQueryBuilder,
@inject(SearchServiceBindings.modelProvider)
private readonly modelProvider: ModelProviderFn,
) {}

value(): SearchFunctionType<T> {
return search => {
return async search => {
let queryBuilder;

if (!search.match) {
Expand All @@ -53,29 +51,10 @@ export class SearchProvider<T extends Model>
Errors.UNSUPPORTED_CONNECTOR,
);
}
let models;
if (search.sources && search.sources.length > 0) {
const sources = search.sources;
models = this.config.models.filter(model => {
if (isSearchableModel(model)) {
return sources.includes(model.identifier ?? model.model.modelName);
} else {
return sources.includes(model.modelName);
}
});
} else {
models = this.config.models;
}
const type = this.config.type ?? SearchResult;

const {query, params} = queryBuilder.build(
models,
this.config.ignoreColumns,
type,
);
const {query, params} = await this.modelProvider(search, queryBuilder);

try {
return this.datasource.execute(query, params) as Promise<T[]>;
return await (this.datasource.execute(query, params) as Promise<T[]>);
} catch (e) {
throw new HttpErrors.InternalServerError(Errors.FAILED);
}
Expand Down
2 changes: 2 additions & 0 deletions services/search-service/src/services/sequelize/index.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,2 @@
export * from '../model.provider';
export * from './search.provider';
Loading

0 comments on commit c344faf

Please sign in to comment.