AST Approach
Why AST?
Section titled “Why AST?”Because it separates query construction from execution. The core defines what to query, the provider defines how to execute it. This enables multi-database and edge compatibility without API changes.
Your code → Client (QueryBuilder → AST) → Provider (Compiler → SQL) → DatabaseWhy not classes?
Section titled “Why not classes?”Classes promise methods that later cause confusion (User.find(), User.delete()). JSORM keeps models as metadata only and leaves queries to jsorm.users.get().
Instance methods mutate state (unpredictable), while pure functions create new instances (deterministic).
Why not include?
Section titled “Why not include?”include is ambiguous. It doesn’t say what columns or filters the nested relation has. Relations live as nested objects in the query:
// Correct — explicitselect: { id: true, name: true, role: { name: true }}
// Incorrect — ambiguousinclude: { role: true }It is explicit, typeable, and consistent with select and where.
Why relations inside the model?
Section titled “Why relations inside the model?”Relations are properties of the model, not the query. User.hasMany("permission") says “this user has many permissions”, which is a model property. In the query you only specify which relations to include and how.
Why typed strings?
Section titled “Why typed strings?”Model fields are referenced as strings (where: { active: true }). Using imports or symbols loses serialization, diffing, and relational database compatibility.
Why no embedded SQL?
Section titled “Why no embedded SQL?”SQL is an implementation detail of each provider. If you exposed it in the API, each provider would have its own syntax and you’d lose portability between SQLite and PostgreSQL.
Why doesn’t the Core execute anything?
Section titled “Why doesn’t the Core execute anything?”Because the core must work in any environment. If it executes queries, opens connections, or generates SQL, it depends on Node, the filesystem, or database drivers. Separating metadata from execution enables pure edge compatibility.
Why separate Providers?
Section titled “Why separate Providers?”Because each database has a different driver, SQL dialect, and connection manager. A SQLite provider doesn’t need the pg driver or PostgreSQL compiler. Each can have its own dependencies without affecting others.
Why Edge-first?
Section titled “Why Edge-first?”Because JSORM is a query ORM, not a database admin ORM. Migrations and CLI can use Node, but queries must work where data is used: at the edge, in the browser, in a worker.
Why db: "main" in models?
Section titled “Why db: "main" in models?”Because an application can have multiple databases. The model explicitly indicates which one it belongs to, and the client automatically resolves the correct provider for each model.
Why not db.use()?
Section titled “Why not db.use()?”Because the database is defined in the config, not the client. defineConfig({ databases: { main: { provider: "..." } } }) already indicates where the model lives.
Why defineConfig with env strings?
Section titled “Why defineConfig with env strings?”Because the client does not resolve environment variables. Each string in options is a variable name that the internal provider resolves. This keeps the client pure and edge-compatible.
Why naming in config?
Section titled “Why naming in config?”Because the ORM must know how to translate model and field names to SQL without guessing. naming.fields controls columns, naming.tables controls tables. This prevents inconsistencies between TypeScript code and the actual database.
