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
New Format (Recommended)
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: truefor computed fields - Put SQL expression in
columnproperty - 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