kastor0.1.0
Começar

GUIA 06Vindo do Laravel

Vindo do Laravel e do Eloquent

O kastor é um ORM em TypeScript. Ele roda em runtimes JavaScript como Node.js, Bun, Deno e Cloudflare Workers, e não roda dentro do PHP. Se você conhece o Eloquent, este guia cobre os dois caminhos que fazem sentido: levar um serviço Laravel para TypeScript ou rodar uma API TypeScript ao lado do Laravel no mesmo banco.

A parte honesta primeiro: dois runtimes, dois caminhos

O Eloquent vive no PHP. O kastor vive em runtimes JavaScript: Node.js, Bun, Deno, Cloudflare Workers e outras plataformas edge. Não existe extensão PHP nem ponte, e este guia não vai fingir que existe. Se o seu produto é um monólito Laravel e você está satisfeito com ele, o Eloquent é a ferramenta certa ali.

Quem vem do Laravel Eloquent costuma chegar ao kastor em uma de duas situações, e o guia cobre as duas:

  • Caminho A: levar para TypeScript. O time reescreve uma aplicação Laravel ou separa um serviço em Node.js ou Bun, e quer um ORM com cara conhecida: models, relações, eager loading, scopes, migrações, seeders e factories.
  • Caminho B: TypeScript ao lado do PHP. O Laravel continua rodando, e uma API TypeScript nova (um backend mobile, um serviço de tempo real, um frontend Next.js com código no servidor) lê e grava no mesmo banco.

Os dois começam do mesmo jeito: o kastor lê o banco que o Laravel construiu.

Passo 1: puxar o schema que as migrações do Laravel criaram

Em um projeto TypeScript novo, instale o kastor e o driver do seu banco. A maioria das aplicações Laravel roda MySQL ou MariaDB; PostgreSQL funciona do mesmo jeito com pg.

Terminal
npm install kastor-orm mysql2
npx kastor-orm init --db mysql
npx kastor db pull --out src/db/schema.ts

Antes do pull, aponte o DATABASE_URL do .env criado pelo init para o banco do Laravel, por exemplo mysql://forge:secret@127.0.0.1:3306/laravel. O db pull lê tabelas, colunas, defaults, índices e chaves estrangeiras criadas pelo php artisan migrate e escreve definições table() com as relações deduzidas das chaves. O Laravel usa snake case nas colunas, então o schema mantém os nomes do banco com .map() e o TypeScript lê em camelCase:

app/Models/Post.php
class Post extends Model
{
    use HasFactory, SoftDeletes;

    protected $casts = ['published' => 'boolean', 'meta' => 'array'];

    public function author() { return $this->belongsTo(User::class, 'user_id'); }
    public function comments() { return $this->hasMany(Comment::class); }

    public function scopePublished($query) { return $query->where('published', true); }
}
src/db/schema.ts
import { table, t, one, many } from 'kastor-orm'

export const users = table('users', {
  id: t.bigId(),
  name: t.string(),
  email: t.string().unique(),
  createdAt: t.datetime().map('created_at').nullable().defaultNow(),
  updatedAt: t.datetime().map('updated_at').nullable().updatedAt(),
})

export const posts = table('posts', {
  id: t.bigId(),
  userId: t.bigint().map('user_id').references(() => users.id, { onDelete: 'cascade' }),
  title: t.string(200),
  published: t.boolean().default(false),
  meta: t.json<{ tags?: string[] }>().nullable(),
  createdAt: t.datetime().map('created_at').nullable().defaultNow(),
  updatedAt: t.datetime().map('updated_at').nullable().updatedAt(),
  deletedAt: t.datetime().map('deleted_at').nullable(),
}, { softDelete: 'deletedAt' })

export const comments = table('comments', {
  id: t.bigId(),
  postId: t.bigint().map('post_id').references(() => posts.id, { onDelete: 'cascade' }),
  body: t.text(),
})

export const schema = { users, posts, comments }

export const relations = ({ users, posts, comments }: typeof schema) => ({
  users: { posts: many(posts) },
  posts: { author: one(users, { fields: ['userId'], references: ['id'] }), comments: many(comments) },
  comments: { post: one(posts, { fields: ['postId'], references: ['id'] }) },
})

O $table->id() do Laravel é um big integer sem sinal, então os ids são t.bigId() e chegam como bigint no JavaScript. É exato para qualquer valor que o MySQL guarda; converta com String(id) antes de enviar como JSON.

Passo 2: decidir quem cuida das migrações

No caminho A, o kastor assume. Registre o banco atual como primeira migração do kastor e faça cada mudança em src/db/schema.ts:

Terminal
npx kastor migrate baseline --name init
npx kastor migrate dev --name add_slug_to_posts
npx kastor migrate deploy

No caminho B, mantenha o Laravel como dono enquanto o PHP for a aplicação principal. Rode php artisan migrate como sempre e depois npx kastor db pull para atualizar o schema TypeScript, com commit junto da mudança. Não rode kastor migrate dev nesse banco enquanto o Laravel cuidar dele. O Laravel guarda o histórico na tabela migrations e o kastor na _kastor_migrations; uma nunca mexe na outra, então trocar de dono depois é seguro.

