Skip to main content

Relations

Relations define how entities connect to each other. Archetype supports three relation types that generate foreign keys, junction tables, and proper TypeScript types.

Relation Types

hasOne()

Creates a foreign key to another entity. Use for "many-to-one" or "one-to-one" relationships where this entity references another.

import { defineEntity, text, hasOne } from 'archetype-engine'

export const Post = defineEntity('Post', {
fields: {
title: text().required(),
},
relations: {
author: hasOne('User'), // Creates author_id column
category: hasOne('Category'), // Creates category_id column
},
})

Generated schema:

export const posts = sqliteTable('posts', {
id: text('id').primaryKey(),
title: text('title').notNull(),
authorId: text('author_id').references(() => users.id),
categoryId: text('category_id').references(() => categories.id),
})

Inverse relation:

The related entity typically has a hasMany() relation pointing back:

export const User = defineEntity('User', {
fields: {
name: text().required(),
},
relations: {
posts: hasMany('Post'), // User has many posts (inverse of Post.author)
},
})

hasMany()

Indicates a one-to-many relationship. No column created on this entity.

export const User = defineEntity('User', {
fields: {
name: text().required(),
},
relations: {
posts: hasMany('Post'), // User has many posts
comments: hasMany('Comment'),
},
})

belongsToMany()

Creates a many-to-many relationship with a junction table.

export const Post = defineEntity('Post', {
fields: {
title: text().required(),
},
relations: {
tags: belongsToMany('Tag'), // Creates post_tags junction table
},
})

export const Tag = defineEntity('Tag', {
fields: {
name: text().required(),
},
relations: {
posts: belongsToMany('Post'), // References same junction table
},
})

Generated junction table:

export const postTags = sqliteTable('post_tags', {
postId: text('post_id').notNull().references(() => posts.id),
tagId: text('tag_id').notNull().references(() => tags.id),
})

Optional Relations

By default, hasOne creates a required foreign key. Make it optional:

relations: {
category: hasOne('Category').optional(), // category_id can be null
}

Pivot Data with through()

For many-to-many relations that need extra data on the junction, use .through():

export const Order = defineEntity('Order', {
fields: {
orderNumber: text().required(),
},
relations: {
products: belongsToMany('Product').through({
table: 'order_items', // Optional custom table name
fields: {
quantity: number().required().min(1),
unitPrice: number().required(),
discount: number().default(0),
},
}),
},
})

Generated junction table:

export const orderItems = sqliteTable('order_items', {
orderId: text('order_id').notNull().references(() => orders.id),
productId: text('product_id').notNull().references(() => products.id),
quantity: integer('quantity').notNull(),
unitPrice: integer('unit_price').notNull(),
discount: integer('discount').default(0),
})

Example: E-commerce Schema

export const Customer = defineEntity('Customer', {
fields: {
email: text().required().email(),
name: text().required(),
},
relations: {
orders: hasMany('Order'),
addresses: hasMany('Address'),
},
})

export const Order = defineEntity('Order', {
fields: {
orderNumber: text().required().unique(),
total: number().required(),
status: text().default('pending'),
},
relations: {
customer: hasOne('Customer'),
shippingAddress: hasOne('Address').optional(),
items: hasMany('OrderItem'),
},
})

export const Product = defineEntity('Product', {
fields: {
name: text().required(),
price: number().required().positive(),
},
relations: {
category: hasOne('Category'),
orderItems: hasMany('OrderItem'),
},
})

export const OrderItem = defineEntity('OrderItem', {
fields: {
quantity: number().required().integer().min(1),
unitPrice: number().required(),
},
relations: {
order: hasOne('Order'),
product: hasOne('Product'),
},
})