@backthread/extractor 0.1.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/LICENSE +21 -0
- package/README.md +83 -0
- package/dist/cluster/domain-grouping.d.ts +113 -0
- package/dist/cluster/domain-grouping.d.ts.map +1 -0
- package/dist/cluster/domain-grouping.js +277 -0
- package/dist/cluster/domain-grouping.js.map +1 -0
- package/dist/cluster/grouping-gate.d.ts +54 -0
- package/dist/cluster/grouping-gate.d.ts.map +1 -0
- package/dist/cluster/grouping-gate.js +0 -0
- package/dist/cluster/grouping-gate.js.map +1 -0
- package/dist/cluster/louvain.d.ts +88 -0
- package/dist/cluster/louvain.d.ts.map +1 -0
- package/dist/cluster/louvain.js +674 -0
- package/dist/cluster/louvain.js.map +1 -0
- package/dist/cluster/overrides.d.ts +19 -0
- package/dist/cluster/overrides.d.ts.map +1 -0
- package/dist/cluster/overrides.js +32 -0
- package/dist/cluster/overrides.js.map +1 -0
- package/dist/cluster/subsystem.d.ts +45 -0
- package/dist/cluster/subsystem.d.ts.map +1 -0
- package/dist/cluster/subsystem.js +299 -0
- package/dist/cluster/subsystem.js.map +1 -0
- package/dist/cluster/workspaces.d.ts +64 -0
- package/dist/cluster/workspaces.d.ts.map +1 -0
- package/dist/cluster/workspaces.js +739 -0
- package/dist/cluster/workspaces.js.map +1 -0
- package/dist/extract.d.ts +52 -0
- package/dist/extract.d.ts.map +1 -0
- package/dist/extract.js +48 -0
- package/dist/extract.js.map +1 -0
- package/dist/framework/celery/celery.d.ts +17 -0
- package/dist/framework/celery/celery.d.ts.map +1 -0
- package/dist/framework/celery/celery.js +524 -0
- package/dist/framework/celery/celery.js.map +1 -0
- package/dist/framework/contribute-step.d.ts +38 -0
- package/dist/framework/contribute-step.d.ts.map +1 -0
- package/dist/framework/contribute-step.js +403 -0
- package/dist/framework/contribute-step.js.map +1 -0
- package/dist/framework/detect-step.d.ts +8 -0
- package/dist/framework/detect-step.d.ts.map +1 -0
- package/dist/framework/detect-step.js +54 -0
- package/dist/framework/detect-step.js.map +1 -0
- package/dist/framework/detect-util.d.ts +24 -0
- package/dist/framework/detect-util.d.ts.map +1 -0
- package/dist/framework/detect-util.js +59 -0
- package/dist/framework/detect-util.js.map +1 -0
- package/dist/framework/django/django.d.ts +22 -0
- package/dist/framework/django/django.d.ts.map +1 -0
- package/dist/framework/django/django.js +940 -0
- package/dist/framework/django/django.js.map +1 -0
- package/dist/framework/fastapi/fastapi.d.ts +27 -0
- package/dist/framework/fastapi/fastapi.d.ts.map +1 -0
- package/dist/framework/fastapi/fastapi.js +477 -0
- package/dist/framework/fastapi/fastapi.js.map +1 -0
- package/dist/framework/faststream/faststream.d.ts +19 -0
- package/dist/framework/faststream/faststream.d.ts.map +1 -0
- package/dist/framework/faststream/faststream.js +765 -0
- package/dist/framework/faststream/faststream.js.map +1 -0
- package/dist/framework/flask/flask.d.ts +17 -0
- package/dist/framework/flask/flask.d.ts.map +1 -0
- package/dist/framework/flask/flask.js +507 -0
- package/dist/framework/flask/flask.js.map +1 -0
- package/dist/framework/graphql/graphql.d.ts +17 -0
- package/dist/framework/graphql/graphql.d.ts.map +1 -0
- package/dist/framework/graphql/graphql.js +708 -0
- package/dist/framework/graphql/graphql.js.map +1 -0
- package/dist/framework/grpc/grpc.d.ts +22 -0
- package/dist/framework/grpc/grpc.d.ts.map +1 -0
- package/dist/framework/grpc/grpc.js +618 -0
- package/dist/framework/grpc/grpc.js.map +1 -0
- package/dist/framework/litestar/litestar.d.ts +17 -0
- package/dist/framework/litestar/litestar.d.ts.map +1 -0
- package/dist/framework/litestar/litestar.js +536 -0
- package/dist/framework/litestar/litestar.js.map +1 -0
- package/dist/framework/nest/nest.d.ts +19 -0
- package/dist/framework/nest/nest.d.ts.map +1 -0
- package/dist/framework/nest/nest.js +613 -0
- package/dist/framework/nest/nest.js.map +1 -0
- package/dist/framework/next/next.d.ts +22 -0
- package/dist/framework/next/next.d.ts.map +1 -0
- package/dist/framework/next/next.js +449 -0
- package/dist/framework/next/next.js.map +1 -0
- package/dist/framework/node/node.d.ts +24 -0
- package/dist/framework/node/node.d.ts.map +1 -0
- package/dist/framework/node/node.js +1009 -0
- package/dist/framework/node/node.js.map +1 -0
- package/dist/framework/orchestrator/orchestrator.d.ts +34 -0
- package/dist/framework/orchestrator/orchestrator.d.ts.map +1 -0
- package/dist/framework/orchestrator/orchestrator.js +691 -0
- package/dist/framework/orchestrator/orchestrator.js.map +1 -0
- package/dist/framework/orm/orm.d.ts +61 -0
- package/dist/framework/orm/orm.d.ts.map +1 -0
- package/dist/framework/orm/orm.js +620 -0
- package/dist/framework/orm/orm.js.map +1 -0
- package/dist/framework/python/analyze.d.ts +38 -0
- package/dist/framework/python/analyze.d.ts.map +1 -0
- package/dist/framework/python/analyze.js +98 -0
- package/dist/framework/python/analyze.js.map +1 -0
- package/dist/framework/python/py-ast.d.ts +130 -0
- package/dist/framework/python/py-ast.d.ts.map +1 -0
- package/dist/framework/python/py-ast.js +344 -0
- package/dist/framework/python/py-ast.js.map +1 -0
- package/dist/framework/python-orm/python-orm.d.ts +19 -0
- package/dist/framework/python-orm/python-orm.d.ts.map +1 -0
- package/dist/framework/python-orm/python-orm.js +516 -0
- package/dist/framework/python-orm/python-orm.js.map +1 -0
- package/dist/framework/react-native/react-native.d.ts +35 -0
- package/dist/framework/react-native/react-native.d.ts.map +1 -0
- package/dist/framework/react-native/react-native.js +1181 -0
- package/dist/framework/react-native/react-native.js.map +1 -0
- package/dist/framework/register.d.ts +2 -0
- package/dist/framework/register.d.ts.map +1 -0
- package/dist/framework/register.js +59 -0
- package/dist/framework/register.js.map +1 -0
- package/dist/framework/registry.d.ts +31 -0
- package/dist/framework/registry.d.ts.map +1 -0
- package/dist/framework/registry.js +70 -0
- package/dist/framework/registry.js.map +1 -0
- package/dist/framework/routing/expo-router.d.ts +31 -0
- package/dist/framework/routing/expo-router.d.ts.map +1 -0
- package/dist/framework/routing/expo-router.js +356 -0
- package/dist/framework/routing/expo-router.js.map +1 -0
- package/dist/framework/routing/index.d.ts +4 -0
- package/dist/framework/routing/index.d.ts.map +1 -0
- package/dist/framework/routing/index.js +16 -0
- package/dist/framework/routing/index.js.map +1 -0
- package/dist/framework/routing/next-router.d.ts +45 -0
- package/dist/framework/routing/next-router.d.ts.map +1 -0
- package/dist/framework/routing/next-router.js +378 -0
- package/dist/framework/routing/next-router.js.map +1 -0
- package/dist/framework/routing/route-tree.d.ts +73 -0
- package/dist/framework/routing/route-tree.d.ts.map +1 -0
- package/dist/framework/routing/route-tree.js +120 -0
- package/dist/framework/routing/route-tree.js.map +1 -0
- package/dist/framework/types.d.ts +230 -0
- package/dist/framework/types.d.ts.map +1 -0
- package/dist/framework/types.js +25 -0
- package/dist/framework/types.js.map +1 -0
- package/dist/graph/cross-language.d.ts +11 -0
- package/dist/graph/cross-language.d.ts.map +1 -0
- package/dist/graph/cross-language.js +123 -0
- package/dist/graph/cross-language.js.map +1 -0
- package/dist/graph/extract.d.ts +13 -0
- package/dist/graph/extract.d.ts.map +1 -0
- package/dist/graph/extract.js +79 -0
- package/dist/graph/extract.js.map +1 -0
- package/dist/graph/file-graph.d.ts +186 -0
- package/dist/graph/file-graph.d.ts.map +1 -0
- package/dist/graph/file-graph.js +427 -0
- package/dist/graph/file-graph.js.map +1 -0
- package/dist/graph/http-callsites.d.ts +14 -0
- package/dist/graph/http-callsites.d.ts.map +1 -0
- package/dist/graph/http-callsites.js +107 -0
- package/dist/graph/http-callsites.js.map +1 -0
- package/dist/graph/incremental.d.ts +122 -0
- package/dist/graph/incremental.d.ts.map +1 -0
- package/dist/graph/incremental.js +448 -0
- package/dist/graph/incremental.js.map +1 -0
- package/dist/graph/language.d.ts +31 -0
- package/dist/graph/language.d.ts.map +1 -0
- package/dist/graph/language.js +146 -0
- package/dist/graph/language.js.map +1 -0
- package/dist/graph/noise-filter.d.ts +46 -0
- package/dist/graph/noise-filter.d.ts.map +1 -0
- package/dist/graph/noise-filter.js +199 -0
- package/dist/graph/noise-filter.js.map +1 -0
- package/dist/graph/python-adapter.d.ts +36 -0
- package/dist/graph/python-adapter.d.ts.map +1 -0
- package/dist/graph/python-adapter.js +402 -0
- package/dist/graph/python-adapter.js.map +1 -0
- package/dist/graph/python-manifest.d.ts +22 -0
- package/dist/graph/python-manifest.d.ts.map +1 -0
- package/dist/graph/python-manifest.js +137 -0
- package/dist/graph/python-manifest.js.map +1 -0
- package/dist/graph/python-stdlib.d.ts +2 -0
- package/dist/graph/python-stdlib.d.ts.map +1 -0
- package/dist/graph/python-stdlib.js +53 -0
- package/dist/graph/python-stdlib.js.map +1 -0
- package/dist/graph/ts-morph-adapter.d.ts +65 -0
- package/dist/graph/ts-morph-adapter.d.ts.map +1 -0
- package/dist/graph/ts-morph-adapter.js +265 -0
- package/dist/graph/ts-morph-adapter.js.map +1 -0
- package/dist/graph/types.d.ts +37 -0
- package/dist/graph/types.d.ts.map +1 -0
- package/dist/graph/types.js +50 -0
- package/dist/graph/types.js.map +1 -0
- package/dist/index.d.ts +45 -0
- package/dist/index.d.ts.map +1 -0
- package/dist/index.js +51 -0
- package/dist/index.js.map +1 -0
- package/dist/infra/aws/aws-parse.d.ts +39 -0
- package/dist/infra/aws/aws-parse.d.ts.map +1 -0
- package/dist/infra/aws/aws-parse.js +149 -0
- package/dist/infra/aws/aws-parse.js.map +1 -0
- package/dist/infra/aws/aws.d.ts +6 -0
- package/dist/infra/aws/aws.d.ts.map +1 -0
- package/dist/infra/aws/aws.js +425 -0
- package/dist/infra/aws/aws.js.map +1 -0
- package/dist/infra/azure/azure-parse.d.ts +73 -0
- package/dist/infra/azure/azure-parse.d.ts.map +1 -0
- package/dist/infra/azure/azure-parse.js +178 -0
- package/dist/infra/azure/azure-parse.js.map +1 -0
- package/dist/infra/azure/azure.d.ts +12 -0
- package/dist/infra/azure/azure.d.ts.map +1 -0
- package/dist/infra/azure/azure.js +673 -0
- package/dist/infra/azure/azure.js.map +1 -0
- package/dist/infra/cloudflare/cloudflare.d.ts +50 -0
- package/dist/infra/cloudflare/cloudflare.d.ts.map +1 -0
- package/dist/infra/cloudflare/cloudflare.js +382 -0
- package/dist/infra/cloudflare/cloudflare.js.map +1 -0
- package/dist/infra/cloudflare/wrangler-parse.d.ts +6 -0
- package/dist/infra/cloudflare/wrangler-parse.d.ts.map +1 -0
- package/dist/infra/cloudflare/wrangler-parse.js +278 -0
- package/dist/infra/cloudflare/wrangler-parse.js.map +1 -0
- package/dist/infra/compose/compose-parse.d.ts +24 -0
- package/dist/infra/compose/compose-parse.d.ts.map +1 -0
- package/dist/infra/compose/compose-parse.js +77 -0
- package/dist/infra/compose/compose-parse.js.map +1 -0
- package/dist/infra/compose/compose.d.ts +12 -0
- package/dist/infra/compose/compose.d.ts.map +1 -0
- package/dist/infra/compose/compose.js +201 -0
- package/dist/infra/compose/compose.js.map +1 -0
- package/dist/infra/convex/convex-parse.d.ts +28 -0
- package/dist/infra/convex/convex-parse.d.ts.map +1 -0
- package/dist/infra/convex/convex-parse.js +74 -0
- package/dist/infra/convex/convex-parse.js.map +1 -0
- package/dist/infra/convex/convex.d.ts +18 -0
- package/dist/infra/convex/convex.d.ts.map +1 -0
- package/dist/infra/convex/convex.js +199 -0
- package/dist/infra/convex/convex.js.map +1 -0
- package/dist/infra/digitalocean/digitalocean.d.ts +33 -0
- package/dist/infra/digitalocean/digitalocean.d.ts.map +1 -0
- package/dist/infra/digitalocean/digitalocean.js +204 -0
- package/dist/infra/digitalocean/digitalocean.js.map +1 -0
- package/dist/infra/firebase/firebase.d.ts +43 -0
- package/dist/infra/firebase/firebase.d.ts.map +1 -0
- package/dist/infra/firebase/firebase.js +271 -0
- package/dist/infra/firebase/firebase.js.map +1 -0
- package/dist/infra/fly/fly-parse.d.ts +52 -0
- package/dist/infra/fly/fly-parse.d.ts.map +1 -0
- package/dist/infra/fly/fly-parse.js +114 -0
- package/dist/infra/fly/fly-parse.js.map +1 -0
- package/dist/infra/fly/fly.d.ts +15 -0
- package/dist/infra/fly/fly.d.ts.map +1 -0
- package/dist/infra/fly/fly.js +265 -0
- package/dist/infra/fly/fly.js.map +1 -0
- package/dist/infra/gcp/gcp-parse.d.ts +24 -0
- package/dist/infra/gcp/gcp-parse.d.ts.map +1 -0
- package/dist/infra/gcp/gcp-parse.js +123 -0
- package/dist/infra/gcp/gcp-parse.js.map +1 -0
- package/dist/infra/gcp/gcp.d.ts +10 -0
- package/dist/infra/gcp/gcp.d.ts.map +1 -0
- package/dist/infra/gcp/gcp.js +502 -0
- package/dist/infra/gcp/gcp.js.map +1 -0
- package/dist/infra/heroku/heroku-parse.d.ts +33 -0
- package/dist/infra/heroku/heroku-parse.d.ts.map +1 -0
- package/dist/infra/heroku/heroku-parse.js +63 -0
- package/dist/infra/heroku/heroku-parse.js.map +1 -0
- package/dist/infra/heroku/heroku.d.ts +24 -0
- package/dist/infra/heroku/heroku.d.ts.map +1 -0
- package/dist/infra/heroku/heroku.js +253 -0
- package/dist/infra/heroku/heroku.js.map +1 -0
- package/dist/infra/image-resolve.d.ts +56 -0
- package/dist/infra/image-resolve.d.ts.map +1 -0
- package/dist/infra/image-resolve.js +229 -0
- package/dist/infra/image-resolve.js.map +1 -0
- package/dist/infra/infra-step.d.ts +39 -0
- package/dist/infra/infra-step.d.ts.map +1 -0
- package/dist/infra/infra-step.js +57 -0
- package/dist/infra/infra-step.js.map +1 -0
- package/dist/infra/k8s/index.d.ts +78 -0
- package/dist/infra/k8s/index.d.ts.map +1 -0
- package/dist/infra/k8s/index.js +251 -0
- package/dist/infra/k8s/index.js.map +1 -0
- package/dist/infra/kamal/kamal-parse.d.ts +23 -0
- package/dist/infra/kamal/kamal-parse.d.ts.map +1 -0
- package/dist/infra/kamal/kamal-parse.js +66 -0
- package/dist/infra/kamal/kamal-parse.js.map +1 -0
- package/dist/infra/kamal/kamal.d.ts +17 -0
- package/dist/infra/kamal/kamal.d.ts.map +1 -0
- package/dist/infra/kamal/kamal.js +162 -0
- package/dist/infra/kamal/kamal.js.map +1 -0
- package/dist/infra/netlify/netlify.d.ts +33 -0
- package/dist/infra/netlify/netlify.d.ts.map +1 -0
- package/dist/infra/netlify/netlify.js +247 -0
- package/dist/infra/netlify/netlify.js.map +1 -0
- package/dist/infra/opentofu/opentofu.d.ts +3 -0
- package/dist/infra/opentofu/opentofu.d.ts.map +1 -0
- package/dist/infra/opentofu/opentofu.js +157 -0
- package/dist/infra/opentofu/opentofu.js.map +1 -0
- package/dist/infra/pulumi/pulumi-parse.d.ts +60 -0
- package/dist/infra/pulumi/pulumi-parse.d.ts.map +1 -0
- package/dist/infra/pulumi/pulumi-parse.js +155 -0
- package/dist/infra/pulumi/pulumi-parse.js.map +1 -0
- package/dist/infra/pulumi/pulumi.d.ts +23 -0
- package/dist/infra/pulumi/pulumi.d.ts.map +1 -0
- package/dist/infra/pulumi/pulumi.js +260 -0
- package/dist/infra/pulumi/pulumi.js.map +1 -0
- package/dist/infra/railway/railway-parse.d.ts +54 -0
- package/dist/infra/railway/railway-parse.d.ts.map +1 -0
- package/dist/infra/railway/railway-parse.js +200 -0
- package/dist/infra/railway/railway-parse.js.map +1 -0
- package/dist/infra/railway/railway.d.ts +19 -0
- package/dist/infra/railway/railway.d.ts.map +1 -0
- package/dist/infra/railway/railway.js +397 -0
- package/dist/infra/railway/railway.js.map +1 -0
- package/dist/infra/register.d.ts +2 -0
- package/dist/infra/register.d.ts.map +1 -0
- package/dist/infra/register.js +81 -0
- package/dist/infra/register.js.map +1 -0
- package/dist/infra/registry.d.ts +64 -0
- package/dist/infra/registry.d.ts.map +1 -0
- package/dist/infra/registry.js +205 -0
- package/dist/infra/registry.js.map +1 -0
- package/dist/infra/relevance.d.ts +16 -0
- package/dist/infra/relevance.d.ts.map +1 -0
- package/dist/infra/relevance.js +132 -0
- package/dist/infra/relevance.js.map +1 -0
- package/dist/infra/render/render-parse.d.ts +76 -0
- package/dist/infra/render/render-parse.d.ts.map +1 -0
- package/dist/infra/render/render-parse.js +155 -0
- package/dist/infra/render/render-parse.js.map +1 -0
- package/dist/infra/render/render.d.ts +11 -0
- package/dist/infra/render/render.d.ts.map +1 -0
- package/dist/infra/render/render.js +303 -0
- package/dist/infra/render/render.js.map +1 -0
- package/dist/infra/sst/sst-parse.d.ts +25 -0
- package/dist/infra/sst/sst-parse.d.ts.map +1 -0
- package/dist/infra/sst/sst-parse.js +45 -0
- package/dist/infra/sst/sst-parse.js.map +1 -0
- package/dist/infra/sst/sst.d.ts +19 -0
- package/dist/infra/sst/sst.d.ts.map +1 -0
- package/dist/infra/sst/sst.js +236 -0
- package/dist/infra/sst/sst.js.map +1 -0
- package/dist/infra/supabase/supabase-parse.d.ts +34 -0
- package/dist/infra/supabase/supabase-parse.d.ts.map +1 -0
- package/dist/infra/supabase/supabase-parse.js +113 -0
- package/dist/infra/supabase/supabase-parse.js.map +1 -0
- package/dist/infra/supabase/supabase.d.ts +15 -0
- package/dist/infra/supabase/supabase.d.ts.map +1 -0
- package/dist/infra/supabase/supabase.js +198 -0
- package/dist/infra/supabase/supabase.js.map +1 -0
- package/dist/infra/terraform/hcl-parse.d.ts +34 -0
- package/dist/infra/terraform/hcl-parse.d.ts.map +1 -0
- package/dist/infra/terraform/hcl-parse.js +203 -0
- package/dist/infra/terraform/hcl-parse.js.map +1 -0
- package/dist/infra/terraform/terraform.d.ts +14 -0
- package/dist/infra/terraform/terraform.d.ts.map +1 -0
- package/dist/infra/terraform/terraform.js +264 -0
- package/dist/infra/terraform/terraform.js.map +1 -0
- package/dist/infra/types.d.ts +151 -0
- package/dist/infra/types.d.ts.map +1 -0
- package/dist/infra/types.js +14 -0
- package/dist/infra/types.js.map +1 -0
- package/dist/infra/vercel/vercel-parse.d.ts +70 -0
- package/dist/infra/vercel/vercel-parse.d.ts.map +1 -0
- package/dist/infra/vercel/vercel-parse.js +194 -0
- package/dist/infra/vercel/vercel-parse.js.map +1 -0
- package/dist/infra/vercel/vercel.d.ts +30 -0
- package/dist/infra/vercel/vercel.d.ts.map +1 -0
- package/dist/infra/vercel/vercel.js +523 -0
- package/dist/infra/vercel/vercel.js.map +1 -0
- package/dist/infra/walk.d.ts +36 -0
- package/dist/infra/walk.d.ts.map +1 -0
- package/dist/infra/walk.js +85 -0
- package/dist/infra/walk.js.map +1 -0
- package/dist/safety.d.ts +13 -0
- package/dist/safety.d.ts.map +1 -0
- package/dist/safety.js +67 -0
- package/dist/safety.js.map +1 -0
- package/dist/types.d.ts +47 -0
- package/dist/types.d.ts.map +1 -0
- package/dist/types.js +111 -0
- package/dist/types.js.map +1 -0
- package/dist/version.d.ts +2 -0
- package/dist/version.d.ts.map +1 -0
- package/dist/version.js +35 -0
- package/dist/version.js.map +1 -0
- package/package.json +65 -0
|
@@ -0,0 +1,708 @@
|
|
|
1
|
+
// the Strawberry + Graphene GraphQL FrameworkAdapter. A Python
|
|
2
|
+
// framework adapter following the FastAPI/Flask template (/997), reusing
|
|
3
|
+
// the shared Python core (py-ast + parsePythonScope). Strawberry is the
|
|
4
|
+
// modern default and the priority here; Graphene is a solid best-effort.
|
|
5
|
+
//
|
|
6
|
+
// GraphQL declares its request surface as CLASSES — a `@strawberry.type` /
|
|
7
|
+
// `graphene.ObjectType` root (Query / Mutation / Subscription) whose resolver
|
|
8
|
+
// methods are the actual query/mutation entry points, plus plain object types
|
|
9
|
+
// (User / Post / …). We read this STATICALLY (install-free, never-store-source —
|
|
10
|
+
// a pure syntactic Pyright parse; never executes repo code) and persist only the
|
|
11
|
+
// derived groups/edges/roles:
|
|
12
|
+
//
|
|
13
|
+
// * detect() — the `strawberry-graphql` and/or `graphene` dependency;
|
|
14
|
+
// shallow nested scan for a `backend/`|`server/`|`api/`
|
|
15
|
+
// package (mirrors FastAPI's ).
|
|
16
|
+
// * roleTags — a Query/Mutation/Subscription ROOT (a `@strawberry.type`
|
|
17
|
+
// or `graphene.ObjectType` used as a schema root, or named
|
|
18
|
+
// by the GraphQL convention) + its field-resolver methods →
|
|
19
|
+
// `gateway` (the GraphQL request entry); every other
|
|
20
|
+
// object/input/interface type → `service` (role
|
|
21
|
+
// 'graphql-type'). METADATA onto the LOCKED MODULE_KINDS
|
|
22
|
+
// enum; never a new kind (only `role` renders).
|
|
23
|
+
// * syntheticEdges — the wiring the import graph doesn't name as verbs
|
|
24
|
+
// (kind 'calls' throughout): a resolver → the type it
|
|
25
|
+
// returns ('resolver-returns'); a graphene field decl → its
|
|
26
|
+
// type ('field-type'); a root field → its cross-file
|
|
27
|
+
// resolver function via `resolver=` ('field-resolver'); a
|
|
28
|
+
// `Schema(query=…, mutation=…)` construction → each root
|
|
29
|
+
// type file ('schema-root').
|
|
30
|
+
// * groupingPrior — files under a dedicated GraphQL directory
|
|
31
|
+
// (`graphql/`|`gql/`|`schema/`) that carry a GraphQL role
|
|
32
|
+
// → one subsystem (the schema layer), authoritative over
|
|
33
|
+
// the directory heuristic. Deterministic → no
|
|
34
|
+
// classificationsNeeded. Skipped when there is no dedicated
|
|
35
|
+
// GraphQL dir (degrades to directory grouping).
|
|
36
|
+
//
|
|
37
|
+
// Unresolvable schema roots / return types / cross-file resolvers DEGRADE + LOG —
|
|
38
|
+
// no silent caps.
|
|
39
|
+
import { existsSync, readdirSync } from 'node:fs';
|
|
40
|
+
import { join } from 'node:path';
|
|
41
|
+
import { clampConfidence, resolveBase } from '../detect-util.js';
|
|
42
|
+
import { readPythonDeps } from '../../graph/python-manifest.js';
|
|
43
|
+
import { parsePythonScope } from '../python/analyze.js';
|
|
44
|
+
import { callCallee, classBaseChains, className, classDirectAssignments, classDirectFunctions, keywordArg, memberChain, positionalArgs, stringValue, PN, } from '../python/py-ast.js';
|
|
45
|
+
/** Gather the signal set for a single root dir (reads manifests only). */
|
|
46
|
+
export function gatherGraphqlSignals(baseDir) {
|
|
47
|
+
const deps = readPythonDeps(baseDir);
|
|
48
|
+
return {
|
|
49
|
+
hasStrawberry: deps.has('strawberry-graphql'),
|
|
50
|
+
// `graphene`, or a `graphene-django` / `graphene-sqlalchemy` integration
|
|
51
|
+
// (which pulls graphene in): either means a Graphene schema is present.
|
|
52
|
+
hasGraphene: deps.has('graphene') || [...deps].some((d) => d.startsWith('graphene-')),
|
|
53
|
+
};
|
|
54
|
+
}
|
|
55
|
+
// Non-source dirs the nested scan skips (cheap + can't hold a first-party manifest).
|
|
56
|
+
const NESTED_SKIP_DIRS = new Set([
|
|
57
|
+
'node_modules',
|
|
58
|
+
'dist',
|
|
59
|
+
'build',
|
|
60
|
+
'out',
|
|
61
|
+
'coverage',
|
|
62
|
+
'venv',
|
|
63
|
+
'.venv',
|
|
64
|
+
'__pycache__',
|
|
65
|
+
'site-packages',
|
|
66
|
+
]);
|
|
67
|
+
/** True if `dir` holds a Python manifest worth scanning for deps. */
|
|
68
|
+
function hasPythonManifest(dir) {
|
|
69
|
+
return (existsSync(join(dir, 'pyproject.toml')) ||
|
|
70
|
+
existsSync(join(dir, 'setup.py')) ||
|
|
71
|
+
existsSync(join(dir, 'setup.cfg')) ||
|
|
72
|
+
existsSync(join(dir, 'requirements.txt')));
|
|
73
|
+
}
|
|
74
|
+
/**
|
|
75
|
+
* Immediate subdirs (depth 1) that contain a Python manifest — the shallow search
|
|
76
|
+
* for a nested GraphQL backend (`backend/` | `server/` | `api/`). Sorted, so the
|
|
77
|
+
* first-match pick is deterministic; skips dot-dirs + non-source dirs to stay cheap.
|
|
78
|
+
*/
|
|
79
|
+
function shallowManifestSubdirs(base) {
|
|
80
|
+
let entries;
|
|
81
|
+
try {
|
|
82
|
+
entries = readdirSync(base, { withFileTypes: true });
|
|
83
|
+
}
|
|
84
|
+
catch {
|
|
85
|
+
return [];
|
|
86
|
+
}
|
|
87
|
+
const out = [];
|
|
88
|
+
for (const e of entries) {
|
|
89
|
+
if (!e.isDirectory() || e.name.startsWith('.') || NESTED_SKIP_DIRS.has(e.name))
|
|
90
|
+
continue;
|
|
91
|
+
if (hasPythonManifest(join(base, e.name)))
|
|
92
|
+
out.push(e.name);
|
|
93
|
+
}
|
|
94
|
+
return out.sort();
|
|
95
|
+
}
|
|
96
|
+
/**
|
|
97
|
+
* Decide a GraphQL stack from the signal set. Either `strawberry-graphql` or
|
|
98
|
+
* `graphene` is sufficient; strawberry (the modern default) scores marginally
|
|
99
|
+
* higher. Returns null → generic-Python fallthrough, byte-for-byte unchanged.
|
|
100
|
+
*/
|
|
101
|
+
export function scoreGraphql(s, rootPath = '') {
|
|
102
|
+
if (!s.hasStrawberry && !s.hasGraphene)
|
|
103
|
+
return null;
|
|
104
|
+
const confidence = s.hasStrawberry ? 0.85 : 0.8;
|
|
105
|
+
return {
|
|
106
|
+
adapter: 'graphql',
|
|
107
|
+
confidence: clampConfidence(confidence),
|
|
108
|
+
rootPath,
|
|
109
|
+
metadata: {
|
|
110
|
+
strawberry: s.hasStrawberry,
|
|
111
|
+
graphene: s.hasGraphene,
|
|
112
|
+
signals: { strawberry: s.hasStrawberry, graphene: s.hasGraphene },
|
|
113
|
+
},
|
|
114
|
+
};
|
|
115
|
+
}
|
|
116
|
+
// Collapse priority when one FILE carries several roles, AND downstream when
|
|
117
|
+
// several files of different roles land in one module. A query root outranks a
|
|
118
|
+
// mutation outranks a subscription (all root request entries) outranks a
|
|
119
|
+
// standalone resolver-function file outranks a schema-assembly file outranks a
|
|
120
|
+
// plain data type. All but 'graphql-type' are GraphQL request entries → gateway.
|
|
121
|
+
const ROLE_PRIORITY = {
|
|
122
|
+
'graphql-query': 8,
|
|
123
|
+
'graphql-mutation': 7,
|
|
124
|
+
'graphql-subscription': 6,
|
|
125
|
+
'graphql-resolver': 5,
|
|
126
|
+
'graphql-schema': 4,
|
|
127
|
+
'graphql-type': 3,
|
|
128
|
+
};
|
|
129
|
+
const ROLE_KIND = {
|
|
130
|
+
'graphql-query': 'gateway',
|
|
131
|
+
'graphql-mutation': 'gateway',
|
|
132
|
+
'graphql-subscription': 'gateway',
|
|
133
|
+
'graphql-resolver': 'gateway',
|
|
134
|
+
'graphql-schema': 'gateway',
|
|
135
|
+
'graphql-type': 'service',
|
|
136
|
+
};
|
|
137
|
+
const SLOT_ROLE = {
|
|
138
|
+
query: 'graphql-query',
|
|
139
|
+
mutation: 'graphql-mutation',
|
|
140
|
+
subscription: 'graphql-subscription',
|
|
141
|
+
};
|
|
142
|
+
// The GraphQL root type names, by convention (a schema requires exactly these
|
|
143
|
+
// three operation roots; strawberry/graphene root classes are near-universally
|
|
144
|
+
// named this) — the fallback signal when a Schema(...) construction can't be
|
|
145
|
+
// resolved to the class.
|
|
146
|
+
const ROOT_NAMES = new Map([
|
|
147
|
+
['Query', 'query'],
|
|
148
|
+
['Mutation', 'mutation'],
|
|
149
|
+
['Subscription', 'subscription'],
|
|
150
|
+
]);
|
|
151
|
+
// The strawberry module roots recognized as first-party GraphQL decorators. The
|
|
152
|
+
// official Django integration `strawberry_django` re-exports `type`/`field`/
|
|
153
|
+
// `mutation` (a large fraction of real strawberry apps use it), so treat its
|
|
154
|
+
// aliases the same as `strawberry`'s.
|
|
155
|
+
const STRAWBERRY_MODULE_ROOTS = ['strawberry', 'strawberry_django'];
|
|
156
|
+
// Strawberry class decorators: `@strawberry.type` (rootable object type) vs
|
|
157
|
+
// `@strawberry.input` / `@strawberry.interface` (always a plain type).
|
|
158
|
+
const STRAWBERRY_TYPE_DECOS = new Set(['type']);
|
|
159
|
+
const STRAWBERRY_PLAIN_DECOS = new Set(['input', 'interface']);
|
|
160
|
+
// Strawberry field-resolver method decorators.
|
|
161
|
+
const STRAWBERRY_FIELD_DECOS = new Set(['field', 'mutation', 'subscription']);
|
|
162
|
+
// Graphene base classes (by leaf name; gated on graphene being detected). The
|
|
163
|
+
// integration ObjectType variants (graphene-django's `DjangoObjectType`,
|
|
164
|
+
// graphene-sqlalchemy's `SQLAlchemyObjectType`, graphene-mongo's
|
|
165
|
+
// `MongoengineObjectType`) are the norm in real graphene apps, so treat them as
|
|
166
|
+
// object types too.
|
|
167
|
+
const GRAPHENE_OBJECT_BASES = new Set([
|
|
168
|
+
'ObjectType',
|
|
169
|
+
'DjangoObjectType',
|
|
170
|
+
'SQLAlchemyObjectType',
|
|
171
|
+
'MongoengineObjectType',
|
|
172
|
+
]);
|
|
173
|
+
const GRAPHENE_ROOT_BASE_SLOT = new Map([
|
|
174
|
+
// A `graphene.Mutation` / `graphene.Subscription` subclass IS an individual
|
|
175
|
+
// request entry (a mutation/subscription field resolver) → a gateway root.
|
|
176
|
+
['Mutation', 'mutation'],
|
|
177
|
+
['Subscription', 'subscription'],
|
|
178
|
+
]);
|
|
179
|
+
const GRAPHENE_PLAIN_BASES = new Set(['InputObjectType', 'Interface']);
|
|
180
|
+
// Graphene field constructors whose FIRST positional arg is the field's type.
|
|
181
|
+
const GRAPHENE_FIELD_CTORS = new Set(['Field', 'List', 'NonNull']);
|
|
182
|
+
// Graphene resolver method: `resolve_<field>` or a Mutation's `mutate`.
|
|
183
|
+
function isGrapheneResolverName(name) {
|
|
184
|
+
return name.startsWith('resolve_') || name === 'mutate';
|
|
185
|
+
}
|
|
186
|
+
// GraphQL-dedicated directory segments that anchor the schema subsystem.
|
|
187
|
+
const GRAPHQL_DIR_SEGMENTS = new Set(['graphql', 'gql', 'schema']);
|
|
188
|
+
// Memoized on the FrameworkContext OBJECT so groupingPrior + syntheticEdges +
|
|
189
|
+
// roleTags share ONE parse, while the merge walk's per-checkpoint ctx gets a fresh
|
|
190
|
+
// analysis — no cross-tree staleness. Mirrors fastapi / flask / nest.
|
|
191
|
+
const ANALYSIS_CACHE = new WeakMap();
|
|
192
|
+
function slugify(s) {
|
|
193
|
+
return s
|
|
194
|
+
.replace(/([a-z0-9])([A-Z])/g, '$1-$2')
|
|
195
|
+
.toLowerCase()
|
|
196
|
+
.replace(/[^a-z0-9]+/g, '-')
|
|
197
|
+
.replace(/^-+|-+$/g, '');
|
|
198
|
+
}
|
|
199
|
+
function humanize(s) {
|
|
200
|
+
const words = s
|
|
201
|
+
.replace(/([a-z0-9])([A-Z])/g, '$1 $2')
|
|
202
|
+
.replace(/[_\-.]+/g, ' ')
|
|
203
|
+
.trim()
|
|
204
|
+
.split(/\s+/)
|
|
205
|
+
.filter(Boolean);
|
|
206
|
+
if (words.length === 0)
|
|
207
|
+
return s;
|
|
208
|
+
return words.map((w) => w.charAt(0).toUpperCase() + w.slice(1)).join(' ');
|
|
209
|
+
}
|
|
210
|
+
function dirSegment(fileId) {
|
|
211
|
+
const slash = fileId.indexOf('/');
|
|
212
|
+
return slash > 0 ? slugify(fileId.slice(0, slash)) : 'root';
|
|
213
|
+
}
|
|
214
|
+
// Keep the highest-priority candidate role per file (deterministic, lexical
|
|
215
|
+
// tiebreak) — mirrors the fastapi/flask/nest addRole + the contribute-step collapse.
|
|
216
|
+
function addRole(map, fileId, role) {
|
|
217
|
+
const cur = map.get(fileId);
|
|
218
|
+
if (cur === undefined ||
|
|
219
|
+
ROLE_PRIORITY[role] > ROLE_PRIORITY[cur] ||
|
|
220
|
+
(ROLE_PRIORITY[role] === ROLE_PRIORITY[cur] && role < cur)) {
|
|
221
|
+
map.set(fileId, role);
|
|
222
|
+
}
|
|
223
|
+
}
|
|
224
|
+
function addEdge(edges, from, to, relation) {
|
|
225
|
+
if (from === to)
|
|
226
|
+
return; // intra-file wiring collapses; the step drops self-edges too
|
|
227
|
+
const key = `${from}→${to}:${relation}`;
|
|
228
|
+
if (!edges.has(key)) {
|
|
229
|
+
edges.set(key, { source: from, target: to, kind: 'calls', metadata: { framework: 'graphql', relation } });
|
|
230
|
+
}
|
|
231
|
+
}
|
|
232
|
+
// A decorator's callee chain (`@strawberry.type(...)` → root 'strawberry', path
|
|
233
|
+
// ['type']; `@strawberry.field` → root 'strawberry', path ['field']). The expr is
|
|
234
|
+
// either a call (with args) or a bare name/attribute.
|
|
235
|
+
function decoratorChain(deco) {
|
|
236
|
+
const expr = deco.d.expr;
|
|
237
|
+
if (expr.nodeType === PN.Call)
|
|
238
|
+
return callCallee(expr);
|
|
239
|
+
return memberChain(expr);
|
|
240
|
+
}
|
|
241
|
+
// The framework import bindings for one module root in a file (both `import X` and
|
|
242
|
+
// `from X import a, b` forms). Deep imports (`import strawberry.federation`) still
|
|
243
|
+
// bind the top-level alias.
|
|
244
|
+
function frameworkImports(file, moduleRoot) {
|
|
245
|
+
const aliases = new Set();
|
|
246
|
+
const symbols = new Map();
|
|
247
|
+
for (const imp of file.nodes.imports) {
|
|
248
|
+
if (imp.nodeType === PN.Import) {
|
|
249
|
+
for (const entry of imp.d.list) {
|
|
250
|
+
const parts = entry.d.module.d.nameParts.map((p) => p.d.value);
|
|
251
|
+
if (parts.length === 0 || parts[0] !== moduleRoot)
|
|
252
|
+
continue;
|
|
253
|
+
aliases.add(entry.d.alias ? entry.d.alias.d.value : parts[0]);
|
|
254
|
+
}
|
|
255
|
+
}
|
|
256
|
+
else if (imp.nodeType === PN.ImportFrom) {
|
|
257
|
+
const from = imp;
|
|
258
|
+
if (from.d.module.d.leadingDots !== 0)
|
|
259
|
+
continue; // relative import — not the framework
|
|
260
|
+
const parts = from.d.module.d.nameParts.map((p) => p.d.value);
|
|
261
|
+
if (parts.length === 0 || parts[0] !== moduleRoot)
|
|
262
|
+
continue;
|
|
263
|
+
for (const spec of from.d.imports) {
|
|
264
|
+
const local = spec.d.alias ? spec.d.alias.d.value : spec.d.name.d.value;
|
|
265
|
+
symbols.set(local, spec.d.name.d.value);
|
|
266
|
+
}
|
|
267
|
+
}
|
|
268
|
+
}
|
|
269
|
+
return { aliases, symbols };
|
|
270
|
+
}
|
|
271
|
+
// Classify a strawberry class decorator: 'type' (rootable object type), 'plain'
|
|
272
|
+
// (input/interface), or undefined (not a strawberry type class).
|
|
273
|
+
function strawberryClassKind(cls, imp) {
|
|
274
|
+
for (const deco of cls.d.decorators) {
|
|
275
|
+
const chain = decoratorChain(deco);
|
|
276
|
+
if (!chain)
|
|
277
|
+
continue;
|
|
278
|
+
// `@strawberry.type` / `@<alias>.type(...)` / `@strawberry.federation.type`.
|
|
279
|
+
if (imp.aliases.has(chain.root) && chain.path.length >= 1) {
|
|
280
|
+
const leaf = chain.path[chain.path.length - 1];
|
|
281
|
+
if (STRAWBERRY_TYPE_DECOS.has(leaf))
|
|
282
|
+
return 'type';
|
|
283
|
+
if (STRAWBERRY_PLAIN_DECOS.has(leaf))
|
|
284
|
+
return 'plain';
|
|
285
|
+
}
|
|
286
|
+
// Bare `@type` / `@input` via `from strawberry import type`.
|
|
287
|
+
if (chain.path.length === 0) {
|
|
288
|
+
const canon = imp.symbols.get(chain.root);
|
|
289
|
+
if (canon && STRAWBERRY_TYPE_DECOS.has(canon))
|
|
290
|
+
return 'type';
|
|
291
|
+
if (canon && STRAWBERRY_PLAIN_DECOS.has(canon))
|
|
292
|
+
return 'plain';
|
|
293
|
+
}
|
|
294
|
+
}
|
|
295
|
+
return undefined;
|
|
296
|
+
}
|
|
297
|
+
// Classify a graphene class from its base classes (gated on graphene detected):
|
|
298
|
+
// a rootable ObjectType, a Mutation/Subscription root resolver, a plain
|
|
299
|
+
// input/interface, or undefined.
|
|
300
|
+
function grapheneClassKind(cls) {
|
|
301
|
+
for (const chain of classBaseChains(cls)) {
|
|
302
|
+
const leaf = chain.path.length ? chain.path[chain.path.length - 1] : chain.root;
|
|
303
|
+
if (GRAPHENE_OBJECT_BASES.has(leaf))
|
|
304
|
+
return { rootable: true };
|
|
305
|
+
const slot = GRAPHENE_ROOT_BASE_SLOT.get(leaf);
|
|
306
|
+
if (slot)
|
|
307
|
+
return { rootable: true, slotBase: slot };
|
|
308
|
+
if (GRAPHENE_PLAIN_BASES.has(leaf))
|
|
309
|
+
return { rootable: false };
|
|
310
|
+
}
|
|
311
|
+
return undefined;
|
|
312
|
+
}
|
|
313
|
+
// Is a strawberry method a field resolver (decorated with @strawberry.field /
|
|
314
|
+
// .mutation / .subscription, in either module-attr or from-import form)?
|
|
315
|
+
function isStrawberryResolver(fn, imp) {
|
|
316
|
+
for (const deco of fn.d.decorators) {
|
|
317
|
+
const chain = decoratorChain(deco);
|
|
318
|
+
if (!chain)
|
|
319
|
+
continue;
|
|
320
|
+
if (imp.aliases.has(chain.root) && chain.path.length >= 1) {
|
|
321
|
+
if (STRAWBERRY_FIELD_DECOS.has(chain.path[chain.path.length - 1]))
|
|
322
|
+
return true;
|
|
323
|
+
}
|
|
324
|
+
if (chain.path.length === 0) {
|
|
325
|
+
const canon = imp.symbols.get(chain.root);
|
|
326
|
+
if (canon && STRAWBERRY_FIELD_DECOS.has(canon))
|
|
327
|
+
return true;
|
|
328
|
+
}
|
|
329
|
+
}
|
|
330
|
+
return false;
|
|
331
|
+
}
|
|
332
|
+
// Collect the internal-resolvable TYPE NAMES from a return/type annotation,
|
|
333
|
+
// unwrapping generics (`List[User]`), unions (`User | None`), subscripts, and
|
|
334
|
+
// forward-ref strings (`"User"`). Container names (List/Optional) and primitives
|
|
335
|
+
// simply don't resolve to an internal file downstream, so they're harmless.
|
|
336
|
+
function collectAnnotationTypeNames(expr, out) {
|
|
337
|
+
if (!expr)
|
|
338
|
+
return;
|
|
339
|
+
const nt = expr.nodeType;
|
|
340
|
+
if (nt === PN.Name || nt === PN.MemberAccess) {
|
|
341
|
+
const chain = memberChain(expr);
|
|
342
|
+
if (chain)
|
|
343
|
+
out.add(chain.path.length ? chain.path[chain.path.length - 1] : chain.root);
|
|
344
|
+
return;
|
|
345
|
+
}
|
|
346
|
+
if (nt === PN.Index) {
|
|
347
|
+
const idx = expr;
|
|
348
|
+
collectAnnotationTypeNames(idx.d.leftExpr, out); // e.g. a relay `Connection[User]`
|
|
349
|
+
for (const item of idx.d.items)
|
|
350
|
+
collectAnnotationTypeNames(item.d.valueExpr, out);
|
|
351
|
+
return;
|
|
352
|
+
}
|
|
353
|
+
if (nt === PN.BinaryOperation) {
|
|
354
|
+
const bin = expr;
|
|
355
|
+
collectAnnotationTypeNames(bin.d.leftExpr, out);
|
|
356
|
+
collectAnnotationTypeNames(bin.d.rightExpr, out);
|
|
357
|
+
return;
|
|
358
|
+
}
|
|
359
|
+
if (nt === PN.StringList) {
|
|
360
|
+
const s = stringValue(expr);
|
|
361
|
+
if (s && /^[A-Za-z_][A-Za-z0-9_]*$/.test(s))
|
|
362
|
+
out.add(s); // a plain forward-ref name
|
|
363
|
+
}
|
|
364
|
+
}
|
|
365
|
+
function analyzeGraphql(ctx) {
|
|
366
|
+
const strawberryEnabled = ctx.match.metadata?.strawberry === true;
|
|
367
|
+
const grapheneEnabled = ctx.match.metadata?.graphene === true;
|
|
368
|
+
const { parsed } = parsePythonScope(ctx);
|
|
369
|
+
// Pre-pass: per-file class-name set (so a return type / schema ref resolves to a
|
|
370
|
+
// co-located class file), plus per-file framework import bindings.
|
|
371
|
+
const localClassNames = new Map();
|
|
372
|
+
const strawberryImp = new Map();
|
|
373
|
+
const grapheneImp = new Map();
|
|
374
|
+
for (const [id, file] of parsed) {
|
|
375
|
+
const names = new Set();
|
|
376
|
+
for (const cls of file.nodes.classes) {
|
|
377
|
+
const n = className(cls);
|
|
378
|
+
if (n)
|
|
379
|
+
names.add(n);
|
|
380
|
+
}
|
|
381
|
+
localClassNames.set(id, names);
|
|
382
|
+
if (strawberryEnabled) {
|
|
383
|
+
// Merge strawberry + strawberry_django import bindings into one alias/symbol set.
|
|
384
|
+
const merged = { aliases: new Set(), symbols: new Map() };
|
|
385
|
+
for (const root of STRAWBERRY_MODULE_ROOTS) {
|
|
386
|
+
const fi = frameworkImports(file, root);
|
|
387
|
+
for (const a of fi.aliases)
|
|
388
|
+
merged.aliases.add(a);
|
|
389
|
+
for (const [k, v] of fi.symbols)
|
|
390
|
+
merged.symbols.set(k, v);
|
|
391
|
+
}
|
|
392
|
+
strawberryImp.set(id, merged);
|
|
393
|
+
}
|
|
394
|
+
if (grapheneEnabled)
|
|
395
|
+
grapheneImp.set(id, frameworkImports(file, 'graphene'));
|
|
396
|
+
}
|
|
397
|
+
// Resolve a bare TYPE/CLASS name to a file: an internal import, else a class
|
|
398
|
+
// co-located in the SAME file. Mirrors nest's resolveClassRef.
|
|
399
|
+
function resolveName(name, fromFile) {
|
|
400
|
+
const binds = parsed.get(fromFile)?.bindings;
|
|
401
|
+
return binds?.get(name) ?? (localClassNames.get(fromFile)?.has(name) ? fromFile : undefined);
|
|
402
|
+
}
|
|
403
|
+
const diag = {
|
|
404
|
+
unresolvedSchemaRoots: new Set(),
|
|
405
|
+
unresolvedReturns: new Set(),
|
|
406
|
+
unresolvedResolvers: new Set(),
|
|
407
|
+
};
|
|
408
|
+
const edges = new Map();
|
|
409
|
+
// Pass 1 — discover every GraphQL class + collect Schema(...) root wiring.
|
|
410
|
+
const classes = [];
|
|
411
|
+
// `${rootFile}#${refName}` → slot, from a resolved Schema(query=…) kwarg.
|
|
412
|
+
const schemaRootSlot = new Map();
|
|
413
|
+
const schemaFiles = new Set(); // files that construct a Schema(...)
|
|
414
|
+
function isSchemaCall(callee, id) {
|
|
415
|
+
const leaf = callee.path.length ? callee.path[callee.path.length - 1] : callee.root;
|
|
416
|
+
if (leaf !== 'Schema')
|
|
417
|
+
return false;
|
|
418
|
+
if (callee.path.length >= 1) {
|
|
419
|
+
// `strawberry.Schema(...)` / `graphene.Schema(...)`.
|
|
420
|
+
const s = strawberryEnabled && strawberryImp.get(id)?.aliases.has(callee.root);
|
|
421
|
+
const g = grapheneEnabled && grapheneImp.get(id)?.aliases.has(callee.root);
|
|
422
|
+
return Boolean(s || g);
|
|
423
|
+
}
|
|
424
|
+
// Bare `Schema(...)` via `from strawberry import Schema`.
|
|
425
|
+
const s = strawberryEnabled && strawberryImp.get(id)?.symbols.get(callee.root) === 'Schema';
|
|
426
|
+
const g = grapheneEnabled && grapheneImp.get(id)?.symbols.get(callee.root) === 'Schema';
|
|
427
|
+
return Boolean(s || g);
|
|
428
|
+
}
|
|
429
|
+
for (const [id, file] of parsed) {
|
|
430
|
+
const sImp = strawberryImp.get(id);
|
|
431
|
+
const gImp = grapheneImp.get(id);
|
|
432
|
+
for (const cls of file.nodes.classes) {
|
|
433
|
+
const name = className(cls);
|
|
434
|
+
if (!name)
|
|
435
|
+
continue;
|
|
436
|
+
if (strawberryEnabled && sImp) {
|
|
437
|
+
const kind = strawberryClassKind(cls, sImp);
|
|
438
|
+
if (kind) {
|
|
439
|
+
classes.push({ fileId: id, name, framework: 'strawberry', rootable: kind === 'type', node: cls });
|
|
440
|
+
continue;
|
|
441
|
+
}
|
|
442
|
+
}
|
|
443
|
+
if (grapheneEnabled && gImp) {
|
|
444
|
+
const gk = grapheneClassKind(cls);
|
|
445
|
+
if (gk) {
|
|
446
|
+
classes.push({
|
|
447
|
+
fileId: id,
|
|
448
|
+
name,
|
|
449
|
+
framework: 'graphene',
|
|
450
|
+
rootable: gk.rootable,
|
|
451
|
+
slotBase: gk.slotBase,
|
|
452
|
+
node: cls,
|
|
453
|
+
});
|
|
454
|
+
}
|
|
455
|
+
}
|
|
456
|
+
}
|
|
457
|
+
// Schema(...) constructions → resolve each operation-root kwarg to a file.
|
|
458
|
+
for (const call of file.nodes.calls) {
|
|
459
|
+
const callee = callCallee(call);
|
|
460
|
+
if (!callee || !isSchemaCall(callee, id))
|
|
461
|
+
continue;
|
|
462
|
+
schemaFiles.add(id); // the GraphQL schema-assembly entry point
|
|
463
|
+
for (const slot of ['query', 'mutation', 'subscription']) {
|
|
464
|
+
const arg = keywordArg(call, slot);
|
|
465
|
+
if (!arg)
|
|
466
|
+
continue;
|
|
467
|
+
const chain = memberChain(arg);
|
|
468
|
+
if (!chain) {
|
|
469
|
+
diag.unresolvedSchemaRoots.add(`${id}: Schema(${slot}=…)`);
|
|
470
|
+
continue;
|
|
471
|
+
}
|
|
472
|
+
const refName = chain.path.length ? chain.path[chain.path.length - 1] : chain.root;
|
|
473
|
+
const targetFile = resolveName(chain.root, id);
|
|
474
|
+
if (targetFile) {
|
|
475
|
+
schemaRootSlot.set(`${targetFile}#${refName}`, slot);
|
|
476
|
+
addEdge(edges, id, targetFile, 'schema-root');
|
|
477
|
+
}
|
|
478
|
+
else {
|
|
479
|
+
diag.unresolvedSchemaRoots.add(`${id}: Schema(${slot}=${refName})`);
|
|
480
|
+
}
|
|
481
|
+
}
|
|
482
|
+
}
|
|
483
|
+
}
|
|
484
|
+
// Pass 2 — roles + resolver/field edges, now that schema roots are known.
|
|
485
|
+
const roleByFile = new Map();
|
|
486
|
+
const roleFiles = new Set(); // every file carrying any GraphQL role (for grouping)
|
|
487
|
+
for (const gc of classes) {
|
|
488
|
+
// Decide the class's slot (root) or plain-type role.
|
|
489
|
+
let slot = gc.slotBase;
|
|
490
|
+
if (!slot && gc.rootable) {
|
|
491
|
+
slot = schemaRootSlot.get(`${gc.fileId}#${gc.name}`) ?? ROOT_NAMES.get(gc.name);
|
|
492
|
+
}
|
|
493
|
+
const role = slot ? SLOT_ROLE[slot] : 'graphql-type';
|
|
494
|
+
addRole(roleByFile, gc.fileId, role);
|
|
495
|
+
roleFiles.add(gc.fileId);
|
|
496
|
+
// Resolver return-type + field edges from the class body.
|
|
497
|
+
for (const fn of classDirectFunctions(gc.node)) {
|
|
498
|
+
const fnName = fn.d.name ? fn.d.name.d.value : undefined;
|
|
499
|
+
const isResolver = (gc.framework === 'strawberry' && isStrawberryResolver(fn, strawberryImp.get(gc.fileId))) ||
|
|
500
|
+
(gc.framework === 'graphene' && fnName !== undefined && isGrapheneResolverName(fnName));
|
|
501
|
+
if (!isResolver)
|
|
502
|
+
continue;
|
|
503
|
+
const typeNames = new Set();
|
|
504
|
+
collectAnnotationTypeNames(fn.d.returnAnnotation, typeNames);
|
|
505
|
+
let resolvedAny = false;
|
|
506
|
+
for (const tn of typeNames) {
|
|
507
|
+
const target = resolveName(tn, gc.fileId);
|
|
508
|
+
if (target) {
|
|
509
|
+
addEdge(edges, gc.fileId, target, 'resolver-returns');
|
|
510
|
+
resolvedAny = true;
|
|
511
|
+
}
|
|
512
|
+
}
|
|
513
|
+
if (typeNames.size > 0 && !resolvedAny) {
|
|
514
|
+
diag.unresolvedReturns.add(`${gc.fileId}: ${gc.name}.${fnName ?? '<fn>'} → ${[...typeNames].sort().join('|')}`);
|
|
515
|
+
}
|
|
516
|
+
}
|
|
517
|
+
// Field-declaration edges: graphene `x = graphene.Field(User)` type target +
|
|
518
|
+
// any `resolver=` cross-file resolver function (strawberry & graphene).
|
|
519
|
+
for (const a of classDirectAssignments(gc.node)) {
|
|
520
|
+
const rhs = a.d.rightExpr;
|
|
521
|
+
if (rhs.nodeType !== PN.Call)
|
|
522
|
+
continue;
|
|
523
|
+
const call = rhs;
|
|
524
|
+
const callee = callCallee(call);
|
|
525
|
+
// graphene field type: `Field(User)` / `graphene.List(User)` → first arg type.
|
|
526
|
+
if (gc.framework === 'graphene' && callee) {
|
|
527
|
+
const leaf = callee.path.length ? callee.path[callee.path.length - 1] : callee.root;
|
|
528
|
+
if (GRAPHENE_FIELD_CTORS.has(leaf)) {
|
|
529
|
+
const typeArg = positionalArgs(call)[0];
|
|
530
|
+
const names = new Set();
|
|
531
|
+
collectAnnotationTypeNames(typeArg, names);
|
|
532
|
+
for (const tn of names) {
|
|
533
|
+
const target = resolveName(tn, gc.fileId);
|
|
534
|
+
if (target)
|
|
535
|
+
addEdge(edges, gc.fileId, target, 'field-type');
|
|
536
|
+
}
|
|
537
|
+
}
|
|
538
|
+
}
|
|
539
|
+
// `resolver=fn` cross-file resolver function (both frameworks).
|
|
540
|
+
const resolverArg = keywordArg(call, 'resolver');
|
|
541
|
+
if (resolverArg) {
|
|
542
|
+
const chain = memberChain(resolverArg);
|
|
543
|
+
const target = chain ? resolveName(chain.root, gc.fileId) : undefined;
|
|
544
|
+
if (target) {
|
|
545
|
+
addEdge(edges, gc.fileId, target, 'field-resolver');
|
|
546
|
+
// A standalone resolver-function file is itself a GraphQL request entry.
|
|
547
|
+
addRole(roleByFile, target, 'graphql-resolver');
|
|
548
|
+
roleFiles.add(target);
|
|
549
|
+
}
|
|
550
|
+
else if (chain) {
|
|
551
|
+
diag.unresolvedResolvers.add(`${gc.fileId}: resolver=${chain.root}`);
|
|
552
|
+
}
|
|
553
|
+
}
|
|
554
|
+
}
|
|
555
|
+
}
|
|
556
|
+
// The schema-assembly files (a `Schema(...)` construction) are GraphQL entries
|
|
557
|
+
// too — tag them (a root class in the same file still wins by priority).
|
|
558
|
+
for (const f of schemaFiles) {
|
|
559
|
+
addRole(roleByFile, f, 'graphql-schema');
|
|
560
|
+
roleFiles.add(f);
|
|
561
|
+
}
|
|
562
|
+
// Grouping — files carrying a GraphQL role, anchored on a dedicated GraphQL dir.
|
|
563
|
+
const groups = buildGraphqlGroups(roleFiles);
|
|
564
|
+
const roles = new Map();
|
|
565
|
+
for (const [fileId, role] of roleByFile) {
|
|
566
|
+
roles.set(fileId, {
|
|
567
|
+
role,
|
|
568
|
+
kind: ROLE_KIND[role],
|
|
569
|
+
priority: ROLE_PRIORITY[role],
|
|
570
|
+
metadata: { framework: 'graphql' },
|
|
571
|
+
});
|
|
572
|
+
}
|
|
573
|
+
const sortedEdges = [...edges.values()].sort((a, b) => a.source < b.source
|
|
574
|
+
? -1
|
|
575
|
+
: a.source > b.source
|
|
576
|
+
? 1
|
|
577
|
+
: a.target < b.target
|
|
578
|
+
? -1
|
|
579
|
+
: a.target > b.target
|
|
580
|
+
? 1
|
|
581
|
+
: a.metadata?.relation < b.metadata?.relation
|
|
582
|
+
? -1
|
|
583
|
+
: a.metadata?.relation > b.metadata?.relation
|
|
584
|
+
? 1
|
|
585
|
+
: 0);
|
|
586
|
+
// Positive signal for validation (mirrors fastapi/flask/nest's log line).
|
|
587
|
+
if (groups.length > 0 || roleByFile.size > 0 || sortedEdges.length > 0) {
|
|
588
|
+
console.log(` [graphql] ${classes.length} type(s) → ${roleByFile.size} role(s) · ${groups.length} schema group(s) · ${sortedEdges.length} edge(s)`);
|
|
589
|
+
}
|
|
590
|
+
// No silent caps (locked): log everything that degraded.
|
|
591
|
+
if (diag.unresolvedSchemaRoots.size > 0 ||
|
|
592
|
+
diag.unresolvedReturns.size > 0 ||
|
|
593
|
+
diag.unresolvedResolvers.size > 0) {
|
|
594
|
+
const parts = [];
|
|
595
|
+
if (diag.unresolvedSchemaRoots.size > 0) {
|
|
596
|
+
parts.push(`${diag.unresolvedSchemaRoots.size} unresolvable schema root(s): ${[...diag.unresolvedSchemaRoots].sort().slice(0, 10).join(' · ')}`);
|
|
597
|
+
}
|
|
598
|
+
if (diag.unresolvedReturns.size > 0) {
|
|
599
|
+
parts.push(`${diag.unresolvedReturns.size} unresolvable resolver return type(s): ${[...diag.unresolvedReturns].sort().slice(0, 10).join(' · ')}`);
|
|
600
|
+
}
|
|
601
|
+
if (diag.unresolvedResolvers.size > 0) {
|
|
602
|
+
parts.push(`${diag.unresolvedResolvers.size} unresolvable resolver ref(s): ${[...diag.unresolvedResolvers].sort().slice(0, 10).join(' · ')}`);
|
|
603
|
+
}
|
|
604
|
+
console.log(` [graphql] degraded: ${parts.join(' · ')} (logged, not silently dropped)`);
|
|
605
|
+
}
|
|
606
|
+
return { groups, edges: sortedEdges, roles };
|
|
607
|
+
}
|
|
608
|
+
// The nearest ancestor directory of `fileId` whose basename is a dedicated GraphQL
|
|
609
|
+
// dir (`graphql`/`gql`/`schema`), shallowest-first; undefined when there is none.
|
|
610
|
+
function graphqlAnchor(fileId) {
|
|
611
|
+
const parts = fileId.split('/');
|
|
612
|
+
for (let i = 0; i < parts.length - 1; i++) {
|
|
613
|
+
if (GRAPHQL_DIR_SEGMENTS.has(parts[i].toLowerCase())) {
|
|
614
|
+
return { anchor: parts.slice(0, i + 1).join('/'), segment: parts[i] };
|
|
615
|
+
}
|
|
616
|
+
}
|
|
617
|
+
return undefined;
|
|
618
|
+
}
|
|
619
|
+
// One subsystem per dedicated GraphQL directory, gathering the GraphQL-role files
|
|
620
|
+
// under it. Deterministic, collision-free ids (the assignGroupIds discipline):
|
|
621
|
+
// process anchors by ascending path so the SMALLEST wins the bare slug; collisions
|
|
622
|
+
// take a `-<dirSegment>` then `-<n>` suffix. Files with no GraphQL dir are left
|
|
623
|
+
// ungrouped (directory grouping handles them).
|
|
624
|
+
function buildGraphqlGroups(roleFiles) {
|
|
625
|
+
const byAnchor = new Map();
|
|
626
|
+
for (const fileId of roleFiles) {
|
|
627
|
+
const a = graphqlAnchor(fileId);
|
|
628
|
+
if (!a)
|
|
629
|
+
continue;
|
|
630
|
+
const seed = byAnchor.get(a.anchor) ?? { anchor: a.anchor, segment: a.segment, fileIds: new Set() };
|
|
631
|
+
seed.fileIds.add(fileId);
|
|
632
|
+
byAnchor.set(a.anchor, seed);
|
|
633
|
+
}
|
|
634
|
+
const taken = new Set();
|
|
635
|
+
const ordered = [...byAnchor.values()].sort((x, y) => (x.anchor < y.anchor ? -1 : x.anchor > y.anchor ? 1 : 0));
|
|
636
|
+
const groups = [];
|
|
637
|
+
for (const seed of ordered) {
|
|
638
|
+
const seg = seed.segment.toLowerCase();
|
|
639
|
+
const label = seg === 'graphql' || seg === 'gql' ? 'GraphQL' : humanize(seed.segment);
|
|
640
|
+
const baseSlug = slugify(seed.segment) || 'graphql';
|
|
641
|
+
let id = baseSlug;
|
|
642
|
+
if (taken.has(id))
|
|
643
|
+
id = `${baseSlug}-${dirSegment(seed.anchor)}`;
|
|
644
|
+
let n = 2;
|
|
645
|
+
while (taken.has(id))
|
|
646
|
+
id = `${baseSlug}-${n++}`;
|
|
647
|
+
taken.add(id);
|
|
648
|
+
groups.push({ id, label, fileIds: [...seed.fileIds].sort() });
|
|
649
|
+
}
|
|
650
|
+
return groups.sort((a, b) => (a.id < b.id ? -1 : a.id > b.id ? 1 : 0));
|
|
651
|
+
}
|
|
652
|
+
function getAnalysis(ctx) {
|
|
653
|
+
let a = ANALYSIS_CACHE.get(ctx);
|
|
654
|
+
if (!a) {
|
|
655
|
+
a = analyzeGraphql(ctx);
|
|
656
|
+
ANALYSIS_CACHE.set(ctx, a);
|
|
657
|
+
}
|
|
658
|
+
return a;
|
|
659
|
+
}
|
|
660
|
+
// ---------------------------------------------------------------------------
|
|
661
|
+
// The adapter.
|
|
662
|
+
export const graphqlAdapter = {
|
|
663
|
+
name: 'graphql',
|
|
664
|
+
async detect(ctx) {
|
|
665
|
+
const { base, rootPath } = resolveBase(ctx);
|
|
666
|
+
// Root scan first (a repo whose manifest is at root reports rootPath '').
|
|
667
|
+
const rootMatch = scoreGraphql(gatherGraphqlSignals(base), rootPath);
|
|
668
|
+
if (rootMatch)
|
|
669
|
+
return rootMatch;
|
|
670
|
+
// A GraphQL backend often lives one dir down (a `backend/` | `server/` | `api/`
|
|
671
|
+
// package in a frontend+backend monorepo). Shallow-scan immediate subdirs for a
|
|
672
|
+
// strawberry/graphene manifest and scope to it. Only when NOT already scoped to
|
|
673
|
+
// a workspace package (the path).
|
|
674
|
+
if (!ctx.packageDir) {
|
|
675
|
+
for (const sub of shallowManifestSubdirs(base)) {
|
|
676
|
+
const m = scoreGraphql(gatherGraphqlSignals(join(base, sub)), sub);
|
|
677
|
+
if (m)
|
|
678
|
+
return m;
|
|
679
|
+
}
|
|
680
|
+
}
|
|
681
|
+
return null;
|
|
682
|
+
},
|
|
683
|
+
// One grouping prior per dedicated GraphQL directory → its own subsystem,
|
|
684
|
+
// authoritative over directory grouping (the fastapi/flask/nest mechanism).
|
|
685
|
+
// Fully deterministic (dir-derived) → no classificationsNeeded.
|
|
686
|
+
async groupingPrior(ctx) {
|
|
687
|
+
return { groups: getAnalysis(ctx).groups };
|
|
688
|
+
},
|
|
689
|
+
// resolver → returned type, graphene field → type, root field → cross-file
|
|
690
|
+
// resolver, Schema(...) → root type files (all kind 'calls'). File-id endpoints;
|
|
691
|
+
// the step resolves to modules, drops self-edges, dedupes, 8-verb-validates.
|
|
692
|
+
async syntheticEdges(ctx) {
|
|
693
|
+
return getAnalysis(ctx).edges;
|
|
694
|
+
},
|
|
695
|
+
// Query/Mutation/Subscription roots + their resolvers → gateway; every other
|
|
696
|
+
// object/input/interface type → service. METADATA; the module's `kind` is
|
|
697
|
+
// unchanged.
|
|
698
|
+
async roleTags(ctx) {
|
|
699
|
+
return getAnalysis(ctx).roles;
|
|
700
|
+
},
|
|
701
|
+
// The hooks READ SOURCE (Python). Declare the paths the diff-driven hosted walk
|
|
702
|
+
// must treat as framework-relevant. Never-store-source holds: parse server-side,
|
|
703
|
+
// persist only the derived groups/edges/roles.
|
|
704
|
+
scansSourcePath(path) {
|
|
705
|
+
return path.endsWith('.py') || path.endsWith('.pyi');
|
|
706
|
+
},
|
|
707
|
+
};
|
|
708
|
+
//# sourceMappingURL=graphql.js.map
|