@quenty/influxdbclient 7.36.0 → 7.38.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 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.38.0](https://github.com/Quenty/NevermoreEngine/compare/@quenty/influxdbclient@7.37.0...@quenty/influxdbclient@7.38.0) (2026-07-21)
7
+
8
+ **Note:** Version bump only for package @quenty/influxdbclient
9
+
10
+ # [7.37.0](https://github.com/Quenty/NevermoreEngine/compare/@quenty/influxdbclient@7.36.0...@quenty/influxdbclient@7.37.0) (2026-07-18)
11
+
12
+ ### Bug Fixes
13
+
14
+ - pin translation behavior with tests and fix camelCase key generation ([#737](https://github.com/Quenty/NevermoreEngine/issues/737)) ([1b7a536](https://github.com/Quenty/NevermoreEngine/commit/1b7a536dde7e124f8432e57612ec8138dd835d75))
15
+
6
16
  # [7.36.0](https://github.com/Quenty/NevermoreEngine/compare/@quenty/influxdbclient@7.35.1...@quenty/influxdbclient@7.36.0) (2026-07-18)
7
17
 
8
18
  **Note:** Version bump only for package @quenty/influxdbclient
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@quenty/influxdbclient",
3
- "version": "7.36.0",
3
+ "version": "7.38.0",
4
4
  "description": "Provides a Roblox Lua InfluxDB client",
5
5
  "keywords": [
6
6
  "Roblox",
@@ -28,24 +28,24 @@
28
28
  "Quenty"
29
29
  ],
30
30
  "dependencies": {
31
- "@quenty/baseobject": "10.13.0",
32
- "@quenty/httppromise": "10.19.0",
33
- "@quenty/jsonutils": "10.19.0",
31
+ "@quenty/baseobject": "10.14.0",
32
+ "@quenty/httppromise": "10.20.0",
33
+ "@quenty/jsonutils": "10.20.0",
34
34
  "@quenty/loader": "10.11.0",
35
- "@quenty/maid": "3.9.0",
35
+ "@quenty/maid": "3.10.0",
36
36
  "@quenty/math": "2.7.5",
37
37
  "@quenty/nevermore-test-runner": "1.4.0",
38
- "@quenty/promise": "10.19.0",
39
- "@quenty/rx": "13.29.0",
40
- "@quenty/servicebag": "11.18.1",
38
+ "@quenty/promise": "10.20.0",
39
+ "@quenty/rx": "13.30.0",
40
+ "@quenty/servicebag": "11.19.0",
41
41
  "@quenty/signal": "7.13.1",
42
42
  "@quenty/string": "3.3.7",
43
43
  "@quenty/table": "3.9.2",
44
- "@quenty/valueobject": "13.32.0",
44
+ "@quenty/valueobject": "13.33.0",
45
45
  "@quentystudios/jest-lua": "3.10.0-quenty.2"
46
46
  },
47
47
  "publishConfig": {
48
48
  "access": "public"
49
49
  },
50
- "gitHead": "cbbb89635bfdcbf32f26a40422ac736292917cca"
50
+ "gitHead": "b7e59984e586064ea3cec6176e79b3f7451ecdc5"
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)
@@ -0,0 +1,69 @@
1
+ --!strict
2
+ --[[
3
+ @class InfluxDBErrorUtils.spec.lua
4
+ ]]
5
+
6
+ local require = require(script.Parent.loader).load(script)
7
+
8
+ local InfluxDBErrorUtils = require("InfluxDBErrorUtils")
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("InfluxDBErrorUtils.isInfluxDBError", function()
16
+ it("should be true for a table with string code and message", function()
17
+ expect(InfluxDBErrorUtils.isInfluxDBError({
18
+ code = "unauthorized",
19
+ message = "unauthorized access",
20
+ })).toEqual(true)
21
+ end)
22
+
23
+ it("should be false for a non-table", function()
24
+ expect(InfluxDBErrorUtils.isInfluxDBError(nil)).toEqual(false)
25
+ expect(InfluxDBErrorUtils.isInfluxDBError("str")).toEqual(false)
26
+ expect(InfluxDBErrorUtils.isInfluxDBError(5)).toEqual(false)
27
+ end)
28
+
29
+ it("should be false when code is missing", function()
30
+ expect(InfluxDBErrorUtils.isInfluxDBError({
31
+ message = "unauthorized access",
32
+ })).toEqual(false)
33
+ end)
34
+
35
+ it("should be false when message is not a string", function()
36
+ expect(InfluxDBErrorUtils.isInfluxDBError({
37
+ code = "unauthorized",
38
+ message = 5,
39
+ })).toEqual(false)
40
+ end)
41
+ end)
42
+
43
+ describe("InfluxDBErrorUtils.tryParseErrorBody", function()
44
+ it("should parse a valid InfluxDB error body", function()
45
+ local errorBody =
46
+ InfluxDBErrorUtils.tryParseErrorBody('{"code":"unauthorized","message":"unauthorized access"}')
47
+
48
+ expect(errorBody).never.toEqual(nil)
49
+ assert(errorBody, "no errorBody")
50
+ expect(errorBody.code).toEqual("unauthorized")
51
+ expect(errorBody.message).toEqual("unauthorized access")
52
+ end)
53
+
54
+ it("should return nil for a body that is not valid JSON", function()
55
+ expect(InfluxDBErrorUtils.tryParseErrorBody("not json")).toEqual(nil)
56
+ end)
57
+
58
+ it("should return nil for valid JSON that is not an InfluxDB error", function()
59
+ expect(InfluxDBErrorUtils.tryParseErrorBody('{"foo":"bar"}')).toEqual(nil)
60
+ end)
61
+
62
+ it("should return nil for a JSON array", function()
63
+ expect(InfluxDBErrorUtils.tryParseErrorBody("[1, 2, 3]")).toEqual(nil)
64
+ end)
65
+
66
+ it("should return nil for an empty string", function()
67
+ expect(InfluxDBErrorUtils.tryParseErrorBody("")).toEqual(nil)
68
+ end)
69
+ end)
@@ -0,0 +1,144 @@
1
+ --!strict
2
+ --[[
3
+ @class InfluxDBWriteBuffer.spec.lua
4
+ ]]
5
+
6
+ local require = require(script.Parent.loader).load(script)
7
+
8
+ local InfluxDBWriteBuffer = require("InfluxDBWriteBuffer")
9
+ local InfluxDBWriteOptionUtils = require("InfluxDBWriteOptionUtils")
10
+ local Jest = require("Jest")
11
+ local Promise = require("Promise")
12
+ local PromiseTestUtils = require("PromiseTestUtils")
13
+
14
+ local describe = Jest.Globals.describe
15
+ local expect = Jest.Globals.expect
16
+ local it = Jest.Globals.it
17
+
18
+ -- Records each flush so tests can assert what was handed to the flush handler and when.
19
+ local function newRecordingBuffer(options: InfluxDBWriteOptionUtils.InfluxDBWriteOptions)
20
+ local flushes: { { string } } = {}
21
+ local buffer = InfluxDBWriteBuffer.new(options, function(entries)
22
+ table.insert(flushes, entries)
23
+ return Promise.resolved()
24
+ end)
25
+
26
+ return buffer, flushes
27
+ end
28
+
29
+ -- A large flush interval keeps the time-based flush from firing during a test; Destroy cancels it.
30
+ local function options(overrides: { [string]: number })
31
+ return InfluxDBWriteOptionUtils.createWriteOptions({
32
+ batchSize = overrides.batchSize or 1000,
33
+ maxBatchBytes = overrides.maxBatchBytes or 50_000_000,
34
+ flushIntervalSeconds = overrides.flushIntervalSeconds or 9999,
35
+ })
36
+ end
37
+
38
+ describe("InfluxDBWriteBuffer.Add", function()
39
+ it("should not flush before the batch size is reached", function()
40
+ local buffer, flushes = newRecordingBuffer(options({ batchSize = 3 }))
41
+
42
+ buffer:Add("a")
43
+ buffer:Add("b")
44
+
45
+ expect(#flushes).toEqual(0)
46
+
47
+ buffer:Destroy()
48
+ end)
49
+
50
+ it("should flush once the batch size is reached", function()
51
+ local buffer, flushes = newRecordingBuffer(options({ batchSize = 2 }))
52
+
53
+ buffer:Add("a")
54
+ buffer:Add("b")
55
+
56
+ expect(#flushes).toEqual(1)
57
+ expect(flushes[1]).toEqual({ "a", "b" })
58
+
59
+ buffer:Destroy()
60
+ end)
61
+
62
+ it("should preserve entry order across a flush", function()
63
+ local buffer, flushes = newRecordingBuffer(options({ batchSize = 3 }))
64
+
65
+ buffer:Add("first")
66
+ buffer:Add("second")
67
+ buffer:Add("third")
68
+
69
+ expect(flushes[1]).toEqual({ "first", "second", "third" })
70
+
71
+ buffer:Destroy()
72
+ end)
73
+
74
+ it("should reset after a flush so a later flush is empty", function()
75
+ local buffer, flushes = newRecordingBuffer(options({ batchSize = 2 }))
76
+
77
+ buffer:Add("a")
78
+ buffer:Add("b")
79
+ expect(#flushes).toEqual(1)
80
+
81
+ -- Nothing buffered now, so an explicit flush must not re-send the batch.
82
+ local promise = buffer:PromiseFlush()
83
+ expect(PromiseTestUtils.awaitSettled(promise)).toEqual(true)
84
+ expect(#flushes).toEqual(1)
85
+
86
+ buffer:Destroy()
87
+ end)
88
+
89
+ it("should flush when the byte ceiling is exceeded", function()
90
+ local buffer, flushes = newRecordingBuffer(options({ maxBatchBytes = 10 }))
91
+
92
+ -- 12 bytes + 1 newline separator clears the 10-byte ceiling on its own.
93
+ buffer:Add("abcdefghijkl")
94
+
95
+ expect(#flushes).toEqual(1)
96
+ expect(flushes[1]).toEqual({ "abcdefghijkl" })
97
+
98
+ buffer:Destroy()
99
+ end)
100
+
101
+ it("should throw on a non-string entry", function()
102
+ local buffer = newRecordingBuffer(options({}))
103
+
104
+ expect(function()
105
+ buffer:Add(5 :: any)
106
+ end).toThrow("Bad entry")
107
+
108
+ buffer:Destroy()
109
+ end)
110
+ end)
111
+
112
+ describe("InfluxDBWriteBuffer.PromiseFlush", function()
113
+ it("should flush pending entries below the batch size", function()
114
+ local buffer, flushes = newRecordingBuffer(options({ batchSize = 100 }))
115
+
116
+ buffer:Add("a")
117
+ buffer:Add("b")
118
+ expect(#flushes).toEqual(0)
119
+
120
+ local promise = buffer:PromiseFlush()
121
+ expect(PromiseTestUtils.awaitSettled(promise)).toEqual(true)
122
+ expect(flushes[1]).toEqual({ "a", "b" })
123
+
124
+ buffer:Destroy()
125
+ end)
126
+
127
+ it("should resolve without calling the handler when empty", function()
128
+ local buffer, flushes = newRecordingBuffer(options({}))
129
+
130
+ local promise = buffer:PromiseFlush()
131
+ expect(PromiseTestUtils.awaitSettled(promise)).toEqual(true)
132
+ expect(#flushes).toEqual(0)
133
+
134
+ buffer:Destroy()
135
+ end)
136
+ end)
137
+
138
+ describe("InfluxDBWriteBuffer.new", function()
139
+ it("should throw without a flush handler", function()
140
+ expect(function()
141
+ InfluxDBWriteBuffer.new(options({}), nil :: any)
142
+ end).toThrow("No promiseHandleFlush")
143
+ end)
144
+ end)
@@ -0,0 +1,87 @@
1
+ --!strict
2
+ --[[
3
+ @class InfluxDBPointSettings.spec.lua
4
+ ]]
5
+
6
+ local require = require(script.Parent.loader).load(script)
7
+
8
+ local InfluxDBPointSettings = require("InfluxDBPointSettings")
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("InfluxDBPointSettings.new", function()
16
+ it("should start with no default tags", function()
17
+ local settings = InfluxDBPointSettings.new()
18
+
19
+ expect(next(settings:GetDefaultTags())).toEqual(nil)
20
+ end)
21
+
22
+ it("should start with no convert time", function()
23
+ local settings = InfluxDBPointSettings.new()
24
+
25
+ expect(settings:GetConvertTime()).toEqual(nil)
26
+ end)
27
+ end)
28
+
29
+ describe("InfluxDBPointSettings.SetDefaultTags", function()
30
+ it("should store and return the tags", function()
31
+ local settings = InfluxDBPointSettings.new()
32
+ settings:SetDefaultTags({
33
+ host = "server1",
34
+ region = "us-east",
35
+ })
36
+
37
+ local tags = settings:GetDefaultTags()
38
+ expect(tags.host).toEqual("server1")
39
+ expect(tags.region).toEqual("us-east")
40
+ end)
41
+
42
+ it("should throw on a non-table", function()
43
+ local settings = InfluxDBPointSettings.new()
44
+
45
+ expect(function()
46
+ settings:SetDefaultTags(5 :: any)
47
+ end).toThrow("Bad tags")
48
+ end)
49
+
50
+ it("should throw when a tag value is not a string", function()
51
+ local settings = InfluxDBPointSettings.new()
52
+
53
+ expect(function()
54
+ settings:SetDefaultTags({ host = 5 :: any })
55
+ end).toThrow("Bad value")
56
+ end)
57
+ end)
58
+
59
+ describe("InfluxDBPointSettings.SetConvertTime", function()
60
+ it("should store and return the convert time function", function()
61
+ local settings = InfluxDBPointSettings.new()
62
+ local function convertTime(_value)
63
+ return 42
64
+ end
65
+ settings:SetConvertTime(convertTime)
66
+
67
+ expect(settings:GetConvertTime()).toBe(convertTime)
68
+ end)
69
+
70
+ it("should allow clearing the convert time with nil", function()
71
+ local settings = InfluxDBPointSettings.new()
72
+ settings:SetConvertTime(function(_value)
73
+ return 42
74
+ end)
75
+ settings:SetConvertTime(nil)
76
+
77
+ expect(settings:GetConvertTime()).toEqual(nil)
78
+ end)
79
+
80
+ it("should throw on a non-function", function()
81
+ local settings = InfluxDBPointSettings.new()
82
+
83
+ expect(function()
84
+ settings:SetConvertTime(5 :: any)
85
+ end).toThrow("Bad convertTime")
86
+ end)
87
+ end)
@@ -1,11 +1,9 @@
1
- --!nonstrict
1
+ --!strict
2
2
  --[[
3
3
  @class InfluxDBEscapeUtils.spec.lua
4
4
  ]]
5
5
 
6
- local require = (require :: any)(
7
- game:GetService("ServerScriptService"):FindFirstChild("LoaderUtils", true).Parent
8
- ).bootstrapStory(script) :: typeof(require(script.Parent.loader).load(script))
6
+ local require = require(script.Parent.loader).load(script)
9
7
 
10
8
  local InfluxDBEscapeUtils = require("InfluxDBEscapeUtils")
11
9
  local Jest = require("Jest")
@@ -36,6 +34,30 @@ describe("InfluxDBEscapeUtils.quoted", function()
36
34
  end)
37
35
  end)
38
36
 
37
+ describe("InfluxDBEscapeUtils.measurement", function()
38
+ it("should escape commas", function()
39
+ expect(InfluxDBEscapeUtils.measurement("a,b")).toBe("a\\,b")
40
+ end)
41
+
42
+ it("should escape spaces", function()
43
+ expect(InfluxDBEscapeUtils.measurement("a b")).toBe("a\\ b")
44
+ end)
45
+
46
+ it("should not escape equals signs", function()
47
+ expect(InfluxDBEscapeUtils.measurement("a=b")).toBe("a=b")
48
+ end)
49
+ end)
50
+
51
+ describe("InfluxDBEscapeUtils.quoted", function()
52
+ it("should escape backslashes", function()
53
+ expect(InfluxDBEscapeUtils.quoted("a\\b")).toBe('"a\\\\b"')
54
+ end)
55
+
56
+ it("should wrap an empty string in quotes", function()
57
+ expect(InfluxDBEscapeUtils.quoted("")).toBe('""')
58
+ end)
59
+ end)
60
+
39
61
  describe("InfluxDBEscapeUtils.tag", function()
40
62
  it("should pass through fine", function()
41
63
  local tag = InfluxDBEscapeUtils.tag("hi")
@@ -61,4 +83,50 @@ describe("InfluxDBEscapeUtils.tag", function()
61
83
  local tag = InfluxDBEscapeUtils.tag("\nhi")
62
84
  expect(tag).toBe("\\nhi")
63
85
  end)
86
+
87
+ it("should escape commas and spaces", function()
88
+ expect(InfluxDBEscapeUtils.tag("a, b")).toBe("a\\,\\ b")
89
+ end)
90
+ end)
91
+
92
+ describe("InfluxDBEscapeUtils.createEscaper", function()
93
+ it("should replace only the characters in the sub table", function()
94
+ local escaper = InfluxDBEscapeUtils.createEscaper({
95
+ ["#"] = "\\#",
96
+ })
97
+
98
+ expect(escaper("a#b#c")).toBe("a\\#b\\#c")
99
+ expect(escaper("abc")).toBe("abc")
100
+ end)
101
+
102
+ it("should escape Lua pattern magic characters used as keys", function()
103
+ local escaper = InfluxDBEscapeUtils.createEscaper({
104
+ ["."] = "\\.",
105
+ ["%"] = "\\%",
106
+ })
107
+
108
+ expect(escaper("a.b%c")).toBe("a\\.b\\%c")
109
+ end)
110
+
111
+ it("should throw on a non-table sub table", function()
112
+ expect(function()
113
+ InfluxDBEscapeUtils.createEscaper(5 :: any)
114
+ end).toThrow("Bad subTable")
115
+ end)
116
+
117
+ it("should throw on a multi-character key", function()
118
+ expect(function()
119
+ InfluxDBEscapeUtils.createEscaper({ ab = "x" })
120
+ end).toThrow("Bad char")
121
+ end)
122
+ end)
123
+
124
+ describe("InfluxDBEscapeUtils.createQuotedEscaper", function()
125
+ it("should wrap the escaped result in quotes", function()
126
+ local escaper = InfluxDBEscapeUtils.createQuotedEscaper({
127
+ ["#"] = "\\#",
128
+ })
129
+
130
+ expect(escaper("a#b")).toBe('"a\\#b"')
131
+ end)
64
132
  end)
@@ -0,0 +1,373 @@
1
+ --!strict
2
+ --[[
3
+ @class InfluxDBPoint.spec.lua
4
+ ]]
5
+
6
+ local require = require(script.Parent.loader).load(script)
7
+
8
+ local InfluxDBPoint = require("InfluxDBPoint")
9
+ local InfluxDBPointSettings = require("InfluxDBPointSettings")
10
+ local Jest = require("Jest")
11
+
12
+ local describe = Jest.Globals.describe
13
+ local expect = Jest.Globals.expect
14
+ local it = Jest.Globals.it
15
+
16
+ -- A fixed timestamp keeps line protocol output deterministic across tests.
17
+ local FIXED_TIMESTAMP = DateTime.fromUnixTimestampMillis(1600000000000)
18
+
19
+ local function newFixedPoint(measurementName: string?): InfluxDBPoint.InfluxDBPoint
20
+ local point = InfluxDBPoint.new(measurementName)
21
+ point:SetTimestamp(FIXED_TIMESTAMP)
22
+ return point
23
+ end
24
+
25
+ describe("InfluxDBPoint.new", function()
26
+ it("should default to an empty set of tags and fields", function()
27
+ local point = InfluxDBPoint.new("temperature")
28
+ local data = point:ToTableData()
29
+
30
+ expect(next(data.tags)).toEqual(nil)
31
+ expect(next(data.fields)).toEqual(nil)
32
+ end)
33
+
34
+ it("should allow a nil measurement name", function()
35
+ local point = InfluxDBPoint.new()
36
+ expect(point:GetMeasurementName()).toEqual(nil)
37
+ end)
38
+
39
+ it("should default the timestamp to a numeric millisecond string", function()
40
+ local point = InfluxDBPoint.new("temperature")
41
+ local timestamp = point:ToTableData().timestamp
42
+
43
+ expect(type(timestamp)).toEqual("string")
44
+ expect(tonumber(timestamp)).never.toEqual(nil)
45
+ end)
46
+
47
+ it("should throw on a non-string measurement name", function()
48
+ expect(function()
49
+ InfluxDBPoint.new(5 :: any)
50
+ end).toThrow("Bad measurementName")
51
+ end)
52
+ end)
53
+
54
+ describe("InfluxDBPoint.isInfluxDBPoint", function()
55
+ it("should be true for a point", function()
56
+ expect(InfluxDBPoint.isInfluxDBPoint(InfluxDBPoint.new("temperature"))).toEqual(true)
57
+ end)
58
+
59
+ it("should be false for a plain table", function()
60
+ expect(InfluxDBPoint.isInfluxDBPoint({})).toEqual(false)
61
+ end)
62
+
63
+ it("should be false for nil and primitives", function()
64
+ expect(InfluxDBPoint.isInfluxDBPoint(nil)).toEqual(false)
65
+ expect(InfluxDBPoint.isInfluxDBPoint(5)).toEqual(false)
66
+ expect(InfluxDBPoint.isInfluxDBPoint("str")).toEqual(false)
67
+ end)
68
+ end)
69
+
70
+ describe("InfluxDBPoint measurement name", function()
71
+ it("should get and set the measurement name", function()
72
+ local point = InfluxDBPoint.new("temperature")
73
+ expect(point:GetMeasurementName()).toEqual("temperature")
74
+
75
+ point:SetMeasurementName("humidity")
76
+ expect(point:GetMeasurementName()).toEqual("humidity")
77
+ end)
78
+ end)
79
+
80
+ describe("InfluxDBPoint.AddTag", function()
81
+ it("should throw on a non-string key", function()
82
+ local point = InfluxDBPoint.new("temperature")
83
+ expect(function()
84
+ point:AddTag(5 :: any, "value")
85
+ end).toThrow("Bad tagKey")
86
+ end)
87
+
88
+ it("should throw on a non-string value", function()
89
+ local point = InfluxDBPoint.new("temperature")
90
+ expect(function()
91
+ point:AddTag("key", 5 :: any)
92
+ end).toThrow("Bad tagValue")
93
+ end)
94
+ end)
95
+
96
+ describe("InfluxDBPoint field setters", function()
97
+ it("should add an int field with an 'i' suffix", function()
98
+ local point = newFixedPoint("temperature")
99
+ point:AddIntField("count", 5)
100
+
101
+ expect(point:ToLineProtocol(InfluxDBPointSettings.new())).toEqual("temperature count=5i 1600000000000")
102
+ end)
103
+
104
+ it("should add a uint field with a 'u' suffix", function()
105
+ local point = newFixedPoint("temperature")
106
+ point:AddUintField("count", 5)
107
+
108
+ expect(point:ToLineProtocol(InfluxDBPointSettings.new())).toEqual("temperature count=5u 1600000000000")
109
+ end)
110
+
111
+ it("should add a float field verbatim", function()
112
+ local point = newFixedPoint("temperature")
113
+ point:AddFloatField("value", 23.5)
114
+
115
+ expect(point:ToLineProtocol(InfluxDBPointSettings.new())).toEqual("temperature value=23.5 1600000000000")
116
+ end)
117
+
118
+ it("should add a boolean field as T or F", function()
119
+ local truePoint = newFixedPoint("temperature")
120
+ truePoint:AddBooleanField("active", true)
121
+ expect(truePoint:ToLineProtocol(InfluxDBPointSettings.new())).toEqual("temperature active=T 1600000000000")
122
+
123
+ local falsePoint = newFixedPoint("temperature")
124
+ falsePoint:AddBooleanField("active", false)
125
+ expect(falsePoint:ToLineProtocol(InfluxDBPointSettings.new())).toEqual("temperature active=F 1600000000000")
126
+ end)
127
+
128
+ it("should add a quoted string field", function()
129
+ local point = newFixedPoint("temperature")
130
+ point:AddStringField("message", "hello")
131
+
132
+ expect(point:ToLineProtocol(InfluxDBPointSettings.new())).toEqual('temperature message="hello" 1600000000000')
133
+ end)
134
+
135
+ it("should escape quotes in a string field value", function()
136
+ local point = newFixedPoint("temperature")
137
+ point:AddStringField("message", 'he said "hi"')
138
+
139
+ expect(point:ToLineProtocol(InfluxDBPointSettings.new())).toEqual(
140
+ 'temperature message="he said \\"hi\\"" 1600000000000'
141
+ )
142
+ end)
143
+
144
+ it("should throw on a non-number int value", function()
145
+ local point = InfluxDBPoint.new("temperature")
146
+ expect(function()
147
+ point:AddIntField("count", "5" :: any)
148
+ end).toThrow("Bad value")
149
+ end)
150
+
151
+ it("should throw on a NaN int value", function()
152
+ local point = InfluxDBPoint.new("temperature")
153
+ expect(function()
154
+ point:AddIntField("count", 0 / 0)
155
+ end).toThrow("invalid integer value")
156
+ end)
157
+
158
+ it("should throw on an out-of-range int value", function()
159
+ local point = InfluxDBPoint.new("temperature")
160
+ expect(function()
161
+ point:AddIntField("count", 1e30)
162
+ end).toThrow("invalid integer value")
163
+ end)
164
+
165
+ it("should throw on a negative uint value", function()
166
+ local point = InfluxDBPoint.new("temperature")
167
+ expect(function()
168
+ point:AddUintField("count", -1)
169
+ end).toThrow("invalid uint value")
170
+ end)
171
+
172
+ it("should throw on an out-of-range uint value", function()
173
+ local point = InfluxDBPoint.new("temperature")
174
+ expect(function()
175
+ point:AddUintField("count", 9007199254740991)
176
+ end).toThrow("invalid uint value")
177
+ end)
178
+
179
+ it("should throw on an infinite float value", function()
180
+ local point = InfluxDBPoint.new("temperature")
181
+ expect(function()
182
+ point:AddFloatField("value", math.huge)
183
+ end).toThrow("invalid float value")
184
+ end)
185
+
186
+ it("should throw on a non-boolean boolean value", function()
187
+ local point = InfluxDBPoint.new("temperature")
188
+ expect(function()
189
+ point:AddBooleanField("active", "true" :: any)
190
+ end).toThrow("Bad value")
191
+ end)
192
+ end)
193
+
194
+ describe("InfluxDBPoint.ToLineProtocol", function()
195
+ it("should return nil without a measurement name", function()
196
+ local point = newFixedPoint()
197
+ point:AddIntField("count", 5)
198
+
199
+ expect(point:ToLineProtocol(InfluxDBPointSettings.new())).toEqual(nil)
200
+ end)
201
+
202
+ it("should return nil without any fields", function()
203
+ local point = newFixedPoint("temperature")
204
+
205
+ expect(point:ToLineProtocol(InfluxDBPointSettings.new())).toEqual(nil)
206
+ end)
207
+
208
+ it("should render tags before the field set", function()
209
+ local point = newFixedPoint("temperature")
210
+ point:AddTag("location", "office")
211
+ point:AddFloatField("value", 23.5)
212
+
213
+ expect(point:ToLineProtocol(InfluxDBPointSettings.new())).toEqual(
214
+ "temperature,location=office value=23.5 1600000000000"
215
+ )
216
+ end)
217
+
218
+ it("should sort field keys alphabetically", function()
219
+ local point = newFixedPoint("temperature")
220
+ point:AddIntField("zeta", 1)
221
+ point:AddIntField("alpha", 2)
222
+
223
+ expect(point:ToLineProtocol(InfluxDBPointSettings.new())).toEqual("temperature alpha=2i,zeta=1i 1600000000000")
224
+ end)
225
+
226
+ it("should sort tag keys alphabetically", function()
227
+ local point = newFixedPoint("temperature")
228
+ point:AddTag("zeta", "z")
229
+ point:AddTag("alpha", "a")
230
+ point:AddFloatField("value", 1)
231
+
232
+ expect(point:ToLineProtocol(InfluxDBPointSettings.new())).toEqual(
233
+ "temperature,alpha=a,zeta=z value=1 1600000000000"
234
+ )
235
+ end)
236
+
237
+ it("should escape spaces in the measurement name", function()
238
+ local point = newFixedPoint("my measurement")
239
+ point:AddFloatField("value", 1)
240
+
241
+ expect(point:ToLineProtocol(InfluxDBPointSettings.new())).toEqual("my\\ measurement value=1 1600000000000")
242
+ end)
243
+
244
+ it("should escape special characters in tag keys and values", function()
245
+ local point = newFixedPoint("temperature")
246
+ point:AddTag("my tag", "a,b")
247
+ point:AddFloatField("value", 1)
248
+
249
+ expect(point:ToLineProtocol(InfluxDBPointSettings.new())).toEqual(
250
+ "temperature,my\\ tag=a\\,b value=1 1600000000000"
251
+ )
252
+ end)
253
+
254
+ it("should merge default tags from the point settings", function()
255
+ local point = newFixedPoint("temperature")
256
+ point:AddTag("location", "office")
257
+ point:AddFloatField("value", 1)
258
+
259
+ local settings = InfluxDBPointSettings.new()
260
+ settings:SetDefaultTags({ host = "server1" })
261
+
262
+ expect(point:ToLineProtocol(settings)).toEqual("temperature,host=server1,location=office value=1 1600000000000")
263
+ end)
264
+
265
+ it("should prefer the point's own tag over a default tag of the same key", function()
266
+ local point = newFixedPoint("temperature")
267
+ point:AddTag("host", "override")
268
+ point:AddFloatField("value", 1)
269
+
270
+ local settings = InfluxDBPointSettings.new()
271
+ settings:SetDefaultTags({ host = "default" })
272
+
273
+ expect(point:ToLineProtocol(settings)).toEqual("temperature,host=override value=1 1600000000000")
274
+ end)
275
+
276
+ it("should apply the convert time function to the timestamp", function()
277
+ local point = newFixedPoint("temperature")
278
+ point:AddFloatField("value", 1)
279
+
280
+ local settings = InfluxDBPointSettings.new()
281
+ settings:SetConvertTime(function(_timestamp)
282
+ return 999
283
+ end)
284
+
285
+ expect(point:ToLineProtocol(settings)).toEqual("temperature value=1 999")
286
+ end)
287
+ end)
288
+
289
+ describe("InfluxDBPoint.SetTimestamp", function()
290
+ it("should throw on a non-DateTime, non-nil timestamp", function()
291
+ local point = InfluxDBPoint.new("temperature")
292
+ expect(function()
293
+ point:SetTimestamp(5 :: any)
294
+ end).toThrow("Bad timestamp")
295
+ end)
296
+ end)
297
+
298
+ describe("InfluxDBPoint.ToTableData", function()
299
+ it("should round-trip through fromTableData", function()
300
+ local point = newFixedPoint("temperature")
301
+ point:AddTag("location", "office")
302
+ point:AddIntField("count", 5)
303
+
304
+ local copy = InfluxDBPoint.fromTableData(point:ToTableData())
305
+
306
+ expect(copy:GetMeasurementName()).toEqual("temperature")
307
+ expect(copy:ToLineProtocol(InfluxDBPointSettings.new())).toEqual(
308
+ point:ToLineProtocol(InfluxDBPointSettings.new())
309
+ )
310
+ end)
311
+
312
+ it("should return clones that do not mutate the source point", function()
313
+ local point = InfluxDBPoint.new("temperature")
314
+ point:AddTag("location", "office")
315
+
316
+ local data = point:ToTableData()
317
+ data.tags.location = "mutated"
318
+
319
+ expect(point:ToTableData().tags.location).toEqual("office")
320
+ end)
321
+ end)
322
+
323
+ describe("InfluxDBPoint.fromTableData", function()
324
+ it("should build a point from table data", function()
325
+ local point = InfluxDBPoint.fromTableData({
326
+ measurementName = "temperature",
327
+ timestamp = 1600000000000,
328
+ tags = { location = "office" },
329
+ fields = { count = "5i" },
330
+ })
331
+
332
+ expect(point:GetMeasurementName()).toEqual("temperature")
333
+ expect(point:ToLineProtocol(InfluxDBPointSettings.new())).toEqual(
334
+ "temperature,location=office count=5i 1600000000000"
335
+ )
336
+ end)
337
+
338
+ it("should throw on non-table data", function()
339
+ expect(function()
340
+ InfluxDBPoint.fromTableData(5 :: any)
341
+ end).toThrow("Bad data")
342
+ end)
343
+
344
+ it("should throw on a non-string measurement name", function()
345
+ expect(function()
346
+ InfluxDBPoint.fromTableData({
347
+ measurementName = 5 :: any,
348
+ tags = {},
349
+ fields = {},
350
+ })
351
+ end).toThrow("Bad data.measurementName")
352
+ end)
353
+
354
+ it("should throw on a non-string tag value", function()
355
+ expect(function()
356
+ InfluxDBPoint.fromTableData({
357
+ measurementName = "temperature",
358
+ tags = { location = 5 :: any },
359
+ fields = {},
360
+ })
361
+ end).toThrow("Bad tagValue")
362
+ end)
363
+
364
+ it("should throw on a non-string field value", function()
365
+ expect(function()
366
+ InfluxDBPoint.fromTableData({
367
+ measurementName = "temperature",
368
+ tags = {},
369
+ fields = { count = 5 :: any },
370
+ })
371
+ end).toThrow("Bad fieldValue")
372
+ end)
373
+ end)