Skip to content

Commit 12d843a

Browse files
committed
feat: finished the search api and config doc
1 parent e7c2977 commit 12d843a

11 files changed

Lines changed: 543 additions & 72 deletions

File tree

app/common/PackageUtil.ts

Lines changed: 19 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -3,6 +3,7 @@ import { Readable } from 'node:stream';
33
import { pipeline } from 'node:stream/promises';
44
import * as ssri from 'ssri';
55
import tar from 'tar';
6+
import { AuthorType } from '../repository/PackageRepository';
67

78
// /@cnpm%2ffoo
89
// /@cnpm%2Ffoo
@@ -98,3 +99,21 @@ export async function hasShrinkWrapInTgz(contentOrFile: Uint8Array | string): Pr
9899
throw Object.assign(new Error('[hasShrinkWrapInTgz] Fail to parse input file'), { cause: e });
99100
}
100101
}
102+
103+
/** 写入 ES 时,格式化 author */
104+
export function formatAuthor(author: string | AuthorType | undefined): AuthorType | undefined {
105+
if (author === undefined) {
106+
return author;
107+
}
108+
109+
let ret = {
110+
name: '',
111+
};
112+
113+
if (typeof author === 'string') {
114+
ret.name = author;
115+
} else {
116+
ret = author;
117+
}
118+
return ret;
119+
}

app/core/event/SyncESPackage.ts

Lines changed: 23 additions & 22 deletions
Original file line numberDiff line numberDiff line change
@@ -11,11 +11,10 @@ import {
1111
PACKAGE_TAG_REMOVED,
1212
PACKAGE_MAINTAINER_CHANGED,
1313
PACKAGE_MAINTAINER_REMOVED,
14-
PACKAGE_META_CHANGED, PackageMetaChange,
14+
PACKAGE_META_CHANGED,
1515
} from './index';
1616

1717
import { PackageSearchService } from '../service/PackageSearchService';
18-
import { User } from '../entity/User';
1918

