diff --git a/apps/zbugs/src/pages/issue/comment.tsx b/apps/zbugs/src/pages/issue/comment.tsx index 36f7c27f4f..3479348133 100644 --- a/apps/zbugs/src/pages/issue/comment.tsx +++ b/apps/zbugs/src/pages/issue/comment.tsx @@ -1,7 +1,6 @@ import type {Row} from '@rocicorp/zero'; import classNames from 'classnames'; import {memo, useState} from 'react'; -import type {Schema} from '../../../schema.js'; import {makePermalink} from '../../comment-permalink.js'; import {Button} from '../../components/button.js'; import {CanEdit} from '../../components/can-edit.js'; @@ -10,21 +9,17 @@ import {EmojiPanel} from '../../components/emoji-panel.js'; import {Link} from '../../components/link.js'; import {Markdown} from '../../components/markdown.js'; import {RelativeTime} from '../../components/relative-time.js'; -import {type Emoji} from '../../emoji-utils.js'; import {useHash} from '../../hooks/use-hash.js'; import {useLogin} from '../../hooks/use-login.js'; import {useZero} from '../../hooks/use-zero.js'; import {CommentComposer} from './comment-composer.js'; import style from './comment.module.css'; +import type {commentQuery} from './issue-page.js'; type Props = { id: string; issueID: string; - comment: Row & { - readonly creator: Row | undefined; - } & { - readonly emoji: readonly Emoji[]; - }; + comment: Row>; /** * Height of the comment. Used to keep the layout stable when comments are * being "loaded". diff --git a/apps/zbugs/src/pages/issue/issue-page.tsx b/apps/zbugs/src/pages/issue/issue-page.tsx index c3ceb7927b..f47512c896 100644 --- a/apps/zbugs/src/pages/issue/issue-page.tsx +++ b/apps/zbugs/src/pages/issue/issue-page.tsx @@ -61,6 +61,17 @@ const emojiToastShowDuration = 3_000; // to load. export const INITIAL_COMMENT_LIMIT = 101; +export function commentQuery(z: Zero, displayed: IssueRow | undefined) { + return z.query.comment + .where('issueID', 'IS', displayed?.id ?? null) + .related('creator', creator => creator.one()) + .related('emoji', emoji => + emoji.related('creator', creator => creator.one()), + ) + .orderBy('created', 'asc') + .orderBy('id', 'asc'); +} + export function IssuePage({onReady}: {onReady: () => void}) { const z = useZero(); const params = useParams(); @@ -232,14 +243,7 @@ export function IssuePage({onReady}: {onReady: () => void}) { const [displayAllComments, setDisplayAllComments] = useState(false); const [allComments, allCommentsResult] = useQuery( - z.query.comment - .where('issueID', displayed?.id ?? '') - .related('creator', creator => creator.one()) - .related('emoji', emoji => - emoji.related('creator', creator => creator.one()), - ) - .orderBy('created', 'asc') - .orderBy('id', 'asc'), + commentQuery(z, displayed), displayAllComments && displayed !== undefined, ); diff --git a/packages/zql/src/query/query.test.ts b/packages/zql/src/query/query.test.ts index d7eb2b7a04..39566494e3 100644 --- a/packages/zql/src/query/query.test.ts +++ b/packages/zql/src/query/query.test.ts @@ -521,6 +521,9 @@ describe('types', () => { query.where('s', '=', null); // @ts-expect-error - cannot compare with undefined query.where('s', '=', undefined); + + // IS can compare to null + query.where('s', 'IS', null); }); test('start', () => { diff --git a/packages/zql/src/query/query.ts b/packages/zql/src/query/query.ts index 391336638d..6df36ade7a 100644 --- a/packages/zql/src/query/query.ts +++ b/packages/zql/src/query/query.ts @@ -50,6 +50,8 @@ export type GetFieldTypeNoUndefined< SchemaValueToTSType, null | undefined >[] + : TOperator extends 'IS' | 'IS NOT' + ? Exclude, undefined> | null : Exclude, undefined>; export type Row> = @@ -57,7 +59,9 @@ export type Row> = ? { [K in keyof T['columns']]: SchemaValueToTSType; } - : QueryRowType>; + : T extends Query + ? QueryRowType + : never; export type Rows> = T extends TableSchema ? Row[] : QueryReturnType>; diff --git a/packages/zqlite/src/query.test.ts b/packages/zqlite/src/query.test.ts index eb10521771..12049610bc 100644 --- a/packages/zqlite/src/query.test.ts +++ b/packages/zqlite/src/query.test.ts @@ -1,4 +1,4 @@ -import {beforeEach, expect, test} from 'vitest'; +import {beforeEach, expect, expectTypeOf, test} from 'vitest'; import {createSilentLogContext} from '../../shared/src/logging-test-utils.js'; import {must} from '../../shared/src/must.js'; import {normalizeTableSchema} from '../../zero-schema/src/normalize-table-schema.js'; @@ -8,6 +8,7 @@ import {newQuery, type QueryDelegate} from '../../zql/src/query/query-impl.js'; import {schemas} from '../../zql/src/query/test/testSchemas.js'; import {Database} from './db.js'; import {TableSource, toSQLiteTypeName} from './table-source.js'; +import type {Row} from '../../zql/src/query/query.js'; let queryDelegate: QueryDelegate; beforeEach(() => { @@ -125,6 +126,24 @@ beforeEach(() => { }); }); +test('row type', () => { + const query = newQuery(queryDelegate, schemas.issue) + .whereExists('labels', q => q.where('name', '=', 'bug')) + .related('labels'); + type RT = Row; + expectTypeOf().toEqualTypeOf<{ + readonly id: string; + readonly title: string; + readonly description: string; + readonly closed: boolean; + readonly ownerId: string | null; + readonly labels: readonly { + readonly id: string; + readonly name: string; + }[]; + }>(); +}); + test('basic query', () => { const query = newQuery(queryDelegate, schemas.issue); const data = query.run();