Skip to content

Commit f313377

Browse files
committed
introduce filters
1 parent b246c70 commit f313377

File tree

6 files changed

+107
-60
lines changed

6 files changed

+107
-60
lines changed
Lines changed: 48 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,48 @@
1+
import { useQuery } from '@graphprotocol/hypergraph-react';
2+
import { Todo } from '../schema';
3+
4+
export const TodosReadOnlyFilter = () => {
5+
const { data: todosCompleted } = useQuery(Todo, { mode: 'local', filter: { completed: true } });
6+
const { data: todosNotCompleted } = useQuery(Todo, { mode: 'local', filter: { completed: false } });
7+
8+
return (
9+
<>
10+
<h1 className="text-2xl font-bold">Todos Filter (read only)</h1>
11+
<h2 className="text-lg font-bold">Not completed</h2>
12+
{todosNotCompleted.map((todo) => (
13+
<div key={todo.id} className="flex flex-row items-center gap-2">
14+
<h2>{todo.name}</h2>
15+
{todo.assignees.length > 0 && (
16+
<span className="text-xs text-gray-500">
17+
Assigned to:{' '}
18+
{todo.assignees.map((assignee) => (
19+
<span key={assignee.id} className="border rounded-sm mr-1 p-1">
20+
{assignee.name}
21+
</span>
22+
))}
23+
</span>
24+
)}
25+
<input type="checkbox" checked={todo.completed} readOnly />
26+
</div>
27+
))}
28+
29+
<h2 className="text-lg font-bold">Completed</h2>
30+
{todosCompleted.map((todo) => (
31+
<div key={todo.id} className="flex flex-row items-center gap-2">
32+
<h2>{todo.name}</h2>
33+
{todo.assignees.length > 0 && (
34+
<span className="text-xs text-gray-500">
35+
Assigned to:{' '}
36+
{todo.assignees.map((assignee) => (
37+
<span key={assignee.id} className="border rounded-sm mr-1 p-1">
38+
{assignee.name}
39+
</span>
40+
))}
41+
</span>
42+
)}
43+
<input type="checkbox" checked={todo.completed} readOnly />
44+
</div>
45+
))}
46+
</>
47+
);
48+
};

