Skip to content

Commit 2b3122e

Browse files
stratoulasddonne
andauthored
[8.19] [ES|QL] Add validations and suggestions for COMPLETION (#222533) (#223206)
# Backport This will backport the following commits from `main` to `8.19`: - [[ES|QL] Add validations and suggestions for `COMPLETION` (#222533)](#222533) <!--- Backport version: 10.0.0 --> ### Questions ? Please refer to the [Backport tool documentation](https://github.com/sorenlouv/backport) <!--BACKPORT [{"author":{"name":"Sebastian Delle Donne","email":"[email protected]"},"sourceCommit":{"committedDate":"2025-06-06T17:31:33Z","message":"[ES|QL] Add validations and suggestions for `COMPLETION` (#222533)\n\n## Summary\n\nMostly covers https://github.com/elastic/kibana/issues/218052\n\nAdds autosuggest and validations to the `COMPLETION` command.\n\n``| COMPLETION <prompt> WITH <inferenceId> (AS <targetField>)``\n\n\n\nhttps://github.com/user-attachments/assets/49b6f368-097a-4d61-80f9-e08364320ad9\n\n\n## Autosuggest\n### `<prompt>`\nPrompt can be a primary expression but only one that evaluates to\ntext/keyword. The suggestions are:\n* An empty string snippet.\n* Any field or user defined column of text|keyword type.\n* Any function with text or unknown type. (Why unknown? So it suggest\nvalid flow control functions as `CASE`).\n* Once within a function or expression the command is no longer\nresponsible for the suggestions.\n\n### ` <inferenceId>`\nWill be addressed in another PR, it needs an API call to retrieve\navailable endpoints.\n\n### ` <targetField>`\nIt will suggest the default elasticsearch target column: `completion`.\n\n## Validations\nAdded a validation to check that the prompt is of type text, keyword or\nunknown.\nFunctions and operators needed to be allowed within the command.\n\n\n## Checklist\n\nCheck the PR satisfies following conditions. \n\nReviewers should verify this PR satisfies this list as well.\n\n- [x] Any text added follows [EUI's writing\nguidelines](https://elastic.github.io/eui/#/guidelines/writing), uses\nsentence case text and includes [i18n\nsupport](https://github.com/elastic/kibana/blob/main/src/platform/packages/shared/kbn-i18n/README.md)\n- [x] [Unit or functional\ntests](https://www.elastic.co/guide/en/kibana/master/development-tests.html)\nwere updated or added to match the most common scenarios\n\n---------\n\nCo-authored-by: Stratoula Kalafateli <[email protected]>\nCo-authored-by: kibanamachine <[email protected]>","sha":"f09bce1108cdd55ba69e11e8b14c947bf052dd91","branchLabelMapping":{"^v9.1.0$":"main","^v(\\d+).(\\d+).\\d+$":"$1.$2"}},"sourcePullRequest":{"labels":["release_note:skip","Feature:ES|QL","Team:ESQL","backport:version","v9.1.0","v8.19.0"],"title":"[ES|QL] Add validations and suggestions for `COMPLETION`","number":222533,"url":"https://github.com/elastic/kibana/pull/222533","mergeCommit":{"message":"[ES|QL] Add validations and suggestions for `COMPLETION` (#222533)\n\n## Summary\n\nMostly covers https://github.com/elastic/kibana/issues/218052\n\nAdds autosuggest and validations to the `COMPLETION` command.\n\n``| COMPLETION <prompt> WITH <inferenceId> (AS <targetField>)``\n\n\n\nhttps://github.com/user-attachments/assets/49b6f368-097a-4d61-80f9-e08364320ad9\n\n\n## Autosuggest\n### `<prompt>`\nPrompt can be a primary expression but only one that evaluates to\ntext/keyword. The suggestions are:\n* An empty string snippet.\n* Any field or user defined column of text|keyword type.\n* Any function with text or unknown type. (Why unknown? So it suggest\nvalid flow control functions as `CASE`).\n* Once within a function or expression the command is no longer\nresponsible for the suggestions.\n\n### ` <inferenceId>`\nWill be addressed in another PR, it needs an API call to retrieve\navailable endpoints.\n\n### ` <targetField>`\nIt will suggest the default elasticsearch target column: `completion`.\n\n## Validations\nAdded a validation to check that the prompt is of type text, keyword or\nunknown.\nFunctions and operators needed to be allowed within the command.\n\n\n## Checklist\n\nCheck the PR satisfies following conditions. \n\nReviewers should verify this PR satisfies this list as well.\n\n- [x] Any text added follows [EUI's writing\nguidelines](https://elastic.github.io/eui/#/guidelines/writing), uses\nsentence case text and includes [i18n\nsupport](https://github.com/elastic/kibana/blob/main/src/platform/packages/shared/kbn-i18n/README.md)\n- [x] [Unit or functional\ntests](https://www.elastic.co/guide/en/kibana/master/development-tests.html)\nwere updated or added to match the most common scenarios\n\n---------\n\nCo-authored-by: Stratoula Kalafateli <[email protected]>\nCo-authored-by: kibanamachine <[email protected]>","sha":"f09bce1108cdd55ba69e11e8b14c947bf052dd91"}},"sourceBranch":"main","suggestedTargetBranches":["8.19"],"targetPullRequestStates":[{"branch":"main","label":"v9.1.0","branchLabelMappingKey":"^v9.1.0$","isSourceBranch":true,"state":"MERGED","url":"https://github.com/elastic/kibana/pull/222533","number":222533,"mergeCommit":{"message":"[ES|QL] Add validations and suggestions for `COMPLETION` (#222533)\n\n## Summary\n\nMostly covers https://github.com/elastic/kibana/issues/218052\n\nAdds autosuggest and validations to the `COMPLETION` command.\n\n``| COMPLETION <prompt> WITH <inferenceId> (AS <targetField>)``\n\n\n\nhttps://github.com/user-attachments/assets/49b6f368-097a-4d61-80f9-e08364320ad9\n\n\n## Autosuggest\n### `<prompt>`\nPrompt can be a primary expression but only one that evaluates to\ntext/keyword. The suggestions are:\n* An empty string snippet.\n* Any field or user defined column of text|keyword type.\n* Any function with text or unknown type. (Why unknown? So it suggest\nvalid flow control functions as `CASE`).\n* Once within a function or expression the command is no longer\nresponsible for the suggestions.\n\n### ` <inferenceId>`\nWill be addressed in another PR, it needs an API call to retrieve\navailable endpoints.\n\n### ` <targetField>`\nIt will suggest the default elasticsearch target column: `completion`.\n\n## Validations\nAdded a validation to check that the prompt is of type text, keyword or\nunknown.\nFunctions and operators needed to be allowed within the command.\n\n\n## Checklist\n\nCheck the PR satisfies following conditions. \n\nReviewers should verify this PR satisfies this list as well.\n\n- [x] Any text added follows [EUI's writing\nguidelines](https://elastic.github.io/eui/#/guidelines/writing), uses\nsentence case text and includes [i18n\nsupport](https://github.com/elastic/kibana/blob/main/src/platform/packages/shared/kbn-i18n/README.md)\n- [x] [Unit or functional\ntests](https://www.elastic.co/guide/en/kibana/master/development-tests.html)\nwere updated or added to match the most common scenarios\n\n---------\n\nCo-authored-by: Stratoula Kalafateli <[email protected]>\nCo-authored-by: kibanamachine <[email protected]>","sha":"f09bce1108cdd55ba69e11e8b14c947bf052dd91"}},{"branch":"8.19","label":"v8.19.0","branchLabelMappingKey":"^v(\\d+).(\\d+).\\d+$","isSourceBranch":false,"state":"NOT_CREATED"}]}] BACKPORT--> --------- Co-authored-by: Sebastian Delle Donne <[email protected]>
1 parent 15c5b60 commit 2b3122e

File tree

19 files changed

+890
-18
lines changed

19 files changed

+890
-18
lines changed

src/platform/packages/shared/kbn-esql-ast/src/parser/factories/completion.ts

Lines changed: 21 additions & 10 deletions
Original file line numberDiff line numberDiff line change
@@ -6,26 +6,33 @@
66
* your election, the "Elastic License 2.0", the "GNU Affero General Public
77
* License v3.0 only", or the "Server Side Public License, v 1".
88
*/
9-
10-
import { Builder, ESQLCommand } from '../../..';
9+
import { ESQLAstCompletionCommand, ESQLSingleAstItem } from '../../types';
10+
import { Builder } from '../../..';
1111
import { visitPrimaryExpression } from '../walkers';
1212
import { CompletionCommandContext } from '../../antlr/esql_parser';
1313
import { createColumn, createCommand, createIdentifierOrParam } from '../factories';
1414
import { getPosition } from '../helpers';
15+
import { EDITOR_MARKER } from '../constants';
1516

1617
export const createCompletionCommand = (
1718
ctx: CompletionCommandContext
18-
): ESQLCommand<'completion'> => {
19-
const command = createCommand<'completion'>('completion', ctx);
19+
): ESQLAstCompletionCommand => {
20+
const command = createCommand<'completion', ESQLAstCompletionCommand>('completion', ctx);
2021

21-
const prompt = visitPrimaryExpression(ctx._prompt);
22+
const prompt = visitPrimaryExpression(ctx._prompt) as ESQLSingleAstItem;
2223
command.args.push(prompt);
24+
command.prompt = prompt;
25+
26+
const withCtx = ctx.WITH();
2327

2428
const inferenceIdCtx = ctx._inferenceId;
2529
const maybeInferenceId = inferenceIdCtx ? createIdentifierOrParam(inferenceIdCtx) : undefined;
2630
const inferenceId = maybeInferenceId ?? Builder.identifier('', { incomplete: true });
2731

28-
const withCtx = ctx.WITH();
32+
if (inferenceId.text.includes(EDITOR_MARKER)) {
33+
inferenceId.incomplete = true;
34+
}
35+
2936
const optionWith = Builder.option(
3037
{
3138
name: 'with',
@@ -38,25 +45,29 @@ export const createCompletionCommand = (
3845
: undefined
3946
);
4047

41-
if (inferenceId.incomplete || !withCtx) {
42-
optionWith.incomplete = true;
43-
}
48+
optionWith.incomplete = withCtx && inferenceId.incomplete;
4449

4550
command.args.push(optionWith);
51+
command.inferenceId = inferenceId;
4652

47-
if (ctx._targetField && ctx._targetField.getText()) {
53+
if (ctx._targetField) {
4854
const targetField = createColumn(ctx._targetField);
55+
targetField.incomplete =
56+
targetField.text.length === 0 || targetField.text.includes(EDITOR_MARKER);
57+
4958
const option = Builder.option(
5059
{
5160
name: 'as',
5261
args: [targetField],
5362
},
5463
{
64+
incomplete: targetField.incomplete,
5565
location: getPosition(ctx.AS().symbol, ctx._targetField.stop),
5666
}
5767
);
5868

5969
command.args.push(option);
70+
command.targetField = targetField;
6071
}
6172

6273
return command;

src/platform/packages/shared/kbn-esql-ast/src/pretty_print/__tests__/basic_pretty_printer.test.ts

Lines changed: 25 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -287,6 +287,31 @@ describe('single line query', () => {
287287
);
288288
});
289289
});
290+
291+
describe('COMPLETION', () => {
292+
test('from single line', () => {
293+
const { text } =
294+
reprint(`FROM search-movies | COMPLETION "Shakespeare" WITH inferenceId AS result
295+
`);
296+
297+
expect(text).toBe(
298+
'FROM search-movies | COMPLETION "Shakespeare" WITH inferenceId AS result'
299+
);
300+
});
301+
302+
test('from multiline', () => {
303+
const { text } = reprint(
304+
`FROM kibana_sample_data_ecommerce
305+
| COMPLETION "prompt" WITH \`openai-completion\` AS result
306+
| LIMIT 2
307+
`
308+
);
309+
310+
expect(text).toBe(
311+
'FROM kibana_sample_data_ecommerce | COMPLETION "prompt" WITH `openai-completion` AS result | LIMIT 2'
312+
);
313+
});
314+
});
290315
});
291316

