@notionhq/apps 0.0.18 → 0.0.20
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 +22 -1
- 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 +1 -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 +2 -0
- package/dist/notion-as-code/database.d.ts.map +1 -1
- package/dist/notion-as-code/database.js +3 -0
- package/dist/notion-as-code/date.d.ts +3 -2
- package/dist/notion-as-code/date.d.ts.map +1 -1
- package/dist/notion-as-code/handles.d.ts +8 -11
- package/dist/notion-as-code/handles.d.ts.map +1 -1
- package/dist/notion-as-code/handles.js +8 -7
- package/dist/notion-as-code/index.d.ts +8 -11
- package/dist/notion-as-code/index.d.ts.map +1 -1
- package/dist/notion-as-code/page.d.ts +50 -1
- 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/page.test.d.ts +2 -0
- package/dist/notion-as-code/page.test.d.ts.map +1 -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.d.ts +17 -6
- package/dist/workflow.d.ts.map +1 -1
- package/dist/workflow.js +8 -1
- package/docs/BUILD.md +38 -4
- package/package.json +1 -1
- package/skills/notion-as-code/SKILL.md +16 -3
- package/skills/workflow/SKILL.md +53 -16
- package/src/cli/build.test.ts +24 -0
- package/src/cli/build.ts +13 -8
- package/src/cli/emit-manifest.ts +107 -0
- package/src/index.ts +20 -1
- package/src/notion-as-code/custom-agent.ts +2 -1
- package/src/notion-as-code/database.ts +5 -0
- package/src/notion-as-code/date.ts +4 -2
- package/src/notion-as-code/handles.ts +16 -12
- package/src/notion-as-code/index.ts +10 -1
- package/src/notion-as-code/page.test.ts +35 -0
- package/src/notion-as-code/page.ts +62 -2
- package/src/workflow-access-types.test.ts +69 -0
- package/src/workflow-access.ts +299 -0
- package/src/workflow.test.ts +112 -0
- package/src/workflow.ts +50 -7
package/src/cli/emit-manifest.ts
CHANGED
|
@@ -217,6 +217,7 @@ export async function extractManifest(
|
|
|
217
217
|
bundlePath: string,
|
|
218
218
|
capabilities: readonly DiscoveredCapability[],
|
|
219
219
|
blockConfigs: Record<string, unknown> = {},
|
|
220
|
+
provisioning?: RecordedProvisioning,
|
|
220
221
|
): Promise<AppManifest> {
|
|
221
222
|
clearRegisteredPacers();
|
|
222
223
|
const bundle = await importBundle(bundlePath);
|
|
@@ -267,8 +268,12 @@ export async function extractManifest(
|
|
|
267
268
|
const { database: _database, ...rest } = config;
|
|
268
269
|
config = { ...rest, databaseKey };
|
|
269
270
|
}
|
|
271
|
+
if (capability.type === "workflow") {
|
|
272
|
+
validateWorkflowAccessReferences(config.access, provisioning, capability.sourcePath);
|
|
273
|
+
}
|
|
270
274
|
|
|
271
275
|
assertJsonSafe(config, `${capability.sourcePath} config`);
|
|
276
|
+
|
|
272
277
|
return { type: capability.type, key: capability.key, config };
|
|
273
278
|
});
|
|
274
279
|
|
|
@@ -300,6 +305,108 @@ export async function extractManifest(
|
|
|
300
305
|
return manifest as AppManifest;
|
|
301
306
|
}
|
|
302
307
|
|
|
308
|
+
const WORKFLOW_ACCESS_TYPES: Record<string, true> = {
|
|
309
|
+
page: true,
|
|
310
|
+
database: true,
|
|
311
|
+
dataSource: true,
|
|
312
|
+
customAgent: true,
|
|
313
|
+
};
|
|
314
|
+
const WORKFLOW_ACCESS_LEVELS: Record<string, true> = {
|
|
315
|
+
view: true,
|
|
316
|
+
comment: true,
|
|
317
|
+
edit: true,
|
|
318
|
+
fullAccess: true,
|
|
319
|
+
};
|
|
320
|
+
|
|
321
|
+
function validateWorkflowAccessReferences(
|
|
322
|
+
access: unknown,
|
|
323
|
+
provisioning: RecordedProvisioning | undefined,
|
|
324
|
+
capabilityPath: string,
|
|
325
|
+
): void {
|
|
326
|
+
if (access === undefined) return;
|
|
327
|
+
if (
|
|
328
|
+
typeof access !== "object" ||
|
|
329
|
+
access === null ||
|
|
330
|
+
Array.isArray(access) ||
|
|
331
|
+
(provisioning === undefined && Object.keys(access).length > 0)
|
|
332
|
+
) {
|
|
333
|
+
throw new Error(
|
|
334
|
+
`${capabilityPath}: workflow access must reference resources recorded by the current build`,
|
|
335
|
+
);
|
|
336
|
+
}
|
|
337
|
+
if (provisioning === undefined) {
|
|
338
|
+
return;
|
|
339
|
+
}
|
|
340
|
+
|
|
341
|
+
const accessObject = access as Record<string, unknown>;
|
|
342
|
+
const aliases = Object.keys(accessObject);
|
|
343
|
+
if (Reflect.ownKeys(accessObject).length !== aliases.length) {
|
|
344
|
+
throw new Error(`${capabilityPath}: workflow access aliases must be enumerable strings`);
|
|
345
|
+
}
|
|
346
|
+
for (const alias of aliases) {
|
|
347
|
+
if (
|
|
348
|
+
!/^[a-zA-Z][a-zA-Z0-9_-]{0,127}$/.test(alias) ||
|
|
349
|
+
alias === "__proto__" ||
|
|
350
|
+
alias === "prototype" ||
|
|
351
|
+
alias === "constructor"
|
|
352
|
+
) {
|
|
353
|
+
throw new Error(`${capabilityPath}: invalid workflow access alias "${alias}"`);
|
|
354
|
+
}
|
|
355
|
+
const requirement = accessObject[alias];
|
|
356
|
+
if (
|
|
357
|
+
typeof requirement !== "object" ||
|
|
358
|
+
requirement === null ||
|
|
359
|
+
Array.isArray(requirement) ||
|
|
360
|
+
Reflect.ownKeys(requirement).length !== 3 ||
|
|
361
|
+
!Object.hasOwn(requirement, "type") ||
|
|
362
|
+
!Object.hasOwn(requirement, "resourceId") ||
|
|
363
|
+
!Object.hasOwn(requirement, "level")
|
|
364
|
+
) {
|
|
365
|
+
throw new Error(`${capabilityPath}: invalid workflow access requirement "${alias}"`);
|
|
366
|
+
}
|
|
367
|
+
const requirementObject = requirement as Record<string, unknown>;
|
|
368
|
+
const type = requirementObject.type;
|
|
369
|
+
const resourceId = requirementObject.resourceId;
|
|
370
|
+
const level = requirementObject.level;
|
|
371
|
+
if (
|
|
372
|
+
typeof type !== "string" ||
|
|
373
|
+
!Object.hasOwn(WORKFLOW_ACCESS_TYPES, type) ||
|
|
374
|
+
typeof resourceId !== "string" ||
|
|
375
|
+
resourceId.length === 0 ||
|
|
376
|
+
typeof level !== "string" ||
|
|
377
|
+
(type === "customAgent"
|
|
378
|
+
? level !== "call"
|
|
379
|
+
: !Object.hasOwn(WORKFLOW_ACCESS_LEVELS, level))
|
|
380
|
+
) {
|
|
381
|
+
throw new Error(`${capabilityPath}: invalid workflow access requirement "${alias}"`);
|
|
382
|
+
}
|
|
383
|
+
|
|
384
|
+
if (
|
|
385
|
+
!provisioning.intents.some((intent) => {
|
|
386
|
+
switch (type) {
|
|
387
|
+
case "page":
|
|
388
|
+
return intent.type === "page" && intent.resourceId === resourceId;
|
|
389
|
+
case "database":
|
|
390
|
+
return intent.type === "database" && intent.resourceId === resourceId;
|
|
391
|
+
case "customAgent":
|
|
392
|
+
return intent.type === "custom_agent" && intent.resourceId === resourceId;
|
|
393
|
+
case "dataSource":
|
|
394
|
+
return (
|
|
395
|
+
intent.type === "database" &&
|
|
396
|
+
(intent.dataSources ?? []).some(
|
|
397
|
+
(dataSource) => dataSource.resourceId === resourceId,
|
|
398
|
+
)
|
|
399
|
+
);
|
|
400
|
+
}
|
|
401
|
+
})
|
|
402
|
+
) {
|
|
403
|
+
throw new Error(
|
|
404
|
+
`${capabilityPath}: workflow access "${alias}" references ${type} "${resourceId}" that is not declared by the current build`,
|
|
405
|
+
);
|
|
406
|
+
}
|
|
407
|
+
}
|
|
408
|
+
}
|
|
409
|
+
|
|
303
410
|
/** Import the metadata bundle and return the provisioning declarations it recorded. */
|
|
304
411
|
export async function extractProvisioning(bundlePath: string): Promise<RecordedProvisioning> {
|
|
305
412
|
const bundle = await importBundle(bundlePath);
|
package/src/index.ts
CHANGED
|
@@ -5,5 +5,24 @@ export { page } from "./notion-as-code/page.js";
|
|
|
5
5
|
export { teamspace } from "./notion-as-code/teamspace.js";
|
|
6
6
|
export { view } from "./notion-as-code/view.js";
|
|
7
7
|
export { sync } from "./sync.js";
|
|
8
|
-
export {
|
|
8
|
+
export {
|
|
9
|
+
workflow,
|
|
10
|
+
type Workflow,
|
|
11
|
+
type WorkflowAccess,
|
|
12
|
+
type WorkflowAccessAgentLevel,
|
|
13
|
+
type WorkflowAccessBindings,
|
|
14
|
+
type WorkflowAccessDeclaration,
|
|
15
|
+
type WorkflowAccessDeclarations,
|
|
16
|
+
type WorkflowAccessLevel,
|
|
17
|
+
type WorkflowAccessLevelFor,
|
|
18
|
+
type WorkflowAccessResource,
|
|
19
|
+
type WorkflowAccessResourceLevel,
|
|
20
|
+
type WorkflowAccessResourceType,
|
|
21
|
+
type WorkflowAccessRequirement,
|
|
22
|
+
type WorkflowAccessRequirements,
|
|
23
|
+
type WorkflowAccessValue,
|
|
24
|
+
type WorkflowConfiguration,
|
|
25
|
+
type WorkflowContext,
|
|
26
|
+
type WorkflowTriggerConfiguration,
|
|
27
|
+
} from "./workflow.js";
|
|
9
28
|
export type { WorkflowState, WorkflowStateValue } from "./workflow-state.js";
|
|
@@ -14,6 +14,7 @@ export type CustomAgentArgs = {
|
|
|
14
14
|
};
|
|
15
15
|
|
|
16
16
|
export type CustomAgentHandle = {
|
|
17
|
+
readonly resourceType: "customAgent";
|
|
17
18
|
readonly resourceId: string;
|
|
18
19
|
};
|
|
19
20
|
|
|
@@ -26,5 +27,5 @@ export function customAgent(args: CustomAgentArgs): CustomAgentHandle {
|
|
|
26
27
|
recordIntent({ type: "custom_agent", ...args } satisfies {
|
|
27
28
|
type: "custom_agent";
|
|
28
29
|
} & CustomAgentIntent);
|
|
29
|
-
return { resourceId: args.resourceId };
|
|
30
|
+
return { resourceType: "customAgent", resourceId: args.resourceId };
|
|
30
31
|
}
|
|
@@ -113,6 +113,7 @@ export type ChildDatabaseArgs<
|
|
|
113
113
|
|
|
114
114
|
/** A source handle retains its keyed authoring schema for sync inference. */
|
|
115
115
|
export type DataSourceHandle<Schema extends NotionAsCodeSchema = NotionAsCodeSchema> = {
|
|
116
|
+
readonly resourceType: "dataSource";
|
|
116
117
|
readonly resourceId: string;
|
|
117
118
|
readonly schema: Schema;
|
|
118
119
|
readonly database: Database<AppsSchemaForSchema<Schema>>;
|
|
@@ -120,6 +121,7 @@ export type DataSourceHandle<Schema extends NotionAsCodeSchema = NotionAsCodeSch
|
|
|
120
121
|
};
|
|
121
122
|
|
|
122
123
|
type DatabaseHandleBase = {
|
|
124
|
+
readonly resourceType: "database";
|
|
123
125
|
readonly resourceId: string;
|
|
124
126
|
addView: (view: ViewSchema) => void;
|
|
125
127
|
};
|
|
@@ -262,6 +264,7 @@ function buildDatabaseHandle(
|
|
|
262
264
|
throw new Error(`Database "${databaseResourceId}" is missing its data source`);
|
|
263
265
|
}
|
|
264
266
|
return {
|
|
267
|
+
resourceType: "database",
|
|
265
268
|
resourceId: databaseResourceId,
|
|
266
269
|
dataSource,
|
|
267
270
|
addView: handle.addView,
|
|
@@ -396,6 +399,7 @@ function createDatabase(
|
|
|
396
399
|
recordIntent({ ...intent, type: "database" });
|
|
397
400
|
|
|
398
401
|
return {
|
|
402
|
+
resourceType: "database",
|
|
399
403
|
resourceId: intent.resourceId,
|
|
400
404
|
datasources: handles,
|
|
401
405
|
addView(view) {
|
|
@@ -428,6 +432,7 @@ function createDataSourceHandle(
|
|
|
428
432
|
}
|
|
429
433
|
|
|
430
434
|
return {
|
|
435
|
+
resourceType: "dataSource",
|
|
431
436
|
resourceId: dataSource.resourceId,
|
|
432
437
|
schema,
|
|
433
438
|
database: createAppsDatabase(dataSource.resourceId, dataSource),
|
|
@@ -1,5 +1,7 @@
|
|
|
1
|
+
import type { TextValue } from "./page.js";
|
|
2
|
+
|
|
1
3
|
/** Serialize Notion-as-Code date values for Apps property payloads. */
|
|
2
|
-
export function date(startDate: string, endDate?: string):
|
|
4
|
+
export function date(startDate: string, endDate?: string): TextValue {
|
|
3
5
|
const start_date = parseIsoDate(startDate);
|
|
4
6
|
const end_date = endDate === undefined ? undefined : parseIsoDate(endDate);
|
|
5
7
|
if (end_date !== undefined && end_date < start_date) {
|
|
@@ -13,7 +15,7 @@ export function date(startDate: string, endDate?: string): unknown[][] {
|
|
|
13
15
|
}
|
|
14
16
|
|
|
15
17
|
/** Serialize Notion-as-Code datetime values for Apps property payloads. */
|
|
16
|
-
export function datetime(value: { start: string; end?: string; timeZone?: string }):
|
|
18
|
+
export function datetime(value: { start: string; end?: string; timeZone?: string }): TextValue {
|
|
17
19
|
const timeZone = value.timeZone ?? "UTC";
|
|
18
20
|
try {
|
|
19
21
|
new Intl.DateTimeFormat("en-US", { timeZone }).format();
|
|
@@ -1,8 +1,12 @@
|
|
|
1
1
|
import { customAgent } from "./custom-agent.js";
|
|
2
2
|
import { database } from "./database.js";
|
|
3
3
|
import { date, datetime } from "./date.js";
|
|
4
|
-
import {
|
|
5
|
-
|
|
4
|
+
import {
|
|
5
|
+
page,
|
|
6
|
+
type TextValue,
|
|
7
|
+
type VerificationInput,
|
|
8
|
+
type VerificationPropertyValue,
|
|
9
|
+
} from "./page.js";
|
|
6
10
|
import { teamspace } from "./teamspace.js";
|
|
7
11
|
import { view } from "./view.js";
|
|
8
12
|
|
|
@@ -12,22 +16,22 @@ const notionAsCodeFacade = {
|
|
|
12
16
|
page,
|
|
13
17
|
customAgent,
|
|
14
18
|
view,
|
|
15
|
-
text: (value: string) => [[value]],
|
|
16
|
-
number: (value: number) => [[String(value)]],
|
|
19
|
+
text: (value: string): TextValue => [[value]],
|
|
20
|
+
number: (value: number): TextValue => [[String(value)]],
|
|
17
21
|
date,
|
|
18
22
|
datetime,
|
|
23
|
+
verification: (value: VerificationInput): VerificationPropertyValue => ({
|
|
24
|
+
...value,
|
|
25
|
+
type: "verification",
|
|
26
|
+
}),
|
|
19
27
|
select: (value: string) => value,
|
|
20
28
|
status: (value: string) => value,
|
|
21
29
|
multiSelect: (values: readonly string[]) => values.join(","),
|
|
22
|
-
checkbox: (value: boolean) => [[value ? "Yes" : "No"]],
|
|
23
|
-
url: (value: string) => [[value, [["a", value]]]],
|
|
24
|
-
email: (value: string) => [[value, [["a", `mailto:${value}`]]]],
|
|
25
|
-
phone: (value: string) => [[value]],
|
|
30
|
+
checkbox: (value: boolean): TextValue => [[value ? "Yes" : "No"]],
|
|
31
|
+
url: (value: string): TextValue => [[value, [["a", value]]]],
|
|
32
|
+
email: (value: string): TextValue => [[value, [["a", `mailto:${value}`]]]],
|
|
33
|
+
phone: (value: string): TextValue => [[value]],
|
|
26
34
|
relation: (value: string | readonly string[]) => (Array.isArray(value) ? [...value] : value),
|
|
27
|
-
file: (resourceId: string) => {
|
|
28
|
-
assertUserResourceId(resourceId);
|
|
29
|
-
return { type: "file" as const, resourceId };
|
|
30
|
-
},
|
|
31
35
|
};
|
|
32
36
|
|
|
33
37
|
export type NotionAsCodeFacade = typeof notionAsCodeFacade;
|
|
@@ -28,7 +28,16 @@ export type {
|
|
|
28
28
|
SingleSourceDatabaseArgs,
|
|
29
29
|
SingleSourceDatabaseHandle,
|
|
30
30
|
} from "./database.js";
|
|
31
|
-
export type {
|
|
31
|
+
export type {
|
|
32
|
+
ChildPageArgs,
|
|
33
|
+
PageArgs,
|
|
34
|
+
PageHandle,
|
|
35
|
+
PropertyValue,
|
|
36
|
+
SimpleTextToken,
|
|
37
|
+
TextValue,
|
|
38
|
+
VerificationInput,
|
|
39
|
+
VerificationPropertyValue,
|
|
40
|
+
} from "./page.js";
|
|
32
41
|
export type { TeamspaceArgs, TeamspaceHandle } from "./teamspace.js";
|
|
33
42
|
export type { ViewArgs, ViewHandle } from "./view.js";
|
|
34
43
|
export type {
|
|
@@ -0,0 +1,35 @@
|
|
|
1
|
+
import { describe, expect, expectTypeOf, it } from "vitest";
|
|
2
|
+
|
|
3
|
+
import { notion, type PageArgs, type PropertyValue } from "./index.js";
|
|
4
|
+
import { finishMetadataRecording, startMetadataRecording } from "./recorder.js";
|
|
5
|
+
|
|
6
|
+
describe("Notion-as-Code page declarations", () => {
|
|
7
|
+
it("records a regular page title separately from Markdown content", () => {
|
|
8
|
+
const properties = {
|
|
9
|
+
title: notion.text("Workflow guide"),
|
|
10
|
+
} satisfies Record<string, PropertyValue | undefined>;
|
|
11
|
+
const recorder = startMetadataRecording();
|
|
12
|
+
|
|
13
|
+
notion.page({
|
|
14
|
+
resourceId: "workflow-guide",
|
|
15
|
+
properties,
|
|
16
|
+
content: "# Body heading",
|
|
17
|
+
});
|
|
18
|
+
|
|
19
|
+
expectTypeOf<PageArgs["properties"]>().toEqualTypeOf<
|
|
20
|
+
Record<string, PropertyValue | undefined> | undefined
|
|
21
|
+
>();
|
|
22
|
+
expect(finishMetadataRecording(recorder).intents).toEqual([
|
|
23
|
+
{
|
|
24
|
+
type: "page",
|
|
25
|
+
resourceId: "workflow-guide",
|
|
26
|
+
parent: {
|
|
27
|
+
type: "resourceId",
|
|
28
|
+
resourceId: "__notion_apps_workspace__",
|
|
29
|
+
},
|
|
30
|
+
properties: { title: [["Workflow guide"]] },
|
|
31
|
+
content: "# Body heading",
|
|
32
|
+
},
|
|
33
|
+
]);
|
|
34
|
+
});
|
|
35
|
+
});
|
|
@@ -8,14 +8,72 @@ import { createChildDatabase, type ChildDatabaseFactory } from "./database.js";
|
|
|
8
8
|
import { recordIntent } from "./recorder.js";
|
|
9
9
|
import { assertParent, assertUserResourceId } from "./resource.js";
|
|
10
10
|
|
|
11
|
+
/**
|
|
12
|
+
* A single text token: plain text as `[string]`, or text with annotations as
|
|
13
|
+
* `[string, ...annotations]`.
|
|
14
|
+
*/
|
|
15
|
+
export type SimpleTextToken = [string] | [string, ...unknown[]];
|
|
16
|
+
|
|
17
|
+
/**
|
|
18
|
+
* The JSON-serializable text format returned by Notion-as-Code text helpers.
|
|
19
|
+
*/
|
|
20
|
+
export type TextValue = SimpleTextToken[];
|
|
21
|
+
|
|
22
|
+
export type VerificationInput =
|
|
23
|
+
| {
|
|
24
|
+
state: "verified";
|
|
25
|
+
/** Set an expiration with `notion.datetime(...)`; omit it for indefinite verification. */
|
|
26
|
+
datetime?: TextValue;
|
|
27
|
+
}
|
|
28
|
+
| { state: "unverified" };
|
|
29
|
+
|
|
30
|
+
export type VerificationPropertyValue = VerificationInput & {
|
|
31
|
+
type: "verification";
|
|
32
|
+
};
|
|
33
|
+
|
|
34
|
+
/**
|
|
35
|
+
* A page property value accepted by Notion as Code.
|
|
36
|
+
*
|
|
37
|
+
* Use helpers from `@notionhq/apps/notion-as-code` to construct rich values
|
|
38
|
+
* such as text, dates, checkboxes, and URLs.
|
|
39
|
+
*/
|
|
40
|
+
export type PropertyValue =
|
|
41
|
+
| string
|
|
42
|
+
| number
|
|
43
|
+
| TextValue
|
|
44
|
+
| string[]
|
|
45
|
+
| VerificationPropertyValue
|
|
46
|
+
| undefined;
|
|
47
|
+
|
|
48
|
+
/**
|
|
49
|
+
* Arguments for declaring a page.
|
|
50
|
+
*
|
|
51
|
+
* For a regular page, set its title through
|
|
52
|
+
* `properties.title: notion.text("My Page Title")`. For a database page, use
|
|
53
|
+
* the title property's schema name instead of `title`.
|
|
54
|
+
*/
|
|
11
55
|
export type PageArgs = {
|
|
12
56
|
resourceId: ResourceId;
|
|
13
57
|
/** Omit to create a private top-level page in the Apps workspace. */
|
|
14
58
|
parent?: Parent;
|
|
15
59
|
// TODO: Port cover types.
|
|
16
60
|
cover?: unknown;
|
|
17
|
-
|
|
18
|
-
|
|
61
|
+
/**
|
|
62
|
+
* Page properties, including the page title.
|
|
63
|
+
*
|
|
64
|
+
* @example Regular page
|
|
65
|
+
* properties: { title: notion.text("My Page Title") }
|
|
66
|
+
*
|
|
67
|
+
* @example Database page
|
|
68
|
+
* properties: { Name: notion.text("Task Name"), Status: "In Progress" }
|
|
69
|
+
*/
|
|
70
|
+
properties?: Record<string, PropertyValue | undefined>;
|
|
71
|
+
/**
|
|
72
|
+
* Page body in Notion-flavored Markdown.
|
|
73
|
+
*
|
|
74
|
+
* Markdown headings create child blocks. They do not set the page title;
|
|
75
|
+
* set a regular page's title with `properties.title`.
|
|
76
|
+
*/
|
|
19
77
|
content?: string;
|
|
20
78
|
fullWidth?: boolean;
|
|
21
79
|
};
|
|
@@ -23,6 +81,7 @@ export type PageArgs = {
|
|
|
23
81
|
export type ChildPageArgs = Omit<PageArgs, "parent">;
|
|
24
82
|
|
|
25
83
|
export type PageHandle = {
|
|
84
|
+
readonly resourceType: "page";
|
|
26
85
|
readonly resourceId: string;
|
|
27
86
|
addDatabase: ChildDatabaseFactory;
|
|
28
87
|
addPage: (args: ChildPageArgs) => PageHandle;
|
|
@@ -50,6 +109,7 @@ export function createPage(args: ChildPageArgs | PageArgs, parent: Parent): Page
|
|
|
50
109
|
} satisfies { type: "page" } & PageIntent);
|
|
51
110
|
|
|
52
111
|
return {
|
|
112
|
+
resourceType: "page",
|
|
53
113
|
resourceId: args.resourceId,
|
|
54
114
|
addDatabase: createChildDatabase({ type: "resourceId", resourceId: args.resourceId }),
|
|
55
115
|
addPage(pageArgs) {
|
|
@@ -0,0 +1,69 @@
|
|
|
1
|
+
import { expectTypeOf, it } from "vitest";
|
|
2
|
+
|
|
3
|
+
import { customAgent } from "./notion-as-code/custom-agent.js";
|
|
4
|
+
import { database } from "./notion-as-code/database.js";
|
|
5
|
+
import { page } from "./notion-as-code/page.js";
|
|
6
|
+
import { triggers } from "./triggers.generated.js";
|
|
7
|
+
import { workflow } from "./workflow.js";
|
|
8
|
+
|
|
9
|
+
it("infers access aliases and rejects resource-specific levels", () => {
|
|
10
|
+
const home = page({ resourceId: "home" });
|
|
11
|
+
const agent = customAgent({ resourceId: "agent", name: "Agent" });
|
|
12
|
+
const singleDatabase = database("single-database", {
|
|
13
|
+
dataSourceResourceId: "single-source",
|
|
14
|
+
name: "Single",
|
|
15
|
+
schema: { Title: { resourceId: "single-title", type: "title" } },
|
|
16
|
+
});
|
|
17
|
+
const multiDatabase = database("multi-database", {
|
|
18
|
+
name: "Multi",
|
|
19
|
+
datasources: {
|
|
20
|
+
Records: {
|
|
21
|
+
resourceId: "multi-source",
|
|
22
|
+
schema: { Title: { resourceId: "multi-title", type: "title" } },
|
|
23
|
+
},
|
|
24
|
+
},
|
|
25
|
+
});
|
|
26
|
+
workflow({
|
|
27
|
+
name: "Access types",
|
|
28
|
+
description: "Checks access types",
|
|
29
|
+
triggers: [triggers.notionPageCreated()],
|
|
30
|
+
access: {
|
|
31
|
+
home: { resource: home, level: "view" },
|
|
32
|
+
singleDatabase: { resource: singleDatabase, level: "edit" },
|
|
33
|
+
multiDatabase: { resource: multiDatabase, level: "view" },
|
|
34
|
+
},
|
|
35
|
+
handler: (_event, context) => {
|
|
36
|
+
expectTypeOf(context.access.home.type).toEqualTypeOf<"page">();
|
|
37
|
+
expectTypeOf(context.access.home.id).toEqualTypeOf<string>();
|
|
38
|
+
expectTypeOf(context.access.singleDatabase.type).toEqualTypeOf<"database">();
|
|
39
|
+
expectTypeOf(context.access.multiDatabase.type).toEqualTypeOf<"database">();
|
|
40
|
+
// @ts-expect-error Access aliases are limited to declarations.
|
|
41
|
+
void context.access.missing;
|
|
42
|
+
// @ts-expect-error Access values are readonly.
|
|
43
|
+
context.access.home.id = "changed";
|
|
44
|
+
// @ts-expect-error The access map is readonly.
|
|
45
|
+
context.access = {};
|
|
46
|
+
},
|
|
47
|
+
});
|
|
48
|
+
|
|
49
|
+
const invalidAccessLevels = () => {
|
|
50
|
+
workflow({
|
|
51
|
+
name: "Agent access types",
|
|
52
|
+
description: "Checks custom agent access types",
|
|
53
|
+
triggers: [triggers.notionPageCreated()],
|
|
54
|
+
// @ts-expect-error Custom agents only support the call level.
|
|
55
|
+
access: { agent: { resource: agent, level: "view" } },
|
|
56
|
+
handler: () => {},
|
|
57
|
+
});
|
|
58
|
+
|
|
59
|
+
workflow({
|
|
60
|
+
name: "Page access types",
|
|
61
|
+
description: "Checks page access types",
|
|
62
|
+
triggers: [triggers.notionPageCreated()],
|
|
63
|
+
// @ts-expect-error Pages do not support the custom-agent call level.
|
|
64
|
+
access: { home: { resource: home, level: "call" } },
|
|
65
|
+
handler: () => {},
|
|
66
|
+
});
|
|
67
|
+
};
|
|
68
|
+
void invalidAccessLevels;
|
|
69
|
+
});
|