Skip to content

Commit

Permalink
feat: Add schema.Query
Browse files Browse the repository at this point in the history
  • Loading branch information
ntucker committed Feb 16, 2024
1 parent dbc825a commit 48e27c5
Show file tree
Hide file tree
Showing 28 changed files with 436 additions and 110 deletions.
2 changes: 1 addition & 1 deletion examples/benchmark/core.js
Original file line number Diff line number Diff line change
Expand Up @@ -137,7 +137,7 @@ export default function addReducerSuite(suite) {
}
})
.add('getResponse Query-sorted', () => {
return controller.getResponse(ProjectQuerySorted, cachedState);
return controller.query(ProjectQuerySorted, cachedState);
})
.add('getResponse Collection', () => {
return controllerCollection.getResponse(
Expand Down
4 changes: 2 additions & 2 deletions examples/benchmark/schemas.js
Original file line number Diff line number Diff line change
@@ -1,4 +1,4 @@
import { Entity, schema, Query } from './dist/index.js';
import { Entity, schema } from './dist/index.js';

export class BuildTypeDescription extends Entity {
id = '';
Expand Down Expand Up @@ -100,7 +100,7 @@ export const ProjectSchemaMixin = {
export const ProjectQuery = {
project: new schema.All(ProjectWithBuildTypesDescription),
};
export const ProjectQuerySorted = new Query(
export const ProjectQuerySorted = new schema.Query(
new schema.All(ProjectWithBuildTypesDescription),
entries => {
return [...entries].sort((a, b) => a.internalId - b.internalId);
Expand Down
4 changes: 2 additions & 2 deletions examples/coin-app/src/resources/Currency.ts
Original file line number Diff line number Diff line change
@@ -1,4 +1,4 @@
import { Entity, Query, createResource, schema } from '@data-client/rest';
import { Entity, createResource, schema } from '@data-client/rest';

import { Stats } from './Stats';

Expand Down Expand Up @@ -61,7 +61,7 @@ export const CurrencyResource = createResource({
interface Args {
type?: string;
}
export const queryCurrency = new Query(
export const queryCurrency = new schema.Query(
new schema.All(Currency),
(entries, { type = 'crypto' }: Args) => {
let sorted = entries.filter(
Expand Down
4 changes: 2 additions & 2 deletions examples/coin-app/src/resources/Product.ts
Original file line number Diff line number Diff line change
@@ -1,4 +1,4 @@
import { Entity, Query, createResource, schema } from '@data-client/rest';
import { Entity, createResource, schema } from '@data-client/rest';

import { Stats } from './Stats';

Expand Down Expand Up @@ -34,7 +34,7 @@ export const ProductResource = createResource({
interface Args {
quote_currency?: string;
}
export const queryProduct = new Query(
export const queryProduct = new schema.Query(
new schema.All(Product),
(entries, { quote_currency }: Args) => {
let sorted = entries.filter(product => product.stats);
Expand Down
4 changes: 2 additions & 2 deletions examples/nextjs/resources/TodoResource.ts
Original file line number Diff line number Diff line change
@@ -1,4 +1,4 @@
import { Query, schema } from '@data-client/rest';
import { schema } from '@data-client/rest';

import {
createPlaceholderResource,
Expand All @@ -20,7 +20,7 @@ export const TodoResource = createPlaceholderResource({
searchParams: {} as { userId?: string | number } | undefined,
});

export const queryRemainingTodos = new Query(
export const queryRemainingTodos = new schema.Query(
TodoResource.getList.schema,
(entries) => entries && entries.filter((todo) => !todo.completed).length,
);
4 changes: 2 additions & 2 deletions examples/todo-app/src/pages/Home/TodoStats.tsx
Original file line number Diff line number Diff line change
@@ -1,8 +1,8 @@
import { useCache } from '@data-client/react';
import { useQuery } from '@data-client/react';
import { queryRemainingTodos } from 'resources/TodoResource';

export default function TodoStats({ userId }: { userId?: number }) {
const remaining = useCache(queryRemainingTodos, { userId });
const remaining = useQuery(queryRemainingTodos, { userId });

return <div>{remaining} tasks remaining</div>;
}
4 changes: 2 additions & 2 deletions examples/todo-app/src/resources/TodoResource.ts
Original file line number Diff line number Diff line change
@@ -1,4 +1,4 @@
import { Query, schema } from '@data-client/rest';
import { schema } from '@data-client/rest';

import {
createPlaceholderResource,
Expand All @@ -20,7 +20,7 @@ export const TodoResource = createPlaceholderResource({
searchParams: {} as { userId?: string | number } | undefined,
});

export const queryRemainingTodos = new Query(
export const queryRemainingTodos = new schema.Query(
TodoResource.getList.schema,
(entries) => entries && entries.filter((todo) => !todo.completed).length,
);
4 changes: 2 additions & 2 deletions examples/todo-app/typetest.ts
Original file line number Diff line number Diff line change
@@ -1,4 +1,4 @@
import { useCache, useController, useSuspense } from '@data-client/react';
import { useQuery, useController, useSuspense } from '@data-client/react';

import {
queryRemainingTodos,
Expand All @@ -23,7 +23,7 @@ function useTest() {
);
});

const remaining = useCache(queryRemainingTodos, { userId: 1 });
const remaining = useQuery(queryRemainingTodos, { userId: 1 });

const users = useSuspense(UserResource.getList);
users.map((user) => {
Expand Down
4 changes: 2 additions & 2 deletions packages/core/src/controller/Controller.ts
Original file line number Diff line number Diff line change
Expand Up @@ -397,7 +397,7 @@ export default class Controller<
let results;
let resultCache: ResultCache;
if (cacheResults === undefined && endpoint.schema !== undefined) {
if (!this.globalCache.inputEndpointCache[key])
if (!this.globalCache.inputEndpointCache[key] || true)
this.globalCache.inputEndpointCache[key] = inferResults(
endpoint.schema,
args,
Expand Down Expand Up @@ -473,7 +473,7 @@ export default class Controller<
const querySchemaCache = this.globalCache.queries.get(schema) as {
[key: string]: unknown;
};
if (!querySchemaCache[key])
if (!querySchemaCache[key] || true)
querySchemaCache[key] = inferResults(
schema,
args,
Expand Down
2 changes: 0 additions & 2 deletions packages/core/src/controller/__tests__/Controller.ts
Original file line number Diff line number Diff line change
@@ -1,9 +1,7 @@
import { Endpoint, Entity } from '@data-client/endpoint';
import { normalize } from '@data-client/normalizr';
import { CoolerArticleResource } from '__tests__/new';
import { createEntityMeta } from '__tests__/utils';

import { ExpiryStatus } from '../..';
import { initialState } from '../../state/reducer/createReducer';
import Controller from '../Controller';

Expand Down
87 changes: 87 additions & 0 deletions packages/core/src/controller/__tests__/__snapshots__/query.ts.snap
Original file line number Diff line number Diff line change
@@ -0,0 +1,87 @@
// Jest Snapshot v1, https://goo.gl/fbAQLP

exports[`Controller.query() Query based on args 1`] = `
[
Tacos {
"id": "1",
"type": "foo",
},
]
`;

exports[`Controller.query() Query based on args 2`] = `
[
Tacos {
"id": "1",
"type": "foo",
},
Tacos {
"id": "2",
"type": "bar",
},
]
`;

exports[`Controller.query() Query+All based on args 1`] = `
[
Tacos {
"id": "1",
"type": "foo",
},
]
`;

exports[`Controller.query() Query+All based on args 2`] = `
[
Tacos {
"id": "1",
"type": "foo",
},
Tacos {
"id": "2",
"type": "bar",
},
]
`;

exports[`Controller.query() query All 1`] = `
[
Tacos {
"id": "1",
"type": "foo",
},
Tacos {
"id": "2",
"type": "bar",
},
]
`;

exports[`Controller.query() query Collection based on args 1`] = `
[
Tacos {
"id": "1",
"type": "foo",
},
]
`;

exports[`Controller.query() query Collection based on args 2`] = `
[
Tacos {
"id": "1",
"type": "foo",
},
Tacos {
"id": "2",
"type": "bar",
},
]
`;

exports[`Controller.query() query Entity based on pk 1`] = `
Tacos {
"id": "1",
"type": "foo",
}
`;
147 changes: 147 additions & 0 deletions packages/core/src/controller/__tests__/query.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,147 @@
import { Entity, schema } from '@data-client/endpoint';

import { initialState } from '../../state/reducer/createReducer';
import Contoller from '../Controller';

describe('Controller.query()', () => {
class Tacos extends Entity {
type = '';
id = '';
pk() {
return this.id;
}
}
const TacoList = new schema.Collection([Tacos]);
const entities = {
Tacos: {
1: { id: '1', type: 'foo' },
2: { id: '2', type: 'bar' },
},
[TacoList.key]: {
[TacoList.pk(undefined, undefined, '', [{ type: 'foo' }])]: ['1'],
[TacoList.pk(undefined, undefined, '', [{ type: 'bar' }])]: ['2'],
[TacoList.pk(undefined, undefined, '', [])]: ['1', '2'],
},
};

it('query Entity based on pk', () => {
const controller = new Contoller();
const state = {
...initialState,
entities,
};
const taco = controller.query(Tacos, { id: '1' }, state);
expect(taco).toBeDefined();
expect(taco).toBeInstanceOf(Tacos);
expect(taco).toMatchSnapshot();
const taco2 = controller.query(Tacos, { id: '2' }, state);
expect(taco2).toBeDefined();
expect(taco2).toBeInstanceOf(Tacos);
expect(taco2).not.toEqual(taco);
// should maintain referential equality
expect(taco).toBe(controller.query(Tacos, { id: '1' }, state));

// @ts-expect-error
() => controller.query(Tacos, { id: { bob: 5 } }, state);
// @ts-expect-error
expect(controller.query(Tacos, 5, state)).toBeUndefined();
// @ts-expect-error
() => controller.query(Tacos, { doesnotexist: 5 }, state);
});

it('query Collection based on args', () => {
const controller = new Contoller();
const state = {
...initialState,
entities,
};
const tacos = controller.query(TacoList, { type: 'foo' }, state);
expect(tacos).toBeDefined();
expect(tacos?.[0]).toBeInstanceOf(Tacos);
expect(tacos).toMatchSnapshot();
const tacosBars = controller.query(TacoList, { type: 'bar' }, state);
expect(tacosBars).toBeDefined();
expect(tacosBars?.[0]).toBeInstanceOf(Tacos);
expect(tacosBars).not.toEqual(tacos);
// should maintain referential equality
expect(tacos).toBe(controller.query(TacoList, { type: 'foo' }, state));

const allTacos = controller.query(TacoList, state);
expect(allTacos).toBeDefined();
expect(allTacos).toHaveLength(2);
expect(allTacos).toMatchSnapshot();

// @ts-expect-error
() => controller.query(TacoList, 5, state);
});

it('query All', () => {
const controller = new Contoller();
const state = {
...initialState,
entities,
};
const AllTacos = new schema.All(Tacos);

const allTacos = controller.query(AllTacos, state);
expect(allTacos).toBeDefined();
expect(allTacos).toHaveLength(2);
expect(allTacos).toMatchSnapshot();
// should maintain referential equality
// TODO: (add infer cache) expect(allTacos).toBe(controller.query(TacoList, state));

// TODO: @ts-expect-error (we have a hack to make this not break other things now)
() => controller.query(AllTacos, 5, state);
});

it('Query+All based on args', () => {
const controller = new Contoller();
const state = {
...initialState,
entities,
};
const QueryTacos = new schema.Query(
new schema.All(Tacos),
(tacos, { type }: { type?: string } = {}) => {
if (!type) return tacos;
return tacos.filter(taco => taco.type === type);
},
);

const tacos = controller.query(QueryTacos, { type: 'foo' }, state);
expect(tacos).toBeDefined();
expect(tacos?.[0]).toBeInstanceOf(Tacos);
expect(tacos).toMatchSnapshot();
const tacosBars = controller.query(QueryTacos, { type: 'bar' }, state);
expect(tacosBars).toBeDefined();
expect(tacosBars?.[0]).toBeInstanceOf(Tacos);
expect(tacosBars).not.toEqual(tacos);
// should maintain referential equality
// TODO: (add infer cache) expect(tacos).toBe(controller.query(QueryTacos, { type: 'foo' }, state));

const allTacos = controller.query(QueryTacos, state);
expect(allTacos).toBeDefined();
expect(allTacos).toHaveLength(2);
expect(allTacos).toMatchSnapshot();

// @ts-expect-error
() => controller.query(QueryTacos, 5, state);
});

it('Query+Collection based on args', () => {
const controller = new Contoller();
const state = {
...initialState,
entities,
};
const tacoCount = new schema.Query(TacoList, tacos => {
return tacos.length;
});

expect(controller.query(tacoCount, { type: 'foo' }, state)).toBe(1);
expect(controller.query(tacoCount, { type: 'bar' }, state)).toBe(1);
expect(controller.query(tacoCount, state)).toBe(2);
});

// TODO: test Union
});
1 change: 0 additions & 1 deletion packages/endpoint/src/index.ts
Original file line number Diff line number Diff line change
Expand Up @@ -50,5 +50,4 @@ export type {
export { default as Endpoint, ExtendableEndpoint } from './endpoint.js';
export type { KeyofEndpointInstance } from './endpoint.js';
export * from './indexEndpoint.js';
export * from './queryEndpoint.js';
export { default as AbortOptimistic } from './AbortOptimistic.js';
2 changes: 1 addition & 1 deletion packages/endpoint/src/interface.ts
Original file line number Diff line number Diff line change
Expand Up @@ -24,7 +24,7 @@ export type Serializable<
T extends { toJSON(): string } = { toJSON(): string },
> = (value: any) => T;

export interface SchemaSimple<T = any, Args extends any[] = any[]> {
export interface SchemaSimple<T = any, Args extends readonly any[] = any[]> {
normalize(
input: any,
parent: any,
Expand Down
4 changes: 2 additions & 2 deletions packages/endpoint/src/normal.ts
Original file line number Diff line number Diff line change
Expand Up @@ -119,8 +119,8 @@ export interface EntityMap<T = any> {
readonly [k: string]: EntityInterface<T>;
}

export type SchemaArgs<S extends Queryable> =
S extends EntityInterface<infer U> ? EntityFields<U>
export type SchemaArgs<S extends Schema | undefined> =
S extends EntityInterface<infer U> ? [EntityFields<U>]
: S extends (
{
infer(
Expand Down
Loading

1 comment on commit 48e27c5

@github-actions
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Benchmark

Benchmark suite Current: 48e27c5 Previous: 5771da3 Ratio
normalizeLong 441 ops/sec (±2.00%) 447 ops/sec (±1.68%) 1.01
infer All 9786 ops/sec (±1.01%) 9629 ops/sec (±1.67%) 0.98
denormalizeLong 314 ops/sec (±2.61%) 321 ops/sec (±2.22%) 1.02
denormalizeLong donotcache 843 ops/sec (±0.56%) 888 ops/sec (±0.46%) 1.05
denormalizeShort donotcache 500x 1353 ops/sec (±0.35%) 1351 ops/sec (±0.13%) 1.00
denormalizeShort 500x 947 ops/sec (±0.40%) 958 ops/sec (±0.24%) 1.01
denormalizeLong with mixin Entity 292 ops/sec (±0.46%) 303 ops/sec (±0.26%) 1.04
denormalizeLong withCache 7124 ops/sec (±0.32%) 6836 ops/sec (±0.26%) 0.96
denormalizeLongAndShort withEntityCacheOnly 1580 ops/sec (±0.70%) 1562 ops/sec (±0.31%) 0.99
denormalizeLong All withCache 5817 ops/sec (±0.19%) 6337 ops/sec (±0.17%) 1.09
denormalizeLong Query-sorted withCache 6238 ops/sec (±0.30%) 6647 ops/sec (±0.38%) 1.07
getResponse 5470 ops/sec (±1.60%) 4951 ops/sec (±0.88%) 0.91
getResponse (null) 2957637 ops/sec (±2.14%) 2888601 ops/sec (±0.21%) 0.98
getResponse (clear cache) 296 ops/sec (±0.56%) 292 ops/sec (±1.10%) 0.99
getSmallResponse 2235 ops/sec (±0.96%) 2328 ops/sec (±0.30%) 1.04
getSmallInferredResponse 1796 ops/sec (±0.32%) 1765 ops/sec (±0.34%) 0.98
getResponse Query-sorted 731 ops/sec (±1.63%) 687 ops/sec (±1.09%) 0.94
getResponse Collection 5229 ops/sec (±1.76%) 5124 ops/sec (±1.06%) 0.98
setLong 428 ops/sec (±2.13%) 437 ops/sec (±2.25%) 1.02
setLongWithMerge 186 ops/sec (±0.62%) 189 ops/sec (±0.36%) 1.02
setLongWithSimpleMerge 197 ops/sec (±0.62%) 202 ops/sec (±0.27%) 1.03

This comment was automatically generated by workflow using github-action-benchmark.

Please sign in to comment.