292317
describe('expressions', () => {

src/platform/packages/shared/kbn-esql-ast/src/types.ts

Lines changed: 8 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -14,7 +14,8 @@ export type ESQLAstCommand =
1414
| ESQLAstTimeseriesCommand
1515
| ESQLAstJoinCommand
1616
| ESQLAstChangePointCommand
17-
| ESQLAstRerankCommand;
17+
| ESQLAstRerankCommand
18+
| ESQLAstCompletionCommand;
1819

1920
export type ESQLAstNode = ESQLAstCommand | ESQLAstExpression | ESQLAstItem;
2021

@@ -110,6 +111,12 @@ export interface ESQLAstChangePointCommand extends ESQLCommand<'change_point'> {
110111
};
111112
}
112113

114+
export interface ESQLAstCompletionCommand extends ESQLCommand<'completion'> {
115+
prompt: ESQLAstExpression;
116+
inferenceId: ESQLIdentifierOrParam;
117+
targetField?: ESQLColumn;
118+
}
119+
113120
export interface ESQLAstRerankCommand extends ESQLCommand<'rerank'> {
114121
query: ESQLLiteral;
115122
fields: ESQLAstField[];

src/platform/packages/shared/kbn-esql-ast/src/visitor/global_visitor_context.ts

Lines changed: 7 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -11,6 +11,7 @@ import * as contexts from './contexts';
1111
import type {
1212
ESQLAstChangePointCommand,
1313
ESQLAstCommand,
14+
ESQLAstCompletionCommand,
1415
ESQLAstJoinCommand,
1516
ESQLAstQueryExpression,
1617
ESQLAstRenameExpression,
@@ -199,7 +200,11 @@ export class GlobalVisitorContext<
199200
}
200201
case 'completion': {
201202
if (!this.methods.visitCompletionCommand) break;
202-
return this.visitCompletionCommand(parent, commandNode, input as any);
203+
return this.visitCompletionCommand(
204+
parent,
205+
commandNode as ESQLAstCompletionCommand,
206+
input as any
207+
);
203208
}
204209
case 'sample': {
205210
if (!this.methods.visitSampleCommand) break;
@@ -431,7 +436,7 @@ export class GlobalVisitorContext<
431436

432437
public visitCompletionCommand(
433438
parent: contexts.VisitorContext | null,
434-
node: ESQLAstCommand,
439+
node: ESQLAstCompletionCommand,
435440
input: types.VisitorInput<Methods, 'visitCompletionCommand'>
436441
): types.VisitorOutput<Methods, 'visitCompletionCommand'> {
437442
const context = new contexts.CompletionCommandVisitorContext(this, node, parent);

src/platform/packages/shared/kbn-esql-ast/tsconfig.json

Lines changed: 1 addition & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -11,8 +11,7 @@
1111
"src/**/*",
1212
"**/*.ts",
1313
],
14-
"kbn_references": [
15-
],
14+
"kbn_references": [],
1615
"exclude": [
1716
"target/**/*",
1817
]

src/platform/packages/shared/kbn-esql-validation-autocomplete/scripts/generate_function_definitions.ts

Lines changed: 7 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -14,13 +14,14 @@ import _ from 'lodash';
1414
import type { RecursivePartial } from '@kbn/utility-types';
1515
import {
1616
FunctionDefinition,
17+
FunctionDefinitionTypes,
1718
FunctionParameterType,
1819
FunctionReturnType,
19-
Signature,
20-
FunctionDefinitionTypes,
2120
Location,
21+
Signature,
2222
} from '../src/definitions/types';
2323
import { FULL_TEXT_SEARCH_FUNCTIONS } from '../src/shared/constants';
24+
2425
const aliasTable: Record<string, string[]> = {
2526
to_version: ['to_ver'],
2627
to_unsigned_long: ['to_ul', 'to_ulong'],
@@ -93,6 +94,7 @@ const defaultScalarFunctionLocations: Location[] = [
9394
Location.STATS,
9495
Location.STATS_BY,
9596
Location.STATS_WHERE,
97+
Location.COMPLETION,
9698
];
9799

98100
const defaultAggFunctionLocations: Location[] = [Location.STATS];
@@ -743,6 +745,7 @@ const enrichOperators = (
743745
Location.SORT,
744746
Location.STATS_WHERE,
745747
Location.STATS_BY,
748+
Location.COMPLETION,
746749
]);
747750
}
748751
if (isMathOperator) {
@@ -755,6 +758,7 @@ const enrichOperators = (
755758
Location.STATS,
756759
Location.STATS_WHERE,
757760
Location.STATS_BY,
761+
Location.COMPLETION,
758762
]);
759763
}
760764
if (isInOperator || isLikeOperator || isNotOperator || arePredicates) {
@@ -764,6 +768,7 @@ const enrichOperators = (
764768
Location.SORT,
765769
Location.ROW,
766770
Location.STATS_WHERE,
771+
Location.COMPLETION,
767772
];
768773
}
769774
if (isInOperator) {
Lines changed: 69 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,69 @@
1+
/*
2+
* Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one
3+
* or more contributor license agreements. Licensed under the "Elastic License
4+
* 2.0", the "GNU Affero General Public License v3.0 only", and the "Server Side
5+
* Public License v 1"; you may not use this file except in compliance with, at
6+
* your election, the "Elastic License 2.0", the "GNU Affero General Public
7+
* License v3.0 only", or the "Server Side Public License, v 1".
8+
*/
9+
10+
import { getFieldNamesByType, setup } from './helpers';
11+
import { ESQL_STRING_TYPES } from '../../shared/esql_types';
12+
import { getFunctionSuggestions } from '../factories';
13+
import { Location } from '../../definitions/types';
14+
15+
describe('autocomplete.suggest', () => {
16+
describe('COMPLETION', () => {
17+
let assertSuggestions: Awaited<ReturnType<typeof setup>>['assertSuggestions'];
18+
19+
beforeEach(async () => {
20+
const setupResult = await setup();
21+
assertSuggestions = setupResult.assertSuggestions;
22+
});
23+
24+
it('suggests columns of STRING types and functions with STRING or UNKNOWN types for the prompt', async () => {
25+
const expectedSuggestions = [
26+
...getFieldNamesByType(ESQL_STRING_TYPES).map((v) => `${v} `),
27+
...getFunctionSuggestions({
28+
location: Location.COMPLETION,
29+
returnTypes: ['text', 'keyword', 'unknown'],
30+
}).map((fn) => `${fn.text} `),
31+
];
32+
33+
await assertSuggestions(`FROM a | COMPLETION /`, [
34+
'"${0:Your prompt to the LLM.}"',
35+
...expectedSuggestions,
36+
]);
37+
38+
await assertSuggestions(`FROM a | COMPLETION kubernetes.some/`, expectedSuggestions);
39+
});
40+
41+
it('suggests WITH after the prompt', async () => {
42+
await assertSuggestions(`FROM a | COMPLETION "prompt" /`, ['WITH ']);
43+
await assertSuggestions(`FROM a | COMPLETION "prompt" WIT/`, ['WITH ']);
44+
});
45+
46+
it('suggests nothing after WITH', async () => {
47+
await assertSuggestions(`FROM a | COMPLETION "prompt" WITH /`, []);
48+
});
49+
50+
describe('optional AS', () => {
51+
it('suggests AS after WITH <inferenceId>', async () => {
52+
await assertSuggestions(`FROM a | COMPLETION "prompt" WITH inferenceId /`, ['AS ', '| ']);
53+
await assertSuggestions(`FROM a | COMPLETION "prompt" WITH inferenceId A/`, ['AS ', '| ']);
54+
});
55+
56+
it('suggests default target field name for AS clauses', async () => {
57+
await assertSuggestions(`FROM a | COMPLETION "prompt" WITH inferenceId AS / `, [
58+
'completion ',
59+
]);
60+
});
61+
62+
it('suggests pipe after complete command', async () => {
63+
await assertSuggestions(`FROM a | COMPLETION "prompt" WITH inferenceId AS completion /`, [
64+
'| ',
65+
]);
66+
});
67+
});
68+
});
69+
});

src/platform/packages/shared/kbn-esql-validation-autocomplete/src/autocomplete/autocomplete.ts

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -397,6 +397,7 @@ async function getSuggestionsWithinCommandExpression(
397397
supportsControls,
398398
getPolicies,
399399
getPolicyMetadata,
400+
references,
400401
});
401402
}
402403

