@fin.cx/opendata 1.1.0 → 1.4.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.
@@ -0,0 +1,339 @@
1
+ import type { BusinessRecord } from './classes.businessrecord.js';
2
+ import type { OpenData } from './classes.main.opendata.js';
3
+ import * as plugins from './plugins.js';
4
+ import * as paths from './paths.js';
5
+
6
+ /**
7
+ * the HandlesRegister exposed as a class
8
+ */
9
+ export class HandelsRegister {
10
+ private openDataRef: OpenData;
11
+ private asyncExecutionStack = new plugins.lik.AsyncExecutionStack();
12
+ private uniqueDowloadFolder = plugins.path.join(paths.downloadDir, plugins.smartunique.uniSimple());
13
+
14
+ // Puppeteer wrapper instance
15
+ public smartbrowserInstance = new plugins.smartbrowser.SmartBrowser();
16
+
17
+ constructor(openDataRef: OpenData) {
18
+ this.openDataRef = openDataRef;
19
+ }
20
+
21
+ public async start() {
22
+ // Start the browser
23
+ await plugins.smartfile.fs.ensureDir(this.uniqueDowloadFolder);
24
+ await this.smartbrowserInstance.start();
25
+ }
26
+
27
+ public async stop() {
28
+ // Stop the browser
29
+ await plugins.smartfile.fs.remove(this.uniqueDowloadFolder);
30
+ await this.smartbrowserInstance.stop();
31
+ }
32
+
33
+ /**
34
+ * Creates a new page and configures it to allow file downloads
35
+ * to a predefined path.
36
+ */
37
+ public getNewPage = async () => {
38
+ const page = await this.smartbrowserInstance.headlessBrowser.newPage();
39
+
40
+ // 1) Create a DevTools session for this page
41
+ const cdpSession = await page.target().createCDPSession();
42
+
43
+ // 2) Allow file downloads and set the download path
44
+ await cdpSession.send('Page.setDownloadBehavior', {
45
+ behavior: 'allow',
46
+ downloadPath: this.uniqueDowloadFolder, // <-- Change this to your desired absolute path
47
+ });
48
+
49
+ // Optionally set viewport and go to page
50
+ await page.setViewport({ width: 1920, height: 1080 });
51
+ await page.goto('https://www.handelsregister.de/');
52
+ return page;
53
+ };
54
+
55
+ private navigateToPage = async (
56
+ pageArg: plugins.smartbrowser.smartpuppeteer.puppeteer.Page,
57
+ pageNameArg: string
58
+ ) => {
59
+ try {
60
+ await pageArg.evaluate((pageNameArg2) => {
61
+ const elements = Array.from(document.querySelectorAll('.ui-menuitem-text > span'));
62
+ const targetElement = elements.find((el) => el.textContent?.trim() === pageNameArg2);
63
+ if (targetElement) {
64
+ (targetElement as HTMLElement).click();
65
+ }
66
+ }, pageNameArg);
67
+ console.log(`Navigated to the ${pageNameArg} page successfully.`);
68
+ } catch (error) {
69
+ console.error(`Failed to navigate to the ${pageNameArg} page:`, error);
70
+ }
71
+ };
72
+
73
+ private waitForResults = async (pageArg: plugins.smartbrowser.smartpuppeteer.puppeteer.Page) => {
74
+ await pageArg
75
+ .waitForSelector('#ergebnissForm\\:selectedSuchErgebnisFormTable_data', {
76
+ timeout: 30000,
77
+ })
78
+ .catch(async (err) => {
79
+ await pageArg.screenshot({ path: paths.downloadDir + '/error.png' });
80
+ throw err;
81
+ });
82
+
83
+ const businessRecords: BusinessRecord['data'][] = await pageArg.evaluate(() => {
84
+ const rows = document.querySelectorAll(
85
+ '#ergebnissForm\\:selectedSuchErgebnisFormTable_data > tr'
86
+ );
87
+ const records: BusinessRecord['data'][] = [];
88
+
89
+ rows.forEach((row) => {
90
+ const nameElement = row.querySelector('td.ui-panelgrid-cell span.marginLeft20');
91
+ const cityElement = row.querySelector('td.ui-panelgrid-cell.sitzSuchErgebnisse span');
92
+ const statusElement = row.querySelector('td.ui-panelgrid-cell span.verticalText');
93
+ const registrationCourtElement = row.querySelector(
94
+ 'td.ui-panelgrid-cell.fontTableNameSize'
95
+ );
96
+
97
+ const name = nameElement?.textContent?.trim();
98
+ const city = cityElement?.textContent?.trim();
99
+ const status = statusElement?.textContent?.trim();
100
+ const registrationId = registrationCourtElement?.textContent?.trim();
101
+
102
+ // Push parsed data into records array
103
+ records.push({
104
+ name,
105
+ city,
106
+ registrationId,
107
+ businessType: status,
108
+ });
109
+ });
110
+
111
+ return records;
112
+ });
113
+ return businessRecords;
114
+ };
115
+
116
+ private clickFindButton = async (pageArg: plugins.smartbrowser.smartpuppeteer.puppeteer.Page) => {
117
+ try {
118
+ // Wait for the button with the text "Find" to appear
119
+ await pageArg.waitForSelector('span.ui-button-text.ui-c', { timeout: 5000 });
120
+
121
+ // adjust to 100 results per page
122
+ await pageArg.select('#form\\:ergebnisseProSeite_input', '100');
123
+
124
+ // Locate and click the button using its text
125
+ await pageArg.evaluate(() => {
126
+ const buttons = Array.from(document.querySelectorAll('span.ui-button-text.ui-c'));
127
+ const targetButton = buttons.find((button) => button.textContent?.trim() === 'Find');
128
+ if (targetButton) {
129
+ const parentButton = targetButton.closest('button') || targetButton;
130
+ (parentButton as HTMLElement).click();
131
+ }
132
+ });
133
+
134
+ console.log('Find button clicked successfully!');
135
+ } catch (error) {
136
+ console.error('Failed to find or click the "Find" button:', error);
137
+ }
138
+ };
139
+
140
+ private async downloadFile(
141
+ pageArg: plugins.smartbrowser.smartpuppeteer.puppeteer.Page,
142
+ typeArg: 'SI' | 'AD'
143
+ ) {
144
+ // Trigger the file download by clicking on the relevant link
145
+ await pageArg.evaluate((typeArg2) => {
146
+ // Locate the table body
147
+ const tableBody = document.querySelector(
148
+ '#ergebnissForm\\:selectedSuchErgebnisFormTable_data'
149
+ );
150
+ if (!tableBody) {
151
+ throw new Error('Table body not found');
152
+ }
153
+
154
+ // Locate the first row
155
+ const firstRow = tableBody.querySelector('tr:nth-child(1)');
156
+ if (!firstRow) {
157
+ throw new Error('First row not found');
158
+ }
159
+
160
+ // Locate the last cell in the first row
161
+ const lastCell = firstRow.querySelector('td:last-child');
162
+ if (!lastCell) {
163
+ throw new Error('Last cell not found in the first row');
164
+ }
165
+
166
+ // Locate the download links
167
+ const adLink = lastCell.querySelector('a:first-of-type');
168
+ const siLink = lastCell.querySelector('a:last-of-type');
169
+ if (!siLink) {
170
+ throw new Error('SI link not found in the last cell');
171
+ }
172
+
173
+ // Simulate a click on the last <a> element
174
+ switch (typeArg2) {
175
+ case 'AD':
176
+ (adLink as HTMLElement).click();
177
+ break;
178
+ case 'SI':
179
+ (siLink as HTMLElement).click();
180
+ break;
181
+ default:
182
+ throw new Error('Invalid file type');
183
+ }
184
+ }, typeArg);
185
+
186
+ // Wait a bit for the download to complete (you might want to implement
187
+ // a more robust file-exists check or a wait-for-download library)
188
+ await pageArg.waitForTimeout(10000);
189
+
190
+ const files = await plugins.smartfile.fs.fileTreeToObject(this.uniqueDowloadFolder, '**/*');
191
+ await plugins.smartfile.fs.ensureEmptyDir(this.uniqueDowloadFolder);
192
+
193
+ return files [0];
194
+ }
195
+
196
+ /**
197
+ * Helper method to parse the German registration string
198
+ */
199
+ private async parseGermanRegistration(
200
+ input: string
201
+ ): Promise<BusinessRecord['data']['germanParsedRegistration']> {
202
+ // e.g. District court Berlin (Charlottenburg) HRB 123456
203
+ const regex =
204
+ /District court (\p{L}[\p{L}\s-]*?(?:\s*\([\p{L}\s-]+\))?)\s+(HRA|HRB|GnR|VR|PR|GsR)\s+(\d+)/u;
205
+ const match = input.match(regex);
206
+
207
+ if (match) {
208
+ return {
209
+ court: match[1],
210
+ type: match[2] as 'HRA' | 'HRB', // Adjust if needed
211
+ number: match[3],
212
+ };
213
+ }
214
+ }
215
+
216
+ /**
217
+ * Search for a company by name and return basic info
218
+ */
219
+ public async searchCompany(companyNameArg: string) {
220
+ return this.asyncExecutionStack.getExclusiveExecutionSlot(async () => {
221
+ const page = await this.getNewPage();
222
+ await this.navigateToPage(page, 'Normal search');
223
+
224
+ try {
225
+ // Wait for the textarea to appear
226
+ await page.waitForSelector('#form\\:schlagwoerter', { timeout: 5000 });
227
+
228
+ // Enter text into the textarea
229
+ const inputText = companyNameArg;
230
+ await page.evaluate((text) => {
231
+ const textarea = document.querySelector<HTMLTextAreaElement>('#form\\:schlagwoerter');
232
+ if (textarea) {
233
+ textarea.value = text; // Set the value
234
+ // Trigger the change event manually if required
235
+ const event = new Event('change', { bubbles: true });
236
+ textarea.dispatchEvent(event);
237
+ }
238
+ }, inputText);
239
+
240
+ console.log('Text entered successfully!');
241
+ } catch (error) {
242
+ console.error('Failed to find or enter text into the textarea:', error);
243
+ }
244
+
245
+ try {
246
+ // Wait for the radio button's label to appear
247
+ await page.waitForSelector('label[for="form:schlagwortOptionen:0"]', { timeout: 5000 });
248
+
249
+ // Click the label to select the radio button
250
+ await page.evaluate(() => {
251
+ const label = document.querySelector<HTMLLabelElement>(
252
+ 'label[for="form:schlagwortOptionen:0"]'
253
+ );
254
+ if (label) {
255
+ label.click();
256
+ }
257
+ });
258
+
259
+ console.log('Radio button clicked successfully!');
260
+ } catch (error) {
261
+ console.error('Failed to find or click the radio button:', error);
262
+ }
263
+
264
+ await this.clickFindButton(page);
265
+
266
+ const businessRecords = await this.waitForResults(page);
267
+
268
+ // Parse out the registration info
269
+ for (const record of businessRecords) {
270
+ if (record.registrationId) {
271
+ record.germanParsedRegistration = await this.parseGermanRegistration(
272
+ record.registrationId
273
+ );
274
+ }
275
+ }
276
+
277
+ await page.close();
278
+ return businessRecords;
279
+ }, 60000);
280
+ }
281
+
282
+ /**
283
+ * Search for a specific company (known register type/number/court),
284
+ * then click on an element that triggers a file download.
285
+ */
286
+ public async getSpecificCompany(companyArg: BusinessRecord['data']['germanParsedRegistration']) {
287
+ return this.asyncExecutionStack.getExclusiveExecutionSlot(async () => {
288
+ const page = await this.getNewPage();
289
+ await this.navigateToPage(page, 'Normal search');
290
+ await page.waitForSelector('#form\\:schlagwoerter', { timeout: 5000 });
291
+
292
+ // 1) Type of Register (e.g. HRB, HRA, etc.)
293
+ await page.waitForSelector('#form\\:registerArt_label');
294
+ await page.click('#form\\:registerArt_label');
295
+ await page.waitForSelector('#form\\:registerArt_items');
296
+ await page.evaluate((type) => {
297
+ const options = Array.from(document.querySelectorAll('#form\\:registerArt_items li'));
298
+ const targetOption = options.find((option) => option.textContent?.trim() === type);
299
+ (targetOption as HTMLElement)?.click();
300
+ }, companyArg.type);
301
+
302
+ // 2) Register number
303
+ await page.waitForSelector('#form\\:registerNummer');
304
+ await page.type('#form\\:registerNummer', companyArg.number);
305
+
306
+ // 3) Register court
307
+ await page.waitForSelector('#form\\:registergericht_label');
308
+ await page.click('#form\\:registergericht_label');
309
+ await page.waitForSelector('#form\\:registergericht_items');
310
+ await page.evaluate((court) => {
311
+ const options = Array.from(document.querySelectorAll('#form\\:registergericht_items li'));
312
+ const targetOption = options.find((option) => option.textContent?.trim() === court);
313
+ (targetOption as HTMLElement)?.click();
314
+ }, companyArg.court);
315
+
316
+ // Click 'Find'
317
+ await this.clickFindButton(page);
318
+
319
+ // Optionally grab the results, just for logging
320
+ const businessRecords = await this.waitForResults(page);
321
+ console.log(businessRecords);
322
+
323
+ const files: plugins.smartfile.SmartFile[] = [];
324
+
325
+ // download files
326
+ files.push(await this.downloadFile(page, 'SI'));
327
+ files.push(await this.downloadFile(page, 'AD'));
328
+
329
+ // At this point, the file should have been downloaded automatically
330
+ // to the path specified by `Page.setDownloadBehavior`
331
+ await page.close();
332
+
333
+ return {
334
+ businessRecords,
335
+ files,
336
+ };
337
+ }, 60000);
338
+ }
339
+ }
@@ -2,28 +2,23 @@ import * as plugins from './plugins.js';
2
2
  import * as paths from './paths.js';
