Azure / azure-functions-core-tools

Command line tools for Azure Functions
MIT License
1.31k stars 434 forks source link

"No job functions found" - model v4 - TypeScript - Not recognizing existing functions #3508

Closed ItWasEnder closed 11 months ago

ItWasEnder commented 11 months ago

Hello all. I have set up a function app using v4 for nodeJS (TypeScript) and everything has worked fine until today when I was setting up some new environments. I am currently unable to run my v4 functions for some reason. I have tried following the v4 upgrade guide to try and diagnose the issue. In short, this did not work.

I have tried running npm run start as well as building the project and then func start however each mother yields the same result.

Here is the console error that is given with either command:

PS C:\Azure\myapp> func start
Can't determine project language from files. Please use one of [--csharp, --javascript, --typescript, --java, --python, --powershell, --custom]
Can't determine project language from files. Please use one of [--csharp, --javascript, --typescript, --java, --python, --powershell, --custom]
Can't determine project language from files. Please use one of [--csharp, --javascript, --typescript, --java, --python, --powershell, --custom]

Azure Functions Core Tools
Core Tools Version:       4.0.5441 Commit hash: N/A  (64-bit)
Function Runtime Version: 4.25.3.21264

Can't determine project language from files. Please use one of [--csharp, --javascript, --typescript, --java, --python, --powershell, --custom]
Can't determine project language from files. Please use one of [--csharp, --javascript, --typescript, --java, --python, --powershell, --custom]
Can't determine project language from files. Please use one of [--csharp, --javascript, --typescript, --java, --python, --powershell, --custom]
[2023-10-30T02:41:44.001Z] No job functions found. Try making your job classes and methods public. If you're using binding extensions (e.g. Azure Storage, ServiceBus, Timers, etc.) make sure you've called the registration method for the extension(s) in your startup code (e.g. builder.AddAzureStorage(), builder.AddServiceBus(), builder.AddTimers(), etc.).
For detailed output, run func with --verbose flag.

And below is my function.

import { app, HttpRequest, HttpResponseInit, InvocationContext } from "@azure/functions";
import Stripe from 'stripe';
import { CosmosClient, ItemResponse } from "@azure/cosmos";
import { Account } from "../../Shared/account";
import { DefaultAzureCredential } from "@azure/identity";

const databaseId: string = process.env['COSMOS_DATABASE'];

export async function CompleteOrder(req: HttpRequest, context: InvocationContext): Promise<HttpResponseInit> {
    const payload: string = await req.text();
    const sig = req.headers.get('stripe-signature');

    if (typeof sig !== 'string') {
        return {
            status: 400,
            body: `Webhook Error: Missing signature`
        }
    }

    const stripe = new Stripe(process.env.STRIPE_SECRET_KEY!, {
        apiVersion: '2023-08-16',
        typescript: true
    });

    let event;
    try {
        event = stripe.webhooks.constructEvent(payload, sig, process.env.STRIPE_WEBHOOK_SECRET!);
    } catch (err) {
        return {
            status: 400,
            body: `Webhook Error: ${(err as any).message}`
        }
    }

    if (event.type === 'checkout.session.completed') {
        context.log('-- CHECKOUT COMPLETED WEBHOOK --');

        try {
            const session: Stripe.Checkout.Session = event.data.object;
            const client = new CosmosClient(getCredentials());
            const userId: string = session.client_reference_id!;

            context.info(`[READ] Fetching account info >> (userId=${userId})`);

            const accResponse: ItemResponse<Account> = await client
                .database(databaseId)
                .container('Users')
                .item(userId, userId)
                .read<Account>();

            context.trace(`[COST] ${accResponse.requestCharge} RU | Activity Id: ${accResponse.activityId}`);

            if (accResponse.statusCode === 200) {
                const account = accResponse.resource as any;
                const { id, invoice, current_period_end } = account.subscription || {};

                if (current_period_end && current_period_end < Date.now()) {
                    return {
                        status: 200,
                        body: `Subscription is still active under ${id} - ${invoice}, until ${new Date(current_period_end * 1000).toUTCString()}`
                    }
                }
            } else {
                context.error(`[FAILURE] Unable to find account >> (userId=${userId})`);
                return {
                    status: 404,
                    body: `Unable to find account for ${userId}`
                }
            }

            // Ask Stripe for the latest subscription info
            const subscription = await stripe.subscriptions.retrieve(session.subscription! as string);

            context

            const patchSpec: any = [
                {
                    "op": "set",
                    "path": "/subscription",
                    "value": {
                        "id": session.subscription,
                        "invoice": session.invoice,
                        "current_period_end": subscription.current_period_end,
                        "interval": subscription.items.data[0].price.recurring.interval,
                    }
                }
            ];

            context.info(`[UPDATE] Updating account >> (userId=${userId})`);

            const item: ItemResponse<Account> = await client
                .database(databaseId)
                .container('Users')
                .item(userId, userId)
                .patch<Account>(patchSpec);

            context.trace(`[COST] ${item.requestCharge} RU | Activity Id: ${item.activityId}`);

            return {
                status: 200,
                body: 'Hello from completeOrder'
            }
        } catch (err) {
            const { name, message, stack }: { name: string, message: string, stack?: string } = err;
            context.error(`[FAILURE] Unexpected error has occured -> ${name}: ${message}`);

            if (stack) {
                context.trace(stack);
            }

            return {
                status: 500,
                body: `${name}: ${message}`
            };
        }
    } else {
        return {
            status: 400,
            body: `Webhook Error: Unhandled event type ${event.type}`
        }
    }
}

