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 15, 2024
1 parent dbc825a commit 461e52a
Show file tree
Hide file tree
Showing 18 changed files with 122 additions and 79 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 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
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
3 changes: 2 additions & 1 deletion packages/endpoint/src/schema.d.ts
Original file line number Diff line number Diff line change
Expand Up @@ -29,14 +29,15 @@ import {
PKClass,
} from './schemas/EntitySchema.js';
import { default as Invalidate } from './schemas/Invalidate.js';
import { default as Query } from './schemas/Query.js';
import type {
CollectionConstructor,
SchemaAttributeFunction,
SchemaFunction,
UnionResult,
} from './schemaTypes.js';

export { EntityMap, Invalidate };
export { EntityMap, Invalidate, Query };

export type { SchemaClass };

Expand Down
1 change: 1 addition & 0 deletions packages/endpoint/src/schema.js
Original file line number Diff line number Diff line change
Expand Up @@ -7,3 +7,4 @@ export { default as Object } from './schemas/Object.js';
export { default as Invalidate } from './schemas/Invalidate.js';
export { default as Collection } from './schemas/Collection.js';
export { default as Entity } from './schemas/EntitySchema.js';
export { default as Query } from './schemas/Query.js';
58 changes: 58 additions & 0 deletions packages/endpoint/src/schemas/Query.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,58 @@
import type {
EntityTable,
NormalizedIndex,
Queryable,
SchemaSimple,
} from '../interface.js';
import type { Denormalize, SchemaArgs } from '../normal.js';

/**
* Programmatic cache reading
* @see https://dataclient.io/rest/api/Query
*/
export default class Query<
S extends Queryable,
P extends readonly unknown[] = SchemaArgs<S>,
R = Denormalize<S>,
> implements SchemaSimple<R | undefined, P>
{
declare schema: S;
declare process: (entries: Denormalize<S>, ...args: P) => R;

constructor(schema: S, process?: (entries: Denormalize<S>, ...args: P) => R) {
this.schema = schema;
if (process) this.process = process;
// allows for inheritance overrides
else if (!this.process)
this.process = ((entries: Denormalize<S>) => entries) as any;
}

normalize(...args: any) {
return (this.schema as any).normalize(...args);
}

denormalize(input: {}, args: P, unvisit: any): R | undefined {
const value = unvisit(input, this.schema);
return typeof value === 'symbol' ? undefined : this.process(value, ...args);
}

infer(
args: P,
indexes: any,
recurse: (
schema: any,
args: any,
indexes: NormalizedIndex,
entities: EntityTable,
) => any,
entities: EntityTable,
) {
return recurse(this.schema, args, indexes, entities);
}

declare _denormalizeNullable: (
input: {},
args: readonly any[],
unvisit: (input: any, schema: any) => any,
) => R | undefined;
}
1 change: 0 additions & 1 deletion packages/endpoint/src/schemas/__tests__/Collection.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -88,7 +88,6 @@ describe(`${schema.Collection.name} normalization`, () => {
() => undefined,
{},
{},
// @ts-expect-error
[],
);
}
Expand Down
67 changes: 28 additions & 39 deletions packages/endpoint/src/schemas/__tests__/Query.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -4,7 +4,7 @@ import { IDEntity } from '__tests__/new';
import { fromJS } from 'immutable';

import { denormalizeSimple } from './denormalize';
import { schema, Query, Denormalize, DenormalizeNullable } from '../..';
import { schema, DenormalizeNullable } from '../..';

