Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

🚀 Feature: Local functions server #5425

Closed
2 tasks done
byawitz opened this issue Apr 24, 2023 · 9 comments
Closed
2 tasks done

🚀 Feature: Local functions server #5425

byawitz opened this issue Apr 24, 2023 · 9 comments
Labels
enhancement New feature or request product / functions Fixes and upgrades for the Appwrite Functions.

Comments

@byawitz
Copy link
Member

byawitz commented Apr 24, 2023

🔖 Feature description

An easy way to build and test functions locally.

Currently, when developing a function, it is necessary to deploy and upload it to the server to fully test it.

Although in the last step of a function development process, I'll probably deploy it and test it on the Appwrite instance a few times, It would be much easier to try it locally.

What I thought could be a good solution is to add a local server script to each one of the function-starter templates.

For example, in the Node template, we can add a file named local-server.js:

├── node-18.0
│   ├── src
│   │   ├── index.js
│   ├── .gitignore
│   ├── package.json
│   ├── README.md
│   ├── local-server.js  !New file

Then this could be the format of package.json:

{
  "name"        : "appwrite-function",
  "version"     : "1.0.0",
  "description" : "",
  "type"        : "commonjs",
  "main"        : "src/index.js",
  "scripts"     : {
    "dev" : "node local-server.js",
    "test": "echo \"Error: no test specified\" && exit 1"
  },
  "keywords"    : [],
  "author"      : "",
  "license"     : "ISC",
  "dependencies": {
    "node-appwrite": "^8.0.0"
  }
}

And this would be the code of local-server.js (a suggestion):

const func = require('./src/index.js');

const req = {
	headers  : {},
	payload  : {},
	variables: {
		APPWRITE_FUNCTION_ID             : '',
		APPWRITE_FUNCTION_NAME           : '',
		APPWRITE_FUNCTION_DEPLOYMENT     : '',
		APPWRITE_FUNCTION_TRIGGER        : '',
		APPWRITE_FUNCTION_RUNTIME_NAME   : '',
		APPWRITE_FUNCTION_RUNTIME_VERSION: '',
		APPWRITE_FUNCTION_EVENT          : '',
		APPWRITE_FUNCTION_EVENT_DATA     : '',
		APPWRITE_FUNCTION_DATA           : '',
		APPWRITE_FUNCTION_PROJECT_ID     : '',
		APPWRITE_FUNCTION_USER_ID        : '',
		APPWRITE_FUNCTION_JWT            : '',
	},
};

const res = {
	send(text, status) {
		console.log(text, status);
	},
	json(obj, status) {
		console.log(obj, status);
	},
};


func(req, res).then();

Then it is just a matter of running the local-server.js file in one of these ways.

#: npm run dev
#: node local-server.js

Being able to quickly mock any of the Appwrite variables in a local environment sounds like something that can make the development process of functions much fast.

P.s. in the future it's also open the option to support HMR whenever possible.

🎤 Pitch

A lot of times, the process of developing a function can be one time task.

But, from my experience, it usually requires many test execution. Using the previous solution will make it much easier to develop any function in almost no time.

👀 Have you spent some time to check if this issue has been raised before?

  • I checked and didn't find similar issue

🏢 Have you read the Code of Conduct?

@joeyouss joeyouss self-assigned this Apr 25, 2023
@joeyouss
Copy link

Hi @byawitz - thank you for opening this. We do realise that this method of testing a function has scope of improvement. Leaving this issue open for now to gather community feedback and suggestions.

@byawitz
Copy link
Member Author

byawitz commented Jun 21, 2023

You can look here for a real-use example.

@mc-stephen
Copy link

🔖 Feature description

An easy way to build and test functions locally.

Currently, when developing a function, it is necessary to deploy and upload it to the server to fully test it.

Although in the last step of a function development process, I'll probably deploy it and test it on the Appwrite instance a few times, It would be much easier to try it locally.

What I thought could be a good solution is to add a local server script to each one of the function-starter templates.

For example, in the Node template, we can add a file named local-server.js:

├── node-18.0
│   ├── src
│   │   ├── index.js
│   ├── .gitignore
│   ├── package.json
│   ├── README.md
│   ├── local-server.js  !New file

Then this could be the format of package.json:

{
  "name"        : "appwrite-function",
  "version"     : "1.0.0",
  "description" : "",
  "type"        : "commonjs",
  "main"        : "src/index.js",
  "scripts"     : {
    "dev" : "node local-server.js",
    "test": "echo \"Error: no test specified\" && exit 1"
  },
  "keywords"    : [],
  "author"      : "",
  "license"     : "ISC",
  "dependencies": {
    "node-appwrite": "^8.0.0"
  }
}

And this would be the code of local-server.js (a suggestion):

const func = require('./src/index.js');

