Skip to content

Commit

Permalink
update query work
Browse files Browse the repository at this point in the history
  • Loading branch information
ntucker committed Nov 8, 2023
1 parent 005c9de commit de19d0f
Show file tree
Hide file tree
Showing 13 changed files with 293 additions and 63 deletions.
15 changes: 15 additions & 0 deletions .changeset/new-gifts-hammer.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,15 @@
---
'@data-client/endpoint': minor
'@data-client/rest': minor
---

Query works with any Schema - including Collections

```ts
export const queryRemainingTodos = new Query(
TodoResource.getList.schema,
(entries) => entries && entries.filter((todo) => !todo.completed).length,
);
```

BREAKING CHANGE: Query.schema internals are laid out differently
9 changes: 2 additions & 7 deletions examples/todo-app/src/resources/TodoResource.ts
Original file line number Diff line number Diff line change
Expand Up @@ -21,11 +21,6 @@ export const TodoResource = createPlaceholderResource({
});

export const queryRemainingTodos = new Query(
new schema.All(Todo),
(entries, { userId } = {}) => {
if (userId !== undefined)
return entries.filter((todo) => todo.userId === userId && !todo.completed)
.length;
return entries.filter((todo) => !todo.completed).length;
},
TodoResource.getList.schema,
(entries) => entries && entries.filter((todo) => !todo.completed).length,
);
1 change: 1 addition & 0 deletions packages/endpoint/src/index.ts
Original file line number Diff line number Diff line change
Expand Up @@ -34,6 +34,7 @@ export type {
NormalizeNullable,
Denormalize,
DenormalizeNullable,
SchemaToArgs,
} from './normal.js';
export type {
EndpointExtraOptions,
Expand Down
4 changes: 2 additions & 2 deletions packages/endpoint/src/interface.ts
Original file line number Diff line number Diff line change
Expand Up @@ -14,7 +14,7 @@ export type Serializable<
T extends { toJSON(): string } = { toJSON(): string },
> = (value: any) => T;

export interface SchemaSimple<T = any> {
export interface SchemaSimple<T = any, Args extends any[] = any[]> {
normalize(
input: any,
parent: any,
Expand All @@ -23,7 +23,7 @@ export interface SchemaSimple<T = any> {
addEntity: (...args: any) => any,
visitedEntities: Record<string, any>,
storeEntities: any,
args?: any[],
args: Args,
): any;
denormalize(
input: {},
Expand Down
28 changes: 28 additions & 0 deletions packages/endpoint/src/normal.ts
Original file line number Diff line number Diff line change
Expand Up @@ -131,3 +131,31 @@ export type NormalizedSchema<E, R> = {
export interface EntityMap<T = any> {
readonly [k: string]: EntityInterface<T>;
}

export type SchemaToArgs<
S extends {
normalize(
input: any,
parent: any,
key: any,
visit: (...args: any) => any,
addEntity: (...args: any) => any,
visitedEntities: Record<string, any>,
storeEntities: any,
args: any,
): any;
},
> = S extends {
normalize(
input: any,
parent: any,
key: any,
visit: (...args: any) => any,
addEntity: (...args: any) => any,
visitedEntities: Record<string, any>,
storeEntities: any,
args: infer Args,
): any;
}
? Args
: never;
19 changes: 7 additions & 12 deletions packages/endpoint/src/queryEndpoint.ts
Original file line number Diff line number Diff line change
Expand Up @@ -3,23 +3,21 @@ import type {
NormalizedIndex,
SchemaSimple,
} from './interface.js';
import type { Denormalize } from './normal.js';
import type { Denormalize, SchemaToArgs } from './normal.js';

/**
* Programmatic cache reading
* @see https://dataclient.io/rest/api/Query
*/
export class Query<
S extends SchemaSimple,
P extends any[] = [],
P extends SchemaToArgs<S> = SchemaToArgs<S>,
R = Denormalize<S>,
> {
declare schema: QuerySchema<S, R>;
// TODO: allow arbitrary return types then inferring it from
declare process: (entries: Denormalize<S>, ...args: P) => R;

readonly sideEffect = undefined;

constructor(schema: S, process?: (entries: Denormalize<S>, ...args: P) => R) {
this.schema = this.createQuerySchema(schema);
if (process) this.process = process;
Expand All @@ -34,17 +32,14 @@ export class Query<

protected createQuerySchema(schema: SchemaSimple) {
const query = Object.create(schema);
query.denormalize = (
{ args, input }: { args: P; input: any },
_: P,
unvisit: any,
) => {
if (input === undefined) return undefined;
const value = (schema as any).denormalize(input, args, unvisit);
query.denormalize = (input: any, args: P, unvisit: any) => {
const value = unvisit(input, schema);
return typeof value === 'symbol'
? undefined
: this.process(value, ...args);
};
// do not look like an entity
query.pk = undefined;
query.infer = (
args: any,
indexes: any,
Expand All @@ -56,7 +51,7 @@ export class Query<
) => any,
entities: EntityTable,
) => {
return { args, input: recurse(schema, args, indexes, entities) };
return recurse(schema, args, indexes, entities);
};
return query;
}
Expand Down
5 changes: 3 additions & 2 deletions packages/endpoint/src/schema.d.ts
Original file line number Diff line number Diff line change
Expand Up @@ -317,11 +317,12 @@ export declare let CollectionRoot: CollectionConstructor;
*/
export declare class Collection<
S extends any[] | PolymorphicInterface = any,
Parent extends any[] = [
Args extends any[] = [
urlParams: Record<string, any>,
body?: Record<string, any>,
],
> extends CollectionRoot<S, Parent> {}
Parent = any,
> extends CollectionRoot<S, Args, Parent> {}

// id is in Instance, so we default to that as pk
/**
Expand Down
25 changes: 16 additions & 9 deletions packages/endpoint/src/schemaTypes.ts
Original file line number Diff line number Diff line change
Expand Up @@ -24,9 +24,10 @@ export type CollectionArrayAdder<S extends PolymorphicInterface> = S extends {

export interface CollectionInterface<
S extends PolymorphicInterface = any,
Parent extends any[] = any,
Args extends any[] = any[],
Parent = any,
> {
addWith<P extends any[] = Parent>(
addWith<P extends any[] = Args>(
merge: (existing: any, incoming: any) => any,
createCollectionFilter?: (
...args: P
Expand All @@ -46,7 +47,7 @@ export interface CollectionInterface<
addEntity: (...args: any) => any,
visitedEntities: Record<string, any>,
storeEntities: any,
args: any[],
args: Args,
): string;

merge(existing: any, incoming: any): any;
Expand Down Expand Up @@ -126,28 +127,34 @@ export interface CollectionInterface<
* @see https://dataclient.io/rest/api/Collection#assign
*/
assign: S extends { denormalize(...args: any): Record<string, unknown> }
? schema.Collection<S, Parent>
? schema.Collection<S, Args, Parent>
: never;
}
export type CollectionFromSchema<
S extends any[] | PolymorphicInterface = any,
Parent extends any[] = [
Args extends any[] = [
urlParams: Record<string, any>,
body?: Record<string, any>,
],
> = CollectionInterface<S extends any[] ? schema.Array<S[number]> : S, Parent>;
Parent = any,
> = CollectionInterface<
S extends any[] ? schema.Array<S[number]> : S,
Args,
Parent
>;

export interface CollectionConstructor {
new <
S extends SchemaSimple[] | PolymorphicInterface = any,
Parent extends any[] = [
Args extends any[] = [
urlParams: Record<string, any>,
body?: Record<string, any>,
],
Parent = any,
>(
schema: S,
options?: CollectionOptions,
): CollectionFromSchema<S, Parent>;
options?: CollectionOptions<Args, Parent>,
): CollectionFromSchema<S, Args, Parent>;
readonly prototype: CollectionInterface;
}

Expand Down
30 changes: 16 additions & 14 deletions packages/endpoint/src/schemas/Collection.ts
Original file line number Diff line number Diff line change
Expand Up @@ -21,10 +21,11 @@ const createValue = (value: any) => ({ ...value });
*/
export default class CollectionSchema<
S extends PolymorphicInterface = any,
Parent extends any[] = [
Args extends any[] = [
urlParams: Record<string, any>,
body?: Record<string, any>,
],
Parent = any,
> {
protected declare nestKey: (parent: any, key: string) => Record<string, any>;

Expand All @@ -35,18 +36,18 @@ export default class CollectionSchema<
declare readonly key: string;

declare push: S extends ArraySchema<any>
? CollectionSchema<S, Parent>
? CollectionSchema<S, Args, Parent>
: undefined;

declare unshift: S extends ArraySchema<any>
? CollectionSchema<S, Parent>
? CollectionSchema<S, Args, Parent>
: undefined;

declare assign: S extends Values<any>
? CollectionSchema<S, Parent>
? CollectionSchema<S, Args, Parent>
: undefined;

addWith<P extends any[] = Parent>(
addWith<P extends any[] = Args>(
merge: (existing: any, incoming: any) => any,
createCollectionFilter?: (
...args: P
Expand All @@ -59,7 +60,7 @@ export default class CollectionSchema<
// so fetch(create, { userId: 'bob', completed: true }, data)
// would possibly add to {}, {userId: 'bob'}, {completed: true}, {userId: 'bob', completed: true } - but only those already in the store
// it ignores keys that start with sort as those are presumed to not filter results
protected createCollectionFilter(...args: Parent) {
protected createCollectionFilter(...args: Args) {
return (collectionKey: Record<string, string>) =>
Object.entries(collectionKey).every(
([key, value]) =>
Expand All @@ -74,7 +75,7 @@ export default class CollectionSchema<
return key.startsWith('order');
}

constructor(schema: S, options?: CollectionOptions) {
constructor(schema: S, options?: CollectionOptions<Args, Parent>) {
this.schema = Array.isArray(schema)
? (new ArraySchema(schema[0]) as any)
: schema;
Expand Down Expand Up @@ -109,7 +110,7 @@ export default class CollectionSchema<
this.createCollectionFilter = (
options as any as {
createCollectionFilter: (
...args: Parent
...args: Args
) => (collectionKey: Record<string, string>) => boolean;
}
).createCollectionFilter.bind(this) as any;
Expand Down Expand Up @@ -152,13 +153,13 @@ export default class CollectionSchema<

normalize(
input: any,
parent: any,
parent: Parent,
key: string,
visit: (...args: any) => any,
addEntity: (...args: any) => any,
visitedEntities: Record<string, any>,
storeEntities: any,
args: any[],
args: Args,
): string {
const pkList = this.schema.normalize(
input,
Expand Down Expand Up @@ -239,22 +240,23 @@ export default class CollectionSchema<
}

export type CollectionOptions<
Parent extends any[] = [
Args extends any[] = [
urlParams: Record<string, any>,
body?: Record<string, any>,
],
Parent = any,
> = (
| {
nestKey?: (parent: any, key: string) => Record<string, any>;
nestKey?: (parent: Parent, key: string) => Record<string, any>;
}
| {
argsKey?: (...args: any) => Record<string, any>;
argsKey?: (...args: Args) => Record<string, any>;
}
) &
(
| {
createCollectionFilter?: (
...args: Parent
...args: Args
) => (collectionKey: Record<string, string>) => boolean;
}
| {
Expand Down
1 change: 1 addition & 0 deletions packages/endpoint/src/schemas/__tests__/Collection.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -88,6 +88,7 @@ describe(`${schema.Collection.name} normalization`, () => {
() => undefined,
{},
{},
// @ts-expect-error
[],
);
}
Expand Down
Loading

1 comment on commit de19d0f

@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: de19d0f Previous: a6202fc Ratio
normalizeLong 217 ops/sec (±2.41%) 271 ops/sec (±0.41%) 1.25
infer All 4607 ops/sec (±0.38%) 5424 ops/sec (±0.44%) 1.18
denormalizeLong 156 ops/sec (±0.79%) 176 ops/sec (±0.84%) 1.13
denormalizeLong donotcache 397 ops/sec (±0.24%) 460 ops/sec (±0.19%) 1.16
denormalizeShort donotcache 500x 684 ops/sec (±2.00%) 798 ops/sec (±3.14%) 1.17
denormalizeShort 500x 507 ops/sec (±1.58%) 591 ops/sec (±0.14%) 1.17
denormalizeLong with mixin Entity 154 ops/sec (±0.85%) 169 ops/sec (±0.87%) 1.10
denormalizeLong withCache 3185 ops/sec (±0.11%) 3616 ops/sec (±0.14%) 1.14
denormalizeLongAndShort withEntityCacheOnly 792 ops/sec (±0.48%) 866 ops/sec (±0.36%) 1.09
denormalizeLong All withCache 4228 ops/sec (±0.27%) 4821 ops/sec (±0.18%) 1.14
denormalizeLong Query-sorted withCache 3824 ops/sec (±0.43%) 4560 ops/sec (±0.30%) 1.19
getResponse 2916 ops/sec (±2.27%) 3587 ops/sec (±2.70%) 1.23
getResponse (null) 1875235 ops/sec (±0.09%) 2228883 ops/sec (±0.13%) 1.19
getResponse (clear cache) 147 ops/sec (±0.42%) 158 ops/sec (±1.59%) 1.07
getSmallResponse 1451 ops/sec (±0.13%) 1613 ops/sec (±0.38%) 1.11
getSmallInferredResponse 1230 ops/sec (±3.34%) 1364 ops/sec (±0.24%) 1.11
getResponse Query-sorted 365 ops/sec (±1.94%) 400 ops/sec (±1.59%) 1.10
setLong 215 ops/sec (±3.11%) 266 ops/sec (±0.62%) 1.24
setLongWithMerge 98.86 ops/sec (±0.58%) 114 ops/sec (±0.51%) 1.15
setLongWithSimpleMerge 110 ops/sec (±0.55%) 127 ops/sec (±0.64%) 1.15

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

Please sign in to comment.