Skip to main content

Entities

Entities are the foundation of every Stack9 application. An entity represents a business object (like Customer, Order, Product) and defines its data structure, validation rules, and relationships.

What is an Entity?

An entity is a JSON configuration file that tells Stack9:

  • What fields the entity has
  • What type each field is (text, number, date, etc.)
  • How to validate the data
  • What relationships exist with other entities
  • What UI behaviors to apply

When you define an entity, Stack9 automatically:

  • ✅ Creates a database table
  • ✅ Generates REST API endpoints (CRUD)
  • ✅ Creates TypeScript models
  • ✅ Handles validation
  • ✅ Manages relationships
  • ✅ Provides audit logging

Entity File Structure

Entities are defined in src/entities/custom/{entity_name}.json:

{
"head": {
// Entity metadata
},
"fields": [
// Field definitions
],
"hooks": [
// Optional validation hooks
]
}

Entity Head Configuration

The head section defines entity-level metadata:

{
"head": {
"name": "Customer", // Display name (singular)
"key": "customer", // Unique identifier (lowercase, no spaces)
"pluralisedName": "customers", // Plural form
"allowComments": true, // Enable comments feature
"allowTasks": true, // Enable tasks feature
"isActive": true, // Entity is active
"maxAttachmentSizeAllowed": 30000000, // 30MB max file size (null for no limit)
"acceptedAttachmentFileTypes": [ // Allowed MIME types
"image/jpg",
"image/jpeg",
"image/png",
"application/pdf"
]
}
}

Key Properties

PropertyTypeRequiredDescription
namestringYesHuman-readable entity name
keystringYesUnique identifier (used in API endpoints)
pluralisedNamestringYesPlural form of the name
isActivebooleanYesWhether entity is active
allowCommentsbooleanYesEnable comments on records
allowTasksbooleanYesEnable tasks on records
maxAttachmentSizeAllowednumber | nullNoMax attachment size in bytes
acceptedAttachmentFileTypesstring[]NoAllowed file MIME types

Field Types

Stack9 supports various field types to model your data:

1. TextField

Single-line text input.

{
"label": "Email Address",
"key": "email",
"type": "TextField",
"placeholder": "Enter email address",
"description": "Primary contact email",
"validateRules": {
"required": true,
"maxLength": 100,
"minLength": 5,
"pattern": "^[^\\s@]+@[^\\s@]+\\.[^\\s@]+$" // Email validation pattern
},
"typeOptions": {
"multiLine": false, // Single-line input
"text": false // Use varchar instead of text column
},
"behaviourOptions": {
"readOnly": false
}
}

Type Options:

  • multiLine: Enable multi-line text input
  • text: Use text database column type instead of varchar
  • sensitive: Mark field as containing sensitive data

Note: Use validation patterns for email, phone, or URL validation instead of a format property.

2. NumericField

Number input with validation.

{
"label": "Age",
"key": "age",
"type": "NumericField",
"validateRules": {
"required": true,
"min": 18, // Minimum value (use min: 0 to prevent negatives)
"max": 120
},
"typeOptions": {
"precision": 0 // Number of decimal places
}
}

Type Options:

  • precision: Number of decimal places

Note: To prevent negative values, use min: 0 in validateRules. There is no allowNegative or decimals property.

3. DateField

Date or datetime picker.

{
"label": "Date of Birth",
"key": "dob",
"type": "DateField",
"placeholder": "Select date",
"typeOptions": {
"time": false // Set to true for datetime
},
"validateRules": {
"required": true
}
}

4. Checkbox

Boolean field (true/false).

{
"label": "Is Active",
"key": "is_active",
"type": "Checkbox",
"defaultValue": true,
"description": "Whether the customer is active"
}

5. OptionSet

Dropdown with predefined values.

{
"label": "Customer Type",
"key": "customer_type",
"type": "OptionSet",
"typeOptions": {
"values": ["Individual", "Business"]
},
"validateRules": {
"required": true
}
}

6. SingleDropDown

Foreign key relationship (Many-to-One).

{
"label": "Sales Channel",
"key": "sales_channel_id",
"type": "SingleDropDown",
"relationshipOptions": {
"ref": "sales_channel" // Related entity key
},
"typeOptions": {
"label": "name" // Field to display in dropdown
},
"validateRules": {
"required": false
}
}

7. MultiDropDown

