@vibeorm/parser 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 +66 -0
- package/package.json +42 -0
- package/src/index.ts +27 -0
- package/src/parser.ts +688 -0
- package/src/schema-validator.ts +625 -0
- package/src/types.ts +223 -0
package/src/parser.ts
ADDED
|
@@ -0,0 +1,688 @@
|
|
|
1
|
+
import { getSchema } from "@mrleebo/prisma-ast";
|
|
2
|
+
import type {
|
|
3
|
+
Schema,
|
|
4
|
+
Model,
|
|
5
|
+
Field,
|
|
6
|
+
ScalarField,
|
|
7
|
+
EnumField,
|
|
8
|
+
RelationField,
|
|
9
|
+
RelationInfo,
|
|
10
|
+
RelationType,
|
|
11
|
+
PrimaryKey,
|
|
12
|
+
UniqueConstraint,
|
|
13
|
+
IndexDefinition,
|
|
14
|
+
Enum,
|
|
15
|
+
EnumValue,
|
|
16
|
+
DefaultValue,
|
|
17
|
+
PrismaScalarType,
|
|
18
|
+
} from "./types.ts";
|
|
19
|
+
import { PRISMA_TO_TS, PRISMA_TO_PG } from "./types.ts";
|
|
20
|
+
|
|
21
|
+
// ─── prisma-ast type helpers ──────────────────────────────────────
|
|
22
|
+
|
|
23
|
+
type AstSchema = ReturnType<typeof getSchema>;
|
|
24
|
+
type AstBlock = AstSchema["list"][number];
|
|
25
|
+
type AstModel = Extract<AstBlock, { type: "model" }>;
|
|
26
|
+
type AstEnum = Extract<AstBlock, { type: "enum" }>;
|
|
27
|
+
type AstProperty = AstModel["properties"][number];
|
|
28
|
+
type AstField = Extract<AstProperty, { type: "field" }>;
|
|
29
|
+
type AstAttribute = NonNullable<AstField["attributes"]>[number];
|
|
30
|
+
type AstAttributeArg = NonNullable<AstAttribute["args"]>[number];
|
|
31
|
+
|
|
32
|
+
const PRISMA_SCALAR_TYPES = new Set<string>([
|
|
33
|
+
"String",
|
|
34
|
+
"Boolean",
|
|
35
|
+
"Int",
|
|
36
|
+
"BigInt",
|
|
37
|
+
"Float",
|
|
38
|
+
"Decimal",
|
|
39
|
+
"DateTime",
|
|
40
|
+
"Json",
|
|
41
|
+
"Bytes",
|
|
42
|
+
]);
|
|
43
|
+
|
|
44
|
+
// ─── Public API ───────────────────────────────────────────────────
|
|
45
|
+
|
|
46
|
+
export function parsePrismaSchema(params: { source: string }): Schema {
|
|
47
|
+
const { source } = params;
|
|
48
|
+
const ast = getSchema(source);
|
|
49
|
+
|
|
50
|
+
// Collect enum names for field type resolution
|
|
51
|
+
const enumNames = new Set<string>();
|
|
52
|
+
for (const block of ast.list) {
|
|
53
|
+
if (block.type === "enum") {
|
|
54
|
+
enumNames.add(block.name);
|
|
55
|
+
}
|
|
56
|
+
}
|
|
57
|
+
|
|
58
|
+
// Collect model names for relation resolution
|
|
59
|
+
const modelNames = new Set<string>();
|
|
60
|
+
for (const block of ast.list) {
|
|
61
|
+
if (block.type === "model") {
|
|
62
|
+
modelNames.add(block.name);
|
|
63
|
+
}
|
|
64
|
+
}
|
|
65
|
+
|
|
66
|
+
// First pass: parse all models with raw fields
|
|
67
|
+
const rawModels = new Map<string, { ast: AstModel; fields: AstField[] }>();
|
|
68
|
+
for (const block of ast.list) {
|
|
69
|
+
if (block.type === "model") {
|
|
70
|
+
const fields = block.properties.filter(
|
|
71
|
+
(p): p is AstField => p.type === "field"
|
|
72
|
+
);
|
|
73
|
+
rawModels.set(block.name, { ast: block, fields });
|
|
74
|
+
}
|
|
75
|
+
}
|
|
76
|
+
|
|
77
|
+
// Second pass: resolve relations and build IR models
|
|
78
|
+
const models: Model[] = [];
|
|
79
|
+
for (const [modelName, { ast: modelAst, fields }] of rawModels) {
|
|
80
|
+
models.push(
|
|
81
|
+
buildModel({
|
|
82
|
+
modelAst,
|
|
83
|
+
fields,
|
|
84
|
+
enumNames,
|
|
85
|
+
modelNames,
|
|
86
|
+
allModels: rawModels,
|
|
87
|
+
})
|
|
88
|
+
);
|
|
89
|
+
}
|
|
90
|
+
|
|
91
|
+
// Parse enums
|
|
92
|
+
const enums: Enum[] = [];
|
|
93
|
+
for (const block of ast.list) {
|
|
94
|
+
if (block.type === "enum") {
|
|
95
|
+
enums.push(buildEnum({ enumAst: block }));
|
|
96
|
+
}
|
|
97
|
+
}
|
|
98
|
+
|
|
99
|
+
return { models, enums };
|
|
100
|
+
}
|
|
101
|
+
|
|
102
|
+
// ─── Model Builder ────────────────────────────────────────────────
|
|
103
|
+
|
|
104
|
+
function buildModel(params: {
|
|
105
|
+
modelAst: AstModel;
|
|
106
|
+
fields: AstField[];
|
|
107
|
+
enumNames: Set<string>;
|
|
108
|
+
modelNames: Set<string>;
|
|
109
|
+
allModels: Map<string, { ast: AstModel; fields: AstField[] }>;
|
|
110
|
+
}): Model {
|
|
111
|
+
const { modelAst, fields, enumNames, modelNames, allModels } = params;
|
|
112
|
+
|
|
113
|
+
const blockAttrs = modelAst.properties.filter(
|
|
114
|
+
(p): p is Extract<AstProperty, { type: "attribute" }> =>
|
|
115
|
+
p.type === "attribute"
|
|
116
|
+
);
|
|
117
|
+
|
|
118
|
+
const dbName = extractBlockMap({ blockAttrs }) ?? modelAst.name;
|
|
119
|
+
const primaryKey = extractPrimaryKey({ fields, blockAttrs });
|
|
120
|
+
const uniqueConstraints = extractUniqueConstraints({ blockAttrs });
|
|
121
|
+
const indexes = extractIndexes({ blockAttrs });
|
|
122
|
+
|
|
123
|
+
const irFields: Field[] = [];
|
|
124
|
+
|
|
125
|
+
// Build a map of field name → preceding documentation comments (///)
|
|
126
|
+
// Comments appear as separate nodes before the field they document
|
|
127
|
+
const fieldDocs = new Map<string, string>();
|
|
128
|
+
const props = modelAst.properties;
|
|
129
|
+
let pendingComment: string | undefined;
|
|
130
|
+
for (const prop of props) {
|
|
131
|
+
if (prop.type === "comment") {
|
|
132
|
+
const text = (prop as { text: string }).text;
|
|
133
|
+
if (text.startsWith("///")) {
|
|
134
|
+
const line = text.slice(3).trim();
|
|
135
|
+
pendingComment = pendingComment ? `${pendingComment}\n${line}` : line;
|
|
136
|
+
}
|
|
137
|
+
} else {
|
|
138
|
+
if (pendingComment && prop.type === "field") {
|
|
139
|
+
fieldDocs.set(prop.name, pendingComment);
|
|
140
|
+
}
|
|
141
|
+
pendingComment = undefined;
|
|
142
|
+
}
|
|
143
|
+
}
|
|
144
|
+
|
|
145
|
+
for (const field of fields) {
|
|
146
|
+
const fieldType =
|
|
147
|
+
typeof field.fieldType === "string"
|
|
148
|
+
? field.fieldType
|
|
149
|
+
: (field.fieldType as { name: string }).name;
|
|
150
|
+
const documentation = fieldDocs.get(field.name);
|
|
151
|
+
|
|
152
|
+
if (PRISMA_SCALAR_TYPES.has(fieldType)) {
|
|
153
|
+
irFields.push(buildScalarField({ field, fieldType, primaryKey, documentation }));
|
|
154
|
+
} else if (enumNames.has(fieldType)) {
|
|
155
|
+
irFields.push(buildEnumField({ field, fieldType, primaryKey, documentation }));
|
|
156
|
+
} else if (modelNames.has(fieldType)) {
|
|
157
|
+
irFields.push(
|
|
158
|
+
buildRelationField({
|
|
159
|
+
field,
|
|
160
|
+
fieldType,
|
|
161
|
+
modelName: modelAst.name,
|
|
162
|
+
allModels,
|
|
163
|
+
})
|
|
164
|
+
);
|
|
165
|
+
}
|
|
166
|
+
// Skip Unsupported types
|
|
167
|
+
}
|
|
168
|
+
|
|
169
|
+
return {
|
|
170
|
+
name: modelAst.name,
|
|
171
|
+
dbName,
|
|
172
|
+
fields: irFields,
|
|
173
|
+
primaryKey,
|
|
174
|
+
uniqueConstraints,
|
|
175
|
+
indexes,
|
|
176
|
+
};
|
|
177
|
+
}
|
|
178
|
+
|
|
179
|
+
// ─── Scalar Field Builder ─────────────────────────────────────────
|
|
180
|
+
|
|
181
|
+
function buildScalarField(params: {
|
|
182
|
+
field: AstField;
|
|
183
|
+
fieldType: string;
|
|
184
|
+
primaryKey: PrimaryKey;
|
|
185
|
+
documentation?: string;
|
|
186
|
+
}): ScalarField {
|
|
187
|
+
const { field, fieldType, primaryKey, documentation } = params;
|
|
188
|
+
const prismaType = fieldType as PrismaScalarType;
|
|
189
|
+
const attrs = field.attributes ?? [];
|
|
190
|
+
|
|
191
|
+
const hasId = attrs.some((a) => a.name === "id");
|
|
192
|
+
const hasUnique = attrs.some((a) => a.name === "unique");
|
|
193
|
+
const hasUpdatedAt = attrs.some((a) => a.name === "updatedAt");
|
|
194
|
+
const defaultValue = extractDefault({ attrs });
|
|
195
|
+
const nativeType = extractNativeType({ attrs });
|
|
196
|
+
const mapName = extractFieldMap({ attrs });
|
|
197
|
+
|
|
198
|
+
// A field is an ID if it has @id or is part of @@id
|
|
199
|
+
const isId = hasId || (primaryKey.fields.length > 0 && primaryKey.fields.includes(field.name));
|
|
200
|
+
|
|
201
|
+
return {
|
|
202
|
+
kind: "scalar",
|
|
203
|
+
name: field.name,
|
|
204
|
+
dbName: mapName ?? field.name,
|
|
205
|
+
prismaType,
|
|
206
|
+
tsType: PRISMA_TO_TS[prismaType],
|
|
207
|
+
pgType: PRISMA_TO_PG[prismaType],
|
|
208
|
+
isRequired: !field.optional,
|
|
209
|
+
isList: field.array ?? false,
|
|
210
|
+
isId,
|
|
211
|
+
isUnique: hasUnique,
|
|
212
|
+
isUpdatedAt: hasUpdatedAt,
|
|
213
|
+
default: defaultValue,
|
|
214
|
+
nativeType,
|
|
215
|
+
documentation,
|
|
216
|
+
};
|
|
217
|
+
}
|
|
218
|
+
|
|
219
|
+
// ─── Enum Field Builder ───────────────────────────────────────────
|
|
220
|
+
|
|
221
|
+
function buildEnumField(params: {
|
|
222
|
+
field: AstField;
|
|
223
|
+
fieldType: string;
|
|
224
|
+
primaryKey: PrimaryKey;
|
|
225
|
+
documentation?: string;
|
|
226
|
+
}): EnumField {
|
|
227
|
+
const { field, fieldType, primaryKey, documentation } = params;
|
|
228
|
+
const attrs = field.attributes ?? [];
|
|
229
|
+
|
|
230
|
+
const hasId = attrs.some((a) => a.name === "id");
|
|
231
|
+
const hasUnique = attrs.some((a) => a.name === "unique");
|
|
232
|
+
const defaultValue = extractDefault({ attrs });
|
|
233
|
+
const mapName = extractFieldMap({ attrs });
|
|
234
|
+
const isId = hasId || primaryKey.fields.includes(field.name);
|
|
235
|
+
|
|
236
|
+
return {
|
|
237
|
+
kind: "enum",
|
|
238
|
+
name: field.name,
|
|
239
|
+
dbName: mapName ?? field.name,
|
|
240
|
+
enumName: fieldType,
|
|
241
|
+
isRequired: !field.optional,
|
|
242
|
+
isList: field.array ?? false,
|
|
243
|
+
isId,
|
|
244
|
+
isUnique: hasUnique,
|
|
245
|
+
default: defaultValue,
|
|
246
|
+
documentation,
|
|
247
|
+
};
|
|
248
|
+
}
|
|
249
|
+
|
|
250
|
+
// ─── Relation Field Builder ───────────────────────────────────────
|
|
251
|
+
|
|
252
|
+
function buildRelationField(params: {
|
|
253
|
+
field: AstField;
|
|
254
|
+
fieldType: string;
|
|
255
|
+
modelName: string;
|
|
256
|
+
allModels: Map<string, { ast: AstModel; fields: AstField[] }>;
|
|
257
|
+
}): RelationField {
|
|
258
|
+
const { field, fieldType, modelName, allModels } = params;
|
|
259
|
+
const attrs = field.attributes ?? [];
|
|
260
|
+
|
|
261
|
+
const relationAttr = attrs.find((a) => a.name === "relation");
|
|
262
|
+
const relationName = extractRelationName({ relationAttr });
|
|
263
|
+
const relationFields = extractRelationArrayArg({
|
|
264
|
+
relationAttr,
|
|
265
|
+
key: "fields",
|
|
266
|
+
});
|
|
267
|
+
const relationReferences = extractRelationArrayArg({
|
|
268
|
+
relationAttr,
|
|
269
|
+
key: "references",
|
|
270
|
+
});
|
|
271
|
+
|
|
272
|
+
const isList = field.array ?? false;
|
|
273
|
+
const hasForeignKey = relationFields.length > 0;
|
|
274
|
+
|
|
275
|
+
// Determine relation type
|
|
276
|
+
const relationType = resolveRelationType({
|
|
277
|
+
isList,
|
|
278
|
+
isOptional: field.optional ?? false,
|
|
279
|
+
hasForeignKey,
|
|
280
|
+
relatedModelName: fieldType,
|
|
281
|
+
currentModelName: modelName,
|
|
282
|
+
allModels,
|
|
283
|
+
fieldName: field.name,
|
|
284
|
+
relationName,
|
|
285
|
+
});
|
|
286
|
+
|
|
287
|
+
const relation: RelationInfo = {
|
|
288
|
+
name: relationName,
|
|
289
|
+
fields: relationFields,
|
|
290
|
+
references: relationReferences,
|
|
291
|
+
relatedModel: fieldType,
|
|
292
|
+
type: relationType,
|
|
293
|
+
isForeignKey: hasForeignKey,
|
|
294
|
+
};
|
|
295
|
+
|
|
296
|
+
return {
|
|
297
|
+
kind: "relation",
|
|
298
|
+
name: field.name,
|
|
299
|
+
relatedModel: fieldType,
|
|
300
|
+
isList,
|
|
301
|
+
isRequired: !field.optional,
|
|
302
|
+
relation,
|
|
303
|
+
};
|
|
304
|
+
}
|
|
305
|
+
|
|
306
|
+
// ─── Relation Type Resolution ─────────────────────────────────────
|
|
307
|
+
|
|
308
|
+
function resolveRelationType(params: {
|
|
309
|
+
isList: boolean;
|
|
310
|
+
isOptional: boolean;
|
|
311
|
+
hasForeignKey: boolean;
|
|
312
|
+
relatedModelName: string;
|
|
313
|
+
currentModelName: string;
|
|
314
|
+
allModels: Map<string, { ast: AstModel; fields: AstField[] }>;
|
|
315
|
+
fieldName: string;
|
|
316
|
+
relationName: string | undefined;
|
|
317
|
+
}): RelationType {
|
|
318
|
+
const { isList, hasForeignKey, relatedModelName, allModels, relationName } =
|
|
319
|
+
params;
|
|
320
|
+
|
|
321
|
+
// If it's a list (Post[]), check if the other side is also a list with no FK (M:N)
|
|
322
|
+
if (isList) {
|
|
323
|
+
// Must not have FK on this side (implicit M:N has no fields/references)
|
|
324
|
+
if (!hasForeignKey) {
|
|
325
|
+
const relatedModel = allModels.get(relatedModelName);
|
|
326
|
+
if (relatedModel) {
|
|
327
|
+
const backRef = relatedModel.fields.find((f) => {
|
|
328
|
+
// Skip the current field itself (for self-referential models)
|
|
329
|
+
if (relatedModelName === params.currentModelName && f.name === params.fieldName) return false;
|
|
330
|
+
const ft =
|
|
331
|
+
typeof f.fieldType === "string"
|
|
332
|
+
? f.fieldType
|
|
333
|
+
: (f.fieldType as { name: string }).name;
|
|
334
|
+
if (ft !== params.currentModelName) return false;
|
|
335
|
+
if (!(f.array ?? false)) return false;
|
|
336
|
+
// The back-ref must also have no FK (no fields/references in @relation)
|
|
337
|
+
const relAttr = (f.attributes ?? []).find(
|
|
338
|
+
(a) => a.name === "relation"
|
|
339
|
+
);
|
|
340
|
+
const backRefFields = extractRelationArrayArg({ relationAttr: relAttr, key: "fields" });
|
|
341
|
+
if (backRefFields.length > 0) return false;
|
|
342
|
+
// Match by relation name if available
|
|
343
|
+
if (relationName) {
|
|
344
|
+
const backRelName = extractRelationName({ relationAttr: relAttr });
|
|
345
|
+
return backRelName === relationName;
|
|
346
|
+
}
|
|
347
|
+
return true;
|
|
348
|
+
});
|
|
349
|
+
if (backRef) return "manyToMany";
|
|
350
|
+
}
|
|
351
|
+
}
|
|
352
|
+
return "oneToMany";
|
|
353
|
+
}
|
|
354
|
+
|
|
355
|
+
// If this side holds the FK, it's the "many" side of a manyToOne
|
|
356
|
+
// or the non-owning side of a oneToOne
|
|
357
|
+
if (hasForeignKey) {
|
|
358
|
+
// Check if the related model's back-reference is also singular (1:1) or list (1:N)
|
|
359
|
+
const relatedModel = allModels.get(relatedModelName);
|
|
360
|
+
if (relatedModel) {
|
|
361
|
+
const backRef = relatedModel.fields.find((f) => {
|
|
362
|
+
const ft =
|
|
363
|
+
typeof f.fieldType === "string"
|
|
364
|
+
? f.fieldType
|
|
365
|
+
: (f.fieldType as { name: string }).name;
|
|
366
|
+
if (ft !== params.currentModelName) return false;
|
|
367
|
+
// Match by relation name if available
|
|
368
|
+
if (relationName) {
|
|
369
|
+
const relAttr = (f.attributes ?? []).find(
|
|
370
|
+
(a) => a.name === "relation"
|
|
371
|
+
);
|
|
372
|
+
const backRelName = extractRelationName({ relationAttr: relAttr });
|
|
373
|
+
return backRelName === relationName;
|
|
374
|
+
}
|
|
375
|
+
return true;
|
|
376
|
+
});
|
|
377
|
+
|
|
378
|
+
if (backRef && !(backRef.array ?? false)) {
|
|
379
|
+
// Back-reference is singular → this is a 1:1 where we hold the FK
|
|
380
|
+
return "oneToOne";
|
|
381
|
+
}
|
|
382
|
+
}
|
|
383
|
+
// Default: this side holds FK and the other side is a list → manyToOne
|
|
384
|
+
return "manyToOne";
|
|
385
|
+
}
|
|
386
|
+
|
|
387
|
+
// No FK on this side, not a list → oneToOne (the side without FK)
|
|
388
|
+
return "oneToOne";
|
|
389
|
+
}
|
|
390
|
+
|
|
391
|
+
// ─── Attribute Extraction Helpers ─────────────────────────────────
|
|
392
|
+
|
|
393
|
+
function extractDefault(params: {
|
|
394
|
+
attrs: AstAttribute[];
|
|
395
|
+
}): DefaultValue | undefined {
|
|
396
|
+
const { attrs } = params;
|
|
397
|
+
const defaultAttr = attrs.find((a) => a.name === "default");
|
|
398
|
+
if (!defaultAttr || !defaultAttr.args || defaultAttr.args.length === 0)
|
|
399
|
+
return undefined;
|
|
400
|
+
|
|
401
|
+
const arg = defaultAttr.args[0]!;
|
|
402
|
+
const value = arg.value;
|
|
403
|
+
|
|
404
|
+
// Function call: autoincrement(), now(), uuid(), cuid(), etc.
|
|
405
|
+
if (typeof value === "object" && value !== null && "type" in value) {
|
|
406
|
+
const obj = value as { type: string; name?: string; params?: string };
|
|
407
|
+
if (obj.type === "function") {
|
|
408
|
+
const funcName = obj.name ?? "";
|
|
409
|
+
switch (funcName) {
|
|
410
|
+
case "autoincrement":
|
|
411
|
+
return { kind: "autoincrement" };
|
|
412
|
+
case "now":
|
|
413
|
+
return { kind: "now" };
|
|
414
|
+
case "uuid":
|
|
415
|
+
return { kind: "uuid" };
|
|
416
|
+
case "cuid":
|
|
417
|
+
return { kind: "cuid" };
|
|
418
|
+
case "nanoid":
|
|
419
|
+
return { kind: "nanoid" };
|
|
420
|
+
case "ulid":
|
|
421
|
+
return { kind: "ulid" };
|
|
422
|
+
case "dbgenerated":
|
|
423
|
+
return {
|
|
424
|
+
kind: "dbgenerated",
|
|
425
|
+
value: obj.params ?? "",
|
|
426
|
+
};
|
|
427
|
+
default:
|
|
428
|
+
return { kind: "dbgenerated", value: funcName };
|
|
429
|
+
}
|
|
430
|
+
}
|
|
431
|
+
}
|
|
432
|
+
|
|
433
|
+
// Literal value
|
|
434
|
+
if (typeof value === "string") {
|
|
435
|
+
if (value === "true") return { kind: "literal", value: true };
|
|
436
|
+
if (value === "false") return { kind: "literal", value: false };
|
|
437
|
+
const num = Number(value);
|
|
438
|
+
if (!isNaN(num)) return { kind: "literal", value: num };
|
|
439
|
+
// String literal (may have quotes)
|
|
440
|
+
const cleaned = value.replace(/^"(.*)"$/, "$1");
|
|
441
|
+
return { kind: "literal", value: cleaned };
|
|
442
|
+
}
|
|
443
|
+
if (typeof value === "number") {
|
|
444
|
+
return { kind: "literal", value };
|
|
445
|
+
}
|
|
446
|
+
if (typeof value === "boolean") {
|
|
447
|
+
return { kind: "literal", value };
|
|
448
|
+
}
|
|
449
|
+
|
|
450
|
+
return undefined;
|
|
451
|
+
}
|
|
452
|
+
|
|
453
|
+
function extractNativeType(params: {
|
|
454
|
+
attrs: AstAttribute[];
|
|
455
|
+
}): string | undefined {
|
|
456
|
+
const { attrs } = params;
|
|
457
|
+
const dbAttr = attrs.find((a) => a.group === "db");
|
|
458
|
+
if (!dbAttr) return undefined;
|
|
459
|
+
return dbAttr.name;
|
|
460
|
+
}
|
|
461
|
+
|
|
462
|
+
function extractFieldMap(params: {
|
|
463
|
+
attrs: AstAttribute[];
|
|
464
|
+
}): string | undefined {
|
|
465
|
+
const { attrs } = params;
|
|
466
|
+
const mapAttr = attrs.find((a) => a.name === "map");
|
|
467
|
+
if (!mapAttr || !mapAttr.args || mapAttr.args.length === 0) return undefined;
|
|
468
|
+
const val = mapAttr.args[0]!.value;
|
|
469
|
+
if (typeof val === "string") {
|
|
470
|
+
return val.replace(/^"(.*)"$/, "$1");
|
|
471
|
+
}
|
|
472
|
+
return undefined;
|
|
473
|
+
}
|
|
474
|
+
|
|
475
|
+
function extractBlockMap(params: {
|
|
476
|
+
blockAttrs: Array<Extract<AstProperty, { type: "attribute" }>>;
|
|
477
|
+
}): string | undefined {
|
|
478
|
+
const { blockAttrs } = params;
|
|
479
|
+
const mapAttr = blockAttrs.find((a) => a.name === "map");
|
|
480
|
+
if (!mapAttr || !("args" in mapAttr)) return undefined;
|
|
481
|
+
const args = (mapAttr as { args?: AstAttributeArg[] }).args;
|
|
482
|
+
if (!args || args.length === 0) return undefined;
|
|
483
|
+
const val = args[0]!.value;
|
|
484
|
+
if (typeof val === "string") {
|
|
485
|
+
return val.replace(/^"(.*)"$/, "$1");
|
|
486
|
+
}
|
|
487
|
+
return undefined;
|
|
488
|
+
}
|
|
489
|
+
|
|
490
|
+
function extractRelationName(params: {
|
|
491
|
+
relationAttr: AstAttribute | undefined;
|
|
492
|
+
}): string | undefined {
|
|
493
|
+
const { relationAttr } = params;
|
|
494
|
+
if (!relationAttr || !relationAttr.args) return undefined;
|
|
495
|
+
|
|
496
|
+
// Relation name is the first positional (non-keyValue) argument
|
|
497
|
+
for (const arg of relationAttr.args) {
|
|
498
|
+
if (typeof arg.value === "string") {
|
|
499
|
+
return arg.value.replace(/^"(.*)"$/, "$1");
|
|
500
|
+
}
|
|
501
|
+
}
|
|
502
|
+
return undefined;
|
|
503
|
+
}
|
|
504
|
+
|
|
505
|
+
function extractRelationArrayArg(params: {
|
|
506
|
+
relationAttr: AstAttribute | undefined;
|
|
507
|
+
key: string;
|
|
508
|
+
}): string[] {
|
|
509
|
+
const { relationAttr, key } = params;
|
|
510
|
+
if (!relationAttr || !relationAttr.args) return [];
|
|
511
|
+
|
|
512
|
+
for (const arg of relationAttr.args) {
|
|
513
|
+
const val = arg.value;
|
|
514
|
+
if (
|
|
515
|
+
typeof val === "object" &&
|
|
516
|
+
val !== null &&
|
|
517
|
+
"type" in val &&
|
|
518
|
+
(val as { type: string }).type === "keyValue"
|
|
519
|
+
) {
|
|
520
|
+
const kv = val as { key: string; value: unknown };
|
|
521
|
+
if (kv.key === key) {
|
|
522
|
+
const arrValue = kv.value as { type: string; args: string[] };
|
|
523
|
+
if (arrValue.type === "array") {
|
|
524
|
+
return arrValue.args;
|
|
525
|
+
}
|
|
526
|
+
}
|
|
527
|
+
}
|
|
528
|
+
}
|
|
529
|
+
return [];
|
|
530
|
+
}
|
|
531
|
+
|
|
532
|
+
// ─── Primary Key Extraction ───────────────────────────────────────
|
|
533
|
+
|
|
534
|
+
function extractPrimaryKey(params: {
|
|
535
|
+
fields: AstField[];
|
|
536
|
+
blockAttrs: Array<Extract<AstProperty, { type: "attribute" }>>;
|
|
537
|
+
}): PrimaryKey {
|
|
538
|
+
const { fields, blockAttrs } = params;
|
|
539
|
+
|
|
540
|
+
// Check for @@id([field1, field2])
|
|
541
|
+
const compositeId = blockAttrs.find((a) => a.name === "id");
|
|
542
|
+
if (compositeId && "args" in compositeId) {
|
|
543
|
+
const args = (compositeId as { args?: AstAttributeArg[] }).args;
|
|
544
|
+
if (args && args.length > 0) {
|
|
545
|
+
const firstArg = args[0]!.value;
|
|
546
|
+
if (
|
|
547
|
+
typeof firstArg === "object" &&
|
|
548
|
+
firstArg !== null &&
|
|
549
|
+
"type" in firstArg
|
|
550
|
+
) {
|
|
551
|
+
const arr = firstArg as { type: string; args: string[] };
|
|
552
|
+
if (arr.type === "array") {
|
|
553
|
+
return { fields: arr.args, isComposite: true };
|
|
554
|
+
}
|
|
555
|
+
}
|
|
556
|
+
}
|
|
557
|
+
}
|
|
558
|
+
|
|
559
|
+
// Check for @id on individual fields
|
|
560
|
+
for (const field of fields) {
|
|
561
|
+
if (field.attributes?.some((a) => a.name === "id")) {
|
|
562
|
+
return { fields: [field.name], isComposite: false };
|
|
563
|
+
}
|
|
564
|
+
}
|
|
565
|
+
|
|
566
|
+
// Fallback — no explicit primary key
|
|
567
|
+
return { fields: [], isComposite: false };
|
|
568
|
+
}
|
|
569
|
+
|
|
570
|
+
// ─── Unique Constraint Extraction ─────────────────────────────────
|
|
571
|
+
|
|
572
|
+
function extractUniqueConstraints(params: {
|
|
573
|
+
blockAttrs: Array<Extract<AstProperty, { type: "attribute" }>>;
|
|
574
|
+
}): UniqueConstraint[] {
|
|
575
|
+
const { blockAttrs } = params;
|
|
576
|
+
const constraints: UniqueConstraint[] = [];
|
|
577
|
+
|
|
578
|
+
for (const attr of blockAttrs) {
|
|
579
|
+
if (attr.name !== "unique" || !("args" in attr)) continue;
|
|
580
|
+
const args = (attr as { args?: AstAttributeArg[] }).args;
|
|
581
|
+
if (!args || args.length === 0) continue;
|
|
582
|
+
|
|
583
|
+
const firstArg = args[0]!.value;
|
|
584
|
+
if (
|
|
585
|
+
typeof firstArg === "object" &&
|
|
586
|
+
firstArg !== null &&
|
|
587
|
+
"type" in firstArg
|
|
588
|
+
) {
|
|
589
|
+
const arr = firstArg as { type: string; args: string[] };
|
|
590
|
+
if (arr.type === "array") {
|
|
591
|
+
// Check for a name argument
|
|
592
|
+
let name: string | undefined;
|
|
593
|
+
if (args.length > 1) {
|
|
594
|
+
const nameArg = args.find((a) => {
|
|
595
|
+
const v = a.value;
|
|
596
|
+
return (
|
|
597
|
+
typeof v === "object" &&
|
|
598
|
+
v !== null &&
|
|
599
|
+
"type" in v &&
|
|
600
|
+
(v as { type: string }).type === "keyValue" &&
|
|
601
|
+
(v as { key: string }).key === "name"
|
|
602
|
+
);
|
|
603
|
+
});
|
|
604
|
+
if (nameArg) {
|
|
605
|
+
const kv = nameArg.value as { value: string };
|
|
606
|
+
name = typeof kv.value === "string" ? kv.value.replace(/^"(.*)"$/, "$1") : undefined;
|
|
607
|
+
}
|
|
608
|
+
}
|
|
609
|
+
|
|
610
|
+
constraints.push({ fields: arr.args, name });
|
|
611
|
+
}
|
|
612
|
+
}
|
|
613
|
+
}
|
|
614
|
+
|
|
615
|
+
return constraints;
|
|
616
|
+
}
|
|
617
|
+
|
|
618
|
+
// ─── Index Extraction ─────────────────────────────────────────────
|
|
619
|
+
|
|
620
|
+
function extractIndexes(params: {
|
|
621
|
+
blockAttrs: Array<Extract<AstProperty, { type: "attribute" }>>;
|
|
622
|
+
}): IndexDefinition[] {
|
|
623
|
+
const { blockAttrs } = params;
|
|
624
|
+
const indexes: IndexDefinition[] = [];
|
|
625
|
+
|
|
626
|
+
for (const attr of blockAttrs) {
|
|
627
|
+
if (attr.name !== "index" || !("args" in attr)) continue;
|
|
628
|
+
const args = (attr as { args?: AstAttributeArg[] }).args;
|
|
629
|
+
if (!args || args.length === 0) continue;
|
|
630
|
+
|
|
631
|
+
const firstArg = args[0]!.value;
|
|
632
|
+
if (
|
|
633
|
+
typeof firstArg === "object" &&
|
|
634
|
+
firstArg !== null &&
|
|
635
|
+
"type" in firstArg
|
|
636
|
+
) {
|
|
637
|
+
const arr = firstArg as { type: string; args: string[] };
|
|
638
|
+
if (arr.type === "array") {
|
|
639
|
+
indexes.push({ fields: arr.args, name: undefined });
|
|
640
|
+
}
|
|
641
|
+
}
|
|
642
|
+
}
|
|
643
|
+
|
|
644
|
+
return indexes;
|
|
645
|
+
}
|
|
646
|
+
|
|
647
|
+
// ─── Enum Builder ─────────────────────────────────────────────────
|
|
648
|
+
|
|
649
|
+
function buildEnum(params: { enumAst: AstEnum }): Enum {
|
|
650
|
+
const { enumAst } = params;
|
|
651
|
+
|
|
652
|
+
const values: EnumValue[] = [];
|
|
653
|
+
let enumDbName: string | undefined;
|
|
654
|
+
|
|
655
|
+
for (const entry of enumAst.enumerators) {
|
|
656
|
+
if (entry.type === "enumerator") {
|
|
657
|
+
// Extract @map("db_value") from enumerator-level attributes
|
|
658
|
+
const attrs = (entry as { attributes?: AstAttribute[] }).attributes ?? [];
|
|
659
|
+
const mapAttr = attrs.find((a) => a.name === "map");
|
|
660
|
+
let dbName: string | undefined;
|
|
661
|
+
if (mapAttr?.args && mapAttr.args.length > 0) {
|
|
662
|
+
const val = mapAttr.args[0]!.value;
|
|
663
|
+
if (typeof val === "string") {
|
|
664
|
+
dbName = val.replace(/^"(.*)"$/, "$1");
|
|
665
|
+
}
|
|
666
|
+
}
|
|
667
|
+
values.push({
|
|
668
|
+
name: entry.name,
|
|
669
|
+
dbName,
|
|
670
|
+
});
|
|
671
|
+
} else if (entry.type === "attribute") {
|
|
672
|
+
// Block-level @@map("db_name") appears as a BlockAttribute in enumerators
|
|
673
|
+
const blockAttr = entry as { name: string; args?: AstAttributeArg[] };
|
|
674
|
+
if (blockAttr.name === "map" && blockAttr.args && blockAttr.args.length > 0) {
|
|
675
|
+
const val = blockAttr.args[0]!.value;
|
|
676
|
+
if (typeof val === "string") {
|
|
677
|
+
enumDbName = val.replace(/^"(.*)"$/, "$1");
|
|
678
|
+
}
|
|
679
|
+
}
|
|
680
|
+
}
|
|
681
|
+
}
|
|
682
|
+
|
|
683
|
+
return {
|
|
684
|
+
name: enumAst.name,
|
|
685
|
+
dbName: enumDbName,
|
|
686
|
+
values,
|
|
687
|
+
};
|
|
688
|
+
}
|