skyeye-svc-common-utils 0.0.181 → 0.0.185

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "skyeye-svc-common-utils",
3
- "version": "0.0.181",
3
+ "version": "0.0.185",
4
4
  "description": "",
5
5
  "main": "dist/index.js",
6
6
  "types": "dist/index.d.ts",
@@ -21,8 +21,10 @@
21
21
  },
22
22
  "homepage": "https://github.com/HandshakesByDC/SkyEye-SVC-Common-Utils#readme",
23
23
  "dependencies": {
24
+ "@azure/functions": "^3.0.0",
24
25
  "@azure/identity": "latest",
25
26
  "@azure/keyvault-secrets": "latest",
27
+ "@azure/search-documents": "^11.2.1",
26
28
  "@azure/service-bus": "^7.1.0",
27
29
  "@azure/storage-blob": "^12.1.2",
28
30
  "@azure/storage-queue": "^12.0.2",
@@ -49,7 +51,7 @@
49
51
  "node-fetch": "^2.6.0",
50
52
  "nyc": "^15.1.0",
51
53
  "rotating-file-stream": "^1.4.6",
52
- "skyeye-common-const": "0.0.155",
54
+ "skyeye-common-const": "0.0.161",
53
55
  "swagger-jsdoc": "^3.5.0",
54
56
  "swagger-ui-express": "^4.1.3",
55
57
  "ts-node": "^9.0.0",
@@ -0,0 +1,127 @@
1
+ import { SearchClient, AzureKeyCredential } from '@azure/search-documents';
2
+ import { Semaphore } from 'async-mutex';
3
+ import { ServiceResponse } from '../../interfaces/serviceResponse';
4
+ import { azureSearchConf, AzureSearchIndex } from '../../utils/appConst';
5
+ import { LoggerModel, ExpressServerLogger } from '../../utils/logger';
6
+ import { getKey } from '../azure/azureKeyVault';
7
+
8
+
9
+ /**
10
+ * AzureCognitiveSearchClient as an adaptor to bind the third-party library
11
+ * (@azure/search-documents) with the internal application, that is to say, the
12
+ * class is aim to enhance dev's maintenance between lib and the exact usage.
13
+ * @param instance - the class is used as a Singleton
14
+ * @param endpoint - Azure cognitive search's request URL
15
+ * @param credential - Azure cognitive search's API key
16
+ * @param semaphore - to ensure not too many request are sent to Azure cognitive search
17
+ * @param indexMap - to store the index's alias
18
+ *
19
+ * @example
20
+ * const client = await AzureCognitiveSearchClient.getInstance();
21
+ * const searchOption = new SearchOption( { top: 1 });
22
+ * logger.info(JSON.stringify(await client.query(AzureSearchIndex.company, '*', searchOption.value())));
23
+ */
24
+ export class AzureCognitiveSearchClient {
25
+ private static instance: AzureCognitiveSearchClient;
26
+ private static endpoint: string;
27
+ private static credential: string;
28
+ private static semaphore: Semaphore;
29
+ private static indexMap: Record<AzureSearchIndex, string>;
30
+ private static logger: LoggerModel;
31
+
32
+ /**
33
+ * To retrieve the instance in the Singleton pattern
34
+ * @returns THE ONLY ONE instance of AzureCognitiveSearchClient
35
+ */
36
+ static async getInstance(): Promise<AzureCognitiveSearchClient> {
37
+ if (!!AzureCognitiveSearchClient.instance) { return AzureCognitiveSearchClient.instance; }
38
+ AzureCognitiveSearchClient.instance = new AzureCognitiveSearchClient();
39
+ const secret: { endpoint: string, credential: string } = JSON.parse(await getKey(azureSearchConf.key));
40
+ AzureCognitiveSearchClient.indexMap = JSON.parse(await getKey(azureSearchConf.indexKey));
41
+ AzureCognitiveSearchClient.endpoint = secret.endpoint;
42
+ AzureCognitiveSearchClient.credential = secret.credential;
43
+ AzureCognitiveSearchClient.semaphore = new Semaphore(azureSearchConf.semaphoreParallelMaximum);
44
+
45
+ // @default: the default of logger is to use Express Server.
46
+ AzureCognitiveSearchClient.logger = new ExpressServerLogger();
47
+ return AzureCognitiveSearchClient.instance;
48
+ }
49
+
50
+ setLogger(logger: LoggerModel) {
51
+ AzureCognitiveSearchClient.logger = logger;
52
+ }
53
+
54
+ /**
55
+ * To query the Azure cognitive search
56
+ * @param index to retrieve the document
57
+ * @param keyword as the prop 'search'
58
+ * @param options additional option for the search
59
+ * @param retryTimes specify how many times if Azure cognitive search returns a bad request
60
+ * @returns ServiceResponse, noted that data is stated in data.document
61
+ */
62
+ async query(
63
+ index: AzureSearchIndex,
64
+ keyword: string = '*',
65
+ options?: any,
66
+ retryTimes: number = azureSearchConf.queryRetryDefault
67
+ ): Promise<ServiceResponse> {
68
+
69
+ AzureCognitiveSearchClient.logger.info('start AzureCognitiveSearchClient/query');
70
+ let serviceResponse: ServiceResponse = { isSuccess: false };
71
+ let searchResults: any;
72
+ try {
73
+ let curRetry = 0;
74
+
75
+ const client = new SearchClient(
76
+ AzureCognitiveSearchClient.endpoint,
77
+ AzureCognitiveSearchClient.indexMap[index],
78
+ new AzureKeyCredential(AzureCognitiveSearchClient.credential)
79
+ );
80
+
81
+ while (curRetry < retryTimes) {
82
+ await AzureCognitiveSearchClient.semaphore.runExclusive(async () => {
83
+ try {
84
+ searchResults = await client.search(keyword, options);
85
+
86
+ serviceResponse.data = { document: [] };
87
+
88
+ for await (const result of searchResults.results) {
89
+ serviceResponse.data.document.push(result.document);
90
+ }
91
+
92
+ if (!!searchResults.facets) {
93
+ serviceResponse.data.facets = searchResults.facets;
94
+ }
95
+
96
+ if (!!searchResults.count) {
97
+ serviceResponse.data.count = searchResults.count;
98
+ }
99
+
100
+ serviceResponse.isSuccess = true;
101
+ } catch (err) {
102
+ AzureCognitiveSearchClient.logger.error(`AzureCognitiveSearchClient/query: fail - ${err}`);
103
+ }
104
+ });
105
+
106
+ if (!serviceResponse.isSuccess) {
107
+ AzureCognitiveSearchClient.logger.warn(`AzureCognitiveSearchClient/query: retry - ${curRetry}`);
108
+ ++curRetry;
109
+ this.sleep(azureSearchConf.retryIntervalSecond * 1000);
110
+ } else {
111
+ break;
112
+ }
113
+ }
114
+
115
+ } catch (err) {
116
+ AzureCognitiveSearchClient.logger.error(`AzureCognitiveSearchClient/query: ${err}`);
117
+ }
118
+
119
+ return serviceResponse;
120
+ }
121
+
122
+ /**
123
+ * To postpone for specific millisecond
124
+ * @param ms - time to wait for
125
+ */
126
+ private async sleep(ms: number) { await new Promise(_ => setTimeout(_, ms)); }
127
+ }
@@ -0,0 +1,126 @@
1
+ /**
2
+ * SearchFilter is the object to construct the Filter when using Azure cognitive search,
3
+ * @param attr - the attribute, so called props, to be filtered by the condition
4
+ * @param condition - the condition to filter, see enum @SearchFilter for more information
5
+ * @param value - attr should under the condition with value
6
+ */
7
+ export class SearchFilter {
8
+ attr: string;
9
+ condition: SearchFilterCondition;
10
+ value: any;
11
+ isCollection: boolean = false;
12
+ collectionOperation: 'any' | 'all' = 'any';
13
+
14
+ constructor(
15
+ attr: string,
16
+ condition: SearchFilterCondition,
17
+ value: any,
18
+ isCollection: boolean = false,
19
+ collectionOperation: 'any' | 'all' = 'any') {
20
+ this.attr = attr;
21
+ this.condition = condition;
22
+ this.value = value;
23
+ this.isCollection = isCollection;
24
+ this.collectionOperation = collectionOperation;
25
+ }
26
+
27
+ /**
28
+ * flatten is to compose the instance into a string when using Search
29
+ * @returns the composite of the instance
30
+ */
31
+ flatten(): string {
32
+ if (this.isCollection) {
33
+ switch (this.condition) {
34
+ case CONTAINS:
35
+ return `${this.attr}/${this.collectionOperation}(i: search.in(i,'${this.value.toString()}', ','))`;
36
+ case EXCLUDE:
37
+ return `${this.attr}/${this.collectionOperation}(i: not search.in(i,'${this.value.toString()}', ','))`;
38
+ default:
39
+ throw new Error(`SearchFilter/flatten: Unknown condition for collection`);
40
+ }
41
+ } else {
42
+ switch (this.condition) {
43
+ case EQUAL:
44
+ case NOT_EQUAL:
45
+ case LESS_THAN_OR_EQUAL:
46
+ case LESS_THAN:
47
+ case GREATER_THAN_OR_EQUAL:
48
+ case GREATER_THAN:
49
+ return `${this.attr} ${this.condition} ${ typeof(this.value) === 'string' ? `'${this.value}'` : this.value }`;
50
+ case CONTAINS:
51
+ return `search.in(${this.attr}, '${this.value.toString()}', ',')`;
52
+ case EXCLUDE:
53
+ return `${this.condition} search.in(${this.attr}, '${this.value.toString()}', ',')`;
54
+ default:
55
+ throw new Error(`SearchFilter/flatten: Unknown condition`);
56
+ }
57
+ }
58
+ }
59
+ }
60
+
61
+ /**
62
+ * SearchFilterCondition is equally to the condition when filtering an params.
63
+ */
64
+ export enum SearchFilterCondition {
65
+ contains = '',
66
+ exclude = 'not',
67
+ equal = 'eq',
68
+ notEqual = 'ne',
69
+ lessThanOrEqual = 'le',
70
+ lessThan = 'lt',
71
+ greaterThanOrEqual = 'ge',
72
+ greaterThan = 'gt'
73
+ }
74
+
75
+ /**
76
+ * The following const params is a easy-to-use type when implementing a SearchFilter,
77
+ * so that the developers no longer need to call the enum with the class reference
78
+ */
79
+ export const CONTAINS = SearchFilterCondition.contains;
80
+ export const EXCLUDE = SearchFilterCondition.exclude;
81
+ export const EQUAL = SearchFilterCondition.equal;
82
+ export const NOT_EQUAL = SearchFilterCondition.notEqual;
83
+ export const LESS_THAN_OR_EQUAL = SearchFilterCondition.lessThanOrEqual;
84
+ export const GREATER_THAN_OR_EQUAL = SearchFilterCondition.greaterThanOrEqual;
85
+ export const GREATER_THAN = SearchFilterCondition.greaterThan;
86
+ export const LESS_THAN = SearchFilterCondition.lessThan;
87
+
88
+ /**
89
+ * combination is a function ONLY be used for constructing two function: AND and OR,
90
+ * then it would be express into a string.
91
+ * @param type - to indicate the combination type
92
+ * @param args - the arguments which need to be construct into string
93
+ */
94
+ const combination = (type: 'and' | 'or', ...args: Array<string | SearchFilter>): string => {
95
+ if (args.length === 0) { return ''; }
96
+
97
+ function flatten(obj: string | SearchFilter): string {
98
+ return typeof(obj) === 'string' ? obj : obj.flatten();
99
+ }
100
+
101
+ let res: string = !!args[0] ? flatten(args[0]) : '';
102
+
103
+ for (let i = 1; i < args.length; ++i) {
104
+ if (!!args) {
105
+ res += ` ${type} ${flatten(args[i])}`;
106
+ }
107
+ }
108
+
109
+ return `(${res})`;
110
+ }
111
+
112
+ /**
113
+ * AND helps developers to compose a nested filter for condition AND.
114
+ * @param args - the arguments which need to be construct into string
115
+ */
116
+ export const AND = (...args: Array<string | SearchFilter>): string => {
117
+ return combination('and', ...args);
118
+ }
119
+
120
+ /**
121
+ * OR helps developers to compose a nested filter for condition OR.
122
+ * @param args - the arguments which need to be construct into string
123
+ */
124
+ export const OR = (...args: Array<string | SearchFilter>): string => {
125
+ return combination('or', ...args);
126
+ }
@@ -0,0 +1,148 @@
1
+ import { AND, SearchFilter } from "./SearchFilter";
2
+
3
+
4
+ /**
5
+ * SearchOptionType is the interface to connect with the library @azure/search-documents,
6
+ * it would only be initiated when creating an instance of class @SearchOption,
7
+ * none of the other functions should access this type declaration.
8
+ * @param select - optional, to indicate the select attributes/props.
9
+ * @param filter - optional, search with the filter
10
+ * @param orderBy - optional, return order
11
+ * @param count - optional, if true, return with the count of the documents.
12
+ * @param top - optional, total volume when retrieving the data
13
+ * @param skip - optional, skip how many document
14
+ * @param facets - optional, retrieve data's facets
15
+ * @param searchMode - required, different mode for search
16
+ */
17
+ type SearchOptionType = {
18
+ select?: string[],
19
+ filter?: string | Array<string | SearchFilter>,
20
+ orderBy?: SearchOrder[],
21
+ count?: boolean,
22
+ top?: number,
23
+ skip?: number,
24
+ facets?: SearchFacets[],
25
+ searchMode?: SearchMode
26
+ }
27
+ /**
28
+ * @classdesc the class is to wrap an option when retrieving the data from
29
+ * @azure/search-documents/query
30
+ * @param searchOptionType - to store the interface of the option, noted that
31
+ * when calling the query, you should use SearchOption.value() instead of using
32
+ * this attribute.
33
+ */
34
+ export class SearchOption {
35
+ searchOptionType?: SearchOptionType;
36
+
37
+ constructor(searchOptionType?: SearchOptionType) {
38
+ this.searchOptionType = searchOptionType ?? {
39
+ count: false,
40
+ top: 50,
41
+ skip: 0,
42
+ searchMode: SearchMode.all
43
+ }
44
+ }
45
+
46
+ /**
47
+ * to compose flatten-filter string for @azure/search-documents
48
+ * @returns flatten-filter string
49
+ */
50
+ private composeFilter(): string {
51
+ if (typeof(this.searchOptionType.filter) === 'string') {
52
+ return this.searchOptionType.filter;
53
+ }
54
+ return AND(...this.searchOptionType.filter);
55
+ }
56
+
57
+ /**
58
+ * to compose flatten-order string for @azure/search-documents
59
+ * @returns flatten-order string
60
+ */
61
+ private composeOrder(): string[] {
62
+ return this.searchOptionType.orderBy.map(
63
+ (s: SearchOrder) => `${s.attr} ${s.orderBy}`
64
+ );
65
+ }
66
+
67
+ /**
68
+ * to compose flatten-facets string for @azure/search-documents
69
+ * @returns flatten-facets string
70
+ */
71
+ private composeFacets(): string[] {
72
+ return this.searchOptionType.facets.map((s: SearchFacets) => {
73
+ let res = `${s.attr},count:${s.count}`;
74
+ if (!!s.sort) { res += `,sort:${s.sort}`; }
75
+ return res;
76
+ });
77
+ }
78
+
79
+ /**
80
+ * as an adaptor to transform the type for @azure/search-documents
81
+ * @returns object which can be accepted by @azure/search-documents
82
+ */
83
+ value = () => {
84
+ let value: {
85
+ select?: string[],
86
+ filter?: string;
87
+ orderBy?: string[],
88
+ includeTotalCount?: boolean,
89
+ top?: number,
90
+ skip?: number,
91
+ facets?: string[],
92
+ searchMode?: string,
93
+ } = {};
94
+
95
+ value.includeTotalCount = this.searchOptionType.count;
96
+ value.select = this.searchOptionType.select;
97
+ value.skip = this.searchOptionType.skip;
98
+ value.top = this.searchOptionType.top;
99
+ value.searchMode = this.searchOptionType.searchMode;
100
+
101
+ if (!!this.searchOptionType.filter) {
102
+ value.filter = this.composeFilter();
103
+ }
104
+
105
+ if (!!this.searchOptionType.select && this.searchOptionType.select.length) {
106
+ value.select = this.searchOptionType.select;
107
+ }
108
+
109
+ if (!!this.searchOptionType.orderBy && this.searchOptionType.orderBy.length) {
110
+ value.orderBy = this.composeOrder();
111
+ }
112
+
113
+ if (!!this.searchOptionType.facets && this.searchOptionType.facets.length) {
114
+ value.facets = this.composeFacets();
115
+ }
116
+
117
+ return value;
118
+ }
119
+ }
120
+
121
+ /**
122
+ * SearchOrder as a type to depict the orderBy props
123
+ * @params attr - the attribute/prop regards to be the target
124
+ * @params orderBy - descent or ascent
125
+ */
126
+ export type SearchOrder = {
127
+ attr: string;
128
+ orderBy: 'desc' | 'asc';
129
+ }
130
+
131
+ /**
132
+ * SearchMode is defined by Azure cognitive search
133
+ */
134
+ export enum SearchMode {
135
+ all = 'all'
136
+ }
137
+
138
+ /**
139
+ * SearchFacets as a type is to wrap facets for @azure/search-documents
140
+ * @param attr - attribute/prop to reach
141
+ * @param count - volume of the search
142
+ * @param sort - order
143
+ */
144
+ export type SearchFacets = {
145
+ attr: string;
146
+ count: number;
147
+ sort?: 'count' | '-count' | 'value' | '-value';
148
+ }
@@ -0,0 +1,3 @@
1
+ export * from './AzureCognitiveSearchClient';
2
+ export * from './SearchFilter';
3
+ export * from './SearchOption';
@@ -112,4 +112,25 @@ export enum CAMPAIGN_USER_STATUS {
112
112
  export const exitErrorMessage:Record<string, string> = {
113
113
  SQLQueryRunner: `Requests can only be made in the LoggedIn state, not the SentClientRequest state`,
114
114
  UnknownTimeout: `operation timed out for an unknown reason`
115
+ }
116
+
117
+ export const azureSearchConf = {
118
+ queryRetryDefault: 3,
119
+ semaphoreParallelMaximum: 5,
120
+ retryIntervalSecond: 1,
121
+ key: 'SkyEye-SVC-Search-Credential',
122
+ indexKey: 'SkyEye-SVC-Search-Index'
123
+ }
124
+
125
+ export enum AzureSearchIndex {
126
+ news = 'news',
127
+ company = 'company',
128
+ market = 'market',
129
+ financial = 'financial',
130
+ ott = 'ott',
131
+ nerl = 'nerl',
132
+ newNerl = 'newNerl',
133
+ pocNews = 'azure-search-content-poc',
134
+ pocNerl = 'azure-search-nerl-poc',
135
+ pocNewsDuplicate = 'azure-search-contentduplicate-poc'
115
136
  }
@@ -0,0 +1,27 @@
1
+ import { LoggerModel } from '.';
2
+ import { Context } from '@azure/functions';
3
+
4
+
5
+ /**
6
+ * @todo document
7
+ */
8
+ export class AzureFunctionLogger extends LoggerModel {
9
+ context: Context;
10
+
11
+ constructor(context?: Context) {
12
+ super();
13
+ this.context = context;
14
+ }
15
+
16
+ info(content: any) {
17
+ !!content ? this.context.log.info(`${content}`) : console.log(`[info] ${content}`);
18
+ }
19
+
20
+ error(content: any) {
21
+ !!content ? this.context.log.error(`${content}`) : console.log(`[error] ${content}`);
22
+ }
23
+
24
+ warn(content: any) {
25
+ !!content ? this.context.log.warn(`${content}`) : console.log(`[warn] ${content}`);
26
+ }
27
+ }
@@ -0,0 +1,18 @@
1
+ import winston from 'winston';
2
+ import { LoggerModel } from '.';
3
+ import { logger as defaultLogger } from '../../utils/logger/logger';
4
+
5
+
6
+ /**
7
+ * @todo document
8
+ */
9
+ export class ExpressServerLogger extends LoggerModel {
10
+ logger: winston.Logger;
11
+ constructor(logger: winston.Logger = defaultLogger) {
12
+ super();
13
+ this.logger = logger;
14
+ }
15
+ info(content: any) { this.logger.info(content); }
16
+ error(content: any) { this.logger.error(content); }
17
+ warn(content: any) { this.logger.warn(content); }
18
+ }
@@ -0,0 +1,8 @@
1
+ /**
2
+ * @todo document
3
+ */
4
+ export abstract class LoggerModel {
5
+ abstract info(content: any): void | Promise<void>;
6
+ abstract error(content: any): void | Promise<void>;
7
+ abstract warn(content: any): void | Promise<void>;
8
+ }
@@ -0,0 +1,3 @@
1
+ export * from './LoggerModel';
2
+ export * from './AzureFunctionLogger';
3
+ export * from './ExpressServerLogger';
@@ -22,9 +22,10 @@ export const logger = createLogger({
22
22
 
23
23
  // If we're not in production then **ALSO** log to the `console`
24
24
  // with the colorized simple format.
25
- if (commonAppConfig.NodeEnv === 'dev') {
25
+ if (commonAppConfig.NodeEnv !== 'production') {
26
26
  logger.add(new transports.Console({
27
27
  format: format.combine(
28
+ format.colorize(),
28
29
  format.simple()
29
30
  )
30
31
  }));