flowquery 1.0.7 → 1.0.9
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/dist/flowquery.min.js +1 -1
- package/dist/parsing/functions/function_factory.d.ts +1 -0
- package/dist/parsing/functions/function_factory.d.ts.map +1 -1
- package/dist/parsing/functions/function_factory.js +1 -0
- package/dist/parsing/functions/function_factory.js.map +1 -1
- package/dist/parsing/functions/function_metadata.d.ts +0 -17
- package/dist/parsing/functions/function_metadata.d.ts.map +1 -1
- package/dist/parsing/functions/function_metadata.js +61 -86
- package/dist/parsing/functions/function_metadata.js.map +1 -1
- package/dist/parsing/functions/keys.d.ts +7 -0
- package/dist/parsing/functions/keys.d.ts.map +1 -0
- package/dist/parsing/functions/keys.js +42 -0
- package/dist/parsing/functions/keys.js.map +1 -0
- package/docs/flowquery.min.js +1 -1
- package/flowquery-vscode/flowQueryEngine/flowquery.min.js +1 -1
- package/misc/apps/RAG/package.json +3 -1
- package/misc/apps/RAG/src/components/AdaptiveCardRenderer.css +172 -0
- package/misc/apps/RAG/src/components/AdaptiveCardRenderer.tsx +312 -0
- package/misc/apps/RAG/src/components/ChatContainer.tsx +159 -112
- package/misc/apps/RAG/src/components/ChatInput.tsx +58 -44
- package/misc/apps/RAG/src/components/ChatMessage.tsx +186 -101
- package/misc/apps/RAG/src/components/FlowQueryAgent.ts +50 -6
- package/misc/apps/RAG/src/components/FlowQueryRunner.css +9 -0
- package/misc/apps/RAG/src/components/FlowQueryRunner.tsx +44 -5
- package/misc/apps/RAG/src/components/index.ts +4 -0
- package/misc/apps/RAG/src/plugins/index.ts +6 -4
- package/misc/apps/RAG/src/plugins/loaders/CatFacts.ts +1 -2
- package/misc/apps/RAG/src/plugins/loaders/FetchJson.ts +1 -2
- package/misc/apps/RAG/src/plugins/loaders/Form.ts +578 -0
- package/misc/apps/RAG/src/plugins/loaders/Llm.ts +1 -2
- package/misc/apps/RAG/src/plugins/loaders/MockData.ts +2 -4
- package/misc/apps/RAG/src/plugins/loaders/Table.ts +271 -0
- package/misc/apps/RAG/src/prompts/FlowQuerySystemPrompt.ts +12 -0
- package/package.json +1 -1
- package/src/parsing/functions/function_factory.ts +1 -0
- package/src/parsing/functions/function_metadata.ts +67 -108
- package/src/parsing/functions/keys.ts +31 -0
- package/tests/compute/runner.test.ts +8 -0
- package/tests/extensibility.test.ts +38 -0
|
@@ -0,0 +1,271 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Table loader plugin - transforms tabular data into Adaptive Card format.
|
|
3
|
+
*
|
|
4
|
+
* Adaptive Cards are platform-agnostic UI snippets that can be rendered in
|
|
5
|
+
* Microsoft Teams, Outlook, Windows, and other applications.
|
|
6
|
+
*
|
|
7
|
+
* Usage in FlowQuery:
|
|
8
|
+
* // First collect data from an async provider, then pass to table:
|
|
9
|
+
* LOAD JSON FROM mockUsers(5) AS u
|
|
10
|
+
* WITH collect(u) AS users
|
|
11
|
+
* LOAD JSON FROM table(users, 'Users') AS card
|
|
12
|
+
* RETURN card
|
|
13
|
+
*
|
|
14
|
+
* Note: Async providers cannot be nested as function arguments.
|
|
15
|
+
*/
|
|
16
|
+
|
|
17
|
+
import { FunctionDef } from 'flowquery/extensibility';
|
|
18
|
+
|
|
19
|
+
/**
|
|
20
|
+
* Interface for Adaptive Card structure
|
|
21
|
+
*/
|
|
22
|
+
interface AdaptiveCard {
|
|
23
|
+
type: 'AdaptiveCard';
|
|
24
|
+
$schema: string;
|
|
25
|
+
version: string;
|
|
26
|
+
body: AdaptiveCardElement[];
|
|
27
|
+
}
|
|
28
|
+
|
|
29
|
+
interface AdaptiveCardElement {
|
|
30
|
+
type: string;
|
|
31
|
+
[key: string]: any;
|
|
32
|
+
}
|
|
33
|
+
|
|
34
|
+
interface TableCell {
|
|
35
|
+
type: 'TableCell';
|
|
36
|
+
items: AdaptiveCardElement[];
|
|
37
|
+
}
|
|
38
|
+
|
|
39
|
+
interface TableRow {
|
|
40
|
+
type: 'TableRow';
|
|
41
|
+
cells: TableCell[];
|
|
42
|
+
}
|
|
43
|
+
|
|
44
|
+
/**
|
|
45
|
+
* Table loader - transforms tabular data into an Adaptive Card table format.
|
|
46
|
+
*/
|
|
47
|
+
@FunctionDef({
|
|
48
|
+
description: 'Transforms tabular data into an Adaptive Card JSON format with a table layout',
|
|
49
|
+
category: 'async',
|
|
50
|
+
parameters: [
|
|
51
|
+
{
|
|
52
|
+
name: 'data',
|
|
53
|
+
description: 'Array of objects or async generator to display as a table',
|
|
54
|
+
type: 'array',
|
|
55
|
+
required: true
|
|
56
|
+
},
|
|
57
|
+
{
|
|
58
|
+
name: 'title',
|
|
59
|
+
description: 'Optional title for the card',
|
|
60
|
+
type: 'string',
|
|
61
|
+
required: false,
|
|
62
|
+
default: 'Data Table'
|
|
63
|
+
},
|
|
64
|
+
{
|
|
65
|
+
name: 'columns',
|
|
66
|
+
description: 'Optional array of column names to include (defaults to all columns from first row)',
|
|
67
|
+
type: 'array',
|
|
68
|
+
required: false
|
|
69
|
+
},
|
|
70
|
+
{
|
|
71
|
+
name: 'maxRows',
|
|
72
|
+
description: 'Maximum number of rows to display',
|
|
73
|
+
type: 'number',
|
|
74
|
+
required: false,
|
|
75
|
+
default: 100
|
|
76
|
+
}
|
|
77
|
+
],
|
|
78
|
+
output: {
|
|
79
|
+
description: 'Adaptive Card JSON object',
|
|
80
|
+
type: 'object',
|
|
81
|
+
properties: {
|
|
82
|
+
type: { description: 'Always "AdaptiveCard"', type: 'string' },
|
|
83
|
+
$schema: { description: 'Adaptive Card schema URL', type: 'string' },
|
|
84
|
+
version: { description: 'Adaptive Card version', type: 'string' },
|
|
85
|
+
body: { description: 'Card body elements including table', type: 'array' }
|
|
86
|
+
}
|
|
87
|
+
},
|
|
88
|
+
examples: [
|
|
89
|
+
"LOAD JSON FROM mockUsers(5) AS u WITH collect(u) AS users LOAD JSON FROM table(users, 'User List') AS card RETURN card",
|
|
90
|
+
"LOAD JSON FROM mockProducts(10) AS p WITH collect(p) AS products LOAD JSON FROM table(products, 'Products', ['name', 'price', 'category']) AS card RETURN card"
|
|
91
|
+
]
|
|
92
|
+
})
|
|
93
|
+
export class TableLoader {
|
|
94
|
+
/**
|
|
95
|
+
* Transforms data into an Adaptive Card with table layout.
|
|
96
|
+
*
|
|
97
|
+
* @param data - Array or async iterable of objects
|
|
98
|
+
* @param title - Card title
|
|
99
|
+
* @param columns - Optional column names to include
|
|
100
|
+
* @param maxRows - Maximum rows to include
|
|
101
|
+
*/
|
|
102
|
+
async *fetch(
|
|
103
|
+
data: any[] | AsyncIterable<any>,
|
|
104
|
+
title: string = 'Data Table',
|
|
105
|
+
columns?: string[],
|
|
106
|
+
maxRows: number = 100
|
|
107
|
+
): AsyncGenerator<AdaptiveCard, void, unknown> {
|
|
108
|
+
// Collect data from array or async iterable
|
|
109
|
+
const rows: any[] = [];
|
|
110
|
+
|
|
111
|
+
if (Symbol.asyncIterator in Object(data)) {
|
|
112
|
+
for await (const item of data as AsyncIterable<any>) {
|
|
113
|
+
rows.push(item);
|
|
114
|
+
if (rows.length >= maxRows) break;
|
|
115
|
+
}
|
|
116
|
+
} else if (Array.isArray(data)) {
|
|
117
|
+
rows.push(...data.slice(0, maxRows));
|
|
118
|
+
} else {
|
|
119
|
+
// Single object
|
|
120
|
+
rows.push(data);
|
|
121
|
+
}
|
|
122
|
+
|
|
123
|
+
if (rows.length === 0) {
|
|
124
|
+
yield this.createEmptyCard(title);
|
|
125
|
+
return;
|
|
126
|
+
}
|
|
127
|
+
|
|
128
|
+
// Determine columns from first row if not specified
|
|
129
|
+
const columnNames = columns || Object.keys(rows[0]);
|
|
130
|
+
|
|
131
|
+
yield this.createTableCard(title, columnNames, rows);
|
|
132
|
+
}
|
|
133
|
+
|
|
134
|
+
/**
|
|
135
|
+
* Creates an Adaptive Card with a table displaying the data.
|
|
136
|
+
* Uses ColumnSet/Column for better compatibility across renderers.
|
|
137
|
+
*/
|
|
138
|
+
private createTableCard(title: string, columnNames: string[], rows: any[]): AdaptiveCard {
|
|
139
|
+
const card: AdaptiveCard = {
|
|
140
|
+
type: 'AdaptiveCard',
|
|
141
|
+
$schema: 'http://adaptivecards.io/schemas/adaptive-card.json',
|
|
142
|
+
version: '1.3',
|
|
143
|
+
body: []
|
|
144
|
+
};
|
|
145
|
+
|
|
146
|
+
// Add title
|
|
147
|
+
card.body.push({
|
|
148
|
+
type: 'TextBlock',
|
|
149
|
+
text: title,
|
|
150
|
+
weight: 'Bolder',
|
|
151
|
+
size: 'Large',
|
|
152
|
+
wrap: true
|
|
153
|
+
});
|
|
154
|
+
|
|
155
|
+
// Add separator
|
|
156
|
+
card.body.push({
|
|
157
|
+
type: 'TextBlock',
|
|
158
|
+
text: ' ',
|
|
159
|
+
separator: true
|
|
160
|
+
});
|
|
161
|
+
|
|
162
|
+
// Create header row using ColumnSet
|
|
163
|
+
const headerColumnSet: AdaptiveCardElement = {
|
|
164
|
+
type: 'ColumnSet',
|
|
165
|
+
columns: columnNames.map(col => ({
|
|
166
|
+
type: 'Column',
|
|
167
|
+
width: 'stretch',
|
|
168
|
+
items: [{
|
|
169
|
+
type: 'TextBlock',
|
|
170
|
+
text: this.formatColumnName(col),
|
|
171
|
+
weight: 'Bolder',
|
|
172
|
+
wrap: true
|
|
173
|
+
}]
|
|
174
|
+
})),
|
|
175
|
+
style: 'accent'
|
|
176
|
+
};
|
|
177
|
+
card.body.push(headerColumnSet);
|
|
178
|
+
|
|
179
|
+
// Add data rows using ColumnSets
|
|
180
|
+
for (const row of rows) {
|
|
181
|
+
const dataColumnSet: AdaptiveCardElement = {
|
|
182
|
+
type: 'ColumnSet',
|
|
183
|
+
columns: columnNames.map(col => ({
|
|
184
|
+
type: 'Column',
|
|
185
|
+
width: 'stretch',
|
|
186
|
+
items: [{
|
|
187
|
+
type: 'TextBlock',
|
|
188
|
+
text: this.formatCellValue(row[col]),
|
|
189
|
+
wrap: true
|
|
190
|
+
}]
|
|
191
|
+
})),
|
|
192
|
+
separator: true
|
|
193
|
+
};
|
|
194
|
+
card.body.push(dataColumnSet);
|
|
195
|
+
}
|
|
196
|
+
|
|
197
|
+
// Add row count footer
|
|
198
|
+
card.body.push({
|
|
199
|
+
type: 'TextBlock',
|
|
200
|
+
text: `Showing ${rows.length} row${rows.length !== 1 ? 's' : ''}`,
|
|
201
|
+
size: 'Small',
|
|
202
|
+
isSubtle: true,
|
|
203
|
+
horizontalAlignment: 'Right',
|
|
204
|
+
separator: true
|
|
205
|
+
});
|
|
206
|
+
|
|
207
|
+
return card;
|
|
208
|
+
}
|
|
209
|
+
|
|
210
|
+
/**
|
|
211
|
+
* Creates an empty card when no data is available.
|
|
212
|
+
*/
|
|
213
|
+
private createEmptyCard(title: string): AdaptiveCard {
|
|
214
|
+
return {
|
|
215
|
+
type: 'AdaptiveCard',
|
|
216
|
+
$schema: 'http://adaptivecards.io/schemas/adaptive-card.json',
|
|
217
|
+
version: '1.3',
|
|
218
|
+
body: [
|
|
219
|
+
{
|
|
220
|
+
type: 'TextBlock',
|
|
221
|
+
text: title,
|
|
222
|
+
weight: 'Bolder',
|
|
223
|
+
size: 'Large',
|
|
224
|
+
wrap: true
|
|
225
|
+
},
|
|
226
|
+
{
|
|
227
|
+
type: 'TextBlock',
|
|
228
|
+
text: 'No data available',
|
|
229
|
+
isSubtle: true,
|
|
230
|
+
wrap: true
|
|
231
|
+
}
|
|
232
|
+
]
|
|
233
|
+
};
|
|
234
|
+
}
|
|
235
|
+
|
|
236
|
+
/**
|
|
237
|
+
* Formats a column name for display (converts camelCase/snake_case to Title Case).
|
|
238
|
+
*/
|
|
239
|
+
private formatColumnName(name: string): string {
|
|
240
|
+
return name
|
|
241
|
+
.replace(/([A-Z])/g, ' $1') // camelCase
|
|
242
|
+
.replace(/_/g, ' ') // snake_case
|
|
243
|
+
.replace(/^\w/, c => c.toUpperCase())
|
|
244
|
+
.trim();
|
|
245
|
+
}
|
|
246
|
+
|
|
247
|
+
/**
|
|
248
|
+
* Formats a cell value for display in the table.
|
|
249
|
+
*/
|
|
250
|
+
private formatCellValue(value: any): string {
|
|
251
|
+
if (value === null || value === undefined) {
|
|
252
|
+
return '-';
|
|
253
|
+
}
|
|
254
|
+
if (typeof value === 'boolean') {
|
|
255
|
+
return value ? '✓' : '✗';
|
|
256
|
+
}
|
|
257
|
+
if (typeof value === 'number') {
|
|
258
|
+
// Format numbers nicely
|
|
259
|
+
if (Number.isInteger(value)) {
|
|
260
|
+
return value.toString();
|
|
261
|
+
}
|
|
262
|
+
return value.toFixed(2);
|
|
263
|
+
}
|
|
264
|
+
if (typeof value === 'object') {
|
|
265
|
+
return JSON.stringify(value);
|
|
266
|
+
}
|
|
267
|
+
return String(value);
|
|
268
|
+
}
|
|
269
|
+
}
|
|
270
|
+
|
|
271
|
+
export { TableLoader as default };
|
|
@@ -33,6 +33,18 @@ FlowQuery is a declarative query language for data processing pipelines. It uses
|
|
|
33
33
|
LOAD JSON FROM 'https://api.example.com/data' AS item
|
|
34
34
|
LOAD JSON FROM myFunction(arg1, arg2) AS item
|
|
35
35
|
\`\`\`
|
|
36
|
+
|
|
37
|
+
**IMPORTANT**: Async data providers (functions used after LOAD JSON FROM) cannot be nested inside other function calls. If you need to pass data from one async provider to another, first load the data into a variable using collect(), then pass that variable:
|
|
38
|
+
\`\`\`
|
|
39
|
+
// WRONG - async providers cannot be nested:
|
|
40
|
+
// LOAD JSON FROM table(mockProducts(5), 'Products') AS card
|
|
41
|
+
|
|
42
|
+
// CORRECT - collect data first, then pass to next provider:
|
|
43
|
+
LOAD JSON FROM mockProducts(5) AS p
|
|
44
|
+
WITH collect(p) AS products
|
|
45
|
+
LOAD JSON FROM table(products, 'Products') AS card
|
|
46
|
+
RETURN card
|
|
47
|
+
\`\`\`
|
|
36
48
|
|
|
37
49
|
3. **LOAD JSON FROM ... HEADERS ... POST** - Make HTTP requests with headers and body
|
|
38
50
|
\`\`\`
|
package/package.json
CHANGED
|
@@ -66,26 +66,73 @@ export interface FunctionMetadata {
|
|
|
66
66
|
notes?: string;
|
|
67
67
|
}
|
|
68
68
|
|
|
69
|
-
/**
|
|
70
|
-
* Registry for function metadata collected via decorators.
|
|
71
|
-
*/
|
|
72
|
-
const functionMetadataRegistry: Map<string, FunctionMetadata> = new Map();
|
|
73
|
-
|
|
74
|
-
/**
|
|
75
|
-
* Registry for function factories collected via decorators.
|
|
76
|
-
* Allows @FunctionDef to automatically register functions for instantiation.
|
|
77
|
-
*/
|
|
78
|
-
const functionFactoryRegistry: Map<string, () => any> = new Map();
|
|
79
|
-
|
|
80
69
|
/**
|
|
81
70
|
* Type for async data provider functions used in LOAD operations.
|
|
82
71
|
*/
|
|
83
72
|
export type AsyncDataProvider = (...args: any[]) => AsyncGenerator<any, void, unknown> | Promise<any>;
|
|
84
73
|
|
|
85
74
|
/**
|
|
86
|
-
*
|
|
75
|
+
* Centralized registry for function metadata, factories, and async providers.
|
|
76
|
+
* Encapsulates all registration logic for the @FunctionDef decorator.
|
|
87
77
|
*/
|
|
88
|
-
|
|
78
|
+
class FunctionRegistry {
|
|
79
|
+
private static metadata: Map<string, FunctionMetadata> = new Map<string, FunctionMetadata>();
|
|
80
|
+
private static factories: Map<string, () => any> = new Map<string, () => any>();
|
|
81
|
+
private static asyncProviders: Map<string, AsyncDataProvider> = new Map<string, AsyncDataProvider>();
|
|
82
|
+
|
|
83
|
+
/** Derives a camelCase display name from a class name, removing 'Loader' suffix. */
|
|
84
|
+
private static deriveDisplayName(className: string): string {
|
|
85
|
+
const baseName: string = className.endsWith('Loader') ? className.slice(0, -6) : className;
|
|
86
|
+
return baseName.charAt(0).toLowerCase() + baseName.slice(1);
|
|
87
|
+
}
|
|
88
|
+
|
|
89
|
+
/** Registers an async data provider class. */
|
|
90
|
+
static registerAsync<T extends new (...args: any[]) => any>(constructor: T, options: FunctionDefOptions): void {
|
|
91
|
+
const displayName: string = this.deriveDisplayName(constructor.name);
|
|
92
|
+
const registryKey: string = displayName.toLowerCase();
|
|
93
|
+
|
|
94
|
+
this.metadata.set(registryKey, { name: displayName, ...options });
|
|
95
|
+
this.asyncProviders.set(registryKey, (...args: any[]) => new constructor().fetch(...args));
|
|
96
|
+
}
|
|
97
|
+
|
|
98
|
+
/** Registers a regular function class. */
|
|
99
|
+
static registerFunction<T extends new (...args: any[]) => any>(constructor: T, options: FunctionDefOptions): void {
|
|
100
|
+
const instance: any = new constructor();
|
|
101
|
+
const baseName: string = (instance.name?.toLowerCase() || constructor.name.toLowerCase());
|
|
102
|
+
const displayName: string = baseName.includes(':') ? baseName.split(':')[0] : baseName;
|
|
103
|
+
const registryKey: string = options.category ? `${displayName}:${options.category}` : displayName;
|
|
104
|
+
|
|
105
|
+
this.metadata.set(registryKey, { name: displayName, ...options });
|
|
106
|
+
|
|
107
|
+
if (options.category !== 'predicate') {
|
|
108
|
+
this.factories.set(displayName, () => new constructor());
|
|
109
|
+
}
|
|
110
|
+
this.factories.set(registryKey, () => new constructor());
|
|
111
|
+
}
|
|
112
|
+
|
|
113
|
+
static getAllMetadata(): FunctionMetadata[] {
|
|
114
|
+
return Array.from(this.metadata.values());
|
|
115
|
+
}
|
|
116
|
+
|
|
117
|
+
static getMetadata(name: string, category?: string): FunctionMetadata | undefined {
|
|
118
|
+
const lowerName: string = name.toLowerCase();
|
|
119
|
+
if (category) return this.metadata.get(`${lowerName}:${category}`);
|
|
120
|
+
for (const meta of this.metadata.values()) {
|
|
121
|
+
if (meta.name === lowerName) return meta;
|
|
122
|
+
}
|
|
123
|
+
return undefined;
|
|
124
|
+
}
|
|
125
|
+
|
|
126
|
+
static getFactory(name: string, category?: string): (() => any) | undefined {
|
|
127
|
+
const lowerName: string = name.toLowerCase();
|
|
128
|
+
if (category) return this.factories.get(`${lowerName}:${category}`);
|
|
129
|
+
return this.factories.get(lowerName);
|
|
130
|
+
}
|
|
131
|
+
|
|
132
|
+
static getAsyncProvider(name: string): AsyncDataProvider | undefined {
|
|
133
|
+
return this.asyncProviders.get(name.toLowerCase());
|
|
134
|
+
}
|
|
135
|
+
}
|
|
89
136
|
|
|
90
137
|
/**
|
|
91
138
|
* Decorator options - metadata without the name (derived from class).
|
|
@@ -131,127 +178,39 @@ export type FunctionDefOptions = Omit<FunctionMetadata, 'name'>;
|
|
|
131
178
|
*/
|
|
132
179
|
export function FunctionDef(options: FunctionDefOptions) {
|
|
133
180
|
return function <T extends new (...args: any[]) => any>(constructor: T): T {
|
|
134
|
-
// Handle async providers differently
|
|
135
181
|
if (options.category === 'async') {
|
|
136
|
-
|
|
137
|
-
|
|
138
|
-
|
|
139
|
-
if (baseName.endsWith('Loader')) {
|
|
140
|
-
baseName = baseName.slice(0, -6);
|
|
141
|
-
}
|
|
142
|
-
// Keep display name in camelCase, but use lowercase for registry keys
|
|
143
|
-
const displayName = baseName.charAt(0).toLowerCase() + baseName.slice(1);
|
|
144
|
-
const registryKey = displayName.toLowerCase();
|
|
145
|
-
|
|
146
|
-
// Register metadata with display name
|
|
147
|
-
const metadata: FunctionMetadata = {
|
|
148
|
-
name: displayName,
|
|
149
|
-
...options
|
|
150
|
-
};
|
|
151
|
-
functionMetadataRegistry.set(registryKey, metadata);
|
|
152
|
-
|
|
153
|
-
// Register the async provider (wraps the class's fetch method)
|
|
154
|
-
asyncProviderRegistry.set(registryKey, (...args: any[]) => new constructor().fetch(...args));
|
|
155
|
-
|
|
156
|
-
return constructor;
|
|
157
|
-
}
|
|
158
|
-
|
|
159
|
-
// Regular function handling
|
|
160
|
-
// Create an instance to get the function name from super() call
|
|
161
|
-
const instance = new constructor();
|
|
162
|
-
const baseName = instance.name?.toLowerCase() || constructor.name.toLowerCase();
|
|
163
|
-
|
|
164
|
-
// Use category-qualified key to avoid collisions (e.g., sum vs sum:predicate)
|
|
165
|
-
// but store the display name without the qualifier
|
|
166
|
-
const displayName = baseName.includes(':') ? baseName.split(':')[0] : baseName;
|
|
167
|
-
const registryKey = options.category ? `${displayName}:${options.category}` : displayName;
|
|
168
|
-
|
|
169
|
-
// Register metadata with display name but category-qualified key
|
|
170
|
-
const metadata: FunctionMetadata = {
|
|
171
|
-
name: displayName,
|
|
172
|
-
...options
|
|
173
|
-
};
|
|
174
|
-
functionMetadataRegistry.set(registryKey, metadata);
|
|
175
|
-
|
|
176
|
-
// Register factory function for automatic instantiation
|
|
177
|
-
// Only register to the simple name if no collision exists (predicate functions use qualified keys)
|
|
178
|
-
if (options.category !== 'predicate') {
|
|
179
|
-
functionFactoryRegistry.set(displayName, () => new constructor());
|
|
182
|
+
FunctionRegistry.registerAsync(constructor, options);
|
|
183
|
+
} else {
|
|
184
|
+
FunctionRegistry.registerFunction(constructor, options);
|
|
180
185
|
}
|
|
181
|
-
functionFactoryRegistry.set(registryKey, () => new constructor());
|
|
182
|
-
|
|
183
186
|
return constructor;
|
|
184
187
|
};
|
|
185
188
|
}
|
|
186
189
|
|
|
187
190
|
/**
|
|
188
191
|
* Gets all registered function metadata from decorators.
|
|
189
|
-
*
|
|
190
|
-
* @returns Array of function metadata
|
|
191
192
|
*/
|
|
192
193
|
export function getRegisteredFunctionMetadata(): FunctionMetadata[] {
|
|
193
|
-
return
|
|
194
|
+
return FunctionRegistry.getAllMetadata();
|
|
194
195
|
}
|
|
195
196
|
|
|
196
197
|
/**
|
|
197
198
|
* Gets a registered function factory by name.
|
|
198
|
-
* Used by FunctionFactory to instantiate decorator-registered functions.
|
|
199
|
-
*
|
|
200
|
-
* @param name - Function name (case-insensitive)
|
|
201
|
-
* @param category - Optional category to disambiguate (e.g., 'predicate')
|
|
202
|
-
* @returns Factory function or undefined
|
|
203
199
|
*/
|
|
204
200
|
export function getRegisteredFunctionFactory(name: string, category?: string): (() => any) | undefined {
|
|
205
|
-
|
|
206
|
-
|
|
207
|
-
// If category specified, look for exact match
|
|
208
|
-
if (category) {
|
|
209
|
-
return functionFactoryRegistry.get(`${lowerName}:${category}`);
|
|
210
|
-
}
|
|
211
|
-
|
|
212
|
-
// Try direct match first
|
|
213
|
-
if (functionFactoryRegistry.has(lowerName)) {
|
|
214
|
-
return functionFactoryRegistry.get(lowerName);
|
|
215
|
-
}
|
|
216
|
-
|
|
217
|
-
return undefined;
|
|
201
|
+
return FunctionRegistry.getFactory(name, category);
|
|
218
202
|
}
|
|
219
203
|
|
|
220
204
|
/**
|
|
221
205
|
* Gets metadata for a specific function by name.
|
|
222
|
-
* If multiple functions share the same name (e.g., aggregate vs predicate),
|
|
223
|
-
* optionally specify the category to get the specific one.
|
|
224
|
-
*
|
|
225
|
-
* @param name - Function name (case-insensitive)
|
|
226
|
-
* @param category - Optional category to disambiguate
|
|
227
|
-
* @returns Function metadata or undefined
|
|
228
206
|
*/
|
|
229
207
|
export function getFunctionMetadata(name: string, category?: string): FunctionMetadata | undefined {
|
|
230
|
-
|
|
231
|
-
|
|
232
|
-
// If category specified, look for exact match
|
|
233
|
-
if (category) {
|
|
234
|
-
return functionMetadataRegistry.get(`${lowerName}:${category}`);
|
|
235
|
-
}
|
|
236
|
-
|
|
237
|
-
// Otherwise, first try direct match (for functions without category conflicts)
|
|
238
|
-
// Then search for any function with matching name
|
|
239
|
-
for (const [key, meta] of functionMetadataRegistry) {
|
|
240
|
-
if (meta.name === lowerName) {
|
|
241
|
-
return meta;
|
|
242
|
-
}
|
|
243
|
-
}
|
|
244
|
-
|
|
245
|
-
return undefined;
|
|
208
|
+
return FunctionRegistry.getMetadata(name, category);
|
|
246
209
|
}
|
|
247
210
|
|
|
248
211
|
/**
|
|
249
212
|
* Gets a registered async data provider by name.
|
|
250
|
-
* Used by FunctionFactory to get decorator-registered async providers.
|
|
251
|
-
*
|
|
252
|
-
* @param name - Function name (case-insensitive)
|
|
253
|
-
* @returns Async data provider or undefined
|
|
254
213
|
*/
|
|
255
214
|
export function getRegisteredAsyncProvider(name: string): AsyncDataProvider | undefined {
|
|
256
|
-
return
|
|
215
|
+
return FunctionRegistry.getAsyncProvider(name);
|
|
257
216
|
}
|
|
@@ -0,0 +1,31 @@
|
|
|
1
|
+
import Function from "./function";
|
|
2
|
+
import { FunctionDef } from "./function_metadata";
|
|
3
|
+
|
|
4
|
+
@FunctionDef({
|
|
5
|
+
description: "Returns the keys of an object (associative array) as an array",
|
|
6
|
+
category: "scalar",
|
|
7
|
+
parameters: [
|
|
8
|
+
{ name: "object", description: "Object to extract keys from", type: "object" }
|
|
9
|
+
],
|
|
10
|
+
output: { description: "Array of keys", type: "array", example: "['name', 'age']" },
|
|
11
|
+
examples: ["WITH { name: 'Alice', age: 30 } AS obj RETURN keys(obj)"]
|
|
12
|
+
})
|
|
13
|
+
class Keys extends Function {
|
|
14
|
+
constructor() {
|
|
15
|
+
super("keys");
|
|
16
|
+
this._expectedParameterCount = 1;
|
|
17
|
+
}
|
|
18
|
+
|
|
19
|
+
public value(): any {
|
|
20
|
+
const obj = this.getChildren()[0].value();
|
|
21
|
+
if (obj === null || obj === undefined) {
|
|
22
|
+
return [];
|
|
23
|
+
}
|
|
24
|
+
if (typeof obj !== "object" || Array.isArray(obj)) {
|
|
25
|
+
throw new Error("keys() expects an object, not an array or primitive");
|
|
26
|
+
}
|
|
27
|
+
return Object.keys(obj);
|
|
28
|
+
}
|
|
29
|
+
}
|
|
30
|
+
|
|
31
|
+
export default Keys;
|
|
@@ -495,4 +495,12 @@ test('Test range with size', async () => {
|
|
|
495
495
|
const results = runner.results;
|
|
496
496
|
expect(results.length).toBe(1);
|
|
497
497
|
expect(results[0]).toEqual({'indices': [0, 1, 2, 3, 4, 5, 6, 7, 8, 9]});
|
|
498
|
+
});
|
|
499
|
+
|
|
500
|
+
test('Test keys function', async () => {
|
|
501
|
+
const runner = new Runner('RETURN keys({name: "Alice", age: 30}) as keys');
|
|
502
|
+
await runner.run();
|
|
503
|
+
const results = runner.results;
|
|
504
|
+
expect(results.length).toBe(1);
|
|
505
|
+
expect(results[0]).toEqual({'keys': ['name', 'age']});
|
|
498
506
|
});
|
|
@@ -560,4 +560,42 @@ describe("Plugin Functions Integration with FlowQuery", () => {
|
|
|
560
560
|
expect(runner.results[0]).toEqual({ id: 1, name: "Alice" });
|
|
561
561
|
expect(runner.results[1]).toEqual({ id: 2, name: "Bob" });
|
|
562
562
|
});
|
|
563
|
+
|
|
564
|
+
test("Custom function can be retrieved via functions() in a FlowQuery statement", async () => {
|
|
565
|
+
@FunctionDef({
|
|
566
|
+
description: "A unique test function for introspection",
|
|
567
|
+
category: "scalar",
|
|
568
|
+
parameters: [{ name: "x", description: "Input value", type: "number" }],
|
|
569
|
+
output: { description: "Output value", type: "number" }
|
|
570
|
+
})
|
|
571
|
+
class IntrospectTestFunc extends Function {
|
|
572
|
+
constructor() {
|
|
573
|
+
super("introspectTestFunc");
|
|
574
|
+
this._expectedParameterCount = 1;
|
|
575
|
+
}
|
|
576
|
+
|
|
577
|
+
public value(): number {
|
|
578
|
+
return this.getChildren()[0].value() + 42;
|
|
579
|
+
}
|
|
580
|
+
}
|
|
581
|
+
|
|
582
|
+
// First verify the function is registered
|
|
583
|
+
const metadata = getFunctionMetadata("introspectTestFunc");
|
|
584
|
+
expect(metadata).toBeDefined();
|
|
585
|
+
expect(metadata?.name).toBe("introspecttestfunc");
|
|
586
|
+
|
|
587
|
+
// Use functions() with UNWIND to find the registered function
|
|
588
|
+
const runner = new Runner(`
|
|
589
|
+
WITH functions() AS funcs
|
|
590
|
+
UNWIND funcs AS f
|
|
591
|
+
WITH f WHERE f.name = 'introspecttestfunc'
|
|
592
|
+
RETURN f.name AS name, f.description AS description, f.category AS category
|
|
593
|
+
`);
|
|
594
|
+
await runner.run();
|
|
595
|
+
|
|
596
|
+
expect(runner.results.length).toBe(1);
|
|
597
|
+
expect(runner.results[0].name).toBe("introspecttestfunc");
|
|
598
|
+
expect(runner.results[0].description).toBe("A unique test function for introspection");
|
|
599
|
+
expect(runner.results[0].category).toBe("scalar");
|
|
600
|
+
});
|
|
563
601
|
});
|