Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Enforce SavedObject.attributes to have a serializable object shape #123605

Closed

Conversation

pgayvallet
Copy link
Contributor

@pgayvallet pgayvallet commented Jan 24, 2022

Summary

Part of #123575

This PR is an attempt to enforce better attributes type for the SavedObject type and all the related APIs.

Note: this PR only impact typescript types and interfaces. The real runtime validation of the shape of the attributes when calling create/bulkCreate will be done in another PR.

Checklist

  • Documentation was added for features that require explanation or tutorials

@pgayvallet pgayvallet changed the title Force savedObject attributes to have an object shape Enforce SavedObject.attributes to have a serializable object shape Jan 25, 2022
Copy link
Contributor Author

@pgayvallet pgayvallet left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

This is an attempt to add better type checking around the saved object attributes shape, to avoid the usage of types such as undefined or javascript primitives or arrays.

As the SavedObject type is used in a lot of places, this required to modify quite a lot of files to propagate the type changes.

To be honest, I'm not 100% sure these changes are really worth it, which is why I'd like to get the team's opinion on these changes.

Note: to address #123575, we'll add proper runtime validation for create and bulkCreate in another PR anyway.

@@ -66,7 +68,7 @@ export interface SavedObjectsMigrationVersion {
[pluginName: string]: string;
}

