@notionhq/apps 0.0.16 → 0.0.18
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 +86 -36
- package/dist/connections.d.ts +39 -11
- package/dist/connections.d.ts.map +1 -1
- package/dist/connections.js +85 -21
- package/dist/index.d.ts +2 -0
- package/dist/index.d.ts.map +1 -1
- package/dist/index.js +2 -0
- package/dist/notion-as-code/database.d.ts +89 -48
- package/dist/notion-as-code/database.d.ts.map +1 -1
- package/dist/notion-as-code/database.js +132 -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/handles.d.ts +2 -0
- package/dist/notion-as-code/handles.d.ts.map +1 -1
- package/dist/notion-as-code/handles.js +2 -0
- package/dist/notion-as-code/index.d.ts +5 -2
- package/dist/notion-as-code/index.d.ts.map +1 -1
- package/dist/notion-as-code/intents.d.ts +36 -3
- package/dist/notion-as-code/intents.d.ts.map +1 -1
- 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/view.d.ts +12 -0
- package/dist/notion-as-code/view.d.ts.map +1 -0
- package/dist/notion-as-code/view.js +13 -0
- package/dist/notion-as-code/views-types.test.d.ts +2 -0
- package/dist/notion-as-code/views-types.test.d.ts.map +1 -0
- package/dist/notion-as-code/views.d.ts +488 -0
- package/dist/notion-as-code/views.d.ts.map +1 -0
- package/dist/notion-as-code/views.js +0 -0
- package/dist/oauth.d.ts +11 -0
- package/dist/oauth.d.ts.map +1 -0
- package/dist/oauth.js +26 -0
- package/dist/providers.generated.d.ts +26 -86
- package/dist/providers.generated.d.ts.map +1 -1
- package/dist/providers.generated.js +54 -66
- package/dist/sync.d.ts +33 -15
- package/dist/sync.d.ts.map +1 -1
- package/dist/sync.js +12 -0
- package/dist/triggers.generated.d.ts +3 -3
- package/dist/triggers.generated.d.ts.map +1 -1
- 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 +56 -11
- package/dist/workflow.d.ts.map +1 -1
- package/dist/workflow.js +123 -10
- package/docs/BUILD.md +96 -0
- package/docs/CONNECTIONS.md +70 -22
- package/package.json +1 -1
- package/skills/connections/SKILL.md +7 -8
- package/skills/notion-as-code/SKILL.md +88 -50
- package/skills/sync/SKILL.md +21 -18
- package/skills/workflow/SKILL.md +52 -3
- package/src/cli/build.test.ts +124 -64
- package/src/connections.test.ts +205 -41
- package/src/connections.ts +144 -38
- package/src/index.ts +2 -0
- package/src/notion-as-code/database.test.ts +661 -0
- package/src/notion-as-code/database.ts +346 -129
- package/src/notion-as-code/handles.ts +2 -0
- package/src/notion-as-code/index.ts +71 -1
- package/src/notion-as-code/intents.ts +41 -4
- package/src/notion-as-code/schema.ts +11 -3
- package/src/notion-as-code/view.ts +23 -0
- package/src/notion-as-code/views-types.test.ts +59 -0
- package/src/notion-as-code/views.ts +573 -0
- package/src/oauth.ts +40 -0
- package/src/providers.generated.ts +68 -163
- package/src/sync.test.ts +295 -0
- package/src/sync.ts +85 -21
- package/src/triggers.generated.ts +4 -4
- package/src/workflow-connections-types.test.ts +16 -10
- package/src/workflow-state.ts +152 -0
- package/src/workflow-types.test.ts +88 -16
- package/src/workflow.test.ts +374 -18
- package/src/workflow.ts +211 -23
|
@@ -17,12 +17,12 @@ 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
26
|
|
|
27
27
|
## Where declarations are picked up
|
|
28
28
|
|
|
@@ -33,24 +33,27 @@ same rule. Their resource type does not determine their file location.
|
|
|
33
33
|
|
|
34
34
|
```text
|
|
35
35
|
src/
|
|
36
|
-
|
|
37
|
-
resources.ts Shared page/database declarations
|
|
36
|
+
notion.ts App-wide resource declarations
|
|
38
37
|
syncs/
|
|
39
|
-
issues.ts Default-exported sync; imports ../
|
|
40
|
-
lib/
|
|
41
|
-
issueSchema.ts Sync-specific helper; must be imported
|
|
38
|
+
issues.ts Default-exported sync; imports ../notion.js
|
|
42
39
|
workflows/
|
|
43
|
-
notify.ts
|
|
40
|
+
notify.ts May declare a small related resource inline
|
|
41
|
+
myWorkflow.ts Imports ./myWorkflow/lib/notion.js
|
|
42
|
+
myWorkflow/
|
|
43
|
+
lib/
|
|
44
|
+
notion.ts Resources owned by myWorkflow
|
|
44
45
|
```
|
|
45
46
|
|
|
46
|
-
|
|
47
|
-
in `src/syncs/issues.ts`
|
|
48
|
-
module can be loaded with a side-effect
|
|
49
|
-
|
|
50
|
-
|
|
47
|
+
Choose the location based on ownership, not resource type. Export an app-wide
|
|
48
|
+
database handle from `src/notion.ts` and import it in `src/syncs/issues.ts` when
|
|
49
|
+
the sync uses it. A declaration-only module can be loaded with a side-effect
|
|
50
|
+
import. Keep these imports at module scope so build evaluation runs the
|
|
51
|
+
declarations.
|
|
51
52
|
|
|
52
|
-
|
|
53
|
-
|
|
53
|
+
Resource helper modules, including `src/notion.ts`, are not automatically scanned.
|
|
54
|
+
There is no automatically scanned `src/pages/` or `src/databases/` directory
|
|
55
|
+
either. Helpers under capability-specific `lib/` directories are not discovered
|
|
56
|
+
on their own.
|
|
54
57
|
Every direct file in a capability directory must still default-export that
|
|
55
58
|
capability, so do not put a resource-only file there. Imports reached only
|
|
56
59
|
through `src/customBlocks/` or browser code do not enter the Notion as Code recording
|
|
@@ -63,15 +66,43 @@ Read the installed Notion as Code types before choosing fields. The Apps
|
|
|
63
66
|
root exports support:
|
|
64
67
|
|
|
65
68
|
- `teamspace({ resourceId, name, accessLevel })`, with `addPage` and
|
|
66
|
-
`addDatabase` on the returned handle.
|
|
69
|
+
`teamspace.addDatabase(id, args)` on the returned handle.
|
|
67
70
|
- `page({ resourceId, parent?, properties?, content? })`, with
|
|
68
|
-
`addPage` and `addDatabase` for children.
|
|
69
|
-
- `database({
|
|
70
|
-
|
|
71
|
-
`
|
|
71
|
+
`addPage` and `page.addDatabase(id, args)` for children.
|
|
72
|
+
- `database(resourceId, { dataSourceResourceId, name, schema, parent?, views? })`,
|
|
73
|
+
returning a single source as `handle.dataSource`, or
|
|
74
|
+
`database(resourceId, { datasources: { Key: { resourceId, schema } }, views?, parent? })`,
|
|
75
|
+
returning sources as `handle.datasources.Key`. A source's `addPage` declares a row;
|
|
76
|
+
the database handle also exposes `addView`.
|
|
72
77
|
- `customAgent({ resourceId, name, instructions?, sharedResources? })`.
|
|
73
78
|
Shared resources are declared resource IDs; inspect the installed types
|
|
74
79
|
before specifying models or triggers.
|
|
80
|
+
- `view({ databaseResourceId, resourceId, type, dataSourceResourceId, ... })`,
|
|
81
|
+
returning a handle with `resourceId`. This is also available as `notion.view`.
|
|
82
|
+
Use property resource IDs in view filters, sorts, and layout options; calendar
|
|
83
|
+
views require `calendarBy`, and timeline views require `timelineBy`.
|
|
84
|
+
|
|
85
|
+
A database declaration must include at least one data source or at least one
|
|
86
|
+
view. A linked-only database may omit `datasources`, or use `datasources: {}`,
|
|
87
|
+
only when `views` is nonempty. `{}`, `{ datasources: {} }`, and
|
|
88
|
+
`{ datasources: {}, views: [] }` are invalid; widened or dynamic maps and arrays
|
|
89
|
+
are checked at runtime as well as by the type-level forms.
|
|
90
|
+
|
|
91
|
+
```ts
|
|
92
|
+
const linkedIssues = database("project-issues-linked", {
|
|
93
|
+
views: [
|
|
94
|
+
{
|
|
95
|
+
resourceId: "project-issues-table",
|
|
96
|
+
type: "table",
|
|
97
|
+
dataSourceResourceId: "issues-source",
|
|
98
|
+
properties: [{ property: "issue-title", visible: true }],
|
|
99
|
+
},
|
|
100
|
+
],
|
|
101
|
+
});
|
|
102
|
+
```
|
|
103
|
+
|
|
104
|
+
The table view above is typed by its `type: "table"` schema and links to the
|
|
105
|
+
explicit `issues-source` resource ID.
|
|
75
106
|
|
|
76
107
|
Keep resource IDs stable across builds. They identify declarations, not live
|
|
77
108
|
Notion UUIDs. Avoid the reserved `__notion_apps_` prefix. Explicit parents
|
|
@@ -79,48 +110,43 @@ use `{ type: "resourceId", resourceId: parent.resourceId }`; child helpers set
|
|
|
79
110
|
this reference for you. Pages and databases without a parent default to private
|
|
80
111
|
top-level resources in the Apps workspace.
|
|
81
112
|
|
|
82
|
-
Several fields, including
|
|
83
|
-
|
|
113
|
+
Several fields, including database covers and custom-agent triggers,
|
|
114
|
+
still have incomplete types. A field typed as `unknown` is not proof
|
|
84
115
|
that any payload is supported. Check implementation and examples before using
|
|
85
|
-
it; do not assume parity with other Notion as Code packages.
|
|
116
|
+
it; do not assume parity with other Notion as Code packages. Database views are
|
|
117
|
+
typed with `ViewSchema`; standalone declarations accept `ViewArgs`, which adds
|
|
118
|
+
the owning `databaseResourceId`.
|
|
86
119
|
|
|
87
120
|
The Apps SDK exposes a subset of Notion as Code. Data sources are nested inside
|
|
88
|
-
`database({
|
|
121
|
+
`database(resourceId, { datasources: { ... } })`, not declared by a separate
|
|
89
122
|
`dataSource` function. It has no `space` workspace declaration:
|
|
90
123
|
Apps deployment rejects workspace creation or changes and supplies the App's
|
|
91
124
|
workspace binding itself. Value helpers and types stay on their existing
|
|
92
125
|
subpaths. For example, import `{ notion }` from `@notionhq/apps/notion-as-code`
|
|
93
126
|
for `notion.text(...)` or `notion.file(resourceId)`. The latter creates a file
|
|
94
127
|
reference, not an upload or file declaration. These helpers are not root exports.
|
|
95
|
-
CLI acceptance of
|
|
96
|
-
|
|
128
|
+
CLI acceptance of a serialized intent envelope alone does not establish server
|
|
129
|
+
support for its contents.
|
|
97
130
|
|
|
98
131
|
## Use a declared data source in a sync
|
|
99
132
|
|
|
100
|
-
This example
|
|
101
|
-
declaration into an imported helper when sharing it with other capabilities.
|
|
133
|
+
This sync-specific example declares the resource inline in `src/syncs/issues.ts`:
|
|
102
134
|
|
|
103
135
|
```ts
|
|
104
136
|
import { database, sync } from "@notionhq/apps";
|
|
105
137
|
import { Builder } from "@notionhq/apps/builder";
|
|
106
138
|
|
|
107
|
-
const issues = database({
|
|
108
|
-
|
|
139
|
+
const issues = database("issues-db", {
|
|
140
|
+
dataSourceResourceId: "issues-source",
|
|
109
141
|
name: "Issues",
|
|
110
|
-
|
|
111
|
-
{
|
|
112
|
-
|
|
113
|
-
|
|
114
|
-
properties: [
|
|
115
|
-
{ resourceId: "issue-name", name: "Name", type: "title" },
|
|
116
|
-
{ resourceId: "issue-id", name: "External ID", type: "text" },
|
|
117
|
-
],
|
|
118
|
-
},
|
|
119
|
-
],
|
|
142
|
+
schema: {
|
|
143
|
+
Name: { resourceId: "issue-name", type: "title" },
|
|
144
|
+
"External ID": { resourceId: "issue-id", type: "text" },
|
|
145
|
+
},
|
|
120
146
|
});
|
|
121
147
|
|
|
122
148
|
export default sync({
|
|
123
|
-
dataSource: issues.
|
|
149
|
+
dataSource: issues.dataSource,
|
|
124
150
|
primaryKey: "External ID",
|
|
125
151
|
mode: "incremental",
|
|
126
152
|
handler: async () => ({
|
|
@@ -138,11 +164,23 @@ export default sync({
|
|
|
138
164
|
|
|
139
165
|
Pass a data source handle, not the whole database handle or a UUID. The SDK
|
|
140
166
|
derives the sync schema from that handle. `primaryKey` names a title or text
|
|
141
|
-
property by its
|
|
142
|
-
upsert values; the SDK fills it from `key`.
|
|
143
|
-
Follow the [sync skill](../sync/SKILL.md)
|
|
144
|
-
|
|
145
|
-
|
|
167
|
+
property by its schema key, which is also its serialized property name, not its
|
|
168
|
+
resource ID. Omit that property from upsert values; the SDK fills it from `key`.
|
|
169
|
+
Use Apps `Builder` for sync values. Follow the [sync skill](../sync/SKILL.md)
|
|
170
|
+
for pagination and reconciliation.
|
|
171
|
+
|
|
172
|
+
Author properties in a keyed `schema` with explicit `resourceId` and `type`.
|
|
173
|
+
Both `datasources` keys and `schema` keys are names: each data-source key is
|
|
174
|
+
that source's name, and each schema key is the property's name used for typed
|
|
175
|
+
access, sync values, and serialized property arrays. Nested data-source configs
|
|
176
|
+
and property configs do not accept `name`. The top-level database `name` remains
|
|
177
|
+
supported; in the single-source shorthand it supplies both the database and
|
|
178
|
+
data-source display names. In a multi-source declaration, it names the database
|
|
179
|
+
while each source name comes from its key. The serialized intent retains the
|
|
180
|
+
server's existing arrays. Authoring-only `schema`, `datasources`, and
|
|
181
|
+
database-level `dataSourceResourceId` fields do not appear in the serialized
|
|
182
|
+
database intent. The provisioning JSON envelope is unchanged.
|
|
183
|
+
No IDs are generated.
|
|
146
184
|
Each data source needs exactly one title property
|
|
147
185
|
and unique property names and IDs. The current adapter supports title, text,
|
|
148
186
|
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
|
package/skills/workflow/SKILL.md
CHANGED
|
@@ -38,6 +38,53 @@ and Notion API calls, mutable state reads, timestamps, random values, generated
|
|
|
38
38
|
IDs, messages, creates, and updates. Keep deterministic transforms of the event
|
|
39
39
|
and completed step results outside a step.
|
|
40
40
|
|
|
41
|
+
Workflow-level key-value state is available through the step closure's `state`.
|
|
42
|
+
State is shared across runs of the same workflow and every operation executes
|
|
43
|
+
inside that step's durability boundary:
|
|
44
|
+
|
|
45
|
+
```ts
|
|
46
|
+
await context.step("Update cursor", async ({ state }) => {
|
|
47
|
+
state.set("cursor", nextCursor);
|
|
48
|
+
const pendingCursor = await state.get("cursor"); // Reads the buffered write.
|
|
49
|
+
if (!(await saveCursor(pendingCursor))) {
|
|
50
|
+
throw new Error("Save failed"); // The state write is discarded.
|
|
51
|
+
}
|
|
52
|
+
});
|
|
53
|
+
```
|
|
54
|
+
|
|
55
|
+
Use `get` and `set`; passing `undefined` to `set` deletes the key, while `null`
|
|
56
|
+
is stored as a value. Mutations are buffered and committed atomically only
|
|
57
|
+
after the step callback succeeds. Repeated mutations of one key are allowed,
|
|
58
|
+
reads observe the latest buffered value, and only the final mutation is
|
|
59
|
+
committed. If the callback throws, none of that step's state mutations are
|
|
60
|
+
persisted.
|
|
61
|
+
|
|
62
|
+
Use `context.wait.until(name, { after })` for a relative wait shorter than seven
|
|
63
|
+
days, or `context.wait.until(name, { at })` for an absolute date within the next
|
|
64
|
+
seven days. The runtime checkpoints the target time, releases the workflow's
|
|
65
|
+
compute, and resumes it from the same point later:
|
|
66
|
+
|
|
67
|
+
```ts
|
|
68
|
+
await context.wait.until("Cooling-off period", {
|
|
69
|
+
after: { days: 1, hours: 6 },
|
|
70
|
+
});
|
|
71
|
+
await context.wait.until("Launch time", { at: launchAt });
|
|
72
|
+
```
|
|
73
|
+
|
|
74
|
+
Relative durations can combine `milliseconds`, `seconds`, `minutes`, `hours`,
|
|
75
|
+
`days`, and `weeks`. Every specified value must be a positive finite integer.
|
|
76
|
+
Repeated waits can keep one stable display name when each call has a unique
|
|
77
|
+
composite `key`:
|
|
78
|
+
|
|
79
|
+
```ts
|
|
80
|
+
for (const page of pages) {
|
|
81
|
+
await context.wait.until("Rate limit delay", {
|
|
82
|
+
key: ["rate-limit", page.id],
|
|
83
|
+
after: { seconds: 1 },
|
|
84
|
+
});
|
|
85
|
+
}
|
|
86
|
+
```
|
|
87
|
+
|
|
41
88
|
Completed steps replay saved results. Do not rely on in-memory mutations inside
|
|
42
89
|
a step. Return the values required by later code. Keep calls in a stable order
|
|
43
90
|
and give every step a stable display name. The name is the replay key by
|
|
@@ -67,8 +114,9 @@ Use [Notion as Code](../notion-as-code/SKILL.md) for pages and databases that
|
|
|
67
114
|
should be created during deployment. Prefer it over equivalent manual setup
|
|
68
115
|
or API calls that create the App's resources. Use runtime API calls for dynamic
|
|
69
116
|
data changes, not as a substitute for supported Notion as Code setup.
|
|
70
|
-
Keep declarations at module scope
|
|
71
|
-
|
|
117
|
+
Keep declarations at module scope. A small workflow-specific resource can be
|
|
118
|
+
declared inline. A larger set can live in a local module; for example, declare a
|
|
119
|
+
guide page in `src/workflows/sayHello/lib/notion.ts`:
|
|
72
120
|
|
|
73
121
|
```ts
|
|
74
122
|
import { page } from "@notionhq/apps";
|
|
@@ -82,10 +130,11 @@ export const guide = page({
|
|
|
82
130
|
Import the module from `src/workflows/sayHello.ts` so the build records it:
|
|
83
131
|
|
|
84
132
|
```ts
|
|
85
|
-
import "../lib/resources";
|
|
86
133
|
import { workflow } from "@notionhq/apps";
|
|
87
134
|
import { triggers } from "@notionhq/apps/triggers";
|
|
88
135
|
|
|
136
|
+
import "./sayHello/lib/notion.js";
|
|
137
|
+
|
|
89
138
|
export default workflow({
|
|
90
139
|
name: "Say Hello",
|
|
91
140
|
description: "Says hello on a recurring schedule.",
|
package/src/cli/build.test.ts
CHANGED
|
@@ -15,17 +15,18 @@ import { finishMetadataRecording, startMetadataRecording } from "../notion-as-co
|
|
|
15
15
|
describe("Notion-as-Code database shorthand", () => {
|
|
16
16
|
it("keeps provisioning and sync identity equivalent to an explicit source", () => {
|
|
17
17
|
const recorder = startMetadataRecording();
|
|
18
|
-
const
|
|
19
|
-
resourceId: "
|
|
18
|
+
const schema = {
|
|
19
|
+
Name: { resourceId: "title", type: "title" },
|
|
20
|
+
Count: { resourceId: "count", type: "number" },
|
|
21
|
+
} as const;
|
|
22
|
+
const database = notion.database("issues", {
|
|
23
|
+
dataSourceResourceId: "issues-source",
|
|
20
24
|
name: "Issues",
|
|
21
|
-
|
|
22
|
-
{ resourceId: "title", name: "Name", type: "title" },
|
|
23
|
-
{ resourceId: "count", name: "Count", type: "number" },
|
|
24
|
-
],
|
|
25
|
+
schema,
|
|
25
26
|
});
|
|
26
|
-
expectTypeOf(database).toExtend<
|
|
27
|
-
|
|
28
|
-
>();
|
|
27
|
+
expectTypeOf(database).toExtend<SingleSourceDatabaseHandle<typeof schema>>();
|
|
28
|
+
expectTypeOf(database.dataSource.schema.Name.type).toEqualTypeOf<"title">();
|
|
29
|
+
expectTypeOf(database.dataSource.schema.Name.resourceId).toEqualTypeOf<"title">();
|
|
29
30
|
expectTypeOf(
|
|
30
31
|
database.dataSource.database.config.schema.Count.type,
|
|
31
32
|
).toEqualTypeOf<"number">();
|
|
@@ -33,18 +34,19 @@ describe("Notion-as-Code database shorthand", () => {
|
|
|
33
34
|
const shorthand = finishMetadataRecording(recorder).intents;
|
|
34
35
|
|
|
35
36
|
const explicitRecorder = startMetadataRecording();
|
|
36
|
-
const explicit = notion.database({
|
|
37
|
-
resourceId: "issues",
|
|
37
|
+
const explicit = notion.database("issues", {
|
|
38
38
|
name: "Issues",
|
|
39
|
-
|
|
40
|
-
{
|
|
39
|
+
datasources: {
|
|
40
|
+
Issues: {
|
|
41
41
|
resourceId: "issues-source",
|
|
42
|
-
|
|
43
|
-
properties: database.dataSource.schema,
|
|
42
|
+
schema,
|
|
44
43
|
},
|
|
45
|
-
|
|
44
|
+
},
|
|
46
45
|
});
|
|
47
|
-
explicit.
|
|
46
|
+
expectTypeOf(explicit.datasources.Issues).toExtend<DataSourceHandle<typeof schema>>();
|
|
47
|
+
expectTypeOf(explicit.datasources.Issues.schema.Name.type).toEqualTypeOf<"title">();
|
|
48
|
+
expectTypeOf(explicit.datasources.Issues.schema.Name.resourceId).toEqualTypeOf<"title">();
|
|
49
|
+
explicit.datasources.Issues.addPage({ resourceId: "issue" });
|
|
48
50
|
// @ts-expect-error Explicit databases do not guarantee a single source.
|
|
49
51
|
void explicit.dataSource;
|
|
50
52
|
expect(shorthand).toEqual(finishMetadataRecording(explicitRecorder).intents);
|
|
@@ -56,22 +58,24 @@ describe("Notion-as-Code database shorthand", () => {
|
|
|
56
58
|
});
|
|
57
59
|
});
|
|
58
60
|
|
|
59
|
-
it("preserves parent binding and
|
|
61
|
+
it("preserves parent binding and keyed source types through child helpers", () => {
|
|
60
62
|
const recorder = startMetadataRecording();
|
|
61
63
|
const page = notion.page({ resourceId: "page" });
|
|
62
64
|
const team = notion.teamspace({ resourceId: "team", name: "Team", accessLevel: "private" });
|
|
65
|
+
const schema = {
|
|
66
|
+
Title: { resourceId: "child-title", type: "title" },
|
|
67
|
+
} as const;
|
|
63
68
|
for (const parent of [page, team]) {
|
|
64
|
-
const database = parent.addDatabase({
|
|
65
|
-
|
|
69
|
+
const database = parent.addDatabase(`${parent.resourceId}-db`, {
|
|
70
|
+
dataSourceResourceId: `${parent.resourceId}-source`,
|
|
66
71
|
name: "Child",
|
|
67
|
-
|
|
68
|
-
{ resourceId: `${parent.resourceId}-title`, name: "Title", type: "title" },
|
|
69
|
-
],
|
|
72
|
+
schema,
|
|
70
73
|
});
|
|
71
|
-
expectTypeOf(database.dataSource).toExtend<
|
|
72
|
-
|
|
73
|
-
|
|
74
|
-
|
|
74
|
+
expectTypeOf(database.dataSource).toExtend<DataSourceHandle<typeof schema>>();
|
|
75
|
+
expectTypeOf(database.dataSource.schema.Title.type).toEqualTypeOf<"title">();
|
|
76
|
+
expectTypeOf(
|
|
77
|
+
database.dataSource.schema.Title.resourceId,
|
|
78
|
+
).toEqualTypeOf<"child-title">();
|
|
75
79
|
}
|
|
76
80
|
const intents = finishMetadataRecording(recorder).intents;
|
|
77
81
|
expect(
|
|
@@ -82,19 +86,60 @@ describe("Notion-as-Code database shorthand", () => {
|
|
|
82
86
|
]);
|
|
83
87
|
});
|
|
84
88
|
|
|
85
|
-
it("
|
|
89
|
+
it("keeps keyed child inference and rejects empty child databases", () => {
|
|
86
90
|
const recorder = startMetadataRecording();
|
|
87
|
-
const
|
|
88
|
-
|
|
89
|
-
|
|
90
|
-
|
|
91
|
-
|
|
92
|
-
|
|
93
|
-
|
|
94
|
-
|
|
91
|
+
const page = notion.page({ resourceId: "child-page" });
|
|
92
|
+
const schema = {
|
|
93
|
+
Name: { type: "title", resourceId: "child-name" },
|
|
94
|
+
"Issue ID": { type: "text", resourceId: "child-issue-id" },
|
|
95
|
+
} as const;
|
|
96
|
+
const database = page.addDatabase("child-multi-database", {
|
|
97
|
+
datasources: {
|
|
98
|
+
Issues: {
|
|
99
|
+
resourceId: "child-issues-source",
|
|
100
|
+
schema,
|
|
101
|
+
},
|
|
102
|
+
},
|
|
103
|
+
});
|
|
104
|
+
expectTypeOf(database.datasources.Issues).toExtend<DataSourceHandle<typeof schema>>();
|
|
105
|
+
expectTypeOf(
|
|
106
|
+
database.datasources.Issues.schema.Name.resourceId,
|
|
107
|
+
).toEqualTypeOf<"child-name">();
|
|
108
|
+
|
|
109
|
+
const invalidChildDeclarations = () => {
|
|
110
|
+
// @ts-expect-error Child databases need a source or a view.
|
|
111
|
+
page.addDatabase("child-empty-datasources", { datasources: {} });
|
|
112
|
+
// @ts-expect-error Child databases need a source or a view.
|
|
113
|
+
page.addDatabase("child-empty-arguments", {});
|
|
114
|
+
// @ts-expect-error Empty child sources and views are invalid.
|
|
115
|
+
page.addDatabase("child-empty-with-views", { datasources: {}, views: [] });
|
|
95
116
|
};
|
|
96
|
-
|
|
97
|
-
expect(
|
|
117
|
+
void invalidChildDeclarations;
|
|
118
|
+
expect(() =>
|
|
119
|
+
page.addDatabase("child-runtime-empty", { datasources: {}, views: [] } as never),
|
|
120
|
+
).toThrow();
|
|
121
|
+
|
|
122
|
+
const intents = finishMetadataRecording(recorder).intents;
|
|
123
|
+
expect(intents).toHaveLength(2);
|
|
124
|
+
expect(intents).toEqual(
|
|
125
|
+
expect.arrayContaining([
|
|
126
|
+
expect.objectContaining({
|
|
127
|
+
type: "database",
|
|
128
|
+
resourceId: "child-multi-database",
|
|
129
|
+
parent: { type: "resourceId", resourceId: "child-page" },
|
|
130
|
+
dataSources: [
|
|
131
|
+
{
|
|
132
|
+
resourceId: "child-issues-source",
|
|
133
|
+
name: "Issues",
|
|
134
|
+
properties: [
|
|
135
|
+
{ resourceId: "child-name", name: "Name", type: "title" },
|
|
136
|
+
{ resourceId: "child-issue-id", name: "Issue ID", type: "text" },
|
|
137
|
+
],
|
|
138
|
+
},
|
|
139
|
+
],
|
|
140
|
+
}),
|
|
141
|
+
]),
|
|
142
|
+
);
|
|
98
143
|
});
|
|
99
144
|
});
|
|
100
145
|
|
|
@@ -163,41 +208,49 @@ describe("buildApp", () => {
|
|
|
163
208
|
it("builds a sync manifest with a Notion-as-Code meetings database", async () => {
|
|
164
209
|
const { manifest } = await buildApp(example("sync"));
|
|
165
210
|
|
|
166
|
-
expect(manifest).toEqual(
|
|
167
|
-
|
|
168
|
-
|
|
169
|
-
|
|
170
|
-
|
|
211
|
+
expect(manifest).toEqual(
|
|
212
|
+
expect.objectContaining({
|
|
213
|
+
$schema: "notion:apps-manifest:v1",
|
|
214
|
+
sdkVersion: SDK_VERSION,
|
|
215
|
+
xldbs: [],
|
|
216
|
+
pacers: [],
|
|
217
|
+
}),
|
|
218
|
+
);
|
|
219
|
+
expect(manifest.databases).toHaveLength(2);
|
|
220
|
+
expect(manifest.databases).toEqual(
|
|
221
|
+
expect.arrayContaining([
|
|
222
|
+
expect.objectContaining({
|
|
171
223
|
key: "issues",
|
|
172
|
-
config: {
|
|
224
|
+
config: expect.objectContaining({
|
|
173
225
|
initialTitle: "GitHub Issues",
|
|
174
|
-
schema: {
|
|
175
|
-
properties: {
|
|
226
|
+
schema: expect.objectContaining({
|
|
227
|
+
properties: expect.objectContaining({
|
|
176
228
|
Name: { type: "title" },
|
|
177
229
|
"GitHub ID": { type: "text" },
|
|
178
230
|
Closed: { type: "checkbox" },
|
|
179
|
-
},
|
|
180
|
-
},
|
|
181
|
-
},
|
|
182
|
-
},
|
|
183
|
-
{
|
|
231
|
+
}),
|
|
232
|
+
}),
|
|
233
|
+
}),
|
|
234
|
+
}),
|
|
235
|
+
expect.objectContaining({
|
|
184
236
|
key: "homebase-meetings-db-source",
|
|
185
|
-
config: {
|
|
237
|
+
config: expect.objectContaining({
|
|
186
238
|
initialTitle: "Meetings",
|
|
187
|
-
schema: {
|
|
188
|
-
properties: {
|
|
239
|
+
schema: expect.objectContaining({
|
|
240
|
+
properties: expect.objectContaining({
|
|
189
241
|
Name: { type: "title" },
|
|
190
242
|
"Meeting ID": { type: "text" },
|
|
191
243
|
"Meeting date": { type: "date" },
|
|
192
244
|
"Prep complete": { type: "checkbox" },
|
|
193
|
-
},
|
|
194
|
-
},
|
|
195
|
-
},
|
|
196
|
-
},
|
|
197
|
-
],
|
|
198
|
-
|
|
199
|
-
|
|
200
|
-
|
|
245
|
+
}),
|
|
246
|
+
}),
|
|
247
|
+
}),
|
|
248
|
+
}),
|
|
249
|
+
]),
|
|
250
|
+
);
|
|
251
|
+
expect(manifest.capabilities).toHaveLength(2);
|
|
252
|
+
expect(manifest.capabilities).toEqual(
|
|
253
|
+
expect.arrayContaining([
|
|
201
254
|
{
|
|
202
255
|
type: "sync",
|
|
203
256
|
key: "issues",
|
|
@@ -218,8 +271,8 @@ describe("buildApp", () => {
|
|
|
218
271
|
schedule: { type: "interval", intervalMs: 3_600_000 },
|
|
219
272
|
},
|
|
220
273
|
},
|
|
221
|
-
],
|
|
222
|
-
|
|
274
|
+
]),
|
|
275
|
+
);
|
|
223
276
|
});
|
|
224
277
|
|
|
225
278
|
it("emits a separate Notion-as-Code provisioning artifact", async () => {
|
|
@@ -285,6 +338,13 @@ describe("buildApp", () => {
|
|
|
285
338
|
resourceId: "__notion_apps_workspace__",
|
|
286
339
|
},
|
|
287
340
|
dataSources: [],
|
|
341
|
+
views: [
|
|
342
|
+
{
|
|
343
|
+
resourceId: "github-app-private-view",
|
|
344
|
+
type: "table",
|
|
345
|
+
dataSourceResourceId: "issues-source",
|
|
346
|
+
},
|
|
347
|
+
],
|
|
288
348
|
},
|
|
289
349
|
{
|
|
290
350
|
type: "database",
|