const req = {
	headers  : {},
	payload  : {},
	variables: {
		APPWRITE_FUNCTION_ID             : '',
		APPWRITE_FUNCTION_NAME           : '',
		APPWRITE_FUNCTION_DEPLOYMENT     : '',
		APPWRITE_FUNCTION_TRIGGER        : '',
		APPWRITE_FUNCTION_RUNTIME_NAME   : '',
		APPWRITE_FUNCTION_RUNTIME_VERSION: '',
		APPWRITE_FUNCTION_EVENT          : '',
		APPWRITE_FUNCTION_EVENT_DATA     : '',
		APPWRITE_FUNCTION_DATA           : '',
		APPWRITE_FUNCTION_PROJECT_ID     : '',
		APPWRITE_FUNCTION_USER_ID        : '',
		APPWRITE_FUNCTION_JWT            : '',
	},
};

const res = {
	send(text, status) {
		console.log(text, status);
	},
	json(obj, status) {
		console.log(obj, status);
	},
};


func(req, res).then();

Then it is just a matter of running the local-server.js file in one of these ways.

#: npm run dev
#: node local-server.js

Being able to quickly mock any of the Appwrite variables in a local environment sounds like something that can make the development process of functions much fast.

P.s. in the future it's also open the option to support HMR whenever possible.

🎤 Pitch

A lot of times, the process of developing a function can be one time task.

But, from my experience, it usually requires many test execution. Using the previous solution will make it much easier to develop any function in almost no time.

👀 Have you spent some time to check if this issue has been raised before?

  • I checked and didn't find similar issue

🏢 Have you read the Code of Conduct?

Hello @byawitz
I just wanna say a very big thank you for this idea of testing locally before pushing to the appwrite console,
pushing to the appwrite console every time i want to test my code in case of any errors is kinda really tireing and not pleasing at all, i have been looking for a fix, but after encounting this post from you, things have been going smothly when it comes to testing, thanks once again for this greate idea.
💓💓💓💯💯💯

@byawitz
Copy link
Member Author

byawitz commented Sep 20, 2023

@Emeka212 Thanks

@harshsinghvi
Copy link

harshsinghvi commented Sep 22, 2023

Thanks for the workaround, I have modified it a little for handling multiple functions and variables for them separately

Instructions

  • create a function using appwrite init function
  • place local-server.js in the root of your folder
  • If needed create a file ./function/<function-name>/req.js to over req object in the local-server.js file to store variables for each function
  • add "aw-function": "node local-server.js", script in your package.json file

Usage

  • node local-server.js ./function/<function-name>
  • or yarn run aw-function ./function/<function-name>
  • or yarn run aw-function <function-name>

You can specify the function name in the command or specify the complete function path

./functions
├── clean-up
│   ├── README.md
│   ├── package.json
│   ├── req.js
│   └── src
│       └── index.js
└── device-crud
    ├── README.md
    ├── package.json
    └── src
        └── index.js
if (process.argv.length < 2) {
  process.exit();
}

const fs = require('fs');
const dotenv = require("dotenv")

let path = process.argv[2].split('/');

if (path[path.length - 1] === 'index.js') path = path.join('/');
if (path[path.length - 1] === 'src') path = path.join('/');
if (path.length > 1) path = `${path.join('/')}`;
if (path.length === 1) path = `./functions/${path}/src`;

// eslint-disable-next-line import/no-dynamic-require
const func = require(path);

// eslint-disable-next-line import/no-dynamic-require
const customReq = fs.existsSync(`${path}/../req.js`) ? require(`${path}/../req.js`) : {};

const req = {
  headers: {},
  payload: {},
  variables: {
                ...process.env, // add env vars
                APPWRITE_FUNCTION_ID             : '',
		APPWRITE_FUNCTION_NAME           : '',
		APPWRITE_FUNCTION_DEPLOYMENT     : '',
		APPWRITE_FUNCTION_TRIGGER        : '',
		APPWRITE_FUNCTION_RUNTIME_NAME   : '',
		APPWRITE_FUNCTION_RUNTIME_VERSION: '',
		APPWRITE_FUNCTION_EVENT          : '',
		APPWRITE_FUNCTION_EVENT_DATA     : '',
		APPWRITE_FUNCTION_DATA           : '',
		APPWRITE_FUNCTION_PROJECT_ID     : '',
		APPWRITE_FUNCTION_USER_ID        : '',
		APPWRITE_FUNCTION_JWT            : '',
  },
  ...customReq, // override req
};

const res = {
  send(text, status) {
    console.log(text, status);
  },
  json(obj, status) {
    console.log(obj, status);
  },
};

func(req, res).then();

UPDATE: New Functions

