@notionhq/apps 0.0.30 → 0.0.32
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/dist/cli/codegen.d.ts.map +1 -1
- package/dist/cli/codegen.js +6 -1
- package/dist/custom-block.d.ts +9 -2
- package/dist/custom-block.d.ts.map +1 -1
- package/dist/custom-block.js +3 -0
- package/dist/index.d.ts +2 -2
- package/dist/index.d.ts.map +1 -1
- package/dist/notion-as-code/custom-agent.d.ts +6 -5
- package/dist/notion-as-code/custom-agent.d.ts.map +1 -1
- package/dist/notion-as-code/custom-agent.js +15 -4
- package/dist/notion-as-code/database.d.ts +11 -0
- package/dist/notion-as-code/database.d.ts.map +1 -1
- package/dist/notion-as-code/database.js +61 -2
- package/dist/notion-as-code/index.d.ts +3 -3
- package/dist/notion-as-code/index.d.ts.map +1 -1
- package/dist/notion-as-code/intents.d.ts +7 -1
- package/dist/notion-as-code/intents.d.ts.map +1 -1
- package/dist/notion-as-code/provisioning.d.ts.map +1 -1
- package/dist/notion-as-code/provisioning.js +25 -0
- package/dist/notion-as-code/provisioning.test.d.ts +2 -0
- package/dist/notion-as-code/provisioning.test.d.ts.map +1 -0
- package/dist/notion-as-code/recorder.d.ts +6 -0
- package/dist/notion-as-code/recorder.d.ts.map +1 -1
- package/dist/notion-as-code/recorder.js +22 -1
- package/dist/notion-as-code/views.d.ts +11 -2
- package/dist/notion-as-code/views.d.ts.map +1 -1
- package/dist/workflow-access.d.ts +8 -3
- package/dist/workflow-access.d.ts.map +1 -1
- package/dist/workflow-access.js +33 -34
- package/dist/workflow.d.ts +1 -1
- package/dist/workflow.d.ts.map +1 -1
- package/package.json +1 -1
- package/skills/notion-as-code/SKILL.md +39 -8
- package/src/cli/build.test.ts +11 -0
- package/src/cli/codegen.test.ts +26 -1
- package/src/cli/codegen.ts +10 -2
- package/src/custom-block.test.ts +14 -0
- package/src/custom-block.ts +10 -0
- package/src/index.ts +2 -2
- package/src/notion-as-code/custom-agent.test.ts +62 -3
- package/src/notion-as-code/custom-agent.ts +19 -10
- package/src/notion-as-code/database.test.ts +105 -1
- package/src/notion-as-code/database.ts +77 -1
- package/src/notion-as-code/index.ts +2 -1
- package/src/notion-as-code/intents.ts +4 -1
- package/src/notion-as-code/notion-as-code-types.test.ts +21 -11
- package/src/notion-as-code/provisioning.test.ts +69 -0
- package/src/notion-as-code/provisioning.ts +27 -0
- package/src/notion-as-code/recorder.ts +27 -0
- package/src/notion-as-code/views-types.test.ts +24 -0
- package/src/notion-as-code/views.ts +24 -2
- package/src/workflow-access.ts +56 -51
- package/src/workflow.ts +1 -1
|
@@ -1,13 +1,117 @@
|
|
|
1
1
|
import { describe, expect, expectTypeOf, it } from "vitest";
|
|
2
2
|
|
|
3
3
|
import { Builder } from "../builder.js";
|
|
4
|
+
import { customBlock } from "../custom-block.js";
|
|
4
5
|
import { ExecutionError } from "../error.js";
|
|
5
6
|
import { sync } from "../sync.js";
|
|
6
7
|
import { notion, type DataSourceHandle, type SingleSourceDatabaseHandle } from "./index.js";
|
|
7
8
|
import { APPS_WORKSPACE_RESOURCE_ID } from "./intents.js";
|
|
8
|
-
import {
|
|
9
|
+
import {
|
|
10
|
+
finishMetadataRecording,
|
|
11
|
+
startMetadataRecording,
|
|
12
|
+
withMetadataCapabilityKey,
|
|
13
|
+
} from "./recorder.js";
|
|
9
14
|
|
|
10
15
|
describe("Notion-as-Code database declarations", () => {
|
|
16
|
+
it("infers a custom view data source from the block's default bindings", async () => {
|
|
17
|
+
const recorder = startMetadataRecording();
|
|
18
|
+
const tasks = notion.database("tasks-db", {
|
|
19
|
+
dataSourceResourceId: "tasks-source",
|
|
20
|
+
name: "Tasks",
|
|
21
|
+
schema: { Name: { resourceId: "task-name", type: "title" } },
|
|
22
|
+
});
|
|
23
|
+
|
|
24
|
+
await withMetadataCapabilityKey("board", async () => {
|
|
25
|
+
const board = customBlock({
|
|
26
|
+
path: "./blocks/board",
|
|
27
|
+
dataSources: { tasks },
|
|
28
|
+
});
|
|
29
|
+
tasks.addCustomView({
|
|
30
|
+
resourceId: "tasks-board-view",
|
|
31
|
+
name: "Board",
|
|
32
|
+
customBlock: board,
|
|
33
|
+
});
|
|
34
|
+
});
|
|
35
|
+
|
|
36
|
+
expect(finishMetadataRecording(recorder).intents.at(-1)).toEqual({
|
|
37
|
+
type: "view",
|
|
38
|
+
databaseResourceId: "tasks-db",
|
|
39
|
+
view: {
|
|
40
|
+
type: "custom",
|
|
41
|
+
resourceId: "tasks-board-view",
|
|
42
|
+
name: "Board",
|
|
43
|
+
dataSourceResourceId: "tasks-source",
|
|
44
|
+
customBlockKey: "board",
|
|
45
|
+
},
|
|
46
|
+
});
|
|
47
|
+
});
|
|
48
|
+
|
|
49
|
+
it("requires an explicit bound source when a block binds multiple database sources", async () => {
|
|
50
|
+
const recorder = startMetadataRecording();
|
|
51
|
+
const tasks = notion.database("tasks-db", {
|
|
52
|
+
name: "Tasks",
|
|
53
|
+
datasources: {
|
|
54
|
+
Active: {
|
|
55
|
+
resourceId: "active-source",
|
|
56
|
+
schema: { Name: { resourceId: "active-name", type: "title" } },
|
|
57
|
+
},
|
|
58
|
+
Archive: {
|
|
59
|
+
resourceId: "archive-source",
|
|
60
|
+
schema: { Name: { resourceId: "archive-name", type: "title" } },
|
|
61
|
+
},
|
|
62
|
+
},
|
|
63
|
+
});
|
|
64
|
+
const projects = notion.database("projects-db", {
|
|
65
|
+
dataSourceResourceId: "projects-source",
|
|
66
|
+
name: "Projects",
|
|
67
|
+
schema: { Name: { resourceId: "project-name", type: "title" } },
|
|
68
|
+
});
|
|
69
|
+
|
|
70
|
+
await withMetadataCapabilityKey("board", async () => {
|
|
71
|
+
const board = customBlock({
|
|
72
|
+
path: "./blocks/board",
|
|
73
|
+
dataSources: {
|
|
74
|
+
active: tasks.datasources.Active,
|
|
75
|
+
archive: tasks.datasources.Archive,
|
|
76
|
+
},
|
|
77
|
+
});
|
|
78
|
+
expect(() =>
|
|
79
|
+
tasks.addCustomView({
|
|
80
|
+
resourceId: "ambiguous-board-view",
|
|
81
|
+
customBlock: board,
|
|
82
|
+
}),
|
|
83
|
+
).toThrow("must bind exactly one data source");
|
|
84
|
+
tasks.addCustomView({
|
|
85
|
+
resourceId: "active-board-view",
|
|
86
|
+
customBlock: board,
|
|
87
|
+
dataSource: tasks.datasources.Active,
|
|
88
|
+
});
|
|
89
|
+
expect(() =>
|
|
90
|
+
tasks.addCustomView({
|
|
91
|
+
resourceId: "foreign-board-view",
|
|
92
|
+
customBlock: board,
|
|
93
|
+
dataSource: projects.dataSource,
|
|
94
|
+
}),
|
|
95
|
+
).toThrow('dataSource must belong to database "tasks-db"');
|
|
96
|
+
const activeOnlyBoard = customBlock({
|
|
97
|
+
path: "./blocks/active-board",
|
|
98
|
+
dataSources: { active: tasks.datasources.Active },
|
|
99
|
+
});
|
|
100
|
+
expect(() =>
|
|
101
|
+
tasks.addCustomView({
|
|
102
|
+
resourceId: "unbound-board-view",
|
|
103
|
+
customBlock: activeOnlyBoard,
|
|
104
|
+
dataSource: tasks.datasources.Archive,
|
|
105
|
+
}),
|
|
106
|
+
).toThrow('dataSource must be bound by custom block "board"');
|
|
107
|
+
});
|
|
108
|
+
|
|
109
|
+
expect(finishMetadataRecording(recorder).intents.at(-1)).toEqual(
|
|
110
|
+
expect.objectContaining({
|
|
111
|
+
view: expect.objectContaining({ dataSourceResourceId: "active-source" }),
|
|
112
|
+
}),
|
|
113
|
+
);
|
|
114
|
+
});
|
|
11
115
|
it("records a single-source page layout on the data source", () => {
|
|
12
116
|
const recorder = startMetadataRecording();
|
|
13
117
|
|
|
@@ -1,4 +1,5 @@
|
|
|
1
1
|
import type { Database } from "../database.js";
|
|
2
|
+
import type { CustomBlockHandle } from "../custom-block.js";
|
|
2
3
|
import {
|
|
3
4
|
APPS_WORKSPACE_RESOURCE_ID,
|
|
4
5
|
type DatabaseIntent,
|
|
@@ -11,7 +12,7 @@ import {
|
|
|
11
12
|
type ResourceId,
|
|
12
13
|
} from "./intents.js";
|
|
13
14
|
import { createPage, type ChildPageArgs, type PageHandle } from "./page.js";
|
|
14
|
-
import { recordIntent } from "./recorder.js";
|
|
15
|
+
import { isMetadataRecording, recordIntent } from "./recorder.js";
|
|
15
16
|
import { assertParent, assertUserResourceId } from "./resource.js";
|
|
16
17
|
import { createAppsDatabase, type AppsSchemaForSchema } from "./schema.js";
|
|
17
18
|
import type { ViewSchema } from "./views.js";
|
|
@@ -132,6 +133,15 @@ type DatabaseHandleBase = {
|
|
|
132
133
|
readonly resourceType: "database";
|
|
133
134
|
readonly resourceId: string;
|
|
134
135
|
addView: (view: ViewSchema) => void;
|
|
136
|
+
addCustomView: (view: AddCustomViewArgs) => void;
|
|
137
|
+
};
|
|
138
|
+
|
|
139
|
+
export type AddCustomViewArgs = {
|
|
140
|
+
resourceId: ResourceId;
|
|
141
|
+
name?: string;
|
|
142
|
+
icon?: Extract<NotionAsCodeIcon, { type: "notion_icon" }>;
|
|
143
|
+
customBlock: CustomBlockHandle;
|
|
144
|
+
dataSource?: DataSourceHandle;
|
|
135
145
|
};
|
|
136
146
|
|
|
137
147
|
/** Map each authoring source key to a handle backed by that source's own schema. */
|
|
@@ -278,6 +288,7 @@ function buildDatabaseHandle(
|
|
|
278
288
|
resourceId: databaseResourceId,
|
|
279
289
|
dataSource,
|
|
280
290
|
addView: handle.addView,
|
|
291
|
+
addCustomView: handle.addCustomView,
|
|
281
292
|
};
|
|
282
293
|
}
|
|
283
294
|
|
|
@@ -416,9 +427,74 @@ function createDatabase(
|
|
|
416
427
|
addView(view) {
|
|
417
428
|
recordIntent({ type: "view", databaseResourceId: intent.resourceId, view });
|
|
418
429
|
},
|
|
430
|
+
addCustomView(view) {
|
|
431
|
+
recordCustomView(intent.resourceId, view, handles);
|
|
432
|
+
},
|
|
419
433
|
};
|
|
420
434
|
}
|
|
421
435
|
|
|
436
|
+
function recordCustomView(
|
|
437
|
+
databaseResourceId: ResourceId,
|
|
438
|
+
view: AddCustomViewArgs,
|
|
439
|
+
handles: Readonly<Record<string, DataSourceHandle>>,
|
|
440
|
+
): void {
|
|
441
|
+
assertUserResourceId(view.resourceId);
|
|
442
|
+
const customBlockKey = view.customBlock.key;
|
|
443
|
+
if (typeof customBlockKey !== "string" || customBlockKey.trim() === "") {
|
|
444
|
+
if (!isMetadataRecording()) {
|
|
445
|
+
return;
|
|
446
|
+
}
|
|
447
|
+
throw new Error(
|
|
448
|
+
"addCustomView must be called from the custom block capability file so its key is available during build",
|
|
449
|
+
);
|
|
450
|
+
}
|
|
451
|
+
|
|
452
|
+
const dataSources = Object.values(handles);
|
|
453
|
+
const boundDataSourceIds = new Set(
|
|
454
|
+
Object.values(view.customBlock.config.defaultBindings ?? {}).map(
|
|
455
|
+
(binding) => binding.dataSourceResourceId,
|
|
456
|
+
),
|
|
457
|
+
);
|
|
458
|
+
let dataSourceResourceId: string;
|
|
459
|
+
if (view.dataSource !== undefined) {
|
|
460
|
+
if (!dataSources.includes(view.dataSource)) {
|
|
461
|
+
throw new Error(
|
|
462
|
+
`Custom view "${view.resourceId}" dataSource must belong to database "${databaseResourceId}"`,
|
|
463
|
+
);
|
|
464
|
+
}
|
|
465
|
+
if (!boundDataSourceIds.has(view.dataSource.resourceId)) {
|
|
466
|
+
throw new Error(
|
|
467
|
+
`Custom view "${view.resourceId}" dataSource must be bound by custom block "${customBlockKey}"`,
|
|
468
|
+
);
|
|
469
|
+
}
|
|
470
|
+
dataSourceResourceId = view.dataSource.resourceId;
|
|
471
|
+
} else {
|
|
472
|
+
const matchingSources = dataSources.filter((source) =>
|
|
473
|
+
boundDataSourceIds.has(source.resourceId),
|
|
474
|
+
);
|
|
475
|
+
const matchingSource = matchingSources[0];
|
|
476
|
+
if (matchingSources.length !== 1 || matchingSource === undefined) {
|
|
477
|
+
throw new Error(
|
|
478
|
+
`Custom view "${view.resourceId}" must bind exactly one data source from database "${databaseResourceId}" or specify dataSource`,
|
|
479
|
+
);
|
|
480
|
+
}
|
|
481
|
+
dataSourceResourceId = matchingSource.resourceId;
|
|
482
|
+
}
|
|
483
|
+
|
|
484
|
+
recordIntent({
|
|
485
|
+
type: "view",
|
|
486
|
+
databaseResourceId,
|
|
487
|
+
view: {
|
|
488
|
+
type: "custom",
|
|
489
|
+
resourceId: view.resourceId,
|
|
490
|
+
...(view.name === undefined ? {} : { name: view.name }),
|
|
491
|
+
...(view.icon === undefined ? {} : { icon: view.icon }),
|
|
492
|
+
dataSourceResourceId,
|
|
493
|
+
customBlockKey,
|
|
494
|
+
},
|
|
495
|
+
});
|
|
496
|
+
}
|
|
497
|
+
|
|
422
498
|
/** Create a data-source handle backed by the Apps database used by syncs. */
|
|
423
499
|
function createDataSourceHandle(
|
|
424
500
|
dataSource: DataSourceDefinition,
|
|
@@ -34,7 +34,6 @@ export type {
|
|
|
34
34
|
} from "./intents.js";
|
|
35
35
|
export type {
|
|
36
36
|
CustomAgentAccess,
|
|
37
|
-
CustomAgentAccessRole,
|
|
38
37
|
CustomAgentArgs,
|
|
39
38
|
CustomAgentHandle,
|
|
40
39
|
CustomAgentPageAddedTrigger,
|
|
@@ -56,6 +55,7 @@ export type {
|
|
|
56
55
|
YearlyRecurrenceSchedule,
|
|
57
56
|
} from "./custom-agent.js";
|
|
58
57
|
export type {
|
|
58
|
+
AddCustomViewArgs,
|
|
59
59
|
ChildDatabaseArgs,
|
|
60
60
|
ChildDatabaseFactory,
|
|
61
61
|
ChildMultiSourceDatabaseArgs,
|
|
@@ -93,6 +93,7 @@ export type {
|
|
|
93
93
|
CoverAspectFormat,
|
|
94
94
|
CoverFormat,
|
|
95
95
|
CoverSizeFormat,
|
|
96
|
+
CustomViewSchema,
|
|
96
97
|
DatabaseViewSortDirection,
|
|
97
98
|
DateFilterMode,
|
|
98
99
|
DateGroupByFormat,
|
|
@@ -1,3 +1,4 @@
|
|
|
1
|
+
import type { ResourceAccessLevel } from "../workflow-access.js";
|
|
1
2
|
import type { CustomAgentArgs } from "./custom-agent.js";
|
|
2
3
|
import type { PageArgs } from "./page.js";
|
|
3
4
|
import type { TeamspaceArgs } from "./teamspace.js";
|
|
@@ -346,7 +347,9 @@ export type DatabaseIntent = {
|
|
|
346
347
|
};
|
|
347
348
|
|
|
348
349
|
export type PageIntent = PageArgs & { parent: Parent };
|
|
349
|
-
export type CustomAgentIntent = CustomAgentArgs
|
|
350
|
+
export type CustomAgentIntent = Omit<CustomAgentArgs, "access"> & {
|
|
351
|
+
access?: readonly { resourceId: ResourceId; level: ResourceAccessLevel }[];
|
|
352
|
+
};
|
|
350
353
|
|
|
351
354
|
export type ViewIntent = {
|
|
352
355
|
databaseResourceId: ResourceId;
|
|
@@ -1,8 +1,9 @@
|
|
|
1
1
|
import { expectTypeOf, it } from "vitest";
|
|
2
|
+
import { access } from "../workflow-access.js";
|
|
3
|
+
import { page, database, customAgent } from "../index.js";
|
|
2
4
|
|
|
3
5
|
import type {
|
|
4
6
|
CustomAgentAccess,
|
|
5
|
-
CustomAgentAccessRole,
|
|
6
7
|
CustomAgentArgs,
|
|
7
8
|
CustomAgentPropertyUpdatedTrigger,
|
|
8
9
|
CustomAgentTrustedUrls,
|
|
@@ -16,17 +17,26 @@ import type {
|
|
|
16
17
|
} from "./index.js";
|
|
17
18
|
|
|
18
19
|
it("types custom agent resource access and URL trust", () => {
|
|
19
|
-
|
|
20
|
-
|
|
21
|
-
|
|
22
|
-
|
|
23
|
-
resourceId: "
|
|
24
|
-
|
|
25
|
-
|
|
26
|
-
expectTypeOf<
|
|
27
|
-
|
|
28
|
-
|
|
20
|
+
const home = page({ resourceId: "home" });
|
|
21
|
+
const tasks = database("tasks", {
|
|
22
|
+
dataSourceResourceId: "source",
|
|
23
|
+
name: "Tasks",
|
|
24
|
+
schema: { Title: { resourceId: "title", type: "title" } },
|
|
25
|
+
});
|
|
26
|
+
expectTypeOf(access.view(home)).toExtend<CustomAgentAccess>();
|
|
27
|
+
expectTypeOf(access.edit(tasks)).toExtend<CustomAgentAccess>();
|
|
28
|
+
expectTypeOf(access.view(tasks.dataSource)).not.toExtend<CustomAgentAccess>();
|
|
29
|
+
expectTypeOf(access.comment(tasks.dataSource)).not.toExtend<CustomAgentAccess>();
|
|
30
|
+
expectTypeOf(access.edit(tasks.dataSource)).not.toExtend<CustomAgentAccess>();
|
|
31
|
+
expectTypeOf(access.fullAccess(tasks.dataSource)).not.toExtend<CustomAgentAccess>();
|
|
32
|
+
expectTypeOf<{
|
|
33
|
+
resource: typeof tasks.dataSource;
|
|
34
|
+
level: "view";
|
|
29
35
|
}>().not.toExtend<CustomAgentAccess>();
|
|
36
|
+
expectTypeOf<{ resourceId: "tasks"; role: "can_view" }>().not.toExtend<CustomAgentAccess>();
|
|
37
|
+
expectTypeOf(
|
|
38
|
+
access.call(customAgent({ resourceId: "agent", name: "Agent" })),
|
|
39
|
+
).not.toExtend<CustomAgentAccess>();
|
|
30
40
|
expectTypeOf<{ type: "all" }>().toEqualTypeOf<CustomAgentTrustedUrls>();
|
|
31
41
|
expectTypeOf<CustomAgentArgs["webAccess"]>().toEqualTypeOf<boolean | undefined>();
|
|
32
42
|
expectTypeOf<CustomAgentArgs["trustedUrls"]>().toEqualTypeOf<
|
|
@@ -0,0 +1,69 @@
|
|
|
1
|
+
import { describe, expect, it } from "vitest";
|
|
2
|
+
|
|
3
|
+
import { notion } from "./index.js";
|
|
4
|
+
import { createProvisioningArtifact } from "./provisioning.js";
|
|
5
|
+
import { finishMetadataRecording, startMetadataRecording } from "./recorder.js";
|
|
6
|
+
import type { CustomViewSchema } from "./views.js";
|
|
7
|
+
|
|
8
|
+
const customView: CustomViewSchema = {
|
|
9
|
+
resourceId: "tasks-board-view",
|
|
10
|
+
dataSourceResourceId: "tasks-source",
|
|
11
|
+
type: "custom",
|
|
12
|
+
customBlockKey: "board",
|
|
13
|
+
};
|
|
14
|
+
const databaseArgs = {
|
|
15
|
+
name: "Tasks",
|
|
16
|
+
dataSourceResourceId: "tasks-source",
|
|
17
|
+
schema: { Name: { resourceId: "task-name", type: "title" } },
|
|
18
|
+
} as const;
|
|
19
|
+
|
|
20
|
+
describe.each([
|
|
21
|
+
{
|
|
22
|
+
name: "standalone views",
|
|
23
|
+
declare: (view: CustomViewSchema) =>
|
|
24
|
+
notion.view({ ...view, databaseResourceId: "tasks-db" }),
|
|
25
|
+
},
|
|
26
|
+
{
|
|
27
|
+
name: "database views",
|
|
28
|
+
declare: (view: CustomViewSchema) =>
|
|
29
|
+
notion.database("tasks-db", { ...databaseArgs, views: [view] }),
|
|
30
|
+
},
|
|
31
|
+
{
|
|
32
|
+
name: "addView",
|
|
33
|
+
declare: (view: CustomViewSchema) =>
|
|
34
|
+
notion.database("tasks-db", databaseArgs).addView(view),
|
|
35
|
+
},
|
|
36
|
+
])("provisioning $name", ({ declare }) => {
|
|
37
|
+
it.each([
|
|
38
|
+
["sorts", []],
|
|
39
|
+
["filters", []],
|
|
40
|
+
["properties", []],
|
|
41
|
+
["showPageIcon", true],
|
|
42
|
+
["defaultTemplate", "template"],
|
|
43
|
+
] as const)("rejects %s on custom views when creating the artifact", (field, value) => {
|
|
44
|
+
const recorder = startMetadataRecording();
|
|
45
|
+
// Extra fields can arrive from JavaScript callers despite the TypeScript contract.
|
|
46
|
+
declare({ ...customView, [field]: value });
|
|
47
|
+
const { intents } = finishMetadataRecording(recorder);
|
|
48
|
+
|
|
49
|
+
expect(() => createProvisioningArtifact(intents)).toThrow(
|
|
50
|
+
`Custom views do not support ${field}`,
|
|
51
|
+
);
|
|
52
|
+
});
|
|
53
|
+
|
|
54
|
+
it("rejects a blank custom block key when creating the artifact", () => {
|
|
55
|
+
const recorder = startMetadataRecording();
|
|
56
|
+
declare({ ...customView, customBlockKey: " " });
|
|
57
|
+
const { intents } = finishMetadataRecording(recorder);
|
|
58
|
+
|
|
59
|
+
expect(() => createProvisioningArtifact(intents)).toThrow("non-empty customBlockKey");
|
|
60
|
+
});
|
|
61
|
+
|
|
62
|
+
it("preserves the custom block key for server-side capability resolution", () => {
|
|
63
|
+
const recorder = startMetadataRecording();
|
|
64
|
+
declare(customView);
|
|
65
|
+
const { intents } = finishMetadataRecording(recorder);
|
|
66
|
+
|
|
67
|
+
expect(createProvisioningArtifact(intents).intents).toEqual(intents);
|
|
68
|
+
});
|
|
69
|
+
});
|
|
@@ -1,4 +1,5 @@
|
|
|
1
1
|
import type { InfraAsCodeIntent } from "./intents.js";
|
|
2
|
+
import type { ViewSchema } from "./views.js";
|
|
2
3
|
|
|
3
4
|
export const APPS_PROVISIONING_SCHEMA = "notion:apps-provisioning:v1";
|
|
4
5
|
|
|
@@ -24,6 +25,13 @@ export function createProvisioningArtifact(
|
|
|
24
25
|
): ProvisioningArtifact {
|
|
25
26
|
const provisioningIntents: ProvisioningIntent[] = [];
|
|
26
27
|
for (const intent of intents) {
|
|
28
|
+
if (intent.type === "view") {
|
|
29
|
+
validateCustomView(intent.view);
|
|
30
|
+
} else if (intent.type === "database") {
|
|
31
|
+
for (const view of intent.views ?? []) {
|
|
32
|
+
validateCustomView(view);
|
|
33
|
+
}
|
|
34
|
+
}
|
|
27
35
|
if (intent.type === "teamspace") {
|
|
28
36
|
const { parent: _parent, ...teamspace } = intent;
|
|
29
37
|
provisioningIntents.push(teamspace);
|
|
@@ -48,3 +56,22 @@ export function isRecordedProvisioning(value: unknown): value is RecordedProvisi
|
|
|
48
56
|
Array.isArray(value.intents)
|
|
49
57
|
);
|
|
50
58
|
}
|
|
59
|
+
|
|
60
|
+
function validateCustomView(view: ViewSchema): void {
|
|
61
|
+
if (view.type === "custom") {
|
|
62
|
+
if (typeof view.customBlockKey !== "string" || view.customBlockKey.trim() === "") {
|
|
63
|
+
throw new Error("Custom views require a non-empty customBlockKey");
|
|
64
|
+
}
|
|
65
|
+
for (const field of [
|
|
66
|
+
"sorts",
|
|
67
|
+
"filters",
|
|
68
|
+
"properties",
|
|
69
|
+
"showPageIcon",
|
|
70
|
+
"defaultTemplate",
|
|
71
|
+
] as const) {
|
|
72
|
+
if (field in view) {
|
|
73
|
+
throw new Error(`Custom views do not support ${field}`);
|
|
74
|
+
}
|
|
75
|
+
}
|
|
76
|
+
}
|
|
77
|
+
}
|
|
@@ -3,11 +3,13 @@ import type { InfraAsCodeIntent } from "./intents.js";
|
|
|
3
3
|
import type { RecordedProvisioning } from "./provisioning.js";
|
|
4
4
|
|
|
5
5
|
let activeRecorder: RecordedProvisioning | undefined;
|
|
6
|
+
let currentCapabilityKey: string | undefined;
|
|
6
7
|
|
|
7
8
|
/** Start collecting declarations from the metadata bundle. */
|
|
8
9
|
export function startMetadataRecording(): RecordedProvisioning {
|
|
9
10
|
const recorder: RecordedProvisioning = { intents: [] };
|
|
10
11
|
activeRecorder = recorder;
|
|
12
|
+
currentCapabilityKey = undefined;
|
|
11
13
|
return recorder;
|
|
12
14
|
}
|
|
13
15
|
|
|
@@ -17,6 +19,7 @@ export function finishMetadataRecording(recorder: RecordedProvisioning): Recorde
|
|
|
17
19
|
throw new Error("Notion-as-Code metadata recorder was replaced before build completed");
|
|
18
20
|
}
|
|
19
21
|
activeRecorder = undefined;
|
|
22
|
+
currentCapabilityKey = undefined;
|
|
20
23
|
return recorder;
|
|
21
24
|
}
|
|
22
25
|
|
|
@@ -24,3 +27,27 @@ export function finishMetadataRecording(recorder: RecordedProvisioning): Recorde
|
|
|
24
27
|
export function recordIntent(intent: InfraAsCodeIntent): void {
|
|
25
28
|
activeRecorder?.intents.push(intent);
|
|
26
29
|
}
|
|
30
|
+
|
|
31
|
+
/** Evaluate one discovered capability with its build-time identity available to declarations. */
|
|
32
|
+
export async function withMetadataCapabilityKey<T>(
|
|
33
|
+
capabilityKey: string | undefined,
|
|
34
|
+
evaluate: () => Promise<T>,
|
|
35
|
+
): Promise<T> {
|
|
36
|
+
const previousCapabilityKey = currentCapabilityKey;
|
|
37
|
+
currentCapabilityKey = capabilityKey;
|
|
38
|
+
try {
|
|
39
|
+
return await evaluate();
|
|
40
|
+
} finally {
|
|
41
|
+
currentCapabilityKey = previousCapabilityKey;
|
|
42
|
+
}
|
|
43
|
+
}
|
|
44
|
+
|
|
45
|
+
/** Return the capability key only while a metadata bundle is recording. */
|
|
46
|
+
export function getCurrentMetadataCapabilityKey(): string | undefined {
|
|
47
|
+
return activeRecorder === undefined ? undefined : currentCapabilityKey;
|
|
48
|
+
}
|
|
49
|
+
|
|
50
|
+
/** Whether declarations are currently being collected by the build metadata bundle. */
|
|
51
|
+
export function isMetadataRecording(): boolean {
|
|
52
|
+
return activeRecorder !== undefined;
|
|
53
|
+
}
|
|
@@ -1,6 +1,7 @@
|
|
|
1
1
|
import { expectTypeOf, it } from "vitest";
|
|
2
2
|
|
|
3
3
|
import type {
|
|
4
|
+
CustomViewSchema,
|
|
4
5
|
DatabaseArgs,
|
|
5
6
|
DatabaseHandle,
|
|
6
7
|
DatePropertyFilter,
|
|
@@ -8,6 +9,29 @@ import type {
|
|
|
8
9
|
ViewSchema,
|
|
9
10
|
} from "./index.js";
|
|
10
11
|
|
|
12
|
+
it("keeps custom views free of standard view query and property configuration", () => {
|
|
13
|
+
expectTypeOf<{
|
|
14
|
+
resourceId: "custom-view";
|
|
15
|
+
dataSourceResourceId: "source";
|
|
16
|
+
type: "custom";
|
|
17
|
+
customBlockKey: "board";
|
|
18
|
+
}>().toExtend<CustomViewSchema>();
|
|
19
|
+
expectTypeOf<{
|
|
20
|
+
resourceId: "custom-view";
|
|
21
|
+
dataSourceResourceId: "source";
|
|
22
|
+
type: "custom";
|
|
23
|
+
customBlockKey: "board";
|
|
24
|
+
sorts: [];
|
|
25
|
+
}>().not.toExtend<ViewSchema>();
|
|
26
|
+
expectTypeOf<{
|
|
27
|
+
resourceId: "custom-view";
|
|
28
|
+
dataSourceResourceId: "source";
|
|
29
|
+
type: "custom";
|
|
30
|
+
customBlockKey: "board";
|
|
31
|
+
properties: [];
|
|
32
|
+
}>().not.toExtend<ViewSchema>();
|
|
33
|
+
});
|
|
34
|
+
|
|
11
35
|
it("requires date properties for calendar and timeline views at both entrypoints", () => {
|
|
12
36
|
type MissingDateProperty = {
|
|
13
37
|
resourceId: "view";
|
|
@@ -1,7 +1,15 @@
|
|
|
1
1
|
import type { NotionAsCodeIcon, NotionAsCodeProperty, ResourceId } from "./intents.js";
|
|
2
2
|
|
|
3
3
|
// Ported from notion-next/src/server/helpers/infraAsCode/types.ts.
|
|
4
|
-
export type ViewType =
|
|
4
|
+
export type ViewType =
|
|
5
|
+
| "table"
|
|
6
|
+
| "board"
|
|
7
|
+
| "calendar"
|
|
8
|
+
| "list"
|
|
9
|
+
| "gallery"
|
|
10
|
+
| "feed"
|
|
11
|
+
| "timeline"
|
|
12
|
+
| "custom";
|
|
5
13
|
|
|
6
14
|
export type PropertyVisibility = "show" | "hide" | "hide_if_empty";
|
|
7
15
|
|
|
@@ -563,6 +571,19 @@ export type TimelineViewSchema = BaseViewSchema & {
|
|
|
563
571
|
showTable?: boolean;
|
|
564
572
|
};
|
|
565
573
|
|
|
574
|
+
export type CustomViewSchema = Omit<
|
|
575
|
+
BaseViewSchema,
|
|
576
|
+
"type" | "sorts" | "filters" | "showPageIcon" | "defaultTemplate"
|
|
577
|
+
> & {
|
|
578
|
+
type: "custom";
|
|
579
|
+
customBlockKey: string;
|
|
580
|
+
sorts?: never;
|
|
581
|
+
filters?: never;
|
|
582
|
+
properties?: never;
|
|
583
|
+
showPageIcon?: never;
|
|
584
|
+
defaultTemplate?: never;
|
|
585
|
+
};
|
|
586
|
+
|
|
566
587
|
export type ViewSchema =
|
|
567
588
|
| TableViewSchema
|
|
568
589
|
| BoardViewSchema
|
|
@@ -570,4 +591,5 @@ export type ViewSchema =
|
|
|
570
591
|
| ListViewSchema
|
|
571
592
|
| GalleryViewSchema
|
|
572
593
|
| FeedViewSchema
|
|
573
|
-
| TimelineViewSchema
|
|
594
|
+
| TimelineViewSchema
|
|
595
|
+
| CustomViewSchema;
|