@elysiumjs/drizzle 0.0.0-stage → 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/CHANGELOG.md +13 -0
- package/README.md +252 -2
- package/dist/bun-require.d.ts +7 -0
- package/dist/cache.d.ts +60 -0
- package/dist/cerberus.d.ts +36 -0
- package/dist/commands/migration_generate.command.d.ts +14 -0
- package/dist/commands/migration_run.command.d.ts +26 -0
- package/dist/commands/project.d.ts +11 -0
- package/dist/commands/schema.d.ts +48 -0
- package/dist/database.d.ts +137 -0
- package/dist/driver.d.ts +53 -0
- package/dist/drivers/bun-sql.d.ts +25 -0
- package/dist/drivers/bun-sql.js +1 -0
- package/dist/drivers/hyperdrive.d.ts +46 -0
- package/dist/drivers/hyperdrive.js +1 -0
- package/dist/drivers/node-postgres.d.ts +23 -0
- package/dist/drivers/node-postgres.js +1 -0
- package/dist/drivers/postgres-js.d.ts +25 -0
- package/dist/drivers/postgres-js.js +1 -0
- package/dist/drizzle.module.d.ts +40 -0
- package/dist/hermes.d.ts +170 -0
- package/dist/index.d.ts +20 -0
- package/dist/index.js +16 -0
- package/dist/lock.d.ts +33 -0
- package/dist/migration-clients.d.ts +37 -0
- package/dist/migration.d.ts +120 -0
- package/dist/migrations.d.ts +45 -0
- package/dist/model.d.ts +62 -0
- package/dist/moirai.d.ts +622 -0
- package/dist/pithos.d.ts +301 -0
- package/dist/query.d.ts +28 -0
- package/dist/repository.d.ts +20 -0
- package/dist/table-markers.d.ts +97 -0
- package/dist/table.d.ts +69 -0
- package/dist/tenancy.d.ts +105 -0
- package/package.json +102 -5
package/CHANGELOG.md
ADDED
|
@@ -0,0 +1,13 @@
|
|
|
1
|
+
# @elysiumjs/drizzle
|
|
2
|
+
|
|
3
|
+
## 0.1.0
|
|
4
|
+
|
|
5
|
+
### Minor Changes
|
|
6
|
+
|
|
7
|
+
- [#112](https://github.com/na2axl/elysiumjs/pull/112) [`72f76ff`](https://github.com/na2axl/elysiumjs/commit/72f76ff2f514aa15dce74d2bc29f0f8429cf5023) Thanks [@na2axl](https://github.com/na2axl)! - `@elysiumjs/drizzle` provides Drizzle ORM for `@elysiumjs/olympus`: database drivers (Bun SQL, postgres.js, node-postgres, Hyperdrive), models, repositories, migrations, tenancy, the moirai stores, the notification and file stores, `rulesToDrizzle()` for abilities as SQL conditions, and the `postgres` lock driver. `@elysiumjs/pino` provides the `pino` logger driver, with pretty output in development.
|
|
8
|
+
|
|
9
|
+
### Patch Changes
|
|
10
|
+
|
|
11
|
+
- Updated dependencies [[`72f76ff`](https://github.com/na2axl/elysiumjs/commit/72f76ff2f514aa15dce74d2bc29f0f8429cf5023), [`72f76ff`](https://github.com/na2axl/elysiumjs/commit/72f76ff2f514aa15dce74d2bc29f0f8429cf5023), [`72f76ff`](https://github.com/na2axl/elysiumjs/commit/72f76ff2f514aa15dce74d2bc29f0f8429cf5023)]:
|
|
12
|
+
- @elysiumjs/olympus@0.25.0
|
|
13
|
+
- @elysiumjs/workers@0.1.0
|
package/README.md
CHANGED
|
@@ -1,3 +1,253 @@
|
|
|
1
|
-
#
|
|
1
|
+
# @elysiumjs/drizzle
|
|
2
2
|
|
|
3
|
-
|
|
3
|
+
The Drizzle ORM backend of [Elysium.js](https://github.com/na2axl/elysiumjs): database connections and drivers for `@elysiumjs/olympus/mnemosyne` (`/bun-sql`, `/postgres-js`, `/node-postgres` and `/hyperdrive` subpaths), models, repositories, queries, migrations and tenancy, the Postgres stores of `@elysiumjs/olympus/moirai`, and the `postgres` driver of the `Lock` port.
|
|
4
|
+
|
|
5
|
+
## Installation
|
|
6
|
+
|
|
7
|
+
```bash
|
|
8
|
+
bun add @elysiumjs/olympus @elysiumjs/drizzle
|
|
9
|
+
```
|
|
10
|
+
|
|
11
|
+
## Usage
|
|
12
|
+
|
|
13
|
+
Add `DrizzleModule` to your application. It declares the connections as global services, registers the tenancy strategy and provides the `migration:generate` and `migration:run` commands.
|
|
14
|
+
|
|
15
|
+
```ts
|
|
16
|
+
import { DrizzleModule } from '@elysiumjs/drizzle';
|
|
17
|
+
import { Application } from '@elysiumjs/olympus';
|
|
18
|
+
|
|
19
|
+
@Application.register({
|
|
20
|
+
modules: [
|
|
21
|
+
DrizzleModule.with({
|
|
22
|
+
default: 'main',
|
|
23
|
+
connections: {
|
|
24
|
+
main: { connection: process.env.DATABASE_URL! }
|
|
25
|
+
}
|
|
26
|
+
})
|
|
27
|
+
]
|
|
28
|
+
})
|
|
29
|
+
export class App extends Application {}
|
|
30
|
+
```
|
|
31
|
+
|
|
32
|
+
`with()` also accepts a function, which is called when the module is booted: `DrizzleModule.with(() => ({ ... }))`. Set `tenancy` to switch between schema (default) and RLS tenancy.
|
|
33
|
+
|
|
34
|
+
Define models with the enhanced `pgTable`:
|
|
35
|
+
|
|
36
|
+
```ts
|
|
37
|
+
import { DrizzleModel, pgTable } from '@elysiumjs/drizzle';
|
|
38
|
+
import { Repository } from '@elysiumjs/olympus/mnemosyne';
|
|
39
|
+
import { uuid, varchar } from 'drizzle-orm/pg-core';
|
|
40
|
+
|
|
41
|
+
const users = pgTable('users', {
|
|
42
|
+
id: uuid().primaryKey().defaultRandom(),
|
|
43
|
+
name: varchar({ length: 255 }).notNull()
|
|
44
|
+
});
|
|
45
|
+
|
|
46
|
+
export class UserModel extends DrizzleModel(users, { supportTenancy: true }) {}
|
|
47
|
+
export class UserRepository extends Repository(UserModel) {}
|
|
48
|
+
```
|
|
49
|
+
|
|
50
|
+
`Repository(Model)` comes from `@elysiumjs/olympus/mnemosyne` and runs on the Drizzle repository adapter for the models of `DrizzleModel()`: `this.db` is the Drizzle connection (or the transaction open on it), and the scope of `list()`, `count()`, `deleteWhere()` and `Pruner` is an SQL condition. Statements honour the tenant isolation of row-level security; a custom query that must too goes through `this.withConnection((db) => ...)`.
|
|
51
|
+
|
|
52
|
+
The repository finds rows by the table's primary key. A table without an `id` column, or with a composite primary key, takes the key as an object; `key` names other columns to use instead:
|
|
53
|
+
|
|
54
|
+
```ts
|
|
55
|
+
export class MembershipRepository extends Repository(MembershipModel) {}
|
|
56
|
+
await memberships.find({ teamId, userId });
|
|
57
|
+
|
|
58
|
+
export class SessionRepository extends Repository(SessionModel, { key: ['token'] }) {}
|
|
59
|
+
await sessions.find('abc123');
|
|
60
|
+
```
|
|
61
|
+
|
|
62
|
+
`list(query, scope)` and `count(query, scope)` take a condition the rows must also meet, on top of the filters of the query:
|
|
63
|
+
|
|
64
|
+
```ts
|
|
65
|
+
public listFor(userId: string, query: ParsedQuery<'cursor'>) {
|
|
66
|
+
return this.list(query, eq(posts.authorId, userId));
|
|
67
|
+
}
|
|
68
|
+
```
|
|
69
|
+
|
|
70
|
+
`deleteWhere(scope, limit)` deletes at most `limit` rows of a scope in one statement, partitioned tables included, which is how `Pruner(Model)` jobs delete stale rows chunk by chunk.
|
|
71
|
+
|
|
72
|
+
### Migration hooks
|
|
73
|
+
|
|
74
|
+
`migration:run --tenant <name>` applies the pending migrations of a tenant in one transaction, under a lock per tenant. It works on the connection given by `--connection` (`default` when omitted), whose driver runs the migrations: `bun-sql`, `postgres-js` and `node-postgres` do, and `hyperdrive` hands them to the driver it wraps, which needs its binding. A custom driver runs them by implementing `migrations(connection)`, with `templateClientMigrations()` or `nodePostgresMigrations()` for clients like those.
|
|
75
|
+
|
|
76
|
+
A migration hook runs SQL around the migrations, in the same transaction: when a hook fails, the whole run is rolled back. Register it with `@Migration.hook()` and list it in the `services` of a module. Every method is optional:
|
|
77
|
+
|
|
78
|
+
```ts
|
|
79
|
+
import type { MigrationHook, MigrationRun, MigrationStep } from '@elysiumjs/drizzle';
|
|
80
|
+
|
|
81
|
+
import { Migration, sql } from '@elysiumjs/drizzle';
|
|
82
|
+
|
|
83
|
+
@Migration.hook()
|
|
84
|
+
export class DatabaseHook implements MigrationHook {
|
|
85
|
+
public async before({ db }: MigrationRun) {
|
|
86
|
+
await db.execute(sql`CREATE EXTENSION IF NOT EXISTS pgcrypto`);
|
|
87
|
+
}
|
|
88
|
+
|
|
89
|
+
public async beforeEach({ name }: MigrationStep) {}
|
|
90
|
+
|
|
91
|
+
public async afterEach({ db, name }: MigrationStep) {
|
|
92
|
+
const [{ count }] = await db.execute<{ count: number }>(
|
|
93
|
+
sql`SELECT count(*)::int AS count FROM pg_constraint WHERE ...`
|
|
94
|
+
);
|
|
95
|
+
if (count > 0) throw new Error(`${name} adds a foreign key across schemas`);
|
|
96
|
+
}
|
|
97
|
+
|
|
98
|
+
public async after({ db, tenant }: MigrationRun) {
|
|
99
|
+
await db.execute(sql`GRANT USAGE ON SCHEMA ${sql.identifier(tenant)} TO app`);
|
|
100
|
+
}
|
|
101
|
+
}
|
|
102
|
+
```
|
|
103
|
+
|
|
104
|
+
- `before` runs once the tenant schema and the migrations table exist, and `after` at the end. Both also run when no migration is pending, so `after` fits what must stay in place, such as grants or partitions.
|
|
105
|
+
- `beforeEach` runs before each migration, and `afterEach` once it is recorded.
|
|
106
|
+
- A `MigrationRun` holds the transaction (`db`), the `tenant`, the `connection`, the `batch`, the `pending` migrations and the ones `applied` so far. A `MigrationStep` holds the transaction, the `name` and `path` of the migration, and its `run`.
|
|
107
|
+
- `db.execute(query)` runs one statement written with the `sql` template of Drizzle and returns its rows. `db.script(text)` runs several statements without parameters.
|
|
108
|
+
|
|
109
|
+
Hooks run for every tenant, in the order the modules register them: read `tenant` to act on some tenants only.
|
|
110
|
+
|
|
111
|
+
### Table markers
|
|
112
|
+
|
|
113
|
+
`DrizzleTable.define()` marks a table with what `migration:generate` adds to the migration that creates it. Later migrations are left as they are.
|
|
114
|
+
|
|
115
|
+
```ts
|
|
116
|
+
import { DrizzleTable } from '@elysiumjs/drizzle';
|
|
117
|
+
import { pgSchema, primaryKey, text, timestamp, uuid } from 'drizzle-orm/pg-core';
|
|
118
|
+
|
|
119
|
+
const kernel = pgSchema('kernel');
|
|
120
|
+
|
|
121
|
+
export const audit = DrizzleTable.define(
|
|
122
|
+
DrizzleTable.partitioned({ range: 'recorded_at' }),
|
|
123
|
+
DrizzleTable.afterCreate(
|
|
124
|
+
({ qualified }) =>
|
|
125
|
+
`CREATE TRIGGER audit_append_only BEFORE UPDATE OR DELETE ON ${qualified} FOR EACH ROW EXECUTE FUNCTION forbid_change()`
|
|
126
|
+
)
|
|
127
|
+
)(
|
|
128
|
+
kernel.table(
|
|
129
|
+
'audit',
|
|
130
|
+
{
|
|
131
|
+
id: uuid('id').notNull().defaultRandom(),
|
|
132
|
+
action: text('action').notNull(),
|
|
133
|
+
recordedAt: timestamp('recorded_at').notNull().defaultNow()
|
|
134
|
+
},
|
|
135
|
+
(t) => [primaryKey({ columns: [t.id, t.recordedAt] })]
|
|
136
|
+
)
|
|
137
|
+
);
|
|
138
|
+
```
|
|
139
|
+
|
|
140
|
+
- `DrizzleTable.partitioned({ range })` adds `PARTITION BY RANGE (...)` to the `CREATE TABLE`. `range` is a column name in the database, a function returning the column (`(t) => t.recordedAt`), or several of them. The primary key and unique constraints must include those columns.
|
|
141
|
+
- `DrizzleTable.afterCreate(build)` adds SQL right after the table is created, as its own statement. `build` receives the `schema` (`undefined` for `public`), the `name` and the quoted, schema-qualified `qualified` name, and returns a string or a `sql` template.
|
|
142
|
+
- `DrizzleTable.define()` returns the table it marks, so its type is unchanged. Marking a table again adds to its markers.
|
|
143
|
+
|
|
144
|
+
## Moirai tables
|
|
145
|
+
|
|
146
|
+
```ts
|
|
147
|
+
import { moiraiTables } from '@elysiumjs/drizzle';
|
|
148
|
+
|
|
149
|
+
// In the Drizzle schema, so that migration:generate creates them.
|
|
150
|
+
export const billing = moiraiTables('billing');
|
|
151
|
+
```
|
|
152
|
+
|
|
153
|
+
`moiraiTables(schema?, options?)` builds the `idempotency_keys`, `outbox`, `inbox`, `events` and `projections` tables, in a Postgres schema when one is given (one set of tables per domain schema, for example).
|
|
154
|
+
|
|
155
|
+
With `{ partitioned: true }`, `events` is partitioned by range of `occurred_at` and `outbox` by range of `created_at`. Their primary and unique keys then include that column, and `migration:generate` creates them with `PARTITION BY RANGE`. The partitions are up to the app: create them in a migration hook, or add them to the migration that creates the table with `DrizzleTable.afterCreate()` from `@elysiumjs/drizzle`:
|
|
156
|
+
|
|
157
|
+
```ts
|
|
158
|
+
import { DrizzleTable } from '@elysiumjs/drizzle';
|
|
159
|
+
|
|
160
|
+
const tables = moiraiTables('billing', { partitioned: true });
|
|
161
|
+
|
|
162
|
+
export const billing = {
|
|
163
|
+
...tables,
|
|
164
|
+
events: DrizzleTable.define(
|
|
165
|
+
DrizzleTable.afterCreate(
|
|
166
|
+
({ qualified }) => `CREATE TABLE "billing"."events_default" PARTITION OF ${qualified} DEFAULT`
|
|
167
|
+
)
|
|
168
|
+
)(tables.events)
|
|
169
|
+
};
|
|
170
|
+
```
|
|
171
|
+
|
|
172
|
+
## Moirai stores
|
|
173
|
+
|
|
174
|
+
```ts
|
|
175
|
+
import {
|
|
176
|
+
DrizzleEventStore,
|
|
177
|
+
DrizzleIdempotencyStore,
|
|
178
|
+
DrizzleInboxStore,
|
|
179
|
+
DrizzleOutboxStore,
|
|
180
|
+
DrizzleProjectionStorage
|
|
181
|
+
} from '@elysiumjs/drizzle';
|
|
182
|
+
import { Lock } from '@elysiumjs/olympus';
|
|
183
|
+
import { MoiraiModule } from '@elysiumjs/olympus/moirai';
|
|
184
|
+
|
|
185
|
+
MoiraiModule.with({
|
|
186
|
+
idempotency: () => new DrizzleIdempotencyStore({ table: billing.idempotencyKeys }),
|
|
187
|
+
outbox: () => new DrizzleOutboxStore({ table: billing.outbox }),
|
|
188
|
+
inbox: () => new DrizzleInboxStore({ table: billing.inbox }),
|
|
189
|
+
lock: () => Lock.for('postgres'),
|
|
190
|
+
events: () => new DrizzleEventStore({ table: billing.events }),
|
|
191
|
+
projections: () => new DrizzleProjectionStorage({ table: billing.projections })
|
|
192
|
+
});
|
|
193
|
+
```
|
|
194
|
+
|
|
195
|
+
- `DrizzleIdempotencyStore` keeps the response of an idempotent action per key.
|
|
196
|
+
- `DrizzleOutboxStore` claims due rows with `FOR UPDATE SKIP LOCKED`, so that several relays never take the same row.
|
|
197
|
+
- `DrizzleInboxStore` records the messages a consumer has processed.
|
|
198
|
+
- `DrizzleEventStore` keeps the event streams of aggregates. An append locks each stream it writes with a transaction advisory lock and checks that the stream ends right before the first appended version, so a stale writer gets a `ConcurrencyConflictError` even when the unique key of the table spans more columns, such as a partition key. Postgres gives each event its `position` and the id of the transaction that wrote it (`transaction_id`).
|
|
199
|
+
- `DrizzleProjectionStorage` resets the tables of the `models` of a projection (`DrizzleModel`s or Drizzle tables) with `DELETE` in the rebuild's transaction, so readers see the rows until it commits, and keeps the version each projection was built with.
|
|
200
|
+
- `PgAdvisoryLock` is the `postgres` driver of the `Lock` port. It serializes the actions that share a `serializeBy` key with a transaction advisory lock, which ends with the transaction. Use it inside a unit of work only: outside a transaction the lock ends with its own statement, so it does not suit the scheduler of `HeraclesModule`.
|
|
201
|
+
|
|
202
|
+
Each store takes `connection`, the name of a database connection, which defaults to `default`. A store's `table` needs the columns of the matching table in `moiraiTables`, and may add others: it can live in a domain's own `pgSchema` and have another primary key, such as a composite one. `StoreTable<T>` is the type it accepts.
|
|
203
|
+
|
|
204
|
+
## Notification store
|
|
205
|
+
|
|
206
|
+
`notificationsTable()` builds the table of the notification store of `@elysiumjs/olympus/hermes`, and `DrizzleNotificationStore` (the `drizzle` driver of `NotificationStore`) keeps the notifications of the `inbox` channel in it:
|
|
207
|
+
|
|
208
|
+
```ts
|
|
209
|
+
import { notificationsTable } from '@elysiumjs/drizzle';
|
|
210
|
+
import { HermesModule, NotificationStore } from '@elysiumjs/olympus/hermes';
|
|
211
|
+
|
|
212
|
+
export const notifications = notificationsTable(); // or notificationsTable('notifications', 'social')
|
|
213
|
+
|
|
214
|
+
HermesModule.with({ store: () => NotificationStore.for('drizzle', { table: notifications }) });
|
|
215
|
+
```
|
|
216
|
+
|
|
217
|
+
The table keeps one row per delivery id, and has an index on the notifiable and the id, which pages use, and a partial index on unread notifications. Any table with the same columns works, given as `table`, with an optional `connection`.
|
|
218
|
+
|
|
219
|
+
## File store
|
|
220
|
+
|
|
221
|
+
`filesTable()` builds the table of the file store of `@elysiumjs/olympus/pithos`, and `DrizzleFileStore` (the `drizzle` driver of `FileStore`) keeps the file records in it:
|
|
222
|
+
|
|
223
|
+
```ts
|
|
224
|
+
import { filesTable } from '@elysiumjs/drizzle';
|
|
225
|
+
import { FileStore, PithosModule } from '@elysiumjs/olympus/pithos';
|
|
226
|
+
|
|
227
|
+
export const files = filesTable(); // or filesTable('files', 'media')
|
|
228
|
+
|
|
229
|
+
PithosModule.with({ disks, files: { store: () => FileStore.for('drizzle', { table: files }) } });
|
|
230
|
+
```
|
|
231
|
+
|
|
232
|
+
Each change of a record is one conditional `UPDATE`, so only one of two racing requests wins. The table has an index on the owner, a partial index on pending uploads by uploader, which the cap on uploads in progress reads, and a partial index on the files whose time runs out (`pending`, `finishing` and `ready`), which pruning reads. Any table with the same columns works, given as `table`, with an optional `connection`.
|
|
233
|
+
|
|
234
|
+
## Abilities as SQL
|
|
235
|
+
|
|
236
|
+
`rulesToDrizzle(ability, action, subject, table)` turns the rules of an ability from `@elysiumjs/olympus/cerberus` into a condition over a table, so a list answers only the rows the same rules let the actor act on:
|
|
237
|
+
|
|
238
|
+
```ts
|
|
239
|
+
import { rulesToDrizzle } from '@elysiumjs/drizzle';
|
|
240
|
+
import { Cerberus } from '@elysiumjs/olympus/cerberus';
|
|
241
|
+
|
|
242
|
+
const ability = await Cerberus.ability();
|
|
243
|
+
const editable = await db
|
|
244
|
+
.select()
|
|
245
|
+
.from(posts)
|
|
246
|
+
.where(rulesToDrizzle(ability, 'update', 'Post', posts)); // false without an ability
|
|
247
|
+
```
|
|
248
|
+
|
|
249
|
+
Rule fields are the properties of the table's columns. A `cannot` bounds every `can` before it. `$eq`, `$ne`, `$in` and `$nin` treat `NULL` as CASL does, and a `cannot` never refuses a row whose condition is unknown because of a `NULL`; `$lt`, `$lte`, `$gt`, `$gte` and `$exists` follow SQL, where `NULL` compares to nothing, so keep the columns they read `NOT NULL` or add an explicit condition. No ability, or no rule that allows the action: `false`; a rule without conditions: `true`. The operators are `$eq`, `$ne`, `$in`, `$nin`, `$lt`, `$lte`, `$gt`, `$gte`, `$exists`, `$and`, `$or` and `$not`; any other operator, or a field the table does not have, throws `UntranslatableRuleError`.
|
|
250
|
+
|
|
251
|
+
## License
|
|
252
|
+
|
|
253
|
+
This project is licensed under the Apache License, Version 2.0. See the [LICENSE](../../LICENSE) file for more info.
|
|
@@ -0,0 +1,7 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* The `require` of the module on Bun (`import.meta.require`), or `undefined` elsewhere. It is read
|
|
3
|
+
* by key: a bundler turns `import.meta.require` into `createRequire(import.meta.url)` at the top of
|
|
4
|
+
* the module, which throws on Workers, where `import.meta.url` is undefined.
|
|
5
|
+
* @internal
|
|
6
|
+
*/
|
|
7
|
+
export declare const bunRequire: () => NodeJS.Require | undefined;
|
package/dist/cache.d.ts
ADDED
|
@@ -0,0 +1,60 @@
|
|
|
1
|
+
import type { CacheConfig } from 'drizzle-orm/cache/core/types';
|
|
2
|
+
import { Table } from 'drizzle-orm';
|
|
3
|
+
import { Cache as DrizzleCache } from 'drizzle-orm/cache/core';
|
|
4
|
+
/**
|
|
5
|
+
* Default time to live of cached query results, in seconds.
|
|
6
|
+
* @author Axel Nana <ax.lnana@outlook.com>
|
|
7
|
+
*/
|
|
8
|
+
export declare const DEFAULT_CACHE_TTL = 60;
|
|
9
|
+
/**
|
|
10
|
+
* Drizzle query cache backed by the framework's cache storage.
|
|
11
|
+
*
|
|
12
|
+
* - Results are cached per connection and tenant: every key starts with the configured prefix
|
|
13
|
+
* (if any) and the connection name, result keys include the current tenant (or `public`), and
|
|
14
|
+
* tags are invalidated for the current tenant. Each connection has its own transaction scope.
|
|
15
|
+
* - Invalidation works across application instances sharing the cache store: each table has a
|
|
16
|
+
* version token in the store, every result records the versions it was read at, and a mutation
|
|
17
|
+
* replaces the versions of the tables it touches. Table versions are shared by all tenants, as
|
|
18
|
+
* a mutation made for one tenant can change rows other tenants read (shared tables, RLS
|
|
19
|
+
* bypass). Every key is written with a TTL.
|
|
20
|
+
* - A result is only stored when the table versions are still the ones its query started at, and
|
|
21
|
+
* every concurrent miss of the key read the same versions: Drizzle does not pair a `put` with its
|
|
22
|
+
* `get`, so a result that may predate a mutation is never stored.
|
|
23
|
+
* - Inside a transaction, the cache is neither read nor written, since the transaction may read
|
|
24
|
+
* its own uncommitted changes. Tables mutated in a transaction are invalidated once it commits,
|
|
25
|
+
* and not at all when it rolls back.
|
|
26
|
+
*
|
|
27
|
+
* Remaining race: a mutation run outside a transaction is invalidated while its statement runs
|
|
28
|
+
* (Drizzle starts both together), so a read on another connection that completes between the
|
|
29
|
+
* invalidation and the commit can cache the previous rows until their TTL expires. Mutations run
|
|
30
|
+
* in a transaction (including RLS-scoped repository calls) are invalidated after the commit and
|
|
31
|
+
* are not affected.
|
|
32
|
+
*
|
|
33
|
+
* @author Axel Nana <ax.lnana@outlook.com>
|
|
34
|
+
*/
|
|
35
|
+
export declare class DrizzleDatabaseCache extends DrizzleCache {
|
|
36
|
+
#private;
|
|
37
|
+
/**
|
|
38
|
+
* @param cacheStrategy Whether queries are cached only when they opt in (`explicit`), or always.
|
|
39
|
+
* @param globalTtl The default time to live of cached results, in seconds.
|
|
40
|
+
* @param storageEngine The cache that holds the results: `memory`, or the name of a mnemosyne cache store.
|
|
41
|
+
* @param connectionName The name of the connection, which prefixes every key.
|
|
42
|
+
* @param prefix A prefix for every key, before the connection name (empty for none).
|
|
43
|
+
*/
|
|
44
|
+
constructor(cacheStrategy?: 'explicit' | 'all', globalTtl?: number, storageEngine?: 'memory' | (string & {}), connectionName?: string, prefix?: string);
|
|
45
|
+
strategy(): 'explicit' | 'all';
|
|
46
|
+
/**
|
|
47
|
+
* Runs a transaction so that the cache is bypassed inside it, and the tables it mutates are
|
|
48
|
+
* invalidated once it commits.
|
|
49
|
+
* @author Axel Nana <ax.lnana@outlook.com>
|
|
50
|
+
* @param run Opens and runs the transaction.
|
|
51
|
+
* @returns The return value of `run`.
|
|
52
|
+
*/
|
|
53
|
+
trackTransaction<T>(run: () => Promise<T>): Promise<T>;
|
|
54
|
+
get(key: string, tables?: string[], isTag?: boolean, autoInvalidate?: boolean): Promise<any[] | undefined>;
|
|
55
|
+
put(key: string, response: any, _tables: string[], isTag: boolean, config?: CacheConfig): Promise<void>;
|
|
56
|
+
onMutate(params: {
|
|
57
|
+
tags?: string | string[];
|
|
58
|
+
tables?: string | string[] | Table<any> | Table<any>[];
|
|
59
|
+
}): Promise<void>;
|
|
60
|
+
}
|
|
@@ -0,0 +1,36 @@
|
|
|
1
|
+
import type { AnyAbility } from '@casl/ability';
|
|
2
|
+
import type { SQL } from 'drizzle-orm';
|
|
3
|
+
import type { PgTable } from 'drizzle-orm/pg-core';
|
|
4
|
+
import { ElysiumError } from '@elysiumjs/olympus';
|
|
5
|
+
/**
|
|
6
|
+
* Thrown by `rulesToDrizzle()` when a rule condition names a field the table does not have, or
|
|
7
|
+
* uses an operator that has no SQL form here.
|
|
8
|
+
* @author Axel Nana <ax.lnana@outlook.com>
|
|
9
|
+
*/
|
|
10
|
+
export declare class UntranslatableRuleError extends ElysiumError {
|
|
11
|
+
constructor(detail: string);
|
|
12
|
+
}
|
|
13
|
+
/**
|
|
14
|
+
* The SQL condition that keeps the rows of `table` the ability allows `action` on, from its rules
|
|
15
|
+
* about `subject`: a list query then answers only what the same rules let the caller act on. CASL
|
|
16
|
+
* flattens the rules first, so a `cannot` bounds every `can` before it. Rule fields are the
|
|
17
|
+
* properties of the table's columns. No ability, or no rule that allows the action: `false`; a
|
|
18
|
+
* rule without conditions: `true`. `$eq`, `$ne`, `$in` and `$nin` treat NULL as CASL does;
|
|
19
|
+
* `$lt`, `$lte`, `$gt`, `$gte` and `$exists` follow SQL, where NULL compares to nothing.
|
|
20
|
+
* @author Axel Nana <ax.lnana@outlook.com>
|
|
21
|
+
* @param ability The ability, or `undefined` for an actor without one.
|
|
22
|
+
* @param action The action, such as `update`.
|
|
23
|
+
* @param subject The subject the rules are about, such as `Post`.
|
|
24
|
+
* @param table The table of that subject.
|
|
25
|
+
* @throws {UntranslatableRuleError} When a condition names a field the table does not have, or
|
|
26
|
+
* uses an operator other than `$eq`, `$ne`, `$in`, `$nin`, `$lt`, `$lte`, `$gt`, `$gte`,
|
|
27
|
+
* `$exists`, `$and`, `$or` and `$not`.
|
|
28
|
+
* @example
|
|
29
|
+
* ```ts
|
|
30
|
+
* const editable = await db
|
|
31
|
+
* .select()
|
|
32
|
+
* .from(posts)
|
|
33
|
+
* .where(rulesToDrizzle(ability, 'update', 'Post', posts));
|
|
34
|
+
* ```
|
|
35
|
+
*/
|
|
36
|
+
export declare const rulesToDrizzle: (ability: AnyAbility | undefined, action: string, subject: string, table: PgTable) => SQL;
|
|
@@ -0,0 +1,14 @@
|
|
|
1
|
+
import { Command } from '@elysiumjs/olympus';
|
|
2
|
+
/**
|
|
3
|
+
* Generates migration files, with the markers of `DrizzleTable.define()` applied to the migration that
|
|
4
|
+
* creates each marked table. The schema is the one of the app: every table, Postgres schema, enum,
|
|
5
|
+
* sequence, view, role and policy that the project files it loads export, directly or as a value
|
|
6
|
+
* of an exported object (like the tables `moiraiTables()` builds).
|
|
7
|
+
* @author Axel Nana <ax.lnana@outlook.com>
|
|
8
|
+
*/
|
|
9
|
+
export declare class MigrationGenerateCommand extends Command {
|
|
10
|
+
static readonly command: string;
|
|
11
|
+
static readonly description: string;
|
|
12
|
+
private name?;
|
|
13
|
+
run(): Promise<void>;
|
|
14
|
+
}
|
|
@@ -0,0 +1,26 @@
|
|
|
1
|
+
import { Command } from '@elysiumjs/olympus';
|
|
2
|
+
/**
|
|
3
|
+
* Execute migration files.
|
|
4
|
+
* @author Axel Nana <ax.lnana@outlook.com>
|
|
5
|
+
*/
|
|
6
|
+
export declare class MigrationRunCommand extends Command {
|
|
7
|
+
static readonly command: string;
|
|
8
|
+
static readonly description: string;
|
|
9
|
+
private tenant;
|
|
10
|
+
private connection;
|
|
11
|
+
private fresh;
|
|
12
|
+
private createSchema;
|
|
13
|
+
run(): Promise<void>;
|
|
14
|
+
/**
|
|
15
|
+
* Runs the pending migrations in one transaction.
|
|
16
|
+
* @param connection The migration connection.
|
|
17
|
+
*/
|
|
18
|
+
private migrate;
|
|
19
|
+
/**
|
|
20
|
+
* Checks that the tenant schema exists, or that it may be created: with `--create-schema`, or
|
|
21
|
+
* when the user agrees. Without a terminal to ask, a missing schema is an error.
|
|
22
|
+
* @param db The migration connection.
|
|
23
|
+
* @returns `false` if the schema must not be created.
|
|
24
|
+
*/
|
|
25
|
+
private ensureSchemaChoice;
|
|
26
|
+
}
|
|
@@ -0,0 +1,11 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Gets the root path of the Elysium.js project: the closest parent holding an `.elysiumrc`, or
|
|
3
|
+
* the current directory when there is none.
|
|
4
|
+
* @author Axel Nana <ax.lnana@outlook.com>
|
|
5
|
+
*/
|
|
6
|
+
export declare const getProjectPath: () => string;
|
|
7
|
+
/**
|
|
8
|
+
* Gets the directory holding the migrations of the Elysium.js project.
|
|
9
|
+
* @author Axel Nana <ax.lnana@outlook.com>
|
|
10
|
+
*/
|
|
11
|
+
export declare const getMigrationsPath: () => string;
|
|
@@ -0,0 +1,48 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* A schema object found in a project file: the export that holds it, and the keys that lead to it
|
|
3
|
+
* when the export is an object grouping several of them.
|
|
4
|
+
* @author Axel Nana <ax.lnana@outlook.com>
|
|
5
|
+
*/
|
|
6
|
+
export type SchemaExport = {
|
|
7
|
+
/** The absolute path of the file. */
|
|
8
|
+
file: string;
|
|
9
|
+
/** The export name, then the keys inside it: `['tables', 'events']` for `tables.events`. */
|
|
10
|
+
path: string[];
|
|
11
|
+
};
|
|
12
|
+
/**
|
|
13
|
+
* Whether a value is something Drizzle Kit generates SQL for: a table, a Postgres schema, an
|
|
14
|
+
* enum, a sequence, a view, a role or a policy.
|
|
15
|
+
* @author Axel Nana <ax.lnana@outlook.com>
|
|
16
|
+
* @param value The value to check.
|
|
17
|
+
*/
|
|
18
|
+
export declare const isSchemaObject: (value: unknown) => boolean;
|
|
19
|
+
/**
|
|
20
|
+
* Finds the schema objects that the given modules export, each once: exported directly, or as a
|
|
21
|
+
* value of an exported plain object (the tables a function builds for a Postgres schema, for
|
|
22
|
+
* example).
|
|
23
|
+
* @author Axel Nana <ax.lnana@outlook.com>
|
|
24
|
+
* @param modules The exports of each file, by absolute path.
|
|
25
|
+
*/
|
|
26
|
+
export declare const findSchemaExports: (modules: Iterable<[file: string, exports: Record<string, unknown>]>) => SchemaExport[];
|
|
27
|
+
/**
|
|
28
|
+
* Lists the source files of the project that the app has loaded, outside `node_modules`.
|
|
29
|
+
* Importing the app loads every model its modules use, whatever the file name; importing a loaded
|
|
30
|
+
* file again gives back its exports without running it.
|
|
31
|
+
* @author Axel Nana <ax.lnana@outlook.com>
|
|
32
|
+
* @param projectPath The root of the project.
|
|
33
|
+
* @param loaded The paths of the loaded files, the keys of the module cache.
|
|
34
|
+
*/
|
|
35
|
+
export declare const loadedProjectFiles: (projectPath: string, loaded: Iterable<string>) => string[];
|
|
36
|
+
/**
|
|
37
|
+
* Finds the schema objects of the app: the ones the project files it loaded export.
|
|
38
|
+
* @author Axel Nana <ax.lnana@outlook.com>
|
|
39
|
+
* @param projectPath The root of the project.
|
|
40
|
+
*/
|
|
41
|
+
export declare const collectAppSchema: (projectPath: string) => Promise<SchemaExport[]>;
|
|
42
|
+
/**
|
|
43
|
+
* Writes the source of a schema file that exports every given schema object at its top level,
|
|
44
|
+
* where Drizzle Kit looks for them.
|
|
45
|
+
* @author Axel Nana <ax.lnana@outlook.com>
|
|
46
|
+
* @param exports The schema objects to export.
|
|
47
|
+
*/
|
|
48
|
+
export declare const renderSchemaEntry: (exports: SchemaExport[]) => string;
|
|
@@ -0,0 +1,137 @@
|
|
|
1
|
+
import type { DatabaseCacheConfig, DatabaseDriver } from '@elysiumjs/olympus/mnemosyne';
|
|
2
|
+
import type { DrizzleConfig } from 'drizzle-orm';
|
|
3
|
+
import type { PgAsyncDatabase, PgQueryResultHKT } from 'drizzle-orm/pg-core';
|
|
4
|
+
import type { DrizzleDriver } from './driver';
|
|
5
|
+
import { ElysiumError } from '@elysiumjs/olympus';
|
|
6
|
+
import { AbstractDatabase } from '@elysiumjs/olympus/mnemosyne';
|
|
7
|
+
export { DEFAULT_CACHE_TTL, DrizzleDatabaseCache } from './cache';
|
|
8
|
+
/**
|
|
9
|
+
* Connection configuration for Drizzle ORM.
|
|
10
|
+
*
|
|
11
|
+
* Extends `DrizzleConfig` with the driver and optional caching. Without `driver`, `connection`
|
|
12
|
+
* or `client` connect with Bun SQL, on Bun only.
|
|
13
|
+
*
|
|
14
|
+
* @author Axel Nana <ax.lnana@outlook.com>
|
|
15
|
+
* @template TSchema The Drizzle schema type.
|
|
16
|
+
*/
|
|
17
|
+
export type DrizzleConnectionProps<TSchema extends Record<string, unknown> = Record<string, never>> = Omit<DrizzleConfig<TSchema>, 'cache'> & ({
|
|
18
|
+
driver: DrizzleDriver;
|
|
19
|
+
} | {
|
|
20
|
+
connection: string | ({
|
|
21
|
+
url?: string;
|
|
22
|
+
} & Record<string, unknown>);
|
|
23
|
+
} | {
|
|
24
|
+
client: unknown;
|
|
25
|
+
}) & {
|
|
26
|
+
cache?: true | DatabaseCacheConfig;
|
|
27
|
+
};
|
|
28
|
+
/**
|
|
29
|
+
* A Drizzle database connection, whatever its driver.
|
|
30
|
+
* @author Axel Nana <ax.lnana@outlook.com>
|
|
31
|
+
*/
|
|
32
|
+
export type DrizzleConnection = PgAsyncDatabase<DrizzleQueryResultHKT, any> & {
|
|
33
|
+
$client: any;
|
|
34
|
+
};
|
|
35
|
+
/**
|
|
36
|
+
* The query results of a {@link DrizzleConnection}: rows, for `returning()` and selects. What
|
|
37
|
+
* `execute()` resolves to depends on the driver.
|
|
38
|
+
* @author Axel Nana <ax.lnana@outlook.com>
|
|
39
|
+
*/
|
|
40
|
+
export interface DrizzleQueryResultHKT extends PgQueryResultHKT {
|
|
41
|
+
type: this['row'][];
|
|
42
|
+
}
|
|
43
|
+
/**
|
|
44
|
+
* The driver of a connection.
|
|
45
|
+
* @internal
|
|
46
|
+
* @author Axel Nana <ax.lnana@outlook.com>
|
|
47
|
+
* @param connection The connection.
|
|
48
|
+
*/
|
|
49
|
+
export declare const getConnectionDriver: (connection: DrizzleConnection) => DrizzleDriver | undefined;
|
|
50
|
+
/**
|
|
51
|
+
* Closes the client of a connection through its driver.
|
|
52
|
+
* @internal
|
|
53
|
+
* @author Axel Nana <ax.lnana@outlook.com>
|
|
54
|
+
* @param connection The connection.
|
|
55
|
+
*/
|
|
56
|
+
export declare const closeConnection: (connection: DrizzleConnection) => Promise<void>;
|
|
57
|
+
/**
|
|
58
|
+
* Thrown in RLS mode when work for one tenant would join a transaction bound to another.
|
|
59
|
+
*
|
|
60
|
+
* A transaction is bound to the tenant set when it is opened, for its whole life. Work for another
|
|
61
|
+
* tenant must run outside the transaction, or in its own `withRLS()` before the transaction opens.
|
|
62
|
+
* @author Axel Nana <ax.lnana@outlook.com>
|
|
63
|
+
*/
|
|
64
|
+
export declare class TenantMismatchError extends ElysiumError {
|
|
65
|
+
readonly transactionTenant: string | null;
|
|
66
|
+
readonly tenant: string;
|
|
67
|
+
/**
|
|
68
|
+
* @param transactionTenant The tenant the open transaction is bound to, or `null` if none.
|
|
69
|
+
* @param tenant The tenant of the work that tried to join it.
|
|
70
|
+
*/
|
|
71
|
+
constructor(transactionTenant: string | null, tenant: string);
|
|
72
|
+
}
|
|
73
|
+
/**
|
|
74
|
+
* Binds a transaction to a tenant: sets the RLS tenant variable for the rest of the transaction.
|
|
75
|
+
* @internal
|
|
76
|
+
* @author Axel Nana <ax.lnana@outlook.com>
|
|
77
|
+
* @param tx The transaction, just opened.
|
|
78
|
+
* @param tenant The tenant identifier.
|
|
79
|
+
*/
|
|
80
|
+
export declare const bindTransactionTenant: (tx: DrizzleConnection, tenant: string) => Promise<void>;
|
|
81
|
+
/**
|
|
82
|
+
* Checks that work for a tenant may join an open transaction: it must be bound to that tenant.
|
|
83
|
+
* @internal
|
|
84
|
+
* @author Axel Nana <ax.lnana@outlook.com>
|
|
85
|
+
* @param tx The open transaction.
|
|
86
|
+
* @param tenant The tenant of the work.
|
|
87
|
+
* @throws {TenantMismatchError} If the transaction is bound to another tenant, or to none.
|
|
88
|
+
*/
|
|
89
|
+
export declare const assertTransactionTenant: (tx: DrizzleConnection, tenant: string) => void;
|
|
90
|
+
/**
|
|
91
|
+
* Drizzle-specific database manager.
|
|
92
|
+
*
|
|
93
|
+
* Implements the abstract database class with a driver that creates
|
|
94
|
+
* Drizzle connections through the {@link DrizzleDriver} of each connection.
|
|
95
|
+
*
|
|
96
|
+
* @author Axel Nana <ax.lnana@outlook.com>
|
|
97
|
+
*/
|
|
98
|
+
declare class DrizzleDatabase extends AbstractDatabase<DrizzleConnection, DrizzleConnectionProps> {
|
|
99
|
+
/**
|
|
100
|
+
* Retrieves the connection with the given name, or the transaction open on it.
|
|
101
|
+
*
|
|
102
|
+
* In RLS mode, an open transaction bound to a tenant is only returned to work for that tenant,
|
|
103
|
+
* the current one (or `public`).
|
|
104
|
+
* @author Axel Nana <ax.lnana@outlook.com>
|
|
105
|
+
* @param name The connection name.
|
|
106
|
+
* @returns The connection, or its open transaction.
|
|
107
|
+
* @throws {TenantMismatchError} If the open transaction is bound to another tenant.
|
|
108
|
+
*/
|
|
109
|
+
getConnection(name?: string): DrizzleConnection;
|
|
110
|
+
/**
|
|
111
|
+
* Runs a callback in a transaction, joining the one open on the connection if any.
|
|
112
|
+
*
|
|
113
|
+
* In RLS mode, a new transaction is bound to the current tenant (or `public`), and an open one
|
|
114
|
+
* is only joined when it is bound to the current tenant.
|
|
115
|
+
* @author Axel Nana <ax.lnana@outlook.com>
|
|
116
|
+
* @param callback The function to run with the transaction.
|
|
117
|
+
* @param name The connection name.
|
|
118
|
+
* @returns The return value of the callback.
|
|
119
|
+
* @throws {TenantMismatchError} If the open transaction is bound to another tenant.
|
|
120
|
+
*/
|
|
121
|
+
transaction<T>(callback: (tx: DrizzleConnection) => Promise<T>, name?: string): Promise<T>;
|
|
122
|
+
/**
|
|
123
|
+
* Creates a Drizzle connection from its properties. Used by the `DrizzleModule` to build
|
|
124
|
+
* the `db.connection.{name}` services.
|
|
125
|
+
* @author Axel Nana <ax.lnana@outlook.com>
|
|
126
|
+
* @param props The connection properties.
|
|
127
|
+
* @param name The name of the connection, which scopes its query cache.
|
|
128
|
+
* @returns A new connection.
|
|
129
|
+
*/
|
|
130
|
+
createConnection(props: DrizzleConnectionProps, name?: string): DrizzleConnection;
|
|
131
|
+
protected driver: DatabaseDriver<DrizzleConnection, DrizzleConnectionProps>;
|
|
132
|
+
}
|
|
133
|
+
/**
|
|
134
|
+
* Singleton Drizzle database manager.
|
|
135
|
+
* @author Axel Nana <ax.lnana@outlook.com>
|
|
136
|
+
*/
|
|
137
|
+
export declare const Database: DrizzleDatabase;
|