2019
class SyncESPackage {
2120
@Inject()
@@ -24,70 +23,72 @@ class SyncESPackage {
2423
@Inject()
2524
protected readonly config: EggAppConfig;
2625

27-
protected async doSomething(): Promise<unknown> {
28-
throw Error('Not Implemented');
26+
protected async syncPackage(fullname: string) {
27+
if (!this.config.cnpmcore.enableElasticsearch) return;
28+
await this.packageSearchService.syncPackage(fullname, true);
2929
}
3030
}
3131

3232
@Event(PACKAGE_UNPUBLISHED)
3333
export class PackageUnpublished extends SyncESPackage {
34-
async handle() {
35-
throw Error('Not Implemented');
34+
async handle(fullname: string) {
35+
if (!this.config.cnpmcore.enableElasticsearch) return;
36+
await this.packageSearchService.removePackage(fullname);
3637
}
3738
}
3839

3940
@Event(PACKAGE_VERSION_ADDED)
4041
export class PackageVersionAdded extends SyncESPackage {
41-
async handle(_fullname: string, _version: string, _tag?: string) {
42-
throw Error('Not Implemented');
42+
async handle(fullname: string) {
43+
await this.syncPackage(fullname);
4344
}
4445
}
4546

4647
@Event(PACKAGE_VERSION_REMOVED)
4748
export class PackageVersionRemoved extends SyncESPackage {
48-
async handle(_fullname: string, _version: string, _tag?: string) {
49-
throw Error('Not Implemented');
49+
async handle(fullname: string) {
50+
await this.syncPackage(fullname);
5051
}
5152
}
5253

5354
@Event(PACKAGE_TAG_ADDED)
5455
export class PackageTagAdded extends SyncESPackage {
55-
async handle(_fullname: string, _tag: string) {
56-
throw Error('Not Implemented');
56+
async handle(fullname: string) {
57+
await this.syncPackage(fullname);
5758
}
5859
}
5960

6061
@Event(PACKAGE_TAG_CHANGED)
6162
export class PackageTagChanged extends SyncESPackage {
62-
async handle(_fullname: string, _tag: string) {
63-
throw Error('Not Implemented');
63+
async handle(fullname: string) {
64+
await this.syncPackage(fullname);
6465
}
6566
}
6667

6768
@Event(PACKAGE_TAG_REMOVED)
6869
export class PackageTagRemoved extends SyncESPackage {
69-
async handle(_fullname: string, _tag: string) {
70-
throw Error('Not Implemented');
70+
async handle(fullname: string) {
71+
await this.syncPackage(fullname);
7172
}
7273
}
7374

7475
@Event(PACKAGE_MAINTAINER_CHANGED)
7576
export class PackageMaintainerChanged extends SyncESPackage {
76-
async handle(_fullname: string, _maintainers: User[]) {
77-
throw Error('Not Implemented');
77+
async handle(fullname: string) {
78+
await this.syncPackage(fullname);
7879
}
7980
}
8081

8182
@Event(PACKAGE_MAINTAINER_REMOVED)
8283
export class PackageMaintainerRemoved extends SyncESPackage {
83-
async handle(_fullname: string, _maintainer: string) {
84-
throw Error('Not Implemented');
84+
async handle(fullname: string) {
85+
await this.syncPackage(fullname);
8586
}
8687
}
8788

8889
@Event(PACKAGE_META_CHANGED)
8990
export class PackageMetaChanged extends SyncESPackage {
90-
async handle(_fullname: string, _meta: PackageMetaChange) {
91-
throw Error('Not Implemented');
91+
async handle(fullname: string) {
92+
await this.syncPackage(fullname);
9293
}
9394
}

app/core/service/PackageSearchService.ts

Lines changed: 68 additions & 15 deletions
Original file line numberDiff line numberDiff line change
@@ -1,8 +1,14 @@
11
import { AccessLevel, Inject, SingletonProto } from '@eggjs/tegg';
2+
import type { estypes } from '@elastic/elasticsearch';
3+
import dayjs from 'dayjs';
4+
25
import { AbstractService } from '../../common/AbstractService';
3-
import { getScopeAndName } from '../../common/PackageUtil';
6+
import { formatAuthor, getScopeAndName } from '../../common/PackageUtil';
47
import { PackageManagerService } from './PackageManagerService';
5-
import { SearchManifestType, SearchRepository } from '../../repository/SearchRepository';
8+
import { SearchManifestType, SearchMappingType, SearchRepository } from '../../repository/SearchRepository';
9+
import { PackageVersionDownloadRepository } from '../../repository/PackageVersionDownloadRepository';
10+
import { PackageRepository } from '../../repository/PackageRepository';
11+
612

713
@SingletonProto({
814
accessLevel: AccessLevel.PUBLIC,
@@ -12,7 +18,10 @@ export class PackageSearchService extends AbstractService {
1218
private readonly packageManagerService: PackageManagerService;
1319
@Inject()
1420
private readonly searchRepository: SearchRepository;
15-
21+
@Inject()
22+
private packageVersionDownloadRepository: PackageVersionDownloadRepository;
23+
@Inject()
24+
protected packageRepository: PackageRepository;
1625

1726
async syncPackage(fullname: string, isSync = true) {
1827
const [ scope, name ] = getScopeAndName(fullname);
@@ -22,26 +31,68 @@ export class PackageSearchService extends AbstractService {
2231
this.logger.warn('[PackageSearchService.syncPackage] save package:%s not found', fullname);
2332
return;
2433
}
34+
35+
const pkg = await this.packageRepository.findPackage(scope, name);
36+
if (!pkg) {
37+
this.logger.warn('[PackageSearchService.syncPackage] findPackage:%s not found', fullname);
38+
return;
39+
}
40+
41+
// get last year download data
42+
const startDate = dayjs().subtract(1, 'year');
43+
const endDate = dayjs();
44+
45+
const entities = await this.packageVersionDownloadRepository.query(pkg.packageId, startDate.toDate(), endDate.toDate());
46+
let downloadsAll = 0;
47+
for (const entity of entities) {
48+
for (let i = 1; i <= 31; i++) {
49+
const day = String(i).padStart(2, '0');
50+
const field = `d${day}`;
51+
const counter = entity[field];
52+
if (!counter) continue;
53+
downloadsAll += counter;
54+
}
55+
}
56+
57+
const { data: manifest } = fullManifests;
58+
59+
const latestVersion = manifest['dist-tags'].latest;
60+
61+
const packageDoc: SearchMappingType = {
62+
name: manifest.name,
63+
version: latestVersion,
64+
_rev: manifest._rev,
65+
scope: scope ? scope.replace('@', '') : 'unscoped',
66+
keywords: manifest.keywords || [],
67+
versions: Object.keys(manifest.versions),
68+
description: manifest.description,
69+
license: manifest.license,
70+
maintainers: manifest.maintainers,
71+
author: formatAuthor(manifest.author),
72+
'dist-tags': manifest['dist-tags'],
73+
date: manifest.time?.[latestVersion],
74+
created: manifest.time.created,
75+
modified: manifest.time.modified,
76+
};
77+
2578
const document: SearchManifestType = {
26-
package: fullManifests.data,
27-
// TODO get download data from internal data
79+
package: packageDoc,
2880
downloads: {
29-
all: 0,
81+
all: downloadsAll,
3082
},
3183
};
3284

3385
return await this.searchRepository.upsertPackage(document);
3486
}
3587

36-
async searchPackage(text: string | undefined, from: number, size: number): Promise<(SearchManifestType | undefined)[]> {
88+
async searchPackage(text: string | undefined, from: number, size: number): Promise<{ objects: (SearchManifestType | undefined)[], total: number }> {
3789
const matchQueries = this._buildMatchQueries(text);
3890
const scriptScore = this._buildScriptScore({
3991
text,
4092
scoreEffect: 0.25,
4193
});
4294

4395
const res = await this.searchRepository.searchPackage({
44-
type: 'score',
4596
body: {
4697
size,
4798
from,
@@ -59,14 +110,17 @@ export class PackageSearchService extends AbstractService {
59110
},
60111
},
61112
});
62-
const data = res.hits.map(item => {
63-
return item._source;
64-
});
65-
return data;
113+
const { hits, total } = res;
114+
return {
115+
objects: hits?.map(item => {
116+
return item._source;
117+
}),
118+
total: (total as estypes.SearchTotalHits).value,
119+
};
66120
}
67121

68122
async removePackage(fullname: string) {
69-
return await this.searchRepository.remotePackage(fullname);
123+
return await this.searchRepository.removePackage(fullname);
70124
}
71125

72126
// https://github.com/npms-io/queries/blob/master/lib/search.js#L8C1-L78C2
@@ -145,8 +199,7 @@ export class PackageSearchService extends AbstractService {
145199
private _buildScriptScore(params: { text: string | undefined, scoreEffect: number }) {
146200
// keep search simple, only download(popularity)
147201
const downloads = 'doc["downloads.all"].value';
148-
const source = `doc["package.name.raw"].value.equals(params.text) ? 100000 + ${downloads} : _score * Math.pow(${downloads}, params.scoreEffect)`;
149-
202+
const source = `doc["package.name.raw"].value.equals("${params.text}") ? 100000 + ${downloads} : _score * Math.pow(${downloads}, ${params.scoreEffect})`;
150203
return {
151204
script: {
152205
source,

app/infra/SearchAdapter.ts

Lines changed: 3 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -23,7 +23,7 @@ export class ESSearchAdapter implements SearchAdapter {
2323
private readonly elasticsearch: ElasticsearchClient; // 由 elasticsearch 插件引入
2424

2525
async search<T>(query: any): Promise<estypes.SearchHitsMetadata<T>> {
26-
const { elasticsearch: { index } } = this.config;
26+
const { cnpmcore: { elasticsearchIndex: index } } = this.config;
2727
const result = await this.elasticsearch.search<T>({
2828
index,
2929
...query,
@@ -32,7 +32,7 @@ export class ESSearchAdapter implements SearchAdapter {
3232
}
3333

3434
async upsert<T>(id: string, document: T): Promise<string> {
35-
const { elasticsearch: { index } } = this.config;
35+
const { cnpmcore: { elasticsearchIndex: index } } = this.config;
3636
const res = await this.elasticsearch.index({
3737
id,
3838
index,
@@ -42,7 +42,7 @@ export class ESSearchAdapter implements SearchAdapter {
4242
}
4343

4444
async delete(id: string): Promise<string> {
45-
const { elasticsearch: { index } } = this.config;
45+
const { cnpmcore: { elasticsearchIndex: index } } = this.config;
4646
const res = await this.elasticsearch.delete({
4747
index,
4848
id,

app/port/config.ts

Lines changed: 6 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -151,7 +151,11 @@ export type CnpmcoreConfig = {
151151
*/
152152
strictSyncSpecivicVersion: boolean,
153153
/**
154-
* enable elastic search
154+
* enable elasticsearch
155155
*/
156-
enableESSearch: boolean,
156+
enableElasticsearch: boolean,
157+
/**
158+
* elasticsearch index. if enableElasticsearch is true, you must set a index to write es doc.
159+
*/
160+
elasticsearchIndex: string,
157161
};

app/port/controller/package/SearchPackageController.ts

Lines changed: 25 additions & 8 deletions
Original file line numberDiff line numberDiff line change
@@ -2,26 +2,43 @@ import {
22
HTTPController,
33
HTTPMethod,
44
HTTPMethodEnum,
5-
Context,
6-
EggContext,
5+
HTTPParam,
76
HTTPQuery,
87
Inject,
98
} from '@eggjs/tegg';
9+
import { Static } from 'egg-typebox-validate/typebox';
1010
import { AbstractController } from '../AbstractController';
11-
import { Client as ElasticsearchClient } from '@elastic/elasticsearch';
11+
import { SearchQueryOptions } from '../../typebox';
12+
import { PackageSearchService } from '../../../core/service/PackageSearchService';
13+
import { FULLNAME_REG_STRING } from '../../../common/PackageUtil';
1214

1315
@HTTPController()
1416
export class SearchPackageController extends AbstractController {
1517
@Inject()
16-
private readonly elasticsearch: ElasticsearchClient;
18+
private readonly packageSearchService: PackageSearchService;
19+
1720
@HTTPMethod({
1821
// GET /-/v1/search?text=react&size=20&from=0&quality=0.65&popularity=0.98&maintenance=0.5
1922
path: '/-/v1/search',
2023
method: HTTPMethodEnum.GET,
2124
})
22-
// eslint-disable-next-line @typescript-eslint/no-unused-vars
23-
async search(@Context() _ctx: EggContext, @HTTPQuery() _text: string) {
24-
console.log(this.elasticsearch);
25-
return null;
25+
async search(
26+
@HTTPQuery() text: Static<typeof SearchQueryOptions>['text'],
27+
@HTTPQuery() from: Static<typeof SearchQueryOptions>['from'],
28+
@HTTPQuery() size: Static<typeof SearchQueryOptions>['size'],
29+
) {
30+
if (!this.config.cnpmcore.enableElasticsearch) return;
31+
const data = await this.packageSearchService.searchPackage(text, from, size);
32+
return data;
33+
}
34+
35+
@HTTPMethod({
36+
path: `/-/v1/search/sync/:fullname(${FULLNAME_REG_STRING})`,
37+
method: HTTPMethodEnum.GET,
38+
})
39+
async sync(@HTTPParam() fullname: string) {
40+
if (!this.config.cnpmcore.enableElasticsearch) return;
41+
const data = await this.packageSearchService.syncPackage(fullname, true);
42+
return data;
2643
}
2744
}

app/port/typebox.ts

Lines changed: 16 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -271,3 +271,19 @@ export const ScopeUpdateOptions = Type.Object({
271271
maxLength: 256,
272272
}),
273273
});
274+
275+
export const SearchQueryOptions = Type.Object({
276+
from: Type.Number({
277+
transform: [ 'trim' ],
278+
minimum: 0,
279+
}),
280+
size: Type.Number({
281+
transform: [ 'trim' ],
282+
minimum: 1,
283+
}),
284+
text: Type.Optional(Type.String({
285+
transform: [ 'trim' ],
286+
minLength: 1,
287+
maxLength: 256,
288+
})),
289+
});

app/repository/PackageRepository.ts

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -117,7 +117,7 @@ type DistType = {
117117
[key: string]: unknown,
118118
};
119119

120-
type AuthorType = {
120+
export type AuthorType = {
121121
name: string;
122122
email?: string;
123123
url?: string;

0 commit comments

Comments
 (0)