Repository: igorkamyshev/farfetched
Pull request #425 (2023-12-12 06:39:33)

`filter` to `connectQuery`

cloudflare-pages[bot]: ## Deploying with &nbsp;<a href="https://pages.dev"><img alt="Cloudflare Pages" src="https://user-images.githubusercontent.com/23264/106598434-9e719e00-654f-11eb-9e59-6167043cfa01.png" width="16"></a> &nbsp;Cloudflare Pages

<table><tr><td><strong>Latest commit:</strong> </td><td>
<code>0390175</code>
</td></tr>
<tr><td><strong>Status:</strong></td><td>&nbsp;✅&nbsp; Deploy successful!</td></tr>
<tr><td><strong>Preview URL:</strong></td><td>
<a href='https://ae2f2806.farfetched.pages.dev'>https://ae2f2806.farfetched.pages.dev</a>
</td></tr>
<tr><td><strong>Branch Preview URL:</strong></td><td>
<a href='https://filter-connectquery.farfetched.pages.dev'>https://filter-connectquery.farfetched.pages.dev</a>
</td></tr>
</table>

[View logs](https://dash.cloudflare.com/?to=/:account/pages/view/farfetched/ae2f2806-bb1e-4b57-bd0e-b3f60a92c841)

github-actions[bot]: # ATTW

<details>
  <summary>Analysis</summary>
  
@farfetched/atomic-router v0.11.2

 No problems found 🌟


"@farfetched/atomic-router"

node10: 🟢 
node16 (from CJS): 🟢 (CJS)
node16 (from ESM): 🟢 (CJS)
bundler: 🟢 

***********************************

@farfetched/core v0.11.2

 No problems found 🌟


"@farfetched/core"

node10: 🟢 
node16 (from CJS): 🟢 (CJS)
node16 (from ESM): 🟢 (CJS)
bundler: 🟢 

***********************************

@farfetched/dev-tools v0.11.1

 No problems found 🌟


"@farfetched/dev-tools"

node10: 🟢 
node16 (from CJS): 🟢 (CJS)
node16 (from ESM): 🟢 (CJS)
bundler: 🟢 

***********************************

@farfetched/io-ts v0.11.2

 No problems found 🌟


"@farfetched/io-ts"

node10: 🟢 
node16 (from CJS): 🟢 (CJS)
node16 (from ESM): 🟢 (CJS)
bundler: 🟢 

***********************************

@farfetched/runtypes v0.11.2

 No problems found 🌟


"@farfetched/runtypes"

node10: 🟢 
node16 (from CJS): 🟢 (CJS)
node16 (from ESM): 🟢 (CJS)
bundler: 🟢 

***********************************

@farfetched/solid v0.11.2

 No problems found 🌟


"@farfetched/solid"

node10: 🟢 
node16 (from CJS): 🟢 (CJS)
node16 (from ESM): 🟢 (CJS)
bundler: 🟢 

***********************************

@farfetched/superstruct v0.11.2

 No problems found 🌟


"@farfetched/superstruct"

node10: 🟢 
node16 (from CJS): 🟢 (CJS)
node16 (from ESM): 🟢 (CJS)
bundler: 🟢 

***********************************

@farfetched/typed-contracts v0.11.2

 No problems found 🌟


"@farfetched/typed-contracts"

node10: 🟢 
node16 (from CJS): 🟢 (CJS)
node16 (from ESM): 🟢 (CJS)
bundler: 🟢 

***********************************

@farfetched/zod v0.11.2

 No problems found 🌟


"@farfetched/zod"

node10: 🟢 
node16 (from CJS): 🟢 (CJS)
node16 (from ESM): 🟢 (CJS)
bundler: 🟢 

***********************************
</details>
github-actions[bot]: # publint

<details>
  <summary>Analysis</summary>
  @farfetched/atomic-router lint results:

Suggestions:

1. pkg.module is used to output ESM but pkg.exports is not defined. As NodeJS doesn't read pkg.module the ESM output may be skipped. Consider adding pkg.exports to export the ESM output. pkg.module can usually be removed alongside too. (This will be a breaking change)

Warnings:

1. /index.esm.js is written in ESM but is interpreted as CJS. Consider using the .mjs extension e.g. /index.esm.mjs


@farfetched/core lint results:

Suggestions:

1. pkg.module is used to output ESM but pkg.exports is not defined. As NodeJS doesn't read pkg.module the ESM output may be skipped. Consider adding pkg.exports to export the ESM output. pkg.module can usually be removed alongside too. (This will be a breaking change)

Warnings:

1. /index.esm.js is written in ESM but is interpreted as CJS. Consider using the .mjs extension e.g. /index.esm.mjs


@farfetched/dev-tools lint results:

Suggestions:

1. pkg.module is used to output ESM but pkg.exports is not defined. As NodeJS doesn't read pkg.module the ESM output may be skipped. Consider adding pkg.exports to export the ESM output. pkg.module can usually be removed alongside too. (This will be a breaking change)


@farfetched/io-ts lint results:

Suggestions:

1. pkg.module is used to output ESM but pkg.exports is not defined. As NodeJS doesn't read pkg.module the ESM output may be skipped. Consider adding pkg.exports to export the ESM output. pkg.module can usually be removed alongside too. (This will be a breaking change)

Warnings:

1. /index.esm.js is written in ESM but is interpreted as CJS. Consider using the .mjs extension e.g. /index.esm.mjs


@farfetched/runtypes lint results:

Suggestions:

1. pkg.module is used to output ESM but pkg.exports is not defined. As NodeJS doesn't read pkg.module the ESM output may be skipped. Consider adding pkg.exports to export the ESM output. pkg.module can usually be removed alongside too. (This will be a breaking change)

Warnings:

1. /index.esm.js is written in ESM but is interpreted as CJS. Consider using the .mjs extension e.g. /index.esm.mjs


@farfetched/solid lint results:

Suggestions:

1. pkg.module is used to output ESM but pkg.exports is not defined. As NodeJS doesn't read pkg.module the ESM output may be skipped. Consider adding pkg.exports to export the ESM output. pkg.module can usually be removed alongside too. (This will be a breaking change)

Warnings:

1. /index.esm.js is written in ESM but is interpreted as CJS. Consider using the .mjs extension e.g. /index.esm.mjs


@farfetched/superstruct lint results:

Suggestions:

1. pkg.module is used to output ESM but pkg.exports is not defined. As NodeJS doesn't read pkg.module the ESM output may be skipped. Consider adding pkg.exports to export the ESM output. pkg.module can usually be removed alongside too. (This will be a breaking change)

Warnings:

1. /index.esm.js is written in ESM but is interpreted as CJS. Consider using the .mjs extension e.g. /index.esm.mjs


@farfetched/typed-contracts lint results:

Suggestions:

1. pkg.module is used to output ESM but pkg.exports is not defined. As NodeJS doesn't read pkg.module the ESM output may be skipped. Consider adding pkg.exports to export the ESM output. pkg.module can usually be removed alongside too. (This will be a breaking change)

Warnings:

1. /index.esm.js is written in ESM but is interpreted as CJS. Consider using the .mjs extension e.g. /index.esm.mjs


@farfetched/zod lint results:

Suggestions:

1. pkg.module is used to output ESM but pkg.exports is not defined. As NodeJS doesn't read pkg.module the ESM output may be skipped. Consider adding pkg.exports to export the ESM output. pkg.module can usually be removed alongside too. (This will be a breaking change)

Warnings:

1. /index.esm.js is written in ESM but is interpreted as CJS. Consider using the .mjs extension e.g. /index.esm.mjs
</details>
github-actions[bot]: # Farfetched Canary

Latest canary version is "1.0.0-425.1"

[More information about canary versions](https://farfetched.pages.dev/releases/canary.html)

---

Used changeset: "["hip-bees-grow","modern-trains-leave","neat-ants-kiss","selfish-ducks-hope","silly-gorillas-yawn","spotty-owls-think","strong-dryers-warn","twelve-suits-jam"]"

Patch:
diff --git a/.changeset/modern-trains-leave.md b/.changeset/modern-trains-leave.md
new file mode 100644
index 0000000..b68ee40
--- /dev/null
+++ b/.changeset/modern-trains-leave.md
@@ -0,0 +1,5 @@
+---
+'@farfetched/core': minor
+---
+
+Add `filter` parameter to `connectQuery` operator
diff --git a/apps/website/docs/api/operators/connect_query.md b/apps/website/docs/api/operators/connect_query.md
index 579129a..56f3f3e 100644
--- a/apps/website/docs/api/operators/connect_query.md
+++ b/apps/website/docs/api/operators/connect_query.md
@@ -4,10 +4,12 @@ Creates static connection between [_Queries_](../primitives/query.md). Every tim
 
 ## Formulae
 
-### `connectQuery({ source, target })`
+### `connectQuery({ source, target, filter? })`
 
 This form is used for `target` [_Query_](../primitives/query.md) without parameters. It does not pass any data to `target`.
 
+<Badge type="tip" text="since v0.12" /> Optional `filter` function can be used to filter out unnecessary updates. It accepts an object with the shape from `source`, each field contains `result` and `params` of the corresponding [_Query_](../primitives/query.md) and returns `true` if `target` [_Query_](../primitives/query.md) should be started.
+
 ```ts
 const languagesQuery: Query<unknown, unknown, unknown>;
 const blocksQuery: Query<unknown, unknown, unknown>;
@@ -20,12 +22,14 @@ connectQuery({
 });
 ```
 
-### `connectQuery({ source, fn, target })`
+### `connectQuery({ source, fn, target, filter? })`
 
 This form is used for `target` [_Query_](../primitives/query.md) with parameters. It gets results and parameters of `source`, transforms it through `fn` and uses it for `target` [_Query_](../primitives/query.md).
 
 `fn` accepts an object with the shape from `source`, each field contains `result` and `params` of the corresponding [_Query_](../primitives/query.md) and returns an object with a single field `params` which is used to start `target` [_Query_](../primitives/query.md).
 
+<Badge type="tip" text="since v0.12" /> Optional `filter` function can be used to filter out unnecessary updates. It accepts an object with the shape from `source`, each field contains `result` and `params` of the corresponding [_Query_](../primitives/query.md) and returns `true` if `target` [_Query_](../primitives/query.md) should be started.
+
 ```ts
 const blocksQuery: Query<unknown, string, unknown>;
 const blocksQuery: Query<unknown, string[], unknown>;
diff --git a/packages/core/src/query/connect_query.ts b/packages/core/src/query/connect_query.ts
index 5a06ee4..0c87b84 100644
--- a/packages/core/src/query/connect_query.ts
+++ b/packages/core/src/query/connect_query.ts
@@ -18,7 +18,7 @@ export function connectQuery<Sources, Target extends Query<any, any, any, any>>(
     target: Target | [...Target[]];
   } & (Target extends Query<infer P, any, any, any>
     ? P extends void
-      ? { fn?: undefined }
+      ? { fn?: undefined; filter?: () => boolean }
       : Sources extends Query<any, any, any>
       ? {
           fn: (sources: {
@@ -27,6 +27,10 @@ export function connectQuery<Sources, Target extends Query<any, any, any, any>>(
           }) => {
             params: RemoteOperationParams<Target>;
           };
+          filter?: (sources: {
+            result: RemoteOperationResult<Sources>;
+            params: RemoteOperationParams<Sources>;
+          }) => boolean;
         }
       : Sources extends Record<string, Query<any, any, any>>
       ? {
@@ -36,6 +40,12 @@ export function connectQuery<Sources, Target extends Query<any, any, any, any>>(
               params: RemoteOperationParams<Sources[index]>;
             };
           }) => { params: RemoteOperationParams<Target> };
+          filter?: (sources: {
+            [index in keyof Sources]: {
+              result: RemoteOperationResult<Sources[index]>;
+              params: RemoteOperationParams<Sources[index]>;
+            };
+          }) => boolean;
         }
       : NonExtendable
     : NonExtendable)
@@ -50,6 +60,7 @@ export function connectQuery<Sources, Target extends Query<any, any, any, any>>(
     ? [source]
     : Object.values(source as any);
   const mapperFn = args?.fn as (args: any) => { params?: any };
+  const filterFn = args?.filter as (args: any) => boolean;
 
   // Helper untis
   const anyParentStarted = merge(parents.map((query) => query.start));
@@ -112,6 +123,16 @@ export function connectQuery<Sources, Target extends Query<any, any, any, any>>(
       data: $allParentDataDictionary,
       params: $allParentParamsDictionary,
     },
+    filter({ data, params }) {
+      console.log({ data, params });
+      return (
+        filterFn?.(
+          singleParentMode
+            ? { result: data, params }
+            : zipObject({ result: data, params })
+        ) ?? true
+      );
+    },
     fn({ data, params }) {
       const mapped = mapperFn?.(
         singleParentMode


Test patch:
diff --git a/packages/core/src/query/__tests__/connect_query.filter.test.ts b/packages/core/src/query/__tests__/connect_query.filter.test.ts
new file mode 100644
index 0000000..44c0dbd
--- /dev/null
+++ b/packages/core/src/query/__tests__/connect_query.filter.test.ts
@@ -0,0 +1,82 @@
+import { allSettled, createWatch, fork } from 'effector';
+import { describe, test, expect, vi } from 'vitest';
+
+import { connectQuery } from '../connect_query';
+import { createQuery } from '../create_query';
+
+describe('connectQuery filter', () => {
+  const languagesQ = createQuery({
+    handler: vi.fn(async (p: string) => [p]),
+  });
+
+  const blocksQ = createQuery({
+    handler: vi.fn(async (p: string) => [p]),
+  });
+
+  const contentQ = createQuery({
+    handler: vi.fn(async (p: string) => [p]),
+  });
+
+  test('filter as function (no args)', async () => {
+    const contentStartedFn = vi.fn();
+
+    connectQuery({
+      source: { language: languagesQ, blocks: blocksQ },
+      filter: () => false,
+      fn: ({ language, blocks }) => ({
+        params: [...language.result, ...blocks.result].join(','),
+      }),
+      target: contentQ,
+    });
+
+    const scope = fork();
+
+    await allSettled(languagesQ.start, { scope, params: '1' });
+    await allSettled(blocksQ.start, { scope, params: '2' });
+
+    expect(contentStartedFn).not.toHaveBeenCalled();
+  });
+
+  test('filter as function (args from parents)', async () => {
+    const filterFn = vi.fn(() => false);
+    const contentStartedFn = vi.fn(() => false);
+
+    connectQuery({
+      source: { language: languagesQ, blocks: blocksQ },
+      filter: filterFn,
+      fn: ({ language, blocks }) => ({
+        params: [...language.result, ...blocks.result].join(','),
+      }),
+      target: contentQ,
+    });
+
+    const scope = fork();
+
+    createWatch({ unit: contentQ.started, scope, fn: contentStartedFn });
+
+    await allSettled(languagesQ.start, { scope, params: '1' });
+    await allSettled(blocksQ.start, { scope, params: '2' });
+
+    expect(filterFn.mock.calls).toMatchInlineSnapshot(`
+      [
+        [
+          {
+            "blocks": {
+              "params": "2",
+              "result": [
+                "2",
+              ],
+            },
+            "language": {
+              "params": "1",
+              "result": [
+                "1",
+              ],
+            },
+          },
+        ],
+      ]
+    `);
+    expect(contentStartedFn).not.toHaveBeenCalled();
+  });
+});