Skip to content

Commit

Permalink
feat: Allow practical combination of Query with any schema (#2837)
Browse files Browse the repository at this point in the history
  • Loading branch information
ntucker authored Nov 8, 2023
1 parent 005c9de commit 57d87d6
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

0 comments on commit 57d87d6

Please sign in to comment.