@chronicle.app/linkedin 0.4.0 → 0.5.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 +5 -4
- package/dist/connectors/LinkedInTransformer.js +3 -2
- package/dist/connectors/archive/LinkedInArchiveConnectionsExtractor.d.ts +5 -1
- package/dist/connectors/archive/LinkedInArchiveConnectionsExtractor.js +21 -12
- package/dist/connectors/archive/LinkedInArchiveEducationExtractor.d.ts +5 -1
- package/dist/connectors/archive/LinkedInArchiveEducationExtractor.js +21 -13
- package/dist/connectors/archive/LinkedInArchiveEndorsementsExtractor.d.ts +5 -1
- package/dist/connectors/archive/LinkedInArchiveEndorsementsExtractor.js +12 -4
- package/dist/connectors/archive/LinkedInArchiveExtractor.d.ts +14 -6
- package/dist/connectors/archive/LinkedInArchiveExtractor.js +39 -33
- package/dist/connectors/archive/LinkedInArchiveFollowsExtractor.d.ts +5 -1
- package/dist/connectors/archive/LinkedInArchiveFollowsExtractor.js +17 -9
- package/dist/connectors/archive/LinkedInArchiveLearningExtractor.d.ts +5 -1
- package/dist/connectors/archive/LinkedInArchiveLearningExtractor.js +23 -11
- package/dist/connectors/archive/LinkedInArchiveMessagesExtractor.d.ts +5 -1
- package/dist/connectors/archive/LinkedInArchiveMessagesExtractor.js +28 -12
- package/dist/connectors/archive/LinkedInArchivePositionsExtractor.d.ts +5 -1
- package/dist/connectors/archive/LinkedInArchivePositionsExtractor.js +21 -13
- package/dist/connectors/archive/LinkedInArchiveProfileExtractor.d.ts +8 -1
- package/dist/connectors/archive/LinkedInArchiveProfileExtractor.js +16 -2
- package/dist/connectors/archive/LinkedInDefaultExtractor.d.ts +8 -1
- package/dist/connectors/archive/LinkedInDefaultExtractor.js +1 -1
- package/dist/connectors/fields.d.ts +8 -0
- package/dist/connectors/fields.js +17 -0
- package/package.json +12 -4
package/README.md
CHANGED
|
@@ -1,10 +1,11 @@
|
|
|
1
1
|
# LinkedIn plugin
|
|
2
2
|
|
|
3
3
|
Reads the **member data export** — the ZIP that Settings & Privacy → Data
|
|
4
|
-
Privacy → "Get a copy of your data" produces, unpacked to a
|
|
4
|
+
Privacy → "Get a copy of your data" produces, read in place or unpacked to a
|
|
5
|
+
directory.
|
|
5
6
|
|
|
6
7
|
```bash
|
|
7
|
-
chronicle extract linkedin --input ~/Downloads/Basic_LinkedInDataExport_08-08-2026 --limit 0
|
|
8
|
+
chronicle extract linkedin --input ~/Downloads/Basic_LinkedInDataExport_08-08-2026.zip --limit 0
|
|
8
9
|
chronicle extract linkedin --input <dir> --type messages --limit 5 # one record kind
|
|
9
10
|
```
|
|
10
11
|
|
|
@@ -114,8 +115,8 @@ made in 2010 would claim they worked there then. `Positions.csv` and
|
|
|
114
115
|
`Education.csv` are snapshots for the same reason. Messages, endorsements,
|
|
115
116
|
follows and learning are events.
|
|
116
117
|
|
|
117
|
-
**Re-importing the same
|
|
118
|
-
export's own file time, not the moment of import, so a second run restates
|
|
118
|
+
**Re-importing the same export is a no-op.** The sighting time is the
|
|
119
|
+
export's own file time, in a zip from its index, not the moment of import, so a second run restates
|
|
119
120
|
identical assertions instead of minting fresh ones.
|
|
120
121
|
|
|
121
122
|
## Not read, and why
|
|
@@ -289,14 +289,15 @@ export default class LinkedInTransformer extends ChronicleTransformer {
|
|
|
289
289
|
'@type',
|
|
290
290
|
'source',
|
|
291
291
|
'timestamp',
|
|
292
|
-
'
|
|
292
|
+
'result.isPartOf[*].sourceId',
|
|
293
293
|
'agent.handle',
|
|
294
294
|
'agent.name',
|
|
295
295
|
],
|
|
296
296
|
source: SOURCE,
|
|
297
297
|
timestamp: data.sentAt,
|
|
298
298
|
...(sender && { agent: sender }),
|
|
299
|
-
|
|
299
|
+
// Sending makes the message, so it's the action's result.
|
|
300
|
+
result: message,
|
|
300
301
|
};
|
|
301
302
|
return [sent];
|
|
302
303
|
}
|
|
@@ -1,4 +1,4 @@
|
|
|
1
|
-
import { Record } from '@chronicle.app/etl';
|
|
1
|
+
import { Record, type Survey, type SurveyOptions } from '@chronicle.app/etl';
|
|
2
2
|
import LinkedInTransformer from '../LinkedInTransformer.js';
|
|
3
3
|
import { LinkedInArchiveExtractor } from './LinkedInArchiveExtractor.js';
|
|
4
4
|
/**
|
|
@@ -43,6 +43,10 @@ export declare class LinkedInArchiveConnectionsExtractor extends LinkedInArchive
|
|
|
43
43
|
/** The connection's profile URL — LinkedIn's own id for them, and stable per export. */
|
|
44
44
|
keyOf(record: Record): string | null;
|
|
45
45
|
extract(): AsyncGenerator<Record>;
|
|
46
|
+
/** The connections a run reads, and the day each was made. */
|
|
47
|
+
survey(options?: SurveyOptions): Promise<Survey>;
|
|
48
|
+
/** Every connection in the export that names someone, newest first. */
|
|
49
|
+
private connections;
|
|
46
50
|
determineCount(): Promise<number>;
|
|
47
51
|
}
|
|
48
52
|
//# sourceMappingURL=LinkedInArchiveConnectionsExtractor.d.ts.map
|
|
@@ -26,6 +26,26 @@ export class LinkedInArchiveConnectionsExtractor extends LinkedInArchiveExtracto
|
|
|
26
26
|
return record.data.url ?? null;
|
|
27
27
|
}
|
|
28
28
|
async *extract() {
|
|
29
|
+
let count = 0;
|
|
30
|
+
for (const connection of await this.connections()) {
|
|
31
|
+
if (this.shouldStopExtracting(count))
|
|
32
|
+
break;
|
|
33
|
+
if (!this.withinWindow(connection.connectedOn))
|
|
34
|
+
continue;
|
|
35
|
+
yield this.createRecordWithArchiveContext({
|
|
36
|
+
...connection,
|
|
37
|
+
occurredAt: connection.connectedOn,
|
|
38
|
+
});
|
|
39
|
+
count++;
|
|
40
|
+
}
|
|
41
|
+
}
|
|
42
|
+
/** The connections a run reads, and the day each was made. */
|
|
43
|
+
async survey(options = {}) {
|
|
44
|
+
const dates = (await this.connections()).map(connection => connection.connectedOn);
|
|
45
|
+
return { connections: this.surveyDates(dates, options) };
|
|
46
|
+
}
|
|
47
|
+
/** Every connection in the export that names someone, newest first. */
|
|
48
|
+
async connections() {
|
|
29
49
|
const rows = await this.readCsv('Connections.csv');
|
|
30
50
|
const connections = rows
|
|
31
51
|
.map(row => {
|
|
@@ -47,18 +67,7 @@ export class LinkedInArchiveConnectionsExtractor extends LinkedInArchiveExtracto
|
|
|
47
67
|
// A row with neither a profile URL nor a name names nobody.
|
|
48
68
|
.filter(connection => connection.handle || connection.name)
|
|
49
69
|
.sort((a, b) => ((a.connectedOn ?? '') < (b.connectedOn ?? '') ? 1 : -1));
|
|
50
|
-
|
|
51
|
-
for (const connection of connections) {
|
|
52
|
-
if (this.shouldStopExtracting(count))
|
|
53
|
-
break;
|
|
54
|
-
if (!this.withinWindow(connection.connectedOn))
|
|
55
|
-
continue;
|
|
56
|
-
yield this.createRecordWithArchiveContext({
|
|
57
|
-
...connection,
|
|
58
|
-
occurredAt: connection.connectedOn,
|
|
59
|
-
});
|
|
60
|
-
count++;
|
|
61
|
-
}
|
|
70
|
+
return connections;
|
|
62
71
|
}
|
|
63
72
|
async determineCount() {
|
|
64
73
|
return (await this.readCsv('Connections.csv')).length;
|
|
@@ -1,4 +1,4 @@
|
|
|
1
|
-
import { Record } from '@chronicle.app/etl';
|
|
1
|
+
import { Record, type Survey, type SurveyOptions } from '@chronicle.app/etl';
|
|
2
2
|
import LinkedInTransformer from '../LinkedInTransformer.js';
|
|
3
3
|
import { LinkedInArchiveExtractor } from './LinkedInArchiveExtractor.js';
|
|
4
4
|
/**
|
|
@@ -31,6 +31,10 @@ export declare class LinkedInArchiveEducationExtractor extends LinkedInArchiveEx
|
|
|
31
31
|
limit?: number | undefined;
|
|
32
32
|
}>;
|
|
33
33
|
extract(): AsyncGenerator<Record>;
|
|
34
|
+
/** The schools a run reads, and when each began. */
|
|
35
|
+
survey(options?: SurveyOptions): Promise<Survey>;
|
|
36
|
+
/** Every school in the export with a name and a start, newest first. */
|
|
37
|
+
private enrollments;
|
|
34
38
|
determineCount(): Promise<number>;
|
|
35
39
|
}
|
|
36
40
|
//# sourceMappingURL=LinkedInArchiveEducationExtractor.d.ts.map
|
|
@@ -12,20 +12,8 @@ export class LinkedInArchiveEducationExtractor extends LinkedInArchiveExtractor
|
|
|
12
12
|
static defaultTransformer = LinkedInTransformer;
|
|
13
13
|
static schema = LinkedInArchiveExtractor.schema;
|
|
14
14
|
async *extract() {
|
|
15
|
-
const rows = await this.readCsv('Education.csv');
|
|
16
|
-
const enrollments = rows
|
|
17
|
-
.map(row => ({
|
|
18
|
-
school: row['School Name']?.trim(),
|
|
19
|
-
degree: row['Degree Name']?.trim(),
|
|
20
|
-
notes: row.Notes?.trim(),
|
|
21
|
-
activities: row.Activities?.trim(),
|
|
22
|
-
startedOn: parseCoarseDate(row['Start Date']),
|
|
23
|
-
finishedOn: parseCoarseDate(row['End Date']),
|
|
24
|
-
}))
|
|
25
|
-
.filter(enrollment => enrollment.school && enrollment.startedOn)
|
|
26
|
-
.sort((a, b) => (a.startedOn < b.startedOn ? 1 : -1));
|
|
27
15
|
let count = 0;
|
|
28
|
-
for (const enrollment of enrollments) {
|
|
16
|
+
for (const enrollment of await this.enrollments()) {
|
|
29
17
|
if (this.shouldStopExtracting(count))
|
|
30
18
|
break;
|
|
31
19
|
if (!this.withinWindow(enrollment.startedOn))
|
|
@@ -37,6 +25,26 @@ export class LinkedInArchiveEducationExtractor extends LinkedInArchiveExtractor
|
|
|
37
25
|
count++;
|
|
38
26
|
}
|
|
39
27
|
}
|
|
28
|
+
/** The schools a run reads, and when each began. */
|
|
29
|
+
async survey(options = {}) {
|
|
30
|
+
const dates = (await this.enrollments()).map(enrollment => enrollment.startedOn);
|
|
31
|
+
return { education: this.surveyDates(dates, options) };
|
|
32
|
+
}
|
|
33
|
+
/** Every school in the export with a name and a start, newest first. */
|
|
34
|
+
async enrollments() {
|
|
35
|
+
const rows = await this.readCsv('Education.csv');
|
|
36
|
+
return rows
|
|
37
|
+
.map(row => ({
|
|
38
|
+
school: row['School Name']?.trim(),
|
|
39
|
+
degree: row['Degree Name']?.trim(),
|
|
40
|
+
notes: row.Notes?.trim(),
|
|
41
|
+
activities: row.Activities?.trim(),
|
|
42
|
+
startedOn: parseCoarseDate(row['Start Date']),
|
|
43
|
+
finishedOn: parseCoarseDate(row['End Date']),
|
|
44
|
+
}))
|
|
45
|
+
.filter(enrollment => enrollment.school && enrollment.startedOn)
|
|
46
|
+
.sort((a, b) => (a.startedOn < b.startedOn ? 1 : -1));
|
|
47
|
+
}
|
|
40
48
|
async determineCount() {
|
|
41
49
|
return (await this.readCsv('Education.csv')).length;
|
|
42
50
|
}
|
|
@@ -1,4 +1,4 @@
|
|
|
1
|
-
import { Record } from '@chronicle.app/etl';
|
|
1
|
+
import { Record, type Survey, type SurveyOptions } from '@chronicle.app/etl';
|
|
2
2
|
import LinkedInTransformer from '../LinkedInTransformer.js';
|
|
3
3
|
import { LinkedInArchiveExtractor } from './LinkedInArchiveExtractor.js';
|
|
4
4
|
/**
|
|
@@ -39,6 +39,10 @@ export declare class LinkedInArchiveEndorsementsExtractor extends LinkedInArchiv
|
|
|
39
39
|
limit?: number | undefined;
|
|
40
40
|
}>;
|
|
41
41
|
extract(): AsyncGenerator<Record>;
|
|
42
|
+
/** The endorsements a run reads, both ways, and when each was made. */
|
|
43
|
+
survey(options?: SurveyOptions): Promise<Survey>;
|
|
44
|
+
/** Both endorsement files as one list, newest first. */
|
|
45
|
+
private endorsements;
|
|
42
46
|
/** One endorsement file, with the other party's columns read by prefix. */
|
|
43
47
|
private readEndorsements;
|
|
44
48
|
determineCount(): Promise<number>;
|
|
@@ -20,11 +20,8 @@ export class LinkedInArchiveEndorsementsExtractor extends LinkedInArchiveExtract
|
|
|
20
20
|
static defaultTransformer = LinkedInTransformer;
|
|
21
21
|
static schema = LinkedInArchiveExtractor.schema;
|
|
22
22
|
async *extract() {
|
|
23
|
-
const given = await this.readEndorsements('Endorsement_Given_Info.csv', 'given');
|
|
24
|
-
const received = await this.readEndorsements('Endorsement_Received_Info.csv', 'received');
|
|
25
|
-
const endorsements = [...given, ...received].sort((a, b) => (a.endorsedAt ?? '') < (b.endorsedAt ?? '') ? 1 : -1);
|
|
26
23
|
let count = 0;
|
|
27
|
-
for (const endorsement of endorsements) {
|
|
24
|
+
for (const endorsement of await this.endorsements()) {
|
|
28
25
|
if (this.shouldStopExtracting(count))
|
|
29
26
|
break;
|
|
30
27
|
if (!this.withinWindow(endorsement.endorsedAt))
|
|
@@ -36,6 +33,17 @@ export class LinkedInArchiveEndorsementsExtractor extends LinkedInArchiveExtract
|
|
|
36
33
|
count++;
|
|
37
34
|
}
|
|
38
35
|
}
|
|
36
|
+
/** The endorsements a run reads, both ways, and when each was made. */
|
|
37
|
+
async survey(options = {}) {
|
|
38
|
+
const dates = (await this.endorsements()).map(endorsement => endorsement.endorsedAt);
|
|
39
|
+
return { endorsements: this.surveyDates(dates, options) };
|
|
40
|
+
}
|
|
41
|
+
/** Both endorsement files as one list, newest first. */
|
|
42
|
+
async endorsements() {
|
|
43
|
+
const given = await this.readEndorsements('Endorsement_Given_Info.csv', 'given');
|
|
44
|
+
const received = await this.readEndorsements('Endorsement_Received_Info.csv', 'received');
|
|
45
|
+
return [...given, ...received].sort((a, b) => (a.endorsedAt ?? '') < (b.endorsedAt ?? '') ? 1 : -1);
|
|
46
|
+
}
|
|
39
47
|
/** One endorsement file, with the other party's columns read by prefix. */
|
|
40
48
|
async readEndorsements(fileName, direction) {
|
|
41
49
|
const prefix = direction === 'given' ? 'Endorsee' : 'Endorser';
|
|
@@ -1,4 +1,4 @@
|
|
|
1
|
-
import { ArchiveExtractor } from '@chronicle.app/etl';
|
|
1
|
+
import { ArchiveExtractor, type KindSurvey, type SurveyOptions } from '@chronicle.app/etl';
|
|
2
2
|
import { z } from 'zod';
|
|
3
3
|
/** What one export knows about the member who requested it. */
|
|
4
4
|
export interface LinkedInAccount {
|
|
@@ -13,8 +13,8 @@ export interface LinkedInAccount {
|
|
|
13
13
|
}
|
|
14
14
|
/**
|
|
15
15
|
* Base for the LinkedIn member data export — the ZIP that Settings & Privacy →
|
|
16
|
-
* Data Privacy → "Get a copy of your data" produces
|
|
17
|
-
*
|
|
16
|
+
* Data Privacy → "Get a copy of your data" produces: a folder of CSVs, read
|
|
17
|
+
* as the zip or unpacked.
|
|
18
18
|
*
|
|
19
19
|
* Shared here: reading a named CSV out of the export, the account info every
|
|
20
20
|
* record carries, and the date window. One subclass per file, because the files
|
|
@@ -50,10 +50,11 @@ export declare abstract class LinkedInArchiveExtractor<T extends typeof LinkedIn
|
|
|
50
50
|
limit?: number | undefined;
|
|
51
51
|
}>;
|
|
52
52
|
protected accountInfo: LinkedInAccount;
|
|
53
|
+
protected readonly exportName = "LinkedIn export";
|
|
53
54
|
/** Lower-cased file name → real file name, so a renamed case still resolves. */
|
|
54
55
|
private fileIndex?;
|
|
55
56
|
/** When LinkedIn wrote this export — see {@link asOfTime}. */
|
|
56
|
-
|
|
57
|
+
protected compiledAt?: Date;
|
|
57
58
|
/**
|
|
58
59
|
* Read the export's own facts before extraction: who it belongs to, and when
|
|
59
60
|
* LinkedIn built it. The base class defines `loadAccountInfo` but never calls
|
|
@@ -71,7 +72,7 @@ export declare abstract class LinkedInArchiveExtractor<T extends typeof LinkedIn
|
|
|
71
72
|
* of a fresh sighting of unchanged facts.
|
|
72
73
|
*/
|
|
73
74
|
protected asOfTime(): Date;
|
|
74
|
-
/** The newest mtime among the export's files, or none if it has none. */
|
|
75
|
+
/** The newest mtime among the export's files, or none if it has none; in a zip, from its index. */
|
|
75
76
|
private newestFileTime;
|
|
76
77
|
/**
|
|
77
78
|
* The export's own account facts, read once per run: the profile, the primary
|
|
@@ -105,7 +106,7 @@ export declare abstract class LinkedInArchiveExtractor<T extends typeof LinkedIn
|
|
|
105
106
|
protected readCsv(fileName: string): Promise<Array<{
|
|
106
107
|
[column: string]: string | undefined;
|
|
107
108
|
}>>;
|
|
108
|
-
/** The real
|
|
109
|
+
/** The real name of a file in the export, matched case-insensitively, or none. */
|
|
109
110
|
private resolveFile;
|
|
110
111
|
/** Every file in the export, listed once and indexed for case-insensitive lookup. */
|
|
111
112
|
private fileNames;
|
|
@@ -116,5 +117,12 @@ export declare abstract class LinkedInArchiveExtractor<T extends typeof LinkedIn
|
|
|
116
117
|
* record on the strength of a date the source never wrote.
|
|
117
118
|
*/
|
|
118
119
|
protected withinWindow(date: string | undefined): boolean;
|
|
120
|
+
/**
|
|
121
|
+
* One record type's survey, from the date of each record a run would read,
|
|
122
|
+
* undated ones too: kept or dropped by the same since/until window, and
|
|
123
|
+
* placed by {@link occurrenceTime}. An undated record counts, but has no
|
|
124
|
+
* time or month to add.
|
|
125
|
+
*/
|
|
126
|
+
protected surveyDates(dates: Array<string | undefined>, options: SurveyOptions): KindSurvey;
|
|
119
127
|
}
|
|
120
128
|
//# sourceMappingURL=LinkedInArchiveExtractor.d.ts.map
|
|
@@ -1,13 +1,11 @@
|
|
|
1
|
-
import { ArchiveExtractor,
|
|
1
|
+
import { ArchiveExtractor, countMonth, Extractor, } from '@chronicle.app/etl';
|
|
2
2
|
import { parse } from 'csv-parse/sync';
|
|
3
|
-
import { readdir, readFile, stat } from 'node:fs/promises';
|
|
4
|
-
import { join } from 'node:path';
|
|
5
3
|
import { z } from 'zod';
|
|
6
|
-
import { profileHandle } from '../fields.js';
|
|
4
|
+
import { occurrenceTime, profileHandle } from '../fields.js';
|
|
7
5
|
/**
|
|
8
6
|
* Base for the LinkedIn member data export — the ZIP that Settings & Privacy →
|
|
9
|
-
* Data Privacy → "Get a copy of your data" produces
|
|
10
|
-
*
|
|
7
|
+
* Data Privacy → "Get a copy of your data" produces: a folder of CSVs, read
|
|
8
|
+
* as the zip or unpacked.
|
|
11
9
|
*
|
|
12
10
|
* Shared here: reading a named CSV out of the export, the account info every
|
|
13
11
|
* record carries, and the date window. One subclass per file, because the files
|
|
@@ -23,13 +21,14 @@ export class LinkedInArchiveExtractor extends ArchiveExtractor {
|
|
|
23
21
|
static source = 'linkedin';
|
|
24
22
|
static strategy = 'archive';
|
|
25
23
|
static schema = Extractor.schema.extend({
|
|
26
|
-
input: z.string().describe('
|
|
24
|
+
input: z.string().describe('The LinkedIn export, as a zip or its unpacked folder'),
|
|
27
25
|
'profile-url': z
|
|
28
26
|
.string()
|
|
29
27
|
.optional()
|
|
30
28
|
.describe('Your LinkedIn profile URL (https://www.linkedin.com/in/…). Only needed when the export carries no invitation to infer it from.'),
|
|
31
29
|
});
|
|
32
30
|
accountInfo = {};
|
|
31
|
+
exportName = 'LinkedIn export';
|
|
33
32
|
/** Lower-cased file name → real file name, so a renamed case still resolves. */
|
|
34
33
|
fileIndex;
|
|
35
34
|
/** When LinkedIn wrote this export — see {@link asOfTime}. */
|
|
@@ -57,19 +56,14 @@ export class LinkedInArchiveExtractor extends ArchiveExtractor {
|
|
|
57
56
|
asOfTime() {
|
|
58
57
|
return this.compiledAt ?? super.asOfTime();
|
|
59
58
|
}
|
|
60
|
-
/** The newest mtime among the export's files, or none if it has none. */
|
|
59
|
+
/** The newest mtime among the export's files, or none if it has none; in a zip, from its index. */
|
|
61
60
|
async newestFileTime() {
|
|
62
|
-
const { input } = this.config;
|
|
63
61
|
let newest;
|
|
64
62
|
for (const name of await this.fileNames()) {
|
|
65
|
-
|
|
66
|
-
|
|
67
|
-
|
|
68
|
-
|
|
69
|
-
}
|
|
70
|
-
catch {
|
|
71
|
-
// A file that vanished between listing and stat tells us nothing.
|
|
72
|
-
}
|
|
63
|
+
// A file that vanished between listing and stat tells us nothing.
|
|
64
|
+
const mtimeMs = (await this.files.modified(name))?.getTime();
|
|
65
|
+
if (mtimeMs !== undefined && (newest === undefined || mtimeMs > newest))
|
|
66
|
+
newest = mtimeMs;
|
|
73
67
|
}
|
|
74
68
|
return newest === undefined ? undefined : new Date(newest);
|
|
75
69
|
}
|
|
@@ -147,7 +141,7 @@ export class LinkedInArchiveExtractor extends ArchiveExtractor {
|
|
|
147
141
|
const path = await this.resolveFile(fileName);
|
|
148
142
|
if (!path)
|
|
149
143
|
return [];
|
|
150
|
-
const text = stripPreamble(await
|
|
144
|
+
const text = stripPreamble(await this.files.readText(path));
|
|
151
145
|
if (!text.trim())
|
|
152
146
|
return [];
|
|
153
147
|
return parse(text, {
|
|
@@ -158,27 +152,15 @@ export class LinkedInArchiveExtractor extends ArchiveExtractor {
|
|
|
158
152
|
bom: true,
|
|
159
153
|
});
|
|
160
154
|
}
|
|
161
|
-
/** The real
|
|
155
|
+
/** The real name of a file in the export, matched case-insensitively, or none. */
|
|
162
156
|
async resolveFile(fileName) {
|
|
163
157
|
await this.fileNames();
|
|
164
|
-
|
|
165
|
-
const { input } = this.config;
|
|
166
|
-
return resolved ? join(input, resolved) : undefined;
|
|
158
|
+
return this.fileIndex.get(fileName.toLowerCase());
|
|
167
159
|
}
|
|
168
160
|
/** Every file in the export, listed once and indexed for case-insensitive lookup. */
|
|
169
161
|
async fileNames() {
|
|
170
162
|
if (!this.fileIndex) {
|
|
171
|
-
const
|
|
172
|
-
let entries = [];
|
|
173
|
-
try {
|
|
174
|
-
entries = await readdir(input);
|
|
175
|
-
}
|
|
176
|
-
catch (error) {
|
|
177
|
-
const typed = fileError(error, input, 'LinkedIn export');
|
|
178
|
-
throw typed === error
|
|
179
|
-
? new InputNotFound("Couldn't read the LinkedIn export", { path: input, cause: error })
|
|
180
|
-
: typed;
|
|
181
|
-
}
|
|
163
|
+
const entries = (await this.files.children('')).filter(name => !name.endsWith('/'));
|
|
182
164
|
this.fileIndex = new Map(entries.map(name => [name.toLowerCase(), name]));
|
|
183
165
|
}
|
|
184
166
|
return [...this.fileIndex.values()];
|
|
@@ -195,6 +177,30 @@ export class LinkedInArchiveExtractor extends ArchiveExtractor {
|
|
|
195
177
|
const parsed = new Date(date);
|
|
196
178
|
return Number.isNaN(parsed.getTime()) ? true : this.isWithinDateRange(parsed);
|
|
197
179
|
}
|
|
180
|
+
/**
|
|
181
|
+
* One record type's survey, from the date of each record a run would read,
|
|
182
|
+
* undated ones too: kept or dropped by the same since/until window, and
|
|
183
|
+
* placed by {@link occurrenceTime}. An undated record counts, but has no
|
|
184
|
+
* time or month to add.
|
|
185
|
+
*/
|
|
186
|
+
surveyDates(dates, options) {
|
|
187
|
+
const kind = { count: 0, ...(options.byMonth && { byMonth: {} }) };
|
|
188
|
+
for (const date of dates) {
|
|
189
|
+
if (!this.withinWindow(date))
|
|
190
|
+
continue;
|
|
191
|
+
kind.count++;
|
|
192
|
+
const time = occurrenceTime(date);
|
|
193
|
+
if (!time)
|
|
194
|
+
continue;
|
|
195
|
+
if (!kind.earliest || time < kind.earliest)
|
|
196
|
+
kind.earliest = time;
|
|
197
|
+
if (!kind.latest || time > kind.latest)
|
|
198
|
+
kind.latest = time;
|
|
199
|
+
if (kind.byMonth)
|
|
200
|
+
countMonth(kind.byMonth, time);
|
|
201
|
+
}
|
|
202
|
+
return kind;
|
|
203
|
+
}
|
|
198
204
|
}
|
|
199
205
|
/**
|
|
200
206
|
* Drop the prose LinkedIn writes above the header of `Connections.csv` — a
|
|
@@ -1,4 +1,4 @@
|
|
|
1
|
-
import { Record } from '@chronicle.app/etl';
|
|
1
|
+
import { Record, type Survey, type SurveyOptions } from '@chronicle.app/etl';
|
|
2
2
|
import LinkedInTransformer from '../LinkedInTransformer.js';
|
|
3
3
|
import { LinkedInArchiveExtractor } from './LinkedInArchiveExtractor.js';
|
|
4
4
|
/**
|
|
@@ -34,6 +34,10 @@ export declare class LinkedInArchiveFollowsExtractor extends LinkedInArchiveExtr
|
|
|
34
34
|
limit?: number | undefined;
|
|
35
35
|
}>;
|
|
36
36
|
extract(): AsyncGenerator<Record>;
|
|
37
|
+
/** The follows a run reads, and when each was made. */
|
|
38
|
+
survey(options?: SurveyOptions): Promise<Survey>;
|
|
39
|
+
/** Every follow in the export that names what it follows, newest first. */
|
|
40
|
+
private follows;
|
|
37
41
|
determineCount(): Promise<number>;
|
|
38
42
|
}
|
|
39
43
|
//# sourceMappingURL=LinkedInArchiveFollowsExtractor.d.ts.map
|
|
@@ -15,16 +15,8 @@ export class LinkedInArchiveFollowsExtractor extends LinkedInArchiveExtractor {
|
|
|
15
15
|
static defaultTransformer = LinkedInTransformer;
|
|
16
16
|
static schema = LinkedInArchiveExtractor.schema;
|
|
17
17
|
async *extract() {
|
|
18
|
-
const rows = await this.readCsv('Company Follows.csv');
|
|
19
|
-
const follows = rows
|
|
20
|
-
.map(row => ({
|
|
21
|
-
organization: row.Organization?.trim(),
|
|
22
|
-
followedAt: parseLongStamp(row['Followed On']),
|
|
23
|
-
}))
|
|
24
|
-
.filter(follow => follow.organization)
|
|
25
|
-
.sort((a, b) => ((a.followedAt ?? '') < (b.followedAt ?? '') ? 1 : -1));
|
|
26
18
|
let count = 0;
|
|
27
|
-
for (const follow of follows) {
|
|
19
|
+
for (const follow of await this.follows()) {
|
|
28
20
|
if (this.shouldStopExtracting(count))
|
|
29
21
|
break;
|
|
30
22
|
if (!this.withinWindow(follow.followedAt))
|
|
@@ -36,6 +28,22 @@ export class LinkedInArchiveFollowsExtractor extends LinkedInArchiveExtractor {
|
|
|
36
28
|
count++;
|
|
37
29
|
}
|
|
38
30
|
}
|
|
31
|
+
/** The follows a run reads, and when each was made. */
|
|
32
|
+
async survey(options = {}) {
|
|
33
|
+
const dates = (await this.follows()).map(follow => follow.followedAt);
|
|
34
|
+
return { follows: this.surveyDates(dates, options) };
|
|
35
|
+
}
|
|
36
|
+
/** Every follow in the export that names what it follows, newest first. */
|
|
37
|
+
async follows() {
|
|
38
|
+
const rows = await this.readCsv('Company Follows.csv');
|
|
39
|
+
return rows
|
|
40
|
+
.map(row => ({
|
|
41
|
+
organization: row.Organization?.trim(),
|
|
42
|
+
followedAt: parseLongStamp(row['Followed On']),
|
|
43
|
+
}))
|
|
44
|
+
.filter(follow => follow.organization)
|
|
45
|
+
.sort((a, b) => ((a.followedAt ?? '') < (b.followedAt ?? '') ? 1 : -1));
|
|
46
|
+
}
|
|
39
47
|
async determineCount() {
|
|
40
48
|
return (await this.readCsv('Company Follows.csv')).length;
|
|
41
49
|
}
|
|
@@ -1,4 +1,4 @@
|
|
|
1
|
-
import { Record } from '@chronicle.app/etl';
|
|
1
|
+
import { Record, type Survey, type SurveyOptions } from '@chronicle.app/etl';
|
|
2
2
|
import LinkedInTransformer from '../LinkedInTransformer.js';
|
|
3
3
|
import { LinkedInArchiveExtractor } from './LinkedInArchiveExtractor.js';
|
|
4
4
|
/**
|
|
@@ -36,6 +36,10 @@ export declare class LinkedInArchiveLearningExtractor extends LinkedInArchiveExt
|
|
|
36
36
|
limit?: number | undefined;
|
|
37
37
|
}>;
|
|
38
38
|
extract(): AsyncGenerator<Record>;
|
|
39
|
+
/** The courses a run reads, and when each was last watched, else completed. */
|
|
40
|
+
survey(options?: SurveyOptions): Promise<Survey>;
|
|
41
|
+
/** Every course in the export with a title, newest-watched first. */
|
|
42
|
+
private courses;
|
|
39
43
|
determineCount(): Promise<number>;
|
|
40
44
|
}
|
|
41
45
|
//# sourceMappingURL=LinkedInArchiveLearningExtractor.d.ts.map
|
|
@@ -17,8 +17,26 @@ export class LinkedInArchiveLearningExtractor extends LinkedInArchiveExtractor {
|
|
|
17
17
|
static defaultTransformer = LinkedInTransformer;
|
|
18
18
|
static schema = LinkedInArchiveExtractor.schema;
|
|
19
19
|
async *extract() {
|
|
20
|
+
let count = 0;
|
|
21
|
+
for (const course of await this.courses()) {
|
|
22
|
+
if (this.shouldStopExtracting(count))
|
|
23
|
+
break;
|
|
24
|
+
const occurredAt = lastSeen(course);
|
|
25
|
+
if (!this.withinWindow(occurredAt))
|
|
26
|
+
continue;
|
|
27
|
+
yield this.createRecordWithArchiveContext({ ...course, occurredAt });
|
|
28
|
+
count++;
|
|
29
|
+
}
|
|
30
|
+
}
|
|
31
|
+
/** The courses a run reads, and when each was last watched, else completed. */
|
|
32
|
+
async survey(options = {}) {
|
|
33
|
+
const dates = (await this.courses()).map(course => lastSeen(course));
|
|
34
|
+
return { learning: this.surveyDates(dates, options) };
|
|
35
|
+
}
|
|
36
|
+
/** Every course in the export with a title, newest-watched first. */
|
|
37
|
+
async courses() {
|
|
20
38
|
const rows = await this.readCsv('Learning.csv');
|
|
21
|
-
|
|
39
|
+
return rows
|
|
22
40
|
.map(row => ({
|
|
23
41
|
title: row['Content Title']?.trim(),
|
|
24
42
|
description: row['Content Description']?.trim() || undefined,
|
|
@@ -29,18 +47,12 @@ export class LinkedInArchiveLearningExtractor extends LinkedInArchiveExtractor {
|
|
|
29
47
|
}))
|
|
30
48
|
.filter(course => course.title)
|
|
31
49
|
.sort((a, b) => ((a.watchedAt ?? '') < (b.watchedAt ?? '') ? 1 : -1));
|
|
32
|
-
let count = 0;
|
|
33
|
-
for (const course of courses) {
|
|
34
|
-
if (this.shouldStopExtracting(count))
|
|
35
|
-
break;
|
|
36
|
-
const occurredAt = course.watchedAt ?? course.completedAt;
|
|
37
|
-
if (!this.withinWindow(occurredAt))
|
|
38
|
-
continue;
|
|
39
|
-
yield this.createRecordWithArchiveContext({ ...course, occurredAt });
|
|
40
|
-
count++;
|
|
41
|
-
}
|
|
42
50
|
}
|
|
43
51
|
async determineCount() {
|
|
44
52
|
return (await this.readCsv('Learning.csv')).length;
|
|
45
53
|
}
|
|
46
54
|
}
|
|
55
|
+
/** When a course dates to: the last time it was watched, else when it was completed. */
|
|
56
|
+
function lastSeen(course) {
|
|
57
|
+
return course.watchedAt ?? course.completedAt;
|
|
58
|
+
}
|
|
@@ -1,4 +1,4 @@
|
|
|
1
|
-
import { Record } from '@chronicle.app/etl';
|
|
1
|
+
import { Record, type Survey, type SurveyOptions } from '@chronicle.app/etl';
|
|
2
2
|
import LinkedInTransformer from '../LinkedInTransformer.js';
|
|
3
3
|
import { LinkedInArchiveExtractor } from './LinkedInArchiveExtractor.js';
|
|
4
4
|
/** A party to a message: the handle when LinkedIn gave one, the name when it did not. */
|
|
@@ -58,6 +58,10 @@ export declare class LinkedInArchiveMessagesExtractor extends LinkedInArchiveExt
|
|
|
58
58
|
limit?: number | undefined;
|
|
59
59
|
}>;
|
|
60
60
|
extract(): AsyncGenerator<Record>;
|
|
61
|
+
/** The messages a run reads and when they were sent, with how many conversations they're in. */
|
|
62
|
+
survey(options?: SurveyOptions): Promise<Survey>;
|
|
63
|
+
/** Every message in the export that can be placed, newest first. */
|
|
64
|
+
private messages;
|
|
61
65
|
determineCount(): Promise<number>;
|
|
62
66
|
}
|
|
63
67
|
//# sourceMappingURL=LinkedInArchiveMessagesExtractor.d.ts.map
|
|
@@ -34,6 +34,33 @@ export class LinkedInArchiveMessagesExtractor extends LinkedInArchiveExtractor {
|
|
|
34
34
|
static defaultTransformer = LinkedInTransformer;
|
|
35
35
|
static schema = LinkedInArchiveExtractor.schema;
|
|
36
36
|
async *extract() {
|
|
37
|
+
let count = 0;
|
|
38
|
+
for (const message of await this.messages()) {
|
|
39
|
+
if (this.shouldStopExtracting(count))
|
|
40
|
+
break;
|
|
41
|
+
if (!this.withinWindow(message.sentAt))
|
|
42
|
+
continue;
|
|
43
|
+
yield this.createRecordWithArchiveContext({
|
|
44
|
+
...message,
|
|
45
|
+
occurredAt: message.sentAt,
|
|
46
|
+
});
|
|
47
|
+
count++;
|
|
48
|
+
}
|
|
49
|
+
}
|
|
50
|
+
/** The messages a run reads and when they were sent, with how many conversations they're in. */
|
|
51
|
+
async survey(options = {}) {
|
|
52
|
+
const messages = (await this.messages()).filter(message => this.withinWindow(message.sentAt));
|
|
53
|
+
const dates = messages.map(message => message.sentAt);
|
|
54
|
+
const conversations = new Set(messages.map(message => message.conversationId)).size;
|
|
55
|
+
return {
|
|
56
|
+
messages: {
|
|
57
|
+
...this.surveyDates(dates, options),
|
|
58
|
+
measures: [{ key: 'conversations', label: 'conversations', value: conversations }],
|
|
59
|
+
},
|
|
60
|
+
};
|
|
61
|
+
}
|
|
62
|
+
/** Every message in the export that can be placed, newest first. */
|
|
63
|
+
async messages() {
|
|
37
64
|
const rows = await this.readCsv('messages.csv');
|
|
38
65
|
const messages = rows
|
|
39
66
|
.map(row => {
|
|
@@ -55,18 +82,7 @@ export class LinkedInArchiveMessagesExtractor extends LinkedInArchiveExtractor {
|
|
|
55
82
|
// a row missing either is not a message we can place.
|
|
56
83
|
.filter(message => message.conversationId && message.sentAt)
|
|
57
84
|
.sort((a, b) => ((a.sentAt ?? '') < (b.sentAt ?? '') ? 1 : -1));
|
|
58
|
-
|
|
59
|
-
for (const message of messages) {
|
|
60
|
-
if (this.shouldStopExtracting(count))
|
|
61
|
-
break;
|
|
62
|
-
if (!this.withinWindow(message.sentAt))
|
|
63
|
-
continue;
|
|
64
|
-
yield this.createRecordWithArchiveContext({
|
|
65
|
-
...message,
|
|
66
|
-
occurredAt: message.sentAt,
|
|
67
|
-
});
|
|
68
|
-
count++;
|
|
69
|
-
}
|
|
85
|
+
return messages;
|
|
70
86
|
}
|
|
71
87
|
async determineCount() {
|
|
72
88
|
return (await this.readCsv('messages.csv')).length;
|
|
@@ -1,4 +1,4 @@
|
|
|
1
|
-
import { Record } from '@chronicle.app/etl';
|
|
1
|
+
import { Record, type Survey, type SurveyOptions } from '@chronicle.app/etl';
|
|
2
2
|
import LinkedInTransformer from '../LinkedInTransformer.js';
|
|
3
3
|
import { LinkedInArchiveExtractor } from './LinkedInArchiveExtractor.js';
|
|
4
4
|
/**
|
|
@@ -35,6 +35,10 @@ export declare class LinkedInArchivePositionsExtractor extends LinkedInArchiveEx
|
|
|
35
35
|
limit?: number | undefined;
|
|
36
36
|
}>;
|
|
37
37
|
extract(): AsyncGenerator<Record>;
|
|
38
|
+
/** The positions a run reads, and when each began. */
|
|
39
|
+
survey(options?: SurveyOptions): Promise<Survey>;
|
|
40
|
+
/** Every position in the export with a company and a start, newest first. */
|
|
41
|
+
private positions;
|
|
38
42
|
determineCount(): Promise<number>;
|
|
39
43
|
}
|
|
40
44
|
//# sourceMappingURL=LinkedInArchivePositionsExtractor.d.ts.map
|
|
@@ -16,20 +16,8 @@ export class LinkedInArchivePositionsExtractor extends LinkedInArchiveExtractor
|
|
|
16
16
|
static defaultTransformer = LinkedInTransformer;
|
|
17
17
|
static schema = LinkedInArchiveExtractor.schema;
|
|
18
18
|
async *extract() {
|
|
19
|
-
const rows = await this.readCsv('Positions.csv');
|
|
20
|
-
const positions = rows
|
|
21
|
-
.map(row => ({
|
|
22
|
-
company: row['Company Name']?.trim(),
|
|
23
|
-
title: row.Title?.trim(),
|
|
24
|
-
description: row.Description?.trim(),
|
|
25
|
-
location: row.Location?.trim(),
|
|
26
|
-
startedOn: parseCoarseDate(row['Started On']),
|
|
27
|
-
finishedOn: parseCoarseDate(row['Finished On']),
|
|
28
|
-
}))
|
|
29
|
-
.filter(position => position.company && position.startedOn)
|
|
30
|
-
.sort((a, b) => (a.startedOn < b.startedOn ? 1 : -1));
|
|
31
19
|
let count = 0;
|
|
32
|
-
for (const position of positions) {
|
|
20
|
+
for (const position of await this.positions()) {
|
|
33
21
|
if (this.shouldStopExtracting(count))
|
|
34
22
|
break;
|
|
35
23
|
if (!this.withinWindow(position.startedOn))
|
|
@@ -41,6 +29,26 @@ export class LinkedInArchivePositionsExtractor extends LinkedInArchiveExtractor
|
|
|
41
29
|
count++;
|
|
42
30
|
}
|
|
43
31
|
}
|
|
32
|
+
/** The positions a run reads, and when each began. */
|
|
33
|
+
async survey(options = {}) {
|
|
34
|
+
const dates = (await this.positions()).map(position => position.startedOn);
|
|
35
|
+
return { positions: this.surveyDates(dates, options) };
|
|
36
|
+
}
|
|
37
|
+
/** Every position in the export with a company and a start, newest first. */
|
|
38
|
+
async positions() {
|
|
39
|
+
const rows = await this.readCsv('Positions.csv');
|
|
40
|
+
return rows
|
|
41
|
+
.map(row => ({
|
|
42
|
+
company: row['Company Name']?.trim(),
|
|
43
|
+
title: row.Title?.trim(),
|
|
44
|
+
description: row.Description?.trim(),
|
|
45
|
+
location: row.Location?.trim(),
|
|
46
|
+
startedOn: parseCoarseDate(row['Started On']),
|
|
47
|
+
finishedOn: parseCoarseDate(row['Finished On']),
|
|
48
|
+
}))
|
|
49
|
+
.filter(position => position.company && position.startedOn)
|
|
50
|
+
.sort((a, b) => (a.startedOn < b.startedOn ? 1 : -1));
|
|
51
|
+
}
|
|
44
52
|
async determineCount() {
|
|
45
53
|
return (await this.readCsv('Positions.csv')).length;
|
|
46
54
|
}
|
|
@@ -1,4 +1,4 @@
|
|
|
1
|
-
import { Record } from '@chronicle.app/etl';
|
|
1
|
+
import { Record, type Survey } from '@chronicle.app/etl';
|
|
2
2
|
import LinkedInTransformer from '../LinkedInTransformer.js';
|
|
3
3
|
import { LinkedInArchiveExtractor } from './LinkedInArchiveExtractor.js';
|
|
4
4
|
/**
|
|
@@ -36,6 +36,13 @@ export declare class LinkedInArchiveProfileExtractor extends LinkedInArchiveExtr
|
|
|
36
36
|
limit?: number | undefined;
|
|
37
37
|
}>;
|
|
38
38
|
extract(): AsyncGenerator<Record>;
|
|
39
|
+
/**
|
|
40
|
+
* The profile a run reads, as of when LinkedIn compiled the export: it has
|
|
41
|
+
* no time of its own, and dates its sighting to that.
|
|
42
|
+
*/
|
|
43
|
+
survey(): Promise<Survey>;
|
|
44
|
+
/** The account, when the export names its member at all. */
|
|
45
|
+
private profile;
|
|
39
46
|
determineCount(): Promise<number>;
|
|
40
47
|
}
|
|
41
48
|
//# sourceMappingURL=LinkedInArchiveProfileExtractor.d.ts.map
|
|
@@ -16,11 +16,25 @@ export class LinkedInArchiveProfileExtractor extends LinkedInArchiveExtractor {
|
|
|
16
16
|
static defaultTransformer = LinkedInTransformer;
|
|
17
17
|
static schema = LinkedInArchiveExtractor.schema;
|
|
18
18
|
async *extract() {
|
|
19
|
-
const account = this.
|
|
20
|
-
if (!account
|
|
19
|
+
const account = this.profile();
|
|
20
|
+
if (!account)
|
|
21
21
|
return;
|
|
22
22
|
yield this.createRecordWithArchiveContext({ ...account });
|
|
23
23
|
}
|
|
24
|
+
/**
|
|
25
|
+
* The profile a run reads, as of when LinkedIn compiled the export: it has
|
|
26
|
+
* no time of its own, and dates its sighting to that.
|
|
27
|
+
*/
|
|
28
|
+
async survey() {
|
|
29
|
+
if (!this.profile())
|
|
30
|
+
return { profile: { count: 0 } };
|
|
31
|
+
return { profile: { count: 1, ...(this.compiledAt && { asOf: this.compiledAt }) } };
|
|
32
|
+
}
|
|
33
|
+
/** The account, when the export names its member at all. */
|
|
34
|
+
profile() {
|
|
35
|
+
const account = this.accountInfo;
|
|
36
|
+
return account.name || account.handle || account.email ? account : undefined;
|
|
37
|
+
}
|
|
24
38
|
async determineCount() {
|
|
25
39
|
return 1;
|
|
26
40
|
}
|
|
@@ -1,6 +1,13 @@
|
|
|
1
1
|
import { MergingExtractor, Record } from '@chronicle.app/etl';
|
|
2
2
|
import LinkedInTransformer from '../LinkedInTransformer.js';
|
|
3
|
+
import { LinkedInArchiveConnectionsExtractor } from './LinkedInArchiveConnectionsExtractor.js';
|
|
4
|
+
import { LinkedInArchiveEducationExtractor } from './LinkedInArchiveEducationExtractor.js';
|
|
5
|
+
import { LinkedInArchiveEndorsementsExtractor } from './LinkedInArchiveEndorsementsExtractor.js';
|
|
6
|
+
import { LinkedInArchiveFollowsExtractor } from './LinkedInArchiveFollowsExtractor.js';
|
|
7
|
+
import { LinkedInArchiveLearningExtractor } from './LinkedInArchiveLearningExtractor.js';
|
|
3
8
|
import { LinkedInArchiveMessagesExtractor } from './LinkedInArchiveMessagesExtractor.js';
|
|
9
|
+
import { LinkedInArchivePositionsExtractor } from './LinkedInArchivePositionsExtractor.js';
|
|
10
|
+
import { LinkedInArchiveProfileExtractor } from './LinkedInArchiveProfileExtractor.js';
|
|
4
11
|
/**
|
|
5
12
|
* The whole export in one reverse-chronological pass — what a bare
|
|
6
13
|
* `extract linkedin -i <dir>` means.
|
|
@@ -18,7 +25,7 @@ export declare class LinkedInDefaultExtractor extends MergingExtractor<typeof Li
|
|
|
18
25
|
static recordTypes: string[];
|
|
19
26
|
static default: boolean;
|
|
20
27
|
static defaultTransformer: typeof LinkedInTransformer;
|
|
21
|
-
static children: (typeof LinkedInArchiveMessagesExtractor)[];
|
|
28
|
+
static children: (typeof LinkedInArchiveMessagesExtractor | typeof LinkedInArchiveConnectionsExtractor | typeof LinkedInArchiveEducationExtractor | typeof LinkedInArchiveEndorsementsExtractor | typeof LinkedInArchiveFollowsExtractor | typeof LinkedInArchiveLearningExtractor | typeof LinkedInArchivePositionsExtractor | typeof LinkedInArchiveProfileExtractor)[];
|
|
22
29
|
static schema: any;
|
|
23
30
|
/**
|
|
24
31
|
* Every child dates its records the same way, on `occurredAt`. A record with
|
|
@@ -48,7 +48,7 @@ export class LinkedInDefaultExtractor extends MergingExtractor {
|
|
|
48
48
|
// The union of the children's flags, so the runner forwards them to the merge
|
|
49
49
|
// and the merge forwards the raw config to each child.
|
|
50
50
|
static schema = MergingExtractor.schema.extend({
|
|
51
|
-
input: z.string().describe('
|
|
51
|
+
input: z.string().describe('The LinkedIn export, as a zip or its unpacked folder'),
|
|
52
52
|
'profile-url': z
|
|
53
53
|
.string()
|
|
54
54
|
.optional()
|
|
@@ -53,4 +53,12 @@ export declare function parseDayDate(raw: string | undefined): string | undefine
|
|
|
53
53
|
* its own precision, which is why widening them to a day would be a lie.
|
|
54
54
|
*/
|
|
55
55
|
export declare function parseCoarseDate(raw: string | undefined): string | undefined;
|
|
56
|
+
/**
|
|
57
|
+
* A date one of these readers returned, as the moment a survey counts it at:
|
|
58
|
+
* an instant as itself, and a civil date (`2009`, `2022-08`, `2026-06-06`) at
|
|
59
|
+
* the start of its year, month, or day in local time. `new Date` would put a
|
|
60
|
+
* civil date at midnight UTC, which west of Greenwich is the day, month, or
|
|
61
|
+
* year before.
|
|
62
|
+
*/
|
|
63
|
+
export declare function occurrenceTime(value: string | undefined): Date | undefined;
|
|
56
64
|
//# sourceMappingURL=fields.d.ts.map
|
|
@@ -107,6 +107,23 @@ export function parseCoarseDate(raw) {
|
|
|
107
107
|
const month = MONTHS[match[1].slice(0, 3).toLowerCase()];
|
|
108
108
|
return month ? `${match[2]}-${month}` : undefined;
|
|
109
109
|
}
|
|
110
|
+
/**
|
|
111
|
+
* A date one of these readers returned, as the moment a survey counts it at:
|
|
112
|
+
* an instant as itself, and a civil date (`2009`, `2022-08`, `2026-06-06`) at
|
|
113
|
+
* the start of its year, month, or day in local time. `new Date` would put a
|
|
114
|
+
* civil date at midnight UTC, which west of Greenwich is the day, month, or
|
|
115
|
+
* year before.
|
|
116
|
+
*/
|
|
117
|
+
export function occurrenceTime(value) {
|
|
118
|
+
if (!value)
|
|
119
|
+
return undefined;
|
|
120
|
+
const civil = /^(\d{4})(?:-(\d{2})(?:-(\d{2}))?)?$/.exec(value);
|
|
121
|
+
if (civil) {
|
|
122
|
+
return new Date(Number(civil[1]), Number(civil[2] ?? 1) - 1, Number(civil[3] ?? 1));
|
|
123
|
+
}
|
|
124
|
+
const date = new Date(value);
|
|
125
|
+
return Number.isNaN(date.getTime()) ? undefined : date;
|
|
126
|
+
}
|
|
110
127
|
const MONTHS = {
|
|
111
128
|
jan: '01',
|
|
112
129
|
feb: '02',
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@chronicle.app/linkedin",
|
|
3
|
-
"version": "0.
|
|
3
|
+
"version": "0.5.0",
|
|
4
4
|
"main": "./dist/index.js",
|
|
5
5
|
"types": "./dist/index.d.ts",
|
|
6
6
|
"files": [
|
|
@@ -22,9 +22,9 @@
|
|
|
22
22
|
"type": "module",
|
|
23
23
|
"description": "Chronicle plugin for the LinkedIn member data export archive",
|
|
24
24
|
"devDependencies": {
|
|
25
|
-
"@chronicle.app/etl": "0.
|
|
26
|
-
"@chronicle.app/schema": "0.
|
|
27
|
-
"@chronicle.app/tsconfig": "0.
|
|
25
|
+
"@chronicle.app/etl": "0.5.0",
|
|
26
|
+
"@chronicle.app/schema": "0.5.0",
|
|
27
|
+
"@chronicle.app/tsconfig": "0.5.0",
|
|
28
28
|
"@types/node": "^22.10.1",
|
|
29
29
|
"typescript": "^5.7.2"
|
|
30
30
|
},
|
|
@@ -62,6 +62,14 @@
|
|
|
62
62
|
"strategies": {
|
|
63
63
|
"archive": {
|
|
64
64
|
"delivery": "export",
|
|
65
|
+
"layouts": [
|
|
66
|
+
{
|
|
67
|
+
"files": [
|
|
68
|
+
"Connections.csv",
|
|
69
|
+
"Profile.csv"
|
|
70
|
+
]
|
|
71
|
+
}
|
|
72
|
+
],
|
|
65
73
|
"recordTypes": [
|
|
66
74
|
"connections",
|
|
67
75
|
"education",
|