@notionhq/apps 0.0.17 → 0.0.19
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/AGENTS.md +27 -0
- package/README.md +77 -35
- package/dist/cli/build.d.ts.map +1 -1
- package/dist/cli/build.js +12 -7
- package/dist/cli/emit-manifest.d.ts +1 -1
- package/dist/cli/emit-manifest.d.ts.map +1 -1
- package/dist/cli/emit-manifest.js +66 -1
- package/dist/index.d.ts +2 -1
- package/dist/index.d.ts.map +1 -1
- package/dist/index.js +3 -1
- package/dist/notion-as-code/custom-agent.d.ts +1 -0
- package/dist/notion-as-code/custom-agent.d.ts.map +1 -1
- package/dist/notion-as-code/custom-agent.js +1 -1
- package/dist/notion-as-code/database.d.ts +90 -48
- package/dist/notion-as-code/database.d.ts.map +1 -1
- package/dist/notion-as-code/database.js +135 -45
- package/dist/notion-as-code/database.test.d.ts +2 -0
- package/dist/notion-as-code/database.test.d.ts.map +1 -0
- package/dist/notion-as-code/index.d.ts +2 -2
- package/dist/notion-as-code/index.d.ts.map +1 -1
- package/dist/notion-as-code/intents.d.ts +34 -2
- package/dist/notion-as-code/intents.d.ts.map +1 -1
- package/dist/notion-as-code/page.d.ts +1 -0
- package/dist/notion-as-code/page.d.ts.map +1 -1
- package/dist/notion-as-code/page.js +1 -0
- package/dist/notion-as-code/schema.d.ts +8 -4
- package/dist/notion-as-code/schema.d.ts.map +1 -1
- package/dist/notion-as-code/views.d.ts +29 -10
- package/dist/notion-as-code/views.d.ts.map +1 -1
- package/dist/sync.d.ts +33 -15
- package/dist/sync.d.ts.map +1 -1
- package/dist/sync.js +12 -0
- package/dist/workflow-access-types.test.d.ts +2 -0
- package/dist/workflow-access-types.test.d.ts.map +1 -0
- package/dist/workflow-access.d.ts +68 -0
- package/dist/workflow-access.d.ts.map +1 -0
- package/dist/workflow-access.js +135 -0
- package/dist/workflow-state.d.ts +13 -0
- package/dist/workflow-state.d.ts.map +1 -0
- package/dist/workflow-state.js +107 -0
- package/dist/workflow.d.ts +65 -9
- package/dist/workflow.d.ts.map +1 -1
- package/dist/workflow.js +119 -5
- package/docs/BUILD.md +134 -4
- package/package.json +1 -1
- package/skills/notion-as-code/SKILL.md +89 -49
- package/skills/sync/SKILL.md +21 -18
- package/skills/workflow/SKILL.md +101 -17
- package/src/cli/build.test.ts +148 -64
- package/src/cli/build.ts +13 -8
- package/src/cli/emit-manifest.ts +107 -0
- package/src/index.ts +21 -1
- package/src/notion-as-code/custom-agent.ts +2 -1
- package/src/notion-as-code/database.test.ts +661 -0
- package/src/notion-as-code/database.ts +349 -127
- package/src/notion-as-code/index.ts +16 -1
- package/src/notion-as-code/intents.ts +39 -2
- package/src/notion-as-code/page.ts +2 -0
- package/src/notion-as-code/schema.ts +11 -3
- package/src/notion-as-code/views.ts +29 -10
- package/src/sync.test.ts +295 -0
- package/src/sync.ts +85 -21
- package/src/workflow-access-types.test.ts +69 -0
- package/src/workflow-access.ts +299 -0
- package/src/workflow-state.ts +152 -0
- package/src/workflow.test.ts +441 -1
- package/src/workflow.ts +230 -13
package/dist/workflow.js
CHANGED
|
@@ -3,6 +3,10 @@ import {
|
|
|
3
3
|
normalizeConnectionRequirements,
|
|
4
4
|
validateTriggerConnections
|
|
5
5
|
} from "./connections.js";
|
|
6
|
+
import {
|
|
7
|
+
hydrateWorkflowAccess,
|
|
8
|
+
normalizeWorkflowAccess
|
|
9
|
+
} from "./workflow-access.js";
|
|
6
10
|
import { createHash } from "node:crypto";
|
|
7
11
|
import { readFile } from "node:fs/promises";
|
|
8
12
|
import { join } from "node:path";
|
|
@@ -12,11 +16,14 @@ import { writeOutput } from "./output.js";
|
|
|
12
16
|
import { resolveRuntimeInput } from "./runtime-input.js";
|
|
13
17
|
import { readRunMetadata } from "./runtime-metadata.js";
|
|
14
18
|
import { createWorkflowTriggers } from "./triggers.generated.js";
|
|
19
|
+
import { createWorkflowStepState } from "./workflow-state.js";
|
|
20
|
+
const MAX_WORKFLOW_WAIT_MS = 7 * 24 * 60 * 60 * 1e3;
|
|
15
21
|
import {
|
|
16
22
|
connections
|
|
17
23
|
} from "./connections.js";
|
|
18
24
|
function workflow(configuration) {
|
|
19
25
|
const requirements = normalizeConnectionRequirements(configuration.connections ?? {});
|
|
26
|
+
const accessRequirements = normalizeWorkflowAccess(configuration.access);
|
|
20
27
|
const connectionDeclarations = configuration.connections === void 0 ? void 0 : structuredClone(configuration.connections);
|
|
21
28
|
const triggers = typeof configuration.triggers === "function" ? configuration.triggers({ triggers: createWorkflowTriggers() }) : configuration.triggers;
|
|
22
29
|
validateTriggerConnections(triggers, requirements);
|
|
@@ -26,21 +33,25 @@ function workflow(configuration) {
|
|
|
26
33
|
name: configuration.name,
|
|
27
34
|
description: configuration.description,
|
|
28
35
|
triggers,
|
|
29
|
-
...configuration.connections === void 0 ? {} : { connections: requirements }
|
|
36
|
+
...configuration.connections === void 0 ? {} : { connections: requirements },
|
|
37
|
+
...configuration.access === void 0 ? {} : { access: accessRequirements }
|
|
30
38
|
},
|
|
31
39
|
async handler(event, options) {
|
|
32
40
|
try {
|
|
33
41
|
event = await resolveRuntimeInput(event);
|
|
34
42
|
const runMetadata = readRunMetadata();
|
|
35
43
|
const baseContext = createCapabilityContext();
|
|
44
|
+
const step = createStep(runMetadata);
|
|
36
45
|
const capabilityContext = {
|
|
37
46
|
...baseContext,
|
|
47
|
+
access: hydrateWorkflowAccess(accessRequirements),
|
|
38
48
|
connections: createWorkflowConnections(
|
|
39
49
|
baseContext.notion,
|
|
40
50
|
connectionDeclarations
|
|
41
51
|
),
|
|
42
52
|
...runMetadata,
|
|
43
|
-
step
|
|
53
|
+
step,
|
|
54
|
+
wait: createWait(step)
|
|
44
55
|
};
|
|
45
56
|
await configuration.handler(event, capabilityContext);
|
|
46
57
|
if (options?.concreteOutput) {
|
|
@@ -48,6 +59,16 @@ function workflow(configuration) {
|
|
|
48
59
|
}
|
|
49
60
|
writeOutput({ _tag: "success", value: { status: "success" } });
|
|
50
61
|
} catch (err) {
|
|
62
|
+
if (err instanceof WorkflowWaitInterrupt) {
|
|
63
|
+
if (options?.concreteOutput) {
|
|
64
|
+
return err.result;
|
|
65
|
+
}
|
|
66
|
+
writeOutput({
|
|
67
|
+
_tag: "wait",
|
|
68
|
+
wait: err.result.wait
|
|
69
|
+
});
|
|
70
|
+
return;
|
|
71
|
+
}
|
|
51
72
|
const error = new ExecutionError(err);
|
|
52
73
|
if (!options?.concreteOutput) {
|
|
53
74
|
writeOutput({
|
|
@@ -81,7 +102,8 @@ function writeStepEvent(event, value, startedAt, emittedAt = performance.now())
|
|
|
81
102
|
`
|
|
82
103
|
);
|
|
83
104
|
}
|
|
84
|
-
function createStep(
|
|
105
|
+
function createStep(metadata) {
|
|
106
|
+
const { runGroupId } = metadata;
|
|
85
107
|
const stepNameByKey = /* @__PURE__ */ new Map();
|
|
86
108
|
async function step(name, optionsOrFn, maybeFn) {
|
|
87
109
|
const options = typeof optionsOrFn === "function" ? void 0 : optionsOrFn;
|
|
@@ -98,7 +120,9 @@ function createStep({ runGroupId }) {
|
|
|
98
120
|
);
|
|
99
121
|
}
|
|
100
122
|
stepNameByKey.set(key, name);
|
|
101
|
-
const
|
|
123
|
+
const id = `${runGroupId}:${key}`;
|
|
124
|
+
const stepState = createWorkflowStepState(metadata, id);
|
|
125
|
+
const context = { id, state: stepState.state };
|
|
102
126
|
const event = { id: context.id, name, key };
|
|
103
127
|
const startedAt = performance.now();
|
|
104
128
|
writeStepEvent("started", event, startedAt, startedAt);
|
|
@@ -124,7 +148,7 @@ function createStep({ runGroupId }) {
|
|
|
124
148
|
);
|
|
125
149
|
return checkpoint.value;
|
|
126
150
|
}
|
|
127
|
-
const value = normalizeWorkflowStepResult(await fn(context));
|
|
151
|
+
const value = normalizeWorkflowStepResult(await stepState.run(() => fn(context)));
|
|
128
152
|
writeStepEvent("success", { ...event, value }, startedAt);
|
|
129
153
|
writeStepEvent(
|
|
130
154
|
"completed",
|
|
@@ -162,6 +186,96 @@ function createStep({ runGroupId }) {
|
|
|
162
186
|
}
|
|
163
187
|
return step;
|
|
164
188
|
}
|
|
189
|
+
class WorkflowWaitInterrupt extends Error {
|
|
190
|
+
constructor(result) {
|
|
191
|
+
super(`Workflow is waiting until ${new Date(result.wait.resumeAtMs).toISOString()}`);
|
|
192
|
+
this.result = result;
|
|
193
|
+
this.name = "WorkflowWaitInterrupt";
|
|
194
|
+
}
|
|
195
|
+
result;
|
|
196
|
+
}
|
|
197
|
+
function createWait(step) {
|
|
198
|
+
return {
|
|
199
|
+
async until(name, options) {
|
|
200
|
+
const keyInput = options.key ?? name;
|
|
201
|
+
const keySegments = [
|
|
202
|
+
"wait",
|
|
203
|
+
"until",
|
|
204
|
+
...typeof keyInput === "string" ? [keyInput] : keyInput
|
|
205
|
+
];
|
|
206
|
+
const resumeAtMs = await step(
|
|
207
|
+
name,
|
|
208
|
+
{ key: keySegments },
|
|
209
|
+
() => resolveWaitUntilMs(options)
|
|
210
|
+
);
|
|
211
|
+
if (Date.now() >= resumeAtMs) {
|
|
212
|
+
return;
|
|
213
|
+
}
|
|
214
|
+
throw new WorkflowWaitInterrupt({
|
|
215
|
+
status: "waiting",
|
|
216
|
+
wait: {
|
|
217
|
+
type: "until",
|
|
218
|
+
name,
|
|
219
|
+
resumeAtMs
|
|
220
|
+
}
|
|
221
|
+
});
|
|
222
|
+
}
|
|
223
|
+
};
|
|
224
|
+
}
|
|
225
|
+
function resolveWaitUntilMs(options) {
|
|
226
|
+
const now = Date.now();
|
|
227
|
+
if ("at" in options) {
|
|
228
|
+
const resumeAtMs = options.at.getTime();
|
|
229
|
+
if (!Number.isFinite(resumeAtMs)) {
|
|
230
|
+
throw new Error("Workflow wait date must be valid.");
|
|
231
|
+
}
|
|
232
|
+
assertWaitFitsCheckpointRetention(resumeAtMs - now);
|
|
233
|
+
return resumeAtMs;
|
|
234
|
+
}
|
|
235
|
+
const durationMs = workflowWaitDurationMs(options.after);
|
|
236
|
+
assertWaitFitsCheckpointRetention(durationMs);
|
|
237
|
+
return now + durationMs;
|
|
238
|
+
}
|
|
239
|
+
function workflowWaitDurationMs(duration) {
|
|
240
|
+
const entries = Object.entries(duration);
|
|
241
|
+
if (entries.length === 0) {
|
|
242
|
+
throw new Error("Workflow wait duration must specify at least one unit.");
|
|
243
|
+
}
|
|
244
|
+
let durationMs = 0;
|
|
245
|
+
for (const [unit, value] of entries) {
|
|
246
|
+
if (!Number.isFinite(value) || !Number.isInteger(value) || value <= 0) {
|
|
247
|
+
throw new Error("Workflow wait duration values must be positive finite integers.");
|
|
248
|
+
}
|
|
249
|
+
durationMs += value * waitDurationUnitMultiplier(unit);
|
|
250
|
+
if (!Number.isSafeInteger(durationMs)) {
|
|
251
|
+
throw new Error("Workflow wait duration is too large.");
|
|
252
|
+
}
|
|
253
|
+
}
|
|
254
|
+
return durationMs;
|
|
255
|
+
}
|
|
256
|
+
function assertWaitFitsCheckpointRetention(durationMs) {
|
|
257
|
+
if (durationMs >= MAX_WORKFLOW_WAIT_MS) {
|
|
258
|
+
throw new Error("Workflow waits must be shorter than 7 days.");
|
|
259
|
+
}
|
|
260
|
+
}
|
|
261
|
+
function waitDurationUnitMultiplier(unit) {
|
|
262
|
+
switch (unit) {
|
|
263
|
+
case "milliseconds":
|
|
264
|
+
return 1;
|
|
265
|
+
case "seconds":
|
|
266
|
+
return 1e3;
|
|
267
|
+
case "minutes":
|
|
268
|
+
return 6e4;
|
|
269
|
+
case "hours":
|
|
270
|
+
return 36e5;
|
|
271
|
+
case "days":
|
|
272
|
+
return 864e5;
|
|
273
|
+
case "weeks":
|
|
274
|
+
return 6048e5;
|
|
275
|
+
default:
|
|
276
|
+
throw new Error(`Unsupported workflow wait duration unit: ${unit}`);
|
|
277
|
+
}
|
|
278
|
+
}
|
|
165
279
|
function createWorkflowStepKey(segments) {
|
|
166
280
|
if (segments.length === 0) {
|
|
167
281
|
throw new Error("Workflow step keys must contain at least one segment.");
|
package/docs/BUILD.md
CHANGED
|
@@ -20,6 +20,7 @@ corresponding capability. The filename becomes its key:
|
|
|
20
20
|
```text
|
|
21
21
|
my-app/
|
|
22
22
|
├── src/
|
|
23
|
+
│ ├── notion.ts
|
|
23
24
|
│ ├── workflows/
|
|
24
25
|
│ │ └── onPageCreated.ts
|
|
25
26
|
│ └── lib/
|
|
@@ -32,21 +33,117 @@ my-app/
|
|
|
32
33
|
|
|
33
34
|
Files elsewhere under `src/` are ordinary modules and enter the build only when imported
|
|
34
35
|
by a capability. Capability discovery does not recurse into subdirectories.
|
|
36
|
+
Notion-as-Code declarations may be inline or in an imported helper. Use a local
|
|
37
|
+
module such as `src/workflows/myWorkflow/lib/notion.ts` for resources owned by one
|
|
38
|
+
capability, and `src/notion.ts` for app-wide declarations or resources without a
|
|
39
|
+
natural capability owner.
|
|
35
40
|
|
|
36
41
|
## Pipeline
|
|
37
42
|
|
|
38
43
|
1. Discover top-level files in the capability directories.
|
|
39
44
|
2. Generate `.notion/entry.ts` with workflow and sync imports and a dispatcher.
|
|
40
45
|
3. Bundle the app's code into `dist/worker.js`, leaving npm packages external.
|
|
41
|
-
4. Evaluate custom-block declarations separately
|
|
42
|
-
configuration, and write `dist/manifest.json`. Blocks do not enter `worker.js`.
|
|
46
|
+
4. Evaluate custom-block declarations separately. Blocks do not enter `worker.js`.
|
|
43
47
|
5. Evaluate worker capabilities in a separate build-only metadata bundle to record
|
|
44
|
-
Notion-as-Code declarations
|
|
48
|
+
Notion-as-Code declarations. Validate workflow access references against those
|
|
49
|
+
declarations before writing the manifest and optional provisioning artifact.
|
|
50
|
+
6. Emit `dist/manifest.json` and, when declarations exist, `dist/provisioning.json`.
|
|
45
51
|
|
|
46
52
|
Capability modules must therefore be importable without secrets or network access.
|
|
47
53
|
Read required environment variables and make requests inside handlers or workflow
|
|
48
54
|
steps, not at module scope.
|
|
49
55
|
|
|
56
|
+
## Declaring Notion-as-Code databases
|
|
57
|
+
|
|
58
|
+
Database declarations use explicit resource IDs and keyed schemas. For a single
|
|
59
|
+
data source, supply the database ID as the first argument and the distinct data
|
|
60
|
+
source ID in `dataSourceResourceId`:
|
|
61
|
+
|
|
62
|
+
```ts
|
|
63
|
+
import { notion } from "@notionhq/apps/notion-as-code";
|
|
64
|
+
|
|
65
|
+
const tasks = notion.database("tasks-db", {
|
|
66
|
+
dataSourceResourceId: "tasks-source",
|
|
67
|
+
name: "Tasks",
|
|
68
|
+
schema: {
|
|
69
|
+
Name: { type: "title", resourceId: "tasks-name" },
|
|
70
|
+
Effort: { type: "text", resourceId: "tasks-effort" },
|
|
71
|
+
},
|
|
72
|
+
});
|
|
73
|
+
```
|
|
74
|
+
|
|
75
|
+
Here `name` is the single-source shorthand: it supplies both the database and
|
|
76
|
+
data-source display names. The source handle is `tasks.dataSource`; pass it to
|
|
77
|
+
`sync({ dataSource, ... })` from `@notionhq/apps`.
|
|
78
|
+
|
|
79
|
+
For multiple sources, use `datasources` instead of the top-level `schema`:
|
|
80
|
+
|
|
81
|
+
````ts
|
|
82
|
+
const work = notion.database("work-db", {
|
|
83
|
+
name: "Work",
|
|
84
|
+
datasources: {
|
|
85
|
+
Tasks: {
|
|
86
|
+
resourceId: "work-tasks-source",
|
|
87
|
+
schema: {
|
|
88
|
+
Name: { type: "title", resourceId: "work-tasks-name" },
|
|
89
|
+
Effort: { type: "text", resourceId: "work-tasks-effort" },
|
|
90
|
+
},
|
|
91
|
+
},
|
|
92
|
+
Projects: {
|
|
93
|
+
resourceId: "work-projects-source",
|
|
94
|
+
schema: {
|
|
95
|
+
Name: { type: "title", resourceId: "work-projects-name" },
|
|
96
|
+
},
|
|
97
|
+
},
|
|
98
|
+
},
|
|
99
|
+
});
|
|
100
|
+
|
|
101
|
+
The keys in `datasources` are the source names, so the handles are indexed by
|
|
102
|
+
those names: `work.datasources.Tasks` and `work.datasources.Projects`. Nested
|
|
103
|
+
data-source configs do not accept `name`. The top-level database `name` remains
|
|
104
|
+
supported and names the database; in a single-source declaration it also
|
|
105
|
+
supplies the source display name. Property configs do not accept `name`; each
|
|
106
|
+
schema key is always the property's name. Typed property access uses
|
|
107
|
+
`work.datasources.Tasks.schema.Effort`; sync primary keys and row properties use
|
|
108
|
+
the string/object key `"Effort"`.
|
|
109
|
+
|
|
110
|
+
Every database, source, and property ID remains explicit and must be unique
|
|
111
|
+
across the provisioning declarations. No IDs are generated from keys or names.
|
|
112
|
+
Preserve explicit IDs when changing authoring keys or display names.
|
|
113
|
+
|
|
114
|
+
Page and teamspace handles accept the same forms through
|
|
115
|
+
`page.addDatabase("database-id", args)` and `teamspace.addDatabase("database-id", args)`.
|
|
116
|
+
Top-level declarations also accept
|
|
117
|
+
`parent: { type: "resourceId", resourceId: "parent-id" }`; omitting it retains the
|
|
118
|
+
private Apps workspace parent. A database must declare at least one data source
|
|
119
|
+
or at least one view. A linked-only declaration may omit `datasources`, or use
|
|
120
|
+
`datasources: {}`, only with a nonempty `views` list. The empty forms `{}`,
|
|
121
|
+
`{ datasources: {} }`, and `{ datasources: {}, views: [] }` are rejected; widened
|
|
122
|
+
or dynamic maps and arrays are checked at runtime. The single-source `schema` and
|
|
123
|
+
multi-source `datasources` forms cannot be combined.
|
|
124
|
+
|
|
125
|
+
Here is a linked-only database whose typed table view references an explicit
|
|
126
|
+
data source resource ID:
|
|
127
|
+
|
|
128
|
+
```ts
|
|
129
|
+
const linked = notion.database("work-linked-db", {
|
|
130
|
+
views: [
|
|
131
|
+
{
|
|
132
|
+
resourceId: "work-issues-table",
|
|
133
|
+
type: "table",
|
|
134
|
+
dataSourceResourceId: "issues-source",
|
|
135
|
+
properties: [{ property: "issue-title", visible: true }],
|
|
136
|
+
},
|
|
137
|
+
],
|
|
138
|
+
});
|
|
139
|
+
````
|
|
140
|
+
|
|
141
|
+
The build converts these declarations to the existing serialized database
|
|
142
|
+
intents: `dataSources` and `properties` remain arrays with exact resource IDs
|
|
143
|
+
and names from the authoring keys. Authoring-only `schema`, `datasources`, and
|
|
144
|
+
database-level `dataSourceResourceId` fields do not leak into the serialized
|
|
145
|
+
database intent. The provisioning JSON envelope and server API are unchanged.
|
|
146
|
+
|
|
50
147
|
## Provisioning artifact and deployment
|
|
51
148
|
|
|
52
149
|
The optional provisioning artifact uses `$schema: "notion:apps-provisioning:v1"`,
|
|
@@ -74,7 +171,8 @@ uploaded project must declare its SDK dependency and a `build` script that invok
|
|
|
74
171
|
Worker using `workersCreateWorker` with `createApp: true`, or updates the existing
|
|
75
172
|
Worker, then uploads source and calls `workersBuildWorker` with the Worker ID.
|
|
76
173
|
For App-linked Workers, the build endpoint coordinates capability registration,
|
|
77
|
-
Notion-as-Code provisioning,
|
|
174
|
+
Notion-as-Code provisioning, database attachment, workflow binding resolution,
|
|
175
|
+
and workflow permission reconciliation before reporting build success.
|
|
78
176
|
It returns the normal build result and run ID, not provisioning state. No separate
|
|
79
177
|
Apps deployment endpoint is required.
|
|
80
178
|
|
|
@@ -97,6 +195,9 @@ flow and local state, and reconciles attachments. It reads the optional
|
|
|
97
195
|
`dist/provisioning.json` locally and does not upload it separately or invoke
|
|
98
196
|
the cloud build endpoint.
|
|
99
197
|
|
|
198
|
+
Workflows with nonempty `access` require cloud deployment. The local-build path
|
|
199
|
+
rejects them after the SDK build and before uploading or deploying the bundle.
|
|
200
|
+
|
|
100
201
|
Resource bindings are not stored in SDK build output. Local deployments retain a
|
|
101
202
|
local state file and report `state_file`; cloud deployments keep installation-owned
|
|
102
203
|
resource mappings and state on the server. Cloud builds do not import local state
|
|
@@ -106,6 +207,35 @@ switching between modes may recreate resources rather than reuse existing bindin
|
|
|
106
207
|
Cloud failures never automatically retry locally. There is no rollback: code or
|
|
107
208
|
resources may already have changed when a deployment fails.
|
|
108
209
|
|
|
210
|
+
## Workflow access
|
|
211
|
+
|
|
212
|
+
`workflow({ access: { handbook: { resource: handbook, level: "view" } }, ... })`
|
|
213
|
+
declares access to a NaC resource and a named runtime binding. The manifest
|
|
214
|
+
contains only the alias and its symbolic `{ type, resourceId, level }`
|
|
215
|
+
requirement; live IDs are not build output.
|
|
216
|
+
Build validation rejects references missing from the current provisioning
|
|
217
|
+
declarations or having the wrong resource kind. `resourceId` is required because
|
|
218
|
+
it identifies the declaration used for this validation and binding.
|
|
219
|
+
|
|
220
|
+
After provisioning, cloud deployment resolves requirements against the successful
|
|
221
|
+
NaC apply result. It reconciles code-managed grants through the existing
|
|
222
|
+
Notion-module permissions and saves those permissions together with resolved
|
|
223
|
+
bindings on the workflow's worker module, keyed by capability and alias, in one
|
|
224
|
+
workflow transaction. Bindings are not authorization: the existing permissions
|
|
225
|
+
authorize `context.notion` calls. Redeployment updates or removes code-managed
|
|
226
|
+
grants as declarations change while preserving manually configured UI grants.
|
|
227
|
+
|
|
228
|
+
Execution reads the selected workflow configuration, not the latest NaC state,
|
|
229
|
+
validates the bindings against the capability requirements, and injects only that
|
|
230
|
+
capability's declared bindings. The SDK exposes typed, readonly `{ type, id }`
|
|
231
|
+
entries in `context.access`; UI-granted resources do not appear there. Use live
|
|
232
|
+
record IDs directly in `context.notion` calls for UI-granted resources; those
|
|
233
|
+
calls remain subject to the existing permissions. Missing or mismatched bindings
|
|
234
|
+
fail before the handler executes.
|
|
235
|
+
|
|
236
|
+
See the [workflow skill](../skills/workflow/SKILL.md#resources-created-with-the-app)
|
|
237
|
+
for supported resource types, access levels, and an authoring example.
|
|
238
|
+
|
|
109
239
|
## Manifest
|
|
110
240
|
|
|
111
241
|
The workflow-only manifest retains the platform's existing resource fields as empty arrays:
|
package/package.json
CHANGED
|
@@ -17,12 +17,20 @@ Import the resource creators you use, such as
|
|
|
17
17
|
resources for deployment; they do not make Notion API requests when called.
|
|
18
18
|
Use `context.notion` for runtime API operations instead.
|
|
19
19
|
|
|
20
|
-
|
|
21
|
-
|
|
22
|
-
|
|
23
|
-
|
|
24
|
-
|
|
25
|
-
credentials or network access.
|
|
20
|
+
Declare resources at module scope, not inside handlers or durable steps. Keep a
|
|
21
|
+
small declaration inline when it belongs to one capability. For a larger set
|
|
22
|
+
owned by one capability, use a local module such as
|
|
23
|
+
`src/workflows/myWorkflow/lib/notion.ts`. Use `src/notion.ts` when declarations
|
|
24
|
+
are shared across capabilities or do not have a natural capability owner. Module
|
|
25
|
+
evaluation must work without credentials or network access.
|
|
26
|
+
|
|
27
|
+
To configure a workflow's access to a declared page, database, data source, or
|
|
28
|
+
custom agent, pass its handle in `access: { alias: { resource: handle, level } }`.
|
|
29
|
+
The handler receives the live identity as `context.access.alias.id`; importing
|
|
30
|
+
a declaration alone does not request access. See the
|
|
31
|
+
[workflow skill](../workflow/SKILL.md#resources-created-with-the-app) for levels
|
|
32
|
+
and examples. Nonempty workflow `access` requires cloud deployment; `--local-build`
|
|
33
|
+
rejects it before upload.
|
|
26
34
|
|
|
27
35
|
## Where declarations are picked up
|
|
28
36
|
|
|
@@ -33,24 +41,27 @@ same rule. Their resource type does not determine their file location.
|
|
|
33
41
|
|
|
34
42
|
```text
|
|
35
43
|
src/
|
|
36
|
-
|
|
37
|
-
resources.ts Shared page/database declarations
|
|
44
|
+
notion.ts App-wide resource declarations
|
|
38
45
|
syncs/
|
|
39
|
-
issues.ts Default-exported sync; imports ../
|
|
40
|
-
lib/
|
|
41
|
-
issueSchema.ts Sync-specific helper; must be imported
|
|
46
|
+
issues.ts Default-exported sync; imports ../notion.js
|
|
42
47
|
workflows/
|
|
43
|
-
notify.ts
|
|
48
|
+
notify.ts May declare a small related resource inline
|
|
49
|
+
myWorkflow.ts Imports ./myWorkflow/lib/notion.js
|
|
50
|
+
myWorkflow/
|
|
51
|
+
lib/
|
|
52
|
+
notion.ts Resources owned by myWorkflow
|
|
44
53
|
```
|
|
45
54
|
|
|
46
|
-
|
|
47
|
-
in `src/syncs/issues.ts`
|
|
48
|
-
module can be loaded with a side-effect
|
|
49
|
-
|
|
50
|
-
|
|
55
|
+
Choose the location based on ownership, not resource type. Export an app-wide
|
|
56
|
+
database handle from `src/notion.ts` and import it in `src/syncs/issues.ts` when
|
|
57
|
+
the sync uses it. A declaration-only module can be loaded with a side-effect
|
|
58
|
+
import. Keep these imports at module scope so build evaluation runs the
|
|
59
|
+
declarations.
|
|
51
60
|
|
|
52
|
-
|
|
53
|
-
|
|
61
|
+
Resource helper modules, including `src/notion.ts`, are not automatically scanned.
|
|
62
|
+
There is no automatically scanned `src/pages/` or `src/databases/` directory
|
|
63
|
+
either. Helpers under capability-specific `lib/` directories are not discovered
|
|
64
|
+
on their own.
|
|
54
65
|
Every direct file in a capability directory must still default-export that
|
|
55
66
|
capability, so do not put a resource-only file there. Imports reached only
|
|
56
67
|
through `src/customBlocks/` or browser code do not enter the Notion as Code recording
|
|
@@ -63,12 +74,14 @@ Read the installed Notion as Code types before choosing fields. The Apps
|
|
|
63
74
|
root exports support:
|
|
64
75
|
|
|
65
76
|
- `teamspace({ resourceId, name, accessLevel })`, with `addPage` and
|
|
66
|
-
`addDatabase` on the returned handle.
|
|
77
|
+
`teamspace.addDatabase(id, args)` on the returned handle.
|
|
67
78
|
- `page({ resourceId, parent?, properties?, content? })`, with
|
|
68
|
-
`addPage` and `addDatabase` for children.
|
|
69
|
-
- `database({
|
|
70
|
-
|
|
71
|
-
`
|
|
79
|
+
`addPage` and `page.addDatabase(id, args)` for children.
|
|
80
|
+
- `database(resourceId, { dataSourceResourceId, name, schema, parent?, views? })`,
|
|
81
|
+
returning a single source as `handle.dataSource`, or
|
|
82
|
+
`database(resourceId, { datasources: { Key: { resourceId, schema } }, views?, parent? })`,
|
|
83
|
+
returning sources as `handle.datasources.Key`. A source's `addPage` declares a row;
|
|
84
|
+
the database handle also exposes `addView`.
|
|
72
85
|
- `customAgent({ resourceId, name, instructions?, sharedResources? })`.
|
|
73
86
|
Shared resources are declared resource IDs; inspect the installed types
|
|
74
87
|
before specifying models or triggers.
|
|
@@ -77,56 +90,71 @@ root exports support:
|
|
|
77
90
|
Use property resource IDs in view filters, sorts, and layout options; calendar
|
|
78
91
|
views require `calendarBy`, and timeline views require `timelineBy`.
|
|
79
92
|
|
|
93
|
+
A database declaration must include at least one data source or at least one
|
|
94
|
+
view. A linked-only database may omit `datasources`, or use `datasources: {}`,
|
|
95
|
+
only when `views` is nonempty. `{}`, `{ datasources: {} }`, and
|
|
96
|
+
`{ datasources: {}, views: [] }` are invalid; widened or dynamic maps and arrays
|
|
97
|
+
are checked at runtime as well as by the type-level forms.
|
|
98
|
+
|
|
99
|
+
```ts
|
|
100
|
+
const linkedIssues = database("project-issues-linked", {
|
|
101
|
+
views: [
|
|
102
|
+
{
|
|
103
|
+
resourceId: "project-issues-table",
|
|
104
|
+
type: "table",
|
|
105
|
+
dataSourceResourceId: "issues-source",
|
|
106
|
+
properties: [{ property: "issue-title", visible: true }],
|
|
107
|
+
},
|
|
108
|
+
],
|
|
109
|
+
});
|
|
110
|
+
```
|
|
111
|
+
|
|
112
|
+
The table view above is typed by its `type: "table"` schema and links to the
|
|
113
|
+
explicit `issues-source` resource ID.
|
|
114
|
+
|
|
80
115
|
Keep resource IDs stable across builds. They identify declarations, not live
|
|
81
116
|
Notion UUIDs. Avoid the reserved `__notion_apps_` prefix. Explicit parents
|
|
82
117
|
use `{ type: "resourceId", resourceId: parent.resourceId }`; child helpers set
|
|
83
118
|
this reference for you. Pages and databases without a parent default to private
|
|
84
119
|
top-level resources in the Apps workspace.
|
|
85
120
|
|
|
86
|
-
Several fields, including database covers
|
|
87
|
-
|
|
121
|
+
Several fields, including database covers and custom-agent triggers,
|
|
122
|
+
still have incomplete types. A field typed as `unknown` is not proof
|
|
88
123
|
that any payload is supported. Check implementation and examples before using
|
|
89
124
|
it; do not assume parity with other Notion as Code packages. Database views are
|
|
90
125
|
typed with `ViewSchema`; standalone declarations accept `ViewArgs`, which adds
|
|
91
126
|
the owning `databaseResourceId`.
|
|
92
127
|
|
|
93
128
|
The Apps SDK exposes a subset of Notion as Code. Data sources are nested inside
|
|
94
|
-
`database({
|
|
129
|
+
`database(resourceId, { datasources: { ... } })`, not declared by a separate
|
|
95
130
|
`dataSource` function. It has no `space` workspace declaration:
|
|
96
131
|
Apps deployment rejects workspace creation or changes and supplies the App's
|
|
97
132
|
workspace binding itself. Value helpers and types stay on their existing
|
|
98
133
|
subpaths. For example, import `{ notion }` from `@notionhq/apps/notion-as-code`
|
|
99
134
|
for `notion.text(...)` or `notion.file(resourceId)`. The latter creates a file
|
|
100
135
|
reference, not an upload or file declaration. These helpers are not root exports.
|
|
101
|
-
CLI acceptance of
|
|
102
|
-
|
|
136
|
+
CLI acceptance of a serialized intent envelope alone does not establish server
|
|
137
|
+
support for its contents.
|
|
103
138
|
|
|
104
139
|
## Use a declared data source in a sync
|
|
105
140
|
|
|
106
|
-
This example
|
|
107
|
-
declaration into an imported helper when sharing it with other capabilities.
|
|
141
|
+
This sync-specific example declares the resource inline in `src/syncs/issues.ts`:
|
|
108
142
|
|
|
109
143
|
```ts
|
|
110
144
|
import { database, sync } from "@notionhq/apps";
|
|
111
145
|
import { Builder } from "@notionhq/apps/builder";
|
|
112
146
|
|
|
113
|
-
const issues = database({
|
|
114
|
-
|
|
147
|
+
const issues = database("issues-db", {
|
|
148
|
+
dataSourceResourceId: "issues-source",
|
|
115
149
|
name: "Issues",
|
|
116
|
-
|
|
117
|
-
{
|
|
118
|
-
|
|
119
|
-
|
|
120
|
-
properties: [
|
|
121
|
-
{ resourceId: "issue-name", name: "Name", type: "title" },
|
|
122
|
-
{ resourceId: "issue-id", name: "External ID", type: "text" },
|
|
123
|
-
],
|
|
124
|
-
},
|
|
125
|
-
],
|
|
150
|
+
schema: {
|
|
151
|
+
Name: { resourceId: "issue-name", type: "title" },
|
|
152
|
+
"External ID": { resourceId: "issue-id", type: "text" },
|
|
153
|
+
},
|
|
126
154
|
});
|
|
127
155
|
|
|
128
156
|
export default sync({
|
|
129
|
-
dataSource: issues.
|
|
157
|
+
dataSource: issues.dataSource,
|
|
130
158
|
primaryKey: "External ID",
|
|
131
159
|
mode: "incremental",
|
|
132
160
|
handler: async () => ({
|
|
@@ -144,11 +172,23 @@ export default sync({
|
|
|
144
172
|
|
|
145
173
|
Pass a data source handle, not the whole database handle or a UUID. The SDK
|
|
146
174
|
derives the sync schema from that handle. `primaryKey` names a title or text
|
|
147
|
-
property by its
|
|
148
|
-
upsert values; the SDK fills it from `key`.
|
|
149
|
-
Follow the [sync skill](../sync/SKILL.md)
|
|
150
|
-
|
|
151
|
-
|
|
175
|
+
property by its schema key, which is also its serialized property name, not its
|
|
176
|
+
resource ID. Omit that property from upsert values; the SDK fills it from `key`.
|
|
177
|
+
Use Apps `Builder` for sync values. Follow the [sync skill](../sync/SKILL.md)
|
|
178
|
+
for pagination and reconciliation.
|
|
179
|
+
|
|
180
|
+
Author properties in a keyed `schema` with explicit `resourceId` and `type`.
|
|
181
|
+
Both `datasources` keys and `schema` keys are names: each data-source key is
|
|
182
|
+
that source's name, and each schema key is the property's name used for typed
|
|
183
|
+
access, sync values, and serialized property arrays. Nested data-source configs
|
|
184
|
+
and property configs do not accept `name`. The top-level database `name` remains
|
|
185
|
+
supported; in the single-source shorthand it supplies both the database and
|
|
186
|
+
data-source display names. In a multi-source declaration, it names the database
|
|
187
|
+
while each source name comes from its key. The serialized intent retains the
|
|
188
|
+
server's existing arrays. Authoring-only `schema`, `datasources`, and
|
|
189
|
+
database-level `dataSourceResourceId` fields do not appear in the serialized
|
|
190
|
+
database intent. The provisioning JSON envelope is unchanged.
|
|
191
|
+
No IDs are generated.
|
|
152
192
|
Each data source needs exactly one title property
|
|
153
193
|
and unique property names and IDs. The current adapter supports title, text,
|
|
154
194
|
number, select, multi-select, status, date, checkbox, URL, email, phone, and file
|
package/skills/sync/SKILL.md
CHANGED
|
@@ -25,26 +25,21 @@ including declaration discovery and the supported schema types.
|
|
|
25
25
|
This example declares an Issues database and syncs into its data source:
|
|
26
26
|
|
|
27
27
|
```ts
|
|
28
|
+
// src/syncs/issues.ts
|
|
28
29
|
import { database, sync } from "@notionhq/apps";
|
|
29
30
|
import { Builder } from "@notionhq/apps/builder";
|
|
30
31
|
|
|
31
|
-
const issues = database({
|
|
32
|
-
|
|
32
|
+
const issues = database("issues-db", {
|
|
33
|
+
dataSourceResourceId: "issues-source",
|
|
33
34
|
name: "Issues",
|
|
34
|
-
|
|
35
|
-
{
|
|
36
|
-
|
|
37
|
-
|
|
38
|
-
properties: [
|
|
39
|
-
{ resourceId: "issue-name", name: "Name", type: "title" },
|
|
40
|
-
{ resourceId: "issue-id", name: "External ID", type: "text" },
|
|
41
|
-
],
|
|
42
|
-
},
|
|
43
|
-
],
|
|
35
|
+
schema: {
|
|
36
|
+
Name: { resourceId: "issue-name", type: "title" },
|
|
37
|
+
"External ID": { resourceId: "issue-id", type: "text" },
|
|
38
|
+
},
|
|
44
39
|
});
|
|
45
40
|
|
|
46
41
|
export default sync({
|
|
47
|
-
dataSource: issues.
|
|
42
|
+
dataSource: issues.dataSource,
|
|
48
43
|
primaryKey: "External ID",
|
|
49
44
|
mode: "incremental",
|
|
50
45
|
handler: async () => ({
|
|
@@ -60,11 +55,19 @@ export default sync({
|
|
|
60
55
|
});
|
|
61
56
|
```
|
|
62
57
|
|
|
63
|
-
Set `primaryKey` on the sync; it names a title or text property by its
|
|
64
|
-
|
|
65
|
-
|
|
66
|
-
|
|
67
|
-
`
|
|
58
|
+
Set `primaryKey` on the sync; it names a title or text property by its schema
|
|
59
|
+
key, which is also the serialized property name. Every upsert must include at least one
|
|
60
|
+
known non-primary-key schema property, while any other non-primary-key properties may be
|
|
61
|
+
omitted; unknown property keys are rejected by TypeScript. Omit the primary-key property
|
|
62
|
+
from upsert properties: the SDK supplies it from `change.key`. Use Apps `Builder` values for
|
|
63
|
+
sync results. Keep resource IDs stable and reuse the declared data source handle. A
|
|
64
|
+
sync-specific database may be declared inline or in a local helper. Shared or
|
|
65
|
+
otherwise app-wide declarations often fit in `src/notion.ts`.
|
|
66
|
+
Both `datasources` keys and `schema` keys are names: each data-source key is
|
|
67
|
+
the source's name, and each schema key is the property's name. Nested
|
|
68
|
+
data-source configs do not accept `name`. The top-level database `name` remains
|
|
69
|
+
supported; in the single-source shorthand it supplies both the database and
|
|
70
|
+
data-source display names.
|
|
68
71
|
|
|
69
72
|
This skill covers only syncs backed by Notion as Code data sources. If the user
|
|
70
73
|
asks to attach an existing database, explain that this recipe does not cover
|