linkedin-api-voyager 1.3.0

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/README.md ADDED
@@ -0,0 +1,197 @@
1
+ # LinkedIn API Voyager
2
+
3
+ Uma biblioteca TypeScript para interagir com a API interna do LinkedIn (Voyager) de forma simples e eficiente.
4
+
5
+ ## 🚀 Instalação
6
+
7
+ ```bash
8
+ npm install linkedin-api-voyager
9
+ # ou
10
+ yarn add linkedin-api-voyager
11
+ ```
12
+
13
+ ## 📋 Pré-requisitos
14
+
15
+ Você precisa ter cookies válidos do LinkedIn para usar esta biblioteca. Os cookies devem estar salvos em um arquivo `linkedin_cookies.json` na raiz do seu projeto.
16
+
17
+ ### Formato dos cookies:
18
+
19
+ ```json
20
+ {
21
+ "JSESSIONID": "seu_token_aqui",
22
+ "li_at": "seu_token_aqui",
23
+ "timestamp": "1234567890"
24
+ }
25
+ ```
26
+
27
+ ## 🔧 Uso
28
+
29
+ ### Configuração inicial
30
+
31
+ ```typescript
32
+ import {
33
+ getProfile,
34
+ getProfissionalExperiences,
35
+ getCompany,
36
+ search,
37
+ } from "linkedin-api-voyager";
38
+ ```
39
+
40
+ ### 👤 Perfil de usuário
41
+
42
+ ```typescript
43
+ // Obter perfil completo
44
+ const profile = await getProfile("username-do-linkedin");
45
+
46
+ // Obter experiências profissionais (ordenadas do mais recente ao mais antigo)
47
+ const experiences = await getProfissionalExperiences("username-do-linkedin");
48
+ ```
49
+
50
+ ### 🏢 Informações de empresa
51
+
52
+ ```typescript
53
+ // Obter dados completos da empresa
54
+ const company = await getCompany("nome-universal-da-empresa");
55
+ ```
56
+
57
+ ### 🔍 Busca
58
+
59
+ ```typescript
60
+ // Busca geral
61
+ const results = await search(
62
+ {
63
+ keywords: "desenvolvedor javascript",
64
+ filters: "List(resultType->PEOPLE)",
65
+ },
66
+ { limit: 50 }
67
+ );
68
+
69
+ // Busca com parâmetros personalizados
70
+ const customSearch = await search({
71
+ q: "all",
72
+ keywords: "react developer",
73
+ filters: "List(resultType->PEOPLE,locationFilter->br:0)",
74
+ start: 0,
75
+ count: "25",
76
+ });
77
+ ```
78
+
79
+ ### 💬 Comentários de posts
80
+
81
+ ```typescript
82
+ import { getCommentsByPostUrl } from "linkedin-api-voyager";
83
+
84
+ // Obter todos os comentários de um post
85
+ const comments = await getCommentsByPostUrl(
86
+ "https://www.linkedin.com/feed/update/urn:li:activity-1234567890/",
87
+ 0, // início
88
+ 50 // limite por página
89
+ );
90
+ ```
91
+
92
+ ## 📊 Estrutura de dados
93
+
94
+ ### Perfil
95
+
96
+ ```typescript
97
+ {
98
+ publicIdentifier: string;
99
+ firstName: string;
100
+ lastName: string;
101
+ fullName: string;
102
+ profilePicture: string;
103
+ backgroundPicture: string;
104
+ location: {
105
+ country: string;
106
+ city: string;
107
+ }
108
+ industry: string;
109
+ headline: string;
110
+ summary: string;
111
+ // ... outros campos
112
+ }
113
+ ```
114
+
115
+ ### Experiências profissionais
116
+
117
+ ```typescript
118
+ [
119
+ {
120
+ id: string;
121
+ title: string;
122
+ companyName: string;
123
+ companyUrn: string;
124
+ universalName: string; // Nome universal da empresa
125
+ description: string;
126
+ location: string;
127
+ startDate: { year: number; month: number };
128
+ endDate: { year: number; month: number } | null; // null = ativo
129
+ // ... outros campos
130
+ }
131
+ ]
132
+ ```
133
+
134
+ ### Empresa
135
+
136
+ ```typescript
137
+ {
138
+ id: string;
139
+ name: string;
140
+ description: string;
141
+ username: string;
142
+ companyPageUrl: string;
143
+ staffCount: number;
144
+ url: string;
145
+ location: string;
146
+ followerCount: number;
147
+ logo: object;
148
+ // ... outros campos
149
+ }
150
+ ```
151
+
152
+ ## 🛠️ Funcionalidades avançadas
153
+
154
+ ### Extração de campos personalizados
155
+
156
+ ```typescript
157
+ import { extractFields, extractFieldsFromIncluded } from "linkedin-api-voyager";
158
+
159
+ // Mapear campos específicos
160
+ const fieldsMap = {
161
+ nome: "firstName",
162
+ empresa: "company.name",
163
+ cargo: "title",
164
+ };
165
+
166
+ const dadosMapeados = extractFields(dados, fieldsMap);
167
+ ```
168
+
169
+ ### Resolução automática de referências
170
+
171
+ A biblioteca resolve automaticamente referências URN aninhadas, permitindo acesso direto a dados relacionados sem necessidade de mapeamento manual.
172
+
173
+ ## ⚠️ Limitações e considerações
174
+
175
+ - Esta biblioteca usa a API interna do LinkedIn (Voyager)
176
+ - Requer cookies válidos de uma sessão autenticada
177
+ - Respeite os termos de uso do LinkedIn
178
+ - Use com moderação para evitar bloqueios
179
+ - Não é uma API oficial do LinkedIn
180
+
181
+ ## 🔒 Segurança
182
+
183
+ - Mantenha seus cookies seguros
184
+ - Não compartilhe o arquivo `linkedin_cookies.json`
185
+ - O arquivo já está incluído no `.gitignore`
186
+
187
+ ## 📝 Licença
188
+
189
+ MIT
190
+
191
+ ## 🤝 Contribuição
192
+
193
+ Contribuições são bem-vindas! Por favor, abra uma issue ou pull request.
194
+
195
+ ## 📞 Suporte
196
+
197
+ Para dúvidas ou problemas, abra uma issue no repositório.
package/package.json ADDED
@@ -0,0 +1,22 @@
1
+ {
2
+ "name": "linkedin-api-voyager",
3
+ "version": "1.3.0",
4
+ "license": "MIT",
5
+ "scripts": {
6
+ "build": "tsc",
7
+ "prepare": "npm run build",
8
+ "dev": "nodemon src/index.ts",
9
+ "publish": "yarn build && npm publish"
10
+ },
11
+ "devDependencies": {
12
+ "@types/fs-extra": "^11.0.4",
13
+ "@types/node": "^22.13.1",
14
+ "nodemon": "^3.1.9",
15
+ "ts-node": "^10.9.2",
16
+ "typescript": "^5.7.3"
17
+ },
18
+ "dependencies": {
19
+ "axios": "^1.11.0",
20
+ "fs-extra": "^11.3.1"
21
+ }
22
+ }
package/src/account.ts ADDED
@@ -0,0 +1,116 @@
1
+ import { fetchData } from "./config";
2
+ import {
3
+ extractDataWithReferences,
4
+ extractFields,
5
+ extractFieldsFromIncluded,
6
+ mergeExtraFields,
7
+ } from "./utils";
8
+
9
+ export const getProfile = async (identifier: string) => {
10
+ const response = await fetchData(
11
+ `/identity/profiles/${identifier}/profileView`
12
+ );
13
+
14
+ const data = response.data;
15
+ const dataResult: any[] = response?.included;
16
+
17
+ const getEntityByUrn = (urn: string) =>
18
+ dataResult.find((item) => item.entityUrn === urn);
19
+
20
+ const keyProfile = getEntityByUrn(data?.["*profile"]);
21
+ if (!keyProfile) throw new Error("Key profile not found");
22
+
23
+ const miniProfile = getEntityByUrn(keyProfile?.["*miniProfile"]);
24
+ if (!miniProfile) throw new Error("Mini profile not found");
25
+
26
+ const profile = {
27
+ // id_urn: keyProfile.entityUrn?.split("urn:li:fs_profile:")[1] || null,
28
+ publicIdentifier: miniProfile?.publicIdentifier || null,
29
+ firstName: keyProfile.firstName || null,
30
+ lastName: keyProfile.lastName || null,
31
+ fullName: `${keyProfile.firstName || ""} ${keyProfile.lastName || ""}`,
32
+ birthDate: keyProfile.birthDate
33
+ ? JSON.stringify({
34
+ month: keyProfile.birthDate.month,
35
+ day: keyProfile.birthDate.day,
36
+ })
37
+ : null,
38
+ profilePicture: miniProfile.picture
39
+ ? `${miniProfile.picture.rootUrl}${
40
+ miniProfile.picture.artifacts[
41
+ miniProfile.picture.artifacts.length - 1
42
+ ]?.fileIdentifyingUrlPathSegment
43
+ }`
44
+ : null,
45
+ backgroundPicture: miniProfile.backgroundImage
46
+ ? `${miniProfile.backgroundImage.rootUrl}${
47
+ miniProfile.backgroundImage.artifacts[
48
+ miniProfile.backgroundImage.artifacts.length - 1
49
+ ]?.fileIdentifyingUrlPathSegment
50
+ }`
51
+ : null,
52
+ location: {
53
+ country: keyProfile.locationName || null,
54
+ city: keyProfile.geoLocationName || null,
55
+ },
56
+ address: keyProfile.address || null,
57
+ industry: keyProfile.industryName || null,
58
+ headline: keyProfile.headline || null,
59
+ summary: keyProfile.summary || null,
60
+ };
61
+
62
+ return profile;
63
+ };
64
+
65
+ export const getProfissionalExperiences = async (identifier: string) => {
66
+ const response = await fetchData(
67
+ `/identity/profiles/${identifier}/positions`
68
+ );
69
+
70
+ let { data, included } = response;
71
+ const elements = data["*elements"] as string[];
72
+
73
+ // Usar a nova função para resolver referências automaticamente
74
+ const dataExperiences = extractDataWithReferences(elements, included);
75
+
76
+ // Extrair campos específicos do included
77
+ const extraFields = extractFieldsFromIncluded(included, ["universalName"]);
78
+
79
+ // Mapeamento de campos
80
+ const fieldsMap = {
81
+ id: "entityUrn",
82
+ title: "title",
83
+ companyName: "company.miniCompany.name",
84
+ companyUrn: "companyUrn",
85
+ companyEmployeeCount: "company.employeeCountRange",
86
+ companyIndustries: "company.miniCompany.industries",
87
+ description: "description",
88
+ location: "locationName",
89
+ geoLocation: "geoLocationName",
90
+ timePeriod: "timePeriod",
91
+ startDate: "timePeriod.startDate",
92
+ endDate: "timePeriod.endDate",
93
+ };
94
+
95
+ // Aplicar mapeamento aos dados resolvidos
96
+ const mappedExperiences = extractFields(dataExperiences, fieldsMap);
97
+
98
+ // Associar campos extras
99
+ const experiencesWithExtras = mergeExtraFields(
100
+ mappedExperiences,
101
+ extraFields,
102
+ "companyUrn"
103
+ );
104
+
105
+ // Ordenar: sem endDate (ativo) primeiro, depois do mais recente ao mais antigo
106
+ return experiencesWithExtras.sort((a, b) => {
107
+ if (!a.endDate && b.endDate) return -1;
108
+ if (a.endDate && !b.endDate) return 1;
109
+ if (!a.endDate && !b.endDate) return 0;
110
+
111
+ const yearDiff = (b.endDate.year || 0) - (a.endDate.year || 0);
112
+ if (yearDiff !== 0) return yearDiff;
113
+
114
+ return (b.endDate.month || 0) - (a.endDate.month || 0);
115
+ });
116
+ };
package/src/company.ts ADDED
@@ -0,0 +1,33 @@
1
+ import { fetchData } from "./config";
2
+ import { extractDataWithReferences, extractFields } from "./utils";
3
+
4
+ export const getCompany = async (identifier: string) => {
5
+ const response = await fetchData(
6
+ `/organization/companies?decorationId=com.linkedin.voyager.deco.organization.web.WebFullCompanyMain-12&q=universalName&universalName=${identifier}`
7
+ );
8
+
9
+ const data = extractDataWithReferences(
10
+ response.data["*elements"],
11
+ response.included
12
+ );
13
+
14
+ const fieldsMap = {
15
+ id: "entityUrn",
16
+ name: "name",
17
+ description: "description",
18
+ username: "universalName",
19
+ companyPageUrl: "companyPageUrl",
20
+ staffCount: "staffCount",
21
+ url: "url",
22
+ companyIndustries: "*companyIndustries[0].localizedName",
23
+ location: "locationName",
24
+ jobSearchPageUrl: "jobSearchPageUrl",
25
+ phone: "phone",
26
+ followerCount: "followingInfo.followerCount",
27
+ backgroundCoverImage: "backgroundCoverImage.image",
28
+ logo: "logo.image",
29
+ permissions: "permissions",
30
+ };
31
+
32
+ return extractFields(data, fieldsMap)[0];
33
+ };
package/src/config.ts ADDED
@@ -0,0 +1,109 @@
1
+ import * as path from "path";
2
+ import * as os from "os";
3
+ import * as fs from "fs-extra";
4
+ import axios from "axios";
5
+
6
+ export const COOKIE_FILE_PATH = "linkedin_cookies.json";
7
+
8
+ export const API_BASE_URL = "https://www.linkedin.com/voyager/api";
9
+ export const AUTH_BASE_URL = "https://www.linkedin.com";
10
+
11
+ // Interface para os cookies
12
+ interface LinkedInCookies {
13
+ JSESSIONID: string;
14
+ li_at: string;
15
+ timestamp: number;
16
+ }
17
+
18
+ // Função para salvar cookies no arquivo JSON
19
+ export const saveCookies = async (
20
+ JSESSIONID: string,
21
+ li_at: string
22
+ ): Promise<void> => {
23
+ try {
24
+ const cookies: LinkedInCookies = {
25
+ JSESSIONID,
26
+ li_at,
27
+ timestamp: Date.now(),
28
+ };
29
+
30
+ await fs.ensureFile(COOKIE_FILE_PATH);
31
+ await fs.writeJson(COOKIE_FILE_PATH, cookies, { spaces: 2 });
32
+ console.log(`Cookies salvos em: ${COOKIE_FILE_PATH}`);
33
+ } catch (error) {
34
+ console.error("Erro ao salvar cookies:", error);
35
+ throw error;
36
+ }
37
+ };
38
+
39
+ // Função para carregar cookies do arquivo JSON
40
+ export const loadCookies = async (): Promise<LinkedInCookies | null> => {
41
+ try {
42
+ const exists = await fs.pathExists(COOKIE_FILE_PATH);
43
+ if (!exists) {
44
+ console.log("Arquivo de cookies não encontrado");
45
+ return null;
46
+ }
47
+
48
+ const cookies = await fs.readJson(COOKIE_FILE_PATH);
49
+
50
+ // Verificar se os cookies têm a estrutura esperada
51
+ if (!cookies.JSESSIONID || !cookies.li_at) {
52
+ console.log("Cookies inválidos encontrados no arquivo");
53
+ return null;
54
+ }
55
+
56
+ console.log(`Cookies carregados de: ${COOKIE_FILE_PATH}`);
57
+ return cookies;
58
+ } catch (error) {
59
+ console.error("Erro ao carregar cookies:", error);
60
+ return null;
61
+ }
62
+ };
63
+
64
+ // Função para criar cliente com cookies automáticos
65
+ export const Client = async (providedCookies?: {
66
+ JSESSIONID: string;
67
+ li_at: string;
68
+ }): Promise<ReturnType<typeof api>> => {
69
+ let cookiesToUse: { JSESSIONID: string; li_at: string };
70
+ const savedCookies = await loadCookies();
71
+
72
+ if (savedCookies) {
73
+ cookiesToUse = {
74
+ JSESSIONID: savedCookies.JSESSIONID,
75
+ li_at: savedCookies.li_at,
76
+ };
77
+ } else {
78
+ if (providedCookies) {
79
+ await saveCookies(providedCookies.JSESSIONID, providedCookies.li_at);
80
+ cookiesToUse = providedCookies;
81
+ } else {
82
+ throw new Error("Nenhum cookie válido fornecido");
83
+ }
84
+ }
85
+
86
+ return api({
87
+ JSESSIONID: parseInt(cookiesToUse.JSESSIONID),
88
+ li_at: cookiesToUse.li_at,
89
+ });
90
+ };
91
+
92
+ const api = ({ JSESSIONID, li_at }: { li_at: string; JSESSIONID: number }) => {
93
+ return axios.create({
94
+ baseURL: API_BASE_URL,
95
+ headers: {
96
+ "accept-language":
97
+ "pt-BR,pt;q=0.9,fr-FR;q=0.8,fr;q=0.7,en-US;q=0.6,en;q=0.5",
98
+ accept: "application/vnd.linkedin.normalized+json+2.1",
99
+ cookie: `li_at=${li_at}; JSESSIONID="ajax:${JSESSIONID}"`,
100
+ "csrf-token": `ajax:${JSESSIONID}`,
101
+ },
102
+ });
103
+ };
104
+
105
+ export const fetchData = async (endpoint: string) => {
106
+ const api = await Client();
107
+ const response = await api.get(endpoint);
108
+ return response.data;
109
+ };
package/src/index.ts ADDED
@@ -0,0 +1,6 @@
1
+ export * from "./account";
2
+ export * from "./company";
3
+ export * from "./posts";
4
+ export * from "./search";
5
+ export * from "./utils";
6
+ export * from "./config";
File without changes
package/src/posts.ts ADDED
@@ -0,0 +1,71 @@
1
+ import { fetchData } from "./config";
2
+ import { extractFields } from "./utils";
3
+
4
+ export const getCommentsByPostUrl = async (
5
+ url: string,
6
+ start: number = 0,
7
+ limit: number = 50,
8
+ accumulatedComments: any[] = []
9
+ ): Promise<any[]> => {
10
+ const postID = url.match(/activity-(\d+)/)?.[1];
11
+
12
+ const response = await fetchData(
13
+ `/graphql?includeWebMetadata=false&queryId=voyagerSocialDashComments.95ed44bc87596acce7c460c70934d0ff&variables=(count:${limit},start:${start},numReplies:1,socialDetailUrn:urn%3Ali%3Afsd_socialDetail%3A%28urn%3Ali%3Aactivity%${postID}%2Curn%3Ali%3Aactivity%3A${postID}%2Curn%3Ali%3AhighlightedReply%3A-%29,sortOrder:RELEVANCE)`
14
+ );
15
+
16
+ const elements = response.data?.data?.socialDashCommentsBySocialDetail?.[
17
+ "*elements"
18
+ ] as string[];
19
+
20
+ // Se não há elementos, retorna os comentários acumulados
21
+ if (!elements || elements.length === 0) {
22
+ // console.log(
23
+ // "✅ Busca finalizada. Total de comentários:",
24
+ // accumulatedComments.length
25
+ // );
26
+ return accumulatedComments;
27
+ }
28
+
29
+ const data =
30
+ response.included?.filter((item: any) =>
31
+ elements.includes(item.entityUrn)
32
+ ) || [];
33
+
34
+ // Mapeamento melhorado dos campos
35
+ const fieldsMap = {
36
+ id: "entityUrn",
37
+ createdAt: "createdAt",
38
+ isAuthor: "commenter.author",
39
+ name: "commenter.title.text",
40
+ headline: "commenter.subtitle",
41
+ profileUrl: "commenter.navigationUrl",
42
+ comment: "commentary.text",
43
+ permalink: "permalink",
44
+ image:
45
+ "commenter.image.attributes.0.detailData.nonEntityProfilePicture.vectorImage",
46
+ };
47
+
48
+ const currentComments = extractFields(data, fieldsMap);
49
+ const allComments = [...accumulatedComments, ...currentComments];
50
+
51
+ // console.log(
52
+ // `🔍 Encontrados ${elements.length} comentários (Total: ${allComments.length})`
53
+ // );
54
+
55
+ // Continua a busca se há mais elementos
56
+ if (elements.length > 0) {
57
+ return await getCommentsByPostUrl(
58
+ url,
59
+ start + elements.length,
60
+ limit,
61
+ allComments
62
+ );
63
+ }
64
+
65
+ // Se retornou menos que o limite, chegou ao fim
66
+ return allComments;
67
+ };
68
+
69
+ export const getPosts = async () => {
70
+ return [];
71
+ };
package/src/search.ts ADDED
@@ -0,0 +1,183 @@
1
+ import { Client as ClientState } from "./config";
2
+
3
+ // Constantes
4
+ const MAX_SEARCH_COUNT = 25;
5
+ const MAX_REPEATED_REQUESTS = 40;
6
+
7
+ // Interfaces
8
+ export interface SearchParams {
9
+ count?: string;
10
+ filters?: string;
11
+ origin?: string;
12
+ q?: string;
13
+ start?: number;
14
+ queryContext?: string;
15
+ [key: string]: any; // Para permitir parâmetros adicionais
16
+ }
17
+
18
+ export interface SearchElement {
19
+ [key: string]: any; // Estrutura flexível para elementos de busca
20
+ }
21
+
22
+ export interface SearchDataElement {
23
+ elements: SearchElement[];
24
+ extendedElements?: SearchElement[];
25
+ }
26
+
27
+ export interface SearchResponse {
28
+ data: {
29
+ elements: SearchDataElement[];
30
+ };
31
+ }
32
+
33
+ export interface SearchOptions {
34
+ limit?: number;
35
+ results?: SearchElement[];
36
+ }
37
+
38
+ // Função utilitária para criar query string
39
+ const createQueryString = (params: Record<string, any>): string => {
40
+ return Object.entries(params)
41
+ .map(
42
+ ([key, value]) =>
43
+ `${encodeURIComponent(key)}=${encodeURIComponent(value)}`
44
+ )
45
+ .join("&");
46
+ };
47
+
48
+ // Função utilitária para fazer fetch (assumindo que existe uma função _fetch no client)
49
+ const fetchData = async (endpoint: string): Promise<SearchResponse> => {
50
+ const api = await ClientState();
51
+ const response = await api.get(endpoint);
52
+ return response.data;
53
+ };
54
+
55
+ // Função principal de busca
56
+ export const search = async (
57
+ params: SearchParams,
58
+ options: SearchOptions = {}
59
+ ): Promise<SearchElement[]> => {
60
+ const { limit, results = [] } = options;
61
+
62
+ // Determinar o count baseado no limite
63
+ const count = limit && limit <= MAX_SEARCH_COUNT ? limit : MAX_SEARCH_COUNT;
64
+
65
+ // Parâmetros padrão
66
+ const defaultParams: SearchParams = {
67
+ count: count.toString(),
68
+ filters: "List()",
69
+ origin: "GLOBAL_SEARCH_HEADER",
70
+ q: "all",
71
+ start: results.length,
72
+ queryContext:
73
+ "List(spellCorrectionEnabled->true,relatedSearchesEnabled->true,kcardTypes->PROFILE|COMPANY)",
74
+ };
75
+
76
+ // Mesclar parâmetros padrão com os fornecidos
77
+ const mergedParams = { ...defaultParams, ...params };
78
+
79
+ // Fazer a requisição
80
+ const endpoint = `/search/blended?${createQueryString(mergedParams)}`;
81
+
82
+ const response = await fetchData(endpoint);
83
+
84
+ // Processar os dados da resposta
85
+ const newElements: SearchElement[] = [];
86
+
87
+ if (response.data && response.data.elements) {
88
+ for (let i = 0; i < response.data.elements.length; i++) {
89
+ if (response.data.elements[i].elements) {
90
+ newElements.push(...response.data.elements[i].elements);
91
+ }
92
+ // Comentário: não tenho certeza do que extendedElements geralmente se refere
93
+ // - busca por palavra-chave retorna um único trabalho?
94
+ // if (response.data.elements[i].extendedElements) {
95
+ // newElements.push(...response.data.elements[i].extendedElements);
96
+ // }
97
+ }
98
+ }
99
+
100
+ // Adicionar novos elementos aos resultados
101
+ const updatedResults = [...results, ...newElements];
102
+
103
+ // Sempre cortar os resultados, não importa o que a requisição retorna
104
+ const trimmedResults = limit
105
+ ? updatedResults.slice(0, limit)
106
+ : updatedResults;
107
+
108
+ // Caso base da recursão
109
+ const shouldStop =
110
+ (limit !== undefined &&
111
+ (trimmedResults.length >= limit || // se nossos resultados excedem o limite definido
112
+ trimmedResults.length / count >= MAX_REPEATED_REQUESTS)) ||
113
+ newElements.length === 0;
114
+
115
+ if (shouldStop) {
116
+ return trimmedResults;
117
+ }
118
+
119
+ // Chamada recursiva
120
+ return search(params, {
121
+ limit,
122
+ results: trimmedResults,
123
+ });
124
+ };
125
+
126
+ // Função auxiliar para busca simples (não recursiva)
127
+ // export const searchSingle = async (
128
+ // client: ClientState,
129
+ // params: SearchParams
130
+ // ): Promise<SearchElement[]> => {
131
+ // return search(client, params, { limit: MAX_SEARCH_COUNT });
132
+ // };
133
+
134
+ // // Função para busca com limite específico
135
+ // export const searchWithLimit = async (
136
+ // client: ClientState,
137
+ // params: SearchParams,
138
+ // limit: number
139
+ // ): Promise<SearchElement[]> => {
140
+ // return search(client, params, { limit });
141
+ // };
142
+
143
+ // // Função para busca de pessoas
144
+ // export const searchPeople = async (
145
+ // client: ClientState,
146
+ // query: string,
147
+ // limit?: number
148
+ // ): Promise<SearchElement[]> => {
149
+ // const params: SearchParams = {
150
+ // keywords: query,
151
+ // filters: "List(resultType->PEOPLE)",
152
+ // };
153
+
154
+ // return search(client, params, { limit });
155
+ // };
156
+
157
+ // // Função para busca de empresas
158
+ // export const searchCompanies = async (
159
+ // client: ClientState,
160
+ // query: string,
161
+ // limit?: number
162
+ // ): Promise<SearchElement[]> => {
163
+ // const params: SearchParams = {
164
+ // keywords: query,
165
+ // filters: "List(resultType->COMPANIES)",
166
+ // };
167
+
168
+ // return search(client, params, { limit });
169
+ // };
170
+
171
+ // // Função para busca de empregos
172
+ // export const searchJobs = async (
173
+ // client: ClientState,
174
+ // query: string,
175
+ // limit?: number
176
+ // ): Promise<SearchElement[]> => {
177
+ // const params: SearchParams = {
178
+ // keywords: query,
179
+ // filters: "List(resultType->JOBS)",
180
+ // };
181
+
182
+ // return search(client, params, { limit });
183
+ // };
package/src/utils.ts ADDED
@@ -0,0 +1,213 @@
1
+ export function filterKeys(obj: any, keysToKeep: string[]) {
2
+ const filteredObject: any = {};
3
+ keysToKeep.forEach((key) => {
4
+ if (obj.hasOwnProperty(key)) {
5
+ filteredObject[key] = obj[key];
6
+ }
7
+ });
8
+ return filteredObject;
9
+ }
10
+
11
+ export function filterOutKeys(obj: any, keysToIgnore: string[]) {
12
+ const filteredObject: any = {};
13
+ Object.keys(obj).forEach((key) => {
14
+ if (!keysToIgnore.includes(key)) {
15
+ filteredObject[key] = obj[key];
16
+ }
17
+ });
18
+ return filteredObject;
19
+ }
20
+
21
+ // Nova função para extrair valores de caminhos aninhados
22
+ export function getNestedValue(obj: any, path: string): any {
23
+ return path.split('.').reduce((current, key) => {
24
+ // Lidar com arrays como attributes[0]
25
+ if (key.includes('[') && key.includes(']')) {
26
+ const [arrayKey, indexStr] = key.split('[');
27
+ const index = parseInt(indexStr.replace(']', ''));
28
+ return current?.[arrayKey]?.[index];
29
+ }
30
+ return current?.[key];
31
+ }, obj);
32
+ }
33
+
34
+ // Nova função melhorada para filtrar com caminhos aninhados
35
+ export function extractFields(data: any[], fieldsMap: Record<string, string>): any[] {
36
+ return data.map(item => {
37
+ const extracted: any = {};
38
+
39
+ Object.entries(fieldsMap).forEach(([newKey, path]) => {
40
+ const value = getNestedValue(item, path);
41
+ if (value !== undefined) {
42
+ extracted[newKey] = value;
43
+ }
44
+ });
45
+
46
+ return extracted;
47
+ });
48
+ }
49
+
50
+ // Função para debug - mostra a estrutura do objeto
51
+ export function debugObjectStructure(obj: any, maxDepth: number = 3, currentDepth: number = 0): void {
52
+ if (currentDepth >= maxDepth) return;
53
+
54
+ const indent = ' '.repeat(currentDepth);
55
+
56
+ if (Array.isArray(obj)) {
57
+ console.log(`${indent}Array[${obj.length}]:`);
58
+ if (obj.length > 0) {
59
+ console.log(`${indent} [0]:`);
60
+ debugObjectStructure(obj[0], maxDepth, currentDepth + 2);
61
+ }
62
+ } else if (obj && typeof obj === 'object') {
63
+ Object.keys(obj).slice(0, 10).forEach(key => {
64
+ const value = obj[key];
65
+ if (typeof value === 'object' && value !== null) {
66
+ console.log(`${indent}${key}:`);
67
+ debugObjectStructure(value, maxDepth, currentDepth + 1);
68
+ } else {
69
+ console.log(`${indent}${key}: ${typeof value} = ${String(value).slice(0, 50)}...`);
70
+ }
71
+ });
72
+ }
73
+ }
74
+
75
+ // Função para resolver referências URN dinamicamente
76
+ export function resolveReferences(data: any, included: any[]): any {
77
+ if (!data || !included) return data;
78
+
79
+ // Criar um mapa de URN para acesso rápido
80
+ const urnMap = new Map();
81
+ included.forEach(item => {
82
+ if (item.entityUrn) {
83
+ urnMap.set(item.entityUrn, item);
84
+ }
85
+ });
86
+
87
+ // Função recursiva para resolver referências
88
+ function resolveObject(obj: any): any {
89
+ if (Array.isArray(obj)) {
90
+ return obj.map(item => resolveObject(item));
91
+ }
92
+
93
+ if (obj && typeof obj === 'object') {
94
+ const resolved: any = {};
95
+
96
+ Object.entries(obj).forEach(([key, value]) => {
97
+ // Detectar chaves que começam com * (referências URN)
98
+ if (key.startsWith('*') && typeof value === 'string') {
99
+ const referencedData = urnMap.get(value);
100
+ if (referencedData) {
101
+ // Remover o * e usar como chave
102
+ const cleanKey = key.substring(1);
103
+ resolved[cleanKey] = resolveObject(referencedData);
104
+ } else {
105
+ resolved[key] = value; // Manter original se não encontrar
106
+ }
107
+ }
108
+ // Detectar arrays de URNs
109
+ else if (Array.isArray(value) && value.length > 0 && typeof value[0] === 'string' && value[0].startsWith('urn:li:')) {
110
+ const resolvedArray = value.map(urn => {
111
+ const referencedData = urnMap.get(urn);
112
+ return referencedData ? resolveObject(referencedData) : urn;
113
+ }).filter(item => item !== null);
114
+ resolved[key] = resolvedArray;
115
+ }
116
+ // Recursão para objetos aninhados
117
+ else if (value && typeof value === 'object') {
118
+ resolved[key] = resolveObject(value);
119
+ }
120
+ // Valores primitivos
121
+ else {
122
+ resolved[key] = value;
123
+ }
124
+ });
125
+
126
+ return resolved;
127
+ }
128
+
129
+ return obj;
130
+ }
131
+
132
+ return resolveObject(data);
133
+ }
134
+
135
+ // Função para extrair dados com resolução automática de referências
136
+ export function extractDataWithReferences(
137
+ elements: string[],
138
+ included: any[],
139
+ fieldsMap?: Record<string, string>
140
+ ): any[] {
141
+ // Filtrar dados pelos elementos
142
+ const filteredData = included.filter(item =>
143
+ elements.includes(item.entityUrn)
144
+ );
145
+
146
+ // Resolver todas as referências
147
+ const resolvedData = filteredData.map(item =>
148
+ resolveReferences(item, included)
149
+ );
150
+
151
+ // Se há mapeamento de campos, aplicar
152
+ if (fieldsMap) {
153
+ return extractFields(resolvedData, fieldsMap);
154
+ }
155
+
156
+ return resolvedData;
157
+ }
158
+
159
+ // Função para debug de estrutura com referências resolvidas
160
+ export function debugResolvedStructure(
161
+ elements: string[],
162
+ included: any[],
163
+ maxDepth: number = 2
164
+ ): void {
165
+ console.log('🔍 Estrutura dos dados com referências resolvidas:');
166
+ const resolved = extractDataWithReferences(elements, included);
167
+
168
+ if (resolved.length > 0) {
169
+ console.log(`📊 Total de itens: ${resolved.length}`);
170
+ console.log('📋 Estrutura do primeiro item:');
171
+ debugObjectStructure(resolved[0], maxDepth);
172
+ }
173
+ }
174
+
175
+ // Função para extrair campos específicos de todos os objetos no included
176
+ export function extractFieldsFromIncluded(
177
+ included: any[],
178
+ fields: string[]
179
+ ): Record<string, any>[] {
180
+ return included
181
+ .filter(item => fields.some(field => item[field] !== undefined))
182
+ .map(item => {
183
+ const extracted: any = { entityUrn: item.entityUrn };
184
+
185
+ fields.forEach(field => {
186
+ if (item[field] !== undefined) {
187
+ extracted[field] = item[field];
188
+ }
189
+ });
190
+
191
+ return extracted;
192
+ });
193
+ }
194
+
195
+ // Função para associar dados extras aos dados principais
196
+ export function mergeExtraFields(
197
+ mainData: any[],
198
+ extraData: Record<string, any>[],
199
+ matchKey: string = 'companyUrn'
200
+ ): any[] {
201
+ return mainData.map(item => {
202
+ const extraItem = extraData.find(extra =>
203
+ item[matchKey] && extra.entityUrn === item[matchKey]
204
+ );
205
+
206
+ if (extraItem) {
207
+ const { entityUrn, ...extraFields } = extraItem;
208
+ return { ...item, ...extraFields };
209
+ }
210
+
211
+ return item;
212
+ });
213
+ }
package/tsconfig.json ADDED
@@ -0,0 +1,10 @@
1
+ {
2
+ "compilerOptions": {
3
+ "target": "ES6",
4
+ "module": "CommonJS",
5
+ "declaration": true,
6
+ "outDir": "./lib",
7
+ "strict": true
8
+ },
9
+ "include": ["src/**/*"]
10
+ }