@quenty/influxdbclient 7.37.0 → 7.39.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/CHANGELOG.md +10 -0
- package/package.json +2 -2
- package/src/Server/Config/InfluxDBClientConfigUtils.spec.lua +86 -0
- package/src/Server/Config/InfluxDBWriteOptionUtils.spec.lua +94 -0
- package/src/Server/InfluxDBClient.lua +45 -3
- package/src/Server/InfluxDBClient.spec.lua +227 -0
- package/src/Server/InfluxDBService.lua +147 -0
- package/src/Server/InfluxDBService.spec.lua +151 -0
- package/src/Server/Mocks/InfluxDBRequestHandlerMock.lua +132 -0
- package/src/Server/Mocks/InfluxDBRequestHandlerMock.spec.lua +107 -0
- package/src/Server/Utils/InfluxDBErrorUtils.spec.lua +69 -0
- package/src/Server/Write/InfluxDBWriteAPI.lua +21 -2
- package/src/Server/Write/InfluxDBWriteAPI.spec.lua +322 -0
- package/src/Server/Write/InfluxDBWriteBuffer.spec.lua +144 -0
- package/src/Shared/Config/InfluxDBPointSettings.spec.lua +87 -0
- package/src/Shared/Utils/InfluxDBEscapeUtils.spec.lua +70 -0
- package/src/Shared/Write/InfluxDBPoint.spec.lua +373 -0
package/CHANGELOG.md
CHANGED
|
@@ -3,6 +3,16 @@
|
|
|
3
3
|
All notable changes to this project will be documented in this file.
|
|
4
4
|
See [Conventional Commits](https://conventionalcommits.org) for commit guidelines.
|
|
5
5
|
|
|
6
|
+
# [7.39.0](https://github.com/Quenty/NevermoreEngine/compare/@quenty/influxdbclient@7.38.0...@quenty/influxdbclient@7.39.0) (2026-07-22)
|
|
7
|
+
|
|
8
|
+
### Features
|
|
9
|
+
|
|
10
|
+
- Add tests to InfluxDBClient package ([c2b138f](https://github.com/Quenty/NevermoreEngine/commit/c2b138f0c0ed050dd6cb3541f644b766b5db6b10))
|
|
11
|
+
|
|
12
|
+
# [7.38.0](https://github.com/Quenty/NevermoreEngine/compare/@quenty/influxdbclient@7.37.0...@quenty/influxdbclient@7.38.0) (2026-07-21)
|
|
13
|
+
|
|
14
|
+
**Note:** Version bump only for package @quenty/influxdbclient
|
|
15
|
+
|
|
6
16
|
# [7.37.0](https://github.com/Quenty/NevermoreEngine/compare/@quenty/influxdbclient@7.36.0...@quenty/influxdbclient@7.37.0) (2026-07-18)
|
|
7
17
|
|
|
8
18
|
### Bug Fixes
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@quenty/influxdbclient",
|
|
3
|
-
"version": "7.
|
|
3
|
+
"version": "7.39.0",
|
|
4
4
|
"description": "Provides a Roblox Lua InfluxDB client",
|
|
5
5
|
"keywords": [
|
|
6
6
|
"Roblox",
|
|
@@ -47,5 +47,5 @@
|
|
|
47
47
|
"publishConfig": {
|
|
48
48
|
"access": "public"
|
|
49
49
|
},
|
|
50
|
-
"gitHead": "
|
|
50
|
+
"gitHead": "93f15b200c0f465a0b5304fcf05f5cfe7fdcbd1d"
|
|
51
51
|
}
|
|
@@ -0,0 +1,86 @@
|
|
|
1
|
+
--!strict
|
|
2
|
+
--[[
|
|
3
|
+
@class InfluxDBClientConfigUtils.spec.lua
|
|
4
|
+
]]
|
|
5
|
+
|
|
6
|
+
local require = require(script.Parent.loader).load(script)
|
|
7
|
+
|
|
8
|
+
local InfluxDBClientConfigUtils = require("InfluxDBClientConfigUtils")
|
|
9
|
+
local Jest = require("Jest")
|
|
10
|
+
|
|
11
|
+
local describe = Jest.Globals.describe
|
|
12
|
+
local expect = Jest.Globals.expect
|
|
13
|
+
local it = Jest.Globals.it
|
|
14
|
+
|
|
15
|
+
describe("InfluxDBClientConfigUtils.isClientConfig", function()
|
|
16
|
+
it("should be true for a config with a string token", function()
|
|
17
|
+
expect(InfluxDBClientConfigUtils.isClientConfig({
|
|
18
|
+
url = "https://us-east-1-1.aws.cloud2.influxdata.com",
|
|
19
|
+
token = "my-token",
|
|
20
|
+
})).toEqual(true)
|
|
21
|
+
end)
|
|
22
|
+
|
|
23
|
+
it("should be false for a non-table", function()
|
|
24
|
+
expect(InfluxDBClientConfigUtils.isClientConfig(nil)).toEqual(false)
|
|
25
|
+
expect(InfluxDBClientConfigUtils.isClientConfig("str")).toEqual(false)
|
|
26
|
+
expect(InfluxDBClientConfigUtils.isClientConfig(5)).toEqual(false)
|
|
27
|
+
end)
|
|
28
|
+
|
|
29
|
+
it("should be false when url is missing", function()
|
|
30
|
+
expect(InfluxDBClientConfigUtils.isClientConfig({
|
|
31
|
+
token = "my-token",
|
|
32
|
+
})).toEqual(false)
|
|
33
|
+
end)
|
|
34
|
+
|
|
35
|
+
it("should be false when url is not a string", function()
|
|
36
|
+
expect(InfluxDBClientConfigUtils.isClientConfig({
|
|
37
|
+
url = 5,
|
|
38
|
+
token = "my-token",
|
|
39
|
+
})).toEqual(false)
|
|
40
|
+
end)
|
|
41
|
+
|
|
42
|
+
it("should be false when token is missing", function()
|
|
43
|
+
expect(InfluxDBClientConfigUtils.isClientConfig({
|
|
44
|
+
url = "https://example.com",
|
|
45
|
+
})).toEqual(false)
|
|
46
|
+
end)
|
|
47
|
+
|
|
48
|
+
it("should be false when token is a number", function()
|
|
49
|
+
expect(InfluxDBClientConfigUtils.isClientConfig({
|
|
50
|
+
url = "https://example.com",
|
|
51
|
+
token = 5,
|
|
52
|
+
})).toEqual(false)
|
|
53
|
+
end)
|
|
54
|
+
end)
|
|
55
|
+
|
|
56
|
+
describe("InfluxDBClientConfigUtils.createClientConfig", function()
|
|
57
|
+
it("should return a config with only url and token fields", function()
|
|
58
|
+
local config = InfluxDBClientConfigUtils.createClientConfig({
|
|
59
|
+
url = "https://example.com",
|
|
60
|
+
token = "my-token",
|
|
61
|
+
extra = "ignored",
|
|
62
|
+
} :: any)
|
|
63
|
+
|
|
64
|
+
expect(config.url).toEqual("https://example.com")
|
|
65
|
+
expect(config.token).toEqual("my-token")
|
|
66
|
+
expect((config :: any).extra).toEqual(nil)
|
|
67
|
+
end)
|
|
68
|
+
|
|
69
|
+
it("should return a fresh table, not the same reference", function()
|
|
70
|
+
local input = {
|
|
71
|
+
url = "https://example.com",
|
|
72
|
+
token = "my-token",
|
|
73
|
+
}
|
|
74
|
+
local config = InfluxDBClientConfigUtils.createClientConfig(input)
|
|
75
|
+
|
|
76
|
+
expect(config).never.toBe(input)
|
|
77
|
+
end)
|
|
78
|
+
|
|
79
|
+
it("should throw on an invalid config", function()
|
|
80
|
+
expect(function()
|
|
81
|
+
InfluxDBClientConfigUtils.createClientConfig({
|
|
82
|
+
token = "my-token",
|
|
83
|
+
} :: any)
|
|
84
|
+
end).toThrow("Bad config")
|
|
85
|
+
end)
|
|
86
|
+
end)
|
|
@@ -0,0 +1,94 @@
|
|
|
1
|
+
--!strict
|
|
2
|
+
--[[
|
|
3
|
+
@class InfluxDBWriteOptionUtils.spec.lua
|
|
4
|
+
]]
|
|
5
|
+
|
|
6
|
+
local require = require(script.Parent.loader).load(script)
|
|
7
|
+
|
|
8
|
+
local InfluxDBWriteOptionUtils = require("InfluxDBWriteOptionUtils")
|
|
9
|
+
local Jest = require("Jest")
|
|
10
|
+
|
|
11
|
+
local describe = Jest.Globals.describe
|
|
12
|
+
local expect = Jest.Globals.expect
|
|
13
|
+
local it = Jest.Globals.it
|
|
14
|
+
|
|
15
|
+
describe("InfluxDBWriteOptionUtils.getDefaultOptions", function()
|
|
16
|
+
it("should return the documented defaults", function()
|
|
17
|
+
local options = InfluxDBWriteOptionUtils.getDefaultOptions()
|
|
18
|
+
|
|
19
|
+
expect(options.batchSize).toEqual(1000)
|
|
20
|
+
expect(options.maxBatchBytes).toEqual(50_000_000)
|
|
21
|
+
expect(options.flushIntervalSeconds).toEqual(60)
|
|
22
|
+
end)
|
|
23
|
+
|
|
24
|
+
it("should return a readonly table", function()
|
|
25
|
+
local options = InfluxDBWriteOptionUtils.getDefaultOptions()
|
|
26
|
+
|
|
27
|
+
expect(function()
|
|
28
|
+
(options :: any).batchSize = 5
|
|
29
|
+
end).toThrow()
|
|
30
|
+
end)
|
|
31
|
+
end)
|
|
32
|
+
|
|
33
|
+
describe("InfluxDBWriteOptionUtils.isWriteOptions", function()
|
|
34
|
+
it("should be true for a fully specified options table", function()
|
|
35
|
+
expect(InfluxDBWriteOptionUtils.isWriteOptions({
|
|
36
|
+
batchSize = 10,
|
|
37
|
+
maxBatchBytes = 1000,
|
|
38
|
+
flushIntervalSeconds = 5,
|
|
39
|
+
})).toEqual(true)
|
|
40
|
+
end)
|
|
41
|
+
|
|
42
|
+
it("should be false for a non-table", function()
|
|
43
|
+
expect(InfluxDBWriteOptionUtils.isWriteOptions(nil)).toEqual(false)
|
|
44
|
+
expect(InfluxDBWriteOptionUtils.isWriteOptions("str")).toEqual(false)
|
|
45
|
+
end)
|
|
46
|
+
|
|
47
|
+
it("should be false when batchSize is missing", function()
|
|
48
|
+
expect(InfluxDBWriteOptionUtils.isWriteOptions({
|
|
49
|
+
maxBatchBytes = 1000,
|
|
50
|
+
flushIntervalSeconds = 5,
|
|
51
|
+
})).toEqual(false)
|
|
52
|
+
end)
|
|
53
|
+
|
|
54
|
+
it("should be false when maxBatchBytes is not a number", function()
|
|
55
|
+
expect(InfluxDBWriteOptionUtils.isWriteOptions({
|
|
56
|
+
batchSize = 10,
|
|
57
|
+
maxBatchBytes = "1000",
|
|
58
|
+
flushIntervalSeconds = 5,
|
|
59
|
+
})).toEqual(false)
|
|
60
|
+
end)
|
|
61
|
+
|
|
62
|
+
it("should be false when flushIntervalSeconds is missing", function()
|
|
63
|
+
expect(InfluxDBWriteOptionUtils.isWriteOptions({
|
|
64
|
+
batchSize = 10,
|
|
65
|
+
maxBatchBytes = 1000,
|
|
66
|
+
})).toEqual(false)
|
|
67
|
+
end)
|
|
68
|
+
end)
|
|
69
|
+
|
|
70
|
+
describe("InfluxDBWriteOptionUtils.createWriteOptions", function()
|
|
71
|
+
it("should return a readonly copy of valid options", function()
|
|
72
|
+
local options = InfluxDBWriteOptionUtils.createWriteOptions({
|
|
73
|
+
batchSize = 10,
|
|
74
|
+
maxBatchBytes = 1000,
|
|
75
|
+
flushIntervalSeconds = 5,
|
|
76
|
+
})
|
|
77
|
+
|
|
78
|
+
expect(options.batchSize).toEqual(10)
|
|
79
|
+
expect(options.maxBatchBytes).toEqual(1000)
|
|
80
|
+
expect(options.flushIntervalSeconds).toEqual(5)
|
|
81
|
+
|
|
82
|
+
expect(function()
|
|
83
|
+
(options :: any).batchSize = 20
|
|
84
|
+
end).toThrow()
|
|
85
|
+
end)
|
|
86
|
+
|
|
87
|
+
it("should throw on invalid options", function()
|
|
88
|
+
expect(function()
|
|
89
|
+
InfluxDBWriteOptionUtils.createWriteOptions({
|
|
90
|
+
batchSize = 10,
|
|
91
|
+
} :: any)
|
|
92
|
+
end).toThrow("Bad options")
|
|
93
|
+
end)
|
|
94
|
+
end)
|
|
@@ -10,6 +10,7 @@ local require = require(script.Parent.loader).load(script)
|
|
|
10
10
|
|
|
11
11
|
local BaseObject = require("BaseObject")
|
|
12
12
|
local InfluxDBClientConfigUtils = require("InfluxDBClientConfigUtils")
|
|
13
|
+
local InfluxDBRequestHandlerMock = require("InfluxDBRequestHandlerMock")
|
|
13
14
|
local InfluxDBWriteAPI = require("InfluxDBWriteAPI")
|
|
14
15
|
local Maid = require("Maid")
|
|
15
16
|
local Promise = require("Promise")
|
|
@@ -26,6 +27,7 @@ export type InfluxDBClient =
|
|
|
26
27
|
_clientConfig: ValueObject.ValueObject<InfluxDBClientConfigUtils.InfluxDBClientConfig>,
|
|
27
28
|
_writeApis: { [string]: { [string]: InfluxDBWriteAPI.InfluxDBWriteAPI } },
|
|
28
29
|
_flushAllPromises: Promise.Promise<()>,
|
|
30
|
+
_requestHandler: InfluxDBWriteAPI.InfluxDBRequestHandler?,
|
|
29
31
|
},
|
|
30
32
|
{} :: typeof({ __index = InfluxDBClient })
|
|
31
33
|
))
|
|
@@ -35,11 +37,20 @@ export type InfluxDBClient =
|
|
|
35
37
|
Creates a new InfluxDB client
|
|
36
38
|
|
|
37
39
|
@param clientConfig InfluxDBClientConfig?
|
|
40
|
+
@param requestHandler InfluxDBRequestHandler? -- Defaults to [HttpPromise.request]. Inject a mock to avoid real HTTP.
|
|
38
41
|
@return InfluxDBClient
|
|
39
42
|
]=]
|
|
40
|
-
function InfluxDBClient.new(
|
|
43
|
+
function InfluxDBClient.new(
|
|
44
|
+
clientConfig: InfluxDBClientConfigUtils.InfluxDBClientConfig?,
|
|
45
|
+
requestHandler: InfluxDBWriteAPI.InfluxDBRequestHandler?
|
|
46
|
+
): InfluxDBClient
|
|
41
47
|
local self: InfluxDBClient = setmetatable(BaseObject.new() :: any, InfluxDBClient)
|
|
42
48
|
|
|
49
|
+
assert(type(requestHandler) == "function" or requestHandler == nil, "Bad requestHandler")
|
|
50
|
+
|
|
51
|
+
-- Threaded down into each write API created by GetWriteAPI so all of them share the same seam.
|
|
52
|
+
self._requestHandler = requestHandler
|
|
53
|
+
|
|
43
54
|
self._clientConfig = self._maid:Add(ValueObject.new(nil))
|
|
44
55
|
|
|
45
56
|
if clientConfig then
|
|
@@ -51,6 +62,33 @@ function InfluxDBClient.new(clientConfig: InfluxDBClientConfigUtils.InfluxDBClie
|
|
|
51
62
|
return self
|
|
52
63
|
end
|
|
53
64
|
|
|
65
|
+
--[=[
|
|
66
|
+
Creates a new InfluxDB client wired to an [InfluxDBRequestHandlerMock] so no real HTTP calls are
|
|
67
|
+
made. Returns both the client and the mock, so a test can inspect the requests it would have sent.
|
|
68
|
+
|
|
69
|
+
```lua
|
|
70
|
+
local client, requestMock = InfluxDBClient.newMock()
|
|
71
|
+
client:SetClientConfig({ url = "https://example.com", token = "token" })
|
|
72
|
+
|
|
73
|
+
local writeAPI = client:GetWriteAPI("org", "bucket")
|
|
74
|
+
writeAPI:QueuePoint(point)
|
|
75
|
+
writeAPI:PromiseFlush():Wait()
|
|
76
|
+
|
|
77
|
+
print(#requestMock:GetRequests()) --> 1
|
|
78
|
+
```
|
|
79
|
+
|
|
80
|
+
@param clientConfig InfluxDBClientConfig?
|
|
81
|
+
@return (InfluxDBClient, InfluxDBRequestHandlerMock)
|
|
82
|
+
]=]
|
|
83
|
+
function InfluxDBClient.newMock(
|
|
84
|
+
clientConfig: InfluxDBClientConfigUtils.InfluxDBClientConfig?
|
|
85
|
+
): (InfluxDBClient, InfluxDBRequestHandlerMock.InfluxDBRequestHandlerMock)
|
|
86
|
+
local requestMock = InfluxDBRequestHandlerMock.new()
|
|
87
|
+
local client = InfluxDBClient.new(clientConfig, requestMock.Handler)
|
|
88
|
+
|
|
89
|
+
return client, requestMock
|
|
90
|
+
end
|
|
91
|
+
|
|
54
92
|
--[=[
|
|
55
93
|
Sets the client config for this client
|
|
56
94
|
|
|
@@ -83,10 +121,14 @@ function InfluxDBClient.GetWriteAPI(
|
|
|
83
121
|
|
|
84
122
|
local maid = Maid.new()
|
|
85
123
|
|
|
86
|
-
local writeAPI = maid:Add(InfluxDBWriteAPI.new(org, bucket, precision))
|
|
124
|
+
local writeAPI = maid:Add(InfluxDBWriteAPI.new(org, bucket, precision, self._requestHandler))
|
|
87
125
|
|
|
88
126
|
maid:GiveTask(self._clientConfig:Observe():Subscribe(function(clientConfig)
|
|
89
|
-
|
|
127
|
+
-- The observable fires the current value immediately, which is nil until a config is set. Guard
|
|
128
|
+
-- so GetWriteAPI can be called before SetClientConfig; the later config is pushed through here.
|
|
129
|
+
if clientConfig then
|
|
130
|
+
writeAPI:SetClientConfig(clientConfig)
|
|
131
|
+
end
|
|
90
132
|
end))
|
|
91
133
|
|
|
92
134
|
maid:GiveTask(writeAPI.Destroying:Connect(function()
|
|
@@ -0,0 +1,227 @@
|
|
|
1
|
+
--!strict
|
|
2
|
+
--[[
|
|
3
|
+
@class InfluxDBClient.spec.lua
|
|
4
|
+
]]
|
|
5
|
+
|
|
6
|
+
local require = require(script.Parent.loader).load(script)
|
|
7
|
+
|
|
8
|
+
local InfluxDBClient = require("InfluxDBClient")
|
|
9
|
+
local InfluxDBPoint = require("InfluxDBPoint")
|
|
10
|
+
local InfluxDBRequestHandlerMock = require("InfluxDBRequestHandlerMock")
|
|
11
|
+
local Jest = require("Jest")
|
|
12
|
+
local Promise = require("Promise")
|
|
13
|
+
local PromiseTestUtils = require("PromiseTestUtils")
|
|
14
|
+
|
|
15
|
+
local describe = Jest.Globals.describe
|
|
16
|
+
local expect = Jest.Globals.expect
|
|
17
|
+
local it = Jest.Globals.it
|
|
18
|
+
|
|
19
|
+
local FIXED_TIMESTAMP = DateTime.fromUnixTimestampMillis(1600000000000)
|
|
20
|
+
|
|
21
|
+
local CONFIG = {
|
|
22
|
+
url = "https://example.com",
|
|
23
|
+
token = "my-token",
|
|
24
|
+
}
|
|
25
|
+
|
|
26
|
+
local function newPoint(): InfluxDBPoint.InfluxDBPoint
|
|
27
|
+
local point = InfluxDBPoint.new("temperature")
|
|
28
|
+
point:SetTimestamp(FIXED_TIMESTAMP)
|
|
29
|
+
point:AddFloatField("value", 23.5)
|
|
30
|
+
return point
|
|
31
|
+
end
|
|
32
|
+
|
|
33
|
+
describe("InfluxDBClient.new", function()
|
|
34
|
+
it("should construct without a config", function()
|
|
35
|
+
local client = InfluxDBClient.new()
|
|
36
|
+
expect(client.ClassName).toEqual("InfluxDBClient")
|
|
37
|
+
client:Destroy()
|
|
38
|
+
end)
|
|
39
|
+
|
|
40
|
+
it("should construct with a config", function()
|
|
41
|
+
local client = InfluxDBClient.new(CONFIG)
|
|
42
|
+
expect(client.ClassName).toEqual("InfluxDBClient")
|
|
43
|
+
client:Destroy()
|
|
44
|
+
end)
|
|
45
|
+
|
|
46
|
+
it("should throw when constructed with an invalid config", function()
|
|
47
|
+
expect(function()
|
|
48
|
+
InfluxDBClient.new({ url = "https://example.com" } :: any)
|
|
49
|
+
end).toThrow("Bad clientConfig")
|
|
50
|
+
end)
|
|
51
|
+
|
|
52
|
+
it("should throw on a non-function, non-nil request handler", function()
|
|
53
|
+
expect(function()
|
|
54
|
+
InfluxDBClient.new(nil, 5 :: any)
|
|
55
|
+
end).toThrow("Bad requestHandler")
|
|
56
|
+
end)
|
|
57
|
+
end)
|
|
58
|
+
|
|
59
|
+
describe("InfluxDBClient.newMock", function()
|
|
60
|
+
it("should return a client and a request handler mock", function()
|
|
61
|
+
local client, requestMock = InfluxDBClient.newMock()
|
|
62
|
+
|
|
63
|
+
expect(client.ClassName).toEqual("InfluxDBClient")
|
|
64
|
+
expect(InfluxDBRequestHandlerMock.isInfluxDBRequestHandlerMock(requestMock)).toEqual(true)
|
|
65
|
+
|
|
66
|
+
client:Destroy()
|
|
67
|
+
end)
|
|
68
|
+
|
|
69
|
+
it("should route write API traffic through the mock instead of the network", function()
|
|
70
|
+
local client, requestMock = InfluxDBClient.newMock(CONFIG)
|
|
71
|
+
|
|
72
|
+
local writeAPI = client:GetWriteAPI("my-org", "my-bucket")
|
|
73
|
+
writeAPI:QueuePoint(newPoint())
|
|
74
|
+
expect(PromiseTestUtils.awaitSettled(writeAPI:PromiseFlush())).toEqual(true)
|
|
75
|
+
|
|
76
|
+
expect(#requestMock:GetRequests()).toEqual(1)
|
|
77
|
+
expect((requestMock:GetLastRequest() :: any).Headers.Authorization).toEqual("Token my-token")
|
|
78
|
+
|
|
79
|
+
client:Destroy()
|
|
80
|
+
end)
|
|
81
|
+
end)
|
|
82
|
+
|
|
83
|
+
describe("InfluxDBClient.SetClientConfig", function()
|
|
84
|
+
it("should throw on an invalid config", function()
|
|
85
|
+
local client = InfluxDBClient.new()
|
|
86
|
+
|
|
87
|
+
expect(function()
|
|
88
|
+
client:SetClientConfig({ token = "my-token" } :: any)
|
|
89
|
+
end).toThrow("Bad clientConfig")
|
|
90
|
+
|
|
91
|
+
client:Destroy()
|
|
92
|
+
end)
|
|
93
|
+
end)
|
|
94
|
+
|
|
95
|
+
describe("InfluxDBClient.GetWriteAPI", function()
|
|
96
|
+
it("should throw on a non-string org", function()
|
|
97
|
+
local client = InfluxDBClient.new()
|
|
98
|
+
expect(function()
|
|
99
|
+
client:GetWriteAPI(5 :: any, "my-bucket")
|
|
100
|
+
end).toThrow("Bad org")
|
|
101
|
+
client:Destroy()
|
|
102
|
+
end)
|
|
103
|
+
|
|
104
|
+
it("should throw on a non-string bucket", function()
|
|
105
|
+
local client = InfluxDBClient.new()
|
|
106
|
+
expect(function()
|
|
107
|
+
client:GetWriteAPI("my-org", 5 :: any)
|
|
108
|
+
end).toThrow("Bad bucket")
|
|
109
|
+
client:Destroy()
|
|
110
|
+
end)
|
|
111
|
+
|
|
112
|
+
it("should throw on a non-string, non-nil precision", function()
|
|
113
|
+
local client = InfluxDBClient.new()
|
|
114
|
+
expect(function()
|
|
115
|
+
client:GetWriteAPI("my-org", "my-bucket", 5 :: any)
|
|
116
|
+
end).toThrow("Bad precision")
|
|
117
|
+
client:Destroy()
|
|
118
|
+
end)
|
|
119
|
+
|
|
120
|
+
-- Regression: GetWriteAPI must work before a config is set. Its ValueObject:Observe() subscription
|
|
121
|
+
-- fires the current value (nil) immediately; without a nil guard in GetWriteAPI this would call
|
|
122
|
+
-- writeAPI:SetClientConfig(nil) and throw "Bad clientConfig".
|
|
123
|
+
it("should return a write API", function()
|
|
124
|
+
local client = InfluxDBClient.new()
|
|
125
|
+
local writeAPI = client:GetWriteAPI("my-org", "my-bucket")
|
|
126
|
+
expect(writeAPI.ClassName).toEqual("InfluxDBWriteAPI")
|
|
127
|
+
client:Destroy()
|
|
128
|
+
end)
|
|
129
|
+
|
|
130
|
+
it("should return the same write API for the same org and bucket", function()
|
|
131
|
+
local client = InfluxDBClient.new()
|
|
132
|
+
local first = client:GetWriteAPI("my-org", "my-bucket")
|
|
133
|
+
local second = client:GetWriteAPI("my-org", "my-bucket")
|
|
134
|
+
expect(second).toBe(first)
|
|
135
|
+
client:Destroy()
|
|
136
|
+
end)
|
|
137
|
+
|
|
138
|
+
it("should return a different write API for a different bucket", function()
|
|
139
|
+
local client = InfluxDBClient.new()
|
|
140
|
+
local first = client:GetWriteAPI("my-org", "bucket-a")
|
|
141
|
+
local second = client:GetWriteAPI("my-org", "bucket-b")
|
|
142
|
+
expect(second).never.toBe(first)
|
|
143
|
+
client:Destroy()
|
|
144
|
+
end)
|
|
145
|
+
|
|
146
|
+
it("should return a different write API for a different org", function()
|
|
147
|
+
local client = InfluxDBClient.new()
|
|
148
|
+
local first = client:GetWriteAPI("org-a", "my-bucket")
|
|
149
|
+
local second = client:GetWriteAPI("org-b", "my-bucket")
|
|
150
|
+
expect(second).never.toBe(first)
|
|
151
|
+
client:Destroy()
|
|
152
|
+
end)
|
|
153
|
+
|
|
154
|
+
it("should propagate the client config set before GetWriteAPI to the write API", function()
|
|
155
|
+
local client, requestMock = InfluxDBClient.newMock(CONFIG)
|
|
156
|
+
local writeAPI = client:GetWriteAPI("my-org", "my-bucket")
|
|
157
|
+
|
|
158
|
+
-- If the config had not reached the write API, the flush would reject with "No client configuration".
|
|
159
|
+
writeAPI:QueuePoint(newPoint())
|
|
160
|
+
expect(PromiseTestUtils.awaitSettled(writeAPI:PromiseFlush())).toEqual(true)
|
|
161
|
+
|
|
162
|
+
expect(#requestMock:GetRequests()).toEqual(1)
|
|
163
|
+
|
|
164
|
+
client:Destroy()
|
|
165
|
+
end)
|
|
166
|
+
|
|
167
|
+
it("should propagate a client config set after GetWriteAPI to the write API", function()
|
|
168
|
+
local client, requestMock = InfluxDBClient.newMock()
|
|
169
|
+
local writeAPI = client:GetWriteAPI("my-org", "my-bucket")
|
|
170
|
+
|
|
171
|
+
client:SetClientConfig(CONFIG)
|
|
172
|
+
|
|
173
|
+
writeAPI:QueuePoint(newPoint())
|
|
174
|
+
expect(PromiseTestUtils.awaitSettled(writeAPI:PromiseFlush())).toEqual(true)
|
|
175
|
+
|
|
176
|
+
expect(#requestMock:GetRequests()).toEqual(1)
|
|
177
|
+
|
|
178
|
+
client:Destroy()
|
|
179
|
+
end)
|
|
180
|
+
end)
|
|
181
|
+
|
|
182
|
+
describe("InfluxDBClient.PromiseFlushAll", function()
|
|
183
|
+
it("should resolve when there are no write APIs", function()
|
|
184
|
+
local client = InfluxDBClient.new()
|
|
185
|
+
|
|
186
|
+
expect(PromiseTestUtils.awaitSettled(client:PromiseFlushAll())).toEqual(true)
|
|
187
|
+
|
|
188
|
+
client:Destroy()
|
|
189
|
+
end)
|
|
190
|
+
|
|
191
|
+
it("should flush buffered points across every write API", function()
|
|
192
|
+
local client, requestMock = InfluxDBClient.newMock(CONFIG)
|
|
193
|
+
|
|
194
|
+
local apiA = client:GetWriteAPI("my-org", "bucket-a")
|
|
195
|
+
local apiB = client:GetWriteAPI("my-org", "bucket-b")
|
|
196
|
+
apiA:QueuePoint(newPoint())
|
|
197
|
+
apiB:QueuePoint(newPoint())
|
|
198
|
+
|
|
199
|
+
expect(PromiseTestUtils.awaitSettled(client:PromiseFlushAll())).toEqual(true)
|
|
200
|
+
expect(#requestMock:GetRequests()).toEqual(2)
|
|
201
|
+
|
|
202
|
+
client:Destroy()
|
|
203
|
+
end)
|
|
204
|
+
|
|
205
|
+
it("should reuse the pending flush promise while one is in flight", function()
|
|
206
|
+
local client, requestMock = InfluxDBClient.newMock(CONFIG)
|
|
207
|
+
|
|
208
|
+
-- A request that never settles keeps the flush pending, so both calls observe the same promise.
|
|
209
|
+
requestMock:SetResponder(function()
|
|
210
|
+
return Promise.new(function() end)
|
|
211
|
+
end)
|
|
212
|
+
|
|
213
|
+
local writeAPI = client:GetWriteAPI("my-org", "my-bucket")
|
|
214
|
+
writeAPI:QueuePoint(newPoint())
|
|
215
|
+
|
|
216
|
+
local first = client:PromiseFlushAll()
|
|
217
|
+
local second = client:PromiseFlushAll()
|
|
218
|
+
|
|
219
|
+
expect(second).toBe(first)
|
|
220
|
+
|
|
221
|
+
-- Destroy cancels the in-flight request, rejecting this promise; consume it so the cancellation
|
|
222
|
+
-- does not surface as an unhandled rejection.
|
|
223
|
+
first:Catch(function() end)
|
|
224
|
+
|
|
225
|
+
client:Destroy()
|
|
226
|
+
end)
|
|
227
|
+
end)
|
|
@@ -0,0 +1,147 @@
|
|
|
1
|
+
--!strict
|
|
2
|
+
--[=[
|
|
3
|
+
Centralized service using serviceBag. Lets other packages share a single [InfluxDBClient] and, in
|
|
4
|
+
tests, swap the underlying request implementation for an [InfluxDBRequestHandlerMock] at the
|
|
5
|
+
ServiceBag layer -- the same pattern as [PlayerDataStoreService.SetRobloxDataStore].
|
|
6
|
+
|
|
7
|
+
```lua
|
|
8
|
+
-- Production
|
|
9
|
+
local influxDBService = serviceBag:GetService(require("InfluxDBService"))
|
|
10
|
+
-- ...after Init, before first use...
|
|
11
|
+
influxDBService:SetClientConfig({ url = "https://example.com", token = "token" })
|
|
12
|
+
|
|
13
|
+
local writeAPI = influxDBService:GetWriteAPI("org", "bucket")
|
|
14
|
+
writeAPI:QueuePoint(point)
|
|
15
|
+
```
|
|
16
|
+
|
|
17
|
+
```lua
|
|
18
|
+
-- Tests: inject a mock so nothing hits the network
|
|
19
|
+
local requestMock = InfluxDBRequestHandlerMock.new()
|
|
20
|
+
influxDBService:SetRequestHandler(requestMock.Handler)
|
|
21
|
+
```
|
|
22
|
+
|
|
23
|
+
@server
|
|
24
|
+
@class InfluxDBService
|
|
25
|
+
]=]
|
|
26
|
+
|
|
27
|
+
local require = require(script.Parent.loader).load(script)
|
|
28
|
+
|
|
29
|
+
local InfluxDBClient = require("InfluxDBClient")
|
|
30
|
+
local InfluxDBClientConfigUtils = require("InfluxDBClientConfigUtils")
|
|
31
|
+
local InfluxDBWriteAPI = require("InfluxDBWriteAPI")
|
|
32
|
+
local Maid = require("Maid")
|
|
33
|
+
local Promise = require("Promise")
|
|
34
|
+
local ServiceBag = require("ServiceBag")
|
|
35
|
+
|
|
36
|
+
local InfluxDBService = {}
|
|
37
|
+
InfluxDBService.ServiceName = "InfluxDBService"
|
|
38
|
+
|
|
39
|
+
export type InfluxDBService = typeof(setmetatable(
|
|
40
|
+
{} :: {
|
|
41
|
+
_serviceBag: ServiceBag.ServiceBag,
|
|
42
|
+
_maid: Maid.Maid,
|
|
43
|
+
_client: InfluxDBClient.InfluxDBClient?,
|
|
44
|
+
_requestHandler: InfluxDBWriteAPI.InfluxDBRequestHandler?,
|
|
45
|
+
_pendingClientConfig: InfluxDBClientConfigUtils.InfluxDBClientConfig?,
|
|
46
|
+
},
|
|
47
|
+
{} :: typeof({ __index = InfluxDBService })
|
|
48
|
+
))
|
|
49
|
+
|
|
50
|
+
--[=[
|
|
51
|
+
Initializes the InfluxDBService. Should be done via [ServiceBag.Init].
|
|
52
|
+
|
|
53
|
+
@param serviceBag ServiceBag
|
|
54
|
+
]=]
|
|
55
|
+
function InfluxDBService.Init(self: InfluxDBService, serviceBag: ServiceBag.ServiceBag): ()
|
|
56
|
+
self._serviceBag = assert(serviceBag, "No serviceBag")
|
|
57
|
+
self._maid = Maid.new()
|
|
58
|
+
end
|
|
59
|
+
|
|
60
|
+
--[=[
|
|
61
|
+
Injects the request handler the underlying client uses to send data, instead of the real
|
|
62
|
+
[HttpPromise.request]. Pass an [InfluxDBRequestHandlerMock] handler so tests never hit the network.
|
|
63
|
+
Intended for testing; must be called before the client is first built.
|
|
64
|
+
|
|
65
|
+
@param requestHandler InfluxDBRequestHandler
|
|
66
|
+
]=]
|
|
67
|
+
function InfluxDBService.SetRequestHandler(
|
|
68
|
+
self: InfluxDBService,
|
|
69
|
+
requestHandler: InfluxDBWriteAPI.InfluxDBRequestHandler
|
|
70
|
+
): ()
|
|
71
|
+
assert(type(requestHandler) == "function", "Bad requestHandler")
|
|
72
|
+
assert(not self._client, "Already built client, cannot override requestHandler")
|
|
73
|
+
|
|
74
|
+
self._requestHandler = requestHandler
|
|
75
|
+
end
|
|
76
|
+
|
|
77
|
+
--[=[
|
|
78
|
+
Sets the client config used to authenticate writes. May be called before or after the client is
|
|
79
|
+
built; a later config is forwarded to the existing client.
|
|
80
|
+
|
|
81
|
+
@param clientConfig InfluxDBClientConfig
|
|
82
|
+
]=]
|
|
83
|
+
function InfluxDBService.SetClientConfig(
|
|
84
|
+
self: InfluxDBService,
|
|
85
|
+
clientConfig: InfluxDBClientConfigUtils.InfluxDBClientConfig
|
|
86
|
+
): ()
|
|
87
|
+
assert(InfluxDBClientConfigUtils.isClientConfig(clientConfig), "Bad clientConfig")
|
|
88
|
+
|
|
89
|
+
self._pendingClientConfig = clientConfig
|
|
90
|
+
|
|
91
|
+
if self._client then
|
|
92
|
+
self._client:SetClientConfig(clientConfig)
|
|
93
|
+
end
|
|
94
|
+
end
|
|
95
|
+
|
|
96
|
+
--[=[
|
|
97
|
+
Returns the shared [InfluxDBClient], building it on first use with any injected request handler and
|
|
98
|
+
client config.
|
|
99
|
+
|
|
100
|
+
@return InfluxDBClient
|
|
101
|
+
]=]
|
|
102
|
+
function InfluxDBService.GetClient(self: InfluxDBService): InfluxDBClient.InfluxDBClient
|
|
103
|
+
if self._client then
|
|
104
|
+
return self._client
|
|
105
|
+
end
|
|
106
|
+
|
|
107
|
+
local client = self._maid:Add(InfluxDBClient.new(self._pendingClientConfig, self._requestHandler))
|
|
108
|
+
self._client = client
|
|
109
|
+
|
|
110
|
+
return client
|
|
111
|
+
end
|
|
112
|
+
|
|
113
|
+
--[=[
|
|
114
|
+
Returns the write API for the given org and bucket from the shared client.
|
|
115
|
+
|
|
116
|
+
@param org string
|
|
117
|
+
@param bucket string
|
|
118
|
+
@param precision string?
|
|
119
|
+
@return InfluxDBWriteAPI
|
|
120
|
+
]=]
|
|
121
|
+
function InfluxDBService.GetWriteAPI(
|
|
122
|
+
self: InfluxDBService,
|
|
123
|
+
org: string,
|
|
124
|
+
bucket: string,
|
|
125
|
+
precision: string?
|
|
126
|
+
): InfluxDBWriteAPI.InfluxDBWriteAPI
|
|
127
|
+
return self:GetClient():GetWriteAPI(org, bucket, precision)
|
|
128
|
+
end
|
|
129
|
+
|
|
130
|
+
--[=[
|
|
131
|
+
Flushes every write API on the shared client. Resolves immediately if the client was never built.
|
|
132
|
+
|
|
133
|
+
@return Promise<()>
|
|
134
|
+
]=]
|
|
135
|
+
function InfluxDBService.PromiseFlushAll(self: InfluxDBService): Promise.Promise<()>
|
|
136
|
+
if not self._client then
|
|
137
|
+
return Promise.resolved()
|
|
138
|
+
end
|
|
139
|
+
|
|
140
|
+
return self._client:PromiseFlushAll()
|
|
141
|
+
end
|
|
142
|
+
|
|
143
|
+
function InfluxDBService.Destroy(self: InfluxDBService): ()
|
|
144
|
+
self._maid:DoCleaning()
|
|
145
|
+
end
|
|
146
|
+
|
|
147
|
+
return InfluxDBService
|