Many-to-Many relationship.

{
"label": "Available Times",
"key": "available_times",
"type": "MultiDropDown",
"relationshipOptions": {
"ref": "available_time"
},
"typeOptions": {
"label": "{{period}}", // Template for display
"value": "{{id}}" // Value field
}
}

8. Grid

One-to-Many relationship (child records).

{
"label": "Alternative Emails",
"key": "alternative_emails",
"type": "Grid",
"relationshipOptions": {
"ref": "alternative_email" // Related entity
},
"typeOptions": {
"relationshipField": "customer_id" // FK field in child entity
}
}

9. RichTextEditor

HTML content editor.

{
"label": "Notes",
"key": "notes",
"type": "RichTextEditor",
"description": "Additional notes about the customer"
}

Field Properties

All fields share these common properties:

PropertyTypeRequiredDescription
labelstringYesDisplay label
keystringYesDatabase column name (snake_case)
typestringYesField type
placeholderstringNoPlaceholder text
descriptionstringNoHelp text
defaultValueanyNoDefault value for new records
validateRulesobjectNoValidation rules
behaviourOptionsobjectNoUI behavior options
typeOptionsobjectNoType-specific options
indexbooleanNoCreate database index

Validation Rules

{
"validateRules": {
"required": true, // Field is required
"maxLength": 100, // Max string length
"minLength": 5, // Min string length
"max": 999, // Max numeric value
"min": 0, // Min numeric value
"pattern": "^[A-Z0-9]+$" // Regex pattern
}
}

Behaviour Options

{
"behaviourOptions": {
"readOnly": true, // Field cannot be edited
"hidden": false // Hide field in forms
}
}

Relationships

One-to-Many (Parent → Children)

Use the Grid field type in the parent entity:

Parent Entity (customer.json):

{
"label": "Sales Orders",
"key": "sales_orders",
"type": "Grid",
"relationshipOptions": {
"ref": "sales_order"
},
"typeOptions": {
"relationshipField": "customer_id"
}
}

Child Entity (sales_order.json):

{
"label": "Customer",
"key": "customer_id",
"type": "SingleDropDown",
"relationshipOptions": {
"ref": "customer"
},
"typeOptions": {
"label": "name"
}
}

Many-to-One (Child → Parent)

Use the SingleDropDown field type:

{
"label": "Customer",
"key": "customer_id",
"type": "SingleDropDown",
"relationshipOptions": {
"ref": "customer"
},
"typeOptions": {
"label": "name"
}
}

Many-to-Many

Use the MultiDropDown field type:

{
"label": "Tags",
"key": "tags",
"type": "MultiDropDown",
"relationshipOptions": {
"ref": "tag"
},
"typeOptions": {
"label": "name",
"value": "id"
}
}

Stack9 automatically creates a junction table for many-to-many relationships.

Real-World Example

Here's a simplified Customer entity from a production Stack9 app:

{
"head": {
"name": "Customer",
"key": "customer",
"pluralisedName": "customers",
"allowComments": true,
"allowTasks": true,
"maxAttachmentSizeAllowed": 30000000,
"acceptedAttachmentFileTypes": [
"image/jpg",
"image/jpeg",
"image/png",
"application/pdf"
]
},
"fields": [
{
"label": "CRN",
"key": "crn",
"type": "NumericField",
"description": "Unique customer reference number",
"behaviourOptions": {
"readOnly": true
}
},
{
"label": "Customer Type",
"key": "customer_type",
"type": "OptionSet",
"typeOptions": {
"values": ["Individual", "Business"]
},
"validateRules": {
"required": true
}
},
{
"label": "First Name",
"key": "name",
"type": "TextField",
"placeholder": "First name or Business Name",
"validateRules": {
"required": true,
"maxLength": 100,
"minLength": 1
}
},
{
"label": "Last Name",
"key": "last_name",
"type": "TextField",
"validateRules": {
"maxLength": 80
}
},
{
"label": "Email",
"key": "email_address",
"type": "TextField",
"validateRules": {
"required": true,
"maxLength": 100,
"pattern": "^[^\\s@]+@[^\\s@]+\\.[^\\s@]+$"
}
},
{
"label": "Phone",
"key": "phone",
"type": "TextField",
"validateRules": {
"maxLength": 13,
"pattern": "^\\+?[1-9]\\d{1,14}$"
}
},
{
"label": "Date of Birth",
"key": "dob",
"type": "DateField",
"typeOptions": {
"time": false
}
},
{
"label": "Is Active",
"key": "is_active",
"type": "Checkbox",
"defaultValue": true
},
{
"label": "Sales Channel",
"key": "sales_channel_id",
"type": "SingleDropDown",
"relationshipOptions": {
"ref": "sales_channel"
},
"typeOptions": {
"label": "name"
}
},
{
"label": "Sales Orders",
"key": "sales_orders",
"type": "Grid",
"relationshipOptions": {
"ref": "sales_order"
},
"typeOptions": {
"relationshipField": "customer_id"
}
}
],
"hooks": []
}

