Add truncate method to grid to support deleting all rows from a table if no filter provided

This commit is contained in:
Joshen Lim committed 2022-07-06 13:29:26 +08:00
1 parent 702d3a867f
commit 96f625f2cd
10 files changed
+155 -134

No files matched your search

+4 -4
View File
@@ -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 {
+87 -82
View File
@@ -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)}`
}
+20 -18
View File
@@ -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)
}
}
+1 -1
View File
@@ -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 }
) {}
+26 -26
View File
@@ -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" />}