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'),
},
})