Skip to content
Merged
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
2 changes: 1 addition & 1 deletion packages/plugins/swr/package.json
Original file line number Diff line number Diff line change
Expand Up @@ -49,7 +49,7 @@
"nock": "^13.3.6",
"react": "18.2.0",
"rimraf": "^3.0.2",
"swr": "^2.0.3",
"swr": "^2.2.4",
"ts-jest": "^29.0.5",
"typescript": "^4.9.4"
}
Expand Down
163 changes: 92 additions & 71 deletions packages/plugins/swr/src/generator.ts
Original file line number Diff line number Diff line change
Expand Up @@ -32,6 +32,8 @@ export async function generate(model: Model, options: PluginOptions, dmmf: DMMF.
);
}

const legacyMutations = options.legacyMutations !== false;

const models = getDataModels(model);

await generateModelMeta(project, models, {
Expand All @@ -49,73 +51,76 @@ export async function generate(model: Model, options: PluginOptions, dmmf: DMMF.
warnings.push(`Unable to find mapping for model ${dataModel.name}`);
return;
}
generateModelHooks(project, outDir, dataModel, mapping);
generateModelHooks(project, outDir, dataModel, mapping, legacyMutations);
});

await saveProject(project);
return warnings;
}

function generateModelHooks(project: Project, outDir: string, model: DataModel, mapping: DMMF.ModelMapping) {
function generateModelHooks(
project: Project,
outDir: string,
model: DataModel,
mapping: DMMF.ModelMapping,
legacyMutations: boolean
) {
const fileName = paramCase(model.name);
const sf = project.createSourceFile(path.join(outDir, `${fileName}.ts`), undefined, { overwrite: true });

sf.addStatements('/* eslint-disable */');

const prismaImport = getPrismaClientImportSpec(model.$container, outDir);
sf.addImportDeclaration({
namedImports: ['Prisma', model.name],
namedImports: ['Prisma'],
isTypeOnly: true,
moduleSpecifier: prismaImport,
});
sf.addStatements([
`import { RequestHandlerContext, type GetNextArgs, type RequestOptions, type InfiniteRequestOptions, type PickEnumerable, type CheckSelect, useHooksContext } from '@zenstackhq/swr/runtime';`,
`import { type GetNextArgs, type QueryOptions, type InfiniteQueryOptions, type MutationOptions, type PickEnumerable, useHooksContext } from '@zenstackhq/swr/runtime';`,
`import metadata from './__model_meta';`,
`import * as request from '@zenstackhq/swr/runtime';`,
]);

const modelNameCap = upperCaseFirst(model.name);
const prismaVersion = getPrismaVersion();

const useMutation = sf.addFunction({
name: `useMutate${model.name}`,
isExported: true,
statements: [
'const { endpoint, fetch, logging } = useHooksContext();',
`const mutate = request.useMutate('${model.name}', metadata, logging);`,
],
});
const useMutation = legacyMutations
? sf.addFunction({
name: `useMutate${model.name}`,
isExported: true,
statements: [
'const { endpoint, fetch } = useHooksContext();',
`const invalidate = request.useInvalidation('${model.name}', metadata);`,
],
})
: undefined;

const mutationFuncs: string[] = [];

// create is somehow named "createOne" in the DMMF
// eslint-disable-next-line @typescript-eslint/no-explicit-any
if (mapping.create || (mapping as any).createOne) {
const argsType = `Prisma.${model.name}CreateArgs`;
const inputType = `Prisma.SelectSubset<T, ${argsType}>`;
const returnType = `CheckSelect<T, ${model.name}, Prisma.${model.name}GetPayload<T>>`;
mutationFuncs.push(
generateMutation(useMutation, model, 'post', 'create', argsType, inputType, returnType, true)
);
mutationFuncs.push(generateMutation(sf, useMutation, model, 'POST', 'create', argsType, false));
}

// createMany
if (mapping.createMany) {
const argsType = `Prisma.${model.name}CreateManyArgs`;
const inputType = `Prisma.SelectSubset<T, ${argsType}>`;
const returnType = `Prisma.BatchPayload`;
mutationFuncs.push(
generateMutation(useMutation, model, 'post', 'createMany', argsType, inputType, returnType, false)
);
mutationFuncs.push(generateMutation(sf, useMutation, model, 'POST', 'createMany', argsType, true));
}

// findMany
if (mapping.findMany) {
const argsType = `Prisma.${model.name}FindManyArgs`;
const inputType = `Prisma.SelectSubset<T, ${argsType}>`;
const returnType = `Array<Prisma.${model.name}GetPayload<T>>`;
const returnElement = `Prisma.${model.name}GetPayload<T>`;
const returnType = `Array<${returnElement}>`;
const optimisticReturn = `Array<${makeOptimistic(returnElement)}>`;

// regular findMany
generateQueryHook(sf, model, 'findMany', argsType, inputType, returnType);
generateQueryHook(sf, model, 'findMany', argsType, inputType, optimisticReturn, undefined, false);

// infinite findMany
generateQueryHook(sf, model, 'findMany', argsType, inputType, returnType, undefined, true);
Expand All @@ -125,72 +130,52 @@ function generateModelHooks(project: Project, outDir: string, model: DataModel,
if (mapping.findUnique) {
const argsType = `Prisma.${model.name}FindUniqueArgs`;
const inputType = `Prisma.SelectSubset<T, ${argsType}>`;
const returnType = `Prisma.${model.name}GetPayload<T>`;
generateQueryHook(sf, model, 'findUnique', argsType, inputType, returnType);
const returnType = makeOptimistic(`Prisma.${model.name}GetPayload<T>`);
generateQueryHook(sf, model, 'findUnique', argsType, inputType, returnType, undefined, false);
}

// findFirst
if (mapping.findFirst) {
const argsType = `Prisma.${model.name}FindFirstArgs`;
const inputType = `Prisma.SelectSubset<T, ${argsType}>`;
const returnType = `Prisma.${model.name}GetPayload<T>`;
generateQueryHook(sf, model, 'findFirst', argsType, inputType, returnType);
const returnType = makeOptimistic(`Prisma.${model.name}GetPayload<T>`);
generateQueryHook(sf, model, 'findFirst', argsType, inputType, returnType, undefined, false);
}

// update
// update is somehow named "updateOne" in the DMMF
// eslint-disable-next-line @typescript-eslint/no-explicit-any
if (mapping.update || (mapping as any).updateOne) {
const argsType = `Prisma.${model.name}UpdateArgs`;
const inputType = `Prisma.SelectSubset<T, ${argsType}>`;
const returnType = `Prisma.${model.name}GetPayload<T>`;
mutationFuncs.push(
generateMutation(useMutation, model, 'put', 'update', argsType, inputType, returnType, true)
);
mutationFuncs.push(generateMutation(sf, useMutation, model, 'PUT', 'update', argsType, false));
}

// updateMany
if (mapping.updateMany) {
const argsType = `Prisma.${model.name}UpdateManyArgs`;
const inputType = `Prisma.SelectSubset<T, ${argsType}>`;
const returnType = `Prisma.BatchPayload`;
mutationFuncs.push(
generateMutation(useMutation, model, 'put', 'updateMany', argsType, inputType, returnType, false)
);
mutationFuncs.push(generateMutation(sf, useMutation, model, 'PUT', 'updateMany', argsType, true));
}

// upsert
// upsert is somehow named "upsertOne" in the DMMF
// eslint-disable-next-line @typescript-eslint/no-explicit-any
if (mapping.upsert || (mapping as any).upsertOne) {
const argsType = `Prisma.${model.name}UpsertArgs`;
const inputType = `Prisma.SelectSubset<T, ${argsType}>`;
const returnType = `Prisma.${model.name}GetPayload<T>`;
mutationFuncs.push(
generateMutation(useMutation, model, 'post', 'upsert', argsType, inputType, returnType, true)
);
mutationFuncs.push(generateMutation(sf, useMutation, model, 'POST', 'upsert', argsType, false));
}

// del
// delete is somehow named "deleteOne" in the DMMF
// eslint-disable-next-line @typescript-eslint/no-explicit-any
if (mapping.delete || (mapping as any).deleteOne) {
const argsType = `Prisma.${model.name}DeleteArgs`;
const inputType = `Prisma.SelectSubset<T, ${argsType}>`;
const returnType = `Prisma.${model.name}GetPayload<T>`;
mutationFuncs.push(
generateMutation(useMutation, model, 'delete', 'delete', argsType, inputType, returnType, true)
);
mutationFuncs.push(generateMutation(sf, useMutation, model, 'DELETE', 'delete', argsType, false));
}

// deleteMany
if (mapping.deleteMany) {
const argsType = `Prisma.${model.name}DeleteManyArgs`;
const inputType = `Prisma.SelectSubset<T, ${argsType}>`;
const returnType = `Prisma.BatchPayload`;
mutationFuncs.push(
generateMutation(useMutation, model, 'delete', 'deleteMany', argsType, inputType, returnType, false)
);
mutationFuncs.push(generateMutation(sf, useMutation, model, 'DELETE', 'deleteMany', argsType, true));
}

// aggregate
Expand Down Expand Up @@ -283,7 +268,11 @@ function generateModelHooks(project: Project, outDir: string, model: DataModel,
generateQueryHook(sf, model, 'count', argsType, inputType, returnType);
}

useMutation.addStatements(`return { ${mutationFuncs.join(', ')} };`);
useMutation?.addStatements(`return { ${mutationFuncs.join(', ')} };`);
}

function makeOptimistic(returnType: string) {
return `${returnType} & { $optimistic?: boolean }`;
}

function generateIndex(project: Project, outDir: string, models: DataModel[]) {
Expand Down Expand Up @@ -321,7 +310,7 @@ function generateQueryHook(
}
parameters.push({
name: 'options?',
type: infinite ? `InfiniteRequestOptions<${returnType}>` : `RequestOptions<${returnType}>`,
type: infinite ? `InfiniteQueryOptions<${returnType}>` : `QueryOptions<${returnType}>`,
});

sf.addFunction({
Expand All @@ -332,40 +321,72 @@ function generateQueryHook(
})
.addBody()
.addStatements([
'const { endpoint, fetch } = useHooksContext();',
!infinite
? `return request.useGet<${returnType}>('${model.name}', '${operation}', endpoint, args, options, fetch);`
: `return request.useInfiniteGet<${inputType} | undefined, ${returnType}>('${model.name}', '${operation}', endpoint, getNextArgs, options, fetch);`,
? `return request.useModelQuery('${model.name}', '${operation}', args, options);`
: `return request.useInfiniteModelQuery('${model.name}', '${operation}', getNextArgs, options);`,
]);
}

function generateMutation(
func: FunctionDeclaration,
sf: SourceFile,
useMutateModelFunc: FunctionDeclaration | undefined,
model: DataModel,
method: 'post' | 'put' | 'patch' | 'delete',
method: 'POST' | 'PUT' | 'PATCH' | 'DELETE',
operation: string,
argsType: string,
inputType: string,
returnType: string,
checkReadBack: boolean
batchResult: boolean
) {
// non-batch mutations are subject to read-back check
const checkReadBack = !batchResult;
const genericReturnType = batchResult ? 'Prisma.BatchPayload' : `Prisma.${model.name}GetPayload<T> | undefined`;
const returnType = batchResult ? 'Prisma.BatchPayload' : `Prisma.${model.name}GetPayload<${argsType}> | undefined`;
const genericInputType = `Prisma.SelectSubset<T, ${argsType}>`;

const modelRouteName = lowerCaseFirst(model.name);
const funcName = `${operation}${model.name}`;
const fetcherFunc = method === 'delete' ? 'del' : method;
func.addFunction({
name: funcName,
isAsync: true,
typeParameters: [`T extends ${argsType}`],

if (useMutateModelFunc) {
// generate async mutation function (legacy)
const mutationFunc = useMutateModelFunc.addFunction({
name: funcName,
isAsync: true,
typeParameters: [`T extends ${argsType}`],
parameters: [
{
name: 'args',
type: genericInputType,
},
],
});
mutationFunc.addJsDoc(`@deprecated Use \`use${upperCaseFirst(operation)}${model.name}\` hook instead.`);
mutationFunc
.addBody()
.addStatements([
`return await request.mutationRequest<${returnType}, ${checkReadBack}>('${method}', \`\${endpoint}/${modelRouteName}/${operation}\`, args, invalidate, fetch, ${checkReadBack});`,
]);
}

// generate mutation hook
sf.addFunction({
name: `use${upperCaseFirst(operation)}${model.name}`,
isExported: true,
parameters: [
{
name: 'args',
type: inputType,
name: 'options?',
type: `MutationOptions<${returnType}, unknown, ${argsType}>`,
},
],
})
.addBody()
.addStatements([
`return await request.${fetcherFunc}<${returnType}, ${checkReadBack}>(\`\${endpoint}/${modelRouteName}/${operation}\`, args, mutate, fetch, ${checkReadBack});`,
`const mutation = request.useModelMutation('${model.name}', '${method}', '${operation}', metadata, options, ${checkReadBack});`,
`return {
...mutation,
trigger<T extends ${argsType}>(args: ${genericInputType}) {
return mutation.trigger(args, options as any) as Promise<${genericReturnType}>;
}
};`,
]);

return funcName;
}
Loading