cvrvaelger 0.1.0 → 0.2.1
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.
- checksums.yaml +4 -4
- data/.env.example +2 -0
- data/.rubocop.yml +2 -2
- data/CHANGELOG.md +16 -0
- data/README.md +35 -2
- data/cvrvaelger.gemspec +3 -3
- data/lib/cvrvaelger/client.rb +116 -9
- data/lib/cvrvaelger/client_events.rb +127 -0
- data/lib/cvrvaelger/event.rb +62 -0
- data/lib/cvrvaelger/file_download.rb +45 -0
- data/lib/cvrvaelger/version.rb +1 -1
- data/lib/cvrvaelger.rb +4 -1
- metadata +9 -6
checksums.yaml
CHANGED
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
---
|
|
2
2
|
SHA256:
|
|
3
|
-
metadata.gz:
|
|
4
|
-
data.tar.gz:
|
|
3
|
+
metadata.gz: 452cfeb8f40727f88b1e30b0fb8a7269e17b8866d2778f1b2330f948d8665e1e
|
|
4
|
+
data.tar.gz: 4e5b72d751fa0553096b8a90187b453141af5730ef1ab7f8b71f5194614fcb14
|
|
5
5
|
SHA512:
|
|
6
|
-
metadata.gz:
|
|
7
|
-
data.tar.gz:
|
|
6
|
+
metadata.gz: b838022a39993b3ffa491c1eb70ed82ab6cd3a4ca7c7e4e251b80873b4ef0f6fded509c86646a9af690f9e8714b5c06dc86d35bc29479eb9b37ef780648d79e0
|
|
7
|
+
data.tar.gz: 8f96d799f99c67083ef103df5591e1a12308cafb0d7cfbd698502e9e21bfadfc1e29f6a0dc7a2ccfeac8eb6f9c074792a76b2cc33672e0e6c89edbf18bc90379
|
data/.env.example
CHANGED
data/.rubocop.yml
CHANGED
data/CHANGELOG.md
CHANGED
|
@@ -1,5 +1,21 @@
|
|
|
1
1
|
# Changelog
|
|
2
2
|
|
|
3
|
+
## Unreleased
|
|
4
|
+
|
|
5
|
+
## 0.2.1
|
|
6
|
+
|
|
7
|
+
- Treat `Errno::ECONNRESET` as `ProviderError` ("unreachable") alongside
|
|
8
|
+
open/read timeouts and connection refused — Datafordeler TLS resets during
|
|
9
|
+
GraphQL and file download no longer bubble as raw syscall errors.
|
|
10
|
+
|
|
11
|
+
## 0.2.0
|
|
12
|
+
|
|
13
|
+
- Fildownload client: `available_file_downloads`, `download_file`, `download_latest`
|
|
14
|
+
for CVR totalextract zips.
|
|
15
|
+
- CVR_Events pull/poll: `events` (cursor via `since_event_id` / `after`),
|
|
16
|
+
`virksomhed_by_row_id` / `navn_by_row_id` / `adressering_by_row_id`, and
|
|
17
|
+
`register_import_status` for incremental sync after a TotalDownload baseline.
|
|
18
|
+
|
|
3
19
|
## 0.1.0
|
|
4
20
|
|
|
5
21
|
- Initial public release: Datafordeler CVR GraphQL v2 client (`lookup` / `search`).
|
data/README.md
CHANGED
|
@@ -1,10 +1,13 @@
|
|
|
1
1
|
# Cvrvaelger
|
|
2
2
|
|
|
3
3
|
Ruby client for **Det Centrale Virksomhedsregister (CVR)** via
|
|
4
|
-
[Datafordeler GraphQL](https://datafordeler.dk/dataoversigt/det-centrale-virksomhedsregister-cvr/cvr-graphql/)
|
|
4
|
+
[Datafordeler GraphQL](https://datafordeler.dk/dataoversigt/det-centrale-virksomhedsregister-cvr/cvr-graphql/)
|
|
5
|
+
and [Fildownload](https://datafordeler.dk/dataoversigt/det-centrale-virksomhedsregister-cvr/cvr-fildownload/).
|
|
5
6
|
|
|
6
7
|
Denmark only — look up an 8-digit CVR or registered company name and return
|
|
7
|
-
picker-shaped fields (name, light address, status).
|
|
8
|
+
picker-shaped fields (name, light address, status). Also supports Datafordeler
|
|
9
|
+
CVR Fildownload (list/download totalextract zips) and `CVR_Events` pull/poll for
|
|
10
|
+
incremental sync.
|
|
8
11
|
|
|
9
12
|
## Install
|
|
10
13
|
|
|
@@ -44,8 +47,35 @@ client.search("lego") # => []
|
|
|
44
47
|
|
|
45
48
|
# Optional bitemporal cut-off (GraphQL `virkningstid`, default: now)
|
|
46
49
|
client.lookup("47458714", at: Time.utc(2020, 1, 1))
|
|
50
|
+
|
|
51
|
+
# Fildownload: list metadata and stream a zip to disk
|
|
52
|
+
files = client.available_file_downloads(entity: "Navn", type_of_data: "Current", format: "json")
|
|
53
|
+
client.download_latest(entity: "Virksomhed", type: "current", format: "json", to: "tmp/virksomhed.zip")
|
|
54
|
+
client.download_file(files.first.file_name, to: "tmp/navn.zip")
|
|
55
|
+
|
|
56
|
+
# Incremental sync after a TotalDownload baseline (persist last event_id yourself).
|
|
57
|
+
# See Datafordeler transitions guide for entitetsbaserede hændelser.
|
|
58
|
+
status = client.register_import_status
|
|
59
|
+
page = client.events(since_event_id: last_id, first: 100)
|
|
60
|
+
page.events.each do |event|
|
|
61
|
+
# from_failed_import marks duplicate catch-up events — handle in the host app.
|
|
62
|
+
next if event.event_action == "d" # delete local row for event.object_datafordeler_row_id
|
|
63
|
+
|
|
64
|
+
case event.entity_name
|
|
65
|
+
when "Virksomhed"
|
|
66
|
+
client.virksomhed_by_row_id(event.object_datafordeler_row_id) # => Hash or nil
|
|
67
|
+
when "Navn"
|
|
68
|
+
client.navn_by_row_id(event.object_datafordeler_row_id)
|
|
69
|
+
when "Adressering"
|
|
70
|
+
client.adressering_by_row_id(event.object_datafordeler_row_id)
|
|
71
|
+
# other entities: query GraphQL yourself
|
|
72
|
+
end
|
|
73
|
+
end
|
|
74
|
+
page = client.events(since_event_id: last_id, first: 100, after: page.end_cursor) if page.has_next_page
|
|
47
75
|
```
|
|
48
76
|
|
|
77
|
+
SSE / GraphQL subscriptions are not wrapped here; poll with `events` instead.
|
|
78
|
+
|
|
49
79
|
Environment variables:
|
|
50
80
|
|
|
51
81
|
| Variable | Purpose |
|
|
@@ -53,6 +83,7 @@ Environment variables:
|
|
|
53
83
|
| `CVRVAELGER_API_KEY` | Datafordeler API key (preferred) |
|
|
54
84
|
| `DATAFORDELER_API_KEY` | Accepted alias |
|
|
55
85
|
| `CVRVAELGER_BASE_URL` | Override GraphQL endpoint (default `https://graphql.datafordeler.dk/CVR/v2`) |
|
|
86
|
+
| `CVRVAELGER_FILE_DOWNLOAD_BASE_URL` | Override Fildownload host (default `https://api.datafordeler.dk`) |
|
|
56
87
|
|
|
57
88
|
## API key
|
|
58
89
|
|
|
@@ -75,6 +106,8 @@ To refresh cassettes locally:
|
|
|
75
106
|
```sh
|
|
76
107
|
VCR_RECORD=all bundle exec rake test TEST=test/client_live_test.rb
|
|
77
108
|
VCR_RECORD=all bundle exec rake test TEST=test/graphql_features_test.rb
|
|
109
|
+
VCR_RECORD=all bundle exec rake test TEST=test/file_download_live_test.rb
|
|
110
|
+
VCR_RECORD=all bundle exec rake test TEST=test/client_events_live_test.rb
|
|
78
111
|
```
|
|
79
112
|
|
|
80
113
|
Never hand-write cassette response bodies.
|
data/cvrvaelger.gemspec
CHANGED
|
@@ -10,9 +10,9 @@ Gem::Specification.new do |spec|
|
|
|
10
10
|
|
|
11
11
|
spec.summary = "Ruby client for Danish CVR company lookup via Datafordeler GraphQL."
|
|
12
12
|
spec.description = <<~DESC
|
|
13
|
-
HTTP client for Det Centrale Virksomhedsregister (CVR) on Datafordeler
|
|
14
|
-
|
|
15
|
-
|
|
13
|
+
HTTP client for Det Centrale Virksomhedsregister (CVR) on Datafordeler:
|
|
14
|
+
GraphQL lookup/search for picker UX, Fildownload helpers for totalextract
|
|
15
|
+
zips, and CVR_Events pull/poll for incremental sync. No Rails dependency.
|
|
16
16
|
DESC
|
|
17
17
|
spec.homepage = "https://github.com/bergholdt/cvrvaelger"
|
|
18
18
|
spec.license = "MIT"
|
data/lib/cvrvaelger/client.rb
CHANGED
|
@@ -5,15 +5,24 @@ require "json"
|
|
|
5
5
|
require "uri"
|
|
6
6
|
|
|
7
7
|
require_relative "company"
|
|
8
|
+
require_relative "file_download"
|
|
9
|
+
require_relative "event"
|
|
10
|
+
require_relative "client_events"
|
|
8
11
|
|
|
9
12
|
module Cvrvaelger
|
|
10
|
-
# HTTP client for Det Centrale Virksomhedsregister via Datafordeler GraphQL
|
|
13
|
+
# HTTP client for Det Centrale Virksomhedsregister via Datafordeler GraphQL
|
|
14
|
+
# and Fildownload.
|
|
11
15
|
#
|
|
12
|
-
#
|
|
16
|
+
# GraphQL: https://graphql.datafordeler.dk/CVR/v2
|
|
17
|
+
# Files: https://api.datafordeler.dk/FileDownloads/...
|
|
13
18
|
# Auth: free API key from Datafordeler Administration (query param `apiKey`).
|
|
14
19
|
# No demo/default secret — configure CVRVAELGER_API_KEY or pass api_key:.
|
|
20
|
+
#
|
|
15
21
|
class Client
|
|
22
|
+
include ClientEvents
|
|
23
|
+
|
|
16
24
|
DEFAULT_BASE_URL = "https://graphql.datafordeler.dk/CVR/v2"
|
|
25
|
+
DEFAULT_FILE_DOWNLOAD_BASE_URL = "https://api.datafordeler.dk"
|
|
17
26
|
|
|
18
27
|
# Datafordeler allows one root field per query. Name match is exact (`eq`);
|
|
19
28
|
# there is no contains/fuzzy operator. `virkningstid` selects the bitemporal row.
|
|
@@ -63,12 +72,15 @@ module Cvrvaelger
|
|
|
63
72
|
}
|
|
64
73
|
GRAPHQL
|
|
65
74
|
|
|
66
|
-
attr_reader :base_url, :api_key
|
|
75
|
+
attr_reader :base_url, :file_download_base_url, :api_key
|
|
67
76
|
|
|
68
|
-
def initialize(api_key: nil, base_url: nil, http: nil)
|
|
77
|
+
def initialize(api_key: nil, base_url: nil, file_download_base_url: nil, http: nil)
|
|
69
78
|
@api_key = present_string(api_key) || present_string(ENV.fetch("CVRVAELGER_API_KEY", nil)) ||
|
|
70
79
|
present_string(ENV.fetch("DATAFORDELER_API_KEY", nil))
|
|
71
80
|
@base_url = present_string(base_url) || present_string(ENV.fetch("CVRVAELGER_BASE_URL", nil)) || DEFAULT_BASE_URL
|
|
81
|
+
@file_download_base_url = present_string(file_download_base_url) ||
|
|
82
|
+
present_string(ENV.fetch("CVRVAELGER_FILE_DOWNLOAD_BASE_URL", nil)) ||
|
|
83
|
+
DEFAULT_FILE_DOWNLOAD_BASE_URL
|
|
72
84
|
@http = http
|
|
73
85
|
end
|
|
74
86
|
|
|
@@ -106,6 +118,51 @@ module Cvrvaelger
|
|
|
106
118
|
end
|
|
107
119
|
end
|
|
108
120
|
|
|
121
|
+
# List available CVR totaldownload zips (metadata only).
|
|
122
|
+
# Optional filters: entity ("Navn"), type_of_data ("Current"), format ("json").
|
|
123
|
+
def available_file_downloads(entity: nil, type_of_data: nil, format: nil)
|
|
124
|
+
ensure_api_key!
|
|
125
|
+
payload = get_json("/FileDownloads/GetAvailableFileDownloads", Register: "CVR")
|
|
126
|
+
rows = Array(payload["availableFileDownloads"])
|
|
127
|
+
rows.filter_map do |row|
|
|
128
|
+
next unless row.is_a?(Hash)
|
|
129
|
+
|
|
130
|
+
file = FileDownload.from_node(row)
|
|
131
|
+
next if entity && !entity.to_s.casecmp?(file.entity_name.to_s)
|
|
132
|
+
next if type_of_data && !type_of_data.to_s.casecmp?(file.type_of_data.to_s)
|
|
133
|
+
next if format && !format.to_s.casecmp?(file.contained_file_format.to_s)
|
|
134
|
+
|
|
135
|
+
file
|
|
136
|
+
end
|
|
137
|
+
end
|
|
138
|
+
|
|
139
|
+
# Download a named zip to `to` (filesystem path). Returns the path.
|
|
140
|
+
def download_file(file_name, to:)
|
|
141
|
+
ensure_api_key!
|
|
142
|
+
raise ArgumentError, "to: path is required" if blank?(to)
|
|
143
|
+
raise ArgumentError, "file_name is required" if blank?(file_name)
|
|
144
|
+
|
|
145
|
+
get_file({ Filename: file_name.to_s }, to: to.to_s)
|
|
146
|
+
end
|
|
147
|
+
|
|
148
|
+
# Download the latest totaldownload for an entity (e.g. Navn / Virksomhed).
|
|
149
|
+
# `type` is Current/Temporal/Bitemporal; `format` is json/csv.
|
|
150
|
+
def download_latest(entity:, to:, type: "current", format: "json")
|
|
151
|
+
ensure_api_key!
|
|
152
|
+
raise ArgumentError, "entity is required" if blank?(entity)
|
|
153
|
+
raise ArgumentError, "to: path is required" if blank?(to)
|
|
154
|
+
|
|
155
|
+
get_file(
|
|
156
|
+
{
|
|
157
|
+
Register: "CVR",
|
|
158
|
+
LatestTotalForEntity: entity.to_s,
|
|
159
|
+
type: type.to_s.downcase,
|
|
160
|
+
format: format.to_s.upcase
|
|
161
|
+
},
|
|
162
|
+
to: to.to_s
|
|
163
|
+
)
|
|
164
|
+
end
|
|
165
|
+
|
|
109
166
|
private
|
|
110
167
|
|
|
111
168
|
attr_reader :http
|
|
@@ -180,10 +237,7 @@ module Cvrvaelger
|
|
|
180
237
|
end
|
|
181
238
|
|
|
182
239
|
def post_graphql(query, **variables)
|
|
183
|
-
uri = URI(base_url)
|
|
184
|
-
query_params = URI.decode_www_form(uri.query.to_s).to_h
|
|
185
|
-
query_params["apiKey"] = api_key
|
|
186
|
-
uri.query = URI.encode_www_form(query_params)
|
|
240
|
+
uri = with_api_key(URI(base_url))
|
|
187
241
|
|
|
188
242
|
body = JSON.generate(query: query, variables: variables)
|
|
189
243
|
request = Net::HTTP::Post.new(uri)
|
|
@@ -209,6 +263,59 @@ module Cvrvaelger
|
|
|
209
263
|
payload
|
|
210
264
|
end
|
|
211
265
|
|
|
266
|
+
def get_json(path, **params)
|
|
267
|
+
uri = with_api_key(URI.join("#{file_download_base_url}/", path.delete_prefix("/")), **params)
|
|
268
|
+
request = Net::HTTP::Get.new(uri)
|
|
269
|
+
request["Accept"] = "application/json"
|
|
270
|
+
request["User-Agent"] = "Cvrvaelger/#{VERSION}"
|
|
271
|
+
|
|
272
|
+
response = perform(uri, request)
|
|
273
|
+
raise ProviderError, "Datafordeler CVR error (#{response.code})" unless response.is_a?(Net::HTTPSuccess)
|
|
274
|
+
|
|
275
|
+
begin
|
|
276
|
+
JSON.parse(response.body)
|
|
277
|
+
rescue JSON::ParserError
|
|
278
|
+
raise ProviderError, "Datafordeler CVR returned invalid JSON"
|
|
279
|
+
end
|
|
280
|
+
end
|
|
281
|
+
|
|
282
|
+
def get_file(params, to:)
|
|
283
|
+
uri = with_api_key(URI.join("#{file_download_base_url}/", "FileDownloads/GetFile"), **params)
|
|
284
|
+
request = Net::HTTP::Get.new(uri)
|
|
285
|
+
request["Accept"] = "application/zip, application/octet-stream, */*"
|
|
286
|
+
request["User-Agent"] = "Cvrvaelger/#{VERSION}"
|
|
287
|
+
|
|
288
|
+
if http
|
|
289
|
+
response = http.call(uri, request)
|
|
290
|
+
raise ProviderError, "Datafordeler CVR error (#{response.code})" unless response.is_a?(Net::HTTPSuccess)
|
|
291
|
+
|
|
292
|
+
File.binwrite(to, response.body)
|
|
293
|
+
return to
|
|
294
|
+
end
|
|
295
|
+
|
|
296
|
+
File.open(to, "wb") do |io|
|
|
297
|
+
Net::HTTP.start(uri.host, uri.port, use_ssl: uri.scheme == "https", open_timeout: 10,
|
|
298
|
+
read_timeout: 600) do |client|
|
|
299
|
+
client.request(request) do |response|
|
|
300
|
+
raise ProviderError, "Datafordeler CVR error (#{response.code})" unless response.is_a?(Net::HTTPSuccess)
|
|
301
|
+
|
|
302
|
+
response.read_body { |chunk| io.write(chunk) }
|
|
303
|
+
end
|
|
304
|
+
end
|
|
305
|
+
end
|
|
306
|
+
to
|
|
307
|
+
rescue Net::OpenTimeout, Net::ReadTimeout, Errno::ECONNREFUSED, Errno::ECONNRESET, SocketError => e
|
|
308
|
+
raise ProviderError, "Datafordeler CVR unreachable (#{e.class})"
|
|
309
|
+
end
|
|
310
|
+
|
|
311
|
+
def with_api_key(uri, **params)
|
|
312
|
+
query_params = URI.decode_www_form(uri.query.to_s).to_h
|
|
313
|
+
params.each { |key, value| query_params[key.to_s] = value unless value.nil? }
|
|
314
|
+
query_params["apiKey"] = api_key
|
|
315
|
+
uri.query = URI.encode_www_form(query_params)
|
|
316
|
+
uri
|
|
317
|
+
end
|
|
318
|
+
|
|
212
319
|
def perform(uri, request)
|
|
213
320
|
return http.call(uri, request) if http
|
|
214
321
|
|
|
@@ -216,7 +323,7 @@ module Cvrvaelger
|
|
|
216
323
|
read_timeout: 30) do |client|
|
|
217
324
|
client.request(request)
|
|
218
325
|
end
|
|
219
|
-
rescue Net::OpenTimeout, Net::ReadTimeout, Errno::ECONNREFUSED, SocketError => e
|
|
326
|
+
rescue Net::OpenTimeout, Net::ReadTimeout, Errno::ECONNREFUSED, Errno::ECONNRESET, SocketError => e
|
|
220
327
|
raise ProviderError, "Datafordeler CVR unreachable (#{e.class})"
|
|
221
328
|
end
|
|
222
329
|
|
|
@@ -0,0 +1,127 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
module Cvrvaelger
|
|
4
|
+
# Pull/poll helpers for CVR_Events and row-id entity fetch (incremental sync).
|
|
5
|
+
module ClientEvents
|
|
6
|
+
EVENT_NODE_FIELDS = <<~GRAPHQL
|
|
7
|
+
eventid
|
|
8
|
+
entityname
|
|
9
|
+
eventaction
|
|
10
|
+
fromfailedimport
|
|
11
|
+
object_id
|
|
12
|
+
object_datafordelerRowId
|
|
13
|
+
datafordelerOpdateringstid
|
|
14
|
+
datafordelerRegisterImportSequenceNumber
|
|
15
|
+
GRAPHQL
|
|
16
|
+
|
|
17
|
+
EVENTS_QUERY = <<~GRAPHQL.freeze
|
|
18
|
+
query Events($first: Int!, $after: String) {
|
|
19
|
+
CVR_Events(first: $first, after: $after) {
|
|
20
|
+
pageInfo { hasNextPage endCursor }
|
|
21
|
+
nodes {
|
|
22
|
+
#{EVENT_NODE_FIELDS}
|
|
23
|
+
}
|
|
24
|
+
}
|
|
25
|
+
}
|
|
26
|
+
GRAPHQL
|
|
27
|
+
|
|
28
|
+
EVENTS_SINCE_QUERY = <<~GRAPHQL.freeze
|
|
29
|
+
query EventsSince($first: Int!, $after: String, $where: CVR_EventsFilterInput) {
|
|
30
|
+
CVR_Events(first: $first, after: $after, where: $where) {
|
|
31
|
+
pageInfo { hasNextPage endCursor }
|
|
32
|
+
nodes {
|
|
33
|
+
#{EVENT_NODE_FIELDS}
|
|
34
|
+
}
|
|
35
|
+
}
|
|
36
|
+
}
|
|
37
|
+
GRAPHQL
|
|
38
|
+
|
|
39
|
+
VIRKSOMHED_BY_ROW_ID_QUERY = <<~GRAPHQL
|
|
40
|
+
query VirksomhedByRowId($rowId: UUID!, $at: DafDateTime!) {
|
|
41
|
+
CVR_Virksomhed(first: 1, virkningstid: $at, where: { datafordelerRowId: { eq: $rowId } }) {
|
|
42
|
+
nodes { id CVRNummer status datafordelerRowId }
|
|
43
|
+
}
|
|
44
|
+
}
|
|
45
|
+
GRAPHQL
|
|
46
|
+
|
|
47
|
+
NAVN_BY_ROW_ID_QUERY = <<~GRAPHQL
|
|
48
|
+
query NavnByRowId($rowId: UUID!, $at: DafDateTime!) {
|
|
49
|
+
CVR_Navn(first: 1, virkningstid: $at, where: { datafordelerRowId: { eq: $rowId } }) {
|
|
50
|
+
nodes { CVREnhedsId vaerdi datafordelerRowId }
|
|
51
|
+
}
|
|
52
|
+
}
|
|
53
|
+
GRAPHQL
|
|
54
|
+
|
|
55
|
+
ADRESSERING_BY_ROW_ID_QUERY = <<~GRAPHQL
|
|
56
|
+
query AdresseringByRowId($rowId: UUID!, $at: DafDateTime!) {
|
|
57
|
+
CVR_Adressering(first: 1, virkningstid: $at, where: { datafordelerRowId: { eq: $rowId } }) {
|
|
58
|
+
nodes {
|
|
59
|
+
CVREnhedsId
|
|
60
|
+
AdresseringAnvendelse
|
|
61
|
+
CVRAdresse_vejnavn
|
|
62
|
+
CVRAdresse_husnummerFra
|
|
63
|
+
CVRAdresse_postnummer
|
|
64
|
+
CVRAdresse_postdistrikt
|
|
65
|
+
datafordelerRowId
|
|
66
|
+
}
|
|
67
|
+
}
|
|
68
|
+
}
|
|
69
|
+
GRAPHQL
|
|
70
|
+
|
|
71
|
+
REGISTER_IMPORT_STATUS_QUERY = <<~GRAPHQL
|
|
72
|
+
query RegisterImportStatus {
|
|
73
|
+
DAF_RegisterImportStatus { lastEventId lastSequenceNumber lastUpdated }
|
|
74
|
+
}
|
|
75
|
+
GRAPHQL
|
|
76
|
+
|
|
77
|
+
# Page CVR_Events since a durable cursor (`eventid`).
|
|
78
|
+
# `since_event_id:` uses `gt` (exclusive); `inclusive: true` uses `gte`.
|
|
79
|
+
def events(since_event_id: nil, first: 100, after: nil, inclusive: false)
|
|
80
|
+
ensure_api_key!
|
|
81
|
+
variables = { first: first.to_i, after: after }
|
|
82
|
+
if since_event_id
|
|
83
|
+
op = inclusive ? "gte" : "gt"
|
|
84
|
+
variables[:where] = { "eventid" => { op => since_event_id.to_i } }
|
|
85
|
+
payload = post_graphql(EVENTS_SINCE_QUERY, **variables)
|
|
86
|
+
else
|
|
87
|
+
payload = post_graphql(EVENTS_QUERY, **variables)
|
|
88
|
+
end
|
|
89
|
+
|
|
90
|
+
EventPage.from_connection(payload.dig("data", "CVR_Events"))
|
|
91
|
+
end
|
|
92
|
+
|
|
93
|
+
# Fetch a Virksomhed GraphQL node by datafordelerRowId, or nil.
|
|
94
|
+
def virksomhed_by_row_id(row_id, at: Time.now)
|
|
95
|
+
entity_by_row_id(VIRKSOMHED_BY_ROW_ID_QUERY, row_id, at: at)
|
|
96
|
+
end
|
|
97
|
+
|
|
98
|
+
# Fetch a Navn GraphQL node by datafordelerRowId, or nil.
|
|
99
|
+
def navn_by_row_id(row_id, at: Time.now)
|
|
100
|
+
entity_by_row_id(NAVN_BY_ROW_ID_QUERY, row_id, at: at)
|
|
101
|
+
end
|
|
102
|
+
|
|
103
|
+
# Fetch an Adressering GraphQL node by datafordelerRowId, or nil.
|
|
104
|
+
def adressering_by_row_id(row_id, at: Time.now)
|
|
105
|
+
entity_by_row_id(ADRESSERING_BY_ROW_ID_QUERY, row_id, at: at)
|
|
106
|
+
end
|
|
107
|
+
|
|
108
|
+
# Latest import watermark from Datafordeler (bootstrap / catch-up).
|
|
109
|
+
def register_import_status
|
|
110
|
+
ensure_api_key!
|
|
111
|
+
payload = post_graphql(REGISTER_IMPORT_STATUS_QUERY)
|
|
112
|
+
status = payload.dig("data", "DAF_RegisterImportStatus")
|
|
113
|
+
status.is_a?(Hash) ? status : {}
|
|
114
|
+
end
|
|
115
|
+
|
|
116
|
+
private
|
|
117
|
+
|
|
118
|
+
def entity_by_row_id(query, row_id, at:)
|
|
119
|
+
ensure_api_key!
|
|
120
|
+
raise ArgumentError, "row_id is required" if blank?(row_id)
|
|
121
|
+
|
|
122
|
+
payload = post_graphql(query, rowId: row_id.to_s, at: format_time(at))
|
|
123
|
+
node = first_node(payload)
|
|
124
|
+
node.is_a?(Hash) ? node : nil
|
|
125
|
+
end
|
|
126
|
+
end
|
|
127
|
+
end
|
|
@@ -0,0 +1,62 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
module Cvrvaelger
|
|
4
|
+
# One CVR_Events row from Datafordeler GraphQL (pull/poll sync cursor).
|
|
5
|
+
Event = Data.define(
|
|
6
|
+
:event_id,
|
|
7
|
+
:entity_name,
|
|
8
|
+
:event_action,
|
|
9
|
+
:from_failed_import,
|
|
10
|
+
:object_entity_id,
|
|
11
|
+
:object_datafordeler_row_id,
|
|
12
|
+
:datafordeler_opdateringstid,
|
|
13
|
+
:datafordeler_register_import_sequence_number
|
|
14
|
+
) do
|
|
15
|
+
def self.from_node(node)
|
|
16
|
+
new(
|
|
17
|
+
event_id: node["eventid"],
|
|
18
|
+
entity_name: present_string(node["entityname"]),
|
|
19
|
+
event_action: present_string(node["eventaction"]),
|
|
20
|
+
from_failed_import: node["fromfailedimport"],
|
|
21
|
+
# GraphQL field object_id — not Ruby Object#object_id.
|
|
22
|
+
object_entity_id: present_string(node["object_id"]),
|
|
23
|
+
object_datafordeler_row_id: present_string(node["object_datafordelerRowId"]),
|
|
24
|
+
datafordeler_opdateringstid: present_string(node["datafordelerOpdateringstid"]),
|
|
25
|
+
datafordeler_register_import_sequence_number: node["datafordelerRegisterImportSequenceNumber"]
|
|
26
|
+
)
|
|
27
|
+
end
|
|
28
|
+
|
|
29
|
+
def self.present_string(value)
|
|
30
|
+
return nil if value.nil?
|
|
31
|
+
|
|
32
|
+
string = value.to_s.strip
|
|
33
|
+
string.empty? ? nil : string
|
|
34
|
+
end
|
|
35
|
+
|
|
36
|
+
private_class_method :present_string
|
|
37
|
+
end
|
|
38
|
+
|
|
39
|
+
# One page of CVR_Events plus GraphQL connection cursors.
|
|
40
|
+
EventPage = Data.define(:events, :has_next_page, :end_cursor) do
|
|
41
|
+
def self.from_connection(connection)
|
|
42
|
+
connection = {} unless connection.is_a?(Hash)
|
|
43
|
+
page_info = connection["pageInfo"]
|
|
44
|
+
page_info = {} unless page_info.is_a?(Hash)
|
|
45
|
+
|
|
46
|
+
new(
|
|
47
|
+
events: Array(connection["nodes"]).grep(Hash).map { |node| Event.from_node(node) },
|
|
48
|
+
has_next_page: page_info["hasNextPage"] == true,
|
|
49
|
+
end_cursor: present_string(page_info["endCursor"])
|
|
50
|
+
)
|
|
51
|
+
end
|
|
52
|
+
|
|
53
|
+
def self.present_string(value)
|
|
54
|
+
return nil if value.nil?
|
|
55
|
+
|
|
56
|
+
string = value.to_s.strip
|
|
57
|
+
string.empty? ? nil : string
|
|
58
|
+
end
|
|
59
|
+
|
|
60
|
+
private_class_method :present_string
|
|
61
|
+
end
|
|
62
|
+
end
|
|
@@ -0,0 +1,45 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
module Cvrvaelger
|
|
4
|
+
# Metadata for a Datafordeler CVR totaldownload zip (no local unzipping).
|
|
5
|
+
FileDownload = Data.define(
|
|
6
|
+
:file_name,
|
|
7
|
+
:register,
|
|
8
|
+
:entity_name,
|
|
9
|
+
:type_of_download,
|
|
10
|
+
:type_of_data,
|
|
11
|
+
:contained_file_format,
|
|
12
|
+
:output_file_format,
|
|
13
|
+
:generation_number,
|
|
14
|
+
:file_size_in_bytes,
|
|
15
|
+
:md5_hash,
|
|
16
|
+
:generation_time,
|
|
17
|
+
:expiration_date
|
|
18
|
+
) do
|
|
19
|
+
def self.from_node(node)
|
|
20
|
+
new(
|
|
21
|
+
file_name: present_string(node["fileName"]),
|
|
22
|
+
register: present_string(node["register"]),
|
|
23
|
+
entity_name: present_string(node["entityName"]),
|
|
24
|
+
type_of_download: present_string(node["typeOfDownload"]),
|
|
25
|
+
type_of_data: present_string(node["typeOfData"]),
|
|
26
|
+
contained_file_format: present_string(node["containedFileFormat"]),
|
|
27
|
+
output_file_format: present_string(node["outputFileFormat"]),
|
|
28
|
+
generation_number: node["generationNumber"],
|
|
29
|
+
file_size_in_bytes: node["fileSizeInBytes"],
|
|
30
|
+
md5_hash: present_string(node["md5Hash"]),
|
|
31
|
+
generation_time: present_string(node["generationTime"]),
|
|
32
|
+
expiration_date: present_string(node["expirationDate"])
|
|
33
|
+
)
|
|
34
|
+
end
|
|
35
|
+
|
|
36
|
+
def self.present_string(value)
|
|
37
|
+
return nil if value.nil?
|
|
38
|
+
|
|
39
|
+
string = value.to_s.strip
|
|
40
|
+
string.empty? ? nil : string
|
|
41
|
+
end
|
|
42
|
+
|
|
43
|
+
private_class_method :present_string
|
|
44
|
+
end
|
|
45
|
+
end
|
data/lib/cvrvaelger/version.rb
CHANGED
data/lib/cvrvaelger.rb
CHANGED
|
@@ -3,9 +3,12 @@
|
|
|
3
3
|
require_relative "cvrvaelger/version"
|
|
4
4
|
require_relative "cvrvaelger/error"
|
|
5
5
|
require_relative "cvrvaelger/company"
|
|
6
|
+
require_relative "cvrvaelger/file_download"
|
|
7
|
+
require_relative "cvrvaelger/event"
|
|
6
8
|
require_relative "cvrvaelger/client"
|
|
7
9
|
|
|
8
|
-
# Ruby client for Det Centrale Virksomhedsregister (CVR) via Datafordeler
|
|
10
|
+
# Ruby client for Det Centrale Virksomhedsregister (CVR) via Datafordeler
|
|
11
|
+
# GraphQL and Fildownload.
|
|
9
12
|
#
|
|
10
13
|
# Denmark only. Rails controllers, Stimulus pickers, and persistence belong in the
|
|
11
14
|
# host application (same split as adressevaelger).
|
metadata
CHANGED
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
--- !ruby/object:Gem::Specification
|
|
2
2
|
name: cvrvaelger
|
|
3
3
|
version: !ruby/object:Gem::Version
|
|
4
|
-
version: 0.1
|
|
4
|
+
version: 0.2.1
|
|
5
5
|
platform: ruby
|
|
6
6
|
authors:
|
|
7
7
|
- Rasmus Bergholdt
|
|
@@ -10,9 +10,9 @@ cert_chain: []
|
|
|
10
10
|
date: 1980-01-02 00:00:00.000000000 Z
|
|
11
11
|
dependencies: []
|
|
12
12
|
description: |
|
|
13
|
-
HTTP client for Det Centrale Virksomhedsregister (CVR) on Datafordeler
|
|
14
|
-
|
|
15
|
-
|
|
13
|
+
HTTP client for Det Centrale Virksomhedsregister (CVR) on Datafordeler:
|
|
14
|
+
GraphQL lookup/search for picker UX, Fildownload helpers for totalextract
|
|
15
|
+
zips, and CVR_Events pull/poll for incremental sync. No Rails dependency.
|
|
16
16
|
email:
|
|
17
17
|
- rasmus.bergholdt@gmail.com
|
|
18
18
|
executables: []
|
|
@@ -27,16 +27,19 @@ files:
|
|
|
27
27
|
- cvrvaelger.gemspec
|
|
28
28
|
- lib/cvrvaelger.rb
|
|
29
29
|
- lib/cvrvaelger/client.rb
|
|
30
|
+
- lib/cvrvaelger/client_events.rb
|
|
30
31
|
- lib/cvrvaelger/company.rb
|
|
31
32
|
- lib/cvrvaelger/error.rb
|
|
33
|
+
- lib/cvrvaelger/event.rb
|
|
34
|
+
- lib/cvrvaelger/file_download.rb
|
|
32
35
|
- lib/cvrvaelger/version.rb
|
|
33
36
|
homepage: https://github.com/bergholdt/cvrvaelger
|
|
34
37
|
licenses:
|
|
35
38
|
- MIT
|
|
36
39
|
metadata:
|
|
37
40
|
homepage_uri: https://github.com/bergholdt/cvrvaelger
|
|
38
|
-
source_code_uri: https://github.com/bergholdt/cvrvaelger/tree/v0.1
|
|
39
|
-
changelog_uri: https://github.com/bergholdt/cvrvaelger/blob/v0.1
|
|
41
|
+
source_code_uri: https://github.com/bergholdt/cvrvaelger/tree/v0.2.1
|
|
42
|
+
changelog_uri: https://github.com/bergholdt/cvrvaelger/blob/v0.2.1/CHANGELOG.md
|
|
40
43
|
bug_tracker_uri: https://github.com/bergholdt/cvrvaelger/issues
|
|
41
44
|
rubygems_mfa_required: 'true'
|
|
42
45
|
rdoc_options: []
|