Skip to content
Merged
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 api/src/app/index.ts
Original file line number Diff line number Diff line change
Expand Up @@ -41,7 +41,7 @@ useContainer(Container); // eslint-disable-line react-hooks/rules-of-hooks

// Initialize Search Service
const searchService = Container.get(SearchService);
await searchService.ensureIndexes();
await searchService.setupSearch();

// Add crons to DI container
const CronServices = [DigestCron];
Expand Down
2 changes: 1 addition & 1 deletion api/src/contributor/table.ts
Original file line number Diff line number Diff line change
Expand Up @@ -8,7 +8,7 @@ export const contributorsTable = pgTable("contributors", {
recordImportedAt: text("record_imported_at")
.notNull()
.default(sql`CURRENT_TIMESTAMP`),
runId: text("run_id").notNull().default("initial-run-id"),
runId: text("run_id").notNull(),
name: text("name").notNull(),
username: text("username").notNull(),
url: text("url").notNull().unique(),
Expand Down
89 changes: 62 additions & 27 deletions api/src/digest/cron.ts
Original file line number Diff line number Diff line change
@@ -1,12 +1,17 @@
import { captureException, cron } from "@sentry/node";
import { CronJob } from "cron";

import { ContributionRepository } from "src/contribution/repository";
import { ContributionRow } from "src/contribution/table";
import { ContributorRepository } from "src/contributor/repository";
import { ContributorRow } from "src/contributor/table";
import { CronJob } from "cron";
import { DataService } from "src/data/service";
import { GithubService } from "src/github/service";
import { LoggerService } from "src/logger/service";
import { ProjectRepository } from "src/project/repository";
import { ProjectRow } from "src/project/table";
import { RepositoryRepository } from "src/repository/repository";
import { SearchService } from "src/search/service";
import { Service } from "typedi";

@Service()
Expand All @@ -22,6 +27,7 @@ export class DigestCron {
private readonly repositoriesRepository: RepositoryRepository,
private readonly contributionsRepository: ContributionRepository,
private readonly contributorsRepository: ContributorRepository,
private readonly searchService: SearchService,
) {
const SentryCronJob = cron.instrumentCron(CronJob, "DigestCron");
new SentryCronJob(
Expand Down Expand Up @@ -67,11 +73,14 @@ export class DigestCron {
const projectsFromDataFolder = await this.dataService.listProjects();

for (const project of projectsFromDataFolder) {
const [{ id: projectId }] = await this.projectsRepository.upsert({
...project,
const projectEntity: ProjectRow = {
runId,
id: project.slug,
});
id: project.slug.replace(/[.]/g, "-"), // NOTE-OB: MeiliSearch doesn't allow dots in ids
name: project.name,
};
const [{ id: projectId }] =
await this.projectsRepository.upsert(projectEntity);
await this.searchService.upsert("project", projectEntity);

let addedRepositoryCount = 0;
try {
Expand All @@ -84,15 +93,16 @@ export class DigestCron {
});

const provider = "github";
const [{ id: repositoryId }] = await this.repositoriesRepository.upsert({
provider,
name: repoInfo.name,
owner: repoInfo.owner.login,
runId,
projectId,
stars: repoInfo.stargazers_count,
id: `${provider}-${repoInfo.id}`,
});
const [{ id: repositoryId }] =
await this.repositoriesRepository.upsert({
provider,
name: repoInfo.name,
owner: repoInfo.owner.login,
runId,
projectId,
stars: repoInfo.stargazers_count,
id: `${provider}-${repoInfo.id}`,
});
addedRepositoryCount++;

const issues = await this.githubService.listRepositoryIssues({
Expand All @@ -101,18 +111,24 @@ export class DigestCron {
});

for (const issue of issues) {
const githubUser = await this.githubService.getUser({ username: issue.user.login });
const githubUser = await this.githubService.getUser({
username: issue.user.login,
});

if (githubUser.type !== "User") continue;

const [{ id: contributorId }] = await this.contributorsRepository.upsert({
const contributorEntity: ContributorRow = {
name: githubUser.name || githubUser.login,
username: githubUser.login,
url: githubUser.html_url,
avatarUrl: githubUser.avatar_url,
runId,
id: `${provider}-${githubUser.login}`,
});
};

const [{ id: contributorId }] =
await this.contributorsRepository.upsert(contributorEntity);
await this.searchService.upsert("contributor", contributorEntity);

await this.contributorsRepository.upsertRelationWithRepository({
contributorId,
Expand All @@ -122,23 +138,32 @@ export class DigestCron {
});

const type = issue.pull_request ? "PULL_REQUEST" : "ISSUE";
await this.contributionsRepository.upsert({
const contributionEntity: ContributionRow = {
title: issue.title,
type,
updatedAt: issue.updated_at,
activityCount: issue.comments,
runId,
url: type === "PULL_REQUEST" ? issue.pull_request.html_url : issue.html_url,
url:
type === "PULL_REQUEST"
? issue.pull_request.html_url
: issue.html_url,
repositoryId,
contributorId,
id: `${provider}-${issue.id}`,
});
};
await this.contributionsRepository.upsert(contributionEntity);
await this.searchService.upsert(
"contribution",
contributionEntity,
);
}

const repoContributors = await this.githubService.listRepositoryContributors({
owner: repository.owner,
repository: repository.name,
});
const repoContributors =
await this.githubService.listRepositoryContributors({
owner: repository.owner,
repository: repository.name,
});

const repoContributorsFiltered = repoContributors.filter(
(contributor) => contributor.type === "User",
Expand All @@ -148,14 +173,17 @@ export class DigestCron {
const contributor = await this.githubService.getUser({
username: repoContributor.login,
});
const [{ id: contributorId }] = await this.contributorsRepository.upsert({
const contributorEntity: ContributorRow = {
name: contributor.name || contributor.login,
username: contributor.login,
url: contributor.html_url,
avatarUrl: contributor.avatar_url,
runId,
id: `${provider}-${contributor.login}`,
});
};
const [{ id: contributorId }] =
await this.contributorsRepository.upsert(contributorEntity);
await this.searchService.upsert("contributor", contributorEntity);

await this.contributorsRepository.upsertRelationWithRepository({
contributorId,
Expand All @@ -179,11 +207,18 @@ export class DigestCron {
}

try {
await this.contributorsRepository.deleteAllRelationWithRepositoryButWithRunId(runId);
await this.contributorsRepository.deleteAllRelationWithRepositoryButWithRunId(
runId,
);
await this.contributionsRepository.deleteAllButWithRunId(runId);
await this.contributorsRepository.deleteAllButWithRunId(runId);
await this.repositoriesRepository.deleteAllButWithRunId(runId);
await this.projectsRepository.deleteAllButWithRunId(runId);
await Promise.all([
this.searchService.deleteAllButWithRunId("project", runId),
this.searchService.deleteAllButWithRunId("contribution", runId),
this.searchService.deleteAllButWithRunId("contributor", runId),
]);
} catch (error) {
captureException(error, { tags: { type: "CRON" } });
}
Expand Down
2 changes: 1 addition & 1 deletion api/src/project/table.ts
Original file line number Diff line number Diff line change
Expand Up @@ -8,7 +8,7 @@ export const projectsTable = pgTable("projects", {
.notNull()
.default(sql`CURRENT_TIMESTAMP`),
name: text("name").notNull(),
runId: text("run_id").notNull().default("initial-run-id"),
runId: text("run_id").notNull(),
});

projectsTable.$inferSelect satisfies ProjectEntity;
Expand Down
72 changes: 56 additions & 16 deletions api/src/search/service.ts
Original file line number Diff line number Diff line change
@@ -1,5 +1,6 @@
import { SearchItem, SearchType } from "./types";
import { SearchResults, SearchType } from "./types";

import { BaseEntity } from "@dzcode.io/models/dist/_base";
import { ConfigService } from "src/config/service";
import { LoggerService } from "src/logger/service";
import { MeiliSearch } from "meilisearch";
Expand All @@ -25,32 +26,71 @@ export class SearchService {
});
}

public search = async (query: string): Promise<SearchItem[]> => {
public search = async (query: string): Promise<SearchResults> => {
this.logger.info({ message: `Searching for ${query}` });
return [];
return {
results: [],
};
};

public index = async (
public upsert = async <T extends BaseEntity>(
index: SearchType,
data: SearchItem[],
data: T,
): Promise<void> => {
this.logger.info({
message: `Indexing ${data.length} items in ${index}`,
message: `Upserting "${data.id}" item to ${index}`,
});
await this.meilisearch.index(index).updateDocuments([data]);
this.logger.info({ message: `Upserted "${data.id}" item to ${index}` });
};

public deleteAllButWithRunId = async (
index: SearchType,
runId: string,
): Promise<void> => {
this.logger.info({
message: `Deleting all ${index} but with runId ${runId}`,
});
await this.meilisearch.index(index).deleteDocuments({
filter: `NOT runId=${runId}`,
});
await this.meilisearch.index(index).addDocuments(data);
this.logger.info({
message: `Indexed ${data.length} items in ${index}`,
message: `Deleted all ${index} but with runId ${runId}`,
});
};

public ensureIndexes = async (): Promise<void> => {
await this.meilisearch.createIndex("project");
this.logger.info({ message: "project index created" });

await this.meilisearch.createIndex("contribution");
this.logger.info({ message: "contribution index created" });
public setupSearch = async (): Promise<void> => {
await this.setupIndexes();
await this.updateFilterableAttributes();
};

await this.meilisearch.createIndex("contributor");
this.logger.info({ message: "contributor index created" });
private setupIndexes = async (): Promise<void> => {
await this.upsertIndex("project");
await this.upsertIndex("contribution");
await this.upsertIndex("contributor");
};

private async upsertIndex(index: SearchType): Promise<void> {
try {
await this.meilisearch.getIndex(index);
this.logger.info({ message: `${index} index already exists` });
} catch {
await this.meilisearch.createIndex(index, {
primaryKey: "id",
});
this.logger.info({ message: `${index} index created` });
}
}

private async updateFilterableAttributes(): Promise<void> {
await this.meilisearch
.index("project")
.updateFilterableAttributes(["runId"]);
await this.meilisearch
.index("contribution")
.updateFilterableAttributes(["runId"]);
await this.meilisearch
.index("contributor")
.updateFilterableAttributes(["runId"]);
}
}
14 changes: 8 additions & 6 deletions api/src/search/types.ts
Original file line number Diff line number Diff line change
@@ -1,13 +1,15 @@
import { ContributionEntity } from "@dzcode.io/models/dist/contribution";
import { ContributorEntity } from "@dzcode.io/models/dist/contributor";
import { GeneralResponse } from "src/app/types";
import { MultiSearchResponse } from "meilisearch";
import { ProjectEntity } from "@dzcode.io/models/dist/project";

export interface GetSearchResponse extends GeneralResponse {
searchResults: Array<SearchItem>;
searchResults: SearchResults;
}

export interface SearchItem {
id: string;
title: string;
type: SearchType;
}
export type SearchResults = MultiSearchResponse<
ProjectEntity | ContributionEntity | ContributorEntity
>;

export type SearchType = "project" | "contribution" | "contributor";
Loading