3
3
  import type { OpenData } from './classes.main.opendata.js';
4
4
 
5
- export class GermanBusinessData {
5
+ export class JsonlDataProcessor {
6
6
  public openDataRef: OpenData;
7
7
  constructor(openDataRefArg: OpenData) {
8
8
  this.openDataRef = openDataRefArg;
9
9
  }
10
10
 
11
- public async start() {
12
- await this.update();
13
- }
14
- public async stop() {}
15
-
16
- public async update() {
11
+ // TODO: define a mapper as argument instead of hard-coding it
12
+ public async processDataFromUrl(dataUrlArg = 'https://daten.offeneregister.de/de_companies_ocdata.jsonl.bz2') {
17
13
  const done = plugins.smartpromise.defer();
18
14
  const promiseArray: Promise<any>[] = [];
19
- const dataUrl = 'https://daten.offeneregister.de/de_companies_ocdata.jsonl.bz2';
20
15
  const dataExists = await plugins.smartfile.fs.isDirectory(paths.germanBusinessDataDir);
21
16
  if (!dataExists) {
22
17
  await plugins.smartfile.fs.ensureDir(paths.germanBusinessDataDir);
23
18
  } else {
24
19
  }
25
20
 
26
- const smartarchive = await plugins.smartarchive.SmartArchive.fromArchiveUrl(dataUrl);
21
+ const smartarchive = await plugins.smartarchive.SmartArchive.fromArchiveUrl(dataUrlArg);
27
22
  promiseArray
28
23
  .push
29
24
  // smartarchive.exportToFs(paths.germanBusinessDataDir, 'de_companies_ocdata.jsonl')
@@ -49,6 +44,8 @@ export class GermanBusinessData {
49
44
  if (!line) continue;
50
45
  try {
51
46
  entry = JSON.parse(line);
47
+ console.log(JSON.stringify(entry, null, 2));
48
+ process.exit(0);
52
49
  } catch (err) {
53
50
  console.log(line);
54
51
  await plugins.smartdelay.delayFor(10000);
@@ -57,12 +54,13 @@ export class GermanBusinessData {
57
54
  totalRecordsCounter++;
58
55
  if (totalRecordsCounter % 10000 === 0) console.log(`${totalRecordsCounter} total records.`);
59
56
  const businessRecord = new this.openDataRef.CBusinessRecord();
57
+ businessRecord.id = await this.openDataRef.CBusinessRecord.getNewId();
60
58
  businessRecord.data.name = entry.name;
61
59
  await businessRecord.save();
62
- // console.log(`stored ${businessRecord.data.name}`);
63
60
  }
64
61
  },
65
62
  finalFunction: async (streamToolsArg) => {
63
+ console.log(`finished processing ${totalRecordsCounter} records.`);
66
64
  if (!nextRest) return;
67
65
  JSON.parse(nextRest);
68
66
  }
@@ -1,13 +1,16 @@
1
1
  import { BusinessRecord } from './classes.businessrecord.js';
2
- import { GermanBusinessData } from './classes.germanbusinessdata.js';
2
+ import { HandelsRegister } from './classes.handelsregister.js';
3
+ import { JsonlDataProcessor } from './classes.jsonldata.js';
3
4
  import * as paths from './paths.js';
4
5
  import * as plugins from './plugins.js';
5
6
 
6
7
  export class OpenData {
7
- db: plugins.smartdata.SmartdataDb;
8
- germanBusinesses: GermanBusinessData;
8
+ public db: plugins.smartdata.SmartdataDb;
9
9
  private serviceQenv = new plugins.qenv.Qenv(paths.packageDir, paths.nogitDir);
10
10
 
11
+ public jsonLDataProcessor: JsonlDataProcessor;
12
+ public handelsregister: HandelsRegister;
13
+
11
14
  public CBusinessRecord = plugins.smartdata.setDefaultManagerForDoc(this, BusinessRecord);
12
15
 
13
16
  public async start() {
@@ -18,8 +21,17 @@ export class OpenData {
18
21
  mongoDbPass: await this.serviceQenv.getEnvVarOnDemand('MONGODB_PASS'),
19
22
  });
20
23
  await this.db.init();
21
- this.germanBusinesses = new GermanBusinessData(this);
22
- await this.germanBusinesses.start();
24
+ this.jsonLDataProcessor = new JsonlDataProcessor(this);
25
+ this.handelsregister = new HandelsRegister(this);
26
+ await this.handelsregister.start();
27
+ }
28
+
29
+ public async buildInitialDb() {
30
+ await this.jsonLDataProcessor.processDataFromUrl();
31
+ }
32
+
33
+ public async stop() {
34
+ await this.db.close();
35
+ await this.handelsregister.stop();
23
36
  }
24
- public async stop() {}
25
37
  }
package/ts/paths.ts CHANGED
@@ -8,4 +8,8 @@ export const packageDir = plugins.path.join(
8
8
  export const nogitDir = plugins.path.join(packageDir, './.nogit/');
9
9
  plugins.smartfile.fs.ensureDirSync(nogitDir);
10
10
 
11
+ export const downloadDir = plugins.path.join(nogitDir, 'downloads');
12
+ plugins.smartfile.fs.ensureDirSync(downloadDir);
13
+
14
+
11
15
  export const germanBusinessDataDir = plugins.path.join(nogitDir, 'germanbusinessdata');
package/ts/plugins.ts CHANGED
@@ -6,8 +6,10 @@ export {
6
6
  }
7
7
 
8
8
  // @push.rocks scope
9
+ import * as lik from '@push.rocks/lik';
9
10
  import * as qenv from '@push.rocks/qenv';
10
11
  import * as smartarchive from '@push.rocks/smartarchive';
12
+ import * as smartbrowser from '@push.rocks/smartbrowser';
11
13
  import * as smartdata from '@push.rocks/smartdata';
12
14
  import * as smartdelay from '@push.rocks/smartdelay';
13
15
  import * as smartfile from '@push.rocks/smartfile';
@@ -15,10 +17,13 @@ import * as smartpath from '@push.rocks/smartpath';
15
17
  import * as smartpromise from '@push.rocks/smartpromise';
16
18
  import * as smartrequest from '@push.rocks/smartrequest';
17
19
  import * as smartstream from '@push.rocks/smartstream';
20
+ import * as smartunique from '@push.rocks/smartunique';
18
21
 
19
22
  export {
23
+ lik,
20
24
  qenv,
21
25
  smartarchive,
26
+ smartbrowser,
22
27
  smartdata,
23
28
  smartdelay,
24
29
  smartfile,
@@ -26,4 +31,12 @@ export {
26
31
  smartpromise,
27
32
  smartrequest,
28
33
  smartstream,
29
- }
34
+ smartunique,
35
+ }
36
+
37
+ // @tsclass scope
38
+ import * as tsclass from '@tsclass/tsclass';
39
+
40
+ export {
41
+ tsclass,
42
+ }