Skip to content

Commit

Permalink
Wrap rison-node to improve types (#146649)
Browse files Browse the repository at this point in the history
@maximpn brought up the issues caused by the types required by the
rison-node package, which attempted to communicate that "encoded values
must be primitive values, or recursive arrays/object of primitive
values". This isn't actually expressible in TypeScript, which lead to
many instances of `rison.encode(value as unknown as RisonValue)` which
is useless. Additionally, the rison-node library actually supports any
value and will either produce valid rison or `undefined` for that value.

To address this I'm adding a wrapper function which accepts `any` and
returns a `string`. If rison-node is totally unable to produce any rison
for the value (because the value is `undefined` or some other type like
Symbol or BigInt) the `encode()` function will throw. If you're
accepting arbitrary input you can use the `encodeUnknown()` function,
which will return a string or undefined, if the value you provided has
zero rison representation.

Like JSON.stringify() any non-circular primitive, object, or array can
be encoded with either function. If the values within those objects are
not encodable (functions, RegExps, etc) then they will be skipped. Any
object/array with the `toJSON()` method will be converted to JSON first,
and if the prototype of the object has the `encode_rison()` method it
will be used to convert he value into rison.

The changes in this PR are mostly updating usage of rison-node to use
`@kbn/rison` (which is also enforced by eslint). There are also several
changes which remove unnecessary casting.
  • Loading branch information
Spencer authored Dec 1, 2022
1 parent 6f7c6ad commit 2e314db
Show file tree
Hide file tree
Showing 82 changed files with 491 additions and 144 deletions.
1 change: 1 addition & 0 deletions .github/CODEOWNERS
Validating CODEOWNERS rules …
Original file line number Diff line number Diff line change
Expand Up @@ -966,6 +966,7 @@ packages/kbn-plugin-helpers @elastic/kibana-operations
packages/kbn-react-field @elastic/kibana-app-services
packages/kbn-repo-source-classifier @elastic/kibana-operations
packages/kbn-repo-source-classifier-cli @elastic/kibana-operations
packages/kbn-rison @elastic/kibana-operations
packages/kbn-rule-data-utils @elastic/security-detections-response @elastic/actionable-observability @elastic/response-ops
packages/kbn-safer-lodash-set @elastic/kibana-security
packages/kbn-securitysolution-autocomplete @elastic/security-solution-platform
Expand Down
1 change: 1 addition & 0 deletions package.json
Original file line number Diff line number Diff line change
Expand Up @@ -356,6 +356,7 @@
"@kbn/osquery-io-ts-types": "link:bazel-bin/packages/kbn-osquery-io-ts-types",
"@kbn/plugin-discovery": "link:bazel-bin/packages/kbn-plugin-discovery",
"@kbn/react-field": "link:bazel-bin/packages/kbn-react-field",
"@kbn/rison": "link:bazel-bin/packages/kbn-rison",
"@kbn/rule-data-utils": "link:bazel-bin/packages/kbn-rule-data-utils",
"@kbn/safer-lodash-set": "link:bazel-bin/packages/kbn-safer-lodash-set",
"@kbn/securitysolution-autocomplete": "link:bazel-bin/packages/kbn-securitysolution-autocomplete",
Expand Down
2 changes: 2 additions & 0 deletions packages/BUILD.bazel
Original file line number Diff line number Diff line change
Expand Up @@ -276,6 +276,7 @@ filegroup(
"//packages/kbn-react-field:build",
"//packages/kbn-repo-source-classifier:build",
"//packages/kbn-repo-source-classifier-cli:build",
"//packages/kbn-rison:build",
"//packages/kbn-rule-data-utils:build",
"//packages/kbn-safer-lodash-set:build",
"//packages/kbn-securitysolution-autocomplete:build",
Expand Down Expand Up @@ -640,6 +641,7 @@ filegroup(
"//packages/kbn-react-field:build_types",
"//packages/kbn-repo-source-classifier:build_types",
"//packages/kbn-repo-source-classifier-cli:build_types",
"//packages/kbn-rison:build_types",
"//packages/kbn-rule-data-utils:build_types",
"//packages/kbn-safer-lodash-set:build_types",
"//packages/kbn-securitysolution-autocomplete:build_types",
Expand Down
4 changes: 4 additions & 0 deletions packages/kbn-eslint-config/.eslintrc.js
Original file line number Diff line number Diff line change
Expand Up @@ -131,6 +131,10 @@ module.exports = {
from: '@elastic/apm-synthtrace',
to: '@kbn/apm-synthtrace',
},
{
from: 'rison-node',
to: '@kbn/rison',
},
],
],

Expand Down
123 changes: 123 additions & 0 deletions packages/kbn-rison/BUILD.bazel
Original file line number Diff line number Diff line change
@@ -0,0 +1,123 @@
load("@npm//@bazel/typescript:index.bzl", "ts_config")
load("@build_bazel_rules_nodejs//:index.bzl", "js_library")
load("//src/dev/bazel:index.bzl", "jsts_transpiler", "pkg_npm", "pkg_npm_types", "ts_project")

PKG_DIRNAME = "kbn-rison"
PKG_REQUIRE_NAME = "@kbn/rison"

SOURCE_FILES = glob(
[
"**/*.ts",
],
exclude = [
"**/*.config.js",
"**/*.mock.*",
"**/*.test.*",
"**/*.stories.*",
"**/__snapshots__/**",
"**/integration_tests/**",
"**/mocks/**",
"**/scripts/**",
"**/storybook/**",
"**/test_fixtures/**",
"**/test_helpers/**",
],
)

SRCS = SOURCE_FILES

filegroup(
name = "srcs",
srcs = SRCS,
)

NPM_MODULE_EXTRA_FILES = [
"package.json",
]

# In this array place runtime dependencies, including other packages and NPM packages
# which must be available for this code to run.
#
# To reference other packages use:
# "//repo/relative/path/to/package"
# eg. "//packages/kbn-utils"
#
# To reference a NPM package use:
# "@npm//name-of-package"
# eg. "@npm//lodash"
RUNTIME_DEPS = [
"@npm//rison-node",
]

# In this array place dependencies necessary to build the types, which will include the
# :npm_module_types target of other packages and packages from NPM, including @types/*
# packages.
#
# To reference the types for another package use:
# "//repo/relative/path/to/package:npm_module_types"
# eg. "//packages/kbn-utils:npm_module_types"
#
# References to NPM packages work the same as RUNTIME_DEPS
TYPES_DEPS = [
"@npm//@types/node",
"@npm//@types/jest",
]

jsts_transpiler(
name = "target_node",
srcs = SRCS,
build_pkg_name = package_name(),
)

ts_config(
name = "tsconfig",
src = "tsconfig.json",
deps = [
"//:tsconfig.base.json",
"//:tsconfig.bazel.json",
],
)

ts_project(
name = "tsc_types",
args = ['--pretty'],
srcs = SRCS,
deps = TYPES_DEPS,
declaration = True,
emit_declaration_only = True,
out_dir = "target_types",
tsconfig = ":tsconfig",
)

js_library(
name = PKG_DIRNAME,
srcs = NPM_MODULE_EXTRA_FILES,
deps = RUNTIME_DEPS + [":target_node"],
package_name = PKG_REQUIRE_NAME,
visibility = ["//visibility:public"],
)

js_library(
name = "npm_module_types",
srcs = NPM_MODULE_EXTRA_FILES,
deps = RUNTIME_DEPS + [":target_node", ":tsc_types"],
package_name = PKG_REQUIRE_NAME,
visibility = ["//visibility:public"],
)

pkg_npm(
name = "npm_module",
deps = [":" + PKG_DIRNAME],
)

filegroup(
name = "build",
srcs = [":npm_module"],
visibility = ["//visibility:public"],
)

filegroup(
name = "build_types",
srcs = [":npm_module_types"],
visibility = ["//visibility:public"],
)
3 changes: 3 additions & 0 deletions packages/kbn-rison/README.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,3 @@
# @kbn/rison

A simple wrapper around [rison-node](https://github.com/w33ble/rison-node) which gives us types and ensures that values are always encoded to a string.
20 changes: 20 additions & 0 deletions packages/kbn-rison/index.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,20 @@
/*
* Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one
* or more contributor license agreements. Licensed under the Elastic License
* 2.0 and the Server Side Public License, v 1; you may not use this file except
* in compliance with, at your election, the Elastic License 2.0 or the Server
* Side Public License, v 1.
*/

export * from './kbn_rison';

import { encode, encodeUnknown, decode, encodeArray, decodeArray } from './kbn_rison';
// maintain compatibility with 'rison-node' and include a default export
// eslint-disable-next-line import/no-default-export
export default {
encode,
encodeUnknown,
decode,
encodeArray,
decodeArray,
};
13 changes: 13 additions & 0 deletions packages/kbn-rison/jest.config.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,13 @@
/*
* Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one
* or more contributor license agreements. Licensed under the Elastic License
* 2.0 and the Server Side Public License, v 1; you may not use this file except
* in compliance with, at your election, the Elastic License 2.0 or the Server
* Side Public License, v 1.
*/

module.exports = {
preset: '@kbn/test/jest_node',
rootDir: '../..',
roots: ['<rootDir>/packages/kbn-rison'],
};
99 changes: 99 additions & 0 deletions packages/kbn-rison/kbn_rison.test.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,99 @@
/*
* Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one
* or more contributor license agreements. Licensed under the Elastic License
* 2.0 and the Server Side Public License, v 1; you may not use this file except
* in compliance with, at your election, the Elastic License 2.0 or the Server
* Side Public License, v 1.
*/

import * as Rison from './kbn_rison';

describe('encoding', () => {
it('encodes basic values', () => {
expect(Rison.encode(false)).toMatchInlineSnapshot(`"!f"`);
expect(Rison.encode(true)).toMatchInlineSnapshot(`"!t"`);
expect(Rison.encode(1)).toMatchInlineSnapshot(`"1"`);
expect(Rison.encode([1])).toMatchInlineSnapshot(`"!(1)"`);
expect(Rison.encode(['1'])).toMatchInlineSnapshot(`"!('1')"`);
expect(Rison.encode([null])).toMatchInlineSnapshot(`"!(!n)"`);
expect(Rison.encode([undefined])).toMatchInlineSnapshot(`"!()"`);
expect(Rison.encode(null)).toMatchInlineSnapshot(`"!n"`);
});
it('throws if it received undefined', () => {
expect(() => Rison.encode(undefined)).toThrowErrorMatchingInlineSnapshot(
`"unable to encode value into rison, expected a primative value array or object"`
);
});
it('encodes a complex object', () => {
expect(
Rison.encode({
foo: 1,
bar: {
bax: 1,
bar: [
'x',
{
a: /foo/,
b: new Date(0),
},
],
},
})
).toMatchInlineSnapshot(`"(bar:(bar:!(x,(a:(),b:'1970-01-01T00:00:00.000Z')),bax:1),foo:1)"`);
});
it('encodes arrays directly as well', () => {
expect(Rison.encodeArray([1, 2, 3])).toMatchInlineSnapshot(`"1,2,3"`);
});
});

describe('decoding', () => {
it('decodes a simple rison string', () => {
expect(Rison.decode('!f')).toMatchInlineSnapshot(`false`);
expect(Rison.decode('!t')).toMatchInlineSnapshot(`true`);
expect(Rison.decode('1')).toMatchInlineSnapshot(`1`);
expect(Rison.decode('!(1)')).toMatchInlineSnapshot(`
Array [
1,
]
`);
expect(Rison.decode("!('1')")).toMatchInlineSnapshot(`
Array [
"1",
]
`);
expect(Rison.decode('!(!n)')).toMatchInlineSnapshot(`
Array [
null,
]
`);
expect(Rison.decode('!()')).toMatchInlineSnapshot(`Array []`);
expect(Rison.decode('!n')).toMatchInlineSnapshot(`null`);
});
it('decodes a complex rison string', () => {
expect(Rison.decode(`(bar:(bar:!(x,(a:(),b:'1970-01-01T00:00:00.000Z')),bax:1),foo:1)`))
.toMatchInlineSnapshot(`
Object {
"bar": Object {
"bar": Array [
"x",
Object {
"a": Object {},
"b": "1970-01-01T00:00:00.000Z",
},
],
"bax": 1,
},
"foo": 1,
}
`);
});
it('decodes an encoded array', () => {
expect(Rison.decodeArray('1,2,3')).toMatchInlineSnapshot(`
Array [
1,
2,
3,
]
`);
});
});
59 changes: 59 additions & 0 deletions packages/kbn-rison/kbn_rison.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,59 @@
/*
* Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one
* or more contributor license agreements. Licensed under the Elastic License
* 2.0 and the Server Side Public License, v 1; you may not use this file except
* in compliance with, at your election, the Elastic License 2.0 or the Server
* Side Public License, v 1.
*/

// @ts-expect-error untyped module from npm
// eslint-disable-next-line @kbn/eslint/module_migration
import Rison from 'rison-node';

export type RisonValue =
| boolean
| string
| number
| RisonValue[]
| { [key: string]: RisonValue }
| null;

export function encodeUnknown(obj: any): string | undefined {
return Rison.encode(obj);
}

/**
* rison-encode a javascript structure
*/
export function encode(obj: any) {
const rison = encodeUnknown(obj);
if (rison === undefined) {
throw new Error(
'unable to encode value into rison, expected a primative value array or object'
);
}
return rison;
}

/**
* parse a rison string into a javascript structure.
*/
export function decode(rison: string): RisonValue {
return Rison.decode(rison);
}

/**
* rison-encode a javascript array without surrounding parens
*/
export function encodeArray(array: any[]) {
return Rison.encode_array(array);
}

/**
* parse an a-rison string into a javascript structure.
*
* this simply adds array markup around the string before parsing.
*/
export function decodeArray(rison: string): RisonValue[] {
return Rison.decode_array(rison);
}
7 changes: 7 additions & 0 deletions packages/kbn-rison/kibana.jsonc
Original file line number Diff line number Diff line change
@@ -0,0 +1,7 @@
{
"type": "shared-common",
"id": "@kbn/rison",
"owner": "@elastic/kibana-operations",
"runtimeDeps": [],
"typeDeps": [],
}
8 changes: 8 additions & 0 deletions packages/kbn-rison/package.json
Original file line number Diff line number Diff line change
@@ -0,0 +1,8 @@
{
"name": "@kbn/rison",
"private": true,
"version": "1.0.0",
"main": "./target_node/index.js",
"types": "./target_types/index.d.ts",
"license": "SSPL-1.0 OR Elastic License 2.0"
}
15 changes: 15 additions & 0 deletions packages/kbn-rison/tsconfig.json
Original file line number Diff line number Diff line change
@@ -0,0 +1,15 @@
{
"extends": "../../tsconfig.bazel.json",
"compilerOptions": {
"declaration": true,
"emitDeclarationOnly": true,
"outDir": "target_types",
"types": [
"jest",
"node"
]
},
"include": [
"**/*.ts",
]
}
Loading

0 comments on commit 2e314db

Please sign in to comment.