@vibeorm/generator 1.0.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 +58 -0
- package/package.json +42 -0
- package/src/generate.ts +65 -0
- package/src/generators/generate-args.ts +322 -0
- package/src/generators/generate-client.ts +186 -0
- package/src/generators/generate-delegates.ts +213 -0
- package/src/generators/generate-enums.ts +28 -0
- package/src/generators/generate-inputs.ts +626 -0
- package/src/generators/generate-models.ts +103 -0
- package/src/generators/generate-result.ts +163 -0
- package/src/generators/generate-schemas.ts +474 -0
- package/src/index.ts +2 -0
- package/src/utils.ts +51 -0
package/README.md
ADDED
|
@@ -0,0 +1,58 @@
|
|
|
1
|
+
# @vibeorm/generator
|
|
2
|
+
|
|
3
|
+
TypeScript client generator for VibeORM. Takes a parsed schema IR and produces fully typed client files including model types, input types, delegates, and Zod v4 validation schemas.
|
|
4
|
+
|
|
5
|
+
## Installation
|
|
6
|
+
|
|
7
|
+
```bash
|
|
8
|
+
bun add @vibeorm/generator
|
|
9
|
+
```
|
|
10
|
+
|
|
11
|
+
## Usage
|
|
12
|
+
|
|
13
|
+
```ts
|
|
14
|
+
import { parsePrismaSchema } from "@vibeorm/parser";
|
|
15
|
+
import { generate } from "@vibeorm/generator";
|
|
16
|
+
import { writeFileSync, mkdirSync } from "fs";
|
|
17
|
+
import { join } from "path";
|
|
18
|
+
|
|
19
|
+
const schema = parsePrismaSchema({ source: prismaSchemaString });
|
|
20
|
+
const files = generate({ schema });
|
|
21
|
+
|
|
22
|
+
const outputDir = "./generated/vibeorm";
|
|
23
|
+
mkdirSync(outputDir, { recursive: true });
|
|
24
|
+
|
|
25
|
+
for (const file of files) {
|
|
26
|
+
writeFileSync(join(outputDir, file.filename), file.content);
|
|
27
|
+
}
|
|
28
|
+
```
|
|
29
|
+
|
|
30
|
+
## API
|
|
31
|
+
|
|
32
|
+
### `generate({ schema })`
|
|
33
|
+
|
|
34
|
+
Accepts a `Schema` IR (from `@vibeorm/parser`) and returns an array of `GeneratedFile` objects:
|
|
35
|
+
|
|
36
|
+
```ts
|
|
37
|
+
type GeneratedFile = {
|
|
38
|
+
filename: string;
|
|
39
|
+
content: string;
|
|
40
|
+
};
|
|
41
|
+
```
|
|
42
|
+
|
|
43
|
+
### Generated Files
|
|
44
|
+
|
|
45
|
+
| File | Contents |
|
|
46
|
+
|------|----------|
|
|
47
|
+
| `enums.ts` | TypeScript enums |
|
|
48
|
+
| `models.ts` | Model types (record shapes) |
|
|
49
|
+
| `inputs.ts` | Create, update, where, and filter input types |
|
|
50
|
+
| `args.ts` | Operation argument types (`FindManyArgs`, `CreateArgs`, etc.) |
|
|
51
|
+
| `result.ts` | Result types narrowed by `select`/`include`/`omit` |
|
|
52
|
+
| `delegates.ts` | Per-model delegate types (the `.findMany()`, `.create()` API surface) |
|
|
53
|
+
| `schemas.ts` | Zod v4 runtime validation schemas |
|
|
54
|
+
| `index.ts` | `VibeClient()` factory that wires model metadata into the runtime |
|
|
55
|
+
|
|
56
|
+
## License
|
|
57
|
+
|
|
58
|
+
[MIT](../../LICENSE)
|
package/package.json
ADDED
|
@@ -0,0 +1,42 @@
|
|
|
1
|
+
{
|
|
2
|
+
"name": "@vibeorm/generator",
|
|
3
|
+
"version": "1.0.0",
|
|
4
|
+
"description": "TypeScript client generator for VibeORM — produces typed delegates, inputs, and Zod schemas from a Prisma schema",
|
|
5
|
+
"license": "MIT",
|
|
6
|
+
"keywords": [
|
|
7
|
+
"orm",
|
|
8
|
+
"codegen",
|
|
9
|
+
"prisma",
|
|
10
|
+
"bun",
|
|
11
|
+
"typescript",
|
|
12
|
+
"postgresql"
|
|
13
|
+
],
|
|
14
|
+
"type": "module",
|
|
15
|
+
"exports": {
|
|
16
|
+
".": {
|
|
17
|
+
"default": "./src/index.ts",
|
|
18
|
+
"types": "./src/index.ts"
|
|
19
|
+
}
|
|
20
|
+
},
|
|
21
|
+
"files": [
|
|
22
|
+
"src"
|
|
23
|
+
],
|
|
24
|
+
"repository": {
|
|
25
|
+
"type": "git",
|
|
26
|
+
"url": "https://github.com/vibeorm/vibeorm.git",
|
|
27
|
+
"directory": "packages/generator"
|
|
28
|
+
},
|
|
29
|
+
"homepage": "https://github.com/vibeorm/vibeorm/tree/master/packages/generator",
|
|
30
|
+
"bugs": {
|
|
31
|
+
"url": "https://github.com/vibeorm/vibeorm/issues"
|
|
32
|
+
},
|
|
33
|
+
"publishConfig": {
|
|
34
|
+
"access": "public"
|
|
35
|
+
},
|
|
36
|
+
"engines": {
|
|
37
|
+
"bun": ">=1.1.0"
|
|
38
|
+
},
|
|
39
|
+
"dependencies": {
|
|
40
|
+
"@vibeorm/parser": "workspace:*"
|
|
41
|
+
}
|
|
42
|
+
}
|
package/src/generate.ts
ADDED
|
@@ -0,0 +1,65 @@
|
|
|
1
|
+
import type { Schema } from "@vibeorm/parser";
|
|
2
|
+
import { generateEnums } from "./generators/generate-enums.ts";
|
|
3
|
+
import { generateModels } from "./generators/generate-models.ts";
|
|
4
|
+
import {
|
|
5
|
+
generateInputs,
|
|
6
|
+
generateFilterTypes,
|
|
7
|
+
generateRelationFilterTypes,
|
|
8
|
+
} from "./generators/generate-inputs.ts";
|
|
9
|
+
import { generateArgs } from "./generators/generate-args.ts";
|
|
10
|
+
import { generateResult } from "./generators/generate-result.ts";
|
|
11
|
+
import { generateDelegates } from "./generators/generate-delegates.ts";
|
|
12
|
+
import { generateClient } from "./generators/generate-client.ts";
|
|
13
|
+
import { generateSchemas } from "./generators/generate-schemas.ts";
|
|
14
|
+
|
|
15
|
+
export type GeneratedFile = {
|
|
16
|
+
filename: string;
|
|
17
|
+
content: string;
|
|
18
|
+
};
|
|
19
|
+
|
|
20
|
+
/**
|
|
21
|
+
* Takes a parsed schema IR and produces all the generated TypeScript files.
|
|
22
|
+
*/
|
|
23
|
+
export function generate(params: { schema: Schema }): GeneratedFile[] {
|
|
24
|
+
const { schema } = params;
|
|
25
|
+
|
|
26
|
+
const files: GeneratedFile[] = [
|
|
27
|
+
{
|
|
28
|
+
filename: "enums.ts",
|
|
29
|
+
content: generateEnums({ enums: schema.enums }),
|
|
30
|
+
},
|
|
31
|
+
{
|
|
32
|
+
filename: "models.ts",
|
|
33
|
+
content: generateModels({ schema }),
|
|
34
|
+
},
|
|
35
|
+
{
|
|
36
|
+
filename: "inputs.ts",
|
|
37
|
+
content:
|
|
38
|
+
generateInputs({ schema }) +
|
|
39
|
+
generateFilterTypes() +
|
|
40
|
+
generateRelationFilterTypes({ schema }),
|
|
41
|
+
},
|
|
42
|
+
{
|
|
43
|
+
filename: "args.ts",
|
|
44
|
+
content: generateArgs({ schema }),
|
|
45
|
+
},
|
|
46
|
+
{
|
|
47
|
+
filename: "result.ts",
|
|
48
|
+
content: generateResult({ schema }),
|
|
49
|
+
},
|
|
50
|
+
{
|
|
51
|
+
filename: "delegates.ts",
|
|
52
|
+
content: generateDelegates({ schema }),
|
|
53
|
+
},
|
|
54
|
+
{
|
|
55
|
+
filename: "index.ts",
|
|
56
|
+
content: generateClient({ schema }),
|
|
57
|
+
},
|
|
58
|
+
{
|
|
59
|
+
filename: "schemas.ts",
|
|
60
|
+
content: generateSchemas({ schema }),
|
|
61
|
+
},
|
|
62
|
+
];
|
|
63
|
+
|
|
64
|
+
return files;
|
|
65
|
+
}
|
|
@@ -0,0 +1,322 @@
|
|
|
1
|
+
import type { Model, Schema, RelationField, ScalarField } from "@vibeorm/parser";
|
|
2
|
+
import { fileHeader } from "../utils.ts";
|
|
3
|
+
|
|
4
|
+
/**
|
|
5
|
+
* Generates Select, Include, and Args types for each model,
|
|
6
|
+
* plus the FindManyArgs, FindFirstArgs, FindUniqueArgs, etc.
|
|
7
|
+
*/
|
|
8
|
+
export function generateArgs(params: { schema: Schema }): string {
|
|
9
|
+
const { schema } = params;
|
|
10
|
+
const parts: string[] = [fileHeader()];
|
|
11
|
+
|
|
12
|
+
// Imports
|
|
13
|
+
const modelImports = schema.models.map((m) => `${m.name}WhereInput, ${m.name}WhereUniqueInput, ${m.name}OrderByInput, ${m.name}CreateInput, ${m.name}UpdateInput`).join(", ");
|
|
14
|
+
parts.push(`import type { ${modelImports}, SortOrder, SortOrderWithNulls } from "./inputs.ts";`);
|
|
15
|
+
|
|
16
|
+
const payloadImports = schema.models.map((m) => `$${m.name}Payload`).join(", ");
|
|
17
|
+
parts.push(`import type { ${payloadImports}, OperationPayload } from "./models.ts";\n`);
|
|
18
|
+
|
|
19
|
+
// Shared types
|
|
20
|
+
parts.push(`export type RelationStrategy = "query" | "join";
|
|
21
|
+
|
|
22
|
+
export type NumberWithAggregatesFilter = {
|
|
23
|
+
equals?: number;
|
|
24
|
+
not?: number;
|
|
25
|
+
lt?: number;
|
|
26
|
+
lte?: number;
|
|
27
|
+
gt?: number;
|
|
28
|
+
gte?: number;
|
|
29
|
+
};
|
|
30
|
+
`);
|
|
31
|
+
|
|
32
|
+
// Generate Select + Include + Args for each model
|
|
33
|
+
for (const model of schema.models) {
|
|
34
|
+
parts.push(generateSelectType({ model, schema }));
|
|
35
|
+
parts.push(generateIncludeType({ model, schema }));
|
|
36
|
+
parts.push(generateOperationArgs({ model }));
|
|
37
|
+
parts.push(generateAggregateInputTypes({ model }));
|
|
38
|
+
}
|
|
39
|
+
|
|
40
|
+
return parts.join("\n");
|
|
41
|
+
}
|
|
42
|
+
|
|
43
|
+
// ─── Select Type ──────────────────────────────────────────────────
|
|
44
|
+
|
|
45
|
+
function generateSelectType(params: {
|
|
46
|
+
model: Model;
|
|
47
|
+
schema: Schema;
|
|
48
|
+
}): string {
|
|
49
|
+
const { model, schema } = params;
|
|
50
|
+
|
|
51
|
+
const entries: string[] = [];
|
|
52
|
+
|
|
53
|
+
for (const field of model.fields) {
|
|
54
|
+
if (field.kind === "scalar" || field.kind === "enum") {
|
|
55
|
+
entries.push(` ${field.name}?: boolean;`);
|
|
56
|
+
} else if (field.kind === "relation") {
|
|
57
|
+
if (field.isList) {
|
|
58
|
+
entries.push(` ${field.name}?: boolean | ${field.relatedModel}FindManyArgs;`);
|
|
59
|
+
} else {
|
|
60
|
+
entries.push(` ${field.name}?: boolean | ${field.relatedModel}FindFirstArgs;`);
|
|
61
|
+
}
|
|
62
|
+
}
|
|
63
|
+
}
|
|
64
|
+
|
|
65
|
+
// Add _count support for relation counting
|
|
66
|
+
const countableRelations = model.fields.filter(
|
|
67
|
+
(f): f is RelationField => f.kind === "relation" && f.isList
|
|
68
|
+
);
|
|
69
|
+
if (countableRelations.length > 0) {
|
|
70
|
+
const countFields = countableRelations.map((f) => ` ${f.name}?: boolean;`);
|
|
71
|
+
entries.push(` _count?: boolean | {\n select?: {\n${countFields.join("\n")}\n };\n };`);
|
|
72
|
+
}
|
|
73
|
+
|
|
74
|
+
return `export type ${model.name}Select = {
|
|
75
|
+
${entries.join("\n")}
|
|
76
|
+
};
|
|
77
|
+
`;
|
|
78
|
+
}
|
|
79
|
+
|
|
80
|
+
// ─── Include Type ─────────────────────────────────────────────────
|
|
81
|
+
|
|
82
|
+
function generateIncludeType(params: {
|
|
83
|
+
model: Model;
|
|
84
|
+
schema: Schema;
|
|
85
|
+
}): string {
|
|
86
|
+
const { model } = params;
|
|
87
|
+
|
|
88
|
+
const relationFields = model.fields.filter(
|
|
89
|
+
(f): f is RelationField => f.kind === "relation"
|
|
90
|
+
);
|
|
91
|
+
|
|
92
|
+
if (relationFields.length === 0) {
|
|
93
|
+
return `export type ${model.name}Include = Record<string, never>;\n`;
|
|
94
|
+
}
|
|
95
|
+
|
|
96
|
+
const entries = relationFields.map((f) => {
|
|
97
|
+
if (f.isList) {
|
|
98
|
+
return ` ${f.name}?: boolean | ${f.relatedModel}FindManyArgs;`;
|
|
99
|
+
}
|
|
100
|
+
return ` ${f.name}?: boolean | ${f.relatedModel}FindFirstArgs;`;
|
|
101
|
+
});
|
|
102
|
+
|
|
103
|
+
// Add _count support for relation counting
|
|
104
|
+
const countableRelations = relationFields.filter((f) => f.isList);
|
|
105
|
+
if (countableRelations.length > 0) {
|
|
106
|
+
const countFields = countableRelations.map((f) => ` ${f.name}?: boolean;`);
|
|
107
|
+
entries.push(` _count?: boolean | {\n select?: {\n${countFields.join("\n")}\n };\n };`);
|
|
108
|
+
}
|
|
109
|
+
|
|
110
|
+
return `export type ${model.name}Include = {
|
|
111
|
+
${entries.join("\n")}
|
|
112
|
+
};
|
|
113
|
+
`;
|
|
114
|
+
}
|
|
115
|
+
|
|
116
|
+
// ─── Operation Args ───────────────────────────────────────────────
|
|
117
|
+
|
|
118
|
+
function generateOperationArgs(params: { model: Model }): string {
|
|
119
|
+
const { model } = params;
|
|
120
|
+
|
|
121
|
+
// Omit type — inverse of select (exclude specific fields)
|
|
122
|
+
const scalarFieldNames = model.fields
|
|
123
|
+
.filter((f) => f.kind === "scalar" || f.kind === "enum")
|
|
124
|
+
.map((f) => ` ${f.name}?: boolean;`);
|
|
125
|
+
|
|
126
|
+
const omitTypeDef = `export type ${model.name}Omit = {
|
|
127
|
+
${scalarFieldNames.join("\n")}
|
|
128
|
+
};
|
|
129
|
+
`;
|
|
130
|
+
|
|
131
|
+
return `${omitTypeDef}
|
|
132
|
+
export type ${model.name}FindManyArgs = {
|
|
133
|
+
select?: ${model.name}Select | null;
|
|
134
|
+
include?: ${model.name}Include | null;
|
|
135
|
+
omit?: ${model.name}Omit;
|
|
136
|
+
where?: ${model.name}WhereInput;
|
|
137
|
+
orderBy?: ${model.name}OrderByInput | ${model.name}OrderByInput[];
|
|
138
|
+
take?: number;
|
|
139
|
+
skip?: number;
|
|
140
|
+
cursor?: ${model.name}WhereUniqueInput;
|
|
141
|
+
distinct?: (keyof ${model.name}Select)[];
|
|
142
|
+
relationStrategy?: RelationStrategy;
|
|
143
|
+
};
|
|
144
|
+
|
|
145
|
+
export type ${model.name}FindFirstArgs = {
|
|
146
|
+
select?: ${model.name}Select | null;
|
|
147
|
+
include?: ${model.name}Include | null;
|
|
148
|
+
omit?: ${model.name}Omit;
|
|
149
|
+
where?: ${model.name}WhereInput;
|
|
150
|
+
orderBy?: ${model.name}OrderByInput | ${model.name}OrderByInput[];
|
|
151
|
+
take?: number;
|
|
152
|
+
skip?: number;
|
|
153
|
+
cursor?: ${model.name}WhereUniqueInput;
|
|
154
|
+
relationStrategy?: RelationStrategy;
|
|
155
|
+
};
|
|
156
|
+
|
|
157
|
+
export type ${model.name}FindUniqueArgs = {
|
|
158
|
+
select?: ${model.name}Select | null;
|
|
159
|
+
include?: ${model.name}Include | null;
|
|
160
|
+
omit?: ${model.name}Omit;
|
|
161
|
+
where: ${model.name}WhereUniqueInput;
|
|
162
|
+
relationStrategy?: RelationStrategy;
|
|
163
|
+
};
|
|
164
|
+
|
|
165
|
+
export type ${model.name}CreateArgs = {
|
|
166
|
+
select?: ${model.name}Select | null;
|
|
167
|
+
include?: ${model.name}Include | null;
|
|
168
|
+
omit?: ${model.name}Omit;
|
|
169
|
+
data: ${model.name}CreateInput;
|
|
170
|
+
relationStrategy?: RelationStrategy;
|
|
171
|
+
};
|
|
172
|
+
|
|
173
|
+
export type ${model.name}UpdateArgs = {
|
|
174
|
+
select?: ${model.name}Select | null;
|
|
175
|
+
include?: ${model.name}Include | null;
|
|
176
|
+
omit?: ${model.name}Omit;
|
|
177
|
+
where: ${model.name}WhereUniqueInput;
|
|
178
|
+
data: ${model.name}UpdateInput;
|
|
179
|
+
relationStrategy?: RelationStrategy;
|
|
180
|
+
};
|
|
181
|
+
|
|
182
|
+
export type ${model.name}UpsertArgs = {
|
|
183
|
+
select?: ${model.name}Select | null;
|
|
184
|
+
include?: ${model.name}Include | null;
|
|
185
|
+
where: ${model.name}WhereUniqueInput;
|
|
186
|
+
create: ${model.name}CreateInput;
|
|
187
|
+
update: ${model.name}UpdateInput;
|
|
188
|
+
};
|
|
189
|
+
|
|
190
|
+
export type ${model.name}DeleteArgs = {
|
|
191
|
+
select?: ${model.name}Select | null;
|
|
192
|
+
include?: ${model.name}Include | null;
|
|
193
|
+
where: ${model.name}WhereUniqueInput;
|
|
194
|
+
};
|
|
195
|
+
|
|
196
|
+
export type ${model.name}DeleteManyArgs = {
|
|
197
|
+
where?: ${model.name}WhereInput;
|
|
198
|
+
};
|
|
199
|
+
|
|
200
|
+
export type ${model.name}CountArgs = {
|
|
201
|
+
where?: ${model.name}WhereInput;
|
|
202
|
+
orderBy?: ${model.name}OrderByInput | ${model.name}OrderByInput[];
|
|
203
|
+
take?: number;
|
|
204
|
+
skip?: number;
|
|
205
|
+
cursor?: ${model.name}WhereUniqueInput;
|
|
206
|
+
};
|
|
207
|
+
|
|
208
|
+
export type ${model.name}CreateManyArgs = {
|
|
209
|
+
data: ${model.name}CreateInput | ${model.name}CreateInput[];
|
|
210
|
+
skipDuplicates?: boolean;
|
|
211
|
+
};
|
|
212
|
+
|
|
213
|
+
export type ${model.name}CreateManyAndReturnArgs = {
|
|
214
|
+
select?: ${model.name}Select | null;
|
|
215
|
+
include?: ${model.name}Include | null;
|
|
216
|
+
data: ${model.name}CreateInput | ${model.name}CreateInput[];
|
|
217
|
+
skipDuplicates?: boolean;
|
|
218
|
+
};
|
|
219
|
+
|
|
220
|
+
export type ${model.name}UpdateManyArgs = {
|
|
221
|
+
where?: ${model.name}WhereInput;
|
|
222
|
+
data: ${model.name}UpdateInput;
|
|
223
|
+
};
|
|
224
|
+
|
|
225
|
+
export type ${model.name}AggregateArgs = {
|
|
226
|
+
where?: ${model.name}WhereInput;
|
|
227
|
+
orderBy?: ${model.name}OrderByInput | ${model.name}OrderByInput[];
|
|
228
|
+
take?: number;
|
|
229
|
+
skip?: number;
|
|
230
|
+
cursor?: ${model.name}WhereUniqueInput;
|
|
231
|
+
_count?: true | ${model.name}CountAggregateInputType;
|
|
232
|
+
_avg?: ${model.name}AvgAggregateInputType;
|
|
233
|
+
_sum?: ${model.name}SumAggregateInputType;
|
|
234
|
+
_min?: ${model.name}MinAggregateInputType;
|
|
235
|
+
_max?: ${model.name}MaxAggregateInputType;
|
|
236
|
+
};
|
|
237
|
+
|
|
238
|
+
export type ${model.name}GroupByArgs = {
|
|
239
|
+
by: (keyof ${model.name}Select)[];
|
|
240
|
+
where?: ${model.name}WhereInput;
|
|
241
|
+
orderBy?: ${model.name}OrderByInput | ${model.name}OrderByInput[];
|
|
242
|
+
having?: ${model.name}ScalarWhereWithAggregatesInput;
|
|
243
|
+
take?: number;
|
|
244
|
+
skip?: number;
|
|
245
|
+
_count?: true | ${model.name}CountAggregateInputType;
|
|
246
|
+
_avg?: ${model.name}AvgAggregateInputType;
|
|
247
|
+
_sum?: ${model.name}SumAggregateInputType;
|
|
248
|
+
_min?: ${model.name}MinAggregateInputType;
|
|
249
|
+
_max?: ${model.name}MaxAggregateInputType;
|
|
250
|
+
};
|
|
251
|
+
`;
|
|
252
|
+
}
|
|
253
|
+
|
|
254
|
+
// ─── Aggregate Input Types ────────────────────────────────────────
|
|
255
|
+
|
|
256
|
+
const NUMERIC_PRISMA_TYPES = new Set(["Int", "Float", "Decimal", "BigInt"]);
|
|
257
|
+
|
|
258
|
+
function generateAggregateInputTypes(params: { model: Model }): string {
|
|
259
|
+
const { model } = params;
|
|
260
|
+
|
|
261
|
+
const scalarFields = model.fields.filter(
|
|
262
|
+
(f): f is ScalarField => f.kind === "scalar"
|
|
263
|
+
);
|
|
264
|
+
|
|
265
|
+
const numericFields = scalarFields.filter(
|
|
266
|
+
(f) => NUMERIC_PRISMA_TYPES.has(f.prismaType)
|
|
267
|
+
);
|
|
268
|
+
|
|
269
|
+
// CountAggregateInputType — all scalar fields + _all
|
|
270
|
+
const countEntries = [
|
|
271
|
+
` _all?: boolean;`,
|
|
272
|
+
...scalarFields.map((f) => ` ${f.name}?: boolean;`),
|
|
273
|
+
];
|
|
274
|
+
|
|
275
|
+
// AvgAggregateInputType — only numeric fields
|
|
276
|
+
const avgEntries = numericFields.map((f) => ` ${f.name}?: boolean;`);
|
|
277
|
+
|
|
278
|
+
// SumAggregateInputType — only numeric fields
|
|
279
|
+
const sumEntries = numericFields.map((f) => ` ${f.name}?: boolean;`);
|
|
280
|
+
|
|
281
|
+
// MinAggregateInputType — all scalar fields
|
|
282
|
+
const minEntries = scalarFields.map((f) => ` ${f.name}?: boolean;`);
|
|
283
|
+
|
|
284
|
+
// MaxAggregateInputType — all scalar fields
|
|
285
|
+
const maxEntries = scalarFields.map((f) => ` ${f.name}?: boolean;`);
|
|
286
|
+
|
|
287
|
+
// ScalarWhereWithAggregatesInput — for HAVING clause in groupBy
|
|
288
|
+
const havingEntries = scalarFields.map((f) => {
|
|
289
|
+
return ` ${f.name}?: {
|
|
290
|
+
_count?: NumberWithAggregatesFilter;
|
|
291
|
+
_avg?: NumberWithAggregatesFilter;
|
|
292
|
+
_sum?: NumberWithAggregatesFilter;
|
|
293
|
+
_min?: NumberWithAggregatesFilter;
|
|
294
|
+
_max?: NumberWithAggregatesFilter;
|
|
295
|
+
};`;
|
|
296
|
+
});
|
|
297
|
+
|
|
298
|
+
return `export type ${model.name}CountAggregateInputType = {
|
|
299
|
+
${countEntries.join("\n")}
|
|
300
|
+
};
|
|
301
|
+
|
|
302
|
+
export type ${model.name}AvgAggregateInputType = {
|
|
303
|
+
${avgEntries.length > 0 ? avgEntries.join("\n") : " [key: string]: never;"}
|
|
304
|
+
};
|
|
305
|
+
|
|
306
|
+
export type ${model.name}SumAggregateInputType = {
|
|
307
|
+
${sumEntries.length > 0 ? sumEntries.join("\n") : " [key: string]: never;"}
|
|
308
|
+
};
|
|
309
|
+
|
|
310
|
+
export type ${model.name}MinAggregateInputType = {
|
|
311
|
+
${minEntries.join("\n")}
|
|
312
|
+
};
|
|
313
|
+
|
|
314
|
+
export type ${model.name}MaxAggregateInputType = {
|
|
315
|
+
${maxEntries.join("\n")}
|
|
316
|
+
};
|
|
317
|
+
|
|
318
|
+
export type ${model.name}ScalarWhereWithAggregatesInput = {
|
|
319
|
+
${havingEntries.join("\n")}
|
|
320
|
+
};
|
|
321
|
+
`;
|
|
322
|
+
}
|
|
@@ -0,0 +1,186 @@
|
|
|
1
|
+
import type { Schema } from "@vibeorm/parser";
|
|
2
|
+
import { fileHeader, toCamelCase } from "../utils.ts";
|
|
3
|
+
|
|
4
|
+
/**
|
|
5
|
+
* Generates the main index.ts file that:
|
|
6
|
+
* - Re-exports all types
|
|
7
|
+
* - Creates the VibeClient class that wires delegates to the runtime
|
|
8
|
+
*/
|
|
9
|
+
export function generateClient(params: { schema: Schema }): string {
|
|
10
|
+
const { schema } = params;
|
|
11
|
+
const parts: string[] = [fileHeader()];
|
|
12
|
+
|
|
13
|
+
// Re-exports
|
|
14
|
+
parts.push(`export * from "./enums.ts";`);
|
|
15
|
+
parts.push(`export * from "./models.ts";`);
|
|
16
|
+
parts.push(`export * from "./inputs.ts";`);
|
|
17
|
+
parts.push(`export * from "./args.ts";`);
|
|
18
|
+
parts.push(`export * from "./result.ts";`);
|
|
19
|
+
parts.push(`export * from "./delegates.ts";`);
|
|
20
|
+
parts.push(`export * from "./schemas.ts";`);
|
|
21
|
+
parts.push(``);
|
|
22
|
+
|
|
23
|
+
// Import delegate types
|
|
24
|
+
const delegateImports = schema.models
|
|
25
|
+
.map((m) => `${m.name}Delegate`)
|
|
26
|
+
.join(", ");
|
|
27
|
+
parts.push(`import type { ${delegateImports} } from "./delegates.ts";`);
|
|
28
|
+
|
|
29
|
+
// Import runtime
|
|
30
|
+
parts.push(`import { createClient } from "@vibeorm/runtime";`);
|
|
31
|
+
parts.push(`import type { VibeClientOptions } from "@vibeorm/runtime";`);
|
|
32
|
+
parts.push(``);
|
|
33
|
+
|
|
34
|
+
// Import schemas for validation wiring
|
|
35
|
+
const schemaImports = schema.models.flatMap((m) => [
|
|
36
|
+
`${m.name}Schema`,
|
|
37
|
+
`${m.name}CreateInputSchema`,
|
|
38
|
+
`${m.name}UpdateInputSchema`,
|
|
39
|
+
`${m.name}WhereInputSchema`,
|
|
40
|
+
]);
|
|
41
|
+
parts.push(`import { ${schemaImports.join(", ")} } from "./schemas.ts";`);
|
|
42
|
+
parts.push(``);
|
|
43
|
+
|
|
44
|
+
// Generate the model metadata object that the runtime needs
|
|
45
|
+
parts.push(generateModelMeta({ schema }));
|
|
46
|
+
|
|
47
|
+
// Generate the schemas map for validation wiring
|
|
48
|
+
parts.push(generateSchemasMap({ schema }));
|
|
49
|
+
|
|
50
|
+
// Generate the VibeClient type (intersection of all delegates)
|
|
51
|
+
const clientProperties = schema.models
|
|
52
|
+
.map((m) => ` ${toCamelCase({ str: m.name })}: ${m.name}Delegate;`)
|
|
53
|
+
.join("\n");
|
|
54
|
+
|
|
55
|
+
parts.push(`export type VibeClientInstance = {
|
|
56
|
+
${clientProperties}
|
|
57
|
+
$transaction<T>(fn: (tx: VibeClientInstance) => Promise<T>): Promise<T>;
|
|
58
|
+
$transaction(promises: Promise<unknown>[]): Promise<unknown[]>;
|
|
59
|
+
$queryRaw<T = unknown>(strings: TemplateStringsArray, ...values: unknown[]): Promise<T[]>;
|
|
60
|
+
$executeRaw(strings: TemplateStringsArray, ...values: unknown[]): Promise<number>;
|
|
61
|
+
$queryRawUnsafe<T = unknown>(query: string, ...values: unknown[]): Promise<T[]>;
|
|
62
|
+
$executeRawUnsafe(query: string, ...values: unknown[]): Promise<number>;
|
|
63
|
+
$connect(): Promise<void>;
|
|
64
|
+
$disconnect(): Promise<void>;
|
|
65
|
+
};
|
|
66
|
+
`);
|
|
67
|
+
|
|
68
|
+
// Generate the VibeClient constructor function
|
|
69
|
+
parts.push(`export function VibeClient(options: VibeClientOptions): VibeClientInstance {
|
|
70
|
+
return createClient({
|
|
71
|
+
options,
|
|
72
|
+
modelMeta: MODEL_META,
|
|
73
|
+
schemas: options.validate ? SCHEMAS : undefined,
|
|
74
|
+
}) as unknown as VibeClientInstance;
|
|
75
|
+
}
|
|
76
|
+
`);
|
|
77
|
+
|
|
78
|
+
return parts.join("\n");
|
|
79
|
+
}
|
|
80
|
+
|
|
81
|
+
function generateModelMeta(params: { schema: Schema }): string {
|
|
82
|
+
const { schema } = params;
|
|
83
|
+
|
|
84
|
+
const modelEntries = schema.models.map((model) => {
|
|
85
|
+
const scalarFields = model.fields
|
|
86
|
+
.filter((f) => f.kind === "scalar" || f.kind === "enum")
|
|
87
|
+
.map((f) => {
|
|
88
|
+
const dbName = f.kind === "scalar" ? f.dbName : f.dbName;
|
|
89
|
+
const isUpdatedAt = f.kind === "scalar" && f.isUpdatedAt;
|
|
90
|
+
const extras: string[] = [];
|
|
91
|
+
if (isUpdatedAt) extras.push(`isUpdatedAt: true`);
|
|
92
|
+
if (f.kind === "scalar") extras.push(`type: "${f.prismaType}"`);
|
|
93
|
+
|
|
94
|
+
// Emit default kind for application-level generation (uuid, cuid, nanoid, ulid)
|
|
95
|
+
if (f.default) {
|
|
96
|
+
const appLevelDefaults = new Set(["uuid", "cuid", "nanoid", "ulid"]);
|
|
97
|
+
if (appLevelDefaults.has(f.default.kind)) {
|
|
98
|
+
extras.push(`hasDefault: "${f.default.kind}"`);
|
|
99
|
+
} else if (f.default.kind !== "dbgenerated") {
|
|
100
|
+
// autoincrement, now, literal — DB handles these, but runtime should know
|
|
101
|
+
extras.push(`hasDefault: true`);
|
|
102
|
+
}
|
|
103
|
+
}
|
|
104
|
+
|
|
105
|
+
// Extract @vibeorm.idPrefix from documentation comment
|
|
106
|
+
const doc = (f.kind === "scalar" || f.kind === "enum") ? f.documentation : undefined;
|
|
107
|
+
if (doc) {
|
|
108
|
+
const prefixMatch = doc.match(/@vibeorm\.idPrefix\(["']([^"']+)["']\)/);
|
|
109
|
+
if (prefixMatch) {
|
|
110
|
+
extras.push(`idPrefix: "${prefixMatch[1]}"`);
|
|
111
|
+
}
|
|
112
|
+
}
|
|
113
|
+
|
|
114
|
+
const extraStr = extras.length > 0 ? `, ${extras.join(", ")}` : "";
|
|
115
|
+
return ` { name: "${f.name}", dbName: "${dbName}", kind: "${f.kind}" as const${extraStr} }`;
|
|
116
|
+
});
|
|
117
|
+
|
|
118
|
+
const relationFields = model.fields
|
|
119
|
+
.filter((f) => f.kind === "relation")
|
|
120
|
+
.map((f) => {
|
|
121
|
+
if (f.kind !== "relation") return "";
|
|
122
|
+
const isM2M = f.relation.type === "manyToMany";
|
|
123
|
+
let joinTableLine = "";
|
|
124
|
+
if (isM2M) {
|
|
125
|
+
const sorted = [model.name, f.relatedModel].sort();
|
|
126
|
+
const jtName = `_${sorted[0]}To${sorted[1]}`;
|
|
127
|
+
joinTableLine = `\n joinTable: "${jtName}",`;
|
|
128
|
+
}
|
|
129
|
+
const relationNameLine = f.relation.name
|
|
130
|
+
? `\n relationName: "${f.relation.name}",`
|
|
131
|
+
: "";
|
|
132
|
+
return ` {
|
|
133
|
+
name: "${f.name}",
|
|
134
|
+
kind: "relation" as const,
|
|
135
|
+
relatedModel: "${f.relatedModel}",
|
|
136
|
+
type: "${f.relation.type}" as const,
|
|
137
|
+
isList: ${f.isList},
|
|
138
|
+
isForeignKey: ${f.relation.isForeignKey},
|
|
139
|
+
fields: ${JSON.stringify(f.relation.fields)},
|
|
140
|
+
references: ${JSON.stringify(f.relation.references)},${relationNameLine}${joinTableLine}
|
|
141
|
+
}`;
|
|
142
|
+
});
|
|
143
|
+
|
|
144
|
+
const pkFields = JSON.stringify(model.primaryKey.fields);
|
|
145
|
+
const uniqueFields = model.fields
|
|
146
|
+
.filter((f) => (f.kind === "scalar" || f.kind === "enum") && f.isUnique)
|
|
147
|
+
.map((f) => `"${f.name}"`);
|
|
148
|
+
|
|
149
|
+
return ` ${toCamelCase({ str: model.name })}: {
|
|
150
|
+
name: "${model.name}",
|
|
151
|
+
dbName: "${model.dbName}",
|
|
152
|
+
primaryKey: ${pkFields},
|
|
153
|
+
uniqueFields: [${uniqueFields.join(", ")}],
|
|
154
|
+
scalarFields: [
|
|
155
|
+
${scalarFields.join(",\n")}
|
|
156
|
+
],
|
|
157
|
+
relationFields: [
|
|
158
|
+
${relationFields.join(",\n")}
|
|
159
|
+
],
|
|
160
|
+
}`;
|
|
161
|
+
});
|
|
162
|
+
|
|
163
|
+
return `const MODEL_META = {
|
|
164
|
+
${modelEntries.join(",\n")}
|
|
165
|
+
} as const;
|
|
166
|
+
`;
|
|
167
|
+
}
|
|
168
|
+
|
|
169
|
+
function generateSchemasMap(params: { schema: Schema }): string {
|
|
170
|
+
const { schema } = params;
|
|
171
|
+
|
|
172
|
+
const entries = schema.models.map((model) => {
|
|
173
|
+
const key = toCamelCase({ str: model.name });
|
|
174
|
+
return ` ${key}: {
|
|
175
|
+
model: ${model.name}Schema,
|
|
176
|
+
createInput: ${model.name}CreateInputSchema,
|
|
177
|
+
updateInput: ${model.name}UpdateInputSchema,
|
|
178
|
+
whereInput: ${model.name}WhereInputSchema,
|
|
179
|
+
}`;
|
|
180
|
+
});
|
|
181
|
+
|
|
182
|
+
return `const SCHEMAS = {
|
|
183
|
+
${entries.join(",\n")}
|
|
184
|
+
};
|
|
185
|
+
`;
|
|
186
|
+
}
|