mirror of
https://github.com/supabase/supabase.git
synced 2026-10-05 17:35:10 +03:00
Add truncate method to grid to support deleting all rows from a table if no filter provided
This commit is contained in:
1 parent
702d3a867f
commit
96f625f2cd
10 files changed
+155
-134
No files matched your search
@@ -147,10 +147,10 @@ const SupabaseGridLayout = forwardRef<SupabaseGridRef, SupabaseGridProps>((props
|
||||
|
||||
if (
|
||||
!state.table ||
|
||||
(typeof props.table == 'string' &&
|
||||
state.table!.name != props.table &&
|
||||
state.table!.schema != props.schema) ||
|
||||
(typeof props.table != 'string' &&
|
||||
(typeof props.table === 'string' &&
|
||||
state.table!.name !== props.table &&
|
||||
state.table!.schema !== props.schema) ||
|
||||
(typeof props.table !== 'string' &&
|
||||
JSON.stringify(props.table) !== JSON.stringify(state.table))
|
||||
) {
|
||||
const { savedState } = initTable(props, state, dispatch, sort as string[], filter as string[])
|
||||
|
||||
@@ -15,6 +15,7 @@ interface IGrid extends GridProps {
|
||||
rows: any[]
|
||||
}
|
||||
|
||||
// [Joshen] Just for visibility this is causing some hook errors in the browser
|
||||
export const Grid = memo(
|
||||
forwardRef<DataGridHandle, IGrid>(
|
||||
(
|
||||
|
||||
@@ -110,7 +110,10 @@ const RowHeader: FC<RowHeaderProps> = ({ sorts, filters }) => {
|
||||
message: 'Are you sure you want to delete the selected rows? This action cannot be undone.',
|
||||
onAsyncConfirm: async () => {
|
||||
if (allRowsSelected) {
|
||||
const { error } = await state.rowService!.deleteAll(filters)
|
||||
const { error } =
|
||||
filters.length === 0
|
||||
? await state.rowService!.truncate()
|
||||
: await state.rowService!.deleteAll(filters)
|
||||
if (error) {
|
||||
if (state.onError) state.onError(error)
|
||||
} else {
|
||||
|
||||
@@ -1,137 +1,146 @@
|
||||
import { ident, literal, format } from '@scaleleap/pg-format';
|
||||
import {
|
||||
Dictionary,
|
||||
Filter,
|
||||
QueryPagination,
|
||||
QueryTable,
|
||||
Sort,
|
||||
} from '../types';
|
||||
import { ident, literal, format } from '@scaleleap/pg-format'
|
||||
import { Dictionary, Filter, QueryPagination, QueryTable, Sort } from '../types'
|
||||
|
||||
export function countQuery(
|
||||
table: QueryTable,
|
||||
options?: {
|
||||
filters?: Filter[];
|
||||
filters?: Filter[]
|
||||
}
|
||||
) {
|
||||
let query = `select count(*) from ${queryTable(table)}`;
|
||||
const { filters } = options ?? {};
|
||||
let query = `select count(*) from ${queryTable(table)}`
|
||||
const { filters } = options ?? {}
|
||||
if (filters) {
|
||||
query = applyFilters(query, filters);
|
||||
query = applyFilters(query, filters)
|
||||
}
|
||||
return query + ';';
|
||||
return query + ';'
|
||||
}
|
||||
|
||||
export function truncateQuery(
|
||||
table: QueryTable,
|
||||
options?: {
|
||||
// [Joshen] yet to implement cascade from UI, just adding first
|
||||
cascade?: boolean
|
||||
}
|
||||
) {
|
||||
let query = `truncate ${queryTable(table)}`
|
||||
const { cascade } = options ?? {}
|
||||
if (cascade) {
|
||||
query += ' cascade'
|
||||
}
|
||||
return query + ';'
|
||||
}
|
||||
|
||||
export function deleteQuery(
|
||||
table: QueryTable,
|
||||
filters?: Filter[],
|
||||
options?: {
|
||||
returning?: boolean;
|
||||
returning?: boolean
|
||||
}
|
||||
) {
|
||||
if (!filters || filters.length == 0) {
|
||||
throw { message: 'no filters for this delete query' };
|
||||
if (!filters || filters.length === 0) {
|
||||
throw { message: 'no filters for this delete query' }
|
||||
}
|
||||
let query = `delete from ${queryTable(table)}`;
|
||||
const { returning } = options ?? {};
|
||||
let query = `delete from ${queryTable(table)}`
|
||||
const { returning } = options ?? {}
|
||||
if (filters) {
|
||||
query = applyFilters(query, filters);
|
||||
query = applyFilters(query, filters)
|
||||
}
|
||||
if (returning) {
|
||||
query += ' returning *';
|
||||
query += ' returning *'
|
||||
}
|
||||
return query + ';';
|
||||
return query + ';'
|
||||
}
|
||||
|
||||
export function insertQuery(
|
||||
table: QueryTable,
|
||||
values: Dictionary<any>[],
|
||||
options?: {
|
||||
returning?: boolean;
|
||||
returning?: boolean
|
||||
}
|
||||
) {
|
||||
if (!values || values.length == 0) {
|
||||
throw { message: 'no value to insert' };
|
||||
if (!values || values.length === 0) {
|
||||
throw { message: 'no value to insert' }
|
||||
}
|
||||
const { returning } = options ?? {};
|
||||
const { returning } = options ?? {}
|
||||
const queryColumns = Object.keys(values[0])
|
||||
.map((x) => ident(x))
|
||||
.join(',');
|
||||
let query = '';
|
||||
.join(',')
|
||||
let query = ''
|
||||
if (queryColumns.length == 0) {
|
||||
query = format(
|
||||
'insert into %1$s select from jsonb_populate_recordset(null::%1$s, %2$s)',
|
||||
queryTable(table),
|
||||
literal(JSON.stringify(values))
|
||||
);
|
||||
)
|
||||
} else {
|
||||
query = format(
|
||||
'insert into %1$s (%2$s) select %2$s from jsonb_populate_recordset(null::%1$s, %3$s)',
|
||||
queryTable(table),
|
||||
queryColumns,
|
||||
literal(JSON.stringify(values))
|
||||
);
|
||||
)
|
||||
}
|
||||
if (returning) {
|
||||
query += ' returning *';
|
||||
query += ' returning *'
|
||||
}
|
||||
return query + ';';
|
||||
return query + ';'
|
||||
}
|
||||
|
||||
export function selectQuery(
|
||||
table: QueryTable,
|
||||
columns?: string[],
|
||||
options?: {
|
||||
filters?: Filter[];
|
||||
pagination?: QueryPagination;
|
||||
sorts?: Sort[];
|
||||
filters?: Filter[]
|
||||
pagination?: QueryPagination
|
||||
sorts?: Sort[]
|
||||
}
|
||||
) {
|
||||
let query = '';
|
||||
const queryColumn = columns?.map((x) => ident(x)).join(', ') ?? '*';
|
||||
query += `select ${queryColumn} from ${queryTable(table)}`;
|
||||
let query = ''
|
||||
const queryColumn = columns?.map((x) => ident(x)).join(', ') ?? '*'
|
||||
query += `select ${queryColumn} from ${queryTable(table)}`
|
||||
|
||||
const { filters, pagination, sorts } = options ?? {};
|
||||
const { filters, pagination, sorts } = options ?? {}
|
||||
if (filters) {
|
||||
query = applyFilters(query, filters);
|
||||
query = applyFilters(query, filters)
|
||||
}
|
||||
if (sorts) {
|
||||
query = applySorts(query, sorts);
|
||||
query = applySorts(query, sorts)
|
||||
}
|
||||
if (pagination) {
|
||||
const { limit, offset } = pagination ?? {};
|
||||
query += ` limit ${literal(limit)} offset ${literal(offset)}`;
|
||||
const { limit, offset } = pagination ?? {}
|
||||
query += ` limit ${literal(limit)} offset ${literal(offset)}`
|
||||
}
|
||||
return query + ';';
|
||||
return query + ';'
|
||||
}
|
||||
|
||||
export function updateQuery(
|
||||
table: QueryTable,
|
||||
value: Dictionary<any>,
|
||||
options?: {
|
||||
filters?: Filter[];
|
||||
returning?: boolean;
|
||||
filters?: Filter[]
|
||||
returning?: boolean
|
||||
}
|
||||
) {
|
||||
const { filters, returning } = options ?? {};
|
||||
if (!filters || filters.length == 0) {
|
||||
throw { message: 'no filters for this update query' };
|
||||
const { filters, returning } = options ?? {}
|
||||
if (!filters || filters.length === 0) {
|
||||
throw { message: 'no filters for this update query' }
|
||||
}
|
||||
const queryColumns = Object.keys(value)
|
||||
.map((x) => ident(x))
|
||||
.join(',');
|
||||
.join(',')
|
||||
let query = format(
|
||||
'update %1$s set (%2$s) = (select %2$s from json_populate_record(null::%1$s, %3$s))',
|
||||
queryTable(table),
|
||||
queryColumns,
|
||||
literal(JSON.stringify(value))
|
||||
);
|
||||
)
|
||||
if (filters) {
|
||||
query = applyFilters(query, filters);
|
||||
query = applyFilters(query, filters)
|
||||
}
|
||||
if (returning) {
|
||||
query += ' returning *';
|
||||
query += ' returning *'
|
||||
}
|
||||
return query + ';';
|
||||
return query + ';'
|
||||
}
|
||||
|
||||
//============================================================
|
||||
@@ -139,60 +148,56 @@ export function updateQuery(
|
||||
//============================================================
|
||||
|
||||
function applyFilters(query: string, filters: Filter[]) {
|
||||
if (filters.length == 0) return query;
|
||||
if (filters.length === 0) return query
|
||||
query += ` where ${filters
|
||||
.map((filter) => {
|
||||
switch (filter.operator) {
|
||||
case 'in':
|
||||
return inFilterSql(filter);
|
||||
return inFilterSql(filter)
|
||||
case 'is':
|
||||
return isFilterSql(filter);
|
||||
return isFilterSql(filter)
|
||||
default:
|
||||
return `${ident(filter.column)} ${filter.operator} ${filterLiteral(
|
||||
filter.value
|
||||
)}`;
|
||||
return `${ident(filter.column)} ${filter.operator} ${filterLiteral(filter.value)}`
|
||||
}
|
||||
})
|
||||
.join(' and ')}`;
|
||||
return query;
|
||||
.join(' and ')}`
|
||||
return query
|
||||
}
|
||||
|
||||
function inFilterSql(filter: Filter) {
|
||||
let values;
|
||||
let values
|
||||
if (Array.isArray(filter.value)) {
|
||||
values = filter.value.map((x: any) => filterLiteral(x));
|
||||
values = filter.value.map((x: any) => filterLiteral(x))
|
||||
} else {
|
||||
const filterValueTxt = String(filter.value);
|
||||
values = filterValueTxt.split(',').map((x: any) => filterLiteral(x));
|
||||
const filterValueTxt = String(filter.value)
|
||||
values = filterValueTxt.split(',').map((x: any) => filterLiteral(x))
|
||||
}
|
||||
return `${ident(filter.column)} ${filter.operator} (${values.join(',')})`;
|
||||
return `${ident(filter.column)} ${filter.operator} (${values.join(',')})`
|
||||
}
|
||||
|
||||
function isFilterSql(filter: Filter) {
|
||||
const filterValueTxt = String(filter.value);
|
||||
const filterValueTxt = String(filter.value)
|
||||
switch (filterValueTxt) {
|
||||
case 'null':
|
||||
case 'false':
|
||||
case 'true':
|
||||
case 'not null':
|
||||
return `${ident(filter.column)} ${filter.operator} ${filterValueTxt}`;
|
||||
return `${ident(filter.column)} ${filter.operator} ${filterValueTxt}`
|
||||
default:
|
||||
return `${ident(filter.column)} ${filter.operator} ${filterLiteral(
|
||||
filter.value
|
||||
)}`;
|
||||
return `${ident(filter.column)} ${filter.operator} ${filterLiteral(filter.value)}`
|
||||
}
|
||||
}
|
||||
|
||||
function filterLiteral(value: any) {
|
||||
if (typeof value === 'string') {
|
||||
const temp = value.trim();
|
||||
const temp = value.trim()
|
||||
if (temp?.startsWith('ARRAY[') && temp?.endsWith(']')) {
|
||||
return temp;
|
||||
return temp
|
||||
} else {
|
||||
return literal(temp);
|
||||
return literal(temp)
|
||||
}
|
||||
}
|
||||
return value;
|
||||
return value
|
||||
}
|
||||
|
||||
//============================================================
|
||||
@@ -200,15 +205,15 @@ function filterLiteral(value: any) {
|
||||
//============================================================
|
||||
|
||||
function applySorts(query: string, sorts: Sort[]) {
|
||||
if (sorts.length == 0) return query;
|
||||
if (sorts.length === 0) return query
|
||||
query += ` order by ${sorts
|
||||
.map((x) => {
|
||||
const order = x.ascending ? 'asc' : 'desc';
|
||||
const nullOrder = x.nullsFirst ? 'nulls first' : 'nulls last';
|
||||
return `${ident(x.column)} ${order} ${nullOrder}`;
|
||||
const order = x.ascending ? 'asc' : 'desc'
|
||||
const nullOrder = x.nullsFirst ? 'nulls first' : 'nulls last'
|
||||
return `${ident(x.column)} ${order} ${nullOrder}`
|
||||
})
|
||||
.join(', ')}`;
|
||||
return query;
|
||||
.join(', ')}`
|
||||
return query
|
||||
}
|
||||
|
||||
//============================================================
|
||||
@@ -216,5 +221,5 @@ function applySorts(query: string, sorts: Sort[]) {
|
||||
//============================================================
|
||||
|
||||
function queryTable(table: QueryTable) {
|
||||
return `${ident(table.schema)}.${ident(table.name)}`;
|
||||
return `${ident(table.schema)}.${ident(table.name)}`
|
||||
}
|
||||
@@ -1,18 +1,13 @@
|
||||
import { Dictionary, QueryTable } from '../types';
|
||||
import { IQueryFilter, QueryFilter } from './QueryFilter';
|
||||
import { Dictionary, QueryTable } from '../types'
|
||||
import { IQueryFilter, QueryFilter } from './QueryFilter'
|
||||
|
||||
export interface IQueryAction {
|
||||
count: () => IQueryFilter;
|
||||
delete: (options?: { returning: boolean }) => IQueryFilter;
|
||||
insert: (
|
||||
values: Dictionary<any>[],
|
||||
options?: { returning: boolean }
|
||||
) => IQueryFilter;
|
||||
select: (columns?: string[]) => IQueryFilter;
|
||||
update: (
|
||||
value: Dictionary<any>,
|
||||
options?: { returning: boolean }
|
||||
) => IQueryFilter;
|
||||
count: () => IQueryFilter
|
||||
delete: (options?: { returning: boolean }) => IQueryFilter
|
||||
insert: (values: Dictionary<any>[], options?: { returning: boolean }) => IQueryFilter
|
||||
select: (columns?: string[]) => IQueryFilter
|
||||
update: (value: Dictionary<any>, options?: { returning: boolean }) => IQueryFilter
|
||||
truncate: (options?: { returning: boolean }) => IQueryFilter
|
||||
}
|
||||
|
||||
export class QueryAction implements IQueryAction {
|
||||
@@ -22,7 +17,7 @@ export class QueryAction implements IQueryAction {
|
||||
* Performs a COUNT on the table.
|
||||
*/
|
||||
count() {
|
||||
return new QueryFilter(this.table, 'count');
|
||||
return new QueryFilter(this.table, 'count')
|
||||
}
|
||||
|
||||
/**
|
||||
@@ -31,7 +26,7 @@ export class QueryAction implements IQueryAction {
|
||||
* @param options.returning If `true`, return the deleted row(s) in the response.
|
||||
*/
|
||||
delete(options?: { returning: boolean }) {
|
||||
return new QueryFilter(this.table, 'delete', undefined, options);
|
||||
return new QueryFilter(this.table, 'delete', undefined, options)
|
||||
}
|
||||
|
||||
/**
|
||||
@@ -41,7 +36,7 @@ export class QueryAction implements IQueryAction {
|
||||
* @param options.returning If `true`, return the inserted row(s) in the response.
|
||||
*/
|
||||
insert(values: Dictionary<any>[], options?: { returning: boolean }) {
|
||||
return new QueryFilter(this.table, 'insert', values, options);
|
||||
return new QueryFilter(this.table, 'insert', values, options)
|
||||
}
|
||||
|
||||
/**
|
||||
@@ -50,7 +45,7 @@ export class QueryAction implements IQueryAction {
|
||||
* @param columns the query columns, by default set to '*'.
|
||||
*/
|
||||
select(columns?: string[]) {
|
||||
return new QueryFilter(this.table, 'select', columns);
|
||||
return new QueryFilter(this.table, 'select', columns)
|
||||
}
|
||||
|
||||
/**
|
||||
@@ -60,6 +55,13 @@ export class QueryAction implements IQueryAction {
|
||||
* @param options.returning If `true`, return the updated row(s) in the response.
|
||||
*/
|
||||
update(value: Dictionary<any>, options?: { returning: boolean }) {
|
||||
return new QueryFilter(this.table, 'update', value, options);
|
||||
return new QueryFilter(this.table, 'update', value, options)
|
||||
}
|
||||
|
||||
/**
|
||||
* Performs a TRUNCATE on the table
|
||||
*/
|
||||
truncate(options?: { returning: boolean }) {
|
||||
return new QueryFilter(this.table, 'truncate', undefined, options)
|
||||
}
|
||||
}
|
||||
@@ -13,7 +13,7 @@ export class QueryFilter implements IQueryFilter, IQueryModifier {
|
||||
|
||||
constructor(
|
||||
protected table: QueryTable,
|
||||
protected action: 'count' | 'delete' | 'insert' | 'select' | 'update',
|
||||
protected action: 'count' | 'delete' | 'insert' | 'select' | 'update' | 'truncate',
|
||||
protected actionValue?: string[] | Dictionary<any> | Dictionary<any>[],
|
||||
protected actionOptions?: { returning: boolean }
|
||||
) {}
|
||||
|
||||
@@ -1,34 +1,29 @@
|
||||
import {
|
||||
Dictionary,
|
||||
Filter,
|
||||
QueryPagination,
|
||||
QueryTable,
|
||||
Sort,
|
||||
} from '../types';
|
||||
import { Dictionary, Filter, QueryPagination, QueryTable, Sort } from '../types'
|
||||
import {
|
||||
countQuery,
|
||||
deleteQuery,
|
||||
insertQuery,
|
||||
selectQuery,
|
||||
truncateQuery,
|
||||
updateQuery,
|
||||
} from './Query.utils';
|
||||
} from './Query.utils'
|
||||
|
||||
export interface IQueryModifier {
|
||||
range: (from: number, to: number) => QueryModifier;
|
||||
toSql: () => string;
|
||||
range: (from: number, to: number) => QueryModifier
|
||||
toSql: () => string
|
||||
}
|
||||
|
||||
export class QueryModifier implements IQueryModifier {
|
||||
protected pagination?: QueryPagination;
|
||||
protected pagination?: QueryPagination
|
||||
|
||||
constructor(
|
||||
protected table: QueryTable,
|
||||
protected action: 'count' | 'delete' | 'insert' | 'select' | 'update',
|
||||
protected action: 'count' | 'delete' | 'insert' | 'select' | 'update' | 'truncate',
|
||||
protected options?: {
|
||||
actionValue?: string[] | Dictionary<any> | Dictionary<any>[];
|
||||
actionOptions?: { returning: boolean };
|
||||
filters?: Filter[];
|
||||
sorts?: Sort[];
|
||||
actionValue?: string[] | Dictionary<any> | Dictionary<any>[]
|
||||
actionOptions?: { returning?: boolean; cascade?: boolean }
|
||||
filters?: Filter[]
|
||||
sorts?: Sort[]
|
||||
}
|
||||
) {}
|
||||
|
||||
@@ -39,8 +34,8 @@ export class QueryModifier implements IQueryModifier {
|
||||
* @param to The last index to which to limit the result, inclusive.
|
||||
*/
|
||||
range(from: number, to: number) {
|
||||
this.pagination = { offset: from, limit: to - from + 1 };
|
||||
return this;
|
||||
this.pagination = { offset: from, limit: to - from + 1 }
|
||||
return this
|
||||
}
|
||||
|
||||
/**
|
||||
@@ -48,40 +43,45 @@ export class QueryModifier implements IQueryModifier {
|
||||
*/
|
||||
toSql() {
|
||||
try {
|
||||
const { actionValue, actionOptions, filters, sorts } = this.options ?? {};
|
||||
const { actionValue, actionOptions, filters, sorts } = this.options ?? {}
|
||||
switch (this.action) {
|
||||
case 'count': {
|
||||
return countQuery(this.table, { filters });
|
||||
return countQuery(this.table, { filters })
|
||||
}
|
||||
case 'delete': {
|
||||
return deleteQuery(this.table, filters, {
|
||||
returning: actionOptions?.returning,
|
||||
});
|
||||
})
|
||||
}
|
||||
case 'insert': {
|
||||
return insertQuery(this.table, actionValue as Dictionary<any>[], {
|
||||
returning: actionOptions?.returning,
|
||||
});
|
||||
})
|
||||
}
|
||||
case 'select': {
|
||||
return selectQuery(this.table, actionValue as string[] | undefined, {
|
||||
filters,
|
||||
pagination: this.pagination,
|
||||
sorts,
|
||||
});
|
||||
})
|
||||
}
|
||||
case 'update': {
|
||||
return updateQuery(this.table, actionValue as Dictionary<any>, {
|
||||
filters,
|
||||
returning: actionOptions?.returning,
|
||||
});
|
||||
})
|
||||
}
|
||||
case 'truncate': {
|
||||
return truncateQuery(this.table, {
|
||||
cascade: actionOptions?.cascade,
|
||||
})
|
||||
}
|
||||
default: {
|
||||
return '';
|
||||
return ''
|
||||
}
|
||||
}
|
||||
} catch (error) {
|
||||
throw error;
|
||||
throw error
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -63,11 +63,12 @@ export class SqlRowService implements IRowService {
|
||||
return {}
|
||||
}
|
||||
|
||||
// For deleting all rows based on a given filter
|
||||
async deleteAll(filters: Filter[]) {
|
||||
let queryChains = this.query.from(this.table.name, this.table.schema ?? undefined).delete()
|
||||
|
||||
filters
|
||||
.filter((x) => x.value && x.value != '')
|
||||
.filter((x) => x.value && x.value !== '')
|
||||
.forEach((x) => {
|
||||
const value = this.formatFilterValue(x)
|
||||
queryChains = queryChains.filter(x.column, x.operator, value)
|
||||
@@ -77,6 +78,13 @@ export class SqlRowService implements IRowService {
|
||||
return await this.onSqlQuery(query)
|
||||
}
|
||||
|
||||
// For deleting all rows without any filter (clear entire table)
|
||||
async truncate() {
|
||||
let queryChains = this.query.from(this.table.name, this.table.schema ?? undefined).truncate()
|
||||
const query = queryChains.toSql()
|
||||
return await this.onSqlQuery(query)
|
||||
}
|
||||
|
||||
async fetchPage(page: number, rowsPerPage: number, filters: Filter[], sorts: Sort[]) {
|
||||
const pageFromZero = page > 0 ? page - 1 : page
|
||||
const from = pageFromZero * rowsPerPage
|
||||
|
||||
@@ -33,6 +33,8 @@ export interface IRowService {
|
||||
changedColumn?: string,
|
||||
onRowUpdate?: (value: any) => void
|
||||
) => { row?: SupaRow; error?: ServiceError }
|
||||
|
||||
truncate: () => Promise<{ error?: ServiceError }>
|
||||
}
|
||||
|
||||
export * from './SqlRowService'
|
||||
@@ -246,7 +246,7 @@ const TableEditorMenu: FC<Props> = ({
|
||||
>
|
||||
Duplicate Table
|
||||
</Dropdown.Item>,
|
||||
<Dropdown.Seperator />,
|
||||
<Dropdown.Seperator key="separator" />,
|
||||
<Dropdown.Item
|
||||
key="delete-table"
|
||||
icon={<IconTrash size="tiny" />}
|
||||
|
||||
Reference in new issue
Block a user