Skip to content

Commit 00cd3ad

Browse files
docs: custom functions to typescript guides
Signed-off-by: David Dal Busco <david.dalbusco@outlook.com>
1 parent 5d14caf commit 00cd3ad

4 files changed

Lines changed: 123 additions & 141 deletions

File tree

Lines changed: 2 additions & 47 deletions
Original file line numberDiff line numberDiff line change
@@ -1,48 +1,3 @@
1-
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.
1+
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.
22

3-
Define a setter function in your frontend dApp as follows:
4-
5-
```typescript
6-
interface Example {
7-
hello: string;
8-
}
9-
10-
let key: string | undefined;
11-
12-
const set = async () => {
13-
key = crypto.randomUUID();
14-
15-
const record = await setDoc<Example>({
16-
collection: "demo",
17-
doc: {
18-
key,
19-
data: {
20-
hello: "world"
21-
}
22-
}
23-
});
24-
25-
console.log("Set done", record);
26-
};
27-
```
28-
29-
This code generates a key and persists a document in a collection of the Datastore named "demo".
30-
31-
Additionally, add a getter to your code:
32-
33-
```typescript
34-
const get = async () => {
35-
if (key === undefined) {
36-
return;
37-
}
38-
39-
const record = await getDoc({
40-
collection: "demo",
41-
key
42-
});
43-
44-
console.log("Get done", record);
45-
};
46-
```
47-
48-
Without a hook, executing these two operations one after the other would result in a record containing "hello: world".
3+
The following example declares a hook that listens to changes in the `demo` collection and modifies the document's data before saving it back:

docs/guides/components/functions/setup.mdx

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -6,7 +6,7 @@ import Cli from "../cli.mdx";
66

77
<Cli />
88

9-
At the root of your application, eject the Satellite if you haven't already used a template.
9+
At your project root, eject the Satellite if you haven't already used a template.
1010