Passo 3: do Eloquent para o kastor, conceito por conceito

Eloquent e Laravelkastor
class Post extends Modeltable('posts', ...) e tipos por InferSelect
$fillable, $castsos tipos de create e update, t.boolean(), t.json<T>()
belongsTo, hasOne, hasManyone() e many()
belongsToMany com tabela pivotmany(tags, { through: postTag })
scopePublishedum objeto where tipado
with(), withCount(), whereHas()include, _count, some
SoftDeletes, withTrashed, restoresoftDelete, withDeleted: true, restore
paginate(), cursorPaginate()take e skip com count, ou findPage
updateOrCreate, incrementupsert, { increment: 1 }
DB::transaction, DB::selectdb.$transaction, db.$sql
php artisan migrate, migrate:fresh --seednpx kastor migrate deploy, migrate reset

O eager loading é onde o kastor fica mais parecido com o Eloquent, com uma vantagem: o with() roda uma consulta extra por relação, enquanto o kastor carrega a árvore inteira em uma instrução por chamada.

src/posts/list.ts
import { db } from '../db'

// Era: scopePublished
const published = { published: true } as const

export const latest = () =>
  db.posts.findMany({
    where: { ...published, comments: { some: {} } },
    include: { author: { select: { id: true, name: true } }, _count: { select: { comments: true } } },
    orderBy: { createdAt: 'desc' },
    take: 20,
  })

Eventos de model e observers não têm equivalente. Coloque essa lógica na função que grava, onde o TypeScript confere tudo. Accessors como getFullNameAttribute viram funções comuns sobre o tipo da linha.

Passo 4: seeders e factories

A chave seed do kastor.config.ts aponta para um arquivo que faz o papel do DatabaseSeeder, e factory() cobre as model factories, inclusive registros aninhados como o has():

src/db/seed.ts
import { defineSeed, factory } from 'kastor-orm'
import type { db as client } from './index'

export default defineSeed<typeof client>(async (db) => {
  const users = factory(db.users, ({ seq }) => ({ name: `Usuário ${seq}`, email: `user${seq}@example.com` }))

  // Era: User::factory()->count(10)->has(Post::factory()->count(3))->create()
  await users.createMany(10, () => ({
    posts: { create: [1, 2, 3].map((n) => ({ title: `Post ${n}` })) },
  }))
})

Caminho B na prática: dividindo o banco com o Laravel

  • Timestamps. O Eloquent preenche created_at e updated_at. No kastor, defaultNow() preenche o primeiro na criação e .updatedAt() preenche o segundo em cada create e update, mantendo a mesma convenção.
  • Soft deletes. Declare softDelete: 'deletedAt' nas tabelas que usam o trait, para que as leituras em TypeScript ignorem registros na lixeira como o Eloquent faz.
  • Autenticação. As sessões do Laravel são criptografadas com a APP_KEY e difíceis de compartilhar. Tokens são mais simples: o Sanctum guarda um hash SHA-256 de cada token, então a API TypeScript consegue validar os tokens emitidos pelo Laravel.
  • Conexões. Os workers do PHP-FPM e o pool do Node seguram conexões. Mantenha a soma abaixo do max_connections do servidor.
  • Relações polimórficas. morphTo guarda um nome de classe e um id sem chave estrangeira; consulte essas colunas com filtros diretos ou com $sql.
  • Hashes de senha. O Laravel grava bcrypt com o prefixo $2y$. Confira se a biblioteca bcrypt que você escolher no Node aceita esse prefixo antes de portar o login.

A versão em inglês deste guia traz a tabela completa de equivalências e um exemplo de validação de tokens do Sanctum em TypeScript. Para o resto do fluxo, veja o guia de migrações de banco de dados.

Perguntas frequentes

Q1Dá para usar o kastor dentro de um projeto Laravel?

Não. O kastor é uma biblioteca TypeScript e roda em runtimes JavaScript: Node.js, Bun, Deno e plataformas edge. Uma aplicação PHP continua usando o Eloquent. O que dá para fazer é rodar um serviço TypeScript ao lado do Laravel no mesmo banco, ou levar um serviço para TypeScript.

Q2Laravel e uma API Node.js podem dividir o mesmo banco?

Sim. Rode npx kastor db pull no banco do Laravel para ter um schema tipado, escolha um lado para cuidar das migrações e siga as convenções do Laravel no lado TypeScript: created_at, updated_at, deleted_at e os mesmos formatos JSON das colunas com cast.

Q3Qual é o equivalente do with() do Eloquent?

include. Post::with(['author', 'tags'])->withCount('comments') vira db.posts.findMany({ include: { author: true, tags: true, _count: { select: { comments: true } } } }), carregado em uma única instrução por chamada.

Q4Tem seeders e factories?

Sim. Um arquivo de seed exporta defineSeed(async (db) => ...) e roda com npx kastor db seed. factory(db.users, ({ seq }) => dados) cria registros de teste, e npx kastor migrate reset faz o papel do migrate:fresh --seed.

Suggestions

Kas looked under every log. Nothing matches that yet.

Try a method name, a database or a framework.