@vertz/compiler 0.2.12 → 0.2.14
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/dist/index.d.ts +75 -39
- package/dist/index.js +1242 -1027
- package/package.json +1 -1
package/dist/index.js
CHANGED
|
@@ -169,7 +169,11 @@ function isFromImport(identifier, moduleSpecifier) {
|
|
|
169
169
|
const importInfo = findImportForIdentifier(identifier);
|
|
170
170
|
if (!importInfo)
|
|
171
171
|
return false;
|
|
172
|
-
|
|
172
|
+
const actual = importInfo.importDecl.getModuleSpecifierValue();
|
|
173
|
+
if (actual === moduleSpecifier)
|
|
174
|
+
return true;
|
|
175
|
+
const metaEquivalent = moduleSpecifier.replace(/^@vertz\//, "vertz/");
|
|
176
|
+
return actual === metaEquivalent;
|
|
173
177
|
}
|
|
174
178
|
function findImportForIdentifier(identifier) {
|
|
175
179
|
const sourceFile = identifier.getSourceFile();
|
|
@@ -355,6 +359,104 @@ class AppAnalyzer extends BaseAnalyzer {
|
|
|
355
359
|
return result;
|
|
356
360
|
}
|
|
357
361
|
}
|
|
362
|
+
// src/analyzers/database-analyzer.ts
|
|
363
|
+
import { SyntaxKind as SyntaxKind3 } from "ts-morph";
|
|
364
|
+
class DatabaseAnalyzer extends BaseAnalyzer {
|
|
365
|
+
async analyze() {
|
|
366
|
+
const databases = [];
|
|
367
|
+
for (const file of this.project.getSourceFiles()) {
|
|
368
|
+
const calls = this.findCreateDbCalls(file);
|
|
369
|
+
for (const call of calls) {
|
|
370
|
+
const db = this.extractDatabase(call);
|
|
371
|
+
if (db)
|
|
372
|
+
databases.push(db);
|
|
373
|
+
}
|
|
374
|
+
}
|
|
375
|
+
return { databases };
|
|
376
|
+
}
|
|
377
|
+
findCreateDbCalls(file) {
|
|
378
|
+
const validCalls = [];
|
|
379
|
+
for (const call of file.getDescendantsOfKind(SyntaxKind3.CallExpression)) {
|
|
380
|
+
const expr = call.getExpression();
|
|
381
|
+
if (expr.isKind(SyntaxKind3.Identifier)) {
|
|
382
|
+
if (isFromImport(expr, "@vertz/db")) {
|
|
383
|
+
validCalls.push(call);
|
|
384
|
+
}
|
|
385
|
+
continue;
|
|
386
|
+
}
|
|
387
|
+
if (expr.isKind(SyntaxKind3.PropertyAccessExpression)) {
|
|
388
|
+
const propName = expr.getName();
|
|
389
|
+
if (propName !== "createDb")
|
|
390
|
+
continue;
|
|
391
|
+
const obj = expr.getExpression();
|
|
392
|
+
if (!obj.isKind(SyntaxKind3.Identifier))
|
|
393
|
+
continue;
|
|
394
|
+
const sourceFile = obj.getSourceFile();
|
|
395
|
+
const importDecl = sourceFile.getImportDeclarations().find((d) => d.getModuleSpecifierValue() === "@vertz/db" && d.getNamespaceImport()?.getText() === obj.getText());
|
|
396
|
+
if (importDecl) {
|
|
397
|
+
validCalls.push(call);
|
|
398
|
+
}
|
|
399
|
+
}
|
|
400
|
+
}
|
|
401
|
+
return validCalls;
|
|
402
|
+
}
|
|
403
|
+
extractDatabase(call) {
|
|
404
|
+
const loc = getSourceLocation(call);
|
|
405
|
+
const config = extractObjectLiteral(call, 0);
|
|
406
|
+
if (!config)
|
|
407
|
+
return null;
|
|
408
|
+
const modelsValue = getPropertyValue(config, "models");
|
|
409
|
+
if (!modelsValue)
|
|
410
|
+
return null;
|
|
411
|
+
const modelsObj = this.resolveObjectLiteral(modelsValue);
|
|
412
|
+
if (!modelsObj)
|
|
413
|
+
return null;
|
|
414
|
+
const modelKeys = this.extractModelKeys(modelsObj, loc);
|
|
415
|
+
return { modelKeys, ...loc };
|
|
416
|
+
}
|
|
417
|
+
resolveObjectLiteral(expr) {
|
|
418
|
+
if (expr.isKind(SyntaxKind3.ObjectLiteralExpression))
|
|
419
|
+
return expr;
|
|
420
|
+
if (expr.isKind(SyntaxKind3.Identifier)) {
|
|
421
|
+
const defs = expr.getDefinitionNodes();
|
|
422
|
+
for (const def of defs) {
|
|
423
|
+
if (def.isKind(SyntaxKind3.VariableDeclaration)) {
|
|
424
|
+
const init = def.getInitializer();
|
|
425
|
+
if (init?.isKind(SyntaxKind3.ObjectLiteralExpression))
|
|
426
|
+
return init;
|
|
427
|
+
}
|
|
428
|
+
}
|
|
429
|
+
}
|
|
430
|
+
return null;
|
|
431
|
+
}
|
|
432
|
+
extractModelKeys(obj, loc) {
|
|
433
|
+
const keys = [];
|
|
434
|
+
for (const prop of obj.getProperties()) {
|
|
435
|
+
if (prop.isKind(SyntaxKind3.PropertyAssignment)) {
|
|
436
|
+
if (prop.getNameNode().isKind(SyntaxKind3.ComputedPropertyName)) {
|
|
437
|
+
this.addDiagnostic({
|
|
438
|
+
severity: "warning",
|
|
439
|
+
code: "ENTITY_MODEL_NOT_REGISTERED",
|
|
440
|
+
message: "createDb() models object contains a computed property name. " + "Entity-model registration check may be incomplete — " + "computed keys cannot be statically resolved.",
|
|
441
|
+
...loc
|
|
442
|
+
});
|
|
443
|
+
} else {
|
|
444
|
+
keys.push(prop.getName());
|
|
445
|
+
}
|
|
446
|
+
} else if (prop.isKind(SyntaxKind3.ShorthandPropertyAssignment)) {
|
|
447
|
+
keys.push(prop.getName());
|
|
448
|
+
} else if (prop.isKind(SyntaxKind3.SpreadAssignment)) {
|
|
449
|
+
this.addDiagnostic({
|
|
450
|
+
severity: "warning",
|
|
451
|
+
code: "ENTITY_MODEL_NOT_REGISTERED",
|
|
452
|
+
message: "createDb() models object contains a spread assignment. " + "Entity-model registration check may be incomplete — " + "spread properties cannot be statically resolved.",
|
|
453
|
+
...loc
|
|
454
|
+
});
|
|
455
|
+
}
|
|
456
|
+
}
|
|
457
|
+
return keys;
|
|
458
|
+
}
|
|
459
|
+
}
|
|
358
460
|
// src/analyzers/dependency-graph-analyzer.ts
|
|
359
461
|
class DependencyGraphAnalyzer extends BaseAnalyzer {
|
|
360
462
|
input = { modules: [], middleware: [] };
|
|
@@ -603,1148 +705,1234 @@ class DependencyGraphAnalyzer extends BaseAnalyzer {
|
|
|
603
705
|
}
|
|
604
706
|
}
|
|
605
707
|
}
|
|
606
|
-
// src/analyzers/
|
|
708
|
+
// src/analyzers/entity-analyzer.ts
|
|
607
709
|
import { SyntaxKind as SyntaxKind4 } from "ts-morph";
|
|
710
|
+
var ENTITY_NAME_PATTERN = /^[a-z][a-z0-9-]*$/;
|
|
711
|
+
var CRUD_OPS = ["list", "get", "create", "update", "delete"];
|
|
608
712
|
|
|
609
|
-
|
|
610
|
-
|
|
611
|
-
|
|
612
|
-
|
|
613
|
-
const schemas = [];
|
|
614
|
-
for (const file of this.project.getSourceFiles()) {
|
|
615
|
-
if (!isSchemaFile(file))
|
|
616
|
-
continue;
|
|
617
|
-
for (const exportSymbol of file.getExportSymbols()) {
|
|
618
|
-
const declarations = exportSymbol.getDeclarations();
|
|
619
|
-
for (const decl of declarations) {
|
|
620
|
-
if (!decl.isKind(SyntaxKind3.VariableDeclaration))
|
|
621
|
-
continue;
|
|
622
|
-
const initializer = decl.getInitializer();
|
|
623
|
-
if (!initializer)
|
|
624
|
-
continue;
|
|
625
|
-
if (!isSchemaExpression(file, initializer))
|
|
626
|
-
continue;
|
|
627
|
-
const name = exportSymbol.getName();
|
|
628
|
-
const loc = getSourceLocation(decl);
|
|
629
|
-
const id = extractSchemaId(initializer);
|
|
630
|
-
schemas.push({
|
|
631
|
-
name,
|
|
632
|
-
...loc,
|
|
633
|
-
id: id ?? undefined,
|
|
634
|
-
moduleName: "",
|
|
635
|
-
namingConvention: parseSchemaName(name),
|
|
636
|
-
isNamed: id !== null
|
|
637
|
-
});
|
|
638
|
-
}
|
|
639
|
-
}
|
|
640
|
-
}
|
|
641
|
-
return { schemas };
|
|
642
|
-
}
|
|
643
|
-
}
|
|
644
|
-
var VALID_OPERATIONS = ["create", "read", "update", "list", "delete"];
|
|
645
|
-
var VALID_PARTS = ["Body", "Response", "Query", "Params", "Headers"];
|
|
646
|
-
function parseSchemaName(name) {
|
|
647
|
-
for (const op of VALID_OPERATIONS) {
|
|
648
|
-
if (!name.startsWith(op))
|
|
649
|
-
continue;
|
|
650
|
-
const rest = name.slice(op.length);
|
|
651
|
-
if (rest.length === 0)
|
|
652
|
-
continue;
|
|
653
|
-
for (const part of VALID_PARTS) {
|
|
654
|
-
if (!rest.endsWith(part))
|
|
655
|
-
continue;
|
|
656
|
-
const entity = rest.slice(0, -part.length);
|
|
657
|
-
if (entity.length === 0)
|
|
658
|
-
continue;
|
|
659
|
-
const firstChar = entity.at(0);
|
|
660
|
-
if (!firstChar || firstChar !== firstChar.toUpperCase())
|
|
661
|
-
continue;
|
|
662
|
-
return { operation: op, entity, part };
|
|
663
|
-
}
|
|
664
|
-
}
|
|
665
|
-
return {};
|
|
666
|
-
}
|
|
667
|
-
function isSchemaExpression(_file, expr) {
|
|
668
|
-
const root = findRootIdentifier(expr);
|
|
669
|
-
if (!root)
|
|
670
|
-
return false;
|
|
671
|
-
return isFromImport(root, "@vertz/schema");
|
|
672
|
-
}
|
|
673
|
-
function extractSchemaId(expr) {
|
|
674
|
-
let current = expr;
|
|
675
|
-
while (current.isKind(SyntaxKind3.CallExpression)) {
|
|
676
|
-
const access = current.getExpression();
|
|
677
|
-
if (access.isKind(SyntaxKind3.PropertyAccessExpression) && access.getName() === "id") {
|
|
678
|
-
const args = current.getArguments();
|
|
679
|
-
if (args.length === 1) {
|
|
680
|
-
const firstArg = args.at(0);
|
|
681
|
-
const value = firstArg ? getStringValue(firstArg) : null;
|
|
682
|
-
if (value !== null)
|
|
683
|
-
return value;
|
|
684
|
-
}
|
|
685
|
-
}
|
|
686
|
-
if (access.isKind(SyntaxKind3.PropertyAccessExpression)) {
|
|
687
|
-
current = access.getExpression();
|
|
688
|
-
} else {
|
|
689
|
-
break;
|
|
713
|
+
class EntityAnalyzer extends BaseAnalyzer {
|
|
714
|
+
debug(msg) {
|
|
715
|
+
if (process.env["VERTZ_DEBUG"]?.includes("entities")) {
|
|
716
|
+
console.log(`[entity-analyzer] ${msg}`);
|
|
690
717
|
}
|
|
691
718
|
}
|
|
692
|
-
return null;
|
|
693
|
-
}
|
|
694
|
-
function isSchemaFile(file) {
|
|
695
|
-
return file.getImportDeclarations().some((decl) => decl.getModuleSpecifierValue() === "@vertz/schema");
|
|
696
|
-
}
|
|
697
|
-
function createNamedSchemaRef(schemaName, sourceFile) {
|
|
698
|
-
return { kind: "named", schemaName, sourceFile };
|
|
699
|
-
}
|
|
700
|
-
function createInlineSchemaRef(sourceFile) {
|
|
701
|
-
return { kind: "inline", sourceFile };
|
|
702
|
-
}
|
|
703
|
-
function findRootIdentifier(expr) {
|
|
704
|
-
if (expr.isKind(SyntaxKind3.CallExpression)) {
|
|
705
|
-
return findRootIdentifier(expr.getExpression());
|
|
706
|
-
}
|
|
707
|
-
if (expr.isKind(SyntaxKind3.PropertyAccessExpression)) {
|
|
708
|
-
return findRootIdentifier(expr.getExpression());
|
|
709
|
-
}
|
|
710
|
-
if (expr.isKind(SyntaxKind3.Identifier)) {
|
|
711
|
-
return expr;
|
|
712
|
-
}
|
|
713
|
-
return null;
|
|
714
|
-
}
|
|
715
|
-
|
|
716
|
-
// src/analyzers/env-analyzer.ts
|
|
717
|
-
class EnvAnalyzer extends BaseAnalyzer {
|
|
718
719
|
async analyze() {
|
|
719
|
-
|
|
720
|
-
|
|
721
|
-
|
|
720
|
+
const entities = [];
|
|
721
|
+
const seenNames = new Map;
|
|
722
|
+
const modelExprs = new Map;
|
|
723
|
+
const files = this.project.getSourceFiles();
|
|
724
|
+
this.debug(`Scanning ${files.length} source files...`);
|
|
725
|
+
for (const file of files) {
|
|
726
|
+
const calls = this.findEntityCalls(file);
|
|
722
727
|
for (const call of calls) {
|
|
723
|
-
const
|
|
724
|
-
if (!
|
|
728
|
+
const result = this.extractEntity(file, call);
|
|
729
|
+
if (!result)
|
|
725
730
|
continue;
|
|
726
|
-
const
|
|
727
|
-
|
|
728
|
-
|
|
731
|
+
const { entity, modelExpr } = result;
|
|
732
|
+
const existing = seenNames.get(entity.name);
|
|
733
|
+
if (existing) {
|
|
734
|
+
this.addDiagnostic({
|
|
735
|
+
code: "ENTITY_DUPLICATE_NAME",
|
|
729
736
|
severity: "error",
|
|
730
|
-
|
|
731
|
-
|
|
732
|
-
})
|
|
737
|
+
message: `Entity "${entity.name}" is already defined at ${existing.sourceFile}:${existing.sourceLine}`,
|
|
738
|
+
...getSourceLocation(call)
|
|
739
|
+
});
|
|
733
740
|
continue;
|
|
734
741
|
}
|
|
735
|
-
|
|
736
|
-
|
|
737
|
-
|
|
738
|
-
|
|
739
|
-
|
|
740
|
-
|
|
742
|
+
seenNames.set(entity.name, entity);
|
|
743
|
+
entities.push(entity);
|
|
744
|
+
if (modelExpr)
|
|
745
|
+
modelExprs.set(entity.name, modelExpr);
|
|
746
|
+
this.debug(`Detected entity: "${entity.name}" at ${entity.sourceFile}:${entity.sourceLine}`);
|
|
747
|
+
this.debug(` model: ${entity.modelRef.variableName} (resolved: ${entity.modelRef.schemaRefs.resolved ? "✅" : "❌"})`);
|
|
748
|
+
const accessStatus = CRUD_OPS.map((op) => {
|
|
749
|
+
const kind = entity.access[op];
|
|
750
|
+
return `${op} ${kind === "false" ? "✗" : "✓"}`;
|
|
751
|
+
}).join(", ");
|
|
752
|
+
this.debug(` access: ${accessStatus}`);
|
|
753
|
+
if (entity.hooks.before.length > 0 || entity.hooks.after.length > 0) {
|
|
754
|
+
this.debug(` hooks: before[${entity.hooks.before.join(",")}], after[${entity.hooks.after.join(",")}]`);
|
|
755
|
+
}
|
|
756
|
+
if (entity.actions.length > 0) {
|
|
757
|
+
this.debug(` actions: ${entity.actions.map((a) => a.name).join(", ")}`);
|
|
741
758
|
}
|
|
742
|
-
env = {
|
|
743
|
-
...loc,
|
|
744
|
-
loadFiles,
|
|
745
|
-
schema,
|
|
746
|
-
variables: []
|
|
747
|
-
};
|
|
748
759
|
}
|
|
749
760
|
}
|
|
750
|
-
|
|
751
|
-
|
|
752
|
-
}
|
|
753
|
-
// src/analyzers/middleware-analyzer.ts
|
|
754
|
-
import { SyntaxKind as SyntaxKind6 } from "ts-morph";
|
|
755
|
-
|
|
756
|
-
// src/analyzers/service-analyzer.ts
|
|
757
|
-
import { SyntaxKind as SyntaxKind5 } from "ts-morph";
|
|
758
|
-
class ServiceAnalyzer extends BaseAnalyzer {
|
|
759
|
-
async analyze() {
|
|
760
|
-
return { services: [] };
|
|
761
|
+
this.resolveRelationEntities(entities, modelExprs);
|
|
762
|
+
return { entities };
|
|
761
763
|
}
|
|
762
|
-
|
|
763
|
-
const
|
|
764
|
-
for (const
|
|
765
|
-
const
|
|
766
|
-
|
|
767
|
-
const
|
|
768
|
-
if (!
|
|
764
|
+
findEntityCalls(file) {
|
|
765
|
+
const validCalls = [];
|
|
766
|
+
for (const call of file.getDescendantsOfKind(SyntaxKind4.CallExpression)) {
|
|
767
|
+
const expr = call.getExpression();
|
|
768
|
+
if (expr.isKind(SyntaxKind4.Identifier)) {
|
|
769
|
+
const isValid = isFromImport(expr, "@vertz/server");
|
|
770
|
+
if (!isValid && expr.getText() === "entity") {
|
|
771
|
+
this.addDiagnostic({
|
|
772
|
+
code: "ENTITY_UNRESOLVED_IMPORT",
|
|
773
|
+
severity: "error",
|
|
774
|
+
message: "entity() call does not resolve to @vertz/server",
|
|
775
|
+
...getSourceLocation(call)
|
|
776
|
+
});
|
|
769
777
|
continue;
|
|
770
|
-
|
|
771
|
-
|
|
772
|
-
|
|
773
|
-
|
|
774
|
-
|
|
775
|
-
name,
|
|
776
|
-
moduleName,
|
|
777
|
-
...loc,
|
|
778
|
-
inject,
|
|
779
|
-
methods
|
|
780
|
-
});
|
|
778
|
+
}
|
|
779
|
+
if (isValid) {
|
|
780
|
+
validCalls.push(call);
|
|
781
|
+
}
|
|
782
|
+
continue;
|
|
781
783
|
}
|
|
782
|
-
|
|
783
|
-
|
|
784
|
-
|
|
785
|
-
|
|
786
|
-
|
|
787
|
-
|
|
788
|
-
|
|
789
|
-
|
|
790
|
-
|
|
791
|
-
|
|
792
|
-
|
|
793
|
-
|
|
794
|
-
|
|
795
|
-
|
|
796
|
-
|
|
797
|
-
}
|
|
798
|
-
function parseInjectRefs(obj) {
|
|
799
|
-
return getProperties(obj).map(({ name, value }) => {
|
|
800
|
-
const resolvedToken = value.isKind(SyntaxKind5.Identifier) ? value.getText() : name;
|
|
801
|
-
return { localName: name, resolvedToken };
|
|
802
|
-
});
|
|
803
|
-
}
|
|
804
|
-
function extractMethodSignatures(expr) {
|
|
805
|
-
if (!expr.isKind(SyntaxKind5.ArrowFunction) && !expr.isKind(SyntaxKind5.FunctionExpression)) {
|
|
806
|
-
return [];
|
|
784
|
+
if (expr.isKind(SyntaxKind4.PropertyAccessExpression)) {
|
|
785
|
+
const propName = expr.getName();
|
|
786
|
+
if (propName !== "entity")
|
|
787
|
+
continue;
|
|
788
|
+
const obj = expr.getExpression();
|
|
789
|
+
if (!obj.isKind(SyntaxKind4.Identifier))
|
|
790
|
+
continue;
|
|
791
|
+
const sourceFile = obj.getSourceFile();
|
|
792
|
+
const importDecl = sourceFile.getImportDeclarations().find((d) => d.getModuleSpecifierValue() === "@vertz/server" && d.getNamespaceImport()?.getText() === obj.getText());
|
|
793
|
+
if (importDecl) {
|
|
794
|
+
validCalls.push(call);
|
|
795
|
+
}
|
|
796
|
+
}
|
|
797
|
+
}
|
|
798
|
+
return validCalls;
|
|
807
799
|
}
|
|
808
|
-
|
|
809
|
-
|
|
810
|
-
|
|
811
|
-
|
|
812
|
-
|
|
813
|
-
|
|
814
|
-
|
|
815
|
-
|
|
800
|
+
extractEntity(_file, call) {
|
|
801
|
+
const args = call.getArguments();
|
|
802
|
+
const loc = getSourceLocation(call);
|
|
803
|
+
if (args.length < 2) {
|
|
804
|
+
this.addDiagnostic({
|
|
805
|
+
code: "ENTITY_MISSING_ARGS",
|
|
806
|
+
severity: "error",
|
|
807
|
+
message: "entity() requires two arguments: name and config",
|
|
808
|
+
...loc
|
|
809
|
+
});
|
|
810
|
+
return null;
|
|
816
811
|
}
|
|
817
|
-
|
|
818
|
-
|
|
819
|
-
|
|
820
|
-
|
|
821
|
-
|
|
812
|
+
const name = getStringValue(args[0]);
|
|
813
|
+
if (name === null) {
|
|
814
|
+
this.addDiagnostic({
|
|
815
|
+
code: "ENTITY_NON_LITERAL_NAME",
|
|
816
|
+
severity: "error",
|
|
817
|
+
message: "entity() name must be a string literal",
|
|
818
|
+
...loc
|
|
819
|
+
});
|
|
820
|
+
return null;
|
|
821
|
+
}
|
|
822
|
+
if (!ENTITY_NAME_PATTERN.test(name)) {
|
|
823
|
+
this.addDiagnostic({
|
|
824
|
+
code: "ENTITY_INVALID_NAME",
|
|
825
|
+
severity: "error",
|
|
826
|
+
message: `Entity name must match /^[a-z][a-z0-9-]*$/. Got: "${name}"`,
|
|
827
|
+
...loc
|
|
828
|
+
});
|
|
829
|
+
return null;
|
|
830
|
+
}
|
|
831
|
+
const configObj = extractObjectLiteral(call, 1);
|
|
832
|
+
if (!configObj) {
|
|
833
|
+
this.addDiagnostic({
|
|
834
|
+
code: "ENTITY_CONFIG_NOT_OBJECT",
|
|
835
|
+
severity: "warning",
|
|
836
|
+
message: "entity() config must be an object literal for static analysis",
|
|
837
|
+
...loc
|
|
838
|
+
});
|
|
839
|
+
return null;
|
|
822
840
|
}
|
|
841
|
+
const modelRef = this.extractModelRef(configObj, loc);
|
|
842
|
+
if (!modelRef)
|
|
843
|
+
return null;
|
|
844
|
+
const access = this.extractAccess(configObj);
|
|
845
|
+
const hooks = this.extractHooks(configObj);
|
|
846
|
+
const actions = this.extractActions(configObj);
|
|
847
|
+
const modelExpr = getPropertyValue(configObj, "model");
|
|
848
|
+
const relations = this.extractRelations(configObj, modelExpr ?? undefined);
|
|
849
|
+
for (const action of actions) {
|
|
850
|
+
if (CRUD_OPS.includes(action.name)) {
|
|
851
|
+
this.addDiagnostic({
|
|
852
|
+
code: "ENTITY_ACTION_NAME_COLLISION",
|
|
853
|
+
severity: "error",
|
|
854
|
+
message: `Custom action "${action.name}" collides with built-in CRUD operation`,
|
|
855
|
+
...action
|
|
856
|
+
});
|
|
857
|
+
}
|
|
858
|
+
}
|
|
859
|
+
for (const customOp of Object.keys(access.custom)) {
|
|
860
|
+
if (!actions.some((a) => a.name === customOp)) {
|
|
861
|
+
this.addDiagnostic({
|
|
862
|
+
code: "ENTITY_UNKNOWN_ACCESS_OP",
|
|
863
|
+
severity: "warning",
|
|
864
|
+
message: `Unknown access operation "${customOp}" — not a CRUD op or custom action`,
|
|
865
|
+
...loc
|
|
866
|
+
});
|
|
867
|
+
}
|
|
868
|
+
}
|
|
869
|
+
return {
|
|
870
|
+
entity: { name, modelRef, access, hooks, actions, relations, ...loc },
|
|
871
|
+
modelExpr: modelExpr ?? undefined
|
|
872
|
+
};
|
|
823
873
|
}
|
|
824
|
-
|
|
825
|
-
|
|
826
|
-
|
|
827
|
-
|
|
828
|
-
|
|
829
|
-
|
|
830
|
-
|
|
831
|
-
|
|
832
|
-
|
|
833
|
-
|
|
834
|
-
|
|
835
|
-
const
|
|
836
|
-
|
|
837
|
-
|
|
838
|
-
|
|
839
|
-
|
|
840
|
-
|
|
874
|
+
extractModelRef(configObj, loc) {
|
|
875
|
+
const modelExpr = getPropertyValue(configObj, "model");
|
|
876
|
+
if (!modelExpr) {
|
|
877
|
+
this.addDiagnostic({
|
|
878
|
+
code: "ENTITY_MISSING_MODEL",
|
|
879
|
+
severity: "error",
|
|
880
|
+
message: "entity() requires a model property",
|
|
881
|
+
...loc
|
|
882
|
+
});
|
|
883
|
+
return null;
|
|
884
|
+
}
|
|
885
|
+
const variableName = modelExpr.isKind(SyntaxKind4.Identifier) ? modelExpr.getText() : modelExpr.getText();
|
|
886
|
+
let importSource;
|
|
887
|
+
if (modelExpr.isKind(SyntaxKind4.Identifier)) {
|
|
888
|
+
const importInfo = this.findImportForIdentifier(modelExpr);
|
|
889
|
+
if (importInfo) {
|
|
890
|
+
importSource = importInfo.importDecl.getModuleSpecifierValue();
|
|
891
|
+
}
|
|
892
|
+
}
|
|
893
|
+
const schemaRefs = this.resolveModelSchemas(modelExpr);
|
|
894
|
+
return { variableName, importSource, schemaRefs };
|
|
841
895
|
}
|
|
842
|
-
|
|
843
|
-
|
|
844
|
-
|
|
845
|
-
|
|
846
|
-
|
|
896
|
+
findImportForIdentifier(identifier) {
|
|
897
|
+
const sourceFile = identifier.getSourceFile();
|
|
898
|
+
const name = identifier.getText();
|
|
899
|
+
for (const importDecl of sourceFile.getImportDeclarations()) {
|
|
900
|
+
for (const specifier of importDecl.getNamedImports()) {
|
|
901
|
+
const localName = specifier.getAliasNode()?.getText() ?? specifier.getName();
|
|
902
|
+
if (localName === name) {
|
|
903
|
+
return { importDecl, originalName: specifier.getName() };
|
|
904
|
+
}
|
|
905
|
+
}
|
|
906
|
+
const nsImport = importDecl.getNamespaceImport();
|
|
907
|
+
if (nsImport && nsImport.getText() === name) {
|
|
908
|
+
return { importDecl, originalName: "*" };
|
|
909
|
+
}
|
|
910
|
+
}
|
|
911
|
+
return null;
|
|
847
912
|
}
|
|
848
|
-
|
|
849
|
-
|
|
850
|
-
|
|
851
|
-
|
|
852
|
-
|
|
853
|
-
|
|
854
|
-
|
|
855
|
-
|
|
856
|
-
|
|
913
|
+
resolveModelSchemas(modelExpr) {
|
|
914
|
+
try {
|
|
915
|
+
const modelType = modelExpr.getType();
|
|
916
|
+
const schemasProp = modelType.getProperty("schemas");
|
|
917
|
+
if (!schemasProp)
|
|
918
|
+
return { resolved: false };
|
|
919
|
+
const schemasType = schemasProp.getTypeAtLocation(modelExpr);
|
|
920
|
+
const response = this.extractSchemaType(schemasType, "response", modelExpr);
|
|
921
|
+
const createInput = this.extractSchemaType(schemasType, "createInput", modelExpr);
|
|
922
|
+
const updateInput = this.extractSchemaType(schemasType, "updateInput", modelExpr);
|
|
923
|
+
return {
|
|
924
|
+
response,
|
|
925
|
+
createInput,
|
|
926
|
+
updateInput,
|
|
927
|
+
resolved: response !== undefined || createInput !== undefined || updateInput !== undefined
|
|
928
|
+
};
|
|
929
|
+
} catch {
|
|
930
|
+
return { resolved: false };
|
|
931
|
+
}
|
|
857
932
|
}
|
|
858
|
-
|
|
859
|
-
|
|
860
|
-
|
|
861
|
-
|
|
862
|
-
|
|
863
|
-
|
|
864
|
-
const
|
|
865
|
-
|
|
866
|
-
|
|
867
|
-
|
|
868
|
-
|
|
869
|
-
|
|
870
|
-
|
|
871
|
-
|
|
872
|
-
|
|
873
|
-
|
|
874
|
-
|
|
875
|
-
|
|
876
|
-
|
|
877
|
-
|
|
878
|
-
|
|
879
|
-
|
|
880
|
-
|
|
881
|
-
|
|
882
|
-
|
|
883
|
-
|
|
884
|
-
|
|
885
|
-
|
|
886
|
-
|
|
887
|
-
|
|
888
|
-
|
|
889
|
-
|
|
890
|
-
|
|
891
|
-
|
|
892
|
-
|
|
893
|
-
|
|
894
|
-
|
|
895
|
-
|
|
896
|
-
|
|
897
|
-
|
|
898
|
-
|
|
933
|
+
extractSchemaType(parentType, propertyName, location) {
|
|
934
|
+
const prop = parentType.getProperty(propertyName);
|
|
935
|
+
if (!prop)
|
|
936
|
+
return;
|
|
937
|
+
const propType = prop.getTypeAtLocation(location);
|
|
938
|
+
const resolvedFields = this.resolveFieldsFromSchemaType(propType, location);
|
|
939
|
+
const jsonSchema = this.buildJsonSchema(resolvedFields);
|
|
940
|
+
return {
|
|
941
|
+
kind: "inline",
|
|
942
|
+
sourceFile: location.getSourceFile().getFilePath(),
|
|
943
|
+
jsonSchema,
|
|
944
|
+
resolvedFields
|
|
945
|
+
};
|
|
946
|
+
}
|
|
947
|
+
buildJsonSchema(resolvedFields) {
|
|
948
|
+
if (!resolvedFields || resolvedFields.length === 0) {
|
|
949
|
+
return {};
|
|
950
|
+
}
|
|
951
|
+
const properties = {};
|
|
952
|
+
const required = [];
|
|
953
|
+
for (const field of resolvedFields) {
|
|
954
|
+
const fieldSchema = this.tsTypeToJsonSchema(field.tsType);
|
|
955
|
+
properties[field.name] = fieldSchema;
|
|
956
|
+
if (!field.optional) {
|
|
957
|
+
required.push(field.name);
|
|
958
|
+
}
|
|
959
|
+
}
|
|
960
|
+
return {
|
|
961
|
+
type: "object",
|
|
962
|
+
properties,
|
|
963
|
+
...required.length > 0 ? { required } : {}
|
|
964
|
+
};
|
|
965
|
+
}
|
|
966
|
+
tsTypeToJsonSchema(tsType) {
|
|
967
|
+
switch (tsType) {
|
|
968
|
+
case "string":
|
|
969
|
+
return { type: "string" };
|
|
970
|
+
case "number":
|
|
971
|
+
return { type: "number" };
|
|
972
|
+
case "boolean":
|
|
973
|
+
return { type: "boolean" };
|
|
974
|
+
case "date":
|
|
975
|
+
return { type: "string", format: "date-time" };
|
|
976
|
+
case "unknown":
|
|
977
|
+
default:
|
|
978
|
+
return {};
|
|
979
|
+
}
|
|
980
|
+
}
|
|
981
|
+
resolveFieldsFromSchemaType(schemaType, location) {
|
|
982
|
+
try {
|
|
983
|
+
const parseProp = schemaType.getProperty("parse");
|
|
984
|
+
if (!parseProp)
|
|
985
|
+
return;
|
|
986
|
+
const parseType = parseProp.getTypeAtLocation(location);
|
|
987
|
+
const callSignatures = parseType.getCallSignatures();
|
|
988
|
+
if (callSignatures.length === 0)
|
|
989
|
+
return;
|
|
990
|
+
const returnType = callSignatures[0]?.getReturnType();
|
|
991
|
+
if (!returnType)
|
|
992
|
+
return;
|
|
993
|
+
const dataType = this.unwrapResultType(returnType, location);
|
|
994
|
+
if (!dataType)
|
|
995
|
+
return;
|
|
996
|
+
const properties = dataType.getProperties();
|
|
997
|
+
if (properties.length === 0)
|
|
998
|
+
return;
|
|
999
|
+
const fields = [];
|
|
1000
|
+
for (const fieldProp of properties) {
|
|
1001
|
+
const name = fieldProp.getName();
|
|
1002
|
+
const fieldType = fieldProp.getTypeAtLocation(location);
|
|
1003
|
+
const optional = fieldProp.isOptional();
|
|
1004
|
+
const tsType = this.mapTsType(fieldType);
|
|
1005
|
+
fields.push({ name, tsType, optional });
|
|
1006
|
+
}
|
|
1007
|
+
return fields;
|
|
1008
|
+
} catch {
|
|
1009
|
+
return;
|
|
1010
|
+
}
|
|
1011
|
+
}
|
|
1012
|
+
unwrapResultType(type, location) {
|
|
1013
|
+
if (type.isUnion()) {
|
|
1014
|
+
for (const member of type.getUnionTypes()) {
|
|
1015
|
+
const dataProp2 = member.getProperty("data");
|
|
1016
|
+
if (dataProp2) {
|
|
1017
|
+
return dataProp2.getTypeAtLocation(location);
|
|
899
1018
|
}
|
|
900
|
-
|
|
901
|
-
|
|
902
|
-
|
|
1019
|
+
}
|
|
1020
|
+
return;
|
|
1021
|
+
}
|
|
1022
|
+
const dataProp = type.getProperty("data");
|
|
1023
|
+
if (dataProp) {
|
|
1024
|
+
return dataProp.getTypeAtLocation(location);
|
|
1025
|
+
}
|
|
1026
|
+
return type;
|
|
1027
|
+
}
|
|
1028
|
+
mapTsType(type) {
|
|
1029
|
+
const typeText = type.getText();
|
|
1030
|
+
if (type.isUnion()) {
|
|
1031
|
+
const nonUndefined = type.getUnionTypes().filter((t) => !t.isUndefined());
|
|
1032
|
+
if (nonUndefined.length === 1 && nonUndefined[0]) {
|
|
1033
|
+
return this.mapTsType(nonUndefined[0]);
|
|
1034
|
+
}
|
|
1035
|
+
}
|
|
1036
|
+
if (type.isString() || type.isStringLiteral())
|
|
1037
|
+
return "string";
|
|
1038
|
+
if (type.isNumber() || type.isNumberLiteral())
|
|
1039
|
+
return "number";
|
|
1040
|
+
if (type.isBoolean() || type.isBooleanLiteral())
|
|
1041
|
+
return "boolean";
|
|
1042
|
+
if (typeText === "Date")
|
|
1043
|
+
return "date";
|
|
1044
|
+
return "unknown";
|
|
1045
|
+
}
|
|
1046
|
+
extractAccess(configObj) {
|
|
1047
|
+
const defaults = {
|
|
1048
|
+
list: "none",
|
|
1049
|
+
get: "none",
|
|
1050
|
+
create: "none",
|
|
1051
|
+
update: "none",
|
|
1052
|
+
delete: "none",
|
|
1053
|
+
custom: {}
|
|
1054
|
+
};
|
|
1055
|
+
const accessExpr = getPropertyValue(configObj, "access");
|
|
1056
|
+
if (!accessExpr || !accessExpr.isKind(SyntaxKind4.ObjectLiteralExpression))
|
|
1057
|
+
return defaults;
|
|
1058
|
+
const result = { ...defaults };
|
|
1059
|
+
const knownOps = new Set([...CRUD_OPS]);
|
|
1060
|
+
for (const { name, value } of getProperties(accessExpr)) {
|
|
1061
|
+
const kind = this.classifyAccessRule(value);
|
|
1062
|
+
if (knownOps.has(name)) {
|
|
1063
|
+
result[name] = kind;
|
|
1064
|
+
} else {
|
|
1065
|
+
result.custom[name] = kind;
|
|
1066
|
+
}
|
|
1067
|
+
}
|
|
1068
|
+
return result;
|
|
1069
|
+
}
|
|
1070
|
+
classifyAccessRule(expr) {
|
|
1071
|
+
const boolVal = getBooleanValue(expr);
|
|
1072
|
+
if (boolVal === false)
|
|
1073
|
+
return "false";
|
|
1074
|
+
if (boolVal === true)
|
|
1075
|
+
return "none";
|
|
1076
|
+
return "function";
|
|
1077
|
+
}
|
|
1078
|
+
extractHooks(configObj) {
|
|
1079
|
+
const hooks = { before: [], after: [] };
|
|
1080
|
+
const beforeExpr = getPropertyValue(configObj, "before");
|
|
1081
|
+
if (beforeExpr?.isKind(SyntaxKind4.ObjectLiteralExpression)) {
|
|
1082
|
+
for (const { name } of getProperties(beforeExpr)) {
|
|
1083
|
+
if (name === "create" || name === "update")
|
|
1084
|
+
hooks.before.push(name);
|
|
1085
|
+
}
|
|
1086
|
+
}
|
|
1087
|
+
const afterExpr = getPropertyValue(configObj, "after");
|
|
1088
|
+
if (afterExpr?.isKind(SyntaxKind4.ObjectLiteralExpression)) {
|
|
1089
|
+
for (const { name } of getProperties(afterExpr)) {
|
|
1090
|
+
if (name === "create" || name === "update" || name === "delete")
|
|
1091
|
+
hooks.after.push(name);
|
|
1092
|
+
}
|
|
1093
|
+
}
|
|
1094
|
+
return hooks;
|
|
1095
|
+
}
|
|
1096
|
+
extractActions(configObj) {
|
|
1097
|
+
const actionsExpr = getPropertyValue(configObj, "actions");
|
|
1098
|
+
if (!actionsExpr?.isKind(SyntaxKind4.ObjectLiteralExpression))
|
|
1099
|
+
return [];
|
|
1100
|
+
return getProperties(actionsExpr).map(({ name, value }) => {
|
|
1101
|
+
const actionObj = value.isKind(SyntaxKind4.ObjectLiteralExpression) ? value : null;
|
|
1102
|
+
const loc = getSourceLocation(value);
|
|
1103
|
+
const bodyExpr = actionObj ? getPropertyValue(actionObj, "body") : null;
|
|
1104
|
+
const responseExpr = actionObj ? getPropertyValue(actionObj, "response") : null;
|
|
1105
|
+
if (!bodyExpr && !responseExpr) {
|
|
1106
|
+
this.addDiagnostic({
|
|
1107
|
+
code: "ENTITY_ACTION_MISSING_SCHEMA",
|
|
1108
|
+
severity: "warning",
|
|
1109
|
+
message: `Custom action "${name}" is missing body and response schema`,
|
|
1110
|
+
...loc
|
|
1111
|
+
});
|
|
1112
|
+
}
|
|
1113
|
+
const body = bodyExpr ? this.resolveSchemaFromExpression(bodyExpr, loc) : undefined;
|
|
1114
|
+
const response = responseExpr ? this.resolveSchemaFromExpression(responseExpr, loc) : undefined;
|
|
1115
|
+
const methodExpr = actionObj ? getPropertyValue(actionObj, "method") : null;
|
|
1116
|
+
let method = "POST";
|
|
1117
|
+
if (methodExpr) {
|
|
1118
|
+
const methodStr = getStringValue(methodExpr);
|
|
1119
|
+
const validMethods = [
|
|
1120
|
+
"GET",
|
|
1121
|
+
"POST",
|
|
1122
|
+
"PUT",
|
|
1123
|
+
"DELETE",
|
|
1124
|
+
"PATCH",
|
|
1125
|
+
"HEAD",
|
|
1126
|
+
"OPTIONS"
|
|
1127
|
+
];
|
|
1128
|
+
if (methodStr && validMethods.includes(methodStr)) {
|
|
1129
|
+
method = methodStr;
|
|
1130
|
+
} else {
|
|
1131
|
+
this.addDiagnostic({
|
|
1132
|
+
code: "ENTITY_ACTION_INVALID_METHOD",
|
|
903
1133
|
severity: "error",
|
|
904
|
-
|
|
905
|
-
|
|
906
|
-
|
|
907
|
-
}));
|
|
908
|
-
continue;
|
|
1134
|
+
message: `Custom action "${name}" has invalid method "${methodStr ?? "(non-string)"}" — must be one of GET, POST, PUT, DELETE, PATCH, HEAD, OPTIONS`,
|
|
1135
|
+
...loc
|
|
1136
|
+
});
|
|
909
1137
|
}
|
|
910
|
-
const injectExpr = getPropertyValue(obj, "inject");
|
|
911
|
-
const inject = injectExpr?.isKind(SyntaxKind6.ObjectLiteralExpression) ? parseInjectRefs(injectExpr) : [];
|
|
912
|
-
const filePath = file.getFilePath();
|
|
913
|
-
const headers = this.resolveSchemaRef(obj, "headers", filePath);
|
|
914
|
-
const params = this.resolveSchemaRef(obj, "params", filePath);
|
|
915
|
-
const query = this.resolveSchemaRef(obj, "query", filePath);
|
|
916
|
-
const body = this.resolveSchemaRef(obj, "body", filePath);
|
|
917
|
-
const requires = this.resolveSchemaRef(obj, "requires", filePath);
|
|
918
|
-
const provides = this.resolveSchemaRef(obj, "provides", filePath);
|
|
919
|
-
middleware.push({
|
|
920
|
-
name,
|
|
921
|
-
...loc,
|
|
922
|
-
inject,
|
|
923
|
-
headers,
|
|
924
|
-
params,
|
|
925
|
-
query,
|
|
926
|
-
body,
|
|
927
|
-
requires,
|
|
928
|
-
provides
|
|
929
|
-
});
|
|
930
1138
|
}
|
|
931
|
-
|
|
932
|
-
|
|
1139
|
+
const pathExpr = actionObj ? getPropertyValue(actionObj, "path") : null;
|
|
1140
|
+
const path = pathExpr ? getStringValue(pathExpr) ?? undefined : undefined;
|
|
1141
|
+
const queryExpr = actionObj ? getPropertyValue(actionObj, "query") : null;
|
|
1142
|
+
const queryRef = queryExpr ? this.resolveSchemaFromExpression(queryExpr, loc) : undefined;
|
|
1143
|
+
const paramsExpr = actionObj ? getPropertyValue(actionObj, "params") : null;
|
|
1144
|
+
const paramsRef = paramsExpr ? this.resolveSchemaFromExpression(paramsExpr, loc) : undefined;
|
|
1145
|
+
const headersExpr = actionObj ? getPropertyValue(actionObj, "headers") : null;
|
|
1146
|
+
const headersRef = headersExpr ? this.resolveSchemaFromExpression(headersExpr, loc) : undefined;
|
|
1147
|
+
return {
|
|
1148
|
+
name,
|
|
1149
|
+
method,
|
|
1150
|
+
path,
|
|
1151
|
+
params: paramsRef,
|
|
1152
|
+
query: queryRef,
|
|
1153
|
+
headers: headersRef,
|
|
1154
|
+
body,
|
|
1155
|
+
response,
|
|
1156
|
+
...loc
|
|
1157
|
+
};
|
|
1158
|
+
});
|
|
933
1159
|
}
|
|
934
|
-
|
|
935
|
-
|
|
936
|
-
|
|
937
|
-
return;
|
|
938
|
-
if (expr.isKind(SyntaxKind6.Identifier)) {
|
|
939
|
-
const resolved = resolveIdentifier(expr, this.project);
|
|
940
|
-
const resolvedPath = resolved ? resolved.sourceFile.getFilePath() : filePath;
|
|
941
|
-
return createNamedSchemaRef(expr.getText(), resolvedPath);
|
|
1160
|
+
resolveSchemaFromExpression(expr, loc) {
|
|
1161
|
+
if (expr.isKind(SyntaxKind4.Identifier)) {
|
|
1162
|
+
const varName = expr.getText();
|
|
1163
|
+
return { kind: "named", schemaName: varName, sourceFile: loc.sourceFile };
|
|
942
1164
|
}
|
|
943
|
-
|
|
944
|
-
|
|
1165
|
+
try {
|
|
1166
|
+
const typeText = expr.getType().getText();
|
|
1167
|
+
return { kind: "inline", sourceFile: loc.sourceFile, jsonSchema: { __typeText: typeText } };
|
|
1168
|
+
} catch {
|
|
1169
|
+
return { kind: "inline", sourceFile: loc.sourceFile };
|
|
945
1170
|
}
|
|
946
|
-
return;
|
|
947
1171
|
}
|
|
948
|
-
|
|
949
|
-
|
|
950
|
-
|
|
951
|
-
|
|
952
|
-
|
|
953
|
-
|
|
954
|
-
|
|
955
|
-
|
|
956
|
-
|
|
957
|
-
|
|
958
|
-
|
|
959
|
-
|
|
960
|
-
|
|
961
|
-
|
|
962
|
-
|
|
963
|
-
|
|
964
|
-
|
|
965
|
-
|
|
966
|
-
|
|
967
|
-
|
|
968
|
-
|
|
1172
|
+
extractRelations(configObj, modelExpr) {
|
|
1173
|
+
const relExpr = getPropertyValue(configObj, "relations");
|
|
1174
|
+
if (!relExpr?.isKind(SyntaxKind4.ObjectLiteralExpression))
|
|
1175
|
+
return [];
|
|
1176
|
+
const modelRelTypes = modelExpr ? this.resolveModelRelationTypes(modelExpr) : new Map;
|
|
1177
|
+
return getProperties(relExpr).filter(({ value }) => {
|
|
1178
|
+
const boolVal = getBooleanValue(value);
|
|
1179
|
+
return boolVal !== false;
|
|
1180
|
+
}).map(({ name, value }) => {
|
|
1181
|
+
const boolVal = getBooleanValue(value);
|
|
1182
|
+
const selection = boolVal === true ? "all" : value.isKind(SyntaxKind4.ObjectLiteralExpression) ? getProperties(value).map((p) => p.name) : "all";
|
|
1183
|
+
const relType = modelRelTypes.get(name);
|
|
1184
|
+
return {
|
|
1185
|
+
name,
|
|
1186
|
+
type: relType?.type,
|
|
1187
|
+
entity: relType?.entity,
|
|
1188
|
+
selection
|
|
1189
|
+
};
|
|
1190
|
+
});
|
|
1191
|
+
}
|
|
1192
|
+
resolveModelRelationTypes(modelExpr) {
|
|
1193
|
+
const result = new Map;
|
|
1194
|
+
try {
|
|
1195
|
+
const modelType = modelExpr.getType();
|
|
1196
|
+
const relProp = modelType.getProperty("relations");
|
|
1197
|
+
if (!relProp)
|
|
1198
|
+
return result;
|
|
1199
|
+
const relType = relProp.getTypeAtLocation(modelExpr);
|
|
1200
|
+
for (const prop of relType.getProperties()) {
|
|
1201
|
+
const propName = prop.getName();
|
|
1202
|
+
const propType = prop.getTypeAtLocation(modelExpr);
|
|
1203
|
+
const typeProp = propType.getProperty("_type");
|
|
1204
|
+
if (!typeProp)
|
|
969
1205
|
continue;
|
|
970
|
-
|
|
971
|
-
|
|
972
|
-
|
|
973
|
-
|
|
974
|
-
|
|
975
|
-
|
|
976
|
-
if (optionsExpr?.isKind(SyntaxKind7.Identifier)) {
|
|
977
|
-
options = createNamedSchemaRef(optionsExpr.getText(), file.getFilePath());
|
|
978
|
-
}
|
|
979
|
-
const loc = getSourceLocation(call);
|
|
980
|
-
const idx = modules.length;
|
|
981
|
-
modules.push({
|
|
982
|
-
name,
|
|
983
|
-
...loc,
|
|
984
|
-
imports,
|
|
985
|
-
options,
|
|
986
|
-
services: [],
|
|
987
|
-
routers: [],
|
|
988
|
-
exports: []
|
|
989
|
-
});
|
|
990
|
-
if (varName) {
|
|
991
|
-
defVarToIndex.set(varName, idx);
|
|
1206
|
+
const typeType = typeProp.getTypeAtLocation(modelExpr);
|
|
1207
|
+
if (typeType.isStringLiteral()) {
|
|
1208
|
+
const literal = typeType.getLiteralValue();
|
|
1209
|
+
if (literal === "one" || literal === "many") {
|
|
1210
|
+
result.set(propName, { type: literal });
|
|
1211
|
+
}
|
|
992
1212
|
}
|
|
993
1213
|
}
|
|
994
|
-
}
|
|
995
|
-
|
|
996
|
-
|
|
997
|
-
|
|
998
|
-
|
|
999
|
-
|
|
1000
|
-
|
|
1001
|
-
const
|
|
1002
|
-
if (!
|
|
1214
|
+
} catch {}
|
|
1215
|
+
return result;
|
|
1216
|
+
}
|
|
1217
|
+
resolveRelationEntities(entities, modelExprs) {
|
|
1218
|
+
try {
|
|
1219
|
+
const tableToEntity = new Map;
|
|
1220
|
+
for (const entity of entities) {
|
|
1221
|
+
const modelExpr = modelExprs.get(entity.name);
|
|
1222
|
+
if (!modelExpr)
|
|
1003
1223
|
continue;
|
|
1004
|
-
const
|
|
1005
|
-
const
|
|
1006
|
-
if (
|
|
1224
|
+
const modelType = modelExpr.getType();
|
|
1225
|
+
const tableProp = modelType.getProperty("table");
|
|
1226
|
+
if (!tableProp)
|
|
1007
1227
|
continue;
|
|
1008
|
-
const
|
|
1009
|
-
|
|
1228
|
+
const tableType = tableProp.getTypeAtLocation(modelExpr);
|
|
1229
|
+
const typeText = tableType.getText(modelExpr);
|
|
1230
|
+
tableToEntity.set(typeText, entity.name);
|
|
1231
|
+
}
|
|
1232
|
+
if (tableToEntity.size === 0)
|
|
1233
|
+
return;
|
|
1234
|
+
for (const entity of entities) {
|
|
1235
|
+
const modelExpr = modelExprs.get(entity.name);
|
|
1236
|
+
if (!modelExpr)
|
|
1010
1237
|
continue;
|
|
1011
|
-
const
|
|
1012
|
-
|
|
1238
|
+
const modelType = modelExpr.getType();
|
|
1239
|
+
const relProp = modelType.getProperty("relations");
|
|
1240
|
+
if (!relProp)
|
|
1013
1241
|
continue;
|
|
1014
|
-
const
|
|
1015
|
-
|
|
1016
|
-
|
|
1242
|
+
const relType = relProp.getTypeAtLocation(modelExpr);
|
|
1243
|
+
for (const relation of entity.relations) {
|
|
1244
|
+
if (relation.entity)
|
|
1245
|
+
continue;
|
|
1246
|
+
const prop = relType.getProperty(relation.name);
|
|
1247
|
+
if (!prop)
|
|
1248
|
+
continue;
|
|
1249
|
+
const propType = prop.getTypeAtLocation(modelExpr);
|
|
1250
|
+
const targetProp = propType.getProperty("_target");
|
|
1251
|
+
if (!targetProp)
|
|
1252
|
+
continue;
|
|
1253
|
+
const targetType = targetProp.getTypeAtLocation(modelExpr);
|
|
1254
|
+
const callSigs = targetType.getCallSignatures();
|
|
1255
|
+
if (callSigs.length === 0)
|
|
1256
|
+
continue;
|
|
1257
|
+
const returnType = callSigs[0].getReturnType();
|
|
1258
|
+
const targetTypeText = returnType.getText(modelExpr);
|
|
1259
|
+
const targetEntity = tableToEntity.get(targetTypeText);
|
|
1260
|
+
if (targetEntity) {
|
|
1261
|
+
relation.entity = targetEntity;
|
|
1262
|
+
}
|
|
1017
1263
|
}
|
|
1018
1264
|
}
|
|
1019
|
-
}
|
|
1020
|
-
return { modules };
|
|
1265
|
+
} catch {}
|
|
1021
1266
|
}
|
|
1022
1267
|
}
|
|
1023
|
-
|
|
1024
|
-
|
|
1025
|
-
localName: name,
|
|
1026
|
-
isEnvImport: false
|
|
1027
|
-
}));
|
|
1028
|
-
}
|
|
1029
|
-
function extractIdentifierNames(expr) {
|
|
1030
|
-
if (!expr.isKind(SyntaxKind7.ArrayLiteralExpression))
|
|
1031
|
-
return [];
|
|
1032
|
-
return expr.getElements().filter((e) => e.isKind(SyntaxKind7.Identifier)).map((e) => e.getText());
|
|
1033
|
-
}
|
|
1034
|
-
// src/analyzers/route-analyzer.ts
|
|
1035
|
-
import { SyntaxKind as SyntaxKind8 } from "ts-morph";
|
|
1036
|
-
var HTTP_METHODS = {
|
|
1037
|
-
get: "GET",
|
|
1038
|
-
post: "POST",
|
|
1039
|
-
put: "PUT",
|
|
1040
|
-
patch: "PATCH",
|
|
1041
|
-
delete: "DELETE",
|
|
1042
|
-
head: "HEAD"
|
|
1043
|
-
};
|
|
1268
|
+
// src/analyzers/env-analyzer.ts
|
|
1269
|
+
import { SyntaxKind as SyntaxKind6 } from "ts-morph";
|
|
1044
1270
|
|
|
1045
|
-
|
|
1271
|
+
// src/analyzers/schema-analyzer.ts
|
|
1272
|
+
import { SyntaxKind as SyntaxKind5 } from "ts-morph";
|
|
1273
|
+
class SchemaAnalyzer extends BaseAnalyzer {
|
|
1046
1274
|
async analyze() {
|
|
1047
|
-
|
|
1048
|
-
}
|
|
1049
|
-
async analyzeForModules(context) {
|
|
1050
|
-
const routers = [];
|
|
1051
|
-
const knownModuleDefVars = new Set(context.moduleDefVariables.keys());
|
|
1275
|
+
const schemas = [];
|
|
1052
1276
|
for (const file of this.project.getSourceFiles()) {
|
|
1053
|
-
|
|
1054
|
-
|
|
1055
|
-
|
|
1056
|
-
|
|
1057
|
-
|
|
1277
|
+
if (!isSchemaFile(file))
|
|
1278
|
+
continue;
|
|
1279
|
+
for (const exportSymbol of file.getExportSymbols()) {
|
|
1280
|
+
const declarations = exportSymbol.getDeclarations();
|
|
1281
|
+
for (const decl of declarations) {
|
|
1282
|
+
if (!decl.isKind(SyntaxKind5.VariableDeclaration))
|
|
1058
1283
|
continue;
|
|
1059
|
-
const
|
|
1060
|
-
|
|
1061
|
-
|
|
1062
|
-
if (!
|
|
1063
|
-
|
|
1064
|
-
|
|
1065
|
-
|
|
1066
|
-
|
|
1067
|
-
|
|
1068
|
-
|
|
1069
|
-
}
|
|
1070
|
-
const loc = getSourceLocation(call);
|
|
1071
|
-
const injectExpr = obj ? getPropertyValue(obj, "inject") : null;
|
|
1072
|
-
const inject = injectExpr?.isKind(SyntaxKind8.ObjectLiteralExpression) ? parseInjectRefs(injectExpr) : [];
|
|
1073
|
-
const routes = this.extractRoutes(file, varName, prefix, moduleName);
|
|
1074
|
-
routers.push({
|
|
1075
|
-
name: varName,
|
|
1076
|
-
moduleName,
|
|
1284
|
+
const initializer = decl.getInitializer();
|
|
1285
|
+
if (!initializer)
|
|
1286
|
+
continue;
|
|
1287
|
+
if (!isSchemaExpression(file, initializer))
|
|
1288
|
+
continue;
|
|
1289
|
+
const name = exportSymbol.getName();
|
|
1290
|
+
const loc = getSourceLocation(decl);
|
|
1291
|
+
const id = extractSchemaId(initializer);
|
|
1292
|
+
schemas.push({
|
|
1293
|
+
name,
|
|
1077
1294
|
...loc,
|
|
1078
|
-
|
|
1079
|
-
|
|
1080
|
-
|
|
1295
|
+
id: id ?? undefined,
|
|
1296
|
+
moduleName: "",
|
|
1297
|
+
namingConvention: parseSchemaName(name),
|
|
1298
|
+
isNamed: id !== null
|
|
1081
1299
|
});
|
|
1082
1300
|
}
|
|
1083
1301
|
}
|
|
1084
|
-
this.detectUnknownRouterCalls(file, knownModuleDefVars);
|
|
1085
1302
|
}
|
|
1086
|
-
return {
|
|
1303
|
+
return { schemas };
|
|
1087
1304
|
}
|
|
1088
|
-
|
|
1089
|
-
|
|
1090
|
-
|
|
1091
|
-
|
|
1092
|
-
|
|
1093
|
-
|
|
1094
|
-
|
|
1095
|
-
|
|
1096
|
-
|
|
1097
|
-
|
|
1098
|
-
|
|
1099
|
-
if (
|
|
1305
|
+
}
|
|
1306
|
+
var VALID_OPERATIONS = ["create", "read", "update", "list", "delete"];
|
|
1307
|
+
var VALID_PARTS = ["Body", "Response", "Query", "Params", "Headers"];
|
|
1308
|
+
function parseSchemaName(name) {
|
|
1309
|
+
for (const op of VALID_OPERATIONS) {
|
|
1310
|
+
if (!name.startsWith(op))
|
|
1311
|
+
continue;
|
|
1312
|
+
const rest = name.slice(op.length);
|
|
1313
|
+
if (rest.length === 0)
|
|
1314
|
+
continue;
|
|
1315
|
+
for (const part of VALID_PARTS) {
|
|
1316
|
+
if (!rest.endsWith(part))
|
|
1100
1317
|
continue;
|
|
1101
|
-
const
|
|
1102
|
-
if (
|
|
1318
|
+
const entity = rest.slice(0, -part.length);
|
|
1319
|
+
if (entity.length === 0)
|
|
1103
1320
|
continue;
|
|
1104
|
-
const
|
|
1105
|
-
if (!
|
|
1321
|
+
const firstChar = entity.at(0);
|
|
1322
|
+
if (!firstChar || firstChar !== firstChar.toUpperCase())
|
|
1106
1323
|
continue;
|
|
1107
|
-
|
|
1108
|
-
|
|
1109
|
-
|
|
1110
|
-
|
|
1111
|
-
|
|
1112
|
-
|
|
1324
|
+
return { operation: op, entity, part };
|
|
1325
|
+
}
|
|
1326
|
+
}
|
|
1327
|
+
return {};
|
|
1328
|
+
}
|
|
1329
|
+
function isSchemaExpression(_file, expr) {
|
|
1330
|
+
const root = findRootIdentifier(expr);
|
|
1331
|
+
if (!root)
|
|
1332
|
+
return false;
|
|
1333
|
+
return isFromImport(root, "@vertz/schema");
|
|
1334
|
+
}
|
|
1335
|
+
function extractSchemaId(expr) {
|
|
1336
|
+
let current = expr;
|
|
1337
|
+
while (current.isKind(SyntaxKind5.CallExpression)) {
|
|
1338
|
+
const access = current.getExpression();
|
|
1339
|
+
if (access.isKind(SyntaxKind5.PropertyAccessExpression) && access.getName() === "id") {
|
|
1340
|
+
const args = current.getArguments();
|
|
1341
|
+
if (args.length === 1) {
|
|
1342
|
+
const firstArg = args.at(0);
|
|
1343
|
+
const value = firstArg ? getStringValue(firstArg) : null;
|
|
1344
|
+
if (value !== null)
|
|
1345
|
+
return value;
|
|
1346
|
+
}
|
|
1347
|
+
}
|
|
1348
|
+
if (access.isKind(SyntaxKind5.PropertyAccessExpression)) {
|
|
1349
|
+
current = access.getExpression();
|
|
1350
|
+
} else {
|
|
1351
|
+
break;
|
|
1113
1352
|
}
|
|
1114
1353
|
}
|
|
1115
|
-
|
|
1116
|
-
|
|
1117
|
-
|
|
1118
|
-
|
|
1119
|
-
|
|
1120
|
-
|
|
1121
|
-
|
|
1122
|
-
|
|
1123
|
-
|
|
1124
|
-
|
|
1125
|
-
|
|
1354
|
+
return null;
|
|
1355
|
+
}
|
|
1356
|
+
function isSchemaFile(file) {
|
|
1357
|
+
return file.getImportDeclarations().some((decl) => decl.getModuleSpecifierValue() === "@vertz/schema");
|
|
1358
|
+
}
|
|
1359
|
+
function createNamedSchemaRef(schemaName, sourceFile) {
|
|
1360
|
+
return { kind: "named", schemaName, sourceFile };
|
|
1361
|
+
}
|
|
1362
|
+
function createInlineSchemaRef(sourceFile) {
|
|
1363
|
+
return { kind: "inline", sourceFile };
|
|
1364
|
+
}
|
|
1365
|
+
function findRootIdentifier(expr) {
|
|
1366
|
+
if (expr.isKind(SyntaxKind5.CallExpression)) {
|
|
1367
|
+
return findRootIdentifier(expr.getExpression());
|
|
1368
|
+
}
|
|
1369
|
+
if (expr.isKind(SyntaxKind5.PropertyAccessExpression)) {
|
|
1370
|
+
return findRootIdentifier(expr.getExpression());
|
|
1371
|
+
}
|
|
1372
|
+
if (expr.isKind(SyntaxKind5.Identifier)) {
|
|
1373
|
+
return expr;
|
|
1374
|
+
}
|
|
1375
|
+
return null;
|
|
1376
|
+
}
|
|
1377
|
+
|
|
1378
|
+
// src/analyzers/env-analyzer.ts
|
|
1379
|
+
class EnvAnalyzer extends BaseAnalyzer {
|
|
1380
|
+
async analyze() {
|
|
1381
|
+
let env;
|
|
1382
|
+
for (const file of this.project.getSourceFiles()) {
|
|
1383
|
+
const calls = findCallExpressions(file, "vertz", "env");
|
|
1384
|
+
for (const call of calls) {
|
|
1385
|
+
const obj = extractObjectLiteral(call, 0);
|
|
1386
|
+
if (!obj)
|
|
1387
|
+
continue;
|
|
1388
|
+
const loc = getSourceLocation(call);
|
|
1389
|
+
if (env) {
|
|
1390
|
+
this.addDiagnostic(createDiagnosticFromLocation(loc, {
|
|
1391
|
+
severity: "error",
|
|
1392
|
+
code: "VERTZ_ENV_DUPLICATE",
|
|
1393
|
+
message: "Multiple vertz.env() calls found. Only one is allowed."
|
|
1394
|
+
}));
|
|
1395
|
+
continue;
|
|
1396
|
+
}
|
|
1397
|
+
const loadExpr = getPropertyValue(obj, "load");
|
|
1398
|
+
const loadFiles = loadExpr ? getArrayElements(loadExpr).map((e) => getStringValue(e)).filter((v) => v !== null) : [];
|
|
1399
|
+
const schemaExpr = getPropertyValue(obj, "schema");
|
|
1400
|
+
let schema;
|
|
1401
|
+
if (schemaExpr?.isKind(SyntaxKind6.Identifier)) {
|
|
1402
|
+
schema = createNamedSchemaRef(schemaExpr.getText(), file.getFilePath());
|
|
1403
|
+
}
|
|
1404
|
+
env = {
|
|
1405
|
+
...loc,
|
|
1406
|
+
loadFiles,
|
|
1407
|
+
schema,
|
|
1408
|
+
variables: []
|
|
1409
|
+
};
|
|
1126
1410
|
}
|
|
1127
1411
|
}
|
|
1128
|
-
return
|
|
1412
|
+
return { env };
|
|
1129
1413
|
}
|
|
1130
|
-
|
|
1131
|
-
|
|
1132
|
-
|
|
1133
|
-
|
|
1134
|
-
|
|
1135
|
-
|
|
1136
|
-
|
|
1137
|
-
|
|
1138
|
-
|
|
1139
|
-
return false;
|
|
1140
|
-
return this.chainResolvesToVariable(obj, routerVarName);
|
|
1141
|
-
});
|
|
1414
|
+
}
|
|
1415
|
+
// src/analyzers/middleware-analyzer.ts
|
|
1416
|
+
import { SyntaxKind as SyntaxKind8 } from "ts-morph";
|
|
1417
|
+
|
|
1418
|
+
// src/analyzers/service-analyzer.ts
|
|
1419
|
+
import { SyntaxKind as SyntaxKind7 } from "ts-morph";
|
|
1420
|
+
class ServiceAnalyzer extends BaseAnalyzer {
|
|
1421
|
+
async analyze() {
|
|
1422
|
+
return { services: [] };
|
|
1142
1423
|
}
|
|
1143
|
-
|
|
1144
|
-
|
|
1145
|
-
|
|
1146
|
-
|
|
1147
|
-
|
|
1148
|
-
|
|
1149
|
-
|
|
1150
|
-
|
|
1424
|
+
async analyzeForModule(moduleDefVarName, moduleName) {
|
|
1425
|
+
const services = [];
|
|
1426
|
+
for (const file of this.project.getSourceFiles()) {
|
|
1427
|
+
const calls = findMethodCallsOnVariable(file, moduleDefVarName, "service");
|
|
1428
|
+
for (const call of calls) {
|
|
1429
|
+
const name = getVariableNameForCall(call);
|
|
1430
|
+
if (!name)
|
|
1431
|
+
continue;
|
|
1432
|
+
const obj = extractObjectLiteral(call, 0);
|
|
1433
|
+
const inject = obj ? parseInjectFromObj(obj) : [];
|
|
1434
|
+
const methods = obj ? parseMethodsFromObj(obj) : [];
|
|
1435
|
+
const loc = getSourceLocation(call);
|
|
1436
|
+
services.push({
|
|
1437
|
+
name,
|
|
1438
|
+
moduleName,
|
|
1439
|
+
...loc,
|
|
1440
|
+
inject,
|
|
1441
|
+
methods
|
|
1442
|
+
});
|
|
1151
1443
|
}
|
|
1152
1444
|
}
|
|
1153
|
-
return
|
|
1445
|
+
return services;
|
|
1154
1446
|
}
|
|
1155
|
-
|
|
1156
|
-
|
|
1157
|
-
|
|
1158
|
-
|
|
1159
|
-
|
|
1160
|
-
|
|
1161
|
-
|
|
1162
|
-
|
|
1163
|
-
|
|
1164
|
-
|
|
1165
|
-
|
|
1166
|
-
|
|
1167
|
-
|
|
1168
|
-
|
|
1169
|
-
|
|
1170
|
-
const
|
|
1171
|
-
|
|
1172
|
-
|
|
1173
|
-
|
|
1174
|
-
|
|
1175
|
-
|
|
1176
|
-
|
|
1177
|
-
code: "VERTZ_RT_DYNAMIC_CONFIG",
|
|
1178
|
-
message: "Route config must be an object literal for static analysis.",
|
|
1179
|
-
suggestion: "Pass an inline object literal as the second argument."
|
|
1180
|
-
}));
|
|
1181
|
-
}
|
|
1182
|
-
const params = obj ? this.resolveSchemaRef(obj, "params", filePath) : undefined;
|
|
1183
|
-
const query = obj ? this.resolveSchemaRef(obj, "query", filePath) : undefined;
|
|
1184
|
-
const body = obj ? this.resolveSchemaRef(obj, "body", filePath) : undefined;
|
|
1185
|
-
const headers = obj ? this.resolveSchemaRef(obj, "headers", filePath) : undefined;
|
|
1186
|
-
const response = obj ? this.resolveSchemaRef(obj, "response", filePath) : undefined;
|
|
1187
|
-
const middleware = obj ? this.extractMiddlewareRefs(obj, filePath) : [];
|
|
1188
|
-
const descriptionExpr = obj ? getPropertyValue(obj, "description") : null;
|
|
1189
|
-
const description = descriptionExpr ? getStringValue(descriptionExpr) ?? undefined : undefined;
|
|
1190
|
-
const tagsExpr = obj ? getPropertyValue(obj, "tags") : null;
|
|
1191
|
-
const tags = tagsExpr ? getArrayElements(tagsExpr).map((e) => getStringValue(e)).filter((v) => v !== null) : [];
|
|
1192
|
-
const handlerExpr = obj ? getPropertyValue(obj, "handler") : null;
|
|
1193
|
-
const operationId = this.generateOperationId(moduleName, method, path, handlerExpr, usedOperationIds);
|
|
1194
|
-
if (obj && !handlerExpr) {
|
|
1195
|
-
this.addDiagnostic(createDiagnosticFromLocation(loc, {
|
|
1196
|
-
severity: "error",
|
|
1197
|
-
code: "VERTZ_RT_MISSING_HANDLER",
|
|
1198
|
-
message: "Route must have a 'handler' property.",
|
|
1199
|
-
suggestion: "Add a 'handler' property to the route config."
|
|
1200
|
-
}));
|
|
1201
|
-
return null;
|
|
1202
|
-
}
|
|
1203
|
-
return {
|
|
1204
|
-
method,
|
|
1205
|
-
path,
|
|
1206
|
-
fullPath,
|
|
1207
|
-
...loc,
|
|
1208
|
-
operationId,
|
|
1209
|
-
params,
|
|
1210
|
-
query,
|
|
1211
|
-
body,
|
|
1212
|
-
headers,
|
|
1213
|
-
response,
|
|
1214
|
-
middleware,
|
|
1215
|
-
description,
|
|
1216
|
-
tags
|
|
1217
|
-
};
|
|
1447
|
+
}
|
|
1448
|
+
function parseInjectFromObj(obj) {
|
|
1449
|
+
const injectExpr = getPropertyValue(obj, "inject");
|
|
1450
|
+
if (!injectExpr?.isKind(SyntaxKind7.ObjectLiteralExpression))
|
|
1451
|
+
return [];
|
|
1452
|
+
return parseInjectRefs(injectExpr);
|
|
1453
|
+
}
|
|
1454
|
+
function parseMethodsFromObj(obj) {
|
|
1455
|
+
const methodsExpr = getPropertyValue(obj, "methods");
|
|
1456
|
+
if (!methodsExpr)
|
|
1457
|
+
return [];
|
|
1458
|
+
return extractMethodSignatures(methodsExpr);
|
|
1459
|
+
}
|
|
1460
|
+
function parseInjectRefs(obj) {
|
|
1461
|
+
return getProperties(obj).map(({ name, value }) => {
|
|
1462
|
+
const resolvedToken = value.isKind(SyntaxKind7.Identifier) ? value.getText() : name;
|
|
1463
|
+
return { localName: name, resolvedToken };
|
|
1464
|
+
});
|
|
1465
|
+
}
|
|
1466
|
+
function extractMethodSignatures(expr) {
|
|
1467
|
+
if (!expr.isKind(SyntaxKind7.ArrowFunction) && !expr.isKind(SyntaxKind7.FunctionExpression)) {
|
|
1468
|
+
return [];
|
|
1218
1469
|
}
|
|
1219
|
-
|
|
1220
|
-
|
|
1221
|
-
|
|
1222
|
-
|
|
1223
|
-
|
|
1224
|
-
|
|
1225
|
-
|
|
1226
|
-
|
|
1470
|
+
const body = expr.getBody();
|
|
1471
|
+
let returnObj = null;
|
|
1472
|
+
if (body.isKind(SyntaxKind7.ObjectLiteralExpression)) {
|
|
1473
|
+
returnObj = body;
|
|
1474
|
+
} else if (body.isKind(SyntaxKind7.ParenthesizedExpression)) {
|
|
1475
|
+
const inner = body.getExpression();
|
|
1476
|
+
if (inner.isKind(SyntaxKind7.ObjectLiteralExpression)) {
|
|
1477
|
+
returnObj = inner;
|
|
1227
1478
|
}
|
|
1228
|
-
|
|
1229
|
-
|
|
1479
|
+
} else if (body.isKind(SyntaxKind7.Block)) {
|
|
1480
|
+
const returnStmt = body.getStatements().find((s) => s.isKind(SyntaxKind7.ReturnStatement));
|
|
1481
|
+
const retExpr = returnStmt?.asKind(SyntaxKind7.ReturnStatement)?.getExpression();
|
|
1482
|
+
if (retExpr?.isKind(SyntaxKind7.ObjectLiteralExpression)) {
|
|
1483
|
+
returnObj = retExpr;
|
|
1230
1484
|
}
|
|
1231
|
-
return;
|
|
1232
1485
|
}
|
|
1233
|
-
|
|
1234
|
-
|
|
1235
|
-
|
|
1236
|
-
|
|
1237
|
-
|
|
1238
|
-
|
|
1239
|
-
|
|
1240
|
-
|
|
1241
|
-
|
|
1242
|
-
|
|
1243
|
-
|
|
1486
|
+
if (!returnObj)
|
|
1487
|
+
return [];
|
|
1488
|
+
const methods = [];
|
|
1489
|
+
for (const prop of returnObj.getProperties()) {
|
|
1490
|
+
if (!prop.isKind(SyntaxKind7.PropertyAssignment))
|
|
1491
|
+
continue;
|
|
1492
|
+
const methodName = prop.getName();
|
|
1493
|
+
const init = prop.getInitializer();
|
|
1494
|
+
if (!init)
|
|
1495
|
+
continue;
|
|
1496
|
+
const params = extractFunctionParams(init);
|
|
1497
|
+
const returnType = inferReturnType(init);
|
|
1498
|
+
methods.push({
|
|
1499
|
+
name: methodName,
|
|
1500
|
+
parameters: params,
|
|
1501
|
+
returnType
|
|
1244
1502
|
});
|
|
1245
1503
|
}
|
|
1246
|
-
|
|
1247
|
-
let handlerName = null;
|
|
1248
|
-
if (handlerExpr?.isKind(SyntaxKind8.Identifier)) {
|
|
1249
|
-
handlerName = handlerExpr.getText();
|
|
1250
|
-
} else if (handlerExpr?.isKind(SyntaxKind8.PropertyAccessExpression)) {
|
|
1251
|
-
handlerName = handlerExpr.getName();
|
|
1252
|
-
}
|
|
1253
|
-
const id = handlerName ? `${moduleName}_${handlerName}` : `${moduleName}_${method.toLowerCase()}_${sanitizePath(path)}`;
|
|
1254
|
-
if (!usedIds.has(id)) {
|
|
1255
|
-
usedIds.add(id);
|
|
1256
|
-
return id;
|
|
1257
|
-
}
|
|
1258
|
-
let counter = 2;
|
|
1259
|
-
while (usedIds.has(`${id}_${counter}`))
|
|
1260
|
-
counter++;
|
|
1261
|
-
const uniqueId = `${id}_${counter}`;
|
|
1262
|
-
usedIds.add(uniqueId);
|
|
1263
|
-
return uniqueId;
|
|
1264
|
-
}
|
|
1504
|
+
return methods;
|
|
1265
1505
|
}
|
|
1266
|
-
function
|
|
1267
|
-
|
|
1268
|
-
|
|
1269
|
-
|
|
1270
|
-
return
|
|
1506
|
+
function extractFunctionParams(expr) {
|
|
1507
|
+
if (!expr.isKind(SyntaxKind7.ArrowFunction) && !expr.isKind(SyntaxKind7.FunctionExpression)) {
|
|
1508
|
+
return [];
|
|
1509
|
+
}
|
|
1510
|
+
return expr.getParameters().map((p) => ({
|
|
1511
|
+
name: p.getName(),
|
|
1512
|
+
type: p.getType().getText(p)
|
|
1513
|
+
}));
|
|
1271
1514
|
}
|
|
1272
|
-
function
|
|
1273
|
-
|
|
1515
|
+
function inferReturnType(expr) {
|
|
1516
|
+
if (expr.isKind(SyntaxKind7.ArrowFunction) || expr.isKind(SyntaxKind7.FunctionExpression)) {
|
|
1517
|
+
const retType = expr.getReturnType();
|
|
1518
|
+
return retType.getText(expr);
|
|
1519
|
+
}
|
|
1520
|
+
return "unknown";
|
|
1274
1521
|
}
|
|
1275
|
-
// src/analyzers/entity-analyzer.ts
|
|
1276
|
-
import { SyntaxKind as SyntaxKind9 } from "ts-morph";
|
|
1277
|
-
var ENTITY_NAME_PATTERN = /^[a-z][a-z0-9-]*$/;
|
|
1278
|
-
var CRUD_OPS = ["list", "get", "create", "update", "delete"];
|
|
1279
1522
|
|
|
1280
|
-
|
|
1281
|
-
|
|
1282
|
-
if (process.env["VERTZ_DEBUG"]?.includes("entities")) {
|
|
1283
|
-
console.log(`[entity-analyzer] ${msg}`);
|
|
1284
|
-
}
|
|
1285
|
-
}
|
|
1523
|
+
// src/analyzers/middleware-analyzer.ts
|
|
1524
|
+
class MiddlewareAnalyzer extends BaseAnalyzer {
|
|
1286
1525
|
async analyze() {
|
|
1287
|
-
const
|
|
1288
|
-
const
|
|
1289
|
-
|
|
1290
|
-
this.debug(`Scanning ${files.length} source files...`);
|
|
1291
|
-
for (const file of files) {
|
|
1292
|
-
const calls = this.findEntityCalls(file);
|
|
1526
|
+
const middleware = [];
|
|
1527
|
+
for (const file of this.project.getSourceFiles()) {
|
|
1528
|
+
const calls = findCallExpressions(file, "vertz", "middleware");
|
|
1293
1529
|
for (const call of calls) {
|
|
1294
|
-
const
|
|
1295
|
-
if (!
|
|
1296
|
-
|
|
1297
|
-
|
|
1298
|
-
|
|
1299
|
-
|
|
1300
|
-
|
|
1301
|
-
|
|
1302
|
-
|
|
1303
|
-
...getSourceLocation(call)
|
|
1304
|
-
});
|
|
1530
|
+
const obj = extractObjectLiteral(call, 0);
|
|
1531
|
+
if (!obj) {
|
|
1532
|
+
const callLoc = getSourceLocation(call);
|
|
1533
|
+
this.addDiagnostic(createDiagnosticFromLocation(callLoc, {
|
|
1534
|
+
severity: "warning",
|
|
1535
|
+
code: "VERTZ_MW_NON_OBJECT_CONFIG",
|
|
1536
|
+
message: "Middleware config must be an object literal for static analysis.",
|
|
1537
|
+
suggestion: "Pass an inline object literal to vertz.middleware()."
|
|
1538
|
+
}));
|
|
1305
1539
|
continue;
|
|
1306
1540
|
}
|
|
1307
|
-
|
|
1308
|
-
|
|
1309
|
-
|
|
1310
|
-
|
|
1311
|
-
const accessStatus = CRUD_OPS.map((op) => {
|
|
1312
|
-
const kind = entity.access[op];
|
|
1313
|
-
return `${op} ${kind === "false" ? "✗" : "✓"}`;
|
|
1314
|
-
}).join(", ");
|
|
1315
|
-
this.debug(` access: ${accessStatus}`);
|
|
1316
|
-
if (entity.hooks.before.length > 0 || entity.hooks.after.length > 0) {
|
|
1317
|
-
this.debug(` hooks: before[${entity.hooks.before.join(",")}], after[${entity.hooks.after.join(",")}]`);
|
|
1318
|
-
}
|
|
1319
|
-
if (entity.actions.length > 0) {
|
|
1320
|
-
this.debug(` actions: ${entity.actions.map((a) => a.name).join(", ")}`);
|
|
1321
|
-
}
|
|
1322
|
-
}
|
|
1323
|
-
}
|
|
1324
|
-
return { entities };
|
|
1325
|
-
}
|
|
1326
|
-
findEntityCalls(file) {
|
|
1327
|
-
const validCalls = [];
|
|
1328
|
-
for (const call of file.getDescendantsOfKind(SyntaxKind9.CallExpression)) {
|
|
1329
|
-
const expr = call.getExpression();
|
|
1330
|
-
if (expr.isKind(SyntaxKind9.Identifier)) {
|
|
1331
|
-
const isValid = isFromImport(expr, "@vertz/server");
|
|
1332
|
-
if (!isValid && expr.getText() === "entity") {
|
|
1333
|
-
this.addDiagnostic({
|
|
1334
|
-
code: "ENTITY_UNRESOLVED_IMPORT",
|
|
1541
|
+
const loc = getSourceLocation(call);
|
|
1542
|
+
const nameExpr = getPropertyValue(obj, "name");
|
|
1543
|
+
if (!nameExpr) {
|
|
1544
|
+
this.addDiagnostic(createDiagnosticFromLocation(loc, {
|
|
1335
1545
|
severity: "error",
|
|
1336
|
-
|
|
1337
|
-
|
|
1338
|
-
|
|
1546
|
+
code: "VERTZ_MW_MISSING_NAME",
|
|
1547
|
+
message: "Middleware must have a 'name' property.",
|
|
1548
|
+
suggestion: "Add a 'name' property to the middleware config."
|
|
1549
|
+
}));
|
|
1339
1550
|
continue;
|
|
1340
1551
|
}
|
|
1341
|
-
|
|
1342
|
-
|
|
1343
|
-
|
|
1344
|
-
|
|
1345
|
-
|
|
1346
|
-
|
|
1347
|
-
|
|
1348
|
-
|
|
1349
|
-
continue;
|
|
1350
|
-
const obj = expr.getExpression();
|
|
1351
|
-
if (!obj.isKind(SyntaxKind9.Identifier))
|
|
1552
|
+
const name = getStringValue(nameExpr);
|
|
1553
|
+
if (!name) {
|
|
1554
|
+
this.addDiagnostic(createDiagnosticFromLocation(loc, {
|
|
1555
|
+
severity: "warning",
|
|
1556
|
+
code: "VERTZ_MW_DYNAMIC_NAME",
|
|
1557
|
+
message: "Middleware name should be a string literal for static analysis.",
|
|
1558
|
+
suggestion: "Use a string literal for the middleware name."
|
|
1559
|
+
}));
|
|
1352
1560
|
continue;
|
|
1353
|
-
const sourceFile = obj.getSourceFile();
|
|
1354
|
-
const importDecl = sourceFile.getImportDeclarations().find((d) => d.getModuleSpecifierValue() === "@vertz/server" && d.getNamespaceImport()?.getText() === obj.getText());
|
|
1355
|
-
if (importDecl) {
|
|
1356
|
-
validCalls.push(call);
|
|
1357
1561
|
}
|
|
1358
|
-
|
|
1359
|
-
|
|
1360
|
-
|
|
1361
|
-
|
|
1362
|
-
|
|
1363
|
-
|
|
1364
|
-
|
|
1365
|
-
|
|
1366
|
-
|
|
1367
|
-
code: "ENTITY_MISSING_ARGS",
|
|
1368
|
-
severity: "error",
|
|
1369
|
-
message: "entity() requires two arguments: name and config",
|
|
1370
|
-
...loc
|
|
1371
|
-
});
|
|
1372
|
-
return null;
|
|
1373
|
-
}
|
|
1374
|
-
const name = getStringValue(args[0]);
|
|
1375
|
-
if (name === null) {
|
|
1376
|
-
this.addDiagnostic({
|
|
1377
|
-
code: "ENTITY_NON_LITERAL_NAME",
|
|
1378
|
-
severity: "error",
|
|
1379
|
-
message: "entity() name must be a string literal",
|
|
1380
|
-
...loc
|
|
1381
|
-
});
|
|
1382
|
-
return null;
|
|
1383
|
-
}
|
|
1384
|
-
if (!ENTITY_NAME_PATTERN.test(name)) {
|
|
1385
|
-
this.addDiagnostic({
|
|
1386
|
-
code: "ENTITY_INVALID_NAME",
|
|
1387
|
-
severity: "error",
|
|
1388
|
-
message: `Entity name must match /^[a-z][a-z0-9-]*$/. Got: "${name}"`,
|
|
1389
|
-
...loc
|
|
1390
|
-
});
|
|
1391
|
-
return null;
|
|
1392
|
-
}
|
|
1393
|
-
const configObj = extractObjectLiteral(call, 1);
|
|
1394
|
-
if (!configObj) {
|
|
1395
|
-
this.addDiagnostic({
|
|
1396
|
-
code: "ENTITY_CONFIG_NOT_OBJECT",
|
|
1397
|
-
severity: "warning",
|
|
1398
|
-
message: "entity() config must be an object literal for static analysis",
|
|
1399
|
-
...loc
|
|
1400
|
-
});
|
|
1401
|
-
return null;
|
|
1402
|
-
}
|
|
1403
|
-
const modelRef = this.extractModelRef(configObj, loc);
|
|
1404
|
-
if (!modelRef)
|
|
1405
|
-
return null;
|
|
1406
|
-
const access = this.extractAccess(configObj);
|
|
1407
|
-
const hooks = this.extractHooks(configObj);
|
|
1408
|
-
const actions = this.extractActions(configObj);
|
|
1409
|
-
const relations = this.extractRelations(configObj);
|
|
1410
|
-
for (const action of actions) {
|
|
1411
|
-
if (CRUD_OPS.includes(action.name)) {
|
|
1412
|
-
this.addDiagnostic({
|
|
1413
|
-
code: "ENTITY_ACTION_NAME_COLLISION",
|
|
1414
|
-
severity: "error",
|
|
1415
|
-
message: `Custom action "${action.name}" collides with built-in CRUD operation`,
|
|
1416
|
-
...action
|
|
1417
|
-
});
|
|
1418
|
-
}
|
|
1419
|
-
}
|
|
1420
|
-
for (const customOp of Object.keys(access.custom)) {
|
|
1421
|
-
if (!actions.some((a) => a.name === customOp)) {
|
|
1422
|
-
this.addDiagnostic({
|
|
1423
|
-
code: "ENTITY_UNKNOWN_ACCESS_OP",
|
|
1424
|
-
severity: "warning",
|
|
1425
|
-
message: `Unknown access operation "${customOp}" — not a CRUD op or custom action`,
|
|
1426
|
-
...loc
|
|
1427
|
-
});
|
|
1428
|
-
}
|
|
1429
|
-
}
|
|
1430
|
-
return { name, modelRef, access, hooks, actions, relations, ...loc };
|
|
1431
|
-
}
|
|
1432
|
-
extractModelRef(configObj, loc) {
|
|
1433
|
-
const modelExpr = getPropertyValue(configObj, "model");
|
|
1434
|
-
if (!modelExpr) {
|
|
1435
|
-
this.addDiagnostic({
|
|
1436
|
-
code: "ENTITY_MISSING_MODEL",
|
|
1437
|
-
severity: "error",
|
|
1438
|
-
message: "entity() requires a model property",
|
|
1439
|
-
...loc
|
|
1440
|
-
});
|
|
1441
|
-
return null;
|
|
1442
|
-
}
|
|
1443
|
-
const variableName = modelExpr.isKind(SyntaxKind9.Identifier) ? modelExpr.getText() : modelExpr.getText();
|
|
1444
|
-
let importSource;
|
|
1445
|
-
if (modelExpr.isKind(SyntaxKind9.Identifier)) {
|
|
1446
|
-
const importInfo = this.findImportForIdentifier(modelExpr);
|
|
1447
|
-
if (importInfo) {
|
|
1448
|
-
importSource = importInfo.importDecl.getModuleSpecifierValue();
|
|
1449
|
-
}
|
|
1450
|
-
}
|
|
1451
|
-
const schemaRefs = this.resolveModelSchemas(modelExpr);
|
|
1452
|
-
return { variableName, importSource, schemaRefs };
|
|
1453
|
-
}
|
|
1454
|
-
findImportForIdentifier(identifier) {
|
|
1455
|
-
const sourceFile = identifier.getSourceFile();
|
|
1456
|
-
const name = identifier.getText();
|
|
1457
|
-
for (const importDecl of sourceFile.getImportDeclarations()) {
|
|
1458
|
-
for (const specifier of importDecl.getNamedImports()) {
|
|
1459
|
-
const localName = specifier.getAliasNode()?.getText() ?? specifier.getName();
|
|
1460
|
-
if (localName === name) {
|
|
1461
|
-
return { importDecl, originalName: specifier.getName() };
|
|
1562
|
+
const handlerExpr = getPropertyValue(obj, "handler");
|
|
1563
|
+
if (!handlerExpr) {
|
|
1564
|
+
this.addDiagnostic(createDiagnosticFromLocation(loc, {
|
|
1565
|
+
severity: "error",
|
|
1566
|
+
code: "VERTZ_MW_MISSING_HANDLER",
|
|
1567
|
+
message: "Middleware must have a 'handler' property.",
|
|
1568
|
+
suggestion: "Add a 'handler' property to the middleware config."
|
|
1569
|
+
}));
|
|
1570
|
+
continue;
|
|
1462
1571
|
}
|
|
1463
|
-
|
|
1464
|
-
|
|
1465
|
-
|
|
1466
|
-
|
|
1467
|
-
|
|
1468
|
-
|
|
1469
|
-
|
|
1470
|
-
|
|
1471
|
-
|
|
1472
|
-
|
|
1473
|
-
|
|
1474
|
-
|
|
1475
|
-
|
|
1476
|
-
|
|
1477
|
-
|
|
1478
|
-
|
|
1479
|
-
|
|
1480
|
-
|
|
1481
|
-
|
|
1482
|
-
|
|
1483
|
-
|
|
1484
|
-
updateInput,
|
|
1485
|
-
resolved: response !== undefined || createInput !== undefined || updateInput !== undefined
|
|
1486
|
-
};
|
|
1487
|
-
} catch {
|
|
1488
|
-
return { resolved: false };
|
|
1572
|
+
const injectExpr = getPropertyValue(obj, "inject");
|
|
1573
|
+
const inject = injectExpr?.isKind(SyntaxKind8.ObjectLiteralExpression) ? parseInjectRefs(injectExpr) : [];
|
|
1574
|
+
const filePath = file.getFilePath();
|
|
1575
|
+
const headers = this.resolveSchemaRef(obj, "headers", filePath);
|
|
1576
|
+
const params = this.resolveSchemaRef(obj, "params", filePath);
|
|
1577
|
+
const query = this.resolveSchemaRef(obj, "query", filePath);
|
|
1578
|
+
const body = this.resolveSchemaRef(obj, "body", filePath);
|
|
1579
|
+
const requires = this.resolveSchemaRef(obj, "requires", filePath);
|
|
1580
|
+
const provides = this.resolveSchemaRef(obj, "provides", filePath);
|
|
1581
|
+
middleware.push({
|
|
1582
|
+
name,
|
|
1583
|
+
...loc,
|
|
1584
|
+
inject,
|
|
1585
|
+
headers,
|
|
1586
|
+
params,
|
|
1587
|
+
query,
|
|
1588
|
+
body,
|
|
1589
|
+
requires,
|
|
1590
|
+
provides
|
|
1591
|
+
});
|
|
1592
|
+
}
|
|
1489
1593
|
}
|
|
1594
|
+
return { middleware };
|
|
1490
1595
|
}
|
|
1491
|
-
|
|
1492
|
-
const
|
|
1493
|
-
if (!
|
|
1596
|
+
resolveSchemaRef(obj, prop, filePath) {
|
|
1597
|
+
const expr = getPropertyValue(obj, prop);
|
|
1598
|
+
if (!expr)
|
|
1494
1599
|
return;
|
|
1495
|
-
|
|
1496
|
-
|
|
1497
|
-
|
|
1498
|
-
|
|
1499
|
-
kind: "inline",
|
|
1500
|
-
sourceFile: location.getSourceFile().getFilePath(),
|
|
1501
|
-
jsonSchema,
|
|
1502
|
-
resolvedFields
|
|
1503
|
-
};
|
|
1504
|
-
}
|
|
1505
|
-
buildJsonSchema(resolvedFields) {
|
|
1506
|
-
if (!resolvedFields || resolvedFields.length === 0) {
|
|
1507
|
-
return {};
|
|
1600
|
+
if (expr.isKind(SyntaxKind8.Identifier)) {
|
|
1601
|
+
const resolved = resolveIdentifier(expr, this.project);
|
|
1602
|
+
const resolvedPath = resolved ? resolved.sourceFile.getFilePath() : filePath;
|
|
1603
|
+
return createNamedSchemaRef(expr.getText(), resolvedPath);
|
|
1508
1604
|
}
|
|
1509
|
-
|
|
1510
|
-
|
|
1511
|
-
for (const field of resolvedFields) {
|
|
1512
|
-
const fieldSchema = this.tsTypeToJsonSchema(field.tsType);
|
|
1513
|
-
properties[field.name] = fieldSchema;
|
|
1514
|
-
if (!field.optional) {
|
|
1515
|
-
required.push(field.name);
|
|
1516
|
-
}
|
|
1605
|
+
if (isSchemaExpression(expr.getSourceFile(), expr)) {
|
|
1606
|
+
return createInlineSchemaRef(filePath);
|
|
1517
1607
|
}
|
|
1518
|
-
return
|
|
1519
|
-
type: "object",
|
|
1520
|
-
properties,
|
|
1521
|
-
...required.length > 0 ? { required } : {}
|
|
1522
|
-
};
|
|
1608
|
+
return;
|
|
1523
1609
|
}
|
|
1524
|
-
|
|
1525
|
-
|
|
1526
|
-
|
|
1527
|
-
|
|
1528
|
-
|
|
1529
|
-
|
|
1530
|
-
|
|
1531
|
-
|
|
1532
|
-
|
|
1533
|
-
|
|
1534
|
-
|
|
1535
|
-
|
|
1536
|
-
|
|
1610
|
+
}
|
|
1611
|
+
// src/analyzers/module-analyzer.ts
|
|
1612
|
+
import { SyntaxKind as SyntaxKind9 } from "ts-morph";
|
|
1613
|
+
class ModuleAnalyzer extends BaseAnalyzer {
|
|
1614
|
+
async analyze() {
|
|
1615
|
+
const modules = [];
|
|
1616
|
+
const defVarToIndex = new Map;
|
|
1617
|
+
for (const file of this.project.getSourceFiles()) {
|
|
1618
|
+
const defCalls = findCallExpressions(file, "vertz", "moduleDef");
|
|
1619
|
+
for (const call of defCalls) {
|
|
1620
|
+
const obj = extractObjectLiteral(call, 0);
|
|
1621
|
+
if (!obj)
|
|
1622
|
+
continue;
|
|
1623
|
+
const nameExpr = getPropertyValue(obj, "name");
|
|
1624
|
+
const name = nameExpr ? getStringValue(nameExpr) : null;
|
|
1625
|
+
if (!name) {
|
|
1626
|
+
this.addDiagnostic(createDiagnosticFromLocation(getSourceLocation(call), {
|
|
1627
|
+
severity: "error",
|
|
1628
|
+
code: "VERTZ_MODULE_DYNAMIC_NAME",
|
|
1629
|
+
message: "vertz.moduleDef() requires a static string `name` property."
|
|
1630
|
+
}));
|
|
1631
|
+
continue;
|
|
1632
|
+
}
|
|
1633
|
+
const varName = getVariableNameForCall(call);
|
|
1634
|
+
const importsExpr = getPropertyValue(obj, "imports");
|
|
1635
|
+
const imports = importsExpr?.isKind(SyntaxKind9.ObjectLiteralExpression) ? parseImports(importsExpr) : [];
|
|
1636
|
+
const optionsExpr = getPropertyValue(obj, "options");
|
|
1637
|
+
let options;
|
|
1638
|
+
if (optionsExpr?.isKind(SyntaxKind9.Identifier)) {
|
|
1639
|
+
options = createNamedSchemaRef(optionsExpr.getText(), file.getFilePath());
|
|
1640
|
+
}
|
|
1641
|
+
const loc = getSourceLocation(call);
|
|
1642
|
+
const idx = modules.length;
|
|
1643
|
+
modules.push({
|
|
1644
|
+
name,
|
|
1645
|
+
...loc,
|
|
1646
|
+
imports,
|
|
1647
|
+
options,
|
|
1648
|
+
services: [],
|
|
1649
|
+
routers: [],
|
|
1650
|
+
exports: []
|
|
1651
|
+
});
|
|
1652
|
+
if (varName) {
|
|
1653
|
+
defVarToIndex.set(varName, idx);
|
|
1654
|
+
}
|
|
1655
|
+
}
|
|
1537
1656
|
}
|
|
1538
|
-
|
|
1539
|
-
|
|
1540
|
-
|
|
1541
|
-
|
|
1542
|
-
|
|
1543
|
-
|
|
1544
|
-
|
|
1545
|
-
|
|
1546
|
-
|
|
1547
|
-
|
|
1548
|
-
|
|
1549
|
-
|
|
1550
|
-
|
|
1551
|
-
|
|
1552
|
-
|
|
1553
|
-
|
|
1554
|
-
|
|
1555
|
-
|
|
1556
|
-
|
|
1557
|
-
|
|
1558
|
-
|
|
1559
|
-
|
|
1560
|
-
|
|
1561
|
-
const optional = fieldProp.isOptional();
|
|
1562
|
-
const tsType = this.mapTsType(fieldType);
|
|
1563
|
-
fields.push({ name, tsType, optional });
|
|
1657
|
+
for (const file of this.project.getSourceFiles()) {
|
|
1658
|
+
const moduleCalls = findCallExpressions(file, "vertz", "module");
|
|
1659
|
+
for (const call of moduleCalls) {
|
|
1660
|
+
const args = call.getArguments();
|
|
1661
|
+
if (args.length < 2)
|
|
1662
|
+
continue;
|
|
1663
|
+
const defArg = args.at(0);
|
|
1664
|
+
if (!defArg?.isKind(SyntaxKind9.Identifier))
|
|
1665
|
+
continue;
|
|
1666
|
+
const defVarName = defArg.getText();
|
|
1667
|
+
const idx = defVarToIndex.get(defVarName);
|
|
1668
|
+
if (idx === undefined)
|
|
1669
|
+
continue;
|
|
1670
|
+
const mod = modules.at(idx);
|
|
1671
|
+
if (!mod)
|
|
1672
|
+
continue;
|
|
1673
|
+
const assemblyObj = extractObjectLiteral(call, 1);
|
|
1674
|
+
if (!assemblyObj)
|
|
1675
|
+
continue;
|
|
1676
|
+
const exportsExpr = getPropertyValue(assemblyObj, "exports");
|
|
1677
|
+
if (exportsExpr) {
|
|
1678
|
+
mod.exports = extractIdentifierNames(exportsExpr);
|
|
1679
|
+
}
|
|
1564
1680
|
}
|
|
1565
|
-
return fields;
|
|
1566
|
-
} catch {
|
|
1567
|
-
return;
|
|
1568
1681
|
}
|
|
1682
|
+
return { modules };
|
|
1569
1683
|
}
|
|
1570
|
-
|
|
1571
|
-
|
|
1572
|
-
|
|
1573
|
-
|
|
1574
|
-
|
|
1575
|
-
|
|
1684
|
+
}
|
|
1685
|
+
function parseImports(obj) {
|
|
1686
|
+
return getProperties(obj).map(({ name }) => ({
|
|
1687
|
+
localName: name,
|
|
1688
|
+
isEnvImport: false
|
|
1689
|
+
}));
|
|
1690
|
+
}
|
|
1691
|
+
function extractIdentifierNames(expr) {
|
|
1692
|
+
if (!expr.isKind(SyntaxKind9.ArrayLiteralExpression))
|
|
1693
|
+
return [];
|
|
1694
|
+
return expr.getElements().filter((e) => e.isKind(SyntaxKind9.Identifier)).map((e) => e.getText());
|
|
1695
|
+
}
|
|
1696
|
+
// src/analyzers/route-analyzer.ts
|
|
1697
|
+
import { SyntaxKind as SyntaxKind10 } from "ts-morph";
|
|
1698
|
+
var HTTP_METHODS = {
|
|
1699
|
+
get: "GET",
|
|
1700
|
+
post: "POST",
|
|
1701
|
+
put: "PUT",
|
|
1702
|
+
patch: "PATCH",
|
|
1703
|
+
delete: "DELETE",
|
|
1704
|
+
head: "HEAD"
|
|
1705
|
+
};
|
|
1706
|
+
|
|
1707
|
+
class RouteAnalyzer extends BaseAnalyzer {
|
|
1708
|
+
async analyze() {
|
|
1709
|
+
return { routers: [] };
|
|
1710
|
+
}
|
|
1711
|
+
async analyzeForModules(context) {
|
|
1712
|
+
const routers = [];
|
|
1713
|
+
const knownModuleDefVars = new Set(context.moduleDefVariables.keys());
|
|
1714
|
+
for (const file of this.project.getSourceFiles()) {
|
|
1715
|
+
for (const [moduleDefVar, moduleName] of context.moduleDefVariables) {
|
|
1716
|
+
const routerCalls = findMethodCallsOnVariable(file, moduleDefVar, "router");
|
|
1717
|
+
for (const call of routerCalls) {
|
|
1718
|
+
const varName = getVariableNameForCall(call);
|
|
1719
|
+
if (!varName)
|
|
1720
|
+
continue;
|
|
1721
|
+
const obj = extractObjectLiteral(call, 0);
|
|
1722
|
+
const prefixExpr = obj ? getPropertyValue(obj, "prefix") : null;
|
|
1723
|
+
const prefix = prefixExpr ? getStringValue(prefixExpr) ?? "/" : "/";
|
|
1724
|
+
if (!prefixExpr) {
|
|
1725
|
+
this.addDiagnostic(createDiagnosticFromLocation(getSourceLocation(call), {
|
|
1726
|
+
severity: "warning",
|
|
1727
|
+
code: "VERTZ_RT_MISSING_PREFIX",
|
|
1728
|
+
message: "Router should have a 'prefix' property.",
|
|
1729
|
+
suggestion: "Add a 'prefix' property to the router config."
|
|
1730
|
+
}));
|
|
1731
|
+
}
|
|
1732
|
+
const loc = getSourceLocation(call);
|
|
1733
|
+
const injectExpr = obj ? getPropertyValue(obj, "inject") : null;
|
|
1734
|
+
const inject = injectExpr?.isKind(SyntaxKind10.ObjectLiteralExpression) ? parseInjectRefs(injectExpr) : [];
|
|
1735
|
+
const routes = this.extractRoutes(file, varName, prefix, moduleName);
|
|
1736
|
+
routers.push({
|
|
1737
|
+
name: varName,
|
|
1738
|
+
moduleName,
|
|
1739
|
+
...loc,
|
|
1740
|
+
prefix,
|
|
1741
|
+
inject,
|
|
1742
|
+
routes
|
|
1743
|
+
});
|
|
1576
1744
|
}
|
|
1577
1745
|
}
|
|
1578
|
-
|
|
1746
|
+
this.detectUnknownRouterCalls(file, knownModuleDefVars);
|
|
1579
1747
|
}
|
|
1580
|
-
|
|
1581
|
-
|
|
1582
|
-
|
|
1748
|
+
return { routers };
|
|
1749
|
+
}
|
|
1750
|
+
detectUnknownRouterCalls(file, knownModuleDefVars) {
|
|
1751
|
+
const allCalls = file.getDescendantsOfKind(SyntaxKind10.CallExpression);
|
|
1752
|
+
for (const call of allCalls) {
|
|
1753
|
+
const expr = call.getExpression();
|
|
1754
|
+
if (!expr.isKind(SyntaxKind10.PropertyAccessExpression))
|
|
1755
|
+
continue;
|
|
1756
|
+
if (expr.getName() !== "router")
|
|
1757
|
+
continue;
|
|
1758
|
+
const obj = expr.getExpression();
|
|
1759
|
+
if (!obj.isKind(SyntaxKind10.Identifier))
|
|
1760
|
+
continue;
|
|
1761
|
+
if (knownModuleDefVars.has(obj.getText()))
|
|
1762
|
+
continue;
|
|
1763
|
+
const varName = getVariableNameForCall(call);
|
|
1764
|
+
if (!varName)
|
|
1765
|
+
continue;
|
|
1766
|
+
const hasHttpMethodCalls = Object.keys(HTTP_METHODS).some((method) => findMethodCallsOnVariable(file, varName, method).length > 0);
|
|
1767
|
+
if (!hasHttpMethodCalls)
|
|
1768
|
+
continue;
|
|
1769
|
+
this.addDiagnostic(createDiagnosticFromLocation(getSourceLocation(call), {
|
|
1770
|
+
severity: "error",
|
|
1771
|
+
code: "VERTZ_RT_UNKNOWN_MODULE_DEF",
|
|
1772
|
+
message: `'${obj.getText()}' is not a known moduleDef variable.`,
|
|
1773
|
+
suggestion: "Ensure the variable is declared with vertz.moduleDef() and is included in the module context."
|
|
1774
|
+
}));
|
|
1583
1775
|
}
|
|
1584
|
-
return type;
|
|
1585
1776
|
}
|
|
1586
|
-
|
|
1587
|
-
const
|
|
1588
|
-
|
|
1589
|
-
|
|
1590
|
-
|
|
1591
|
-
|
|
1777
|
+
extractRoutes(file, routerVarName, prefix, moduleName) {
|
|
1778
|
+
const routes = [];
|
|
1779
|
+
const usedOperationIds = new Set;
|
|
1780
|
+
for (const [methodName, httpMethod] of Object.entries(HTTP_METHODS)) {
|
|
1781
|
+
const directCalls = findMethodCallsOnVariable(file, routerVarName, methodName);
|
|
1782
|
+
const chainedCalls = this.findChainedHttpCalls(file, routerVarName, methodName);
|
|
1783
|
+
const allCalls = [...directCalls, ...chainedCalls];
|
|
1784
|
+
for (const call of allCalls) {
|
|
1785
|
+
const route = this.extractRoute(call, httpMethod, prefix, moduleName, file, usedOperationIds);
|
|
1786
|
+
if (route)
|
|
1787
|
+
routes.push(route);
|
|
1592
1788
|
}
|
|
1593
1789
|
}
|
|
1594
|
-
|
|
1595
|
-
return "string";
|
|
1596
|
-
if (type.isNumber() || type.isNumberLiteral())
|
|
1597
|
-
return "number";
|
|
1598
|
-
if (type.isBoolean() || type.isBooleanLiteral())
|
|
1599
|
-
return "boolean";
|
|
1600
|
-
if (typeText === "Date")
|
|
1601
|
-
return "date";
|
|
1602
|
-
return "unknown";
|
|
1790
|
+
return routes;
|
|
1603
1791
|
}
|
|
1604
|
-
|
|
1605
|
-
|
|
1606
|
-
|
|
1607
|
-
|
|
1608
|
-
|
|
1609
|
-
|
|
1610
|
-
|
|
1611
|
-
|
|
1612
|
-
|
|
1613
|
-
|
|
1614
|
-
|
|
1615
|
-
|
|
1616
|
-
|
|
1617
|
-
|
|
1618
|
-
|
|
1619
|
-
|
|
1620
|
-
|
|
1621
|
-
|
|
1622
|
-
|
|
1623
|
-
|
|
1792
|
+
findChainedHttpCalls(file, routerVarName, methodName) {
|
|
1793
|
+
return file.getDescendantsOfKind(SyntaxKind10.CallExpression).filter((call) => {
|
|
1794
|
+
const expr = call.getExpression();
|
|
1795
|
+
if (!expr.isKind(SyntaxKind10.PropertyAccessExpression))
|
|
1796
|
+
return false;
|
|
1797
|
+
if (expr.getName() !== methodName)
|
|
1798
|
+
return false;
|
|
1799
|
+
const obj = expr.getExpression();
|
|
1800
|
+
if (!obj.isKind(SyntaxKind10.CallExpression))
|
|
1801
|
+
return false;
|
|
1802
|
+
return this.chainResolvesToVariable(obj, routerVarName);
|
|
1803
|
+
});
|
|
1804
|
+
}
|
|
1805
|
+
chainResolvesToVariable(expr, varName) {
|
|
1806
|
+
if (expr.isKind(SyntaxKind10.Identifier)) {
|
|
1807
|
+
return expr.getText() === varName;
|
|
1808
|
+
}
|
|
1809
|
+
if (expr.isKind(SyntaxKind10.CallExpression)) {
|
|
1810
|
+
const inner = expr.getExpression();
|
|
1811
|
+
if (inner.isKind(SyntaxKind10.PropertyAccessExpression)) {
|
|
1812
|
+
return this.chainResolvesToVariable(inner.getExpression(), varName);
|
|
1624
1813
|
}
|
|
1625
1814
|
}
|
|
1626
|
-
return
|
|
1815
|
+
return false;
|
|
1627
1816
|
}
|
|
1628
|
-
|
|
1629
|
-
const
|
|
1630
|
-
|
|
1631
|
-
|
|
1632
|
-
|
|
1633
|
-
|
|
1634
|
-
|
|
1817
|
+
extractRoute(call, method, prefix, moduleName, file, usedOperationIds) {
|
|
1818
|
+
const args = call.getArguments();
|
|
1819
|
+
const pathArg = args[0];
|
|
1820
|
+
if (!pathArg)
|
|
1821
|
+
return null;
|
|
1822
|
+
const path = getStringValue(pathArg);
|
|
1823
|
+
if (path === null) {
|
|
1824
|
+
this.addDiagnostic(createDiagnosticFromLocation(getSourceLocation(call), {
|
|
1825
|
+
severity: "error",
|
|
1826
|
+
code: "VERTZ_RT_DYNAMIC_PATH",
|
|
1827
|
+
message: "Route paths must be string literals for static analysis.",
|
|
1828
|
+
suggestion: "Use a string literal for the route path."
|
|
1829
|
+
}));
|
|
1830
|
+
return null;
|
|
1831
|
+
}
|
|
1832
|
+
const fullPath = joinPaths(prefix, path);
|
|
1833
|
+
const loc = getSourceLocation(call);
|
|
1834
|
+
const filePath = file.getFilePath();
|
|
1835
|
+
const obj = extractObjectLiteral(call, 1);
|
|
1836
|
+
if (!obj && args.length > 1) {
|
|
1837
|
+
this.addDiagnostic(createDiagnosticFromLocation(loc, {
|
|
1838
|
+
severity: "warning",
|
|
1839
|
+
code: "VERTZ_RT_DYNAMIC_CONFIG",
|
|
1840
|
+
message: "Route config must be an object literal for static analysis.",
|
|
1841
|
+
suggestion: "Pass an inline object literal as the second argument."
|
|
1842
|
+
}));
|
|
1843
|
+
}
|
|
1844
|
+
const params = obj ? this.resolveSchemaRef(obj, "params", filePath) : undefined;
|
|
1845
|
+
const query = obj ? this.resolveSchemaRef(obj, "query", filePath) : undefined;
|
|
1846
|
+
const body = obj ? this.resolveSchemaRef(obj, "body", filePath) : undefined;
|
|
1847
|
+
const headers = obj ? this.resolveSchemaRef(obj, "headers", filePath) : undefined;
|
|
1848
|
+
const response = obj ? this.resolveSchemaRef(obj, "response", filePath) : undefined;
|
|
1849
|
+
const middleware = obj ? this.extractMiddlewareRefs(obj, filePath) : [];
|
|
1850
|
+
const descriptionExpr = obj ? getPropertyValue(obj, "description") : null;
|
|
1851
|
+
const description = descriptionExpr ? getStringValue(descriptionExpr) ?? undefined : undefined;
|
|
1852
|
+
const tagsExpr = obj ? getPropertyValue(obj, "tags") : null;
|
|
1853
|
+
const tags = tagsExpr ? getArrayElements(tagsExpr).map((e) => getStringValue(e)).filter((v) => v !== null) : [];
|
|
1854
|
+
const handlerExpr = obj ? getPropertyValue(obj, "handler") : null;
|
|
1855
|
+
const operationId = this.generateOperationId(moduleName, method, path, handlerExpr, usedOperationIds);
|
|
1856
|
+
if (obj && !handlerExpr) {
|
|
1857
|
+
this.addDiagnostic(createDiagnosticFromLocation(loc, {
|
|
1858
|
+
severity: "error",
|
|
1859
|
+
code: "VERTZ_RT_MISSING_HANDLER",
|
|
1860
|
+
message: "Route must have a 'handler' property.",
|
|
1861
|
+
suggestion: "Add a 'handler' property to the route config."
|
|
1862
|
+
}));
|
|
1863
|
+
return null;
|
|
1864
|
+
}
|
|
1865
|
+
return {
|
|
1866
|
+
method,
|
|
1867
|
+
path,
|
|
1868
|
+
fullPath,
|
|
1869
|
+
...loc,
|
|
1870
|
+
operationId,
|
|
1871
|
+
params,
|
|
1872
|
+
query,
|
|
1873
|
+
body,
|
|
1874
|
+
headers,
|
|
1875
|
+
response,
|
|
1876
|
+
middleware,
|
|
1877
|
+
description,
|
|
1878
|
+
tags
|
|
1879
|
+
};
|
|
1635
1880
|
}
|
|
1636
|
-
|
|
1637
|
-
const
|
|
1638
|
-
|
|
1639
|
-
|
|
1640
|
-
|
|
1641
|
-
|
|
1642
|
-
|
|
1643
|
-
|
|
1881
|
+
resolveSchemaRef(obj, prop, filePath) {
|
|
1882
|
+
const expr = getPropertyValue(obj, prop);
|
|
1883
|
+
if (!expr)
|
|
1884
|
+
return;
|
|
1885
|
+
if (expr.isKind(SyntaxKind10.Identifier)) {
|
|
1886
|
+
const resolved = resolveIdentifier(expr, this.project);
|
|
1887
|
+
const resolvedPath = resolved ? resolved.sourceFile.getFilePath() : filePath;
|
|
1888
|
+
return createNamedSchemaRef(expr.getText(), resolvedPath);
|
|
1644
1889
|
}
|
|
1645
|
-
|
|
1646
|
-
|
|
1647
|
-
for (const { name } of getProperties(afterExpr)) {
|
|
1648
|
-
if (name === "create" || name === "update" || name === "delete")
|
|
1649
|
-
hooks.after.push(name);
|
|
1650
|
-
}
|
|
1890
|
+
if (isSchemaExpression(expr.getSourceFile(), expr)) {
|
|
1891
|
+
return createInlineSchemaRef(filePath);
|
|
1651
1892
|
}
|
|
1652
|
-
return
|
|
1893
|
+
return;
|
|
1653
1894
|
}
|
|
1654
|
-
|
|
1655
|
-
const
|
|
1656
|
-
if (!
|
|
1895
|
+
extractMiddlewareRefs(obj, filePath) {
|
|
1896
|
+
const expr = getPropertyValue(obj, "middlewares");
|
|
1897
|
+
if (!expr)
|
|
1657
1898
|
return [];
|
|
1658
|
-
|
|
1659
|
-
|
|
1660
|
-
const
|
|
1661
|
-
const bodyExpr = actionObj ? getPropertyValue(actionObj, "body") : null;
|
|
1662
|
-
const responseExpr = actionObj ? getPropertyValue(actionObj, "response") : null;
|
|
1663
|
-
if (!bodyExpr && !responseExpr) {
|
|
1664
|
-
this.addDiagnostic({
|
|
1665
|
-
code: "ENTITY_ACTION_MISSING_SCHEMA",
|
|
1666
|
-
severity: "warning",
|
|
1667
|
-
message: `Custom action "${name}" is missing body and response schema`,
|
|
1668
|
-
...loc
|
|
1669
|
-
});
|
|
1670
|
-
}
|
|
1671
|
-
const body = bodyExpr ? this.resolveSchemaFromExpression(bodyExpr, loc) : undefined;
|
|
1672
|
-
const response = responseExpr ? this.resolveSchemaFromExpression(responseExpr, loc) : undefined;
|
|
1673
|
-
const methodExpr = actionObj ? getPropertyValue(actionObj, "method") : null;
|
|
1674
|
-
let method = "POST";
|
|
1675
|
-
if (methodExpr) {
|
|
1676
|
-
const methodStr = getStringValue(methodExpr);
|
|
1677
|
-
const validMethods = [
|
|
1678
|
-
"GET",
|
|
1679
|
-
"POST",
|
|
1680
|
-
"PUT",
|
|
1681
|
-
"DELETE",
|
|
1682
|
-
"PATCH",
|
|
1683
|
-
"HEAD",
|
|
1684
|
-
"OPTIONS"
|
|
1685
|
-
];
|
|
1686
|
-
if (methodStr && validMethods.includes(methodStr)) {
|
|
1687
|
-
method = methodStr;
|
|
1688
|
-
} else {
|
|
1689
|
-
this.addDiagnostic({
|
|
1690
|
-
code: "ENTITY_ACTION_INVALID_METHOD",
|
|
1691
|
-
severity: "error",
|
|
1692
|
-
message: `Custom action "${name}" has invalid method "${methodStr ?? "(non-string)"}" — must be one of GET, POST, PUT, DELETE, PATCH, HEAD, OPTIONS`,
|
|
1693
|
-
...loc
|
|
1694
|
-
});
|
|
1695
|
-
}
|
|
1696
|
-
}
|
|
1697
|
-
const pathExpr = actionObj ? getPropertyValue(actionObj, "path") : null;
|
|
1698
|
-
const path = pathExpr ? getStringValue(pathExpr) ?? undefined : undefined;
|
|
1699
|
-
const queryExpr = actionObj ? getPropertyValue(actionObj, "query") : null;
|
|
1700
|
-
const queryRef = queryExpr ? this.resolveSchemaFromExpression(queryExpr, loc) : undefined;
|
|
1701
|
-
const paramsExpr = actionObj ? getPropertyValue(actionObj, "params") : null;
|
|
1702
|
-
const paramsRef = paramsExpr ? this.resolveSchemaFromExpression(paramsExpr, loc) : undefined;
|
|
1703
|
-
const headersExpr = actionObj ? getPropertyValue(actionObj, "headers") : null;
|
|
1704
|
-
const headersRef = headersExpr ? this.resolveSchemaFromExpression(headersExpr, loc) : undefined;
|
|
1899
|
+
const elements = getArrayElements(expr);
|
|
1900
|
+
return elements.filter((el) => el.isKind(SyntaxKind10.Identifier)).map((el) => {
|
|
1901
|
+
const resolved = resolveIdentifier(el, this.project);
|
|
1705
1902
|
return {
|
|
1706
|
-
name,
|
|
1707
|
-
|
|
1708
|
-
path,
|
|
1709
|
-
params: paramsRef,
|
|
1710
|
-
query: queryRef,
|
|
1711
|
-
headers: headersRef,
|
|
1712
|
-
body,
|
|
1713
|
-
response,
|
|
1714
|
-
...loc
|
|
1903
|
+
name: el.getText(),
|
|
1904
|
+
sourceFile: resolved ? resolved.sourceFile.getFilePath() : filePath
|
|
1715
1905
|
};
|
|
1716
1906
|
});
|
|
1717
1907
|
}
|
|
1718
|
-
|
|
1719
|
-
|
|
1720
|
-
|
|
1721
|
-
|
|
1908
|
+
generateOperationId(moduleName, method, path, handlerExpr, usedIds) {
|
|
1909
|
+
let handlerName = null;
|
|
1910
|
+
if (handlerExpr?.isKind(SyntaxKind10.Identifier)) {
|
|
1911
|
+
handlerName = handlerExpr.getText();
|
|
1912
|
+
} else if (handlerExpr?.isKind(SyntaxKind10.PropertyAccessExpression)) {
|
|
1913
|
+
handlerName = handlerExpr.getName();
|
|
1722
1914
|
}
|
|
1723
|
-
|
|
1724
|
-
|
|
1725
|
-
|
|
1726
|
-
|
|
1727
|
-
return { kind: "inline", sourceFile: loc.sourceFile };
|
|
1915
|
+
const id = handlerName ? `${moduleName}_${handlerName}` : `${moduleName}_${method.toLowerCase()}_${sanitizePath(path)}`;
|
|
1916
|
+
if (!usedIds.has(id)) {
|
|
1917
|
+
usedIds.add(id);
|
|
1918
|
+
return id;
|
|
1728
1919
|
}
|
|
1920
|
+
let counter = 2;
|
|
1921
|
+
while (usedIds.has(`${id}_${counter}`))
|
|
1922
|
+
counter++;
|
|
1923
|
+
const uniqueId = `${id}_${counter}`;
|
|
1924
|
+
usedIds.add(uniqueId);
|
|
1925
|
+
return uniqueId;
|
|
1729
1926
|
}
|
|
1730
|
-
|
|
1731
|
-
|
|
1732
|
-
|
|
1733
|
-
|
|
1734
|
-
return
|
|
1735
|
-
|
|
1736
|
-
|
|
1737
|
-
|
|
1738
|
-
|
|
1739
|
-
if (boolVal === true)
|
|
1740
|
-
return { name, selection: "all" };
|
|
1741
|
-
if (value.isKind(SyntaxKind9.ObjectLiteralExpression)) {
|
|
1742
|
-
const fields = getProperties(value).map((p) => p.name);
|
|
1743
|
-
return { name, selection: fields };
|
|
1744
|
-
}
|
|
1745
|
-
return { name, selection: "all" };
|
|
1746
|
-
});
|
|
1747
|
-
}
|
|
1927
|
+
}
|
|
1928
|
+
function joinPaths(prefix, path) {
|
|
1929
|
+
const normalizedPrefix = prefix.endsWith("/") ? prefix.slice(0, -1) : prefix;
|
|
1930
|
+
if (path === "/")
|
|
1931
|
+
return normalizedPrefix || "/";
|
|
1932
|
+
return normalizedPrefix + path;
|
|
1933
|
+
}
|
|
1934
|
+
function sanitizePath(path) {
|
|
1935
|
+
return path.replace(/^\//, "").replace(/[/:.]/g, "_").replace(/_+/g, "_").replace(/^_|_$/g, "") || "root";
|
|
1748
1936
|
}
|
|
1749
1937
|
// src/compiler.ts
|
|
1750
1938
|
import { mkdir } from "node:fs/promises";
|
|
@@ -2340,6 +2528,7 @@ function createEmptyAppIR() {
|
|
|
2340
2528
|
middleware: [],
|
|
2341
2529
|
schemas: [],
|
|
2342
2530
|
entities: [],
|
|
2531
|
+
databases: [],
|
|
2343
2532
|
dependencyGraph: createEmptyDependencyGraph(),
|
|
2344
2533
|
diagnostics: []
|
|
2345
2534
|
};
|
|
@@ -2553,6 +2742,7 @@ class CompletenessValidator {
|
|
|
2553
2742
|
this.checkPathParamMatch(ir, diagnostics);
|
|
2554
2743
|
this.checkModuleOptions(ir, diagnostics);
|
|
2555
2744
|
this.checkRoutePathFormat(ir, diagnostics);
|
|
2745
|
+
this.checkEntityModelRegistration(ir, diagnostics);
|
|
2556
2746
|
return diagnostics;
|
|
2557
2747
|
}
|
|
2558
2748
|
checkResponseSchemas(ir, diagnostics) {
|
|
@@ -2760,6 +2950,27 @@ class CompletenessValidator {
|
|
|
2760
2950
|
}
|
|
2761
2951
|
}
|
|
2762
2952
|
}
|
|
2953
|
+
checkEntityModelRegistration(ir, diagnostics) {
|
|
2954
|
+
if (ir.databases.length === 0)
|
|
2955
|
+
return;
|
|
2956
|
+
const allModelKeys = new Set;
|
|
2957
|
+
for (const db of ir.databases) {
|
|
2958
|
+
for (const key of db.modelKeys) {
|
|
2959
|
+
allModelKeys.add(key);
|
|
2960
|
+
}
|
|
2961
|
+
}
|
|
2962
|
+
for (const entity of ir.entities) {
|
|
2963
|
+
if (!allModelKeys.has(entity.name)) {
|
|
2964
|
+
const registeredList = ir.databases.flatMap((db) => db.modelKeys.map((key) => `"${key}" (${db.sourceFile}:${db.sourceLine})`)).join(", ");
|
|
2965
|
+
diagnostics.push(createDiagnosticFromLocation(entity, {
|
|
2966
|
+
severity: "error",
|
|
2967
|
+
code: "ENTITY_MODEL_NOT_REGISTERED",
|
|
2968
|
+
message: `Entity "${entity.name}" is not registered in any createDb() call. ` + `Add "${entity.name}: ${entity.modelRef.variableName}" to the models object in createDb().`,
|
|
2969
|
+
suggestion: registeredList ? `Registered models: ${registeredList}` : "No models registered in any createDb() call."
|
|
2970
|
+
}));
|
|
2971
|
+
}
|
|
2972
|
+
}
|
|
2973
|
+
}
|
|
2763
2974
|
checkCtxKeyCollisions(ir, diagnostics) {
|
|
2764
2975
|
const keyProviders = new Map;
|
|
2765
2976
|
for (const mw of ir.middleware) {
|
|
@@ -3058,6 +3269,7 @@ class Compiler {
|
|
|
3058
3269
|
const middlewareResult = await analyzers.middleware.analyze();
|
|
3059
3270
|
const appResult = await analyzers.app.analyze();
|
|
3060
3271
|
const entityResult = await analyzers.entity.analyze();
|
|
3272
|
+
const databaseResult = await analyzers.database.analyze();
|
|
3061
3273
|
const depGraphResult = await analyzers.dependencyGraph.analyze();
|
|
3062
3274
|
ir.env = envResult.env;
|
|
3063
3275
|
ir.schemas = schemaResult.schemas;
|
|
@@ -3065,8 +3277,9 @@ class Compiler {
|
|
|
3065
3277
|
ir.middleware = middlewareResult.middleware;
|
|
3066
3278
|
ir.app = appResult.app;
|
|
3067
3279
|
ir.entities = entityResult.entities;
|
|
3280
|
+
ir.databases = databaseResult.databases;
|
|
3068
3281
|
ir.dependencyGraph = depGraphResult.graph;
|
|
3069
|
-
ir.diagnostics.push(...analyzers.env.getDiagnostics(), ...analyzers.schema.getDiagnostics(), ...analyzers.middleware.getDiagnostics(), ...analyzers.module.getDiagnostics(), ...analyzers.app.getDiagnostics(), ...analyzers.entity.getDiagnostics(), ...analyzers.dependencyGraph.getDiagnostics());
|
|
3282
|
+
ir.diagnostics.push(...analyzers.env.getDiagnostics(), ...analyzers.schema.getDiagnostics(), ...analyzers.middleware.getDiagnostics(), ...analyzers.module.getDiagnostics(), ...analyzers.app.getDiagnostics(), ...analyzers.entity.getDiagnostics(), ...analyzers.database.getDiagnostics(), ...analyzers.dependencyGraph.getDiagnostics());
|
|
3070
3283
|
injectEntityRoutes(ir);
|
|
3071
3284
|
const collisionDiags = detectRouteCollisions(ir);
|
|
3072
3285
|
ir.diagnostics.push(...collisionDiags);
|
|
@@ -3110,6 +3323,7 @@ function createCompiler(config) {
|
|
|
3110
3323
|
module: new ModuleAnalyzer(project, resolved),
|
|
3111
3324
|
app: new AppAnalyzer(project, resolved),
|
|
3112
3325
|
entity: new EntityAnalyzer(project, resolved),
|
|
3326
|
+
database: new DatabaseAnalyzer(project, resolved),
|
|
3113
3327
|
dependencyGraph: new DependencyGraphAnalyzer(project, resolved)
|
|
3114
3328
|
},
|
|
3115
3329
|
validators: [
|
|
@@ -3473,6 +3687,7 @@ export {
|
|
|
3473
3687
|
EnvAnalyzer,
|
|
3474
3688
|
EntityAnalyzer,
|
|
3475
3689
|
DependencyGraphAnalyzer,
|
|
3690
|
+
DatabaseAnalyzer,
|
|
3476
3691
|
CompletenessValidator,
|
|
3477
3692
|
Compiler,
|
|
3478
3693
|
BootGenerator,
|