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