Skip to content

Commit

Permalink
Merge pull request #384 from psteinroe/feat/delete-many
Browse files Browse the repository at this point in the history
feat: add delete many mutation
  • Loading branch information
psteinroe committed Feb 14, 2024
2 parents d5f999f + 9c76f6a commit 6ddf52a
Show file tree
Hide file tree
Showing 16 changed files with 772 additions and 66 deletions.
7 changes: 7 additions & 0 deletions .changeset/rotten-pillows-eat.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,7 @@
---
"@supabase-cache-helpers/postgrest-react-query": minor
"@supabase-cache-helpers/postgrest-core": minor
"@supabase-cache-helpers/postgrest-swr": minor
---

feat: add delete many mutation
79 changes: 62 additions & 17 deletions packages/postgrest-core/__tests__/delete-fetcher.spec.ts
Original file line number Diff line number Diff line change
Expand Up @@ -22,7 +22,7 @@ describe('delete', () => {
await expect(
buildDeleteFetcher(client.from('contact'), ['id'], {
queriesForTable: () => [],
})({}),
})([{ username: 'test' }]),
).rejects.toThrowError('Missing value for primary key id');
});

Expand All @@ -40,9 +40,11 @@ describe('delete', () => {
{
queriesForTable: () => [],
},
)({
id: contact?.id,
});
)([
{
id: contact?.id,
},
]);
expect(deletedContact).toEqual(null);
const { data } = await client
.from('contact')
Expand All @@ -67,12 +69,16 @@ describe('delete', () => {
{
queriesForTable: () => [{ paths: [], filters: [] }],
},
)({
id: contact?.id,
});
expect(deletedContact).toEqual({
normalizedData: { id: contact?.id },
});
)([
{
id: contact?.id,
},
]);
expect(deletedContact).toEqual([
{
normalizedData: { id: contact?.id },
},
]);
});

it('should apply query if provided', async () => {
Expand All @@ -86,12 +92,51 @@ describe('delete', () => {
const result = await buildDeleteFetcher(client.from('contact'), ['id'], {
query: 'ticket_number',
queriesForTable: () => [],
})({
id: contact?.id,
});
expect(result).toEqual({
normalizedData: { id: contact?.id },
userQueryData: { ticket_number: 1234 },
});
})([
{
id: contact?.id,
},
]);
expect(result).toEqual([
{
normalizedData: { id: contact?.id },
userQueryData: { id: contact?.id, ticket_number: 1234 },
},
]);
});

it('should delete multiple entities by primary keys', async () => {
const { data: contacts } = await client
.from('contact')
.insert([
{ username: `${testRunPrefix}-test-1` },
{ username: `${testRunPrefix}-test-2` },
])
.select('id')
.throwOnError();

expect(contacts).toBeDefined();
expect(contacts!.length).toEqual(2);

const deletedContact = await buildDeleteFetcher(
client.from('contact'),
['id'],
{
queriesForTable: () => [],
},
)((contacts ?? []).map((c) => ({ id: c.id })));

expect(deletedContact).toEqual(null);

const { data } = await client
.from('contact')
.select('*')
.in(
'id',
(contacts ?? []).map((c) => c.id),
)
.throwOnError();

expect(data).toEqual([]);
});
});
89 changes: 65 additions & 24 deletions packages/postgrest-core/src/delete-fetcher.ts
Original file line number Diff line number Diff line change
Expand Up @@ -7,10 +7,11 @@ import {

import { MutationFetcherResponse } from './fetch/build-mutation-fetcher-response';
import { BuildNormalizedQueryOps } from './fetch/build-normalized-query';
import { parseSelectParam } from './lib/parse-select-param';

export type DeleteFetcher<T extends GenericTable, R> = (
input: Partial<T['Row']>,
) => Promise<MutationFetcherResponse<R> | null>;
input: Partial<T['Row']>[],
) => Promise<MutationFetcherResponse<R>[] | null>;