const main = async () => {
  if (process.argv.length < 3) {
    console.log('Invalid Usage');
    return;
  }

  let path = process.argv[2].split('/');

  if (path[path.length - 1] === 'index.js') path = path.join('/');
  if (path[path.length - 1] === 'src') path = path.join('/');
  if (path.length > 1) path = `${path.join('/')}`;
  if (path.length === 1) path = `./functions/${path}/src`;

  // eslint-disable-next-line global-require
  require('dotenv').config({ path: `${path}/../.env` });

  // eslint-disable-next-line import/no-dynamic-require
  const func = await import(`${path}/main.js`);

  const req = {
    method: 'POST',
    body: {
      name: 'test',
      status: 'unregistered',
      owner: null,
      model: null,
      delete: false,
      battery: null,
      carrierName: null,
      signalStrength: null,
      lastSeen: null,
      $id: '',
      $permissions: [],
      $createdAt: '2023-09-22T20:34:32.293+00:00',
      $updatedAt: '2023-09-22T20:34:32.293+00:00',
      $databaseId: '',
      $collectionId: '',
    },
    headers: {
      host: ':3000',
      'user-agent': 'Appwrite/1.4.3',
      'x-appwrite-trigger': 'event',
      'x-appwrite-event': 'databases.sms-api.*.*.documents.*.create',
      'x-appwrite-user-id': '',
      'content-type': 'application/x-www-form-urlencoded',
      connection: 'keep-alive',
      'content-length': '339',
    },
  };

  const res = {
    send(text, status) {
      console.log('[SEND] ', text, status);
    },
    json(obj, status) {
      console.log('[JSON] ', obj, status);
    },
  };

  const log = (...args) => console.log('[LOG] ', ...args);
  const error = (...args) => console.error('[ERR] ', ...args);

  func.default({ req, res, log, error }).then();
};

main();

@joeyouss joeyouss removed their assignment Sep 22, 2023
@harshsinghvi
Copy link

harshsinghvi commented Sep 22, 2023

@byawitz I spent some time to make this functionality work, and they updated the functions !! 😂😂

https://appwrite.io/docs/functions-develop#upgrade

@byawitz
Copy link
Member Author

byawitz commented Sep 22, 2023

@harshsinghvi Oops, Yes they just did, and also in the cloud.
But I think it will be fast to adjust it to the new Context

@harshsinghvi
Copy link

harshsinghvi commented Sep 22, 2023

I followed the mentioned guidelines, but it still doesn't work. The functions-starter is not updated with the new syntax

Screenshot 2023-09-23 at 1 25 13 AM

EDIT: They have mentioned in the guide to use import/exports but it throws errors

/usr/code-start/src/index.js:1
import { Client, Databases, Permission, Role } from 'node-appwrite';
^^^^^^

SyntaxError: Cannot use import statement outside a module
    at Object.compileFunction (node:vm:352:18)
    at wrapSafe (node:internal/modules/cjs/loader:1033:15)
    at Module._compile (node:internal/modules/cjs/loader:1069:27)
    at Module._extensions..js (node:internal/modules/cjs/loader:1159:10)
    at Module.load (node:internal/modules/cjs/loader:981:32)
    at Module._load (node:internal/modules/cjs/loader:827:12)
    at Module.require (node:internal/modules/cjs/loader:1005:19)
    at Hook._require.Module.require (/usr/local/lib/node_modules/pm2/node_modules/require-in-the-middle/index.js:101:39)
    at require (node:internal/modules/cjs/helpers:102:18)
    at /usr/local/src/server.js:55:28

Screenshot 2023-09-23 at 1 29 18 AM

@harshsinghvi Oops, Yes they just did, and also in the cloud. But I think it will be fast to adjust it to the new Context

@harshsinghvi
Copy link

Finally, my code works, there was an issue with req.body It was supposed to be an Object but it was a String, so it has to be parsed manually,

new local-server.js with env support

const main = async () => {
  if (process.argv.length < 3) {
    console.log('Invalid Usage');
    return;
  }

  let path = process.argv[2].split('/');

  if (path[path.length - 1] === 'index.js') path = path.join('/');
  if (path[path.length - 1] === 'src') path = path.join('/');
  if (path.length > 1) path = `${path.join('/')}`;
  if (path.length === 1) path = `./functions/${path}/src`;

  // eslint-disable-next-line global-require
  require('dotenv').config({ path: `${path}/../.env` });

  // eslint-disable-next-line import/no-dynamic-require
  const func = await import(`${path}/main.js`);

  const req = {
    method: 'POST',
    body: {
      name: 'test',
      status: 'unregistered',
      owner: null,
      model: null,
      delete: false,
      battery: null,
      carrierName: null,
      signalStrength: null,
      lastSeen: null,
      $id: '',
      $permissions: [],
      $createdAt: '2023-09-22T20:34:32.293+00:00',
      $updatedAt: '2023-09-22T20:34:32.293+00:00',
      $databaseId: '',
      $collectionId: '',
    },
    headers: {
      host: ':3000',
      'user-agent': 'Appwrite/1.4.3',
      'x-appwrite-trigger': 'event',
      'x-appwrite-event': 'databases.sms-api.*.*.documents.*.create',
      'x-appwrite-user-id': '',
      'content-type': 'application/x-www-form-urlencoded',
      connection: 'keep-alive',
      'content-length': '339',
    },
  };

  const res = {
    send(text, status) {
      console.log('[SEND] ', text, status);
    },
    json(obj, status) {
      console.log('[JSON] ', obj, status);
    },
  };

  const log = (...args) => console.log('[LOG] ', ...args);
  const error = (...args) => console.error('[ERR] ', ...args);

  func.default({ req, res, log, error }).then();
};

main();

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment
Labels
enhancement New feature or request product / functions Fixes and upgrades for the Appwrite Functions.
Projects
Status: Done
Development

No branches or pull requests

5 participants