export interface SavedObject<T = unknown> {
export interface SavedObject<T extends SavedObjectAttributes = SavedObjectAttributes> {
Copy link
Contributor Author

@pgayvallet pgayvallet Jan 25, 2022

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

So this is the root change of the PR. We're no longer defaulting to unknown and we're enforcing that T extends a serializable object/record, which will allow to detect misusages such as

soClient.create('dashboard', undefined, { id: 'test-undefined' });

Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I've tried something like this long ago but ended up abandoning it.

A serializable type means that specific types like https://github.com/elastic/kibana/blob/main/src/plugins/dashboard/public/application/lib/load_dashboard_by_title.ts#L16 fail because they don't have an index signature. But if we change the type to have an index signature it becomes less type safe because it allows properties not specified by the plugin's own generic.

So ideally we want to maybe say, if you provide your own type we'll take that as is (without the index signature) but if you don't provide a type we'll use Serializable because that's safer than unknown. It also seems like this should only apply to create operations, because when reading unknown is a safer type, and nudges plugins to create better types whereas Serializable will allow any property access.

So I don't know if there's a way to always provide better type safety, but IMO we're improving some type safety but reducing it in other places.

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Yea, that's my feeling exactly.

There's no way to tell typescript that what we want as a type is an object WITHOUT index signature, without any property by default, and that can be extended, but only by objects having the correct type for their underlying properties (e.g serializable), which is a pain.

Comment on lines +24 to +26
// type required for index signature
// eslint-disable-next-line @typescript-eslint/consistent-type-definitions
export type SavedQueryAttributes = {
Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

This is one of the side-effect of having T extends SavedObjectAttributes. We're forced to use TS types instead of interfaces when overloading the default type of T to avoid an index signature error.

interface SavedObjectAttributes {
  [key: string]: SavedObjectAttribute;
}

interface SavedObject<T extends SavedObjectAttributes = SavedObjectAttributes> { ... }

// valid
let t: SavedObject<{ foo: string; bar: number }>

// valid but eslint error because we should be using an interface
type Foo = { foo: string; bar: number };
let t: SavedObject<Foo>;

// TS2344: Type 'Foo' does not satisfy the constraint 'SavedObjectAttributes'. Index signature for type 'string' is missing in type 'Foo'.
interface Foo { foo: string; bar: number };
let t: SavedObject<Foo>;

See microsoft/TypeScript#15300 for more details

it's not really blocker for the changes, but with our lint rules enforcing that we're using interfaces instead of types, this is awkward as it forces to add a linter exception for all type-based SO properties structures

Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

IMO interface Foo extends SavedObjectAttributes { ... } should work.

However, there's the annoyance of importing the type everywhere. Also, it would allow devs to add additional non-typed keys because of the index signature 😕


I wonder if we should find an alternative to the current definition of SavedObjectAttributes. The current index signature does not stop us from storing an empty saved object:

await context.core.savedObjects.client.create('my_type', {});

So, probably, defining SavedObjectAttributes as type SavedObjectAttributes = object is enough to ensure that an object is specified (vs. undefined, null or any other primitive), and we avoid the issue type vs. interface issue. WDYT?

Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

we hit a similar issue with persistable state interfaces and went on changing a bunch of interfaces to types

const response = (await this.savedObjectClient.find<T>(options)).savedObjects;
return response.map<SavedObject<T>>(simpleSavedObjectToSavedObject);
}

async get<T = unknown>(type: string, id: string) {
async get<T extends SavedObjectAttributes = SavedObjectAttributes>(type: string, id: string) {
Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

This is the second thing a bit bothersome with that T extends SavedObjectAttributes = SavedObjectAttributes type change. We can no longer use unknown as a default for higher-level APIs when the shape of the attributes is not relevant, forcing to adapt all these higher-level APIs to also define the correct generic types.

@pgayvallet pgayvallet added Feature:Saved Objects Team:Core Core services & architecture: plugins, logging, config, saved objects, http, ES client, i18n, etc labels Jan 25, 2022
@pgayvallet pgayvallet requested a review from a team January 25, 2022 12:51
@pgayvallet pgayvallet marked this pull request as ready for review January 25, 2022 12:52
@pgayvallet pgayvallet requested a review from a team as a code owner January 25, 2022 12:52
@elasticmachine
Copy link
Contributor

Pinging @elastic/kibana-core (Team:Core)

@pgayvallet pgayvallet added release_note:skip Skip the PR/issue when compiling release notes v8.1.0 labels Jan 25, 2022
@kibana-ci
Copy link
Collaborator

kibana-ci commented Jan 31, 2022

💔 Build Failed

Failed CI Steps

Metrics [docs]

✅ unchanged

History

To update your PR or re-run it, just comment with:
@elasticmachine merge upstream

@@ -115,7 +115,6 @@ export type {
SavedObject,
SavedObjectAttribute,
SavedObjectAttributes,
SavedObjectAttributeSingle,
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

NIT: Do we need to run yarn docs:acceptApiChanges --docs?

@@ -533,7 +548,9 @@ export class SavedObjectsClient {
};
};

private async performBulkResolve<T>(objects: ObjectTypeAndId[]) {
private async performBulkResolve<T extends SavedObjectAttributes = SavedObjectAttributes>(
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

nit: Previously, T wasn't optional. Should we keep it that way?

@@ -176,7 +176,10 @@ export interface SavedObjectsResolveImportErrorsOptions {
createNewCopies: boolean;
}

export type CreatedObject<T> = SavedObject<T> & { destinationId?: string };
export type CreatedObject<T extends SavedObjectAttributes = SavedObjectAttributes> =
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Are we OK with making T optional when it was required?

@@ -11,7 +11,7 @@ import { SavedObjectsImportFailure, CreatedObject } from '../types';

export function extractErrors(
// TODO: define saved object type
savedObjectResults: Array<CreatedObject<unknown>>,
savedObjectResults: CreatedObject[],
savedObjectsToImport: Array<SavedObject<any>>
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

nit: Removing anys

Suggested change
savedObjectsToImport: Array<SavedObject<any>>
savedObjectsToImport: Array<SavedObject<{ title?: string }>>

@@ -33,7 +22,7 @@ export type SavedObjectAttribute = SavedObjectAttributeSingle | SavedObjectAttri
* @public
*/
export interface SavedObjectAttributes {
[key: string]: SavedObjectAttribute;
[key: string]: Serializable;
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

This change is not needed. Is it?

Comment on lines +24 to +26
// type required for index signature
// eslint-disable-next-line @typescript-eslint/consistent-type-definitions
export type SavedQueryAttributes = {
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

IMO interface Foo extends SavedObjectAttributes { ... } should work.

However, there's the annoyance of importing the type everywhere. Also, it would allow devs to add additional non-typed keys because of the index signature 😕


I wonder if we should find an alternative to the current definition of SavedObjectAttributes. The current index signature does not stop us from storing an empty saved object:

await context.core.savedObjects.client.create('my_type', {});

So, probably, defining SavedObjectAttributes as type SavedObjectAttributes = object is enough to ensure that an object is specified (vs. undefined, null or any other primitive), and we avoid the issue type vs. interface issue. WDYT?

@afharo afharo requested a review from a team January 31, 2022 13:02
Copy link
Member

@ppisljar ppisljar left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

app services changes LGTM

@pgayvallet
Copy link
Contributor Author

After discussing it with @rudolf we came to the conclusion that this wasn't worth it because of the TS limitation around what we're trying to achieve (#123605 (comment)). Closing

@pgayvallet pgayvallet closed this Feb 9, 2022
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment
Labels
Feature:Saved Objects release_note:skip Skip the PR/issue when compiling release notes Team:Core Core services & architecture: plugins, logging, config, saved objects, http, ES client, i18n, etc v8.1.0
Projects
None yet
Development

Successfully merging this pull request may close these issues.

6 participants