Core Concepts

DataSources

DataSources define your database schema with attributes, relationships, access control, and lifecycle hooks. They provide type-safe, automatic CRUD operations for your tables.

What is a DataSource?

A DataSource is a configuration object that describes:

  • Table structure - Columns, types, constraints
  • Relationships - Joins to other tables
  • Calculated fields - SQL expressions for derived values
  • Access control - Role-based permissions
  • Lifecycle hooks - Custom logic for insert/update/delete

Basic Structure

import type { DataSource } from '@wayvo-ai/core/common';
import { DefaultAttribute, DefaultDataSource, DefaultFullAccess } from '@wayvo-ai/core/ds';

export const EntityDataSource: DataSource<Entity> = {
  ...DefaultDataSource,
  id: 'Entity',
  tableName: 'entities',
  description: 'Entity description',
  attributes: [
    // Attribute definitions
  ],
  access: [
    // Access control rules
  ],
};

Attribute Types

Text

{
  ...DefaultAttribute,
  code: 'name',
  name: 'Name',
  type: 'Text',
  column: 'name',
  maxLength: 120,
  optional: false,
}

Number

{
  ...DefaultAttribute,
  code: 'amount',
  name: 'Amount',
  type: 'Number',
  column: 'amount',
  allowDecimals: true,
}

Date

{
  ...DefaultAttribute,
  code: 'createdAt',
  name: 'Created At',
  type: 'Date',
  column: 'created_at',
  optional: false,
}

Boolean

{
  ...DefaultAttribute,
  code: 'isActive',
  name: 'Is Active',
  type: 'Boolean',
  column: 'is_active',
  optional: false,
}

Joins and Relationships

Define joins in a centralized joins array:

export const ProjectDS: DataSource<Project> = {
  ...DefaultDataSource,
  id: 'Project',
  tableName: 'projects',
  joins: [
    {
      alias: 'pm',
      tableName: 'wv_users',
      joinType: 'LEFT',
      on: 'pm.user_name = x.project_manager',
    },
  ],
  attributes: [
    {
      ...DefaultAttribute,
      code: 'projectManager',
      name: 'Project Manager',
      type: 'Reference',
      column: 'project_manager',
      joinAlias: 'pm',
    },
    {
      ...DefaultAttribute,
      code: 'projectManagerName',
      name: 'Project Manager Name',
      type: 'Text',
      column: 'pm.display_name',
      maxLength: 240,
      optional: true,
      joinAlias: 'pm',
      calculated: true,  // MUST be calculated (from joined table)
    },
  ],
};

Nested Joins

Use dependsOn for nested joins:

joins: [
  {
    alias: 'cust',
    tableName: 'customers',
    joinType: 'INNER',
    on: 'cust.customer_id = x.customer_id',
  },
  {
    alias: 'custType',
    tableName: 'customer_types',
    joinType: 'LEFT',
    on: 'custType.id = cust.type_id',
    dependsOn: 'cust',  // Explicit dependency
  },
],

Calculated Attributes

Use SQL expressions for computed fields:

{
  ...DefaultAttribute,
  code: 'totalAmount',
  name: 'Total Amount',
  type: 'Number',
  allowDecimals: true,
  calculated: true,
  column: 'quantity * unit_price',  // SQL expression
},
{
  ...DefaultAttribute,
  code: 'fullName',
  name: 'Full Name',
  type: 'Text',
  calculated: true,
  column: "CONCAT(first_name, ' ', last_name)",
},

Important Rules:

  • Set calculated: true for computed fields
  • Put SQL expression in column property
  • Use column names (snake_case), not attribute codes
  • Calculated fields are read-only

Access Control

access: [
  // Read-only access
  {
    ...DefaultReadOnlyAccess,  // query: true, export: true
    roleCode: 'viewer',
  },
  // Full access
  {
    ...DefaultFullAccess,  // query, insert, update, delete, audit, export: true
    roleCode: 'admin',
  },
  // Custom access
  {
    roleCode: 'editor',
    query: true,
    insert: true,
    update: true,
    delete: false,  // Cannot delete
    audit: true,
    export: true,
  },
],

Lifecycle Hooks

Query Hooks

// Modify query before execution
preQuery: async ({ query, session, client }) => {
  if (!session.user.roles.includes('admin')) {
    query.filters = [...(query.filters || []), { userId: { is: session.user.id } }];
  }
  return query;
},

// Process results after query
postQuery: async ({ query, rows, session, client }) => {
  return rows.map(row => ({
    ...row,
    displayName: `${row.firstName} ${row.lastName}`,
  }));
},

Insert/Update/Delete Hooks

// Validate before insert
beforeInsert: async ({ rows, session, client }) => {
  for (const row of rows) {
    if (!row.name?.trim()) {
      throw new UserError('Name is required');
    }
  }
  return { rows };
},

// Post-process after insert
afterInsert: async ({ rows, session, client }) => {
  // Send notifications, create related records, etc.
  return rows;
},

// Validate before update
beforeUpdate: async ({ rows, session, client }) => {
  for (const row of rows) {
    if (row.status === 'published' && !row.publishedAt) {
      row.publishedAt = new Date().toISOString();
    }
  }
  return { rows };
},

// Validate before delete
beforeDelete: async ({ rows, session, client }) => {
  for (const row of rows) {
    if (row.isProtected) {
      throw new UserError('Cannot delete protected records');
    }
  }
  return rows;
},

Detecting status change with previousRows

afterUpdate (and beforeUpdate) receive optional previousRows: the row state from the DB before the update. When present, it is guaranteed to have the same length and order as rows (so you can safely pair rows[i] with previousRows[i]). It is undefined when current rows were not loaded (e.g. when skipQueryForUpdate is true or there are no rows). Use it to compare before/after (e.g. status) and run side effects (e.g. send email when status becomes Approved).

// sendEmail is from your app's email module (e.g. @wayvo-ai/core/server) and requires the client
afterUpdate: async ({ rows, previousRows, session, client }) => {
  if (!previousRows) return rows;
  for (let i = 0; i < rows.length; i++) {
    const prev = previousRows[i];
    const row = rows[i];
    if (prev?.status !== row.status && row.status === 'Approved') {
      await sendEmail(
        {
          to: row.requestedByEmail ?? session.user.userName,
          subject: 'Request approved',
          text: `Your request (id: ${row.id}) has been approved.`,
        },
        client,
      );
    }
  }
  return rows;
},

Type Definition

Create matching TypeScript type:

// src/lib/common/ds/types/module/Entity.ts
export interface Entity {
  id: string;
  name: string;
  description?: string;
  amount?: number;
  createdAt: string;
  isActive: boolean;
  userId?: string;
  createdBy: string;
  updatedAt?: string;
  updatedBy?: string;
}

Registration

Register your DataSource:

// src/lib/server/ds/defs/module/index.ts
export { EntityDataSource } from './EntityDS';

// src/lib/server/ds/defs/index.ts
import { EntityDataSource } from './module';

const allDataSources = {
  Entity: EntityDataSource,
  // ... other DataSources
};

Next Steps

  • Stores - Use DataSources with reactive stores
  • Components - Display DataSource data in tables
Previous
Quick Start
Next
Stores