export type DeleteFetcherOptions<
S extends GenericSchema,
Expand All @@ -32,36 +33,76 @@ export const buildDeleteFetcher =
opts: BuildNormalizedQueryOps<Q> & DeleteFetcherOptions<S, T, RelationName>,
): DeleteFetcher<T, R> =>
async (
input: Partial<T['Row']>,
): Promise<MutationFetcherResponse<R> | null> => {
input: Partial<T['Row']>[],
): Promise<MutationFetcherResponse<R>[] | null> => {
let filterBuilder = qb.delete(opts);
for (const key of primaryKeys) {
const value = input[key];
if (!value)
throw new Error(`Missing value for primary key ${String(key)}`);
filterBuilder = filterBuilder.eq(key as string, value);

if (primaryKeys.length === 1) {
const primaryKey = primaryKeys[0];
filterBuilder.in(
primaryKey as string,
input.map((i) => {
const v = i[primaryKey];
if (!v) {
throw new Error(
`Missing value for primary key ${primaryKey as string}`,
);
}
return v;
}),
);
} else {
filterBuilder = filterBuilder.or(
input
.map(
(i) =>
`and(${primaryKeys.map((c) => {
const v = i[c];
if (!v) {
throw new Error(
`Missing value for primary key ${c as string}`,
);
}
return `${c as string}.eq.${v}`;
})})`,
)
.join(','),
);
}
const primaryKeysData = primaryKeys.reduce<R>((prev, key) => {
return {
...prev,
[key]: input[key],
};
}, {} as R);

const primaryKeysData = input.map((i) =>
primaryKeys.reduce<R>((prev, key) => {
return {
...prev,
[key]: i[key],
};
}, {} as R),
);

if (!opts.disabled && opts.query) {
// make sure query returns the primary keys
const paths = parseSelectParam(opts.query);
const addKeys = primaryKeys.filter(
(key) => !paths.find((p) => p.path === key),
);
const { data } = await filterBuilder
.select(opts.query)
.throwOnError()
.single();
return {
.select([opts.query, ...addKeys].join(','))
.throwOnError();
return primaryKeysData.map<MutationFetcherResponse<R>>((pk) => ({
// since we are deleting, only the primary keys are required
normalizedData: primaryKeysData,
userQueryData: data as R,
};
normalizedData: pk,
userQueryData: ((data as R[]) ?? []).find((d) =>
primaryKeys.every((k) => d[k as keyof R] === pk[k as keyof R]),
),
}));
}
await filterBuilder.throwOnError().single();

await filterBuilder.throwOnError();

if (opts.queriesForTable().length > 0) {
// if there is at least one query on the table we are deleting from, return primary keys
return { normalizedData: primaryKeysData };
return primaryKeysData.map((pk) => ({ normalizedData: pk }));
}

return null;
};
Original file line number Diff line number Diff line change
@@ -0,0 +1,133 @@
import { createClient, SupabaseClient } from '@supabase/supabase-js';
import { QueryClient } from '@tanstack/react-query';
import { fireEvent, screen } from '@testing-library/react';
import React, { useState } from 'react';

import { useDeleteManyMutation, useQuery } from '../../src';
import type { Database } from '../database.types';
import { renderWithConfig } from '../utils';

const TEST_PREFIX = 'postgrest-react-query-delmany';

describe('useDeleteManyMutation', () => {
let client: SupabaseClient<Database>;
let testRunPrefix: string;

let contacts: Database['public']['Tables']['contact']['Row'][];

beforeAll(async () => {
testRunPrefix = `${TEST_PREFIX}-${Math.floor(Math.random() * 100)}`;
client = createClient(
process.env.SUPABASE_URL as string,
process.env.SUPABASE_ANON_KEY as string,
);
});

beforeEach(async () => {
await client.from('contact').delete().ilike('username', `${TEST_PREFIX}%`);

const { data } = await client
.from('contact')
.insert(
new Array(3)
.fill(0)
.map((idx) => ({ username: `${testRunPrefix}-${idx}` })),
)
.select('*');
contacts = data as Database['public']['Tables']['contact']['Row'][];
});

it('should delete existing cache item and reduce count', async () => {
const queryClient = new QueryClient();
function Page() {
const [success, setSuccess] = useState<boolean>(false);
const { data, count } = useQuery(
client
.from('contact')
.select('id,username', { count: 'exact' })
.eq('username', contacts[0].username ?? ''),
);
const { mutateAsync: deleteContact } = useDeleteManyMutation(
client.from('contact'),
['id'],
null,
{ onSuccess: () => setSuccess(true) },
);
const { mutateAsync: deleteWithEmptyOptions } = useDeleteManyMutation(
client.from('contact'),
['id'],
null,
{},
);
const { mutateAsync: deleteWithoutOptions } = useDeleteManyMutation(
client.from('contact'),
['id'],
);
return (
<div>
<div
data-testid="delete"
onClick={async () =>
await deleteContact([
{
id: (data ?? []).find((c) => c)?.id,
},
])
}
/>
<div
data-testid="deleteWithEmptyOptions"
onClick={async () =>
await deleteWithEmptyOptions([
{
id: (data ?? []).find((c) => c)?.id,
},
])
}
/>
<div
data-testid="deleteWithoutOptions"
onClick={async () =>
await deleteWithoutOptions([
{
id: (data ?? []).find((c) => c)?.id,
},
])
}
/>
{(data ?? []).map((d) => (
<span key={d.id}>{d.username}</span>
))}
<span data-testid="count">{`count: ${count}`}</span>
<span data-testid="success">{`success: ${success}`}</span>
</div>
);
}

renderWithConfig(<Page />, queryClient);
await screen.findByText(
`count: ${contacts.length}`,
{},
{ timeout: 10000 },
);
fireEvent.click(screen.getByTestId('deleteWithEmptyOptions'));
await screen.findByText(
`count: ${contacts.length - 1}`,
{},
{ timeout: 10000 },
);
fireEvent.click(screen.getByTestId('deleteWithoutOptions'));
await screen.findByText(
`count: ${contacts.length - 2}`,
{},
{ timeout: 10000 },
);
fireEvent.click(screen.getByTestId('delete'));
await screen.findByText('success: true', {}, { timeout: 10000 });
await screen.findByText(
`count: ${contacts.length - 3}`,
{},
{ timeout: 10000 },
);
});
});
2 changes: 1 addition & 1 deletion packages/postgrest-react-query/package.json
Original file line number Diff line number Diff line change
Expand Up @@ -26,7 +26,7 @@
"license": "MIT",
"scripts": {
"build": "tsup",
"test": "jest --coverage",
"test": "jest --coverage --runInBand",
"clean": "rm -rf .turbo && rm -rf lint-results && rm -rf .nyc_output && rm -rf node_modules && rm -rf dist",
"lint": "eslint src/**",
"lint:report": "eslint {src/**,__tests__/**} --format json --output-file ./lint-results/postgrest-react-query.json",
Expand Down
1 change: 1 addition & 0 deletions packages/postgrest-react-query/src/mutate/index.ts
Original file line number Diff line number Diff line change
@@ -1,3 +1,4 @@
export * from './use-delete-many-mutation';
export * from './use-delete-mutation';
export * from './use-insert-mutation';
export * from './use-update-mutation';
Expand Down
Loading

0 comments on commit 6ddf52a

Please sign in to comment.