Skip to content

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:

app/pages/users/api.server.js
tsx
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:

app/pages/users/api.server.js
typescript
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, optional

    Specifies the default column to sort the table by. If not specified, the default value is the primary key column.

  • defaultSortDirection

    "asc" | "desc", optional

    Specifies the default sort direction. If not specified, the default value is "desc".

  • customDataFetcher

    function, optional

    Defines a custom function to retrieve data for the table. This function is required if you are using the customFetch fetch strategy.

    Learn more: Custom data fetcher

  • allowedRolesToInsert

    string[], optional

    Specifies the role names that are allowed to insert records into the table. If not specified, all users can insert records unless allowInsert is set to false.

  • validateRecordBeforeInsert

    function, optional

    Defines 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.

    Example
    typescript
    validateRecordBeforeInsert: (values) => {
      if (!values.email.includes('@')) {
        throw new Error('Invalid email');
      }
    
      return true;
    }
  • transformRecordBeforeInsert

    function, optional

    A 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.

    Example
    typescript
    transformRecordBeforeInsert: (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, optional

    A 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.

    Example
    typescript
    afterInsert: (primaryKey, values) => {
      console.log(`Record with ID ${primaryKey} was inserted with values:`, values);
    }
  • allowedRolesToUpdate

    string[], optional

    Specifies the role names that are allowed to update records in the table. If not specified, all users can update records unless allowUpdate is set to false.

  • validateRecordBeforeUpdate

    function, optional

    Defines 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.

    Example
    typescript
    validateRecordBeforeUpdate: (primaryKey, values) => {
      if (!values.email.includes('@')) {
        throw new Error('Invalid email');
      }
    
      return true;
    }
  • transformRecordBeforeUpdate

    function, optional

    Defines 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.

    Example
    typescript
    transformRecordBeforeUpdate: (primaryKey, values) => {
      return {
        ...values,
        updated_at: new Date()
      }
    }
  • afterUpdate

    function, optional

    A 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.

    Example
    typescript
    afterUpdate: (primaryKey, values) => {
      console.log(`Record with ID ${primaryKey} was updated with values:`, values);
    }
  • allowedRolesToDelete

    string[], optional

    Specifies the role names that are allowed to delete records from the table. If not specified, all users can delete records unless allowDelete is set to false.

  • validateRecordBeforeDelete

    function, optional

    Defines 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.

    Example
    typescript
    validateRecordBeforeDelete: (primaryKey) => {
      if (primaryKey === 1) {
        throw new Error('Admin user cannot be deleted');
      }
    
      return true;
    }
  • afterDelete

    function, optional

    A 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.

    Example
    typescript
    afterDelete: (primaryKey) => {
      console.log(`Record with ID ${primaryKey} was deleted`);
    }
  • knexQueryModifier

    (knex: Knex.QueryBuilder) => Knex.QueryBuilder, optional

    A 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.

    Example with where clause
    typescript
    knexQueryModifier: (knex) => {
      return knex.where('is_active', true);
    }
    Example with group by
    typescript
    knexQueryModifier: (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:

tsx
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;
typescript
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:

tsx
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);
          },
        },
      ]}
    />
  );
};
tsx
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: