Skip to content

Commit

Permalink
Merge pull request #85 from cardstack/type-filter-first-pass
Browse files Browse the repository at this point in the history
Specify card types when filtering
  • Loading branch information
ef4 authored Aug 4, 2022
2 parents 47a4963 + da46297 commit 1be2628
Show file tree
Hide file tree
Showing 3 changed files with 246 additions and 104 deletions.
162 changes: 94 additions & 68 deletions host/tests/unit/search-index-test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -649,6 +649,7 @@ posts/ignore-me.gts
import { contains, field, Card } from 'https://cardstack.com/base/card-api';
import StringCard from 'https://cardstack.com/base/string';
import IntegerCard from 'https://cardstack.com/base/integer';
import DatetimeCard from 'https://cardstack.com/base/datetime';
export class Person extends Card {
@field name = contains(StringCard);
Expand All @@ -660,6 +661,19 @@ posts/ignore-me.gts
@field description = contains(StringCard);
@field author = contains(Person);
@fields views = contains(IntegerCard);
@fields createdAt = contains(DatetimeCard);
}
export class Article extends Post {
@fields publishedDate = contains(DatetimeCard);
}
`,
'book.gts': `
import { contains, field, Card } from 'https://cardstack.com/base/card-api';
import Person from './cards.gts';
export class Book extends Card {
@field author = contains(Person);
}
`,
'card-1.json': {
Expand All @@ -668,16 +682,16 @@ posts/ignore-me.gts
attributes: {
title: 'Card 1',
description: 'Sample post',
author: {
name: 'Cardy',
},
},
meta: {
adoptsFrom: {
module: 'https://cardstack.com/base/card-api',
name: 'Card',
},
author: { name: 'Cardy' },
},
meta: { adoptsFrom: { module: `./cards`, name: 'Article' } },
},
},
'card-2.json': {
data: {
type: 'card',
attributes: { author: { name: 'Cardy' } },
meta: { adoptsFrom: { module: `./book.gts`, name: 'Book' } },
},
},
'cards/1.json': {
Expand All @@ -686,17 +700,12 @@ posts/ignore-me.gts
attributes: {
title: 'Card 1',
description: 'Sample post',
author: {
name: 'Carl Stack',
},
author: { name: 'Carl Stack' },
createdAt: new Date(2022, 7, 1),
views: 10,
},
meta: {
adoptsFrom: {
module: `${paths.url}/Post`,
name: 'Card',
},
adoptsFrom: { module: `${paths.url}cards`, name: 'Post' },
},
},
},
Expand All @@ -714,10 +723,7 @@ posts/ignore-me.gts
views: 5,
},
meta: {
adoptsFrom: {
module: `${paths.url}/Post`,
name: 'Card',
},
adoptsFrom: { module: `${paths.url}cards`, name: 'Article' },
},
},
},
Expand All @@ -734,33 +740,23 @@ posts/ignore-me.gts
test(`can search for cards by using the 'eq' filter`, async function (assert) {
let matching = await indexer.search({
filter: {
eq: {
title: 'Card 1',
description: 'Sample post',
},
on: { module: `${paths.url}cards`, name: 'Post' },
eq: { title: 'Card 1', description: 'Sample post' },
},
});
assert.strictEqual(matching.length, 2, 'found two cards');
assert.strictEqual(matching[0]?.id, `${testRealmURL}card-1`);
assert.strictEqual(matching[1]?.id, `${testRealmURL}cards/1`);
assert.deepEqual(
matching.map((m) => m.id),
[`${paths.url}card-1`, `${paths.url}cards/1`]
);
});

test('can combine multiple filters', async function (assert) {
let matching = await indexer.search({
filter: {
every: [
{
eq: {
title: 'Card 1',
},
},
{
not: {
eq: {
'author.name': 'Cardy',
},
},
},
{ type: { module: `${paths.url}cards`, name: 'Post' } },
{ eq: { title: 'Card 1' } },
{ not: { eq: { 'author.name': 'Cardy' } } },
],
},
});
Expand All @@ -769,60 +765,90 @@ posts/ignore-me.gts
});

test('can handle a filter with double negatives', async function (assert) {
// note: do we allow this?
let matching = await indexer.search({
filter: {
not: {
not: {
not: {
eq: {
'author.email': '[email protected]',
},
},
},
},
on: { module: `${paths.url}cards`, name: 'Post' },
not: { not: { not: { eq: { 'author.email': '[email protected]' } } } },
},
});
assert.strictEqual(matching.length, 2);
assert.strictEqual(matching[0]?.id, `${testRealmURL}card-1`);
assert.strictEqual(matching[1]?.id, `${testRealmURL}cards/1`);
assert.deepEqual(
matching.map((m) => m.id),
[`${paths.url}card-1`, `${paths.url}cards/1`]
);
});

test('can filter by card type', async function (assert) {
let matching = await indexer.search({
filter: { type: { module: `${paths.url}cards`, name: 'Article' } },
});
assert.deepEqual(
matching.map((m) => m.id),
[`${paths.url}card-1`, `${paths.url}cards/2`],
'found cards of type Article'
);

matching = await indexer.search({
filter: { type: { module: `${testRealmURL}cards`, name: 'Post' } },
});
assert.deepEqual(
matching.map((m) => m.id),
[`${paths.url}card-1`, `${paths.url}cards/1`, `${paths.url}cards/2`],
'found cards of type Post'
);
});

// Tests from hub/**/**/card-service-test.ts
skip('can filter by card type');
skip(`can filter on a card's own fields using gt`);
skip(`gives a good error when query refers to missing card`);
skip(`gives a good error when query refers to missing field`);

