@c-rex/core 0.0.3 → 0.0.6

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,31 +1,26 @@
1
1
  {
2
2
  "name": "@c-rex/core",
3
- "version": "0.0.3",
4
- "main": "dist/index.js",
5
- "module": "dist/index.mjs",
6
- "types": "dist/index.d.ts",
7
- "files": ["dist"],
3
+ "version": "0.0.6",
4
+ "files": [
5
+ "src"
6
+ ],
7
+ "publishConfig": {
8
+ "access": "public"
9
+ },
8
10
  "exports": {
9
11
  ".": {
10
- "types": "./dist/index.d.ts",
11
- "import": "./dist/index.mjs",
12
- "require": "./dist/index.js",
13
- "default": "./dist/index.js"
14
- },
15
- "./package.json": "./package.json",
16
- "./logger.server": {
17
- "import": "./dist/logger.server.mjs",
18
- "require": "./dist/logger.server.cjs"
12
+ "types": "./src/index.ts",
13
+ "import": "./src/index.ts",
14
+ "require": "./src/index.ts",
15
+ "default": "./src/index.ts"
19
16
  },
20
- "./logger.client": {
21
- "import": "./dist/logger.client.mjs",
22
- "require": "./dist/logger.client.cjs"
23
- }
17
+ "./package.json": "./package.json"
24
18
  },
25
- "sideEffects": false,
26
19
  "scripts": {
27
- "dev": "tsup src/index.ts --splitting --format cjs,esm --dts --watch",
28
- "build": "tsup src/index.ts --splitting --format cjs,esm --dts"
20
+ "dev": "echo 'Nothing to build using raw TypeScript files'",
21
+ "build": "echo 'No build needed'",
22
+ "test:watch": "jest --watch",
23
+ "test": "jest"
29
24
  },
30
25
  "devDependencies": {
31
26
  "@c-rex/eslint-config": "*",
@@ -34,13 +29,15 @@
34
29
  "@types/jest": "^29.5.14",
35
30
  "@types/node": "^22.13.10",
36
31
  "eslint": "^9.23.0",
37
- "typescript": "latest",
38
- "tsup": "^8.4.0"
32
+ "jest": "^29.7.0",
33
+ "ts-jest": "^29.3.2",
34
+ "tsup": "^8.4.0",
35
+ "typescript": "latest"
39
36
  },
40
37
  "dependencies": {
41
- "@c-rex/types": "*",
42
38
  "@c-rex/constants": "*",
43
39
  "@c-rex/interfaces": "*",
40
+ "@c-rex/types": "*",
44
41
  "axios": "^1.8.4",
45
42
  "winston": "^3.17.0",
46
43
  "winston-graylog2": "^2.1.2"
@@ -0,0 +1,7 @@
1
+ import '../logger';
2
+
3
+ describe('Force coverage inclusion', () => {
4
+ it('Dummy', () => {
5
+ expect(true).toBe(true);
6
+ });
7
+ });
@@ -0,0 +1,91 @@
1
+ import { CrexApi } from '../requests';
2
+ import { CrexLogger } from '../logger';
3
+ import axios from 'axios';
4
+
5
+ jest.mock('axios');
6
+ jest.mock('../logger');
7
+
8
+ describe('CrexApi', () => {
9
+ const mockBaseUrl = 'http://api.test.com';
10
+ const mockLogger = new CrexLogger({} as any);
11
+ let api: CrexApi;
12
+
13
+ beforeEach(() => {
14
+ jest.clearAllMocks();
15
+ (axios.create as jest.Mock).mockReturnValue({
16
+ request: jest.fn()
17
+ });
18
+ api = new CrexApi(mockBaseUrl, mockLogger);
19
+ });
20
+
21
+ describe('execute', () => {
22
+ it('should make a successful API request', async () => {
23
+ const mockResponse = { data: { id: 1 }, status: 200 };
24
+ const mockAxiosInstance = axios.create();
25
+ (mockAxiosInstance.request as jest.Mock).mockResolvedValue(mockResponse);
26
+
27
+ const result = await api.execute({
28
+ url: '/test',
29
+ method: 'GET',
30
+ headers: { 'X-Test': 'test' }
31
+ });
32
+
33
+ expect(result).toEqual(mockResponse.data);
34
+ expect(mockAxiosInstance.request).toHaveBeenCalledWith({
35
+ url: '/test',
36
+ method: 'GET',
37
+ headers: { 'X-Test': 'test' },
38
+ data: undefined,
39
+ params: undefined
40
+ });
41
+ });
42
+
43
+ it('should handle request with body and params', async () => {
44
+ const mockResponse = { data: { success: true }, status: 200 };
45
+ const mockAxiosInstance = axios.create();
46
+ (mockAxiosInstance.request as jest.Mock).mockResolvedValue(mockResponse);
47
+
48
+ await api.execute({
49
+ url: '/test',
50
+ method: 'POST',
51
+ body: { name: 'test' },
52
+ params: { id: 1 }
53
+ });
54
+
55
+ expect(mockAxiosInstance.request).toHaveBeenCalledWith({
56
+ url: '/test',
57
+ method: 'POST',
58
+ data: { name: 'test' },
59
+ params: { id: 1 },
60
+ headers: undefined
61
+ });
62
+ });
63
+
64
+ it('should retry failed requests', async () => {
65
+ const mockError = new Error('Network error');
66
+ const mockAxiosInstance = axios.create();
67
+ (mockAxiosInstance.request as jest.Mock).mockRejectedValue(mockError);
68
+
69
+ await expect(api.execute({
70
+ url: '/test',
71
+ method: 'GET'
72
+ })).rejects.toThrow('Network error');
73
+
74
+ expect(mockLogger.log).toHaveBeenCalledWith(
75
+ 'error',
76
+ expect.stringContaining('Network error')
77
+ );
78
+ expect(mockAxiosInstance.request).toHaveBeenCalledTimes(1);
79
+ });
80
+
81
+ it('should throw error when no valid response after retries', async () => {
82
+ const mockAxiosInstance = axios.create();
83
+ (mockAxiosInstance.request as jest.Mock).mockResolvedValue(undefined);
84
+
85
+ await expect(api.execute({
86
+ url: '/test',
87
+ method: 'GET'
88
+ })).rejects.toThrow('Failed to retrieve a valid response');
89
+ });
90
+ });
91
+ });
@@ -0,0 +1,82 @@
1
+ import { CrexSDK } from '../sdk';
2
+ import { CrexLogger } from '../logger';
3
+ import { CrexApi } from '../requests';
4
+ import { ConfigInterface } from '@c-rex/interfaces';
5
+
6
+ jest.mock('../logger');
7
+ jest.mock('../requests');
8
+
9
+ describe('CrexSDK', () => {
10
+ const mockConfig: ConfigInterface = {
11
+ projectName: 'test-project',
12
+ baseUrl: 'http://test.com',
13
+ search: {
14
+ fields: [],
15
+ tags: [],
16
+ restrict: [],
17
+ filter: [],
18
+ sparqlWhere: ''
19
+ },
20
+ logs: {
21
+ graylog: {
22
+ silent: true,
23
+ url: 'http://graylog.test',
24
+ app: 'test-app',
25
+ minimumLevel: 'info',
26
+ categoriesLevel: ['Document']
27
+ },
28
+ matomo: {
29
+ silent: false,
30
+ url: '',
31
+ app: '',
32
+ minimumLevel: 'info',
33
+ categoriesLevel: []
34
+ },
35
+ console: {
36
+ silent: false,
37
+ url: '',
38
+ app: '',
39
+ minimumLevel: 'info',
40
+ categoriesLevel: []
41
+ }
42
+ }
43
+ };
44
+
45
+ beforeEach(() => {
46
+ jest.clearAllMocks();
47
+ // Reset the singleton instance
48
+ (CrexSDK as any).instance = undefined;
49
+ });
50
+
51
+ describe('constructor', () => {
52
+ it('should create a new instance with correct configuration', () => {
53
+ const sdk = new CrexSDK(mockConfig);
54
+
55
+ expect(sdk.customerConfig).toBe(mockConfig);
56
+ expect(sdk.logger).toBeInstanceOf(CrexLogger);
57
+ expect(sdk.api).toBeInstanceOf(CrexApi);
58
+ expect(CrexLogger).toHaveBeenCalledWith(mockConfig);
59
+ expect(CrexApi).toHaveBeenCalledWith(mockConfig.baseUrl, expect.any(CrexLogger));
60
+ });
61
+
62
+ it('should return existing instance if already initialized', () => {
63
+ const firstInstance = new CrexSDK(mockConfig);
64
+ const secondInstance = new CrexSDK(mockConfig);
65
+
66
+ expect(secondInstance).toBe(firstInstance);
67
+ });
68
+ });
69
+
70
+ describe('getInstance', () => {
71
+ it('should return existing instance', () => {
72
+ const sdk = new CrexSDK(mockConfig);
73
+ const instance = CrexSDK.getInstance();
74
+
75
+ expect(instance).toBe(sdk);
76
+ });
77
+
78
+ it('should throw error if instance not initialized', () => {
79
+ expect(() => CrexSDK.getInstance()).toThrow('SDK not initialized');
80
+ });
81
+ });
82
+ });
package/src/cli.ts ADDED
@@ -0,0 +1,189 @@
1
+ /*
2
+ import { execSync } from 'child_process';
3
+ import { checkbox, confirm, input, select } from '@inquirer/prompts';
4
+
5
+ import fs from 'fs';
6
+ import path from 'path';
7
+ import { FILTER_OPTIONS, LOG_CATEGORIES, LOG_LEVELS } from '../constants/log';
8
+ import { RESULT_VIEW_OPTIONS } from '../constants/components';
9
+
10
+ type Restriction = {
11
+ key: string;
12
+ value: string;
13
+ operator: string;
14
+ }
15
+ const addRestriction = async (word: string): Promise<Restriction[]> => {
16
+ const restrictions: any = []
17
+
18
+ let aux = await confirm({
19
+ message: `Do you want to add ${word}s to your search?`,
20
+ default: false,
21
+ });
22
+
23
+ while (aux) {
24
+ const restriction: Restriction = {
25
+ key: "",
26
+ value: '${value}',
27
+ operator: ""
28
+ }
29
+
30
+ restriction.key = await input({
31
+ message: `Type the ${word} key:`,
32
+ });
33
+
34
+ restriction.operator = await select({
35
+ message: `Type the ${word} operator:`,
36
+ choices: FILTER_OPTIONS.map(key => ({ value: key })),
37
+ default: FILTER_OPTIONS[0]
38
+ });
39
+
40
+ const byValue = await confirm({
41
+ message: `Do you want apply your ${word} using a fixed value?`,
42
+ default: false
43
+ });
44
+
45
+ if (byValue) {
46
+ restriction.value = await input({
47
+ message: `Type the ${word} value:`,
48
+ });
49
+ }
50
+
51
+ restrictions.push(restriction);
52
+
53
+ aux = await confirm({
54
+ message: `Do you want to add more ${word}s?`,
55
+ default: false
56
+ });
57
+ }
58
+
59
+ return restrictions;
60
+ }
61
+
62
+ const log = async (name: string): Promise<any> => {
63
+ const log = {
64
+ silent: false,
65
+ logLevel: [],
66
+ categoriesLevel: [],
67
+ url: "",
68
+ app: "",
69
+ }
70
+ log.silent = await confirm({
71
+ message: `Do you want to silent ${name}?`,
72
+ default: false
73
+ });
74
+
75
+ if (!log.silent) {
76
+ log.url = await input({
77
+ message: `Type the ${name} URL:`,
78
+ });
79
+ log.app = await input({
80
+ message: `Type the ${name} app name:`,
81
+ });
82
+ const logLevel = await checkbox({
83
+ message: `Select the log level to ${name}:`,
84
+ choices: Object.keys(LOG_LEVELS).map(key => {
85
+ return {
86
+ value: key,
87
+ checked: true,
88
+ };
89
+ }),
90
+ });
91
+ const categoriesLevel = await checkbox({
92
+ message: `Select the categories level to ${name}:`,
93
+ choices: LOG_CATEGORIES.map(key => {
94
+ return {
95
+ value: key,
96
+ checked: true,
97
+ };
98
+ }),
99
+ });
100
+
101
+ log.logLevel = logLevel as any;
102
+ log.categoriesLevel = categoriesLevel as any;
103
+ }
104
+
105
+ return log;
106
+ }
107
+
108
+ async function main() {
109
+ try {
110
+ const projectName = await input({
111
+ message: 'Set the project name:',
112
+ default: "c-rex.net"
113
+ });
114
+
115
+ const baseUrl = await input({
116
+ message: 'Set the base URL:',
117
+ default: "https://c-rex.net/ids/api/iirds/v1/"
118
+ });
119
+
120
+ const resultViewStyle = await select({
121
+ message: 'Results should be shown as:',
122
+ choices: Object.keys(RESULT_VIEW_OPTIONS).map(key => {
123
+ return { value: key };
124
+ }),
125
+ default: RESULT_VIEW_OPTIONS.table
126
+ });
127
+
128
+ const searchFields = await input({
129
+ message: 'Type the fields used in search separated by comma:',
130
+ default: "titles,labels,languages"
131
+ });
132
+
133
+ const searchTags = await input({
134
+ message: 'Type the tags used in search separated by comma:',
135
+ default: ""
136
+ });
137
+
138
+ const restrictions = await addRestriction("restriction");
139
+ const filters = await addRestriction("filter");
140
+
141
+ const sparqlWhere = await input({
142
+ message: 'Type the sparqlWhere string:',
143
+ default: ""
144
+ });
145
+
146
+ const graylog = await log("Graylog");
147
+ const matomo = await log("Matomo");
148
+
149
+ const config = {
150
+ projectName: projectName,
151
+ baseUrl: baseUrl,
152
+ search: {
153
+ fields: searchFields.split(','),
154
+ tags: searchTags.split(','),
155
+ restrict: restrictions,
156
+ filter: filters,
157
+ sparqlWhere: sparqlWhere
158
+ },
159
+ resultViewStyle: resultViewStyle,
160
+ logs: {
161
+ graylog: graylog,
162
+ matomo: matomo
163
+ }
164
+ }
165
+
166
+ const configFilePath = path.join(process.cwd(), 'src', 'config', 'customerConfig.ts');
167
+
168
+ const configFileContent = `
169
+ import { ConfigInterface } from "@/interfaces/config";
170
+ export const CUSTOMER_CONFIG: ConfigInterface = ${JSON.stringify(config, null, 4)};
171
+ `;
172
+
173
+ fs.writeFileSync(configFilePath, configFileContent);
174
+
175
+ console.log('Installing dependencies...');
176
+ execSync('npm install', { stdio: 'inherit' });
177
+
178
+ console.log('Configuration completed successfully!');
179
+ } catch (error: any) {
180
+ if (error instanceof Error && error.name === 'ExitPromptError') {
181
+ console.log('👋 until next time!');
182
+ } else {
183
+ console.error('Error during configuration:', error);
184
+ }
185
+ }
186
+ }
187
+
188
+ main();
189
+ */
package/src/index.ts ADDED
@@ -0,0 +1,3 @@
1
+ //export * from "./logger";
2
+ export * from "./requests";
3
+ export * from "./sdk";
package/src/logger.ts ADDED
@@ -0,0 +1,24 @@
1
+ import { ConfigInterface } from "@c-rex/interfaces";
2
+ import { LogCategoriesType, LogLevelType } from "@c-rex/types";
3
+
4
+ export class CrexLogger {
5
+ private customerConfig: ConfigInterface;
6
+
7
+ constructor(config: ConfigInterface) {
8
+ this.customerConfig = config;
9
+ }
10
+
11
+ async log(level: LogLevelType, message: string, category?: LogCategoriesType) {
12
+ if (typeof window === "undefined") {
13
+ const { LoggerServer } = await import("./logs/server");
14
+ const serverLog = new LoggerServer(this.customerConfig);
15
+ serverLog.log(level, message, category);
16
+ } else {
17
+ fetch("/api/log", {
18
+ method: "POST",
19
+ headers: { "Content-Type": "application/json" },
20
+ body: JSON.stringify({ level, message, category }),
21
+ }).catch((err) => console.error("Erro ao enviar log", err));
22
+ }
23
+ }
24
+ }
@@ -0,0 +1,97 @@
1
+ import winston from 'winston';
2
+ import { LogLevelType, LogCategoriesType } from '@c-rex/types';
3
+ import { ConfigInterface } from '@c-rex/interfaces';
4
+ import { LoggerServer } from '../server';
5
+ import { MatomoTransport } from '../../transports/matomo';
6
+ import { GraylogTransport } from '../../transports/graylog';
7
+
8
+ jest.mock('winston', () => {
9
+ const actual = jest.requireActual('winston');
10
+ return {
11
+ ...actual,
12
+ createLogger: jest.fn(),
13
+ transports: {
14
+ Console: jest.fn().mockImplementation(() => ({
15
+ name: 'ConsoleTransport',
16
+ })),
17
+ },
18
+ };
19
+ });
20
+
21
+ jest.mock('../../transports/matomo', () => ({
22
+ MatomoTransport: jest.fn().mockImplementation(() => ({
23
+ name: 'MatomoTransport',
24
+ })),
25
+ }));
26
+
27
+ jest.mock('../../transports/graylog', () => ({
28
+ GraylogTransport: jest.fn().mockImplementation(() => ({
29
+ name: 'GraylogTransport',
30
+ })),
31
+ }));
32
+
33
+ describe('LoggerServer', () => {
34
+ const mockConfig: ConfigInterface = {
35
+ logs: {
36
+ console: {
37
+ minimumLevel: 'info',
38
+ silent: false,
39
+ },
40
+ graylog: {
41
+ minimumLevel: 'error',
42
+ silent: true,
43
+ },
44
+ matomo: {
45
+ categoriesLevel: [],
46
+ },
47
+ },
48
+ customerConfig: {
49
+ logs: {
50
+ matomo: {
51
+ categoriesLevel: [],
52
+ },
53
+ },
54
+ },
55
+ } as unknown as ConfigInterface;
56
+
57
+ const mockLogger = {
58
+ log: jest.fn(),
59
+ };
60
+
61
+ beforeEach(() => {
62
+ (winston.createLogger as jest.Mock).mockReturnValue(mockLogger);
63
+ });
64
+
65
+ afterEach(() => {
66
+ jest.clearAllMocks();
67
+ });
68
+
69
+ it('deve criar o logger com os transports corretos', () => {
70
+ new LoggerServer(mockConfig);
71
+
72
+ expect(winston.transports.Console).toHaveBeenCalledWith({
73
+ level: 'info',
74
+ silent: false,
75
+ });
76
+
77
+ expect(MatomoTransport).toHaveBeenCalledWith({
78
+ level: 'info',
79
+ silent: false,
80
+ });
81
+
82
+ expect(GraylogTransport).toHaveBeenCalledWith({
83
+ level: 'error',
84
+ silent: true,
85
+ });
86
+
87
+ expect(winston.createLogger).toHaveBeenCalled();
88
+ });
89
+
90
+ it('deve chamar logger.log com os parâmetros corretos', () => {
91
+ const loggerServer = new LoggerServer(mockConfig);
92
+
93
+ loggerServer.log('info' as LogLevelType, 'mensagem de teste', 'analytics' as LogCategoriesType);
94
+
95
+ expect(mockLogger.log).toHaveBeenCalledWith('info', 'mensagem de teste', 'analytics');
96
+ });
97
+ });
@@ -0,0 +1,38 @@
1
+ import winston from "winston"
2
+ import { LOG_LEVELS } from "@c-rex/constants";
3
+ import { MatomoTransport } from "../transports/matomo"
4
+ import { GraylogTransport } from "../transports/graylog"
5
+ import { LogLevelType, LogCategoriesType } from "@c-rex/types";
6
+ import { ConfigInterface } from "@c-rex/interfaces";
7
+
8
+ const logger = (config: ConfigInterface) => {
9
+ return winston.createLogger({
10
+ levels: LOG_LEVELS,
11
+ transports: [
12
+ new winston.transports.Console({
13
+ level: config.logs.console.minimumLevel as string,
14
+ silent: config.logs.console.silent,
15
+ }),
16
+ new MatomoTransport({
17
+ level: config.logs.console.minimumLevel,
18
+ silent: config.logs.console.silent,
19
+ }),
20
+ new GraylogTransport({
21
+ level: config.logs.graylog.minimumLevel,
22
+ silent: config.logs.graylog.silent,
23
+ }),
24
+ ],
25
+ });
26
+ }
27
+
28
+ export class LoggerServer {
29
+ logger: winston.Logger;
30
+
31
+ constructor(config: ConfigInterface) {
32
+ this.logger = logger(config);
33
+ }
34
+
35
+ log(level: LogLevelType, message: string, category?: LogCategoriesType) {
36
+ this.logger.log(level, message, category);
37
+ }
38
+ }
@@ -0,0 +1,62 @@
1
+ import axios, { AxiosResponse, Method, AxiosInstance } from "axios";
2
+ import { API } from "@c-rex/constants";
3
+ import { CrexLogger } from "./logger";
4
+
5
+ interface APIGenericResponse<T> extends AxiosResponse {
6
+ data: T;
7
+ statusCode: number;
8
+ }
9
+
10
+ interface CallParams {
11
+ url: string;
12
+ method: Method;
13
+ body?: any;
14
+ headers?: any;
15
+ params?: any;
16
+ }
17
+
18
+ export class CrexApi {
19
+ private apiClient: AxiosInstance;
20
+ private logger: CrexLogger;
21
+
22
+ public constructor(baseUrl: string, logger: CrexLogger) {
23
+ this.apiClient = axios.create({
24
+ baseURL: baseUrl,
25
+ headers: {
26
+ "content-Type": "application/json",
27
+ },
28
+ });
29
+ this.logger = logger;
30
+ }
31
+
32
+ async execute<T>({
33
+ url,
34
+ method,
35
+ params,
36
+ body,
37
+ headers,
38
+ }: CallParams): Promise<any> {
39
+ let response: APIGenericResponse<T> | undefined = undefined;
40
+
41
+ for (let retry = 0; retry < API.MAX_RETRY; retry++) {
42
+ try {
43
+ response = await this.apiClient.request({
44
+ url,
45
+ method,
46
+ data: body,
47
+ params,
48
+ headers,
49
+ });
50
+ } catch (error) {
51
+ this.logger.log("error", `API.execute error when request ${url}. Error: ${error}`);
52
+ throw error;
53
+ }
54
+ }
55
+
56
+ if (response) {
57
+ return response.data
58
+ }
59
+
60
+ throw new Error("API.execute error: Failed to retrieve a valid response");
61
+ }
62
+ }