flowquery 1.0.7 → 1.0.8

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.
@@ -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
- * Registry for async data providers collected via decorators.
75
+ * Centralized registry for function metadata, factories, and async providers.
76
+ * Encapsulates all registration logic for the @FunctionDef decorator.
87
77
  */
88
- const asyncProviderRegistry: Map<string, AsyncDataProvider> = new Map();
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
- // Derive the function name from the class name
137
- // Remove 'Loader' suffix if present and convert to lowercase for registry
138
- let baseName = constructor.name;
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 Array.from(functionMetadataRegistry.values());
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
- const lowerName = name.toLowerCase();
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
- const lowerName = name.toLowerCase();
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 asyncProviderRegistry.get(name.toLowerCase());
215
+ return FunctionRegistry.getAsyncProvider(name);
257
216
  }
@@ -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
  });