@rwillians/qx 0.1.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/LICENSE +21 -0
- package/README.md +230 -0
- package/dist/.gitignore +2 -0
- package/package.json +61 -0
package/LICENSE
ADDED
|
@@ -0,0 +1,21 @@
|
|
|
1
|
+
MIT License
|
|
2
|
+
|
|
3
|
+
Copyright (c) 2025 Rafael Willians
|
|
4
|
+
|
|
5
|
+
Permission is hereby granted, free of charge, to any person obtaining a copy
|
|
6
|
+
of this software and associated documentation files (the "Software"), to deal
|
|
7
|
+
in the Software without restriction, including without limitation the rights
|
|
8
|
+
to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
|
|
9
|
+
copies of the Software, and to permit persons to whom the Software is
|
|
10
|
+
furnished to do so, subject to the following conditions:
|
|
11
|
+
|
|
12
|
+
The above copyright notice and this permission notice shall be included in all
|
|
13
|
+
copies or substantial portions of the Software.
|
|
14
|
+
|
|
15
|
+
THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
|
|
16
|
+
IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
|
|
17
|
+
FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
|
|
18
|
+
AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
|
|
19
|
+
LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
|
|
20
|
+
OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
|
|
21
|
+
SOFTWARE.
|
package/README.md
ADDED
|
@@ -0,0 +1,230 @@
|
|
|
1
|
+
# qx (provisory name)
|
|
2
|
+
|
|
3
|
+
A teeny tiny ORM for TypeScript and JavaScript inspired by Elixir's
|
|
4
|
+
[Ecto](https://hexdocs.pm/ecto).
|
|
5
|
+
|
|
6
|
+
Built for you who wants a simple, small ORM that just works.
|
|
7
|
+
|
|
8
|
+
```ts
|
|
9
|
+
import * as sqlite from '@rwillians/qx/bun-sqlite';
|
|
10
|
+
import { create, from, into, table } from '@rwillians/qx';
|
|
11
|
+
|
|
12
|
+
const users = table('users', t => ({
|
|
13
|
+
id: t.integer({ primaryKey: true, autogenerate: true }),
|
|
14
|
+
name: t.string(),
|
|
15
|
+
email: t.string(),
|
|
16
|
+
createdAt: t.timestamp({ autogenerate: true }),
|
|
17
|
+
}));
|
|
18
|
+
|
|
19
|
+
// ...
|
|
20
|
+
|
|
21
|
+
const db = sqlite.connect('./db.sqlite');
|
|
22
|
+
|
|
23
|
+
await create.table(users).onto(db);
|
|
24
|
+
|
|
25
|
+
// ...
|
|
26
|
+
|
|
27
|
+
const rows = await into(users)
|
|
28
|
+
.insert({ name: 'John Doe', email: 'john.doe@gmail.com' })
|
|
29
|
+
.insert([ userA, userB, userC ])
|
|
30
|
+
.run(db);
|
|
31
|
+
|
|
32
|
+
// ...
|
|
33
|
+
|
|
34
|
+
const user = await from(users.as('u'))
|
|
35
|
+
.where(({ u }) => e.eq(u.id, 1))
|
|
36
|
+
.one(conn);
|
|
37
|
+
|
|
38
|
+
if (!user) {
|
|
39
|
+
throw new Error('User not found');
|
|
40
|
+
}
|
|
41
|
+
|
|
42
|
+
console.log(user);
|
|
43
|
+
```
|
|
44
|
+
|
|
45
|
+
- See [roadmap to v1.0.0](https://github.com/users/rwillians/projects/1/views/1).
|
|
46
|
+
- See more [examples](#examples).
|
|
47
|
+
|
|
48
|
+
|
|
49
|
+
# Vision
|
|
50
|
+
|
|
51
|
+
Here's the basics of what I need from an ORM, thus it's my priority to
|
|
52
|
+
build it first:
|
|
53
|
+
|
|
54
|
+
- [ ] Defining model fields should be very similar to defining a
|
|
55
|
+
schema with [Zod](https://zod.dev) (with support for validation
|
|
56
|
+
refiements and transformations).
|
|
57
|
+
- [ ] The model should have a schema compatible with
|
|
58
|
+
[standard schema](https://github.com/standard-schema/standard-schema),
|
|
59
|
+
meaning it should be interoperable with [Zod](https://zod.dev),
|
|
60
|
+
[ArkType](https://arktype.io), [Joi](https://joi.dev), etc.
|
|
61
|
+
- [ ] It should have a SQL-Like, type-safe, fluent query builder api
|
|
62
|
+
that works even for NoSQL databases¹, allowing us to write
|
|
63
|
+
queries once then use them with any supported database.
|
|
64
|
+
- [ ] The query builder should output a plain map representation of
|
|
65
|
+
the query that can be encoded to JSON, mostly for three reasons:
|
|
66
|
+
1. It's easy to test;
|
|
67
|
+
2. Makes it easier to debug queries; and
|
|
68
|
+
3. Makes `qx` more modular, allowing the community to build
|
|
69
|
+
their own extensions.
|
|
70
|
+
- [ ] The query results should be type-safe.
|
|
71
|
+
|
|
72
|
+
_¹ Some database adapters might not support all query features, that's
|
|
73
|
+
expected._
|
|
74
|
+
|
|
75
|
+
Once this vision is fullfilled, `qx` will become `v1.0.0`.
|
|
76
|
+
|
|
77
|
+
> [!NOTE]
|
|
78
|
+
> Migrations are not part of the scope yet, sorry.
|
|
79
|
+
> I don't like the way migrations work in most ORMs, so I'll take my
|
|
80
|
+
> time to figure out what coulde be a better way to do it.
|
|
81
|
+
|
|
82
|
+
|
|
83
|
+
## Components
|
|
84
|
+
|
|
85
|
+
The vision above implies the existence of four main components to this
|
|
86
|
+
library:
|
|
87
|
+
|
|
88
|
+
1. A table factory that outputs a model with a [standard schema](https://github.com/standard-schema/standard-schema);
|
|
89
|
+
2. A query builder that outputs a plain map representation of the
|
|
90
|
+
query;
|
|
91
|
+
3. A query engine that orchestrates the query execution using a
|
|
92
|
+
database adapter; and
|
|
93
|
+
4. Database adapters that can execute queries for a specific database
|
|
94
|
+
driver.
|
|
95
|
+
|
|
96
|
+
|
|
97
|
+
# Database Adapters
|
|
98
|
+
|
|
99
|
+
Database adapters are per driver implementation. Quex ships with a few
|
|
100
|
+
hand picked built-in database adapters:
|
|
101
|
+
|
|
102
|
+
- [ ] bun-sqlite3 (prioritary)
|
|
103
|
+
- [ ] bun-postgres
|
|
104
|
+
- [ ] mongodb
|
|
105
|
+
|
|
106
|
+
For community-built adapters, check GitHub's tag [#qx-adapter](https://github.com/topics/qx-adapter)
|
|
107
|
+
(you won't find anything there yet).
|
|
108
|
+
|
|
109
|
+
|
|
110
|
+
## Examples
|
|
111
|
+
|
|
112
|
+
Here are some examples that I'm using to guide the implementation.
|
|
113
|
+
|
|
114
|
+
**Define a table:**
|
|
115
|
+
```ts
|
|
116
|
+
// src/db/tables/backups.ts
|
|
117
|
+
import { z } from 'zod/v4';
|
|
118
|
+
import { type as arktype } from 'arktype';
|
|
119
|
+
import { defineColumn, create, table } from 'qx';
|
|
120
|
+
|
|
121
|
+
// custom types
|
|
122
|
+
const tc = {
|
|
123
|
+
absolutePath: () => defineColumn({
|
|
124
|
+
type: 'VARCHAR',
|
|
125
|
+
schema: z
|
|
126
|
+
.string()
|
|
127
|
+
.refine(str => str.startsWith('/'), "must be an absolute path")
|
|
128
|
+
.transform(str => str.endsWith('/') ? str.slice(0, -1) : str)
|
|
129
|
+
}),
|
|
130
|
+
bytes: () => defineColumn({
|
|
131
|
+
type: 'INTEGER',
|
|
132
|
+
schema: arktype('number.integer > 0'),
|
|
133
|
+
}),
|
|
134
|
+
email: () => defineColumn({
|
|
135
|
+
type: 'VARCHAR',
|
|
136
|
+
schema: z.string().email(),
|
|
137
|
+
}),
|
|
138
|
+
};
|
|
139
|
+
|
|
140
|
+
export const backups = table('backups', t => ({
|
|
141
|
+
id: t.integer().autoincrement().primaryKey(),
|
|
142
|
+
parentId: t.integer().nullable(),
|
|
143
|
+
state: t.enum(['succeeded', 'failed']).default('succeeded'),
|
|
144
|
+
path: tc.absolutePath(),
|
|
145
|
+
size: tc.bytes().nullable(),
|
|
146
|
+
notifyableContacts: tc.email().array().default([]),
|
|
147
|
+
// ↑ should be stored as VARCHAR[] in postgres
|
|
148
|
+
// should be stored as json encoded TEXT in sqlite
|
|
149
|
+
createdAt: t.datetime().default(() => new Date),
|
|
150
|
+
}));
|
|
151
|
+
|
|
152
|
+
export type Backup = typeof backups.infer;
|
|
153
|
+
export type BackupForInsert = typeof backups.inferForInsert;
|
|
154
|
+
export type BackupForUpdate = typeof backups.inferForUpdate;
|
|
155
|
+
```
|
|
156
|
+
|
|
157
|
+
**Create the table in the database:**
|
|
158
|
+
```ts
|
|
159
|
+
import * as sqlite from 'qx/bun-sqlite';
|
|
160
|
+
|
|
161
|
+
// ...
|
|
162
|
+
|
|
163
|
+
const db = sqlite.connect('./db.sqlite');
|
|
164
|
+
|
|
165
|
+
await create.table(backups).onto(db);
|
|
166
|
+
```
|
|
167
|
+
|
|
168
|
+
**Insert rows:**
|
|
169
|
+
```ts
|
|
170
|
+
import { into } from 'qx';
|
|
171
|
+
import * as sqlite from 'qx/bun-sqlite';
|
|
172
|
+
|
|
173
|
+
// ...
|
|
174
|
+
|
|
175
|
+
const db = sqlite.connect('./db.sqlite');
|
|
176
|
+
|
|
177
|
+
const rows = await into(backups)
|
|
178
|
+
.insert({ state: 'succeeded', path: '/data/backup_1.tar.gz', size: 104857600 })
|
|
179
|
+
.run(db);
|
|
180
|
+
```
|
|
181
|
+
|
|
182
|
+
**Query the table:**
|
|
183
|
+
```ts
|
|
184
|
+
import { expr, from } from 'qx';
|
|
185
|
+
import * as sqlite from 'qx/bun-sqlite';
|
|
186
|
+
|
|
187
|
+
const conn = sqlite.connect('./db.sqlite');
|
|
188
|
+
|
|
189
|
+
// ...
|
|
190
|
+
|
|
191
|
+
const yesterday = new Date(Date.now() - (24 * 60 * 60 * 1000));
|
|
192
|
+
|
|
193
|
+
const results = await from(backups.as('b1'))
|
|
194
|
+
.leftJoin(backups.as('b2'), ({ b1, b2 }) => expr.eq(b2.id, b1.parentId))
|
|
195
|
+
.where(({ b1, b2 }) => expr.and([
|
|
196
|
+
expr.eq(b1.state, 'failed'),
|
|
197
|
+
expr.gte(b1.failedAt, yesterday),
|
|
198
|
+
expr.eq(b1.scheduledBy, 'johndoe@gmail.com'),
|
|
199
|
+
]))
|
|
200
|
+
.orderBy(({ b1 }) => expr.desc(b1.scheduledAt))
|
|
201
|
+
.limit(25)
|
|
202
|
+
.offset(0)
|
|
203
|
+
.select(({ b1, b2 }) => ({
|
|
204
|
+
...b1,
|
|
205
|
+
parentPath: b2.path,
|
|
206
|
+
totalSizeMiB: expr.div(expr.add(b1.size, expr.coalesce(b2.size, 0)), 1048576),
|
|
207
|
+
}))
|
|
208
|
+
.all(db);
|
|
209
|
+
```
|
|
210
|
+
|
|
211
|
+
No singleton magic here! Not on my watch. You need to explicitly pass
|
|
212
|
+
the db connection to the query.
|
|
213
|
+
|
|
214
|
+
The results would look like this:
|
|
215
|
+
|
|
216
|
+
```ts
|
|
217
|
+
[
|
|
218
|
+
{
|
|
219
|
+
id: 2,
|
|
220
|
+
parentId: 1,
|
|
221
|
+
state: 'failed',
|
|
222
|
+
path: '/backups/20251130133100.tar.gz',
|
|
223
|
+
size: 104857600,
|
|
224
|
+
notifyableContacts: ['devops@ecma.com'],
|
|
225
|
+
createdAt: new Date('2025-11-30T13:31:00.000Z'),
|
|
226
|
+
parentPath: '/backups/20251030134200.tar.gz',
|
|
227
|
+
totalSizeMiB: 42069,
|
|
228
|
+
}
|
|
229
|
+
]
|
|
230
|
+
```
|
package/dist/.gitignore
ADDED
package/package.json
ADDED
|
@@ -0,0 +1,61 @@
|
|
|
1
|
+
{
|
|
2
|
+
"name": "@rwillians/qx",
|
|
3
|
+
"description": "A teeny tiny ORM for SQLite.",
|
|
4
|
+
"version": "0.1.0",
|
|
5
|
+
"author": "Rafael Willians <me@rwillians.com>",
|
|
6
|
+
"license": "MIT",
|
|
7
|
+
"repository": {
|
|
8
|
+
"type": "git",
|
|
9
|
+
"url": "git://github.com/rwillians/qx.git"
|
|
10
|
+
},
|
|
11
|
+
"keywords": [
|
|
12
|
+
"query",
|
|
13
|
+
"builder",
|
|
14
|
+
"query builder",
|
|
15
|
+
"sql",
|
|
16
|
+
"sqlite",
|
|
17
|
+
"typescript",
|
|
18
|
+
"type-safe",
|
|
19
|
+
"type-safety",
|
|
20
|
+
"orm",
|
|
21
|
+
"bun"
|
|
22
|
+
],
|
|
23
|
+
"type": "module",
|
|
24
|
+
"module": "dist/index.js",
|
|
25
|
+
"files": ["dist/", "LICENSE"],
|
|
26
|
+
"exports": {
|
|
27
|
+
".": {
|
|
28
|
+
"types": "./dist/types/index.d.ts",
|
|
29
|
+
"import": "./dist/esm/index.js",
|
|
30
|
+
"require": "./dist/cjs/index.js",
|
|
31
|
+
"default": "./dist/cjs/index.js"
|
|
32
|
+
},
|
|
33
|
+
"./bun-sqlite": {
|
|
34
|
+
"types": "./dist/types/bun-sqlite.d.ts",
|
|
35
|
+
"import": "./dist/esm/bun-sqlite.js",
|
|
36
|
+
"require": "./dist/cjs/bun-sqlite.js",
|
|
37
|
+
"default": "./dist/cjs/bun-sqlite.js"
|
|
38
|
+
},
|
|
39
|
+
"./pretty-logger": {
|
|
40
|
+
"types": "./dist/types/pretty-logger.d.ts",
|
|
41
|
+
"import": "./dist/esm/pretty-logger.js",
|
|
42
|
+
"require": "./dist/cjs/pretty-logger.js",
|
|
43
|
+
"default": "./dist/cjs/pretty-logger.js"
|
|
44
|
+
}
|
|
45
|
+
},
|
|
46
|
+
"scripts": {
|
|
47
|
+
"build": "bun run build:cjs && bun run build:esm",
|
|
48
|
+
"build:cjs": "bun run --bun tsc --project tsconfig-cjs.json",
|
|
49
|
+
"build:esm": "bun run --bun tsc --project tsconfig-esm.json"
|
|
50
|
+
},
|
|
51
|
+
"dependencies": {
|
|
52
|
+
"@standard-schema/spec": "^1.0.0",
|
|
53
|
+
"sql-highlight": "^6.1.0"
|
|
54
|
+
},
|
|
55
|
+
"devDependencies": {
|
|
56
|
+
"@types/bun": "latest"
|
|
57
|
+
},
|
|
58
|
+
"peerDependencies": {
|
|
59
|
+
"typescript": "^5.9.3"
|
|
60
|
+
}
|
|
61
|
+
}
|