@vibeorm/dsl 0.2.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/README.md ADDED
@@ -0,0 +1,523 @@
1
+ # @vibeorm/dsl
2
+
3
+ TypeScript DSL for defining Prisma schemas programmatically. Define models, columns, relations, and enums in TypeScript and generate valid `.prisma` schema files.
4
+
5
+ ## Install
6
+
7
+ ```bash
8
+ bun add @vibeorm/dsl
9
+ ```
10
+
11
+ ## Quick Start
12
+
13
+ ```typescript
14
+ import { dsl } from "@vibeorm/dsl";
15
+
16
+ // Define models
17
+ const Workspace = dsl.create({
18
+ name: "Workspace",
19
+ schema: {
20
+ ...dsl.templates.base,
21
+ name: dsl.string(),
22
+ slug: dsl.string().unique(),
23
+ },
24
+ });
25
+
26
+ const User = dsl.create({
27
+ name: "User",
28
+ schema: {
29
+ ...dsl.templates.base,
30
+ email: dsl.string().unique(),
31
+ workspaceId: dsl.string().index(),
32
+ },
33
+ });
34
+
35
+ // Define relations
36
+ const relations = dsl.defineRelations({ Workspace, User }, (r) => ({
37
+ Workspace: {
38
+ users: r.many.User({
39
+ from: r.Workspace.id,
40
+ to: r.User.workspaceId,
41
+ }),
42
+ },
43
+ User: {
44
+ workspace: r.one.Workspace({
45
+ from: r.User.workspaceId,
46
+ to: r.Workspace.id,
47
+ }),
48
+ },
49
+ }));
50
+
51
+ // Assemble schema and generate
52
+ const schema = dsl.schema({ Workspace, User }, { relations });
53
+ const prismaOutput = dsl.generate.prisma(schema);
54
+ ```
55
+
56
+ ## Column Types
57
+
58
+ Every column builder supports both **object syntax** and **chainable syntax**:
59
+
60
+ ```typescript
61
+ // Object syntax
62
+ dsl.string({ optional: true, unique: true, default: "hello" })
63
+
64
+ // Chainable syntax (equivalent)
65
+ dsl.string().optional().unique().default("hello")
66
+ ```
67
+
68
+ ### Available Types
69
+
70
+ | Builder | Prisma Type | Notes |
71
+ |---|---|---|
72
+ | `dsl.string()` | `String` | `.maxLength(n)` for `@db.VarChar(n)`, `.text()` for `@db.Text` |
73
+ | `dsl.int()` | `Int` | |
74
+ | `dsl.bigint()` | `BigInt` | |
75
+ | `dsl.float()` | `Float` | |
76
+ | `dsl.decimal()` | `Decimal` | `.precision(p, s)` for `@db.Decimal(p, s)` |
77
+ | `dsl.boolean()` | `Boolean` | |
78
+ | `dsl.datetime()` | `DateTime` | `.defaultNow()`, `.updatedAt()`, `.tz()` for `@db.Timestamptz()` |
79
+ | `dsl.json()` | `Json` | |
80
+ | `dsl.id()` | `String`/`Int` | `.type("ulid" \| "cuid" \| "uuid" \| "autoincrement")` |
81
+ | `dsl.bytes()` | `Bytes` | |
82
+ | `dsl.unsupported("tsvector")` | `Unsupported("tsvector")` | For database-native types without Prisma mapping |
83
+
84
+ ### Common Modifiers
85
+
86
+ All column types support these chainable methods:
87
+
88
+ ```typescript
89
+ dsl.string()
90
+ .optional() // Nullable (String?)
91
+ .unique() // @unique
92
+ .index() // @@index
93
+ .array() // String[]
94
+ .default("value") // @default("value")
95
+ .map("db_column_name") // @map("db_column_name")
96
+ .dbgenerated("expression") // @default(dbgenerated("expression"))
97
+ .comment("text") // /// text (triple-slash comment above column)
98
+ ```
99
+
100
+ ### ID Types
101
+
102
+ ```typescript
103
+ dsl.id() // String @id @default(ulid()) — default
104
+ dsl.id().type("cuid") // String @id @default(cuid())
105
+ dsl.id().type("uuid") // String @id @default(uuid())
106
+ dsl.id().type("autoincrement") // Int @id @default(autoincrement())
107
+ ```
108
+
109
+ ### Datetime Shortcuts
110
+
111
+ ```typescript
112
+ dsl.datetime().defaultNow() // @default(now())
113
+ dsl.datetime().updatedAt() // @updatedAt
114
+ dsl.datetime().tz() // @db.Timestamptz()
115
+ dsl.datetime().defaultNow().updatedAt().tz() // All combined
116
+ ```
117
+
118
+ ## Enums
119
+
120
+ ### Standalone Enums (Reusable)
121
+
122
+ ```typescript
123
+ const UserRole = dsl.enum({
124
+ name: "UserRole",
125
+ options: ["admin", "user", "guest"],
126
+ });
127
+
128
+ const User = dsl.create({
129
+ name: "User",
130
+ schema: {
131
+ id: dsl.id(),
132
+ role: UserRole.field().default("user"),
133
+ },
134
+ });
135
+ ```
136
+
137
+ ### Inline Enums (One-Off)
138
+
139
+ Auto-named as `ModelNameColumnName`:
140
+
141
+ ```typescript
142
+ const Project = dsl.create({
143
+ name: "Project",
144
+ schema: {
145
+ id: dsl.id(),
146
+ // Generates: enum ProjectStatus { active, archived, deleted }
147
+ status: dsl.enum({ options: ["active", "archived", "deleted"] }).default("active"),
148
+ },
149
+ });
150
+ ```
151
+
152
+ ## Models
153
+
154
+ ### Basic Model
155
+
156
+ ```typescript
157
+ const User = dsl.create({
158
+ name: "User",
159
+ schema: {
160
+ id: dsl.id(),
161
+ email: dsl.string().unique(),
162
+ name: dsl.string(),
163
+ bio: dsl.string().optional(),
164
+ },
165
+ });
166
+ ```
167
+
168
+ ### With Indexes
169
+
170
+ The `indexes` callback provides a type-safe index builder:
171
+
172
+ ```typescript
173
+ const User = dsl.create({
174
+ name: "User",
175
+ schema: {
176
+ id: dsl.id(),
177
+ email: dsl.string().index(), // Single-column: @@index([email])
178
+ organizationId: dsl.string().index(),
179
+ },
180
+ indexes: (i) => [
181
+ i.unique("organizationId", "email"), // @@unique([organizationId, email])
182
+ i.gin("tags"), // @@index([tags], type: Gin)
183
+ ],
184
+ });
185
+ ```
186
+
187
+ ### With Compound Primary Key
188
+
189
+ ```typescript
190
+ const UserGroup = dsl.create({
191
+ name: "UserGroup",
192
+ schema: {
193
+ userId: dsl.string(),
194
+ groupId: dsl.string(),
195
+ joinedAt: dsl.datetime().defaultNow(),
196
+ },
197
+ primaryKey: ["userId", "groupId"], // @@id([userId, groupId])
198
+ });
199
+ ```
200
+
201
+ ### With Table Mapping
202
+
203
+ ```typescript
204
+ const User = dsl.create({
205
+ name: "User",
206
+ schema: {
207
+ id: dsl.id(),
208
+ firstName: dsl.string().map("first_name"),
209
+ },
210
+ map: "users", // @@map("users")
211
+ });
212
+ ```
213
+
214
+ ### With Children (Inline Nested Models)
215
+
216
+ Use a callback for `schema` to access the children builder:
217
+
218
+ ```typescript
219
+ const Order = dsl.create({
220
+ name: "Order",
221
+ schema: (s) => ({
222
+ id: dsl.id(),
223
+ total: dsl.decimal(),
224
+ // Creates an OrderItem model with an auto-generated orderId FK
225
+ items: s.children({
226
+ name: "OrderItem",
227
+ schema: {
228
+ id: dsl.id(),
229
+ quantity: dsl.int(),
230
+ price: dsl.decimal(),
231
+ },
232
+ }),
233
+ }),
234
+ });
235
+ ```
236
+
237
+ ## Relations
238
+
239
+ Relations are defined separately from models using `dsl.defineRelations()`. This keeps models focused on columns and allows relations to reference each other.
240
+
241
+ ### One-to-Many
242
+
243
+ ```typescript
244
+ const relations = dsl.defineRelations({ Workspace, User }, (r) => ({
245
+ Workspace: {
246
+ users: r.many.User({
247
+ from: r.Workspace.id,
248
+ to: r.User.workspaceId,
249
+ }),
250
+ },
251
+ User: {
252
+ workspace: r.one.Workspace({
253
+ from: r.User.workspaceId,
254
+ to: r.Workspace.id,
255
+ }),
256
+ },
257
+ }));
258
+ ```
259
+
260
+ ### One-to-One
261
+
262
+ When both sides define `r.one`, the generator merges them into a single Prisma relation. The side with the FK column becomes the owning side:
263
+
264
+ ```typescript
265
+ const relations = dsl.defineRelations({ User, UserProfile }, (r) => ({
266
+ User: {
267
+ profile: r.one.UserProfile({
268
+ from: r.User.id,
269
+ to: r.UserProfile.userId,
270
+ }),
271
+ },
272
+ UserProfile: {
273
+ user: r.one.User({
274
+ from: r.UserProfile.userId,
275
+ to: r.User.id,
276
+ onDelete: "Cascade",
277
+ }),
278
+ },
279
+ }));
280
+ ```
281
+
282
+ Generates:
283
+
284
+ ```prisma
285
+ model User {
286
+ // ...
287
+ Profile UserProfile?
288
+ }
289
+
290
+ model UserProfile {
291
+ // ...
292
+ User User @relation(fields: [userId], references: [id], onDelete: Cascade)
293
+ }
294
+ ```
295
+
296
+ ### Self-Referential Relations
297
+
298
+ Use `alias` to disambiguate when a model references itself:
299
+
300
+ ```typescript
301
+ Task: {
302
+ parentTask: r.one.Task({
303
+ from: r.Task.parentTaskId,
304
+ to: r.Task.id,
305
+ optional: true,
306
+ alias: "TaskHierarchy",
307
+ }),
308
+ childTasks: r.many.Task({
309
+ from: r.Task.id,
310
+ to: r.Task.parentTaskId,
311
+ alias: "TaskHierarchy",
312
+ }),
313
+ },
314
+ ```
315
+
316
+ ### Multiple Relations to Same Model
317
+
318
+ Use `alias` to distinguish different relations between the same models:
319
+
320
+ ```typescript
321
+ Referral: {
322
+ referrer: r.one.User({
323
+ from: r.Referral.referrerId,
324
+ to: r.User.id,
325
+ alias: "ReferralReferrer",
326
+ }),
327
+ referredUser: r.one.User({
328
+ from: r.Referral.referredUserId,
329
+ to: r.User.id,
330
+ optional: true,
331
+ alias: "ReferralReferred",
332
+ }),
333
+ },
334
+ ```
335
+
336
+ ### Optional Relations
337
+
338
+ ```typescript
339
+ User: {
340
+ workspace: r.one.Workspace({
341
+ from: r.User.workspaceId,
342
+ to: r.Workspace.id,
343
+ optional: true, // Workspace?
344
+ }),
345
+ },
346
+ ```
347
+
348
+ ### Cascade Actions
349
+
350
+ ```typescript
351
+ UserProfile: {
352
+ user: r.one.User({
353
+ from: r.UserProfile.userId,
354
+ to: r.User.id,
355
+ onDelete: "Cascade", // onDelete: Cascade
356
+ onUpdate: "Cascade", // onUpdate: Cascade
357
+ }),
358
+ },
359
+ ```
360
+
361
+ Supported actions: `"Cascade"`, `"SetNull"`, `"Restrict"`, `"NoAction"`, `"SetDefault"`.
362
+
363
+ ### Splitting Relations Across Files
364
+
365
+ For large schemas, split relation definitions with `defineRelationsPart` and `mergeRelations`:
366
+
367
+ ```typescript
368
+ // file: user-relations.ts
369
+ export const userRelations = dsl.defineRelationsPart({ User, Workspace }, (r) => ({
370
+ User: {
371
+ workspace: r.one.Workspace({
372
+ from: r.User.workspaceId,
373
+ to: r.Workspace.id,
374
+ }),
375
+ },
376
+ }));
377
+
378
+ // file: workspace-relations.ts
379
+ export const workspaceRelations = dsl.defineRelationsPart({ User, Workspace }, (r) => ({
380
+ Workspace: {
381
+ users: r.many.User({
382
+ from: r.Workspace.id,
383
+ to: r.User.workspaceId,
384
+ }),
385
+ },
386
+ }));
387
+
388
+ // file: schema.ts
389
+ const relations = dsl.mergeRelations(userRelations, workspaceRelations);
390
+ const schema = dsl.schema({ User, Workspace }, { relations });
391
+ ```
392
+
393
+ ## Templates
394
+
395
+ Built-in field templates for common patterns. Templates use getters, so every spread produces fresh column instances (no shared mutable state):
396
+
397
+ ```typescript
398
+ const User = dsl.create({
399
+ name: "User",
400
+ schema: {
401
+ ...dsl.templates.base, // id, createdAt, updatedAt, deletedAt
402
+ ...dsl.templates.withMetadata, // metadata, settings (optional Json)
403
+ email: dsl.string().unique(),
404
+ },
405
+ });
406
+ ```
407
+
408
+ ### Available Templates
409
+
410
+ | Template | Fields |
411
+ |---|---|
412
+ | `dsl.templates.base` | `id` (ULID), `createdAt`, `updatedAt`, `deletedAt` (optional) |
413
+ | `dsl.templates.baseWithWorkspace` | Same as `base` + `workspaceId` (indexed String) |
414
+ | `dsl.templates.withMetadata` | `metadata`, `settings` (optional Json) |
415
+ | `dsl.templates.withActive` | `isActive` (Boolean, default `true`) |
416
+ | `dsl.templates.withAudit` | `createdById`, `updatedById` (optional, indexed) |
417
+ | `dsl.templates.withExternalId` | `externalId` (indexed String) |
418
+ | `dsl.templates.withNameDescription` | `name` (String), `description` (optional String) |
419
+
420
+ Individual fields are also available via `dsl.templates.fields`:
421
+
422
+ ```typescript
423
+ dsl.templates.fields.id // ULID primary key
424
+ dsl.templates.fields.datetime // DateTime with now + updatedAt + tz
425
+ dsl.templates.fields.title // String
426
+ dsl.templates.fields.description // Optional String
427
+ dsl.templates.fields.metadata // Optional Json
428
+ dsl.templates.fields.settings // Optional Json
429
+ dsl.templates.fields.isActive // Boolean default true
430
+ dsl.templates.fields.externalId // Indexed String
431
+ ```
432
+
433
+ ## Schema Assembly
434
+
435
+ ### Basic Schema
436
+
437
+ ```typescript
438
+ const schema = dsl.schema({ User, Workspace }, { relations });
439
+ ```
440
+
441
+ ### With Standalone Enums
442
+
443
+ Pass standalone enums alongside models:
444
+
445
+ ```typescript
446
+ const UserRole = dsl.enum({ name: "UserRole", options: ["admin", "user"] });
447
+
448
+ const schema = dsl.schema(
449
+ { UserRole, User, Workspace },
450
+ { relations }
451
+ );
452
+ ```
453
+
454
+ ### Grouped by Domain
455
+
456
+ ```typescript
457
+ const schema = dsl.schema({
458
+ UserRole,
459
+ User,
460
+ shipping: { Carrier, CarrierService },
461
+ printing: { PrintAccount, PrintJob },
462
+ }, { relations });
463
+ ```
464
+
465
+ ## Generating Prisma Output
466
+
467
+ ```typescript
468
+ const schema = dsl.schema({ User, Workspace }, { relations });
469
+
470
+ // Generate just the models + enums (no datasource/generator blocks)
471
+ const prismaBody = dsl.generate.prisma(schema);
472
+ ```
473
+
474
+ The output contains only model and enum definitions. Add your own datasource and generator blocks:
475
+
476
+ ```typescript
477
+ import { writeFileSync } from "fs";
478
+
479
+ const fullSchema = `datasource db {
480
+ provider = "postgresql"
481
+ url = env("DATABASE_URL")
482
+ }
483
+
484
+ generator client {
485
+ provider = "prisma-client"
486
+ output = "../src/generated/prisma"
487
+ }
488
+
489
+ ${prismaBody}
490
+ `;
491
+
492
+ writeFileSync("prisma/schema.prisma", fullSchema);
493
+ ```
494
+
495
+ ## Validation
496
+
497
+ Check for common issues before generating:
498
+
499
+ ```typescript
500
+ const errors = dsl.validateSchema(schema);
501
+
502
+ for (const error of errors) {
503
+ console.log(`[${error.type}] ${error.model}: ${error.message}`);
504
+ }
505
+ ```
506
+
507
+ Checks for:
508
+ - Duplicate model/enum names
509
+ - Missing FK columns referenced in relations
510
+ - Self-referential relations without aliases
511
+ - Models without an `id` column or compound `primaryKey`
512
+ - Relations targeting non-existent models
513
+
514
+ ## Full Example
515
+
516
+ See `tests/fixtures/advanced-schema.ts` for a comprehensive 11-model schema covering:
517
+
518
+ - Standalone and inline enums
519
+ - One-to-many, one-to-one, self-referential, and multi-FK relations
520
+ - Many-to-many via junction table
521
+ - Children (nested inline models)
522
+ - Array columns, optional FKs, composite indexes
523
+ - Relation aliases for disambiguation
package/package.json ADDED
@@ -0,0 +1,45 @@
1
+ {
2
+ "name": "@vibeorm/dsl",
3
+ "version": "0.2.0",
4
+ "description": "TypeScript DSL for defining Prisma schemas with type-safe models, relations, and templates",
5
+ "license": "MIT",
6
+ "keywords": [
7
+ "orm",
8
+ "dsl",
9
+ "prisma",
10
+ "schema",
11
+ "typescript",
12
+ "codegen",
13
+ "postgresql"
14
+ ],
15
+ "type": "module",
16
+ "exports": {
17
+ ".": {
18
+ "default": "./src/index.ts",
19
+ "types": "./src/index.ts"
20
+ }
21
+ },
22
+ "files": [
23
+ "src"
24
+ ],
25
+ "repository": {
26
+ "type": "git",
27
+ "url": "https://github.com/vibeorm/vibeorm.git",
28
+ "directory": "packages/dsl"
29
+ },
30
+ "homepage": "https://github.com/vibeorm/vibeorm",
31
+ "bugs": {
32
+ "url": "https://github.com/vibeorm/vibeorm/issues"
33
+ },
34
+ "publishConfig": {
35
+ "access": "public"
36
+ },
37
+ "engines": {
38
+ "bun": ">=1.1.0"
39
+ },
40
+ "devDependencies": {
41
+ "@types/bun": "latest",
42
+ "prisma": "^7.3.0",
43
+ "typescript": "^5.9.3"
44
+ }
45
+ }