@vertz/compiler 0.2.12 → 0.2.14

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