@kattebak/typespec-drizzle-orm-generator 1.0.1
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 +318 -0
- package/package.json +44 -0
- package/src/decorators.tsp +22 -0
package/LICENSE
ADDED
|
@@ -0,0 +1,21 @@
|
|
|
1
|
+
MIT License
|
|
2
|
+
|
|
3
|
+
Copyright (c) 2026 kattebak
|
|
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,318 @@
|
|
|
1
|
+
# typespec-drizzle-orm-generator
|
|
2
|
+
|
|
3
|
+
Generate a complete [Drizzle ORM](https://orm.drizzle.team/) package from TypeSpec models. You define your domain in TypeSpec, the emitter gives you table schemas, relations, and typed query functions for PostgreSQL.
|
|
4
|
+
|
|
5
|
+
## Install
|
|
6
|
+
|
|
7
|
+
```bash
|
|
8
|
+
npm install @kattebak/typespec-drizzle-orm-generator
|
|
9
|
+
```
|
|
10
|
+
|
|
11
|
+
## Quick start
|
|
12
|
+
|
|
13
|
+
### 1. Define your models
|
|
14
|
+
|
|
15
|
+
```typespec
|
|
16
|
+
import "@kattebak/typespec-drizzle-orm-generator";
|
|
17
|
+
|
|
18
|
+
using DrizzleEmitter;
|
|
19
|
+
|
|
20
|
+
@entity("Author", "bookstore")
|
|
21
|
+
@primaryKey("authors")
|
|
22
|
+
model Author {
|
|
23
|
+
@pk @uuid("base36", true)
|
|
24
|
+
authorId: string;
|
|
25
|
+
|
|
26
|
+
name: string;
|
|
27
|
+
bio?: string;
|
|
28
|
+
|
|
29
|
+
@createdAt createdAt: utcDateTime;
|
|
30
|
+
@updatedAt updatedAt: utcDateTime;
|
|
31
|
+
}
|
|
32
|
+
|
|
33
|
+
@entity("Book", "bookstore")
|
|
34
|
+
@primaryKey("books")
|
|
35
|
+
model Book {
|
|
36
|
+
@pk @uuid("base36", true)
|
|
37
|
+
bookId: string;
|
|
38
|
+
|
|
39
|
+
@uuid("base36") @references(Author.authorId)
|
|
40
|
+
authorId: string;
|
|
41
|
+
|
|
42
|
+
title: string;
|
|
43
|
+
@unique isbn?: string;
|
|
44
|
+
@minValue(1) @maxValue(5) rating: int32;
|
|
45
|
+
|
|
46
|
+
@createdAt createdAt: utcDateTime;
|
|
47
|
+
@updatedAt updatedAt: utcDateTime;
|
|
48
|
+
}
|
|
49
|
+
```
|
|
50
|
+
|
|
51
|
+
### 2. Configure and run the emitter
|
|
52
|
+
|
|
53
|
+
Add a `tspconfig.yaml` to your TypeSpec project:
|
|
54
|
+
|
|
55
|
+
```yaml
|
|
56
|
+
emit:
|
|
57
|
+
- "@kattebak/typespec-drizzle-orm-generator"
|
|
58
|
+
|
|
59
|
+
options:
|
|
60
|
+
"@kattebak/typespec-drizzle-orm-generator":
|
|
61
|
+
"package-name": "@myorg/bookstore-db"
|
|
62
|
+
"package-version": "1.0.0"
|
|
63
|
+
```
|
|
64
|
+
|
|
65
|
+
Then compile:
|
|
66
|
+
|
|
67
|
+
```bash
|
|
68
|
+
npx tsp compile .
|
|
69
|
+
```
|
|
70
|
+
|
|
71
|
+
The emitter produces 6 files as a ready-to-use npm package:
|
|
72
|
+
|
|
73
|
+
| File | What's in it |
|
|
74
|
+
| -------------- | -------------------------------------------------------------------- |
|
|
75
|
+
| `schema.ts` | `pgTable()` definitions, `pgEnum()`, constraints |
|
|
76
|
+
| `relations.ts` | `defineRelations()` with `through()` for many-to-many |
|
|
77
|
+
| `describe.ts` | One typed query function per entity (fetch by PK with all relations) |
|
|
78
|
+
| `types.ts` | `base36Uuid` custom type, `DrizzleClient` type alias |
|
|
79
|
+
| `index.ts` | Barrel re-exports |
|
|
80
|
+
| `package.json` | Package metadata with `drizzle-orm` dependency |
|
|
81
|
+
|
|
82
|
+
### 3. Use the generated code
|
|
83
|
+
|
|
84
|
+
Install the generated package in your application (e.g. from the emitter output directory or after publishing):
|
|
85
|
+
|
|
86
|
+
```bash
|
|
87
|
+
npm install ./tsp-output/@kattebak/typespec-drizzle-orm-generator
|
|
88
|
+
```
|
|
89
|
+
|
|
90
|
+
Then use it:
|
|
91
|
+
|
|
92
|
+
```typescript
|
|
93
|
+
import { drizzle } from "drizzle-orm/node-postgres";
|
|
94
|
+
import { relations } from "@myorg/bookstore-db/relations.js";
|
|
95
|
+
import * as schema from "@myorg/bookstore-db/schema.js";
|
|
96
|
+
import { describeAuthor, describeBook } from "@myorg/bookstore-db/describe.js";
|
|
97
|
+
|
|
98
|
+
// Initialize the client with schema + relations
|
|
99
|
+
const db = drizzle({ connection: process.env.DATABASE_URL, schema, relations });
|
|
100
|
+
|
|
101
|
+
// Fetch an author with all their books in one query
|
|
102
|
+
const author = await describeAuthor(db, "abc123");
|
|
103
|
+
// author.name, author.books[0].title — fully typed
|
|
104
|
+
|
|
105
|
+
// Fetch a book with its author
|
|
106
|
+
const book = await describeBook(db, "xyz789");
|
|
107
|
+
// book.author.name, book.rating — fully typed
|
|
108
|
+
|
|
109
|
+
// Or use the schema directly for custom queries
|
|
110
|
+
import { authors } from "@myorg/bookstore-db/schema.js";
|
|
111
|
+
import { eq } from "drizzle-orm";
|
|
112
|
+
|
|
113
|
+
const result = await db.select().from(authors).where(eq(authors.name, "Tolkien"));
|
|
114
|
+
```
|
|
115
|
+
|
|
116
|
+
## Generated output examples
|
|
117
|
+
|
|
118
|
+
### schema.ts
|
|
119
|
+
|
|
120
|
+
```typescript
|
|
121
|
+
import { check, integer, pgTable, text, timestamp } from "drizzle-orm/pg-core";
|
|
122
|
+
import { sql } from "drizzle-orm";
|
|
123
|
+
import { base36Uuid } from "./types.js";
|
|
124
|
+
|
|
125
|
+
export const authors = pgTable("authors", {
|
|
126
|
+
authorId: base36Uuid("author_id").primaryKey().defaultRandom(),
|
|
127
|
+
name: text("name").notNull(),
|
|
128
|
+
bio: text("bio"),
|
|
129
|
+
createdAt: timestamp("created_at", { withTimezone: true })
|
|
130
|
+
.notNull()
|
|
131
|
+
.defaultNow(),
|
|
132
|
+
updatedAt: timestamp("updated_at", { withTimezone: true })
|
|
133
|
+
.notNull()
|
|
134
|
+
.defaultNow(),
|
|
135
|
+
});
|
|
136
|
+
|
|
137
|
+
export const books = pgTable(
|
|
138
|
+
"books",
|
|
139
|
+
{
|
|
140
|
+
bookId: base36Uuid("book_id").primaryKey().defaultRandom(),
|
|
141
|
+
authorId: base36Uuid("author_id")
|
|
142
|
+
.notNull()
|
|
143
|
+
.references(() => authors.authorId),
|
|
144
|
+
title: text("title").notNull(),
|
|
145
|
+
isbn: text("isbn").unique(),
|
|
146
|
+
rating: integer("rating").notNull(),
|
|
147
|
+
createdAt: timestamp("created_at", { withTimezone: true })
|
|
148
|
+
.notNull()
|
|
149
|
+
.defaultNow(),
|
|
150
|
+
updatedAt: timestamp("updated_at", { withTimezone: true })
|
|
151
|
+
.notNull()
|
|
152
|
+
.defaultNow(),
|
|
153
|
+
},
|
|
154
|
+
(table) => [
|
|
155
|
+
check(
|
|
156
|
+
"books_rating_check",
|
|
157
|
+
sql`${table.rating} >= 1 AND ${table.rating} <= 5`,
|
|
158
|
+
),
|
|
159
|
+
],
|
|
160
|
+
);
|
|
161
|
+
```
|
|
162
|
+
|
|
163
|
+
### relations.ts
|
|
164
|
+
|
|
165
|
+
```typescript
|
|
166
|
+
import { defineRelations } from "drizzle-orm";
|
|
167
|
+
import * as schema from "./schema.js";
|
|
168
|
+
|
|
169
|
+
export const relations = defineRelations(schema, (r) => ({
|
|
170
|
+
authors: {
|
|
171
|
+
books: r.many.books(),
|
|
172
|
+
},
|
|
173
|
+
books: {
|
|
174
|
+
author: r.one.authors({
|
|
175
|
+
from: r.books.authorId,
|
|
176
|
+
to: r.authors.authorId,
|
|
177
|
+
}),
|
|
178
|
+
},
|
|
179
|
+
}));
|
|
180
|
+
```
|
|
181
|
+
|
|
182
|
+
### describe.ts
|
|
183
|
+
|
|
184
|
+
```typescript
|
|
185
|
+
import type { DrizzleClient } from "./types.js";
|
|
186
|
+
import * as schema from "./schema.js";
|
|
187
|
+
|
|
188
|
+
export type AuthorDescription = typeof schema.authors.$inferSelect & {
|
|
189
|
+
books: (typeof schema.books.$inferSelect)[];
|
|
190
|
+
};
|
|
191
|
+
|
|
192
|
+
export const describeAuthor = (
|
|
193
|
+
db: DrizzleClient,
|
|
194
|
+
authorId: string,
|
|
195
|
+
): Promise<AuthorDescription | undefined> =>
|
|
196
|
+
db.query.authors.findFirst({
|
|
197
|
+
where: { authorId },
|
|
198
|
+
with: { books: true },
|
|
199
|
+
});
|
|
200
|
+
```
|
|
201
|
+
|
|
202
|
+
## Decorators
|
|
203
|
+
|
|
204
|
+
### Defining entities
|
|
205
|
+
|
|
206
|
+
```typespec
|
|
207
|
+
@entity("Author", "bookstore") // entity name + service grouping
|
|
208
|
+
@primaryKey("authors") // SQL table name
|
|
209
|
+
model Author {
|
|
210
|
+
@pk // marks field as primary key column
|
|
211
|
+
@uuid("base36", true) // UUID with base36 encoding, auto-generated
|
|
212
|
+
authorId: string;
|
|
213
|
+
|
|
214
|
+
@references(Author.authorId) // foreign key to another entity
|
|
215
|
+
authorId: string;
|
|
216
|
+
|
|
217
|
+
@junction // on model: marks as many-to-many junction table
|
|
218
|
+
|
|
219
|
+
@createdAt // DEFAULT NOW()
|
|
220
|
+
createdAt: utcDateTime;
|
|
221
|
+
|
|
222
|
+
@updatedAt // DEFAULT NOW() + BEFORE UPDATE trigger
|
|
223
|
+
updatedAt: utcDateTime;
|
|
224
|
+
}
|
|
225
|
+
```
|
|
226
|
+
|
|
227
|
+
### Adding constraints
|
|
228
|
+
|
|
229
|
+
```typespec
|
|
230
|
+
@unique // single-column UNIQUE
|
|
231
|
+
isbn?: string;
|
|
232
|
+
|
|
233
|
+
@minValue(1) @maxValue(5) // CHECK (rating >= 1 AND rating <= 5)
|
|
234
|
+
rating: int32;
|
|
235
|
+
|
|
236
|
+
@check("price > 0") // arbitrary CHECK expression
|
|
237
|
+
price: float64;
|
|
238
|
+
|
|
239
|
+
// Model-level decorators:
|
|
240
|
+
@compositeUnique("uq_book_lang", [Edition.bookId, Edition.language])
|
|
241
|
+
@indexDef("idx_author_year", [Book.authorId, Book.publicationYear])
|
|
242
|
+
@indexDef("idx_isbn", [Book.isbn], true) // unique index
|
|
243
|
+
@foreignKeyDef("fk_order", [cols...], [foreignCols...]) // composite FK
|
|
244
|
+
```
|
|
245
|
+
|
|
246
|
+
### Many-to-many with junction tables
|
|
247
|
+
|
|
248
|
+
```typespec
|
|
249
|
+
@entity("BookGenre", "bookstore")
|
|
250
|
+
@junction // skip describe generation, enable through()
|
|
251
|
+
@primaryKey("book_genres")
|
|
252
|
+
model BookGenre {
|
|
253
|
+
@pk @uuid("base36") @references(Book.bookId)
|
|
254
|
+
bookId: string;
|
|
255
|
+
|
|
256
|
+
@pk @uuid("base36") @references(Genre.genreId)
|
|
257
|
+
genreId: string;
|
|
258
|
+
}
|
|
259
|
+
```
|
|
260
|
+
|
|
261
|
+
This generates many-to-many relations using Drizzle v2's `through()`:
|
|
262
|
+
|
|
263
|
+
```typescript
|
|
264
|
+
books: {
|
|
265
|
+
genres: r.many.genres({
|
|
266
|
+
from: r.books.bookId.through(r.bookGenres.bookId),
|
|
267
|
+
to: r.genres.genreId.through(r.bookGenres.genreId),
|
|
268
|
+
}),
|
|
269
|
+
}
|
|
270
|
+
```
|
|
271
|
+
|
|
272
|
+
### Nullable foreign keys
|
|
273
|
+
|
|
274
|
+
Make a field optional to get a nullable FK. The describe type reflects this as `| null`:
|
|
275
|
+
|
|
276
|
+
```typespec
|
|
277
|
+
@references(Translator.translatorId)
|
|
278
|
+
translatorId?: string; // nullable FK
|
|
279
|
+
```
|
|
280
|
+
|
|
281
|
+
```typescript
|
|
282
|
+
export type EditionDescription = typeof schema.editions.$inferSelect & {
|
|
283
|
+
translator: typeof schema.translators.$inferSelect | null;
|
|
284
|
+
};
|
|
285
|
+
```
|
|
286
|
+
|
|
287
|
+
## Type mapping
|
|
288
|
+
|
|
289
|
+
| TypeSpec | Drizzle | PostgreSQL |
|
|
290
|
+
| ---------------------- | ----------------------------------- | ------------------------- |
|
|
291
|
+
| `string` | `text()` | `text` |
|
|
292
|
+
| `string` (with length) | `varchar({ length })` | `varchar(n)` |
|
|
293
|
+
| `int32` | `integer()` | `integer` |
|
|
294
|
+
| `int64` | `bigint({ mode: "number" })` | `bigint` |
|
|
295
|
+
| `float32` | `real()` | `real` |
|
|
296
|
+
| `float64` | `doublePrecision()` | `double precision` |
|
|
297
|
+
| `boolean` | `boolean()` | `boolean` |
|
|
298
|
+
| `utcDateTime` | `timestamp({ withTimezone: true })` | `timestamptz` |
|
|
299
|
+
| `string` + `@uuid` | `base36Uuid()` | `uuid` |
|
|
300
|
+
| TypeSpec `enum` | `pgEnum()` | `CREATE TYPE ... AS ENUM` |
|
|
301
|
+
|
|
302
|
+
## Full example
|
|
303
|
+
|
|
304
|
+
See [test/fixtures/bookstore.tsp](test/fixtures/bookstore.tsp) for a complete 9-entity bookstore domain with authors, books, genres (many-to-many via junction), editions (3 FKs including nullable), reviews, tags, translators, and publishers.
|
|
305
|
+
|
|
306
|
+
## Contributing
|
|
307
|
+
|
|
308
|
+
```bash
|
|
309
|
+
npm install
|
|
310
|
+
npm test # type-check + 230 tests
|
|
311
|
+
npm run lint:fix # auto-fix with Biome
|
|
312
|
+
```
|
|
313
|
+
|
|
314
|
+
Tests run against SQLite in-memory (no Postgres needed). See [doc/](doc/) for the RFC and implementation plan.
|
|
315
|
+
|
|
316
|
+
## License
|
|
317
|
+
|
|
318
|
+
MIT
|
package/package.json
ADDED
|
@@ -0,0 +1,44 @@
|
|
|
1
|
+
{
|
|
2
|
+
"name": "@kattebak/typespec-drizzle-orm-generator",
|
|
3
|
+
"version": "1.0.1",
|
|
4
|
+
"type": "module",
|
|
5
|
+
"main": "dist/index.js",
|
|
6
|
+
"types": "dist/index.d.ts",
|
|
7
|
+
"exports": {
|
|
8
|
+
".": {
|
|
9
|
+
"types": "./dist/index.d.ts",
|
|
10
|
+
"import": "./dist/index.js"
|
|
11
|
+
}
|
|
12
|
+
},
|
|
13
|
+
"tspMain": "src/decorators.tsp",
|
|
14
|
+
"files": [
|
|
15
|
+
"dist",
|
|
16
|
+
"src/decorators.tsp"
|
|
17
|
+
],
|
|
18
|
+
"peerDependencies": {
|
|
19
|
+
"@typespec/compiler": ">=1.0.0"
|
|
20
|
+
},
|
|
21
|
+
"scripts": {
|
|
22
|
+
"build": "tsgo --project tsconfig.build.json",
|
|
23
|
+
"check": "tsgo --noEmit",
|
|
24
|
+
"lint": "biome check .",
|
|
25
|
+
"fix": "biome check --write .",
|
|
26
|
+
"format": "biome format --write .",
|
|
27
|
+
"test": "node --test --experimental-strip-types 'test/**/*.test.ts'",
|
|
28
|
+
"pretest": "npm run check"
|
|
29
|
+
},
|
|
30
|
+
"devDependencies": {
|
|
31
|
+
"@biomejs/biome": "^2.0.0",
|
|
32
|
+
"@types/better-sqlite3": "^7.6.13",
|
|
33
|
+
"@typescript/native-preview": "latest",
|
|
34
|
+
"@typespec/compiler": "^1.9.0",
|
|
35
|
+
"better-sqlite3": "^12.6.2",
|
|
36
|
+
"drizzle-orm": "^1.0.0-beta.15-859cf75"
|
|
37
|
+
},
|
|
38
|
+
"publishConfig": {
|
|
39
|
+
"access": "public"
|
|
40
|
+
},
|
|
41
|
+
"engines": {
|
|
42
|
+
"node": ">=22.0.0"
|
|
43
|
+
}
|
|
44
|
+
}
|
|
@@ -0,0 +1,22 @@
|
|
|
1
|
+
import "./lib.js";
|
|
2
|
+
|
|
3
|
+
using TypeSpec.Reflection;
|
|
4
|
+
|
|
5
|
+
namespace DrizzleEmitter;
|
|
6
|
+
|
|
7
|
+
extern dec table(target: Model, name: valueof string, service: valueof string);
|
|
8
|
+
extern dec primaryKey(target: Model, tableName: valueof string);
|
|
9
|
+
extern dec pk(target: ModelProperty);
|
|
10
|
+
extern dec references(target: ModelProperty, ref: ModelProperty);
|
|
11
|
+
extern dec junction(target: Model);
|
|
12
|
+
extern dec uuid(target: ModelProperty, encoding: valueof string, autoGenerate?: valueof boolean);
|
|
13
|
+
extern dec createdAt(target: ModelProperty);
|
|
14
|
+
extern dec updatedAt(target: ModelProperty);
|
|
15
|
+
extern dec unique(target: ModelProperty);
|
|
16
|
+
extern dec compositeUnique(target: Model, name: valueof string, columns: ModelProperty[]);
|
|
17
|
+
extern dec check(target: ModelProperty, expression: valueof string);
|
|
18
|
+
extern dec indexDef(target: Model, name: valueof string, columns: ModelProperty[], unique?: valueof boolean);
|
|
19
|
+
extern dec foreignKeyDef(target: Model, name: valueof string, columns: ModelProperty[], foreignColumns: ModelProperty[]);
|
|
20
|
+
extern dec minValue(target: ModelProperty, value: valueof int32);
|
|
21
|
+
extern dec maxValue(target: ModelProperty, value: valueof int32);
|
|
22
|
+
extern dec visibility(target: ModelProperty, value: valueof string);
|