@vertz/compiler 0.2.12 → 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.
Files changed (3) hide show
  1. package/dist/index.d.ts +75 -39
  2. package/dist/index.js +1237 -1026
  3. 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/env-analyzer.ts
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
- // src/analyzers/schema-analyzer.ts
610
- import { SyntaxKind as SyntaxKind3 } from "ts-morph";
611
- class SchemaAnalyzer extends BaseAnalyzer {
612
- async analyze() {
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
- let env;
720
- for (const file of this.project.getSourceFiles()) {
721
- const calls = findCallExpressions(file, "vertz", "env");
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 obj = extractObjectLiteral(call, 0);
724
- if (!obj)
724
+ const result = this.extractEntity(file, call);
725
+ if (!result)
725
726
  continue;
726
- const loc = getSourceLocation(call);
727
- if (env) {
728
- this.addDiagnostic(createDiagnosticFromLocation(loc, {
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
- code: "VERTZ_ENV_DUPLICATE",
731
- message: "Multiple vertz.env() calls found. Only one is allowed."
732
- }));
733
+ message: `Entity "${entity.name}" is already defined at ${existing.sourceFile}:${existing.sourceLine}`,
734
+ ...getSourceLocation(call)
735
+ });
733
736
  continue;
734
737
  }
735
- const loadExpr = getPropertyValue(obj, "load");
736
- const loadFiles = loadExpr ? getArrayElements(loadExpr).map((e) => getStringValue(e)).filter((v) => v !== null) : [];
737
- const schemaExpr = getPropertyValue(obj, "schema");
738
- let schema;
739
- if (schemaExpr?.isKind(SyntaxKind4.Identifier)) {
740
- schema = createNamedSchemaRef(schemaExpr.getText(), file.getFilePath());
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
- return { env };
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
- async analyzeForModule(moduleDefVarName, moduleName) {
763
- const services = [];
764
- for (const file of this.project.getSourceFiles()) {
765
- const calls = findMethodCallsOnVariable(file, moduleDefVarName, "service");
766
- for (const call of calls) {
767
- const name = getVariableNameForCall(call);
768
- if (!name)
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
- const obj = extractObjectLiteral(call, 0);
771
- const inject = obj ? parseInjectFromObj(obj) : [];
772
- const methods = obj ? parseMethodsFromObj(obj) : [];
773
- const loc = getSourceLocation(call);
774
- services.push({
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
- return services;
784
- }
785
- }
786
- function parseInjectFromObj(obj) {
787
- const injectExpr = getPropertyValue(obj, "inject");
788
- if (!injectExpr?.isKind(SyntaxKind5.ObjectLiteralExpression))
789
- return [];
790
- return parseInjectRefs(injectExpr);
791
- }
792
- function parseMethodsFromObj(obj) {
793
- const methodsExpr = getPropertyValue(obj, "methods");
794
- if (!methodsExpr)
795
- return [];
796
- return extractMethodSignatures(methodsExpr);
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
- const body = expr.getBody();
809
- let returnObj = null;
810
- if (body.isKind(SyntaxKind5.ObjectLiteralExpression)) {
811
- returnObj = body;
812
- } else if (body.isKind(SyntaxKind5.ParenthesizedExpression)) {
813
- const inner = body.getExpression();
814
- if (inner.isKind(SyntaxKind5.ObjectLiteralExpression)) {
815
- returnObj = inner;
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
- } else if (body.isKind(SyntaxKind5.Block)) {
818
- const returnStmt = body.getStatements().find((s) => s.isKind(SyntaxKind5.ReturnStatement));
819
- const retExpr = returnStmt?.asKind(SyntaxKind5.ReturnStatement)?.getExpression();
820
- if (retExpr?.isKind(SyntaxKind5.ObjectLiteralExpression)) {
821
- returnObj = retExpr;
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
- if (!returnObj)
825
- return [];
826
- const methods = [];
827
- for (const prop of returnObj.getProperties()) {
828
- if (!prop.isKind(SyntaxKind5.PropertyAssignment))
829
- continue;
830
- const methodName = prop.getName();
831
- const init = prop.getInitializer();
832
- if (!init)
833
- continue;
834
- const params = extractFunctionParams(init);
835
- const returnType = inferReturnType(init);
836
- methods.push({
837
- name: methodName,
838
- parameters: params,
839
- returnType
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
- return methods;
843
- }
844
- function extractFunctionParams(expr) {
845
- if (!expr.isKind(SyntaxKind5.ArrowFunction) && !expr.isKind(SyntaxKind5.FunctionExpression)) {
846
- return [];
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
- return expr.getParameters().map((p) => ({
849
- name: p.getName(),
850
- type: p.getType().getText(p)
851
- }));
852
- }
853
- function inferReturnType(expr) {
854
- if (expr.isKind(SyntaxKind5.ArrowFunction) || expr.isKind(SyntaxKind5.FunctionExpression)) {
855
- const retType = expr.getReturnType();
856
- return retType.getText(expr);
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
- return "unknown";
859
- }
860
-
861
- // src/analyzers/middleware-analyzer.ts
862
- class MiddlewareAnalyzer extends BaseAnalyzer {
863
- async analyze() {
864
- const middleware = [];
865
- for (const file of this.project.getSourceFiles()) {
866
- const calls = findCallExpressions(file, "vertz", "middleware");
867
- for (const call of calls) {
868
- const obj = extractObjectLiteral(call, 0);
869
- if (!obj) {
870
- const callLoc = getSourceLocation(call);
871
- this.addDiagnostic(createDiagnosticFromLocation(callLoc, {
872
- severity: "warning",
873
- code: "VERTZ_MW_NON_OBJECT_CONFIG",
874
- message: "Middleware config must be an object literal for static analysis.",
875
- suggestion: "Pass an inline object literal to vertz.middleware()."
876
- }));
877
- continue;
878
- }
879
- const loc = getSourceLocation(call);
880
- const nameExpr = getPropertyValue(obj, "name");
881
- if (!nameExpr) {
882
- this.addDiagnostic(createDiagnosticFromLocation(loc, {
883
- severity: "error",
884
- code: "VERTZ_MW_MISSING_NAME",
885
- message: "Middleware must have a 'name' property.",
886
- suggestion: "Add a 'name' property to the middleware config."
887
- }));
888
- continue;
889
- }
890
- const name = getStringValue(nameExpr);
891
- if (!name) {
892
- this.addDiagnostic(createDiagnosticFromLocation(loc, {
893
- severity: "warning",
894
- code: "VERTZ_MW_DYNAMIC_NAME",
895
- message: "Middleware name should be a string literal for static analysis.",
896
- suggestion: "Use a string literal for the middleware name."
897
- }));
898
- continue;
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
- const handlerExpr = getPropertyValue(obj, "handler");
901
- if (!handlerExpr) {
902
- this.addDiagnostic(createDiagnosticFromLocation(loc, {
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
- code: "VERTZ_MW_MISSING_HANDLER",
905
- message: "Middleware must have a 'handler' property.",
906
- suggestion: "Add a 'handler' property to the middleware config."
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
- return { middleware };
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
- resolveSchemaRef(obj, prop, filePath) {
935
- const expr = getPropertyValue(obj, prop);
936
- if (!expr)
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
- if (isSchemaExpression(expr.getSourceFile(), expr)) {
944
- return createInlineSchemaRef(filePath);
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
- // src/analyzers/module-analyzer.ts
950
- import { SyntaxKind as SyntaxKind7 } from "ts-morph";
951
- class ModuleAnalyzer extends BaseAnalyzer {
952
- async analyze() {
953
- const modules = [];
954
- const defVarToIndex = new Map;
955
- for (const file of this.project.getSourceFiles()) {
956
- const defCalls = findCallExpressions(file, "vertz", "moduleDef");
957
- for (const call of defCalls) {
958
- const obj = extractObjectLiteral(call, 0);
959
- if (!obj)
960
- continue;
961
- const nameExpr = getPropertyValue(obj, "name");
962
- const name = nameExpr ? getStringValue(nameExpr) : null;
963
- if (!name) {
964
- this.addDiagnostic(createDiagnosticFromLocation(getSourceLocation(call), {
965
- severity: "error",
966
- code: "VERTZ_MODULE_DYNAMIC_NAME",
967
- message: "vertz.moduleDef() requires a static string `name` property."
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
- const varName = getVariableNameForCall(call);
972
- const importsExpr = getPropertyValue(obj, "imports");
973
- const imports = importsExpr?.isKind(SyntaxKind7.ObjectLiteralExpression) ? parseImports(importsExpr) : [];
974
- const optionsExpr = getPropertyValue(obj, "options");
975
- let options;
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
- for (const file of this.project.getSourceFiles()) {
996
- const moduleCalls = findCallExpressions(file, "vertz", "module");
997
- for (const call of moduleCalls) {
998
- const args = call.getArguments();
999
- if (args.length < 2)
1000
- continue;
1001
- const defArg = args.at(0);
1002
- if (!defArg?.isKind(SyntaxKind7.Identifier))
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 defVarName = defArg.getText();
1005
- const idx = defVarToIndex.get(defVarName);
1006
- if (idx === undefined)
1220
+ const modelType = modelExpr.getType();
1221
+ const tableProp = modelType.getProperty("table");
1222
+ if (!tableProp)
1007
1223
  continue;
1008
- const mod = modules.at(idx);
1009
- if (!mod)
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 assemblyObj = extractObjectLiteral(call, 1);
1012
- if (!assemblyObj)
1234
+ const modelType = modelExpr.getType();
1235
+ const relProp = modelType.getProperty("relations");
1236
+ if (!relProp)
1013
1237
  continue;
1014
- const exportsExpr = getPropertyValue(assemblyObj, "exports");
1015
- if (exportsExpr) {
1016
- mod.exports = extractIdentifierNames(exportsExpr);
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
- function parseImports(obj) {
1024
- return getProperties(obj).map(({ name }) => ({
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
- class RouteAnalyzer extends BaseAnalyzer {
1267
+ // src/analyzers/schema-analyzer.ts
1268
+ import { SyntaxKind as SyntaxKind5 } from "ts-morph";
1269
+ class SchemaAnalyzer extends BaseAnalyzer {
1046
1270
  async analyze() {
1047
- return { routers: [] };
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
- for (const [moduleDefVar, moduleName] of context.moduleDefVariables) {
1054
- const routerCalls = findMethodCallsOnVariable(file, moduleDefVar, "router");
1055
- for (const call of routerCalls) {
1056
- const varName = getVariableNameForCall(call);
1057
- if (!varName)
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 obj = extractObjectLiteral(call, 0);
1060
- const prefixExpr = obj ? getPropertyValue(obj, "prefix") : null;
1061
- const prefix = prefixExpr ? getStringValue(prefixExpr) ?? "/" : "/";
1062
- if (!prefixExpr) {
1063
- this.addDiagnostic(createDiagnosticFromLocation(getSourceLocation(call), {
1064
- severity: "warning",
1065
- code: "VERTZ_RT_MISSING_PREFIX",
1066
- message: "Router should have a 'prefix' property.",
1067
- suggestion: "Add a 'prefix' property to the router config."
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
- prefix,
1079
- inject,
1080
- routes
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 { routers };
1299
+ return { schemas };
1087
1300
  }
1088
- detectUnknownRouterCalls(file, knownModuleDefVars) {
1089
- const allCalls = file.getDescendantsOfKind(SyntaxKind8.CallExpression);
1090
- for (const call of allCalls) {
1091
- const expr = call.getExpression();
1092
- if (!expr.isKind(SyntaxKind8.PropertyAccessExpression))
1093
- continue;
1094
- if (expr.getName() !== "router")
1095
- continue;
1096
- const obj = expr.getExpression();
1097
- if (!obj.isKind(SyntaxKind8.Identifier))
1098
- continue;
1099
- if (knownModuleDefVars.has(obj.getText()))
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 varName = getVariableNameForCall(call);
1102
- if (!varName)
1314
+ const entity = rest.slice(0, -part.length);
1315
+ if (entity.length === 0)
1103
1316
  continue;
1104
- const hasHttpMethodCalls = Object.keys(HTTP_METHODS).some((method) => findMethodCallsOnVariable(file, varName, method).length > 0);
1105
- if (!hasHttpMethodCalls)
1317
+ const firstChar = entity.at(0);
1318
+ if (!firstChar || firstChar !== firstChar.toUpperCase())
1106
1319
  continue;
1107
- this.addDiagnostic(createDiagnosticFromLocation(getSourceLocation(call), {
1108
- severity: "error",
1109
- code: "VERTZ_RT_UNKNOWN_MODULE_DEF",
1110
- message: `'${obj.getText()}' is not a known moduleDef variable.`,
1111
- suggestion: "Ensure the variable is declared with vertz.moduleDef() and is included in the module context."
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
- extractRoutes(file, routerVarName, prefix, moduleName) {
1116
- const routes = [];
1117
- const usedOperationIds = new Set;
1118
- for (const [methodName, httpMethod] of Object.entries(HTTP_METHODS)) {
1119
- const directCalls = findMethodCallsOnVariable(file, routerVarName, methodName);
1120
- const chainedCalls = this.findChainedHttpCalls(file, routerVarName, methodName);
1121
- const allCalls = [...directCalls, ...chainedCalls];
1122
- for (const call of allCalls) {
1123
- const route = this.extractRoute(call, httpMethod, prefix, moduleName, file, usedOperationIds);
1124
- if (route)
1125
- routes.push(route);
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 routes;
1408
+ return { env };
1129
1409
  }
1130
- findChainedHttpCalls(file, routerVarName, methodName) {
1131
- return file.getDescendantsOfKind(SyntaxKind8.CallExpression).filter((call) => {
1132
- const expr = call.getExpression();
1133
- if (!expr.isKind(SyntaxKind8.PropertyAccessExpression))
1134
- return false;
1135
- if (expr.getName() !== methodName)
1136
- return false;
1137
- const obj = expr.getExpression();
1138
- if (!obj.isKind(SyntaxKind8.CallExpression))
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
- chainResolvesToVariable(expr, varName) {
1144
- if (expr.isKind(SyntaxKind8.Identifier)) {
1145
- return expr.getText() === varName;
1146
- }
1147
- if (expr.isKind(SyntaxKind8.CallExpression)) {
1148
- const inner = expr.getExpression();
1149
- if (inner.isKind(SyntaxKind8.PropertyAccessExpression)) {
1150
- return this.chainResolvesToVariable(inner.getExpression(), varName);
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 false;
1441
+ return services;
1154
1442
  }
1155
- extractRoute(call, method, prefix, moduleName, file, usedOperationIds) {
1156
- const args = call.getArguments();
1157
- const pathArg = args[0];
1158
- if (!pathArg)
1159
- return null;
1160
- const path = getStringValue(pathArg);
1161
- if (path === null) {
1162
- this.addDiagnostic(createDiagnosticFromLocation(getSourceLocation(call), {
1163
- severity: "error",
1164
- code: "VERTZ_RT_DYNAMIC_PATH",
1165
- message: "Route paths must be string literals for static analysis.",
1166
- suggestion: "Use a string literal for the route path."
1167
- }));
1168
- return null;
1169
- }
1170
- const fullPath = joinPaths(prefix, path);
1171
- const loc = getSourceLocation(call);
1172
- const filePath = file.getFilePath();
1173
- const obj = extractObjectLiteral(call, 1);
1174
- if (!obj && args.length > 1) {
1175
- this.addDiagnostic(createDiagnosticFromLocation(loc, {
1176
- severity: "warning",
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
- resolveSchemaRef(obj, prop, filePath) {
1220
- const expr = getPropertyValue(obj, prop);
1221
- if (!expr)
1222
- return;
1223
- if (expr.isKind(SyntaxKind8.Identifier)) {
1224
- const resolved = resolveIdentifier(expr, this.project);
1225
- const resolvedPath = resolved ? resolved.sourceFile.getFilePath() : filePath;
1226
- return createNamedSchemaRef(expr.getText(), resolvedPath);
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
- if (isSchemaExpression(expr.getSourceFile(), expr)) {
1229
- return createInlineSchemaRef(filePath);
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
- extractMiddlewareRefs(obj, filePath) {
1234
- const expr = getPropertyValue(obj, "middlewares");
1235
- if (!expr)
1236
- return [];
1237
- const elements = getArrayElements(expr);
1238
- return elements.filter((el) => el.isKind(SyntaxKind8.Identifier)).map((el) => {
1239
- const resolved = resolveIdentifier(el, this.project);
1240
- return {
1241
- name: el.getText(),
1242
- sourceFile: resolved ? resolved.sourceFile.getFilePath() : filePath
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
- generateOperationId(moduleName, method, path, handlerExpr, usedIds) {
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 joinPaths(prefix, path) {
1267
- const normalizedPrefix = prefix.endsWith("/") ? prefix.slice(0, -1) : prefix;
1268
- if (path === "/")
1269
- return normalizedPrefix || "/";
1270
- return normalizedPrefix + path;
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 sanitizePath(path) {
1273
- return path.replace(/^\//, "").replace(/[/:.]/g, "_").replace(/_+/g, "_").replace(/^_|_$/g, "") || "root";
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
- class EntityAnalyzer extends BaseAnalyzer {
1281
- debug(msg) {
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 entities = [];
1288
- const seenNames = new Map;
1289
- const files = this.project.getSourceFiles();
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 entity = this.extractEntity(file, call);
1295
- if (!entity)
1296
- continue;
1297
- const existing = seenNames.get(entity.name);
1298
- if (existing) {
1299
- this.addDiagnostic({
1300
- code: "ENTITY_DUPLICATE_NAME",
1301
- severity: "error",
1302
- message: `Entity "${entity.name}" is already defined at ${existing.sourceFile}:${existing.sourceLine}`,
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
- seenNames.set(entity.name, entity);
1308
- entities.push(entity);
1309
- this.debug(`Detected entity: "${entity.name}" at ${entity.sourceFile}:${entity.sourceLine}`);
1310
- this.debug(` model: ${entity.modelRef.variableName} (resolved: ${entity.modelRef.schemaRefs.resolved ? "✅" : "❌"})`);
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
- message: "entity() call does not resolve to @vertz/server",
1337
- ...getSourceLocation(call)
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
- if (isValid) {
1342
- validCalls.push(call);
1343
- }
1344
- continue;
1345
- }
1346
- if (expr.isKind(SyntaxKind9.PropertyAccessExpression)) {
1347
- const propName = expr.getName();
1348
- if (propName !== "entity")
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
- return validCalls;
1361
- }
1362
- extractEntity(_file, call) {
1363
- const args = call.getArguments();
1364
- const loc = getSourceLocation(call);
1365
- if (args.length < 2) {
1366
- this.addDiagnostic({
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
- const nsImport = importDecl.getNamespaceImport();
1465
- if (nsImport && nsImport.getText() === name) {
1466
- return { importDecl, originalName: "*" };
1467
- }
1468
- }
1469
- return null;
1470
- }
1471
- resolveModelSchemas(modelExpr) {
1472
- try {
1473
- const modelType = modelExpr.getType();
1474
- const schemasProp = modelType.getProperty("schemas");
1475
- if (!schemasProp)
1476
- return { resolved: false };
1477
- const schemasType = schemasProp.getTypeAtLocation(modelExpr);
1478
- const response = this.extractSchemaType(schemasType, "response", modelExpr);
1479
- const createInput = this.extractSchemaType(schemasType, "createInput", modelExpr);
1480
- const updateInput = this.extractSchemaType(schemasType, "updateInput", modelExpr);
1481
- return {
1482
- response,
1483
- createInput,
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
- extractSchemaType(parentType, propertyName, location) {
1492
- const prop = parentType.getProperty(propertyName);
1493
- if (!prop)
1592
+ resolveSchemaRef(obj, prop, filePath) {
1593
+ const expr = getPropertyValue(obj, prop);
1594
+ if (!expr)
1494
1595
  return;
1495
- const propType = prop.getTypeAtLocation(location);
1496
- const resolvedFields = this.resolveFieldsFromSchemaType(propType, location);
1497
- const jsonSchema = this.buildJsonSchema(resolvedFields);
1498
- return {
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
- const properties = {};
1510
- const required = [];
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
- tsTypeToJsonSchema(tsType) {
1525
- switch (tsType) {
1526
- case "string":
1527
- return { type: "string" };
1528
- case "number":
1529
- return { type: "number" };
1530
- case "boolean":
1531
- return { type: "boolean" };
1532
- case "date":
1533
- return { type: "string", format: "date-time" };
1534
- case "unknown":
1535
- default:
1536
- return {};
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
- resolveFieldsFromSchemaType(schemaType, location) {
1540
- try {
1541
- const parseProp = schemaType.getProperty("parse");
1542
- if (!parseProp)
1543
- return;
1544
- const parseType = parseProp.getTypeAtLocation(location);
1545
- const callSignatures = parseType.getCallSignatures();
1546
- if (callSignatures.length === 0)
1547
- return;
1548
- const returnType = callSignatures[0]?.getReturnType();
1549
- if (!returnType)
1550
- return;
1551
- const dataType = this.unwrapResultType(returnType, location);
1552
- if (!dataType)
1553
- return;
1554
- const properties = dataType.getProperties();
1555
- if (properties.length === 0)
1556
- return;
1557
- const fields = [];
1558
- for (const fieldProp of properties) {
1559
- const name = fieldProp.getName();
1560
- const fieldType = fieldProp.getTypeAtLocation(location);
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
- unwrapResultType(type, location) {
1571
- if (type.isUnion()) {
1572
- for (const member of type.getUnionTypes()) {
1573
- const dataProp2 = member.getProperty("data");
1574
- if (dataProp2) {
1575
- return dataProp2.getTypeAtLocation(location);
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
- return;
1742
+ this.detectUnknownRouterCalls(file, knownModuleDefVars);
1579
1743
  }
1580
- const dataProp = type.getProperty("data");
1581
- if (dataProp) {
1582
- return dataProp.getTypeAtLocation(location);
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
- mapTsType(type) {
1587
- const typeText = type.getText();
1588
- if (type.isUnion()) {
1589
- const nonUndefined = type.getUnionTypes().filter((t) => !t.isUndefined());
1590
- if (nonUndefined.length === 1 && nonUndefined[0]) {
1591
- return this.mapTsType(nonUndefined[0]);
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
- if (type.isString() || type.isStringLiteral())
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
- extractAccess(configObj) {
1605
- const defaults = {
1606
- list: "none",
1607
- get: "none",
1608
- create: "none",
1609
- update: "none",
1610
- delete: "none",
1611
- custom: {}
1612
- };
1613
- const accessExpr = getPropertyValue(configObj, "access");
1614
- if (!accessExpr || !accessExpr.isKind(SyntaxKind9.ObjectLiteralExpression))
1615
- return defaults;
1616
- const result = { ...defaults };
1617
- const knownOps = new Set([...CRUD_OPS]);
1618
- for (const { name, value } of getProperties(accessExpr)) {
1619
- const kind = this.classifyAccessRule(value);
1620
- if (knownOps.has(name)) {
1621
- result[name] = kind;
1622
- } else {
1623
- result.custom[name] = kind;
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 result;
1811
+ return false;
1627
1812
  }
1628
- classifyAccessRule(expr) {
1629
- const boolVal = getBooleanValue(expr);
1630
- if (boolVal === false)
1631
- return "false";
1632
- if (boolVal === true)
1633
- return "none";
1634
- return "function";
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
- extractHooks(configObj) {
1637
- const hooks = { before: [], after: [] };
1638
- const beforeExpr = getPropertyValue(configObj, "before");
1639
- if (beforeExpr?.isKind(SyntaxKind9.ObjectLiteralExpression)) {
1640
- for (const { name } of getProperties(beforeExpr)) {
1641
- if (name === "create" || name === "update")
1642
- hooks.before.push(name);
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
- const afterExpr = getPropertyValue(configObj, "after");
1646
- if (afterExpr?.isKind(SyntaxKind9.ObjectLiteralExpression)) {
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 hooks;
1889
+ return;
1653
1890
  }
1654
- extractActions(configObj) {
1655
- const actionsExpr = getPropertyValue(configObj, "actions");
1656
- if (!actionsExpr?.isKind(SyntaxKind9.ObjectLiteralExpression))
1891
+ extractMiddlewareRefs(obj, filePath) {
1892
+ const expr = getPropertyValue(obj, "middlewares");
1893
+ if (!expr)
1657
1894
  return [];
1658
- return getProperties(actionsExpr).map(({ name, value }) => {
1659
- const actionObj = value.isKind(SyntaxKind9.ObjectLiteralExpression) ? value : null;
1660
- const loc = getSourceLocation(value);
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
- method,
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
- resolveSchemaFromExpression(expr, loc) {
1719
- if (expr.isKind(SyntaxKind9.Identifier)) {
1720
- const varName = expr.getText();
1721
- return { kind: "named", schemaName: varName, sourceFile: loc.sourceFile };
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
- try {
1724
- const typeText = expr.getType().getText();
1725
- return { kind: "inline", sourceFile: loc.sourceFile, jsonSchema: { __typeText: typeText } };
1726
- } catch {
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
- extractRelations(configObj) {
1731
- const relExpr = getPropertyValue(configObj, "relations");
1732
- if (!relExpr?.isKind(SyntaxKind9.ObjectLiteralExpression))
1733
- return [];
1734
- return getProperties(relExpr).filter(({ value }) => {
1735
- const boolVal = getBooleanValue(value);
1736
- return boolVal !== false;
1737
- }).map(({ name, value }) => {
1738
- const boolVal = getBooleanValue(value);
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,