@simtlix/simfinity-js 2.6.4 → 3.0.1

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/README.md CHANGED
@@ -53,6 +53,18 @@ A powerful Node.js framework that automatically generates GraphQL schemas from y
53
53
  - [Custom Mutations](#custom-mutations)
54
54
  - [Working with Existing Mongoose Models](#working-with-existing-mongoose-models)
55
55
  - [Programmatic Data Access](#programmatic-data-access)
56
+ - [MCP Generation](#-mcp-generation)
57
+ - [Generating tool definitions](#generating-tool-definitions)
58
+ - [Standalone MCP server (stdio)](#standalone-mcp-server-stdio)
59
+ - [HTTP MCP endpoint (alongside GraphQL)](#http-mcp-endpoint-alongside-graphql)
60
+ - [Options](#options)
61
+ - [Selecting and naming tools](#selecting-and-naming-tools)
62
+ - [Customizing tools](#customizing-tools)
63
+ - [Limits](#limits)
64
+ - [Tool middleware](#tool-middleware)
65
+ - [Remote execution](#remote-execution)
66
+ - [Tool results and errors](#tool-results-and-errors)
67
+ - [Authentication](#authentication)
56
68
  - [Aggregation Queries](#-aggregation-queries)
57
69
  - [Complete Example](#-complete-example)
58
70
  - [Resources](#-resources)
@@ -2560,6 +2572,289 @@ console.log(UserType.getFields()); // Access GraphQL fields
2560
2572
  const BookInput = simfinity.getInputType(BookType);
2561
2573
  ```
2562
2574
 
2575
+ ## 🤖 MCP Generation
2576
+
2577
+ Simfinity can automatically expose every generated GraphQL operation as a [Model Context Protocol](https://modelcontextprotocol.io) (MCP) tool. For each connected type you get one MCP tool per generated query and mutation — exactly mirroring the GraphQL surface:
2578
+
2579
+ | GraphQL operation | MCP tool name |
2580
+ | --- | --- |
2581
+ | Single-entity query | `book` |
2582
+ | List query | `books` |
2583
+ | Aggregate query | `books_aggregate` |
2584
+ | Add mutation | `addbook` |
2585
+ | Update mutation | `updatebook` |
2586
+ | Delete mutation | `deletebook` |
2587
+ | State-machine action | `publish_book` |
2588
+ | Custom mutation | _the registered name_ |
2589
+
2590
+ Each generated tool follows MCP best practices so an agent can use it without extra context:
2591
+
2592
+ - **`description`**: reuses the GraphQL type/field descriptions and adds an actionable summary. List and aggregate tools spell out the available filter operators (`EQ, NE, LT, LTE, GT, GTE, IN, NIN, BTW, LIKE`), `AND`/`OR` groups, pagination and sorting; aggregate tools additionally document the `SUM/COUNT/AVG/MIN/MAX` operations and the `aggregation` argument.
2593
+ - **`title`**: a human-readable label (e.g. `List Book`, `Create Book`, `Aggregate Book`).
2594
+ - **`inputSchema`**: JSON Schema derived with full fidelity from the GraphQL arguments (scalars, enums, input objects, lists, non-null wrappers, and recursive filter types such as `QLFilterGroup` via `$defs`/`$ref`). GraphQL type and field descriptions are propagated, with curated descriptions for the synthetic filter/pagination/sort/aggregation types. `Date`/`DateTime`/`Time` scalars map to `string` with the matching JSON Schema `format` (`date` / `date-time` / `time`), and arguments with a GraphQL default value carry a JSON Schema `default` and are never listed as `required`.
2595
+ - **`outputSchema`**: JSON Schema describing the returned data (mirroring the auto-generated selection set), with type/field descriptions. Every nullable GraphQL position also accepts `null` (e.g. `type: ['string', 'null']`; enums get `null` appended), so validating MCP clients accept the `null`s GraphQL legitimately returns in `structuredContent` (get-by-id misses, unset optional fields). Non-null positions stay single-typed, and input schemas are never null-widened.
2596
+ - **`annotations`**: behavioral hints — queries are `readOnlyHint: true`, `delete*` is `destructiveHint: true`, `update*` is `idempotentHint: true`. Generated CRUD mutations are recognized by the placeholder descriptions Simfinity stamps on them, so a custom mutation that happens to be named `updateReport` (and carries its own description) keeps that description and gets no inferred hints.
2597
+
2598
+ On execution, each tool returns both a serialized JSON `text` content block and a machine-readable `structuredContent` (the GraphQL `data`) that conforms to the `outputSchema`. When the caller requests `pagination: { count: true }` on a **list** tool, the total record count is delivered in the tool result `_meta.count` (aggregate tools ignore the flag — their resolver never computes a total).
2599
+
2600
+ > The MCP transports require the optional `@modelcontextprotocol/sdk` dependency (`^1.13.0` or newer). Install it with `npm install @modelcontextprotocol/sdk`. `generateMCPTools` works without it. SDK load problems raise distinct error codes: `MCP_SDK_NOT_INSTALLED` (not installed), `MCP_SDK_INCOMPATIBLE` (installed but too old to provide the requested transport — upgrade it), `MCP_SDK_LOAD_FAILED` (any other import failure).
2601
+
2602
+ ### Installation
2603
+
2604
+ ```bash
2605
+ npm install @modelcontextprotocol/sdk
2606
+ ```
2607
+
2608
+ ### Generating tool definitions
2609
+
2610
+ `generateMCPTools(schema, options)` returns the tool list, a `callTool` executor and a `getOperation` inspector. It does not require the SDK and is handy for inspection or custom wiring:
2611
+
2612
+ ```javascript
2613
+ import * as simfinity from '@simtlix/simfinity-js';
2614
+
2615
+ const schema = simfinity.createSchema();
2616
+ const { tools, callTool, getOperation } = simfinity.generateMCPTools(schema);
2617
+
2618
+ // tools: [{ name, title, description, inputSchema, outputSchema, annotations, kind }, ...]
2619
+ const result = await callTool('addbook', { input: { title: 'Dune' } });
2620
+ // result: { content: [{ type: 'text', text: '...JSON...' }],
2621
+ // structuredContent: { addbook: { ... } }, isError: false }
2622
+
2623
+ // getOperation returns the prebuilt GraphQL document a tool executes
2624
+ console.log(getOperation('books'));
2625
+ // query booksOperation($id: QLFilter, $pagination: QLPagination, ...) {
2626
+ // books(id: $id, pagination: $pagination, ...) { id title ... }
2627
+ // }
2628
+ ```
2629
+
2630
+ Both `callTool` and `getOperation` throw a `SimfinityError` with code `MCP_TOOL_NOT_FOUND` for unknown tool names.
2631
+
2632
+ ### Standalone MCP server (stdio)
2633
+
2634
+ Use stdio for a standalone executable consumed by clients like Cursor or Claude Desktop:
2635
+
2636
+ ```javascript
2637
+ import * as simfinity from '@simtlix/simfinity-js';
2638
+
2639
+ const schema = simfinity.createSchema();
2640
+ await simfinity.startStdioMCPServer(schema, {
2641
+ serverName: 'my-api-mcp',
2642
+ context: { user: { id: 'system' } },
2643
+ });
2644
+ ```
2645
+
2646
+ ### HTTP MCP endpoint (alongside GraphQL)
2647
+
2648
+ Mount an MCP endpoint (Streamable HTTP) next to your existing GraphQL server:
2649
+
2650
+ ```javascript
2651
+ import express from 'express';
2652
+ import * as simfinity from '@simtlix/simfinity-js';
2653
+
2654
+ const schema = simfinity.createSchema();
2655
+ const app = express();
2656
+ app.use(express.json());
2657
+
2658
+ const mcpHandler = await simfinity.createHTTPMCPHandler(schema, {
2659
+ // In HTTP mode the context factory receives the Express `req`,
2660
+ // so you can authenticate per request (e.g. from the Authorization header).
2661
+ context: (req) => ({ user: resolveUserFrom(req.headers.authorization) }),
2662
+ });
2663
+ app.post('/mcp', mcpHandler);
2664
+ ```
2665
+
2666
+ For deployments reachable from a browser (especially localhost), enable the SDK's DNS-rebinding protection via `transportOptions`, and observe failures with `onError`:
2667
+
2668
+ ```javascript
2669
+ const mcpHandler = await simfinity.createHTTPMCPHandler(schema, {
2670
+ context: (req) => ({ user: resolveUserFrom(req.headers.authorization) }),
2671
+ // Spread into the SDK's StreamableHTTPServerTransport options
2672
+ transportOptions: {
2673
+ enableDnsRebindingProtection: true,
2674
+ allowedHosts: ['127.0.0.1:3000', 'localhost:3000'],
2675
+ allowedOrigins: ['http://localhost:3000'],
2676
+ },
2677
+ onError: (err, req, res) => console.error('MCP request failed', err),
2678
+ });
2679
+ ```
2680
+
2681
+ The handler catches all errors itself: `onError` is for logging/metrics, and a JSON-RPC internal error (HTTP 500) is sent automatically when response headers were not already written.
2682
+
2683
+ ### Options
2684
+
2685
+ | Option | Default | Description |
2686
+ | --- | --- | --- |
2687
+ | `execution` | `{ mode: 'in-process' }` | `in-process` runs against the in-memory `schema`. `remote` issues HTTP GraphQL requests: `{ mode: 'remote', endpoint, headers, timeoutMs, retry: { attempts, backoffMs } }`. See [Remote execution](#remote-execution). |
2688
+ | `context` | `{}` | GraphQL context value, or a factory. For in-process/stdio it is called as `(extra) => context`; for the HTTP handler it is called as `(req, extra) => context` so you can authenticate per request. Passed through to execution so `scope` functions and auth rules work (they read `context.user`). |
2689
+ | `include` / `exclude` | — | String or array: raw GraphQL field names, prefixed tool names, or the categories `'query'` / `'mutation'`. `exclude` wins over `include`. |
2690
+ | `includeTypes` / `excludeTypes` | — | String or array of entity (return) type names — one entry covers every tool for that entity (`'Book'` matches `book`, `books`, `books_aggregate`, `addbook`, ...). `excludeTypes` wins. |
2691
+ | `selectionDepth` | `1` | Nesting depth for the auto-generated output selection set (and the mirrored `outputSchema`). |
2692
+ | `includeId` | `true` | Always select `id` when nothing else is selectable on an object type. |
2693
+ | `toolNamePrefix` | — | Prefix prepended to every published tool name (e.g. `'catalog_'`). Must match `/^[a-zA-Z0-9_-]+$/`; the resulting names must match `/^[a-zA-Z0-9_-]{1,128}$/` (`MCP_INVALID_TOOL_NAME` otherwise). |
2694
+ | `toolOverrides` | `{}` | Per-tool overrides keyed by tool (or unprefixed field) name: `{ description, title, annotations, selectionDepth, includeId, selection }`. See [Customizing tools](#customizing-tools). |
2695
+ | `toolMiddleware` | — | Array of koa-style `async (call, next)` functions run around every tool call. See [Tool middleware](#tool-middleware). |
2696
+ | `limits` | — | Guardrails: `{ maxPageSize, defaultPagination, maxResultBytes }`. See [Limits](#limits). |
2697
+ | `schemaPlugins` | — | Envelop-style plugins whose `onSchemaChange` hook is applied once before serving (in-process execution only). See [Authentication](#authentication). |
2698
+ | `serverName` / `serverVersion` | `'simfinity-mcp'` / `'1.0.0'` | Identity reported by the MCP server. |
2699
+ | `transportOptions` | — | `createHTTPMCPHandler` only: extra options spread into the SDK's `StreamableHTTPServerTransport` (e.g. `enableDnsRebindingProtection`, `allowedHosts`, `allowedOrigins`). |
2700
+ | `onError` | — | `createHTTPMCPHandler` only: `(err, req, res)` callback invoked when a request fails; the handler still responds with a JSON-RPC internal error (HTTP 500) if headers were not sent. |
2701
+
2702
+ Selection sets always fall back to `id` and then `__typename` when nothing else is selectable (interface/union return types select `__typename`), so the generated documents are always statically valid — and the output schemas mirror the same fallbacks.
2703
+
2704
+ ### Selecting and naming tools
2705
+
2706
+ Trim and namespace the published surface with `include`/`exclude`, `includeTypes`/`excludeTypes` and `toolNamePrefix`:
2707
+
2708
+ ```javascript
2709
+ const { tools, callTool, getOperation } = simfinity.generateMCPTools(schema, {
2710
+ exclude: 'mutation', // read-only server
2711
+ excludeTypes: 'AuditLog', // hide every tool whose entity type is AuditLog
2712
+ toolNamePrefix: 'catalog_', // books -> catalog_books
2713
+ });
2714
+
2715
+ // callTool/getOperation use the published (prefixed) name;
2716
+ // the underlying GraphQL document still uses the raw field name.
2717
+ console.log(getOperation('catalog_books'));
2718
+ // query booksOperation(...) { books(...) { ... } }
2719
+ ```
2720
+
2721
+ - `include`/`exclude` entries match the raw GraphQL field name, the prefixed tool name, or the categories `'query'` / `'mutation'`. `exclude` always wins over `include`.
2722
+ - `includeTypes`/`excludeTypes` match the entity (return) type. Aggregate queries resolve their entity through the sibling list field (`books_aggregate` → `books`), so `excludeTypes: 'Book'` also hides the aggregate tool. With an `includeTypes` allowlist, tools without an object return type are dropped too.
2723
+ - Duplicate tool names (e.g. a Query field and a Mutation field sharing a name) are not allowed by MCP: the first tool wins and the later one is skipped with a `console.warn` — rename one of the GraphQL fields or use `include`/`exclude`.
2724
+
2725
+ ### Customizing tools
2726
+
2727
+ `toolOverrides` tunes individual tools without touching the schema. Keys accept either the published tool name or the unprefixed field name:
2728
+
2729
+ ```javascript
2730
+ const { tools } = simfinity.generateMCPTools(schema, {
2731
+ toolOverrides: {
2732
+ books: {
2733
+ title: 'Search books',
2734
+ description: 'Search the book catalog. Filter by title, author or publication year.',
2735
+ selectionDepth: 2, // deeper selection set for this tool only
2736
+ },
2737
+ publish_book: {
2738
+ annotations: { idempotentHint: true }, // merged over the generated annotations
2739
+ },
2740
+ book: {
2741
+ selection: 'id title author { name }', // explicit selection set
2742
+ },
2743
+ },
2744
+ });
2745
+ ```
2746
+
2747
+ `selection` accepts `'id title'` or `'{ id title }'` and replaces the auto-generated selection set. When it is set, the `outputSchema` is **omitted** for that tool — it can no longer be guaranteed to match the returned data, and publishing a wrong schema would make validating clients reject valid results.
2748
+
2749
+ ### Limits
2750
+
2751
+ `limits` protects agents (and your database) from oversized requests and responses. Violations come back as regular `isError` tool results, not exceptions:
2752
+
2753
+ ```javascript
2754
+ const { callTool } = simfinity.generateMCPTools(schema, {
2755
+ limits: {
2756
+ maxPageSize: 100, // MCP_PAGE_SIZE_EXCEEDED above this
2757
+ defaultPagination: { page: 1, size: 20 }, // injected when the caller sends none
2758
+ maxResultBytes: 262144, // MCP_RESULT_TOO_LARGE above this
2759
+ },
2760
+ });
2761
+ ```
2762
+
2763
+ - `maxPageSize`: rejects calls whose `pagination.size` exceeds the cap.
2764
+ - `defaultPagination`: injected only for tools that have a `pagination` argument, and only when the caller did not send one. `page` and `size` are both required (QLPagination declares them non-null), and `size` must not exceed `maxPageSize` — that misconfiguration throws `MCP_INVALID_LIMITS` at setup.
2765
+ - `maxResultBytes`: rejects results whose serialized `data` is larger than the cap (the error message suggests narrowing the query, lowering `selectionDepth` or paginating).
2766
+
2767
+ ### Tool middleware
2768
+
2769
+ `toolMiddleware` wraps every tool execution with koa-style `(call, next)` functions — for logging, metrics, argument rewriting or policy checks. `call` is `{ name, args, extra, kind, operation }`:
2770
+
2771
+ ```javascript
2772
+ const { callTool } = simfinity.generateMCPTools(schema, {
2773
+ toolMiddleware: [
2774
+ async (call, next) => {
2775
+ const started = Date.now();
2776
+ const result = await next();
2777
+ console.log(`[mcp] ${call.name} (${call.kind}) took ${Date.now() - started}ms`);
2778
+ return result;
2779
+ },
2780
+ async (call, next) => {
2781
+ if (call.kind === 'mutation' && process.env.MCP_READ_ONLY === 'true') {
2782
+ // Short-circuit: return a result without calling next()
2783
+ return {
2784
+ content: [{ type: 'text', text: 'Mutations are disabled on this server.' }],
2785
+ isError: true,
2786
+ };
2787
+ }
2788
+ return next();
2789
+ },
2790
+ ],
2791
+ });
2792
+ ```
2793
+
2794
+ Middleware can mutate `call.args` before calling `next()`, short-circuit by returning a result, or throw. Calling `next()` twice rejects with `MCP_MIDDLEWARE_ERROR`.
2795
+
2796
+ ### Remote execution
2797
+
2798
+ `execution.mode: 'remote'` turns the MCP server into a thin proxy that POSTs each operation to an existing GraphQL endpoint. Harden it with `timeoutMs` and `retry`:
2799
+
2800
+ ```javascript
2801
+ const { callTool } = simfinity.generateMCPTools(schema, {
2802
+ execution: {
2803
+ mode: 'remote',
2804
+ endpoint: 'https://api.example.com/graphql',
2805
+ headers: { Authorization: `Bearer ${token}` },
2806
+ timeoutMs: 10000, // abort requests slower than 10s
2807
+ retry: { attempts: 2, backoffMs: 250 }, // up to 2 extra attempts, linear backoff
2808
+ },
2809
+ });
2810
+ ```
2811
+
2812
+ - `timeoutMs` aborts slow requests (`AbortSignal.timeout`); the timeout signal is combined with the MCP client's cancellation signal.
2813
+ - `retry` re-issues failed **queries only — never mutations** — after network errors/timeouts, HTTP 5xx and 429 responses. Backoff is linear: the first retry waits `backoffMs`, the second `2 × backoffMs`, and so on (default `backoffMs`: 250).
2814
+ - Transport failures do not throw: they surface as `isError` tool results carrying GraphQL-shaped errors with the codes `MCP_REMOTE_HTTP_ERROR` (non-2xx response, includes `extensions.status`), `MCP_REMOTE_REQUEST_FAILED` (network error/timeout) and `MCP_REMOTE_INVALID_RESPONSE` (non-JSON body, or JSON with neither `data` nor `errors`).
2815
+ - If the remote response carries a numeric `extensions.count` (e.g. exposed via `simfinity.plugins.envelopCountPlugin()`), it is surfaced as `_meta.count` on the tool result.
2816
+
2817
+ `schemaPlugins` is not applied in remote mode — the remote GraphQL server enforces its own plugins.
2818
+
2819
+ ### Tool results and errors
2820
+
2821
+ Successful calls return `{ content: [textJSON], structuredContent: data, isError: false }`. Failures and edge cases behave as follows:
2822
+
2823
+ - **Total count**: with `pagination: { count: true }` on a **list** tool, the count lands in `_meta.count` on the tool result — in-process it is read back from a per-call context layer (Simfinity's find resolver writes it there; counts never leak across calls), in remote mode from the response `extensions.count`. Aggregate tools never deliver a count.
2824
+ - **Execution errors**: `isError: true` with a `text` payload of `{ errors, data? }` — partial data is preserved alongside the errors when GraphQL returned any. `structuredContent` is omitted on errors.
2825
+ - **Remote transport failures** and **limit violations** are also `isError` results (see [Remote execution](#remote-execution) and [Limits](#limits) for the codes).
2826
+ - **Unknown tool names** are the exception: `callTool` and `getOperation` throw `MCP_TOOL_NOT_FOUND`. A call whose `extra.signal` is already aborted throws `MCP_CALL_CANCELLED` before executing.
2827
+
2828
+ ### Authentication
2829
+
2830
+ The MCP tools execute the same resolvers as GraphQL, so the GraphQL `context` flows through unchanged: pass credentials via `options.context` (a value or a factory). [Query scope](#-query-scope) functions read the context on every execution and need no extra wiring.
2831
+
2832
+ Field-level rules from [`createAuthPlugin`](#-authorization) are different. The auth plugin is an Envelop plugin that wraps resolvers from its `onSchemaChange` hook — a hook that only fires when an Envelop/Yoga server boots. A standalone MCP server executes operations with bare `graphql()`, so without help the hook never fires and the field-level rules are silently skipped. Pass the plugin via `schemaPlugins` to get auth parity: every plugin's `onSchemaChange` is invoked once before serving (in-process execution only):
2833
+
2834
+ ```javascript
2835
+ import * as simfinity from '@simtlix/simfinity-js';
2836
+
2837
+ const { createAuthPlugin, requireAuth, requireRole } = simfinity.auth;
2838
+
2839
+ // Permissions are keyed by the schema's type names. Simfinity names its
2840
+ // roots `RootQueryType` and `Mutation`.
2841
+ const permissions = {
2842
+ RootQueryType: { books: requireAuth() },
2843
+ Mutation: { deletebook: requireRole('admin') },
2844
+ };
2845
+
2846
+ const schema = simfinity.createSchema();
2847
+ await simfinity.startStdioMCPServer(schema, {
2848
+ // requireRole reads `user.role` by default (see rolePath to customize).
2849
+ context: { user: { id: 'agent', role: 'admin' } },
2850
+ schemaPlugins: [createAuthPlugin(permissions, { defaultPolicy: 'ALLOW' })],
2851
+ });
2852
+ ```
2853
+
2854
+ Mounting the MCP handler next to a running Envelop/Yoga server that shares the same `schema` object also works: Yoga fires `onSchemaChange` at startup and `createAuthPlugin` wraps the resolvers in place, so the MCP tools execute the already-wrapped schema.
2855
+
2856
+ For per-request HTTP auth, use a function `context`: the HTTP handler invokes it as `(req, extra) => context`, so you can read the `Authorization` header from the Express `req` and map it to `context.user` exactly like your GraphQL server does.
2857
+
2563
2858
  ## 📊 Aggregation Queries
2564
2859
 
2565
2860
  Simfinity.js now supports powerful GraphQL aggregation queries with GROUP BY functionality, allowing you to perform aggregate operations (SUM, COUNT, AVG, MIN, MAX) on your data.
package/package.json CHANGED
@@ -1,8 +1,9 @@
1
1
  {
2
2
  "name": "@simtlix/simfinity-js",
3
- "version": "2.6.4",
3
+ "version": "3.0.1",
4
4
  "description": "",
5
5
  "main": "src/index.js",
6
+ "types": "./types/index.d.ts",
6
7
  "type": "module",
7
8
  "scripts": {
8
9
  "test": "vitest run",
@@ -28,12 +29,13 @@
28
29
  "@eslint/js": "^9.30.1",
29
30
  "eslint": "^9.30.1",
30
31
  "globals": "^16.3.0",
31
- "vitest": "^3.2.4"
32
+ "vitest": "^4.1.9"
32
33
  },
33
34
  "config": {
34
35
  "owner": "simtlix"
35
36
  },
36
37
  "optionalDependencies": {
38
+ "@modelcontextprotocol/sdk": "^1.13.0",
37
39
  "graphql": "^16.11.0",
38
40
  "graphql-middleware": "^6.1.35",
39
41
  "mongoose": "^8.16.2"
package/src/index.js CHANGED
@@ -553,11 +553,13 @@ const buildInputType = (gqltype) => {
553
553
 
554
554
  const inputTypeBody = {
555
555
  name: `${gqltype.name}Input`,
556
+ description: gqltype.description,
556
557
  fields: fieldsArgs,
557
558
  };
558
559
 
559
560
  const inputTypeBodyForUpdate = {
560
561
  name: `${gqltype.name}InputForUpdate`,
562
+ description: gqltype.description,
561
563
  fields: fieldsArgForUpdate,
562
564
  };
563
565
 
@@ -1939,6 +1941,14 @@ export { default as validators } from './validators.js';
1939
1941
  export { default as scalars } from './scalars.js';
1940
1942
  export { default as plugins } from './plugins.js';
1941
1943
  export { default as auth } from './auth/index.js';
1944
+ export { default as mcp } from './mcp.js';
1945
+ export {
1946
+ generateMCPTools,
1947
+ graphqlArgsToJSONSchema,
1948
+ createMCPServer,
1949
+ startStdioMCPServer,
1950
+ createHTTPMCPHandler,
1951
+ } from './mcp.js';
1942
1952
 
1943
1953
  export { buildQuery, buildFilterGroupMatch };
1944
1954