test(`can filter on a nested field using 'eq'`, async function (assert) {
let matching = await indexer.search({
filter: {
eq: {
'author.name': 'Carl Stack',
},
on: { module: `${paths.url}cards`, name: 'Post' },
eq: { 'author.name': 'Carl Stack' },
},
});
assert.strictEqual(matching.length, 2);
assert.strictEqual(matching[0]?.id, `${testRealmURL}cards/1`);
assert.strictEqual(matching[1]?.id, `${testRealmURL}cards/2`);
assert.deepEqual(
matching.map((m) => m.id),
[`${paths.url}cards/1`, `${paths.url}cards/2`]
);
});

test('can negate a filter', async function (assert) {
let matching = await indexer.search({
filter: {
not: {
eq: {
'author.email': '[email protected]',
},
},
every: [
{ type: { module: `${paths.url}cards`, name: 'Article' } },
{ not: { eq: { 'author.email': '[email protected]' } } },
],
},
});
assert.strictEqual(matching.length, 2);
assert.strictEqual(matching.length, 1);
assert.strictEqual(matching[0]?.id, `${testRealmURL}card-1`);
assert.strictEqual(matching[1]?.id, `${testRealmURL}cards/1`);
});

skip('can combine multiple types');
test('can combine multiple types', async function (assert) {
let matching = await indexer.search({
filter: {
any: [
{
on: { module: `${paths.url}cards`, name: 'Article' },
eq: { 'author.name': 'Cardy' },
},
{
on: { module: `${paths.url}book`, name: 'Book' },
eq: { 'author.name': 'Cardy' },
},
],
},
});
assert.deepEqual(
matching.map((m) => m.id),
[`${paths.url}card-1`, `${paths.url}card-2`]
);
});

// sorting
skip('can sort in alphabetical order');
skip('can sort in reverse alphabetical order');
skip('can sort in multiple string field conditions');
Expand Down
21 changes: 12 additions & 9 deletions runtime-common/query.ts
Original file line number Diff line number Diff line change
Expand Up @@ -2,6 +2,7 @@ import * as JSON from "json-typescript";
import isEqual from "lodash/isEqual";
import { assertJSONValue, assertJSONPrimitive } from "./json-validation";
import qs from "qs";
import { ExportedCardRef } from "@cardstack/runtime-common/search-index";

export interface Query {
filter?: Filter;
Expand All @@ -20,7 +21,7 @@ export type Filter =
| CardTypeFilter;

export interface TypedFilter {
on?: CardURL;
on?: ExportedCardRef;
}

interface SortExpression {
Expand All @@ -35,7 +36,7 @@ export type Sort = SortExpression[];
// adopt from some particular card type--no other predicates are included in
// this filter.
export interface CardTypeFilter {
type: CardURL;
type: ExportedCardRef;
}

export interface AnyFilter extends TypedFilter {
Expand Down Expand Up @@ -154,14 +155,14 @@ function assertFilter(
}

if ("type" in filter) {
assertCardId(filter.type, pointer.concat("type"));
assertCardType(filter.type, pointer.concat("type"));
if (isEqual(Object.keys(filter), ["type"])) {
return; // This is a pure card type filter
}
}

if ("on" in filter) {
assertCardId(filter.on, pointer.concat("on"));
assertCardType(filter.on, pointer.concat("on"));
}

if ("any" in filter) {
Expand All @@ -181,11 +182,13 @@ function assertFilter(
}
}

function assertCardId(id: any, pointer: string[]): asserts id is CardURL {
if (typeof id !== "string") {
throw new Error(
`${pointer.join("/") || "/"}: card id must be a string URL`
);
function assertCardType(type: any, pointer: string[]) {
if (
Object.keys(type).length > 2 ||
!("module" in type) ||
!("name" in type)
) {
throw new Error(`${pointer.join("/") || "/"}: type is not valid`);
}
}

Expand Down
Loading

0 comments on commit 1be2628

Please sign in to comment.