app.http('CompleteOrder', {
    methods: ['POST', "GET"],
    authLevel: 'anonymous',
    route: 'orders/complete',
    handler: CompleteOrder
});

function getCredentials(): any {
    var conn = {
        endpoint: process.env['cosmosDB__accountEndpoint']
    }

    if (process.env['cosmosDB__credential']) {
        conn['key'] = process.env['cosmosDB__credential']
    } else {
        conn['aadCredentials'] = new DefaultAzureCredential();
    }

    return conn;
}

package.json

{
  "name": "myapp",
  "version": "1.0.0",
  "description": "",
  "scripts": {
    "build": "tsc",
    "watch": "tsc -w",
    "clean": "rimraf dist",
    "prestart": "npm run clean && npm run build",
    "start": "func start",
    "test": "echo \"No tests yet...\""
  },
  "dependencies": {
    "@azure/cosmos": "^4.0.0",
    "@azure/functions": "^4.0.0",
    "@azure/identity": "^3.3.1",
    "@azure/storage-blob": "^12.16.0",
    "crypto": "^1.0.1",
    "joi": "^17.11.0",
    "stripe": "^13.10.0"
  },
  "devDependencies": {
    "@types/node": "^18.x",
    "rimraf": "^5.0.0",
    "typescript": "^4.0.0"
  },
  "main": "dist/src/functions/*.js"
}
ejizba commented 11 months ago

Hi @EnderGamingFilms can you make sure you have a local.settings.json file with FUNCTIONS_WORKER_RUNTIME set to node

ItWasEnder commented 11 months ago

I can't believe I didn't notice that file was missing. Completely my bad 🤝

johnnyreilly commented 8 months ago

For anyone searching the internet struggling - in my case I had to update my host.json to this:

{
    "version": "2.0",
    "logging": {
        "applicationInsights": {
            "samplingSettings": {
                "isEnabled": true,
                "excludedTypes": "Request"
            }
        }
    },
    "extensionBundle": {
        "id": "Microsoft.Azure.Functions.ExtensionBundle",
        "version": "[4.*, 5.0.0)"
    }
}

https://johnnyreilly.com/migrating-azure-functions-node-js-v4-typescript#3-running-locally

ejizba commented 8 months ago

Hi @johnnyreilly cool blog post! Always nice to see real-life scenarios, as opposed to our official docs which are a bit more generic.

In terms of the host.json, I don't think you should need to update it to get local debugging to work. It depends what your host.json was before and what types of triggers you have in your app. If you want, feel free to file a new issue in the node.js repo and we can follow up: https://github.com/Azure/azure-functions-nodejs-library/issues/new

johnnyreilly commented 8 months ago

Hey @ejizba!

I'll try and reverse my change tomorrow and see if that does reliably break local debugging - if it does then I'll report back!

johnnyreilly commented 8 months ago

So it's the weirdest thing. I spent literally hours being unable to run locally. I changed the host.json as suggested in https://johnnyreilly.com/migrating-azure-functions-node-js-v4-typescript#3-running-locally and I was able to run locally. I just tried changing it back locally and it still runs locally?! I cannot explain this!

I've updated the blog post to reference this discussion BTW

ejizba commented 8 months ago

Haha I've been there. Just let us know if you ever get a repro of your old issue

johnnyreilly commented 8 months ago

Will do!