@@ -571,6 +572,7 @@ async function getFunctionArgsSuggestions(
571572
};
572573

573574
// Fields
575+
574576
suggestions.push(
575577
...pushItUpInTheList(
576578
await getFieldsByType(
Lines changed: 55 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,55 @@
1+
/*
2+
* Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one
3+
* or more contributor license agreements. Licensed under the "Elastic License
4+
* 2.0", the "GNU Affero General Public License v3.0 only", and the "Server Side
5+
* Public License v 1"; you may not use this file except in compliance with, at
6+
* your election, the "Elastic License 2.0", the "GNU Affero General Public
7+
* License v3.0 only", or the "Server Side Public License, v 1".
8+
*/
9+
import { synth } from '@kbn/esql-ast';
10+
import type { ESQLFieldWithMetadata } from '../../../validation/types';
11+
import { fieldsSuggestionsAfter } from './fields_suggestions_after';
12+
13+
describe('COMPLETION', () => {
14+
it('adds "completion" field, when AS option not specified', () => {
15+
const previousCommandFields = [
16+
{ name: 'field1', type: 'keyword' },
17+
{ name: 'count', type: 'double' },
18+
] as ESQLFieldWithMetadata[];
19+
20+
const userDefinedColumns = [] as ESQLFieldWithMetadata[];
21+
22+
const result = fieldsSuggestionsAfter(
23+
synth.cmd`COMPLETION "prompt" WITH inferenceId`,
24+
previousCommandFields,
25+
userDefinedColumns
26+
);
27+
28+
expect(result).toEqual([
29+
{ name: 'field1', type: 'keyword' },
30+
{ name: 'count', type: 'double' },
31+
{ name: 'completion', type: 'keyword' },
32+
]);
33+
});
34+
35+
it('adds the given targetField as field, when AS option is specified', () => {
36+
const previousCommandFields = [
37+
{ name: 'field1', type: 'keyword' },
38+
{ name: 'count', type: 'double' },
39+
] as ESQLFieldWithMetadata[];
40+
41+
const userDefinedColumns = [] as ESQLFieldWithMetadata[];
42+
43+
const result = fieldsSuggestionsAfter(
44+
synth.cmd`COMPLETION "prompt" WITH inferenceId AS customField`,
45+
previousCommandFields,
46+
userDefinedColumns
47+
);
48+
49+
expect(result).toEqual([
50+
{ name: 'field1', type: 'keyword' },
51+
{ name: 'count', type: 'double' },
52+
{ name: 'customField', type: 'keyword' },
53+
]);
54+
});
55+
});
Lines changed: 31 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,31 @@
1+
/*
2+
* Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one
3+
* or more contributor license agreements. Licensed under the "Elastic License
4+
* 2.0", the "GNU Affero General Public License v3.0 only", and the "Server Side
5+
* Public License v 1"; you may not use this file except in compliance with, at
6+
* your election, the "Elastic License 2.0", the "GNU Affero General Public
7+
* License v3.0 only", or the "Server Side Public License, v 1".
8+
*/
9+
import { type ESQLAstCommand, LeafPrinter } from '@kbn/esql-ast';
10+
import uniqBy from 'lodash/uniqBy';
11+
import { ESQLAstCompletionCommand } from '@kbn/esql-ast/src/types';
12+
import type { ESQLFieldWithMetadata } from '../../../validation/types';
13+
14+
export const fieldsSuggestionsAfter = (
15+
command: ESQLAstCommand,
16+
previousCommandFields: ESQLFieldWithMetadata[],
17+
userDefinedColumns: ESQLFieldWithMetadata[]
18+
) => {
19+
const { targetField } = command as ESQLAstCompletionCommand;
20+
21+
return uniqBy(
22+
[
23+
...previousCommandFields,
24+
{
25+
name: targetField ? LeafPrinter.column(targetField) : 'completion',
26+
type: 'keyword' as const,
27+
},
28+
],
29+
'name'
30+
);
31+
};

0 commit comments

Comments
 (0)