Package Exports
- @operativa/verse
- @operativa/verse/conventions/convention
- @operativa/verse/conventions/database
- @operativa/verse/conventions/model
- @operativa/verse/db/driver
- @operativa/verse/db/optimizer
- @operativa/verse/db/ordering
- @operativa/verse/db/printer
- @operativa/verse/db/rewriter
- @operativa/verse/db/schema
- @operativa/verse/db/semantics
- @operativa/verse/db/sql
- @operativa/verse/db/visitor
- @operativa/verse/identity/generator
- @operativa/verse/identity/identity
- @operativa/verse/identity/seqhilo
- @operativa/verse/identity/uuid
- @operativa/verse/inheritance/sti
- @operativa/verse/inheritance/strategy
- @operativa/verse/model/binder
- @operativa/verse/model/builder
- @operativa/verse/model/model
- @operativa/verse/model/rewriter
- @operativa/verse/model/validator
- @operativa/verse/model/visitor
- @operativa/verse/query/compiler
- @operativa/verse/query/eager
- @operativa/verse/query/expression
- @operativa/verse/query/printer
- @operativa/verse/query/queryable
- @operativa/verse/query/rewriter
- @operativa/verse/query/shaping
- @operativa/verse/test/db/creation
- @operativa/verse/test/identity/seqhilo
- @operativa/verse/test/infra
- @operativa/verse/test/mapping/conditions
- @operativa/verse/test/mapping/conversion
- @operativa/verse/test/mapping/inheritance
- @operativa/verse/test/mapping/objects
- @operativa/verse/test/mapping/relationships
- @operativa/verse/test/mapping/shadow
- @operativa/verse/test/mapping/value
- @operativa/verse/test/query/navigations
- @operativa/verse/test/query/query
- @operativa/verse/test/query/raw
- @operativa/verse/test/query/with
- @operativa/verse/test/query/with.complex
- @operativa/verse/test/update/cascade
- @operativa/verse/test/update/optimistic
- @operativa/verse/test/update/uow
- @operativa/verse/test/update/uuids
- @operativa/verse/uow
- @operativa/verse/utils/check
- @operativa/verse/utils/logging
- @operativa/verse/utils/utils
Readme
Verse: Powerful TypeScript Data Access
Introduction
Verse is a new data access package for TypeScript, developed by Operativa. Sparked by a dissatisfaction with the popular data access options available in the JavaScript ecosystem, Verse aims to provide the JS community with a robust and efficient way to work with data in their applications. It borrows from the best practices of popular O/RMs on other platforms like Entity Framework Core for .NET, while also taking advantage of the unique features of TypeScript.
Features Overview
- Type-Safe: LINQ-like queries, authored in pure TypeScript with full type-checking and tooling support.
- Efficient: Queries can be compiled and are async iterable. Updates use Unit of Work with change tracking, pipelining and partial updates.
- Safe: SQL is always parameterized and escaped, so you don't have to worry about SQL injection attacks.
- Flexible: Supports a variety of database drivers, and can be used with any database that has a Node.js driver.
- Powerful: Rich query operators, eager-loading, identity generation, schema creation, change-tracking, metadata model, and more.
Getting Started
1. Declare Entities
Entities are classes corresponding to the business objects in you application domain. Here we are modelling a music database, so we will create two entities: Artist and Album:
class Artist {
constructor(
readonly artistId: number,
public name: string,
readonly albums: Album[]
) {}
}
class Album {
readonly albumId: number;
constructor(
public title: string,
public artistId: number
) {}
}2. Create a Verse Object
A "verse" is the top-level object that we use to interact with our data. It holds our configuration, like the database driver we want to use, and our model, which is a collection of entity models, specified via a type-safe DSL API.
const verse = verse({
config: { driver: new SqliteDriver("Chinook.sqlite") },
model: {
artists: entity(Artist, a => {
a.properties({
artistId: int(),
name: string(),
albums: many(Album),
});
}),
albums: entity(Album, a => {
a.properties({
albumId: int(),
title: string(),
artistId: int(),
});
}),
},
});💡 Tip: We are using the open-source Chinook database for our example. You can download it here.
3. Our First Query
Now that we have our entities and our verse object, we can start querying our database. Let's get all of the albums in our database:
const albums = await verse.from.albums.toArray();which produces:
[
{
albumId: 1,
title: "For Those About To Rock We Salute You",
artistId: 1,
},
{
albumId: 2,
title: "Balls to the Wall",
artistId: 2,
},
//...
];💡 Tip: We use
toArrayto buffer results. Verse queries are also async iterable, so we can usefor awaitto asynchronously stream results.
How many albums are there?
const count = await verse.from.albums.count();which produces:
347;And the SQL that Verse used to realise this query:
select count(*)
from (
select "t0"."AlbumId", "t0"."Title", "t0"."ArtistId"
from "Album" as "t0"
) as "t1"💡 Tip: Our generated SQL is not being optimized yet. We are working on it!
Verse supports many query operators,
like where, select, orderBy, offset, limit, groupBy, join, any, all, first, and more. You can
find a complete list of operators in the API Reference.
For example:
const albums = await verse.from.albums
.where(a => a.title.like("A%"))
.select(a => a.title)
.orderBy(t => t)
.offset(10)
.limit(5)
.toArray();which produces:
[
"Acústico MTV",
"Acústico MTV [Live]",
"Adams, John: The Chairman Dances",
"Adorate Deum: Gregorian Chant from the Proper of the Mass",
"Afrociberdelia",
];What if we want to query an Artist with their associated Albums? Verse supports eager-loading via the with query operator:
const miles = await verse.from.artists
.with(a => a.albums)
.where(a => a.name === "Miles Davis")
.toArray();which produces:
[
{
artistId: 68,
name: "Miles Davis",
albums: [
{
albumId: 48,
title: "The Essential Miles Davis [Disc 1]",
artistId: 68,
},
{
albumId: 49,
title: "The Essential Miles Davis [Disc 2]",
artistId: 68,
},
{
albumId: 157,
title: "Miles Ahead",
artistId: 68,
},
],
},
];What about query parameters? Verse supports a couple of methods of query parameterization:
The first is Compiled Queries, which is a way to pre-compile a query and then execute it multiple times with different parameters. Compiled queries are efficient because the cost of processing the query is only incurred once, and the query can be cached for future use:
const query = verse.compile((from, $title: string) =>
from.albums
.where(a => a.title == $title)
.select(a => `Title: ${a.title}!`)
.single()
);
const result = await query("Miles Ahead");which produces:
"Title: Miles Ahead!";The second is Operator-Local Parameters, which is a way to parameterize a query operator in-place, avoiding the need to create a compiled query. This is useful for simple or infrequently run queries that you don't want to create a compiled query for:
const title = "Miles Ahead";
const result = await verse.from.albums
.where((a, $title: string) => a.title === $title, title)
.select(a => `Title: ${a.title}!`)
.single();💡 Tip: Queries produced by Verse are always parameterized (and prepared where possible), so you don't have to worry about SQL injection attacks.
Updating Data
Verse supports the Unit of Work pattern for updates, and will automatically track changes to entities that you load from the database. For example:
const uow = verse.uow();
const album = await uow.albums.where(a => a.title === "Miles Ahead").single();
album.title = "Miles Ahead - Remastered";
await uow.commit();When we call commit, Verse will generate the necessary SQL to update the database:
-- Executing SQL: Parameters: [$1='Miles Ahead - Remastered', $2=68, $3=157]
update "Album" set "Title" = ?, "ArtistId" = ? where "AlbumId" = ?We can also create new objects:
const album = new Album("Miles Ahead - Remastered", 68);
const uow = verse.uow();
await uow.albums.add(album);
await uow.commit();and remove existing ones:
const uow = verse.uow();
const album = await uow.albums.where(a => a.title === "Miles Ahead - Remastered").single();
uow.albums.remove(album);
await uow.commit();💡 Tip: Unit of work commits are transactional, so if an error occurs during the commit, the transaction will be rolled back.
Installation
TODO!
Development Status
Please note that "Verse" is in its early development and the presented version is an alpha release. It's intended to be a preview for select developers. We appreciate bug reports and improvement suggestions as we continue to refine and enhance this library.
Contribution Guidelines
To learn how you can contribute to this project, please check our Contribution Guide.
Versioning
TODO
License
Our project is released under the MIT license. For more information, see the LICENSE file in our repository.