From 00cd3adafcb5799ee7647b84c42eb3c4ac122e7d Mon Sep 17 00:00:00 2001 From: David Dal Busco Date: Sun, 15 Mar 2026 20:42:30 +0100 Subject: [PATCH 1/4] docs: custom functions to typescript guides Signed-off-by: David Dal Busco --- docs/guides/components/functions/hooks.md | 49 +---- docs/guides/components/functions/setup.mdx | 2 +- docs/guides/rust.mdx | 14 +- docs/guides/typescript.mdx | 199 ++++++++++++--------- 4 files changed, 123 insertions(+), 141 deletions(-) diff --git a/docs/guides/components/functions/hooks.md b/docs/guides/components/functions/hooks.md index ed996391..7b7b9eaa 100644 --- a/docs/guides/components/functions/hooks.md +++ b/docs/guides/components/functions/hooks.md @@ -1,48 +1,3 @@ -Serverless Functions are triggered by hooks, which respond to events occurring in the Satellite, such as setting a document. Before implementing a hook that manipulates data ("backend"), let's first set up a JavaScript function in your ("frontend") dApp. +Hooks respond to events occurring in your Satellite, such as a document being created or updated. They run automatically in the background and are not invoked directly. -Define a setter function in your frontend dApp as follows: - -```typescript -interface Example { - hello: string; -} - -let key: string | undefined; - -const set = async () => { - key = crypto.randomUUID(); - - const record = await setDoc({ - collection: "demo", - doc: { - key, - data: { - hello: "world" - } - } - }); - - console.log("Set done", record); -}; -``` - -This code generates a key and persists a document in a collection of the Datastore named "demo". - -Additionally, add a getter to your code: - -```typescript -const get = async () => { - if (key === undefined) { - return; - } - - const record = await getDoc({ - collection: "demo", - key - }); - - console.log("Get done", record); -}; -``` - -Without a hook, executing these two operations one after the other would result in a record containing "hello: world". +The following example declares a hook that listens to changes in the `demo` collection and modifies the document's data before saving it back: diff --git a/docs/guides/components/functions/setup.mdx b/docs/guides/components/functions/setup.mdx index ce29af86..47907946 100644 --- a/docs/guides/components/functions/setup.mdx +++ b/docs/guides/components/functions/setup.mdx @@ -6,7 +6,7 @@ import Cli from "../cli.mdx"; -At the root of your application, eject the Satellite if you haven't already used a template. +At your project root, eject the Satellite if you haven't already used a template. ```bash juno functions eject diff --git a/docs/guides/rust.mdx b/docs/guides/rust.mdx index 407c0da8..a4cf6b3c 100644 --- a/docs/guides/rust.mdx +++ b/docs/guides/rust.mdx @@ -2,7 +2,7 @@ id: rust title: Rust toc_min_heading_level: 2 -toc_max_heading_level: 3 +toc_max_heading_level: 2 --- # Code Functions in Rust @@ -35,14 +35,12 @@ Changes are detected and automatically deployed, allowing you to test your custo --- -## Hooks and Data Operations +## Hooks import Hooks from "./components/functions/hooks.md"; -Now, let's create a hook within `src/satellite/src/lib.rs` with the following implementation: - ```rust use ic_cdk::print; use junobuild_macros::{ @@ -99,9 +97,11 @@ async fn on_set_doc(context: OnSetDocContext) -> Result<(), String> { include_satellite!(); ``` -As outlined in the [Quickstart](#quickstart) chapter, run `juno emulator build` to compile and deploy the code locally. +:::note + +Hooks execute asynchronously, separate from the request-response cycle. Changes made by a hook will not be immediately visible to the caller. -When testing this feature, if you wait a bit before calling the getter, unlike in the previous step, you should now receive the modified "hello: world checked" text set by the hook. This delay occurs because serverless Functions run fully asynchronously from the request-response between your frontend and the Satellite. +::: --- @@ -137,7 +137,7 @@ This example ensures that any document added to the notes collectio --- -## Calling Canisters on ICP +## Calling Other Canisters You can make calls to other canisters on the Internet Computer directly from your serverless functions using `ic_cdk::call`. diff --git a/docs/guides/typescript.mdx b/docs/guides/typescript.mdx index a15642b6..00abf680 100644 --- a/docs/guides/typescript.mdx +++ b/docs/guides/typescript.mdx @@ -2,12 +2,12 @@ id: typescript title: TypeScript toc_min_heading_level: 2 -toc_max_heading_level: 3 +toc_max_heading_level: 2 --- -# Code Functions in TypeScript +# Serverless Functions in TypeScript -Learn how to develop, integrate, and extend Juno Satellites with serverless functions written in TypeScript. +Learn how to write and extend serverless functions for your Satellite in TypeScript. --- @@ -23,20 +23,16 @@ In a new terminal window, kick off the emulator: juno emulator start --watch ``` -Now, your local development environment is up and running, ready for you to start coding. - -Every time you make changes to your code, it will automatically recompile and reload. +Your local development environment is now up and running. --- -## Hooks and Data Operations +## Hooks import Hooks from "./components/functions/hooks.md"; -Now, let's create a hook within `src/satellite/index.ts` with the following implementation: - ```typescript import { defineHook, type OnSetDoc } from "@junobuild/functions"; import { @@ -80,48 +76,119 @@ export const onSetDoc = defineHook({ }); ``` -Once saved, your code should be automatically compiled and deployed. +:::note -When testing this feature, if you wait a bit before calling the getter, you should now receive the modified "hello: world checked" text set by the hook. This delay occurs because serverless Functions execute fully asynchronously, separate from the request-response cycle between your frontend and the Satellite. +Hooks execute asynchronously, separate from the request-response cycle. Changes made by a hook will not be immediately visible to the caller. ---- +::: -## Assertions +### Handling Multiple Collections -Assertions allow you to validate or reject operations before they are executed. They're useful for enforcing data integrity, security policies, or business rules inside your Satellite, and they run synchronously during the request lifecycle. +If your hook applies to many collections, a switch statement is one way to route logic: ```typescript -import { decodeDocData } from "@junobuild/functions/sdk"; -import { defineAssert, type AssertSetDoc } from "@junobuild/functions"; +import { defineHook, type OnSetDoc } from "@junobuild/functions"; -interface NoteData { - text: string; -} +export const onSetDoc = defineHook({ + collections: ["posts", "comments"], + run: async (context) => { + switch (context.data.collection) { + case "posts": + // Handle posts logic + break; + case "comments": + // Handle comments logic + break; + } + } +}); +``` -export const assertSetDoc = defineAssert({ - collections: ["notes"], - assert: (context) => { - const data = decodeDocData(context.data.data.proposed.data); +While this works, you might accidentally forget to handle one of the observed collections. To prevent that, you can use a typed map: - if (data.text.toLowerCase().includes("hello")) { - throw new Error("The text must not include the word 'hello'"); - } +```typescript +import { + defineHook, + type OnSetDoc, + type OnSetDocContext, + type RunFunction +} from "@junobuild/functions"; + +const collections = ["posts", "comments"] as const; + +type OnSetDocCollection = (typeof collections)[number]; + +export const onSetDoc = defineHook({ + collections, + run: async (context) => { + const fn: Record> = { + posts: yourFunction, + comments: yourOtherFunction + }; + + await fn[context.data.collection as OnSetDocCollection]?.(context); } }); ``` -This example ensures that any document added to the notes collection does not contain the word "hello" (case-insensitive). If it does, the operation is rejected before the data is saved. +This ensures all collections are handled and you'll get a TypeScript error if one is missing. --- -### Validating with Zod +## Custom Functions + +Custom Functions let you define callable endpoints directly inside your Satellite. Unlike hooks, which react to events, custom functions are explicitly invoked - from your frontend or from other modules. -To simplify and strengthen your assertions, we recommend using [Zod](https://zod.dev/) — a TypeScript-first schema validation library. It's already bundled as a dependency of the `@junobuild/functions` package, so there's nothing else to install. +You define them using `defineQuery` or `defineUpdate`, describe their input and output shapes with the `j` type system, and Juno takes care of generating all the necessary bindings under the hood. -Here's how you can rewrite your assertion using Zod for a cleaner and more declarative approach: +### Query vs. Update + +A **query** is a read-only function. It returns data without modifying any state. Queries are fast and suitable for fetching or computing information. + +An **update** is a function that can read and write state. Use it when your logic needs to persist data or trigger side effects. Updates can also be used for read operations when the response needs to be certified - making them suitable for security-sensitive use cases where data integrity must be guaranteed. + +### Defining a Function + +Describe your function's input and output shapes using the `j` type system, then pass them to `defineQuery` or `defineUpdate` along with your handler: + +```typescript +import { defineUpdate } from "@junobuild/functions"; +import { j } from "@junobuild/schema"; + +const Schema = j.strictObject({ + name: j.string(), + id: j.principal() +}); + +export const helloWorld = defineUpdate({ + args: Schema, + returns: Schema, + handler: async ({ args }) => { + // Your logic here + return args; + } +}); +``` + +Handlers can be synchronous or asynchronous. Both `args` and `returns` are optional. + +### Calling from the Frontend + +When you build your project, a type-safe client API is automatically generated based on your function definitions. You can import and call your functions directly from your frontend without writing any glue code: + +```typescript +import { functions } from "../declarations/satellite/satellite.api.ts"; + +await functions.helloWorld({ name: "World", id: Principal.anonymous() }); +``` + +--- + +## Assertions + +Assertions allow you to validate or reject operations before they are executed. They're useful for enforcing data integrity, security policies, or business rules inside your Satellite, and they run synchronously during the request lifecycle. ```typescript -import { z } from "zod"; import { decodeDocData } from "@junobuild/functions/sdk"; import { defineAssert, type AssertSetDoc } from "@junobuild/functions"; @@ -129,29 +196,23 @@ interface NoteData { text: string; } -const noteSchema = z.object({ - text: z - .string() - .refine( - (value) => !value.toLowerCase().includes("hello"), - "The text must not include the word 'hello'" - ) -}); - export const assertSetDoc = defineAssert({ collections: ["notes"], assert: (context) => { const data = decodeDocData(context.data.data.proposed.data); - noteSchema.parse(data); + + if (data.text.toLowerCase().includes("hello")) { + throw new Error("The text must not include the word 'hello'"); + } } }); ``` -This approach is more expressive, easier to extend, and automatically gives you type safety and error messaging. If the validation fails, `parse()` will throw and reject the request. +This example ensures that any document added to the `notes` collection does not contain the word `"hello"` (case-insensitive). If it does, the operation is rejected before the data is saved. --- -## Calling Canisters on ICP +## Calling Other Canisters import Call from "./components/functions/call.md"; @@ -211,57 +272,23 @@ The `args` field contains a tuple with the Candid type definition and the corres The `call` function handles both encoding the request and decoding the response using the provided types. -To encode and decode these calls, you need JavaScript structures that match the Candid types used by the target canister. Currently, the best (and slightly annoying) way to get them is to copy/paste from the `service` output generated by tools like `didc`. It's not ideal, but that’s the current status. We’ll improve this in the future — meanwhile, feel free to reach out if you need help finding or shaping the types. +To encode and decode these calls, you need JavaScript structures that match the Candid IDL types used by the target canister. --- -## Handling Multiple Collections - -If your hook applies to many collections, a switch statement is one way to route logic: - -```typescript -import { defineHook, type OnSetDoc } from "@junobuild/functions"; +## Schema Types -export const onSetDoc = defineHook({ - collections: ["posts", "comments"], - run: async (context) => { - switch (context.data.collection) { - case "posts": - // Handle posts logic - break; - case "comments": - // Handle comments logic - break; - } - } -}); -``` +The `j` type system is Juno's schema layer for custom functions. It is built on top of [Zod](https://zod.dev/) and extends it with types specific to the Juno and Internet Computer environment, such as `j.principal()`. -While this works, you might accidentally forget to handle one of the observed collections. To prevent that, you can use a typed map: +You use it to describe the shape of your function's arguments and return value. These schemas are both validated at runtime and used at build time to generate the necessary types and bindings. ```typescript -import { - defineHook, - type OnSetDoc, - type OnSetDocContext, - type RunFunction -} from "@junobuild/functions"; - -const collections = ["posts", "comments"] as const; - -type OnSetDocCollection = (typeof collections)[number]; - -export const onSetDoc = defineHook({ - collections, - run: async (context) => { - const fn: Record> = { - posts: yourFunction, - comments: yourOtherFunction - }; +import { j } from "@junobuild/schema"; - await fn[context.data.collection as OnSetDocCollection]?.(context); - } +const Schema = j.strictObject({ + name: j.string(), + id: j.principal() }); ``` -This ensures all collections are handled and you'll get a TypeScript error if one is missing. +📦 Import from `@junobuild/schema` From 0c1d8180d30ba840289629db1a3f8e526085742b Mon Sep 17 00:00:00 2001 From: github-actions <41898282+github-actions[bot]@users.noreply.github.com> Date: Sun, 15 Mar 2026 19:44:18 +0000 Subject: [PATCH 2/4] =?UTF-8?q?=F0=9F=93=84=20Update=20LLMs.txt=20snapshot?= =?UTF-8?q?=20for=20PR=20review?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .llms-snapshots/llms-full.txt | 124 ++++++++++++++++------------------ .llms-snapshots/llms.txt | 2 +- 2 files changed, 61 insertions(+), 65 deletions(-) diff --git a/.llms-snapshots/llms-full.txt b/.llms-snapshots/llms-full.txt index cfac712d..ee007262 100644 --- a/.llms-snapshots/llms-full.txt +++ b/.llms-snapshots/llms-full.txt @@ -6205,7 +6205,7 @@ yarn global add @junobuild/cli pnpm add -g @junobuild/cli ``` -At the root of your application, eject the Satellite if you haven't already used a template. +At your project root, eject the Satellite if you haven't already used a template. ``` juno functions eject @@ -6229,35 +6229,19 @@ Changes are detected and automatically deployed, allowing you to test your custo --- -## Hooks and Data Operations +## Hooks -Serverless Functions are triggered by hooks, which respond to events occurring in the Satellite, such as setting a document. Before implementing a hook that manipulates data ("backend"), let's first set up a JavaScript function in your ("frontend") dApp. +Hooks respond to events occurring in your Satellite, such as a document being created or updated. They run automatically in the background and are not invoked directly. -Define a setter function in your frontend dApp as follows: - -``` -interface Example { hello: string;}let key: string | undefined;const set = async () => { key = crypto.randomUUID(); const record = await setDoc({ collection: "demo", doc: { key, data: { hello: "world" } } }); console.log("Set done", record);}; -``` - -This code generates a key and persists a document in a collection of the Datastore named "demo". - -Additionally, add a getter to your code: - -``` -const get = async () => { if (key === undefined) { return; } const record = await getDoc({ collection: "demo", key }); console.log("Get done", record);}; -``` - -Without a hook, executing these two operations one after the other would result in a record containing "hello: world". - -Now, let's create a hook within `src/satellite/src/lib.rs` with the following implementation: +The following example declares a hook that listens to changes in the `demo` collection and modifies the document's data before saving it back: ``` use ic_cdk::print;use junobuild_macros::{ on_delete_asset, on_delete_doc, on_delete_many_assets, on_delete_many_docs, on_set_doc, on_set_many_docs, on_upload_asset,};use junobuild_satellite::{ include_satellite, set_doc_store, OnDeleteAssetContext, OnDeleteDocContext, OnDeleteManyAssetsContext, OnDeleteManyDocsContext, OnSetDocContext, OnSetManyDocsContext, OnUploadAssetContext, SetDoc,};use junobuild_utils::{decode_doc_data, encode_doc_data};use serde::{Deserialize, Serialize};// The data of the document we are looking to update in the Satellite's Datastore.#[derive(Serialize, Deserialize)]struct Person { hello: String,}// We tells the hooks that we only want to listen to changes in collection "demo".#[on_set_doc(collections = ["demo"])]async fn on_set_doc(context: OnSetDocContext) -> Result<(), String> { // We decode the new data saved in the Datastore because it holds those as blob. let mut data: Person = decode_doc_data(&context.data.data.after.data)?; // We update the document's data that was saved in the Datastore with the call from the frontend dapp. // i.e. the frontend saved "hello: world" and we enhance it to "hello: world checked" data.hello = format!("{} checked", data.hello); // We encode the data back to blob. let encode_data = encode_doc_data(&data)?; // We construct the parameters required to call the function that save the data in the Datastore. let doc: SetDoc = SetDoc { data: encode_data, description: context.data.data.after.description, version: context.data.data.after.version, }; // We save the document for the same caller as the one who triggered the original on_set_doc, in the same collection with the same key as well. set_doc_store( context.caller, context.data.collection, context.data.key, doc, )?; Ok(())}// Other hooksinclude_satellite!(); ``` -As outlined in the ([Quickstart](#quickstart)) chapter, run `juno emulator build` to compile and deploy the code locally. +**Note:** -When testing this feature, if you wait a bit before calling the getter, unlike in the previous step, you should now receive the modified "hello: world checked" text set by the hook. This delay occurs because serverless Functions run fully asynchronously from the request-response between your frontend and the Satellite. +Hooks execute asynchronously, separate from the request-response cycle. Changes made by a hook will not be immediately visible to the caller. --- @@ -6273,7 +6257,7 @@ This example ensures that any document added to the `notes` collection does not --- -## Calling Canisters on ICP +## Calling Other Canisters You can make calls to other canisters on the Internet Computer directly from your serverless functions using `ic_cdk::call`. @@ -6359,9 +6343,9 @@ Learn how to integrate Juno with SvelteKit. Follow our quickstart guide to set u Learn how to deploy your SvelteKit project to Juno. Follow the deployment guide to configure static exports, set up your satellite, and publish your site to production.](/docs/guides/sveltekit/deploy.md) -# Code Functions in TypeScript +# Serverless Functions in TypeScript -Learn how to develop, integrate, and extend Juno Satellites with serverless functions written in TypeScript. +Learn how to write and extend serverless functions for your Satellite in TypeScript. --- @@ -6387,7 +6371,7 @@ yarn global add @junobuild/cli pnpm add -g @junobuild/cli ``` -At the root of your application, eject the Satellite if you haven't already used a template. +At your project root, eject the Satellite if you haven't already used a template. ``` juno functions eject @@ -6399,71 +6383,87 @@ In a new terminal window, kick off the emulator: juno emulator start --watch ``` -Now, your local development environment is up and running, ready for you to start coding. - -Every time you make changes to your code, it will automatically recompile and reload. +Your local development environment is now up and running. --- -## Hooks and Data Operations +## Hooks -Serverless Functions are triggered by hooks, which respond to events occurring in the Satellite, such as setting a document. Before implementing a hook that manipulates data ("backend"), let's first set up a JavaScript function in your ("frontend") dApp. +Hooks respond to events occurring in your Satellite, such as a document being created or updated. They run automatically in the background and are not invoked directly. -Define a setter function in your frontend dApp as follows: +The following example declares a hook that listens to changes in the `demo` collection and modifies the document's data before saving it back: ``` -interface Example { hello: string;}let key: string | undefined;const set = async () => { key = crypto.randomUUID(); const record = await setDoc({ collection: "demo", doc: { key, data: { hello: "world" } } }); console.log("Set done", record);}; +import { defineHook, type OnSetDoc } from "@junobuild/functions";import { decodeDocData, encodeDocData, setDocStore} from "@junobuild/functions/sdk";// The data shape stored in the Satellite's Datastoreinterface Person { hello: string;}// We declare a hook that listens to changes in the "demo" collectionexport const onSetDoc = defineHook({ collections: ["demo"], run: async (context) => { // Decode the document's data (stored as a blob) const data = decodeDocData(context.data.data.after.data); // Update the document's data by enhancing the "hello" field const updated = { hello: `${data.hello} checked` }; // Encode the data back to blob format const encoded = encodeDocData(updated); // Save the updated document using the same caller, collection, and key await setDocStore({ caller: context.caller, collection: context.data.collection, key: context.data.key, doc: { data: encoded, description: context.data.data.after.description, version: context.data.data.after.version } }); }}); ``` -This code generates a key and persists a document in a collection of the Datastore named "demo". +**Note:** + +Hooks execute asynchronously, separate from the request-response cycle. Changes made by a hook will not be immediately visible to the caller. -Additionally, add a getter to your code: +### Handling Multiple Collections + +If your hook applies to many collections, a switch statement is one way to route logic: ``` -const get = async () => { if (key === undefined) { return; } const record = await getDoc({ collection: "demo", key }); console.log("Get done", record);}; +import { defineHook, type OnSetDoc } from "@junobuild/functions";export const onSetDoc = defineHook({ collections: ["posts", "comments"], run: async (context) => { switch (context.data.collection) { case "posts": // Handle posts logic break; case "comments": // Handle comments logic break; } }}); ``` -Without a hook, executing these two operations one after the other would result in a record containing "hello: world". - -Now, let's create a hook within `src/satellite/index.ts` with the following implementation: +While this works, you might accidentally forget to handle one of the observed collections. To prevent that, you can use a typed map: ``` -import { defineHook, type OnSetDoc } from "@junobuild/functions";import { decodeDocData, encodeDocData, setDocStore} from "@junobuild/functions/sdk";// The data shape stored in the Satellite's Datastoreinterface Person { hello: string;}// We declare a hook that listens to changes in the "demo" collectionexport const onSetDoc = defineHook({ collections: ["demo"], run: async (context) => { // Decode the document's data (stored as a blob) const data = decodeDocData(context.data.data.after.data); // Update the document's data by enhancing the "hello" field const updated = { hello: `${data.hello} checked` }; // Encode the data back to blob format const encoded = encodeDocData(updated); // Save the updated document using the same caller, collection, and key await setDocStore({ caller: context.caller, collection: context.data.collection, key: context.data.key, doc: { data: encoded, description: context.data.data.after.description, version: context.data.data.after.version } }); }}); +import { defineHook, type OnSetDoc, type OnSetDocContext, type RunFunction} from "@junobuild/functions";const collections = ["posts", "comments"] as const;type OnSetDocCollection = (typeof collections)[number];export const onSetDoc = defineHook({ collections, run: async (context) => { const fn: Record> = { posts: yourFunction, comments: yourOtherFunction }; await fn[context.data.collection as OnSetDocCollection]?.(context); }}); ``` -Once saved, your code should be automatically compiled and deployed. - -When testing this feature, if you wait a bit before calling the getter, you should now receive the modified "hello: world checked" text set by the hook. This delay occurs because serverless Functions execute fully asynchronously, separate from the request-response cycle between your frontend and the Satellite. +This ensures all collections are handled and you'll get a TypeScript error if one is missing. --- -## Assertions +## Custom Functions -Assertions allow you to validate or reject operations before they are executed. They're useful for enforcing data integrity, security policies, or business rules inside your Satellite, and they run synchronously during the request lifecycle. +Custom Functions let you define callable endpoints directly inside your Satellite. Unlike hooks, which react to events, custom functions are explicitly invoked - from your frontend or from other modules. + +You define them using `defineQuery` or `defineUpdate`, describe their input and output shapes with the `j` type system, and Juno takes care of generating all the necessary bindings under the hood. + +### Query vs. Update + +A **query** is a read-only function. It returns data without modifying any state. Queries are fast and suitable for fetching or computing information. + +An **update** is a function that can read and write state. Use it when your logic needs to persist data or trigger side effects. Updates can also be used for read operations when the response needs to be certified - making them suitable for security-sensitive use cases where data integrity must be guaranteed. + +### Defining a Function + +Describe your function's input and output shapes using the `j` type system, then pass them to `defineQuery` or `defineUpdate` along with your handler: ``` -import { decodeDocData } from "@junobuild/functions/sdk";import { defineAssert, type AssertSetDoc } from "@junobuild/functions";interface NoteData { text: string;}export const assertSetDoc = defineAssert({ collections: ["notes"], assert: (context) => { const data = decodeDocData(context.data.data.proposed.data); if (data.text.toLowerCase().includes("hello")) { throw new Error("The text must not include the word 'hello'"); } }}); +import { defineUpdate } from "@junobuild/functions";import { j } from "@junobuild/schema";const Schema = j.strictObject({ name: j.string(), id: j.principal()});export const helloWorld = defineUpdate({ args: Schema, returns: Schema, handler: async ({ args }) => { // Your logic here return args; }}); ``` -This example ensures that any document added to the `notes` collection does not contain the word `"hello"` (case-insensitive). If it does, the operation is rejected before the data is saved. +Handlers can be synchronous or asynchronous. Both `args` and `returns` are optional. ---- +### Calling from the Frontend + +When you build your project, a type-safe client API is automatically generated based on your function definitions. You can import and call your functions directly from your frontend without writing any glue code: + +``` +import { functions } from "../declarations/satellite/satellite.api.ts";await functions.helloWorld({ name: "World", id: Principal.anonymous() }); +``` -### Validating with Zod +--- -To simplify and strengthen your assertions, we recommend using [Zod](https://zod.dev/) — a TypeScript-first schema validation library. It's already bundled as a dependency of the `@junobuild/functions` package, so there's nothing else to install. +## Assertions -Here's how you can rewrite your assertion using Zod for a cleaner and more declarative approach: +Assertions allow you to validate or reject operations before they are executed. They're useful for enforcing data integrity, security policies, or business rules inside your Satellite, and they run synchronously during the request lifecycle. ``` -import { z } from "zod";import { decodeDocData } from "@junobuild/functions/sdk";import { defineAssert, type AssertSetDoc } from "@junobuild/functions";interface NoteData { text: string;}const noteSchema = z.object({ text: z .string() .refine( (value) => !value.toLowerCase().includes("hello"), "The text must not include the word 'hello'" )});export const assertSetDoc = defineAssert({ collections: ["notes"], assert: (context) => { const data = decodeDocData(context.data.data.proposed.data); noteSchema.parse(data); }}); +import { decodeDocData } from "@junobuild/functions/sdk";import { defineAssert, type AssertSetDoc } from "@junobuild/functions";interface NoteData { text: string;}export const assertSetDoc = defineAssert({ collections: ["notes"], assert: (context) => { const data = decodeDocData(context.data.data.proposed.data); if (data.text.toLowerCase().includes("hello")) { throw new Error("The text must not include the word 'hello'"); } }}); ``` -This approach is more expressive, easier to extend, and automatically gives you type safety and error messaging. If the validation fails, `parse()` will throw and reject the request. +This example ensures that any document added to the `notes` collection does not contain the word `"hello"` (case-insensitive). If it does, the operation is rejected before the data is saved. --- -## Calling Canisters on ICP +## Calling Other Canisters This is useful if you want to: @@ -6483,25 +6483,21 @@ The `args` field contains a tuple with the Candid type definition and the corres The `call` function handles both encoding the request and decoding the response using the provided types. -To encode and decode these calls, you need JavaScript structures that match the Candid types used by the target canister. Currently, the best (and slightly annoying) way to get them is to copy/paste from the `service` output generated by tools like `didc`. It's not ideal, but that’s the current status. We’ll improve this in the future — meanwhile, feel free to reach out if you need help finding or shaping the types. +To encode and decode these calls, you need JavaScript structures that match the Candid IDL types used by the target canister. --- -## Handling Multiple Collections +## Schema Types -If your hook applies to many collections, a switch statement is one way to route logic: +The `j` type system is Juno's schema layer for custom functions. It is built on top of [Zod](https://zod.dev/) and extends it with types specific to the Juno and Internet Computer environment, such as `j.principal()`. -``` -import { defineHook, type OnSetDoc } from "@junobuild/functions";export const onSetDoc = defineHook({ collections: ["posts", "comments"], run: async (context) => { switch (context.data.collection) { case "posts": // Handle posts logic break; case "comments": // Handle comments logic break; } }}); -``` - -While this works, you might accidentally forget to handle one of the observed collections. To prevent that, you can use a typed map: +You use it to describe the shape of your function's arguments and return value. These schemas are both validated at runtime and used at build time to generate the necessary types and bindings. ``` -import { defineHook, type OnSetDoc, type OnSetDocContext, type RunFunction} from "@junobuild/functions";const collections = ["posts", "comments"] as const;type OnSetDocCollection = (typeof collections)[number];export const onSetDoc = defineHook({ collections, run: async (context) => { const fn: Record> = { posts: yourFunction, comments: yourOtherFunction }; await fn[context.data.collection as OnSetDocCollection]?.(context); }}); +import { j } from "@junobuild/schema";const Schema = j.strictObject({ name: j.string(), id: j.principal()}); ``` -This ensures all collections are handled and you'll get a TypeScript error if one is missing. +📦 Import from `@junobuild/schema` # Vue diff --git a/.llms-snapshots/llms.txt b/.llms-snapshots/llms.txt index 287fbaf0..f526731c 100644 --- a/.llms-snapshots/llms.txt +++ b/.llms-snapshots/llms.txt @@ -104,7 +104,7 @@ Juno is your self-contained serverless platform for building full-stack web apps - [React](https://juno.build/docs/guides/react.md): Explore how to create a Juno project developed with React. - [Rust](https://juno.build/docs/guides/rust.md): Learn how to develop, integrate, and extend Juno Satellites with serverless functions written in Rust. - [SvelteKit](https://juno.build/docs/guides/sveltekit.md): Explore how to create a Juno project developed with SvelteKit. -- [TypeScript](https://juno.build/docs/guides/typescript.md): Learn how to develop, integrate, and extend Juno Satellites with serverless functions written in TypeScript. +- [TypeScript](https://juno.build/docs/guides/typescript.md): Learn how to write and extend serverless functions for your Satellite in TypeScript. - [Vue](https://juno.build/docs/guides/vue.md): Explore how to create a Juno project developed with Vue. ## Guides - Angular From 9680132fc0edf0fb9aa08df42213ad44c1b4f62d Mon Sep 17 00:00:00 2001 From: David Dal Busco Date: Sun, 15 Mar 2026 20:51:28 +0100 Subject: [PATCH 3/4] docs: review Signed-off-by: David Dal Busco --- docs/guides/rust.mdx | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/docs/guides/rust.mdx b/docs/guides/rust.mdx index a4cf6b3c..3e5af06c 100644 --- a/docs/guides/rust.mdx +++ b/docs/guides/rust.mdx @@ -7,7 +7,7 @@ toc_max_heading_level: 2 # Code Functions in Rust -Learn how to develop, integrate, and extend Juno Satellites with serverless functions written in Rust. +Learn how to write and extend serverless functions for your Satellite in Rust. --- From fa9701b354029d36ad430155b61d84db1619479d Mon Sep 17 00:00:00 2001 From: github-actions <41898282+github-actions[bot]@users.noreply.github.com> Date: Sun, 15 Mar 2026 19:53:15 +0000 Subject: [PATCH 4/4] =?UTF-8?q?=F0=9F=93=84=20Update=20LLMs.txt=20snapshot?= =?UTF-8?q?=20for=20PR=20review?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .llms-snapshots/llms-full.txt | 2 +- .llms-snapshots/llms.txt | 2 +- 2 files changed, 2 insertions(+), 2 deletions(-) diff --git a/.llms-snapshots/llms-full.txt b/.llms-snapshots/llms-full.txt index ee007262..abbdc80c 100644 --- a/.llms-snapshots/llms-full.txt +++ b/.llms-snapshots/llms-full.txt @@ -6179,7 +6179,7 @@ Learn how to deploy your React project to Juno. Follow the deployment guide to c # Code Functions in Rust -Learn how to develop, integrate, and extend Juno Satellites with serverless functions written in Rust. +Learn how to write and extend serverless functions for your Satellite in Rust. --- diff --git a/.llms-snapshots/llms.txt b/.llms-snapshots/llms.txt index f526731c..9f26b5f5 100644 --- a/.llms-snapshots/llms.txt +++ b/.llms-snapshots/llms.txt @@ -102,7 +102,7 @@ Juno is your self-contained serverless platform for building full-stack web apps - [Next.js](https://juno.build/docs/guides/nextjs.md): Explore how to create a Juno project developed with Next.js. - [NodeJS](https://juno.build/docs/guides/nodejs.md): SDK usage in a NodeJS or CLI context - [React](https://juno.build/docs/guides/react.md): Explore how to create a Juno project developed with React. -- [Rust](https://juno.build/docs/guides/rust.md): Learn how to develop, integrate, and extend Juno Satellites with serverless functions written in Rust. +- [Rust](https://juno.build/docs/guides/rust.md): Learn how to write and extend serverless functions for your Satellite in Rust. - [SvelteKit](https://juno.build/docs/guides/sveltekit.md): Explore how to create a Juno project developed with SvelteKit. - [TypeScript](https://juno.build/docs/guides/typescript.md): Learn how to write and extend serverless functions for your Satellite in TypeScript. - [Vue](https://juno.build/docs/guides/vue.md): Explore how to create a Juno project developed with Vue.