2021-02-08 15:55:24 +01:00
import { randomBytes } from 'crypto'
2022-08-15 09:54:09 +02:00
import { DateTime } from 'luxon'
2021-02-08 15:55:24 +01:00
2023-01-20 14:08:26 +01:00
import { ClickHouseTimestamp } from '../src/types'
2022-04-11 16:59:38 +02:00
import { safeClickhouseString } from '../src/utils/db/utils'
2020-12-08 23:01:46 +01:00
import {
2021-02-08 15:55:24 +01:00
bufferToStream ,
2022-08-15 09:54:09 +02:00
clickHouseTimestampToDateTime ,
2021-02-08 15:55:24 +01:00
cloneObject ,
escapeClickHouseString ,
2023-06-05 18:25:39 +02:00
getPropertyValueByPath ,
2021-05-27 12:55:20 +02:00
groupBy ,
2021-02-08 15:55:24 +01:00
sanitizeSqlIdentifier ,
2021-06-18 16:23:08 +02:00
stringify ,
2020-12-17 16:50:03 +01:00
UUID ,
UUIDT ,
2021-05-03 13:14:30 +02:00
} from '../src/utils/utils'
2020-12-01 13:55:04 +01:00
2020-12-01 20:31:35 +01:00
// .zip in Base64: github repo posthog/helloworldplugin
2020-12-01 13:55:04 +01:00
const zip =
'UEsDBAoAAAAAAA8LbVEAAAAAAAAAAAAAAAAjAAkAaGVsbG93b3JsZHBsdWdpbi1pbWFnZWxlc3MtdmVyc2lvbi9VVAUAAc9Qrl9QSwMECgAAAAgADwttUQT7a+JUAAAAdQAAAC4ACQBoZWxsb3dvcmxkcGx1Z2luLWltYWdlbGVzcy12ZXJzaW9uLy5wcmV0dGllcnJjVVQFAAHPUK5fq+ZSAAKlkqLEzJzMvHTn/NzcRCUrBaXUYlMlHahcYlJ4ZkpJBlDYBCpUnJqbCeSmJeYUp8KEgLpzUgNL80tSgTIlRaUwiYKizLwSmAGGRgZctVwAUEsDBAoAAAAIAA8LbVG/Zg9y6wAAAPMBAAArAAkAaGVsbG93b3JsZHBsdWdpbi1pbWFnZWxlc3MtdmVyc2lvbi9pbmRleC5qc1VUBQABz1CuX31RTUvEMBC951eMi5AUS1E8e9Sz4FFE0jjNBrKTkkxcZOl/N9u0hyo0hyHDe/PefOj0QwaGTIZdIEjIeXz12TpSFzCBBmdhauAioLySp+Cx88GqwxsyO7KQR+AjAqM+3Ryaf7yq0YhJCL31GmMwmNLzNxIrvMYWVs8WjDZFdWPNJWZijPAE+qwdV1JnkZVcINnC/dLEjKUttgrcwUMjZpoboJp3pZ8RIztMq+n1/cXe5RG9D/KjNCHPIfovucPtdZyZdaqupDvk27XPWjH/d+je9Z+UT/1SUNKXZbXqsa5gqiPGctRIVaHc4RdQSwMECgAAAAgADwttUVpiCFkvAAAAOQAAACkACQBoZWxsb3dvcmxkcGx1Z2luLWltYWdlbGVzcy12ZXJzaW9uL2xpYi5qc1VUBQABz1CuX0srzUsuyczPU8jJTHKDsTXySnOTUos0Faq5FICgKLWktChPASKooKVgZM1VywUAUEsDBAoAAAAIAA8LbVE7Twti+wAAAO0BAAAvAAkAaGVsbG93b3JsZHBsdWdpbi1pbWFnZWxlc3MtdmVyc2lvbi9wYWNrYWdlLmpzb25VVAUAAc9Qrl+VUbtOAzEQ7O8rjJHSQO6S9noEFEgUlGkud4vtyOe1dm1IFOXf8eNE0qazZ2ZnvONzI4R0wwyyF9IjB41qrcFa/EWy09rbqIyTz1n2A8QGXVZu2k27regEPJLxYWFeCSCIoEEUg4cqmgdTWOMmOLYHrmgd5ESc0zUBAThkGYwaxU6+ECH1wqHIhGAPo/k2MO2kWK0EHE0QW5kmL8WNIL3fBKTTjeHJl82UCSUyQZHsgjzpEDz3XZfOOu7bEefuM1Xwhqq7VlAbaLPDf9QQU0+Ubeoi1ozguCR9vH9VbB/VzWZL6h2JnWGOwNdQjTP4QcGdPo8Ew5T+t7k0f1BLAwQKAAAACAAPC21Ru8C8oc0AAABTAQAALgAJAGhlbGxvd29ybGRwbHVnaW4taW1hZ2VsZXNzLXZlcnNpb24vcGx1Z2luLmpzb25VVAUAAc9Qrl9tjz1rwzAQhnf/iquXLCHesxQytKVToEPms3WWr8g6VzqRL/LfKymQdOggxPu8zwvStQFoPc7UbqGdyDk5SnBmccmyb9elTcHVUnWJ266zrFPqN4PM3V6ifojt/t8ZikPgRVl82b8HIgWdCA7FBPQG3kQAYYdhDZ9fQIaL/HKfz8h1x97QafMd79RxX2C+HmgQP7LN9JpTzj2GR/jzucOEuorAvr4hS691Xh09L9WJGtjbJzc0YnJaqh4vTx7oJ3Egk4sRXaTKb005t+YXUEsBAgAACgAAAAAADwttUQAAAAAAAAAAAAAAACMACQAAAAAAAAAQAAAAAAAAAGhlbGxvd29ybGRwbHVnaW4taW1hZ2VsZXNzLXZlcnNpb24vVVQFAAHPUK5fUEsBAgAACgAAAAgADwttUQT7a+JUAAAAdQAAAC4ACQAAAAAAAQAAAAAASgAAAGhlbGxvd29ybGRwbHVnaW4taW1hZ2VsZXNzLXZlcnNpb24vLnByZXR0aWVycmNVVAUAAc9Qrl9QSwECAAAKAAAACAAPC21Rv2YPcusAAADzAQAAKwAJAAAAAAABAAAAAADzAAAAaGVsbG93b3JsZHBsdWdpbi1pbWFnZWxlc3MtdmVyc2lvbi9pbmRleC5qc1VUBQABz1CuX1BLAQIAAAoAAAAIAA8LbVFaYghZLwAAADkAAAApAAkAAAAAAAEAAAAAADACAABoZWxsb3dvcmxkcGx1Z2luLWltYWdlbGVzcy12ZXJzaW9uL2xpYi5qc1VUBQABz1CuX1BLAQIAAAoAAAAIAA8LbVE7Twti+wAAAO0BAAAvAAkAAAAAAAEAAAAAAK8CAABoZWxsb3dvcmxkcGx1Z2luLWltYWdlbGVzcy12ZXJzaW9uL3BhY2thZ2UuanNvblVUBQABz1CuX1BLAQIAAAoAAAAIAA8LbVG7wLyhzQAAAFMBAAAuAAkAAAAAAAEAAAAAAAAEAABoZWxsb3dvcmxkcGx1Z2luLWltYWdlbGVzcy12ZXJzaW9uL3BsdWdpbi5qc29uVVQFAAHPUK5fUEsFBgAAAAAGAAYATAIAACIFAAAoAGQ1YWExZDJiOGE1MzRmMzdjZDkzYmU0OGIyMTRmNDkwZWY5ZWU5MDQ='
2020-12-01 20:31:35 +01:00
2022-03-10 12:18:24 +01:00
describe ( 'utils' , ( ) = > {
test ( 'bufferToStream' , ( ) = > {
const buffer = Buffer . from ( zip , 'base64' )
const stream = bufferToStream ( buffer )
expect ( stream . read ( ) ) . toEqual ( buffer )
} )
2020-12-08 23:01:46 +01:00
2022-03-10 12:18:24 +01:00
test ( 'cloneObject' , ( ) = > {
const o1 = [ 'string' , 'value' ]
expect ( cloneObject ( o1 ) ) . toEqual ( o1 )
expect ( cloneObject ( o1 ) === o1 ) . toBe ( false )
2020-12-08 23:01:46 +01:00
2022-03-10 12:18:24 +01:00
const o2 = { key : 'value' }
expect ( cloneObject ( o2 ) ) . toEqual ( o2 )
expect ( cloneObject ( o2 ) === o2 ) . toBe ( false )
2020-12-08 23:01:46 +01:00
2022-03-10 12:18:24 +01:00
const o3 = { key : 'value' , nested : [ 'a1' , 'a2' ] , nestedObj : { key : 'other' } }
expect ( cloneObject ( o3 ) ) . toEqual ( o3 )
expect ( cloneObject ( o3 ) === o3 ) . toBe ( false )
expect ( ( cloneObject ( o3 ) as typeof o3 ) . nested === o3 . nested ) . toBe ( false )
expect ( ( cloneObject ( o3 ) as typeof o3 ) . nestedObj === o3 . nestedObj ) . toBe ( false )
2020-12-17 16:50:03 +01:00
2022-03-10 12:18:24 +01:00
const o4 = null
expect ( cloneObject ( o4 ) ) . toEqual ( o4 )
expect ( cloneObject ( o4 ) === o4 ) . toBe ( true )
2020-12-17 16:50:03 +01:00
2022-03-10 12:18:24 +01:00
const o5 = 'string'
expect ( cloneObject ( o5 ) ) . toEqual ( o5 )
expect ( cloneObject ( o5 ) === o5 ) . toBe ( true )
} )
2020-12-17 16:50:03 +01:00
2022-03-10 12:18:24 +01:00
describe ( 'UUID' , ( ) = > {
describe ( '#constructor' , ( ) = > {
it ( 'works with a valid string' , ( ) = > {
const uuid = new UUID ( '99aBcDeF-1234-4321-0000-dcba87654321' )
2020-12-17 16:50:03 +01:00
expect ( uuid . array ) . toStrictEqual (
new Uint8Array ( [
2021-06-14 14:53:55 +02:00
0x99 , 0xab , 0xcd , 0xef , 0x12 , 0x34 , 0x43 , 0x21 , 0 , 0 , 0xdc , 0xba , 0x87 , 0x65 , 0x43 , 0x21 ,
2020-12-17 16:50:03 +01:00
] )
)
2022-03-10 12:18:24 +01:00
} )
it ( 'throws on an invalid string' , ( ) = > {
expect ( ( ) = > new UUID ( '99aBcDeF-1234-4321-WXyz-dcba87654321' ) ) . toThrow ( ) // "WXyz" are not hexadecimal
expect ( ( ) = > new UUID ( '99aBcDeF123443210000dcba87654321' ) ) . toThrow ( ) // lack of hyphens
expect ( ( ) = > new UUID ( '99aBcDeF-1234-4321-0000-dcba87654321A' ) ) . toThrow ( ) // one character too many
expect ( ( ) = > new UUID ( '99aBcDeF-1234-4321-0000-dcba8765432' ) ) . toThrow ( ) // one character too few
expect ( ( ) = > new UUID ( '' ) ) . toThrow ( ) // empty string
} )
it ( 'works with a Uint8Array' , ( ) = > {
for ( let i = 0 ; i < 10 ; i ++ ) {
const uuid = new UUID (
new Uint8Array ( [
0x99 , 0xab , 0xcd , 0xef , 0x12 , 0x34 , 0x43 , 0x21 , 0 , 0 , 0xdc , 0xba , 0x87 , 0x65 , 0x43 , 0x21 ,
] )
)
expect ( uuid . array ) . toStrictEqual (
new Uint8Array ( [
0x99 , 0xab , 0xcd , 0xef , 0x12 , 0x34 , 0x43 , 0x21 , 0 , 0 , 0xdc , 0xba , 0x87 , 0x65 , 0x43 , 0x21 ,
] )
)
}
} )
it ( 'works with a random buffer' , ( ) = > {
for ( let i = 0 ; i < 10 ; i ++ ) {
const uuid = new UUID ( randomBytes ( 16 ) )
expect ( uuid . array ) . toHaveLength ( 16 )
}
} )
2020-12-17 16:50:03 +01:00
} )
2022-03-10 12:18:24 +01:00
describe ( '#valueOf' , ( ) = > {
it ( 'returns the right big integer' , ( ) = > {
const uuid = new UUID ( '99aBcDeF-1234-4321-0000-dcba87654321' )
2022-08-16 08:44:52 +02:00
2022-03-10 12:18:24 +01:00
expect ( uuid . valueOf ( ) ) . toStrictEqual ( 0x99abcdef123443210000dcba87654321 n )
} )
2020-12-17 16:50:03 +01:00
} )
2022-03-10 12:18:24 +01:00
describe ( '#toString' , ( ) = > {
it ( 'returns the right string' , ( ) = > {
const original = '99aBcDeF-1234-4321-0000-dcba87654321'
const uuid = new UUID ( original )
const uuidString = uuid . toString ( )
// 32 hexadecimal digits + 4 hyphens
expect ( uuidString ) . toHaveLength ( 36 )
expect ( uuidString ) . toStrictEqual ( original . toLowerCase ( ) )
} )
2020-12-17 16:50:03 +01:00
} )
} )
2022-03-10 12:18:24 +01:00
describe ( 'UUIDT' , ( ) = > {
it ( 'is well-formed' , ( ) = > {
const uuidt = new UUIDT ( )
const uuidtString = uuidt . toString ( )
// UTC timestamp matching (roughly, only comparing the beginning as the timestamp's end inevitably drifts away)
expect ( uuidtString . slice ( 0 , 8 ) ) . toEqual ( Date . now ( ) . toString ( 16 ) . padStart ( 12 , '0' ) . slice ( 0 , 8 ) )
// series matching
expect ( uuidtString . slice ( 14 , 18 ) ) . toEqual ( '0000' )
2020-12-17 16:50:03 +01:00
} )
} )
2021-02-04 14:59:46 +01:00
2022-03-10 12:18:24 +01:00
describe ( 'sanitizeSqlIdentifier' , ( ) = > {
it ( 'removes all characters that are neither letter, digit or underscore and adds quotes around identifier' , ( ) = > {
const rawIdentifier = 'some_field"; DROP TABLE actually_an_injection-9;'
2021-02-04 14:59:46 +01:00
2022-03-10 12:18:24 +01:00
const sanitizedIdentifier = sanitizeSqlIdentifier ( rawIdentifier )
2021-02-04 14:59:46 +01:00
2022-03-10 12:18:24 +01:00
expect ( sanitizedIdentifier ) . toStrictEqual ( 'some_fieldDROPTABLEactually_an_injection9' )
} )
2021-02-04 14:59:46 +01:00
} )
2022-03-10 12:18:24 +01:00
describe ( 'escapeClickHouseString' , ( ) = > {
it ( 'escapes single quotes and slashes' , ( ) = > {
const rawString = "insert'escape \\"
2021-02-04 14:59:46 +01:00
2022-03-10 12:18:24 +01:00
const sanitizedString = escapeClickHouseString ( rawString )
2021-02-04 14:59:46 +01:00
2022-03-10 12:18:24 +01:00
expect ( sanitizedString ) . toStrictEqual ( "insert\\'escape \\\\" )
} )
2021-02-04 14:59:46 +01:00
} )
2021-05-27 12:55:20 +02:00
2022-03-10 12:18:24 +01:00
describe ( 'groupBy' , ( ) = > {
it ( 'groups simple objects' , ( ) = > {
const objects = [
2021-05-27 12:55:20 +02:00
{ i : 2 , foo : 'x' } ,
{ i : 2 , foo : 'y' } ,
2022-03-10 12:18:24 +01:00
{ i : 4 , foo : 'x' } ,
{ i : 7 , foo : 'z' } ,
]
const groupingByI = groupBy ( objects , 'i' )
expect ( groupingByI ) . toEqual ( {
2 : [
{ i : 2 , foo : 'x' } ,
{ i : 2 , foo : 'y' } ,
] ,
4 : [ { i : 4 , foo : 'x' } ] ,
7 : [ { i : 7 , foo : 'z' } ] ,
} )
const groupingByFoo = groupBy ( objects , 'foo' )
expect ( groupingByFoo ) . toEqual ( {
x : [
{ i : 2 , foo : 'x' } ,
{ i : 4 , foo : 'x' } ,
] ,
y : [ { i : 2 , foo : 'y' } ] ,
z : [ { i : 7 , foo : 'z' } ] ,
} )
2021-05-27 12:55:20 +02:00
} )
2022-03-10 12:18:24 +01:00
it ( 'handles undefineds' , ( ) = > {
const objects = [ { i : 2 , foo : 'x' } , { i : 2 , foo : 'y' } , { i : 4 , foo : 'x' } , { foo : 'z' } ]
const groupingByI = groupBy ( objects , 'i' )
expect ( groupingByI ) . toEqual ( {
2 : [
{ i : 2 , foo : 'x' } ,
{ i : 2 , foo : 'y' } ,
] ,
4 : [ { i : 4 , foo : 'x' } ] ,
undefined : [ { foo : 'z' } ] ,
} )
} )
it ( 'works in flat mode' , ( ) = > {
const objects = [
2021-05-27 12:55:20 +02:00
{ i : 2 , foo : 'x' } ,
{ i : 4 , foo : 'x' } ,
2022-03-10 12:18:24 +01:00
{ i : 7 , foo : 'z' } ,
]
const groupingByI = groupBy ( objects , 'i' , true )
expect ( groupingByI ) . toEqual ( {
2 : { i : 2 , foo : 'x' } ,
4 : { i : 4 , foo : 'x' } ,
7 : { i : 7 , foo : 'z' } ,
} )
2021-05-27 12:55:20 +02:00
} )
2022-03-10 12:18:24 +01:00
it ( "doesn't work in flat mode if multiple values match a single key" , ( ) = > {
const objects = [
2021-05-27 12:55:20 +02:00
{ i : 2 , foo : 'x' } ,
{ i : 2 , foo : 'y' } ,
2022-03-10 12:18:24 +01:00
{ i : 4 , foo : 'x' } ,
{ i : 7 , foo : 'z' } ,
]
2021-05-27 12:55:20 +02:00
2022-03-10 12:18:24 +01:00
expect ( ( ) = > groupBy ( objects , 'i' , true ) ) . toThrowError (
'Key "i" has more than one matching value, which is not allowed in flat groupBy!'
)
2021-05-27 12:55:20 +02:00
} )
} )
2022-03-10 12:18:24 +01:00
describe ( 'stringify' , ( ) = > {
it ( 'leaves strings unaffected' , ( ) = > {
expect ( stringify ( 'test!' ) ) . toStrictEqual ( 'test!' )
} )
2021-06-18 16:23:08 +02:00
2022-03-10 12:18:24 +01:00
it ( 'transforms numbers into strings' , ( ) = > {
expect ( stringify ( 3 ) ) . toStrictEqual ( '3' )
expect ( stringify ( 21.37 ) ) . toStrictEqual ( '21.37' )
} )
2021-06-18 16:23:08 +02:00
2022-03-10 12:18:24 +01:00
it ( 'transforms nullish values into strings' , ( ) = > {
expect ( stringify ( null ) ) . toStrictEqual ( 'null' )
expect ( stringify ( undefined ) ) . toStrictEqual ( 'undefined' )
} )
2021-06-18 16:23:08 +02:00
2022-03-10 12:18:24 +01:00
it ( 'transforms object values into strings' , ( ) = > {
expect ( stringify ( { } ) ) . toStrictEqual ( '{}' )
expect ( stringify ( [ ] ) ) . toStrictEqual ( '[]' )
} )
2021-06-18 16:23:08 +02:00
} )
2022-04-11 16:59:38 +02:00
describe ( 'safeClickhouseString' , ( ) = > {
// includes real data
const validStrings = [
` $ autocapture ` ,
` correlation analyzed ` ,
` docs_search_used ` ,
` $ $ plugin_metrics ` ,
` 996f3e2f-830b-42f0-b2b8-df42bb7f7144 ` ,
` some?819)389**^371=2++211!!@==-''''..,,weird___id ` ,
` form.form-signin:attr__action="/signup"attr__class="form-signin"attr__method="post"nth-child="1"nth-of-type="1";body:nth-child="2"nth-of-type="1" ` ,
` a:attr__href="/signup"href="/signup"nth-child="1"nth-of-type="1"text="Create one here.";p:nth-child="8"nth-of-type="1";form.form-signin:attr__action="/login"attr__class="form-signin"attr__method="post"nth-child="1"nth-of-type="1";body:nth-child="2"nth-of-type="1" ` ,
` input:nth-child="7"nth-of-type="3";form.form-signin:attr__action="/signup"attr__class="form-signin"attr__method="post"nth-child="1"nth-of-type="1";body:nth-child="2"nth-of-type="1" ` ,
` a.nav-link:attr__class="nav-link"attr__href="/actions"href="/actions"nth-child="1"nth-of-type="1"text="Actions";li:nth-child="2"nth-of-type="2";ul.flex-sm-column.nav:attr__class="nav flex-sm-column"nth-child="1"nth-of-type="1";div.bg-light.col-md-2.col-sm-3.flex-shrink-1.pt-3.sidebar:attr__class="col-sm-3 col-md-2 sidebar flex-shrink-1 bg-light pt-3"attr__style="min-height: 100vh;"nth-child="1"nth-of-type="1";div.flex-column.flex-fill.flex-sm-row.row:attr__class="row flex-fill flex-column flex-sm-row"nth-child="1"nth-of-type="1";div.container-fluid.d-flex.flex-grow-1:attr__class="container-fluid flex-grow-1 d-flex"nth-child="1"nth-of-type="1";div:attr__id="root"attr_id="root"nth-child="1"nth-of-type="1";body:nth-child="2"nth-of-type="1" ` ,
]
test ( 'does not modify valid strings' , ( ) = > {
for ( const str of validStrings ) {
expect ( safeClickhouseString ( str ) ) . toEqual ( str )
}
} )
test ( 'handles surrogate unicode characters correctly' , ( ) = > {
expect ( safeClickhouseString ( ` foo \ ud83d \ bar ` ) ) . toEqual ( ` foo \\ ud83d \\ bar ` )
expect ( safeClickhouseString ( ` \ ud83d \ bar ` ) ) . toEqual ( ` \\ ud83d \\ bar ` )
expect ( safeClickhouseString ( ` \ ud800 \ \ ud803 \ ` ) ) . toEqual ( ` \\ ud800 \\ \\ ud803 \\ ` )
} )
test ( 'does not modify non-surrogate unicode characters' , ( ) = > {
expect ( safeClickhouseString ( ` ✨ ` ) ) . toEqual ( ` ✨ ` )
expect ( safeClickhouseString ( ` foo \ u2728 \ bar ` ) ) . toEqual ( ` foo \ u2728 \ bar ` )
expect ( safeClickhouseString ( ` 💜 \ u1f49c \ 💜 ` ) ) . toEqual ( ` 💜 \ u1f49c \ 💜 ` )
} )
} )
2022-08-15 09:54:09 +02:00
describe ( 'clickHouseTimestampToDateTime()' , ( ) = > {
it ( 'casts to a datetime' , ( ) = > {
expect ( clickHouseTimestampToDateTime ( '2020-02-23 02:15:00.00' as ClickHouseTimestamp ) ) . toEqual (
DateTime . fromISO ( '2020-02-23T02:15:00.000Z' ) . toUTC ( )
)
} )
} )
2021-11-04 11:38:20 +01:00
} )
2023-06-05 18:25:39 +02:00
describe ( 'getPropertyValueByPath' , ( ) = > {
it ( 'returns primitive value when present' , ( ) = > {
expect ( getPropertyValueByPath ( { a : { b : 1 } } , [ 'a' , 'b' ] ) ) . toEqual ( 1 )
} )
it ( 'returns object value when present' , ( ) = > {
expect ( getPropertyValueByPath ( { a : { b : 1 } } , [ 'a' ] ) ) . toEqual ( { b : 1 } )
} )
it ( 'returns undefined when not present' , ( ) = > {
expect ( getPropertyValueByPath ( { a : { b : 1 } } , [ 'a' , 'c' ] ) ) . toEqual ( undefined )
} )
it ( 'returns undefined when trying to access a property of a primitive' , ( ) = > {
expect ( getPropertyValueByPath ( { a : { b : 1 } } , [ 'a' , 'b' , 'c' , 'd' ] ) ) . toEqual ( undefined )
} )
it ( 'returns value from array' , ( ) = > {
expect ( getPropertyValueByPath ( { a : { b : [ 1 , 2 , 3 ] } } , [ 'a' , 'b' , '1' ] ) ) . toEqual ( 2 )
} )
it ( 'requires at least one path key' , ( ) = > {
expect ( ( ) = > getPropertyValueByPath ( { a : { b : 'foo' } } , [ ] ) ) . toThrowError ( 'No path to property was provided' )
} )
} )