4 ms·
This looks great. But in my view there's still some scope for improvement in expressive-ness (compared to something like LINQ). A future area of exploration co
by jeswin 2y ago
This looks great.
But in my view there's still some scope for improvement in expressive-ness (compared to something like LINQ). A future area of exploration could be to add a parser to the mix, so that we can write:
const jane = db.contacts.one(c => c.email === janeEmail);
instead of:
const jane = db.one<Contact>($.from(Contact).where(c => $`${c.email} = ${janeEmail}`))!
The former is so naturally typesafe, without having to resort to advanced typescript tricks. I got half way there [1], before dropping the project due to work pressure (but many years back, so the code is quite stale). Parsers were viable with caching back then. These days, performance is not going to be problem whatever you do. If anyone is interested, happy to exchange notes.
[1]: https://github.com/isotropy/isotropy-db/blob/master/src/test/test.ts https://github.com/isotropy/isotropy-db/blob/master/src/test...
- mythz 2y agoIt's an intentional decision that drivers are decoupled from the Application and query builder it executes, i.e. all driver APIs provide different ways of executing SQL + Params or a function that returns SQL + Params (Query Builder). This allows drivers to remain flexible to executing (non-litdb) SQL/Params or Query builders from a different library. Your proposed API would require augmenting the driver with an application schema which would couple drivers to both litdb implementations and an application's schema where it would no longer be suitable for querying outside of your Application schema. Also not a fan of global Application schemas, i.e. litdb classes are self encapsulating so you could create a class at runtime and execute it without having to register it with a global application schema. With that said you could implement a similar API to what you want with something like: class AppDb { constructor(public $:ReturnType<typeof Sql.create>) {} contacts() { return this.$.from(Contact) } } const app = new AppDb($) Which you could then use like: db.one(app.contacts.where(c => $`${c.email} = ${janeEmail}`)) I also prefer APIs to be SQL-like (i.e. instead of inventing a custom object model) where APIs and Typed Query Builders designed around SQL so it's intuitive how to construct queries and what SQL a query would execute. At the moment there's a clear separation of Query Builders which generates SQL + Params and the drivers which just executes them. BTW you don't need the generic type if you only have "1 table without a custom select", i.e. this will implicitly return a Contact instance: const jane = db.one($.from(Contact).where(c => $`${c.email} = ${janeEmail}`)) Specifying `one<Contact>` generic type is similar to `as Contact` inert type hint, i.e. it informs TS what the shape of the returned object is but doesn't change behavior, it's preferable to use `.into(Contact)` which explicitly returns a `Contact` instance. const jane = db.one($.from(Contact).where(c => $`${c.email} = ${janeEmail}`).into(Contact))
- jeswin 2y ago> It's an intentional decision that drivers are decoupled from the Application and query builder it executes, i.e. all driver APIs provide different ways of executing SQL + Params or a function that returns SQL + Params (Query Builder). This allows drivers to flexible to exec (non-litdb) SQL/Params or Query builders from a different library. But my suggestion doesn't couple the driver and the Application/query builder. Here's a simplified example. const db = { // table() is a factory that returns (for example) PostgresTable // or MySQLTable or even MongoTable based on config customers: table() } This has some other interesting properties - for example, you could stub these tables with pure JS lists for tests. The broad applicability of this approach has been proven to work by EF being the defacto DB access method in the .Net world (over 15 years now).
- mythz 2y ago> The broad applicability of this approach has been proven to work by EF being the defacto DB access method in the .Net work (over 15 years now). It's only the defacto DB access method in .NET because that's what Microsoft's EF library chose and .NET ecosystem just uses the default MS option but EF's influence doesn't extend outside of .NET. AFAIK no other .NET ORM does this including our .NET ORM [1] which litdb is the spiritual port of. [1] https://docs.servicestack.net/ormlite/ https://docs.servicestack.net/ormlite/
- jeswin 2y agoThe approach has more to do with C# the language, and the Code-as-Data principles it explored and executed well. I had seen various ORMs starting from the early 2000s (Hibernate etc), and when C# added Code-as-Data into the mix my jaw dropped. Like a bit of Lisp in a mainstream language, with mainstream applicability. Nothing wrong with your approach, I was just arguing that language-native query patterns (such as customers.filter(c => c.country === "Chile")) can be appealing. And at the same time, reachable with current JS/TS tooling. But like you said, it may not be the direction litdb wants to go.
- maxloh 2y agoThat won't be possible without a lot of work. You'll need to tweak the TypeScript compiler to make the parser comprehend exported variables. For example, import { contacts } from "db" const jane = contacts.one(c => c.email === janeEmail) Marco-like magic can be confusing, as we learned from Svelte. Especially when it's half-baked like the one you are suggesting. Another drawback is that you'll need to compile the code twice to run it: once with the custom parser and once with the TypeScript compiler.
- jeswin 2y ago> Marco-like magic can be confusing, as we learned from Svelte. Especially when it's half-baked like the one you are suggesting. When I'm giving examples on a forum thread, I try to provide one that's simpler to understand and captures the essence of the argument. I am not making any claims on completeness. > Marco-like magic can be confusing, as we learned from Svelte. Especially when it's half-baked like the one you are suggesting. Svelte invents new syntax, which in my view isn't great. This is pure JS, and it could even run in a browser against arrays (for tables). Add: > That won't be possible without a lot of work. You'll need to tweak the TypeScript compiler to make the parser comprehend exported variables. Not required. I was suggesting that the expression could be parsed at runtime. There are various options, with different tradeoffs.
- maxloh 2y ago> Not required. I was suggesting that the expression could be parsed at runtime. There are various options, with different tradeoffs. Good idea. That might be possible, though not every JavaScript runtime implements Function.prototype.toString. I'm not sure if Node does. const aFunction = arg => arg.a === arg.b const aFunctionSrc = aFunction.toString() console.log(aFunctionSrc) // 'arg => arg.a === arg.b' const transformedFunctionSrc = transform(aFunctionSrc) const transformedFunction = new Function(transformedFunctionSrc) console.log(transformedFunction) // [Function: anonymous] // Cache the transformed function in a WeakMap // WeakMap values are garbage collected when their keys are garbage collected const functionMap = new WeakMap() functionMap.set(aFunction, transformedFunction) console.log(functionMap.get(aFunction)) // [Function: anonymous]