1111
```bash
1212
juno functions eject

docs/guides/rust.mdx

Lines changed: 7 additions & 7 deletions
Original file line numberDiff line numberDiff line change
@@ -2,7 +2,7 @@
22
id: rust
33
title: Rust
44
toc_min_heading_level: 2
5-
toc_max_heading_level: 3
5+
toc_max_heading_level: 2
66
---
77

88
# Code Functions in Rust
@@ -35,14 +35,12 @@ Changes are detected and automatically deployed, allowing you to test your custo
3535

3636
---
3737

38-
## Hooks and Data Operations
38+
## Hooks
3939

4040
import Hooks from "./components/functions/hooks.md";
4141

4242
<Hooks />
4343

44-
Now, let's create a hook within `src/satellite/src/lib.rs` with the following implementation:
45-
4644
```rust
4745
use ic_cdk::print;
4846
use junobuild_macros::{
@@ -99,9 +97,11 @@ async fn on_set_doc(context: OnSetDocContext) -> Result<(), String> {
9997
include_satellite!();
10098
```
10199

102-
As outlined in the [Quickstart](#quickstart) chapter, run `juno emulator build` to compile and deploy the code locally.
100+
:::note
101+
102+
Hooks execute asynchronously, separate from the request-response cycle. Changes made by a hook will not be immediately visible to the caller.
103103

104-
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.
104+
:::
105105

106106
---
107107

@@ -137,7 +137,7 @@ This example ensures that any document added to the <code>notes</code> collectio
137137

138138
---
139139

140-
## Calling Canisters on ICP
140+
## Calling Other Canisters
141141

142142
You can make calls to other canisters on the Internet Computer directly from your serverless functions using `ic_cdk::call`.
143143

docs/guides/typescript.mdx

Lines changed: 113 additions & 86 deletions
Original file line numberDiff line numberDiff line change
@@ -2,12 +2,12 @@
22
id: typescript
33
title: TypeScript
44
toc_min_heading_level: 2
5-
toc_max_heading_level: 3
5+
toc_max_heading_level: 2
66
---
77

8-
# Code Functions in TypeScript
8+
# Serverless Functions in TypeScript
99

10-
Learn how to develop, integrate, and extend Juno Satellites with serverless functions written in TypeScript.
10+
Learn how to write and extend serverless functions for your Satellite in TypeScript.
1111

1212
---
1313

@@ -23,20 +23,16 @@ In a new terminal window, kick off the emulator:
2323
juno emulator start --watch
2424
```
2525

26-
Now, your local development environment is up and running, ready for you to start coding.
27-
28-
Every time you make changes to your code, it will automatically recompile and reload.
26+
Your local development environment is now up and running.
2927

3028
---
3129

32-
## Hooks and Data Operations
30+
## Hooks
3331

3432
import Hooks from "./components/functions/hooks.md";
3533

3634
<Hooks />
3735

38-
Now, let's create a hook within `src/satellite/index.ts` with the following implementation:
39-
4036
```typescript
4137
import { defineHook, type OnSetDoc } from "@junobuild/functions";
4238
import {
@@ -80,78 +76,143 @@ export const onSetDoc = defineHook<OnSetDoc>({
8076
});
8177
```
8278

83-
Once saved, your code should be automatically compiled and deployed.
79+
:::note
8480

85-
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.
81+
Hooks execute asynchronously, separate from the request-response cycle. Changes made by a hook will not be immediately visible to the caller.
8682

87-
---
83+
:::
8884

89-
## Assertions
85+
### Handling Multiple Collections
9086

91-
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.
87+
If your hook applies to many collections, a switch statement is one way to route logic:
9288

9389
```typescript
94-
import { decodeDocData } from "@junobuild/functions/sdk";
95-
import { defineAssert, type AssertSetDoc } from "@junobuild/functions";
90+
import { defineHook, type OnSetDoc } from "@junobuild/functions";
9691

97-
interface NoteData {
98-
text: string;
99-
}
92+
export const onSetDoc = defineHook<OnSetDoc>({
93+
collections: ["posts", "comments"],
94+
run: async (context) => {
95+
switch (context.data.collection) {
96+
case "posts":
97+
// Handle posts logic
98+
break;
99+
case "comments":
100+
// Handle comments logic
101+
break;
102+
}
103+
}
104+
});
105+
```
100106

101-
export const assertSetDoc = defineAssert<AssertSetDoc>({
102-
collections: ["notes"],
103-
assert: (context) => {
104-
const data = decodeDocData<NoteData>(context.data.data.proposed.data);
107+
While this works, you might accidentally forget to handle one of the observed collections. To prevent that, you can use a typed map:
105108

106-
if (data.text.toLowerCase().includes("hello")) {
107-
throw new Error("The text must not include the word 'hello'");
108-
}
109+
```typescript
110+
import {
111+
defineHook,
112+
type OnSetDoc,
113+
type OnSetDocContext,
114+
type RunFunction
115+
} from "@junobuild/functions";
116+
117+
const collections = ["posts", "comments"] as const;
118+
119+
type OnSetDocCollection = (typeof collections)[number];
120+
121+
export const onSetDoc = defineHook<OnSetDoc>({
122+
collections,
123+
run: async (context) => {
124+
const fn: Record<OnSetDocCollection, RunFunction<OnSetDocContext>> = {
125+
posts: yourFunction,
126+
comments: yourOtherFunction
127+
};
128+
129+
await fn[context.data.collection as OnSetDocCollection]?.(context);
109130
}
110131
});
111132
```
112133

113-
This example ensures that any document added to the <code>notes</code> collection does not contain the word <code>"hello"</code> (case-insensitive). If it does, the operation is rejected before the data is saved.
134+
This ensures all collections are handled and you'll get a TypeScript error if one is missing.
114135

115136
---
116137

117-
### Validating with Zod
138+
## Custom Functions
139+
140+
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.
118141

119-
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.
142+
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.
120143

121-
Here's how you can rewrite your assertion using Zod for a cleaner and more declarative approach:
144+
### Query vs. Update
145+
146+
A **query** is a read-only function. It returns data without modifying any state. Queries are fast and suitable for fetching or computing information.
147+
148+
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.
149+
150+
### Defining a Function
151+
152+
Describe your function's input and output shapes using the `j` type system, then pass them to `defineQuery` or `defineUpdate` along with your handler:
153+
154+
```typescript
155+
import { defineUpdate } from "@junobuild/functions";
156+
import { j } from "@junobuild/schema";
157+
158+
const Schema = j.strictObject({
159+
name: j.string(),
160+
id: j.principal()
161+
});
162+
163+
export const helloWorld = defineUpdate({
164+
args: Schema,
165+
returns: Schema,
166+
handler: async ({ args }) => {
167+
// Your logic here
168+
return args;
169+
}
170+
});
171+
```
172+
173+
Handlers can be synchronous or asynchronous. Both `args` and `returns` are optional.
174+
175+
### Calling from the Frontend
176+
177+
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:
178+
179+
```typescript
180+
import { functions } from "../declarations/satellite/satellite.api.ts";
181+
182+
await functions.helloWorld({ name: "World", id: Principal.anonymous() });
183+
```
184+
185+
---
186+
187+
## Assertions
188+
189+
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.
122190

123191
```typescript
124-
import { z } from "zod";
125192
import { decodeDocData } from "@junobuild/functions/sdk";
126193
import { defineAssert, type AssertSetDoc } from "@junobuild/functions";
127194

128195
interface NoteData {
129196
text: string;
130197
}
131198

132-
const noteSchema = z.object({
133-
text: z
134-
.string()
135-
.refine(
136-
(value) => !value.toLowerCase().includes("hello"),
137-
"The text must not include the word 'hello'"
138-
)
139-
});
140-
141199
export const assertSetDoc = defineAssert<AssertSetDoc>({
142200
collections: ["notes"],
143201
assert: (context) => {
144202
const data = decodeDocData<NoteData>(context.data.data.proposed.data);
145-
noteSchema.parse(data);
203+
204+
if (data.text.toLowerCase().includes("hello")) {
205+
throw new Error("The text must not include the word 'hello'");
206+
}
146207
}
147208
});
148209
```
149210

150-
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.
211+
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.
151212

152213
---
153214

154-
## Calling Canisters on ICP
215+
## Calling Other Canisters
155216

156217
import Call from "./components/functions/call.md";
157218

@@ -211,57 +272,23 @@ The `args` field contains a tuple with the Candid type definition and the corres
211272

212273
The `call` function handles both encoding the request and decoding the response using the provided types.
213274

214-
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.
275+
To encode and decode these calls, you need JavaScript structures that match the Candid IDL types used by the target canister.
215276

216277
---
217278

218-
## Handling Multiple Collections
219-
220-
If your hook applies to many collections, a switch statement is one way to route logic:
221-
222-
```typescript
223-
import { defineHook, type OnSetDoc } from "@junobuild/functions";
279+
## Schema Types
224280

225-
export const onSetDoc = defineHook<OnSetDoc>({
226-
collections: ["posts", "comments"],
227-
run: async (context) => {
228-
switch (context.data.collection) {
229-
case "posts":
230-
// Handle posts logic
231-
break;
232-
case "comments":
233-
// Handle comments logic
234-
break;
235-
}
236-
}
237-
});
238-
```
281+
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()`.
239282

240-
While this works, you might accidentally forget to handle one of the observed collections. To prevent that, you can use a typed map:
283+
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.
241284

242285
```typescript
243-
import {
244-
defineHook,
245-
type OnSetDoc,
246-
type OnSetDocContext,
247-
type RunFunction
248-
} from "@junobuild/functions";
249-
250-
const collections = ["posts", "comments"] as const;
251-
252-
type OnSetDocCollection = (typeof collections)[number];
253-
254-
export const onSetDoc = defineHook<OnSetDoc>({
255-
collections,
256-
run: async (context) => {
257-
const fn: Record<OnSetDocCollection, RunFunction<OnSetDocContext>> = {
258-
posts: yourFunction,
259-
comments: yourOtherFunction
260-
};
286+
import { j } from "@junobuild/schema";
261287

262-
await fn[context.data.collection as OnSetDocCollection]?.(context);
263-
}
288+
const Schema = j.strictObject({
289+
name: j.string(),
290+
id: j.principal()
264291
});
265292
```
266293

267-
This ensures all collections are handled and you'll get a TypeScript error if one is missing.
294+
📦 Import from `@junobuild/schema`

0 commit comments

Comments
 (0)