mirror of
https://github.com/zenstackhq/zenstack
synced 2026-05-24 10:08:55 +00:00
refactor(tanstack-query): strongly type TransactionOperation and normalize model name lookup
- Make `TransactionOperation<Schema>` a discriminated union over (model, op) pairs with schema-derived args types - Normalize incoming mutation model name via case-insensitive schema lookup so lowerCaseFirst names resolve - Guard transaction onSuccess against malformed variables and per-item shape - Move `normalizeEndpoint` from constants.ts to client.ts - Group react-query tests into CRUD/Optimistic/Sequential describe blocks Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
This commit is contained in:
parent
29bf3e6678
commit
520cdf4d6e
8 changed files with 1752 additions and 1681 deletions
|
|
@ -30,10 +30,11 @@ export function createInvalidator(
|
|||
invalidator: InvalidateFunc,
|
||||
logging: Logger | undefined,
|
||||
) {
|
||||
const normalizedModel = normalizeModelName(model, schema);
|
||||
return async (...args: unknown[]) => {
|
||||
const [_, variables] = args;
|
||||
const predicate = await getInvalidationPredicate(
|
||||
model,
|
||||
normalizedModel,
|
||||
operation as ORMWriteActionType,
|
||||
variables,
|
||||
schema,
|
||||
|
|
@ -87,3 +88,9 @@ function findNestedRead(visitingModel: string, targetModels: string[], schema: S
|
|||
const modelsRead = getReadModels(visitingModel, schema, args);
|
||||
return targetModels.some((m) => modelsRead.includes(m));
|
||||
}
|
||||
|
||||
// resolves a model name to its canonical form as defined in the schema (case-insensitive match)
|
||||
function normalizeModelName(model: string, schema: SchemaDef) {
|
||||
const target = model.toLowerCase();
|
||||
return Object.keys(schema.models).find((k) => k.toLowerCase() === target) ?? model;
|
||||
}
|
||||
|
|
|
|||
|
|
@ -297,10 +297,6 @@ export class NestedWriteVisitor {
|
|||
}
|
||||
}
|
||||
break;
|
||||
|
||||
default: {
|
||||
throw new Error(`unhandled action type ${action}`);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
|
|
|
|||
|
|
@ -9,8 +9,11 @@ import type { TransactionOperation } from './types.js';
|
|||
/**
|
||||
* Builds the mutation function for a sequential transaction request.
|
||||
*/
|
||||
export function makeTransactionMutationFn(endpoint: string, fetch: FetchFn | undefined) {
|
||||
return (operations: TransactionOperation[]) => {
|
||||
export function makeTransactionMutationFn<Schema extends SchemaDef>(
|
||||
endpoint: string,
|
||||
fetch: FetchFn | undefined,
|
||||
) {
|
||||
return (operations: TransactionOperation<Schema>[]) => {
|
||||
const reqUrl = `${endpoint}/${TRANSACTION_ROUTE_PREFIX}/sequential`;
|
||||
const fetchInit = {
|
||||
method: 'POST',
|
||||
|
|
@ -37,7 +40,7 @@ export function makeTransactionOnSuccess(
|
|||
origOnSuccess: ((...args: any[]) => any) | undefined,
|
||||
) {
|
||||
return async (...args: any[]) => {
|
||||
const variables = Array.isArray(args[1]) ? (args[1] as TransactionOperation[]) : [];
|
||||
const variables = Array.isArray(args[1]) ? args[1] : [];
|
||||
for (const op of variables) {
|
||||
if (typeof op?.model !== 'string' || typeof op?.op !== 'string') {
|
||||
continue;
|
||||
|
|
|
|||
|
|
@ -1,12 +1,28 @@
|
|||
import type { Logger, OptimisticDataProvider } from '@zenstackhq/client-helpers';
|
||||
import type { FetchFn } from '@zenstackhq/client-helpers/fetch';
|
||||
import type {
|
||||
AggregateArgs,
|
||||
CountArgs,
|
||||
CreateArgs,
|
||||
CreateManyAndReturnArgs,
|
||||
CreateManyArgs,
|
||||
DeleteArgs,
|
||||
DeleteManyArgs,
|
||||
ExistsArgs,
|
||||
FindFirstArgs,
|
||||
FindManyArgs,
|
||||
FindUniqueArgs,
|
||||
GetProcedureNames,
|
||||
GetSlicedOperations,
|
||||
GroupByArgs,
|
||||
ModelAllowsCreate,
|
||||
OperationsRequiringCreate,
|
||||
ProcedureFunc,
|
||||
QueryOptions,
|
||||
UpdateArgs,
|
||||
UpdateManyAndReturnArgs,
|
||||
UpdateManyArgs,
|
||||
UpsertArgs,
|
||||
} from '@zenstackhq/orm';
|
||||
import type { GetModels, SchemaDef } from '@zenstackhq/schema';
|
||||
|
||||
|
|
@ -102,10 +118,48 @@ export type ProcedureReturn<Schema extends SchemaDef, Name extends GetProcedureN
|
|||
>;
|
||||
|
||||
/**
|
||||
* Represents a single operation to execute within a sequential transaction.
|
||||
* Maps each core CRUD operation to its argument type for a given model.
|
||||
*/
|
||||
export type TransactionOperation = {
|
||||
model: string;
|
||||
op: string;
|
||||
args?: unknown;
|
||||
type CrudArgsMap<Schema extends SchemaDef, Model extends GetModels<Schema>> = {
|
||||
findMany: FindManyArgs<Schema, Model>;
|
||||
findUnique: FindUniqueArgs<Schema, Model>;
|
||||
findFirst: FindFirstArgs<Schema, Model>;
|
||||
create: CreateArgs<Schema, Model>;
|
||||
createMany: CreateManyArgs<Schema, Model>;
|
||||
createManyAndReturn: CreateManyAndReturnArgs<Schema, Model>;
|
||||
update: UpdateArgs<Schema, Model>;
|
||||
updateMany: UpdateManyArgs<Schema, Model>;
|
||||
updateManyAndReturn: UpdateManyAndReturnArgs<Schema, Model>;
|
||||
upsert: UpsertArgs<Schema, Model>;
|
||||
delete: DeleteArgs<Schema, Model>;
|
||||
deleteMany: DeleteManyArgs<Schema, Model>;
|
||||
count: CountArgs<Schema, Model>;
|
||||
aggregate: AggregateArgs<Schema, Model>;
|
||||
groupBy: GroupByArgs<Schema, Model>;
|
||||
exists: ExistsArgs<Schema, Model>;
|
||||
};
|
||||
|
||||
/**
|
||||
* Operations available for a given model, omitting create-style operations
|
||||
* for models that don't allow them (e.g. delegate models).
|
||||
*/
|
||||
type AllowedTransactionOps<Schema extends SchemaDef, Model extends GetModels<Schema>> =
|
||||
ModelAllowsCreate<Schema, Model> extends true
|
||||
? keyof CrudArgsMap<Schema, Model>
|
||||
: Exclude<keyof CrudArgsMap<Schema, Model>, OperationsRequiringCreate>;
|
||||
|
||||
/**
|
||||
* Represents a single operation to execute within a sequential transaction.
|
||||
*
|
||||
* The `model`, `op`, and `args` fields are correlated: `op` is constrained to
|
||||
* the CRUD operations available on `model`, and `args` is typed accordingly.
|
||||
*/
|
||||
export type TransactionOperation<Schema extends SchemaDef> = {
|
||||
[Model in GetModels<Schema>]: {
|
||||
[Op in AllowedTransactionOps<Schema, Model>]: {
|
||||
model: Model;
|
||||
op: Op;
|
||||
args?: CrudArgsMap<Schema, Model>[Op];
|
||||
};
|
||||
}[AllowedTransactionOps<Schema, Model>];
|
||||
}[GetModels<Schema>];
|
||||
|
|
|
|||
|
|
@ -167,8 +167,8 @@ export type ModelMutationModelResult<
|
|||
): Promise<SimplifiedResult<Schema, Model, T, Options, false, Array, ExtResult>>;
|
||||
};
|
||||
|
||||
export type TransactionMutationOptions = Omit<
|
||||
UseMutationOptions<unknown[], DefaultError, TransactionOperation[]>,
|
||||
export type TransactionMutationOptions<Schema extends SchemaDef> = Omit<
|
||||
UseMutationOptions<unknown[], DefaultError, TransactionOperation<Schema>[]>,
|
||||
'mutationFn'
|
||||
> &
|
||||
Omit<ExtraMutationOptions, 'optimisticUpdate' | 'optimisticDataProvider'>;
|
||||
|
|
@ -187,8 +187,8 @@ export type ClientHooks<
|
|||
} & ProcedureHooks<Schema, Options> & {
|
||||
$transaction: {
|
||||
useSequential(
|
||||
options?: TransactionMutationOptions,
|
||||
): UseMutationResult<unknown[], DefaultError, TransactionOperation[]>;
|
||||
options?: TransactionMutationOptions<Schema>,
|
||||
): UseMutationResult<unknown[], DefaultError, TransactionOperation<Schema>[]>;
|
||||
};
|
||||
};
|
||||
|
||||
|
|
@ -807,11 +807,14 @@ export function useInternalMutation<TArgs, R = any>(
|
|||
return useMutation(finalOptions);
|
||||
}
|
||||
|
||||
export function useInternalTransactionMutation(schema: SchemaDef, options?: TransactionMutationOptions) {
|
||||
export function useInternalTransactionMutation<Schema extends SchemaDef>(
|
||||
schema: Schema,
|
||||
options?: TransactionMutationOptions<Schema>,
|
||||
) {
|
||||
const { endpoint, fetch, logging } = useFetchOptions(options);
|
||||
const queryClient = useQueryClient();
|
||||
|
||||
const mutationFn = makeTransactionMutationFn(endpoint, fetch);
|
||||
const mutationFn = makeTransactionMutationFn<Schema>(endpoint, fetch);
|
||||
|
||||
const finalOptions = { ...options, mutationFn };
|
||||
|
||||
|
|
|
|||
|
|
@ -160,8 +160,8 @@ export type ModelMutationModelResult<
|
|||
): Promise<SimplifiedResult<Schema, Model, T, Options, false, Array, ExtResult>>;
|
||||
};
|
||||
|
||||
export type TransactionMutationOptions = Omit<
|
||||
CreateMutationOptions<unknown[], DefaultError, TransactionOperation[]>,
|
||||
export type TransactionMutationOptions<Schema extends SchemaDef> = Omit<
|
||||
CreateMutationOptions<unknown[], DefaultError, TransactionOperation<Schema>[]>,
|
||||
'mutationFn'
|
||||
> &
|
||||
Omit<ExtraMutationOptions, 'optimisticUpdate' | 'optimisticDataProvider'>;
|
||||
|
|
@ -180,8 +180,8 @@ export type ClientHooks<
|
|||
} & ProcedureHooks<Schema, Options> & {
|
||||
$transaction: {
|
||||
useSequential(
|
||||
options?: TransactionMutationOptions,
|
||||
): CreateMutationResult<unknown[], DefaultError, TransactionOperation[]>;
|
||||
options?: TransactionMutationOptions<Schema>,
|
||||
): CreateMutationResult<unknown[], DefaultError, TransactionOperation<Schema>[]>;
|
||||
};
|
||||
};
|
||||
|
||||
|
|
@ -709,11 +709,14 @@ export function useInternalMutation<TArgs, R = any>(
|
|||
return createMutation(finalOptions);
|
||||
}
|
||||
|
||||
export function useInternalTransactionMutation(schema: SchemaDef, options?: Accessor<TransactionMutationOptions>) {
|
||||
export function useInternalTransactionMutation<Schema extends SchemaDef>(
|
||||
schema: Schema,
|
||||
options?: Accessor<TransactionMutationOptions<Schema>>,
|
||||
) {
|
||||
const { endpoint, fetch, logging } = useFetchOptions(options);
|
||||
const queryClient = useQueryClient();
|
||||
|
||||
const mutationFn = makeTransactionMutationFn(endpoint, fetch);
|
||||
const mutationFn = makeTransactionMutationFn<Schema>(endpoint, fetch);
|
||||
|
||||
const finalOptions = () => {
|
||||
const optionsValue = options?.();
|
||||
|
|
|
|||
|
|
@ -154,8 +154,8 @@ export type ModelMutationModelResult<
|
|||
): Promise<SimplifiedResult<Schema, Model, T, Options, false, Array, ExtResult>>;
|
||||
};
|
||||
|
||||
export type TransactionMutationOptions = MaybeRefOrGetter<
|
||||
Omit<UnwrapRef<UseMutationOptions<unknown[], DefaultError, TransactionOperation[]>>, 'mutationFn'> &
|
||||
export type TransactionMutationOptions<Schema extends SchemaDef> = MaybeRefOrGetter<
|
||||
Omit<UnwrapRef<UseMutationOptions<unknown[], DefaultError, TransactionOperation<Schema>[]>>, 'mutationFn'> &
|
||||
Omit<ExtraMutationOptions, 'optimisticUpdate' | 'optimisticDataProvider'>
|
||||
>;
|
||||
|
||||
|
|
@ -173,8 +173,8 @@ export type ClientHooks<
|
|||
} & ProcedureHooks<Schema, Options> & {
|
||||
$transaction: {
|
||||
useSequential(
|
||||
options?: TransactionMutationOptions,
|
||||
): UseMutationReturnType<unknown[], DefaultError, TransactionOperation[], unknown>;
|
||||
options?: TransactionMutationOptions<Schema>,
|
||||
): UseMutationReturnType<unknown[], DefaultError, TransactionOperation<Schema>[], unknown>;
|
||||
};
|
||||
};
|
||||
|
||||
|
|
@ -726,17 +726,14 @@ export function useInternalMutation<TArgs, R = any>(
|
|||
return useMutation(finalOptions);
|
||||
}
|
||||
|
||||
export function useInternalTransactionMutation(
|
||||
schema: SchemaDef,
|
||||
options?: MaybeRefOrGetter<
|
||||
Omit<UnwrapRef<UseMutationOptions<unknown[], DefaultError, TransactionOperation[]>>, 'mutationFn'> &
|
||||
Omit<ExtraMutationOptions, 'optimisticUpdate' | 'optimisticDataProvider'>
|
||||
>,
|
||||
export function useInternalTransactionMutation<Schema extends SchemaDef>(
|
||||
schema: Schema,
|
||||
options?: TransactionMutationOptions<Schema>,
|
||||
) {
|
||||
const queryClient = useQueryClient();
|
||||
const { endpoint, fetch, logging } = useFetchOptions(options);
|
||||
|
||||
const mutationFn = makeTransactionMutationFn(endpoint, fetch);
|
||||
const mutationFn = makeTransactionMutationFn<Schema>(endpoint, fetch);
|
||||
|
||||
const finalOptions = computed(() => {
|
||||
const optionsValue = toValue(options);
|
||||
|
|
|
|||
File diff suppressed because it is too large
Load diff
Loading…
Reference in a new issue