You signed in with another tab or window. Reload to refresh your session.You signed out in another tab or window. Reload to refresh your session.You switched accounts on another tab or window. Reload to refresh your session.Dismiss alert
docs: custom functions to typescript guides (#666)
* docs: custom functions to typescript guides
Signed-off-by: David Dal Busco <david.dalbusco@outlook.com>
* 📄 Update LLMs.txt snapshot for PR review
* docs: review
Signed-off-by: David Dal Busco <david.dalbusco@outlook.com>
* 📄 Update LLMs.txt snapshot for PR review
---------
Signed-off-by: David Dal Busco <david.dalbusco@outlook.com>
Co-authored-by: github-actions <41898282+github-actions[bot]@users.noreply.github.com>
Copy file name to clipboardExpand all lines: .llms-snapshots/llms-full.txt
+61-65Lines changed: 61 additions & 65 deletions
Original file line number
Diff line number
Diff line change
@@ -6179,7 +6179,7 @@ Learn how to deploy your React project to Juno. Follow the deployment guide to c
6179
6179
6180
6180
# Code Functions in Rust
6181
6181
6182
-
Learn how to develop, integrate, and extend Juno Satellites with serverless functions written in Rust.
6182
+
Learn how to write and extend serverless functions for your Satellite in Rust.
6183
6183
6184
6184
---
6185
6185
@@ -6205,7 +6205,7 @@ yarn global add @junobuild/cli
6205
6205
pnpm add -g @junobuild/cli
6206
6206
```
6207
6207
6208
-
At the root of your application, eject the Satellite if you haven't already used a template.
6208
+
At your project root, eject the Satellite if you haven't already used a template.
6209
6209
6210
6210
```
6211
6211
juno functions eject
@@ -6229,35 +6229,19 @@ Changes are detected and automatically deployed, allowing you to test your custo
6229
6229
6230
6230
---
6231
6231
6232
-
## Hooks and Data Operations
6232
+
## Hooks
6233
6233
6234
-
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.
6234
+
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.
6235
6235
6236
-
Define a setter function in your frontend dApp as follows:
This code generates a key and persists a document in a collection of the Datastore named "demo".
6243
-
6244
-
Additionally, add a getter to your code:
6245
-
6246
-
```
6247
-
const get = async () => { if (key === undefined) { return; } const record = await getDoc({ collection: "demo", key }); console.log("Get done", record);};
6248
-
```
6249
-
6250
-
Without a hook, executing these two operations one after the other would result in a record containing "hello: world".
6251
-
6252
-
Now, let's create a hook within `src/satellite/src/lib.rs` with the following implementation:
6236
+
The following example declares a hook that listens to changes in the `demo` collection and modifies the document's data before saving it back:
6253
6237
6254
6238
```
6255
6239
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!();
6256
6240
```
6257
6241
6258
-
As outlined in the ([Quickstart](#quickstart)) chapter, run `juno emulator build` to compile and deploy the code locally.
6242
+
**Note:**
6259
6243
6260
-
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.
6244
+
Hooks execute asynchronously, separate from the request-response cycle. Changes made by a hook will not be immediately visible to the caller.
6261
6245
6262
6246
---
6263
6247
@@ -6273,7 +6257,7 @@ This example ensures that any document added to the `notes` collection does not
6273
6257
6274
6258
---
6275
6259
6276
-
## Calling Canisters on ICP
6260
+
## Calling Other Canisters
6277
6261
6278
6262
You can make calls to other canisters on the Internet Computer directly from your serverless functions using `ic_cdk::call`.
6279
6263
@@ -6359,9 +6343,9 @@ Learn how to integrate Juno with SvelteKit. Follow our quickstart guide to set u
6359
6343
6360
6344
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)
6361
6345
6362
-
# Code Functions in TypeScript
6346
+
# Serverless Functions in TypeScript
6363
6347
6364
-
Learn how to develop, integrate, and extend Juno Satellites with serverless functions written in TypeScript.
6348
+
Learn how to write and extend serverless functions for your Satellite in TypeScript.
6365
6349
6366
6350
---
6367
6351
@@ -6387,7 +6371,7 @@ yarn global add @junobuild/cli
6387
6371
pnpm add -g @junobuild/cli
6388
6372
```
6389
6373
6390
-
At the root of your application, eject the Satellite if you haven't already used a template.
6374
+
At your project root, eject the Satellite if you haven't already used a template.
6391
6375
6392
6376
```
6393
6377
juno functions eject
@@ -6399,71 +6383,87 @@ In a new terminal window, kick off the emulator:
6399
6383
juno emulator start --watch
6400
6384
```
6401
6385
6402
-
Now, your local development environment is up and running, ready for you to start coding.
6403
-
6404
-
Every time you make changes to your code, it will automatically recompile and reload.
6386
+
Your local development environment is now up and running.
6405
6387
6406
6388
---
6407
6389
6408
-
## Hooks and Data Operations
6390
+
## Hooks
6409
6391
6410
-
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.
6392
+
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.
6411
6393
6412
-
Define a setter function in your frontend dApp as follows:
6394
+
The following example declares a hook that listens to changes in the `demo` collection and modifies the document's data before saving it back:
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<OnSetDoc>({ collections: ["demo"], run: async (context) => { // Decode the document's data (stored as a blob) const data = decodeDocData<Person>(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 } }); }});
6416
6398
```
6417
6399
6418
-
This code generates a key and persists a document in a collection of the Datastore named "demo".
6400
+
**Note:**
6401
+
6402
+
Hooks execute asynchronously, separate from the request-response cycle. Changes made by a hook will not be immediately visible to the caller.
6419
6403
6420
-
Additionally, add a getter to your code:
6404
+
### Handling Multiple Collections
6405
+
6406
+
If your hook applies to many collections, a switch statement is one way to route logic:
6421
6407
6422
6408
```
6423
-
const get = async () => { if (key === undefined) { return; } const record = await getDoc({ collection: "demo", key }); console.log("Get done", record);};
6409
+
import { defineHook, type OnSetDoc } from "@junobuild/functions";export const onSetDoc = defineHook<OnSetDoc>({ collections: ["posts", "comments"], run: async (context) => { switch (context.data.collection) { case "posts": // Handle posts logic break; case "comments": // Handle comments logic break; } }});
6424
6410
```
6425
6411
6426
-
Without a hook, executing these two operations one after the other would result in a record containing "hello: world".
6427
-
6428
-
Now, let's create a hook within `src/satellite/index.ts` with the following implementation:
6412
+
While this works, you might accidentally forget to handle one of the observed collections. To prevent that, you can use a typed map:
6429
6413
6430
6414
```
6431
-
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<OnSetDoc>({ collections: ["demo"], run: async (context) => { // Decode the document's data (stored as a blob) const data = decodeDocData<Person>(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 } }); }});
6415
+
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<OnSetDoc>({ collections, run: async (context) => { const fn: Record<OnSetDocCollection, RunFunction<OnSetDocContext>> = { posts: yourFunction, comments: yourOtherFunction }; await fn[context.data.collection as OnSetDocCollection]?.(context); }});
6432
6416
```
6433
6417
6434
-
Once saved, your code should be automatically compiled and deployed.
6435
-
6436
-
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.
6418
+
This ensures all collections are handled and you'll get a TypeScript error if one is missing.
6437
6419
6438
6420
---
6439
6421
6440
-
## Assertions
6422
+
## Custom Functions
6441
6423
6442
-
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.
6424
+
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.
6425
+
6426
+
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.
6427
+
6428
+
### Query vs. Update
6429
+
6430
+
A **query** is a read-only function. It returns data without modifying any state. Queries are fast and suitable for fetching or computing information.
6431
+
6432
+
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.
6433
+
6434
+
### Defining a Function
6435
+
6436
+
Describe your function's input and output shapes using the `j` type system, then pass them to `defineQuery` or `defineUpdate` along with your handler:
6443
6437
6444
6438
```
6445
-
import { decodeDocData } from "@junobuild/functions/sdk";import { defineAssert, type AssertSetDoc } from "@junobuild/functions";interface NoteData { text: string;}export const assertSetDoc = defineAssert<AssertSetDoc>({ collections: ["notes"], assert: (context) => { const data = decodeDocData<NoteData>(context.data.data.proposed.data); if (data.text.toLowerCase().includes("hello")) { throw new Error("The text must not include the word 'hello'"); } }});
6439
+
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; }});
6446
6440
```
6447
6441
6448
-
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.
6442
+
Handlers can be synchronous or asynchronous. Both `args` and `returns` are optional.
6449
6443
6450
-
---
6444
+
### Calling from the Frontend
6445
+
6446
+
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:
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.
6454
+
## Assertions
6455
6455
6456
-
Here's how you can rewrite your assertion using Zod for a cleaner and more declarative approach:
6456
+
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.
6457
6457
6458
6458
```
6459
-
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<AssertSetDoc>({ collections: ["notes"], assert: (context) => { const data = decodeDocData<NoteData>(context.data.data.proposed.data); noteSchema.parse(data); }});
6459
+
import { decodeDocData } from "@junobuild/functions/sdk";import { defineAssert, type AssertSetDoc } from "@junobuild/functions";interface NoteData { text: string;}export const assertSetDoc = defineAssert<AssertSetDoc>({ collections: ["notes"], assert: (context) => { const data = decodeDocData<NoteData>(context.data.data.proposed.data); if (data.text.toLowerCase().includes("hello")) { throw new Error("The text must not include the word 'hello'"); } }});
6460
6460
```
6461
6461
6462
-
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.
6462
+
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.
6463
6463
6464
6464
---
6465
6465
6466
-
## Calling Canisters on ICP
6466
+
## Calling Other Canisters
6467
6467
6468
6468
This is useful if you want to:
6469
6469
@@ -6483,25 +6483,21 @@ The `args` field contains a tuple with the Candid type definition and the corres
6483
6483
6484
6484
The `call` function handles both encoding the request and decoding the response using the provided types.
6485
6485
6486
-
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.
6486
+
To encode and decode these calls, you need JavaScript structures that match the Candid IDL types used by the target canister.
6487
6487
6488
6488
---
6489
6489
6490
-
## Handling Multiple Collections
6490
+
## Schema Types
6491
6491
6492
-
If your hook applies to many collections, a switch statement is one way to route logic:
6492
+
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()`.
6493
6493
6494
-
```
6495
-
import { defineHook, type OnSetDoc } from "@junobuild/functions";export const onSetDoc = defineHook<OnSetDoc>({ collections: ["posts", "comments"], run: async (context) => { switch (context.data.collection) { case "posts": // Handle posts logic break; case "comments": // Handle comments logic break; } }});
6496
-
```
6497
-
6498
-
While this works, you might accidentally forget to handle one of the observed collections. To prevent that, you can use a typed map:
6494
+
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.
6499
6495
6500
6496
```
6501
-
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<OnSetDoc>({ collections, run: async (context) => { const fn: Record<OnSetDocCollection, RunFunction<OnSetDocContext>> = { posts: yourFunction, comments: yourOtherFunction }; await fn[context.data.collection as OnSetDocCollection]?.(context); }});
0 commit comments