apps/events/src/routes/space/$spaceId/index.tsx

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,7 @@
11
import { DevTool } from '@/components/dev-tool';
22
import { Todos } from '@/components/todos';
33
import { TodosReadOnly } from '@/components/todos-read-only';
4+
import { TodosReadOnlyFilter } from '@/components/todos-read-only-filter';
45
import { Button } from '@/components/ui/button';
56
import { Users } from '@/components/users';
67
import { availableAccounts } from '@/lib/availableAccounts';
@@ -43,6 +44,7 @@ function Space() {
4344
<Users />
4445
<Todos />
4546
<TodosReadOnly />
47+
<TodosReadOnlyFilter />
4648
{show2ndTodos && <Todos />}
4749
<h3 className="text-xl font-bold">Invite people</h3>
4850
<div className="flex flex-row gap-2">

packages/hypergraph-react/src/HypergraphSpaceContext.tsx

Lines changed: 5 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -83,12 +83,13 @@ export function useHardDeleteEntity() {
8383
return Entity.delete(hypergraph.handle);
8484
}
8585

86-
type QueryParams = {
86+
type QueryParams<S extends Entity.AnyNoContext> = {
8787
enabled: boolean;
88+
filter?: Schema.Simplify<Partial<Schema.Schema.Type<S>>> | undefined;
8889
};
8990

90-
export function useQueryLocal<const S extends Entity.AnyNoContext>(type: S, params?: QueryParams) {
91-
const { enabled = true } = params ?? {};
91+
export function useQueryLocal<const S extends Entity.AnyNoContext>(type: S, params?: QueryParams<S>) {
92+
const { enabled = true, filter } = params ?? {};
9293
const entitiesRef = useRef<Entity.Entity<S>[]>([]);
9394

9495
const hypergraph = useHypergraph();
@@ -100,7 +101,7 @@ export function useQueryLocal<const S extends Entity.AnyNoContext>(type: S, para
100101
};
101102
}
102103

103-
return Entity.subscribeToFindMany(hypergraph.handle, type);
104+
return Entity.subscribeToFindMany(hypergraph.handle, type, filter);
104105
});
105106

106107
// TODO: allow to change the enabled state

packages/hypergraph-react/src/use-query.tsx

Lines changed: 6 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -1,5 +1,6 @@
11
import type { Entity } from '@graphprotocol/hypergraph';
22
import { Utils } from '@graphprotocol/hypergraph';
3+
import type * as Schema from 'effect/Schema';
34
import { useMemo } from 'react';
45
import { useHypergraph, useQueryLocal } from './HypergraphSpaceContext.js';
56
import { generateDeleteOps } from './internal/generate-delete-ops-geo.js';
@@ -8,8 +9,9 @@ import { useGenerateUpdateOps } from './internal/use-generate-update-ops.js';
89
import { parseResult, useQueryPublic } from './internal/use-query-public-geo.js';
910
import type { DiffEntry, PublishDiffInfo } from './types.js';
1011

11-
type QueryParams = {
12+
type QueryParams<S extends Entity.AnyNoContext> = {
1213
mode: 'merged' | 'public' | 'local';
14+
filter?: Schema.Simplify<Partial<Schema.Schema.Type<S>>> | undefined;
1315
};
1416

1517
const mergeEntities = <S extends Entity.AnyNoContext>(
@@ -116,10 +118,10 @@ const getDiff = <S extends Entity.AnyNoContext>(
116118

117119
const preparePublishDummy = () => undefined;
118120

119-
export function useQuery<const S extends Entity.AnyNoContext>(type: S, params?: QueryParams) {
120-
const { mode = 'merged' } = params ?? {};
121+
export function useQuery<const S extends Entity.AnyNoContext>(type: S, params?: QueryParams<S>) {
122+
const { mode = 'merged', filter } = params ?? {};
121123
const publicResult = useQueryPublic(type, { enabled: mode === 'public' || mode === 'merged' });
122-
const localResult = useQueryLocal(type, { enabled: mode === 'local' || mode === 'merged' });
124+
const localResult = useQueryLocal(type, { enabled: mode === 'local' || mode === 'merged', filter });
123125
const { mapping } = useHypergraph();
124126
const generateCreateOps = useGenerateCreateOps(type, mode === 'merged');
125127
const generateUpdateOps = useGenerateUpdateOps(type, mode === 'merged');

packages/hypergraph/src/entity/findMany.ts

Lines changed: 37 additions & 51 deletions
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,7 @@
11
import type { DocHandle, Patch } from '@automerge/automerge-repo';
22
import * as Schema from 'effect/Schema';
33
import { isRelationField } from '../utils/isRelationField.js';
4+
import { canonicalize } from '../utils/jsc.js';
45
import { type DecodedEntitiesCacheEntry, type QueryEntry, decodedEntitiesCache } from './decodedEntitiesCache.js';
56
import { entityRelationParentsMap } from './entityRelationParentsMap.js';
67
import { getEntityRelations } from './getEntityRelations.js';
@@ -122,15 +123,9 @@ const subscribeToDocumentChanges = (handle: DocHandle<DocumentContent>) => {
122123
}
123124
}
124125

125-
const query = cacheEntry.queries.get('all');
126-
if (query && decoded) {
127-
const index = query.data.findIndex((entity) => entity.id === entityId);
128-
if (index !== -1) {
129-
query.data[index] = decoded;
130-
} else {
131-
query.data.push(decoded);
132-
}
133-
touchedQueries.add([typeName, 'all']);
126+
for (const [queryKey, query] of cacheEntry.queries) {
127+
touchedQueries.add([typeName, queryKey]);
128+
query.isInvalidated = true;
134129
}
135130

136131
entityTypes.add(typeName);
@@ -155,12 +150,12 @@ const subscribeToDocumentChanges = (handle: DocHandle<DocumentContent>) => {
155150
entityTypes.add(affectedTypeName);
156151
cacheEntry.entities.delete(entityId);
157152

158-
for (const [, query] of cacheEntry.queries) {
153+
for (const [queryKey, query] of cacheEntry.queries) {
159154
// find the entity in the query and remove it using splice
160155
const index = query.data.findIndex((entity) => entity.id === entityId);
161156
if (index !== -1) {
162157
query.data.splice(index, 1);
163-
touchedQueries.add([affectedTypeName, 'all']);
158+
touchedQueries.add([affectedTypeName, queryKey]);
164159
}
165160
}
166161
}
@@ -228,6 +223,7 @@ const subscribeToDocumentChanges = (handle: DocHandle<DocumentContent>) => {
228223
export function findMany<const S extends AnyNoContext>(
229224
handle: DocHandle<DocumentContent>,
230225
type: S,
226+
filter?: Schema.Simplify<Partial<Schema.Schema.Type<S>>> | undefined,
231227
): { entities: Readonly<Array<Entity<S>>>; corruptEntityIds: Readonly<Array<string>> } {
232228
const decode = Schema.decodeUnknownSync(type);
233229
// TODO: what's the right way to get the name of the type?
@@ -246,7 +242,16 @@ export function findMany<const S extends AnyNoContext>(
246242
if (hasValidTypesProperty(entity) && entity['@@types@@'].includes(typeName)) {
247243
const relations = getEntityRelations(id, type, doc);
248244
try {
249-
filtered.push({ ...decode({ ...entity, ...relations, id }), type: typeName });
245+
const decoded = { ...decode({ ...entity, ...relations, id }), type: typeName };
246+
if (filter) {
247+
for (const filterEntry in filter) {
248+
if (decoded[filterEntry] === filter[filterEntry]) {
249+
filtered.push(decoded);
250+
}
251+
}
252+
} else {
253+
filtered.push(decoded);
254+
}
250255
} catch (error) {
251256
corruptEntityIds.push(id);
252257
}
@@ -261,11 +266,12 @@ const stableEmptyArray: Array<unknown> = [];
261266
export function subscribeToFindMany<const S extends AnyNoContext>(
262267
handle: DocHandle<DocumentContent>,
263268
type: S,
269+
filter?: Schema.Simplify<Partial<Schema.Schema.Type<S>>> | undefined,
264270
): {
265271
subscribe: (callback: () => void) => () => void;
266272
getEntities: () => Readonly<Array<Entity<S>>>;
267273
} {
268-
const queryKey = 'all';
274+
const queryKey = filter ? canonicalize(filter) : 'all';
269275
const decode = Schema.decodeUnknownSync(type);
270276
// TODO: what's the right way to get the name of the type?
271277
// @ts-expect-error name is defined
@@ -282,24 +288,17 @@ export function subscribeToFindMany<const S extends AnyNoContext>(
282288
return query.data;
283289
}
284290

285-
const { entities } = findMany(handle, type);
291+
const { entities } = findMany(handle, type, filter);
292+
286293
for (const entity of entities) {
287294
cacheEntry?.entities.set(entity.id, entity);
288-
289-
if (!query) continue;
290-
291-
const index = query.data.findIndex((e) => e.id === entity.id);
292-
if (index !== -1) {
293-
query.data[index] = entity;
294-
} else {
295-
query.data.push(entity);
296-
}
297295
}
298296

297+
// must be a new reference to ensure it can be used in React.useMemo
298+
query.data = [...entities];
299299
cacheEntry.isInvalidated = false;
300300
query.isInvalidated = false;
301-
// must be a new reference to ensure it can be used in React.useMemo
302-
query.data = [...query.data];
301+
303302
return query.data;
304303
};
305304

@@ -314,49 +313,36 @@ export function subscribeToFindMany<const S extends AnyNoContext>(
314313
let cacheEntry = decodedEntitiesCache.get(typeName);
315314

316315
if (!cacheEntry) {
317-
const { entities } = findMany(handle, type);
318316
const entitiesMap = new Map();
319-
for (const entity of entities) {
320-
entitiesMap.set(entity.id, entity);
321-
}
322-
323317
const queries = new Map<string, QueryEntry>();
324318

325319
queries.set(queryKey, {
326-
data: [...entities],
320+
data: [],
327321
listeners: [],
328-
isInvalidated: false,
322+
isInvalidated: true,
329323
});
330324

331325
cacheEntry = {
332326
decoder: decode,
333327
type,
334328
entities: entitiesMap,
335329
queries,
336-
isInvalidated: false,
330+
isInvalidated: true,
337331
};
338332

339333
decodedEntitiesCache.set(typeName, cacheEntry);
340-
341-
for (const entity of entities) {
342-
for (const [, value] of Object.entries(entity)) {
343-
if (Array.isArray(value)) {
344-
for (const relationEntity of value) {
345-
let relationParentEntry = entityRelationParentsMap.get(relationEntity.id);
346-
if (relationParentEntry) {
347-
relationParentEntry.set(cacheEntry, (relationParentEntry.get(cacheEntry) ?? 0) + 1);
348-
} else {
349-
relationParentEntry = new Map();
350-
entityRelationParentsMap.set(relationEntity.id, relationParentEntry);
351-
relationParentEntry.set(cacheEntry, 1);
352-
}
353-
}
354-
}
355-
}
356-
}
357334
}
358335

359-
const query = cacheEntry.queries.get(queryKey);
336+
let query = cacheEntry.queries.get(queryKey);
337+
if (!query) {
338+
query = {
339+
data: [],
340+
listeners: [],
341+
isInvalidated: true,
342+
};
343+
// we just set up the query and expect it to correctly set itself up in findMany
344+
cacheEntry.queries.set(queryKey, query);
345+
}
360346

361347
if (query?.listeners) {
362348
query.listeners.push(callback);

packages/hypergraph/src/entity/update.ts

Lines changed: 9 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -33,12 +33,20 @@ export const update = <const S extends AnyNoContext>(handle: DocHandle<DocumentC
3333

3434
// TODO: Try to get a diff of the entity properties and only override the changed ones.
3535
updated = { ...decode(entity), ...data };
36-
doc.entities[id] = {
36+
37+
const encoded = {
3738
...encode(updated),
3839
'@@types@@': [typeName],
3940
__deleted: entity.__deleted ?? false,
4041
__version: entity.__version ?? '',
4142
};
43+
// filter out undefined values otherwise Automerge will throw an error
44+
for (const key in updated) {
45+
if (updated[key] === undefined) {
46+
delete encoded[key];
47+
}
48+
}
49+
doc.entities[id] = encoded;
4250
});
4351

4452
if (updated === undefined) {

0 commit comments

Comments
 (0)