Auto-Generated Features

When you create this entity, Stack9 automatically generates:

Database Table

CREATE TABLE customers (
id SERIAL PRIMARY KEY,
crn INTEGER,
customer_type VARCHAR(50),
name VARCHAR(100) NOT NULL,
last_name VARCHAR(80),
email_address VARCHAR(100) NOT NULL,
phone VARCHAR(13),
dob DATE,
is_active BOOLEAN DEFAULT true,
sales_channel_id INTEGER REFERENCES sales_channels(id),
_created_at TIMESTAMP DEFAULT NOW(),
_updated_at TIMESTAMP DEFAULT NOW(),
_created_by INTEGER,
_updated_by INTEGER,
_is_deleted BOOLEAN DEFAULT false
);

REST API Endpoints

GET    /api/customer/:id          - Get customer by ID
POST /api/customer - Create customer
PUT /api/customer/:id - Update customer
DELETE /api/customer/:id - Soft delete customer
POST /api/customer/search - Search customers
POST /api/customer/query - Query with aggregations

TypeScript Models

interface Customer {
id: number;
crn?: number;
customer_type?: 'Individual' | 'Business';
name: string;
last_name?: string;
email_address: string;
phone?: string;
dob?: Date;
is_active?: boolean;
sales_channel_id?: number;
_created_at?: Date;
_updated_at?: Date;
_created_by?: number;
_updated_by?: number;
_is_deleted?: boolean;
}

System Fields

Every entity automatically includes these system fields:

FieldTypeDescription
idintegerPrimary key (auto-increment)
_created_attimestampCreation timestamp
_updated_attimestampLast update timestamp
_created_byintegerUser who created the record
_updated_byintegerUser who last updated the record
_is_deletedbooleanSoft delete flag

Best Practices

1. Use Consistent Naming

  • Entity keys: customer, sales_order (lowercase, underscore)
  • Field keys: first_name, email_address (snake_case)
  • Label everything clearly for non-technical users

2. Add Descriptions

{
"label": "CRN",
"key": "crn",
"description": "Unique customer reference number - automatically generated"
}

3. Set Appropriate Validation

{
"validateRules": {
"required": true,
"maxLength": 100,
"minLength": 1
}
}

4. Use Read-Only Fields for System-Generated Values

{
"key": "crn",
"behaviourOptions": {
"readOnly": true
}
}

5. Index Frequently Queried Fields

{
"key": "email_address",
"index": true
}

Organize fields in the JSON in a logical order:

  • Basic info first (name, email)
  • Contact details next
  • Relationships at the end

Common Patterns

Auto-Generated Sequence Numbers

{
"label": "Order Number",
"key": "order_number",
"type": "NumericField",
"behaviourOptions": {
"readOnly": true
}
}

Then use an entity hook to generate the sequence:

// entity-hooks/order.vat.ts
const orderNumber = await db.sequence.nextVal('sales_order', 1, 999999);

Conditional Fields

Conditional field display is typically handled through custom UI components or screen configurations, not at the entity level. Use behaviourOptions to control basic visibility:

{
"label": "Business ABN",
"key": "business_abn",
"type": "TextField",
"behaviourOptions": {
"hidden": false // Can be toggled via entity hooks based on conditions
}
}

Note: Complex conditional logic should be implemented in entity hooks or custom UI components.

Calculated Fields

Define fields that are calculated in entity hooks:

{
"label": "Age",
"key": "age",
"type": "NumericField",
"behaviourOptions": {
"readOnly": true
}
}

Calculate in hook:

const age = dayjs().diff(dayjs(entity.dob), 'year');

Next Steps

Now that you understand entities, learn about: