Skip to content
Closed
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
18 changes: 16 additions & 2 deletions packages/tanstack-query/package.json
Original file line number Diff line number Diff line change
Expand Up @@ -28,15 +28,29 @@
"@zenstackhq/runtime": "workspace:*"
},
"devDependencies": {
"@testing-library/dom": "^10.4.1",
"@testing-library/jest-dom": "^6.9.1",
"@testing-library/react": "^16.3.0",
"@types/react": "catalog:",
"@types/react-dom": "catalog:",
"@zenstackhq/eslint-config": "workspace:*",
"@zenstackhq/typescript-config": "workspace:*"
"@zenstackhq/typescript-config": "workspace:*",
"jsdom": "^27.0.1"
},
"peerDependencies": {
"@tanstack/react-query": "^5.0.0"
"@tanstack/react-query": "^5.0.0",
"react": "catalog:",
"react-dom": "catalog:"
},
"peerDependenciesMeta": {
"@tanstack/react-query": {
"optional": true
},
"react": {
"optional": true
},
"react-dom": {
"optional": true
}
}
}
166 changes: 118 additions & 48 deletions packages/tanstack-query/src/react.ts
Original file line number Diff line number Diff line change
@@ -1,78 +1,148 @@
import type {
DefaultError,
UseMutationOptions,
UseMutationResult,
UseQueryOptions,
UseQueryResult,
/* eslint-disable @typescript-eslint/no-unused-vars */
import {
useMutation,
useQuery,
type DefaultError,
type UseMutationOptions,
type UseMutationResult,
type UseQueryOptions,
type UseQueryResult,
} from '@tanstack/react-query';
import type { CreateArgs, FindArgs, ModelResult, SelectSubset } from '@zenstackhq/runtime';
import type { GetModels, SchemaDef } from '@zenstackhq/runtime/schema';
import { type CreateArgs, type FindArgs, type ModelResult, type SelectSubset } from '@zenstackhq/runtime';
import { type GetModels, type SchemaDef } from '@zenstackhq/runtime/schema';
import { useContext } from 'react';
import { getQueryKey, type MutationMethod, type MutationOperation, type OperationArgs, type OperationResult, type QueryOperation } from './runtime/common';
import { RequestHandlerContext } from './runtime/react';

export type toHooks<Schema extends SchemaDef> = {
[Model in GetModels<Schema> as Uncapitalize<Model>]: ToModelHooks<Schema, Model>;
export type useHooks<Schema extends SchemaDef> = {
[Model in GetModels<Schema> as Uncapitalize<Model>]: UseModelHooks<Schema, Model>;
};

type ToModelHooks<Schema extends SchemaDef, Model extends GetModels<Schema>> = {
findMany<T extends FindArgs<Schema, Model, true>>(
export type UseModelHooks<Schema extends SchemaDef, Model extends GetModels<Schema>> = {
useFindFirst<T extends FindArgs<Schema, Model, true>>(
args?: SelectSubset<T, FindArgs<Schema, Model, true>>,
options?: Omit<UseQueryOptions<ModelResult<Schema, Model, T>[]>, 'queryKey'>,
): UseQueryResult<ModelResult<Schema, Model, T>[]>;

findFirst<T extends FindArgs<Schema, Model, true>>(
args?: SelectSubset<T, FindArgs<Schema, Model, true>>,
options?: Omit<UseQueryOptions<ModelResult<Schema, Model, T>[]>, 'queryKey'>,
options?: Omit<UseQueryOptions<ModelResult<Schema, Model, T>[]>, 'queryKey' | 'queryFn'>,
): UseQueryResult<ModelResult<Schema, Model, T> | null>;

create<T extends CreateArgs<Schema, Model>>(
options?: UseMutationOptions<ModelResult<Schema, Model, T>, DefaultError, T>,
): UseMutationResult<ModelResult<Schema, Model, T>, DefaultError, T>;
// useCreate<T extends CreateArgs<Schema, Model>>(
// options?: Omit<UseMutationOptions<ModelResult<Schema, Model, T>, DefaultError, T>, 'mutationFn'>,
// ): UseMutationResult<ModelResult<Schema, Model, T>, DefaultError, T>;
};

function uncapitalize(s: string) {
return s.charAt(0).toLowerCase() + s.slice(1);
}

export function toHooks<Schema extends SchemaDef>(schema: Schema): toHooks<Schema> {
export function useHooks<Schema extends SchemaDef>(schema: Schema): useHooks<Schema> {
return Object.entries(schema.models).reduce(
(acc, [model, _]) =>
(acc, [model]) =>
Object.assign(acc, {
[uncapitalize(model)]: toModelHooks(schema, model as GetModels<Schema>),
[uncapitalize(model)]: useModelHooks(schema, model as GetModels<Schema>),
}),
{} as toHooks<Schema>,
{} as useHooks<Schema>,
);
}

function toModelHooks<Schema extends SchemaDef, Model extends GetModels<Schema>>(schema: Schema, model: Model): any {
export function useModelHooks<
Schema extends SchemaDef,
Model extends GetModels<Schema>,
>(
schema: Schema,
model: Model,
): UseModelHooks<Schema, Model> {
const modelDef = schema.models[model];
if (!modelDef) {
throw new Error(`Model ${model} not found in schema`);
}

return {
findMany: () => {
return {
data: [],
isLoading: false,
isError: false,
};
useFindFirst: <T extends FindArgs<Schema, Model, true>>(
args: SelectSubset<T, FindArgs<Schema, Model, true>>,
options?: Omit<UseQueryOptions<ModelResult<Schema, Model, T>[]>, 'queryKey' | 'queryFn'>,
) => {
return useModelQuery(schema, model, 'findFirst', args);
},

findFirst: () => {
return {
data: null,
isLoading: false,
isError: false,
};
},
// useCreate: <T extends CreateArgs<Schema, Model>>(
// options?: Omit<UseMutationOptions<ModelResult<Schema, Model, T>, DefaultError, T>, 'mutationFn'>,
// ) => {
// return useModelMutation(schema, model, 'create', 'POST');
// },
}
}

export type ModelQuery<
Schema extends SchemaDef,
Model extends GetModels<Schema>,
Operation extends QueryOperation,
Result extends OperationResult<Schema, Model, Operation>,
> = UseQueryResult<Result, DefaultError>

export function useModelQuery<
Schema extends SchemaDef,
Model extends GetModels<Schema>,
Operation extends QueryOperation,
Args extends OperationArgs<Schema, Model, Operation>,
Result extends OperationResult<Schema, Model, Operation>,
>(
schema: Schema,
model: Model,
operation: Operation,
args: Args,
) {
const context = useContext(RequestHandlerContext);
if (!context) {
throw new Error('Missing context');
}

create: () => {
return {
mutate: async () => {
return null;
},
isLoading: false,
isError: false,
};
const queryKey = getQueryKey(schema, model, operation, args);
const argsQuery = encodeURIComponent(JSON.stringify(args));
const query = useQuery<unknown, DefaultError, Result>({
queryKey,
queryFn: async () => {
const response = await context.fetch!(context.endpoint!);

return response as unknown as Result;
},
};
});

return query;
}

export type ModelMutation<
Schema extends SchemaDef,
Model extends GetModels<Schema>,
Operation extends MutationOperation,
Args extends OperationArgs<Schema, Model, Operation>,
Result extends OperationResult<Schema, Model, Operation>,
> = UseMutationResult<Result, DefaultError, Args, unknown>;

export function useModelMutation<
Schema extends SchemaDef,
Model extends GetModels<Schema>,
Operation extends MutationOperation,
Args extends OperationArgs<Schema, Model, Operation>,
Result extends OperationResult<Schema, Model, Operation>,
>(
schema: Schema,
model: Model,
operation: Operation,
method: MutationMethod,
): ModelMutation<Schema, Model, Operation, Args, Result> {
const context = useContext(RequestHandlerContext);
if (!context) {
throw new Error('Missing context');
}

const mutation = useMutation<Result, DefaultError, Args>({
mutationFn: async () => {
const response = await context.fetch!(context.endpoint!, {
method,
});

return response as unknown as Result;
}
});

return mutation;
}
160 changes: 160 additions & 0 deletions packages/tanstack-query/src/runtime/common.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,160 @@
import type { AggregateArgs, AggregateResult, BatchResult, CountArgs, CountResult, CreateArgs, CreateManyAndReturnArgs, CreateManyArgs, CrudOperation, DeleteArgs, DeleteManyArgs, FindFirstArgs, FindManyArgs, FindUniqueArgs, GroupByArgs, GroupByResult, ModelResult, UpdateArgs, UpdateManyAndReturnArgs, UpdateManyArgs, UpsertArgs } from '@zenstackhq/runtime';
import type { GetModels, SchemaDef } from '@zenstackhq/runtime/schema';

/**
* The default query endpoint.
*/
export const DEFAULT_QUERY_ENDPOINT = '/api/model';

/**
* Prefix for TanStack Query keys.
*/
export const QUERY_KEY_PREFIX = 'zenstack';

/**
* Function signature for `fetch`.
*/
export type FetchFn = (url: string, options?: RequestInit) => Promise<Response>;

/**
* Context type for configuring the hooks.
*/
export type APIContext = {
/**
* The endpoint to use for the queries.
*/
endpoint?: string;

/**
* A custom fetch function for sending the HTTP requests.
*/
fetch?: FetchFn;

/**
* If logging is enabled.
*/
logging?: boolean;
};

/**
* Extra query options.
*/
export type ExtraQueryOptions = {
/**
* Whether this is an infinite query. Defaults to `false`.
*/
infinite?: boolean;

/**
* Whether to opt-in to optimistic updates for this query. Defaults to `true`.
*/
optimisticUpdate?: boolean;
};

export type CrudOperationArgsMap<Schema extends SchemaDef, Model extends GetModels<Schema>> = {
findFirst: FindFirstArgs<Schema, Model>,
findMany: FindManyArgs<Schema, Model>,
findUnique: FindUniqueArgs<Schema, Model>,
create: CreateArgs<Schema, Model>,
createMany: CreateManyArgs<Schema, Model>,
createManyAndReturn: CreateManyAndReturnArgs<Schema, Model>,
upsert: UpsertArgs<Schema, Model>,
update: UpdateArgs<Schema, Model>,
updateMany: UpdateManyArgs<Schema, Model>,
updateManyAndReturn: UpdateManyAndReturnArgs<Schema, Model>,
delete: DeleteArgs<Schema, Model>,
deleteMany: DeleteManyArgs<Schema, Model>,
count: CountArgs<Schema, Model>,
aggregate: AggregateArgs<Schema, Model>,
groupBy: GroupByArgs<Schema, Model>,
};

export type CrudOperationResultsMap<Schema extends SchemaDef, Model extends GetModels<Schema>> = {
findFirst: ModelResult<Schema, Model, CrudOperationArgsMap<Schema, Model>['findFirst']> | null;
findMany: ModelResult<Schema, Model, CrudOperationArgsMap<Schema, Model>['findMany']>[];
findUnique: ModelResult<Schema, Model, CrudOperationArgsMap<Schema, Model>['findUnique']>;
create: ModelResult<Schema, Model, CrudOperationArgsMap<Schema, Model>['create']>;
createMany: BatchResult;
createManyAndReturn: ModelResult<Schema, Model, CrudOperationArgsMap<Schema, Model>['createManyAndReturn']>[];
upsert: ModelResult<Schema, Model, CrudOperationArgsMap<Schema, Model>['upsert']>;
update: ModelResult<Schema, Model, CrudOperationArgsMap<Schema, Model>['update']>;
updateMany: BatchResult;
updateManyAndReturn: ModelResult<Schema, Model, CrudOperationArgsMap<Schema, Model>['updateManyAndReturn']>[];
delete: ModelResult<Schema, Model, CrudOperationArgsMap<Schema, Model>['delete']>;
deleteMany: BatchResult;
count: CountResult<Schema, Model, CrudOperationArgsMap<Schema, Model>['count']>;
aggregate: AggregateResult<Schema, Model, CrudOperationArgsMap<Schema, Model>['aggregate']>;
groupBy: GroupByResult<Schema, Model, CrudOperationArgsMap<Schema, Model>['groupBy']>;
};

export type QueryOperation = Extract<
CrudOperation,
'findFirst' | 'findMany' | 'findUnique' | 'count' | 'aggregate' | 'groupBy'
>;

export type MutationOperation = Exclude<CrudOperation, QueryOperation>;

export type MutationMethod = 'POST' | 'PUT' | 'DELETE';

export type QueryKey<
Schema extends SchemaDef,
Model extends GetModels<Schema>,
Operation extends CrudOperation,
Args extends OperationArgs<Schema, Model, Operation>,
> = [
prefix: typeof QUERY_KEY_PREFIX,
model: Model,
operation: Operation,
args: Args,
extraOptions: ExtraQueryOptions,
];

export type OperationArgs<
Schema extends SchemaDef,
Model extends GetModels<Schema>,
Operation extends CrudOperation,
> = CrudOperationArgsMap<Schema, Model>[Operation];

export type OperationResult<
Schema extends SchemaDef,
Model extends GetModels<Schema>,
Operation extends CrudOperation,
> = CrudOperationResultsMap<Schema, Model>[Operation];

export function getQueryKey<
Schema extends SchemaDef,
Model extends GetModels<Schema>,
Operation extends CrudOperation,
Args extends OperationArgs<Schema, Model, Operation>,
>(
schema: Schema,
model: Model,
operation: Operation,
args: Args,

extraOptions: ExtraQueryOptions = {
infinite: false,
optimisticUpdate: true,
},
): QueryKey<Schema, Model, Operation, Args> {
const modelDef = schema.models[model];
if (!modelDef) {
throw new Error(`Model ${model} not found in schema`);
}

return [QUERY_KEY_PREFIX, model, operation, args, extraOptions]
}

export function isZenStackQueryKey(
queryKey: readonly unknown[]
): queryKey is QueryKey<SchemaDef, GetModels<SchemaDef>, CrudOperation, any> {
if (queryKey.length < 5) {
return false;
}

if (queryKey[0] !== QUERY_KEY_PREFIX) {
return false;
}

return true;
}
5 changes: 5 additions & 0 deletions packages/tanstack-query/src/runtime/index.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,5 @@
export {
getQueryKey,
type ExtraQueryOptions,
type FetchFn,
} from './common';
Loading