@notionhq/apps 0.0.21 → 0.0.23
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/README.md +2 -1
- package/dist/cli/build.d.ts.map +1 -1
- package/dist/cli/build.js +1 -4
- package/dist/custom-block.d.ts +14 -2
- package/dist/custom-block.d.ts.map +1 -1
- package/dist/custom-block.js +27 -2
- package/dist/index.d.ts +1 -1
- package/dist/index.d.ts.map +1 -1
- package/dist/index.js +2 -0
- package/dist/notion-as-code/database.d.ts +3 -0
- package/dist/notion-as-code/database.d.ts.map +1 -1
- package/dist/notion-as-code/database.js +6 -2
- package/dist/notion-as-code/index.d.ts +1 -0
- package/dist/notion-as-code/index.d.ts.map +1 -1
- package/dist/notion-as-code/index.js +4 -1
- package/dist/notion-as-code/intents.d.ts +1 -0
- package/dist/notion-as-code/intents.d.ts.map +1 -1
- package/dist/translate.d.ts +10 -0
- package/dist/translate.d.ts.map +1 -0
- package/dist/translate.js +65 -0
- package/dist/workflow-access.d.ts +29 -0
- package/dist/workflow-access.d.ts.map +1 -1
- package/dist/workflow-access.js +23 -0
- package/dist/workflow.d.ts +1 -0
- package/dist/workflow.d.ts.map +1 -1
- package/dist/workflow.js +2 -0
- package/docs/BUILD.md +5 -3
- package/package.json +1 -1
- package/skills/custom-blocks/SKILL.md +4 -4
- package/skills/notion-as-code/SKILL.md +8 -5
- package/skills/workflow/SKILL.md +5 -5
- package/src/cli/build.test.ts +55 -0
- package/src/cli/build.ts +1 -4
- package/src/cli/codegen.test.ts +17 -1
- package/src/custom-block.test.ts +138 -2
- package/src/custom-block.ts +49 -2
- package/src/index.ts +1 -0
- package/src/notion-as-code/database.test.ts +5 -1
- package/src/notion-as-code/database.ts +9 -1
- package/src/notion-as-code/index.ts +1 -0
- package/src/notion-as-code/intents.ts +1 -0
- package/src/translate.ts +77 -0
- package/src/workflow-access-types.test.ts +8 -4
- package/src/workflow-access.ts +41 -0
- package/src/workflow.test.ts +5 -4
- package/src/workflow.ts +1 -0
|
@@ -25,7 +25,8 @@ are shared across capabilities or do not have a natural capability owner. Module
|
|
|
25
25
|
evaluation must work without credentials or network access.
|
|
26
26
|
|
|
27
27
|
To configure a workflow's access to a declared page, database, data source, or
|
|
28
|
-
custom agent,
|
|
28
|
+
custom agent, use the matching helper, such as
|
|
29
|
+
`access: { guide: access.view(guide), analyst: access.call(analyst) }`.
|
|
29
30
|
The handler receives the live identity as `context.access.alias.id`; importing
|
|
30
31
|
a declaration alone does not request access. See the
|
|
31
32
|
[workflow skill](../workflow/SKILL.md#resources-created-with-the-app) for levels
|
|
@@ -34,8 +35,8 @@ rejects it before upload.
|
|
|
34
35
|
|
|
35
36
|
## Where declarations are picked up
|
|
36
37
|
|
|
37
|
-
The Apps build discovers direct `.ts` children of `src/workflows
|
|
38
|
-
`src/syncs/`, then evaluates their imports with the Notion as Code recorder active.
|
|
38
|
+
The Apps build discovers direct `.ts` children of `src/workflows/`,
|
|
39
|
+
`src/syncs/`, and `src/customBlocks/`, then evaluates their imports with the Notion as Code recorder active.
|
|
39
40
|
Pages, databases, data sources, teamspaces, and custom agents all follow this
|
|
40
41
|
same rule. Their resource type does not determine their file location.
|
|
41
42
|
|
|
@@ -50,6 +51,8 @@ src/
|
|
|
50
51
|
myWorkflow/
|
|
51
52
|
lib/
|
|
52
53
|
notion.ts Resources owned by myWorkflow
|
|
54
|
+
customBlocks/
|
|
55
|
+
board.ts May import shared resource declarations
|
|
53
56
|
```
|
|
54
57
|
|
|
55
58
|
Choose the location based on ownership, not resource type. Export an app-wide
|
|
@@ -64,8 +67,8 @@ either. Helpers under capability-specific `lib/` directories are not discovered
|
|
|
64
67
|
on their own.
|
|
65
68
|
Every direct file in a capability directory must still default-export that
|
|
66
69
|
capability, so do not put a resource-only file there. Imports reached only
|
|
67
|
-
through
|
|
68
|
-
|
|
70
|
+
through browser code do not enter the Notion as Code recording phase. An App containing
|
|
71
|
+
only unimported resource declarations is not a
|
|
69
72
|
standalone Notion as Code project and has no discovered capabilities.
|
|
70
73
|
|
|
71
74
|
## Declare resources
|
package/skills/workflow/SKILL.md
CHANGED
|
@@ -132,7 +132,7 @@ export const guide = page({
|
|
|
132
132
|
Import the handle into the workflow and declare the named binding it needs:
|
|
133
133
|
|
|
134
134
|
```ts
|
|
135
|
-
import { workflow } from "@notionhq/apps";
|
|
135
|
+
import { access, workflow } from "@notionhq/apps";
|
|
136
136
|
import { triggers } from "@notionhq/apps/triggers";
|
|
137
137
|
|
|
138
138
|
import { guide } from "./sayHello/lib/notion.js";
|
|
@@ -141,7 +141,7 @@ export default workflow({
|
|
|
141
141
|
name: "Read guide",
|
|
142
142
|
description: "Reads the App's guide on a recurring schedule.",
|
|
143
143
|
triggers: [triggers.scheduled()],
|
|
144
|
-
access: { guide:
|
|
144
|
+
access: { guide: access.view(guide) },
|
|
145
145
|
handler: async (_event, context) => {
|
|
146
146
|
await context.step("Read guide", () =>
|
|
147
147
|
context.notion.pages.retrieve({ page_id: context.access.guide.id }),
|
|
@@ -155,9 +155,9 @@ declarations in `access` work too. Importing a declaration alone provisions the
|
|
|
155
155
|
resource but does not create a `context.access` alias. An `access` entry declares
|
|
156
156
|
the needed permission level and a named binding for that NaC resource.
|
|
157
157
|
|
|
158
|
-
`access`
|
|
159
|
-
|
|
160
|
-
|
|
158
|
+
`access.view()`, `access.comment()`, `access.edit()`, and `access.fullAccess()`
|
|
159
|
+
accept page, database, and data-source handles. `access.call()` accepts custom-agent
|
|
160
|
+
handles. Keys are workflow-local aliases. The handler's
|
|
161
161
|
typed `context.access` exposes each declared alias as a readonly `{ type, id }`,
|
|
162
162
|
not the declaration handle or a permission-management API. UI-granted resources
|
|
163
163
|
are permissions only: they do not create `context.access` aliases.
|
package/src/cli/build.test.ts
CHANGED
|
@@ -194,6 +194,56 @@ describe("buildApp", () => {
|
|
|
194
194
|
expect(worker).not.toMatch(/custom_block|customBlock|react|createRoot|NotionCustomBlock/);
|
|
195
195
|
expect(fs.existsSync(path.join(root, ".notion/blocks.js"))).toBe(false);
|
|
196
196
|
});
|
|
197
|
+
|
|
198
|
+
it("records resources imported only by a custom block and emits default bindings", async () => {
|
|
199
|
+
const root = fixture("custom-block-notion-as-code");
|
|
200
|
+
const { manifest } = await buildApp(root);
|
|
201
|
+
|
|
202
|
+
expect(manifest.capabilities).toEqual([
|
|
203
|
+
{
|
|
204
|
+
type: "custom_block",
|
|
205
|
+
key: "board",
|
|
206
|
+
config: expect.objectContaining({
|
|
207
|
+
defaultBindings: {
|
|
208
|
+
tasks: {
|
|
209
|
+
dataSourceResourceId: "tasks-source",
|
|
210
|
+
propertyResourceIdsByKey: {
|
|
211
|
+
Name: "task-name",
|
|
212
|
+
Status: "task-status",
|
|
213
|
+
Due: "task-due",
|
|
214
|
+
},
|
|
215
|
+
},
|
|
216
|
+
},
|
|
217
|
+
manifest: expect.objectContaining({
|
|
218
|
+
dataSources: {
|
|
219
|
+
tasks: expect.objectContaining({
|
|
220
|
+
name: "Tasks",
|
|
221
|
+
description: "Work items shown on the board",
|
|
222
|
+
}),
|
|
223
|
+
},
|
|
224
|
+
}),
|
|
225
|
+
}),
|
|
226
|
+
},
|
|
227
|
+
]);
|
|
228
|
+
const provisioning = JSON.parse(
|
|
229
|
+
await fs.promises.readFile(path.join(root, "dist", "provisioning.json"), "utf8"),
|
|
230
|
+
);
|
|
231
|
+
expect(provisioning.intents).toEqual([
|
|
232
|
+
expect.objectContaining({
|
|
233
|
+
type: "database",
|
|
234
|
+
resourceId: "tasks-db",
|
|
235
|
+
dataSources: [
|
|
236
|
+
expect.objectContaining({
|
|
237
|
+
resourceId: "tasks-source",
|
|
238
|
+
properties: expect.arrayContaining([
|
|
239
|
+
expect.objectContaining({ resourceId: "task-status" }),
|
|
240
|
+
]),
|
|
241
|
+
}),
|
|
242
|
+
],
|
|
243
|
+
}),
|
|
244
|
+
]);
|
|
245
|
+
});
|
|
246
|
+
|
|
197
247
|
it("names the block file when path is missing", async () => {
|
|
198
248
|
await expect(buildApp(fixture("custom-block-missing-path"))).rejects.toThrow(
|
|
199
249
|
/hello.ts.*non-empty path/,
|
|
@@ -217,6 +267,11 @@ describe("buildApp", () => {
|
|
|
217
267
|
access: {
|
|
218
268
|
home: { type: "page", resourceId: "home", level: "view" },
|
|
219
269
|
records: { type: "database", resourceId: "records", level: "edit" },
|
|
270
|
+
fullRecords: {
|
|
271
|
+
type: "database",
|
|
272
|
+
resourceId: "records",
|
|
273
|
+
level: "fullAccess",
|
|
274
|
+
},
|
|
220
275
|
source: {
|
|
221
276
|
type: "dataSource",
|
|
222
277
|
resourceId: "records-source",
|
package/src/cli/build.ts
CHANGED
|
@@ -54,10 +54,7 @@ export async function buildApp(projectRoot: string): Promise<BuildResult> {
|
|
|
54
54
|
}
|
|
55
55
|
|
|
56
56
|
const blockConfigs = await buildBlockDeclarations(projectRoot, capabilities);
|
|
57
|
-
const entityProvisioning = await buildMetadata(
|
|
58
|
-
projectRoot,
|
|
59
|
-
capabilities.filter((capability) => capability.runtime === "worker"),
|
|
60
|
-
);
|
|
57
|
+
const entityProvisioning = await buildMetadata(projectRoot, capabilities);
|
|
61
58
|
const manifest = await extractManifest(
|
|
62
59
|
bundlePath,
|
|
63
60
|
capabilities,
|
package/src/cli/codegen.test.ts
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
import { describe, expect, it } from "vitest";
|
|
2
2
|
|
|
3
|
-
import { generateEntry } from "./codegen.js";
|
|
3
|
+
import { generateEntry, generateMetadataEntry } from "./codegen.js";
|
|
4
4
|
|
|
5
5
|
describe("generateEntry", () => {
|
|
6
6
|
it("statically imports and registers workflows by key", () => {
|
|
@@ -36,3 +36,19 @@ describe("generateEntry", () => {
|
|
|
36
36
|
expect(entry).toContain(`"issues": capability_0,`);
|
|
37
37
|
});
|
|
38
38
|
});
|
|
39
|
+
|
|
40
|
+
describe("generateMetadataEntry", () => {
|
|
41
|
+
it("records resources imported by custom blocks", () => {
|
|
42
|
+
const entry = generateMetadataEntry([
|
|
43
|
+
{
|
|
44
|
+
type: "custom_block",
|
|
45
|
+
tag: "custom_block",
|
|
46
|
+
runtime: "build",
|
|
47
|
+
key: "board",
|
|
48
|
+
sourcePath: "src/customBlocks/board.ts",
|
|
49
|
+
},
|
|
50
|
+
]);
|
|
51
|
+
|
|
52
|
+
expect(entry).toContain(`await import("../src/customBlocks/board.ts");`);
|
|
53
|
+
});
|
|
54
|
+
});
|
package/src/custom-block.test.ts
CHANGED
|
@@ -1,5 +1,8 @@
|
|
|
1
|
-
import { describe, expect, it } from "vitest";
|
|
2
|
-
|
|
1
|
+
import { describe, expect, expectTypeOf, it } from "vitest";
|
|
2
|
+
|
|
3
|
+
import { customBlock, type CustomBlockOptions } from "./custom-block.js";
|
|
4
|
+
import { notion, type DatabaseHandle } from "./notion-as-code/index.js";
|
|
5
|
+
import { toCustomBlockPropertyType } from "./translate.js";
|
|
3
6
|
|
|
4
7
|
describe("customBlock", () => {
|
|
5
8
|
it("supports static assets without a build command", () => {
|
|
@@ -16,4 +19,137 @@ describe("customBlock", () => {
|
|
|
16
19
|
);
|
|
17
20
|
},
|
|
18
21
|
);
|
|
22
|
+
|
|
23
|
+
it("generates mappings and default bindings from a single-source database", () => {
|
|
24
|
+
const tasks = notion.database("tasks-db", {
|
|
25
|
+
dataSourceResourceId: "tasks-source",
|
|
26
|
+
name: "Tasks",
|
|
27
|
+
description: "Work items shown on the board",
|
|
28
|
+
schema: {
|
|
29
|
+
Name: { resourceId: "task-name", type: "title" },
|
|
30
|
+
Notes: {
|
|
31
|
+
resourceId: "task-notes",
|
|
32
|
+
type: "text",
|
|
33
|
+
description: "Extra context",
|
|
34
|
+
},
|
|
35
|
+
Files: { resourceId: "task-files", type: "file" },
|
|
36
|
+
Due: { resourceId: "task-due", type: "date" },
|
|
37
|
+
},
|
|
38
|
+
});
|
|
39
|
+
|
|
40
|
+
expect(
|
|
41
|
+
customBlock({
|
|
42
|
+
path: "./blocks/board",
|
|
43
|
+
name: "Task board",
|
|
44
|
+
description: "Shows tasks by status",
|
|
45
|
+
dataSources: { tasks },
|
|
46
|
+
}).config,
|
|
47
|
+
).toEqual({
|
|
48
|
+
source: {
|
|
49
|
+
type: "project",
|
|
50
|
+
path: "./blocks/board",
|
|
51
|
+
command: "npx vite build",
|
|
52
|
+
output: "dist",
|
|
53
|
+
},
|
|
54
|
+
manifest: {
|
|
55
|
+
version: 1,
|
|
56
|
+
name: "Task board",
|
|
57
|
+
description: "Shows tasks by status",
|
|
58
|
+
dataSources: {
|
|
59
|
+
tasks: {
|
|
60
|
+
name: "Tasks",
|
|
61
|
+
description: "Work items shown on the board",
|
|
62
|
+
properties: {
|
|
63
|
+
Name: { name: "Name", type: "title" },
|
|
64
|
+
Notes: {
|
|
65
|
+
name: "Notes",
|
|
66
|
+
description: "Extra context",
|
|
67
|
+
type: "rich_text",
|
|
68
|
+
},
|
|
69
|
+
Files: { name: "Files", type: "files" },
|
|
70
|
+
Due: { name: "Due", type: "date" },
|
|
71
|
+
},
|
|
72
|
+
},
|
|
73
|
+
},
|
|
74
|
+
},
|
|
75
|
+
defaultBindings: {
|
|
76
|
+
tasks: {
|
|
77
|
+
dataSourceResourceId: "tasks-source",
|
|
78
|
+
propertyResourceIdsByKey: {
|
|
79
|
+
Name: "task-name",
|
|
80
|
+
Notes: "task-notes",
|
|
81
|
+
Files: "task-files",
|
|
82
|
+
Due: "task-due",
|
|
83
|
+
},
|
|
84
|
+
},
|
|
85
|
+
},
|
|
86
|
+
});
|
|
87
|
+
});
|
|
88
|
+
|
|
89
|
+
it("accepts a data-source handle alongside an inline mapping", () => {
|
|
90
|
+
const tasks = notion.database("tasks-db", {
|
|
91
|
+
name: "Project",
|
|
92
|
+
datasources: {
|
|
93
|
+
Tasks: {
|
|
94
|
+
resourceId: "tasks-source",
|
|
95
|
+
description: "Current tasks",
|
|
96
|
+
schema: { Name: { resourceId: "task-name", type: "title" } },
|
|
97
|
+
},
|
|
98
|
+
},
|
|
99
|
+
});
|
|
100
|
+
const block = customBlock({
|
|
101
|
+
path: "./blocks/board",
|
|
102
|
+
dataSources: {
|
|
103
|
+
tasks: tasks.datasources.Tasks,
|
|
104
|
+
people: { name: "People", properties: {} },
|
|
105
|
+
},
|
|
106
|
+
});
|
|
107
|
+
|
|
108
|
+
expect(block.config.manifest.dataSources).toEqual({
|
|
109
|
+
tasks: {
|
|
110
|
+
name: "Tasks",
|
|
111
|
+
description: "Current tasks",
|
|
112
|
+
properties: { Name: { name: "Name", type: "title" } },
|
|
113
|
+
},
|
|
114
|
+
people: { name: "People", properties: {} },
|
|
115
|
+
});
|
|
116
|
+
expect(block.config.defaultBindings).toEqual({
|
|
117
|
+
tasks: {
|
|
118
|
+
dataSourceResourceId: "tasks-source",
|
|
119
|
+
propertyResourceIdsByKey: { Name: "task-name" },
|
|
120
|
+
},
|
|
121
|
+
});
|
|
122
|
+
});
|
|
123
|
+
|
|
124
|
+
it("rejects multi-source database handles in the dataSources type", () => {
|
|
125
|
+
type DataSourceOption = NonNullable<CustomBlockOptions["dataSources"]>[string];
|
|
126
|
+
expectTypeOf<DatabaseHandle>().not.toExtend<DataSourceOption>();
|
|
127
|
+
});
|
|
128
|
+
|
|
129
|
+
it.each([
|
|
130
|
+
["title", "title"],
|
|
131
|
+
["text", "rich_text"],
|
|
132
|
+
["number", "number"],
|
|
133
|
+
["select", "select"],
|
|
134
|
+
["multi_select", "multi_select"],
|
|
135
|
+
["status", "status"],
|
|
136
|
+
["date", "date"],
|
|
137
|
+
["checkbox", "checkbox"],
|
|
138
|
+
["url", "url"],
|
|
139
|
+
["email", "email"],
|
|
140
|
+
["phone_number", "phone_number"],
|
|
141
|
+
["file", "files"],
|
|
142
|
+
["person", "people"],
|
|
143
|
+
["auto_increment_id", "unique_id"],
|
|
144
|
+
["relation", "relation"],
|
|
145
|
+
["formula", "formula"],
|
|
146
|
+
["rollup", "rollup"],
|
|
147
|
+
["created_time", "created_time"],
|
|
148
|
+
["last_edited_time", "last_edited_time"],
|
|
149
|
+
["created_by", "created_by"],
|
|
150
|
+
["last_edited_by", "last_edited_by"],
|
|
151
|
+
["verification", "verification"],
|
|
152
|
+
] as const)("maps Notion-as-Code %s properties to manifest %s properties", (input, output) => {
|
|
153
|
+
expect(toCustomBlockPropertyType(input)).toBe(output);
|
|
154
|
+
});
|
|
19
155
|
});
|
package/src/custom-block.ts
CHANGED
|
@@ -1,9 +1,21 @@
|
|
|
1
1
|
import type { ManifestDataSource } from "@notionhq/custom-blocks";
|
|
2
2
|
|
|
3
|
+
import type { DataSourceHandle, SingleSourceDatabaseHandle } from "./notion-as-code/database.js";
|
|
4
|
+
import { toCustomBlockDefaultBinding, toCustomBlockDataSource } from "./translate.js";
|
|
5
|
+
|
|
6
|
+
export type CustomBlockDefaultBinding = {
|
|
7
|
+
dataSourceResourceId: string;
|
|
8
|
+
propertyResourceIdsByKey: Record<string, string>;
|
|
9
|
+
};
|
|
10
|
+
|
|
11
|
+
type CustomBlockDataSource = ManifestDataSource | DataSourceHandle | SingleSourceDatabaseHandle;
|
|
12
|
+
|
|
3
13
|
export type CustomBlockOptions = {
|
|
4
14
|
path: string;
|
|
15
|
+
name?: string;
|
|
16
|
+
description?: string;
|
|
5
17
|
slashCommand?: string;
|
|
6
|
-
dataSources?: Record<string,
|
|
18
|
+
dataSources?: Record<string, CustomBlockDataSource>;
|
|
7
19
|
} & (
|
|
8
20
|
| { static: true; command?: never; output?: never }
|
|
9
21
|
| { static?: false; command?: string; output?: string }
|
|
@@ -20,6 +32,7 @@ export function customBlock(options: CustomBlockOptions) {
|
|
|
20
32
|
"Custom block slashCommand must be 1–64 lowercase letters, numbers, hyphens or underscores, starting with a letter or number",
|
|
21
33
|
);
|
|
22
34
|
}
|
|
35
|
+
const { dataSources, defaultBindings } = lowerDataSources(options.dataSources ?? {});
|
|
23
36
|
return {
|
|
24
37
|
_tag: "custom_block" as const,
|
|
25
38
|
config: {
|
|
@@ -33,9 +46,43 @@ export function customBlock(options: CustomBlockOptions) {
|
|
|
33
46
|
},
|
|
34
47
|
manifest: {
|
|
35
48
|
version: 1 as const,
|
|
49
|
+
...(options.name === undefined ? {} : { name: options.name }),
|
|
50
|
+
...(options.description === undefined ? {} : { description: options.description }),
|
|
36
51
|
...(slashCommand === undefined ? {} : { slashCommand }),
|
|
37
|
-
dataSources
|
|
52
|
+
dataSources,
|
|
38
53
|
},
|
|
54
|
+
...(Object.keys(defaultBindings).length === 0 ? {} : { defaultBindings }),
|
|
39
55
|
},
|
|
40
56
|
};
|
|
41
57
|
}
|
|
58
|
+
|
|
59
|
+
function lowerDataSources(dataSources: Record<string, CustomBlockDataSource>) {
|
|
60
|
+
const manifestDataSources: Record<string, ManifestDataSource> = {};
|
|
61
|
+
const defaultBindings: Record<string, CustomBlockDefaultBinding> = {};
|
|
62
|
+
|
|
63
|
+
for (const [key, dataSource] of Object.entries(dataSources)) {
|
|
64
|
+
const handle = isSingleSourceDatabaseHandle(dataSource)
|
|
65
|
+
? dataSource.dataSource
|
|
66
|
+
: dataSource;
|
|
67
|
+
if (isDataSourceHandle(handle)) {
|
|
68
|
+
manifestDataSources[key] = toCustomBlockDataSource(handle);
|
|
69
|
+
defaultBindings[key] = toCustomBlockDefaultBinding(handle);
|
|
70
|
+
} else {
|
|
71
|
+
manifestDataSources[key] = handle;
|
|
72
|
+
}
|
|
73
|
+
}
|
|
74
|
+
|
|
75
|
+
return { dataSources: manifestDataSources, defaultBindings };
|
|
76
|
+
}
|
|
77
|
+
|
|
78
|
+
function isSingleSourceDatabaseHandle(
|
|
79
|
+
value: CustomBlockDataSource,
|
|
80
|
+
): value is SingleSourceDatabaseHandle {
|
|
81
|
+
return "dataSource" in value;
|
|
82
|
+
}
|
|
83
|
+
|
|
84
|
+
function isDataSourceHandle(
|
|
85
|
+
value: ManifestDataSource | DataSourceHandle,
|
|
86
|
+
): value is DataSourceHandle {
|
|
87
|
+
return "resourceId" in value && "schema" in value && "database" in value;
|
|
88
|
+
}
|
package/src/index.ts
CHANGED
|
@@ -75,6 +75,7 @@ describe("Notion-as-Code database declarations", () => {
|
|
|
75
75
|
database.dataSource.schema.Title.resourceId,
|
|
76
76
|
).toEqualTypeOf<"single-title-id">();
|
|
77
77
|
expect(database.dataSource.schema).toEqual(schema);
|
|
78
|
+
expect(database.dataSource.name).toBe("Tasks");
|
|
78
79
|
|
|
79
80
|
const declaration = sync({
|
|
80
81
|
dataSource: database.dataSource,
|
|
@@ -636,6 +637,7 @@ describe("Notion-as-Code database declarations", () => {
|
|
|
636
637
|
notion.database("metadata-database", {
|
|
637
638
|
dataSourceResourceId: "metadata-source",
|
|
638
639
|
name: "Metadata",
|
|
640
|
+
description: "Metadata source",
|
|
639
641
|
schema: {
|
|
640
642
|
Name: { type: "title", resourceId: "metadata-name" },
|
|
641
643
|
},
|
|
@@ -658,11 +660,13 @@ describe("Notion-as-Code database declarations", () => {
|
|
|
658
660
|
type: "database",
|
|
659
661
|
resourceId: "metadata-database",
|
|
660
662
|
name: "Metadata",
|
|
663
|
+
description: "Metadata source",
|
|
661
664
|
parent: { type: "resourceId", resourceId: APPS_WORKSPACE_RESOURCE_ID },
|
|
662
665
|
dataSources: [
|
|
663
666
|
{
|
|
664
667
|
resourceId: "metadata-source",
|
|
665
668
|
name: "Metadata",
|
|
669
|
+
description: "Metadata source",
|
|
666
670
|
properties: [{ type: "title", resourceId: "metadata-name", name: "Name" }],
|
|
667
671
|
},
|
|
668
672
|
],
|
|
@@ -680,7 +684,7 @@ describe("Notion-as-Code database declarations", () => {
|
|
|
680
684
|
expect(serialized).not.toHaveProperty("schema");
|
|
681
685
|
expect(serialized).not.toHaveProperty("dataSourceResourceId");
|
|
682
686
|
expect(serialized).not.toHaveProperty("datasources");
|
|
683
|
-
expect(serialized).
|
|
687
|
+
expect(serialized.description).toBe("Metadata source");
|
|
684
688
|
});
|
|
685
689
|
|
|
686
690
|
it("requires the status property configuration in authoring types", () => {
|
|
@@ -25,6 +25,7 @@ type NonEmptyViewSchemas = readonly [ViewSchema, ...ViewSchema[]];
|
|
|
25
25
|
export type DataSourceArgs<Schema extends NotionAsCodeSchema = NotionAsCodeSchema> = {
|
|
26
26
|
resourceId: ResourceId;
|
|
27
27
|
name?: never;
|
|
28
|
+
description?: string;
|
|
28
29
|
schema: Schema;
|
|
29
30
|
icon?: NotionAsCodeIcon;
|
|
30
31
|
pageLayout?: DatabasePageLayout<Schema>;
|
|
@@ -120,6 +121,8 @@ export type ChildDatabaseArgs<
|
|
|
120
121
|
export type DataSourceHandle<Schema extends NotionAsCodeSchema = NotionAsCodeSchema> = {
|
|
121
122
|
readonly resourceType: "dataSource";
|
|
122
123
|
readonly resourceId: string;
|
|
124
|
+
readonly name: string;
|
|
125
|
+
readonly description?: string;
|
|
123
126
|
readonly schema: Schema;
|
|
124
127
|
readonly database: Database<AppsSchemaForSchema<Schema>>;
|
|
125
128
|
addPage: (args: ChildPageArgs) => PageHandle;
|
|
@@ -259,6 +262,7 @@ function buildDatabaseHandle(
|
|
|
259
262
|
const source: DataSourceDefinition = {
|
|
260
263
|
resourceId: args.dataSourceResourceId,
|
|
261
264
|
name: args.name,
|
|
265
|
+
...(args.description === undefined ? {} : { description: args.description }),
|
|
262
266
|
properties: normalizeProperties(args.schema),
|
|
263
267
|
...(args.pageLayout === undefined ? {} : { pageLayout: args.pageLayout }),
|
|
264
268
|
};
|
|
@@ -400,7 +404,7 @@ function createDatabase(
|
|
|
400
404
|
);
|
|
401
405
|
}
|
|
402
406
|
dataSourceIds.add(definition.resourceId);
|
|
403
|
-
handles[key] = createDataSourceHandle(definition, schema);
|
|
407
|
+
handles[key] = createDataSourceHandle(definition, schema, intent.description);
|
|
404
408
|
}
|
|
405
409
|
|
|
406
410
|
recordIntent({ ...intent, type: "database" });
|
|
@@ -419,6 +423,7 @@ function createDatabase(
|
|
|
419
423
|
function createDataSourceHandle(
|
|
420
424
|
dataSource: DataSourceDefinition,
|
|
421
425
|
schema: NotionAsCodeSchema,
|
|
426
|
+
databaseDescription: string | undefined,
|
|
422
427
|
): DataSourceHandle {
|
|
423
428
|
const propertyResourceIds = new Set<string>();
|
|
424
429
|
const propertyNames = new Set<string>();
|
|
@@ -437,10 +442,13 @@ function createDataSourceHandle(
|
|
|
437
442
|
propertyResourceIds.add(property.resourceId);
|
|
438
443
|
propertyNames.add(property.name);
|
|
439
444
|
}
|
|
445
|
+
const description = dataSource.description ?? databaseDescription;
|
|
440
446
|
|
|
441
447
|
return {
|
|
442
448
|
resourceType: "dataSource",
|
|
443
449
|
resourceId: dataSource.resourceId,
|
|
450
|
+
name: dataSource.name,
|
|
451
|
+
...(description === undefined ? {} : { description }),
|
|
444
452
|
schema,
|
|
445
453
|
database: createAppsDatabase(dataSource.resourceId, dataSource),
|
|
446
454
|
addPage(pageArgs) {
|
package/src/translate.ts
ADDED
|
@@ -0,0 +1,77 @@
|
|
|
1
|
+
import type {
|
|
2
|
+
ManifestDataSource,
|
|
3
|
+
ManifestProperty,
|
|
4
|
+
NotionPropertyType,
|
|
5
|
+
} from "@notionhq/custom-blocks";
|
|
6
|
+
|
|
7
|
+
import type { CustomBlockDefaultBinding } from "./custom-block.js";
|
|
8
|
+
import type { DataSourceHandle } from "./notion-as-code/database.js";
|
|
9
|
+
import type { NotionAsCodeProperty } from "./notion-as-code/intents.js";
|
|
10
|
+
|
|
11
|
+
/** Lower a Notion-as-Code handle into the declaration consumed by custom block hosts. */
|
|
12
|
+
export function toCustomBlockDataSource(handle: DataSourceHandle): ManifestDataSource {
|
|
13
|
+
return {
|
|
14
|
+
name: handle.name,
|
|
15
|
+
...(handle.description === undefined ? {} : { description: handle.description }),
|
|
16
|
+
properties: Object.fromEntries(
|
|
17
|
+
Object.entries(handle.schema).map(([key, property]) => [
|
|
18
|
+
key,
|
|
19
|
+
{
|
|
20
|
+
name: key,
|
|
21
|
+
...(typeof property.description !== "string"
|
|
22
|
+
? {}
|
|
23
|
+
: { description: property.description }),
|
|
24
|
+
type: toCustomBlockPropertyType(property.type),
|
|
25
|
+
} satisfies ManifestProperty,
|
|
26
|
+
]),
|
|
27
|
+
),
|
|
28
|
+
};
|
|
29
|
+
}
|
|
30
|
+
|
|
31
|
+
/** Keep resource IDs in the build artifact so deploy can resolve workspace-local IDs later. */
|
|
32
|
+
export function toCustomBlockDefaultBinding(handle: DataSourceHandle): CustomBlockDefaultBinding {
|
|
33
|
+
return {
|
|
34
|
+
dataSourceResourceId: handle.resourceId,
|
|
35
|
+
propertyResourceIdsByKey: Object.fromEntries(
|
|
36
|
+
Object.entries(handle.schema).map(([key, property]) => [key, property.resourceId]),
|
|
37
|
+
),
|
|
38
|
+
};
|
|
39
|
+
}
|
|
40
|
+
|
|
41
|
+
export function toCustomBlockPropertyType(type: NotionAsCodeProperty["type"]): NotionPropertyType {
|
|
42
|
+
switch (type) {
|
|
43
|
+
case "text":
|
|
44
|
+
return "rich_text";
|
|
45
|
+
case "file":
|
|
46
|
+
return "files";
|
|
47
|
+
case "person":
|
|
48
|
+
return "people";
|
|
49
|
+
case "auto_increment_id":
|
|
50
|
+
return "unique_id";
|
|
51
|
+
case "title":
|
|
52
|
+
case "number":
|
|
53
|
+
case "select":
|
|
54
|
+
case "multi_select":
|
|
55
|
+
case "status":
|
|
56
|
+
case "date":
|
|
57
|
+
case "checkbox":
|
|
58
|
+
case "url":
|
|
59
|
+
case "email":
|
|
60
|
+
case "phone_number":
|
|
61
|
+
case "relation":
|
|
62
|
+
case "formula":
|
|
63
|
+
case "rollup":
|
|
64
|
+
case "created_time":
|
|
65
|
+
case "last_edited_time":
|
|
66
|
+
case "created_by":
|
|
67
|
+
case "last_edited_by":
|
|
68
|
+
case "verification":
|
|
69
|
+
return type;
|
|
70
|
+
default:
|
|
71
|
+
return assertNever(type);
|
|
72
|
+
}
|
|
73
|
+
}
|
|
74
|
+
|
|
75
|
+
function assertNever(value: never): never {
|
|
76
|
+
throw new Error(`Unsupported Notion-as-Code property type: ${String(value)}`);
|
|
77
|
+
}
|
|
@@ -4,7 +4,7 @@ import { customAgent } from "./notion-as-code/custom-agent.js";
|
|
|
4
4
|
import { database } from "./notion-as-code/database.js";
|
|
5
5
|
import { page } from "./notion-as-code/page.js";
|
|
6
6
|
import { triggers } from "./triggers.generated.js";
|
|
7
|
-
import { workflow } from "./workflow.js";
|
|
7
|
+
import { access, workflow } from "./workflow.js";
|
|
8
8
|
|
|
9
9
|
it("infers access aliases and rejects resource-specific levels", () => {
|
|
10
10
|
const home = page({ resourceId: "home" });
|
|
@@ -28,15 +28,19 @@ it("infers access aliases and rejects resource-specific levels", () => {
|
|
|
28
28
|
description: "Checks access types",
|
|
29
29
|
triggers: [triggers.notionPageCreated()],
|
|
30
30
|
access: {
|
|
31
|
-
home:
|
|
32
|
-
singleDatabase:
|
|
33
|
-
multiDatabase:
|
|
31
|
+
home: access.view(home),
|
|
32
|
+
singleDatabase: access.edit(singleDatabase),
|
|
33
|
+
multiDatabase: access.fullAccess(multiDatabase),
|
|
34
|
+
source: access.comment(singleDatabase.dataSource),
|
|
35
|
+
agent: access.call(agent),
|
|
34
36
|
},
|
|
35
37
|
handler: (_event, context) => {
|
|
36
38
|
expectTypeOf(context.access.home.type).toEqualTypeOf<"page">();
|
|
37
39
|
expectTypeOf(context.access.home.id).toEqualTypeOf<string>();
|
|
38
40
|
expectTypeOf(context.access.singleDatabase.type).toEqualTypeOf<"database">();
|
|
39
41
|
expectTypeOf(context.access.multiDatabase.type).toEqualTypeOf<"database">();
|
|
42
|
+
expectTypeOf(context.access.source.type).toEqualTypeOf<"dataSource">();
|
|
43
|
+
expectTypeOf(context.access.agent.type).toEqualTypeOf<"customAgent">();
|
|
40
44
|
// @ts-expect-error Access aliases are limited to declarations.
|
|
41
45
|
void context.access.missing;
|
|
42
46
|
// @ts-expect-error Access values are readonly.
|