let dateSpy: jest.SpyInstance<number, []>;
beforeAll(() => {
Expand Down Expand Up @@ -43,9 +43,9 @@ describe.each([
}

describe.each(SCHEMA_CASES)(
`${Query.name} denormalization (%s schema)`,
`${schema.Query.name} denormalization (%s schema)`,
(_, usersSchema) => {
const sortedUsers = new Query(
const sortedUsers = new schema.Query(
usersSchema,
({ results }, { asc } = { asc: false }) => {
if (!results) return results;
Expand All @@ -71,10 +71,10 @@ describe.each([
],
},
};
const users: DenormalizeNullable<typeof sortedUsers.schema> | symbol =
const users: DenormalizeNullable<typeof sortedUsers> | symbol =
denormalize(
inferResults(sortedUsers.schema, [], {}, entities),
sortedUsers.schema,
inferResults(sortedUsers, [], {}, entities),
sortedUsers,
createInput(entities),
);
expect(users).not.toEqual(expect.any(Symbol));
Expand All @@ -99,8 +99,8 @@ describe.each([
};
expect(
denormalize(
inferResults(sortedUsers.schema, [{ asc: true }], {}, entities),
sortedUsers.schema,
inferResults(sortedUsers, [{ asc: true }], {}, entities),
sortedUsers,
createInput(entities),
{},
new WeakEntityMap(),
Expand All @@ -116,19 +116,19 @@ describe.each([
2: { id: '2', name: 'Jake' },
},
};
const input = inferResults(sortedUsers.schema, [], {}, entities);
const input = inferResults(sortedUsers, [], {}, entities);

const value = denormalize(
createInput(input),
sortedUsers.schema,
sortedUsers,
createInput(entities),
);

expect(createOutput(value)).toEqual(undefined);
});

test('denormalize aggregates', () => {
const userCountByAdmin = new Query(
const userCountByAdmin = new schema.Query(
usersSchema,
({ results }, { isAdmin }: { isAdmin?: boolean } = {}) => {
if (isAdmin === undefined) return results.length;
Expand All @@ -155,39 +155,29 @@ describe.each([
},
};
const totalCount:
| DenormalizeNullable<typeof userCountByAdmin.schema>
| DenormalizeNullable<typeof userCountByAdmin>
| symbol = denormalize(
inferResults(userCountByAdmin.schema, [], {}, entities),
userCountByAdmin.schema,
inferResults(userCountByAdmin, [], {}, entities),
userCountByAdmin,
createInput(entities),
);
expect(totalCount).toBe(4);
const nonAdminCount:
| DenormalizeNullable<typeof userCountByAdmin.schema>
| DenormalizeNullable<typeof userCountByAdmin>
| symbol = denormalize(
inferResults(
userCountByAdmin.schema,
[{ isAdmin: false }],
{},
entities,
),
userCountByAdmin.schema,
inferResults(userCountByAdmin, [{ isAdmin: false }], {}, entities),
userCountByAdmin,
createInput(entities),
{},
new WeakEntityMap(),
[{ isAdmin: false }],
);
expect(nonAdminCount).toBe(3);
const adminCount:
| DenormalizeNullable<typeof userCountByAdmin.schema>
| DenormalizeNullable<typeof userCountByAdmin>
| symbol = denormalize(
inferResults(
userCountByAdmin.schema,
[{ isAdmin: true }],
{},
entities,
),
userCountByAdmin.schema,
inferResults(userCountByAdmin, [{ isAdmin: true }], {}, entities),
userCountByAdmin,
createInput(entities),
{},
new WeakEntityMap(),
Expand All @@ -206,7 +196,7 @@ describe.each([
});

describe('top level schema', () => {
const sortedUsers = new Query(
const sortedUsers = new schema.Query(
new schema.Collection([User]),
(results, { asc } = { asc: false }) => {
if (!results) return results;
Expand All @@ -228,12 +218,11 @@ describe('top level schema', () => {
[new schema.Collection([User]).pk({}, undefined, '', [])]: [1, 2, 3, 4],
},
};
const users: DenormalizeNullable<typeof sortedUsers.schema> | symbol =
denormalize(
inferResults(sortedUsers.schema, [], {}, entities),
sortedUsers.schema,
entities,
);
const users: DenormalizeNullable<typeof sortedUsers> | symbol = denormalize(
inferResults(sortedUsers, [], {}, entities),
sortedUsers,
entities,
);
expect(users).not.toEqual(expect.any(Symbol));
if (typeof users === 'symbol') return;
expect(users && users[0].name).toBe('Zeta');
Expand All @@ -247,9 +236,9 @@ describe('top level schema', () => {
2: { id: '2', name: 'Jake' },
},
};
const input = inferResults(sortedUsers.schema, [], {}, entities);
const input = inferResults(sortedUsers, [], {}, entities);

const value = denormalize(input, sortedUsers.schema, entities);
const value = denormalize(input, sortedUsers, entities);

expect(value).toEqual(undefined);
});
Expand Down
Loading

1 comment on commit 461e52a

@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: 461e52a Previous: 5771da3 Ratio
normalizeLong 430 ops/sec (±2.33%) 447 ops/sec (±1.68%) 1.04
infer All 9805 ops/sec (±0.41%) 9629 ops/sec (±1.67%) 0.98
denormalizeLong 319 ops/sec (±2.54%) 321 ops/sec (±2.22%) 1.01
denormalizeLong donotcache 833 ops/sec (±1.68%) 888 ops/sec (±0.46%) 1.07
denormalizeShort donotcache 500x 1349 ops/sec (±0.37%) 1351 ops/sec (±0.13%) 1.00
denormalizeShort 500x 934 ops/sec (±0.23%) 958 ops/sec (±0.24%) 1.03
denormalizeLong with mixin Entity 304 ops/sec (±0.30%) 303 ops/sec (±0.26%) 1.00
denormalizeLong withCache 7221 ops/sec (±0.21%) 6836 ops/sec (±0.26%) 0.95
denormalizeLongAndShort withEntityCacheOnly 1604 ops/sec (±1.79%) 1562 ops/sec (±0.31%) 0.97
denormalizeLong All withCache 6383 ops/sec (±0.19%) 6337 ops/sec (±0.17%) 0.99
denormalizeLong Query-sorted withCache 6597 ops/sec (±0.34%) 6647 ops/sec (±0.38%) 1.01
getResponse 4826 ops/sec (±0.59%) 4951 ops/sec (±0.88%) 1.03
getResponse (null) 3038083 ops/sec (±0.22%) 2888601 ops/sec (±0.21%) 0.95
getResponse (clear cache) 301 ops/sec (±0.26%) 292 ops/sec (±1.10%) 0.97
getSmallResponse 2258 ops/sec (±0.90%) 2328 ops/sec (±0.30%) 1.03
getSmallInferredResponse 1794 ops/sec (±0.42%) 1765 ops/sec (±0.34%) 0.98
getResponse Query-sorted 761 ops/sec (±1.63%) 687 ops/sec (±1.09%) 0.90
getResponse Collection 4650 ops/sec (±0.54%) 5124 ops/sec (±1.06%) 1.10
setLong 435 ops/sec (±1.99%) 437 ops/sec (±2.25%) 1.00
setLongWithMerge 189 ops/sec (±0.42%) 189 ops/sec (±0.36%) 1
setLongWithSimpleMerge 202 ops/sec (±0.27%) 202 ops/sec (±0.27%) 1

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

Please sign in to comment.