Table page configuration
The defineTableController function creates a server-side controller that handles requests from table pages. It connects to your database and defines what data is available to the table component and how it behaves.
This function is used in the optional api.server.js file within a page directory and should be exported as the default export.
Basic usage
Configuring the main table
When you need customization beyond what the visual builder provides, you can pass additional configuration to the defineTableController function in the api.server.js file.
Example:
import { app } from '../../_server/app';
const controller = app.defineTableController({
// Additional configuration here
validateRecordBeforeInsert: (values) => {
if (!values.email.includes('@')) {
throw new Error('Invalid email');
}
return true;
},
});
export default controller;Configuring nested tables
You can also configure nested tables by adding a nested property in the table controller configuration. Each nested table can have its own configuration.
Example:
import { app } from '../../_server/app';
const controller = app.defineTableController({
// Additional configuration for the main table
validateRecordBeforeInsert: (values) => {
if (!values.email.includes('@')) {
throw new Error('Invalid email');
}
return true;
},
nested: {
orders__p__user_id: {
// Additional configuration for the nested table
validateRecordBeforeInsert: (values) => {
if (!values.amount || values.amount <= 0) {
throw new Error('Amount must be greater than zero');
}
return true;
},
},
},
});
export default controller;Usage
The defineTableController function takes two arguments:
config: A configuration object that defines the behavior of the table and its nested tables.serverProcedures: An optional object that allows you to define custom server procedures for the page. See the Custom server API section for more details.
If you only need to change the way columns and fields are rendered, you can use the TablePage component's properties like customColumns, columnTransformer, or columnOverrides. This approach is useful for modifying the display of columns and fields without changing the backend logic.
Parameters
defaultSortColumn
string, optionalSpecifies the default column to sort the table by. If not specified, the default value is the primary key column.
defaultSortDirection
"asc" | "desc", optionalSpecifies the default sort direction. If not specified, the default value is
"desc".
customDataFetcher
function, optionalDefines a custom function to retrieve data for the table. This function is required if you are using the
customFetchfetch strategy.Learn more: Custom data fetcher
allowedRolesToInsert
string[], optionalSpecifies the role names that are allowed to insert records into the table. If not specified, all users can insert records unless
allowInsertis set tofalse.validateRecordBeforeInsert
function, optionalDefines a function to validate the record values before they are inserted into the table. This function is called with the record values and should return a boolean indicating whether the values are valid.
It can also throw an error with a message to indicate why the record is invalid.
typescriptvalidateRecordBeforeInsert: (values) => { if (!values.email.includes('@')) { throw new Error('Invalid email'); } return true; }transformRecordBeforeInsert
function, optionalA function to transform the record values before they are inserted into the table. This function is called with the record values and should return the transformed values.
typescripttransformRecordBeforeInsert: (values) => { const secret_token = generate_random_token(); const created_at = new Date(); const updated_at = new Date(); return { ...values, secret_token, created_at, updated_at } }afterInsert
function, optionalA function executed after the record is inserted into the table. This function is often used to perform additional actions, such as sending notifications or updating related records.
typescriptafterInsert: (primaryKey, values) => { console.log(`Record with ID ${primaryKey} was inserted with values:`, values); }
allowedRolesToUpdate
string[], optionalSpecifies the role names that are allowed to update records in the table. If not specified, all users can update records unless
allowUpdateis set tofalse.validateRecordBeforeUpdate
function, optionalDefines a function to validate the record values before they are updated in the table. This function is called with the primary key and record values, and should return a boolean indicating whether the values are valid.
It can also throw an error with a message to indicate why the record is invalid.
typescriptvalidateRecordBeforeUpdate: (primaryKey, values) => { if (!values.email.includes('@')) { throw new Error('Invalid email'); } return true; }transformRecordBeforeUpdate
function, optionalDefines a function to transform the record values before they are updated in the table. This function is called with the primary key and record values, and should return the transformed values.
typescripttransformRecordBeforeUpdate: (primaryKey, values) => { return { ...values, updated_at: new Date() } }afterUpdate
function, optionalA function executed after the record is updated in the table. This function is often used to perform additional actions, such as sending notifications or updating related records.
typescriptafterUpdate: (primaryKey, values) => { console.log(`Record with ID ${primaryKey} was updated with values:`, values); }
allowedRolesToDelete
string[], optionalSpecifies the role names that are allowed to delete records from the table. If not specified, all users can delete records unless
allowDeleteis set tofalse.validateRecordBeforeDelete
function, optionalDefines a function to validate the record before it is deleted from the table. This function is called with the primary key and should return a boolean indicating whether the record can be deleted.
It can also throw an error with a message to indicate why the record cannot be deleted.
typescriptvalidateRecordBeforeDelete: (primaryKey) => { if (primaryKey === 1) { throw new Error('Admin user cannot be deleted'); } return true; }afterDelete
function, optionalA function executed after the record is deleted from the table. This function is often used to perform additional actions, such as sending notifications or deleting related records.
typescriptafterDelete: (primaryKey) => { console.log(`Record with ID ${primaryKey} was deleted`); }
knexQueryModifier
(knex: Knex.QueryBuilder) => Knex.QueryBuilder, optionalA function that modifies the Knex query before it is executed. This function is often used to add custom where clauses, joins, or other query modifications. Learn more in the Knex documentation.
typescriptknexQueryModifier: (knex) => { return knex.where('is_active', true); }typescriptknexQueryModifier: (knex) => { return knex.groupBy('user_id'); }
nested
An object that defines configurations for nested tables. Each key in this object corresponds to a nested table configuration.
Each nested table configuration has all the properties of a regular table configuration.
Custom server API
Adding custom server procedures
You can extend your table controller with custom server procedures to handle specific business logic that goes beyond standard table operations.
Example:
import { app } from '../../_server/app';
import postgresDataSource from '../../_server/data-sources/postgres_db';
// Get Knex client to interact with the database
const knex = postgresDataSource.getClient();
const controller = app.defineTableController({}, {
// Define a server-side procedure to send a welcome email
sendWelcomeEmail: async ({ userId }) => {
// Get user email from the database
const user = await knex('users').where({ id: userId }).first();
if (!user) {
throw new Error('User not found');
}
// Send email logic here...
console.log(`[server] Sending welcome email to ${user.email}`);
return {
success: true,
sentTo: user.email
};
},
});
export default controller;import { app } from '../../_server/app';
import postgresDataSource from '../../_server/data-sources/postgres_db';
interface SendWelcomeEmailInput {
userId: number;
}
// Get Knex client to interact with the database
const knex = postgresDataSource.getClient();
const controller = app.defineTableController({}, {
// Define a server-side procedure to send a welcome email
sendWelcomeEmail: async ({ userId }: SendWelcomeEmailInput) => {
// Get user email from the database
const user = await knex('users').where({ id: userId }).first();
if (!user) {
throw new Error('User not found');
}
// Send email logic here...
console.log(`[server] Sending welcome email to ${user.email}`);
return {
success: true,
sentTo: user.email
};
},
});
export type Procedures = typeof controller.procedures;
export default controller;Calling procedures from the frontend
Use the useCallProcedure hook to call your custom procedures from the table page:
Example:
import { TablePage, useCallProcedure } from '@kottster/react';
export default () => {
// Get the callProcedure function
const callProcedure = useCallProcedure();
const handleSendWelcomeEmail = async (userId) => {
try {
// Call the backend procedure
const result = await callProcedure('sendWelcomeEmail', { userId });
if (result.success) {
console.log(`Email sent successfully to ${result.sentTo}`);
} else {
console.error('Failed to send email');
}
} catch (error) {
console.error('Failed to send email:', error);
}
};
return (
<TablePage
customActions={[
{
label: 'Send Welcome Email',
onClick: (record) => {
handleSendWelcomeEmail(record.id);
},
},
]}
/>
);
};import { TablePage, useCallProcedure } from '@kottster/react';
import { type Procedures } from './api.server';
export default () => {
// Get the type-safe callProcedure function
const callProcedure = useCallProcedure<Procedures>();
const handleSendWelcomeEmail = async (userId: number) => {
try {
// Call the bakend procedure
const result = await callProcedure('sendWelcomeEmail', { userId });
if (result.success) {
console.log(`Email sent successfully to ${result.sentTo}`);
} else {
console.error('Failed to send email');
}
} catch (error) {
console.error('Failed to send email:', error);
}
};
return (
<TablePage
customActions={[
{
label: 'Send Welcome Email',
onClick: (record) => {
handleSendWelcomeEmail(record.id);
},
},
]}
/>
);
};You can learn more about custom server procedures in the following sections: