@tango-ts/orm
v0.9.0
Published
Tango ORM: declarative models, managers, lazy querysets, Django-style lookups, compiled to Kysely (MySQL).
Downloads
3,450
Readme
@tango-ts/orm
Responsibility
The runtime ORM. Turns a declarative model definition into a typed, lazy query API
(Model.objects.filter(...).get(), .create(...)) and compiles it to SQL via
Kysely for MySQL. Owns field builders, the model/manager,
the lazy QuerySet, Django-style lookups, and the request-scoped connection. Does
not own migrations (schema diff/DDL) or serialization — those are sibling packages.
What it responds to
- A model declared with
model(name, { ...f.* fields }). - Filter objects (
Lookups<F>), insert objects (InferInsert<F>), inferred from the model by@tango-ts/core-types. - An active connection provided per request via
withConnection(db, () => ...).
Functionality
f.*— field builders (int,float,varchar,text,boolean,datetime,date,foreignKey) with.nullable(),.primaryKey(),.autoIncrement(),.unique(),.default(),.autoNow()/.autoNowAdd(),.choices([...])..choices()is Django'schoices: pure metadata (no DDL change) that narrows the field's TypeScript type to the literal union (f.varchar(20).choices(['draft', 'published'])types as'draft' | 'published'), makes serializers reject out-of-set values, emits an OpenAPIenum, and renders selects in the admin.f.foreignKeyaccepts{ dbConstraint: false }(Django'sdb_constraint=False) to keep the reference for joins/typing while skipping FOREIGN KEY DDL — required on PlanetScale (Vitess), which rejects FK constraints.model()/Manager—all,filter,exclude,get,count,create,update,delete,selectRelated.QuerySet— lazy + immutable; thenable (awaiting it runs the query);.compile()to SQL with no DB;.orderBy('name', '-createdAt'),.limit(n),.offset(n), and.count()(SQLCOUNT(*), with.compileCount()for assertions).- Lookups:
exact,in,isnull,gt/gte/lt/lte,contains,icontains,startswith,endswith(case-sensitivity matches Django on MySQL). - Relation traversal for FK fields by convention:
authorIdexposesauthor, soPost.objects.filter({ author__email__icontains: 'x' })compiles to a join. - Nested FK traversal:
Book.objects.filter({ author__organization__name: 'Labs' }). - Explicit reverse relations via
r.hasMany, e.g.Organization.objects.filter({ authors__name__icontains: 'ada' }). selectRelated('author')andselectRelated('author__organization')eager-load FK targets and inflate joined columns into nested objects on each result row.withConnection/getConnection/createMysqlConnection, andCOMPILE_ONLY.atomic(fn)— runs ORM work inside a transaction scoped to the current connection.mysqlConfigFromEnv(options?, env?)— the single resolution path for database configuration: explicit options >TANGO_DATABASE_URL/DATABASE_URL>TANGO_DB_*variables > development defaults. Supports TLS (TANGO_DB_SSL) and pool sizing (TANGO_DB_POOL_SIZE), and refuses development defaults whenNODE_ENV=production.
Design patterns that matter here
- Inferred types (P2):
Manager/QuerySetcarryInferSelect/Lookupsfrom the model. Relation lookups andselectRelatedrow shapes are inferred from FK targets. Never accept or return a hand-written row type. - Lazy + immutable: building never executes; chaining returns new QuerySets.
- Serverless (P5): the connection is request-scoped via
AsyncLocalStorage; no module-level mutable connection.COMPILE_ONLYis pure/immutable, so it is allowed. - Atomic transactions:
atomic(...)rebinds the request-scoped connection to a Kysely transaction for the callback, so normal manager/queryset calls participate. - Declarative (P4): the public surface is builders + config. Internal Kysely usage
is the one place we bridge to a loosely-typed DB, isolated in
connection.ts/queryset.ts. - No
any: the internal bridge usesunknown-typedLooseDatabase, neverany.
Public contract
Everything re-exported from src/index.ts. The internal LooseDatabase bridge is
exported for adapters/tests but is not the user-facing API.
Testing
- Unit (
test/queryset.test.ts): asserts compiled SQL + parameters for each lookup, nested FK join, reverse join, and selected relation join, usingCOMPILE_ONLY(no DB). - Type-level (
test/model.test-d.ts): assertsobjects.filter/createinference and that invalid lookups / wrong value types / unknown nested or reverse relations fail to compile. - Integration (
test/db.integration.test.ts): real MySQL — create, filter, get, update/delete helpers, transaction commit/rollback, nested relation traversal, reverse relation traversal, nestedselectRelated, thenable execution. Never skips when the DB is down; it fails loudly.
