diff --git a/README.md b/README.md index 26b2838c46..d4cfaa54c5 100644 --- a/README.md +++ b/README.md @@ -1,12 +1,12 @@ -![laf](https://socialify.git.ci/labring/laf/image?description=1&descriptionEditable=%E5%83%8F%E5%86%99%E5%8D%9A%E5%AE%A2%E4%B8%80%E6%A0%B7%E5%86%99%E4%BB%A3%E7%A0%81%EF%BC%81&font=Inter&forks=1&language=1&name=1&owner=1&pattern=Circuit%20Board&stargazers=1&theme=Dark) +![laf](https://socialify.git.ci/labring/laf/image?description=1&descriptionEditable=Write%20code%20like%20writing%20a%20blog!&font=Inter&forks=1&language=1&name=1&owner=1&pattern=Circuit%20Board&stargazers=1&theme=Dark)

- 像写博客一样写函数! + Write code like writing a blog!

- + [![Awesome](https://cdn.rawgit.com/sindresorhus/awesome/d7305f38d29fed78fa85652e3a63e154dd8e8829/media/badge.svg)](https://github.com/labring/laf) [![](https://img.shields.io/docker/pulls/lafyun/system-server)](https://hub.docker.com/r/lafyun/system-server) ![TypeScript](https://img.shields.io/badge/typescript-%23007ACC.svg?logo=typescript&logoColor=white) @@ -18,81 +18,141 @@ --- -> [English](README_en.md) | 中文 +> English | [中文](README_cn.md) + +## 👀 What is `laf` + +- laf is a cloud-native development platform +- laf is an open-source BaaS platform (Backend as a Service) +- laf is an out-of-the-box serverless platform. +- laf is an one-stop-shop for all your cloud-native development needs: + - Cloud Function + - Cloud Datastore + - Cloud Storage + - API Gateway + - _And MORE!_ +- laf can be the open-source alternative to **Firebase**. +- laf can be the self-hosted and automatically configured alternative to **AWS** with all its cloud native capabilities and so much more! + +[`laf`](https://github.com/labring/laf) provides **teams of any sizes** with a single, unified cloud-native development platform **at any time** with almost **zero cost**! + +## 🎉 What does `laf` provide + +- **Application Management** + - deploy/start/stop your application within seconds. There's no need to configure anything! +- **Cloud Function** + - run your code in the cloud zero extra cost. +- **Cloud Database** + - out-of-the-box DB service for your applications. +- **Cloud Storage** + - easy-to-use storage service that are **compatible with AWS S3** and more. +- **WebIDE** + - a cloud-native IDE for your code with code-linting, formatting and auto completion. +- **Static Site Hosting** + - only one click to deploy your static sites, no more **Nginx** configuration! +- **Client DB** + - Supports **"direct" access** from front-end client to your cloud database through [laf-client-sdk](https://github.com/labring/laf/tree/main/packages/client-sdk) with fine-grained access control. + - Speed up your development and **no more naive CRUD**! +- **WebSocket** + - Built-in support for WebSocket, everything you need is included! -## 🚀 Quick Start +![](https://sif268-laf-image.oss.laf.dev/dev.png) -[三分钟体验使用 laf 写一个自己的 ChatGPT (开发到上线)](https://icloudnative.io/posts/build-chatgpt-web-using-laf/) -[三分钟体验使用 laf 开发一个简单的 「Todo List」 ](./docs/guide/quick-start/Todo.md) +## 👨‍💻 Who is `laf` for? -## 🖥 在线体验 +1. **Front-end Developer** + `laf` = Full Stack Developer + - `laf` provided [laf-client-sdk](https://github.com/labring/laf/tree/main/packages/client-sdk) for front-end which can be used in any JS runtime. + - `laf` cloud function are developed using JS/TS, no need to learn any other languages. + - `laf` provides static site hosting in one click, no more worries about server config, nginx, domain name, etc. + - `laf` will provides more SDK in the future (Flutter/Android/iOS) to give you a unified experience on any platforms. +2. **Front-end Developer**, free you from all the trivia and configs, focus on the code itself! -🎉 [laf.run](https://laf.run) 可免费体验 `laf` 云开发。(国内版) + - `laf` saves you from tedious server admin/operation works. + - `laf` saves you from boring `nginx` configs. + - `laf` saves you from the hassle of manual DB deployment, security config. + - `laf` saves you from the torment of [10 min coding, 10 hour deploying]. + - `laf` lets you inspect logs in the browser in any places at any time. No more SSH to the server! + - `laf` lets you [write functions like blog], just code and click to deploy! -🎉 [laf.dev](https://laf.dev) 可免费体验 `laf` 云开发。(海外版) +3. **Cloud Native Developer**, get a more powerful, user-friendly and flexiable platform. No more contraints from **AWS** or **GCP**! -## 👀 `laf` 是什么 + - You can provide full source code to your clients which enables them to deploy the application in **any** environment. + - You can modify/customize your cloud platform, `laf` is open-sourced and built with customization in mind. -laf 是开源的云开发平台,提供云函数、云数据库、云存储等开箱即用的应用资源。让开发者专注于业务开发,无需折腾服务器,快速释放创意。 +4. **Node.js Developer**,`laf` is developed using `Node.js`, you can treat it as another **Node.js** framework/platform. + - You can write/debug/deploy your cloud functions in the browser with minimal effort. + - You can inspect/search logs with no configuration needed. + - No more hassle of DB/Storage/Nginx configuration, deploy your application at any time. + - Make any `Node.js` code cloud-native (a crawler, a automatic script, etc), write code like writing a blog! -## 🎉 `laf` 有什么 +5. **Individual Developer & Startup Team**, reduce cost and start fast! + - Reduce development time, shorten your product verfication cycle. + - Be agile and adpat to the changing market. + - Focus on your product, start fast and fail fast. + - One developer + `laf` = A whole team. -- 云函数 -- 云数据库 -- 云存储 -- WebIDE,像写博客一样写代码 -- 网站托管 -- WebSocket 支持 +> life is short, you need laf:) -![](https://sif268-laf-image.oss.laf.dev/dev.png) +## 💥 How can `laf` be used? -## 👨‍💻 谁适合使用 `laf` ? +> `laf` is a back-end development platform which can theoretically support any kinds of application! -1. 前端开发者 + `laf` = 全栈开发者,前端秒变全栈,成为真正的大前端 +1. Develop Android, iOS or Web Application: - - `laf` 为前端提供了 [laf-client-sdk](https://github.com/labring/laf/tree/main/packages/client-sdk),适用于任何 js 运行环境 - - `laf` 云函数使用 js/ts 开发,前后端代码无隔裂,无门槛快速上手 - - `laf` 提供了静态网站托管,可将前端构建的网页直接同步部署上来,无需再配置服务器、nginx、域名等 - - `laf` 后续会提供多种客户端的 SDK(Flutter/Android/iOS 等),为所有客户端开发者提供后端开发服务和一致的开发体验 + - Use Cloud Function/DB/Storage for your product. + - Deploy your admin front-end in `laf` with on click. + - User Cloud Function for payment, authentication, hot-update, etc. -2. 后端开发者,可以从琐事中解放出来,专注于业务本身,提升开发效率 +2. Deploy blog or homepage - - `laf` 可以节约服务器运维、多环境部署和管理精力 - - `laf` 让你告别配置、调试 nginx - - `laf` 让你告别「为每个项目手动部署数据库、安全顾虑等重复性工作」 - - `laf` 让你告别「修改一次、发布半天」的重复繁琐的迭代体验 - - `laf` 让你随时随地在 Web 上查看函数的运行日志,不必再连接服务器,费神费眼翻找 - - `laf` 让你「像写博客一样写一个函数」,招之即来,挥之即去,随手发布! + - For static blogs generated by vuepress/hexo/hugo, you can deploy them with `laf` in one click. (See [laf-cli](https://github.com/labring/laf-cli) for more info) + - Use Cloud Function to handle comments, likes, statistics, etc. + - Use Cloud Function to support features like online course/voting/survey/etc. + - Use Cloud Function for crawling/live-feed/etc. + - Use Cloud Storage for video/images/etc. -3. 云开发用户,若你是其它厂商的云开发用户,你不仅可以获得更强大、快速的开发体验,还不被云厂商锁定 +3. Enterprise Informatization: Deploy your own cloud platform. - - 你可以为客户提供源码交付,为客户私有部署一套 `laf` + 你的云开发应用,而使用闭源的云开发服务,无法交付可独立运行的源码 - - 你可以根据未来的需要,随时将自己的产品部署到自己的服务器上,`laf` 是开源免费的 - - 你甚至可以修改、订制自己的云开发平台,`laf` 是开源的、高度可扩展的 + - Fast development/deployment of any internal systems. + - Multi-tenancy/users/roles/apps support, segragate or connect different sectors/teams/apps. + - Take advantage of the `laf` community, use and customize existing applications, save your budget! + - Use free and open-source software, no litmitations, customize as you wish! -4. 独立开发者、创业团队, 节约成本,快速开始,专注业务 - - 减少启动项目开发的流程,快速启动,缩短产品验证周期 - - 极大程度提高迭代速度,随时应对变化,随时发布 - - 专注于产品业务本身,快速推出最小可用产品(MVP),快速进行产品、市场验证 - - 一个人 + `laf` = 团队 +4. Handy toolkit for Individual Developers -> life is short, you need laf:) + - `laf` makes any of your code cloud-native instantly. + - Just like writing a note in your memo, but also auto synced to cloud and accessible from anywhere. + - Make `laf` your notebook or "personal assitent", write a reminder app, email forwarding app, etc. +5. Other + - Some use `laf` as a cloud drive. + - Some use `laf` as a logging server for collection and analyzing data. + - Some use `laf` as a crawler for latest news, etc. + - Some use `laf` as a webhook for Github, Slack, Discord, etc. + - Some use `laf` as a chaos-monkey for other services. + - ... -## 🎉 Self-hosted Deployment +> In the future, `lafyun.com` will have a `application market` where users can publish sample applications/templates! -[Deployment](./deploy/README.md) +## 🖥 Try it now -## 🏘️ Community Groups +🎉 [lafyun.com](http://www.lafyun.com) is a `laf` service hosted by us, you can try `laf` for free! -- [Discord](https://discord.gg/uWZqAwwdvy) -- [微信群](https://oss.lafyun.com/wofnib-image/2022-04-22-14-21-MRJH9o.png) -- [QQ 群:603059673](https://jq.qq.com/?_wv=1027&k=DdRCCiuz) +Independent domain names and HTTPS licenses can be applied to your applications now, develop fast, and enjoy the freedom of `laf`! -## :point_right: Roadmap -- [**laf project roadmap**](https://github.com/orgs/labring/projects/5/views/1) +## 🚀 Quick Start + +[develop a [Todo List] feature within 3 minutes](./docs/guide/quick-start/Todo.md) + +## 🎉 Self-hosting +[self-hosting](./deploy/README.md) + +## 🏘️ Community + +- [Discord](https://discord.gg/uWZqAwwdvy) +- [Twitter](https://twitter.com/laf_dev) ## 🌟 Star History diff --git a/README_cn.md b/README_cn.md new file mode 100644 index 0000000000..8dabcce6bb --- /dev/null +++ b/README_cn.md @@ -0,0 +1,96 @@ +![laf](https://socialify.git.ci/labring/laf/image?description=1&descriptionEditable=%E5%83%8F%E5%86%99%E5%8D%9A%E5%AE%A2%E4%B8%80%E6%A0%B7%E5%86%99%E4%BB%A3%E7%A0%81%EF%BC%81&font=Inter&forks=1&language=1&name=1&owner=1&pattern=Circuit%20Board&stargazers=1&theme=Dark) + +

+

+ 像写博客一样写函数! +

+ +

+ + [![Awesome](https://cdn.rawgit.com/sindresorhus/awesome/d7305f38d29fed78fa85652e3a63e154dd8e8829/media/badge.svg)](https://github.com/labring/laf) + [![](https://img.shields.io/docker/pulls/lafyun/system-server)](https://hub.docker.com/r/lafyun/system-server) + ![TypeScript](https://img.shields.io/badge/typescript-%23007ACC.svg?logo=typescript&logoColor=white) + [![Website](https://img.shields.io/website?url=https%3A%2F%2Fdocs.lafyun.com&logo=Postwoman)](https://docs.lafyun.com/) + + +

+
+ +--- + +> 中文 | [English](README.md) + +## 🚀 Quick Start + +[三分钟体验使用 laf 写一个自己的 ChatGPT (开发到上线)](https://icloudnative.io/posts/build-chatgpt-web-using-laf/) +[三分钟体验使用 laf 开发一个简单的「Todo List」](./docs/guide/quick-start/Todo.md) + +## 🖥 在线体验 + +🎉 [laf.run](https://laf.run) 可免费体验 `laf` 云开发。(国内版) + +🎉 [laf.dev](https://laf.dev) 可免费体验 `laf` 云开发。(海外版) + +## 👀 `laf` 是什么 + +laf 是开源的云开发平台,提供云函数、云数据库、云存储等开箱即用的应用资源。让开发者专注于业务开发,无需折腾服务器,快速释放创意。 + +## 🎉 `laf` 有什么 + +- 云函数 +- 云数据库 +- 云存储 +- WebIDE,像写博客一样写代码 +- 网站托管 +- WebSocket 支持 + +![](https://sif268-laf-image.oss.laf.dev/dev.png) + +## 👨‍💻 谁适合使用 `laf` ? + +1. 前端开发者 + `laf` = 全栈开发者,前端秒变全栈,成为真正的大前端 + + - `laf` 为前端提供了 [laf-client-sdk](https://github.com/labring/laf/tree/main/packages/client-sdk),适用于任何 js 运行环境 + - `laf` 云函数使用 js/ts 开发,前后端代码无隔裂,无门槛快速上手 + - `laf` 提供了静态网站托管,可将前端构建的网页直接同步部署上来,无需再配置服务器、nginx、域名等 + - `laf` 后续会提供多种客户端的 SDK(Flutter/Android/iOS 等),为所有客户端开发者提供后端开发服务和一致的开发体验 + +2. 后端开发者,可以从琐事中解放出来,专注于业务本身,提升开发效率 + + - `laf` 可以节约服务器运维、多环境部署和管理精力 + - `laf` 让你告别配置、调试 nginx + - `laf` 让你告别「为每个项目手动部署数据库、安全顾虑等重复性工作」 + - `laf` 让你告别「修改一次、发布半天」的重复繁琐的迭代体验 + - `laf` 让你随时随地在 Web 上查看函数的运行日志,不必再连接服务器,费神费眼翻找 + - `laf` 让你「像写博客一样写一个函数」,招之即来,挥之即去,随手发布! + +3. 云开发用户,若你是其它厂商的云开发用户,你不仅可以获得更强大、快速的开发体验,还不被云厂商锁定 + + - 你可以为客户提供源码交付,为客户私有部署一套 `laf` + 你的云开发应用,而使用闭源的云开发服务,无法交付可独立运行的源码 + - 你可以根据未来的需要,随时将自己的产品部署到自己的服务器上,`laf` 是开源免费的 + - 你甚至可以修改、订制自己的云开发平台,`laf` 是开源的、高度可扩展的 + +4. 独立开发者、创业团队,节约成本,快速开始,专注业务 + - 减少启动项目开发的流程,快速启动,缩短产品验证周期 + - 极大程度提高迭代速度,随时应对变化,随时发布 + - 专注于产品业务本身,快速推出最小可用产品 (MVP),快速进行产品、市场验证 + - 一个人 + `laf` = 团队 + +> life is short, you need laf:) + +## 🎉 Self-hosted Deployment + +[Deployment](./deploy/README.md) + +## 🏘️ Community Groups + +- [微信群](https://w4mci7-images.oss.laf.run/wechat.png) +- [QQ 群:603059673](https://jq.qq.com/?_wv=1027&k=DdRCCiuz) + +## :point_right: Roadmap + +- [**laf project roadmap**](https://github.com/orgs/labring/projects/5/views/1) + +## 🌟 Star History + +[![Star History Chart](https://api.star-history.com/svg?repos=labring/laf&type=Date)](https://star-history.com/#labring/laf&Date) diff --git a/README_en.md b/README_en.md deleted file mode 100644 index 454ba33213..0000000000 --- a/README_en.md +++ /dev/null @@ -1,160 +0,0 @@ -![laf](https://socialify.git.ci/labring/laf/image?description=1&descriptionEditable=Write%20code%20like%20writing%20a%20blog!&font=Inter&forks=1&language=1&name=1&owner=1&pattern=Circuit%20Board&stargazers=1&theme=Dark) - -
-

- Write code like writing a blog! -

- -

- - [![Awesome](https://cdn.rawgit.com/sindresorhus/awesome/d7305f38d29fed78fa85652e3a63e154dd8e8829/media/badge.svg)](https://github.com/labring/laf) - [![](https://img.shields.io/docker/pulls/lafyun/system-server)](https://hub.docker.com/r/lafyun/system-server) - ![TypeScript](https://img.shields.io/badge/typescript-%23007ACC.svg?logo=typescript&logoColor=white) - [![Website](https://img.shields.io/website?url=https%3A%2F%2Fdocs.lafyun.com&logo=Postwoman)](https://docs.lafyun.com/) - - -

-
- ---- - -> English | [中文](README.md) - -## 👀 What is `laf` - -- laf is a cloud-native development platform -- laf is an open-source BaaS platform (Backend as a Service) -- laf is an out-of-the-box serverless platform. -- laf is an one-stop-shop for all your cloud-native development needs: - - Cloud Function - - Cloud Datastore - - Cloud Storage - - API Gateway - - _And MORE!_ -- laf can be the open-source alternative to **Firebase**. -- laf can be the self-hosted and automatically configured alternative to **AWS** with all its cloud native capabilities and so much more! - -[`laf`](https://github.com/labring/laf) provides **teams of any sizes** with a single, unified cloud-native development platform **at any time** with almost **zero cost**! - -## 🎉 What does `laf` provide - -- **Application Management** - - deploy/start/stop your application within seconds. There's no need to configure anything! -- **Cloud Function** - - run your code in the cloud zero extra cost. -- **Cloud Database** - - out-of-the-box DB service for your applications. -- **Cloud Storage** - - easy-to-use storage service that are **compatible with AWS S3** and more. -- **WebIDE** - - a cloud-native IDE for your code with code-linting, formatting and auto completion. -- **Static Site Hosting** - - only one click to deploy your static sites, no more **Nginx** configuration! -- **Client DB** - - Supports **"direct" access** from front-end client to your cloud database through [laf-client-sdk](https://github.com/labring/laf/tree/main/packages/client-sdk) with fine-grained access control. - - Speed up your development and **no more naive CRUD**! -- **WebSocket** - - Built-in support for WebSocket, everything you need is included! - -![](https://sif268-laf-image.oss.laf.dev/dev.png) - -## 👨‍💻 Who is `laf` for? - -1. **Front-end Developer** + `laf` = Full Stack Developer - - `laf` provided [laf-client-sdk](https://github.com/labring/laf/tree/main/packages/client-sdk) for front-end which can be used in any JS runtime. - - `laf` cloud function are developed using JS/TS, no need to learn any other languages. - - `laf` provides static site hosting in one click, no more worries about server config, nginx, domain name, etc. - - `laf` will provides more SDK in the future (Flutter/Android/iOS) to give you a unified experience on any platforms. -2. **Front-end Developer**, free you from all the trivia and configs, focus on the code itself! - - - `laf` saves you from tedious server admin/operation works. - - `laf` saves you from boring `nginx` configs. - - `laf` saves you from the hassle of manual DB deployment, security config. - - `laf` saves you from the torment of「10 min coding, 10 hour deploying」. - - `laf` lets you inspect logs in the browser in any places at any time. No more SSH to the server! - - `laf` lets you「write functions like blog」, just code and click to deploy! - -3. **Cloud Native Developer**, get a more powerful, user-friendly and flexiable platform. No more contraints from **AWS** or **GCP**! - - - You can provide full source code to your clients which enables them to deploy the application in **any** environment. - - You can modify/customize your cloud platform, `laf` is open-sourced and built with customization in mind. - -4. **Node.js Developer**,`laf` is developed using `Node.js`, you can treat it as another **Node.js** framework/platform. - - - You can write/debug/deploy your cloud functions in the browser with minimal effort. - - You can inspect/search logs with no configuration needed. - - No more hassle of DB/Storage/Nginx configuration, deploy your application at any time. - - Make any `Node.js` code cloud-native (a crawler, a automatic script, etc), write code like writing a blog! - -5. **Individual Developer & Startup Team**, reduce cost and start fast! - - Reduce development time, shorten your product verfication cycle. - - Be agile and adpat to the changing market. - - Focus on your product, start fast and fail fast. - - One developer + `laf` = A whole team. - -> life is short, you need laf:) - -## 💥 How can `laf` be used? - -> `laf` is a back-end development platform which can theoretically support any kinds of application! - -1. Develop Android, iOS or Web Application: - - - Use Cloud Function/DB/Storage for your product. - - Deploy your admin front-end in `laf` with on click. - - User Cloud Function for payment, authentication, hot-update, etc. - -2. Deploy blog or homepage - - - For static blogs generated by vuepress/hexo/hugo, you can deploy them with `laf` in one click. (See [laf-cli](https://github.com/labring/laf-cli) for more info) - - Use Cloud Function to handle comments, likes, statistics, etc. - - Use Cloud Function to support features like online course/voting/survey/etc. - - Use Cloud Function for crawling/live-feed/etc. - - Use Cloud Storage for video/images/etc. - -3. Enterprise Informatization: Deploy your own cloud platform. - - - Fast development/deployment of any internal systems. - - Multi-tenancy/users/roles/apps support, segragate or connect different sectors/teams/apps. - - Take advantage of the `laf` community, use and customize existing applications, save your budget! - - Use free and open-source software, no litmitations, customize as you wish! - -4. Handy toolkit for Individual Developers - - - `laf` makes any of your code cloud-native instantly. - - Just like writing a note in your memo, but also auto synced to cloud and accessible from anywhere. - - Make `laf` your notebook or "personal assitent", write a reminder app, email forwarding app, etc. - -5. Other - - Some use `laf` as a cloud drive. - - Some use `laf` as a logging server for collection and analyzing data. - - Some use `laf` as a crawler for latest news, etc. - - Some use `laf` as a webhook for Github, Slack, Discord, etc. - - Some use `laf` as a chaos-monkey for other services. - - ... - -> In the future, `lafyun.com` will have a `application market` where users can publish sample applications/templates! - -## 🖥 Try it now - -🎉 [lafyun.com](http://www.lafyun.com) is a `laf` service hosted by us, you can try `laf` for free! - -Independent domain names and HTTPS licenses can be applied to your applications now, develop fast, and enjoy the freedom of `laf`! - -## 🚀 Quick Start - -[develop a 「Todo List」 feature within 3 minutes](./docs/guide/quick-start/Todo.md) - -## 🎉 Self-hosting - -[self-hosting](./deploy/README.md) - -## 🏘️ Community - -- [WeChat Group](https://oss.lafyun.com/wofnib-image/2022-04-22-14-21-MRJH9o.png) -- [QQ Group: 603059673](https://jq.qq.com/?_wv=1027&k=DdRCCiuz) -- _More community resources will be added soon!_ - -## 🌟 Star History - -[![Star History Chart](https://api.star-history.com/svg?repos=labring/laf&type=Date)](https://star-history.com/#labring/laf&Date) diff --git a/docs/.vitepress/config.js b/docs/.vitepress/config.js index 6d1af7d232..a8144b03c6 100644 --- a/docs/.vitepress/config.js +++ b/docs/.vitepress/config.js @@ -1,5 +1,10 @@ import { defineConfig, DefaultTheme } from "vitepress"; import mdItCustomAttrs from "markdown-it-custom-attrs"; +import { + guideSiderbarConfig_en, + examplesSideBarConfig_en, + NavConfig_en, +} from "./en"; /** * @type {DefaultTheme.NavItem[]} @@ -322,57 +327,119 @@ const examplesSideBarConfig = [ ]; export default defineConfig({ - lang: "zh-CN", - title: "laf 云开发", - description: "laf 云开发,像写博客一样写函数,随手上线", - markdown: { - lineNumbers: true, - config: (md) => { - // use more markdown-it plugins! - md.use(mdItCustomAttrs, "image", { - "data-fancybox": "gallery", - }); - }, - }, - themeConfig: { - logo: "/logo.png", - repo: "labring/laf", - docsBranch: "main", - docsDir: "docs", - footer: { - message: "Apache License V2.0", - copyright: "Copyright © 2021-present labring/laf", - }, - editLink: { - pattern: "https://github.com/labring/laf/edit/main/docs/:path", - text: "在 GitHub 上编辑此页", - }, - lastUpdated: "更新于", - nav: NavConfig, - socialLinks: [ - { icon: "github", link: "https://github.com/labring/laf" }, - { icon: "discord", link: "https://discord.gg/KaxHF86CcF" }, - { icon: "twitter", link: "https://twitter.com/laf_dev" }, - ], - sidebar: { - "/guide/": guideSiderbarConfig, - "/3min/": examplesSideBarConfig, + locales: { + root: { + label: "简体中文", + lang: "zh-CN", + link: "/", + title: "laf 云开发", + description: "laf 云开发,像写博客一样写函数,随手上线", + markdown: { + lineNumbers: true, + config: (md) => { + // use more markdown-it plugins! + md.use(mdItCustomAttrs, "image", { + "data-fancybox": "gallery", + }); + }, + }, + themeConfig: { + logo: "/logo.png", + repo: "labring/laf", + docsBranch: "main", + docsDir: "docs", + footer: { + message: "Apache License V2.0", + copyright: "Copyright © 2021-present labring/laf", + }, + editLink: { + pattern: "https://github.com/labring/laf/edit/main/docs/:path", + text: "在 GitHub 上编辑此页", + }, + lastUpdated: "更新于", + nav: NavConfig, + socialLinks: [ + { icon: "github", link: "https://github.com/labring/laf" }, + { icon: "discord", link: "https://discord.gg/KaxHF86CcF" }, + { icon: "twitter", link: "https://twitter.com/laf_dev" }, + ], + sidebar: { + "/guide/": guideSiderbarConfig, + "/3min/": examplesSideBarConfig, + }, + }, + head: [ + ["link", { rel: "icon", href: "/logo.png" }], + [ + "link", + { + rel: "stylesheet", + href: "/fancybox.css", + }, + ], + [ + "script", + { + src: "/fancybox.umd.js", + }, + ], + ], }, - }, - head: [ - ["link", { rel: "icon", href: "/logo.png" }], - [ - "link", - { - rel: "stylesheet", - href: "/fancybox.css", + en: { + label: "English", + lang: "en", + link: "/en/", + title: "laf Cloud Development", + markdown: { + lineNumbers: true, + config: (md) => { + // use more markdown-it plugins! + md.use(mdItCustomAttrs, "image", { + "data-fancybox": "gallery", + }); + }, }, - ], - [ - "script", - { - src: "/fancybox.umd.js", + themeConfig: { + logo: "/logo.png", + repo: "labring/laf", + docsBranch: "main", + docsDir: "docs", + footer: { + message: "Apache License V2.0", + copyright: "Copyright © 2021-present labring/laf", + }, + editLink: { + pattern: "https://github.com/labring/laf/edit/main/docs/:path", + text: "Edited this page on github", + }, + lastUpdated: "Update time", + nav: NavConfig_en, + socialLinks: [ + { icon: "github", link: "https://github.com/labring/laf" }, + { icon: "discord", link: "https://discord.gg/KaxHF86CcF" }, + { icon: "twitter", link: "https://twitter.com/laf_dev" }, + ], + sidebar: { + "/en/guide/": guideSiderbarConfig_en, + "/en/3min/": examplesSideBarConfig_en, + }, }, - ], - ], + head: [ + ["link", { rel: "icon", href: "/logo.png" }], + [ + "link", + { + rel: "stylesheet", + href: "/fancybox.css", + }, + ], + [ + "script", + { + src: "/fancybox.umd.js", + }, + ], + ], + }, + }, }); diff --git a/docs/.vitepress/en.js b/docs/.vitepress/en.js new file mode 100644 index 0000000000..d15d367f36 --- /dev/null +++ b/docs/.vitepress/en.js @@ -0,0 +1,335 @@ +import { defineConfig, DefaultTheme } from "vitepress"; + +/** + * @type {DefaultTheme.NavItem[]} + */ +const NavConfig_en = [ + { text: "Home", link: "https://laf.dev" }, + { + text: "Development of guidelines", + activeMatch: "^/en/guide/", + items: [ + { + text: "Quick start", + items: [ + { + text: "Laf Cloud development is introduced", + link: "/en/guide/", + }, + { + text: "Web IDE", + link: "/en/guide/web-ide/", + }, + ], + }, + { + text: "Function", + items: [ + { + text: "Cloud Function", + link: "/en/guide/function/", + }, + { + text: "Cloud Database", + link: "/en/guide/db/", + }, + { + text: "Cloud Storage", + link: "/en/guide/oss/", + }, + ], + }, + { + text: "More", + items: [ + { + text: "Website hosting", + link: "/en/guide/website-hosting/", + }, + { + text: "Client SDK", + link: "/en/guide/client-sdk/", + }, + { + text: "Cli tool", + link: "/en/guide/cli/", + }, + { + text: "VSCode Local development", + link: "/en/guide/laf-assistant/", + }, + ], + }, + ], + }, + { + text: "3 minutes lab", + link: "/en/3min/", + }, + { + text: "Immediate use", + // target: "_self", + link: "https://laf.run/", + }, +]; + +/** + * @type {DefaultTheme.MultiSideBarConfig} + */ +const guideSiderbarConfig_en = [ + { + text: "introduce", + items: [ + { + text: "Overview", + link: "/en/guide/", + }, + { + text: "Web IDE Online development", + link: "/en/guide//web-ide/", + }, + ], + }, + { + text: "Quick start", + items: [ + { + text: "Three minute implementation [Todo List]", + link: "/en/guide/quick-start/Todo", + }, + { + text: "Three minute implementation [user login/registration]", + link: "/en/guide/quick-start/login", + }, + ], + }, + { + text: "Cloud Function", + collapsed: false, + items: [ + { + text: "Introduction to Cloud Function", + link: "/en/guide/function/", + }, + { + text: "Cloud Function Usage", + link: "/en/guide/function/use-function", + }, + { + text: "Cloud Function SDK", + collapsed: false, + items: [ + { + text: "Introduction of the SDK", + link: "/en/guide/function/function-sdk#frontmatter-title", + }, + { + text: "Import the SDK", + link: "/en/guide/function/function-sdk#importing-the-sdk", + }, + { + text: "Send the requests", + link: "/en/guide/function/function-sdk#sending-network-requests", + }, + { + text: "Database operation", + link: "/en/guide/function/function-sdk#working-with-databases", + }, + { + text: "Call other Cloud Function", + link: "/en/guide/function/function-sdk#invoking-other-cloud-functions", + }, + { + text: "Generating and Decrypting JWT Token", + link: "/en/guide/function/function-sdk#generating-and-decrypting-jwt-tokens", + }, + { + text: "Cloud Function global shared", + link: "/en/guide/function/function-sdk#cloud-function-global-cache", + }, + { + text: "Native MongoDriver instance", + link: "/en/guide/function/function-sdk#native-mongodriverobject-instance-in-cloud-functions", + }, + ], + }, + { + text: "Cloud Function called", + items: [ + { + text: "In the client calls", + link: "/en/guide/function/call-function-in-client", + }, + { + text: "In cloud function calls", + link: "/en/guide/function/call-function", + }, + { + text: "HTTP invocation", + link: "/en/guide/function/call-function-in-http", + }, + ], + }, + { + text: "Cloud Function log", + link: "/en/guide/function/logs", + }, + { + text: "Dependency management", + link: "/en/guide/function/depend", + }, + { + text: "Trigger", + link: "/en/guide/function/trigger", + }, + { + text: "Environment variable", + link: "/en/guide/function/env", + }, + { + text: "WebSocket connection", + link: "/en/guide/function/websocket", + }, + { + text: "Interceptor", + link: "/en/guide/function/interceptor", + }, + { + text: "Application of the initialization", + link: "/en/guide/function/init", + }, + { + text: "Cloud Function FAQ", + link: "/en/guide/function/faq", + }, + ], + }, + { + text: "Cloud Database", + collapsed: false, + items: [ + { text: "Introduction of the database", link: "/en/guide/db/" }, + { text: "Introduction to the database", link: "/en/guide/db/quickstart" }, + { text: "Add new data", link: "/en/guide/db/add" }, + { text: "Query data", link: "/en/guide/db/find" }, + { text: "Update data", link: "/en/guide/db/update" }, + { text: "Delete data", link: "/en/guide/db/del" }, + { text: "Database command", link: "/en/guide/db/command" }, + // { text: "数据库聚合操作", link: "/en/guide/db/aggregate" }, + // { text: "数据库运算", link: "/en/guide/db/operator" }, + // { + // text: "操作地理信息", + // link: "/guide/db/geo", + // }, + ], + }, + { + text: "Cloud Storage", + collapsed: false, + items: [ + { text: "Introduction of cloud storage", link: "/en/guide/oss/" }, + { + text: "Cloud storage cloud function call", + link: "/en/guide/oss/oss-by-function", + }, + { + text: "The front end through the cloud function to upload files", + link: "/en/guide/oss/upload-by-function", + }, + { + text: "Temporary token generated cloud storage (STS)", + link: "/en/guide/oss/get-sts", + }, + { + text: "Front end using STS tokens to upload files", + link: "/en/guide/oss/use-sts-in-client", + }, + ], + }, + { + text: "Static Website Hosting", + items: [{ text: "Rapid hosting", link: "/en/guide/website-hosting/" }], + }, + { + text: "laf-cli The command line tool", + items: [{ text: "laf-cli Directions for use", link: "/en/guide/cli/" }], + }, + { + text: "Client SDK", + items: [ + { + text: "laf-client-sdk Directions for use", + link: "/en/guide/client-sdk/", + }, + { text: "Database access strategy", link: "/en/guide/db/policy" }, + ], + }, + { + text: "VSCode Local development", + items: [{ text: "laf assistant", link: "/en/guide/laf-assistant/" }], + }, +]; + +/** + * @type {DefaultTheme.MultiSideBarConfig} + */ +const examplesSideBarConfig_en = [ + { + text: "3 minutes Lab", + items: [ + { + text: "Introduce", + link: "/en/3min/", + }, + ], + }, + { + text: "Access to WeChat", + items: [ + { + text: "Official WeChat account", + items: [ + { + text: "Server docking and text messages", + link: "/en/3min/wechat/MediaPlatform/ServerDocking.md", + }, + { + text: "Customize Menu", + link: "/en/3min/wechat/MediaPlatform/Menu.md", + }, + { + text: "H5 develop", + link: "/en/3min/wechat/MediaPlatform/H5.md", + }, + ], + }, + { + text: "WeChat mini program", + link: "/en/3min/wechat/MiniProgram/", + }, + { + text: "WeCom", + link: "/en/3min/wechat/CorporateWeChat/", + }, + ], + }, + { + text: "Accessing AI", + items: [ + { + text: "Accessing ChatGPT", + link: "/en/3min/chatgpt/", + }, + { + text: "Accessing Claude", + link: "/en/3min/claude/", + }, + { + text: "Accessing Midjourney", + link: "/en/3min/midjourney/", + }, + ], + }, +]; + +export { guideSiderbarConfig_en, examplesSideBarConfig_en, NavConfig_en }; diff --git a/docs/.vitepress/theme/index.js b/docs/.vitepress/theme/index.js index 0c7322625b..05a07ea7b2 100644 --- a/docs/.vitepress/theme/index.js +++ b/docs/.vitepress/theme/index.js @@ -1,10 +1,10 @@ -import DefaultTheme from 'vitepress/theme' -import './custom.css' +import DefaultTheme from "vitepress/theme"; +import "./custom.css"; export default { ...DefaultTheme, enhanceApp({ app }) { // register global components // app.component('MyGlobalComponent' /* ... */) - } -} + }, +}; diff --git a/docs/en/3min/chatgpt/index.md b/docs/en/3min/chatgpt/index.md new file mode 100644 index 0000000000..1a3bcecade --- /dev/null +++ b/docs/en/3min/chatgpt/index.md @@ -0,0 +1,94 @@ +--- +title: Accessing ChatGPT +--- + +# {{ $frontmatter.title }} + +## Choosing an Overseas Server + +Directly accessing the OpenAI API is not possible in China. + +`laf.dev` can be used to make direct requests. + +## Installing Dependencies + +`chatgpt` + +Refer to the dependency installation documentation for instructions on how to install the dependencies: [Dependency Management](/guide/function/depend) + +[Official Documentation for `chatgpt`](https://www.npmjs.com/package/chatgpt#usage---chatgptapi) + +## Invoking Cloud Functions + +```typescript +import cloud from '@lafjs/cloud' + +exports.main = async function (ctx: FunctionContext) { + + const stream = true // Enable streaming mode + const content = "Who are you and what can you do?" // Question + const systemMessage = `Regardless of the question, avoid using terms like ChatGPT, OpenAi, bot, AI, etc. Remember, you are developing an intelligent assistant for Laf` + const parentMessageId = ctx.query.parentMessageId + + const { ChatGPTAPI } = await import('chatgpt') + let api = cloud.shared.get('chatgpt_api') + if (!api) { + api = new ChatGPTAPI({ + apiKey: process.env.CHAT_GPT_API_KEY + }) + cloud.shared.set('chatgpt_api', api) + } + let options: any = {} + options.systemMessage = systemMessage + // If parent message ID is provided + if (parentMessageId) { + options.parentMessageId = parentMessageId + } + // If streaming mode is enabled + if (stream) { + ctx.response.setHeader('Content-Type', 'application/octet-stream'); + options.onProgress = (partialResponse => { + if (partialResponse && partialResponse.delta != undefined) { + console.log("loading", partialResponse) + ctx.response.write(partialResponse.delta); + } + }) + const res = await api.sendMessage(content, options) + console.log("success", res) + ctx.response.end() + } + else { + const res = await api.sendMessage(content, options) + console.log("success", res) + return res + } +} +``` + +- Configure the environment variable `CHAT_GPT_API_KEY`: This is your OpenAI Key. +- Configure `stream`: This is the switch for streaming mode. +- Configure `systemMessage`: This can be used to impose restrictions on the AI. + +> Note: The API instance needs to be cached in order to preserve context. + +## Other + +### Difference between OpenAI API and Plus + +Both are different services offered by OpenAI. In this case, we do not use Plus. + +OpenAI API is an API service provided by OpenAI for developers. + +Plus membership is a personal web service provided by OpenAI, which allows the use of the 4.0 models. + +### Checking Remaining OpenAI API Quota + +After logging in, go to the Usage page to view the remaining quota. + +### OpenAI API Credit Card Binding + +Please follow the link below to bind your credit card to your OpenAI API account: + + + +Please note that you will need to provide a valid international credit card for this process. Once successfully bound, you will be able to upgrade your account to the $120 plan. diff --git a/docs/en/3min/claude/index.md b/docs/en/3min/claude/index.md new file mode 100644 index 0000000000..eaeb7ac72f --- /dev/null +++ b/docs/en/3min/claude/index.md @@ -0,0 +1,133 @@ +--- +title: Accessing Claude +--- + +# {{ $frontmatter.title }} + +Accessing Claude through Laf is done using the Slack API, so it can be directly called from domestic servers. + +## Applying for Claude in Slack + +### Registering on Slack and creating a workspace + +You need to register using a Google email account. + + + +### Adding the Claude app to your workspace + +This step requires some magic + + + +### Enabling Slack Connect + +Slacks registered with a Google account can use a free trial. + +After entering the workspace, click on `Slack Connect` on the left side, then click on create channel, and finally click on start free trial. There will be no need to renew later. + +![slack-connect](../../doc-images/slack-connect.jpg) + +![slack-connect-create](../../doc-images/slack-connect-create.jpg) + +![slack-connect-create-1](../../doc-images/slack-connect-create-1.jpg) + +### Initiating a conversation with Claude to enable its features + +Click on Claude in the app and send any text. A popup will appear with an agree button. Once you click on it, the feature will be enabled and Claude will be able to answer questions. This indicates success! + +### Inviting Claude to a channel + +In the channel where you want to invite Claude, type `@Claude`. You will be prompted if you want to invite Claude to the channel. + +## Configuring the Slack bot + +- 1. Login to Slack, +- 2. Go to +- 3. Create an app. After clicking on `Your Apps` in the upper right corner, create a new app. + +![slack-create-app](../../doc-images/slack-create-app.jpg) + +![slack-create-app-bot](../../doc-images/slack-create-app-bot.jpg) + +- 4. Configure Scopes. Click on OAuth & Permissions in the left sidebar, find User Token Scopes under the Scopes module, click on Add an OAuth Scopes button, and then add the following permissions one by one: + +```js +channels:history +channels:read +channels:write +groups:history +groups:read +groups:write +chat:write +im:history +im:write +mpim:history +mpim:write +``` + +- 5. Install the bot to your workspace by clicking on Install to Workspace under OAuth Tokens for Your Workspace. + +![slack-install-bot](../../doc-images/slack-install-bot.jpg) + +## Installing dependencies + +`claude-api-slack` + +## Cloud Function Invocation + +```typescript +import cloud from '@lafjs/cloud' + +export default async function (ctx: FunctionContext) { + const { question, conversationId } = ctx.query + return await askClaudeAPI(question, conversationId) +} + +async function askClaudeAPI(question, conversationId) { + const token = '' // bot token + const bot = '' // bot id + const ChannelName = "" // channel name + + // Initialize claude + const { Authenticator } = await import('claude-api-slack') + + // Save the client instance in cache to maintain context + let claudeClient = cloud.shared.get('claudeClient') + if (!claudeClient) { + claudeClient = new Authenticator(token, bot) + cloud.shared.set('claudeClient', claudeClient) + } + // Create a channel and get the room ID: channel + const channel = await claudeClient.newChannel(ChannelName) + + let result + if (conversationId) { + result = await claudeClient.sendMessage({ + text: question, + channel, + conversationId, + onMessage: (originalMessage) => { + console.log("loading", originalMessage) + } + }) + } else { + result = await claudeClient.sendMessage({ + text: question, + channel, + onMessage: (originalMessage) => { + // console.log("loading", originalMessage) + console.log("loading", originalMessage) + } + }) + } + console.log("success", result) + return { + code: 0, + msg: result.text, + conversationId: result.conversationId + } +} +``` + +> Note: The API instance needs to be saved to cache in order to maintain context. \ No newline at end of file diff --git a/docs/en/3min/index.md b/docs/en/3min/index.md new file mode 100644 index 0000000000..c4a2255f86 --- /dev/null +++ b/docs/en/3min/index.md @@ -0,0 +1,9 @@ +--- +title: Three Minute Lab +--- + +# {{ $frontmatter.title }} + +What is the Three-Minute Lab? It aims to provide quick access and help developers achieve rapid integration with various applications and AI. + +The Three-Minute Lab offers developers a variety of "plug-and-play" AI and application integration examples. diff --git a/docs/en/3min/midjourney/index.md b/docs/en/3min/midjourney/index.md new file mode 100644 index 0000000000..99e26b98af --- /dev/null +++ b/docs/en/3min/midjourney/index.md @@ -0,0 +1,201 @@ +--- +title: Integration with midjourney +--- + +# {{ $frontmatter.title }} + +Laf integrates with Midjourney using the Discord API and requires the use of `laf.dev`. + +## Setting up Midjourney + +To set up Midjourney, you need to purchase a Midjourney membership package. + +## Installing Dependencies + +`midjourney` + +## Cloud Function Invocation + +```typescript +import cloud from '@lafjs/cloud' +import { Midjourney, MidjourneyMessage } from 'midjourney' +const SERVER_ID = '' // Midjourney server ID +const CHANNEL_ID = '' // Midjourney channel ID +const SALAI_TOKEN = '' // Midjourney service token + +const Limit = 100 +const MaxWait = 3 + +const client = new Midjourney({ + ServerId: SERVER_ID, + ChannelId: CHANNEL_ID, + SalaiToken: SALAI_TOKEN, + Debug: true, + SessionId: SALAI_TOKEN, + Limit: Limit, + MaxWait: MaxWait +}); + +export default async function (ctx: FunctionContext) { + const { type, param } = ctx.body + switch (type) { + case 'RetrieveMessages': + return await RetrieveMessages(param) + case 'imagine': + return await imagine(param) + case 'upscale': + return await upscale(param) + case 'variation': + return await variation(param) + } + +} + +// Retrieve recent messages +async function RetrieveMessages(param) { + console.log("RetrieveMessages") + const client = new MidjourneyMessage({ + ChannelId: CHANNEL_ID, + SalaiToken: SALAI_TOKEN, + }); + const msg = await client.RetrieveMessages(); + console.log("RetrieveMessages success ", msg) + return msg +} + +// Create an image generation task +async function imagine(param) { + console.log("imagine", param) + const { question, msg_Id } = param + const msg = await client.Imagine( + `[${msg_Id}] ${question}`, + (uri: string, progress: string) => { + console.log("loading", uri, "progress", progress); + } + ); + console.log("imagine success ", msg) + return true +} + +// Upscale an image +async function upscale(param) { + console.log("upscale", param) + const { question, index, id, url } = param + const hash = url.split("_").pop()?.split(".")[0] ?? "" + console.log(hash) + const msg = await client.Upscale( + question, + index, + id, + hash, + (uri: string, progress: string) => { + console.log("loading", uri, "progress", progress); + } + ); + console.log("upscale success ", msg) + return msg +} + +// Variation of an image +async function variation(param) { + console.log("variation", param) + const client = new Midjourney({ + ServerId: SERVER_ID, + ChannelId: CHANNEL_ID, + SalaiToken: SALAI_TOKEN, + Debug: true, + SessionId: SALAI_TOKEN, + Limit: Limit, + MaxWait: 100 + }); + const { question, index, id, url } = param + const hash = url.split("_").pop()?.split(".")[0] ?? "" + const msg = await client.Variation( + question, + index, + id, + hash, + (uri: string, progress: string) => { + console.log("loading", uri, "progress", progress); + } + ); + console.log("variation success ", msg) + return msg +} +``` + +## Get Parameters + +- Create a new channel and invite the midjourney bot to join the group. +- Retrieve SERVER_ID and CHANNEL_ID from the URL. +- Obtain SALAI_TOKEN from the browser console. + +![mj-id](../../doc-images/mj-id.png) + +Open the browser console and switch to the NETWORK tab. Refresh the webpage and select any request. Find the "authorization" header, which contains the SALAI_TOKEN. + +![SALAI_TOKEN](../../doc-images/SALAI_TOKEN.jpg) + +Modify the SERVER_ID, CHANNEL_ID, and SALAI_TOKEN in the cloud function and publish it. + +## Test Cases + +Use a POST request to call this cloud function. + +The following are the calling parameters. + +### Generate Image Parameters + +`msg_Id` is generated when calling the function and will be used for querying later. `question` is the prompt. + +```typescript +{ + "type": "imagine", + "param": { + "question": "a super man", + "msg_Id": "1" + } +} +``` + +### Query All History Messages Parameters + +```typescript +{ + "type": "RetrieveMessages" +} +``` + +By querying the history messages, you can obtain image information, etc. + +### Enlarge Image Parameters + +`id` and `url` come from querying all history messages. `index` is the image index, and `question` is the prompt. `id` is the first `id`. + +```typescript +{ + "type": "upscale", + "param": { + "index": 2, + "question": "a dog and a cat", + "id": "1111579111815659551", + "url": "https://cdn.discordapp.com/attachments/1110206663958466611/1111579111287164988/johnsonmaureen_2023_a_dog_and_a_cat_5927bc5d-5d80-423c-bf89-c2357d5aaf6b.png" + } +} +``` + +### Transform Image Parameters + +`id` and `url` are obtained from querying all the historical messages, `index` is the index of the image, `question` is the prompt. `id` is the first `id`. + +```typescript +{ + "type": "variation", + "param": { + "index": 2, + "question": "a dog and a cat", + "id": "1111579111815659551", + "url": "https://cdn.discordapp.com/attachments/1110206663958466611/1111579111287164988/johnsonmaureen_2023_a_dog_and_a_cat_5927bc5d-5d80-423c-bf89-c2357d5aaf6b.png" + } +} +``` \ No newline at end of file diff --git a/docs/en/3min/wechat/CorporateWeChat/index.md b/docs/en/3min/wechat/CorporateWeChat/index.md new file mode 100644 index 0000000000..0273fae0fb --- /dev/null +++ b/docs/en/3min/wechat/CorporateWeChat/index.md @@ -0,0 +1,9 @@ +--- +title: Three Minute Laboratory +--- + + + +# {{ $frontmatter.title }} + +> TODO \ No newline at end of file diff --git a/docs/en/3min/wechat/MediaPlatform/H5.md b/docs/en/3min/wechat/MediaPlatform/H5.md new file mode 100644 index 0000000000..494b890d54 --- /dev/null +++ b/docs/en/3min/wechat/MediaPlatform/H5.md @@ -0,0 +1,340 @@ +--- +title: Official Account H5 +--- + +# {{ $frontmatter.title }} + +Official Account H5 development must be based on an authenticated service account. Personal accounts and subscription accounts can be ignored. + +The development of WeChat Official Account H5 will use [JSAPI](https://developers.weixin.qq.com/doc/offiaccount/OA_Web_Apps/JS-SDK.html). + +In this document, we will use a pre-packaged JSAPI dependency to implement it: [weixin-js-sdk](https://www.npmjs.com/package/weixin-js-sdk). + +## Silent Login (Get OpenID) + +### Cloud Function Part + +Named as: `h5-login` + +```typescript +import cloud from '@lafjs/cloud' + +const appid = process.env.WECHAT_APPID +const appsecret = process.env.WECHAT_SECRET + +export default async function (ctx: FunctionContext) { + const { code } = ctx.body + return await login(code) +} + +// Get user openid based on code +async function login(code) { + const userInfo = await cloud.fetch.get(`https://api.weixin.qq.com/sns/oauth2/access_token?appid=${appid}&secret=${appsecret}&code=${code}&grant_type=authorization_code`) + console.log(userInfo.data) + return userInfo.data +} +``` + +### Frontend Part + +Taking uniapp as an example. + +Replace baseUrl with the URL of the cloud function, without the name of the cloud function. appID is the appid of the Official Account. + +>Vconsole and weixin-js-sdk dependencies are used. Please remember to install them with npm install. + +```typescript + +``` + +## JSAPI Payment (Official Account H5 Payment) + +### Obtain V3 Certificate and V3 Secret Key + +![set-wechat-pay](/doc-images/set-wechat-pay.png) + +- Step 1: Click on "Account Security" +- Step 2: Click on "API Security" +- Step 3: Click on "Apply for Certificate" +- Step 4: Click on "Set APIv3 Secret Key" + +### Binding Official Account with Merchant Number for Payment + +![bind-pay](/doc-images/bind-pay.png) + +![bind-pay2](/doc-images/bind-pay2.png) + +- Step 1: Log in to the Official Account backend at and click on "WeChat Payment" +- Step 2: Click on "Associate More Merchant Numbers" and log in to the WeChat Payment merchant account that needs to be bound +- Step 3: Click on "Product Center" +- Step 4: Click on "AppID Account Management" +- Step 5: Click on "My Associated AppID Accounts" +- Step 6: Click on "Associate AppID" +- Step 7: Fill in the information of the Official Account's AppID and click on submit +- Step 8: On the Official Account's WeChat Payment page, click on "Confirm" to complete the binding. + +### Cloud Function and Front-end Example Code + +>laf function dependency installation `wechatpay-node-v3-laf` +> +>Front-end dependency installation `weixin-js-sdk` + +Payment cloud function, named: `h5-pay` + +```typescript +import cloud from "@lafjs/cloud"; +const baseUrl = '' // laf function domain + +export default async function (ctx: FunctionContext) { + const Pay = require('wechatpay-node-v3-laf'); + const pay = new Pay({ + appid: '', // Authentication Service Number appid + mchid: '', // WeChat Pay merchant number bound to the authentication service number + publicKey: savePublicKey(), // V3 public key apiclient_cert.pem + privateKey: savePrivateKey(), // V3 private key apiclient_key.pem + }); + + const params = { + description: 'pay test', + out_trade_no: '12345678', // Order number, can be randomly generated for testing purposes only, in actual use, it needs to be managed separately + notify_url: `${baseUrl}/h5-pay-notify`, + amount: { + total: 1, + }, + payer: { + openid: ctx.body.openid, + }, + scene_info: { + payer_client_ip: ctx.headers['x-real-ip'], + }, + }; + const result = await pay.transactions_jsapi(params); + console.log("pay params",result); + return result +}; + +function savePrivateKey() { + const key = `-----BEGIN PRIVATE KEY----- +MIIEvgIBADANBgkqhkiG9w0BAQEFAASCBKgwggSkAgEAAoIBAQDGVc+DdK1VRxFt +... +... +... +NJ0Fpw91iHlttL5IzRmQ/Y+m6zaieCaRlvkEuW9xnzGJY6JPDcz/Y3D+5bYYlWir +dDdYre8DP5exGNAJ5V+cQeVn +-----END PRIVATE KEY----- +`; + return Buffer.from(key); +} + +function savePublicKey() { + const key = `-----BEGIN CERTIFICATE----- +MIID7DCCAtSgAwIBAgIUf0yvWQ0DLN+H2GG9Cz2VC6MkLfEwDQYJKoZIhvcNAQEL +... +... +... +t1MYZCa6F/ePNugyXhxkhGGB1gUB8HLYN2OuwlxNtASzlp2CifCBKojFLAduhdnJ +-----END CERTIFICATE----- +`; + return Buffer.from(key); +} +``` + +Callback notification cloud function, named: `h5-pay-notify` + +When the user's payment is successful or fails, the WeChat Pay server will send a request to the callback notification cloud function. Based on the information received from the callback notification, it can be determined whether the user's payment was successful. + +```typescript +import cloud from '@lafjs/cloud' +const Pay = require('wechatpay-node-v3-laf'); +const pay = new Pay({ + appid: '', // Authentication Service Number appid + mchid: '', // WeChat Pay merchant number bound to the authentication service number + publicKey: savePublicKey(), // V3 public key apiclient_cert.pem + privateKey: savePrivateKey(), // V3 private key apiclient_key.pem +}); +const key = "" // APIv3 key set in the merchant platform【WeChat Merchant Platform->Account Settings->API Security->Set APIv3 Key】 + +export default async function (ctx: FunctionContext) { + + // Sample content of ctx.body when payment is successfully notified + // { + // id: '8e6ff231-f7bc-514f-bec3-e24157fa2db5', + // create_time: '2023-05-22T03:44:20+08:00', + // resource_type: 'encrypt-resource', + // event_type: 'TRANSACTION.SUCCESS', + // summary: 'Payment successful', + // resource: { + // original_type: 'transaction', + // algorithm: 'AEAD_AES_256_GCM', + // ciphertext: 'tm+BtFF2/mxDJ49uQp39JlOv6Ss6rVjoxyxQE8/rbNtRR+TLVniHGq9cWlycXd408wYx0OmnpUV0BADqEB/VuIk+w4DmvoXH7ingWcnHP4xjnLaO4jDfsLMMcnPdZyMHiBYhAgGyXpmftqgGZs+5AnCcuMq3A7o8dpAyV/qxRQypaqcRhwaYw+TErGecpPw3SDTi7ekwQ8J5PUVCmchBk3n1Li064NsYnkmUzDd1WDzXcVqmw/Vlt/JnoNMBKh7+AhyZGv6pwZLcmgBlaYjHeUdtHNjidBpSCtXQ1HV8mxLGi4L3hQ/ZFK2kpUbt2tRsby1ai5KtUj4rxaAyOZ79j40v3RYjTSAiwKdCiP7c5jT6Q4uwmK8Dt4CfrMGLem4W8Ah6+bp8/b26Ib7JMc42lSeKaufltNY+1o/ffL5sBI9LiJRr7T4Cn2JeYBP0Nuaw2RMfUlT3cCUaotJx9BBm8oVzr0NY0kWhdrKgyO63SeGS9WMmd3F5//pkbsCgsumva8gcvhL5Pch79SKzNYiguRUnqg8teR0gD0E8u90Jc7WfcDOZylPY', + // associated_data: 'transaction', + +```javascript +// nonce: 'n1NofrM384Ci' + // } + // } + + const { resource } = ctx.body + const { ciphertext, associated_data, nonce } = resource + const result = pay.decipher_gcm(ciphertext, associated_data, nonce, key); + console.log("Payment decryption", result) + if (result.trade_state == "SUCCESS") { + // Logic for successful payment + + } + + + return ` + + +` +} + +function savePrivateKey() { + const key = `-----BEGIN PRIVATE KEY----- +MIIEvQIBADANBgkqhkiG9w0BAQEFAASCBKcwggSjAgEAAoIBAQC2msJBre5qr9Z7 +... +... +... +iXTIs7zGtva7qSMNX6r5844/UnMeZv4ohK8pwMMPXOtVPc3grVx7DX5D2jY3ITid +cTg/LKJksgROdEz3E7EIQ7k= +-----END PRIVATE KEY----- +`; + return Buffer.from(key); +} + +function savePublicKey() { + const key = `-----BEGIN CERTIFICATE----- +MIIEKzCCAxOgAwIBAgIUTb+JfWjcUWhea7fP1TyqzJIUNVIwDQYJKoZIhvcNAQEL +... +... +... +0hhIiCOXNdeeo92JZUJLp8BsomSNFbHPckunxt99gaQYPYTF+LlBQRUYOP5MnS65 +cOKTpMEZD57/fDqBzbvU +-----END CERTIFICATE----- +`; + return Buffer.from(key); +} +``` + +Front-end + +To obtain the user's openid, please refer to the code above. + +```typescript +import weixin from 'weixin-js-sdk' +// Payment +pay() { + const token = uni.getStorageSync('token') + const openid = token.openid + uni.request({ + url: `${baseUrl}/h5-pay`, + method: 'POST', + data: { + openid: openid + }, + success: (res) => { + weixin.config({ + // debug: true, + appId: this.appID, + timestamp: res.data.timeStamp, + nonceStr: res.data.nonceStr, + signature: res.data.signature, + jsApiList: ['chooseWXPay'] + }) + weixin.ready(function () { + weixin.chooseWXPay({ + timestamp: res.data.timeStamp, + nonceStr: res.data.nonceStr, + package: res.data.package, + signType: res.data.signType, + paySign: res.data.paySign, + success: function (res) { + console.log('Payment successful', res) + }, + fail: function (res) { + console.log('Payment failed', res) + } + }) + }) + }, + fail: (err) => { + console.log(err); + } + }) +}, +``` diff --git a/docs/en/3min/wechat/MediaPlatform/Menu.md b/docs/en/3min/wechat/MediaPlatform/Menu.md new file mode 100644 index 0000000000..6f357fbc5e --- /dev/null +++ b/docs/en/3min/wechat/MediaPlatform/Menu.md @@ -0,0 +1,135 @@ +--- +title: Custom Menu +--- + +# {{ $frontmatter.title }} + +Since our own server is integrated, the built-in menu management of the official account becomes unavailable. We can manage the menu through cloud functions. + +## Create and Publish a Cloud Function + +Obtain the `appid` and `appsecret` from the backend of the official account and set them as environment variables `WECHAT_APPID` and `WECHAT_SECRET`. + +```typescript +const appid = process.env.WECHAT_APPID +const appsecret = process.env.WECHAT_SECRET + +// Take the menu configuration of laf developer official account as an example +const menu = { + button: [ + { type: 'view', name: 'User Forum', url: 'https://forum.laf.run/' }, + { + type: 'media_id', + name: 'Join the Group Now', + media_id: 'EvjUO0_eaTT2pBbYYcM5trVz_aFexNcXDkACOvQLDAUZhJXSe0zTenPiOQZPHzRJ' + }, + { + name: 'About Us', + sub_button: [ + { type: 'view', name: 'Domestic Website', url: 'https://laf.run' }, + { type: 'view', name: 'Overseas Website', url: 'https://laf.dev' }, + { + type: 'view', + name: 'GitHub', + url: 'https://github.com/labring/laf' + }, + { + type: 'view', + name: 'Contact Us', + url: 'https://www.wenjuan.com/s/I36ZNbl/#' + } + ] + } + ] +} + +export default async function (ctx: FunctionContext) { + // Print the current menu + console.log(await getMenu()) + // Set the menu + const res = await setMenu(menu) +} + +// Get the menu of the official account +async function getMenu() { + const access_token = await getAccess_token() + const res = await cloud.fetch.get(`https://api.weixin.qq.com/cgi-bin/get_current_selfmenu_info?access_token=${access_token}`) + return res.data +} + +// Set the menu of the official account +export async function setMenu(menu) { + const access_token = await getAccess_token() + const res = await cloud.fetch.post(`https://api.weixin.qq.com/cgi-bin/menu/create?access_token=${access_token}`, menu) + console.log("setting success", res.data) + return res.data +} + +// Get the ACCESS_TOKEN of WeChat official account +async function getAccess_token() { + // Check if the ACCESS_TOKEN of the WeChat official account is expired + const shared_access_token = cloud.shared.get("mp_access_token") + if (shared_access_token) { + if (shared_access_token.exp > Date.now()) { + return shared_access_token.access_token + } + } + // Obtain the ACCESS_TOKEN of the WeChat official account, save it with an expiration time, and then save it to the cache + const mp_access_token = await cloud.fetch.get(`https://api.weixin.qq.com/cgi-bin/token?grant_type=client_credential&appid=${appid}&secret=${appsecret}`) + cloud.shared.set("mp_access_token", { + access_token: mp_access_token.data.access_token, + exp: Date.now() + 7100 * 1000 + }) + return mp_access_token.data.access_token +} +``` + +## Debugging and Running + +You can directly debug and run it in the Web IDE to publish the official account menu. You can also make it a POST request, pass in the value of menu, and configure the official account menu through other front-end methods. + +## Get Media_id + +If there is a requirement to send a file after clicking on a menu, the Media_id of the file needs to be obtained. + +For example, if there is a requirement to send a specified QR code image automatically when "Join Group Now" is clicked, the image needs to be uploaded and its Media_id needs to be obtained. + +```typescript +export default async function (ctx: FunctionContext) { + + const url = 'https://xxx.xxx.xxx/pic.jpg' + const res = await updatePic(url) + console.log('Media_id',res) +} + +// Uploads the image to WeChat temporary media by the image URL +async function updatePic(url) { + const match = /^https?:\/\/[^\/]*\/.*?[^A-Za-z0-9]*?([A-Za-z0-9]+).([A-Za-z0-9]+)[^A-Za-z0-9]*?(?:.*)$/.exec(url) + const pic_name = `${match[1]}.${match[2]}` + const res = await cloud.fetch.get(url, { + responseType: 'arraybuffer' + }) + const access_token = await getAccess_token() + const formData = { + media: { + value: Buffer.from(res.data), + options: { + filename: pic_name, + contentType: 'image/png' + } + } + }; + const request = require('request'); + const util = require('util'); + const postRequest = util.promisify(request.post); + const uploadResponse = await postRequest({ + url: `https://api.weixin.qq.com/cgi-bin/media/upload?access_token=${access_token}&type=image`, + formData: formData + }); + return uploadResponse.body +} +``` + +## For more menu-related information, you can refer to the WeChat development documentation for integration + +Link: diff --git a/docs/en/3min/wechat/MediaPlatform/ServerDocking.md b/docs/en/3min/wechat/MediaPlatform/ServerDocking.md new file mode 100644 index 0000000000..0a960511a8 --- /dev/null +++ b/docs/en/3min/wechat/MediaPlatform/ServerDocking.md @@ -0,0 +1,120 @@ +--- +title: Server Docking +--- + +# {{ $frontmatter.title }} + +## Create Docking Cloud Function + +You can name it as you like. The sample code provided here is for plaintext access, so you don't need to fill in the "Message Encryption Key". + +:::tip +The toXML method is applicable to all WeChat subscription accounts and service accounts. + +Authenticated subscription accounts and service accounts can also use the custom message function to reply to messages. +::: + +```typescript +// Import crypto and cloud modules +import * as crypto from 'crypto'; +import cloud from '@lafjs/cloud'; + +// Handle incoming WeChat Official Account messages +export async function main(event) { + const { signature, timestamp, nonce, echostr } = event.query; + const token = process.dev.WECHAT_TOKEN; + + // Verify the legitimacy of the message, return an error message if illegitimate + if (!verifySignature(signature, timestamp, nonce, token)) { + return 'Invalid signature'; + } + + // If it is the first verification, return echostr to the WeChat server + if (echostr) { + return echostr; + } + + // Handle the incoming message + const payload = event.body.xml; + console.log("receive message:", payload) + // Text message + if (payload.msgtype[0] === 'text') { + const newMessage = { + msgid: payload.msgid[0], + question: payload.content[0].trim(), + username: payload.fromusername[0], + sessionId: payload.fromusername[0], + createdAt: Date.now() + } + // Reply with text, this is just a demo, the reply will be the same as the received message + const responseText = newMessage.question + return toXML(payload, responseText); + } + + // For other cases, reply with 'success' or '' to avoid timeout issues + return 'success' +} + + +// Verify the legitimacy of the message sent by the WeChat server +function verifySignature(signature, timestamp, nonce, token) { + const arr = [token, timestamp, nonce].sort(); + const str = arr.join(''); + const sha1 = crypto.createHash('sha1'); + sha1.update(str); + return sha1.digest('hex') === signature; +} + +// Return the assembled XML +function toXML(payload, content) { + const timestamp = Date.now(); + const { tousername: fromUserName, fromusername: toUserName } = payload; + return ` + + + + ${timestamp} + + + + ` +} +``` + +## Get Official Account Token and Configure it + +### Log in to the Official Account Backend + +Scan the QR code at to log in to your WeChat Official Account. + +### Configure the Server + +![MediaPlatformBaseSetting](/doc-images/MediaPlatformBaseSetting.png) + +![MediaPlatformBaseSetting2](/doc-images/MediaPlatformBaseSetting2.png) + +- Step 3: Enter the URL of the newly created and published cloud function. + +- Step 4: Customize a "Token" that matches the environment variable "WECHAT_TOKEN" in the cloud function. After setting it here, you also need to configure the environment variable in the laf application. + +- Step 5: The plaintext mode is not used in this case. + +If the configuration is correct, clicking on "Submit" will display "Submission Successful". + +## Test the Conversation Functionality + +Send a text message in your WeChat Official Account. If the account replies with the same text, it means that the docking is successful. + +## More features can be found in the WeChat development documentation + +:::tip +The most common pitfall when accessing the message functionality of a WeChat Official Account is failing to respond within 5 seconds after receiving a message from a user. If no response is made within this time frame, WeChat will automatically retry the request 3 times. This can easily lead to timeout issues when integrating with ChatGPT. + +To solve this issue, you can refer to the document: [Setting a Timeout for a Specific Request in Cloud Functions](/guide/function/faq.html#云函数设置某请求超时时间). + +If you are using an authenticated subscription account or service account, it is recommended to use the customer service message feature for a better user experience. +::: + +For information on developing message-related capabilities, please refer to: . + +All other features can also be integrated normally. \ No newline at end of file diff --git a/docs/en/3min/wechat/MediaPlatform/index.md b/docs/en/3min/wechat/MediaPlatform/index.md new file mode 100644 index 0000000000..3b1f4f93f9 --- /dev/null +++ b/docs/en/3min/wechat/MediaPlatform/index.md @@ -0,0 +1,7 @@ +--- +Title: Three Minute Lab +--- + +# {{ $frontmatter.title }} + +> TODO diff --git a/docs/en/3min/wechat/MiniProgram/index.md b/docs/en/3min/wechat/MiniProgram/index.md new file mode 100644 index 0000000000..0273fae0fb --- /dev/null +++ b/docs/en/3min/wechat/MiniProgram/index.md @@ -0,0 +1,9 @@ +--- +title: Three Minute Laboratory +--- + + + +# {{ $frontmatter.title }} + +> TODO \ No newline at end of file diff --git a/docs/en/3min/wechat/index.md b/docs/en/3min/wechat/index.md new file mode 100644 index 0000000000..0553c65c4d --- /dev/null +++ b/docs/en/3min/wechat/index.md @@ -0,0 +1,9 @@ +--- +title: Three-Minute Laboratory +--- + + + +# {{ $frontmatter.title }} + +> TODO \ No newline at end of file diff --git a/docs/en/README.md b/docs/en/README.md new file mode 100644 index 0000000000..a781d8822f --- /dev/null +++ b/docs/en/README.md @@ -0,0 +1,3 @@ +### (WIP) laf documentation v0.8 + +> TODO \ No newline at end of file diff --git a/docs/en/doc-images/AppID.png b/docs/en/doc-images/AppID.png new file mode 100644 index 0000000000..6b4cd6af08 Binary files /dev/null and b/docs/en/doc-images/AppID.png differ diff --git a/docs/en/doc-images/MediaPlatformBaseSetting.png b/docs/en/doc-images/MediaPlatformBaseSetting.png new file mode 100644 index 0000000000..5ffb08c11a Binary files /dev/null and b/docs/en/doc-images/MediaPlatformBaseSetting.png differ diff --git a/docs/en/doc-images/MediaPlatformBaseSetting2.png b/docs/en/doc-images/MediaPlatformBaseSetting2.png new file mode 100644 index 0000000000..b58a559817 Binary files /dev/null and b/docs/en/doc-images/MediaPlatformBaseSetting2.png differ diff --git a/docs/en/doc-images/SALAI_TOKEN.jpg b/docs/en/doc-images/SALAI_TOKEN.jpg new file mode 100644 index 0000000000..518b6a6c0d Binary files /dev/null and b/docs/en/doc-images/SALAI_TOKEN.jpg differ diff --git a/docs/en/doc-images/add-env-step1.png b/docs/en/doc-images/add-env-step1.png new file mode 100644 index 0000000000..e59bcbf11d Binary files /dev/null and b/docs/en/doc-images/add-env-step1.png differ diff --git a/docs/en/doc-images/add-env-step2.png b/docs/en/doc-images/add-env-step2.png new file mode 100644 index 0000000000..415f061139 Binary files /dev/null and b/docs/en/doc-images/add-env-step2.png differ diff --git a/docs/en/doc-images/add-env-step3.png b/docs/en/doc-images/add-env-step3.png new file mode 100644 index 0000000000..daf0c3691e Binary files /dev/null and b/docs/en/doc-images/add-env-step3.png differ diff --git a/docs/en/doc-images/add-env.png b/docs/en/doc-images/add-env.png new file mode 100644 index 0000000000..9af5525e5d Binary files /dev/null and b/docs/en/doc-images/add-env.png differ diff --git a/docs/en/doc-images/add-packages.png b/docs/en/doc-images/add-packages.png new file mode 100644 index 0000000000..2247b12cb3 Binary files /dev/null and b/docs/en/doc-images/add-packages.png differ diff --git a/docs/en/doc-images/add-polocy.png b/docs/en/doc-images/add-polocy.png new file mode 100644 index 0000000000..6bde41e193 Binary files /dev/null and b/docs/en/doc-images/add-polocy.png differ diff --git a/docs/en/doc-images/app-list.png b/docs/en/doc-images/app-list.png new file mode 100644 index 0000000000..c66eb249d9 Binary files /dev/null and b/docs/en/doc-images/app-list.png differ diff --git a/docs/en/doc-images/appList.png b/docs/en/doc-images/appList.png new file mode 100644 index 0000000000..489052d5ba Binary files /dev/null and b/docs/en/doc-images/appList.png differ diff --git a/docs/en/doc-images/application-list.png b/docs/en/doc-images/application-list.png new file mode 100644 index 0000000000..d0545ce98e Binary files /dev/null and b/docs/en/doc-images/application-list.png differ diff --git a/docs/en/doc-images/auto-build1.png b/docs/en/doc-images/auto-build1.png new file mode 100644 index 0000000000..314aeb2b42 Binary files /dev/null and b/docs/en/doc-images/auto-build1.png differ diff --git a/docs/en/doc-images/auto-build2.png b/docs/en/doc-images/auto-build2.png new file mode 100644 index 0000000000..39cd3a9f92 Binary files /dev/null and b/docs/en/doc-images/auto-build2.png differ diff --git a/docs/en/doc-images/bind-pay.png b/docs/en/doc-images/bind-pay.png new file mode 100644 index 0000000000..194ed24530 Binary files /dev/null and b/docs/en/doc-images/bind-pay.png differ diff --git a/docs/en/doc-images/bind-pay2.png b/docs/en/doc-images/bind-pay2.png new file mode 100644 index 0000000000..629b6bbd03 Binary files /dev/null and b/docs/en/doc-images/bind-pay2.png differ diff --git a/docs/en/doc-images/change-package-version.png b/docs/en/doc-images/change-package-version.png new file mode 100644 index 0000000000..c6537d2ff0 Binary files /dev/null and b/docs/en/doc-images/change-package-version.png differ diff --git a/docs/en/doc-images/cli-mind.png b/docs/en/doc-images/cli-mind.png new file mode 100644 index 0000000000..8ea6005479 Binary files /dev/null and b/docs/en/doc-images/cli-mind.png differ diff --git a/docs/en/doc-images/creat-polocy.png b/docs/en/doc-images/creat-polocy.png new file mode 100644 index 0000000000..3094797579 Binary files /dev/null and b/docs/en/doc-images/creat-polocy.png differ diff --git a/docs/en/doc-images/creat-token.png b/docs/en/doc-images/creat-token.png new file mode 100644 index 0000000000..f0b14fe98f Binary files /dev/null and b/docs/en/doc-images/creat-token.png differ diff --git a/docs/en/doc-images/create-application.png b/docs/en/doc-images/create-application.png new file mode 100644 index 0000000000..33637995fb Binary files /dev/null and b/docs/en/doc-images/create-application.png differ diff --git a/docs/en/doc-images/create-bucket-1.png b/docs/en/doc-images/create-bucket-1.png new file mode 100644 index 0000000000..dad0aa04c3 Binary files /dev/null and b/docs/en/doc-images/create-bucket-1.png differ diff --git a/docs/en/doc-images/create-bucket.png b/docs/en/doc-images/create-bucket.png new file mode 100644 index 0000000000..f7eb407840 Binary files /dev/null and b/docs/en/doc-images/create-bucket.png differ diff --git a/docs/en/doc-images/create-cloudfunction.png b/docs/en/doc-images/create-cloudfunction.png new file mode 100644 index 0000000000..096a68d8d0 Binary files /dev/null and b/docs/en/doc-images/create-cloudfunction.png differ diff --git a/docs/en/doc-images/create-function.jpg b/docs/en/doc-images/create-function.jpg new file mode 100644 index 0000000000..43e487b9bf Binary files /dev/null and b/docs/en/doc-images/create-function.jpg differ diff --git a/docs/en/doc-images/create-function.png b/docs/en/doc-images/create-function.png new file mode 100644 index 0000000000..ed777e388e Binary files /dev/null and b/docs/en/doc-images/create-function.png differ diff --git a/docs/en/doc-images/create-gather.png b/docs/en/doc-images/create-gather.png new file mode 100644 index 0000000000..e6b7e0666b Binary files /dev/null and b/docs/en/doc-images/create-gather.png differ diff --git a/docs/en/doc-images/create-injector.png b/docs/en/doc-images/create-injector.png new file mode 100644 index 0000000000..43b42d2226 Binary files /dev/null and b/docs/en/doc-images/create-injector.png differ diff --git a/docs/en/doc-images/create-laf-app.jpg b/docs/en/doc-images/create-laf-app.jpg new file mode 100644 index 0000000000..a4a547070f Binary files /dev/null and b/docs/en/doc-images/create-laf-app.jpg differ diff --git a/docs/en/doc-images/db.png b/docs/en/doc-images/db.png new file mode 100644 index 0000000000..1c2c3580bd Binary files /dev/null and b/docs/en/doc-images/db.png differ diff --git a/docs/en/doc-images/dblist.jpg b/docs/en/doc-images/dblist.jpg new file mode 100644 index 0000000000..845cebe2db Binary files /dev/null and b/docs/en/doc-images/dblist.jpg differ diff --git a/docs/en/doc-images/delete-package.png b/docs/en/doc-images/delete-package.png new file mode 100644 index 0000000000..ff69f50d25 Binary files /dev/null and b/docs/en/doc-images/delete-package.png differ diff --git a/docs/en/doc-images/doc-app-list.png b/docs/en/doc-images/doc-app-list.png new file mode 100644 index 0000000000..4e8c522b90 Binary files /dev/null and b/docs/en/doc-images/doc-app-list.png differ diff --git a/docs/en/doc-images/doc-db.png b/docs/en/doc-images/doc-db.png new file mode 100644 index 0000000000..5124dbb058 Binary files /dev/null and b/docs/en/doc-images/doc-db.png differ diff --git a/docs/en/doc-images/doc-function-list.png b/docs/en/doc-images/doc-function-list.png new file mode 100644 index 0000000000..38e0cf9b60 Binary files /dev/null and b/docs/en/doc-images/doc-function-list.png differ diff --git a/docs/en/doc-images/doc-policy.png b/docs/en/doc-images/doc-policy.png new file mode 100644 index 0000000000..edcc5a67a8 Binary files /dev/null and b/docs/en/doc-images/doc-policy.png differ diff --git a/docs/en/doc-images/doc-storage.png b/docs/en/doc-images/doc-storage.png new file mode 100644 index 0000000000..8173a586c5 Binary files /dev/null and b/docs/en/doc-images/doc-storage.png differ diff --git a/docs/en/doc-images/edit-cloudfunction.png b/docs/en/doc-images/edit-cloudfunction.png new file mode 100644 index 0000000000..478cb69027 Binary files /dev/null and b/docs/en/doc-images/edit-cloudfunction.png differ diff --git a/docs/en/doc-images/file.png b/docs/en/doc-images/file.png new file mode 100644 index 0000000000..aa88488627 Binary files /dev/null and b/docs/en/doc-images/file.png differ diff --git a/docs/en/doc-images/function-body.png b/docs/en/doc-images/function-body.png new file mode 100644 index 0000000000..f29ce9cafe Binary files /dev/null and b/docs/en/doc-images/function-body.png differ diff --git a/docs/en/doc-images/function-log.png b/docs/en/doc-images/function-log.png new file mode 100644 index 0000000000..b10595c90f Binary files /dev/null and b/docs/en/doc-images/function-log.png differ diff --git a/docs/en/doc-images/function-query.png b/docs/en/doc-images/function-query.png new file mode 100644 index 0000000000..46197ae91c Binary files /dev/null and b/docs/en/doc-images/function-query.png differ diff --git a/docs/en/doc-images/function-url.png b/docs/en/doc-images/function-url.png new file mode 100644 index 0000000000..afbcde73f0 Binary files /dev/null and b/docs/en/doc-images/function-url.png differ diff --git a/docs/en/doc-images/function.png b/docs/en/doc-images/function.png new file mode 100644 index 0000000000..c21904e868 Binary files /dev/null and b/docs/en/doc-images/function.png differ diff --git a/docs/en/doc-images/getToken-parseToken.png b/docs/en/doc-images/getToken-parseToken.png new file mode 100644 index 0000000000..a39fd68dec Binary files /dev/null and b/docs/en/doc-images/getToken-parseToken.png differ diff --git a/docs/en/doc-images/index.png b/docs/en/doc-images/index.png new file mode 100644 index 0000000000..e5852130f2 Binary files /dev/null and b/docs/en/doc-images/index.png differ diff --git a/docs/en/doc-images/mj-id.png b/docs/en/doc-images/mj-id.png new file mode 100644 index 0000000000..bd9709f248 Binary files /dev/null and b/docs/en/doc-images/mj-id.png differ diff --git a/docs/en/doc-images/open-website.png b/docs/en/doc-images/open-website.png new file mode 100644 index 0000000000..f2272520bb Binary files /dev/null and b/docs/en/doc-images/open-website.png differ diff --git a/docs/en/doc-images/package-list.png b/docs/en/doc-images/package-list.png new file mode 100644 index 0000000000..63b68e72c6 Binary files /dev/null and b/docs/en/doc-images/package-list.png differ diff --git a/docs/en/doc-images/pakcage-list.jpg b/docs/en/doc-images/pakcage-list.jpg new file mode 100644 index 0000000000..348f7b075d Binary files /dev/null and b/docs/en/doc-images/pakcage-list.jpg differ diff --git a/docs/en/doc-images/polocy-db-data.png b/docs/en/doc-images/polocy-db-data.png new file mode 100644 index 0000000000..95e647c6f0 Binary files /dev/null and b/docs/en/doc-images/polocy-db-data.png differ diff --git a/docs/en/doc-images/publish-cloudfunction.png b/docs/en/doc-images/publish-cloudfunction.png new file mode 100644 index 0000000000..ac1499c251 Binary files /dev/null and b/docs/en/doc-images/publish-cloudfunction.png differ diff --git a/docs/en/doc-images/register.png b/docs/en/doc-images/register.png new file mode 100644 index 0000000000..cf9ab88694 Binary files /dev/null and b/docs/en/doc-images/register.png differ diff --git a/docs/en/doc-images/run-cloudfunction.png b/docs/en/doc-images/run-cloudfunction.png new file mode 100644 index 0000000000..9adf18e682 Binary files /dev/null and b/docs/en/doc-images/run-cloudfunction.png differ diff --git a/docs/en/doc-images/select-package-version.png b/docs/en/doc-images/select-package-version.png new file mode 100644 index 0000000000..9c9bfe0c6c Binary files /dev/null and b/docs/en/doc-images/select-package-version.png differ diff --git a/docs/en/doc-images/set-wechat-pay.png b/docs/en/doc-images/set-wechat-pay.png new file mode 100644 index 0000000000..2de73ca956 Binary files /dev/null and b/docs/en/doc-images/set-wechat-pay.png differ diff --git a/docs/en/doc-images/slack-connect-create-1.jpg b/docs/en/doc-images/slack-connect-create-1.jpg new file mode 100644 index 0000000000..07350e22ef Binary files /dev/null and b/docs/en/doc-images/slack-connect-create-1.jpg differ diff --git a/docs/en/doc-images/slack-connect-create.jpg b/docs/en/doc-images/slack-connect-create.jpg new file mode 100644 index 0000000000..bf725acfc8 Binary files /dev/null and b/docs/en/doc-images/slack-connect-create.jpg differ diff --git a/docs/en/doc-images/slack-connect.jpg b/docs/en/doc-images/slack-connect.jpg new file mode 100644 index 0000000000..02c7c73003 Binary files /dev/null and b/docs/en/doc-images/slack-connect.jpg differ diff --git a/docs/en/doc-images/slack-create-app-bot.jpg b/docs/en/doc-images/slack-create-app-bot.jpg new file mode 100644 index 0000000000..802b4febf6 Binary files /dev/null and b/docs/en/doc-images/slack-create-app-bot.jpg differ diff --git a/docs/en/doc-images/slack-create-app.jpg b/docs/en/doc-images/slack-create-app.jpg new file mode 100644 index 0000000000..ef12d6b29b Binary files /dev/null and b/docs/en/doc-images/slack-create-app.jpg differ diff --git a/docs/en/doc-images/slack-install-bot.jpg b/docs/en/doc-images/slack-install-bot.jpg new file mode 100644 index 0000000000..9597cd1be3 Binary files /dev/null and b/docs/en/doc-images/slack-install-bot.jpg differ diff --git a/docs/en/doc-images/specification.jpg b/docs/en/doc-images/specification.jpg new file mode 100644 index 0000000000..12e8680d97 Binary files /dev/null and b/docs/en/doc-images/specification.jpg differ diff --git a/docs/en/doc-images/todo-demo.png b/docs/en/doc-images/todo-demo.png new file mode 100644 index 0000000000..8d38230e7a Binary files /dev/null and b/docs/en/doc-images/todo-demo.png differ diff --git a/docs/en/doc-images/upload.png b/docs/en/doc-images/upload.png new file mode 100644 index 0000000000..2a5ba2b4ec Binary files /dev/null and b/docs/en/doc-images/upload.png differ diff --git a/docs/en/doc-images/use-injector.png b/docs/en/doc-images/use-injector.png new file mode 100644 index 0000000000..b5c3e2576c Binary files /dev/null and b/docs/en/doc-images/use-injector.png differ diff --git a/docs/en/doc-images/web-ide-index.png b/docs/en/doc-images/web-ide-index.png new file mode 100644 index 0000000000..29ec5c0f65 Binary files /dev/null and b/docs/en/doc-images/web-ide-index.png differ diff --git a/docs/en/doc-images/website-hosting.png b/docs/en/doc-images/website-hosting.png new file mode 100644 index 0000000000..a462ba28dd Binary files /dev/null and b/docs/en/doc-images/website-hosting.png differ diff --git a/docs/en/guide/cases/index.md b/docs/en/guide/cases/index.md new file mode 100644 index 0000000000..bc4ad4196a --- /dev/null +++ b/docs/en/guide/cases/index.md @@ -0,0 +1,3 @@ +## Work in progress, coming soon + +> TODO diff --git a/docs/en/guide/cli/index.md b/docs/en/guide/cli/index.md new file mode 100644 index 0000000000..b9b07533df --- /dev/null +++ b/docs/en/guide/cli/index.md @@ -0,0 +1,204 @@ +--- +title: laf-cli Command Line Tool +--- + +# {{ $frontmatter.title }} + +## Introduction + +`laf-cli` enables you to sync your web development with the web platform, allowing you to be more efficient using your familiar development tools. + +Let's first take a look at all the operations provided by the cli. +![](../../doc-images/cli-mind.png) + +## Installation + +```shell +# Requires node version >= 16 +npm i laf-cli -g +``` + +The main function of the cli is to integrate the operations on the laf web platform into the command line. Let's demonstrate each operation based on the web platform's operations. + +## Login + +To perform the login operation, we need to obtain our Personal Access Token (PAT). + +![](../../doc-images/creat-token.png) + +By default, it logs in to `laf.run`. If you want to log in to `laf.dev` or a privately deployed instance of laf, or another `laf.run` account, you can add a user: + +```shell +laf user add dev -r https://laf.dev +laf user switch dev +laf user list +laf login [pat] +``` + +### Logout + +```shell +laf logout +``` + +## App + +After logging in on the web platform, we will see our app list. To view the app list in the cli, simply execute the following command. + +```shell +laf app list +``` + +### Initialize an App + +To initialize an app, we need the appid, which can be obtained from the web platform homepage. +Here's a brief explanation: initializing an app means generating template files in the directory where you run this command. By default, the template files are empty. If you want to sync the web platform's content, add `-s` flag. +::: tip +It is recommended to try this command in an empty directory. +::: + +```shell +laf app init [appid] +``` + +## Dependencies + +We can pull the dependencies from the web platform to the local environment using the `pull` command, and then run `npm i` to install them. + +```shell +laf dep pull +``` + +If we want to add a dependency, we can use `add`. Note that adding a dependency here means adding it both on the web platform and locally. After adding, you can use `npm i` to use it. + +```shell +laf dep add [dependencyName] +``` + +If our dependency file, or the entire local files, are copied from elsewhere, we can use the `push` command to install all the dependencies in the `dependency.yaml` file to the web platform. + +```shell +laf dep push +``` + +## Cloud Functions + +Create a new cloud function. This command creates the cloud function both locally and on the web platform. + +```shell + laf func create [funcName] +``` + +Delete a cloud function. Same as creating, deleting a cloud function is performed both locally and on the web platform. + +```shell +laf func del [funcName] +``` + +List cloud functions. + +```shell +laf func list +``` + +Pull the web platform's cloud function code to the local environment. + +```shell +laf func pull [funcName] +``` + +Push the local cloud function code to the web platform. + +```shell +laf func push [funcName] +``` + +Execute a cloud function. The execution result will be printed on the command line, while the logs need to be viewed on the web platform. + +```shell +laf func exec [funcName] +``` + +## Storage + +List buckets. + +```shell +laf storage list +``` + +Create a new bucket. + +```shell +laf storage create [bucketName] +``` + +Delete a bucket. + +```shell +laf storage del [bucketName] +``` + +Update bucket permissions. + +```shell +laf storage update [bucketName] +``` + +Download files from a bucket to the local environment. + +```shell +laf storage pull [bucketName] [outPath] +``` + +Upload local files to a bucket. + +```shell +laf storage push [bucketName] [inPath] +``` + +## Access Policies + +List all access policies. + +```shell +laf policy list +``` + +Pull access policies to the local environment. The `policyName` parameter is optional. If not provided, it pulls all policies. + +```shell +laf policy pull [policyName] +``` + +Push access policies to the web platform. The `policyName` parameter is optional. If not provided, it pushes all policies. + +```shell +laf policy push [policyName] +``` + +## Website Hosting + +View the hosting list. + +```shell +laf website list +``` + +Enable website hosting, this command enables website hosting for [bucketName]. + +```shell +laf website create [bucketName] +``` + +Disable website hosting, this command disables website hosting for [bucketName]. + +```shell +laf website del [bucketName] +``` + +Customize domain name, this command sets a custom domain for the [bucketName] with website hosting enabled. + +```shell +laf website custom [bucketName] [domain] +``` diff --git a/docs/en/guide/client-sdk/index.md b/docs/en/guide/client-sdk/index.md new file mode 100644 index 0000000000..9efd137a8d --- /dev/null +++ b/docs/en/guide/client-sdk/index.md @@ -0,0 +1,157 @@ +--- +title: laf-client-sdk +--- + +# {{ $frontmatter.title }} + +## Introduction + +laf provides frontend with `laf-client-sdk` for use in JavaScript runtime environments. + +## Installation + +```bash +npm install laf-client-sdk +``` + +## Usage Example + +```js +import { Cloud } from "laf-client-sdk"; + +const cloud = new Cloud({ + // Replace APPID with your corresponding APPID here + baseUrl: "https://APPID.laf.run", + // This is the entry point for accessing database proxies, leave empty if not needed + dbProxyUrl: "/proxy/app", + // Token to be included in requests, can be empty + getAccessToken: () => localStorage.getItem("access_token"), +}); +``` + +## Parameters + +`baseUrl`: Laf application link in the format of `https://APPID.laf.run`, where APPID is the appid of your Laf application. + +`dbProxyUrl`: Database access strategy entry, starting with `/proxy/` followed by the name of the newly created strategy. Leave this parameter empty if no database operations are required. + +`getAccessToken`: Token to be included in requests. The token is a JWT token. Can be empty if no permission is involved. + +`environment`: Currently compatible with three types of environments `wxmp` `uniapp` `h5`. `wxmp` represents Wechat Mini Program. + +## Usage in Wechat Mini Program + +```js +import { Cloud } from "laf-client-sdk"; + +const cloud = new Cloud({ + baseUrl: "https://APPID.laf.run", + dbProxyUrl: "/proxy/app", + getAccessToken: () => wx.getStorageSync('access_token'), + environment: "wxmp", +}); +``` + +::: warning +When using NPM dependencies in Wechat Mini Program, you need to `Build NPM`. +::: + +### Building Wechat Mini Program with `Typescript` + +1. Initialize NPM in the terminal by executing `npm init -y` in the mini program project folder. + +2. Install the client SDK by executing `npm i laf-client-sdk` in the mini program project folder. + +3. Modify project.config.json + + Add the following under `setting`: + + ```typescript + "packNpmManually": true, + "packNpmRelationList": [ + { + "packageJsonPath": "./package.json", + "miniprogramNpmDistDir": "miniprogram/" + } + ] + ``` + +4. Build NPM by clicking on "Tools"-"Build npm" in the Wechat developer tool. + +5. Call the functions of the SDK in the page. + +### Building Wechat Mini Program with `JavaScript` + +1. Initialize NPM in the terminal by executing `npm init -y` in the mini program project folder. + +2. Install the client SDK by executing `npm i laf-client-sdk` in the mini program project folder. + +3. Build NPM by clicking on "Tools"-"Build npm" in the Wechat developer tool. + +6. Call the functions of the SDK in the page. + +## Usage in UNI-APP + +```js +import { Cloud } from "laf-client-sdk"; + +const cloud = new Cloud({ + baseUrl: "https://APPID.laf.run", + dbProxyUrl: "/proxy/app", + getAccessToken: () => uni.getStorageSync("access_token"), + environment: "uniapp", +}); +``` + +## Usage in H5 + +```js +import { Cloud } from "laf-client-sdk"; + +const cloud = new Cloud({ + baseUrl: "https://APPID.laf.run", + dbProxyUrl: "/proxy/app", + getAccessToken: () => localStorage.getItem("access_token"), + environment: "h5", +}); +``` + +## Calling Cloud Functions + +::: tip +`laf-client-sdk` only supports POST requests for calling cloud functions. +::: + +```typescript +import { Cloud } from "laf-client-sdk"; + +const cloud = new Cloud({ + baseUrl: "https://APPID.laf.run", + dbProxyUrl: "/proxy/app", + getAccessToken: () => localStorage.getItem("access_token"), +}); + +// Call the getCode cloud function and pass in parameters +const res = await cloud.invoke("getCode", { phone: phone.value }); +``` + +## Database Operation + +:::tip +Using `laf-client-sdk`, we can operate the database just like in cloud functions. For more operation details, please refer to the [Cloud Database](/guide/db/) section. +Also, you need to configure the corresponding access policy. +::: + +```typescript +import { Cloud } from "laf-client-sdk"; + +const cloud = new Cloud({ + baseUrl: "https://APPID.laf.run", + dbProxyUrl: "/proxy/app", // Database access policy + getAccessToken: () => localStorage.getItem("access_token"), +}); +const db = cloud.database() + +// Get data from the user table. +const res = await db.collection('user').get() +``` \ No newline at end of file diff --git a/docs/en/guide/db/add.md b/docs/en/guide/db/add.md new file mode 100644 index 0000000000..1dee6464bf --- /dev/null +++ b/docs/en/guide/db/add.md @@ -0,0 +1,64 @@ +--- +title: Add Data +--- + +# {{ $frontmatter.title }} + +In the Laf function library, adding data is very simple. More formally, it is called inserting a document. Here are the ways to insert a single document and insert multiple documents. + +At the same time, Laf Cloud Database is `Schema Free`, which means you can insert any fields and data types. + +::: tip +Using the method of adding data with `cloud.database()` does not allow for customizing the ID. All added data will have an automatically generated ID, which is of type `string`. + +Using the `mongodb native syntax`, you can customize the ID or have it automatically generated, with the generated ID being of type `ObjectId`. +::: + +## Inserting a Single Document + +The following example adds a record to the `user` collection. + +```typescript +import cloud from '@lafjs/cloud' +const db = cloud.database() + +export default async function (ctx: FunctionContext) { + // Add a record to the user collection + const res = await db.collection('user').add({ name: "jack" }) + console.log(res) +} +``` + +## Batch Adding Documents + +Of course, we can also add multiple records at once by passing in an additional object `{ multi: true }`. + +```typescript +const list = [ + { name: "jack" }, + { name: "rose" } +] +// Add multiple records to the user collection +const res = await db.collection('user').add(list, { multi: true }) +console.log(res) +``` + +## MongoDB Native Syntax + +```typescript +import cloud from '@lafjs/cloud' +const db = cloud.mongo.db + +export async function main(ctx: FunctionContext) { + // Insert a single document into the user collection + const result1 = db.collection('user').insertOne({ name: "jack" }) + console.log(result1) + // Insert multiple documents into the user collection + const documents = [ + { name: "Jane Doe", age: 25 }, + { name: "Bob Smith", age: 40 } + ]; + const result2 = db.collection('user').insertMany(documents); + console.log(result2) +} +``` \ No newline at end of file diff --git a/docs/en/guide/db/command.md b/docs/en/guide/db/command.md new file mode 100644 index 0000000000..3bd02b91c0 --- /dev/null +++ b/docs/en/guide/db/command.md @@ -0,0 +1,1141 @@ +--- +title: Database Operators +--- + +# {{ $frontmatter.title }} + +Laf cloud functions support various database operators for executing queries, updates, deletions, and other operations. + +## Initialization Operators + +```typescript +const db = cloud.database() +const _ = db.command +// Use _ for subsequent operations +``` + +## Query - Logical Operators + +### and + +The `and` operator is used to represent the logical "and" relationship, which means that multiple query conditions need to be satisfied at the same time. + +#### Usage + +There are two ways to use `and`: + +**1. Used in the root query condition** + +In this case, you need to pass in multiple query conditions to represent multiple complete query conditions that need to be satisfied at the same time. + +```js +const _ = db.command +let res = await db.collection('todo').where(_.and([ + { + progress: _.gt(50) + }, + { + tags: 'cloud' + } +])).get() +``` + +However, using `and` in the above query conditions is unnecessary because the implicitly combined fields of the passed-in object already represent the "and" relationship. The above conditions are equivalent to the more concise syntax below: + +```js +const _ = db.command +let res = await db.collection('todo').where({ + progress: _.gt(50), + tags: 'cloud' +}).get() +``` + +The explicit use of `and` is usually required when there are cross-field or cross-operation conditions. + +**2. Used in field query conditions** + +You need to pass in multiple query operators or constants to indicate that the field needs to satisfy or match the given conditions. + +For example, the following code represents "progress field value greater than 50 and less than 100" using the prefix notation: + +```js +const _ = db.command +let res = await db.collection('todo').where({ + progress: _.and(_.gt(50), _.lt(100)) +}).get() +``` + +The same condition can also be represented using the postfix notation: + +```js +const _ = db.command +let res = await db.collection('todo').where({ + progress: _.gt(50).and(_.lt(100)) +}).get() +``` + +Note that `Command` can also directly chain other `Command` methods by default, which represents the logical "and" operation between multiple `Command`s. Therefore, the above code can be further simplified as follows: + +```js +const _ = db.command +let res = await db.collection('todo').where({ + progress: _.gt(50).lt(100) +}).get() +``` + +### or + +The `or` operator is used to represent the logical "or" relationship, which means that multiple query filtering conditions need to be satisfied at the same time. The `or` directive has two uses: performing "or" operations on field values, and performing "or" operations across fields. + +#### "or" operation on field values + +The "or" operation on field values means that a specified field value can be one of multiple values. + +For example, to filter out todos with a progress greater than 80 or less than 20: + +Streaming syntax: + +```js +const _ = db.command +let res = await db.collection('todo').where({ + progress: _.gt(80).or(_.lt(20)) +}).get() +``` + +Prefix notation: + +```js +const _ = db.command +let res = await db.collection('todo').where({ + progress: _.or(_.gt(80), _.lt(20)) +}).get() +``` + +The prefix notation can also accept an array: + +```js +const _ = db.command +let res = await db.collection('todo').where({ + progress: _.or([_.gt(80), _.lt(20)]) +}).get() +``` + +#### "or" operation across fields + +The "or" operation across fields means that the condition is satisfied if any of the conditions in the `where` statements are met. + +For example, to filter out todos with a progress greater than 80 or marked as completed: + +```js +const _ = db.command +let res = await db.collection('todo').where(_.or([ + { + progress: _.gt(80) + }, + { + done: true + } +])).get() +``` + +### not + +The `not` operator is used to represent the logical "not" relationship, which means that a specified condition needs to be unsatisfied. + +#### Example + +For example, to filter out todos with a progress not equal to 100: + +```js +const _ = db.command +let res = await db.collection('todo').where({ + progress: _.not(_.eq(100)) +}).get() +``` + +The `not` operator can also be used in combination with other logical directives, including `and`, `or`, `nor`, and `not`. For example, using `or`: + +```js +const _ = db.command +let res = await db.collection('todo').where({ + progress: _.not(_.or([_.lt(50), _.eq(100)])) +}).get() +``` + +### nor + +The `nor` operator is used to represent the logical "nor" relationship, which means that none of the specified conditions should be satisfied. If there is no corresponding field in the record, it is considered to satisfy the condition by default. + +#### Example 1 + +Filter todos with progress not less than 20 and not greater than 80: + +```js +const _ = db.command +let res = await db.collection('todo').where({ + progress: _.nor([_.lt(20), _.gt(80)]) +}).get() +``` + +The above query will also filter out records that do not have the `progress` field. If you want to require the existence of the `progress` field, you can use the `exists` command: + +```js +const _ = db.command +let res = await db.collection('todo').where({ + progress: _.exists(true).nor([_.lt(20), _.gt(80)]) + // Equivalent to the following non-chainable syntax: + // progress: _.exists(true).and(_.nor([_.lt(20), _.gt(80)])) +}).get() +``` + +#### Example 2 + +Filter records where `progress` is not less than 20 and `tags` array does not contain the string "miniprogram": + +```js +const _ = db.command +db.collection('todo').where(_.nor([{ + progress: _.lt(20), +}, { + tags: 'miniprogram', +}])).get() +``` + +The above query will filter records that satisfy one of the following conditions: + +1. `progress` is not less than 20 and `tags` array does not contain the string "miniprogram" +3. `progress` is not less than 20 and `tags` field does not exist +5. `progress` field does not exist and `tags` array does not contain the string "miniprogram" +7. `progress` is not less than 20 and `tags` field does not exist + +If you want to require the existence of both the `progress` and `tags` fields, you can use the `exists` command: + +```js +const _ = db.command +let res = await db.collection('todo').where( + _.nor([{ + progress: _.lt(20), + }, { + tags: 'miniprogram', + }]) + .and({ + progress: _.exists(true), + tags: _.exists(true), + }) +).get() +``` + +## Query Comparison Operators + +### eq + +Query filter condition, represents a field that is equal to a specific value. The `eq` command accepts a literal, which can be a number, boolean, string, object, array, or Date. + +#### Usage + +For example, to filter out all articles posted by oneself using the object notation: + +```js +const openID = 'xxx' +let res = await db.collection('articles').where({ + _openid: openID +}).get() +``` + +You can also use the command notation: + +```js +const _ = db.command +const openID = 'xxx' +let res = await db.collection('articles').where({ + _openid: _.eq(openid) +}).get() +``` + +Note that the `eq` command provides more flexibility than the object notation and can be used to represent a field that is equal to an object, for example: + +```js +// This syntax represents a match where stat.publishYear == 2018 && stat.language == 'zh-CN' +let res = await db.collection('articles').where({ + stat: { + publishYear: 2018, + language: 'zh-CN' + } +}).get() + +// This syntax represents stat equal to { publishYear: 2018, language: 'zh-CN' } +const _ = db.command +let res = await db.collection('articles').where({ + stat: _.eq({ + publishYear: 2018, + language: 'zh-CN' + }) +}).get() +``` + +### neq + +Query filter condition, represents a field that is not equal to a specific value. The `neq` command accepts a literal, which can be a number, boolean, string, object, array, or Date. + +#### Usage + +Represents a field that is not equal to a specific value, opposite to [eq](#eq). + +### lt + +Query filter operator, represents a field that is less than a specific value. Can pass a `Date` object for date comparison. + +#### Example + +Find todos with progress less than 50: + +```js +const _ = db.command +let res = await db.collection('todos').where({ + progress: _.lt(50) +}) +.get() +``` + +### lte + +Query filter operator, represents a field that is less than or equal to a specific value. Can pass a `Date` object for date comparison. + +#### Example + +Find todos with progress less than or equal to 50: + +```js +const _ = db.command +let res = await db.collection('todos').where({ + progress: _.lte(50) +}) +.get() +``` + +### gt + +Query filter operator, represents a field that is greater than a specific value. Can pass a `Date` object for date comparison. + +#### Example + +Find todos with progress greater than 50: + +```js +const _ = db.command +let res = await db.collection('todos').where({ + progress: _.gt(50) +}) +.get() +``` + +### gte + +Query filter operator, represents a field that is greater than or equal to a specific value. Can pass a `Date` object for date comparison. + +#### Example + +Find todos with progress greater than or equal to 50: + +```js +const _ = db.command +let res = await db.collection('todos').where({ + progress: _.gte(50) +}) +.get() +``` + +### in + +The query filter operator that requires the value to be within the given array. + +#### Example code + +Find todos with progress of 0 or 100. + +```js +const _ = db.command +let res = await db.collection('todos').where({ + progress: _.in([0, 100]) +}) +.get() +``` + +### nin + +The query filter operator that requires the value not to be within the given array. + +#### Example code + +Find todos with progress that is not 0 or 100. + +```js +const _ = db.command +let res = await db.collection('todos').where({ + progress: _.nin([0, 100]) +}) +.get() +``` + +## Query Field Operators + +### exists + +Check if the field exists. True for existence, false for non-existence. + +#### Example code + +Find records with the tags field present. + +```js +const _ = db.command +let res = await db.collection('todos').where({ + tags: _.exists(true) +}) +.get() +``` + +### mod + +The query filter operator that requires the field, when used as the dividend, to satisfy value % divisor = remainder. + +#### Example code + +Find records where the progress field is a multiple of 10. + +```js +const _ = db.command +let res = await db.collection('todos').where({ + progress: _.mod(10, 0) +}) +.get() +``` + +## Array Operators + +### all + +Array query operator. Used for querying array fields, requiring that the array field contains all elements of the given array. + +#### Example code 1: Regular array + +Find records with the tags array field containing both "cloud" and "database". + +```js +const _ = db.command +let res = await db.collection('todos').where({ + tags: _.all(['cloud', 'database']) +}) +.get() +``` + +#### Example code 2: Object array + +If the array elements are objects, you can use `_.elemMatch` to match partial fields of the object. + +Assume that the places field is defined as follows: + +```js +{ + "type": string + "area": number + "age": number +} +``` + +Find records where the array field contains at least one element satisfying "area > 100 and age < 2" and another element satisfying "type is mall and age > 5". + +```js +const _ = db.command +let res = await db.collection('todos').where({ + places: _.all([ + _.elemMatch({ + area: _.gt(100), + age: _.lt(2), + }), + _.elemMatch({ + type: 'mall', + age: _.gt(5), + }), + ]), +}) +.get() +``` + +### elemMatch + +Used for querying array fields, requiring that the array contains at least one element satisfying all conditions given by `elemMatch`. + +#### Example code: Array of objects + +Assume the collection example data is as follows: + +```js +{ + "_id": "a0", + "city": "x0", + "places": [{ + "type": "garden", + "area": 300, + "age": 1 + }, { + "type": "theatre", + "area": 50, + "age": 15 + }] +} +``` + +Find records where the `places` array field contains at least one element satisfying "area > 100 and age < 2". + +```js +const _ = db.command +let res = await db.collection('todos').where({ + places: _.elemMatch({ + area: _.gt(100), + age: _.lt(2), + }) +}) +.get() +``` + +*Note*: If you don't use `elemMatch` and directly specify the conditions as follows, it means that at least one element in the `places` array field has an `area` field greater than 100 and at least one element in the `places` array field has an `age` field less than 2: + +```js +const _ = db.command +let res = await db.collection('todos').where({ + places: { + area: _.gt(100), + age: _.lt(2), + } +}) +.get() +``` + +#### Example code: Array of primitive data types + +Assume the collection example data is as follows: + +```js +{ + "_id": "a0", + "scores": [60, 80, 90] +} +``` + +Find records where the `scores` array field contains at least one element satisfying "greater than 80 and less than 100". + +```js +const _ = db.command +let res = await db.collection('todos').where({ + scores: _.elemMatch(_.gt(80).lt(100)) +}) +.get() +``` + +### size + +The query filter operator used for querying array fields, requiring the array length to be equal to the given value. + +#### Example + +Find all records with a tags array field length of 2. + +```js +const _ = db.command +let res = await db.collection('todos').where({ + places: _.size(2) +}) +.get() +``` + +## Geospatial Operators + +### geoNear + +Find records in order of proximity to the given point. + +#### Index Requirement + +A geospatial index must be created on the query field. + +#### Example Code + +Find records within a range of 1 km to 5 km from the given location. + +```js +const _ = db.command +let res = await db.collection('restaurants').where({ + location: _.geoNear({ + geometry: new db.Geo.Point(113.323809, 23.097732), + minDistance: 1000, + maxDistance: 5000, + }) +}).get() +``` + +### geoWithin + +Find records with field values within a specified area, without sorting. The specified area must be a polygon or a collection of polygons. + +#### Index Requirements + +A geospatial index is required for the query field. + +#### Example Code 1: Given a Polygon + +```js +const _ = db.command +const { Point, LineString, Polygon } = db.Geo +let res = await .collection('restaurants').where({ + location: _.geoWithin({ + geometry: new Polygon([ + new LineString([ + new Point(0, 0), + new Point(3, 2), + new Point(2, 3), + new Point(0, 0) + ]) + ]), + }) +}).get() +``` + +#### Example Code 2: Given a Circle + +You can construct a circle using `centerSphere` instead of `geometry`. + +The value of `centerSphere` is defined as: `[ [longitude, latitude], radius ]`. + +The radius should be in radians. For example, if you need a radius of 10 km, divide the distance by the Earth's radius of 6378.1 km to get the number. + +```js +const _ = db.command +let res = await db.collection('restaurants').where({ + location: _.geoWithin({ + centerSphere: [ + [-88, 30], + 10 / 6378.1, + ] + }) +}).get() +``` + +### geoIntersects + +Find records that intersect with the given geospatial shapes. + +#### Index Requirements + +A geospatial index is required for the query field. + +#### Example Code: Find Records Intersecting with a Polygon + +```js +const _ = db.command +const { Point, LineString, Polygon } = db.Geo +let res = await db.collection('restaurants').where({ + location: _.geoIntersects({ + geometry: new Polygon([ + new LineString([ + new Point(0, 0), + new Point(3, 2), + new Point(2, 3), + new Point(0, 0) + ]) + ]), + }) +}).get() +``` + +## Query Expression Operators + +### expr + +A query operator used to introduce an aggregation expression in a query statement. It takes one argument, which must be an aggregation expression. + +#### Usage + +1. `expr` can be used to introduce an aggregation expression in the `match` stage of an aggregation pipeline. +2. If the `match` stage is within a `lookup` stage, the `expr` expression can use variables defined in the `let` parameter of the `lookup` stage. For specific examples, refer to the example on "Specifying multiple join conditions" in the documentation for `lookup`. +3. `expr` can be used to introduce an aggregation expression in a regular query statement (`where`). + +#### Example Code 1: Compare two fields in the same record + +Assuming the data structure of the `items` collection is as follows: + +```js +{ + "_id": string, + "inStock": number, // Stock quantity + "ordered": number // Quantity ordered +} +``` + +Find records where the quantity ordered is greater than the stock quantity: + +```js +const _ = db.command +const $ = _.aggregate +let res = await db.collection('items').where(_.expr($.gt(['$ordered', '$inStock']))).get() +``` + +#### Example Code 2: Combine with conditional statements + +Assuming the data structure of the `items` collection is as follows: + +```json +{ + "_id": string, + "price": number +} +``` + +Assume that a discount of 20% is applied to prices less than or equal to 10, and a discount of 50% is applied to prices greater than 10. Query the database to return records with a discounted price less than or equal to 8: + +```js +const _ = db.command +const $ = _.aggregate +let res = await db.collection('items').where( + _.expr( + $.lt([ + $.cond({ + if: $.gte(['$price', 10]), + then: $.multiply(['$price', '0.5']), + else: $.multiply(['$price', '0.8']), + }), + 8 + ]) +)).get() +``` + +## Update Field Operators + +### set + +An update operator used to set a field to a specified value. + +#### Usage + +This method allows specifying an object as the field value, unlike passing a plain JavaScript object. + +#### Example + +```js +// The following method only updates style.color to red, instead of updating the entire style object to { color: 'red' }, it does not affect other fields in style +let res = await db.collection('todos').doc('doc-id').update({ + style: { + color: 'red' + } +}) + +// The following method updates style to { color: 'red', size: 'large' } +let res = await db.collection('todos').doc('doc-id').update({ + style: _.set({ + color: 'red', + size: 'large' + }) +}) +``` + +### remove + +Update operator used to indicate the deletion of a field. + +#### Example + +Remove the style field: + +```js +const _ = db.command +let res = await db.collection('todos').doc('todo-id').update({ + style: _.remove() +}) +``` + +### inc + +Update operator used for atomic increment of a field. + +#### Atomic Increment + +When multiple users write simultaneously, the field is incremented for the database, and there is no situation where a later writer overwrites an earlier one. + +#### Example + +Increment the progress of a todo by 10: + +```js +const _ = db.command +let res = await db.collection('todos').doc('todo-id').update({ + progress: _.inc(10) +}) +``` + +### mul + +Update operator used for atomic multiplication of a field. + +#### Atomic Multiplication + +When multiple users write simultaneously, the field is multiplied for the database, and there is no situation where a later writer overwrites an earlier one. + +#### Example + +Multiply the progress of a todo by 10: + +```js +const _ = db.command +let res = await db.collection('todos').doc('todo-id').update({ + progress: _.mul(10) +}) +``` + +### min + +Update operator used to update a field with a given value only if the value is smaller than the current value of the field. + +#### Example + +If the progress field > 50, update it to 50: + +```js +const _ = db.command +let res = await db.collection('todos').doc('doc-id').update({ + progress: _.min(50) +}) +``` + +### max + +Update operator used to update a field with a given value only if the value is larger than the current value of the field. + +#### Example + +If the progress field < 50, update it to 50: + +```js +const _ = db.command +let res = await db.collection('todos').doc('doc-id').update({ + progress: _.max(50) +}) +``` + +### rename + +Update operator used to rename a field. If you need to rename a deeply nested field, use dot notation. Cannot rename fields nested in arrays. + +#### Example 1: Rename top-level field + +```js +const _ = db.command +let res = await db.collection('todos').doc('doc-id').update({ + progress: _.rename('totalProgress') +}) +``` + +#### Example 2: Rename nested field + +```js +const _ = db.command +let res = await db.collection('todos').doc('doc-id').update({ + someObject: { + someField: _.rename('someObject.renamedField') + } +}) +``` + +or: + +```js +const _ = db.command +let res = await db.collection('todos').doc('doc-id').update({ + 'someObject.someField': _.rename('someObject.renamedField') +}) +``` + +## Update Array Operators + +### push + +Array update operator. Used to add one or more values to an array field. If the field does not exist, it will be created and set to the input value. + +#### Parameter Description + +**position** + +Must be used together with the `each` parameter. + +A non-negative number represents the position counting from the beginning of the array, starting from 0. If the value is greater than or equal to the length of the array, it will be added to the end. A negative number represents the position counting from the end of the array, e.g., -1 represents the second-to-last element. If the absolute value of a negative number is greater than or equal to the length of the array, it will be added to the beginning of the array. + +**sort** + +Must be used together with the `each` parameter. + +Given 1 for ascending order and -1 for descending order. + +If the array elements are records, use `{ : 1 | -1 }` format to specify the field based on which to sort in ascending or descending order. + +**slice** + +Must be used together with the `each` parameter. + +|Value|Description| +|:-:|:-:| +|0|Update the field to an empty array| +|Positive integer|Keep only the first n elements| +|Negative integer|Keep only the last n elements| + +#### Example 1: Add elements to the end + +```js +const _ = db.command +let res = await db.collection('todos').doc('doc-id').update({ + tags: _.push(['mini-program', 'cloud']) +}) +``` + +#### Example 2: Insert at the second position + +```js +const _ = db.command +let res = await db.collection('todos').doc('doc-id').update({ + tags: _.push({ + each: ['mini-program', 'cloud'], + position: 1, + }) +}) +``` + + +#### Example 3: Sorting + +Sort the entire array after inserting. + +```js +const _ = db.command +let res = await db.collection('todos').doc('doc-id').update({ + tags: _.push({ + each: ['mini-program', 'cloud'], + sort: 1, + }) +}) +``` + +Sort the array without inserting. + +```js +const _ = db.command +let res = await db.collection('todos').doc('doc-id').update({ + tags: _.push({ + each: [], + sort: 1, + }) +}) +``` + +If the field is an array of objects, you can sort it based on a field in the element object like this: + +```js +const _ = db.command +let res = await db.collection('todos').doc('doc-id').update({ + tags: _.push({ + each: [ + { name: 'miniprogram', weight: 8 }, + { name: 'cloud', weight: 6 }, + ], + sort: { + weight: 1, + }, + }) +}) +``` + +#### Example 4: Truncation + +Keep only the last 2 elements after inserting. + +```js +const _ = db.command +let res = await db.collection('todos').doc('doc-id').update({ + tags: _.push({ + each: ['mini-program', 'cloud'], + slice: -2, + }) +}) +``` + +#### Example 5: Insert at a specified position, then sort, and finally keep only the first 2 elements + +```js +const _ = db.command +let res = await db.collection('todos').doc('doc-id').update({ + tags: _.push({ + each: ['mini-program', 'cloud'], + position: 1, + slice: 2, + sort: 1, + }) +}) +``` + +### pop + +Array update operator. Deletes the last element of an array field. Only the last element can be deleted. + +#### Example code + +```js +const _ = db.command +let res = await db.collection('todos').doc('doc-id').update({ + tags: _.pop() +}) +``` + +### unshift + +Array update operator. Adds one or more values to the beginning of an array field. If the field is originally empty, it creates the field and sets the array to the given value. + +#### Example code + +```js +const _ = db.command +let res = await db.collection('todos').doc('doc-id').update({ + tags: _.unshift(['mini-program', 'cloud']) +}) +``` + +### shift + +Array update operator. Deletes the first element of an array field. + +#### Example code + +```js +const _ = db.command +let res = await db.collection('todos').doc('doc-id').update({ + tags: _.shift() +}) +``` + +### pull + +Array update operator. Removes all elements in the array that match a given value or query condition. + +#### Example code 1: Remove based on constant value + +```js +const _ = db.command +let res = await db.collection('todos').doc('doc-id').update({ + tags: _.pull('database') +}) +``` + +#### Example code 2: Remove based on query condition + +```js +const _ = db.command +let res = await db.collection('todos').doc('doc-id').update({ + tags: _.pull(_.in(['database', 'cloud'])) +}) +``` + +#### Example code 3: Remove based on query condition when the field is an array of objects + +Assume the elements in the `places` array have the following structure: + +```json +{ + "type": string + "area": number + "age": number +} +``` + +```js +const _ = db.command +let res = await db.collection('todos').doc('doc-id').update({ + places: _.pull({ + area: _.gt(100), + age: _.lt(2), + }) +}) +``` + +#### Example code 4: Remove based on query condition when the field is an array of objects with nested objects + +Assume the elements in the `cities` array have the following structure: + +```js +{ + "name": string + "places": Place[] +} +``` + +The `Place` structure is as follows: + +```js +{ + "type": string + "area": number + "age": number +} +``` + +You can use `elemMatch` to match the nested array field `places`. + +```js +const _ = db.command +let res = await db.collection('todos').doc('doc-id').update({ + cities: _.pull({ + places: _.elemMatch({ + area: _.gt(100), + age: _.lt(2), + }) + }) +}) +``` + +### pullAll + +Array update operator. Removes all elements in the array that match a given value. Unlike `pull`, it can only specify constant values, and the input must be an array. + +#### Example Code: Remove by Constant Matching + +Remove all strings 'database' and 'cloud' from tags. + +```js +const _ = db.command +let res = await db.collection('todos').doc('doc-id').update({ + tags: _.pullAll(['database', 'cloud']) +}) +``` + +### addToSet + +Array update operator. Atomic operation. Given one or more elements, add them to the array if they don't already exist. + +#### Example Code 1: Add an element + +If the tags array does not contain 'database', add it. + +```js +const _ = db.command +let res = await db.collection('todos').doc('doc-id').update({ + tags: _.addToSet('database') +}) +``` + +#### Example Code 2: Adding Multiple Elements + +You need to pass in an object with a field `each`, whose value is an array, where each element represents the element to be added. + +```js +const _ = db.command +let res = await db.collection('todos').doc('doc-id').update({ + tags: _.addToSet({ + $each: ['database', 'cloud'] + }) +}) +``` \ No newline at end of file diff --git a/docs/en/guide/db/del.md b/docs/en/guide/db/del.md new file mode 100644 index 0000000000..a05d4e2d43 --- /dev/null +++ b/docs/en/guide/db/del.md @@ -0,0 +1,37 @@ +--- +title: Delete Data +--- + +# {{ $frontmatter.title }} + +Laf Cloud Database provides the ability to delete data, including deleting documents and collections. It supports both single deletion and batch deletion. + +## Deleting a Single Document by Specifying Conditions + +We can use `where` and various advanced commands in conjunction with `remove` to delete a single document. + +```js + // Delete a record with name "jack" from the user collection + const res = await db.collection('user').where({ name: "jack" }).remove() + console.log(res) +``` + +## Deleting Multiple Documents by Specifying Conditions + +The above example can only delete one piece of data at a time. If you want to delete multiple documents, add `{multi: true}` to `remove()`. + +```js +// Delete all records in the user collection with the name "jack" +await db.collection('user') +.where({ name: "jack" }) +.remove({ multi: true }) +``` + +## Clearing a Collection (Dangerous Operation) + +If we want to remove all data from a collection, we can do it like this. + +```js +// Clear the user collection +await db.collection('user').remove({ multi: true }) +``` \ No newline at end of file diff --git a/docs/en/guide/db/find.md b/docs/en/guide/db/find.md new file mode 100644 index 0000000000..877bfce76d --- /dev/null +++ b/docs/en/guide/db/find.md @@ -0,0 +1,608 @@ +--- +title: Query Data +--- + +# {{ $frontmatter.title }} + +Laf Cloud Database supports querying data with different conditions and processing the query results. This document will illustrate how to execute queries in cloud functions using `cloud.database()` through examples. + +The data query operation mainly supports `where()`, `limit()`, `skip()`, `orderBy()`, `field()`, `get()`, `getOne()`, `count()`, etc. + +Including: + +[[toc]] + +## Get All Records + +::: tip +You can set query conditions with `where` and specify the number of items to display with `limit`. +::: + +```typescript +import cloud from '@lafjs/cloud' +// Get the database reference +const db = cloud.database() + +export async function main(ctx: FunctionContext) { + // Use the get method to send a query request, without where it will query all data by default with a maximum of 100 records + const result1 = await db.collection('user').get() + console.log(result1) + + // Use the get method to send a query request with where condition + const result2 = await db.collection('user').where({ + name: 'laf' + }).get() + console.log(result2) + + // Use the get method to send a query request to get more data at once, with a maximum of 1000 records + const result3 = await db.collection('user').limit(1000).get() + console.log(result3) +} +``` + +Operations like `where()`, `limit()`, `skip()`, `orderBy()`, `field()` can be used before `get()`. They will be explained one by one below. + +## Get One Record + +If we only have one record to query, we can also use the `getOne` method. The difference between `getOne` and `get` is that `getOne` can only retrieve one record, and the data format is an object. + +```typescript +import cloud from '@lafjs/cloud' +// Get the database reference +const db = cloud.database() + +export async function main(ctx: FunctionContext) { + const res = await db.collection('user').getOne() + console.log(res) +// Result of getOne: +// { +// ok: true, +// data: { _id: '641d21992de2b789c963e5e0', name: 'jack' }, +// requestId: undefined +// } + + const res = await db.collection('user').get() + console.log(res) +// Result of get: +// { +// data: [ { _id: '641d22292de2b789c963e5fd', name: 'jack' } ], +// requestId: undefined, +// ok: true +// } +} +``` + +Operations like `where()`, `limit()`, `skip()`, `orderBy()`, `field()` can be used before `getOne()`. They will be explained one by one below. + +## Add Query Conditions + +`collection.where()` + +Set query conditions. +Where can accept an object as a parameter, indicating filtering out documents with the same key-value as the passed-in object. Multiple conditions can be filtered simultaneously. + +For example, filter out all users with the name "jack": + +```typescript +// Query records in the user collection where the name field is equal to jack +await db.collection("user").where({ + name:"jack" +}); +``` + +:::tip +Here note that `where` does not query data directly. Please refer to our examples above and add `get()` or `getOne()`. +::: + +## Query Data by ID + +`collection.doc()` + +The difference from `where` is that `doc` only filters based on _id. + +::: warning +If the document is added using cloud.database(), the _id type is string. +If the document is added using cloud.mongo.db, the _id type is generally ObjectId. +::: + +```typescript +// Query the document in the user collection with _id as '644148fd1eeb2b524dba499e' +await db.collection("user").doc('644148fd1eeb2b524dba499e'); + +// In fact, it is equivalent to filtering conditions where _id is the only condition +await db.collection("user").where({ + _id: '644148fd1eeb2b524dba499e' +}); +``` + +```typescript +// Case where the type of _id is ObjectId +import { ObjectId } from 'mongodb' // Need to import the ObjectId type at the top of the cloud function + +await db.collection("user").doc(ObjectId('644148fd1eeb2b524dba499e')); +``` + +## Advanced Query Commands + +If you want to express more complex queries, you can use advanced query commands. This requires the use of database operators. For detailed usage, please refer to [Database Operators](/guide/db/command). + +::: tip +`where` does not directly query data at the end. You need to follow it with `get()` or `getOne()`. +::: + +### gt: Field Greater Than a Specified Value + +Can be used to query fields of numeric, date, and other types. If it is string comparison, it will be compared according to lexicographical order. + +This example filters out all users whose age is greater than 18: + +```typescript +const db = cloud.database() +const _ = db.command; // Get the command here +await db.collection("user").where({ + age: _.gt(18) // Represents greater than 18 + }, +); +``` + +### gte field greater than or equal to specified value + +Can be used to query fields of numeric, date, and other types. If comparing strings, it will be compared in dictionary order. + +```typescript +const db = cloud.database() +const _ = db.command; // Get command here +await db.collection("user").where({ + age: _.gte(18) // Represents greater than or equal to 18 + }, +); +``` + +### lt field less than specified value + +Can be used to query fields of numeric, date, and other types. If comparing strings, it will be compared in dictionary order. + +### lte field less than or equal to specified value + +Can be used to query fields of numeric, date, and other types. If comparing strings, it will be compared in dictionary order. + +### eq represents field equals a certain value + +The `eq` command accepts a literal, which can be a `number`, `boolean`, `string`, `object`, or `array`. + +For example, to filter out all articles published by yourself, besides using the object way: + +```typescript +const myOpenID = "xxx"; +await db.collection("articles").where({ + _openid: myOpenID, +}); +``` + +You can also use the command: + +```typescript +const db = cloud.database() +const _ = db.command; +const myOpenID = "xxx"; +await db.collection("articles").where({ + _openid: _.eq(myOpenID), +}); +``` + +Note that the `eq` command is more flexible than the object way, as it can be used to represent cases where a field is equal to an object, for example: + +```typescript +// This syntax represents matching stat.publishYear == 2018 and stat.language == 'zh-CN' +await db.collection("articles").where({ + stat: { + publishYear: 2018, + language: "zh-CN", + }, +}); + +// This syntax represents stat object equals { publishYear: 2018, language: 'zh-CN' } +const _ = db.command; +await db.collection("articles").where({ + stat: _.eq({ + publishYear: 2018, + language: "zh-CN", + }), +}); +``` + +### neq represents field not equal to a certain value + +Field not equal to. The `neq` command accepts a literal, which can be a `number`, `boolean`, `string`, `object`, or `array`. + +For example, to filter out computers with brands other than X: + +```typescript +const _ = db.command; +await db.collection("goods").where({ + category: "computer", + type: { + brand: _.neq("X"), + }, +}); +``` + +### in field value within the given array + +For example, to filter out users with age of 18 or 20: + +```typescript +const _ = db.command; +await db.collection("user").where({ + age: _.in([18, 20]), +}); +``` + +### nin field value not within the given array + +For example, to filter out users with age not being 18 or 20: + +```typescript +const _ = db.command; +await db.collection("user").where({ + age: _.nin([8, 20]), +}); +``` + +### and represents that all specified conditions must be met at the same time + +For example, to filter out users with age greater than 18 and less than 60: + +Streaming syntax: + +```typescript +const _ = db.command; +await db.collection("user").where({ + age: _.gt(18).and(_.lt(60)), +}); +``` + +Prefix syntax: + +```typescript +const _ = db.command; +await db.collection("user").where({ + age: _.and(_.gt(18), _.lt(60)), +}); +``` + +### or represents that at least one of the specified conditions must be met + +For example, to filter out users with age equal to 18 or equal to 60: + +Streaming syntax: + +```typescript +const _ = db.command; +await db.collection("user").where({ + age: _.eq(18).or(_.eq(60)), +}); +``` + +Prefix syntax: + +```typescript +const _ = db.command; +await db.collection("user").where({ + age: _.or(_.eq(18),_.eq(60)), +}); +``` + +If you want to perform an "or" operation across fields: (for example, to filter out computers with memory of 8g or CPU of 3.2 ghz) + +```typescript +const _ = db.command; +await db.collection("goods").where( + _.or( + { + type: { + memory: _.gt(8), + }, + }, + { + type: { + cpu: 3.2, + }, + } + ) +); +``` + +### exists determines whether a field exists + +```typescript +const _ = db.command; +await db.collection("users").where( + name: _.exists(true), // Name field exists + age: _.exists(false), // Age field does not exist +); +``` + +## Regular Expression Query + +`db.RegExp` filters based on regular expressions. + +For example, the following code can filter out records where the `version` field starts with a "number + s" and ignore case sensitivity: + +```typescript +// You can directly use regular expressions +await db.collection('articles').where({ + version: /^\ds/i +}) + +// Or +await db.collection('articles').where({ + version: new db.RegExp({ + regex: '^\\ds', // Regular expression is /^\ds/, escaped as '^\\ds' + options: 'i' // i indicates ignore case + }) +}) +``` + +## Get Query Count + +`collection.count()` queries the number of documents that match the conditions. + +Parameters + +```typescript +await db.collection("goods").where({ + category: "computer", + type: { + memory: 8, + }, +}).count() +``` + +Response Parameters + +| Field | Type | Required | Description | +| --------- | ------- | -------- | ---------------------------------- | +| code | string | No | Status code, not returned if successful | +| message | string | No | Error description | +| total | Integer | No | The count result | +| requestId | string | No | Request ID for error troubleshooting | + +## Set Record Limit + +`collection.limit()` limits the number of records displayed, up to 1000. + +Parameter Explanation + +| Parameter | Type | Required | Description | +| --------- | ------- | -------- | --------------------- | +| value | Integer | Yes | Number to limit display | + +Usage Example + +```typescript +await db.collection("user").limit(1).get() +``` + +## Set Starting Position + +`collection.skip()` skips the displayed data. + +Parameter Explanation + +| Parameter | Type | Required | Description | +| --------- | ------- | -------- | ---------------------- | +| value | Integer | Yes | Data to skip displaying | + +Usage Example + +```typescript +await db.collection("user").skip(4).get() +``` + +## Pagination Query + +Combining `skip()` and `limit()` can be used for pagination queries, and `getOne()` cannot be used here. + +```typescript +import cloud from '@lafjs/cloud' +const db = cloud.database() + +export async function main(ctx: FunctionContext) { + // Number of records per page + const pageSize = 3; + // Page number + const page = 2; + const res = await db.collection('user') + .skip((page - 1) * pageSize) + .limit(pageSize) + .get() +} +``` + +## Nested Query + +If querying based on a field in an object or array: + +```typescript +import cloud from '@lafjs/cloud' +const db = cloud.database() + +export async function main(ctx: FunctionContext) { + // Query data where userInfo.name = 'Jack' and userInfo.age = 10 + // Method 1 + const result1 = await db.collection('user') + .where({ + userInfo: { + name: "Jack", + age: 10 + } + }).get() + // Method 2 + const result2 = await db.collection('user') + .where({ + 'userInfo.name': 'Jack', + 'userInfo.age': 10 + }).get() +} +``` + +For example, if the `test` collection in the database has the following data: + +```json +[ + { + "arr":[{ + "name": "item-1" + },{ + "name": "item-2" + }] + }, + { + "arr":[{ + "name": "item-3" + },{ + "name": "item-4" + }] + } +] +``` + +```typescript +import cloud from '@lafjs/cloud' +const db = cloud.database() + +export async function main(ctx: FunctionContext) { + // Query data where arr[0].name = 'item-1' + const result = await db.collection('test') + .where({ + 'arr.0.name': "item-1" + }).get() +} +``` + +```typescript +import cloud from '@lafjs/cloud' +const db = cloud.database() + +export async function main(ctx: FunctionContext) { + // Query documents where the name of an element in arr is 'item-2' + const result = await db.collection('test') + .where({ + 'arr.name': "item-2" + }).get() +} +``` + +## Sort the Results + +Use `collection.orderBy()` to sort the data before displaying it. + +Parameter Description + +| Parameter | Type | Required | Description | +|------------|--------|----------|---------------------------------| +| field | string | Yes | The field to sort by | +| orderType | string | Yes | The order of sorting, either ascending (asc) or descending (desc) | + +Usage Example + +```typescript +// Sort in ascending order by the creation time createAt +await db.collection("user").orderBy("createAt", "asc").get() +``` + +## Specify the Returning Fields + +Use `collection.field()` to only return specified fields. + +Parameter Description + +| Parameter | Type | Required | Description | +|-----------|--------|----------|--------------------------------------------------------| +| - | object | Yes | The fields to filter, pass 0 if not returning and 1 if returning | + +::: tip +Note: You can only specify the fields to return or not return, i.e. `{'a': 1, 'b': 0}` is an incorrect parameter format. The default behavior is to display the id field. +::: + +Usage Example + +```typescript +await db.collection("user").field({ age: 1 }); +``` + +Similarly, you need to add `get()` or `getOne()` afterwards to query the results. + +## with Associated Query + +Use `with` or `withOne` for associated queries, which allows you to query a collection and retrieve associated records from a specific field (can be across tables), such as querying "Class" and retrieving the "Students" within the class, or querying "Articles" and retrieving their "Authors". + +:::info +Note: The `with` and `withOne` associated queries first query the main table internally, then query the subtable, and finally complete the concatenation locally (in cloud functions or clients) before returning the results to the application developer. If you haven't used the `with` associated query yet, it is recommended to use the aggregation operation [lookup associated query](#lookup-associated-query). +::: + +### One-to-Many Relationship Query + +This is mainly used for subqueries in "one-to-many" relationships and can be used for cross-table queries. Users need to have query permissions for the subtable. + +```typescript +await const { data } = await db + .collection("article") + .with({ + query: db.collection("tag"), + localField: "id", // Main table join key, i.e. article.id + foreignField: "article_id", // Subtable join key, i.e. tag.article_id + as: "tags", // Field renaming in the query result, defaults to the subtable name + }) + .get(); +console.log(data); +// [ { id: 1, name: xxx, tags: [...] } ] +``` + +### One-to-One Relationship Query + +> Similar to the SQL left join query + +```typescript +const { data } = await db + .collection("article") + .withOne({ + query: db.collection("user"), + localField: "author_id", // Main table join key, i.e. article.id + foreignField: "id", // Subtable join key, i.e. tag.article_id + as: "author", // Field renaming in the query result, defaults to the subtable name + }) + .get(); + +console.log(data); +// [ { id: 1, name: xxx, author: {...} } ] +``` + +## Lookup Associated Query + +:::info +The lookup associated query is not a method under `collection`! + +In fact, it is a method under `aggregate`, but since the previous section mentioned that the purpose of the `with` associated query is the same as this method, I will explain it here first to avoid developers thinking that lookup cannot be used, resulting in additional adaptation costs to switch to the with associated query. +::: + +The purpose of the lookup associated query is basically the same as the `with` associated query. Here is an example similar to the `with` associated query: when querying the `article` collection, retrieve the tag labels of each record together. + +```typescript +const { data } = await db + .collection("article") + .aggregate() + .lookup({ + from: "tag", + localField: "id", // Main table join key, i.e. article.id + foreignField: "article_id", // Subtable join key, i.e. tag.article_id + as: "tags", // Field renaming in the query result, defaults to the subtable name + }) + .end(); +console.log(data); +``` + +## Group By Query + +For group by queries, please refer to the [Aggregation Operations](/guide/db/aggregate.html#bucket) document. \ No newline at end of file diff --git a/docs/en/guide/db/geo.md b/docs/en/guide/db/geo.md new file mode 100644 index 0000000000..9ca9eaf74d --- /dev/null +++ b/docs/en/guide/db/geo.md @@ -0,0 +1,202 @@ +--- +title: Geographic Information API +--- + +# {{ $frontmatter.title }} + +Note: **If you need to search for fields of type geographic location, be sure to establish a geospatial index**. + +## GEO Data Types + +### Point + +Used to represent a geographical location point, uniquely identified by longitude and latitude. This is a special data storage type. + +Signature: `Point(longitude: number, latitude: number)` + +Example: + +```js +new db.Geo.Point(longitude, latitude); +``` + +### LineString + +Used to represent a geographical path, composed of two or more `Point` lines. + +Signature: `LineString(points: Point[])` + +Example: + +```js +new db.Geo.LineString([ + new db.Geo.Point(lngA, latA), + new db.Geo.Point(lngB, latB), + // ... +]); +``` + +### Polygon + +Used to represent a polygon in geography (with or without holes), consists of one or more **closed loops** of `LineString` geometry. + +A `Polygon` consisting of a single loop is a polygon without holes, while a `Polygon` consisting of multiple loops has holes. For a `Polygon` composed of multiple loops (`LineString`), the first loop is the outer loop, and all other loops are inner loops (holes). + +Signature: `Polygon(lines: LineString[])` + +Example: + +```js +new db.Geo.Polygon([ + new db.Geo.LineString(...), + new db.Geo.LineString(...), + // ... +]) +``` + +### MultiPoint + +Used to represent a collection of multiple points `Point`. + +Signature: `MultiPoint(points: Point[])` + +Example: + +```js +new db.Geo.MultiPoint([ + new db.Geo.Point(lngA, latA), + new db.Geo.Point(lngB, latB), + // ... +]); +``` + +### MultiLineString + +Used to represent a collection of multiple geographical paths `LineString`. + +Signature: `MultiLineString(lines: LineString[])` + +Example: + +```js +new db.Geo.MultiLineString([ + new db.Geo.LineString(...), + new db.Geo.LineString(...), + // ... +]) +``` + +### MultiPolygon + +Used to represent a collection of multiple geographical polygons `Polygon`. + +Signature: `MultiPolygon(polygons: Polygon[])` + +Example: + +```js +new db.Geo.MultiPolygon([ + new db.Geo.Polygon(...), + new db.Geo.Polygon(...), + // ... +]) +``` + +## GEO Operators + +### geoNear + +Finds records near the given point, in order from closest to farthest. + +Signature: + +```js +db.command.geoNear(options: IOptions) + +interface IOptions { + geometry: Point // Geographical location of the point + maxDistance?: number // Optional. Maximum distance in meters + minDistance?: number // Optional. Minimum distance in meters +} +``` + +Example: + +```js +db.collection("user").where({ + location: db.command.geoNear({ + geometry: new db.Geo.Point(lngA, latA), + maxDistance: 1000, + minDistance: 0, + }), +}); +``` + +### geoWithin + +Finds records with field values within the specified Polygon / MultiPolygon, unsorted. + +Signature: + +```js +db.command.geoWithin(IOptions); + +interface IOptions { + geometry: Polygon | MultiPolygon; // Geographical location +} +``` + +Example: + +```js +// A closed area +const area = new Polygon([ + new LineString([ + new Point(lngA, latA), + new Point(lngB, latB), + new Point(lngC, latC), + new Point(lngA, latA), + ]), +]); + +// Search for users with the location field within this area +db.collection("user").where({ + location: db.command.geoWithin({ + geometry: area, + }), +}); +``` + +### geoIntersects + +Find records that intersect with a given geographic location geometry. + +Signature: + +```js +db.command.geoIntersects(IOptions); + +interface IOptions { + geometry: + | Point + | LineString + | MultiPoint + | MultiLineString + | Polygon + | MultiPolygon; // Geographic location +} +``` + +Example: + +```js +// A line +const line = new LineString([new Point(lngA, latA), new Point(lngB, latB)]); + +// Search for users whose location intersects with this line +db.collection("user").where({ + location: db.command.geoIntersects({ + geometry: line, + }), +}); +``` \ No newline at end of file diff --git a/docs/en/guide/db/index.md b/docs/en/guide/db/index.md new file mode 100644 index 0000000000..117a7dbc35 --- /dev/null +++ b/docs/en/guide/db/index.md @@ -0,0 +1,148 @@ +--- +title: Introduction to Cloud Database +--- + +# {{ $frontmatter.title }} + +Laf Cloud Database provides a plug-and-play database that requires no complex configuration or connection. In the cloud functions, you can use `cloud.database()` to create a new DB instance for database operations. + +Laf Cloud Database uses `MongoDB` as its underlying database, which retains the native querying methods of `MongoDB` and also encapsulates more convenient methods. + +Laf Cloud Database is a JSON-formatted document-oriented database, where each record in the database is a JSON-formatted document. Therefore, in the Laf database, a collection corresponds to a data table in MySQL, a document corresponds to a row in MySQL, and a field corresponds to a column in MySQL. + +## Basic Concepts + +### Document + +Each record in the database is a JSON-formatted document, such as: + +```typescript +{ + "username": "hello", + "password": "123456", + "extraInfo": { + "mobile": "15912345678" + } +} +``` + +### Collection + +A collection is a group of documents, with each document residing in a collection. For example, all users are stored in the `users` collection. + +```typescript +[ + { + "username": "name1", + "password": "123456", + "extraInfo": { + "mobile": "15912345678" + } + }, + { + "username": "name2", + "password": "12345678", + "extraInfo": { + "mobile": "15912345679" + } + } + ... +] +``` + +### Database + +Each Laf application has one and only one database, but a database can have multiple collections. + +![dblist](/doc-images/dblist.jpg) + +The above diagram represents that there are two collections, namely the `test` collection and the `messages` collection, in the current Laf application. + +At the same time, in the Laf `Web IDE`, you can conveniently view the entire collection list and perform simple management. + +## Data Types + +The cloud database provides the following types: + +__Common Data Types__ + +- `String`: string type, stores UTF-8 encoded strings of any length +- `Number`: number type, including integers and floating-point numbers +- `Boolean`: boolean type, including true and false +- `Date`: date type, stores dates and times +- `ObjectId`: object ID type, used to store unique identifiers for documents +- `Array`: array type, can contain any number of values, including other data types and nested arrays +- `Object`: object type, can contain any number of key-value pairs, where the value can be any data type, including other objects and nested arrays + +__Other Data Types__ + +- `Null`: acts as a placeholder, represents a field that exists but has no value +- `GeoPoint`: geographic location point +- `GeoLineStringLine`: geographic path +- `GeoPolygon`: geographic polygon +- `GeoMultiPoint`: multiple geographic location points +- `GeoMultiLineString`: multiple geographic paths +- `GeoMultiPolygon`: multiple geographic polygons + +### Date Type + +The Date type is used to represent time, accurate to milliseconds, and can be created using the built-in JavaScript Date object. + +::: warning +Note: the current server time may have timezone issues and may not be in GMT+8, when storing in the database, you can convert it to GMT+8 or use timestamps. +::: + +In cloud functions, you can directly use `new Date()` to get the current server time. + +```typescript +// Current server time +console.log(new Date()) +// Output: 2023-04-21T14:47:32.697Z + +const date = new Date(); +const timestamp = date.getTime(); +console.log(timestamp); +// Output: a timestamp in milliseconds, e.g., 1650560477427 +``` + +To get GMT+8 time: + +```typescript +// Get the GMT+8 time of the current time +const date = new Date(); +const offset = 8; // GMT+8 offset is +8 + +// Calculate the UTC time of the current time, and then add the offset to get the GMT+8 time +const utcTime = date.getTime() + (date.getTimezoneOffset() * 60 * 1000); +const beijingTime = new Date(utcTime + (offset * 60 * 60 * 1000)); +console.log(beijingTime); // Output: Date object of GMT+8 time +``` + +The following demonstrates adding a record to the `test` collection in a cloud function, and adding two fields, `createTime` and `createTimestamp`, representing the creation time. + +```typescript +import cloud from '@lafjs/cloud' +const db = cloud.database() + +export async function main(ctx: FunctionContext) { + + // Get the GMT+8 time of the current time + const date = new Date(); + const offset = 8; // GMT+8 offset is +8 + + // Calculate the UTC time of the current time, and then add the offset to get the GMT+8 time + const utcTime = date.getTime() + (date.getTimezoneOffset() * 60 * 1000); + const beijingTime = new Date(utcTime + (offset * 60 * 60 * 1000)); + console.log(beijingTime); // Output: Date object of GMT+8 time + + await db.collection('test').add({ + name: 'xiaoming', + createTime: beijingTime, + createTimestamp: date.getTime() + }) +} +``` + +### Null Type + +Null is equivalent to a placeholder that indicates a field exists but has no value. \ No newline at end of file diff --git a/docs/en/guide/db/policy.md b/docs/en/guide/db/policy.md new file mode 100644 index 0000000000..7ede046e37 --- /dev/null +++ b/docs/en/guide/db/policy.md @@ -0,0 +1,262 @@ +--- +title: Access Policies +--- + +# {{ $frontmatter.title }} + +Front-end can use [laf-client-sdk](https://github.com/labring/laf/tree/main/packages/client-sdk) to "directly connect" to the database without interacting with the server. + +Access policies are used to securely control client operations on the database. An access policy consists of multiple access rules for collections, and each collection can have access rules configured for read and write operations. + +Developers can create multiple access policies for an application to be used by different clients. + +Typically, an application can create two access policies: + +- `app` is used for access policy for user-side clients, such as App, mini-programs, H5, etc. +- `admin` is used for access policy for the application's back-end management. + +## Creating a Policy + +First, we switch to the collections page and follow the steps below to add an access policy to the `user` collection. + +:::tip +If you don't have a `user` collection yet, please create one first. +::: + +![creat-polocy](../../doc-images/creat-polocy.png) + +![add-polocy](../../doc-images/add-polocy.png) + +After completing these two steps, we have successfully created an access policy and applied it to the `user` collection. +Let's explain the rule content here: + +```js +{ + "read": true, // read permission + "count": true, // count permission + "update": false, // update permission + "remove": false, // remove permission + "add": false // add permission +} +``` + +Setting the value to `true` means allowed, while `false` means not allowed. According to the default rules we just mentioned, the front-end can directly perform `query` and `count` operations on our collection, but adding, updating, and removing operations are not allowed. +There is another thing worth noting here, which is the "Entry URL". We set the policy name to `app`, so the entry URL is `/proxy/app`. Let's demonstrate how to use it next. + +## Front-end "Direct Connection" + +Before the demonstration, let's add some data to the `user` collection. + +![polocy-db-data](../../doc-images/polocy-db-data.png) + +Now we come to the front-end project, and for this demonstration, we will use a Vue project, but the steps are similar for other projects. + +### Install the SDK First + +```bash +npm i laf-client-sdk +``` + +### Then Create a Cloud Object + +Pay attention here that we fill in an additional parameter `dbProxyUrl`, and its value is the entry URL we just mentioned: `/proxy/app`. + +```js +import { Cloud } from "laf-client-sdk"; + +const cloud = new Cloud({ + baseUrl: "https:/.laf.run", // is obtained from the application list on the homepage + getAccessToken: () => "", // We don't need authorization here, so leave it blank for now + dbProxyUrl: "/proxy/app", // Fill in the entry URL we just mentioned here +}) +``` + +After creating the cloud object, let's try sending a query directly on the front-end. + +```js +async function get() { + const res = await cloud.database().collection("user").get(); + console.log(res); +// { +// "data": [ +// { +// "_id": "641d22292de2b789c963e5fd", +// "name": "jack" +// }, +// { +// "_id": "641e87d52c9ce609db4d6047", +// "name": "rose" +// } +// ], +// "ok": true +// } +} +``` + +Now let's try counting. + +```js +async function get() { + const res = await cloud.database().collection("user").count(); + console.log(res); + // { + // "total": 2, + // "ok": true + // } +} +``` + +As you can see, we no longer need to write a cloud function for simple query and count operations; we can directly implement them on the front-end. If you want to experience `add`, `update`, or `remove` operations, you can modify the corresponding access policy to allow them, but this is a dangerous behavior. + +Access policies can also be expressed as expressions. Here is an example of an access policy for the `users` collection: +In this example, `query` is the data query condition object, and `uid === query._id` indicates that this policy only allows the current user to read their own user data. + +```json + "users" + { + "read": "uid === query._id" + } +``` + +> Note: `uid` is a field in the JWT Payload, representing the ID of the currently logged-in user. You can generate a JWT token in a cloud function, and the fields in the payload can be used in the access policy. + +## Validators + +An access rule consists of one or more validators. `"read": true` uses the `condition` validator, which takes an expression to control access permission. +The above example uses the shorthand form, and the full syntax is as follows: + +```json +{ + "read": { + "condition": true + } +} +``` + +Currently, the following validators are built-in: + +- `condition` is a condition validator that takes an expression and returns true if access is allowed. The default available objects in the expression are: + - `JWT Payloads` fields in the JWT token payload, such as `uid` in the example + - `query` data query condition + - `data` data object for update or add operations +- `data` is a data validator that takes an object and validates each field in the "data object" for update or add operations. Please refer to the following example for details. +- `query` is a validator for the query condition object, which takes an object and validates each field in the "query condition object". Please refer to the following example for details. +- `multi` is used to indicate whether batch operations are allowed. It takes an expression as configuration. By default, only the `read` operation allows batch operations. If you need to enable other batch operations, you need to explicitly specify this validator. + +## Best Practices + +Access rules may seem tedious and difficult to grasp at first, and it is easy to make mistakes or lack confidence in their security. Based on our extensive experience in various projects, we have some recommendations: + +- Principle of least privilege: only grant the necessary access permissions to clients and only expose the necessary collections. + + - By only granting `read` permission, you can save 30% to 50% of backend API calls. + - Additionally, by using basic `condition` checks, you can effectively reduce the number of backend API calls by 70% to 90%. + +- For transactional data operations or complex form submissions, it is recommended to use cloud functions: + + - Cloud functions have the same interface as client SDKs, making them simple, lightweight, and easy to debug without the need for deployment. + - When accessing databases in cloud functions, there is no need to write access rules because cloud functions are trusted server-side code. + +- Although access rules can handle many complex validations, it is recommended to use cloud functions to implement this logic unless you are very proficient in access rules. + +## Access Rule Examples + +> Below are a few simple examples for reference and understanding. + +### Simple Example 1: Simple personal blog + +```json +"categories" +{ + "read": true, + "update": "uid", + "add": "uid", + "remove": "uid" +}, +"articles" +{ + "read": true, + "update": "uid", + "add": "uid", + "remove": "uid" +} +``` + +> Note: The rule `"update": "uid"` means that `uid` should not be a falsy value (undefined | null | false, etc.). + +### Simple Example 2: Multi-user blog + +```json +{ + "read": true, + "update": "uid === query.author_id", + "add": "uid === query.author_id", + "remove": "uid === query.author_id" +} +``` + +### Complex Example 1: Data validation + +```json +{ + "read": true, + "add": { + "condition": "uid === query.author_id", + "data": { + "title": { "length": [1, 64], "required": true }, + "content": { "length": [1, 4096] }, + "status": { "boolean": [true, false] }, + "likes": { "number": [0], "default": 0 }, + "author_id": "$value == uid" + } + }, + "remove": "uid === query.author_id", + "count": true +} +``` + +### Complex Example 2: Advanced data validation + +> Scenario: Access rules for a table of messages between users + +```json +{ + "read": "uid === query.receiver || uid === query.sender", + "update": { + "condition": "$uid === query.receiver", + "data": { + "read": { "in": [true] } + } + }, + "add": { + "condition": "uid === data.sender", + "data": { + "read": { "in": [false] }, + "content": { "length": [1, 20480], "required": true }, + "receiver": { "exists": "/users/id" }, + "read": { "in": [true, false], "default": false } + } + }, + "remove": false +} +``` + +### Example of a data validator + +```json +"categories": { + "read": true, + "update": "role === 'admin'", + "add": { + "condition": "role === 'admin'", + "data": { + "password": { "match": "^\\d{6,10}$" }, + "author_id": "$value == uid", + "type": { "required": true, "in": ["choice", "fill"] }, + "title": { "length": [4, 64], "required": true, "unique": true }, + "content": { "length": [4, 20480] }, + "total": { "number": [0, 100], "default": 0, "required": true } + } + } +} +``` \ No newline at end of file diff --git a/docs/en/guide/db/quickstart.md b/docs/en/guide/db/quickstart.md new file mode 100644 index 0000000000..4c62e1c598 --- /dev/null +++ b/docs/en/guide/db/quickstart.md @@ -0,0 +1,146 @@ +--- +title: Introduction to Databases +--- + +# {{ $frontmatter.title }} + +Laf provides a ready-to-use database for each application, which is very easy to use. Here is a simple example of database CRUD operations to quickly understand Laf's database operations. + +## Creating a Database Instance + +```typescript +import cloud from '@lafjs/cloud' +const db = cloud.database() +// db is the newly created database instance +``` + +## Inserting Documents in Cloud Functions + +Use the `add` method to insert data into a collection. + +For example, add a document with `name` as `Jack` to the `user` collection. + +```typescript +import cloud from '@lafjs/cloud' +const db = cloud.database() + +export async function main(ctx: FunctionContext) { + const res = await db.collection('user').add({ + name: 'Jack' + }) + console.log(res) +} +``` + +```json +// A new document will be added to the 'test' collection, with an automatically generated _id +{ + "_id": "6442b2cac4f3afd9a186ecd9", + "name": "Jack" +} +``` + +## Querying Documents in Cloud Functions + +Use the `get` method to query documents in a collection. + +::: tip +The `get` method can retrieve up to 100 records at a time. If you need to query more records at once, please refer to the data query documentation. +::: + +```typescript +import cloud from '@lafjs/cloud' +const db = cloud.database() + +export async function main(ctx: FunctionContext) { + const res = await db.collection('user').get() + console.log(res) + // Query result + // { + // data: [ { _id: '6442b2cac4f3afd9a186ecd9', name: 'Jack' } ], + // requestId: undefined, + // ok: true + // } +} +``` + +Use the `getOne` method to query the latest document in a collection. + +::: tip +The `getOne` method retrieves one latest record at a time. +::: + +```typescript +import cloud from '@lafjs/cloud' +const db = cloud.database() + +export async function main(ctx: FunctionContext) { + const res = await db.collection('user').getOne() + console.log(res) + // Query result + // { + // ok: true, + // data: { _id: '6442b2cac4f3afd9a186ecd9', name: 'Jack' }, + // requestId: undefined + // } +} +``` + +## Modifying Documents in Cloud Functions + +Retrieve the `_id` of the document with `name` as `Jack`, and then update the document based on the `_id`. + +Use the `update` method to modify documents. + +::: tip +`where` sets the query condition, and `doc` queries based on the id. +::: + +```typescript +import cloud from '@lafjs/cloud' +const db = cloud.database() + +export async function main(ctx: FunctionContext) { + const res = await db.collection('test').where({ + name:'Jack' + }).get() + // console.log(res) + const id = res.data[0]._id + const updateRes = await db.collection('test').doc(id).update({ + name:'Tom' + }) + console.log(updateRes) + // Modification result: updated:1 represents successfully modified 1 document + // { + // requestId: undefined, + // updated: 1, + // matched: 1, + // upsertId: null, + // ok: true + // } +} +``` + +## Cloud Function: Delete Document + +To delete a document in the database where the `name` is `Tom`, follow the steps below: + +::: tip +By default, `remove` can only delete a single piece of data. If you need to delete multiple documents, please refer to the document on data deletion. +::: + +```typescript +import cloud from '@lafjs/cloud' +const db = cloud.database() + +export async function main(ctx: FunctionContext) { + const res = await db.collection('test').where({ name: "Tom" }).remove() + console.log(res) + // Deletion result: deleted:1 indicates the successful deletion of 1 document + // { requestId: undefined, deleted: 1, ok: true } +} +``` + +-------- + +By following the documentation step by step, you should have a basic understanding of CRUD operations in the database. However, if you need to dive deeper into using database operations, you will need to carefully review the subsequent documents. \ No newline at end of file diff --git a/docs/en/guide/db/update.md b/docs/en/guide/db/update.md new file mode 100644 index 0000000000..a317a14ff2 --- /dev/null +++ b/docs/en/guide/db/update.md @@ -0,0 +1,129 @@ +--- +title: Update Data +--- + +# {{ $frontmatter.title }} + +Updating data in Laf Cloud Database involves modifying documents in a collection. You can set query conditions using operators like `where`, and then use update operators like `update` or `set` to make the changes. + +## Updating Documents + +### Updating a Specific Document + +Here, we will change the name of "jack" to "Tom". + +```typescript +await db.collection('user') +.where({ name: 'jack' }) +.update({ name: "Tom" }) +``` + +Similarly, you can update multiple properties of a document at once. For example, changing the name and age together: + +```typescript +await db.collection('user') +.where({ name: 'jack' }) +.update({ name: "Tom", age: "18" }) +``` + +### Updating Documents in Bulk + +In this case, we are not adding any condition restrictions. We directly change the name of all records in the "user" table to "Tom". The operation is the same as before, except now we need to pass an additional object `{multi:true}`. + +```typescript +await db.collection('user') +.update({ name: "Tom" },{ multi:true }) +``` + +## Update Operators + +### set + +Update operator. If the data does not exist, a new document will be created. + +::: warning +Note: Using `set` requires using `doc` to query the document based on its ID, and then perform the update. If the document is not found, a new document will be created. +`doc` [Usage Guide](/guide/db/find.html#根据id查询数据) +::: + +```typescript +await collection.doc('644148fd1eeb2b524dba499e').set({ + name: "Hey" +}) +``` + +### inc + +Update operator. Used to increment a field by a certain value. The value must be a positive or negative integer. The advantage of using this atomic operation instead of reading the data, adding to it, and writing it back is: + +1. Atomicity: Multiple users can write at the same time, and the database will increment the field for each user without overwriting each other. +2. Reducing network requests: There's no need to read before writing. + +Similarly, the `mul` operator works. + +For example, incrementing the number of favorite products: + +```typescript +const _ = db.command; +await db.collection("user") + .where({ + _openid: "my-open-id", + }) + .update({ + count: { + favorites: _.inc(1), + }, + }) +``` + +### mul + +Update operator. Used to multiply a field by a certain value. The value must be a positive or negative integer or 0. + +### remove + +Update operator. Used to delete a field. For example, if someone deletes the rating in their product review: + +```typescript +const _ = db.command; +await db.collection("comments") + .doc("comment-id") + .update({ + rating: _.remove(), + }) +``` + +### push + +Append elements to the end of an array. You can pass a single element or an array. + +```typescript +const _ = db.command; +await db.collection("comments") + .doc("comment-id") + .update({ + // users: _.push('aaa') + users: _.push(["aaa", "bbb"]), + }) +``` + +### pop + +Remove the last element of an array. + +```typescript +const _ = db.command; +await db.collection("comments") + .doc("comment-id") + .update({ + users: _.pop(), + }) +``` + +### unshift + +Add elements to the beginning of an array. You can pass a single element or an array. Same usage as `push`. + +### shift + +Removes the first element from an array. Similar to `pop`. \ No newline at end of file diff --git a/docs/en/guide/function/call-function-in-client.md b/docs/en/guide/function/call-function-in-client.md new file mode 100644 index 0000000000..e7a0b38faa --- /dev/null +++ b/docs/en/guide/function/call-function-in-client.md @@ -0,0 +1,43 @@ +--- +title: Calling in the Client +--- + +# {{ $frontmatter.title }} + +Once the cloud function is written and published, it can be called from the client using `laf-client-sdk`. + +::: info +Currently, the SDK only supports sending POST requests. +::: + +## Installing the SDK in the Frontend Project + +```shell +npm i laf-client-sdk +``` + +## Initializing the `cloud` Object + +```typescript +import { Cloud } from "laf-client-sdk"; + +const cloud = new Cloud({ + baseUrl: "https://APPID.laf.run", // Get APPID from the application list on the homepage + getAccessToken: () => "", // No authorization required here, leave it empty for now +}) +``` + +## Calling the Cloud Function + +```typescript +// The first argument here is the name of the cloud function, and the second argument is the data passed in, corresponding to ctx.body in the cloud function +const res = await cloud.invoke('get-user-info', { userid }) + +console.log(res) // The content returned by the cloud function is stored in the variable `res` +``` + +It's convenient, right? With just a simple configuration and one line of code, you can call the cloud function. + +## laf-client-sdk Detailed Documentation + +View the detailed documentation: [client-sdk](/guide/client-sdk/) diff --git a/docs/en/guide/function/call-function-in-http.md b/docs/en/guide/function/call-function-in-http.md new file mode 100644 index 0000000000..73f8ee61c9 --- /dev/null +++ b/docs/en/guide/function/call-function-in-http.md @@ -0,0 +1,56 @@ +# HTTP Calls + +Each cloud function provides an API endpoint that can be called from anywhere capable of making HTTP requests. + +![function-url](../../doc-images/function-url.png) + +## Using `axios` for the call + +Here is a simple example of using `axios` in the frontend to make a request to a cloud function. + +```typescript +// get request +import axios from 'axios'; + +const { data } = await axios({ + url: "", + method: "get", +}); + +console.log(data); +``` + +```typescript +// post request +import axios from 'axios'; + +const { data } = await axios({ + url: "", + method: "post", + data: { + name: 'Jack' + } +}); + +console.log(data); +``` + +## `curl` Invocation + +You can also use `curl` to invoke cloud functions. + +GET request + +```shell +curl ?query=hello&limit=10 \ + -H "Authorization: Bearer abc123" \ + -H "Content-Type: application/json" +``` + +POST request + +```shell +curl -X POST \ + -H "Content-Type: application/json" \ + -d '{"name": "John", "age": 30}' +``` \ No newline at end of file diff --git a/docs/en/guide/function/call-function.md b/docs/en/guide/function/call-function.md new file mode 100644 index 0000000000..47f5f617d8 --- /dev/null +++ b/docs/en/guide/function/call-function.md @@ -0,0 +1,56 @@ +--- +title: Invoking Cloud Functions +--- + +# {{ $frontmatter.title }} + +Once a cloud function is developed and published, it can be called from other cloud functions. + +::: info +The `cloud.invoke` method is deprecated. Please use [importing cloud functions](/guide/function/use-function.html#importing-cloud-functions) instead. +::: + +## Writing and Publishing the Function to be Called + +For example, let's create a cloud function named `get-user-info` and write the following code: + +```typescript +import cloud from '@lafjs/cloud' + +const db = cloud.database() + +export async function main(ctx: FunctionContext) { + + const { body: { userid } } = ctx + if (!userid) return { err: 1, errmsg: 'userid not exists' } + + const userCollection = db.collection('user') + const { data: user } = await userCollection.where({ id: userid }).get() + + if (!user) return { err: 2, errmsg: 'user not found' } + return { err: 0, data: user } +} +``` + +This function accepts a parameter called `userid`, searches for the corresponding user in the database based on the `id`, and returns the retrieved data. + + + +## Invoking a Published Cloud Function + +After the `get-user-info` cloud function is published, we can call this function from other cloud functions. + +```typescript +import cloud from '@lafjs/cloud' + +export async function main(ctx: FunctionContext) { + const res = await cloud.invoke('get-user-info', { ...ctx, body: { userid: 'user id' }}) + console.log(res) +} +``` + +We can use the `cloud.invoke` method to call other cloud functions within a cloud function. + +This method takes two parameters: the first parameter is the name of the function to be called, and the second parameter is the data to be passed. + +As you can see, we pass the request parameters in the `body` object. If the called function needs to access certain properties from the `ctx` object, you can also pass the `ctx` object using `...ctx` for the called function to use. \ No newline at end of file diff --git a/docs/en/guide/function/depend.md b/docs/en/guide/function/depend.md new file mode 100644 index 0000000000..c588b74ae5 --- /dev/null +++ b/docs/en/guide/function/depend.md @@ -0,0 +1,66 @@ +--- +title: Dependency Management +--- + +# {{ $frontmatter.title }} + +During application development, there is often a need to add `npm` dependencies. Laf provides an online visualization method for managing these third-party package modules, allowing users to easily search, install, upgrade, and uninstall them. + +::: warning +Laf cloud development can install dependencies from . If the required dependencies cannot be found on this website, they cannot be installed. +::: + +## Adding Dependencies + +![add-packages](/doc-images/add-packages.png) + +As shown in the above image, we click on `NPM Dependency` at the bottom left of the screen, then click on the Add button, search for the package name that you want to install (in this example, we use [moment](https://www.npmjs.com/package/moment)), check the checkbox, and click on the `Save and Restart` button. + +> The installation time will vary depending on the size of the package and the network conditions. Please be patient and wait for it to complete. + +![package-list](/doc-images/package-list.png) + +After installation, users can view the installed dependencies and their versions in the `Dependency Management` section at the bottom left of the interface. + +## Selecting Dependency Versions + +![select-package-version](/doc-images/select-package-version.png) + +To ensure the stability of the user's application, Laf does not automatically update the version of the installed `Npm packages`. + +By default, when adding a dependency, the latest version (`latest`) is selected. If the user wants to specify a version, they can choose the corresponding version from the version dropdown during installation. + +## Using in Cloud Functions + +After installation, the package can be imported and used in cloud functions. For example, create a cloud function called `hello-moment` and modify the code as follows: + +```typescript +import cloud from '@lafjs/cloud' +import moment from 'moment' + +export async function main(ctx: FunctionContext) { + const momentVersion = moment.version + const now = moment().format('YYYY-MM-DD hh:mm:ss') + const twoHoursLater = moment().add(2, 'hours').format('YYYY-MM-DD hh:mm:ss') + + return { momentVersion, now, twoHoursLater} +} +``` + +Click the Run button on the right side of the interface or press `Ctrl + S` to save. You will see the following output: + +```json +{ + "momentVersion": "2.29.4", + "now": "2023-02-08 02:14:05", + "twoHoursLater": "2023-02-08 04:14:05" +} +``` + +## Switching Installed Dependency Versions + +![change-package-version](/doc-images/change-package-version.png) + +## Uninstalling installed dependencies + +![delete-package](/doc-images/delete-package.png) \ No newline at end of file diff --git a/docs/en/guide/function/env.md b/docs/en/guide/function/env.md new file mode 100644 index 0000000000..0cd1bd6087 --- /dev/null +++ b/docs/en/guide/function/env.md @@ -0,0 +1,30 @@ +--- +title: Using Environment Variables in Cloud Functions +--- + +# {{ $frontmatter.title }} + +## Adding Environment Variables + +1. First, click on the small gear button at the bottom left corner of the page to open the application settings page. +2. Next, in the popped-up application settings interface, click on "Environment Variables" and enter your environment variable on a new line below. The format should be: `NAME='value'` +3. Click on "Update" to make it effective. + +## Using Environment Variables + +Once the environment variables are added, you can use them in any cloud function by accessing `process.env`. + +:::tip +`cloud.env` is deprecated. +::: + +```typescript +import cloud from '@lafjs/cloud' + +export async function main(ctx: FunctionContext) { + + const env = process.env + console.log(env) + // All the environment variables are available in process.env +} +``` \ No newline at end of file diff --git a/docs/en/guide/function/faq.md b/docs/en/guide/function/faq.md new file mode 100644 index 0000000000..80887d57be --- /dev/null +++ b/docs/en/guide/function/faq.md @@ -0,0 +1,584 @@ +--- +title: Common Issues with Cloud Functions +--- + +# {{ $frontmatter.title }} + +Here are some common issues that you may encounter during cloud function development. Feel free to contribute through PR~ + +[[toc]] + +## Frontend Cross-Origin Resource Sharing (CORS) + +By default, LAF cloud functions do not have any domain restrictions. However, some developers still face CORS issues during the development process. You can set `withCredentials` to `false`. + +Here are three common ways to set `withCredentials` to `false`: + +```js +// Method 1 +axios.defaults.withCredentials = false + +// Method 2 +axios.get('http://example.com', { + withCredentials: false +}) + +// Method 3 +XMLHttpRequest.prototype.withCredentials = false +``` + +## Delayed Execution of Cloud Functions + +```typescript +import cloud from '@lafjs/cloud' + +export async function main(ctx: FunctionContext) { + const startTime = Date.now() + console.log(startTime) + await sleep(5000) // Delay for 5 seconds + console.log(Date.now() - startTime) +} + +async function sleep(ms) { + return new Promise(resolve => + setTimeout(resolve, ms) + ); +} +``` + +## Setting a Timeout for a Cloud Function + +```typescript +import cloud from '@lafjs/cloud' + +export async function main(ctx: FunctionContext) { + // If the asynchronous operation in getData is completed and returned within 4 seconds, responseText will be the returned value of getData + // If not completed within 4 seconds, responseText will be '', but it does not affect the actual execution of getData + const responseText = await Promise.race([ + getData(), + sleep(4000).then(() => ''), + ]); + console.log(responseText, Date.now()) +} + +async function sleep(ms) { + return new Promise(resolve => + setTimeout(resolve, ms) + ); +} + +async function getData(){ + // Some asynchronous operation, here we simulate a case where it takes more than 4 seconds using sleep + await sleep(5000) + const text = "Return value of getData" + console.log(text, Date.now()) + return text +} +``` + +## Simple Example of Integrating Cloud Functions with WeChat Official Account + +The following code is only compatible with plaintext mode. + +```typescript +import * as crypto from 'crypto' +import cloud from '@lafjs/cloud' + +export async function main(ctx: FunctionContext) { + const { signature, timestamp, nonce, echostr } = ctx.query; + const token = '123456'; // The token here is custom and needs to correspond to the token configured in the WeChat backend + + // Verify if the message is legitimate. If not, return an error message. + if (!verifySignature(signature, timestamp, nonce, token)) { + return 'Invalid signature'; + } + + // If it is the first verification, return echostr to the WeChat server + if (echostr) { + return echostr; + } + + // Handle the received message + const payload = ctx.body.xml; + // If the received message type is text + if (payload.msgtype[0] === 'text') { + // Reply with the same content sent by the official account + return toXML(payload, payload.content[0]); + } +} + +// Check if the message sent by the WeChat server is legitimate +function verifySignature(signature, timestamp, nonce, token) { + const arr = [token, timestamp, nonce].sort(); + const str = arr.join(''); + const sha1 = crypto.createHash('sha1'); + sha1.update(str); + return sha1.digest('hex') === signature; +} + +// Return the assembled XML +function toXML(payload, content) { + const timestamp = Date.now(); + const { tousername: fromUserName, fromusername: toUserName } = payload; + return ` + + + + ${timestamp} + + + + ` +} +``` + +## Cloud Function for Image Composition + +First, you need to install the dependency `canvas`. + +```typescript +import { createCanvas } from 'canvas' + +export async function main(ctx: FunctionContext) { + const canvas = createCanvas(200, 200) + const context = canvas.getContext('2d') + + // Write "hello!" + context.font = '30px Impact' + context.rotate(0.1) + context.fillText('hello!', 50, 100) + + // Draw line under text + var text = context.measureText('hello!') + context.strokeStyle = 'rgba(0,0,0,0.5)' + context.beginPath() + context.lineTo(50, 102) + context.lineTo(30 + text.width, 102) + context.stroke() + + // Write "Laf!" + context.font = '30px Impact' + context.rotate(0.1) + context.fillText('Laf!', 50, 150) + console.log(canvas.toDataURL()) + return `` +} +``` + +## Cloud Function for Image Composition with Chinese Fonts + +First, upload the font file that supports Chinese fonts to cloud storage and get the download URL for the font. Then save it to a temporary file and use it for composition through canvas. + +:::tip +Temporary files will be cleared when Laf restarts. +::: + +```typescript +import { createCanvas, registerFont } from 'canvas' +import fs from 'fs' +import cloud from '@lafjs/cloud' + +export async function main(ctx: FunctionContext) { + + const url = '' // Font file URL + const dest = '/tmp/fangzheng.ttf' // Local save path + + if (fs.existsSync(dest)) { + console.log('File exists!') + } else { + console.log('File does not exist') + const res = await cloud.fetch({ + method: 'get', + url, + responseType: 'arraybuffer' + }) + fs.writeFileSync(dest, Buffer.from(res.data)) + } + + registerFont('/tmp/fangzheng.ttf', { family: 'fangzheng' }) + const canvas = createCanvas(200, 200) + const context = canvas.getContext('2d') + + // Write "你好!" + context.font = '30px fangzheng' + context.rotate(0.1) + context.fillText('你好!', 50, 100) + + // Draw line under text + var text = context.measureText('hello!') + context.strokeStyle = 'rgba(0,0,0,0.5)' + context.beginPath() + context.lineTo(50, 102) + context.lineTo(30 + text.width, 102) + context.stroke() + + // Write "Laf!" + context.font = '30px Impact' + context.rotate(0.1) + context.fillText('Laf!', 50, 150) + console.log(canvas.toDataURL()) + return `` +} +``` + +## Cloud Function for Debouncing + +Debouncing can be easily set up using the global cache of Laf Cloud Functions. + +Here is a simple example of debouncing. When making a request from the frontend, you need to include the user token in the header. + +```typescript +// Cloud function generates Token +const accessToken_payload = { + // Besides the examples, you can also add other parameters + uid: login_user[0]._id, // Usually the _id of the user table + role: login_user[0].role, // If there is no role, it can be omitted + exp: (Math.floor(Date.now() / 1000) + 60 * 60 * 24 * 7) * 1000, // Expires in 7 days +} +const token = cloud.getToken(accessToken_payload) +console.log(token) +``` + +```typescript +import cloud from '@lafjs/cloud' + +export async function main(ctx: FunctionContext) { + const FunctionName = ctx.request.params.name + const sharedName = FunctionName + ctx.user.uid + let lastCallTime = cloud.shared.get(sharedName) + console.log(lastCallTime) + if (lastCallTime > Date.now()) { + console.log("Request is too fast") + return 'Request is too fast' + } + cloud.shared.set(sharedName, Date.now() + 1000) + // Original logic + + // Delete global cache after logic completion + cloud.shared.delete(sharedName) +} +``` + +## Cloud Function Domain Verification + +Some WeChat services require verification of the value of a txt file starting with "MP" in order to determine if the domain has permission. + +You can create a cloud function with the same file name, such as `MP_123456789.txt`, and simply return the content of the text file. + +```typescript +import cloud from '@lafjs/cloud' + +export async function main(ctx: FunctionContext) { + // Here we simply return the content of the text file + return 'abcd...' +} +``` + +## Laf Application IP Pool (IP Whitelist) + +To check the IP pool used by Laf.run, use laf.dev or other variations, replace the domain in the following commands. + +- For Windows, execute `nslookup laf.run` in CMD. +- For Mac, execute `nslookup laf.run` in Terminal. + +This will display the entire IP pool. + +## Cloud Function for Sending Aliyun SMS Verification Code + +Use the Aliyun SMS API to write a cloud function for sending SMS verification codes. You will need to provide the `AccessKey` and `SecretKey` for Aliyun SMS service, as well as the SMS template ID. + +Create a `sendsms` cloud function and add the dependency `@alicloud/dysmsapi20170525`. Write the following code: + +```typescript +import Dysmsapi, * as dysmsapi from "@alicloud/dysmsapi20170525"; +import * as OpenApi from "@alicloud/openapi-client"; +import * as Util from "@alicloud/tea-util"; + +const accessKeyId = "xxxxxxxxxxxxxx"; +const accessKeySecret = "xxxxxxxxxxxxxx"; +const signName = "XXXX"; +const templateCode = "SMS_xxxxx"; +const endpoint = "dysmsapi.aliyuncs.com"; + + +export default async function (ctx: FunctionContext) { + const { phone, code } = ctx.body; + + const sendSmsRequest = new dysmsapi.SendSmsRequest({ + phoneNumbers: phone, + signName, + templateCode, + templateParam: `{"code":${code}}`, + }); + + const config = new OpenApi.Config({ accessKeyId, accessKeySecret, endpoint }); + const client = new Dysmsapi(config); + const runtime = new Util.RuntimeOptions({}); + const res = await client.sendSmsWithOptions(sendSmsRequest, runtime); + return res.body; +}; +``` + +## Cloud Function for WeChat Native Payment + +Native payment is suitable for scenarios such as PC websites, physical store items or orders, and media advertising payments. [Official WeChat Documentation](https://pay.weixin.qq.com/wiki/doc/apiv3_partner/open/pay/chapter2_7_0.shtml) + +After the server-side order is placed for native payment, WeChat Pay will return a `code-url`, which can be directly used by the frontend to generate a QR code for WeChat payment. + +- The structure of `code-url` is similar to: `weixin://wxpay/bizpayurl?pr=aIQrOYOzz` +- You can also write a Laf cloud function to receive payment result notifications. This code does not include the `notify_url`. + +Create a `wx-pay` cloud function and install the dependency [wxpay-v3](https://github.com/yangfuhe/node-wxpay). Here is the code: + +```typescript +import cloud from "@lafjs/cloud"; +const Payment = require("wxpay-v3"); + +export default async function (ctx: FunctionContext) { + const { goodsName, totalFee, payOrderId } = ctx.body; + + // create payment instance + const payment = new Payment({ + appid: "应用 ID", + mchid: "商户 id", + private_key: getPrivateKey(), + serial_no: "序列号", + apiv3_private_key: "api v3 密钥", + notify_url: "付退款结果通知的回调地址", + }); + + // place an order + const result = await payment.native({ + description: goodsName, + out_trade_no: payOrderId, + amount: { + total: totalFee, + }, + }); + return result; +}; + +function getPrivateKey() { + const key = `-----BEGIN PRIVATE KEY----- +HOBHEk+4cdiPcvhowhC8ii7838DP4qC+18ibL/KAySWyZjUC/keOr4MxhxQ1T+OV +... +... +475J8ALCRltkgTSxicoXS7SpjLqvIH2FPpv2BI+qQ3nOmAugsRkeH9lZdC/nSC0m +uI205SwTsTaT70/vF90AwQ== +-----END PRIVATE KEY----- +`; + return key; +} +``` + +## Cloud Function AliPay Payment + +Create an `aliPay` cloud function, install the `alipay-sdk` dependency, and write the following code: + +```typescript +import clouasssssad from "@lafjs/cloud"; +import alipay from "alipay-sdk"; +const AlipayFormData = require("alipay-sdk/lib/form").default; + + +export default async function (ctx: FunctionContext) { + const { totalFee, goodsName, goodsDetail, payOrderId } = ctx.body; + + const ali = new alipay({ + appId: "2016091800536572", + signType: "RSA2", + privateKey: "MIIEow......Yf2Mlz6xqG/Aq", + alipayPublicKey: "MIIBI......IDAQAB", + gateway: "https://openapi.alipaydev.com/gateway.do", // Sandbox test gateway + }); + + const formData = new AlipayFormData(); + formData.setMethod("get"); + formData.addField( + "notifyUrl", + "https://APPID.laf.run/alipay_notify_callback" + ); + formData.addField("bizContent", { + subject: goodsName, + body: goodsDetail, + outTradeNo: payOrderId, + totalAmount: totalFee, + }); + + const result = await ali.exec( + "alipay.trade.app.pay", + {}, + { formData: formData } + ); + + return { + code: 0, + data: result, + }; +}; +``` + +## Cloud Function Send Email + +Send emails using SMTP service. + +Create a `sendmail` cloud function, install the `nodemailer` dependency, and write the following code: + +```typescript +import nodemailer from 'nodemailer' + +// Mail server configuration +const transportConfig = { + host: 'smtp.exmail.qq.com', // smtp service address, e.g., Tencent Enterprise Mail address + port: 465, // smtp service port, usually not open by default, need to manually enable + secureConnection: true, // Use SSL + auth: { + user: 'sender@xx.com', // sender's email, replace it with your own email address + pass: 'your password', // SMTP dedicated password or login password set by you, different for each service, QQ Mail needs to enable and configure an authorization code + } +} + +// Email configuration +const mailOptions = { + from: '"SenderName" ', // sender + to: 'hi@xx.com', // recipient + subject: 'Hello', // email subject + html: 'Hello world?' // HTML formatted email body + // text: 'hello' // text format email body +} + +export default async function (ctx: FunctionContext) { + const transporter = nodemailer.createTransport(transportConfig) + + transporter.sendMail(mailOptions, (error, info) => { + if (error) { + return console.log(error); + } + console.log('Message sent: %s', info.messageId); + return info.messageId + }) +}; +``` + +## Uploading Files to WeChat Official Account Temporary Media + +To reply with an image or file in a WeChat Official Account, you need to upload the file to temporary media first. + +The following example shows how to upload a file to temporary media using a file URL: + +```typescript +import fs from 'fs'; +const request = require('request'); +const util = require('util'); +const postRequest = util.promisify(request.post); + +export default async function (ctx: FunctionContext) { + const res = await UploadToWeixin(url); + console.log(res); +} + +async function UploadToWeixin(url) { + const res = await cloud.fetch.get(url, { + responseType: 'arraybuffer' + }); + fs.writeFileSync('/tmp/tmp.jpg', Buffer.from(res.data)); + // The demonstration of getAccess_token is not provided here + const access_token = await getAccess_token(); + const formData = { + media: { + value: fs.createReadStream('/tmp/tmp.jpg'), + options: { + filename: 'tmp.png', + contentType: 'image/png' + } + } + }; + // Due to a bug in axios when uploading WeChat media, request is used here to wrap the post request for uploading + const uploadResponse = await postRequest({ + url: `https://api.weixin.qq.com/cgi-bin/media/upload?access_token=${access_token}&type=image`, + formData: formData + }); + return uploadResponse.body; +} +``` + +## Laf Function Authentication, Token Retrieval, and Token Expiration Handling + +Original Post: + +Process: + +1. The cloud function generates a token and returns it to the frontend. +2. The frontend includes the token in its requests. +3. The cloud function checks whether the `ctx.user` contains the token and if it has expired. + +Example Code: + +1. The cloud function generates a token and returns it to the frontend. + +```typescript +import cloud from '@lafjs/cloud' + +export async function main(ctx: FunctionContext) { + + const payload = { + // uid is usually the user ID from the database, this is just a demo + uid: 1, + // For demonstration purposes, the expiration time is set to 10 seconds + // In reality, it would be something like: exp: Math.floor(Date.now() / 1000) + 60 * 60 * 24 * 7, + exp: Math.floor(Date.now() / 1000) + 10, + }; + + // Generate the access_token + const access_token = cloud.getToken(payload); + + return { access_token } +} +``` + +2. The frontend includes the token in its requests. +The first method involves using the `laf-client-sdk`. + +```typescript +import { Cloud } from "laf-client-sdk"; // Import laf-client-sdk +import axios from "axios"; + +// Create a cloud object +const cloud = new Cloud({ + baseUrl: "", // Fill in your cloud function URL, e.g., https://appid.laf.dev + // Retrieve the access_token from local storage + getAccessToken: () => localStorage.getItem("access_token"), +}); +// When invoking a cloud function, the access_token will be automatically included +const res = await cloud.invoke("test"); +``` + +The second method involves using axios. + +```typescript +import axios from "axios"; + +const token = localStorage.getItem("access_token"); +axios({ + method: "get", + url: "functionUrl", + headers: { + // Include the token here + Authorization: `Bearer ${token}`, + }, +}) +``` + +3. The cloud function checks whether the `ctx.user` contains the token and if it has expired. + +```typescript +import cloud from '@lafjs/cloud' + +export async function main(ctx: FunctionContext) { + + console.log(ctx.user) + // If the token was included by the frontend and has not expired: { uid: 1, exp: 1683861234, iat: 1683861224 } + // If the token was not included or has expired: null + +} +``` diff --git a/docs/en/guide/function/function-param.md b/docs/en/guide/function/function-param.md new file mode 100644 index 0000000000..892f51d2ff --- /dev/null +++ b/docs/en/guide/function/function-param.md @@ -0,0 +1,49 @@ +--- +title: Cloud Function Parameters and Return Values +--- + +# {{ $frontmatter.title }} + +## Parameters + +In the `main` function, you can access the request information passed by the user through the first parameter `ctx`. +The following example reads the Query parameter `username` passed by the front-end: + +```js +export default async function (ctx: FunctionContext) { + return `hello, ${ctx.query.username}`; +}; +``` + +Similarly, you can read the body parameter passed by the front-end: + +```js +export default async function (ctx: FunctionContext) { + return `hello, ${ctx.body}`; +}; +``` + +The `ctx` object contains the following properties: + +| Property | Description | +| --------------- | ------------------------------------------------------------------------- | +| `ctx.requestId` | The unique ID of the current request | +| `ctx.method` | The method of the current request, such as `GET` or `POST` | +| `ctx.headers` | All headers of the request | +| `ctx.auth` | The token value parsed when using Http Bearer Token authentication | +| `ctx.query` | The query parameters of the current request | +| `ctx.body` | The body parameters of the current request | +| `ctx.response` | The HTTP response, consistent with the `Response` instance in `express` | +| `ctx.socket` | WebSocket instance ([WebSocket](https://developer.mozilla.org/en-US/docs/Web/API/WebSocket)) | +| `ctx.files` | Uploaded files (an array of [File](https://developer.mozilla.org/en-US/docs/Web/API/File) objects) | + +## Return Value + +So how do we pass data to the frontend? It's simple, just return it in the cloud function. + +```js +export default async function (ctx: FunctionContext) { + // Here is an example using a string, but you can return any data type. + return "This is the data returned to the frontend"; +}; +``` \ No newline at end of file diff --git a/docs/en/guide/function/function-sdk.md b/docs/en/guide/function/function-sdk.md new file mode 100644 index 0000000000..fd3e7e8d0d --- /dev/null +++ b/docs/en/guide/function/function-sdk.md @@ -0,0 +1,223 @@ +--- +title: Cloud Function SDK +--- + +# {{ $frontmatter.title }} + +Introduction to Laf's Cloud SDK + +For cloud functions, Laf provides a dedicated SDK `@lafjs/cloud` to support accessing networks, databases, object storage, and more. + +::: danger +`@lafjs/cloud` is a proprietary module that can only be used in cloud functions and cannot be installed via npm in other locations. +::: + +## Importing the SDK + +Each Laf application already has the SDK dependencies installed by default, so there is no need for additional installation. Simply import it at the top of your cloud function. + +```js +import cloud from "@lafjs/cloud"; +``` + +## Sending Network Requests + +Use `cloud.fetch()` to send HTTP requests and call third-party APIs, such as payment interfaces, SMS verification codes, etc. + +This interface is a wrapper for the `axios` request library, and its usage is identical to `axios`. You can refer to the [axios documentation](https://www.axios-http.cn/docs/api_intro) for the calling method. + +You can think of `cloud.fetch === axios`, which means they can be used interchangeably. + +```typescript +import cloud from "@lafjs/cloud"; + +export async function main(ctx: FunctionContext) { + const ret = await cloud.fetch({ + url: "http://api.github.com/", + method: "post", + }); + console.log(ret.data); + return ret.data; +}; +``` + +Alternatively, you can write it like this: + +```typescript +// GET request +const getRes = await cloud.fetch.get("http://api.github.com/"); +// POST request +const postRes = await cloud.fetch.post("http://api.github.com/",{ + name: 'laf' +}); +``` + +## Working with Databases + +You can use `cloud.database()` to obtain a database object and perform database operations. + +::: info +For detailed database API operation methods, refer to the _[Cloud Database](/guide/db/)_ section. +::: + +The following example retrieves user information from the database: + +```typescript +import cloud from "@lafjs/cloud"; + +export async function main(ctx: FunctionContext) { + const { username } = ctx.body; + // Database operation + const db = cloud.database(); + const ret = await db.collection("users").where({ username }).get(); + console.log(ret); +}; +``` + +## Invoking Other Cloud Functions + +You can use `cloud.invoke()` to call other cloud functions within the same application. + +::: info +This method is no longer recommended. You can directly [import cloud functions](#Importing-Cloud-Functions) now. +::: + +```typescript +import cloud from '@lafjs/cloud' + +export async function main(ctx: FunctionContext) { + // Call the hello cloud function + await cloud.invoke('hello') +} +``` + +If the invoked cloud function needs to access something in the `ctx` object, you can pass it like this: + +```typescript +import cloud from '@lafjs/cloud' + +export async function main(ctx: FunctionContext) { + // Call the hello cloud function and pass the ctx object + await cloud.invoke('hello', ctx) +} +``` + +## Generating and Decrypting JWT Tokens + +```typescript +cloud.getToken(payload); // payload can refer to the example code below +cloud.parseToken(token); // token is the token in the authorization field of the request header from the frontend +``` + +Here is a simple implementation for generating and decrypting JWT tokens: + +```js +import cloud from "@lafjs/cloud"; + +export async function main(ctx: FunctionContext) { + const payload = { + uid: 1, + // The default expiration time of the token is 7 days. Please make sure to provide the `exp` field, see JWT documentation for more details. + exp: Math.floor(Date.now() / 1000) + 60 * 60 * 24 * 7, + }; + // Generate the access_token + const access_token = cloud.getToken(payload); + console.log("Token generated by cloud function:", access_token) + // ctx.user is automatically decrypted + console.log(ctx.user) + const authHeader = ctx.headers.authorization; + const token = authHeader.split(' ')[1]; // Extract the JWT + console.log("Token sent by frontend:", token) + const parseToken = cloud.parseToken(token); + console.log("Decrypted data from token:", parseToken) +}; +``` + +![getToken-parseToken](/doc-images/getToken-parseToken.png) + +Here is a simple login function that generates a JWT token: + +> Note: For demonstration purposes, the password is queried in plain text and not hashed. This is not recommended in actual development. + +```typescript +import cloud from "@lafjs/cloud"; + +export async function main(ctx: FunctionContext) { + const { username, password } = ctx.body; + + const db = cloud.database(); + const { data: user } = await db + .collection("users") + .where({ username, password }) + .getOne(); + + if (!user) { + return "invalid username or password"; + } + + // payload of token + const payload = { + uid: user._id, + // The default expiration time of the token is 7 days. Please make sure to provide the `exp` field, see JWT documentation for more details. + exp: Math.floor(Date.now() / 1000) + 60 * 60 * 24 * 7, + }; + const access_token = cloud.getToken(payload); + + return { + access_token, + uid: user._id, + username: user.username, + expired_at: payload.exp, + }; +}; +``` + +## Cloud Function Global Cache + +The global memory singleton object of the cloud function allows sharing data across multiple invocations and different cloud functions. + +`cloud.shared` is a standard Map object in JavaScript. You can refer to the MDN documentation to learn how to use it: [Map](https://developer.mozilla.org/en-US/docs/Web/JavaScript/Reference/Global_Objects/Map) + +Use cases: + +1. Initialize global configurations into shared, such as WeChat development information, SMS sending configurations. +2. Share common methods, such as checkPermission, to improve the performance of cloud functions. +3. Cache hot data, such as caching WeChat access_tokens. (It is recommended to use it sparingly, as this object is allocated in the node vm heap and has memory limitations.) + +::: info +The cache will be cleared completely after the application restarts. It will be preserved if there is no restart. +::: + +```typescript +import cloud from "@lafjs/cloud"; + +export async function main(ctx: FunctionContext) { + cloud.shared.set(key, val); // Set a cache + cloud.shared.get(key); // Get the value of a cache + cloud.shared.has(key); // Check if a cache exists + cloud.shared.delete(key); // Delete a cache + cloud.shared.clear(); // Clear all caches + // ... Other methods can be found in the MDN Map documentation above +}; +``` + +## Native MongoDriverObject Instance in Cloud Functions + +It is recommended that those familiar with MongoDB use the native MongoDriverObject instance to manipulate databases. + +You can refer to the MongoDB official CRUD documentation for learning how to use it: [MongoDB](https://www.mongodb.com/docs/mongodb-shell/crud/) + +Here is a simple example: + +```typescript +import cloud from "@lafjs/cloud"; + +export async function main(ctx: FunctionContext) { + const db = cloud.mongo.db; + const data = { + name : "张三" + }; + const res = await db.collection('test').insertOne(data); + console.log(res); +}; +``` \ No newline at end of file diff --git a/docs/en/guide/function/index.md b/docs/en/guide/function/index.md new file mode 100644 index 0000000000..b1d408e6f3 --- /dev/null +++ b/docs/en/guide/function/index.md @@ -0,0 +1,59 @@ +--- +title: Introduction to Cloud Functions +--- + +# {{ $frontmatter.title }} + +## Overview of Cloud Functions + +`Laf Cloud Functions` are JavaScript code that runs in the cloud. + +Cloud functions can be written in TypeScript/JavaScript and do not require server management. They can be written, debugged, and saved online in the Web Development Console. + +With Laf Cloud Functions, you can quickly develop backend code without the need to purchase or manage servers. It also comes with a built-in database and object storage, greatly reducing the difficulty of backend development. + +Each cloud function is a separate TypeScript file. Laf provides the `@lafjs/cloud` module specifically for cloud functions, making it more convenient to write them. + +## Creating Cloud Functions + +After creating and entering the Laf application, click the "Functions" button on the top left of the page, then click the plus sign to add a cloud function. + +![create-function](/doc-images/create-function.png) + +**Function Name**: Can use letters, numbers, "-", "_", ".". Function names must be unique. + +**Tags**: Used for classification and management. Cloud functions can be filtered by tag name. Multiple tags can be set for each cloud function. + +**Request Method**: Only selected request methods are allowed. + +**Function Description**: Helps to understand the functionality of the cloud function, similar to comments or remarks. + +**Function Template**: Choosing different function templates initializes different code. + +## Editing Cloud Functions + +Laf comes with a Web IDE that allows you to edit, run (debug), and deploy cloud functions directly in the browser. + +![edit-cloudfunction](/doc-images/edit-cloudfunction.png) + +## Running Cloud Functions + +Cloud functions can be directly executed and debugged after being written. Unpublished cloud functions can also be tested here. + +![run-cloudfunction](/doc-images/run-cloudfunction.png) + +If you add code with `console.log` to print logs in the cloud function, it will be displayed directly in the Console console when running. The return value of the cloud function will also be printed in the execution result. + +You can switch request methods and configure request parameters here. The default is a `GET` request. The displayed request method depends on the selected request method(s). + +## Publish Cloud Function + +Once the cloud function is published, it will become effective and can be requested by the frontend. + +::: danger +**Any code modifications made to the cloud function will be automatically cached in the current browser and will only be saved and take effect after publishing!** +::: + +![publish-cloudfunction](/doc-images/publish-cloudfunction.png) + +On the publish page, the left side displays the previously published code, while the right side displays the code that is about to be published. This allows for easy comparison of the code differences to check if publishing is necessary. \ No newline at end of file diff --git a/docs/en/guide/function/init.md b/docs/en/guide/function/init.md new file mode 100644 index 0000000000..a0b4833ca8 --- /dev/null +++ b/docs/en/guide/function/init.md @@ -0,0 +1,27 @@ +--- +title: Application Initialization +--- + + + +# {{ $frontmatter.title }} + +After restarting, Laf application can perform initialization operations in a cloud function, such as database initialization and global cache initialization. + +:::tip +The cloud function for application initialization has a fixed name: `__init__`, other names will not take effect. +::: + +Here is a simple example of an application initialization cloud function that records the startup time when the Laf application starts: + +```typescript +import cloud from '@lafjs/cloud' + +export async function main(ctx: FunctionContext) { + // Record the startup time of the Laf application + const db = cloud.database() + await db.collection('init').add({ + time: Date.now() + }) +} +``` \ No newline at end of file diff --git a/docs/en/guide/function/interceptor.md b/docs/en/guide/function/interceptor.md new file mode 100644 index 0000000000..17b6f3c9cf --- /dev/null +++ b/docs/en/guide/function/interceptor.md @@ -0,0 +1,68 @@ +# Cloud Function Interceptors + +Interceptors are a powerful feature in cloud functions that allow you to intercept and modify requests and responses before they are processed by the function. This can be useful for various purposes such as authentication, logging, error handling, and more. + +## How Interceptors Work + +When a request is made to a cloud function, it goes through a series of interceptors before reaching the actual function logic. Each interceptor has the ability to modify the request or response object, and can also choose to pass the request to the next interceptor in the chain. + +## Creating an Interceptor + +To create an interceptor, you need to define a function that accepts two parameters: the request object and a callback function. The callback function should be called with either an error object (if an error occurred) or the modified request object. + +Here's an example of how to create an interceptor that adds a timestamp to the request object: + +```javascript +function addTimestamp(request, callback) { + request.timestamp = Date.now(); + callback(null, request); +} +``` + +## Registering an Interceptor + +Once you have defined an interceptor, you can register it with your cloud function. This can be done using the appropriate configuration option provided by your cloud function provider. + +For example, if you are using Google Cloud Functions, you can register an interceptor by defining it in the `index.js` file and adding it to the `interceptors` array in the function configuration: + +```javascript +const functions = require('firebase-functions'); + +exports.myFunction = functions.https.onRequest((request, response) => { + // Function logic here +}); + +exports.myFunction.interceptors = [addTimestamp]; +``` + +## Conclusion + +Interceptors provide a powerful way to intercept and modify requests and responses in cloud functions. They can be used for various purposes and allow for flexible customization of your function's behavior. Consider using interceptors to enhance the functionality and security of your cloud functions. + +# {{ $frontmatter.title }} + +If you need to use an interceptor, you need to create a cloud function named `__interceptor__`. + +::: info +`__interceptor__` is a fixed name. +::: + +Laf Cloud Function Interceptor is a pre-interceptor that is requested before all cloud function requests. + +Only if the return value of the interceptor is `true`, the original cloud function will be requested. + +Here is a simple interceptor example. If the IP is `111.111.111.111`, the original cloud function can be accessed: + +```typescript +export async function main(ctx: FunctionContext) { + // Get the actual IP of the request + const ip = ctx.headers['x-real-ip'] + if(ip == '111.111.111.111'){ + return true + }else{ + return false + } +} +``` + +Feel free to explore more uses! \ No newline at end of file diff --git a/docs/en/guide/function/logs.md b/docs/en/guide/function/logs.md new file mode 100644 index 0000000000..696fe014c6 --- /dev/null +++ b/docs/en/guide/function/logs.md @@ -0,0 +1,41 @@ +--- +title: Cloud Function History Logs +--- + +# {{ $frontmatter.title }} + +:::warning +Only logs generated by using `console` in cloud functions will be saved! +::: + +All historical logs of cloud functions are automatically saved in the log section, and logs are retained for 3 days. + +You can filter logs based on the `requestId` and the name of the cloud function. + +![function-log](/doc-images/function-log.png) + +1. Click on a log to switch to the log section. +2. Use the `requestId` and the name of the cloud function to search for specific logs. +3. Click on an individual log to view detailed content. + +## Manual Log Cleaning + +The running logs of Laf cloud functions are stored in a hidden collection called `__function_logs__`. Therefore, we can use the method to manipulate the database in cloud functions to clean the logs. + +The following is the code to clean all logs in a cloud function: + +::: danger +The following operation will delete all historical logs. Please proceed with caution. +::: + +```typescript +import cloud from '@lafjs/cloud' + +export async function main(ctx: FunctionContext) { + console.log('Hello World') + // Database, remove all logs + const db = cloud.database(); + const res = await db.collection('__function_logs__').remove({multi:true}) + console.log(res) +} +``` diff --git a/docs/en/guide/function/trigger.md b/docs/en/guide/function/trigger.md new file mode 100644 index 0000000000..465cc5f018 --- /dev/null +++ b/docs/en/guide/function/trigger.md @@ -0,0 +1,73 @@ +--- +title: Cloud Function Triggers +--- + +# {{ $frontmatter.title }} + +A trigger is a way to initiate the execution of a function. We can bind triggers to cloud functions to control when they are invoked. + +::: info +Currently, triggers only support the scheduled trigger type. +::: + +## Creating a Trigger + +![](/doc-images/create-injector.png) + +Step 1: Click on the trigger button on the right side of the function list. + +Step 2: Create a new trigger. + +Step 3: Enter a name for the trigger for easy search and management in the future. + +Step 4: Select the type of trigger. Currently, only scheduled triggers are supported. + +Step 5: Choose the execution strategy for the scheduled task. Here, you can use cron expressions. + +If you are not familiar with `cron` expressions, you can click on the link below to choose from three preset options. + +Alternatively, you can click on the [More Examples](https://crontab.guru/examples.html) button to find the scheduled strategy you need or generate your own expression using the [Online Cron Expression](http://cron.ciding.cc/) website. + +## A specific example + +In the development process of WeChat Official Accounts, it is usually necessary to use a central control server to obtain and refresh the `access_token`. The `access_token` used by other business logic servers should all come from this central control server, and should not be refreshed individually. Otherwise, conflicts may occur, resulting in the access_token being overwritten and affecting the business. +Generally, the validity period of the `access_token` is 2 hours, and this problem can be solved well using a scheduled trigger. + +1. First, let's create a cloud function named `get-access-token`. + +```typescript +import cloud from '@lafjs/cloud' +import axios from 'axios' + +const AT_BASEURL = 'https://api.weixin.qq.com/cgi-bin/token?grant_type=client_credential' +const APP_ID = 'your appid' +const APP_SECRET = 'your app secret' + +export async function main(ctx: FunctionContext) { + + try { + const request_url = `${AT_BASEURL}&appid=${APP_ID}&secret=${APP_SECRET}` + const { data: { access_token, expires_in, errcode, errmsg } } = await axios.get(request_url) + + if (errcode) return { errcode, errmsg } + + // write into cloud.shared + cloud.shared.set('_access_token', access_token) + cloud.shared.set('_expired_time', expires_in) + + } catch (error) { + return { + errcode: 500, + err_msg: 'Get access token error: ' + error.toString() } + } + +} +``` + +In the above code, we follow the [Interface Call Request Instructions](https://developers.weixin.qq.com/doc/offiaccount/Basic_Information/Get_access_token.html) in the WeChat official documentation to request the `access_token` and store it in `cloud.shared`. Other business functions can read it at any time. + +2. Use a trigger + +![](/doc-images/use-injector.png) + +Do you remember the process of creating a trigger just now? We only need to select the associated function as `get-access-token` and set the Cron expression to 2 hours to request the WeChat API every two hours to refresh the `access_token` to ensure validity. \ No newline at end of file diff --git a/docs/en/guide/function/use-function.md b/docs/en/guide/function/use-function.md new file mode 100644 index 0000000000..6b61e90662 --- /dev/null +++ b/docs/en/guide/function/use-function.md @@ -0,0 +1,180 @@ +--- +title: Usage of Cloud Functions +--- + +# {{ $frontmatter.title }} + +## Cloud Function Parameters + +In the `main` function, you can retrieve the request information passed by the user through the `ctx` parameter. The following example demonstrates how to read the `Query` parameter `username` passed from the front-end: + +![function-query](/doc-images/function-query.png) + +The cloud function code is as follows: + +```js +export function main(ctx: FunctionContext) { + console.log(ctx.query.username) +}; +``` + +You can also read the `body` parameter `username` passed from the front-end HTTP request: + +![function-query](/doc-images/function-body.png) + +The cloud function code is as follows: + +```js +export function main(ctx: FunctionContext) { + console.log(ctx.body.username) +}; +``` + +`ctx` has the following properties: + +| Property | Description | +| --------------- | ---------------------------------------------------------------------------------- | +| `ctx.requestId` | The unique ID of the current request | +| `ctx.method` | The method of the current request, e.g., `GET`, `POST` | +| `ctx.headers` | All headers of the request | +| `ctx.user` | The token value parsed when using Http Bearer Token authentication | +| `ctx.query` | The query parameters of the current request | +| `ctx.body` | The body parameters of the current request | +| `ctx.request` | HTTP request, consistent with the `Request` instance in `express` | +| `ctx.response` | HTTP response, consistent with the `Response` instance in `express` | +| `ctx.socket` | [WebSocket](https://developer.mozilla.org/en-US/docs/Web/API/WebSocket) instance, [Laf WebSocket Documentation](/guide/function/websocket.html) | +| `ctx.files` | Uploaded files (an array of [File](https://developer.mozilla.org/en-US/docs/Web/API/File) objects) | +| `ctx.env` | Custom environment variables for the application ([env](env.md)) | + +## Cloud Function Return Values + +So, how do we return data from a cloud function? + +### Method 1: Using `return` + +It's simple, just use `return` in the cloud function. + +```js +export function main (ctx: FunctionContext) { + // Here is an example with a string, you can return any data type. + return "This is the data to be returned to the front-end" +}; +``` + +Cloud function return values support various types: + +```js +return Buffer.from("whoop"); // Buffer +return { + some: "json"; +} // Object, will be processed as JSON +return ("

some html

"); // HTML +return ("Sorry, we cannot find that!"); // String +``` + +### Method 2: Setting the response headers, status code, and response body using ctx.response + +Here `ctx.response` aligns with the `Response` instance in the Express framework. + +Here are some common methods of the res object: + +```js +ctx.response.send(body) // Sends the response body, which can be a string, a Buffer object, a JSON object, an array, etc. +ctx.response.json(body) // Sends a JSON response +ctx.response.status(statusCode) // Sets the HTTP response status code +ctx.response.setHeader(name, value) // Sets a response header +... +``` + +If you need to send a status code, you can use `ctx.response.status`: + +```js +ctx.response.status(403); // Sends a 403 status code +``` + +If you need to send data in chunks, you can use `ctx.response.write` and `ctx.response.end`: + +For example: + +```js +export function main (ctx: FunctionContext) { + // Set response headers + ctx.response.type = 'text/html'; + ctx.response.status = 200; + // Write data chunks + ctx.response.write(''); + ctx.response.write('

Hello, world!

'); + ctx.response.write(''); + // End the response + ctx.response.end(); +}; +``` + +## Support for asynchronous operations + +In practical applications, cloud functions may need to perform asynchronous operations such as network requests, database operations, etc. + +Fortunately, cloud functions themselves support asynchronous invocation. You just need to add `async` before the function, and it will easily support asynchronous operations. + +When creating a new cloud function, `async` is already added before the main function by default. + +:::tip +When performing asynchronous operations in cloud functions, try to use `await` to wait for their completion. +::: + +In the following example, we query the "user" collection in the database: + +```js +import cloud from '@lafjs/cloud' +const db = cloud.database() + +export default async function (ctx: FunctionContext) { + // Add await before asynchronous operations like database queries + const res = await db.collection('user').get() + // Synchronous operations do not require await + console.log(res.data) +}; +``` + +## Introduction to Cloud Function Import + +Now you can directly import another cloud function into a cloud function. + +::: tip +The imported cloud function needs to be published before it can be imported. +::: + +Import syntax: + +```js +// funcName is the default function +import funcName from '@/funcName' +// Import function named func +import { func } from '@/funcName' +``` + +For example, import the `util` cloud function into the `test` cloud function. + +```js +// util cloud function +export default async function main () { + return "util has been imported" +}; + +export function add(a: number, b: number) { + return a + b +}; +``` + +```js +// test cloud function +import util, { add } from '@/util' + +export async function main(ctx: FunctionContext) { + // Since the default method of util is async, await is needed + console.log(await util()) + // Output: "util has been imported" + console.log(add(1, 2)) + // Output: 3 +} +``` \ No newline at end of file diff --git a/docs/en/guide/function/websocket.md b/docs/en/guide/function/websocket.md new file mode 100644 index 0000000000..8962024b1d --- /dev/null +++ b/docs/en/guide/function/websocket.md @@ -0,0 +1,136 @@ +--- +title: Handling WebSocket Connections in Cloud Functions +--- + +# {{ $frontmatter.title }} + +WebSocket is a long-lived connection that allows for bi-directional communication between the server and the client. However, unfortunately, many Serverless architectures do not support WebSocket functionality. Fortunately, Laf makes it easy to use WebSocket, opening up more possibilities for our applications. + +## WebSocket Cloud Function + +To use WebSocket in Laf, you need to create a cloud function named `__websocket__`. + +:::tip +The name of the WebSocket cloud function must be `__websocket__`. Any other name will not work. +::: + +Here is an example of how to handle WebSocket in a cloud function: + +```typescript +export async function main(ctx: FunctionContext) { + + if (ctx.method === "WebSocket:connection") { + ctx.socket.send('hi connection succeed') + } + + if (ctx.method === 'WebSocket:message') { + const { data } = ctx.params; + console.log(data.toString()); + ctx.socket.send("I have received your message"); + } +} +``` + +For more information, please refer to: + +## Client WebSocket Connection + +```typescript +const wss = new WebSocket("wss://your-own-appid.laf.run/__websocket__"); + +wss.onopen = (socket) => { + console.log("connected"); + wss.send("hi"); +}; + +wss.onmessage = (res) => { + console.log("Received a new message..."); + console.log(res.data); +}; + +wss.onclose = () => { + console.log("closed"); +}; +``` + + +## Connection Demo + +1. Replace the `__websocket__` code and publish it. + +```js +import cloud from '@lafjs/cloud' + +export async function main(ctx: FunctionContext) { + // Initialize the websocket user Map list + // You can also use a database to save it, this example code uses the global cache of Laf functions + let wsMap = cloud.shared.get("wsMap") // Get wsMap + if(!wsMap){ + wsMap = new Map() + cloud.shared.set("wsMap", wsMap) // Set wsMap + } + + // Websocket connection established + if (ctx.method === "WebSocket:connection") { + const userId = generateUserId() + wsMap = cloud.shared.get("wsMap") // Get wsMap + wsMap.set(userId, ctx.socket); + cloud.shared.set("wsMap", wsMap) // Set wsMap + ctx.socket.send("Connection established, your userID is: "+userId); + } + + // Websocket message event + if (ctx.method === "WebSocket:message") { + const { data } = ctx.params; + console.log("Received message: ",data.toString()); + const userId = getKeyByValue(wsMap, ctx.socket); + ctx.socket.send("Server has received the message event, your userID is: "+userId); + } + + // Websocket close event + if (ctx.method === "WebSocket:close") { + wsMap = cloud.shared.get("wsMap") // Get wsMap + const userId = getKeyByValue(wsMap, ctx.socket); + wsMap.delete(userId); + cloud.shared.set("wsMap", wsMap) // Set wsMap + ctx.socket.send("Server has received the close event message, your userID is: "+userId); + } +} + +// Generate random user ID +function generateUserId() { + return Math.random().toString(36).substring(2, 15); +} + +// Traverse userID +function getKeyByValue(map, value) { + for (const [key, val] of map.entries()) { + if (val === value) { + return key; + } + } +} +``` + +2. Get your WebSocket URL + +The general format is: + +Replace `your-own-appid` with your Laf application appid. + +`wss://your-own-appid.laf.run/__websocket__` + +3. Connect to the WebSocket URL on the client side and obtain the userID. + +4. Create a new cloud function to push messages using the userID. + +```js +import cloud from '@lafjs/cloud' + +export async function main(ctx: FunctionContext) { + const userID = '' + let wsMap = cloud.shared.get("wsMap") + ctx.socket = wsMap.get(userID) + ctx.socket.send("Message test"); +} +``` \ No newline at end of file diff --git a/docs/en/guide/index.md b/docs/en/guide/index.md new file mode 100644 index 0000000000..5dfb5967e9 --- /dev/null +++ b/docs/en/guide/index.md @@ -0,0 +1,142 @@ +--- +title: Introduction +--- + +# {{ $frontmatter.title }} + +## 👀 What is `laf` + +- `laf` is a cloud development platform that allows for rapid application development. +- `laf` is an open-source Backend as a Service (BaaS) development platform. +- `laf` is an out-of-the-box serverless development platform. +- `laf` combines functions computing, database, and object storage into one integrated development platform. +- `laf` can be seen as an open-source version of Tencent Cloud Development, Google Firebase, or UniCloud. + +[`laf`](https://github.com/labring/laf) enables every development team to have their own cloud development platform! + +## 🎉 What does `laf` offer + +- Multi-application management: create and start/stop applications without the need for server configuration. Get your application online in just one minute. +- Cloud functions: `laf` provides function computing services to quickly implement backend business logic. +- Cloud database: provides ready-to-use database services for application development. +- Cloud storage: offers professional file object storage services compatible with S3 and other storage service interfaces. +- WebIDE: write code online with comprehensive type hints and code auto-completion. Write functions just like writing a blog post and publish them instantly. +- Static hosting: supports hosting static websites, allowing for quick deployment without the need for configuring nginx. +- Client Db: supports clients using [laf-client-sdk](https://github.com/labring/laf/tree/main/packages/client-sdk) to "directly connect" to the database. Access control through access policies greatly enhances application development efficiency. +- WebSocket: supports long-lived connections, leaving no blind spots in business processes. + +Further understand `laf` through the following screenshots: + + + + + + + + + + + + + + + + + + +
Cloud FunctionsCloud Storage
Cloud Database: Data ManagementApplication List
+ +## 👨‍💻 Who should use `laf`? + +1. Front-end developers + `laf` = full-stack developers. Front-end developers can become true full-stack developers with `laf`. + + - `laf` provides [laf-client-sdk](https://github.com/labring/laf/tree/main/packages/client-sdk) for front-end developers, which is suitable for any JS runtime environment. + - `laf` utilizes JS/TS for cloud functions development, allowing seamless integration of front-end and back-end code without barriers. + - `laf` offers static website hosting, allowing direct deployment of web pages built by front-end developers without the need for server, nginx, or domain configuration. + - `laf` will provide various SDKs for different clients (e.g., Flutter/Android/iOS), providing backend development services and a consistent development experience for all client developers. + +2. Backend developers can free themselves from mundane tasks and focus on business logic to improve development efficiency. + + - `laf` saves effort in server maintenance, multi-environment deployment, and management. + - `laf` eliminates the need for configuring and debugging nginx. + - `laf` eliminates repetitive work such as manually deploying databases and addressing security concerns for each project. + - `laf` streamlines the iterative experience, avoiding the hassle of spending half a day for every modification and release. + - `laf` allows you to view function runtime logs on the web anytime, anywhere, without the need to connect to servers or search through logs. + - `laf` enables you to "write a function like writing a blog post," making it easy to publish with just a few clicks. + +3. Users of cloud development platforms, especially those using WeChat cloud development. With `laf`, you can enjoy a more powerful and faster development experience without being locked into WeChat's cloud development platform. + + - You can deliver source code to clients, allowing them to privately deploy a `laf` + your cloud development application. Closed-source cloud development services cannot provide independently runnable source code. + - You can deploy your own products to your own servers at any time in the future since `laf` is open-source and free. + - You can modify and customize your own cloud development platform as `laf` is open-source and highly scalable. + +4. Node.js developers: `laf` is developed using Node.js, so you can consider it as a more convenient Node.js development platform or framework. + + - You can write and debug functions online without restarting the service. Publish with just one click. + - You can view and search for function call logs online. + - You don't have to worry about setting up databases, object storage, or nginx. Get your application online anytime, anywhere. + - You can easily deploy a piece of Node.js code to the cloud, such as a web crawler or monitoring code. Write Node.js like writing a blog! + +5. Independent developers and startup teams can save costs, start quickly, and focus on their business. + + - Reduce the project development process to quickly start and shorten the product validation cycle. + - Greatly increase iteration speed, adapt to changes at any time, and publish instantly. + - Focus on the core product business, quickly launch a minimum viable product (MVP), and conduct rapid product and market validation. + - One person + `laf` = a team + +> Life is short, you need laf :) + +## 💥 What can be done with laf + +> `laf` is a backend development platform for applications and theoretically can be used for any type of application! + +1. Quickly develop WeChat mini-programs/public accounts using laf: e-commerce, social networking, utilities, education, finance, games, short videos, communities, enterprises, and more! + + - WeChat mini-programs require HTTPS access, and you can directly use [laf.run](https://laf.run) to create an application that provides HTTPS interface services for mini-programs. + - You can deploy the H5 pages and admin portal directly to be statically hosted by `laf`. + - Host H5 directly on `laf` and configure the assigned domain name in the public account for online access. + - Use cloud functions to implement WeChat authorization, payment, and other services. + - Use cloud storage to store user data such as videos and avatars. + +2. Develop Android or iOS applications using laf. + + - Use cloud functions, cloud databases, and cloud storage for business processing. + - Deploy the backend management (admin) of the application to be statically hosted by `laf`. + - Use cloud functions to implement WeChat authorization, payment, hot updates, and other features. + +3. Deploy personal blogs and corporate websites. + + - Deploy static-generated blogs such as vuepress / hexo / hugo to `laf` static hosting with one click, see [laf-cli](https://github.com/labring/laf/tree/main/cli). + - Use cloud functions to handle user messages, comments, access statistics, and more. + - Use cloud functions to extend other capabilities of the blog, such as courses, voting, questions, etc. + - Use cloud storage to store videos, images. + - Use cloud functions for web scraping, push notifications, and other functionalities. + +4. Enterprise information construction: Private deployment of a `laf` cloud development platform for enterprises. + + - Quickly develop internal information systems for enterprises, with quick online, modification, and iteration to reduce costs. + - Supports multiple applications and accounts, allowing for isolation and connectivity between different departments and systems. + - Take advantage of the `laf` community ecosystem and directly use existing `laf` applications out-of-the-box to reduce costs. + - `laf` is open source and free, without concerns about technology lock-in, allowing for customization and freedom of use. + +5. "Cloud at Hand" for individual developers. + + - `laf` allows developers to easily deploy code to the cloud. + - Just like typing a piece of text in the notes on your phone, it automatically syncs to the cloud and can be accessed and executed by anyone on the internet. + - `laf` is every developer's "scratch pad," where you can write a function like taking notes. + - `laf` is every developer's personal assistant, allowing you to write functions such as sending scheduled SMS or email notifications at any time. + +6. Others + - Some users use `laf` cloud storage as a personal cloud drive. + - Some users use `laf` applications as a log server, collecting client-side log data and using cloud functions for analysis and statistics. + - Some users use `laf` for running web scraping, capturing third-party news and information content. + - Some users use `laf` cloud functions as webhooks, listening to Git repository commit messages and pushing them to DingTalk or WeChat Work groups. + - Some users use `laf` cloud functions for monitoring, regularly checking the health status of online services. + - ... + +## 🏘️ Community + +- [Forum](https://forum.laf.run/) +- [WeChat Group](https://cdn.jsdelivr.net/gh/yangchuansheng/imghosting3@main/uPic/2022-04-22-14-21-MRJH9o.png) +- QQ Group: `603059673` +- Official WeChat Account: `laf-dev` \ No newline at end of file diff --git a/docs/en/guide/laf-assistant/index.md b/docs/en/guide/laf-assistant/index.md new file mode 100644 index 0000000000..637c3a3b60 --- /dev/null +++ b/docs/en/guide/laf-assistant/index.md @@ -0,0 +1,69 @@ +--- +title: Laf Assistant +--- + +# {{ $frontmatter.title }} + +`Laf Assistant` is developed by the former community user "Bai Ye" out of interest. It is built on top of `laf-cli`. + +For more details, please refer to: [Laf Assistant: Cloud Development Has Never Been This Easy!](https://mp.weixin.qq.com/s/SueTSmWFXDySaRSx3uAPIg) + +## Installation + +Search for `Laf Assistant` in the `VSCode` application or click the link to [install online](https://marketplace.visualstudio.com/items?itemName=NightWhite.laf-assistant). + +:::tip +To use `Laf Assistant`, a minimum Node version of 18 is required. + +node version >= 18 +::: + +## Modifying VSCode Settings + +Search for `typescript.preferences.importModuleSpecifier` in the settings and change it to `non-relative`. + +## Initialization + +Create a new directory named `laf-cloud` in your frontend project that is connected to Laf Cloud Development. + +Right-click on the `laf-cloud` directory and select "Login". + +You will need to configure the relevant information of the Laf application, including `apiurl`, `pat`, and `appid`. + +- For `apiurl`, add `api` before the Laf website URL you are currently logged into. For example: `https://api.laf.dev` + +- For `pat`, refer to the [laf-cli documentation](/guide/cli/#登录) for the way to obtain it. + +- For `appid`, refer to the [Web IDE introduction](/guide/web-ide/#应用管理) for the Laf application's appid. + +:::warning +By default, `Laf Assistant` will automatically install `laf-cli`. If the automatic installation fails, you can [manually install laf-cli](/guide/cli/#安装). +::: + +After filling in all the information, right-click on the `laf-cloud` directory again and select "Login" to login with the provided credentials. You will see a successful login notification in the bottom right corner. + +Once logged in, continue by right-clicking on the `laf-cloud` directory and selecting "Initialize". This will complete the initialization process. + +:::warning +After successful initialization, many files will be created in the `laf-cloud` directory. Please do not delete them randomly. +::: + +## Synchronize Online Dependencies + +The dependencies installed in the Laf application can be downloaded locally by synchronizing the online dependencies. This is only for the convenience of viewing code hints during development and is not necessary for compilation or code execution. You can synchronize again if new dependencies are added online. + +## Publish/Download All Cloud Functions + +After logging in and initializing, right-click on the `laf-cloud` folder and select "Publish/Download All Cloud Functions". + +## Add New Cloud Function + +Right-click on the `laf-cloud` folder and select "Add New Cloud Function". + +## Managing Individual Cloud Functions + +Open the `laf-cloud/functions` directory and open any cloud function file. Right-click in the editor window to perform actions such as "Publish/Download/Run" the current cloud function. You can also customize keyboard shortcuts in the `VSCode` settings for easier operation. + +## Other Features + +Other features of `laf-cli` have not been added yet. You can check them by typing `laf -h` in the terminal or directly refer to the [laf-cli documentation](/guide/cli/). \ No newline at end of file diff --git a/docs/en/guide/oss/get-sts.md b/docs/en/guide/oss/get-sts.md new file mode 100644 index 0000000000..e8b88d9b98 --- /dev/null +++ b/docs/en/guide/oss/get-sts.md @@ -0,0 +1,53 @@ +--- +title: Generate Temporary Tokens for Cloud Storage (STS) +--- + +# {{ $frontmatter.title }} + +To request cloud storage from places outside of the frontend or cloud function environment, a temporary token called STS is required. The following cloud function can directly request and obtain a STS temporary token. + +## Install Dependencies + +Install the `@aws-sdk/client-sts` dependency (restart the application for it to take effect). + +## Create the `get-oss-sts` Cloud Function + +Create the cloud function `get-oss-sts` and add the following code: + +```typescript +import cloud from "@lafjs/cloud"; +import { STSClient, AssumeRoleCommand } from "@aws-sdk/client-sts"; + +export default async function (ctx: FunctionContext) { + const sts: any = new STSClient({ + region: process.env.OSS_REGION, + endpoint: process.env.OSS_INTERNAL_ENDPOINT, + credentials: { + accessKeyId: process.env.OSS_ACCESS_KEY, + secretAccessKey: process.env.OSS_ACCESS_SECRET, + }, + }); + + const cmd = new AssumeRoleCommand({ + DurationSeconds: 3600, + Policy: + '{"Version":"2012-10-17","Statement":[{"Sid":"Stmt1","Effect":"Allow","Action":"s3:*","Resource":"arn:aws:s3:::*"}]}', + RoleArn: "arn:xxx:xxx:xxx:xxxx", + RoleSessionName: cloud.appid, + }); + + const res = await sts.send(cmd); + + return { + credentials: res.Credentials, + endpoint: process.env.OSS_EXTERNAL_ENDPOINT, + region: process.env.OSS_REGION, + }; +}; +``` + +> Save and publish the cloud function to make it accessible. + +## Accessing Cloud Storage with STS Tokens in Front-end + +@see [Accessing Cloud Storage with STS Tokens in Front-end](use-sts-in-client.md) \ No newline at end of file diff --git a/docs/en/guide/oss/index.md b/docs/en/guide/oss/index.md new file mode 100644 index 0000000000..6443cfac47 --- /dev/null +++ b/docs/en/guide/oss/index.md @@ -0,0 +1,39 @@ +--- +title: Introduction to Cloud Storage +--- + +# {{ $frontmatter.title }} + +`laf` cloud storage is an object storage service based on [MinIO](https://min.io/), providing full support for MinIO object storage service. It also adheres to the standard s3 interface, allowing us to control file operations in the cloud storage through the s3 API. + +## Creating a Bucket + +![create-bucket-1](../../doc-images/create-bucket-1.png) + +Step 1. Switch to the storage page. + +Step 2. Click on the plus sign. + +Step 3. Set the bucket name, which consists of `lowercase letters`, `numbers`, and `-`. + +Step 4. Set the read and write permissions. + +Step 5. Click OK to create successfully. + +## Uploading Files via Web Console + +![upload](../../doc-images/upload.png) + +Step 1. Click on the upload button. + +Step 2. Select the files or folders to upload. Upload them separately for files and folders. + +## Obtaining File Access URLs via Web Console + +Click on the eye icon next to the file. If the file has readable properties enabled, it means you have obtained the access/download URL for the file. + +If the file has non-readable properties enabled, clicking on the eye icon will generate a temporary access link. + +## Deleting Files in Web Console + +To delete a file, simply click on the trash bin symbol. However, please note that currently, it is not possible to delete multiple files simultaneously in the web console. \ No newline at end of file diff --git a/docs/en/guide/oss/oss-by-function.md b/docs/en/guide/oss/oss-by-function.md new file mode 100644 index 0000000000..4fe2781af2 --- /dev/null +++ b/docs/en/guide/oss/oss-by-function.md @@ -0,0 +1,396 @@ +--- +title: Cloud Storage Operations with S3 Client +--- + +# {{ $frontmatter.title }} + +Here are several common operations commands for the S3 client to manipulate the database. For more commands, please refer to the [official documentation of `@aws-sdk/client-s3`](https://docs.aws.amazon.com/AWSJavaScriptSDK/v3/latest/clients/client-s3/index.html). + +:::tip +The S3 client depends on `@aws-sdk/client-s3` which is already installed by default and does not require reinstallation. +::: + +## Initialize the S3 Client + +```typescript +import { S3 } from "@aws-sdk/client-s3"; +const s3Client = new S3({ + endpoint: process.env.OSS_EXTERNAL_ENDPOINT, + region: process.env.OSS_REGION, + credentials: { + accessKeyId: process.env.OSS_ACCESS_KEY, + secretAccessKey: process.env.OSS_ACCESS_SECRET + }, + forcePathStyle: true, +}) +``` + +## Upload Files + +:::tip +If the uploaded file already exists, it will automatically overwrite the previous file. +::: + +```typescript +await s3Client.putObject({ + Bucket: bucket, + Key: path, + Body: content, + ContentType: contentType +}); +``` + +- Bucket: The name of the file bucket, formatted as `${appid}-${bucketName}`. +- Key: The unique identifier of the uploaded file in the S3 storage bucket. + - File path and name: For example, `avatars/user1.png`. + - Folder name (ending with /): For example, `avatars/` used to create a folder. + - Relative path: For example, `avatars/2021/01/`. + - Absolute path: For example, `/avatars/2021/01/user1.png`. +- Body: The file object. + - Blob: The native file object, such as the file selected from the input file control. + - Buffer: The binary data object of Node.js. + - String: The content of a text file. + - ReadableStream: The file stream used to upload large files. +- ContentType (MIME type) + - image/png: PNG image + - image/jpeg: JPEG image + - application/json: JSON file + - text/plain: TXT text file + - application/octet-stream: Binary stream file + - and so on + +## Delete Cloud Storage Objects + +You can delete cloud storage files or folders. + +```typescript +await s3Client.deleteObject({ + Bucket: bucket, + Key: path +}) +``` + +- Bucket: The name of the file bucket, formatted as `${appid}-${bucketName}`. +- Key: The unique identifier of the file in the S3 storage bucket. + - File path and name: For example, `avatars/user1.png`. + - Folder name (ending with /): For example, `avatars/` used to delete a folder. + - Relative path: For example, `avatars/2021/01/`. + - Absolute path: For example, `/avatars/2021/01/user1.png`. + +## Download Cloud Storage Objects + +You can download cloud storage files or folders. + +```typescript +await s3Client.getObject({ + Bucket: bucket, + Key: path +}) +``` + +- Bucket: The name of the file bucket, formatted as `${appid}-${bucketName}`. +- Key: The unique identifier of the file in the S3 storage bucket. + - File path and name: For example, `avatars/user1.png`. + - Folder name (ending with /): For example, `avatars/` used to delete a folder. + - Relative path: For example, `avatars/2021/01/`. + - Absolute path: For example, `/avatars/2021/01/user1.png`. + +The following is the code to retrieve `gen.py`: + +```typescript +import { S3 } from "@aws-sdk/client-s3" + +export default async function (ctx: FunctionContext) { + const s3 = new S3({ + endpoint: process.env.OSS_EXTERNAL_ENDPOINT, + region: process.env.OSS_REGION, + credentials: { + accessKeyId: process.env.OSS_ACCESS_KEY, + secretAccessKey: process.env.OSS_ACCESS_SECRET + }, + forcePathStyle: true, + }) + + const res = await s3.getObject({ + Bucket: 'c5nodm-test', + Key: 'gen.py', + }) + + const data = await res.Body.transformToString() + console.log(data) +} +``` + +## Generating Pre-signed Links for Files + +Used to generate temporary access links for files without read permission. + +```typescript +import { S3, GetObjectCommand } from "@aws-sdk/client-s3"; // Import the GetObjectCommand +import { getSignedUrl } from "@aws-sdk/s3-request-presigner"; // Import at the top of the cloud function + +const url = await getSignedUrl(s3Client, new GetObjectCommand({ + Bucket: bucket, + Key: path, +}), +{ + expiresIn: expiresSeconds +}); +``` + +- **Bucket**: The name of the file bucket in the format `${appid}-${bucketName}`. +- **Key**: The unique identifier of the shared file in the S3 storage bucket. + - File path and name, e.g., `avatars/user1.png`. + - Absolute path, e.g., `/avatars/2021/01/user1.png`. +- **expiresIn**: Access expiration time. After this time, the link will no longer be accessible. + +## Listing Objects in a Cloud Storage Bucket + +```typescript +await s3Client.listObjectsV2({ + Bucket: bucket, + MaxKeys: size, + Prefix: prefix, + StartAfter: startAfter +}) +``` + +- **Bucket**: The name of the file bucket in the format `${appid}-${bucketName}`. +- **Delimiter**: Optional. A delimiter that indicates that listObjectsV2 should not return objects after the delimiter. Used to skip objects within folders. +- **EncodingType**: Optional. Specifies the encoding method for object keys returned in the response. Can be "url". +- **FetchOwner**: Optional. If true, the response will include owner information. Default is false. +- **MaxKeys**: Optional. The maximum number of objects to return. Default is 1000. If there are more results, the response will be truncated. +- **NextContinuationToken**: Optional. Used to get the next page of results for a list operation. Obtained from the previous listObjectsV2 call. +- **Prefix**: Optional. Filters the object keys based on prefix. Only returns keys that match this prefix. Typically used for getting a list of files within a folder. +- **StartAfter**: Optional. The starting point for listing objects. Used for more efficient pagination. + +For example, to list all objects in a bucket with the prefix "photos/", 100 objects per page: + +```typescript +let response = await s3Client.listObjectsV2({ + Bucket: 'bucket-name', + Prefix: 'photos/', + MaxKeys: 100 +}); +``` + +To implement paginated queries for bucket file objects: + +Assume there are 100,000 objects in your bucket and MaxKeys is set to 1000. + +Pagination Method 1: + +```typescript +const pageSize = 2; +let response = await s3Client.listObjectsV2({ + Bucket: 'bucket-name', + MaxKeys: pageSize, + Delimiter: '/' // Show only root-level objects and skip all sub-folders. You can set Delimiter to / +}); +console.log(response); +// If IsTruncated is true, there are more pages +if (response.IsTruncated) { + // To get the next page, use the Key of the last object as StartAfter: + const array = response.Contents; + const lastObject = array[array.length - 1]; + const lastKey = lastObject.Key; + let response2 = await s3Client.listObjectsV2({ + Bucket: 'bucket-name', + MaxKeys: pageSize, + StartAfter: lastKey + }); + console.log(response2); +} +``` + +Pagination Method 2: + +```typescript +const pageSize = 2; +let response = await s3Client.listObjectsV2({ + Bucket: 'bucket-name', + MaxKeys: pageSize +}); +console.log(response); +// If IsTruncated is true, there are more pages +if (response.IsTruncated) { + // Pass the current NextContinuationToken to the ContinuationToken in the next query: + const nextToken = response.NextContinuationToken; + let response2 = await s3Client.listObjectsV2({ + Bucket: 'bucket-name', + MaxKeys: pageSize, + ContinuationToken: nextToken, + }); + console.log(response2); +} +``` + +## Uploading Files to Cloud Storage from Frontend via Cloud Functions + +### Uploading Files to Cloud Functions from Frontend + +Using WeChat Mini Program as an example: + +```js +// Select file from WeChat chat session +wx.chooseMessageFile({ + count: 1, + type: 'all', + success(res) { + // Temporary path of the selected file + const tempFilePaths = res.tempFiles[0].path + wx.uploadFile({ + url: 'Cloud Function URL', + filePath: tempFilePaths, + name: 'file', + success(res) { + console.log(res.data) + } + }) + } +}) +``` + +### Parameters for File Upload in Cloud Functions + +The file uploaded by the frontend can be found in `ctx.files` in the cloud function. + +In Laf, the uploaded file can be found in `ctx.files`. Here is an example of printing the content of `ctx.files` after receiving the file in the cloud function: + +```javascript +console.log(ctx.files) +// Output: +// [ +// { +// fieldname: 'file', +// originalname: 'WWcBsfDKw45X965dd934f04a7b0a405467b91800d7ce.jpg', +// encoding: '7bit', +// mimetype: 'image/jpeg', +// destination: '/tmp', +// filename: 'e6feb0a3-85d7-4fe3-b9ae-78701146acd8.jpg', +// path: '/tmp/e6feb0a3-85d7-4fe3-b9ae-78701146acd8.jpg', +// size: 219043 +// } +// ] +``` + +You can use `ctx.files[0].path` to get the temporary path of the uploaded file. + +You can also use the built-in `fs` library in Node.js to get the file object: + +```javascript +var fs = require("fs"); +var data = await fs.readFileSync(ctx.files[0].path); +``` + +- The `data` variable contains the file object. + +You can use `ctx.files[0].mimetype` to get the mimetype of the uploaded file. + +### Creating a Cloud Function for File Upload + +For example, let's create a cloud function called `uploadFile` with the following code: + +```typescript +import cloud from "@lafjs/cloud"; +import { GetObjectCommand, S3 } from "@aws-sdk/client-s3"; +import { getSignedUrl } from "@aws-sdk/s3-request-presigner"; +const s3Client = new S3({ + endpoint: process.env.OSS_EXTERNAL_ENDPOINT, + region: process.env.OSS_REGION, + credentials: { + accessKeyId: process.env.OSS_ACCESS_KEY, + secretAccessKey: process.env.OSS_ACCESS_SECRET + }, + forcePathStyle: true, +}) +var fs = require("fs"); +const bucketName = 'bucketName' // Replace with the actual bucket name without Laf appid + +// Concatenate the bucket name +function getInternalBucketName() { + const appid = process.env.APP_ID; + return `${appid}-${bucketName}`; +} + +// Upload the file +async function uploadAppFile(key, body, contentType) { + const bucket = getInternalBucketName(); + const res = await s3Client + .putObject({ + Bucket: bucket, + Key: key, + ContentType: contentType, + Body: body, + }) + return res; +} + +// Get the file URL +async function getAppFileUrl(key) { + const bucket = getInternalBucketName(); + const res = await getSignedUrl(s3Client, new GetObjectCommand({ + Bucket: bucket, + Key: key, + })); + return res; +} + +export default async function (ctx: FunctionContext) { + // Get the file object + var data = await fs.readFileSync(ctx.files[0].path); + const res = await uploadAppFile( + ctx.files[0].filename, + data, + ctx.files[0].mimetype + ); + const fileUrl = await getAppFileUrl(ctx.files[0].filename); + return fileUrl; +}; +``` + +### Testing the File Upload + +1. Publish the cloud function and modify the URL of the cloud function request in the frontend. + +2. Select a file in the frontend and upload it. The URL returned by the cloud storage will be printed in the console, indicating a successful upload. + +### Delete the uploaded file + +Create a new cloud function called `deleteFile`. + +```typescript +import cloud from "@lafjs/cloud"; +import { S3 } from "@aws-sdk/client-s3"; +const s3Client = new S3({ + endpoint: process.env.OSS_EXTERNAL_ENDPOINT, + region: process.env.OSS_REGION, + credentials: { + accessKeyId: process.env.OSS_ACCESS_KEY, + secretAccessKey: process.env.OSS_ACCESS_SECRET + }, + forcePathStyle: true, +}) +const bucketName = 'bucketName' // without Laf appid + +// Concatenate the bucket name +function getInternalBucketName() { + const appid = process.env.APP_ID; + return `${appid}-${bucketName}`; +} + +export default async function (ctx: FunctionContext) { + const key = "" // Fill in the file name that was just uploaded, which can be found in the Cloud Storage web page + const bucket = getInternalBucketName() + await s3Client.deleteObject({ + Bucket: bucket, + Key: key + }) +} +``` + +- Key: File storage path + - If the path does not exist, it will be automatically created. + - If the file already exists, it will be automatically overwritten. +- ContentType: The mimetype type of the uploaded file +- Body: The file object. \ No newline at end of file diff --git a/docs/en/guide/oss/upload-by-function.md b/docs/en/guide/oss/upload-by-function.md new file mode 100644 index 0000000000..713d149422 --- /dev/null +++ b/docs/en/guide/oss/upload-by-function.md @@ -0,0 +1,167 @@ +--- +title: Uploading Files to Cloud Functions in Frontend +--- + +# {{ $frontmatter.title }} + +## Uploading Files to Cloud Functions in Frontend + +Using WeChat Mini Program as an example: + +```js +// Select files in WeChat chat session +wx.chooseMessageFile({ + count: 1, + type: 'all', + success(res) { + // Temporary file path of the selected file + const tempFilePaths = res.tempFiles[0].path + wx.uploadFile({ + url: 'Cloud Function URL', + filePath: tempFilePaths, + name: 'file', + success(res) { + console.log(res.data) + } + }) + } +}) +``` + +## Parameters for Receiving Files in Cloud Functions + +The file uploaded by the frontend can be found in `ctx.files` in the cloud function. + +In Laf, the uploaded file can be found in `ctx.files`. The following is an example of printing the `ctx.files` after receiving a file in the cloud function: + +```javascript +console.log(ctx.files) +// Output +// [ +// { +// fieldname: 'file', +// originalname: 'WWcBsfDKw45X965dd934f04a7b0a405467b91800d7ce.jpg', +// encoding: '7bit', +// mimetype: 'image/jpeg', +// destination: '/tmp', +// filename: 'e6feb0a3-85d7-4fe3-b9ae-78701146acd8.jpg', +// path: '/tmp/e6feb0a3-85d7-4fe3-b9ae-78701146acd8.jpg', +// size: 219043 +// } +// ] +``` + +You can use `ctx.files[0].path` to get the temporary path of the uploaded file. + +You can also use the built-in `fs` library in Node.js to get the file object: + +```javascript +var fs = require("fs"); +var data = await fs.readFileSync(ctx.files[0].path); +``` + +- The file object is stored in `data`. + +You can use `ctx.files[0].mimetype` to get the mimetype of the uploaded file. + +## Creating a Cloud Function for Uploading Files + +For example, create a cloud function named `uploadFile`, with the following code: + +```typescript +import cloud from "@lafjs/cloud"; +import { GetObjectCommand, S3 } from "@aws-sdk/client-s3"; +import { getSignedUrl } from "@aws-sdk/s3-request-presigner"; +const s3Client = new S3({ + endpoint: process.env.OSS_EXTERNAL_ENDPOINT, + region: process.env.OSS_REGION, + credentials: { + accessKeyId: process.env.OSS_ACCESS_KEY, + secretAccessKey: process.env.OSS_ACCESS_SECRET + }, + forcePathStyle: true, +}); +var fs = require("fs"); +const bucketName = 'bucketName' // Without Laf application appid + +// Concatenate the bucket name +function getInternalBucketName() { + const appid = process.env.APP_ID; + return `${appid}-${bucketName}`; +} + +// Upload file +async function uploadAppFile(key, body, contentType) { + const bucket = getInternalBucketName(); + const res = await s3Client + .putObject({ + Bucket: bucket, + Key: key, + ContentType: contentType, + Body: body, + }) + return res; +} + +// Get file URL +async function getAppFileUrl(key) { + const bucket = getInternalBucketName(); + const res = await getSignedUrl(s3Client, new GetObjectCommand({ + Bucket: bucket, + Key: key, + })); + return res; +} + +export default async function (ctx: FunctionContext) { + // Get the uploaded file object + var data = await fs.readFileSync(ctx.files[0].path); + const res = await uploadAppFile( + ctx.files[0].filename, + data, + ctx.files[0].mimetype + ); + const fileUrl = await getAppFileUrl(ctx.files[0].filename); + return fileUrl; +}; +``` + +## Testing the Upload + +1. Publish the cloud function and modify the URL of the cloud function in the frontend request. + +2. Select a file in the frontend and upload it. The URL returned by the cloud storage will be printed in the console, indicating success. + +## Delete the uploaded file + +Create a new cloud function named `deleteFile`. + +```typescript +import cloud from "@lafjs/cloud"; +import { S3 } from "@aws-sdk/client-s3"; +const s3Client = new S3({ + endpoint: process.env.OSS_EXTERNAL_ENDPOINT, + region: process.env.OSS_REGION, + credentials: { + accessKeyId: process.env.OSS_ACCESS_KEY, + secretAccessKey: process.env.OSS_ACCESS_SECRET + }, + forcePathStyle: true, +}) +const bucketName = 'bucketName' // The bucket name without Laf application appid + +// Concatenate the bucket name +function getInternalBucketName() { + const appid = process.env.APP_ID; + return `${appid}-${bucketName}`; +} + +export default async function (ctx: FunctionContext) { + const key = "" // Enter the filename of the uploaded file, which can be found on the Cloud Storage web page + const bucket = getInternalBucketName() + await s3Client.deleteObject({ + Bucket: bucket, + Key: key + }) +} +``` \ No newline at end of file diff --git a/docs/en/guide/oss/use-sts-in-client.md b/docs/en/guide/oss/use-sts-in-client.md new file mode 100644 index 0000000000..35628c4ba7 --- /dev/null +++ b/docs/en/guide/oss/use-sts-in-client.md @@ -0,0 +1,62 @@ +--- +title: Accessing Cloud Storage using STS Tokens in Front-end +--- + + + +# {{ $frontmatter.title }} + +To access cloud storage using the STS token information generated by [Using Cloud Functions to Generate Temporary Tokens (STS)](get-sts.md) in the frontend, you can directly upload files to cloud storage instead of uploading them to the cloud function first and then to cloud storage. + +1. Install frontend dependencies: + +```bash +npm i @aws-sdk/client-s3 laf-client-sdk +``` + +2. Write frontend code to implement file upload: + +```typescript +import { S3, PutObjectCommand } from "@aws-sdk/client-s3"; +import { Cloud } from "laf-client-sdk"; + +const APPID = "YOUR_APPID"; // Laf application appid + +const cloud = new Cloud({ + baseUrl: `https://${appid}.laf.run`, + getAccessToken: () => localStorage.getItem("access_token"), +}); + +// Get the temporary token for cloud storage +const { credentials, endpoint, region } = await cloud.invoke("get-sts"); + +const s3Client = new S3({ + endpoint: endpoint, + region: region, + credentials: { + accessKeyId: credentials.AccessKeyId, + secretAccessKey: credentials.SecretAccessKey, + sessionToken: credentials.SessionToken, + expiration: credentials.Expiration, + }, + forcePathStyle: true, +}); + +// Bucket name prefixed with appid +const bucket = `${APPID}-public`; +const cmd = new PutObjectCommand({ + Bucket: bucket, + Key: "index.html", + Body: "Hello from laf oss!", // File content can be binary data, text data, or a File object + ContentType: "text/html", +}); + +const res = await s3Client.send(cmd); +console.log(res); +``` + +::: tip +Here we use the `@aws-sdk/client-s3` library to implement file upload. The usage of other commands is the same as [Cloud Function Calling Cloud Storage](/guide/oss/oss-by-function). + +You can use any SDK compatible with the AWS S3 interface as an alternative, such as the [MinIO JavaScript SDK](https://docs.min.io/docs/javascript-client-quickstart-guide.html). +::: \ No newline at end of file diff --git a/docs/en/guide/quick-start/Todo.md b/docs/en/guide/quick-start/Todo.md new file mode 100644 index 0000000000..95d8ea76d8 --- /dev/null +++ b/docs/en/guide/quick-start/Todo.md @@ -0,0 +1,171 @@ +--- +title: Quick Start +--- + +# {{ $frontmatter.title }} + +We will quickly experience the `laf` cloud development by developing a simple "Todo" feature on [laf.run](https://laf.run). + +![todo-demo](../../doc-images/todo-demo.png) + +## Preparation + +1. You need to register an account on [laf.run](https://laf.run). +2. Log in to [laf.run](https://laf.run), click the `New` button to create an empty application. +3. After the application starts successfully, click the "Development" button on the right to enter the "Development Console" of the application. Next, we will develop the first `laf` application's feature in the "Development Console". + +## Write Cloud Functions + +First, you need to create a cloud function. + +![create-function](../../doc-images/create-function.png) + +Then write the following code in the `get-list` cloud function, and don't forget to find the word "Publish" in the upper right corner and click it after finishing writing. + +```typescript +import cloud from '@lafjs/cloud' + +const db = cloud.database() +export async function main(ctx: FunctionContext) { + // Query data from the list collection + const res = await db.collection('list').get() + // Return to the front end + return res +} +``` + +Following the same way, we create the `add-todo`, `del-todo`, `update-todo` functions, and write the code for each of them. + +`add-todo` + +```typescript +import cloud from '@lafjs/cloud' + +const db = cloud.database() +export async function main(ctx: FunctionContext) { + // ctx.body is the parameter passed in from the front end + const data = ctx.body + const res = await db.collection('list').add(data) + + return res +} +``` + +`del-todo` + +```typescript +import cloud from '@lafjs/cloud' + +const db = cloud.database() +export async function main(ctx: FunctionContext) { + + const { id } = ctx.body + // Delete data based on id + const res = db.collection("list").where({ _id: id }).remove() + + return res +} +``` + +`update-todo` + +```typescript +import cloud from '@lafjs/cloud' + +const db = cloud.database() +export async function main(ctx: FunctionContext) { + + const { id, data } = ctx.body + // _id is a unique primary key and cannot be modified, so we delete it here + delete data._id + // Modify data based on id + const res = await db.collection('list').where({ _id: id }).update(data) + + return res +} + +``` + +:::tip +Reminder again, every modified cloud function needs to be "Published" to take effect! +::: + +## Create Collections + +Here, the collection corresponds to the table in a traditional database and is used to store data. + +![create-gather](../../doc-images/create-gather.png) + +## Front-end + +In the front-end, we use a Vue project as an example, but the operations are the same for React/Angular/WeChat mini programs. +First, you need to install `laf-client-sdk` in your front-end project. + +```shell +npm install laf-client-sdk +``` + +Do you remember the page where we just created the project? We need to go back there and find the `` that we need to use. + +![AppID](../../doc-images/AppID.png) + +Import and create a cloud object, where you need to replace `` with your own. + +```js +import { Cloud } from "laf-client-sdk"; // Import + +// Create cloud object +const cloud = new Cloud({ + baseUrl: "https://.laf.run", // Replace with your AppID here + getAccessToken: () => '', // Set it as empty for now +}); +``` + +Then, call our cloud functions wherever needed in the front-end. + +```js +async function getList() { + // Call the get-list cloud function without any parameters + const res = await cloud.invoke("get-list"); + list.value = res.data; +} + +async function submit() { + if (!newTodo.value) return; + + const obj = { + name: newTodo.value, + complete: false, + }; + // Call the add-todo cloud function with the parameter obj + await cloud.invoke("add-todo", obj); + newTodo.value = ""; + + getList(); +} + + +async function complete(index, id) { + list.value[index].complete = !list.value[index].complete; + // Call the update-todo cloud function with parameters + await cloud.invoke("update-todo", { + id, + data: list.value[index], + }); +} + + +async function del(id) { + // Call the del-todo cloud function with the parameter + await cloud.invoke("del-todo", { id }); + getList(); +} +``` + +With these steps, we have completed the core functionalities of the project. You can also download the code template to try it out. + +:::tip +Remember to modify `` in src/App.vue. +::: + +Template link: \ No newline at end of file diff --git a/docs/en/guide/quick-start/login.md b/docs/en/guide/quick-start/login.md new file mode 100644 index 0000000000..87857c209d --- /dev/null +++ b/docs/en/guide/quick-start/login.md @@ -0,0 +1,202 @@ +--- +title: Quick Start +--- + +# {{ $frontmatter.title }} + +We will quickly experience the `laf` cloud development by developing a simple "user login/registration" feature on [laf.run](https://laf.run). + +The following code may be difficult for beginners to understand, but you can first experience cloud functions and then learn more about the specific content of cloud functions. + +## Preparation + +1. You need to register an account on [laf.run](https://laf.run) +2. Log in to the [laf.run console](https://laf.run), click the "New" button in the upper left corner to create an empty application. +3. After the application starts successfully, click the "Development" button on the right side to enter the "Development Console" of the application. Next, we will develop the first `laf` application function in the "Development Console". + +## Write Cloud Functions + +This tutorial will write two cloud functions: + +- `register` handles registration requests. +- `login` handles login requests. + +### User Registration Cloud Function + +On the "Cloud Function" management page, click "Create Function" to create the registration cloud function `register`. + +Click the `register` function to enter WebIDE and write the following code: + +```typescript +import cloud from "@lafjs/cloud"; +import { createHash } from "crypto"; + +export default async function (ctx: FunctionContext) { + const username = ctx.body?.username || ""; + const password = ctx.body?.password || ""; + + // check param + if (!/[a-zA-Z0-9]{3,16}/.test(username)) return { error: "invalid username" }; + if (!/[a-zA-Z0-9]{3,16}/.test(password)) return { error: "invalid password" }; + + // check username existed + const db = cloud.database(); + const exists = await db + .collection("users") + .where({ username: username }) + .count(); + + if (exists.total > 0) return { error: "username already existed" }; + + // add user + const { id } = await db.collection("users").add({ + username: username, + password: createHash("sha256").update(password).digest("hex"), + created_at: new Date(), + }); + + console.log("user registered: ", id); + return { data: id }; +}; +``` + +Click the "Publish" button in the upper right corner to publish it online! + +### User Login Cloud Function + +The same as above, create the `login` cloud function and write the following code: + +```typescript +import cloud from "@lafjs/cloud"; +import { createHash } from "crypto"; + +export default async function (ctx: FunctionContext) { + const username = ctx.body?.username || ""; + const password = ctx.body?.password || ""; + + // check user login + const db = cloud.database(); + const res = await db + .collection("users") + .where({ + username: username, + password: createHash("sha256").update(password).digest("hex"), + }) + .getOne(); + + if (!res.data) return { error: "invalid username or password" }; + + // generate jwt token + const user_id = res.data._id; + const payload = { + uid: user_id, + exp: Math.floor(Date.now() / 1000) + 60 * 60 * 24 * 7, + }; + + const access_token = cloud.getToken(payload); + + return { + uid: res.data._id, + access_token: access_token, + }; +}; +``` + +Click the "Publish" button in the upper right corner to publish it online! + +## Use curl to call cloud functions + +You can copy the calling address of the cloud function in the upper right corner, +or replace `APPID` in the following curl command with your own APPID and execute it: + +```bash +# Register user +curl -X POST -H "Content-Type: application/json" -d '{"username": "admin", "password": "admin"}' https://APPID.laf.run/register + +# User login +curl -X POST -H "Content-Type: application/json" -d '{"username": "admin", "password": "admin"}' https://APPID.laf.run/login + + +``` + +## Calling Cloud Functions with SDK in Front-end Projects + +Install laf client sdk in your front-end project: + +```bash +npm install laf-client-sdk +``` + +Then fill in the following code: + +```typescript +// user.ts + +import { Cloud } from "laf-client-sdk"; + +const cloud = new Cloud({ + baseUrl: "https://APPID.laf.run", + getAccessToken: () => localStorage.getItem("access_token"), +}); + +// register function +export async function register(username: string, password: string) { + const res = await cloud.invoke("register", { + username: username, + password: password, + }); + + return res; +} + +// login function +export async function login(username: string, password: string) { + const res = await cloud.invoke("login", { + username: username, + password: password, + }); + + if (res.access_token) { + // save token + localStorage.setItem("access_token", res.access_token); + } + + return res; +} +``` + +## Calling Cloud Functions with HTTP Requests in Frontend Projects + +```js +import axios from 'axios' + +const url = 'https://APPID.laf.run' + +// register +async function register(username, password) { + try { + const response = await axios.post(url + "register", { + username: username, + password: password + }); + console.log(response); + } catch (error) { + console.error(error); + } +} + +// login +async function login(username, password) { + try { + const response = await axios.post(url + "login", { + username: username, + password: password + }); + console.log(response); + } catch (error) { + console.error(error); + } +} +``` + +Finally, you can call these two cloud functions in your Vue/React/Angular/WeChat Mini Program pages to implement the login and registration functionalities! \ No newline at end of file diff --git a/docs/en/guide/web-ide/index.md b/docs/en/guide/web-ide/index.md new file mode 100644 index 0000000000..bc201d7829 --- /dev/null +++ b/docs/en/guide/web-ide/index.md @@ -0,0 +1,77 @@ +--- +title: Web IDE +--- + +# {{ $frontmatter.title }} + +Laf provides developers with a very useful Web IDE, where you can write code online, have perfect type prompts and code auto-completion. It allows you to write functions like writing a blog and easily deploy them online! + +## Account Registration + +Server Availability + +- China Server: [laf.run](https://laf.run) + +- Singapore Server: [laf.dev](https://laf.dev) + +::: tip +laf.run is a registered domain, while laf.dev is an unregistered domain. + +laf.dev can directly connect to the OpenAI API. Please choose the server according to your actual business needs. + +Data on China servers and Singapore servers are independent of each other. +::: + +![index](../../doc-images/index.png) + +Click on the "Try Now" button on the homepage. + +![register](../../doc-images/register.png) + +Enter your phone number to register and log in. + +::: tip +If you want to log in with a password, you can set a new password by clicking on "Forgot Password" after successful registration. +::: + +## Create New Application + +![create-laf-app](../../doc-images/create-laf-app.jpg) + +After logging in, you will enter the Laf Web IDE console. + +Here, you can create new applications and manage existing ones. + +![specification](../../doc-images/specification.jpg) + +Choose different application specifications and create Laf applications. + +::: tip +Each account can only create one free application. Please note that free applications may stop running periodically and need to be manually restarted. + +Free applications require monthly renewal in order to continue using them! + +Free applications have lower performance. If you need to use them for formal commercial purposes, please upgrade to a paid specification. +::: + +## Application Management + +The Laf application list displays the application name, application App ID, application running status, server location, expiration date, and options for developing code and managing the application. + +![app-list](../../doc-images/app-list.png) + +## Application Development + +Click the "Develop" button in the application list to enter Laf application development IDE. + +![web-ide-index](../../doc-images/web-ide-index.png) + +The Web IDE is divided into three parts: + + ① Sidebar: It includes cloud functions, collections (database management), storage, logs, user settings, and application settings. + + ② Middle Area: After switching between different features, the displayed content will vary. The image shows the function list of cloud functions, code editor, debugging and documentation, NPM dependency management, console (debug logs), and running results (debugging results). + + ③ Status Bar: It displays the name of the current application, running status, and expiration time. + +Laf's Web IDE is very simple and allows you to edit cloud functions, operate databases, and manage cloud storage online. You can use a browser anytime, anywhere to edit and publish code. \ No newline at end of file diff --git a/docs/en/guide/website-hosting/index.md b/docs/en/guide/website-hosting/index.md new file mode 100644 index 0000000000..82f12db1ea --- /dev/null +++ b/docs/en/guide/website-hosting/index.md @@ -0,0 +1,135 @@ +--- +title: Introduction to Static Website Hosting +--- + +# {{ $frontmatter.title }} + +## Introduction + +Static hosting is a website hosting service based on cloud storage. + +You can upload your web static files to the bucket in cloud storage through the "Development Console" or `laf-cli`. + +Static hosting will automatically provide a unique domain name for your website, and you can also bind a custom domain name. + +## How to use? + +- Package your developed project. +- Create a bucket and put the packaged project into it. +- Click on hosting, and the deployment will be successful! + +## Bind your own domain name + +Laf static website hosting supports binding your own domain name and automatically generates an `SSL` certificate, compatible with both `https` and `http` access. + +@see [Steps to bind your own domain name](#Deployment-Successful) + +## Demo + +### Create a bucket + +![create-bucket](../../doc-images/create-bucket.png) + +### Upload files and enable website hosting + +Upload the compiled files of the front-end project to the newly created cloud storage. + +::: tip +Most of the compiled code for front-end projects is in the `dist` folder. Uploading the `dist` folder will upload all the files in the folder to the cloud storage. +::: + +![open-website](../../doc-images/open-website.png) + +### Deployment Successful + +Now we have successfully deployed the website. Click on the link to access it, or we can click on the custom domain name to bind our own domain. + +::: tip +After binding your own domain name, Laf will automatically configure an `SSL` certificate for your domain name. This process may take 30 seconds to 2 minutes. After that, you can access it via `https`. +::: + +![website-hosting](../../doc-images/website-hosting.png) + +## Automatic Frontend Compilation and Deployment + +You can use `Github Actions` to automatically compile the frontend and push it to Laf Cloud Storage. + + + +1. In the main branch of your frontend project, create an Actions workflow. Here is a basic template: + + This template triggers the Actions automatically whenever new code is pushed to the main branch. + + - `API_URL` is the API address of your current Laf application, such as `laf.dev` corresponds to `https://api.laf.dev`, `laf.run` corresponds to `https://api.laf.run`. + + - `WEB_PATH` is the path of your frontend in the current project. If the frontend project is in the root directory, no modification is needed. If it is under the `web` directory, change it to `'web'`. + + - `DIST_PATH` is the name of the compiled directory. Most projects have a directory named `dist` after compilation. + +```yaml +name: Build +on: + push: + branches: + - "*" + +env: + BUCKET_NAME: ${{ secrets.DOC_BUCKET_NAME }} + LAF_APPID: ${{ secrets.LAF_APPID }} + LAF_PAT: ${{ secrets.LAF_PAT }} + API_URL: "https://api.laf.dev" + WEB_PATH: . + DIST_PATH: "dist" +jobs: + build: + runs-on: ubuntu-latest + steps: + - uses: actions/checkout@v3 + - name: Setup Node.js + uses: actions/setup-node@v3 + with: + node-version: "16.x" + # Install project dependencies + - name: Install Dependencies + working-directory: ${{ env.WEB_PATH }} + run: npm install + # Build the project + - name: Build + working-directory: ${{ env.WEB_PATH }} + run: npm run build + # Install laf-cli + - name: Install Laf-CLI + run: npm i laf-cli -g + # Login to laf api + - name: Login laf-cli + working-directory: ${{ env.WEB_PATH }} + run: laf login -r ${{ env.API_URL }} $LAF_PAT + # Initialize the Laf application and push the compiled code to cloud storage + - name: Init appid and push + working-directory: ${{ env.WEB_PATH }} + env: + LAF_APPID: ${{ env.LAF_APPID }} + run: | + laf app init ${{ env.LAF_APPID }} + laf storage push -f ${{ env.BUCKET_NAME }} ${{ env.DIST_PATH }}/ +``` + +2. Configure some environment variables: + + - `DOC_BUCKET_NAME` is the name of the bucket where your frontend is hosted. + + - `LAF_APPID` is the appid of your Laf application. + + - `LAF_PAT` is the PAT (Personal Access Token) of your Laf application. You can refer to [Obtaining PAT](/guide/cli/#login) for the method to obtain it. + + Set these three parameters as secrets in your project. + + ![auto-build1](/doc-images/auto-build1.png) + + Step 1: Click on `Settings`. + + ![auto-build2](/doc-images/auto-build2.png) + + Step 2: Click on `Actions` under `Secrets and variables`. + + Step 3: Add `DOC_BUCKET_NAME`, `LAF_APPID`, and `LAF_PAT` one by one. \ No newline at end of file diff --git a/docs/en/index.md b/docs/en/index.md new file mode 100644 index 0000000000..bf148f1a7a --- /dev/null +++ b/docs/en/index.md @@ -0,0 +1,37 @@ +--- +title: laf Cloud Development Documentation +layout: home + +hero: + name: laf + text: Write code like writing a blog Go live anytime, anywhere + tagline: life is short, you need laf :) + actions: + - theme: brand + text: Get Started Quickly + link: /guide/ + - theme: alt + text: Start Using Now + link: https://laf.run/ + - theme: alt + text: GitHub Repository + link: https://github.com/labring/laf + +heroImage: https://socialify.git.ci/labring/laf/image?description=1& +tagline: Write functions like writing a blog, go live with ease +actionText: Get Started +actionLink: /guide/ +features: + - title: Cloud Functions + details: Code running in the cloud, write cloud functions using TypeScript, support WebSocket, can be directly called from the frontend. Cloud functions run on the Node.js runtime environment, no cold start required. + - title: Cloud Database + details: Ready to use, no need to create. The database can also be accessed "directly" by the client through access policies, saving more than 90% of backend APIs. Frontend developers can independently complete application development. + - title: Online IDE + details: Support AI-generated cloud functions, code with intelligence. Support intelligent autocompletion for all types, write, debug, and view logs online, code as content. Go live anytime, anywhere. + - title: Triggers + details: Cloud functions can be configured with timers and event triggers, and can listen for database change events. Data changes can trigger the execution of cloud functions (event triggers will be updated later). + - title: Cloud Storage + details: Provide a professional S3-compatible object storage service (MinIO), support Service Account open capabilities. + - title: Static Website Hosting + details: One-click deployment and hosting for frontend static websites, automatically assign subdomains that can be accessed directly. Support binding custom domains and automatically generate SSL certificates. +--- \ No newline at end of file diff --git a/docs/guide/cli/index.md b/docs/guide/cli/index.md index 3df8145b36..66d5835f5d 100644 --- a/docs/guide/cli/index.md +++ b/docs/guide/cli/index.md @@ -26,17 +26,12 @@ cli 的主要功能就是把在 laf web 上的操作集成到命令行里,下 ![](../../doc-images/creat-token.png) -生成 token 之后我们复制放在 laf login 后面执行此命令即可登录。 - -```shell -laf login [pat] -``` - -默认登录 `laf.run`,如果要登录 `laf.dev` 或私有部署的 laf 或其他`laf.run`账号可通过 添加user: +默认登录 `laf.run`,如果要登录 `laf.dev` 或私有部署的 laf 或其他`laf.run`账号可通过 添加 user: ```shell laf user add dev -r https://laf.dev laf user switch dev +laf user list laf login [pat] ``` @@ -56,8 +51,8 @@ laf app list ### 初始化 app -初始化需要用到 appid ,我们可以在 web 端首页拿到。 -这里稍微解释一下,初始化 app 是指在你运行这个命令的目录下生成模版文件,默认是空的,如果想把 web 端的东西同步过来需要加上 -s 。 +初始化需要用到 appid,我们可以在 web 端首页拿到。 +这里稍微解释一下,初始化 app 是指在你运行这个命令的目录下生成模版文件,默认是空的,如果想把 web 端的东西同步过来需要加上 -s。 ::: tip 建议在一个空的目录下尝试此命令。 ::: @@ -74,7 +69,7 @@ laf app init [appid] laf dep pull ``` -如果我们想添加依赖可以使用 add ,注意这里的 add 是在 web 端和本地同时添加这个依赖,添加之后 npm i 即可使用。 +如果我们想添加依赖可以使用 add,注意这里的 add 是在 web 端和本地同时添加这个依赖,添加之后 npm i 即可使用。 ```shell laf dep add [dependencyName] @@ -112,7 +107,7 @@ laf func list laf func pull [funcName] ``` -推送本地云函数代码到 web 。 +推送本地云函数代码到 web。 ```shell laf func push [funcName] @@ -132,13 +127,13 @@ laf func exec [funcName] laf storage list ``` -新建 bucket 。 +新建 bucket。 ```shell laf storage create [bucketName] ``` -删除 bucket 。 +删除 bucket。 ```shell laf storage del [bucketName] @@ -156,7 +151,7 @@ laf storage update [bucketName] laf storage pull [bucketName] [outPath] ``` -上传本地文件到 bucket 。 +上传本地文件到 bucket。 ```shell laf storage push [bucketName] [inPath] diff --git a/docs/guide/function/faq.md b/docs/guide/function/faq.md index 0db19894bb..ecf10a6028 100644 --- a/docs/guide/function/faq.md +++ b/docs/guide/function/faq.md @@ -402,7 +402,7 @@ export default async function (ctx: FunctionContext) { formData.setMethod("get"); formData.addField( "notifyUrl", - "https://APPID.lafyun.com/alipay_notify_callback" + "https://APPID.laf.run/alipay_notify_callback" ); formData.addField("bizContent", { subject: goodsName, diff --git a/docs/guide/website-hosting/images/create-bucket.png b/docs/guide/website-hosting/images/create-bucket.png deleted file mode 100644 index 075841f216..0000000000 Binary files a/docs/guide/website-hosting/images/create-bucket.png and /dev/null differ diff --git a/docs/guide/website-hosting/images/create-hosts-02.png b/docs/guide/website-hosting/images/create-hosts-02.png deleted file mode 100644 index a5b2c54dfc..0000000000 Binary files a/docs/guide/website-hosting/images/create-hosts-02.png and /dev/null differ diff --git a/docs/guide/website-hosting/images/create-hosts.png b/docs/guide/website-hosting/images/create-hosts.png deleted file mode 100644 index 5f59e426d9..0000000000 Binary files a/docs/guide/website-hosting/images/create-hosts.png and /dev/null differ diff --git a/docs/guide/website-hosting/images/upload-01.png b/docs/guide/website-hosting/images/upload-01.png deleted file mode 100644 index 5123890d6b..0000000000 Binary files a/docs/guide/website-hosting/images/upload-01.png and /dev/null differ diff --git a/docs/guide/website-hosting/images/upload-02.png b/docs/guide/website-hosting/images/upload-02.png deleted file mode 100644 index 8068d5e6fb..0000000000 Binary files a/docs/guide/website-hosting/images/upload-02.png and /dev/null differ